1#include "save.h"
2
3#include "player.h"
4#include "teams.h"
5
6#include <engine/shared/config.h>
7#include <engine/shared/protocol.h>
8
9#include <game/server/entities/character.h>
10#include <game/server/gamemodes/DDRace.h>
11#include <game/team_state.h>
12
13#include <cstdio> // sscanf
14
15CSaveTee::CSaveTee() = default;
16
17void CSaveTee::Save(CCharacter *pChr, bool AddPenalty)
18{
19 m_ClientId = pChr->m_pPlayer->GetCid();
20 str_copy(dst: m_aName, src: pChr->Server()->ClientName(ClientId: m_ClientId), dst_size: sizeof(m_aName));
21
22 m_Alive = pChr->m_Alive;
23
24 // This is extremely suspect code, probably interacts badly with force pause
25 m_Paused = absolute(a: pChr->m_pPlayer->IsPaused());
26 if(m_Paused == CPlayer::PAUSE_SPEC && !pChr->m_Paused)
27 {
28 m_Paused = CPlayer::PAUSE_NONE;
29 }
30
31 m_NeededFaketuning = pChr->m_NeededFaketuning;
32
33 m_TeeStarted = pChr->Teams()->TeeStarted(ClientId: m_ClientId);
34 m_TeeFinished = pChr->Teams()->TeeFinished(ClientId: m_ClientId);
35 m_IsSolo = pChr->m_Core.m_Solo;
36
37 for(int i = 0; i < NUM_WEAPONS; i++)
38 {
39 m_aWeapons[i].m_AmmoRegenStart = pChr->m_Core.m_aWeapons[i].m_AmmoRegenStart;
40 m_aWeapons[i].m_Ammo = pChr->m_Core.m_aWeapons[i].m_Ammo;
41 m_aWeapons[i].m_Ammocost = pChr->m_Core.m_aWeapons[i].m_Ammocost;
42 m_aWeapons[i].m_Got = pChr->m_Core.m_aWeapons[i].m_Got;
43 }
44
45 m_Ninja.m_ActivationDir = pChr->m_Core.m_Ninja.m_ActivationDir;
46 if(pChr->m_Core.m_Ninja.m_ActivationTick)
47 m_Ninja.m_ActivationTick = pChr->Server()->Tick() - pChr->m_Core.m_Ninja.m_ActivationTick;
48 else
49 m_Ninja.m_ActivationTick = 0;
50 m_Ninja.m_CurrentMoveTime = pChr->m_Core.m_Ninja.m_CurrentMoveTime;
51 m_Ninja.m_OldVelAmount = pChr->m_Core.m_Ninja.m_OldVelAmount;
52
53 m_LastWeapon = pChr->m_LastWeapon;
54 m_QueuedWeapon = pChr->m_QueuedWeapon;
55
56 m_EndlessJump = pChr->m_Core.m_EndlessJump;
57 m_Jetpack = pChr->m_Core.m_Jetpack;
58 m_NinjaJetpack = pChr->m_NinjaJetpack;
59 m_FreezeTime = pChr->m_FreezeTime;
60 m_FreezeStart = pChr->Server()->Tick() - pChr->m_Core.m_FreezeStart;
61
62 m_DeepFrozen = pChr->m_Core.m_DeepFrozen;
63 m_LiveFrozen = pChr->m_Core.m_LiveFrozen;
64 m_EndlessHook = pChr->m_Core.m_EndlessHook;
65 m_DDRaceState = static_cast<int>(pChr->m_DDRaceState);
66
67 m_HitDisabledFlags = 0;
68 if(pChr->m_Core.m_HammerHitDisabled)
69 m_HitDisabledFlags |= CSaveTee::HAMMER_HIT_DISABLED;
70 if(pChr->m_Core.m_ShotgunHitDisabled)
71 m_HitDisabledFlags |= CSaveTee::SHOTGUN_HIT_DISABLED;
72 if(pChr->m_Core.m_GrenadeHitDisabled)
73 m_HitDisabledFlags |= CSaveTee::GRENADE_HIT_DISABLED;
74 if(pChr->m_Core.m_LaserHitDisabled)
75 m_HitDisabledFlags |= CSaveTee::LASER_HIT_DISABLED;
76
77 m_TuneZone = pChr->m_TuneZone;
78 m_TuneZoneOld = pChr->m_TuneZoneOld;
79
80 if(pChr->m_StartTime)
81 m_Time = pChr->Server()->Tick() - pChr->m_StartTime;
82 else
83 m_Time = 0;
84
85 if(AddPenalty && pChr->m_StartTime)
86 m_Time += g_Config.m_SvSaveSwapGamesPenalty * pChr->Server()->TickSpeed();
87
88 m_Pos = pChr->m_Pos;
89 m_PrevPos = pChr->m_PrevPos;
90 m_TeleCheckpoint = pChr->m_TeleCheckpoint;
91 m_LastPenalty = pChr->m_LastPenalty;
92
93 if(pChr->m_TimeCpBroadcastEndTick)
94 m_TimeCpBroadcastEndTime = pChr->Server()->Tick() - pChr->m_TimeCpBroadcastEndTick;
95
96 m_LastTimeCp = pChr->m_LastTimeCp;
97 m_LastTimeCpBroadcasted = pChr->m_LastTimeCpBroadcasted;
98
99 for(int i = 0; i < MAX_CHECKPOINTS; i++)
100 m_aCurrentTimeCp[i] = pChr->m_aCurrentTimeCp[i];
101
102 m_NotEligibleForFinish = pChr->m_pPlayer->m_NotEligibleForFinish;
103
104 m_HasTelegunGun = pChr->m_Core.m_HasTelegunGun;
105 m_HasTelegunGrenade = pChr->m_Core.m_HasTelegunGrenade;
106 m_HasTelegunLaser = pChr->m_Core.m_HasTelegunLaser;
107
108 // Core
109 m_CorePos = pChr->m_Core.m_Pos;
110 m_Vel = pChr->m_Core.m_Vel;
111 m_HookHitEnabled = !pChr->m_Core.m_HookHitDisabled;
112 m_CollisionEnabled = !pChr->m_Core.m_CollisionDisabled;
113 m_ActiveWeapon = pChr->m_Core.m_ActiveWeapon;
114 m_Jumped = pChr->m_Core.m_Jumped;
115 m_JumpedTotal = pChr->m_Core.m_JumpedTotal;
116 m_Jumps = pChr->m_Core.m_Jumps;
117 m_HookPos = pChr->m_Core.m_HookPos;
118 m_HookDir = pChr->m_Core.m_HookDir;
119 m_HookTeleBase = pChr->m_Core.m_HookTeleBase;
120
121 m_HookTick = pChr->m_Core.m_HookTick;
122
123 m_HookState = pChr->m_Core.m_HookState;
124 m_HookedPlayer = pChr->m_Core.HookedPlayer();
125 m_NewHook = pChr->m_Core.m_NewHook != 0;
126
127 m_InputDirection = pChr->m_SavedInput.m_Direction;
128 m_InputJump = pChr->m_SavedInput.m_Jump;
129 m_InputFire = pChr->m_SavedInput.m_Fire;
130 m_InputHook = pChr->m_SavedInput.m_Hook;
131
132 m_ReloadTimer = pChr->m_ReloadTimer;
133
134 FormatUuid(Uuid: pChr->GameServer()->GameUuid(), pBuffer: m_aGameUuid, BufferLength: sizeof(m_aGameUuid));
135}
136
137bool CSaveTee::Load(CCharacter *pChr, std::optional<int> Team)
138{
139 bool Valid = true;
140
141 pChr->m_pPlayer->Pause(State: m_Paused, Force: true);
142
143 pChr->m_Alive = m_Alive;
144 pChr->m_NeededFaketuning = m_NeededFaketuning;
145
146 if(Team.has_value())
147 {
148 pChr->Teams()->SetForceCharacterTeam(ClientId: pChr->m_pPlayer->GetCid(), Team: Team.value());
149 pChr->Teams()->SetStarted(ClientId: pChr->m_pPlayer->GetCid(), Started: m_TeeStarted);
150 pChr->Teams()->SetFinished(ClientId: pChr->m_pPlayer->GetCid(), Finished: m_TeeFinished);
151 }
152
153 for(int i = 0; i < NUM_WEAPONS; i++)
154 {
155 pChr->m_Core.m_aWeapons[i].m_AmmoRegenStart = m_aWeapons[i].m_AmmoRegenStart;
156 // m_Ammo not used anymore for tracking freeze following https://github.com/ddnet/ddnet/pull/2086
157 pChr->m_Core.m_aWeapons[i].m_Ammo = -1;
158 pChr->m_Core.m_aWeapons[i].m_Ammocost = m_aWeapons[i].m_Ammocost;
159 pChr->m_Core.m_aWeapons[i].m_Got = m_aWeapons[i].m_Got;
160 }
161
162 pChr->m_Core.m_Ninja.m_ActivationDir = m_Ninja.m_ActivationDir;
163 if(m_Ninja.m_ActivationTick)
164 pChr->m_Core.m_Ninja.m_ActivationTick = pChr->Server()->Tick() - m_Ninja.m_ActivationTick;
165 else
166 pChr->m_Core.m_Ninja.m_ActivationTick = 0;
167 pChr->m_Core.m_Ninja.m_CurrentMoveTime = m_Ninja.m_CurrentMoveTime;
168 pChr->m_Core.m_Ninja.m_OldVelAmount = m_Ninja.m_OldVelAmount;
169
170 pChr->m_LastWeapon = m_LastWeapon;
171 pChr->m_QueuedWeapon = m_QueuedWeapon;
172
173 pChr->m_Core.m_EndlessJump = m_EndlessJump;
174 pChr->m_Core.m_Jetpack = m_Jetpack;
175 pChr->m_NinjaJetpack = m_NinjaJetpack;
176 pChr->m_FreezeTime = m_FreezeTime;
177 pChr->m_Core.m_FreezeStart = pChr->Server()->Tick() - m_FreezeStart;
178
179 pChr->m_Core.m_DeepFrozen = m_DeepFrozen;
180 pChr->m_Core.m_LiveFrozen = m_LiveFrozen;
181 pChr->m_Core.m_EndlessHook = m_EndlessHook;
182 pChr->m_DDRaceState = static_cast<ERaceState>(m_DDRaceState);
183
184 pChr->m_Core.m_HammerHitDisabled = m_HitDisabledFlags & CSaveTee::HAMMER_HIT_DISABLED;
185 pChr->m_Core.m_ShotgunHitDisabled = m_HitDisabledFlags & CSaveTee::SHOTGUN_HIT_DISABLED;
186 pChr->m_Core.m_GrenadeHitDisabled = m_HitDisabledFlags & CSaveTee::GRENADE_HIT_DISABLED;
187 pChr->m_Core.m_LaserHitDisabled = m_HitDisabledFlags & CSaveTee::LASER_HIT_DISABLED;
188
189 pChr->m_TuneZone = m_TuneZone;
190 pChr->m_TuneZoneOld = m_TuneZoneOld;
191
192 if(m_Time)
193 pChr->m_StartTime = pChr->Server()->Tick() - m_Time;
194
195 pChr->m_Pos = m_Pos;
196 pChr->m_PrevPos = m_PrevPos;
197 pChr->m_TeleCheckpoint = m_TeleCheckpoint;
198 pChr->m_LastPenalty = m_LastPenalty;
199
200 if(m_TimeCpBroadcastEndTime)
201 pChr->m_TimeCpBroadcastEndTick = pChr->Server()->Tick() - m_TimeCpBroadcastEndTime;
202
203 pChr->m_LastTimeCp = m_LastTimeCp;
204 pChr->m_LastTimeCpBroadcasted = m_LastTimeCpBroadcasted;
205
206 for(int i = 0; i < MAX_CHECKPOINTS; i++)
207 pChr->m_aCurrentTimeCp[i] = m_aCurrentTimeCp[i];
208
209 pChr->m_pPlayer->m_NotEligibleForFinish = pChr->m_pPlayer->m_NotEligibleForFinish || m_NotEligibleForFinish;
210
211 pChr->m_Core.m_HasTelegunGun = m_HasTelegunGun;
212 pChr->m_Core.m_HasTelegunLaser = m_HasTelegunLaser;
213 pChr->m_Core.m_HasTelegunGrenade = m_HasTelegunGrenade;
214
215 // Core
216 pChr->m_Core.m_Pos = m_CorePos;
217 pChr->m_Core.m_Vel = m_Vel;
218 pChr->m_Core.m_HookHitDisabled = !m_HookHitEnabled;
219 pChr->m_Core.m_CollisionDisabled = !m_CollisionEnabled;
220 pChr->m_Core.m_ActiveWeapon = m_ActiveWeapon;
221 pChr->m_Core.m_Jumped = m_Jumped;
222 pChr->m_Core.m_JumpedTotal = m_JumpedTotal;
223 pChr->m_Core.m_Jumps = m_Jumps;
224 pChr->m_Core.m_HookPos = m_HookPos;
225 pChr->m_Core.m_HookDir = m_HookDir;
226 pChr->m_Core.m_HookTeleBase = m_HookTeleBase;
227
228 pChr->m_Core.m_HookTick = m_HookTick;
229
230 pChr->m_Core.m_HookState = m_HookState;
231 if(m_HookedPlayer != -1 && Team.has_value() && pChr->Teams()->m_Core.Team(ClientId: m_HookedPlayer) != Team.value())
232 {
233 pChr->m_Core.SetHookedPlayer(-1);
234 pChr->m_Core.m_HookState = HOOK_FLYING;
235 }
236 else
237 {
238 pChr->m_Core.SetHookedPlayer(m_HookedPlayer);
239 }
240 pChr->m_Core.m_NewHook = m_NewHook;
241 pChr->m_SavedInput.m_Direction = m_InputDirection;
242 pChr->m_SavedInput.m_Jump = m_InputJump;
243 pChr->m_SavedInput.m_Fire = m_InputFire;
244 pChr->m_SavedInput.m_Hook = m_InputHook;
245
246 pChr->m_ReloadTimer = m_ReloadTimer;
247
248 pChr->SetSolo(m_IsSolo);
249
250 if(Team.has_value())
251 {
252 // Always create a rescue tee at the exact location we loaded from so that
253 // the old one gets overwritten.
254 pChr->ForceSetRescue(RescueMode: RESCUEMODE_AUTO);
255 pChr->ForceSetRescue(RescueMode: RESCUEMODE_MANUAL);
256 }
257
258 if(pChr->m_pPlayer->IsPaused() == -1 * CPlayer::PAUSE_SPEC && !pChr->m_pPlayer->CanSpec())
259 {
260 Valid = false;
261 }
262
263 return Valid;
264}
265
266char *CSaveTee::GetString(const CSaveTeam *pTeam)
267{
268 int HookedPlayer = -1;
269 if(m_HookedPlayer != -1)
270 {
271 for(int n = 0; n < pTeam->GetMembersCount(); n++)
272 {
273 if(m_HookedPlayer == pTeam->m_pSavedTees[n].GetClientId())
274 {
275 HookedPlayer = n;
276 break;
277 }
278 }
279 }
280
281 str_format(buffer: m_aString, buffer_size: sizeof(m_aString),
282 format: "%s\t%d\t%d\t%d\t%d\t%d\t"
283 // weapons
284 "%d\t%d\t%d\t%d\t"
285 "%d\t%d\t%d\t%d\t"
286 "%d\t%d\t%d\t%d\t"
287 "%d\t%d\t%d\t%d\t"
288 "%d\t%d\t%d\t%d\t"
289 "%d\t%d\t%d\t%d\t"
290 "%d\t%d\t"
291 // tee stats
292 "%d\t%d\t%d\t%d\t%d\t%d\t%d\t" // m_EndlessJump
293 "%d\t%d\t%d\t%d\t%d\t%d\t%d\t" // m_DDRaceState
294 "%d\t%d\t%d\t%d\t" // m_Pos.x
295 "%d\t%d\t" // m_TeleCheckpoint
296 "%d\t%d\t%f\t%f\t" // m_CorePos.x
297 "%d\t%d\t%d\t%d\t" // m_ActiveWeapon
298 "%d\t%d\t%f\t%f\t" // m_HookPos.x
299 "%d\t%d\t%d\t%d\t" // m_HookTeleBase.x
300 // time checkpoints
301 "%d\t%d\t%d\t"
302 "%f\t%f\t%f\t%f\t%f\t"
303 "%f\t%f\t%f\t%f\t%f\t"
304 "%f\t%f\t%f\t%f\t%f\t"
305 "%f\t%f\t%f\t%f\t%f\t"
306 "%f\t%f\t%f\t%f\t%f\t"
307 "%d\t" // m_NotEligibleForFinish
308 "%d\t%d\t%d\t" // tele weapons
309 "%s\t" // m_aGameUuid
310 "%d\t%d\t" // m_HookedPlayer, m_NewHook
311 "%d\t%d\t%d\t%d\t" // input stuff
312 "%d\t" // m_ReloadTimer
313 "%d\t" // m_TeeStarted
314 "%d\t" //m_LiveFreeze
315 "%f\t%f\t%d\t%d\t%d", // m_Ninja
316 m_aName, m_Alive, m_Paused, m_NeededFaketuning, m_TeeFinished, m_IsSolo,
317 // weapons
318 m_aWeapons[0].m_AmmoRegenStart, m_aWeapons[0].m_Ammo, m_aWeapons[0].m_Ammocost, m_aWeapons[0].m_Got,
319 m_aWeapons[1].m_AmmoRegenStart, m_aWeapons[1].m_Ammo, m_aWeapons[1].m_Ammocost, m_aWeapons[1].m_Got,
320 m_aWeapons[2].m_AmmoRegenStart, m_aWeapons[2].m_Ammo, m_aWeapons[2].m_Ammocost, m_aWeapons[2].m_Got,
321 m_aWeapons[3].m_AmmoRegenStart, m_aWeapons[3].m_Ammo, m_aWeapons[3].m_Ammocost, m_aWeapons[3].m_Got,
322 m_aWeapons[4].m_AmmoRegenStart, m_aWeapons[4].m_Ammo, m_aWeapons[4].m_Ammocost, m_aWeapons[4].m_Got,
323 m_aWeapons[5].m_AmmoRegenStart, m_aWeapons[5].m_Ammo, m_aWeapons[5].m_Ammocost, m_aWeapons[5].m_Got,
324 m_LastWeapon, m_QueuedWeapon,
325 // tee states
326 m_EndlessJump, m_Jetpack, m_NinjaJetpack, m_FreezeTime, m_FreezeStart, m_DeepFrozen, m_EndlessHook,
327 m_DDRaceState, m_HitDisabledFlags, m_CollisionEnabled, m_TuneZone, m_TuneZoneOld, m_HookHitEnabled, m_Time,
328 (int)m_Pos.x, (int)m_Pos.y, (int)m_PrevPos.x, (int)m_PrevPos.y,
329 m_TeleCheckpoint, m_LastPenalty,
330 (int)m_CorePos.x, (int)m_CorePos.y, m_Vel.x, m_Vel.y,
331 m_ActiveWeapon, m_Jumped, m_JumpedTotal, m_Jumps,
332 (int)m_HookPos.x, (int)m_HookPos.y, m_HookDir.x, m_HookDir.y,
333 (int)m_HookTeleBase.x, (int)m_HookTeleBase.y, m_HookTick, m_HookState,
334 // time checkpoints
335 m_TimeCpBroadcastEndTime, m_LastTimeCp, m_LastTimeCpBroadcasted,
336 m_aCurrentTimeCp[0], m_aCurrentTimeCp[1], m_aCurrentTimeCp[2], m_aCurrentTimeCp[3], m_aCurrentTimeCp[4],
337 m_aCurrentTimeCp[5], m_aCurrentTimeCp[6], m_aCurrentTimeCp[7], m_aCurrentTimeCp[8], m_aCurrentTimeCp[9],
338 m_aCurrentTimeCp[10], m_aCurrentTimeCp[11], m_aCurrentTimeCp[12], m_aCurrentTimeCp[13], m_aCurrentTimeCp[14],
339 m_aCurrentTimeCp[15], m_aCurrentTimeCp[16], m_aCurrentTimeCp[17], m_aCurrentTimeCp[18], m_aCurrentTimeCp[19],
340 m_aCurrentTimeCp[20], m_aCurrentTimeCp[21], m_aCurrentTimeCp[22], m_aCurrentTimeCp[23], m_aCurrentTimeCp[24],
341 m_NotEligibleForFinish,
342 m_HasTelegunGun, m_HasTelegunLaser, m_HasTelegunGrenade,
343 m_aGameUuid,
344 HookedPlayer, m_NewHook,
345 m_InputDirection, m_InputJump, m_InputFire, m_InputHook,
346 m_ReloadTimer,
347 m_TeeStarted,
348 m_LiveFrozen,
349 m_Ninja.m_ActivationDir.x, m_Ninja.m_ActivationDir.y, m_Ninja.m_ActivationTick, m_Ninja.m_CurrentMoveTime, m_Ninja.m_OldVelAmount);
350 return m_aString;
351}
352
353int CSaveTee::FromString(const char *pString)
354{
355 int Num;
356 Num = sscanf(s: pString,
357 format: "%[^\t]\t%d\t%d\t%d\t%d\t%d\t"
358 // weapons
359 "%d\t%d\t%d\t%d\t"
360 "%d\t%d\t%d\t%d\t"
361 "%d\t%d\t%d\t%d\t"
362 "%d\t%d\t%d\t%d\t"
363 "%d\t%d\t%d\t%d\t"
364 "%d\t%d\t%d\t%d\t"
365 "%d\t%d\t"
366 // tee states
367 "%d\t%d\t%d\t%d\t%d\t%d\t%d\t" // m_EndlessJump
368 "%d\t%d\t%d\t%d\t%d\t%d\t%d\t" // m_DDRaceState
369 "%f\t%f\t%f\t%f\t" // m_Pos.x
370 "%d\t%d\t" // m_TeleCheckpoint
371 "%f\t%f\t%f\t%f\t" // m_CorePos.x
372 "%d\t%d\t%d\t%d\t" // m_ActiveWeapon
373 "%f\t%f\t%f\t%f\t" // m_HookPos.x
374 "%f\t%f\t%d\t%d\t" // m_HookTeleBase.x
375 // time checkpoints
376 "%d\t%d\t%d\t"
377 "%f\t%f\t%f\t%f\t%f\t"
378 "%f\t%f\t%f\t%f\t%f\t"
379 "%f\t%f\t%f\t%f\t%f\t"
380 "%f\t%f\t%f\t%f\t%f\t"
381 "%f\t%f\t%f\t%f\t%f\t"
382 "%d\t" // m_NotEligibleForFinish
383 "%d\t%d\t%d\t" // tele weapons
384 "%36s\t" // m_aGameUuid
385 "%d\t%d\t" // m_HookedPlayer, m_NewHook
386 "%d\t%d\t%d\t%d\t" // input stuff
387 "%d\t" // m_ReloadTimer
388 "%d\t" // m_TeeStarted
389 "%d\t" // m_LiveFreeze
390 "%f\t%f\t%d\t%d\t%d", // m_Ninja
391 m_aName, &m_Alive, &m_Paused, &m_NeededFaketuning, &m_TeeFinished, &m_IsSolo,
392 // weapons
393 &m_aWeapons[0].m_AmmoRegenStart, &m_aWeapons[0].m_Ammo, &m_aWeapons[0].m_Ammocost, &m_aWeapons[0].m_Got,
394 &m_aWeapons[1].m_AmmoRegenStart, &m_aWeapons[1].m_Ammo, &m_aWeapons[1].m_Ammocost, &m_aWeapons[1].m_Got,
395 &m_aWeapons[2].m_AmmoRegenStart, &m_aWeapons[2].m_Ammo, &m_aWeapons[2].m_Ammocost, &m_aWeapons[2].m_Got,
396 &m_aWeapons[3].m_AmmoRegenStart, &m_aWeapons[3].m_Ammo, &m_aWeapons[3].m_Ammocost, &m_aWeapons[3].m_Got,
397 &m_aWeapons[4].m_AmmoRegenStart, &m_aWeapons[4].m_Ammo, &m_aWeapons[4].m_Ammocost, &m_aWeapons[4].m_Got,
398 &m_aWeapons[5].m_AmmoRegenStart, &m_aWeapons[5].m_Ammo, &m_aWeapons[5].m_Ammocost, &m_aWeapons[5].m_Got,
399 &m_LastWeapon, &m_QueuedWeapon,
400 // tee states
401 &m_EndlessJump, &m_Jetpack, &m_NinjaJetpack, &m_FreezeTime, &m_FreezeStart, &m_DeepFrozen, &m_EndlessHook,
402 &m_DDRaceState, &m_HitDisabledFlags, &m_CollisionEnabled, &m_TuneZone, &m_TuneZoneOld, &m_HookHitEnabled, &m_Time,
403 &m_Pos.x, &m_Pos.y, &m_PrevPos.x, &m_PrevPos.y,
404 &m_TeleCheckpoint, &m_LastPenalty,
405 &m_CorePos.x, &m_CorePos.y, &m_Vel.x, &m_Vel.y,
406 &m_ActiveWeapon, &m_Jumped, &m_JumpedTotal, &m_Jumps,
407 &m_HookPos.x, &m_HookPos.y, &m_HookDir.x, &m_HookDir.y,
408 &m_HookTeleBase.x, &m_HookTeleBase.y, &m_HookTick, &m_HookState,
409 // time checkpoints
410 &m_TimeCpBroadcastEndTime, &m_LastTimeCp, &m_LastTimeCpBroadcasted,
411 &m_aCurrentTimeCp[0], &m_aCurrentTimeCp[1], &m_aCurrentTimeCp[2], &m_aCurrentTimeCp[3], &m_aCurrentTimeCp[4],
412 &m_aCurrentTimeCp[5], &m_aCurrentTimeCp[6], &m_aCurrentTimeCp[7], &m_aCurrentTimeCp[8], &m_aCurrentTimeCp[9],
413 &m_aCurrentTimeCp[10], &m_aCurrentTimeCp[11], &m_aCurrentTimeCp[12], &m_aCurrentTimeCp[13], &m_aCurrentTimeCp[14],
414 &m_aCurrentTimeCp[15], &m_aCurrentTimeCp[16], &m_aCurrentTimeCp[17], &m_aCurrentTimeCp[18], &m_aCurrentTimeCp[19],
415 &m_aCurrentTimeCp[20], &m_aCurrentTimeCp[21], &m_aCurrentTimeCp[22], &m_aCurrentTimeCp[23], &m_aCurrentTimeCp[24],
416 &m_NotEligibleForFinish,
417 &m_HasTelegunGun, &m_HasTelegunLaser, &m_HasTelegunGrenade,
418 m_aGameUuid,
419 &m_HookedPlayer, &m_NewHook,
420 &m_InputDirection, &m_InputJump, &m_InputFire, &m_InputHook,
421 &m_ReloadTimer,
422 &m_TeeStarted,
423 &m_LiveFrozen,
424 &m_Ninja.m_ActivationDir.x, &m_Ninja.m_ActivationDir.y, &m_Ninja.m_ActivationTick, &m_Ninja.m_CurrentMoveTime, &m_Ninja.m_OldVelAmount);
425 switch(Num) // Don't forget to update this when you save / load more / less.
426 {
427 case 96:
428 m_NotEligibleForFinish = false;
429 [[fallthrough]];
430 case 97:
431 m_HasTelegunGrenade = 0;
432 m_HasTelegunLaser = 0;
433 m_HasTelegunGun = 0;
434 FormatUuid(Uuid: CalculateUuid(pName: "game-uuid-nonexistent@ddnet.tw"), pBuffer: m_aGameUuid, BufferLength: sizeof(m_aGameUuid));
435 [[fallthrough]];
436 case 101:
437 m_HookedPlayer = -1;
438 m_NewHook = false;
439 if(m_HookState == HOOK_GRABBED)
440 m_HookState = HOOK_FLYING;
441 m_InputDirection = 0;
442 m_InputJump = 0;
443 m_InputFire = 0;
444 m_InputHook = 0;
445 m_ReloadTimer = 0;
446 [[fallthrough]];
447 case 108:
448 m_TeeStarted = true;
449 [[fallthrough]];
450 case 109:
451 m_LiveFrozen = false;
452 [[fallthrough]];
453 case 110:
454 if(m_aWeapons[WEAPON_NINJA].m_Got)
455 {
456 // remove ninja
457 m_aWeapons[WEAPON_NINJA].m_Got = false;
458 m_aWeapons[WEAPON_NINJA].m_Ammo = 0;
459 m_ActiveWeapon = m_LastWeapon;
460 }
461 m_Ninja.m_ActivationDir.x = 0.0;
462 m_Ninja.m_ActivationDir.y = 0.0;
463 m_Ninja.m_ActivationTick = 0;
464 m_Ninja.m_CurrentMoveTime = 0;
465 m_Ninja.m_OldVelAmount = 0;
466 [[fallthrough]];
467 case 115:
468 return 0;
469 default:
470 dbg_msg(sys: "load", fmt: "failed to load tee-string");
471 dbg_msg(sys: "load", fmt: "loaded %d vars", Num);
472 return Num + 1; // never 0 here
473 }
474}
475
476void CSaveTee::LoadHookedPlayer(const CSaveTeam *pTeam)
477{
478 if(m_HookedPlayer == -1)
479 return;
480 m_HookedPlayer = pTeam->m_pSavedTees[m_HookedPlayer].GetClientId();
481}
482
483bool CSaveTee::IsHooking() const
484{
485 return m_HookState == HOOK_GRABBED || m_HookState == HOOK_FLYING;
486}
487
488void CSaveHotReloadTee::Save(CCharacter *pChr, bool AddPenalty)
489{
490 m_SaveTee.Save(pChr, AddPenalty);
491 m_Super = pChr->m_Core.m_Super;
492 m_Invincible = pChr->m_Core.m_Invincible;
493 m_SavedTeleTee = pChr->GetPlayer()->m_LastTeleTee;
494 m_LastDeath = pChr->GetPlayer()->m_LastDeath;
495}
496
497bool CSaveHotReloadTee::Load(CCharacter *pChr, int Team)
498{
499 bool Result = m_SaveTee.Load(pChr, Team);
500 pChr->SetSuper(m_Super);
501 pChr->m_Core.m_Invincible = m_Invincible;
502 pChr->GetPlayer()->m_LastTeleTee = m_SavedTeleTee;
503 pChr->GetPlayer()->m_LastDeath = m_LastDeath;
504
505 return Result;
506}
507
508CSaveTeam::CSaveTeam()
509{
510 m_aString[0] = '\0';
511}
512
513CSaveTeam::~CSaveTeam()
514{
515 delete[] m_pSwitchers;
516 delete[] m_pSavedTees;
517}
518
519ESaveResult CSaveTeam::Save(CGameContext *pGameServer, int Team, bool Dry, bool Force)
520{
521 IGameController *pController = pGameServer->m_pController;
522 CGameTeams *pTeams = &pController->Teams();
523
524 if(g_Config.m_SvTeam != SV_TEAM_FORCED_SOLO && (Team == TEAM_FLOCK || !pTeams->IsValidTeamNumber(Team)) && !Force)
525 return ESaveResult::TEAM_FLOCK;
526
527 if(pTeams->TeamFlock(Team) && !Force)
528 {
529 return ESaveResult::TEAM_0_MODE;
530 }
531
532 m_MembersCount = pTeams->Count(Team);
533 if(m_MembersCount <= 0)
534 {
535 return ESaveResult::TEAM_NOT_FOUND;
536 }
537
538 m_TeamState = pTeams->GetTeamState(Team);
539
540 if(m_TeamState != ETeamState::STARTED && !Force)
541 {
542 return ESaveResult::NOT_STARTED;
543 }
544
545 m_HighestSwitchNumber = pGameServer->Collision()->m_HighestSwitchNumber;
546 m_TeamLocked = pTeams->TeamLocked(Team);
547 m_Practice = pTeams->IsPractice(Team);
548
549 m_pSavedTees = new CSaveTee[m_MembersCount];
550 int aPlayerCids[MAX_CLIENTS];
551 int j = 0;
552 CCharacter *p = (CCharacter *)pGameServer->m_World.FindFirst(Type: CGameWorld::ENTTYPE_CHARACTER);
553 for(; p; p = (CCharacter *)p->TypeNext())
554 {
555 if(pTeams->m_Core.Team(ClientId: p->GetPlayer()->GetCid()) != Team)
556 continue;
557 if(m_MembersCount == j && !Force)
558 return ESaveResult::CHAR_NOT_FOUND;
559 ESaveResult Result = pGameServer->m_World.BlocksSave(ClientId: p->GetPlayer()->GetCid());
560 if(Result != ESaveResult::SUCCESS && !Force)
561 return Result;
562 m_pSavedTees[j].Save(pChr: p);
563 aPlayerCids[j] = p->GetPlayer()->GetCid();
564 j++;
565 }
566 if(m_MembersCount != j && !Force)
567 return ESaveResult::CHAR_NOT_FOUND;
568
569 if(pGameServer->Collision()->m_HighestSwitchNumber)
570 {
571 m_pSwitchers = new SSimpleSwitchers[pGameServer->Collision()->m_HighestSwitchNumber + 1];
572
573 for(int i = 1; i < pGameServer->Collision()->m_HighestSwitchNumber + 1; i++)
574 {
575 m_pSwitchers[i].m_Status = pGameServer->Switchers()[i].m_aStatus[Team];
576 if(pGameServer->Switchers()[i].m_aEndTick[Team])
577 m_pSwitchers[i].m_EndTime = pController->Server()->Tick() - pGameServer->Switchers()[i].m_aEndTick[Team];
578 else
579 m_pSwitchers[i].m_EndTime = 0;
580 m_pSwitchers[i].m_Type = pGameServer->Switchers()[i].m_aType[Team];
581 }
582 }
583 if(!Dry)
584 {
585 pGameServer->m_World.RemoveEntitiesFromPlayers(PlayerIds: aPlayerCids, NumPlayers: m_MembersCount);
586 }
587 return ESaveResult::SUCCESS;
588}
589
590bool CSaveTeam::HandleSaveError(ESaveResult Result, int ClientId, CGameContext *pGameContext)
591{
592 switch(Result)
593 {
594 case ESaveResult::SUCCESS:
595 return false;
596 case ESaveResult::TEAM_FLOCK:
597 pGameContext->SendChatTarget(To: ClientId, pText: "You have to be in a team (from 1-63)");
598 break;
599 case ESaveResult::TEAM_NOT_FOUND:
600 pGameContext->SendChatTarget(To: ClientId, pText: "Could not find your Team");
601 break;
602 case ESaveResult::CHAR_NOT_FOUND:
603 pGameContext->SendChatTarget(To: ClientId, pText: "To save all players in your team have to be alive and not in '/spec'");
604 break;
605 case ESaveResult::NOT_STARTED:
606 pGameContext->SendChatTarget(To: ClientId, pText: "Your team has not started yet");
607 break;
608 case ESaveResult::TEAM_0_MODE:
609 pGameContext->SendChatTarget(To: ClientId, pText: "Team can't be saved while in team 0 mode");
610 break;
611 case ESaveResult::DRAGGER_ACTIVE:
612 pGameContext->SendChatTarget(To: ClientId, pText: "Team can't be saved while a dragger is active");
613 break;
614 }
615 return true;
616}
617
618bool CSaveTeam::Load(CGameContext *pGameServer, int Team, bool KeepCurrentWeakStrong, bool IgnorePlayers)
619{
620 IGameController *pController = pGameServer->m_pController;
621 CGameTeams *pTeams = &pController->Teams();
622
623 pTeams->ChangeTeamState(Team, State: m_TeamState);
624 pTeams->SetTeamLock(Team, Lock: m_TeamLocked);
625 pTeams->SetPractice(Team, Enabled: m_Practice);
626
627 bool ContainsInvalidPlayer = false;
628 int aPlayerCids[MAX_CLIENTS];
629
630 if(!IgnorePlayers)
631 {
632 for(int i = m_MembersCount; i-- > 0;)
633 {
634 int ClientId = m_pSavedTees[i].GetClientId();
635 aPlayerCids[i] = ClientId;
636 if(pGameServer->m_apPlayers[ClientId] && pTeams->m_Core.Team(ClientId) == Team)
637 {
638 CCharacter *pChr = MatchCharacter(pGameServer, ClientId: m_pSavedTees[i].GetClientId(), SaveId: i, KeepCurrentCharacter: KeepCurrentWeakStrong);
639 ContainsInvalidPlayer |= !m_pSavedTees[i].Load(pChr, Team);
640 }
641 }
642 }
643
644 if(pGameServer->Collision()->m_HighestSwitchNumber)
645 {
646 for(int i = 1; i < minimum(a: m_HighestSwitchNumber, b: pGameServer->Collision()->m_HighestSwitchNumber) + 1; i++)
647 {
648 pGameServer->Switchers()[i].m_aStatus[Team] = m_pSwitchers[i].m_Status;
649 if(m_pSwitchers[i].m_EndTime)
650 pGameServer->Switchers()[i].m_aEndTick[Team] = pController->Server()->Tick() - m_pSwitchers[i].m_EndTime;
651 pGameServer->Switchers()[i].m_aType[Team] = m_pSwitchers[i].m_Type;
652 }
653 }
654 // remove projectiles and laser
655 if(!IgnorePlayers)
656 pGameServer->m_World.RemoveEntitiesFromPlayers(PlayerIds: aPlayerCids, NumPlayers: m_MembersCount);
657
658 return !ContainsInvalidPlayer;
659}
660
661CCharacter *CSaveTeam::MatchCharacter(CGameContext *pGameServer, int ClientId, int SaveId, bool KeepCurrentCharacter) const
662{
663 if(KeepCurrentCharacter && pGameServer->m_apPlayers[ClientId]->GetCharacter())
664 {
665 // keep old character to retain current weak/strong order
666 return pGameServer->m_apPlayers[ClientId]->GetCharacter();
667 }
668 pGameServer->m_apPlayers[ClientId]->KillCharacter(Weapon: WEAPON_GAME);
669 return pGameServer->m_apPlayers[ClientId]->ForceSpawn(Pos: m_pSavedTees[SaveId].GetPos());
670}
671
672char *CSaveTeam::GetString()
673{
674 str_format(buffer: m_aString, buffer_size: sizeof(m_aString), format: "%d\t%d\t%d\t%d\t%d", static_cast<int>(m_TeamState), m_MembersCount, m_HighestSwitchNumber, m_TeamLocked, m_Practice);
675
676 for(int i = 0; i < m_MembersCount; i++)
677 {
678 char aBuf[1024];
679 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "\n%s", m_pSavedTees[i].GetString(pTeam: this));
680 str_append(dst&: m_aString, src: aBuf);
681 }
682
683 if(m_pSwitchers && m_HighestSwitchNumber)
684 {
685 for(int i = 1; i < m_HighestSwitchNumber + 1; i++)
686 {
687 char aBuf[64];
688 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "\n%d\t%d\t%d", m_pSwitchers[i].m_Status, m_pSwitchers[i].m_EndTime, m_pSwitchers[i].m_Type);
689 str_append(dst&: m_aString, src: aBuf);
690 }
691 }
692
693 return m_aString;
694}
695
696int CSaveTeam::FromString(const char *pString)
697{
698 char aTeamStats[MAX_CLIENTS];
699 char aSwitcher[64];
700 char aSaveTee[1024];
701
702 char *pCopyPos;
703 unsigned int Pos = 0;
704 unsigned int LastPos = 0;
705 unsigned int StrSize;
706
707 str_copy(dst: m_aString, src: pString, dst_size: sizeof(m_aString));
708
709 while(m_aString[Pos] != '\n' && Pos < sizeof(m_aString) && m_aString[Pos]) // find next \n or \0
710 Pos++;
711
712 pCopyPos = m_aString + LastPos;
713 StrSize = Pos - LastPos + 1;
714 if(m_aString[Pos] == '\n')
715 {
716 Pos++; // skip \n
717 LastPos = Pos;
718 }
719
720 if(StrSize <= 0)
721 {
722 dbg_msg(sys: "load", fmt: "savegame: wrong format (couldn't load teamstats)");
723 return 1;
724 }
725
726 if(StrSize < sizeof(aTeamStats))
727 {
728 str_copy(dst: aTeamStats, src: pCopyPos, dst_size: StrSize);
729 int TeamState;
730 int Num = sscanf(s: aTeamStats, format: "%d\t%d\t%d\t%d\t%d", &TeamState, &m_MembersCount, &m_HighestSwitchNumber, &m_TeamLocked, &m_Practice);
731 m_TeamState = static_cast<ETeamState>(TeamState);
732 switch(Num) // Don't forget to update this when you save / load more / less.
733 {
734 case 4:
735 m_Practice = false;
736 [[fallthrough]];
737 case 5:
738 break;
739 default:
740 dbg_msg(sys: "load", fmt: "failed to load teamstats");
741 dbg_msg(sys: "load", fmt: "loaded %d vars", Num);
742 return Num + 1; // never 0 here
743 }
744 }
745 else
746 {
747 dbg_msg(sys: "load", fmt: "savegame: wrong format (couldn't load teamstats, too big)");
748 return 1;
749 }
750
751 if(m_pSavedTees)
752 {
753 delete[] m_pSavedTees;
754 m_pSavedTees = nullptr;
755 }
756
757 if(m_MembersCount > 64)
758 {
759 dbg_msg(sys: "load", fmt: "savegame: team has too many players");
760 return 1;
761 }
762 else if(m_MembersCount)
763 {
764 m_pSavedTees = new CSaveTee[m_MembersCount];
765 }
766
767 for(int n = 0; n < m_MembersCount; n++)
768 {
769 while(m_aString[Pos] != '\n' && Pos < sizeof(m_aString) && m_aString[Pos]) // find next \n or \0
770 Pos++;
771
772 pCopyPos = m_aString + LastPos;
773 StrSize = Pos - LastPos + 1;
774 if(m_aString[Pos] == '\n')
775 {
776 Pos++; // skip \n
777 LastPos = Pos;
778 }
779
780 if(StrSize <= 0)
781 {
782 dbg_msg(sys: "load", fmt: "savegame: wrong format (couldn't load tee)");
783 return 1;
784 }
785
786 if(StrSize < sizeof(aSaveTee))
787 {
788 str_copy(dst: aSaveTee, src: pCopyPos, dst_size: StrSize);
789 int Num = m_pSavedTees[n].FromString(pString: aSaveTee);
790 if(Num)
791 {
792 dbg_msg(sys: "load", fmt: "failed to load tee");
793 dbg_msg(sys: "load", fmt: "loaded %d vars", Num - 1);
794 return 1;
795 }
796 }
797 else
798 {
799 dbg_msg(sys: "load", fmt: "savegame: wrong format (couldn't load tee, too big)");
800 return 1;
801 }
802 }
803
804 if(m_pSwitchers)
805 {
806 delete[] m_pSwitchers;
807 m_pSwitchers = nullptr;
808 }
809
810 if(m_HighestSwitchNumber)
811 m_pSwitchers = new SSimpleSwitchers[m_HighestSwitchNumber + 1];
812
813 for(int n = 1; n < m_HighestSwitchNumber + 1; n++)
814 {
815 while(m_aString[Pos] != '\n' && Pos < sizeof(m_aString) && m_aString[Pos]) // find next \n or \0
816 Pos++;
817
818 pCopyPos = m_aString + LastPos;
819 StrSize = Pos - LastPos + 1;
820 if(m_aString[Pos] == '\n')
821 {
822 Pos++; // skip \n
823 LastPos = Pos;
824 }
825
826 if(StrSize <= 0)
827 {
828 dbg_msg(sys: "load", fmt: "savegame: wrong format (couldn't load switcher)");
829 return 1;
830 }
831
832 if(StrSize < sizeof(aSwitcher))
833 {
834 str_copy(dst: aSwitcher, src: pCopyPos, dst_size: StrSize);
835 int Num = sscanf(s: aSwitcher, format: "%d\t%d\t%d", &(m_pSwitchers[n].m_Status), &(m_pSwitchers[n].m_EndTime), &(m_pSwitchers[n].m_Type));
836 if(Num != 3)
837 {
838 dbg_msg(sys: "load", fmt: "failed to load switcher");
839 dbg_msg(sys: "load", fmt: "loaded %d vars", Num - 1);
840 }
841 }
842 else
843 {
844 dbg_msg(sys: "load", fmt: "savegame: wrong format (couldn't load switcher, too big)");
845 return 1;
846 }
847 }
848
849 return 0;
850}
851
852bool CSaveTeam::MatchPlayers(const char (*paNames)[MAX_NAME_LENGTH], const int *pClientId, int NumPlayer, char *pMessage, int MessageLen) const
853{
854 if(NumPlayer > m_MembersCount)
855 {
856 str_format(buffer: pMessage, buffer_size: MessageLen, format: "Too many players in this team, should be %d", m_MembersCount);
857 return false;
858 }
859 // check for wrong players
860 for(int i = 0; i < NumPlayer; i++)
861 {
862 int Found = false;
863 for(int j = 0; j < m_MembersCount; j++)
864 {
865 if(str_comp(a: paNames[i], b: m_pSavedTees[j].GetName()) == 0)
866 {
867 Found = true;
868 }
869 }
870 if(!Found)
871 {
872 str_format(buffer: pMessage, buffer_size: MessageLen, format: "'%s' doesn't belong to this team", paNames[i]);
873 return false;
874 }
875 }
876 // check for missing players
877 for(int i = 0; i < m_MembersCount; i++)
878 {
879 int Found = false;
880 for(int j = 0; j < NumPlayer; j++)
881 {
882 if(str_comp(a: m_pSavedTees[i].GetName(), b: paNames[j]) == 0)
883 {
884 m_pSavedTees[i].SetClientId(pClientId[j]);
885 Found = true;
886 break;
887 }
888 }
889 if(!Found)
890 {
891 str_format(buffer: pMessage, buffer_size: MessageLen, format: "'%s' has to be in this team", m_pSavedTees[i].GetName());
892 return false;
893 }
894 }
895 // match hook to correct ClientId
896 for(int n = 0; n < m_MembersCount; n++)
897 m_pSavedTees[n].LoadHookedPlayer(pTeam: this);
898 return true;
899}
900