diff --git a/rts/Lua/LuaHandleSynced.cpp b/rts/Lua/LuaHandleSynced.cpp index 85851f3bc3..04f14faa61 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 5769543daa..6acda8aa85 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/Lua/LuaSyncedCtrl.cpp b/rts/Lua/LuaSyncedCtrl.cpp index d2fc09845c..41c666c237 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 84239a079a..3b59dcae8c 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); diff --git a/rts/Sim/Misc/Team.cpp b/rts/Sim/Misc/Team.cpp index 1c89bb3ec3..2486a94ed7 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 6bc74267dc..7bc426d30d 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 d8b5d4e5af..f582e9f4b2 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 2e0e2aa7a2..da7499d658 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 3e17d1171c..6d938cebd5 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 d7969be7aa..f815ae7f33 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 e7432aaaf5..31d7779059 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 a0e069c27d..e36f11dac3 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)