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