Merge remote-tracking branch 'jjyg/master'

develop
Kelly Martin 2012-07-05 10:04:28 -05:00
commit 6f4f3039e1
29 changed files with 2599 additions and 1658 deletions

@ -1361,3 +1361,60 @@ also tries to have dwarves specialize in specific skills.
while it is enabled.
For detailed usage information, see 'help autolabor'.
growcrops
=========
Instantly grow seeds inside farming plots.
With no argument, this command list the various seed types currently in
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:
::
growcrops plump 40
This is a ruby script and needs the ruby plugin.
removebadthoughts
=================
This script remove negative thoughts from your dwarves. Very useful against
tantrum spirals.
With a selected unit in 'v' mode, will clear this unit's mind, otherwise
clear all your fort's units minds.
Individual dwarf happiness may not increase right after this command is run,
but in the short term your dwarves will get much more joyful.
The thoughts are set to be very old, and the game will remove them soon when
you unpause.
With the optional ``-v`` parameter, the script will dump the negative thoughts
it removed.
This is a ruby script and needs the ruby plugin.
slayrace
========
Kills any unit of a given race.
With no argument, lists the available races.
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. May not work
on vampires and other weird creatures.
Targets any unit on a revealed tile of the map, including ambushers. Ex:
::
slayrace gob
To kill a single creature in the same way, you can use the following line,
after selecting the unit with the 'v' cursor:
::
rb_eval df.unit_find.body.blood_count = 0

@ -204,7 +204,7 @@ struct sortable
};
};
static std::string getLuaHelp(std::string path)
static std::string getScriptHelp(std::string path, std::string helpprefix)
{
ifstream script(path.c_str());
@ -212,14 +212,14 @@ static std::string getLuaHelp(std::string path)
{
std::string help;
if (getline(script, help) &&
help.substr(0,3) == "-- ")
return help.substr(3);
help.substr(0,helpprefix.length()) == helpprefix)
return help.substr(helpprefix.length());
}
return "Lua script.";
return "No help available.";
}
static std::map<string,string> listLuaScripts(std::string path)
static std::map<string,string> listScripts(PluginManager *plug_mgr, std::string path)
{
std::vector<string> files;
getdir(path, files);
@ -229,10 +229,16 @@ static std::map<string,string> listLuaScripts(std::string path)
{
if (hasEnding(files[i], ".lua"))
{
std::string help = getLuaHelp(path + files[i]);
std::string help = getScriptHelp(path + files[i], "-- ");
pset[files[i].substr(0, files[i].size()-4)] = help;
}
else if (plug_mgr->eval_ruby && hasEnding(files[i], ".rb"))
{
std::string help = getScriptHelp(path + files[i], "# ");
pset[files[i].substr(0, files[i].size()-3)] = help;
}
}
return pset;
}
@ -275,6 +281,18 @@ static command_result runLuaScript(color_ostream &out, std::string name, vector<
return ok ? CR_OK : CR_FAILURE;
}
static command_result runRubyScript(color_ostream &out, PluginManager *plug_mgr, std::string name, vector<string> &args)
{
std::string rbcmd = "$script_args = [";
for (size_t i = 0; i < args.size(); i++)
rbcmd += "'" + args[i] + "', ";
rbcmd += "]\n";
rbcmd += "load './hack/scripts/" + name + ".rb'";
return plug_mgr->eval_ruby(out, rbcmd.c_str());
}
command_result Core::runCommand(color_ostream &out, const std::string &command)
{
if (!command.empty())
@ -348,10 +366,16 @@ command_result Core::runCommand(color_ostream &con, const std::string &first, ve
return CR_OK;
}
}
auto filename = getHackPath() + "scripts/" + parts[0] + ".lua";
if (fileExists(filename))
auto filename = getHackPath() + "scripts/" + parts[0];
if (fileExists(filename + ".lua"))
{
string help = getScriptHelp(filename + ".lua", "-- ");
con.print("%s: %s\n", parts[0].c_str(), help.c_str());
return CR_OK;
}
if (plug_mgr->eval_ruby && fileExists(filename + ".rb"))
{
string help = getLuaHelp(filename);
string help = getScriptHelp(filename + ".rb", "# ");
con.print("%s: %s\n", parts[0].c_str(), help.c_str());
return CR_OK;
}
@ -499,7 +523,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first, ve
con.print(" %-22s- %s\n",(*iter).name.c_str(), (*iter).description.c_str());
con.reset_color();
}
auto scripts = listLuaScripts(getHackPath() + "scripts/");
auto scripts = listScripts(plug_mgr, getHackPath() + "scripts/");
if (!scripts.empty())
{
con.print("\nscripts:\n");
@ -604,9 +628,11 @@ command_result Core::runCommand(color_ostream &con, const std::string &first, ve
command_result res = plug_mgr->InvokeCommand(con, first, parts);
if(res == CR_NOT_IMPLEMENTED)
{
auto filename = getHackPath() + "scripts/" + first + ".lua";
if (fileExists(filename))
auto filename = getHackPath() + "scripts/" + first;
if (fileExists(filename + ".lua"))
res = runLuaScript(con, first, parts);
else if (plug_mgr->eval_ruby && fileExists(filename + ".rb"))
res = runRubyScript(con, plug_mgr, first, parts);
else
con.printerr("%s is not a recognized command.\n", first.c_str());
}
@ -1198,7 +1224,8 @@ bool Core::ncurses_wgetch(int in, int & out)
{
df::viewscreen * ws = Gui::GetCurrentScreen();
if (strict_virtual_cast<df::viewscreen_dwarfmodest>(ws) &&
df::global::ui->main.mode != ui_sidebar_mode::Hotkeys)
df::global::ui->main.mode != ui_sidebar_mode::Hotkeys &&
df::global::ui->main.hotkeys[idx].cmd == df::ui_hotkey::T_cmd::None)
{
setHotkeyCmd(df::global::ui->main.hotkeys[idx].name);
return false;
@ -1346,7 +1373,8 @@ bool Core::SelectHotkey(int sym, int modifiers)
idx += 8;
if (strict_virtual_cast<df::viewscreen_dwarfmodest>(screen) &&
df::global::ui->main.mode != ui_sidebar_mode::Hotkeys)
df::global::ui->main.mode != ui_sidebar_mode::Hotkeys &&
df::global::ui->main.hotkeys[idx].cmd == df::ui_hotkey::T_cmd::None)
{
cmd = df::global::ui->main.hotkeys[idx].name;
}

@ -188,6 +188,7 @@ bool Plugin::load(color_ostream &con)
plugin_shutdown = (command_result (*)(color_ostream &)) LookupPlugin(plug, "plugin_shutdown");
plugin_onstatechange = (command_result (*)(color_ostream &, state_change_event)) LookupPlugin(plug, "plugin_onstatechange");
plugin_rpcconnect = (RPCService* (*)(color_ostream &)) LookupPlugin(plug, "plugin_rpcconnect");
plugin_eval_ruby = (command_result (*)(color_ostream &, const char*)) LookupPlugin(plug, "plugin_eval_ruby");
index_lua(plug);
this->name = *plug_name;
plugin_lib = plug;
@ -538,6 +539,7 @@ PluginManager::PluginManager(Core * core)
const string searchstr = ".plug.dll";
#endif
cmdlist_mutex = new mutex();
eval_ruby = NULL;
vector <string> filez;
getdir(path, filez);
for(size_t i = 0; i < filez.size();i++)
@ -620,6 +622,8 @@ void PluginManager::registerCommands( Plugin * p )
{
belongs[cmds[i].name] = p;
}
if (p->plugin_eval_ruby)
eval_ruby = p->plugin_eval_ruby;
cmdlist_mutex->unlock();
}
@ -632,5 +636,7 @@ void PluginManager::unregisterCommands( Plugin * p )
{
belongs.erase(cmds[i].name);
}
if (p->plugin_eval_ruby)
eval_ruby = NULL;
cmdlist_mutex->unlock();
}
}

@ -209,6 +209,7 @@ namespace DFHack
command_result (*plugin_onupdate)(color_ostream &);
command_result (*plugin_onstatechange)(color_ostream &, state_change_event);
RPCService* (*plugin_rpcconnect)(color_ostream &);
command_result (*plugin_eval_ruby)(color_ostream &, const char*);
};
class DFHACK_EXPORT PluginManager
{
@ -237,6 +238,7 @@ namespace DFHack
{
return all_plugins.size();
}
command_result (*eval_ruby)(color_ostream &, const char*);
// DATA
private:
tthread::mutex * cmdlist_mutex;

@ -293,6 +293,12 @@ std::string MaterialInfo::getToken()
switch (mode) {
case Builtin:
if (material->id == "COAL") {
if (index == 0)
return "COAL:COKE";
else if (index == 1)
return "COAL:CHARCOAL";
}
return material->id;
case Inorganic:
return "INORGANIC:" + inorganic->id;

@ -1 +1 @@
Subproject commit ad38c5e96b05fedf16114fd16bd463e933f13582
Subproject commit eb6f706d702367ea3121272670e603000bbdd42a

@ -752,7 +752,7 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out )
else if (df::enums::building_type::TradeDepot == type)
{
df::building_tradedepotst* depot = (df::building_tradedepotst*) build;
trader_requested = depot->flags.bits.trader_requested;
trader_requested = depot->trade_flags.bits.trader_requested;
if (print_debug)
out.print("Trade depot found and trader requested, trader will be excluded from all labors.\n");
}

@ -1,5 +1,5 @@
OPTION(DL_RUBY "download libruby from the internet" ON)
IF (DL_RUBY)
IF (DL_RUBY AND NOT APPLE)
IF (UNIX)
FILE(DOWNLOAD http://cloud.github.com/downloads/jjyg/dfhack/libruby187.tar.gz ${CMAKE_CURRENT_SOURCE_DIR}/libruby187.tar.gz
EXPECTED_MD5 eb2adea59911f68e6066966c1352f291)
@ -15,7 +15,7 @@ IF (DL_RUBY)
FILE(RENAME msvcrt-ruby18.dll libruby.dll)
SET(RUBYLIB libruby.dll)
ENDIF(UNIX)
ENDIF(DL_RUBY)
ENDIF(DL_RUBY AND NOT APPLE)
ADD_CUSTOM_COMMAND(
OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/ruby-autogen.rb
@ -30,4 +30,8 @@ INCLUDE_DIRECTORIES("${dfhack_SOURCE_DIR}/depends/tthread")
DFHACK_PLUGIN(ruby ruby.cpp LINK_LIBRARIES dfhack-tinythread)
ADD_DEPENDENCIES(ruby ruby-autogen-rb)
INSTALL(FILES ruby.rb ruby-autogen.rb ${RUBYLIB} DESTINATION ${DFHACK_LIBRARY_DESTINATION})
INSTALL(FILES ${RUBYLIB} DESTINATION ${DFHACK_LIBRARY_DESTINATION})
INSTALL(DIRECTORY .
DESTINATION hack/ruby
FILES_MATCHING PATTERN "*.rb")

@ -2,61 +2,200 @@ This plugins embeds a ruby interpreter inside DFHack (ie inside Dwarf Fortress).
The plugin maps all the structures available in library/xml/ to ruby objects.
These objects are described in ruby-autogen.rb, they are all in the DFHack::
These objects are described in ruby-autogen.rb, they are all in the DFHack
module. The toplevel 'df' method is a shortcut to the DFHack module.
The plugin does *not* map most of dfhack methods (MapCache, ...) ; only direct
access to the raw DF data structures in memory is provided.
Some library methods are stored in the ruby.rb file, with shortcuts to read a
map block, find an unit or an item, etc.
Some library methods are stored in the various .rb file, e.g. shortcuts to read
a map block, find an unit or an item, etc.
Global objects are accessible through the 'df' accessor (eg df.world).
Global dfhack objects are accessible through the 'df' accessor (eg 'df.world').
The ruby plugin defines 2 dfhack console commands:
rb_load <filename> ; load a ruby script. Ex: rb_load hack/plants.rb (no quotes)
rb_eval <ruby expression> ; evaluate a ruby expression, show the result in the
console. Ex: rb_eval df.find_unit.name.first_name
DFHack structures are renamed in CamelCase in the ruby namespace.
For a list of the structures and their methods, grep the ruby-autogen.rb file.
All ruby code runs while the main DF process and other plugins are suspended.
DFHack console
--------------
The ruby plugin defines 1 dfhack console command:
rb_eval <ruby expression> ; evaluate a ruby expression and show the result in
the console. Ex: rb_eval df.unit_find().name.first_name
You can use single-quotes for strings ; avoid double-quotes that are parsed
and removed by the dfhack console.
and removed by the dfhack console code.
Text output from ruby code, through the standard 'puts', 'p' or 'raise' are
redirected to the dfhack console window.
If dfhack reports 'rb_eval is not a recognized command', check stderr.log. You
need a valid 32-bit ruby library to work, and ruby1.8 is prefered (ruby1.9 may
crash DF on startup for now). Install the library in the df root folder (or
hack/ on linux), the library should be named 'libruby.dll' (.so on linux).
df/hack/ on linux), the library should be named 'libruby.dll' (.so on linux).
You can download a tested version at http://github.com/jjyg/dfhack/downloads/
The plugin also interfaces with dfhack 'onupdate' hook.
Ruby scripts
------------
The ruby plugin allows the creation of '.rb' scripts in df/hack/scripts/.
If you create such a script, e.g. 'test.rb', that will add a new dfhack console
command 'test'.
The script can access the console command arguments through the global variable
'$script_args', which is an array of ruby Strings.
The help string displayed in dfhack 'ls' command is the first line of the
script, if it is a comment (starts with '# ').
Ruby helper functions
---------------------
This is an excerpt of the functions defined in dfhack/plugins/ruby/*.rb. Check
the files and the comments for a complete list.
df.same_pos?(obj1, obj2)
Returns true if both objects are at the same game coordinates.
obj1 and 2 should respond to #pos and #x #y #z.
df.map_block_at(pos) / map_block_at(x, y, z)
Returns the MapBlock for the coordinates or nil.
df.map_tile_at(pos)
Returns a MapTile, holds all information relative to the map tile.
df.each_map_block { |b| }
df.each_map_block_z(zlevel) { |b| }
Iterates over every map block (opt. on a single z-level).
df.center_viewscreen(coords)
Centers the DF view on the given coordinates. Accepts x/y/z arguments, or a
single argument responding to pos/x/y/z, eg an Unit, Item, ...
df.unit_find(arg)
Returns an Unit.
With no arg, returns the currently selected unit (through the (v) or (k) menus)
With a number, returns the unit with this ID
With something else, returns the first unit at the same game coordinates
df.unit_workers
Returns a list of worker citizen: units of your race & civilization, adults,
not dead, crazy, ghosts or nobles exempted of work.
df.unit_entitypositions(unit)
Returns the list of EntityPosition occupied by the unit.
Check the 'code' field for a readable name (MANAGER, CHIEF_MEDICAL_DWARF, ...)
df.match_rawname(name, list)
String fuzzy matching. Returns the list entry most similar to 'name'.
First searches for an exact match, then for a case-insensitive match, and
finally for a case-insensitive substring.
Returns the element from list if there is only one match, or nil.
Most useful to allow the user to specify a raw-defined name,
eg 'gob' for 'GOBLIN' or 'coal' for 'COAL_BITUMINOUS', hence the name.
df.building_alloc(type, subtype, customtype)
df.building_position(bld, pos, w, h)
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.
df.each_tree(material) { |t| }
Iterates over every tree of the given material (eg 'maple').
df.translate_name(name, in_english=true, only_lastpart=false)
Decode the LanguageName structure as a String as displayed in the game UI.
A shortcut is available through name.to_s
df.decode_mat(obj)
Returns a MaterialInfo definition for the given object, using its mat_type
and mat_index fields. Also works with a token string argument ('STONE:DOLOMITE')
DFHack callbacks
----------------
The plugin interfaces with dfhack 'onupdate' hook.
To register ruby code to be run every graphic frame, use:
handle = df.onupdate_register { puts 'i love flooding the console' }
You can also rate-limit when your callback is called to a number of game ticks:
handle = df.onupdate_register(10) { puts '10 more in-game ticks elapsed' }
In this case, the callback is called immediately, and then every X in-game
ticks (advances only when the game is unpaused).
To stop being called, use:
df.onupdate_unregister handle
The same mechanism is available for onstatechange.
The same mechanism is available for 'onstatechange', but the
SC_BEGIN_UNLOAD event is not propagated to the ruby handler.
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
only.
Deallocation is not supported. You may manually call df.free if you know
what you are doing (maps directly to the native malloc/free)
C++ std::string fields may be directly re-allocated using standard ruby strings,
e.g. some_unit.name.nickname = 'moo'
More subtle string manipulation, e.g. changing a single character, are not
supported. Read the whole string, manipulate it in ruby, and re-assign it
instead.
C++ std::vector<> can be iterated as standard ruby Enumerable objects, using
each/map/etc.
To append data to a vector, use vector << newelement or vector.push(newelement)
To insert at a given pos, vector.insert_at(index, value)
To delete an element, vector.delete_at(index)
You can binary search an element in a vector for a given numeric field value:
df.world.unit.all.binsearch(42, :id)
will find the element whose 'id' field is 42 (needs the vector to be initially
sorted by this field). The binsearch 2nd argument defaults to :id.
Any numeric field defined as being an enum value will be converted to a ruby
Symbol. This works for array indexes too.
Virtual method calls are supported for C++ objects, with a maximum of 4
arguments. Arguments / return value are interpreted as Compound/Enums as
specified in the vmethod definition in the xmls.
Pointer fields are automatically dereferenced ; so a vector of pointer to
Units will yield Units directly. NULL pointers yield the 'nil' value.
Exemples
--------
For more complex exemples, check the ruby/plugins/ source folder.
For more complex exemples, check the dfhack/scripts/*.rb files.
Show info on the currently selected unit ('v' or 'k' DF menu)
p df.find_unit.flags1
p df.unit_find.flags1
Set a custom nickname to unit with id '123'
df.find_unit(123).name.nickname = 'moo'
df.unit_find(123).name.nickname = 'moo'
Show current unit profession
p df.find_unit.profession
p df.unit_find.profession
Change current unit profession
df.find_unit.profession = :MASON
df.unit_find.profession = :MASON
Center the screen on unit '123'
df.center_viewscreen(df.find_unit(123))
Center the screen on unit ID '123'
df.center_viewscreen(df.unit_find(123))
Find an item at a given position, show its C++ classname
df.find_item(df.cursor)._rtti_classname
p df.item_find(df.cursor)._rtti_classname
Find the raws name of the plant under cursor
plant = df.world.plants.all.find { |plt| df.at_cursor?(plt) }
@ -67,15 +206,14 @@ Dig a channel under the cursor
df.map_block_at(df.cursor).flags.designated = true
Compilation
-----------
Plugin compilation
------------------
The plugin consists of the ruby.rb file including user comfort functions and
The plugin consists of the *.rb file including user comfort functions and
describing basic classes used by the autogenerated code, and ruby-autogen.rb,
the auto-generated code.
The generated code is generated by codegen.pl, which takes the codegen.out.xml
file as input.
autogen is output by codegen.pl from dfhack/library/include/df/codegen.out.xml
For exemple,
<ld:global-type ld:meta="struct-type" type-name="unit">
@ -89,17 +227,10 @@ Will generate
field(:custom_profession, 60) { stl_string }
field(:profession, 64) { number 16, true }
The syntax for the 'field' method is:
The syntax for the 'field' method in ruby-autogen.rb is:
1st argument = name of the method
2nd argument = offset of this field from the beginning of the struct.
2nd argument = offset of this field from the beginning of the current struct.
The block argument describes the type of the field: uint32, ptr to global...
Primitive type access is done through native methods in ruby.cpp (vector length,
Primitive type access is done through native methods from ruby.cpp (vector length,
raw memory access, etc)
MemHack::Pointers are automatically dereferenced ; so a vector of pointer to
Units will yield Units directly. Null pointers yield the 'nil' value.
This allows to use code such as 'df.world.units.all[0].pos', with 'all' being
in fact a vector of *pointers* to DFHack::Unit objects.

@ -0,0 +1,305 @@
module DFHack
class << self
def building_find(what=:selected, y=nil, z=nil)
if what == :selected
case ui.main.mode
when :LookAround
k = ui_look_list.items[ui_look_cursor]
k.building if k.type == :Building
when :BuildingItems, :QueryBuilding
world.selected_building
end
elsif what.kind_of?(Integer)
# search by building.id
return world.buildings.all.binsearch(what) if not z
# search by coordinates
x = what
world.buildings.all.find { |b|
b.z == z and
if b.room.extents
dx = x - b.room.x
dy = y - b.room.y
dx >= 0 and dx <= b.room.width and
dy >= 0 and dy <= b.room.height and
b.room.extents[ dy*b.room.width + dx ] > 0
else
b.x1 <= x and b.x2 >= x and
b.y1 <= y and b.y2 >= y
end
}
elsif what.respond_to?(:x) or what.respond_to?(:pos)
# find the building at the same position
what = what.pos if what.respond_to?(:pos)
building_find(what.x, what.y, what.z)
else
raise "what what?"
end
end
# allocate a new building object
def building_alloc(type, subtype=-1, custom=-1)
cls = rtti_n2c[BuildingType::Classname[type].to_sym]
raise "invalid building type #{type.inspect}" if not cls
bld = cls.cpp_new
bld.race = ui.race_id
bld.setSubtype(subtype) if subtype != -1
bld.setCustomType(custom) if custom != -1
case type
when :Furnace; bld.melt_remainder[world.raws.inorganics.length] = 0
when :Coffin; bld.initBurialFlags
when :Trap; bld.unk_cc = 500 if bld.trap_type == :PressurePlate
end
bld
end
# used by building_setsize
def building_check_bridge_support(bld)
x1 = bld.x1-1
x2 = bld.x2+1
y1 = bld.y1-1
y2 = bld.y2+1
z = bld.z
(x1..x2).each { |x|
(y1..y2).each { |y|
next if ((x == x1 or x == x2) and
(y == y1 or y == y2))
if mb = map_block_at(x, y, z) and tile = mb.tiletype[x%16][y%16] and TiletypeShape::BasicShape[Tiletype::Shape[tile]] != :Open
bld.gate_flags.has_support = true
return
end
}
}
bld.gate_flags.has_support = false
end
# sets x2/centerx/y2/centery from x1/y1/bldtype
# x2/y2 preserved for :FarmPlot etc
def building_setsize(bld)
bld.x2 = bld.x1 if bld.x1 > bld.x2
bld.y2 = bld.y1 if bld.y1 > bld.y2
case bld.getType
when :Bridge
bld.centerx = bld.x1 + (bld.x2+1-bld.x1)/2
bld.centery = bld.y1 + (bld.y2+1-bld.y1)/2
building_check_bridge_support(bld)
when :FarmPlot, :RoadDirt, :RoadPaved, :Stockpile, :Civzone
bld.centerx = bld.x1 + (bld.x2+1-bld.x1)/2
bld.centery = bld.y1 + (bld.y2+1-bld.y1)/2
when :TradeDepot, :Shop
bld.x2 = bld.x1+4
bld.y2 = bld.y1+4
bld.centerx = bld.x1+2
bld.centery = bld.y1+2
when :SiegeEngine, :Windmill, :Wagon
bld.x2 = bld.x1+2
bld.y2 = bld.y1+2
bld.centerx = bld.x1+1
bld.centery = bld.y1+1
when :AxleHorizontal
if bld.is_vertical == 1
bld.x2 = bld.centerx = bld.x1
bld.centery = bld.y1 + (bld.y2+1-bld.y1)/2
else
bld.centerx = bld.x1 + (bld.x2+1-bld.x1)/2
bld.y2 = bld.centery = bld.y1
end
when :WaterWheel
if bld.is_vertical == 1
bld.x2 = bld.centerx = bld.x1
bld.y2 = bld.y1+2
bld.centery = bld.y1+1
else
bld.x2 = bld.x1+2
bld.centerx = bld.x1+1
bld.y2 = bld.centery = bld.y1
end
when :Workshop, :Furnace
# Furnace = Custom or default case only
case bld.type
when :Quern, :Millstone, :Tool
bld.x2 = bld.centerx = bld.x1
bld.y2 = bld.centery = bld.y1
when :Siege, :Kennels
bld.x2 = bld.x1+4
bld.y2 = bld.y1+4
bld.centerx = bld.x1+2
bld.centery = bld.y1+2
when :Custom
if bdef = world.raws.buildings.all.binsearch(bld.getCustomType)
bld.x2 = bld.x1 + bdef.dim_x - 1
bld.y2 = bld.y1 + bdef.dim_y - 1
bld.centerx = bld.x1 + bdef.workloc_x
bld.centery = bld.y1 + bdef.workloc_y
end
else
bld.x2 = bld.x1+2
bld.y2 = bld.y1+2
bld.centerx = bld.x1+1
bld.centery = bld.y1+1
end
when :ScrewPump
case bld.direction
when :FromEast
bld.x2 = bld.centerx = bld.x1+1
bld.y2 = bld.centery = bld.y1
when :FromSouth
bld.x2 = bld.centerx = bld.x1
bld.y2 = bld.centery = bld.y1+1
when :FromWest
bld.x2 = bld.x1+1
bld.y2 = bld.centery = bld.y1
bld.centerx = bld.x1
else
bld.x2 = bld.x1+1
bld.y2 = bld.centery = bld.y1
bld.centerx = bld.x1
end
when :Well
bld.bucket_z = bld.z
bld.x2 = bld.centerx = bld.x1
bld.y2 = bld.centery = bld.y1
when :Construction
bld.x2 = bld.centerx = bld.x1
bld.y2 = bld.centery = bld.y1
bld.setMaterialAmount(1)
return
else
bld.x2 = bld.centerx = bld.x1
bld.y2 = bld.centery = bld.y1
end
bld.setMaterialAmount((bld.x2-bld.x1+1)*(bld.y2-bld.y1+1)/4+1)
end
# set building at position, with optional width/height
def building_position(bld, pos, w=nil, h=nil)
bld.x1 = pos.x
bld.y1 = pos.y
bld.z = pos.z
bld.x2 = bld.x1+w-1 if w
bld.y2 = bld.y1+h-1 if h
building_setsize(bld)
end
# set map occupancy/stockpile/etc for a building
def building_setoccupancy(bld)
stockpile = (bld.getType == :Stockpile)
complete = (bld.getBuildStage >= bld.getMaxBuildStage)
extents = (bld.room.extents and bld.isExtentShaped)
z = bld.z
(bld.x1..bld.x2).each { |x|
(bld.y1..bld.y2).each { |y|
next if !extents or bld.room.extents[bld.room.width*(y-bld.room.y)+(x-bld.room.x)] == 0
next if not mb = map_block_at(x, y, z)
des = mb.designation[x%16][y%16]
des.pile = stockpile
des.dig = :No
if complete
bld.updateOccupancy(x, y)
else
mb.occupancy[x%16][y%16].building = :Planned
end
}
}
end
# link bld into other rooms if it is inside their extents
def building_linkrooms(bld)
didstuff = false
world.buildings.other[:ANY_FREE].each { |ob|
next if !ob.is_room or ob.z != bld.z
next if !ob.room.extents or !ob.isExtentShaped or ob.room.extents[ob.room.width*(bld.y1-ob.room.y)+(bld.x1-ob.room.x)] == 0
didstuff = true
ob.children << bld
bld.parents << ob
}
ui.equipment.update.buildings = true if didstuff
end
# link the building into the world, set map data, link rooms, bld.id
def building_link(bld)
bld.id = df.building_next_id
df.building_next_id += 1
world.buildings.all << bld
bld.categorize(true)
building_setoccupancy(bld) if bld.isSettingOccupancy
building_linkrooms(bld)
end
# set a design for the building
def building_createdesign(bld, rough=true)
job = bld.jobs[0]
job.mat_type = bld.mat_type
job.mat_index = bld.mat_index
if bld.needsDesign
bld.design = BuildingDesign.cpp_new
bld.design.flags.rough = rough
end
end
# creates a job to build bld, return it
def building_linkforconstruct(bld)
building_link bld
ref = GeneralRefBuildingHolderst.cpp_new
ref.building_id = bld.id
job = Job.cpp_new
job.job_type = :ConstructBuilding
job.pos = [bld.centerx, bld.centery, bld.z]
job.references << ref
bld.jobs << job
job_link job
job
end
# construct a building with items or JobItems
def building_construct(bld, items)
job = building_linkforconstruct(bld)
rough = false
items.each { |item|
if items.kind_of?(JobItem)
item.quantity = (bld.x2-bld.x1+1)*(bld.y2-bld.y1+1)/4+1 if item.quantity < 0
job.job_items << item
else
job_attachitem(job, item, :Hauled)
end
rough = true if item.getType == :BOULDER
bld.mat_type = item.getMaterial if bld.mat_type == -1
bld.mat_index = item.getMaterialIndex if bld.mat_index == -1
}
building_createdesign(bld, rough)
end
# creates a job to deconstruct the building
def building_deconstruct(bld)
job = Job.cpp_new
refbuildingholder = GeneralRefBuildingHolderst.cpp_new
job.job_type = :DestroyBuilding
refbuildingholder.building_id = building.id
job.references << refbuildingholder
building.jobs << job
job_link job
job
end
# exemple usage
def buildbed(pos=cursor)
raise 'where to ?' if pos.x < 0
item = world.items.all.find { |i|
i.kind_of?(ItemBedst) and
i.itemrefs.empty? and
!i.flags.in_job
}
raise 'no free bed, build more !' if not item
bld = building_alloc(:Bed)
building_position(bld, pos)
building_construct(bld, [item])
end
end
end

@ -8,11 +8,12 @@ use XML::LibXML;
our @lines_rb;
my $os;
if ($^O =~ /linux/i) {
if ($^O =~ /linux/i or $^O =~ /darwin/i) {
$os = 'linux';
} else {
$os = 'windows';
}
$os = $ARGV[2] if ($ARGV[2]);
sub indent_rb(&) {
my ($sub) = @_;
@ -260,6 +261,8 @@ sub render_struct_fields {
render_item($field);
};
push @lines_rb, "}";
render_struct_field_refs($type, $field, $name);
}
}
@ -267,6 +270,104 @@ sub render_struct_fields {
}
}
# handle generating accessor for xml attributes ref-target, refers-to etc
sub render_struct_field_refs {
my ($parent, $field, $name) = @_;
my $reftg = $field->getAttribute('ref-target');
render_field_reftarget($parent, $field, $name, $reftg) if ($reftg);
my $refto = $field->getAttribute('refers-to');
render_field_refto($parent, $name, $refto) if ($refto);
my $meta = $field->getAttribute('ld:meta');
my $item = $field->findnodes('child::ld:item')->[0];
if ($meta and $meta eq 'container' and $item) {
my $itemreftg = $item->getAttribute('ref-target');
render_container_reftarget($parent, $item, $name, $itemreftg) if $itemreftg;
}
}
sub render_field_reftarget {
my ($parent, $field, $name, $reftg) = @_;
my $aux = $field->getAttribute('aux-value');
return if ($aux); # TODO
my $tg = $global_types{$reftg};
return if (!$tg);
my $tgvec = $tg->getAttribute('instance-vector');
return if (!$tgvec);
my $idx = $tg->getAttribute('key-field');
$tgvec =~ s/^\$global/df/;
return if $tgvec !~ /^[\w\.]+$/;
my $tgname = "${name}_tg";
$tgname =~ s/_id(.?.?)_tg/_tg$1/;
for my $othername (map { $_->getAttribute('name') } $parent->findnodes('child::ld:field')) {
$tgname .= '_' if ($othername and $tgname eq $othername);
}
if ($idx) {
my $fidx = '';
$fidx = ', :' . $idx if ($idx ne 'id');
push @lines_rb, "def $tgname ; ${tgvec}.binsearch($name$fidx) ; end";
} else {
push @lines_rb, "def $tgname ; ${tgvec}[$name] ; end";
}
}
sub render_field_refto {
my ($parent, $name, $tgvec) = @_;
$tgvec =~ s/^\$global/df/;
$tgvec =~ s/\[\$\]$//;
return if $tgvec !~ /^[\w\.]+$/;
my $tgname = "${name}_tg";
$tgname =~ s/_id(.?.?)_tg/_tg$1/;
for my $othername (map { $_->getAttribute('name') } $parent->findnodes('child::ld:field')) {
$tgname .= '_' if ($othername and $tgname eq $othername);
}
push @lines_rb, "def $tgname ; ${tgvec}[$name] ; end";
}
sub render_container_reftarget {
my ($parent, $item, $name, $reftg) = @_;
my $aux = $item->getAttribute('aux-value');
return if ($aux); # TODO
my $tg = $global_types{$reftg};
return if (!$tg);
my $tgvec = $tg->getAttribute('instance-vector');
return if (!$tgvec);
my $idx = $tg->getAttribute('key-field');
$tgvec =~ s/^\$global/df/;
return if $tgvec !~ /^[\w\.]+$/;
my $tgname = "${name}_tg";
$tgname =~ s/_id(.?.?)_tg/_tg$1/;
for my $othername (map { $_->getAttribute('name') } $parent->findnodes('child::ld:field')) {
$tgname .= '_' if ($othername and $tgname eq $othername);
}
if ($idx) {
my $fidx = '';
$fidx = ', :' . $idx if ($idx ne 'id');
push @lines_rb, "def $tgname ; $name.map { |i| $tgvec.binsearch(i$fidx) } ; end";
} else {
push @lines_rb, "def $tgname ; $name.map { |i| ${tgvec}[i] } ; end";
}
}
sub render_class_vmethods {
my ($vms) = @_;
my $voff = 0;
@ -439,7 +540,9 @@ sub get_field_align {
if ($meta eq 'number') {
$al = $field->getAttribute('ld:bits')/8;
$al = 4 if $al > 4;
# linux aligns int64_t to 4, windows to 8
# floats are 4 bytes so no pb
$al = 4 if ($al > 4 and ($os eq 'linux' or $al != 8));
} elsif ($meta eq 'global') {
$al = get_global_align($field);
} elsif ($meta eq 'compound') {
@ -696,7 +799,7 @@ sub render_item_number {
$initvalue ||= 'nil' if $typename;
$subtype = $item->getAttribute('base-type') if (!$subtype or $subtype eq 'bitfield' or $subtype eq 'enum');
$subtype = $g->getAttribute('base-type') if ($g);
$subtype ||= $g->getAttribute('base-type') if ($g);
$subtype = 'int32_t' if (!$subtype);
if ($subtype eq 'int64_t') {
@ -715,7 +818,7 @@ sub render_item_number {
push @lines_rb, 'number 8, false';
} elsif ($subtype eq 'bool') {
push @lines_rb, 'number 8, true';
$initvalue ||= 'nil';
$initvalue ||= 'nil';
$typename ||= 'BooleanEnum';
} elsif ($subtype eq 's-float') {
push @lines_rb, 'float';

@ -0,0 +1,44 @@
module DFHack
class << self
# return an Item
# arg similar to unit.rb/unit_find; no arg = 'k' menu
def item_find(what=:selected, y=nil, z=nil)
if what == :selected
if curview._rtti_classname == :viewscreen_itemst
ref = curview.entry_ref[curview.cursor_pos]
ref.item_tg if ref.kind_of?(GeneralRefItem)
else
case ui.main.mode
when :LookAround
k = ui_look_list.items[ui_look_cursor]
case k.type
when :Item
k.item
when :Building
# hilight a constructed bed/coffer
mats = k.building.contained_items.find_all { |i| i.use_mode == 2 }
mats[0].item if mats.length == 1
end
when :BuildingItems
bld = world.selected_building
bld.contained_items[ui_building_item_cursor].item if bld
when :ViewUnits
u = world.units.active[ui_selected_unit]
u.inventory[ui_look_cursor].item if u and u.pos.z == cursor.z and
ui_unit_view_mode.value == :Inventory and u.inventory[ui_look_cursor]
end
end
elsif what.kind_of?(Integer)
# search by id
return world.items.all.binsearch(what) if not z
# search by position
x = what
world.items.all.find { |i| i.pos.x == x and i.pos.y == y and i.pos.z == z }
elsif what.respond_to?(:x) or what.respond_to?(:pos)
world.items.all.find { |i| same_pos?(what, i) }
else
raise "what what?"
end
end
end
end

@ -0,0 +1,35 @@
module DFHack
class << self
# link a job to the world
# allocate & set job.id, allocate a JobListLink, link to job & world.job_list
def job_link(job)
lastjob = world.job_list
lastjob = lastjob.next while lastjob.next
joblink = JobListLink.cpp_new
joblink.prev = lastjob
joblink.item = job
job.list_link = joblink
job.id = df.job_next_id
df.job_next_id += 1
lastjob.next = joblink
end
# attach an item to a job, flag item in_job
def job_attachitem(job, item, role=:Hauled, filter_idx=-1)
if role != :TargetContainer
item.flags.in_job = true
end
itemlink = SpecificRef.cpp_new
itemlink.type = :JOB
itemlink.job = job
item.specific_refs << itemlink
joblink = JobItemRef.cpp_new
joblink.item = item
joblink.role = role
joblink.job_item_idx = filter_idx
job.items << joblink
end
end
end

@ -0,0 +1,166 @@
module DFHack
class << self
# return a map block by tile coordinates
# you can also use find_map_block(cursor) or anything that respond to x/y/z
def map_block_at(x, y=nil, z=nil)
x = x.pos if x.respond_to?(:pos)
x, y, z = x.x, x.y, x.z if x.respond_to?(:x)
if x >= 0 and x < world.map.x_count and y >= 0 and y < world.map.y_count and z >= 0 and z < world.map.z_count
world.map.block_index[x/16][y/16][z]
end
end
def map_designation_at(x, y=nil, z=nil)
x = x.pos if x.respond_to?(:pos)
x, y, z = x.x, x.y, x.z if x.respond_to?(:x)
if b = map_block_at(x, y, z)
b.designation[x%16][y%16]
end
end
def map_occupancy_at(x, y=nil, z=nil)
x = x.pos if x.respond_to?(:pos)
x, y, z = x.x, x.y, x.z if x.respond_to?(:x)
if b = map_block_at(x, y, z)
b.occupancy[x%16][y%16]
end
end
def map_tile_at(x, y=nil, z=nil)
x = x.pos if x.respond_to?(:pos)
x, y, z = x.x, x.y, x.z if x.respond_to?(:x)
b = map_block_at(x, y, z)
MapTile.new(b, x, y, z) if b
end
# yields every map block
def each_map_block
(0...world.map.x_count_block).each { |xb|
xl = world.map.block_index[xb]
(0...world.map.y_count_block).each { |yb|
yl = xl[yb]
(0...world.map.z_count_block).each { |z|
p = yl[z]
yield p if p
}
}
}
end
# yields every map block for a given z level
def each_map_block_z(z)
(0...world.map.x_count_block).each { |xb|
xl = world.map.block_index[xb]
(0...world.map.y_count_block).each { |yb|
p = xl[yb][z]
yield p if p
}
}
end
end
class MapTile
attr_accessor :x, :y, :z, :dx, :dy, :mapblock
def initialize(b, x, y, z)
@x, @y, @z = x, y, z
@dx, @dy = @x&15, @y&15
@mapblock = b
end
def designation
@mapblock.designation[@dx][@dy]
end
def occupancy
@mapblock.occupancy[@dx][@dy]
end
def tiletype
@mapblock.tiletype[@dx][@dy]
end
def tiletype=(t)
@mapblock.tiletype[@dx][@dy] = t
end
def caption
Tiletype::Caption[tiletype]
end
def shape
Tiletype::Shape[tiletype]
end
def tilemat
Tiletype::Material[tiletype]
end
def variant
Tiletype::Variant[tiletype]
end
def special
Tiletype::Special[tiletype]
end
def direction
Tiletype::Direction[tiletype]
end
# return all veins for current mapblock
def all_veins
mapblock.block_events.grep(BlockSquareEventMineralst)
end
# return the vein applicable to current tile
def vein
# last vein wins
all_veins.reverse.find { |v|
(v.tile_bitmask.bits[@dy] & (1 << @dx)) > 0
}
end
# return the mat_index for the current tile (if in vein)
def mat_index_vein
v = vein
v.inorganic_mat if v
end
# return the world_data.geo_biome for current tile
def geo_biome
b = designation.biome
wd = df.world.world_data
# region coords + [[-1, -1], [0, -1], ..., [1, 1]][b]
# clipped to world dimensions
rx = df.world.map.region_x/16
rx -= 1 if b % 3 == 0 and rx > 0
rx += 1 if b % 3 == 2 and rx < wd.world_width-1
ry = df.world.map.region_y/16
ry -= 1 if b < 3 and ry > 0
ry += 1 if b > 5 and ry < wd.world_height-1
wd.geo_biomes[ wd.region_map[rx][ry].geo_index ]
end
# return the world_data.geo_biome.layer for current tile
def stone_layer
geo_biome.layers[designation.geolayer_index]
end
# current tile mat_index (vein if applicable, or base material)
def mat_index
mat_index_vein or stone_layer.mat_index
end
# MaterialInfo: inorganic token for current tile
def mat_info
MaterialInfo.new(0, mat_index)
end
def inspect
"#<MapTile pos=[#@x, #@y, #@z] shape=#{shape} tilemat=#{tilemat} material=#{mat_info.token}>"
end
end
end

@ -0,0 +1,199 @@
module DFHack
class MaterialInfo
attr_accessor :mat_type, :mat_index
attr_accessor :mode, :material, :creature, :figure, :plant, :inorganic
def initialize(what, idx=nil)
case what
when Integer
@mat_type, @mat_index = what, idx
decode_type_index
when String
decode_string(what)
else
@mat_type, @mat_index = what.mat_type, what.mat_index
decode_type_index
end
end
CREATURE_BASE = 19
FIGURE_BASE = CREATURE_BASE+200
PLANT_BASE = FIGURE_BASE+200
END_BASE = PLANT_BASE+200
# interpret the mat_type and mat_index fields
def decode_type_index
if @mat_index < 0 or @mat_type >= END_BASE
@mode = :Builtin
@material = df.world.raws.mat_table.builtin[@mat_type]
elsif @mat_type >= PLANT_BASE
@mode = :Plant
@plant = df.world.raws.plants.all[@mat_index]
@material = @plant.material[@mat_type-PLANT_BASE] if @plant
elsif @mat_type >= FIGURE_BASE
@mode = :Figure
@figure = df.world.history.figures.binsearch(@mat_index)
@creature = df.world.raws.creatures.all[@figure.race] if @figure
@material = @creature.material[@mat_type-FIGURE_BASE] if @creature
elsif @mat_type >= CREATURE_BASE
@mode = :Creature
@creature = df.world.raws.creatures.all[@mat_index]
@material = @creature.material[@mat_type-CREATURE_BASE] if @creature
elsif @mat_type > 0
@mode = :Builtin
@material = df.world.raws.mat_table.builtin[@mat_type]
elsif @mat_type == 0
@mode = :Inorganic
@inorganic = df.world.raws.inorganics[@mat_index]
@material = @inorganic.material if @inorganic
end
end
def decode_string(str)
parts = str.split(':')
case parts[0].chomp('_MAT')
when 'INORGANIC', 'STONE', 'METAL'
decode_string_inorganic(parts)
when 'PLANT'
decode_string_plant(parts)
when 'CREATURE'
if parts[3] and parts[3] != 'NONE'
decode_string_figure(parts)
else
decode_string_creature(parts)
end
when 'INVALID'
@mat_type = parts[1].to_i
@mat_index = parts[2].to_i
else
decode_string_builtin(parts)
end
end
def decode_string_inorganic(parts)
@@inorganics_index ||= (0...df.world.raws.inorganics.length).inject({}) { |h, i| h.update df.world.raws.inorganics[i].id => i }
@mode = :Inorganic
@mat_type = 0
if parts[1] and parts[1] != 'NONE'
@mat_index = @@inorganics_index[parts[1]]
raise "invalid inorganic token #{parts.join(':').inspect}" if not @mat_index
@inorganic = df.world.raws.inorganics[@mat_index]
@material = @inorganic.material
end
end
def decode_string_builtin(parts)
@@builtins_index ||= (1...df.world.raws.mat_table.builtin.length).inject({}) { |h, i| b = df.world.raws.mat_table.builtin[i] ; b ? h.update(b.id => i) : h }
@mode = :Builtin
@mat_index = -1
@mat_type = @@builtins_index[parts[0]]
raise "invalid builtin token #{parts.join(':').inspect}" if not @mat_type
@material = df.world.raws.mat_table.builtin[@mat_type]
if parts[0] == 'COAL' and parts[1]
@mat_index = ['COKE', 'CHARCOAL'].index(parts[1]) || -1
end
end
def decode_string_creature(parts)
@@creatures_index ||= (0...df.world.raws.creatures.all.length).inject({}) { |h, i| h.update df.world.raws.creatures.all[i].creature_id => i }
@mode = :Creature
if parts[1] and parts[1] != 'NONE'
@mat_index = @@creatures_index[parts[1]]
raise "invalid creature token #{parts.join(':').inspect}" if not @mat_index
@creature = df.world.raws.creatures.all[@mat_index]
end
if @creature and parts[2] and parts[2] != 'NONE'
@mat_type = @creature.material.index { |m| m.id == parts[2] }
@material = @creature.material[@mat_type]
@mat_type += CREATURE_BASE
end
end
def decode_string_figure(parts)
@mode = :Figure
@mat_index = parts[3].to_i
@figure = df.world.history.figures.binsearch(@mat_index)
raise "invalid creature histfig #{parts.join(':').inspect}" if not @figure
@creature = df.world.raws.creatures.all[@figure.race]
if parts[1] and parts[1] != 'NONE'
raise "invalid histfig race #{parts.join(':').inspect}" if @creature.creature_id != parts[1]
end
if @creature and parts[2] and parts[2] != 'NONE'
@mat_type = @creature.material.index { |m| m.id == parts[2] }
@material = @creature.material[@mat_type]
@mat_type += FIGURE_BASE
end
end
def decode_string_plant(parts)
@@plants_index ||= (0...df.world.raws.plants.all.length).inject({}) { |h, i| h.update df.world.raws.plants.all[i].id => i }
@mode = :Plant
if parts[1] and parts[1] != 'NONE'
@mat_index = @@plants_index[parts[1]]
raise "invalid plant token #{parts.join(':').inspect}" if not @mat_index
@plant = df.world.raws.plants.all[@mat_index]
end
if @plant and parts[2] and parts[2] != 'NONE'
@mat_type = @plant.material.index { |m| m.id == parts[2] }
raise "invalid plant type #{parts.join(':').inspect}" if not @mat_type
@material = @plant.material[@mat_type]
@mat_type += PLANT_BASE
end
end
# delete the caches of raws id => index used in decode_string
def self.flush_raws_cache
@@inorganics_index = @@plants_index = @@creatures_index = @@builtins_index = nil
end
def token
out = []
case @mode
when :Builtin
out << (@material ? @material.id : 'NONE')
out << (['COKE', 'CHARCOAL'][@mat_index] || 'NONE') if @material and @material.id == 'COAL' and @mat_index >= 0
when :Inorganic
out << 'INORGANIC'
out << @inorganic.id if @inorganic
when :Plant
out << 'PLANT_MAT'
out << @plant.id if @plant
out << @material.id if @plant and @material
when :Creature, :Figure
out << 'CREATURE_MAT'
out << @creature.creature_id if @creature
out << @material.id if @creature and @material
out << @figure.id.to_s if @creature and @material and @figure
else
out << 'INVALID'
out << @mat_type.to_s
out << @mat_index.to_s
end
out.join(':')
end
def to_s ; token ; end
end
class << self
def decode_mat(what, idx=nil)
MaterialInfo.new(what, idx)
end
end
end

@ -0,0 +1,111 @@
module DFHack
class << self
# return a Plant
# arg similar to unit.rb/unit_find, no menu
def plant_find(what=cursor)
if what.kind_of?(Integer)
world.items.all.binsearch(what)
elsif what.respond_to?(:x) or what.respond_to?(:pos)
world.plants.all.find { |p| same_pos?(what, p) }
else
raise "what what?"
end
end
def each_tree(material=:any)
@raws_tree_name ||= {}
if @raws_tree_name.empty?
df.world.raws.plants.all.each_with_index { |p, idx|
@raws_tree_name[idx] = p.id if p.flags[:TREE]
}
end
if material != :any
mat = match_rawname(material, @raws_tree_name.values)
unless wantmat = @raws_tree_name.index(mat)
raise "invalid tree material #{material}"
end
end
world.plants.all.each { |plant|
next if not @raws_tree_name[plant.material]
next if wantmat and plant.material != wantmat
yield plant
}
end
def each_shrub(material=:any)
@raws_shrub_name ||= {}
if @raws_tree_name.empty?
df.world.raws.plants.all.each_with_index { |p, idx|
@raws_shrub_name[idx] = p.id if not p.flags[:GRASS] and not p.flags[:TREE]
}
end
if material != :any
mat = match_rawname(material, @raws_shrub_name.values)
unless wantmat = @raws_shrub_name.index(mat)
raise "invalid shrub material #{material}"
end
end
end
SaplingToTreeAge = 120960
def cuttrees(material=nil, count_max=100)
if !material
# list trees
cnt = Hash.new(0)
each_tree { |plant|
next if plant.grow_counter < SaplingToTreeAge
next if map_designation_at(plant).hidden
cnt[plant.material] += 1
}
cnt.sort_by { |mat, c| c }.each { |mat, c|
name = @raws_tree_name[mat]
puts " #{name} #{c}"
}
else
cnt = 0
each_tree(material) { |plant|
next if plant.grow_counter < SaplingToTreeAge
b = map_block_at(plant)
d = b.designation[plant.pos.x%16][plant.pos.y%16]
next if d.hidden
if d.dig == :No
d.dig = :Default
b.flags.designated = true
cnt += 1
break if cnt == count_max
end
}
puts "Updated #{cnt} plant designations"
end
end
def growtrees(material=nil, count_max=100)
if !material
# list plants
cnt = Hash.new(0)
each_tree { |plant|
next if plant.grow_counter >= SaplingToTreeAge
next if map_designation_at(plant).hidden
cnt[plant.material] += 1
}
cnt.sort_by { |mat, c| c }.each { |mat, c|
name = @raws_tree_name[mat]
puts " #{name} #{c}"
}
else
cnt = 0
each_tree(material) { |plant|
next if plant.grow_counter >= SaplingToTreeAge
next if map_designation_at(plant).hidden
plant.grow_counter = SaplingToTreeAge
cnt += 1
break if cnt == count_max
}
puts "Grown #{cnt} saplings"
end
end
end
end

@ -1,266 +0,0 @@
module DFHack
# allocate a new building object
def self.building_alloc(type, subtype=-1, custom=-1)
cls = rtti_n2c[BuildingType::Classname[type].to_sym]
raise "invalid building type #{type.inspect}" if not cls
bld = cls.cpp_new
bld.race = ui.race_id
bld.setSubtype(subtype) if subtype != -1
bld.setCustomType(custom) if custom != -1
case type
when :Furnace; bld.melt_remainder[world.raws.inorganics.length] = 0
when :Coffin; bld.initBurialFlags
when :Trap; bld.unk_cc = 500 if bld.trap_type == :PressurePlate
end
bld
end
# used by building_setsize
def self.building_check_bridge_support(bld)
x1 = bld.x1-1
x2 = bld.x2+1
y1 = bld.y1-1
y2 = bld.y2+1
z = bld.z
(x1..x2).each { |x|
(y1..y2).each { |y|
next if ((x == x1 or x == x2) and
(y == y1 or y == y2))
if mb = map_block_at(x, y, z) and tile = mb.tiletype[x%16][y%16] and TiletypeShape::BasicShape[Tiletype::Shape[tile]] == :Open
bld.gate_flags.has_support = true
return
end
}
}
bld.gate_flags.has_support = false
end
# sets x2/centerx/y2/centery from x1/y1/bldtype
# x2/y2 preserved for :FarmPlot etc
def self.building_setsize(bld)
bld.x2 = bld.x1 if bld.x1 > bld.x2
bld.y2 = bld.y1 if bld.y1 > bld.y2
case bld.getType
when :Bridge
bld.centerx = bld.x1 + (bld.x2+1-bld.x1)/2
bld.centery = bld.y1 + (bld.y2+1-bld.y1)/2
building_check_bridge_support(bld)
when :FarmPlot, :RoadDirt, :RoadPaved, :Stockpile, :Civzone
bld.centerx = bld.x1 + (bld.x2+1-bld.x1)/2
bld.centery = bld.y1 + (bld.y2+1-bld.y1)/2
when :TradeDepot, :Shop
bld.x2 = bld.x1+4
bld.y2 = bld.y1+4
bld.centerx = bld.x1+2
bld.centery = bld.y1+2
when :SiegeEngine, :Windmill, :Wagon
bld.x2 = bld.x1+2
bld.y2 = bld.y1+2
bld.centerx = bld.x1+1
bld.centery = bld.y1+1
when :AxleHorizontal
if bld.is_vertical == 1
bld.x2 = bld.centerx = bld.x1
bld.centery = bld.y1 + (bld.y2+1-bld.y1)/2
else
bld.centerx = bld.x1 + (bld.x2+1-bld.x1)/2
bld.y2 = bld.centery = bld.y1
end
when :WaterWheel
if bld.is_vertical == 1
bld.x2 = bld.centerx = bld.x1
bld.y2 = bld.y1+2
bld.centery = bld.y1+1
else
bld.x2 = bld.x1+2
bld.centerx = bld.x1+1
bld.y2 = bld.centery = bld.y1
end
when :Workshop, :Furnace
# Furnace = Custom or default case only
case bld.type
when :Quern, :Millstone, :Tool
bld.x2 = bld.centerx = bld.x1
bld.y2 = bld.centery = bld.y1
when :Siege, :Kennels
bld.x2 = bld.x1+4
bld.y2 = bld.y1+4
bld.centerx = bld.x1+2
bld.centery = bld.y1+2
when :Custom
if bdef = world.raws.buildings.all.binsearch(bld.getCustomType)
bld.x2 = bld.x1 + bdef.dim_x - 1
bld.y2 = bld.y1 + bdef.dim_y - 1
bld.centerx = bld.x1 + bdef.workloc_x
bld.centery = bld.y1 + bdef.workloc_y
end
else
bld.x2 = bld.x1+2
bld.y2 = bld.y1+2
bld.centerx = bld.x1+1
bld.centery = bld.y1+1
end
when :ScrewPump
case bld.direction
when :FromEast
bld.x2 = bld.centerx = bld.x1+1
bld.y2 = bld.centery = bld.y1
when :FromSouth
bld.x2 = bld.centerx = bld.x1
bld.y2 = bld.centery = bld.y1+1
when :FromWest
bld.x2 = bld.x1+1
bld.y2 = bld.centery = bld.y1
bld.centerx = bld.x1
else
bld.x2 = bld.x1+1
bld.y2 = bld.centery = bld.y1
bld.centerx = bld.x1
end
when :Well
bld.bucket_z = bld.z
bld.x2 = bld.centerx = bld.x1
bld.y2 = bld.centery = bld.y1
when :Construction
bld.x2 = bld.centerx = bld.x1
bld.y2 = bld.centery = bld.y1
bld.setMaterialAmount(1)
return
else
bld.x2 = bld.centerx = bld.x1
bld.y2 = bld.centery = bld.y1
end
bld.setMaterialAmount((bld.x2-bld.x1+1)*(bld.y2-bld.y1+1)/4+1)
end
# set building at position, with optional width/height
def self.building_position(bld, pos, w=nil, h=nil)
bld.x1 = pos.x
bld.y1 = pos.y
bld.z = pos.z
bld.x2 = bld.x1+w-1 if w
bld.y2 = bld.y1+h-1 if h
building_setsize(bld)
end
# set map occupancy/stockpile/etc for a building
def self.building_setoccupancy(bld)
stockpile = (bld.getType == :Stockpile)
complete = (bld.getBuildStage >= bld.getMaxBuildStage)
extents = (bld.room.extents and bld.isExtentShaped)
z = bld.z
(bld.x1..bld.x2).each { |x|
(bld.y1..bld.y2).each { |y|
next if !extents or bld.room.extents[bld.room.width*(y-bld.room.y)+(x-bld.room.x)] == 0
next if not mb = map_block_at(x, y, z)
des = mb.designation[x%16][y%16]
des.pile = stockpile
des.dig = :No
if complete
bld.updateOccupancy(x, y)
else
mb.occupancy[x%16][y%16].building = :Planned
end
}
}
end
# link bld into other rooms if it is inside their extents
def self.building_linkrooms(bld)
didstuff = false
world.buildings.other[:ANY_FREE].each { |ob|
next if !ob.is_room or ob.z != bld.z
next if !ob.room.extents or !ob.isExtentShaped or ob.room.extents[ob.room.width*(bld.y1-ob.room.y)+(bld.x1-ob.room.x)] == 0
didstuff = true
ob.children << bld
bld.parents << ob
}
ui.equipment.update.buildings = true if didstuff
end
# link the building into the world, set map data, link rooms, bld.id
def self.building_link(bld)
bld.id = df.building_next_id
df.building_next_id += 1
world.buildings.all << bld
bld.categorize(true)
building_setoccupancy(bld) if bld.isSettingOccupancy
building_linkrooms(bld)
end
# set a design for the building
def self.building_createdesign(bld, rough=true)
job = bld.jobs[0]
job.mat_type = bld.mat_type
job.mat_index = bld.mat_index
if bld.needsDesign
bld.design = BuildingDesign.cpp_new
bld.design.flags.rough = rough
end
end
# creates a job to build bld, return it
def self.building_linkforconstruct(bld)
building_link bld
ref = GeneralRefBuildingHolderst.cpp_new
ref.building_id = bld.id
job = Job.cpp_new
job.job_type = :ConstructBuilding
job.pos = [bld.centerx, bld.centery, bld.z]
job.references << ref
bld.jobs << job
job_link job
job
end
# construct a building with items or JobItems
def self.building_construct(bld, items)
job = building_linkforconstruct(bld)
rough = false
items.each { |item|
if items.kind_of?(JobItem)
item.quantity = (bld.x2-bld.x1+1)*(bld.y2-bld.y1+1)/4+1 if item.quantity < 0
job.job_items << item
else
job_attachitem(job, item, :Hauled)
end
rough = true if item.getType == :BOULDER
bld.mat_type = item.getMaterial if bld.mat_type == -1
bld.mat_index = item.getMaterialIndex if bld.mat_index == -1
}
building_createdesign(bld, rough)
end
# creates a job to deconstruct the building
def self.building_deconstruct(bld)
job = Job.cpp_new
refbuildingholder = GeneralRefBuildingHolderst.cpp_new
job.job_type = :DestroyBuilding
refbuildingholder.building_id = building.id
job.references << refbuildingholder
building.jobs << job
job_link job
job
end
# exemple usage
def self.buildbed(pos=cursor)
suspend {
raise 'where to ?' if pos.x < 0
item = world.items.all.find { |i|
i.kind_of?(ItemBedst) and
i.itemrefs.empty? and
!i.flags.in_job
}
raise 'no free bed, build more !' if not item
bld = building_alloc(:Bed)
building_position(bld, pos)
building_construct(bld, [item])
}
end
end

@ -1,152 +0,0 @@
module DFHack
def self.each_tree(material=:any)
@raws_tree_name ||= {}
if @raws_tree_name.empty?
df.world.raws.plants.all.each_with_index { |p, idx|
@raws_tree_name[idx] = p.id if p.flags[:TREE]
}
end
if material != :any
mat = match_rawname(material, @raws_tree_name.values)
unless wantmat = @raws_tree_name.index(mat)
raise "invalid tree material #{material}"
end
end
world.plants.all.each { |plant|
next if not @raws_tree_name[plant.material]
next if wantmat and plant.material != wantmat
yield plant
}
end
def self.each_shrub(material=:any)
@raws_shrub_name ||= {}
if @raws_tree_name.empty?
df.world.raws.plants.all.each_with_index { |p, idx|
@raws_shrub_name[idx] = p.id if not p.flags[:GRASS] and not p.flags[:TREE]
}
end
if material != :any
mat = match_rawname(material, @raws_shrub_name.values)
unless wantmat = @raws_shrub_name.index(mat)
raise "invalid shrub material #{material}"
end
end
end
SaplingToTreeAge = 120960
def self.cuttrees(material=nil, count_max=100)
if !material
# list trees
cnt = Hash.new(0)
suspend {
each_tree { |plant|
next if plant.grow_counter < SaplingToTreeAge
next if map_designation_at(plant).hidden
cnt[plant.material] += 1
}
}
cnt.sort_by { |mat, c| c }.each { |mat, c|
name = @raws_tree_name[mat]
puts " #{name} #{c}"
}
else
cnt = 0
suspend {
each_tree(material) { |plant|
next if plant.grow_counter < SaplingToTreeAge
b = map_block_at(plant)
d = b.designation[plant.pos.x%16][plant.pos.y%16]
next if d.hidden
if d.dig == :No
d.dig = :Default
b.flags.designated = true
cnt += 1
break if cnt == count_max
end
}
}
puts "Updated #{cnt} plant designations"
end
end
def self.growtrees(material=nil, count_max=100)
if !material
# list plants
cnt = Hash.new(0)
suspend {
each_tree { |plant|
next if plant.grow_counter >= SaplingToTreeAge
next if map_designation_at(plant).hidden
cnt[plant.material] += 1
}
}
cnt.sort_by { |mat, c| c }.each { |mat, c|
name = @raws_tree_name[mat]
puts " #{name} #{c}"
}
else
cnt = 0
suspend {
each_tree(material) { |plant|
next if plant.grow_counter >= SaplingToTreeAge
next if map_designation_at(plant).hidden
plant.grow_counter = SaplingToTreeAge
cnt += 1
break if cnt == count_max
}
}
puts "Grown #{cnt} saplings"
end
end
def self.growcrops(material=nil, count_max=100)
@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
if !material
cnt = Hash.new(0)
suspend {
world.items.other[:SEEDS].each { |seed|
next if not seed.flags.in_building
next if not seed.itemrefs.find { |ref| ref._rtti_classname == :general_ref_building_holderst }
next if seed.grow_counter >= @raws_plant_growdur[seed.mat_index]
cnt[seed.mat_index] += 1
}
}
cnt.sort_by { |mat, c| c }.each { |mat, c|
name = world.raws.plants.all[mat].id
puts " #{name} #{c}"
}
else
if material != :any
mat = match_rawname(material, @raws_plant_name.values)
unless wantmat = @raws_plant_name.index(mat)
raise "invalid plant material #{material}"
end
end
cnt = 0
suspend {
world.items.other[:SEEDS].each { |seed|
next if wantmat and seed.mat_index != wantmat
next if not seed.flags.in_building
next if not seed.itemrefs.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]
cnt += 1
}
}
puts "Grown #{cnt} crops"
end
end
end

@ -1,52 +0,0 @@
module DFHack
# returns an Array of all units that are current fort citizen (dwarves, on map, not hostile)
def self.unit_citizens
race = ui.race_id
civ = ui.civ_id
world.units.active.find_all { |u|
u.race == race and u.civ_id == civ and !u.flags1.dead and !u.flags1.merchant and
!u.flags1.diplomat and !u.flags2.resident and !u.flags3.ghostly and
!u.curse.add_tags1.OPPOSED_TO_LIFE and !u.curse.add_tags1.CRAZED and
u.mood != :Berserk
# TODO check curse ; currently this should keep vampires, but may include werebeasts
}
end
# list workers (citizen, not crazy / child / inmood / noble)
def self.unit_workers
unit_citizens.find_all { |u|
u.mood == :None and
u.profession != :CHILD and
u.profession != :BABY and
# TODO MENIAL_WORK_EXEMPTION_SPOUSE
!unit_entitypositions(u).find { |pos| pos.flags[:MENIAL_WORK_EXEMPTION] }
}
end
# list currently idle workers
def self.unit_idlers
unit_workers.find_all { |u|
# current_job includes eat/drink/sleep/pickupequip
!u.job.current_job._getv and
# filter 'attend meeting'
u.meetings.length == 0 and
# filter soldiers (TODO check schedule)
u.military.squad_index == -1 and
# filter 'on break'
!u.status.misc_traits.find { |t| id == :OnBreak }
}
end
def self.unit_entitypositions(unit)
list = []
return list if not hf = world.history.figures.binsearch(unit.hist_figure_id)
hf.entity_links.each { |el|
next if el._rtti_classname != :histfig_entity_link_positionst
next if not ent = world.entities.all.binsearch(el.entity_id)
next if not pa = ent.positions.assignments.binsearch(el.assignment_id)
next if not pos = ent.positions.own.binsearch(pa.position_id)
list << pos
}
list
end
end

@ -0,0 +1,753 @@
# definition of classes used by ruby-autogen
module DFHack
module MemHack
INSPECT_SIZE_LIMIT=16384
class MemStruct
attr_accessor :_memaddr
def _at(addr) ; d = dup ; d._memaddr = addr ; d ; end
def _get ; self ; end
def _cpp_init ; end
end
class Compound < MemStruct
class << self
attr_accessor :_fields, :_rtti_classname, :_sizeof
def field(name, offset)
struct = yield
return if not struct
@_fields ||= []
@_fields << [name, offset, struct]
define_method(name) { struct._at(@_memaddr+offset)._get }
define_method("#{name}=") { |v| struct._at(@_memaddr+offset)._set(v) }
end
def _fields_ancestors
if superclass.respond_to?(:_fields_ancestors)
superclass._fields_ancestors + _fields.to_a
else
_fields.to_a
end
end
def number(bits, signed, initvalue=nil, enum=nil)
Number.new(bits, signed, initvalue, enum)
end
def float
Float.new
end
def bit(shift)
BitField.new(shift, 1)
end
def bits(shift, len, enum=nil)
BitField.new(shift, len, enum)
end
def pointer
Pointer.new((yield if block_given?))
end
def pointer_ary(tglen)
PointerAry.new(tglen, yield)
end
def static_array(len, tglen, indexenum=nil)
StaticArray.new(tglen, len, indexenum, yield)
end
def static_string(len)
StaticString.new(len)
end
def stl_vector(tglen=nil)
tg = yield if tglen
case tglen
when 1; StlVector8.new(tg)
when 2; StlVector16.new(tg)
else StlVector32.new(tg)
end
end
def stl_string
StlString.new
end
def stl_bit_vector
StlBitVector.new
end
def stl_deque(tglen)
StlDeque.new(tglen, yield)
end
def df_flagarray(indexenum=nil)
DfFlagarray.new(indexenum)
end
def df_array(tglen)
DfArray.new(tglen, yield)
end
def df_linked_list
DfLinkedList.new(yield)
end
def global(glob)
Global.new(glob)
end
def compound(name=nil, &b)
m = Class.new(Compound)
DFHack.const_set(name, m) if name
m.class_eval(&b)
m.new
end
def rtti_classname(n)
DFHack.rtti_register(n, self)
@_rtti_classname = n
end
def sizeof(n)
@_sizeof = n
end
# allocate a new c++ object, return its ruby wrapper
def cpp_new(init=nil)
ptr = DFHack.malloc(_sizeof)
if _rtti_classname and vt = DFHack.rtti_getvtable(_rtti_classname)
DFHack.memory_write_int32(ptr, vt)
# TODO call constructor
end
o = new._at(ptr)
o._cpp_init
o._set(init) if init
o
end
end
def _cpp_init
_fields_ancestors.each { |n, o, s| s._at(@_memaddr+o)._cpp_init }
end
def _set(h)
case h
when Hash; h.each { |k, v| send("#{k}=", v) }
when Array; names = _field_names ; raise 'bad size' if names.length != h.length ; names.zip(h).each { |n, a| send("#{n}=", a) }
when Compound; _field_names.each { |n| send("#{n}=", h.send(n)) }
else raise 'wut?'
end
end
def _fields ; self.class._fields.to_a ; end
def _fields_ancestors ; self.class._fields_ancestors.to_a ; end
def _field_names ; _fields_ancestors.map { |n, o, s| n } ; end
def _rtti_classname ; self.class._rtti_classname ; end
def _sizeof ; self.class._sizeof ; end
@@inspecting = {} # avoid infinite recursion on mutually-referenced objects
def inspect
cn = self.class.name.sub(/^DFHack::/, '')
cn << ' @' << ('0x%X' % _memaddr) if cn != ''
out = "#<#{cn}"
return out << ' ...>' if @@inspecting[_memaddr]
@@inspecting[_memaddr] = true
_fields_ancestors.each { |n, o, s|
out << ' ' if out.length != 0 and out[-1, 1] != ' '
if out.length > INSPECT_SIZE_LIMIT
out << '...'
break
end
out << inspect_field(n, o, s)
}
out.chomp!(' ')
@@inspecting.delete _memaddr
out << '>'
end
def inspect_field(n, o, s)
if s.kind_of?(BitField) and s._len == 1
send(n) ? n.to_s : ''
elsif s.kind_of?(Pointer)
"#{n}=#{s._at(@_memaddr+o).inspect}"
elsif n == :_whole
"_whole=0x#{_whole.to_s(16)}"
else
v = send(n).inspect
"#{n}=#{v}"
end
rescue
"#{n}=ERR(#{$!})"
end
end
class Enum
# number -> symbol
def self.enum
# ruby weirdness, needed to make the constants 'virtual'
@enum ||= const_get(:ENUM)
end
# symbol -> number
def self.nume
@nume ||= const_get(:NUME)
end
def self.int(i)
nume[i] || i
end
def self.sym(i)
enum[i] || i
end
end
class Number < MemStruct
attr_accessor :_bits, :_signed, :_initvalue, :_enum
def initialize(bits, signed, initvalue, enum)
@_bits = bits
@_signed = signed
@_initvalue = initvalue
@_enum = enum
end
def _get
v = case @_bits
when 32; DFHack.memory_read_int32(@_memaddr)
when 16; DFHack.memory_read_int16(@_memaddr)
when 8; DFHack.memory_read_int8( @_memaddr)
when 64;(DFHack.memory_read_int32(@_memaddr) & 0xffffffff) + (DFHack.memory_read_int32(@_memaddr+4) << 32)
end
v &= (1 << @_bits) - 1 if not @_signed
v = @_enum.sym(v) if @_enum
v
end
def _set(v)
v = @_enum.int(v) if @_enum
case @_bits
when 32; DFHack.memory_write_int32(@_memaddr, v)
when 16; DFHack.memory_write_int16(@_memaddr, v)
when 8; DFHack.memory_write_int8( @_memaddr, v)
when 64; DFHack.memory_write_int32(@_memaddr, v & 0xffffffff) ; DFHack.memory_write_int32(@memaddr+4, v>>32)
end
end
def _cpp_init
_set(@_initvalue) if @_initvalue
end
end
class Float < MemStruct
def _get
DFHack.memory_read_float(@_memaddr)
end
def _set(v)
DFHack.memory_write_float(@_memaddr, v)
end
def _cpp_init
_set(0.0)
end
end
class BitField < MemStruct
attr_accessor :_shift, :_len, :_enum
def initialize(shift, len, enum=nil)
@_shift = shift
@_len = len
@_enum = enum
end
def _mask
(1 << @_len) - 1
end
def _get
v = DFHack.memory_read_int32(@_memaddr) >> @_shift
if @_len == 1
((v & 1) == 0) ? false : true
else
v &= _mask
v = @_enum.sym(v) if @_enum
v
end
end
def _set(v)
if @_len == 1
# allow 'bit = 0'
v = (v && v != 0 ? 1 : 0)
end
v = @_enum.int(v) if @_enum
v = (v & _mask) << @_shift
ori = DFHack.memory_read_int32(@_memaddr) & 0xffffffff
DFHack.memory_write_int32(@_memaddr, ori - (ori & ((-1 & _mask) << @_shift)) + v)
end
end
class Pointer < MemStruct
attr_accessor :_tg
def initialize(tg)
@_tg = tg
end
def _getp
DFHack.memory_read_int32(@_memaddr) & 0xffffffff
end
def _get
addr = _getp
return if addr == 0
return addr if not @_tg
@_tg._at(addr)._get
end
# XXX shaky...
def _set(v)
if v.kind_of?(Pointer)
DFHack.memory_write_int32(@_memaddr, v._getp)
elsif v.kind_of?(MemStruct)
DFHack.memory_write_int32(@_memaddr, v._memaddr)
else
_get._set(v)
end
end
def inspect
ptr = _getp
if ptr == 0
'NULL'
else
cn = ''
cn = @_tg.class.name.sub(/^DFHack::/, '').sub(/^MemHack::/, '') if @_tg
cn = @_tg._glob if cn == 'Global'
"#<Pointer #{cn} #{'0x%X' % _getp}>"
end
end
end
class PointerAry < MemStruct
attr_accessor :_tglen, :_tg
def initialize(tglen, tg)
@_tglen = tglen
@_tg = tg
end
def _getp(i=0)
delta = (i != 0 ? i*@_tglen : 0)
(DFHack.memory_read_int32(@_memaddr) & 0xffffffff) + delta
end
def _get
addr = _getp
return if addr == 0
self
end
def [](i)
addr = _getp(i)
return if addr == 0
@_tg._at(addr)._get
end
def []=(i, v)
addr = _getp(i)
raise 'null pointer' if addr == 0
@_tg._at(addr)._set(v)
end
def inspect ; ptr = _getp ; (ptr == 0) ? 'NULL' : "#<PointerAry #{'0x%X' % ptr}>" ; end
end
module Enumerable
include ::Enumerable
attr_accessor :_indexenum
def each ; (0...length).each { |i| yield self[i] } ; end
def inspect
out = '['
each_with_index { |e, idx|
out << ', ' if out.length > 1
if out.length > INSPECT_SIZE_LIMIT
out << '...'
break
end
out << "#{_indexenum.sym(idx)}=" if _indexenum
out << e.inspect
}
out << ']'
end
def empty? ; length == 0 ; end
def flatten ; map { |e| e.respond_to?(:flatten) ? e.flatten : e }.flatten ; end
def index(elem=nil, &b) ; (0...length).find { |i| b ? b[self[i]] : self[i] == elem } ; end
end
class StaticArray < MemStruct
attr_accessor :_tglen, :_length, :_indexenum, :_tg
def initialize(tglen, length, indexenum, tg)
@_tglen = tglen
@_length = length
@_indexenum = indexenum
@_tg = tg
end
def _set(a)
a.each_with_index { |v, i| self[i] = v }
end
def _cpp_init
_length.times { |i| _tgat(i)._cpp_init }
end
alias length _length
alias size _length
def _tgat(i)
@_tg._at(@_memaddr + i*@_tglen) if i >= 0 and i < @_length
end
def [](i)
i = _indexenum.int(i) if _indexenum
i += @_length if i < 0
_tgat(i)._get
end
def []=(i, v)
i = _indexenum.int(i) if _indexenum
i += @_length if i < 0
_tgat(i)._set(v)
end
include Enumerable
end
class StaticString < MemStruct
attr_accessor :_length
def initialize(length)
@_length = length
end
def _get
DFHack.memory_read(@_memaddr, @_length)
end
def _set(v)
DFHack.memory_write(@_memaddr, v[0, @_length])
end
end
class StlVector32 < MemStruct
attr_accessor :_tg
def initialize(tg)
@_tg = tg
end
def length
DFHack.memory_vector32_length(@_memaddr)
end
def size ; length ; end # alias wouldnt work for subclasses
def valueptr_at(idx)
DFHack.memory_vector32_ptrat(@_memaddr, idx)
end
def insert_at(idx, val)
DFHack.memory_vector32_insert(@_memaddr, idx, val)
end
def delete_at(idx)
DFHack.memory_vector32_delete(@_memaddr, idx)
end
def _set(v)
delete_at(length-1) while length > v.length # match lengthes
v.each_with_index { |e, i| self[i] = e } # patch entries
end
def _cpp_init
DFHack.memory_vector_init(@_memaddr)
end
def clear
delete_at(length-1) while length > 0
end
def [](idx)
idx += length if idx < 0
@_tg._at(valueptr_at(idx))._get if idx >= 0 and idx < length
end
def []=(idx, v)
idx += length if idx < 0
if idx >= length
insert_at(idx, 0)
elsif idx < 0
raise 'invalid idx'
end
@_tg._at(valueptr_at(idx))._set(v)
end
def push(v)
self[length] = v
self
end
def <<(v) ; push(v) ; end
def pop
l = length
if l > 0
v = self[l-1]
delete_at(l-1)
end
v
end
include Enumerable
# do a binary search in an ordered vector for a specific target attribute
# ex: world.history.figures.binsearch(unit.hist_figure_id)
def binsearch(target, field=:id)
o_start = 0
o_end = length - 1
while o_end >= o_start
o_half = o_start + (o_end-o_start)/2
obj = self[o_half]
oval = obj.send(field)
if oval == target
return obj
elsif oval < target
o_start = o_half+1
else
o_end = o_half-1
end
end
end
end
class StlVector16 < StlVector32
def length
DFHack.memory_vector16_length(@_memaddr)
end
def valueptr_at(idx)
DFHack.memory_vector16_ptrat(@_memaddr, idx)
end
def insert_at(idx, val)
DFHack.memory_vector16_insert(@_memaddr, idx, val)
end
def delete_at(idx)
DFHack.memory_vector16_delete(@_memaddr, idx)
end
end
class StlVector8 < StlVector32
def length
DFHack.memory_vector8_length(@_memaddr)
end
def valueptr_at(idx)
DFHack.memory_vector8_ptrat(@_memaddr, idx)
end
def insert_at(idx, val)
DFHack.memory_vector8_insert(@_memaddr, idx, val)
end
def delete_at(idx)
DFHack.memory_vector8_delete(@_memaddr, idx)
end
end
class StlBitVector < StlVector32
def initialize ; end
def length
DFHack.memory_vectorbool_length(@_memaddr)
end
def insert_at(idx, val)
DFHack.memory_vectorbool_insert(@_memaddr, idx, val)
end
def delete_at(idx)
DFHack.memory_vectorbool_delete(@_memaddr, idx)
end
def [](idx)
idx += length if idx < 0
DFHack.memory_vectorbool_at(@_memaddr, idx) if idx >= 0 and idx < length
end
def []=(idx, v)
idx += length if idx < 0
if idx >= length
insert_at(idx, v)
elsif idx < 0
raise 'invalid idx'
else
DFHack.memory_vectorbool_setat(@_memaddr, idx, v)
end
end
end
class StlString < MemStruct
def _get
DFHack.memory_read_stlstring(@_memaddr)
end
def _set(v)
DFHack.memory_write_stlstring(@_memaddr, v)
end
def _cpp_init
DFHack.memory_stlstring_init(@_memaddr)
end
end
class StlDeque < MemStruct
attr_accessor :_tglen, :_tg
def initialize(tglen, tg)
@_tglen = tglen
@_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.
def inspect ; "#<StlDeque>" ; end
end
class DfFlagarray < MemStruct
attr_accessor :_indexenum
def initialize(indexenum)
@_indexenum = indexenum
end
def length
DFHack.memory_bitarray_length(@_memaddr)
end
# TODO _cpp_init
def size ; length ; end
def resize(len)
DFHack.memory_bitarray_resize(@_memaddr, len)
end
def [](idx)
idx = _indexenum.int(idx) if _indexenum
idx += length if idx < 0
DFHack.memory_bitarray_isset(@_memaddr, idx) if idx >= 0 and idx < length
end
def []=(idx, v)
idx = _indexenum.int(idx) if _indexenum
idx += length if idx < 0
if idx >= length or idx < 0
raise 'invalid idx'
else
DFHack.memory_bitarray_set(@_memaddr, idx, v)
end
end
include Enumerable
end
class DfArray < Compound
attr_accessor :_tglen, :_tg
def initialize(tglen, tg)
@_tglen = tglen
@_tg = tg
end
field(:_ptr, 0) { number 32, false }
field(:_length, 4) { number 16, false }
def length ; _length ; end
def size ; _length ; end
# TODO _cpp_init
def _tgat(i)
@_tg._at(_ptr + i*@_tglen) if i >= 0 and i < _length
end
def [](i)
i += _length if i < 0
_tgat(i)._get
end
def []=(i, v)
i += _length if i < 0
_tgat(i)._set(v)
end
def _set(a)
a.each_with_index { |v, i| self[i] = v }
end
include Enumerable
end
class DfLinkedList < Compound
attr_accessor :_tg
def initialize(tg)
@_tg = tg
end
field(:_ptr, 0) { number 32, false }
field(:_prev, 4) { number 32, false }
field(:_next, 8) { number 32, false }
def item
# With the current xml structure, currently _tg designate
# the type of the 'next' and 'prev' fields, not 'item'.
# List head has item == NULL, so we can safely return nil.
#addr = _ptr
#return if addr == 0
#@_tg._at(addr)._get
end
def item=(v)
#addr = _ptr
#raise 'null pointer' if addr == 0
#@_tg.at(addr)._set(v)
raise 'null pointer'
end
def prev
addr = _prev
return if addr == 0
@_tg._at(addr)._get
end
def next
addr = _next
return if addr == 0
@_tg._at(addr)._get
end
include Enumerable
def each
o = self
while o
yield o.item if o.item
o = o.next
end
end
def inspect ; "#<DfLinkedList #{item.inspect} prev=#{'0x%X' % _prev} next=#{'0x%X' % _next}>" ; end
end
class Global < MemStruct
attr_accessor :_glob
def initialize(glob)
@_glob = glob
end
def _at(addr)
g = DFHack.const_get(@_glob)
g = DFHack.rtti_getclassat(g, addr)
g.new._at(addr)
end
def inspect ; "#<#{@_glob}>" ; end
end
end
class BooleanEnum
def self.int(v) ; ((v == true) || (v == 1)) ? 1 : 0 ; end
def self.sym(v) ; (!v || (v == 0)) ? false : true ; end
end
# cpp rtti name -> rb class
@rtti_n2c = {}
@rtti_c2n = {}
# cpp rtti name -> vtable ptr
@rtti_n2v = {}
@rtti_v2n = {}
def self.rtti_n2c ; @rtti_n2c ; end
def self.rtti_c2n ; @rtti_c2n ; end
def self.rtti_n2v ; @rtti_n2v ; end
def self.rtti_v2n ; @rtti_v2n ; end
# register a ruby class with a cpp rtti class name
def self.rtti_register(cppname, cls)
@rtti_n2c[cppname] = cls
@rtti_c2n[cls] = cppname
end
# return the ruby class to use for the cpp object at address if rtti info is available
def self.rtti_getclassat(cls, addr)
if addr != 0 and @rtti_c2n[cls]
# rtti info exist for class => cpp object has a vtable
@rtti_n2c[rtti_readclassname(get_vtable_ptr(addr))] || cls
else
cls
end
end
# try to read the rtti classname from an object vtable pointer
def self.rtti_readclassname(vptr)
unless n = @rtti_v2n[vptr]
n = @rtti_v2n[vptr] = get_rtti_classname(vptr).to_sym
@rtti_n2v[n] = vptr
end
n
end
# return the vtable pointer from the cpp rtti name
def self.rtti_getvtable(cppname)
unless v = @rtti_n2v[cppname]
v = get_vtable(cppname.to_s)
@rtti_n2v[cppname] = v
@rtti_v2n[v] = cppname if v != 0
end
v if v != 0
end
def self.vmethod_call(obj, voff, a0=0, a1=0, a2=0, a3=0, a4=0)
vmethod_do_call(obj._memaddr, voff, vmethod_arg(a0), vmethod_arg(a1), vmethod_arg(a2), vmethod_arg(a3))
end
def self.vmethod_arg(arg)
case arg
when nil, false; 0
when true; 1
when Integer; arg
#when String; [arg].pack('p').unpack('L')[0] # raw pointer to buffer
when MemHack::Compound; arg._memaddr
else raise "bad vmethod arg #{arg.class}"
end
end
end

@ -6,8 +6,7 @@
#include "VersionInfo.h"
#include "DataDefs.h"
#include "df/world.h"
#include "df/unit.h"
#include "df/global_objects.h"
#include "tinythread.h"
@ -21,7 +20,6 @@ using namespace DFHack;
static int df_loadruby(void);
static void df_unloadruby(void);
static void df_rubythread(void*);
static command_result df_rubyload(color_ostream &out, std::vector <std::string> & parameters);
static command_result df_rubyeval(color_ostream &out, std::vector <std::string> & parameters);
static void ruby_bind_dfhack(void);
@ -31,45 +29,56 @@ enum RB_command {
RB_INIT,
RB_DIE,
RB_EVAL,
RB_CUSTOM,
};
tthread::mutex *m_irun;
tthread::mutex *m_mutex;
static RB_command r_type;
static volatile RB_command r_type;
static volatile command_result r_result;
static color_ostream *r_console; // color_ostream given as argument, if NULL resort to console_proxy
static const char *r_command;
static command_result r_result;
static tthread::thread *r_thread;
static int onupdate_active;
static int onupdate_minyear, onupdate_minyeartick;
static color_ostream_proxy *console_proxy;
DFHACK_PLUGIN("ruby")
DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <PluginCommand> &commands)
{
// fail silently instead of spamming the console with 'failed to initialize' if libruby is not present
// the error is still logged in stderr.log
onupdate_active = 0;
// fail silently instead of spamming the console with 'failed to initialize'
// if libruby is not present, the error is still logged in stderr.log
if (!df_loadruby())
return CR_OK;
// the ruby thread sleeps trying to lock this
// when it gets it, it runs according to r_type
// when finished, it sets r_type to IDLE and unlocks
m_irun = new tthread::mutex();
// when any thread is going to request something to the ruby thread,
// lock this before anything, and release when everything is done
m_mutex = new tthread::mutex();
r_type = RB_INIT;
// create the dedicated ruby thread
// df_rubythread starts the ruby interpreter and goes to type=IDLE when done
r_thread = new tthread::thread(df_rubythread, 0);
// wait until init phase 1 is done
while (r_type != RB_IDLE)
tthread::this_thread::yield();
// ensure the ruby thread sleeps until we have a command to handle
m_irun->lock();
// check return value from rbinit
if (r_result == CR_FAILURE)
return CR_FAILURE;
onupdate_active = 0;
commands.push_back(PluginCommand("rb_load",
"Ruby interpreter. Loads the given ruby script.",
df_rubyload));
commands.push_back(PluginCommand("rb_eval",
"Ruby interpreter. Eval() a ruby string.",
df_rubyeval));
@ -79,70 +88,93 @@ DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <Plug
DFhackCExport command_result plugin_shutdown ( color_ostream &out )
{
// if dlopen failed
if (!r_thread)
return CR_OK;
// ensure ruby thread is idle
m_mutex->lock();
r_type = RB_DIE;
r_command = 0;
r_command = NULL;
// start ruby thread
m_irun->unlock();
// wait until ruby thread ends after RB_DIE
r_thread->join();
// cleanup everything
delete r_thread;
r_thread = 0;
delete m_irun;
// we can release m_mutex, other users will check r_thread
m_mutex->unlock();
delete m_mutex;
// dlclose libruby
df_unloadruby();
return CR_OK;
}
// send a single ruby line to be evaluated by the ruby thread
static command_result plugin_eval_rb(const char *command)
DFhackCExport command_result plugin_eval_ruby( color_ostream &out, const char *command)
{
// if dlopen failed
if (!r_thread)
return CR_FAILURE;
// 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;
command_result ret;
// serialize 'accesses' to the ruby thread
// ensure ruby thread is idle
m_mutex->lock();
if (!r_thread)
// raced with plugin_shutdown ?
// raced with plugin_shutdown
return CR_OK;
r_type = RB_EVAL;
r_command = command;
r_console = &out;
// wake ruby thread up
m_irun->unlock();
// could use a condition_variable or something...
// semi-active loop until ruby thread is done
while (r_type != RB_IDLE)
tthread::this_thread::yield();
// XXX non-atomic with previous r_type change check
ret = r_result;
r_console = NULL;
// block ruby thread
m_irun->lock();
// let other plugin_eval_ruby run
m_mutex->unlock();
return ret;
}
static command_result plugin_eval_rb(std::string &command)
{
return plugin_eval_rb(command.c_str());
}
DFhackCExport command_result plugin_onupdate ( color_ostream &out )
{
if (!r_thread)
return CR_OK;
// ruby sets this flag when needed, to avoid lag running ruby code every
// frame if not necessary
if (!onupdate_active)
return CR_OK;
return plugin_eval_rb("DFHack.onupdate");
if (*df::global::cur_year < onupdate_minyear)
return CR_OK;
if (*df::global::cur_year == onupdate_minyear &&
*df::global::cur_year_tick < onupdate_minyeartick)
return CR_OK;
return plugin_eval_ruby(out, "DFHack.onupdate");
}
DFhackCExport command_result plugin_onstatechange ( color_ostream &out, state_change_event e)
@ -159,26 +191,13 @@ DFhackCExport command_result plugin_onstatechange ( color_ostream &out, state_ch
SCASE(MAP_UNLOADED);
SCASE(VIEWSCREEN_CHANGED);
SCASE(CORE_INITIALIZED);
SCASE(BEGIN_UNLOAD);
// if we go through plugin_eval at BEGIN_UNLOAD, it'll
// try to get the suspend lock and deadlock at df exit
case SC_BEGIN_UNLOAD : return CR_OK;
#undef SCASE
}
return plugin_eval_rb(cmd);
}
static command_result df_rubyload(color_ostream &out, std::vector <std::string> & parameters)
{
if (parameters.size() == 1 && (parameters[0] == "help" || parameters[0] == "?"))
{
out.print("This command loads the ruby script whose path is given as parameter, and run it.\n");
return CR_OK;
}
std::string cmd = "load '";
cmd += parameters[0]; // TODO escape singlequotes
cmd += "'";
return plugin_eval_rb(cmd);
return plugin_eval_ruby(out, cmd.c_str());
}
static command_result df_rubyeval(color_ostream &out, std::vector <std::string> & parameters)
@ -191,6 +210,7 @@ static command_result df_rubyeval(color_ostream &out, std::vector <std::string>
return CR_OK;
}
// reconstruct the text from dfhack console line
std::string full = "";
for (unsigned i=0 ; i<parameters.size() ; ++i) {
@ -199,20 +219,21 @@ static command_result df_rubyeval(color_ostream &out, std::vector <std::string>
full += " ";
}
return plugin_eval_rb(full);
return plugin_eval_ruby(out, full.c_str());
}
// ruby stuff
// ruby-dev on windows is messy
// ruby.h on linux 64 is broken
// so we dynamically load libruby instead of linking it at compile time
// lib path can be set in dfhack.ini to use the system libruby, but by
// default we'll embed our own (downloaded by cmake)
// - ruby-dev on windows is messy
// - ruby.h with gcc -m32 on linux 64 is broken
// so we dynamically load libruby with dlopen/LoadLibrary
// lib path is hardcoded here, and by default downloaded by cmake
// this code should work with ruby1.9, but ruby1.9 doesn't like running
// in a dedicated non-main thread, so use ruby1.8 binaries only for now
// these ruby definitions are invalid for windows 64bit
// these ruby definitions are invalid for windows 64bit (need long long)
typedef unsigned long VALUE;
typedef unsigned long ID;
@ -224,23 +245,17 @@ typedef unsigned long ID;
#define FIX2INT(i) (((long)i) >> 1)
#define RUBY_METHOD_FUNC(func) ((VALUE(*)(...))func)
VALUE *rb_eRuntimeError;
void (*ruby_sysinit)(int *, const char ***);
void (*ruby_init)(void);
void (*ruby_init_loadpath)(void);
void (*ruby_script)(const char*);
void (*ruby_finalize)(void);
ID (*rb_intern)(const char*);
VALUE (*rb_raise)(VALUE, const char*, ...);
VALUE (*rb_funcall)(VALUE, ID, int, ...);
VALUE (*rb_define_module)(const char*);
void (*rb_define_singleton_method)(VALUE, const char*, VALUE(*)(...), int);
void (*rb_define_const)(VALUE, const char*, VALUE);
void (*rb_load_protect)(VALUE, int, int*);
VALUE (*rb_gv_get)(const char*);
VALUE (*rb_str_new)(const char*, long);
VALUE (*rb_str_new2)(const char*);
char* (*rb_string_value_ptr)(VALUE*);
VALUE (*rb_eval_string_protect)(const char*, int*);
VALUE (*rb_ary_shift)(VALUE);
@ -257,8 +272,10 @@ DFHack::DFLibrary *libruby_handle;
static int df_loadruby(void)
{
const char *libpath =
#ifdef WIN32
#if defined(WIN32)
"./libruby.dll";
#elif defined(__APPLE__)
"/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/libruby.1.dylib";
#else
"hack/libruby.so";
#endif
@ -269,11 +286,6 @@ static int df_loadruby(void)
return 0;
}
if (!(rb_eRuntimeError = (VALUE*)LookupPlugin(libruby_handle, "rb_eRuntimeError")))
return 0;
// XXX does msvc support decltype ? might need a #define decltype typeof
// or just assign to *(void**)(&s) = ...
// ruby_sysinit is optional (ruby1.9 only)
ruby_sysinit = (decltype(ruby_sysinit))LookupPlugin(libruby_handle, "ruby_sysinit");
#define rbloadsym(s) if (!(s = (decltype(s))LookupPlugin(libruby_handle, #s))) return 0
@ -282,15 +294,11 @@ static int df_loadruby(void)
rbloadsym(ruby_script);
rbloadsym(ruby_finalize);
rbloadsym(rb_intern);
rbloadsym(rb_raise);
rbloadsym(rb_funcall);
rbloadsym(rb_define_module);
rbloadsym(rb_define_singleton_method);
rbloadsym(rb_define_const);
rbloadsym(rb_load_protect);
rbloadsym(rb_gv_get);
rbloadsym(rb_str_new);
rbloadsym(rb_str_new2);
rbloadsym(rb_string_value_ptr);
rbloadsym(rb_eval_string_protect);
rbloadsym(rb_ary_shift);
@ -312,6 +320,13 @@ static void df_unloadruby(void)
}
}
static void printerr(const char* fmt, const char *arg)
{
if (r_console)
r_console->printerr(fmt, arg);
else
Core::printerr(fmt, arg);
}
// ruby thread code
static void dump_rb_error(void)
@ -322,19 +337,17 @@ static void dump_rb_error(void)
s = rb_funcall(err, rb_intern("class"), 0);
s = rb_funcall(s, rb_intern("name"), 0);
Core::printerr("E: %s: ", rb_string_value_ptr(&s));
printerr("E: %s: ", rb_string_value_ptr(&s));
s = rb_funcall(err, rb_intern("message"), 0);
Core::printerr("%s\n", rb_string_value_ptr(&s));
printerr("%s\n", rb_string_value_ptr(&s));
err = rb_funcall(err, rb_intern("backtrace"), 0);
for (int i=0 ; i<8 ; ++i)
if ((s = rb_ary_shift(err)) != Qnil)
Core::printerr(" %s\n", rb_string_value_ptr(&s));
printerr(" %s\n", rb_string_value_ptr(&s));
}
static color_ostream_proxy *console_proxy;
// ruby thread main loop
static void df_rubythread(void *p)
{
@ -358,12 +371,25 @@ static void df_rubythread(void *p)
console_proxy = new color_ostream_proxy(Core::getInstance().getConsole());
// ensure noone bothers us while we load data defs in the background
m_mutex->lock();
// tell the main thread our initialization is finished
r_result = CR_OK;
r_type = RB_IDLE;
// load the default ruby-level definitions in the background
state=0;
rb_eval_string_protect("require './hack/ruby/ruby'", &state);
if (state)
dump_rb_error();
// ready to go
m_mutex->unlock();
running = 1;
while (running) {
// wait for new command
// sleep waiting for new command
m_irun->lock();
switch (r_type) {
@ -382,10 +408,6 @@ static void df_rubythread(void *p)
if (state)
dump_rb_error();
break;
case RB_CUSTOM:
// TODO handle ruby custom commands
break;
}
r_result = CR_OK;
@ -405,7 +427,7 @@ static VALUE rb_cDFHack;
// DFHack module ruby methods, binds specific dfhack methods
// enable/disable calls to DFHack.onupdate()
static VALUE rb_dfonupdateactive(VALUE self)
static VALUE rb_dfonupdate_active(VALUE self)
{
if (onupdate_active)
return Qtrue;
@ -413,76 +435,46 @@ static VALUE rb_dfonupdateactive(VALUE self)
return Qfalse;
}
static VALUE rb_dfonupdateactiveset(VALUE self, VALUE val)
static VALUE rb_dfonupdate_active_set(VALUE self, VALUE val)
{
onupdate_active = (BOOL_ISFALSE(val) ? 0 : 1);
return Qtrue;
}
static VALUE rb_dfresume(VALUE self)
static VALUE rb_dfonupdate_minyear(VALUE self)
{
Core::getInstance().Resume();
return Qtrue;
return rb_uint2inum(onupdate_minyear);
}
static VALUE rb_dfsuspend(VALUE self)
static VALUE rb_dfonupdate_minyear_set(VALUE self, VALUE val)
{
Core::getInstance().Suspend();
onupdate_minyear = rb_num2ulong(val);
return Qtrue;
}
// returns the delta to apply to dfhack xml addrs wrt actual memory addresses
// usage: real_addr = addr_from_xml + this_delta;
static VALUE rb_dfrebase_delta(void)
static VALUE rb_dfonupdate_minyeartick(VALUE self)
{
uint32_t expected_base_address;
uint32_t actual_base_address = 0;
#ifdef WIN32
expected_base_address = 0x00400000;
actual_base_address = (uint32_t)GetModuleHandle(0);
#else
expected_base_address = 0x08048000;
FILE *f = fopen("/proc/self/maps", "r");
char line[256];
while (fgets(line, sizeof(line), f)) {
if (strstr(line, "libs/Dwarf_Fortress")) {
actual_base_address = strtoul(line, 0, 16);
break;
}
}
#endif
return rb_int2inum(actual_base_address - expected_base_address);
return rb_uint2inum(onupdate_minyeartick);
}
static VALUE rb_dfprint_str(VALUE self, VALUE s)
static VALUE rb_dfonupdate_minyeartick_set(VALUE self, VALUE val)
{
console_proxy->print("%s", rb_string_value_ptr(&s));
return Qnil;
onupdate_minyeartick = rb_num2ulong(val);
return Qtrue;
}
static VALUE rb_dfprint_err(VALUE self, VALUE s)
static VALUE rb_dfprint_str(VALUE self, VALUE s)
{
Core::printerr("%s", rb_string_value_ptr(&s));
if (r_console)
r_console->print("%s", rb_string_value_ptr(&s));
else
console_proxy->print("%s", rb_string_value_ptr(&s));
return Qnil;
}
/* TODO needs main dfhack support
this needs a custom DFHack::Plugin subclass to pass the cmdname to invoke(), to match the ruby callback
// register a ruby method as dfhack console command
// usage: DFHack.register("moo", "this commands prints moo on the console") { DFHack.puts "moo !" }
static VALUE rb_dfregister(VALUE self, VALUE name, VALUE descr)
{
commands.push_back(PluginCommand(rb_string_value_ptr(&name),
rb_string_value_ptr(&descr),
df_rubycustom));
return Qtrue;
}
*/
static VALUE rb_dfregister(VALUE self, VALUE name, VALUE descr)
static VALUE rb_dfprint_err(VALUE self, VALUE s)
{
rb_raise(*rb_eRuntimeError, "not implemented");
printerr("%s", rb_string_value_ptr(&s));
return Qnil;
}
@ -511,7 +503,7 @@ static VALUE rb_dfget_rtti_classname(VALUE self, VALUE vptr)
char *typestring = *(char**)(typeinfo + 0x4);
while (*typestring >= '0' && *typestring <= '9')
typestring++;
return rb_str_new2(typestring);
return rb_str_new(typestring, strlen(typestring));
#endif
}
@ -532,7 +524,7 @@ static VALUE rb_dfmalloc(VALUE self, VALUE len)
{
char *ptr = (char*)malloc(FIX2INT(len));
if (!ptr)
rb_raise(*rb_eRuntimeError, "no memory");
return Qnil;
memset(ptr, 0, FIX2INT(len));
return rb_uint2inum((long)ptr);
}
@ -761,22 +753,49 @@ static VALUE rb_dfmemory_bitarray_set(VALUE self, VALUE addr, VALUE idx, VALUE v
/* call an arbitrary object virtual method */
static VALUE rb_dfvcall(VALUE self, VALUE cppobj, VALUE cppvoff, VALUE a0, VALUE a1, VALUE a2, VALUE a3)
{
#ifdef WIN32
int (__fastcall *fptr)(char **me, int dummy_edx, int, int, int, int);
__declspec(naked) static int raw_vcall(char **that, unsigned long voff, unsigned long a0,
unsigned long a1, unsigned long a2, unsigned long a3)
{
// __thiscall requires that the callee cleans up the stack
// here we dont know how many arguments it will take, so
// we simply fix esp across the funcall
__asm {
push ebp
mov ebp, esp
push a3
push a2
push a1
push a0
mov ecx, that
mov eax, [ecx]
add eax, voff
call [eax]
mov esp, ebp
pop ebp
ret
}
}
#else
static int raw_vcall(char **that, unsigned long voff, unsigned long a0,
unsigned long a1, unsigned long a2, unsigned long a3)
{
int (*fptr)(char **me, int, int, int, int);
fptr = (decltype(fptr))*(void**)(*that + voff);
return fptr(that, a0, a1, a2, a3);
}
#endif
char **that = (char**)rb_num2ulong(cppobj);
int ret;
fptr = (decltype(fptr))*(void**)(*that + rb_num2ulong(cppvoff));
ret = fptr(that,
#ifdef WIN32
0,
#endif
rb_num2ulong(a0), rb_num2ulong(a1), rb_num2ulong(a2), rb_num2ulong(a3));
return rb_int2inum(ret);
// call an arbitrary vmethod, convert args/ret to native values for raw_vcall
static VALUE rb_dfvcall(VALUE self, VALUE cppobj, VALUE cppvoff, VALUE a0, VALUE a1, VALUE a2, VALUE a3)
{
return rb_int2inum(raw_vcall((char**)rb_num2ulong(cppobj), rb_num2ulong(cppvoff),
rb_num2ulong(a0), rb_num2ulong(a1),
rb_num2ulong(a2), rb_num2ulong(a3)));
}
@ -785,22 +804,21 @@ static VALUE rb_dfvcall(VALUE self, VALUE cppobj, VALUE cppvoff, VALUE a0, VALUE
static void ruby_bind_dfhack(void) {
rb_cDFHack = rb_define_module("DFHack");
// global DFHack commands
rb_define_singleton_method(rb_cDFHack, "onupdate_active", RUBY_METHOD_FUNC(rb_dfonupdateactive), 0);
rb_define_singleton_method(rb_cDFHack, "onupdate_active=", RUBY_METHOD_FUNC(rb_dfonupdateactiveset), 1);
rb_define_singleton_method(rb_cDFHack, "resume", RUBY_METHOD_FUNC(rb_dfresume), 0);
rb_define_singleton_method(rb_cDFHack, "do_suspend", RUBY_METHOD_FUNC(rb_dfsuspend), 0);
rb_define_singleton_method(rb_cDFHack, "onupdate_active", RUBY_METHOD_FUNC(rb_dfonupdate_active), 0);
rb_define_singleton_method(rb_cDFHack, "onupdate_active=", RUBY_METHOD_FUNC(rb_dfonupdate_active_set), 1);
rb_define_singleton_method(rb_cDFHack, "onupdate_minyear", RUBY_METHOD_FUNC(rb_dfonupdate_minyear), 0);
rb_define_singleton_method(rb_cDFHack, "onupdate_minyear=", RUBY_METHOD_FUNC(rb_dfonupdate_minyear_set), 1);
rb_define_singleton_method(rb_cDFHack, "onupdate_minyeartick", RUBY_METHOD_FUNC(rb_dfonupdate_minyeartick), 0);
rb_define_singleton_method(rb_cDFHack, "onupdate_minyeartick=", RUBY_METHOD_FUNC(rb_dfonupdate_minyeartick_set), 1);
rb_define_singleton_method(rb_cDFHack, "get_global_address", RUBY_METHOD_FUNC(rb_dfget_global_address), 1);
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, "register_dfcommand", RUBY_METHOD_FUNC(rb_dfregister), 2);
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);
rb_define_singleton_method(rb_cDFHack, "free", RUBY_METHOD_FUNC(rb_dffree), 1);
rb_define_singleton_method(rb_cDFHack, "vmethod_do_call", RUBY_METHOD_FUNC(rb_dfvcall), 6);
rb_define_const(rb_cDFHack, "REBASE_DELTA", rb_dfrebase_delta());
rb_define_singleton_method(rb_cDFHack, "memory_read", RUBY_METHOD_FUNC(rb_dfmemory_read), 2);
rb_define_singleton_method(rb_cDFHack, "memory_read_int8", RUBY_METHOD_FUNC(rb_dfmemory_read_int8), 1);
@ -839,11 +857,4 @@ static void ruby_bind_dfhack(void) {
rb_define_singleton_method(rb_cDFHack, "memory_bitarray_resize", RUBY_METHOD_FUNC(rb_dfmemory_bitarray_resize), 2);
rb_define_singleton_method(rb_cDFHack, "memory_bitarray_isset", RUBY_METHOD_FUNC(rb_dfmemory_bitarray_isset), 2);
rb_define_singleton_method(rb_cDFHack, "memory_bitarray_set", RUBY_METHOD_FUNC(rb_dfmemory_bitarray_set), 3);
// load the default ruby-level definitions
int state=0;
// windows cmake installs files in df/, linux installs files in df/hack/
rb_eval_string_protect("File.exist?('hack/ruby.rb') ? load('hack/ruby.rb') : load('ruby.rb')", &state);
if (state)
dump_rb_error();
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,72 @@
# df user-interface related methods
module DFHack
class << self
# returns the current active viewscreen
def curview
ret = gview.view
ret = ret.child while ret.child
ret
end
# center the DF screen on something
# updates the cursor position if visible
def center_viewscreen(x, y=nil, z=nil)
x = x.pos if x.respond_to?(:pos)
x, y, z = x.x, x.y, x.z if x.respond_to?(:x)
# compute screen 'map' size (tiles)
menuwidth = ui_menu_width
# ui_menu_width shows only the 'tab' status
menuwidth = 1 if menuwidth == 2 and ui_area_map_width == 2 and cursor.x != -30000
menuwidth = 2 if menuwidth == 3 and cursor.x != -30000
w_w = gps.dimx - 2
w_h = gps.dimy - 2
case menuwidth
when 1; w_w -= 55
when 2; w_w -= (ui_area_map_width == 2 ? 24 : 31)
end
# center view
w_x = x - w_w/2
w_y = y - w_h/2
w_z = z
# round view coordinates (optional)
#w_x -= w_x % 10
#w_y -= w_y % 10
# crop to map limits
w_x = [[w_x, world.map.x_count - w_w].min, 0].max
w_y = [[w_y, world.map.y_count - w_h].min, 0].max
self.window_x = w_x
self.window_y = w_y
self.window_z = w_z
if cursor.x != -30000
cursor.x, cursor.y, cursor.z = x, y, z
end
end
# add an announcement
# color = integer, bright = bool
def add_announcement(str, color=nil, bright=nil)
cont = false
while str.length > 0
rep = Report.cpp_new
rep.color = color if color
rep.bright = ((bright && bright != 0) ? 1 : 0) if bright != nil
rep.year = cur_year
rep.time = cur_year_tick
rep.flags.continuation = cont
cont = true
rep.flags.announcement = true
rep.text = str[0, 73]
str = str[73..-1].to_s
rep.id = world.status.next_report_id
world.status.next_report_id += 1
world.status.reports << rep
world.status.announcements << rep
world.status.display_timer = 2000
end
end
end
end

@ -0,0 +1,93 @@
module DFHack
class << self
# return an Unit
# with no arg, return currently selected unit in df UI ('v' or 'k' menu)
# with numeric arg, search unit by unit.id
# with an argument that respond to x/y/z (eg cursor), find first unit at this position
def unit_find(what=:selected, y=nil, z=nil)
if what == :selected
if curview._rtti_classname == :viewscreen_itemst
ref = curview.entry_ref[curview.cursor_pos]
ref.unit_tg if ref.kind_of?(GeneralRefUnit)
else
case ui.main.mode
when :ViewUnits
# nobody selected => idx == 0
v = world.units.active[ui_selected_unit]
v if v and v.pos.z == cursor.z
when :LookAround
k = ui_look_list.items[ui_look_cursor]
k.unit if k.type == :Unit
end
end
elsif what.kind_of?(Integer)
# search by id
return world.units.all.binsearch(what) if not z
# search by coords
x = what
world.units.all.find { |u| u.pos.x == x and u.pos.y == y and u.pos.z == z }
elsif what.respond_to?(:x) or what.respond_to?(:pos)
world.units.all.find { |u| same_pos?(what, u) }
else
raise "what what?"
end
end
# returns an Array of all units that are current fort citizen (dwarves, on map, not hostile)
def unit_citizens
race = ui.race_id
civ = ui.civ_id
world.units.active.find_all { |u|
u.race == race and u.civ_id == civ and !u.flags1.dead and !u.flags1.merchant and
!u.flags1.diplomat and !u.flags2.resident and !u.flags3.ghostly and
!u.curse.add_tags1.OPPOSED_TO_LIFE and !u.curse.add_tags1.CRAZED and
u.mood != :Berserk
# TODO check curse ; currently this should keep vampires, but may include werebeasts
}
end
# list workers (citizen, not crazy / child / inmood / noble)
def unit_workers
unit_citizens.find_all { |u|
u.mood == :None and
u.profession != :CHILD and
u.profession != :BABY and
# TODO MENIAL_WORK_EXEMPTION_SPOUSE
!unit_entitypositions(u).find { |pos| pos.flags[:MENIAL_WORK_EXEMPTION] }
}
end
# list currently idle workers
def unit_idlers
unit_workers.find_all { |u|
# current_job includes eat/drink/sleep/pickupequip
!u.job.current_job and
# filter 'attend meeting'
u.meetings.length == 0 and
# filter soldiers (TODO check schedule)
u.military.squad_index == -1 and
# filter 'on break'
!u.status.misc_traits.find { |t| id == :OnBreak }
}
end
def unit_entitypositions(unit)
list = []
return list if not hf = world.history.figures.binsearch(unit.hist_figure_id)
hf.entity_links.each { |el|
next if el._rtti_classname != :histfig_entity_link_positionst
next if not ent = world.entities.all.binsearch(el.entity_id)
next if not pa = ent.positions.assignments.binsearch(el.assignment_id)
next if not pos = ent.positions.own.binsearch(pa.position_id)
list << pos
}
list
end
end
class LanguageName
def to_s(english=true)
df.translate_name(self, english)
end
end
end

@ -165,6 +165,8 @@ command_result df_showmood (color_ostream &out, vector <string> & parameters)
out.print("not yet claimed a workshop but will want");
out.print(" the following items:\n");
int count_got = job->items.size(), got;
for (size_t i = 0; i < job->job_items.size(); i++)
{
df::job_item *item = job->job_items[i];
@ -267,7 +269,11 @@ command_result df_showmood (color_ostream &out, vector <string> & parameters)
}
}
out.print(", quantity %i\n", item->quantity);
got = count_got;
if (got > item->quantity)
got = item->quantity;
out.print(", quantity %i (got %i)\n", item->quantity, got);
count_got -= got;
}
}
if (!found)

@ -0,0 +1,20 @@
# 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"

@ -0,0 +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.itemrefs.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.itemrefs.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

@ -0,0 +1,27 @@
# remove bad thoughts for the selected unit or the whole fort
# with removebadthoughts -v, dump the bad thoughts types we removed
verbose = $script_args.delete('-v')
if u = df.unit_find(:selected)
targets = [u]
else
targets = df.unit_citizens
end
seenbad = Hash.new(0)
targets.each { |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
}
}
if verbose
seenbad.sort_by { |k, v| v }.each { |k, v| puts " #{v} #{k}" }
end
count = seenbad.values.inject(0) { |s, v| s+v }
puts "removed #{count} bad thought#{'s' if count != 1}"

@ -0,0 +1,33 @@
# slay all creatures of a given race
race = $script_args[0]
checkunit = lambda { |u|
u.body.blood_count != 0 and
not u.flags1.dead and
not u.flags1.caged and
not df.map_designation_at(u).hidden
}
all_races = df.world.units.active.map { |u|
u.race_tg.creature_id if checkunit[u]
}.compact.uniq.sort
if !race
puts all_races
else
raw_race = df.match_rawname(race, all_races)
raise 'invalid race' if not raw_race
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]
u.body.blood_count = 0
count += 1
end
}
puts "slain #{count} #{raw_race}"
end