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