1. We have added a Gift Upgrades feature that allows you to gift an account upgrade to another member, just in time for the holiday season. You can see the gift option when going to the Account Upgrades screen, or on any user profile screen.
    Dismiss Notice

[TOTPP] Prof. Garfield's Lua Code Thread

Discussion in 'Civ2 - Scenario League' started by Prof. Garfield, Apr 11, 2020.

  1. JPetroski

    JPetroski Deity

    Joined:
    Jan 24, 2011
    Messages:
    2,856
    PM sent
     
  2. Prof. Garfield

    Prof. Garfield Deity Supporter

    Joined:
    Mar 6, 2004
    Messages:
    2,826
    Location:
    Ontario
    I've started the work. Might be a few days to get it done.
     
    JPetroski likes this.
  3. JPetroski

    JPetroski Deity

    Joined:
    Jan 24, 2011
    Messages:
    2,856
    Thank you. Might be a few days before I have power!
     
  4. JPetroski

    JPetroski Deity

    Joined:
    Jan 24, 2011
    Messages:
    2,856
    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.
     
  5. Prof. Garfield

    Prof. Garfield Deity Supporter

    Joined:
    Mar 6, 2004
    Messages:
    2,826
    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).
     
  6. Prof. Garfield

    Prof. Garfield Deity Supporter

    Joined:
    Mar 6, 2004
    Messages:
    2,826
    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: Aug 15, 2020
  7. JPetroski

    JPetroski Deity

    Joined:
    Jan 24, 2011
    Messages:
    2,856
    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.
     
  8. Prof. Garfield

    Prof. Garfield Deity Supporter

    Joined:
    Mar 6, 2004
    Messages:
    2,826
    Location:
    Ontario
    My bad. I didn't upload a modified file.

    You'll also need an updated General Library.
     

    Attached Files:

  9. JPetroski

    JPetroski Deity

    Joined:
    Jan 24, 2011
    Messages:
    2,856
    This worked like a charm and under a significant stress test, I didn't notice any lag at all.
     
  10. Prof. Garfield

    Prof. Garfield Deity Supporter

    Joined:
    Mar 6, 2004
    Messages:
    2,826
    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.
     

    Attached Files:

  11. JPetroski

    JPetroski Deity

    Joined:
    Jan 24, 2011
    Messages:
    2,856
    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.
     
  12. Prof. Garfield

    Prof. Garfield Deity Supporter

    Joined:
    Mar 6, 2004
    Messages:
    2,826
    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
    
     
  13. Knighttime

    Knighttime Warlord

    Joined:
    Sep 20, 2002
    Messages:
    159
    @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: Aug 20, 2020
    Prof. Garfield and JPetroski like this.
  14. Prof. Garfield

    Prof. Garfield Deity Supporter

    Joined:
    Mar 6, 2004
    Messages:
    2,826
    Location:
    Ontario
    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.
     
  15. Knighttime

    Knighttime Warlord

    Joined:
    Sep 20, 2002
    Messages:
    159
    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.
     
  16. Prof. Garfield

    Prof. Garfield Deity Supporter

    Joined:
    Mar 6, 2004
    Messages:
    2,826
    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.
     
  17. Knighttime

    Knighttime Warlord

    Joined:
    Sep 20, 2002
    Messages:
    159
    I see... you're saying I should have tried overriding ephemeral values in onScenarioLoaded() instead of in onLoad(). I'll try that out, thanks.

    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.
     
  18. Knighttime

    Knighttime Warlord

    Joined:
    Sep 20, 2002
    Messages:
    159
    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.

    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: Aug 25, 2020
  19. JPetroski

    JPetroski Deity

    Joined:
    Jan 24, 2011
    Messages:
    2,856
    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.
     
  20. Prof. Garfield

    Prof. Garfield Deity Supporter

    Joined:
    Mar 6, 2004
    Messages:
    2,826
    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.
     

Share This Page