1/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
2/* If you are missing that file, acquire a complete release at teeworlds.com. */
3
4#include "gameclient.h"
5
6#include "components/background.h"
7#include "components/binds.h"
8#include "components/broadcast.h"
9#include "components/camera.h"
10#include "components/chat.h"
11#include "components/console.h"
12#include "components/controls.h"
13#include "components/countryflags.h"
14#include "components/damageind.h"
15#include "components/debughud.h"
16#include "components/effects.h"
17#include "components/emoticon.h"
18#include "components/freezebars.h"
19#include "components/ghost.h"
20#include "components/hud.h"
21#include "components/infomessages.h"
22#include "components/items.h"
23#include "components/mapimages.h"
24#include "components/maplayers.h"
25#include "components/mapsounds.h"
26#include "components/menu_background.h"
27#include "components/menus.h"
28#include "components/motd.h"
29#include "components/nameplates.h"
30#include "components/particles.h"
31#include "components/players.h"
32#include "components/race_demo.h"
33#include "components/scoreboard.h"
34#include "components/skins.h"
35#include "components/skins7.h"
36#include "components/sounds.h"
37#include "components/spectator.h"
38#include "components/statboard.h"
39#include "components/voting.h"
40#include "lineinput.h"
41#include "prediction/entities/character.h"
42#include "prediction/entities/projectile.h"
43#include "race.h"
44#include "render.h"
45
46#include <base/log.h>
47#include <base/math.h>
48#include <base/system.h>
49#include <base/vmath.h>
50
51#include <engine/client/checksum.h>
52#include <engine/client/enums.h>
53#include <engine/demo.h>
54#include <engine/discord.h>
55#include <engine/editor.h>
56#include <engine/engine.h>
57#include <engine/favorites.h>
58#include <engine/friends.h>
59#include <engine/graphics.h>
60#include <engine/map.h>
61#include <engine/serverbrowser.h>
62#include <engine/shared/config.h>
63#include <engine/shared/csv.h>
64#include <engine/sound.h>
65#include <engine/storage.h>
66#include <engine/textrender.h>
67#include <engine/updater.h>
68
69#include <generated/client_data.h>
70#include <generated/client_data7.h>
71#include <generated/protocol.h>
72#include <generated/protocol7.h>
73#include <generated/protocolglue.h>
74
75#include <game/client/projectile_data.h>
76#include <game/localization.h>
77#include <game/mapitems.h>
78#include <game/version.h>
79
80#include <chrono>
81#include <limits>
82
83using namespace std::chrono_literals;
84
85const char *CGameClient::Version() const { return GAME_VERSION; }
86const char *CGameClient::NetVersion() const { return GAME_NETVERSION; }
87const char *CGameClient::NetVersion7() const { return GAME_NETVERSION7; }
88int CGameClient::DDNetVersion() const { return DDNET_VERSION_NUMBER; }
89const char *CGameClient::DDNetVersionStr() const { return m_aDDNetVersionStr; }
90int CGameClient::ClientVersion7() const { return CLIENT_VERSION7; }
91const char *CGameClient::GetItemName(int Type) const { return m_NetObjHandler.GetObjName(Type); }
92
93void CGameClient::OnConsoleInit()
94{
95 m_pEngine = Kernel()->RequestInterface<IEngine>();
96 m_pClient = Kernel()->RequestInterface<IClient>();
97 m_pTextRender = Kernel()->RequestInterface<ITextRender>();
98 m_pSound = Kernel()->RequestInterface<ISound>();
99 m_pConfigManager = Kernel()->RequestInterface<IConfigManager>();
100 m_pConfig = m_pConfigManager->Values();
101 m_pInput = Kernel()->RequestInterface<IInput>();
102 m_pConsole = Kernel()->RequestInterface<IConsole>();
103 m_pStorage = Kernel()->RequestInterface<IStorage>();
104 m_pDemoPlayer = Kernel()->RequestInterface<IDemoPlayer>();
105 m_pServerBrowser = Kernel()->RequestInterface<IServerBrowser>();
106 m_pEditor = Kernel()->RequestInterface<IEditor>();
107 m_pFavorites = Kernel()->RequestInterface<IFavorites>();
108 m_pFriends = Kernel()->RequestInterface<IFriends>();
109 m_pFoes = Client()->Foes();
110 m_pDiscord = Kernel()->RequestInterface<IDiscord>();
111#if defined(CONF_AUTOUPDATE)
112 m_pUpdater = Kernel()->RequestInterface<IUpdater>();
113#endif
114 m_pHttp = Kernel()->RequestInterface<IHttp>();
115
116 // make a list of all the systems, make sure to add them in the correct render order
117 m_vpAll.insert(position: m_vpAll.end(), l: {&m_Skins,
118 &m_Skins7,
119 &m_CountryFlags,
120 &m_MapImages,
121 &m_Effects, // doesn't render anything, just updates effects
122 &m_Binds,
123 &m_Binds.m_SpecialBinds,
124 &m_Controls,
125 &m_Camera,
126 &m_Sounds,
127 &m_Voting,
128 &m_Particles, // doesn't render anything, just updates all the particles
129 &m_RaceDemo,
130 &m_MapSounds,
131 &m_Censor,
132 &m_Background, // render instead of m_MapLayersBackground when g_Config.m_ClOverlayEntities == 100
133 &m_MapLayersBackground, // first to render
134 &m_Particles.m_RenderTrail,
135 &m_Particles.m_RenderTrailExtra,
136 &m_Items,
137 &m_Ghost,
138 &m_Players,
139 &m_MapLayersForeground,
140 &m_Particles.m_RenderExplosions,
141 &m_NamePlates,
142 &m_Particles.m_RenderExtra,
143 &m_Particles.m_RenderGeneral,
144 &m_FreezeBars,
145 &m_DamageInd,
146 &m_Hud,
147 &m_Spectator,
148 &m_Emoticon,
149 &m_InfoMessages,
150 &m_Chat,
151 &m_Broadcast,
152 &m_ImportantAlert,
153 &m_DebugHud,
154 &m_TouchControls,
155 &m_Scoreboard,
156 &m_Statboard,
157 &m_Motd,
158 &m_Menus,
159 &m_Tooltips,
160 &m_KeyBinder,
161 &m_GameConsole,
162 &m_MenuBackground});
163
164 // build the input stack
165 m_vpInput.insert(position: m_vpInput.end(), l: {&m_KeyBinder, // this will take over all input when we want to bind a key
166 &m_Binds.m_SpecialBinds,
167 &m_GameConsole,
168 &m_Chat, // chat has higher prio, due to that you can quit it by pressing esc
169 &m_Scoreboard,
170 &m_Motd, // for pressing esc to remove it
171 &m_Spectator,
172 &m_Emoticon,
173 &m_ImportantAlert,
174 &m_Menus,
175 &m_Controls,
176 &m_TouchControls,
177 &m_Binds});
178
179 // initialize client data
180 for(int ClientId = 0; ClientId < MAX_CLIENTS; ClientId++)
181 {
182 CClientData &Client = m_aClients[ClientId];
183 Client.m_pGameClient = this;
184 Client.m_ClientId = ClientId;
185 }
186
187 // add basic console commands
188 Console()->Register(pName: "team", pParams: "i[team-id]", Flags: CFGFLAG_CLIENT, pfnFunc: ConTeam, pUser: this, pHelp: "Switch team");
189 Console()->Register(pName: "kill", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConKill, pUser: this, pHelp: "Kill yourself to restart");
190 Console()->Register(pName: "ready_change", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConReadyChange7, pUser: this, pHelp: "Change ready state (0.7 only)");
191
192 // register game commands to allow the client prediction to load settings from the map
193 Console()->Register(pName: "tune", pParams: "s[tuning] ?f[value]", Flags: CFGFLAG_GAME, pfnFunc: ConTuneParam, pUser: this, pHelp: "Tune variable to value");
194 Console()->Register(pName: "tune_zone", pParams: "i[zone] s[tuning] f[value]", Flags: CFGFLAG_GAME, pfnFunc: ConTuneZone, pUser: this, pHelp: "Tune in zone a variable to value");
195 Console()->Register(pName: "mapbug", pParams: "s[mapbug]", Flags: CFGFLAG_GAME, pfnFunc: ConMapbug, pUser: this, pHelp: "Enable map compatibility mode using the specified bug (example: grenade-doubleexplosion@ddnet.tw)");
196
197 for(auto &pComponent : m_vpAll)
198 pComponent->OnInterfacesInit(pClient: this);
199
200 m_LocalServer.OnInterfacesInit(pClient: this);
201
202 // let all the other components register their console commands
203 for(auto &pComponent : m_vpAll)
204 pComponent->OnConsoleInit();
205
206 Console()->Chain(pName: "cl_languagefile", pfnChainFunc: ConchainLanguageUpdate, pUser: this);
207
208 Console()->Chain(pName: "player_name", pfnChainFunc: ConchainSpecialInfoupdate, pUser: this);
209 Console()->Chain(pName: "player_clan", pfnChainFunc: ConchainSpecialInfoupdate, pUser: this);
210 Console()->Chain(pName: "player_country", pfnChainFunc: ConchainSpecialInfoupdate, pUser: this);
211 Console()->Chain(pName: "player_use_custom_color", pfnChainFunc: ConchainSpecialInfoupdate, pUser: this);
212 Console()->Chain(pName: "player_color_body", pfnChainFunc: ConchainSpecialInfoupdate, pUser: this);
213 Console()->Chain(pName: "player_color_feet", pfnChainFunc: ConchainSpecialInfoupdate, pUser: this);
214 Console()->Chain(pName: "player_skin", pfnChainFunc: ConchainSpecialInfoupdate, pUser: this);
215
216 Console()->Chain(pName: "player7_skin", pfnChainFunc: ConchainSpecialInfoupdate, pUser: this);
217 Console()->Chain(pName: "player7_skin_body", pfnChainFunc: ConchainSpecialInfoupdate, pUser: this);
218 Console()->Chain(pName: "player7_skin_marking", pfnChainFunc: ConchainSpecialInfoupdate, pUser: this);
219 Console()->Chain(pName: "player7_skin_decoration", pfnChainFunc: ConchainSpecialInfoupdate, pUser: this);
220 Console()->Chain(pName: "player7_skin_hands", pfnChainFunc: ConchainSpecialInfoupdate, pUser: this);
221 Console()->Chain(pName: "player7_skin_feet", pfnChainFunc: ConchainSpecialInfoupdate, pUser: this);
222 Console()->Chain(pName: "player7_skin_eyes", pfnChainFunc: ConchainSpecialInfoupdate, pUser: this);
223 Console()->Chain(pName: "player7_color_body", pfnChainFunc: ConchainSpecialInfoupdate, pUser: this);
224 Console()->Chain(pName: "player7_color_marking", pfnChainFunc: ConchainSpecialInfoupdate, pUser: this);
225 Console()->Chain(pName: "player7_color_decoration", pfnChainFunc: ConchainSpecialInfoupdate, pUser: this);
226 Console()->Chain(pName: "player7_color_hands", pfnChainFunc: ConchainSpecialInfoupdate, pUser: this);
227 Console()->Chain(pName: "player7_color_feet", pfnChainFunc: ConchainSpecialInfoupdate, pUser: this);
228 Console()->Chain(pName: "player7_color_eyes", pfnChainFunc: ConchainSpecialInfoupdate, pUser: this);
229 Console()->Chain(pName: "player7_use_custom_color_body", pfnChainFunc: ConchainSpecialInfoupdate, pUser: this);
230 Console()->Chain(pName: "player7_use_custom_color_marking", pfnChainFunc: ConchainSpecialInfoupdate, pUser: this);
231 Console()->Chain(pName: "player7_use_custom_color_decoration", pfnChainFunc: ConchainSpecialInfoupdate, pUser: this);
232 Console()->Chain(pName: "player7_use_custom_color_hands", pfnChainFunc: ConchainSpecialInfoupdate, pUser: this);
233 Console()->Chain(pName: "player7_use_custom_color_feet", pfnChainFunc: ConchainSpecialInfoupdate, pUser: this);
234 Console()->Chain(pName: "player7_use_custom_color_eyes", pfnChainFunc: ConchainSpecialInfoupdate, pUser: this);
235
236 Console()->Chain(pName: "dummy_name", pfnChainFunc: ConchainSpecialDummyInfoupdate, pUser: this);
237 Console()->Chain(pName: "dummy_clan", pfnChainFunc: ConchainSpecialDummyInfoupdate, pUser: this);
238 Console()->Chain(pName: "dummy_country", pfnChainFunc: ConchainSpecialDummyInfoupdate, pUser: this);
239 Console()->Chain(pName: "dummy_use_custom_color", pfnChainFunc: ConchainSpecialDummyInfoupdate, pUser: this);
240 Console()->Chain(pName: "dummy_color_body", pfnChainFunc: ConchainSpecialDummyInfoupdate, pUser: this);
241 Console()->Chain(pName: "dummy_color_feet", pfnChainFunc: ConchainSpecialDummyInfoupdate, pUser: this);
242 Console()->Chain(pName: "dummy_skin", pfnChainFunc: ConchainSpecialDummyInfoupdate, pUser: this);
243
244 Console()->Chain(pName: "dummy7_skin", pfnChainFunc: ConchainSpecialDummyInfoupdate, pUser: this);
245 Console()->Chain(pName: "dummy7_skin_body", pfnChainFunc: ConchainSpecialDummyInfoupdate, pUser: this);
246 Console()->Chain(pName: "dummy7_skin_marking", pfnChainFunc: ConchainSpecialDummyInfoupdate, pUser: this);
247 Console()->Chain(pName: "dummy7_skin_decoration", pfnChainFunc: ConchainSpecialDummyInfoupdate, pUser: this);
248 Console()->Chain(pName: "dummy7_skin_hands", pfnChainFunc: ConchainSpecialDummyInfoupdate, pUser: this);
249 Console()->Chain(pName: "dummy7_skin_feet", pfnChainFunc: ConchainSpecialDummyInfoupdate, pUser: this);
250 Console()->Chain(pName: "dummy7_skin_eyes", pfnChainFunc: ConchainSpecialDummyInfoupdate, pUser: this);
251 Console()->Chain(pName: "dummy7_color_body", pfnChainFunc: ConchainSpecialDummyInfoupdate, pUser: this);
252 Console()->Chain(pName: "dummy7_color_marking", pfnChainFunc: ConchainSpecialDummyInfoupdate, pUser: this);
253 Console()->Chain(pName: "dummy7_color_decoration", pfnChainFunc: ConchainSpecialDummyInfoupdate, pUser: this);
254 Console()->Chain(pName: "dummy7_color_hands", pfnChainFunc: ConchainSpecialDummyInfoupdate, pUser: this);
255 Console()->Chain(pName: "dummy7_color_feet", pfnChainFunc: ConchainSpecialDummyInfoupdate, pUser: this);
256 Console()->Chain(pName: "dummy7_color_eyes", pfnChainFunc: ConchainSpecialDummyInfoupdate, pUser: this);
257 Console()->Chain(pName: "dummy7_use_custom_color_body", pfnChainFunc: ConchainSpecialDummyInfoupdate, pUser: this);
258 Console()->Chain(pName: "dummy7_use_custom_color_marking", pfnChainFunc: ConchainSpecialDummyInfoupdate, pUser: this);
259 Console()->Chain(pName: "dummy7_use_custom_color_decoration", pfnChainFunc: ConchainSpecialDummyInfoupdate, pUser: this);
260 Console()->Chain(pName: "dummy7_use_custom_color_hands", pfnChainFunc: ConchainSpecialDummyInfoupdate, pUser: this);
261 Console()->Chain(pName: "dummy7_use_custom_color_feet", pfnChainFunc: ConchainSpecialDummyInfoupdate, pUser: this);
262 Console()->Chain(pName: "dummy7_use_custom_color_eyes", pfnChainFunc: ConchainSpecialDummyInfoupdate, pUser: this);
263
264 Console()->Chain(pName: "cl_skin_download_url", pfnChainFunc: ConchainRefreshSkins, pUser: this);
265 Console()->Chain(pName: "cl_skin_community_download_url", pfnChainFunc: ConchainRefreshSkins, pUser: this);
266 Console()->Chain(pName: "cl_skin_prefix", pfnChainFunc: ConchainRefreshSkins, pUser: this);
267 Console()->Chain(pName: "cl_download_skins", pfnChainFunc: ConchainRefreshSkins, pUser: this);
268 Console()->Chain(pName: "cl_download_community_skins", pfnChainFunc: ConchainRefreshSkins, pUser: this);
269 Console()->Chain(pName: "cl_vanilla_skins_only", pfnChainFunc: ConchainRefreshSkins, pUser: this);
270
271 Console()->Chain(pName: "cl_dummy", pfnChainFunc: ConchainSpecialDummy, pUser: this);
272
273 Console()->Chain(pName: "cl_menu_map", pfnChainFunc: ConchainMenuMap, pUser: this);
274}
275
276static void GenerateTimeoutCode(char *pTimeoutCode)
277{
278 if(pTimeoutCode[0] == '\0' || str_comp(a: pTimeoutCode, b: "hGuEYnfxicsXGwFq") == 0)
279 {
280 for(unsigned int i = 0; i < 16; i++)
281 {
282 if(rand() % 2)
283 pTimeoutCode[i] = (char)((rand() % ('z' - 'a' + 1)) + 'a');
284 else
285 pTimeoutCode[i] = (char)((rand() % ('Z' - 'A' + 1)) + 'A');
286 }
287 }
288}
289
290void CGameClient::InitializeLanguage()
291{
292 // set the language
293 g_Localization.LoadIndexfile(pStorage: Storage(), pConsole: Console());
294 if(g_Config.m_ClShowWelcome)
295 g_Localization.SelectDefaultLanguage(pConsole: Console(), pFilename: g_Config.m_ClLanguagefile, Length: sizeof(g_Config.m_ClLanguagefile));
296 g_Localization.Load(pFilename: g_Config.m_ClLanguagefile, pStorage: Storage(), pConsole: Console());
297}
298
299void CGameClient::ForceUpdateConsoleRemoteCompletionSuggestions()
300{
301 m_GameConsole.ForceUpdateRemoteCompletionSuggestions();
302}
303
304void CGameClient::OnInit()
305{
306 const int64_t OnInitStart = time_get();
307
308 Client()->SetLoadingCallback([this](IClient::ELoadingCallbackDetail Detail) {
309 const char *pTitle;
310 if(Detail == IClient::LOADING_CALLBACK_DETAIL_DEMO || DemoPlayer()->IsPlaying())
311 {
312 pTitle = Localize(pStr: "Preparing demo playback");
313 }
314 else
315 {
316 pTitle = Localize(pStr: "Connected");
317 }
318
319 const char *pMessage;
320 switch(Detail)
321 {
322 case IClient::LOADING_CALLBACK_DETAIL_MAP:
323 pMessage = Localize(pStr: "Loading map file from storage");
324 break;
325 case IClient::LOADING_CALLBACK_DETAIL_DEMO:
326 pMessage = Localize(pStr: "Loading demo file from storage");
327 break;
328 default:
329 dbg_assert_failed("Invalid callback loading detail");
330 }
331 m_Menus.RenderLoading(pCaption: pTitle, pContent: pMessage, IncreaseCounter: 0);
332 });
333
334 m_pGraphics = Kernel()->RequestInterface<IGraphics>();
335
336 // propagate pointers
337 m_UI.Init(pKernel: Kernel());
338 m_RenderTools.Init(pGraphics: Graphics(), pTextRender: TextRender());
339 m_RenderMap.Init(pGraphics: Graphics(), pTextRender: TextRender());
340
341 if(GIT_SHORTREV_HASH)
342 {
343 str_format(buffer: m_aDDNetVersionStr, buffer_size: sizeof(m_aDDNetVersionStr), format: "%s %s (%s)", GAME_NAME, GAME_RELEASE_VERSION, GIT_SHORTREV_HASH);
344 }
345 else
346 {
347 str_format(buffer: m_aDDNetVersionStr, buffer_size: sizeof(m_aDDNetVersionStr), format: "%s %s", GAME_NAME, GAME_RELEASE_VERSION);
348 }
349
350 // TODO: this should be different
351 // setup item sizes
352 for(int i = 0; i < NUM_NETOBJTYPES; i++)
353 Client()->SnapSetStaticsize(ItemType: i, Size: m_NetObjHandler.GetObjSize(Type: i));
354 // HACK: only set static size for items, which were available in the first 0.7 release
355 // so new items don't break the snapshot delta
356 static const int OLD_NUM_NETOBJTYPES = 23;
357 for(int i = 0; i < OLD_NUM_NETOBJTYPES; i++)
358 Client()->SnapSetStaticsize7(ItemType: i, Size: m_NetObjHandler7.GetObjSize(Type: i));
359
360 if(!TextRender()->LoadFonts())
361 {
362 Client()->AddWarning(Warning: SWarning(Localize(pStr: "Some fonts could not be loaded. Check the local console for details.")));
363 }
364 TextRender()->SetFontLanguageVariant(g_Config.m_ClLanguagefile);
365
366 // update and swap after font loading, they are quite huge
367 Client()->UpdateAndSwap();
368
369 const char *pLoadingDDNetCaption = Localize(pStr: "Loading DDNet Client");
370 const char *pLoadingMessageComponents = Localize(pStr: "Initializing components");
371 const char *pLoadingMessageComponentsSpecial = Localize(pStr: "Why are you slowmo replaying to read this?");
372 char aLoadingMessage[256];
373
374 // init all components
375 int SkippedComps = 1;
376 int CompCounter = 1;
377 const int NumComponents = ComponentCount();
378 for(int i = NumComponents - 1; i >= 0; --i)
379 {
380 m_vpAll[i]->OnInit();
381 // try to render a frame after each component, also flushes GPU uploads
382 if(m_Menus.IsInit())
383 {
384 str_format(buffer: aLoadingMessage, buffer_size: std::size(aLoadingMessage), format: "%s [%d/%d]", CompCounter == NumComponents ? pLoadingMessageComponentsSpecial : pLoadingMessageComponents, CompCounter, NumComponents);
385 m_Menus.RenderLoading(pCaption: pLoadingDDNetCaption, pContent: aLoadingMessage, IncreaseCounter: SkippedComps);
386 SkippedComps = 1;
387 }
388 else
389 {
390 ++SkippedComps;
391 }
392 ++CompCounter;
393 }
394
395 m_GameSkinLoaded = false;
396 m_ParticlesSkinLoaded = false;
397 m_EmoticonsSkinLoaded = false;
398 m_HudSkinLoaded = false;
399
400 // setup load amount, load textures
401 const char *pLoadingMessageAssets = Localize(pStr: "Initializing assets");
402 for(int i = 0; i < g_pData->m_NumImages; i++)
403 {
404 if(i == IMAGE_GAME)
405 LoadGameSkin(pPath: g_Config.m_ClAssetGame);
406 else if(i == IMAGE_EMOTICONS)
407 LoadEmoticonsSkin(pPath: g_Config.m_ClAssetEmoticons);
408 else if(i == IMAGE_PARTICLES)
409 LoadParticlesSkin(pPath: g_Config.m_ClAssetParticles);
410 else if(i == IMAGE_HUD)
411 LoadHudSkin(pPath: g_Config.m_ClAssetHud);
412 else if(i == IMAGE_EXTRAS)
413 LoadExtrasSkin(pPath: g_Config.m_ClAssetExtras);
414 else if(g_pData->m_aImages[i].m_pFilename[0] == '\0') // handle special null image without filename
415 g_pData->m_aImages[i].m_Id = IGraphics::CTextureHandle();
416 else
417 g_pData->m_aImages[i].m_Id = Graphics()->LoadTexture(pFilename: g_pData->m_aImages[i].m_pFilename, StorageType: IStorage::TYPE_ALL);
418 m_Menus.RenderLoading(pCaption: pLoadingDDNetCaption, pContent: pLoadingMessageAssets, IncreaseCounter: 1);
419 }
420
421 m_GameWorld.Init(pCollision: Collision(), pTuningList: m_aTuningList, pMapBugs: &m_MapBugs);
422 OnReset();
423
424 // Set free binds to DDRace binds if it's active
425 m_Binds.SetDDRaceBinds(true);
426
427 GenerateTimeoutCode(pTimeoutCode: g_Config.m_ClTimeoutCode);
428 GenerateTimeoutCode(pTimeoutCode: g_Config.m_ClDummyTimeoutCode);
429
430 // Aggressively try to grab window again since some Windows users report
431 // window not being focused after starting client.
432 Graphics()->SetWindowGrab(true);
433
434 CChecksumData *pChecksum = Client()->ChecksumData();
435 pChecksum->m_SizeofGameClient = sizeof(*this);
436 pChecksum->m_NumComponents = m_vpAll.size();
437 for(size_t i = 0; i < m_vpAll.size(); i++)
438 {
439 if(i >= std::size(pChecksum->m_aComponentsChecksum))
440 {
441 break;
442 }
443 int Size = m_vpAll[i]->Sizeof();
444 pChecksum->m_aComponentsChecksum[i] = Size;
445 }
446
447 m_Menus.FinishLoading();
448 log_trace("gameclient", "initialization finished after %.2fms", (time_get() - OnInitStart) * 1000.0f / (float)time_freq());
449}
450
451void CGameClient::OnUpdate()
452{
453 HandleLanguageChanged();
454
455 CUIElementBase::Init(pUI: Ui()); // update static pointer because game and editor use separate UI
456
457 // handle mouse movement
458 float x = 0.0f, y = 0.0f;
459 IInput::ECursorType CursorType = Input()->CursorRelative(pX: &x, pY: &y);
460 if(CursorType != IInput::CURSOR_NONE)
461 {
462 for(auto &pComponent : m_vpInput)
463 {
464 if(pComponent->OnCursorMove(x, y, CursorType))
465 break;
466 }
467 }
468
469 // handle touch events
470 const std::vector<IInput::CTouchFingerState> &vTouchFingerStates = Input()->TouchFingerStates();
471 bool TouchHandled = false;
472 for(auto &pComponent : m_vpInput)
473 {
474 if(TouchHandled)
475 {
476 // Also update inactive components so they can handle touch fingers being released.
477 pComponent->OnTouchState(vTouchFingerStates: {});
478 }
479 else if(pComponent->OnTouchState(vTouchFingerStates))
480 {
481 Input()->ClearTouchDeltas();
482 TouchHandled = true;
483 }
484 }
485
486 // handle key presses
487 Input()->ConsumeEvents(Consumer: [&](const IInput::CEvent &Event) {
488 for(auto &pComponent : m_vpInput)
489 {
490 // Events with flag `FLAG_RELEASE` must always be forwarded to all components so keys being
491 // released can be handled in all components also after some components have been disabled.
492 if(pComponent->OnInput(Event) && (Event.m_Flags & ~IInput::FLAG_RELEASE) != 0)
493 break;
494 }
495 });
496
497 if(g_Config.m_ClSubTickAiming && m_Binds.m_MouseOnAction)
498 {
499 m_Controls.m_aMousePosOnAction[g_Config.m_ClDummy] = m_Controls.m_aMousePos[g_Config.m_ClDummy];
500 m_Binds.m_MouseOnAction = false;
501 }
502
503 for(auto &pComponent : m_vpAll)
504 {
505 pComponent->OnUpdate();
506 }
507}
508
509void CGameClient::OnDummySwap()
510{
511 if(g_Config.m_ClDummyResetOnSwitch)
512 {
513 int PlayerOrDummy = (g_Config.m_ClDummyResetOnSwitch == 2) ? g_Config.m_ClDummy : (!g_Config.m_ClDummy);
514 m_Controls.ResetInput(Dummy: PlayerOrDummy);
515 m_Controls.m_aInputData[PlayerOrDummy].m_Hook = 0;
516 }
517 const int PrevDummyFire = m_DummyInput.m_Fire;
518 m_DummyInput = m_Controls.m_aInputData[!g_Config.m_ClDummy];
519 m_Controls.m_aInputData[g_Config.m_ClDummy].m_Fire = PrevDummyFire;
520 m_IsDummySwapping = 1;
521}
522
523int CGameClient::OnSnapInput(int *pData, bool Dummy, bool Force)
524{
525 if(!Dummy)
526 {
527 return m_Controls.SnapInput(pData);
528 }
529 if(m_aLocalIds[!g_Config.m_ClDummy] < 0)
530 {
531 return 0;
532 }
533
534 if(!g_Config.m_ClDummyHammer)
535 {
536 if(m_DummyFire != 0)
537 {
538 m_DummyInput.m_Fire = (m_HammerInput.m_Fire + 1) & ~1;
539 m_DummyFire = 0;
540 }
541
542 if(!Force && (!m_DummyInput.m_Direction && !m_DummyInput.m_Jump && !m_DummyInput.m_Hook))
543 {
544 return 0;
545 }
546
547 mem_copy(dest: pData, source: &m_DummyInput, size: sizeof(m_DummyInput));
548 return sizeof(m_DummyInput);
549 }
550 else
551 {
552 if(m_DummyFire % 25 != 0)
553 {
554 m_DummyFire++;
555 return 0;
556 }
557 m_DummyFire++;
558
559 m_HammerInput.m_Fire = (m_HammerInput.m_Fire + 1) | 1;
560 m_HammerInput.m_WantedWeapon = WEAPON_HAMMER + 1;
561 if(!g_Config.m_ClDummyRestoreWeapon)
562 {
563 m_DummyInput.m_WantedWeapon = WEAPON_HAMMER + 1;
564 }
565
566 const vec2 Dir = m_LocalCharacterPos - m_aClients[m_aLocalIds[!g_Config.m_ClDummy]].m_Predicted.m_Pos;
567 m_HammerInput.m_TargetX = (int)Dir.x;
568 m_HammerInput.m_TargetY = (int)Dir.y;
569
570 mem_copy(dest: pData, source: &m_HammerInput, size: sizeof(m_HammerInput));
571 return sizeof(m_HammerInput);
572 }
573}
574
575void CGameClient::OnConnected()
576{
577 const char *pConnectCaption = DemoPlayer()->IsPlaying() ? Localize(pStr: "Preparing demo playback") : Localize(pStr: "Connected");
578 const char *pLoadMapContent = Localize(pStr: "Initializing map logic");
579 // render loading before skip is calculated
580 m_Menus.RenderLoading(pCaption: pConnectCaption, pContent: pLoadMapContent, IncreaseCounter: 0);
581 m_Layers.Init(pMap: Kernel()->RequestInterface<IMap>(), GameOnly: false);
582 m_Collision.Init(pLayers: Layers());
583 m_GameWorld.m_Core.InitSwitchers(HighestSwitchNumber: m_Collision.m_HighestSwitchNumber);
584 m_RaceHelper.Init(pGameClient: this);
585
586 // render loading before going through all components
587 m_Menus.RenderLoading(pCaption: pConnectCaption, pContent: pLoadMapContent, IncreaseCounter: 0);
588 for(auto &pComponent : m_vpAll)
589 {
590 pComponent->OnMapLoad();
591 pComponent->OnReset();
592 }
593
594 ConfigManager()->ResetGameSettings();
595 LoadMapSettings();
596
597 if(Client()->State() != IClient::STATE_DEMOPLAYBACK)
598 {
599 Client()->SetLoadingStateDetail(IClient::LOADING_STATE_DETAIL_GETTING_READY);
600 m_Menus.RenderLoading(pCaption: pConnectCaption, pContent: Localize(pStr: "Sending initial client info"), IncreaseCounter: 0);
601
602 // send the initial info
603 SendInfo(Start: true);
604 // we should keep this in for now, because otherwise you can't spectate
605 // people at start as the other info 64 packet is only sent after the first
606 // snap
607 Client()->Rcon(pLine: "crashmeplx");
608
609 if(g_Config.m_ClAutoDemoOnConnect)
610 Client()->DemoRecorder_HandleAutoStart();
611
612 m_LocalServer.RconAuthIfPossible();
613 }
614}
615
616void CGameClient::OnReset()
617{
618 InvalidateSnapshot();
619
620 m_EditorMovementDelay = 5;
621
622 m_PredictedTick = -1;
623 std::fill(first: std::begin(arr&: m_aLastNewPredictedTick), last: std::end(arr&: m_aLastNewPredictedTick), value: -1);
624
625 m_LastRoundStartTick = -1;
626 m_LastRaceTick = -1;
627 m_LastFlagCarrierRed = -4;
628 m_LastFlagCarrierBlue = -4;
629
630 std::fill(first: std::begin(arr&: m_aCheckInfo), last: std::end(arr&: m_aCheckInfo), value: -1);
631
632 // m_aDDNetVersionStr is initialized once in OnInit
633
634 std::fill(first: std::begin(arr&: m_aLastPos), last: std::end(arr&: m_aLastPos), value: vec2(0.0f, 0.0f));
635 std::fill(first: std::begin(arr&: m_aLastActive), last: std::end(arr&: m_aLastActive), value: false);
636
637 m_GameOver = false;
638 m_GamePaused = false;
639
640 m_SuppressEvents = false;
641 m_NewTick = false;
642 m_NewPredictedTick = false;
643
644 m_aFlagDropTick[TEAM_RED] = 0;
645 m_aFlagDropTick[TEAM_BLUE] = 0;
646
647 m_ServerMode = SERVERMODE_PURE;
648 mem_zero(block: &m_GameInfo, size: sizeof(m_GameInfo));
649
650 m_DemoSpecId = SPEC_FOLLOW;
651 m_LocalCharacterPos = vec2(0.0f, 0.0f);
652
653 m_PredictedPrevChar.Reset();
654 m_PredictedChar.Reset();
655
656 // m_Snap was cleared in InvalidateSnapshot
657
658 std::fill(first: std::begin(arr&: m_aLocalTuneZone), last: std::end(arr&: m_aLocalTuneZone), value: -1);
659 std::fill(first: std::begin(arr&: m_aReceivedTuning), last: std::end(arr&: m_aReceivedTuning), value: false);
660 std::fill(first: std::begin(arr&: m_aExpectingTuningForZone), last: std::end(arr&: m_aExpectingTuningForZone), value: -1);
661 std::fill(first: std::begin(arr&: m_aExpectingTuningSince), last: std::end(arr&: m_aExpectingTuningSince), value: 0);
662 std::fill(first: std::begin(arr&: m_aTuning), last: std::end(arr&: m_aTuning), value: CTuningParams());
663
664 m_ActiveRecordings.reset();
665
666 for(auto &Client : m_aClients)
667 Client.Reset();
668
669 for(auto &Stats : m_aStats)
670 Stats.Reset();
671
672 std::fill(first: std::begin(arr&: m_aNextChangeInfo), last: std::end(arr&: m_aNextChangeInfo), value: -1);
673 std::fill(first: std::begin(arr&: m_aLocalIds), last: std::end(arr&: m_aLocalIds), value: -1);
674 m_DummyInput = {};
675 m_HammerInput = {};
676 m_DummyFire = 0;
677 m_ReceivedDDNetPlayer = false;
678
679 m_Teams.Reset();
680 m_GameWorld.Clear();
681 m_GameWorld.m_WorldConfig.m_InfiniteAmmo = true;
682 m_PredictedWorld.CopyWorld(pFrom: &m_GameWorld);
683 m_PrevPredictedWorld.CopyWorld(pFrom: &m_PredictedWorld);
684
685 m_vSnapEntities.clear();
686
687 std::fill(first: std::begin(arr&: m_aDDRaceMsgSent), last: std::end(arr&: m_aDDRaceMsgSent), value: false);
688 std::fill(first: std::begin(arr&: m_aShowOthers), last: std::end(arr&: m_aShowOthers), value: SHOW_OTHERS_NOT_SET);
689 std::fill(first: std::begin(arr&: m_aEnableSpectatorCount), last: std::end(arr&: m_aEnableSpectatorCount), value: -1);
690 std::fill(first: std::begin(arr&: m_aLastUpdateTick), last: std::end(arr&: m_aLastUpdateTick), value: 0);
691
692 m_IsDummySwapping = false;
693 m_CharOrder.Reset();
694 std::fill(first: std::begin(arr&: m_aSwitchStateTeam), last: std::end(arr&: m_aSwitchStateTeam), value: -1);
695
696 // m_MapBugs and m_aTuningList are reset in LoadMapSettings
697
698 m_LastShowDistanceZoom = 0.0f;
699 m_LastZoom = 0.0f;
700 m_LastScreenAspect = 0.0f;
701 m_LastDeadzone = 0.0f;
702 m_LastFollowFactor = 0.0f;
703 m_LastDummyConnected = false;
704
705 m_MultiViewPersonalZoom = 0.0f;
706 m_MultiViewActivated = false;
707 m_MultiView.m_IsInit = false;
708
709 m_CursorInfo.m_CursorOwnerId = -1;
710 m_CursorInfo.m_NumSamples = 0;
711
712 for(auto &pComponent : m_vpAll)
713 pComponent->OnReset();
714
715 Editor()->ResetMentions();
716 Editor()->ResetIngameMoved();
717
718 Collision()->Unload();
719 Layers()->Unload();
720}
721
722void CGameClient::UpdatePositions()
723{
724 // local character position
725 if(g_Config.m_ClPredict && Client()->State() != IClient::STATE_DEMOPLAYBACK)
726 {
727 if(!AntiPingPlayers())
728 {
729 if(!m_Snap.m_pLocalCharacter || (m_Snap.m_pGameInfoObj && m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER))
730 {
731 // don't use predicted
732 }
733 else
734 m_LocalCharacterPos = mix(a: m_PredictedPrevChar.m_Pos, b: m_PredictedChar.m_Pos, amount: Client()->PredIntraGameTick(Conn: g_Config.m_ClDummy));
735 }
736 else
737 {
738 if(!(m_Snap.m_pGameInfoObj && m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER))
739 {
740 if(m_Snap.m_pLocalCharacter)
741 m_LocalCharacterPos = mix(a: m_PredictedPrevChar.m_Pos, b: m_PredictedChar.m_Pos, amount: Client()->PredIntraGameTick(Conn: g_Config.m_ClDummy));
742 }
743 // else
744 // m_LocalCharacterPos = mix(m_PredictedPrevChar.m_Pos, m_PredictedChar.m_Pos, Client()->PredIntraGameTick(g_Config.m_ClDummy));
745 }
746 }
747 else if(m_Snap.m_pLocalCharacter && m_Snap.m_pLocalPrevCharacter)
748 {
749 m_LocalCharacterPos = mix(
750 a: vec2(m_Snap.m_pLocalPrevCharacter->m_X, m_Snap.m_pLocalPrevCharacter->m_Y),
751 b: vec2(m_Snap.m_pLocalCharacter->m_X, m_Snap.m_pLocalCharacter->m_Y), amount: Client()->IntraGameTick(Conn: g_Config.m_ClDummy));
752 }
753
754 // spectator position
755 if(m_Snap.m_SpecInfo.m_Active)
756 {
757 if(m_MultiViewActivated)
758 {
759 HandleMultiView();
760 }
761 else if(Client()->State() == IClient::STATE_DEMOPLAYBACK && m_DemoSpecId != SPEC_FOLLOW && m_Snap.m_SpecInfo.m_SpectatorId != SPEC_FREEVIEW)
762 {
763 m_Snap.m_SpecInfo.m_Position = mix(
764 a: vec2(m_Snap.m_aCharacters[m_Snap.m_SpecInfo.m_SpectatorId].m_Prev.m_X, m_Snap.m_aCharacters[m_Snap.m_SpecInfo.m_SpectatorId].m_Prev.m_Y),
765 b: vec2(m_Snap.m_aCharacters[m_Snap.m_SpecInfo.m_SpectatorId].m_Cur.m_X, m_Snap.m_aCharacters[m_Snap.m_SpecInfo.m_SpectatorId].m_Cur.m_Y),
766 amount: Client()->IntraGameTick(Conn: g_Config.m_ClDummy));
767 m_Snap.m_SpecInfo.m_UsePosition = true;
768 }
769 else if(m_Snap.m_pSpectatorInfo && ((Client()->State() == IClient::STATE_DEMOPLAYBACK && m_DemoSpecId == SPEC_FOLLOW) || (Client()->State() != IClient::STATE_DEMOPLAYBACK && m_Snap.m_SpecInfo.m_SpectatorId != SPEC_FREEVIEW)))
770 {
771 if(m_Snap.m_pPrevSpectatorInfo && m_Snap.m_pPrevSpectatorInfo->m_SpectatorId == m_Snap.m_pSpectatorInfo->m_SpectatorId)
772 m_Snap.m_SpecInfo.m_Position = mix(a: vec2(m_Snap.m_pPrevSpectatorInfo->m_X, m_Snap.m_pPrevSpectatorInfo->m_Y),
773 b: vec2(m_Snap.m_pSpectatorInfo->m_X, m_Snap.m_pSpectatorInfo->m_Y), amount: Client()->IntraGameTick(Conn: g_Config.m_ClDummy));
774 else
775 m_Snap.m_SpecInfo.m_Position = vec2(m_Snap.m_pSpectatorInfo->m_X, m_Snap.m_pSpectatorInfo->m_Y);
776 m_Snap.m_SpecInfo.m_UsePosition = true;
777 }
778 }
779
780 if(!m_MultiViewActivated && m_MultiView.m_IsInit)
781 ResetMultiView();
782
783 UpdateRenderedCharacters();
784}
785
786void CGameClient::OnRender()
787{
788 const ColorRGBA ClearColor = color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClOverlayEntities ? g_Config.m_ClBackgroundEntitiesColor : g_Config.m_ClBackgroundColor));
789 Graphics()->Clear(r: ClearColor.r, g: ClearColor.g, b: ClearColor.b);
790
791 // check if multi view got activated
792 if(!m_MultiView.m_IsInit && m_MultiViewActivated)
793 {
794 int TeamId = 0;
795 if(m_Snap.m_SpecInfo.m_SpectatorId >= 0)
796 TeamId = m_Teams.Team(ClientId: m_Snap.m_SpecInfo.m_SpectatorId);
797
798 if(TeamId > MAX_CLIENTS || TeamId < 0)
799 TeamId = 0;
800
801 if(!InitMultiView(Team: TeamId))
802 {
803 dbg_msg(sys: "MultiView", fmt: "No players found to spectate");
804 ResetMultiView();
805 }
806 }
807
808 // update the local character and spectate position
809 UpdatePositions();
810
811 // display warnings
812 if(m_Menus.CanDisplayWarning())
813 {
814 std::optional<SWarning> Warning = Graphics()->CurrentWarning();
815 if(!Warning.has_value())
816 {
817 Warning = Client()->CurrentWarning();
818 }
819 if(Warning.has_value())
820 {
821 const SWarning &TheWarning = Warning.value();
822 m_Menus.PopupWarning(pTopic: TheWarning.m_aWarningTitle[0] == '\0' ? Localize(pStr: "Warning") : TheWarning.m_aWarningTitle, pBody: TheWarning.m_aWarningMsg, pButton: Localize(pStr: "Ok"), Duration: TheWarning.m_AutoHide ? 10s : 0s);
823 }
824 }
825
826 // update camera data prior to CControls::OnRender to allow CControls::m_aTargetPos to compensate using camera data
827 m_Camera.UpdateCamera();
828
829 UpdateSpectatorCursor();
830
831 // render all systems
832 for(auto &pComponent : m_vpAll)
833 pComponent->OnRender();
834
835 // clear all events/input for this frame
836 Input()->Clear();
837
838 CLineInput::RenderCandidates();
839
840 const bool WasNewTick = m_NewTick;
841
842 // clear new tick flags
843 m_NewTick = false;
844 m_NewPredictedTick = false;
845
846 if(g_Config.m_ClDummy && !Client()->DummyConnected())
847 g_Config.m_ClDummy = 0;
848
849 // resend player and dummy info if it was filtered by server
850 if(m_aLocalIds[0] >= 0 && Client()->State() == IClient::STATE_ONLINE && !m_Menus.IsActive() && WasNewTick)
851 {
852 if(m_aCheckInfo[0] == 0)
853 {
854 if(m_pClient->IsSixup())
855 {
856 if(!GotWantedSkin7(Dummy: false))
857 SendSkinChange7(Dummy: false);
858 else
859 m_aCheckInfo[0] = -1;
860 }
861 else
862 {
863 if(
864 str_comp(a: m_aClients[m_aLocalIds[0]].m_aName, b: Client()->PlayerName()) ||
865 str_comp(a: m_aClients[m_aLocalIds[0]].m_aClan, b: g_Config.m_PlayerClan) ||
866 m_aClients[m_aLocalIds[0]].m_Country != g_Config.m_PlayerCountry ||
867 str_comp(a: m_aClients[m_aLocalIds[0]].m_aSkinName, b: g_Config.m_ClPlayerSkin) ||
868 m_aClients[m_aLocalIds[0]].m_UseCustomColor != g_Config.m_ClPlayerUseCustomColor ||
869 m_aClients[m_aLocalIds[0]].m_ColorBody != (int)g_Config.m_ClPlayerColorBody ||
870 m_aClients[m_aLocalIds[0]].m_ColorFeet != (int)g_Config.m_ClPlayerColorFeet)
871 SendInfo(Start: false);
872 else
873 m_aCheckInfo[0] = -1;
874 }
875 }
876
877 if(m_aCheckInfo[0] > 0)
878 {
879 m_aCheckInfo[0] -= minimum(a: Client()->GameTick(Conn: 0) - Client()->PrevGameTick(Conn: 0), b: m_aCheckInfo[0]);
880 }
881
882 if(m_aLocalIds[1] >= 0)
883 {
884 if(m_aCheckInfo[1] == 0)
885 {
886 if(m_pClient->IsSixup())
887 {
888 if(!GotWantedSkin7(Dummy: true))
889 SendSkinChange7(Dummy: true);
890 else
891 m_aCheckInfo[1] = -1;
892 }
893 else
894 {
895 if(
896 str_comp(a: m_aClients[m_aLocalIds[1]].m_aName, b: Client()->DummyName()) ||
897 str_comp(a: m_aClients[m_aLocalIds[1]].m_aClan, b: g_Config.m_ClDummyClan) ||
898 m_aClients[m_aLocalIds[1]].m_Country != g_Config.m_ClDummyCountry ||
899 str_comp(a: m_aClients[m_aLocalIds[1]].m_aSkinName, b: g_Config.m_ClDummySkin) ||
900 m_aClients[m_aLocalIds[1]].m_UseCustomColor != g_Config.m_ClDummyUseCustomColor ||
901 m_aClients[m_aLocalIds[1]].m_ColorBody != (int)g_Config.m_ClDummyColorBody ||
902 m_aClients[m_aLocalIds[1]].m_ColorFeet != (int)g_Config.m_ClDummyColorFeet)
903 SendDummyInfo(Start: false);
904 else
905 m_aCheckInfo[1] = -1;
906 }
907 }
908
909 if(m_aCheckInfo[1] > 0)
910 {
911 m_aCheckInfo[1] -= minimum(a: Client()->GameTick(Conn: 1) - Client()->PrevGameTick(Conn: 1), b: m_aCheckInfo[1]);
912 }
913 }
914 }
915
916 UpdateManagedTeeRenderInfos();
917}
918
919void CGameClient::OnDummyDisconnect()
920{
921 m_aLocalIds[1] = -1;
922 m_aDDRaceMsgSent[1] = false;
923 m_aShowOthers[1] = SHOW_OTHERS_NOT_SET;
924 m_aEnableSpectatorCount[1] = -1;
925 m_aLastNewPredictedTick[1] = -1;
926}
927
928int CGameClient::LastRaceTick() const
929{
930 return m_LastRaceTick;
931}
932
933int CGameClient::CurrentRaceTime() const
934{
935 if(m_LastRaceTick < 0)
936 {
937 return 0;
938 }
939 return (Client()->GameTick(Conn: g_Config.m_ClDummy) - m_LastRaceTick) / Client()->GameTickSpeed();
940}
941
942bool CGameClient::Predict() const
943{
944 if(!g_Config.m_ClPredict)
945 return false;
946
947 if(m_Snap.m_pGameInfoObj)
948 {
949 if(m_Snap.m_pGameInfoObj->m_GameStateFlags & (GAMESTATEFLAG_GAMEOVER | GAMESTATEFLAG_PAUSED))
950 {
951 return false;
952 }
953 }
954
955 if(Client()->State() == IClient::STATE_DEMOPLAYBACK)
956 return false;
957
958 return !m_Snap.m_SpecInfo.m_Active && m_Snap.m_pLocalCharacter;
959}
960
961ColorRGBA CGameClient::GetDDTeamColor(int DDTeam, float Lightness) const
962{
963 // Use golden angle to generate unique colors with distinct adjacent colors.
964 // The first DDTeam (team 1) gets angle 0°, i.e. red hue.
965 const float Hue = std::fmod(x: (DDTeam - 1) * (137.50776f / 360.0f), y: 1.0f);
966 return color_cast<ColorRGBA>(hsl: ColorHSLA(Hue, 1.0f, Lightness));
967}
968
969void CGameClient::FormatClientId(int ClientId, char (&aClientId)[16], EClientIdFormat Format) const
970{
971 if(Format == EClientIdFormat::NO_INDENT)
972 {
973 str_format(buffer: aClientId, buffer_size: sizeof(aClientId), format: "%d", ClientId);
974 }
975 else
976 {
977 const int HighestClientId = Format == EClientIdFormat::INDENT_AUTO ? m_Snap.m_HighestClientId : 64;
978 const char *pFigureSpace = " ";
979 char aNumber[8];
980 str_format(buffer: aNumber, buffer_size: sizeof(aNumber), format: "%d", ClientId);
981 aClientId[0] = '\0';
982 if(ClientId < 100 && HighestClientId >= 100)
983 {
984 str_append(dst&: aClientId, src: pFigureSpace);
985 }
986 if(ClientId < 10 && HighestClientId >= 10)
987 {
988 str_append(dst&: aClientId, src: pFigureSpace);
989 }
990 str_append(dst&: aClientId, src: aNumber);
991 }
992 str_append(dst&: aClientId, src: ": ");
993}
994
995void CGameClient::OnRelease()
996{
997 // release all systems
998 for(auto &pComponent : m_vpAll)
999 pComponent->OnRelease();
1000}
1001
1002void CGameClient::OnMessage(int MsgId, CUnpacker *pUnpacker, int Conn, bool Dummy)
1003{
1004 // special messages
1005 static_assert((int)NETMSGTYPE_SV_TUNEPARAMS == (int)protocol7::NETMSGTYPE_SV_TUNEPARAMS, "0.6 and 0.7 tune message id do not match");
1006 if(MsgId == NETMSGTYPE_SV_TUNEPARAMS)
1007 {
1008 // unpack the new tuning
1009 CTuningParams NewTuning;
1010 int *pParams = (int *)&NewTuning;
1011
1012 // No jetpack on DDNet incompatible servers,
1013 // jetpack strength will be received by tune params
1014 NewTuning.m_JetpackStrength = 0;
1015
1016 for(unsigned i = 0; i < sizeof(CTuningParams) / sizeof(int); i++)
1017 {
1018 // 31 is the magic number index of laser_damage
1019 // which was removed in 0.7
1020 // also in 0.6 it is unused so we just set it to 0
1021 const int Value = (Client()->IsSixup() && i == 30) ? 0 : pUnpacker->GetInt();
1022
1023 // check for unpacking errors
1024 if(pUnpacker->Error())
1025 break;
1026
1027 pParams[i] = Value;
1028 }
1029
1030 m_ServerMode = SERVERMODE_PURE;
1031
1032 m_aReceivedTuning[Conn] = true;
1033 // apply new tuning
1034 m_aTuning[Conn] = NewTuning;
1035 return;
1036 }
1037
1038 void *pRawMsg = TranslateGameMsg(pMsgId: &MsgId, pUnpacker, Conn);
1039
1040 if(!pRawMsg)
1041 {
1042 // the 0.7 version of this error message is printed on translation
1043 // in sixup/translate_game.cpp
1044 if(!Client()->IsSixup())
1045 {
1046 char aBuf[256];
1047 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "dropped weird message '%s' (%d), failed on '%s'", m_NetObjHandler.GetMsgName(Type: MsgId), MsgId, m_NetObjHandler.FailedMsgOn());
1048 Console()->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "client", pStr: aBuf);
1049 }
1050 return;
1051 }
1052
1053 if(MsgId == NETMSGTYPE_SV_CHANGEINFOCOOLDOWN)
1054 {
1055 CNetMsg_Sv_ChangeInfoCooldown *pMsg = (CNetMsg_Sv_ChangeInfoCooldown *)pRawMsg;
1056 m_aNextChangeInfo[Conn] = pMsg->m_WaitUntil;
1057 return;
1058 }
1059
1060 if(Dummy)
1061 {
1062 if(MsgId == NETMSGTYPE_SV_CHAT && m_aLocalIds[0] >= 0 && m_aLocalIds[1] >= 0)
1063 {
1064 CNetMsg_Sv_Chat *pMsg = (CNetMsg_Sv_Chat *)pRawMsg;
1065
1066 if((pMsg->m_Team == 1 && (m_aClients[m_aLocalIds[0]].m_Team != m_aClients[m_aLocalIds[1]].m_Team || m_Teams.Team(ClientId: m_aLocalIds[0]) != m_Teams.Team(ClientId: m_aLocalIds[1]))) || pMsg->m_Team > 1)
1067 {
1068 m_Chat.OnMessage(MsgType: MsgId, pRawMsg);
1069 }
1070 }
1071 return; // no need of all that stuff for the dummy
1072 }
1073
1074 // TODO: this should be done smarter
1075 for(auto &pComponent : m_vpAll)
1076 pComponent->OnMessage(Msg: MsgId, pRawMsg);
1077
1078 if(MsgId == NETMSGTYPE_SV_READYTOENTER)
1079 {
1080 Client()->EnterGame(Conn);
1081 }
1082 else if(MsgId == NETMSGTYPE_SV_EMOTICON)
1083 {
1084 CNetMsg_Sv_Emoticon *pMsg = (CNetMsg_Sv_Emoticon *)pRawMsg;
1085
1086 // apply
1087 m_aClients[pMsg->m_ClientId].m_Emoticon = pMsg->m_Emoticon;
1088 m_aClients[pMsg->m_ClientId].m_EmoticonStartTick = Client()->GameTick(Conn);
1089 m_aClients[pMsg->m_ClientId].m_EmoticonStartFraction = Client()->IntraGameTickSincePrev(Conn);
1090 }
1091 else if(MsgId == NETMSGTYPE_SV_SOUNDGLOBAL)
1092 {
1093 if(m_SuppressEvents)
1094 return;
1095
1096 // don't enqueue pseudo-global sounds from demos (created by PlayAndRecord)
1097 CNetMsg_Sv_SoundGlobal *pMsg = (CNetMsg_Sv_SoundGlobal *)pRawMsg;
1098 if(pMsg->m_SoundId == SOUND_CTF_DROP || pMsg->m_SoundId == SOUND_CTF_RETURN ||
1099 pMsg->m_SoundId == SOUND_CTF_CAPTURE || pMsg->m_SoundId == SOUND_CTF_GRAB_EN ||
1100 pMsg->m_SoundId == SOUND_CTF_GRAB_PL)
1101 {
1102 if(g_Config.m_SndGame)
1103 m_Sounds.Enqueue(Channel: CSounds::CHN_GLOBAL, SetId: pMsg->m_SoundId);
1104 }
1105 else
1106 {
1107 if(g_Config.m_SndGame)
1108 m_Sounds.Play(Channel: CSounds::CHN_GLOBAL, SetId: pMsg->m_SoundId, Volume: 1.0f);
1109 }
1110 }
1111 else if(MsgId == NETMSGTYPE_SV_TEAMSSTATE || MsgId == NETMSGTYPE_SV_TEAMSSTATELEGACY)
1112 {
1113 unsigned int i;
1114
1115 for(i = 0; i < MAX_CLIENTS; i++)
1116 {
1117 const int Team = pUnpacker->GetInt();
1118 if(!pUnpacker->Error() && Team >= TEAM_FLOCK && Team <= TEAM_SUPER)
1119 m_Teams.Team(ClientId: i, Team);
1120 else
1121 {
1122 m_Teams.Team(ClientId: i, Team: 0);
1123 break;
1124 }
1125 }
1126
1127 if(i <= 16)
1128 m_Teams.m_IsDDRace16 = true;
1129
1130 m_Ghost.m_AllowRestart = true;
1131 m_RaceDemo.m_AllowRestart = true;
1132 }
1133 else if(MsgId == NETMSGTYPE_SV_KILLMSG)
1134 {
1135 CNetMsg_Sv_KillMsg *pMsg = (CNetMsg_Sv_KillMsg *)pRawMsg;
1136 // reset character prediction
1137 if(!(m_GameWorld.m_WorldConfig.m_IsFNG && pMsg->m_Weapon == WEAPON_LASER))
1138 {
1139 m_CharOrder.GiveWeak(c: pMsg->m_Victim);
1140 if(CCharacter *pChar = m_GameWorld.GetCharacterById(Id: pMsg->m_Victim))
1141 pChar->ResetPrediction();
1142 m_GameWorld.ReleaseHooked(ClientId: pMsg->m_Victim);
1143 }
1144
1145 // if we are spectating a static id set (team 0) and somebody killed, and its not a guy in solo, we remove him from the list
1146 // never remove players from the list if it is a pvp server
1147 if(IsMultiViewIdSet() && m_MultiViewTeam == 0 && m_aMultiViewId[pMsg->m_Victim] && !m_aClients[pMsg->m_Victim].m_Spec && !m_MultiView.m_Solo && !m_GameInfo.m_Pvp)
1148 {
1149 m_aMultiViewId[pMsg->m_Victim] = false;
1150
1151 // if everyone of a team killed, we have no ids to spectate anymore, so we disable multi view
1152 if(!IsMultiViewIdSet())
1153 ResetMultiView();
1154 else
1155 {
1156 // the "main" tee killed, search a new one
1157 if(m_Snap.m_SpecInfo.m_SpectatorId == pMsg->m_Victim)
1158 {
1159 int NewClientId = FindFirstMultiViewId();
1160 if(NewClientId < MAX_CLIENTS && NewClientId >= 0)
1161 {
1162 CleanMultiViewId(ClientId: NewClientId);
1163 m_aMultiViewId[NewClientId] = true;
1164 m_Spectator.Spectate(SpectatorId: NewClientId);
1165 }
1166 }
1167 }
1168 }
1169 }
1170 else if(MsgId == NETMSGTYPE_SV_KILLMSGTEAM)
1171 {
1172 CNetMsg_Sv_KillMsgTeam *pMsg = (CNetMsg_Sv_KillMsgTeam *)pRawMsg;
1173
1174 // reset prediction
1175 std::vector<std::pair<int, int>> vStrongWeakSorted;
1176 for(int i = 0; i < MAX_CLIENTS; i++)
1177 {
1178 if(m_Teams.Team(ClientId: i) == pMsg->m_Team)
1179 {
1180 if(CCharacter *pChar = m_GameWorld.GetCharacterById(Id: i))
1181 {
1182 pChar->ResetPrediction();
1183 vStrongWeakSorted.emplace_back(args&: i, args: pMsg->m_First == i ? MAX_CLIENTS : (pChar ? pChar->GetStrongWeakId() : 0));
1184 }
1185 m_GameWorld.ReleaseHooked(ClientId: i);
1186 }
1187 }
1188 std::stable_sort(first: vStrongWeakSorted.begin(), last: vStrongWeakSorted.end(), comp: [](auto &Left, auto &Right) { return Left.second > Right.second; });
1189 for(auto Id : vStrongWeakSorted)
1190 {
1191 m_CharOrder.GiveWeak(c: Id.first);
1192 }
1193 }
1194 else if(MsgId == NETMSGTYPE_SV_MAPSOUNDGLOBAL)
1195 {
1196 if(m_SuppressEvents)
1197 return;
1198
1199 if(!g_Config.m_SndGame)
1200 return;
1201
1202 CNetMsg_Sv_MapSoundGlobal *pMsg = (CNetMsg_Sv_MapSoundGlobal *)pRawMsg;
1203 m_MapSounds.Play(Channel: CSounds::CHN_GLOBAL, SoundId: pMsg->m_SoundId);
1204 }
1205 else if(MsgId == NETMSGTYPE_SV_PREINPUT)
1206 {
1207 CNetMsg_Sv_PreInput *pMsg = (CNetMsg_Sv_PreInput *)pRawMsg;
1208 m_aClients[pMsg->m_Owner].m_aPreInputs[pMsg->m_IntendedTick % 200] = *pMsg;
1209 }
1210 else if(MsgId == NETMSGTYPE_SV_SAVECODE)
1211 {
1212 const CNetMsg_Sv_SaveCode *pMsg = (CNetMsg_Sv_SaveCode *)pRawMsg;
1213 OnSaveCodeNetMessage(pMsg);
1214 }
1215}
1216
1217void CGameClient::OnStateChange(int NewState, int OldState)
1218{
1219 // reset everything when not already connected (to keep gathered stuff)
1220 if(NewState < IClient::STATE_ONLINE)
1221 OnReset();
1222
1223 // then change the state
1224 for(auto &pComponent : m_vpAll)
1225 pComponent->OnStateChange(NewState, OldState);
1226}
1227
1228void CGameClient::OnShutdown()
1229{
1230 for(auto &pComponent : m_vpAll)
1231 pComponent->OnShutdown();
1232
1233 m_LocalServer.KillServer();
1234}
1235
1236void CGameClient::OnEnterGame()
1237{
1238}
1239
1240void CGameClient::OnGameOver()
1241{
1242 if(Client()->State() != IClient::STATE_DEMOPLAYBACK && g_Config.m_ClEditor == 0)
1243 Client()->AutoScreenshot_Start();
1244}
1245
1246void CGameClient::OnStartGame()
1247{
1248 if(Client()->State() != IClient::STATE_DEMOPLAYBACK && !g_Config.m_ClAutoDemoOnConnect)
1249 Client()->DemoRecorder_HandleAutoStart();
1250 m_Statboard.OnReset();
1251}
1252
1253void CGameClient::OnStartRound()
1254{
1255 // In GamePaused or GameOver state RoundStartTick is updated on each tick
1256 // hence no need to reset stats until player leaves GameOver
1257 // and it would be a mistake to reset stats after or during the pause
1258 m_Statboard.OnReset();
1259
1260 // Restart automatic race demo recording
1261 m_RaceDemo.OnReset();
1262}
1263
1264void CGameClient::OnFlagGrab(int TeamId)
1265{
1266 if(TeamId == TEAM_RED)
1267 m_aStats[m_Snap.m_pGameDataObj->m_FlagCarrierRed].m_FlagGrabs++;
1268 else
1269 m_aStats[m_Snap.m_pGameDataObj->m_FlagCarrierBlue].m_FlagGrabs++;
1270}
1271
1272void CGameClient::OnWindowResize()
1273{
1274 for(auto &pComponent : m_vpAll)
1275 pComponent->OnWindowResize();
1276
1277 Ui()->OnWindowResize();
1278}
1279
1280void CGameClient::OnLanguageChange()
1281{
1282 // The actual language change is delayed because it
1283 // might require clearing the text render font atlas,
1284 // which would invalidate text that is currently drawn.
1285 m_LanguageChanged = true;
1286}
1287
1288void CGameClient::HandleLanguageChanged()
1289{
1290 if(!m_LanguageChanged)
1291 return;
1292 m_LanguageChanged = false;
1293
1294 g_Localization.Load(pFilename: g_Config.m_ClLanguagefile, pStorage: Storage(), pConsole: Console());
1295 TextRender()->SetFontLanguageVariant(g_Config.m_ClLanguagefile);
1296
1297 // Clear all text containers
1298 Client()->OnWindowResize();
1299}
1300
1301void CGameClient::RenderShutdownMessage()
1302{
1303 const char *pMessage = nullptr;
1304 if(Client()->State() == IClient::STATE_QUITTING)
1305 pMessage = Localize(pStr: "Quitting. Please wait…");
1306 else if(Client()->State() == IClient::STATE_RESTARTING)
1307 pMessage = Localize(pStr: "Restarting. Please wait…");
1308 else
1309 dbg_assert_failed("Invalid client state for quitting message");
1310
1311 // This function only gets called after the render loop has already terminated, so we have to call Swap manually.
1312 Graphics()->Clear(r: 0.0f, g: 0.0f, b: 0.0f);
1313 Ui()->MapScreen();
1314 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
1315 Ui()->DoLabel(pRect: Ui()->Screen(), pText: pMessage, Size: 16.0f, Align: TEXTALIGN_MC);
1316 Graphics()->Swap();
1317 Graphics()->Clear(r: 0.0f, g: 0.0f, b: 0.0f);
1318}
1319
1320void CGameClient::ProcessDemoSnapshot(CSnapshot *pSnap)
1321{
1322 for(int Index = 0; Index < pSnap->NumItems(); Index++)
1323 {
1324 const CSnapshotItem *pItem = pSnap->GetItem(Index);
1325 int ItemType = pSnap->GetItemType(Index);
1326
1327 if(ItemType == NETOBJTYPE_PROJECTILE)
1328 {
1329 // for antiping: if the projectile netobjects from the server contains extra data, this is removed and the original content restored before recording demo
1330 CNetObj_Projectile *pProj = (CNetObj_Projectile *)((void *)pItem->Data());
1331 DemoObjectRemoveExtraProjectileInfo(pProj);
1332 }
1333 else if(ItemType == NETOBJTYPE_DDNETSPECTATORINFO)
1334 {
1335 // always record local camera info as follow mode
1336 CNetObj_DDNetSpectatorInfo *pDDNetSpectatorInfo = (CNetObj_DDNetSpectatorInfo *)((void *)pItem->Data());
1337 pDDNetSpectatorInfo->m_HasCameraInfo = true;
1338 pDDNetSpectatorInfo->m_Zoom = (m_Camera.m_Zooming ? m_Camera.m_ZoomSmoothingTarget : m_Camera.m_Zoom) * 1000.0f;
1339 pDDNetSpectatorInfo->m_Deadzone = m_Camera.Deadzone();
1340 pDDNetSpectatorInfo->m_FollowFactor = m_Camera.FollowFactor();
1341 }
1342 }
1343}
1344
1345void CGameClient::OnRconType(bool UsernameReq)
1346{
1347 m_GameConsole.RequireUsername(UsernameReq);
1348}
1349
1350void CGameClient::OnRconLine(const char *pLine)
1351{
1352 m_GameConsole.PrintLine(Type: CGameConsole::CONSOLETYPE_REMOTE, pLine);
1353}
1354
1355void CGameClient::ProcessEvents()
1356{
1357 if(m_SuppressEvents)
1358 return;
1359
1360 int SnapType = IClient::SNAP_CURRENT;
1361 int Num = Client()->SnapNumItems(SnapId: SnapType);
1362 for(int Index = 0; Index < Num; Index++)
1363 {
1364 const IClient::CSnapItem Item = Client()->SnapGetItem(SnapId: SnapType, Index);
1365
1366 // TODO: We don't have enough info about us, others, to know a correct alpha or volume value.
1367 const float Alpha = 1.0f;
1368 const float Volume = 1.0f;
1369
1370 if(Item.m_Type == NETEVENTTYPE_DAMAGEIND)
1371 {
1372 const CNetEvent_DamageInd *pEvent = (const CNetEvent_DamageInd *)Item.m_pData;
1373 m_Effects.DamageIndicator(Pos: vec2(pEvent->m_X, pEvent->m_Y), Dir: direction(angle: pEvent->m_Angle / 256.0f), Alpha);
1374 }
1375 else if(Item.m_Type == NETEVENTTYPE_EXPLOSION)
1376 {
1377 const CNetEvent_Explosion *pEvent = (const CNetEvent_Explosion *)Item.m_pData;
1378 m_Effects.Explosion(Pos: vec2(pEvent->m_X, pEvent->m_Y), Alpha);
1379 }
1380 else if(Item.m_Type == NETEVENTTYPE_HAMMERHIT)
1381 {
1382 const CNetEvent_HammerHit *pEvent = (const CNetEvent_HammerHit *)Item.m_pData;
1383 m_Effects.HammerHit(Pos: vec2(pEvent->m_X, pEvent->m_Y), Alpha, Volume);
1384 }
1385 else if(Item.m_Type == NETEVENTTYPE_BIRTHDAY)
1386 {
1387 const CNetEvent_Birthday *pEvent = (const CNetEvent_Birthday *)Item.m_pData;
1388 m_Effects.Confetti(Pos: vec2(pEvent->m_X, pEvent->m_Y), Alpha);
1389 }
1390 else if(Item.m_Type == NETEVENTTYPE_FINISH)
1391 {
1392 const CNetEvent_Finish *pEvent = (const CNetEvent_Finish *)Item.m_pData;
1393 m_Effects.Confetti(Pos: vec2(pEvent->m_X, pEvent->m_Y), Alpha);
1394 }
1395 else if(Item.m_Type == NETEVENTTYPE_SPAWN)
1396 {
1397 const CNetEvent_Spawn *pEvent = (const CNetEvent_Spawn *)Item.m_pData;
1398 m_Effects.PlayerSpawn(Pos: vec2(pEvent->m_X, pEvent->m_Y), Alpha, Volume);
1399 }
1400 else if(Item.m_Type == NETEVENTTYPE_DEATH)
1401 {
1402 const CNetEvent_Death *pEvent = (const CNetEvent_Death *)Item.m_pData;
1403 m_Effects.PlayerDeath(Pos: vec2(pEvent->m_X, pEvent->m_Y), ClientId: pEvent->m_ClientId, Alpha);
1404 }
1405 else if(Item.m_Type == NETEVENTTYPE_SOUNDWORLD)
1406 {
1407 const CNetEvent_SoundWorld *pEvent = (const CNetEvent_SoundWorld *)Item.m_pData;
1408 if(!Config()->m_SndGame)
1409 continue;
1410
1411 if(m_GameInfo.m_RaceSounds && ((pEvent->m_SoundId == SOUND_GUN_FIRE && !g_Config.m_SndGun) || (pEvent->m_SoundId == SOUND_PLAYER_PAIN_LONG && !g_Config.m_SndLongPain)))
1412 continue;
1413
1414 m_Sounds.PlayAt(Channel: CSounds::CHN_WORLD, SetId: pEvent->m_SoundId, Volume: 1.0f, Position: vec2(pEvent->m_X, pEvent->m_Y));
1415 }
1416 else if(Item.m_Type == NETEVENTTYPE_MAPSOUNDWORLD)
1417 {
1418 CNetEvent_MapSoundWorld *pEvent = (CNetEvent_MapSoundWorld *)Item.m_pData;
1419 if(!Config()->m_SndGame)
1420 continue;
1421
1422 m_MapSounds.PlayAt(Channel: CSounds::CHN_WORLD, SoundId: pEvent->m_SoundId, Position: vec2(pEvent->m_X, pEvent->m_Y));
1423 }
1424 }
1425}
1426
1427static CGameInfo GetGameInfo(const CNetObj_GameInfoEx *pInfoEx, int InfoExSize, const CServerInfo *pFallbackServerInfo)
1428{
1429 int Version = -1;
1430 if(InfoExSize >= 12)
1431 {
1432 Version = pInfoEx->m_Version;
1433 }
1434 else if(InfoExSize >= 8)
1435 {
1436 Version = minimum(a: pInfoEx->m_Version, b: 4);
1437 }
1438 else if(InfoExSize >= 4)
1439 {
1440 Version = 0;
1441 }
1442 int Flags = 0;
1443 if(Version >= 0)
1444 {
1445 Flags = pInfoEx->m_Flags;
1446 }
1447 int Flags2 = 0;
1448 if(Version >= 5)
1449 {
1450 Flags2 = pInfoEx->m_Flags2;
1451 }
1452 bool Race;
1453 bool FastCap;
1454 bool FNG;
1455 bool DDRace;
1456 bool DDNet;
1457 bool BlockWorlds;
1458 bool City;
1459 bool Vanilla;
1460 bool Plus;
1461 bool FDDrace;
1462 if(Version < 1)
1463 {
1464 // The game type is intentionally only available inside this
1465 // `if`. Game type sniffing should be avoided and ideally not
1466 // extended. Mods should set the relevant game flags instead.
1467 const char *pGameType = pFallbackServerInfo->m_aGameType;
1468 Race = str_find_nocase(haystack: pGameType, needle: "race") || str_find_nocase(haystack: pGameType, needle: "fastcap");
1469 FastCap = str_find_nocase(haystack: pGameType, needle: "fastcap");
1470 FNG = str_find_nocase(haystack: pGameType, needle: "fng");
1471 DDRace = str_find_nocase(haystack: pGameType, needle: "ddrace") || str_find_nocase(haystack: pGameType, needle: "mkrace");
1472 DDNet = str_find_nocase(haystack: pGameType, needle: "ddracenet") || str_find_nocase(haystack: pGameType, needle: "ddnet");
1473 BlockWorlds = str_startswith(str: pGameType, prefix: "bw ") || str_comp_nocase(a: pGameType, b: "bw") == 0;
1474 City = str_find_nocase(haystack: pGameType, needle: "city");
1475 Vanilla = str_comp(a: pGameType, b: "DM") == 0 || str_comp(a: pGameType, b: "TDM") == 0 || str_comp(a: pGameType, b: "CTF") == 0;
1476 Plus = str_find(haystack: pGameType, needle: "+");
1477 FDDrace = false;
1478 }
1479 else
1480 {
1481 Race = Flags & GAMEINFOFLAG_GAMETYPE_RACE;
1482 FastCap = Flags & GAMEINFOFLAG_GAMETYPE_FASTCAP;
1483 FNG = Flags & GAMEINFOFLAG_GAMETYPE_FNG;
1484 DDRace = Flags & GAMEINFOFLAG_GAMETYPE_DDRACE;
1485 DDNet = Flags & GAMEINFOFLAG_GAMETYPE_DDNET;
1486 BlockWorlds = Flags & GAMEINFOFLAG_GAMETYPE_BLOCK_WORLDS;
1487 Vanilla = Flags & GAMEINFOFLAG_GAMETYPE_VANILLA;
1488 Plus = Flags & GAMEINFOFLAG_GAMETYPE_PLUS;
1489 City = Version >= 5 && Flags2 & GAMEINFOFLAG2_GAMETYPE_CITY;
1490 FDDrace = Version >= 6 && Flags2 & GAMEINFOFLAG2_GAMETYPE_FDDRACE;
1491
1492 // Ensure invariants upheld by the server info parsing business.
1493 DDRace = DDRace || DDNet || FDDrace;
1494 Race = Race || FastCap || DDRace;
1495 }
1496
1497 CGameInfo Info;
1498 Info.m_FlagStartsRace = FastCap;
1499 Info.m_TimeScore = Race;
1500 Info.m_UnlimitedAmmo = Race;
1501 Info.m_DDRaceRecordMessage = DDRace && !DDNet;
1502 Info.m_RaceRecordMessage = DDNet || (Race && !DDRace);
1503 Info.m_RaceSounds = DDRace || FNG || BlockWorlds;
1504 Info.m_AllowEyeWheel = DDRace || BlockWorlds || City || Plus;
1505 Info.m_AllowHookColl = DDRace;
1506 Info.m_AllowZoom = Race || BlockWorlds || City;
1507 Info.m_BugDDRaceGhost = DDRace;
1508 Info.m_BugDDRaceInput = DDRace;
1509 Info.m_BugFNGLaserRange = FNG;
1510 Info.m_BugVanillaBounce = Vanilla;
1511 Info.m_PredictFNG = FNG;
1512 Info.m_PredictDDRace = DDRace;
1513 Info.m_PredictDDRaceTiles = DDRace && !BlockWorlds;
1514 Info.m_PredictVanilla = Vanilla || FastCap;
1515 Info.m_EntitiesDDNet = DDNet;
1516 Info.m_EntitiesDDRace = DDRace;
1517 Info.m_EntitiesRace = Race;
1518 Info.m_EntitiesFNG = FNG;
1519 Info.m_EntitiesVanilla = Vanilla;
1520 Info.m_EntitiesBW = BlockWorlds;
1521 Info.m_Race = Race;
1522 Info.m_Pvp = !Race;
1523 Info.m_DontMaskEntities = !DDNet;
1524 Info.m_AllowXSkins = false;
1525 Info.m_EntitiesFDDrace = FDDrace;
1526 Info.m_HudHealthArmor = true;
1527 Info.m_HudAmmo = true;
1528 Info.m_HudDDRace = false;
1529 Info.m_NoWeakHookAndBounce = false;
1530 Info.m_NoSkinChangeForFrozen = false;
1531 Info.m_DDRaceTeam = false;
1532
1533 if(Version >= 0)
1534 {
1535 Info.m_TimeScore = Flags & GAMEINFOFLAG_TIMESCORE;
1536 }
1537 if(Version >= 2)
1538 {
1539 Info.m_FlagStartsRace = Flags & GAMEINFOFLAG_FLAG_STARTS_RACE;
1540 Info.m_UnlimitedAmmo = Flags & GAMEINFOFLAG_UNLIMITED_AMMO;
1541 Info.m_DDRaceRecordMessage = Flags & GAMEINFOFLAG_DDRACE_RECORD_MESSAGE;
1542 Info.m_RaceRecordMessage = Flags & GAMEINFOFLAG_RACE_RECORD_MESSAGE;
1543 Info.m_AllowEyeWheel = Flags & GAMEINFOFLAG_ALLOW_EYE_WHEEL;
1544 Info.m_AllowHookColl = Flags & GAMEINFOFLAG_ALLOW_HOOK_COLL;
1545 Info.m_AllowZoom = Flags & GAMEINFOFLAG_ALLOW_ZOOM;
1546 Info.m_BugDDRaceGhost = Flags & GAMEINFOFLAG_BUG_DDRACE_GHOST;
1547 Info.m_BugDDRaceInput = Flags & GAMEINFOFLAG_BUG_DDRACE_INPUT;
1548 Info.m_BugFNGLaserRange = Flags & GAMEINFOFLAG_BUG_FNG_LASER_RANGE;
1549 Info.m_BugVanillaBounce = Flags & GAMEINFOFLAG_BUG_VANILLA_BOUNCE;
1550 Info.m_PredictFNG = Flags & GAMEINFOFLAG_PREDICT_FNG;
1551 Info.m_PredictDDRace = Flags & GAMEINFOFLAG_PREDICT_DDRACE;
1552 Info.m_PredictDDRaceTiles = Flags & GAMEINFOFLAG_PREDICT_DDRACE_TILES;
1553 Info.m_PredictVanilla = Flags & GAMEINFOFLAG_PREDICT_VANILLA;
1554 Info.m_EntitiesDDNet = Flags & GAMEINFOFLAG_ENTITIES_DDNET;
1555 Info.m_EntitiesDDRace = Flags & GAMEINFOFLAG_ENTITIES_DDRACE;
1556 Info.m_EntitiesRace = Flags & GAMEINFOFLAG_ENTITIES_RACE;
1557 Info.m_EntitiesFNG = Flags & GAMEINFOFLAG_ENTITIES_FNG;
1558 Info.m_EntitiesVanilla = Flags & GAMEINFOFLAG_ENTITIES_VANILLA;
1559 }
1560 if(Version >= 3)
1561 {
1562 Info.m_Race = Flags & GAMEINFOFLAG_RACE;
1563 Info.m_DontMaskEntities = Flags & GAMEINFOFLAG_DONT_MASK_ENTITIES;
1564 }
1565 if(Version >= 4)
1566 {
1567 Info.m_EntitiesBW = Flags & GAMEINFOFLAG_ENTITIES_BW;
1568 }
1569 if(Version >= 5)
1570 {
1571 Info.m_AllowXSkins = Flags2 & GAMEINFOFLAG2_ALLOW_X_SKINS;
1572 }
1573 if(Version >= 6)
1574 {
1575 Info.m_EntitiesFDDrace = Flags2 & GAMEINFOFLAG2_ENTITIES_FDDRACE;
1576 }
1577 if(Version >= 7)
1578 {
1579 Info.m_HudHealthArmor = Flags2 & GAMEINFOFLAG2_HUD_HEALTH_ARMOR;
1580 Info.m_HudAmmo = Flags2 & GAMEINFOFLAG2_HUD_AMMO;
1581 Info.m_HudDDRace = Flags2 & GAMEINFOFLAG2_HUD_DDRACE;
1582 }
1583 if(Version >= 8)
1584 {
1585 Info.m_NoWeakHookAndBounce = Flags2 & GAMEINFOFLAG2_NO_WEAK_HOOK;
1586 }
1587 if(Version >= 9)
1588 {
1589 Info.m_NoSkinChangeForFrozen = Flags2 & GAMEINFOFLAG2_NO_SKIN_CHANGE_FOR_FROZEN;
1590 }
1591 if(Version >= 10)
1592 {
1593 Info.m_DDRaceTeam = Flags2 & GAMEINFOFLAG2_DDRACE_TEAM;
1594 }
1595
1596 return Info;
1597}
1598
1599void CGameClient::InvalidateSnapshot()
1600{
1601 // clear all pointers
1602 mem_zero(block: &m_Snap, size: sizeof(m_Snap));
1603 m_Snap.m_SpecInfo.m_Zoom = 1.0f;
1604 m_Snap.m_LocalClientId = -1;
1605 SnapCollectEntities();
1606}
1607
1608void CGameClient::OnNewSnapshot()
1609{
1610 auto &&Evolve = [this](CNetObj_Character *pCharacter, int Tick) {
1611 CWorldCore TempWorld;
1612 CCharacterCore TempCore = CCharacterCore();
1613 CTeamsCore TempTeams = CTeamsCore();
1614 TempCore.Init(pWorld: &TempWorld, pCollision: Collision(), pTeams: &TempTeams);
1615 TempCore.Read(pObjCore: pCharacter);
1616 TempCore.m_ActiveWeapon = pCharacter->m_Weapon;
1617
1618 while(pCharacter->m_Tick < Tick)
1619 {
1620 pCharacter->m_Tick++;
1621 TempCore.Tick(UseInput: false);
1622 TempCore.Move();
1623 TempCore.Quantize();
1624 }
1625
1626 TempCore.Write(pObjCore: pCharacter);
1627 };
1628
1629 InvalidateSnapshot();
1630
1631 m_NewTick = true;
1632
1633 ProcessEvents();
1634
1635#ifdef CONF_DEBUG
1636 if(g_Config.m_DbgStress)
1637 {
1638 if((Client()->GameTick(Conn: g_Config.m_ClDummy) % 100) == 0)
1639 {
1640 char aMessage[64];
1641 int MsgLen = rand() % (sizeof(aMessage) - 1);
1642 for(int i = 0; i < MsgLen; i++)
1643 aMessage[i] = (char)('a' + (rand() % ('z' - 'a')));
1644 aMessage[MsgLen] = 0;
1645
1646 m_Chat.SendChat(Team: rand() & 1, pLine: aMessage);
1647 }
1648 }
1649#endif
1650
1651 CServerInfo ServerInfo;
1652 Client()->GetServerInfo(pServerInfo: &ServerInfo);
1653
1654 bool FoundGameInfoEx = false;
1655 bool GotSwitchStateTeam = false;
1656 m_aSwitchStateTeam[g_Config.m_ClDummy] = -1;
1657
1658 for(auto &Client : m_aClients)
1659 {
1660 Client.m_SpecCharPresent = false;
1661 }
1662
1663 // go through all the items in the snapshot and gather the info we want
1664 {
1665 m_Snap.m_aTeamSize[TEAM_RED] = m_Snap.m_aTeamSize[TEAM_BLUE] = 0;
1666
1667 int Num = Client()->SnapNumItems(SnapId: IClient::SNAP_CURRENT);
1668 for(int i = 0; i < Num; i++)
1669 {
1670 const IClient::CSnapItem Item = Client()->SnapGetItem(SnapId: IClient::SNAP_CURRENT, Index: i);
1671
1672 if(Item.m_Type == NETOBJTYPE_CLIENTINFO)
1673 {
1674 const CNetObj_ClientInfo *pInfo = (const CNetObj_ClientInfo *)Item.m_pData;
1675 int ClientId = Item.m_Id;
1676 if(ClientId < MAX_CLIENTS)
1677 {
1678 CClientData *pClient = &m_aClients[ClientId];
1679
1680 if(!IntsToStr(pInts: pInfo->m_aName, NumInts: std::size(pInfo->m_aName), pStr: pClient->m_aName, StrSize: std::size(pClient->m_aName)))
1681 {
1682 str_copy(dst&: pClient->m_aName, src: "nameless tee");
1683 }
1684 IntsToStr(pInts: pInfo->m_aClan, NumInts: std::size(pInfo->m_aClan), pStr: pClient->m_aClan, StrSize: std::size(pClient->m_aClan));
1685 pClient->m_Country = pInfo->m_Country;
1686
1687 IntsToStr(pInts: pInfo->m_aSkin, NumInts: std::size(pInfo->m_aSkin), pStr: pClient->m_aSkinName, StrSize: std::size(pClient->m_aSkinName));
1688 if(!CSkin::IsValidName(pName: pClient->m_aSkinName) ||
1689 (!m_GameInfo.m_AllowXSkins && CSkins::IsSpecialSkin(pName: pClient->m_aSkinName)))
1690 {
1691 str_copy(dst&: pClient->m_aSkinName, src: "default");
1692 }
1693
1694 pClient->m_UseCustomColor = pInfo->m_UseCustomColor;
1695 pClient->m_ColorBody = pInfo->m_ColorBody;
1696 pClient->m_ColorFeet = pInfo->m_ColorFeet;
1697 }
1698 }
1699 else if(Item.m_Type == NETOBJTYPE_PLAYERINFO)
1700 {
1701 const CNetObj_PlayerInfo *pInfo = (const CNetObj_PlayerInfo *)Item.m_pData;
1702
1703 if(pInfo->m_ClientId < MAX_CLIENTS && pInfo->m_ClientId == Item.m_Id)
1704 {
1705 m_aClients[pInfo->m_ClientId].m_Team = pInfo->m_Team;
1706 m_aClients[pInfo->m_ClientId].m_Active = true;
1707 m_Snap.m_apPlayerInfos[pInfo->m_ClientId] = pInfo;
1708 m_Snap.m_apPrevPlayerInfos[pInfo->m_ClientId] = static_cast<const CNetObj_PlayerInfo *>(Client()->SnapFindItem(SnapId: IClient::SNAP_PREV, Type: Item.m_Type, Id: pInfo->m_ClientId));
1709 m_Snap.m_NumPlayers++;
1710
1711 if(pInfo->m_Local)
1712 {
1713 m_Snap.m_LocalClientId = pInfo->m_ClientId;
1714 m_Snap.m_pLocalInfo = pInfo;
1715
1716 if(pInfo->m_Team == TEAM_SPECTATORS)
1717 {
1718 m_Snap.m_SpecInfo.m_Active = true;
1719 }
1720 }
1721
1722 m_Snap.m_HighestClientId = maximum(a: m_Snap.m_HighestClientId, b: pInfo->m_ClientId);
1723
1724 // calculate team-balance
1725 if(pInfo->m_Team != TEAM_SPECTATORS)
1726 {
1727 m_Snap.m_aTeamSize[pInfo->m_Team]++;
1728 if(!m_aStats[pInfo->m_ClientId].IsActive())
1729 m_aStats[pInfo->m_ClientId].JoinGame(Tick: Client()->GameTick(Conn: g_Config.m_ClDummy));
1730 }
1731 else if(m_aStats[pInfo->m_ClientId].IsActive())
1732 m_aStats[pInfo->m_ClientId].JoinSpec(Tick: Client()->GameTick(Conn: g_Config.m_ClDummy));
1733 }
1734 }
1735 else if(Item.m_Type == NETOBJTYPE_DDNETPLAYER)
1736 {
1737 m_ReceivedDDNetPlayer = true;
1738 const CNetObj_DDNetPlayer *pInfo = (const CNetObj_DDNetPlayer *)Item.m_pData;
1739 if(Item.m_Id < MAX_CLIENTS)
1740 {
1741 m_aClients[Item.m_Id].m_AuthLevel = pInfo->m_AuthLevel;
1742 m_aClients[Item.m_Id].m_Afk = pInfo->m_Flags & EXPLAYERFLAG_AFK;
1743 m_aClients[Item.m_Id].m_Paused = pInfo->m_Flags & EXPLAYERFLAG_PAUSED;
1744 m_aClients[Item.m_Id].m_Spec = pInfo->m_Flags & EXPLAYERFLAG_SPEC;
1745
1746 if(Item.m_Id == m_Snap.m_LocalClientId && (m_aClients[Item.m_Id].m_Paused || m_aClients[Item.m_Id].m_Spec))
1747 {
1748 m_Snap.m_SpecInfo.m_Active = true;
1749 }
1750 }
1751 }
1752 else if(Item.m_Type == NETOBJTYPE_CHARACTER)
1753 {
1754 if(Item.m_Id < MAX_CLIENTS)
1755 {
1756 const void *pOld = Client()->SnapFindItem(SnapId: IClient::SNAP_PREV, Type: NETOBJTYPE_CHARACTER, Id: Item.m_Id);
1757 m_Snap.m_aCharacters[Item.m_Id].m_Cur = *((const CNetObj_Character *)Item.m_pData);
1758 if(pOld)
1759 {
1760 m_Snap.m_aCharacters[Item.m_Id].m_Active = true;
1761 m_Snap.m_aCharacters[Item.m_Id].m_Prev = *((const CNetObj_Character *)pOld);
1762
1763 // limit evolving to 3 seconds
1764 bool EvolvePrev = Client()->PrevGameTick(Conn: g_Config.m_ClDummy) - m_Snap.m_aCharacters[Item.m_Id].m_Prev.m_Tick <= 3 * Client()->GameTickSpeed();
1765 bool EvolveCur = Client()->GameTick(Conn: g_Config.m_ClDummy) - m_Snap.m_aCharacters[Item.m_Id].m_Cur.m_Tick <= 3 * Client()->GameTickSpeed();
1766
1767 // reuse the result from the previous evolve if the snapped character didn't change since the previous snapshot
1768 if(EvolveCur && m_aClients[Item.m_Id].m_Evolved.m_Tick == Client()->PrevGameTick(Conn: g_Config.m_ClDummy))
1769 {
1770 if(mem_comp(a: &m_Snap.m_aCharacters[Item.m_Id].m_Prev, b: &m_aClients[Item.m_Id].m_Snapped, size: sizeof(CNetObj_Character)) == 0)
1771 m_Snap.m_aCharacters[Item.m_Id].m_Prev = m_aClients[Item.m_Id].m_Evolved;
1772 if(mem_comp(a: &m_Snap.m_aCharacters[Item.m_Id].m_Cur, b: &m_aClients[Item.m_Id].m_Snapped, size: sizeof(CNetObj_Character)) == 0)
1773 m_Snap.m_aCharacters[Item.m_Id].m_Cur = m_aClients[Item.m_Id].m_Evolved;
1774 }
1775
1776 if(EvolvePrev && m_Snap.m_aCharacters[Item.m_Id].m_Prev.m_Tick)
1777 Evolve(&m_Snap.m_aCharacters[Item.m_Id].m_Prev, Client()->PrevGameTick(Conn: g_Config.m_ClDummy));
1778 if(EvolveCur && m_Snap.m_aCharacters[Item.m_Id].m_Cur.m_Tick)
1779 Evolve(&m_Snap.m_aCharacters[Item.m_Id].m_Cur, Client()->GameTick(Conn: g_Config.m_ClDummy));
1780
1781 m_aClients[Item.m_Id].m_Snapped = *((const CNetObj_Character *)Item.m_pData);
1782 m_aClients[Item.m_Id].m_Evolved = m_Snap.m_aCharacters[Item.m_Id].m_Cur;
1783 }
1784 else
1785 {
1786 m_aClients[Item.m_Id].m_Evolved.m_Tick = -1;
1787 }
1788 }
1789 }
1790 else if(Item.m_Type == NETOBJTYPE_DDNETCHARACTER)
1791 {
1792 const CNetObj_DDNetCharacter *pCharacterData = (const CNetObj_DDNetCharacter *)Item.m_pData;
1793
1794 if(Item.m_Id < MAX_CLIENTS)
1795 {
1796 m_Snap.m_aCharacters[Item.m_Id].m_ExtendedData = *pCharacterData;
1797 m_Snap.m_aCharacters[Item.m_Id].m_pPrevExtendedData = (const CNetObj_DDNetCharacter *)Client()->SnapFindItem(SnapId: IClient::SNAP_PREV, Type: NETOBJTYPE_DDNETCHARACTER, Id: Item.m_Id);
1798 m_Snap.m_aCharacters[Item.m_Id].m_HasExtendedData = true;
1799 m_Snap.m_aCharacters[Item.m_Id].m_HasExtendedDisplayInfo = false;
1800 if(pCharacterData->m_JumpedTotal != -1)
1801 {
1802 m_Snap.m_aCharacters[Item.m_Id].m_HasExtendedDisplayInfo = true;
1803 }
1804 CClientData *pClient = &m_aClients[Item.m_Id];
1805 // Collision
1806 pClient->m_Solo = pCharacterData->m_Flags & CHARACTERFLAG_SOLO;
1807 pClient->m_Jetpack = pCharacterData->m_Flags & CHARACTERFLAG_JETPACK;
1808 pClient->m_CollisionDisabled = pCharacterData->m_Flags & CHARACTERFLAG_COLLISION_DISABLED;
1809 pClient->m_HammerHitDisabled = pCharacterData->m_Flags & CHARACTERFLAG_HAMMER_HIT_DISABLED;
1810 pClient->m_GrenadeHitDisabled = pCharacterData->m_Flags & CHARACTERFLAG_GRENADE_HIT_DISABLED;
1811 pClient->m_LaserHitDisabled = pCharacterData->m_Flags & CHARACTERFLAG_LASER_HIT_DISABLED;
1812 pClient->m_ShotgunHitDisabled = pCharacterData->m_Flags & CHARACTERFLAG_SHOTGUN_HIT_DISABLED;
1813 pClient->m_HookHitDisabled = pCharacterData->m_Flags & CHARACTERFLAG_HOOK_HIT_DISABLED;
1814 pClient->m_Super = pCharacterData->m_Flags & CHARACTERFLAG_SUPER;
1815 pClient->m_Invincible = pCharacterData->m_Flags & CHARACTERFLAG_INVINCIBLE;
1816
1817 // Endless
1818 pClient->m_EndlessHook = pCharacterData->m_Flags & CHARACTERFLAG_ENDLESS_HOOK;
1819 pClient->m_EndlessJump = pCharacterData->m_Flags & CHARACTERFLAG_ENDLESS_JUMP;
1820
1821 // Freeze
1822 pClient->m_FreezeEnd = pCharacterData->m_FreezeEnd;
1823 pClient->m_DeepFrozen = pCharacterData->m_FreezeEnd == -1;
1824 pClient->m_LiveFrozen = (pCharacterData->m_Flags & CHARACTERFLAG_MOVEMENTS_DISABLED) != 0;
1825
1826 // Telegun
1827 pClient->m_HasTelegunGrenade = pCharacterData->m_Flags & CHARACTERFLAG_TELEGUN_GRENADE;
1828 pClient->m_HasTelegunGun = pCharacterData->m_Flags & CHARACTERFLAG_TELEGUN_GUN;
1829 pClient->m_HasTelegunLaser = pCharacterData->m_Flags & CHARACTERFLAG_TELEGUN_LASER;
1830
1831 pClient->m_Predicted.ReadDDNet(pObjDDNet: pCharacterData);
1832
1833 m_Teams.SetSolo(ClientId: Item.m_Id, Value: pClient->m_Solo);
1834 }
1835 }
1836 else if(Item.m_Type == NETOBJTYPE_SPECCHAR)
1837 {
1838 const CNetObj_SpecChar *pSpecCharData = (const CNetObj_SpecChar *)Item.m_pData;
1839
1840 if(Item.m_Id < MAX_CLIENTS)
1841 {
1842 CClientData *pClient = &m_aClients[Item.m_Id];
1843 pClient->m_SpecCharPresent = true;
1844 pClient->m_SpecChar.x = pSpecCharData->m_X;
1845 pClient->m_SpecChar.y = pSpecCharData->m_Y;
1846 }
1847 }
1848 else if(Item.m_Type == NETOBJTYPE_SPECTATORINFO)
1849 {
1850 m_Snap.m_pSpectatorInfo = (const CNetObj_SpectatorInfo *)Item.m_pData;
1851 m_Snap.m_pPrevSpectatorInfo = (const CNetObj_SpectatorInfo *)Client()->SnapFindItem(SnapId: IClient::SNAP_PREV, Type: NETOBJTYPE_SPECTATORINFO, Id: Item.m_Id);
1852
1853 // needed for 0.7 survival
1854 // to auto spec players when dead
1855 if(Client()->IsSixup())
1856 m_Snap.m_SpecInfo.m_Active = true;
1857 m_Snap.m_SpecInfo.m_SpectatorId = m_Snap.m_pSpectatorInfo->m_SpectatorId;
1858 }
1859 else if(Item.m_Type == NETOBJTYPE_DDNETSPECTATORINFO)
1860 {
1861 const CNetObj_DDNetSpectatorInfo *pDDNetSpecInfo = (const CNetObj_DDNetSpectatorInfo *)Item.m_pData;
1862 m_Snap.m_SpecInfo.m_HasCameraInfo = pDDNetSpecInfo->m_HasCameraInfo;
1863 m_Snap.m_SpecInfo.m_Zoom = pDDNetSpecInfo->m_Zoom / 1000.0f;
1864 m_Snap.m_SpecInfo.m_Deadzone = pDDNetSpecInfo->m_Deadzone;
1865 m_Snap.m_SpecInfo.m_FollowFactor = pDDNetSpecInfo->m_FollowFactor;
1866 }
1867 else if(Item.m_Type == NETOBJTYPE_SPECTATORCOUNT)
1868 {
1869 m_Snap.m_pSpectatorCount = (const CNetObj_SpectatorCount *)Item.m_pData;
1870 }
1871 else if(Item.m_Type == NETOBJTYPE_GAMEINFO)
1872 {
1873 m_Snap.m_pGameInfoObj = (const CNetObj_GameInfo *)Item.m_pData;
1874 bool CurrentTickGameOver = (bool)(m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER);
1875 if(!m_GameOver && CurrentTickGameOver)
1876 OnGameOver();
1877 else if(m_GameOver && !CurrentTickGameOver)
1878 OnStartGame();
1879 // Handle case that a new round is started (RoundStartTick changed)
1880 // New round is usually started after `restart` on server
1881 if(m_Snap.m_pGameInfoObj->m_RoundStartTick != m_LastRoundStartTick && !(CurrentTickGameOver || m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_PAUSED || m_GamePaused))
1882 OnStartRound();
1883 m_LastRoundStartTick = m_Snap.m_pGameInfoObj->m_RoundStartTick;
1884 m_GameOver = CurrentTickGameOver;
1885 m_GamePaused = (bool)(m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_PAUSED);
1886 }
1887 else if(Item.m_Type == NETOBJTYPE_GAMEINFOEX)
1888 {
1889 if(FoundGameInfoEx)
1890 {
1891 continue;
1892 }
1893 FoundGameInfoEx = true;
1894 m_GameInfo = GetGameInfo(pInfoEx: (const CNetObj_GameInfoEx *)Item.m_pData, InfoExSize: Item.m_DataSize, pFallbackServerInfo: &ServerInfo);
1895 }
1896 else if(Item.m_Type == NETOBJTYPE_GAMEDATA)
1897 {
1898 m_Snap.m_pGameDataObj = static_cast<const CNetObj_GameData *>(Item.m_pData);
1899 m_Snap.m_pPrevGameDataObj = static_cast<const CNetObj_GameData *>(Client()->SnapFindItem(SnapId: IClient::SNAP_PREV, Type: Item.m_Type, Id: Item.m_Id));
1900 if(m_Snap.m_pGameDataObj->m_FlagCarrierRed == FLAG_TAKEN)
1901 {
1902 if(m_aFlagDropTick[TEAM_RED] == 0)
1903 m_aFlagDropTick[TEAM_RED] = Client()->GameTick(Conn: g_Config.m_ClDummy);
1904 }
1905 else
1906 m_aFlagDropTick[TEAM_RED] = 0;
1907 if(m_Snap.m_pGameDataObj->m_FlagCarrierBlue == FLAG_TAKEN)
1908 {
1909 if(m_aFlagDropTick[TEAM_BLUE] == 0)
1910 m_aFlagDropTick[TEAM_BLUE] = Client()->GameTick(Conn: g_Config.m_ClDummy);
1911 }
1912 else
1913 m_aFlagDropTick[TEAM_BLUE] = 0;
1914 if(m_LastFlagCarrierRed == FLAG_ATSTAND && m_Snap.m_pGameDataObj->m_FlagCarrierRed >= 0)
1915 OnFlagGrab(TeamId: TEAM_RED);
1916 else if(m_LastFlagCarrierBlue == FLAG_ATSTAND && m_Snap.m_pGameDataObj->m_FlagCarrierBlue >= 0)
1917 OnFlagGrab(TeamId: TEAM_BLUE);
1918
1919 m_LastFlagCarrierRed = m_Snap.m_pGameDataObj->m_FlagCarrierRed;
1920 m_LastFlagCarrierBlue = m_Snap.m_pGameDataObj->m_FlagCarrierBlue;
1921 }
1922 else if(Item.m_Type == NETOBJTYPE_FLAG)
1923 {
1924 const CNetObj_Flag *pPrevFlag = static_cast<const CNetObj_Flag *>(Client()->SnapFindItem(SnapId: IClient::SNAP_PREV, Type: Item.m_Type, Id: Item.m_Id));
1925 if(pPrevFlag == nullptr)
1926 {
1927 continue;
1928 }
1929 m_Snap.m_apFlags[m_Snap.m_NumFlags] = static_cast<const CNetObj_Flag *>(Item.m_pData);
1930 m_Snap.m_apPrevFlags[m_Snap.m_NumFlags] = pPrevFlag;
1931 ++m_Snap.m_NumFlags;
1932 }
1933 else if(Item.m_Type == NETOBJTYPE_SWITCHSTATE)
1934 {
1935 if(Item.m_DataSize < 36)
1936 {
1937 continue;
1938 }
1939 const CNetObj_SwitchState *pSwitchStateData = (const CNetObj_SwitchState *)Item.m_pData;
1940 int Team = std::clamp(val: Item.m_Id, lo: (int)TEAM_FLOCK, hi: (int)TEAM_SUPER - 1);
1941
1942 int HighestSwitchNumber = std::clamp(val: pSwitchStateData->m_HighestSwitchNumber, lo: 0, hi: 255);
1943 if(HighestSwitchNumber != maximum(a: 0, b: (int)Switchers().size() - 1))
1944 {
1945 m_GameWorld.m_Core.InitSwitchers(HighestSwitchNumber);
1946 Collision()->m_HighestSwitchNumber = HighestSwitchNumber;
1947 }
1948
1949 for(int j = 0; j < (int)Switchers().size(); j++)
1950 {
1951 Switchers()[j].m_aStatus[Team] = (pSwitchStateData->m_aStatus[j / 32] >> (j % 32)) & 1;
1952 }
1953
1954 if(Item.m_DataSize >= 68)
1955 {
1956 // update the endtick of up to four timed switchers
1957 for(int j = 0; j < (int)std::size(pSwitchStateData->m_aEndTicks); j++)
1958 {
1959 int SwitchNumber = pSwitchStateData->m_aSwitchNumbers[j];
1960 int EndTick = pSwitchStateData->m_aEndTicks[j];
1961 if(EndTick > 0 && in_range(a: SwitchNumber, lower: 0, upper: (int)Switchers().size()))
1962 {
1963 Switchers()[SwitchNumber].m_aEndTick[Team] = EndTick;
1964 }
1965 }
1966 }
1967
1968 // update switch types
1969 for(auto &Switcher : Switchers())
1970 {
1971 if(Switcher.m_aStatus[Team])
1972 Switcher.m_aType[Team] = Switcher.m_aEndTick[Team] ? TILE_SWITCHTIMEDOPEN : TILE_SWITCHOPEN;
1973 else
1974 Switcher.m_aType[Team] = Switcher.m_aEndTick[Team] ? TILE_SWITCHTIMEDCLOSE : TILE_SWITCHCLOSE;
1975 }
1976
1977 if(!GotSwitchStateTeam)
1978 m_aSwitchStateTeam[g_Config.m_ClDummy] = Team;
1979 else
1980 m_aSwitchStateTeam[g_Config.m_ClDummy] = -1;
1981 GotSwitchStateTeam = true;
1982 }
1983 }
1984 }
1985
1986 if(!FoundGameInfoEx)
1987 {
1988 m_GameInfo = GetGameInfo(pInfoEx: nullptr, InfoExSize: 0, pFallbackServerInfo: &ServerInfo);
1989 }
1990
1991 for(CClientData &Client : m_aClients)
1992 {
1993 Client.UpdateSkinInfo();
1994 }
1995
1996 // setup local pointers
1997 if(m_Snap.m_LocalClientId >= 0)
1998 {
1999 m_aLocalIds[g_Config.m_ClDummy] = m_Snap.m_LocalClientId;
2000
2001 CSnapState::CCharacterInfo *pChr = &m_Snap.m_aCharacters[m_Snap.m_LocalClientId];
2002 if(pChr->m_Active)
2003 {
2004 if(!m_Snap.m_SpecInfo.m_Active)
2005 {
2006 m_Snap.m_pLocalCharacter = &pChr->m_Cur;
2007 m_Snap.m_pLocalPrevCharacter = &pChr->m_Prev;
2008 m_LocalCharacterPos = vec2(m_Snap.m_pLocalCharacter->m_X, m_Snap.m_pLocalCharacter->m_Y);
2009 }
2010 }
2011 else if(Client()->SnapFindItem(SnapId: IClient::SNAP_PREV, Type: NETOBJTYPE_CHARACTER, Id: m_Snap.m_LocalClientId))
2012 {
2013 // player died
2014 m_Controls.OnPlayerDeath();
2015 }
2016 }
2017 if(Client()->State() == IClient::STATE_DEMOPLAYBACK)
2018 {
2019 if(m_Snap.m_LocalClientId == -1 && m_DemoSpecId == SPEC_FOLLOW)
2020 {
2021 // TODO: can this be done in the translation layer?
2022 if(!Client()->IsSixup())
2023 m_DemoSpecId = SPEC_FREEVIEW;
2024 }
2025 if(m_DemoSpecId != SPEC_FOLLOW)
2026 {
2027 m_Snap.m_SpecInfo.m_Active = true;
2028 if(m_DemoSpecId > SPEC_FREEVIEW && m_Snap.m_aCharacters[m_DemoSpecId].m_Active)
2029 m_Snap.m_SpecInfo.m_SpectatorId = m_DemoSpecId;
2030 else
2031 m_Snap.m_SpecInfo.m_SpectatorId = SPEC_FREEVIEW;
2032 }
2033 }
2034
2035 // clear out unneeded client data
2036 for(int i = 0; i < MAX_CLIENTS; ++i)
2037 {
2038 if(!m_Snap.m_apPlayerInfos[i] && m_aClients[i].m_Active)
2039 {
2040 m_aClients[i].Reset();
2041 m_aStats[i].Reset();
2042 }
2043 }
2044
2045 if(Client()->State() == IClient::STATE_ONLINE)
2046 {
2047 m_pDiscord->UpdatePlayerCount(Count: m_Snap.m_NumPlayers);
2048 }
2049
2050 for(int i = 0; i < MAX_CLIENTS; ++i)
2051 {
2052 // update friend state
2053 m_aClients[i].m_Friend = !(i == m_Snap.m_LocalClientId || !m_Snap.m_apPlayerInfos[i] || !Friends()->IsFriend(pName: m_aClients[i].m_aName, pClan: m_aClients[i].m_aClan, PlayersOnly: true));
2054
2055 // update foe state
2056 m_aClients[i].m_Foe = !(i == m_Snap.m_LocalClientId || !m_Snap.m_apPlayerInfos[i] || !Foes()->IsFriend(pName: m_aClients[i].m_aName, pClan: m_aClients[i].m_aClan, PlayersOnly: true));
2057 }
2058
2059 // sort player infos by name
2060 mem_copy(dest: m_Snap.m_apInfoByName, source: m_Snap.m_apPlayerInfos, size: sizeof(m_Snap.m_apInfoByName));
2061 std::stable_sort(first: m_Snap.m_apInfoByName, last: m_Snap.m_apInfoByName + MAX_CLIENTS,
2062 comp: [this](const CNetObj_PlayerInfo *pPlayer1, const CNetObj_PlayerInfo *pPlayer2) -> bool {
2063 if(!pPlayer2)
2064 return static_cast<bool>(pPlayer1);
2065 if(!pPlayer1)
2066 return false;
2067 return str_comp_nocase(a: m_aClients[pPlayer1->m_ClientId].m_aName, b: m_aClients[pPlayer2->m_ClientId].m_aName) < 0;
2068 });
2069
2070 bool TimeScore = m_GameInfo.m_TimeScore;
2071 bool Race7 = Client()->IsSixup() && m_Snap.m_pGameInfoObj && m_Snap.m_pGameInfoObj->m_GameFlags & protocol7::GAMEFLAG_RACE;
2072
2073 // sort player infos by score
2074 mem_copy(dest: m_Snap.m_apInfoByScore, source: m_Snap.m_apInfoByName, size: sizeof(m_Snap.m_apInfoByScore));
2075 if(Race7)
2076 std::stable_sort(first: m_Snap.m_apInfoByScore, last: m_Snap.m_apInfoByScore + MAX_CLIENTS,
2077 comp: [](const CNetObj_PlayerInfo *pPlayer1, const CNetObj_PlayerInfo *pPlayer2) -> bool {
2078 if(!pPlayer2)
2079 return static_cast<bool>(pPlayer1);
2080 if(!pPlayer1)
2081 return false;
2082 return (((pPlayer1->m_Score == -1) ? std::numeric_limits<int>::max() : pPlayer1->m_Score) <
2083 ((pPlayer2->m_Score == -1) ? std::numeric_limits<int>::max() : pPlayer2->m_Score));
2084 });
2085 else
2086 std::stable_sort(first: m_Snap.m_apInfoByScore, last: m_Snap.m_apInfoByScore + MAX_CLIENTS,
2087 comp: [TimeScore](const CNetObj_PlayerInfo *pPlayer1, const CNetObj_PlayerInfo *pPlayer2) -> bool {
2088 if(!pPlayer2)
2089 return static_cast<bool>(pPlayer1);
2090 if(!pPlayer1)
2091 return false;
2092 return (((TimeScore && pPlayer1->m_Score == -9999) ? std::numeric_limits<int>::min() : pPlayer1->m_Score) >
2093 ((TimeScore && pPlayer2->m_Score == -9999) ? std::numeric_limits<int>::min() : pPlayer2->m_Score));
2094 });
2095
2096 // sort player infos by DDRace Team (and score between)
2097 int Index = 0;
2098 for(int Team = TEAM_FLOCK; Team <= TEAM_SUPER; ++Team)
2099 {
2100 for(int i = 0; i < MAX_CLIENTS && Index < MAX_CLIENTS; ++i)
2101 {
2102 if(m_Snap.m_apInfoByScore[i] && m_Teams.Team(ClientId: m_Snap.m_apInfoByScore[i]->m_ClientId) == Team)
2103 m_Snap.m_apInfoByDDTeamScore[Index++] = m_Snap.m_apInfoByScore[i];
2104 }
2105 }
2106
2107 // sort player infos by DDRace Team (and name between)
2108 Index = 0;
2109 for(int Team = TEAM_FLOCK; Team <= TEAM_SUPER; ++Team)
2110 {
2111 for(int i = 0; i < MAX_CLIENTS && Index < MAX_CLIENTS; ++i)
2112 {
2113 if(m_Snap.m_apInfoByName[i] && m_Teams.Team(ClientId: m_Snap.m_apInfoByName[i]->m_ClientId) == Team)
2114 m_Snap.m_apInfoByDDTeamName[Index++] = m_Snap.m_apInfoByName[i];
2115 }
2116 }
2117
2118 if(ServerInfo.m_aGameType[0] != '0')
2119 {
2120 if(str_comp(a: ServerInfo.m_aGameType, b: "DM") != 0 && str_comp(a: ServerInfo.m_aGameType, b: "TDM") != 0 && str_comp(a: ServerInfo.m_aGameType, b: "CTF") != 0)
2121 m_ServerMode = SERVERMODE_MOD;
2122 else if(mem_comp(a: &CTuningParams::DEFAULT, b: &m_aTuning[g_Config.m_ClDummy], size: 33) == 0)
2123 m_ServerMode = SERVERMODE_PURE;
2124 else
2125 m_ServerMode = SERVERMODE_PUREMOD;
2126 }
2127
2128 // add tuning to demo when new recording was started, because server tune message was already received before
2129 std::bitset<RECORDER_MAX> CurrentRecordings;
2130 for(int i = 0; i < RECORDER_MAX; i++)
2131 {
2132 if(DemoRecorder(Recorder: i)->IsRecording())
2133 {
2134 CurrentRecordings.set(position: i);
2135 }
2136 }
2137 const bool HasNewRecordings = (CurrentRecordings & ~m_ActiveRecordings).any();
2138 m_ActiveRecordings = CurrentRecordings;
2139 if(HasNewRecordings)
2140 {
2141 CMsgPacker Msg(NETMSGTYPE_SV_TUNEPARAMS);
2142 int *pParams = (int *)&m_aTuning[g_Config.m_ClDummy];
2143 for(unsigned i = 0; i < sizeof(m_aTuning[0]) / sizeof(int); i++)
2144 Msg.AddInt(i: pParams[i]);
2145 Client()->SendMsgActive(pMsg: &Msg, Flags: MSGFLAG_RECORD | MSGFLAG_NOSEND);
2146 }
2147
2148 for(int i = 0; i < 2; i++)
2149 {
2150 if(m_aDDRaceMsgSent[i] || !m_Snap.m_pLocalInfo)
2151 {
2152 continue;
2153 }
2154 if(i == IClient::CONN_DUMMY && !Client()->DummyConnected())
2155 {
2156 continue;
2157 }
2158 CMsgPacker Msg(NETMSGTYPE_CL_ISDDNETLEGACY, false);
2159 Msg.AddInt(i: DDNetVersion());
2160 Client()->SendMsg(Conn: i, pMsg: &Msg, Flags: MSGFLAG_VITAL);
2161 m_aDDRaceMsgSent[i] = true;
2162 }
2163
2164 if(m_Snap.m_SpecInfo.m_Active && m_MultiViewActivated)
2165 {
2166 // dont show other teams while spectating in multi view
2167 CNetMsg_Cl_ShowOthers Msg;
2168 Msg.m_Show = SHOW_OTHERS_ONLY_TEAM;
2169 Client()->SendPackMsgActive(pMsg: &Msg, Flags: MSGFLAG_VITAL);
2170
2171 // update state
2172 m_aShowOthers[g_Config.m_ClDummy] = SHOW_OTHERS_ONLY_TEAM;
2173 }
2174 else if(m_aShowOthers[g_Config.m_ClDummy] == SHOW_OTHERS_NOT_SET || m_aShowOthers[g_Config.m_ClDummy] != g_Config.m_ClShowOthers)
2175 {
2176 {
2177 CNetMsg_Cl_ShowOthers Msg;
2178 Msg.m_Show = g_Config.m_ClShowOthers;
2179 Client()->SendPackMsgActive(pMsg: &Msg, Flags: MSGFLAG_VITAL);
2180 }
2181
2182 // update state
2183 m_aShowOthers[g_Config.m_ClDummy] = g_Config.m_ClShowOthers;
2184 }
2185
2186 if(m_aEnableSpectatorCount[0] == -1 || m_aEnableSpectatorCount[0] != g_Config.m_ClShowhudSpectatorCount)
2187 {
2188 CNetMsg_Cl_EnableSpectatorCount Msg;
2189 Msg.m_Enable = g_Config.m_ClShowhudSpectatorCount;
2190 Client()->SendPackMsg(Conn: 0, pMsg: &Msg, Flags: MSGFLAG_VITAL);
2191 m_aEnableSpectatorCount[0] = g_Config.m_ClShowhudSpectatorCount;
2192 }
2193 if(Client()->DummyConnected() && (m_aEnableSpectatorCount[1] == -1 || m_aEnableSpectatorCount[1] != g_Config.m_ClShowhudSpectatorCount))
2194 {
2195 CNetMsg_Cl_EnableSpectatorCount Msg;
2196 Msg.m_Enable = g_Config.m_ClShowhudSpectatorCount;
2197 Client()->SendPackMsg(Conn: 1, pMsg: &Msg, Flags: MSGFLAG_VITAL);
2198 m_aEnableSpectatorCount[1] = g_Config.m_ClShowhudSpectatorCount;
2199 }
2200
2201 float ShowDistanceZoom = m_Camera.m_Zoom;
2202 float Zoom = m_Camera.m_Zoom;
2203 if(m_Camera.m_Zooming)
2204 {
2205 if(m_Camera.m_ZoomSmoothingTarget > m_Camera.m_Zoom) // Zooming out
2206 ShowDistanceZoom = m_Camera.m_ZoomSmoothingTarget;
2207 else if(m_Camera.m_ZoomSmoothingTarget < m_Camera.m_Zoom && m_LastShowDistanceZoom > 0) // Zooming in
2208 ShowDistanceZoom = m_LastShowDistanceZoom;
2209
2210 Zoom = m_Camera.m_ZoomSmoothingTarget;
2211 }
2212
2213 float Deadzone = m_Camera.Deadzone();
2214 float FollowFactor = m_Camera.FollowFactor();
2215
2216 if(m_Snap.m_SpecInfo.m_Active)
2217 {
2218 // don't send camera information when spectating
2219 Zoom = m_LastZoom;
2220 Deadzone = m_LastDeadzone;
2221 FollowFactor = m_LastFollowFactor;
2222 }
2223
2224 // initialize dummy vital when first connected
2225 if(Client()->DummyConnected() && !m_LastDummyConnected)
2226 {
2227 {
2228 CNetMsg_Cl_ShowDistance Msg;
2229 float x, y;
2230 Graphics()->CalcScreenParams(Aspect: Graphics()->ScreenAspect(), Zoom: ShowDistanceZoom, pWidth: &x, pHeight: &y);
2231 Msg.m_X = x;
2232 Msg.m_Y = y;
2233 CMsgPacker Packer(&Msg);
2234 Msg.Pack(pPacker: &Packer);
2235 Client()->SendMsg(Conn: IClient::CONN_DUMMY, pMsg: &Packer, Flags: MSGFLAG_VITAL);
2236 }
2237 {
2238 CNetMsg_Cl_CameraInfo Msg;
2239 Msg.m_Zoom = round_truncate(f: Zoom * 1000.f);
2240 Msg.m_Deadzone = Deadzone;
2241 Msg.m_FollowFactor = FollowFactor;
2242 CMsgPacker Packer(&Msg);
2243 Msg.Pack(pPacker: &Packer);
2244 Client()->SendMsg(Conn: IClient::CONN_DUMMY, pMsg: &Packer, Flags: MSGFLAG_VITAL);
2245 }
2246 }
2247
2248 // send show distance
2249 if(ShowDistanceZoom != m_LastShowDistanceZoom || Graphics()->ScreenAspect() != m_LastScreenAspect)
2250 {
2251 CNetMsg_Cl_ShowDistance Msg;
2252 float x, y;
2253 Graphics()->CalcScreenParams(Aspect: Graphics()->ScreenAspect(), Zoom: ShowDistanceZoom, pWidth: &x, pHeight: &y);
2254 Msg.m_X = x;
2255 Msg.m_Y = y;
2256 Client()->ChecksumData()->m_Zoom = ShowDistanceZoom;
2257 CMsgPacker Packer(&Msg);
2258 Msg.Pack(pPacker: &Packer);
2259
2260 Client()->SendMsg(Conn: IClient::CONN_MAIN, pMsg: &Packer, Flags: MSGFLAG_VITAL);
2261 if(Client()->DummyConnected() && m_LastDummyConnected)
2262 Client()->SendMsg(Conn: IClient::CONN_DUMMY, pMsg: &Packer, Flags: MSGFLAG_VITAL);
2263 }
2264
2265 // send camera info
2266 if(Zoom != m_LastZoom || Deadzone != m_LastDeadzone || FollowFactor != m_LastFollowFactor)
2267 {
2268 CNetMsg_Cl_CameraInfo Msg;
2269 Msg.m_Zoom = round_truncate(f: Zoom * 1000.f);
2270 Msg.m_Deadzone = Deadzone;
2271 Msg.m_FollowFactor = FollowFactor;
2272 CMsgPacker Packer(&Msg);
2273 Msg.Pack(pPacker: &Packer);
2274
2275 Client()->SendMsg(Conn: IClient::CONN_MAIN, pMsg: &Packer, Flags: MSGFLAG_VITAL);
2276 if(Client()->DummyConnected() && m_LastDummyConnected)
2277 Client()->SendMsg(Conn: IClient::CONN_DUMMY, pMsg: &Packer, Flags: MSGFLAG_VITAL);
2278 }
2279
2280 m_LastShowDistanceZoom = ShowDistanceZoom;
2281 m_LastZoom = Zoom;
2282 m_LastScreenAspect = Graphics()->ScreenAspect();
2283 m_LastDeadzone = Deadzone;
2284 m_LastFollowFactor = FollowFactor;
2285 m_LastDummyConnected = Client()->DummyConnected();
2286
2287 for(auto &pComponent : m_vpAll)
2288 pComponent->OnNewSnapshot();
2289
2290 // notify editor when local character moved
2291 UpdateEditorIngameMoved();
2292
2293 // detect air jump for other players
2294 for(int i = 0; i < MAX_CLIENTS; i++)
2295 if(m_Snap.m_aCharacters[i].m_Active && (m_Snap.m_aCharacters[i].m_Cur.m_Jumped & 2) && !(m_Snap.m_aCharacters[i].m_Prev.m_Jumped & 2))
2296 if(!Predict() || (i != m_Snap.m_LocalClientId && (!AntiPingPlayers() || (Client()->DummyConnected() && i != m_aLocalIds[!g_Config.m_ClDummy]))))
2297 {
2298 vec2 Pos = mix(a: vec2(m_Snap.m_aCharacters[i].m_Prev.m_X, m_Snap.m_aCharacters[i].m_Prev.m_Y),
2299 b: vec2(m_Snap.m_aCharacters[i].m_Cur.m_X, m_Snap.m_aCharacters[i].m_Cur.m_Y),
2300 amount: Client()->IntraGameTick(Conn: g_Config.m_ClDummy));
2301 float Alpha = 1.0f;
2302 if(IsOtherTeam(ClientId: i))
2303 Alpha = g_Config.m_ClShowOthersAlpha / 100.0f;
2304 const float Volume = 1.0f; // TODO snd_game_volume_others
2305 m_Effects.AirJump(Pos, Alpha, Volume);
2306 }
2307
2308 if(g_Config.m_ClFreezeStars && !m_SuppressEvents)
2309 {
2310 for(auto &Character : m_Snap.m_aCharacters)
2311 {
2312 if(Character.m_Active && Character.m_HasExtendedData && Character.m_pPrevExtendedData)
2313 {
2314 int FreezeTimeNow = Character.m_ExtendedData.m_FreezeEnd - Client()->GameTick(Conn: g_Config.m_ClDummy);
2315 int FreezeTimePrev = Character.m_pPrevExtendedData->m_FreezeEnd - Client()->PrevGameTick(Conn: g_Config.m_ClDummy);
2316 vec2 Pos = vec2(Character.m_Cur.m_X, Character.m_Cur.m_Y);
2317 int StarsNow = (FreezeTimeNow + 1) / Client()->GameTickSpeed();
2318 int StarsPrev = (FreezeTimePrev + 1) / Client()->GameTickSpeed();
2319 if(StarsNow < StarsPrev || (StarsPrev == 0 && StarsNow > 0))
2320 {
2321 int Amount = StarsNow + 1;
2322 float Mid = 3 * pi / 2;
2323 float Min = Mid - pi / 3;
2324 float Max = Mid + pi / 3;
2325 for(int j = 0; j < Amount; j++)
2326 {
2327 float Angle = mix(a: Min, b: Max, amount: (j + 1) / (float)(Amount + 2));
2328 m_Effects.DamageIndicator(Pos, Dir: direction(angle: Angle), Alpha: 1.0f);
2329 }
2330 }
2331 }
2332 }
2333 }
2334
2335 // Record m_LastRaceTick for g_Config.m_ClConfirmDisconnect/QuitTime
2336 if(m_GameInfo.m_Race &&
2337 Client()->State() == IClient::STATE_ONLINE &&
2338 m_Snap.m_pGameInfoObj &&
2339 !m_Snap.m_SpecInfo.m_Active &&
2340 m_Snap.m_pLocalCharacter &&
2341 m_Snap.m_pLocalPrevCharacter)
2342 {
2343 const bool RaceFlag = m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_RACETIME;
2344 m_LastRaceTick = RaceFlag ? -m_Snap.m_pGameInfoObj->m_WarmupTimer : -1;
2345 }
2346
2347 SnapCollectEntities(); // creates a collection that associates EntityEx snap items with the entities they belong to
2348
2349 UpdateLocalTuning();
2350 m_IsDummySwapping = 0;
2351 if(Client()->State() != IClient::STATE_DEMOPLAYBACK)
2352 UpdatePrediction();
2353}
2354
2355void CGameClient::UpdateEditorIngameMoved()
2356{
2357 const bool LocalCharacterMoved = m_Snap.m_pLocalCharacter && m_Snap.m_pLocalPrevCharacter && (m_Snap.m_pLocalCharacter->m_X != m_Snap.m_pLocalPrevCharacter->m_X || m_Snap.m_pLocalCharacter->m_Y != m_Snap.m_pLocalPrevCharacter->m_Y);
2358 if(!g_Config.m_ClEditor)
2359 {
2360 m_EditorMovementDelay = 5;
2361 }
2362 else if(m_EditorMovementDelay > 0 && !LocalCharacterMoved)
2363 {
2364 --m_EditorMovementDelay;
2365 }
2366 if(m_EditorMovementDelay == 0 && LocalCharacterMoved)
2367 {
2368 Editor()->OnIngameMoved();
2369 }
2370}
2371
2372void CGameClient::OnPredict()
2373{
2374 // store the previous values so we can detect prediction errors
2375 CCharacterCore BeforePrevChar = m_PredictedPrevChar;
2376 CCharacterCore BeforeChar = m_PredictedChar;
2377
2378 // we can't predict without our own id or own character
2379 if(m_Snap.m_LocalClientId == -1 || !m_Snap.m_aCharacters[m_Snap.m_LocalClientId].m_Active)
2380 return;
2381
2382 // don't predict anything if we are paused
2383 if(m_Snap.m_pGameInfoObj && m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_PAUSED)
2384 {
2385 if(m_Snap.m_pLocalCharacter)
2386 {
2387 m_PredictedChar.Read(pObjCore: m_Snap.m_pLocalCharacter);
2388 m_PredictedChar.m_ActiveWeapon = m_Snap.m_pLocalCharacter->m_Weapon;
2389 }
2390 if(m_Snap.m_pLocalPrevCharacter)
2391 {
2392 m_PredictedPrevChar.Read(pObjCore: m_Snap.m_pLocalPrevCharacter);
2393 m_PredictedPrevChar.m_ActiveWeapon = m_Snap.m_pLocalPrevCharacter->m_Weapon;
2394 }
2395 return;
2396 }
2397
2398 vec2 aBeforeRender[MAX_CLIENTS];
2399 for(int i = 0; i < MAX_CLIENTS; i++)
2400 aBeforeRender[i] = GetSmoothPos(ClientId: i);
2401
2402 // init
2403 bool Dummy = g_Config.m_ClDummy ^ m_IsDummySwapping;
2404 m_PredictedWorld.CopyWorld(pFrom: &m_GameWorld);
2405
2406 // don't predict inactive players, or entities from other teams
2407 for(int i = 0; i < MAX_CLIENTS; i++)
2408 if(CCharacter *pChar = m_PredictedWorld.GetCharacterById(Id: i))
2409 if((!m_Snap.m_aCharacters[i].m_Active && pChar->m_SnapTicks > 10) || IsOtherTeam(ClientId: i))
2410 pChar->Destroy();
2411
2412 CProjectile *pProjNext = nullptr;
2413 for(CProjectile *pProj = (CProjectile *)m_PredictedWorld.FindFirst(Type: CGameWorld::ENTTYPE_PROJECTILE); pProj; pProj = pProjNext)
2414 {
2415 pProjNext = (CProjectile *)pProj->TypeNext();
2416 if(IsOtherTeam(ClientId: pProj->GetOwner()))
2417 {
2418 pProj->Destroy();
2419 }
2420 }
2421
2422 CCharacter *pLocalChar = m_PredictedWorld.GetCharacterById(Id: m_Snap.m_LocalClientId);
2423 if(!pLocalChar)
2424 return;
2425 CCharacter *pDummyChar = nullptr;
2426 if(PredictDummy())
2427 pDummyChar = m_PredictedWorld.GetCharacterById(Id: m_aLocalIds[!g_Config.m_ClDummy]);
2428
2429 int PredictionTick = Client()->GetPredictionTick();
2430 // predict
2431 for(int Tick = Client()->GameTick(Conn: g_Config.m_ClDummy) + 1; Tick <= Client()->PredGameTick(Conn: g_Config.m_ClDummy); Tick++)
2432 {
2433 // fetch the previous characters
2434 if(Tick == PredictionTick)
2435 {
2436 for(int i = 0; i < MAX_CLIENTS; i++)
2437 if(CCharacter *pChar = m_PredictedWorld.GetCharacterById(Id: i))
2438 m_aClients[i].m_PrevPredicted = pChar->GetCore();
2439 }
2440
2441 if(Tick == Client()->PredGameTick(Conn: g_Config.m_ClDummy))
2442 {
2443 m_PredictedPrevChar = pLocalChar->GetCore();
2444 m_aClients[m_Snap.m_LocalClientId].m_PrevPredicted = pLocalChar->GetCore();
2445
2446 if(pDummyChar)
2447 m_aClients[m_aLocalIds[!g_Config.m_ClDummy]].m_PrevPredicted = pDummyChar->GetCore();
2448 }
2449
2450 // optionally allow some movement in freeze by not predicting freeze the last one to two ticks
2451 if(g_Config.m_ClPredictFreeze == 2 && Client()->PredGameTick(Conn: g_Config.m_ClDummy) - 1 - Client()->PredGameTick(Conn: g_Config.m_ClDummy) % 2 <= Tick)
2452 pLocalChar->m_CanMoveInFreeze = true;
2453
2454 // apply inputs and tick
2455 CNetObj_PlayerInput *pInputData = (CNetObj_PlayerInput *)Client()->GetInput(Tick, IsDummy: m_IsDummySwapping);
2456 CNetObj_PlayerInput *pDummyInputData = !pDummyChar ? nullptr : (CNetObj_PlayerInput *)Client()->GetInput(Tick, IsDummy: m_IsDummySwapping ^ 1);
2457 bool DummyFirst = pInputData && pDummyInputData && pDummyChar->GetCid() < pLocalChar->GetCid();
2458
2459 if(DummyFirst)
2460 pDummyChar->OnDirectInput(pNewInput: pDummyInputData);
2461 if(pInputData)
2462 pLocalChar->OnDirectInput(pNewInput: pInputData);
2463 if(pDummyInputData && !DummyFirst)
2464 pDummyChar->OnDirectInput(pNewInput: pDummyInputData);
2465
2466 if(g_Config.m_ClAntiPingPreInput)
2467 {
2468 for(int i = 0; i < MAX_CLIENTS; i++)
2469 {
2470 if(CCharacter *pChar = m_PredictedWorld.GetCharacterById(Id: i))
2471 {
2472 if(i == m_aLocalIds[0] || (Client()->DummyConnected() && i == m_aLocalIds[1]))
2473 continue;
2474
2475 const CNetMsg_Sv_PreInput PreInput = m_aClients[i].m_aPreInputs[Tick % 200];
2476 if(PreInput.m_IntendedTick != Tick)
2477 continue;
2478
2479 //convert preinput to input
2480 CNetObj_PlayerInput Input = {.m_Direction: 0};
2481 Input.m_Direction = PreInput.m_Direction;
2482 Input.m_TargetX = PreInput.m_TargetX;
2483 Input.m_TargetY = PreInput.m_TargetY;
2484 Input.m_Jump = PreInput.m_Jump;
2485 Input.m_Fire = PreInput.m_Fire;
2486 Input.m_Hook = PreInput.m_Hook;
2487 Input.m_WantedWeapon = PreInput.m_WantedWeapon;
2488 Input.m_NextWeapon = PreInput.m_NextWeapon;
2489 Input.m_PrevWeapon = PreInput.m_PrevWeapon;
2490
2491 pChar->OnDirectInput(pNewInput: &Input);
2492 }
2493 }
2494 }
2495
2496 m_PredictedWorld.m_GameTick = Tick;
2497 if(pInputData)
2498 pLocalChar->OnPredictedInput(pNewInput: pInputData);
2499 if(pDummyInputData)
2500 pDummyChar->OnPredictedInput(pNewInput: pDummyInputData);
2501
2502 if(g_Config.m_ClAntiPingPreInput)
2503 {
2504 for(int i = 0; i < MAX_CLIENTS; i++)
2505 {
2506 if(CCharacter *pChar = m_PredictedWorld.GetCharacterById(Id: i))
2507 {
2508 if(pDummyChar == pChar || pLocalChar == pChar)
2509 continue;
2510
2511 const CNetMsg_Sv_PreInput PreInput = m_aClients[i].m_aPreInputs[Tick % 200];
2512 if(PreInput.m_IntendedTick != Tick)
2513 continue;
2514
2515 //convert preinput to input
2516 CNetObj_PlayerInput Input = {.m_Direction: 0};
2517 Input.m_Direction = PreInput.m_Direction;
2518 Input.m_TargetX = PreInput.m_TargetX;
2519 Input.m_TargetY = PreInput.m_TargetY;
2520 Input.m_Jump = PreInput.m_Jump;
2521 Input.m_Fire = PreInput.m_Fire;
2522 Input.m_Hook = PreInput.m_Hook;
2523 Input.m_WantedWeapon = PreInput.m_WantedWeapon;
2524 Input.m_NextWeapon = PreInput.m_NextWeapon;
2525 Input.m_PrevWeapon = PreInput.m_PrevWeapon;
2526
2527 pChar->OnPredictedInput(pNewInput: &Input);
2528 }
2529 }
2530 }
2531
2532 m_PredictedWorld.Tick();
2533
2534 // fetch the current characters
2535 if(Tick == PredictionTick)
2536 {
2537 m_PrevPredictedWorld.CopyWorld(pFrom: &m_PredictedWorld);
2538
2539 for(int i = 0; i < MAX_CLIENTS; i++)
2540 if(CCharacter *pChar = m_PredictedWorld.GetCharacterById(Id: i))
2541 m_aClients[i].m_Predicted = pChar->GetCore();
2542 }
2543
2544 if(Tick == Client()->PredGameTick(Conn: g_Config.m_ClDummy))
2545 {
2546 m_PredictedChar = pLocalChar->GetCore();
2547 m_aClients[m_Snap.m_LocalClientId].m_Predicted = pLocalChar->GetCore();
2548
2549 if(pDummyChar)
2550 m_aClients[m_aLocalIds[!g_Config.m_ClDummy]].m_Predicted = pDummyChar->GetCore();
2551 }
2552
2553 for(int i = 0; i < MAX_CLIENTS; i++)
2554 if(CCharacter *pChar = m_PredictedWorld.GetCharacterById(Id: i))
2555 {
2556 m_aClients[i].m_aPredPos[Tick % 200] = pChar->Core()->m_Pos;
2557 m_aClients[i].m_aPredTick[Tick % 200] = Tick;
2558 }
2559
2560 // check if we want to trigger effects
2561 if(Tick > m_aLastNewPredictedTick[Dummy])
2562 {
2563 m_aLastNewPredictedTick[Dummy] = Tick;
2564 m_NewPredictedTick = true;
2565 vec2 Pos = pLocalChar->Core()->m_Pos;
2566 int Events = pLocalChar->Core()->m_TriggeredEvents;
2567 if(g_Config.m_ClPredict && !m_SuppressEvents)
2568 if(Events & COREEVENT_AIR_JUMP)
2569 m_Effects.AirJump(Pos, Alpha: 1.0f, Volume: 1.0f);
2570 if(g_Config.m_SndGame && !m_SuppressEvents)
2571 {
2572 if(Events & COREEVENT_GROUND_JUMP)
2573 m_Sounds.PlayAndRecord(Channel: CSounds::CHN_WORLD, SetId: SOUND_PLAYER_JUMP, Volume: 1.0f, Position: Pos);
2574 if(Events & COREEVENT_HOOK_ATTACH_GROUND)
2575 m_Sounds.PlayAndRecord(Channel: CSounds::CHN_WORLD, SetId: SOUND_HOOK_ATTACH_GROUND, Volume: 1.0f, Position: Pos);
2576 if(Events & COREEVENT_HOOK_HIT_NOHOOK)
2577 m_Sounds.PlayAndRecord(Channel: CSounds::CHN_WORLD, SetId: SOUND_HOOK_NOATTACH, Volume: 1.0f, Position: Pos);
2578 }
2579 }
2580
2581 // check if we want to trigger predicted airjump for dummy
2582 if(AntiPingPlayers() && pDummyChar && Tick > m_aLastNewPredictedTick[!Dummy])
2583 {
2584 m_aLastNewPredictedTick[!Dummy] = Tick;
2585 vec2 Pos = pDummyChar->Core()->m_Pos;
2586 int Events = pDummyChar->Core()->m_TriggeredEvents;
2587 if(g_Config.m_ClPredict && !m_SuppressEvents)
2588 if(Events & COREEVENT_AIR_JUMP)
2589 m_Effects.AirJump(Pos, Alpha: 1.0f, Volume: 1.0f);
2590 }
2591 }
2592
2593 // detect mispredictions of other players and make corrections smoother when possible
2594 if(g_Config.m_ClAntiPingSmooth && Predict() && AntiPingPlayers() && m_NewTick && m_PredictedTick >= MIN_TICK && absolute(a: m_PredictedTick - Client()->PredGameTick(Conn: g_Config.m_ClDummy)) <= 1 && absolute(a: Client()->GameTick(Conn: g_Config.m_ClDummy) - Client()->PrevGameTick(Conn: g_Config.m_ClDummy)) <= 2)
2595 {
2596 int PredTime = std::clamp(val: Client()->GetPredictionTime(), lo: 0, hi: 800);
2597 float SmoothPace = 4 - 1.5f * PredTime / 800.f; // smoothing pace (a lower value will make the smoothing quicker)
2598 int64_t Len = 1000 * PredTime * SmoothPace;
2599
2600 for(int i = 0; i < MAX_CLIENTS; i++)
2601 {
2602 if(!m_Snap.m_aCharacters[i].m_Active || i == m_Snap.m_LocalClientId || !m_aLastActive[i])
2603 continue;
2604 vec2 NewPos = (m_PredictedTick == Client()->PredGameTick(Conn: g_Config.m_ClDummy)) ? m_aClients[i].m_Predicted.m_Pos : m_aClients[i].m_PrevPredicted.m_Pos;
2605 vec2 PredErr = (m_aLastPos[i] - NewPos) / (float)minimum(a: Client()->GetPredictionTime(), b: 200);
2606 if(in_range(a: length(a: PredErr), lower: 0.05f, upper: 5.f))
2607 {
2608 vec2 PredPos = mix(a: m_aClients[i].m_PrevPredicted.m_Pos, b: m_aClients[i].m_Predicted.m_Pos, amount: Client()->PredIntraGameTick(Conn: g_Config.m_ClDummy));
2609 vec2 CurPos = mix(
2610 a: vec2(m_Snap.m_aCharacters[i].m_Prev.m_X, m_Snap.m_aCharacters[i].m_Prev.m_Y),
2611 b: vec2(m_Snap.m_aCharacters[i].m_Cur.m_X, m_Snap.m_aCharacters[i].m_Cur.m_Y),
2612 amount: Client()->IntraGameTick(Conn: g_Config.m_ClDummy));
2613 vec2 RenderDiff = PredPos - aBeforeRender[i];
2614 vec2 PredDiff = PredPos - CurPos;
2615
2616 float aMixAmount[2];
2617 for(int j = 0; j < 2; j++)
2618 {
2619 aMixAmount[j] = 1.0f;
2620 if(absolute(a: PredErr[j]) > 0.05f)
2621 {
2622 aMixAmount[j] = 0.0f;
2623 if(absolute(a: RenderDiff[j]) > 0.01f)
2624 {
2625 aMixAmount[j] = 1.f - std::clamp(val: RenderDiff[j] / PredDiff[j], lo: 0.f, hi: 1.f);
2626 aMixAmount[j] = 1.f - std::pow(x: 1.f - aMixAmount[j], y: 1 / 1.2f);
2627 }
2628 }
2629 int64_t TimePassed = time_get() - m_aClients[i].m_aSmoothStart[j];
2630 if(in_range(a: TimePassed, lower: (int64_t)0, upper: Len - 1))
2631 aMixAmount[j] = minimum(a: aMixAmount[j], b: (float)(TimePassed / (double)Len));
2632 }
2633 for(int j = 0; j < 2; j++)
2634 if(absolute(a: RenderDiff[j]) < 0.01f && absolute(a: PredDiff[j]) < 0.01f && absolute(a: m_aClients[i].m_PrevPredicted.m_Pos[j] - m_aClients[i].m_Predicted.m_Pos[j]) < 0.01f && aMixAmount[j] > aMixAmount[j ^ 1])
2635 aMixAmount[j] = aMixAmount[j ^ 1];
2636 for(int j = 0; j < 2; j++)
2637 {
2638 int64_t Remaining = minimum(a: (1.f - aMixAmount[j]) * Len, b: minimum(a: time_freq() * 0.700f, b: (1.f - aMixAmount[j ^ 1]) * Len + time_freq() * 0.300f)); // don't smooth for longer than 700ms, or more than 300ms longer along one axis than the other axis
2639 int64_t Start = time_get() - (Len - Remaining);
2640 if(!in_range(a: Start + Len, lower: m_aClients[i].m_aSmoothStart[j], upper: m_aClients[i].m_aSmoothStart[j] + Len))
2641 {
2642 m_aClients[i].m_aSmoothStart[j] = Start;
2643 m_aClients[i].m_aSmoothLen[j] = Len;
2644 }
2645 }
2646 }
2647 }
2648 }
2649
2650 for(int i = 0; i < MAX_CLIENTS; i++)
2651 {
2652 if(m_Snap.m_aCharacters[i].m_Active)
2653 {
2654 m_aLastPos[i] = m_aClients[i].m_Predicted.m_Pos;
2655 m_aLastActive[i] = true;
2656 }
2657 else
2658 m_aLastActive[i] = false;
2659 }
2660
2661 if(g_Config.m_Debug && g_Config.m_ClPredict && m_PredictedTick == Client()->PredGameTick(Conn: g_Config.m_ClDummy))
2662 {
2663 CNetObj_CharacterCore Before = {.m_Tick: 0}, Now = {.m_Tick: 0}, BeforePrev = {.m_Tick: 0}, NowPrev = {.m_Tick: 0};
2664 BeforeChar.Write(pObjCore: &Before);
2665 BeforePrevChar.Write(pObjCore: &BeforePrev);
2666 m_PredictedChar.Write(pObjCore: &Now);
2667 m_PredictedPrevChar.Write(pObjCore: &NowPrev);
2668
2669 if(mem_comp(a: &Before, b: &Now, size: sizeof(CNetObj_CharacterCore)) != 0)
2670 {
2671 Console()->Print(Level: IConsole::OUTPUT_LEVEL_DEBUG, pFrom: "client", pStr: "prediction error");
2672 for(unsigned i = 0; i < sizeof(CNetObj_CharacterCore) / sizeof(int); i++)
2673 if(((int *)&Before)[i] != ((int *)&Now)[i])
2674 {
2675 char aBuf[256];
2676 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: " %d %d %d (%d %d)", i, ((int *)&Before)[i], ((int *)&Now)[i], ((int *)&BeforePrev)[i], ((int *)&NowPrev)[i]);
2677 Console()->Print(Level: IConsole::OUTPUT_LEVEL_DEBUG, pFrom: "client", pStr: aBuf);
2678 }
2679 }
2680 }
2681
2682 m_PredictedTick = Client()->PredGameTick(Conn: g_Config.m_ClDummy);
2683
2684 if(m_NewPredictedTick)
2685 m_Ghost.OnNewPredictedSnapshot();
2686}
2687
2688void CGameClient::OnActivateEditor()
2689{
2690 OnRelease();
2691}
2692
2693CGameClient::CClientStats::CClientStats()
2694{
2695 Reset();
2696}
2697
2698void CGameClient::CClientStats::Reset()
2699{
2700 m_JoinTick = 0;
2701 m_IngameTicks = 0;
2702 m_Active = false;
2703
2704 std::fill(first: std::begin(arr&: m_aFragsWith), last: std::end(arr&: m_aFragsWith), value: 0);
2705 std::fill(first: std::begin(arr&: m_aDeathsFrom), last: std::end(arr&: m_aDeathsFrom), value: 0);
2706 m_Frags = 0;
2707 m_Deaths = 0;
2708 m_Suicides = 0;
2709 m_BestSpree = 0;
2710 m_CurrentSpree = 0;
2711
2712 m_FlagGrabs = 0;
2713 m_FlagCaptures = 0;
2714}
2715
2716void CGameClient::CClientData::UpdateSkinInfo()
2717{
2718 const CSkinDescriptor SkinDescriptor = ToSkinDescriptor();
2719 if(SkinDescriptor.m_Flags == 0)
2720 {
2721 return;
2722 }
2723
2724 const auto &&ApplySkinProperties = [&]() {
2725 if(SkinDescriptor.m_Flags & CSkinDescriptor::FLAG_SIX)
2726 {
2727 m_pSkinInfo->TeeRenderInfo().ApplyColors(CustomColoredSkin: m_UseCustomColor, ColorBody: m_ColorBody, ColorFeet: m_ColorFeet);
2728 }
2729 if(SkinDescriptor.m_Flags & CSkinDescriptor::FLAG_SEVEN)
2730 {
2731 for(int Dummy = 0; Dummy < NUM_DUMMIES; Dummy++)
2732 {
2733 const CClientData::CSixup &SixupData = m_aSixup[Dummy];
2734 CTeeRenderInfo::CSixup &SixupSkinInfo = m_pSkinInfo->TeeRenderInfo().m_aSixup[Dummy];
2735 for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++)
2736 {
2737 m_pGameClient->m_Skins7.ApplyColorTo(SixupRenderInfo&: SixupSkinInfo, UseCustomColors: SixupData.m_aUseCustomColors[Part], Value: SixupData.m_aSkinPartColors[Part], Part);
2738 }
2739 UpdateSkin7HatSprite(Dummy);
2740 UpdateSkin7BotDecoration(Dummy);
2741 }
2742 }
2743 m_pSkinInfo->TeeRenderInfo().m_Size = 64.0f;
2744 };
2745
2746 if(m_pSkinInfo == nullptr)
2747 {
2748 CTeeRenderInfo TeeRenderInfo;
2749 m_pSkinInfo = m_pGameClient->CreateManagedTeeRenderInfo(TeeRenderInfo, SkinDescriptor);
2750 m_pSkinInfo->SetRefreshCallback([&]() { UpdateRenderInfo(); });
2751 ApplySkinProperties();
2752 m_pSkinInfo->m_RefreshCallback();
2753 }
2754 else if(m_pSkinInfo->SkinDescriptor() != SkinDescriptor)
2755 {
2756 m_pSkinInfo->m_SkinDescriptor = SkinDescriptor;
2757 m_pGameClient->RefreshSkin(pManagedTeeRenderInfo: m_pSkinInfo);
2758 ApplySkinProperties();
2759 }
2760 else
2761 {
2762 ApplySkinProperties();
2763 m_pSkinInfo->m_RefreshCallback();
2764 }
2765}
2766
2767void CGameClient::CClientData::UpdateRenderInfo()
2768{
2769 m_RenderInfo = m_pSkinInfo->TeeRenderInfo();
2770
2771 // force team colors
2772 if(m_pGameClient->IsTeamPlay())
2773 {
2774 m_RenderInfo.m_CustomColoredSkin = true;
2775 for(auto &Sixup : m_RenderInfo.m_aSixup)
2776 {
2777 std::fill(first: std::begin(arr&: Sixup.m_aUseCustomColors), last: std::end(arr&: Sixup.m_aUseCustomColors), value: true);
2778 }
2779
2780 if(m_Team >= TEAM_RED && m_Team <= TEAM_BLUE)
2781 {
2782 const int aTeamColors[2] = {65461, 10223541};
2783 m_RenderInfo.m_ColorBody = color_cast<ColorRGBA>(hsl: ColorHSLA(aTeamColors[m_Team]));
2784 m_RenderInfo.m_ColorFeet = color_cast<ColorRGBA>(hsl: ColorHSLA(aTeamColors[m_Team]));
2785
2786 // 0.7
2787 for(auto &Sixup : m_RenderInfo.m_aSixup)
2788 {
2789 const ColorRGBA aTeamColorsSixup[2] = {
2790 ColorRGBA(0.753f, 0.318f, 0.318f, 1.0f),
2791 ColorRGBA(0.318f, 0.471f, 0.753f, 1.0f)};
2792 const ColorRGBA aMarkingColorsSixup[2] = {
2793 ColorRGBA(0.824f, 0.345f, 0.345f, 1.0f),
2794 ColorRGBA(0.345f, 0.514f, 0.824f, 1.0f)};
2795 float MarkingAlpha = Sixup.m_aColors[protocol7::SKINPART_MARKING].a;
2796 for(auto &Color : Sixup.m_aColors)
2797 {
2798 Color = aTeamColorsSixup[m_Team];
2799 }
2800 if(MarkingAlpha > 0.1f)
2801 {
2802 Sixup.m_aColors[protocol7::SKINPART_MARKING] = aMarkingColorsSixup[m_Team];
2803 }
2804 }
2805 }
2806 else
2807 {
2808 m_RenderInfo.m_ColorBody = color_cast<ColorRGBA>(hsl: ColorHSLA(12829350));
2809 m_RenderInfo.m_ColorFeet = color_cast<ColorRGBA>(hsl: ColorHSLA(12829350));
2810 for(auto &Sixup : m_RenderInfo.m_aSixup)
2811 {
2812 for(auto &Color : Sixup.m_aColors)
2813 {
2814 Color = color_cast<ColorRGBA>(hsl: ColorHSLA(12829350));
2815 }
2816 }
2817 }
2818 }
2819}
2820
2821void CGameClient::CClientData::Reset()
2822{
2823 m_UseCustomColor = 0;
2824 m_ColorBody = 0;
2825 m_ColorFeet = 0;
2826
2827 m_aName[0] = '\0';
2828 m_aClan[0] = '\0';
2829 m_Country = -1;
2830 str_copy(dst&: m_aSkinName, src: "default");
2831
2832 m_Team = 0;
2833 m_Emoticon = 0;
2834 m_EmoticonStartFraction = 0;
2835 m_EmoticonStartTick = -1;
2836
2837 m_Solo = false;
2838 m_Jetpack = false;
2839 m_CollisionDisabled = false;
2840 m_EndlessHook = false;
2841 m_EndlessJump = false;
2842 m_HammerHitDisabled = false;
2843 m_GrenadeHitDisabled = false;
2844 m_LaserHitDisabled = false;
2845 m_ShotgunHitDisabled = false;
2846 m_HookHitDisabled = false;
2847 m_Super = false;
2848 m_Invincible = false;
2849 m_HasTelegunGun = false;
2850 m_HasTelegunGrenade = false;
2851 m_HasTelegunLaser = false;
2852 m_FreezeEnd = 0;
2853 m_DeepFrozen = false;
2854 m_LiveFrozen = false;
2855
2856 m_Predicted.Reset();
2857 m_PrevPredicted.Reset();
2858
2859 if(m_pSkinInfo != nullptr)
2860 {
2861 // Make sure other `shared_ptr`s to this skin info will not use the refresh callback that refers to this reset client data
2862 m_pSkinInfo->SetRefreshCallback(nullptr);
2863 m_pSkinInfo = nullptr;
2864 }
2865 m_RenderInfo.Reset();
2866
2867 m_Angle = 0.0f;
2868 m_Active = false;
2869 m_ChatIgnore = false;
2870 m_EmoticonIgnore = false;
2871 m_Friend = false;
2872 m_Foe = false;
2873
2874 m_AuthLevel = AUTHED_NO;
2875 m_Afk = false;
2876 m_Paused = false;
2877 m_Spec = false;
2878
2879 std::fill(first: std::begin(arr&: m_aSwitchStates), last: std::end(arr&: m_aSwitchStates), value: 0);
2880
2881 m_Snapped.m_Tick = -1;
2882 m_Evolved.m_Tick = -1;
2883
2884 for(auto &PreInput : m_aPreInputs)
2885 {
2886 PreInput.m_IntendedTick = -1;
2887 }
2888
2889 m_RenderCur.m_Tick = -1;
2890 m_RenderPrev.m_Tick = -1;
2891 m_RenderPos = vec2(0.0f, 0.0f);
2892 m_IsPredicted = false;
2893 m_IsPredictedLocal = false;
2894 std::fill(first: std::begin(arr&: m_aSmoothStart), last: std::end(arr&: m_aSmoothStart), value: 0);
2895 std::fill(first: std::begin(arr&: m_aSmoothLen), last: std::end(arr&: m_aSmoothLen), value: 0);
2896 std::fill(first: std::begin(arr&: m_aPredPos), last: std::end(arr&: m_aPredPos), value: vec2(0.0f, 0.0f));
2897 std::fill(first: std::begin(arr&: m_aPredTick), last: std::end(arr&: m_aPredTick), value: 0);
2898 m_SpecCharPresent = false;
2899 m_SpecChar = vec2(0.0f, 0.0f);
2900
2901 for(auto &Info : m_aSixup)
2902 Info.Reset();
2903}
2904
2905CSkinDescriptor CGameClient::CClientData::ToSkinDescriptor() const
2906{
2907 CSkinDescriptor SkinDescriptor;
2908
2909 CTranslationContext::CClientData &TranslatedClient = m_pGameClient->m_pClient->m_TranslationContext.m_aClients[ClientId()];
2910 if(m_Active && !TranslatedClient.m_Active)
2911 {
2912 SkinDescriptor.m_Flags |= CSkinDescriptor::FLAG_SIX;
2913 str_copy(dst&: SkinDescriptor.m_aSkinName, src: m_aSkinName);
2914 }
2915 else if(TranslatedClient.m_Active)
2916 {
2917 SkinDescriptor.m_Flags |= CSkinDescriptor::FLAG_SEVEN;
2918 for(int Dummy = 0; Dummy < NUM_DUMMIES; Dummy++)
2919 {
2920 for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++)
2921 {
2922 str_copy(dst&: SkinDescriptor.m_aSixup[Dummy].m_aaSkinPartNames[Part], src: m_aSixup[Dummy].m_aaSkinPartNames[Part]);
2923 }
2924 SkinDescriptor.m_aSixup[Dummy].m_XmasHat = time_season() == SEASON_XMAS;
2925 SkinDescriptor.m_aSixup[Dummy].m_BotDecoration = (TranslatedClient.m_PlayerFlags7 & protocol7::PLAYERFLAG_BOT) != 0;
2926 }
2927 }
2928
2929 return SkinDescriptor;
2930}
2931
2932void CGameClient::CClientData::CSixup::Reset()
2933{
2934 for(int i = 0; i < protocol7::NUM_SKINPARTS; ++i)
2935 {
2936 m_aaSkinPartNames[i][0] = '\0';
2937 m_aUseCustomColors[i] = 0;
2938 m_aSkinPartColors[i] = 0;
2939 }
2940}
2941
2942void CGameClient::SendSwitchTeam(int Team) const
2943{
2944 CNetMsg_Cl_SetTeam Msg;
2945 Msg.m_Team = Team;
2946 Client()->SendPackMsgActive(pMsg: &Msg, Flags: MSGFLAG_VITAL);
2947}
2948
2949void CGameClient::SendStartInfo7(bool Dummy)
2950{
2951 protocol7::CNetMsg_Cl_StartInfo Msg;
2952 Msg.m_pName = Dummy ? Client()->DummyName() : Client()->PlayerName();
2953 Msg.m_pClan = Dummy ? Config()->m_ClDummyClan : Config()->m_PlayerClan;
2954 Msg.m_Country = Dummy ? Config()->m_ClDummyCountry : Config()->m_PlayerCountry;
2955 for(int p = 0; p < protocol7::NUM_SKINPARTS; p++)
2956 {
2957 Msg.m_apSkinPartNames[p] = CSkins7::ms_apSkinVariables[(int)Dummy][p];
2958 Msg.m_aUseCustomColors[p] = *CSkins7::ms_apUCCVariables[(int)Dummy][p];
2959 Msg.m_aSkinPartColors[p] = *CSkins7::ms_apColorVariables[(int)Dummy][p];
2960 }
2961 CMsgPacker Packer(&Msg, false, true);
2962 if(Msg.Pack(pPacker: &Packer))
2963 return;
2964 Client()->SendMsg(Conn: (int)Dummy, pMsg: &Packer, Flags: MSGFLAG_VITAL | MSGFLAG_FLUSH);
2965 m_aCheckInfo[(int)Dummy] = -1;
2966}
2967
2968void CGameClient::SendSkinChange7(bool Dummy)
2969{
2970 protocol7::CNetMsg_Cl_SkinChange Msg;
2971 for(int p = 0; p < protocol7::NUM_SKINPARTS; p++)
2972 {
2973 Msg.m_apSkinPartNames[p] = CSkins7::ms_apSkinVariables[(int)Dummy][p];
2974 Msg.m_aUseCustomColors[p] = *CSkins7::ms_apUCCVariables[(int)Dummy][p];
2975 Msg.m_aSkinPartColors[p] = *CSkins7::ms_apColorVariables[(int)Dummy][p];
2976 }
2977 CMsgPacker Packer(&Msg, false, true);
2978 if(Msg.Pack(pPacker: &Packer))
2979 return;
2980 Client()->SendMsg(Conn: (int)Dummy, pMsg: &Packer, Flags: MSGFLAG_VITAL | MSGFLAG_FLUSH);
2981 m_aCheckInfo[(int)Dummy] = Client()->GameTickSpeed();
2982}
2983
2984bool CGameClient::GotWantedSkin7(bool Dummy)
2985{
2986 // validate the wanted skinparts before comparison
2987 // because the skin parts we compare against are also validated
2988 // otherwise it tries to resend the skin info when the eyes are set to "negative"
2989 // in team based modes
2990 char aSkinParts[protocol7::NUM_SKINPARTS][protocol7::MAX_SKIN_ARRAY_SIZE];
2991 char *apSkinPartsPtr[protocol7::NUM_SKINPARTS];
2992 int aUCCVars[protocol7::NUM_SKINPARTS];
2993 int aColorVars[protocol7::NUM_SKINPARTS];
2994 for(int SkinPart = 0; SkinPart < protocol7::NUM_SKINPARTS; SkinPart++)
2995 {
2996 str_copy(dst: aSkinParts[SkinPart], src: CSkins7::ms_apSkinVariables[(int)Dummy][SkinPart], dst_size: protocol7::MAX_SKIN_ARRAY_SIZE);
2997 apSkinPartsPtr[SkinPart] = aSkinParts[SkinPart];
2998 aUCCVars[SkinPart] = *CSkins7::ms_apUCCVariables[(int)Dummy][SkinPart];
2999 aColorVars[SkinPart] = *CSkins7::ms_apColorVariables[(int)Dummy][SkinPart];
3000 }
3001 m_Skins7.ValidateSkinParts(apPartNames: apSkinPartsPtr, pUseCustomColors: aUCCVars, pPartColors: aColorVars, GameFlags: m_pClient->m_TranslationContext.m_GameFlags);
3002
3003 for(int SkinPart = 0; SkinPart < protocol7::NUM_SKINPARTS; SkinPart++)
3004 {
3005 if(str_comp(a: m_aClients[m_aLocalIds[(int)Dummy]].m_aSixup[g_Config.m_ClDummy].m_aaSkinPartNames[SkinPart], b: apSkinPartsPtr[SkinPart]))
3006 return false;
3007 if(m_aClients[m_aLocalIds[(int)Dummy]].m_aSixup[g_Config.m_ClDummy].m_aUseCustomColors[SkinPart] != aUCCVars[SkinPart])
3008 return false;
3009 if(m_aClients[m_aLocalIds[(int)Dummy]].m_aSixup[g_Config.m_ClDummy].m_aSkinPartColors[SkinPart] != aColorVars[SkinPart])
3010 return false;
3011 }
3012
3013 // TODO: add name change ddnet extension to 0.7 protocol
3014 // if(str_comp(m_aClients[m_aLocalIds[(int)Dummy]].m_aName, Dummy ? Client()->DummyName() : Client()->PlayerName()))
3015 // return false;
3016 // if(str_comp(m_aClients[m_aLocalIds[(int)Dummy]].m_aClan, Dummy ? g_Config.m_ClDummyClan : g_Config.m_PlayerClan))
3017 // return false;
3018 // if(m_aClients[m_aLocalIds[(int)Dummy]].m_Country != (Dummy ? g_Config.m_ClDummyCountry : g_Config.m_PlayerCountry))
3019 // return false;
3020
3021 return true;
3022}
3023
3024void CGameClient::SendInfo(bool Start)
3025{
3026 if(m_pClient->IsSixup())
3027 {
3028 if(Start)
3029 SendStartInfo7(Dummy: false);
3030 else
3031 SendSkinChange7(Dummy: false);
3032 return;
3033 }
3034 if(Start)
3035 {
3036 CNetMsg_Cl_StartInfo Msg;
3037 Msg.m_pName = Client()->PlayerName();
3038 Msg.m_pClan = g_Config.m_PlayerClan;
3039 Msg.m_Country = g_Config.m_PlayerCountry;
3040 Msg.m_pSkin = g_Config.m_ClPlayerSkin;
3041 Msg.m_UseCustomColor = g_Config.m_ClPlayerUseCustomColor;
3042 Msg.m_ColorBody = g_Config.m_ClPlayerColorBody;
3043 Msg.m_ColorFeet = g_Config.m_ClPlayerColorFeet;
3044 CMsgPacker Packer(&Msg);
3045 Msg.Pack(pPacker: &Packer);
3046 Client()->SendMsg(Conn: IClient::CONN_MAIN, pMsg: &Packer, Flags: MSGFLAG_VITAL | MSGFLAG_FLUSH);
3047 m_aCheckInfo[0] = -1;
3048 }
3049 else
3050 {
3051 CNetMsg_Cl_ChangeInfo Msg;
3052 Msg.m_pName = Client()->PlayerName();
3053 Msg.m_pClan = g_Config.m_PlayerClan;
3054 Msg.m_Country = g_Config.m_PlayerCountry;
3055 Msg.m_pSkin = g_Config.m_ClPlayerSkin;
3056 Msg.m_UseCustomColor = g_Config.m_ClPlayerUseCustomColor;
3057 Msg.m_ColorBody = g_Config.m_ClPlayerColorBody;
3058 Msg.m_ColorFeet = g_Config.m_ClPlayerColorFeet;
3059 CMsgPacker Packer(&Msg);
3060 Msg.Pack(pPacker: &Packer);
3061 Client()->SendMsg(Conn: IClient::CONN_MAIN, pMsg: &Packer, Flags: MSGFLAG_VITAL);
3062 m_aCheckInfo[0] = Client()->GameTickSpeed();
3063 }
3064}
3065
3066void CGameClient::SendDummyInfo(bool Start)
3067{
3068 if(m_pClient->IsSixup())
3069 {
3070 if(Start)
3071 SendStartInfo7(Dummy: true);
3072 else
3073 SendSkinChange7(Dummy: true);
3074 return;
3075 }
3076 if(Start)
3077 {
3078 CNetMsg_Cl_StartInfo Msg;
3079 Msg.m_pName = Client()->DummyName();
3080 Msg.m_pClan = g_Config.m_ClDummyClan;
3081 Msg.m_Country = g_Config.m_ClDummyCountry;
3082 Msg.m_pSkin = g_Config.m_ClDummySkin;
3083 Msg.m_UseCustomColor = g_Config.m_ClDummyUseCustomColor;
3084 Msg.m_ColorBody = g_Config.m_ClDummyColorBody;
3085 Msg.m_ColorFeet = g_Config.m_ClDummyColorFeet;
3086 CMsgPacker Packer(&Msg);
3087 Msg.Pack(pPacker: &Packer);
3088 Client()->SendMsg(Conn: IClient::CONN_DUMMY, pMsg: &Packer, Flags: MSGFLAG_VITAL);
3089 m_aCheckInfo[1] = -1;
3090 }
3091 else
3092 {
3093 CNetMsg_Cl_ChangeInfo Msg;
3094 Msg.m_pName = Client()->DummyName();
3095 Msg.m_pClan = g_Config.m_ClDummyClan;
3096 Msg.m_Country = g_Config.m_ClDummyCountry;
3097 Msg.m_pSkin = g_Config.m_ClDummySkin;
3098 Msg.m_UseCustomColor = g_Config.m_ClDummyUseCustomColor;
3099 Msg.m_ColorBody = g_Config.m_ClDummyColorBody;
3100 Msg.m_ColorFeet = g_Config.m_ClDummyColorFeet;
3101 CMsgPacker Packer(&Msg);
3102 Msg.Pack(pPacker: &Packer);
3103 Client()->SendMsg(Conn: IClient::CONN_DUMMY, pMsg: &Packer, Flags: MSGFLAG_VITAL);
3104 m_aCheckInfo[1] = Client()->GameTickSpeed();
3105 }
3106}
3107
3108void CGameClient::SendKill() const
3109{
3110 CNetMsg_Cl_Kill Msg;
3111 Client()->SendPackMsgActive(pMsg: &Msg, Flags: MSGFLAG_VITAL);
3112
3113 if(g_Config.m_ClDummyCopyMoves)
3114 {
3115 CMsgPacker MsgP(NETMSGTYPE_CL_KILL, false);
3116 Client()->SendMsg(Conn: !g_Config.m_ClDummy, pMsg: &MsgP, Flags: MSGFLAG_VITAL);
3117 }
3118}
3119
3120void CGameClient::SendReadyChange7()
3121{
3122 if(!Client()->IsSixup())
3123 {
3124 Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "client", pStr: "Error you have to be connected to a 0.7 server to use ready_change");
3125 return;
3126 }
3127 protocol7::CNetMsg_Cl_ReadyChange Msg;
3128 Client()->SendPackMsgActive(pMsg: &Msg, Flags: MSGFLAG_VITAL, NoTranslate: true);
3129}
3130
3131void CGameClient::ConTeam(IConsole::IResult *pResult, void *pUserData)
3132{
3133 ((CGameClient *)pUserData)->SendSwitchTeam(Team: pResult->GetInteger(Index: 0));
3134}
3135
3136void CGameClient::ConKill(IConsole::IResult *pResult, void *pUserData)
3137{
3138 ((CGameClient *)pUserData)->SendKill();
3139}
3140
3141void CGameClient::ConReadyChange7(IConsole::IResult *pResult, void *pUserData)
3142{
3143 CGameClient *pClient = static_cast<CGameClient *>(pUserData);
3144 if(pClient->Client()->State() == IClient::STATE_ONLINE)
3145 pClient->SendReadyChange7();
3146}
3147
3148void CGameClient::ConchainLanguageUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
3149{
3150 CGameClient *pThis = static_cast<CGameClient *>(pUserData);
3151 const bool Changed = pThis->Client()->GlobalTime() && pResult->NumArguments() && str_comp(a: pResult->GetString(Index: 0), b: g_Config.m_ClLanguagefile) != 0;
3152 pfnCallback(pResult, pCallbackUserData);
3153 if(Changed)
3154 {
3155 pThis->OnLanguageChange();
3156 }
3157}
3158
3159void CGameClient::ConchainSpecialInfoupdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
3160{
3161 pfnCallback(pResult, pCallbackUserData);
3162 if(pResult->NumArguments())
3163 ((CGameClient *)pUserData)->SendInfo(Start: false);
3164}
3165
3166void CGameClient::ConchainSpecialDummyInfoupdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
3167{
3168 pfnCallback(pResult, pCallbackUserData);
3169 if(pResult->NumArguments())
3170 ((CGameClient *)pUserData)->SendDummyInfo(Start: false);
3171}
3172
3173void CGameClient::ConchainSpecialDummy(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
3174{
3175 pfnCallback(pResult, pCallbackUserData);
3176 if(pResult->NumArguments())
3177 {
3178 if(g_Config.m_ClDummy && !((CGameClient *)pUserData)->Client()->DummyConnected())
3179 g_Config.m_ClDummy = 0;
3180 }
3181}
3182
3183IGameClient *CreateGameClient()
3184{
3185 return new CGameClient();
3186}
3187
3188int CGameClient::IntersectCharacter(vec2 HookPos, vec2 NewPos, vec2 &NewPos2, int OwnId, vec2 *pPlayerPosition)
3189{
3190 float Distance = 0.0f;
3191 int ClosestId = -1;
3192
3193 const CClientData &OwnClientData = m_aClients[OwnId];
3194
3195 for(int i = 0; i < MAX_CLIENTS; i++)
3196 {
3197 if(i == OwnId)
3198 continue;
3199
3200 const CClientData &Data = m_aClients[i];
3201
3202 if(!Data.m_Active || !m_Snap.m_aCharacters[i].m_Active)
3203 continue;
3204
3205 CNetObj_Character Prev = m_Snap.m_aCharacters[i].m_Prev;
3206 CNetObj_Character Player = m_Snap.m_aCharacters[i].m_Cur;
3207
3208 vec2 Position = mix(a: vec2(Prev.m_X, Prev.m_Y), b: vec2(Player.m_X, Player.m_Y), amount: Client()->IntraGameTick(Conn: g_Config.m_ClDummy));
3209
3210 bool IsOneSuper = Data.m_Super || OwnClientData.m_Super;
3211 bool IsOneSolo = Data.m_Solo || OwnClientData.m_Solo;
3212
3213 if(!IsOneSuper && (!m_Teams.SameTeam(ClientId1: i, ClientId2: OwnId) || IsOneSolo || OwnClientData.m_HookHitDisabled))
3214 continue;
3215
3216 vec2 ClosestPoint;
3217 if(closest_point_on_line(line_pointA: HookPos, line_pointB: NewPos, target_point: Position, out_pos&: ClosestPoint))
3218 {
3219 if(distance(a: Position, b: ClosestPoint) < CCharacterCore::PhysicalSize() + 2.0f)
3220 {
3221 if(ClosestId == -1 || distance(a: HookPos, b: Position) < Distance)
3222 {
3223 NewPos2 = ClosestPoint;
3224 ClosestId = i;
3225 Distance = distance(a: HookPos, b: Position);
3226 if(pPlayerPosition)
3227 *pPlayerPosition = Position;
3228 }
3229 }
3230 }
3231 }
3232
3233 return ClosestId;
3234}
3235
3236ColorRGBA CalculateNameColor(ColorHSLA TextColorHSL)
3237{
3238 return color_cast<ColorRGBA>(hsl: ColorHSLA(TextColorHSL.h, TextColorHSL.s * 0.68f, TextColorHSL.l * 0.81f));
3239}
3240
3241void CGameClient::UpdateLocalTuning()
3242{
3243 m_GameWorld.m_WorldConfig.m_UseTuneZones = m_GameInfo.m_PredictDDRaceTiles;
3244
3245 // always update default tune zone, even without character
3246 if(!m_GameWorld.m_WorldConfig.m_UseTuneZones)
3247 m_GameWorld.TuningList()[0] = m_aTuning[g_Config.m_ClDummy];
3248
3249 if(!m_Snap.m_pLocalCharacter && !m_Snap.m_pSpectatorInfo)
3250 return;
3251
3252 vec2 LocalPos = m_Snap.m_pLocalCharacter ? vec2(m_Snap.m_pLocalCharacter->m_X, m_Snap.m_pLocalCharacter->m_Y) : vec2(m_Snap.m_pSpectatorInfo->m_X, m_Snap.m_pSpectatorInfo->m_Y);
3253
3254 // update the tuning at the local position with the latest tunings received before the new snapshot
3255 if(m_GameWorld.m_WorldConfig.m_UseTuneZones)
3256 {
3257 int TuneZone =
3258 m_Snap.m_aCharacters[m_Snap.m_LocalClientId].m_HasExtendedData &&
3259 m_Snap.m_aCharacters[m_Snap.m_LocalClientId].m_ExtendedData.m_TuneZoneOverride != TuneZone::OVERRIDE_NONE ?
3260 m_Snap.m_aCharacters[m_Snap.m_LocalClientId].m_ExtendedData.m_TuneZoneOverride :
3261 Collision()->IsTune(Index: Collision()->GetMapIndex(Pos: LocalPos));
3262
3263 if(TuneZone != m_aLocalTuneZone[g_Config.m_ClDummy])
3264 {
3265 // our tunezone changed, expecting tuning message
3266 m_aLocalTuneZone[g_Config.m_ClDummy] = m_aExpectingTuningForZone[g_Config.m_ClDummy] = TuneZone;
3267 m_aExpectingTuningSince[g_Config.m_ClDummy] = 0;
3268 }
3269
3270 // tunezone could have changed, send dummy tuning to demo
3271 if(m_ActiveRecordings.any() && m_IsDummySwapping && m_aLocalTuneZone[0] != m_aLocalTuneZone[1])
3272 {
3273 CMsgPacker Msg(NETMSGTYPE_SV_TUNEPARAMS);
3274 int *pParams = (int *)&m_aTuning[g_Config.m_ClDummy];
3275 for(unsigned i = 0; i < sizeof(m_aTuning[0]) / sizeof(int); i++)
3276 Msg.AddInt(i: pParams[i]);
3277 Client()->SendMsgActive(pMsg: &Msg, Flags: MSGFLAG_RECORD | MSGFLAG_NOSEND);
3278 }
3279
3280 if(m_aExpectingTuningForZone[g_Config.m_ClDummy] >= 0)
3281 {
3282 if(m_aReceivedTuning[g_Config.m_ClDummy])
3283 {
3284 TuningList()[m_aExpectingTuningForZone[g_Config.m_ClDummy]] = m_aTuning[g_Config.m_ClDummy];
3285 m_GameWorld.TuningList()[m_aExpectingTuningForZone[g_Config.m_ClDummy]] = m_aTuning[g_Config.m_ClDummy];
3286 m_aReceivedTuning[g_Config.m_ClDummy] = false;
3287 m_aExpectingTuningForZone[g_Config.m_ClDummy] = -1;
3288 }
3289 else if(m_aExpectingTuningSince[g_Config.m_ClDummy] >= 5)
3290 {
3291 // if we are expecting tuning for more than 10 snaps (less than a quarter of a second)
3292 // it is probably dropped or it was received out of order
3293 // or applied to another tunezone.
3294 // we need to fallback to current tuning to fix ourselves.
3295 m_aExpectingTuningForZone[g_Config.m_ClDummy] = -1;
3296 m_aExpectingTuningSince[g_Config.m_ClDummy] = 0;
3297 m_aReceivedTuning[g_Config.m_ClDummy] = false;
3298 dbg_msg(sys: "tunezone", fmt: "the tuning was missed");
3299 }
3300 else
3301 {
3302 // if we are expecting tuning and have not received one yet.
3303 // do not update any tuning, so we don't apply it to the wrong tunezone.
3304 dbg_msg(sys: "tunezone", fmt: "waiting for tuning for zone %d", m_aExpectingTuningForZone[g_Config.m_ClDummy]);
3305 m_aExpectingTuningSince[g_Config.m_ClDummy]++;
3306 }
3307 }
3308 else
3309 {
3310 // if we have processed what we need, and the tuning is still wrong due to out of order message
3311 // fix our tuning by using the current one
3312 m_GameWorld.TuningList()[TuneZone] = m_aTuning[g_Config.m_ClDummy];
3313 m_aExpectingTuningSince[g_Config.m_ClDummy] = 0;
3314 m_aReceivedTuning[g_Config.m_ClDummy] = false;
3315 }
3316 }
3317}
3318
3319void CGameClient::UpdatePrediction()
3320{
3321 m_GameWorld.m_WorldConfig.m_IsVanilla = m_GameInfo.m_PredictVanilla;
3322 m_GameWorld.m_WorldConfig.m_IsDDRace = m_GameInfo.m_PredictDDRace;
3323 m_GameWorld.m_WorldConfig.m_IsFNG = m_GameInfo.m_PredictFNG;
3324 m_GameWorld.m_WorldConfig.m_PredictDDRace = m_GameInfo.m_PredictDDRace;
3325 m_GameWorld.m_WorldConfig.m_PredictTiles = m_GameInfo.m_PredictDDRace && m_GameInfo.m_PredictDDRaceTiles;
3326 m_GameWorld.m_WorldConfig.m_PredictFreeze = g_Config.m_ClPredictFreeze;
3327 m_GameWorld.m_WorldConfig.m_PredictWeapons = AntiPingWeapons();
3328 m_GameWorld.m_WorldConfig.m_BugDDRaceInput = m_GameInfo.m_BugDDRaceInput;
3329 m_GameWorld.m_WorldConfig.m_NoWeakHookAndBounce = m_GameInfo.m_NoWeakHookAndBounce;
3330
3331 if(!m_Snap.m_pLocalCharacter)
3332 {
3333 if(CCharacter *pLocalChar = m_GameWorld.GetCharacterById(Id: m_Snap.m_LocalClientId))
3334 pLocalChar->Destroy();
3335 return;
3336 }
3337
3338 if(m_Snap.m_pLocalCharacter->m_AmmoCount > 0 && m_Snap.m_pLocalCharacter->m_Weapon != WEAPON_NINJA)
3339 m_GameWorld.m_WorldConfig.m_InfiniteAmmo = false;
3340 m_GameWorld.m_WorldConfig.m_IsSolo = !m_Snap.m_aCharacters[m_Snap.m_LocalClientId].m_HasExtendedData && !m_aTuning[g_Config.m_ClDummy].m_PlayerCollision && !m_aTuning[g_Config.m_ClDummy].m_PlayerHooking;
3341
3342 CCharacter *pLocalChar = m_GameWorld.GetCharacterById(Id: m_Snap.m_LocalClientId);
3343 CCharacter *pDummyChar = nullptr;
3344 if(PredictDummy())
3345 pDummyChar = m_GameWorld.GetCharacterById(Id: m_aLocalIds[!g_Config.m_ClDummy]);
3346
3347 // update strong and weak hook
3348 if(pLocalChar && !m_Snap.m_SpecInfo.m_Active && Client()->State() != IClient::STATE_DEMOPLAYBACK && (m_aTuning[g_Config.m_ClDummy].m_PlayerCollision || m_aTuning[g_Config.m_ClDummy].m_PlayerHooking))
3349 {
3350 if(m_Snap.m_aCharacters[m_Snap.m_LocalClientId].m_HasExtendedData)
3351 {
3352 int aIds[MAX_CLIENTS];
3353 for(int &Id : aIds)
3354 Id = -1;
3355 for(int i = 0; i < MAX_CLIENTS; i++)
3356 if(CCharacter *pChar = m_GameWorld.GetCharacterById(Id: i))
3357 aIds[pChar->GetStrongWeakId()] = i;
3358 for(int Id : aIds)
3359 if(Id >= 0)
3360 m_CharOrder.GiveStrong(c: Id);
3361 }
3362 else
3363 {
3364 // manual detection
3365 DetectStrongHook();
3366 }
3367 for(int i : m_CharOrder.m_Ids)
3368 {
3369 if(CCharacter *pChar = m_GameWorld.GetCharacterById(Id: i))
3370 {
3371 m_GameWorld.RemoveEntity(pEntity: pChar);
3372 m_GameWorld.InsertEntity(pEntity: pChar);
3373 }
3374 }
3375 }
3376
3377 // advance the gameworld to the current gametick
3378 if(pLocalChar && absolute(a: m_GameWorld.GameTick() - Client()->GameTick(Conn: g_Config.m_ClDummy)) < Client()->GameTickSpeed())
3379 {
3380 for(int Tick = m_GameWorld.GameTick() + 1; Tick <= Client()->GameTick(Conn: g_Config.m_ClDummy); Tick++)
3381 {
3382 CNetObj_PlayerInput *pInput = (CNetObj_PlayerInput *)Client()->GetInput(Tick);
3383 CNetObj_PlayerInput *pDummyInput = nullptr;
3384 if(pDummyChar)
3385 pDummyInput = (CNetObj_PlayerInput *)Client()->GetInput(Tick, IsDummy: 1);
3386 if(pInput)
3387 pLocalChar->OnDirectInput(pNewInput: pInput);
3388 if(pDummyInput)
3389 pDummyChar->OnDirectInput(pNewInput: pDummyInput);
3390 m_GameWorld.m_GameTick = Tick;
3391 if(pInput)
3392 pLocalChar->OnPredictedInput(pNewInput: pInput);
3393 if(pDummyInput)
3394 pDummyChar->OnPredictedInput(pNewInput: pDummyInput);
3395 m_GameWorld.Tick();
3396
3397 for(int i = 0; i < MAX_CLIENTS; i++)
3398 if(CCharacter *pChar = m_GameWorld.GetCharacterById(Id: i))
3399 {
3400 m_aClients[i].m_aPredPos[Tick % 200] = pChar->Core()->m_Pos;
3401 m_aClients[i].m_aPredTick[Tick % 200] = Tick;
3402 }
3403 }
3404 }
3405 else
3406 {
3407 // skip to current gametick
3408 m_GameWorld.m_GameTick = Client()->GameTick(Conn: g_Config.m_ClDummy);
3409 if(pLocalChar)
3410 if(CNetObj_PlayerInput *pInput = (CNetObj_PlayerInput *)Client()->GetInput(Tick: Client()->GameTick(Conn: g_Config.m_ClDummy)))
3411 pLocalChar->SetInput(pInput);
3412 if(pDummyChar)
3413 if(CNetObj_PlayerInput *pInput = (CNetObj_PlayerInput *)Client()->GetInput(Tick: Client()->GameTick(Conn: g_Config.m_ClDummy), IsDummy: 1))
3414 pDummyChar->SetInput(pInput);
3415 }
3416
3417 for(int i = 0; i < MAX_CLIENTS; i++)
3418 if(CCharacter *pChar = m_GameWorld.GetCharacterById(Id: i))
3419 {
3420 m_aClients[i].m_aPredPos[Client()->GameTick(Conn: g_Config.m_ClDummy) % 200] = pChar->Core()->m_Pos;
3421 m_aClients[i].m_aPredTick[Client()->GameTick(Conn: g_Config.m_ClDummy) % 200] = Client()->GameTick(Conn: g_Config.m_ClDummy);
3422 }
3423
3424 // update the local gameworld with the new snapshot
3425 m_GameWorld.NetObjBegin(Teams: m_Teams, LocalClientId: m_Snap.m_LocalClientId);
3426
3427 for(int i = 0; i < MAX_CLIENTS; i++)
3428 if(m_Snap.m_aCharacters[i].m_Active)
3429 {
3430 bool IsLocal = (i == m_Snap.m_LocalClientId || (PredictDummy() && i == m_aLocalIds[!g_Config.m_ClDummy]));
3431 int GameTeam = IsTeamPlay() ? m_aClients[i].m_Team : i;
3432 m_GameWorld.NetCharAdd(ObjId: i, pChar: &m_Snap.m_aCharacters[i].m_Cur,
3433 pExtended: m_Snap.m_aCharacters[i].m_HasExtendedData ? &m_Snap.m_aCharacters[i].m_ExtendedData : nullptr,
3434 GameTeam, IsLocal);
3435 }
3436
3437 for(const CSnapEntities &EntData : SnapEntities())
3438 m_GameWorld.NetObjAdd(ObjId: EntData.m_Item.m_Id, ObjType: EntData.m_Item.m_Type, pObjData: EntData.m_Item.m_pData, pDataEx: EntData.m_pDataEx);
3439
3440 m_GameWorld.NetObjEnd();
3441}
3442
3443void CGameClient::UpdateSpectatorCursor()
3444{
3445 int CursorOwnerId = m_Snap.m_LocalClientId;
3446 if(m_Snap.m_SpecInfo.m_Active)
3447 {
3448 CursorOwnerId = m_Snap.m_SpecInfo.m_SpectatorId;
3449 }
3450
3451 if(CursorOwnerId != m_CursorInfo.m_CursorOwnerId)
3452 {
3453 // reset cursor sample count upon changing spectating character
3454 m_CursorInfo.m_NumSamples = 0;
3455 m_CursorInfo.m_CursorOwnerId = CursorOwnerId;
3456 }
3457
3458 if(m_MultiViewActivated || CursorOwnerId < 0 || CursorOwnerId >= MAX_CLIENTS)
3459 {
3460 // do not show spec cursor in multi-view
3461 m_CursorInfo.m_Available = false;
3462 m_CursorInfo.m_NumSamples = 0;
3463 return;
3464 }
3465
3466 const CSnapState::CCharacterInfo &CharInfo = m_Snap.m_aCharacters[CursorOwnerId];
3467 const CClientData &CursorOwnerClient = m_aClients[CursorOwnerId];
3468 if(!CharInfo.m_HasExtendedDisplayInfo || !CursorOwnerClient.m_Active || (!g_Config.m_Debug && CursorOwnerClient.m_Paused))
3469 {
3470 // hide cursor when the spectating player is paused
3471 m_CursorInfo.m_Available = false;
3472 m_CursorInfo.m_NumSamples = 0;
3473 return;
3474 }
3475
3476 m_CursorInfo.m_Available = true;
3477 m_CursorInfo.m_Position = CursorOwnerClient.m_RenderPos;
3478 m_CursorInfo.m_Weapon = CharInfo.m_Cur.m_Weapon;
3479
3480 const vec2 Target = vec2(CharInfo.m_ExtendedData.m_TargetX, CharInfo.m_ExtendedData.m_TargetY);
3481
3482 if(Client()->State() == IClient::STATE_DEMOPLAYBACK && DemoPlayer()->BaseInfo()->m_Paused)
3483 {
3484 m_CursorInfo.m_CursorOwnerId = -1;
3485 m_CursorInfo.m_NumSamples = 0;
3486 const vec2 TargetNew = vec2(CharInfo.m_ExtendedData.m_TargetX, CharInfo.m_ExtendedData.m_TargetY);
3487 if(CharInfo.m_pPrevExtendedData)
3488 {
3489 const vec2 TargetOld = vec2(CharInfo.m_pPrevExtendedData->m_TargetX, CharInfo.m_pPrevExtendedData->m_TargetY);
3490 m_CursorInfo.m_Target = mix(a: TargetOld, b: TargetNew, amount: Client()->IntraGameTick(Conn: g_Config.m_ClDummy));
3491 }
3492 else
3493 {
3494 m_CursorInfo.m_Target = TargetNew;
3495 }
3496 }
3497 else
3498 {
3499 // interpolate cursor positions
3500 const double Tick = Client()->GameTick(Conn: g_Config.m_ClDummy);
3501
3502 const bool HasSample = m_CursorInfo.m_NumSamples > 0;
3503 const vec2 LastInput = HasSample ? m_CursorInfo.m_aTargetSamplesData[m_CursorInfo.m_NumSamples - 1] : vec2(0.0f, 0.0f);
3504 const double LastTime = HasSample ? m_CursorInfo.m_aTargetSamplesTime[m_CursorInfo.m_NumSamples - 1] : 0.0;
3505 bool NewSample = LastInput != Target || LastTime + CCursorInfo::REST_THRESHOLD < Tick;
3506
3507 if(LastTime > Tick)
3508 {
3509 // clear samples when time flows backwards
3510 m_CursorInfo.m_NumSamples = 0;
3511 NewSample = true;
3512 }
3513
3514 if(m_CursorInfo.m_NumSamples == 0)
3515 {
3516 m_CursorInfo.m_aTargetSamplesTime[0] = Tick - CCursorInfo::INTERP_DELAY;
3517 m_CursorInfo.m_aTargetSamplesData[0] = Target;
3518 }
3519
3520 if(NewSample)
3521 {
3522 if(m_CursorInfo.m_NumSamples == CCursorInfo::CURSOR_SAMPLES)
3523 {
3524 m_CursorInfo.m_NumSamples--;
3525 mem_move(dest: m_CursorInfo.m_aTargetSamplesTime, source: m_CursorInfo.m_aTargetSamplesTime + 1, size: m_CursorInfo.m_NumSamples * sizeof(double));
3526 mem_move(dest: m_CursorInfo.m_aTargetSamplesData, source: m_CursorInfo.m_aTargetSamplesData + 1, size: m_CursorInfo.m_NumSamples * sizeof(vec2));
3527 }
3528 m_CursorInfo.m_aTargetSamplesTime[m_CursorInfo.m_NumSamples] = Tick;
3529 m_CursorInfo.m_aTargetSamplesData[m_CursorInfo.m_NumSamples] = Target;
3530 m_CursorInfo.m_NumSamples++;
3531 }
3532
3533 // using double to avoid precision loss when converting int tick to decimal type
3534 const double DisplayTime = Tick - CCursorInfo::INTERP_DELAY + double(Client()->IntraGameTickSincePrev(Conn: g_Config.m_ClDummy));
3535 double aTime[CCursorInfo::SAMPLE_FRAME_WINDOW];
3536 vec2 aData[CCursorInfo::SAMPLE_FRAME_WINDOW];
3537
3538 // find the available sample timing
3539 int Index = m_CursorInfo.m_NumSamples;
3540 for(int i = 0; i < m_CursorInfo.m_NumSamples; i++)
3541 {
3542 if(m_CursorInfo.m_aTargetSamplesTime[i] > DisplayTime)
3543 {
3544 Index = i;
3545 break;
3546 }
3547 }
3548
3549 for(int i = 0; i < CCursorInfo::SAMPLE_FRAME_WINDOW; i++)
3550 {
3551 const int Offset = i - CCursorInfo::SAMPLE_FRAME_OFFSET;
3552 const int SampleIndex = Index + Offset;
3553 if(SampleIndex < 0)
3554 {
3555 aTime[i] = m_CursorInfo.m_aTargetSamplesTime[0] + CCursorInfo::REST_THRESHOLD * Offset;
3556 aData[i] = m_CursorInfo.m_aTargetSamplesData[0];
3557 }
3558 else if(SampleIndex >= m_CursorInfo.m_NumSamples)
3559 {
3560 aTime[i] = m_CursorInfo.m_aTargetSamplesTime[m_CursorInfo.m_NumSamples - 1] + CCursorInfo::REST_THRESHOLD * (Offset + 1);
3561 aData[i] = m_CursorInfo.m_aTargetSamplesData[m_CursorInfo.m_NumSamples - 1];
3562 }
3563 else
3564 {
3565 aTime[i] = m_CursorInfo.m_aTargetSamplesTime[SampleIndex];
3566 aData[i] = m_CursorInfo.m_aTargetSamplesData[SampleIndex];
3567 }
3568 }
3569
3570 m_CursorInfo.m_Target = mix_polynomial(time: aTime, data: aData, samples: CCursorInfo::SAMPLE_FRAME_WINDOW, amount: DisplayTime, init: vec2(0.0f, 0.0f));
3571 }
3572
3573 vec2 TargetCameraOffset(0, 0);
3574 float l = length(a: m_CursorInfo.m_Target);
3575
3576 if(l > 0.0001f) // make sure that this isn't 0
3577 {
3578 float OffsetAmount = maximum(a: l - m_Snap.m_SpecInfo.m_Deadzone, b: 0.0f) * (m_Snap.m_SpecInfo.m_FollowFactor / 100.0f);
3579 TargetCameraOffset = normalize(v: m_CursorInfo.m_Target) * OffsetAmount;
3580 }
3581
3582 // if we are in auto spec mode, use camera zoom to smooth out cursor transitions
3583 const float Zoom = (m_Camera.m_Zooming && m_Camera.m_AutoSpecCameraZooming) ? m_Camera.m_Zoom : m_Snap.m_SpecInfo.m_Zoom;
3584 m_CursorInfo.m_WorldTarget = m_CursorInfo.m_Position + (m_CursorInfo.m_Target - TargetCameraOffset) * Zoom + TargetCameraOffset;
3585}
3586
3587void CGameClient::UpdateRenderedCharacters()
3588{
3589 for(int i = 0; i < MAX_CLIENTS; i++)
3590 {
3591 if(!m_Snap.m_aCharacters[i].m_Active)
3592 continue;
3593 m_aClients[i].m_RenderCur = m_Snap.m_aCharacters[i].m_Cur;
3594 m_aClients[i].m_RenderPrev = m_Snap.m_aCharacters[i].m_Prev;
3595 m_aClients[i].m_IsPredicted = false;
3596 m_aClients[i].m_IsPredictedLocal = false;
3597 vec2 UnpredPos = mix(
3598 a: vec2(m_Snap.m_aCharacters[i].m_Prev.m_X, m_Snap.m_aCharacters[i].m_Prev.m_Y),
3599 b: vec2(m_Snap.m_aCharacters[i].m_Cur.m_X, m_Snap.m_aCharacters[i].m_Cur.m_Y),
3600 amount: Client()->IntraGameTick(Conn: g_Config.m_ClDummy));
3601 vec2 Pos = UnpredPos;
3602
3603 CCharacter *pChar = m_PredictedWorld.GetCharacterById(Id: i);
3604 if(Predict() && (i == m_Snap.m_LocalClientId || (AntiPingPlayers() && !IsOtherTeam(ClientId: i))) && pChar)
3605 {
3606 m_aClients[i].m_Predicted.Write(pObjCore: &m_aClients[i].m_RenderCur);
3607 m_aClients[i].m_PrevPredicted.Write(pObjCore: &m_aClients[i].m_RenderPrev);
3608
3609 m_aClients[i].m_IsPredicted = true;
3610
3611 Pos = mix(
3612 a: vec2(m_aClients[i].m_RenderPrev.m_X, m_aClients[i].m_RenderPrev.m_Y),
3613 b: vec2(m_aClients[i].m_RenderCur.m_X, m_aClients[i].m_RenderCur.m_Y),
3614 amount: m_aClients[i].m_IsPredicted ? Client()->PredIntraGameTick(Conn: g_Config.m_ClDummy) : Client()->IntraGameTick(Conn: g_Config.m_ClDummy));
3615
3616 if(i == m_Snap.m_LocalClientId)
3617 {
3618 m_aClients[i].m_IsPredictedLocal = true;
3619 if(AntiPingGunfire() && ((pChar->m_NinjaJetpack && pChar->m_FreezeTime == 0) || m_Snap.m_aCharacters[i].m_Cur.m_Weapon != WEAPON_NINJA || m_Snap.m_aCharacters[i].m_Cur.m_Weapon == m_aClients[i].m_Predicted.m_ActiveWeapon))
3620 {
3621 m_aClients[i].m_RenderCur.m_AttackTick = pChar->GetAttackTick();
3622 if(m_Snap.m_aCharacters[i].m_Cur.m_Weapon != WEAPON_NINJA && !(pChar->m_NinjaJetpack && pChar->Core()->m_ActiveWeapon == WEAPON_GUN))
3623 m_aClients[i].m_RenderCur.m_Weapon = m_aClients[i].m_Predicted.m_ActiveWeapon;
3624 }
3625 }
3626 else
3627 {
3628 // use unpredicted values for other players
3629 m_aClients[i].m_RenderPrev.m_Angle = m_Snap.m_aCharacters[i].m_Prev.m_Angle;
3630 m_aClients[i].m_RenderCur.m_Angle = m_Snap.m_aCharacters[i].m_Cur.m_Angle;
3631
3632 if(g_Config.m_ClAntiPingSmooth)
3633 Pos = GetSmoothPos(ClientId: i);
3634 }
3635 }
3636 m_aClients[i].m_RenderPos = Pos;
3637 if(Predict() && i == m_Snap.m_LocalClientId)
3638 m_LocalCharacterPos = Pos;
3639 }
3640}
3641
3642void CGameClient::DetectStrongHook()
3643{
3644 // attempt to detect strong/weak between players
3645 for(int FromPlayer = 0; FromPlayer < MAX_CLIENTS; FromPlayer++)
3646 {
3647 if(!m_Snap.m_aCharacters[FromPlayer].m_Active)
3648 continue;
3649 int ToPlayer = m_Snap.m_aCharacters[FromPlayer].m_Prev.m_HookedPlayer;
3650 if(ToPlayer < 0 || ToPlayer >= MAX_CLIENTS || !m_Snap.m_aCharacters[ToPlayer].m_Active || ToPlayer != m_Snap.m_aCharacters[FromPlayer].m_Cur.m_HookedPlayer)
3651 continue;
3652 if(absolute(a: minimum(a: m_aLastUpdateTick[ToPlayer], b: m_aLastUpdateTick[FromPlayer]) - Client()->GameTick(Conn: g_Config.m_ClDummy)) < Client()->GameTickSpeed() / 4)
3653 continue;
3654 if(m_Snap.m_aCharacters[FromPlayer].m_Prev.m_Direction != m_Snap.m_aCharacters[FromPlayer].m_Cur.m_Direction || m_Snap.m_aCharacters[ToPlayer].m_Prev.m_Direction != m_Snap.m_aCharacters[ToPlayer].m_Cur.m_Direction)
3655 continue;
3656
3657 CCharacter *pFromCharWorld = m_GameWorld.GetCharacterById(Id: FromPlayer);
3658 CCharacter *pToCharWorld = m_GameWorld.GetCharacterById(Id: ToPlayer);
3659 if(!pFromCharWorld || !pToCharWorld)
3660 continue;
3661
3662 m_aLastUpdateTick[ToPlayer] = m_aLastUpdateTick[FromPlayer] = Client()->GameTick(Conn: g_Config.m_ClDummy);
3663
3664 float aPredictErr[2];
3665 CCharacterCore ToCharCur;
3666 ToCharCur.Read(pObjCore: &m_Snap.m_aCharacters[ToPlayer].m_Cur);
3667
3668 CWorldCore World;
3669
3670 for(int Direction = 0; Direction < 2; Direction++)
3671 {
3672 CCharacterCore ToChar = pFromCharWorld->GetCore();
3673 ToChar.Init(pWorld: &World, pCollision: Collision(), pTeams: &m_Teams);
3674 World.m_apCharacters[ToPlayer] = &ToChar;
3675 ToChar.Read(pObjCore: &m_Snap.m_aCharacters[ToPlayer].m_Prev);
3676
3677 CCharacterCore FromChar = pFromCharWorld->GetCore();
3678 FromChar.Init(pWorld: &World, pCollision: Collision(), pTeams: &m_Teams);
3679 World.m_apCharacters[FromPlayer] = &FromChar;
3680 FromChar.Read(pObjCore: &m_Snap.m_aCharacters[FromPlayer].m_Prev);
3681
3682 for(int Tick = Client()->PrevGameTick(Conn: g_Config.m_ClDummy); Tick < Client()->GameTick(Conn: g_Config.m_ClDummy); Tick++)
3683 {
3684 if(Direction == 0)
3685 {
3686 FromChar.Tick(UseInput: false);
3687 ToChar.Tick(UseInput: false);
3688 }
3689 else
3690 {
3691 ToChar.Tick(UseInput: false);
3692 FromChar.Tick(UseInput: false);
3693 }
3694 FromChar.Move();
3695 FromChar.Quantize();
3696 ToChar.Move();
3697 ToChar.Quantize();
3698 }
3699 aPredictErr[Direction] = distance(a: ToChar.m_Vel, b: ToCharCur.m_Vel);
3700 }
3701 const float LOW = 0.0001f;
3702 const float HIGH = 0.07f;
3703 if(aPredictErr[1] < LOW && aPredictErr[0] > HIGH)
3704 {
3705 if(m_CharOrder.HasStrongAgainst(From: ToPlayer, To: FromPlayer))
3706 {
3707 if(ToPlayer != m_Snap.m_LocalClientId)
3708 m_CharOrder.GiveWeak(c: ToPlayer);
3709 else
3710 m_CharOrder.GiveStrong(c: FromPlayer);
3711 }
3712 }
3713 else if(aPredictErr[0] < LOW && aPredictErr[1] > HIGH)
3714 {
3715 if(m_CharOrder.HasStrongAgainst(From: FromPlayer, To: ToPlayer))
3716 {
3717 if(ToPlayer != m_Snap.m_LocalClientId)
3718 m_CharOrder.GiveStrong(c: ToPlayer);
3719 else
3720 m_CharOrder.GiveWeak(c: FromPlayer);
3721 }
3722 }
3723 }
3724}
3725
3726vec2 CGameClient::GetSmoothPos(int ClientId)
3727{
3728 vec2 Pos = mix(a: m_aClients[ClientId].m_PrevPredicted.m_Pos, b: m_aClients[ClientId].m_Predicted.m_Pos, amount: Client()->PredIntraGameTick(Conn: g_Config.m_ClDummy));
3729 int64_t Now = time_get();
3730 for(int i = 0; i < 2; i++)
3731 {
3732 int64_t Len = std::clamp(val: m_aClients[ClientId].m_aSmoothLen[i], lo: (int64_t)1, hi: time_freq());
3733 int64_t TimePassed = Now - m_aClients[ClientId].m_aSmoothStart[i];
3734 if(in_range(a: TimePassed, lower: (int64_t)0, upper: Len - 1))
3735 {
3736 float MixAmount = 1.f - std::pow(x: 1.f - TimePassed / (float)Len, y: 1.2f);
3737 int SmoothTick;
3738 float SmoothIntra;
3739 Client()->GetSmoothTick(pSmoothTick: &SmoothTick, pSmoothIntraTick: &SmoothIntra, MixAmount);
3740 if(SmoothTick > 0 && m_aClients[ClientId].m_aPredTick[(SmoothTick - 1) % 200] >= Client()->PrevGameTick(Conn: g_Config.m_ClDummy) && m_aClients[ClientId].m_aPredTick[SmoothTick % 200] <= Client()->PredGameTick(Conn: g_Config.m_ClDummy))
3741 Pos[i] = mix(a: m_aClients[ClientId].m_aPredPos[(SmoothTick - 1) % 200][i], b: m_aClients[ClientId].m_aPredPos[SmoothTick % 200][i], amount: SmoothIntra);
3742 }
3743 }
3744 return Pos;
3745}
3746
3747void CGameClient::Echo(const char *pString)
3748{
3749 m_Chat.Echo(pString);
3750}
3751
3752bool CGameClient::IsOtherTeam(int ClientId) const
3753{
3754 bool Local = m_Snap.m_LocalClientId == ClientId;
3755
3756 if(m_Snap.m_LocalClientId < 0)
3757 return false;
3758 else if((m_Snap.m_SpecInfo.m_Active && m_Snap.m_SpecInfo.m_SpectatorId == SPEC_FREEVIEW) || ClientId < 0)
3759 return false;
3760 else if(m_Snap.m_SpecInfo.m_Active && m_Snap.m_SpecInfo.m_SpectatorId != SPEC_FREEVIEW)
3761 {
3762 if(m_Teams.Team(ClientId) == TEAM_SUPER || m_Teams.Team(ClientId: m_Snap.m_SpecInfo.m_SpectatorId) == TEAM_SUPER)
3763 return false;
3764 return m_Teams.Team(ClientId) != m_Teams.Team(ClientId: m_Snap.m_SpecInfo.m_SpectatorId);
3765 }
3766 else if((m_aClients[m_Snap.m_LocalClientId].m_Solo || m_aClients[ClientId].m_Solo) && !Local)
3767 return true;
3768
3769 if(m_Teams.Team(ClientId) == TEAM_SUPER || m_Teams.Team(ClientId: m_Snap.m_LocalClientId) == TEAM_SUPER)
3770 return false;
3771
3772 return m_Teams.Team(ClientId) != m_Teams.Team(ClientId: m_Snap.m_LocalClientId);
3773}
3774
3775int CGameClient::SwitchStateTeam() const
3776{
3777 if(m_aSwitchStateTeam[g_Config.m_ClDummy] >= 0)
3778 return m_aSwitchStateTeam[g_Config.m_ClDummy];
3779 else if(m_Snap.m_LocalClientId < 0)
3780 return 0;
3781 else if(m_Snap.m_SpecInfo.m_Active && m_Snap.m_SpecInfo.m_SpectatorId != SPEC_FREEVIEW)
3782 return m_Teams.Team(ClientId: m_Snap.m_SpecInfo.m_SpectatorId);
3783 return m_Teams.Team(ClientId: m_Snap.m_LocalClientId);
3784}
3785
3786bool CGameClient::IsLocalCharSuper() const
3787{
3788 if(m_Snap.m_LocalClientId < 0)
3789 return false;
3790 return m_aClients[m_Snap.m_LocalClientId].m_Super;
3791}
3792
3793void CGameClient::LoadGameSkin(const char *pPath, bool AsDir)
3794{
3795 if(m_GameSkinLoaded)
3796 {
3797 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteHealthFull);
3798 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteHealthEmpty);
3799 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteArmorFull);
3800 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteArmorEmpty);
3801
3802 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponHammerCursor);
3803 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponGunCursor);
3804 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponShotgunCursor);
3805 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponGrenadeCursor);
3806 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponNinjaCursor);
3807 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponLaserCursor);
3808
3809 for(auto &SpriteWeaponCursor : m_GameSkin.m_aSpriteWeaponCursors)
3810 {
3811 SpriteWeaponCursor = IGraphics::CTextureHandle();
3812 }
3813
3814 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteHookChain);
3815 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteHookHead);
3816 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponHammer);
3817 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponGun);
3818 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponShotgun);
3819 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponGrenade);
3820 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponNinja);
3821 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponLaser);
3822
3823 for(auto &SpriteWeapon : m_GameSkin.m_aSpriteWeapons)
3824 {
3825 SpriteWeapon = IGraphics::CTextureHandle();
3826 }
3827
3828 for(auto &SpriteParticle : m_GameSkin.m_aSpriteParticles)
3829 {
3830 Graphics()->UnloadTexture(pIndex: &SpriteParticle);
3831 }
3832
3833 for(auto &SpriteStar : m_GameSkin.m_aSpriteStars)
3834 {
3835 Graphics()->UnloadTexture(pIndex: &SpriteStar);
3836 }
3837
3838 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponGunProjectile);
3839 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponShotgunProjectile);
3840 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponGrenadeProjectile);
3841 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponHammerProjectile);
3842 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponNinjaProjectile);
3843 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponLaserProjectile);
3844
3845 for(auto &SpriteWeaponProjectile : m_GameSkin.m_aSpriteWeaponProjectiles)
3846 {
3847 SpriteWeaponProjectile = IGraphics::CTextureHandle();
3848 }
3849
3850 for(int i = 0; i < 3; ++i)
3851 {
3852 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_aSpriteWeaponGunMuzzles[i]);
3853 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_aSpriteWeaponShotgunMuzzles[i]);
3854 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_aaSpriteWeaponNinjaMuzzles[i]);
3855
3856 for(auto &SpriteWeaponsMuzzle : m_GameSkin.m_aaSpriteWeaponsMuzzles)
3857 {
3858 SpriteWeaponsMuzzle[i] = IGraphics::CTextureHandle();
3859 }
3860 }
3861
3862 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpritePickupHealth);
3863 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpritePickupArmor);
3864 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpritePickupArmorShotgun);
3865 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpritePickupArmorGrenade);
3866 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpritePickupArmorLaser);
3867 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpritePickupArmorNinja);
3868 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpritePickupGrenade);
3869 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpritePickupShotgun);
3870 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpritePickupLaser);
3871 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpritePickupNinja);
3872 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpritePickupGun);
3873 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpritePickupHammer);
3874
3875 for(auto &SpritePickupWeapon : m_GameSkin.m_aSpritePickupWeapons)
3876 {
3877 SpritePickupWeapon = IGraphics::CTextureHandle();
3878 }
3879
3880 for(auto &SpritePickupWeaponArmor : m_GameSkin.m_aSpritePickupWeaponArmor)
3881 {
3882 SpritePickupWeaponArmor = IGraphics::CTextureHandle();
3883 }
3884
3885 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteFlagBlue);
3886 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteFlagRed);
3887
3888 if(m_GameSkin.IsSixup())
3889 {
3890 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteNinjaBarFullLeft);
3891 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteNinjaBarFull);
3892 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteNinjaBarEmpty);
3893 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteNinjaBarEmptyRight);
3894 }
3895
3896 m_GameSkinLoaded = false;
3897 }
3898
3899 char aPath[IO_MAX_PATH_LENGTH];
3900 bool IsDefault = false;
3901 if(str_comp(a: pPath, b: "default") == 0)
3902 {
3903 str_copy(dst&: aPath, src: g_pData->m_aImages[IMAGE_GAME].m_pFilename);
3904 IsDefault = true;
3905 }
3906 else
3907 {
3908 if(AsDir)
3909 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "assets/game/%s/%s", pPath, g_pData->m_aImages[IMAGE_GAME].m_pFilename);
3910 else
3911 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "assets/game/%s.png", pPath);
3912 }
3913
3914 CImageInfo ImgInfo;
3915 bool PngLoaded = Graphics()->LoadPng(Image&: ImgInfo, pFilename: aPath, StorageType: IStorage::TYPE_ALL);
3916 if(!PngLoaded && !IsDefault)
3917 {
3918 if(AsDir)
3919 LoadGameSkin(pPath: "default");
3920 else
3921 LoadGameSkin(pPath, AsDir: true);
3922 }
3923 else if(PngLoaded && Graphics()->CheckImageDivisibility(pContextName: aPath, Image&: ImgInfo, DivX: g_pData->m_aSprites[SPRITE_HEALTH_FULL].m_pSet->m_Gridx, DivY: g_pData->m_aSprites[SPRITE_HEALTH_FULL].m_pSet->m_Gridy, AllowResize: true) && Graphics()->IsImageFormatRgba(pContextName: aPath, Image: ImgInfo))
3924 {
3925 m_GameSkin.m_SpriteHealthFull = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HEALTH_FULL]);
3926 m_GameSkin.m_SpriteHealthEmpty = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HEALTH_EMPTY]);
3927 m_GameSkin.m_SpriteArmorFull = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_ARMOR_FULL]);
3928 m_GameSkin.m_SpriteArmorEmpty = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_ARMOR_EMPTY]);
3929
3930 m_GameSkin.m_SpriteWeaponHammerCursor = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_HAMMER_CURSOR]);
3931 m_GameSkin.m_SpriteWeaponGunCursor = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_GUN_CURSOR]);
3932 m_GameSkin.m_SpriteWeaponShotgunCursor = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_SHOTGUN_CURSOR]);
3933 m_GameSkin.m_SpriteWeaponGrenadeCursor = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_GRENADE_CURSOR]);
3934 m_GameSkin.m_SpriteWeaponNinjaCursor = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_NINJA_CURSOR]);
3935 m_GameSkin.m_SpriteWeaponLaserCursor = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_LASER_CURSOR]);
3936
3937 m_GameSkin.m_aSpriteWeaponCursors[0] = m_GameSkin.m_SpriteWeaponHammerCursor;
3938 m_GameSkin.m_aSpriteWeaponCursors[1] = m_GameSkin.m_SpriteWeaponGunCursor;
3939 m_GameSkin.m_aSpriteWeaponCursors[2] = m_GameSkin.m_SpriteWeaponShotgunCursor;
3940 m_GameSkin.m_aSpriteWeaponCursors[3] = m_GameSkin.m_SpriteWeaponGrenadeCursor;
3941 m_GameSkin.m_aSpriteWeaponCursors[4] = m_GameSkin.m_SpriteWeaponLaserCursor;
3942 m_GameSkin.m_aSpriteWeaponCursors[5] = m_GameSkin.m_SpriteWeaponNinjaCursor;
3943
3944 // weapons and hook
3945 m_GameSkin.m_SpriteHookChain = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HOOK_CHAIN]);
3946 m_GameSkin.m_SpriteHookHead = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HOOK_HEAD]);
3947 m_GameSkin.m_SpriteWeaponHammer = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_HAMMER_BODY]);
3948 m_GameSkin.m_SpriteWeaponGun = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_GUN_BODY]);
3949 m_GameSkin.m_SpriteWeaponShotgun = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_SHOTGUN_BODY]);
3950 m_GameSkin.m_SpriteWeaponGrenade = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_GRENADE_BODY]);
3951 m_GameSkin.m_SpriteWeaponNinja = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_NINJA_BODY]);
3952 m_GameSkin.m_SpriteWeaponLaser = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_LASER_BODY]);
3953
3954 m_GameSkin.m_aSpriteWeapons[0] = m_GameSkin.m_SpriteWeaponHammer;
3955 m_GameSkin.m_aSpriteWeapons[1] = m_GameSkin.m_SpriteWeaponGun;
3956 m_GameSkin.m_aSpriteWeapons[2] = m_GameSkin.m_SpriteWeaponShotgun;
3957 m_GameSkin.m_aSpriteWeapons[3] = m_GameSkin.m_SpriteWeaponGrenade;
3958 m_GameSkin.m_aSpriteWeapons[4] = m_GameSkin.m_SpriteWeaponLaser;
3959 m_GameSkin.m_aSpriteWeapons[5] = m_GameSkin.m_SpriteWeaponNinja;
3960
3961 // particles
3962 for(int i = 0; i < 9; ++i)
3963 {
3964 m_GameSkin.m_aSpriteParticles[i] = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PART1 + i]);
3965 }
3966
3967 // stars
3968 for(int i = 0; i < 3; ++i)
3969 {
3970 m_GameSkin.m_aSpriteStars[i] = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_STAR1 + i]);
3971 }
3972
3973 // projectiles
3974 m_GameSkin.m_SpriteWeaponGunProjectile = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_GUN_PROJ]);
3975 m_GameSkin.m_SpriteWeaponShotgunProjectile = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_SHOTGUN_PROJ]);
3976 m_GameSkin.m_SpriteWeaponGrenadeProjectile = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_GRENADE_PROJ]);
3977
3978 // these weapons have no projectiles
3979 m_GameSkin.m_SpriteWeaponHammerProjectile = IGraphics::CTextureHandle();
3980 m_GameSkin.m_SpriteWeaponNinjaProjectile = IGraphics::CTextureHandle();
3981
3982 m_GameSkin.m_SpriteWeaponLaserProjectile = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_LASER_PROJ]);
3983
3984 m_GameSkin.m_aSpriteWeaponProjectiles[0] = m_GameSkin.m_SpriteWeaponHammerProjectile;
3985 m_GameSkin.m_aSpriteWeaponProjectiles[1] = m_GameSkin.m_SpriteWeaponGunProjectile;
3986 m_GameSkin.m_aSpriteWeaponProjectiles[2] = m_GameSkin.m_SpriteWeaponShotgunProjectile;
3987 m_GameSkin.m_aSpriteWeaponProjectiles[3] = m_GameSkin.m_SpriteWeaponGrenadeProjectile;
3988 m_GameSkin.m_aSpriteWeaponProjectiles[4] = m_GameSkin.m_SpriteWeaponLaserProjectile;
3989 m_GameSkin.m_aSpriteWeaponProjectiles[5] = m_GameSkin.m_SpriteWeaponNinjaProjectile;
3990
3991 // muzzles
3992 for(int i = 0; i < 3; ++i)
3993 {
3994 m_GameSkin.m_aSpriteWeaponGunMuzzles[i] = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_GUN_MUZZLE1 + i]);
3995 m_GameSkin.m_aSpriteWeaponShotgunMuzzles[i] = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_SHOTGUN_MUZZLE1 + i]);
3996 m_GameSkin.m_aaSpriteWeaponNinjaMuzzles[i] = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_NINJA_MUZZLE1 + i]);
3997
3998 m_GameSkin.m_aaSpriteWeaponsMuzzles[1][i] = m_GameSkin.m_aSpriteWeaponGunMuzzles[i];
3999 m_GameSkin.m_aaSpriteWeaponsMuzzles[2][i] = m_GameSkin.m_aSpriteWeaponShotgunMuzzles[i];
4000 m_GameSkin.m_aaSpriteWeaponsMuzzles[5][i] = m_GameSkin.m_aaSpriteWeaponNinjaMuzzles[i];
4001 }
4002
4003 // pickups
4004 m_GameSkin.m_SpritePickupHealth = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PICKUP_HEALTH]);
4005 m_GameSkin.m_SpritePickupArmor = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PICKUP_ARMOR]);
4006 m_GameSkin.m_SpritePickupHammer = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PICKUP_HAMMER]);
4007 m_GameSkin.m_SpritePickupGun = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PICKUP_GUN]);
4008 m_GameSkin.m_SpritePickupShotgun = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PICKUP_SHOTGUN]);
4009 m_GameSkin.m_SpritePickupGrenade = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PICKUP_GRENADE]);
4010 m_GameSkin.m_SpritePickupLaser = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PICKUP_LASER]);
4011 m_GameSkin.m_SpritePickupNinja = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PICKUP_NINJA]);
4012 m_GameSkin.m_SpritePickupArmorShotgun = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PICKUP_ARMOR_SHOTGUN]);
4013 m_GameSkin.m_SpritePickupArmorGrenade = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PICKUP_ARMOR_GRENADE]);
4014 m_GameSkin.m_SpritePickupArmorNinja = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PICKUP_ARMOR_NINJA]);
4015 m_GameSkin.m_SpritePickupArmorLaser = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PICKUP_ARMOR_LASER]);
4016
4017 m_GameSkin.m_aSpritePickupWeapons[0] = m_GameSkin.m_SpritePickupHammer;
4018 m_GameSkin.m_aSpritePickupWeapons[1] = m_GameSkin.m_SpritePickupGun;
4019 m_GameSkin.m_aSpritePickupWeapons[2] = m_GameSkin.m_SpritePickupShotgun;
4020 m_GameSkin.m_aSpritePickupWeapons[3] = m_GameSkin.m_SpritePickupGrenade;
4021 m_GameSkin.m_aSpritePickupWeapons[4] = m_GameSkin.m_SpritePickupLaser;
4022 m_GameSkin.m_aSpritePickupWeapons[5] = m_GameSkin.m_SpritePickupNinja;
4023
4024 m_GameSkin.m_aSpritePickupWeaponArmor[0] = m_GameSkin.m_SpritePickupArmorShotgun;
4025 m_GameSkin.m_aSpritePickupWeaponArmor[1] = m_GameSkin.m_SpritePickupArmorGrenade;
4026 m_GameSkin.m_aSpritePickupWeaponArmor[2] = m_GameSkin.m_SpritePickupArmorNinja;
4027 m_GameSkin.m_aSpritePickupWeaponArmor[3] = m_GameSkin.m_SpritePickupArmorLaser;
4028
4029 // flags
4030 m_GameSkin.m_SpriteFlagBlue = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_FLAG_BLUE]);
4031 m_GameSkin.m_SpriteFlagRed = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_FLAG_RED]);
4032
4033 // ninja bar (0.7)
4034 if(!Graphics()->IsSpriteTextureFullyTransparent(FromImageInfo: ImgInfo, pSprite: &client_data7::g_pData->m_aSprites[client_data7::SPRITE_NINJA_BAR_FULL_LEFT]) ||
4035 !Graphics()->IsSpriteTextureFullyTransparent(FromImageInfo: ImgInfo, pSprite: &client_data7::g_pData->m_aSprites[client_data7::SPRITE_NINJA_BAR_FULL]) ||
4036 !Graphics()->IsSpriteTextureFullyTransparent(FromImageInfo: ImgInfo, pSprite: &client_data7::g_pData->m_aSprites[client_data7::SPRITE_NINJA_BAR_EMPTY]) ||
4037 !Graphics()->IsSpriteTextureFullyTransparent(FromImageInfo: ImgInfo, pSprite: &client_data7::g_pData->m_aSprites[client_data7::SPRITE_NINJA_BAR_EMPTY_RIGHT]))
4038 {
4039 m_GameSkin.m_SpriteNinjaBarFullLeft = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &client_data7::g_pData->m_aSprites[client_data7::SPRITE_NINJA_BAR_FULL_LEFT]);
4040 m_GameSkin.m_SpriteNinjaBarFull = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &client_data7::g_pData->m_aSprites[client_data7::SPRITE_NINJA_BAR_FULL]);
4041 m_GameSkin.m_SpriteNinjaBarEmpty = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &client_data7::g_pData->m_aSprites[client_data7::SPRITE_NINJA_BAR_EMPTY]);
4042 m_GameSkin.m_SpriteNinjaBarEmptyRight = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &client_data7::g_pData->m_aSprites[client_data7::SPRITE_NINJA_BAR_EMPTY_RIGHT]);
4043 }
4044
4045 m_GameSkinLoaded = true;
4046 }
4047 ImgInfo.Free();
4048}
4049
4050void CGameClient::LoadEmoticonsSkin(const char *pPath, bool AsDir)
4051{
4052 if(m_EmoticonsSkinLoaded)
4053 {
4054 for(auto &SpriteEmoticon : m_EmoticonsSkin.m_aSpriteEmoticons)
4055 Graphics()->UnloadTexture(pIndex: &SpriteEmoticon);
4056
4057 m_EmoticonsSkinLoaded = false;
4058 }
4059
4060 char aPath[IO_MAX_PATH_LENGTH];
4061 bool IsDefault = false;
4062 if(str_comp(a: pPath, b: "default") == 0)
4063 {
4064 str_copy(dst&: aPath, src: g_pData->m_aImages[IMAGE_EMOTICONS].m_pFilename);
4065 IsDefault = true;
4066 }
4067 else
4068 {
4069 if(AsDir)
4070 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "assets/emoticons/%s/%s", pPath, g_pData->m_aImages[IMAGE_EMOTICONS].m_pFilename);
4071 else
4072 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "assets/emoticons/%s.png", pPath);
4073 }
4074
4075 CImageInfo ImgInfo;
4076 bool PngLoaded = Graphics()->LoadPng(Image&: ImgInfo, pFilename: aPath, StorageType: IStorage::TYPE_ALL);
4077 if(!PngLoaded && !IsDefault)
4078 {
4079 if(AsDir)
4080 LoadEmoticonsSkin(pPath: "default");
4081 else
4082 LoadEmoticonsSkin(pPath, AsDir: true);
4083 }
4084 else if(PngLoaded && Graphics()->CheckImageDivisibility(pContextName: aPath, Image&: ImgInfo, DivX: g_pData->m_aSprites[SPRITE_OOP].m_pSet->m_Gridx, DivY: g_pData->m_aSprites[SPRITE_OOP].m_pSet->m_Gridy, AllowResize: true) && Graphics()->IsImageFormatRgba(pContextName: aPath, Image: ImgInfo))
4085 {
4086 for(int i = 0; i < 16; ++i)
4087 m_EmoticonsSkin.m_aSpriteEmoticons[i] = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_OOP + i]);
4088
4089 m_EmoticonsSkinLoaded = true;
4090 }
4091 ImgInfo.Free();
4092}
4093
4094void CGameClient::LoadParticlesSkin(const char *pPath, bool AsDir)
4095{
4096 if(m_ParticlesSkinLoaded)
4097 {
4098 Graphics()->UnloadTexture(pIndex: &m_ParticlesSkin.m_SpriteParticleSlice);
4099 Graphics()->UnloadTexture(pIndex: &m_ParticlesSkin.m_SpriteParticleBall);
4100 for(auto &SpriteParticleSplat : m_ParticlesSkin.m_aSpriteParticleSplat)
4101 Graphics()->UnloadTexture(pIndex: &SpriteParticleSplat);
4102 Graphics()->UnloadTexture(pIndex: &m_ParticlesSkin.m_SpriteParticleSmoke);
4103 Graphics()->UnloadTexture(pIndex: &m_ParticlesSkin.m_SpriteParticleShell);
4104 Graphics()->UnloadTexture(pIndex: &m_ParticlesSkin.m_SpriteParticleExpl);
4105 Graphics()->UnloadTexture(pIndex: &m_ParticlesSkin.m_SpriteParticleAirJump);
4106 Graphics()->UnloadTexture(pIndex: &m_ParticlesSkin.m_SpriteParticleHit);
4107
4108 for(auto &SpriteParticle : m_ParticlesSkin.m_aSpriteParticles)
4109 SpriteParticle = IGraphics::CTextureHandle();
4110
4111 m_ParticlesSkinLoaded = false;
4112 }
4113
4114 char aPath[IO_MAX_PATH_LENGTH];
4115 bool IsDefault = false;
4116 if(str_comp(a: pPath, b: "default") == 0)
4117 {
4118 str_copy(dst&: aPath, src: g_pData->m_aImages[IMAGE_PARTICLES].m_pFilename);
4119 IsDefault = true;
4120 }
4121 else
4122 {
4123 if(AsDir)
4124 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "assets/particles/%s/%s", pPath, g_pData->m_aImages[IMAGE_PARTICLES].m_pFilename);
4125 else
4126 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "assets/particles/%s.png", pPath);
4127 }
4128
4129 CImageInfo ImgInfo;
4130 bool PngLoaded = Graphics()->LoadPng(Image&: ImgInfo, pFilename: aPath, StorageType: IStorage::TYPE_ALL);
4131 if(!PngLoaded && !IsDefault)
4132 {
4133 if(AsDir)
4134 LoadParticlesSkin(pPath: "default");
4135 else
4136 LoadParticlesSkin(pPath, AsDir: true);
4137 }
4138 else if(PngLoaded && Graphics()->CheckImageDivisibility(pContextName: aPath, Image&: ImgInfo, DivX: g_pData->m_aSprites[SPRITE_PART_SLICE].m_pSet->m_Gridx, DivY: g_pData->m_aSprites[SPRITE_PART_SLICE].m_pSet->m_Gridy, AllowResize: true) && Graphics()->IsImageFormatRgba(pContextName: aPath, Image: ImgInfo))
4139 {
4140 m_ParticlesSkin.m_SpriteParticleSlice = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PART_SLICE]);
4141 m_ParticlesSkin.m_SpriteParticleBall = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PART_BALL]);
4142 for(int i = 0; i < 3; ++i)
4143 m_ParticlesSkin.m_aSpriteParticleSplat[i] = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PART_SPLAT01 + i]);
4144 m_ParticlesSkin.m_SpriteParticleSmoke = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PART_SMOKE]);
4145 m_ParticlesSkin.m_SpriteParticleShell = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PART_SHELL]);
4146 m_ParticlesSkin.m_SpriteParticleExpl = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PART_EXPL01]);
4147 m_ParticlesSkin.m_SpriteParticleAirJump = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PART_AIRJUMP]);
4148 m_ParticlesSkin.m_SpriteParticleHit = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PART_HIT01]);
4149
4150 m_ParticlesSkin.m_aSpriteParticles[0] = m_ParticlesSkin.m_SpriteParticleSlice;
4151 m_ParticlesSkin.m_aSpriteParticles[1] = m_ParticlesSkin.m_SpriteParticleBall;
4152 for(int i = 0; i < 3; ++i)
4153 m_ParticlesSkin.m_aSpriteParticles[2 + i] = m_ParticlesSkin.m_aSpriteParticleSplat[i];
4154 m_ParticlesSkin.m_aSpriteParticles[5] = m_ParticlesSkin.m_SpriteParticleSmoke;
4155 m_ParticlesSkin.m_aSpriteParticles[6] = m_ParticlesSkin.m_SpriteParticleShell;
4156 m_ParticlesSkin.m_aSpriteParticles[7] = m_ParticlesSkin.m_SpriteParticleExpl;
4157 m_ParticlesSkin.m_aSpriteParticles[8] = m_ParticlesSkin.m_SpriteParticleAirJump;
4158 m_ParticlesSkin.m_aSpriteParticles[9] = m_ParticlesSkin.m_SpriteParticleHit;
4159
4160 m_ParticlesSkinLoaded = true;
4161 }
4162 ImgInfo.Free();
4163}
4164
4165void CGameClient::LoadHudSkin(const char *pPath, bool AsDir)
4166{
4167 if(m_HudSkinLoaded)
4168 {
4169 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudAirjump);
4170 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudAirjumpEmpty);
4171 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudSolo);
4172 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudCollisionDisabled);
4173 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudEndlessJump);
4174 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudEndlessHook);
4175 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudJetpack);
4176 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudFreezeBarFullLeft);
4177 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudFreezeBarFull);
4178 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudFreezeBarEmpty);
4179 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudFreezeBarEmptyRight);
4180 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudNinjaBarFullLeft);
4181 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudNinjaBarFull);
4182 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudNinjaBarEmpty);
4183 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudNinjaBarEmptyRight);
4184 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudHookHitDisabled);
4185 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudHammerHitDisabled);
4186 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudShotgunHitDisabled);
4187 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudGrenadeHitDisabled);
4188 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudLaserHitDisabled);
4189 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudGunHitDisabled);
4190 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudDeepFrozen);
4191 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudLiveFrozen);
4192 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudTeleportGrenade);
4193 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudTeleportGun);
4194 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudTeleportLaser);
4195 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudPracticeMode);
4196 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudLockMode);
4197 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudTeam0Mode);
4198 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudDummyHammer);
4199 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudDummyCopy);
4200 m_HudSkinLoaded = false;
4201 }
4202
4203 char aPath[IO_MAX_PATH_LENGTH];
4204 bool IsDefault = false;
4205 if(str_comp(a: pPath, b: "default") == 0)
4206 {
4207 str_copy(dst&: aPath, src: g_pData->m_aImages[IMAGE_HUD].m_pFilename);
4208 IsDefault = true;
4209 }
4210 else
4211 {
4212 if(AsDir)
4213 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "assets/hud/%s/%s", pPath, g_pData->m_aImages[IMAGE_HUD].m_pFilename);
4214 else
4215 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "assets/hud/%s.png", pPath);
4216 }
4217
4218 CImageInfo ImgInfo;
4219 bool PngLoaded = Graphics()->LoadPng(Image&: ImgInfo, pFilename: aPath, StorageType: IStorage::TYPE_ALL);
4220 if(!PngLoaded && !IsDefault)
4221 {
4222 if(AsDir)
4223 LoadHudSkin(pPath: "default");
4224 else
4225 LoadHudSkin(pPath, AsDir: true);
4226 }
4227 else if(PngLoaded && Graphics()->CheckImageDivisibility(pContextName: aPath, Image&: ImgInfo, DivX: g_pData->m_aSprites[SPRITE_HUD_AIRJUMP].m_pSet->m_Gridx, DivY: g_pData->m_aSprites[SPRITE_HUD_AIRJUMP].m_pSet->m_Gridy, AllowResize: true) && Graphics()->IsImageFormatRgba(pContextName: aPath, Image: ImgInfo))
4228 {
4229 m_HudSkin.m_SpriteHudAirjump = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_AIRJUMP]);
4230 m_HudSkin.m_SpriteHudAirjumpEmpty = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_AIRJUMP_EMPTY]);
4231 m_HudSkin.m_SpriteHudSolo = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_SOLO]);
4232 m_HudSkin.m_SpriteHudCollisionDisabled = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_COLLISION_DISABLED]);
4233 m_HudSkin.m_SpriteHudEndlessJump = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_ENDLESS_JUMP]);
4234 m_HudSkin.m_SpriteHudEndlessHook = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_ENDLESS_HOOK]);
4235 m_HudSkin.m_SpriteHudJetpack = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_JETPACK]);
4236 m_HudSkin.m_SpriteHudFreezeBarFullLeft = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_FREEZE_BAR_FULL_LEFT]);
4237 m_HudSkin.m_SpriteHudFreezeBarFull = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_FREEZE_BAR_FULL]);
4238 m_HudSkin.m_SpriteHudFreezeBarEmpty = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_FREEZE_BAR_EMPTY]);
4239 m_HudSkin.m_SpriteHudFreezeBarEmptyRight = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_FREEZE_BAR_EMPTY_RIGHT]);
4240 m_HudSkin.m_SpriteHudNinjaBarFullLeft = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_NINJA_BAR_FULL_LEFT]);
4241 m_HudSkin.m_SpriteHudNinjaBarFull = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_NINJA_BAR_FULL]);
4242 m_HudSkin.m_SpriteHudNinjaBarEmpty = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_NINJA_BAR_EMPTY]);
4243 m_HudSkin.m_SpriteHudNinjaBarEmptyRight = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_NINJA_BAR_EMPTY_RIGHT]);
4244 m_HudSkin.m_SpriteHudHookHitDisabled = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_HOOK_HIT_DISABLED]);
4245 m_HudSkin.m_SpriteHudHammerHitDisabled = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_HAMMER_HIT_DISABLED]);
4246 m_HudSkin.m_SpriteHudShotgunHitDisabled = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_SHOTGUN_HIT_DISABLED]);
4247 m_HudSkin.m_SpriteHudGrenadeHitDisabled = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_GRENADE_HIT_DISABLED]);
4248 m_HudSkin.m_SpriteHudLaserHitDisabled = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_LASER_HIT_DISABLED]);
4249 m_HudSkin.m_SpriteHudGunHitDisabled = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_GUN_HIT_DISABLED]);
4250 m_HudSkin.m_SpriteHudDeepFrozen = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_DEEP_FROZEN]);
4251 m_HudSkin.m_SpriteHudLiveFrozen = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_LIVE_FROZEN]);
4252 m_HudSkin.m_SpriteHudTeleportGrenade = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_TELEPORT_GRENADE]);
4253 m_HudSkin.m_SpriteHudTeleportGun = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_TELEPORT_GUN]);
4254 m_HudSkin.m_SpriteHudTeleportLaser = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_TELEPORT_LASER]);
4255 m_HudSkin.m_SpriteHudPracticeMode = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_PRACTICE_MODE]);
4256 m_HudSkin.m_SpriteHudLockMode = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_LOCK_MODE]);
4257 m_HudSkin.m_SpriteHudTeam0Mode = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_TEAM0_MODE]);
4258 m_HudSkin.m_SpriteHudDummyHammer = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_DUMMY_HAMMER]);
4259 m_HudSkin.m_SpriteHudDummyCopy = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_DUMMY_COPY]);
4260
4261 m_HudSkinLoaded = true;
4262 }
4263 ImgInfo.Free();
4264}
4265
4266void CGameClient::LoadExtrasSkin(const char *pPath, bool AsDir)
4267{
4268 if(m_ExtrasSkinLoaded)
4269 {
4270 Graphics()->UnloadTexture(pIndex: &m_ExtrasSkin.m_SpriteParticleSnowflake);
4271 Graphics()->UnloadTexture(pIndex: &m_ExtrasSkin.m_SpriteParticleSparkle);
4272 Graphics()->UnloadTexture(pIndex: &m_ExtrasSkin.m_SpritePulley);
4273 Graphics()->UnloadTexture(pIndex: &m_ExtrasSkin.m_SpriteHectagon);
4274
4275 for(auto &SpriteParticle : m_ExtrasSkin.m_aSpriteParticles)
4276 SpriteParticle = IGraphics::CTextureHandle();
4277
4278 m_ExtrasSkinLoaded = false;
4279 }
4280
4281 char aPath[IO_MAX_PATH_LENGTH];
4282 bool IsDefault = false;
4283 if(str_comp(a: pPath, b: "default") == 0)
4284 {
4285 str_copy(dst&: aPath, src: g_pData->m_aImages[IMAGE_EXTRAS].m_pFilename);
4286 IsDefault = true;
4287 }
4288 else
4289 {
4290 if(AsDir)
4291 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "assets/extras/%s/%s", pPath, g_pData->m_aImages[IMAGE_EXTRAS].m_pFilename);
4292 else
4293 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "assets/extras/%s.png", pPath);
4294 }
4295
4296 CImageInfo ImgInfo;
4297 bool PngLoaded = Graphics()->LoadPng(Image&: ImgInfo, pFilename: aPath, StorageType: IStorage::TYPE_ALL);
4298 if(!PngLoaded && !IsDefault)
4299 {
4300 if(AsDir)
4301 LoadExtrasSkin(pPath: "default");
4302 else
4303 LoadExtrasSkin(pPath, AsDir: true);
4304 }
4305 else if(PngLoaded && Graphics()->CheckImageDivisibility(pContextName: aPath, Image&: ImgInfo, DivX: g_pData->m_aSprites[SPRITE_PART_SNOWFLAKE].m_pSet->m_Gridx, DivY: g_pData->m_aSprites[SPRITE_PART_SNOWFLAKE].m_pSet->m_Gridy, AllowResize: true) && Graphics()->IsImageFormatRgba(pContextName: aPath, Image: ImgInfo))
4306 {
4307 m_ExtrasSkin.m_SpriteParticleSnowflake = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PART_SNOWFLAKE]);
4308 m_ExtrasSkin.m_SpriteParticleSparkle = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PART_SPARKLE]);
4309 m_ExtrasSkin.m_SpritePulley = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PART_PULLEY]);
4310 m_ExtrasSkin.m_SpriteHectagon = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PART_HECTAGON]);
4311
4312 m_ExtrasSkin.m_aSpriteParticles[0] = m_ExtrasSkin.m_SpriteParticleSnowflake;
4313 m_ExtrasSkin.m_aSpriteParticles[1] = m_ExtrasSkin.m_SpriteParticleSparkle;
4314 m_ExtrasSkin.m_aSpriteParticles[2] = m_ExtrasSkin.m_SpritePulley;
4315 m_ExtrasSkin.m_aSpriteParticles[3] = m_ExtrasSkin.m_SpriteHectagon;
4316
4317 m_ExtrasSkinLoaded = true;
4318 }
4319 ImgInfo.Free();
4320}
4321
4322void CGameClient::RefreshSkin(const std::shared_ptr<CManagedTeeRenderInfo> &pManagedTeeRenderInfo)
4323{
4324 CTeeRenderInfo &TeeInfo = pManagedTeeRenderInfo->TeeRenderInfo();
4325 const CSkinDescriptor &SkinDescriptor = pManagedTeeRenderInfo->SkinDescriptor();
4326
4327 if(SkinDescriptor.m_Flags & CSkinDescriptor::FLAG_SIX)
4328 {
4329 TeeInfo.Apply(pSkin: m_Skins.Find(pName: SkinDescriptor.m_aSkinName));
4330 }
4331
4332 if(SkinDescriptor.m_Flags & CSkinDescriptor::FLAG_SEVEN)
4333 {
4334 for(int Dummy = 0; Dummy < NUM_DUMMIES; Dummy++)
4335 {
4336 for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++)
4337 {
4338 m_Skins7.FindSkinPart(Part, pName: SkinDescriptor.m_aSixup[Dummy].m_aaSkinPartNames[Part], AllowSpecialPart: true)->ApplyTo(SixupRenderInfo&: TeeInfo.m_aSixup[Dummy]);
4339
4340 if(SkinDescriptor.m_aSixup[Dummy].m_XmasHat)
4341 {
4342 TeeInfo.m_aSixup[Dummy].m_HatTexture = m_Skins7.XmasHatTexture();
4343 }
4344 else
4345 {
4346 TeeInfo.m_aSixup[Dummy].m_HatTexture.Invalidate();
4347 }
4348
4349 if(SkinDescriptor.m_aSixup[Dummy].m_BotDecoration)
4350 {
4351 TeeInfo.m_aSixup[Dummy].m_BotTexture = m_Skins7.BotDecorationTexture();
4352 }
4353 else
4354 {
4355 TeeInfo.m_aSixup[Dummy].m_BotTexture.Invalidate();
4356 }
4357 }
4358 }
4359 }
4360
4361 if(SkinDescriptor.m_Flags != 0 && pManagedTeeRenderInfo->m_RefreshCallback)
4362 {
4363 pManagedTeeRenderInfo->m_RefreshCallback();
4364 }
4365}
4366
4367void CGameClient::RefreshSkins(int SkinDescriptorFlags)
4368{
4369 dbg_assert(SkinDescriptorFlags != 0, "SkinDescriptorFlags invalid");
4370
4371 const auto SkinStartLoadTime = time_get_nanoseconds();
4372 const auto &&ProgressCallback = [&]() {
4373 // if skin refreshing takes to long, swap to a loading screen
4374 if(time_get_nanoseconds() - SkinStartLoadTime > 500ms)
4375 {
4376 m_Menus.RenderLoading(pCaption: Localize(pStr: "Loading skin files"), pContent: "", IncreaseCounter: 0);
4377 }
4378 };
4379 if(SkinDescriptorFlags & CSkinDescriptor::FLAG_SIX)
4380 {
4381 m_Skins.Refresh(SkinLoadedCallback: ProgressCallback);
4382 }
4383 if(SkinDescriptorFlags & CSkinDescriptor::FLAG_SEVEN)
4384 {
4385 m_Skins7.Refresh(SkinLoadedCallback: ProgressCallback);
4386 }
4387
4388 for(std::shared_ptr<CManagedTeeRenderInfo> &pManagedTeeRenderInfo : m_vpManagedTeeRenderInfos)
4389 {
4390 if(!(pManagedTeeRenderInfo->SkinDescriptor().m_Flags & SkinDescriptorFlags))
4391 {
4392 continue;
4393 }
4394 RefreshSkin(pManagedTeeRenderInfo);
4395 }
4396}
4397
4398void CGameClient::OnSkinUpdate(const char *pSkinName)
4399{
4400 // If the refreshed skin's name starts with the current skin prefix, we also have to
4401 // refresh skins matching the unprefixed skin name, e.g. if "santa_cammo" is refreshed
4402 // with prefix "santa" we need to refresh both "santa_cammo" and "cammo".
4403 const char *pSkinPrefix = m_Skins.SkinPrefix();
4404 const int SkinPrefixLength = str_length(str: pSkinPrefix);
4405 char aSkinNameWithoutPrefix[MAX_SKIN_LENGTH];
4406 if(SkinPrefixLength > 0 &&
4407 str_comp_num(a: pSkinName, b: pSkinPrefix, num: SkinPrefixLength) == 0 &&
4408 pSkinName[SkinPrefixLength] == '_' &&
4409 pSkinName[SkinPrefixLength + 1] != '\0')
4410 {
4411 str_copy(dst&: aSkinNameWithoutPrefix, src: &pSkinName[SkinPrefixLength + 1]);
4412 }
4413 else
4414 {
4415 aSkinNameWithoutPrefix[0] = '\0';
4416 }
4417 const auto &&NameMatches = [&](const char *pCheckName) {
4418 if(str_comp(a: pCheckName, b: pSkinName) == 0)
4419 {
4420 return true;
4421 }
4422 if(aSkinNameWithoutPrefix[0] != '\0' &&
4423 str_comp(a: pCheckName, b: aSkinNameWithoutPrefix) == 0)
4424 {
4425 return true;
4426 }
4427 return false;
4428 };
4429
4430 for(std::shared_ptr<CManagedTeeRenderInfo> &pManagedTeeRenderInfo : m_vpManagedTeeRenderInfos)
4431 {
4432 if(!(pManagedTeeRenderInfo->SkinDescriptor().m_Flags & CSkinDescriptor::FLAG_SIX) ||
4433 !NameMatches(pManagedTeeRenderInfo->SkinDescriptor().m_aSkinName))
4434 {
4435 continue;
4436 }
4437 RefreshSkin(pManagedTeeRenderInfo);
4438 }
4439}
4440
4441std::shared_ptr<CManagedTeeRenderInfo> CGameClient::CreateManagedTeeRenderInfo(const CTeeRenderInfo &TeeRenderInfo, const CSkinDescriptor &SkinDescriptor)
4442{
4443 std::shared_ptr<CManagedTeeRenderInfo> pManagedTeeRenderInfo = std::make_shared<CManagedTeeRenderInfo>(args: TeeRenderInfo, args: SkinDescriptor);
4444 RefreshSkin(pManagedTeeRenderInfo);
4445 m_vpManagedTeeRenderInfos.emplace_back(args&: pManagedTeeRenderInfo);
4446 return pManagedTeeRenderInfo;
4447}
4448
4449std::shared_ptr<CManagedTeeRenderInfo> CGameClient::CreateManagedTeeRenderInfo(const CClientData &Client)
4450{
4451 return CreateManagedTeeRenderInfo(TeeRenderInfo: Client.m_RenderInfo, SkinDescriptor: Client.ToSkinDescriptor());
4452}
4453
4454void CGameClient::UpdateManagedTeeRenderInfos()
4455{
4456 while(!m_vpManagedTeeRenderInfos.empty())
4457 {
4458 auto UnusedInfo = std::find_if(first: m_vpManagedTeeRenderInfos.begin(), last: m_vpManagedTeeRenderInfos.end(), pred: [&](const auto &pItem) {
4459 return pItem.use_count() <= 1;
4460 });
4461 if(UnusedInfo == m_vpManagedTeeRenderInfos.end())
4462 {
4463 break;
4464 }
4465 m_vpManagedTeeRenderInfos.erase(position: UnusedInfo);
4466 }
4467}
4468
4469void CGameClient::CollectManagedTeeRenderInfos(const std::function<void(const char *pSkinName)> &ActiveSkinAcceptor)
4470{
4471 for(const std::shared_ptr<CManagedTeeRenderInfo> &pManagedTeeRenderInfo : m_vpManagedTeeRenderInfos)
4472 {
4473 if(pManagedTeeRenderInfo->m_SkinDescriptor.m_Flags & CSkinDescriptor::FLAG_SIX)
4474 {
4475 ActiveSkinAcceptor(pManagedTeeRenderInfo->m_SkinDescriptor.m_aSkinName);
4476 }
4477 }
4478}
4479
4480void CGameClient::ConchainRefreshSkins(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
4481{
4482 CGameClient *pThis = static_cast<CGameClient *>(pUserData);
4483 pfnCallback(pResult, pCallbackUserData);
4484 if(pResult->NumArguments() && pThis->m_Menus.IsInit())
4485 {
4486 pThis->RefreshSkins(SkinDescriptorFlags: CSkinDescriptor::FLAG_SIX);
4487 }
4488}
4489
4490static bool UnknownMapSettingCallback(const char *pCommand, void *pUser)
4491{
4492 return true;
4493}
4494
4495void CGameClient::LoadMapSettings()
4496{
4497 IEngineMap *pMap = Kernel()->RequestInterface<IEngineMap>();
4498
4499 m_MapBugs = CMapBugs::Create(pName: Client()->GetCurrentMap(), Size: pMap->MapSize(), Sha256: pMap->Sha256());
4500
4501 // Reset Tunezones
4502 for(int TuneZone = 0; TuneZone < TuneZone::NUM; TuneZone++)
4503 {
4504 TuningList()[TuneZone] = CTuningParams::DEFAULT;
4505 TuningList()[TuneZone].Set(pName: "gun_curvature", Value: 0);
4506 TuningList()[TuneZone].Set(pName: "gun_speed", Value: 1400);
4507 TuningList()[TuneZone].Set(pName: "shotgun_curvature", Value: 0);
4508 TuningList()[TuneZone].Set(pName: "shotgun_speed", Value: 500);
4509 TuningList()[TuneZone].Set(pName: "shotgun_speeddiff", Value: 0);
4510 }
4511
4512 // Load map tunings
4513 int Start, Num;
4514 pMap->GetType(Type: MAPITEMTYPE_INFO, pStart: &Start, pNum: &Num);
4515 for(int i = Start; i < Start + Num; i++)
4516 {
4517 int ItemId;
4518 CMapItemInfoSettings *pItem = (CMapItemInfoSettings *)pMap->GetItem(Index: i, pType: nullptr, pId: &ItemId);
4519 int ItemSize = pMap->GetItemSize(Index: i);
4520 if(!pItem || ItemId != 0)
4521 continue;
4522
4523 if(ItemSize < (int)sizeof(CMapItemInfoSettings))
4524 break;
4525 if(!(pItem->m_Settings > -1))
4526 break;
4527
4528 int Size = pMap->GetDataSize(Index: pItem->m_Settings);
4529 char *pSettings = (char *)pMap->GetData(Index: pItem->m_Settings);
4530 char *pNext = pSettings;
4531 Console()->SetUnknownCommandCallback(pfnCallback: UnknownMapSettingCallback, pUser: nullptr);
4532 while(pNext < pSettings + Size)
4533 {
4534 int StrSize = str_length(str: pNext) + 1;
4535 Console()->ExecuteLine(pStr: pNext, ClientId: IConsole::CLIENT_ID_GAME);
4536 pNext += StrSize;
4537 }
4538 Console()->SetUnknownCommandCallback(pfnCallback: IConsole::EmptyUnknownCommandCallback, pUser: nullptr);
4539 pMap->UnloadData(Index: pItem->m_Settings);
4540 break;
4541 }
4542}
4543
4544void CGameClient::ConTuneParam(IConsole::IResult *pResult, void *pUserData)
4545{
4546 CGameClient *pSelf = (CGameClient *)pUserData;
4547 const char *pParamName = pResult->GetString(Index: 0);
4548 if(pResult->NumArguments() == 2)
4549 {
4550 float NewValue = pResult->GetFloat(Index: 1);
4551 pSelf->TuningList()[0].Set(pName: pParamName, Value: NewValue);
4552 }
4553}
4554
4555void CGameClient::ConTuneZone(IConsole::IResult *pResult, void *pUserData)
4556{
4557 CGameClient *pSelf = (CGameClient *)pUserData;
4558 int List = pResult->GetInteger(Index: 0);
4559 const char *pParamName = pResult->GetString(Index: 1);
4560 float NewValue = pResult->GetFloat(Index: 2);
4561
4562 if(List >= 0 && List < TuneZone::NUM)
4563 pSelf->TuningList()[List].Set(pName: pParamName, Value: NewValue);
4564}
4565
4566void CGameClient::ConMapbug(IConsole::IResult *pResult, void *pUserData)
4567{
4568 CGameClient *pSelf = (CGameClient *)pUserData;
4569 const char *pMapBugName = pResult->GetString(Index: 0);
4570
4571 switch(pSelf->m_MapBugs.Update(pBug: pMapBugName))
4572 {
4573 case EMapBugUpdate::OK:
4574 break;
4575 case EMapBugUpdate::OVERRIDDEN:
4576 log_debug("mapbugs", "map-internal setting overridden by database");
4577 break;
4578 case EMapBugUpdate::NOTFOUND:
4579 log_debug("mapbugs", "unknown map bug '%s', ignoring", pMapBugName);
4580 break;
4581 default:
4582 dbg_assert_failed("unreachable");
4583 }
4584}
4585
4586void CGameClient::ConchainMenuMap(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
4587{
4588 CGameClient *pSelf = (CGameClient *)pUserData;
4589 if(pResult->NumArguments())
4590 {
4591 if(str_comp(a: g_Config.m_ClMenuMap, b: pResult->GetString(Index: 0)) != 0)
4592 {
4593 str_copy(dst&: g_Config.m_ClMenuMap, src: pResult->GetString(Index: 0));
4594 pSelf->m_MenuBackground.LoadMenuBackground();
4595 }
4596 }
4597 else
4598 pfnCallback(pResult, pCallbackUserData);
4599}
4600
4601void CGameClient::DummyResetInput()
4602{
4603 if(!Client()->DummyConnected())
4604 return;
4605
4606 if((m_DummyInput.m_Fire & 1) != 0)
4607 m_DummyInput.m_Fire++;
4608
4609 m_Controls.ResetInput(Dummy: !g_Config.m_ClDummy);
4610 m_Controls.m_aInputData[!g_Config.m_ClDummy].m_Hook = 0;
4611 m_Controls.m_aInputData[!g_Config.m_ClDummy].m_Fire = m_DummyInput.m_Fire;
4612
4613 m_DummyInput = m_Controls.m_aInputData[!g_Config.m_ClDummy];
4614}
4615
4616bool CGameClient::CanDisplayWarning() const
4617{
4618 return m_Menus.CanDisplayWarning();
4619}
4620
4621CNetObjHandler *CGameClient::GetNetObjHandler()
4622{
4623 return &m_NetObjHandler;
4624}
4625
4626protocol7::CNetObjHandler *CGameClient::GetNetObjHandler7()
4627{
4628 return &m_NetObjHandler7;
4629}
4630
4631void CGameClient::SnapCollectEntities()
4632{
4633 int NumSnapItems = Client()->SnapNumItems(SnapId: IClient::SNAP_CURRENT);
4634
4635 std::vector<CSnapEntities> vItemData;
4636 std::vector<CSnapEntities> vItemEx;
4637
4638 for(int Index = 0; Index < NumSnapItems; Index++)
4639 {
4640 const IClient::CSnapItem Item = Client()->SnapGetItem(SnapId: IClient::SNAP_CURRENT, Index);
4641 if(Item.m_Type == NETOBJTYPE_ENTITYEX)
4642 vItemEx.push_back(x: {.m_Item: Item, .m_pDataEx: nullptr});
4643 else if(Item.m_Type == NETOBJTYPE_PICKUP || Item.m_Type == NETOBJTYPE_DDNETPICKUP || Item.m_Type == NETOBJTYPE_LASER || Item.m_Type == NETOBJTYPE_DDNETLASER || Item.m_Type == NETOBJTYPE_PROJECTILE || Item.m_Type == NETOBJTYPE_DDRACEPROJECTILE || Item.m_Type == NETOBJTYPE_DDNETPROJECTILE)
4644 vItemData.push_back(x: {.m_Item: Item, .m_pDataEx: nullptr});
4645 }
4646
4647 // sort by id
4648 class CEntComparer
4649 {
4650 public:
4651 bool operator()(const CSnapEntities &Lhs, const CSnapEntities &Rhs) const
4652 {
4653 return Lhs.m_Item.m_Id < Rhs.m_Item.m_Id;
4654 }
4655 };
4656
4657 std::sort(first: vItemData.begin(), last: vItemData.end(), comp: CEntComparer());
4658 std::sort(first: vItemEx.begin(), last: vItemEx.end(), comp: CEntComparer());
4659
4660 // merge extended items with items they belong to
4661 m_vSnapEntities.clear();
4662
4663 size_t IndexEx = 0;
4664 for(const CSnapEntities &Ent : vItemData)
4665 {
4666 while(IndexEx < vItemEx.size() && vItemEx[IndexEx].m_Item.m_Id < Ent.m_Item.m_Id)
4667 IndexEx++;
4668
4669 const CNetObj_EntityEx *pDataEx = nullptr;
4670 if(IndexEx < vItemEx.size() && vItemEx[IndexEx].m_Item.m_Id == Ent.m_Item.m_Id)
4671 pDataEx = (const CNetObj_EntityEx *)vItemEx[IndexEx].m_Item.m_pData;
4672
4673 m_vSnapEntities.push_back(x: {.m_Item: Ent.m_Item, .m_pDataEx: pDataEx});
4674 }
4675}
4676
4677void CGameClient::HandleMultiView()
4678{
4679 bool IsTeamZero = IsMultiViewIdSet();
4680 bool Init = false;
4681 vec2 MinPos, MaxPos;
4682 float SumVel = 0.0f;
4683 int AmountPlayers = 0;
4684
4685 for(int ClientId = 0; ClientId < MAX_CLIENTS; ClientId++)
4686 {
4687 // look at players who are vanished
4688 if(m_MultiView.m_aVanish[ClientId])
4689 {
4690 // not in freeze anymore and the delay is over
4691 if(m_MultiView.m_aLastFreeze[ClientId] + 6.0f <= Client()->LocalTime() && m_aClients[ClientId].m_FreezeEnd == 0)
4692 {
4693 m_MultiView.m_aVanish[ClientId] = false;
4694 m_MultiView.m_aLastFreeze[ClientId] = 0.0f;
4695 }
4696 }
4697
4698 // we look at team 0 and the player is not in the spec list
4699 if(IsTeamZero && !m_aMultiViewId[ClientId])
4700 continue;
4701
4702 // player is vanished
4703 if(m_MultiView.m_aVanish[ClientId])
4704 continue;
4705
4706 // the player is not in the team we are spectating
4707 if(m_Teams.Team(ClientId) != m_MultiViewTeam)
4708 continue;
4709
4710 vec2 PlayerPos;
4711 if(m_Snap.m_aCharacters[ClientId].m_Active)
4712 PlayerPos = m_aClients[ClientId].m_RenderPos;
4713 else if(m_aClients[ClientId].m_Spec) // tee is in spec
4714 PlayerPos = m_aClients[ClientId].m_SpecChar;
4715 else
4716 continue;
4717
4718 // player is far away and frozen
4719 if(distance(a: m_MultiView.m_OldPos, b: PlayerPos) > 1100 && m_aClients[ClientId].m_FreezeEnd != 0)
4720 {
4721 // check if the player is frozen for more than 3 seconds, if so vanish him
4722 if(m_MultiView.m_aLastFreeze[ClientId] == 0.0f)
4723 {
4724 m_MultiView.m_aLastFreeze[ClientId] = Client()->LocalTime();
4725 }
4726 else if(m_MultiView.m_aLastFreeze[ClientId] + 3.0f <= Client()->LocalTime())
4727 {
4728 m_MultiView.m_aVanish[ClientId] = true;
4729 // player we want to be vanished is our "main" tee, so lets switch the tee
4730 if(ClientId == m_Snap.m_SpecInfo.m_SpectatorId)
4731 m_Spectator.Spectate(SpectatorId: FindFirstMultiViewId());
4732 }
4733 }
4734 else if(m_MultiView.m_aLastFreeze[ClientId] != 0)
4735 {
4736 m_MultiView.m_aLastFreeze[ClientId] = 0;
4737 }
4738
4739 // set the minimum and maximum position
4740 if(!Init)
4741 {
4742 MinPos = PlayerPos;
4743 MaxPos = PlayerPos;
4744 Init = true;
4745 }
4746 else
4747 {
4748 MinPos.x = std::min(a: MinPos.x, b: PlayerPos.x);
4749 MaxPos.x = std::max(a: MaxPos.x, b: PlayerPos.x);
4750 MinPos.y = std::min(a: MinPos.y, b: PlayerPos.y);
4751 MaxPos.y = std::max(a: MaxPos.y, b: PlayerPos.y);
4752 }
4753
4754 // sum up the velocity of all players we are spectating
4755 const CNetObj_Character &CurrentCharacter = m_Snap.m_aCharacters[ClientId].m_Cur;
4756 SumVel += length(a: vec2(CurrentCharacter.m_VelX / 256.0f, CurrentCharacter.m_VelY / 256.0f)) * 50.0f / 32.0f;
4757 AmountPlayers++;
4758 }
4759
4760 // if we have found no players, we disable multi view
4761 if(AmountPlayers == 0)
4762 {
4763 if(m_MultiView.m_SecondChance == 0.0f)
4764 {
4765 m_MultiView.m_SecondChance = Client()->LocalTime() + 0.3f;
4766 }
4767 else if(m_MultiView.m_SecondChance < Client()->LocalTime())
4768 {
4769 ResetMultiView();
4770 return;
4771 }
4772 return;
4773 }
4774 else if(m_MultiView.m_SecondChance != 0.0f)
4775 {
4776 m_MultiView.m_SecondChance = 0.0f;
4777 }
4778
4779 // if we only have one tee that's in the list, we activate solo-mode
4780 m_MultiView.m_Solo = std::count(first: std::begin(arr&: m_aMultiViewId), last: std::end(arr&: m_aMultiViewId), value: true) == 1;
4781
4782 vec2 TargetPos = vec2((MinPos.x + MaxPos.x) / 2.0f, (MinPos.y + MaxPos.y) / 2.0f);
4783 // dont hide the position hud if its only one player
4784 m_MultiViewShowHud = AmountPlayers == 1;
4785 // get the average velocity
4786 float AvgVel = std::clamp(val: SumVel / AmountPlayers ? SumVel / (float)AmountPlayers : 0.0f, lo: 0.0f, hi: 1000.0f);
4787
4788 if(m_MultiView.m_OldPersonalZoom == m_MultiViewPersonalZoom)
4789 m_Camera.SetZoom(Target: CalculateMultiViewZoom(MinPos, MaxPos, Vel: AvgVel), Smoothness: g_Config.m_ClMultiViewZoomSmoothness, IsUser: false);
4790 else
4791 m_Camera.SetZoom(Target: CalculateMultiViewZoom(MinPos, MaxPos, Vel: AvgVel), Smoothness: 50, IsUser: false);
4792
4793 m_Snap.m_SpecInfo.m_Position = m_MultiView.m_OldPos + ((TargetPos - m_MultiView.m_OldPos) * CalculateMultiViewMultiplier(TargetPos));
4794 m_MultiView.m_OldPos = m_Snap.m_SpecInfo.m_Position;
4795 m_Snap.m_SpecInfo.m_UsePosition = true;
4796}
4797
4798bool CGameClient::InitMultiView(int Team)
4799{
4800 float Width, Height;
4801 CleanMultiViewIds();
4802 m_MultiView.m_IsInit = true;
4803
4804 // get the current view coordinates
4805 Graphics()->CalcScreenParams(Aspect: Graphics()->ScreenAspect(), Zoom: m_Camera.m_Zoom, pWidth: &Width, pHeight: &Height);
4806 vec2 AxisX = vec2(m_Camera.m_Center.x - (Width / 2.0f), m_Camera.m_Center.x + (Width / 2.0f));
4807 vec2 AxisY = vec2(m_Camera.m_Center.y - (Height / 2.0f), m_Camera.m_Center.y + (Height / 2.0f));
4808
4809 if(Team > 0)
4810 {
4811 m_MultiViewTeam = Team;
4812 for(int ClientId = 0; ClientId < MAX_CLIENTS; ClientId++)
4813 m_aMultiViewId[ClientId] = m_Teams.Team(ClientId) == Team;
4814 }
4815 else
4816 {
4817 // we want to allow spectating players in teams directly if there is no other team on screen
4818 // to do that, -1 is used temporarily for "we don't know which team to spectate yet"
4819 m_MultiViewTeam = -1;
4820
4821 int Count = 0;
4822 for(int ClientId = 0; ClientId < MAX_CLIENTS; ClientId++)
4823 {
4824 vec2 PlayerPos;
4825
4826 // get the position of the player
4827 if(m_Snap.m_aCharacters[ClientId].m_Active)
4828 PlayerPos = vec2(m_Snap.m_aCharacters[ClientId].m_Cur.m_X, m_Snap.m_aCharacters[ClientId].m_Cur.m_Y);
4829 else if(m_aClients[ClientId].m_Spec)
4830 PlayerPos = m_aClients[ClientId].m_SpecChar;
4831 else
4832 continue;
4833
4834 if(PlayerPos.x == 0 || PlayerPos.y == 0)
4835 continue;
4836
4837 // skip players that aren't in view
4838 if(PlayerPos.x <= AxisX.x || PlayerPos.x >= AxisX.y || PlayerPos.y <= AxisY.x || PlayerPos.y >= AxisY.y)
4839 continue;
4840
4841 if(m_MultiViewTeam == -1)
4842 {
4843 // use the current player's team for now, but it might switch to team 0 if any other team is found
4844 m_MultiViewTeam = m_Teams.Team(ClientId);
4845 }
4846 else if(m_MultiViewTeam != 0 && m_Teams.Team(ClientId) != m_MultiViewTeam)
4847 {
4848 // mismatched teams; remove all previously added players again and switch to team 0 instead
4849 std::fill_n(first: m_aMultiViewId, n: ClientId, value: false);
4850 m_MultiViewTeam = 0;
4851 }
4852
4853 m_aMultiViewId[ClientId] = true;
4854 Count++;
4855 }
4856
4857 // might still be -1 if not a single player was in view; fallback to team 0 in that case
4858 if(m_MultiViewTeam == -1)
4859 m_MultiViewTeam = 0;
4860
4861 // we are spectating only one player
4862 m_MultiView.m_Solo = Count == 1;
4863 }
4864
4865 if(IsMultiViewIdSet())
4866 {
4867 int SpectatorId = m_Snap.m_SpecInfo.m_SpectatorId;
4868 int NewSpectatorId = -1;
4869
4870 vec2 CurPosition(m_Camera.m_Center);
4871 if(SpectatorId != SPEC_FREEVIEW)
4872 {
4873 const CNetObj_Character &CurCharacter = m_Snap.m_aCharacters[SpectatorId].m_Cur;
4874 CurPosition.x = CurCharacter.m_X;
4875 CurPosition.y = CurCharacter.m_Y;
4876 }
4877
4878 int ClosestDistance = std::numeric_limits<int>::max();
4879 for(int ClientId = 0; ClientId < MAX_CLIENTS; ClientId++)
4880 {
4881 if(!m_Snap.m_apPlayerInfos[ClientId] || m_Snap.m_apPlayerInfos[ClientId]->m_Team == TEAM_SPECTATORS || m_Teams.Team(ClientId) != m_MultiViewTeam)
4882 continue;
4883
4884 vec2 PlayerPos;
4885 if(m_Snap.m_aCharacters[ClientId].m_Active)
4886 PlayerPos = vec2(m_aClients[ClientId].m_RenderPos.x, m_aClients[ClientId].m_RenderPos.y);
4887 else if(m_aClients[ClientId].m_Spec) // tee is in spec
4888 PlayerPos = m_aClients[ClientId].m_SpecChar;
4889 else
4890 continue;
4891
4892 int Distance = distance(a: CurPosition, b: PlayerPos);
4893 if(NewSpectatorId == -1 || Distance < ClosestDistance)
4894 {
4895 NewSpectatorId = ClientId;
4896 ClosestDistance = Distance;
4897 }
4898 }
4899
4900 if(NewSpectatorId > -1)
4901 m_Spectator.Spectate(SpectatorId: NewSpectatorId);
4902 }
4903
4904 return IsMultiViewIdSet();
4905}
4906
4907float CGameClient::CalculateMultiViewMultiplier(vec2 TargetPos)
4908{
4909 float MaxCameraDist = 200.0f;
4910 float MinCameraDist = 20.0f;
4911 float MaxVel = g_Config.m_ClMultiViewSensitivity / 150.0f;
4912 float MinVel = 0.007f;
4913 float CurrentCameraDistance = distance(a: m_MultiView.m_OldPos, b: TargetPos);
4914 float UpperLimit = 1.0f;
4915
4916 if(m_MultiView.m_Teleported && CurrentCameraDistance <= 100.0f)
4917 m_MultiView.m_Teleported = false;
4918
4919 // somebody got teleported very likely
4920 if((m_MultiView.m_Teleported || CurrentCameraDistance - m_MultiView.m_OldCameraDistance > 100.0f) && m_MultiView.m_OldCameraDistance != 0.0f)
4921 {
4922 UpperLimit = 0.1f; // dont try to compensate it by flickering
4923 m_MultiView.m_Teleported = true;
4924 }
4925 m_MultiView.m_OldCameraDistance = CurrentCameraDistance;
4926
4927 return std::clamp(val: MapValue(MaxValue: MaxCameraDist, MinValue: MinCameraDist, MaxRange: MaxVel, MinRange: MinVel, Value: CurrentCameraDistance), lo: MinVel, hi: UpperLimit);
4928}
4929
4930float CGameClient::CalculateMultiViewZoom(vec2 MinPos, vec2 MaxPos, float Vel)
4931{
4932 float Ratio = Graphics()->ScreenAspect();
4933 float ZoomX = 0.0f, ZoomY;
4934
4935 // only calc two axis if the aspect ratio is not 1:1
4936 if(Ratio != 1.0f)
4937 ZoomX = (0.001309f - 0.000328f * Ratio) * (MaxPos.x - MinPos.x) + (0.741413f - 0.032959f * Ratio);
4938
4939 // calculate the according zoom with linear function
4940 ZoomY = 0.001309f * (MaxPos.y - MinPos.y) + 0.741413f;
4941 // choose the highest zoom
4942 float Zoom = std::max(a: ZoomX, b: ZoomY);
4943 // zoom out to maximum 10 percent of the current zoom for 70 velocity
4944 float Diff = std::clamp(val: MapValue(MaxValue: 70.0f, MinValue: 15.0f, MaxRange: Zoom * 0.10f, MinRange: 0.0f, Value: Vel), lo: 0.0f, hi: Zoom * 0.10f);
4945 // zoom should stay between 1.1 and 20.0
4946 Zoom = std::clamp(val: Zoom + Diff, lo: 1.1f, hi: 20.0f);
4947 // dont go below default zoom
4948 Zoom = std::max(a: CCamera::ZoomStepsToValue(Steps: g_Config.m_ClDefaultZoom - 10), b: Zoom);
4949 // add the user preference
4950 Zoom -= Zoom * 0.1f * m_MultiViewPersonalZoom;
4951 m_MultiView.m_OldPersonalZoom = m_MultiViewPersonalZoom;
4952
4953 return Zoom;
4954}
4955
4956float CGameClient::MapValue(float MaxValue, float MinValue, float MaxRange, float MinRange, float Value)
4957{
4958 return (MaxRange - MinRange) / (MaxValue - MinValue) * (Value - MinValue) + MinRange;
4959}
4960
4961void CGameClient::ResetMultiView()
4962{
4963 m_Camera.SetZoom(Target: CCamera::ZoomStepsToValue(Steps: g_Config.m_ClDefaultZoom - 10), Smoothness: g_Config.m_ClSmoothZoomTime, IsUser: true);
4964 m_MultiViewPersonalZoom = 0.0f;
4965 m_MultiViewActivated = false;
4966 m_MultiView.m_Solo = false;
4967 m_MultiView.m_IsInit = false;
4968 m_MultiView.m_Teleported = false;
4969 m_MultiView.m_OldCameraDistance = 0.0f;
4970}
4971
4972void CGameClient::CleanMultiViewIds()
4973{
4974 std::fill(first: std::begin(arr&: m_aMultiViewId), last: std::end(arr&: m_aMultiViewId), value: false);
4975 std::fill(first: std::begin(arr&: m_MultiView.m_aLastFreeze), last: std::end(arr&: m_MultiView.m_aLastFreeze), value: 0.0f);
4976 std::fill(first: std::begin(arr&: m_MultiView.m_aVanish), last: std::end(arr&: m_MultiView.m_aVanish), value: false);
4977}
4978
4979void CGameClient::CleanMultiViewId(int ClientId)
4980{
4981 if(ClientId >= MAX_CLIENTS || ClientId < 0)
4982 return;
4983
4984 m_aMultiViewId[ClientId] = false;
4985 m_MultiView.m_aLastFreeze[ClientId] = 0.0f;
4986 m_MultiView.m_aVanish[ClientId] = false;
4987}
4988
4989bool CGameClient::IsMultiViewIdSet()
4990{
4991 return std::any_of(first: std::begin(arr&: m_aMultiViewId), last: std::end(arr&: m_aMultiViewId), pred: [](bool IsSet) { return IsSet; });
4992}
4993
4994int CGameClient::FindFirstMultiViewId()
4995{
4996 int ClientId = -1;
4997 for(int i = 0; i < MAX_CLIENTS; i++)
4998 {
4999 if(m_aMultiViewId[i] && !m_MultiView.m_aVanish[i])
5000 return i;
5001 }
5002 return ClientId;
5003}
5004
5005void CGameClient::OnSaveCodeNetMessage(const CNetMsg_Sv_SaveCode *pMsg)
5006{
5007 char aBuf[512];
5008 if(pMsg->m_pError[0] != '\0')
5009 m_Chat.AddLine(ClientId: -1, Team: TEAM_ALL, pLine: pMsg->m_pError);
5010
5011 int State = pMsg->m_State;
5012 if(State == SAVESTATE_PENDING)
5013 {
5014 if(pMsg->m_pCode[0] == '\0')
5015 {
5016 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
5017 format: Localize(pStr: "Team save in progress. You'll be able to load with '/load %s'"),
5018 pMsg->m_pGeneratedCode);
5019 }
5020 else
5021 {
5022 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
5023 format: Localize(pStr: "Team save in progress. You'll be able to load with '/load %s' if save is successful or with '/load %s' if it fails"),
5024 pMsg->m_pCode,
5025 pMsg->m_pGeneratedCode);
5026 }
5027 m_Chat.AddLine(ClientId: -1, Team: TEAM_ALL, pLine: aBuf);
5028 }
5029 else if(State == SAVESTATE_DONE)
5030 {
5031 if(pMsg->m_pServerName[0] == '\0')
5032 {
5033 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
5034 format: "Team successfully saved by %s. Use '/load %s' to continue",
5035 pMsg->m_pSaveRequester,
5036 pMsg->m_pCode[0] ? pMsg->m_pCode : pMsg->m_pGeneratedCode);
5037 }
5038 else
5039 {
5040 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
5041 format: "Team successfully saved by %s. Use '/load %s' on %s to continue",
5042 pMsg->m_pSaveRequester,
5043 pMsg->m_pCode[0] ? pMsg->m_pCode : pMsg->m_pGeneratedCode,
5044 pMsg->m_pServerName);
5045 }
5046 m_Chat.AddLine(ClientId: -1, Team: TEAM_ALL, pLine: aBuf);
5047 }
5048 else if(State == SAVESTATE_FALLBACKFILE)
5049 {
5050 if(pMsg->m_pServerName[0] == '\0')
5051 {
5052 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
5053 format: Localize(pStr: "Team successfully saved by %s. The database connection failed, using generated save code instead to avoid collisions. Use '/load %s' to continue"),
5054 pMsg->m_pSaveRequester,
5055 pMsg->m_pGeneratedCode);
5056 }
5057 else
5058 {
5059 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
5060 format: Localize(pStr: "Team successfully saved by %s. The database connection failed, using generated save code instead to avoid collisions. Use '/load %s' on %s to continue"),
5061 pMsg->m_pSaveRequester,
5062 pMsg->m_pGeneratedCode,
5063 pMsg->m_pServerName);
5064 }
5065 m_Chat.AddLine(ClientId: -1, Team: TEAM_ALL, pLine: aBuf);
5066 }
5067 else if(State == SAVESTATE_ERROR)
5068 {
5069 m_Chat.AddLine(ClientId: -1, Team: TEAM_ALL, pLine: Localize(pStr: "Save failed!"));
5070 }
5071
5072 if(State != SAVESTATE_PENDING && State != SAVESTATE_ERROR && Client()->State() != IClient::STATE_DEMOPLAYBACK)
5073 {
5074 StoreSave(pTeamMembers: pMsg->m_pTeamMembers, pGeneratedCode: pMsg->m_pCode[0] ? pMsg->m_pCode : pMsg->m_pGeneratedCode);
5075 }
5076}
5077
5078void CGameClient::StoreSave(const char *pTeamMembers, const char *pGeneratedCode) const
5079{
5080 static constexpr const char *SAVES_HEADER[] = {
5081 "Time",
5082 "Players",
5083 "Map",
5084 "Code",
5085 };
5086
5087 char aTimestamp[20];
5088 str_timestamp_format(buffer: aTimestamp, buffer_size: sizeof(aTimestamp), FORMAT_SPACE);
5089
5090 const bool SavesFileExists = Storage()->FileExists(pFilename: SAVES_FILE, Type: IStorage::TYPE_SAVE);
5091 IOHANDLE File = Storage()->OpenFile(pFilename: SAVES_FILE, Flags: IOFLAG_APPEND, Type: IStorage::TYPE_SAVE);
5092 if(!File)
5093 {
5094 log_error("saves", "Failed to open the saves file '%s'", SAVES_FILE);
5095 return;
5096 }
5097
5098 const char *apColumns[std::size(SAVES_HEADER)] = {
5099 aTimestamp,
5100 pTeamMembers,
5101 Client()->GetCurrentMap(),
5102 pGeneratedCode,
5103 };
5104
5105 if(!SavesFileExists)
5106 {
5107 CsvWrite(File, NumColumns: std::size(SAVES_HEADER), ppColumns: SAVES_HEADER);
5108 }
5109 CsvWrite(File, NumColumns: std::size(SAVES_HEADER), ppColumns: apColumns);
5110 io_close(io: File);
5111}
5112