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