Adventures in Border Plots

Ja Mes

Chieftain
Joined
Dec 5, 2020
Messages
12
I've been working on a tool to keep track of hex ownership and adjacency (borders).
Suffice to say, I am in need of dire assistance.

Code:
local tPlotBorders = {}

local tIndices = {}
local NumOfIndices = 0

local iNumDirections = DirectionTypes.NUM_DIRECTION_TYPES - 1

function PlotsDoBeLikeThat()
  for i = 0, Map.GetNumPlots() - 1, 1 do
    local pPlot = Map.GetPlotByIndex(i)
    tPlotBorders[i] = {}
    tPlotBorders[i][-1] = pPlot:GetOwner()
    for iDir = 0, iNumDirections, 1 do
      local pAdjPlot = Map.PlotDirection(pPlot:GetX(), pPlot:GetY(), iDir)
      if pAdjPlot then
        tPlotBorders[i][iDir] = pAdjPlot:GetOwner()
      else
        tPlotBorders[i][iDir] = -1
      end
    end
  end
end
Events.SequenceGameInitComplete.Add(PlotsDoBeLikeThat)

function OnTileNotification(hexX, hexY, playerID, isUnknown)
local index = Map.GetPlot(hexX, hexY):GetPlotIndex()

NumOfIndices = NumOfIndices + 1

tIndices[NumOfIndices] = index
print ("NumOfIndices " .. NumOfIndices .. " Hex: " .. hexX .. " " .. hexY .. " Index: " .. index)
end
Events.SerialEventHexCultureChanged.Add(OnTileNotification)

function ProcessIndices(iPlayer)
if iPlayer == 0 then
    for k, v in pairs(tIndices) do
    print("K: " .. k .. " V: " .. v)
    print("X: " .. Map.GetPlotByIndex(v):GetX() .. " Y: " .. Map.GetPlotByIndex(v):GetY())
        if Map.GetPlotByIndex(v):GetOwner() ~= -1 then
            tPlotBorders[v] = {}
            tPlotBorders[v][-1] = Map.GetPlotByIndex(v):GetOwner()
                print("Processing Owned Indices" .. tPlotBorders[v][-1])
            for iDir = 0, iNumDirections, 1 do
                local pAdjPlot2 = Map.PlotDirection(Map.GetPlotByIndex(v):GetX(), Map.GetPlotByIndex(v):GetY(), iDir)
                if pAdjPlot2 then
                    tPlotBorders[v][iDir] = pAdjPlot2:GetOwner()
                 else
                    tPlotBorders[v][iDir] = -1
                 end
            end
        end
    tIndices[k] = nil
    print("Emptied Table")
    end
end
end
GameEvents.PlayerDoTurn.Add(ProcessIndices)


function CountBorderTiles(iPlayer)
local player = Players[iPlayer]
local PlayerIsHuman = (player:IsHuman() and player:IsTurnActive())

if PlayerIsHuman then
    for k, v in pairs(tPlotBorders) do
    if Map.GetPlotByIndex(k):GetOwner() ~= -1 then
        if Players[Map.GetPlotByIndex(k):GetOwner()]:GetCivilizationType() == GameInfoTypes["CIVILIZATION_ENGLAND"] then
                print("England")
            for i = -1, 5, 1 do
                print(v[i])
            if v[i] ~= -1 and v[i] ~= Map.GetPlotByIndex(k):GetOwner() then
                print("You Border Someone!")
            end
            end
        end
    end   
    end
end
end
GameEvents.PlayerDoTurn.Add(CountBorderTiles)

Here is the general process in theory
1. At the beginning of the game, create an array of the entire map. The keys are all of the plots (as their indices) with 7 (-1 through 5) values representing the ownership of the plot and its 6 bordering plots
2. When an plot changes ownership, it is put into an intermediary table that stores an increasing integer and the plot index
3. At the end of every turn, we go through the intermediary table and update the array, then empty the intermediary table
4. At the end of the player's turn, we go through the table of plots, check if they're owned, check if the owner is the England civilization, and then print the owners of the bordering plots
(None of this is optimized so there are some redundancies)

Yet the problem is, any hexes changed after the first turn still trigger the SerialEventHexCultureChanged (as evidenced by the print statements in my testing), yet for some reason the game prints these plots and their bordering plots as being unowned, despite the printed hex information showing otherwise. Additionally, on some occasions the Index will not be the correct one - it will output the wrong XY coordinates.

I believe the issue originates with some funkiness in the Lua tables. There isn't anything else in the mod so it's somewhere in the 100 lines.

Any help would be greatly appreciated, and all the best.
 
At a guess, the issue stems from never resetting NumOfIndices to 0 at the end of the ProcessIndices function and hence not emptying the tIndices array in one go (tIndices = {}), which has lead you to altering what you're iterating over WITHIN the iteration loop
Code:
for k, v in pairs(tIndices) do
  ...
  tIndices[k] = nil
end
which is a common mistake.

Remove the tIndices[k] = nil line and just before the closing end of the ProcessIndices function add NumOfIndices = 0 and tIndices = {}

I think there will also be an issue with using the SerialEventHexCultureChanged event as IIRC this group of events only fire IF the active player can see the affected tile.

I've not run the code, so there may be other issues
 
Well if it were me I'd have used
Code:
GameEvents.CityBoughtPlot(iPlayer, iCity, iPlotX, iPlotY, bGold, bFaithOrCulture)
as my main trigger event and built a preliminary table on game load by running through all cities of all players and looking at plots adjacent to those rather than relying on SequenceGameInitComplete or running from a PlayerDoTurn. SequenceGameInitComplete I recall only fires at game creation and so far as I recall does not fire on game reload.

Unless of course CityBoughtPlot is the event hook that has the booger in it that Firaxis copied direct from William's VMC into the game's stock DLL.

I would think using CityBoughtPlot (which fires for normal cultural expansion as well as actual plot-buying) would result in much less processing overhead than PlayerDoTurn in this particular instance.
 
Well if it were me I'd have used
Code:
GameEvents.CityBoughtPlot(iPlayer, iCity, iPlotX, iPlotY, bGold, bFaithOrCulture)
as my main trigger event and built a preliminary table on game load by running through all cities of all players and looking at plots adjacent to those rather than relying on SequenceGameInitComplete or running from a PlayerDoTurn. SequenceGameInitComplete I recall only fires at game creation and so far as I recall does not fire on game reload.

Unless of course CityBoughtPlot is the event hook that has the booger in it that Firaxis copied direct from William's VMC into the game's stock DLL.

I would think using CityBoughtPlot (which fires for normal cultural expansion as well as actual plot-buying) would result in much less processing overhead than PlayerDoTurn in this particular instance.

The only problem with CityBoughtPlot (from what I understand of what TopHat told me the other day), is that the table needs to update for any cultural update to the map, including functions that use Plot:SetOwner. From what I understood, the Serial Event is the only catch all for this compatibility. From my testing, I believe SerialEventHexCultureChanged updates for any tile updates, judging by it firing when the AI founds their first city.

As for what you suggested whoward, I implemented the fix but there are still two (three?) problems:

1. Plots updated culturally after the first turn do not get updated. They are passed through SerialEventHexCultureChanged, which prints their location (I'll get to a problem with this in 2). I know this information is passed through the intermediary table tIndices because of the print statements in that table, but it never gets updated in tPlotBorders because it doesn't recognize the plot as having an owner (and I can confirm this because of a print statement I added then removed to get the owner - after the first turn, it always returns -1). This is even weirder because in the final function,
CountBorderTiles, which iterates through tPlotBorders, detects which Plot Indices have owners (if 1 city is founded as England, it will print "England" 7 times every turn).

2. I noticed that sometimes, when founding a city, when the information is passed through SerialEventHexCultureChanged (and I have it print where the plot is) it seems to be incorrect. The Firetuner Lua Console lets me see all the cities on the map and displays the coordinate location of the city center, yet this does not match with any of the hexes printed in OnTileNotification. For example, London will be founded at (7, 7) yet the function prints that a tile was claimed at (4, 7). This leads me to the conclusion that someway, somehow, either the table tIndices is being misinterpreted by my code or Civ5 has an inconsistent coordinate system, which is frankly horrifying.

3. The line "local index = Map.GetPlot(hexX, hexY):GetPlotIndex()" has been spewing the error "attempt to index a nil value" which is, well, baffling to me. This only started after I implemented the change whoward suggested, which is even more confusing.

Here is the full updated code as of now:
Code:
print("Border Counter has loaded.")

local tPlotBorders = {}

local tIndices = {}
local NumOfIndices = 0

local iNumDirections = DirectionTypes.NUM_DIRECTION_TYPES - 1

function PlotsDoBeLikeThat()
  for i = 0, Map.GetNumPlots() - 1, 1 do
    local pPlot = Map.GetPlotByIndex(i)
    tPlotBorders[i] = {}
    tPlotBorders[i][-1] = pPlot:GetOwner()
    for iDir = 0, iNumDirections, 1 do
      local pAdjPlot = Map.PlotDirection(pPlot:GetX(), pPlot:GetY(), iDir)
      if pAdjPlot then
        tPlotBorders[i][iDir] = pAdjPlot:GetOwner()
      else
        tPlotBorders[i][iDir] = -1
      end
    end
  end
end
Events.SequenceGameInitComplete.Add(PlotsDoBeLikeThat)

function OnTileNotification(hexX, hexY, playerID, isUnknown)
local index = Map.GetPlot(hexX, hexY):GetPlotIndex()

NumOfIndices = NumOfIndices + 1

tIndices[NumOfIndices] = index
print ("NumOfIndices " .. NumOfIndices .. " Hex: " .. hexX .. " " .. hexY .. " Index: " .. index)

--[[for iDir = 0, iNumDirections, 1 do
    local pAdjPlot3 = Map.PlotDirection(hexX, hexY, iDir)
        if pAdjPlot3 then
            NumOfIndices = NumOfIndices + 1
            tIndices[NumOfIndices] = pAdjPlot3:GetPlotIndex()
        end
end]]
end
Events.SerialEventHexCultureChanged.Add(OnTileNotification)

function ProcessIndices(iPlayer)
if iPlayer == 0 then
    for k, v in pairs(tIndices) do
    print("K: " .. k .. " V: " .. v)
    print("X: " .. Map.GetPlotByIndex(v):GetX() .. " Y: " .. Map.GetPlotByIndex(v):GetY())
        if Map.GetPlotByIndex(v):GetOwner() ~= -1 then
            tPlotBorders[v] = {}
            tPlotBorders[v][-1] = Map.GetPlotByIndex(v):GetOwner()
                print("Processing Owned Indices" .. tPlotBorders[v][-1])
            for iDir = 0, iNumDirections, 1 do
                local pAdjPlot2 = Map.PlotDirection(Map.GetPlotByIndex(v):GetX(), Map.GetPlotByIndex(v):GetY(), iDir)
                if pAdjPlot2 then
                    tPlotBorders[v][iDir] = pAdjPlot2:GetOwner()
                 else
                    tPlotBorders[v][iDir] = -1
                 end
            end
        end
    end
NumOfIndices = 0
tIndices = {}
print("Emptied Table")
end
end
GameEvents.PlayerDoTurn.Add(ProcessIndices)


function CountBorderTiles(iPlayer)
local player = Players[iPlayer]
local PlayerIsHuman = (player:IsHuman() and player:IsTurnActive())

if PlayerIsHuman then
    for k, v in pairs(tPlotBorders) do
    if Map.GetPlotByIndex(k):GetOwner() ~= -1 then
        if Players[Map.GetPlotByIndex(k):GetOwner()]:GetCivilizationType() == GameInfoTypes["CIVILIZATION_ENGLAND"] then
                print("England")
            for i = -1, 5, 1 do
                print(v[i])
            if v[i] ~= -1 and v[i] ~= Map.GetPlotByIndex(k):GetOwner() then
                print("You Border Someone!")
            end
            end
        end
    end   
    end
end
end
GameEvents.PlayerDoTurn.Add(CountBorderTiles)

All the best, and an incredible thanks for all the help.

Additionally, the wonderful Chrisy on our Discord suggested the following:
"only comments I can really think of are: -the ever increasing NumOfIndices value should work fine given how pairs works, but I don't see why it can't be reset and doing so would reduce the inter-turn interactions which have to be the cause of the issue -I've always been kinda sus about removing rows from a table while you're iterating through it, and while I don't think the pointer falling out of place could cause your issue I don't know what could be really

obviously a key step will be determining how the information dissonance comes about, which'd be for you to work out given that you've got a better of what you're meaning by the statement"
 
Civ V has TWO plot index schemes, one based on a "rectangular" grid and one based on a true hex grid - make sure you're using the correct one - there are functions to convert between the two
 
Which methods convert these functions, and if you had to guess, where is it using the wrong index?
 
  1. Code:
    include("FLuaVector.lua")
    At the top of your file allows you to use the function(s)
    Code:
    ToHexFromGrid(args)
    which translates a grid (ie, plot) XY position into the Hex system's XY position.
  2. In order to use Map.GetPlot(X,Y) you need to be using Grid (ie, plot) XY position.
  3. SerialEventHexCultureChanged gives Hex XY coordinates so that the use of these coordinates directly in Map.GetPlot(X,Y) will give you bogus data for the plot.
  4. You need
    Code:
    local gridPosX, gridPosY = ToGridFromHex( hexPosX, hexPosY );
    local pPlot = Map.GetPlot( gridPosX, gridPosY );
    But I can't remember at the moment whether you need to include FLuaVector.lua in your file to make use of ToGridFromHex.
  5. This is what William was referring to as regards the coordinate system you are using and making sure it is the correct one for the correct system.
  6. The Modwiki on the lua API will almost always give you accurate info on which SerialEvent is passing HexPos instead of GridPos: http://modiki.civfanatics.com/index.php?title=Events_(Civ5_Type)
  7. I can't remember a case of a GameEvent passing any XY position as argument data that was not GridPos
-------------------------------------------------------------------------------------------------------------------

This is something brutal-force and a bit inelegant I put together for Civ6 but it works in Cvi5 as well for a double-check on whether you are getting GridPos values that make sense when looking at adjacent plots or plots at some radial distance from a starting plot in a given direction:
Code:
local AdjacentPlotDirectionCalcs = { NW = (function(X, Y) if (Y % 2 ~= 0) then return (X + 1), (Y + 1) else return X, (Y + 1) end end),
	W = (function(X, Y) return (X + 1), Y end),
	SW = (function(X, Y) if (Y % 2 ~= 0) then return (X + 1), (Y - 1) else return X, (Y - 1) end end),
	SE = (function(X, Y) if (Y % 2 == 0) then return (X - 1), (Y - 1) else return X, (Y - 1) end end),
	E = (function(X, Y) return (X - 1), Y end),
	NE = (function(X, Y) if (Y % 2 == 0) then return (X - 1), (Y + 1) else return X, (Y + 1) end end) }

--------------------------------------------------------------------------------------------------------------------------------------------------------
function GetPlotCoordsInCardinalDirectionAtRadiusR(X, Y, R, sDirection)
	if (R < 1) or (sDirection == nil) or (AdjacentPlotDirectionCalcs[sDirection] == nil) then
		return X, Y
	end
	local iX, iY = AdjacentPlotDirectionCalcs[sDirection](X, Y)
	if R > 1 then
		local iStartRadius = 1
		while iStartRadius < R do
			iX, iY = AdjacentPlotDirectionCalcs[sDirection](iX, iY)
			iStartRadius = iStartRadius + 1
		end
	end
	return iX, iY
end
And can be used in this sort of a way
Code:
local iCityX, iCityY = pCity:GetX(), pCity:GetY()
local iNewX, iNewY = GetPlotCoordsInCardinalDirectionAtRadiusR(iCityX, iCityY, 3, "NW")
local iNextX, iNextY = GetPlotCoordsInCardinalDirectionAtRadiusR(iNewX, iNewY, 4, "NE")
local pPlot = Map.GetPlot(iNextX, iNextY)
  • When in GridPos, moving one tile to the Northwest for example does not always result in a Grid X,Y change of +1,+1.
  • It depends on the starting plot's starting GridY value and whether or not that starting plot's Grid Y value is a "modulus" of 2.
  • So you may expect so see an adjacent plot's X & Y values both be higher or lower than the starting plot's value, but they may not be based on the modulus of the starting Y grid position and the direction of the adjacent plot.
 
Well, what can I say except that you two were incredibly correct. I don't know how it didn't click when I was researching and testing the Serial Event that the hex coordinates were hex coordinates and not grid coordinates. I don't know how the "hexX, hexY" in the function's description didn't tip me off right away, but all's said and done.

I implemented the fix to convert the hex coordinates into grid coordinates, and now it's working incredibly well. To finish it off, I just need to 1. Add in a very brief bit of code to update border plots when a tile updates and 2. Get everything working on reloading the game / usuability in other mods. (Soooo... expect more questions if I run into a seeming brick wall)

All the best, hopefully this turns out to be fruitful
 
SequenceGameInitComplete I recall only fires at game creation and so far as I recall does not fire on game reload.

I was curious about this (because i wouldve had to update a lot of civs if it was true :b ) and decided to run a quick test— I loaded up an existing save that had SequenceGameInitComplete included and threw in some prints. Turns out, SequenceGameInitComplete does actually fire upon reload.

(I know this is very tangential to the actual point of this thread, but I figured it was worth mentioning for reference.)
 
Well tbh you would have a better idea since I am going off multi-year-back memory nowadays, and back before 6 I never much used SequenceGameInitComplete in any script I wrote for 5. I tended to do game-session "Initialize" code from LoadScreenClose or at root scope-level of the file so that it would execute during game loading and not be apparent to the user.
 
Have you tested your code with city razing? I think you miss all the ex-city plots becoming unowned by including the following check in the ProcessIndices() function

if Map.GetPlotByIndex(v):GetOwner() ~= -1 then -- When a city is razed all the plots revert to unowned, but you'll miss this with this condition

(based on the code in your thread in the Mod Comp sub-forum)
 
Top Bottom