From cc2892079e82d9048768d8f9833e53671ce45a34 Mon Sep 17 00:00:00 2001 From: sprunk Date: Fri, 15 Aug 2025 03:52:51 +0200 Subject: [PATCH 1/2] Add `gadget:ResourceExcess(excessTable) -> bool` Passes a table: { [teamID] = {metal, energy, ...} } The resources are the amount over the storage max, accumulated over the frame. Called at the end of every frame, contains data for every team. Return true to take over engine handling; otherwise the native behaviour of buffering it until the next slowupdate will take place. --- rts/Lua/LuaHandleSynced.cpp | 37 ++++++++++++++++++++++++++++++++++++ rts/Lua/LuaHandleSynced.h | 2 ++ rts/Sim/Misc/Team.cpp | 7 ++++--- rts/Sim/Misc/Team.h | 4 ++++ rts/Sim/Misc/TeamHandler.cpp | 27 ++++++++++++++++++++++++++ rts/Sim/Misc/TeamHandler.h | 1 + rts/System/EventClient.h | 4 ++++ rts/System/EventHandler.cpp | 6 ++++++ rts/System/EventHandler.h | 2 ++ rts/System/Events.def | 4 ++++ 10 files changed, 91 insertions(+), 3 deletions(-) diff --git a/rts/Lua/LuaHandleSynced.cpp b/rts/Lua/LuaHandleSynced.cpp index 85851f3bc3c..04f14faa610 100644 --- a/rts/Lua/LuaHandleSynced.cpp +++ b/rts/Lua/LuaHandleSynced.cpp @@ -54,6 +54,7 @@ #include "System/Misc/TracyDefs.h" +#include LuaRulesParams::Params CSplitLuaHandle::gameParams; @@ -1196,6 +1197,42 @@ bool CSyncedLuaHandle::AllowResourceTransfer(int oldTeam, int newTeam, const cha return allow; } +/*** Called when excess resources are added. + * Accumulates all excesses within a single gameframe. + * + * @function SyncedCallins:ResourceExcess + * @param excesses table + * @return boolean whether or not Lua handled the event + */ +bool CSyncedLuaHandle::ResourceExcess(const std::map & excesses) +{ + RECOIL_DETAILED_TRACY_ZONE; + LUA_CALL_IN_CHECK(L, true); + luaL_checkstack(L, 3, __func__); + + static const LuaHashString cmdStr(__func__); + if (!cmdStr.GetGlobalFunc(L)) + return false; + + lua_createtable(L, excesses.size(), 1); + + for (const auto &[teamID, excess] : excesses) { + lua_createtable(L, excess.MAX_RESOURCES, 0); + for (const auto &[resourceID, resource] : std::views::enumerate(excess)) { + lua_pushnumber(L, resource); + lua_rawseti(L, -2, resourceID + 1); + } + lua_rawseti(L, -2, teamID); + } + + if (!RunCallIn(L, cmdStr, 1, 1)) + return false; + + const bool handled = luaL_optboolean(L, -1, false); + lua_pop(L, 1); + return handled; +} + /*** Determines if this unit can be controlled directly in FPS view. * diff --git a/rts/Lua/LuaHandleSynced.h b/rts/Lua/LuaHandleSynced.h index 5769543daa1..6acda8aa859 100644 --- a/rts/Lua/LuaHandleSynced.h +++ b/rts/Lua/LuaHandleSynced.h @@ -57,6 +57,8 @@ class CSyncedLuaHandle : public CLuaHandle bool CommandFallback(const CUnit* unit, const Command& cmd) override; bool AllowCommand(const CUnit* unit, const Command& cmd, int playerNum, bool fromSynced, bool fromLua) override; + bool ResourceExcess(const std::map & excess) override; + std::pair AllowUnitCreation(const UnitDef* unitDef, const CUnit* builder, const BuildInfo* buildInfo) override; bool AllowUnitTransfer(const CUnit* unit, int newTeam, bool capture) override; bool AllowUnitBuildStep(const CUnit* builder, const CUnit* unit, float part) override; diff --git a/rts/Sim/Misc/Team.cpp b/rts/Sim/Misc/Team.cpp index 1c89bb3ec34..2486a94ed7e 100644 --- a/rts/Sim/Misc/Team.cpp +++ b/rts/Sim/Misc/Team.cpp @@ -45,6 +45,7 @@ CR_REG_METADATA(CTeam, ( CR_MEMBER(resPrevExpense), CR_MEMBER(resShare), CR_MEMBER(resDelayedShare), + CR_MEMBER(resExcessThisFrame), CR_MEMBER(resSent), CR_MEMBER(resPrevSent), CR_MEMBER(resReceived), @@ -156,7 +157,7 @@ void CTeam::AddMetal(float amount, bool useIncomeMultiplier) if (res.metal <= resStorage.metal) return; - resDelayedShare.metal += (res.metal - resStorage.metal); + resExcessThisFrame.metal += (res.metal - resStorage.metal); res.metal = resStorage.metal; } @@ -170,7 +171,7 @@ void CTeam::AddEnergy(float amount, bool useIncomeMultiplier) resIncome.energy += amount; if (res.energy > resStorage.energy) { - resDelayedShare.energy += (res.energy - resStorage.energy); + resExcessThisFrame.energy += (res.energy - resStorage.energy); res.energy = resStorage.energy; } } @@ -196,7 +197,7 @@ void CTeam::AddResources(SResourcePack amount, bool useIncomeMultiplier) if (res[i] <= resStorage[i]) continue; - resDelayedShare[i] += (res[i] - resStorage[i]); + resExcessThisFrame[i] += (res[i] - resStorage[i]); res[i] = resStorage[i]; } } diff --git a/rts/Sim/Misc/Team.h b/rts/Sim/Misc/Team.h index 6bc74267dc9..7bc426d30d8 100644 --- a/rts/Sim/Misc/Team.h +++ b/rts/Sim/Misc/Team.h @@ -73,6 +73,9 @@ class CTeam : public TeamBase void AddUnit(CUnit* unit, AddType type); void RemoveUnit(CUnit* unit, RemoveType type); +private: + void HandleFrameExcess(); + public: int teamNum; unsigned int numUnits; // number of units this team controls @@ -88,6 +91,7 @@ class CTeam : public TeamBase SResourcePack resIncome, resPrevIncome; SResourcePack resExpense, resPrevExpense; SResourcePack resShare; + SResourcePack resExcessThisFrame; //< accumulates excess over a gameframe SResourcePack resDelayedShare; //< excess that might be shared next SlowUpdate SResourcePack resSent, resPrevSent; SResourcePack resReceived, resPrevReceived; diff --git a/rts/Sim/Misc/TeamHandler.cpp b/rts/Sim/Misc/TeamHandler.cpp index d8b5d4e5afc..f582e9f4b2a 100644 --- a/rts/Sim/Misc/TeamHandler.cpp +++ b/rts/Sim/Misc/TeamHandler.cpp @@ -7,6 +7,7 @@ #include "Game/GameSetup.h" #include "Sim/Misc/GlobalConstants.h" #include "Sim/Misc/GlobalSynced.h" +#include "System/EventHandler.h" #include "System/Misc/TracyDefs.h" @@ -113,9 +114,35 @@ void CTeamHandler::SetDefaultStartPositions(const CGameSetup* setup) } } +void CTeamHandler::HandleFrameExcess() +{ + std::map excesses; + for (const auto &team : teams) + excesses.emplace(team.teamNum, team.resExcessThisFrame); + + /* Note that `resDelayedShare` is a metaaccumulator, + * the reason to have this two-layer accumulation is + * that handling excess right when it happens would + * be too expensive (for example you can have tens of + * thousands of windgens each generating a resource + * instance), having the Lua event handled at slow + * update would reduce control, and having the engine + * handle excess natively outside slow update would + * be inconsistent with other native resource handling. */ + if (!eventHandler.ResourceExcess(excesses)) + for (auto &team : teams) + team.resDelayedShare += team.resExcessThisFrame; + + for (auto &team : teams) + team.resExcessThisFrame = 0.0f; +} + void CTeamHandler::GameFrame(int frameNum) { RECOIL_DETAILED_TRACY_ZONE; + + HandleFrameExcess(); + if ((frameNum % TEAM_SLOWUPDATE_RATE) != 0) return; diff --git a/rts/Sim/Misc/TeamHandler.h b/rts/Sim/Misc/TeamHandler.h index 2e0e2aa7a2b..da7499d6581 100644 --- a/rts/Sim/Misc/TeamHandler.h +++ b/rts/Sim/Misc/TeamHandler.h @@ -161,6 +161,7 @@ class CTeamHandler bool TransferTeamMaxUnits(CTeam* fromTeam, CTeam* toTeam, int transferAmnt); private: + void HandleFrameExcess(); /** * @brief gaia team diff --git a/rts/System/EventClient.h b/rts/System/EventClient.h index 3e17d1171c2..6d938cebd5a 100644 --- a/rts/System/EventClient.h +++ b/rts/System/EventClient.h @@ -4,6 +4,7 @@ #define EVENT_CLIENT_H #include +#include #include #include #include @@ -35,6 +36,7 @@ struct BuildInfo; struct FeatureDef; class LuaMaterial; struct WeaponDef; +struct SResourcePack; #ifndef zipFile // might be defined through zip.h already @@ -122,6 +124,8 @@ class CEventClient virtual void TeamDied(int teamID) {} virtual void TeamChanged(int teamID) {} + virtual bool ResourceExcess(const std::map & excess) { return false; } + virtual void PlayerChanged(int playerID) {} virtual void PlayerAdded(int playerID) {} virtual void PlayerRemoved(int playerID, int reason) {} diff --git a/rts/System/EventHandler.cpp b/rts/System/EventHandler.cpp index d7969be7aa7..f815ae7f33e 100644 --- a/rts/System/EventHandler.cpp +++ b/rts/System/EventHandler.cpp @@ -581,6 +581,12 @@ void CEventHandler::TeamDied(int teamID) ITERATE_EVENTCLIENTLIST(TeamDied, teamID); } +bool CEventHandler::ResourceExcess(const std::map &excess) +{ + ZoneScoped; + return ControlIterateDefFalse(listResourceExcess, &CEventClient::ResourceExcess, excess); +} + void CEventHandler::TeamChanged(int teamID) { ZoneScoped; diff --git a/rts/System/EventHandler.h b/rts/System/EventHandler.h index e7432aaaf50..31d77790597 100644 --- a/rts/System/EventHandler.h +++ b/rts/System/EventHandler.h @@ -60,6 +60,8 @@ class CEventHandler void TeamDied(int teamID); void TeamChanged(int teamID); + bool ResourceExcess(const std::map & excess); + void PlayerChanged(int playerID); void PlayerAdded(int playerID); void PlayerRemoved(int playerID, int reason); diff --git a/rts/System/Events.def b/rts/System/Events.def index a0e069c27d3..e36f11dac3d 100644 --- a/rts/System/Events.def +++ b/rts/System/Events.def @@ -31,8 +31,12 @@ SETUP_EVENT(GamePaused, MANAGED_BIT) SETUP_EVENT(GameFrame, MANAGED_BIT) SETUP_EVENT(GameFramePost, MANAGED_BIT) + SETUP_EVENT(TeamDied, MANAGED_BIT) SETUP_EVENT(TeamChanged, MANAGED_BIT) + + SETUP_EVENT(ResourceExcess, MANAGED_BIT | CONTROL_BIT) + SETUP_EVENT(PlayerChanged, MANAGED_BIT | UNSYNCED_BIT) SETUP_EVENT(PlayerAdded, MANAGED_BIT | UNSYNCED_BIT) SETUP_EVENT(PlayerRemoved, MANAGED_BIT | UNSYNCED_BIT) From 48acdac7054dc33433fc1861d5721468d17778dc Mon Sep 17 00:00:00 2001 From: Keith Harvey Date: Tue, 30 Jun 2026 03:46:48 -0600 Subject: [PATCH 2/2] Add spAddTeamResourceExcessStats Mostly for use with the new ResourceExcess callin. --- rts/Lua/LuaSyncedCtrl.cpp | 47 +++++++++++++++++++++++++++++++++++++++ rts/Lua/LuaSyncedCtrl.h | 1 + 2 files changed, 48 insertions(+) diff --git a/rts/Lua/LuaSyncedCtrl.cpp b/rts/Lua/LuaSyncedCtrl.cpp index d2fc09845cf..41c666c237b 100644 --- a/rts/Lua/LuaSyncedCtrl.cpp +++ b/rts/Lua/LuaSyncedCtrl.cpp @@ -164,6 +164,7 @@ bool LuaSyncedCtrl::PushEntries(lua_State* L) REGISTER_LUA_CFUNC(AddTeamResource); REGISTER_LUA_CFUNC(UseTeamResource); REGISTER_LUA_CFUNC(SetTeamResource); + REGISTER_LUA_CFUNC(AddTeamResourceExcessStats); REGISTER_LUA_CFUNC(SetTeamShareLevel); REGISTER_LUA_CFUNC(ShareTeamResource); @@ -1404,6 +1405,52 @@ int LuaSyncedCtrl::SetTeamShareLevel(lua_State* L) } +/*** + * Records resource excess for a team without moving resources. + * + * The engine normally tracks excess, but if you use `gadget:ResourceExcess` + * to handle it manually it's now also up to you to track stats. + * + * @function Spring.AddTeamResourceExcessStats + * @param teamID integer + * @param type ResourceName + * @param excess number Amount wasted this tick. + * @return nil + */ +int LuaSyncedCtrl::AddTeamResourceExcessStats(lua_State* L) +{ + const int teamID = luaL_checkint(L, 1); + + if (!teamHandler.IsValidTeam(teamID)) + return 0; + + if (!CanControlTeam(L, teamID)) + return 0; + + CTeam* team = teamHandler.Team(teamID); + + if (team == nullptr) + return 0; + + const char rtype = luaL_checkstring(L, 2)[0]; + if (rtype != 'm' && rtype != 'e') + return 0; + + const bool isMetal = (rtype == 'm'); + const float val = std::max(0.0f, luaL_checkfloat(L, 3)); + + float& resExcess = isMetal ? team->resPrevExcess.metal : team->resPrevExcess.energy; + + TeamStatistics& stats = team->GetCurrentStats(); + float& statExcess = isMetal ? stats.metalExcess : stats.energyExcess; + + resExcess += val; + statExcess += val; + + return 0; +} + + /*** Transfers resources between two teams. * Transfers directly, without involving AllowResourceTransfer callin. * Approximately equivalent to doing Use and Add for the sender and receiver, diff --git a/rts/Lua/LuaSyncedCtrl.h b/rts/Lua/LuaSyncedCtrl.h index 84239a079af..3b59dcae8cc 100644 --- a/rts/Lua/LuaSyncedCtrl.h +++ b/rts/Lua/LuaSyncedCtrl.h @@ -52,6 +52,7 @@ class LuaSyncedCtrl static int AddTeamResource(lua_State* L); static int UseTeamResource(lua_State* L); static int SetTeamResource(lua_State* L); + static int AddTeamResourceExcessStats(lua_State* L); static int SetTeamShareLevel(lua_State* L); static int ShareTeamResource(lua_State* L);