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 <chrono>
5#include <limits>
6
7#include <engine/client/checksum.h>
8#include <engine/demo.h>
9#include <engine/editor.h>
10#include <engine/engine.h>
11#include <engine/favorites.h>
12#include <engine/friends.h>
13#include <engine/graphics.h>
14#include <engine/map.h>
15#include <engine/serverbrowser.h>
16#include <engine/shared/config.h>
17#include <engine/sound.h>
18#include <engine/storage.h>
19#include <engine/textrender.h>
20#include <engine/updater.h>
21
22#include <game/generated/client_data.h>
23#include <game/generated/client_data7.h>
24#include <game/generated/protocol.h>
25
26#include <base/log.h>
27#include <base/math.h>
28#include <base/system.h>
29#include <base/vmath.h>
30
31#include "gameclient.h"
32#include "lineinput.h"
33#include "race.h"
34#include "render.h"
35
36#include <game/localization.h>
37#include <game/mapitems.h>
38#include <game/version.h>
39
40#include "components/background.h"
41#include "components/binds.h"
42#include "components/broadcast.h"
43#include "components/camera.h"
44#include "components/chat.h"
45#include "components/console.h"
46#include "components/controls.h"
47#include "components/countryflags.h"
48#include "components/damageind.h"
49#include "components/debughud.h"
50#include "components/effects.h"
51#include "components/emoticon.h"
52#include "components/freezebars.h"
53#include "components/ghost.h"
54#include "components/hud.h"
55#include "components/infomessages.h"
56#include "components/items.h"
57#include "components/mapimages.h"
58#include "components/maplayers.h"
59#include "components/mapsounds.h"
60#include "components/menu_background.h"
61#include "components/menus.h"
62#include "components/motd.h"
63#include "components/nameplates.h"
64#include "components/particles.h"
65#include "components/players.h"
66#include "components/race_demo.h"
67#include "components/scoreboard.h"
68#include "components/skins.h"
69#include "components/sounds.h"
70#include "components/spectator.h"
71#include "components/statboard.h"
72#include "components/voting.h"
73#include "prediction/entities/character.h"
74#include "prediction/entities/projectile.h"
75
76using namespace std::chrono_literals;
77
78const char *CGameClient::Version() const { return GAME_VERSION; }
79const char *CGameClient::NetVersion() const { return GAME_NETVERSION; }
80int CGameClient::DDNetVersion() const { return DDNET_VERSION_NUMBER; }
81const char *CGameClient::DDNetVersionStr() const { return m_aDDNetVersionStr; }
82const char *CGameClient::GetItemName(int Type) const { return m_NetObjHandler.GetObjName(Type); }
83
84void CGameClient::OnConsoleInit()
85{
86 m_pEngine = Kernel()->RequestInterface<IEngine>();
87 m_pClient = Kernel()->RequestInterface<IClient>();
88 m_pTextRender = Kernel()->RequestInterface<ITextRender>();
89 m_pSound = Kernel()->RequestInterface<ISound>();
90 m_pConfigManager = Kernel()->RequestInterface<IConfigManager>();
91 m_pConfig = m_pConfigManager->Values();
92 m_pInput = Kernel()->RequestInterface<IInput>();
93 m_pConsole = Kernel()->RequestInterface<IConsole>();
94 m_pStorage = Kernel()->RequestInterface<IStorage>();
95 m_pDemoPlayer = Kernel()->RequestInterface<IDemoPlayer>();
96 m_pServerBrowser = Kernel()->RequestInterface<IServerBrowser>();
97 m_pEditor = Kernel()->RequestInterface<IEditor>();
98 m_pFavorites = Kernel()->RequestInterface<IFavorites>();
99 m_pFriends = Kernel()->RequestInterface<IFriends>();
100 m_pFoes = Client()->Foes();
101#if defined(CONF_AUTOUPDATE)
102 m_pUpdater = Kernel()->RequestInterface<IUpdater>();
103#endif
104 m_pHttp = Kernel()->RequestInterface<IHttp>();
105
106 // make a list of all the systems, make sure to add them in the correct render order
107 m_vpAll.insert(position: m_vpAll.end(), l: {&m_Skins,
108 &m_CountryFlags,
109 &m_MapImages,
110 &m_Effects, // doesn't render anything, just updates effects
111 &m_Binds,
112 &m_Binds.m_SpecialBinds,
113 &m_Controls,
114 &m_Camera,
115 &m_Sounds,
116 &m_Voting,
117 &m_Particles, // doesn't render anything, just updates all the particles
118 &m_RaceDemo,
119 &m_MapSounds,
120 &m_Background, // render instead of m_MapLayersBackground when g_Config.m_ClOverlayEntities == 100
121 &m_MapLayersBackground, // first to render
122 &m_Particles.m_RenderTrail,
123 &m_Items,
124 &m_Ghost,
125 &m_Players,
126 &m_MapLayersForeground,
127 &m_Particles.m_RenderExplosions,
128 &m_NamePlates,
129 &m_Particles.m_RenderExtra,
130 &m_Particles.m_RenderGeneral,
131 &m_FreezeBars,
132 &m_DamageInd,
133 &m_Hud,
134 &m_Spectator,
135 &m_Emoticon,
136 &m_InfoMessages,
137 &m_Chat,
138 &m_Broadcast,
139 &m_DebugHud,
140 &m_Scoreboard,
141 &m_Statboard,
142 &m_Motd,
143 &m_Menus,
144 &m_Tooltips,
145 &CMenus::m_Binder,
146 &m_GameConsole,
147 &m_MenuBackground});
148
149 // build the input stack
150 m_vpInput.insert(position: m_vpInput.end(), l: {&CMenus::m_Binder, // this will take over all input when we want to bind a key
151 &m_Binds.m_SpecialBinds,
152 &m_GameConsole,
153 &m_Chat, // chat has higher prio, due to that you can quit it by pressing esc
154 &m_Motd, // for pressing esc to remove it
155 &m_Menus,
156 &m_Spectator,
157 &m_Emoticon,
158 &m_Controls,
159 &m_Binds});
160
161 // add basic console commands
162 Console()->Register(pName: "team", pParams: "i[team-id]", Flags: CFGFLAG_CLIENT, pfnFunc: ConTeam, pUser: this, pHelp: "Switch team");
163 Console()->Register(pName: "kill", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConKill, pUser: this, pHelp: "Kill yourself to restart");
164
165 // register tune zone command to allow the client prediction to load tunezones from the map
166 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");
167
168 for(auto &pComponent : m_vpAll)
169 pComponent->m_pClient = this;
170
171 // let all the other components register their console commands
172 for(auto &pComponent : m_vpAll)
173 pComponent->OnConsoleInit();
174
175 Console()->Chain(pName: "cl_languagefile", pfnChainFunc: ConchainLanguageUpdate, pUser: this);
176
177 Console()->Chain(pName: "player_name", pfnChainFunc: ConchainSpecialInfoupdate, pUser: this);
178 Console()->Chain(pName: "player_clan", pfnChainFunc: ConchainSpecialInfoupdate, pUser: this);
179 Console()->Chain(pName: "player_country", pfnChainFunc: ConchainSpecialInfoupdate, pUser: this);
180 Console()->Chain(pName: "player_use_custom_color", pfnChainFunc: ConchainSpecialInfoupdate, pUser: this);
181 Console()->Chain(pName: "player_color_body", pfnChainFunc: ConchainSpecialInfoupdate, pUser: this);
182 Console()->Chain(pName: "player_color_feet", pfnChainFunc: ConchainSpecialInfoupdate, pUser: this);
183 Console()->Chain(pName: "player_skin", pfnChainFunc: ConchainSpecialInfoupdate, pUser: this);
184
185 Console()->Chain(pName: "dummy_name", pfnChainFunc: ConchainSpecialDummyInfoupdate, pUser: this);
186 Console()->Chain(pName: "dummy_clan", pfnChainFunc: ConchainSpecialDummyInfoupdate, pUser: this);
187 Console()->Chain(pName: "dummy_country", pfnChainFunc: ConchainSpecialDummyInfoupdate, pUser: this);
188 Console()->Chain(pName: "dummy_use_custom_color", pfnChainFunc: ConchainSpecialDummyInfoupdate, pUser: this);
189 Console()->Chain(pName: "dummy_color_body", pfnChainFunc: ConchainSpecialDummyInfoupdate, pUser: this);
190 Console()->Chain(pName: "dummy_color_feet", pfnChainFunc: ConchainSpecialDummyInfoupdate, pUser: this);
191 Console()->Chain(pName: "dummy_skin", pfnChainFunc: ConchainSpecialDummyInfoupdate, pUser: this);
192
193 Console()->Chain(pName: "cl_skin_download_url", pfnChainFunc: ConchainRefreshSkins, pUser: this);
194 Console()->Chain(pName: "cl_skin_community_download_url", pfnChainFunc: ConchainRefreshSkins, pUser: this);
195 Console()->Chain(pName: "cl_download_skins", pfnChainFunc: ConchainRefreshSkins, pUser: this);
196 Console()->Chain(pName: "cl_download_community_skins", pfnChainFunc: ConchainRefreshSkins, pUser: this);
197 Console()->Chain(pName: "cl_vanilla_skins_only", pfnChainFunc: ConchainRefreshSkins, pUser: this);
198
199 Console()->Chain(pName: "cl_dummy", pfnChainFunc: ConchainSpecialDummy, pUser: this);
200 Console()->Chain(pName: "cl_text_entities_size", pfnChainFunc: ConchainClTextEntitiesSize, pUser: this);
201
202 Console()->Chain(pName: "cl_menu_map", pfnChainFunc: ConchainMenuMap, pUser: this);
203}
204
205static void GenerateTimeoutCode(char *pTimeoutCode)
206{
207 if(pTimeoutCode[0] == '\0' || str_comp(a: pTimeoutCode, b: "hGuEYnfxicsXGwFq") == 0)
208 {
209 for(unsigned int i = 0; i < 16; i++)
210 {
211 if(rand() % 2)
212 pTimeoutCode[i] = (char)((rand() % ('z' - 'a' + 1)) + 'a');
213 else
214 pTimeoutCode[i] = (char)((rand() % ('Z' - 'A' + 1)) + 'A');
215 }
216 }
217}
218
219void CGameClient::OnInit()
220{
221 const int64_t OnInitStart = time_get();
222
223 Client()->SetLoadingCallback([this](IClient::ELoadingCallbackDetail Detail) {
224 const char *pTitle;
225 if(Detail == IClient::LOADING_CALLBACK_DETAIL_DEMO || DemoPlayer()->IsPlaying())
226 {
227 pTitle = Localize(pStr: "Preparing demo playback");
228 }
229 else
230 {
231 pTitle = Localize(pStr: "Connected");
232 }
233
234 const char *pMessage;
235 switch(Detail)
236 {
237 case IClient::LOADING_CALLBACK_DETAIL_MAP:
238 pMessage = Localize(pStr: "Loading map file from storage");
239 break;
240 case IClient::LOADING_CALLBACK_DETAIL_DEMO:
241 pMessage = Localize(pStr: "Loading demo file from storage");
242 break;
243 default:
244 dbg_assert(false, "Invalid callback loading detail");
245 dbg_break();
246 }
247 m_Menus.RenderLoading(pCaption: pTitle, pContent: pMessage, IncreaseCounter: 0, RenderLoadingBar: false);
248 });
249
250 m_pGraphics = Kernel()->RequestInterface<IGraphics>();
251
252 // propagate pointers
253 m_UI.Init(pKernel: Kernel());
254 m_RenderTools.Init(pGraphics: Graphics(), pTextRender: TextRender());
255
256 if(GIT_SHORTREV_HASH)
257 {
258 str_format(buffer: m_aDDNetVersionStr, buffer_size: sizeof(m_aDDNetVersionStr), format: "%s %s (%s)", GAME_NAME, GAME_RELEASE_VERSION, GIT_SHORTREV_HASH);
259 }
260 else
261 {
262 str_format(buffer: m_aDDNetVersionStr, buffer_size: sizeof(m_aDDNetVersionStr), format: "%s %s", GAME_NAME, GAME_RELEASE_VERSION);
263 }
264
265 // set the language
266 g_Localization.LoadIndexfile(pStorage: Storage(), pConsole: Console());
267 if(g_Config.m_ClShowWelcome)
268 g_Localization.SelectDefaultLanguage(pConsole: Console(), pFilename: g_Config.m_ClLanguagefile, Length: sizeof(g_Config.m_ClLanguagefile));
269 g_Localization.Load(pFilename: g_Config.m_ClLanguagefile, pStorage: Storage(), pConsole: Console());
270
271 // TODO: this should be different
272 // setup item sizes
273 for(int i = 0; i < NUM_NETOBJTYPES; i++)
274 Client()->SnapSetStaticsize(ItemType: i, Size: m_NetObjHandler.GetObjSize(Type: i));
275
276 TextRender()->LoadFonts();
277 TextRender()->SetFontLanguageVariant(g_Config.m_ClLanguagefile);
278
279 // update and swap after font loading, they are quite huge
280 Client()->UpdateAndSwap();
281
282 const char *pLoadingDDNetCaption = Localize(pStr: "Loading DDNet Client");
283 const char *pLoadingMessageComponents = Localize(pStr: "Initializing components");
284 const char *pLoadingMessageComponentsSpecial = Localize(pStr: "Why are you slowmo replaying to read this?");
285 char aLoadingMessage[256];
286
287 // init all components
288 int SkippedComps = 1;
289 int CompCounter = 1;
290 const int NumComponents = ComponentCount();
291 for(int i = NumComponents - 1; i >= 0; --i)
292 {
293 m_vpAll[i]->OnInit();
294 // try to render a frame after each component, also flushes GPU uploads
295 if(m_Menus.IsInit())
296 {
297 str_format(buffer: aLoadingMessage, buffer_size: std::size(aLoadingMessage), format: "%s [%d/%d]", CompCounter == NumComponents ? pLoadingMessageComponentsSpecial : pLoadingMessageComponents, CompCounter, NumComponents);
298 m_Menus.RenderLoading(pCaption: pLoadingDDNetCaption, pContent: aLoadingMessage, IncreaseCounter: SkippedComps);
299 SkippedComps = 1;
300 }
301 else
302 {
303 ++SkippedComps;
304 }
305 ++CompCounter;
306 }
307
308 m_GameSkinLoaded = false;
309 m_ParticlesSkinLoaded = false;
310 m_EmoticonsSkinLoaded = false;
311 m_HudSkinLoaded = false;
312
313 // setup load amount, load textures
314 const char *pLoadingMessageAssets = Localize(pStr: "Initializing assets");
315 for(int i = 0; i < g_pData->m_NumImages; i++)
316 {
317 if(i == IMAGE_GAME)
318 LoadGameSkin(pPath: g_Config.m_ClAssetGame);
319 else if(i == IMAGE_EMOTICONS)
320 LoadEmoticonsSkin(pPath: g_Config.m_ClAssetEmoticons);
321 else if(i == IMAGE_PARTICLES)
322 LoadParticlesSkin(pPath: g_Config.m_ClAssetParticles);
323 else if(i == IMAGE_HUD)
324 LoadHudSkin(pPath: g_Config.m_ClAssetHud);
325 else if(i == IMAGE_EXTRAS)
326 LoadExtrasSkin(pPath: g_Config.m_ClAssetExtras);
327 else if(g_pData->m_aImages[i].m_pFilename[0] == '\0') // handle special null image without filename
328 g_pData->m_aImages[i].m_Id = IGraphics::CTextureHandle();
329 else
330 g_pData->m_aImages[i].m_Id = Graphics()->LoadTexture(pFilename: g_pData->m_aImages[i].m_pFilename, StorageType: IStorage::TYPE_ALL);
331 m_Menus.RenderLoading(pCaption: pLoadingDDNetCaption, pContent: pLoadingMessageAssets, IncreaseCounter: 1);
332 }
333
334 m_GameWorld.m_pCollision = Collision();
335 m_GameWorld.m_pTuningList = m_aTuningList;
336 OnReset();
337
338 // Set free binds to DDRace binds if it's active
339 m_Binds.SetDDRaceBinds(true);
340
341 GenerateTimeoutCode(pTimeoutCode: g_Config.m_ClTimeoutCode);
342 GenerateTimeoutCode(pTimeoutCode: g_Config.m_ClDummyTimeoutCode);
343
344 m_MapImages.SetTextureScale(g_Config.m_ClTextEntitiesSize);
345
346 // Aggressively try to grab window again since some Windows users report
347 // window not being focused after starting client.
348 Graphics()->SetWindowGrab(true);
349
350 CChecksumData *pChecksum = Client()->ChecksumData();
351 pChecksum->m_SizeofGameClient = sizeof(*this);
352 pChecksum->m_NumComponents = m_vpAll.size();
353 for(size_t i = 0; i < m_vpAll.size(); i++)
354 {
355 if(i >= std::size(pChecksum->m_aComponentsChecksum))
356 {
357 break;
358 }
359 int Size = m_vpAll[i]->Sizeof();
360 pChecksum->m_aComponentsChecksum[i] = Size;
361 }
362
363 log_trace("gameclient", "initialization finished after %.2fms", (time_get() - OnInitStart) * 1000.0f / (float)time_freq());
364}
365
366void CGameClient::OnUpdate()
367{
368 HandleLanguageChanged();
369
370 CUIElementBase::Init(pUI: Ui()); // update static pointer because game and editor use separate UI
371
372 // handle mouse movement
373 float x = 0.0f, y = 0.0f;
374 IInput::ECursorType CursorType = Input()->CursorRelative(pX: &x, pY: &y);
375 if(CursorType != IInput::CURSOR_NONE)
376 {
377 for(auto &pComponent : m_vpInput)
378 {
379 if(pComponent->OnCursorMove(x, y, CursorType))
380 break;
381 }
382 }
383
384 // handle key presses
385 Input()->ConsumeEvents(Consumer: [&](const IInput::CEvent &Event) {
386 for(auto &pComponent : m_vpInput)
387 {
388 if(pComponent->OnInput(Event))
389 break;
390 }
391 });
392
393 if(g_Config.m_ClSubTickAiming && m_Binds.m_MouseOnAction)
394 {
395 m_Controls.m_aMousePosOnAction[g_Config.m_ClDummy] = m_Controls.m_aMousePos[g_Config.m_ClDummy];
396 m_Binds.m_MouseOnAction = false;
397 }
398}
399
400void CGameClient::OnDummySwap()
401{
402 if(g_Config.m_ClDummyResetOnSwitch)
403 {
404 int PlayerOrDummy = (g_Config.m_ClDummyResetOnSwitch == 2) ? g_Config.m_ClDummy : (!g_Config.m_ClDummy);
405 m_Controls.ResetInput(Dummy: PlayerOrDummy);
406 m_Controls.m_aInputData[PlayerOrDummy].m_Hook = 0;
407 }
408 int tmp = m_DummyInput.m_Fire;
409 m_DummyInput = m_Controls.m_aInputData[!g_Config.m_ClDummy];
410 m_Controls.m_aInputData[g_Config.m_ClDummy].m_Fire = tmp;
411 m_IsDummySwapping = 1;
412}
413
414int CGameClient::OnSnapInput(int *pData, bool Dummy, bool Force)
415{
416 if(!Dummy)
417 {
418 return m_Controls.SnapInput(pData);
419 }
420
421 if(!g_Config.m_ClDummyHammer)
422 {
423 if(m_DummyFire != 0)
424 {
425 m_DummyInput.m_Fire = (m_HammerInput.m_Fire + 1) & ~1;
426 m_DummyFire = 0;
427 }
428
429 if(!Force && (!m_DummyInput.m_Direction && !m_DummyInput.m_Jump && !m_DummyInput.m_Hook))
430 {
431 return 0;
432 }
433
434 mem_copy(dest: pData, source: &m_DummyInput, size: sizeof(m_DummyInput));
435 return sizeof(m_DummyInput);
436 }
437 else
438 {
439 if(m_DummyFire % 25 != 0)
440 {
441 m_DummyFire++;
442 return 0;
443 }
444 m_DummyFire++;
445
446 m_HammerInput.m_Fire = (m_HammerInput.m_Fire + 1) | 1;
447 m_HammerInput.m_WantedWeapon = WEAPON_HAMMER + 1;
448 if(!g_Config.m_ClDummyRestoreWeapon)
449 {
450 m_DummyInput.m_WantedWeapon = WEAPON_HAMMER + 1;
451 }
452
453 vec2 MainPos = m_LocalCharacterPos;
454 vec2 DummyPos = m_aClients[m_aLocalIds[!g_Config.m_ClDummy]].m_Predicted.m_Pos;
455 vec2 Dir = MainPos - DummyPos;
456 m_HammerInput.m_TargetX = (int)(Dir.x);
457 m_HammerInput.m_TargetY = (int)(Dir.y);
458
459 mem_copy(dest: pData, source: &m_HammerInput, size: sizeof(m_HammerInput));
460 return sizeof(m_HammerInput);
461 }
462}
463
464void CGameClient::OnConnected()
465{
466 const char *pConnectCaption = DemoPlayer()->IsPlaying() ? Localize(pStr: "Preparing demo playback") : Localize(pStr: "Connected");
467 const char *pLoadMapContent = Localize(pStr: "Initializing map logic");
468 // render loading before skip is calculated
469 m_Menus.RenderLoading(pCaption: pConnectCaption, pContent: pLoadMapContent, IncreaseCounter: 0, RenderLoadingBar: false);
470 m_Layers.Init(pKernel: Kernel());
471 m_Collision.Init(pLayers: Layers());
472 m_GameWorld.m_Core.InitSwitchers(HighestSwitchNumber: m_Collision.m_HighestSwitchNumber);
473
474 CRaceHelper::ms_aFlagIndex[0] = -1;
475 CRaceHelper::ms_aFlagIndex[1] = -1;
476
477 CTile *pGameTiles = static_cast<CTile *>(Layers()->Map()->GetData(Index: Layers()->GameLayer()->m_Data));
478
479 // get flag positions
480 for(int i = 0; i < m_Collision.GetWidth() * m_Collision.GetHeight(); i++)
481 {
482 if(pGameTiles[i].m_Index - ENTITY_OFFSET == ENTITY_FLAGSTAND_RED)
483 CRaceHelper::ms_aFlagIndex[TEAM_RED] = i;
484 else if(pGameTiles[i].m_Index - ENTITY_OFFSET == ENTITY_FLAGSTAND_BLUE)
485 CRaceHelper::ms_aFlagIndex[TEAM_BLUE] = i;
486 i += pGameTiles[i].m_Skip;
487 }
488
489 // render loading before going through all components
490 m_Menus.RenderLoading(pCaption: pConnectCaption, pContent: pLoadMapContent, IncreaseCounter: 0, RenderLoadingBar: false);
491 for(auto &pComponent : m_vpAll)
492 {
493 pComponent->OnMapLoad();
494 pComponent->OnReset();
495 }
496
497 Client()->SetLoadingStateDetail(IClient::LOADING_STATE_DETAIL_GETTING_READY);
498 m_Menus.RenderLoading(pCaption: pConnectCaption, pContent: Localize(pStr: "Sending initial client info"), IncreaseCounter: 0, RenderLoadingBar: false);
499
500 // send the initial info
501 SendInfo(Start: true);
502 // we should keep this in for now, because otherwise you can't spectate
503 // people at start as the other info 64 packet is only sent after the first
504 // snap
505 Client()->Rcon(pLine: "crashmeplx");
506
507 ConfigManager()->ResetGameSettings();
508 LoadMapSettings();
509
510 if(Client()->State() != IClient::STATE_DEMOPLAYBACK && g_Config.m_ClAutoDemoOnConnect)
511 Client()->DemoRecorder_HandleAutoStart();
512}
513
514void CGameClient::OnReset()
515{
516 InvalidateSnapshot();
517
518 m_EditorMovementDelay = 5;
519
520 m_PredictedTick = -1;
521 std::fill(first: std::begin(arr&: m_aLastNewPredictedTick), last: std::end(arr&: m_aLastNewPredictedTick), value: -1);
522
523 m_LastRoundStartTick = -1;
524 m_LastFlagCarrierRed = -4;
525 m_LastFlagCarrierBlue = -4;
526
527 std::fill(first: std::begin(arr&: m_aCheckInfo), last: std::end(arr&: m_aCheckInfo), value: -1);
528
529 // m_aDDNetVersionStr is initialized once in OnInit
530
531 std::fill(first: std::begin(arr&: m_aLastPos), last: std::end(arr&: m_aLastPos), value: vec2(0.0f, 0.0f));
532 std::fill(first: std::begin(arr&: m_aLastActive), last: std::end(arr&: m_aLastActive), value: false);
533
534 m_GameOver = false;
535 m_GamePaused = false;
536 m_PrevLocalId = -1;
537
538 m_SuppressEvents = false;
539 m_NewTick = false;
540 m_NewPredictedTick = false;
541
542 m_aFlagDropTick[TEAM_RED] = 0;
543 m_aFlagDropTick[TEAM_BLUE] = 0;
544
545 m_ServerMode = SERVERMODE_PURE;
546 mem_zero(block: &m_GameInfo, size: sizeof(m_GameInfo));
547
548 m_DemoSpecId = SPEC_FOLLOW;
549 m_LocalCharacterPos = vec2(0.0f, 0.0f);
550
551 m_PredictedPrevChar.Reset();
552 m_PredictedChar.Reset();
553
554 // m_Snap was cleared in InvalidateSnapshot
555
556 std::fill(first: std::begin(arr&: m_aLocalTuneZone), last: std::end(arr&: m_aLocalTuneZone), value: 0);
557 std::fill(first: std::begin(arr&: m_aReceivedTuning), last: std::end(arr&: m_aReceivedTuning), value: false);
558 std::fill(first: std::begin(arr&: m_aExpectingTuningForZone), last: std::end(arr&: m_aExpectingTuningForZone), value: -1);
559 std::fill(first: std::begin(arr&: m_aExpectingTuningSince), last: std::end(arr&: m_aExpectingTuningSince), value: 0);
560 std::fill(first: std::begin(arr&: m_aTuning), last: std::end(arr&: m_aTuning), value: CTuningParams());
561
562 for(auto &Client : m_aClients)
563 Client.Reset();
564
565 for(auto &Stats : m_aStats)
566 Stats.Reset();
567
568 m_NextChangeInfo = 0;
569 std::fill(first: std::begin(arr&: m_aLocalIds), last: std::end(arr&: m_aLocalIds), value: -1);
570 m_DummyInput = {};
571 m_HammerInput = {};
572 m_DummyFire = 0;
573 m_ReceivedDDNetPlayer = false;
574
575 m_Teams.Reset();
576 m_GameWorld.Clear();
577 m_GameWorld.m_WorldConfig.m_InfiniteAmmo = true;
578 m_PredictedWorld.CopyWorld(pFrom: &m_GameWorld);
579 m_PrevPredictedWorld.CopyWorld(pFrom: &m_PredictedWorld);
580
581 m_vSnapEntities.clear();
582
583 std::fill(first: std::begin(arr&: m_aDDRaceMsgSent), last: std::end(arr&: m_aDDRaceMsgSent), value: false);
584 std::fill(first: std::begin(arr&: m_aShowOthers), last: std::end(arr&: m_aShowOthers), value: SHOW_OTHERS_NOT_SET);
585 std::fill(first: std::begin(arr&: m_aLastUpdateTick), last: std::end(arr&: m_aLastUpdateTick), value: 0);
586
587 m_PredictedDummyId = -1;
588 m_IsDummySwapping = false;
589 m_CharOrder.Reset();
590 std::fill(first: std::begin(arr&: m_aSwitchStateTeam), last: std::end(arr&: m_aSwitchStateTeam), value: -1);
591
592 // m_aTuningList is reset in LoadMapSettings
593
594 m_LastZoom = 0.0f;
595 m_LastScreenAspect = 0.0f;
596 m_LastDummyConnected = false;
597
598 m_MultiViewPersonalZoom = 0;
599 m_MultiViewActivated = false;
600 m_MultiView.m_IsInit = false;
601
602 for(auto &pComponent : m_vpAll)
603 pComponent->OnReset();
604
605 Editor()->ResetMentions();
606 Editor()->ResetIngameMoved();
607
608 Collision()->Unload();
609 Layers()->Unload();
610}
611
612void CGameClient::UpdatePositions()
613{
614 // local character position
615 if(g_Config.m_ClPredict && Client()->State() != IClient::STATE_DEMOPLAYBACK)
616 {
617 if(!AntiPingPlayers())
618 {
619 if(!m_Snap.m_pLocalCharacter || (m_Snap.m_pGameInfoObj && m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER))
620 {
621 // don't use predicted
622 }
623 else
624 m_LocalCharacterPos = mix(a: m_PredictedPrevChar.m_Pos, b: m_PredictedChar.m_Pos, amount: Client()->PredIntraGameTick(Conn: g_Config.m_ClDummy));
625 }
626 else
627 {
628 if(!(m_Snap.m_pGameInfoObj && m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER))
629 {
630 if(m_Snap.m_pLocalCharacter)
631 m_LocalCharacterPos = mix(a: m_PredictedPrevChar.m_Pos, b: m_PredictedChar.m_Pos, amount: Client()->PredIntraGameTick(Conn: g_Config.m_ClDummy));
632 }
633 // else
634 // m_LocalCharacterPos = mix(m_PredictedPrevChar.m_Pos, m_PredictedChar.m_Pos, Client()->PredIntraGameTick(g_Config.m_ClDummy));
635 }
636 }
637 else if(m_Snap.m_pLocalCharacter && m_Snap.m_pLocalPrevCharacter)
638 {
639 m_LocalCharacterPos = mix(
640 a: vec2(m_Snap.m_pLocalPrevCharacter->m_X, m_Snap.m_pLocalPrevCharacter->m_Y),
641 b: vec2(m_Snap.m_pLocalCharacter->m_X, m_Snap.m_pLocalCharacter->m_Y), amount: Client()->IntraGameTick(Conn: g_Config.m_ClDummy));
642 }
643
644 // spectator position
645 if(m_Snap.m_SpecInfo.m_Active)
646 {
647 if(m_MultiViewActivated)
648 {
649 HandleMultiView();
650 }
651 else if(Client()->State() == IClient::STATE_DEMOPLAYBACK && m_DemoSpecId != SPEC_FOLLOW && m_Snap.m_SpecInfo.m_SpectatorId != SPEC_FREEVIEW)
652 {
653 m_Snap.m_SpecInfo.m_Position = mix(
654 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),
655 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),
656 amount: Client()->IntraGameTick(Conn: g_Config.m_ClDummy));
657 m_Snap.m_SpecInfo.m_UsePosition = true;
658 }
659 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)))
660 {
661 if(m_Snap.m_pPrevSpectatorInfo && m_Snap.m_pPrevSpectatorInfo->m_SpectatorId == m_Snap.m_pSpectatorInfo->m_SpectatorId)
662 m_Snap.m_SpecInfo.m_Position = mix(a: vec2(m_Snap.m_pPrevSpectatorInfo->m_X, m_Snap.m_pPrevSpectatorInfo->m_Y),
663 b: vec2(m_Snap.m_pSpectatorInfo->m_X, m_Snap.m_pSpectatorInfo->m_Y), amount: Client()->IntraGameTick(Conn: g_Config.m_ClDummy));
664 else
665 m_Snap.m_SpecInfo.m_Position = vec2(m_Snap.m_pSpectatorInfo->m_X, m_Snap.m_pSpectatorInfo->m_Y);
666 m_Snap.m_SpecInfo.m_UsePosition = true;
667 }
668 }
669
670 if(!m_MultiViewActivated && m_MultiView.m_IsInit)
671 ResetMultiView();
672
673 UpdateRenderedCharacters();
674}
675
676void CGameClient::OnRender()
677{
678 // check if multi view got activated
679 if(!m_MultiView.m_IsInit && m_MultiViewActivated)
680 {
681 int TeamId = 0;
682 if(m_Snap.m_SpecInfo.m_SpectatorId >= 0)
683 TeamId = m_Teams.Team(ClientId: m_Snap.m_SpecInfo.m_SpectatorId);
684
685 if(TeamId > MAX_CLIENTS || TeamId < 0)
686 TeamId = 0;
687
688 if(!InitMultiView(Team: TeamId))
689 {
690 dbg_msg(sys: "MultiView", fmt: "No players found to spectate");
691 ResetMultiView();
692 }
693 }
694
695 // update the local character and spectate position
696 UpdatePositions();
697
698 // display gfx & client warnings
699 for(SWarning *pWarning : {Graphics()->GetCurWarning(), Client()->GetCurWarning()})
700 {
701 if(pWarning != nullptr && m_Menus.CanDisplayWarning())
702 {
703 m_Menus.PopupWarning(pTopic: pWarning->m_aWarningTitle[0] == '\0' ? Localize(pStr: "Warning") : pWarning->m_aWarningTitle, pBody: pWarning->m_aWarningMsg, pButton: Localize(pStr: "Ok"), Duration: pWarning->m_AutoHide ? 10s : 0s);
704 pWarning->m_WasShown = true;
705 }
706 }
707
708 // render all systems
709 for(auto &pComponent : m_vpAll)
710 pComponent->OnRender();
711
712 // clear all events/input for this frame
713 Input()->Clear();
714
715 CLineInput::RenderCandidates();
716
717 // clear new tick flags
718 m_NewTick = false;
719 m_NewPredictedTick = false;
720
721 if(g_Config.m_ClDummy && !Client()->DummyConnected())
722 g_Config.m_ClDummy = 0;
723
724 // resend player and dummy info if it was filtered by server
725 if(Client()->State() == IClient::STATE_ONLINE && !m_Menus.IsActive())
726 {
727 if(m_aCheckInfo[0] == 0)
728 {
729 if(
730 str_comp(a: m_aClients[m_aLocalIds[0]].m_aName, b: Client()->PlayerName()) ||
731 str_comp(a: m_aClients[m_aLocalIds[0]].m_aClan, b: g_Config.m_PlayerClan) ||
732 m_aClients[m_aLocalIds[0]].m_Country != g_Config.m_PlayerCountry ||
733 str_comp(a: m_aClients[m_aLocalIds[0]].m_aSkinName, b: g_Config.m_ClPlayerSkin) ||
734 m_aClients[m_aLocalIds[0]].m_UseCustomColor != g_Config.m_ClPlayerUseCustomColor ||
735 m_aClients[m_aLocalIds[0]].m_ColorBody != (int)g_Config.m_ClPlayerColorBody ||
736 m_aClients[m_aLocalIds[0]].m_ColorFeet != (int)g_Config.m_ClPlayerColorFeet)
737 SendInfo(Start: false);
738 else
739 m_aCheckInfo[0] = -1;
740 }
741
742 if(m_aCheckInfo[0] > 0)
743 m_aCheckInfo[0]--;
744
745 if(Client()->DummyConnected())
746 {
747 if(m_aCheckInfo[1] == 0)
748 {
749 if(
750 str_comp(a: m_aClients[m_aLocalIds[1]].m_aName, b: Client()->DummyName()) ||
751 str_comp(a: m_aClients[m_aLocalIds[1]].m_aClan, b: g_Config.m_ClDummyClan) ||
752 m_aClients[m_aLocalIds[1]].m_Country != g_Config.m_ClDummyCountry ||
753 str_comp(a: m_aClients[m_aLocalIds[1]].m_aSkinName, b: g_Config.m_ClDummySkin) ||
754 m_aClients[m_aLocalIds[1]].m_UseCustomColor != g_Config.m_ClDummyUseCustomColor ||
755 m_aClients[m_aLocalIds[1]].m_ColorBody != (int)g_Config.m_ClDummyColorBody ||
756 m_aClients[m_aLocalIds[1]].m_ColorFeet != (int)g_Config.m_ClDummyColorFeet)
757 SendDummyInfo(Start: false);
758 else
759 m_aCheckInfo[1] = -1;
760 }
761
762 if(m_aCheckInfo[1] > 0)
763 m_aCheckInfo[1]--;
764 }
765 }
766}
767
768void CGameClient::OnDummyDisconnect()
769{
770 m_aDDRaceMsgSent[1] = false;
771 m_aShowOthers[1] = SHOW_OTHERS_NOT_SET;
772 m_aLastNewPredictedTick[1] = -1;
773 m_PredictedDummyId = -1;
774}
775
776int CGameClient::GetLastRaceTick() const
777{
778 return m_Ghost.GetLastRaceTick();
779}
780
781bool CGameClient::Predict() const
782{
783 if(!g_Config.m_ClPredict)
784 return false;
785
786 if(m_Snap.m_pGameInfoObj)
787 {
788 if(m_Snap.m_pGameInfoObj->m_GameStateFlags & (GAMESTATEFLAG_GAMEOVER | GAMESTATEFLAG_PAUSED))
789 {
790 return false;
791 }
792 }
793
794 if(Client()->State() == IClient::STATE_DEMOPLAYBACK)
795 return false;
796
797 return !m_Snap.m_SpecInfo.m_Active && m_Snap.m_pLocalCharacter;
798}
799
800ColorRGBA CGameClient::GetDDTeamColor(int DDTeam, float Lightness) const
801{
802 // Use golden angle to generate unique colors with distinct adjacent colors.
803 // The first DDTeam (team 1) gets angle 0°, i.e. red hue.
804 const float Hue = std::fmod(x: (DDTeam - 1) * (137.50776f / 360.0f), y: 1.0f);
805 return color_cast<ColorRGBA>(hsl: ColorHSLA(Hue, 1.0f, Lightness));
806}
807
808void CGameClient::OnRelease()
809{
810 // release all systems
811 for(auto &pComponent : m_vpAll)
812 pComponent->OnRelease();
813}
814
815void CGameClient::OnMessage(int MsgId, CUnpacker *pUnpacker, int Conn, bool Dummy)
816{
817 // special messages
818 if(MsgId == NETMSGTYPE_SV_TUNEPARAMS)
819 {
820 // unpack the new tuning
821 CTuningParams NewTuning;
822 int *pParams = (int *)&NewTuning;
823 // No jetpack on DDNet incompatible servers:
824 NewTuning.m_JetpackStrength = 0;
825 for(unsigned i = 0; i < sizeof(CTuningParams) / sizeof(int); i++)
826 {
827 int value = pUnpacker->GetInt();
828
829 // check for unpacking errors
830 if(pUnpacker->Error())
831 break;
832
833 pParams[i] = value;
834 }
835
836 m_ServerMode = SERVERMODE_PURE;
837
838 m_aReceivedTuning[Conn] = true;
839 // apply new tuning
840 m_aTuning[Conn] = NewTuning;
841 return;
842 }
843
844 void *pRawMsg = m_NetObjHandler.SecureUnpackMsg(Type: MsgId, pUnpacker);
845 if(!pRawMsg)
846 {
847 char aBuf[256];
848 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());
849 Console()->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "client", pStr: aBuf);
850 return;
851 }
852
853 if(Dummy)
854 {
855 if(MsgId == NETMSGTYPE_SV_CHAT && m_aLocalIds[0] >= 0 && m_aLocalIds[1] >= 0)
856 {
857 CNetMsg_Sv_Chat *pMsg = (CNetMsg_Sv_Chat *)pRawMsg;
858
859 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)
860 {
861 m_Chat.OnMessage(MsgType: MsgId, pRawMsg);
862 }
863 }
864 return; // no need of all that stuff for the dummy
865 }
866
867 // TODO: this should be done smarter
868 for(auto &pComponent : m_vpAll)
869 pComponent->OnMessage(Msg: MsgId, pRawMsg);
870
871 if(MsgId == NETMSGTYPE_SV_READYTOENTER)
872 {
873 Client()->EnterGame(Conn);
874 }
875 else if(MsgId == NETMSGTYPE_SV_EMOTICON)
876 {
877 CNetMsg_Sv_Emoticon *pMsg = (CNetMsg_Sv_Emoticon *)pRawMsg;
878
879 // apply
880 m_aClients[pMsg->m_ClientId].m_Emoticon = pMsg->m_Emoticon;
881 m_aClients[pMsg->m_ClientId].m_EmoticonStartTick = Client()->GameTick(Conn);
882 m_aClients[pMsg->m_ClientId].m_EmoticonStartFraction = Client()->IntraGameTickSincePrev(Conn);
883 }
884 else if(MsgId == NETMSGTYPE_SV_SOUNDGLOBAL)
885 {
886 if(m_SuppressEvents)
887 return;
888
889 // don't enqueue pseudo-global sounds from demos (created by PlayAndRecord)
890 CNetMsg_Sv_SoundGlobal *pMsg = (CNetMsg_Sv_SoundGlobal *)pRawMsg;
891 if(pMsg->m_SoundId == SOUND_CTF_DROP || pMsg->m_SoundId == SOUND_CTF_RETURN ||
892 pMsg->m_SoundId == SOUND_CTF_CAPTURE || pMsg->m_SoundId == SOUND_CTF_GRAB_EN ||
893 pMsg->m_SoundId == SOUND_CTF_GRAB_PL)
894 {
895 if(g_Config.m_SndGame)
896 m_Sounds.Enqueue(Channel: CSounds::CHN_GLOBAL, SetId: pMsg->m_SoundId);
897 }
898 else
899 {
900 if(g_Config.m_SndGame)
901 m_Sounds.Play(Channel: CSounds::CHN_GLOBAL, SetId: pMsg->m_SoundId, Vol: 1.0f);
902 }
903 }
904 else if(MsgId == NETMSGTYPE_SV_TEAMSSTATE || MsgId == NETMSGTYPE_SV_TEAMSSTATELEGACY)
905 {
906 unsigned int i;
907
908 for(i = 0; i < MAX_CLIENTS; i++)
909 {
910 const int Team = pUnpacker->GetInt();
911 if(!pUnpacker->Error() && Team >= TEAM_FLOCK && Team <= TEAM_SUPER)
912 m_Teams.Team(ClientId: i, Team);
913 else
914 {
915 m_Teams.Team(ClientId: i, Team: 0);
916 break;
917 }
918 }
919
920 if(i <= 16)
921 m_Teams.m_IsDDRace16 = true;
922
923 m_Ghost.m_AllowRestart = true;
924 m_RaceDemo.m_AllowRestart = true;
925 }
926 else if(MsgId == NETMSGTYPE_SV_KILLMSG)
927 {
928 CNetMsg_Sv_KillMsg *pMsg = (CNetMsg_Sv_KillMsg *)pRawMsg;
929 // reset character prediction
930 if(!(m_GameWorld.m_WorldConfig.m_IsFNG && pMsg->m_Weapon == WEAPON_LASER))
931 {
932 m_CharOrder.GiveWeak(c: pMsg->m_Victim);
933 if(CCharacter *pChar = m_GameWorld.GetCharacterById(Id: pMsg->m_Victim))
934 pChar->ResetPrediction();
935 m_GameWorld.ReleaseHooked(ClientId: pMsg->m_Victim);
936 }
937
938 // if we are spectating a static id set (team 0) and somebody killed, and its not a guy in solo, we remove him from the list
939 // never remove players from the list if it is a pvp server
940 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)
941 {
942 m_aMultiViewId[pMsg->m_Victim] = false;
943
944 // if everyone of a team killed, we have no ids to spectate anymore, so we disable multi view
945 if(!IsMultiViewIdSet())
946 ResetMultiView();
947 else
948 {
949 // the "main" tee killed, search a new one
950 if(m_Snap.m_SpecInfo.m_SpectatorId == pMsg->m_Victim)
951 {
952 int NewClientId = FindFirstMultiViewId();
953 if(NewClientId < MAX_CLIENTS && NewClientId >= 0)
954 {
955 CleanMultiViewId(ClientId: NewClientId);
956 m_aMultiViewId[NewClientId] = true;
957 m_Spectator.Spectate(SpectatorId: NewClientId);
958 }
959 }
960 }
961 }
962 }
963 else if(MsgId == NETMSGTYPE_SV_KILLMSGTEAM)
964 {
965 CNetMsg_Sv_KillMsgTeam *pMsg = (CNetMsg_Sv_KillMsgTeam *)pRawMsg;
966
967 // reset prediction
968 std::vector<std::pair<int, int>> vStrongWeakSorted;
969 for(int i = 0; i < MAX_CLIENTS; i++)
970 {
971 if(m_Teams.Team(ClientId: i) == pMsg->m_Team)
972 {
973 if(CCharacter *pChar = m_GameWorld.GetCharacterById(Id: i))
974 {
975 pChar->ResetPrediction();
976 vStrongWeakSorted.emplace_back(args&: i, args: pMsg->m_First == i ? MAX_CLIENTS : pChar ? pChar->GetStrongWeakId() : 0);
977 }
978 m_GameWorld.ReleaseHooked(ClientId: i);
979 }
980 }
981 std::stable_sort(first: vStrongWeakSorted.begin(), last: vStrongWeakSorted.end(), comp: [](auto &Left, auto &Right) { return Left.second > Right.second; });
982 for(auto Id : vStrongWeakSorted)
983 {
984 m_CharOrder.GiveWeak(c: Id.first);
985 }
986 }
987 else if(MsgId == NETMSGTYPE_SV_CHANGEINFOCOOLDOWN)
988 {
989 CNetMsg_Sv_ChangeInfoCooldown *pMsg = (CNetMsg_Sv_ChangeInfoCooldown *)pRawMsg;
990 m_NextChangeInfo = pMsg->m_WaitUntil;
991 }
992}
993
994void CGameClient::OnStateChange(int NewState, int OldState)
995{
996 // reset everything when not already connected (to keep gathered stuff)
997 if(NewState < IClient::STATE_ONLINE)
998 OnReset();
999
1000 // then change the state
1001 for(auto &pComponent : m_vpAll)
1002 pComponent->OnStateChange(NewState, OldState);
1003}
1004
1005void CGameClient::OnShutdown()
1006{
1007 for(auto &pComponent : m_vpAll)
1008 pComponent->OnShutdown();
1009}
1010
1011void CGameClient::OnEnterGame()
1012{
1013 m_Effects.ResetDamageIndicator();
1014}
1015
1016void CGameClient::OnGameOver()
1017{
1018 if(Client()->State() != IClient::STATE_DEMOPLAYBACK && g_Config.m_ClEditor == 0)
1019 Client()->AutoScreenshot_Start();
1020}
1021
1022void CGameClient::OnStartGame()
1023{
1024 if(Client()->State() != IClient::STATE_DEMOPLAYBACK && !g_Config.m_ClAutoDemoOnConnect)
1025 Client()->DemoRecorder_HandleAutoStart();
1026 m_Statboard.OnReset();
1027}
1028
1029void CGameClient::OnStartRound()
1030{
1031 // In GamePaused or GameOver state RoundStartTick is updated on each tick
1032 // hence no need to reset stats until player leaves GameOver
1033 // and it would be a mistake to reset stats after or during the pause
1034 m_Statboard.OnReset();
1035
1036 // Restart automatic race demo recording
1037 m_RaceDemo.OnReset();
1038}
1039
1040void CGameClient::OnFlagGrab(int TeamId)
1041{
1042 if(TeamId == TEAM_RED)
1043 m_aStats[m_Snap.m_pGameDataObj->m_FlagCarrierRed].m_FlagGrabs++;
1044 else
1045 m_aStats[m_Snap.m_pGameDataObj->m_FlagCarrierBlue].m_FlagGrabs++;
1046}
1047
1048void CGameClient::OnWindowResize()
1049{
1050 for(auto &pComponent : m_vpAll)
1051 pComponent->OnWindowResize();
1052
1053 Ui()->OnWindowResize();
1054}
1055
1056void CGameClient::OnLanguageChange()
1057{
1058 // The actual language change is delayed because it
1059 // might require clearing the text render font atlas,
1060 // which would invalidate text that is currently drawn.
1061 m_LanguageChanged = true;
1062}
1063
1064void CGameClient::HandleLanguageChanged()
1065{
1066 if(!m_LanguageChanged)
1067 return;
1068 m_LanguageChanged = false;
1069
1070 g_Localization.Load(pFilename: g_Config.m_ClLanguagefile, pStorage: Storage(), pConsole: Console());
1071 TextRender()->SetFontLanguageVariant(g_Config.m_ClLanguagefile);
1072
1073 // Clear all text containers
1074 Client()->OnWindowResize();
1075}
1076
1077void CGameClient::RenderShutdownMessage()
1078{
1079 const char *pMessage = nullptr;
1080 if(Client()->State() == IClient::STATE_QUITTING)
1081 pMessage = Localize(pStr: "Quitting. Please wait…");
1082 else if(Client()->State() == IClient::STATE_RESTARTING)
1083 pMessage = Localize(pStr: "Restarting. Please wait…");
1084 else
1085 dbg_assert(false, "Invalid client state for quitting message");
1086
1087 // This function only gets called after the render loop has already terminated, so we have to call Swap manually.
1088 Graphics()->Clear(r: 0.0f, g: 0.0f, b: 0.0f);
1089 Ui()->MapScreen();
1090 TextRender()->TextColor(rgb: TextRender()->DefaultTextColor());
1091 Ui()->DoLabel(pRect: Ui()->Screen(), pText: pMessage, Size: 16.0f, Align: TEXTALIGN_MC);
1092 Graphics()->Swap();
1093 Graphics()->Clear(r: 0.0f, g: 0.0f, b: 0.0f);
1094}
1095
1096void CGameClient::OnRconType(bool UsernameReq)
1097{
1098 m_GameConsole.RequireUsername(UsernameReq);
1099}
1100
1101void CGameClient::OnRconLine(const char *pLine)
1102{
1103 m_GameConsole.PrintLine(Type: CGameConsole::CONSOLETYPE_REMOTE, pLine);
1104}
1105
1106void CGameClient::ProcessEvents()
1107{
1108 if(m_SuppressEvents)
1109 return;
1110
1111 int SnapType = IClient::SNAP_CURRENT;
1112 int Num = Client()->SnapNumItems(SnapId: SnapType);
1113 for(int Index = 0; Index < Num; Index++)
1114 {
1115 IClient::CSnapItem Item;
1116 const void *pData = Client()->SnapGetItem(SnapId: SnapType, Index, pItem: &Item);
1117
1118 // We don't have enough info about us, others, to know a correct alpha value.
1119 float Alpha = 1.0f;
1120
1121 if(Item.m_Type == NETEVENTTYPE_DAMAGEIND)
1122 {
1123 CNetEvent_DamageInd *pEvent = (CNetEvent_DamageInd *)pData;
1124 m_Effects.DamageIndicator(Pos: vec2(pEvent->m_X, pEvent->m_Y), Dir: direction(angle: pEvent->m_Angle / 256.0f), Alpha);
1125 }
1126 else if(Item.m_Type == NETEVENTTYPE_EXPLOSION)
1127 {
1128 CNetEvent_Explosion *pEvent = (CNetEvent_Explosion *)pData;
1129 m_Effects.Explosion(Pos: vec2(pEvent->m_X, pEvent->m_Y), Alpha);
1130 }
1131 else if(Item.m_Type == NETEVENTTYPE_HAMMERHIT)
1132 {
1133 CNetEvent_HammerHit *pEvent = (CNetEvent_HammerHit *)pData;
1134 m_Effects.HammerHit(Pos: vec2(pEvent->m_X, pEvent->m_Y), Alpha);
1135 }
1136 else if(Item.m_Type == NETEVENTTYPE_FINISH)
1137 {
1138 CNetEvent_Finish *pEvent = (CNetEvent_Finish *)pData;
1139 m_Effects.FinishConfetti(Pos: vec2(pEvent->m_X, pEvent->m_Y), Alpha);
1140 }
1141 else if(Item.m_Type == NETEVENTTYPE_SPAWN)
1142 {
1143 CNetEvent_Spawn *pEvent = (CNetEvent_Spawn *)pData;
1144 m_Effects.PlayerSpawn(Pos: vec2(pEvent->m_X, pEvent->m_Y), Alpha);
1145 }
1146 else if(Item.m_Type == NETEVENTTYPE_DEATH)
1147 {
1148 CNetEvent_Death *pEvent = (CNetEvent_Death *)pData;
1149 m_Effects.PlayerDeath(Pos: vec2(pEvent->m_X, pEvent->m_Y), ClientId: pEvent->m_ClientId, Alpha);
1150 }
1151 else if(Item.m_Type == NETEVENTTYPE_SOUNDWORLD)
1152 {
1153 CNetEvent_SoundWorld *pEvent = (CNetEvent_SoundWorld *)pData;
1154 if(!Config()->m_SndGame)
1155 continue;
1156
1157 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)))
1158 continue;
1159
1160 m_Sounds.PlayAt(Channel: CSounds::CHN_WORLD, SetId: pEvent->m_SoundId, Vol: 1.0f, Pos: vec2(pEvent->m_X, pEvent->m_Y));
1161 }
1162 }
1163}
1164
1165static CGameInfo GetGameInfo(const CNetObj_GameInfoEx *pInfoEx, int InfoExSize, CServerInfo *pFallbackServerInfo)
1166{
1167 int Version = -1;
1168 if(InfoExSize >= 12)
1169 {
1170 Version = pInfoEx->m_Version;
1171 }
1172 else if(InfoExSize >= 8)
1173 {
1174 Version = minimum(a: pInfoEx->m_Version, b: 4);
1175 }
1176 else if(InfoExSize >= 4)
1177 {
1178 Version = 0;
1179 }
1180 int Flags = 0;
1181 if(Version >= 0)
1182 {
1183 Flags = pInfoEx->m_Flags;
1184 }
1185 int Flags2 = 0;
1186 if(Version >= 5)
1187 {
1188 Flags2 = pInfoEx->m_Flags2;
1189 }
1190 bool Race;
1191 bool FastCap;
1192 bool FNG;
1193 bool DDRace;
1194 bool DDNet;
1195 bool BlockWorlds;
1196 bool City;
1197 bool Vanilla;
1198 bool Plus;
1199 bool FDDrace;
1200 if(Version < 1)
1201 {
1202 const char *pGameType = pFallbackServerInfo->m_aGameType;
1203 Race = str_find_nocase(haystack: pGameType, needle: "race") || str_find_nocase(haystack: pGameType, needle: "fastcap");
1204 FastCap = str_find_nocase(haystack: pGameType, needle: "fastcap");
1205 FNG = str_find_nocase(haystack: pGameType, needle: "fng");
1206 DDRace = str_find_nocase(haystack: pGameType, needle: "ddrace") || str_find_nocase(haystack: pGameType, needle: "mkrace");
1207 DDNet = str_find_nocase(haystack: pGameType, needle: "ddracenet") || str_find_nocase(haystack: pGameType, needle: "ddnet");
1208 BlockWorlds = str_startswith(str: pGameType, prefix: "bw ") || str_comp_nocase(a: pGameType, b: "bw") == 0;
1209 City = str_find_nocase(haystack: pGameType, needle: "city");
1210 Vanilla = str_comp(a: pGameType, b: "DM") == 0 || str_comp(a: pGameType, b: "TDM") == 0 || str_comp(a: pGameType, b: "CTF") == 0;
1211 Plus = str_find(haystack: pGameType, needle: "+");
1212 FDDrace = false;
1213 }
1214 else
1215 {
1216 Race = Flags & GAMEINFOFLAG_GAMETYPE_RACE;
1217 FastCap = Flags & GAMEINFOFLAG_GAMETYPE_FASTCAP;
1218 FNG = Flags & GAMEINFOFLAG_GAMETYPE_FNG;
1219 DDRace = Flags & GAMEINFOFLAG_GAMETYPE_DDRACE;
1220 DDNet = Flags & GAMEINFOFLAG_GAMETYPE_DDNET;
1221 BlockWorlds = Flags & GAMEINFOFLAG_GAMETYPE_BLOCK_WORLDS;
1222 Vanilla = Flags & GAMEINFOFLAG_GAMETYPE_VANILLA;
1223 Plus = Flags & GAMEINFOFLAG_GAMETYPE_PLUS;
1224 City = Version >= 5 && Flags2 & GAMEINFOFLAG2_GAMETYPE_CITY;
1225 FDDrace = Version >= 6 && Flags2 & GAMEINFOFLAG2_GAMETYPE_FDDRACE;
1226
1227 // Ensure invariants upheld by the server info parsing business.
1228 DDRace = DDRace || DDNet || FDDrace;
1229 Race = Race || FastCap || DDRace;
1230 }
1231
1232 CGameInfo Info;
1233 Info.m_FlagStartsRace = FastCap;
1234 Info.m_TimeScore = Race;
1235 Info.m_UnlimitedAmmo = Race;
1236 Info.m_DDRaceRecordMessage = DDRace && !DDNet;
1237 Info.m_RaceRecordMessage = DDNet || (Race && !DDRace);
1238 Info.m_RaceSounds = DDRace || FNG || BlockWorlds;
1239 Info.m_AllowEyeWheel = DDRace || BlockWorlds || City || Plus;
1240 Info.m_AllowHookColl = DDRace;
1241 Info.m_AllowZoom = Race || BlockWorlds || City;
1242 Info.m_BugDDRaceGhost = DDRace;
1243 Info.m_BugDDRaceInput = DDRace;
1244 Info.m_BugFNGLaserRange = FNG;
1245 Info.m_BugVanillaBounce = Vanilla;
1246 Info.m_PredictFNG = FNG;
1247 Info.m_PredictDDRace = DDRace;
1248 Info.m_PredictDDRaceTiles = DDRace && !BlockWorlds;
1249 Info.m_PredictVanilla = Vanilla || FastCap;
1250 Info.m_EntitiesDDNet = DDNet;
1251 Info.m_EntitiesDDRace = DDRace;
1252 Info.m_EntitiesRace = Race;
1253 Info.m_EntitiesFNG = FNG;
1254 Info.m_EntitiesVanilla = Vanilla;
1255 Info.m_EntitiesBW = BlockWorlds;
1256 Info.m_Race = Race;
1257 Info.m_Pvp = !Race;
1258 Info.m_DontMaskEntities = !DDNet;
1259 Info.m_AllowXSkins = false;
1260 Info.m_EntitiesFDDrace = FDDrace;
1261 Info.m_HudHealthArmor = true;
1262 Info.m_HudAmmo = true;
1263 Info.m_HudDDRace = false;
1264 Info.m_NoWeakHookAndBounce = false;
1265 Info.m_NoSkinChangeForFrozen = false;
1266
1267 if(Version >= 0)
1268 {
1269 Info.m_TimeScore = Flags & GAMEINFOFLAG_TIMESCORE;
1270 }
1271 if(Version >= 2)
1272 {
1273 Info.m_FlagStartsRace = Flags & GAMEINFOFLAG_FLAG_STARTS_RACE;
1274 Info.m_UnlimitedAmmo = Flags & GAMEINFOFLAG_UNLIMITED_AMMO;
1275 Info.m_DDRaceRecordMessage = Flags & GAMEINFOFLAG_DDRACE_RECORD_MESSAGE;
1276 Info.m_RaceRecordMessage = Flags & GAMEINFOFLAG_RACE_RECORD_MESSAGE;
1277 Info.m_AllowEyeWheel = Flags & GAMEINFOFLAG_ALLOW_EYE_WHEEL;
1278 Info.m_AllowHookColl = Flags & GAMEINFOFLAG_ALLOW_HOOK_COLL;
1279 Info.m_AllowZoom = Flags & GAMEINFOFLAG_ALLOW_ZOOM;
1280 Info.m_BugDDRaceGhost = Flags & GAMEINFOFLAG_BUG_DDRACE_GHOST;
1281 Info.m_BugDDRaceInput = Flags & GAMEINFOFLAG_BUG_DDRACE_INPUT;
1282 Info.m_BugFNGLaserRange = Flags & GAMEINFOFLAG_BUG_FNG_LASER_RANGE;
1283 Info.m_BugVanillaBounce = Flags & GAMEINFOFLAG_BUG_VANILLA_BOUNCE;
1284 Info.m_PredictFNG = Flags & GAMEINFOFLAG_PREDICT_FNG;
1285 Info.m_PredictDDRace = Flags & GAMEINFOFLAG_PREDICT_DDRACE;
1286 Info.m_PredictDDRaceTiles = Flags & GAMEINFOFLAG_PREDICT_DDRACE_TILES;
1287 Info.m_PredictVanilla = Flags & GAMEINFOFLAG_PREDICT_VANILLA;
1288 Info.m_EntitiesDDNet = Flags & GAMEINFOFLAG_ENTITIES_DDNET;
1289 Info.m_EntitiesDDRace = Flags & GAMEINFOFLAG_ENTITIES_DDRACE;
1290 Info.m_EntitiesRace = Flags & GAMEINFOFLAG_ENTITIES_RACE;
1291 Info.m_EntitiesFNG = Flags & GAMEINFOFLAG_ENTITIES_FNG;
1292 Info.m_EntitiesVanilla = Flags & GAMEINFOFLAG_ENTITIES_VANILLA;
1293 }
1294 if(Version >= 3)
1295 {
1296 Info.m_Race = Flags & GAMEINFOFLAG_RACE;
1297 Info.m_DontMaskEntities = Flags & GAMEINFOFLAG_DONT_MASK_ENTITIES;
1298 }
1299 if(Version >= 4)
1300 {
1301 Info.m_EntitiesBW = Flags & GAMEINFOFLAG_ENTITIES_BW;
1302 }
1303 if(Version >= 5)
1304 {
1305 Info.m_AllowXSkins = Flags2 & GAMEINFOFLAG2_ALLOW_X_SKINS;
1306 }
1307 if(Version >= 6)
1308 {
1309 Info.m_EntitiesFDDrace = Flags2 & GAMEINFOFLAG2_ENTITIES_FDDRACE;
1310 }
1311 if(Version >= 7)
1312 {
1313 Info.m_HudHealthArmor = Flags2 & GAMEINFOFLAG2_HUD_HEALTH_ARMOR;
1314 Info.m_HudAmmo = Flags2 & GAMEINFOFLAG2_HUD_AMMO;
1315 Info.m_HudDDRace = Flags2 & GAMEINFOFLAG2_HUD_DDRACE;
1316 }
1317 if(Version >= 8)
1318 {
1319 Info.m_NoWeakHookAndBounce = Flags2 & GAMEINFOFLAG2_NO_WEAK_HOOK;
1320 }
1321 if(Version >= 9)
1322 {
1323 Info.m_NoSkinChangeForFrozen = Flags2 & GAMEINFOFLAG2_NO_SKIN_CHANGE_FOR_FROZEN;
1324 }
1325
1326 return Info;
1327}
1328
1329void CGameClient::InvalidateSnapshot()
1330{
1331 // clear all pointers
1332 mem_zero(block: &m_Snap, size: sizeof(m_Snap));
1333 m_Snap.m_LocalClientId = -1;
1334 SnapCollectEntities();
1335}
1336
1337void CGameClient::OnNewSnapshot()
1338{
1339 auto &&Evolve = [this](CNetObj_Character *pCharacter, int Tick) {
1340 CWorldCore TempWorld;
1341 CCharacterCore TempCore = CCharacterCore();
1342 CTeamsCore TempTeams = CTeamsCore();
1343 TempCore.Init(pWorld: &TempWorld, pCollision: Collision(), pTeams: &TempTeams);
1344 TempCore.Read(pObjCore: pCharacter);
1345 TempCore.m_ActiveWeapon = pCharacter->m_Weapon;
1346
1347 while(pCharacter->m_Tick < Tick)
1348 {
1349 pCharacter->m_Tick++;
1350 TempCore.Tick(UseInput: false);
1351 TempCore.Move();
1352 TempCore.Quantize();
1353 }
1354
1355 TempCore.Write(pObjCore: pCharacter);
1356 };
1357
1358 InvalidateSnapshot();
1359
1360 m_NewTick = true;
1361
1362 ProcessEvents();
1363
1364#ifdef CONF_DEBUG
1365 if(g_Config.m_DbgStress)
1366 {
1367 if((Client()->GameTick(Conn: g_Config.m_ClDummy) % 100) == 0)
1368 {
1369 char aMessage[64];
1370 int MsgLen = rand() % (sizeof(aMessage) - 1);
1371 for(int i = 0; i < MsgLen; i++)
1372 aMessage[i] = (char)('a' + (rand() % ('z' - 'a')));
1373 aMessage[MsgLen] = 0;
1374
1375 m_Chat.SendChat(Team: rand() & 1, pLine: aMessage);
1376 }
1377 }
1378#endif
1379
1380 bool FoundGameInfoEx = false;
1381 bool GotSwitchStateTeam = false;
1382 m_aSwitchStateTeam[g_Config.m_ClDummy] = -1;
1383
1384 for(auto &Client : m_aClients)
1385 {
1386 Client.m_SpecCharPresent = false;
1387 }
1388
1389 // go through all the items in the snapshot and gather the info we want
1390 {
1391 m_Snap.m_aTeamSize[TEAM_RED] = m_Snap.m_aTeamSize[TEAM_BLUE] = 0;
1392
1393 int Num = Client()->SnapNumItems(SnapId: IClient::SNAP_CURRENT);
1394 for(int i = 0; i < Num; i++)
1395 {
1396 IClient::CSnapItem Item;
1397 const void *pData = Client()->SnapGetItem(SnapId: IClient::SNAP_CURRENT, Index: i, pItem: &Item);
1398
1399 if(Item.m_Type == NETOBJTYPE_CLIENTINFO)
1400 {
1401 const CNetObj_ClientInfo *pInfo = (const CNetObj_ClientInfo *)pData;
1402 int ClientId = Item.m_Id;
1403 if(ClientId < MAX_CLIENTS)
1404 {
1405 CClientData *pClient = &m_aClients[ClientId];
1406
1407 if(!IntsToStr(pInts: &pInfo->m_Name0, NumInts: 4, pStr: pClient->m_aName, StrSize: std::size(pClient->m_aName)))
1408 {
1409 str_copy(dst&: pClient->m_aName, src: "nameless tee");
1410 }
1411 IntsToStr(pInts: &pInfo->m_Clan0, NumInts: 3, pStr: pClient->m_aClan, StrSize: std::size(pClient->m_aClan));
1412 pClient->m_Country = pInfo->m_Country;
1413 IntsToStr(pInts: &pInfo->m_Skin0, NumInts: 6, pStr: pClient->m_aSkinName, StrSize: std::size(pClient->m_aSkinName));
1414
1415 pClient->m_UseCustomColor = pInfo->m_UseCustomColor;
1416 pClient->m_ColorBody = pInfo->m_ColorBody;
1417 pClient->m_ColorFeet = pInfo->m_ColorFeet;
1418
1419 // prepare the info
1420 if(!m_GameInfo.m_AllowXSkins && (pClient->m_aSkinName[0] == 'x' && pClient->m_aSkinName[1] == '_'))
1421 str_copy(dst&: pClient->m_aSkinName, src: "default");
1422
1423 pClient->m_SkinInfo.m_ColorBody = color_cast<ColorRGBA>(hsl: ColorHSLA(pClient->m_ColorBody).UnclampLighting());
1424 pClient->m_SkinInfo.m_ColorFeet = color_cast<ColorRGBA>(hsl: ColorHSLA(pClient->m_ColorFeet).UnclampLighting());
1425 pClient->m_SkinInfo.m_Size = 64;
1426
1427 // find new skin
1428 const CSkin *pSkin = m_Skins.Find(pName: pClient->m_aSkinName);
1429 pClient->m_SkinInfo.m_OriginalRenderSkin = pSkin->m_OriginalSkin;
1430 pClient->m_SkinInfo.m_ColorableRenderSkin = pSkin->m_ColorableSkin;
1431 pClient->m_SkinInfo.m_SkinMetrics = pSkin->m_Metrics;
1432 pClient->m_SkinInfo.m_BloodColor = pSkin->m_BloodColor;
1433 pClient->m_SkinInfo.m_CustomColoredSkin = pClient->m_UseCustomColor;
1434
1435 if(!pClient->m_UseCustomColor)
1436 {
1437 pClient->m_SkinInfo.m_ColorBody = ColorRGBA(1, 1, 1);
1438 pClient->m_SkinInfo.m_ColorFeet = ColorRGBA(1, 1, 1);
1439 }
1440
1441 pClient->UpdateRenderInfo(IsTeamPlay: IsTeamPlay());
1442 }
1443 }
1444 else if(Item.m_Type == NETOBJTYPE_PLAYERINFO)
1445 {
1446 const CNetObj_PlayerInfo *pInfo = (const CNetObj_PlayerInfo *)pData;
1447
1448 if(pInfo->m_ClientId < MAX_CLIENTS && pInfo->m_ClientId == Item.m_Id)
1449 {
1450 m_aClients[pInfo->m_ClientId].m_Team = pInfo->m_Team;
1451 m_aClients[pInfo->m_ClientId].m_Active = true;
1452 m_Snap.m_apPlayerInfos[pInfo->m_ClientId] = pInfo;
1453 m_Snap.m_NumPlayers++;
1454
1455 if(pInfo->m_Local)
1456 {
1457 m_Snap.m_LocalClientId = pInfo->m_ClientId;
1458 m_Snap.m_pLocalInfo = pInfo;
1459
1460 if(pInfo->m_Team == TEAM_SPECTATORS)
1461 {
1462 m_Snap.m_SpecInfo.m_Active = true;
1463 }
1464 }
1465
1466 // calculate team-balance
1467 if(pInfo->m_Team != TEAM_SPECTATORS)
1468 {
1469 m_Snap.m_aTeamSize[pInfo->m_Team]++;
1470 if(!m_aStats[pInfo->m_ClientId].IsActive())
1471 m_aStats[pInfo->m_ClientId].JoinGame(Tick: Client()->GameTick(Conn: g_Config.m_ClDummy));
1472 }
1473 else if(m_aStats[pInfo->m_ClientId].IsActive())
1474 m_aStats[pInfo->m_ClientId].JoinSpec(Tick: Client()->GameTick(Conn: g_Config.m_ClDummy));
1475 }
1476 }
1477 else if(Item.m_Type == NETOBJTYPE_DDNETPLAYER)
1478 {
1479 m_ReceivedDDNetPlayer = true;
1480 const CNetObj_DDNetPlayer *pInfo = (const CNetObj_DDNetPlayer *)pData;
1481 if(Item.m_Id < MAX_CLIENTS)
1482 {
1483 m_aClients[Item.m_Id].m_AuthLevel = pInfo->m_AuthLevel;
1484 m_aClients[Item.m_Id].m_Afk = pInfo->m_Flags & EXPLAYERFLAG_AFK;
1485 m_aClients[Item.m_Id].m_Paused = pInfo->m_Flags & EXPLAYERFLAG_PAUSED;
1486 m_aClients[Item.m_Id].m_Spec = pInfo->m_Flags & EXPLAYERFLAG_SPEC;
1487
1488 if(Item.m_Id == m_Snap.m_LocalClientId && (m_aClients[Item.m_Id].m_Paused || m_aClients[Item.m_Id].m_Spec))
1489 {
1490 m_Snap.m_SpecInfo.m_Active = true;
1491 }
1492 }
1493 }
1494 else if(Item.m_Type == NETOBJTYPE_CHARACTER)
1495 {
1496 if(Item.m_Id < MAX_CLIENTS)
1497 {
1498 const void *pOld = Client()->SnapFindItem(SnapId: IClient::SNAP_PREV, Type: NETOBJTYPE_CHARACTER, Id: Item.m_Id);
1499 m_Snap.m_aCharacters[Item.m_Id].m_Cur = *((const CNetObj_Character *)pData);
1500 if(pOld)
1501 {
1502 m_Snap.m_aCharacters[Item.m_Id].m_Active = true;
1503 m_Snap.m_aCharacters[Item.m_Id].m_Prev = *((const CNetObj_Character *)pOld);
1504
1505 // limit evolving to 3 seconds
1506 bool EvolvePrev = Client()->PrevGameTick(Conn: g_Config.m_ClDummy) - m_Snap.m_aCharacters[Item.m_Id].m_Prev.m_Tick <= 3 * Client()->GameTickSpeed();
1507 bool EvolveCur = Client()->GameTick(Conn: g_Config.m_ClDummy) - m_Snap.m_aCharacters[Item.m_Id].m_Cur.m_Tick <= 3 * Client()->GameTickSpeed();
1508
1509 // reuse the result from the previous evolve if the snapped character didn't change since the previous snapshot
1510 if(EvolveCur && m_aClients[Item.m_Id].m_Evolved.m_Tick == Client()->PrevGameTick(Conn: g_Config.m_ClDummy))
1511 {
1512 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)
1513 m_Snap.m_aCharacters[Item.m_Id].m_Prev = m_aClients[Item.m_Id].m_Evolved;
1514 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)
1515 m_Snap.m_aCharacters[Item.m_Id].m_Cur = m_aClients[Item.m_Id].m_Evolved;
1516 }
1517
1518 if(EvolvePrev && m_Snap.m_aCharacters[Item.m_Id].m_Prev.m_Tick)
1519 Evolve(&m_Snap.m_aCharacters[Item.m_Id].m_Prev, Client()->PrevGameTick(Conn: g_Config.m_ClDummy));
1520 if(EvolveCur && m_Snap.m_aCharacters[Item.m_Id].m_Cur.m_Tick)
1521 Evolve(&m_Snap.m_aCharacters[Item.m_Id].m_Cur, Client()->GameTick(Conn: g_Config.m_ClDummy));
1522
1523 m_aClients[Item.m_Id].m_Snapped = *((const CNetObj_Character *)pData);
1524 m_aClients[Item.m_Id].m_Evolved = m_Snap.m_aCharacters[Item.m_Id].m_Cur;
1525 }
1526 else
1527 {
1528 m_aClients[Item.m_Id].m_Evolved.m_Tick = -1;
1529 }
1530 }
1531 }
1532 else if(Item.m_Type == NETOBJTYPE_DDNETCHARACTER)
1533 {
1534 const CNetObj_DDNetCharacter *pCharacterData = (const CNetObj_DDNetCharacter *)pData;
1535
1536 if(Item.m_Id < MAX_CLIENTS)
1537 {
1538 m_Snap.m_aCharacters[Item.m_Id].m_ExtendedData = *pCharacterData;
1539 m_Snap.m_aCharacters[Item.m_Id].m_PrevExtendedData = (const CNetObj_DDNetCharacter *)Client()->SnapFindItem(SnapId: IClient::SNAP_PREV, Type: NETOBJTYPE_DDNETCHARACTER, Id: Item.m_Id);
1540 m_Snap.m_aCharacters[Item.m_Id].m_HasExtendedData = true;
1541 m_Snap.m_aCharacters[Item.m_Id].m_HasExtendedDisplayInfo = false;
1542 if(pCharacterData->m_JumpedTotal != -1)
1543 {
1544 m_Snap.m_aCharacters[Item.m_Id].m_HasExtendedDisplayInfo = true;
1545 }
1546 CClientData *pClient = &m_aClients[Item.m_Id];
1547 // Collision
1548 pClient->m_Solo = pCharacterData->m_Flags & CHARACTERFLAG_SOLO;
1549 pClient->m_Jetpack = pCharacterData->m_Flags & CHARACTERFLAG_JETPACK;
1550 pClient->m_CollisionDisabled = pCharacterData->m_Flags & CHARACTERFLAG_COLLISION_DISABLED;
1551 pClient->m_HammerHitDisabled = pCharacterData->m_Flags & CHARACTERFLAG_HAMMER_HIT_DISABLED;
1552 pClient->m_GrenadeHitDisabled = pCharacterData->m_Flags & CHARACTERFLAG_GRENADE_HIT_DISABLED;
1553 pClient->m_LaserHitDisabled = pCharacterData->m_Flags & CHARACTERFLAG_LASER_HIT_DISABLED;
1554 pClient->m_ShotgunHitDisabled = pCharacterData->m_Flags & CHARACTERFLAG_SHOTGUN_HIT_DISABLED;
1555 pClient->m_HookHitDisabled = pCharacterData->m_Flags & CHARACTERFLAG_HOOK_HIT_DISABLED;
1556 pClient->m_Super = pCharacterData->m_Flags & CHARACTERFLAG_SUPER;
1557
1558 // Endless
1559 pClient->m_EndlessHook = pCharacterData->m_Flags & CHARACTERFLAG_ENDLESS_HOOK;
1560 pClient->m_EndlessJump = pCharacterData->m_Flags & CHARACTERFLAG_ENDLESS_JUMP;
1561
1562 // Freeze
1563 pClient->m_FreezeEnd = pCharacterData->m_FreezeEnd;
1564 pClient->m_DeepFrozen = pCharacterData->m_FreezeEnd == -1;
1565 pClient->m_LiveFrozen = (pCharacterData->m_Flags & CHARACTERFLAG_MOVEMENTS_DISABLED) != 0;
1566
1567 // Telegun
1568 pClient->m_HasTelegunGrenade = pCharacterData->m_Flags & CHARACTERFLAG_TELEGUN_GRENADE;
1569 pClient->m_HasTelegunGun = pCharacterData->m_Flags & CHARACTERFLAG_TELEGUN_GUN;
1570 pClient->m_HasTelegunLaser = pCharacterData->m_Flags & CHARACTERFLAG_TELEGUN_LASER;
1571
1572 pClient->m_Predicted.ReadDDNet(pObjDDNet: pCharacterData);
1573
1574 m_Teams.SetSolo(ClientId: Item.m_Id, Value: pClient->m_Solo);
1575 }
1576 }
1577 else if(Item.m_Type == NETOBJTYPE_SPECCHAR)
1578 {
1579 const CNetObj_SpecChar *pSpecCharData = (const CNetObj_SpecChar *)pData;
1580
1581 if(Item.m_Id < MAX_CLIENTS)
1582 {
1583 CClientData *pClient = &m_aClients[Item.m_Id];
1584 pClient->m_SpecCharPresent = true;
1585 pClient->m_SpecChar.x = pSpecCharData->m_X;
1586 pClient->m_SpecChar.y = pSpecCharData->m_Y;
1587 }
1588 }
1589 else if(Item.m_Type == NETOBJTYPE_SPECTATORINFO)
1590 {
1591 m_Snap.m_pSpectatorInfo = (const CNetObj_SpectatorInfo *)pData;
1592 m_Snap.m_pPrevSpectatorInfo = (const CNetObj_SpectatorInfo *)Client()->SnapFindItem(SnapId: IClient::SNAP_PREV, Type: NETOBJTYPE_SPECTATORINFO, Id: Item.m_Id);
1593
1594 m_Snap.m_SpecInfo.m_SpectatorId = m_Snap.m_pSpectatorInfo->m_SpectatorId;
1595 }
1596 else if(Item.m_Type == NETOBJTYPE_GAMEINFO)
1597 {
1598 m_Snap.m_pGameInfoObj = (const CNetObj_GameInfo *)pData;
1599 bool CurrentTickGameOver = (bool)(m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER);
1600 if(!m_GameOver && CurrentTickGameOver)
1601 OnGameOver();
1602 else if(m_GameOver && !CurrentTickGameOver)
1603 OnStartGame();
1604 // Handle case that a new round is started (RoundStartTick changed)
1605 // New round is usually started after `restart` on server
1606 if(m_Snap.m_pGameInfoObj->m_RoundStartTick != m_LastRoundStartTick && !(CurrentTickGameOver || m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_PAUSED || m_GamePaused))
1607 OnStartRound();
1608 m_LastRoundStartTick = m_Snap.m_pGameInfoObj->m_RoundStartTick;
1609 m_GameOver = CurrentTickGameOver;
1610 m_GamePaused = (bool)(m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_PAUSED);
1611 }
1612 else if(Item.m_Type == NETOBJTYPE_GAMEINFOEX)
1613 {
1614 if(FoundGameInfoEx)
1615 {
1616 continue;
1617 }
1618 FoundGameInfoEx = true;
1619 CServerInfo ServerInfo;
1620 Client()->GetServerInfo(pServerInfo: &ServerInfo);
1621 m_GameInfo = GetGameInfo(pInfoEx: (const CNetObj_GameInfoEx *)pData, InfoExSize: Client()->SnapItemSize(SnapId: IClient::SNAP_CURRENT, Index: i), pFallbackServerInfo: &ServerInfo);
1622 }
1623 else if(Item.m_Type == NETOBJTYPE_GAMEDATA)
1624 {
1625 m_Snap.m_pGameDataObj = (const CNetObj_GameData *)pData;
1626 m_Snap.m_GameDataSnapId = Item.m_Id;
1627 if(m_Snap.m_pGameDataObj->m_FlagCarrierRed == FLAG_TAKEN)
1628 {
1629 if(m_aFlagDropTick[TEAM_RED] == 0)
1630 m_aFlagDropTick[TEAM_RED] = Client()->GameTick(Conn: g_Config.m_ClDummy);
1631 }
1632 else
1633 m_aFlagDropTick[TEAM_RED] = 0;
1634 if(m_Snap.m_pGameDataObj->m_FlagCarrierBlue == FLAG_TAKEN)
1635 {
1636 if(m_aFlagDropTick[TEAM_BLUE] == 0)
1637 m_aFlagDropTick[TEAM_BLUE] = Client()->GameTick(Conn: g_Config.m_ClDummy);
1638 }
1639 else
1640 m_aFlagDropTick[TEAM_BLUE] = 0;
1641 if(m_LastFlagCarrierRed == FLAG_ATSTAND && m_Snap.m_pGameDataObj->m_FlagCarrierRed >= 0)
1642 OnFlagGrab(TeamId: TEAM_RED);
1643 else if(m_LastFlagCarrierBlue == FLAG_ATSTAND && m_Snap.m_pGameDataObj->m_FlagCarrierBlue >= 0)
1644 OnFlagGrab(TeamId: TEAM_BLUE);
1645
1646 m_LastFlagCarrierRed = m_Snap.m_pGameDataObj->m_FlagCarrierRed;
1647 m_LastFlagCarrierBlue = m_Snap.m_pGameDataObj->m_FlagCarrierBlue;
1648 }
1649 else if(Item.m_Type == NETOBJTYPE_FLAG)
1650 m_Snap.m_apFlags[Item.m_Id % 2] = (const CNetObj_Flag *)pData;
1651 else if(Item.m_Type == NETOBJTYPE_SWITCHSTATE)
1652 {
1653 if(Item.m_DataSize < 36)
1654 {
1655 continue;
1656 }
1657 const CNetObj_SwitchState *pSwitchStateData = (const CNetObj_SwitchState *)pData;
1658 int Team = clamp(val: Item.m_Id, lo: (int)TEAM_FLOCK, hi: (int)TEAM_SUPER - 1);
1659
1660 int HighestSwitchNumber = clamp(val: pSwitchStateData->m_HighestSwitchNumber, lo: 0, hi: 255);
1661 if(HighestSwitchNumber != maximum(a: 0, b: (int)Switchers().size() - 1))
1662 {
1663 m_GameWorld.m_Core.InitSwitchers(HighestSwitchNumber);
1664 Collision()->m_HighestSwitchNumber = HighestSwitchNumber;
1665 }
1666
1667 for(int j = 0; j < (int)Switchers().size(); j++)
1668 {
1669 Switchers()[j].m_aStatus[Team] = (pSwitchStateData->m_aStatus[j / 32] >> (j % 32)) & 1;
1670 }
1671
1672 if(Item.m_DataSize >= 68)
1673 {
1674 // update the endtick of up to four timed switchers
1675 for(int j = 0; j < (int)std::size(pSwitchStateData->m_aEndTicks); j++)
1676 {
1677 int SwitchNumber = pSwitchStateData->m_aSwitchNumbers[j];
1678 int EndTick = pSwitchStateData->m_aEndTicks[j];
1679 if(EndTick > 0 && in_range(a: SwitchNumber, lower: 0, upper: (int)Switchers().size()))
1680 {
1681 Switchers()[SwitchNumber].m_aEndTick[Team] = EndTick;
1682 }
1683 }
1684 }
1685
1686 // update switch types
1687 for(auto &Switcher : Switchers())
1688 {
1689 if(Switcher.m_aStatus[Team])
1690 Switcher.m_aType[Team] = Switcher.m_aEndTick[Team] ? TILE_SWITCHTIMEDOPEN : TILE_SWITCHOPEN;
1691 else
1692 Switcher.m_aType[Team] = Switcher.m_aEndTick[Team] ? TILE_SWITCHTIMEDCLOSE : TILE_SWITCHCLOSE;
1693 }
1694
1695 if(!GotSwitchStateTeam)
1696 m_aSwitchStateTeam[g_Config.m_ClDummy] = Team;
1697 else
1698 m_aSwitchStateTeam[g_Config.m_ClDummy] = -1;
1699 GotSwitchStateTeam = true;
1700 }
1701 }
1702 }
1703
1704 if(!FoundGameInfoEx)
1705 {
1706 CServerInfo ServerInfo;
1707 Client()->GetServerInfo(pServerInfo: &ServerInfo);
1708 m_GameInfo = GetGameInfo(pInfoEx: 0, InfoExSize: 0, pFallbackServerInfo: &ServerInfo);
1709 }
1710
1711 // setup local pointers
1712 if(m_Snap.m_LocalClientId >= 0)
1713 {
1714 m_aLocalIds[g_Config.m_ClDummy] = m_Snap.m_LocalClientId;
1715
1716 CSnapState::CCharacterInfo *pChr = &m_Snap.m_aCharacters[m_Snap.m_LocalClientId];
1717 if(pChr->m_Active)
1718 {
1719 if(!m_Snap.m_SpecInfo.m_Active)
1720 {
1721 m_Snap.m_pLocalCharacter = &pChr->m_Cur;
1722 m_Snap.m_pLocalPrevCharacter = &pChr->m_Prev;
1723 m_LocalCharacterPos = vec2(m_Snap.m_pLocalCharacter->m_X, m_Snap.m_pLocalCharacter->m_Y);
1724 }
1725 }
1726 else if(Client()->SnapFindItem(SnapId: IClient::SNAP_PREV, Type: NETOBJTYPE_CHARACTER, Id: m_Snap.m_LocalClientId))
1727 {
1728 // player died
1729 m_Controls.OnPlayerDeath();
1730 }
1731 }
1732 if(Client()->State() == IClient::STATE_DEMOPLAYBACK)
1733 {
1734 if(m_Snap.m_LocalClientId == -1 && m_DemoSpecId == SPEC_FOLLOW)
1735 m_DemoSpecId = SPEC_FREEVIEW;
1736 if(m_DemoSpecId != SPEC_FOLLOW)
1737 {
1738 m_Snap.m_SpecInfo.m_Active = true;
1739 if(m_DemoSpecId > SPEC_FREEVIEW && m_Snap.m_aCharacters[m_DemoSpecId].m_Active)
1740 m_Snap.m_SpecInfo.m_SpectatorId = m_DemoSpecId;
1741 else
1742 m_Snap.m_SpecInfo.m_SpectatorId = SPEC_FREEVIEW;
1743 }
1744 }
1745
1746 // clear out unneeded client data
1747 for(int i = 0; i < MAX_CLIENTS; ++i)
1748 {
1749 if(!m_Snap.m_apPlayerInfos[i] && m_aClients[i].m_Active)
1750 {
1751 m_aClients[i].Reset();
1752 m_aStats[i].Reset();
1753 }
1754 }
1755
1756 for(int i = 0; i < MAX_CLIENTS; ++i)
1757 {
1758 // update friend state
1759 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));
1760
1761 // update foe state
1762 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));
1763 }
1764
1765 // sort player infos by name
1766 mem_copy(dest: m_Snap.m_apInfoByName, source: m_Snap.m_apPlayerInfos, size: sizeof(m_Snap.m_apInfoByName));
1767 std::stable_sort(first: m_Snap.m_apInfoByName, last: m_Snap.m_apInfoByName + MAX_CLIENTS,
1768 comp: [this](const CNetObj_PlayerInfo *p1, const CNetObj_PlayerInfo *p2) -> bool {
1769 if(!p2)
1770 return static_cast<bool>(p1);
1771 if(!p1)
1772 return false;
1773 return str_comp_nocase(a: m_aClients[p1->m_ClientId].m_aName, b: m_aClients[p2->m_ClientId].m_aName) < 0;
1774 });
1775
1776 bool TimeScore = m_GameInfo.m_TimeScore;
1777
1778 // sort player infos by score
1779 mem_copy(dest: m_Snap.m_apInfoByScore, source: m_Snap.m_apInfoByName, size: sizeof(m_Snap.m_apInfoByScore));
1780 std::stable_sort(first: m_Snap.m_apInfoByScore, last: m_Snap.m_apInfoByScore + MAX_CLIENTS,
1781 comp: [TimeScore](const CNetObj_PlayerInfo *p1, const CNetObj_PlayerInfo *p2) -> bool {
1782 if(!p2)
1783 return static_cast<bool>(p1);
1784 if(!p1)
1785 return false;
1786 return (((TimeScore && p1->m_Score == -9999) ? std::numeric_limits<int>::min() : p1->m_Score) >
1787 ((TimeScore && p2->m_Score == -9999) ? std::numeric_limits<int>::min() : p2->m_Score));
1788 });
1789
1790 // sort player infos by DDRace Team (and score between)
1791 int Index = 0;
1792 for(int Team = TEAM_FLOCK; Team <= TEAM_SUPER; ++Team)
1793 {
1794 for(int i = 0; i < MAX_CLIENTS && Index < MAX_CLIENTS; ++i)
1795 {
1796 if(m_Snap.m_apInfoByScore[i] && m_Teams.Team(ClientId: m_Snap.m_apInfoByScore[i]->m_ClientId) == Team)
1797 m_Snap.m_apInfoByDDTeamScore[Index++] = m_Snap.m_apInfoByScore[i];
1798 }
1799 }
1800
1801 // sort player infos by DDRace Team (and name between)
1802 Index = 0;
1803 for(int Team = TEAM_FLOCK; Team <= TEAM_SUPER; ++Team)
1804 {
1805 for(int i = 0; i < MAX_CLIENTS && Index < MAX_CLIENTS; ++i)
1806 {
1807 if(m_Snap.m_apInfoByName[i] && m_Teams.Team(ClientId: m_Snap.m_apInfoByName[i]->m_ClientId) == Team)
1808 m_Snap.m_apInfoByDDTeamName[Index++] = m_Snap.m_apInfoByName[i];
1809 }
1810 }
1811
1812 CServerInfo CurrentServerInfo;
1813 Client()->GetServerInfo(pServerInfo: &CurrentServerInfo);
1814 CTuningParams StandardTuning;
1815 if(CurrentServerInfo.m_aGameType[0] != '0')
1816 {
1817 if(str_comp(a: CurrentServerInfo.m_aGameType, b: "DM") != 0 && str_comp(a: CurrentServerInfo.m_aGameType, b: "TDM") != 0 && str_comp(a: CurrentServerInfo.m_aGameType, b: "CTF") != 0)
1818 m_ServerMode = SERVERMODE_MOD;
1819 else if(mem_comp(a: &StandardTuning, b: &m_aTuning[g_Config.m_ClDummy], size: 33) == 0)
1820 m_ServerMode = SERVERMODE_PURE;
1821 else
1822 m_ServerMode = SERVERMODE_PUREMOD;
1823 }
1824
1825 // add tuning to demo
1826 bool AnyRecording = false;
1827 for(int i = 0; i < RECORDER_MAX; i++)
1828 if(DemoRecorder(Recorder: i)->IsRecording())
1829 {
1830 AnyRecording = true;
1831 break;
1832 }
1833 if(AnyRecording && mem_comp(a: &StandardTuning, b: &m_aTuning[g_Config.m_ClDummy], size: sizeof(CTuningParams)) != 0)
1834 {
1835 CMsgPacker Msg(NETMSGTYPE_SV_TUNEPARAMS);
1836 int *pParams = (int *)&m_aTuning[g_Config.m_ClDummy];
1837 for(unsigned i = 0; i < sizeof(m_aTuning[0]) / sizeof(int); i++)
1838 Msg.AddInt(i: pParams[i]);
1839 Client()->SendMsgActive(pMsg: &Msg, Flags: MSGFLAG_RECORD | MSGFLAG_NOSEND);
1840 }
1841
1842 for(int i = 0; i < 2; i++)
1843 {
1844 if(m_aDDRaceMsgSent[i] || !m_Snap.m_pLocalInfo)
1845 {
1846 continue;
1847 }
1848 if(i == IClient::CONN_DUMMY && !Client()->DummyConnected())
1849 {
1850 continue;
1851 }
1852 CMsgPacker Msg(NETMSGTYPE_CL_ISDDNETLEGACY, false);
1853 Msg.AddInt(i: DDNetVersion());
1854 Client()->SendMsg(Conn: i, pMsg: &Msg, Flags: MSGFLAG_VITAL);
1855 m_aDDRaceMsgSent[i] = true;
1856 }
1857
1858 if(m_Snap.m_SpecInfo.m_Active && m_MultiViewActivated)
1859 {
1860 // dont show other teams while spectating in multi view
1861 CNetMsg_Cl_ShowOthers Msg;
1862 Msg.m_Show = SHOW_OTHERS_ONLY_TEAM;
1863 Client()->SendPackMsgActive(pMsg: &Msg, Flags: MSGFLAG_VITAL);
1864
1865 // update state
1866 m_aShowOthers[g_Config.m_ClDummy] = SHOW_OTHERS_ONLY_TEAM;
1867 }
1868 else if(m_aShowOthers[g_Config.m_ClDummy] == SHOW_OTHERS_NOT_SET || m_aShowOthers[g_Config.m_ClDummy] != g_Config.m_ClShowOthers)
1869 {
1870 {
1871 CNetMsg_Cl_ShowOthers Msg;
1872 Msg.m_Show = g_Config.m_ClShowOthers;
1873 Client()->SendPackMsgActive(pMsg: &Msg, Flags: MSGFLAG_VITAL);
1874 }
1875
1876 // update state
1877 m_aShowOthers[g_Config.m_ClDummy] = g_Config.m_ClShowOthers;
1878 }
1879
1880 float ZoomToSend = m_Camera.m_Zoom;
1881 if(m_Camera.m_Zooming)
1882 {
1883 if(m_Camera.m_ZoomSmoothingTarget > m_Camera.m_Zoom) // Zooming out
1884 ZoomToSend = m_Camera.m_ZoomSmoothingTarget;
1885 else if(m_Camera.m_ZoomSmoothingTarget < m_Camera.m_Zoom && m_LastZoom > 0) // Zooming in
1886 ZoomToSend = m_LastZoom;
1887 }
1888
1889 if(ZoomToSend != m_LastZoom || Graphics()->ScreenAspect() != m_LastScreenAspect || (Client()->DummyConnected() && !m_LastDummyConnected))
1890 {
1891 CNetMsg_Cl_ShowDistance Msg;
1892 float x, y;
1893 RenderTools()->CalcScreenParams(Aspect: Graphics()->ScreenAspect(), Zoom: ZoomToSend, pWidth: &x, pHeight: &y);
1894 Msg.m_X = x;
1895 Msg.m_Y = y;
1896 Client()->ChecksumData()->m_Zoom = ZoomToSend;
1897 CMsgPacker Packer(&Msg);
1898 Msg.Pack(pPacker: &Packer);
1899 if(ZoomToSend != m_LastZoom)
1900 Client()->SendMsg(Conn: IClient::CONN_MAIN, pMsg: &Packer, Flags: MSGFLAG_VITAL);
1901 if(Client()->DummyConnected())
1902 Client()->SendMsg(Conn: IClient::CONN_DUMMY, pMsg: &Packer, Flags: MSGFLAG_VITAL);
1903 m_LastZoom = ZoomToSend;
1904 m_LastScreenAspect = Graphics()->ScreenAspect();
1905 }
1906 m_LastDummyConnected = Client()->DummyConnected();
1907
1908 for(auto &pComponent : m_vpAll)
1909 pComponent->OnNewSnapshot();
1910
1911 // notify editor when local character moved
1912 UpdateEditorIngameMoved();
1913
1914 // detect air jump for other players
1915 for(int i = 0; i < MAX_CLIENTS; i++)
1916 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))
1917 if(!Predict() || (i != m_Snap.m_LocalClientId && (!AntiPingPlayers() || i != m_PredictedDummyId)))
1918 {
1919 vec2 Pos = mix(a: vec2(m_Snap.m_aCharacters[i].m_Prev.m_X, m_Snap.m_aCharacters[i].m_Prev.m_Y),
1920 b: vec2(m_Snap.m_aCharacters[i].m_Cur.m_X, m_Snap.m_aCharacters[i].m_Cur.m_Y),
1921 amount: Client()->IntraGameTick(Conn: g_Config.m_ClDummy));
1922 float Alpha = 1.0f;
1923 bool SameTeam = m_Teams.SameTeam(ClientId1: m_Snap.m_LocalClientId, ClientId2: i);
1924 if(!SameTeam || m_aClients[i].m_Solo || m_aClients[m_Snap.m_LocalClientId].m_Solo)
1925 Alpha = g_Config.m_ClShowOthersAlpha / 100.0f;
1926 m_Effects.AirJump(Pos, Alpha);
1927 }
1928
1929 if(m_Snap.m_LocalClientId != m_PrevLocalId)
1930 m_PredictedDummyId = m_PrevLocalId;
1931 m_PrevLocalId = m_Snap.m_LocalClientId;
1932 m_IsDummySwapping = 0;
1933
1934 SnapCollectEntities(); // creates a collection that associates EntityEx snap items with the entities they belong to
1935
1936 // update prediction data
1937 if(Client()->State() != IClient::STATE_DEMOPLAYBACK)
1938 UpdatePrediction();
1939}
1940
1941void CGameClient::UpdateEditorIngameMoved()
1942{
1943 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);
1944 if(!g_Config.m_ClEditor)
1945 {
1946 m_EditorMovementDelay = 5;
1947 }
1948 else if(m_EditorMovementDelay > 0 && !LocalCharacterMoved)
1949 {
1950 --m_EditorMovementDelay;
1951 }
1952 if(m_EditorMovementDelay == 0 && LocalCharacterMoved)
1953 {
1954 Editor()->OnIngameMoved();
1955 }
1956}
1957
1958void CGameClient::OnPredict()
1959{
1960 // store the previous values so we can detect prediction errors
1961 CCharacterCore BeforePrevChar = m_PredictedPrevChar;
1962 CCharacterCore BeforeChar = m_PredictedChar;
1963
1964 // we can't predict without our own id or own character
1965 if(m_Snap.m_LocalClientId == -1 || !m_Snap.m_aCharacters[m_Snap.m_LocalClientId].m_Active)
1966 return;
1967
1968 // don't predict anything if we are paused
1969 if(m_Snap.m_pGameInfoObj && m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_PAUSED)
1970 {
1971 if(m_Snap.m_pLocalCharacter)
1972 {
1973 m_PredictedChar.Read(pObjCore: m_Snap.m_pLocalCharacter);
1974 m_PredictedChar.m_ActiveWeapon = m_Snap.m_pLocalCharacter->m_Weapon;
1975 }
1976 if(m_Snap.m_pLocalPrevCharacter)
1977 {
1978 m_PredictedPrevChar.Read(pObjCore: m_Snap.m_pLocalPrevCharacter);
1979 m_PredictedPrevChar.m_ActiveWeapon = m_Snap.m_pLocalPrevCharacter->m_Weapon;
1980 }
1981 return;
1982 }
1983
1984 vec2 aBeforeRender[MAX_CLIENTS];
1985 for(int i = 0; i < MAX_CLIENTS; i++)
1986 aBeforeRender[i] = GetSmoothPos(ClientId: i);
1987
1988 // init
1989 bool Dummy = g_Config.m_ClDummy ^ m_IsDummySwapping;
1990 m_PredictedWorld.CopyWorld(pFrom: &m_GameWorld);
1991
1992 // don't predict inactive players, or entities from other teams
1993 for(int i = 0; i < MAX_CLIENTS; i++)
1994 if(CCharacter *pChar = m_PredictedWorld.GetCharacterById(Id: i))
1995 if((!m_Snap.m_aCharacters[i].m_Active && pChar->m_SnapTicks > 10) || IsOtherTeam(ClientId: i))
1996 pChar->Destroy();
1997
1998 CProjectile *pProjNext = 0;
1999 for(CProjectile *pProj = (CProjectile *)m_PredictedWorld.FindFirst(Type: CGameWorld::ENTTYPE_PROJECTILE); pProj; pProj = pProjNext)
2000 {
2001 pProjNext = (CProjectile *)pProj->TypeNext();
2002 if(IsOtherTeam(ClientId: pProj->GetOwner()))
2003 {
2004 pProj->Destroy();
2005 }
2006 }
2007
2008 CCharacter *pLocalChar = m_PredictedWorld.GetCharacterById(Id: m_Snap.m_LocalClientId);
2009 if(!pLocalChar)
2010 return;
2011 CCharacter *pDummyChar = 0;
2012 if(PredictDummy())
2013 pDummyChar = m_PredictedWorld.GetCharacterById(Id: m_PredictedDummyId);
2014
2015 // predict
2016 for(int Tick = Client()->GameTick(Conn: g_Config.m_ClDummy) + 1; Tick <= Client()->PredGameTick(Conn: g_Config.m_ClDummy); Tick++)
2017 {
2018 // fetch the previous characters
2019 if(Tick == Client()->PredGameTick(Conn: g_Config.m_ClDummy))
2020 {
2021 m_PrevPredictedWorld.CopyWorld(pFrom: &m_PredictedWorld);
2022 m_PredictedPrevChar = pLocalChar->GetCore();
2023 for(int i = 0; i < MAX_CLIENTS; i++)
2024 if(CCharacter *pChar = m_PredictedWorld.GetCharacterById(Id: i))
2025 m_aClients[i].m_PrevPredicted = pChar->GetCore();
2026 }
2027
2028 // optionally allow some movement in freeze by not predicting freeze the last one to two ticks
2029 if(g_Config.m_ClPredictFreeze == 2 && Client()->PredGameTick(Conn: g_Config.m_ClDummy) - 1 - Client()->PredGameTick(Conn: g_Config.m_ClDummy) % 2 <= Tick)
2030 pLocalChar->m_CanMoveInFreeze = true;
2031
2032 // apply inputs and tick
2033 CNetObj_PlayerInput *pInputData = (CNetObj_PlayerInput *)Client()->GetInput(Tick, IsDummy: m_IsDummySwapping);
2034 CNetObj_PlayerInput *pDummyInputData = !pDummyChar ? 0 : (CNetObj_PlayerInput *)Client()->GetInput(Tick, IsDummy: m_IsDummySwapping ^ 1);
2035 bool DummyFirst = pInputData && pDummyInputData && pDummyChar->GetCid() < pLocalChar->GetCid();
2036
2037 if(DummyFirst)
2038 pDummyChar->OnDirectInput(pNewInput: pDummyInputData);
2039 if(pInputData)
2040 pLocalChar->OnDirectInput(pNewInput: pInputData);
2041 if(pDummyInputData && !DummyFirst)
2042 pDummyChar->OnDirectInput(pNewInput: pDummyInputData);
2043 m_PredictedWorld.m_GameTick = Tick;
2044 if(pInputData)
2045 pLocalChar->OnPredictedInput(pNewInput: pInputData);
2046 if(pDummyInputData)
2047 pDummyChar->OnPredictedInput(pNewInput: pDummyInputData);
2048 m_PredictedWorld.Tick();
2049
2050 // fetch the current characters
2051 if(Tick == Client()->PredGameTick(Conn: g_Config.m_ClDummy))
2052 {
2053 m_PredictedChar = pLocalChar->GetCore();
2054 for(int i = 0; i < MAX_CLIENTS; i++)
2055 if(CCharacter *pChar = m_PredictedWorld.GetCharacterById(Id: i))
2056 m_aClients[i].m_Predicted = pChar->GetCore();
2057 }
2058
2059 for(int i = 0; i < MAX_CLIENTS; i++)
2060 if(CCharacter *pChar = m_PredictedWorld.GetCharacterById(Id: i))
2061 {
2062 m_aClients[i].m_aPredPos[Tick % 200] = pChar->Core()->m_Pos;
2063 m_aClients[i].m_aPredTick[Tick % 200] = Tick;
2064 }
2065
2066 // check if we want to trigger effects
2067 if(Tick > m_aLastNewPredictedTick[Dummy])
2068 {
2069 m_aLastNewPredictedTick[Dummy] = Tick;
2070 m_NewPredictedTick = true;
2071 vec2 Pos = pLocalChar->Core()->m_Pos;
2072 int Events = pLocalChar->Core()->m_TriggeredEvents;
2073 if(g_Config.m_ClPredict && !m_SuppressEvents)
2074 if(Events & COREEVENT_AIR_JUMP)
2075 m_Effects.AirJump(Pos, Alpha: 1.0f);
2076 if(g_Config.m_SndGame && !m_SuppressEvents)
2077 {
2078 if(Events & COREEVENT_GROUND_JUMP)
2079 m_Sounds.PlayAndRecord(Channel: CSounds::CHN_WORLD, SetId: SOUND_PLAYER_JUMP, Vol: 1.0f, Pos);
2080 if(Events & COREEVENT_HOOK_ATTACH_GROUND)
2081 m_Sounds.PlayAndRecord(Channel: CSounds::CHN_WORLD, SetId: SOUND_HOOK_ATTACH_GROUND, Vol: 1.0f, Pos);
2082 if(Events & COREEVENT_HOOK_HIT_NOHOOK)
2083 m_Sounds.PlayAndRecord(Channel: CSounds::CHN_WORLD, SetId: SOUND_HOOK_NOATTACH, Vol: 1.0f, Pos);
2084 }
2085 }
2086
2087 // check if we want to trigger predicted airjump for dummy
2088 if(AntiPingPlayers() && pDummyChar && Tick > m_aLastNewPredictedTick[!Dummy])
2089 {
2090 m_aLastNewPredictedTick[!Dummy] = Tick;
2091 vec2 Pos = pDummyChar->Core()->m_Pos;
2092 int Events = pDummyChar->Core()->m_TriggeredEvents;
2093 if(g_Config.m_ClPredict && !m_SuppressEvents)
2094 if(Events & COREEVENT_AIR_JUMP)
2095 m_Effects.AirJump(Pos, Alpha: 1.0f);
2096 }
2097 }
2098
2099 // detect mispredictions of other players and make corrections smoother when possible
2100 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)
2101 {
2102 int PredTime = clamp(val: Client()->GetPredictionTime(), lo: 0, hi: 800);
2103 float SmoothPace = 4 - 1.5f * PredTime / 800.f; // smoothing pace (a lower value will make the smoothing quicker)
2104 int64_t Len = 1000 * PredTime * SmoothPace;
2105
2106 for(int i = 0; i < MAX_CLIENTS; i++)
2107 {
2108 if(!m_Snap.m_aCharacters[i].m_Active || i == m_Snap.m_LocalClientId || !m_aLastActive[i])
2109 continue;
2110 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;
2111 vec2 PredErr = (m_aLastPos[i] - NewPos) / (float)minimum(a: Client()->GetPredictionTime(), b: 200);
2112 if(in_range(a: length(a: PredErr), lower: 0.05f, upper: 5.f))
2113 {
2114 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));
2115 vec2 CurPos = mix(
2116 a: vec2(m_Snap.m_aCharacters[i].m_Prev.m_X, m_Snap.m_aCharacters[i].m_Prev.m_Y),
2117 b: vec2(m_Snap.m_aCharacters[i].m_Cur.m_X, m_Snap.m_aCharacters[i].m_Cur.m_Y),
2118 amount: Client()->IntraGameTick(Conn: g_Config.m_ClDummy));
2119 vec2 RenderDiff = PredPos - aBeforeRender[i];
2120 vec2 PredDiff = PredPos - CurPos;
2121
2122 float aMixAmount[2];
2123 for(int j = 0; j < 2; j++)
2124 {
2125 aMixAmount[j] = 1.0f;
2126 if(absolute(a: PredErr[j]) > 0.05f)
2127 {
2128 aMixAmount[j] = 0.0f;
2129 if(absolute(a: RenderDiff[j]) > 0.01f)
2130 {
2131 aMixAmount[j] = 1.f - clamp(val: RenderDiff[j] / PredDiff[j], lo: 0.f, hi: 1.f);
2132 aMixAmount[j] = 1.f - std::pow(x: 1.f - aMixAmount[j], y: 1 / 1.2f);
2133 }
2134 }
2135 int64_t TimePassed = time_get() - m_aClients[i].m_aSmoothStart[j];
2136 if(in_range(a: TimePassed, lower: (int64_t)0, upper: Len - 1))
2137 aMixAmount[j] = minimum(a: aMixAmount[j], b: (float)(TimePassed / (double)Len));
2138 }
2139 for(int j = 0; j < 2; j++)
2140 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])
2141 aMixAmount[j] = aMixAmount[j ^ 1];
2142 for(int j = 0; j < 2; j++)
2143 {
2144 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
2145 int64_t Start = time_get() - (Len - Remaining);
2146 if(!in_range(a: Start + Len, lower: m_aClients[i].m_aSmoothStart[j], upper: m_aClients[i].m_aSmoothStart[j] + Len))
2147 {
2148 m_aClients[i].m_aSmoothStart[j] = Start;
2149 m_aClients[i].m_aSmoothLen[j] = Len;
2150 }
2151 }
2152 }
2153 }
2154 }
2155
2156 for(int i = 0; i < MAX_CLIENTS; i++)
2157 {
2158 if(m_Snap.m_aCharacters[i].m_Active)
2159 {
2160 m_aLastPos[i] = m_aClients[i].m_Predicted.m_Pos;
2161 m_aLastActive[i] = true;
2162 }
2163 else
2164 m_aLastActive[i] = false;
2165 }
2166
2167 if(g_Config.m_Debug && g_Config.m_ClPredict && m_PredictedTick == Client()->PredGameTick(Conn: g_Config.m_ClDummy))
2168 {
2169 CNetObj_CharacterCore Before = {.m_Tick: 0}, Now = {.m_Tick: 0}, BeforePrev = {.m_Tick: 0}, NowPrev = {.m_Tick: 0};
2170 BeforeChar.Write(pObjCore: &Before);
2171 BeforePrevChar.Write(pObjCore: &BeforePrev);
2172 m_PredictedChar.Write(pObjCore: &Now);
2173 m_PredictedPrevChar.Write(pObjCore: &NowPrev);
2174
2175 if(mem_comp(a: &Before, b: &Now, size: sizeof(CNetObj_CharacterCore)) != 0)
2176 {
2177 Console()->Print(Level: IConsole::OUTPUT_LEVEL_DEBUG, pFrom: "client", pStr: "prediction error");
2178 for(unsigned i = 0; i < sizeof(CNetObj_CharacterCore) / sizeof(int); i++)
2179 if(((int *)&Before)[i] != ((int *)&Now)[i])
2180 {
2181 char aBuf[256];
2182 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]);
2183 Console()->Print(Level: IConsole::OUTPUT_LEVEL_DEBUG, pFrom: "client", pStr: aBuf);
2184 }
2185 }
2186 }
2187
2188 m_PredictedTick = Client()->PredGameTick(Conn: g_Config.m_ClDummy);
2189
2190 if(m_NewPredictedTick)
2191 m_Ghost.OnNewPredictedSnapshot();
2192}
2193
2194void CGameClient::OnActivateEditor()
2195{
2196 OnRelease();
2197}
2198
2199CGameClient::CClientStats::CClientStats()
2200{
2201 Reset();
2202}
2203
2204void CGameClient::CClientStats::Reset()
2205{
2206 m_JoinTick = 0;
2207 m_IngameTicks = 0;
2208 m_Active = false;
2209
2210 std::fill(first: std::begin(arr&: m_aFragsWith), last: std::end(arr&: m_aFragsWith), value: 0);
2211 std::fill(first: std::begin(arr&: m_aDeathsFrom), last: std::end(arr&: m_aDeathsFrom), value: 0);
2212 m_Frags = 0;
2213 m_Deaths = 0;
2214 m_Suicides = 0;
2215 m_BestSpree = 0;
2216 m_CurrentSpree = 0;
2217
2218 m_FlagGrabs = 0;
2219 m_FlagCaptures = 0;
2220}
2221
2222void CGameClient::CClientData::UpdateRenderInfo(bool IsTeamPlay)
2223{
2224 m_RenderInfo = m_SkinInfo;
2225
2226 // force team colors
2227 if(IsTeamPlay)
2228 {
2229 m_RenderInfo.m_CustomColoredSkin = true;
2230 const int aTeamColors[2] = {65461, 10223541};
2231 if(m_Team >= TEAM_RED && m_Team <= TEAM_BLUE)
2232 {
2233 m_RenderInfo.m_ColorBody = color_cast<ColorRGBA>(hsl: ColorHSLA(aTeamColors[m_Team]));
2234 m_RenderInfo.m_ColorFeet = color_cast<ColorRGBA>(hsl: ColorHSLA(aTeamColors[m_Team]));
2235 }
2236 else
2237 {
2238 m_RenderInfo.m_ColorBody = color_cast<ColorRGBA>(hsl: ColorHSLA(12829350));
2239 m_RenderInfo.m_ColorFeet = color_cast<ColorRGBA>(hsl: ColorHSLA(12829350));
2240 }
2241 }
2242}
2243
2244void CGameClient::CClientData::Reset()
2245{
2246 m_UseCustomColor = 0;
2247 m_ColorBody = 0;
2248 m_ColorFeet = 0;
2249
2250 m_aName[0] = '\0';
2251 m_aClan[0] = '\0';
2252 m_Country = -1;
2253 m_aSkinName[0] = '\0';
2254 m_SkinColor = 0;
2255 m_Team = 0;
2256 m_Emoticon = 0;
2257 m_EmoticonStartFraction = 0;
2258 m_EmoticonStartTick = -1;
2259
2260 m_Solo = false;
2261 m_Jetpack = false;
2262 m_CollisionDisabled = false;
2263 m_EndlessHook = false;
2264 m_EndlessJump = false;
2265 m_HammerHitDisabled = false;
2266 m_GrenadeHitDisabled = false;
2267 m_LaserHitDisabled = false;
2268 m_ShotgunHitDisabled = false;
2269 m_HookHitDisabled = false;
2270 m_Super = false;
2271 m_HasTelegunGun = false;
2272 m_HasTelegunGrenade = false;
2273 m_HasTelegunLaser = false;
2274 m_FreezeEnd = 0;
2275 m_DeepFrozen = false;
2276 m_LiveFrozen = false;
2277
2278 m_Predicted.Reset();
2279 m_PrevPredicted.Reset();
2280
2281 m_SkinInfo.Reset();
2282 m_RenderInfo.Reset();
2283
2284 m_Angle = 0.0f;
2285 m_Active = false;
2286 m_ChatIgnore = false;
2287 m_EmoticonIgnore = false;
2288 m_Friend = false;
2289 m_Foe = false;
2290
2291 m_AuthLevel = AUTHED_NO;
2292 m_Afk = false;
2293 m_Paused = false;
2294 m_Spec = false;
2295
2296 std::fill(first: std::begin(arr&: m_aSwitchStates), last: std::end(arr&: m_aSwitchStates), value: 0);
2297
2298 m_Snapped.m_Tick = -1;
2299 m_Evolved.m_Tick = -1;
2300
2301 m_RenderCur.m_Tick = -1;
2302 m_RenderPrev.m_Tick = -1;
2303 m_RenderPos = vec2(0.0f, 0.0f);
2304 m_IsPredicted = false;
2305 m_IsPredictedLocal = false;
2306 std::fill(first: std::begin(arr&: m_aSmoothStart), last: std::end(arr&: m_aSmoothStart), value: 0);
2307 std::fill(first: std::begin(arr&: m_aSmoothLen), last: std::end(arr&: m_aSmoothLen), value: 0);
2308 std::fill(first: std::begin(arr&: m_aPredPos), last: std::end(arr&: m_aPredPos), value: vec2(0.0f, 0.0f));
2309 std::fill(first: std::begin(arr&: m_aPredTick), last: std::end(arr&: m_aPredTick), value: 0);
2310 m_SpecCharPresent = false;
2311 m_SpecChar = vec2(0.0f, 0.0f);
2312}
2313
2314void CGameClient::SendSwitchTeam(int Team)
2315{
2316 CNetMsg_Cl_SetTeam Msg;
2317 Msg.m_Team = Team;
2318 Client()->SendPackMsgActive(pMsg: &Msg, Flags: MSGFLAG_VITAL);
2319
2320 if(Team != TEAM_SPECTATORS)
2321 m_Camera.OnReset();
2322}
2323
2324void CGameClient::SendInfo(bool Start)
2325{
2326 if(Start)
2327 {
2328 CNetMsg_Cl_StartInfo Msg;
2329 Msg.m_pName = Client()->PlayerName();
2330 Msg.m_pClan = g_Config.m_PlayerClan;
2331 Msg.m_Country = g_Config.m_PlayerCountry;
2332 Msg.m_pSkin = g_Config.m_ClPlayerSkin;
2333 Msg.m_UseCustomColor = g_Config.m_ClPlayerUseCustomColor;
2334 Msg.m_ColorBody = g_Config.m_ClPlayerColorBody;
2335 Msg.m_ColorFeet = g_Config.m_ClPlayerColorFeet;
2336 CMsgPacker Packer(&Msg);
2337 Msg.Pack(pPacker: &Packer);
2338 Client()->SendMsg(Conn: IClient::CONN_MAIN, pMsg: &Packer, Flags: MSGFLAG_VITAL | MSGFLAG_FLUSH);
2339 m_aCheckInfo[0] = -1;
2340 }
2341 else
2342 {
2343 CNetMsg_Cl_ChangeInfo Msg;
2344 Msg.m_pName = Client()->PlayerName();
2345 Msg.m_pClan = g_Config.m_PlayerClan;
2346 Msg.m_Country = g_Config.m_PlayerCountry;
2347 Msg.m_pSkin = g_Config.m_ClPlayerSkin;
2348 Msg.m_UseCustomColor = g_Config.m_ClPlayerUseCustomColor;
2349 Msg.m_ColorBody = g_Config.m_ClPlayerColorBody;
2350 Msg.m_ColorFeet = g_Config.m_ClPlayerColorFeet;
2351 CMsgPacker Packer(&Msg);
2352 Msg.Pack(pPacker: &Packer);
2353 Client()->SendMsg(Conn: IClient::CONN_MAIN, pMsg: &Packer, Flags: MSGFLAG_VITAL);
2354 m_aCheckInfo[0] = Client()->GameTickSpeed();
2355 }
2356}
2357
2358void CGameClient::SendDummyInfo(bool Start)
2359{
2360 if(Start)
2361 {
2362 CNetMsg_Cl_StartInfo Msg;
2363 Msg.m_pName = Client()->DummyName();
2364 Msg.m_pClan = g_Config.m_ClDummyClan;
2365 Msg.m_Country = g_Config.m_ClDummyCountry;
2366 Msg.m_pSkin = g_Config.m_ClDummySkin;
2367 Msg.m_UseCustomColor = g_Config.m_ClDummyUseCustomColor;
2368 Msg.m_ColorBody = g_Config.m_ClDummyColorBody;
2369 Msg.m_ColorFeet = g_Config.m_ClDummyColorFeet;
2370 CMsgPacker Packer(&Msg);
2371 Msg.Pack(pPacker: &Packer);
2372 Client()->SendMsg(Conn: IClient::CONN_DUMMY, pMsg: &Packer, Flags: MSGFLAG_VITAL);
2373 m_aCheckInfo[1] = -1;
2374 }
2375 else
2376 {
2377 CNetMsg_Cl_ChangeInfo Msg;
2378 Msg.m_pName = Client()->DummyName();
2379 Msg.m_pClan = g_Config.m_ClDummyClan;
2380 Msg.m_Country = g_Config.m_ClDummyCountry;
2381 Msg.m_pSkin = g_Config.m_ClDummySkin;
2382 Msg.m_UseCustomColor = g_Config.m_ClDummyUseCustomColor;
2383 Msg.m_ColorBody = g_Config.m_ClDummyColorBody;
2384 Msg.m_ColorFeet = g_Config.m_ClDummyColorFeet;
2385 CMsgPacker Packer(&Msg);
2386 Msg.Pack(pPacker: &Packer);
2387 Client()->SendMsg(Conn: IClient::CONN_DUMMY, pMsg: &Packer, Flags: MSGFLAG_VITAL);
2388 m_aCheckInfo[1] = Client()->GameTickSpeed();
2389 }
2390}
2391
2392void CGameClient::SendKill(int ClientId) const
2393{
2394 CNetMsg_Cl_Kill Msg;
2395 Client()->SendPackMsgActive(pMsg: &Msg, Flags: MSGFLAG_VITAL);
2396
2397 if(g_Config.m_ClDummyCopyMoves)
2398 {
2399 CMsgPacker MsgP(NETMSGTYPE_CL_KILL, false);
2400 Client()->SendMsg(Conn: !g_Config.m_ClDummy, pMsg: &MsgP, Flags: MSGFLAG_VITAL);
2401 }
2402}
2403
2404void CGameClient::ConTeam(IConsole::IResult *pResult, void *pUserData)
2405{
2406 ((CGameClient *)pUserData)->SendSwitchTeam(Team: pResult->GetInteger(Index: 0));
2407}
2408
2409void CGameClient::ConKill(IConsole::IResult *pResult, void *pUserData)
2410{
2411 ((CGameClient *)pUserData)->SendKill(ClientId: -1);
2412}
2413
2414void CGameClient::ConchainLanguageUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
2415{
2416 CGameClient *pThis = static_cast<CGameClient *>(pUserData);
2417 const bool Changed = pThis->Client()->GlobalTime() && pResult->NumArguments() && str_comp(a: pResult->GetString(Index: 0), b: g_Config.m_ClLanguagefile) != 0;
2418 pfnCallback(pResult, pCallbackUserData);
2419 if(Changed)
2420 {
2421 pThis->OnLanguageChange();
2422 }
2423}
2424
2425void CGameClient::ConchainSpecialInfoupdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
2426{
2427 pfnCallback(pResult, pCallbackUserData);
2428 if(pResult->NumArguments())
2429 ((CGameClient *)pUserData)->SendInfo(Start: false);
2430}
2431
2432void CGameClient::ConchainSpecialDummyInfoupdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
2433{
2434 pfnCallback(pResult, pCallbackUserData);
2435 if(pResult->NumArguments())
2436 ((CGameClient *)pUserData)->SendDummyInfo(Start: false);
2437}
2438
2439void CGameClient::ConchainSpecialDummy(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
2440{
2441 pfnCallback(pResult, pCallbackUserData);
2442 if(pResult->NumArguments())
2443 if(g_Config.m_ClDummy && !((CGameClient *)pUserData)->Client()->DummyConnected())
2444 g_Config.m_ClDummy = 0;
2445}
2446
2447void CGameClient::ConchainClTextEntitiesSize(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
2448{
2449 pfnCallback(pResult, pCallbackUserData);
2450
2451 if(pResult->NumArguments())
2452 {
2453 CGameClient *pGameClient = (CGameClient *)pUserData;
2454 pGameClient->m_MapImages.SetTextureScale(g_Config.m_ClTextEntitiesSize);
2455 }
2456}
2457
2458IGameClient *CreateGameClient()
2459{
2460 return new CGameClient();
2461}
2462
2463int CGameClient::IntersectCharacter(vec2 HookPos, vec2 NewPos, vec2 &NewPos2, int ownId)
2464{
2465 float Distance = 0.0f;
2466 int ClosestId = -1;
2467
2468 const CClientData &OwnClientData = m_aClients[ownId];
2469
2470 for(int i = 0; i < MAX_CLIENTS; i++)
2471 {
2472 if(i == ownId)
2473 continue;
2474
2475 const CClientData &Data = m_aClients[i];
2476
2477 if(!Data.m_Active)
2478 continue;
2479
2480 CNetObj_Character Prev = m_Snap.m_aCharacters[i].m_Prev;
2481 CNetObj_Character Player = m_Snap.m_aCharacters[i].m_Cur;
2482
2483 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));
2484
2485 bool IsOneSuper = Data.m_Super || OwnClientData.m_Super;
2486 bool IsOneSolo = Data.m_Solo || OwnClientData.m_Solo;
2487
2488 if(!IsOneSuper && (!m_Teams.SameTeam(ClientId1: i, ClientId2: ownId) || IsOneSolo || OwnClientData.m_HookHitDisabled))
2489 continue;
2490
2491 vec2 ClosestPoint;
2492 if(closest_point_on_line(line_pointA: HookPos, line_pointB: NewPos, target_point: Position, out_pos&: ClosestPoint))
2493 {
2494 if(distance(a: Position, b: ClosestPoint) < CCharacterCore::PhysicalSize() + 2.0f)
2495 {
2496 if(ClosestId == -1 || distance(a: HookPos, b: Position) < Distance)
2497 {
2498 NewPos2 = ClosestPoint;
2499 ClosestId = i;
2500 Distance = distance(a: HookPos, b: Position);
2501 }
2502 }
2503 }
2504 }
2505
2506 return ClosestId;
2507}
2508
2509ColorRGBA CalculateNameColor(ColorHSLA TextColorHSL)
2510{
2511 return color_cast<ColorRGBA>(hsl: ColorHSLA(TextColorHSL.h, TextColorHSL.s * 0.68f, TextColorHSL.l * 0.81f));
2512}
2513
2514void CGameClient::UpdatePrediction()
2515{
2516 m_GameWorld.m_WorldConfig.m_IsVanilla = m_GameInfo.m_PredictVanilla;
2517 m_GameWorld.m_WorldConfig.m_IsDDRace = m_GameInfo.m_PredictDDRace;
2518 m_GameWorld.m_WorldConfig.m_IsFNG = m_GameInfo.m_PredictFNG;
2519 m_GameWorld.m_WorldConfig.m_PredictDDRace = m_GameInfo.m_PredictDDRace;
2520 m_GameWorld.m_WorldConfig.m_PredictTiles = m_GameInfo.m_PredictDDRace && m_GameInfo.m_PredictDDRaceTiles;
2521 m_GameWorld.m_WorldConfig.m_UseTuneZones = m_GameInfo.m_PredictDDRaceTiles;
2522 m_GameWorld.m_WorldConfig.m_PredictFreeze = g_Config.m_ClPredictFreeze;
2523 m_GameWorld.m_WorldConfig.m_PredictWeapons = AntiPingWeapons();
2524 m_GameWorld.m_WorldConfig.m_BugDDRaceInput = m_GameInfo.m_BugDDRaceInput;
2525 m_GameWorld.m_WorldConfig.m_NoWeakHookAndBounce = m_GameInfo.m_NoWeakHookAndBounce;
2526
2527 // always update default tune zone, even without character
2528 if(!m_GameWorld.m_WorldConfig.m_UseTuneZones)
2529 m_GameWorld.TuningList()[0] = m_aTuning[g_Config.m_ClDummy];
2530
2531 if(!m_Snap.m_pLocalCharacter)
2532 {
2533 if(CCharacter *pLocalChar = m_GameWorld.GetCharacterById(Id: m_Snap.m_LocalClientId))
2534 pLocalChar->Destroy();
2535 return;
2536 }
2537
2538 if(m_Snap.m_pLocalCharacter->m_AmmoCount > 0 && m_Snap.m_pLocalCharacter->m_Weapon != WEAPON_NINJA)
2539 m_GameWorld.m_WorldConfig.m_InfiniteAmmo = false;
2540 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;
2541
2542 // update the tuning/tunezone at the local character position with the latest tunings received before the new snapshot
2543 vec2 LocalCharPos = vec2(m_Snap.m_pLocalCharacter->m_X, m_Snap.m_pLocalCharacter->m_Y);
2544 m_GameWorld.m_Core.m_aTuning[g_Config.m_ClDummy] = m_aTuning[g_Config.m_ClDummy];
2545
2546 if(m_GameWorld.m_WorldConfig.m_UseTuneZones)
2547 {
2548 int TuneZone = Collision()->IsTune(Index: Collision()->GetMapIndex(Pos: LocalCharPos));
2549
2550 if(TuneZone != m_aLocalTuneZone[g_Config.m_ClDummy])
2551 {
2552 // our tunezone changed, expecting tuning message
2553 m_aLocalTuneZone[g_Config.m_ClDummy] = m_aExpectingTuningForZone[g_Config.m_ClDummy] = TuneZone;
2554 m_aExpectingTuningSince[g_Config.m_ClDummy] = 0;
2555 }
2556
2557 if(m_aExpectingTuningForZone[g_Config.m_ClDummy] >= 0)
2558 {
2559 if(m_aReceivedTuning[g_Config.m_ClDummy])
2560 {
2561 m_GameWorld.TuningList()[m_aExpectingTuningForZone[g_Config.m_ClDummy]] = m_aTuning[g_Config.m_ClDummy];
2562 m_aReceivedTuning[g_Config.m_ClDummy] = false;
2563 m_aExpectingTuningForZone[g_Config.m_ClDummy] = -1;
2564 }
2565 else if(m_aExpectingTuningSince[g_Config.m_ClDummy] >= 5)
2566 {
2567 // if we are expecting tuning for more than 10 snaps (less than a quarter of a second)
2568 // it is probably dropped or it was received out of order
2569 // or applied to another tunezone.
2570 // we need to fallback to current tuning to fix ourselves.
2571 m_aExpectingTuningForZone[g_Config.m_ClDummy] = -1;
2572 m_aExpectingTuningSince[g_Config.m_ClDummy] = 0;
2573 m_aReceivedTuning[g_Config.m_ClDummy] = false;
2574 dbg_msg(sys: "tunezone", fmt: "the tuning was missed");
2575 }
2576 else
2577 {
2578 // if we are expecting tuning and have not received one yet.
2579 // do not update any tuning, so we don't apply it to the wrong tunezone.
2580 dbg_msg(sys: "tunezone", fmt: "waiting for tuning for zone %d", m_aExpectingTuningForZone[g_Config.m_ClDummy]);
2581 m_aExpectingTuningSince[g_Config.m_ClDummy]++;
2582 }
2583 }
2584 else
2585 {
2586 // if we have processed what we need, and the tuning is still wrong due to out of order messege
2587 // fix our tuning by using the current one
2588 m_GameWorld.TuningList()[TuneZone] = m_aTuning[g_Config.m_ClDummy];
2589 m_aExpectingTuningSince[g_Config.m_ClDummy] = 0;
2590 m_aReceivedTuning[g_Config.m_ClDummy] = false;
2591 }
2592 }
2593
2594 // if ddnetcharacter is available, ignore server-wide tunings for hook and collision
2595 if(m_Snap.m_aCharacters[m_Snap.m_LocalClientId].m_HasExtendedData)
2596 {
2597 m_GameWorld.m_Core.m_aTuning[g_Config.m_ClDummy].m_PlayerCollision = 1;
2598 m_GameWorld.m_Core.m_aTuning[g_Config.m_ClDummy].m_PlayerHooking = 1;
2599 }
2600
2601 CCharacter *pLocalChar = m_GameWorld.GetCharacterById(Id: m_Snap.m_LocalClientId);
2602 CCharacter *pDummyChar = 0;
2603 if(PredictDummy())
2604 pDummyChar = m_GameWorld.GetCharacterById(Id: m_PredictedDummyId);
2605
2606 // update strong and weak hook
2607 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))
2608 {
2609 if(m_Snap.m_aCharacters[m_Snap.m_LocalClientId].m_HasExtendedData)
2610 {
2611 int aIds[MAX_CLIENTS];
2612 for(int &Id : aIds)
2613 Id = -1;
2614 for(int i = 0; i < MAX_CLIENTS; i++)
2615 if(CCharacter *pChar = m_GameWorld.GetCharacterById(Id: i))
2616 aIds[pChar->GetStrongWeakId()] = i;
2617 for(int Id : aIds)
2618 if(Id >= 0)
2619 m_CharOrder.GiveStrong(c: Id);
2620 }
2621 else
2622 {
2623 // manual detection
2624 DetectStrongHook();
2625 }
2626 for(int i : m_CharOrder.m_Ids)
2627 {
2628 if(CCharacter *pChar = m_GameWorld.GetCharacterById(Id: i))
2629 {
2630 m_GameWorld.RemoveEntity(pEntity: pChar);
2631 m_GameWorld.InsertEntity(pEntity: pChar);
2632 }
2633 }
2634 }
2635
2636 // advance the gameworld to the current gametick
2637 if(pLocalChar && absolute(a: m_GameWorld.GameTick() - Client()->GameTick(Conn: g_Config.m_ClDummy)) < Client()->GameTickSpeed())
2638 {
2639 for(int Tick = m_GameWorld.GameTick() + 1; Tick <= Client()->GameTick(Conn: g_Config.m_ClDummy); Tick++)
2640 {
2641 CNetObj_PlayerInput *pInput = (CNetObj_PlayerInput *)Client()->GetInput(Tick);
2642 CNetObj_PlayerInput *pDummyInput = 0;
2643 if(pDummyChar)
2644 pDummyInput = (CNetObj_PlayerInput *)Client()->GetInput(Tick, IsDummy: 1);
2645 if(pInput)
2646 pLocalChar->OnDirectInput(pNewInput: pInput);
2647 if(pDummyInput)
2648 pDummyChar->OnDirectInput(pNewInput: pDummyInput);
2649 m_GameWorld.m_GameTick = Tick;
2650 if(pInput)
2651 pLocalChar->OnPredictedInput(pNewInput: pInput);
2652 if(pDummyInput)
2653 pDummyChar->OnPredictedInput(pNewInput: pDummyInput);
2654 m_GameWorld.Tick();
2655
2656 for(int i = 0; i < MAX_CLIENTS; i++)
2657 if(CCharacter *pChar = m_GameWorld.GetCharacterById(Id: i))
2658 {
2659 m_aClients[i].m_aPredPos[Tick % 200] = pChar->Core()->m_Pos;
2660 m_aClients[i].m_aPredTick[Tick % 200] = Tick;
2661 }
2662 }
2663 }
2664 else
2665 {
2666 // skip to current gametick
2667 m_GameWorld.m_GameTick = Client()->GameTick(Conn: g_Config.m_ClDummy);
2668 if(pLocalChar)
2669 if(CNetObj_PlayerInput *pInput = (CNetObj_PlayerInput *)Client()->GetInput(Tick: Client()->GameTick(Conn: g_Config.m_ClDummy)))
2670 pLocalChar->SetInput(pInput);
2671 if(pDummyChar)
2672 if(CNetObj_PlayerInput *pInput = (CNetObj_PlayerInput *)Client()->GetInput(Tick: Client()->GameTick(Conn: g_Config.m_ClDummy), IsDummy: 1))
2673 pDummyChar->SetInput(pInput);
2674 }
2675
2676 for(int i = 0; i < MAX_CLIENTS; i++)
2677 if(CCharacter *pChar = m_GameWorld.GetCharacterById(Id: i))
2678 {
2679 m_aClients[i].m_aPredPos[Client()->GameTick(Conn: g_Config.m_ClDummy) % 200] = pChar->Core()->m_Pos;
2680 m_aClients[i].m_aPredTick[Client()->GameTick(Conn: g_Config.m_ClDummy) % 200] = Client()->GameTick(Conn: g_Config.m_ClDummy);
2681 }
2682
2683 // update the local gameworld with the new snapshot
2684 m_GameWorld.NetObjBegin(Teams: m_Teams, LocalClientId: m_Snap.m_LocalClientId);
2685
2686 for(int i = 0; i < MAX_CLIENTS; i++)
2687 if(m_Snap.m_aCharacters[i].m_Active)
2688 {
2689 bool IsLocal = (i == m_Snap.m_LocalClientId || (PredictDummy() && i == m_PredictedDummyId));
2690 int GameTeam = (m_Snap.m_pGameInfoObj && (m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_TEAMS)) ? m_aClients[i].m_Team : i;
2691 m_GameWorld.NetCharAdd(ObjId: i, pChar: &m_Snap.m_aCharacters[i].m_Cur,
2692 pExtended: m_Snap.m_aCharacters[i].m_HasExtendedData ? &m_Snap.m_aCharacters[i].m_ExtendedData : 0,
2693 GameTeam, IsLocal);
2694 }
2695
2696 for(const CSnapEntities &EntData : SnapEntities())
2697 m_GameWorld.NetObjAdd(ObjId: EntData.m_Item.m_Id, ObjType: EntData.m_Item.m_Type, pObjData: EntData.m_pData, pDataEx: EntData.m_pDataEx);
2698
2699 m_GameWorld.NetObjEnd();
2700}
2701
2702void CGameClient::UpdateRenderedCharacters()
2703{
2704 for(int i = 0; i < MAX_CLIENTS; i++)
2705 {
2706 if(!m_Snap.m_aCharacters[i].m_Active)
2707 continue;
2708 m_aClients[i].m_RenderCur = m_Snap.m_aCharacters[i].m_Cur;
2709 m_aClients[i].m_RenderPrev = m_Snap.m_aCharacters[i].m_Prev;
2710 m_aClients[i].m_IsPredicted = false;
2711 m_aClients[i].m_IsPredictedLocal = false;
2712 vec2 UnpredPos = mix(
2713 a: vec2(m_Snap.m_aCharacters[i].m_Prev.m_X, m_Snap.m_aCharacters[i].m_Prev.m_Y),
2714 b: vec2(m_Snap.m_aCharacters[i].m_Cur.m_X, m_Snap.m_aCharacters[i].m_Cur.m_Y),
2715 amount: Client()->IntraGameTick(Conn: g_Config.m_ClDummy));
2716 vec2 Pos = UnpredPos;
2717
2718 CCharacter *pChar = m_PredictedWorld.GetCharacterById(Id: i);
2719 if(Predict() && (i == m_Snap.m_LocalClientId || (AntiPingPlayers() && !IsOtherTeam(ClientId: i))) && pChar)
2720 {
2721 m_aClients[i].m_Predicted.Write(pObjCore: &m_aClients[i].m_RenderCur);
2722 m_aClients[i].m_PrevPredicted.Write(pObjCore: &m_aClients[i].m_RenderPrev);
2723
2724 m_aClients[i].m_IsPredicted = true;
2725
2726 Pos = mix(
2727 a: vec2(m_aClients[i].m_RenderPrev.m_X, m_aClients[i].m_RenderPrev.m_Y),
2728 b: vec2(m_aClients[i].m_RenderCur.m_X, m_aClients[i].m_RenderCur.m_Y),
2729 amount: m_aClients[i].m_IsPredicted ? Client()->PredIntraGameTick(Conn: g_Config.m_ClDummy) : Client()->IntraGameTick(Conn: g_Config.m_ClDummy));
2730
2731 if(i == m_Snap.m_LocalClientId)
2732 {
2733 m_aClients[i].m_IsPredictedLocal = true;
2734 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))
2735 {
2736 m_aClients[i].m_RenderCur.m_AttackTick = pChar->GetAttackTick();
2737 if(m_Snap.m_aCharacters[i].m_Cur.m_Weapon != WEAPON_NINJA && !(pChar->m_NinjaJetpack && pChar->Core()->m_ActiveWeapon == WEAPON_GUN))
2738 m_aClients[i].m_RenderCur.m_Weapon = m_aClients[i].m_Predicted.m_ActiveWeapon;
2739 }
2740 }
2741 else
2742 {
2743 // use unpredicted values for other players
2744 m_aClients[i].m_RenderPrev.m_Angle = m_Snap.m_aCharacters[i].m_Prev.m_Angle;
2745 m_aClients[i].m_RenderCur.m_Angle = m_Snap.m_aCharacters[i].m_Cur.m_Angle;
2746
2747 if(g_Config.m_ClAntiPingSmooth)
2748 Pos = GetSmoothPos(ClientId: i);
2749 }
2750 }
2751 m_Snap.m_aCharacters[i].m_Position = Pos;
2752 m_aClients[i].m_RenderPos = Pos;
2753 if(Predict() && i == m_Snap.m_LocalClientId)
2754 m_LocalCharacterPos = Pos;
2755 }
2756}
2757
2758void CGameClient::DetectStrongHook()
2759{
2760 // attempt to detect strong/weak between players
2761 for(int FromPlayer = 0; FromPlayer < MAX_CLIENTS; FromPlayer++)
2762 {
2763 if(!m_Snap.m_aCharacters[FromPlayer].m_Active)
2764 continue;
2765 int ToPlayer = m_Snap.m_aCharacters[FromPlayer].m_Prev.m_HookedPlayer;
2766 if(ToPlayer < 0 || ToPlayer >= MAX_CLIENTS || !m_Snap.m_aCharacters[ToPlayer].m_Active || ToPlayer != m_Snap.m_aCharacters[FromPlayer].m_Cur.m_HookedPlayer)
2767 continue;
2768 if(absolute(a: minimum(a: m_aLastUpdateTick[ToPlayer], b: m_aLastUpdateTick[FromPlayer]) - Client()->GameTick(Conn: g_Config.m_ClDummy)) < Client()->GameTickSpeed() / 4)
2769 continue;
2770 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)
2771 continue;
2772
2773 CCharacter *pFromCharWorld = m_GameWorld.GetCharacterById(Id: FromPlayer);
2774 CCharacter *pToCharWorld = m_GameWorld.GetCharacterById(Id: ToPlayer);
2775 if(!pFromCharWorld || !pToCharWorld)
2776 continue;
2777
2778 m_aLastUpdateTick[ToPlayer] = m_aLastUpdateTick[FromPlayer] = Client()->GameTick(Conn: g_Config.m_ClDummy);
2779
2780 float aPredictErr[2];
2781 CCharacterCore ToCharCur;
2782 ToCharCur.Read(pObjCore: &m_Snap.m_aCharacters[ToPlayer].m_Cur);
2783
2784 CWorldCore World;
2785 World.m_aTuning[g_Config.m_ClDummy] = m_aTuning[g_Config.m_ClDummy];
2786
2787 for(int dir = 0; dir < 2; dir++)
2788 {
2789 CCharacterCore ToChar = pFromCharWorld->GetCore();
2790 ToChar.Init(pWorld: &World, pCollision: Collision(), pTeams: &m_Teams);
2791 World.m_apCharacters[ToPlayer] = &ToChar;
2792 ToChar.Read(pObjCore: &m_Snap.m_aCharacters[ToPlayer].m_Prev);
2793
2794 CCharacterCore FromChar = pFromCharWorld->GetCore();
2795 FromChar.Init(pWorld: &World, pCollision: Collision(), pTeams: &m_Teams);
2796 World.m_apCharacters[FromPlayer] = &FromChar;
2797 FromChar.Read(pObjCore: &m_Snap.m_aCharacters[FromPlayer].m_Prev);
2798
2799 for(int Tick = Client()->PrevGameTick(Conn: g_Config.m_ClDummy); Tick < Client()->GameTick(Conn: g_Config.m_ClDummy); Tick++)
2800 {
2801 if(dir == 0)
2802 {
2803 FromChar.Tick(UseInput: false);
2804 ToChar.Tick(UseInput: false);
2805 }
2806 else
2807 {
2808 ToChar.Tick(UseInput: false);
2809 FromChar.Tick(UseInput: false);
2810 }
2811 FromChar.Move();
2812 FromChar.Quantize();
2813 ToChar.Move();
2814 ToChar.Quantize();
2815 }
2816 aPredictErr[dir] = distance(a: ToChar.m_Vel, b: ToCharCur.m_Vel);
2817 }
2818 const float LOW = 0.0001f;
2819 const float HIGH = 0.07f;
2820 if(aPredictErr[1] < LOW && aPredictErr[0] > HIGH)
2821 {
2822 if(m_CharOrder.HasStrongAgainst(From: ToPlayer, To: FromPlayer))
2823 {
2824 if(ToPlayer != m_Snap.m_LocalClientId)
2825 m_CharOrder.GiveWeak(c: ToPlayer);
2826 else
2827 m_CharOrder.GiveStrong(c: FromPlayer);
2828 }
2829 }
2830 else if(aPredictErr[0] < LOW && aPredictErr[1] > HIGH)
2831 {
2832 if(m_CharOrder.HasStrongAgainst(From: FromPlayer, To: ToPlayer))
2833 {
2834 if(ToPlayer != m_Snap.m_LocalClientId)
2835 m_CharOrder.GiveStrong(c: ToPlayer);
2836 else
2837 m_CharOrder.GiveWeak(c: FromPlayer);
2838 }
2839 }
2840 }
2841}
2842
2843vec2 CGameClient::GetSmoothPos(int ClientId)
2844{
2845 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));
2846 int64_t Now = time_get();
2847 for(int i = 0; i < 2; i++)
2848 {
2849 int64_t Len = clamp(val: m_aClients[ClientId].m_aSmoothLen[i], lo: (int64_t)1, hi: time_freq());
2850 int64_t TimePassed = Now - m_aClients[ClientId].m_aSmoothStart[i];
2851 if(in_range(a: TimePassed, lower: (int64_t)0, upper: Len - 1))
2852 {
2853 float MixAmount = 1.f - std::pow(x: 1.f - TimePassed / (float)Len, y: 1.2f);
2854 int SmoothTick;
2855 float SmoothIntra;
2856 Client()->GetSmoothTick(pSmoothTick: &SmoothTick, pSmoothIntraTick: &SmoothIntra, MixAmount);
2857 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))
2858 Pos[i] = mix(a: m_aClients[ClientId].m_aPredPos[(SmoothTick - 1) % 200][i], b: m_aClients[ClientId].m_aPredPos[SmoothTick % 200][i], amount: SmoothIntra);
2859 }
2860 }
2861 return Pos;
2862}
2863
2864void CGameClient::Echo(const char *pString)
2865{
2866 m_Chat.Echo(pString);
2867}
2868
2869bool CGameClient::IsOtherTeam(int ClientId) const
2870{
2871 bool Local = m_Snap.m_LocalClientId == ClientId;
2872
2873 if(m_Snap.m_LocalClientId < 0)
2874 return false;
2875 else if((m_Snap.m_SpecInfo.m_Active && m_Snap.m_SpecInfo.m_SpectatorId == SPEC_FREEVIEW) || ClientId < 0)
2876 return false;
2877 else if(m_Snap.m_SpecInfo.m_Active && m_Snap.m_SpecInfo.m_SpectatorId != SPEC_FREEVIEW)
2878 {
2879 if(m_Teams.Team(ClientId) == TEAM_SUPER || m_Teams.Team(ClientId: m_Snap.m_SpecInfo.m_SpectatorId) == TEAM_SUPER)
2880 return false;
2881 return m_Teams.Team(ClientId) != m_Teams.Team(ClientId: m_Snap.m_SpecInfo.m_SpectatorId);
2882 }
2883 else if((m_aClients[m_Snap.m_LocalClientId].m_Solo || m_aClients[ClientId].m_Solo) && !Local)
2884 return true;
2885
2886 if(m_Teams.Team(ClientId) == TEAM_SUPER || m_Teams.Team(ClientId: m_Snap.m_LocalClientId) == TEAM_SUPER)
2887 return false;
2888
2889 return m_Teams.Team(ClientId) != m_Teams.Team(ClientId: m_Snap.m_LocalClientId);
2890}
2891
2892int CGameClient::SwitchStateTeam() const
2893{
2894 if(m_aSwitchStateTeam[g_Config.m_ClDummy] >= 0)
2895 return m_aSwitchStateTeam[g_Config.m_ClDummy];
2896 else if(m_Snap.m_LocalClientId < 0)
2897 return 0;
2898 else if(m_Snap.m_SpecInfo.m_Active && m_Snap.m_SpecInfo.m_SpectatorId != SPEC_FREEVIEW)
2899 return m_Teams.Team(ClientId: m_Snap.m_SpecInfo.m_SpectatorId);
2900 return m_Teams.Team(ClientId: m_Snap.m_LocalClientId);
2901}
2902
2903bool CGameClient::IsLocalCharSuper() const
2904{
2905 if(m_Snap.m_LocalClientId < 0)
2906 return false;
2907 return m_aClients[m_Snap.m_LocalClientId].m_Super;
2908}
2909
2910void CGameClient::LoadGameSkin(const char *pPath, bool AsDir)
2911{
2912 if(m_GameSkinLoaded)
2913 {
2914 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteHealthFull);
2915 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteHealthEmpty);
2916 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteArmorFull);
2917 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteArmorEmpty);
2918
2919 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponHammerCursor);
2920 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponGunCursor);
2921 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponShotgunCursor);
2922 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponGrenadeCursor);
2923 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponNinjaCursor);
2924 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponLaserCursor);
2925
2926 for(auto &SpriteWeaponCursor : m_GameSkin.m_aSpriteWeaponCursors)
2927 {
2928 SpriteWeaponCursor = IGraphics::CTextureHandle();
2929 }
2930
2931 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteHookChain);
2932 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteHookHead);
2933 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponHammer);
2934 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponGun);
2935 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponShotgun);
2936 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponGrenade);
2937 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponNinja);
2938 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponLaser);
2939
2940 for(auto &SpriteWeapon : m_GameSkin.m_aSpriteWeapons)
2941 {
2942 SpriteWeapon = IGraphics::CTextureHandle();
2943 }
2944
2945 for(auto &SpriteParticle : m_GameSkin.m_aSpriteParticles)
2946 {
2947 Graphics()->UnloadTexture(pIndex: &SpriteParticle);
2948 }
2949
2950 for(auto &SpriteStar : m_GameSkin.m_aSpriteStars)
2951 {
2952 Graphics()->UnloadTexture(pIndex: &SpriteStar);
2953 }
2954
2955 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponGunProjectile);
2956 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponShotgunProjectile);
2957 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponGrenadeProjectile);
2958 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponHammerProjectile);
2959 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponNinjaProjectile);
2960 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteWeaponLaserProjectile);
2961
2962 for(auto &SpriteWeaponProjectile : m_GameSkin.m_aSpriteWeaponProjectiles)
2963 {
2964 SpriteWeaponProjectile = IGraphics::CTextureHandle();
2965 }
2966
2967 for(int i = 0; i < 3; ++i)
2968 {
2969 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_aSpriteWeaponGunMuzzles[i]);
2970 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_aSpriteWeaponShotgunMuzzles[i]);
2971 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_aaSpriteWeaponNinjaMuzzles[i]);
2972
2973 for(auto &SpriteWeaponsMuzzle : m_GameSkin.m_aaSpriteWeaponsMuzzles)
2974 {
2975 SpriteWeaponsMuzzle[i] = IGraphics::CTextureHandle();
2976 }
2977 }
2978
2979 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpritePickupHealth);
2980 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpritePickupArmor);
2981 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpritePickupArmorShotgun);
2982 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpritePickupArmorGrenade);
2983 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpritePickupArmorLaser);
2984 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpritePickupArmorNinja);
2985 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpritePickupGrenade);
2986 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpritePickupShotgun);
2987 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpritePickupLaser);
2988 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpritePickupNinja);
2989 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpritePickupGun);
2990 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpritePickupHammer);
2991
2992 for(auto &SpritePickupWeapon : m_GameSkin.m_aSpritePickupWeapons)
2993 {
2994 SpritePickupWeapon = IGraphics::CTextureHandle();
2995 }
2996
2997 for(auto &SpritePickupWeaponArmor : m_GameSkin.m_aSpritePickupWeaponArmor)
2998 {
2999 SpritePickupWeaponArmor = IGraphics::CTextureHandle();
3000 }
3001
3002 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteFlagBlue);
3003 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteFlagRed);
3004
3005 if(m_GameSkin.IsSixup())
3006 {
3007 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteNinjaBarFullLeft);
3008 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteNinjaBarFull);
3009 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteNinjaBarEmpty);
3010 Graphics()->UnloadTexture(pIndex: &m_GameSkin.m_SpriteNinjaBarEmptyRight);
3011 }
3012
3013 m_GameSkinLoaded = false;
3014 }
3015
3016 char aPath[IO_MAX_PATH_LENGTH];
3017 bool IsDefault = false;
3018 if(str_comp(a: pPath, b: "default") == 0)
3019 {
3020 str_copy(dst&: aPath, src: g_pData->m_aImages[IMAGE_GAME].m_pFilename);
3021 IsDefault = true;
3022 }
3023 else
3024 {
3025 if(AsDir)
3026 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "assets/game/%s/%s", pPath, g_pData->m_aImages[IMAGE_GAME].m_pFilename);
3027 else
3028 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "assets/game/%s.png", pPath);
3029 }
3030
3031 CImageInfo ImgInfo;
3032 bool PngLoaded = Graphics()->LoadPng(Image&: ImgInfo, pFilename: aPath, StorageType: IStorage::TYPE_ALL);
3033 if(!PngLoaded && !IsDefault)
3034 {
3035 if(AsDir)
3036 LoadGameSkin(pPath: "default");
3037 else
3038 LoadGameSkin(pPath, AsDir: true);
3039 }
3040 else if(PngLoaded && Graphics()->CheckImageDivisibility(pFileName: aPath, Img&: 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(pFileName: aPath, Img&: ImgInfo))
3041 {
3042 m_GameSkin.m_SpriteHealthFull = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HEALTH_FULL]);
3043 m_GameSkin.m_SpriteHealthEmpty = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HEALTH_EMPTY]);
3044 m_GameSkin.m_SpriteArmorFull = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_ARMOR_FULL]);
3045 m_GameSkin.m_SpriteArmorEmpty = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_ARMOR_EMPTY]);
3046
3047 m_GameSkin.m_SpriteWeaponHammerCursor = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_HAMMER_CURSOR]);
3048 m_GameSkin.m_SpriteWeaponGunCursor = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_GUN_CURSOR]);
3049 m_GameSkin.m_SpriteWeaponShotgunCursor = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_SHOTGUN_CURSOR]);
3050 m_GameSkin.m_SpriteWeaponGrenadeCursor = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_GRENADE_CURSOR]);
3051 m_GameSkin.m_SpriteWeaponNinjaCursor = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_NINJA_CURSOR]);
3052 m_GameSkin.m_SpriteWeaponLaserCursor = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_LASER_CURSOR]);
3053
3054 m_GameSkin.m_aSpriteWeaponCursors[0] = m_GameSkin.m_SpriteWeaponHammerCursor;
3055 m_GameSkin.m_aSpriteWeaponCursors[1] = m_GameSkin.m_SpriteWeaponGunCursor;
3056 m_GameSkin.m_aSpriteWeaponCursors[2] = m_GameSkin.m_SpriteWeaponShotgunCursor;
3057 m_GameSkin.m_aSpriteWeaponCursors[3] = m_GameSkin.m_SpriteWeaponGrenadeCursor;
3058 m_GameSkin.m_aSpriteWeaponCursors[4] = m_GameSkin.m_SpriteWeaponLaserCursor;
3059 m_GameSkin.m_aSpriteWeaponCursors[5] = m_GameSkin.m_SpriteWeaponNinjaCursor;
3060
3061 // weapons and hook
3062 m_GameSkin.m_SpriteHookChain = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HOOK_CHAIN]);
3063 m_GameSkin.m_SpriteHookHead = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HOOK_HEAD]);
3064 m_GameSkin.m_SpriteWeaponHammer = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_HAMMER_BODY]);
3065 m_GameSkin.m_SpriteWeaponGun = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_GUN_BODY]);
3066 m_GameSkin.m_SpriteWeaponShotgun = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_SHOTGUN_BODY]);
3067 m_GameSkin.m_SpriteWeaponGrenade = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_GRENADE_BODY]);
3068 m_GameSkin.m_SpriteWeaponNinja = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_NINJA_BODY]);
3069 m_GameSkin.m_SpriteWeaponLaser = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_LASER_BODY]);
3070
3071 m_GameSkin.m_aSpriteWeapons[0] = m_GameSkin.m_SpriteWeaponHammer;
3072 m_GameSkin.m_aSpriteWeapons[1] = m_GameSkin.m_SpriteWeaponGun;
3073 m_GameSkin.m_aSpriteWeapons[2] = m_GameSkin.m_SpriteWeaponShotgun;
3074 m_GameSkin.m_aSpriteWeapons[3] = m_GameSkin.m_SpriteWeaponGrenade;
3075 m_GameSkin.m_aSpriteWeapons[4] = m_GameSkin.m_SpriteWeaponLaser;
3076 m_GameSkin.m_aSpriteWeapons[5] = m_GameSkin.m_SpriteWeaponNinja;
3077
3078 // particles
3079 for(int i = 0; i < 9; ++i)
3080 {
3081 m_GameSkin.m_aSpriteParticles[i] = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PART1 + i]);
3082 }
3083
3084 // stars
3085 for(int i = 0; i < 3; ++i)
3086 {
3087 m_GameSkin.m_aSpriteStars[i] = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_STAR1 + i]);
3088 }
3089
3090 // projectiles
3091 m_GameSkin.m_SpriteWeaponGunProjectile = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_GUN_PROJ]);
3092 m_GameSkin.m_SpriteWeaponShotgunProjectile = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_SHOTGUN_PROJ]);
3093 m_GameSkin.m_SpriteWeaponGrenadeProjectile = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_GRENADE_PROJ]);
3094
3095 // these weapons have no projectiles
3096 m_GameSkin.m_SpriteWeaponHammerProjectile = IGraphics::CTextureHandle();
3097 m_GameSkin.m_SpriteWeaponNinjaProjectile = IGraphics::CTextureHandle();
3098
3099 m_GameSkin.m_SpriteWeaponLaserProjectile = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_LASER_PROJ]);
3100
3101 m_GameSkin.m_aSpriteWeaponProjectiles[0] = m_GameSkin.m_SpriteWeaponHammerProjectile;
3102 m_GameSkin.m_aSpriteWeaponProjectiles[1] = m_GameSkin.m_SpriteWeaponGunProjectile;
3103 m_GameSkin.m_aSpriteWeaponProjectiles[2] = m_GameSkin.m_SpriteWeaponShotgunProjectile;
3104 m_GameSkin.m_aSpriteWeaponProjectiles[3] = m_GameSkin.m_SpriteWeaponGrenadeProjectile;
3105 m_GameSkin.m_aSpriteWeaponProjectiles[4] = m_GameSkin.m_SpriteWeaponLaserProjectile;
3106 m_GameSkin.m_aSpriteWeaponProjectiles[5] = m_GameSkin.m_SpriteWeaponNinjaProjectile;
3107
3108 // muzzles
3109 for(int i = 0; i < 3; ++i)
3110 {
3111 m_GameSkin.m_aSpriteWeaponGunMuzzles[i] = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_GUN_MUZZLE1 + i]);
3112 m_GameSkin.m_aSpriteWeaponShotgunMuzzles[i] = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_SHOTGUN_MUZZLE1 + i]);
3113 m_GameSkin.m_aaSpriteWeaponNinjaMuzzles[i] = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_WEAPON_NINJA_MUZZLE1 + i]);
3114
3115 m_GameSkin.m_aaSpriteWeaponsMuzzles[1][i] = m_GameSkin.m_aSpriteWeaponGunMuzzles[i];
3116 m_GameSkin.m_aaSpriteWeaponsMuzzles[2][i] = m_GameSkin.m_aSpriteWeaponShotgunMuzzles[i];
3117 m_GameSkin.m_aaSpriteWeaponsMuzzles[5][i] = m_GameSkin.m_aaSpriteWeaponNinjaMuzzles[i];
3118 }
3119
3120 // pickups
3121 m_GameSkin.m_SpritePickupHealth = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PICKUP_HEALTH]);
3122 m_GameSkin.m_SpritePickupArmor = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PICKUP_ARMOR]);
3123 m_GameSkin.m_SpritePickupHammer = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PICKUP_HAMMER]);
3124 m_GameSkin.m_SpritePickupGun = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PICKUP_GUN]);
3125 m_GameSkin.m_SpritePickupShotgun = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PICKUP_SHOTGUN]);
3126 m_GameSkin.m_SpritePickupGrenade = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PICKUP_GRENADE]);
3127 m_GameSkin.m_SpritePickupLaser = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PICKUP_LASER]);
3128 m_GameSkin.m_SpritePickupNinja = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PICKUP_NINJA]);
3129 m_GameSkin.m_SpritePickupArmorShotgun = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PICKUP_ARMOR_SHOTGUN]);
3130 m_GameSkin.m_SpritePickupArmorGrenade = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PICKUP_ARMOR_GRENADE]);
3131 m_GameSkin.m_SpritePickupArmorNinja = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PICKUP_ARMOR_NINJA]);
3132 m_GameSkin.m_SpritePickupArmorLaser = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PICKUP_ARMOR_LASER]);
3133
3134 m_GameSkin.m_aSpritePickupWeapons[0] = m_GameSkin.m_SpritePickupHammer;
3135 m_GameSkin.m_aSpritePickupWeapons[1] = m_GameSkin.m_SpritePickupGun;
3136 m_GameSkin.m_aSpritePickupWeapons[2] = m_GameSkin.m_SpritePickupShotgun;
3137 m_GameSkin.m_aSpritePickupWeapons[3] = m_GameSkin.m_SpritePickupGrenade;
3138 m_GameSkin.m_aSpritePickupWeapons[4] = m_GameSkin.m_SpritePickupLaser;
3139 m_GameSkin.m_aSpritePickupWeapons[5] = m_GameSkin.m_SpritePickupNinja;
3140
3141 m_GameSkin.m_aSpritePickupWeaponArmor[0] = m_GameSkin.m_SpritePickupArmorShotgun;
3142 m_GameSkin.m_aSpritePickupWeaponArmor[1] = m_GameSkin.m_SpritePickupArmorGrenade;
3143 m_GameSkin.m_aSpritePickupWeaponArmor[2] = m_GameSkin.m_SpritePickupArmorNinja;
3144 m_GameSkin.m_aSpritePickupWeaponArmor[3] = m_GameSkin.m_SpritePickupArmorLaser;
3145
3146 // flags
3147 m_GameSkin.m_SpriteFlagBlue = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_FLAG_BLUE]);
3148 m_GameSkin.m_SpriteFlagRed = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_FLAG_RED]);
3149
3150 // ninja bar (0.7)
3151 if(!Graphics()->IsSpriteTextureFullyTransparent(FromImageInfo: ImgInfo, pSprite: &client_data7::g_pData->m_aSprites[client_data7::SPRITE_NINJA_BAR_FULL_LEFT]) ||
3152 !Graphics()->IsSpriteTextureFullyTransparent(FromImageInfo: ImgInfo, pSprite: &client_data7::g_pData->m_aSprites[client_data7::SPRITE_NINJA_BAR_FULL]) ||
3153 !Graphics()->IsSpriteTextureFullyTransparent(FromImageInfo: ImgInfo, pSprite: &client_data7::g_pData->m_aSprites[client_data7::SPRITE_NINJA_BAR_EMPTY]) ||
3154 !Graphics()->IsSpriteTextureFullyTransparent(FromImageInfo: ImgInfo, pSprite: &client_data7::g_pData->m_aSprites[client_data7::SPRITE_NINJA_BAR_EMPTY_RIGHT]))
3155 {
3156 m_GameSkin.m_SpriteNinjaBarFullLeft = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &client_data7::g_pData->m_aSprites[client_data7::SPRITE_NINJA_BAR_FULL_LEFT]);
3157 m_GameSkin.m_SpriteNinjaBarFull = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &client_data7::g_pData->m_aSprites[client_data7::SPRITE_NINJA_BAR_FULL]);
3158 m_GameSkin.m_SpriteNinjaBarEmpty = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &client_data7::g_pData->m_aSprites[client_data7::SPRITE_NINJA_BAR_EMPTY]);
3159 m_GameSkin.m_SpriteNinjaBarEmptyRight = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &client_data7::g_pData->m_aSprites[client_data7::SPRITE_NINJA_BAR_EMPTY_RIGHT]);
3160 }
3161
3162 m_GameSkinLoaded = true;
3163 ImgInfo.Free();
3164 }
3165}
3166
3167void CGameClient::LoadEmoticonsSkin(const char *pPath, bool AsDir)
3168{
3169 if(m_EmoticonsSkinLoaded)
3170 {
3171 for(auto &SpriteEmoticon : m_EmoticonsSkin.m_aSpriteEmoticons)
3172 Graphics()->UnloadTexture(pIndex: &SpriteEmoticon);
3173
3174 m_EmoticonsSkinLoaded = false;
3175 }
3176
3177 char aPath[IO_MAX_PATH_LENGTH];
3178 bool IsDefault = false;
3179 if(str_comp(a: pPath, b: "default") == 0)
3180 {
3181 str_copy(dst&: aPath, src: g_pData->m_aImages[IMAGE_EMOTICONS].m_pFilename);
3182 IsDefault = true;
3183 }
3184 else
3185 {
3186 if(AsDir)
3187 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "assets/emoticons/%s/%s", pPath, g_pData->m_aImages[IMAGE_EMOTICONS].m_pFilename);
3188 else
3189 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "assets/emoticons/%s.png", pPath);
3190 }
3191
3192 CImageInfo ImgInfo;
3193 bool PngLoaded = Graphics()->LoadPng(Image&: ImgInfo, pFilename: aPath, StorageType: IStorage::TYPE_ALL);
3194 if(!PngLoaded && !IsDefault)
3195 {
3196 if(AsDir)
3197 LoadEmoticonsSkin(pPath: "default");
3198 else
3199 LoadEmoticonsSkin(pPath, AsDir: true);
3200 }
3201 else if(PngLoaded && Graphics()->CheckImageDivisibility(pFileName: aPath, Img&: 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(pFileName: aPath, Img&: ImgInfo))
3202 {
3203 for(int i = 0; i < 16; ++i)
3204 m_EmoticonsSkin.m_aSpriteEmoticons[i] = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_OOP + i]);
3205
3206 m_EmoticonsSkinLoaded = true;
3207 ImgInfo.Free();
3208 }
3209}
3210
3211void CGameClient::LoadParticlesSkin(const char *pPath, bool AsDir)
3212{
3213 if(m_ParticlesSkinLoaded)
3214 {
3215 Graphics()->UnloadTexture(pIndex: &m_ParticlesSkin.m_SpriteParticleSlice);
3216 Graphics()->UnloadTexture(pIndex: &m_ParticlesSkin.m_SpriteParticleBall);
3217 for(auto &SpriteParticleSplat : m_ParticlesSkin.m_aSpriteParticleSplat)
3218 Graphics()->UnloadTexture(pIndex: &SpriteParticleSplat);
3219 Graphics()->UnloadTexture(pIndex: &m_ParticlesSkin.m_SpriteParticleSmoke);
3220 Graphics()->UnloadTexture(pIndex: &m_ParticlesSkin.m_SpriteParticleShell);
3221 Graphics()->UnloadTexture(pIndex: &m_ParticlesSkin.m_SpriteParticleExpl);
3222 Graphics()->UnloadTexture(pIndex: &m_ParticlesSkin.m_SpriteParticleAirJump);
3223 Graphics()->UnloadTexture(pIndex: &m_ParticlesSkin.m_SpriteParticleHit);
3224
3225 for(auto &SpriteParticle : m_ParticlesSkin.m_aSpriteParticles)
3226 SpriteParticle = IGraphics::CTextureHandle();
3227
3228 m_ParticlesSkinLoaded = false;
3229 }
3230
3231 char aPath[IO_MAX_PATH_LENGTH];
3232 bool IsDefault = false;
3233 if(str_comp(a: pPath, b: "default") == 0)
3234 {
3235 str_copy(dst&: aPath, src: g_pData->m_aImages[IMAGE_PARTICLES].m_pFilename);
3236 IsDefault = true;
3237 }
3238 else
3239 {
3240 if(AsDir)
3241 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "assets/particles/%s/%s", pPath, g_pData->m_aImages[IMAGE_PARTICLES].m_pFilename);
3242 else
3243 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "assets/particles/%s.png", pPath);
3244 }
3245
3246 CImageInfo ImgInfo;
3247 bool PngLoaded = Graphics()->LoadPng(Image&: ImgInfo, pFilename: aPath, StorageType: IStorage::TYPE_ALL);
3248 if(!PngLoaded && !IsDefault)
3249 {
3250 if(AsDir)
3251 LoadParticlesSkin(pPath: "default");
3252 else
3253 LoadParticlesSkin(pPath, AsDir: true);
3254 }
3255 else if(PngLoaded && Graphics()->CheckImageDivisibility(pFileName: aPath, Img&: 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(pFileName: aPath, Img&: ImgInfo))
3256 {
3257 m_ParticlesSkin.m_SpriteParticleSlice = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PART_SLICE]);
3258 m_ParticlesSkin.m_SpriteParticleBall = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PART_BALL]);
3259 for(int i = 0; i < 3; ++i)
3260 m_ParticlesSkin.m_aSpriteParticleSplat[i] = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PART_SPLAT01 + i]);
3261 m_ParticlesSkin.m_SpriteParticleSmoke = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PART_SMOKE]);
3262 m_ParticlesSkin.m_SpriteParticleShell = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PART_SHELL]);
3263 m_ParticlesSkin.m_SpriteParticleExpl = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PART_EXPL01]);
3264 m_ParticlesSkin.m_SpriteParticleAirJump = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PART_AIRJUMP]);
3265 m_ParticlesSkin.m_SpriteParticleHit = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PART_HIT01]);
3266
3267 m_ParticlesSkin.m_aSpriteParticles[0] = m_ParticlesSkin.m_SpriteParticleSlice;
3268 m_ParticlesSkin.m_aSpriteParticles[1] = m_ParticlesSkin.m_SpriteParticleBall;
3269 for(int i = 0; i < 3; ++i)
3270 m_ParticlesSkin.m_aSpriteParticles[2 + i] = m_ParticlesSkin.m_aSpriteParticleSplat[i];
3271 m_ParticlesSkin.m_aSpriteParticles[5] = m_ParticlesSkin.m_SpriteParticleSmoke;
3272 m_ParticlesSkin.m_aSpriteParticles[6] = m_ParticlesSkin.m_SpriteParticleShell;
3273 m_ParticlesSkin.m_aSpriteParticles[7] = m_ParticlesSkin.m_SpriteParticleExpl;
3274 m_ParticlesSkin.m_aSpriteParticles[8] = m_ParticlesSkin.m_SpriteParticleAirJump;
3275 m_ParticlesSkin.m_aSpriteParticles[9] = m_ParticlesSkin.m_SpriteParticleHit;
3276
3277 m_ParticlesSkinLoaded = true;
3278 ImgInfo.Free();
3279 }
3280}
3281
3282void CGameClient::LoadHudSkin(const char *pPath, bool AsDir)
3283{
3284 if(m_HudSkinLoaded)
3285 {
3286 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudAirjump);
3287 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudAirjumpEmpty);
3288 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudSolo);
3289 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudCollisionDisabled);
3290 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudEndlessJump);
3291 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudEndlessHook);
3292 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudJetpack);
3293 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudFreezeBarFullLeft);
3294 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudFreezeBarFull);
3295 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudFreezeBarEmpty);
3296 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudFreezeBarEmptyRight);
3297 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudNinjaBarFullLeft);
3298 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudNinjaBarFull);
3299 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudNinjaBarEmpty);
3300 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudNinjaBarEmptyRight);
3301 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudHookHitDisabled);
3302 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudHammerHitDisabled);
3303 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudShotgunHitDisabled);
3304 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudGrenadeHitDisabled);
3305 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudLaserHitDisabled);
3306 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudGunHitDisabled);
3307 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudDeepFrozen);
3308 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudLiveFrozen);
3309 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudTeleportGrenade);
3310 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudTeleportGun);
3311 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudTeleportLaser);
3312 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudPracticeMode);
3313 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudLockMode);
3314 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudTeam0Mode);
3315 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudDummyHammer);
3316 Graphics()->UnloadTexture(pIndex: &m_HudSkin.m_SpriteHudDummyCopy);
3317 m_HudSkinLoaded = false;
3318 }
3319
3320 char aPath[IO_MAX_PATH_LENGTH];
3321 bool IsDefault = false;
3322 if(str_comp(a: pPath, b: "default") == 0)
3323 {
3324 str_copy(dst&: aPath, src: g_pData->m_aImages[IMAGE_HUD].m_pFilename);
3325 IsDefault = true;
3326 }
3327 else
3328 {
3329 if(AsDir)
3330 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "assets/hud/%s/%s", pPath, g_pData->m_aImages[IMAGE_HUD].m_pFilename);
3331 else
3332 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "assets/hud/%s.png", pPath);
3333 }
3334
3335 CImageInfo ImgInfo;
3336 bool PngLoaded = Graphics()->LoadPng(Image&: ImgInfo, pFilename: aPath, StorageType: IStorage::TYPE_ALL);
3337 if(!PngLoaded && !IsDefault)
3338 {
3339 if(AsDir)
3340 LoadHudSkin(pPath: "default");
3341 else
3342 LoadHudSkin(pPath, AsDir: true);
3343 }
3344 else if(PngLoaded && Graphics()->CheckImageDivisibility(pFileName: aPath, Img&: 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(pFileName: aPath, Img&: ImgInfo))
3345 {
3346 m_HudSkin.m_SpriteHudAirjump = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_AIRJUMP]);
3347 m_HudSkin.m_SpriteHudAirjumpEmpty = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_AIRJUMP_EMPTY]);
3348 m_HudSkin.m_SpriteHudSolo = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_SOLO]);
3349 m_HudSkin.m_SpriteHudCollisionDisabled = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_COLLISION_DISABLED]);
3350 m_HudSkin.m_SpriteHudEndlessJump = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_ENDLESS_JUMP]);
3351 m_HudSkin.m_SpriteHudEndlessHook = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_ENDLESS_HOOK]);
3352 m_HudSkin.m_SpriteHudJetpack = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_JETPACK]);
3353 m_HudSkin.m_SpriteHudFreezeBarFullLeft = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_FREEZE_BAR_FULL_LEFT]);
3354 m_HudSkin.m_SpriteHudFreezeBarFull = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_FREEZE_BAR_FULL]);
3355 m_HudSkin.m_SpriteHudFreezeBarEmpty = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_FREEZE_BAR_EMPTY]);
3356 m_HudSkin.m_SpriteHudFreezeBarEmptyRight = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_FREEZE_BAR_EMPTY_RIGHT]);
3357 m_HudSkin.m_SpriteHudNinjaBarFullLeft = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_NINJA_BAR_FULL_LEFT]);
3358 m_HudSkin.m_SpriteHudNinjaBarFull = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_NINJA_BAR_FULL]);
3359 m_HudSkin.m_SpriteHudNinjaBarEmpty = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_NINJA_BAR_EMPTY]);
3360 m_HudSkin.m_SpriteHudNinjaBarEmptyRight = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_NINJA_BAR_EMPTY_RIGHT]);
3361 m_HudSkin.m_SpriteHudHookHitDisabled = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_HOOK_HIT_DISABLED]);
3362 m_HudSkin.m_SpriteHudHammerHitDisabled = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_HAMMER_HIT_DISABLED]);
3363 m_HudSkin.m_SpriteHudShotgunHitDisabled = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_SHOTGUN_HIT_DISABLED]);
3364 m_HudSkin.m_SpriteHudGrenadeHitDisabled = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_GRENADE_HIT_DISABLED]);
3365 m_HudSkin.m_SpriteHudLaserHitDisabled = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_LASER_HIT_DISABLED]);
3366 m_HudSkin.m_SpriteHudGunHitDisabled = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_GUN_HIT_DISABLED]);
3367 m_HudSkin.m_SpriteHudDeepFrozen = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_DEEP_FROZEN]);
3368 m_HudSkin.m_SpriteHudLiveFrozen = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_LIVE_FROZEN]);
3369 m_HudSkin.m_SpriteHudTeleportGrenade = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_TELEPORT_GRENADE]);
3370 m_HudSkin.m_SpriteHudTeleportGun = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_TELEPORT_GUN]);
3371 m_HudSkin.m_SpriteHudTeleportLaser = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_TELEPORT_LASER]);
3372 m_HudSkin.m_SpriteHudPracticeMode = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_PRACTICE_MODE]);
3373 m_HudSkin.m_SpriteHudLockMode = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_LOCK_MODE]);
3374 m_HudSkin.m_SpriteHudTeam0Mode = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_TEAM0_MODE]);
3375 m_HudSkin.m_SpriteHudDummyHammer = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_DUMMY_HAMMER]);
3376 m_HudSkin.m_SpriteHudDummyCopy = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_HUD_DUMMY_COPY]);
3377
3378 m_HudSkinLoaded = true;
3379 ImgInfo.Free();
3380 }
3381}
3382
3383void CGameClient::LoadExtrasSkin(const char *pPath, bool AsDir)
3384{
3385 if(m_ExtrasSkinLoaded)
3386 {
3387 Graphics()->UnloadTexture(pIndex: &m_ExtrasSkin.m_SpriteParticleSnowflake);
3388
3389 for(auto &SpriteParticle : m_ExtrasSkin.m_aSpriteParticles)
3390 SpriteParticle = IGraphics::CTextureHandle();
3391
3392 m_ExtrasSkinLoaded = false;
3393 }
3394
3395 char aPath[IO_MAX_PATH_LENGTH];
3396 bool IsDefault = false;
3397 if(str_comp(a: pPath, b: "default") == 0)
3398 {
3399 str_copy(dst&: aPath, src: g_pData->m_aImages[IMAGE_EXTRAS].m_pFilename);
3400 IsDefault = true;
3401 }
3402 else
3403 {
3404 if(AsDir)
3405 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "assets/extras/%s/%s", pPath, g_pData->m_aImages[IMAGE_EXTRAS].m_pFilename);
3406 else
3407 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "assets/extras/%s.png", pPath);
3408 }
3409
3410 CImageInfo ImgInfo;
3411 bool PngLoaded = Graphics()->LoadPng(Image&: ImgInfo, pFilename: aPath, StorageType: IStorage::TYPE_ALL);
3412 if(!PngLoaded && !IsDefault)
3413 {
3414 if(AsDir)
3415 LoadExtrasSkin(pPath: "default");
3416 else
3417 LoadExtrasSkin(pPath, AsDir: true);
3418 }
3419 else if(PngLoaded && Graphics()->CheckImageDivisibility(pFileName: aPath, Img&: 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(pFileName: aPath, Img&: ImgInfo))
3420 {
3421 m_ExtrasSkin.m_SpriteParticleSnowflake = Graphics()->LoadSpriteTexture(FromImageInfo: ImgInfo, pSprite: &g_pData->m_aSprites[SPRITE_PART_SNOWFLAKE]);
3422 m_ExtrasSkin.m_aSpriteParticles[0] = m_ExtrasSkin.m_SpriteParticleSnowflake;
3423 m_ExtrasSkinLoaded = true;
3424 ImgInfo.Free();
3425 }
3426}
3427
3428void CGameClient::RefreshSkins()
3429{
3430 const auto SkinStartLoadTime = time_get_nanoseconds();
3431 m_Skins.Refresh(SkinLoadedFunc: [&](int) {
3432 // if skin refreshing takes to long, swap to a loading screen
3433 if(time_get_nanoseconds() - SkinStartLoadTime > 500ms)
3434 {
3435 m_Menus.RenderLoading(pCaption: Localize(pStr: "Loading skin files"), pContent: "", IncreaseCounter: 0, RenderLoadingBar: false);
3436 }
3437 });
3438
3439 for(auto &Client : m_aClients)
3440 {
3441 Client.m_SkinInfo.m_OriginalRenderSkin.Reset();
3442 Client.m_SkinInfo.m_ColorableRenderSkin.Reset();
3443 if(Client.m_aSkinName[0] != '\0')
3444 {
3445 const CSkin *pSkin = m_Skins.Find(pName: Client.m_aSkinName);
3446 Client.m_SkinInfo.m_OriginalRenderSkin = pSkin->m_OriginalSkin;
3447 Client.m_SkinInfo.m_ColorableRenderSkin = pSkin->m_ColorableSkin;
3448 }
3449 else
3450 {
3451 Client.m_SkinInfo.m_OriginalRenderSkin.Reset();
3452 Client.m_SkinInfo.m_ColorableRenderSkin.Reset();
3453 }
3454 Client.UpdateRenderInfo(IsTeamPlay: IsTeamPlay());
3455 }
3456
3457 for(auto &pComponent : m_vpAll)
3458 pComponent->OnRefreshSkins();
3459}
3460
3461void CGameClient::ConchainRefreshSkins(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
3462{
3463 CGameClient *pThis = static_cast<CGameClient *>(pUserData);
3464 pfnCallback(pResult, pCallbackUserData);
3465 if(pResult->NumArguments() && pThis->m_Menus.IsInit())
3466 {
3467 pThis->RefreshSkins();
3468 }
3469}
3470
3471static bool UnknownMapSettingCallback(const char *pCommand, void *pUser)
3472{
3473 return true;
3474}
3475
3476void CGameClient::LoadMapSettings()
3477{
3478 // Reset Tunezones
3479 CTuningParams TuningParams;
3480 for(int i = 0; i < NUM_TUNEZONES; i++)
3481 {
3482 TuningList()[i] = TuningParams;
3483 TuningList()[i].Set(pName: "gun_curvature", Value: 0);
3484 TuningList()[i].Set(pName: "gun_speed", Value: 1400);
3485 TuningList()[i].Set(pName: "shotgun_curvature", Value: 0);
3486 TuningList()[i].Set(pName: "shotgun_speed", Value: 500);
3487 TuningList()[i].Set(pName: "shotgun_speeddiff", Value: 0);
3488 }
3489
3490 // Load map tunings
3491 IMap *pMap = Kernel()->RequestInterface<IMap>();
3492 int Start, Num;
3493 pMap->GetType(Type: MAPITEMTYPE_INFO, pStart: &Start, pNum: &Num);
3494 for(int i = Start; i < Start + Num; i++)
3495 {
3496 int ItemId;
3497 CMapItemInfoSettings *pItem = (CMapItemInfoSettings *)pMap->GetItem(Index: i, pType: nullptr, pId: &ItemId);
3498 int ItemSize = pMap->GetItemSize(Index: i);
3499 if(!pItem || ItemId != 0)
3500 continue;
3501
3502 if(ItemSize < (int)sizeof(CMapItemInfoSettings))
3503 break;
3504 if(!(pItem->m_Settings > -1))
3505 break;
3506
3507 int Size = pMap->GetDataSize(Index: pItem->m_Settings);
3508 char *pSettings = (char *)pMap->GetData(Index: pItem->m_Settings);
3509 char *pNext = pSettings;
3510 Console()->SetUnknownCommandCallback(pfnCallback: UnknownMapSettingCallback, pUser: nullptr);
3511 while(pNext < pSettings + Size)
3512 {
3513 int StrSize = str_length(str: pNext) + 1;
3514 Console()->ExecuteLine(pStr: pNext, ClientId: IConsole::CLIENT_ID_GAME);
3515 pNext += StrSize;
3516 }
3517 Console()->SetUnknownCommandCallback(pfnCallback: IConsole::EmptyUnknownCommandCallback, pUser: nullptr);
3518 pMap->UnloadData(Index: pItem->m_Settings);
3519 break;
3520 }
3521}
3522
3523void CGameClient::ConTuneZone(IConsole::IResult *pResult, void *pUserData)
3524{
3525 CGameClient *pSelf = (CGameClient *)pUserData;
3526 int List = pResult->GetInteger(Index: 0);
3527 const char *pParamName = pResult->GetString(Index: 1);
3528 float NewValue = pResult->GetFloat(Index: 2);
3529
3530 if(List >= 0 && List < NUM_TUNEZONES)
3531 pSelf->TuningList()[List].Set(pName: pParamName, Value: NewValue);
3532}
3533
3534void CGameClient::ConchainMenuMap(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
3535{
3536 CGameClient *pSelf = (CGameClient *)pUserData;
3537 if(pResult->NumArguments())
3538 {
3539 if(str_comp(a: g_Config.m_ClMenuMap, b: pResult->GetString(Index: 0)) != 0)
3540 {
3541 str_copy(dst&: g_Config.m_ClMenuMap, src: pResult->GetString(Index: 0));
3542 pSelf->m_MenuBackground.LoadMenuBackground();
3543 }
3544 }
3545 else
3546 pfnCallback(pResult, pCallbackUserData);
3547}
3548
3549void CGameClient::DummyResetInput()
3550{
3551 if(!Client()->DummyConnected())
3552 return;
3553
3554 if((m_DummyInput.m_Fire & 1) != 0)
3555 m_DummyInput.m_Fire++;
3556
3557 m_Controls.ResetInput(Dummy: !g_Config.m_ClDummy);
3558 m_Controls.m_aInputData[!g_Config.m_ClDummy].m_Hook = 0;
3559 m_Controls.m_aInputData[!g_Config.m_ClDummy].m_Fire = m_DummyInput.m_Fire;
3560
3561 m_DummyInput = m_Controls.m_aInputData[!g_Config.m_ClDummy];
3562}
3563
3564bool CGameClient::CanDisplayWarning() const
3565{
3566 return m_Menus.CanDisplayWarning();
3567}
3568
3569CNetObjHandler *CGameClient::GetNetObjHandler()
3570{
3571 return &m_NetObjHandler;
3572}
3573
3574void CGameClient::SnapCollectEntities()
3575{
3576 int NumSnapItems = Client()->SnapNumItems(SnapId: IClient::SNAP_CURRENT);
3577
3578 std::vector<CSnapEntities> vItemData;
3579 std::vector<CSnapEntities> vItemEx;
3580
3581 for(int Index = 0; Index < NumSnapItems; Index++)
3582 {
3583 IClient::CSnapItem Item;
3584 const void *pData = Client()->SnapGetItem(SnapId: IClient::SNAP_CURRENT, Index, pItem: &Item);
3585 if(Item.m_Type == NETOBJTYPE_ENTITYEX)
3586 vItemEx.push_back(x: {.m_Item: Item, .m_pData: pData, .m_pDataEx: 0});
3587 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)
3588 vItemData.push_back(x: {.m_Item: Item, .m_pData: pData, .m_pDataEx: 0});
3589 }
3590
3591 // sort by id
3592 class CEntComparer
3593 {
3594 public:
3595 bool operator()(const CSnapEntities &lhs, const CSnapEntities &rhs) const
3596 {
3597 return lhs.m_Item.m_Id < rhs.m_Item.m_Id;
3598 }
3599 };
3600
3601 std::sort(first: vItemData.begin(), last: vItemData.end(), comp: CEntComparer());
3602 std::sort(first: vItemEx.begin(), last: vItemEx.end(), comp: CEntComparer());
3603
3604 // merge extended items with items they belong to
3605 m_vSnapEntities.clear();
3606
3607 size_t IndexEx = 0;
3608 for(const CSnapEntities &Ent : vItemData)
3609 {
3610 const CNetObj_EntityEx *pDataEx = 0;
3611 while(IndexEx < vItemEx.size() && vItemEx[IndexEx].m_Item.m_Id < Ent.m_Item.m_Id)
3612 IndexEx++;
3613 if(IndexEx < vItemEx.size() && vItemEx[IndexEx].m_Item.m_Id == Ent.m_Item.m_Id)
3614 pDataEx = (const CNetObj_EntityEx *)vItemEx[IndexEx].m_pData;
3615
3616 m_vSnapEntities.push_back(x: {.m_Item: Ent.m_Item, .m_pData: Ent.m_pData, .m_pDataEx: pDataEx});
3617 }
3618}
3619
3620void CGameClient::HandleMultiView()
3621{
3622 bool IsTeamZero = IsMultiViewIdSet();
3623 bool Init = false;
3624 int AmountPlayers = 0;
3625 vec2 Minpos, Maxpos;
3626 float TmpVel = 0.0f;
3627
3628 for(int i = 0; i < MAX_CLIENTS; i++)
3629 {
3630 // look at players who are vanished
3631 if(m_MultiView.m_aVanish[i])
3632 {
3633 // not in freeze anymore and the delay is over
3634 if(m_MultiView.m_aLastFreeze[i] + 6.0f <= Client()->LocalTime() && m_aClients[i].m_FreezeEnd == 0)
3635 {
3636 m_MultiView.m_aVanish[i] = false;
3637 m_MultiView.m_aLastFreeze[i] = 0.0f;
3638 }
3639 }
3640
3641 // we look at team 0 and the player is not in the spec list
3642 if(IsTeamZero && !m_aMultiViewId[i])
3643 continue;
3644
3645 // player is vanished
3646 if(m_MultiView.m_aVanish[i])
3647 continue;
3648
3649 // the player is not in the team we are spectating
3650 if(m_Teams.Team(ClientId: i) != m_MultiViewTeam)
3651 continue;
3652
3653 vec2 PlayerPos;
3654 if(m_Snap.m_aCharacters[i].m_Active)
3655 PlayerPos = vec2(m_aClients[i].m_RenderPos.x, m_aClients[i].m_RenderPos.y);
3656 else if(m_aClients[i].m_Spec) // tee is in spec
3657 PlayerPos = m_aClients[i].m_SpecChar;
3658 else
3659 continue;
3660
3661 // player is far away and frozen
3662 if(distance(a: m_MultiView.m_OldPos, b: PlayerPos) > 1100 && m_aClients[i].m_FreezeEnd != 0)
3663 {
3664 // check if the player is frozen for more than 3 seconds, if so vanish him
3665 if(m_MultiView.m_aLastFreeze[i] == 0.0f)
3666 m_MultiView.m_aLastFreeze[i] = Client()->LocalTime();
3667 else if(m_MultiView.m_aLastFreeze[i] + 3.0f <= Client()->LocalTime())
3668 {
3669 m_MultiView.m_aVanish[i] = true;
3670 // player we want to be vanished is our "main" tee, so lets switch the tee
3671 if(i == m_Snap.m_SpecInfo.m_SpectatorId)
3672 m_Spectator.Spectate(SpectatorId: FindFirstMultiViewId());
3673 }
3674 }
3675 else if(m_MultiView.m_aLastFreeze[i] != 0)
3676 m_MultiView.m_aLastFreeze[i] = 0;
3677
3678 // set the minimum and maximum position
3679 if(!Init)
3680 {
3681 Minpos = PlayerPos;
3682 Maxpos = PlayerPos;
3683 Init = true;
3684 }
3685 else
3686 {
3687 Minpos.x = std::min(a: Minpos.x, b: PlayerPos.x);
3688 Maxpos.x = std::max(a: Maxpos.x, b: PlayerPos.x);
3689 Minpos.y = std::min(a: Minpos.y, b: PlayerPos.y);
3690 Maxpos.y = std::max(a: Maxpos.y, b: PlayerPos.y);
3691 }
3692
3693 // sum up the velocity of all players we are spectating
3694 const CNetObj_Character &CurrentCharacter = m_Snap.m_aCharacters[i].m_Cur;
3695 TmpVel += (length(a: vec2(CurrentCharacter.m_VelX / 256.0f, CurrentCharacter.m_VelY / 256.0f)) * 50) / 32.0f;
3696 AmountPlayers++;
3697 }
3698
3699 // if we have found no players, we disable multi view
3700 if(AmountPlayers == 0)
3701 {
3702 if(m_MultiView.m_SecondChance == 0.0f)
3703 m_MultiView.m_SecondChance = Client()->LocalTime() + 0.3f;
3704 else if(m_MultiView.m_SecondChance < Client()->LocalTime())
3705 {
3706 ResetMultiView();
3707 return;
3708 }
3709 return;
3710 }
3711 else if(m_MultiView.m_SecondChance != 0.0f)
3712 m_MultiView.m_SecondChance = 0.0f;
3713
3714 // if we only have one tee that's in the list, we activate solo-mode
3715 m_MultiView.m_Solo = std::count(first: std::begin(arr&: m_aMultiViewId), last: std::end(arr&: m_aMultiViewId), value: true) == 1;
3716
3717 vec2 TargetPos = vec2((Minpos.x + Maxpos.x) / 2.0f, (Minpos.y + Maxpos.y) / 2.0f);
3718 // dont hide the position hud if its only one player
3719 m_MultiViewShowHud = AmountPlayers == 1;
3720 // get the average velocity
3721 float AvgVel = clamp(val: TmpVel / AmountPlayers ? TmpVel / (float)AmountPlayers : 0.0f, lo: 0.0f, hi: 1000.0f);
3722
3723 if(m_MultiView.m_OldPersonalZoom == m_MultiViewPersonalZoom)
3724 m_Camera.SetZoom(Target: CalculateMultiViewZoom(MinPos: Minpos, MaxPos: Maxpos, Vel: AvgVel), Smoothness: g_Config.m_ClMultiViewZoomSmoothness);
3725 else
3726 m_Camera.SetZoom(Target: CalculateMultiViewZoom(MinPos: Minpos, MaxPos: Maxpos, Vel: AvgVel), Smoothness: 50);
3727
3728 m_Snap.m_SpecInfo.m_Position = m_MultiView.m_OldPos + ((TargetPos - m_MultiView.m_OldPos) * CalculateMultiViewMultiplier(TargetPos));
3729 m_MultiView.m_OldPos = m_Snap.m_SpecInfo.m_Position;
3730 m_Snap.m_SpecInfo.m_UsePosition = true;
3731}
3732
3733bool CGameClient::InitMultiView(int Team)
3734{
3735 float Width, Height;
3736 CleanMultiViewIds();
3737 m_MultiView.m_IsInit = true;
3738
3739 // get the current view coordinates
3740 RenderTools()->CalcScreenParams(Aspect: Graphics()->ScreenAspect(), Zoom: m_Camera.m_Zoom, pWidth: &Width, pHeight: &Height);
3741 vec2 AxisX = vec2(m_Camera.m_Center.x - (Width / 2), m_Camera.m_Center.x + (Width / 2));
3742 vec2 AxisY = vec2(m_Camera.m_Center.y - (Height / 2), m_Camera.m_Center.y + (Height / 2));
3743
3744 if(Team > 0)
3745 {
3746 m_MultiViewTeam = Team;
3747 for(int i = 0; i < MAX_CLIENTS; i++)
3748 m_aMultiViewId[i] = m_Teams.Team(ClientId: i) == Team;
3749 }
3750 else
3751 {
3752 // we want to allow spectating players in teams directly if there is no other team on screen
3753 // to do that, -1 is used temporarily for "we don't know which team to spectate yet"
3754 m_MultiViewTeam = -1;
3755
3756 int Count = 0;
3757 for(int i = 0; i < MAX_CLIENTS; i++)
3758 {
3759 vec2 PlayerPos;
3760
3761 // get the position of the player
3762 if(m_Snap.m_aCharacters[i].m_Active)
3763 PlayerPos = vec2(m_Snap.m_aCharacters[i].m_Cur.m_X, m_Snap.m_aCharacters[i].m_Cur.m_Y);
3764 else if(m_aClients[i].m_Spec)
3765 PlayerPos = m_aClients[i].m_SpecChar;
3766 else
3767 continue;
3768
3769 if(PlayerPos.x == 0 || PlayerPos.y == 0)
3770 continue;
3771
3772 // skip players that aren't in view
3773 if(PlayerPos.x <= AxisX.x || PlayerPos.x >= AxisX.y || PlayerPos.y <= AxisY.x || PlayerPos.y >= AxisY.y)
3774 continue;
3775
3776 if(m_MultiViewTeam == -1)
3777 {
3778 // use the current player's team for now, but it might switch to team 0 if any other team is found
3779 m_MultiViewTeam = m_Teams.Team(ClientId: i);
3780 }
3781 else if(m_MultiViewTeam != 0 && m_Teams.Team(ClientId: i) != m_MultiViewTeam)
3782 {
3783 // mismatched teams; remove all previously added players again and switch to team 0 instead
3784 std::fill_n(first: m_aMultiViewId, n: i, value: false);
3785 m_MultiViewTeam = 0;
3786 }
3787
3788 m_aMultiViewId[i] = true;
3789 Count++;
3790 }
3791
3792 // might still be -1 if not a single player was in view; fallback to team 0 in that case
3793 if(m_MultiViewTeam == -1)
3794 m_MultiViewTeam = 0;
3795
3796 // we are spectating only one player
3797 m_MultiView.m_Solo = Count == 1;
3798 }
3799
3800 if(IsMultiViewIdSet())
3801 {
3802 int SpectatorId = m_Snap.m_SpecInfo.m_SpectatorId;
3803 int NewSpectatorId = -1;
3804
3805 vec2 CurPosition(m_Camera.m_Center);
3806 if(SpectatorId != SPEC_FREEVIEW)
3807 {
3808 const CNetObj_Character &CurCharacter = m_Snap.m_aCharacters[SpectatorId].m_Cur;
3809 CurPosition.x = CurCharacter.m_X;
3810 CurPosition.y = CurCharacter.m_Y;
3811 }
3812
3813 int ClosestDistance = std::numeric_limits<int>::max();
3814 for(int i = 0; i < MAX_CLIENTS; i++)
3815 {
3816 if(!m_Snap.m_apPlayerInfos[i] || m_Snap.m_apPlayerInfos[i]->m_Team == TEAM_SPECTATORS || m_Teams.Team(ClientId: i) != m_MultiViewTeam)
3817 continue;
3818
3819 vec2 PlayerPos;
3820 if(m_Snap.m_aCharacters[i].m_Active)
3821 PlayerPos = vec2(m_aClients[i].m_RenderPos.x, m_aClients[i].m_RenderPos.y);
3822 else if(m_aClients[i].m_Spec) // tee is in spec
3823 PlayerPos = m_aClients[i].m_SpecChar;
3824 else
3825 continue;
3826
3827 int Distance = distance(a: CurPosition, b: PlayerPos);
3828 if(NewSpectatorId == -1 || Distance < ClosestDistance)
3829 {
3830 NewSpectatorId = i;
3831 ClosestDistance = Distance;
3832 }
3833 }
3834
3835 if(NewSpectatorId > -1)
3836 m_Spectator.Spectate(SpectatorId: NewSpectatorId);
3837 }
3838
3839 return IsMultiViewIdSet();
3840}
3841
3842float CGameClient::CalculateMultiViewMultiplier(vec2 TargetPos)
3843{
3844 float MaxCameraDist = 200.0f;
3845 float MinCameraDist = 20.0f;
3846 float MaxVel = g_Config.m_ClMultiViewSensitivity / 150.0f;
3847 float MinVel = 0.007f;
3848 float CurrentCameraDistance = distance(a: m_MultiView.m_OldPos, b: TargetPos);
3849 float UpperLimit = 1.0f;
3850
3851 if(m_MultiView.m_Teleported && CurrentCameraDistance <= 100)
3852 m_MultiView.m_Teleported = false;
3853
3854 // somebody got teleported very likely
3855 if((m_MultiView.m_Teleported || CurrentCameraDistance - m_MultiView.m_OldCameraDistance > 100) && m_MultiView.m_OldCameraDistance != 0.0f)
3856 {
3857 UpperLimit = 0.1f; // dont try to compensate it by flickering
3858 m_MultiView.m_Teleported = true;
3859 }
3860 m_MultiView.m_OldCameraDistance = CurrentCameraDistance;
3861
3862 return clamp(val: MapValue(MaxValue: MaxCameraDist, MinValue: MinCameraDist, MaxRange: MaxVel, MinRange: MinVel, Value: CurrentCameraDistance), lo: MinVel, hi: UpperLimit);
3863}
3864
3865float CGameClient::CalculateMultiViewZoom(vec2 MinPos, vec2 MaxPos, float Vel)
3866{
3867 float Ratio = Graphics()->ScreenAspect();
3868 float ZoomX = 0.0f, ZoomY;
3869
3870 // only calc two axis if the aspect ratio is not 1:1
3871 if(Ratio != 1.0f)
3872 ZoomX = (0.001309f - 0.000328 * Ratio) * (MaxPos.x - MinPos.x) + (0.741413f - 0.032959 * Ratio);
3873
3874 // calculate the according zoom with linear function
3875 ZoomY = 0.001309f * (MaxPos.y - MinPos.y) + 0.741413f;
3876 // choose the highest zoom
3877 float Zoom = std::max(a: ZoomX, b: ZoomY);
3878 // zoom out to maximum 10 percent of the current zoom for 70 velocity
3879 float Diff = 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);
3880 // zoom should stay between 1.1 and 20.0
3881 Zoom = clamp(val: Zoom + Diff, lo: 1.1f, hi: 20.0f);
3882 // dont go below default zoom
3883 Zoom = std::max(a: float(std::pow(x: CCamera::ZOOM_STEP, y: g_Config.m_ClDefaultZoom - 10)), b: Zoom);
3884 // add the user preference
3885 Zoom -= (Zoom * 0.1f) * m_MultiViewPersonalZoom;
3886 m_MultiView.m_OldPersonalZoom = m_MultiViewPersonalZoom;
3887
3888 return Zoom;
3889}
3890
3891float CGameClient::MapValue(float MaxValue, float MinValue, float MaxRange, float MinRange, float Value)
3892{
3893 return (MaxRange - MinRange) / (MaxValue - MinValue) * (Value - MinValue) + MinRange;
3894}
3895
3896void CGameClient::ResetMultiView()
3897{
3898 m_Camera.SetZoom(Target: std::pow(x: CCamera::ZOOM_STEP, y: g_Config.m_ClDefaultZoom - 10), Smoothness: g_Config.m_ClSmoothZoomTime);
3899 m_MultiViewPersonalZoom = 0;
3900 m_MultiViewActivated = false;
3901 m_MultiView.m_Solo = false;
3902 m_MultiView.m_IsInit = false;
3903 m_MultiView.m_Teleported = false;
3904 m_MultiView.m_OldCameraDistance = 0.0f;
3905}
3906
3907void CGameClient::CleanMultiViewIds()
3908{
3909 std::fill(first: std::begin(arr&: m_aMultiViewId), last: std::end(arr&: m_aMultiViewId), value: false);
3910 std::fill(first: std::begin(arr&: m_MultiView.m_aLastFreeze), last: std::end(arr&: m_MultiView.m_aLastFreeze), value: 0.0f);
3911 std::fill(first: std::begin(arr&: m_MultiView.m_aVanish), last: std::end(arr&: m_MultiView.m_aVanish), value: false);
3912}
3913
3914void CGameClient::CleanMultiViewId(int ClientId)
3915{
3916 if(ClientId >= MAX_CLIENTS || ClientId < 0)
3917 return;
3918
3919 m_aMultiViewId[ClientId] = false;
3920 m_MultiView.m_aLastFreeze[ClientId] = 0.0f;
3921 m_MultiView.m_aVanish[ClientId] = false;
3922}
3923
3924bool CGameClient::IsMultiViewIdSet()
3925{
3926 return std::any_of(first: std::begin(arr&: m_aMultiViewId), last: std::end(arr&: m_aMultiViewId), pred: [](bool IsSet) { return IsSet; });
3927}
3928
3929int CGameClient::FindFirstMultiViewId()
3930{
3931 int ClientId = -1;
3932 for(int i = 0; i < MAX_CLIENTS; i++)
3933 {
3934 if(m_aMultiViewId[i] && !m_MultiView.m_aVanish[i])
3935 return i;
3936 }
3937 return ClientId;
3938}
3939