[TOTPP] Prof. Garfield's Lua Code Thread

OK, thank you. This is making more sense now!
How can I register a simple object, like ensure that the "nukeMessage" (and any options I include in there) only come up for the human player?
I've tried registering tribe = civ.getCurrentTribe(), and then later in the function saying, if tribe.isHuman, but that doesn't seem to be working.
 
How can I register a simple object, like ensure that the "nukeMessage" (and any options I include in there) only come up for the human player?
I've tried registering tribe = civ.getCurrentTribe(), and then later in the function saying, if tribe.isHuman, but that doesn't seem to be working.
Are you sure you specified the 3rd argument when calling delayedAction.doNextOpportunity? If you didn't, then it defaults to the onTurn event, and I think tribe 7 is the result of civ.getCurrentTribe() during the onTurn event.

If that doesn't work, you could specify the tribe in the argTable. That is, {cityName=tile.city.name, id=tribeID}, and get tribe from civ.getTribe(argTable.id).
 
I could only get the delay to work by identifying in onUseNuclearWeapon, "local delay = require," and instead of "delayedAction," just writing "delay."

Yet no matter what I do, the message comes up for every tribe for the human player. I think the tricky part is that I can't identify a specific tribe, as the human player could be any 1-7 when playing a mod. So I'm trying to identify the human player.

Essentially, trying to make it so when an AI city is attacked by a nuke, the player has a chance to help that civ out.
 
So, something like this doesn't work?
Code:
local function nukeMessage(argTable)
    local tribe = civ.getCurrentTribe()
    if tribe.isHuman then
        civ.ui.text("Nuclear attack at "..argTable.cityName.."!")
    end
end
Nor does something like this?
Code:
local function nukeMessage(argTable)
    local tribe = civ.getTribe(argTable.id)
    if tribe.isHuman then
        civ.ui.text("Nuclear attack at "..argTable.cityName.."!")
    end
end
delay.makeFunctionDelayable("sampleDelayedFunction",nukeMessage)

function register.onUseNuclearWeapon(unit,tile)
    if tile.city then
        for tribeID = 1,7 do
            if unit.owner.id ~= tribeID then
                delay.doNextOpportunity("sampleDelayedFunction",{cityName = tile.city.name,id=tribeID},tribeID)
            end
        end
    end
    return true
end
Can you post your current code then?
 
Thanks Prof. Neither of those are working for me. This is in the onUseNuclearWeapon.lua file. Is it in the right place?
Spoiler :
Code:
local delay = require("delayedAction")

local function nukeMessage(argTable)
    local tribe = civ.getTribe(argTable.id)
    if tribe.isHuman then
          civ.ui.text("A terrible disaster has befallen "..argTable.cityName.."! Some believe marauders and riots are to blame; others believe the Gods have exacted vengeance. Dreadful suffering has been reported and the city is in ruins.")
    end
end
delay.makeFunctionDelayable("disasterHelp",nukeMessage)

function register.onUseNuclearWeapon(unit,tile)
    if tile.city then
        for tribeID = 1,7 do
            if unit.owner.id ~= tribeID then
                delay.doNextOpportunity("disasterHelp",{cityName = tile.city.name},tribeID)
            end
        end
    end
    return true
end
 
There is a mistake in the template. There are 2 files called onUseNuclearWeapon.lua, and the one actually getting registered is in the MechanicsFiles folder. Delete one of the files and try again. I thought you were having a problem where you couldn't distinguish between human and AI tribes, not that there was no event at all.

This code worked in a quick test once I got rid of the duplicate file.
Code:
local delay = require("delayedAction")

local function nukeMessage(argTable)
    local tribe = civ.getTribe(argTable.id)
    civ.ui.text(tostring(tribe))
    if tribe.isHuman then
          civ.ui.text("A terrible disaster has befallen "..argTable.cityName.."! Some believe marauders and riots are to blame; others believe the Gods have exacted vengeance. Dreadful suffering has been reported and the city is in ruins.")
    end
end
delay.makeFunctionDelayable("disasterHelp",nukeMessage)

function register.onUseNuclearWeapon(unit,tile)
    civ.ui.text(tostring(tile.city))
    if tile.city then
        for tribeID = 1,7 do
            if unit.owner.id ~= tribeID then
                delay.doNextOpportunity("disasterHelp",{cityName = tile.city.name, id=tribeID},tribeID)
            end
        end
    end
    return true
end
 
Hmmm, still not working for me. I tried both files, too (tried removing 1 and then the other). It still says bad argument for tribe, and now the dialogue that comes up also points out an X,Y coordinate and seems to have some kind of error (does not show the civ.ui.text).
 
delay.doNextOpportunity("disasterHelp",{cityName = tile.city.name},tribeID)
change this to
Code:
delay.doNextOpportunity("disasterHelp",{cityName = tile.city.name, id=tribeID},tribeID)
or
Code:
local tribe = civ.getTribe(argTable.id)
to
Code:
local tribe = civ.getCurrentTribe()

If neither of these work, copy the error or post a screenshot of it.
 
error.png


Still no luck. No error in the console, but this is what I'm getting (with both changes above).

On another note, I have started getting complete game crashes -- the screen freezes up, and the program has to force quit. I think it has to do with another function I put in my events.lua. But not sure. It happens 1/10 times, I'd say. It's puzzling!
 
Still no luck. No error in the console, but this is what I'm getting (with both changes above).
Oh, looks like I left a couple of diagnostic lines in the code I supplied, since I was trying to figure out whether the code was executing at all.
Remove these lines:
Code:
civ.ui.text(tostring(tribe))
civ.ui.text(tostring(tile.city))
On another note, I have started getting complete game crashes -- the screen freezes up, and the program has to force quit. I think it has to do with another function I put in my events.lua. But not sure. It happens 1/10 times, I'd say. It's puzzling!
Maybe you have an infinite (or at least really long to execute) loop under certain conditions. I'd need more information to suggest any kind of diagnosis.
 
OK! That fixed the error... but now nothing is coming up (no dialog or text). I'm using the one in EventsFiles, not Mechanics. Would that make a difference?

Maybe you have an infinite (or at least really long to execute) loop under certain conditions. I'd need more information to suggest any kind of diagnosis.

You are exactly right. I think this was the issue. I removed the function causing it. Phew! Here is what I was doing:
Spoiler :
Code:
-- SERVILE WAR

local function isInHumanCityRadius(tile,tribe)
    for _,nearbyTile in pairs(gen.cityRadiusTiles(tile)) do
        if tile.city and tile.city.owner.isHuman then
            return true
        end
    end
    return false
    end
    
local function getRandomCityRadiusTile()
    local tile = nil
    local width,height,maps = civ.getAtlasDimensions()
    repeat
        local xVal = math.random(0,width-1)
        local yVal = math.random(0,height-1)
        tile = civ.getTile(xVal,yVal,0)
    until tile == isInHumanCityRadius
    return tile
end




local servileWarSpot = getRandomCityRadiusTile()
local servileWarTurn = servileWarTiming[math.random()]    
discreteEvents.performOnTribeTurnEnd(function(turn,tribe)
if turn == 3 and tribe.isHuman then
    civ.createUnit(unitAliases.Gladiator,civ.getTribe(0),servileWarSpot)
    end
end)

So I think the radius tile is looping on itself and caused a crash? I'm trying to register a random human tribe city radius tile, so that barb units or a group of units would appear there, if defender = nil (I tried putting that in there, too).
 
until tile == isInHumanCityRadius
This will never be true, so the loop will never stop. isInHumanCityRadius is a function, which is never a tile object. Use this
Code:
until isInHumanCityRadius(tile)
I don't know why isInHumanCityRadius has a tribe argument, since it doesn't do anything.
OK! That fixed the error... but now nothing is coming up (no dialog or text). I'm using the one in EventsFiles, not Mechanics. Would that make a difference?
I don't see why it would matter which file you use, as long as the other file is deleted (or renamed).

Please post the code for me to examine what might be missing.
 
Ah, at some point I had tribe = civ.getCurrentTribe() in that code, but had since removed it to the isHuman piece. That makes sense that a loop was happening. Thank you! No crashes. It still can't find a tile object from that, so I must not be providing adequate search conditions for those city radii tiles.

Here is the nuke code, by the way. Thanks for looking, Prof!
Code:
local delay = require("delayedAction")

local function nukeMessage(argTable)
    local tribe = civ.getCurrentTribe()
    if tribe.isHuman then
          civ.ui.text("A terrible disaster has befallen "..argTable.cityName.."! Some believe marauders and riots are to blame; others believe the Gods have exacted vengeance. Dreadful suffering has been reported and the city is in ruins.")
    end
end
delay.makeFunctionDelayable("disasterHelp",nukeMessage)

function register.onUseNuclearWeapon(unit,tile)
    if tile.city then
        for tribeID = 1,7 do
            if unit.owner.id ~= tribeID then
                delay.doNextOpportunity("disasterHelp",{cityName = tile.city.name, id=tribeID},tribeID)
            end
        end
    end
    return true
end
 
Ah, at some point I had tribe = civ.getCurrentTribe() in that code, but had since removed it to the isHuman piece. That makes sense that a loop was happening. Thank you! No crashes. It still can't find a tile object from that, so I must not be providing adequate search conditions for those city radii tiles.

Here is the nuke code, by the way. Thanks for looking, Prof!
Is the player that is launching the nuke also the human player that is expecting to see the disaster message? The code doesn't send a message to that player, due to this line:
Code:
if unit.owner.id ~= tribeID then
 
OK, I figured it out, based on that note. After the last code edit, I was trying to get barbarians to launch the attack. I changed that to "0,7" -- (does that even matter?) -- but more importantly, I noticed that barbarians are only attacking other units with the nukes, not cities, because I was testing at too early of a part of the mod. I advanced a city, added a lot of valuable units, etc., and at that point, the disaster struck an AI city, and the message came up.

I also changed reputation and created a vendetta against 2 civs, just for good measure, and the message came up on an attack on a city then.

Thank you Prof.!

I was wondering if you would give me general tips on how to call on those city radius tiles that fall only in a human city. The code I put in can't locate a tile object. I was able to do it with terrainTypes no problem, but not just general tiles in city radius. Maybe I need to specify something like if tile.terrainType == 0,9 or similar?
 
I changed that to "0,7" -- (does that even matter?)
I probably thought that the barbarian tribe didn't need a chance to get a message, or make some choice.

I was wondering if you would give me general tips on how to call on those city radius tiles that fall only in a human city. The code I put in can't locate a tile object. I was able to do it with terrainTypes no problem, but not just general tiles in city radius. Maybe I need to specify something like if tile.terrainType == 0,9 or similar?
I strongly suggest not using tile.terrainType, because the byte the number represents also has bits that represent whether the tile has a river or if a resource is currently being animated on the tile. tile.baseTerrain.type is better. You may see old code using tile.terrainType % 16 == 10, for example to check for ocean terrain, where the % 16 serves to remove the other bits from the number.

The % is the 'modulo' operator. x % y means divide x by y, and return the remainder. So, 25 % 16 = 9, for example. (25/16 = 1, with remainder 9)

Code:
-- SERVILE WAR

local function isInHumanCityRadius(tile,tribe)
    for _,nearbyTile in pairs(gen.cityRadiusTiles(tile)) do
        if tile.city and tile.city.owner.isHuman then
            return true
        end
    end
    return false
    end
    
local function getRandomCityRadiusTile()
    local tile = nil
    local width,height,maps = civ.getAtlasDimensions()
    repeat
        local xVal = math.random(0,width-1)
        local yVal = math.random(0,height-1)
        tile = civ.getTile(xVal,yVal,0)
    until tile == isInHumanCityRadius
    return tile
end

local servileWarSpot = getRandomCityRadiusTile()
local servileWarTurn = servileWarTiming[math.random()]    
discreteEvents.performOnTribeTurnEnd(function(turn,tribe)
if turn == 3 and tribe.isHuman then
    civ.createUnit(unitAliases.Gladiator,civ.getTribe(0),servileWarSpot)
    end
end)

Taking a look at this code, I notice a few things.

Code:
        tile = civ.getTile(xVal,yVal,0)
    until tile == isInHumanCityRadius
should be something like
Code:
        tile = civ.getTile(xVal,yVal,0)
until tile and isInHumanCityRadius(tile)
Tile coordinates are only valid if the x and y coordinates are both even or both odd. You can check this by
Code:
x % 2 == y % 2
but an invalid tile will just be nil.

Code:
local servileWarSpot = getRandomCityRadiusTile()
local servileWarTurn = servileWarTiming[math.random()]
These locations are being generated when you load the scenario from a saved game, not at the time of the actual event. This is probably not what you want. For example, if you load a game and play 5 turns, any cities you founded during those 5 turns will not be a possible servile war spot. However, if you save and load the game, then they will be.

In any case, this is probably not the best way to find a city radius tile owned by a human player. This code will choose a random human city, then a random tile from within that city's radius.
Code:
local function chooseHumanCity()
    local choiceList = {}
    local index = 1
    for city in civ.iterateCities() do
        if city.owner.isHuman then
            choiceList[index] = city
            index = index + 1
        end
    end
    return choiceList[math.random(1,index-1)]
end

local function randomCityRadiusTile(city)
    local tileList = gen.cityRadiusTiles(city)
    tileList[21] = nil -- don't want the city tile itself
    gen.makeArrayOneToN(tileList) -- the city might be on the edge of a map, so there might be gaps in the array
    return tileList[math.random(#tileList)]
end

local function randomTileNearHumanCity()
    local city = chooseHumanCity()
    if city then
        return randomCityRadiusTile(city)
    end
    return nil -- this is the case where there are no human cities
end
This is slightly different than the choose a tile and check if it is in a city radius system. In this system, tiles shared between 2 human cities have increased weight, as do tiles belonging to cities on the edge of the map. Unless most of the tiles are owned by human cities, this will be a much faster way of selecting tiles.
 
Fascinating. And this makes a lot of sense as to why, also, some of my other features were glitching in other ways. Your explanation about loading a game vs continuing a game, and the events happening from that point onward, is very clear. I'll have to do a bug-hunting operation for similar instances.

This is a bigger question, but at some point I'd love to pick your brain on the gen.makeArrayOneToN function and re-indexing more generally.

I'll give this code a shot and report back. I also haven't posted screenshots in quite some time. Will try to get some up tomorrow to show these things in action. Thanks again for all your help, always!
 
This is a bigger question, but at some point I'd love to pick your brain on the gen.makeArrayOneToN function and re-indexing more generally.

Here's an analogy to understand tables.

Imagine a giant warehouse with lots of shelves to put boxes on, and each of these shelves has a name. At the front of the warehouse, there are some storage lockers. Every function is assigned its own set of lockers (the local variables) that only it can use, but everyone can look inside (or even remove) the boxes on the shelves. You can put 'values' like numbers, strings, unitObjects, etc. into lockers or boxes, one in each locker or box. Crucially, one of the values you can put in a box/locker is the name of a shelf.

If a function has the name of a shelf, it can do things to that shelf, like adding boxes, changing the contents of existing boxes, or removing boxes. The table constructor instruction {} can be thought of as the program finding an empty shelf, and copying its name (and placing a sticky note telling other programs to get a different empty shelf).

For values like numbers and strings, if I copy them in order to place them in a different locker or box, I'm copying the entire thing. If I change the original, the copy doesn't change. However, in the case of tables (our "shelves" in the above analogy), only the name is copied, so changing the table in one place effectively changes it everywhere. This is also true for stuff like unitObjects or baseTerrainObjects. If you want a completely new table which has the same structure as the existing one, gen.copyTable can be used (I won't link, since the documentation is wrong at the moment)
Code:
local newCopy = gen.copyTable(originalTable)

Since changing a table changes it everywhere, I can call gen.makeArrayOneToN on a table in order to re-index the integer keys, and I don't have to update the variable containing the table, like I would if I were updating a string or a number. The variable only contains the name of the table, and, since the function has changed the table with that name, the variable is still good as is.
 
I ran into a slight snag with the nuke code from earlier. As far as I can tell, the "victim" tribe isn't being IDed until the 2nd piece of code, which is essential to be second. How can I ID the victim in the first chunk of code? I tried just flipping the two, and it still worked, but I can't seem to find how to ID that victim or the city location.
 
Back
Top Bottom