[TOTPP] Prof. Garfield's Lua Code Thread

JPetroski

Deity
SLeague Staff
Joined
Jan 24, 2011
Messages
4,598
Just an FYI on the canBuild modules. I ran into significant (1 second or greater) lag within the city screen as I attempted to change city production. The lag appeared when I utilized a large numbers of city locations for units buildable by all tribes. The lag appeared to be driven by the location= field (I had a field with probably 70 cities at that point "allHomeCities"). I was able to solve this by droping the location requirement for these universal units and relying on other restrictions (types of improvements don't appear to drive as much lag).

I note you've provided numerous ways to write the locations. I used the {{110,88,0},{112,86,0},{114,84,0},etc...} method. I am unsure if another method would be less problematic but it's not an easy thing to swap out and test :)

Anyway, not that big of a deal for me because I had been planning on using outdated "Caesar" canBuild methods hence most cities have an improvement that I can restrict items to, but I thought I'd mention it.
 

Prof. Garfield

Deity
Supporter
Joined
Mar 6, 2004
Messages
3,975
Location
Ontario
Thanks for mentioning that. I think I have a way to fix it by doing some post processing work on location tables. So, it wouldn't change anything for the end user.

The trouble appears to be that for every unit, every location in the table must be checked. So ~100 units and ~70 cities is something like 7000 checks. I intend to process the data so there is a table that takes the tileID and returns true if the unit can be built.

I'll fix it tonight or tomorrow. Using a function to determine if the unit is in the location or not would probably be faster (as long as you have a way to check that doesn't involve a giant list of locations).
 

Prof. Garfield

Deity
Supporter
Joined
Mar 6, 2004
Messages
3,975
Location
Ontario
Can you try this canBuild.lua module and let me know if it works and if the delay still exists? (Keep a backup just in case, I guess.)

In testing, I realized that I can't have integers be used to refer to city id numbers in the location data, since some of my code can't tell the difference between a few cities and a coordinate triple. I don't think this is a big issue, since even if someone wants to specify cities with id numbers, they can always use civ.getCity instead.

EDIT: Deleted Attachment, which was incorrect.
 
Last edited:

JPetroski

Deity
SLeague Staff
Joined
Jan 24, 2011
Messages
4,598
I stressed it out and it's still there - I'm going to PM you a copy of the scenario with the current situation so you can take a look for yourself. Perhaps it is my computer.
 

Prof. Garfield

Deity
Supporter
Joined
Mar 6, 2004
Messages
3,975
Location
Ontario
My bad. I didn't upload a modified file.

You'll also need an updated General Library.
 

Attachments

  • canBuild-and-genLib.zip
    16.3 KB · Views: 83

JPetroski

Deity
SLeague Staff
Joined
Jan 24, 2011
Messages
4,598
This worked like a charm and under a significant stress test, I didn't notice any lag at all.
 

Prof. Garfield

Deity
Supporter
Joined
Mar 6, 2004
Messages
3,975
Location
Ontario
I've updated the repository. There are some bugfixes, most notably the canBuild fix.

I made some changes to the General Library, which I've also attached to this post. @JPetroski , I added domain 3 to the function that calculates a unit's movement allowance from its remaining HP. I think you asked for that for your special forces unit.

Today I went to the trouble of filling in a large number of terrain improvement, unit orders, and unit type attributes functions, which were listed in the General Library, but not actually implemented. Stuff like placing farmland, giving a unit orders to mine, changing whether a unit can see two squares, etc. Much of this has only very minimal testing, and I relied on @Catfish 's Saved Game Format to know what the relevant values are. This means that a good deal of functionality which previously required bitwise operations is now directly available, but some bugs might crop up.

I also introduced functions to change city attributes, stuff like whether the city is in disorder or can build hydro plants. However, Catfish's document doesn't say what most of these things do. For the flags I don't know about, I just have a set of generic AttributeX functions (i.e. gen.isAttributeX(city), gen.setAttributeX(city), gen.clearAttributeX(city).

If anyone is interested in exploring city attributes and determining what they are, I've attached a script version of the General Library, which will give you access to the functions by using the load script button in the console. gen.printBits(city.attributes) will print the 32 flags (0s and 1's) with the rightmost flag being Attribute1. A city being in 'famine mode' shouldn't be too hard to find if anyone is interested.
 

Attachments

  • generalLibrary-17-Aug.zip
    14.9 KB · Views: 66
  • generalLibraryScript-17-Aug.zip
    14.9 KB · Views: 82

JPetroski

Deity
SLeague Staff
Joined
Jan 24, 2011
Messages
4,598
I have a general question about your modular system and how it might be used for scenarios that wish to have events files for different tribes. Historically, the designer would simply write multiple event files and have a batch file that would load one (ideally) before the scenario began. These event files were all different and tailored to different tribes.

This is an option I'm considering for Cold War eventually (basically, complete a "base" events file and then add to it 5-7 times), but that would create much work because any modification made to one (at least the base events) would need to be made to all.

Can your modular system be used to say, have files like:

"americanEvents"
"sovietEvents"
"chineseEvents"

and then link them with a

Code:
local americanEvents = require "americanEvents"

And then simply have all the events from americanEvents "add into" the "base" events file?

This would require a bit of planning on the designer's part as the "base" events file would need to be tribe-neutral and events that are wanted in all games, but I do think it would be a huge time saver and bug-reducer.

Is this sort of thing possible, or can you not have two types of events files using all of the same functions (like capturing cities, killing units, etc.)?

Random question but I think an interesting one.
 

Prof. Garfield

Deity
Supporter
Joined
Mar 6, 2004
Messages
3,975
Location
Ontario
You can only have one of each type of civ.scen.onTrigger(instructions) within your code. If you have more, only the last one run will be used. (I suppose you could have an event that runs the command again to replace events, but that seems very error prone when you could just use if statements.)

What you could do for each tribe is define functions for events in their own file, then add them under an if statement to your main events.

For example,
Code:
local ussr = {}
function ussr.spOnUnitKilled(loser,winner)
-- unit killed events when the player has chosen the USSR
end
return ussr

Then, in the unitKilled event

Code:
if object.tUSSR.isHuman then
    ussr.spOnUnitKilled(loser,winner)
end

In this example, you would have to have different events for the single player and multi player versions, but you could just have a single player flag and multi player flag govern that, i.e.

Code:
-- events for all versions of the game
if flag.value("SinglePlayer") and object.tUSSR.isHuman then
    ussr.spOnUnitKilled(loser,winner)
end
 

Knighttime

Prince
Joined
Sep 20, 2002
Messages
364
@JPetroski As Prof. Garfield said, the key is that each trigger -- meaning civ.scen.on___() -- should only appear once. From an organizational perspective, if you want to use a modular approach, I'd offer two suggestions or recommendations:

1. Keep all of those triggers within the top-level events.lua, never within other modules. The modules should have functions called from within triggers, not the trigger definitions directly.
2. As much as possible, try to keep events.lua "lean" by not filling it up with a ton of direct event code. Put the event code in functions within separate modules, so that events.lua only has a single line calling that function. Events.lua then becomes more of a "broker", coordinating references to the separate modules and managing the triggers and timing. (Note that I said "as much as possible" -- this is a goal, but it doesn't need to be a hard-and-fast rule.)

If I wanted to set up a single-player scenario that would mimic the previous structure of having separate events files for each playable tribe, while integrating them as seamlessly as possible, I think I'd try something like this:

Events.lua:
Code:
baseEvents = require("baseEvents")
if object.tUSA.isHuman then
    tribeEvents = require("usaEvents")
elseif object.tUSSR.isHuman then
    tribeEvents = require("ussrEvents")
-- continue in a similar manner for all other playable tribes
end

civ.scen.onActivateUnit(function(unit, source)
    baseEvents.activateUnitTrigger(unit, source)
    tribeEvents.activateUnitTrigger(unit, source)
end)
civ.scen.onCityProduction(function(city, prod)
    baseEvents.cityProductionTrigger(city, prod)
    tribeEvents.cityProductionTrigger(city, prod)
end)
-- continue in a similar manner for every civ.scen.on___() trigger that your scenario requires

As you can see, after it's initial definition, the tribeEvents variable can be referenced anywhere in the file without any checks for which tribe the human is actually playing. The key, though, is that every one of your tribe-specific events files must contain the same set of trigger-related functions. If you don't actually have any code to put there for a given playable tribe, just add an empty function declaration anyway with no code inside it.

Then baseEvents.lua would contain the code you always want to run, regardless of which tribe the human is playing. It would have definitions for activateUnitTrigger() and cityProductionTrigger() functions, along with ones like turnTrigger() or unitKilledTrigger() if you need them. This gives you one place to make updates when an event isn't affected by which tribe the human is playing.

The tribe-specific files like usaEvents.lua and ussrEvents.lua would look very similar structurally, with the same trigger functions, but of course with different contents. Remember that usaEvents.lua doesn't [only] contain events affecting the USA, it contains all events that should run only when the human is playing the USA. Also, you can add as many other functions in those files as you want/need, which are called from that set of trigger functions. Those don't have to match across all the tribe-specific events files, since they wouldn't be called directly from events.lua.

Disclaimer: I haven't tested out running code exactly like I'm proposing it here, but I'm pretty confident the approach is sound. Let me know if you have further questions, or if you try this and run into error messages!
 
Last edited:

Prof. Garfield

Deity
Supporter
Joined
Mar 6, 2004
Messages
3,975
Location
Ontario
Disclaimer: I haven't tested out running code exactly like I'm proposing it here, but I'm pretty confident the approach is sound. Let me know if you have further questions, or if you try this and run into error messages!

Very brief testing suggests that there is a problem when starting a new scenario. The events script seems run before selecting a tribe. The game does recognize a tribe as being human controlled, so this isn't a situation of Lua thinking that no tribe is human.

Code:
local tribeEvents = nil
if civ.getTribe(1).isHuman or civ.getTribe(2).isHuman or civ.getTribe(3).isHuman then
    tribeEvents = require("tribeA")
else
    tribeEvents = require("tribeB")
end

With this code, starting a scenario selected the tribeA file (I had an onScenarioLoaded message) when starting a new scenario. When loading a saved game, the human player did seem to be recognized correctly.

There's probably a way to work around this. Perhaps setting tribeEvents as an empty table in the initialization, and then replacing it with a table of functions in onScenarioLoaded, once the player tribe is known.
 

Knighttime

Prince
Joined
Sep 20, 2002
Messages
364
Thanks for testing this out and sharing a workaround idea.

I'm not sure if it matters, but just want to mention that when you start a new scenario, or when you load a saved game that was saved while playing a scenario, the civ.scen.onLoad() trigger fires first, and then the civ.scen.onScenarioLoaded() trigger also fires. At least that's what I seemed to find in my testing. I wonder if this is a TOTPP bug, that both triggers fire in both situations?

There are indeed some issues with timing that need to be watched carefully. Another odd thing that I noticed in my testing is that if you overwrite "ephemeral" values (e.g., an object field like unittype.attack) within civ.scen.onLoad(), the changes don't seem to stick. I could confirm that they were set correctly while inside that trigger, but if I queried those values immediately afterwards in onActivateUnit(), they had been reset. My conclusion was that Rules.txt gets read into memory after onLoad() runs, at least when loading saved game. To work around this, I set a "justLoaded" flag in onLoad() and then checked that flag in other triggers like onActivateUnit() and onKeyPress(), in order to overwrite ephemeral values at a slightly later point in time, and that seems to be successful.
 

Prof. Garfield

Deity
Supporter
Joined
Mar 6, 2004
Messages
3,975
Location
Ontario
In my experience, by using text boxes, the events.lua "script" is run, then the onLoad event is run, then the onScenarioLoaded event is run. If you need stuff from the rules text, or overriding the rules text, you should just be able to put it into onScenarioLoaded rather than using a flag and putting everything in an activate unit trigger. (I believe Caesar's Gallic Wars checks seasons in onScenarioLoaded). I don't think it is a bug that both events fire. I believe onScenarioLoaded is meant to duplicate an event in the macro system, while onLoad is meant to work with onSave for getting and saving the state table.

I suppose I could have suggested putting the event selection in onLoad instead of onScenarioLoaded. I don't know of a reason not to do so.
 

Knighttime

Prince
Joined
Sep 20, 2002
Messages
364
If you need stuff from the rules text, or overriding the rules text, you should just be able to put it into onScenarioLoaded rather than using a flag and putting everything in an activate unit trigger. (I believe Caesar's Gallic Wars checks seasons in onScenarioLoaded).
I see... you're saying I should have tried overriding ephemeral values in onScenarioLoaded() instead of in onLoad(). I'll try that out, thanks.

I don't think it is a bug that both events fire. I believe onScenarioLoaded is meant to duplicate an event in the macro system, while onLoad is meant to work with onSave for getting and saving the state table.
Maybe you're right. Not being super familiar with the macro language and how TNO was trying to replicate its capabilities in Lua, my perspective is that I have some events I want to run when a .SCN file is loaded (starting a new scenario), and other events I want to run when a .SAV file is loaded. So I was expecting one trigger to fire in each situation... but instead, both triggers fire in both situations, which makes them seem redundant. If I have events I want to run only when the .SCN is loaded, apparently I need to wrap them in a check for turn == 1 no matter which trigger I put them in. I'm not saying it's broken, but I still don't feel like it's intuitive.
 

Knighttime

Prince
Joined
Sep 20, 2002
Messages
364
In my experience, by using text boxes, the events.lua "script" is run, then the onLoad event is run, then the onScenarioLoaded event is run. If you need stuff from the rules text, or overriding the rules text, you should just be able to put it into onScenarioLoaded rather than using a flag and putting everything in an activate unit trigger.
I confirmed this is correct and works as you said.

What I find interesting is that if I override something from Rules.txt in onLoad(), my change is lost. I tested overriding a unittype attack value in onLoad() and then displayed that value in onScenarioLoaded(), and the value that displayed was the default value found in Rules.txt, not the one I had just set. Then I tried overriding that value in onScenarioLoaded() and displaying the value in onActivateUnit(), and this time my change was properly preserved.

I believe... onLoad is meant to work with onSave for getting and saving the state table.
Agreed, but my new takeaway is that it's best to only use onLoad() for that purpose. Any other code that the designer wants to run when a game is loaded should probably go in onScenarioLoaded() instead. I assumed that since they both ran when a game was loaded, it didn't matter where I put my code, but that's not true.
 
Last edited:

JPetroski

Deity
SLeague Staff
Joined
Jan 24, 2011
Messages
4,598
I was doing some testing on your canBuildSettings and noticed that you added "overrideDefaultBuildFunction" as an option. I had a situation where destroyers weren't being permitted to be built for some unknown reason (though I suspect it had to do with unit slot) and by adding this in, it magically appeared. I'd have to test it but I'll bet you have fixed the age old musketeer/knight dilemma for us.
 

Prof. Garfield

Deity
Supporter
Joined
Mar 6, 2004
Messages
3,975
Location
Ontario
I updated the repository.

A few highlights that I remember:

text.lua

text.money(amount)-->string
Converts an integer amount of money into a string, adding digit separators and a currency.

By default
text.money(50000) == "50,000 Gold"

text.setMoney(convertString) --> void
changes text.money
e.g.
text.setMoney("$%STRING1,000")
text.money(50000) == "$50,000,000"

canBuild.lua

Fixed a bug with forbidden locations.
Added the following three options
-- .onlyBuildCoastal = bool or nil
-- if true, the item can only be built if the city has the 'coastal' flag,
-- that is, in the default game it could build harbors and offshore platforms
-- .onlyBuildShips = bool or nil
-- if true, the item can only be built if the city has the 'ship building' flag
-- that is, in the default game it could build sea units
-- .onlyBuildHydroPlant = bool or nil
-- if true, the item can only be built if the city has the 'can build hydro plant' flag
-- that is, the city could build hydro plants in the default game

generalLibrary.lua

gen.getState()-->table
Gets the state table, so it can be used in other files without writing a "linkState" function.
(If you're writing a reusable module, I'd still recommend writing a linkState for that module, so you don't have to worry about conflicting keys.)

gen.linkState(stateTable)
Provides the state table for getState

-- gen.linkState(stateTable)
-- gen.getState()-->table

-- gen.errorForNilKey(table,tableName)-->void
After applying this function, if you try to access a nil key from the table, it will give you an error. tableName is the name of the table that you want in the error message. This means that typos in table keys will give you errors rather than simply returning nil, which should make bugfinding easier.

-- gen.noNewKey(table,tableName)-->void
After applying this function, you won't be able to add new keys to a table, but you can still change existing keys.

-- gen.noGlobal()
Applies the above two functions to the table of global variables, which will cause typos and forgotten locals to cause errors instead of manifesting in weird ways as bugs. If you need any globals (say, a console table to make functions available to the console), define them before using this function (or, use rawset). You'll need to comment this out to do work in the console if that work involves defining variables for use in the console.
 
Top Bottom