develop
Timothy Collett 2012-09-10 09:18:24 -04:00
commit 270351f510
47 changed files with 39952 additions and 1855 deletions

@ -152,10 +152,13 @@ Valid and useful build types include 'Release', 'Debug' and
================================
Using the library as a developer
================================
Currently, the only way to use the library is to write a plugin that can be loaded by it.
Currently, the most direct way to use the library is to write a plugin that can be loaded by it.
All the plugins can be found in the 'plugins' folder. There's no in-depth documentation
on how to write one yet, but it should be easy enough to copy one and just follow the pattern.
Other than through plugins, it is possible to use DFHack via remote access interface, or by writing Lua scripts.
The most important parts of DFHack are the Core, Console, Modules and Plugins.
* Core acts as the centerpiece of DFHack - it acts as a filter between DF and SDL and synchronizes the various plugins with DF.
@ -171,6 +174,24 @@ The main license is zlib/libpng, some bits are MIT licensed, and some are BSD li
Feel free to add your own extensions and plugins. Contributing back to
the dfhack repository is welcome and the right thing to do :)
DF data structure definitions
=============================
DFHack uses information about the game data structures, represented via xml files in the library/xml/ submodule.
Data structure layouts are described in files following the df.*.xml name pattern. This information is transformed by a perl script into C++ headers describing the structures, and associated metadata for the Lua wrapper. These headers and data are then compiled into the DFHack libraries, thus necessitating a compatibility break every time layouts change; in return it significantly boosts the efficiency and capabilities of DFHack code.
Global object addresses are stored in symbols.xml, which is copied to the dfhack release package and loaded as data at runtime.
Remote access interface
=======================
DFHack supports remote access by exchanging Google protobuf messages via a TCP socket. Both the core and plugins can define remotely accessible methods. The ``dfhack-run`` command uses this interface to invoke ordinary console commands.
Currently the supported set of requests is limited, because the developers don't know what exactly is most useful.
Protocol client implementations exist for Java and C#.
Contributing to DFHack
======================

@ -334,10 +334,12 @@ ul.auto-toc {
</li>
<li><a class="reference internal" href="#build-types" id="id12">Build types</a></li>
<li><a class="reference internal" href="#using-the-library-as-a-developer" id="id13">Using the library as a developer</a><ul>
<li><a class="reference internal" href="#contributing-to-dfhack" id="id14">Contributing to DFHack</a><ul>
<li><a class="reference internal" href="#coding-style" id="id15">Coding style</a></li>
<li><a class="reference internal" href="#how-to-get-new-code-into-dfhack" id="id16">How to get new code into DFHack</a></li>
<li><a class="reference internal" href="#memory-research" id="id17">Memory research</a></li>
<li><a class="reference internal" href="#df-data-structure-definitions" id="id14">DF data structure definitions</a></li>
<li><a class="reference internal" href="#remote-access-interface" id="id15">Remote access interface</a></li>
<li><a class="reference internal" href="#contributing-to-dfhack" id="id16">Contributing to DFHack</a><ul>
<li><a class="reference internal" href="#coding-style" id="id17">Coding style</a></li>
<li><a class="reference internal" href="#how-to-get-new-code-into-dfhack" id="id18">How to get new code into DFHack</a></li>
<li><a class="reference internal" href="#memory-research" id="id19">Memory research</a></li>
</ul>
</li>
</ul>
@ -470,9 +472,10 @@ cmake .. -DCMAKE_BUILD_TYPE:string=BUILD_TYPE
</div>
<div class="section" id="using-the-library-as-a-developer">
<h1><a class="toc-backref" href="#id13">Using the library as a developer</a></h1>
<p>Currently, the only way to use the library is to write a plugin that can be loaded by it.
<p>Currently, the most direct way to use the library is to write a plugin that can be loaded by it.
All the plugins can be found in the 'plugins' folder. There's no in-depth documentation
on how to write one yet, but it should be easy enough to copy one and just follow the pattern.</p>
<p>Other than through plugins, it is possible to use DFHack via remote access interface, or by writing Lua scripts.</p>
<p>The most important parts of DFHack are the Core, Console, Modules and Plugins.</p>
<ul class="simple">
<li>Core acts as the centerpiece of DFHack - it acts as a filter between DF and SDL and synchronizes the various plugins with DF.</li>
@ -485,18 +488,30 @@ on how to write one yet, but it should be easy enough to copy one and just follo
The main license is zlib/libpng, some bits are MIT licensed, and some are BSD licensed.</p>
<p>Feel free to add your own extensions and plugins. Contributing back to
the dfhack repository is welcome and the right thing to do :)</p>
<div class="section" id="df-data-structure-definitions">
<h2><a class="toc-backref" href="#id14">DF data structure definitions</a></h2>
<p>DFHack uses information about the game data structures, represented via xml files in the library/xml/ submodule.</p>
<p>Data structure layouts are described in files following the df.*.xml name pattern. This information is transformed by a perl script into C++ headers describing the structures, and associated metadata for the Lua wrapper. These headers and data are then compiled into the DFHack libraries, thus necessitating a compatibility break every time layouts change; in return it significantly boosts the efficiency and capabilities of DFHack code.</p>
<p>Global object addresses are stored in symbols.xml, which is copied to the dfhack release package and loaded as data at runtime.</p>
</div>
<div class="section" id="remote-access-interface">
<h2><a class="toc-backref" href="#id15">Remote access interface</a></h2>
<p>DFHack supports remote access by exchanging Google protobuf messages via a TCP socket. Both the core and plugins can define remotely accessible methods. The <tt class="docutils literal"><span class="pre">dfhack-run</span></tt> command uses this interface to invoke ordinary console commands.</p>
<p>Currently the supported set of requests is limited, because the developers don't know what exactly is most useful.</p>
<p>Protocol client implementations exist for Java and C#.</p>
</div>
<div class="section" id="contributing-to-dfhack">
<h2><a class="toc-backref" href="#id14">Contributing to DFHack</a></h2>
<h2><a class="toc-backref" href="#id16">Contributing to DFHack</a></h2>
<p>Several things should be kept in mind when contributing to DFHack.</p>
<div class="section" id="coding-style">
<h3><a class="toc-backref" href="#id15">Coding style</a></h3>
<h3><a class="toc-backref" href="#id17">Coding style</a></h3>
<p>DFhack uses ANSI formatting and four spaces as indentation. Line
endings are UNIX. The files use UTF-8 encoding. Code not following this
won't make me happy, because I'll have to fix it. There's a good chance
I'll make <em>you</em> fix it ;)</p>
</div>
<div class="section" id="how-to-get-new-code-into-dfhack">
<h3><a class="toc-backref" href="#id16">How to get new code into DFHack</a></h3>
<h3><a class="toc-backref" href="#id18">How to get new code into DFHack</a></h3>
<p>You can send patches or make a clone of the github repo and ask me on
the IRC channel to pull your code in. I'll review it and see if there
are any problems. I'll fix them if they are minor.</p>
@ -506,7 +521,7 @@ this is also a good place to dump new ideas and/or bugs that need
fixing.</p>
</div>
<div class="section" id="memory-research">
<h3><a class="toc-backref" href="#id17">Memory research</a></h3>
<h3><a class="toc-backref" href="#id19">Memory research</a></h3>
<p>If you want to do memory research, you'll need some tools and some knowledge.
In general, you'll need a good memory viewer and optionally something
to look at machine code without getting crazy :)</p>

@ -4,9 +4,26 @@ DFHack Lua API
.. contents::
====================
DF structure wrapper
====================
The current version of DFHack has extensive support for
the Lua scripting language, providing access to:
1. Raw data structures used by the game.
2. Many C++ functions for high-level access to these
structures, and interaction with dfhack itself.
3. Some functions exported by C++ plugins.
Lua code can be used both for writing scripts, which
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
implemented by Lua files located in hack/lua/...
=========================
DF data structure wrapper
=========================
DF structures described by the xml files in library/xml are exported
to lua code as a tree of objects and functions under the ``df`` global,
@ -426,13 +443,17 @@ not destroy any objects allocated in this way, so the user
should be prepared to catch the error and do the necessary
cleanup.
================
DFHack utilities
================
==========
DFHack API
==========
DFHack utility functions are placed in the ``dfhack`` global tree.
Currently it defines the following features:
Native utilities
================
Input & Output
--------------
* ``dfhack.print(args...)``
@ -451,6 +472,7 @@ Currently it defines the following features:
* ``dfhack.color([color])``
Sets the current output color. If color is *nil* or *-1*, resets to default.
Returns the previous color value.
* ``dfhack.is_interactive()``
@ -473,15 +495,27 @@ Currently it defines the following features:
If the interactive console is not accessible, returns *nil, error*.
Exception handling
------------------
* ``dfhack.error(msg[,level[,verbose]])``
Throws a dfhack exception object with location and stack trace.
The verbose parameter controls whether the trace is printed by default.
* ``qerror(msg[,level])``
Calls ``dfhack.error()`` with ``verbose`` being *false*. Intended to
be used for user-caused errors in scripts, where stack traces are not
desirable.
* ``dfhack.pcall(f[,args...])``
Invokes f via xpcall, using an error function that attaches
a stack trace to the error. The same function is used by SafeCall
in C++, and dfhack.safecall.
The returned error is a table with separate ``message`` and
``stacktrace`` string fields; it implements ``__tostring``.
* ``safecall(f[,args...])``, ``dfhack.safecall(f[,args...])``
Just like pcall, but also prints the error using printerr before
@ -491,12 +525,33 @@ Currently it defines the following features:
Compares to coroutine.resume like dfhack.safecall vs pcall.
* ``dfhack.run_script(name[,args...])``
* ``dfhack.exception``
Run a lua script in hack/scripts/, as if it was started from dfhack command-line.
The ``name`` argument should be the name stem, as would be used on the command line.
Note that the script is re-read from the file every time it is called, and errors
are propagated to the caller.
Metatable of error objects used by dfhack. The objects have the
following properties:
``err.where``
The location prefix string, or *nil*.
``err.message``
The base message string.
``err.stacktrace``
The stack trace string, or *nil*.
``err.cause``
A different exception object, or *nil*.
``err.thread``
The coroutine that has thrown the exception.
``err.verbose``
Boolean, or *nil*; specifies if where and stacktrace should be printed.
``tostring(err)``, or ``err:tostring([verbose])``
Converts the exception to string.
* ``dfhack.exception.verbose``
The default value of the ``verbose`` argument of ``err:tostring()``.
Locking and finalization
------------------------
* ``dfhack.with_suspend(f[,args...])``
@ -536,7 +591,7 @@ Currently it defines the following features:
Persistent configuration storage
================================
--------------------------------
This api is intended for storing configuration options in the world itself.
It probably should be restricted to data that is world-dependent.
@ -578,7 +633,7 @@ functions can just copy values in memory without doing any actual I/O.
However, currently every entry has a 180+-byte dead-weight overhead.
Material info lookup
====================
--------------------
A material info record has fields:
@ -1172,9 +1227,9 @@ and are only documented here for completeness:
Returns the pre-extracted vtable address ``name``, or *nil*.
* ``dfhack.internal.getBase()``
* ``dfhack.internal.getRebaseDelta()``
Returns the base address of the process.
Returns the ASLR rebase offset of the DF executable.
* ``dfhack.internal.getMemRanges()``
@ -1267,6 +1322,42 @@ Features:
Invokes all listeners contained in the event in an arbitrary
order using ``dfhack.safecall``.
=======
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/.
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:
* ``mkmodule(name)``
Creates an environment table for the module. Intended to be used as::
local _ENV = mkmodule('foo')
...
return _ENV
If called the second time, returns the same table; thus providing reload support.
* ``reload(name)``
Reloads a previously ``require``-d module *"name"* from the file.
Intended as a help for module development.
* ``dfhack.BASE_G``
This variable contains the root global environment table, which is
used as a base for all module and script environments. Its contents
should be kept limited to the standard Lua library and API described
in this document.
=======
Plugins
=======
@ -1328,3 +1419,40 @@ sort
Does not export any native functions as of now. Instead, it
calls lua code to perform the actual ordering of list items.
=======
Scripts
=======
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.
**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.
When calling such scripts, always use '/' as the separator for
directories, e.g. ``devel/lua-example``.
Scripts are re-read from disk every time they are used
(this may be changed later to check the file change time); however
the global variable values persist in memory between calls.
Every script gets its own separate environment for global
variables.
Arguments are passed in to the scripts via the **...** built-in
quasi-variable; when the script is called by the DFHack core,
they are all guaranteed to be non-nil strings.
DFHack core invokes the scripts in the *core context* (see above);
however it is possible to call them from any lua code (including
from other scripts) in any context, via the same function the core uses:
* ``dfhack.run_script(name[,args...])``
Run a lua script in hack/scripts/, as if it was started from dfhack command-line.
The ``name`` argument should be the name stem, as would be used on the command line.
Note that this function lets errors propagate to the caller.

@ -320,7 +320,7 @@ ul.auto-toc {
<div class="contents topic" id="contents">
<p class="topic-title first">Contents</p>
<ul class="simple">
<li><a class="reference internal" href="#df-structure-wrapper" id="id1">DF structure wrapper</a><ul>
<li><a class="reference internal" href="#df-data-structure-wrapper" id="id1">DF data structure wrapper</a><ul>
<li><a class="reference internal" href="#typed-object-references" id="id2">Typed object references</a><ul>
<li><a class="reference internal" href="#primitive-references" id="id3">Primitive references</a></li>
<li><a class="reference internal" href="#struct-references" id="id4">Struct references</a></li>
@ -333,36 +333,58 @@ ul.auto-toc {
<li><a class="reference internal" href="#recursive-table-assignment" id="id9">Recursive table assignment</a></li>
</ul>
</li>
<li><a class="reference internal" href="#dfhack-utilities" id="id10">DFHack utilities</a><ul>
<li><a class="reference internal" href="#persistent-configuration-storage" id="id11">Persistent configuration storage</a></li>
<li><a class="reference internal" href="#material-info-lookup" id="id12">Material info lookup</a></li>
<li><a class="reference internal" href="#c-function-wrappers" id="id13">C++ function wrappers</a><ul>
<li><a class="reference internal" href="#gui-module" id="id14">Gui module</a></li>
<li><a class="reference internal" href="#job-module" id="id15">Job module</a></li>
<li><a class="reference internal" href="#units-module" id="id16">Units module</a></li>
<li><a class="reference internal" href="#items-module" id="id17">Items module</a></li>
<li><a class="reference internal" href="#maps-module" id="id18">Maps module</a></li>
<li><a class="reference internal" href="#burrows-module" id="id19">Burrows module</a></li>
<li><a class="reference internal" href="#buildings-module" id="id20">Buildings module</a></li>
<li><a class="reference internal" href="#constructions-module" id="id21">Constructions module</a></li>
<li><a class="reference internal" href="#internal-api" id="id22">Internal API</a></li>
<li><a class="reference internal" href="#dfhack-api" id="id10">DFHack API</a><ul>
<li><a class="reference internal" href="#native-utilities" id="id11">Native utilities</a><ul>
<li><a class="reference internal" href="#input-output" id="id12">Input &amp; Output</a></li>
<li><a class="reference internal" href="#exception-handling" id="id13">Exception handling</a></li>
<li><a class="reference internal" href="#locking-and-finalization" id="id14">Locking and finalization</a></li>
<li><a class="reference internal" href="#persistent-configuration-storage" id="id15">Persistent configuration storage</a></li>
<li><a class="reference internal" href="#material-info-lookup" id="id16">Material info lookup</a></li>
</ul>
</li>
<li><a class="reference internal" href="#core-interpreter-context" id="id23">Core interpreter context</a><ul>
<li><a class="reference internal" href="#event-type" id="id24">Event type</a></li>
<li><a class="reference internal" href="#c-function-wrappers" id="id17">C++ function wrappers</a><ul>
<li><a class="reference internal" href="#gui-module" id="id18">Gui module</a></li>
<li><a class="reference internal" href="#job-module" id="id19">Job module</a></li>
<li><a class="reference internal" href="#units-module" id="id20">Units module</a></li>
<li><a class="reference internal" href="#items-module" id="id21">Items module</a></li>
<li><a class="reference internal" href="#maps-module" id="id22">Maps module</a></li>
<li><a class="reference internal" href="#burrows-module" id="id23">Burrows module</a></li>
<li><a class="reference internal" href="#buildings-module" id="id24">Buildings module</a></li>
<li><a class="reference internal" href="#constructions-module" id="id25">Constructions module</a></li>
<li><a class="reference internal" href="#internal-api" id="id26">Internal API</a></li>
</ul>
</li>
<li><a class="reference internal" href="#core-interpreter-context" id="id27">Core interpreter context</a><ul>
<li><a class="reference internal" href="#event-type" id="id28">Event type</a></li>
</ul>
</li>
<li><a class="reference internal" href="#plugins" id="id25">Plugins</a><ul>
<li><a class="reference internal" href="#burrows" id="id26">burrows</a></li>
<li><a class="reference internal" href="#sort" id="id27">sort</a></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>
</ul>
</li>
<li><a class="reference internal" href="#scripts" id="id33">Scripts</a></li>
</ul>
</div>
<div class="section" id="df-structure-wrapper">
<h1><a class="toc-backref" href="#id1">DF structure wrapper</a></h1>
<p>The current version of DFHack has extensive support for
the Lua scripting language, providing access to:</p>
<ol class="arabic simple">
<li>Raw data structures used by the game.</li>
<li>Many C++ functions for high-level access to these
structures, and interaction with dfhack itself.</li>
<li>Some functions exported by C++ plugins.</li>
</ol>
<p>Lua code can be used both for writing scripts, which
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
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>
<p>DF structures described by the xml files in library/xml are exported
to lua code as a tree of objects and functions under the <tt class="docutils literal">df</tt> global,
which broadly maps to the <tt class="docutils literal">df</tt> namespace in C++.</p>
@ -717,10 +739,13 @@ should be prepared to catch the error and do the necessary
cleanup.</p>
</div>
</div>
<div class="section" id="dfhack-utilities">
<h1><a class="toc-backref" href="#id10">DFHack utilities</a></h1>
<div class="section" id="dfhack-api">
<h1><a class="toc-backref" href="#id10">DFHack API</a></h1>
<p>DFHack utility functions are placed in the <tt class="docutils literal">dfhack</tt> global tree.</p>
<p>Currently it defines the following features:</p>
<div class="section" id="native-utilities">
<h2><a class="toc-backref" href="#id11">Native utilities</a></h2>
<div class="section" id="input-output">
<h3><a class="toc-backref" href="#id12">Input &amp; Output</a></h3>
<ul>
<li><p class="first"><tt class="docutils literal"><span class="pre">dfhack.print(args...)</span></tt></p>
<p>Output tab-separated args as standard lua print would do,
@ -734,7 +759,8 @@ works with DFHack output infrastructure.</p>
<p>Same as println; intended for errors. Uses red color and logs to stderr.log.</p>
</li>
<li><p class="first"><tt class="docutils literal"><span class="pre">dfhack.color([color])</span></tt></p>
<p>Sets the current output color. If color is <em>nil</em> or <em>-1</em>, resets to default.</p>
<p>Sets the current output color. If color is <em>nil</em> or <em>-1</em>, resets to default.
Returns the previous color value.</p>
</li>
<li><p class="first"><tt class="docutils literal">dfhack.is_interactive()</tt></p>
<p>Checks if the thread can access the interactive console and returns <em>true</em> or <em>false</em>.</p>
@ -752,12 +778,24 @@ this, forcing the function to block on input with lock held.</p>
string, global environment and command-line history file.</p>
<p>If the interactive console is not accessible, returns <em>nil, error</em>.</p>
</li>
</ul>
</div>
<div class="section" id="exception-handling">
<h3><a class="toc-backref" href="#id13">Exception handling</a></h3>
<ul>
<li><p class="first"><tt class="docutils literal"><span class="pre">dfhack.error(msg[,level[,verbose]])</span></tt></p>
<p>Throws a dfhack exception object with location and stack trace.
The verbose parameter controls whether the trace is printed by default.</p>
</li>
<li><p class="first"><tt class="docutils literal"><span class="pre">qerror(msg[,level])</span></tt></p>
<p>Calls <tt class="docutils literal">dfhack.error()</tt> with <tt class="docutils literal">verbose</tt> being <em>false</em>. Intended to
be used for user-caused errors in scripts, where stack traces are not
desirable.</p>
</li>
<li><p class="first"><tt class="docutils literal"><span class="pre">dfhack.pcall(f[,args...])</span></tt></p>
<p>Invokes f via xpcall, using an error function that attaches
a stack trace to the error. The same function is used by SafeCall
in C++, and dfhack.safecall.</p>
<p>The returned error is a table with separate <tt class="docutils literal">message</tt> and
<tt class="docutils literal">stacktrace</tt> string fields; it implements <tt class="docutils literal">__tostring</tt>.</p>
</li>
<li><p class="first"><tt class="docutils literal"><span class="pre">safecall(f[,args...])</span></tt>, <tt class="docutils literal"><span class="pre">dfhack.safecall(f[,args...])</span></tt></p>
<p>Just like pcall, but also prints the error using printerr before
@ -766,12 +804,41 @@ returning. Intended as a convenience function.</p>
<li><p class="first"><tt class="docutils literal"><span class="pre">dfhack.saferesume(coroutine[,args...])</span></tt></p>
<p>Compares to coroutine.resume like dfhack.safecall vs pcall.</p>
</li>
<li><p class="first"><tt class="docutils literal"><span class="pre">dfhack.run_script(name[,args...])</span></tt></p>
<p>Run a lua script in hack/scripts/, as if it was started from dfhack command-line.
The <tt class="docutils literal">name</tt> argument should be the name stem, as would be used on the command line.
Note that the script is re-read from the file every time it is called, and errors
are propagated to the caller.</p>
<li><p class="first"><tt class="docutils literal">dfhack.exception</tt></p>
<p>Metatable of error objects used by dfhack. The objects have the
following properties:</p>
<dl class="docutils">
<dt><tt class="docutils literal">err.where</tt></dt>
<dd><p class="first last">The location prefix string, or <em>nil</em>.</p>
</dd>
<dt><tt class="docutils literal">err.message</tt></dt>
<dd><p class="first last">The base message string.</p>
</dd>
<dt><tt class="docutils literal">err.stacktrace</tt></dt>
<dd><p class="first last">The stack trace string, or <em>nil</em>.</p>
</dd>
<dt><tt class="docutils literal">err.cause</tt></dt>
<dd><p class="first last">A different exception object, or <em>nil</em>.</p>
</dd>
<dt><tt class="docutils literal">err.thread</tt></dt>
<dd><p class="first last">The coroutine that has thrown the exception.</p>
</dd>
<dt><tt class="docutils literal">err.verbose</tt></dt>
<dd><p class="first last">Boolean, or <em>nil</em>; specifies if where and stacktrace should be printed.</p>
</dd>
<dt><tt class="docutils literal">tostring(err)</tt>, or <tt class="docutils literal"><span class="pre">err:tostring([verbose])</span></tt></dt>
<dd><p class="first last">Converts the exception to string.</p>
</dd>
</dl>
</li>
<li><p class="first"><tt class="docutils literal">dfhack.exception.verbose</tt></p>
<p>The default value of the <tt class="docutils literal">verbose</tt> argument of <tt class="docutils literal">err:tostring()</tt>.</p>
</li>
</ul>
</div>
<div class="section" id="locking-and-finalization">
<h3><a class="toc-backref" href="#id14">Locking and finalization</a></h3>
<ul>
<li><p class="first"><tt class="docutils literal"><span class="pre">dfhack.with_suspend(f[,args...])</span></tt></p>
<p>Calls <tt class="docutils literal">f</tt> with arguments after grabbing the DF core suspend lock.
Suspending is necessary for accessing a consistent state of DF memory.</p>
@ -801,8 +868,9 @@ Implemented using <tt class="docutils literal"><span class="pre">call_with_final
<p>Calls <tt class="docutils literal"><span class="pre">fn(obj,args...)</span></tt>, then finalizes with <tt class="docutils literal">obj:delete()</tt>.</p>
</li>
</ul>
</div>
<div class="section" id="persistent-configuration-storage">
<h2><a class="toc-backref" href="#id11">Persistent configuration storage</a></h2>
<h3><a class="toc-backref" href="#id15">Persistent configuration storage</a></h3>
<p>This api is intended for storing configuration options in the world itself.
It probably should be restricted to data that is world-dependent.</p>
<p>Entries are identified by a string <tt class="docutils literal">key</tt>, but it is also possible to manage
@ -837,7 +905,7 @@ functions can just copy values in memory without doing any actual I/O.
However, currently every entry has a 180+-byte dead-weight overhead.</p>
</div>
<div class="section" id="material-info-lookup">
<h2><a class="toc-backref" href="#id12">Material info lookup</a></h2>
<h3><a class="toc-backref" href="#id16">Material info lookup</a></h3>
<p>A material info record has fields:</p>
<ul>
<li><p class="first"><tt class="docutils literal">type</tt>, <tt class="docutils literal">index</tt>, <tt class="docutils literal">material</tt></p>
@ -880,8 +948,9 @@ Accept dfhack_material_category auto-assign table.</p>
</li>
</ul>
</div>
</div>
<div class="section" id="c-function-wrappers">
<h2><a class="toc-backref" href="#id13">C++ function wrappers</a></h2>
<h2><a class="toc-backref" href="#id17">C++ function wrappers</a></h2>
<p>Thin wrappers around C++ functions, similar to the ones for virtual methods.
One notable difference is that these explicit wrappers allow argument count
adjustment according to the usual lua rules, so trailing false/nil arguments
@ -910,7 +979,7 @@ can be omitted.</p>
</li>
</ul>
<div class="section" id="gui-module">
<h3><a class="toc-backref" href="#id14">Gui module</a></h3>
<h3><a class="toc-backref" href="#id18">Gui module</a></h3>
<ul>
<li><p class="first"><tt class="docutils literal">dfhack.gui.getCurViewscreen()</tt></p>
<p>Returns the viewscreen that is current in the core.</p>
@ -946,7 +1015,7 @@ The is_bright boolean actually seems to invert the brightness.</p>
</ul>
</div>
<div class="section" id="job-module">
<h3><a class="toc-backref" href="#id15">Job module</a></h3>
<h3><a class="toc-backref" href="#id19">Job module</a></h3>
<ul>
<li><p class="first"><tt class="docutils literal">dfhack.job.cloneJobStruct(job)</tt></p>
<p>Creates a deep copy of the given job.</p>
@ -983,7 +1052,7 @@ a lua list containing them.</p>
</ul>
</div>
<div class="section" id="units-module">
<h3><a class="toc-backref" href="#id16">Units module</a></h3>
<h3><a class="toc-backref" href="#id20">Units module</a></h3>
<ul>
<li><p class="first"><tt class="docutils literal">dfhack.units.getPosition(unit)</tt></p>
<p>Returns true <em>x,y,z</em> of the unit, or <em>nil</em> if invalid; may be not equal to unit.pos if caged.</p>
@ -1037,7 +1106,7 @@ or raws. The <tt class="docutils literal">ignore_noble</tt> boolean disables the
</ul>
</div>
<div class="section" id="items-module">
<h3><a class="toc-backref" href="#id17">Items module</a></h3>
<h3><a class="toc-backref" href="#id21">Items module</a></h3>
<ul>
<li><p class="first"><tt class="docutils literal">dfhack.items.getPosition(item)</tt></p>
<p>Returns true <em>x,y,z</em> of the item, or <em>nil</em> if invalid; may be not equal to item.pos if in inventory.</p>
@ -1080,7 +1149,7 @@ Returns <em>false</em> in case of error.</p>
</ul>
</div>
<div class="section" id="maps-module">
<h3><a class="toc-backref" href="#id18">Maps module</a></h3>
<h3><a class="toc-backref" href="#id22">Maps module</a></h3>
<ul>
<li><p class="first"><tt class="docutils literal">dfhack.maps.getSize()</tt></p>
<p>Returns map size in blocks: <em>x, y, z</em></p>
@ -1121,7 +1190,7 @@ burrows, or the presence of invaders.</p>
</ul>
</div>
<div class="section" id="burrows-module">
<h3><a class="toc-backref" href="#id19">Burrows module</a></h3>
<h3><a class="toc-backref" href="#id23">Burrows module</a></h3>
<ul>
<li><p class="first"><tt class="docutils literal">dfhack.burrows.findByName(name)</tt></p>
<p>Returns the burrow pointer or <em>nil</em>.</p>
@ -1156,7 +1225,7 @@ burrows, or the presence of invaders.</p>
</ul>
</div>
<div class="section" id="buildings-module">
<h3><a class="toc-backref" href="#id20">Buildings module</a></h3>
<h3><a class="toc-backref" href="#id24">Buildings module</a></h3>
<ul>
<li><p class="first"><tt class="docutils literal">dfhack.buildings.getSize(building)</tt></p>
<p>Returns <em>width, height, centerx, centery</em>.</p>
@ -1296,7 +1365,7 @@ can be determined this way, <tt class="docutils literal">constructBuilding</tt>
</ul>
</div>
<div class="section" id="constructions-module">
<h3><a class="toc-backref" href="#id21">Constructions module</a></h3>
<h3><a class="toc-backref" href="#id25">Constructions module</a></h3>
<ul>
<li><p class="first"><tt class="docutils literal">dfhack.constructions.designateNew(pos,type,item_type,mat_index)</tt></p>
<p>Designates a new construction at given position. If there already is
@ -1312,7 +1381,7 @@ Returns <em>true, was_only_planned</em> if removed; or <em>false</em> if none fo
</ul>
</div>
<div class="section" id="internal-api">
<h3><a class="toc-backref" href="#id22">Internal API</a></h3>
<h3><a class="toc-backref" href="#id26">Internal API</a></h3>
<p>These functions are intended for the use by dfhack developers,
and are only documented here for completeness:</p>
<ul>
@ -1329,8 +1398,8 @@ global environment, persistent between calls to the script.</p>
<li><p class="first"><tt class="docutils literal">dfhack.internal.getVTable(name)</tt></p>
<p>Returns the pre-extracted vtable address <tt class="docutils literal">name</tt>, or <em>nil</em>.</p>
</li>
<li><p class="first"><tt class="docutils literal">dfhack.internal.getBase()</tt></p>
<p>Returns the base address of the process.</p>
<li><p class="first"><tt class="docutils literal">dfhack.internal.getRebaseDelta()</tt></p>
<p>Returns the ASLR rebase offset of the DF executable.</p>
</li>
<li><p class="first"><tt class="docutils literal">dfhack.internal.getMemRanges()</tt></p>
<p>Returns a sequence of tables describing virtual memory ranges of the process.</p>
@ -1355,7 +1424,7 @@ Returns: <em>found_index</em>, or <em>nil</em> if end reached.</p>
</div>
</div>
<div class="section" id="core-interpreter-context">
<h2><a class="toc-backref" href="#id23">Core interpreter context</a></h2>
<h2><a class="toc-backref" href="#id27">Core interpreter context</a></h2>
<p>While plugins can create any number of interpreter instances,
there is one special context managed by dfhack core. It is the
only context that can receive events from DF and plugins.</p>
@ -1386,7 +1455,7 @@ Using <tt class="docutils literal">timeout_active(id,nil)</tt> cancels the timer
</li>
</ul>
<div class="section" id="event-type">
<h3><a class="toc-backref" href="#id24">Event type</a></h3>
<h3><a class="toc-backref" href="#id28">Event type</a></h3>
<p>An event is just a lua table with a predefined metatable that
contains a __call metamethod. When it is invoked, it loops
through the table with next and calls all contained values.
@ -1411,15 +1480,45 @@ 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>
<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>
<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>
<pre class="literal-block">
local _ENV = mkmodule('foo')
...
return _ENV
</pre>
<p>If called the second time, returns the same table; thus providing reload support.</p>
</li>
<li><p class="first"><tt class="docutils literal">reload(name)</tt></p>
<p>Reloads a previously <tt class="docutils literal">require</tt>-d module <em>&quot;name&quot;</em> from the file.
Intended as a help for module development.</p>
</li>
<li><p class="first"><tt class="docutils literal">dfhack.BASE_G</tt></p>
<p>This variable contains the root global environment table, which is
used as a base for all module and script environments. Its contents
should be kept limited to the standard Lua library and API described
in this document.</p>
</li>
</ul>
</div>
<div class="section" id="plugins">
<h1><a class="toc-backref" href="#id25">Plugins</a></h1>
<h1><a class="toc-backref" href="#id30">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="#id26">burrows</a></h2>
<h2><a class="toc-backref" href="#id31">burrows</a></h2>
<p>Implements extended burrow manipulations.</p>
<p>Events:</p>
<ul>
@ -1457,11 +1556,41 @@ 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="#id27">sort</a></h2>
<h2><a class="toc-backref" href="#id32">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>
<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><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.
When calling such scripts, always use '/' as the separator for
directories, e.g. <tt class="docutils literal"><span class="pre">devel/lua-example</span></tt>.</p>
<p>Scripts are re-read from disk every time they are used
(this may be changed later to check the file change time); however
the global variable values persist in memory between calls.
Every script gets its own separate environment for global
variables.</p>
<p>Arguments are passed in to the scripts via the <strong>...</strong> built-in
quasi-variable; when the script is called by the DFHack core,
they are all guaranteed to be non-nil strings.</p>
<p>DFHack core invokes the scripts in the <em>core context</em> (see above);
however it is possible to call them from any lua code (including
from other scripts) in any context, via the same function the core uses:</p>
<ul>
<li><p class="first"><tt class="docutils literal"><span class="pre">dfhack.run_script(name[,args...])</span></tt></p>
<p>Run a lua script in hack/scripts/, as if it was started from dfhack command-line.
The <tt class="docutils literal">name</tt> argument should be the name stem, as would be used on the command line.</p>
</li>
</ul>
<p>Note that this function lets errors propagate to the caller.</p>
</div>
</div>
</body>
</html>

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

@ -1350,7 +1350,12 @@ produce undesirable results. There are a few good ones though.</p>
<p class="last">You are in fort game mode, managing your fortress and paused.
You switch to the arena game mode, <em>assume control of a creature</em> and then
switch to adventure game mode(1).
You just lost a fortress and gained an adventurer.</p>
You just lost a fortress and gained an adventurer.
You could also do this.
You are in fort game mode, managing your fortress and paused at the esc menu.
You switch to the adventure game mode, then use Dfusion to <em>assume control of a creature</em> and then
save or retire.
You just created a returnable mountain home and gained an adventurer.</p>
</div>
<p>I take no responsibility of anything that happens as a result of using this tool</p>
</div>

@ -1 +1 @@
Subproject commit 651c722295da233ca8d918e298ed226cc0e6c9b9
Subproject commit 3808a8ac4fc1bbc0422492cb042099c47a312b58

@ -8,8 +8,8 @@ IF(UNIX)
OPTION(CONSOLE_NO_CATCH "Make the console not catch 'CTRL+C' events for easier debugging." OFF)
ENDIF()
include_directories (include)
include_directories (proto)
include_directories (include)
SET(PERL_EXECUTABLE "perl" CACHE FILEPATH "This is the perl executable to run in the codegen step. Tweak it if you need to run a specific one.")

@ -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,31 +281,34 @@ 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)
{
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(rbcmd.c_str());
}
command_result Core::runCommand(color_ostream &out, const std::string &command)
{
//fprintf(stderr,"Inside runCommand");
//fprintf(stderr," with command %s\n",command.c_str());
if (!command.empty())
{
//fprintf(stderr,"Command is not empty, tokenizing\n");
vector <string> parts;
Core::cheap_tokenise(command,parts);
//fprintf(stderr,"Tokenized, got %d parts\n",parts.size());
if(parts.size() == 0)
return CR_NOT_IMPLEMENTED;
string first = parts[0];
//fprintf(stderr,"Erasing beginning\n");
parts.erase(parts.begin());
//fprintf(stderr,"I think we're about there\n");
if (first[0] == '#')
return CR_OK;
cerr << "Invoking: " << command << endl;
//fprintf(stderr,"Returning with the next recursion\n");
return runCommand(out, first, parts);
}
else
@ -357,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;
}
@ -508,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");
@ -613,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(plug_mgr, first, parts);
else
con.printerr("%s is not a recognized command.\n", first.c_str());
}
@ -680,7 +697,7 @@ void fIOthread(void * iodata)
{
string command = "";
int ret = con.lineedit("[DFHack]# ",command, main_history);
//fprintf(stderr,"Command: [%s]\n",command.c_str());
fprintf(stderr,"Command: [%s]\n",command.c_str());
if(ret == -2)
{
cerr << "Console is shutting down properly." << endl;
@ -694,13 +711,13 @@ void fIOthread(void * iodata)
else if(ret)
{
// a proper, non-empty command was entered
//fprintf(stderr,"Adding command to history\n");
fprintf(stderr,"Adding command to history\n");
main_history.add(command);
//fprintf(stderr,"Saving history\n");
fprintf(stderr,"Saving history\n");
main_history.save("dfhack.history");
}
//fprintf(stderr,"Running command\n");
fprintf(stderr,"Running command\n");
auto rv = core->runCommand(con, command);
@ -1207,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;
@ -1355,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;
}

@ -38,11 +38,11 @@ distribution.
#include <string>
#include <map>
/*typedef struct interpose_s
typedef struct interpose_s
{
void *new_func;
void *orig_func;
} interpose_t;*/
} interpose_t;
#include "DFHack.h"
#include "Core.h"
@ -58,11 +58,18 @@ distribution.
};*/
#define DYLD_INTERPOSE(_replacment,_replacee) __attribute__((used)) static struct{ const void* replacment; const void* replacee; } _interpose_##_replacee __attribute__ ((section ("__DATA,__interpose"))) = { (const void*)(unsigned long)&_replacment, (const void*)(unsigned long)&_replacee };
DYLD_INTERPOSE(DFH_SDL_Init,SDL_Init);
DYLD_INTERPOSE(DFH_SDL_PollEvent,SDL_PollEvent);
DYLD_INTERPOSE(DFH_SDL_Quit,SDL_Quit);
DYLD_INTERPOSE(DFH_SDL_NumJoysticks,SDL_NumJoysticks);
/*******************************************************************************
* SDL part starts here *
*******************************************************************************/
// hook - called for each game tick (or more often)
DFhackCExport int SDL_NumJoysticks(void)
DFhackCExport int DFH_SDL_NumJoysticks(void)
{
DFHack::Core & c = DFHack::Core::getInstance();
return c.Update();
@ -70,7 +77,7 @@ DFhackCExport int SDL_NumJoysticks(void)
// hook - called at program exit
static void (*_SDL_Quit)(void) = 0;
DFhackCExport void SDL_Quit(void)
DFhackCExport void DFH_SDL_Quit(void)
{
DFHack::Core & c = DFHack::Core::getInstance();
c.Shutdown();
@ -79,16 +86,16 @@ DFhackCExport void SDL_Quit(void)
_SDL_Quit();
}*/
_SDL_Quit();
SDL_Quit();
}
// called by DF to check input events
static int (*_SDL_PollEvent)(SDL::Event* event) = 0;
DFhackCExport int SDL_PollEvent(SDL::Event* event)
DFhackCExport int DFH_SDL_PollEvent(SDL::Event* event)
{
pollevent_again:
// if SDL returns 0 here, it means there are no more events. return 0
int orig_return = _SDL_PollEvent(event);
int orig_return = SDL_PollEvent(event);
if(!orig_return)
return 0;
// otherwise we have an event to filter
@ -128,7 +135,7 @@ DFhackCExport int wgetch(WINDOW *win)
// hook - called at program start, initialize some stuffs we'll use later
static int (*_SDL_Init)(uint32_t flags) = 0;
DFhackCExport int SDL_Init(uint32_t flags)
DFhackCExport int DFH_SDL_Init(uint32_t flags)
{
// reroute stderr
fprintf(stderr,"dfhack: attempting to hook in\n");
@ -158,6 +165,7 @@ DFhackCExport int SDL_Init(uint32_t flags)
DFHack::Core & c = DFHack::Core::getInstance();
//c.Init();
int ret = _SDL_Init(flags);
//int ret = _SDL_Init(flags);
int ret = SDL_Init(flags);
return ret;
}

@ -1036,10 +1036,10 @@ static void *checkaddr(lua_State *L, int idx, bool allow_null = false)
return rv;
}
static uint32_t getBase() { return Core::getInstance().p->getBase(); }
static int getRebaseDelta() { return Core::getInstance().vinfo->getRebaseDelta(); }
static const LuaWrapper::FunctionReg dfhack_internal_module[] = {
WRAP(getBase),
WRAP(getRebaseDelta),
{ NULL, NULL }
};
@ -1074,8 +1074,9 @@ static int internal_setAddress(lua_State *L)
}
// Print via printerr, so that it is definitely logged to stderr.log.
std::string msg = stl_sprintf("<global-address name='%s' value='0x%x'/>", name.c_str(), addr);
dfhack_printerr(L, msg);
uint32_t iaddr = addr - Core::getInstance().vinfo->getRebaseDelta();
fprintf(stderr, "Setting global '%s' to %x (%x)\n", name.c_str(), addr, iaddr);
fflush(stderr);
return 1;
}

@ -256,8 +256,11 @@ static int lua_dfhack_color(lua_State *S)
luaL_argerror(S, 1, "invalid color value");
color_ostream *out = Lua::GetOutput(S);
if (out)
if (out) {
lua_pushinteger(S, (int)out->color());
out->color(color_ostream::color_value(cv));
return 1;
}
return 0;
}
@ -423,10 +426,12 @@ static bool convert_to_exception(lua_State *L, int slevel, lua_State *thread = N
// Create a new exception for this thread
lua_newtable(L);
luaL_where(L, 1);
luaL_where(L, slevel);
lua_setfield(L, -2, "where");
lua_pushstring(L, "coroutine resume failed");
lua_concat(L, 2);
lua_setfield(L, -2, "message");
lua_getfield(L, -2, "verbose");
lua_setfield(L, -2, "verbose");
lua_swap(L);
lua_setfield(L, -2, "cause");
}
@ -480,12 +485,57 @@ static int dfhack_onerror(lua_State *L)
return 1;
}
static int dfhack_error(lua_State *L)
{
luaL_checkany(L, 1);
lua_settop(L, 3);
int level = std::max(1, luaL_optint(L, 2, 1));
lua_pushvalue(L, 1);
if (convert_to_exception(L, level))
{
luaL_where(L, level);
lua_setfield(L, -2, "where");
if (!lua_isnil(L, 3))
{
lua_pushvalue(L, 3);
lua_setfield(L, -2, "verbose");
}
}
return lua_error(L);
}
static int dfhack_exception_tostring(lua_State *L)
{
luaL_checktype(L, 1, LUA_TTABLE);
lua_settop(L, 2);
if (lua_isnil(L, 2))
{
lua_rawgetp(L, LUA_REGISTRYINDEX, &DFHACK_EXCEPTION_META_TOKEN);
lua_getfield(L, -1, "verbose");
lua_insert(L, 2);
lua_settop(L, 2);
}
lua_getfield(L, 1, "verbose");
bool verbose =
lua_toboolean(L, 2) || lua_toboolean(L, 3) ||
(lua_isnil(L, 2) && lua_isnil(L, 3));
int base = lua_gettop(L);
if (verbose || lua_isnil(L, 3))
{
lua_getfield(L, 1, "where");
if (!lua_isstring(L, -1))
lua_pop(L, 1);
}
lua_getfield(L, 1, "message");
if (!lua_isstring(L, -1))
{
@ -493,15 +543,26 @@ static int dfhack_exception_tostring(lua_State *L)
lua_pushstring(L, "(error message is not a string)");
}
lua_pushstring(L, "\n");
lua_getfield(L, 1, "stacktrace");
if (!lua_isstring(L, -1))
lua_pop(L, 2);
if (verbose)
{
lua_pushstring(L, "\n");
lua_getfield(L, 1, "stacktrace");
if (!lua_isstring(L, -1))
lua_pop(L, 2);
}
lua_pushstring(L, "\ncaused by:\n");
lua_getfield(L, 1, "cause");
if (lua_isnil(L, -1))
lua_pop(L, 2);
else if (lua_istable(L, -1))
{
lua_pushcfunction(L, dfhack_exception_tostring);
lua_swap(L);
lua_pushvalue(L, 2);
if (lua_pcall(L, 2, 1, 0) != LUA_OK)
error_tostring(L);
}
else
error_tostring(L);
@ -652,7 +713,12 @@ static int dfhack_coauxwrap (lua_State *L) {
if (Lua::IsSuccess(r))
return lua_gettop(L);
else
{
if (lua_checkstack(L, LUA_MINSTACK))
convert_to_exception(L, 1);
return lua_error(L);
}
}
static int dfhack_cowrap (lua_State *L) {
@ -1159,6 +1225,7 @@ static const luaL_Reg dfhack_funcs[] = {
{ "safecall", dfhack_safecall },
{ "saferesume", dfhack_saferesume },
{ "onerror", dfhack_onerror },
{ "error", dfhack_error },
{ "call_with_finalizer", dfhack_call_with_finalizer },
{ "with_suspend", lua_dfhack_with_suspend },
{ "open_plugin", dfhack_open_plugin },
@ -1359,6 +1426,8 @@ lua_State *DFHack::Lua::Open(color_ostream &out, lua_State *state)
lua_newtable(state);
lua_pushcfunction(state, dfhack_exception_tostring);
lua_setfield(state, -2, "__tostring");
lua_pushcfunction(state, dfhack_exception_tostring);
lua_setfield(state, -2, "tostring");
lua_dup(state);
lua_rawsetp(state, LUA_REGISTRYINDEX, &DFHACK_EXCEPTION_META_TOKEN);
lua_setfield(state, -2, "exception");

@ -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 (*)(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();
}
}

@ -233,19 +233,55 @@ struct HeapBlock
ULONG reserved;
};
*/
// FIXME: NEEDS TESTING!
// FIXME: <warmist> i noticed that if you enumerate it twice, second time it returns wrong .text region size
static void GetDosNames(std::map<string, string> &table)
{
// Partially based on example from msdn:
// Translate path with device name to drive letters.
TCHAR szTemp[512];
szTemp[0] = '\0';
if (GetLogicalDriveStrings(sizeof(szTemp)-1, szTemp))
{
TCHAR szName[MAX_PATH];
TCHAR szDrive[3] = " :";
BOOL bFound = FALSE;
TCHAR* p = szTemp;
do
{
// Copy the drive letter to the template string
*szDrive = *p;
// Look up each device name
if (QueryDosDevice(szDrive, szName, MAX_PATH))
table[szName] = szDrive;
// Go to the next NULL character.
while (*p++);
} while (*p); // end of string
}
}
void Process::getMemRanges( vector<t_memrange> & ranges )
{
MEMORY_BASIC_INFORMATION MBI;
//map<char *, unsigned int> heaps;
uint64_t movingStart = 0;
PVOID LastAllocationBase = 0;
map <char *, string> nameMap;
map <string,string> dosDrives;
// get page size
SYSTEM_INFO si;
GetSystemInfo(&si);
uint64_t PageSize = si.dwPageSize;
// get dos drive names
GetDosNames(dosDrives);
ranges.clear();
// enumerate heaps
// HeapNodes(d->my_pid, heaps);
// go through all the VM regions, convert them to our internal format
@ -254,52 +290,106 @@ void Process::getMemRanges( vector<t_memrange> & ranges )
movingStart = ((uint64_t)MBI.BaseAddress + MBI.RegionSize);
if(movingStart % PageSize != 0)
movingStart = (movingStart / PageSize + 1) * PageSize;
// skip empty regions and regions we share with other processes (DLLs)
if( !(MBI.State & MEM_COMMIT) /*|| !(MBI.Type & MEM_PRIVATE)*/ )
// Skip unallocated address space
if (MBI.State & MEM_FREE)
continue;
// Find range and permissions
t_memrange temp;
memset(&temp, 0, sizeof(temp));
temp.start = (char *) MBI.BaseAddress;
temp.end = ((char *)MBI.BaseAddress + (uint64_t)MBI.RegionSize);
temp.read = MBI.Protect & PAGE_EXECUTE_READ || MBI.Protect & PAGE_EXECUTE_READWRITE || MBI.Protect & PAGE_READONLY || MBI.Protect & PAGE_READWRITE;
temp.write = MBI.Protect & PAGE_EXECUTE_READWRITE || MBI.Protect & PAGE_READWRITE;
temp.execute = MBI.Protect & PAGE_EXECUTE_READ || MBI.Protect & PAGE_EXECUTE_READWRITE || MBI.Protect & PAGE_EXECUTE;
temp.valid = true;
if(!GetModuleBaseName(d->my_handle, (HMODULE) temp.start, temp.name, 1024))
temp.valid = true;
if (!(MBI.State & MEM_COMMIT))
temp.valid = false; // reserved address space
else if (MBI.Protect & PAGE_EXECUTE)
temp.execute = true;
else if (MBI.Protect & PAGE_EXECUTE_READ)
temp.execute = temp.read = true;
else if (MBI.Protect & PAGE_EXECUTE_READWRITE)
temp.execute = temp.read = temp.write = true;
else if (MBI.Protect & PAGE_EXECUTE_WRITECOPY)
temp.execute = temp.read = temp.write = true;
else if (MBI.Protect & PAGE_READONLY)
temp.read = true;
else if (MBI.Protect & PAGE_READWRITE)
temp.read = temp.write = true;
else if (MBI.Protect & PAGE_WRITECOPY)
temp.read = temp.write = true;
// Merge areas with the same properties
if (!ranges.empty() && LastAllocationBase == MBI.AllocationBase)
{
if(nameMap.count((char *)temp.start))
auto &last = ranges.back();
if (last.end == temp.start &&
last.valid == temp.valid && last.execute == temp.execute &&
last.read == temp.read && last.write == temp.write)
{
// potential buffer overflow...
strcpy(temp.name, nameMap[(char *)temp.start].c_str());
last.end = temp.end;
continue;
}
else
}
#if 1
// Find the mapped file name
if (GetMappedFileName(d->my_handle, temp.start, temp.name, 1024))
{
int vsize = strlen(temp.name);
// Translate NT name to DOS name
for (auto it = dosDrives.begin(); it != dosDrives.end(); ++it)
{
// filter away shared segments without a name.
if( !(MBI.Type & MEM_PRIVATE) )
int ksize = it->first.size();
if (strncmp(temp.name, it->first.data(), ksize) != 0)
continue;
else
temp.name[0]=0;
memcpy(temp.name, it->second.data(), it->second.size());
memmove(temp.name + it->second.size(), temp.name + ksize, vsize + 1 - ksize);
break;
}
}
else
temp.name[0] = 0;
#else
// Find the executable name
char *base = (char*)MBI.AllocationBase;
if(nameMap.count(base))
{
strncpy(temp.name, nameMap[base].c_str(), 1023);
}
else if(GetModuleBaseName(d->my_handle, (HMODULE)base, temp.name, 1024))
{
std::string nm(temp.name);
nameMap[base] = nm;
// this is our executable! (could be generalized to pull segments from libs, but whatever)
if(d->base == temp.start)
if(d->base == base)
{
for(int i = 0; i < d->pe_header.FileHeader.NumberOfSections; i++)
{
char sectionName[9];
/*char sectionName[9];
memcpy(sectionName,d->sections[i].Name,8);
sectionName[8] = 0;
string nm;
nm.append(temp.name);
nm.append(" : ");
nm.append(sectionName);
nameMap[(char *)temp.start + d->sections[i].VirtualAddress] = nm;
nm.append(sectionName);*/
nameMap[base + d->sections[i].VirtualAddress] = nm;
}
}
else
continue;
}
else
temp.name[0] = 0;
#endif
// Push the entry
LastAllocationBase = MBI.AllocationBase;
ranges.push_back(temp);
}
}

@ -111,6 +111,8 @@ namespace DFHack
void printerr(const char *format, ...);
void vprinterr(const char *format, va_list args);
/// Get color
color_value color() { return cur_color; }
/// Set color (ANSI color number)
void color(color_value c);
/// Reset color to default

@ -83,10 +83,17 @@ namespace DFHack
// Better than tracking some weird variables all over the place.
class DFHACK_EXPORT Core
{
#ifdef _DARWIN
friend int ::DFH_SDL_NumJoysticks(void);
friend void ::DFH_SDL_Quit(void);
friend int ::DFH_SDL_PollEvent(SDL::Event *);
friend int ::DFH_SDL_Init(uint32_t flags);
#else
friend int ::SDL_NumJoysticks(void);
friend void ::SDL_Quit(void);
friend int ::SDL_PollEvent(SDL::Event *);
friend int ::SDL_Init(uint32_t flags);
#endif
friend int ::wgetch(WINDOW * w);
friend int ::egg_init(void);
friend int ::egg_shutdown(void);

@ -44,6 +44,12 @@ namespace SDL
// these functions are here because they call into DFHack::Core and therefore need to
// be declared as friend functions/known
#ifdef _DARWIN
DFhackCExport int DFH_SDL_NumJoysticks(void);
DFhackCExport void DFH_SDL_Quit(void);
DFhackCExport int DFH_SDL_PollEvent(SDL::Event* event);
DFhackCExport int DFH_SDL_Init(uint32_t flags);
#endif
DFhackCExport int SDL_NumJoysticks(void);
DFhackCExport void SDL_Quit(void);
DFhackCExport int SDL_PollEvent(SDL::Event* event);

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

@ -49,6 +49,10 @@ function dfhack.pcall(f, ...)
return xpcall(f, dfhack.onerror, ...)
end
function qerror(msg, level)
dfhack.error(msg, (level or 1) + 1, false)
end
function dfhack.with_finalize(...)
return dfhack.call_with_finalizer(0,true,...)
end
@ -64,6 +68,8 @@ function dfhack.with_temp_object(obj,fn,...)
return dfhack.call_with_finalizer(1,true,call_delete,obj,fn,obj,...)
end
dfhack.exception.__index = dfhack.exception
-- Module loading
function mkmodule(module,env)

@ -154,7 +154,8 @@ function MemoryArea.new(astart, aend)
int16_t = CheckedArray.new('int16_t',astart,aend),
uint16_t = CheckedArray.new('uint16_t',astart,aend),
int32_t = CheckedArray.new('int32_t',astart,aend),
uint32_t = CheckedArray.new('uint32_t',astart,aend)
uint32_t = CheckedArray.new('uint32_t',astart,aend),
float = CheckedArray.new('float',astart,aend)
}
setmetatable(obj, MemoryArea)
return obj
@ -168,7 +169,7 @@ function MemoryArea:__tostring()
return string.format('<MemoryArea: %x..%x>', self.start_addr, self.end_addr)
end
function MemoryArea:contains_range(start,size)
return start >= self.start_addr and (start+size) <= self.end_addr
return size >= 0 and start >= self.start_addr and (start+size) <= self.end_addr
end
function MemoryArea:contains_obj(obj,count)
local size, base = df.sizeof(obj)
@ -208,7 +209,8 @@ local function find_data_segment()
end
elseif mem.read and mem.write
and (string.match(mem.name,'/dwarfort%.exe$')
or string.match(mem.name,'/Dwarf_Fortress$'))
or string.match(mem.name,'/Dwarf_Fortress$')
or string.match(mem.name,'Dwarf Fortress%.exe'))
then
data_start = mem.start_addr
data_end = mem.end_addr
@ -233,7 +235,7 @@ function found_offset(name,val)
if not val then
print('Could not find offset '..name)
if not cval and not utils.prompt_yes_no('Continue with the script?') then
error('User quit')
qerror('User quit')
end
return
end
@ -250,6 +252,16 @@ function found_offset(name,val)
end
else
dfhack.internal.setAddress(name, val)
local ival = val - dfhack.internal.getRebaseDelta()
local entry = string.format("<global-address name='%s' value='0x%x'/>\n", name, ival)
local ccolor = dfhack.color(COLOR_LIGHTGREEN)
dfhack.print(entry)
dfhack.color(ccolor)
io.stdout:write(entry)
io.stdout:flush()
end
end
@ -452,4 +464,30 @@ function DiffSearcher:find_counter(prompt,data_type,delta,action_prompt)
)
end
-- Screen size
function get_screen_size()
-- Use already known globals
if dfhack.internal.getAddress('init') then
local d = df.global.init.display
return d.grid_x, d.grid_y
end
if dfhack.internal.getAddress('gps') then
local g = df.global.gps
return g.dimx, g.dimy
end
-- Parse stdout.log for resize notifications
io.stdout:flush()
local w,h = 80,25
for line in io.lines('stdout.log') do
local cw, ch = string.match(line, '^Resizing grid to (%d+)x(%d+)$')
if cw and ch then
w, h = tonumber(cw), tonumber(ch)
end
end
return w,h
end
return _ENV

@ -379,7 +379,7 @@ function prompt_yes_no(msg,default)
elseif string.match(rv,'^[Nn]') then
return false
elseif rv == 'abort' then
error('User abort in utils.prompt_yes_no()')
qerror('User abort in utils.prompt_yes_no()')
elseif rv == '' and default ~= nil then
return default
end

@ -10,7 +10,7 @@ if(BUILD_DFUSION)
add_subdirectory (Dfusion)
endif()
OPTION(BUILD_STONESENSE "Build stonesense (needs a checkout first)." OFF)
OPTION(BUILD_STONESENSE "Build stonesense (needs a checkout first)." ON)
if(BUILD_STONESENSE)
add_subdirectory (stonesense)
endif()
@ -36,7 +36,7 @@ if (BUILD_DWARFEXPORT)
add_subdirectory (dwarfexport)
endif()
OPTION(BUILD_RUBY "Build ruby binding." OFF)
OPTION(BUILD_RUBY "Build ruby binding." ON)
if (BUILD_RUBY)
add_subdirectory (ruby)
endif()

@ -7,8 +7,8 @@ ENDIF()
include_directories("${dfhack_SOURCE_DIR}/library/include")
include_directories("${dfhack_SOURCE_DIR}/library/proto")
include_directories("${dfhack_SOURCE_DIR}/library/depends/xgetopt")
include_directories("${CMAKE_CURRENT_SOURCE_DIR}/proto")
include_directories("${dfhack_SOURCE_DIR}/library/depends/xgetopt")
MACRO(CAR var)
SET(${var} ${ARGV1})

@ -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,185 @@ 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.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').
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' }
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 +191,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 +212,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,265 @@
module DFHack
class << self
# 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

@ -13,6 +13,7 @@ if ($^O =~ /linux/i) {
} 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,80 @@ 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);
render_field_refto($parent, $name, $tgvec);
}
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);
$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 ; $name.map { |i| ${tgvec}[i] } ; end";
}
sub render_class_vmethods {
my ($vms) = @_;
my $voff = 0;
@ -687,6 +764,7 @@ sub render_item_number {
my $initvalue = $item->getAttribute('init-value');
my $typename = $item->getAttribute('type-name');
undef $typename if ($meta and $meta eq 'bitfield-type');
my $g = $global_types{$typename} if ($typename);
$typename = rb_ucase($typename) if $typename;
$typename = $classname if (!$typename and $subtype and $subtype eq 'enum'); # compound enum
@ -695,6 +773,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 = 'int32_t' if (!$subtype);
if ($subtype eq 'int64_t') {
@ -713,7 +792,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,21 @@
module DFHack
class << self
# return an Item
# arg similar to unit.rb/unit_find; no arg = 'k' menu
def item_find(what=:selected)
if what == :selected
case ui.main.mode
when :LookAround
k = ui_look_list.items[ui_look_cursor]
k.item if k.type == :Item
end
elsif what.kind_of?(Integer)
world.items.all.binsearch(what)
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,54 @@
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
# 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
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,752 @@
# 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.instance_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
@_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

File diff suppressed because it is too large Load Diff

@ -21,7 +21,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,13 +30,12 @@ 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 const char *r_command;
static command_result r_result;
static tthread::thread *r_thread;
static int onupdate_active;
@ -45,31 +43,39 @@ 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 +85,86 @@ 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(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;
// 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;
// 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
// TODO dynamic check on df::cur_year{_tick}
if (!onupdate_active)
return CR_OK;
return plugin_eval_rb("DFHack.onupdate");
return plugin_eval_ruby("DFHack.onupdate");
}
DFhackCExport command_result plugin_onstatechange ( color_ostream &out, state_change_event e)
@ -159,26 +181,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(cmd.c_str());
}
static command_result df_rubyeval(color_ostream &out, std::vector <std::string> & parameters)
@ -191,6 +200,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 +209,21 @@ static command_result df_rubyeval(color_ostream &out, std::vector <std::string>
full += " ";
}
return plugin_eval_rb(full);
return plugin_eval_ruby(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 +235,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 +262,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 +276,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 +284,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);
@ -358,12 +356,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 +393,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;
@ -419,42 +426,6 @@ static VALUE rb_dfonupdateactiveset(VALUE self, VALUE val)
return Qtrue;
}
static VALUE rb_dfresume(VALUE self)
{
Core::getInstance().Resume();
return Qtrue;
}
static VALUE rb_dfsuspend(VALUE self)
{
Core::getInstance().Suspend();
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)
{
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);
}
static VALUE rb_dfprint_str(VALUE self, VALUE s)
{
console_proxy->print("%s", rb_string_value_ptr(&s));
@ -467,24 +438,6 @@ static VALUE rb_dfprint_err(VALUE self, VALUE 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)
{
rb_raise(*rb_eRuntimeError, "not implemented");
}
static VALUE rb_dfget_global_address(VALUE self, VALUE name)
{
return rb_uint2inum(Core::getInstance().vinfo->getAddress(rb_string_value_ptr(&name)));
@ -510,7 +463,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
}
@ -531,7 +484,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);
}
@ -760,17 +713,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
__thiscall
#endif
__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);
char **that = (char**)rb_num2ulong(cppobj);
int ret;
fptr = (decltype(fptr))*(void**)(*that + rb_num2ulong(cppvoff));
ret = fptr(that, rb_num2ulong(a0), rb_num2ulong(a1), rb_num2ulong(a2), rb_num2ulong(a3));
return rb_int2inum(ret);
fptr = (decltype(fptr))*(void**)(*that + voff);
return fptr(that, a0, a1, a2, a3);
}
#endif
// 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)));
}
@ -779,22 +764,17 @@ 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, "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);
@ -833,10 +813,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;
rb_load_protect(rb_str_new2("./hack/ruby.rb"), Qfalse, &state);
if (state)
dump_rb_error();
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,65 @@
# df user-interface related methods
module DFHack
class << self
# 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,78 @@
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)
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
end
elsif what.kind_of?(Integer)
world.units.all.binsearch(what)
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
end

@ -5,6 +5,8 @@ local ms = require 'memscan'
local is_known = dfhack.internal.getAddress
local os_type = dfhack.getOSType()
local force_scan = {}
for _,v in ipairs({...}) do
force_scan[v] = true
@ -20,10 +22,17 @@ MAKE IT RUN CORRECTLY if any data structures
changed, thus possibly leading to CRASHES AND/OR
PERMANENT SAVE CORRUPTION.
This script should be initially started immediately
after loading the game, WITHOUT first loading a world.
It expects vanilla game configuration, without any
custom tilesets or init file changes.
Finding the first few globals requires this script to be
started immediately after loading the game, WITHOUT
first loading a world. The rest expect a loaded save,
not a fresh embark. Finding current_weather requires
a special save previously processed with devel/prepare-save
on a DF version with working dfhack.
The script expects vanilla game configuration, without
any custom tilesets or init file changes. Never unpause
the game unless instructed. When done, quit the game
without saving using 'die'.
]]
if not utils.prompt_yes_no('Proceed?') then
@ -34,12 +43,12 @@ end
local data = ms.get_data_segment()
if not data then
error('Could not find data segment')
qerror('Could not find data segment')
end
print('\nData section: '..tostring(data))
if data.size < 5000000 then
error('Data segment too short.')
qerror('Data segment too short.')
end
local searcher = ms.DiffSearcher.new(data)
@ -52,6 +61,34 @@ local function validate_offset(name,validator,addr,tname,...)
ms.found_offset(name,obj)
end
local function zoomed_searcher(startn, end_or_sz)
if force_scan.nozoom then
return nil
end
local sv = is_known(startn)
if not sv then
return nil
end
local ev
if type(end_or_sz) == 'number' then
ev = sv + end_or_sz
if end_or_sz < 0 then
sv, ev = ev, sv
end
else
ev = is_known(end_or_sz)
if not ev then
return nil
end
end
sv = sv - (sv % 4)
ev = ev + 3
ev = ev - (ev % 4)
if data:contains_range(sv, ev-sv) then
return ms.DiffSearcher.new(ms.MemoryArea.new(sv,ev))
end
end
local function exec_finder(finder, names)
if type(names) ~= 'table' then
names = { names }
@ -66,7 +103,7 @@ local function exec_finder(finder, names)
if not dfhack.safecall(finder) then
if not utils.prompt_yes_no('Proceed with the rest of the script?') then
searcher:reset()
error('Quit')
qerror('Quit')
end
end
else
@ -76,7 +113,8 @@ end
local ordinal_names = {
[0] = '1st entry',
[1] = '2nd entry'
[1] = '2nd entry',
[2] = '3rd entry'
}
setmetatable(ordinal_names, {
__index = function(self,idx) return (idx+1)..'th entry' end
@ -140,8 +178,6 @@ local function find_cursor()
return false
end
exec_finder(find_cursor, { 'cursor', 'selection_rect', 'gamemode', 'gametype' })
--
-- Announcements
--
@ -158,8 +194,6 @@ local function find_announcements()
dfhack.printerr('Could not find announcements.')
end
exec_finder(find_announcements, 'announcements')
--
-- d_init
--
@ -198,8 +232,6 @@ local function find_d_init()
dfhack.printerr('Could not find d_init')
end
exec_finder(find_d_init, 'd_init')
--
-- gview
--
@ -220,7 +252,82 @@ local function find_gview()
dfhack.printerr('Could not find gview')
end
exec_finder(find_gview, 'gview')
--
-- enabler
--
local function is_valid_enabler(e)
if not ms.is_valid_vector(e.textures.raws, 4)
or not ms.is_valid_vector(e.text_system, 4)
then
dfhack.printerr('Vector layout check failed.')
return false
end
return true
end
local function find_enabler()
-- Data from data/init/colors.txt
local colors = {
0, 0, 0, 0, 0, 128, 0, 128, 0,
0, 128, 128, 128, 0, 0, 128, 0, 128,
128, 128, 0, 192, 192, 192, 128, 128, 128,
0, 0, 255, 0, 255, 0, 0, 255, 255,
255, 0, 0, 255, 0, 255, 255, 255, 0,
255, 255, 255
}
for i = 1,#colors do colors[i] = colors[i]/255 end
local idx, addr = data.float:find_one(colors)
if idx then
validate_offset('enabler', is_valid_enabler, addr, df.enabler, 'ccolor')
return
end
dfhack.printerr('Could not find enabler')
end
--
-- gps
--
local function is_valid_gps(g)
if g.clipx[0] < 0 or g.clipx[0] > g.clipx[1] or g.clipx[1] >= g.dimx then
dfhack.printerr('Invalid clipx: ', g.clipx[0], g.clipx[1], g.dimx)
end
if g.clipy[0] < 0 or g.clipy[0] > g.clipy[1] or g.clipy[1] >= g.dimy then
dfhack.printerr('Invalid clipy: ', g.clipy[0], g.clipy[1], g.dimy)
end
return true
end
local function find_gps()
print('\nPlease ensure the mouse cursor is not over the game window.')
if not utils.prompt_yes_no('Proceed?', true) then
return
end
local zone
if os_type == 'windows' or os_type == 'linux' then
zone = zoomed_searcher('cursor', 0x1000)
elseif os_type == 'darwin' then
zone = zoomed_searcher('enabler', 0x1000)
end
zone = zone or searcher
local w,h = ms.get_screen_size()
local idx, addr = zone.area.int32_t:find_one{w, h, -1, -1}
if idx then
validate_offset('gps', is_valid_gps, addr, df.graphic, 'dimx')
return
end
dfhack.printerr('Could not find gps')
end
--
-- World
@ -257,8 +364,6 @@ menu, and select different types as instructed below:]],
validate_offset('world', is_valid_world, addr, df.world, 'selected_stockpile_type')
end
exec_finder(find_world, 'world')
--
-- UI
--
@ -291,8 +396,6 @@ menu, and switch modes as instructed below:]],
validate_offset('ui', is_valid_ui, addr, df.ui, 'main', 'mode')
end
exec_finder(find_ui, 'ui')
--
-- ui_sidebar_menus
--
@ -319,9 +422,9 @@ end
local function find_ui_sidebar_menus()
local addr = searcher:find_menu_cursor([[
Searching for ui_sidebar_menus. Please open the add job
ui of Mason, Craftsdwarfs, or Carpenters workshop, and
select entries in the list:]],
Searching for ui_sidebar_menus. Please switch to 'q' mode,
select a Mason, Craftsdwarfs, or Carpenters workshop, open
the Add Job menu, and move the cursor within:]],
'int32_t',
{ 0, 1, 2, 3, 4, 5, 6 },
ordinal_names
@ -330,8 +433,6 @@ select entries in the list:]],
addr, df.ui_sidebar_menus, 'workshop_job', 'cursor')
end
exec_finder(find_ui_sidebar_menus, 'ui_sidebar_menus')
--
-- ui_build_selector
--
@ -366,7 +467,107 @@ number, so when it shows "Min (5000df", it means 50000:]],
addr, df.ui_build_selector, 'plate_info', 'unit_min')
end
exec_finder(find_ui_build_selector, 'ui_build_selector')
--
-- init
--
local function is_valid_init(i)
-- derived from curses_*.png image sizes presumably
if i.font.small_font_dispx ~= 8 or i.font.small_font_dispy ~= 12 or
i.font.large_font_dispx ~= 10 or i.font.large_font_dispy ~= 12 then
print('Unexpected font sizes: ',
i.font.small_font_dispx, i.font.small_font_dispy,
i.font.large_font_dispx, i.font.large_font_dispy)
if not utils.prompt_yes_no('Ignore?') then
return false
end
end
return true
end
local function find_init()
local zone
if os_type == 'windows' then
zone = zoomed_searcher('ui_build_selector', 0x3000)
elseif os_type == 'linux' or os_type == 'darwin' then
zone = zoomed_searcher('d_init', -0x2000)
end
zone = zone or searcher
local idx, addr = zone.area.int32_t:find_one{250, 150, 15, 0}
if idx then
validate_offset('init', is_valid_init, addr, df.init, 'input', 'hold_time')
return
end
local w,h = ms.get_screen_size()
local idx, addr = zone.area.int32_t:find_one{w, h}
if idx then
validate_offset('init', is_valid_init, addr, df.init, 'display', 'grid_x')
return
end
dfhack.printerr('Could not find init')
end
--
-- current_weather
--
local function find_current_weather()
print('\nPlease load the save previously processed with prepare-save.')
if not utils.prompt_yes_no('Proceed?', true) then
return
end
local zone
if os_type == 'windows' then
zone = zoomed_searcher('crime_next_id', 512)
elseif os_type == 'darwin' then
zone = zoomed_searcher('cursor', -64)
elseif os_type == 'linux' then
zone = zoomed_searcher('ui_building_assign_type', -512)
end
zone = zone or searcher
local wbytes = {
2, 1, 0, 2, 0,
1, 2, 1, 0, 0,
2, 0, 2, 1, 2,
1, 2, 0, 1, 1,
2, 0, 1, 0, 2
}
local idx, addr = zone.area.int8_t:find_one(wbytes)
if idx then
ms.found_offset('current_weather', addr)
return
end
dfhack.printerr('Could not find current_weather - must be a wrong save.')
end
--
-- ui_menu_width
--
local function find_ui_menu_width()
local addr = searcher:find_menu_cursor([[
Searching for ui_menu_width. Please exit to the main
dwarfmode menu, then use Tab to do as instructed below:]],
'int8_t',
{ 2, 3, 1 },
{ [2] = 'switch to the most usual [mapmap][menu] layout',
[3] = 'hide the menu completely',
[1] = 'switch to the default [map][menu][map] layout' }
)
ms.found_offset('ui_menu_width', addr)
-- NOTE: Assume that the vars are adjacent, as always
ms.found_offset('ui_area_map_width', addr+1)
end
--
-- ui_selected_unit
@ -395,8 +596,6 @@ into the prompts below:]],
ms.found_offset('ui_selected_unit', addr)
end
exec_finder(find_ui_selected_unit, 'ui_selected_unit')
--
-- ui_unit_view_mode
--
@ -412,8 +611,6 @@ with 'v', switch the pages as requested:]],
ms.found_offset('ui_unit_view_mode', addr)
end
exec_finder(find_ui_unit_view_mode, 'ui_unit_view_mode')
--
-- ui_look_cursor
--
@ -434,8 +631,6 @@ and select list entries as instructed:]],
ms.found_offset('ui_look_cursor', addr)
end
exec_finder(find_ui_look_cursor, 'ui_look_cursor')
--
-- ui_building_item_cursor
--
@ -456,8 +651,6 @@ with many contained items, and select as instructed:]],
ms.found_offset('ui_building_item_cursor', addr)
end
exec_finder(find_ui_building_item_cursor, 'ui_building_item_cursor')
--
-- ui_workshop_in_add
--
@ -468,7 +661,7 @@ Searching for ui_workshop_in_add. Please activate the 'q'
mode, find a workshop without jobs (or delete jobs),
and do as instructed below.
NOTE: After first 3 steps resize the game window.]],
NOTE: If not done after first 3-4 steps, resize the game window.]],
'int8_t',
{ 1, 0 },
{ [1] = 'enter the add job menu',
@ -477,8 +670,6 @@ NOTE: After first 3 steps resize the game window.]],
ms.found_offset('ui_workshop_in_add', addr)
end
exec_finder(find_ui_workshop_in_add, 'ui_workshop_in_add')
--
-- ui_workshop_job_cursor
--
@ -498,8 +689,6 @@ mode, find a workshop with many jobs, and select as instructed:]],
ms.found_offset('ui_workshop_job_cursor', addr)
end
exec_finder(find_ui_workshop_job_cursor, 'ui_workshop_job_cursor')
--
-- ui_building_in_assign
--
@ -510,7 +699,7 @@ Searching for ui_building_in_assign. Please activate
the 'q' mode, select a room building (e.g. a bedroom)
and do as instructed below.
NOTE: After first 3 steps resize the game window.]],
NOTE: If not done after first 3-4 steps, resize the game window.]],
'int8_t',
{ 1, 0 },
{ [1] = 'enter the Assign owner menu',
@ -519,8 +708,6 @@ NOTE: After first 3 steps resize the game window.]],
ms.found_offset('ui_building_in_assign', addr)
end
exec_finder(find_ui_building_in_assign, 'ui_building_in_assign')
--
-- ui_building_in_resize
--
@ -531,7 +718,7 @@ Searching for ui_building_in_resize. Please activate
the 'q' mode, select a room building (e.g. a bedroom)
and do as instructed below.
NOTE: After first 3 steps resize the game window.]],
NOTE: If not done after first 3-4 steps, resize the game window.]],
'int8_t',
{ 1, 0 },
{ [1] = 'enter the Resize room mode',
@ -540,9 +727,6 @@ NOTE: After first 3 steps resize the game window.]],
ms.found_offset('ui_building_in_resize', addr)
end
exec_finder(find_ui_building_in_resize, 'ui_building_in_resize')
--
-- window_x
--
@ -557,8 +741,6 @@ scroll to the LEFT edge, then do as instructed:]],
ms.found_offset('window_x', addr)
end
exec_finder(find_window_x, 'window_x')
--
-- window_y
--
@ -573,8 +755,6 @@ scroll to the TOP edge, then do as instructed:]],
ms.found_offset('window_y', addr)
end
exec_finder(find_window_y, 'window_y')
--
-- window_z
--
@ -582,20 +762,177 @@ exec_finder(find_window_y, 'window_y')
local function find_window_z()
local addr = searcher:find_counter([[
Searching for window_z. Please exit to main dwarfmode menu,
scroll to ground level, then do as instructed below.
scroll to a Z level near surface, then do as instructed below.
NOTE: After first 3 steps resize the game window.]],
NOTE: If not done after first 3-4 steps, resize the game window.]],
'int32_t', -1,
"Please press '>' to scroll one Z level down."
)
ms.found_offset('window_z', addr)
end
exec_finder(find_window_z, 'window_z')
--
-- cur_year
--
local function find_cur_year()
local zone
if os_type == 'windows' then
zone = zoomed_searcher('formation_next_id', 32)
elseif os_type == 'darwin' then
zone = zoomed_searcher('cursor', -32)
elseif os_type == 'linux' then
zone = zoomed_searcher('ui_building_assign_type', -512)
end
if not zone then
dfhack.printerr('Cannot search for cur_year - prerequisites missing.')
return
end
local yvalue = utils.prompt_input('Please enter current in-game year: ', utils.check_number)
local idx, addr = zone.area.int32_t:find_one{yvalue}
if idx then
ms.found_offset('cur_year', addr)
return
end
dfhack.printerr('Could not find cur_year')
end
--
-- cur_year_tick
--
local function find_cur_year_tick()
local zone
if os_type == 'windows' then
zone = zoomed_searcher('artifact_next_id', -32)
else
zone = zoomed_searcher('cur_year', 128)
end
if not zone then
dfhack.printerr('Cannot search for cur_year_tick - prerequisites missing.')
return
end
local addr = zone:find_counter([[
Searching for cur_year_tick. Please exit to main dwarfmode
menu, then do as instructed below:]],
'int32_t', 1,
"Please press '.' to step the game one frame."
)
ms.found_offset('cur_year_tick', addr)
end
--
-- process_jobs
--
local function get_process_zone()
if os_type == 'windows' then
return zoomed_searcher('ui_workshop_job_cursor', 'ui_building_in_resize')
elseif os_type == 'linux' or os_type == 'darwin' then
return zoomed_searcher('cur_year', 'cur_year_tick')
end
end
local function find_process_jobs()
local zone = get_process_zone() or searcher
local addr = zone:find_menu_cursor([[
Searching for process_jobs. Please do as instructed below:]],
'int8_t',
{ 1, 0 },
{ [1] = 'designate a building to be constructed, e.g a bed',
[0] = 'step or unpause the game to reset the flag' }
)
ms.found_offset('process_jobs', addr)
end
--
-- process_dig
--
local function find_process_dig()
local zone = get_process_zone() or searcher
local addr = zone:find_menu_cursor([[
Searching for process_dig. Please do as instructed below:]],
'int8_t',
{ 1, 0 },
{ [1] = 'designate a tile to be mined out',
[0] = 'step or unpause the game to reset the flag' }
)
ms.found_offset('process_dig', addr)
end
--
-- pause_state
--
local function find_pause_state()
local zone
if os_type == 'linux' or os_type == 'darwin' then
zone = zoomed_searcher('ui_look_cursor', 32)
elseif os_type == 'windows' then
zone = zoomed_searcher('ui_workshop_job_cursor', 80)
end
zone = zone or searcher
local addr = zone:find_menu_cursor([[
Searching for pause_state. Please do as instructed below:]],
'int8_t',
{ 1, 0 },
{ [1] = 'PAUSE the game',
[0] = 'UNPAUSE the game' }
)
ms.found_offset('pause_state', addr)
end
--
-- THE END
-- MAIN FLOW
--
print('Done.')
print('\nInitial globals (need title screen):\n')
exec_finder(find_cursor, { 'cursor', 'selection_rect', 'gamemode', 'gametype' })
exec_finder(find_announcements, 'announcements')
exec_finder(find_d_init, 'd_init')
exec_finder(find_gview, 'gview')
exec_finder(find_enabler, 'enabler')
exec_finder(find_gps, 'gps')
print('\nCompound globals (need loaded world):\n')
exec_finder(find_world, 'world')
exec_finder(find_ui, 'ui')
exec_finder(find_ui_sidebar_menus, 'ui_sidebar_menus')
exec_finder(find_ui_build_selector, 'ui_build_selector')
exec_finder(find_init, 'init')
print('\nPrimitive globals:\n')
exec_finder(find_current_weather, 'current_weather')
exec_finder(find_ui_menu_width, { 'ui_menu_width', 'ui_area_map_width' })
exec_finder(find_ui_selected_unit, 'ui_selected_unit')
exec_finder(find_ui_unit_view_mode, 'ui_unit_view_mode')
exec_finder(find_ui_look_cursor, 'ui_look_cursor')
exec_finder(find_ui_building_item_cursor, 'ui_building_item_cursor')
exec_finder(find_ui_workshop_in_add, 'ui_workshop_in_add')
exec_finder(find_ui_workshop_job_cursor, 'ui_workshop_job_cursor')
exec_finder(find_ui_building_in_assign, 'ui_building_in_assign')
exec_finder(find_ui_building_in_resize, 'ui_building_in_resize')
exec_finder(find_window_x, 'window_x')
exec_finder(find_window_y, 'window_y')
exec_finder(find_window_z, 'window_z')
print('\nUnpausing globals:\n')
exec_finder(find_cur_year, 'cur_year')
exec_finder(find_cur_year_tick, 'cur_year_tick')
exec_finder(find_process_jobs, 'process_jobs')
exec_finder(find_process_dig, 'process_dig')
exec_finder(find_pause_state, 'pause_state')
print('\nDone. Now add newly-found globals to symbols.xml.')
searcher:reset()

@ -0,0 +1,71 @@
-- Prepare the current save for use with devel/find-offsets.
df.global.pause_state = true
--[[print('Placing anchor...')
do
local wp = df.global.ui.waypoints
for _,pt in ipairs(wp.points) do
if pt.name == 'dfhack_anchor' then
print('Already placed.')
goto found
end
end
local x,y,z = pos2xyz(df.global.cursor)
if not x then
error("Place cursor at your preferred anchor point.")
end
local id = wp.next_point_id
wp.next_point_id = id + 1
wp.points:insert('#',{
new = true, id = id, name = 'dfhack_anchor',
comment=(x..','..y..','..z),
tile = string.byte('!'), fg_color = COLOR_LIGHTRED, bg_color = COLOR_BLUE,
pos = xyz2pos(x,y,z)
})
::found::
end]]
print('Nicknaming units...')
for i,unit in ipairs(df.global.world.units.active) do
dfhack.units.setNickname(unit, i..':'..unit.id)
end
print('Setting weather...')
local wbytes = {
2, 1, 0, 2, 0,
1, 2, 1, 0, 0,
2, 0, 2, 1, 2,
1, 2, 0, 1, 1,
2, 0, 1, 0, 2
}
for i=0,4 do
for j = 0,4 do
df.global.current_weather[i][j] = (wbytes[i*5+j+1] or 2)
end
end
local yearstr = df.global.cur_year..','..df.global.cur_year_tick
print('Cur year and tick: '..yearstr)
dfhack.persistent.save{
key='prepare-save/cur_year',
value=yearstr,
ints={df.global.cur_year, df.global.cur_year_tick}
}
-- Save
dfhack.run_script('quicksave')

@ -116,8 +116,7 @@ if opt then
if opt == '--fix' then
fix = true
else
dfhack.printerr('Invalid option: '..opt)
return
qerror('Invalid option: '..opt)
end
end

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

@ -1,8 +1,7 @@
-- Makes the game immediately save the state.
if not dfhack.isMapLoaded() then
dfhack.printerr("World and map aren't loaded.")
return
qerror("World and map aren't loaded.")
end
local ui_main = df.global.ui.main

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