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