Initiative: Common Lua Events

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):
  • 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

attachment.php



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()
 

Attachments

  • contentline.png
    contentline.png
    4.2 KB · Views: 472
Absolutely. Probably worth including TurnStartPlots(pPlot) as well, as possibly the most expensive to have lots of running.

However, what would be really great is if the standard event (start-of-turn) was picked up by the common script, the common script looked at each of the events it handles for this, and only bothers with the relevant loop if there are any. There must be some Lua magic to see if there are any events registered to a given name, without actually calling them, right? Otherwise the common script will create a pointless loop over each one that isn't being used.

I suppose the common script could set some values to zero and we make it a matter of protocol to increment the relevant value if we register an event - then it would just be able to check those values for >0, but magic would be better.
 
Absolutely. Probably worth including TurnStartPlots(pPlot) as well, as possibly the most expensive to have lots of running.

However, what would be really great is if the standard event (start-of-turn) was picked up by the common script, the common script looked at each of the events it handles for this, and only bothers with the relevant loop if there are any. There must be some Lua magic to see if there are any events registered to a given name, without actually calling them, right? Otherwise the common script will create a pointless loop over each one that isn't being used.

I suppose the common script could set some values to zero and we make it a matter of protocol to increment the relevant value if we register an event - then it would just be able to check those values for >0, but magic would be better.

LuaEvents have a count function, so yes, that should be pretty trivial

A question: If an event stores data, should we save the data using SaveUtils or should we re-construct it on game load? I think the latter is preferable in this case to allow plug&play into existing saves
 
I started working on the TechResearched event. Code in the first post as a basis for discussion and to lay down some conventions (using namespaces, for instance). Should be defined as an InGameUIAddin.

The count variable only counts locally, so I had to do a little trick. Instead of adding a listener to the TechResearchedEvent event, you invoke the TechResearched event with your listener as an argument. This allows the common events script to keep track of your listener. Basically, instead of the intuitive Add you just call the event itself, like this:

LuaEvents.TechResearched(function(p,i) print("Tech researched", i) end)

Note that the event is executed only if there is a listener, but at the moment it's not possible to remove a listener. Might be interesting to allow removing listeners, too, for example if you trigger a singular event on a certain tech, you could remove the listener once that tech rolled around

Edit: Oh, and make sure you call LuaEvents.TechResearched on LoadScreenClose or something to avoid load order problems.
 
Is it worth defining a new addin class to resolve load order issues? Make the common script define all the things it has to define, then load other addins to register the relevant events. Is there anything else we have to wait for before they define their events?
 
Is it worth defining a new addin class to resolve load order issues? Make the common script define all the things it has to define, then load other addins to register the relevant events. Is there anything else we have to wait for before they define their events?

No, you just run into problems if you try to hook up the event before CommonEvents is ready to receive them. Adding a new add-in type doesn't really cost anything. In fact, I think we can abuse the description as a priority parameter for load order to avoid such problems in client scripts, which would be useful at any rate. See first post for new proposition using a content type called "ModScript"
 
Added plot loop event. You can hook up to it directly or only tune in to listen for plots of a specific player only. The second variant was added as a demonstration of tricks with functions but also because it avoids each client script checking ownership for each plot if you only want to do stuff for the active player, for example (useful for UI functions).
 
That is looking beautiful... on the buildings thing, I can think of two semantics we'd want to catch, depending on what we were doing in reaction to them -

BuildingCreated/BuildingDestroyed(pCity,iBuilding)
BuildingGained/BuildingLost(pPlayer,iBuilding)

The first is appropriate if you're applying a modifier to a city for the building, certainly; the second when you're applying the modifier to the player (such as resources), as you need to know however it is gained (such as a building surviving city capture) and however it is lost (such as losing a city, whether or not the building survives). Some instances (such as building or selling) would trigger both. I don't think you can capture both with a single pair of functions, as in the case of a city being lost you'd be wanting to send a 'gained' for one player, a 'lost' for another, and they'd both be pointing at the same city... however, I think an adaptation of Why's BuildingResources logic would actually cover both without much extra adaptation.
 
That is looking beautiful... on the buildings thing, I can think of two semantics we'd want to catch, depending on what we were doing in reaction to them -

BuildingCreated/BuildingDestroyed(pCity,iBuilding)
BuildingGained/BuildingLost(pPlayer,iBuilding)

The first is appropriate if you're applying a modifier to a city for the building, certainly; the second when you're applying the modifier to the player (such as resources), as you need to know however it is gained (such as a building surviving city capture) and however it is lost (such as losing a city, whether or not the building survives). Some instances (such as building or selling) would trigger both. I don't think you can capture both with a single pair of functions, as in the case of a city being lost you'd be wanting to send a 'gained' for one player, a 'lost' for another, and they'd both be pointing at the same city... however, I think an adaptation of Why's BuildingResources logic would actually cover both without much extra adaptation.

The only cases I can think of where there would be a difference between a building being created and one being gained are things where you reward the player for the act of building something, i.e. a one-off wonder like the Porcelain Tower. In any case, I agree that BuildingCreated would be a subset of BuildingGained (i.e. BuildingCreated triggering implies BuildingGained triggering).

I'm not sure if a difference between BuildingLost and BuildingDestroyed is necessary but it's conceptually cleaner
 
The only cases I can think of where there would be a difference between a building being created and one being gained are things where you reward the player for the act of building something, i.e. a one-off wonder like the Porcelain Tower. In any case, I agree that BuildingCreated would be a subset of BuildingGained (i.e. BuildingCreated triggering implies BuildingGained triggering).

I'm not sure if a difference between BuildingLost and BuildingDestroyed is necessary but it's conceptually cleaner
Consider the case where a city is taken; if the building survives, but the bonus for the building is applied to the player (such as resource production), then one side needs to gain it and the other lose it, and you have to know which is which (arguments sent to the event). If, on the other hand, the building is destroyed, there's no difference (as long as the destruction event associates with the player who lost the city, not the one who just won it, as it would if you pick it up at the next ActivePlayerTurnStart with reference only to the city). For a final possibility, the bonus is attached to the city (say a happiness-from-buildings change, if they bring back that function), in which case all that matters is when it is built or destroyed.

BuildingCreated triggering BuildingGained itself would have some edge cases that would have to be watched for, perhaps, if a one-time bonus is to be applied (in case of one player building it, and then losing the city by the next ActivePlayerTurnStart). In the resources case, that wouldn't be a problem.
 
I think we can abuse the description as a priority parameter for load order to avoid such problems in client scripts, which would be useful at any rate.

There is a mechanic for this sort of thing "built in" to ModBuddy, but it remains disabled. :P

Might be interesting to allow removing listeners, to...

Or maybe just disable/enable?

alpaca said:
If an event stores data, should we save the data using SaveUtils or should we re-construct it on game load? I think the latter is preferable in this case to allow plug&play into existing saves
alpaca said:
you just run into problems if you try to hook up the event before CommonEvents is ready to receive them. Adding a new add-in type doesn't really cost anything.
You lost me a little here. CommonEvents.lua is added as an InGameUIAddin, correct? So by using the ModScript addin, you know CommonEvents is already loaded. But how does that relate to saved data and SaveUtils?
 
You lost me a little here. CommonEvents.lua is added as an InGameUIAddin, correct? So by using the ModScript addin, you know CommonEvents is already loaded. But how does that relate to saved data and SaveUtils?

Well, to implement the TechResearched event I'm storing global tables that contain information about which techs each player hasn't researched yet. I was asking whether this data should be stored in a savegame or re-generated when the game is loaded (basically, initialise by going through the whole tech tree again for each player and checking if the tech is researched). I opted for the latter, anyhow. It's safer and I guess it's not much slower than deserialization, maybe even faster.
 
I see. I'm surprised deserialization isn't faster in this case. Thanks for explaining and I like what I'm seeing.
 
I see. I'm surprised deserialization isn't faster in this case. Thanks for explaining and I like what I'm seeing.

Well actually I didn't try it. But since I store N techs for K players, I have to loop through N*K anyways (ok a bit less as time goes on for deserializing). When checking for techs I only do a couple of look-ups and one function call so it's not very expensive, and when deserializing you work a lot with strings and lua creation of new strings is fairly slow. It could be interesting to investigate this anyways, but in this case I just didn't think I would gain much by storing data in the save.
 
That makes sense. You have to do the loops anyway, so deserializing would be an extra step.

So... what's left?
 
That makes sense. You have to do the loops anyway, so deserializing would be an extra step.

So... what's left?

Everything that's not bold in the list and whatever else you can think of ;)
 
  • BuildingCreated(pCity, iBuilding)
  • BuildingGained(pCity, iBuilding)
  • BuildingDestroyed(pCity, iBuilding)
  • BuildingLost(pCity, iBuilding)
Probably just an oversight, but as I explained before, (given the reason to have separate gained/lost and created/destroyed), gained/destroyed want to get pPlayer rather than pCity.
 
I'm just considering when I should start pushing it out with my mods. Feels a bit too alpha at the moment.

What's... include("lib")?

I can't find that file anywhere.
 
I actually think BuildingGained could just as easily be a subset of BuildingCreated instead. Depends on how you think of it. Does the player gain a building that's applied to a city, or does a city gain a building that's applied to a player? If we need them both, I say leave them separate and let the common script decide.
 
Back
Top Bottom