ddnet/src/game/client/gameclient.cpp

3871 lines
137 KiB
C++
Raw Normal View History

2010-11-20 10:37:14 +00:00
/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
/* If you are missing that file, acquire a complete release at teeworlds.com. */
2022-05-18 16:00:05 +00:00
#include <chrono>
#include <limits>
#include <engine/client/checksum.h>
#include <engine/demo.h>
#include <engine/editor.h>
2011-02-27 16:56:03 +00:00
#include <engine/engine.h>
#include <engine/favorites.h>
2011-03-23 12:06:35 +00:00
#include <engine/friends.h>
2010-05-29 07:25:38 +00:00
#include <engine/graphics.h>
#include <engine/map.h>
#include <engine/serverbrowser.h>
#include <engine/shared/config.h>
#include <engine/sound.h>
#include <engine/storage.h>
#include <engine/textrender.h>
#include <engine/updater.h>
2010-05-29 07:25:38 +00:00
#include <game/generated/client_data.h>
#include <game/generated/client_data7.h>
#include <game/generated/protocol.h>
2010-05-29 07:25:38 +00:00
2013-08-23 23:50:35 +00:00
#include <base/math.h>
2022-05-29 16:33:38 +00:00
#include <base/system.h>
2013-08-23 23:50:35 +00:00
#include <base/vmath.h>
#include "gameclient.h"
Port line input and IME support from 0.7 Port the line input (UI edit boxes, chat, console) and Input Method Editor (IME) support from upstream. Closes #4397. General ------------------------------ Fix issues with the text input. Closes #4346. Closes #4524. Word skipping (when holding Ctrl) is overhauled to be consistent with the Windows / Firefox experience that I took as reference. Improve usability by not blinking (i.e. always rendering) the caret shortly after is has been moved. UI text input ------------------------------ Fix inconsistent mouse-based left and right scrolling (closes #4347). Support smooth left and right scrolling. Chat ------------------------------ Support keyboard-based text selection of the chat input. Mouse-based selection could be support in the future when we decide to add something like an ingame UI cursor. Support smooth up and down scrolling of the chat input, removing the old hack that offsets the input string to simulate scrolling. Console ------------------------------ Also support mouse-based text selection of the command input. Only text from either the command input or the console log can be selected at the same time. This ensures that Ctrl+C will always copy the text that is currently visually selected in the console. Check for Ctrl+C input event in event handler instead of in render function, to hopefully fix the issue that copying does not work sometimes (closes #5974 until further notice). When Ctrl+C is used to copy text from the console log, the selection is cleared. This should make it more clear when text was copied from the log. Fix an issue that was preventing the console log selection from being cleared, when all log lines are selected. Remove Ctrl+A/E hotkeys that move cursor to beginning/end respectively. Ctrl+A now selectes all text like for all other inputs. Home and End keys can still be used to go the beginning and end. Remove Ctrl+U/K hotkeys that clear everything before/after the cursor respectively. Hold shift and use Home/End to select everything instead. IME support ------------------------------ Render list of IME candidates in the client on Windows, so the candidate list can also be viewed in fullscreen mode. There is no API available to retrieve a candidate list on the other operating systems. Improve composition rendering by underlining the composition text instead of putting it in square brackets. Track active input globally to properly activate and deactivate IME through the SDL functions. Closes #1030. Closes #1008. Password rendering ------------------------------ Fix rendering of passwords containing unicode. Instead of rendering one star character for each UTF-8 `char`, render on star for every unicode codepoint. Show the composition text also for passwords. Without seeing the composition text it's hard to type a password containing those characters. The candidate window exposes the composition anyway. If you don't want to expose your password this way, e.g. while streaming, you could: 1. Use a latin password and switch off the IME for the password input with the IME hotkey. 2. Blank your screen with an external program while you are streaming and entering passwords. 3. Use binds to authenticate in rcon or to set the server browser password. Refactoring ------------------------------ Move all text input logic and general rendering to `CLineInput`. A `CLineInput` is associated with a particular `char` buffer given as a pointer either in the constructor or with `SetBuffer`. The maximum byte size of the buffer must also be specified. The maximum length in unicode codepoints can also be specified separately (e.g. on upstream, name are limited by the number of unicode codepoints instead). Add `CLineInputBuffered`, which is a `CLineInput` that own a `char` buffer of a fixed size, which is specified as a template argument. As `CLineInput` does not own a buffer anymore, this reduces duplicate code for line inputs that need their own buffer. Add `CLineInputNumber` which has additional convenience functions to consider the text as an `int` or `float`, to reduce duplicate code in those cases. In the future we could also add an input filter function so that only numbers can be entered in the number input. Add `CLineInput::SetClipboardLineCallback` to handle the case that multiple lines of text are pasted into a lineinput. This reduces duplicate code, as this behavior was previously implemented separately for chat and console. The behavior is also fixed to be consistent with the console on Windows, so the first line being pasted edits the current input text and then sends it instead of being sent on its own without the existing input text. Add `CalcFontSizeAndBoundingBox` to UI to reduce duplicate code. Expose `CalcAlignedCursorPos` as static member function to reuse it for line input. Dispatch input events to UI inputs through the event handler instead of storing them in a duplicate buffer. Use `size_t` for line input cursor position, length etc. and for `str_utf8_stats`. Add `IButtonColorFunction` to UI to describe a functions that defines colors for the Default, Active and Hovered states of UI elements. Add some default button color functions. Use button color function to reduce duplicate code in scrollbar rendering. Use `vec2` instead of two `floats` to represent the mouse positions in the text renderer. Remove `CaretPosition` again, as it does not calculate the correct Y position near line breaks due to the wrapping being different when not rendering the entire string. Instead, calculate the exact caret position when rending a text container and store the caret position in the text cursor for later use. IME usage guide (Windows) ------------------------------ 1. Install the respective language and the Microsoft-IME keyboard (e.g. for Chinese, Japanese or Korean). 2. Launch the game (or a text editor to first try out the IME). Note that Windows may track the input language separately for every application. You can change this in the Windows input settings so the input language is changed globally. 2. Switch the input language using the hotkey Windows+Space or another hotkey that you configured in the Windows input settings (Alt+Shift is the default, but you should consider disabling it, to avoid accidentally changing the input language while playing). 3. Switch from Latin/English input mode to the respective asian input mode. - Chinese: Use Ctrl+Space to switch between English and Chinese input mode. You can change this hotkey in the IME's settings. - Japanese: Use Ctrl+Space to switch between Alphanumeric and Hiragana/Katakana input mode. You can change this hotkey in the IME's settings. - Korean: Use Right Alt to switch between English and Hangul input mode. You cannot change this hotkey as of yet. - Note that the input mode is also tracked per application, but there is no setting to change this behavior as far as I know, so you'll need to switch for every application separately. 4. Start typing. The underlined text is the current composition text. While a composition is active, you can only edit the composition text. Confirm the composition with Space or by selecting a candidate from the candidate list with the arrow keys. Cancel the composition with Escape or by using Backspace to delete the composition text. Note that not all languages offer a candidate list. SDL version-specific issues ------------------------------ - 2.26.5, 2.24.2, 2.0.22: IME candidates work. But there are minor bugs when moving the composition cursor. - 2.0.18, 2.0.20: IME candidates work. - 2.0.16 (our current version): IME candidates cannot be determined with Windows API. Windows tries to draw the composition window like before, so this does not work in fullscreen mode. - 2.0.8 (upstream 0.7): IME candidates work. But this SDL version is too old for us.
2023-01-03 21:28:38 +00:00
#include "lineinput.h"
#include "race.h"
#include "render.h"
2010-05-29 07:25:38 +00:00
#include <game/localization.h>
#include <game/mapitems.h>
2010-05-29 07:25:38 +00:00
#include <game/version.h>
2015-08-25 00:11:04 +00:00
#include "components/background.h"
2010-05-29 07:25:38 +00:00
#include "components/binds.h"
#include "components/broadcast.h"
#include "components/camera.h"
#include "components/chat.h"
#include "components/console.h"
#include "components/controls.h"
#include "components/countryflags.h"
2010-05-29 07:25:38 +00:00
#include "components/damageind.h"
#include "components/debughud.h"
#include "components/effects.h"
#include "components/emoticon.h"
2022-03-25 11:54:11 +00:00
#include "components/freezebars.h"
2022-05-29 16:33:38 +00:00
#include "components/ghost.h"
2010-05-29 07:25:38 +00:00
#include "components/hud.h"
#include "components/infomessages.h"
2023-11-20 11:23:56 +00:00
#include "components/items.h"
2010-05-29 07:25:38 +00:00
#include "components/mapimages.h"
#include "components/maplayers.h"
2014-10-10 17:10:57 +00:00
#include "components/mapsounds.h"
2020-09-18 16:45:42 +00:00
#include "components/menu_background.h"
2010-05-29 07:25:38 +00:00
#include "components/menus.h"
#include "components/motd.h"
2020-09-18 16:45:42 +00:00
#include "components/nameplates.h"
2010-05-29 07:25:38 +00:00
#include "components/particles.h"
#include "components/players.h"
2022-05-29 16:33:38 +00:00
#include "components/race_demo.h"
2010-05-29 07:25:38 +00:00
#include "components/scoreboard.h"
#include "components/skins.h"
#include "components/sounds.h"
#include "components/spectator.h"
2015-05-21 09:41:59 +00:00
#include "components/statboard.h"
2010-05-29 07:25:38 +00:00
#include "components/voting.h"
2022-05-29 16:33:38 +00:00
#include "prediction/entities/character.h"
#include "prediction/entities/projectile.h"
2011-08-31 11:56:04 +00:00
2022-05-18 16:00:05 +00:00
using namespace std::chrono_literals;
const char *CGameClient::Version() const { return GAME_VERSION; }
const char *CGameClient::NetVersion() const { return GAME_NETVERSION; }
int CGameClient::DDNetVersion() const { return DDNET_VERSION_NUMBER; }
const char *CGameClient::DDNetVersionStr() const { return m_aDDNetVersionStr; }
const char *CGameClient::GetItemName(int Type) const { return m_NetObjHandler.GetObjName(Type); }
2009-06-15 06:45:44 +00:00
2010-05-29 07:25:38 +00:00
void CGameClient::OnConsoleInit()
{
2011-02-27 16:56:03 +00:00
m_pEngine = Kernel()->RequestInterface<IEngine>();
2010-05-29 07:25:38 +00:00
m_pClient = Kernel()->RequestInterface<IClient>();
m_pTextRender = Kernel()->RequestInterface<ITextRender>();
m_pSound = Kernel()->RequestInterface<ISound>();
m_pConfigManager = Kernel()->RequestInterface<IConfigManager>();
m_pConfig = m_pConfigManager->Values();
2010-05-29 07:25:38 +00:00
m_pInput = Kernel()->RequestInterface<IInput>();
m_pConsole = Kernel()->RequestInterface<IConsole>();
m_pStorage = Kernel()->RequestInterface<IStorage>();
m_pDemoPlayer = Kernel()->RequestInterface<IDemoPlayer>();
m_pServerBrowser = Kernel()->RequestInterface<IServerBrowser>();
m_pEditor = Kernel()->RequestInterface<IEditor>();
m_pFavorites = Kernel()->RequestInterface<IFavorites>();
2011-03-23 12:06:35 +00:00
m_pFriends = Kernel()->RequestInterface<IFriends>();
2015-07-22 20:16:49 +00:00
m_pFoes = Client()->Foes();
#if defined(CONF_AUTOUPDATE)
m_pUpdater = Kernel()->RequestInterface<IUpdater>();
2015-03-13 14:13:19 +00:00
#endif
2023-12-18 19:01:26 +00:00
m_pHttp = Kernel()->RequestInterface<IHttp>();
2021-07-12 09:43:56 +00:00
m_Menus.SetMenuBackground(&m_MenuBackground);
m_NamePlates.SetPlayers(&m_Players);
2015-05-19 22:51:02 +00:00
// make a list of all the systems, make sure to add them in the correct render order
m_vpAll.insert(m_vpAll.end(), {&m_Skins,
&m_CountryFlags,
&m_MapImages,
&m_Effects, // doesn't render anything, just updates effects
&m_Binds,
&m_Binds.m_SpecialBinds,
&m_Controls,
&m_Camera,
&m_Sounds,
&m_Voting,
&m_Particles, // doesn't render anything, just updates all the particles
&m_RaceDemo,
&m_MapSounds,
&m_Background, // render instead of m_MapLayersBackground when g_Config.m_ClOverlayEntities == 100
&m_MapLayersBackground, // first to render
&m_Particles.m_RenderTrail,
&m_Items,
&m_Ghost,
2023-10-28 12:45:43 +00:00
&m_Players,
&m_MapLayersForeground,
&m_Particles.m_RenderExplosions,
&m_NamePlates,
2022-06-14 17:28:51 +00:00
&m_Particles.m_RenderExtra,
&m_Particles.m_RenderGeneral,
&m_FreezeBars,
&m_DamageInd,
&m_Hud,
&m_Spectator,
&m_Emoticon,
&m_InfoMessages,
&m_Chat,
&m_Broadcast,
&m_DebugHud,
&m_Scoreboard,
&m_Statboard,
&m_Motd,
&m_Menus,
&m_Tooltips,
&CMenus::m_Binder,
&m_GameConsole,
&m_MenuBackground});
2020-09-18 16:45:42 +00:00
// build the input stack
m_vpInput.insert(m_vpInput.end(), {&CMenus::m_Binder, // this will take over all input when we want to bind a key
&m_Binds.m_SpecialBinds,
&m_GameConsole,
2022-10-25 16:51:56 +00:00
&m_Chat, // chat has higher prio, due to that you can quit it by pressing esc
&m_Motd, // for pressing esc to remove it
&m_Menus,
&m_Spectator,
&m_Emoticon,
&m_Controls,
&m_Binds});
2008-09-04 21:36:44 +00:00
// add the some console commands
Console()->Register("team", "i[team-id]", CFGFLAG_CLIENT, ConTeam, this, "Switch team");
2020-09-01 13:35:17 +00:00
Console()->Register("kill", "", CFGFLAG_CLIENT, ConKill, this, "Kill yourself to restart");
// register server dummy commands for tab completion
Console()->Register("tune", "s[tuning] ?f[value]", CFGFLAG_SERVER, 0, 0, "Tune variable to value or show current value");
Console()->Register("tune_reset", "?s[tuning]", CFGFLAG_SERVER, 0, 0, "Reset all or one tuning variable to default");
Console()->Register("tunes", "", CFGFLAG_SERVER, 0, 0, "List all tuning variables and their values");
Console()->Register("change_map", "?r[map]", CFGFLAG_SERVER, 0, 0, "Change map");
Console()->Register("restart", "?i[seconds]", CFGFLAG_SERVER, 0, 0, "Restart in x seconds");
Console()->Register("broadcast", "r[message]", CFGFLAG_SERVER, 0, 0, "Broadcast message");
Console()->Register("say", "r[message]", CFGFLAG_SERVER, 0, 0, "Say in chat");
Console()->Register("set_team", "i[id] i[team-id] ?i[delay in minutes]", CFGFLAG_SERVER, 0, 0, "Set team of player to team");
Console()->Register("set_team_all", "i[team-id]", CFGFLAG_SERVER, 0, 0, "Set team of all players to team");
Console()->Register("add_vote", "s[name] r[command]", CFGFLAG_SERVER, 0, 0, "Add a voting option");
Console()->Register("remove_vote", "s[name]", CFGFLAG_SERVER, 0, 0, "remove a voting option");
Console()->Register("force_vote", "s[name] s[command] ?r[reason]", CFGFLAG_SERVER, 0, 0, "Force a voting option");
Console()->Register("clear_votes", "", CFGFLAG_SERVER, 0, 0, "Clears the voting options");
Console()->Register("add_map_votes", "", CFGFLAG_SERVER, 0, 0, "Automatically adds voting options for all maps");
Console()->Register("vote", "r['yes'|'no']", CFGFLAG_SERVER, 0, 0, "Force a vote to yes/no");
2011-09-04 09:13:30 +00:00
Console()->Register("swap_teams", "", CFGFLAG_SERVER, 0, 0, "Swap the current teams");
Console()->Register("shuffle_teams", "", CFGFLAG_SERVER, 0, 0, "Shuffle the current teams");
2010-05-29 07:25:38 +00:00
2019-09-08 22:53:07 +00:00
// register tune zone command to allow the client prediction to load tunezones from the map
Console()->Register("tune_zone", "i[zone] s[tuning] f[value]", CFGFLAG_CLIENT | CFGFLAG_GAME, ConTuneZone, this, "Tune in zone a variable to value");
for(auto &pComponent : m_vpAll)
pComponent->m_pClient = this;
2008-09-23 07:43:41 +00:00
// let all the other components register their console commands
for(auto &pComponent : m_vpAll)
pComponent->OnConsoleInit();
Add font index, support font family variants depending on language Add `fonts/index.json` which specifies: - List of all font files that should be loaded (filenames). - Default font (specified by family name or by family and style name). - Font variants for different languages, using the name of the language file as key. - Fallback fonts. - Icon font. There are characters (e.g. all in `刃直海角骨入`) that look different depending on the language of the content being Japanese, Simplified Chinese, Traditional Chinese and Hangul, because Unicode uses the same codepoint for characters regardless of the language. To render these characters correctly, the active variant font is switched depending on the selected language. The `ITextRender` interface is changed so the current language variant can be set using `SetFontLanguageVariant` and the default and icon fonts can be toggled using `SetFontPreset`. The class `CFont` is removed entirely. The text render is restructured: The font faces and font atlas are now managed by a separate class `CGlyphMap` like on upstream. As the text fill and outline textures always have the same size, the texture skyline only needs to be stored once and free positions in the atlas only need to be calculated once for each glyph instead of separately for the fill and outline textures. The font files and their licenses are also updated: - Update Source Han Sans to version 2.001. - Update Glow Sans Japanese Compressed to version 0.93. - Update Deja Vu Sans to version 2.37. - Update Font Awesome icons font to March 2023 version. Closes #6881.
2023-07-17 13:49:41 +00:00
Console()->Chain("cl_languagefile", ConchainLanguageUpdate, this);
2010-05-29 07:25:38 +00:00
Console()->Chain("player_name", ConchainSpecialInfoupdate, this);
Console()->Chain("player_clan", ConchainSpecialInfoupdate, this);
Console()->Chain("player_country", ConchainSpecialInfoupdate, this);
2010-05-29 07:25:38 +00:00
Console()->Chain("player_use_custom_color", ConchainSpecialInfoupdate, this);
Console()->Chain("player_color_body", ConchainSpecialInfoupdate, this);
Console()->Chain("player_color_feet", ConchainSpecialInfoupdate, this);
Console()->Chain("player_skin", ConchainSpecialInfoupdate, this);
2014-04-28 13:19:57 +00:00
Console()->Chain("dummy_name", ConchainSpecialDummyInfoupdate, this);
Console()->Chain("dummy_clan", ConchainSpecialDummyInfoupdate, this);
Console()->Chain("dummy_country", ConchainSpecialDummyInfoupdate, this);
Console()->Chain("dummy_use_custom_color", ConchainSpecialDummyInfoupdate, this);
Console()->Chain("dummy_color_body", ConchainSpecialDummyInfoupdate, this);
Console()->Chain("dummy_color_feet", ConchainSpecialDummyInfoupdate, this);
Console()->Chain("dummy_skin", ConchainSpecialDummyInfoupdate, this);
Console()->Chain("cl_dummy", ConchainSpecialDummy, this);
Console()->Chain("cl_text_entities_size", ConchainClTextEntitiesSize, this);
2020-09-18 16:45:42 +00:00
Console()->Chain("cl_menu_map", ConchainMenuMap, this);
//
2010-05-29 07:25:38 +00:00
m_SuppressEvents = false;
2008-09-04 21:36:44 +00:00
}
2010-05-29 07:25:38 +00:00
void CGameClient::OnInit()
2008-09-04 21:36:44 +00:00
{
Client()->SetMapLoadingCBFunc([this]() {
m_Menus.RenderLoading(DemoPlayer()->IsPlaying() ? Localize("Preparing demo playback") : Localize("Connected"), Localize("Loading map file from storage"), 0, false);
});
m_pGraphics = Kernel()->RequestInterface<IGraphics>();
// propagate pointers
m_UI.Init(Kernel());
m_RenderTools.Init(Graphics(), TextRender());
2021-06-23 05:05:49 +00:00
int64_t Start = time_get();
2011-02-13 12:58:59 +00:00
if(GIT_SHORTREV_HASH)
{
str_format(m_aDDNetVersionStr, sizeof(m_aDDNetVersionStr), "%s %s (%s)", GAME_NAME, GAME_RELEASE_VERSION, GIT_SHORTREV_HASH);
}
else
{
str_format(m_aDDNetVersionStr, sizeof(m_aDDNetVersionStr), "%s %s", GAME_NAME, GAME_RELEASE_VERSION);
}
2010-05-29 07:25:38 +00:00
// set the language
g_Localization.LoadIndexfile(Storage(), Console());
if(g_Config.m_ClShowWelcome)
g_Localization.SelectDefaultLanguage(Console(), g_Config.m_ClLanguagefile, sizeof(g_Config.m_ClLanguagefile));
2010-10-06 21:07:35 +00:00
g_Localization.Load(g_Config.m_ClLanguagefile, Storage(), Console());
2011-02-27 16:56:03 +00:00
// TODO: this should be different
2008-08-30 22:38:56 +00:00
// setup item sizes
for(int i = 0; i < NUM_NETOBJTYPES; i++)
2010-05-29 07:25:38 +00:00
Client()->SnapSetStaticsize(i, m_NetObjHandler.GetObjSize(i));
2011-02-27 16:56:03 +00:00
Add font index, support font family variants depending on language Add `fonts/index.json` which specifies: - List of all font files that should be loaded (filenames). - Default font (specified by family name or by family and style name). - Font variants for different languages, using the name of the language file as key. - Fallback fonts. - Icon font. There are characters (e.g. all in `刃直海角骨入`) that look different depending on the language of the content being Japanese, Simplified Chinese, Traditional Chinese and Hangul, because Unicode uses the same codepoint for characters regardless of the language. To render these characters correctly, the active variant font is switched depending on the selected language. The `ITextRender` interface is changed so the current language variant can be set using `SetFontLanguageVariant` and the default and icon fonts can be toggled using `SetFontPreset`. The class `CFont` is removed entirely. The text render is restructured: The font faces and font atlas are now managed by a separate class `CGlyphMap` like on upstream. As the text fill and outline textures always have the same size, the texture skyline only needs to be stored once and free positions in the atlas only need to be calculated once for each glyph instead of separately for the fill and outline textures. The font files and their licenses are also updated: - Update Source Han Sans to version 2.001. - Update Glow Sans Japanese Compressed to version 0.93. - Update Deja Vu Sans to version 2.37. - Update Font Awesome icons font to March 2023 version. Closes #6881.
2023-07-17 13:49:41 +00:00
TextRender()->LoadFonts();
TextRender()->SetFontLanguageVariant(g_Config.m_ClLanguagefile);
// update and swap after font loading, they are quite huge
Client()->UpdateAndSwap();
const char *pLoadingDDNetCaption = Localize("Loading DDNet Client");
2011-02-27 16:56:03 +00:00
// init all components
int SkippedComps = 0;
int CompCounter = 0;
for(int i = m_vpAll.size() - 1; i >= 0; --i)
{
m_vpAll[i]->OnInit();
// try to render a frame after each component, also flushes GPU uploads
if(m_Menus.IsInit())
{
char aBuff[256];
2022-08-08 14:37:17 +00:00
str_format(aBuff, std::size(aBuff), "%s [%d/%d]", CompCounter == 40 ? Localize("Why are you slowmo replaying to read this?") : Localize("Initializing components"), (CompCounter + 1), (int)ComponentCount());
m_Menus.RenderLoading(pLoadingDDNetCaption, aBuff, 1 + SkippedComps);
SkippedComps = 0;
}
else
{
++SkippedComps;
}
++CompCounter;
}
2011-02-27 16:56:03 +00:00
2014-04-27 22:41:19 +00:00
char aBuf[256];
m_GameSkinLoaded = false;
m_ParticlesSkinLoaded = false;
m_EmoticonsSkinLoaded = false;
m_HudSkinLoaded = false;
// setup load amount, load textures
2010-05-29 07:25:38 +00:00
for(int i = 0; i < g_pData->m_NumImages; i++)
2008-08-30 22:38:56 +00:00
{
2020-09-26 07:37:35 +00:00
if(i == IMAGE_GAME)
LoadGameSkin(g_Config.m_ClAssetGame);
else if(i == IMAGE_EMOTICONS)
LoadEmoticonsSkin(g_Config.m_ClAssetEmoticons);
else if(i == IMAGE_PARTICLES)
LoadParticlesSkin(g_Config.m_ClAssetParticles);
else if(i == IMAGE_HUD)
LoadHudSkin(g_Config.m_ClAssetHud);
2022-06-14 17:28:51 +00:00
else if(i == IMAGE_EXTRAS)
LoadExtrasSkin(g_Config.m_ClAssetExtras);
else if(g_pData->m_aImages[i].m_pFilename[0] == '\0') // handle special null image without filename
g_pData->m_aImages[i].m_Id = IGraphics::CTextureHandle();
2020-09-26 07:37:35 +00:00
else
g_pData->m_aImages[i].m_Id = Graphics()->LoadTexture(g_pData->m_aImages[i].m_pFilename, IStorage::TYPE_ALL);
m_Menus.RenderLoading(pLoadingDDNetCaption, Localize("Initializing assets"), 1);
2010-05-29 07:25:38 +00:00
}
for(auto &pComponent : m_vpAll)
pComponent->OnReset();
2010-05-29 07:25:38 +00:00
m_ServerMode = SERVERMODE_PURE;
2011-01-06 03:46:10 +00:00
m_aDDRaceMsgSent[0] = false;
m_aDDRaceMsgSent[1] = false;
m_aShowOthers[0] = SHOW_OTHERS_NOT_SET;
m_aShowOthers[1] = SHOW_OTHERS_NOT_SET;
m_aSwitchStateTeam[0] = -1;
m_aSwitchStateTeam[1] = -1;
2011-04-17 17:14:49 +00:00
2021-01-21 16:07:07 +00:00
m_LastZoom = .0;
m_LastScreenAspect = .0;
m_LastDummyConnected = false;
2011-04-17 17:14:49 +00:00
// Set free binds to DDRace binds if it's active
2021-07-12 09:29:59 +00:00
m_Binds.SetDDRaceBinds(true);
2014-08-10 10:54:01 +00:00
if(g_Config.m_ClTimeoutCode[0] == '\0' || str_comp(g_Config.m_ClTimeoutCode, "hGuEYnfxicsXGwFq") == 0)
{
for(unsigned int i = 0; i < 16; i++)
{
if(rand() % 2)
g_Config.m_ClTimeoutCode[i] = (char)((rand() % 26) + 97);
else
2018-10-02 18:52:21 +00:00
g_Config.m_ClTimeoutCode[i] = (char)((rand() % 26) + 65);
}
}
if(g_Config.m_ClDummyTimeoutCode[0] == '\0' || str_comp(g_Config.m_ClDummyTimeoutCode, "hGuEYnfxicsXGwFq") == 0)
{
for(unsigned int i = 0; i < 16; i++)
{
if(rand() % 2)
2018-10-02 18:52:21 +00:00
g_Config.m_ClDummyTimeoutCode[i] = (char)((rand() % 26) + 97);
else
2018-10-02 18:52:21 +00:00
g_Config.m_ClDummyTimeoutCode[i] = (char)((rand() % 26) + 65);
}
}
2021-06-23 05:05:49 +00:00
int64_t End = time_get();
str_format(aBuf, sizeof(aBuf), "initialisation finished after %.2fms", ((End - Start) * 1000) / (float)time_freq());
Console()->Print(IConsole::OUTPUT_LEVEL_DEBUG, "gameclient", aBuf);
m_GameWorld.m_pCollision = Collision();
2019-09-08 22:53:07 +00:00
m_GameWorld.m_pTuningList = m_aTuningList;
2021-07-12 09:43:56 +00:00
m_MapImages.SetTextureScale(g_Config.m_ClTextEntitiesSize);
2022-10-25 16:51:56 +00:00
// Aggressively try to grab window again since some Windows users report
// window not being focused after starting client.
Graphics()->SetWindowGrab(true);
CChecksumData *pChecksum = Client()->ChecksumData();
pChecksum->m_SizeofGameClient = sizeof(*this);
pChecksum->m_NumComponents = m_vpAll.size();
for(size_t i = 0; i < m_vpAll.size(); i++)
{
if(i >= std::size(pChecksum->m_aComponentsChecksum))
{
break;
}
int Size = m_vpAll[i]->Sizeof();
pChecksum->m_aComponentsChecksum[i] = Size;
}
}
void CGameClient::OnUpdate()
{
Add font index, support font family variants depending on language Add `fonts/index.json` which specifies: - List of all font files that should be loaded (filenames). - Default font (specified by family name or by family and style name). - Font variants for different languages, using the name of the language file as key. - Fallback fonts. - Icon font. There are characters (e.g. all in `刃直海角骨入`) that look different depending on the language of the content being Japanese, Simplified Chinese, Traditional Chinese and Hangul, because Unicode uses the same codepoint for characters regardless of the language. To render these characters correctly, the active variant font is switched depending on the selected language. The `ITextRender` interface is changed so the current language variant can be set using `SetFontLanguageVariant` and the default and icon fonts can be toggled using `SetFontPreset`. The class `CFont` is removed entirely. The text render is restructured: The font faces and font atlas are now managed by a separate class `CGlyphMap` like on upstream. As the text fill and outline textures always have the same size, the texture skyline only needs to be stored once and free positions in the atlas only need to be calculated once for each glyph instead of separately for the fill and outline textures. The font files and their licenses are also updated: - Update Source Han Sans to version 2.001. - Update Glow Sans Japanese Compressed to version 0.93. - Update Deja Vu Sans to version 2.37. - Update Font Awesome icons font to March 2023 version. Closes #6881.
2023-07-17 13:49:41 +00:00
HandleLanguageChanged();
CUIElementBase::Init(UI()); // update static pointer because game and editor use separate UI
// handle mouse movement
float x = 0.0f, y = 0.0f;
IInput::ECursorType CursorType = Input()->CursorRelative(&x, &y);
if(CursorType != IInput::CURSOR_NONE)
{
for(auto &pComponent : m_vpInput)
2008-09-12 07:20:26 +00:00
{
if(pComponent->OnCursorMove(x, y, CursorType))
2008-09-12 07:20:26 +00:00
break;
}
}
// handle key presses
for(size_t i = 0; i < Input()->NumEvents(); i++)
{
const IInput::CEvent &Event = Input()->GetEvent(i);
if(!Input()->IsEventValid(Event))
continue;
for(auto &pComponent : m_vpInput)
{
if(pComponent->OnInput(Event))
break;
}
}
2023-10-03 17:27:53 +00:00
2023-10-08 15:31:43 +00:00
if(g_Config.m_ClSubTickAiming && m_Binds.m_MouseOnAction)
2023-10-03 17:27:53 +00:00
{
2023-10-08 15:31:43 +00:00
m_Controls.m_aMousePosOnAction[g_Config.m_ClDummy] = m_Controls.m_aMousePos[g_Config.m_ClDummy];
m_Binds.m_MouseOnAction = false;
2023-10-03 17:27:53 +00:00
}
}
void CGameClient::OnDummySwap()
{
if(g_Config.m_ClDummyResetOnSwitch)
{
int PlayerOrDummy = (g_Config.m_ClDummyResetOnSwitch == 2) ? g_Config.m_ClDummy : (!g_Config.m_ClDummy);
2021-07-12 09:43:56 +00:00
m_Controls.ResetInput(PlayerOrDummy);
m_Controls.m_aInputData[PlayerOrDummy].m_Hook = 0;
}
int tmp = m_DummyInput.m_Fire;
m_DummyInput = m_Controls.m_aInputData[!g_Config.m_ClDummy];
m_Controls.m_aInputData[g_Config.m_ClDummy].m_Fire = tmp;
m_IsDummySwapping = 1;
}
int CGameClient::OnSnapInput(int *pData, bool Dummy, bool Force)
{
if(!Dummy)
{
2021-07-12 09:43:56 +00:00
return m_Controls.SnapInput(pData);
}
if(!g_Config.m_ClDummyHammer)
{
if(m_DummyFire != 0)
{
m_DummyInput.m_Fire = (m_HammerInput.m_Fire + 1) & ~1;
m_DummyFire = 0;
}
if(!Force && (!m_DummyInput.m_Direction && !m_DummyInput.m_Jump && !m_DummyInput.m_Hook))
{
return 0;
}
mem_copy(pData, &m_DummyInput, sizeof(m_DummyInput));
return sizeof(m_DummyInput);
}
else
{
if(m_DummyFire % 25 != 0)
{
m_DummyFire++;
return 0;
}
m_DummyFire++;
m_HammerInput.m_Fire = (m_HammerInput.m_Fire + 1) | 1;
m_HammerInput.m_WantedWeapon = WEAPON_HAMMER + 1;
if(!g_Config.m_ClDummyRestoreWeapon)
{
m_DummyInput.m_WantedWeapon = WEAPON_HAMMER + 1;
}
vec2 MainPos = m_LocalCharacterPos;
vec2 DummyPos = m_aClients[m_aLocalIDs[!g_Config.m_ClDummy]].m_Predicted.m_Pos;
vec2 Dir = MainPos - DummyPos;
2018-10-02 18:52:21 +00:00
m_HammerInput.m_TargetX = (int)(Dir.x);
m_HammerInput.m_TargetY = (int)(Dir.y);
mem_copy(pData, &m_HammerInput, sizeof(m_HammerInput));
return sizeof(m_HammerInput);
}
}
2010-05-29 07:25:38 +00:00
void CGameClient::OnConnected()
{
const char *pConnectCaption = DemoPlayer()->IsPlaying() ? Localize("Preparing demo playback") : Localize("Connected");
const char *pLoadMapContent = Localize("Initializing map logic");
// render loading before skip is calculated
m_Menus.RenderLoading(pConnectCaption, pLoadMapContent, 0, false);
2010-05-29 07:25:38 +00:00
m_Layers.Init(Kernel());
m_Collision.Init(Layers());
m_GameWorld.m_Core.InitSwitchers(m_Collision.m_HighestSwitchNumber);
CRaceHelper::ms_aFlagIndex[0] = -1;
CRaceHelper::ms_aFlagIndex[1] = -1;
CTile *pGameTiles = static_cast<CTile *>(Layers()->Map()->GetData(Layers()->GameLayer()->m_Data));
// get flag positions
for(int i = 0; i < m_Collision.GetWidth() * m_Collision.GetHeight(); i++)
{
if(pGameTiles[i].m_Index - ENTITY_OFFSET == ENTITY_FLAGSTAND_RED)
CRaceHelper::ms_aFlagIndex[TEAM_RED] = i;
else if(pGameTiles[i].m_Index - ENTITY_OFFSET == ENTITY_FLAGSTAND_BLUE)
CRaceHelper::ms_aFlagIndex[TEAM_BLUE] = i;
i += pGameTiles[i].m_Skip;
}
// render loading before going through all components
m_Menus.RenderLoading(pConnectCaption, pLoadMapContent, 0, false);
for(auto &pComponent : m_vpAll)
{
pComponent->OnMapLoad();
pComponent->OnReset();
}
Client()->SetLoadingStateDetail(IClient::LOADING_STATE_DETAIL_GETTING_READY);
m_Menus.RenderLoading(pConnectCaption, Localize("Sending initial client info"), 0, false);
2010-05-29 07:25:38 +00:00
m_ServerMode = SERVERMODE_PURE;
2018-02-04 15:00:47 +00:00
// send the initial info
2010-05-29 07:25:38 +00:00
SendInfo(true);
2014-02-13 18:53:30 +00:00
// we should keep this in for now, because otherwise you can't spectate
// people at start as the other info 64 packet is only sent after the first
// snap
2014-06-22 13:30:15 +00:00
Client()->Rcon("crashmeplx");
m_GameWorld.Clear();
m_GameWorld.m_WorldConfig.m_InfiniteAmmo = true;
mem_zero(&m_GameInfo, sizeof(m_GameInfo));
m_PredictedDummyID = -1;
Refactor config manager, move config variable handling Move all code for handling of config variables from console to config manager. The console no longer depends on the config manager, instead the config manager now depends on the console. Add `struct`s to manage config variables of different types (int, color and string). The config manager now keeps a list of all config variables, so usage of the preprocessor can be avoided except for code to initially create all config variables. Additionally, a separate list of just the game config variables (config variables with `CFGFLAG_GAME`) is kept to optimize the `ResetGameSettings` function, as this function is called whenever connecting to a server and should be fast. Previously, this function was even less efficient because it preformed a linear search for every individual game config variable to find the respective command data. Move console commands that opperate only on config variables (`reset`, `toggle` and `+toggle`) to config manager. Ensure that these commands only opperate on the desired config variables (client or server, respectively) by checking `IConsole::FlagMask`. Add `IConfigManager::SetReadOnly` function to set/unset config variables as read-only instead of manually overriding the command handlers for the `sv_rescue` and `sv_test_cmds` config variables. This also fixes that read-only config variables could still be changed by using the `reset` command. A console message is now printed when trying to change a read-only config variable. Removing the special handling for these two config variables is additionally useful so the console does not need to keep a pointer to config values and manager. Use a `CHeap` for the config variables, their help texts and the previous values of string config variables to avoid many separate allocations as well usage of static variables. Also use the heap to store the unknown commands instead of using `std::string`s. Properly trigger command chain when resetting config variables with the `reset` command and when resetting game settings on map loading. Closes #7461. Format default value for color variables as RGB/RGBA hex with dollar sign prefix. Closes #5523. Add log message when using `reset` with a variable that does not exist. Use `log_error` instead of `dbg_msg` when saving config file fails. Support unlimited number of config save callbacks instead of at most 16. The code also becomes more readable by using an `std::vector` instead of a fixed-size array and a separate num variable. Consistently name `MACRO_CONFIG_*` parameters when declaring the macros. Add `IConsole::CMDLINE_LENGTH` constant to represent the maximum length of the console input and thereby reduce usage of magic numbers for buffer sizes.
2023-11-22 22:07:08 +00:00
ConfigManager()->ResetGameSettings();
2019-09-08 22:53:07 +00:00
LoadMapSettings();
if(Client()->State() != IClient::STATE_DEMOPLAYBACK && g_Config.m_ClAutoDemoOnConnect)
Client()->DemoRecorder_HandleAutoStart();
}
2010-05-29 07:25:38 +00:00
void CGameClient::OnReset()
{
m_aLastNewPredictedTick[0] = -1;
m_aLastNewPredictedTick[1] = -1;
m_aLocalTuneZone[0] = 0;
m_aLocalTuneZone[1] = 0;
2021-05-12 16:57:50 +00:00
m_aExpectingTuningForZone[0] = -1;
m_aExpectingTuningForZone[1] = -1;
2021-05-12 16:57:50 +00:00
m_aReceivedTuning[0] = false;
m_aReceivedTuning[1] = false;
2021-05-12 16:57:50 +00:00
InvalidateSnapshot();
2020-10-26 14:14:07 +00:00
for(auto &Client : m_aClients)
Client.Reset();
for(auto &pComponent : m_vpAll)
pComponent->OnReset();
2011-01-06 03:46:10 +00:00
m_DemoSpecID = SPEC_FOLLOW;
m_aFlagDropTick[TEAM_RED] = 0;
m_aFlagDropTick[TEAM_BLUE] = 0;
m_LastRoundStartTick = -1;
m_LastFlagCarrierRed = -4;
m_LastFlagCarrierBlue = -4;
m_aTuning[g_Config.m_ClDummy] = CTuningParams();
m_Teams.Reset();
m_aDDRaceMsgSent[0] = false;
m_aDDRaceMsgSent[1] = false;
m_aShowOthers[0] = SHOW_OTHERS_NOT_SET;
m_aShowOthers[1] = SHOW_OTHERS_NOT_SET;
2021-01-21 16:07:07 +00:00
m_LastZoom = .0;
m_LastScreenAspect = .0;
m_LastDummyConnected = false;
m_ReceivedDDNetPlayer = false;
Editor()->ResetMentions();
Editor()->ResetIngameMoved();
}
void CGameClient::UpdatePositions()
{
// local character position
2010-05-29 07:25:38 +00:00
if(g_Config.m_ClPredict && Client()->State() != IClient::STATE_DEMOPLAYBACK)
{
if(!AntiPingPlayers())
{
if(!m_Snap.m_pLocalCharacter || (m_Snap.m_pGameInfoObj && m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER))
2013-10-09 14:02:23 +00:00
{
// don't use predicted
}
else
m_LocalCharacterPos = mix(m_PredictedPrevChar.m_Pos, m_PredictedChar.m_Pos, Client()->PredIntraGameTick(g_Config.m_ClDummy));
}
else
2013-10-09 14:02:23 +00:00
{
if(!(m_Snap.m_pGameInfoObj && m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER))
2013-10-09 14:02:23 +00:00
{
if(m_Snap.m_pLocalCharacter)
m_LocalCharacterPos = mix(m_PredictedPrevChar.m_Pos, m_PredictedChar.m_Pos, Client()->PredIntraGameTick(g_Config.m_ClDummy));
2013-10-09 14:02:23 +00:00
}
// else
// m_LocalCharacterPos = mix(m_PredictedPrevChar.m_Pos, m_PredictedChar.m_Pos, Client()->PredIntraGameTick(g_Config.m_ClDummy));
2013-10-09 14:02:23 +00:00
}
}
2010-05-29 07:25:38 +00:00
else if(m_Snap.m_pLocalCharacter && m_Snap.m_pLocalPrevCharacter)
{
2010-05-29 07:25:38 +00:00
m_LocalCharacterPos = mix(
vec2(m_Snap.m_pLocalPrevCharacter->m_X, m_Snap.m_pLocalPrevCharacter->m_Y),
vec2(m_Snap.m_pLocalCharacter->m_X, m_Snap.m_pLocalCharacter->m_Y), Client()->IntraGameTick(g_Config.m_ClDummy));
}
// spectator position
if(m_Snap.m_SpecInfo.m_Active)
{
2023-05-24 17:29:38 +00:00
if(m_MultiViewActivated)
{
HandleMultiView();
}
else if(Client()->State() == IClient::STATE_DEMOPLAYBACK && m_DemoSpecID != SPEC_FOLLOW && m_Snap.m_SpecInfo.m_SpectatorID != SPEC_FREEVIEW)
{
m_Snap.m_SpecInfo.m_Position = mix(
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),
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),
Client()->IntraGameTick(g_Config.m_ClDummy));
m_Snap.m_SpecInfo.m_UsePosition = true;
}
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)))
{
if(m_Snap.m_pPrevSpectatorInfo && m_Snap.m_pPrevSpectatorInfo->m_SpectatorID == m_Snap.m_pSpectatorInfo->m_SpectatorID)
m_Snap.m_SpecInfo.m_Position = mix(vec2(m_Snap.m_pPrevSpectatorInfo->m_X, m_Snap.m_pPrevSpectatorInfo->m_Y),
vec2(m_Snap.m_pSpectatorInfo->m_X, m_Snap.m_pSpectatorInfo->m_Y), Client()->IntraGameTick(g_Config.m_ClDummy));
else
m_Snap.m_SpecInfo.m_Position = vec2(m_Snap.m_pSpectatorInfo->m_X, m_Snap.m_pSpectatorInfo->m_Y);
m_Snap.m_SpecInfo.m_UsePosition = true;
}
}
2023-05-24 17:29:38 +00:00
if(!m_MultiViewActivated && m_MultiView.m_IsInit)
ResetMultiView();
UpdateRenderedCharacters();
}
2010-05-29 07:25:38 +00:00
void CGameClient::OnRender()
{
2023-05-24 17:29:38 +00:00
// check if multi view got activated
if(!m_MultiView.m_IsInit && m_MultiViewActivated)
{
int TeamId = 0;
if(m_Snap.m_SpecInfo.m_SpectatorID >= 0)
TeamId = m_Teams.Team(m_Snap.m_SpecInfo.m_SpectatorID);
if(TeamId > MAX_CLIENTS || TeamId < 0)
TeamId = 0;
if(!InitMultiView(TeamId))
2023-05-24 17:29:38 +00:00
{
dbg_msg("MultiView", "No players found to spectate");
ResetMultiView();
2023-05-24 17:29:38 +00:00
}
}
// update the local character and spectate position
UpdatePositions();
2021-02-10 22:02:34 +00:00
// display gfx & client warnings
for(SWarning *pWarning : {Graphics()->GetCurWarning(), Client()->GetCurWarning()})
{
if(pWarning != nullptr && m_Menus.CanDisplayWarning())
{
m_Menus.PopupWarning(pWarning->m_aWarningTitle[0] == '\0' ? Localize("Warning") : pWarning->m_aWarningTitle, pWarning->m_aWarningMsg, "Ok", pWarning->m_AutoHide ? 10s : 0s);
pWarning->m_WasShown = true;
}
}
// render all systems
for(auto &pComponent : m_vpAll)
pComponent->OnRender();
// clear all events/input for this frame
Input()->Clear();
Port line input and IME support from 0.7 Port the line input (UI edit boxes, chat, console) and Input Method Editor (IME) support from upstream. Closes #4397. General ------------------------------ Fix issues with the text input. Closes #4346. Closes #4524. Word skipping (when holding Ctrl) is overhauled to be consistent with the Windows / Firefox experience that I took as reference. Improve usability by not blinking (i.e. always rendering) the caret shortly after is has been moved. UI text input ------------------------------ Fix inconsistent mouse-based left and right scrolling (closes #4347). Support smooth left and right scrolling. Chat ------------------------------ Support keyboard-based text selection of the chat input. Mouse-based selection could be support in the future when we decide to add something like an ingame UI cursor. Support smooth up and down scrolling of the chat input, removing the old hack that offsets the input string to simulate scrolling. Console ------------------------------ Also support mouse-based text selection of the command input. Only text from either the command input or the console log can be selected at the same time. This ensures that Ctrl+C will always copy the text that is currently visually selected in the console. Check for Ctrl+C input event in event handler instead of in render function, to hopefully fix the issue that copying does not work sometimes (closes #5974 until further notice). When Ctrl+C is used to copy text from the console log, the selection is cleared. This should make it more clear when text was copied from the log. Fix an issue that was preventing the console log selection from being cleared, when all log lines are selected. Remove Ctrl+A/E hotkeys that move cursor to beginning/end respectively. Ctrl+A now selectes all text like for all other inputs. Home and End keys can still be used to go the beginning and end. Remove Ctrl+U/K hotkeys that clear everything before/after the cursor respectively. Hold shift and use Home/End to select everything instead. IME support ------------------------------ Render list of IME candidates in the client on Windows, so the candidate list can also be viewed in fullscreen mode. There is no API available to retrieve a candidate list on the other operating systems. Improve composition rendering by underlining the composition text instead of putting it in square brackets. Track active input globally to properly activate and deactivate IME through the SDL functions. Closes #1030. Closes #1008. Password rendering ------------------------------ Fix rendering of passwords containing unicode. Instead of rendering one star character for each UTF-8 `char`, render on star for every unicode codepoint. Show the composition text also for passwords. Without seeing the composition text it's hard to type a password containing those characters. The candidate window exposes the composition anyway. If you don't want to expose your password this way, e.g. while streaming, you could: 1. Use a latin password and switch off the IME for the password input with the IME hotkey. 2. Blank your screen with an external program while you are streaming and entering passwords. 3. Use binds to authenticate in rcon or to set the server browser password. Refactoring ------------------------------ Move all text input logic and general rendering to `CLineInput`. A `CLineInput` is associated with a particular `char` buffer given as a pointer either in the constructor or with `SetBuffer`. The maximum byte size of the buffer must also be specified. The maximum length in unicode codepoints can also be specified separately (e.g. on upstream, name are limited by the number of unicode codepoints instead). Add `CLineInputBuffered`, which is a `CLineInput` that own a `char` buffer of a fixed size, which is specified as a template argument. As `CLineInput` does not own a buffer anymore, this reduces duplicate code for line inputs that need their own buffer. Add `CLineInputNumber` which has additional convenience functions to consider the text as an `int` or `float`, to reduce duplicate code in those cases. In the future we could also add an input filter function so that only numbers can be entered in the number input. Add `CLineInput::SetClipboardLineCallback` to handle the case that multiple lines of text are pasted into a lineinput. This reduces duplicate code, as this behavior was previously implemented separately for chat and console. The behavior is also fixed to be consistent with the console on Windows, so the first line being pasted edits the current input text and then sends it instead of being sent on its own without the existing input text. Add `CalcFontSizeAndBoundingBox` to UI to reduce duplicate code. Expose `CalcAlignedCursorPos` as static member function to reuse it for line input. Dispatch input events to UI inputs through the event handler instead of storing them in a duplicate buffer. Use `size_t` for line input cursor position, length etc. and for `str_utf8_stats`. Add `IButtonColorFunction` to UI to describe a functions that defines colors for the Default, Active and Hovered states of UI elements. Add some default button color functions. Use button color function to reduce duplicate code in scrollbar rendering. Use `vec2` instead of two `floats` to represent the mouse positions in the text renderer. Remove `CaretPosition` again, as it does not calculate the correct Y position near line breaks due to the wrapping being different when not rendering the entire string. Instead, calculate the exact caret position when rending a text container and store the caret position in the text cursor for later use. IME usage guide (Windows) ------------------------------ 1. Install the respective language and the Microsoft-IME keyboard (e.g. for Chinese, Japanese or Korean). 2. Launch the game (or a text editor to first try out the IME). Note that Windows may track the input language separately for every application. You can change this in the Windows input settings so the input language is changed globally. 2. Switch the input language using the hotkey Windows+Space or another hotkey that you configured in the Windows input settings (Alt+Shift is the default, but you should consider disabling it, to avoid accidentally changing the input language while playing). 3. Switch from Latin/English input mode to the respective asian input mode. - Chinese: Use Ctrl+Space to switch between English and Chinese input mode. You can change this hotkey in the IME's settings. - Japanese: Use Ctrl+Space to switch between Alphanumeric and Hiragana/Katakana input mode. You can change this hotkey in the IME's settings. - Korean: Use Right Alt to switch between English and Hangul input mode. You cannot change this hotkey as of yet. - Note that the input mode is also tracked per application, but there is no setting to change this behavior as far as I know, so you'll need to switch for every application separately. 4. Start typing. The underlined text is the current composition text. While a composition is active, you can only edit the composition text. Confirm the composition with Space or by selecting a candidate from the candidate list with the arrow keys. Cancel the composition with Escape or by using Backspace to delete the composition text. Note that not all languages offer a candidate list. SDL version-specific issues ------------------------------ - 2.26.5, 2.24.2, 2.0.22: IME candidates work. But there are minor bugs when moving the composition cursor. - 2.0.18, 2.0.20: IME candidates work. - 2.0.16 (our current version): IME candidates cannot be determined with Windows API. Windows tries to draw the composition window like before, so this does not work in fullscreen mode. - 2.0.8 (upstream 0.7): IME candidates work. But this SDL version is too old for us.
2023-01-03 21:28:38 +00:00
CLineInput::RenderCandidates();
// clear new tick flags
2010-05-29 07:25:38 +00:00
m_NewTick = false;
m_NewPredictedTick = false;
if(g_Config.m_ClDummy && !Client()->DummyConnected())
g_Config.m_ClDummy = 0;
// resend player and dummy info if it was filtered by server
2021-07-12 09:43:56 +00:00
if(Client()->State() == IClient::STATE_ONLINE && !m_Menus.IsActive())
{
if(m_aCheckInfo[0] == 0)
{
if(
str_comp(m_aClients[m_aLocalIDs[0]].m_aName, Client()->PlayerName()) ||
str_comp(m_aClients[m_aLocalIDs[0]].m_aClan, g_Config.m_PlayerClan) ||
m_aClients[m_aLocalIDs[0]].m_Country != g_Config.m_PlayerCountry ||
str_comp(m_aClients[m_aLocalIDs[0]].m_aSkinName, g_Config.m_ClPlayerSkin) ||
m_aClients[m_aLocalIDs[0]].m_UseCustomColor != g_Config.m_ClPlayerUseCustomColor ||
m_aClients[m_aLocalIDs[0]].m_ColorBody != (int)g_Config.m_ClPlayerColorBody ||
m_aClients[m_aLocalIDs[0]].m_ColorFeet != (int)g_Config.m_ClPlayerColorFeet)
2014-04-28 13:19:57 +00:00
SendInfo(false);
else
m_aCheckInfo[0] = -1;
}
if(m_aCheckInfo[0] > 0)
m_aCheckInfo[0]--;
if(Client()->DummyConnected())
{
if(m_aCheckInfo[1] == 0)
{
if(
str_comp(m_aClients[m_aLocalIDs[1]].m_aName, Client()->DummyName()) ||
str_comp(m_aClients[m_aLocalIDs[1]].m_aClan, g_Config.m_ClDummyClan) ||
m_aClients[m_aLocalIDs[1]].m_Country != g_Config.m_ClDummyCountry ||
str_comp(m_aClients[m_aLocalIDs[1]].m_aSkinName, g_Config.m_ClDummySkin) ||
m_aClients[m_aLocalIDs[1]].m_UseCustomColor != g_Config.m_ClDummyUseCustomColor ||
m_aClients[m_aLocalIDs[1]].m_ColorBody != (int)g_Config.m_ClDummyColorBody ||
m_aClients[m_aLocalIDs[1]].m_ColorFeet != (int)g_Config.m_ClDummyColorFeet)
SendDummyInfo(false);
else
m_aCheckInfo[1] = -1;
}
if(m_aCheckInfo[1] > 0)
m_aCheckInfo[1]--;
2010-05-29 07:25:38 +00:00
}
}
}
2014-04-28 14:47:44 +00:00
void CGameClient::OnDummyDisconnect()
{
m_aDDRaceMsgSent[1] = false;
m_aShowOthers[1] = SHOW_OTHERS_NOT_SET;
m_aLastNewPredictedTick[1] = -1;
m_PredictedDummyID = -1;
2014-04-28 14:47:44 +00:00
}
int CGameClient::GetLastRaceTick() const
{
2021-07-12 09:43:56 +00:00
return m_Ghost.GetLastRaceTick();
}
bool CGameClient::Predict() const
{
if(!g_Config.m_ClPredict)
return false;
if(m_Snap.m_pGameInfoObj)
{
if(m_Snap.m_pGameInfoObj->m_GameStateFlags & (GAMESTATEFLAG_GAMEOVER | GAMESTATEFLAG_PAUSED))
{
return false;
}
}
if(Client()->State() == IClient::STATE_DEMOPLAYBACK)
return false;
return !m_Snap.m_SpecInfo.m_Active && m_Snap.m_pLocalCharacter;
}
ColorRGBA CGameClient::GetDDTeamColor(int DDTeam, float Lightness) const
{
// Use golden angle to generate unique colors with distinct adjacent colors.
// The first DDTeam (team 1) gets angle 0°, i.e. red hue.
const float Hue = std::fmod((DDTeam - 1) * (137.50776f / 360.0f), 1.0f);
return color_cast<ColorRGBA>(ColorHSLA(Hue, 1.0f, Lightness));
}
void CGameClient::OnRelease()
{
// release all systems
for(auto &pComponent : m_vpAll)
pComponent->OnRelease();
}
void CGameClient::OnMessage(int MsgId, CUnpacker *pUnpacker, int Conn, bool Dummy)
{
2008-08-29 05:34:18 +00:00
// special messages
2020-12-06 18:58:04 +00:00
if(MsgId == NETMSGTYPE_SV_TUNEPARAMS)
2008-08-29 05:34:18 +00:00
{
// unpack the new tuning
2010-05-29 07:25:38 +00:00
CTuningParams NewTuning;
int *pParams = (int *)&NewTuning;
// No jetpack on DDNet incompatible servers:
NewTuning.m_JetpackStrength = 0;
for(unsigned i = 0; i < sizeof(CTuningParams) / sizeof(int); i++)
2014-04-12 09:12:29 +00:00
{
2014-12-27 11:05:02 +00:00
int value = pUnpacker->GetInt();
2008-08-29 05:34:18 +00:00
2014-04-12 09:12:29 +00:00
// check for unpacking errors
if(pUnpacker->Error())
break;
2014-12-27 11:05:02 +00:00
pParams[i] = value;
2014-04-12 09:12:29 +00:00
}
2010-05-29 07:25:38 +00:00
m_ServerMode = SERVERMODE_PURE;
m_aReceivedTuning[Conn] = true;
2008-08-29 05:34:18 +00:00
// apply new tuning
m_aTuning[Conn] = NewTuning;
2008-08-29 05:34:18 +00:00
return;
}
2010-05-29 07:25:38 +00:00
void *pRawMsg = m_NetObjHandler.SecureUnpackMsg(MsgId, pUnpacker);
if(!pRawMsg)
{
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "dropped weird message '%s' (%d), failed on '%s'", m_NetObjHandler.GetMsgName(MsgId), MsgId, m_NetObjHandler.FailedMsgOn());
Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client", aBuf);
return;
}
if(Dummy)
{
if(MsgId == NETMSGTYPE_SV_CHAT && m_aLocalIDs[0] >= 0 && m_aLocalIDs[1] >= 0)
{
CNetMsg_Sv_Chat *pMsg = (CNetMsg_Sv_Chat *)pRawMsg;
if((pMsg->m_Team == 1 && (m_aClients[m_aLocalIDs[0]].m_Team != m_aClients[m_aLocalIDs[1]].m_Team || m_Teams.Team(m_aLocalIDs[0]) != m_Teams.Team(m_aLocalIDs[1]))) || pMsg->m_Team > 1)
{
2021-07-12 09:43:56 +00:00
m_Chat.OnMessage(MsgId, pRawMsg);
}
}
return; // no need of all that stuff for the dummy
}
// TODO: this should be done smarter
for(auto &pComponent : m_vpAll)
pComponent->OnMessage(MsgId, pRawMsg);
2010-05-29 07:25:38 +00:00
if(MsgId == NETMSGTYPE_SV_READYTOENTER)
{
Client()->EnterGame(Conn);
}
else if(MsgId == NETMSGTYPE_SV_EMOTICON)
{
2010-05-29 07:25:38 +00:00
CNetMsg_Sv_Emoticon *pMsg = (CNetMsg_Sv_Emoticon *)pRawMsg;
// apply
m_aClients[pMsg->m_ClientID].m_Emoticon = pMsg->m_Emoticon;
m_aClients[pMsg->m_ClientID].m_EmoticonStartTick = Client()->GameTick(Conn);
m_aClients[pMsg->m_ClientID].m_EmoticonStartFraction = Client()->IntraGameTickSincePrev(Conn);
}
2010-05-29 07:25:38 +00:00
else if(MsgId == NETMSGTYPE_SV_SOUNDGLOBAL)
{
2010-05-29 07:25:38 +00:00
if(m_SuppressEvents)
return;
// don't enqueue pseudo-global sounds from demos (created by PlayAndRecord)
2010-05-29 07:25:38 +00:00
CNetMsg_Sv_SoundGlobal *pMsg = (CNetMsg_Sv_SoundGlobal *)pRawMsg;
if(pMsg->m_SoundID == SOUND_CTF_DROP || pMsg->m_SoundID == SOUND_CTF_RETURN ||
pMsg->m_SoundID == SOUND_CTF_CAPTURE || pMsg->m_SoundID == SOUND_CTF_GRAB_EN ||
pMsg->m_SoundID == SOUND_CTF_GRAB_PL)
{
if(g_Config.m_SndGame)
2021-07-12 09:43:56 +00:00
m_Sounds.Enqueue(CSounds::CHN_GLOBAL, pMsg->m_SoundID);
}
else
{
if(g_Config.m_SndGame)
2021-07-12 09:43:56 +00:00
m_Sounds.Play(CSounds::CHN_GLOBAL, pMsg->m_SoundID, 1.0f);
}
}
else if(MsgId == NETMSGTYPE_SV_TEAMSSTATE || MsgId == NETMSGTYPE_SV_TEAMSSTATELEGACY)
{
2014-01-21 23:08:30 +00:00
unsigned int i;
for(i = 0; i < MAX_CLIENTS; i++)
{
2023-01-03 22:27:00 +00:00
const int Team = pUnpacker->GetInt();
if(!pUnpacker->Error() && Team >= TEAM_FLOCK && Team <= TEAM_SUPER)
2014-02-08 22:41:12 +00:00
m_Teams.Team(i, Team);
else
2014-01-21 23:08:30 +00:00
{
m_Teams.Team(i, 0);
break;
}
}
if(i <= 16)
2014-01-21 23:08:30 +00:00
m_Teams.m_IsDDRace16 = true;
2021-07-12 09:43:56 +00:00
m_Ghost.m_AllowRestart = true;
m_RaceDemo.m_AllowRestart = true;
}
else if(MsgId == NETMSGTYPE_SV_KILLMSG)
{
CNetMsg_Sv_KillMsg *pMsg = (CNetMsg_Sv_KillMsg *)pRawMsg;
// reset character prediction
if(!(m_GameWorld.m_WorldConfig.m_IsFNG && pMsg->m_Weapon == WEAPON_LASER))
{
m_CharOrder.GiveWeak(pMsg->m_Victim);
2019-05-04 01:48:17 +00:00
if(CCharacter *pChar = m_GameWorld.GetCharacterByID(pMsg->m_Victim))
pChar->ResetPrediction();
m_GameWorld.ReleaseHooked(pMsg->m_Victim);
}
2023-05-24 17:29:38 +00:00
// 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
if(IsMultiViewIdSet() && m_MultiViewTeam == 0 && m_aMultiViewId[pMsg->m_Victim] && !m_aClients[pMsg->m_Victim].m_Spec && !m_MultiView.m_Solo)
2023-05-24 17:29:38 +00:00
{
m_aMultiViewId[pMsg->m_Victim] = false;
2023-05-24 17:29:38 +00:00
// if everyone of a team killed, we have no ids to spectate anymore, so we disable multi view
if(!IsMultiViewIdSet())
ResetMultiView();
else
{
// the "main" tee killed, search a new one
if(m_Snap.m_SpecInfo.m_SpectatorID == pMsg->m_Victim)
{
int NewClientID = FindFirstMultiViewId();
if(NewClientID < MAX_CLIENTS && NewClientID >= 0)
{
CleanMultiViewId(NewClientID);
m_aMultiViewId[NewClientID] = true;
m_Spectator.Spectate(NewClientID);
}
}
}
2023-05-24 17:29:38 +00:00
}
}
2023-11-23 10:57:35 +00:00
else if(MsgId == NETMSGTYPE_SV_KILLMSGTEAM)
{
CNetMsg_Sv_KillMsgTeam *pMsg = (CNetMsg_Sv_KillMsgTeam *)pRawMsg;
// reset prediction
std::vector<std::pair<int, int>> vStrongWeakSorted;
for(int i = 0; i < MAX_CLIENTS; i++)
{
if(m_Teams.Team(i) == pMsg->m_Team)
{
if(CCharacter *pChar = m_GameWorld.GetCharacterByID(i))
{
pChar->ResetPrediction();
vStrongWeakSorted.emplace_back(i, pMsg->m_First == i ? MAX_CLIENTS : pChar ? pChar->GetStrongWeakID() : 0);
}
m_GameWorld.ReleaseHooked(i);
}
}
std::stable_sort(vStrongWeakSorted.begin(), vStrongWeakSorted.end(), [](auto &Left, auto &Right) { return Left.second > Right.second; });
for(auto ID : vStrongWeakSorted)
{
m_CharOrder.GiveWeak(ID.first);
}
}
}
2010-05-29 07:25:38 +00:00
void CGameClient::OnStateChange(int NewState, int OldState)
{
2010-05-29 07:25:38 +00:00
// reset everything when not already connected (to keep gathered stuff)
if(NewState < IClient::STATE_ONLINE)
OnReset();
// then change the state
for(auto &pComponent : m_vpAll)
pComponent->OnStateChange(NewState, OldState);
}
void CGameClient::OnShutdown()
{
RenderShutdownMessage();
for(auto &pComponent : m_vpAll)
pComponent->OnShutdown();
}
void CGameClient::OnEnterGame()
{
2021-07-12 09:43:56 +00:00
m_Effects.ResetDamageIndicator();
}
void CGameClient::OnGameOver()
{
if(Client()->State() != IClient::STATE_DEMOPLAYBACK && g_Config.m_ClEditor == 0)
Client()->AutoScreenshot_Start();
}
void CGameClient::OnStartGame()
{
if(Client()->State() != IClient::STATE_DEMOPLAYBACK && !g_Config.m_ClAutoDemoOnConnect)
Client()->DemoRecorder_HandleAutoStart();
2021-07-12 09:43:56 +00:00
m_Statboard.OnReset();
}
void CGameClient::OnStartRound()
{
// In GamePaused or GameOver state RoundStartTick is updated on each tick
// hence no need to reset stats until player leaves GameOver
// and it would be a mistake to reset stats after or during the pause
m_Statboard.OnReset();
// Restart automatic race demo recording
m_RaceDemo.OnReset();
}
void CGameClient::OnFlagGrab(int TeamID)
{
if(TeamID == TEAM_RED)
m_aStats[m_Snap.m_pGameDataObj->m_FlagCarrierRed].m_FlagGrabs++;
else
m_aStats[m_Snap.m_pGameDataObj->m_FlagCarrierBlue].m_FlagGrabs++;
}
void CGameClient::OnWindowResize()
{
for(auto &pComponent : m_vpAll)
pComponent->OnWindowResize();
2020-10-12 10:29:47 +00:00
UI()->OnWindowResize();
}
2020-10-12 10:29:47 +00:00
void CGameClient::OnLanguageChange()
{
Add font index, support font family variants depending on language Add `fonts/index.json` which specifies: - List of all font files that should be loaded (filenames). - Default font (specified by family name or by family and style name). - Font variants for different languages, using the name of the language file as key. - Fallback fonts. - Icon font. There are characters (e.g. all in `刃直海角骨入`) that look different depending on the language of the content being Japanese, Simplified Chinese, Traditional Chinese and Hangul, because Unicode uses the same codepoint for characters regardless of the language. To render these characters correctly, the active variant font is switched depending on the selected language. The `ITextRender` interface is changed so the current language variant can be set using `SetFontLanguageVariant` and the default and icon fonts can be toggled using `SetFontPreset`. The class `CFont` is removed entirely. The text render is restructured: The font faces and font atlas are now managed by a separate class `CGlyphMap` like on upstream. As the text fill and outline textures always have the same size, the texture skyline only needs to be stored once and free positions in the atlas only need to be calculated once for each glyph instead of separately for the fill and outline textures. The font files and their licenses are also updated: - Update Source Han Sans to version 2.001. - Update Glow Sans Japanese Compressed to version 0.93. - Update Deja Vu Sans to version 2.37. - Update Font Awesome icons font to March 2023 version. Closes #6881.
2023-07-17 13:49:41 +00:00
// The actual language change is delayed because it
// might require clearing the text render font atlas,
// which would invalidate text that is currently drawn.
m_LanguageChanged = true;
}
void CGameClient::HandleLanguageChanged()
{
if(!m_LanguageChanged)
return;
m_LanguageChanged = false;
g_Localization.Load(g_Config.m_ClLanguagefile, Storage(), Console());
TextRender()->SetFontLanguageVariant(g_Config.m_ClLanguagefile);
// Clear all text containers
Client()->OnWindowResize();
2020-10-12 10:29:47 +00:00
}
void CGameClient::RenderShutdownMessage()
{
const char *pMessage = nullptr;
if(Client()->State() == IClient::STATE_QUITTING)
pMessage = Localize("Quitting. Please wait…");
else if(Client()->State() == IClient::STATE_RESTARTING)
pMessage = Localize("Restarting. Please wait…");
else
dbg_assert(false, "Invalid client state for quitting message");
// This function only gets called after the render loop has already terminated, so we have to call Swap manually.
Graphics()->Clear(0.0f, 0.0f, 0.0f);
UI()->MapScreen();
TextRender()->TextColor(TextRender()->DefaultTextColor());
UI()->DoLabel(UI()->Screen(), pMessage, 16.0f, TEXTALIGN_MC);
Graphics()->Swap();
Graphics()->Clear(0.0f, 0.0f, 0.0f);
}
void CGameClient::OnRconType(bool UsernameReq)
{
2021-07-12 09:43:56 +00:00
m_GameConsole.RequireUsername(UsernameReq);
}
2010-05-29 07:25:38 +00:00
void CGameClient::OnRconLine(const char *pLine)
{
2021-07-12 09:43:56 +00:00
m_GameConsole.PrintLine(CGameConsole::CONSOLETYPE_REMOTE, pLine);
2010-05-29 07:25:38 +00:00
}
2010-05-29 07:25:38 +00:00
void CGameClient::ProcessEvents()
{
2010-05-29 07:25:38 +00:00
if(m_SuppressEvents)
return;
2010-05-29 07:25:38 +00:00
int SnapType = IClient::SNAP_CURRENT;
int Num = Client()->SnapNumItems(SnapType);
for(int Index = 0; Index < Num; Index++)
{
2010-05-29 07:25:38 +00:00
IClient::CSnapItem Item;
const void *pData = Client()->SnapGetItem(SnapType, Index, &Item);
// We don't have enough info about us, others, to know a correct alpha value.
float Alpha = 1.0f;
2010-05-29 07:25:38 +00:00
if(Item.m_Type == NETEVENTTYPE_DAMAGEIND)
{
CNetEvent_DamageInd *pEvent = (CNetEvent_DamageInd *)pData;
m_Effects.DamageIndicator(vec2(pEvent->m_X, pEvent->m_Y), direction(pEvent->m_Angle / 256.0f), Alpha);
}
2010-05-29 07:25:38 +00:00
else if(Item.m_Type == NETEVENTTYPE_EXPLOSION)
{
CNetEvent_Explosion *pEvent = (CNetEvent_Explosion *)pData;
m_Effects.Explosion(vec2(pEvent->m_X, pEvent->m_Y), Alpha);
}
2010-05-29 07:25:38 +00:00
else if(Item.m_Type == NETEVENTTYPE_HAMMERHIT)
{
CNetEvent_HammerHit *pEvent = (CNetEvent_HammerHit *)pData;
m_Effects.HammerHit(vec2(pEvent->m_X, pEvent->m_Y), Alpha);
}
2010-05-29 07:25:38 +00:00
else if(Item.m_Type == NETEVENTTYPE_SPAWN)
{
CNetEvent_Spawn *pEvent = (CNetEvent_Spawn *)pData;
m_Effects.PlayerSpawn(vec2(pEvent->m_X, pEvent->m_Y), Alpha);
}
2010-05-29 07:25:38 +00:00
else if(Item.m_Type == NETEVENTTYPE_DEATH)
{
CNetEvent_Death *pEvent = (CNetEvent_Death *)pData;
m_Effects.PlayerDeath(vec2(pEvent->m_X, pEvent->m_Y), pEvent->m_ClientID, Alpha);
}
2010-05-29 07:25:38 +00:00
else if(Item.m_Type == NETEVENTTYPE_SOUNDWORLD)
{
CNetEvent_SoundWorld *pEvent = (CNetEvent_SoundWorld *)pData;
if(!Config()->m_SndGame)
continue;
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)))
continue;
m_Sounds.PlayAt(CSounds::CHN_WORLD, pEvent->m_SoundID, 1.0f, vec2(pEvent->m_X, pEvent->m_Y));
}
}
}
static CGameInfo GetGameInfo(const CNetObj_GameInfoEx *pInfoEx, int InfoExSize, CServerInfo *pFallbackServerInfo)
{
int Version = -1;
2020-09-25 06:17:14 +00:00
if(InfoExSize >= 12)
{
2019-06-13 22:24:50 +00:00
Version = pInfoEx->m_Version;
}
2020-09-25 06:17:14 +00:00
else if(InfoExSize >= 8)
{
Version = minimum(pInfoEx->m_Version, 4);
}
2019-06-13 22:24:50 +00:00
else if(InfoExSize >= 4)
{
2019-06-13 22:24:50 +00:00
Version = 0;
}
int Flags = 0;
if(Version >= 0)
{
Flags = pInfoEx->m_Flags;
}
int Flags2 = 0;
if(Version >= 5)
{
Flags2 = pInfoEx->m_Flags2;
}
2019-06-13 22:24:50 +00:00
bool Race;
bool FastCap;
bool FNG;
bool DDRace;
bool DDNet;
bool BlockWorlds;
2020-09-24 10:01:12 +00:00
bool City;
2019-06-13 22:24:50 +00:00
bool Vanilla;
bool Plus;
2020-12-21 15:45:19 +00:00
bool FDDrace;
2019-06-13 22:24:50 +00:00
if(Version < 1)
{
const char *pGameType = pFallbackServerInfo->m_aGameType;
Race = str_find_nocase(pGameType, "race") || str_find_nocase(pGameType, "fastcap");
FastCap = str_find_nocase(pGameType, "fastcap");
FNG = str_find_nocase(pGameType, "fng");
DDRace = str_find_nocase(pGameType, "ddrace") || str_find_nocase(pGameType, "mkrace");
DDNet = str_find_nocase(pGameType, "ddracenet") || str_find_nocase(pGameType, "ddnet");
BlockWorlds = str_startswith(pGameType, "bw ") || str_comp_nocase(pGameType, "bw") == 0;
City = str_find_nocase(pGameType, "city");
Vanilla = str_comp(pGameType, "DM") == 0 || str_comp(pGameType, "TDM") == 0 || str_comp(pGameType, "CTF") == 0;
Plus = str_find(pGameType, "+");
2020-12-21 15:45:19 +00:00
FDDrace = false;
2019-06-13 22:24:50 +00:00
}
else
{
2020-09-24 10:01:54 +00:00
Race = Flags & GAMEINFOFLAG_GAMETYPE_RACE;
FastCap = Flags & GAMEINFOFLAG_GAMETYPE_FASTCAP;
FNG = Flags & GAMEINFOFLAG_GAMETYPE_FNG;
DDRace = Flags & GAMEINFOFLAG_GAMETYPE_DDRACE;
DDNet = Flags & GAMEINFOFLAG_GAMETYPE_DDNET;
BlockWorlds = Flags & GAMEINFOFLAG_GAMETYPE_BLOCK_WORLDS;
Vanilla = Flags & GAMEINFOFLAG_GAMETYPE_VANILLA;
Plus = Flags & GAMEINFOFLAG_GAMETYPE_PLUS;
2020-09-25 06:17:14 +00:00
City = Version >= 5 && Flags2 & GAMEINFOFLAG2_GAMETYPE_CITY;
2020-12-21 15:45:19 +00:00
FDDrace = Version >= 6 && Flags2 & GAMEINFOFLAG2_GAMETYPE_FDDRACE;
2019-06-13 22:24:50 +00:00
// Ensure invariants upheld by the server info parsing business.
2020-12-21 15:45:19 +00:00
DDRace = DDRace || DDNet || FDDrace;
Race = Race || FastCap || DDRace;
}
CGameInfo Info;
Info.m_FlagStartsRace = FastCap;
Info.m_TimeScore = Race;
Info.m_UnlimitedAmmo = Race;
2019-07-19 09:28:08 +00:00
Info.m_DDRaceRecordMessage = DDRace && !DDNet;
Info.m_RaceRecordMessage = DDNet || (Race && !DDRace);
2022-04-16 17:36:25 +00:00
Info.m_RaceSounds = DDRace || FNG || BlockWorlds;
2020-09-24 10:01:12 +00:00
Info.m_AllowEyeWheel = DDRace || BlockWorlds || City || Plus;
Info.m_AllowHookColl = DDRace;
2020-09-24 10:01:12 +00:00
Info.m_AllowZoom = Race || BlockWorlds || City;
Info.m_BugDDRaceGhost = DDRace;
Info.m_BugDDRaceInput = DDRace;
Info.m_BugFNGLaserRange = FNG;
Info.m_BugVanillaBounce = Vanilla;
Info.m_PredictFNG = FNG;
Info.m_PredictDDRace = DDRace;
Info.m_PredictDDRaceTiles = DDRace && !BlockWorlds;
Info.m_PredictVanilla = Vanilla || FastCap;
Info.m_EntitiesDDNet = DDNet;
Info.m_EntitiesDDRace = DDRace;
Info.m_EntitiesRace = Race;
Info.m_EntitiesFNG = FNG;
Info.m_EntitiesVanilla = Vanilla;
2020-05-19 14:49:28 +00:00
Info.m_EntitiesBW = BlockWorlds;
2019-07-19 09:28:08 +00:00
Info.m_Race = Race;
Info.m_DontMaskEntities = !DDNet;
2020-09-25 06:17:14 +00:00
Info.m_AllowXSkins = false;
2020-12-21 15:45:19 +00:00
Info.m_EntitiesFDDrace = FDDrace;
Info.m_HudHealthArmor = true;
Info.m_HudAmmo = true;
Info.m_HudDDRace = false;
Info.m_NoWeakHookAndBounce = false;
Info.m_NoSkinChangeForFrozen = false;
if(Version >= 0)
{
Info.m_TimeScore = Flags & GAMEINFOFLAG_TIMESCORE;
}
if(Version >= 2)
{
Info.m_FlagStartsRace = Flags & GAMEINFOFLAG_FLAG_STARTS_RACE;
Info.m_UnlimitedAmmo = Flags & GAMEINFOFLAG_UNLIMITED_AMMO;
Info.m_DDRaceRecordMessage = Flags & GAMEINFOFLAG_DDRACE_RECORD_MESSAGE;
Info.m_RaceRecordMessage = Flags & GAMEINFOFLAG_RACE_RECORD_MESSAGE;
Info.m_AllowEyeWheel = Flags & GAMEINFOFLAG_ALLOW_EYE_WHEEL;
Info.m_AllowHookColl = Flags & GAMEINFOFLAG_ALLOW_HOOK_COLL;
Info.m_AllowZoom = Flags & GAMEINFOFLAG_ALLOW_ZOOM;
Info.m_BugDDRaceGhost = Flags & GAMEINFOFLAG_BUG_DDRACE_GHOST;
Info.m_BugDDRaceInput = Flags & GAMEINFOFLAG_BUG_DDRACE_INPUT;
Info.m_BugFNGLaserRange = Flags & GAMEINFOFLAG_BUG_FNG_LASER_RANGE;
Info.m_BugVanillaBounce = Flags & GAMEINFOFLAG_BUG_VANILLA_BOUNCE;
Info.m_PredictFNG = Flags & GAMEINFOFLAG_PREDICT_FNG;
Info.m_PredictDDRace = Flags & GAMEINFOFLAG_PREDICT_DDRACE;
Info.m_PredictDDRaceTiles = Flags & GAMEINFOFLAG_PREDICT_DDRACE_TILES;
Info.m_PredictVanilla = Flags & GAMEINFOFLAG_PREDICT_VANILLA;
Info.m_EntitiesDDNet = Flags & GAMEINFOFLAG_ENTITIES_DDNET;
Info.m_EntitiesDDRace = Flags & GAMEINFOFLAG_ENTITIES_DDRACE;
Info.m_EntitiesRace = Flags & GAMEINFOFLAG_ENTITIES_RACE;
Info.m_EntitiesFNG = Flags & GAMEINFOFLAG_ENTITIES_FNG;
Info.m_EntitiesVanilla = Flags & GAMEINFOFLAG_ENTITIES_VANILLA;
}
2019-07-19 08:21:32 +00:00
if(Version >= 3)
{
Info.m_Race = Flags & GAMEINFOFLAG_RACE;
Info.m_DontMaskEntities = Flags & GAMEINFOFLAG_DONT_MASK_ENTITIES;
2019-07-19 08:21:32 +00:00
}
2020-09-25 06:17:14 +00:00
if(Version >= 4)
2020-05-19 15:13:34 +00:00
{
Info.m_EntitiesBW = Flags & GAMEINFOFLAG_ENTITIES_BW;
2020-05-19 15:13:34 +00:00
}
2020-09-25 06:17:14 +00:00
if(Version >= 5)
{
Info.m_AllowXSkins = Flags2 & GAMEINFOFLAG2_ALLOW_X_SKINS;
}
2020-12-21 15:45:19 +00:00
if(Version >= 6)
{
Info.m_EntitiesFDDrace = Flags2 & GAMEINFOFLAG2_ENTITIES_FDDRACE;
}
if(Version >= 7)
{
Info.m_HudHealthArmor = Flags2 & GAMEINFOFLAG2_HUD_HEALTH_ARMOR;
Info.m_HudAmmo = Flags2 & GAMEINFOFLAG2_HUD_AMMO;
Info.m_HudDDRace = Flags2 & GAMEINFOFLAG2_HUD_DDRACE;
}
if(Version >= 8)
{
2022-10-27 15:50:08 +00:00
Info.m_NoWeakHookAndBounce = Flags2 & GAMEINFOFLAG2_NO_WEAK_HOOK;
}
if(Version >= 9)
{
Info.m_NoSkinChangeForFrozen = Flags2 & GAMEINFOFLAG2_NO_SKIN_CHANGE_FOR_FROZEN;
}
return Info;
}
void CGameClient::InvalidateSnapshot()
{
// clear all pointers
2021-07-12 09:43:56 +00:00
mem_zero(&m_Snap, sizeof(m_Snap));
m_Snap.m_LocalClientID = -1;
SnapCollectEntities();
}
void CGameClient::OnNewSnapshot()
{
auto &&Evolve = [this](CNetObj_Character *pCharacter, int Tick) {
2021-07-12 10:04:45 +00:00
CWorldCore TempWorld;
CCharacterCore TempCore = CCharacterCore();
CTeamsCore TempTeams = CTeamsCore();
2021-07-12 10:04:45 +00:00
TempCore.Init(&TempWorld, Collision(), &TempTeams);
TempCore.Read(pCharacter);
TempCore.m_ActiveWeapon = pCharacter->m_Weapon;
2021-07-12 10:04:45 +00:00
while(pCharacter->m_Tick < Tick)
{
pCharacter->m_Tick++;
TempCore.Tick(false);
TempCore.Move();
TempCore.Quantize();
}
TempCore.Write(pCharacter);
};
InvalidateSnapshot();
m_NewTick = true;
2010-05-29 07:25:38 +00:00
ProcessEvents();
#ifdef CONF_DEBUG
2010-05-29 07:25:38 +00:00
if(g_Config.m_DbgStress)
{
if((Client()->GameTick(g_Config.m_ClDummy) % 100) == 0)
{
2010-05-29 07:25:38 +00:00
char aMessage[64];
int MsgLen = rand() % (sizeof(aMessage) - 1);
2010-05-29 07:25:38 +00:00
for(int i = 0; i < MsgLen; i++)
2018-10-02 18:52:21 +00:00
aMessage[i] = (char)('a' + (rand() % ('z' - 'a')));
2010-05-29 07:25:38 +00:00
aMessage[MsgLen] = 0;
2010-05-29 07:25:38 +00:00
CNetMsg_Cl_Say Msg;
Msg.m_Team = rand() & 1;
2010-05-29 07:25:38 +00:00
Msg.m_pMessage = aMessage;
Client()->SendPackMsgActive(&Msg, MSGFLAG_VITAL);
}
}
#endif
bool FoundGameInfoEx = false;
bool GotSwitchStateTeam = false;
m_aSwitchStateTeam[g_Config.m_ClDummy] = -1;
2020-10-26 14:14:07 +00:00
for(auto &Client : m_aClients)
{
2020-10-26 14:14:07 +00:00
Client.m_SpecCharPresent = false;
}
2022-10-25 16:51:56 +00:00
// go through all the items in the snapshot and gather the info we want
{
2011-01-03 11:50:38 +00:00
m_Snap.m_aTeamSize[TEAM_RED] = m_Snap.m_aTeamSize[TEAM_BLUE] = 0;
2010-05-29 07:25:38 +00:00
int Num = Client()->SnapNumItems(IClient::SNAP_CURRENT);
for(int i = 0; i < Num; i++)
{
2010-05-29 07:25:38 +00:00
IClient::CSnapItem Item;
const void *pData = Client()->SnapGetItem(IClient::SNAP_CURRENT, i, &Item);
2010-05-29 07:25:38 +00:00
if(Item.m_Type == NETOBJTYPE_CLIENTINFO)
{
2010-05-29 07:25:38 +00:00
const CNetObj_ClientInfo *pInfo = (const CNetObj_ClientInfo *)pData;
int ClientID = Item.m_ID;
if(ClientID < MAX_CLIENTS)
{
CClientData *pClient = &m_aClients[ClientID];
IntsToStr(&pInfo->m_Name0, 4, pClient->m_aName);
IntsToStr(&pInfo->m_Clan0, 3, pClient->m_aClan);
pClient->m_Country = pInfo->m_Country;
IntsToStr(&pInfo->m_Skin0, 6, pClient->m_aSkinName);
pClient->m_UseCustomColor = pInfo->m_UseCustomColor;
pClient->m_ColorBody = pInfo->m_ColorBody;
pClient->m_ColorFeet = pInfo->m_ColorFeet;
// prepare the info
if(!m_GameInfo.m_AllowXSkins && (pClient->m_aSkinName[0] == 'x' && pClient->m_aSkinName[1] == '_'))
2022-07-09 16:14:56 +00:00
str_copy(pClient->m_aSkinName, "default");
pClient->m_SkinInfo.m_ColorBody = color_cast<ColorRGBA>(ColorHSLA(pClient->m_ColorBody).UnclampLighting());
pClient->m_SkinInfo.m_ColorFeet = color_cast<ColorRGBA>(ColorHSLA(pClient->m_ColorFeet).UnclampLighting());
pClient->m_SkinInfo.m_Size = 64;
// find new skin
const CSkin *pSkin = m_Skins.Find(pClient->m_aSkinName);
pClient->m_SkinInfo.m_OriginalRenderSkin = pSkin->m_OriginalSkin;
pClient->m_SkinInfo.m_ColorableRenderSkin = pSkin->m_ColorableSkin;
2020-11-08 05:39:16 +00:00
pClient->m_SkinInfo.m_SkinMetrics = pSkin->m_Metrics;
pClient->m_SkinInfo.m_BloodColor = pSkin->m_BloodColor;
pClient->m_SkinInfo.m_CustomColoredSkin = pClient->m_UseCustomColor;
if(!pClient->m_UseCustomColor)
{
pClient->m_SkinInfo.m_ColorBody = ColorRGBA(1, 1, 1);
pClient->m_SkinInfo.m_ColorFeet = ColorRGBA(1, 1, 1);
}
2021-07-12 10:04:45 +00:00
pClient->UpdateRenderInfo(IsTeamPlay());
}
}
2010-05-29 07:25:38 +00:00
else if(Item.m_Type == NETOBJTYPE_PLAYERINFO)
{
2010-05-29 07:25:38 +00:00
const CNetObj_PlayerInfo *pInfo = (const CNetObj_PlayerInfo *)pData;
if(pInfo->m_ClientID < MAX_CLIENTS && pInfo->m_ClientID == Item.m_ID)
{
m_aClients[pInfo->m_ClientID].m_Team = pInfo->m_Team;
m_aClients[pInfo->m_ClientID].m_Active = true;
m_Snap.m_apPlayerInfos[pInfo->m_ClientID] = pInfo;
m_Snap.m_NumPlayers++;
if(pInfo->m_Local)
{
m_Snap.m_LocalClientID = pInfo->m_ClientID;
m_Snap.m_pLocalInfo = pInfo;
if(pInfo->m_Team == TEAM_SPECTATORS)
{
m_Snap.m_SpecInfo.m_Active = true;
}
}
// calculate team-balance
if(pInfo->m_Team != TEAM_SPECTATORS)
{
m_Snap.m_aTeamSize[pInfo->m_Team]++;
if(!m_aStats[pInfo->m_ClientID].IsActive())
m_aStats[pInfo->m_ClientID].JoinGame(Client()->GameTick(g_Config.m_ClDummy));
}
else if(m_aStats[pInfo->m_ClientID].IsActive())
m_aStats[pInfo->m_ClientID].JoinSpec(Client()->GameTick(g_Config.m_ClDummy));
2015-05-19 22:51:02 +00:00
}
}
2019-05-14 23:03:30 +00:00
else if(Item.m_Type == NETOBJTYPE_DDNETPLAYER)
{
m_ReceivedDDNetPlayer = true;
2019-05-14 23:03:30 +00:00
const CNetObj_DDNetPlayer *pInfo = (const CNetObj_DDNetPlayer *)pData;
if(Item.m_ID < MAX_CLIENTS)
{
m_aClients[Item.m_ID].m_AuthLevel = pInfo->m_AuthLevel;
m_aClients[Item.m_ID].m_Afk = pInfo->m_Flags & EXPLAYERFLAG_AFK;
m_aClients[Item.m_ID].m_Paused = pInfo->m_Flags & EXPLAYERFLAG_PAUSED;
m_aClients[Item.m_ID].m_Spec = pInfo->m_Flags & EXPLAYERFLAG_SPEC;
2022-03-14 12:25:47 +00:00
if(Item.m_ID == m_Snap.m_LocalClientID && (m_aClients[Item.m_ID].m_Paused || m_aClients[Item.m_ID].m_Spec))
{
m_Snap.m_SpecInfo.m_Active = true;
}
}
2019-05-14 23:03:30 +00:00
}
2010-05-29 07:25:38 +00:00
else if(Item.m_Type == NETOBJTYPE_CHARACTER)
{
if(Item.m_ID < MAX_CLIENTS)
2008-09-23 07:43:41 +00:00
{
const void *pOld = Client()->SnapFindItem(IClient::SNAP_PREV, NETOBJTYPE_CHARACTER, Item.m_ID);
m_Snap.m_aCharacters[Item.m_ID].m_Cur = *((const CNetObj_Character *)pData);
if(pOld)
2019-11-09 21:49:53 +00:00
{
m_Snap.m_aCharacters[Item.m_ID].m_Active = true;
m_Snap.m_aCharacters[Item.m_ID].m_Prev = *((const CNetObj_Character *)pOld);
// limit evolving to 3 seconds
bool EvolvePrev = Client()->PrevGameTick(g_Config.m_ClDummy) - m_Snap.m_aCharacters[Item.m_ID].m_Prev.m_Tick <= 3 * Client()->GameTickSpeed();
bool EvolveCur = Client()->GameTick(g_Config.m_ClDummy) - m_Snap.m_aCharacters[Item.m_ID].m_Cur.m_Tick <= 3 * Client()->GameTickSpeed();
// reuse the result from the previous evolve if the snapped character didn't change since the previous snapshot
if(EvolveCur && m_aClients[Item.m_ID].m_Evolved.m_Tick == Client()->PrevGameTick(g_Config.m_ClDummy))
{
if(mem_comp(&m_Snap.m_aCharacters[Item.m_ID].m_Prev, &m_aClients[Item.m_ID].m_Snapped, sizeof(CNetObj_Character)) == 0)
m_Snap.m_aCharacters[Item.m_ID].m_Prev = m_aClients[Item.m_ID].m_Evolved;
if(mem_comp(&m_Snap.m_aCharacters[Item.m_ID].m_Cur, &m_aClients[Item.m_ID].m_Snapped, sizeof(CNetObj_Character)) == 0)
m_Snap.m_aCharacters[Item.m_ID].m_Cur = m_aClients[Item.m_ID].m_Evolved;
}
if(EvolvePrev && m_Snap.m_aCharacters[Item.m_ID].m_Prev.m_Tick)
Evolve(&m_Snap.m_aCharacters[Item.m_ID].m_Prev, Client()->PrevGameTick(g_Config.m_ClDummy));
if(EvolveCur && m_Snap.m_aCharacters[Item.m_ID].m_Cur.m_Tick)
Evolve(&m_Snap.m_aCharacters[Item.m_ID].m_Cur, Client()->GameTick(g_Config.m_ClDummy));
m_aClients[Item.m_ID].m_Snapped = *((const CNetObj_Character *)pData);
m_aClients[Item.m_ID].m_Evolved = m_Snap.m_aCharacters[Item.m_ID].m_Cur;
}
else
{
m_aClients[Item.m_ID].m_Evolved.m_Tick = -1;
2019-11-09 21:49:53 +00:00
}
2008-09-23 07:43:41 +00:00
}
}
else if(Item.m_Type == NETOBJTYPE_DDNETCHARACTER)
{
const CNetObj_DDNetCharacter *pCharacterData = (const CNetObj_DDNetCharacter *)pData;
if(Item.m_ID < MAX_CLIENTS)
{
m_Snap.m_aCharacters[Item.m_ID].m_ExtendedData = *pCharacterData;
m_Snap.m_aCharacters[Item.m_ID].m_PrevExtendedData = (const CNetObj_DDNetCharacter *)Client()->SnapFindItem(IClient::SNAP_PREV, NETOBJTYPE_DDNETCHARACTER, Item.m_ID);
m_Snap.m_aCharacters[Item.m_ID].m_HasExtendedData = true;
m_Snap.m_aCharacters[Item.m_ID].m_HasExtendedDisplayInfo = false;
if(pCharacterData->m_JumpedTotal != -1)
{
m_Snap.m_aCharacters[Item.m_ID].m_HasExtendedDisplayInfo = true;
}
CClientData *pClient = &m_aClients[Item.m_ID];
// Collision
pClient->m_Solo = pCharacterData->m_Flags & CHARACTERFLAG_SOLO;
2021-08-08 10:36:25 +00:00
pClient->m_Jetpack = pCharacterData->m_Flags & CHARACTERFLAG_JETPACK;
pClient->m_CollisionDisabled = pCharacterData->m_Flags & CHARACTERFLAG_COLLISION_DISABLED;
pClient->m_HammerHitDisabled = pCharacterData->m_Flags & CHARACTERFLAG_HAMMER_HIT_DISABLED;
pClient->m_GrenadeHitDisabled = pCharacterData->m_Flags & CHARACTERFLAG_GRENADE_HIT_DISABLED;
pClient->m_LaserHitDisabled = pCharacterData->m_Flags & CHARACTERFLAG_LASER_HIT_DISABLED;
pClient->m_ShotgunHitDisabled = pCharacterData->m_Flags & CHARACTERFLAG_SHOTGUN_HIT_DISABLED;
pClient->m_HookHitDisabled = pCharacterData->m_Flags & CHARACTERFLAG_HOOK_HIT_DISABLED;
pClient->m_Super = pCharacterData->m_Flags & CHARACTERFLAG_SUPER;
// Endless
pClient->m_EndlessHook = pCharacterData->m_Flags & CHARACTERFLAG_ENDLESS_HOOK;
pClient->m_EndlessJump = pCharacterData->m_Flags & CHARACTERFLAG_ENDLESS_JUMP;
// Freeze
pClient->m_FreezeEnd = pCharacterData->m_FreezeEnd;
pClient->m_DeepFrozen = pCharacterData->m_FreezeEnd == -1;
pClient->m_LiveFrozen = (pCharacterData->m_Flags & CHARACTERFLAG_MOVEMENTS_DISABLED) != 0;
// Telegun
pClient->m_HasTelegunGrenade = pCharacterData->m_Flags & CHARACTERFLAG_TELEGUN_GRENADE;
pClient->m_HasTelegunGun = pCharacterData->m_Flags & CHARACTERFLAG_TELEGUN_GUN;
pClient->m_HasTelegunLaser = pCharacterData->m_Flags & CHARACTERFLAG_TELEGUN_LASER;
pClient->m_Predicted.ReadDDNet(pCharacterData);
2023-05-29 11:35:39 +00:00
m_Teams.SetSolo(Item.m_ID, pClient->m_Solo);
}
}
2020-06-24 17:01:01 +00:00
else if(Item.m_Type == NETOBJTYPE_SPECCHAR)
{
const CNetObj_SpecChar *pSpecCharData = (const CNetObj_SpecChar *)pData;
if(Item.m_ID < MAX_CLIENTS)
{
CClientData *pClient = &m_aClients[Item.m_ID];
pClient->m_SpecCharPresent = true;
pClient->m_SpecChar.x = pSpecCharData->m_X;
pClient->m_SpecChar.y = pSpecCharData->m_Y;
}
2020-06-24 17:01:01 +00:00
}
else if(Item.m_Type == NETOBJTYPE_SPECTATORINFO)
{
m_Snap.m_pSpectatorInfo = (const CNetObj_SpectatorInfo *)pData;
m_Snap.m_pPrevSpectatorInfo = (const CNetObj_SpectatorInfo *)Client()->SnapFindItem(IClient::SNAP_PREV, NETOBJTYPE_SPECTATORINFO, Item.m_ID);
m_Snap.m_SpecInfo.m_SpectatorID = m_Snap.m_pSpectatorInfo->m_SpectatorID;
}
2011-03-04 16:08:10 +00:00
else if(Item.m_Type == NETOBJTYPE_GAMEINFO)
{
2011-03-04 16:08:10 +00:00
m_Snap.m_pGameInfoObj = (const CNetObj_GameInfo *)pData;
2018-10-02 18:52:21 +00:00
bool CurrentTickGameOver = (bool)(m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER);
if(!m_GameOver && CurrentTickGameOver)
OnGameOver();
else if(m_GameOver && !CurrentTickGameOver)
OnStartGame();
// Handle case that a new round is started (RoundStartTick changed)
// New round is usually started after `restart` on server
if(m_Snap.m_pGameInfoObj->m_RoundStartTick != m_LastRoundStartTick && !(CurrentTickGameOver || m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_PAUSED || m_GamePaused))
OnStartRound();
m_LastRoundStartTick = m_Snap.m_pGameInfoObj->m_RoundStartTick;
m_GameOver = CurrentTickGameOver;
m_GamePaused = (bool)(m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_PAUSED);
2011-03-04 16:08:10 +00:00
}
else if(Item.m_Type == NETOBJTYPE_GAMEINFOEX)
2019-05-21 08:11:02 +00:00
{
if(FoundGameInfoEx)
{
continue;
}
FoundGameInfoEx = true;
CServerInfo ServerInfo;
Client()->GetServerInfo(&ServerInfo);
m_GameInfo = GetGameInfo((const CNetObj_GameInfoEx *)pData, Client()->SnapItemSize(IClient::SNAP_CURRENT, i), &ServerInfo);
2019-05-21 08:11:02 +00:00
}
2011-03-04 16:08:10 +00:00
else if(Item.m_Type == NETOBJTYPE_GAMEDATA)
{
m_Snap.m_pGameDataObj = (const CNetObj_GameData *)pData;
m_Snap.m_GameDataSnapID = Item.m_ID;
if(m_Snap.m_pGameDataObj->m_FlagCarrierRed == FLAG_TAKEN)
{
if(m_aFlagDropTick[TEAM_RED] == 0)
m_aFlagDropTick[TEAM_RED] = Client()->GameTick(g_Config.m_ClDummy);
}
else
m_aFlagDropTick[TEAM_RED] = 0;
if(m_Snap.m_pGameDataObj->m_FlagCarrierBlue == FLAG_TAKEN)
{
if(m_aFlagDropTick[TEAM_BLUE] == 0)
m_aFlagDropTick[TEAM_BLUE] = Client()->GameTick(g_Config.m_ClDummy);
}
else
m_aFlagDropTick[TEAM_BLUE] = 0;
if(m_LastFlagCarrierRed == FLAG_ATSTAND && m_Snap.m_pGameDataObj->m_FlagCarrierRed >= 0)
OnFlagGrab(TEAM_RED);
else if(m_LastFlagCarrierBlue == FLAG_ATSTAND && m_Snap.m_pGameDataObj->m_FlagCarrierBlue >= 0)
OnFlagGrab(TEAM_BLUE);
m_LastFlagCarrierRed = m_Snap.m_pGameDataObj->m_FlagCarrierRed;
m_LastFlagCarrierBlue = m_Snap.m_pGameDataObj->m_FlagCarrierBlue;
}
2010-05-29 07:25:38 +00:00
else if(Item.m_Type == NETOBJTYPE_FLAG)
m_Snap.m_apFlags[Item.m_ID % 2] = (const CNetObj_Flag *)pData;
2021-08-15 10:53:14 +00:00
else if(Item.m_Type == NETOBJTYPE_SWITCHSTATE)
{
2022-06-02 13:40:55 +00:00
if(Item.m_DataSize < 36)
{
continue;
}
2021-08-15 10:53:14 +00:00
const CNetObj_SwitchState *pSwitchStateData = (const CNetObj_SwitchState *)pData;
int Team = clamp(Item.m_ID, (int)TEAM_FLOCK, (int)TEAM_SUPER - 1);
2021-08-15 10:53:14 +00:00
int HighestSwitchNumber = clamp(pSwitchStateData->m_HighestSwitchNumber, 0, 255);
if(HighestSwitchNumber != maximum(0, (int)Switchers().size() - 1))
{
m_GameWorld.m_Core.InitSwitchers(HighestSwitchNumber);
Collision()->m_HighestSwitchNumber = HighestSwitchNumber;
}
2021-08-15 10:53:14 +00:00
for(int j = 0; j < (int)Switchers().size(); j++)
2021-08-15 10:53:14 +00:00
{
Switchers()[j].m_aStatus[Team] = (pSwitchStateData->m_aStatus[j / 32] >> (j % 32)) & 1;
}
2022-06-02 13:40:55 +00:00
if(Item.m_DataSize >= 68)
{
// update the endtick of up to four timed switchers
for(int j = 0; j < (int)std::size(pSwitchStateData->m_aEndTicks); j++)
{
int SwitchNumber = pSwitchStateData->m_aSwitchNumbers[j];
int EndTick = pSwitchStateData->m_aEndTicks[j];
if(EndTick > 0 && in_range(SwitchNumber, 0, (int)Switchers().size()))
{
Switchers()[SwitchNumber].m_aEndTick[Team] = EndTick;
}
}
}
// update switch types
for(auto &Switcher : Switchers())
{
if(Switcher.m_aStatus[Team])
Switcher.m_aType[Team] = Switcher.m_aEndTick[Team] ? TILE_SWITCHTIMEDOPEN : TILE_SWITCHOPEN;
else
Switcher.m_aType[Team] = Switcher.m_aEndTick[Team] ? TILE_SWITCHTIMEDCLOSE : TILE_SWITCHCLOSE;
2021-08-15 10:53:14 +00:00
}
if(!GotSwitchStateTeam)
m_aSwitchStateTeam[g_Config.m_ClDummy] = Team;
else
m_aSwitchStateTeam[g_Config.m_ClDummy] = -1;
GotSwitchStateTeam = true;
2021-08-15 10:53:14 +00:00
}
2008-09-23 07:43:41 +00:00
}
}
if(!FoundGameInfoEx)
{
CServerInfo ServerInfo;
Client()->GetServerInfo(&ServerInfo);
m_GameInfo = GetGameInfo(0, 0, &ServerInfo);
}
2008-09-23 07:43:41 +00:00
// setup local pointers
if(m_Snap.m_LocalClientID >= 0)
2008-09-23 07:43:41 +00:00
{
m_aLocalIDs[g_Config.m_ClDummy] = m_Snap.m_LocalClientID;
2020-06-16 15:19:53 +00:00
CSnapState::CCharacterInfo *pChr = &m_Snap.m_aCharacters[m_Snap.m_LocalClientID];
if(pChr->m_Active)
2008-09-23 07:43:41 +00:00
{
2016-09-20 14:34:49 +00:00
if(!m_Snap.m_SpecInfo.m_Active)
{
m_Snap.m_pLocalCharacter = &pChr->m_Cur;
m_Snap.m_pLocalPrevCharacter = &pChr->m_Prev;
2016-09-20 14:34:49 +00:00
m_LocalCharacterPos = vec2(m_Snap.m_pLocalCharacter->m_X, m_Snap.m_pLocalCharacter->m_Y);
}
}
else if(Client()->SnapFindItem(IClient::SNAP_PREV, NETOBJTYPE_CHARACTER, m_Snap.m_LocalClientID))
2010-08-10 11:54:13 +00:00
{
// player died
2021-07-12 09:43:56 +00:00
m_Controls.OnPlayerDeath();
2010-08-10 11:54:13 +00:00
}
}
if(Client()->State() == IClient::STATE_DEMOPLAYBACK)
{
if(m_Snap.m_LocalClientID == -1 && m_DemoSpecID == SPEC_FOLLOW)
m_DemoSpecID = SPEC_FREEVIEW;
if(m_DemoSpecID != SPEC_FOLLOW)
{
m_Snap.m_SpecInfo.m_Active = true;
if(m_DemoSpecID > SPEC_FREEVIEW && m_Snap.m_aCharacters[m_DemoSpecID].m_Active)
m_Snap.m_SpecInfo.m_SpectatorID = m_DemoSpecID;
else
m_Snap.m_SpecInfo.m_SpectatorID = SPEC_FREEVIEW;
}
}
// clear out unneeded client data
for(int i = 0; i < MAX_CLIENTS; ++i)
{
if(!m_Snap.m_apPlayerInfos[i] && m_aClients[i].m_Active)
{
m_aClients[i].Reset();
m_aStats[i].Reset();
}
}
2011-03-23 12:06:35 +00:00
for(int i = 0; i < MAX_CLIENTS; ++i)
{
// update friend state
m_aClients[i].m_Friend = !(i == m_Snap.m_LocalClientID || !m_Snap.m_apPlayerInfos[i] || !Friends()->IsFriend(m_aClients[i].m_aName, m_aClients[i].m_aClan, true));
// update foe state
m_aClients[i].m_Foe = !(i == m_Snap.m_LocalClientID || !m_Snap.m_apPlayerInfos[i] || !Foes()->IsFriend(m_aClients[i].m_aName, m_aClients[i].m_aClan, true));
2015-07-22 20:16:49 +00:00
}
2014-02-08 21:24:57 +00:00
// sort player infos by name
mem_copy(m_Snap.m_apInfoByName, m_Snap.m_apPlayerInfos, sizeof(m_Snap.m_apInfoByName));
std::stable_sort(m_Snap.m_apInfoByName, m_Snap.m_apInfoByName + MAX_CLIENTS,
[this](const CNetObj_PlayerInfo *p1, const CNetObj_PlayerInfo *p2) -> bool {
if(!p2)
return static_cast<bool>(p1);
if(!p1)
return false;
return str_comp_nocase(m_aClients[p1->m_ClientID].m_aName, m_aClients[p2->m_ClientID].m_aName) < 0;
});
bool TimeScore = m_GameInfo.m_TimeScore;
2014-02-08 21:24:57 +00:00
// sort player infos by score
mem_copy(m_Snap.m_apInfoByScore, m_Snap.m_apInfoByName, sizeof(m_Snap.m_apInfoByScore));
std::stable_sort(m_Snap.m_apInfoByScore, m_Snap.m_apInfoByScore + MAX_CLIENTS,
[TimeScore](const CNetObj_PlayerInfo *p1, const CNetObj_PlayerInfo *p2) -> bool {
if(!p2)
return static_cast<bool>(p1);
if(!p1)
return false;
return (((TimeScore && p1->m_Score == -9999) ? std::numeric_limits<int>::min() : p1->m_Score) >
((TimeScore && p2->m_Score == -9999) ? std::numeric_limits<int>::min() : p2->m_Score));
});
2018-02-04 15:00:47 +00:00
// sort player infos by DDRace Team (and score between)
int Index = 0;
for(int Team = TEAM_FLOCK; Team <= TEAM_SUPER; ++Team)
2014-01-22 00:39:18 +00:00
{
for(int i = 0; i < MAX_CLIENTS && Index < MAX_CLIENTS; ++i)
{
if(m_Snap.m_apInfoByScore[i] && m_Teams.Team(m_Snap.m_apInfoByScore[i]->m_ClientID) == Team)
m_Snap.m_apInfoByDDTeamScore[Index++] = m_Snap.m_apInfoByScore[i];
}
}
// sort player infos by DDRace Team (and name between)
Index = 0;
for(int Team = TEAM_FLOCK; Team <= TEAM_SUPER; ++Team)
{
for(int i = 0; i < MAX_CLIENTS && Index < MAX_CLIENTS; ++i)
{
if(m_Snap.m_apInfoByName[i] && m_Teams.Team(m_Snap.m_apInfoByName[i]->m_ClientID) == Team)
m_Snap.m_apInfoByDDTeamName[Index++] = m_Snap.m_apInfoByName[i];
2014-01-22 00:39:18 +00:00
}
}
CServerInfo CurrentServerInfo;
Client()->GetServerInfo(&CurrentServerInfo);
2010-05-29 07:25:38 +00:00
CTuningParams StandardTuning;
if(CurrentServerInfo.m_aGameType[0] != '0')
{
2010-05-29 07:25:38 +00:00
if(str_comp(CurrentServerInfo.m_aGameType, "DM") != 0 && str_comp(CurrentServerInfo.m_aGameType, "TDM") != 0 && str_comp(CurrentServerInfo.m_aGameType, "CTF") != 0)
m_ServerMode = SERVERMODE_MOD;
else if(mem_comp(&StandardTuning, &m_aTuning[g_Config.m_ClDummy], 33) == 0)
2010-05-29 07:25:38 +00:00
m_ServerMode = SERVERMODE_PURE;
else
2010-05-29 07:25:38 +00:00
m_ServerMode = SERVERMODE_PUREMOD;
}
2011-01-06 03:46:10 +00:00
2012-01-08 23:49:20 +00:00
// add tuning to demo
bool AnyRecording = false;
for(int i = 0; i < RECORDER_MAX; i++)
if(DemoRecorder(i)->IsRecording())
{
AnyRecording = true;
break;
}
if(AnyRecording && mem_comp(&StandardTuning, &m_aTuning[g_Config.m_ClDummy], sizeof(CTuningParams)) != 0)
2012-01-08 23:49:20 +00:00
{
CMsgPacker Msg(NETMSGTYPE_SV_TUNEPARAMS);
int *pParams = (int *)&m_aTuning[g_Config.m_ClDummy];
for(unsigned i = 0; i < sizeof(m_aTuning[0]) / sizeof(int); i++)
2012-01-08 23:49:20 +00:00
Msg.AddInt(pParams[i]);
Client()->SendMsgActive(&Msg, MSGFLAG_RECORD | MSGFLAG_NOSEND);
2012-01-08 23:49:20 +00:00
}
for(int i = 0; i < 2; i++)
{
if(m_aDDRaceMsgSent[i] || !m_Snap.m_pLocalInfo)
{
continue;
}
if(i == IClient::CONN_DUMMY && !Client()->DummyConnected())
{
continue;
}
CMsgPacker Msg(NETMSGTYPE_CL_ISDDNETLEGACY, false);
Msg.AddInt(DDNetVersion());
Client()->SendMsg(i, &Msg, MSGFLAG_VITAL);
m_aDDRaceMsgSent[i] = true;
}
2011-02-23 20:22:05 +00:00
if(m_Snap.m_SpecInfo.m_Active && m_MultiViewActivated)
2023-05-24 17:29:38 +00:00
{
// dont show other teams while spectating in multi view
CNetMsg_Cl_ShowOthers Msg;
Msg.m_Show = SHOW_OTHERS_ONLY_TEAM;
Client()->SendPackMsgActive(&Msg, MSGFLAG_VITAL);
// update state
m_aShowOthers[g_Config.m_ClDummy] = SHOW_OTHERS_ONLY_TEAM;
}
else if(m_aShowOthers[g_Config.m_ClDummy] == SHOW_OTHERS_NOT_SET || m_aShowOthers[g_Config.m_ClDummy] != g_Config.m_ClShowOthers)
{
{
CNetMsg_Cl_ShowOthers Msg;
2014-05-24 22:59:52 +00:00
Msg.m_Show = g_Config.m_ClShowOthers;
Client()->SendPackMsgActive(&Msg, MSGFLAG_VITAL);
}
// update state
m_aShowOthers[g_Config.m_ClDummy] = g_Config.m_ClShowOthers;
}
2017-10-06 20:01:33 +00:00
2021-07-12 09:43:56 +00:00
float ZoomToSend = m_Camera.m_Zoom;
2021-09-10 22:14:02 +00:00
if(m_Camera.m_Zooming)
2021-02-20 21:51:36 +00:00
{
2021-07-12 09:43:56 +00:00
if(m_Camera.m_ZoomSmoothingTarget > m_Camera.m_Zoom) // Zooming out
ZoomToSend = m_Camera.m_ZoomSmoothingTarget;
else if(m_Camera.m_ZoomSmoothingTarget < m_Camera.m_Zoom && m_LastZoom > 0) // Zooming in
2021-02-20 21:51:36 +00:00
ZoomToSend = m_LastZoom;
}
2021-01-21 16:07:07 +00:00
if(ZoomToSend != m_LastZoom || Graphics()->ScreenAspect() != m_LastScreenAspect || (Client()->DummyConnected() && !m_LastDummyConnected))
{
CNetMsg_Cl_ShowDistance Msg;
float x, y;
2021-01-21 16:07:07 +00:00
RenderTools()->CalcScreenParams(Graphics()->ScreenAspect(), ZoomToSend, &x, &y);
Msg.m_X = x;
Msg.m_Y = y;
Client()->ChecksumData()->m_Zoom = ZoomToSend;
CMsgPacker Packer(&Msg);
2020-07-08 21:25:07 +00:00
Msg.Pack(&Packer);
2021-01-21 16:07:07 +00:00
if(ZoomToSend != m_LastZoom)
Client()->SendMsg(IClient::CONN_MAIN, &Packer, MSGFLAG_VITAL);
2020-07-08 21:25:07 +00:00
if(Client()->DummyConnected())
Client()->SendMsg(IClient::CONN_DUMMY, &Packer, MSGFLAG_VITAL);
2021-01-21 16:07:07 +00:00
m_LastZoom = ZoomToSend;
m_LastScreenAspect = Graphics()->ScreenAspect();
}
2021-01-21 16:07:07 +00:00
m_LastDummyConnected = Client()->DummyConnected();
2023-04-15 10:47:33 +00:00
for(auto &pComponent : m_vpAll)
pComponent->OnNewSnapshot();
// notify editor when local character moved
UpdateEditorIngameMoved();
2021-04-22 12:52:49 +00:00
// detect air jump for other players
for(int i = 0; i < MAX_CLIENTS; i++)
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))
2021-04-22 12:52:49 +00:00
if(!Predict() || (i != m_Snap.m_LocalClientID && (!AntiPingPlayers() || i != m_PredictedDummyID)))
{
vec2 Pos = mix(vec2(m_Snap.m_aCharacters[i].m_Prev.m_X, m_Snap.m_aCharacters[i].m_Prev.m_Y),
vec2(m_Snap.m_aCharacters[i].m_Cur.m_X, m_Snap.m_aCharacters[i].m_Cur.m_Y),
Client()->IntraGameTick(g_Config.m_ClDummy));
2023-05-11 21:04:05 +00:00
float Alpha = 1.0f;
bool SameTeam = m_Teams.SameTeam(m_Snap.m_LocalClientID, i);
if(!SameTeam || m_aClients[i].m_Solo || m_aClients[m_Snap.m_LocalClientID].m_Solo)
Alpha = g_Config.m_ClShowOthersAlpha / 100.0f;
m_Effects.AirJump(Pos, Alpha);
}
if(m_Snap.m_LocalClientID != m_PrevLocalID)
m_PredictedDummyID = m_PrevLocalID;
m_PrevLocalID = m_Snap.m_LocalClientID;
m_IsDummySwapping = 0;
SnapCollectEntities(); // creates a collection that associates EntityEx snap items with the entities they belong to
// update prediction data
if(Client()->State() != IClient::STATE_DEMOPLAYBACK)
UpdatePrediction();
}
void CGameClient::UpdateEditorIngameMoved()
{
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);
if(!g_Config.m_ClEditor)
{
m_EditorMovementDelay = 5;
}
else if(m_EditorMovementDelay > 0 && !LocalCharacterMoved)
{
--m_EditorMovementDelay;
}
if(m_EditorMovementDelay == 0 && LocalCharacterMoved)
{
Editor()->OnIngameMoved();
}
}
2010-05-29 07:25:38 +00:00
void CGameClient::OnPredict()
{
2008-09-23 07:43:41 +00:00
// store the previous values so we can detect prediction errors
2010-05-29 07:25:38 +00:00
CCharacterCore BeforePrevChar = m_PredictedPrevChar;
CCharacterCore BeforeChar = m_PredictedChar;
2008-09-23 07:43:41 +00:00
// we can't predict without our own id or own character
if(m_Snap.m_LocalClientID == -1 || !m_Snap.m_aCharacters[m_Snap.m_LocalClientID].m_Active)
2008-09-23 07:43:41 +00:00
return;
// don't predict anything if we are paused
if(m_Snap.m_pGameInfoObj && m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_PAUSED)
{
2010-05-29 07:25:38 +00:00
if(m_Snap.m_pLocalCharacter)
2014-04-14 08:56:14 +00:00
{
m_PredictedChar.Read(m_Snap.m_pLocalCharacter);
m_PredictedChar.m_ActiveWeapon = m_Snap.m_pLocalCharacter->m_Weapon;
2014-04-14 08:56:14 +00:00
}
2010-05-29 07:25:38 +00:00
if(m_Snap.m_pLocalPrevCharacter)
2014-04-14 08:56:14 +00:00
{
m_PredictedPrevChar.Read(m_Snap.m_pLocalPrevCharacter);
m_PredictedPrevChar.m_ActiveWeapon = m_Snap.m_pLocalPrevCharacter->m_Weapon;
2014-04-14 08:56:14 +00:00
}
return;
}
2008-09-23 07:43:41 +00:00
vec2 aBeforeRender[MAX_CLIENTS];
for(int i = 0; i < MAX_CLIENTS; i++)
aBeforeRender[i] = GetSmoothPos(i);
2015-01-11 16:36:38 +00:00
// init
bool Dummy = g_Config.m_ClDummy ^ m_IsDummySwapping;
m_PredictedWorld.CopyWorld(&m_GameWorld);
2021-02-07 19:51:28 +00:00
// don't predict inactive players, or entities from other teams
2008-09-23 07:43:41 +00:00
for(int i = 0; i < MAX_CLIENTS; i++)
if(CCharacter *pChar = m_PredictedWorld.GetCharacterByID(i))
2021-02-07 19:51:28 +00:00
if((!m_Snap.m_aCharacters[i].m_Active && pChar->m_SnapTicks > 10) || IsOtherTeam(i))
pChar->Destroy();
2021-02-07 19:51:28 +00:00
CProjectile *pProjNext = 0;
for(CProjectile *pProj = (CProjectile *)m_PredictedWorld.FindFirst(CGameWorld::ENTTYPE_PROJECTILE); pProj; pProj = pProjNext)
{
pProjNext = (CProjectile *)pProj->TypeNext();
if(IsOtherTeam(pProj->GetOwner()))
{
pProj->Destroy();
}
2021-02-07 19:51:28 +00:00
}
CCharacter *pLocalChar = m_PredictedWorld.GetCharacterByID(m_Snap.m_LocalClientID);
if(!pLocalChar)
return;
CCharacter *pDummyChar = 0;
if(PredictDummy())
pDummyChar = m_PredictedWorld.GetCharacterByID(m_PredictedDummyID);
// predict
for(int Tick = Client()->GameTick(g_Config.m_ClDummy) + 1; Tick <= Client()->PredGameTick(g_Config.m_ClDummy); Tick++)
{
// fetch the previous characters
if(Tick == Client()->PredGameTick(g_Config.m_ClDummy))
{
m_PrevPredictedWorld.CopyWorld(&m_PredictedWorld);
m_PredictedPrevChar = pLocalChar->GetCore();
for(int i = 0; i < MAX_CLIENTS; i++)
if(CCharacter *pChar = m_PredictedWorld.GetCharacterByID(i))
m_aClients[i].m_PrevPredicted = pChar->GetCore();
}
// optionally allow some movement in freeze by not predicting freeze the last one to two ticks
if(g_Config.m_ClPredictFreeze == 2 && Client()->PredGameTick(g_Config.m_ClDummy) - 1 - Client()->PredGameTick(g_Config.m_ClDummy) % 2 <= Tick)
pLocalChar->m_CanMoveInFreeze = true;
// apply inputs and tick
CNetObj_PlayerInput *pInputData = (CNetObj_PlayerInput *)Client()->GetInput(Tick, m_IsDummySwapping);
CNetObj_PlayerInput *pDummyInputData = !pDummyChar ? 0 : (CNetObj_PlayerInput *)Client()->GetInput(Tick, m_IsDummySwapping ^ 1);
bool DummyFirst = pInputData && pDummyInputData && pDummyChar->GetCID() < pLocalChar->GetCID();
if(DummyFirst)
pDummyChar->OnDirectInput(pDummyInputData);
if(pInputData)
pLocalChar->OnDirectInput(pInputData);
if(pDummyInputData && !DummyFirst)
pDummyChar->OnDirectInput(pDummyInputData);
m_PredictedWorld.m_GameTick = Tick;
if(pInputData)
pLocalChar->OnPredictedInput(pInputData);
if(pDummyInputData)
pDummyChar->OnPredictedInput(pDummyInputData);
m_PredictedWorld.Tick();
// fetch the current characters
if(Tick == Client()->PredGameTick(g_Config.m_ClDummy))
{
m_PredictedChar = pLocalChar->GetCore();
for(int i = 0; i < MAX_CLIENTS; i++)
if(CCharacter *pChar = m_PredictedWorld.GetCharacterByID(i))
m_aClients[i].m_Predicted = pChar->GetCore();
}
for(int i = 0; i < MAX_CLIENTS; i++)
if(CCharacter *pChar = m_PredictedWorld.GetCharacterByID(i))
{
m_aClients[i].m_aPredPos[Tick % 200] = pChar->Core()->m_Pos;
m_aClients[i].m_aPredTick[Tick % 200] = Tick;
}
// check if we want to trigger effects
if(Tick > m_aLastNewPredictedTick[Dummy])
{
m_aLastNewPredictedTick[Dummy] = Tick;
m_NewPredictedTick = true;
vec2 Pos = pLocalChar->Core()->m_Pos;
int Events = pLocalChar->Core()->m_TriggeredEvents;
if(g_Config.m_ClPredict && !m_SuppressEvents)
if(Events & COREEVENT_AIR_JUMP)
m_Effects.AirJump(Pos, 1.0f);
if(g_Config.m_SndGame && !m_SuppressEvents)
{
if(Events & COREEVENT_GROUND_JUMP)
2021-07-12 09:43:56 +00:00
m_Sounds.PlayAndRecord(CSounds::CHN_WORLD, SOUND_PLAYER_JUMP, 1.0f, Pos);
if(Events & COREEVENT_HOOK_ATTACH_GROUND)
2021-07-12 09:43:56 +00:00
m_Sounds.PlayAndRecord(CSounds::CHN_WORLD, SOUND_HOOK_ATTACH_GROUND, 1.0f, Pos);
if(Events & COREEVENT_HOOK_HIT_NOHOOK)
2021-07-12 09:43:56 +00:00
m_Sounds.PlayAndRecord(CSounds::CHN_WORLD, SOUND_HOOK_NOATTACH, 1.0f, Pos);
}
}
2021-04-22 12:52:49 +00:00
// check if we want to trigger predicted airjump for dummy
if(AntiPingPlayers() && pDummyChar && Tick > m_aLastNewPredictedTick[!Dummy])
2021-04-22 12:52:49 +00:00
{
m_aLastNewPredictedTick[!Dummy] = Tick;
2021-04-22 12:52:49 +00:00
vec2 Pos = pDummyChar->Core()->m_Pos;
2021-04-22 12:58:30 +00:00
int Events = pDummyChar->Core()->m_TriggeredEvents;
if(g_Config.m_ClPredict && !m_SuppressEvents)
2021-04-22 12:52:49 +00:00
if(Events & COREEVENT_AIR_JUMP)
m_Effects.AirJump(Pos, 1.0f);
2021-04-22 12:52:49 +00:00
}
}
// detect mispredictions of other players and make corrections smoother when possible
if(g_Config.m_ClAntiPingSmooth && Predict() && AntiPingPlayers() && m_NewTick && absolute(m_PredictedTick - Client()->PredGameTick(g_Config.m_ClDummy)) <= 1 && absolute(Client()->GameTick(g_Config.m_ClDummy) - Client()->PrevGameTick(g_Config.m_ClDummy)) <= 2)
{
int PredTime = clamp(Client()->GetPredictionTime(), 0, 800);
float SmoothPace = 4 - 1.5f * PredTime / 800.f; // smoothing pace (a lower value will make the smoothing quicker)
2021-06-23 05:05:49 +00:00
int64_t Len = 1000 * PredTime * SmoothPace;
2018-10-02 18:45:44 +00:00
for(int i = 0; i < MAX_CLIENTS; i++)
{
if(!m_Snap.m_aCharacters[i].m_Active || i == m_Snap.m_LocalClientID || !m_aLastActive[i])
continue;
vec2 NewPos = (m_PredictedTick == Client()->PredGameTick(g_Config.m_ClDummy)) ? m_aClients[i].m_Predicted.m_Pos : m_aClients[i].m_PrevPredicted.m_Pos;
vec2 PredErr = (m_aLastPos[i] - NewPos) / (float)minimum(Client()->GetPredictionTime(), 200);
if(in_range(length(PredErr), 0.05f, 5.f))
2018-10-02 18:45:44 +00:00
{
vec2 PredPos = mix(m_aClients[i].m_PrevPredicted.m_Pos, m_aClients[i].m_Predicted.m_Pos, Client()->PredIntraGameTick(g_Config.m_ClDummy));
vec2 CurPos = mix(
vec2(m_Snap.m_aCharacters[i].m_Prev.m_X, m_Snap.m_aCharacters[i].m_Prev.m_Y),
vec2(m_Snap.m_aCharacters[i].m_Cur.m_X, m_Snap.m_aCharacters[i].m_Cur.m_Y),
Client()->IntraGameTick(g_Config.m_ClDummy));
vec2 RenderDiff = PredPos - aBeforeRender[i];
vec2 PredDiff = PredPos - CurPos;
float aMixAmount[2];
for(int j = 0; j < 2; j++)
2018-10-02 18:45:44 +00:00
{
aMixAmount[j] = 1.0f;
if(absolute(PredErr[j]) > 0.05f)
2018-10-02 18:45:44 +00:00
{
aMixAmount[j] = 0.0f;
if(absolute(RenderDiff[j]) > 0.01f)
2018-10-02 18:45:44 +00:00
{
aMixAmount[j] = 1.f - clamp(RenderDiff[j] / PredDiff[j], 0.f, 1.f);
aMixAmount[j] = 1.f - std::pow(1.f - aMixAmount[j], 1 / 1.2f);
}
2018-10-02 18:45:44 +00:00
}
int64_t TimePassed = time_get() - m_aClients[i].m_aSmoothStart[j];
2021-06-23 05:05:49 +00:00
if(in_range(TimePassed, (int64_t)0, Len - 1))
aMixAmount[j] = minimum(aMixAmount[j], (float)(TimePassed / (double)Len));
}
for(int j = 0; j < 2; j++)
if(absolute(RenderDiff[j]) < 0.01f && absolute(PredDiff[j]) < 0.01f && absolute(m_aClients[i].m_PrevPredicted.m_Pos[j] - m_aClients[i].m_Predicted.m_Pos[j]) < 0.01f && aMixAmount[j] > aMixAmount[j ^ 1])
aMixAmount[j] = aMixAmount[j ^ 1];
for(int j = 0; j < 2; j++)
{
int64_t Remaining = minimum((1.f - aMixAmount[j]) * Len, minimum(time_freq() * 0.700f, (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
2021-06-23 05:05:49 +00:00
int64_t Start = time_get() - (Len - Remaining);
if(!in_range(Start + Len, m_aClients[i].m_aSmoothStart[j], m_aClients[i].m_aSmoothStart[j] + Len))
{
m_aClients[i].m_aSmoothStart[j] = Start;
m_aClients[i].m_aSmoothLen[j] = Len;
}
}
}
}
}
for(int i = 0; i < MAX_CLIENTS; i++)
{
if(m_Snap.m_aCharacters[i].m_Active)
2015-05-04 15:53:07 +00:00
{
m_aLastPos[i] = m_aClients[i].m_Predicted.m_Pos;
m_aLastActive[i] = true;
2015-05-04 15:53:07 +00:00
}
else
m_aLastActive[i] = false;
}
if(g_Config.m_Debug && g_Config.m_ClPredict && m_PredictedTick == Client()->PredGameTick(g_Config.m_ClDummy))
{
2010-05-29 07:25:38 +00:00
CNetObj_CharacterCore Before = {0}, Now = {0}, BeforePrev = {0}, NowPrev = {0};
BeforeChar.Write(&Before);
BeforePrevChar.Write(&BeforePrev);
m_PredictedChar.Write(&Now);
m_PredictedPrevChar.Write(&NowPrev);
2010-05-29 07:25:38 +00:00
if(mem_comp(&Before, &Now, sizeof(CNetObj_CharacterCore)) != 0)
{
Console()->Print(IConsole::OUTPUT_LEVEL_DEBUG, "client", "prediction error");
for(unsigned i = 0; i < sizeof(CNetObj_CharacterCore) / sizeof(int); i++)
2010-05-29 07:25:38 +00:00
if(((int *)&Before)[i] != ((int *)&Now)[i])
2008-09-23 07:43:41 +00:00
{
char aBuf[256];
str_format(aBuf, sizeof(aBuf), " %d %d %d (%d %d)", i, ((int *)&Before)[i], ((int *)&Now)[i], ((int *)&BeforePrev)[i], ((int *)&NowPrev)[i]);
Console()->Print(IConsole::OUTPUT_LEVEL_DEBUG, "client", aBuf);
2008-09-23 07:43:41 +00:00
}
}
}
m_PredictedTick = Client()->PredGameTick(g_Config.m_ClDummy);
2017-10-06 20:01:33 +00:00
if(m_NewPredictedTick)
2021-07-12 09:43:56 +00:00
m_Ghost.OnNewPredictedSnapshot();
}
void CGameClient::OnActivateEditor()
{
OnRelease();
}
2015-05-19 22:51:02 +00:00
CGameClient::CClientStats::CClientStats()
{
Reset();
2015-05-19 22:51:02 +00:00
}
void CGameClient::CClientStats::Reset()
{
m_JoinTick = 0;
m_IngameTicks = 0;
2015-05-19 22:51:02 +00:00
m_Active = false;
m_Frags = 0;
m_Deaths = 0;
m_Suicides = 0;
m_BestSpree = 0;
m_CurrentSpree = 0;
for(int j = 0; j < NUM_WEAPONS; j++)
{
m_aFragsWith[j] = 0;
m_aDeathsFrom[j] = 0;
}
m_FlagGrabs = 0;
m_FlagCaptures = 0;
}
2021-07-12 10:04:45 +00:00
void CGameClient::CClientData::UpdateRenderInfo(bool IsTeamPlay)
{
2010-05-29 07:25:38 +00:00
m_RenderInfo = m_SkinInfo;
// force team colors
2021-07-12 10:04:45 +00:00
if(IsTeamPlay)
{
m_RenderInfo.m_CustomColoredSkin = true;
const int aTeamColors[2] = {65461, 10223541};
2011-01-03 11:50:38 +00:00
if(m_Team >= TEAM_RED && m_Team <= TEAM_BLUE)
{
m_RenderInfo.m_ColorBody = color_cast<ColorRGBA>(ColorHSLA(aTeamColors[m_Team]));
m_RenderInfo.m_ColorFeet = color_cast<ColorRGBA>(ColorHSLA(aTeamColors[m_Team]));
}
2011-03-27 09:52:16 +00:00
else
{
2019-05-11 16:04:32 +00:00
m_RenderInfo.m_ColorBody = color_cast<ColorRGBA>(ColorHSLA(12829350));
m_RenderInfo.m_ColorFeet = color_cast<ColorRGBA>(ColorHSLA(12829350));
2011-03-27 09:52:16 +00:00
}
}
}
void CGameClient::CClientData::Reset()
{
m_aName[0] = 0;
m_aClan[0] = 0;
m_Country = -1;
m_aSkinName[0] = '\0';
m_SkinColor = 0;
m_Team = 0;
m_Angle = 0;
m_Emoticon = 0;
m_EmoticonStartTick = -1;
m_EmoticonStartFraction = 0;
m_Active = false;
m_ChatIgnore = false;
m_EmoticonIgnore = false;
m_Friend = false;
m_Foe = false;
m_AuthLevel = AUTHED_NO;
2019-08-01 21:07:40 +00:00
m_Afk = false;
2019-08-02 16:16:58 +00:00
m_Paused = false;
m_Spec = false;
m_SkinInfo.m_BloodColor = ColorRGBA(1, 1, 1);
m_SkinInfo.m_ColorableRenderSkin.Reset();
m_SkinInfo.m_OriginalRenderSkin.Reset();
m_SkinInfo.m_CustomColoredSkin = false;
m_SkinInfo.m_ColorBody = ColorRGBA(1, 1, 1);
m_SkinInfo.m_ColorFeet = ColorRGBA(1, 1, 1);
2020-11-08 05:39:16 +00:00
m_SkinInfo.m_SkinMetrics.Reset();
m_Solo = false;
m_Jetpack = false;
m_CollisionDisabled = false;
m_EndlessHook = false;
m_EndlessJump = false;
m_HammerHitDisabled = false;
m_GrenadeHitDisabled = false;
m_LaserHitDisabled = false;
m_ShotgunHitDisabled = false;
m_HookHitDisabled = false;
m_Super = false;
m_HasTelegunGun = false;
m_HasTelegunGrenade = false;
m_HasTelegunLaser = false;
m_FreezeEnd = 0;
m_DeepFrozen = false;
m_LiveFrozen = false;
2019-11-09 21:49:53 +00:00
m_Evolved.m_Tick = -1;
m_SpecChar = vec2(0, 0);
m_SpecCharPresent = false;
2020-06-24 17:01:01 +00:00
mem_zero(m_aSwitchStates, sizeof(m_aSwitchStates));
2021-08-15 10:53:14 +00:00
2021-07-12 10:04:45 +00:00
UpdateRenderInfo(false);
}
2010-05-29 07:25:38 +00:00
void CGameClient::SendSwitchTeam(int Team)
{
2010-05-29 07:25:38 +00:00
CNetMsg_Cl_SetTeam Msg;
Msg.m_Team = Team;
Client()->SendPackMsgActive(&Msg, MSGFLAG_VITAL);
2013-07-11 15:25:51 +00:00
if(Team != TEAM_SPECTATORS)
2021-07-12 09:43:56 +00:00
m_Camera.OnReset();
}
2010-05-29 07:25:38 +00:00
void CGameClient::SendInfo(bool Start)
{
2010-05-29 07:25:38 +00:00
if(Start)
{
2010-05-29 07:25:38 +00:00
CNetMsg_Cl_StartInfo Msg;
Msg.m_pName = Client()->PlayerName();
Msg.m_pClan = g_Config.m_PlayerClan;
Msg.m_Country = g_Config.m_PlayerCountry;
Msg.m_pSkin = g_Config.m_ClPlayerSkin;
Msg.m_UseCustomColor = g_Config.m_ClPlayerUseCustomColor;
2019-05-01 20:35:29 +00:00
Msg.m_ColorBody = g_Config.m_ClPlayerColorBody;
Msg.m_ColorFeet = g_Config.m_ClPlayerColorFeet;
CMsgPacker Packer(&Msg);
2014-04-28 13:19:57 +00:00
Msg.Pack(&Packer);
Client()->SendMsg(IClient::CONN_MAIN, &Packer, MSGFLAG_VITAL | MSGFLAG_FLUSH);
m_aCheckInfo[0] = -1;
}
else
{
2010-05-29 07:25:38 +00:00
CNetMsg_Cl_ChangeInfo Msg;
Msg.m_pName = Client()->PlayerName();
Msg.m_pClan = g_Config.m_PlayerClan;
Msg.m_Country = g_Config.m_PlayerCountry;
Msg.m_pSkin = g_Config.m_ClPlayerSkin;
Msg.m_UseCustomColor = g_Config.m_ClPlayerUseCustomColor;
2019-05-01 20:35:29 +00:00
Msg.m_ColorBody = g_Config.m_ClPlayerColorBody;
Msg.m_ColorFeet = g_Config.m_ClPlayerColorFeet;
CMsgPacker Packer(&Msg);
2014-04-28 13:19:57 +00:00
Msg.Pack(&Packer);
Client()->SendMsg(IClient::CONN_MAIN, &Packer, MSGFLAG_VITAL);
m_aCheckInfo[0] = Client()->GameTickSpeed();
}
}
2014-04-28 13:19:57 +00:00
void CGameClient::SendDummyInfo(bool Start)
{
if(Start)
{
CNetMsg_Cl_StartInfo Msg;
Msg.m_pName = Client()->DummyName();
Msg.m_pClan = g_Config.m_ClDummyClan;
Msg.m_Country = g_Config.m_ClDummyCountry;
Msg.m_pSkin = g_Config.m_ClDummySkin;
Msg.m_UseCustomColor = g_Config.m_ClDummyUseCustomColor;
2019-05-01 20:35:29 +00:00
Msg.m_ColorBody = g_Config.m_ClDummyColorBody;
Msg.m_ColorFeet = g_Config.m_ClDummyColorFeet;
CMsgPacker Packer(&Msg);
2014-04-28 13:19:57 +00:00
Msg.Pack(&Packer);
Client()->SendMsg(IClient::CONN_DUMMY, &Packer, MSGFLAG_VITAL);
m_aCheckInfo[1] = -1;
2014-04-28 13:19:57 +00:00
}
else
{
CNetMsg_Cl_ChangeInfo Msg;
Msg.m_pName = Client()->DummyName();
Msg.m_pClan = g_Config.m_ClDummyClan;
Msg.m_Country = g_Config.m_ClDummyCountry;
Msg.m_pSkin = g_Config.m_ClDummySkin;
Msg.m_UseCustomColor = g_Config.m_ClDummyUseCustomColor;
2019-05-01 20:35:29 +00:00
Msg.m_ColorBody = g_Config.m_ClDummyColorBody;
Msg.m_ColorFeet = g_Config.m_ClDummyColorFeet;
CMsgPacker Packer(&Msg);
2014-04-28 13:19:57 +00:00
Msg.Pack(&Packer);
Client()->SendMsg(IClient::CONN_DUMMY, &Packer, MSGFLAG_VITAL);
m_aCheckInfo[1] = Client()->GameTickSpeed();
2014-04-28 13:19:57 +00:00
}
}
void CGameClient::SendKill(int ClientID) const
{
2010-05-29 07:25:38 +00:00
CNetMsg_Cl_Kill Msg;
Client()->SendPackMsgActive(&Msg, MSGFLAG_VITAL);
2015-02-18 13:23:25 +00:00
if(g_Config.m_ClDummyCopyMoves)
{
CMsgPacker MsgP(NETMSGTYPE_CL_KILL, false);
Client()->SendMsg(!g_Config.m_ClDummy, &MsgP, MSGFLAG_VITAL);
2015-02-18 13:23:25 +00:00
}
}
void CGameClient::ConTeam(IConsole::IResult *pResult, void *pUserData)
{
((CGameClient *)pUserData)->SendSwitchTeam(pResult->GetInteger(0));
}
void CGameClient::ConKill(IConsole::IResult *pResult, void *pUserData)
{
((CGameClient *)pUserData)->SendKill(-1);
}
Add font index, support font family variants depending on language Add `fonts/index.json` which specifies: - List of all font files that should be loaded (filenames). - Default font (specified by family name or by family and style name). - Font variants for different languages, using the name of the language file as key. - Fallback fonts. - Icon font. There are characters (e.g. all in `刃直海角骨入`) that look different depending on the language of the content being Japanese, Simplified Chinese, Traditional Chinese and Hangul, because Unicode uses the same codepoint for characters regardless of the language. To render these characters correctly, the active variant font is switched depending on the selected language. The `ITextRender` interface is changed so the current language variant can be set using `SetFontLanguageVariant` and the default and icon fonts can be toggled using `SetFontPreset`. The class `CFont` is removed entirely. The text render is restructured: The font faces and font atlas are now managed by a separate class `CGlyphMap` like on upstream. As the text fill and outline textures always have the same size, the texture skyline only needs to be stored once and free positions in the atlas only need to be calculated once for each glyph instead of separately for the fill and outline textures. The font files and their licenses are also updated: - Update Source Han Sans to version 2.001. - Update Glow Sans Japanese Compressed to version 0.93. - Update Deja Vu Sans to version 2.37. - Update Font Awesome icons font to March 2023 version. Closes #6881.
2023-07-17 13:49:41 +00:00
void CGameClient::ConchainLanguageUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
{
pfnCallback(pResult, pCallbackUserData);
if(pResult->NumArguments())
((CGameClient *)pUserData)->OnLanguageChange();
}
2010-05-29 07:25:38 +00:00
void CGameClient::ConchainSpecialInfoupdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
{
pfnCallback(pResult, pCallbackUserData);
2010-05-29 07:25:38 +00:00
if(pResult->NumArguments())
((CGameClient *)pUserData)->SendInfo(false);
2009-10-27 14:38:53 +00:00
}
2014-04-28 13:19:57 +00:00
void CGameClient::ConchainSpecialDummyInfoupdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
{
pfnCallback(pResult, pCallbackUserData);
if(pResult->NumArguments())
((CGameClient *)pUserData)->SendDummyInfo(false);
2014-04-28 13:19:57 +00:00
}
void CGameClient::ConchainSpecialDummy(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
{
pfnCallback(pResult, pCallbackUserData);
if(pResult->NumArguments())
if(g_Config.m_ClDummy && !((CGameClient *)pUserData)->Client()->DummyConnected())
g_Config.m_ClDummy = 0;
}
void CGameClient::ConchainClTextEntitiesSize(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
{
pfnCallback(pResult, pCallbackUserData);
if(pResult->NumArguments())
{
CGameClient *pGameClient = (CGameClient *)pUserData;
2021-07-12 09:43:56 +00:00
pGameClient->m_MapImages.SetTextureScale(g_Config.m_ClTextEntitiesSize);
}
}
2010-05-29 07:25:38 +00:00
IGameClient *CreateGameClient()
2009-10-27 14:38:53 +00:00
{
2021-07-12 10:04:45 +00:00
return new CGameClient();
}
2013-08-23 23:50:35 +00:00
int CGameClient::IntersectCharacter(vec2 HookPos, vec2 NewPos, vec2 &NewPos2, int ownID)
2013-08-23 23:50:35 +00:00
{
2014-01-27 04:06:23 +00:00
float Distance = 0.0f;
2013-08-23 23:50:35 +00:00
int ClosestID = -1;
Declare variables as `const` when possible According to cppcheck's `constVariable` error: ``` src\engine\client\backend\opengl\opengl_sl.cpp:74:43: style: Variable 'Define' can be declared as reference to const [constVariable] for(CGLSLCompiler::SGLSLCompilerDefine &Define : pCompiler->m_vDefines) ^ src\engine\client\backend\vulkan\backend_vulkan.cpp:2149:12: style: Variable 'GraphicThreadCommandBuffer' can be declared as reference to const [constVariable] auto &GraphicThreadCommandBuffer = m_vvThreadDrawCommandBuffers[i + 1][m_CurImageIndex]; ^ src\engine\client\backend\vulkan\backend_vulkan.cpp:3192:9: style: Variable 'BufferObject' can be declared as reference to const [constVariable] auto &BufferObject = m_vBufferObjects[BufferObjectIndex]; ^ src\engine\client\backend\vulkan\backend_vulkan.cpp:3200:10: style: Variable 'DescrSet' can be declared as reference to const [constVariable] auto &DescrSet = m_vTextures[State.m_Texture].m_VKStandard3DTexturedDescrSet; ^ src\engine\client\backend\vulkan\backend_vulkan.cpp:3810:13: style: Variable 'Mode' can be declared as reference to const [constVariable] for(auto &Mode : vPresentModeList) ^ src\engine\client\backend\vulkan\backend_vulkan.cpp:3818:13: style: Variable 'Mode' can be declared as reference to const [constVariable] for(auto &Mode : vPresentModeList) ^ src\engine\client\backend\vulkan\backend_vulkan.cpp:6511:10: style: Variable 'DescrSet' can be declared as reference to const [constVariable] auto &DescrSet = m_vTextures[pCommand->m_State.m_Texture].m_aVKStandardTexturedDescrSets[AddressModeIndex]; ^ src\engine\client\backend\vulkan\backend_vulkan.cpp:6555:10: style: Variable 'DescrSet' can be declared as reference to const [constVariable] auto &DescrSet = m_vTextures[pCommand->m_State.m_Texture].m_VKStandard3DTexturedDescrSet; ^ src\engine\client\backend\vulkan\backend_vulkan.cpp:6660:9: style: Variable 'MemBlock' can be declared as reference to const [constVariable] auto &MemBlock = m_vBufferObjects[BufferIndex].m_BufferObject.m_Mem; ^ src\engine\client\backend\vulkan\backend_vulkan.cpp:6799:9: style: Variable 'BufferObject' can be declared as reference to const [constVariable] auto &BufferObject = m_vBufferObjects[BufferObjectIndex]; ^ src\engine\client\backend\vulkan\backend_vulkan.cpp:6808:10: style: Variable 'DescrSet' can be declared as reference to const [constVariable] auto &DescrSet = m_vTextures[pCommand->m_State.m_Texture].m_aVKStandardTexturedDescrSets[AddressModeIndex]; ^ src\engine\client\backend\vulkan\backend_vulkan.cpp:6902:9: style: Variable 'BufferObject' can be declared as reference to const [constVariable] auto &BufferObject = m_vBufferObjects[BufferObjectIndex]; ^ src\engine\client\backend\vulkan\backend_vulkan.cpp:6907:9: style: Variable 'TextTextureDescr' can be declared as reference to const [constVariable] auto &TextTextureDescr = m_vTextures[pCommand->m_TextTextureIndex].m_VKTextDescrSet; ^ src\engine\client\backend\vulkan\backend_vulkan.cpp:6961:9: style: Variable 'BufferObject' can be declared as reference to const [constVariable] auto &BufferObject = m_vBufferObjects[BufferObjectIndex]; ^ src\engine\client\backend\vulkan\backend_vulkan.cpp:6970:10: style: Variable 'DescrSet' can be declared as reference to const [constVariable] auto &DescrSet = m_vTextures[State.m_Texture].m_aVKStandardTexturedDescrSets[AddressModeIndex]; ^ src\game\client\components\hud.cpp:178:8: style: Variable 'aFlagCarrier' can be declared as const array [constVariable] int aFlagCarrier[2] = { ^ src\game\client\components\hud.cpp:519:16: style: Variable 's_aTextWidth' can be declared as const array [constVariable] static float s_aTextWidth[5] = {s_TextWidth0, s_TextWidth00, s_TextWidth000, s_TextWidth0000, s_TextWidth00000}; ^ src\game\client\components\killmessages.cpp:305:30: style: Variable 'Client' can be declared as reference to const [constVariable] CGameClient::CClientData &Client = GameClient()->m_aClients[m_aKillmsgs[r].m_KillerID]; ^ src\game\client\components\killmessages.cpp:314:30: style: Variable 'Client' can be declared as reference to const [constVariable] CGameClient::CClientData &Client = GameClient()->m_aClients[m_aKillmsgs[r].m_VictimID]; ^ src\game\client\components\menus_ingame.cpp:243:12: style: Variable 'pInfoByName' can be declared as reference to const [constVariable] for(auto &pInfoByName : m_pClient->m_Snap.m_apInfoByName) ^ src\game\client\components\menus_ingame.cpp:530:12: style: Variable 'pInfoByName' can be declared as reference to const [constVariable] for(auto &pInfoByName : m_pClient->m_Snap.m_apInfoByName) ^ src\game\client\components\players.cpp:767:44: style: Variable 'CharacterInfo' can be declared as reference to const [constVariable] CGameClient::CSnapState::CCharacterInfo &CharacterInfo = m_pClient->m_Snap.m_aCharacters[i]; ^ src\game\client\components\spectator.cpp:122:27: style: Variable 'Snap' can be declared as reference to const [constVariable] CGameClient::CSnapState &Snap = pSelf->m_pClient->m_Snap; ^ src\game\client\components\spectator.cpp:221:12: style: Variable 'pInfo' can be declared as reference to const [constVariable] for(auto &pInfo : m_pClient->m_Snap.m_apInfoByDDTeamName) ^ src\game\client\gameclient.cpp:2220:15: style: Variable 'OwnClientData' can be declared as reference to const [constVariable] CClientData &OwnClientData = m_aClients[ownID]; ^ src\game\client\gameclient.cpp:2227:16: style: Variable 'cData' can be declared as reference to const [constVariable] CClientData &cData = m_aClients[i]; ^ src\game\client\prediction\entities\character.cpp:397:11: style: Variable 'aSpreading' can be declared as const array [constVariable] float aSpreading[] = {-0.185f, -0.070f, 0, 0.070f, 0.185f}; ^ src\game\client\prediction\entities\laser.cpp:53:9: style: Variable 'HitPos' can be declared as reference to const [constVariable] vec2 &HitPos = pHit->Core()->m_Pos; ^ src\game\editor\auto_map.cpp:507:18: style: Variable 'Index' can be declared as reference to const [constVariable] for(auto &Index : pRule->m_vIndexList) ^ src\game\editor\auto_map.cpp:518:18: style: Variable 'Index' can be declared as reference to const [constVariable] for(auto &Index : pRule->m_vIndexList) ^ src\game\editor\editor.cpp:118:12: style: Variable 'Item' can be declared as reference to const [constVariable] for(auto &Item : vList) ^ src\game\editor\editor.cpp:2983:11: style: Variable 'aAspects' can be declared as const array [constVariable] float aAspects[] = {4.0f / 3.0f, 16.0f / 10.0f, 5.0f / 4.0f, 16.0f / 9.0f}; ^ src\game\editor\editor.cpp:3141:15: style: Variable 's_aShift' can be declared as const array [constVariable] static int s_aShift[] = {24, 16, 8, 0}; ^ src\engine\server\server.cpp:2807:14: style: Variable 'Client' can be declared as reference to const [constVariable] for(auto &Client : m_aClients) ^ src\engine\server\sql_string_helpers.cpp:51:6: style: Variable 'aTimes' can be declared as const array [constVariable] int aTimes[7] = ^ src\test\secure_random.cpp:24:6: style: Variable 'BOUNDS' can be declared as const array [constVariable] int BOUNDS[] = {2, 3, 4, 5, 10, 100, 127, 128, 129}; ^ ```
2022-11-13 15:29:13 +00:00
const CClientData &OwnClientData = m_aClients[ownID];
2014-02-08 20:55:08 +00:00
2019-04-21 17:35:07 +00:00
for(int i = 0; i < MAX_CLIENTS; i++)
2013-08-23 23:50:35 +00:00
{
2019-06-09 17:26:20 +00:00
if(i == ownID)
continue;
Declare variables as `const` when possible According to cppcheck's `constVariable` error: ``` src\engine\client\backend\opengl\opengl_sl.cpp:74:43: style: Variable 'Define' can be declared as reference to const [constVariable] for(CGLSLCompiler::SGLSLCompilerDefine &Define : pCompiler->m_vDefines) ^ src\engine\client\backend\vulkan\backend_vulkan.cpp:2149:12: style: Variable 'GraphicThreadCommandBuffer' can be declared as reference to const [constVariable] auto &GraphicThreadCommandBuffer = m_vvThreadDrawCommandBuffers[i + 1][m_CurImageIndex]; ^ src\engine\client\backend\vulkan\backend_vulkan.cpp:3192:9: style: Variable 'BufferObject' can be declared as reference to const [constVariable] auto &BufferObject = m_vBufferObjects[BufferObjectIndex]; ^ src\engine\client\backend\vulkan\backend_vulkan.cpp:3200:10: style: Variable 'DescrSet' can be declared as reference to const [constVariable] auto &DescrSet = m_vTextures[State.m_Texture].m_VKStandard3DTexturedDescrSet; ^ src\engine\client\backend\vulkan\backend_vulkan.cpp:3810:13: style: Variable 'Mode' can be declared as reference to const [constVariable] for(auto &Mode : vPresentModeList) ^ src\engine\client\backend\vulkan\backend_vulkan.cpp:3818:13: style: Variable 'Mode' can be declared as reference to const [constVariable] for(auto &Mode : vPresentModeList) ^ src\engine\client\backend\vulkan\backend_vulkan.cpp:6511:10: style: Variable 'DescrSet' can be declared as reference to const [constVariable] auto &DescrSet = m_vTextures[pCommand->m_State.m_Texture].m_aVKStandardTexturedDescrSets[AddressModeIndex]; ^ src\engine\client\backend\vulkan\backend_vulkan.cpp:6555:10: style: Variable 'DescrSet' can be declared as reference to const [constVariable] auto &DescrSet = m_vTextures[pCommand->m_State.m_Texture].m_VKStandard3DTexturedDescrSet; ^ src\engine\client\backend\vulkan\backend_vulkan.cpp:6660:9: style: Variable 'MemBlock' can be declared as reference to const [constVariable] auto &MemBlock = m_vBufferObjects[BufferIndex].m_BufferObject.m_Mem; ^ src\engine\client\backend\vulkan\backend_vulkan.cpp:6799:9: style: Variable 'BufferObject' can be declared as reference to const [constVariable] auto &BufferObject = m_vBufferObjects[BufferObjectIndex]; ^ src\engine\client\backend\vulkan\backend_vulkan.cpp:6808:10: style: Variable 'DescrSet' can be declared as reference to const [constVariable] auto &DescrSet = m_vTextures[pCommand->m_State.m_Texture].m_aVKStandardTexturedDescrSets[AddressModeIndex]; ^ src\engine\client\backend\vulkan\backend_vulkan.cpp:6902:9: style: Variable 'BufferObject' can be declared as reference to const [constVariable] auto &BufferObject = m_vBufferObjects[BufferObjectIndex]; ^ src\engine\client\backend\vulkan\backend_vulkan.cpp:6907:9: style: Variable 'TextTextureDescr' can be declared as reference to const [constVariable] auto &TextTextureDescr = m_vTextures[pCommand->m_TextTextureIndex].m_VKTextDescrSet; ^ src\engine\client\backend\vulkan\backend_vulkan.cpp:6961:9: style: Variable 'BufferObject' can be declared as reference to const [constVariable] auto &BufferObject = m_vBufferObjects[BufferObjectIndex]; ^ src\engine\client\backend\vulkan\backend_vulkan.cpp:6970:10: style: Variable 'DescrSet' can be declared as reference to const [constVariable] auto &DescrSet = m_vTextures[State.m_Texture].m_aVKStandardTexturedDescrSets[AddressModeIndex]; ^ src\game\client\components\hud.cpp:178:8: style: Variable 'aFlagCarrier' can be declared as const array [constVariable] int aFlagCarrier[2] = { ^ src\game\client\components\hud.cpp:519:16: style: Variable 's_aTextWidth' can be declared as const array [constVariable] static float s_aTextWidth[5] = {s_TextWidth0, s_TextWidth00, s_TextWidth000, s_TextWidth0000, s_TextWidth00000}; ^ src\game\client\components\killmessages.cpp:305:30: style: Variable 'Client' can be declared as reference to const [constVariable] CGameClient::CClientData &Client = GameClient()->m_aClients[m_aKillmsgs[r].m_KillerID]; ^ src\game\client\components\killmessages.cpp:314:30: style: Variable 'Client' can be declared as reference to const [constVariable] CGameClient::CClientData &Client = GameClient()->m_aClients[m_aKillmsgs[r].m_VictimID]; ^ src\game\client\components\menus_ingame.cpp:243:12: style: Variable 'pInfoByName' can be declared as reference to const [constVariable] for(auto &pInfoByName : m_pClient->m_Snap.m_apInfoByName) ^ src\game\client\components\menus_ingame.cpp:530:12: style: Variable 'pInfoByName' can be declared as reference to const [constVariable] for(auto &pInfoByName : m_pClient->m_Snap.m_apInfoByName) ^ src\game\client\components\players.cpp:767:44: style: Variable 'CharacterInfo' can be declared as reference to const [constVariable] CGameClient::CSnapState::CCharacterInfo &CharacterInfo = m_pClient->m_Snap.m_aCharacters[i]; ^ src\game\client\components\spectator.cpp:122:27: style: Variable 'Snap' can be declared as reference to const [constVariable] CGameClient::CSnapState &Snap = pSelf->m_pClient->m_Snap; ^ src\game\client\components\spectator.cpp:221:12: style: Variable 'pInfo' can be declared as reference to const [constVariable] for(auto &pInfo : m_pClient->m_Snap.m_apInfoByDDTeamName) ^ src\game\client\gameclient.cpp:2220:15: style: Variable 'OwnClientData' can be declared as reference to const [constVariable] CClientData &OwnClientData = m_aClients[ownID]; ^ src\game\client\gameclient.cpp:2227:16: style: Variable 'cData' can be declared as reference to const [constVariable] CClientData &cData = m_aClients[i]; ^ src\game\client\prediction\entities\character.cpp:397:11: style: Variable 'aSpreading' can be declared as const array [constVariable] float aSpreading[] = {-0.185f, -0.070f, 0, 0.070f, 0.185f}; ^ src\game\client\prediction\entities\laser.cpp:53:9: style: Variable 'HitPos' can be declared as reference to const [constVariable] vec2 &HitPos = pHit->Core()->m_Pos; ^ src\game\editor\auto_map.cpp:507:18: style: Variable 'Index' can be declared as reference to const [constVariable] for(auto &Index : pRule->m_vIndexList) ^ src\game\editor\auto_map.cpp:518:18: style: Variable 'Index' can be declared as reference to const [constVariable] for(auto &Index : pRule->m_vIndexList) ^ src\game\editor\editor.cpp:118:12: style: Variable 'Item' can be declared as reference to const [constVariable] for(auto &Item : vList) ^ src\game\editor\editor.cpp:2983:11: style: Variable 'aAspects' can be declared as const array [constVariable] float aAspects[] = {4.0f / 3.0f, 16.0f / 10.0f, 5.0f / 4.0f, 16.0f / 9.0f}; ^ src\game\editor\editor.cpp:3141:15: style: Variable 's_aShift' can be declared as const array [constVariable] static int s_aShift[] = {24, 16, 8, 0}; ^ src\engine\server\server.cpp:2807:14: style: Variable 'Client' can be declared as reference to const [constVariable] for(auto &Client : m_aClients) ^ src\engine\server\sql_string_helpers.cpp:51:6: style: Variable 'aTimes' can be declared as const array [constVariable] int aTimes[7] = ^ src\test\secure_random.cpp:24:6: style: Variable 'BOUNDS' can be declared as const array [constVariable] int BOUNDS[] = {2, 3, 4, 5, 10, 100, 127, 128, 129}; ^ ```
2022-11-13 15:29:13 +00:00
const CClientData &cData = m_aClients[i];
2019-06-09 17:26:20 +00:00
if(!cData.m_Active)
continue;
2014-01-11 23:45:25 +00:00
CNetObj_Character Prev = m_Snap.m_aCharacters[i].m_Prev;
CNetObj_Character Player = m_Snap.m_aCharacters[i].m_Cur;
2013-08-23 23:50:35 +00:00
vec2 Position = mix(vec2(Prev.m_X, Prev.m_Y), vec2(Player.m_X, Player.m_Y), Client()->IntraGameTick(g_Config.m_ClDummy));
2013-08-23 23:50:35 +00:00
2019-06-09 17:26:20 +00:00
bool IsOneSuper = cData.m_Super || OwnClientData.m_Super;
bool IsOneSolo = cData.m_Solo || OwnClientData.m_Solo;
2019-04-21 17:35:07 +00:00
if(!IsOneSuper && (!m_Teams.SameTeam(i, ownID) || IsOneSolo || OwnClientData.m_HookHitDisabled))
2014-01-11 23:45:25 +00:00
continue;
2013-08-23 23:50:35 +00:00
vec2 ClosestPoint;
if(closest_point_on_line(HookPos, NewPos, Position, ClosestPoint))
2013-08-23 23:50:35 +00:00
{
if(distance(Position, ClosestPoint) < CCharacterCore::PhysicalSize() + 2.0f)
2013-08-23 23:50:35 +00:00
{
if(ClosestID == -1 || distance(HookPos, Position) < Distance)
{
NewPos2 = ClosestPoint;
ClosestID = i;
Distance = distance(HookPos, Position);
}
2013-08-23 23:50:35 +00:00
}
}
}
2014-10-23 15:31:29 +00:00
2013-08-23 23:50:35 +00:00
return ClosestID;
}
2019-04-25 16:49:27 +00:00
ColorRGBA CalculateNameColor(ColorHSLA TextColorHSL)
{
2019-04-25 16:49:27 +00:00
return color_cast<ColorRGBA>(ColorHSLA(TextColorHSL.h, TextColorHSL.s * 0.68f, TextColorHSL.l * 0.81f));
}
2019-04-21 17:35:07 +00:00
void CGameClient::UpdatePrediction()
{
m_GameWorld.m_WorldConfig.m_IsVanilla = m_GameInfo.m_PredictVanilla;
m_GameWorld.m_WorldConfig.m_IsDDRace = m_GameInfo.m_PredictDDRace;
m_GameWorld.m_WorldConfig.m_IsFNG = m_GameInfo.m_PredictFNG;
2022-04-25 13:57:10 +00:00
m_GameWorld.m_WorldConfig.m_PredictDDRace = m_GameInfo.m_PredictDDRace;
m_GameWorld.m_WorldConfig.m_PredictTiles = m_GameInfo.m_PredictDDRace && m_GameInfo.m_PredictDDRaceTiles;
m_GameWorld.m_WorldConfig.m_UseTuneZones = m_GameInfo.m_PredictDDRaceTiles;
m_GameWorld.m_WorldConfig.m_PredictFreeze = g_Config.m_ClPredictFreeze;
m_GameWorld.m_WorldConfig.m_PredictWeapons = AntiPingWeapons();
m_GameWorld.m_WorldConfig.m_BugDDRaceInput = m_GameInfo.m_BugDDRaceInput;
m_GameWorld.m_WorldConfig.m_NoWeakHookAndBounce = m_GameInfo.m_NoWeakHookAndBounce;
2021-09-15 20:14:16 +00:00
// always update default tune zone, even without character
if(!m_GameWorld.m_WorldConfig.m_UseTuneZones)
m_GameWorld.TuningList()[0] = m_aTuning[g_Config.m_ClDummy];
2021-09-15 20:14:16 +00:00
if(!m_Snap.m_pLocalCharacter)
{
if(CCharacter *pLocalChar = m_GameWorld.GetCharacterByID(m_Snap.m_LocalClientID))
pLocalChar->Destroy();
return;
}
if(m_Snap.m_pLocalCharacter->m_AmmoCount > 0 && m_Snap.m_pLocalCharacter->m_Weapon != WEAPON_NINJA)
m_GameWorld.m_WorldConfig.m_InfiniteAmmo = false;
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;
// update the tuning/tunezone at the local character position with the latest tunings received before the new snapshot
2021-05-12 16:57:50 +00:00
vec2 LocalCharPos = vec2(m_Snap.m_pLocalCharacter->m_X, m_Snap.m_pLocalCharacter->m_Y);
m_GameWorld.m_Core.m_aTuning[g_Config.m_ClDummy] = m_aTuning[g_Config.m_ClDummy];
2021-05-12 16:57:50 +00:00
if(m_GameWorld.m_WorldConfig.m_UseTuneZones)
2021-05-12 16:57:50 +00:00
{
int TuneZone = Collision()->IsTune(Collision()->GetMapIndex(LocalCharPos));
2021-05-12 16:57:50 +00:00
if(TuneZone != m_aLocalTuneZone[g_Config.m_ClDummy])
2021-05-12 16:57:50 +00:00
{
// our tunezone changed, expecting tuning message
m_aLocalTuneZone[g_Config.m_ClDummy] = m_aExpectingTuningForZone[g_Config.m_ClDummy] = TuneZone;
m_aExpectingTuningSince[g_Config.m_ClDummy] = 0;
2021-05-12 16:57:50 +00:00
}
if(m_aExpectingTuningForZone[g_Config.m_ClDummy] >= 0)
2021-05-12 16:57:50 +00:00
{
if(m_aReceivedTuning[g_Config.m_ClDummy])
2021-05-12 16:57:50 +00:00
{
m_GameWorld.TuningList()[m_aExpectingTuningForZone[g_Config.m_ClDummy]] = m_aTuning[g_Config.m_ClDummy];
m_aReceivedTuning[g_Config.m_ClDummy] = false;
m_aExpectingTuningForZone[g_Config.m_ClDummy] = -1;
2021-05-12 16:57:50 +00:00
}
else if(m_aExpectingTuningSince[g_Config.m_ClDummy] >= 5)
2021-05-12 16:57:50 +00:00
{
// if we are expecting tuning for more than 10 snaps (less than a quarter of a second)
// it is probably dropped or it was received out of order
// or applied to another tunezone.
// we need to fallback to current tuning to fix ourselves.
m_aExpectingTuningForZone[g_Config.m_ClDummy] = -1;
m_aExpectingTuningSince[g_Config.m_ClDummy] = 0;
m_aReceivedTuning[g_Config.m_ClDummy] = false;
2021-05-12 16:57:50 +00:00
dbg_msg("tunezone", "the tuning was missed");
}
else
{
// if we are expecting tuning and have not received one yet.
// do not update any tuning, so we don't apply it to the wrong tunezone.
dbg_msg("tunezone", "waiting for tuning for zone %d", m_aExpectingTuningForZone[g_Config.m_ClDummy]);
m_aExpectingTuningSince[g_Config.m_ClDummy]++;
2021-05-12 16:57:50 +00:00
}
}
else
{
// if we have processed what we need, and the tuning is still wrong due to out of order messege
// fix our tuning by using the current one
m_GameWorld.TuningList()[TuneZone] = m_aTuning[g_Config.m_ClDummy];
m_aExpectingTuningSince[g_Config.m_ClDummy] = 0;
m_aReceivedTuning[g_Config.m_ClDummy] = false;
2021-05-12 16:57:50 +00:00
}
}
// if ddnetcharacter is available, ignore server-wide tunings for hook and collision
if(m_Snap.m_aCharacters[m_Snap.m_LocalClientID].m_HasExtendedData)
{
m_GameWorld.m_Core.m_aTuning[g_Config.m_ClDummy].m_PlayerCollision = 1;
m_GameWorld.m_Core.m_aTuning[g_Config.m_ClDummy].m_PlayerHooking = 1;
}
CCharacter *pLocalChar = m_GameWorld.GetCharacterByID(m_Snap.m_LocalClientID);
CCharacter *pDummyChar = 0;
if(PredictDummy())
pDummyChar = m_GameWorld.GetCharacterByID(m_PredictedDummyID);
// update strong and weak hook
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))
{
if(m_Snap.m_aCharacters[m_Snap.m_LocalClientID].m_HasExtendedData)
{
int aIDs[MAX_CLIENTS];
2020-10-26 14:14:07 +00:00
for(int &ID : aIDs)
ID = -1;
for(int i = 0; i < MAX_CLIENTS; i++)
if(CCharacter *pChar = m_GameWorld.GetCharacterByID(i))
aIDs[pChar->GetStrongWeakID()] = i;
2020-10-26 14:14:07 +00:00
for(int ID : aIDs)
if(ID >= 0)
m_CharOrder.GiveStrong(ID);
}
else
{
// manual detection
DetectStrongHook();
}
for(int i : m_CharOrder.m_IDs)
{
if(CCharacter *pChar = m_GameWorld.GetCharacterByID(i))
{
m_GameWorld.RemoveEntity(pChar);
m_GameWorld.InsertEntity(pChar);
}
}
}
// advance the gameworld to the current gametick
if(pLocalChar && absolute(m_GameWorld.GameTick() - Client()->GameTick(g_Config.m_ClDummy)) < Client()->GameTickSpeed())
{
for(int Tick = m_GameWorld.GameTick() + 1; Tick <= Client()->GameTick(g_Config.m_ClDummy); Tick++)
{
CNetObj_PlayerInput *pInput = (CNetObj_PlayerInput *)Client()->GetInput(Tick);
CNetObj_PlayerInput *pDummyInput = 0;
if(pDummyChar)
pDummyInput = (CNetObj_PlayerInput *)Client()->GetInput(Tick, 1);
if(pInput)
pLocalChar->OnDirectInput(pInput);
if(pDummyInput)
pDummyChar->OnDirectInput(pDummyInput);
m_GameWorld.m_GameTick = Tick;
if(pInput)
pLocalChar->OnPredictedInput(pInput);
if(pDummyInput)
pDummyChar->OnPredictedInput(pDummyInput);
m_GameWorld.Tick();
for(int i = 0; i < MAX_CLIENTS; i++)
if(CCharacter *pChar = m_GameWorld.GetCharacterByID(i))
{
m_aClients[i].m_aPredPos[Tick % 200] = pChar->Core()->m_Pos;
m_aClients[i].m_aPredTick[Tick % 200] = Tick;
}
}
}
else
{
// skip to current gametick
m_GameWorld.m_GameTick = Client()->GameTick(g_Config.m_ClDummy);
if(pLocalChar)
if(CNetObj_PlayerInput *pInput = (CNetObj_PlayerInput *)Client()->GetInput(Client()->GameTick(g_Config.m_ClDummy)))
pLocalChar->SetInput(pInput);
if(pDummyChar)
if(CNetObj_PlayerInput *pInput = (CNetObj_PlayerInput *)Client()->GetInput(Client()->GameTick(g_Config.m_ClDummy), 1))
pDummyChar->SetInput(pInput);
}
for(int i = 0; i < MAX_CLIENTS; i++)
if(CCharacter *pChar = m_GameWorld.GetCharacterByID(i))
{
m_aClients[i].m_aPredPos[Client()->GameTick(g_Config.m_ClDummy) % 200] = pChar->Core()->m_Pos;
m_aClients[i].m_aPredTick[Client()->GameTick(g_Config.m_ClDummy) % 200] = Client()->GameTick(g_Config.m_ClDummy);
}
// update the local gameworld with the new snapshot
2023-05-29 11:35:39 +00:00
m_GameWorld.NetObjBegin(m_Teams, m_Snap.m_LocalClientID);
for(int i = 0; i < MAX_CLIENTS; i++)
if(m_Snap.m_aCharacters[i].m_Active)
{
bool IsLocal = (i == m_Snap.m_LocalClientID || (PredictDummy() && i == m_PredictedDummyID));
int GameTeam = (m_Snap.m_pGameInfoObj && (m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_TEAMS)) ? m_aClients[i].m_Team : i;
m_GameWorld.NetCharAdd(i, &m_Snap.m_aCharacters[i].m_Cur,
m_Snap.m_aCharacters[i].m_HasExtendedData ? &m_Snap.m_aCharacters[i].m_ExtendedData : 0,
GameTeam, IsLocal);
}
2021-02-07 19:51:28 +00:00
for(const CSnapEntities &EntData : SnapEntities())
m_GameWorld.NetObjAdd(EntData.m_Item.m_ID, EntData.m_Item.m_Type, EntData.m_pData, EntData.m_pDataEx);
2023-05-29 11:35:39 +00:00
m_GameWorld.NetObjEnd();
}
void CGameClient::UpdateRenderedCharacters()
{
for(int i = 0; i < MAX_CLIENTS; i++)
{
if(!m_Snap.m_aCharacters[i].m_Active)
continue;
m_aClients[i].m_RenderCur = m_Snap.m_aCharacters[i].m_Cur;
m_aClients[i].m_RenderPrev = m_Snap.m_aCharacters[i].m_Prev;
m_aClients[i].m_IsPredicted = false;
2019-05-12 23:45:49 +00:00
m_aClients[i].m_IsPredictedLocal = false;
vec2 UnpredPos = mix(
vec2(m_Snap.m_aCharacters[i].m_Prev.m_X, m_Snap.m_aCharacters[i].m_Prev.m_Y),
vec2(m_Snap.m_aCharacters[i].m_Cur.m_X, m_Snap.m_aCharacters[i].m_Cur.m_Y),
Client()->IntraGameTick(g_Config.m_ClDummy));
vec2 Pos = UnpredPos;
2023-05-29 11:35:39 +00:00
CCharacter *pChar = m_PredictedWorld.GetCharacterByID(i);
if(Predict() && (i == m_Snap.m_LocalClientID || (AntiPingPlayers() && !IsOtherTeam(i))) && pChar)
{
m_aClients[i].m_Predicted.Write(&m_aClients[i].m_RenderCur);
m_aClients[i].m_PrevPredicted.Write(&m_aClients[i].m_RenderPrev);
m_aClients[i].m_IsPredicted = true;
Pos = mix(
vec2(m_aClients[i].m_RenderPrev.m_X, m_aClients[i].m_RenderPrev.m_Y),
vec2(m_aClients[i].m_RenderCur.m_X, m_aClients[i].m_RenderCur.m_Y),
m_aClients[i].m_IsPredicted ? Client()->PredIntraGameTick(g_Config.m_ClDummy) : Client()->IntraGameTick(g_Config.m_ClDummy));
if(i == m_Snap.m_LocalClientID)
2019-05-12 23:45:49 +00:00
{
m_aClients[i].m_IsPredictedLocal = true;
2023-05-29 11:35:39 +00:00
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))
2019-05-12 23:45:49 +00:00
{
m_aClients[i].m_RenderCur.m_AttackTick = pChar->GetAttackTick();
if(m_Snap.m_aCharacters[i].m_Cur.m_Weapon != WEAPON_NINJA && !(pChar->m_NinjaJetpack && pChar->Core()->m_ActiveWeapon == WEAPON_GUN))
m_aClients[i].m_RenderCur.m_Weapon = m_aClients[i].m_Predicted.m_ActiveWeapon;
}
}
else
{
// use unpredicted values for other players
m_aClients[i].m_RenderPrev.m_Angle = m_Snap.m_aCharacters[i].m_Prev.m_Angle;
m_aClients[i].m_RenderCur.m_Angle = m_Snap.m_aCharacters[i].m_Cur.m_Angle;
if(g_Config.m_ClAntiPingSmooth)
Pos = GetSmoothPos(i);
}
}
m_Snap.m_aCharacters[i].m_Position = Pos;
m_aClients[i].m_RenderPos = Pos;
if(Predict() && i == m_Snap.m_LocalClientID)
m_LocalCharacterPos = Pos;
}
}
void CGameClient::DetectStrongHook()
2015-01-11 16:36:38 +00:00
{
// attempt to detect strong/weak between players
for(int FromPlayer = 0; FromPlayer < MAX_CLIENTS; FromPlayer++)
2015-01-11 16:36:38 +00:00
{
if(!m_Snap.m_aCharacters[FromPlayer].m_Active)
continue;
int ToPlayer = m_Snap.m_aCharacters[FromPlayer].m_Prev.m_HookedPlayer;
if(ToPlayer < 0 || ToPlayer >= MAX_CLIENTS || !m_Snap.m_aCharacters[ToPlayer].m_Active || ToPlayer != m_Snap.m_aCharacters[FromPlayer].m_Cur.m_HookedPlayer)
continue;
if(absolute(minimum(m_aLastUpdateTick[ToPlayer], m_aLastUpdateTick[FromPlayer]) - Client()->GameTick(g_Config.m_ClDummy)) < Client()->GameTickSpeed() / 4)
continue;
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)
continue;
CCharacter *pFromCharWorld = m_GameWorld.GetCharacterByID(FromPlayer);
CCharacter *pToCharWorld = m_GameWorld.GetCharacterByID(ToPlayer);
if(!pFromCharWorld || !pToCharWorld)
continue;
m_aLastUpdateTick[ToPlayer] = m_aLastUpdateTick[FromPlayer] = Client()->GameTick(g_Config.m_ClDummy);
float aPredictErr[2];
CCharacterCore ToCharCur;
ToCharCur.Read(&m_Snap.m_aCharacters[ToPlayer].m_Cur);
CWorldCore World;
World.m_aTuning[g_Config.m_ClDummy] = m_aTuning[g_Config.m_ClDummy];
2015-01-11 16:36:38 +00:00
for(int dir = 0; dir < 2; dir++)
{
CCharacterCore ToChar = pFromCharWorld->GetCore();
ToChar.Init(&World, Collision(), &m_Teams);
World.m_apCharacters[ToPlayer] = &ToChar;
ToChar.Read(&m_Snap.m_aCharacters[ToPlayer].m_Prev);
2015-01-11 16:36:38 +00:00
CCharacterCore FromChar = pFromCharWorld->GetCore();
FromChar.Init(&World, Collision(), &m_Teams);
World.m_apCharacters[FromPlayer] = &FromChar;
FromChar.Read(&m_Snap.m_aCharacters[FromPlayer].m_Prev);
2015-01-11 16:36:38 +00:00
for(int Tick = Client()->PrevGameTick(g_Config.m_ClDummy); Tick < Client()->GameTick(g_Config.m_ClDummy); Tick++)
2015-01-11 16:36:38 +00:00
{
if(dir == 0)
{
FromChar.Tick(false);
ToChar.Tick(false);
2015-01-11 16:36:38 +00:00
}
else
{
ToChar.Tick(false);
FromChar.Tick(false);
2015-01-11 16:36:38 +00:00
}
FromChar.Move();
FromChar.Quantize();
ToChar.Move();
ToChar.Quantize();
2015-01-11 16:36:38 +00:00
}
aPredictErr[dir] = distance(ToChar.m_Vel, ToCharCur.m_Vel);
2015-01-11 16:36:38 +00:00
}
const float LOW = 0.0001f;
const float HIGH = 0.07f;
if(aPredictErr[1] < LOW && aPredictErr[0] > HIGH)
{
if(m_CharOrder.HasStrongAgainst(ToPlayer, FromPlayer))
{
if(ToPlayer != m_Snap.m_LocalClientID)
m_CharOrder.GiveWeak(ToPlayer);
else
m_CharOrder.GiveStrong(FromPlayer);
}
}
else if(aPredictErr[0] < LOW && aPredictErr[1] > HIGH)
{
if(m_CharOrder.HasStrongAgainst(FromPlayer, ToPlayer))
{
if(ToPlayer != m_Snap.m_LocalClientID)
m_CharOrder.GiveStrong(ToPlayer);
else
m_CharOrder.GiveWeak(FromPlayer);
}
}
2015-01-11 16:36:38 +00:00
}
}
2017-03-06 13:04:09 +00:00
vec2 CGameClient::GetSmoothPos(int ClientID)
2017-03-06 13:04:09 +00:00
{
vec2 Pos = mix(m_aClients[ClientID].m_PrevPredicted.m_Pos, m_aClients[ClientID].m_Predicted.m_Pos, Client()->PredIntraGameTick(g_Config.m_ClDummy));
2021-06-23 05:05:49 +00:00
int64_t Now = time_get();
for(int i = 0; i < 2; i++)
{
int64_t Len = clamp(m_aClients[ClientID].m_aSmoothLen[i], (int64_t)1, time_freq());
int64_t TimePassed = Now - m_aClients[ClientID].m_aSmoothStart[i];
2021-06-23 05:05:49 +00:00
if(in_range(TimePassed, (int64_t)0, Len - 1))
{
float MixAmount = 1.f - std::pow(1.f - TimePassed / (float)Len, 1.2f);
int SmoothTick;
float SmoothIntra;
Client()->GetSmoothTick(&SmoothTick, &SmoothIntra, MixAmount);
if(SmoothTick > 0 && m_aClients[ClientID].m_aPredTick[(SmoothTick - 1) % 200] >= Client()->PrevGameTick(g_Config.m_ClDummy) && m_aClients[ClientID].m_aPredTick[SmoothTick % 200] <= Client()->PredGameTick(g_Config.m_ClDummy))
Pos[i] = mix(m_aClients[ClientID].m_aPredPos[(SmoothTick - 1) % 200][i], m_aClients[ClientID].m_aPredPos[SmoothTick % 200][i], SmoothIntra);
}
}
return Pos;
2017-03-06 13:04:09 +00:00
}
void CGameClient::Echo(const char *pString)
{
2021-07-12 09:43:56 +00:00
m_Chat.Echo(pString);
}
2019-07-16 20:06:57 +00:00
bool CGameClient::IsOtherTeam(int ClientID) const
2019-07-16 20:06:57 +00:00
{
bool Local = m_Snap.m_LocalClientID == ClientID;
if(m_Snap.m_LocalClientID < 0)
return false;
2022-03-14 12:25:47 +00:00
else if((m_Snap.m_SpecInfo.m_Active && m_Snap.m_SpecInfo.m_SpectatorID == SPEC_FREEVIEW) || ClientID < 0)
2019-07-16 20:06:57 +00:00
return false;
else if(m_Snap.m_SpecInfo.m_Active && m_Snap.m_SpecInfo.m_SpectatorID != SPEC_FREEVIEW)
{
if(m_Teams.Team(ClientID) == TEAM_SUPER || m_Teams.Team(m_Snap.m_SpecInfo.m_SpectatorID) == TEAM_SUPER)
return false;
2019-07-16 20:06:57 +00:00
return m_Teams.Team(ClientID) != m_Teams.Team(m_Snap.m_SpecInfo.m_SpectatorID);
}
2019-07-16 20:06:57 +00:00
else if((m_aClients[m_Snap.m_LocalClientID].m_Solo || m_aClients[ClientID].m_Solo) && !Local)
return true;
if(m_Teams.Team(ClientID) == TEAM_SUPER || m_Teams.Team(m_Snap.m_LocalClientID) == TEAM_SUPER)
return false;
2019-07-16 20:06:57 +00:00
return m_Teams.Team(ClientID) != m_Teams.Team(m_Snap.m_LocalClientID);
}
2019-09-08 22:53:07 +00:00
int CGameClient::SwitchStateTeam() const
{
if(m_aSwitchStateTeam[g_Config.m_ClDummy] >= 0)
return m_aSwitchStateTeam[g_Config.m_ClDummy];
else if(m_Snap.m_LocalClientID < 0)
return 0;
else if(m_Snap.m_SpecInfo.m_Active && m_Snap.m_SpecInfo.m_SpectatorID != SPEC_FREEVIEW)
return m_Teams.Team(m_Snap.m_SpecInfo.m_SpectatorID);
return m_Teams.Team(m_Snap.m_LocalClientID);
}
bool CGameClient::IsLocalCharSuper() const
{
if(m_Snap.m_LocalClientID < 0)
2022-02-14 23:12:52 +00:00
return false;
return m_aClients[m_Snap.m_LocalClientID].m_Super;
}
2020-09-26 07:37:35 +00:00
void CGameClient::LoadGameSkin(const char *pPath, bool AsDir)
{
if(m_GameSkinLoaded)
2020-09-26 07:37:35 +00:00
{
Graphics()->UnloadTexture(&m_GameSkin.m_SpriteHealthFull);
Graphics()->UnloadTexture(&m_GameSkin.m_SpriteHealthEmpty);
Graphics()->UnloadTexture(&m_GameSkin.m_SpriteArmorFull);
Graphics()->UnloadTexture(&m_GameSkin.m_SpriteArmorEmpty);
Graphics()->UnloadTexture(&m_GameSkin.m_SpriteWeaponHammerCursor);
Graphics()->UnloadTexture(&m_GameSkin.m_SpriteWeaponGunCursor);
Graphics()->UnloadTexture(&m_GameSkin.m_SpriteWeaponShotgunCursor);
Graphics()->UnloadTexture(&m_GameSkin.m_SpriteWeaponGrenadeCursor);
Graphics()->UnloadTexture(&m_GameSkin.m_SpriteWeaponNinjaCursor);
Graphics()->UnloadTexture(&m_GameSkin.m_SpriteWeaponLaserCursor);
for(auto &SpriteWeaponCursor : m_GameSkin.m_aSpriteWeaponCursors)
{
2020-10-26 14:14:07 +00:00
SpriteWeaponCursor = IGraphics::CTextureHandle();
}
Graphics()->UnloadTexture(&m_GameSkin.m_SpriteHookChain);
Graphics()->UnloadTexture(&m_GameSkin.m_SpriteHookHead);
Graphics()->UnloadTexture(&m_GameSkin.m_SpriteWeaponHammer);
Graphics()->UnloadTexture(&m_GameSkin.m_SpriteWeaponGun);
Graphics()->UnloadTexture(&m_GameSkin.m_SpriteWeaponShotgun);
Graphics()->UnloadTexture(&m_GameSkin.m_SpriteWeaponGrenade);
Graphics()->UnloadTexture(&m_GameSkin.m_SpriteWeaponNinja);
Graphics()->UnloadTexture(&m_GameSkin.m_SpriteWeaponLaser);
for(auto &SpriteWeapon : m_GameSkin.m_aSpriteWeapons)
{
2020-10-26 14:14:07 +00:00
SpriteWeapon = IGraphics::CTextureHandle();
}
for(auto &SpriteParticle : m_GameSkin.m_aSpriteParticles)
{
Graphics()->UnloadTexture(&SpriteParticle);
}
for(auto &SpriteStar : m_GameSkin.m_aSpriteStars)
{
Graphics()->UnloadTexture(&SpriteStar);
}
Graphics()->UnloadTexture(&m_GameSkin.m_SpriteWeaponGunProjectile);
Graphics()->UnloadTexture(&m_GameSkin.m_SpriteWeaponShotgunProjectile);
Graphics()->UnloadTexture(&m_GameSkin.m_SpriteWeaponGrenadeProjectile);
Graphics()->UnloadTexture(&m_GameSkin.m_SpriteWeaponHammerProjectile);
Graphics()->UnloadTexture(&m_GameSkin.m_SpriteWeaponNinjaProjectile);
Graphics()->UnloadTexture(&m_GameSkin.m_SpriteWeaponLaserProjectile);
for(auto &SpriteWeaponProjectile : m_GameSkin.m_aSpriteWeaponProjectiles)
{
2020-10-26 14:14:07 +00:00
SpriteWeaponProjectile = IGraphics::CTextureHandle();
}
for(int i = 0; i < 3; ++i)
{
Graphics()->UnloadTexture(&m_GameSkin.m_aSpriteWeaponGunMuzzles[i]);
Graphics()->UnloadTexture(&m_GameSkin.m_aSpriteWeaponShotgunMuzzles[i]);
Graphics()->UnloadTexture(&m_GameSkin.m_aaSpriteWeaponNinjaMuzzles[i]);
for(auto &SpriteWeaponsMuzzle : m_GameSkin.m_aaSpriteWeaponsMuzzles)
{
2020-10-26 14:14:07 +00:00
SpriteWeaponsMuzzle[i] = IGraphics::CTextureHandle();
}
}
Graphics()->UnloadTexture(&m_GameSkin.m_SpritePickupHealth);
Graphics()->UnloadTexture(&m_GameSkin.m_SpritePickupArmor);
Graphics()->UnloadTexture(&m_GameSkin.m_SpritePickupArmorShotgun);
Graphics()->UnloadTexture(&m_GameSkin.m_SpritePickupArmorGrenade);
Graphics()->UnloadTexture(&m_GameSkin.m_SpritePickupArmorLaser);
Graphics()->UnloadTexture(&m_GameSkin.m_SpritePickupArmorNinja);
Graphics()->UnloadTexture(&m_GameSkin.m_SpritePickupGrenade);
Graphics()->UnloadTexture(&m_GameSkin.m_SpritePickupShotgun);
Graphics()->UnloadTexture(&m_GameSkin.m_SpritePickupLaser);
Graphics()->UnloadTexture(&m_GameSkin.m_SpritePickupNinja);
Graphics()->UnloadTexture(&m_GameSkin.m_SpritePickupGun);
Graphics()->UnloadTexture(&m_GameSkin.m_SpritePickupHammer);
for(auto &SpritePickupWeapon : m_GameSkin.m_aSpritePickupWeapons)
{
2020-10-26 14:14:07 +00:00
SpritePickupWeapon = IGraphics::CTextureHandle();
}
for(auto &SpritePickupWeaponArmor : m_GameSkin.m_aSpritePickupWeaponArmor)
{
SpritePickupWeaponArmor = IGraphics::CTextureHandle();
}
Graphics()->UnloadTexture(&m_GameSkin.m_SpriteFlagBlue);
Graphics()->UnloadTexture(&m_GameSkin.m_SpriteFlagRed);
if(m_GameSkin.IsSixup())
{
Graphics()->UnloadTexture(&m_GameSkin.m_SpriteNinjaBarFullLeft);
Graphics()->UnloadTexture(&m_GameSkin.m_SpriteNinjaBarFull);
Graphics()->UnloadTexture(&m_GameSkin.m_SpriteNinjaBarEmpty);
Graphics()->UnloadTexture(&m_GameSkin.m_SpriteNinjaBarEmptyRight);
}
m_GameSkinLoaded = false;
2020-09-26 07:37:35 +00:00
}
char aPath[IO_MAX_PATH_LENGTH];
2020-09-26 07:37:35 +00:00
bool IsDefault = false;
if(str_comp(pPath, "default") == 0)
{
str_copy(aPath, g_pData->m_aImages[IMAGE_GAME].m_pFilename);
2020-09-26 07:37:35 +00:00
IsDefault = true;
}
else
{
if(AsDir)
str_format(aPath, sizeof(aPath), "assets/game/%s/%s", pPath, g_pData->m_aImages[IMAGE_GAME].m_pFilename);
else
str_format(aPath, sizeof(aPath), "assets/game/%s.png", pPath);
}
CImageInfo ImgInfo;
bool PngLoaded = Graphics()->LoadPNG(&ImgInfo, aPath, IStorage::TYPE_ALL);
if(!PngLoaded && !IsDefault)
{
if(AsDir)
LoadGameSkin("default");
else
LoadGameSkin(pPath, true);
}
else if(PngLoaded && Graphics()->CheckImageDivisibility(aPath, ImgInfo, g_pData->m_aSprites[SPRITE_HEALTH_FULL].m_pSet->m_Gridx, g_pData->m_aSprites[SPRITE_HEALTH_FULL].m_pSet->m_Gridy, true) && Graphics()->IsImageFormatRGBA(aPath, ImgInfo))
2020-09-26 07:37:35 +00:00
{
m_GameSkin.m_SpriteHealthFull = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HEALTH_FULL]);
m_GameSkin.m_SpriteHealthEmpty = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HEALTH_EMPTY]);
m_GameSkin.m_SpriteArmorFull = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_ARMOR_FULL]);
m_GameSkin.m_SpriteArmorEmpty = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_ARMOR_EMPTY]);
m_GameSkin.m_SpriteWeaponHammerCursor = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_WEAPON_HAMMER_CURSOR]);
m_GameSkin.m_SpriteWeaponGunCursor = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_WEAPON_GUN_CURSOR]);
m_GameSkin.m_SpriteWeaponShotgunCursor = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_WEAPON_SHOTGUN_CURSOR]);
m_GameSkin.m_SpriteWeaponGrenadeCursor = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_WEAPON_GRENADE_CURSOR]);
m_GameSkin.m_SpriteWeaponNinjaCursor = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_WEAPON_NINJA_CURSOR]);
m_GameSkin.m_SpriteWeaponLaserCursor = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_WEAPON_LASER_CURSOR]);
m_GameSkin.m_aSpriteWeaponCursors[0] = m_GameSkin.m_SpriteWeaponHammerCursor;
m_GameSkin.m_aSpriteWeaponCursors[1] = m_GameSkin.m_SpriteWeaponGunCursor;
m_GameSkin.m_aSpriteWeaponCursors[2] = m_GameSkin.m_SpriteWeaponShotgunCursor;
m_GameSkin.m_aSpriteWeaponCursors[3] = m_GameSkin.m_SpriteWeaponGrenadeCursor;
m_GameSkin.m_aSpriteWeaponCursors[4] = m_GameSkin.m_SpriteWeaponLaserCursor;
m_GameSkin.m_aSpriteWeaponCursors[5] = m_GameSkin.m_SpriteWeaponNinjaCursor;
// weapons and hook
m_GameSkin.m_SpriteHookChain = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HOOK_CHAIN]);
m_GameSkin.m_SpriteHookHead = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HOOK_HEAD]);
m_GameSkin.m_SpriteWeaponHammer = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_WEAPON_HAMMER_BODY]);
m_GameSkin.m_SpriteWeaponGun = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_WEAPON_GUN_BODY]);
m_GameSkin.m_SpriteWeaponShotgun = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_WEAPON_SHOTGUN_BODY]);
m_GameSkin.m_SpriteWeaponGrenade = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_WEAPON_GRENADE_BODY]);
m_GameSkin.m_SpriteWeaponNinja = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_WEAPON_NINJA_BODY]);
m_GameSkin.m_SpriteWeaponLaser = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_WEAPON_LASER_BODY]);
m_GameSkin.m_aSpriteWeapons[0] = m_GameSkin.m_SpriteWeaponHammer;
m_GameSkin.m_aSpriteWeapons[1] = m_GameSkin.m_SpriteWeaponGun;
m_GameSkin.m_aSpriteWeapons[2] = m_GameSkin.m_SpriteWeaponShotgun;
m_GameSkin.m_aSpriteWeapons[3] = m_GameSkin.m_SpriteWeaponGrenade;
m_GameSkin.m_aSpriteWeapons[4] = m_GameSkin.m_SpriteWeaponLaser;
m_GameSkin.m_aSpriteWeapons[5] = m_GameSkin.m_SpriteWeaponNinja;
// particles
for(int i = 0; i < 9; ++i)
{
m_GameSkin.m_aSpriteParticles[i] = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_PART1 + i]);
}
// stars
for(int i = 0; i < 3; ++i)
{
m_GameSkin.m_aSpriteStars[i] = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_STAR1 + i]);
}
// projectiles
m_GameSkin.m_SpriteWeaponGunProjectile = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_WEAPON_GUN_PROJ]);
m_GameSkin.m_SpriteWeaponShotgunProjectile = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_WEAPON_SHOTGUN_PROJ]);
m_GameSkin.m_SpriteWeaponGrenadeProjectile = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_WEAPON_GRENADE_PROJ]);
// these weapons have no projectiles
m_GameSkin.m_SpriteWeaponHammerProjectile = IGraphics::CTextureHandle();
m_GameSkin.m_SpriteWeaponNinjaProjectile = IGraphics::CTextureHandle();
m_GameSkin.m_SpriteWeaponLaserProjectile = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_WEAPON_LASER_PROJ]);
m_GameSkin.m_aSpriteWeaponProjectiles[0] = m_GameSkin.m_SpriteWeaponHammerProjectile;
m_GameSkin.m_aSpriteWeaponProjectiles[1] = m_GameSkin.m_SpriteWeaponGunProjectile;
m_GameSkin.m_aSpriteWeaponProjectiles[2] = m_GameSkin.m_SpriteWeaponShotgunProjectile;
m_GameSkin.m_aSpriteWeaponProjectiles[3] = m_GameSkin.m_SpriteWeaponGrenadeProjectile;
m_GameSkin.m_aSpriteWeaponProjectiles[4] = m_GameSkin.m_SpriteWeaponLaserProjectile;
m_GameSkin.m_aSpriteWeaponProjectiles[5] = m_GameSkin.m_SpriteWeaponNinjaProjectile;
// muzzles
for(int i = 0; i < 3; ++i)
{
m_GameSkin.m_aSpriteWeaponGunMuzzles[i] = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_WEAPON_GUN_MUZZLE1 + i]);
m_GameSkin.m_aSpriteWeaponShotgunMuzzles[i] = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_WEAPON_SHOTGUN_MUZZLE1 + i]);
m_GameSkin.m_aaSpriteWeaponNinjaMuzzles[i] = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_WEAPON_NINJA_MUZZLE1 + i]);
m_GameSkin.m_aaSpriteWeaponsMuzzles[1][i] = m_GameSkin.m_aSpriteWeaponGunMuzzles[i];
m_GameSkin.m_aaSpriteWeaponsMuzzles[2][i] = m_GameSkin.m_aSpriteWeaponShotgunMuzzles[i];
m_GameSkin.m_aaSpriteWeaponsMuzzles[5][i] = m_GameSkin.m_aaSpriteWeaponNinjaMuzzles[i];
}
// pickups
m_GameSkin.m_SpritePickupHealth = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_PICKUP_HEALTH]);
m_GameSkin.m_SpritePickupArmor = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_PICKUP_ARMOR]);
m_GameSkin.m_SpritePickupHammer = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_PICKUP_HAMMER]);
m_GameSkin.m_SpritePickupGun = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_PICKUP_GUN]);
m_GameSkin.m_SpritePickupShotgun = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_PICKUP_SHOTGUN]);
m_GameSkin.m_SpritePickupGrenade = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_PICKUP_GRENADE]);
m_GameSkin.m_SpritePickupLaser = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_PICKUP_LASER]);
m_GameSkin.m_SpritePickupNinja = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_PICKUP_NINJA]);
m_GameSkin.m_SpritePickupArmorShotgun = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_PICKUP_ARMOR_SHOTGUN]);
m_GameSkin.m_SpritePickupArmorGrenade = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_PICKUP_ARMOR_GRENADE]);
m_GameSkin.m_SpritePickupArmorNinja = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_PICKUP_ARMOR_NINJA]);
m_GameSkin.m_SpritePickupArmorLaser = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_PICKUP_ARMOR_LASER]);
m_GameSkin.m_aSpritePickupWeapons[0] = m_GameSkin.m_SpritePickupHammer;
m_GameSkin.m_aSpritePickupWeapons[1] = m_GameSkin.m_SpritePickupGun;
m_GameSkin.m_aSpritePickupWeapons[2] = m_GameSkin.m_SpritePickupShotgun;
m_GameSkin.m_aSpritePickupWeapons[3] = m_GameSkin.m_SpritePickupGrenade;
m_GameSkin.m_aSpritePickupWeapons[4] = m_GameSkin.m_SpritePickupLaser;
m_GameSkin.m_aSpritePickupWeapons[5] = m_GameSkin.m_SpritePickupNinja;
m_GameSkin.m_aSpritePickupWeaponArmor[0] = m_GameSkin.m_SpritePickupArmorShotgun;
m_GameSkin.m_aSpritePickupWeaponArmor[1] = m_GameSkin.m_SpritePickupArmorGrenade;
m_GameSkin.m_aSpritePickupWeaponArmor[2] = m_GameSkin.m_SpritePickupArmorNinja;
m_GameSkin.m_aSpritePickupWeaponArmor[3] = m_GameSkin.m_SpritePickupArmorLaser;
// flags
m_GameSkin.m_SpriteFlagBlue = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_FLAG_BLUE]);
m_GameSkin.m_SpriteFlagRed = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_FLAG_RED]);
// ninja bar (0.7)
if(!Graphics()->IsSpriteTextureFullyTransparent(ImgInfo, &client_data7::g_pData->m_aSprites[client_data7::SPRITE_NINJA_BAR_FULL_LEFT]) ||
!Graphics()->IsSpriteTextureFullyTransparent(ImgInfo, &client_data7::g_pData->m_aSprites[client_data7::SPRITE_NINJA_BAR_FULL]) ||
!Graphics()->IsSpriteTextureFullyTransparent(ImgInfo, &client_data7::g_pData->m_aSprites[client_data7::SPRITE_NINJA_BAR_EMPTY]) ||
!Graphics()->IsSpriteTextureFullyTransparent(ImgInfo, &client_data7::g_pData->m_aSprites[client_data7::SPRITE_NINJA_BAR_EMPTY_RIGHT]))
{
m_GameSkin.m_SpriteNinjaBarFullLeft = Graphics()->LoadSpriteTexture(ImgInfo, &client_data7::g_pData->m_aSprites[client_data7::SPRITE_NINJA_BAR_FULL_LEFT]);
m_GameSkin.m_SpriteNinjaBarFull = Graphics()->LoadSpriteTexture(ImgInfo, &client_data7::g_pData->m_aSprites[client_data7::SPRITE_NINJA_BAR_FULL]);
m_GameSkin.m_SpriteNinjaBarEmpty = Graphics()->LoadSpriteTexture(ImgInfo, &client_data7::g_pData->m_aSprites[client_data7::SPRITE_NINJA_BAR_EMPTY]);
m_GameSkin.m_SpriteNinjaBarEmptyRight = Graphics()->LoadSpriteTexture(ImgInfo, &client_data7::g_pData->m_aSprites[client_data7::SPRITE_NINJA_BAR_EMPTY_RIGHT]);
}
m_GameSkinLoaded = true;
2020-11-25 12:05:53 +00:00
Graphics()->FreePNG(&ImgInfo);
2020-09-26 07:37:35 +00:00
}
}
void CGameClient::LoadEmoticonsSkin(const char *pPath, bool AsDir)
{
if(m_EmoticonsSkinLoaded)
2020-09-26 07:37:35 +00:00
{
for(auto &SpriteEmoticon : m_EmoticonsSkin.m_aSpriteEmoticons)
Graphics()->UnloadTexture(&SpriteEmoticon);
m_EmoticonsSkinLoaded = false;
2020-09-26 07:37:35 +00:00
}
char aPath[IO_MAX_PATH_LENGTH];
2020-09-26 07:37:35 +00:00
bool IsDefault = false;
if(str_comp(pPath, "default") == 0)
{
str_copy(aPath, g_pData->m_aImages[IMAGE_EMOTICONS].m_pFilename);
2020-09-26 07:37:35 +00:00
IsDefault = true;
}
else
{
if(AsDir)
str_format(aPath, sizeof(aPath), "assets/emoticons/%s/%s", pPath, g_pData->m_aImages[IMAGE_EMOTICONS].m_pFilename);
else
str_format(aPath, sizeof(aPath), "assets/emoticons/%s.png", pPath);
}
CImageInfo ImgInfo;
bool PngLoaded = Graphics()->LoadPNG(&ImgInfo, aPath, IStorage::TYPE_ALL);
if(!PngLoaded && !IsDefault)
{
if(AsDir)
LoadEmoticonsSkin("default");
else
LoadEmoticonsSkin(pPath, true);
}
else if(PngLoaded && Graphics()->CheckImageDivisibility(aPath, ImgInfo, g_pData->m_aSprites[SPRITE_OOP].m_pSet->m_Gridx, g_pData->m_aSprites[SPRITE_OOP].m_pSet->m_Gridy, true) && Graphics()->IsImageFormatRGBA(aPath, ImgInfo))
2020-09-26 07:37:35 +00:00
{
for(int i = 0; i < 16; ++i)
m_EmoticonsSkin.m_aSpriteEmoticons[i] = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_OOP + i]);
m_EmoticonsSkinLoaded = true;
2020-11-25 12:05:53 +00:00
Graphics()->FreePNG(&ImgInfo);
2020-09-26 07:37:35 +00:00
}
}
void CGameClient::LoadParticlesSkin(const char *pPath, bool AsDir)
{
if(m_ParticlesSkinLoaded)
2020-09-26 07:37:35 +00:00
{
Graphics()->UnloadTexture(&m_ParticlesSkin.m_SpriteParticleSlice);
Graphics()->UnloadTexture(&m_ParticlesSkin.m_SpriteParticleBall);
for(auto &SpriteParticleSplat : m_ParticlesSkin.m_aSpriteParticleSplat)
Graphics()->UnloadTexture(&SpriteParticleSplat);
Graphics()->UnloadTexture(&m_ParticlesSkin.m_SpriteParticleSmoke);
Graphics()->UnloadTexture(&m_ParticlesSkin.m_SpriteParticleShell);
Graphics()->UnloadTexture(&m_ParticlesSkin.m_SpriteParticleExpl);
Graphics()->UnloadTexture(&m_ParticlesSkin.m_SpriteParticleAirJump);
Graphics()->UnloadTexture(&m_ParticlesSkin.m_SpriteParticleHit);
for(auto &SpriteParticle : m_ParticlesSkin.m_aSpriteParticles)
2020-10-26 14:14:07 +00:00
SpriteParticle = IGraphics::CTextureHandle();
m_ParticlesSkinLoaded = false;
2020-09-26 07:37:35 +00:00
}
char aPath[IO_MAX_PATH_LENGTH];
2020-09-26 07:37:35 +00:00
bool IsDefault = false;
if(str_comp(pPath, "default") == 0)
{
str_copy(aPath, g_pData->m_aImages[IMAGE_PARTICLES].m_pFilename);
2020-09-26 07:37:35 +00:00
IsDefault = true;
}
else
{
if(AsDir)
str_format(aPath, sizeof(aPath), "assets/particles/%s/%s", pPath, g_pData->m_aImages[IMAGE_PARTICLES].m_pFilename);
else
str_format(aPath, sizeof(aPath), "assets/particles/%s.png", pPath);
}
CImageInfo ImgInfo;
bool PngLoaded = Graphics()->LoadPNG(&ImgInfo, aPath, IStorage::TYPE_ALL);
if(!PngLoaded && !IsDefault)
{
if(AsDir)
LoadParticlesSkin("default");
else
LoadParticlesSkin(pPath, true);
}
else if(PngLoaded && Graphics()->CheckImageDivisibility(aPath, ImgInfo, g_pData->m_aSprites[SPRITE_PART_SLICE].m_pSet->m_Gridx, g_pData->m_aSprites[SPRITE_PART_SLICE].m_pSet->m_Gridy, true) && Graphics()->IsImageFormatRGBA(aPath, ImgInfo))
2020-09-26 07:37:35 +00:00
{
m_ParticlesSkin.m_SpriteParticleSlice = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_PART_SLICE]);
m_ParticlesSkin.m_SpriteParticleBall = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_PART_BALL]);
for(int i = 0; i < 3; ++i)
m_ParticlesSkin.m_aSpriteParticleSplat[i] = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_PART_SPLAT01 + i]);
m_ParticlesSkin.m_SpriteParticleSmoke = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_PART_SMOKE]);
m_ParticlesSkin.m_SpriteParticleShell = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_PART_SHELL]);
m_ParticlesSkin.m_SpriteParticleExpl = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_PART_EXPL01]);
m_ParticlesSkin.m_SpriteParticleAirJump = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_PART_AIRJUMP]);
m_ParticlesSkin.m_SpriteParticleHit = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_PART_HIT01]);
m_ParticlesSkin.m_aSpriteParticles[0] = m_ParticlesSkin.m_SpriteParticleSlice;
m_ParticlesSkin.m_aSpriteParticles[1] = m_ParticlesSkin.m_SpriteParticleBall;
for(int i = 0; i < 3; ++i)
m_ParticlesSkin.m_aSpriteParticles[2 + i] = m_ParticlesSkin.m_aSpriteParticleSplat[i];
m_ParticlesSkin.m_aSpriteParticles[5] = m_ParticlesSkin.m_SpriteParticleSmoke;
m_ParticlesSkin.m_aSpriteParticles[6] = m_ParticlesSkin.m_SpriteParticleShell;
m_ParticlesSkin.m_aSpriteParticles[7] = m_ParticlesSkin.m_SpriteParticleExpl;
m_ParticlesSkin.m_aSpriteParticles[8] = m_ParticlesSkin.m_SpriteParticleAirJump;
m_ParticlesSkin.m_aSpriteParticles[9] = m_ParticlesSkin.m_SpriteParticleHit;
m_ParticlesSkinLoaded = true;
2020-09-30 21:42:00 +00:00
free(ImgInfo.m_pData);
2020-09-26 07:37:35 +00:00
}
}
void CGameClient::LoadHudSkin(const char *pPath, bool AsDir)
{
if(m_HudSkinLoaded)
{
Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudAirjump);
Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudAirjumpEmpty);
Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudSolo);
Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudCollisionDisabled);
Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudEndlessJump);
Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudEndlessHook);
Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudJetpack);
Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudFreezeBarFullLeft);
Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudFreezeBarFull);
Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudFreezeBarEmpty);
Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudFreezeBarEmptyRight);
2022-03-29 21:24:39 +00:00
Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudNinjaBarFullLeft);
Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudNinjaBarFull);
Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudNinjaBarEmpty);
Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudNinjaBarEmptyRight);
Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudHookHitDisabled);
Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudHammerHitDisabled);
Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudShotgunHitDisabled);
Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudGrenadeHitDisabled);
Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudLaserHitDisabled);
Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudGunHitDisabled);
2022-03-25 09:09:13 +00:00
Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudDeepFrozen);
Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudLiveFrozen);
Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudTeleportGrenade);
Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudTeleportGun);
Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudTeleportLaser);
Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudPracticeMode);
Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudDummyHammer);
Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudDummyCopy);
m_HudSkinLoaded = false;
}
char aPath[IO_MAX_PATH_LENGTH];
bool IsDefault = false;
if(str_comp(pPath, "default") == 0)
{
str_copy(aPath, g_pData->m_aImages[IMAGE_HUD].m_pFilename);
IsDefault = true;
}
else
{
if(AsDir)
str_format(aPath, sizeof(aPath), "assets/hud/%s/%s", pPath, g_pData->m_aImages[IMAGE_HUD].m_pFilename);
else
str_format(aPath, sizeof(aPath), "assets/hud/%s.png", pPath);
}
CImageInfo ImgInfo;
bool PngLoaded = Graphics()->LoadPNG(&ImgInfo, aPath, IStorage::TYPE_ALL);
if(!PngLoaded && !IsDefault)
{
if(AsDir)
LoadHudSkin("default");
else
LoadHudSkin(pPath, true);
}
else if(PngLoaded && Graphics()->CheckImageDivisibility(aPath, ImgInfo, g_pData->m_aSprites[SPRITE_HUD_AIRJUMP].m_pSet->m_Gridx, g_pData->m_aSprites[SPRITE_HUD_AIRJUMP].m_pSet->m_Gridy, true) && Graphics()->IsImageFormatRGBA(aPath, ImgInfo))
{
m_HudSkin.m_SpriteHudAirjump = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_AIRJUMP]);
m_HudSkin.m_SpriteHudAirjumpEmpty = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_AIRJUMP_EMPTY]);
m_HudSkin.m_SpriteHudSolo = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_SOLO]);
m_HudSkin.m_SpriteHudCollisionDisabled = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_COLLISION_DISABLED]);
m_HudSkin.m_SpriteHudEndlessJump = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_ENDLESS_JUMP]);
m_HudSkin.m_SpriteHudEndlessHook = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_ENDLESS_HOOK]);
m_HudSkin.m_SpriteHudJetpack = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_JETPACK]);
m_HudSkin.m_SpriteHudFreezeBarFullLeft = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_FREEZE_BAR_FULL_LEFT]);
m_HudSkin.m_SpriteHudFreezeBarFull = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_FREEZE_BAR_FULL]);
m_HudSkin.m_SpriteHudFreezeBarEmpty = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_FREEZE_BAR_EMPTY]);
m_HudSkin.m_SpriteHudFreezeBarEmptyRight = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_FREEZE_BAR_EMPTY_RIGHT]);
2022-03-29 21:24:39 +00:00
m_HudSkin.m_SpriteHudNinjaBarFullLeft = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_NINJA_BAR_FULL_LEFT]);
m_HudSkin.m_SpriteHudNinjaBarFull = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_NINJA_BAR_FULL]);
m_HudSkin.m_SpriteHudNinjaBarEmpty = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_NINJA_BAR_EMPTY]);
m_HudSkin.m_SpriteHudNinjaBarEmptyRight = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_NINJA_BAR_EMPTY_RIGHT]);
m_HudSkin.m_SpriteHudHookHitDisabled = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_HOOK_HIT_DISABLED]);
m_HudSkin.m_SpriteHudHammerHitDisabled = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_HAMMER_HIT_DISABLED]);
m_HudSkin.m_SpriteHudShotgunHitDisabled = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_SHOTGUN_HIT_DISABLED]);
m_HudSkin.m_SpriteHudGrenadeHitDisabled = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_GRENADE_HIT_DISABLED]);
m_HudSkin.m_SpriteHudLaserHitDisabled = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_LASER_HIT_DISABLED]);
m_HudSkin.m_SpriteHudGunHitDisabled = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_GUN_HIT_DISABLED]);
2022-03-25 09:09:13 +00:00
m_HudSkin.m_SpriteHudDeepFrozen = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_DEEP_FROZEN]);
m_HudSkin.m_SpriteHudLiveFrozen = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_LIVE_FROZEN]);
m_HudSkin.m_SpriteHudTeleportGrenade = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_TELEPORT_GRENADE]);
m_HudSkin.m_SpriteHudTeleportGun = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_TELEPORT_GUN]);
m_HudSkin.m_SpriteHudTeleportLaser = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_TELEPORT_LASER]);
m_HudSkin.m_SpriteHudPracticeMode = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_PRACTICE_MODE]);
m_HudSkin.m_SpriteHudDummyHammer = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_DUMMY_HAMMER]);
m_HudSkin.m_SpriteHudDummyCopy = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_DUMMY_COPY]);
m_HudSkinLoaded = true;
free(ImgInfo.m_pData);
}
}
2022-06-14 17:28:51 +00:00
void CGameClient::LoadExtrasSkin(const char *pPath, bool AsDir)
{
if(m_ExtrasSkinLoaded)
{
Graphics()->UnloadTexture(&m_ExtrasSkin.m_SpriteParticleSnowflake);
for(auto &SpriteParticle : m_ExtrasSkin.m_aSpriteParticles)
2022-06-14 17:28:51 +00:00
SpriteParticle = IGraphics::CTextureHandle();
m_ExtrasSkinLoaded = false;
}
char aPath[IO_MAX_PATH_LENGTH];
bool IsDefault = false;
if(str_comp(pPath, "default") == 0)
{
str_copy(aPath, g_pData->m_aImages[IMAGE_EXTRAS].m_pFilename);
2022-06-14 17:28:51 +00:00
IsDefault = true;
}
else
{
if(AsDir)
str_format(aPath, sizeof(aPath), "assets/extras/%s/%s", pPath, g_pData->m_aImages[IMAGE_EXTRAS].m_pFilename);
else
str_format(aPath, sizeof(aPath), "assets/extras/%s.png", pPath);
}
CImageInfo ImgInfo;
bool PngLoaded = Graphics()->LoadPNG(&ImgInfo, aPath, IStorage::TYPE_ALL);
if(!PngLoaded && !IsDefault)
{
if(AsDir)
LoadExtrasSkin("default");
else
LoadExtrasSkin(pPath, true);
}
else if(PngLoaded && Graphics()->CheckImageDivisibility(aPath, ImgInfo, g_pData->m_aSprites[SPRITE_PART_SNOWFLAKE].m_pSet->m_Gridx, g_pData->m_aSprites[SPRITE_PART_SNOWFLAKE].m_pSet->m_Gridy, true) && Graphics()->IsImageFormatRGBA(aPath, ImgInfo))
{
m_ExtrasSkin.m_SpriteParticleSnowflake = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_PART_SNOWFLAKE]);
m_ExtrasSkin.m_aSpriteParticles[0] = m_ExtrasSkin.m_SpriteParticleSnowflake;
2022-06-14 17:28:51 +00:00
m_ExtrasSkinLoaded = true;
free(ImgInfo.m_pData);
}
}
void CGameClient::RefindSkins()
{
2020-10-26 14:14:07 +00:00
for(auto &Client : m_aClients)
{
2020-11-08 05:39:16 +00:00
Client.m_SkinInfo.m_OriginalRenderSkin.Reset();
Client.m_SkinInfo.m_ColorableRenderSkin.Reset();
2020-10-26 14:14:07 +00:00
if(Client.m_aSkinName[0] != '\0')
{
const CSkin *pSkin = m_Skins.Find(Client.m_aSkinName);
2020-10-26 14:14:07 +00:00
Client.m_SkinInfo.m_OriginalRenderSkin = pSkin->m_OriginalSkin;
Client.m_SkinInfo.m_ColorableRenderSkin = pSkin->m_ColorableSkin;
}
2023-09-10 15:26:58 +00:00
else
{
Client.m_SkinInfo.m_OriginalRenderSkin.Reset();
Client.m_SkinInfo.m_ColorableRenderSkin.Reset();
}
Client.UpdateRenderInfo(IsTeamPlay());
}
m_Ghost.RefindSkins();
2021-07-12 09:43:56 +00:00
m_Chat.RefindSkins();
m_InfoMessages.RefindSkins();
}
static bool UnknownMapSettingCallback(const char *pCommand, void *pUser)
{
return true;
}
2019-09-08 22:53:07 +00:00
void CGameClient::LoadMapSettings()
{
// Reset Tunezones
CTuningParams TuningParams;
for(int i = 0; i < NUM_TUNEZONES; i++)
{
TuningList()[i] = TuningParams;
TuningList()[i].Set("gun_curvature", 0);
TuningList()[i].Set("gun_speed", 1400);
TuningList()[i].Set("shotgun_curvature", 0);
TuningList()[i].Set("shotgun_speed", 500);
TuningList()[i].Set("shotgun_speeddiff", 0);
2019-09-08 22:53:07 +00:00
}
// Load map tunings
IMap *pMap = Kernel()->RequestInterface<IMap>();
int Start, Num;
pMap->GetType(MAPITEMTYPE_INFO, &Start, &Num);
for(int i = Start; i < Start + Num; i++)
{
int ItemID;
CMapItemInfoSettings *pItem = (CMapItemInfoSettings *)pMap->GetItem(i, nullptr, &ItemID);
2019-09-08 22:53:07 +00:00
int ItemSize = pMap->GetItemSize(i);
if(!pItem || ItemID != 0)
continue;
if(ItemSize < (int)sizeof(CMapItemInfoSettings))
break;
if(!(pItem->m_Settings > -1))
break;
int Size = pMap->GetDataSize(pItem->m_Settings);
char *pSettings = (char *)pMap->GetData(pItem->m_Settings);
char *pNext = pSettings;
Console()->SetUnknownCommandCallback(UnknownMapSettingCallback, nullptr);
2019-09-08 22:53:07 +00:00
while(pNext < pSettings + Size)
{
int StrSize = str_length(pNext) + 1;
Console()->ExecuteLine(pNext, IConsole::CLIENT_ID_GAME);
pNext += StrSize;
}
Console()->SetUnknownCommandCallback(IConsole::EmptyUnknownCommandCallback, nullptr);
2019-09-08 22:53:07 +00:00
pMap->UnloadData(pItem->m_Settings);
break;
}
}
void CGameClient::ConTuneZone(IConsole::IResult *pResult, void *pUserData)
{
CGameClient *pSelf = (CGameClient *)pUserData;
int List = pResult->GetInteger(0);
const char *pParamName = pResult->GetString(1);
float NewValue = pResult->GetFloat(2);
if(List >= 0 && List < NUM_TUNEZONES)
pSelf->TuningList()[List].Set(pParamName, NewValue);
}
2020-09-18 16:45:42 +00:00
void CGameClient::ConchainMenuMap(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
{
CGameClient *pSelf = (CGameClient *)pUserData;
if(pResult->NumArguments())
{
if(str_comp(g_Config.m_ClMenuMap, pResult->GetString(0)) != 0)
{
str_copy(g_Config.m_ClMenuMap, pResult->GetString(0));
2021-07-12 09:43:56 +00:00
pSelf->m_MenuBackground.LoadMenuBackground();
2020-09-18 16:45:42 +00:00
}
}
else
pfnCallback(pResult, pCallbackUserData);
}
2021-03-17 15:09:39 +00:00
void CGameClient::DummyResetInput()
{
if(!Client()->DummyConnected())
return;
if((m_DummyInput.m_Fire & 1) != 0)
m_DummyInput.m_Fire++;
2021-07-12 09:43:56 +00:00
m_Controls.ResetInput(!g_Config.m_ClDummy);
m_Controls.m_aInputData[!g_Config.m_ClDummy].m_Hook = 0;
m_Controls.m_aInputData[!g_Config.m_ClDummy].m_Fire = m_DummyInput.m_Fire;
2021-03-17 15:09:39 +00:00
m_DummyInput = m_Controls.m_aInputData[!g_Config.m_ClDummy];
2021-03-17 15:09:39 +00:00
}
bool CGameClient::CanDisplayWarning() const
{
2021-07-12 09:43:56 +00:00
return m_Menus.CanDisplayWarning();
}
bool CGameClient::IsDisplayingWarning() const
{
2021-07-12 09:43:56 +00:00
return m_Menus.GetCurPopup() == CMenus::POPUP_WARNING;
}
CNetObjHandler *CGameClient::GetNetObjHandler()
{
return &m_NetObjHandler;
}
void CGameClient::SnapCollectEntities()
{
int NumSnapItems = Client()->SnapNumItems(IClient::SNAP_CURRENT);
2022-06-15 17:34:41 +00:00
std::vector<CSnapEntities> vItemData;
std::vector<CSnapEntities> vItemEx;
2021-11-07 10:00:48 +00:00
for(int Index = 0; Index < NumSnapItems; Index++)
{
IClient::CSnapItem Item;
const void *pData = Client()->SnapGetItem(IClient::SNAP_CURRENT, Index, &Item);
2021-11-07 10:00:48 +00:00
if(Item.m_Type == NETOBJTYPE_ENTITYEX)
2022-06-15 17:34:41 +00:00
vItemEx.push_back({Item, pData, 0});
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)
2022-06-15 17:34:41 +00:00
vItemData.push_back({Item, pData, 0});
}
2021-11-07 10:00:48 +00:00
// sort by id
class CEntComparer
{
public:
bool operator()(const CSnapEntities &lhs, const CSnapEntities &rhs) const
{
return lhs.m_Item.m_ID < rhs.m_Item.m_ID;
}
};
2022-06-15 17:34:41 +00:00
std::sort(vItemData.begin(), vItemData.end(), CEntComparer());
std::sort(vItemEx.begin(), vItemEx.end(), CEntComparer());
// merge extended items with items they belong to
m_vSnapEntities.clear();
2021-11-07 10:00:48 +00:00
size_t IndexEx = 0;
2022-06-15 17:34:41 +00:00
for(const CSnapEntities &Ent : vItemData)
2021-11-07 10:00:48 +00:00
{
const CNetObj_EntityEx *pDataEx = 0;
2022-06-15 17:34:41 +00:00
while(IndexEx < vItemEx.size() && vItemEx[IndexEx].m_Item.m_ID < Ent.m_Item.m_ID)
2021-11-07 10:00:48 +00:00
IndexEx++;
2022-06-15 17:34:41 +00:00
if(IndexEx < vItemEx.size() && vItemEx[IndexEx].m_Item.m_ID == Ent.m_Item.m_ID)
pDataEx = (const CNetObj_EntityEx *)vItemEx[IndexEx].m_pData;
m_vSnapEntities.push_back({Ent.m_Item, Ent.m_pData, pDataEx});
}
}
2023-05-24 17:29:38 +00:00
void CGameClient::HandleMultiView()
{
bool IsTeamZero = IsMultiViewIdSet();
bool Init = false;
int AmountPlayers = 0;
vec2 Minpos, Maxpos;
float TmpVel = 0.0f;
for(int i = 0; i < MAX_CLIENTS; i++)
{
// look at players who are vanished
if(m_MultiView.m_aVanish[i])
{
// not in freeze anymore and the delay is over
if(m_MultiView.m_aLastFreeze[i] + 6.0f <= Client()->LocalTime() && m_aClients[i].m_FreezeEnd == 0)
{
m_MultiView.m_aVanish[i] = false;
m_MultiView.m_aLastFreeze[i] = 0.0f;
}
}
// we look at team 0 and the player is not in the spec list
if(IsTeamZero && !m_aMultiViewId[i])
continue;
// player is vanished
if(m_MultiView.m_aVanish[i])
continue;
// the player is not in the team we are spectating
if(m_Teams.Team(i) != m_MultiViewTeam)
2023-05-24 17:29:38 +00:00
continue;
vec2 PlayerPos;
if(m_Snap.m_aCharacters[i].m_Active)
PlayerPos = vec2(m_aClients[i].m_RenderPos.x, m_aClients[i].m_RenderPos.y);
else if(m_aClients[i].m_Spec) // tee is in spec
PlayerPos = m_aClients[i].m_SpecChar;
else
continue;
// player is far away and frozen
if(distance(m_MultiView.m_OldPos, PlayerPos) > 1100 && m_aClients[i].m_FreezeEnd != 0)
{
// check if the player is frozen for more than 3 seconds, if so vanish him
if(m_MultiView.m_aLastFreeze[i] == 0.0f)
m_MultiView.m_aLastFreeze[i] = Client()->LocalTime();
else if(m_MultiView.m_aLastFreeze[i] + 3.0f <= Client()->LocalTime())
{
2023-05-24 17:29:38 +00:00
m_MultiView.m_aVanish[i] = true;
// player we want to be vanished is our "main" tee, so lets switch the tee
if(i == m_Snap.m_SpecInfo.m_SpectatorID)
m_Spectator.Spectate(FindFirstMultiViewId());
}
2023-05-24 17:29:38 +00:00
}
else if(m_MultiView.m_aLastFreeze[i] != 0)
m_MultiView.m_aLastFreeze[i] = 0;
// set the minimum and maximum position
if(!Init)
{
Minpos = PlayerPos;
Maxpos = PlayerPos;
Init = true;
}
else
{
Minpos.x = std::min(Minpos.x, PlayerPos.x);
Maxpos.x = std::max(Maxpos.x, PlayerPos.x);
Minpos.y = std::min(Minpos.y, PlayerPos.y);
Maxpos.y = std::max(Maxpos.y, PlayerPos.y);
}
// sum up the velocity of all players we are spectating
const CNetObj_Character &CurrentCharacter = m_Snap.m_aCharacters[i].m_Cur;
TmpVel += (length(vec2(CurrentCharacter.m_VelX / 256.0f, CurrentCharacter.m_VelY / 256.0f)) * 50) / 32.0f;
AmountPlayers++;
}
// if we have found no players, we disable multi view
if(AmountPlayers == 0)
{
if(m_MultiView.m_SecondChance == 0.0f)
m_MultiView.m_SecondChance = Client()->LocalTime() + 0.3f;
else if(m_MultiView.m_SecondChance < Client()->LocalTime())
{
ResetMultiView();
return;
}
2023-05-24 17:29:38 +00:00
return;
}
else if(m_MultiView.m_SecondChance != 0.0f)
m_MultiView.m_SecondChance = 0.0f;
2023-05-24 17:29:38 +00:00
2023-09-05 19:24:33 +00:00
// if we only have one tee that's in the list, we activate solo-mode
m_MultiView.m_Solo = std::count(std::begin(m_aMultiViewId), std::end(m_aMultiViewId), true) == 1;
2023-05-24 17:29:38 +00:00
vec2 TargetPos = vec2((Minpos.x + Maxpos.x) / 2.0f, (Minpos.y + Maxpos.y) / 2.0f);
// dont hide the position hud if its only one player
m_MultiViewShowHud = AmountPlayers == 1;
// get the average velocity
float AvgVel = clamp(TmpVel / AmountPlayers ? TmpVel / (float)AmountPlayers : 0.0f, 0.0f, 1000.0f);
if(m_MultiView.m_OldPersonalZoom == m_MultiViewPersonalZoom)
m_Camera.SetZoom(CalculateMultiViewZoom(Minpos, Maxpos, AvgVel), g_Config.m_ClMultiViewZoomSmoothness);
else
m_Camera.SetZoom(CalculateMultiViewZoom(Minpos, Maxpos, AvgVel), 50);
m_Snap.m_SpecInfo.m_Position = m_MultiView.m_OldPos + ((TargetPos - m_MultiView.m_OldPos) * CalculateMultiViewMultiplier(TargetPos));
m_MultiView.m_OldPos = m_Snap.m_SpecInfo.m_Position;
m_Snap.m_SpecInfo.m_UsePosition = true;
}
bool CGameClient::InitMultiView(int Team)
2023-05-24 17:29:38 +00:00
{
float Width, Height;
CleanMultiViewIds();
m_MultiView.m_IsInit = true;
// get the current view coordinates
RenderTools()->CalcScreenParams(Graphics()->ScreenAspect(), m_Camera.m_Zoom, &Width, &Height);
vec2 AxisX = vec2(m_Camera.m_Center.x - (Width / 2), m_Camera.m_Center.x + (Width / 2));
vec2 AxisY = vec2(m_Camera.m_Center.y - (Height / 2), m_Camera.m_Center.y + (Height / 2));
if(Team > 0)
{
m_MultiViewTeam = Team;
for(int i = 0; i < MAX_CLIENTS; i++)
m_aMultiViewId[i] = m_Teams.Team(i) == Team;
}
2023-05-24 17:29:38 +00:00
else
{
// we want to allow spectating players in teams directly if there is no other team on screen
// to do that, -1 is used temporarily for "we don't know which team to spectate yet"
m_MultiViewTeam = -1;
2023-05-24 17:29:38 +00:00
int Count = 0;
for(int i = 0; i < MAX_CLIENTS; i++)
{
vec2 PlayerPos;
// get the position of the player
if(m_Snap.m_aCharacters[i].m_Active)
PlayerPos = vec2(m_Snap.m_aCharacters[i].m_Cur.m_X, m_Snap.m_aCharacters[i].m_Cur.m_Y);
else if(m_aClients[i].m_Spec)
PlayerPos = m_aClients[i].m_SpecChar;
else
continue;
if(PlayerPos.x == 0 || PlayerPos.y == 0)
2023-05-24 17:29:38 +00:00
continue;
// skip players that aren't in view
if(PlayerPos.x <= AxisX.x || PlayerPos.x >= AxisX.y || PlayerPos.y <= AxisY.x || PlayerPos.y >= AxisY.y)
continue;
if(m_MultiViewTeam == -1)
2023-05-24 17:29:38 +00:00
{
// use the current player's team for now, but it might switch to team 0 if any other team is found
m_MultiViewTeam = m_Teams.Team(i);
}
else if(m_MultiViewTeam != 0 && m_Teams.Team(i) != m_MultiViewTeam)
{
// mismatched teams; remove all previously added players again and switch to team 0 instead
std::fill_n(m_aMultiViewId, i, false);
m_MultiViewTeam = 0;
2023-05-24 17:29:38 +00:00
}
m_aMultiViewId[i] = true;
Count++;
2023-05-24 17:29:38 +00:00
}
// might still be -1 if not a single player was in view; fallback to team 0 in that case
if(m_MultiViewTeam == -1)
m_MultiViewTeam = 0;
2023-05-24 17:29:38 +00:00
// we are spectating only one player
m_MultiView.m_Solo = Count == 1;
}
if(IsMultiViewIdSet())
{
int SpectatorID = m_Snap.m_SpecInfo.m_SpectatorID;
int NewSpectatorID = -1;
vec2 CurPosition(m_Camera.m_Center);
if(SpectatorID != SPEC_FREEVIEW)
{
const CNetObj_Character &CurCharacter = m_Snap.m_aCharacters[SpectatorID].m_Cur;
CurPosition.x = CurCharacter.m_X;
CurPosition.y = CurCharacter.m_Y;
}
int ClosestDistance = std::numeric_limits<int>::max();
for(int i = 0; i < MAX_CLIENTS; i++)
{
if(!m_Snap.m_apPlayerInfos[i] || m_Snap.m_apPlayerInfos[i]->m_Team == TEAM_SPECTATORS || m_Teams.Team(i) != m_MultiViewTeam)
continue;
2023-05-24 17:29:38 +00:00
vec2 PlayerPos;
if(m_Snap.m_aCharacters[i].m_Active)
PlayerPos = vec2(m_aClients[i].m_RenderPos.x, m_aClients[i].m_RenderPos.y);
else if(m_aClients[i].m_Spec) // tee is in spec
PlayerPos = m_aClients[i].m_SpecChar;
else
continue;
int Distance = distance(CurPosition, PlayerPos);
if(NewSpectatorID == -1 || Distance < ClosestDistance)
{
NewSpectatorID = i;
ClosestDistance = Distance;
}
}
if(NewSpectatorID > -1)
m_Spectator.Spectate(NewSpectatorID);
2023-05-24 17:29:38 +00:00
}
return IsMultiViewIdSet();
2023-05-24 17:29:38 +00:00
}
float CGameClient::CalculateMultiViewMultiplier(vec2 TargetPos)
2023-05-24 17:29:38 +00:00
{
float MaxCameraDist = 200.0f;
float MinCameraDist = 20.0f;
float MaxVel = g_Config.m_ClMultiViewSensitivity / 150.0f;
2023-05-24 17:29:38 +00:00
float MinVel = 0.007f;
float CurrentCameraDistance = distance(m_MultiView.m_OldPos, TargetPos);
float UpperLimit = 1.0f;
2023-05-24 17:29:38 +00:00
if(m_MultiView.m_Teleported && CurrentCameraDistance <= 100)
m_MultiView.m_Teleported = false;
2023-05-24 17:29:38 +00:00
// somebody got teleported very likely
if((m_MultiView.m_Teleported || CurrentCameraDistance - m_MultiView.m_OldCameraDistance > 100) && m_MultiView.m_OldCameraDistance != 0.0f)
{
UpperLimit = 0.1f; // dont try to compensate it by flickering
m_MultiView.m_Teleported = true;
}
m_MultiView.m_OldCameraDistance = CurrentCameraDistance;
return clamp(MapValue(MaxCameraDist, MinCameraDist, MaxVel, MinVel, CurrentCameraDistance), MinVel, UpperLimit);
2023-05-24 17:29:38 +00:00
}
float CGameClient::CalculateMultiViewZoom(vec2 MinPos, vec2 MaxPos, float Vel)
{
float Ratio = Graphics()->ScreenAspect();
float ZoomX = 0.0f, ZoomY;
// only calc two axis if the aspect ratio is not 1:1
if(Ratio != 1.0f)
2023-06-30 16:15:29 +00:00
ZoomX = (0.001309f - 0.000328 * Ratio) * (MaxPos.x - MinPos.x) + (0.741413f - 0.032959 * Ratio);
2023-05-24 17:29:38 +00:00
// calculate the according zoom with linear function
2023-06-30 16:15:29 +00:00
ZoomY = 0.001309f * (MaxPos.y - MinPos.y) + 0.741413f;
2023-05-24 17:29:38 +00:00
// choose the highest zoom
float Zoom = std::max(ZoomX, ZoomY);
// zoom out to maximum 10 percent of the current zoom for 70 velocity
float Diff = clamp(MapValue(70.0f, 15.0f, Zoom * 0.10f, 0.0f, Vel), 0.0f, Zoom * 0.10f);
2023-09-05 19:24:33 +00:00
// zoom should stay between 1.1 and 20.0
2023-05-24 17:29:38 +00:00
Zoom = clamp(Zoom + Diff, 1.1f, 20.0f);
2023-10-11 19:08:37 +00:00
// dont go below default zoom
Zoom = std::max(float(std::pow(CCamera::ZOOM_STEP, g_Config.m_ClDefaultZoom - 10)), Zoom);
2023-05-24 17:29:38 +00:00
// add the user preference
Zoom -= (Zoom * 0.1f) * m_MultiViewPersonalZoom;
2023-05-24 17:29:38 +00:00
m_MultiView.m_OldPersonalZoom = m_MultiViewPersonalZoom;
return Zoom;
}
float CGameClient::MapValue(float MaxValue, float MinValue, float MaxRange, float MinRange, float Value)
{
return (MaxRange - MinRange) / (MaxValue - MinValue) * (Value - MinValue) + MinRange;
}
void CGameClient::ResetMultiView()
{
2023-08-29 13:52:31 +00:00
m_Camera.SetZoom(std::pow(CCamera::ZOOM_STEP, g_Config.m_ClDefaultZoom - 10), g_Config.m_ClSmoothZoomTime);
m_MultiViewPersonalZoom = 0;
m_MultiViewActivated = false;
2023-05-24 17:29:38 +00:00
m_MultiView.m_Solo = false;
m_MultiView.m_IsInit = false;
m_MultiView.m_Teleported = false;
m_MultiView.m_OldCameraDistance = 0.0f;
2023-05-24 17:29:38 +00:00
}
void CGameClient::CleanMultiViewIds()
{
std::fill(std::begin(m_aMultiViewId), std::end(m_aMultiViewId), false);
std::fill(std::begin(m_MultiView.m_aLastFreeze), std::end(m_MultiView.m_aLastFreeze), 0.0f);
std::fill(std::begin(m_MultiView.m_aVanish), std::end(m_MultiView.m_aVanish), false);
}
void CGameClient::CleanMultiViewId(int ClientID)
{
if(ClientID >= MAX_CLIENTS || ClientID < 0)
return;
m_aMultiViewId[ClientID] = false;
m_MultiView.m_aLastFreeze[ClientID] = 0.0f;
m_MultiView.m_aVanish[ClientID] = false;
}
2023-05-24 17:29:38 +00:00
bool CGameClient::IsMultiViewIdSet()
{
return std::any_of(std::begin(m_aMultiViewId), std::end(m_aMultiViewId), [](bool IsSet) { return IsSet; });
}
int CGameClient::FindFirstMultiViewId()
{
int ClientID = -1;
for(int i = 0; i < MAX_CLIENTS; i++)
{
if(m_aMultiViewId[i] && !m_MultiView.m_aVanish[i])
return i;
}
return ClientID;
}