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