1/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
2/* If you are missing that file, acquire a complete release at teeworlds.com. */
3#include "character.h"
4
5#include "laser.h"
6#include "pickup.h"
7#include "projectile.h"
8
9#include <antibot/antibot_data.h>
10
11#include <base/log.h>
12
13#include <engine/antibot.h>
14#include <engine/shared/config.h>
15
16#include <generated/protocol.h>
17#include <generated/server_data.h>
18
19#include <game/mapitems.h>
20#include <game/server/gamecontext.h>
21#include <game/server/gamecontroller.h>
22#include <game/server/player.h>
23#include <game/server/score.h>
24#include <game/server/teams.h>
25#include <game/team_state.h>
26
27MACRO_ALLOC_POOL_ID_IMPL(CCharacter, MAX_CLIENTS)
28
29// Character, "physical" player's part
30CCharacter::CCharacter(CGameWorld *pWorld, CNetObj_PlayerInput LastInput) :
31 CEntity(pWorld, CGameWorld::ENTTYPE_CHARACTER, vec2(0, 0), CCharacterCore::PhysicalSize())
32{
33 m_Health = 0;
34 m_Armor = 0;
35 m_TriggeredEvents7 = 0;
36 m_StrongWeakId = 0;
37
38 m_Input = LastInput;
39 // never initialize both to zero
40 m_Input.m_TargetX = 0;
41 m_Input.m_TargetY = -1;
42
43 m_LatestPrevPrevInput = m_LatestPrevInput = m_LatestInput = m_PrevInput = m_SavedInput = m_Input;
44
45 m_LastTimeCp = -1;
46 m_LastTimeCpBroadcasted = -1;
47 for(float &CurrentTimeCp : m_aCurrentTimeCp)
48 {
49 CurrentTimeCp = 0.0f;
50 }
51}
52
53void CCharacter::Reset()
54{
55 StopRecording();
56 Destroy();
57}
58
59bool CCharacter::Spawn(CPlayer *pPlayer, vec2 Pos)
60{
61 m_EmoteStop = -1;
62 m_LastAction = -1;
63 m_LastWeapon = WEAPON_HAMMER;
64 m_QueuedWeapon = -1;
65 m_LastRefillJumps = false;
66 m_LastPenalty = false;
67 m_LastBonus = false;
68
69 m_TeleGunTeleport = false;
70 m_IsBlueTeleGunTeleport = false;
71
72 m_pPlayer = pPlayer;
73 m_Pos = Pos;
74
75 mem_zero(block: &m_LatestPrevPrevInput, size: sizeof(m_LatestPrevPrevInput));
76 m_LatestPrevPrevInput.m_TargetY = -1;
77 m_NumInputs = 0;
78 m_SpawnTick = Server()->Tick();
79 m_WeaponChangeTick = Server()->Tick();
80 Antibot()->OnSpawn(ClientId: m_pPlayer->GetCid());
81
82 m_Core.Reset();
83 m_Core.Init(pWorld: &GameServer()->m_World.m_Core, pCollision: Collision());
84 m_Core.m_ActiveWeapon = WEAPON_GUN;
85 m_Core.m_Pos = m_Pos;
86 m_Core.m_Id = m_pPlayer->GetCid();
87 int TuneZone = Collision()->IsTune(Index: Collision()->GetMapIndex(Pos));
88 m_Core.m_Tuning = TuningList()[TuneZone];
89 GameServer()->m_World.m_Core.m_apCharacters[m_pPlayer->GetCid()] = &m_Core;
90
91 m_ReckoningTick = 0;
92 m_SendCore = CCharacterCore();
93 m_ReckoningCore = CCharacterCore();
94
95 GameServer()->m_World.InsertEntity(pEntity: this);
96 m_Alive = true;
97
98 GameServer()->m_pController->OnCharacterSpawn(pChr: this);
99
100 DDRaceInit();
101
102 m_TuneZone = TuneZone;
103 m_TuneZoneOld = -1; // no zone leave msg on spawn
104 m_NeededFaketuning = 0; // reset fake tunings on respawn and send the client
105 SendZoneMsgs(); // we want a entermessage also on spawn
106 GameServer()->SendTuningParams(ClientId: m_pPlayer->GetCid(), Zone: m_TuneZone);
107
108 TrySetRescue(RescueMode: RESCUEMODE_MANUAL);
109 Server()->StartRecord(ClientId: m_pPlayer->GetCid());
110
111 int Team = GameServer()->m_aTeamMapping[m_pPlayer->GetCid()];
112
113 if(Team != -1)
114 {
115 GameServer()->m_pController->Teams().SetForceCharacterTeam(ClientId: m_pPlayer->GetCid(), Team);
116 GameServer()->m_aTeamMapping[m_pPlayer->GetCid()] = -1;
117
118 if(GameServer()->m_apSavedTeams[Team])
119 {
120 GameServer()->m_apSavedTeams[Team]->Load(pGameServer: GameServer(), Team, KeepCurrentWeakStrong: true, IgnorePlayers: true);
121 delete GameServer()->m_apSavedTeams[Team];
122 GameServer()->m_apSavedTeams[Team] = nullptr;
123 }
124
125 if(GameServer()->m_apSavedTees[m_pPlayer->GetCid()])
126 {
127 GameServer()->m_apSavedTees[m_pPlayer->GetCid()]->Load(pChr: m_pPlayer->GetCharacter(), Team);
128 delete GameServer()->m_apSavedTees[m_pPlayer->GetCid()];
129 GameServer()->m_apSavedTees[m_pPlayer->GetCid()] = nullptr;
130 }
131 }
132
133 return true;
134}
135
136void CCharacter::Destroy()
137{
138 GameServer()->m_World.m_Core.m_apCharacters[m_pPlayer->GetCid()] = nullptr;
139 m_Alive = false;
140 SetSolo(false);
141}
142
143void CCharacter::SetWeapon(int W)
144{
145 if(W == m_Core.m_ActiveWeapon)
146 return;
147
148 m_LastWeapon = m_Core.m_ActiveWeapon;
149 m_QueuedWeapon = -1;
150 m_Core.m_ActiveWeapon = W;
151 GameServer()->CreateSound(Pos: m_Pos, Sound: SOUND_WEAPON_SWITCH, Mask: TeamMask());
152
153 if(m_Core.m_ActiveWeapon < 0 || m_Core.m_ActiveWeapon >= NUM_WEAPONS)
154 m_Core.m_ActiveWeapon = 0;
155}
156
157void CCharacter::SetJetpack(bool Active)
158{
159 m_Core.m_Jetpack = Active;
160}
161
162void CCharacter::SetEndlessJump(bool Active)
163{
164 m_Core.m_EndlessJump = Active;
165}
166
167void CCharacter::SetJumps(int Jumps)
168{
169 m_Core.m_Jumps = Jumps;
170}
171
172void CCharacter::SetSolo(bool Solo)
173{
174 m_Core.m_Solo = Solo;
175 Teams()->m_Core.SetSolo(ClientId: m_pPlayer->GetCid(), Value: Solo);
176}
177
178void CCharacter::SetSuper(bool Super)
179{
180 // Disable invincible mode before activating super mode. Both modes active at the same time wouldn't necessarily break anything but it's not useful.
181 if(Super)
182 SetInvincible(false);
183
184 bool WasSuper = m_Core.m_Super;
185 m_Core.m_Super = Super;
186 if(Super && !WasSuper)
187 {
188 m_TeamBeforeSuper = Team();
189 char aError[512];
190 if(!Teams()->SetCharacterTeam(ClientId: GetPlayer()->GetCid(), Team: TEAM_SUPER, pError: aError, ErrorSize: sizeof(aError)))
191 log_error("character", "failed to set super: %s", aError);
192 m_DDRaceState = ERaceState::CHEATED;
193 }
194 else if(!Super && WasSuper)
195 {
196 Teams()->SetForceCharacterTeam(ClientId: GetPlayer()->GetCid(), Team: m_TeamBeforeSuper);
197 }
198}
199
200void CCharacter::SetInvincible(bool Invincible)
201{
202 // Disable super mode before activating invincible mode. Both modes active at the same time wouldn't necessarily break anything but it's not useful.
203 if(Invincible)
204 SetSuper(false);
205
206 m_Core.m_Invincible = Invincible;
207 if(Invincible)
208 UnFreeze();
209
210 SetEndlessJump(Invincible);
211}
212
213void CCharacter::SetCollisionDisabled(bool CollisionDisabled)
214{
215 m_Core.m_CollisionDisabled = CollisionDisabled;
216}
217
218void CCharacter::SetHookHitDisabled(bool HookHitDisabled)
219{
220 m_Core.m_HookHitDisabled = HookHitDisabled;
221}
222
223void CCharacter::SetLiveFrozen(bool Active)
224{
225 m_Core.m_LiveFrozen = Active;
226}
227
228void CCharacter::SetDeepFrozen(bool Active)
229{
230 m_Core.m_DeepFrozen = Active;
231}
232
233bool CCharacter::IsGrounded()
234{
235 if(Collision()->CheckPoint(x: m_Pos.x + GetProximityRadius() / 2, y: m_Pos.y + GetProximityRadius() / 2 + 5))
236 return true;
237 if(Collision()->CheckPoint(x: m_Pos.x - GetProximityRadius() / 2, y: m_Pos.y + GetProximityRadius() / 2 + 5))
238 return true;
239
240 int MoveRestrictionsBelow = Collision()->GetMoveRestrictions(Pos: m_Pos + vec2(0, GetProximityRadius() / 2 + 4), Distance: 0.0f);
241 return (MoveRestrictionsBelow & CANTMOVE_DOWN) != 0;
242}
243
244void CCharacter::HandleJetpack()
245{
246 vec2 Direction = normalize(v: vec2(m_LatestInput.m_TargetX, m_LatestInput.m_TargetY));
247
248 bool FullAuto = false;
249 if(m_Core.m_ActiveWeapon == WEAPON_GRENADE || m_Core.m_ActiveWeapon == WEAPON_SHOTGUN || m_Core.m_ActiveWeapon == WEAPON_LASER)
250 FullAuto = true;
251 if(m_Core.m_Jetpack && m_Core.m_ActiveWeapon == WEAPON_GUN)
252 FullAuto = true;
253
254 // check if we gonna fire
255 bool WillFire = false;
256 if(CountInput(Prev: m_LatestPrevInput.m_Fire, Cur: m_LatestInput.m_Fire).m_Presses)
257 WillFire = true;
258
259 if(FullAuto && (m_LatestInput.m_Fire & 1) && m_Core.m_aWeapons[m_Core.m_ActiveWeapon].m_Ammo)
260 WillFire = true;
261
262 if(!WillFire)
263 return;
264
265 // check for ammo
266 if(!m_Core.m_aWeapons[m_Core.m_ActiveWeapon].m_Ammo || m_FreezeTime)
267 {
268 return;
269 }
270
271 switch(m_Core.m_ActiveWeapon)
272 {
273 case WEAPON_GUN:
274 {
275 if(m_Core.m_Jetpack)
276 {
277 float Strength = GetTuning(Zone: m_TuneZone)->m_JetpackStrength;
278 TakeDamage(Force: Direction * -1.0f * (Strength / 100.0f / 6.11f), Dmg: 0, From: m_pPlayer->GetCid(), Weapon: m_Core.m_ActiveWeapon);
279 }
280 }
281 }
282}
283
284void CCharacter::HandleNinja()
285{
286 if(m_Core.m_ActiveWeapon != WEAPON_NINJA)
287 return;
288
289 if((Server()->Tick() - m_Core.m_Ninja.m_ActivationTick) > (g_pData->m_Weapons.m_Ninja.m_Duration * Server()->TickSpeed() / 1000))
290 {
291 // time's up, return
292 RemoveNinja();
293 return;
294 }
295
296 int NinjaTime = m_Core.m_Ninja.m_ActivationTick + (g_pData->m_Weapons.m_Ninja.m_Duration * Server()->TickSpeed() / 1000) - Server()->Tick();
297
298 if(NinjaTime % Server()->TickSpeed() == 0 && NinjaTime / Server()->TickSpeed() <= 5)
299 {
300 GameServer()->CreateDamageInd(Pos: m_Pos, AngleMod: 0, Amount: NinjaTime / Server()->TickSpeed(), Mask: TeamMask() & GameServer()->ClientsMaskExcludeClientVersionAndHigher(Version: VERSION_DDNET_NEW_HUD));
301 }
302
303 GameServer()->m_pController->SetArmorProgress(pCharacter: this, Progress: NinjaTime);
304
305 // force ninja Weapon
306 SetWeapon(WEAPON_NINJA);
307
308 m_Core.m_Ninja.m_CurrentMoveTime--;
309
310 if(m_Core.m_Ninja.m_CurrentMoveTime == 0)
311 {
312 // reset velocity
313 m_Core.m_Vel = m_Core.m_Ninja.m_ActivationDir * m_Core.m_Ninja.m_OldVelAmount;
314 }
315
316 if(m_Core.m_Ninja.m_CurrentMoveTime > 0)
317 {
318 // Set velocity
319 m_Core.m_Vel = m_Core.m_Ninja.m_ActivationDir * g_pData->m_Weapons.m_Ninja.m_Velocity;
320 vec2 OldPos = m_Pos;
321 vec2 GroundElasticity = vec2(
322 GetTuning(Zone: m_TuneZone)->m_GroundElasticityX,
323 GetTuning(Zone: m_TuneZone)->m_GroundElasticityY);
324
325 Collision()->MoveBox(pInoutPos: &m_Core.m_Pos, pInoutVel: &m_Core.m_Vel, Size: vec2(GetProximityRadius(), GetProximityRadius()), Elasticity: GroundElasticity);
326
327 // reset velocity so the client doesn't predict stuff
328 ResetVelocity();
329
330 // check if we Hit anything along the way
331 {
332 CEntity *apEnts[MAX_CLIENTS];
333 float Radius = GetProximityRadius() * 2.0f;
334 int Num = GameServer()->m_World.FindEntities(Pos: OldPos, Radius, ppEnts: apEnts, Max: MAX_CLIENTS, Type: CGameWorld::ENTTYPE_CHARACTER);
335
336 // check that we're not in solo part
337 if(Teams()->m_Core.GetSolo(ClientId: m_pPlayer->GetCid()))
338 return;
339
340 for(int i = 0; i < Num; ++i)
341 {
342 auto *pChr = static_cast<CCharacter *>(apEnts[i]);
343 if(pChr == this)
344 continue;
345
346 // Don't hit players in other teams
347 if(Team() != pChr->Team())
348 continue;
349
350 const int ClientId = pChr->m_pPlayer->GetCid();
351
352 // Don't hit players in solo parts
353 if(Teams()->m_Core.GetSolo(ClientId))
354 continue;
355
356 // make sure we haven't Hit this object before
357 bool AlreadyHit = false;
358 for(int j = 0; j < m_NumObjectsHit; j++)
359 {
360 if(m_aHitObjects[j] == ClientId)
361 AlreadyHit = true;
362 }
363 if(AlreadyHit)
364 continue;
365
366 // check so we are sufficiently close
367 if(distance(a: pChr->m_Pos, b: m_Pos) > Radius)
368 continue;
369
370 // Hit a player, give them damage and stuffs...
371 GameServer()->CreateSound(Pos: pChr->m_Pos, Sound: SOUND_NINJA_HIT, Mask: TeamMask());
372 // set his velocity to fast upward (for now)
373 dbg_assert(m_NumObjectsHit < MAX_CLIENTS, "m_aHitObjects overflow");
374 m_aHitObjects[m_NumObjectsHit++] = ClientId;
375
376 pChr->TakeDamage(Force: vec2(0, -10.0f), Dmg: g_pData->m_Weapons.m_Ninja.m_pBase->m_Damage, From: m_pPlayer->GetCid(), Weapon: WEAPON_NINJA);
377 }
378 }
379
380 return;
381 }
382}
383
384void CCharacter::DoWeaponSwitch()
385{
386 // make sure we can switch
387 if(m_ReloadTimer != 0 || m_QueuedWeapon == -1)
388 return;
389 if(m_Core.m_aWeapons[WEAPON_NINJA].m_Got || !m_Core.m_aWeapons[m_QueuedWeapon].m_Got)
390 return;
391
392 // switch Weapon
393 SetWeapon(m_QueuedWeapon);
394}
395
396void CCharacter::HandleWeaponSwitch()
397{
398 int WantedWeapon = m_Core.m_ActiveWeapon;
399 if(m_QueuedWeapon != -1)
400 WantedWeapon = m_QueuedWeapon;
401
402 bool Anything = false;
403 for(int i = 0; i < NUM_WEAPONS - 1; ++i)
404 if(m_Core.m_aWeapons[i].m_Got)
405 Anything = true;
406 if(!Anything)
407 return;
408 // select Weapon
409 int Next = CountInput(Prev: m_LatestPrevInput.m_NextWeapon, Cur: m_LatestInput.m_NextWeapon).m_Presses;
410 int Prev = CountInput(Prev: m_LatestPrevInput.m_PrevWeapon, Cur: m_LatestInput.m_PrevWeapon).m_Presses;
411
412 if(Next < 128) // make sure we only try sane stuff
413 {
414 while(Next) // Next Weapon selection
415 {
416 WantedWeapon = (WantedWeapon + 1) % NUM_WEAPONS;
417 if(m_Core.m_aWeapons[WantedWeapon].m_Got)
418 Next--;
419 }
420 }
421
422 if(Prev < 128) // make sure we only try sane stuff
423 {
424 while(Prev) // Prev Weapon selection
425 {
426 WantedWeapon = (WantedWeapon - 1) < 0 ? NUM_WEAPONS - 1 : WantedWeapon - 1;
427 if(m_Core.m_aWeapons[WantedWeapon].m_Got)
428 Prev--;
429 }
430 }
431
432 // Direct Weapon selection
433 if(m_LatestInput.m_WantedWeapon)
434 WantedWeapon = m_Input.m_WantedWeapon - 1;
435
436 // check for insane values
437 if(WantedWeapon >= 0 && WantedWeapon < NUM_WEAPONS && WantedWeapon != m_Core.m_ActiveWeapon && m_Core.m_aWeapons[WantedWeapon].m_Got)
438 m_QueuedWeapon = WantedWeapon;
439
440 DoWeaponSwitch();
441}
442
443void CCharacter::FireWeapon()
444{
445 if(m_ReloadTimer != 0)
446 {
447 if(m_LatestInput.m_Fire & 1)
448 {
449 Antibot()->OnHammerFireReloading(ClientId: m_pPlayer->GetCid());
450 }
451 return;
452 }
453
454 DoWeaponSwitch();
455 vec2 MouseTarget = vec2(m_LatestInput.m_TargetX, m_LatestInput.m_TargetY);
456 vec2 Direction = normalize(v: MouseTarget);
457
458 bool FullAuto = false;
459 if(m_Core.m_ActiveWeapon == WEAPON_GRENADE || m_Core.m_ActiveWeapon == WEAPON_SHOTGUN || m_Core.m_ActiveWeapon == WEAPON_LASER)
460 FullAuto = true;
461 if(m_Core.m_Jetpack && m_Core.m_ActiveWeapon == WEAPON_GUN)
462 FullAuto = true;
463 // allow firing directly after coming out of freeze or being unfrozen
464 // by something
465 if(m_FrozenLastTick)
466 FullAuto = true;
467
468 // don't fire hammer when player is deep and sv_deepfly is disabled
469 if(!g_Config.m_SvDeepfly && m_Core.m_ActiveWeapon == WEAPON_HAMMER && m_Core.m_DeepFrozen)
470 return;
471
472 // check if we gonna fire
473 bool WillFire = false;
474 if(CountInput(Prev: m_LatestPrevInput.m_Fire, Cur: m_LatestInput.m_Fire).m_Presses)
475 WillFire = true;
476
477 if(FullAuto && (m_LatestInput.m_Fire & 1) && m_Core.m_aWeapons[m_Core.m_ActiveWeapon].m_Ammo)
478 WillFire = true;
479
480 if(!WillFire)
481 return;
482
483 if(m_FreezeTime)
484 {
485 // Timer stuff to avoid shrieking orchestra caused by unfreeze-plasma
486 if(m_PainSoundTimer <= 0 && !(m_LatestPrevInput.m_Fire & 1))
487 {
488 m_PainSoundTimer = 1 * Server()->TickSpeed();
489 GameServer()->CreateSound(Pos: m_Pos, Sound: SOUND_PLAYER_PAIN_LONG, Mask: TeamMask()); // NOLINT(clang-analyzer-unix.Malloc)
490 }
491 return;
492 }
493
494 // check for ammo
495 if(!m_Core.m_aWeapons[m_Core.m_ActiveWeapon].m_Ammo)
496 return;
497
498 vec2 ProjStartPos = m_Pos + Direction * GetProximityRadius() * 0.75f;
499
500 switch(m_Core.m_ActiveWeapon)
501 {
502 case WEAPON_HAMMER:
503 {
504 GameServer()->CreateSound(Pos: m_Pos, Sound: SOUND_HAMMER_FIRE, Mask: TeamMask()); // NOLINT(clang-analyzer-unix.Malloc)
505
506 Antibot()->OnHammerFire(ClientId: m_pPlayer->GetCid());
507
508 if(m_Core.m_HammerHitDisabled)
509 break;
510
511 CEntity *apEnts[MAX_CLIENTS];
512 int Hits = 0;
513 int Num = GameServer()->m_World.FindEntities(Pos: ProjStartPos, Radius: GetProximityRadius() * 0.5f, ppEnts: apEnts,
514 Max: MAX_CLIENTS, Type: CGameWorld::ENTTYPE_CHARACTER);
515
516 for(int i = 0; i < Num; ++i)
517 {
518 auto *pTarget = static_cast<CCharacter *>(apEnts[i]);
519
520 if((pTarget == this || (pTarget->IsAlive() && !CanCollide(ClientId: pTarget->GetPlayer()->GetCid()))))
521 continue;
522
523 // set his velocity to fast upward (for now)
524 if(length(a: pTarget->m_Pos - ProjStartPos) > 0.0f)
525 GameServer()->CreateHammerHit(Pos: pTarget->m_Pos - normalize(v: pTarget->m_Pos - ProjStartPos) * GetProximityRadius() * 0.5f, Mask: TeamMask());
526 else
527 GameServer()->CreateHammerHit(Pos: ProjStartPos, Mask: TeamMask());
528
529 vec2 Dir;
530 if(length(a: pTarget->m_Pos - m_Pos) > 0.0f)
531 Dir = normalize(v: pTarget->m_Pos - m_Pos);
532 else
533 Dir = vec2(0.f, -1.f);
534
535 float Strength = GetTuning(Zone: m_TuneZone)->m_HammerStrength;
536
537 vec2 Temp = pTarget->m_Core.m_Vel + normalize(v: Dir + vec2(0.f, -1.1f)) * 10.0f;
538 Temp = ClampVel(MoveRestriction: pTarget->m_MoveRestrictions, Vel: Temp);
539 Temp -= pTarget->m_Core.m_Vel;
540 pTarget->TakeDamage(Force: (vec2(0.f, -1.0f) + Temp) * Strength, Dmg: g_pData->m_Weapons.m_Hammer.m_pBase->m_Damage,
541 From: m_pPlayer->GetCid(), Weapon: m_Core.m_ActiveWeapon);
542 pTarget->UnFreeze();
543
544 Antibot()->OnHammerHit(ClientId: m_pPlayer->GetCid(), TargetId: pTarget->GetPlayer()->GetCid());
545
546 Hits++;
547 }
548
549 // if we Hit anything, we have to wait for the reload
550 if(Hits)
551 {
552 float FireDelay = GetTuning(Zone: m_TuneZone)->m_HammerHitFireDelay;
553 m_ReloadTimer = FireDelay * Server()->TickSpeed() / 1000;
554 }
555 }
556 break;
557
558 case WEAPON_GUN:
559 {
560 if(!m_Core.m_Jetpack || !m_pPlayer->m_NinjaJetpack || m_Core.m_HasTelegunGun)
561 {
562 int Lifetime = (int)(Server()->TickSpeed() * GetTuning(Zone: m_TuneZone)->m_GunLifetime);
563
564 new CProjectile(
565 GameWorld(),
566 WEAPON_GUN, //Type
567 m_pPlayer->GetCid(), //Owner
568 ProjStartPos, //Pos
569 Direction, //Dir
570 Lifetime, //Span
571 false, //Freeze
572 false, //Explosive
573 -1, //SoundImpact
574 MouseTarget //InitDir
575 );
576
577 GameServer()->CreateSound(Pos: m_Pos, Sound: SOUND_GUN_FIRE, Mask: TeamMask()); // NOLINT(clang-analyzer-unix.Malloc)
578 }
579 }
580 break;
581
582 case WEAPON_SHOTGUN:
583 {
584 float LaserReach = GetTuning(Zone: m_TuneZone)->m_LaserReach;
585
586 new CLaser(&GameServer()->m_World, m_Pos, Direction, LaserReach, m_pPlayer->GetCid(), WEAPON_SHOTGUN);
587 GameServer()->CreateSound(Pos: m_Pos, Sound: SOUND_SHOTGUN_FIRE, Mask: TeamMask()); // NOLINT(clang-analyzer-unix.Malloc)
588 }
589 break;
590
591 case WEAPON_GRENADE:
592 {
593 int Lifetime = (int)(Server()->TickSpeed() * GetTuning(Zone: m_TuneZone)->m_GrenadeLifetime);
594
595 new CProjectile(
596 GameWorld(),
597 WEAPON_GRENADE, //Type
598 m_pPlayer->GetCid(), //Owner
599 ProjStartPos, //Pos
600 Direction, //Dir
601 Lifetime, //Span
602 false, //Freeze
603 true, //Explosive
604 SOUND_GRENADE_EXPLODE, //SoundImpact
605 MouseTarget // MouseTarget
606 );
607
608 GameServer()->CreateSound(Pos: m_Pos, Sound: SOUND_GRENADE_FIRE, Mask: TeamMask()); // NOLINT(clang-analyzer-unix.Malloc)
609 }
610 break;
611
612 case WEAPON_LASER:
613 {
614 float LaserReach = GetTuning(Zone: m_TuneZone)->m_LaserReach;
615
616 new CLaser(GameWorld(), m_Pos, Direction, LaserReach, m_pPlayer->GetCid(), WEAPON_LASER);
617 GameServer()->CreateSound(Pos: m_Pos, Sound: SOUND_LASER_FIRE, Mask: TeamMask()); // NOLINT(clang-analyzer-unix.Malloc)
618 }
619 break;
620
621 case WEAPON_NINJA:
622 {
623 // reset Hit objects
624 m_NumObjectsHit = 0;
625
626 m_Core.m_Ninja.m_ActivationDir = Direction;
627 m_Core.m_Ninja.m_CurrentMoveTime = g_pData->m_Weapons.m_Ninja.m_Movetime * Server()->TickSpeed() / 1000;
628
629 // clamp to prevent massive MoveBox calculation lag with SG bug
630 m_Core.m_Ninja.m_OldVelAmount = std::clamp(val: length(a: m_Core.m_Vel), lo: 0.0f, hi: 6000.0f);
631
632 GameServer()->CreateSound(Pos: m_Pos, Sound: SOUND_NINJA_FIRE, Mask: TeamMask()); // NOLINT(clang-analyzer-unix.Malloc)
633 }
634 break;
635 }
636
637 m_AttackTick = Server()->Tick();
638
639 if(!m_ReloadTimer)
640 {
641 float FireDelay;
642 GetTuning(Zone: m_TuneZone)->Get(offsetof(CTuningParams, m_HammerFireDelay) / sizeof(CTuneParam) + m_Core.m_ActiveWeapon, pValue: &FireDelay);
643 m_ReloadTimer = FireDelay * Server()->TickSpeed() / 1000;
644 }
645}
646
647void CCharacter::HandleWeapons()
648{
649 //ninja
650 HandleNinja();
651 HandleJetpack();
652
653 if(m_PainSoundTimer > 0)
654 m_PainSoundTimer--;
655
656 // check reload timer
657 if(m_ReloadTimer)
658 {
659 m_ReloadTimer--;
660 return;
661 }
662
663 // fire Weapon, if wanted
664 FireWeapon();
665}
666
667void CCharacter::GiveNinja()
668{
669 m_Core.m_Ninja.m_ActivationTick = Server()->Tick();
670 m_Core.m_aWeapons[WEAPON_NINJA].m_Got = true;
671 m_Core.m_aWeapons[WEAPON_NINJA].m_Ammo = -1;
672 if(m_Core.m_ActiveWeapon != WEAPON_NINJA)
673 m_LastWeapon = m_Core.m_ActiveWeapon;
674 m_Core.m_ActiveWeapon = WEAPON_NINJA;
675
676 if(!m_Core.m_aWeapons[WEAPON_NINJA].m_Got)
677 GameServer()->CreateSound(Pos: m_Pos, Sound: SOUND_PICKUP_NINJA, Mask: TeamMask());
678}
679
680void CCharacter::RemoveNinja()
681{
682 m_Core.m_Ninja.m_ActivationDir = vec2(0, 0);
683 m_Core.m_Ninja.m_ActivationTick = 0;
684 m_Core.m_Ninja.m_CurrentMoveTime = 0;
685 m_Core.m_Ninja.m_OldVelAmount = 0;
686 m_Core.m_aWeapons[WEAPON_NINJA].m_Got = false;
687 m_Core.m_aWeapons[WEAPON_NINJA].m_Ammo = 0;
688 m_Core.m_ActiveWeapon = m_LastWeapon;
689
690 SetWeapon(m_Core.m_ActiveWeapon);
691}
692
693void CCharacter::SetEmote(int Emote, int Tick)
694{
695 m_EmoteType = Emote;
696 m_EmoteStop = Tick;
697}
698
699int CCharacter::DetermineEyeEmote()
700{
701 const bool IsFrozen = m_Core.m_DeepFrozen || m_FreezeTime > 0 || m_Core.m_LiveFrozen;
702 const bool HasNinjajetpack = m_pPlayer->m_NinjaJetpack && m_Core.m_Jetpack && m_Core.m_ActiveWeapon == WEAPON_GUN;
703
704 if(GetPlayer()->IsAfk() || GetPlayer()->IsPaused())
705 return (m_Core.m_DeepFrozen || m_FreezeTime > 0) ? EMOTE_NORMAL : EMOTE_BLINK;
706 if(m_EmoteType != EMOTE_NORMAL) // user manually set an eye emote using /emote
707 return m_EmoteType;
708 if(IsFrozen)
709 return (m_Core.m_DeepFrozen || m_Core.m_LiveFrozen) ? EMOTE_PAIN : EMOTE_BLINK;
710 if(HasNinjajetpack && !m_Core.m_DeepFrozen && m_FreezeTime == 0 && !m_Core.m_HasTelegunGun)
711 return EMOTE_HAPPY;
712 if(5 * Server()->TickSpeed() - ((Server()->Tick() - m_LastAction) % (5 * Server()->TickSpeed())) < 5)
713 return EMOTE_BLINK;
714 return EMOTE_NORMAL;
715}
716
717void CCharacter::OnPredictedInput(const CNetObj_PlayerInput *pNewInput)
718{
719 // check for changes
720 if(mem_comp(a: &m_SavedInput, b: pNewInput, size: sizeof(CNetObj_PlayerInput)) != 0)
721 m_LastAction = Server()->Tick();
722
723 // copy new input
724 mem_copy(dest: &m_Input, source: pNewInput, size: sizeof(m_Input));
725
726 // it is not allowed to aim in the center
727 if(m_Input.m_TargetX == 0 && m_Input.m_TargetY == 0)
728 m_Input.m_TargetY = -1;
729
730 mem_copy(dest: &m_SavedInput, source: &m_Input, size: sizeof(m_SavedInput));
731}
732
733void CCharacter::OnDirectInput(const CNetObj_PlayerInput *pNewInput)
734{
735 mem_copy(dest: &m_LatestPrevInput, source: &m_LatestInput, size: sizeof(m_LatestInput));
736 mem_copy(dest: &m_LatestInput, source: pNewInput, size: sizeof(m_LatestInput));
737 m_NumInputs++;
738
739 // it is not allowed to aim in the center
740 if(m_LatestInput.m_TargetX == 0 && m_LatestInput.m_TargetY == 0)
741 m_LatestInput.m_TargetY = -1;
742
743 Antibot()->OnDirectInput(ClientId: m_pPlayer->GetCid());
744
745 if(m_NumInputs > 1 && m_pPlayer->GetTeam() != TEAM_SPECTATORS)
746 {
747 HandleWeaponSwitch();
748 FireWeapon();
749 }
750
751 mem_copy(dest: &m_LatestPrevPrevInput, source: &m_LatestPrevInput, size: sizeof(m_LatestInput));
752 mem_copy(dest: &m_LatestPrevInput, source: &m_LatestInput, size: sizeof(m_LatestInput));
753}
754
755void CCharacter::ReleaseHook()
756{
757 m_Core.SetHookedPlayer(-1);
758 m_Core.m_HookState = HOOK_RETRACTED;
759 m_Core.m_TriggeredEvents |= COREEVENT_HOOK_RETRACT;
760}
761
762void CCharacter::ResetHook()
763{
764 ReleaseHook();
765 m_Core.m_HookPos = m_Core.m_Pos;
766}
767
768void CCharacter::ResetInput()
769{
770 m_Input.m_Direction = 0;
771 // simulate releasing the fire button
772 if((m_Input.m_Fire & 1) != 0)
773 m_Input.m_Fire++;
774 m_Input.m_Fire &= INPUT_STATE_MASK;
775 m_Input.m_Jump = 0;
776 m_LatestPrevInput = m_LatestInput = m_Input;
777}
778
779void CCharacter::PreTick()
780{
781 if(m_StartTime > Server()->Tick())
782 {
783 // Prevent the player from getting a negative time
784 // The main reason why this can happen is because of time penalty tiles
785 // However, other reasons are hereby also excluded
786 GameServer()->SendChatTarget(To: m_pPlayer->GetCid(), pText: "You died of old age");
787 Die(Killer: m_pPlayer->GetCid(), Weapon: WEAPON_WORLD);
788 }
789
790 if(m_Paused)
791 return;
792
793 // set emote
794 if(m_EmoteStop < Server()->Tick())
795 {
796 SetEmote(Emote: m_pPlayer->GetDefaultEmote(), Tick: -1);
797 }
798
799 DDRaceTick();
800
801 Antibot()->OnCharacterTick(ClientId: m_pPlayer->GetCid());
802
803 m_Core.m_Input = m_Input;
804 m_Core.Tick(UseInput: true, DoDeferredTick: !g_Config.m_SvNoWeakHook);
805}
806
807void CCharacter::Tick()
808{
809 if(g_Config.m_SvNoWeakHook)
810 {
811 if(m_Paused)
812 return;
813
814 m_Core.TickDeferred();
815 }
816 else
817 {
818 PreTick();
819 }
820
821 if(!m_PrevInput.m_Hook && m_Input.m_Hook && !(m_Core.m_TriggeredEvents & COREEVENT_HOOK_ATTACH_PLAYER))
822 {
823 Antibot()->OnHookAttach(ClientId: m_pPlayer->GetCid(), Player: false);
824 }
825
826 // handle Weapons
827 HandleWeapons();
828
829 DDRacePostCoreTick();
830
831 if(m_Core.m_TriggeredEvents & COREEVENT_HOOK_ATTACH_PLAYER)
832 {
833 const int HookedPlayer = m_Core.HookedPlayer();
834 if(HookedPlayer != -1 && GameServer()->m_apPlayers[HookedPlayer]->GetTeam() != TEAM_SPECTATORS)
835 {
836 Antibot()->OnHookAttach(ClientId: m_pPlayer->GetCid(), Player: true);
837 }
838 }
839
840 // Previnput
841 m_PrevInput = m_Input;
842
843 m_PrevPos = m_Core.m_Pos;
844}
845
846void CCharacter::TickDeferred()
847{
848 // advance the dummy
849 {
850 CWorldCore TempWorld;
851 m_ReckoningCore.Init(pWorld: &TempWorld, pCollision: Collision(), pTeams: &Teams()->m_Core);
852 m_ReckoningCore.m_Id = m_pPlayer->GetCid();
853 m_ReckoningCore.m_Tuning = CTuningParams();
854 m_ReckoningCore.Tick(UseInput: false);
855 m_ReckoningCore.Move();
856 m_ReckoningCore.Quantize();
857 }
858
859 //lastsentcore
860 vec2 StartPos = m_Core.m_Pos;
861 vec2 StartVel = m_Core.m_Vel;
862 bool StuckBefore = Collision()->TestBox(Pos: m_Core.m_Pos, Size: CCharacterCore::PhysicalSizeVec2());
863
864 m_Core.m_Id = m_pPlayer->GetCid();
865 m_Core.Move();
866 bool StuckAfterMove = Collision()->TestBox(Pos: m_Core.m_Pos, Size: CCharacterCore::PhysicalSizeVec2());
867 m_Core.Quantize();
868 bool StuckAfterQuant = Collision()->TestBox(Pos: m_Core.m_Pos, Size: CCharacterCore::PhysicalSizeVec2());
869 m_Pos = m_Core.m_Pos;
870
871 if(!StuckBefore && (StuckAfterMove || StuckAfterQuant))
872 {
873 // Hackish solution to get rid of strict-aliasing warning
874 union
875 {
876 float f;
877 unsigned u;
878 } StartPosX, StartPosY, StartVelX, StartVelY;
879
880 StartPosX.f = StartPos.x;
881 StartPosY.f = StartPos.y;
882 StartVelX.f = StartVel.x;
883 StartVelY.f = StartVel.y;
884
885 char aBuf[256];
886 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "STUCK!!! %d %d %d %f %f %f %f %x %x %x %x",
887 StuckBefore,
888 StuckAfterMove,
889 StuckAfterQuant,
890 StartPos.x, StartPos.y,
891 StartVel.x, StartVel.y,
892 StartPosX.u, StartPosY.u,
893 StartVelX.u, StartVelY.u);
894 GameServer()->Console()->Print(Level: IConsole::OUTPUT_LEVEL_DEBUG, pFrom: "game", pStr: aBuf);
895 }
896
897 {
898 int Events = m_Core.m_TriggeredEvents;
899 int CID = m_pPlayer->GetCid();
900
901 // Some sounds are triggered client-side for the acting player (or for all players on Sixup)
902 // so we need to avoid duplicating them
903 CClientMask TeamMaskExceptSelfAndSixup = Teams()->TeamMask(Team: Team(), ExceptId: CID, Asker: CID, VersionFlags: CGameContext::FLAG_SIX);
904 // Some are triggered client-side but only on Sixup
905 CClientMask TeamMaskExceptSixup = Teams()->TeamMask(Team: Team(), ExceptId: -1, Asker: CID, VersionFlags: CGameContext::FLAG_SIX);
906
907 if(Events & COREEVENT_GROUND_JUMP)
908 GameServer()->CreateSound(Pos: m_Pos, Sound: SOUND_PLAYER_JUMP, Mask: TeamMaskExceptSelfAndSixup);
909
910 if(Events & COREEVENT_HOOK_ATTACH_PLAYER)
911 GameServer()->CreateSound(Pos: m_Pos, Sound: SOUND_HOOK_ATTACH_PLAYER, Mask: TeamMaskExceptSixup);
912
913 if(Events & COREEVENT_HOOK_ATTACH_GROUND)
914 GameServer()->CreateSound(Pos: m_Pos, Sound: SOUND_HOOK_ATTACH_GROUND, Mask: TeamMaskExceptSelfAndSixup);
915
916 if(Events & COREEVENT_HOOK_HIT_NOHOOK)
917 GameServer()->CreateSound(Pos: m_Pos, Sound: SOUND_HOOK_NOATTACH, Mask: TeamMaskExceptSelfAndSixup);
918
919 if(Events & COREEVENT_GROUND_JUMP)
920 m_TriggeredEvents7 |= protocol7::COREEVENTFLAG_GROUND_JUMP;
921 if(Events & COREEVENT_AIR_JUMP)
922 m_TriggeredEvents7 |= protocol7::COREEVENTFLAG_AIR_JUMP;
923 if(Events & COREEVENT_HOOK_ATTACH_PLAYER)
924 m_TriggeredEvents7 |= protocol7::COREEVENTFLAG_HOOK_ATTACH_PLAYER;
925 if(Events & COREEVENT_HOOK_ATTACH_GROUND)
926 m_TriggeredEvents7 |= protocol7::COREEVENTFLAG_HOOK_ATTACH_GROUND;
927 if(Events & COREEVENT_HOOK_HIT_NOHOOK)
928 m_TriggeredEvents7 |= protocol7::COREEVENTFLAG_HOOK_HIT_NOHOOK;
929 }
930
931 if(m_pPlayer->GetTeam() == TEAM_SPECTATORS)
932 {
933 m_Pos.x = m_Input.m_TargetX;
934 m_Pos.y = m_Input.m_TargetY;
935 }
936
937 // update the m_SendCore if needed
938 {
939 CNetObj_Character Predicted;
940 CNetObj_Character Current;
941 mem_zero(block: &Predicted, size: sizeof(Predicted));
942 mem_zero(block: &Current, size: sizeof(Current));
943 m_ReckoningCore.Write(pObjCore: &Predicted);
944 m_Core.Write(pObjCore: &Current);
945
946 // only allow dead reckoning for a top of 3 seconds
947 if(m_Core.m_Reset || m_ReckoningTick + Server()->TickSpeed() * 3 < Server()->Tick() || mem_comp(a: &Predicted, b: &Current, size: sizeof(CNetObj_Character)) != 0)
948 {
949 m_ReckoningTick = Server()->Tick();
950 m_SendCore = m_Core;
951 m_ReckoningCore = m_Core;
952 m_Core.m_Reset = false;
953 }
954 }
955}
956
957void CCharacter::TickPaused()
958{
959 ++m_AttackTick;
960 ++m_DamageTakenTick;
961 ++m_Core.m_Ninja.m_ActivationTick;
962 ++m_ReckoningTick;
963 if(m_LastAction != -1)
964 ++m_LastAction;
965 if(m_Core.m_aWeapons[m_Core.m_ActiveWeapon].m_AmmoRegenStart > -1)
966 ++m_Core.m_aWeapons[m_Core.m_ActiveWeapon].m_AmmoRegenStart;
967 if(m_EmoteStop > -1)
968 ++m_EmoteStop;
969}
970
971bool CCharacter::IncreaseHealth(int Amount)
972{
973 if(m_Health >= 10)
974 return false;
975 m_Health = std::clamp(val: m_Health + Amount, lo: 0, hi: 10);
976 return true;
977}
978
979bool CCharacter::IncreaseArmor(int Amount)
980{
981 if(m_Armor >= 10)
982 return false;
983 m_Armor = std::clamp(val: m_Armor + Amount, lo: 0, hi: 10);
984 return true;
985}
986
987void CCharacter::StopRecording()
988{
989 if(Server()->IsRecording(ClientId: m_pPlayer->GetCid()))
990 {
991 CPlayerData *pData = GameServer()->Score()->PlayerData(Id: m_pPlayer->GetCid());
992
993 if(pData->m_RecordStopTick - Server()->Tick() <= Server()->TickSpeed() && pData->m_RecordStopTick != -1)
994 Server()->SaveDemo(ClientId: m_pPlayer->GetCid(), Time: pData->m_RecordFinishTime);
995 else
996 Server()->StopRecord(ClientId: m_pPlayer->GetCid());
997
998 pData->m_RecordStopTick = -1;
999 }
1000}
1001
1002void CCharacter::Die(int Killer, int Weapon, bool SendKillMsg)
1003{
1004 if(Killer != WEAPON_GAME && m_SetSavePos[RESCUEMODE_AUTO])
1005 GetPlayer()->m_LastDeath = m_RescueTee[RESCUEMODE_AUTO];
1006 StopRecording();
1007 int ModeSpecial = GameServer()->m_pController->OnCharacterDeath(pVictim: this, pKiller: GameServer()->m_apPlayers[Killer], Weapon);
1008
1009 log_info("game", "kill killer='%d:%s' victim='%d:%s' weapon=%d special=%d",
1010 Killer, Server()->ClientName(Killer),
1011 m_pPlayer->GetCid(), Server()->ClientName(m_pPlayer->GetCid()), Weapon, ModeSpecial);
1012
1013 if(SendKillMsg)
1014 {
1015 SendDeathMessageIfNotInLockedTeam(Killer, Weapon, ModeSpecial);
1016 }
1017
1018 // a nice sound, and bursting tee death effect
1019 GameServer()->CreateSound(Pos: m_Pos, Sound: SOUND_PLAYER_DIE, Mask: TeamMask());
1020 GameServer()->CreateDeath(Pos: m_Pos, ClientId: m_pPlayer->GetCid(), Mask: TeamMask());
1021
1022 // this is to rate limit respawning to 3 secs
1023 m_pPlayer->m_PreviousDieTick = m_pPlayer->m_DieTick;
1024 m_pPlayer->m_DieTick = Server()->Tick();
1025
1026 m_Alive = false;
1027 SetSolo(false);
1028
1029 GameServer()->m_World.RemoveEntity(pEntity: this);
1030 GameServer()->m_World.m_Core.m_apCharacters[m_pPlayer->GetCid()] = nullptr;
1031 Teams()->OnCharacterDeath(ClientId: GetPlayer()->GetCid(), Weapon);
1032 CancelSwapRequests();
1033}
1034
1035bool CCharacter::TakeDamage(vec2 Force, int Dmg, int From, int Weapon)
1036{
1037 if(Dmg)
1038 {
1039 SetEmote(Emote: EMOTE_PAIN, Tick: Server()->Tick() + 500 * Server()->TickSpeed() / 1000);
1040 }
1041
1042 vec2 Temp = m_Core.m_Vel + Force;
1043 m_Core.m_Vel = ClampVel(MoveRestriction: m_MoveRestrictions, Vel: Temp);
1044
1045 return true;
1046}
1047
1048void CCharacter::SendDeathMessageIfNotInLockedTeam(int Killer, int Weapon, int ModeSpecial)
1049{
1050 if((Team() == TEAM_FLOCK || Teams()->TeamFlock(Team: Team()) || Teams()->Count(Team: Team()) == 1 || Teams()->GetTeamState(Team: Team()) == ETeamState::OPEN || !Teams()->TeamLocked(Team: Team())))
1051 {
1052 CNetMsg_Sv_KillMsg Msg;
1053 Msg.m_Killer = Killer;
1054 Msg.m_Victim = m_pPlayer->GetCid();
1055 Msg.m_Weapon = Weapon;
1056 Msg.m_ModeSpecial = ModeSpecial;
1057 Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL, ClientId: -1);
1058 }
1059}
1060
1061void CCharacter::CancelSwapRequests()
1062{
1063 for(auto &pPlayer : GameServer()->m_apPlayers)
1064 {
1065 if(pPlayer && pPlayer->m_SwapTargetsClientId == GetPlayer()->GetCid())
1066 pPlayer->m_SwapTargetsClientId = -1;
1067 }
1068 GetPlayer()->m_SwapTargetsClientId = -1;
1069}
1070
1071void CCharacter::SnapCharacter(int SnappingClient, int Id)
1072{
1073 int SnappingClientVersion = GameServer()->GetClientVersion(ClientId: SnappingClient);
1074 CCharacterCore *pCore;
1075 int Weapon = m_Core.m_ActiveWeapon, AmmoCount = 0,
1076 Health = 0, Armor = 0;
1077 int Emote = DetermineEyeEmote();
1078 int Tick;
1079 if(!m_ReckoningTick || GameServer()->m_World.m_Paused)
1080 {
1081 Tick = 0;
1082 pCore = &m_Core;
1083 }
1084 else
1085 {
1086 Tick = m_ReckoningTick;
1087 pCore = &m_SendCore;
1088 }
1089
1090 // use ninja graphic for old clients if player is frozen
1091 if(m_Core.m_DeepFrozen || m_FreezeTime > 0 || m_Core.m_LiveFrozen)
1092 {
1093 if((m_Core.m_DeepFrozen || m_FreezeTime > 0) && SnappingClientVersion < VERSION_DDNET_NEW_HUD)
1094 Weapon = WEAPON_NINJA;
1095 }
1096
1097 // solo, collision, jetpack and ninjajetpack prediction
1098 if(m_pPlayer->GetCid() == SnappingClient)
1099 {
1100 int Faketuning = 0;
1101 if(m_pPlayer->GetClientVersion() < VERSION_DDNET_NEW_HUD)
1102 {
1103 if(m_Core.m_Jetpack && Weapon != WEAPON_NINJA)
1104 Faketuning |= FAKETUNE_JETPACK;
1105 if(m_Core.m_Solo)
1106 Faketuning |= FAKETUNE_SOLO;
1107 if(m_Core.m_HammerHitDisabled)
1108 Faketuning |= FAKETUNE_NOHAMMER;
1109 if(m_Core.m_CollisionDisabled)
1110 Faketuning |= FAKETUNE_NOCOLL;
1111 if(m_Core.m_HookHitDisabled)
1112 Faketuning |= FAKETUNE_NOHOOK;
1113 if(!m_Core.m_EndlessJump && m_Core.m_Jumps == 0)
1114 Faketuning |= FAKETUNE_NOJUMP;
1115 }
1116 if(Faketuning != m_NeededFaketuning)
1117 {
1118 m_NeededFaketuning = Faketuning;
1119 GameServer()->SendTuningParams(ClientId: m_pPlayer->GetCid(), Zone: m_TuneZone); // update tunings
1120 }
1121 }
1122
1123 // use ninja graphic and set ammo count if player has ninjajetpack
1124 if(m_pPlayer->m_NinjaJetpack && m_Core.m_Jetpack && m_Core.m_ActiveWeapon == WEAPON_GUN && !m_Core.m_DeepFrozen && m_FreezeTime == 0 && !m_Core.m_HasTelegunGun)
1125 {
1126 Weapon = WEAPON_NINJA;
1127 AmmoCount = 10;
1128 }
1129
1130 if(m_pPlayer->GetCid() == SnappingClient || SnappingClient == SERVER_DEMO_CLIENT ||
1131 (!g_Config.m_SvStrictSpectateMode && m_pPlayer->GetCid() == GameServer()->m_apPlayers[SnappingClient]->SpectatorId()))
1132 {
1133 Health = m_Health;
1134 Armor = m_Armor;
1135 AmmoCount = (m_FreezeTime == 0) ? m_Core.m_aWeapons[m_Core.m_ActiveWeapon].m_Ammo : 0;
1136 }
1137
1138 if(!Server()->IsSixup(ClientId: SnappingClient))
1139 {
1140 CNetObj_Character *pCharacter = Server()->SnapNewItem<CNetObj_Character>(Id);
1141 if(!pCharacter)
1142 return;
1143
1144 pCore->Write(pObjCore: pCharacter);
1145
1146 pCharacter->m_Tick = Tick;
1147 pCharacter->m_Emote = Emote;
1148
1149 if(pCharacter->m_HookedPlayer != -1)
1150 {
1151 if(!Server()->Translate(Target&: pCharacter->m_HookedPlayer, Client: SnappingClient))
1152 pCharacter->m_HookedPlayer = -1;
1153 }
1154
1155 pCharacter->m_AttackTick = m_AttackTick;
1156 pCharacter->m_Direction = m_Input.m_Direction;
1157 pCharacter->m_Weapon = Weapon;
1158 pCharacter->m_AmmoCount = AmmoCount;
1159 pCharacter->m_Health = Health;
1160 pCharacter->m_Armor = Armor;
1161 pCharacter->m_PlayerFlags = GetPlayer()->m_PlayerFlags;
1162 }
1163 else
1164 {
1165 protocol7::CNetObj_Character *pCharacter = Server()->SnapNewItem<protocol7::CNetObj_Character>(Id);
1166 if(!pCharacter)
1167 return;
1168
1169 pCore->Write(pObjCore: reinterpret_cast<CNetObj_CharacterCore *>(static_cast<protocol7::CNetObj_CharacterCore *>(pCharacter)));
1170 if(pCharacter->m_Angle > (int)(pi * 256.0f))
1171 {
1172 pCharacter->m_Angle -= (int)(2.0f * pi * 256.0f);
1173 }
1174
1175 // m_HookTick can be negative when using the hook_duration tune, which 0.7 clients
1176 // will consider invalid. https://github.com/ddnet/ddnet/issues/3915
1177 pCharacter->m_HookTick = maximum(a: 0, b: pCharacter->m_HookTick);
1178
1179 pCharacter->m_Tick = Tick;
1180 pCharacter->m_Emote = Emote;
1181 pCharacter->m_AttackTick = m_AttackTick;
1182 pCharacter->m_Direction = m_Input.m_Direction;
1183 pCharacter->m_Weapon = Weapon;
1184 pCharacter->m_AmmoCount = AmmoCount;
1185
1186 if(m_FreezeTime > 0 || m_Core.m_DeepFrozen)
1187 pCharacter->m_AmmoCount = m_Core.m_FreezeStart + g_Config.m_SvFreezeDelay * Server()->TickSpeed();
1188 else if(Weapon == WEAPON_NINJA)
1189 pCharacter->m_AmmoCount = m_Core.m_Ninja.m_ActivationTick + g_pData->m_Weapons.m_Ninja.m_Duration * Server()->TickSpeed() / 1000;
1190
1191 pCharacter->m_Health = Health;
1192 pCharacter->m_Armor = Armor;
1193 pCharacter->m_TriggeredEvents = m_TriggeredEvents7;
1194 }
1195}
1196
1197bool CCharacter::CanSnapCharacter(int SnappingClient)
1198{
1199 if(SnappingClient == SERVER_DEMO_CLIENT)
1200 return true;
1201
1202 CCharacter *pSnapChar = GameServer()->GetPlayerChar(ClientId: SnappingClient);
1203 CPlayer *pSnapPlayer = GameServer()->m_apPlayers[SnappingClient];
1204
1205 if(pSnapPlayer->GetTeam() == TEAM_SPECTATORS || pSnapPlayer->IsPaused())
1206 {
1207 if(pSnapPlayer->SpectatorId() != SPEC_FREEVIEW && !CanCollide(ClientId: pSnapPlayer->SpectatorId()) && (pSnapPlayer->m_ShowOthers == SHOW_OTHERS_OFF || (pSnapPlayer->m_ShowOthers == SHOW_OTHERS_ONLY_TEAM && !SameTeam(ClientId: pSnapPlayer->SpectatorId()))))
1208 return false;
1209 else if(pSnapPlayer->SpectatorId() == SPEC_FREEVIEW && !CanCollide(ClientId: SnappingClient) && pSnapPlayer->m_SpecTeam && !SameTeam(ClientId: SnappingClient))
1210 return false;
1211 }
1212 else if(pSnapChar && !pSnapChar->m_Core.m_Super && !CanCollide(ClientId: SnappingClient) && (pSnapPlayer->m_ShowOthers == SHOW_OTHERS_OFF || (pSnapPlayer->m_ShowOthers == SHOW_OTHERS_ONLY_TEAM && !SameTeam(ClientId: SnappingClient))))
1213 return false;
1214
1215 return true;
1216}
1217
1218bool CCharacter::IsSnappingCharacterInView(int SnappingClientId)
1219{
1220 int Id = m_pPlayer->GetCid();
1221
1222 // A player may not be clipped away if his hook or a hook attached to him is in the field of view
1223 bool PlayerAndHookNotInView = NetworkClippedLine(SnappingClient: SnappingClientId, StartPos: m_Pos, EndPos: m_Core.m_HookPos);
1224 bool AttachedHookInView = false;
1225 if(PlayerAndHookNotInView)
1226 {
1227 for(const auto &AttachedPlayerId : m_Core.m_AttachedPlayers)
1228 {
1229 const CCharacter *pOtherPlayer = GameServer()->GetPlayerChar(ClientId: AttachedPlayerId);
1230 if(pOtherPlayer && pOtherPlayer->m_Core.HookedPlayer() == Id)
1231 {
1232 if(!NetworkClippedLine(SnappingClient: SnappingClientId, StartPos: m_Pos, EndPos: pOtherPlayer->m_Pos))
1233 {
1234 AttachedHookInView = true;
1235 break;
1236 }
1237 }
1238 }
1239 }
1240 if(PlayerAndHookNotInView && !AttachedHookInView)
1241 {
1242 return false;
1243 }
1244 return true;
1245}
1246
1247void CCharacter::Snap(int SnappingClient)
1248{
1249 int Id = m_pPlayer->GetCid();
1250
1251 if(!Server()->Translate(Target&: Id, Client: SnappingClient))
1252 return;
1253
1254 if(!CanSnapCharacter(SnappingClient))
1255 {
1256 return;
1257 }
1258
1259 // always snap the snapping client, even if it is not in view
1260 if(!IsSnappingCharacterInView(SnappingClientId: SnappingClient) && Id != SnappingClient)
1261 return;
1262
1263 SnapCharacter(SnappingClient, Id);
1264
1265 CNetObj_DDNetCharacter *pDDNetCharacter = Server()->SnapNewItem<CNetObj_DDNetCharacter>(Id);
1266 if(!pDDNetCharacter)
1267 return;
1268
1269 pDDNetCharacter->m_Flags = 0;
1270 if(m_Core.m_Solo)
1271 pDDNetCharacter->m_Flags |= CHARACTERFLAG_SOLO;
1272 if(m_Core.m_Super)
1273 pDDNetCharacter->m_Flags |= CHARACTERFLAG_SUPER;
1274 if(m_Core.m_Invincible)
1275 pDDNetCharacter->m_Flags |= CHARACTERFLAG_INVINCIBLE;
1276 if(m_Core.m_EndlessHook)
1277 pDDNetCharacter->m_Flags |= CHARACTERFLAG_ENDLESS_HOOK;
1278 if(m_Core.m_CollisionDisabled || !GetTuning(Zone: m_TuneZone)->m_PlayerCollision)
1279 pDDNetCharacter->m_Flags |= CHARACTERFLAG_COLLISION_DISABLED;
1280 if(m_Core.m_HookHitDisabled || !GetTuning(Zone: m_TuneZone)->m_PlayerHooking)
1281 pDDNetCharacter->m_Flags |= CHARACTERFLAG_HOOK_HIT_DISABLED;
1282 if(m_Core.m_EndlessJump)
1283 pDDNetCharacter->m_Flags |= CHARACTERFLAG_ENDLESS_JUMP;
1284 if(m_Core.m_Jetpack)
1285 pDDNetCharacter->m_Flags |= CHARACTERFLAG_JETPACK;
1286 if(m_Core.m_HammerHitDisabled)
1287 pDDNetCharacter->m_Flags |= CHARACTERFLAG_HAMMER_HIT_DISABLED;
1288 if(m_Core.m_ShotgunHitDisabled)
1289 pDDNetCharacter->m_Flags |= CHARACTERFLAG_SHOTGUN_HIT_DISABLED;
1290 if(m_Core.m_GrenadeHitDisabled)
1291 pDDNetCharacter->m_Flags |= CHARACTERFLAG_GRENADE_HIT_DISABLED;
1292 if(m_Core.m_LaserHitDisabled)
1293 pDDNetCharacter->m_Flags |= CHARACTERFLAG_LASER_HIT_DISABLED;
1294 if(m_Core.m_HasTelegunGun)
1295 pDDNetCharacter->m_Flags |= CHARACTERFLAG_TELEGUN_GUN;
1296 if(m_Core.m_HasTelegunGrenade)
1297 pDDNetCharacter->m_Flags |= CHARACTERFLAG_TELEGUN_GRENADE;
1298 if(m_Core.m_HasTelegunLaser)
1299 pDDNetCharacter->m_Flags |= CHARACTERFLAG_TELEGUN_LASER;
1300 if(m_Core.m_aWeapons[WEAPON_HAMMER].m_Got)
1301 pDDNetCharacter->m_Flags |= CHARACTERFLAG_WEAPON_HAMMER;
1302 if(m_Core.m_aWeapons[WEAPON_GUN].m_Got)
1303 pDDNetCharacter->m_Flags |= CHARACTERFLAG_WEAPON_GUN;
1304 if(m_Core.m_aWeapons[WEAPON_SHOTGUN].m_Got)
1305 pDDNetCharacter->m_Flags |= CHARACTERFLAG_WEAPON_SHOTGUN;
1306 if(m_Core.m_aWeapons[WEAPON_GRENADE].m_Got)
1307 pDDNetCharacter->m_Flags |= CHARACTERFLAG_WEAPON_GRENADE;
1308 if(m_Core.m_aWeapons[WEAPON_LASER].m_Got)
1309 pDDNetCharacter->m_Flags |= CHARACTERFLAG_WEAPON_LASER;
1310 if(m_Core.m_ActiveWeapon == WEAPON_NINJA)
1311 pDDNetCharacter->m_Flags |= CHARACTERFLAG_WEAPON_NINJA;
1312 if(m_Core.m_LiveFrozen)
1313 pDDNetCharacter->m_Flags |= CHARACTERFLAG_MOVEMENTS_DISABLED;
1314
1315 pDDNetCharacter->m_FreezeEnd = m_Core.m_DeepFrozen ? -1 : (m_FreezeTime == 0 ? 0 : Server()->Tick() + m_FreezeTime);
1316 pDDNetCharacter->m_Jumps = m_Core.m_Jumps;
1317 pDDNetCharacter->m_TeleCheckpoint = m_TeleCheckpoint;
1318 pDDNetCharacter->m_StrongWeakId = m_StrongWeakId;
1319
1320 // Display Information
1321 pDDNetCharacter->m_JumpedTotal = m_Core.m_JumpedTotal;
1322 pDDNetCharacter->m_NinjaActivationTick = m_Core.m_Ninja.m_ActivationTick;
1323 pDDNetCharacter->m_FreezeStart = m_Core.m_FreezeStart;
1324 if(m_Core.m_IsInFreeze)
1325 {
1326 pDDNetCharacter->m_Flags |= CHARACTERFLAG_IN_FREEZE;
1327 }
1328 if(Teams()->IsPractice(Team: Team()))
1329 {
1330 pDDNetCharacter->m_Flags |= CHARACTERFLAG_PRACTICE_MODE;
1331 }
1332 if(Teams()->TeamLocked(Team: Team()))
1333 {
1334 pDDNetCharacter->m_Flags |= CHARACTERFLAG_LOCK_MODE;
1335 }
1336 if(Teams()->TeamFlock(Team: Team()))
1337 {
1338 pDDNetCharacter->m_Flags |= CHARACTERFLAG_TEAM0_MODE;
1339 }
1340 pDDNetCharacter->m_TargetX = m_Core.m_Input.m_TargetX;
1341 pDDNetCharacter->m_TargetY = m_Core.m_Input.m_TargetY;
1342
1343 // OVERRIDE_NONE is the default value, SnapNewItem zeroes the object, so it would incorrectly become 0
1344 pDDNetCharacter->m_TuneZoneOverride = TuneZone::OVERRIDE_NONE;
1345}
1346
1347void CCharacter::PostGlobalSnap()
1348{
1349 m_TriggeredEvents7 = 0;
1350}
1351
1352// DDRace
1353
1354bool CCharacter::CanCollide(int ClientId)
1355{
1356 return Teams()->m_Core.CanCollide(ClientId1: GetPlayer()->GetCid(), ClientId2: ClientId);
1357}
1358bool CCharacter::SameTeam(int ClientId)
1359{
1360 return Teams()->m_Core.SameTeam(ClientId1: GetPlayer()->GetCid(), ClientId2: ClientId);
1361}
1362
1363int CCharacter::Team()
1364{
1365 return Teams()->m_Core.Team(ClientId: m_pPlayer->GetCid());
1366}
1367
1368void CCharacter::FillAntibot(CAntibotCharacterData *pData)
1369{
1370 pData->m_Pos = m_Pos;
1371 pData->m_Vel = m_Core.m_Vel;
1372 pData->m_Angle = m_Core.m_Angle;
1373 pData->m_HookedPlayer = m_Core.HookedPlayer();
1374 pData->m_SpawnTick = m_SpawnTick;
1375 pData->m_WeaponChangeTick = m_WeaponChangeTick;
1376
1377 // 0
1378 pData->m_aLatestInputs[0].m_Direction = m_LatestInput.m_Direction;
1379 pData->m_aLatestInputs[0].m_TargetX = m_LatestInput.m_TargetX;
1380 pData->m_aLatestInputs[0].m_TargetY = m_LatestInput.m_TargetY;
1381 pData->m_aLatestInputs[0].m_Jump = m_LatestInput.m_Jump;
1382 pData->m_aLatestInputs[0].m_Fire = m_LatestInput.m_Fire;
1383 pData->m_aLatestInputs[0].m_Hook = m_LatestInput.m_Hook;
1384 pData->m_aLatestInputs[0].m_PlayerFlags = m_LatestInput.m_PlayerFlags;
1385 pData->m_aLatestInputs[0].m_WantedWeapon = m_LatestInput.m_WantedWeapon;
1386 pData->m_aLatestInputs[0].m_NextWeapon = m_LatestInput.m_NextWeapon;
1387 pData->m_aLatestInputs[0].m_PrevWeapon = m_LatestInput.m_PrevWeapon;
1388
1389 // 1
1390 pData->m_aLatestInputs[1].m_Direction = m_LatestPrevInput.m_Direction;
1391 pData->m_aLatestInputs[1].m_TargetX = m_LatestPrevInput.m_TargetX;
1392 pData->m_aLatestInputs[1].m_TargetY = m_LatestPrevInput.m_TargetY;
1393 pData->m_aLatestInputs[1].m_Jump = m_LatestPrevInput.m_Jump;
1394 pData->m_aLatestInputs[1].m_Fire = m_LatestPrevInput.m_Fire;
1395 pData->m_aLatestInputs[1].m_Hook = m_LatestPrevInput.m_Hook;
1396 pData->m_aLatestInputs[1].m_PlayerFlags = m_LatestPrevInput.m_PlayerFlags;
1397 pData->m_aLatestInputs[1].m_WantedWeapon = m_LatestPrevInput.m_WantedWeapon;
1398 pData->m_aLatestInputs[1].m_NextWeapon = m_LatestPrevInput.m_NextWeapon;
1399 pData->m_aLatestInputs[1].m_PrevWeapon = m_LatestPrevInput.m_PrevWeapon;
1400
1401 // 2
1402 pData->m_aLatestInputs[2].m_Direction = m_LatestPrevPrevInput.m_Direction;
1403 pData->m_aLatestInputs[2].m_TargetX = m_LatestPrevPrevInput.m_TargetX;
1404 pData->m_aLatestInputs[2].m_TargetY = m_LatestPrevPrevInput.m_TargetY;
1405 pData->m_aLatestInputs[2].m_Jump = m_LatestPrevPrevInput.m_Jump;
1406 pData->m_aLatestInputs[2].m_Fire = m_LatestPrevPrevInput.m_Fire;
1407 pData->m_aLatestInputs[2].m_Hook = m_LatestPrevPrevInput.m_Hook;
1408 pData->m_aLatestInputs[2].m_PlayerFlags = m_LatestPrevPrevInput.m_PlayerFlags;
1409 pData->m_aLatestInputs[2].m_WantedWeapon = m_LatestPrevPrevInput.m_WantedWeapon;
1410 pData->m_aLatestInputs[2].m_NextWeapon = m_LatestPrevPrevInput.m_NextWeapon;
1411 pData->m_aLatestInputs[2].m_PrevWeapon = m_LatestPrevPrevInput.m_PrevWeapon;
1412}
1413
1414void CCharacter::HandleBroadcast()
1415{
1416 CPlayerData *pData = GameServer()->Score()->PlayerData(Id: m_pPlayer->GetCid());
1417
1418 if(m_DDRaceState == ERaceState::STARTED && m_pPlayer->GetClientVersion() == VERSION_VANILLA && !Server()->IsSixup(ClientId: m_pPlayer->GetCid()) &&
1419 m_LastTimeCpBroadcasted != m_LastTimeCp && m_LastTimeCp > -1 &&
1420 m_TimeCpBroadcastEndTick > Server()->Tick() && pData->m_BestTime && pData->m_aBestTimeCp[m_LastTimeCp] != 0)
1421 {
1422 char aBroadcast[128];
1423 float Diff = m_aCurrentTimeCp[m_LastTimeCp] - pData->m_aBestTimeCp[m_LastTimeCp];
1424 str_format(buffer: aBroadcast, buffer_size: sizeof(aBroadcast), format: "Checkpoint | Diff : %+5.2f", Diff);
1425 GameServer()->SendBroadcast(pText: aBroadcast, ClientId: m_pPlayer->GetCid());
1426 m_LastTimeCpBroadcasted = m_LastTimeCp;
1427 m_LastBroadcast = Server()->Tick();
1428 }
1429 else if((m_pPlayer->m_TimerType == CPlayer::TIMERTYPE_BROADCAST || m_pPlayer->m_TimerType == CPlayer::TIMERTYPE_GAMETIMER_AND_BROADCAST) && m_DDRaceState == ERaceState::STARTED && m_LastBroadcast + Server()->TickSpeed() * g_Config.m_SvTimeInBroadcastInterval <= Server()->Tick())
1430 {
1431 char aBuf[32];
1432 int Time = (int64_t)100 * ((float)(Server()->Tick() - m_StartTime) / ((float)Server()->TickSpeed()));
1433 str_time(centisecs: Time, format: TIME_HOURS, buffer: aBuf, buffer_size: sizeof(aBuf));
1434 GameServer()->SendBroadcast(pText: aBuf, ClientId: m_pPlayer->GetCid(), IsImportant: false);
1435 m_LastTimeCpBroadcasted = m_LastTimeCp;
1436 m_LastBroadcast = Server()->Tick();
1437 }
1438}
1439
1440void CCharacter::HandleSkippableTiles(int Index)
1441{
1442 // handle death-tiles and leaving gamelayer
1443 if((Collision()->GetCollisionAt(x: m_Pos.x + GetProximityRadius() / 3.f, y: m_Pos.y - GetProximityRadius() / 3.f) == TILE_DEATH ||
1444 Collision()->GetCollisionAt(x: m_Pos.x + GetProximityRadius() / 3.f, y: m_Pos.y + GetProximityRadius() / 3.f) == TILE_DEATH ||
1445 Collision()->GetCollisionAt(x: m_Pos.x - GetProximityRadius() / 3.f, y: m_Pos.y - GetProximityRadius() / 3.f) == TILE_DEATH ||
1446 Collision()->GetCollisionAt(x: m_Pos.x - GetProximityRadius() / 3.f, y: m_Pos.y + GetProximityRadius() / 3.f) == TILE_DEATH ||
1447 Collision()->GetFrontCollisionAt(x: m_Pos.x + GetProximityRadius() / 3.f, y: m_Pos.y - GetProximityRadius() / 3.f) == TILE_DEATH ||
1448 Collision()->GetFrontCollisionAt(x: m_Pos.x + GetProximityRadius() / 3.f, y: m_Pos.y + GetProximityRadius() / 3.f) == TILE_DEATH ||
1449 Collision()->GetFrontCollisionAt(x: m_Pos.x - GetProximityRadius() / 3.f, y: m_Pos.y - GetProximityRadius() / 3.f) == TILE_DEATH ||
1450 Collision()->GetFrontCollisionAt(x: m_Pos.x - GetProximityRadius() / 3.f, y: m_Pos.y + GetProximityRadius() / 3.f) == TILE_DEATH) &&
1451 !m_Core.m_Super && !m_Core.m_Invincible && !(Team() && Teams()->TeeFinished(ClientId: m_pPlayer->GetCid())))
1452 {
1453 if(Teams()->IsPractice(Team: Team()))
1454 {
1455 Freeze();
1456 // Rate limit death effects to once per second
1457 if(Server()->Tick() - m_pPlayer->m_DieTick >= Server()->TickSpeed())
1458 {
1459 m_pPlayer->m_DieTick = Server()->Tick();
1460 GameServer()->CreateSound(Pos: m_Pos, Sound: SOUND_PLAYER_DIE, Mask: TeamMask());
1461 GameServer()->CreateDeath(Pos: m_Pos, ClientId: m_pPlayer->GetCid(), Mask: TeamMask());
1462 }
1463 }
1464 else
1465 {
1466 Die(Killer: m_pPlayer->GetCid(), Weapon: WEAPON_WORLD);
1467 return;
1468 }
1469 }
1470
1471 if(GameLayerClipped(CheckPos: m_Pos))
1472 {
1473 Die(Killer: m_pPlayer->GetCid(), Weapon: WEAPON_WORLD);
1474 return;
1475 }
1476
1477 if(Index < 0)
1478 return;
1479
1480 // handle speedup tiles
1481 if(Collision()->IsSpeedup(Index))
1482 {
1483 vec2 Direction, TempVel = m_Core.m_Vel;
1484 int Force, Type, MaxSpeed = 0;
1485 Collision()->GetSpeedup(Index, pDir: &Direction, pForce: &Force, pMaxSpeed: &MaxSpeed, pType: &Type);
1486
1487 if(Type == TILE_SPEED_BOOST_OLD)
1488 {
1489 float TeeAngle, SpeederAngle, DiffAngle, SpeedLeft, TeeSpeed;
1490 if(Force == 255 && MaxSpeed)
1491 {
1492 m_Core.m_Vel = Direction * (MaxSpeed / 5);
1493 }
1494 else
1495 {
1496 if(MaxSpeed > 0 && MaxSpeed < 5)
1497 MaxSpeed = 5;
1498 if(MaxSpeed > 0)
1499 {
1500 if(Direction.x > 0.0000001f)
1501 SpeederAngle = -std::atan(x: Direction.y / Direction.x);
1502 else if(Direction.x < 0.0000001f)
1503 SpeederAngle = std::atan(x: Direction.y / Direction.x) + 2.0f * std::asin(x: 1.0f);
1504 else if(Direction.y > 0.0000001f)
1505 SpeederAngle = std::asin(x: 1.0f);
1506 else
1507 SpeederAngle = std::asin(x: -1.0f);
1508
1509 if(SpeederAngle < 0)
1510 SpeederAngle = 4.0f * std::asin(x: 1.0f) + SpeederAngle;
1511
1512 if(TempVel.x > 0.0000001f)
1513 TeeAngle = -std::atan(x: TempVel.y / TempVel.x);
1514 else if(TempVel.x < 0.0000001f)
1515 TeeAngle = std::atan(x: TempVel.y / TempVel.x) + 2.0f * std::asin(x: 1.0f);
1516 else if(TempVel.y > 0.0000001f)
1517 TeeAngle = std::asin(x: 1.0f);
1518 else
1519 TeeAngle = std::asin(x: -1.0f);
1520
1521 if(TeeAngle < 0)
1522 TeeAngle = 4.0f * std::asin(x: 1.0f) + TeeAngle;
1523
1524 TeeSpeed = std::sqrt(x: std::pow(x: TempVel.x, y: 2) + std::pow(x: TempVel.y, y: 2));
1525
1526 DiffAngle = SpeederAngle - TeeAngle;
1527 SpeedLeft = MaxSpeed / 5.0f - std::cos(x: DiffAngle) * TeeSpeed;
1528 if(absolute(a: (int)SpeedLeft) > Force && SpeedLeft > 0.0000001f)
1529 TempVel += Direction * Force;
1530 else if(absolute(a: (int)SpeedLeft) > Force)
1531 TempVel += Direction * -Force;
1532 else
1533 TempVel += Direction * SpeedLeft;
1534 }
1535 else
1536 TempVel += Direction * Force;
1537
1538 m_Core.m_Vel = ClampVel(MoveRestriction: m_MoveRestrictions, Vel: TempVel);
1539 }
1540 }
1541 else if(Type == TILE_SPEED_BOOST)
1542 {
1543 constexpr float MaxSpeedScale = 5.0f;
1544 if(MaxSpeed == 0)
1545 {
1546 float MaxRampSpeed = GetTuning(Zone: m_TuneZone)->m_VelrampRange / (50 * log(x: maximum(a: (float)GetTuning(Zone: m_TuneZone)->m_VelrampCurvature, b: 1.01f)));
1547 MaxSpeed = maximum(a: MaxRampSpeed, b: GetTuning(Zone: m_TuneZone)->m_VelrampStart / 50) * MaxSpeedScale;
1548 }
1549
1550 // (signed) length of projection
1551 float CurrentDirectionalSpeed = dot(a: Direction, b: m_Core.m_Vel);
1552 float TempMaxSpeed = MaxSpeed / MaxSpeedScale;
1553 if(CurrentDirectionalSpeed + Force > TempMaxSpeed)
1554 TempVel += Direction * (TempMaxSpeed - CurrentDirectionalSpeed);
1555 else
1556 TempVel += Direction * Force;
1557
1558 m_Core.m_Vel = ClampVel(MoveRestriction: m_MoveRestrictions, Vel: TempVel);
1559 }
1560 }
1561}
1562
1563bool CCharacter::IsSwitchActiveCb(int Number, void *pUser)
1564{
1565 CCharacter *pThis = (CCharacter *)pUser;
1566 auto &aSwitchers = pThis->Switchers();
1567 return !aSwitchers.empty() && pThis->Team() != TEAM_SUPER && aSwitchers[Number].m_aStatus[pThis->Team()];
1568}
1569
1570void CCharacter::SetTimeCheckpoint(int TimeCheckpoint)
1571{
1572 if(TimeCheckpoint > -1 && m_DDRaceState == ERaceState::STARTED && m_aCurrentTimeCp[TimeCheckpoint] == 0.0f && m_Time != 0.0f)
1573 {
1574 m_LastTimeCp = TimeCheckpoint;
1575 m_aCurrentTimeCp[m_LastTimeCp] = m_Time;
1576 m_TimeCpBroadcastEndTick = Server()->Tick() + Server()->TickSpeed() * 2;
1577 if(m_pPlayer->GetClientVersion() >= VERSION_DDRACE || Server()->IsSixup(ClientId: m_pPlayer->GetCid()))
1578 {
1579 CPlayerData *pData = GameServer()->Score()->PlayerData(Id: m_pPlayer->GetCid());
1580 if(pData->m_aBestTimeCp[m_LastTimeCp] != 0.0f)
1581 {
1582 if(Server()->IsSixup(ClientId: m_pPlayer->GetCid()))
1583 {
1584 protocol7::CNetMsg_Sv_Checkpoint Msg;
1585 float Diff = (m_aCurrentTimeCp[m_LastTimeCp] - pData->m_aBestTimeCp[m_LastTimeCp]) * 1000;
1586 Msg.m_Diff = (int)Diff;
1587 Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL, ClientId: m_pPlayer->GetCid());
1588 }
1589 else
1590 {
1591 CNetMsg_Sv_DDRaceTime Msg;
1592 Msg.m_Time = (int)(m_Time * 100.0f);
1593 Msg.m_Finish = 0;
1594 float Diff = (m_aCurrentTimeCp[m_LastTimeCp] - pData->m_aBestTimeCp[m_LastTimeCp]) * 100;
1595 Msg.m_Check = (int)Diff;
1596 Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL, ClientId: m_pPlayer->GetCid());
1597 }
1598 }
1599 }
1600 }
1601}
1602
1603void CCharacter::HandleTiles(int Index)
1604{
1605 int MapIndex = Index;
1606 m_TileIndex = Collision()->GetTileIndex(Index: MapIndex);
1607 m_TileFIndex = Collision()->GetFrontTileIndex(Index: MapIndex);
1608 m_MoveRestrictions = Collision()->GetMoveRestrictions(pfnSwitchActive: IsSwitchActiveCb, pUser: this, Pos: m_Pos, Distance: 18.0f, OverrideCenterTileIndex: MapIndex);
1609 if(Index < 0)
1610 {
1611 m_LastRefillJumps = false;
1612 m_LastPenalty = false;
1613 m_LastBonus = false;
1614 return;
1615 }
1616 SetTimeCheckpoint(Collision()->IsTimeCheckpoint(Index: MapIndex));
1617 SetTimeCheckpoint(Collision()->IsFrontTimeCheckpoint(Index: MapIndex));
1618 int TeleCheckpoint = Collision()->IsTeleCheckpoint(Index: MapIndex);
1619 if(TeleCheckpoint)
1620 m_TeleCheckpoint = TeleCheckpoint;
1621
1622 GameServer()->m_pController->HandleCharacterTiles(pChr: this, MapIndex: Index);
1623 if(!m_Alive)
1624 return;
1625
1626 // freeze
1627 if(((m_TileIndex == TILE_FREEZE) || (m_TileFIndex == TILE_FREEZE)) && !m_Core.m_Super && !m_Core.m_Invincible && !m_Core.m_DeepFrozen)
1628 {
1629 Freeze();
1630 }
1631 else if(((m_TileIndex == TILE_UNFREEZE) || (m_TileFIndex == TILE_UNFREEZE)) && !m_Core.m_DeepFrozen)
1632 UnFreeze();
1633
1634 // deep freeze
1635 if(((m_TileIndex == TILE_DFREEZE) || (m_TileFIndex == TILE_DFREEZE)) && !m_Core.m_Super && !m_Core.m_Invincible && !m_Core.m_DeepFrozen)
1636 m_Core.m_DeepFrozen = true;
1637 else if(((m_TileIndex == TILE_DUNFREEZE) || (m_TileFIndex == TILE_DUNFREEZE)) && !m_Core.m_Super && !m_Core.m_Invincible && m_Core.m_DeepFrozen)
1638 m_Core.m_DeepFrozen = false;
1639
1640 // live freeze
1641 if(((m_TileIndex == TILE_LFREEZE) || (m_TileFIndex == TILE_LFREEZE)) && !m_Core.m_Super && !m_Core.m_Invincible)
1642 {
1643 m_Core.m_LiveFrozen = true;
1644 }
1645 else if(((m_TileIndex == TILE_LUNFREEZE) || (m_TileFIndex == TILE_LUNFREEZE)) && !m_Core.m_Super && !m_Core.m_Invincible)
1646 {
1647 m_Core.m_LiveFrozen = false;
1648 }
1649
1650 // endless hook
1651 if(((m_TileIndex == TILE_EHOOK_ENABLE) || (m_TileFIndex == TILE_EHOOK_ENABLE)))
1652 {
1653 SetEndlessHook(true);
1654 }
1655 else if(((m_TileIndex == TILE_EHOOK_DISABLE) || (m_TileFIndex == TILE_EHOOK_DISABLE)))
1656 {
1657 SetEndlessHook(false);
1658 }
1659
1660 // hit others
1661 if(((m_TileIndex == TILE_HIT_DISABLE) || (m_TileFIndex == TILE_HIT_DISABLE)) && (!m_Core.m_HammerHitDisabled || !m_Core.m_ShotgunHitDisabled || !m_Core.m_GrenadeHitDisabled || !m_Core.m_LaserHitDisabled))
1662 {
1663 GameServer()->SendChatTarget(To: GetPlayer()->GetCid(), pText: "You can't hit others");
1664 m_Core.m_HammerHitDisabled = true;
1665 m_Core.m_ShotgunHitDisabled = true;
1666 m_Core.m_GrenadeHitDisabled = true;
1667 m_Core.m_LaserHitDisabled = true;
1668 }
1669 else if(((m_TileIndex == TILE_HIT_ENABLE) || (m_TileFIndex == TILE_HIT_ENABLE)) && (m_Core.m_HammerHitDisabled || m_Core.m_ShotgunHitDisabled || m_Core.m_GrenadeHitDisabled || m_Core.m_LaserHitDisabled))
1670 {
1671 GameServer()->SendChatTarget(To: GetPlayer()->GetCid(), pText: "You can hit others");
1672 m_Core.m_ShotgunHitDisabled = false;
1673 m_Core.m_GrenadeHitDisabled = false;
1674 m_Core.m_HammerHitDisabled = false;
1675 m_Core.m_LaserHitDisabled = false;
1676 }
1677
1678 // collide with others
1679 if(((m_TileIndex == TILE_NPC_DISABLE) || (m_TileFIndex == TILE_NPC_DISABLE)) && !m_Core.m_CollisionDisabled)
1680 {
1681 GameServer()->SendChatTarget(To: GetPlayer()->GetCid(), pText: "You can't collide with others");
1682 m_Core.m_CollisionDisabled = true;
1683 }
1684 else if(((m_TileIndex == TILE_NPC_ENABLE) || (m_TileFIndex == TILE_NPC_ENABLE)) && m_Core.m_CollisionDisabled)
1685 {
1686 GameServer()->SendChatTarget(To: GetPlayer()->GetCid(), pText: "You can collide with others");
1687 m_Core.m_CollisionDisabled = false;
1688 }
1689
1690 // hook others
1691 if(((m_TileIndex == TILE_NPH_DISABLE) || (m_TileFIndex == TILE_NPH_DISABLE)) && !m_Core.m_HookHitDisabled)
1692 {
1693 GameServer()->SendChatTarget(To: GetPlayer()->GetCid(), pText: "You can't hook others");
1694 m_Core.m_HookHitDisabled = true;
1695 }
1696 else if(((m_TileIndex == TILE_NPH_ENABLE) || (m_TileFIndex == TILE_NPH_ENABLE)) && m_Core.m_HookHitDisabled)
1697 {
1698 GameServer()->SendChatTarget(To: GetPlayer()->GetCid(), pText: "You can hook others");
1699 m_Core.m_HookHitDisabled = false;
1700 }
1701
1702 // unlimited air jumps
1703 if(((m_TileIndex == TILE_UNLIMITED_JUMPS_ENABLE) || (m_TileFIndex == TILE_UNLIMITED_JUMPS_ENABLE)) && !m_Core.m_EndlessJump)
1704 {
1705 GameServer()->SendChatTarget(To: GetPlayer()->GetCid(), pText: "You have unlimited air jumps");
1706 m_Core.m_EndlessJump = true;
1707 }
1708 else if(((m_TileIndex == TILE_UNLIMITED_JUMPS_DISABLE) || (m_TileFIndex == TILE_UNLIMITED_JUMPS_DISABLE)) && m_Core.m_EndlessJump)
1709 {
1710 GameServer()->SendChatTarget(To: GetPlayer()->GetCid(), pText: "You don't have unlimited air jumps");
1711 m_Core.m_EndlessJump = false;
1712 }
1713
1714 // walljump
1715 if((m_TileIndex == TILE_WALLJUMP) || (m_TileFIndex == TILE_WALLJUMP))
1716 {
1717 if(m_Core.m_Vel.y > 0 && m_Core.m_Colliding && m_Core.m_LeftWall)
1718 {
1719 m_Core.m_LeftWall = false;
1720 m_Core.m_JumpedTotal = m_Core.m_Jumps >= 2 ? m_Core.m_Jumps - 2 : 0;
1721 m_Core.m_Jumped = 1;
1722 }
1723 }
1724
1725 // jetpack gun
1726 if(((m_TileIndex == TILE_JETPACK_ENABLE) || (m_TileFIndex == TILE_JETPACK_ENABLE)) && !m_Core.m_Jetpack)
1727 {
1728 GameServer()->SendChatTarget(To: GetPlayer()->GetCid(), pText: "You have a jetpack gun");
1729 m_Core.m_Jetpack = true;
1730 }
1731 else if(((m_TileIndex == TILE_JETPACK_DISABLE) || (m_TileFIndex == TILE_JETPACK_DISABLE)) && m_Core.m_Jetpack)
1732 {
1733 GameServer()->SendChatTarget(To: GetPlayer()->GetCid(), pText: "You lost your jetpack gun");
1734 m_Core.m_Jetpack = false;
1735 }
1736
1737 // refill jumps
1738 if(((m_TileIndex == TILE_REFILL_JUMPS) || (m_TileFIndex == TILE_REFILL_JUMPS)) && !m_LastRefillJumps)
1739 {
1740 m_Core.m_JumpedTotal = 0;
1741 m_Core.m_Jumped = 0;
1742 m_LastRefillJumps = true;
1743 }
1744 if((m_TileIndex != TILE_REFILL_JUMPS) && (m_TileFIndex != TILE_REFILL_JUMPS))
1745 {
1746 m_LastRefillJumps = false;
1747 }
1748
1749 // Teleport gun
1750 if(((m_TileIndex == TILE_TELE_GUN_ENABLE) || (m_TileFIndex == TILE_TELE_GUN_ENABLE)) && !m_Core.m_HasTelegunGun)
1751 {
1752 m_Core.m_HasTelegunGun = true;
1753
1754 GameServer()->SendChatTarget(To: GetPlayer()->GetCid(), pText: "Teleport gun enabled");
1755 }
1756 else if(((m_TileIndex == TILE_TELE_GUN_DISABLE) || (m_TileFIndex == TILE_TELE_GUN_DISABLE)) && m_Core.m_HasTelegunGun)
1757 {
1758 m_Core.m_HasTelegunGun = false;
1759
1760 GameServer()->SendChatTarget(To: GetPlayer()->GetCid(), pText: "Teleport gun disabled");
1761 }
1762
1763 if(((m_TileIndex == TILE_TELE_GRENADE_ENABLE) || (m_TileFIndex == TILE_TELE_GRENADE_ENABLE)) && !m_Core.m_HasTelegunGrenade)
1764 {
1765 m_Core.m_HasTelegunGrenade = true;
1766
1767 GameServer()->SendChatTarget(To: GetPlayer()->GetCid(), pText: "Teleport grenade enabled");
1768 }
1769 else if(((m_TileIndex == TILE_TELE_GRENADE_DISABLE) || (m_TileFIndex == TILE_TELE_GRENADE_DISABLE)) && m_Core.m_HasTelegunGrenade)
1770 {
1771 m_Core.m_HasTelegunGrenade = false;
1772
1773 GameServer()->SendChatTarget(To: GetPlayer()->GetCid(), pText: "Teleport grenade disabled");
1774 }
1775
1776 if(((m_TileIndex == TILE_TELE_LASER_ENABLE) || (m_TileFIndex == TILE_TELE_LASER_ENABLE)) && !m_Core.m_HasTelegunLaser)
1777 {
1778 m_Core.m_HasTelegunLaser = true;
1779
1780 GameServer()->SendChatTarget(To: GetPlayer()->GetCid(), pText: "Teleport laser enabled");
1781 }
1782 else if(((m_TileIndex == TILE_TELE_LASER_DISABLE) || (m_TileFIndex == TILE_TELE_LASER_DISABLE)) && m_Core.m_HasTelegunLaser)
1783 {
1784 m_Core.m_HasTelegunLaser = false;
1785
1786 GameServer()->SendChatTarget(To: GetPlayer()->GetCid(), pText: "Teleport laser disabled");
1787 }
1788
1789 // stopper
1790 if(m_Core.m_Vel.y > 0 && (m_MoveRestrictions & CANTMOVE_DOWN))
1791 {
1792 m_Core.m_Jumped = 0;
1793 m_Core.m_JumpedTotal = 0;
1794 }
1795 ApplyMoveRestrictions();
1796
1797 // handle switch tiles
1798 if(Collision()->GetSwitchType(Index: MapIndex) == TILE_SWITCHOPEN && Team() != TEAM_SUPER && Collision()->GetSwitchNumber(Index: MapIndex) > 0)
1799 {
1800 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aStatus[Team()] = true;
1801 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aEndTick[Team()] = 0;
1802 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aType[Team()] = TILE_SWITCHOPEN;
1803 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aLastUpdateTick[Team()] = Server()->Tick();
1804 }
1805 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_SWITCHTIMEDOPEN && Team() != TEAM_SUPER && Collision()->GetSwitchNumber(Index: MapIndex) > 0)
1806 {
1807 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aStatus[Team()] = true;
1808 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aEndTick[Team()] = Server()->Tick() + 1 + Collision()->GetSwitchDelay(Index: MapIndex) * Server()->TickSpeed();
1809 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aType[Team()] = TILE_SWITCHTIMEDOPEN;
1810 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aLastUpdateTick[Team()] = Server()->Tick();
1811 }
1812 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_SWITCHTIMEDCLOSE && Team() != TEAM_SUPER && Collision()->GetSwitchNumber(Index: MapIndex) > 0)
1813 {
1814 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aStatus[Team()] = false;
1815 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aEndTick[Team()] = Server()->Tick() + 1 + Collision()->GetSwitchDelay(Index: MapIndex) * Server()->TickSpeed();
1816 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aType[Team()] = TILE_SWITCHTIMEDCLOSE;
1817 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aLastUpdateTick[Team()] = Server()->Tick();
1818 }
1819 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_SWITCHCLOSE && Team() != TEAM_SUPER && Collision()->GetSwitchNumber(Index: MapIndex) > 0)
1820 {
1821 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aStatus[Team()] = false;
1822 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aEndTick[Team()] = 0;
1823 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aType[Team()] = TILE_SWITCHCLOSE;
1824 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aLastUpdateTick[Team()] = Server()->Tick();
1825 }
1826 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_FREEZE && Team() != TEAM_SUPER && !m_Core.m_Invincible)
1827 {
1828 if(Collision()->GetSwitchNumber(Index: MapIndex) == 0 || Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aStatus[Team()])
1829 {
1830 Freeze(Seconds: Collision()->GetSwitchDelay(Index: MapIndex));
1831 }
1832 }
1833 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_DFREEZE && Team() != TEAM_SUPER && !m_Core.m_Invincible)
1834 {
1835 if(Collision()->GetSwitchNumber(Index: MapIndex) == 0 || Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aStatus[Team()])
1836 m_Core.m_DeepFrozen = true;
1837 }
1838 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_DUNFREEZE && Team() != TEAM_SUPER && !m_Core.m_Invincible)
1839 {
1840 if(Collision()->GetSwitchNumber(Index: MapIndex) == 0 || Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aStatus[Team()])
1841 m_Core.m_DeepFrozen = false;
1842 }
1843 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_LFREEZE && Team() != TEAM_SUPER && !m_Core.m_Invincible)
1844 {
1845 if(Collision()->GetSwitchNumber(Index: MapIndex) == 0 || Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aStatus[Team()])
1846 {
1847 m_Core.m_LiveFrozen = true;
1848 }
1849 }
1850 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_LUNFREEZE && Team() != TEAM_SUPER && !m_Core.m_Invincible)
1851 {
1852 if(Collision()->GetSwitchNumber(Index: MapIndex) == 0 || Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aStatus[Team()])
1853 {
1854 m_Core.m_LiveFrozen = false;
1855 }
1856 }
1857 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_HIT_ENABLE && m_Core.m_HammerHitDisabled && Collision()->GetSwitchDelay(Index: MapIndex) == WEAPON_HAMMER)
1858 {
1859 GameServer()->SendChatTarget(To: GetPlayer()->GetCid(), pText: "You can hammer hit others");
1860 m_Core.m_HammerHitDisabled = false;
1861 }
1862 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_HIT_DISABLE && !(m_Core.m_HammerHitDisabled) && Collision()->GetSwitchDelay(Index: MapIndex) == WEAPON_HAMMER)
1863 {
1864 GameServer()->SendChatTarget(To: GetPlayer()->GetCid(), pText: "You can't hammer hit others");
1865 m_Core.m_HammerHitDisabled = true;
1866 }
1867 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_HIT_ENABLE && m_Core.m_ShotgunHitDisabled && Collision()->GetSwitchDelay(Index: MapIndex) == WEAPON_SHOTGUN)
1868 {
1869 GameServer()->SendChatTarget(To: GetPlayer()->GetCid(), pText: "You can shoot others with shotgun");
1870 m_Core.m_ShotgunHitDisabled = false;
1871 }
1872 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_HIT_DISABLE && !(m_Core.m_ShotgunHitDisabled) && Collision()->GetSwitchDelay(Index: MapIndex) == WEAPON_SHOTGUN)
1873 {
1874 GameServer()->SendChatTarget(To: GetPlayer()->GetCid(), pText: "You can't shoot others with shotgun");
1875 m_Core.m_ShotgunHitDisabled = true;
1876 }
1877 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_HIT_ENABLE && m_Core.m_GrenadeHitDisabled && Collision()->GetSwitchDelay(Index: MapIndex) == WEAPON_GRENADE)
1878 {
1879 GameServer()->SendChatTarget(To: GetPlayer()->GetCid(), pText: "You can shoot others with grenade");
1880 m_Core.m_GrenadeHitDisabled = false;
1881 }
1882 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_HIT_DISABLE && !(m_Core.m_GrenadeHitDisabled) && Collision()->GetSwitchDelay(Index: MapIndex) == WEAPON_GRENADE)
1883 {
1884 GameServer()->SendChatTarget(To: GetPlayer()->GetCid(), pText: "You can't shoot others with grenade");
1885 m_Core.m_GrenadeHitDisabled = true;
1886 }
1887 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_HIT_ENABLE && m_Core.m_LaserHitDisabled && Collision()->GetSwitchDelay(Index: MapIndex) == WEAPON_LASER)
1888 {
1889 GameServer()->SendChatTarget(To: GetPlayer()->GetCid(), pText: "You can shoot others with laser");
1890 m_Core.m_LaserHitDisabled = false;
1891 }
1892 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_HIT_DISABLE && !(m_Core.m_LaserHitDisabled) && Collision()->GetSwitchDelay(Index: MapIndex) == WEAPON_LASER)
1893 {
1894 GameServer()->SendChatTarget(To: GetPlayer()->GetCid(), pText: "You can't shoot others with laser");
1895 m_Core.m_LaserHitDisabled = true;
1896 }
1897 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_JUMP)
1898 {
1899 int NewJumps = Collision()->GetSwitchDelay(Index: MapIndex);
1900 if(NewJumps == 255)
1901 {
1902 NewJumps = -1;
1903 }
1904
1905 if(NewJumps != m_Core.m_Jumps)
1906 {
1907 char aBuf[256];
1908 if(NewJumps == -1)
1909 str_copy(dst&: aBuf, src: "You only have your ground jump now");
1910 else if(NewJumps == 1)
1911 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "You can jump %d time", NewJumps);
1912 else
1913 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "You can jump %d times", NewJumps);
1914 GameServer()->SendChatTarget(To: GetPlayer()->GetCid(), pText: aBuf);
1915 m_Core.m_Jumps = NewJumps;
1916 }
1917 }
1918 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_ADD_TIME && !m_LastPenalty)
1919 {
1920 const int Minutes = Collision()->GetSwitchDelay(Index: MapIndex);
1921 const int Seconds = Collision()->GetSwitchNumber(Index: MapIndex);
1922 int Team = Teams()->m_Core.Team(ClientId: m_Core.m_Id);
1923
1924 m_StartTime -= (Minutes * 60 + Seconds) * Server()->TickSpeed();
1925
1926 if((g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO || (Team != TEAM_FLOCK && !Teams()->TeamFlock(Team))) && Team != TEAM_SUPER)
1927 {
1928 for(int i = 0; i < MAX_CLIENTS; i++)
1929 {
1930 if(Teams()->m_Core.Team(ClientId: i) == Team && i != m_Core.m_Id && GameServer()->m_apPlayers[i])
1931 {
1932 CCharacter *pChar = GameServer()->m_apPlayers[i]->GetCharacter();
1933
1934 if(pChar)
1935 pChar->m_StartTime = m_StartTime;
1936 }
1937 }
1938 }
1939
1940 m_LastPenalty = true;
1941 }
1942 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_SUBTRACT_TIME && !m_LastBonus)
1943 {
1944 const int Minutes = Collision()->GetSwitchDelay(Index: MapIndex);
1945 const int Seconds = Collision()->GetSwitchNumber(Index: MapIndex);
1946 int Team = Teams()->m_Core.Team(ClientId: m_Core.m_Id);
1947
1948 m_StartTime += (Minutes * 60 + Seconds) * Server()->TickSpeed();
1949 if(m_StartTime > Server()->Tick())
1950 m_StartTime = Server()->Tick();
1951
1952 if((g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO || (Team != TEAM_FLOCK && !Teams()->TeamFlock(Team))) && Team != TEAM_SUPER)
1953 {
1954 for(int i = 0; i < MAX_CLIENTS; i++)
1955 {
1956 if(Teams()->m_Core.Team(ClientId: i) == Team && i != m_Core.m_Id && GameServer()->m_apPlayers[i])
1957 {
1958 CCharacter *pChar = GameServer()->m_apPlayers[i]->GetCharacter();
1959
1960 if(pChar)
1961 pChar->m_StartTime = m_StartTime;
1962 }
1963 }
1964 }
1965
1966 m_LastBonus = true;
1967 }
1968
1969 if(Collision()->GetSwitchType(Index: MapIndex) != TILE_ADD_TIME)
1970 {
1971 m_LastPenalty = false;
1972 }
1973
1974 if(Collision()->GetSwitchType(Index: MapIndex) != TILE_SUBTRACT_TIME)
1975 {
1976 m_LastBonus = false;
1977 }
1978
1979 int z = Collision()->IsTeleport(Index: MapIndex);
1980 if(!g_Config.m_SvOldTeleportHook && !g_Config.m_SvOldTeleportWeapons && z && !Collision()->TeleOuts(Number: z - 1).empty())
1981 {
1982 if(m_Core.m_Super || m_Core.m_Invincible)
1983 return;
1984 int TeleOut = GameWorld()->m_Core.RandomOr0(BelowThis: Collision()->TeleOuts(Number: z - 1).size());
1985 m_Core.m_Pos = Collision()->TeleOuts(Number: z - 1)[TeleOut];
1986 if(!g_Config.m_SvTeleportHoldHook)
1987 {
1988 ResetHook();
1989 }
1990 if(g_Config.m_SvTeleportLoseWeapons)
1991 ResetPickups();
1992 return;
1993 }
1994 const int EvilTeleport = Collision()->IsEvilTeleport(Index: MapIndex);
1995 if(EvilTeleport && !Collision()->TeleOuts(Number: EvilTeleport - 1).empty())
1996 {
1997 if(m_Core.m_Super || m_Core.m_Invincible)
1998 return;
1999 int TeleOut = GameWorld()->m_Core.RandomOr0(BelowThis: Collision()->TeleOuts(Number: EvilTeleport - 1).size());
2000 m_Core.m_Pos = Collision()->TeleOuts(Number: EvilTeleport - 1)[TeleOut];
2001 if(!g_Config.m_SvOldTeleportHook && !g_Config.m_SvOldTeleportWeapons)
2002 {
2003 m_Core.m_Vel = vec2(0, 0);
2004
2005 if(!g_Config.m_SvTeleportHoldHook)
2006 {
2007 ResetHook();
2008 GameWorld()->ReleaseHooked(ClientId: GetPlayer()->GetCid());
2009 }
2010 if(g_Config.m_SvTeleportLoseWeapons)
2011 {
2012 ResetPickups();
2013 }
2014 }
2015 return;
2016 }
2017 if(Collision()->IsCheckEvilTeleport(Index: MapIndex))
2018 {
2019 if(m_Core.m_Super || m_Core.m_Invincible)
2020 return;
2021 // first check if there is a TeleCheckOut for the current recorded checkpoint, if not check previous checkpoints
2022 for(int k = m_TeleCheckpoint - 1; k >= 0; k--)
2023 {
2024 if(!Collision()->TeleCheckOuts(Number: k).empty())
2025 {
2026 int TeleOut = GameWorld()->m_Core.RandomOr0(BelowThis: Collision()->TeleCheckOuts(Number: k).size());
2027 m_Core.m_Pos = Collision()->TeleCheckOuts(Number: k)[TeleOut];
2028 m_Core.m_Vel = vec2(0, 0);
2029
2030 if(!g_Config.m_SvTeleportHoldHook)
2031 {
2032 ResetHook();
2033 GameWorld()->ReleaseHooked(ClientId: GetPlayer()->GetCid());
2034 }
2035
2036 return;
2037 }
2038 }
2039 // if no checkpointout have been found (or if there no recorded checkpoint), teleport to start
2040 vec2 SpawnPos;
2041 if(GameServer()->m_pController->CanSpawn(Team: m_pPlayer->GetTeam(), pOutPos: &SpawnPos, ClientId: GetPlayer()->GetCid()))
2042 {
2043 m_Core.m_Pos = SpawnPos;
2044 m_Core.m_Vel = vec2(0, 0);
2045
2046 if(!g_Config.m_SvTeleportHoldHook)
2047 {
2048 ResetHook();
2049 GameWorld()->ReleaseHooked(ClientId: GetPlayer()->GetCid());
2050 }
2051 }
2052 return;
2053 }
2054 if(Collision()->IsCheckTeleport(Index: MapIndex))
2055 {
2056 if(m_Core.m_Super || m_Core.m_Invincible)
2057 return;
2058 // first check if there is a TeleCheckOut for the current recorded checkpoint, if not check previous checkpoints
2059 for(int k = m_TeleCheckpoint - 1; k >= 0; k--)
2060 {
2061 if(!Collision()->TeleCheckOuts(Number: k).empty())
2062 {
2063 int TeleOut = GameWorld()->m_Core.RandomOr0(BelowThis: Collision()->TeleCheckOuts(Number: k).size());
2064 m_Core.m_Pos = Collision()->TeleCheckOuts(Number: k)[TeleOut];
2065
2066 if(!g_Config.m_SvTeleportHoldHook)
2067 {
2068 ResetHook();
2069 }
2070
2071 return;
2072 }
2073 }
2074 // if no checkpointout have been found (or if there no recorded checkpoint), teleport to start
2075 vec2 SpawnPos;
2076 if(GameServer()->m_pController->CanSpawn(Team: m_pPlayer->GetTeam(), pOutPos: &SpawnPos, ClientId: GetPlayer()->GetCid()))
2077 {
2078 m_Core.m_Pos = SpawnPos;
2079
2080 if(!g_Config.m_SvTeleportHoldHook)
2081 {
2082 ResetHook();
2083 }
2084 }
2085 return;
2086 }
2087}
2088
2089void CCharacter::HandleTuneLayer()
2090{
2091 m_TuneZoneOld = m_TuneZone;
2092 int CurrentIndex = Collision()->GetMapIndex(Pos: m_Pos);
2093 m_TuneZone = Collision()->IsTune(Index: CurrentIndex);
2094 m_Core.m_Tuning = TuningList()[m_TuneZone]; // throw tunings from specific zone into gamecore
2095
2096 if(m_TuneZone != m_TuneZoneOld) // don't send tunigs all the time
2097 {
2098 // send zone msgs
2099 SendZoneMsgs();
2100 }
2101}
2102
2103void CCharacter::SendZoneMsgs()
2104{
2105 // send zone leave msg
2106 // (m_TuneZoneOld >= 0: avoid zone leave msgs on spawn)
2107 if(m_TuneZoneOld >= 0 && GameServer()->m_aaZoneLeaveMsg[m_TuneZoneOld][0])
2108 {
2109 const char *pCur = GameServer()->m_aaZoneLeaveMsg[m_TuneZoneOld];
2110 const char *pPos;
2111 while((pPos = str_find(haystack: pCur, needle: "\\n")))
2112 {
2113 char aBuf[256];
2114 str_copy(dst: aBuf, src: pCur, dst_size: pPos - pCur + 1);
2115 aBuf[pPos - pCur + 1] = '\0';
2116 pCur = pPos + 2;
2117 GameServer()->SendChatTarget(To: m_pPlayer->GetCid(), pText: aBuf);
2118 }
2119 GameServer()->SendChatTarget(To: m_pPlayer->GetCid(), pText: pCur);
2120 }
2121 // send zone enter msg
2122 if(GameServer()->m_aaZoneEnterMsg[m_TuneZone][0])
2123 {
2124 const char *pCur = GameServer()->m_aaZoneEnterMsg[m_TuneZone];
2125 const char *pPos;
2126 while((pPos = str_find(haystack: pCur, needle: "\\n")))
2127 {
2128 char aBuf[256];
2129 str_copy(dst: aBuf, src: pCur, dst_size: pPos - pCur + 1);
2130 aBuf[pPos - pCur + 1] = '\0';
2131 pCur = pPos + 2;
2132 GameServer()->SendChatTarget(To: m_pPlayer->GetCid(), pText: aBuf);
2133 }
2134 GameServer()->SendChatTarget(To: m_pPlayer->GetCid(), pText: pCur);
2135 }
2136}
2137
2138IAntibot *CCharacter::Antibot()
2139{
2140 return GameServer()->Antibot();
2141}
2142
2143void CCharacter::SetTeams(CGameTeams *pTeams)
2144{
2145 m_pTeams = pTeams;
2146 m_Core.SetTeamsCore(&m_pTeams->m_Core);
2147}
2148
2149bool CCharacter::TrySetRescue(int RescueMode)
2150{
2151 bool Set = false;
2152 if(g_Config.m_SvRescue || ((g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO || Team() > TEAM_FLOCK) && Teams()->IsValidTeamNumber(Team: Team())))
2153 {
2154 // check for nearby health pickups (also freeze)
2155 bool InHealthPickup = false;
2156 if(!m_Core.m_IsInFreeze)
2157 {
2158 CEntity *apEnts[9];
2159 int Num = GameWorld()->FindEntities(Pos: m_Pos, Radius: GetProximityRadius() + CPickup::ms_CollisionExtraSize, ppEnts: apEnts, Max: std::size(apEnts), Type: CGameWorld::ENTTYPE_PICKUP);
2160 for(int i = 0; i < Num; ++i)
2161 {
2162 CPickup *pPickup = static_cast<CPickup *>(apEnts[i]);
2163 if(pPickup->Type() == POWERUP_HEALTH)
2164 {
2165 // This uses a separate variable InHealthPickup instead of setting m_Core.m_IsInFreeze
2166 // as the latter causes freezebars to flicker when standing in the freeze range of a
2167 // health pickup. When the same code for client prediction is added, the freezebars
2168 // still flicker, but only when standing at the edge of the health pickup's freeze range.
2169 InHealthPickup = true;
2170 break;
2171 }
2172 }
2173 }
2174
2175 if(!m_Core.m_IsInFreeze && IsGrounded() && !m_Core.m_DeepFrozen && !InHealthPickup)
2176 {
2177 ForceSetRescue(RescueMode);
2178 Set = true;
2179 }
2180 }
2181
2182 return Set;
2183}
2184
2185void CCharacter::ForceSetRescue(int RescueMode)
2186{
2187 m_RescueTee[RescueMode].Save(pChr: this);
2188 m_SetSavePos[RescueMode] = true;
2189}
2190
2191void CCharacter::DDRaceTick()
2192{
2193 mem_copy(dest: &m_Input, source: &m_SavedInput, size: sizeof(m_Input));
2194 GameServer()->m_pController->SetArmorProgress(pCharacter: this, Progress: m_FreezeTime);
2195 if(m_Input.m_Direction != 0 || m_Input.m_Jump != 0)
2196 m_LastMove = Server()->Tick();
2197
2198 if(m_Core.m_LiveFrozen && !m_Core.m_Super && !m_Core.m_Invincible)
2199 {
2200 m_Input.m_Direction = 0;
2201 m_Input.m_Jump = 0;
2202 // Hook is possible in live freeze
2203 }
2204 if(m_FreezeTime > 0)
2205 {
2206 if(m_FreezeTime % Server()->TickSpeed() == Server()->TickSpeed() - 1)
2207 {
2208 GameServer()->CreateDamageInd(Pos: m_Pos, AngleMod: 0, Amount: (m_FreezeTime + 1) / Server()->TickSpeed(), Mask: TeamMask() & GameServer()->ClientsMaskExcludeClientVersionAndHigher(Version: VERSION_DDNET_NEW_HUD));
2209 }
2210 m_FreezeTime--;
2211 m_Input.m_Direction = 0;
2212 m_Input.m_Jump = 0;
2213 m_Input.m_Hook = 0;
2214 if(m_FreezeTime == 1)
2215 UnFreeze();
2216 }
2217
2218 HandleTuneLayer(); // need this before coretick
2219
2220 // check if the tee is in any type of freeze
2221 int Index = Collision()->GetPureMapIndex(Pos: m_Pos);
2222 const int aTiles[] = {
2223 Collision()->GetTileIndex(Index),
2224 Collision()->GetFrontTileIndex(Index),
2225 Collision()->GetSwitchType(Index)};
2226 m_Core.m_IsInFreeze = false;
2227 for(const int Tile : aTiles)
2228 {
2229 if(Tile == TILE_FREEZE || Tile == TILE_DFREEZE || Tile == TILE_LFREEZE || Tile == TILE_DEATH)
2230 {
2231 m_Core.m_IsInFreeze = true;
2232 break;
2233 }
2234 }
2235 m_Core.m_IsInFreeze |= (Collision()->GetCollisionAt(x: m_Pos.x + GetProximityRadius() / 3.f, y: m_Pos.y - GetProximityRadius() / 3.f) == TILE_DEATH ||
2236 Collision()->GetCollisionAt(x: m_Pos.x + GetProximityRadius() / 3.f, y: m_Pos.y + GetProximityRadius() / 3.f) == TILE_DEATH ||
2237 Collision()->GetCollisionAt(x: m_Pos.x - GetProximityRadius() / 3.f, y: m_Pos.y - GetProximityRadius() / 3.f) == TILE_DEATH ||
2238 Collision()->GetCollisionAt(x: m_Pos.x - GetProximityRadius() / 3.f, y: m_Pos.y + GetProximityRadius() / 3.f) == TILE_DEATH ||
2239 Collision()->GetFrontCollisionAt(x: m_Pos.x + GetProximityRadius() / 3.f, y: m_Pos.y - GetProximityRadius() / 3.f) == TILE_DEATH ||
2240 Collision()->GetFrontCollisionAt(x: m_Pos.x + GetProximityRadius() / 3.f, y: m_Pos.y + GetProximityRadius() / 3.f) == TILE_DEATH ||
2241 Collision()->GetFrontCollisionAt(x: m_Pos.x - GetProximityRadius() / 3.f, y: m_Pos.y - GetProximityRadius() / 3.f) == TILE_DEATH ||
2242 Collision()->GetFrontCollisionAt(x: m_Pos.x - GetProximityRadius() / 3.f, y: m_Pos.y + GetProximityRadius() / 3.f) == TILE_DEATH);
2243
2244 // look for save position for rescue feature
2245 // always update auto rescue
2246 TrySetRescue(RescueMode: RESCUEMODE_AUTO);
2247
2248 m_Core.m_Id = GetPlayer()->GetCid();
2249}
2250
2251void CCharacter::DDRacePostCoreTick()
2252{
2253 m_Time = (float)(Server()->Tick() - m_StartTime) / ((float)Server()->TickSpeed());
2254
2255 if(m_Core.m_EndlessHook || (m_Core.m_Super && g_Config.m_SvEndlessSuperHook))
2256 m_Core.m_HookTick = 0;
2257
2258 m_FrozenLastTick = false;
2259
2260 if(m_Core.m_DeepFrozen && !m_Core.m_Super && !m_Core.m_Invincible)
2261 Freeze();
2262
2263 // following jump rules can be overridden by tiles, like Refill Jumps, Stopper and Wall Jump
2264 if(m_Core.m_Jumps == -1)
2265 {
2266 // The player has only one ground jump, so his feet are always dark
2267 m_Core.m_Jumped |= 2;
2268 }
2269 else if(m_Core.m_Jumps == 0)
2270 {
2271 // The player has no jumps at all, so his feet are always dark
2272 m_Core.m_Jumped |= 2;
2273 }
2274 else if(m_Core.m_Jumps == 1 && m_Core.m_Jumped > 0)
2275 {
2276 // If the player has only one jump, each jump is the last one
2277 m_Core.m_Jumped |= 2;
2278 }
2279 else if(m_Core.m_JumpedTotal < m_Core.m_Jumps - 1 && m_Core.m_Jumped > 1)
2280 {
2281 // The player has not yet used up all his jumps, so his feet remain light
2282 m_Core.m_Jumped = 1;
2283 }
2284
2285 if((m_Core.m_Super || m_Core.m_EndlessJump) && m_Core.m_Jumped > 1)
2286 {
2287 // Super players and players with infinite jumps always have light feet
2288 m_Core.m_Jumped = 1;
2289 }
2290
2291 int CurrentIndex = Collision()->GetMapIndex(Pos: m_Pos);
2292 HandleSkippableTiles(Index: CurrentIndex);
2293 if(!m_Alive)
2294 return;
2295
2296 // handle Anti-Skip tiles
2297 std::vector<int> vIndices = Collision()->GetMapIndices(PrevPos: m_PrevPos, Pos: m_Pos);
2298 if(!vIndices.empty())
2299 {
2300 for(int &Index : vIndices)
2301 {
2302 HandleTiles(Index);
2303 if(!m_Alive)
2304 return;
2305 }
2306 }
2307 else
2308 {
2309 HandleTiles(Index: CurrentIndex);
2310 if(!m_Alive)
2311 return;
2312 }
2313
2314 // teleport gun
2315 if(m_TeleGunTeleport)
2316 {
2317 GameServer()->CreateDeath(Pos: m_Pos, ClientId: m_pPlayer->GetCid(), Mask: TeamMask());
2318 m_Core.m_Pos = m_TeleGunPos;
2319 if(!m_IsBlueTeleGunTeleport)
2320 m_Core.m_Vel = vec2(0, 0);
2321 GameServer()->CreateDeath(Pos: m_TeleGunPos, ClientId: m_pPlayer->GetCid(), Mask: TeamMask());
2322 GameServer()->CreateSound(Pos: m_TeleGunPos, Sound: SOUND_WEAPON_SPAWN, Mask: TeamMask());
2323 m_TeleGunTeleport = false;
2324 m_IsBlueTeleGunTeleport = false;
2325 }
2326
2327 HandleBroadcast();
2328}
2329
2330bool CCharacter::Freeze(int Seconds)
2331{
2332 if(Seconds <= 0 || m_Core.m_Super || m_Core.m_Invincible || m_FreezeTime > Seconds * Server()->TickSpeed())
2333 return false;
2334 if(m_FreezeTime == 0 || m_Core.m_FreezeStart < Server()->Tick() - Server()->TickSpeed())
2335 {
2336 m_Armor = 0;
2337 m_FreezeTime = Seconds * Server()->TickSpeed();
2338 m_Core.m_FreezeStart = Server()->Tick();
2339 return true;
2340 }
2341 return false;
2342}
2343
2344bool CCharacter::Freeze()
2345{
2346 return Freeze(Seconds: g_Config.m_SvFreezeDelay);
2347}
2348
2349bool CCharacter::UnFreeze()
2350{
2351 if(m_FreezeTime > 0)
2352 {
2353 m_Armor = 10;
2354 if(!m_Core.m_aWeapons[m_Core.m_ActiveWeapon].m_Got)
2355 m_Core.m_ActiveWeapon = WEAPON_GUN;
2356 m_FreezeTime = 0;
2357 m_Core.m_FreezeStart = 0;
2358 m_FrozenLastTick = true;
2359 return true;
2360 }
2361 return false;
2362}
2363
2364void CCharacter::ResetJumps()
2365{
2366 m_Core.m_JumpedTotal = 0;
2367 m_Core.m_Jumped = 0;
2368}
2369
2370void CCharacter::GiveWeapon(int Weapon, bool Remove)
2371{
2372 if(Weapon == WEAPON_NINJA)
2373 {
2374 if(Remove)
2375 RemoveNinja();
2376 else
2377 GiveNinja();
2378 return;
2379 }
2380
2381 if(Remove)
2382 {
2383 if(GetActiveWeapon() == Weapon)
2384 SetActiveWeapon(WEAPON_GUN);
2385 }
2386 else
2387 {
2388 m_Core.m_aWeapons[Weapon].m_Ammo = -1;
2389 }
2390
2391 m_Core.m_aWeapons[Weapon].m_Got = !Remove;
2392}
2393
2394void CCharacter::GiveAllWeapons()
2395{
2396 for(int i = WEAPON_GUN; i < NUM_WEAPONS - 1; i++)
2397 {
2398 GiveWeapon(Weapon: i);
2399 }
2400}
2401
2402void CCharacter::ResetPickups()
2403{
2404 for(int i = WEAPON_SHOTGUN; i < NUM_WEAPONS - 1; i++)
2405 {
2406 m_Core.m_aWeapons[i].m_Got = false;
2407 if(m_Core.m_ActiveWeapon == i)
2408 m_Core.m_ActiveWeapon = WEAPON_GUN;
2409 }
2410}
2411
2412void CCharacter::SetEndlessHook(bool Enable)
2413{
2414 if(m_Core.m_EndlessHook == Enable)
2415 {
2416 return;
2417 }
2418 GameServer()->SendChatTarget(To: GetPlayer()->GetCid(), pText: Enable ? "Endless hook has been activated" : "Endless hook has been deactivated");
2419
2420 m_Core.m_EndlessHook = Enable;
2421}
2422
2423void CCharacter::Pause(bool Pause)
2424{
2425 m_Paused = Pause;
2426 if(Pause)
2427 {
2428 GameServer()->m_World.m_Core.m_apCharacters[m_pPlayer->GetCid()] = nullptr;
2429 GameServer()->m_World.RemoveEntity(pEntity: this);
2430
2431 if(m_Core.HookedPlayer() != -1) // Keeping hook would allow cheats
2432 {
2433 ResetHook();
2434 GameWorld()->ReleaseHooked(ClientId: GetPlayer()->GetCid());
2435 }
2436 m_PausedTick = Server()->Tick();
2437 }
2438 else
2439 {
2440 m_Core.m_Vel = vec2(0, 0);
2441 GameServer()->m_World.m_Core.m_apCharacters[m_pPlayer->GetCid()] = &m_Core;
2442 GameServer()->m_World.InsertEntity(pEntity: this);
2443 if(m_Core.m_FreezeStart > 0 && m_PausedTick >= 0)
2444 {
2445 m_Core.m_FreezeStart += Server()->Tick() - m_PausedTick;
2446 }
2447 }
2448}
2449
2450void CCharacter::DDRaceInit()
2451{
2452 m_Paused = false;
2453 m_DDRaceState = ERaceState::NONE;
2454 m_PrevPos = m_Pos;
2455 for(bool &Set : m_SetSavePos)
2456 Set = false;
2457 m_LastBroadcast = 0;
2458 m_TeamBeforeSuper = 0;
2459 m_Core.m_Id = GetPlayer()->GetCid();
2460 m_TeleCheckpoint = 0;
2461 m_Core.m_EndlessHook = g_Config.m_SvEndlessDrag;
2462 if(g_Config.m_SvHit)
2463 {
2464 m_Core.m_HammerHitDisabled = false;
2465 m_Core.m_ShotgunHitDisabled = false;
2466 m_Core.m_GrenadeHitDisabled = false;
2467 m_Core.m_LaserHitDisabled = false;
2468 }
2469 else
2470 {
2471 m_Core.m_HammerHitDisabled = true;
2472 m_Core.m_ShotgunHitDisabled = true;
2473 m_Core.m_GrenadeHitDisabled = true;
2474 m_Core.m_LaserHitDisabled = true;
2475 }
2476 m_Core.m_Jumps = 2;
2477
2478 int Team = Teams()->m_Core.Team(ClientId: m_Core.m_Id);
2479
2480 if(Teams()->TeamLocked(Team) && !Teams()->TeamFlock(Team))
2481 {
2482 for(int i = 0; i < MAX_CLIENTS; i++)
2483 {
2484 if(Teams()->m_Core.Team(ClientId: i) == Team && i != m_Core.m_Id && GameServer()->m_apPlayers[i])
2485 {
2486 CCharacter *pChar = GameServer()->m_apPlayers[i]->GetCharacter();
2487
2488 if(pChar)
2489 {
2490 m_DDRaceState = pChar->m_DDRaceState;
2491 m_StartTime = pChar->m_StartTime;
2492 }
2493 }
2494 }
2495 }
2496
2497 if(g_Config.m_SvTeam == SV_TEAM_MANDATORY && Team == TEAM_FLOCK)
2498 {
2499 GameServer()->SendStartWarning(ClientId: GetPlayer()->GetCid(), pMessage: "Please join a team before you start");
2500 }
2501}
2502
2503void CCharacter::Rescue()
2504{
2505 if(m_SetSavePos[GetPlayer()->m_RescueMode] && !m_Core.m_Super && !m_Core.m_Invincible)
2506 {
2507 if(m_LastRescue + (int64_t)g_Config.m_SvRescueDelay * Server()->TickSpeed() > Server()->Tick() && !Teams()->IsPractice(Team: Team()))
2508 {
2509 char aBuf[256];
2510 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "You have to wait %d seconds until you can rescue yourself", (int)((m_LastRescue + (int64_t)g_Config.m_SvRescueDelay * Server()->TickSpeed() - Server()->Tick()) / Server()->TickSpeed()));
2511 GameServer()->SendChatTarget(To: GetPlayer()->GetCid(), pText: aBuf);
2512 return;
2513 }
2514
2515 m_LastRescue = Server()->Tick();
2516 int StartTime = m_StartTime;
2517 m_RescueTee[GetPlayer()->m_RescueMode].Load(pChr: this);
2518 // Don't load these from saved tee:
2519 m_Core.m_Vel = vec2(0, 0);
2520 m_Core.m_HookState = HOOK_IDLE;
2521 m_StartTime = StartTime;
2522 m_SavedInput.m_Direction = 0;
2523 m_SavedInput.m_Jump = 0;
2524 // simulate releasing the fire button
2525 if((m_SavedInput.m_Fire & 1) != 0)
2526 m_SavedInput.m_Fire++;
2527 m_SavedInput.m_Fire &= INPUT_STATE_MASK;
2528 m_SavedInput.m_Hook = 0;
2529 m_pPlayer->Pause(State: CPlayer::PAUSE_NONE, Force: true);
2530 }
2531}
2532
2533CClientMask CCharacter::TeamMask()
2534{
2535 return Teams()->TeamMask(Team: Team(), ExceptId: -1, Asker: GetPlayer()->GetCid());
2536}
2537
2538void CCharacter::SetPosition(const vec2 &Position)
2539{
2540 m_Core.m_Pos = Position;
2541}
2542
2543void CCharacter::Move(vec2 RelPos)
2544{
2545 m_Core.m_Pos += RelPos;
2546}
2547
2548void CCharacter::ResetVelocity()
2549{
2550 m_Core.m_Vel = vec2(0, 0);
2551}
2552
2553void CCharacter::SetVelocity(vec2 NewVelocity)
2554{
2555 m_Core.m_Vel = ClampVel(MoveRestriction: m_MoveRestrictions, Vel: NewVelocity);
2556}
2557
2558// The method is needed only to reproduce 'shotgun bug' ddnet#5258
2559// Use SetVelocity() instead.
2560void CCharacter::SetRawVelocity(vec2 NewVelocity)
2561{
2562 m_Core.m_Vel = NewVelocity;
2563}
2564
2565void CCharacter::AddVelocity(vec2 Addition)
2566{
2567 SetVelocity(m_Core.m_Vel + Addition);
2568}
2569
2570void CCharacter::ApplyMoveRestrictions()
2571{
2572 m_Core.m_Vel = ClampVel(MoveRestriction: m_MoveRestrictions, Vel: m_Core.m_Vel);
2573}
2574
2575void CCharacter::SwapClients(int Client1, int Client2)
2576{
2577 const int HookedPlayer = m_Core.HookedPlayer();
2578 m_Core.SetHookedPlayer(HookedPlayer == Client1 ? Client2 : (HookedPlayer == Client2 ? Client1 : HookedPlayer));
2579}
2580