diff --git a/Lua API.html b/Lua API.html index aa41b8b63..e5318165d 100644 --- a/Lua API.html +++ b/Lua API.html @@ -404,7 +404,10 @@ ul.auto-toc {
  • sort
  • -
  • Scripts
  • +
  • Scripts +
  • The current version of DFHack has extensive support for @@ -1034,6 +1037,9 @@ can be omitted.

  • dfhack.getHackPath()

    Returns the dfhack directory path, i.e. ".../df/hack/".

  • +
  • dfhack.getSavePath()

    +

    Returns the path to the current save directory, or nil if no save loaded.

    +
  • dfhack.getTickCount()

    Returns the tick count in ms, exactly as DF ui uses.

  • @@ -3064,6 +3070,25 @@ The name argument should be the name stem, as

    Note that this function lets errors propagate to the caller.

    +
    +

    Save init script

    +

    If a save directory contains a file called raw/init.lua, it is +automatically loaded and executed every time the save is loaded. It +can also define the following functions to be called by dfhack:

    + +

    Within the init script, the path to the save directory is available as SAVE_PATH.

    +
    diff --git a/Lua API.rst b/Lua API.rst index 4a203d7bd..95ce5923e 100644 --- a/Lua API.rst +++ b/Lua API.rst @@ -741,6 +741,10 @@ can be omitted. Returns the dfhack directory path, i.e. ``".../df/hack/"``. +* ``dfhack.getSavePath()`` + + Returns the path to the current save directory, or *nil* if no save loaded. + * ``dfhack.getTickCount()`` Returns the tick count in ms, exactly as DF ui uses. @@ -3036,3 +3040,24 @@ from other scripts) in any context, via the same function the core uses: The ``name`` argument should be the name stem, as would be used on the command line. Note that this function lets errors propagate to the caller. + +Save init script +================ + +If a save directory contains a file called ``raw/init.lua``, it is +automatically loaded and executed every time the save is loaded. It +can also define the following functions to be called by dfhack: + +* ``function onStateChange(op) ... end`` + + Automatically called from the regular onStateChange event as long + as the save is still loaded. This avoids the need to install a hook + into the global ``dfhack.onStateChange`` table, with associated + cleanup concerns. + +* ``function onUnload() ... end`` + + Called when the save containing the script is unloaded. This function + should clean up any global hooks installed by the script. + +Within the init script, the path to the save directory is available as ``SAVE_PATH``. diff --git a/NEWS b/NEWS index 893d34740..f7a7a6281 100644 --- a/NEWS +++ b/NEWS @@ -13,6 +13,7 @@ DFHack future - superdwarf: work in adventure mode too - tweak stable-cursor: carries cursor location from/to Build menu. - deathcause: allow selection from the unitlist screen + - slayrace: allow targetting undeads New tweaks: - tweak military-training: speed up melee squad training up to 10x (normally 3-5x). New scripts: @@ -24,7 +25,7 @@ DFHack future - lever: list and pull fort levers from the dfhack console. - stripcaged: mark items inside cages for dumping, eg caged goblin weapons. - soundsense-season: writes the correct season to gamelog.txt on world load. - - devel/create-items: spawn items + - create-items: spawn items New GUI scripts: - gui/guide-path: displays the cached path for minecart Guide orders. - gui/workshop-job: displays inputs of a workshop job and allows tweaking them. diff --git a/Readme.rst b/Readme.rst index 60fd1b78f..24be76f96 100644 --- a/Readme.rst +++ b/Readme.rst @@ -1850,13 +1850,16 @@ slayrace ======== Kills any unit of a given race. -With no argument, lists the available races. +With no argument, lists the available races and count eligible targets. With the special argument ``him``, targets only the selected creature. +With the special argument ``undead``, targets all undeads on the map, +regardless of their race. + Any non-dead non-caged unit of the specified race gets its ``blood_count`` set to 0, which means immediate death at the next game tick. For creatures -such as vampires, also set animal.vanish_countdown to 2. +such as vampires, it also sets animal.vanish_countdown to 2. An alternate mode is selected by adding a 2nd argument to the command, ``magma``. In this case, a column of 7/7 magma is generated on top of the @@ -1939,6 +1942,7 @@ deathcause ========== Focus a body part ingame, and this script will display the cause of death of the creature. +Also works when selecting units from the 'u'nitlist viewscreen. lua === @@ -2001,6 +2005,33 @@ alternatively pass cage IDs as arguments:: stripcaged weapons 25321 34228 +create-items +============ +Spawn arbitrary items under the cursor. + +The first argument gives the item category, the second gives the material, +and the optionnal third gives the number of items to create (defaults to 20). + +Currently supported item categories: ``boulder``, ``bar``, ``plant``, ``log``, +``web``. + +Instead of material, using ``list`` makes the script list eligible materials. + +The ``web`` item category will create an uncollected cobweb on the floor. + +Note that the script does not enforce anything, and will let you create +boulders of toad blood and stuff like that. +However the ``list`` mode will only show 'normal' materials. + +Exemples:: + + create-items boulders COAL_BITUMINOUS 12 + create-items plant tail_pig + create-items log list + create-items web CREATURE:SPIDER_CAVE_GIANT:SILK + create-items bar CREATURE:CAT:SOAP + create-items bar adamantine + ======================= In-game interface tools ======================= diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp index c052b88aa..392659bc8 100644 --- a/library/LuaTools.cpp +++ b/library/LuaTools.cpp @@ -1551,6 +1551,10 @@ void DFHack::Lua::Notification::bind(lua_State *state, const char *name) void OpenDFHackApi(lua_State *state); +namespace DFHack { namespace Lua { namespace Core { + static void InitCoreContext(); +}}} + lua_State *DFHack::Lua::Open(color_ostream &out, lua_State *state) { if (!state) @@ -1654,6 +1658,10 @@ lua_State *DFHack::Lua::Open(color_ostream &out, lua_State *state) lua_dup(state); lua_rawseti(state, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS); + // Init core-context specific stuff before loading dfhack.lua + if (IsCoreContext(state)) + Lua::Core::InitCoreContext(); + // load dfhack.lua Require(out, state, "dfhack"); @@ -1829,8 +1837,12 @@ void DFHack::Lua::Core::Init(color_ostream &out) State = luaL_newstate(); + // Calls InitCoreContext after checking IsCoreContext Lua::Open(out, State); +} +static void Lua::Core::InitCoreContext() +{ lua_newtable(State); lua_rawsetp(State, LUA_REGISTRYINDEX, &DFHACK_TIMEOUTS_TOKEN); diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua index 3f57e5722..1354701ff 100644 --- a/library/lua/dfhack.lua +++ b/library/lua/dfhack.lua @@ -328,9 +328,11 @@ end -- Command scripts -dfhack.internal.scripts = dfhack.internal.scripts or {} +local internal = dfhack.internal -local scripts = dfhack.internal.scripts +internal.scripts = internal.scripts or {} + +local scripts = internal.scripts local hack_path = dfhack.getHackPath() function dfhack.run_script(name,...) @@ -349,5 +351,42 @@ function dfhack.run_script(name,...) return f(...) end +-- Per-save init file + +function dfhack.getSavePath() + if dfhack.isWorldLoaded() then + return dfhack.getDFPath() .. '/data/save/' .. df.global.world.cur_savegame.save_dir + end +end + +if dfhack.is_core_context then + dfhack.onStateChange.DFHACK_PER_SAVE = function(op) + if op == SC_WORLD_LOADED or op == SC_WORLD_UNLOADED then + if internal.save_init then + if internal.save_init.onUnload then + safecall(internal.save_init.onUnload) + end + internal.save_init = nil + end + + local path = dfhack.getSavePath() + + if path and op == SC_WORLD_LOADED then + local env = setmetatable({ SAVE_PATH = path }, { __index = base_env }) + local f,perr = loadfile(path..'/raw/init.lua', 't', env) + if f == nil then + if not string.match(perr, 'No such file or directory') then + dfhack.printerr(perr) + end + elseif safecall(f) then + internal.save_init = env + end + end + elseif internal.save_init and internal.save_init.onStateChange then + safecall(internal.save_init.onStateChange, op) + end + end +end + -- Feed the table back to the require() mechanism. return dfhack diff --git a/plugins/ruby/unit.rb b/plugins/ruby/unit.rb index 5e2de110e..a5bf6e00e 100644 --- a/plugins/ruby/unit.rb +++ b/plugins/ruby/unit.rb @@ -103,18 +103,157 @@ module DFHack # some other stuff with ui.race_id ? (jobs only?) end + # merchant: df.ui.caravans.find { |cv| cv.entity == u.civ_id } + # diplomat: df.ui.dip_meeting_info.find { |m| m.diplomat_id == u.hist_figure_id or m.diplomat_id2 == u.hist_figure_id } + + + def unit_nemesis(u) + if ref = u.general_refs.find { |r| r.kind_of?(DFHack::GeneralRefIsNemesisst) } + ref.nemesis_tg + end + end + + # return the subcategory for :Others (from vs_unitlist) + def unit_other_category(u) + # comment is actual code returned by the df function + return :Berserk if u.mood == :Berserk # 5 + return :Berserk if unit_testflagcurse(u, :CRAZED) # 14 + return :Undead if unit_testflagcurse(u, :OPPOSED_TO_LIFE) # 1 + return :Undead if u.flags3.ghostly # 15 + + if df.gamemode == :ADVENTURE + return :Hostile if u.civ_id == -1 # 2 + if u.animal.population.region_x == -1 + return :Wild if u.flags2.roaming_wilderness_population_source_not_a_map_feature # 0 + else + return :Hostile if u.flags2.important_historical_figure and n = unit_nemesis(u) and n.flags[:ACTIVE_ADVENTURER] # 2 + end + return :Hostile if u.flags2.resident # 3 + return :Hostile # 4 + end + + return :Invader if u.flags1.active_invader or u.flags1.invader_origin # 6 + return :Friendly if u.flags1.forest or u.flags1.merchant or u.flags1.diplomat # 8 + return :Hostile if u.flags1.tame # 7 + + if u.civ_id != -1 + return :Unsure if u.civ_id != df.ui.civ_id or u.flags1.resident or u.flags1.visitor or u.flags1.visitor_uninvited # 10 + return :Hostile # 7 + + elsif u.animal.population.region_x == -1 + return :Friendly if u.flags2.visitor # 8 + return :Uninvited if u.flags2.visitor_uninvited # 12 + return :Underworld if r = u.race_tg and r.underground_layer_min == 5 # 9 + return :Resident if u.flags2.resident # 13 + return :Friendly # 8 + + else + return :Friendly if u.flags2.visitor # 8 + return :Underworld if r = u.race_tg and r.underground_layer_min == 5 # 9 + return :Wild if u.animal.population.feature_idx == -1 and u.animal.population.cave_id == -1 # 0 + return :Wild # 11 + end + end + def unit_iscitizen(u) unit_category(u) == :Citizens end + def unit_hostiles + world.units.active.find_all { |u| + unit_ishostile(u) + } + end + + # returns if an unit is openly hostile + # does not include ghosts / wildlife def unit_ishostile(u) - unit_category(u) == :Others and - # TODO - true + # return true if u.flags3.ghostly and not u.flags1.dead + return unless unit_category(u) == :Others + + case unit_other_category(u) + when :Berserk, :Undead, :Hostile, :Invader, :Underworld + # XXX :Resident, :Uninvited? + true + + when :Unsure + # from df code, with removed duplicate checks already in other_category + return true if u.enemy.undead or u.flags3.ghostly or u.flags1.marauder + return false if u.flags1.forest or u.flags1.merchant or u.flags1.diplomat or u.flags2.visitor + return true if u.flags1.tame or u.flags2.underworld + + if histfig = u.hist_figure_tg + group = df.ui.group_tg + case unit_checkdiplomacy_hf_ent(histfig, group) + when 4, 5 + true + end + + elsif diplo = u.civ_tg.unknown1b.diplomacy.binsearch(df.ui.group_id, :group_id) + diplo.relation != 1 and diplo.relation != 5 + + else + u.animal.population.region_x != -1 or u.flags2.resident or u.flags2.visitor_uninvited + end + end + end + + def unit_checkdiplomacy_hf_ent(histfig, group) + var_3d = var_3e = var_45 = var_46 = var_47 = var_48 = var_49 = nil + + var_3d = 1 if group.type == :Outcast or group.type == :NomadicGroup or + (group.type == :Civilization and group.entity_raw.flags[:LOCAL_BANDITRY]) + + histfig.entity_links.each { |link| + if link.entity_id == group.id + case link.getType + when :MEMBER, :MERCENARY, :SLAVE, :PRISONER, :POSITION, :HERO + var_47 = 1 + when :FORMER_MEMBER, :FORMER_MERCENARY, :FORMER_SLAVE, :FORMER_PRISONER + var_48 = 1 + when :ENEMY + var_49 = 1 + when :CRIMINAL + var_45 = 1 + end + else + case link.getType + when :MEMBER, :MERCENARY, :SLAVE + if link_entity = link.entity_tg + diplo = group.unknown1b.diplomacy.binsearch(link.entity_id, :group_id) + case diplo.relation + when 0, 3, 4 + var_48 = 1 + when 1, 5 + var_46 = 1 + end + + var_3e = 1 if link_entity.type == :Outcast or link_entity.type == :NomadicGroup or + (link_entity.type == :Civilization and link_entity.entity_raw.flags[:LOCAL_BANDITRY]) + end + end + end + } + + if var_49 + 4 + elsif var_46 + 5 + elsif !var_47 and group.resources.ethic[:KILL_NEUTRAL] == 16 + 4 + elsif df.gamemode == :ADVENTURE and !var_47 and (var_3e or !var_3d) + 4 + elsif var_45 + 3 + elsif var_47 + 2 + elsif var_48 + 1 + else + 0 + end end - # merchant: df.ui.caravans.find { |cv| cv.entity == u.civ_id } - # diplomat: df.ui.dip_meeting_info.find { |m| m.diplomat_id == u.hist_figure_id or m.diplomat_id2 == u.hist_figure_id } # list workers (citizen, not crazy / child / inmood / noble) def unit_workers @@ -125,6 +264,7 @@ module DFHack def unit_isworker(u) unit_iscitizen(u) and + u.race == df.ui.race_id and u.mood == :None and u.profession != :CHILD and u.profession != :BABY and @@ -153,8 +293,8 @@ module DFHack def unit_entitypositions(unit) list = [] - return list if not hf = unit.hist_figure_tg - hf.entity_links.each { |el| + return list if not histfig = unit.hist_figure_tg + histfig.entity_links.each { |el| next if el._rtti_classname != :histfig_entity_link_positionst next if not ent = el.entity_tg next if not pa = ent.positions.assignments.binsearch(el.assignment_id) diff --git a/scripts/devel/create-items.rb b/scripts/create-items.rb similarity index 100% rename from scripts/devel/create-items.rb rename to scripts/create-items.rb diff --git a/scripts/slayrace.rb b/scripts/slayrace.rb index ca50020f7..4844538d4 100644 --- a/scripts/slayrace.rb +++ b/scripts/slayrace.rb @@ -1,14 +1,17 @@ # slay all creatures of a given race # race = name of the race to eradicate, use 'him' to target only the selected creature +# use 'undead' to target all undeads race = $script_args[0] + # if the 2nd parameter is 'magma', magma rain for the targets instead of instant death magma = ($script_args[1] == 'magma') checkunit = lambda { |u| - u.body.blood_count != 0 and + (u.body.blood_count != 0 or u.body.blood_max == 0) and not u.flags1.dead and - not u.flags1.caged and + not u.flags1.caged and not u.flags1.chained and + #not u.flags1.hidden_in_ambush and not df.map_designation_at(u).hidden } @@ -26,7 +29,8 @@ slayit = lambda { |u| df.onupdate_unregister(ouh) else x, y, z = u.pos.x, u.pos.y, u.pos.z - z += 1 while tile = df.map_tile_at(x, y, z+1) and tile.shape_passableflow + z += 1 while tile = df.map_tile_at(x, y, z+1) and + tile.shape_passableflow and tile.shape_passablelow df.map_tile_at(x, y, z).spawn_magma(7) end } @@ -36,17 +40,41 @@ slayit = lambda { |u| all_races = Hash.new(0) df.world.units.active.map { |u| - all_races[u.race_tg.creature_id] += 1 if checkunit[u] + if checkunit[u] + if (u.enemy.undead or + (u.curse.add_tags1.OPPOSED_TO_LIFE and not + u.curse.rem_tags1.OPPOSED_TO_LIFE)) + all_races['Undead'] += 1 + else + all_races[u.race_tg.creature_id] += 1 + end + end } -if !race +case race +when nil all_races.sort_by { |race, cnt| [cnt, race] }.each{ |race, cnt| puts " #{race} #{cnt}" } -elsif race == 'him' + +when 'him' if him = df.unit_find slayit[him] else - puts "Choose target" + puts "Select a target ingame" end + +when /^undead/i + count = 0 + df.world.units.active.each { |u| + if (u.enemy.undead or + (u.curse.add_tags1.OPPOSED_TO_LIFE and not + u.curse.rem_tags1.OPPOSED_TO_LIFE)) and + checkunit[u] + slayit[u] + count += 1 + end + } + puts "slain #{count} undeads" + else raw_race = df.match_rawname(race, all_races.keys) raise 'invalid race' if not raw_race @@ -60,6 +88,6 @@ else count += 1 end } - puts "slain #{count} #{raw_race}" + end diff --git a/scripts/stripcaged.rb b/scripts/stripcaged.rb index 07694f711..07f37c756 100644 --- a/scripts/stripcaged.rb +++ b/scripts/stripcaged.rb @@ -138,7 +138,10 @@ if here_only if not it.kind_of?(DFHack::ItemCagest) and not it.kind_of?(DFHack::ItemAnimaltrapst) list = df.world.items.other[:ANY_CAGE_OR_TRAP].find_all { |i| df.at_cursor?(i) } end - puts 'Please select a cage' if list.empty? + if list.empty? + puts 'Please select a cage' + throw :script_finished + end elsif ids = $script_args.find_all { |arg| arg =~ /^\d+$/ } and ids.first list = [] @@ -153,7 +156,10 @@ elsif ids = $script_args.find_all { |arg| arg =~ /^\d+$/ } and ids.first list << it end } - puts 'Please use a valid cage id' if list.empty? + if list.empty? + puts 'Please use a valid cage id' + throw :script_finished + end else list = df.world.items.other[:ANY_CAGE_OR_TRAP] @@ -162,18 +168,16 @@ end # act case $script_args[0] -when 'items' - cage_dump_items(list) if not list.empty? -when 'armor' - cage_dump_armor(list) if not list.empty? -when 'weapons' - cage_dump_weapons(list) if not list.empty? +when /^it/i + cage_dump_items(list) +when /^arm/i + cage_dump_armor(list) +when /^wea/i + cage_dump_weapons(list) when 'all' - cage_dump_all(list) if not list.empty? - + cage_dump_all(list) when 'list' - cage_dump_list(list) if not list.empty? - + cage_dump_list(list) else puts <