Bay 12 Games Forum

Please login or register.

Login with username, password and session length
Advanced search  
Pages: 1 ... 225 226 [227] 228 229 ... 243

Author Topic: DFHack 50.12-r3  (Read 807612 times)

myk

  • Bay Watcher
    • View Profile
Re: DFHack 0.47.05-r8 | 50.07-beta1 (pre-release)
« Reply #3390 on: April 01, 2023, 12:34:08 am »

Thanks! The goal is to make scripted modding more accessible to modders and to make installation (and update) of these mods painless for players. I hope to make the process even smoother for the next DFHack version -- right now mods need to be marked "active" at least once for DFHack to pick them up. This doesn't really make sense for script-only mods that have no effect on worldgen. By the next version of DFHack, just subscribing to those mods in Steam Workshop should be enough for DFHack to find them and make them available in a running game.
Logged

Rumrusher

  • Bay Watcher
  • current project : searching...
    • View Profile
Re: DFHack 0.47.05-r8 | 50.07-beta2 (pre-release)
« Reply #3391 on: April 03, 2023, 10:36:56 pm »



Code: ("ann.lua") [Select]
--proof of concept of an announcement menu.

local dlg=require("gui.dialogs")

local JUNK={}
local Dang=df.global.world.status.announcements

  for ultimate,checked_Ann in pairs(Dang) do
local mono=checked_Ann.text

      table.insert(JUNK,{mono,search_key = mono:lower()})
end
 
 dlg.showListPrompt("Announcement list","scroll through the list of Announced alerts",COLOR_WHITE,JUNK,nil,nil,nil,nil,true)

ok so out of boredom I decided to write up a script that grabs all the alerts/reports and shoves them into a list... there was a previous version of this script that grab every report but some folks don't want to sift through combat logs so I made this instead. there was an attempt at adding a filter on it but shrugs.
Logged
I thought I would I had never hear my daughter's escapades from some boy...
DAMN YOU RUMRUSHER!!!!!!!!
"body swapping and YOU!"
Adventure in baby making!Adv Homes

0x517A5D

  • Bay Watcher
  • Hex Editor‬‬
    • View Profile
Re: DFHack 0.47.05-r8 | 50.07-beta2 (pre-release)
« Reply #3392 on: April 06, 2023, 03:33:10 pm »

So, scripts included in mods.

I can only get them to auto-run if I make them into modules:
Code: (example_mod.lua) [Select]
--@ module = true

if not dfhack_flags.module then
    print("data/installed_mods/example_mod (100)/scripts_modactive/example_mod.lua ran.")
else
    print("data/installed_mods/example_mod (100)/scripts_modactive/example_mod.lua ran as a module.")
end

This will log the 'ran as a module' line the first time a world is loaded.  (One that uses the mod, of course.)

But only the first time.  If I exit back to the startup menu, and reload the world, the script doesn't run again.

Is this expected behavior?  Do I need to figure out how to make hooks/callbacks for the module?

Is there any (implemented or planned) support for onLoad.init and friends?  I couldn't make it work.
Logged

myk

  • Bay Watcher
    • View Profile
Re: DFHack 0.47.05-r8 | 50.07-beta2 (pre-release)
« Reply #3393 on: April 06, 2023, 03:59:51 pm »

To run code on world load/unload, you should add a hook to  dfhack.onStateChange. There's a full example you can follow in the DFHack modding guide: https://docs.dfhack.org/en/latest/docs/guides/modding-guide.html#putting-it-all-together

The script itself only "runs" once right now because of caching. That's usually the correct behavior, but I should actually change that in this case so active mods can completely remove their hooks on world unload and still have a chance to recreate them for the next world load. Active mods shouldn't let their state change hooks be permanent since otherwise they might load into worlds where they're not active.
Logged

xzaxza

  • Bay Watcher
    • View Profile
Re: DFHack 0.47.05-r8 | 50.07-beta2 (pre-release)
« Reply #3394 on: April 07, 2023, 05:32:40 am »

Hi,

I'm not a lua expert, so when I'm making dfhack scripts I run into the occasional problem. Like, when I made a script to list links of a workshop, I figured I could make it more complete and include stockpiles too. But for workshops, links are apparently stored in building.profile.links, while for stockpiles they're at building.links. Then this might happen:

...47.05-r5/df_47_05_linux/hack/scripts/cust/links_test.lua:47: Cannot read field building_stockpilest.profile: not found.

I've managed to work around that this time, but my actual question is: is there a sane way to test if a field exists? That is, in such a way that doesn't cause an error.

Also, sort-items doesn't work with building materials, so I've been kinda thinking about making something that'd allow searching for a specific material or furniture from the menu. Or has it been done? Also, I've been missing similar functionality for display furniture, because it's a pain to find an item there. Or does that exist already?
« Last Edit: April 07, 2023, 06:17:08 am by xzaxza »
Logged
Known issues
You may get a dwarf that likes bugged stockpiles.

myk

  • Bay Watcher
    • View Profile
Re: DFHack 0.47.05-r8 | 50.07-beta2 (pre-release)
« Reply #3395 on: April 07, 2023, 03:00:39 pm »

In general, you can protect yourself from errors with pcall. For example:
Code: [Select]
local ok, ret = pcall(function()
    -- do something sketchy
end)

but in this case, you'd probably be best off with testing the type of the pointer before trying to access fields that only exist in some types.

Code: [Select]
local bld = thebuidlinginquestion
if bld:getType() == df.building_type.Workshop then
    -- access workshop fields
elseif bld::getType() == df.building_type.Stockpile then
    -- access stockpile fields
end
Logged

0x517A5D

  • Bay Watcher
  • Hex Editor‬‬
    • View Profile
Re: DFHack 0.47.05-r8 | 50.07-beta2 (pre-release)
« Reply #3396 on: April 11, 2023, 11:06:53 am »

Is this the proper way to do a mod's auto-run module?  Are there best practices that I'm not following?



Code: (info.txt) [Select]
[ID:lighter_green_glass]
[NUMERIC_VERSION:100]
[DISPLAYED_VERSION:1.00]
[EARLIEST_COMPATIBLE_NUMERIC_VERSION:100]
[EARLIEST_COMPATIBLE_DISPLAYED_VERSION:1.00]
[AUTHOR:0x517A5D]
[NAME:Lighter Green Glass Items]
[DESCRIPTION:Reduces the density of green glass, which reduces the weight of green glass items.  Requires DFHack.]



Code: (lighter_green_glass.lua) [Select]
--@ module = true
local utils = require('utils')

local mod_ID = 'lighter_green_glass' -- ID of the mod, per init.txt.
local script_name = 'lighter_green_glass' -- name of this script, without the .lua extension.
local GLOBAL_KEY = script_name -- unique key for the state-change callback.

if not dfhack_flags.module then
    print(string.format("The %s script is auto-executed when the %s mod is loaded;", script_name, mod_ID))
    print('It is not intended to be invoked directly.  No action taken.')
    return
end

---- define locals.  Note: only initialize locals that will never change, even on world reload.

local gg_original_density = df.global.world.raws.mat_table.builtin.GLASS_GREEN.solid_density

---- implementation code

local function activate_mod_raws_changes()
    df.global.world.raws.mat_table.builtin.GLASS_GREEN.solid_density = math.floor(gg_original_density/5)
end

local function deactivate_mod_raws_changes()
    df.global.world.raws.mat_table.builtin.GLASS_GREEN.solid_density = gg_original_density
end

local function mod_is_loaded()
    -- df.global.world.object_loader.object_loader_order_id[] is an array of strings.
    -- search it for the ID of our mod.  if it exists, the mod is loaded.
    return(utils.linear_index(df.global.world.object_loader.object_load_order_id, mod_ID, 'value') ~= nil)
end

---- install hook for callbacks.

if dfhack_flags.module then
    dfhack.onStateChange[GLOBAL_KEY] = function(event)
if event == SC_WORLD_LOADED and mod_is_loaded() then
            activate_mod_raws_changes()
elseif event == SC_WORLD_UNLOADED and mod_is_loaded() then
            deactivate_mod_raws_changes()
end
    end
end



This mod doesn't need any user interaction.  I tried to get it to load as an internal module, but was not successful.  Here's a list of the directories I tried:
  • lighter_green_glass (100)\internal
  • lighter_green_glass (100)\internal\scripts_modactive
  • lighter_green_glass (100)\scripts_modactive\internal
  • lighter_green_glass (100)\scripts_modactive\internal\lighter_green_glass
  • lighter_green_glass (100)\scripts_modactive\internal\scripts_modactive
Logged

myk

  • Bay Watcher
    • View Profile
Re: DFHack 0.47.05-r8 | 50.07-beta2 (pre-release)
« Reply #3397 on: April 11, 2023, 11:43:22 am »

Is this the proper way to do a mod's auto-run module?  Are there best practices that I'm not following?

Let me see if I can do code review in this format. Hopefully it will be easy enough to follow.

First of all, "internal" modules refer to modules that are internal relative to your mod. They are explicitly ignored by the DFHack mod scanning. The script should go in
Code: [Select]
lighter_green_glass (100)/scripts_modactive in order to get picked up and loaded.

This *does* allow your script to be shown in gui/launcher as a runnable script, but that's not a bad thing. That gives you a mechanism to show your help text, reminding the user what the mod does and telling them how to control it (if/when you implement adjustable controls).

Incidentally, this file needs help text : ) The format is documented here: https://docs.dfhack.org/en/latest/docs/dev/Documentation.html#external-scripts-and-plugins

Quote
Code: [Select]
local function mod_is_loaded()
    -- df.global.world.object_loader.object_loader_order_id[] is an array of strings.
    -- search it for the ID of our mod.  if it exists, the mod is loaded.
    return(utils.linear_index(df.global.world.object_loader.object_load_order_id, mod_ID, 'value') ~= nil)
end
You don't need this part. Once your script is in the correct location, it will load if and only if it is active for the current world.

Quote
Code: [Select]
if dfhack_flags.module then
    dfhack.onStateChange[GLOBAL_KEY] = function(event)
if event == SC_WORLD_LOADED and mod_is_loaded() then
            activate_mod_raws_changes()
elseif event == SC_WORLD_UNLOADED and mod_is_loaded() then
            deactivate_mod_raws_changes()
end
    end
end
You don't need to check for dfhack_flags.module since you've already filtered out the cases where that variable is not true. Also, as mentioned above, you don't need to check if the mod is loaded since being in the correct path will do that for you.

Also, be sure to completely remove your state change handler on world unload, otherwise your handler will still run in a later-loaded world where the mod is not active. I just recently realized this was a problem and updated the example in the modding guide. If a world with your mod active is loaded a second time, the script will be reloaded as a module before the world loaded event and will have an opportunity to reattach the handler.

Suggested replacement code:
Code: [Select]
dfhack.onStateChange[GLOBAL_KEY] = function(event)
    if event == SC_WORLD_LOADED then
        activate_mod_raws_changes()
    elseif event == SC_WORLD_UNLOADED then
        deactivate_mod_raws_changes()
        dfhack.onStateChange[GLOBAL_KEY] = nil
    end
end

It is not required, but if you implement the enabled API, it would:
1. feature your mod in the DFHack control panel (in the "Fort" tab), including a link to show your help text
2. allow players to use the enable and disable commands to dynamically control your mod

Implementing the enabled API just means adding a --@enable=true tag at the top of the file, implementing an isEnabled() function, and handling the dfhack_flags.enable_state flag. The example in the modding guide does this.

I have to say, this is a beautifully simple mod. Do you mind if I use it as an example in the modding guide?
« Last Edit: April 11, 2023, 11:48:00 am by myk »
Logged

0x517A5D

  • Bay Watcher
  • Hex Editor‬‬
    • View Profile
Re: DFHack 0.47.05-r8 | 50.07-beta2 (pre-release)
« Reply #3398 on: April 12, 2023, 03:22:11 pm »

Quote
Code: [Select]
local function mod_is_loaded()
    -- df.global.world.object_loader.object_loader_order_id[] is an array of strings.
    -- search it for the ID of our mod.  if it exists, the mod is loaded.
    return(utils.linear_index(df.global.world.object_loader.object_load_order_id, mod_ID, 'value') ~= nil)
end
You don't need this part. Once your script is in the correct location, it will load if and only if it is active for the current world.
This was a workaround, because I was experiencing the mod being active.  I was pretty sure the mod wasn't being loaded into the new world, but rather that it was still active in the dfhack.onStateChange[] table.  I'll  put in your patch to remove the onStateChange[], hopefully removing the need for this.

Quote
You don't need to check for dfhack_flags.module since you've already filtered out the cases where that variable is not true. Also, as mentioned above, you don't need to check if the mod is loaded since being in the correct path will do that for you.
I did know that.  That test is in there for development debugging.  Like:

Code: [Select]
if dfhack_flags.module then
    dfhack.onStateChange[GLOBAL_KEY] = function(event)
if event == SC_WORLD_LOADED and mod_is_loaded() then
            activate_mod_raws_changes()
elseif event == SC_WORLD_UNLOADED and mod_is_loaded() then
            deactivate_mod_raws_changes()
end
    end
else -- this branch is only for debugging.
    print(string.format('%s: manually invoking activate_mod_raws_changes()', script_name))
    activate_mod_raws_changes()
end

I've been thinking about the help text and the enable/disable capability.  I certainly understand why those are useful/necessary for standard scripts.  But I feel that, if you don't want the mod, don't install it into your world.

Actually, I don't even see the need for the scripts_modactive/internal subdirectory, except maybe for some absolutely massive total-conversion mod.  I think most people will just lump all their code into one script.

Quote
I have to say, this is a beautifully simple mod. Do you mind if I use it as an example in the modding guide?

You have my permission.

My intent was to write the simplest mod that actually did something useful.  (My intended use case is making green glass terrariums easier to haul around, but now I'm thinking that green glass trap components will be less effective.)



IMPORTANT EDIT:

If a world with your mod active is loaded a second time, the script will be reloaded as a module before the world loaded event and will have an opportunity to reattach the handler.

This bit is not true (dfhack 50.07-beta2).  I'm guessing you've implemented this but not yet had a release that contains it.  For now, I'm reverting to NOT unhooking the event handler and using mod_is_loaded().
« Last Edit: April 13, 2023, 11:59:06 am by 0x517A5D »
Logged

myk

  • Bay Watcher
    • View Profile
Re: DFHack 0.47.05-r8 | 50.07-beta2 (pre-release)
« Reply #3399 on: April 13, 2023, 07:30:20 pm »

I've been thinking about the help text and the enable/disable capability.  I certainly understand why those are useful/necessary for standard scripts.  But I feel that, if you don't want the mod, don't install it into your world.

Actually, I don't even see the need for the scripts_modactive/internal subdirectory, except maybe for some absolutely massive total-conversion mod.  I think most people will just lump all their code into one script.

Help text should always be included, since otherwise your script will show up as "No description" in ls and gui/launcher. But yes, for simple script-based mods, there is no need for an internal/ subdir or explicit enablement.

Quote
You have my permission.

Thank you!

Quote
IMPORTANT EDIT:

If a world with your mod active is loaded a second time, the script will be reloaded as a module before the world loaded event and will have an opportunity to reattach the handler.

This bit is not true (dfhack 50.07-beta2).  I'm guessing you've implemented this but not yet had a release that contains it.  For now, I'm reverting to NOT unhooking the event handler and using mod_is_loaded().

You are absolutely right. Sorry about that. There has been so much activity lately that I forgot which release that went out with. It's in the release I tagged today (50.07-r1) which is available on our GitHub page as usual. I was planning on announcing it tomorrow, concurrently with the debut of our Steam release (which is exactly the same as what's on GitHub), but people who read this can sneak the release early : )

https://github.com/DFHack/dfhack/releases/tag/50.07-r1
Logged

myk

  • Bay Watcher
    • View Profile
Re: DFHack 50.07-r1
« Reply #3400 on: April 14, 2023, 02:26:08 am »

Logged

Heretic

  • Bay Watcher
    • View Profile
Re: DFHack 0.47.05-r8 | 50.07-beta2 (pre-release)
« Reply #3401 on: April 21, 2023, 08:27:31 am »



Code: ("ann.lua") [Select]
--proof of concept of an announcement menu.

local dlg=require("gui.dialogs")

local JUNK={}
local Dang=df.global.world.status.announcements

  for ultimate,checked_Ann in pairs(Dang) do
local mono=checked_Ann.text

      table.insert(JUNK,{mono,search_key = mono:lower()})
end
 
 dlg.showListPrompt("Announcement list","scroll through the list of Announced alerts",COLOR_WHITE,JUNK,nil,nil,nil,nil,true)

ok so out of boredom I decided to write up a script that grabs all the alerts/reports and shoves them into a list... there was a previous version of this script that grab every report but some folks don't want to sift through combat logs so I made this instead. there was an attempt at adding a filter on it but shrugs.

Amazing job!
Really wish to see full-functional annoncement menu.
Logged

xzaxza

  • Bay Watcher
    • View Profile
Re: DFHack 0.47.05-r8 | 50.07-beta2 (pre-release)
« Reply #3402 on: April 22, 2023, 04:47:40 am »

In general, you can protect yourself from errors with pcall. For example:
Code: [Select]
local ok, ret = pcall(function()
    -- do something sketchy
end)

but in this case, you'd probably be best off with testing the type of the pointer before trying to access fields that only exist in some types.

Code: [Select]
local bld = thebuidlinginquestion
if bld:getType() == df.building_type.Workshop then
    -- access workshop fields
elseif bld::getType() == df.building_type.Stockpile then
    -- access stockpile fields
end
Yeah, I had a similar structure in the end. But thanks, pcall's been useful elsewhere.

Another sort of Lua question: if a value in game data is set to nil, how does one set it to something other than nil? I've given it a couple of tries, and an error message like this appears: Cannot write field: null and autovivify not requested.

I guess I could just create a new entry and copy the other fields and insert a non-null value there, and remove the old one, but is there a better way?
Logged
Known issues
You may get a dwarf that likes bugged stockpiles.

myk

  • Bay Watcher
    • View Profile
Re: DFHack 50.07-r1
« Reply #3403 on: April 22, 2023, 12:48:22 pm »

That's exactly right -- you create a new entry and insert it into the appropriate place. Here's an example from build-now where it allocates a new construction and integrates it into the DF state:
Code: [Select]
local function create_and_link_construction(pos, item, top_of_wall)
    local construction = df.construction:new()
    utils.assign(construction.pos, pos)
    construction.item_type = item:getType()
    construction.item_subtype = item:getSubtype()
    construction.mat_type = item:getMaterial()
    construction.mat_index = item:getMaterialIndex()
    construction.flags.top_of_wall = top_of_wall
    construction.flags.no_build_item = not top_of_wall
    construction.original_tile = get_original_tiletype(pos)
    utils.insert_sorted(df.global.world.constructions, construction,
                        'pos', pos_cmp)
end

See the context here: https://github.com/DFHack/scripts/blob/master/build-now.lua#L349-L361
Logged

lethosor

  • Bay Watcher
    • View Profile
Re: DFHack 0.47.05-r8 | 50.07-beta2 (pre-release)
« Reply #3404 on: April 22, 2023, 07:31:56 pm »

Myk's approach is one way of doing it. However, the error message you posted isn't one I see very often:
Cannot write field: null and autovivify not requested.

I guess I could just create a new entry and copy the other fields and insert a non-null value there, and remove the old one, but is there a better way?
Can you provide an example of the code you're using that triggers this error? There are a couple ways I could interpret your last statement, and I'm not sure which one you mean.

If you're assigning a table to a structure field, like this:
Code: [Select]
object.some_construction_field = {
  item_type = df.item_type.Foo,
  item_subtype = -1,
  -- more stuff
}
where "some_construction_field" is normally a pointer to an object, that is one case I know of that could cause the error you saw. In order for this to work, you can add a special "new=true" entry to the table, which indicates to our C++/Lua wrapper that a new object should be created based on the type of the field you're assigning the table to, like this:
Code: [Select]
object.some_construction_field = {
  new = true,
  item_type = df.item_type.Foo,
  item_subtype = -1,
  -- more stuff
}
This process is referred to as "auto-vivification" in our Lua API docs: https://docs.dfhack.org/en/stable/docs/dev/Lua%20API.html#recursive-table-assignment
Note that per (2)(b), you can also pass a type as the value under "new" to indicate the object's type, if the assignment target could have multiple types (e.g. if it's a pointer to an object of a class that has subclasses). For instance:
Code: [Select]
object.some_construction_field = {
  new = df.construction,
  item_type = df.item_type.Foo,
  item_subtype = -1,
  -- more stuff
}


In any case, this "new=" syntax is somewhat of a shorthand syntax provided for convenience. Myk's version is a more verbose equivalent.

It's also entirely possible that my guess about what you're trying to do was incorrect, so let me know if the code you're having trouble with is different.
Logged
DFHack - Dwarf Manipulator (Lua) - DF Wiki talk

There was a typo in the siegers' campfire code. When the fires went out, so did the game.
Pages: 1 ... 225 226 [227] 228 229 ... 243