Petr Mrázek 2013-04-24 16:24:14 +02:00
commit df6355a092
33 changed files with 1625 additions and 1362 deletions

@ -63,6 +63,23 @@ extra options.
You can also use a cmake-friendly IDE like KDevelop 4 or the cmake-gui
program.
Fixing the libstdc++ version bug
================================
When compiling dfhack yourself, it builds against your system libc.
When Dwarf Fortress runs, it uses a libstdc++ shipped with the binary, which
is usually way older, and incompatible with your dfhack. This manifests with
the error message::
./libs/Dwarf_Fortress: /pathToDF/libs/libstdc++.so.6: version
`GLIBCXX_3.4.15' not found (required by ./hack/libdfhack.so)
To fix this, simply remove the libstdc++ shipped with DF, it will fall back
to your system lib and everything will work fine::
cd /path/to/DF/
rm libs/libstdc++.so.6
========
Mac OS X
========

@ -5,8 +5,13 @@ DFHack future
- restrictice - Restrict traffic on squares above visible ice.
New scripts:
- masspit: designate caged creatures in a zone for pitting
- locate_ore: scan the map for unmined ore veins
- multicmd: run a sequence of dfhack commands, separated by ';'
Misc improvements:
- exterminate: renamed from slayrace, add help message
- exterminate: renamed from slayrace, add help message, add butcher mode
- autoSyndrome: disable by default
- ruby: add df.dfhack_run "somecommand"
- magmasource: rename to source, allow water/magma sources/drains
DFHack v0.34.11-r3

@ -1857,7 +1857,7 @@ use in your farming plots.
With a seed type, the script will grow 100 of these seeds, ready to be
harvested. You can change the number with a 2nd argument.
For exemple, to grow 40 plump helmet spawn:
For example, to grow 40 plump helmet spawn:
::
growcrops plump 40
@ -1900,9 +1900,10 @@ 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
targets until they die (Warning: do not call on magma-safe creatures. Also,
using this mode for birds is not recommanded.)
using this mode on birds is not recommanded.)
Will target any unit on a revealed tile of the map, including ambushers.
Will target any unit on a revealed tile of the map, including ambushers,
but ignore caged/chained creatures.
Ex::
@ -1917,24 +1918,32 @@ To purify all elves on the map with fire (may have side-effects)::
exterminate elve magma
magmasource
===========
Create an infinite magma source on a tile.
source
======
Create an infinite magma or water source or drain on a tile.
This script registers a map tile as a magma source, and every 12 game ticks
that tile receives 1 new unit of flowing magma.
This script registers a map tile as a liquid source, and every 12 game ticks
that tile receives or remove 1 new unit of flow based on the configuration.
Place the game cursor where you want to create the source (must be a
flow-passable tile, and not too high in the sky) and call::
magmasource here
source add [magma|water] [0-7]
To add more than 1 unit everytime, call the command again.
The number argument is the target liquid level (0 = drain, 7 = source).
To delete one source, place the cursor over its tile and use ``delete-here``.
To remove all placed sources, call ``magmasource stop``.
To add more than 1 unit everytime, call the command again on the same spot.
With no argument, this command shows an help message and list existing sources.
To delete one source, place the cursor over its tile and use ``delete``.
To remove all existing sources, call ``source clear``.
The ``list`` argument shows all existing sources.
Ex::
source add water - water source
source add magma 7 - magma source
source add water 0 - water drain
masspit
=======
@ -2066,7 +2075,7 @@ 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::
Examples::
create-items boulders COAL_BITUMINOUS 12
create-items plant tail_pig
@ -2075,6 +2084,20 @@ Exemples::
create-items bar CREATURE:CAT:SOAP
create-items bar adamantine
locate-ore
==========
Scan the map for metal ores.
Finds and designate for digging one tile of a specific metal ore.
Only works for native metal ores, does not handle reaction stuff (eg STEEL).
When invoked with the ``list`` argument, lists metal ores available on the map.
Examples::
locate-ore list
locate-ore hematite
locate-ore iron
soundsense-season
=================
@ -2086,6 +2109,15 @@ This script registers a hook that prints the appropriate string
to gamelog.txt on every map load to fix this. For best results
call the script from ``dfhack.init``.
multicmd
========
Run multiple dfhack commands. The argument is split around the
character ; and all parts are run sequencially as independent
dfhack commands. Useful for hotkeys.
Example::
multicmd locate-ore iron ; digv
=======================
In-game interface tools
=======================

@ -137,6 +137,9 @@ tweak military-color-assigned
# remove inverse dependency of squad training speed on unit list size and use more sparring
tweak military-training
# enable autoSyndrome
autoSyndrome enable
###########
# Scripts #
###########
@ -149,23 +152,21 @@ fix/cloth-stockpile enable
#######################################################
# Apply binary patches at runtime #
# #
# Commented out by default; enable the ones you want. #
#######################################################
# Bug 5994 - items teleported when removing a construction
#binpatch apply deconstruct-teleport
#binpatch apply deconstruct-heapfall
binpatch apply deconstruct-teleport
binpatch apply deconstruct-heapfall
# Bug 4406 - hospital overstocking on all items
#binpatch apply hospital-overstocking
binpatch apply hospital-overstocking
# Bug 808 - custom reactions completely using up all of their reagents
#binpatch apply custom-reagent-size
binpatch apply custom-reagent-size
# Bug 4530 - marksdwarves not training when quiver full of combat-only ammo
#binpatch apply training-ammo
binpatch apply training-ammo
# Bug 1445 - weapon racks broken, armor stand capacity too low
#binpatch apply weaponrack-unassign
#binpatch apply armorstand-capacity
binpatch apply weaponrack-unassign
binpatch apply armorstand-capacity

@ -98,7 +98,7 @@ reaction_duck
Next, start a new fort in a new world, build a duck workshop, then have someone become a duck.
*/
bool enabled = true;
bool enabled = false;
DFHACK_PLUGIN("autoSyndrome");
@ -279,6 +279,9 @@ void processJob(color_ostream& out, void* jobPtr) {
}
}
if ( workerId == -1 )
return;
int32_t workerIndex = df::unit::binsearch_index(df::global::world->units.all, workerId);
if ( workerIndex < 0 ) {
out.print("%s line %d: Couldn't find unit %d.\n", __FILE__, __LINE__, workerId);

@ -55,7 +55,7 @@ DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <Plug
" Also follows the stone between z-levels with stairs, like 'digl x' would.\n"
));
commands.push_back(PluginCommand("digexp","Select or designate an exploratory pattern. Use 'digexp ?' for help.",digexp));
commands.push_back(PluginCommand("digcircle","Dig designate a circle (filled or hollow) with given radius.",digcircle));
commands.push_back(PluginCommand("digcircle","Dig designate a circle (filled or hollow) with given diameter.",digcircle));
//commands.push_back(PluginCommand("digauto","Mark a tile for continuous digging.",autodig));
commands.push_back(PluginCommand("digtype", "Dig all veins of a given type.", digtype,Gui::cursor_hotkey,
"For every tile on the map of the same vein type as the selected tile, this command designates it to have the same designation as the selected tile. If the selected tile has no designation, they will be dig designated.\n"
@ -120,7 +120,7 @@ bool dig (MapExtras::MapCache & MCache,
if(tileMaterial(tt) == tiletype_material::CONSTRUCTION && !des.bits.hidden)
return false;
df::tiletype_shape ts = tileShape(tt);
if (ts == tiletype_shape::EMPTY)
if (ts == tiletype_shape::EMPTY && !des.bits.hidden)
return false;
if(!des.bits.hidden)
{
@ -293,7 +293,7 @@ command_result digcircle (color_ostream &out, vector <string> & parameters)
"\n"
"After you have set the options, the command called with no options\n"
"repeats with the last selected parameters:\n"
"'digcircle filled 3' = Dig a filled circle with radius = 3.\n"
"'digcircle filled 3' = Dig a filled circle with diameter = 3.\n"
"'digcircle' = Do it again.\n"
);
return CR_OK;

@ -54,6 +54,19 @@ The help string displayed in dfhack 'ls' command is the first line of the
script, if it is a comment (ie starts with '# ').
Calling dfhack commands
-----------------------
The ruby plugin allows the calling of arbitrary dfhack commands, as if typed
directly on the dfhack prompt.
However due to locks and stuff, the dfhack command is delayed until the current
ruby command is finished, so it is restricted to interactive uses.
It is possible to call the method many times, this will queue dfhack commands
to be run in order.
df.dfhack_run "reveal"
Ruby helper functions
---------------------
@ -107,7 +120,7 @@ eg 'gob' for 'GOBLIN' or 'coal' for 'COAL_BITUMINOUS', hence the name.
df.building_construct(bld, item_list)
Allocates a new building in DF memory, define its position / dimensions, and
create a dwarf job to construct it from the given list of items.
See buildings.rb/buildbed for an exemple.
See buildings.rb/buildbed for an example.
df.each_tree(material) { |t| }
Iterates over every tree of the given material (eg 'maple').
@ -137,13 +150,17 @@ To stop being called, use:
The same mechanism is available for 'onstatechange', but the
SC_BEGIN_UNLOAD event is not propagated to the ruby handler.
Available states:
:WORLD_LOADED, :WORLD_UNLOADED, :MAP_LOADED, :MAP_UNLOADED,
:VIEWSCREEN_CHANGED, :CORE_INITIALIZED, :PAUSED, :UNPAUSED
C++ object manipulation
-----------------------
The ruby classes defined in ruby-autogen.rb are accessors to the underlying
df C++ objects in-memory. To allocate a new C++ object for use in DF, use the
RubyClass.cpp_new method (see buildings.rb for exemples), works for Compounds
RubyClass.cpp_new method (see buildings.rb for examples), works for Compounds
only.
A special Compound DFHack::StlString is available for allocating a single c++
stl::string, so that you can call vmethods that take a string pointer argument
@ -186,10 +203,10 @@ Pointer fields are automatically dereferenced ; so a vector of pointer to
Units will yield Units directly. NULL pointers yield the 'nil' value.
Exemples
Examples
--------
For more complex exemples, check the dfhack/scripts/*.rb files.
For more complex examples, check the dfhack/scripts/*.rb files.
Show info on the currently selected unit ('v' or 'k' DF menu)
p df.unit_find.flags1
@ -240,7 +257,7 @@ which differ between Windows and Linux. Linux and Macosx are the same, as they
both use gcc).
It is stored inside the build directory (eg build/plugins/ruby/ruby-autogen.rb)
For exemple,
For example,
<ld:global-type ld:meta="struct-type" type-name="unit">
<ld:field type-name="language_name" name="name" ld:meta="global"/>
<ld:field name="custom_profession" ld:meta="primitive" ld:subtype="stl-string"/>

@ -48,6 +48,8 @@ module DFHack
raise "invalid building type #{type.inspect}" if not cls
bld = cls.cpp_new
bld.race = ui.race_id
subtype = ConstructionType.int(subtype) if subtype.kind_of?(::Symbol) and type == :Construction
subtype = SiegeengineType.int(subtype) if subtype.kind_of?(::Symbol) and type == :SiegeEngine
subtype = WorkshopType.int(subtype) if subtype.kind_of?(::Symbol) and type == :Workshop
subtype = FurnaceType.int(subtype) if subtype.kind_of?(::Symbol) and type == :Furnace
subtype = CivzoneType.int(subtype) if subtype.kind_of?(::Symbol) and type == :Civzone

@ -149,7 +149,21 @@ module DFHack
def vein
# last vein wins
all_veins.reverse.find { |v|
(v.tile_bitmask.bits[@dy] & (1 << @dx)) > 0
v.tile_bitmask.bits[@dy][@dx] > 0
}
end
# return the first BlockBurrow this tile is in (nil if none)
def burrow
mapblock.block_burrows.find { |b|
b.tile_bitmask.bits[@dy][@dx] > 0
}
end
# return the array of BlockBurrow this tile is in
def all_burrows
mapblock.block_burrows.find_all { |b|
b.tile_bitmask.bits[@dy][@dx] > 0
}
end

@ -122,7 +122,8 @@ module DFHack
_fields_ancestors.each { |n, o, s| s._at(@_memaddr+o)._cpp_init }
end
def _cpp_delete
_fields_ancestors.each { |n, o, s| s._at(@_memaddr+o)._cpp_delete }
# cannot call delete on compound members (would call free on member address)
#_fields_ancestors.each { |n, o, s| s._at(@_memaddr+o)._cpp_delete }
DFHack.free(@_memaddr)
@_memaddr = nil # turn future segfaults in harmless ruby exceptions
end
@ -642,7 +643,7 @@ module DFHack
@_tg = tg
end
# XXX DF uses stl::deque<some_struct>, so to have a C binding we'd need to single-case every
# possible struct size, like for StlVector. Just ignore it for now, deque are rare enough.
# possible struct size, like for StlVector. Just ignore it for now, deques are rare enough.
def inspect ; "#<StlDeque>" ; end
end
@ -676,7 +677,7 @@ module DFHack
def inspect
out = "#<DfFlagarray"
each_with_index { |e, idx|
out << " #{_indexenum.sym(idx)}" if e
out << " #{_indexenum ? _indexenum.sym(idx) : idx}" if e
}
out << '>'
end

@ -41,6 +41,7 @@ static tthread::thread *r_thread;
static int onupdate_active;
static int onupdate_minyear, onupdate_minyeartick=-1, onupdate_minyeartickadv=-1;
static color_ostream_proxy *console_proxy;
static std::vector<std::string> *dfhack_run_queue;
DFHACK_PLUGIN("ruby")
@ -63,6 +64,9 @@ DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <Plug
// lock this before anything, and release when everything is done
m_mutex = new tthread::mutex();
// list of dfhack commands to run when the current ruby run is done (once locks are released)
dfhack_run_queue = new std::vector<std::string>;
r_type = RB_INIT;
// create the dedicated ruby thread
@ -84,6 +88,10 @@ DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <Plug
"Ruby interpreter. Eval() a ruby string.",
df_rubyeval));
commands.push_back(PluginCommand("rb",
"Ruby interpreter. Eval() a ruby string (alias for rb_eval).",
df_rubyeval));
return CR_OK;
}
@ -111,6 +119,7 @@ DFhackCExport command_result plugin_shutdown ( color_ostream &out )
// we can release m_mutex, other users will check r_thread
m_mutex->unlock();
delete m_mutex;
delete dfhack_run_queue;
// dlclose libruby
df_unloadruby();
@ -152,6 +161,8 @@ static command_result do_plugin_eval_ruby(color_ostream &out, const char *comman
// send a single ruby line to be evaluated by the ruby thread
DFhackCExport command_result plugin_eval_ruby( color_ostream &out, const char *command)
{
command_result ret;
// if dlopen failed
if (!r_thread)
return CR_FAILURE;
@ -160,14 +171,24 @@ DFhackCExport command_result plugin_eval_ruby( color_ostream &out, const char *c
// debug only!
// run ruby commands without locking the main thread
// useful when the game is frozen after a segfault
return do_plugin_eval_ruby(out, command+7);
ret = do_plugin_eval_ruby(out, command+7);
} else {
// wrap all ruby code inside a suspend block
// if we dont do that and rely on ruby code doing it, we'll deadlock in
// onupdate
CoreSuspender suspend;
return do_plugin_eval_ruby(out, command);
ret = do_plugin_eval_ruby(out, command);
}
// if any dfhack command is queued for run, do it now
while (!dfhack_run_queue->empty()) {
std::string cmd = dfhack_run_queue->at(0);
// delete before running the command, which may be ruby and cause infinite loops
dfhack_run_queue->erase(dfhack_run_queue->begin());
Core::getInstance().runCommand(out, cmd);
}
return ret;
}
DFhackCExport command_result plugin_onupdate ( color_ostream &out )
@ -550,18 +571,10 @@ static VALUE rb_dfget_vtable_ptr(VALUE self, VALUE objptr)
// run a dfhack command, as if typed from the dfhack console
static VALUE rb_dfhack_run(VALUE self, VALUE cmd)
{
if (!r_console) // XXX
return Qnil;
std::string s;
int strlen = FIX2INT(rb_funcall(cmd, rb_intern("length"), 0));
s.assign(rb_string_value_ptr(&cmd), strlen);
// allow the target command to suspend
// FIXME
//CoreSuspendClaimer suspend(true);
Core::getInstance().runCommand(*r_console, s);
dfhack_run_queue->push_back(s);
return Qtrue;
}
@ -1045,7 +1058,7 @@ static void ruby_bind_dfhack(void) {
rb_define_singleton_method(rb_cDFHack, "get_vtable", RUBY_METHOD_FUNC(rb_dfget_vtable), 1);
rb_define_singleton_method(rb_cDFHack, "get_rtti_classname", RUBY_METHOD_FUNC(rb_dfget_rtti_classname), 1);
rb_define_singleton_method(rb_cDFHack, "get_vtable_ptr", RUBY_METHOD_FUNC(rb_dfget_vtable_ptr), 1);
//rb_define_singleton_method(rb_cDFHack, "dfhack_run", RUBY_METHOD_FUNC(rb_dfhack_run), 1);
rb_define_singleton_method(rb_cDFHack, "dfhack_run", RUBY_METHOD_FUNC(rb_dfhack_run), 1);
rb_define_singleton_method(rb_cDFHack, "print_str", RUBY_METHOD_FUNC(rb_dfprint_str), 1);
rb_define_singleton_method(rb_cDFHack, "print_err", RUBY_METHOD_FUNC(rb_dfprint_err), 1);
rb_define_singleton_method(rb_cDFHack, "malloc", RUBY_METHOD_FUNC(rb_dfmalloc), 1);

@ -1,176 +1,176 @@
class AutoFarm
def initialize
@thresholds = Hash.new(50)
@lastcounts = Hash.new(0)
end
def setthreshold(id, v)
list = df.world.raws.plants.all.find_all { |plt| plt.flags[:SEED] }.map { |plt| plt.id }
if tok = df.match_rawname(id, list)
@thresholds[tok] = v.to_i
else
puts "No plant with id #{id}, try one of " +
list.map { |w| w =~ /[^\w]/ ? w.inspect : w }.sort.join(' ')
end
end
def setdefault(v)
@thresholds.default = v.to_i
end
def is_plantable(plant)
has_seed = plant.flags[:SEED]
season = df.cur_season
harvest = df.cur_season_tick + plant.growdur * 10
will_finish = harvest < 10080
can_plant = has_seed && plant.flags[season]
can_plant = can_plant && (will_finish || plant.flags[(season+1)%4])
can_plant
end
def find_plantable_plants
plantable = {}
counts = Hash.new(0)
df.world.items.other[:SEEDS].each { |i|
if (!i.flags.dump && !i.flags.forbid && !i.flags.garbage_collect &&
!i.flags.hostile && !i.flags.on_fire && !i.flags.rotten &&
!i.flags.trader && !i.flags.in_building && !i.flags.construction &&
!i.flags.artifact)
counts[i.mat_index] += i.stack_size
end
}
counts.keys.each { |i|
if df.ui.tasks.known_plants[i]
plant = df.world.raws.plants.all[i]
if is_plantable(plant)
plantable[i] = :Surface if (plant.underground_depth_min == 0 || plant.underground_depth_max == 0)
plantable[i] = :Underground if (plant.underground_depth_min > 0 || plant.underground_depth_max > 0)
end
end
}
return plantable
end
def set_farms(plants, farms)
return if farms.length == 0
if plants.length == 0
plants = [-1]
end
season = df.cur_season
farms.each_with_index { |f, idx|
f.plant_id[season] = plants[idx % plants.length]
}
end
def process
plantable = find_plantable_plants
@lastcounts = Hash.new(0)
df.world.items.other[:PLANT].each { |i|
if (!i.flags.dump && !i.flags.forbid && !i.flags.garbage_collect &&
!i.flags.hostile && !i.flags.on_fire && !i.flags.rotten &&
!i.flags.trader && !i.flags.in_building && !i.flags.construction &&
!i.flags.artifact && plantable.has_key?(i.mat_index))
id = df.world.raws.plants.all[i.mat_index].id
@lastcounts[id] += i.stack_size
end
}
return unless @running
plants_s = []
plants_u = []
plantable.each_key { |k|
plant = df.world.raws.plants.all[k]
if (@lastcounts[plant.id] < @thresholds[plant.id])
plants_s.push(k) if plantable[k] == :Surface
plants_u.push(k) if plantable[k] == :Underground
end
}
farms_s = []
farms_u = []
df.world.buildings.other[:FARM_PLOT].each { |f|
if (f.flags.exists)
underground = df.map_designation_at(f.centerx,f.centery,f.z).subterranean
farms_s.push(f) unless underground
farms_u.push(f) if underground
end
}
set_farms(plants_s, farms_s)
set_farms(plants_u, farms_u)
end
def start
return if @running
@onupdate = df.onupdate_register('autofarm', 1200) { process }
@running = true
end
def stop
df.onupdate_unregister(@onupdate)
@running = false
end
def status
stat = @running ? "Running." : "Stopped."
@lastcounts.each { |k,v|
stat << "\n#{k} limit #{@thresholds.fetch(k, 'default')} current #{v}"
}
@thresholds.each { |k,v|
stat << "\n#{k} limit #{v} current 0" unless @lastcounts.has_key?(k)
}
stat << "\nDefault: #{@thresholds.default}"
stat
end
end
$AutoFarm ||= AutoFarm.new
case $script_args[0]
when 'start'
$AutoFarm.start
puts $AutoFarm.status
when 'end', 'stop', 'disable'
$AutoFarm.stop
puts 'Stopped.'
when 'default'
$AutoFarm.setdefault($script_args[1])
when 'threshold'
t = $script_args[1]
$script_args[2..-1].each {|i|
$AutoFarm.setthreshold(i, t)
}
when 'delete'
$AutoFarm.stop
$AutoFarm = nil
when 'help', '?'
puts <<EOS
Automatically handle crop selection in farm plots based on current plant stocks.
Selects a crop for planting if current stock is below a threshold.
Selected crops are dispatched on all farmplots.
Usage:
autofarm start
autofarm default 30
autofarm threshold 150 helmet_plump tail_pig
EOS
else
$AutoFarm.process
puts $AutoFarm.status
end
class AutoFarm
def initialize
@thresholds = Hash.new(50)
@lastcounts = Hash.new(0)
end
def setthreshold(id, v)
list = df.world.raws.plants.all.find_all { |plt| plt.flags[:SEED] }.map { |plt| plt.id }
if tok = df.match_rawname(id, list)
@thresholds[tok] = v.to_i
else
puts "No plant with id #{id}, try one of " +
list.map { |w| w =~ /[^\w]/ ? w.inspect : w }.sort.join(' ')
end
end
def setdefault(v)
@thresholds.default = v.to_i
end
def is_plantable(plant)
has_seed = plant.flags[:SEED]
season = df.cur_season
harvest = df.cur_season_tick + plant.growdur * 10
will_finish = harvest < 10080
can_plant = has_seed && plant.flags[season]
can_plant = can_plant && (will_finish || plant.flags[(season+1)%4])
can_plant
end
def find_plantable_plants
plantable = {}
counts = Hash.new(0)
df.world.items.other[:SEEDS].each { |i|
if (!i.flags.dump && !i.flags.forbid && !i.flags.garbage_collect &&
!i.flags.hostile && !i.flags.on_fire && !i.flags.rotten &&
!i.flags.trader && !i.flags.in_building && !i.flags.construction &&
!i.flags.artifact)
counts[i.mat_index] += i.stack_size
end
}
counts.keys.each { |i|
if df.ui.tasks.known_plants[i]
plant = df.world.raws.plants.all[i]
if is_plantable(plant)
plantable[i] = :Surface if (plant.underground_depth_min == 0 || plant.underground_depth_max == 0)
plantable[i] = :Underground if (plant.underground_depth_min > 0 || plant.underground_depth_max > 0)
end
end
}
return plantable
end
def set_farms(plants, farms)
return if farms.length == 0
if plants.length == 0
plants = [-1]
end
season = df.cur_season
farms.each_with_index { |f, idx|
f.plant_id[season] = plants[idx % plants.length]
}
end
def process
plantable = find_plantable_plants
@lastcounts = Hash.new(0)
df.world.items.other[:PLANT].each { |i|
if (!i.flags.dump && !i.flags.forbid && !i.flags.garbage_collect &&
!i.flags.hostile && !i.flags.on_fire && !i.flags.rotten &&
!i.flags.trader && !i.flags.in_building && !i.flags.construction &&
!i.flags.artifact && plantable.has_key?(i.mat_index))
id = df.world.raws.plants.all[i.mat_index].id
@lastcounts[id] += i.stack_size
end
}
return unless @running
plants_s = []
plants_u = []
plantable.each_key { |k|
plant = df.world.raws.plants.all[k]
if (@lastcounts[plant.id] < @thresholds[plant.id])
plants_s.push(k) if plantable[k] == :Surface
plants_u.push(k) if plantable[k] == :Underground
end
}
farms_s = []
farms_u = []
df.world.buildings.other[:FARM_PLOT].each { |f|
if (f.flags.exists)
underground = df.map_designation_at(f.centerx,f.centery,f.z).subterranean
farms_s.push(f) unless underground
farms_u.push(f) if underground
end
}
set_farms(plants_s, farms_s)
set_farms(plants_u, farms_u)
end
def start
return if @running
@onupdate = df.onupdate_register('autofarm', 1200) { process }
@running = true
end
def stop
df.onupdate_unregister(@onupdate)
@running = false
end
def status
stat = @running ? "Running." : "Stopped."
@lastcounts.each { |k,v|
stat << "\n#{k} limit #{@thresholds.fetch(k, 'default')} current #{v}"
}
@thresholds.each { |k,v|
stat << "\n#{k} limit #{v} current 0" unless @lastcounts.has_key?(k)
}
stat << "\nDefault: #{@thresholds.default}"
stat
end
end
$AutoFarm ||= AutoFarm.new
case $script_args[0]
when 'start', 'enable'
$AutoFarm.start
puts $AutoFarm.status
when 'end', 'stop', 'disable'
$AutoFarm.stop
puts 'Stopped.'
when 'default'
$AutoFarm.setdefault($script_args[1])
when 'threshold'
t = $script_args[1]
$script_args[2..-1].each {|i|
$AutoFarm.setthreshold(i, t)
}
when 'delete'
$AutoFarm.stop
$AutoFarm = nil
when 'help', '?'
puts <<EOS
Automatically handle crop selection in farm plots based on current plant stocks.
Selects a crop for planting if current stock is below a threshold.
Selected crops are dispatched on all farmplots.
Usage:
autofarm start
autofarm default 30
autofarm threshold 150 helmet_plump tail_pig
EOS
else
$AutoFarm.process
puts $AutoFarm.status
end

@ -1,58 +1,58 @@
class AutoUnsuspend
def initialize
end
def process
return false unless @running
joblist = df.world.job_list.next
count = 0
while joblist
job = joblist.item
joblist = joblist.next
if job.job_type == :ConstructBuilding
if (job.flags.suspend)
item = job.items[0].item
job.flags.suspend = false
count += 1
end
end
end
puts "Unsuspended #{count} job(s)." unless count == 0
end
def start
@onupdate = df.onupdate_register('autounsuspend', 5) { process }
@running = true
end
def stop
df.onupdate_unregister(@onupdate)
@running = false
end
def status
@running ? 'Running.' : 'Stopped.'
end
end
case $script_args[0]
when 'start'
$AutoUnsuspend = AutoUnsuspend.new unless $AutoUnsuspend
$AutoUnsuspend.start
when 'end', 'stop'
$AutoUnsuspend.stop
else
if $AutoUnsuspend
puts $AutoUnsuspend.status
else
puts 'Not loaded.'
end
end
class AutoUnsuspend
def initialize
end
def process
return false unless @running
joblist = df.world.job_list.next
count = 0
while joblist
job = joblist.item
joblist = joblist.next
if job.job_type == :ConstructBuilding
if (job.flags.suspend)
item = job.items[0].item
job.flags.suspend = false
count += 1
end
end
end
puts "Unsuspended #{count} job(s)." unless count == 0
end
def start
@onupdate = df.onupdate_register('autounsuspend', 5) { process }
@running = true
end
def stop
df.onupdate_unregister(@onupdate)
@running = false
end
def status
@running ? 'Running.' : 'Stopped.'
end
end
case $script_args[0]
when 'start'
$AutoUnsuspend = AutoUnsuspend.new unless $AutoUnsuspend
$AutoUnsuspend.start
when 'end', 'stop'
$AutoUnsuspend.stop
else
if $AutoUnsuspend
puts $AutoUnsuspend.status
else
puts 'Not loaded.'
end
end

@ -1,177 +1,177 @@
# create first necessity items under cursor
category = $script_args[0] || 'help'
mat_raw = $script_args[1] || 'list'
count = $script_args[2]
category = df.match_rawname(category, ['help', 'bars', 'boulders', 'plants', 'logs', 'webs', 'anvils']) || 'help'
if category == 'help'
puts <<EOS
Create first necessity items under the cursor.
Usage:
create-items [category] [raws token] [number]
Item categories:
bars, boulders, plants, logs, webs, anvils
Raw token:
Either a full token (PLANT_MAT:ADLER:WOOD) or the middle part only
(the missing part is autocompleted depending on the item category)
Use 'list' to show all possibilities
Exemples:
create-items boulders hematite 30
create-items bars CREATURE_MAT:CAT:SOAP 10
create-items web cave_giant
create-items plants list
EOS
throw :script_finished
elsif mat_raw == 'list'
# allowed with no cursor
elsif df.cursor.x == -30000
puts "Please place the game cursor somewhere"
throw :script_finished
elsif !(maptile = df.map_tile_at(df.cursor))
puts "Error: unallocated map block !"
throw :script_finished
elsif !maptile.shape_passablehigh
puts "Error: impassible tile !"
throw :script_finished
end
def match_list(tok, list)
if tok != 'list'
tok = df.match_rawname(tok, list)
if not tok
puts "Invalid raws token, use one in:"
tok = 'list'
end
end
if tok == 'list'
puts list.map { |w| w =~ /[^\w]/ ? w.inspect : w }.join(' ')
throw :script_finished
end
tok
end
case category
when 'bars'
# create metal bar, eg createbar INORGANIC:IRON
cls = DFHack::ItemBarst
if mat_raw !~ /:/ and !(df.decode_mat(mat_raw) rescue nil)
list = df.world.raws.inorganics.find_all { |ino|
ino.material.flags[:IS_METAL]
}.map { |ino| ino.id }
mat_raw = match_list(mat_raw, list)
mat_raw = "INORGANIC:#{mat_raw}"
puts mat_raw
end
customize = lambda { |item|
item.dimension = 150
item.subtype = -1
}
when 'boulders'
cls = DFHack::ItemBoulderst
if mat_raw !~ /:/ and !(df.decode_mat(mat_raw) rescue nil)
list = df.world.raws.inorganics.find_all { |ino|
ino.material.flags[:IS_STONE]
}.map { |ino| ino.id }
mat_raw = match_list(mat_raw, list)
mat_raw = "INORGANIC:#{mat_raw}"
puts mat_raw
end
when 'plants'
cls = DFHack::ItemPlantst
if mat_raw !~ /:/ and !(df.decode_mat(mat_raw) rescue nil)
list = df.world.raws.plants.all.find_all { |plt|
plt.material.find { |mat| mat.id == 'STRUCTURAL' }
}.map { |plt| plt.id }
mat_raw = match_list(mat_raw, list)
mat_raw = "PLANT_MAT:#{mat_raw}:STRUCTURAL"
puts mat_raw
end
when 'logs'
cls = DFHack::ItemWoodst
if mat_raw !~ /:/ and !(df.decode_mat(mat_raw) rescue nil)
list = df.world.raws.plants.all.find_all { |plt|
plt.material.find { |mat| mat.id == 'WOOD' }
}.map { |plt| plt.id }
mat_raw = match_list(mat_raw, list)
mat_raw = "PLANT_MAT:#{mat_raw}:WOOD"
puts mat_raw
end
when 'webs'
cls = DFHack::ItemThreadst
if mat_raw !~ /:/ and !(df.decode_mat(mat_raw) rescue nil)
list = df.world.raws.creatures.all.find_all { |cre|
cre.material.find { |mat| mat.id == 'SILK' }
}.map { |cre| cre.creature_id }
mat_raw = match_list(mat_raw, list)
mat_raw = "CREATURE_MAT:#{mat_raw}:SILK"
puts mat_raw
end
count ||= 1
customize = lambda { |item|
item.flags.spider_web = true
item.dimension = 15000 # XXX may depend on creature (this is for GCS)
}
when 'anvils'
cls = DFHack::ItemAnvilst
if mat_raw !~ /:/ and !(df.decode_mat(mat_raw) rescue nil)
list = df.world.raws.inorganics.find_all { |ino|
ino.material.flags[:IS_METAL]
}.map { |ino| ino.id }
mat_raw = match_list(mat_raw, list)
mat_raw = "INORGANIC:#{mat_raw}"
puts mat_raw
end
count ||= 1
end
mat = df.decode_mat mat_raw
count ||= 20
count.to_i.times {
item = cls.cpp_new
item.id = df.item_next_id
item.stack_size = 1
item.mat_type = mat.mat_type
item.mat_index = mat.mat_index
customize[item] if customize
df.item_next_id += 1
item.categorize(true)
df.world.items.all << item
item.pos = df.cursor
item.flags.on_ground = true
df.map_tile_at.mapblock.items << item.id
df.map_tile_at.occupancy.item = true
}
# move game view, so that the ui menu updates
if df.cursor.z > 5
df.curview.feed_keys(:CURSOR_DOWN_Z)
df.curview.feed_keys(:CURSOR_UP_Z)
else
df.curview.feed_keys(:CURSOR_UP_Z)
df.curview.feed_keys(:CURSOR_DOWN_Z)
end
# create first necessity items under cursor
category = $script_args[0] || 'help'
mat_raw = $script_args[1] || 'list'
count = $script_args[2]
category = df.match_rawname(category, ['help', 'bars', 'boulders', 'plants', 'logs', 'webs', 'anvils']) || 'help'
if category == 'help'
puts <<EOS
Create first necessity items under the cursor.
Usage:
create-items [category] [raws token] [number]
Item categories:
bars, boulders, plants, logs, webs, anvils
Raw token:
Either a full token (PLANT_MAT:ADLER:WOOD) or the middle part only
(the missing part is autocompleted depending on the item category)
Use 'list' to show all possibilities
Exemples:
create-items boulders hematite 30
create-items bars CREATURE_MAT:CAT:SOAP 10
create-items web cave_giant
create-items plants list
EOS
throw :script_finished
elsif mat_raw == 'list'
# allowed with no cursor
elsif df.cursor.x == -30000
puts "Please place the game cursor somewhere"
throw :script_finished
elsif !(maptile = df.map_tile_at(df.cursor))
puts "Error: unallocated map block !"
throw :script_finished
elsif !maptile.shape_passablehigh
puts "Error: impassible tile !"
throw :script_finished
end
def match_list(tok, list)
if tok != 'list'
tok = df.match_rawname(tok, list)
if not tok
puts "Invalid raws token, use one in:"
tok = 'list'
end
end
if tok == 'list'
puts list.map { |w| w =~ /[^\w]/ ? w.inspect : w }.join(' ')
throw :script_finished
end
tok
end
case category
when 'bars'
# create metal bar, eg createbar INORGANIC:IRON
cls = DFHack::ItemBarst
if mat_raw !~ /:/ and !(df.decode_mat(mat_raw) rescue nil)
list = df.world.raws.inorganics.find_all { |ino|
ino.material.flags[:IS_METAL]
}.map { |ino| ino.id }
mat_raw = match_list(mat_raw, list)
mat_raw = "INORGANIC:#{mat_raw}"
puts mat_raw
end
customize = lambda { |item|
item.dimension = 150
item.subtype = -1
}
when 'boulders'
cls = DFHack::ItemBoulderst
if mat_raw !~ /:/ and !(df.decode_mat(mat_raw) rescue nil)
list = df.world.raws.inorganics.find_all { |ino|
ino.material.flags[:IS_STONE]
}.map { |ino| ino.id }
mat_raw = match_list(mat_raw, list)
mat_raw = "INORGANIC:#{mat_raw}"
puts mat_raw
end
when 'plants'
cls = DFHack::ItemPlantst
if mat_raw !~ /:/ and !(df.decode_mat(mat_raw) rescue nil)
list = df.world.raws.plants.all.find_all { |plt|
plt.material.find { |mat| mat.id == 'STRUCTURAL' }
}.map { |plt| plt.id }
mat_raw = match_list(mat_raw, list)
mat_raw = "PLANT_MAT:#{mat_raw}:STRUCTURAL"
puts mat_raw
end
when 'logs'
cls = DFHack::ItemWoodst
if mat_raw !~ /:/ and !(df.decode_mat(mat_raw) rescue nil)
list = df.world.raws.plants.all.find_all { |plt|
plt.material.find { |mat| mat.id == 'WOOD' }
}.map { |plt| plt.id }
mat_raw = match_list(mat_raw, list)
mat_raw = "PLANT_MAT:#{mat_raw}:WOOD"
puts mat_raw
end
when 'webs'
cls = DFHack::ItemThreadst
if mat_raw !~ /:/ and !(df.decode_mat(mat_raw) rescue nil)
list = df.world.raws.creatures.all.find_all { |cre|
cre.material.find { |mat| mat.id == 'SILK' }
}.map { |cre| cre.creature_id }
mat_raw = match_list(mat_raw, list)
mat_raw = "CREATURE_MAT:#{mat_raw}:SILK"
puts mat_raw
end
count ||= 1
customize = lambda { |item|
item.flags.spider_web = true
item.dimension = 15000 # XXX may depend on creature (this is for GCS)
}
when 'anvils'
cls = DFHack::ItemAnvilst
if mat_raw !~ /:/ and !(df.decode_mat(mat_raw) rescue nil)
list = df.world.raws.inorganics.find_all { |ino|
ino.material.flags[:IS_METAL]
}.map { |ino| ino.id }
mat_raw = match_list(mat_raw, list)
mat_raw = "INORGANIC:#{mat_raw}"
puts mat_raw
end
count ||= 1
end
mat = df.decode_mat mat_raw
count ||= 20
count.to_i.times {
item = cls.cpp_new
item.id = df.item_next_id
item.stack_size = 1
item.mat_type = mat.mat_type
item.mat_index = mat.mat_index
customize[item] if customize
df.item_next_id += 1
item.categorize(true)
df.world.items.all << item
item.pos = df.cursor
item.flags.on_ground = true
df.map_tile_at.mapblock.items << item.id
df.map_tile_at.occupancy.item = true
}
# move game view, so that the ui menu updates
if df.cursor.z > 5
df.curview.feed_keys(:CURSOR_DOWN_Z)
df.curview.feed_keys(:CURSOR_UP_Z)
else
df.curview.feed_keys(:CURSOR_UP_Z)
df.curview.feed_keys(:CURSOR_DOWN_Z)
end

@ -1,67 +1,67 @@
# show death cause of a creature
def display_death_event(e)
str = "The #{e.victim_hf_tg.race_tg.name[0]} #{e.victim_hf_tg.name} died in year #{e.year}"
str << " (cause: #{e.death_cause.to_s.downcase}),"
str << " killed by the #{e.slayer_race_tg.name[0]} #{e.slayer_hf_tg.name}" if e.slayer_hf != -1
str << " using a #{df.world.raws.itemdefs.weapons[e.weapon.item_subtype].name}" if e.weapon.item_type == :WEAPON
str << ", shot by a #{df.world.raws.itemdefs.weapons[e.weapon.bow_item_subtype].name}" if e.weapon.bow_item_type == :WEAPON
puts str.chomp(',') + '.'
end
def display_death_unit(u)
death_info = u.counters.death_tg
killer = death_info.killer_tg if death_info
str = "The #{u.race_tg.name[0]}"
str << " #{u.name}" if u.name.has_name
str << " died"
str << " in year #{death_info.event_year}" if death_info
str << " (cause: #{u.counters.death_cause.to_s.downcase})," if u.counters.death_cause != -1
str << " killed by the #{killer.race_tg.name[0]} #{killer.name}" if killer
puts str.chomp(',') + '.'
end
item = df.item_find(:selected)
unit = df.unit_find(:selected)
if !item or !item.kind_of?(DFHack::ItemBodyComponent)
item = df.world.items.other[:ANY_CORPSE].find { |i| df.at_cursor?(i) }
end
if item and item.kind_of?(DFHack::ItemBodyComponent)
hf = item.hist_figure_id
elsif unit
hf = unit.hist_figure_id
end
if not hf
puts "Please select a corpse in the loo'k' menu, or an unit in the 'u'nitlist screen"
elsif hf == -1
if unit ||= item.unit_tg
display_death_unit(unit)
else
puts "Not a historical figure, cannot death find info"
end
else
histfig = df.world.history.figures.binsearch(hf)
unit = histfig ? df.unit_find(histfig.unit_id) : nil
if unit and not unit.flags1.dead and not unit.flags3.ghostly
puts "#{unit.name} is not dead yet !"
else
events = df.world.history.events
(0...events.length).reverse_each { |i|
e = events[i]
if e.kind_of?(DFHack::HistoryEventHistFigureDiedst) and e.victim_hf == hf
display_death_event(e)
break
end
}
end
end
# show death cause of a creature
def display_death_event(e)
str = "The #{e.victim_hf_tg.race_tg.name[0]} #{e.victim_hf_tg.name} died in year #{e.year}"
str << " (cause: #{e.death_cause.to_s.downcase}),"
str << " killed by the #{e.slayer_race_tg.name[0]} #{e.slayer_hf_tg.name}" if e.slayer_hf != -1
str << " using a #{df.world.raws.itemdefs.weapons[e.weapon.item_subtype].name}" if e.weapon.item_type == :WEAPON
str << ", shot by a #{df.world.raws.itemdefs.weapons[e.weapon.bow_item_subtype].name}" if e.weapon.bow_item_type == :WEAPON
puts str.chomp(',') + '.'
end
def display_death_unit(u)
death_info = u.counters.death_tg
killer = death_info.killer_tg if death_info
str = "The #{u.race_tg.name[0]}"
str << " #{u.name}" if u.name.has_name
str << " died"
str << " in year #{death_info.event_year}" if death_info
str << " (cause: #{u.counters.death_cause.to_s.downcase})," if u.counters.death_cause != -1
str << " killed by the #{killer.race_tg.name[0]} #{killer.name}" if killer
puts str.chomp(',') + '.'
end
item = df.item_find(:selected)
unit = df.unit_find(:selected)
if !item or !item.kind_of?(DFHack::ItemBodyComponent)
item = df.world.items.other[:ANY_CORPSE].find { |i| df.at_cursor?(i) }
end
if item and item.kind_of?(DFHack::ItemBodyComponent)
hf = item.hist_figure_id
elsif unit
hf = unit.hist_figure_id
end
if not hf
puts "Please select a corpse in the loo'k' menu, or an unit in the 'u'nitlist screen"
elsif hf == -1
if unit ||= item.unit_tg
display_death_unit(unit)
else
puts "Not a historical figure, cannot death find info"
end
else
histfig = df.world.history.figures.binsearch(hf)
unit = histfig ? df.unit_find(histfig.unit_id) : nil
if unit and not unit.flags1.dead and not unit.flags3.ghostly
puts "#{unit.name} is not dead yet !"
else
events = df.world.history.events
(0...events.length).reverse_each { |i|
e = events[i]
if e.kind_of?(DFHack::HistoryEventHistFigureDiedst) and e.victim_hf == hf
display_death_event(e)
break
end
}
end
end

@ -0,0 +1,10 @@
# list indexes in world.item.other[] where current selected item appears
tg = df.item_find
raise 'select an item' if not tg
o = df.world.items.other
# discard ANY/BAD
o._indexenum::ENUM.sort.transpose[1][1..-2].each { |k|
puts k if o[k].find { |i| i == tg }
}

@ -0,0 +1,3 @@
# unforbid all items
df.world.items.all.each { |i| i.flags.forbid = false }

@ -1,38 +1,38 @@
# designate an area for digging according to a plan in csv format
raise "usage: digfort <plan filename>" if not $script_args[0]
planfile = File.read($script_args[0])
if df.cursor.x == -30000
raise "place the game cursor to the top-left corner of the design"
end
tiles = planfile.lines.map { |l|
l.sub(/#.*/, '').split(';').map { |t| t.strip }
}
x = x0 = df.cursor.x
y = df.cursor.y
z = df.cursor.z
tiles.each { |line|
next if line.empty? or line == ['']
line.each { |tile|
t = df.map_tile_at(x, y, z)
s = t.shape_basic
case tile
when 'd'; t.dig(:Default) if s == :Wall
when 'u'; t.dig(:UpStair) if s == :Wall
when 'j'; t.dig(:DownStair) if s == :Wall or s == :Floor
when 'i'; t.dig(:UpDownStair) if s == :Wall
when 'h'; t.dig(:Channel) if s == :Wall or s == :Floor
when 'r'; t.dig(:Ramp) if s == :Wall
when 'x'; t.dig(:No)
end
x += 1
}
x = x0
y += 1
}
puts 'done'
# designate an area for digging according to a plan in csv format
raise "usage: digfort <plan filename>" if not $script_args[0]
planfile = File.read($script_args[0])
if df.cursor.x == -30000
raise "place the game cursor to the top-left corner of the design"
end
tiles = planfile.lines.map { |l|
l.sub(/#.*/, '').split(';').map { |t| t.strip }
}
x = x0 = df.cursor.x
y = df.cursor.y
z = df.cursor.z
tiles.each { |line|
next if line.empty? or line == ['']
line.each { |tile|
t = df.map_tile_at(x, y, z)
s = t.shape_basic
case tile
when 'd'; t.dig(:Default) if s == :Wall
when 'u'; t.dig(:UpStair) if s == :Wall
when 'j'; t.dig(:DownStair) if s == :Wall or s == :Floor
when 'i'; t.dig(:UpDownStair) if s == :Wall
when 'h'; t.dig(:Channel) if s == :Wall or s == :Floor
when 'r'; t.dig(:Ramp) if s == :Wall
when 'x'; t.dig(:No)
end
x += 1
}
x = x0
y += 1
}
puts 'done'

@ -1,11 +1,13 @@
# remove all aquifers from the map
count = 0
df.each_map_block { |b|
if b.designation[0][0].water_table or b.designation[15][15].water_table
count += 1
b.designation.each { |dx| dx.each { |dy| dy.water_table = false } }
end
}
puts "cleared #{count} map blocks"
# remove all aquifers from the map
count = 0
df.each_map_block { |b|
if b.designation[0][0].water_table or b.designation[8][8].water_table
count += 1
df.each_map_block_z(b.map_pos.z) { |bz|
bz.designation.each { |dx| dx.each { |dy| dy.water_table = false } }
}
end
}
puts "cleared #{count} aquifer#{'s' if count > 1}"

@ -1,112 +1,135 @@
# exterminate creatures
# 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 or u.body.blood_max == 0) and
not u.flags1.dead 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
}
slayit = lambda { |u|
if not magma
# just make them drop dead
u.body.blood_count = 0
# some races dont mind having no blood, ensure they are still taken care of.
u.animal.vanish_countdown = 2
else
# it's getting hot around here
# !!WARNING!! do not call on a magma-safe creature
ouh = df.onupdate_register("exterminate ensure #{u.id}", 1) {
if u.flags1.dead
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 and tile.shape_passablelow
df.map_tile_at(x, y, z).spawn_magma(7)
end
}
end
}
all_races = Hash.new(0)
df.world.units.active.map { |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
}
case race
when nil
all_races.sort_by { |race, cnt| [cnt, race] }.each{ |race, cnt| puts " #{race} #{cnt}" }
when 'help', '?'
puts <<EOS
Kills all creatures of a given race.
With no argument, lists possible targets with their head count.
With the special argument 'him' or 'her', kill only the currently selected creature.
With the special argument 'undead', kill all undead creatures/thralls.
The targets will bleed out on the next game tick, or if they are immune to that, will vanish in a puff of smoke.
The special final argument 'magma' will make magma rain on the targets instead.
Ex: exterminate gob
exterminate elve magma
exterminate him
EOS
when 'him', 'her'
if him = df.unit_find
slayit[him]
else
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)
if not raw_race
puts "Invalid race, use one of #{all_races.keys.sort.join(' ')}"
throw :script_finished
end
race_nr = df.world.raws.creatures.all.index { |cr| cr.creature_id == raw_race }
count = 0
df.world.units.active.each { |u|
if u.race == race_nr and checkunit[u]
slayit[u]
count += 1
end
}
puts "slain #{count} #{raw_race}"
end
# exterminate creatures
# 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
# if it is 'butcher' mark all units for butchering (wont work with hostiles)
kill_by = $script_args[1]
case kill_by
when 'magma'
slain = 'burning'
when 'slaughter', 'butcher'
slain = 'marked for butcher'
when nil
slain = 'slain'
else
race = 'help'
end
checkunit = lambda { |u|
(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.chained and
#not u.flags1.hidden_in_ambush and
not df.map_designation_at(u).hidden
}
slayit = lambda { |u|
case kill_by
when 'magma'
# it's getting hot around here
# !!WARNING!! do not call on a magma-safe creature
ouh = df.onupdate_register("exterminate ensure #{u.id}", 1) {
if u.flags1.dead
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 and tile.shape_passablelow
df.map_tile_at(x, y, z).spawn_magma(7)
end
}
when 'butcher', 'slaughter'
# mark for slaughter at butcher's shop
u.flags2.slaughter = true
else
# just make them drop dead
u.body.blood_count = 0
# some races dont mind having no blood, ensure they are still taken care of.
u.animal.vanish_countdown = 2
end
}
all_races = Hash.new(0)
df.world.units.active.map { |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
}
case race
when nil
all_races.sort_by { |race, cnt| [cnt, race] }.each{ |race, cnt| puts " #{race} #{cnt}" }
when 'help', '?'
puts <<EOS
Kills all creatures of a given race.
With no argument, lists possible targets with their head count.
With the special argument 'him' or 'her', kill only the currently selected creature.
With the special argument 'undead', kill all undead creatures/thralls.
The targets will bleed out on the next game tick, or if they are immune to that, will vanish in a puff of smoke.
The special final argument 'magma' will make magma rain on the targets instead.
The special final argument 'butcher' will mark the targets for butchering instead.
Ex: exterminate gob
exterminate elve magma
exterminate him
exterminate pig butcher
EOS
when 'him', 'her', 'it', 'that'
if him = df.unit_find
case him.race_tg.caste[him.caste].gender
when 0; puts 'its a she !' if race != 'her'
when 1; puts 'its a he !' if race != 'him'
else; puts 'its an it !' if race != 'it' and race != 'that'
end
slayit[him]
else
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)
if not raw_race
puts "Invalid race, use one of #{all_races.keys.sort.join(' ')}"
throw :script_finished
end
race_nr = df.world.raws.creatures.all.index { |cr| cr.creature_id == raw_race }
count = 0
df.world.units.active.each { |u|
if u.race == race_nr and checkunit[u]
slayit[u]
count += 1
end
}
puts "#{slain} #{count} #{raw_race}"
end

@ -1,64 +1,64 @@
# script to fix loyalty cascade, when you order your militia to kill friendly units
def fixunit(unit)
return if unit.race != df.ui.race_id or unit.civ_id != df.ui.civ_id
links = unit.hist_figure_tg.entity_links
fixed = false
# check if the unit is a civ renegade
if i1 = links.index { |l|
l.kind_of?(DFHack::HistfigEntityLinkFormerMemberst) and
l.entity_id == df.ui.civ_id
} and i2 = links.index { |l|
l.kind_of?(DFHack::HistfigEntityLinkEnemyst) and
l.entity_id == df.ui.civ_id
}
fixed = true
i1, i2 = i2, i1 if i1 > i2
links.delete_at i2
links.delete_at i1
links << DFHack::HistfigEntityLinkMemberst.cpp_new(:entity_id => df.ui.civ_id, :link_strength => 100)
df.add_announcement "fixloyalty: #{unit.name} is now a member of #{df.ui.civ_tg.name} again"
end
# check if the unit is a group renegade
if i1 = links.index { |l|
l.kind_of?(DFHack::HistfigEntityLinkFormerMemberst) and
l.entity_id == df.ui.group_id
} and i2 = links.index { |l|
l.kind_of?(DFHack::HistfigEntityLinkEnemyst) and
l.entity_id == df.ui.group_id
}
fixed = true
i1, i2 = i2, i1 if i1 > i2
links.delete_at i2
links.delete_at i1
links << DFHack::HistfigEntityLinkMemberst.cpp_new(:entity_id => df.ui.group_id, :link_strength => 100)
df.add_announcement "fixloyalty: #{unit.name} is now a member of #{df.ui.group_tg.name} again"
end
# fix the 'is an enemy' cache matrix (mark to be recalculated by the game when needed)
if fixed and unit.enemy.enemy_status_slot != -1
i = unit.enemy.enemy_status_slot
unit.enemy.enemy_status_slot = -1
cache = df.world.enemy_status_cache
cache.slot_used[i] = false
cache.rel_map[i].map! { -1 }
cache.rel_map.each { |a| a[i] = -1 }
cache.next_slot = i if cache.next_slot > i
end
# return true if we actually fixed the unit
fixed
end
count = 0
df.unit_citizens.each { |u|
count += 1 if fixunit(u)
}
if count > 0
puts "loyalty cascade fixed (#{count} dwarves)"
else
puts "no loyalty cascade found"
end
# script to fix loyalty cascade, when you order your militia to kill friendly units
def fixunit(unit)
return if unit.race != df.ui.race_id or unit.civ_id != df.ui.civ_id
links = unit.hist_figure_tg.entity_links
fixed = false
# check if the unit is a civ renegade
if i1 = links.index { |l|
l.kind_of?(DFHack::HistfigEntityLinkFormerMemberst) and
l.entity_id == df.ui.civ_id
} and i2 = links.index { |l|
l.kind_of?(DFHack::HistfigEntityLinkEnemyst) and
l.entity_id == df.ui.civ_id
}
fixed = true
i1, i2 = i2, i1 if i1 > i2
links.delete_at i2
links.delete_at i1
links << DFHack::HistfigEntityLinkMemberst.cpp_new(:entity_id => df.ui.civ_id, :link_strength => 100)
df.add_announcement "fixloyalty: #{unit.name} is now a member of #{df.ui.civ_tg.name} again"
end
# check if the unit is a group renegade
if i1 = links.index { |l|
l.kind_of?(DFHack::HistfigEntityLinkFormerMemberst) and
l.entity_id == df.ui.group_id
} and i2 = links.index { |l|
l.kind_of?(DFHack::HistfigEntityLinkEnemyst) and
l.entity_id == df.ui.group_id
}
fixed = true
i1, i2 = i2, i1 if i1 > i2
links.delete_at i2
links.delete_at i1
links << DFHack::HistfigEntityLinkMemberst.cpp_new(:entity_id => df.ui.group_id, :link_strength => 100)
df.add_announcement "fixloyalty: #{unit.name} is now a member of #{df.ui.group_tg.name} again"
end
# fix the 'is an enemy' cache matrix (mark to be recalculated by the game when needed)
if fixed and unit.enemy.enemy_status_slot != -1
i = unit.enemy.enemy_status_slot
unit.enemy.enemy_status_slot = -1
cache = df.world.enemy_status_cache
cache.slot_used[i] = false
cache.rel_map[i].map! { -1 }
cache.rel_map.each { |a| a[i] = -1 }
cache.next_slot = i if cache.next_slot > i
end
# return true if we actually fixed the unit
fixed
end
count = 0
df.unit_citizens.each { |u|
count += 1 if fixunit(u)
}
if count > 0
puts "loyalty cascade fixed (#{count} dwarves)"
else
puts "no loyalty cascade found"
end

@ -1,20 +1,25 @@
# fix doors that are frozen in 'open' state
# door is stuck in open state if the map occupancy flag incorrectly indicates
# that an unit is present (and creatures will prone to pass through)
count = 0
df.world.buildings.all.each { |bld|
# for all doors
next if bld._rtti_classname != :building_doorst
# check if it is open
next if bld.close_timer == 0
# check if occupancy is set
occ = df.map_occupancy_at(bld.x1, bld.y1, bld.z)
next if not occ.unit
# check if an unit is present
next if df.world.units.active.find { |u| u.pos.x == bld.x1 and u.pos.y == bld.y1 and u.pos.z == bld.z }
count += 1
occ.unit = false
}
puts "unstuck #{count} doors"
# fix doors that are frozen in 'open' state
# this may happen after people mess with the game by (incorrectly) teleporting units or items
# a door may stick open if the map occupancy flags are wrong
count = 0
df.world.buildings.all.each { |bld|
# for all doors
next if bld._rtti_classname != :building_doorst
# check if it is open
next if bld.close_timer == 0
# check if occupancy is set
occ = df.map_occupancy_at(bld.x1, bld.y1, bld.z)
if (occ.unit or occ.unit_grounded) and not
# check if an unit is present
df.world.units.active.find { |u| u.pos.x == bld.x1 and u.pos.y == bld.y1 and u.pos.z == bld.z }
count += 1
occ.unit = occ.unit_grounded = false
end
if occ.item and not df.world.items.all.find { |i| i.pos.x == bld.x1 and i.pos.y == bld.y1 and i.pos.z == bld.z }
count += 1
occ.item = false
end
}
puts "unstuck #{count} doors"

@ -1,49 +1,49 @@
# grow crops in farm plots. ex: growcrops helmet_plump 20
material = $script_args[0]
count_max = $script_args[1].to_i
count_max = 100 if count_max == 0
# cache information from the raws
@raws_plant_name ||= {}
@raws_plant_growdur ||= {}
if @raws_plant_name.empty?
df.world.raws.plants.all.each_with_index { |p, idx|
@raws_plant_name[idx] = p.id
@raws_plant_growdur[idx] = p.growdur
}
end
inventory = Hash.new(0)
df.world.items.other[:SEEDS].each { |seed|
next if not seed.flags.in_building
next if not seed.general_refs.find { |ref| ref._rtti_classname == :general_ref_building_holderst }
next if seed.grow_counter >= @raws_plant_growdur[seed.mat_index]
inventory[seed.mat_index] += 1
}
if !material or material == 'help' or material == 'list'
# show a list of available crop types
inventory.sort_by { |mat, c| c }.each { |mat, c|
name = df.world.raws.plants.all[mat].id
puts " #{name} #{c}"
}
else
mat = df.match_rawname(material, inventory.keys.map { |k| @raws_plant_name[k] })
unless wantmat = @raws_plant_name.index(mat)
raise "invalid plant material #{material}"
end
count = 0
df.world.items.other[:SEEDS].each { |seed|
next if seed.mat_index != wantmat
next if not seed.flags.in_building
next if not seed.general_refs.find { |ref| ref._rtti_classname == :general_ref_building_holderst }
next if seed.grow_counter >= @raws_plant_growdur[seed.mat_index]
seed.grow_counter = @raws_plant_growdur[seed.mat_index]
count += 1
}
puts "Grown #{count} #{mat}"
end
# grow crops in farm plots. ex: growcrops helmet_plump 20
material = $script_args[0]
count_max = $script_args[1].to_i
count_max = 100 if count_max == 0
# cache information from the raws
@raws_plant_name ||= {}
@raws_plant_growdur ||= {}
if @raws_plant_name.empty?
df.world.raws.plants.all.each_with_index { |p, idx|
@raws_plant_name[idx] = p.id
@raws_plant_growdur[idx] = p.growdur
}
end
inventory = Hash.new(0)
df.world.items.other[:SEEDS].each { |seed|
next if not seed.flags.in_building
next if not seed.general_refs.find { |ref| ref._rtti_classname == :general_ref_building_holderst }
next if seed.grow_counter >= @raws_plant_growdur[seed.mat_index]
inventory[seed.mat_index] += 1
}
if !material or material == 'help' or material == 'list'
# show a list of available crop types
inventory.sort_by { |mat, c| c }.each { |mat, c|
name = df.world.raws.plants.all[mat].id
puts " #{name} #{c}"
}
else
mat = df.match_rawname(material, inventory.keys.map { |k| @raws_plant_name[k] })
unless wantmat = @raws_plant_name.index(mat)
raise "invalid plant material #{material}"
end
count = 0
df.world.items.other[:SEEDS].each { |seed|
next if seed.mat_index != wantmat
next if not seed.flags.in_building
next if not seed.general_refs.find { |ref| ref._rtti_classname == :general_ref_building_holderst }
next if seed.grow_counter >= @raws_plant_growdur[seed.mat_index]
seed.grow_counter = @raws_plant_growdur[seed.mat_index]
count += 1
}
puts "Grown #{count} #{mat}"
end

@ -1,120 +1,120 @@
# control your levers from the dfhack console
def lever_pull_job(bld)
ref = DFHack::GeneralRefBuildingHolderst.cpp_new
ref.building_id = bld.id
job = DFHack::Job.cpp_new
job.job_type = :PullLever
job.pos = [bld.centerx, bld.centery, bld.z]
job.general_refs << ref
bld.jobs << job
df.job_link job
puts lever_descr(bld)
end
def lever_pull_cheat(bld)
bld.linked_mechanisms.each { |i|
i.general_refs.grep(DFHack::GeneralRefBuildingHolderst).each { |r|
r.building_tg.setTriggerState(bld.state)
}
}
bld.state = (bld.state == 0 ? 1 : 0)
puts lever_descr(bld)
end
def lever_descr(bld, idx=nil)
ret = []
# lever description
descr = ''
descr << "#{idx}: " if idx
descr << "lever ##{bld.id} @[#{bld.centerx}, #{bld.centery}, #{bld.z}] #{bld.state == 0 ? '\\' : '/'}"
bld.jobs.each { |j|
if j.job_type == :PullLever
flags = ''
flags << ', repeat' if j.flags.repeat
flags << ', suspended' if j.flags.suspend
descr << " (pull order#{flags})"
end
}
bld.linked_mechanisms.map { |i|
i.general_refs.grep(DFHack::GeneralRefBuildingHolderst)
}.flatten.each { |r|
# linked building description
tg = r.building_tg
state = ''
if tg.respond_to?(:gate_flags)
state << (tg.gate_flags.closed ? 'closed' : 'opened')
state << ", closing (#{tg.timer})" if tg.gate_flags.closing
state << ", opening (#{tg.timer})" if tg.gate_flags.opening
end
ret << (descr + " linked to #{tg._rtti_classname} ##{tg.id} @[#{tg.centerx}, #{tg.centery}, #{tg.z}] #{state}")
# indent other links
descr = descr.gsub(/./, ' ')
}
ret << descr if ret.empty?
ret
end
def lever_list
@lever_list = []
df.world.buildings.other[:TRAP].find_all { |bld|
bld.trap_type == :Lever
}.sort_by { |bld| bld.id }.each { |bld|
puts lever_descr(bld, @lever_list.length)
@lever_list << bld.id
}
end
case $script_args[0]
when 'pull'
cheat = $script_args.delete('--cheat') || $script_args.delete('--now')
id = $script_args[1].to_i
id = @lever_list[id] || id
bld = df.building_find(id)
raise 'invalid lever id' if not bld
if cheat
lever_pull_cheat(bld)
else
lever_pull_job(bld)
end
when 'list'
lever_list
when /^\d+$/
id = $script_args[0].to_i
id = @lever_list[id] || id
bld = df.building_find(id)
raise 'invalid lever id' if not bld
puts lever_descr(bld)
else
puts <<EOS
Lever control from the dfhack console
Usage:
lever list
shows the list of levers in the fortress, with their id and links
lever pull 42
order the dwarves to pull lever 42
lever pull 42 --cheat
magically pull lever 42 immediately
EOS
end
# control your levers from the dfhack console
def lever_pull_job(bld)
ref = DFHack::GeneralRefBuildingHolderst.cpp_new
ref.building_id = bld.id
job = DFHack::Job.cpp_new
job.job_type = :PullLever
job.pos = [bld.centerx, bld.centery, bld.z]
job.general_refs << ref
bld.jobs << job
df.job_link job
puts lever_descr(bld)
end
def lever_pull_cheat(bld)
bld.linked_mechanisms.each { |i|
i.general_refs.grep(DFHack::GeneralRefBuildingHolderst).each { |r|
r.building_tg.setTriggerState(bld.state)
}
}
bld.state = (bld.state == 0 ? 1 : 0)
puts lever_descr(bld)
end
def lever_descr(bld, idx=nil)
ret = []
# lever description
descr = ''
descr << "#{idx}: " if idx
descr << "lever ##{bld.id} @[#{bld.centerx}, #{bld.centery}, #{bld.z}] #{bld.state == 0 ? '\\' : '/'}"
bld.jobs.each { |j|
if j.job_type == :PullLever
flags = ''
flags << ', repeat' if j.flags.repeat
flags << ', suspended' if j.flags.suspend
descr << " (pull order#{flags})"
end
}
bld.linked_mechanisms.map { |i|
i.general_refs.grep(DFHack::GeneralRefBuildingHolderst)
}.flatten.each { |r|
# linked building description
tg = r.building_tg
state = ''
if tg.respond_to?(:gate_flags)
state << (tg.gate_flags.closed ? 'closed' : 'opened')
state << ", closing (#{tg.timer})" if tg.gate_flags.closing
state << ", opening (#{tg.timer})" if tg.gate_flags.opening
end
ret << (descr + " linked to #{tg._rtti_classname} ##{tg.id} @[#{tg.centerx}, #{tg.centery}, #{tg.z}] #{state}")
# indent other links
descr = descr.gsub(/./, ' ')
}
ret << descr if ret.empty?
ret
end
def lever_list
@lever_list = []
df.world.buildings.other[:TRAP].find_all { |bld|
bld.trap_type == :Lever
}.sort_by { |bld| bld.id }.each { |bld|
puts lever_descr(bld, @lever_list.length)
@lever_list << bld.id
}
end
case $script_args[0]
when 'pull'
cheat = $script_args.delete('--cheat') || $script_args.delete('--now')
id = $script_args[1].to_i
id = @lever_list[id] || id
bld = df.building_find(id)
raise 'invalid lever id' if not bld
if cheat
lever_pull_cheat(bld)
else
lever_pull_job(bld)
end
when 'list'
lever_list
when /^\d+$/
id = $script_args[0].to_i
id = @lever_list[id] || id
bld = df.building_find(id)
raise 'invalid lever id' if not bld
puts lever_descr(bld)
else
puts <<EOS
Lever control from the dfhack console
Usage:
lever list
shows the list of levers in the fortress, with their id and links
lever pull 42
order the dwarves to pull lever 42
lever pull 42 --cheat
magically pull lever 42 immediately
EOS
end

@ -0,0 +1,84 @@
# scan the map for ore veins
target_ore = $script_args[0]
def find_all_ore_veins
puts 'scanning map...'
$ore_veins = {}
seen_mat = {}
df.each_map_block { |block|
block.block_events.grep(DFHack::BlockSquareEventMineralst).each { |vein|
mat_index = vein.inorganic_mat
if not seen_mat[mat_index] or $ore_veins[mat_index]
seen_mat[mat_index] = true
if df.world.raws.inorganics[mat_index].flags[:METAL_ORE]
$ore_veins[mat_index] ||= []
$ore_veins[mat_index] << [block.map_pos.x, block.map_pos.y, block.map_pos.z]
end
end
}
}
df.onstatechange_register_once { |st|
if st == :MAP_LOADED
$ore_veins = nil # invalidate veins cache
true
end
}
$ore_veins
end
$ore_veins ||= find_all_ore_veins
if not target_ore or target_ore == 'help'
puts <<EOS
Scan the map to find one random tile of unmined ore.
It will center the game view on that tile and mark it for digging.
Only works with metal ores.
Usage:
locate_ore list list all existing vein materials (including mined ones)
locate_ore hematite find one tile of unmined hematite ore
locate_ore iron find one tile of unmined ore you can smelt into iron
EOS
elsif target_ore and mats = $ore_veins.keys.find_all { |k|
ino = df.world.raws.inorganics[k]
ino.id =~ /#{target_ore}/i or ino.metal_ore.mat_index.find { |m|
df.world.raws.inorganics[m].id =~ /#{target_ore}/i
}
} and not mats.empty?
pos = nil
dxs = (0..15).sort_by { rand }
dys = (0..15).sort_by { rand }
if found_mat = mats.sort_by { rand }.find { |mat|
$ore_veins[mat].sort_by { rand }.find { |bx, by, bz|
dys.find { |dy|
dxs.find { |dx|
tile = df.map_tile_at(bx+dx, by+dy, bz)
if tile.tilemat == :MINERAL and tile.designation.dig == :No and tile.shape == :WALL and
tile.mat_index_vein == mat and
# ignore map borders
bx+dx > 0 and bx+dx < df.world.map.x_count-1 and by+dy > 0 and by+dy < df.world.map.y_count-1
pos = [bx+dx, by+dy, bz]
end
}
}
}
}
df.center_viewscreen(*pos)
df.map_tile_at(*pos).dig
puts "Here is some #{df.world.raws.inorganics[found_mat].id}"
else
puts "Cannot find unmined #{mats.map { |mat| df.world.raws.inorganics[mat].id }.join(', ')}"
end
else
puts "Available ores:", $ore_veins.sort_by { |mat, pos| pos.length }.map { |mat, pos|
ore = df.world.raws.inorganics[mat]
metals = ore.metal_ore.mat_index.map { |m| df.world.raws.inorganics[m] }
' ' + ore.id.downcase + ' (' + metals.map { |m| m.id.downcase }.join(', ') + ')'
}
end

@ -1,56 +0,0 @@
# create an infinite magma source at the cursor
$magma_sources ||= []
case $script_args[0]
when 'here'
$magma_onupdate ||= df.onupdate_register('magmasource', 12) {
# called every 12 game ticks (100x a dwarf day)
if $magma_sources.empty?
df.onupdate_unregister($magma_onupdate)
$magma_onupdate = nil
end
$magma_sources.each { |x, y, z|
if tile = df.map_tile_at(x, y, z) and tile.shape_passableflow
des = tile.designation
tile.spawn_magma(des.flow_size + 1) if des.flow_size < 7
end
}
}
if df.cursor.x != -30000
if tile = df.map_tile_at(df.cursor)
if tile.shape_passableflow
$magma_sources << [df.cursor.x, df.cursor.y, df.cursor.z]
else
puts "Impassable tile: I'm afraid I can't do that, Dave"
end
else
puts "Unallocated map block - build something here first"
end
else
puts "Please put the game cursor where you want a magma source"
end
when 'delete-here'
$magma_sources.delete [df.cursor.x, df.cursor.y, df.cursor.z]
when 'stop'
$magma_sources.clear
else
puts <<EOS
Creates a new infinite magma source at the cursor.
Arguments:
here - create a new source at the current cursor position
(call multiple times for higher flow)
delete-here - delete the source under the cursor
stop - delete all created magma sources
EOS
if $magma_sources.first
puts '', 'Current magma sources:', $magma_sources.map { |s| " #{s.inspect}" }
end
end

@ -1,40 +1,40 @@
# pit all caged creatures in a zone
case $script_args[0]
when '?', 'help'
puts <<EOS
Run this script with the cursor on top of a pit/pond activity zone, or with a zone identifier as argument.
It will mark all caged creatures on tiles covered by the zone to be dumped.
Works best with an animal stockpile on top of the pit/pond zone.
EOS
throw :script_finished
when /(\d+)/
nr = $1.to_i
bld = df.world.buildings.other[:ACTIVITY_ZONE].find { |zone| zone.zone_num == nr }
else
bld = df.world.buildings.other[:ACTIVITY_ZONE].find { |zone|
zone.zone_flags.pit_pond and zone.z == df.cursor.z and
zone.x1 <= df.cursor.x and zone.x2 >= df.cursor.x and zone.y1 <= df.cursor.y and zone.y2 >= df.cursor.y
}
end
if not bld
puts "Please select a pit/pond zone"
throw :script_finished
end
found = 0
df.world.items.other[:CAGE].each { |cg|
next if not cg.flags.on_ground
next if cg.pos.z != bld.z or cg.pos.x < bld.x1 or cg.pos.x > bld.x2 or cg.pos.y < bld.y1 or cg.pos.y > bld.y2
next if not uref = cg.general_refs.grep(DFHack::GeneralRefContainsUnitst).first
found += 1
u = uref.unit_tg
puts "Pitting #{u.race_tg.name[0]} #{u.id} #{u.name}"
u.general_refs << DFHack::GeneralRefBuildingCivzoneAssignedst.cpp_new(:building_id => bld.id)
bld.assigned_creature << u.id
}
puts "No creature available for pitting" if found == 0
# pit all caged creatures in a zone
case $script_args[0]
when '?', 'help'
puts <<EOS
Run this script with the cursor on top of a pit/pond activity zone, or with a zone identifier as argument.
It will mark all caged creatures on tiles covered by the zone to be dumped.
Works best with an animal stockpile on top of the pit/pond zone.
EOS
throw :script_finished
when /(\d+)/
nr = $1.to_i
bld = df.world.buildings.other[:ACTIVITY_ZONE].find { |zone| zone.zone_num == nr }
else
bld = df.world.buildings.other[:ACTIVITY_ZONE].find { |zone|
zone.zone_flags.pit_pond and zone.z == df.cursor.z and
zone.x1 <= df.cursor.x and zone.x2 >= df.cursor.x and zone.y1 <= df.cursor.y and zone.y2 >= df.cursor.y
}
end
if not bld
puts "Please select a pit/pond zone"
throw :script_finished
end
found = 0
df.world.items.other[:CAGE].each { |cg|
next if not cg.flags.on_ground
next if cg.pos.z != bld.z or cg.pos.x < bld.x1 or cg.pos.x > bld.x2 or cg.pos.y < bld.y1 or cg.pos.y > bld.y2
next if not uref = cg.general_refs.grep(DFHack::GeneralRefContainsUnitst).first
found += 1
u = uref.unit_tg
puts "Pitting #{u.race_tg.name[0]} #{u.id} #{u.name}"
u.general_refs << DFHack::GeneralRefBuildingCivzoneAssignedst.cpp_new(:building_id => bld.id)
bld.assigned_creature << u.id
}
puts "No creature available for pitting" if found == 0

@ -0,0 +1,4 @@
# run many dfhack commands separated by ;
# ex: multicmd locate-ore IRON ; digv ; digcircle 16
$script_args.join(' ').split(/\s*;\s*/).each { |cmd| df.dfhack_run cmd }

@ -1,43 +1,43 @@
# remove bad thoughts for the selected unit or the whole fort
dry_run = $script_args.delete('--dry-run') || $script_args.delete('-n')
$script_args << 'all' if dry_run and $script_args.empty?
seenbad = Hash.new(0)
clear_mind = lambda { |u|
u.status.recent_events.each { |e|
next if DFHack::UnitThoughtType::Value[e.type].to_s[0, 1] != '-'
seenbad[e.type] += 1
e.age = 0x1000_0000 unless dry_run
}
}
summary = lambda {
seenbad.sort_by { |thought, cnt| cnt }.each { |thought, cnt|
puts " #{thought} #{cnt}"
}
count = seenbad.values.inject(0) { |sum, cnt| sum+cnt }
puts "Removed #{count} bad thought#{'s' if count != 1}." if count > 0 and not dry_run
}
case $script_args[0]
when 'him'
if u = df.unit_find
clear_mind[u]
summary[]
else
puts 'Please select a dwarf ingame'
end
when 'all'
df.unit_citizens.each { |uu|
clear_mind[uu]
}
summary[]
else
puts "Usage: removebadthoughts [--dry-run] <him|all>"
end
# remove bad thoughts for the selected unit or the whole fort
dry_run = $script_args.delete('--dry-run') || $script_args.delete('-n')
$script_args << 'all' if dry_run and $script_args.empty?
seenbad = Hash.new(0)
clear_mind = lambda { |u|
u.status.recent_events.each { |e|
next if DFHack::UnitThoughtType::Value[e.type].to_s[0, 1] != '-'
seenbad[e.type] += 1
e.age = 0x1000_0000 unless dry_run
}
}
summary = lambda {
seenbad.sort_by { |thought, cnt| cnt }.each { |thought, cnt|
puts " #{thought} #{cnt}"
}
count = seenbad.values.inject(0) { |sum, cnt| sum+cnt }
puts "Removed #{count} bad thought#{'s' if count != 1}." if count > 0 and not dry_run
}
case $script_args[0]
when 'him'
if u = df.unit_find
clear_mind[u]
summary[]
else
puts 'Please select a dwarf ingame'
end
when 'all'
df.unit_citizens.each { |uu|
clear_mind[uu]
}
summary[]
else
puts "Usage: removebadthoughts [--dry-run] <him|all>"
end

@ -0,0 +1,83 @@
# create an infinite magma/water source/drain at the cursor
$sources ||= []
cur_source = {
:liquid => 'water',
:amount => 7,
:pos => [df.cursor.x, df.cursor.y, df.cursor.z]
}
cmd = 'help'
$script_args.each { |a|
case a.downcase
when 'water', 'magma'
cur_source[:liquid] = a.downcase
when /^\d+$/
cur_source[:amount] = a.to_i
when 'add', 'del', 'delete', 'clear', 'help', 'list'
cmd = a.downcase
else
puts "source: unhandled argument #{a}"
end
}
case cmd
when 'add'
$sources_onupdate ||= df.onupdate_register('sources', 12) {
# called every 12 game ticks (100x a dwarf day)
$sources.each { |s|
if tile = df.map_tile_at(*s[:pos]) and tile.shape_passableflow
# XXX does not check current liquid_type
des = tile.designation
cur = des.flow_size
if cur != s[:amount]
tile.spawn_liquid((cur > s[:amount] ? cur-1 : cur+1), s[:liquid] == 'magma')
end
end
}
if $sources.empty?
df.onupdate_unregister($sources_onupdate)
$sources_onupdate = nil
end
}
if cur_source[:pos][0] >= 0
if tile = df.map_tile_at(*cur_source[:pos])
if tile.shape_passableflow
$sources << cur_source
else
puts "Impassable tile: I'm afraid I can't do that, Dave"
end
else
puts "Unallocated map block - build something here first"
end
else
puts "Please put the game cursor where you want a source"
end
when 'del', 'delete'
$sources.delete_if { |s| s[:pos] == cur_source[:pos] }
when 'clear'
$sources.clear
when 'list'
puts "Source list:", $sources.map { |s|
" #{s[:pos].inspect} #{s[:liquid]} #{s[:amount]}"
}
puts "Current cursor pos: #{[df.cursor.x, df.cursor.y, df.cursor.z].inspect}" if df.cursor.x >= 0
else
puts <<EOS
Creates a new infinite liquid source at the cursor.
Examples:
source add water - create a water source under cursor
source add water 0 - create a water drain
source add magma 5 - create a magma source, up to 5/7 deep
source delete - delete source under cursor
source clear - remove all sources
source list
EOS
end

@ -1,204 +1,204 @@
# mark stuff inside of cages for dumping.
def plural(nr, name)
# '1 cage' / '4 cages'
"#{nr} #{name}#{'s' if nr > 1}"
end
def cage_dump_items(list)
count = 0
count_cage = 0
list.each { |cage|
pre_count = count
cage.general_refs.each { |ref|
next unless ref.kind_of?(DFHack::GeneralRefContainsItemst)
next if ref.item_tg.flags.dump
count += 1
ref.item_tg.flags.dump = true
}
count_cage += 1 if pre_count != count
}
puts "Dumped #{plural(count, 'item')} in #{plural(count_cage, 'cage')}"
end
def cage_dump_armor(list)
count = 0
count_cage = 0
list.each { |cage|
pre_count = count
cage.general_refs.each { |ref|
next unless ref.kind_of?(DFHack::GeneralRefContainsUnitst)
ref.unit_tg.inventory.each { |it|
next if it.mode != :Worn
next if it.item.flags.dump
count += 1
it.item.flags.dump = true
}
}
count_cage += 1 if pre_count != count
}
puts "Dumped #{plural(count, 'armor piece')} in #{plural(count_cage, 'cage')}"
end
def cage_dump_weapons(list)
count = 0
count_cage = 0
list.each { |cage|
pre_count = count
cage.general_refs.each { |ref|
next unless ref.kind_of?(DFHack::GeneralRefContainsUnitst)
ref.unit_tg.inventory.each { |it|
next if it.mode != :Weapon
next if it.item.flags.dump
count += 1
it.item.flags.dump = true
}
}
count_cage += 1 if pre_count != count
}
puts "Dumped #{plural(count, 'weapon')} in #{plural(count_cage, 'cage')}"
end
def cage_dump_all(list)
count = 0
count_cage = 0
list.each { |cage|
pre_count = count
cage.general_refs.each { |ref|
case ref
when DFHack::GeneralRefContainsItemst
next if ref.item_tg.flags.dump
count += 1
ref.item_tg.flags.dump = true
when DFHack::GeneralRefContainsUnitst
ref.unit_tg.inventory.each { |it|
next if it.item.flags.dump
count += 1
it.item.flags.dump = true
}
end
}
count_cage += 1 if pre_count != count
}
puts "Dumped #{plural(count, 'item')} in #{plural(count_cage, 'cage')}"
end
def cage_dump_list(list)
count_total = Hash.new(0)
empty_cages = 0
list.each { |cage|
count = Hash.new(0)
cage.general_refs.each { |ref|
case ref
when DFHack::GeneralRefContainsItemst
count[ref.item_tg._rtti_classname] += 1
when DFHack::GeneralRefContainsUnitst
ref.unit_tg.inventory.each { |it|
count[it.item._rtti_classname] += 1
}
# TODO vermin ?
else
puts "unhandled ref #{ref.inspect}" if $DEBUG
end
}
type = case cage
when DFHack::ItemCagest; 'Cage'
when DFHack::ItemAnimaltrapst; 'Animal trap'
else cage._rtti_classname
end
if count.empty?
empty_cages += 1
else
puts "#{type} ##{cage.id}: ", count.sort_by { |k, v| v }.map { |k, v| " #{v} #{k}" }
end
count.each { |k, v| count_total[k] += v }
}
if list.length > 2
puts '', "Total: ", count_total.sort_by { |k, v| v }.map { |k, v| " #{v} #{k}" }
puts "with #{plural(empty_cages, 'empty cage')}"
end
end
# handle magic script arguments
here_only = $script_args.delete 'here'
if here_only
it = df.item_find
list = [it]
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
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 = []
ids.each { |id|
$script_args.delete id
if not it = df.item_find(id.to_i)
puts "Invalid item id #{id}"
elsif not it.kind_of?(DFHack::ItemCagest) and not it.kind_of?(DFHack::ItemAnimaltrapst)
puts "Item ##{id} is not a cage"
list << it
else
list << it
end
}
if list.empty?
puts 'Please use a valid cage id'
throw :script_finished
end
else
list = df.world.items.other[:ANY_CAGE_OR_TRAP]
end
# act
case $script_args[0]
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)
when 'list'
cage_dump_list(list)
else
puts <<EOS
Marks items inside all cages for dumping.
Add 'here' to dump stuff only for selected cage.
Add a cage id to dump stuff for this cage only.
See 'autodump' to actually dump stuff.
Usage:
stripcaged items
dump items directly in cages (eg seeds after training)
stripcaged [armor|weapons] here
dump armor or weapons of caged creatures in selected cage
stripcaged all 28 29
dump every item in cage id 28 and 29, along with every item worn by creatures in there too
stripcaged list
show content of the cages
EOS
end
# mark stuff inside of cages for dumping.
def plural(nr, name)
# '1 cage' / '4 cages'
"#{nr} #{name}#{'s' if nr > 1}"
end
def cage_dump_items(list)
count = 0
count_cage = 0
list.each { |cage|
pre_count = count
cage.general_refs.each { |ref|
next unless ref.kind_of?(DFHack::GeneralRefContainsItemst)
next if ref.item_tg.flags.dump
count += 1
ref.item_tg.flags.dump = true
}
count_cage += 1 if pre_count != count
}
puts "Dumped #{plural(count, 'item')} in #{plural(count_cage, 'cage')}"
end
def cage_dump_armor(list)
count = 0
count_cage = 0
list.each { |cage|
pre_count = count
cage.general_refs.each { |ref|
next unless ref.kind_of?(DFHack::GeneralRefContainsUnitst)
ref.unit_tg.inventory.each { |it|
next if it.mode != :Worn
next if it.item.flags.dump
count += 1
it.item.flags.dump = true
}
}
count_cage += 1 if pre_count != count
}
puts "Dumped #{plural(count, 'armor piece')} in #{plural(count_cage, 'cage')}"
end
def cage_dump_weapons(list)
count = 0
count_cage = 0
list.each { |cage|
pre_count = count
cage.general_refs.each { |ref|
next unless ref.kind_of?(DFHack::GeneralRefContainsUnitst)
ref.unit_tg.inventory.each { |it|
next if it.mode != :Weapon
next if it.item.flags.dump
count += 1
it.item.flags.dump = true
}
}
count_cage += 1 if pre_count != count
}
puts "Dumped #{plural(count, 'weapon')} in #{plural(count_cage, 'cage')}"
end
def cage_dump_all(list)
count = 0
count_cage = 0
list.each { |cage|
pre_count = count
cage.general_refs.each { |ref|
case ref
when DFHack::GeneralRefContainsItemst
next if ref.item_tg.flags.dump
count += 1
ref.item_tg.flags.dump = true
when DFHack::GeneralRefContainsUnitst
ref.unit_tg.inventory.each { |it|
next if it.item.flags.dump
count += 1
it.item.flags.dump = true
}
end
}
count_cage += 1 if pre_count != count
}
puts "Dumped #{plural(count, 'item')} in #{plural(count_cage, 'cage')}"
end
def cage_dump_list(list)
count_total = Hash.new(0)
empty_cages = 0
list.each { |cage|
count = Hash.new(0)
cage.general_refs.each { |ref|
case ref
when DFHack::GeneralRefContainsItemst
count[ref.item_tg._rtti_classname] += 1
when DFHack::GeneralRefContainsUnitst
ref.unit_tg.inventory.each { |it|
count[it.item._rtti_classname] += 1
}
# TODO vermin ?
else
puts "unhandled ref #{ref.inspect}" if $DEBUG
end
}
type = case cage
when DFHack::ItemCagest; 'Cage'
when DFHack::ItemAnimaltrapst; 'Animal trap'
else cage._rtti_classname
end
if count.empty?
empty_cages += 1
else
puts "#{type} ##{cage.id}: ", count.sort_by { |k, v| v }.map { |k, v| " #{v} #{k}" }
end
count.each { |k, v| count_total[k] += v }
}
if list.length > 2
puts '', "Total: ", count_total.sort_by { |k, v| v }.map { |k, v| " #{v} #{k}" }
puts "with #{plural(empty_cages, 'empty cage')}"
end
end
# handle magic script arguments
here_only = $script_args.delete 'here'
if here_only
it = df.item_find
list = [it]
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
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 = []
ids.each { |id|
$script_args.delete id
if not it = df.item_find(id.to_i)
puts "Invalid item id #{id}"
elsif not it.kind_of?(DFHack::ItemCagest) and not it.kind_of?(DFHack::ItemAnimaltrapst)
puts "Item ##{id} is not a cage"
list << it
else
list << it
end
}
if list.empty?
puts 'Please use a valid cage id'
throw :script_finished
end
else
list = df.world.items.other[:ANY_CAGE_OR_TRAP]
end
# act
case $script_args[0]
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)
when 'list'
cage_dump_list(list)
else
puts <<EOS
Marks items inside all cages for dumping.
Add 'here' to dump stuff only for selected cage.
Add a cage id to dump stuff for this cage only.
See 'autodump' to actually dump stuff.
Usage:
stripcaged items
dump items directly in cages (eg seeds after training)
stripcaged [armor|weapons] here
dump armor or weapons of caged creatures in selected cage
stripcaged all 28 29
dump every item in cage id 28 and 29, along with every item worn by creatures in there too
stripcaged list
show content of the cages
EOS
end

@ -1,61 +1,61 @@
# give super-dwarven speed to an unit
$superdwarf_onupdate ||= nil
$superdwarf_ids ||= []
case $script_args[0]
when 'add'
if u = df.unit_find
$superdwarf_ids |= [u.id]
$superdwarf_onupdate ||= df.onupdate_register('superdwarf', 1) {
if $superdwarf_ids.empty?
df.onupdate_unregister($superdwarf_onupdate)
$superdwarf_onupdate = nil
else
$superdwarf_ids.each { |id|
if u = df.unit_find(id) and not u.flags1.dead
# faster walk/work
if u.counters.job_counter > 0
u.counters.job_counter = 0
end
# no sleep
if u.counters2.sleepiness_timer > 10000
u.counters2.sleepiness_timer = 1
end
# no break
if b = u.status.misc_traits.find { |t| t.id == :OnBreak }
b.value = 500_000
end
else
$superdwarf_ids.delete id
end
}
end
}
else
puts "Select a creature using 'v'"
end
when 'del'
if u = df.unit_find
$superdwarf_ids.delete u.id
else
puts "Select a creature using 'v'"
end
when 'clear'
$superdwarf_ids.clear
when 'list'
puts "current superdwarves:", $superdwarf_ids.map { |id| df.unit_find(id).name }
else
puts "Usage:",
" - superdwarf add: give superspeed to currently selected creature",
" - superdwarf del: remove superspeed to current creature",
" - superdwarf clear: remove all superpowers",
" - superdwarf list: list super-dwarves"
end
# give super-dwarven speed to an unit
$superdwarf_onupdate ||= nil
$superdwarf_ids ||= []
case $script_args[0]
when 'add'
if u = df.unit_find
$superdwarf_ids |= [u.id]
$superdwarf_onupdate ||= df.onupdate_register('superdwarf', 1) {
if $superdwarf_ids.empty?
df.onupdate_unregister($superdwarf_onupdate)
$superdwarf_onupdate = nil
else
$superdwarf_ids.each { |id|
if u = df.unit_find(id) and not u.flags1.dead
# faster walk/work
if u.counters.job_counter > 0
u.counters.job_counter = 0
end
# no sleep
if u.counters2.sleepiness_timer > 10000
u.counters2.sleepiness_timer = 1
end
# no break
if b = u.status.misc_traits.find { |t| t.id == :OnBreak }
b.value = 500_000
end
else
$superdwarf_ids.delete id
end
}
end
}
else
puts "Select a creature using 'v'"
end
when 'del'
if u = df.unit_find
$superdwarf_ids.delete u.id
else
puts "Select a creature using 'v'"
end
when 'clear'
$superdwarf_ids.clear
when 'list'
puts "current superdwarves:", $superdwarf_ids.map { |id| df.unit_find(id).name }
else
puts "Usage:",
" - superdwarf add: give superspeed to currently selected creature",
" - superdwarf del: remove superspeed to current creature",
" - superdwarf clear: remove all superpowers",
" - superdwarf list: list super-dwarves"
end

@ -1,17 +1,17 @@
joblist = df.world.job_list.next
count = 0
while joblist
job = joblist.item
joblist = joblist.next
if job.job_type == :ConstructBuilding
if (job.flags.suspend && job.items && job.items[0])
item = job.items[0].item
job.flags.suspend = false
count += 1
end
end
end
puts "Unsuspended #{count} job(s)."
joblist = df.world.job_list.next
count = 0
while joblist
job = joblist.item
joblist = joblist.next
if job.job_type == :ConstructBuilding
if (job.flags.suspend && job.items && job.items[0])
item = job.items[0].item
job.flags.suspend = false
count += 1
end
end
end
puts "Unsuspended #{count} job(s)."