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
4#include "gameworld.h"
5
6#include "entities/character.h"
7#include "entities/door.h"
8#include "entities/dragger.h"
9#include "entities/laser.h"
10#include "entities/pickup.h"
11#include "entities/plasma.h"
12#include "entities/projectile.h"
13#include "entity.h"
14
15#include <engine/shared/config.h>
16
17#include <game/client/laser_data.h>
18#include <game/client/pickup_data.h>
19#include <game/client/projectile_data.h>
20#include <game/mapbugs.h>
21#include <game/mapitems.h>
22
23#include <algorithm>
24#include <utility>
25
26//////////////////////////////////////////////////
27// game world
28//////////////////////////////////////////////////
29CGameWorld::CGameWorld()
30{
31 for(auto &pFirstEntityType : m_apFirstEntityTypes)
32 pFirstEntityType = nullptr;
33 for(auto &pCharacter : m_apCharacters)
34 pCharacter = nullptr;
35 m_pCollision = nullptr;
36 m_GameTick = 0;
37 m_pParent = nullptr;
38 m_pChild = nullptr;
39}
40
41CGameWorld::~CGameWorld()
42{
43 Clear();
44 if(m_pChild && m_pChild->m_pParent == this)
45 {
46 OnModified();
47 m_pChild->m_pParent = nullptr;
48 }
49 if(m_pParent && m_pParent->m_pChild == this)
50 m_pParent->m_pChild = nullptr;
51}
52
53void CGameWorld::Init(CCollision *pCollision, CTuningParams *pTuningList, const CMapBugs *pMapBugs)
54{
55 m_pCollision = pCollision;
56 m_pTuningList = pTuningList;
57 m_pMapBugs = pMapBugs;
58}
59
60CEntity *CGameWorld::FindFirst(int Type)
61{
62 return Type < 0 || Type >= NUM_ENTTYPES ? nullptr : m_apFirstEntityTypes[Type];
63}
64
65CEntity *CGameWorld::FindLast(int Type)
66{
67 CEntity *pLast = FindFirst(Type);
68 if(pLast)
69 while(pLast->TypeNext())
70 pLast = pLast->TypeNext();
71 return pLast;
72}
73
74int CGameWorld::FindEntities(vec2 Pos, float Radius, CEntity **ppEnts, int Max, int Type)
75{
76 if(Type < 0 || Type >= NUM_ENTTYPES)
77 return 0;
78
79 int Num = 0;
80 for(CEntity *pEnt = m_apFirstEntityTypes[Type]; pEnt; pEnt = pEnt->m_pNextTypeEntity)
81 {
82 if(distance(a: pEnt->m_Pos, b: Pos) < Radius + pEnt->m_ProximityRadius)
83 {
84 if(ppEnts)
85 ppEnts[Num] = pEnt;
86 Num++;
87 if(Num == Max)
88 break;
89 }
90 }
91
92 return Num;
93}
94
95void CGameWorld::InsertEntity(CEntity *pEnt, bool Last)
96{
97 pEnt->m_pGameWorld = this;
98 pEnt->m_pNextTypeEntity = nullptr;
99 pEnt->m_pPrevTypeEntity = nullptr;
100
101 // insert it
102 if(!Last)
103 {
104 if(m_apFirstEntityTypes[pEnt->m_ObjType])
105 m_apFirstEntityTypes[pEnt->m_ObjType]->m_pPrevTypeEntity = pEnt;
106 pEnt->m_pNextTypeEntity = m_apFirstEntityTypes[pEnt->m_ObjType];
107 pEnt->m_pPrevTypeEntity = nullptr;
108 m_apFirstEntityTypes[pEnt->m_ObjType] = pEnt;
109 }
110 else
111 {
112 // insert it at the end of the list
113 CEntity *pLast = m_apFirstEntityTypes[pEnt->m_ObjType];
114 if(pLast)
115 {
116 while(pLast->m_pNextTypeEntity)
117 pLast = pLast->m_pNextTypeEntity;
118 pLast->m_pNextTypeEntity = pEnt;
119 }
120 else
121 m_apFirstEntityTypes[pEnt->m_ObjType] = pEnt;
122 pEnt->m_pPrevTypeEntity = pLast;
123 pEnt->m_pNextTypeEntity = nullptr;
124 }
125
126 if(pEnt->m_ObjType == ENTTYPE_CHARACTER)
127 {
128 auto *pChar = (CCharacter *)pEnt;
129 int Id = pChar->GetCid();
130 if(Id >= 0 && Id < MAX_CLIENTS)
131 {
132 m_apCharacters[Id] = pChar;
133 m_Core.m_apCharacters[Id] = &pChar->m_Core;
134 }
135 pChar->SetCoreWorld(this);
136 }
137}
138
139void CGameWorld::RemoveEntity(CEntity *pEnt)
140{
141 // not in the list
142 if(!pEnt->m_pNextTypeEntity && !pEnt->m_pPrevTypeEntity && m_apFirstEntityTypes[pEnt->m_ObjType] != pEnt)
143 return;
144
145 // remove
146 if(pEnt->m_pPrevTypeEntity)
147 pEnt->m_pPrevTypeEntity->m_pNextTypeEntity = pEnt->m_pNextTypeEntity;
148 else
149 m_apFirstEntityTypes[pEnt->m_ObjType] = pEnt->m_pNextTypeEntity;
150 if(pEnt->m_pNextTypeEntity)
151 pEnt->m_pNextTypeEntity->m_pPrevTypeEntity = pEnt->m_pPrevTypeEntity;
152
153 // keep list traversing valid
154 if(m_pNextTraverseEntity == pEnt)
155 m_pNextTraverseEntity = pEnt->m_pNextTypeEntity;
156
157 pEnt->m_pNextTypeEntity = nullptr;
158 pEnt->m_pPrevTypeEntity = nullptr;
159
160 if(pEnt->m_pParent)
161 {
162 if(m_IsValidCopy && m_pParent && m_pParent->m_pChild == this)
163 pEnt->m_pParent->m_DestroyTick = GameTick();
164 pEnt->m_pParent->m_pChild = nullptr;
165 pEnt->m_pParent = nullptr;
166 }
167 if(pEnt->m_pChild)
168 {
169 pEnt->m_pChild->m_pParent = nullptr;
170 pEnt->m_pChild = nullptr;
171 }
172}
173
174void CGameWorld::RemoveCharacter(CCharacter *pChar)
175{
176 int Id = pChar->GetCid();
177 if(Id >= 0 && Id < MAX_CLIENTS)
178 {
179 m_apCharacters[Id] = nullptr;
180 m_Core.m_apCharacters[Id] = nullptr;
181 }
182}
183
184void CGameWorld::RemoveEntities()
185{
186 // destroy objects marked for destruction
187 for(auto *pEnt : m_apFirstEntityTypes)
188 for(; pEnt;)
189 {
190 m_pNextTraverseEntity = pEnt->m_pNextTypeEntity;
191 if(pEnt->m_MarkedForDestroy)
192 {
193 pEnt->Destroy();
194 }
195 pEnt = m_pNextTraverseEntity;
196 }
197}
198
199void CGameWorld::Tick()
200{
201 // update all objects
202 for(int i = 0; i < NUM_ENTTYPES; i++)
203 {
204 // It's important to call PreTick() and Tick() after each other.
205 // If we call PreTick() before, and Tick() after other entities have been processed, it causes physics changes such as a stronger shotgun or grenade.
206 if(m_WorldConfig.m_NoWeakHookAndBounce && i == ENTTYPE_CHARACTER)
207 {
208 auto *pEnt = m_apFirstEntityTypes[i];
209 for(; pEnt;)
210 {
211 m_pNextTraverseEntity = pEnt->m_pNextTypeEntity;
212 ((CCharacter *)pEnt)->PreTick();
213 pEnt = m_pNextTraverseEntity;
214 }
215 }
216
217 auto *pEnt = m_apFirstEntityTypes[i];
218 for(; pEnt;)
219 {
220 m_pNextTraverseEntity = pEnt->m_pNextTypeEntity;
221 pEnt->Tick();
222 pEnt = m_pNextTraverseEntity;
223 }
224 }
225
226 for(auto *pEnt : m_apFirstEntityTypes)
227 for(; pEnt;)
228 {
229 m_pNextTraverseEntity = pEnt->m_pNextTypeEntity;
230 pEnt->TickDeferred();
231 pEnt->m_SnapTicks++;
232 pEnt = m_pNextTraverseEntity;
233 }
234
235 RemoveEntities();
236
237 // update switch state
238 for(auto &Switcher : Switchers())
239 {
240 for(int j = 0; j < NUM_DDRACE_TEAMS; ++j)
241 {
242 if(Switcher.m_aEndTick[j] <= GameTick() && Switcher.m_aType[j] == TILE_SWITCHTIMEDOPEN)
243 {
244 Switcher.m_aStatus[j] = false;
245 Switcher.m_aEndTick[j] = 0;
246 Switcher.m_aType[j] = TILE_SWITCHCLOSE;
247 }
248 else if(Switcher.m_aEndTick[j] <= GameTick() && Switcher.m_aType[j] == TILE_SWITCHTIMEDCLOSE)
249 {
250 Switcher.m_aStatus[j] = true;
251 Switcher.m_aEndTick[j] = 0;
252 Switcher.m_aType[j] = TILE_SWITCHOPEN;
253 }
254 }
255 }
256
257 OnModified();
258}
259
260CCharacter *CGameWorld::IntersectCharacter(vec2 Pos0, vec2 Pos1, float Radius, vec2 &NewPos, const CCharacter *pNotThis, int CollideWith, const CCharacter *pThisOnly)
261{
262 return (CCharacter *)IntersectEntity(Pos0, Pos1, Radius, Type: ENTTYPE_CHARACTER, NewPos, pNotThis, CollideWith, pThisOnly);
263}
264
265CEntity *CGameWorld::IntersectEntity(vec2 Pos0, vec2 Pos1, float Radius, int Type, vec2 &NewPos, const CEntity *pNotThis, int CollideWith, const CEntity *pThisOnly)
266{
267 float ClosestLen = distance(a: Pos0, b: Pos1) * 100.0f;
268 CEntity *pClosest = nullptr;
269
270 CEntity *pEntity = FindFirst(Type);
271 for(; pEntity; pEntity = pEntity->TypeNext())
272 {
273 if(pEntity == pNotThis)
274 continue;
275
276 if(pThisOnly && pEntity != pThisOnly)
277 continue;
278
279 if(CollideWith != -1 && !pEntity->CanCollide(ClientId: CollideWith))
280 continue;
281
282 vec2 IntersectPos;
283 if(closest_point_on_line(line_pointA: Pos0, line_pointB: Pos1, target_point: pEntity->m_Pos, out_pos&: IntersectPos))
284 {
285 float Len = distance(a: pEntity->m_Pos, b: IntersectPos);
286 if(Len < pEntity->m_ProximityRadius + Radius)
287 {
288 Len = distance(a: Pos0, b: IntersectPos);
289 if(Len < ClosestLen)
290 {
291 NewPos = IntersectPos;
292 ClosestLen = Len;
293 pClosest = pEntity;
294 }
295 }
296 }
297 }
298
299 return pClosest;
300}
301
302std::vector<CCharacter *> CGameWorld::IntersectedCharacters(vec2 Pos0, vec2 Pos1, float Radius, const CEntity *pNotThis)
303{
304 std::vector<CCharacter *> vpCharacters;
305 CCharacter *pChr = (CCharacter *)FindFirst(Type: CGameWorld::ENTTYPE_CHARACTER);
306 for(; pChr; pChr = (CCharacter *)pChr->TypeNext())
307 {
308 if(pChr == pNotThis)
309 continue;
310
311 vec2 IntersectPos;
312 if(closest_point_on_line(line_pointA: Pos0, line_pointB: Pos1, target_point: pChr->m_Pos, out_pos&: IntersectPos))
313 {
314 float Len = distance(a: pChr->m_Pos, b: IntersectPos);
315 if(Len < pChr->m_ProximityRadius + Radius)
316 {
317 vpCharacters.push_back(x: pChr);
318 }
319 }
320 }
321 return vpCharacters;
322}
323
324void CGameWorld::ReleaseHooked(int ClientId)
325{
326 CCharacter *pChr = (CCharacter *)CGameWorld::FindFirst(Type: CGameWorld::ENTTYPE_CHARACTER);
327 for(; pChr; pChr = (CCharacter *)pChr->TypeNext())
328 {
329 if(pChr->Core()->HookedPlayer() == ClientId && !pChr->IsSuper())
330 {
331 pChr->ReleaseHook();
332 }
333 }
334}
335
336CEntity *CGameWorld::GetEntity(int Id, int EntityType)
337{
338 for(CEntity *pEnt = m_apFirstEntityTypes[EntityType]; pEnt; pEnt = pEnt->m_pNextTypeEntity)
339 if(pEnt->m_Id == Id)
340 return pEnt;
341 return nullptr;
342}
343
344void CGameWorld::CreateExplosion(vec2 Pos, int Owner, int Weapon, bool NoDamage, int ActivatedTeam, CClientMask Mask, int Id)
345{
346 if(Owner < 0 && m_WorldConfig.m_IsSolo && !(Weapon == WEAPON_SHOTGUN && m_WorldConfig.m_IsDDRace))
347 return;
348
349 if(m_WorldConfig.m_IsDDRace && m_WorldConfig.m_PredictDDRace)
350 {
351 // vanilla has different projectile physics
352 CreatePredictedExplosionEvent(Pos, Id);
353 }
354
355 // deal damage
356 CEntity *apEnts[MAX_CLIENTS];
357 float Radius = 135.0f;
358 float InnerRadius = 48.0f;
359 int Num = FindEntities(Pos, Radius, ppEnts: apEnts, Max: MAX_CLIENTS, Type: CGameWorld::ENTTYPE_CHARACTER);
360 for(int i = 0; i < Num; i++)
361 {
362 auto *pChar = static_cast<CCharacter *>(apEnts[i]);
363 vec2 Diff = pChar->m_Pos - Pos;
364 vec2 ForceDir(0, 1);
365 float l = length(a: Diff);
366 if(l)
367 ForceDir = normalize(v: Diff);
368 l = 1 - std::clamp(val: (l - InnerRadius) / (Radius - InnerRadius), lo: 0.0f, hi: 1.0f);
369 float Strength;
370 if(Owner == -1 || !GetCharacterById(Id: Owner))
371 Strength = GlobalTuning()->m_ExplosionStrength;
372 else
373 Strength = GetCharacterById(Id: Owner)->GetTuning(i: GetCharacterById(Id: Owner)->GetOverriddenTuneZone())->m_ExplosionStrength;
374
375 float Dmg = Strength * l;
376 if((int)Dmg)
377 if((GetCharacterById(Id: Owner) ? !GetCharacterById(Id: Owner)->GrenadeHitDisabled() : g_Config.m_SvHit || NoDamage) || Owner == pChar->GetCid())
378 {
379 if(Owner != -1 && !pChar->CanCollide(ClientId: Owner))
380 continue;
381 if(Owner == -1 && ActivatedTeam != -1 && pChar->Team() != ActivatedTeam)
382 continue;
383 pChar->TakeDamage(Force: ForceDir * Dmg * 2, Dmg: (int)Dmg, From: Owner, Weapon);
384 if(GetCharacterById(Id: Owner) ? GetCharacterById(Id: Owner)->GrenadeHitDisabled() : !g_Config.m_SvHit || NoDamage)
385 break;
386 }
387 }
388}
389
390bool CGameWorld::IsLocalTeam(int OwnerId) const
391{
392 return OwnerId < 0 || m_Teams.CanCollide(ClientId1: m_LocalClientId, ClientId2: OwnerId);
393}
394
395void CGameWorld::NetObjBegin(CTeamsCore Teams, int LocalClientId)
396{
397 m_Teams = Teams;
398 m_LocalClientId = LocalClientId;
399
400 for(int i = 0; i < NUM_ENTTYPES; i++)
401 for(CEntity *pEnt = FindFirst(Type: i); pEnt; pEnt = pEnt->TypeNext())
402 {
403 pEnt->m_MarkedForDestroy = true;
404 if(i == ENTTYPE_CHARACTER)
405 ((CCharacter *)pEnt)->m_KeepHooked = false;
406 }
407 OnModified();
408}
409
410void CGameWorld::NetCharAdd(int ObjId, CNetObj_Character *pCharObj, CNetObj_DDNetCharacter *pExtended, int GameTeam, bool IsLocal)
411{
412 if(IsLocalTeam(OwnerId: ObjId))
413 {
414 CCharacter *pChar;
415 if((pChar = (CCharacter *)GetEntity(Id: ObjId, EntityType: ENTTYPE_CHARACTER)))
416 {
417 pChar->Read(pChar: pCharObj, pExtended, IsLocal);
418 pChar->Keep();
419 }
420 else
421 {
422 pChar = new CCharacter(this, ObjId, pCharObj, pExtended);
423 InsertEntity(pEnt: pChar);
424 }
425
426 if(pChar)
427 pChar->m_GameTeam = GameTeam;
428 }
429}
430
431void CGameWorld::NetObjAdd(int ObjId, int ObjType, const void *pObjData, const CNetObj_EntityEx *pDataEx)
432{
433 if((ObjType == NETOBJTYPE_PROJECTILE || ObjType == NETOBJTYPE_DDRACEPROJECTILE || ObjType == NETOBJTYPE_DDNETPROJECTILE) && m_WorldConfig.m_PredictWeapons)
434 {
435 CProjectileData Data = ExtractProjectileInfo(NetObjType: ObjType, pData: pObjData, pGameWorld: this, pEntEx: pDataEx);
436 if(!IsLocalTeam(OwnerId: Data.m_Owner))
437 return;
438
439 CProjectile NetProj = CProjectile(this, ObjId, &Data);
440
441 if(NetProj.m_Type != WEAPON_SHOTGUN && absolute(a: length(a: NetProj.m_Direction) - 1.f) > 0.02f) // workaround to skip grenades on ball mod
442 return;
443
444 if(CProjectile *pProj = (CProjectile *)GetEntity(Id: ObjId, EntityType: ENTTYPE_PROJECTILE))
445 {
446 if(NetProj.Match(pProj))
447 {
448 pProj->Keep();
449 if(pProj->m_Type == WEAPON_SHOTGUN && m_WorldConfig.m_IsDDRace)
450 pProj->m_LifeSpan = 20 * GameTickSpeed() - (GameTick() - pProj->m_StartTick);
451 return;
452 }
453 }
454 if(!Data.m_ExtraInfo)
455 {
456 // try to match the newly received (unrecognized) projectile with a locally fired one
457 for(CProjectile *pProj = (CProjectile *)FindFirst(Type: CGameWorld::ENTTYPE_PROJECTILE); pProj; pProj = (CProjectile *)pProj->TypeNext())
458 {
459 if(pProj->m_Id == -1 && NetProj.Match(pProj))
460 {
461 pProj->m_Id = ObjId;
462 pProj->Keep();
463 return;
464 }
465 }
466 // otherwise try to determine its owner by checking if there is only one player nearby
467 if(NetProj.m_StartTick >= GameTick() - 4)
468 {
469 const vec2 NetPos = NetProj.m_Pos - normalize(v: NetProj.m_Direction) * CCharacterCore::PhysicalSize() * 0.75;
470 const bool Prev = (GameTick() - NetProj.m_StartTick) > 1;
471 float First = 200.0f, Second = 200.0f;
472 CCharacter *pClosest = nullptr;
473 for(CCharacter *pChar = (CCharacter *)FindFirst(Type: ENTTYPE_CHARACTER); pChar; pChar = (CCharacter *)pChar->TypeNext())
474 {
475 float Dist = distance(a: Prev ? pChar->m_PrevPrevPos : pChar->m_PrevPos, b: NetPos);
476 if(Dist < First)
477 {
478 pClosest = pChar;
479 First = Dist;
480 }
481 else if(Dist < Second)
482 Second = Dist;
483 }
484 if(pClosest && maximum(a: First, b: 2.f) * 1.2f < Second)
485 NetProj.m_Owner = pClosest->m_Id;
486 }
487 }
488 CProjectile *pProj = new CProjectile(NetProj);
489 InsertEntity(pEnt: pProj);
490 }
491 else if((ObjType == NETOBJTYPE_PICKUP || ObjType == NETOBJTYPE_DDNETPICKUP) && m_WorldConfig.m_PredictWeapons)
492 {
493 CPickupData Data = ExtractPickupInfo(NetObjType: ObjType, pData: pObjData, pEntEx: pDataEx);
494 if(Data.m_Flags & PICKUPFLAG_NO_PREDICT)
495 return;
496 CPickup NetPickup = CPickup(this, ObjId, &Data);
497 if(CPickup *pPickup = (CPickup *)GetEntity(Id: ObjId, EntityType: ENTTYPE_PICKUP))
498 {
499 if(NetPickup.Match(pPickup))
500 {
501 pPickup->m_Pos = NetPickup.m_Pos;
502 pPickup->Keep();
503 return;
504 }
505 }
506 CEntity *pEnt = new CPickup(NetPickup);
507 InsertEntity(pEnt, Last: true);
508 }
509 else if((ObjType == NETOBJTYPE_LASER || ObjType == NETOBJTYPE_DDNETLASER) && m_WorldConfig.m_PredictWeapons)
510 {
511 CLaserData Data = ExtractLaserInfo(NetObjType: ObjType, pData: pObjData, pGameWorld: this, pEntEx: pDataEx);
512 if(!IsLocalTeam(OwnerId: Data.m_Owner) || !Data.m_Predict)
513 {
514 return;
515 }
516
517 if(Data.m_Type == LASERTYPE_RIFLE || Data.m_Type == LASERTYPE_SHOTGUN || Data.m_Type < 0)
518 {
519 CLaser NetLaser = CLaser(this, ObjId, &Data);
520 CLaser *pMatching = nullptr;
521 if(CLaser *pLaser = dynamic_cast<CLaser *>(GetEntity(Id: ObjId, EntityType: ENTTYPE_LASER)))
522 if(NetLaser.Match(pLaser))
523 pMatching = pLaser;
524 if(!pMatching)
525 {
526 for(CEntity *pEnt = FindFirst(Type: CGameWorld::ENTTYPE_LASER); pEnt; pEnt = pEnt->TypeNext())
527 {
528 auto *const pLaser = dynamic_cast<CLaser *>(pEnt);
529 if(pLaser && pLaser->m_Id == -1 && NetLaser.Match(pLaser))
530 {
531 pMatching = pLaser;
532 pMatching->m_Id = ObjId;
533 break;
534 }
535 }
536 }
537 if(pMatching)
538 {
539 pMatching->Keep();
540 if(distance(a: NetLaser.m_From, b: NetLaser.m_Pos) < distance(a: pMatching->m_From, b: pMatching->m_Pos) - 2.f)
541 {
542 // if the laser stopped earlier than predicted, set the energy to 0
543 pMatching->m_Energy = 0.f;
544 pMatching->m_Pos = NetLaser.m_Pos;
545 }
546 }
547 }
548 else if(Data.m_Type == LASERTYPE_DRAGGER)
549 {
550 CDragger NetDragger = CDragger(this, ObjId, &Data);
551 if(NetDragger.GetStrength() > 0)
552 {
553 auto *pDragger = dynamic_cast<CDragger *>(GetEntity(Id: ObjId, EntityType: ENTTYPE_DRAGGER));
554 if(pDragger && NetDragger.Match(pDragger))
555 {
556 pDragger->Keep();
557 pDragger->Read(pData: &Data);
558 return;
559 }
560 CEntity *pEnt = new CDragger(NetDragger);
561 InsertEntity(pEnt);
562 }
563 }
564 else if(Data.m_Type == LASERTYPE_DOOR)
565 {
566 CDoor NetDoor = CDoor(this, ObjId, &Data);
567 auto *pDoor = dynamic_cast<CDoor *>(GetEntity(Id: ObjId, EntityType: ENTTYPE_DOOR));
568 if(pDoor && NetDoor.Match(pDoor))
569 {
570 pDoor->Keep();
571 pDoor->Read(pData: &Data);
572 return;
573 }
574 CDoor *pEnt = new CDoor(NetDoor);
575 pEnt->ResetCollision();
576 InsertEntity(pEnt);
577 }
578 else if(Data.m_Type == LASERTYPE_PLASMA)
579 {
580 CPlasma NetPlasma = CPlasma(this, ObjId, &Data);
581 auto *pPlasma = dynamic_cast<CPlasma *>(GetEntity(Id: ObjId, EntityType: ENTTYPE_PLASMA));
582 if(pPlasma && NetPlasma.Match(pPlasma))
583 {
584 pPlasma->Keep();
585 pPlasma->Read(pData: &Data);
586 return;
587 }
588 CPlasma *pEnt = new CPlasma(NetPlasma);
589 InsertEntity(pEnt);
590 }
591 }
592}
593
594void CGameWorld::NetObjEnd()
595{
596 // keep predicting hooked characters, based on hook position
597 for(int i = 0; i < MAX_CLIENTS; i++)
598 if(CCharacter *pChar = GetCharacterById(Id: i))
599 if(!pChar->m_MarkedForDestroy)
600 if(CCharacter *pHookedChar = GetCharacterById(Id: pChar->m_Core.HookedPlayer()))
601 if(pHookedChar->m_MarkedForDestroy)
602 {
603 pHookedChar->m_Pos = pHookedChar->m_Core.m_Pos = pChar->m_Core.m_HookPos;
604 pHookedChar->ResetVelocity();
605 mem_zero(block: &pHookedChar->m_SavedInput, size: sizeof(pHookedChar->m_SavedInput));
606 pHookedChar->m_SavedInput.m_TargetY = -1;
607 pHookedChar->m_KeepHooked = true;
608 pHookedChar->m_MarkedForDestroy = false;
609 }
610 RemoveEntities();
611
612 // Update character IDs and pointers
613 for(int i = 0; i < MAX_CLIENTS; i++)
614 {
615 m_apCharacters[i] = nullptr;
616 m_Core.m_apCharacters[i] = nullptr;
617 }
618 for(CCharacter *pChar = (CCharacter *)FindFirst(Type: ENTTYPE_CHARACTER); pChar; pChar = (CCharacter *)pChar->TypeNext())
619 {
620 int Id = pChar->GetCid();
621 if(Id >= 0 && Id < MAX_CLIENTS)
622 {
623 m_apCharacters[Id] = pChar;
624 m_Core.m_apCharacters[Id] = &pChar->m_Core;
625 }
626 }
627}
628
629void CGameWorld::CopyWorld(CGameWorld *pFrom)
630{
631 if(pFrom == this || !pFrom)
632 return;
633 m_IsValidCopy = false;
634 m_pParent = pFrom;
635 if(m_pParent->m_pChild && m_pParent->m_pChild != this)
636 m_pParent->m_pChild->m_IsValidCopy = false;
637 pFrom->m_pChild = this;
638
639 m_GameTick = pFrom->m_GameTick;
640 m_pCollision = pFrom->m_pCollision;
641 m_WorldConfig = pFrom->m_WorldConfig;
642 m_pTuningList = pFrom->m_pTuningList;
643 m_pMapBugs = pFrom->m_pMapBugs;
644 m_Teams = pFrom->m_Teams;
645 m_Core.m_vSwitchers = pFrom->m_Core.m_vSwitchers;
646 m_PredictedEvents = pFrom->m_PredictedEvents;
647 // delete the previous entities
648 Clear();
649 for(int i = 0; i < MAX_CLIENTS; i++)
650 {
651 m_apCharacters[i] = nullptr;
652 m_Core.m_apCharacters[i] = nullptr;
653 }
654 // copy and add the new entities
655 for(int Type = 0; Type < NUM_ENTTYPES; Type++)
656 {
657 for(CEntity *pEnt = pFrom->FindLast(Type); pEnt; pEnt = pEnt->TypePrev())
658 {
659 CEntity *pCopy = nullptr;
660 if(Type == ENTTYPE_PROJECTILE)
661 pCopy = new CProjectile(*((CProjectile *)pEnt));
662 else if(Type == ENTTYPE_LASER)
663 pCopy = new CLaser(*((CLaser *)pEnt));
664 else if(Type == ENTTYPE_DRAGGER)
665 pCopy = new CDragger(*((CDragger *)pEnt));
666 else if(Type == ENTTYPE_CHARACTER)
667 pCopy = new CCharacter(*((CCharacter *)pEnt));
668 else if(Type == ENTTYPE_PICKUP)
669 pCopy = new CPickup(*((CPickup *)pEnt));
670 else if(Type == ENTTYPE_PLASMA)
671 pCopy = new CPlasma(*((CPlasma *)pEnt));
672 if(pCopy)
673 {
674 pCopy->m_pParent = pEnt;
675 pEnt->m_pChild = pCopy;
676 this->InsertEntity(pEnt: pCopy);
677 }
678 }
679 }
680 m_IsValidCopy = true;
681}
682
683CEntity *CGameWorld::FindMatch(int ObjId, int ObjType, const void *pObjData)
684{
685 switch(ObjType)
686 {
687 case NETOBJTYPE_CHARACTER:
688 {
689 CCharacter *pEnt = (CCharacter *)GetEntity(Id: ObjId, EntityType: ENTTYPE_CHARACTER);
690 if(pEnt && CCharacter(this, ObjId, (CNetObj_Character *)pObjData).Match(pChar: pEnt))
691 {
692 return pEnt;
693 }
694 return nullptr;
695 }
696 case NETOBJTYPE_PROJECTILE:
697 case NETOBJTYPE_DDRACEPROJECTILE:
698 case NETOBJTYPE_DDNETPROJECTILE:
699 {
700 CProjectileData Data = ExtractProjectileInfo(NetObjType: ObjType, pData: pObjData, pGameWorld: this, pEntEx: nullptr);
701 CProjectile *pEnt = (CProjectile *)GetEntity(Id: ObjId, EntityType: ENTTYPE_PROJECTILE);
702 if(pEnt && CProjectile(this, ObjId, &Data).Match(pProj: pEnt))
703 {
704 return pEnt;
705 }
706 return nullptr;
707 }
708 case NETOBJTYPE_LASER:
709 case NETOBJTYPE_DDNETLASER:
710 {
711 CLaserData Data = ExtractLaserInfo(NetObjType: ObjType, pData: pObjData, pGameWorld: this, pEntEx: nullptr);
712 if(Data.m_Type == LASERTYPE_RIFLE || Data.m_Type == LASERTYPE_SHOTGUN)
713 {
714 CLaser *pEnt = (CLaser *)GetEntity(Id: ObjId, EntityType: ENTTYPE_LASER);
715 if(pEnt && CLaser(this, ObjId, &Data).Match(pLaser: pEnt))
716 {
717 return pEnt;
718 }
719 }
720 else if(Data.m_Type == LASERTYPE_DRAGGER)
721 {
722 CDragger *pEnt = (CDragger *)GetEntity(Id: ObjId, EntityType: ENTTYPE_DRAGGER);
723 if(pEnt && CDragger(this, ObjId, &Data).Match(pDragger: pEnt))
724 {
725 return pEnt;
726 }
727 }
728 else if(Data.m_Type == LASERTYPE_DOOR)
729 {
730 CDoor *pEnt = (CDoor *)GetEntity(Id: ObjId, EntityType: ENTTYPE_DOOR);
731 if(pEnt && CDoor(this, ObjId, &Data).Match(pDoor: pEnt))
732 {
733 return pEnt;
734 }
735 }
736 else if(Data.m_Type == LASERTYPE_PLASMA)
737 {
738 CPlasma *pEnt = (CPlasma *)GetEntity(Id: ObjId, EntityType: ENTTYPE_PLASMA);
739 if(pEnt && CPlasma(this, ObjId, &Data).Match(pPlasma: pEnt))
740 {
741 return pEnt;
742 }
743 }
744 return nullptr;
745 }
746 case NETOBJTYPE_PICKUP:
747 case NETOBJTYPE_DDNETPICKUP:
748 {
749 CPickupData Data = ExtractPickupInfo(NetObjType: ObjType, pData: pObjData, pEntEx: nullptr);
750 CPickup *pEnt = (CPickup *)GetEntity(Id: ObjId, EntityType: ENTTYPE_PICKUP);
751 if(pEnt && CPickup(this, ObjId, &Data).Match(pPickup: pEnt))
752 {
753 return pEnt;
754 }
755 return nullptr;
756 }
757 }
758 return nullptr;
759}
760
761void CGameWorld::OnModified() const
762{
763 if(m_pChild)
764 m_pChild->m_IsValidCopy = false;
765}
766
767void CGameWorld::Clear()
768{
769 // delete all entities
770 for(auto &pFirstEntityType : m_apFirstEntityTypes)
771 while(pFirstEntityType)
772 delete pFirstEntityType; // NOLINT(clang-analyzer-cplusplus.NewDelete)
773}
774
775bool CGameWorld::EmulateBug(int Bug) const
776{
777 return m_pMapBugs->Contains(Bug);
778}
779
780void CGameWorld::CreatePredictedEvent(const CPredictedEvent &NewEvent)
781{
782 if(!g_Config.m_ClPredictEvents || !m_WorldConfig.m_PredictEvents)
783 return;
784
785 // prediction is ran multiple times per tick, check if event already exists
786 const auto It = std::find_if(
787 first: m_PredictedEvents.begin(),
788 last: m_PredictedEvents.end(),
789 pred: [NewEvent](const CPredictedEvent &Event) {
790 return Event.m_EventId == NewEvent.m_EventId && Event.m_ExtraInfo == NewEvent.m_ExtraInfo &&
791 Event.m_Pos == NewEvent.m_Pos && Event.m_Id == NewEvent.m_Id && Event.m_Tick == NewEvent.m_Tick;
792 });
793
794 if(It == m_PredictedEvents.end())
795 {
796 m_PredictedEvents.push_back(x: NewEvent);
797 }
798}
799
800bool CGameWorld::CheckPredictedEventHandled(const CPredictedEvent &CheckEvent)
801{
802 // events could be delayed by ping, so don't check for exact tick match
803 // also received events don't have Id
804 auto It = std::find_if(
805 first: m_PredictedEvents.begin(),
806 last: m_PredictedEvents.end(),
807 pred: [CheckEvent](const CPredictedEvent &Event) {
808 return Event.m_Handled == true && Event.m_EventId == CheckEvent.m_EventId &&
809 Event.m_Pos == CheckEvent.m_Pos && Event.m_Tick <= CheckEvent.m_Tick && Event.m_ExtraInfo == CheckEvent.m_ExtraInfo;
810 });
811
812 if(It == m_PredictedEvents.end())
813 {
814 return false;
815 }
816
817 // remove the event after it has been confirmed played
818 m_PredictedEvents.erase(position: It);
819 return true;
820}
821
822void CGameWorld::CreatePredictedSound(vec2 Pos, int SoundId, int Id)
823{
824 if(!g_Config.m_SndEnable)
825 return;
826
827 CPredictedEvent Event(NETEVENTTYPE_SOUNDWORLD, Pos, Id, GameTick(), SoundId);
828 CreatePredictedEvent(NewEvent: Event);
829}
830
831void CGameWorld::CreatePredictedExplosionEvent(vec2 Pos, int Id)
832{
833 CPredictedEvent Event(NETEVENTTYPE_EXPLOSION, Pos, Id, GameTick());
834 CreatePredictedEvent(NewEvent: Event);
835}
836
837void CGameWorld::CreatePredictedHammerHitEvent(vec2 Pos, int Id)
838{
839 CPredictedEvent Event(NETEVENTTYPE_HAMMERHIT, Pos, Id, GameTick());
840 CreatePredictedEvent(NewEvent: Event);
841}
842
843void CGameWorld::CreatePredictedDamageIndEvent(vec2 Pos, float Angle, int Amount, int Id)
844{
845 float a = 3 * pi / 2 + Angle;
846 float s = a - pi / 3;
847 float e = a + pi / 3;
848 for(int i = 0; i < Amount; i++)
849 {
850 float f = mix(a: s, b: e, amount: (i + 1) / (float)(Amount + 1));
851
852 CPredictedEvent Event(NETEVENTTYPE_DAMAGEIND, Pos, Id, GameTick(), (int)(f * 256.0f));
853 CreatePredictedEvent(NewEvent: Event);
854 }
855}
856