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 if(Owner == -1 || !GetCharacterById(Id: Owner))
373 Strength = GlobalTuning()->m_ExplosionStrength;
374 else
375 Strength = GetCharacterById(Id: Owner)->GetTuning(i: GetCharacterById(Id: Owner)->GetOverriddenTuneZone())->m_ExplosionStrength;
376
377 float Dmg = Strength * l;
378 if((int)Dmg)
379 if((GetCharacterById(Id: Owner) ? !GetCharacterById(Id: Owner)->GrenadeHitDisabled() : g_Config.m_SvHit || NoDamage) || Owner == pChar->GetCid())
380 {
381 if(Owner != -1 && !pChar->CanCollide(ClientId: Owner))
382 continue;
383 if(Owner == -1 && ActivatedTeam != -1 && pChar->Team() != ActivatedTeam)
384 continue;
385 pChar->TakeDamage(Force: ForceDir * Dmg * 2, Dmg: (int)Dmg, From: Owner, Weapon);
386 if(GetCharacterById(Id: Owner) ? GetCharacterById(Id: Owner)->GrenadeHitDisabled() : !g_Config.m_SvHit || NoDamage)
387 break;
388 }
389 }
390}
391
392bool CGameWorld::IsLocalTeam(int OwnerId) const
393{
394 return OwnerId < 0 || m_Teams.CanCollide(ClientId1: m_LocalClientId, ClientId2: OwnerId);
395}
396
397void CGameWorld::NetObjBegin(CTeamsCore Teams, int LocalClientId)
398{
399 m_Teams = Teams;
400 m_LocalClientId = LocalClientId;
401
402 for(int i = 0; i < NUM_ENTTYPES; i++)
403 for(CEntity *pEnt = FindFirst(Type: i); pEnt; pEnt = pEnt->TypeNext())
404 {
405 pEnt->m_MarkedForDestroy = true;
406 if(i == ENTTYPE_CHARACTER)
407 ((CCharacter *)pEnt)->m_KeepHooked = false;
408 }
409 OnModified();
410}
411
412void CGameWorld::NetCharAdd(int ObjId, CNetObj_Character *pCharObj, CNetObj_DDNetCharacter *pExtended, int GameTeam, bool IsLocal)
413{
414 if(IsLocalTeam(OwnerId: ObjId))
415 {
416 CCharacter *pChar;
417 if((pChar = (CCharacter *)GetEntity(Id: ObjId, EntityType: ENTTYPE_CHARACTER)))
418 {
419 pChar->Read(pChar: pCharObj, pExtended, IsLocal);
420 pChar->Keep();
421 }
422 else
423 {
424 pChar = new CCharacter(this, ObjId, pCharObj, pExtended);
425 InsertEntity(pEnt: pChar);
426 }
427
428 if(pChar)
429 pChar->m_GameTeam = GameTeam;
430 }
431}
432
433void CGameWorld::NetObjAdd(int ObjId, int ObjType, const void *pObjData, const CNetObj_EntityEx *pDataEx)
434{
435 if((ObjType == NETOBJTYPE_PROJECTILE || ObjType == NETOBJTYPE_DDRACEPROJECTILE || ObjType == NETOBJTYPE_DDNETPROJECTILE) && m_WorldConfig.m_PredictWeapons)
436 {
437 CProjectileData Data = ExtractProjectileInfo(NetObjType: ObjType, pData: pObjData, pGameWorld: this, pEntEx: pDataEx);
438 if(!IsLocalTeam(OwnerId: Data.m_Owner))
439 return;
440
441 CProjectile NetProj = CProjectile(this, ObjId, &Data);
442
443 if(NetProj.m_Type != WEAPON_SHOTGUN && absolute(a: length(a: NetProj.m_Direction) - 1.f) > 0.02f) // workaround to skip grenades on ball mod
444 return;
445
446 if(CProjectile *pProj = (CProjectile *)GetEntity(Id: ObjId, EntityType: ENTTYPE_PROJECTILE))
447 {
448 if(NetProj.Match(pProj))
449 {
450 pProj->Keep();
451 if(pProj->m_Type == WEAPON_SHOTGUN && m_WorldConfig.m_IsDDRace)
452 pProj->m_LifeSpan = 20 * GameTickSpeed() - (GameTick() - pProj->m_StartTick);
453 return;
454 }
455 }
456 if(!Data.m_ExtraInfo)
457 {
458 // try to match the newly received (unrecognized) projectile with a locally fired one
459 for(CProjectile *pProj = (CProjectile *)FindFirst(Type: CGameWorld::ENTTYPE_PROJECTILE); pProj; pProj = (CProjectile *)pProj->TypeNext())
460 {
461 if(pProj->m_Id == -1 && NetProj.Match(pProj))
462 {
463 pProj->m_Id = ObjId;
464 pProj->Keep();
465 return;
466 }
467 }
468 // otherwise try to determine its owner by checking if there is only one player nearby
469 if(NetProj.m_StartTick >= GameTick() - 4)
470 {
471 const vec2 NetPos = NetProj.m_Pos - normalize(v: NetProj.m_Direction) * CCharacterCore::PhysicalSize() * 0.75;
472 const bool Prev = (GameTick() - NetProj.m_StartTick) > 1;
473 float First = 200.0f, Second = 200.0f;
474 CCharacter *pClosest = nullptr;
475 for(CCharacter *pChar = (CCharacter *)FindFirst(Type: ENTTYPE_CHARACTER); pChar; pChar = (CCharacter *)pChar->TypeNext())
476 {
477 float Dist = distance(a: Prev ? pChar->m_PrevPrevPos : pChar->m_PrevPos, b: NetPos);
478 if(Dist < First)
479 {
480 pClosest = pChar;
481 First = Dist;
482 }
483 else if(Dist < Second)
484 {
485 Second = Dist;
486 }
487 }
488 if(pClosest && std::max(a: First, b: 2.0f) * 1.2f < Second)
489 NetProj.m_Owner = pClosest->m_Id;
490 }
491 }
492 CProjectile *pProj = new CProjectile(NetProj);
493 InsertEntity(pEnt: pProj);
494 }
495 else if((ObjType == NETOBJTYPE_PICKUP || ObjType == NETOBJTYPE_DDNETPICKUP) && m_WorldConfig.m_PredictWeapons)
496 {
497 CPickupData Data = ExtractPickupInfo(NetObjType: ObjType, pData: pObjData, pEntEx: pDataEx);
498 if(Data.m_Flags & PICKUPFLAG_NO_PREDICT)
499 return;
500 CPickup NetPickup = CPickup(this, ObjId, &Data);
501 if(CPickup *pPickup = (CPickup *)GetEntity(Id: ObjId, EntityType: ENTTYPE_PICKUP))
502 {
503 if(NetPickup.Match(pPickup))
504 {
505 pPickup->m_Pos = NetPickup.m_Pos;
506 pPickup->Keep();
507 return;
508 }
509 }
510 CEntity *pEnt = new CPickup(NetPickup);
511 InsertEntity(pEnt, Last: true);
512 }
513 else if((ObjType == NETOBJTYPE_LASER || ObjType == NETOBJTYPE_DDNETLASER) && m_WorldConfig.m_PredictWeapons)
514 {
515 CLaserData Data = ExtractLaserInfo(NetObjType: ObjType, pData: pObjData, pGameWorld: this, pEntEx: pDataEx);
516 if(!IsLocalTeam(OwnerId: Data.m_Owner) || !Data.m_Predict)
517 {
518 return;
519 }
520
521 if(Data.m_Type == LASERTYPE_RIFLE || Data.m_Type == LASERTYPE_SHOTGUN || Data.m_Type < 0)
522 {
523 CLaser NetLaser = CLaser(this, ObjId, &Data);
524 CLaser *pMatching = nullptr;
525 if(CLaser *pLaser = dynamic_cast<CLaser *>(GetEntity(Id: ObjId, EntityType: ENTTYPE_LASER)))
526 if(NetLaser.Match(pLaser))
527 pMatching = pLaser;
528 if(!pMatching)
529 {
530 for(CEntity *pEnt = FindFirst(Type: CGameWorld::ENTTYPE_LASER); pEnt; pEnt = pEnt->TypeNext())
531 {
532 auto *const pLaser = dynamic_cast<CLaser *>(pEnt);
533 if(pLaser && pLaser->m_Id == -1 && NetLaser.Match(pLaser))
534 {
535 pMatching = pLaser;
536 pMatching->m_Id = ObjId;
537 break;
538 }
539 }
540 }
541 if(pMatching)
542 {
543 pMatching->Keep();
544 if(distance(a: NetLaser.m_From, b: NetLaser.m_Pos) < distance(a: pMatching->m_From, b: pMatching->m_Pos) - 2.f)
545 {
546 // if the laser stopped earlier than predicted, set the energy to 0
547 pMatching->m_Energy = 0.f;
548 pMatching->m_Pos = NetLaser.m_Pos;
549 }
550 }
551 }
552 else if(Data.m_Type == LASERTYPE_DRAGGER)
553 {
554 CDragger NetDragger = CDragger(this, ObjId, &Data);
555 if(NetDragger.GetStrength() > 0)
556 {
557 auto *pDragger = dynamic_cast<CDragger *>(GetEntity(Id: ObjId, EntityType: ENTTYPE_DRAGGER));
558 if(pDragger && NetDragger.Match(pDragger))
559 {
560 pDragger->Keep();
561 pDragger->Read(pData: &Data);
562 return;
563 }
564 CEntity *pEnt = new CDragger(NetDragger);
565 InsertEntity(pEnt);
566 }
567 }
568 else if(Data.m_Type == LASERTYPE_DOOR)
569 {
570 CDoor NetDoor = CDoor(this, ObjId, &Data);
571 auto *pDoor = dynamic_cast<CDoor *>(GetEntity(Id: ObjId, EntityType: ENTTYPE_DOOR));
572 if(pDoor && NetDoor.Match(pDoor))
573 {
574 pDoor->Keep();
575 pDoor->Read(pData: &Data);
576 return;
577 }
578 CDoor *pEnt = new CDoor(NetDoor);
579 pEnt->ResetCollision();
580 InsertEntity(pEnt);
581 }
582 else if(Data.m_Type == LASERTYPE_PLASMA)
583 {
584 CPlasma NetPlasma = CPlasma(this, ObjId, &Data);
585 auto *pPlasma = dynamic_cast<CPlasma *>(GetEntity(Id: ObjId, EntityType: ENTTYPE_PLASMA));
586 if(pPlasma && NetPlasma.Match(pPlasma))
587 {
588 pPlasma->Keep();
589 pPlasma->Read(pData: &Data);
590 return;
591 }
592 CPlasma *pEnt = new CPlasma(NetPlasma);
593 InsertEntity(pEnt);
594 }
595 }
596}
597
598void CGameWorld::NetObjEnd()
599{
600 // keep predicting hooked characters, based on hook position
601 for(int i = 0; i < MAX_CLIENTS; i++)
602 if(CCharacter *pChar = GetCharacterById(Id: i))
603 if(!pChar->m_MarkedForDestroy)
604 if(CCharacter *pHookedChar = GetCharacterById(Id: pChar->m_Core.HookedPlayer()))
605 if(pHookedChar->m_MarkedForDestroy)
606 {
607 pHookedChar->m_Pos = pHookedChar->m_Core.m_Pos = pChar->m_Core.m_HookPos;
608 pHookedChar->ResetVelocity();
609 mem_zero(block: &pHookedChar->m_SavedInput, size: sizeof(pHookedChar->m_SavedInput));
610 pHookedChar->m_SavedInput.m_TargetY = -1;
611 pHookedChar->m_KeepHooked = true;
612 pHookedChar->m_MarkedForDestroy = false;
613 }
614 RemoveEntities();
615
616 // Update character IDs and pointers
617 for(int i = 0; i < MAX_CLIENTS; i++)
618 {
619 m_apCharacters[i] = nullptr;
620 m_Core.m_apCharacters[i] = nullptr;
621 }
622 for(CCharacter *pChar = (CCharacter *)FindFirst(Type: ENTTYPE_CHARACTER); pChar; pChar = (CCharacter *)pChar->TypeNext())
623 {
624 int Id = pChar->GetCid();
625 if(Id >= 0 && Id < MAX_CLIENTS)
626 {
627 m_apCharacters[Id] = pChar;
628 m_Core.m_apCharacters[Id] = &pChar->m_Core;
629 }
630 }
631}
632
633void CGameWorld::CopyWorld(CGameWorld *pFrom)
634{
635 if(pFrom == this || !pFrom)
636 return;
637 m_IsValidCopy = false;
638 m_pParent = pFrom;
639 if(m_pParent->m_pChild && m_pParent->m_pChild != this)
640 m_pParent->m_pChild->m_IsValidCopy = false;
641 pFrom->m_pChild = this;
642
643 m_GameTick = pFrom->m_GameTick;
644 m_pCollision = pFrom->m_pCollision;
645 m_WorldConfig = pFrom->m_WorldConfig;
646 m_pTuningList = pFrom->m_pTuningList;
647 m_pMapBugs = pFrom->m_pMapBugs;
648 m_Teams = pFrom->m_Teams;
649 m_Core.m_vSwitchers = pFrom->m_Core.m_vSwitchers;
650 m_PredictedEvents = pFrom->m_PredictedEvents;
651 // delete the previous entities
652 Clear();
653 for(int i = 0; i < MAX_CLIENTS; i++)
654 {
655 m_apCharacters[i] = nullptr;
656 m_Core.m_apCharacters[i] = nullptr;
657 }
658 // copy and add the new entities
659 for(int Type = 0; Type < NUM_ENTTYPES; Type++)
660 {
661 for(CEntity *pEnt = pFrom->FindLast(Type); pEnt; pEnt = pEnt->TypePrev())
662 {
663 CEntity *pCopy = nullptr;
664 if(Type == ENTTYPE_PROJECTILE)
665 pCopy = new CProjectile(*((CProjectile *)pEnt));
666 else if(Type == ENTTYPE_LASER)
667 pCopy = new CLaser(*((CLaser *)pEnt));
668 else if(Type == ENTTYPE_DRAGGER)
669 pCopy = new CDragger(*((CDragger *)pEnt));
670 else if(Type == ENTTYPE_CHARACTER)
671 pCopy = new CCharacter(*((CCharacter *)pEnt));
672 else if(Type == ENTTYPE_PICKUP)
673 pCopy = new CPickup(*((CPickup *)pEnt));
674 else if(Type == ENTTYPE_PLASMA)
675 pCopy = new CPlasma(*((CPlasma *)pEnt));
676 if(pCopy)
677 {
678 pCopy->m_pParent = pEnt;
679 pEnt->m_pChild = pCopy;
680 this->InsertEntity(pEnt: pCopy);
681 }
682 }
683 }
684 m_IsValidCopy = true;
685}
686
687CEntity *CGameWorld::FindMatch(int ObjId, int ObjType, const void *pObjData)
688{
689 switch(ObjType)
690 {
691 case NETOBJTYPE_CHARACTER:
692 {
693 CCharacter *pEnt = (CCharacter *)GetEntity(Id: ObjId, EntityType: ENTTYPE_CHARACTER);
694 if(pEnt && CCharacter(this, ObjId, (CNetObj_Character *)pObjData).Match(pChar: pEnt))
695 {
696 return pEnt;
697 }
698 return nullptr;
699 }
700 case NETOBJTYPE_PROJECTILE:
701 case NETOBJTYPE_DDRACEPROJECTILE:
702 case NETOBJTYPE_DDNETPROJECTILE:
703 {
704 CProjectileData Data = ExtractProjectileInfo(NetObjType: ObjType, pData: pObjData, pGameWorld: this, pEntEx: nullptr);
705 CProjectile *pEnt = (CProjectile *)GetEntity(Id: ObjId, EntityType: ENTTYPE_PROJECTILE);
706 if(pEnt && CProjectile(this, ObjId, &Data).Match(pProj: pEnt))
707 {
708 return pEnt;
709 }
710 return nullptr;
711 }
712 case NETOBJTYPE_LASER:
713 case NETOBJTYPE_DDNETLASER:
714 {
715 CLaserData Data = ExtractLaserInfo(NetObjType: ObjType, pData: pObjData, pGameWorld: this, pEntEx: nullptr);
716 if(Data.m_Type == LASERTYPE_RIFLE || Data.m_Type == LASERTYPE_SHOTGUN)
717 {
718 CLaser *pEnt = (CLaser *)GetEntity(Id: ObjId, EntityType: ENTTYPE_LASER);
719 if(pEnt && CLaser(this, ObjId, &Data).Match(pLaser: pEnt))
720 {
721 return pEnt;
722 }
723 }
724 else if(Data.m_Type == LASERTYPE_DRAGGER)
725 {
726 CDragger *pEnt = (CDragger *)GetEntity(Id: ObjId, EntityType: ENTTYPE_DRAGGER);
727 if(pEnt && CDragger(this, ObjId, &Data).Match(pDragger: pEnt))
728 {
729 return pEnt;
730 }
731 }
732 else if(Data.m_Type == LASERTYPE_DOOR)
733 {
734 CDoor *pEnt = (CDoor *)GetEntity(Id: ObjId, EntityType: ENTTYPE_DOOR);
735 if(pEnt && CDoor(this, ObjId, &Data).Match(pDoor: pEnt))
736 {
737 return pEnt;
738 }
739 }
740 else if(Data.m_Type == LASERTYPE_PLASMA)
741 {
742 CPlasma *pEnt = (CPlasma *)GetEntity(Id: ObjId, EntityType: ENTTYPE_PLASMA);
743 if(pEnt && CPlasma(this, ObjId, &Data).Match(pPlasma: pEnt))
744 {
745 return pEnt;
746 }
747 }
748 return nullptr;
749 }
750 case NETOBJTYPE_PICKUP:
751 case NETOBJTYPE_DDNETPICKUP:
752 {
753 CPickupData Data = ExtractPickupInfo(NetObjType: ObjType, pData: pObjData, pEntEx: nullptr);
754 CPickup *pEnt = (CPickup *)GetEntity(Id: ObjId, EntityType: ENTTYPE_PICKUP);
755 if(pEnt && CPickup(this, ObjId, &Data).Match(pPickup: pEnt))
756 {
757 return pEnt;
758 }
759 return nullptr;
760 }
761 }
762 return nullptr;
763}
764
765void CGameWorld::OnModified() const
766{
767 if(m_pChild)
768 m_pChild->m_IsValidCopy = false;
769}
770
771void CGameWorld::Clear()
772{
773 // delete all entities
774 for(auto &pFirstEntityType : m_apFirstEntityTypes)
775 while(pFirstEntityType)
776 delete pFirstEntityType; // NOLINT(clang-analyzer-cplusplus.NewDelete)
777}
778
779bool CGameWorld::EmulateBug(int Bug) const
780{
781 return m_pMapBugs->Contains(Bug);
782}
783
784void CGameWorld::CreatePredictedEvent(const CPredictedEvent &NewEvent)
785{
786 if(!g_Config.m_ClPredictEvents || !m_WorldConfig.m_PredictEvents)
787 return;
788
789 // prediction is ran multiple times per tick, check if event already exists
790 const auto It = std::find_if(
791 first: m_PredictedEvents.begin(),
792 last: m_PredictedEvents.end(),
793 pred: [NewEvent](const CPredictedEvent &Event) {
794 return Event.m_EventId == NewEvent.m_EventId && Event.m_ExtraInfo == NewEvent.m_ExtraInfo &&
795 Event.m_Pos == NewEvent.m_Pos && Event.m_Id == NewEvent.m_Id && Event.m_Tick == NewEvent.m_Tick;
796 });
797
798 if(It == m_PredictedEvents.end())
799 {
800 m_PredictedEvents.push_back(x: NewEvent);
801 }
802}
803
804bool CGameWorld::CheckPredictedEventHandled(const CPredictedEvent &CheckEvent)
805{
806 // events could be delayed by ping, so don't check for exact tick match
807 // also received events don't have Id
808 auto It = std::find_if(
809 first: m_PredictedEvents.begin(),
810 last: m_PredictedEvents.end(),
811 pred: [CheckEvent](const CPredictedEvent &Event) {
812 return Event.m_Handled == true && Event.m_EventId == CheckEvent.m_EventId &&
813 Event.m_Pos == CheckEvent.m_Pos && Event.m_Tick <= CheckEvent.m_Tick && Event.m_ExtraInfo == CheckEvent.m_ExtraInfo;
814 });
815
816 if(It == m_PredictedEvents.end())
817 {
818 return false;
819 }
820
821 // remove the event after it has been confirmed played
822 m_PredictedEvents.erase(position: It);
823 return true;
824}
825
826void CGameWorld::CreatePredictedSound(vec2 Pos, int SoundId, int Id)
827{
828 if(!g_Config.m_SndEnable)
829 return;
830
831 CPredictedEvent Event(NETEVENTTYPE_SOUNDWORLD, Pos, Id, GameTick(), SoundId);
832 CreatePredictedEvent(NewEvent: Event);
833}
834
835void CGameWorld::CreatePredictedExplosionEvent(vec2 Pos, int Id)
836{
837 CPredictedEvent Event(NETEVENTTYPE_EXPLOSION, Pos, Id, GameTick());
838 CreatePredictedEvent(NewEvent: Event);
839}
840
841void CGameWorld::CreatePredictedHammerHitEvent(vec2 Pos, int Id)
842{
843 CPredictedEvent Event(NETEVENTTYPE_HAMMERHIT, Pos, Id, GameTick());
844 CreatePredictedEvent(NewEvent: Event);
845}
846
847void CGameWorld::CreatePredictedDamageIndEvent(vec2 Pos, float Angle, int Amount, int Id)
848{
849 float a = 3 * pi / 2 + Angle;
850 float s = a - pi / 3;
851 float e = a + pi / 3;
852 for(int i = 0; i < Amount; i++)
853 {
854 float f = mix(a: s, b: e, amount: (i + 1) / (float)(Amount + 1));
855
856 CPredictedEvent Event(NETEVENTTYPE_DAMAGEIND, Pos, Id, GameTick(), (int)(f * 256.0f));
857 CreatePredictedEvent(NewEvent: Event);
858 }
859}
860