alpaca
King of Ungulates
- Joined
- Aug 3, 2006
- Messages
- 2,322
We all do stuff like looping over all players and all cities every turn, but doing so in every mod, and worse, often in every component of a mod where we need it, of course wastes a lot of system resources. So SamBC (I think) has been proposing to collect all these loops into single entities for a while now. I thought it might be a good idea to give that idea its own thread to discuss.
The proposition is basically this: We could define a set of common events that often come up in mods. Then, a single script would be created that everybody could add to their mod using the same name so it only exists once. This script would trigger events at the appropriate times and clients could simply create an event listener for these instead of writing their own loop. More importantly, meta-events could be created that handle more excessive checks, like checking for researched techs every turn or buildings being finished/purchased in cities.
Events to add (events listed in bold are already implemented):
This script defines a new content type called "ModScript" that uses the description type for load-order priority. Files with a higher priority are loaded later, so if you have two files A and B where B depends on A being executed, you can for example set A's description to 0 and B's to 1, which will load A first. If you want to add a script that uses CommonEvents, please load it as a ModScript rather than an InGameUIAddin to make sure the event system is ready to receive listeners. Alternatively, you can add the listeners at a later point in your client, such as when Events.LoadScreenClose is executed
CommonEvents.lua v0.2
The proposition is basically this: We could define a set of common events that often come up in mods. Then, a single script would be created that everybody could add to their mod using the same name so it only exists once. This script would trigger events at the appropriate times and clients could simply create an event listener for these instead of writing their own loop. More importantly, meta-events could be created that handle more excessive checks, like checking for researched techs every turn or buildings being finished/purchased in cities.
Events to add (events listed in bold are already implemented):
- TurnStartLoopPlayers(pPlayer)
- TurnStartLoopCities(pCity)
- TurnStartLoopPlayerCities(pPlayer, pCity)
- TurnStartLoopPlots(pPlot)
- TechResearched(pPlayer, iTech)
- BuildingCreated(pCity, iBuilding)
- BuildingGained(pCity, iBuilding)
- BuildingDestroyed(pCity, iBuilding)
- BuildingLost(pCity, iBuilding)
This script defines a new content type called "ModScript" that uses the description type for load-order priority. Files with a higher priority are loaded later, so if you have two files A and B where B depends on A being executed, you can for example set A's description to 0 and B's to 1, which will load A first. If you want to add a script that uses CommonEvents, please load it as a ModScript rather than an InGameUIAddin to make sure the event system is ready to receive listeners. Alternatively, you can add the listeners at a later point in your client, such as when Events.LoadScreenClose is executed
CommonEvents.lua v0.2
Code:
--[[
CommonEvents.lua
Creator: alpaca
Last Change: 06.02.2011
Version: 0.2
Description: Defines common lua events
]]--
include("lib")
------------------------------------------------------------------
-- Add-in handling
------------------------------------------------------------------
ModScriptAddins = {}
MapModData.ModScriptAddins = ModScriptAddins
function LoadAddins()
local addins = {}
-- get all addins
for addin in Modding.GetActivatedModEntryPoints("ModScript") do
local addinFile = addin.File;
local priority = tonumber(addin.Description) or 0
-- Get the absolute path and filename without extension.
local extension = Path.GetExtension(addinFile);
local path = string.sub(addinFile, 1, #addinFile - #extension);
addins[#addins + 1] = {priority, path}
end
-- sort addins; high priority is loaded last
table.sort(addins, function(lhs, rhs) return lhs[1] < rhs[1] end)
-- load addins
local ptr
for _, tab in ipairs(addins) do
ptr = ContextPtr:LoadNewContext(tab[2])
ModScriptAddins[#ModScriptAddins + 1] = ptr
end
end
------------------------------------------------------------------
-- TechResearched
------------------------------------------------------------------
--[[
Executed at: ActivePlayerTurnStart, SerialEventGameMessagePopup (tech popup)
Arguments: pPlayer, iTechID
Author: alpaca
Description:
TechResearched triggers whenever a player researches a technology. The check is performed at ActivePlayerTurnStart and, for the player, whenever a pop-up is displayed.
]]--
-- define namespaces so other mods can inspect the book-keeping tables if they want to
MapModData.CommonEvents = {}
MapModData.CommonEvents.TechResearched = {}
-- internal namespace
TechResearched = {}
TechResearched.HasListeners = false -- true if any listeners are hooked up
TechResearched.listeners = {} -- store a local list of hooked up listeners
TechResearched.techsUnresearched = {}
TechResearched.numTechsResearched = {}
-- initialise
-- gather all unresearched techs at game start (or load)
TechResearched.Init = function()
for kPlayer, pPlayer in pairs(Players) do
if pPlayer:IsAlive() then
TechResearched.techsUnresearched[kPlayer] = {}
local teamTechs = Teams[pPlayer:GetTeam()]:GetTeamTechs()
for pTech in GameInfo.Technologies() do
if not teamTechs:HasTech(pTech.ID) then
TechResearched.techsUnresearched[kPlayer][pTech.ID] = true
end
end
TechResearched.numTechsResearched[kPlayer] = teamTechs:GetNumTechsKnown()
end
end
-- inject into superglobal namespace
MapModData.CommonEvents.TechResearched.TechsUnresearched = TechResearched.techsUnresearched
MapModData.CommonEvents.TechResearched.NumTechsResearched = TechResearched.numTechsResearched
end
TechResearched.CheckTechs = function(kPlayer, pPlayer)
local teamTechs = Teams[pPlayer:GetTeam()]:GetTeamTechs()
-- only check the number of techs; since techs cannot be lost, it has to change if new techs were researched
local numTechs = teamTechs:GetNumTechsKnown()
if TechResearched.numTechsResearched[kPlayer] < numTechs - 1 then
-- more than one tech researched this turn, or initialising: check all techs
for iTechID, val in pairs(TechResearched.techsUnresearched[kPlayer]) do
if teamTechs:HasTech(iTechID) == true then
TechResearched.techsUnresearched[kPlayer][iTechID] = nil
TechResearched.Fire(pPlayer, iTechID)
end
end
elseif TechResearched.numTechsResearched[kPlayer] < numTechs then
-- only one tech researched, use fast path ofer LastTechAcquired
local lastTech = teamTechs:GetLastTechAcquired()
TechResearched.techsUnresearched[kPlayer][lastTech] = nil
TechResearched.Fire(pPlayer, lastTech)
end
TechResearched.numTechsResearched[kPlayer] = numTechs
end
-- checks for new techs on turn start
TechResearched.APTSListener = function()
for kPlayer,pPlayer in pairs(Players) do
if pPlayer:IsAlive() then
TechResearched.CheckTechs(kPlayer, pPlayer)
end
end
end
-- checks for new techs on pop-up for the active player
TechResearched.PopupListener = function(popupInfo)
-- AP always gets a pop-up when researching a tech so we can use it to do some more costly during-the-turn updates for him
if popupInfo.Type == ButtonPopupTypes.BUTTONPOPUP_TECH_AWARD then
TechResearched.CheckTechs(Game.GetActivePlayer(), Players[Game.GetActivePlayer()])
end
end
-- fires the event
TechResearched.Fire = function(pPlayer, iTechID)
-- only fire if there are listeners
if LuaEvents.TechResearchedEvent and LuaEvents.TechResearchedEvent.Count() > 0 then
LuaEvents.TechResearchedEvent(pPlayer, iTechID)
end
end
TechResearched.AddListener = function(listener)
if not TechResearched.HasListeners then
Events.ActivePlayerTurnStart.Add(TechResearched.APTSListener)
Events.SerialEventGameMessagePopup.Add(TechResearched.PopupListener)
end
TechResearched.HasListeners = true
TechResearched.listeners[#TechResearched.listeners + 1] = listener
LuaEvents.TechResearchedEvent.Add(listener)
end
-- listeners
LuaEvents.TechResearched.Add(TechResearched.AddListener)
TechResearched.Init()
------------------------------------------------------------------
-- TurnStartLoopPlots
------------------------------------------------------------------
--[[
Executed at: ActivePlayerTurnStart
Arguments: pPlot
Author: alpaca
Triggers events: TurnStartLoopPlots, TurnStartLoopPlotsPlayerN where N is any player ID
Description:
Loops through all plots at the start of the active player's turn. Provides additional listener interfaces that allow listening only to a certain player's plots.
]]--
TSLoopPlots = {}
TSLoopPlots.HasListeners = false
TSLoopPlots.listeners = {}
TSLoopPlots.playerListeners = {}
function TSLoopPlots.APTSListener()
for i = 0, Map.GetNumPlots() - 1 do
local pPlot = Map.GetPlotByIndex(i)
-- fire generic listeners first
LuaEvents.TurnStartLoopPlotsEvent(pPlot)
TSLoopPlots.FirePlayerEvents(pPlot)
end
end
function TSLoopPlots.FirePlayerEvents()
end
function TSLoopPlots.FirePlayerEventsIfListened(pPlot)
local iOwner = pPlot:GetOwner()
if iOwner > -1 and TSLoopPlots.playerListeners[iOwner] then
LuaEvents["TurnStartLoopPlotsPlayer"..iOwner](pPlot)
end
end
function TSLoopPlots.AddListener(listener, iPlayerID)
local pointer
if TSLoopPlots.HasListeners == false then
Events.ActivePlayerTurnStart.Add(TSLoopPlots.APTSListener)
end
if iPlayerID then
-- if the second argument is given, hook up to a specific player
pointer = TSLoopPlots.playerListeners[iPlayerID] or {}
pointer[#pointer + 1] = listener
TSLoopPlots.playerListeners[iPlayerID] = pointer
LuaEvents["TurnStartLoopPlotsPlayer"..iPlayerID].Add(listener)
-- hook up owner-checking once a listener wants to listen for them
TSLoopPlots.FirePlayerEvents = TSLoopPlots.FirePlayerEventsIfListened
else
-- otherwise trigger for all plots
TSLoopPlots.listeners[#TSLoopPlots.listeners + 1] = listener
LuaEvents.TurnStartLoopPlotsEvent.Add(listener)
end
TSLoopPlots.HasListeners = true
end
LuaEvents.TurnStartLoopPlots.Add(TSLoopPlots.AddListener)
------------------------------------------------------------------
-- load addins
LoadAddins()