From a460fbeaeb14cc94bc4a6bcedd84268c03d534bb Mon Sep 17 00:00:00 2001 From: jenz Date: Wed, 10 Jun 2026 01:05:57 +0200 Subject: [PATCH] initial commit of render distance controller for the webclient to compensate for lacking rasterize --- fog_controller/gamedata/player-fog.games.txt | 33 +++ .../scripting/fog_controller_client.sp | 278 ++++++++++++++++++ 2 files changed, 311 insertions(+) create mode 100644 fog_controller/gamedata/player-fog.games.txt create mode 100644 fog_controller/scripting/fog_controller_client.sp diff --git a/fog_controller/gamedata/player-fog.games.txt b/fog_controller/gamedata/player-fog.games.txt new file mode 100644 index 0000000..791a750 --- /dev/null +++ b/fog_controller/gamedata/player-fog.games.txt @@ -0,0 +1,33 @@ +"Games" +{ + "cstrike" + { + "Signatures" + { + "CBasePlayer::InputSetFogController" + { + "library" "server" + "windows" "\x55\x8B\xEC\x56\x8B\xF1\x8B\x4D\x08\x83\xC1\x08\x83\x79\x10\x02\x75\x2A\x8B\x01\xBA\x2A\x2A\x2A\x2A\x85\xC0\x0F\x45\xD0\xEB\x2A\xE8\x2A\x2A\x2A\x2A\x8B\xD0\x6A\x00" + "linux" "@_ZN11CBasePlayer21InputSetFogControllerER11inputdata_t" + } + } + + "Functions" + { + "CBasePlayer::InputSetFogController" + { + "signature" "CBasePlayer::InputSetFogController" + "callconv" "thiscall" + "return" "void" + "this" "entity" + "arguments" + { + "inputdata" + { + "type" "objectptr" + } + } + } + } + } +} diff --git a/fog_controller/scripting/fog_controller_client.sp b/fog_controller/scripting/fog_controller_client.sp new file mode 100644 index 0000000..0d3b1b6 --- /dev/null +++ b/fog_controller/scripting/fog_controller_client.sp @@ -0,0 +1,278 @@ +#include +#include +#include + +public Plugin myinfo = +{ + name = "Render Distance Limiter", + author = "Dysphie, repurposed for unloze webclient using some AI", + description = "", + version = "", + url = "" +}; + +int g_iClientFogEntity[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; +float g_OverrideDist[MAXPLAYERS + 1] = { -1.0, ... }; +bool bEnabled[MAXPLAYERS + 1]; + +float g_fClientEstimatedFPS[MAXPLAYERS + 1]; + +// New tracking arrays for your global variables section +float g_fLastStableDist[MAXPLAYERS + 1] = { -1.0, ... }; +float g_fDistCooldown[MAXPLAYERS + 1] = { 0.0, ... }; + +int g_iLastCmdNum[MAXPLAYERS + 1]; +float g_fFPSIntervalStart[MAXPLAYERS + 1]; +int g_iCmdsInInterval[MAXPLAYERS + 1]; + +public void OnPluginStart() +{ + SetupDetours(); + CreateTimer(0.5, Timer_printFPS, _, TIMER_REPEAT); +} + +//ai generated +public Action OnPlayerRunCmd(int client, int &buttons, int &impulse, float vel[3], float angles[3], int &weapon, int &subtype, int &cmdnum, int &tickcount, int &seed, int mouse[2]) +{ + if (IsFakeClient(client) || !bEnabled[client]) + return Plugin_Continue; + + // Check if this is a brand new user command execution frame + if (cmdnum != g_iLastCmdNum[client]) + { + g_iLastCmdNum[client] = cmdnum; + g_iCmdsInInterval[client]++; + } + + float fCurrentTime = GetEngineTime(); + float fTimeElapsed = fCurrentTime - g_fFPSIntervalStart[client]; + + // Evaluate the true input frequency every 0.5 seconds + if (fTimeElapsed >= 0.5) + { + if (g_fFPSIntervalStart[client] > 0.0) + { + // Raw calculated input framerate based on sequential command progression + float fCalculatedFPS = float(g_iCmdsInInterval[client]) / fTimeElapsed; + + // FAST-DROP BYPASS: If the client just lost more than 25% of their frame rate instantly + // skip the dampening math and force the estimation down immediately. + if (fCalculatedFPS < (g_fClientEstimatedFPS[client] * 0.75)) + { + g_fClientEstimatedFPS[client] = fCalculatedFPS; // Hard-drop the value + } + else + { + // Smoothly recover/climb when frame rates are stable or increasing + g_fClientEstimatedFPS[client] = (g_fClientEstimatedFPS[client] * 0.8) + (fCalculatedFPS * 0.2); + } + } + + // Reset interval buckets + g_fFPSIntervalStart[client] = fCurrentTime; + g_iCmdsInInterval[client] = 0; + } + + return Plugin_Continue; +} + +//ai generated +public Action Timer_printFPS(Handle timer) +{ + for (int i = 1; i <= MaxClients; i++) + { + if (IsClientConnected(i) && IsClientInGame(i) && !IsFakeClient(i) && bEnabled[i]) + { + float fCurrentFPS = g_fClientEstimatedFPS[i] - 4.0; + float currentDist = g_OverrideDist[i]; + float targetDist = currentDist; + + // 1. HARD CRASH: FPS has tanked down to unplayable levels. + if (fCurrentFPS <= 25.0) + { + // If we have a cached, proven stable distance, use it immediately! + if (g_fLastStableDist[i] != -1.0 && g_fLastStableDist[i] < currentDist) + { + targetDist = g_fLastStableDist[i]; + } + else + { + // Emergency fallback steps if we dont have a cached history yet + if (currentDist == -1.0 || currentDist > 1200.0) + { + targetDist = 1200.0; + } + else + { + targetDist = 600.0; + } + } + + // Penalize the cooldown heavily because we just choked on rendering + g_fDistCooldown[i] = GetEngineTime() + 15.0; + } + + // 2. WARNING ZONE: Framerate is beginning to dip + else if (fCurrentFPS <= 38.0) + { + if (currentDist == -1.0 || currentDist > 2000.0) + { + targetDist = 2000.0; + g_fDistCooldown[i] = GetEngineTime() + 10.0; + } + } + + // 3. RECOVERY ZONE: Performance is solid. Lets see if we can open up the view. + else if (fCurrentFPS >= 48.0) + { + if (currentDist != -1.0) + { + // Since the game is running flawlessly at this specific distance, + // we update our safety cache to remember this specific float. + g_fLastStableDist[i] = currentDist; + + // Only venture out if our time barrier has expired + if (GetEngineTime() >= g_fDistCooldown[i]) + { + if (currentDist == 600.0) targetDist = 1200.0; + else if (currentDist == 1200.0) targetDist = 2000.0; + else if (currentDist == 2000.0) targetDist = 4000.0; + else if (currentDist == 4000.0) targetDist = -1.0; + + // Give the client a test window to sample the new distance + g_fDistCooldown[i] = GetEngineTime() + 8.0; + } + } + else + { + // If the client is running fine at infinite distance (-1.0), + // clear the history out so we can recalculate from a clean slate later + g_fLastStableDist[i] = -1.0; + } + } + + if (targetDist != currentDist) + { + ApplyFogValues(i, targetDist); + } + } + } + return Plugin_Continue; +} + +void ApplyFogValues(int client, float dist) +{ + g_OverrideDist[client] = dist; + int fog = EntRefToEntIndex(g_iClientFogEntity[client]); + + // Create the persistent entity once if it vanished or hasnt been created yet + if (fog == -1 || !IsValidEntity(fog)) + { + fog = CreateEntityByName("env_fog_controller"); + if (fog != -1) + { + DispatchSpawn(fog); + g_iClientFogEntity[client] = EntIndexToEntRef(fog); + } + } + + if (fog != -1 && IsValidEntity(fog)) + { + if (dist == -1.0) + { + // Back to map defaults safely + SetEntProp(fog, Prop_Send, "m_fog.enable", 0); + SetEntPropFloat(fog, Prop_Send, "m_fog.farz", 0.0); + } + else + { + // Apply strict culling boundaries explicitly + SetEntProp(fog, Prop_Send, "m_fog.enable", 1); + SetEntPropFloat(fog, Prop_Send, "m_fog.farz", dist); + SetEntPropFloat(fog, Prop_Send, "m_fog.end", dist); + SetEntPropFloat(fog, Prop_Send, "m_fog.maxdensity", 1.0); + } + + // Keep the entity assigned cleanly inside data tables + SetFogController(client, fog); + + // Single fire state changes to replicate out smoothly + ChangeEdictState(client, FindSendPropInfo("CBasePlayer", "m_hCtrl")); + } +} + +public void OnClientDisconnect(int client) +{ + // Clean up entity allocation to prevent lingering map errors + int fog = EntRefToEntIndex(g_iClientFogEntity[client]); + if (fog != -1 && IsValidEntity(fog)) + { + RemoveEntity(fog); + } + + g_iClientFogEntity[client] = INVALID_ENT_REFERENCE; + g_OverrideDist[client] = -1.0; + bEnabled[client] = false; +} + +void SetupDetours() +{ + GameData gamedata = new GameData("player-fog.games"); + if (!gamedata) { + SetFailState("Missing gamedata file"); + } + + DynamicDetour detour = DynamicDetour.FromConf(gamedata, "CBasePlayer::InputSetFogController"); + if (!detour) + SetFailState("Failed to find signature CBasePlayer::InputSetFogController"); + detour.Enable(Hook_Pre, Detour_InputSetFogController); + delete detour; +} + +MRESReturn Detour_InputSetFogController(int client, DHookParam params) +{ + RequestFrame(Frame_FogControllerChanged, GetClientSerial(client)); + return MRES_Ignored; +} + +void FogControllerChanged(int client) +{ + if (g_OverrideDist[client] != -1.0) { + ApplyFogValues(client, g_OverrideDist[client]); + } +} + +void Frame_FogControllerChanged(int clientSerial) +{ + int client = GetClientFromSerial(clientSerial); + if (client && IsClientInGame(client)) + { + FogControllerChanged(client); + } +} + +public void OnClientAuthorized(int client, const char[] auth) +{ + bEnabled[client] = false; + g_OverrideDist[client] = -1.0; + g_iClientFogEntity[client] = INVALID_ENT_REFERENCE; + + char sIP[32]; + GetClientIP(client, sIP, sizeof(sIP)); + + char allowed_ips[128]; + ConVar sv_set_steam_id_ips = FindConVar("sv_set_steam_id_ips"); + if (sv_set_steam_id_ips != null) + { + sv_set_steam_id_ips.GetString(allowed_ips, sizeof(allowed_ips)); + if (StrEqual(sIP, allowed_ips)) + { + bEnabled[client] = true; + } + } +} + +void SetFogController(int client, int fog) +{ + SetEntPropEnt(client, Prop_Data, "m_hCtrl", fog); +}