GameEvent for border expansion?

Danmacsch

Geheimekabinetsminister
Joined
Jan 14, 2014
Messages
1,316
Location
Copenhagen, Denmark
As the title indicates, I'd like to know if there by any chance exists a game event one can use to detect border expansion. I've looked through modiki and the post with the hooks added in bnw, but can't find any. Is that simply because there isn't one?

The method I contemplate to use right now is to save the number of plots the player owns, and then check if this number increases with a function that fires with both PlayerDoTurn, CityBoughtPlot and GreatPersonExpended (citadel claiming tiles). While I think this method is viable, there might be an simpler one I've overlooked.
 
PlayerDoTurn, I believe, runs after the City has already expanded its borders, so for Holo at least, her PlayerDoTurn function which scans all plots around each City each turn will pick up the new plot.

However, for what you are asking specifically, GameEvents.CityBoughtPlot will do what you need. When a City expands its borders via Culture (normally, in other words) this will fire and provide you with the coordinates of the plot that was just acquired, along with the City, I believe.

Bear in mind that the version of the code Firaxis copied from whoward contains a bug where plots purchased with Gold will trigger this event, but provide the wrong coordinates -- instead of the coordinates of the plot purchased, it returns the coordinates of the City.

I can't really comment on GreatPersonExpended, but for a Citadel claiming plots, I use BuildFinished for Holo. However, it seems to fire twice for Great Person improvements, so you may want to check if GreatPersonExpended does the same, and to account for it if it does.
 
Great, thanks for clearing that up. I thought that CityBoughtPlot only fired when you actually bought a plot via the city screen, but never tested it myself - I'll try my luck with that.
Thanks.
 
Thanks for the info. I've never used any of the serial events before - are that used the same way as game events?
 
Serial Events are tied to the graphical engine and generally are not reliable for firing for AI's, but there are exceptions. I'm not sure which ones reliably work for AI, but I suspect you can probably just test them.
 
Okay. Actually I did read about SerialEventForestRemoved in a thread a while back. In that case, it wasn't advised to use, since it only fires if the active player can see the tile where the forest is removed. So, as you say, it won't fire for the AI.
But in regards to that, I have another question. Now, I practically don't know anything about TableSaverLoader, but could one solve the forest-(or-jungle)-removal-detection by using that (possibly along with your TSL Serializer); saving the features of all plots in an area, and then, as the game progresses, check if these features are removed?
 
I do recall another thread from quite a while ago which talked about something similar -- trying to figure out when a forest was cleared.

However, it's been so long ago that I don't remember what it was, or if any solution was found.

With that said, TableSaverLoader can definitely do that, it's just a matter of what you need it for. I use it heavily in my Holo Civ to save plot data and such, which means it can easily store data for which tiles have forests and jungles on them for you.

However, considering the sheer number of plots that start out with forests and jungles on them, it has the potential to become pretty crazy in terms of resource consumption and overhead depending on what you want to do with that data, and how often. If you can think of some rules to narrow that search with it would help quite a bit.

In either case, if you do utilize TableSaverLoader, I definitely would recommend usage of my TSL Serializer component (though I may be biased there) if your mod is not a total conversion. If it's meant to be loaded alongside other mods, using TSL without the Serializer could potentially cause data loss for your, or the other mods.
 
Great - I'm reading through your TSL Serializer thread now. It's really detailed and informative :goodjob: Great work!

If I choose to have some feature of sorts based on forest/jungle removal, it will be concerning a pretty minimal amount of plots; the working area of one city. That can't be too demanding, right? Changes in the data would only have to be checked for whenever a worker removes a forest/jungle feature, i.e. finish an action. Can that be checked with a gameevent?
 
Thanks; let me know there if you have issues with the Serializer.

I don't know if there is any game event for clearing forests and jungles (I don't believe it counts as an improvement finishing or anything) but if you're only checking the working radius around specific Cities, that's much more manageable. In general though, you can simply scan those plots once per turn on PlayerDoTurn and it should be fine. It won't pick up those times where a player manually finishes clearing a forest before ending their turn, but for the most part, it will work fine.

Simply loop through a player's Cities, scan all the plots around it, and if it has a forest or jungle, add it to the table. Then scan the table to see which tiles no longer have forests on them, conduct your function, and remove that plot from the table.
 
Back to the original question. I've run into a problem, which may be something pretty simple (I would think that is the case), but for the life of me, I can't figure out what is causing it.
Here's the code:
Code:
local civilisationID = GameInfoTypes["CIVILIZATION_DMS_FAROE_ISLANDS"]

function DMS_DetectBorderExpansion(playerID, cityID, plotX, plotY, bGold, bFaithOrCulture)
	print("border expansion detected!")
	local pPlayer = Players[playerID]
	if (pPlayer and pPlayer:IsAlive() and pPlayer:GetCivilizationType() == civilisationID) then
		print("playerID is correct type.")
		local pPlot = Map.GetPlot(plotX, plotY)
		[COLOR="Red"]if (pPlot:IsWater() == true) then[/COLOR]
			print("plot is water.")
			local pCity = GetTargetCity(pPlayer, cityID)
			print("city found -> start conversion of citizen")
			DMS_ConvertCitizenToCapitalReligion(pPlayer, pCity, plotX, plotY)
		end
	end
end
if JFD_IsCivilisationActive(civilisationID) then
	GameEvents.CityBoughtPlot.Add(DMS_DetectBorderExpansion)
end
Of some reason the function stops after the print("playerID is correct type.") statement, or at least I think that is the case, since the next print statement doesn't appear in the lua console.
I've tried changing the line (marked red in the above code) to
Code:
if pPlot:IsWater() then
I'm pretty sure I've seen that coded (and working) before in some mod, which could possibly have guided me further, but I can't remember which mod. In any case it doesn't change any thing for me.

Any help is much appreciated.
 
I think if you buy the plot with gold, there is a bug wherein you get the reference for the city's plot in plotX, plotY rather than the plot that was just purchased. In that circumstance you would never get "true" for pPlot:IsWater()

The only exception for a city being on a water plot might be that one mod that allows cities on water tiles, but I think that mod may actually be creating a land tile under the city when the city is created. I've seen and heard about that mod but don't really know how it works.

[edit]
If it were me I would add an "else" clause to your if (pPlot:IsWater() == true) then and add a print statement under that "else" so you can confirm better what is actually happening:
Code:
if (pPlot:IsWater() == true) then
	print("plot is water.")
	local pCity = GetTargetCity(pPlayer, cityID)
	print("city found -> start conversion of citizen")
	DMS_ConvertCitizenToCapitalReligion(pPlayer, pCity, plotX, plotY)
else
	print("plot was not water")
	print("playerID was " .. playerID)
	print("cityID was " .. cityID)
	print("plotX was " .. plotX)
	print("plotY was " .. plotY)
	print("bGold was " .. tostring(bGold))
	print("bFaithOrCulture was " .. tostring(bFaithOrCulture))
end
This would at least allow you to see what the game is sending to that function. While trying to debug what might be wrong it might not be a bad idea to move all those print commands for the data sent down from the game out of that "else" clause and paste them in as the top lines of the function.

[edit][edit]Also, I am not sure that pPlot:IsWater() returns true for Lake tiles. There is a specific Plot:IsLake()
 
The best way to check what's going on is to add an else clause there and have it print something else.

This way, if pPlot:IsWater() returned true, it will print your thing, and if it returned false it would print something else. If it still doesn't print anything at all, then you know that you have some other issue you need to look into.

But yes, you need to let us know whether or not this happened during 'regular' border expansion via a City's Culture output, or if you attempted to purchase a plot with Gold. If you purchased it with Gold, refer to the bug I stated in this very thread, in the first response... Post #2. Haha.
 
Thanks for your answers.

I totally overlooked/forgot that you already mentioned the issue with plots bought with gold, DarkScythe. That was indeed what I was trying to do. With regular expansion by culture, I experienced no problems.
But how would one then go about achieving the desired result? That is, detecting whenever the player expands it's borders (be it by culture, gold/faith, etc.). Any ideas?
 
one possibility, if this method gives anything useful and not a lot of garbage info:
Code:
Plot:GetOwnershipDuration()
would be to look through a city's plots when the argument for bGold is true and find the one with the smallest plot ownership duration. But that method may not even work or may give bizarre results (all depends on whether Firaxis hooked it up or not).

So before you do much of anything else you could sort for true/false of bGold.
  • If false, essentially do nothing in that "if" block other than use the plotX, plotY to generate the "pPlot" information.
  • If true, then look through the city plots to determine the plot with the lowest ownship duration, and stick that info in the "pPlot" information
Then follow with the stuff for pPlot:IsWater(), etc.

This is just off the top of my head is more conceptual than "real" code:
Code:
local pPlot = Map.GetPlot(plotX, plotY)
if bGold == true then
	pPlot = ScanCityPlotsForNewestPlot(pPlayer, plotX, plotY)
end
if (pPlot:IsWater() == true) then
	print("plot is water.")
	local pCity = GetTargetCity(pPlayer, cityID)
	print("city found -> start conversion of citizen")
	DMS_ConvertCitizenToCapitalReligion(pPlayer, pCity, plotX, plotY)
else
	print("plot is not water: nothing to do here")
end
ScanCityPlotsForNewestPlot(pPlayer, plotX, plotY) would be an additional function where you handle the heavy-lifting of figuring the newest plot and returning it's pPlot value. One of the 1st things you would have to do within ScanCityPlotsForNewestPlot() is translate plotX, plotY into a city pCity sort of object so you can use methods such as pCity:GetNumCityPlots() to get a "listing" of all a city's plots and then loop through them.

All this assumes, as mentioned at the top, that Plot:GetOwnershipDuration() gives anything usable. DarkScythe may have a more direct and better way to do this since he has, ummm, a small bit more experience with manipulating plot-scanning and such. It might also be possible to use an entirely different hook event for the Plot-Buying-With-Gold that would be a little less "intuit-it" and a bit more "we can have the direct info on the plot being purchased", since any time you have to "intuit" something in the game it is prone to exception-cases causing GIGO bizzare behaviors.
 
The short answer: It's a bit convoluted.

As far as I'm aware, the Player is the only person to ever buy plots with Gold (I have no way of knowing if the AI ever does so, but at least I've never seen them suddenly own 5 tiles to cut me off of resources.)

You can check my Holo mod's Lua; I handled this plot-buying-detection handler with my CityInfoPlotUpdate() function. It is tied to Events.SerialEventCityInfoDirty() which fires every time the player views the City Info screen and something changes in there. As part of its logic, it caches the Player's Gold reserves, as well as whether or not they've visited the "Purchase Plot" screen by tracking the interface mode:
Code:
if UI.GetInterfaceMode() == InterfaceModeTypes.INTERFACEMODE_PURCHASE_PLOT then
Afterward, it checks to see whether or not the Player's Gold reserves have dropped or not, which would be a decent indication that the Player just bought a plot.

However, this method is not foolproof, as it is possible for it to be 'tricked' by users entering the Plot Purchase screen, and then buying something else.

Having said all that, the function in the public release of Holo (v2 currently) is outdated, as that code was written before the latest patch introduced the CityBoughtPlot GameEvent. I've since rewritten that code and function, although the basic logic hasn't changed very much for me. It is still pending testing in my Beta branch of development, though.

Either way, the core of the function is still the same; the only difference is that CityBoughtPlot, even if broken, does the heavy lifting of actually detecting when a plot has been purchased for you. It doesn't know which plot it is, though, which puts it in the same position as my current function, so it's simply a matter of migrating the logic over. The main benefit is that I would no longer have to track and cache the Player's Gold reserves and InterfaceMode changes.

My method also relies heavily on data persistence, because I find no completely reliable way to determine plot status from within the game itself. The method LeeS proposed is a very reasonable one which does not require the use of data persistence. However, the main problem I have with it is that (if GetOwnershipDuration works the way I think it does) I can't think of any way it can distinguish between plots which you have already calculated or not.

Because you have no way of knowing which plot was just bought, you have to run a loop to scan all the plots around the City to find plots currently owned. The above method may return to you the newest plots (those with an ownership duration of zero, or something) but let's say that in one given turn, your City's borders expand naturally, claiming one tile, and then you go and purchase another plot or two?

Even if you limit the function to firing when bGold returned true, how would it work out which of the three new plots have had effects applied to them, and which ones haven't? At worst, one plot may have affects applied to it, or at least be counted three times.

With that said, in terms of logic, once you can persist the data about each plot, it's not entirely too difficult.

Basically, on CityBoughtPlot, when bGold returns true, run a plot scan around the City for all plots owned by the Player, and of interest to you. Once you have such plots, you check whether or not:
  • The plot exists in the table persisted through game saves (for example, via TableSaverLoader)
  • The plot has already been accounted for, for whatever functions you need to run
If they do exist, then either do nothing, or whatever else with this "old" plot. If it is a new plot, then run your functions, and add an entry into the table stating that it has been accounted for. This additionally prevents plots which "overlap" other Cities from firing twice (once for each City.)

If you do anything normally-irreversible to the plot, you'll also want to set up a secondary function to scan this table every turn to prune it of plots which are no longer owned by the Player, or if the Player has lost control of this plot, and then to run functions to reverse any bonus.

Holo does this with Wheat plots, because her code directly modifies the plot's yields, and she needs to remember which plots to remove the yields for if she loses it for any reason.
 
Okay, this seems to be a bit of a complex task. I think I need to get a (much) better understanding of TableSaverLoader, and tables in general, before I can manage do work this out. I know you, DarkScythe, have made a pretty extensive guide for implementing the TSL Serializer tool, so I'll start there and revisit this thread if/when I run into problems.

A quick question though, one needs to import both Pazyryk's TableSaverLoader016.lua, as well as your TSLSerializerCoreV3.lua and TSLSerializerV3.lua of which the latter needs to be renamed, right? You've specified this in your guide, but I can't see the TableSaverLoader016.lua file present in your Holo mod - why is that?
 
He appears to have renamed TableSaverLoader016.lua to HoloTSLCore.lua in V2 of the Holo Mod. Are you looking for the file itself or for an example of how to impliment into your mod, or how to "use" the table saving and table manipulation?
 
I've added the three files to my mod and renamed the TSL Client file:

- TableSaverLoader016.lua (imported via VFS)
- TSLSerializerCoreV3.lua (imported via VFS)
- TSLSerializerV3_DMS_Faroe.lua (renamed and imported via VFS)

Then I've added the following at the beginning of my lua script:
Code:
include("TableSaverLoader016.lua")
tDMS_FaroeTable = {}
tDMS_FaroeTable.Plots = {}
tPlots = tDMS_FaroeTable.Plots
tableRoot = tDMS_FaroeTable
tableName = "tDMS_FaroePlots"
include("TSLSerializerV3_DMS_Faroe.lua")
I've also included a TableLoad at the end of the script:
Code:
TableLoad(tableRoot, tableName)
But yes, I guess it's the whole usage of table manipulation I have a hard time grasping. But it may be because I just haven't spend enough time trying to understand it yet. Pointers are welcome though.
 
Sorry for the delay; It's been busy over here.

To respond to the last few posts in order:

You are correct; My TSL Serializer component does not include a copy of Pazyryk's TableSaverLoader file. I thought about bundling it, but as it's not my script, I felt it was best to direct users to Pazyryk's original thread to get a copy -- this way, they would at least know where to look if Pazyryk ever comes back to update TSL. I could still bundle it just to make it an "all-in-one" sort of component, but I'm unsure about doing so largely because I don't feel I have permission from Pazyryk to do so.

You also have the basic instructions correct -- leave the Core file alone, and rename the other (Client) file, then throw everything into VFS.

The reason you don't see TableSaverLoader016.lua in my Holo's Lua folder is exactly as LeeS pointed out -- I have renamed my version. The reason is two-fold:
  1. This was prior to the last August update from Pazyryk, where I discovered a fairly severe conflict / error with outdated copies of TableSaverLoader.
    Specifically, I ran into issues when I tested with Vice Virtuoso's Civs which ran an (at the time) outdated version of TableSaverLoader (0.14) while I ran the latest 0.16 version. This conflict caused my mod's reliance on TSL to fail, because both versions of the file had the same filename.
  2. I notified Pazyryk of the issue back then, and he renamed the file to TableSaverLoader016.lua in an attempt to alleviate such version-conflict errors going forward, but I had already renamed my version at this point in order to properly test.
    At the same time, I have also lightly modified my version to include extra debug to fit and flow with Holo's own internal debugging, so I decided to simply leave my renamed version as-is.

Finally, your code snippet looks correct as far as I can tell, so you should have TSL properly hooked up and saving your main table (tDMS_FaroeTable) automatically now.

Table manipulation is intimidating at first, but it is really quite simple once you understand how to 'envision' their structure. At least, with our more simplified needs, it's not too bad. Things get hairy when you try to go more advanced and have tables referencing each other, though.

I'm not too sure what to offer here, though, unless you have specific questions.
 
Top Bottom