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