develop
Warmist 2012-12-23 12:34:05 +02:00
commit e4f4943b10
10 changed files with 338 additions and 33 deletions

@ -404,7 +404,10 @@ ul.auto-toc {
<li><a class="reference internal" href="#sort" id="id54">sort</a></li> <li><a class="reference internal" href="#sort" id="id54">sort</a></li>
</ul> </ul>
</li> </li>
<li><a class="reference internal" href="#scripts" id="id55">Scripts</a></li> <li><a class="reference internal" href="#scripts" id="id55">Scripts</a><ul>
<li><a class="reference internal" href="#save-init-script" id="id56">Save init script</a></li>
</ul>
</li>
</ul> </ul>
</div> </div>
<p>The current version of DFHack has extensive support for <p>The current version of DFHack has extensive support for
@ -1034,6 +1037,9 @@ can be omitted.</p>
<li><p class="first"><tt class="docutils literal">dfhack.getHackPath()</tt></p> <li><p class="first"><tt class="docutils literal">dfhack.getHackPath()</tt></p>
<p>Returns the dfhack directory path, i.e. <tt class="docutils literal"><span class="pre">&quot;.../df/hack/&quot;</span></tt>.</p> <p>Returns the dfhack directory path, i.e. <tt class="docutils literal"><span class="pre">&quot;.../df/hack/&quot;</span></tt>.</p>
</li> </li>
<li><p class="first"><tt class="docutils literal">dfhack.getSavePath()</tt></p>
<p>Returns the path to the current save directory, or <em>nil</em> if no save loaded.</p>
</li>
<li><p class="first"><tt class="docutils literal">dfhack.getTickCount()</tt></p> <li><p class="first"><tt class="docutils literal">dfhack.getTickCount()</tt></p>
<p>Returns the tick count in ms, exactly as DF ui uses.</p> <p>Returns the tick count in ms, exactly as DF ui uses.</p>
</li> </li>
@ -3064,6 +3070,25 @@ The <tt class="docutils literal">name</tt> argument should be the name stem, as
</li> </li>
</ul> </ul>
<p>Note that this function lets errors propagate to the caller.</p> <p>Note that this function lets errors propagate to the caller.</p>
<div class="section" id="save-init-script">
<h2><a class="toc-backref" href="#id56">Save init script</a></h2>
<p>If a save directory contains a file called <tt class="docutils literal">raw/init.lua</tt>, it is
automatically loaded and executed every time the save is loaded. It
can also define the following functions to be called by dfhack:</p>
<ul>
<li><p class="first"><tt class="docutils literal">function onStateChange(op) ... end</tt></p>
<p>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 <tt class="docutils literal">dfhack.onStateChange</tt> table, with associated
cleanup concerns.</p>
</li>
<li><p class="first"><tt class="docutils literal">function onUnload() ... end</tt></p>
<p>Called when the save containing the script is unloaded. This function
should clean up any global hooks installed by the script.</p>
</li>
</ul>
<p>Within the init script, the path to the save directory is available as <tt class="docutils literal">SAVE_PATH</tt>.</p>
</div>
</div> </div>
</div> </div>
</body> </body>

@ -741,6 +741,10 @@ can be omitted.
Returns the dfhack directory path, i.e. ``".../df/hack/"``. 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()`` * ``dfhack.getTickCount()``
Returns the tick count in ms, exactly as DF ui uses. 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. 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. 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``.

@ -13,6 +13,7 @@ DFHack future
- superdwarf: work in adventure mode too - superdwarf: work in adventure mode too
- tweak stable-cursor: carries cursor location from/to Build menu. - tweak stable-cursor: carries cursor location from/to Build menu.
- deathcause: allow selection from the unitlist screen - deathcause: allow selection from the unitlist screen
- slayrace: allow targetting undeads
New tweaks: New tweaks:
- tweak military-training: speed up melee squad training up to 10x (normally 3-5x). - tweak military-training: speed up melee squad training up to 10x (normally 3-5x).
New scripts: New scripts:
@ -24,7 +25,7 @@ DFHack future
- lever: list and pull fort levers from the dfhack console. - lever: list and pull fort levers from the dfhack console.
- stripcaged: mark items inside cages for dumping, eg caged goblin weapons. - stripcaged: mark items inside cages for dumping, eg caged goblin weapons.
- soundsense-season: writes the correct season to gamelog.txt on world load. - soundsense-season: writes the correct season to gamelog.txt on world load.
- devel/create-items: spawn items - create-items: spawn items
New GUI scripts: New GUI scripts:
- gui/guide-path: displays the cached path for minecart Guide orders. - gui/guide-path: displays the cached path for minecart Guide orders.
- gui/workshop-job: displays inputs of a workshop job and allows tweaking them. - gui/workshop-job: displays inputs of a workshop job and allows tweaking them.

@ -1850,13 +1850,16 @@ slayrace
======== ========
Kills any unit of a given race. 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 ``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`` 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 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, 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 ``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 Focus a body part ingame, and this script will display the cause of death of
the creature. the creature.
Also works when selecting units from the 'u'nitlist viewscreen.
lua lua
=== ===
@ -2001,6 +2005,33 @@ alternatively pass cage IDs as arguments::
stripcaged weapons 25321 34228 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 In-game interface tools
======================= =======================

@ -1551,6 +1551,10 @@ void DFHack::Lua::Notification::bind(lua_State *state, const char *name)
void OpenDFHackApi(lua_State *state); 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) lua_State *DFHack::Lua::Open(color_ostream &out, lua_State *state)
{ {
if (!state) if (!state)
@ -1654,6 +1658,10 @@ lua_State *DFHack::Lua::Open(color_ostream &out, lua_State *state)
lua_dup(state); lua_dup(state);
lua_rawseti(state, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS); 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 // load dfhack.lua
Require(out, state, "dfhack"); Require(out, state, "dfhack");
@ -1829,8 +1837,12 @@ void DFHack::Lua::Core::Init(color_ostream &out)
State = luaL_newstate(); State = luaL_newstate();
// Calls InitCoreContext after checking IsCoreContext
Lua::Open(out, State); Lua::Open(out, State);
}
static void Lua::Core::InitCoreContext()
{
lua_newtable(State); lua_newtable(State);
lua_rawsetp(State, LUA_REGISTRYINDEX, &DFHACK_TIMEOUTS_TOKEN); lua_rawsetp(State, LUA_REGISTRYINDEX, &DFHACK_TIMEOUTS_TOKEN);

@ -328,9 +328,11 @@ end
-- Command scripts -- 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() local hack_path = dfhack.getHackPath()
function dfhack.run_script(name,...) function dfhack.run_script(name,...)
@ -349,5 +351,42 @@ function dfhack.run_script(name,...)
return f(...) return f(...)
end 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. -- Feed the table back to the require() mechanism.
return dfhack return dfhack

@ -103,18 +103,157 @@ module DFHack
# some other stuff with ui.race_id ? (jobs only?) # some other stuff with ui.race_id ? (jobs only?)
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 }
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) def unit_iscitizen(u)
unit_category(u) == :Citizens unit_category(u) == :Citizens
end 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) def unit_ishostile(u)
unit_category(u) == :Others and # return true if u.flags3.ghostly and not u.flags1.dead
# TODO return unless unit_category(u) == :Others
true
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 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) # list workers (citizen, not crazy / child / inmood / noble)
def unit_workers def unit_workers
@ -125,6 +264,7 @@ module DFHack
def unit_isworker(u) def unit_isworker(u)
unit_iscitizen(u) and unit_iscitizen(u) and
u.race == df.ui.race_id and
u.mood == :None and u.mood == :None and
u.profession != :CHILD and u.profession != :CHILD and
u.profession != :BABY and u.profession != :BABY and
@ -153,8 +293,8 @@ module DFHack
def unit_entitypositions(unit) def unit_entitypositions(unit)
list = [] list = []
return list if not hf = unit.hist_figure_tg return list if not histfig = unit.hist_figure_tg
hf.entity_links.each { |el| histfig.entity_links.each { |el|
next if el._rtti_classname != :histfig_entity_link_positionst next if el._rtti_classname != :histfig_entity_link_positionst
next if not ent = el.entity_tg next if not ent = el.entity_tg
next if not pa = ent.positions.assignments.binsearch(el.assignment_id) next if not pa = ent.positions.assignments.binsearch(el.assignment_id)

@ -1,14 +1,17 @@
# slay all creatures of a given race # slay all creatures of a given race
# race = name of the race to eradicate, use 'him' to target only the selected creature # 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] race = $script_args[0]
# if the 2nd parameter is 'magma', magma rain for the targets instead of instant death # if the 2nd parameter is 'magma', magma rain for the targets instead of instant death
magma = ($script_args[1] == 'magma') magma = ($script_args[1] == 'magma')
checkunit = lambda { |u| 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.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 not df.map_designation_at(u).hidden
} }
@ -26,7 +29,8 @@ slayit = lambda { |u|
df.onupdate_unregister(ouh) df.onupdate_unregister(ouh)
else else
x, y, z = u.pos.x, u.pos.y, u.pos.z 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) df.map_tile_at(x, y, z).spawn_magma(7)
end end
} }
@ -36,17 +40,41 @@ slayit = lambda { |u|
all_races = Hash.new(0) all_races = Hash.new(0)
df.world.units.active.map { |u| 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}" } all_races.sort_by { |race, cnt| [cnt, race] }.each{ |race, cnt| puts " #{race} #{cnt}" }
elsif race == 'him'
when 'him'
if him = df.unit_find if him = df.unit_find
slayit[him] slayit[him]
else else
puts "Choose target" puts "Select a target ingame"
end 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 else
raw_race = df.match_rawname(race, all_races.keys) raw_race = df.match_rawname(race, all_races.keys)
raise 'invalid race' if not raw_race raise 'invalid race' if not raw_race
@ -60,6 +88,6 @@ else
count += 1 count += 1
end end
} }
puts "slain #{count} #{raw_race}" puts "slain #{count} #{raw_race}"
end end

@ -138,7 +138,10 @@ if here_only
if not it.kind_of?(DFHack::ItemCagest) and not it.kind_of?(DFHack::ItemAnimaltrapst) 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) } list = df.world.items.other[:ANY_CAGE_OR_TRAP].find_all { |i| df.at_cursor?(i) }
end 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 elsif ids = $script_args.find_all { |arg| arg =~ /^\d+$/ } and ids.first
list = [] list = []
@ -153,7 +156,10 @@ elsif ids = $script_args.find_all { |arg| arg =~ /^\d+$/ } and ids.first
list << it list << it
end 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 else
list = df.world.items.other[:ANY_CAGE_OR_TRAP] list = df.world.items.other[:ANY_CAGE_OR_TRAP]
@ -162,18 +168,16 @@ end
# act # act
case $script_args[0] case $script_args[0]
when 'items' when /^it/i
cage_dump_items(list) if not list.empty? cage_dump_items(list)
when 'armor' when /^arm/i
cage_dump_armor(list) if not list.empty? cage_dump_armor(list)
when 'weapons' when /^wea/i
cage_dump_weapons(list) if not list.empty? cage_dump_weapons(list)
when 'all' when 'all'
cage_dump_all(list) if not list.empty? cage_dump_all(list)
when 'list' when 'list'
cage_dump_list(list) if not list.empty? cage_dump_list(list)
else else
puts <<EOS puts <<EOS
Marks items inside all cages for dumping. Marks items inside all cages for dumping.