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