Petr Mrázek 2012-07-09 01:13:07 +02:00
commit a5977db443
35 changed files with 1131 additions and 214 deletions

5
.gitignore vendored

@ -35,6 +35,7 @@ build/library
build/tools
build/plugins
build/depends
build/install_manifest.txt
#ignore Kdevelop stuff
.kdev4
@ -49,6 +50,10 @@ dfhack/python/PyDFHack.egg-info
dfhack/python/build
dfhack/python/dist
# Ruby binding binaries
plugins/ruby/libruby*
plugins/ruby/ruby-autogen.rb
# CPack stuff
build/CPack*Config.cmake

@ -17,7 +17,7 @@ are treated by DFHack command line prompt almost as
native C++ commands, and invoked by plugins written in c++.
This document describes native API available to Lua in detail.
For the most part it does not describe utility functions
It does not describe all of the utility functions
implemented by Lua files located in hack/lua/...
@ -1323,9 +1323,9 @@ Features:
order using ``dfhack.safecall``.
=======
Modules
=======
===========
Lua Modules
===========
DFHack sets up the lua interpreter so that the built-in ``require``
function can be used to load shared lua code from hack/lua/.
@ -1333,7 +1333,7 @@ The ``dfhack`` namespace reference itself may be obtained via
``require('dfhack')``, although it is initially created as a
global by C++ bootstrap code.
The following functions are provided:
The following module management functions are provided:
* ``mkmodule(name)``
@ -1357,6 +1357,194 @@ The following functions are provided:
should be kept limited to the standard Lua library and API described
in this document.
Global environment
==================
A number of variables and functions are provided in the base global
environment by the mandatory init file dfhack.lua:
* Color constants
These are applicable both for ``dfhack.color()`` and color fields
in DF functions or structures:
COLOR_RESET, COLOR_BLACK, COLOR_BLUE, COLOR_GREEN, COLOR_CYAN,
COLOR_RED, COLOR_MAGENTA, COLOR_BROWN, COLOR_GREY, COLOR_DARKGREY,
COLOR_LIGHTBLUE, COLOR_LIGHTGREEN, COLOR_LIGHTCYAN, COLOR_LIGHTRED,
COLOR_LIGHTMAGENTA, COLOR_YELLOW, COLOR_WHITE
* ``dfhack.onStateChange`` event codes
Available only in the core context, as is the event itself:
SC_WORLD_LOADED, SC_WORLD_UNLOADED, SC_MAP_LOADED,
SC_MAP_UNLOADED, SC_VIEWSCREEN_CHANGED, SC_CORE_INITIALIZED
* Functions already described above
safecall, qerror, mkmodule, reload
* ``printall(obj)``
If the argument is a lua table or DF object reference, prints all fields.
* ``copyall(obj)``
Returns a shallow copy of the table or reference as a lua table.
* ``pos2xyz(obj)``
The object must have fields x, y and z. Returns them as 3 values.
If obj is *nil*, or x is -30000 (the usual marker for undefined
coordinates), returns *nil*.
* ``xyz2pos(x,y,z)``
Returns a table with x, y and z as fields.
* ``safe_index(obj,index...)``
Walks a sequence of dereferences, which may be represented by numbers or strings.
Returns *nil* if any of obj or indices is *nil*, or a numeric index is out of array bounds.
utils
=====
* ``utils.compare(a,b)``
Comparator function; returns *-1* if a<b, *1* if a>b, *0* otherwise.
* ``utils.compare_name(a,b)``
Comparator for names; compares empty string last.
* ``utils.is_container(obj)``
Checks if obj is a container ref.
* ``utils.make_index_sequence(start,end)``
Returns a lua sequence of numbers in start..end.
* ``utils.make_sort_order(data, ordering)``
Computes a sorted permutation of objects in data, as a table of integer
indices into the data sequence. Uses ``data.n`` as input length
if present.
The ordering argument is a sequence of ordering specs, represented
as lua tables with following possible fields:
ord.key = *function(value)*
Computes comparison key from input data value. Not called on nil.
If omitted, the comparison key is the value itself.
ord.key_table = *function(data)*
Computes a key table from the data table in one go.
ord.compare = *function(a,b)*
Comparison function. Defaults to ``utils.compare`` above.
Called on non-nil keys; nil sorts last.
ord.nil_first = *true/false*
If true, nil keys are sorted first instead of last.
ord.reverse = *true/false*
If true, sort non-nil keys in descending order.
For every comparison during sorting the specs are applied in
order until an unambiguous decision is reached. Sorting is stable.
Example of sorting a sequence by field foo::
local spec = { key = function(v) return v.foo end }
local order = utils.make_sort_order(data, { spec })
local output = {}
for i = 1,#order do output[i] = data[order[i]] end
Separating the actual reordering of the sequence in this
way enables applying the same permutation to multiple arrays.
This function is used by the sort plugin.
* ``utils.assign(tgt, src)``
Does a recursive assignment of src into tgt.
Uses ``df.assign`` if tgt is a native object ref; otherwise
recurses into lua tables.
* ``utils.clone(obj, deep)``
Performs a shallow, or semi-deep copy of the object as a lua table tree.
The deep mode recurses into lua tables and subobjects, except pointers
to other heap objects.
Null pointers are represented as df.NULL. Zero-based native containers
are converted to 1-based lua sequences.
* ``utils.clone_with_default(obj, default, force)``
Copies the object, using the ``default`` lua table tree
as a guide to which values should be skipped as uninteresting.
The ``force`` argument makes it always return a non-*nil* value.
* ``utils.sort_vector(vector,field,cmpfun)``
Sorts a native vector or lua sequence using the comparator function.
If ``field`` is not *nil*, applies the comparator to the field instead
of the whole object.
* ``utils.binsearch(vector,key,field,cmpfun,min,max)``
Does a binary search in a native vector or lua sequence for
``key``, using ``cmpfun`` and ``field`` like sort_vector.
If ``min`` and ``max`` are specified, they are used as the
search subrange bounds.
If found, returns *item, true, idx*. Otherwise returns
*nil, false, insert_idx*, where *insert_idx* is the correct
insertion point.
* ``utils.insert_sorted(vector,item,field,cmpfun)``
Does a binary search, and inserts item if not found.
Returns *did_insert, vector[idx], idx*.
* ``utils.insert_or_update(vector,item,field,cmpfun)``
Like ``insert_sorted``, but also assigns the item into
the vector cell if insertion didn't happen.
As an example, you can use this to set skill values::
utils.insert_or_update(soul.skills, {new=true, id=..., rating=...}, 'id')
(For an explanation of ``new=true``, see table assignment in the wrapper section)
* ``utils.prompt_yes_no(prompt, default)``
Presents a yes/no prompt to the user. If ``default`` is not *nil*,
allows just pressing Enter to submit the default choice.
If the user enters ``'abort'``, throws an error.
* ``utils.prompt_input(prompt, checkfun, quit_str)``
Presents a prompt to input data, until a valid string is entered.
Once ``checkfun(input)`` returns *true, ...*, passes the values
through. If the user enters the quit_str (defaults to ``'~~~'``),
throws an error.
* ``utils.check_number(text)``
A ``prompt_input`` ``checkfun`` that verifies a number input.
dumper
======
A third-party lua table dumper module from
http://lua-users.org/wiki/DataDumper. Defines one
function:
* ``dumper.DataDumper(value, varname, fastmode, ident, indent_step)``
Returns ``value`` converted to a string. The ``indent_step``
argument specifies the indentation step size in spaces. For
the other arguments see the original documentation link above.
=======
Plugins
@ -1430,6 +1618,9 @@ are automatically used by the DFHack core as commands. The
matching command name consists of the name of the file sans
the extension.
If the first line of the script is a one-line comment, it is
used by the built-in ``ls`` and ``help`` commands.
**NOTE:** Scripts placed in subdirectories still can be accessed, but
do not clutter the ``ls`` command list; thus it is preferred
for obscure developer-oriented scripts and scripts used by tools.

@ -360,13 +360,18 @@ ul.auto-toc {
</li>
</ul>
</li>
<li><a class="reference internal" href="#modules" id="id29">Modules</a></li>
<li><a class="reference internal" href="#plugins" id="id30">Plugins</a><ul>
<li><a class="reference internal" href="#burrows" id="id31">burrows</a></li>
<li><a class="reference internal" href="#sort" id="id32">sort</a></li>
<li><a class="reference internal" href="#lua-modules" id="id29">Lua Modules</a><ul>
<li><a class="reference internal" href="#global-environment" id="id30">Global environment</a></li>
<li><a class="reference internal" href="#utils" id="id31">utils</a></li>
<li><a class="reference internal" href="#dumper" id="id32">dumper</a></li>
</ul>
</li>
<li><a class="reference internal" href="#scripts" id="id33">Scripts</a></li>
<li><a class="reference internal" href="#plugins" id="id33">Plugins</a><ul>
<li><a class="reference internal" href="#burrows" id="id34">burrows</a></li>
<li><a class="reference internal" href="#sort" id="id35">sort</a></li>
</ul>
</li>
<li><a class="reference internal" href="#scripts" id="id36">Scripts</a></li>
</ul>
</div>
<p>The current version of DFHack has extensive support for
@ -381,7 +386,7 @@ structures, and interaction with dfhack itself.</li>
are treated by DFHack command line prompt almost as
native C++ commands, and invoked by plugins written in c++.</p>
<p>This document describes native API available to Lua in detail.
For the most part it does not describe utility functions
It does not describe all of the utility functions
implemented by Lua files located in hack/lua/...</p>
<div class="section" id="df-data-structure-wrapper">
<h1><a class="toc-backref" href="#id1">DF data structure wrapper</a></h1>
@ -1480,14 +1485,14 @@ order using <tt class="docutils literal">dfhack.safecall</tt>.</p>
</div>
</div>
</div>
<div class="section" id="modules">
<h1><a class="toc-backref" href="#id29">Modules</a></h1>
<div class="section" id="lua-modules">
<h1><a class="toc-backref" href="#id29">Lua Modules</a></h1>
<p>DFHack sets up the lua interpreter so that the built-in <tt class="docutils literal">require</tt>
function can be used to load shared lua code from hack/lua/.
The <tt class="docutils literal">dfhack</tt> namespace reference itself may be obtained via
<tt class="docutils literal"><span class="pre">require('dfhack')</span></tt>, although it is initially created as a
global by C++ bootstrap code.</p>
<p>The following functions are provided:</p>
<p>The following module management functions are provided:</p>
<ul>
<li><p class="first"><tt class="docutils literal">mkmodule(name)</tt></p>
<p>Creates an environment table for the module. Intended to be used as:</p>
@ -1509,16 +1514,183 @@ should be kept limited to the standard Lua library and API described
in this document.</p>
</li>
</ul>
<div class="section" id="global-environment">
<h2><a class="toc-backref" href="#id30">Global environment</a></h2>
<p>A number of variables and functions are provided in the base global
environment by the mandatory init file dfhack.lua:</p>
<ul>
<li><p class="first">Color constants</p>
<p>These are applicable both for <tt class="docutils literal">dfhack.color()</tt> and color fields
in DF functions or structures:</p>
<p>COLOR_RESET, COLOR_BLACK, COLOR_BLUE, COLOR_GREEN, COLOR_CYAN,
COLOR_RED, COLOR_MAGENTA, COLOR_BROWN, COLOR_GREY, COLOR_DARKGREY,
COLOR_LIGHTBLUE, COLOR_LIGHTGREEN, COLOR_LIGHTCYAN, COLOR_LIGHTRED,
COLOR_LIGHTMAGENTA, COLOR_YELLOW, COLOR_WHITE</p>
</li>
<li><p class="first"><tt class="docutils literal">dfhack.onStateChange</tt> event codes</p>
<p>Available only in the core context, as is the event itself:</p>
<p>SC_WORLD_LOADED, SC_WORLD_UNLOADED, SC_MAP_LOADED,
SC_MAP_UNLOADED, SC_VIEWSCREEN_CHANGED, SC_CORE_INITIALIZED</p>
</li>
<li><p class="first">Functions already described above</p>
<p>safecall, qerror, mkmodule, reload</p>
</li>
<li><p class="first"><tt class="docutils literal">printall(obj)</tt></p>
<p>If the argument is a lua table or DF object reference, prints all fields.</p>
</li>
<li><p class="first"><tt class="docutils literal">copyall(obj)</tt></p>
<p>Returns a shallow copy of the table or reference as a lua table.</p>
</li>
<li><p class="first"><tt class="docutils literal">pos2xyz(obj)</tt></p>
<p>The object must have fields x, y and z. Returns them as 3 values.
If obj is <em>nil</em>, or x is -30000 (the usual marker for undefined
coordinates), returns <em>nil</em>.</p>
</li>
<li><p class="first"><tt class="docutils literal">xyz2pos(x,y,z)</tt></p>
<p>Returns a table with x, y and z as fields.</p>
</li>
<li><p class="first"><tt class="docutils literal"><span class="pre">safe_index(obj,index...)</span></tt></p>
<p>Walks a sequence of dereferences, which may be represented by numbers or strings.
Returns <em>nil</em> if any of obj or indices is <em>nil</em>, or a numeric index is out of array bounds.</p>
</li>
</ul>
</div>
<div class="section" id="utils">
<h2><a class="toc-backref" href="#id31">utils</a></h2>
<ul>
<li><p class="first"><tt class="docutils literal">utils.compare(a,b)</tt></p>
<p>Comparator function; returns <em>-1</em> if a&lt;b, <em>1</em> if a&gt;b, <em>0</em> otherwise.</p>
</li>
<li><p class="first"><tt class="docutils literal">utils.compare_name(a,b)</tt></p>
<p>Comparator for names; compares empty string last.</p>
</li>
<li><p class="first"><tt class="docutils literal">utils.is_container(obj)</tt></p>
<p>Checks if obj is a container ref.</p>
</li>
<li><p class="first"><tt class="docutils literal">utils.make_index_sequence(start,end)</tt></p>
<p>Returns a lua sequence of numbers in start..end.</p>
</li>
<li><p class="first"><tt class="docutils literal">utils.make_sort_order(data, ordering)</tt></p>
<p>Computes a sorted permutation of objects in data, as a table of integer
indices into the data sequence. Uses <tt class="docutils literal">data.n</tt> as input length
if present.</p>
<p>The ordering argument is a sequence of ordering specs, represented
as lua tables with following possible fields:</p>
<dl class="docutils">
<dt>ord.key = <em>function(value)</em></dt>
<dd><p class="first last">Computes comparison key from input data value. Not called on nil.
If omitted, the comparison key is the value itself.</p>
</dd>
<dt>ord.key_table = <em>function(data)</em></dt>
<dd><p class="first last">Computes a key table from the data table in one go.</p>
</dd>
<dt>ord.compare = <em>function(a,b)</em></dt>
<dd><p class="first last">Comparison function. Defaults to <tt class="docutils literal">utils.compare</tt> above.
Called on non-nil keys; nil sorts last.</p>
</dd>
<dt>ord.nil_first = <em>true/false</em></dt>
<dd><p class="first last">If true, nil keys are sorted first instead of last.</p>
</dd>
<dt>ord.reverse = <em>true/false</em></dt>
<dd><p class="first last">If true, sort non-nil keys in descending order.</p>
</dd>
</dl>
<p>For every comparison during sorting the specs are applied in
order until an unambiguous decision is reached. Sorting is stable.</p>
<p>Example of sorting a sequence by field foo:</p>
<pre class="literal-block">
local spec = { key = function(v) return v.foo end }
local order = utils.make_sort_order(data, { spec })
local output = {}
for i = 1,#order do output[i] = data[order[i]] end
</pre>
<p>Separating the actual reordering of the sequence in this
way enables applying the same permutation to multiple arrays.
This function is used by the sort plugin.</p>
</li>
<li><p class="first"><tt class="docutils literal">utils.assign(tgt, src)</tt></p>
<p>Does a recursive assignment of src into tgt.
Uses <tt class="docutils literal">df.assign</tt> if tgt is a native object ref; otherwise
recurses into lua tables.</p>
</li>
<li><p class="first"><tt class="docutils literal">utils.clone(obj, deep)</tt></p>
<p>Performs a shallow, or semi-deep copy of the object as a lua table tree.
The deep mode recurses into lua tables and subobjects, except pointers
to other heap objects.
Null pointers are represented as df.NULL. Zero-based native containers
are converted to 1-based lua sequences.</p>
</li>
<li><p class="first"><tt class="docutils literal">utils.clone_with_default(obj, default, force)</tt></p>
<p>Copies the object, using the <tt class="docutils literal">default</tt> lua table tree
as a guide to which values should be skipped as uninteresting.
The <tt class="docutils literal">force</tt> argument makes it always return a non-<em>nil</em> value.</p>
</li>
<li><p class="first"><tt class="docutils literal">utils.sort_vector(vector,field,cmpfun)</tt></p>
<p>Sorts a native vector or lua sequence using the comparator function.
If <tt class="docutils literal">field</tt> is not <em>nil</em>, applies the comparator to the field instead
of the whole object.</p>
</li>
<li><p class="first"><tt class="docutils literal">utils.binsearch(vector,key,field,cmpfun,min,max)</tt></p>
<p>Does a binary search in a native vector or lua sequence for
<tt class="docutils literal">key</tt>, using <tt class="docutils literal">cmpfun</tt> and <tt class="docutils literal">field</tt> like sort_vector.
If <tt class="docutils literal">min</tt> and <tt class="docutils literal">max</tt> are specified, they are used as the
search subrange bounds.</p>
<p>If found, returns <em>item, true, idx</em>. Otherwise returns
<em>nil, false, insert_idx</em>, where <em>insert_idx</em> is the correct
insertion point.</p>
</li>
<li><p class="first"><tt class="docutils literal">utils.insert_sorted(vector,item,field,cmpfun)</tt></p>
<p>Does a binary search, and inserts item if not found.
Returns <em>did_insert, vector[idx], idx</em>.</p>
</li>
<li><p class="first"><tt class="docutils literal">utils.insert_or_update(vector,item,field,cmpfun)</tt></p>
<p>Like <tt class="docutils literal">insert_sorted</tt>, but also assigns the item into
the vector cell if insertion didn't happen.</p>
<p>As an example, you can use this to set skill values:</p>
<pre class="literal-block">
utils.insert_or_update(soul.skills, {new=true, id=..., rating=...}, 'id')
</pre>
<p>(For an explanation of <tt class="docutils literal">new=true</tt>, see table assignment in the wrapper section)</p>
</li>
<li><p class="first"><tt class="docutils literal">utils.prompt_yes_no(prompt, default)</tt></p>
<p>Presents a yes/no prompt to the user. If <tt class="docutils literal">default</tt> is not <em>nil</em>,
allows just pressing Enter to submit the default choice.
If the user enters <tt class="docutils literal">'abort'</tt>, throws an error.</p>
</li>
<li><p class="first"><tt class="docutils literal">utils.prompt_input(prompt, checkfun, quit_str)</tt></p>
<p>Presents a prompt to input data, until a valid string is entered.
Once <tt class="docutils literal">checkfun(input)</tt> returns <em>true, ...</em>, passes the values
through. If the user enters the quit_str (defaults to <tt class="docutils literal"><span class="pre">'~~~'</span></tt>),
throws an error.</p>
</li>
<li><p class="first"><tt class="docutils literal">utils.check_number(text)</tt></p>
<p>A <tt class="docutils literal">prompt_input</tt> <tt class="docutils literal">checkfun</tt> that verifies a number input.</p>
</li>
</ul>
</div>
<div class="section" id="dumper">
<h2><a class="toc-backref" href="#id32">dumper</a></h2>
<p>A third-party lua table dumper module from
<a class="reference external" href="http://lua-users.org/wiki/DataDumper">http://lua-users.org/wiki/DataDumper</a>. Defines one
function:</p>
<ul>
<li><p class="first"><tt class="docutils literal">dumper.DataDumper(value, varname, fastmode, ident, indent_step)</tt></p>
<p>Returns <tt class="docutils literal">value</tt> converted to a string. The <tt class="docutils literal">indent_step</tt>
argument specifies the indentation step size in spaces. For
the other arguments see the original documentation link above.</p>
</li>
</ul>
</div>
</div>
<div class="section" id="plugins">
<h1><a class="toc-backref" href="#id30">Plugins</a></h1>
<h1><a class="toc-backref" href="#id33">Plugins</a></h1>
<p>DFHack plugins may export native functions and events
to lua contexts. They are automatically imported by
<tt class="docutils literal"><span class="pre">mkmodule('plugins.&lt;name&gt;')</span></tt>; this means that a lua
module file is still necessary for <tt class="docutils literal">require</tt> to read.</p>
<p>The following plugins have lua support.</p>
<div class="section" id="burrows">
<h2><a class="toc-backref" href="#id31">burrows</a></h2>
<h2><a class="toc-backref" href="#id34">burrows</a></h2>
<p>Implements extended burrow manipulations.</p>
<p>Events:</p>
<ul>
@ -1556,17 +1728,19 @@ set is the same as used by the command line.</p>
<p>The lua module file also re-exports functions from <tt class="docutils literal">dfhack.burrows</tt>.</p>
</div>
<div class="section" id="sort">
<h2><a class="toc-backref" href="#id32">sort</a></h2>
<h2><a class="toc-backref" href="#id35">sort</a></h2>
<p>Does not export any native functions as of now. Instead, it
calls lua code to perform the actual ordering of list items.</p>
</div>
</div>
<div class="section" id="scripts">
<h1><a class="toc-backref" href="#id33">Scripts</a></h1>
<h1><a class="toc-backref" href="#id36">Scripts</a></h1>
<p>Any files with the .lua extension placed into hack/scripts/*
are automatically used by the DFHack core as commands. The
matching command name consists of the name of the file sans
the extension.</p>
<p>If the first line of the script is a one-line comment, it is
used by the built-in <tt class="docutils literal">ls</tt> and <tt class="docutils literal">help</tt> commands.</p>
<p><strong>NOTE:</strong> Scripts placed in subdirectories still can be accessed, but
do not clutter the <tt class="docutils literal">ls</tt> command list; thus it is preferred
for obscure developer-oriented scripts and scripts used by tools.

@ -281,7 +281,7 @@ static command_result runLuaScript(color_ostream &out, std::string name, vector<
return ok ? CR_OK : CR_FAILURE;
}
static command_result runRubyScript(PluginManager *plug_mgr, std::string name, vector<string> &args)
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++)
@ -290,7 +290,7 @@ static command_result runRubyScript(PluginManager *plug_mgr, std::string name, v
rbcmd += "load './hack/scripts/" + name + ".rb'";
return plug_mgr->eval_ruby(rbcmd.c_str());
return plug_mgr->eval_ruby(out, rbcmd.c_str());
}
command_result Core::runCommand(color_ostream &out, const std::string &command)
@ -632,7 +632,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first, ve
if (fileExists(filename + ".lua"))
res = runLuaScript(con, first, parts);
else if (plug_mgr->eval_ruby && fileExists(filename + ".rb"))
res = runRubyScript(plug_mgr, first, parts);
res = runRubyScript(con, plug_mgr, first, parts);
else
con.printerr("%s is not a recognized command.\n", first.c_str());
}
@ -752,6 +752,7 @@ Core::Core()
misc_data_mutex=0;
last_world_data_ptr = NULL;
last_local_map_ptr = NULL;
last_pause_state = false;
top_viewscreen = NULL;
screen_window = NULL;
server = NULL;
@ -1116,6 +1117,15 @@ int Core::Update()
}
}
if (df::global::pause_state)
{
if (*df::global::pause_state != last_pause_state)
{
onStateChange(out, last_pause_state ? SC_UNPAUSED : SC_PAUSED);
last_pause_state = *df::global::pause_state;
}
}
// Execute per-frame handlers
onUpdate(out);

@ -188,7 +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 (*)(const char*)) LookupPlugin(plug, "plugin_eval_ruby");
plugin_eval_ruby = (command_result (*)(color_ostream &, const char*)) LookupPlugin(plug, "plugin_eval_ruby");
index_lua(plug);
this->name = *plug_name;
plugin_lib = plug;

@ -75,7 +75,9 @@ namespace DFHack
SC_MAP_UNLOADED = 3,
SC_VIEWSCREEN_CHANGED = 4,
SC_CORE_INITIALIZED = 5,
SC_BEGIN_UNLOAD = 6
SC_BEGIN_UNLOAD = 6,
SC_PAUSED = 7,
SC_UNPAUSED = 8
};
// Core is a singleton. Why? Because it is closely tied to SDL calls. It tracks the global state of DF.
@ -228,6 +230,7 @@ namespace DFHack
// for state change tracking
void *last_local_map_ptr;
df::viewscreen *top_viewscreen;
bool last_pause_state;
// Very important!
bool started;

@ -209,7 +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)(const char*);
command_result (*plugin_eval_ruby)(color_ostream &, const char*);
};
class DFHACK_EXPORT PluginManager
{
@ -238,7 +238,7 @@ namespace DFHack
{
return all_plugins.size();
}
command_result (*eval_ruby)(const char*);
command_result (*eval_ruby)(color_ostream &, const char*);
// DATA
private:
tthread::mutex * cmdlist_mutex;

@ -39,6 +39,8 @@ if dfhack.is_core_context then
SC_MAP_UNLOADED = 3
SC_VIEWSCREEN_CHANGED = 4
SC_CORE_INITIALIZED = 5
SC_PAUSED = 7
SC_UNPAUSED = 8
end
-- Error handling

@ -57,10 +57,10 @@ function is_container(obj)
end
-- Make a sequence of numbers in 1..size
function make_index_sequence(size)
function make_index_sequence(istart,iend)
local index = {}
for i=1,size do
index[i] = i
for i=istart,iend do
index[i-istart+1] = i
end
return index
end
@ -114,7 +114,7 @@ function make_sort_order(data,ordering)
end
-- Make an order table
local index = make_index_sequence(size)
local index = make_index_sequence(1,size)
-- Sort the ordering table
table.sort(index, function(ia,ib)
@ -379,7 +379,7 @@ function prompt_yes_no(msg,default)
elseif string.match(rv,'^[Nn]') then
return false
elseif rv == 'abort' then
qerror('User abort in utils.prompt_yes_no()')
qerror('User abort')
elseif rv == '' and default ~= nil then
return default
end
@ -393,7 +393,7 @@ function prompt_input(prompt,check,quit_str)
while true do
local rv = dfhack.lineedit(prompt)
if rv == quit_str then
return nil
qerror('User abort')
end
local rtbl = table.pack(check(rv))
if rtbl[1] then

@ -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;

@ -36,7 +36,7 @@ function GetTextRegion()
--if num>=100 then
--print(string.format("%d %x->%x %s %s",k,v["start"],v["end"],v.name or "",flgs))
--end
local pos=string.find(v.name,".text") or string.find(v.name,"libs/Dwarf_Fortress")
local pos=string.find(v.name,"Dwarf Fortress.exe") or string.find(v.name,"libs/Dwarf_Fortress")
if(pos~=nil) and v["execute"] then
__TEXT=v;
return v;
@ -99,6 +99,7 @@ function SetExecute(pos)
UpdateRanges()
local reg=GetRegionIn(pos)
reg.execute=true
reg["write"]=true
Process.setPermisions(reg,reg) -- TODO maybe make a page with only execute permisions or sth
end
-- engine bindings
@ -224,6 +225,11 @@ function engine.LoadModData(file)
end
return T2
end
function engine.FindMarkerCall(moddata,name)
if moddata.symbols[name] ~=nil then
return moddata.symbols[name]+1
end
end
function engine.FindMarker(moddata,name)
if moddata.symbols[name] ~=nil then
return engine.findmarker(0xDEADBEEF,moddata.data,moddata.size,moddata.symbols[name])

@ -81,7 +81,7 @@ table.insert(plugins,{"migrants","multi race imigrations"})
--table.insert(plugins,{"onfunction","run lua on some df function"})
--table.insert(plugins,{"editor","edit internals of df",EditDF})
table.insert(plugins,{"saves","run current worlds's init.lua",RunSaved})
table.insert(plugins,{"adv_tools","some tools for (mainly) advneturer hacking"})
table.insert(plugins,{"adv_tools","some tools for (mainly) adventurer hacking"})
loadall(plugins)
dofile_silent("dfusion/initcustom.lua")

@ -1 +0,0 @@
as -anl --32 -o functions.o functions.asm

@ -1,23 +0,0 @@
.intel_syntax
push eax
push ebp
push esp
push esi
push edi
push edx
push ecx
push ebx
push eax
mov eax,[esp+36]
push eax
function:
call 0xdeadbee0
function2:
mov [0xdeadbeef],eax
pop eax
function3:
jmp [0xdeadbeef]

@ -1,68 +0,0 @@
onfunction=onfunction or {}
function onfunction.install()
ModData=engine.installMod("dfusion/onfunction/functions.o","functions",4)
modpos=ModData.pos
modsize=ModData.size
onfunction.pos=modpos
trgpos=engine.getpushvalue()
print(string.format("Function installed in:%x function to call is: %x",modpos,trgpos))
local firstpos=modpos+engine.FindMarker(ModData,"function")
engine.poked(firstpos,trgpos-firstpos-4) --call Lua-Onfunction
onfunction.fpos=modpos+engine.FindMarker(ModData,"function3")
engine.poked(modpos+engine.FindMarker(ModData,"function2"),modpos+modsize)
engine.poked(onfunction.fpos,modpos+modsize)
SetExecute(modpos)
onfunction.calls={}
onfunction.functions={}
onfunction.names={}
onfunction.hints={}
end
function OnFunction(values)
--[=[print("Onfunction called!")
print("Data:")
for k,v in pairs(values) do
print(string.format("%s=%x",k,v))
end
print("stack:")
for i=0,3 do
print(string.format("%d %x",i,engine.peekd(values.esp+i*4)))
end
--]=]
if onfunction.functions[values.ret] ~=nil then
onfunction.functions[values.ret](values)
end
return onfunction.calls[values.ret] --returns real function to call
end
function onfunction.patch(addr)
if(engine.peekb(addr)~=0xe8) then
error("Incorrect address, not a function call")
else
onfunction.calls[addr+5]=addr+engine.peekd(addr+1)+5 --adds real function to call
engine.poked(addr+1,engine.getmod("functions")-addr-5)
end
end
function onfunction.AddFunction(addr,name,hints)
onfunction.patch(addr)
onfunction.names[name]=addr+5
if hints~=nil then
onfunction.hints[name]=hints
end
end
function onfunction.ReadHint(values,name,hintname)
local hints=onfunction.hints[name]
if hints ==nil then return nil end
local hint=hints[hintname]
if type(hint)=="string" then return values[hint] end
local off=hint.off or 0
return engine.peek(off+values[hint.reg],hints[hintname].rtype)
end
function onfunction.SetCallback(name,func)
if onfunction.names[name]==nil then
error("No such function:"..name)
else
onfunction.functions[onfunction.names[name]]=func
end
end

@ -1,16 +0,0 @@
if WINDOWS then --windows function defintions
--[=[onfunction.AddFunction(0x55499D+offsets.base(),"Move") --on creature move found with "watch mem=xcoord"
onfunction.AddFunction(0x275933+offsets.base(),"Die",{creature="edi"}) --on creature death? found by watching dead flag then stepping until new function
onfunction.AddFunction(0x2c1834+offsets.base(),"CreateCreature",{protocreature="eax"}) --arena
onfunction.AddFunction(0x349640+offsets.base(),"AddItem",{item="esp"}) --or esp
onfunction.AddFunction(0x26e840+offsets.base(),"Dig_Create",{item_type="esp"}) --esp+8 -> material esp->block type
onfunction.AddFunction(0x3d4301+offsets.base(),"Make_Item",{item_type="esp"})
onfunction.AddFunction(0x5af826+offsets.base(),"Hurt",{target="esi",attacker={off=0x74,rtype=DWORD,reg="esp"}})
onfunction.AddFunction(0x3D5886+offsets.base(),"Flip",{building="esi"})
onfunction.AddFunction(0x35E340+offsets.base(),"ItemCreate")--]=]
--onfunction.AddFunction(0x4B34B6+offsets.base(),"ReactionFinish") --esp item. Ecx creature, edx? 0.34.07
onfunction.AddFunction(0x72aB6+offsets.base(),"Die",{creature="edi"}) --0.34.07
else --linux
--[=[onfunction.AddFunction(0x899befe+offsets.base(),"Move") -- found out by attaching watch...
onfunction.AddFunction(0x850eecd+offsets.base(),"Die",{creature="ebx"}) -- same--]=]
end

@ -1,15 +0,0 @@
mypos=engine.getmod("functions")
function DeathMsg(values)
local name
local u=engine.cast(df.unit,values[onfunction.hints["Die"].creature])
print(u.name.first_name.." died")
end
if mypos then
print("Onfunction already installed")
--onfunction.patch(0x189dd6+offsets.base())
else
onfunction.install()
dofile("dfusion/onfunction/locations.lua")
onfunction.SetCallback("Die",DeathMsg)
end

@ -140,7 +140,7 @@ static command_result autodump_main(color_ostream &out, vector <string> & parame
return CR_FAILURE;
}
df::tiletype ttype = MC.tiletypeAt(pos_cursor);
if(!DFHack::isFloorTerrain(ttype))
if(!DFHack::isWalkable(ttype) || DFHack::isOpenTerrain(ttype))
{
out.printerr("Cursor should be placed over a floor.\n");
return CR_FAILURE;

@ -740,7 +740,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");
}

@ -221,10 +221,11 @@ command_result df_probe (color_ostream &out, vector <string> & parameters)
out.print("temperature2: %d U\n",mc.temperature2At(cursor));
int offset = block.region_offset[des.bits.biome];
df::coord2d region_pos = block.region_pos + df::coord2d ((offset % 3) - 1, (offset / 3) -1);
int bx = clip_range(block.region_pos.x + (offset % 3) - 1, 0, world->world_data->world_width-1);
int by = clip_range(block.region_pos.y + (offset / 3) - 1, 0, world->world_data->world_height-1);
df::world_data::T_region_map* biome =
&world->world_data->region_map[region_pos.x][region_pos.y];
&world->world_data->region_map[bx][by];
int sav = biome->savagery;
int evi = biome->evilness;

@ -66,6 +66,9 @@ 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).
@ -106,6 +109,14 @@ 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
----------------
@ -113,6 +124,10 @@ 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

@ -1,5 +1,45 @@
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]

@ -8,7 +8,7 @@ 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';
@ -298,8 +298,26 @@ sub render_field_reftarget {
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";
}
render_field_refto($parent, $name, $tgvec);
}
sub render_field_refto {
@ -329,9 +347,9 @@ sub render_container_reftarget {
return if (!$tg);
my $tgvec = $tg->getAttribute('instance-vector');
return if (!$tgvec);
my $idx = $tg->getAttribute('key-field');
$tgvec =~ s/^\$global/df/;
$tgvec =~ s/\[\$\]$//;
return if $tgvec !~ /^[\w\.]+$/;
my $tgname = "${name}_tg";
@ -341,7 +359,13 @@ sub render_container_reftarget {
$tgname .= '_' if ($othername and $tgname eq $othername);
}
push @lines_rb, "def $tgname ; $name.map { |i| ${tgvec}[i] } ; end";
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 {
@ -516,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') {

@ -2,15 +2,38 @@ module DFHack
class << self
# return an Item
# arg similar to unit.rb/unit_find; no arg = 'k' menu
def item_find(what=:selected)
def item_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.item if k.type == :Item
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)
world.items.all.binsearch(what)
# 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

@ -26,6 +26,13 @@ module DFHack
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|
@ -51,4 +58,109 @@ module DFHack
}
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

@ -87,7 +87,7 @@ module DFHack
def compound(name=nil, &b)
m = Class.new(Compound)
DFHack.const_set(name, m) if name
m.instance_eval(&b)
m.class_eval(&b)
m.new
end
def rtti_classname(n)
@ -277,6 +277,7 @@ module DFHack
def _get
addr = _getp
return if addr == 0
return addr if not @_tg
@_tg._at(addr)._get
end
@ -353,7 +354,9 @@ module DFHack
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
def index(e=nil, &b) ; (0...length).find { |i| b ? b[self[i]] : self[i] == e } ; end
def first ; self[0] ; end
def last ; self[length-1] ; end
end
class StaticArray < MemStruct
attr_accessor :_tglen, :_length, :_indexenum, :_tg
@ -377,12 +380,18 @@ module DFHack
def [](i)
i = _indexenum.int(i) if _indexenum
i += @_length if i < 0
_tgat(i)._get
if t = _tgat(i)
t._get
end
end
def []=(i, v)
i = _indexenum.int(i) if _indexenum
i += @_length if i < 0
_tgat(i)._set(v)
if t = _tgat(i)
t._set(v)
else
raise 'index out of bounds'
end
end
include Enumerable
@ -441,7 +450,7 @@ module DFHack
if idx >= length
insert_at(idx, 0)
elsif idx < 0
raise 'invalid idx'
raise 'index out of bounds'
end
@_tg._at(valueptr_at(idx))._set(v)
end
@ -527,7 +536,7 @@ module DFHack
if idx >= length
insert_at(idx, v)
elsif idx < 0
raise 'invalid idx'
raise 'index out of bounds'
else
DFHack.memory_vectorbool_setat(@_memaddr, idx, v)
end
@ -579,7 +588,7 @@ module DFHack
idx = _indexenum.int(idx) if _indexenum
idx += length if idx < 0
if idx >= length or idx < 0
raise 'invalid idx'
raise 'index out of bounds'
else
DFHack.memory_bitarray_set(@_memaddr, idx, v)
end
@ -605,11 +614,17 @@ module DFHack
end
def [](i)
i += _length if i < 0
_tgat(i)._get
if t = _tgat(i)
t._get
end
end
def []=(i, v)
i += _length if i < 0
_tgat(i)._set(v)
if t = _tgat(i)
t._set(v)
else
raise 'index out of bounds'
end
end
def _set(a)
a.each_with_index { |v, i| self[i] = v }

@ -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"
@ -35,9 +34,13 @@ tthread::mutex *m_irun;
tthread::mutex *m_mutex;
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 tthread::thread *r_thread;
static int onupdate_active;
static int onupdate_minyear, onupdate_minyeartick;
static color_ostream_proxy *console_proxy;
DFHACK_PLUGIN("ruby")
@ -115,7 +118,7 @@ DFhackCExport command_result plugin_shutdown ( color_ostream &out )
}
// send a single ruby line to be evaluated by the ruby thread
DFhackCExport command_result plugin_eval_ruby(const char *command)
DFhackCExport command_result plugin_eval_ruby( color_ostream &out, const char *command)
{
// if dlopen failed
if (!r_thread)
@ -136,6 +139,7 @@ DFhackCExport command_result plugin_eval_ruby(const char *command)
r_type = RB_EVAL;
r_command = command;
r_console = &out;
// wake ruby thread up
m_irun->unlock();
@ -144,6 +148,7 @@ DFhackCExport command_result plugin_eval_ruby(const char *command)
tthread::this_thread::yield();
ret = r_result;
r_console = NULL;
// block ruby thread
m_irun->lock();
@ -160,11 +165,16 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out )
// ruby sets this flag when needed, to avoid lag running ruby code every
// frame if not necessary
// TODO dynamic check on df::cur_year{_tick}
if (!onupdate_active)
return CR_OK;
return plugin_eval_ruby("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)
@ -184,10 +194,12 @@ DFhackCExport command_result plugin_onstatechange ( color_ostream &out, state_ch
// 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;
SCASE(PAUSED);
SCASE(UNPAUSED);
#undef SCASE
}
return plugin_eval_ruby(cmd.c_str());
return plugin_eval_ruby(out, cmd.c_str());
}
static command_result df_rubyeval(color_ostream &out, std::vector <std::string> & parameters)
@ -209,7 +221,7 @@ static command_result df_rubyeval(color_ostream &out, std::vector <std::string>
full += " ";
}
return plugin_eval_ruby(full.c_str());
return plugin_eval_ruby(out, full.c_str());
}
@ -265,7 +277,7 @@ static int df_loadruby(void)
#if defined(WIN32)
"./libruby.dll";
#elif defined(__APPLE__)
"/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/libruby.1.dylib";
"/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/libruby.1.dylib";
#else
"hack/libruby.so";
#endif
@ -310,6 +322,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)
@ -320,19 +339,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)
{
@ -412,7 +429,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;
@ -420,21 +437,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_dfonupdate_minyear(VALUE self)
{
return rb_uint2inum(onupdate_minyear);
}
static VALUE rb_dfonupdate_minyear_set(VALUE self, VALUE val)
{
onupdate_minyear = rb_num2ulong(val);
return Qtrue;
}
static VALUE rb_dfonupdate_minyeartick(VALUE self)
{
return rb_uint2inum(onupdate_minyeartick);
}
static VALUE rb_dfonupdate_minyeartick_set(VALUE self, VALUE val)
{
onupdate_minyeartick = rb_num2ulong(val);
return Qtrue;
}
static VALUE rb_dfprint_str(VALUE self, VALUE s)
{
console_proxy->print("%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;
}
static VALUE rb_dfprint_err(VALUE self, VALUE s)
{
Core::printerr("%s", rb_string_value_ptr(&s));
printerr("%s", rb_string_value_ptr(&s));
return Qnil;
}
@ -764,8 +806,12 @@ 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");
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, "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);

@ -23,26 +23,78 @@ module Kernel
end
module DFHack
class OnupdateCallback
attr_accessor :callback, :timelimit, :minyear, :minyeartick
def initialize(cb, tl)
@callback = cb
@ticklimit = tl
@minyear = (tl ? df.cur_year : 0)
@minyeartick = (tl ? df.cur_year_tick : 0)
end
# run callback if timedout
def check_run(year, yeartick, yearlen)
if !@ticklimit
@callback.call
else
if year > @minyear or (year == @minyear and yeartick >= @minyeartick)
@callback.call
@minyear = year
@minyeartick = yeartick + @ticklimit
if @minyeartick > yearlen
@minyear += 1
@minyeartick -= yearlen
end
end
end
end
def <=>(o)
[@minyear, @minyeartick] <=> [o.minyear, o.minyeartick]
end
end
class << self
attr_accessor :onupdate_list, :onstatechange_list
# register a callback to be called every gframe or more
# ex: DFHack.onupdate_register { DFHack.world.units[0].counters.job_counter = 0 }
def onupdate_register(&b)
def onupdate_register(ticklimit=nil, &b)
@onupdate_list ||= []
@onupdate_list << b
@onupdate_list << OnupdateCallback.new(b, ticklimit)
DFHack.onupdate_active = true
if onext = @onupdate_list.sort.first
DFHack.onupdate_minyear = onext.minyear
DFHack.onupdate_minyeartick = onext.minyeartick
end
@onupdate_list.last
end
# delete the callback for onupdate ; use the value returned by onupdate_register
def onupdate_unregister(b)
@onupdate_list.delete b
DFHack.onupdate_active = false if @onupdate_list.empty?
if @onupdate_list.empty?
DFHack.onupdate_active = false
DFHack.onupdate_minyear = DFHack.onupdate_minyeartick = 0
end
end
TICKS_PER_YEAR = 1200*28*12
# this method is called by dfhack every 'onupdate' if onupdate_active is true
def onupdate
@onupdate_list ||= []
@onupdate_list.each { |cb| cb.call }
ticks_per_year = TICKS_PER_YEAR
ticks_per_year *= 72 if gametype == :ADVENTURE_MAIN or gametype == :ADVENTURE_ARENA
@onupdate_list.each { |o|
o.check_run(cur_year, cur_year_tick, ticks_per_year)
}
if onext = @onupdate_list.sort.first
DFHack.onupdate_minyear = onext.minyear
DFHack.onupdate_minyeartick = onext.minyeartick
end
end
# register a callback to be called every gframe or more
@ -85,6 +137,57 @@ module DFHack
may = rawlist.find_all { |r| r.downcase.index(name.downcase) }
may.first if may.length == 1
end
def translate_name(name, english=true, onlylastpart=false)
out = []
if not onlylastpart
out << name.first_name if name.first_name != ''
if name.nickname != ''
case respond_to?(:d_init) && d_init.nickname_dwarf
when :REPLACE_ALL; return "`#{name.nickname}'"
when :REPLACE_FIRST; out.pop
end
out << "`#{name.nickname}'"
end
end
return out.join(' ') unless name.words.find { |w| w >= 0 }
if not english
tsl = world.raws.language.translations[name.language]
if name.words[0] >= 0 or name.words[1] >= 0
out << ''
out.last << tsl.words[name.words[0]] if name.words[0] >= 0
out.last << tsl.words[name.words[1]] if name.words[1] >= 0
end
if name.words[5] >= 0
out << ''
(2..5).each { |i| out.last << tsl.words[name.words[i]] if name.words[i] >= 0 }
end
if name.words[6] >= 0
out << tsl.words[name.words[6]]
end
else
wl = world.raws.language
if name.words[0] >= 0 or name.words[1] >= 0
out << ''
out.last << wl.words[name.words[0]].forms[name.parts_of_speech[0]] if name.words[0] >= 0
out.last << wl.words[name.words[1]].forms[name.parts_of_speech[1]] if name.words[1] >= 0
end
if name.words[5] >= 0
out << 'the '
out.last.capitalize! if out.length == 1
(2..5).each { |i| out.last << wl.words[name.words[i]].forms[name.parts_of_speech[i]] if name.words[i] >= 0 }
end
if name.words[6] >= 0
out << 'of'
out.last.capitalize! if out.length == 1
out << wl.words[name.words[6]].forms[name.parts_of_speech[6]]
end
end
out.join(' ')
end
end
end

@ -1,6 +1,13 @@
# 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)

@ -4,19 +4,28 @@ module DFHack
# 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)
def unit_find(what=:selected, y=nil, z=nil)
if what == :selected
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
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)
world.units.all.binsearch(what)
# 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
@ -75,4 +84,10 @@ module DFHack
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)

@ -1,7 +1,22 @@
-- Prepare the current save for use with devel/find-offsets.
local utils = require 'utils'
df.global.pause_state = true
print[[
WARNING: THIS SCRIPT IS STRICTLY FOR DFHACK DEVELOPERS.
This script prepares the current savegame to be used
with devel/find-offsets. It CHANGES THE GAME STATE
to predefined values, and initiates an immediate
quicksave, thus PERMANENTLY MODIFYING the save.
]]
if not utils.prompt_yes_no('Proceed?') then
return
end
--[[print('Placing anchor...')
do

@ -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"