Changing Experience to Units in City

trystero49

Prince
Joined
Apr 30, 2012
Messages
515
What event fires when a unit is built in a city? Can I use that event to check for certain buildings in the city it was built, whether the player to whom the city belongs has a certain trait, and then award XP to that unit based on the previous information? I don't know lua very well, so I am at a loss. Thanks.
 
As far as i can tell, the only event related to unit creation is Events.SerialEventUnitCreated
However, this event will trigger for any unit creation : built in city, unit upgraded, spawnrd by whatever event, even game loaded!
So you need another way to tell if the unit was actually just built. A "new unit" promotion can help, Unit.GetGameTurnCreated method can help.
 
Don't use Events.SerialEventUnitCreated for the reasons Bob noted, particularly the fact that it fires for all of your units when you load a saved game. I doubt it could be stretched to your purposes anyway though.

No useful event is fired. It'd be relatively simple for a skilled C++ coder to create an event in the DLL, but that's overkill given how it would break compatibility with many other mods.

You don't actually need that to achieve what you want to do, though. Listen for the end of turn event. When it fires, cycle through all the cities of that player. For each city, check if it has the building in question. If it does, check if it has a unit in it*, and if it does, do your logic to that unit.

* I don't know how straightforward this is but I'll bet it's possible - it might be as simple as City.GetGarrisonedUnit() or some such, but it might be as convoluted as finding the X,Y coordinates of the city and checking that hex for a unit, or even finding the X,Y coordinates for each unit owned by the owner of the city and comparing their X,Y coordinates of that unit to the X,Y coordinates of the city. You may have to get clever, but it should be possible.
 
Listen for the end of turn event.

Events.ActivePlayerTurnEnd ... which is also useless as it only fires for active (ie human) players :(
 
Events.ActivePlayerTurnEnd ... which is also useless as it only fires for active (ie human) players :(

You can get close enough that nobody will ever notice the difference by just cycling all players at the end of the human player's turn. Not an ideal solution but good enough that people won't know that it works that way.
 
Don't use Events.SerialEventUnitCreated for the reasons Bob noted, particularly the fact that it fires for all of your units when you load a saved game. I doubt it could be stretched to your purposes anyway though.

No useful event is fired. It'd be relatively simple for a skilled C++ coder to create an event in the DLL, but that's overkill given how it would break compatibility with many other mods.

You don't actually need that to achieve what you want to do, though. Listen for the end of turn event. When it fires, cycle through all the cities of that player. For each city, check if it has the building in question. If it does, check if it has a unit in it*, and if it does, do your logic to that unit.

* I don't know how straightforward this is but I'll bet it's possible - it might be as simple as City.GetGarrisonedUnit() or some such, but it might be as convoluted as finding the X,Y coordinates of the city and checking that hex for a unit, or even finding the X,Y coordinates for each unit owned by the owner of the city and comparing their X,Y coordinates of that unit to the X,Y coordinates of the city. You may have to get clever, but it should be possible.

Code:
local pPlot = pCity:Plot()
for iUnit = 0, pPlot:GetNumUnits()-1, 1 do
  local pUnit = pPlot:GetUnit(iUnit)

  -- Do something with pUnit
end
 
You can get close enough that nobody will ever notice the difference by just cycling all players at the end of the human player's turn. Not an ideal solution but good enough that people won't know that it works that way.

If you know that AI 1 doesn't need the effects before being attacked by AI 2 and that you're in a single player (ie non-hot-seat) game, that'll work ;)
 
If you know that AI 1 doesn't need the effects before being attacked by AI 2 and that you're in a single player (ie non-hot-seat) game, that'll work ;)

The game logic is:
- If unit ends turn in this city, award XP

right? If that's so, then you shouldn't generally get to spend the XP until your next turn anyway, so we just need to be sure it runs once at some point between the end of their turn and the start of their next.

You're right about single-player, though I'm sure we could figure out a clever workaround for that (like storing out the ID of the first player to take their turn, to make sure it only runs once, or potentially just running it for all AIs on their turn and leaving humans for their own turns) - I thought mods didn't work MP?
 
So, from what I understand, I'll either have to create a GameEvent in a custom .dll or use Events.ActivePlayerTurnEnd. Hrmm...well, thanks for the info. I think I might make a GameEvent for it, because it sounds like a better fix, even though you lose compatibility.
 
So, from what I understand, I'll either have to create a GameEvent in a custom .dll or use Events.ActivePlayerTurnEnd. Hrmm...well, thanks for the info. I think I might make a GameEvent for it, because it sounds like a better fix, even though you lose compatibility.

You can do it in Lua, but it's not neat. There is an event that fires at the start of every player turn, and you can use that to process the previous player - you just need to be aware of the fact that "the previous player" is not always "iPlayer-1" as they may be dead, and for player 0, you have to work out who the previous player is - certainly not player 63 as they are the barbs, probably not player 62 as they are probably a "never in play" City State. You also get the same problem with player 22 (the first CS) in finding the previous major.

Edit: and the event is

Code:
function OnPlayerDoTurn(iPlayer)
  -- print(string.format("Start turn for player %d", iPlayer))
  -- You MUST use iPlayer, do NOT use Game.GetActivePlayer()
end
GameEvents.PlayerDoTurn.Add(OnPlayerDoTurn)
 
You can do it in Lua, but it's not neat. There is an event that fires at the start of every player turn, and you can use that to process the previous player - you just need to be aware of the fact that "the previous player" is not always "iPlayer-1" as they may be dead, and for player 0, you have to work out who the previous player is - certainly not player 63 as they are the barbs, probably not player 62 as they are probably a "never in play" City State. You also get the same problem with player 22 (the first CS) in finding the previous major.
[/CODE]

So then I would have to do something like

Code:
function CheckforPreviousMajorCiv(iPlayer)
   local CurrentTurn = Game.GetGameTurn()
   local count = iPlayer
   for count, count + 1, -1 do
      if (Players[count]:IsEverAlive() && not Players[count]:IsMinorCiv()) then 
         AwardXPIfSkaldic(count, currentTurn) --this will check if the Player in question has a certain trait
         -- i.e. TRAIT_SKALDIC_VERSE, then it will check if any of the cities have a unit
         -- such that Unit.GetGameTurnCreated == CurrentTurn, then it will check some
         -- stuff in the city (if it has certain buildings), then it will reward XP based on
         -- which buildings are present.
         break
      end
      if count == 0 then
         count = GameDefines.MAX_MAJOR_CIVS - 1
         CurrentTurn = CurrentTurn - 1
      end
   end
end
GameEvents.PlayerDoTurn.Add(CheckforPreviousMajorCiv)

While I'm on this topic, then I'll post up what I think I should do next.

Code:
function AwardXPIfSkaldic(iPlayer, iCurrentTurn)
   -- Norway has skaldic verse trait
   if iPlayer:GetCivilizationType() == GameInfo.Civilizations.CIVILIZATION_NORWAY.ID then
      CheckForNewlyBuiltUnits(iPlayer, iCurrentTurn)
   end
end

Which would then call...

Code:
function CheckForNewlyBuiltUnits(iPlayer, iCurrentTurn)
   local pPlot = pCity:Plot()
   for iUnit = 0, pPlot:GetNumUnits()-1, 1 do
      local pUnit = pPlot:GetUnit(iUnit)
      --note to self, IsGarrisoned may not work.
      if pUnit:IsGarrisoned() && pUnit:GetGameTurnCreated() == iCurrentTurn then
         CheckBuildingsInCity(pUnit)
      end
   end
end
 
Just reading the code, I can several mistakes

IsAlive and IsMinorCiv are methods, so should be IsAlive() and IsMinorCiv() - as is GetGameTurn above them

&& in Lua is "and"

"--I'm just reaching here, not sure if Units[pUnit] is actually a thing"

pUnit is already the thing you want, so just pUnit:Xyz()

(And I'm pretty sure that the garrison functions all stopped working post .511 build when the feature was removed from the UI and units now just auto-garrison)

iPlayer is already a player id, so iPlayer.ID is an error, and even if it wasn't "iPlayer.ID == GameInfo.Civilizations.CIVILIZATION_NORWAY.ID" is wrong as you can't compare a player ID to a civilization ID

there may be others, and I didn't look at the logic
 
Mhhh .... :think:
What exactly is the building you want to check for ?
Might be easier to just give your civ a custom building that works like the barracks, giving free XP.

BTW, if you go the Lua way :
wiki said:
Before you invoke a method on a player, ensure that IsEverAlive is true, otherwise Civilzation V will crash. Do not use IsAlive: while it may work sometimes, it does not always prevent a crash.
(link)
 
Mhhh .... :think:
What exactly is the building you want to check for ?
Might be easier to just give your civ a custom building that works like the barracks, giving free XP.

BTW, if you go the Lua way :

I'm checking for: Monument, Ampitheatre, Operahouse, Museum, Shrine, Unique Temple (I guess I could add XP with XML to this one). I'm also checking for all World Wonders. (I haven't worked this out yet.)
 
I'm checking for: Monument, Ampitheatre, Operahouse, Museum, Shrine, Unique Temple (I guess I could add XP with XML to this one). I'm also checking for all World Wonders. (I haven't worked this out yet.)

Code:
function IsWonder(pBuilding)
  return (GameInfo.BuildingClasses[pBuilding.BuildingClass].MaxGlobalInstances == 1)
end

function AddWonders(buildings)
  for pWonder in GameInfo.Buildings() do
    if (IsWonder(pWonder)) then
      table.insert(buildings, pWonder.ID)
    end
  end
end

-- Manually add all the standard buildings to the array/table
local buildings = {
  -- Put these in the order you expect them to occur most in cities
  GameInfoTypes.BUILDING_SHRINE,
  GameInfoTypes.BUILDING_MONUMENT,
  GameInfoTypes.BUILDING_OPERA_HOUSE,
  -- etc
}

-- Now add all the World Wonders
AddWonders(buildings)

function CheckForBuilding(pCity)
  for _, iBuilding in pairs(buildings) do
    if (pCity:GetNumBuilding(iBuilding) > 0) then
      return true
    end
  end
  
  return false
end
 
Wow, thank you so much! I'm going to try and put the whole together and hopefully I can get it to work.

Just a couple questions.

-- Put these in the order you expect them to occur most in cities

Why? Minimizing runtime?

for _, iBuilding in pairs(buildings) do

I understand how for loops work, and I get that pairs is used for iterating over a table, but I don't understand why arg1 is "_".
 
Why? Minimizing runtime?
Yep, the array is being iterated from 1 to N, so no point putting the rare buildings first and the common buildings last - to quote a UK advert "Every little helps"



I understand how for loops work, and I get that pairs is used for iterating over a table, but I don't understand why arg1 is "_".

Because we don't use it (it being the index into the array) and the accepted convention for "needed but unused variables" is to call them _ (see http://www.lua.org/pil/1.3.html)
 
Ok, so I've tried putting it all together and I can't quite get it to work.

Basically I've set it up like this:
Code:
--Wonder Functions
function a
function b
--create table
table buildings
--then this runs in main
add wonders to table
--------------
--down here are functions that will do the logic
function one
function two
etc.
--finally, I set up the main function that will run on the firing of the GameEvent
function MAINSCRIPT
--do stuff
end
GameEvents.[GameEvent].Add(MAINSCRIPT)

I can provide the actual code if you like, just thought it'd be easier to ask this way.

Basically, I know the wonder functions and the buildings table work, because I set up a print() function in a for loop to print out the list of buildings...and I got all the buildings I was looking for. My problem is that I cannot get the rest to work.

Essentially, MAINSCRIPT is the only function that is added to the GameEvent, and then MAINSCRIPT does a rudimentary check and then, if that passes, it calls the logic functions. But from print() functions, I can see that the logic functions aren't being called/aren't running, because nothing is printed, and I don't understand why.

For example, in MAINSCRIPT, I would have
Code:
local variable = Game:Whatever
if variable then
   somelogicfunction()
end

but it doesn't seem that the function is being called. I get no error messages and none of my print() debugs are showing. Am I calling these functions incorrectly? Did I miss out on something? I don't think it's a matter of my conditionals not resolving the way I want them to, because I have print("error")s set up that don't print either.
 
So is the event hook being called?

What's the actual code for

Code:
--finally, I set up the main function that will run on the firing of the GameEvent
function MAINSCRIPT
[COLOR="Red"]print("In MAINSCRIPT")[/COLOR]
--do stuff
end
GameEvents.[GameEvent].Add(MAINSCRIPT)

or just put a print() where indicated and see if the events are actually being caught
 
Back
Top Bottom