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