[TOTPP] Prof. Garfield's Lua Code Thread

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.
I don't really understand what you mean. In the argTable, you could store the city ID number, and from that get the city back in the nukeMessage function. Similarly, you could store the tribe's ID in the argTable, so that the victim is known even if the city changes hands. You could also use gen.getTileID and gen.getTileFromID to store the location of the attack (or, you could just store x and y coordinates as two separate keys).
 
Here is my attempt. I know I'm logging/storing things incorrectly, but just can't pinpoint how.
Spoiler :
Code:
local delay = require("delayedAction")


local function nukeMessage(argTable)
    local tribe = civ.getCurrentTribe()
    local text = require("text")
    local choice = nil
    if tribe.isHuman then
    local dialog = civ.ui.createDialog()
                dialog.title = "A Fallen City"
                dialog.width = 500
            local multiLineText = "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."
                text.addMultiLineTextToDialog(multiLineText,dialog)
                    if tribe.money >= 500 then
                    dialog:addOption("We should send them gold so that they may rebuild. (-500 gold; +attitude with victim civ)", 1)
                    end
                    if tribe.money >= 1000 then
                    dialog:addOption("How terrible! We should send hands to assist in the rebuilding efforts. (-1000 gold, +reputation with all civs)", 2)
                    end
                    dialog:addOption("This is an awful disaster, but we have our own problems at the moment. (Migrations from "..argTable.cityName..")", 3)
                choice = dialog:show()
    end
    local victimID = argTable.tribeID
    local victimCity = argTable.city
        if choice == 1 then
            tribe.money = tribe.money -500
            tribe.attitude[civ.getTribe(victimID)] = tribe.attitude[civ.getTribe(victimID)] -100
        elseif choice == 2 then
            tribe.money = tribe.money -1000
            civ.createUnit(civ.getUnitType(68),civ.getTribe(victimID),victimCity)
            civ.createUnit(civ.getUnitType(68),civ.getTribe(victimID),victimCity)
        elseif choice == 3 then
            civ.createUnit(civ.getUnitType(44),civ.getTribe(victimID),victimCity)
        end
end
delay.makeFunctionDelayable("disasterHelp",nukeMessage)


function register.onUseNuclearWeapon(unit,tile)
    if tile.city then
        for tribeID = 0,7 do
            if unit.owner.id ~= tribeID then
                delay.doNextOpportunity("disasterHelp",{cityName = tile.city.name, id=tribeID},tribeID)
            end
        end
    end
    return true
end
---&endAutoDoc

return register
 
Add extra items to the argTable:
Code:
 delay.doNextOpportunity("disasterHelp",{cityName = tile.city.name, id=tribeID, cityID = tile.city.id, victimID = tile.city.owner.id, victimTileX = tile.x, victimTileY = tile.y},tribeID)

Get the city from the argTable, and make sure the city exists and is the same city (theoretically, the city could have been razed and a new city with the same ID constructed)
Code:
local victimID = argTable.victimID
local victimCity = civ.getCity(argTable.cityID)
if victimCity and (victimCity.location.x ~= argTable.victimTileX or victimCity.location.y ~= argTable.victimTileY) then
    victimCity = nil
end

You need to create units on a tile, not a city. You should probably also handle the case where the victim city was destroyed or captured by another tribe.
Code:
            civ.createUnit(civ.getUnitType(68),civ.getTribe(victimID),victimCity.location)
            civ.createUnit(civ.getUnitType(68),civ.getTribe(victimID),victimCity.location)
Code:
civ.createUnit(civ.getUnitType(44),civ.getTribe(victimID),victimCity.location)
 
Got it, thanks Prof. I needed that extra nudge on where things went.

So now it is not registering civ.getCity... it is trying to find a number, but returning nil. Any ideas?
The parts not reliant on a city location are working (improving attitude). But I found a quirk with attitude, which maybe is already known: if, say, the attitude is set at 80, and I subtract 100, instead of going to "0" it actually will reverse and make the attitude the most negative (in the 200s). There has to be a way to just store the current attitude and return it to 0, instead of subtracting an arbitrary value, but not sure how. This is what the Eiffel Tower does, I think.

Leave it to me to make this overcomplicated.
 
The parts not reliant on a city location are working (improving attitude). But I found a quirk with attitude, which maybe is already known: if, say, the attitude is set at 80, and I subtract 100, instead of going to "0" it actually will reverse and make the attitude the most negative (in the 200s). There has to be a way to just store the current attitude and return it to 0, instead of subtracting an arbitrary value, but not sure how. This is what the Eiffel Tower does, I think.
Use math.max(0,calculationResult) to make sure results are at least 0.

Code:
local atLeastZeroVar = 100
atLeastZeroVar = math.max(0,atLeastZeroVar - 200)
Got it, thanks Prof. I needed that extra nudge on where things went.

So now it is not registering civ.getCity... it is trying to find a number, but returning nil. Any ideas?
Did you use Id instead of ID in one of the keys? Otherwise, I can't see why argTable.cityID would be nil.

Maybe try
Code:
local victimCity = civ.getTile(argTable.victimTileX,argTable.victimTileY,0).city
 
Thanks Prof. Now the effects work, but it still gives me a console error "bad argument to getTile (number expected, got nil)" -- yet, the effects that require the location are working just fine!
The only thing I can think of is that there is a typo in the argTable creation or use. It may be time to write some print statements and see what the variables actually are during the code execution.

You might find it useful to use
Code:
print(gen.tableToString(argTable))
 
I figured it out!
The "local" for victimCity, and the "if/then" statement underneath needed to be embedded in the option code itself. Thanks for your help Prof.! This works like a charm.

I have a slightly abstract question, and I know this is probably best answered on a case-by-case basis, but thought I'd inquire: what kind of code would I look at if I wanted to "save" an event from the past of a game, and re-use it later?

Here is an example (not from my mod, but just out of the blue here): say the Footmen kill the Samurai Leader. But I don't want the effect of this kill to register until the tribe researches "Ritual Beheading."
 
I have a slightly abstract question, and I know this is probably best answered on a case-by-case basis, but thought I'd inquire: what kind of code would I look at if I wanted to "save" an event from the past of a game, and re-use it later?

Here is an example (not from my mod, but just out of the blue here): say the Footmen kill the Samurai Leader. But I don't want the effect of this kill to register until the tribe researches "Ritual Beheading."
You would probably want to use a "regular" flag. Something like this:

Code:
local flag = require("flag")
flag.define("SamuraiLeaderKilled",false)

discreteEvents.onUnitKilled(function(loser,winner,aggressor,victim,loserLocation,winnerVetStatus,loserVetStatus)
    if loser.type == object.uSamuraiLeader and winner.type == object.uFootmen then
        flag.setTrue("SamuraiLeaderKilled")
    end
end)

discreteEvents.onCityProcessingComplete(function (turn, tribe)
    if flag.value("SamuraiLeaderKilled") and tribe:hasTech(object.aRitualBeheading) then
        civ.ui.text("Samurai Leader killed and Ritual Beheading researched")
    end
end)

I built the flag and counter modules quite some time before unitData/cityData/tileData, so the usage is a bit different.

Code:
flag.define(flagName,initialValue)
flag.value(flagName) --> boolean -- the current value of the flag
flag.setTrue(flagName)
flag.setFalse(flagName)
flag.toggle(flagName) -- if the flag is false, set to true; if flag is true, set to false
 
Nice! I tried this -- no error, but also, nothing happened. But I realized it must be how I've organized the savegame file I was loading.

This led me to my next conundrum. I tried starting a new game with my mod, and it is crashing ToT. Loading saved games are not. Any ideas on why this might be happening?
 
This led me to my next conundrum. I tried starting a new game with my mod, and it is crashing ToT. Loading saved games are not. Any ideas on why this might be happening?
I've never built a mod before, so I don't know why this would be. Perhaps @Knighttime has some insight.
 
@Knighttime , I would greatly appreciate any guidance! I'd also be very happy to share my events.lua file. I haven't parsed the code into the various files yet... before doing so, I may need some tips.

@Prof. Garfield , here is the code for the flags. Do you happen to see anything amiss here? No errors in console, just nothing happening. But I wonder if I need to start a new game w/these parameters for them to actually work.

Spoiler :
Code:
flag.define("SirenKilled",false)
flag.define("CharybdisKilled",false)
flag.define("MinotaurKilled",false)

-- Siren

discreteEvents.onUnitKilled(function(loser,winner,aggressor,victim,loserLocation,winnerVetStatus,loserVetStatus)
    if loser.type == unitAliases.Siren and winner.type == unitAliases.Hero or winner.type == unitAliases.HeroBoat then
        flag.setTrue("SirenKilled")
    end
end)

local SirenKilledImage = civ.ui.loadImage("Images/sirens.bmp")
discreteEvents.onCityProcessingComplete(function (turn,tribe)
    if flag.value("SirenKilled") and tribe:hasTech(techAliases.EpicPoetry) then
        if tribe.isHuman then
        local dialog = civ.ui.createDialog()
                local dialog = civ.ui.createDialog()
                dialog.title = "An Epic Tale of Sirens"
                dialog.width = 500
                dialog:addImage(SirenKilledImage)
            local multiLineText = "With the proliferation of epic poetry, stories of the feats of a great hero from our realm's past have become popular.\n^\n^A particular story of interest has been the hero's resistance against the alluring song of sirens."
                text.addMultiLineTextToDialog(multiLineText,dialog)
                    dialog:addOption("A tale for the ages, indeed. We should spread this story to the edges of the world!/n^(reputation reset with other civs)", 1)
                    dialog:addOption("Our talented poets and their stunning tales hold secrets to life itself./n^(fills current research progress; tech next turn)", 2)
                    dialog:addOption("There is truth to some of these stories; may our finest explorers find the hero's treasures plundered from these sirens./n^(+2000 gold)",3)
                choice = dialog:show()
        else
                choice = math.random(1,3)
        end
            if choice == 1 then
                local atLeastZeroVar = 100
                atLeastZeroVar = math.max(0,atLeastZeroVar - 200)
                tribe.attitude[civ.getTribe(victimID)] = math.max(0,atLeastZeroVar - 200)
                tribe.reputation[civ.getTribe(victimID)] = math.max(0,atLeastZeroVar - 200)
            elseif choice == 2 then
                tribe.researchProgress = tribe.researchProgress +2000
            elseif choice == 3 then
                tribe.money = tribe.money +1000
            end
    end
end)

-- Charybdis

discreteEvents.onUnitKilled(function(loser,winner,aggressor,victim,loserLocation,winnerVetStatus,loserVetStatus)
    if loser.type == unitAliases.Charybdis and winner.type == unitAliases.Hero or winner.type == unitAliases.HeroBoat then
        flag.setTrue("CharybdisKilled")
    end
end)

local intrepidNavy = getRandomOceanTile()
local CharybdisKilledImage = civ.ui.loadImage("Images/charybdis.bmp")
discreteEvents.onCityProcessingComplete(function (turn,tribe)
    if flag.value("CharybdisKilled") and tribe:hasTech(techAliases.EpicPoetry) then
        if tribe.isHuman then
        local dialog = civ.ui.createDialog()
                local dialog = civ.ui.createDialog()
                dialog.title = "An Epic Tale of a Sea Monster"
                dialog.width = 500
                dialog:addImage(CharybdisKilledImage)
            local multiLineText = "With the proliferation of epic poetry, stories of the feats of a great hero from our realm's past have become popular.\n^\n^A particular story of interest has been the hero's dramatic escape from a sea monster."
                text.addMultiLineTextToDialog(multiLineText,dialog)
                    dialog:addOption("A tale for the ages, indeed. We should spread this story to the edges of the world!/n^(reputation reset with other civs)", 1)
                    dialog:addOption("Our talented poets and their stunning tales hold secrets to life itself./n^(fills current research progress; tech next turn)", 2)
                    dialog:addOption("This story merits further exploration of our seas! Let us commission intrepid sailors and captains./n^(+new navy)",3)
                choice = dialog:show()
        else
                choice = math.random(1,3)
        end
            if choice == 1 then
                local atLeastZeroVar = 100
                atLeastZeroVar = math.max(0,atLeastZeroVar - 200)
                tribe.attitude[civ.getTribe(victimID)] = math.max(0,atLeastZeroVar - 200)
                tribe.reputation[civ.getTribe(victimID)] = math.max(0,atLeastZeroVar - 200)
            elseif choice == 2 then
                tribe.researchProgress = tribe.researchProgress +2000
            elseif choice == 3 then
                gen.createUnit(unitAliases.Quadrireme,civ.getCurrentTribe(),intrepidNavy,{count = 3, randomize = true, scatter = true, inCapital = false, veteran = true, homeCity = nil, overrideCanEnter = false, overrideDomain = false, overrideDefender = false})
                gen.createUnit(unitAliases.Trireme,civ.getCurrentTribe(),intrepidNavy,{count = 3, randomize = true, scatter = true, inCapital = false, veteran = true, homeCity = nil, overrideCanEnter = false, overrideDomain = false, overrideDefender = false})
                gen.createUnit(unitAliases.SiegeShip,civ.getCurrentTribe(),intrepidNavy,{count = 1, randomize = true, scatter = true, inCapital = false, veteran = true, homeCity = nil, overrideCanEnter = false, overrideDomain = false, overrideDefender = false})
                gen.createUnit(unitAliases.Liburna,civ.getCurrentTribe(),intrepidNavy,{count = 2, randomize = true, scatter = true, inCapital = false, veteran = true, homeCity = nil, overrideCanEnter = false, overrideDomain = false, overrideDefender = false})
            end
    end
end)

-- Minotaur

discreteEvents.onUnitKilled(function(loser,winner,aggressor,victim,loserLocation,winnerVetStatus,loserVetStatus)
    if loser.type == unitAliases.Minotaur and winner.type == unitAliases.Hero or winner.type == unitAliases.HeroBoat then
        flag.setTrue("MinotaurKilled")
    end
end)

local MinotaurKilledImage = civ.ui.loadImage("Images/minotaur.bmp")
discreteEvents.onCityProcessingComplete(function (turn,tribe)
    if flag.value("MinotaurKilled") and tribe:hasTech(techAliases.EpicPoetry) then
        if tribe.isHuman then
        local dialog = civ.ui.createDialog()
                local dialog = civ.ui.createDialog()
                dialog.title = "An Epic Tale of a Labyrinth"
                dialog.width = 500
                dialog:addImage(MinotaurKilledImage)
            local multiLineText = "With the proliferation of epic poetry, stories of the feats of a great hero from our realm's past have become popular.\n^\n^A particular story of interest has been the hero's struggle against a half-man, half-bull monstrosity."
                text.addMultiLineTextToDialog(multiLineText,dialog)
                    dialog:addOption("A tale for the ages, indeed. We should spread this story to the edges of the world!/n^(reputation reset with other civs)", 1)
                    dialog:addOption("Our talented poets and their stunning tales hold secrets to life itself./n^(fills current research progress; tech next turn)", 2)
                    dialog:addOption("May this story inspire a new generation of adventurous warriors for ages to come!/n^(+elite infantry)",3)
                choice = dialog:show()
        else
                choice = math.random(1,3)
        end
            if choice == 1 then
                local atLeastZeroVar = 100
                atLeastZeroVar = math.max(0,atLeastZeroVar - 200)
                tribe.attitude[civ.getTribe(victimID)] = math.max(0,atLeastZeroVar - 200)
                tribe.reputation[civ.getTribe(victimID)] = math.max(0,atLeastZeroVar - 200)
            elseif choice == 2 then
                tribe.researchProgress = tribe.researchProgress +2000
            elseif choice == 3 then
                gen.createUnit(unitAliases.EliteInfantry,civ.getCurrentTribe(),{0,0},{count = 5, randomize = false, scatter = false, inCapital = true, veteran = true, homeCity = nil, overrideCanEnter = false, overrideDomain = false, overrideDefender = false})
            end
    end
end)
 
But I wonder if I need to start a new game w/these parameters for them to actually work.
No, the flag module should just introduce them into new saved files.

Do you happen to see anything amiss here? No errors in console, just nothing happening.
I don't see why something isn't happening when you kill a siren, give a tribe Epic Poetry, and then wait for its next city processing complete phase to trigger the second part of the event. I think you should first check that the unit names are correct (using object.lua would give you an error if you ask for an item that isn't there). Next, add some print or civ.ui.text lines report the current state of variables and whether if statements are triggering, in order to figure out exactly where things are wrong.

All this said, I have noticed some issues that mean the code won't do what you expect it to do.

This needs a pair of parentheses (unless you want a hero boat to trigger the siren killed flag if it defeats any unit):
Code:
if loser.type == unitAliases.Siren and winner.type == unitAliases.Hero or winner.type == unitAliases.HeroBoat then
should be
Code:
if loser.type == unitAliases.Siren and (winner.type == unitAliases.Hero or winner.type == unitAliases.HeroBoat) then

You don't need the first two lines. I just used 'atLeastZeroVar' as an example of how to update a variable while making sure it is at least 0.
Code:
                local atLeastZeroVar = 100
                atLeastZeroVar = math.max(0,atLeastZeroVar - 200)
                tribe.attitude[civ.getTribe(victimID)] = math.max(0,atLeastZeroVar - 200)
                tribe.reputation[civ.getTribe(victimID)] = math.max(0,atLeastZeroVar - 200)
You want to do something like:
Code:
                tribe.attitude[civ.getTribe(victimID)] = math.max(0,tribe.attitude[civ.getTribe(victimID)]- 40)
                tribe.reputation[civ.getTribe(victimID)] = math.max(0,tribe.reputation[civ.getTribe(victimID)] - 50)

The Epic Poetry message/decision should be wrapped in a gen.justOnce call, unless you want it to happen every turn.

In this version of code, the unit that kills the siren and the tribe making the epic poetry decision don't have to be from the same tribe. Use the tribeData module (which works like unitData/TileData/cityData) for this. (Github tells me that this module was added to the template 3 weeks ago, so you may have to download the file if you don't already have it.)
 
This led me to my next conundrum. I tried starting a new game with my mod, and it is crashing ToT. Loading saved games are not. Any ideas on why this might be happening?
@Knighttime , I would greatly appreciate any guidance! I'd also be very happy to share my events.lua file. I haven't parsed the code into the various files yet... before doing so, I may need some tips.
At what point, exactly, does it crash? Do you get through all of the game setup dialog boxes, to the point where the "Warning: This scenario uses Lua events" dialog appears?

If it crashes before that, then your problem isn't in Events.lua or any of the Lua files, since those haven't been loaded yet. Most likely it's something in Rules.txt, but it's hard to know for sure without more information. Could it be something in the @ MOD section at the top of that file? That's one of several sections in Rules.txt that are only read at the beginning of a new game, so issues there wouldn't prevent loading a valid saved game. Does the crash message give any additional details?

If it makes it all the way to that "Warning" dialog, does it still crash if you choose the last option, "No, load old events instead"? Or does it only crash if you choose one of the first two options to load the Lua events?

Last thought: If you close Civ2 entirely, and then try to start a mod as the first thing you do when firing up the game, does it crash? What if you close Civ2 entirely, and then load a saved game first, then Quit back to the main menu but do not close Civ2, and try to Play a Mod at that point? I've noticed some inconsistent crashes that I can't always replicate, and the order of operation in what I've done previously seems to make a difference.

I'd be happy to look at your Events.lua file, although I doubt I'd find anything there that Prof. Garfield wouldn't be able to spot more quickly. If the problem isn't there, you'd need to send me more files (maybe even a .zip of your scenario directory). Feel free to PM me if you'd like.
 
If you determine that it is likely that the crash is due to Lua code, I'll be happy to have a look.
 
Last thought: If you close Civ2 entirely, and then try to start a mod as the first thing you do when firing up the game, does it crash? What if you close Civ2 entirely, and then load a saved game first, then Quit back to the main menu but do not close Civ2, and try to Play a Mod at that point? I've noticed some inconsistent crashes that I can't always replicate, and the order of operation in what I've done previously seems to make a difference.
I'm pretty sure some data are not initialized back when leaving a game for main menu or another loaded game, and that these may cause issues.
Per exemple, civ colors ?
 
Last edited:
Thank you all, very much appreciated. And apologies for not providing more info on the crash. I did some tinkering this morning, and this is what I can report:

Knighttime said:
At what point, exactly, does it crash? Do you get through all of the game setup dialog boxes, to the point where the "Warning: This scenario uses Lua events" dialog appears?

If I select "Yes, load lua events," the game crashes. If I select, "No," the game loads, but a small message comes up along the lines of "error in events.txt file."

Knighttime said:
If it crashes before that, then your problem isn't in Events.lua or any of the Lua files, since those haven't been loaded yet. Most likely it's something in Rules.txt, but it's hard to know for sure without more information. Could it be something in the @ MOD section at the top of that file? That's one of several sections in Rules.txt that are only read at the beginning of a new game, so issues there wouldn't prevent loading a valid saved game. Does the crash message give any additional details?

In the past when I have a rules.txt issue, the game doesn't even load at all. The main reason though why I think that the rules.txt is OK is that I (don't think) I've modified it in the last few days.

This crash causes total OS pause-up, in contrast to a Rules.txt crash which is more immediate. It "freezes" before it crashes, too -- and for a while. It requires a force exit. "The program is not responding." So I suppose crash is also not the right word to be using!

Knighttime said:
If it makes it all the way to that "Warning" dialog, does it still crash if you choose the last option, "No, load old events instead"? Or does it only crash if you choose one of the first two options to load the Lua events?

Exactly! Only if I select the first 2. But selecting the 3rd brings up an error message in-game.

Knighttime said:
Last thought: If you close Civ2 entirely, and then try to start a mod as the first thing you do when firing up the game, does it crash? What if you close Civ2 entirely, and then load a saved game first, then Quit back to the main menu but do not close Civ2, and try to Play a Mod at that point? I've noticed some inconsistent crashes that I can't always replicate, and the order of operation in what I've done previously seems to make a difference.

This is interesting. I tried loading the save, and then doing this, but it still froze up.

I've been using saves to log my changes to events.lua, and so it is likely I changed something that impacts the beginning of the game? I can't tell. I went back and sleuthed my changes, but couldn't find anything. Tried taking some things out, but no luck other than removing it entirely.
 
If I select "Yes, load lua events," the game crashes.
This crash causes total OS pause-up, in contrast to a Rules.txt crash which is more immediate. It "freezes" before it crashes, too -- and for a while. It requires a force exit. "The program is not responding." So I suppose crash is also not the right word to be using!
OK, send me a copy of the entire mod folder and I'll have a look.
 
For anyone else following along, the cause of the freeze was an infinite loop. I discovered the approximate location by introducing lines like this
Code:
civ.ui.text("line XYZA")
to figure out how far in the code the Lua Interpreter got.

The code was a modification of code that I suggested before, except that this time, there was no valid tile to return, since the game doesn't generate terrain 11-15.
Code:
local function getRandomVolcano()
    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 and tile.baseTerrain.type == 15
    return tile
end
Here is an infinite loop proof version of the code
Code:
local function getRandomVolcano()
    local tile = nil
    local width,height,maps = civ.getAtlasDimensions()
    for i = 1, 1000 do
        local xVal = math.random(0,width-1)
        local yVal = math.random(0,height-1)
        tile = civ.getTile(xVal,yVal,0)
        if tile and tile.baseTerrain.type == 15 then
            return tile
        end
    end
    error("No volcano found")
end

I can't remember the reason I didn't suggest this version of code before, since I know that I've used it at least once.
 
Back
Top Bottom