diff --git a/cl_dll/CMakeLists.txt b/cl_dll/CMakeLists.txt index 5407e2b8f..4f2f1f9ed 100644 --- a/cl_dll/CMakeLists.txt +++ b/cl_dll/CMakeLists.txt @@ -108,6 +108,7 @@ set (CLDLL_SOURCES health.cpp hud.cpp hud_caption.cpp + hud_error_collection.cpp hud_inventory.cpp hud_msg.cpp hud_redraw.cpp @@ -130,6 +131,7 @@ set (CLDLL_SOURCES ../pm_shared/pm_shared.cpp ../game_shared/tex_materials.cpp ../game_shared/parsetext.cpp + ../game_shared/error_collector.cpp ../game_shared/fx_types.cpp ../game_shared/json_utils.cpp ../game_shared/util_shared.cpp diff --git a/cl_dll/hud.cpp b/cl_dll/hud.cpp index 2c2760337..7c06ef0a0 100644 --- a/cl_dll/hud.cpp +++ b/cl_dll/hud.cpp @@ -32,6 +32,7 @@ #include "demo_api.h" #include "environment.h" +#include "error_collector.h" hud_player_info_t g_PlayerInfoList[MAX_PLAYERS+1]; // player info from the engine extra_player_info_t g_PlayerExtraInfo[MAX_PLAYERS+1]; // additional player info sent directly to the client dll @@ -734,6 +735,7 @@ void CHud::Init( void ) m_MOTD.Init(); m_Scoreboard.Init(); + m_ErrorCollection.Init(); m_Menu.Init(); @@ -748,6 +750,12 @@ void CHud::Init( void ) MsgFunc_ResetHUD( 0, 0, NULL ); ClientCmd( "richpresence_gamemode\n" ); ClientCmd( "richpresence_update\n" ); + + if (g_errorCollector.HasErrors()) + { + m_ErrorCollection.SetClientErrors(g_errorCollector.GetFullString()); + g_errorCollector.Clear(); + } } const char* strStartsWith(const char* str, const char* start) @@ -1251,6 +1259,7 @@ void CHud::VidInit( void ) #endif m_MOTD.VidInit(); m_Scoreboard.VidInit(); + m_ErrorCollection.VidInit(); m_Nightvision.VidInit(); m_Caption.VidInit(); diff --git a/cl_dll/hud.h b/cl_dll/hud.h index 198bef171..6cb84eafb 100644 --- a/cl_dll/hud.h +++ b/cl_dll/hud.h @@ -250,6 +250,25 @@ class CHudMOTD : public CHudBase int m_iMaxLength; }; +class CHudErrorCollection : public CHudBase +{ +public: + int Init(); + int VidInit(); + void Reset(); + int Draw(float flTime); + int MsgFunc_ParseErrors( const char *pszName, int iSize, void *pbuf ); + void SetClientErrors(const std::string& str); + +private: + int DrawMultiLineString(const char* str, int xpos, int ypos, int xmax, const int LineHeight); + + std::string m_clientErrorString; + std::string m_serverErrorString; + + cvar_t* m_pCvarDeveloper; +}; + struct CaptionProfile_t { char firstLetter; @@ -1011,6 +1030,7 @@ class CHud CHudStatusIcons m_StatusIcons; CHudScoreboard m_Scoreboard; CHudMOTD m_MOTD; + CHudErrorCollection m_ErrorCollection; CHudNightvision m_Nightvision; CHudCaption m_Caption; diff --git a/cl_dll/hud_error_collection.cpp b/cl_dll/hud_error_collection.cpp new file mode 100644 index 000000000..a301d0212 --- /dev/null +++ b/cl_dll/hud_error_collection.cpp @@ -0,0 +1,116 @@ +#include "hud.h" +#include "cl_util.h" +#include "parsemsg.h" +#include "r_studioint.h" + +extern engine_studio_api_t IEngineStudio; + +DECLARE_MESSAGE( m_ErrorCollection, ParseErrors ) + +int CHudErrorCollection::Init() +{ + m_pCvarDeveloper = nullptr; + gHUD.AddHudElem(this); + m_iFlags &= ~HUD_ACTIVE; + HOOK_MESSAGE(ParseErrors); + return 1; +} + +int CHudErrorCollection::VidInit() +{ + m_pCvarDeveloper = IEngineStudio.GetCvar("developer"); + return 1; +} + +void CHudErrorCollection::Reset() +{ + if (m_clientErrorString.empty()) + m_iFlags &= ~HUD_ACTIVE; + else + m_iFlags |= HUD_ACTIVE; + m_serverErrorString.clear(); +} + +int CHudErrorCollection::Draw(float flTime) +{ + if (!m_pCvarDeveloper || m_pCvarDeveloper->value == 0) + return 1; + + if (m_serverErrorString.empty() && m_clientErrorString.empty()) + return 1; + + const int LineHeight = CHud::UtfText::LineHeight(); + int ypos = LineHeight * 2; + int xpos = 30; + int xmax = ScreenWidth; + + int r = 255; + int g = 140; + int b = 0; + + if (m_serverErrorString.size()) + { + CHud::UtfText::DrawString(xpos, ypos, xmax, "SERVER ERRORS:", r, g, b); + ypos += LineHeight; + ypos = DrawMultiLineString(m_serverErrorString.c_str(), xpos, ypos, xmax, LineHeight); + ypos += LineHeight; + } + + if (m_clientErrorString.size()) + { + CHud::UtfText::DrawString(xpos, ypos, xmax, "CLIENT ERRORS:", r, g, b); + ypos += LineHeight; + DrawMultiLineString(m_clientErrorString.c_str(), xpos, ypos, xmax, LineHeight); + } + + + return 1; +} + +int CHudErrorCollection::MsgFunc_ParseErrors(const char *pszName, int iSize, void *pbuf) +{ + BEGIN_READ( pbuf, iSize ); + int is_finished = READ_BYTE(); + const char* str = READ_STRING(); + + m_serverErrorString += str; + + if (is_finished) + { + m_iFlags |= HUD_ACTIVE; + } + + return is_finished ? 1 : 0; +} + +void CHudErrorCollection::SetClientErrors(const std::string &str) +{ + m_clientErrorString = str; + if (m_clientErrorString.size()) + { + m_iFlags |= HUD_ACTIVE; + } +} + +int CHudErrorCollection::DrawMultiLineString(const char *str, int xpos, int ypos, int xmax, const int LineHeight) +{ + int r = 255; + int g = 140; + int b = 0; + + const char *ch = str; + while(*ch) + { + const char *next_line = ch; + for(; *next_line != '\n' && *next_line != '\0'; next_line++) + ; + CHud::UtfText::DrawString( xpos, ypos, xmax, ch, r, g, b, next_line - ch ); + + ypos += LineHeight; + + ch = next_line; + if (*ch == '\n') + ch++; + } + return ypos; +} diff --git a/dlls/CMakeLists.txt b/dlls/CMakeLists.txt index 5e8df5f92..28026ada5 100644 --- a/dlls/CMakeLists.txt +++ b/dlls/CMakeLists.txt @@ -198,6 +198,7 @@ set (SVDLL_SOURCES ../pm_shared/pm_shared.cpp ../game_shared/tex_materials.cpp ../game_shared/parsetext.cpp + ../game_shared/error_collector.cpp ../game_shared/fx_types.cpp ../game_shared/json_utils.cpp ../game_shared/util_shared.cpp diff --git a/dlls/player.cpp b/dlls/player.cpp index 7e0355a1e..310e16d50 100644 --- a/dlls/player.cpp +++ b/dlls/player.cpp @@ -42,6 +42,7 @@ #include "inventory.h" #include "followers.h" #include "common_soundscripts.h" +#include "error_collector.h" #if FEATURE_ROPE #include "ropes.h" @@ -199,6 +200,7 @@ int gmsgTeamInfo = 0; int gmsgTeamScore = 0; int gmsgGameMode = 0; int gmsgMOTD = 0; +int gmsgParseErrors = 0; int gmsgServerName = 0; int gmsgAmmoPickup = 0; int gmsgWeapPickup = 0; @@ -300,6 +302,7 @@ void LinkUserMessages( void ) gmsgTeamScore = REG_USER_MSG( "TeamScore", -1 ); // sets the score of a team on the scoreboard gmsgGameMode = REG_USER_MSG( "GameMode", 1 ); gmsgMOTD = REG_USER_MSG( "MOTD", -1 ); + gmsgParseErrors = REG_USER_MSG( "ParseErrors", -1 ); gmsgServerName = REG_USER_MSG( "ServerName", -1 ); gmsgAmmoPickup = REG_USER_MSG( "AmmoPickup", 3 ); gmsgWeapPickup = REG_USER_MSG( "WeapPickup", 1 ); @@ -4919,6 +4922,24 @@ void CBasePlayer::SendAmmoUpdate( void ) } } +static void SendParseErrorsToClient(edict_t* client, const std::string& str) +{ + char chunk[121]; + const char* cstr = str.c_str(); + size_t offset = 0; + while (offset <= str.size()) + { + strncpy(chunk, cstr + offset, sizeof(chunk)-1); + chunk[sizeof(chunk)-1] = '\0'; + offset += sizeof(chunk)-1; + + MESSAGE_BEGIN( MSG_ONE, gmsgParseErrors, NULL, client ); + WRITE_BYTE( offset >= str.size() ? TRUE : FALSE ); + WRITE_STRING( chunk ); + MESSAGE_END(); + } +} + /* ========================================================= UpdateClientData @@ -4960,6 +4981,15 @@ void CBasePlayer::UpdateClientData( void ) } } + if ( g_psv_developer->value > 0 && !g_pGameRules->IsMultiplayer() || (entindex() == 1 && !IS_DEDICATED_SERVER()) ) + { + if (g_errorCollector.HasErrors()) + { + std::string fullErrorString = g_errorCollector.GetFullString(); + SendParseErrorsToClient(edict(), fullErrorString); + } + } + m_fGameHUDInitialized = TRUE; m_iObserverLastMode = OBS_ROAMING; diff --git a/dlls/wscript b/dlls/wscript index 25751bdf1..ba4bae781 100644 --- a/dlls/wscript +++ b/dlls/wscript @@ -25,6 +25,7 @@ def build(bld): source = bld.path.ant_glob('**/*.cpp', excl=excluded_files) source += bld.path.parent.ant_glob([ 'pm_shared/*.cpp', + 'game_shared/error_collector.cpp', 'game_shared/fx_types.cpp', 'game_shared/json_utils.cpp', 'game_shared/parsetext.cpp', diff --git a/game_shared/error_collector.cpp b/game_shared/error_collector.cpp new file mode 100644 index 000000000..59cd58053 --- /dev/null +++ b/game_shared/error_collector.cpp @@ -0,0 +1,30 @@ +#include "error_collector.h" + +void ErrorCollector::AddError(const char *str) +{ + if (str) + _errors.push_back(str); +} + +bool ErrorCollector::HasErrors() const +{ + return _errors.size() > 0; +} + +std::string ErrorCollector::GetFullString() const +{ + std::string fullString; + for (const std::string& str : _errors) + { + fullString += str; + fullString += '\n'; + } + return fullString; +} + +void ErrorCollector::Clear() +{ + _errors.clear(); +} + +ErrorCollector g_errorCollector; diff --git a/game_shared/error_collector.h b/game_shared/error_collector.h new file mode 100644 index 000000000..4b8f630b7 --- /dev/null +++ b/game_shared/error_collector.h @@ -0,0 +1,21 @@ +#pragma once +#ifndef ERROR_COLLECTOR_H +#define ERROR_COLLECTOR_H + +#include +#include + +class ErrorCollector +{ +public: + void AddError(const char* str); + bool HasErrors() const; + std::string GetFullString() const; + void Clear(); +private: + std::vector _errors; +}; + +extern ErrorCollector g_errorCollector; + +#endif diff --git a/game_shared/json_utils.cpp b/game_shared/json_utils.cpp index f690c8f02..79ce89cec 100644 --- a/game_shared/json_utils.cpp +++ b/game_shared/json_utils.cpp @@ -1,17 +1,8 @@ #include "json_utils.h" -#if CLIENT_DLL -#include "cl_dll.h" -#define JSON_LOG gEngfuncs.Con_DPrintf -#define JSON_ERROR gEngfuncs.Con_DPrintf -#else -#include "util.h" -#define JSON_LOG(...) ALERT(at_aiconsole, ##__VA_ARGS__ ) -#define JSON_ERROR(...) ALERT(at_error, ##__VA_ARGS__ ) -#endif - #include "color_utils.h" #include "parsetext.h" +#include "error_collector.h" #include "rapidjson/stringbuffer.h" #include "rapidjson/writer.h" @@ -103,7 +94,10 @@ static void ReportParseErrors(const char* fileName, ParseResult& parseResult, co { size_t errorLine, errorColumn; CalculateLineAndColumnFromOffset(pMemFile, parseResult.Offset(), errorLine, errorColumn); - JSON_ERROR("%s: JSON parse error: %s (Line %lu, column %lu)\n", fileName, GetParseError_En(parseResult.Code()), errorLine, errorColumn); + + char buf[1024]; + _snprintf(buf, sizeof(buf), "%s: JSON parse error: %s (Line %zu, column %zu)", fileName, GetParseError_En(parseResult.Code()), errorLine, errorColumn); + g_errorCollector.AddError(buf); } class DefinitionsProvider : public IRemoteSchemaDocumentProvider @@ -177,13 +171,15 @@ bool ReadJsonDocumentWithSchema(Document &document, const char *pMemFile, int fi schemaPartValue->Accept(writer); } - JSON_ERROR("%s: value %s of property '%s' doesn't match the constraint '%s' in '%s': %s\n", + char buf[1028]; + _snprintf(buf, sizeof(buf), "%s: value %s of property '%s' doesn't match the constraint '%s' in '%s': %s\n", fileName, badValueBuffer.GetString(), docPathBuffer.GetString(), validator.GetInvalidSchemaKeyword(), schemaPathBuffer.GetString(), schemaPartBuffer.GetString()); + g_errorCollector.AddError(buf); return false; }