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 | |
76 | using namespace std::chrono_literals; |
77 | |
78 | const char *CGameClient::Version() const { return GAME_VERSION; } |
79 | const char *CGameClient::NetVersion() const { return GAME_NETVERSION; } |
80 | int CGameClient::DDNetVersion() const { return DDNET_VERSION_NUMBER; } |
81 | const char *CGameClient::DDNetVersionStr() const { return m_aDDNetVersionStr; } |
82 | const char *CGameClient::GetItemName(int Type) const { return m_NetObjHandler.GetObjName(Type); } |
83 | |
84 | void 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 | |
205 | static 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 | |
219 | void 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 | |
366 | void 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 | |
400 | void 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 | |
414 | int 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 | |
464 | void 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 | |
514 | void 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 | |
612 | void 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 | |
676 | void 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 | |
768 | void 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 | |
776 | int CGameClient::GetLastRaceTick() const |
777 | { |
778 | return m_Ghost.GetLastRaceTick(); |
779 | } |
780 | |
781 | bool 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 | |
800 | ColorRGBA 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 | |
808 | void CGameClient::OnRelease() |
809 | { |
810 | // release all systems |
811 | for(auto &pComponent : m_vpAll) |
812 | pComponent->OnRelease(); |
813 | } |
814 | |
815 | void 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 | |
994 | void 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 | |
1005 | void CGameClient::OnShutdown() |
1006 | { |
1007 | for(auto &pComponent : m_vpAll) |
1008 | pComponent->OnShutdown(); |
1009 | } |
1010 | |
1011 | void CGameClient::OnEnterGame() |
1012 | { |
1013 | m_Effects.ResetDamageIndicator(); |
1014 | } |
1015 | |
1016 | void CGameClient::OnGameOver() |
1017 | { |
1018 | if(Client()->State() != IClient::STATE_DEMOPLAYBACK && g_Config.m_ClEditor == 0) |
1019 | Client()->AutoScreenshot_Start(); |
1020 | } |
1021 | |
1022 | void CGameClient::OnStartGame() |
1023 | { |
1024 | if(Client()->State() != IClient::STATE_DEMOPLAYBACK && !g_Config.m_ClAutoDemoOnConnect) |
1025 | Client()->DemoRecorder_HandleAutoStart(); |
1026 | m_Statboard.OnReset(); |
1027 | } |
1028 | |
1029 | void 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 | |
1040 | void 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 | |
1048 | void CGameClient::OnWindowResize() |
1049 | { |
1050 | for(auto &pComponent : m_vpAll) |
1051 | pComponent->OnWindowResize(); |
1052 | |
1053 | Ui()->OnWindowResize(); |
1054 | } |
1055 | |
1056 | void 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 | |
1064 | void 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 | |
1077 | void 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 | |
1096 | void CGameClient::OnRconType(bool UsernameReq) |
1097 | { |
1098 | m_GameConsole.RequireUsername(UsernameReq); |
1099 | } |
1100 | |
1101 | void CGameClient::OnRconLine(const char *pLine) |
1102 | { |
1103 | m_GameConsole.PrintLine(Type: CGameConsole::CONSOLETYPE_REMOTE, pLine); |
1104 | } |
1105 | |
1106 | void 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 | |
1165 | static 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 | |
1329 | void 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 | |
1337 | void 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 | |
1941 | void 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 | |
1958 | void 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 | |
2194 | void CGameClient::OnActivateEditor() |
2195 | { |
2196 | OnRelease(); |
2197 | } |
2198 | |
2199 | CGameClient::CClientStats::CClientStats() |
2200 | { |
2201 | Reset(); |
2202 | } |
2203 | |
2204 | void 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 | |
2222 | void 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 | |
2244 | void 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 | |
2314 | void 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 | |
2324 | void 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 | |
2358 | void 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 | |
2392 | void 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 | |
2404 | void CGameClient::ConTeam(IConsole::IResult *pResult, void *pUserData) |
2405 | { |
2406 | ((CGameClient *)pUserData)->SendSwitchTeam(Team: pResult->GetInteger(Index: 0)); |
2407 | } |
2408 | |
2409 | void CGameClient::ConKill(IConsole::IResult *pResult, void *pUserData) |
2410 | { |
2411 | ((CGameClient *)pUserData)->SendKill(ClientId: -1); |
2412 | } |
2413 | |
2414 | void 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 | |
2425 | void 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 | |
2432 | void 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 | |
2439 | void 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 | |
2447 | void 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 | |
2458 | IGameClient *CreateGameClient() |
2459 | { |
2460 | return new CGameClient(); |
2461 | } |
2462 | |
2463 | int 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 | |
2509 | ColorRGBA CalculateNameColor(ColorHSLA TextColorHSL) |
2510 | { |
2511 | return color_cast<ColorRGBA>(hsl: ColorHSLA(TextColorHSL.h, TextColorHSL.s * 0.68f, TextColorHSL.l * 0.81f)); |
2512 | } |
2513 | |
2514 | void 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 | |
2702 | void 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 | |
2758 | void 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 | |
2843 | vec2 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 | |
2864 | void CGameClient::Echo(const char *pString) |
2865 | { |
2866 | m_Chat.Echo(pString); |
2867 | } |
2868 | |
2869 | bool 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 | |
2892 | int 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 | |
2903 | bool 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 | |
2910 | void 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 | |
3167 | void 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 | |
3211 | void 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 | |
3282 | void 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 | |
3383 | void CGameClient::(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 | |
3428 | void 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 | |
3461 | void 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 | |
3471 | static bool UnknownMapSettingCallback(const char *pCommand, void *pUser) |
3472 | { |
3473 | return true; |
3474 | } |
3475 | |
3476 | void 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 | |
3523 | void 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 | |
3534 | void CGameClient::(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 | |
3549 | void 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 | |
3564 | bool CGameClient::CanDisplayWarning() const |
3565 | { |
3566 | return m_Menus.CanDisplayWarning(); |
3567 | } |
3568 | |
3569 | CNetObjHandler *CGameClient::GetNetObjHandler() |
3570 | { |
3571 | return &m_NetObjHandler; |
3572 | } |
3573 | |
3574 | void 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 | |
3620 | void 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 | |
3733 | bool 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 | |
3842 | float 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 | |
3865 | float 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 | |
3891 | float CGameClient::MapValue(float MaxValue, float MinValue, float MaxRange, float MinRange, float Value) |
3892 | { |
3893 | return (MaxRange - MinRange) / (MaxValue - MinValue) * (Value - MinValue) + MinRange; |
3894 | } |
3895 | |
3896 | void 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 | |
3907 | void 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 | |
3914 | void 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 | |
3924 | bool 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 | |
3929 | int 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 | |