| 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 | |
| 19 | CSaveTee::CSaveTee() = default; |
| 20 | |
| 21 | void 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 | |
| 141 | bool 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 | |
| 270 | char *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 | |
| 357 | bool 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 | |
| 516 | void CSaveTee::LoadHookedPlayer(const CSaveTeam *pTeam) |
| 517 | { |
| 518 | if(m_HookedPlayer == -1) |
| 519 | return; |
| 520 | m_HookedPlayer = pTeam->m_pSavedTees[m_HookedPlayer].GetClientId(); |
| 521 | } |
| 522 | |
| 523 | bool CSaveTee::IsHooking() const |
| 524 | { |
| 525 | return m_HookState == HOOK_GRABBED || m_HookState == HOOK_FLYING; |
| 526 | } |
| 527 | |
| 528 | void 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 | |
| 537 | bool 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 | |
| 548 | CSaveTeam::CSaveTeam() |
| 549 | { |
| 550 | m_aString[0] = '\0'; |
| 551 | } |
| 552 | |
| 553 | CSaveTeam::~CSaveTeam() |
| 554 | { |
| 555 | delete[] m_pSwitchers; |
| 556 | delete[] m_pSavedTees; |
| 557 | } |
| 558 | |
| 559 | ESaveResult 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 | |
| 630 | bool 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 | |
| 658 | bool 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 | |
| 701 | CCharacter *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 | |
| 712 | char *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 | |
| 736 | int 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 | |
| 896 | bool 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 | |