develop
Anuradha Dissanayake 2013-03-16 12:51:31 +13:00
commit b961cb6a09
301 changed files with 27227 additions and 9930 deletions

3
.gitignore vendored

@ -57,3 +57,6 @@ dfhack/python/dist
build/CPack*Config.cmake
/cmakeall.bat
# vim swap files
*.swp

@ -58,14 +58,10 @@ if (NOT EXISTS ${dfhack_SOURCE_DIR}/library/xml/codegen.pl OR NOT EXISTS ${dfhac
endif()
# set up versioning.
set(DF_VERSION_MAJOR "0")
set(DF_VERSION_MINOR "34")
set(DF_VERSION_PATCH "11")
set(DF_VERSION "${DF_VERSION_MAJOR}.${DF_VERSION_MINOR}.${DF_VERSION_PATCH}")
set(DF_VERSION "0.34.11")
SET(DFHACK_RELEASE "r3" CACHE STRING "Current release revision.")
SET(DFHACK_RELEASE "r2" CACHE STRING "Current release revision.")
set(DFHACK_VERSION "${DF_VERSION_MAJOR}.${DF_VERSION_MINOR}.${DF_VERSION_PATCH}-${DFHACK_RELEASE}")
set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}")
add_definitions(-DDFHACK_VERSION="${DFHACK_VERSION}")
## where to install things (after the build is done, classic 'make install' or package structure)
@ -146,7 +142,7 @@ include_directories(depends/clsocket/src)
add_subdirectory(depends)
find_package(Docutils)
#find_package(Docutils)
#set (RST_FILES
#"Readme"
@ -173,6 +169,7 @@ IF(BUILD_LIBRARY)
add_subdirectory (library)
## install the default documentation files
install(FILES LICENSE "Lua API.html" Readme.html Compile.html Contributors.html DESTINATION ${DFHACK_USERDOC_DESTINATION})
install(DIRECTORY images DESTINATION ${DFHACK_USERDOC_DESTINATION})
endif()
#build the plugins

@ -334,20 +334,21 @@ ul.auto-toc {
<li><a class="reference internal" href="#build" id="id7">Build</a></li>
</ul>
</li>
<li><a class="reference internal" href="#windows" id="id8">Windows</a><ul>
<li><a class="reference internal" href="#id1" id="id9">How to get the code</a></li>
<li><a class="reference internal" href="#id2" id="id10">Dependencies</a></li>
<li><a class="reference internal" href="#id3" id="id11">Build</a></li>
<li><a class="reference internal" href="#mac-os-x" id="id8">Mac OS X</a></li>
<li><a class="reference internal" href="#windows" id="id9">Windows</a><ul>
<li><a class="reference internal" href="#id1" id="id10">How to get the code</a></li>
<li><a class="reference internal" href="#id2" id="id11">Dependencies</a></li>
<li><a class="reference internal" href="#id3" id="id12">Build</a></li>
</ul>
</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="#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>
<li><a class="reference internal" href="#build-types" id="id13">Build types</a></li>
<li><a class="reference internal" href="#using-the-library-as-a-developer" id="id14">Using the library as a developer</a><ul>
<li><a class="reference internal" href="#df-data-structure-definitions" id="id15">DF data structure definitions</a></li>
<li><a class="reference internal" href="#remote-access-interface" id="id16">Remote access interface</a></li>
<li><a class="reference internal" href="#contributing-to-dfhack" id="id17">Contributing to DFHack</a><ul>
<li><a class="reference internal" href="#coding-style" id="id18">Coding style</a></li>
<li><a class="reference internal" href="#how-to-get-new-code-into-dfhack" id="id19">How to get new code into DFHack</a></li>
<li><a class="reference internal" href="#memory-research" id="id20">Memory research</a></li>
</ul>
</li>
</ul>
@ -403,11 +404,66 @@ extra options.</p>
program.</p>
</div>
</div>
<div class="section" id="mac-os-x">
<h1><a class="toc-backref" href="#id8">Mac OS X</a></h1>
<ol class="arabic">
<li><p class="first">Download and unpack a copy of the latest DF</p>
</li>
<li><p class="first">Install Xcode from Mac App Store</p>
</li>
<li><p class="first">Open Xcode, go to Preferences &gt; Downloads, and install the Command Line Tools.</p>
</li>
<li><p class="first">Install MacPorts.</p>
</li>
<li><p class="first">Install dependencies from MacPorts:</p>
<ul>
<li><p class="first"><tt class="docutils literal">sudo port install gcc45 +universal cmake +universal <span class="pre">git-core</span> +universal</tt></p>
<p>This will take some time—maybe hours, depending on your machine.</p>
</li>
<li><p class="first">At some point during this process, it may ask you to install a Java environment; let it do so.</p>
</li>
</ul>
</li>
<li><p class="first">Install perl dependencies</p>
<blockquote>
<ol class="arabic">
<li><p class="first"><tt class="docutils literal">sudo cpan</tt></p>
<p>If this is the first time you've run cpan, you will need to go through the setup
process. Just stick with the defaults for everything and you'll be fine.</p>
</li>
<li><p class="first"><tt class="docutils literal">install <span class="pre">XML::LibXML</span></tt></p>
</li>
<li><p class="first"><tt class="docutils literal">install <span class="pre">XML::LibXSLT</span></tt></p>
</li>
</ol>
</blockquote>
</li>
<li><p class="first">Get the dfhack source:</p>
<pre class="literal-block">
git clone https://github.com/danaris/dfhack.git
cd dfhack
git submodule init
git submodule update
</pre>
</li>
<li><p class="first">Build dfhack:</p>
<pre class="literal-block">
mkdir build-osx
cd build-osx
export CC=/opt/local/bin/gcc-mp-4.5
export CXX=/opt/local/bin/g++-mp-4.5
cmake .. -DCMAKE_BUILD_TYPE:string=Release -DCMAKE_INSTALL_PREFIX=/path/to/DF/directory
make
make install
</pre>
</li>
</ol>
</div>
<div class="section" id="windows">
<h1><a class="toc-backref" href="#id8">Windows</a></h1>
<h1><a class="toc-backref" href="#id9">Windows</a></h1>
<p>On Windows, DFHack replaces the SDL library distributed with DF.</p>
<div class="section" id="id1">
<h2><a class="toc-backref" href="#id9">How to get the code</a></h2>
<h2><a class="toc-backref" href="#id10">How to get the code</a></h2>
<p>DFHack doesn't have any kind of system of code snapshots in place, so you will have to get code from the github repository using git.
You will need some sort of Windows port of git, or a GUI. Some examples:</p>
<blockquote>
@ -428,7 +484,7 @@ git submodule update
<p>If you want to get really involved with the development, create an account on github, make a clone there and then use that as your remote repository instead. Detailed instructions are beyond the scope of this document. If you need help, join us on IRC (#dfhack channel on freenode).</p>
</div>
<div class="section" id="id2">
<h2><a class="toc-backref" href="#id10">Dependencies</a></h2>
<h2><a class="toc-backref" href="#id11">Dependencies</a></h2>
<p>First, you need <tt class="docutils literal">cmake</tt>. Get the win32 installer version from the official
site: <a class="reference external" href="http://www.cmake.org/cmake/resources/software.html">http://www.cmake.org/cmake/resources/software.html</a></p>
<p>It has the usual installer wizard. Make sure you let it add its binary folder
@ -445,7 +501,7 @@ Grab it from Microsoft's site.</p>
<p>If you already have a different version of perl (for example the one from cygwin), you can run into some trouble. Either remove the other perl install from PATH, or install libxml and libxslt for it instead. Strawberry perl works though and has all the required packages.</p>
</div>
<div class="section" id="id3">
<h2><a class="toc-backref" href="#id11">Build</a></h2>
<h2><a class="toc-backref" href="#id12">Build</a></h2>
<p>There are several different batch files in the <tt class="docutils literal">build</tt> folder along with a script that's used for picking the DF path.</p>
<p>First, run set_df_path.vbs and point the dialog that pops up at your DF folder that you want to use for development.
Next, run one of the scripts with <tt class="docutils literal">generate</tt> prefix. These create the MSVC solution file(s):</p>
@ -467,7 +523,7 @@ So pick either Release or RelWithDebInfo build and build the INSTALL target.</p>
</div>
</div>
<div class="section" id="build-types">
<h1><a class="toc-backref" href="#id12">Build types</a></h1>
<h1><a class="toc-backref" href="#id13">Build types</a></h1>
<p><tt class="docutils literal">cmake</tt> allows you to pick a build type by changing this
variable: <tt class="docutils literal">CMAKE_BUILD_TYPE</tt></p>
<pre class="literal-block">
@ -479,7 +535,7 @@ cmake .. -DCMAKE_BUILD_TYPE:string=BUILD_TYPE
'RelWithDebInfo'. 'Debug' is not available on Windows.</p>
</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>
<h1><a class="toc-backref" href="#id14">Using the library as a developer</a></h1>
<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>
@ -497,29 +553,29 @@ The main license is zlib/libpng, some bits are MIT licensed, and some are BSD li
<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>
<h2><a class="toc-backref" href="#id15">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>
<h2><a class="toc-backref" href="#id16">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="#id16">Contributing to DFHack</a></h2>
<h2><a class="toc-backref" href="#id17">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="#id17">Coding style</a></h3>
<h3><a class="toc-backref" href="#id18">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="#id18">How to get new code into DFHack</a></h3>
<h3><a class="toc-backref" href="#id19">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>
@ -529,7 +585,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="#id19">Memory research</a></h3>
<h3><a class="toc-backref" href="#id20">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>

@ -63,6 +63,63 @@ extra options.
You can also use a cmake-friendly IDE like KDevelop 4 or the cmake-gui
program.
========
Mac OS X
========
If you are building on 10.6, please read the subsection below titled "Snow Leopard Changes" FIRST.
1. Download and unpack a copy of the latest DF
2. Install Xcode from Mac App Store
3. Open Xcode, go to Preferences > Downloads, and install the Command Line Tools.
4. Install MacPorts.
5. Install dependencies from MacPorts:
* ``sudo port install gcc45 +universal cmake +universal git-core +universal``
This will take some time—maybe hours, depending on your machine.
* At some point during this process, it may ask you to install a Java environment; let it do so.
6. Install perl dependencies
1. ``sudo cpan``
If this is the first time you've run cpan, you will need to go through the setup
process. Just stick with the defaults for everything and you'll be fine.
2. ``install XML::LibXML``
3. ``install XML::LibXSLT``
7. Get the dfhack source::
git clone https://github.com/danaris/dfhack.git
cd dfhack
git submodule init
git submodule update
8. Build dfhack::
mkdir build-osx
cd build-osx
export CC=/opt/local/bin/gcc-mp-4.5
export CXX=/opt/local/bin/g++-mp-4.5
cmake .. -DCMAKE_BUILD_TYPE:string=Release -DCMAKE_INSTALL_PREFIX=/path/to/DF/directory
make
make install
Snow Leopard Changes
====================
1. Add a step 6.2a (before Install XML::LibXSLT)::
In a separate Terminal window or tab, run:
``sudo ln -s /usr/include/libxml2/libxml /usr/include/libxml``
2. Add a step 7a (before building)::
In <dfhack directory>/library/LuaTypes.cpp, change line 467 to
``int len = strlen((char*)ptr);``
=======
Windows
=======

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

79
NEWS

@ -1,6 +1,83 @@
DFHack future
Nothing yet!
Is not yet known.
DFHack v0.34.11-r3
Internals:
- support for displaying active keybindings properly.
- support for reusable widgets in lua screen library.
- Maps::canStepBetween: returns whether you can walk between two tiles in one step.
- EventManager: monitors various in game events centrally so that individual plugins
don't have to monitor the same things redundantly.
Notable bugfixes:
- autobutcher can be re-enabled again after being stopped.
- stopped Dwarf Manipulator from unmasking vampires.
Misc improvements:
- fastdwarf: new mode using debug flags, and some internal consistency fixes.
- added a small stand-alone utility for applying and removing binary patches.
- removebadthoughts: add --dry-run option
- superdwarf: work in adventure mode too
- tweak stable-cursor: carries cursor location from/to Build menu.
- deathcause: allow selection from the unitlist screen
- slayrace: allow targetting undeads
New tweaks:
- tweak military-training: speed up melee squad training up to 10x (normally 3-5x).
New scripts:
- binpatch: the same as the stand-alone binpatch.exe, but works at runtime.
- region-pops: displays animal populations of the region and allows tweaking them.
- lua: lua interpreter front-end converted to a script from a native command.
- dfusion: misc scripts with a text based menu.
- embark: lets you embark anywhere.
- lever: list and pull fort levers from the dfhack console.
- stripcaged: mark items inside cages for dumping, eg caged goblin weapons.
- soundsense-season: writes the correct season to gamelog.txt on world load.
- create-items: spawn items
- fix/cloth-stockpile: fixes bug 5739; needs to be run after savegame load every time.
New GUI scripts:
- gui/guide-path: displays the cached path for minecart Guide orders.
- gui/workshop-job: displays inputs of a workshop job and allows tweaking them.
- gui/workflow: a front-end for the workflow plugin (part inspired by falconne).
- gui/assign-rack: works together with a binary patch to fix weapon racks.
- gui/gm-editor: an universal editor for lots of dfhack things.
- gui/companion-order: a adventure mode command interface for your companions.
- gui/advfort: a way to do jobs with your adventurer (e.g. build fort).
New binary patches (for use with binpatch):
- armorstand-capacity: doubles the capacity of armor stands.
- custom-reagent-size: lets custom reactions use small amounts of inputs.
- deconstruct-heapfall: stops some items still falling on head when deconstructing.
- deconstruct-teleport: stops items from 16x16 block teleporting when deconstructing.
- hospital-overstocking: stops hospital overstocking with supplies.
- training-ammo: lets dwarves with quiver full of combat-only ammo train.
- weaponrack-unassign: fixes bug that negates work done by gui/assign-rack.
Workflow plugin:
- properly considers minecarts assigned to routes busy.
- code for deducing job outputs rewritten in lua for flexibility.
- logic fix: collecting webs produces silk, and ungathered webs are not thread.
- items assigned to squads are considered busy, even if not in inventory.
- shearing and milking jobs are supported, but only with generic MILK or YARN outputs.
- workflow announces when the stock level gets very low once a season.
New Fix Armory plugin:
Together with a couple of binary patches and the gui/assign-rack script,
this plugin makes weapon racks, armor stands, chests and cabinets in
properly designated barracks be used again for storage of squad equipment.
New Search plugin by falconne:
Adds an incremental search function to the Stocks, Trading, Stockpile and Unit List screens.
New AutoMaterial plugin by falconne:
Makes building constructions (walls, floors, fortifications, etc) a little bit easier by
saving you from having to trawl through long lists of materials each time you place one.
Dfusion plugin:
Reworked to make use of lua modules, now all the scripts can be used from other scripts.
New Eventful plugin:
A collection of lua events, that will allow new ways to interact with df world.
Auto syndrome plugin:
A way of automatically applying boiling rock syndromes and calling dfhack commands controlled by raws.
Infinite sky plugin:
Create new z-levels automatically or on request.
True transformation plugin:
A better way of doing permanent transformations that allows later transformations.
Work now plugin:
Makes the game assign jobs every time you pause.
DFHack v0.34.11-r2

File diff suppressed because it is too large Load Diff

@ -58,9 +58,35 @@ The stonesense plugin might require some additional libraries on Linux.
If any of the plugins or dfhack itself refuses to load, check the stderr.log
file created in your DF folder.
Getting started
===============
If DFHack is installed correctly, it will automatically pop up a console
window once DF is started as usual on windows. Linux and Mac OS X require
running the dfhack script from the terminal, and will use that terminal for
the console.
**NOTE**: The dfhack-run executable is there for calling DFHack commands in
an already running DF+DFHack instance from external OS scripts and programs,
and is *not* the way how you use DFHack normally.
DFHack has a lot of features, which can be accessed by typing commands in the
console, or by mapping them to keyboard shortcuts. Most of the newer and more
user-friendly tools are designed to be at least partially used via the latter
way.
In order to set keybindings, you have to create a text configuration file
called ``dfhack.init``; the installation comes with an example version called
``dfhack.init-example``, which is fully functional, covers all of the recent
features and can be simply renamed to ``dfhack.init``. You are encouraged to look
through it to learn which features it makes available under which key combinations.
For more information, refer to the rest of this document.
============
Using DFHack
============
DFHack basically extends what DF can do with something similar to the drop-down
console found in Quake engine games. On Windows, this is a separate command line
window. On linux, the terminal used to launch the dfhack script is taken over
@ -88,6 +114,46 @@ Interactive commands like 'liquids' cannot be used as hotkeys.
Most of the commands come from plugins. Those reside in 'hack/plugins/'.
Patched binaries
================
On linux and OSX, users of patched binaries may have to find the relevant
section in symbols.xml, and add a new line with the checksum of their
executable::
<md5-hash value='????????????????????????????????'/>
In order to find the correct value of the hash, look into stderr.log;
DFHack prints an error there if it does not recognize the hash.
DFHack includes a small stand-alone utility for applying and removing
binary patches from the game executable. Use it from the regular operating
system console:
* ``binpatch check "Dwarf Fortress.exe" patch.dif``
Checks and prints if the patch is currently applied.
* ``binpatch apply "Dwarf Fortress.exe" patch.dif``
Applies the patch, unless it is already applied or in conflict.
* ``binpatch remove "Dwarf Fortress.exe" patch.dif``
Removes the patch, unless it is already removed.
The patches are expected to be encoded in text format used by IDA.
Live patching
-------------
As an alternative, you can use the ``binpatch`` dfhack command to apply/remove
patches live in memory during a DF session.
In this case, updating symbols.xml is not necessary.
=============================
Something doesn't work, help!
=============================
@ -201,6 +267,8 @@ Controls speedydwarf and teledwarf. Speedydwarf makes dwarves move quickly and p
* 'fastdwarf 1 1' enables both
* 'fastdwarf 0' disables both
* 'fastdwarf 1' enables speedydwarf and disables teledwarf
* 'fastdwarf 2 ...' sets a native debug flag in the game memory
that implements an even more aggressive version of speedydwarf.
Game interface
==============
@ -381,6 +449,26 @@ Options:
:bees: turn colonies into honey bee colonies
createitem
----------
Allows creating new items of arbitrary types and made of arbitrary materials.
Any items created are spawned at the feet of the selected unit.
Specify the item and material information as you would indicate them in custom reaction raws, with the following differences:
* Separate the item and material with a space rather than a colon
* If the item has no subtype, omit the :NONE
* If the item is REMAINS, FISH, FISH_RAW, VERMIN, PET, or EGG, specify a CREATURE:CASTE pair instead of a material token.
Corpses, body parts, and prepared meals cannot be created using this tool.
Examples:
``createitem GLOVES:ITEM_GLOVES_GAUNTLETS INORGANIC:STEEL 2``
Create 2 pairs of steel gauntlets.
``createitem WOOD PLANT_MAT:TOWER_CAP:WOOD``
Create tower-cap logs.
``createitem FISH FISH_SHAD:MALE 5``
Create a stack of 5 cleaned shad, ready to eat.
deramp (by zilpin)
------------------
Removes all ramps designated for removal from the map. This is useful for replicating the old channel digging designation.
@ -1019,6 +1107,9 @@ Subcommands that persist until disabled or DF quit:
:patrol-duty: Makes Train orders not count as patrol duty to stop unhappy thoughts.
Does NOT fix the problem when soldiers go off-duty (i.e. civilian).
:readable-build-plate: Fixes rendering of creature weight limits in pressure plate build menu.
.. image:: images/tweak-plate.png
:stable-temp: Fixes performance bug 6012 by squashing jitter in temperature updates.
In very item-heavy forts with big stockpiles this can improve FPS by 50-100%
:fast-heat: Further improves temperature update performance by ensuring that 1 degree
@ -1037,9 +1128,78 @@ Subcommands that persist until disabled or DF quit:
:military-stable-assign: Preserve list order and cursor position when assigning to squad,
i.e. stop the rightmost list of the Positions page of the military
screen from constantly resetting to the top.
:military-color-assigned: Color squad candidates already assigned to other squads in brown/green
:military-color-assigned: Color squad candidates already assigned to other squads in yellow/green
to make them stand out more in the list.
.. image:: images/tweak-mil-color.png
:military-training: Speeds up melee squad training by removing an almost certainly
unintended inverse dependency of training speed on unit count
(i.e. the more units you have, the slower it becomes), and making
the units spar more.
fix-armory
----------
Enables a fix for storage of squad equipment in barracks.
Specifically, it prevents your haulers from moving squad equipment
to stockpiles, and instead queues jobs to store it on weapon racks,
armor stands, and in containers.
.. note::
In order to actually be used, weapon racks have to be patched and
manually assigned to a squad. See documentation for ``gui/assign-rack``
below.
Also, the default capacity of armor stands is way too low, so you
may want to also apply the ``armorstand-capacity`` patch. Check out
http://www.bay12games.com/dwarves/mantisbt/view.php?id=1445
for more information about the bugs.
Note that the buildings in the armory are used as follows:
* Weapon racks (when patched) are used to store any assigned weapons.
Each rack belongs to a specific squad, and can store up to 5 weapons.
* Armor stands belong to specific squad members and are used for
armor and shields. By default one stand can store one item of each
type (hence one boot or gauntlet); if patched, the limit is raised to 2,
which should be sufficient.
* Cabinets are used to store assigned clothing for a specific squad member.
They are **never** used to store owned clothing.
* Chests (boxes, etc) are used for a flask, backpack or quiver assigned
to the squad member. Due to a probable bug, food is dropped out of the
backpack when it is stored.
.. warning::
Although armor stands, cabinets and chests properly belong only to one
squad member, the owner of the building used to create the barracks will
randomly use any containers inside the room. Thus, it is recommended to
always create the armory from a weapon rack.
Contrary to the common misconception, all these uses are controlled by the
*Individual Equipment* usage flag. The *Squad Equipment* flag is actually
intended for ammo, but the game does even less in that area than for armor
and weapons. This plugin implements the following rules almost from scratch:
* Combat ammo is stored in chests inside rooms with Squad Equipment enabled.
* If a chest is assigned to a squad member due to Individual Equipment also
being set, it is only used for that squad's ammo; otherwise, any squads
with Squad Equipment on the room will use all of the chests at random.
* Training ammo is stored in chests inside archery ranges designated from
archery targets, and controlled by the same Train flag as archery training
itself. This is inspired by some defunct code for weapon racks.
There are some minor traces in the game code to suggest that the first of
these rules is intended by Toady; the rest are invented by this plugin.
Mode switch and reclaim
=======================
@ -1165,10 +1325,18 @@ Usage:
List workflow-controlled jobs (if in a workshop, filtered by it).
``workflow list``
List active constraints, and their job counts.
``workflow count <constraint-spec> <cnt-limit> [cnt-gap], workflow amount <constraint-spec> <cnt-limit> [cnt-gap]``
Set a constraint. The first form counts each stack as only 1 item.
``workflow list-commands``
List active constraints as workflow commands that re-create them;
this list can be copied to a file, and then reloaded using the
``script`` built-in command.
``workflow count <constraint-spec> <cnt-limit> [cnt-gap]``
Set a constraint, counting every stack as 1 item.
``workflow amount <constraint-spec> <cnt-limit> [cnt-gap]``
Set a constraint, counting all items within stacks.
``workflow unlimit <constraint-spec>``
Delete a constraint.
``workflow unlimit-all``
Delete all constraints.
Function
........
@ -1183,6 +1351,37 @@ amount goes above or below the limit. The gap specifies how much below the limit
the amount has to drop before jobs are resumed; this is intended to reduce
the frequency of jobs being toggled.
Check out the ``gui/workflow`` script below for a simple front-end integrated
in the game UI.
Constraint format
.................
The contstraint spec consists of 4 parts, separated with '/' characters::
ITEM[:SUBTYPE]/[GENERIC_MAT,...]/[SPECIFIC_MAT:...]/[LOCAL,<quality>]
The first part is mandatory and specifies the item type and subtype,
using the raw tokens for items, in the same syntax you would e.g. use
for a custom reaction input. See this list for more info: http://dwarffortresswiki.org/index.php/Item_token
The subsequent parts are optional:
- A generic material spec constrains the item material to one of
the hard-coded generic classes, which currently include::
PLANT WOOD CLOTH SILK LEATHER BONE SHELL SOAP TOOTH HORN PEARL YARN
METAL STONE SAND GLASS CLAY MILK
- A specific material spec chooses the material exactly, using the
raw syntax for reaction input materials, e.g. INORGANIC:IRON,
although for convenience it also allows just IRON, or ACACIA:WOOD etc.
See this page for more details on the unabbreviated raw syntax:
http://dwarffortresswiki.org/index.php/Material_token
- A comma-separated list of miscellaneous flags, which currently can
be used to ignore imported items or items below a certain quality.
Constraint examples
...................
@ -1212,6 +1411,11 @@ Make sure there are always 15-20 coal and 25-30 copper bars.
workflow count BAR//COAL 20
workflow count BAR//COPPER 30
Produce 15-20 gold crafts.
::
workflow count CRAFTS//GOLD 20
Collect 15-20 sand bags and clay boulders.
::
@ -1223,9 +1427,16 @@ Make sure there are always 80-100 units of dimple dye.
workflow amount POWDER_MISC//MUSHROOM_CUP_DIMPLE:MILL 100 20
.. note::
In order for this to work, you have to set the material of the PLANT input
on the Mill Plants job to MUSHROOM_CUP_DIMPLE using the 'job item-material'
command.
command. Otherwise the plugin won't be able to deduce the output material.
Maintain 10-100 locally-made crafts of exceptional quality.
::
workflow count CRAFTS///LOCAL,EXCEPTIONAL 100 90
Fortress activity management
@ -1515,19 +1726,17 @@ twice.
dfusion
-------
This is the DFusion lua plugin system by warmist/darius, running as a DFHack plugin.
See the bay12 thread for details: http://www.bay12forums.com/smf/index.php?topic=69682.15
This is the DFusion lua plugin system by Warmist, running as a DFHack plugin. There are two parts to this plugin: an interactive script that shows a text based menu and lua modules. Some of the functionality of is intentionaly left out of the menu:
:Friendship: a binary plugin that allows multi race forts (to use make a script that imports plugins.dfusion.friendship and use Friendship:install{table} table should contain list of race names.)
:Embark: a binary plugin that allows multi race embark (to use make a script that imports plugins.dfusion.embark and use Embark:install{table} table should contain list of race names or list of pairs (race-name, caste_id)).
Confirmed working DFusion plugins:
See the bay12 thread for details: http://www.bay12forums.com/smf/index.php?topic=93317.0
:simple_embark: allows changing the number of dwarves available on embark.
.. note::
* Some of the DFusion plugins aren't completely ported yet. This can lead to crashes.
* This is currently working only on Windows.
* The game will be suspended while you're using dfusion. Don't panic when it doen't respond.
* The game will be suspended while you're using dfusion. Don't panic when it doesn't respond.
misery
------
@ -1584,6 +1793,12 @@ Scripts in this subdirectory fix various bugs and issues, some of them obscure.
Diagnoses and fixes issues with nonexistant 'items occupying site', usually
caused by autodump bugs or other hacking mishaps.
* fix/cloth-stockpile
Fixes erratic behavior of cloth stockpiles by scanning material objects
in memory and patching up some invalid reference fields. Needs to be run
every time a save game is loaded; putting ``fix/cloth-stockpile enable``
in ``dfhack.init`` makes it run automatically.
gui/*
=====
@ -1591,6 +1806,17 @@ gui/*
Scripts that implement dialogs inserted into the main game window are put in this
directory.
binpatch
========
Checks, applies or removes binary patches directly in memory at runtime::
binpatch check/apply/remove <patchname>
If the name of the patch has no extension or directory separators, the
script uses ``hack/patches/<df-version>/<name>.dif``, thus auto-selecting
the version appropriate for the currently loaded executable.
quicksave
=========
@ -1632,29 +1858,34 @@ 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.
The script can target a single creature, when used with the ``him`` argument,
or the whole fort population, with ``all``.
To show every bad thought present without actually removing them, run the
script with the ``-n`` or ``--dry-run`` argument. This can give a quick
hint on what bothers your dwarves the most.
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.
Internals: the thoughts are set to be very old, so that the game remove them
quickly after you unpause.
slayrace
========
Kills any unit of a given race.
With no argument, lists the available races.
With no argument, lists the available races and count eligible targets.
With the special argument 'him', targets only the selected creature.
With the special argument ``him``, targets only the selected creature.
With the special argument ``undead``, targets all undeads on the map,
regardless of their race.
Any non-dead non-caged unit of the specified race gets its ``blood_count``
set to 0, which means immediate death at the next game tick. For creatures
such as vampires, also set animal.vanish_countdown to 2.
such as vampires, it also sets animal.vanish_countdown to 2.
An alternate mode is selected by adding a 2nd argument to the command,
``magma``. In this case, a column of 7/7 magma is generated on top of the
@ -1737,7 +1968,106 @@ deathcause
==========
Focus a body part ingame, and this script will display the cause of death of
the creature.
Also works when selecting units from the 'u'nitlist viewscreen.
lua
===
There are the following ways to invoke this command:
1. ``lua`` (without any parameters)
This starts an interactive lua interpreter.
2. ``lua -f "filename"`` or ``lua --file "filename"``
This loads and runs the file indicated by filename.
3. ``lua -s ["filename"]`` or ``lua --save ["filename"]``
This loads and runs the file indicated by filename from the save
directory. If the filename is not supplied, it loads "dfhack.lua".
4. ``:lua`` *lua statement...*
Parses and executes the lua statement like the interactive interpreter would.
embark
======
Allows to embark anywhere. Currently windows only.
lever
=====
Allow manipulation of in-game levers from the dfhack console.
Can list levers, including state and links, with::
lever list
To queue a job so that a dwarf will pull the lever 42, use ``lever pull 42``.
This is the same as 'q'uerying the building and queue a 'P'ull request.
To magically toggle the lever immediately, use::
lever pull 42 --now
stripcaged
==========
For dumping items inside cages. Will mark selected items for dumping, then
a dwarf may come and actually dump it. See also ``autodump``.
With the ``items`` argument, only dumps items laying in the cage, excluding
stuff worn by caged creatures. ``weapons`` will dump worn weapons, ``armor``
will dump everything worn by caged creatures (including armor and clothing),
and ``all`` will dump everything, on a creature or not.
``stripcaged list`` will display on the dfhack console the list of all cages
and their item content.
Without further arguments, all commands work on all cages and animal traps on
the map. With the ``here`` argument, considers only the in-game selected cage
(or the cage under the game cursor). To target only specific cages, you can
alternatively pass cage IDs as arguments::
stripcaged weapons 25321 34228
create-items
============
Spawn arbitrary items under the cursor.
The first argument gives the item category, the second gives the material,
and the optionnal third gives the number of items to create (defaults to 20).
Currently supported item categories: ``boulder``, ``bar``, ``plant``, ``log``,
``web``.
Instead of material, using ``list`` makes the script list eligible materials.
The ``web`` item category will create an uncollected cobweb on the floor.
Note that the script does not enforce anything, and will let you create
boulders of toad blood and stuff like that.
However the ``list`` mode will only show 'normal' materials.
Exemples::
create-items boulders COAL_BITUMINOUS 12
create-items plant tail_pig
create-items log list
create-items web CREATURE:SPIDER_CAVE_GIANT:SILK
create-items bar CREATURE:CAT:SOAP
create-items bar adamantine
soundsense-season
=================
It is a well known issue that Soundsense cannot detect the correct
current season when a savegame is loaded and has to play random
season music until a season switch occurs.
This script registers a hook that prints the appropriate string
to gamelog.txt on every map load to fix this. For best results
call the script from ``dfhack.init``.
=======================
In-game interface tools
@ -1751,6 +2081,9 @@ are mostly implemented by lua scripts.
In order to avoid user confusion, as a matter of policy all these tools
display the word "DFHack" on the screen somewhere while active.
When that is not appropriate because they merely add keybinding hints to
existing DF screens, they deliberately use red instead of green for the key.
As an exception, the tweak plugin described above does not follow this
guideline because it arguably just fixes small usability bugs in the game UI.
@ -1761,12 +2094,18 @@ Dwarf Manipulator
Implemented by the manipulator plugin. To activate, open the unit screen and
press 'l'.
.. image:: images/manipulator.png
This tool implements a Dwarf Therapist-like interface within the game UI. The
far left column displays the unit's Happiness (color-coded based on its
value), and the right half of the screen displays each dwarf's labor settings
and skill levels (0-9 for Dabbling thru Professional, A-E for Great thru Grand
Master, and U-Z for Legendary thru Legendary+5). Cells with red backgrounds
denote skills not controlled by labors.
Master, and U-Z for Legendary thru Legendary+5).
Cells with teal backgrounds denote skills not controlled by labors, e.g.
military and social skills.
.. image:: images/manipulator2.png
Use the arrow keys or number pad to move the cursor around, holding Shift to
move 10 tiles at a time.
@ -1800,19 +2139,116 @@ Pressing ESC normally returns to the unit screen, but Shift-ESC would exit
directly to the main dwarf mode screen.
Search
======
The search plugin adds search to the Stocks, Animals, Trading, Stockpile,
Noble (assignment candidates), Military (position candidates), Burrows
(unit list), Rooms, Announcements, Job List and Unit List screens.
.. image:: images/search.png
Searching works the same way as the search option in "Move to Depot" does.
You will see the Search option displayed on screen with a hotkey (usually 's').
Pressing it lets you start typing a query and the relevant list will start
filtering automatically.
Pressing ENTER, ESC or the arrow keys will return you to browsing the now
filtered list, which still functions as normal. You can clear the filter
by either going back into search mode and backspacing to delete it, or
pressing the "shifted" version of the search hotkey while browsing the
list (e.g. if the hotkey is 's', then hitting 'shift-s' will clear any
filter).
Leaving any screen automatically clears the filter.
In the Trade screen, the actual trade will always only act on items that
are actually visible in the list; the same effect applies to the Trade
Value numbers displayed by the screen. Because of this, the 't' key is
blocked while search is active, so you have to reset the filters first.
Pressing Alt-C will clear both search strings.
In the stockpile screen the option only appears if the cursor is in the
rightmost list:
.. image:: images/search-stockpile.png
Note that the 'Permit XXX'/'Forbid XXX' keys conveniently operate only
on items actually shown in the rightmost list, so it is possible to select
only fat or tallow by forbidding fats, then searching for fat/tallow, and
using Permit Fats again while the list is filtered.
AutoMaterial
============
The automaterial plugin makes building constructions (walls, floors, fortifications,
etc) a little bit easier by saving you from having to trawl through long lists of
materials each time you place one.
Firstly, it moves the last used material for a given construction type to the top of
the list, if there are any left. So if you build a wall with chalk blocks, the next
time you place a wall the chalk blocks will be at the top of the list, regardless of
distance (it only does this in "grouped" mode, as individual item lists could be huge).
This should mean you can place most constructions without having to search for your
preferred material type.
.. image:: images/automaterial-mat.png
Pressing 'a' while highlighting any material will enable that material for "auto select"
for this construction type. You can enable multiple materials as autoselect. Now the next
time you place this type of construction, the plugin will automatically choose materials
for you from the kinds you enabled. If there is enough to satisfy the whole placement,
you won't be prompted with the material screen - the construction will be placed and you
will be back in the construction menu as if you did it manually.
When choosing the construction placement, you will see a couple of options:
.. image:: images/automaterial-pos.png
Use 'a' here to temporarily disable the material autoselection, e.g. if you need
to go to the material selection screen so you can toggle some materials on or off.
The other option (auto type selection, off by default) can be toggled on with 't'. If you
toggle this option on, instead of returning you to the main construction menu after selecting
materials, it returns you back to this screen. If you use this along with several autoselect
enabled materials, you should be able to place complex constructions more conveniently.
gui/liquids
===========
To use, bind to a key and activate in the 'k' mode.
To use, bind to a key (the example config uses Alt-L) and activate in the 'k' mode.
.. image:: images/liquids.png
This script is a gui front-end to the liquids plugin and works similar to it,
allowing you to add or remove water & magma, and create obsidian walls & floors.
Note that there is **no undo support**, and that bugs in this plugin have been
known to create pathfinding problems and heat traps.
The ``b`` key changes how the affected area is selected. The default *Rectangle*
mode works by selecting two corners like any ordinary designation. The ``p``
key chooses between adding water, magma, obsidian walls & floors, or just
tweaking flags.
When painting liquids, it is possible to select the desired level with ``+-``,
and choose between setting it exactly, only increasing or only decreasing
with ``s``.
In addition, ``f`` allows disabling or enabling the flowing water computations
for an area, and ``r`` operates on the "permanent flow" property that makes
rivers power water wheels even when full and technically not flowing.
While active, use the suggested keys to switch the usual liquids parameters, and Enter
to select the target area and apply changes.
After setting up the desired operations using the described keys, use ``Enter`` to apply them.
gui/mechanisms
==============
To use, bind to a key and activate in the 'q' mode.
To use, bind to a key (the example config uses Ctrl-M) and activate in the 'q' mode.
.. image:: images/mechanisms.png
Lists mechanisms connected to the building, and their links. Navigating the list centers
the view on the relevant linked buildings.
@ -1830,21 +2266,35 @@ via a simple dialog in the game ui.
* ``gui/rename [building]`` in 'q' mode changes the name of a building.
.. image:: images/rename-bld.png
The selected building must be one of stockpile, workshop, furnace, trap, or siege engine.
It is also possible to rename zones from the 'i' menu.
* ``gui/rename [unit]`` with a unit selected changes the nickname.
Unlike the built-in interface, this works even on enemies and animals.
* ``gui/rename unit-profession`` changes the selected unit's custom profession name.
.. image:: images/rename-prof.png
Likewise, this can be applied to any unit, and when used on animals it overrides
their species string.
The ``building`` or ``unit`` options are automatically assumed when in relevant ui state.
The example config binds building/unit rename to Ctrl-Shift-N, and
unit profession change to Ctrl-Shift-T.
gui/room-list
=============
To use, bind to a key and activate in the 'q' mode, either immediately or after opening
the assign owner page.
To use, bind to a key (the example config uses Alt-R) and activate in the 'q' mode,
either immediately or after opening the assign owner page.
.. image:: images/room-list.png
The script lists other rooms owned by the same owner, or by the unit selected in the assign
list, and allows unassigning them.
@ -1853,7 +2303,8 @@ list, and allows unassigning them.
gui/choose-weapons
==================
Bind to a key, and activate in the Equip->View/Customize page of the military screen.
Bind to a key (the example config uses Ctrl-W), and activate in the Equip->View/Customize
page of the military screen.
Depending on the cursor location, it rewrites all 'individual choice weapon' entries
in the selected squad or position to use a specific weapon type matching the assigned
@ -1864,6 +2315,208 @@ Rationale: individual choice seems to be unreliable when there is a weapon short
and may lead to inappropriate weapons being selected.
gui/guide-path
==============
Bind to a key (the example config uses Alt-P), and activate in the Hauling menu with
the cursor over a Guide order.
.. image:: images/guide-path.png
The script displays the cached path that will be used by the order; the game
computes it when the order is executed for the first time.
gui/workshop-job
================
Bind to a key (the example config uses Alt-A), and activate with a job selected in
a workshop in the 'q' mode.
.. image:: images/workshop-job.png
The script shows a list of the input reagents of the selected job, and allows changing
them like the ``job item-type`` and ``job item-material`` commands.
Specifically, pressing the 'i' key pops up a dialog that lets you select an item
type from a list.
.. image:: images/workshop-job-item.png
Pressing 'm', unless the item type does not allow a material,
lets you choose a material.
.. image:: images/workshop-job-material.png
Since there are a lot more materials than item types, this dialog is more complex
and uses a hierarchy of sub-menus. List choices that open a sub-menu are marked
with an arrow on the left.
.. warning::
Due to the way input reagent matching works in DF, you must select an item type
if you select a material, or the material will be matched incorrectly in some cases.
If you press 'm' without choosing an item type, the script will auto-choose it
if there is only one valid choice, or pop up an error message box instead of the
material selection dialog.
Note that both materials and item types presented in the dialogs are filtered
by the job input flags, and even the selected item type for material selection,
or material for item type selection. Many jobs would let you select only one
input item type.
For example, if you choose a *plant* input item type for your prepare meal job,
it will only let you select cookable materials.
If you choose a *barrel* item instead (meaning things stored in barrels, like
drink or milk), it will let you select any material, since in this case the
material is matched against the barrel itself. Then, if you select, say, iron,
and then try to change the input item type, now it won't let you select *plant*;
you have to unset the material first.
gui/workflow
============
Bind to a key (the example config uses Alt-W), and activate with a job selected
in a workshop in the 'q' mode.
.. image:: images/workflow.png
This script provides a simple interface to constraints managed by the workflow
plugin. When active, it displays a list of all constraints applicable to the
current job, and their current status.
A constraint specifies a certain range to be compared against either individual
*item* or whole *stack* count, an item type and optionally a material. When the
current count is below the lower bound of the range, the job is resumed; if it
is above or equal to the top bound, it will be suspended. Within the range, the
specific constraint has no effect on the job; others may still affect it.
Pressing 'I' switches the current constraint between counting stacks or items.
Pressing 'R' lets you input the range directly; 'e', 'r', 'd', 'f' adjust the
bounds by 5, 10, or 20 depending on the direction and the 'I' setting (counting
items and expanding the range each gives a 2x bonus).
Pressing 'A' produces a list of possible outputs of this job as guessed by
workflow, and lets you create a new constraint by choosing one as template. If you
don't see the choice you want in the list, it likely means you have to adjust
the job material first using ``job item-material`` or ``gui/workshop-job``,
as described in ``workflow`` documentation above. In this manner, this feature
can be used for troubleshooting jobs that don't match the right constraints.
.. image:: images/workflow-new1.png
If you select one of the outputs with Enter, the matching constraint is simply
added to the list. If you use Shift-Enter, the interface proceeds to the
next dialog, which allows you to edit the suggested constraint parameters to
suit your need, and set the item count range.
.. image:: images/workflow-new2.png
Pressing 'S' (or, with the example config, Alt-W in the 'z' stocks screen)
opens the overall status screen, which was copied from the C++ implementation
by falconne for better integration with the rest of the lua script:
.. image:: images/workflow-status.png
This screen shows all currently existing workflow constraints, and allows
monitoring and/or changing them from one screen. The constraint list can
be filtered by typing text in the field below.
The color of the stock level number indicates how "healthy" the stock level
is, based on current count and trend. Bright green is very good, green is good,
red is bad, bright red is very bad.
The limit number is also color-coded. Red means that there are currently no
workshops producing that item (i.e. no jobs). If it's yellow, that means the
production has been delayed, possibly due to lack of input materials.
The chart on the right is a plot of the last 14 days (28 half day plots) worth
of stock history for the selected item, with the rightmost point representing
the current stock value. The bright green dashed line is the target
limit (maximum) and the dark green line is that minus the gap (minimum).
gui/assign-rack
===============
Bind to a key (the example config uses P), and activate when viewing a weapon
rack in the 'q' mode.
.. image:: images/assign-rack.png
This script is part of a group of related fixes to make the armory storage
work again. The existing issues are:
* Weapon racks have to each be assigned to a specific squad, like with
beds/boxes/armor stands and individual squad members, but nothing in
the game does this. This issue is what this script addresses.
* Even if assigned by the script, **the game will unassign the racks again without a binary patch**.
This patch is called ``weaponrack-unassign``, and can be applied via
the binpatch program, or the matching script. See this for more info
about the bug:
http://www.bay12games.com/dwarves/mantisbt/view.php?id=1445
* Haulers still take equpment stored in the armory away to the stockpiles,
unless the ``fix-armory`` plugin above is used.
The script interface simply lets you designate one of the squads that
are assigned to the barracks/armory containing the selected stand as
the intended user. In order to aid in the choice, it shows the number
of currently assigned racks for every valid squad.
gui/advfort
=============
This script allows to perform jobs in adventure mode. For more complete help
press '?' while script is running. It's most confortable to use this as a
keybinding. (e.g. keybinding set Ctrl-T gui/advfort). Possible arguments:
* -a or --nodfassign - uses different method to assign items.
* -i or --inventory - checks inventory for possible items to use in the job.
* -c or --cheat - relaxes item requirements for buildings (e.g. walls from bones).
implies -a
* job - selects that job (e.g. Dig or FellTree)
gui/companion-order
=======================
A script to issue orders for companions. Select companions with lower case chars, issue orders with upper
case. Must be in look or talk mode to issue command on tile.
* move - orders selected companions to move to location. If companions are following they will move no more than 3 tiles from you.
* equip - try to equip items on the ground.
* pick-up - try to take items into hand (also wield)
* unequip - remove and drop equipment
* unwield - drop held items
* wait - temporarely remove from party
* follow - rejoin the party after "wait"
* leave - remove from party (can be rejoined by talking)
gui/gm-editor
=============
There are three ways to open this editor:
* using gui/gm-editor command/keybinding - opens editor on what is selected
or viewed (e.g. unit/item description screen)
* using gui/gm-editor <lua command> - executes lua command and opens editor on
it's results (e.g. gui/gm-editor "df.global.world.items.all" shows all items)
* using gui/gm-editor dialog - shows an in game dialog to input lua command. Works
the same as version above.
This editor allows to change and modify almost anything in df. Press '?' for an
in-game help.
=============
Behavior Mods
=============
@ -1899,7 +2552,10 @@ Configuration UI
----------------
The configuration front-end to the plugin is implemented by the gui/siege-engine
script. Bind it to a key and activate after selecting a siege engine in 'q' mode.
script. Bind it to a key (the example config uses Alt-A) and activate after selecting
a siege engine in 'q' mode.
.. image:: images/siege-engine.png
The main mode displays the current target, selected ammo item type, linked stockpiles and
the allowed operator skill range. The map tile color is changed to signify if it can be
@ -1929,7 +2585,10 @@ The power-meter plugin implements a modified pressure plate that detects power b
supplied to gear boxes built in the four adjacent N/S/W/E tiles.
The configuration front-end is implemented by the gui/power-meter script. Bind it to a
key and activate after selecting Pressure Plate in the build menu.
key (the example config uses Ctrl-Shift-M) and activate after selecting Pressure Plate
in the build menu.
.. image:: images/power-meter.png
The script follows the general look and feel of the regular pressure plate build
configuration page, but configures parameters relevant to the modded power meter building.
@ -2051,3 +2710,4 @@ be bought from caravans. :)
To be really useful this needs patches from bug 808, ``tweak fix-dimensions``
and ``tweak advmode-contained``.

@ -36,16 +36,14 @@
* internal hash function, calling
* the basic methods from md5.h
*/
std::string md5wrapper::hashit(std::string text)
std::string md5wrapper::hashit(unsigned char *data, size_t length)
{
MD5Context ctx;
//init md5
MD5Init(&ctx);
//update with our string
MD5Update(&ctx,
(unsigned char*)text.c_str(),
text.length());
MD5Update(&ctx, data, length);
//create the hash
unsigned char buff[16] = "";
@ -95,10 +93,9 @@ md5wrapper::~md5wrapper()
*/
std::string md5wrapper::getHashFromString(std::string text)
{
return this->hashit(text);
return this->hashit((unsigned char*)text.data(), text.length());
}
/*
* creates a MD5 hash from
* a file specified in "filename" and

@ -31,7 +31,7 @@ class md5wrapper
* internal hash function, calling
* the basic methods from md5.h
*/
std::string hashit(std::string text);
std::string hashit(unsigned char *data, size_t length);
/*
* converts the numeric giets to
@ -52,6 +52,10 @@ class md5wrapper
*/
std::string getHashFromString(std::string text);
std::string getHashFromBytes(const unsigned char *data, size_t size) {
return hashit(const_cast<unsigned char*>(data),size);
}
/*
* creates a MD5 hash from
* a file specified in "filename" and

@ -2,23 +2,34 @@
# Generic dwarfmode bindings #
##############################
# toggle the display of water level as 1-7 tiles
keybinding add Ctrl-W twaterlvl
# with cursor:
# designate the whole vein for digging
keybinding add Ctrl-V digv
keybinding add Ctrl-Shift-V "digv x"
# clean the selected tile of blood etc
keybinding add Ctrl-C spotclean
# destroy items designated for dump in the selected tile
keybinding add Ctrl-Shift-K autodump-destroy-here
# any item:
# with an item selected:
# destroy the selected item
keybinding add Ctrl-K autodump-destroy-item
# scripts:
# quicksave, only in main dwarfmode screen and menu page
keybinding add Ctrl-Alt-S@dwarfmode/Default quicksave
# gui/rename script
# gui/rename script - rename units and buildings
keybinding add Ctrl-Shift-N gui/rename
keybinding add Alt-Shift-P "gui/rename unit-profession"
keybinding add Ctrl-Shift-T "gui/rename unit-profession"
##############################
# Generic adv mode bindings #
@ -31,10 +42,10 @@ keybinding add Ctrl-Shift-B "adv-bodyswap force"
# Context-specific bindings #
#############################
# q->stockpile; p
# q->stockpile; p - copy & paste stockpiles
keybinding add Alt-P copystock
# q->workshop
# q->workshop - duplicate the selected job
keybinding add Ctrl-D job-duplicate
# materials: q->workshop; b->select items
@ -48,7 +59,7 @@ keybinding add Shift-O "job-material OBSIDIAN"
keybinding add Shift-T "job-material ORTHOCLASE"
keybinding add Shift-G "job-material GLASS_GREEN"
# sort units and items
# sort units and items in the on-screen list
keybinding add Alt-Shift-N "sort-units name" "sort-items description"
keybinding add Alt-Shift-R "sort-units arrival"
keybinding add Alt-Shift-T "sort-units profession" "sort-items type material"
@ -60,7 +71,7 @@ keybinding add Ctrl-M@dwarfmode/QueryBuilding/Some gui/mechanisms
# browse rooms of same owner
keybinding add Alt-R@dwarfmode/QueryBuilding/Some gui/room-list
# interface for the liquids plugin
# interface for the liquids plugin - spawn water/magma/obsidian
keybinding add Alt-L@dwarfmode/LookAround gui/liquids
# machine power sensitive pressure plate construction
@ -72,6 +83,19 @@ keybinding add Alt-A@dwarfmode/QueryBuilding/Some/SiegeEngine gui/siege-engine
# military weapon auto-select
keybinding add Ctrl-W@layer_military/Equip/Customize/View gui/choose-weapons
# minecart Guide path
keybinding add Alt-P@dwarfmode/Hauling/DefineStop/Cond/Guide gui/guide-path
# workshop job details
keybinding add Alt-A@dwarfmode/QueryBuilding/Some/Workshop/Job gui/workshop-job
# workflow front-end
keybinding add Alt-W@dwarfmode/QueryBuilding/Some/Workshop/Job gui/workflow
keybinding add Alt-W@overallstatus "gui/workflow status"
# assign weapon racks to squads so that they can be used
keybinding add P@dwarfmode/QueryBuilding/Some/Weaponrack gui/assign-rack
############################
# UI and game logic tweaks #
############################
@ -109,3 +133,39 @@ tweak fast-trade
tweak military-stable-assign
# in same list, color units already assigned to squads in brown & green
tweak military-color-assigned
# remove inverse dependency of squad training speed on unit list size and use more sparring
tweak military-training
###########
# Scripts #
###########
# write the correct season to gamelog on world load
soundsense-season
# patch the material objects in memory to fix cloth stockpiles
fix/cloth-stockpile enable
#######################################################
# Apply binary patches at runtime #
# #
# Commented out by default; enable the ones you want. #
#######################################################
# Bug 5994 - items teleported when removing a construction
#binpatch apply deconstruct-teleport
#binpatch apply deconstruct-heapfall
# Bug 4406 - hospital overstocking on all items
#binpatch apply hospital-overstocking
# Bug 808 - custom reactions completely using up all of their reagents
#binpatch apply custom-reagent-size
# Bug 4530 - marksdwarves not training when quiver full of combat-only ammo
#binpatch apply training-ammo
# Bug 1445 - weapon racks broken, armor stand capacity too low
#binpatch apply weaponrack-unassign
#binpatch apply armorstand-capacity

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

@ -111,6 +111,7 @@ include/modules/Burrows.h
include/modules/Constructions.h
include/modules/Units.h
include/modules/Engravings.h
include/modules/EventManager.h
include/modules/Gui.h
include/modules/Items.h
include/modules/Job.h
@ -121,7 +122,6 @@ include/modules/Materials.h
include/modules/Notes.h
include/modules/Screen.h
include/modules/Translation.h
include/modules/Vegetation.h
include/modules/Vermin.h
include/modules/World.h
include/modules/Graphic.h
@ -133,6 +133,7 @@ modules/Burrows.cpp
modules/Constructions.cpp
modules/Units.cpp
modules/Engravings.cpp
modules/EventManager.cpp
modules/Gui.cpp
modules/Items.cpp
modules/Job.cpp
@ -142,7 +143,6 @@ modules/Materials.cpp
modules/Notes.cpp
modules/Screen.cpp
modules/Translation.cpp
modules/Vegetation.cpp
modules/Vermin.cpp
modules/World.cpp
modules/Graphic.cpp
@ -250,6 +250,9 @@ ADD_DEPENDENCIES(dfhack-client dfhack)
ADD_EXECUTABLE(dfhack-run dfhack-run.cpp)
ADD_EXECUTABLE(binpatch binpatch.cpp)
TARGET_LINK_LIBRARIES(binpatch dfhack-md5)
IF(BUILD_EGGY)
SET_TARGET_PROPERTIES(dfhack PROPERTIES OUTPUT_NAME "egg" )
else()
@ -329,7 +332,7 @@ install(FILES xml/symbols.xml
install(FILES ../dfhack.init-example
DESTINATION ${DFHACK_BINARY_DESTINATION})
install(TARGETS dfhack-run dfhack-client
install(TARGETS dfhack-run dfhack-client binpatch
LIBRARY DESTINATION ${DFHACK_LIBRARY_DESTINATION}
RUNTIME DESTINATION ${DFHACK_LIBRARY_DESTINATION})
@ -343,6 +346,10 @@ install(DIRECTORY ${dfhack_SOURCE_DIR}/scripts
PATTERN "*.rb"
)
install(DIRECTORY ${dfhack_SOURCE_DIR}/patches
DESTINATION ${DFHACK_DATA_DESTINATION}
FILES_MATCHING PATTERN "*.dif")
# Unused for so long that it's not even relevant now...
if(BUILD_DEVEL)
if(WIN32)

@ -44,6 +44,7 @@ using namespace std;
#include "VersionInfo.h"
#include "PluginManager.h"
#include "ModuleFactory.h"
#include "modules/EventManager.h"
#include "modules/Gui.h"
#include "modules/World.h"
#include "modules/Graphic.h"
@ -316,7 +317,7 @@ static command_result runRubyScript(color_ostream &out, PluginManager *plug_mgr,
rbcmd += "'" + args[i] + "', ";
rbcmd += "]\n";
rbcmd += "load './hack/scripts/" + name + ".rb'";
rbcmd += "catch(:script_finished) { load './hack/scripts/" + name + ".rb' }";
return plug_mgr->eval_ruby(out, rbcmd.c_str());
}
@ -343,6 +344,52 @@ command_result Core::runCommand(color_ostream &out, const std::string &command)
return CR_NOT_IMPLEMENTED;
}
static bool try_autocomplete(color_ostream &con, const std::string &first, std::string &completed)
{
std::vector<std::string> possible;
auto plug_mgr = Core::getInstance().getPluginManager();
for(size_t i = 0; i < plug_mgr->size(); i++)
{
const Plugin * plug = (plug_mgr->operator[](i));
for (size_t j = 0; j < plug->size(); j++)
{
const PluginCommand &pcmd = plug->operator[](j);
if (pcmd.isHotkeyCommand())
continue;
if (pcmd.name.substr(0, first.size()) == first)
possible.push_back(pcmd.name);
}
}
bool all = (first.find('/') != std::string::npos);
std::map<string, string> scripts;
listScripts(plug_mgr, scripts, Core::getInstance().getHackPath() + "scripts/", all);
for (auto iter = scripts.begin(); iter != scripts.end(); ++iter)
if (iter->first.substr(0, first.size()) == first)
possible.push_back(iter->first);
if (possible.size() == 1)
{
completed = possible[0];
//fprintf(stderr, "Autocompleted %s to %s\n", , );
con.printerr("%s is not recognized. Did you mean %s?\n", first.c_str(), completed.c_str());
return true;
}
if (possible.size() > 1 && possible.size() < 8)
{
std::string out;
for (size_t i = 0; i < possible.size(); i++)
out += " " + possible[i];
con.printerr("%s is not recognized. Possible completions:%s\n", first.c_str(), out.c_str());
return true;
}
return false;
}
command_result Core::runCommand(color_ostream &con, const std::string &first, vector<string> &parts)
{
if (!first.empty())
@ -374,6 +421,8 @@ command_result Core::runCommand(color_ostream &con, const std::string &first, ve
" unload PLUGIN|all - Unload a plugin or all loaded plugins.\n"
" reload PLUGIN|all - Reload a plugin or all loaded plugins.\n"
);
con.print("\nDFHack version " DFHACK_VERSION ".\n");
}
else if (parts.size() == 1)
{
@ -627,8 +676,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first, ve
}
else if(first == "fpause")
{
World * w = getWorld();
w->SetPauseState(true);
World::SetPauseState(true);
con.print("The game was forced to pause!\n");
}
else if(first == "cls")
@ -664,10 +712,14 @@ command_result Core::runCommand(color_ostream &con, const std::string &first, ve
if(res == CR_NOT_IMPLEMENTED)
{
auto filename = getHackPath() + "scripts/" + first;
std::string completed;
if (fileExists(filename + ".lua"))
res = runLuaScript(con, first, parts);
else if (plug_mgr->eval_ruby && fileExists(filename + ".rb"))
res = runRubyScript(con, plug_mgr, first, parts);
else if (try_autocomplete(con, first, completed))
return CR_NOT_IMPLEMENTED;// runCommand(con, completed, parts);
else
con.printerr("%s is not a recognized command.\n", first.c_str());
}
@ -732,7 +784,6 @@ void fIOthread(void * iodata)
{
string command = "";
int ret = con.lineedit("[DFHack]# ",command, main_history);
fprintf(stderr,"Command: [%s]\n",command.c_str());
if(ret == -2)
{
cerr << "Console is shutting down properly." << endl;
@ -746,14 +797,10 @@ void fIOthread(void * iodata)
else if(ret)
{
// a proper, non-empty command was entered
fprintf(stderr,"Adding command to history\n");
main_history.add(command);
fprintf(stderr,"Saving history\n");
main_history.save("dfhack.history");
}
fprintf(stderr,"Running command\n");
auto rv = core->runCommand(con, command);
if (rv == CR_NOT_IMPLEMENTED)
@ -821,6 +868,8 @@ std::string Core::getHackPath()
#endif
}
void init_screen_module(Core *);
bool Core::Init()
{
if(started)
@ -891,6 +940,7 @@ bool Core::Init()
*/
// initialize data defs
virtual_identity::Init(this);
init_screen_module(this);
// initialize common lua context
Lua::Core::Init(con);
@ -900,6 +950,7 @@ bool Core::Init()
cerr << "Initializing Plugins.\n";
// create plugin manager
plug_mgr = new PluginManager(this);
plug_mgr->init(this);
IODATA *temp = new IODATA;
temp->core = this;
temp->plug_mgr = plug_mgr;
@ -1122,7 +1173,7 @@ void Core::doUpdate(color_ostream &out, bool first_update)
last_world_data_ptr = new_wdata;
last_local_map_ptr = new_mapdata;
getWorld()->ClearPersistentCache();
World::ClearPersistentCache();
// and if the world is going away, we report the map change first
if(had_map)
@ -1140,7 +1191,7 @@ void Core::doUpdate(color_ostream &out, bool first_update)
if (isMapLoaded() != had_map)
{
getWorld()->ClearPersistentCache();
World::ClearPersistentCache();
onStateChange(out, new_mapdata ? SC_MAP_LOADED : SC_MAP_UNLOADED);
}
}
@ -1234,6 +1285,8 @@ static int buildings_timer = 0;
void Core::onUpdate(color_ostream &out)
{
EventManager::manageEvents(out);
// convert building reagents
if (buildings_do_onupdate && (++buildings_timer & 1))
buildings_onUpdate(out);
@ -1247,6 +1300,8 @@ void Core::onUpdate(color_ostream &out)
void Core::onStateChange(color_ostream &out, state_change_event event)
{
EventManager::onStateChange(out, event);
buildings_onStateChange(out, event);
plug_mgr->OnStateChange(out, event);
@ -1611,15 +1666,27 @@ void ClassNameCheck::getKnownClassNames(std::vector<std::string> &names)
names.push_back(*it);
}
bool Process::patchMemory(void *target, const void* src, size_t count)
MemoryPatcher::MemoryPatcher(Process *p_) : p(p_)
{
if (!p)
p = Core::getInstance().p;
}
MemoryPatcher::~MemoryPatcher()
{
close();
}
bool MemoryPatcher::verifyAccess(void *target, size_t count, bool write)
{
uint8_t *sptr = (uint8_t*)target;
uint8_t *eptr = sptr + count;
// Find the valid memory ranges
std::vector<t_memrange> ranges;
getMemRanges(ranges);
if (ranges.empty())
p->getMemRanges(ranges);
// Find the ranges that this area spans
unsigned start = 0;
while (start < ranges.size() && ranges[start].end <= sptr)
start++;
@ -1642,23 +1709,45 @@ bool Process::patchMemory(void *target, const void* src, size_t count)
return false;
// Apply writable permissions & update
bool ok = true;
for (unsigned i = start; i < end && ok; i++)
for (unsigned i = start; i < end; i++)
{
t_memrange perms = ranges[i];
auto &perms = ranges[i];
if ((perms.write || !write) && perms.read)
continue;
save.push_back(perms);
perms.write = perms.read = true;
if (!setPermisions(perms, perms))
ok = false;
if (!p->setPermisions(perms, perms))
return false;
}
if (ok)
memmove(target, src, count);
return true;
}
bool MemoryPatcher::write(void *target, const void *src, size_t size)
{
if (!makeWritable(target, size))
return false;
memmove(target, src, size);
return true;
}
for (unsigned i = start; i < end && ok; i++)
setPermisions(ranges[i], ranges[i]);
void MemoryPatcher::close()
{
for (size_t i = 0; i < save.size(); i++)
p->setPermisions(save[i], save[i]);
save.clear();
ranges.clear();
};
bool Process::patchMemory(void *target, const void* src, size_t count)
{
MemoryPatcher patcher(this);
return ok;
return patcher.write(target, src, count);
}
/*******************************************************************************
@ -1678,7 +1767,6 @@ TYPE * Core::get##TYPE() \
return s_mods.p##TYPE;\
}
MODULE_GETTER(World);
MODULE_GETTER(Materials);
MODULE_GETTER(Notes);
MODULE_GETTER(Graphic);

@ -30,6 +30,7 @@ distribution.
#include "MemAccess.h"
#include "Core.h"
#include "Error.h"
#include "VersionInfo.h"
#include "tinythread.h"
// must be last due to MS stupidity
@ -81,6 +82,7 @@ distribution.
#include "df/flow_info.h"
#include "df/unit_misc_trait.h"
#include "df/proj_itemst.h"
#include "df/itemdef.h"
#include <lua.h>
#include <lauxlib.h>
@ -258,7 +260,7 @@ static PersistentDataItem persistent_by_struct(lua_State *state, int idx)
int id = lua_tointeger(state, -1);
lua_pop(state, 1);
PersistentDataItem ref = Core::getInstance().getWorld()->GetPersistentData(id);
PersistentDataItem ref = World::GetPersistentData(id);
if (ref.isValid())
{
@ -311,19 +313,19 @@ static PersistentDataItem get_persistent(lua_State *state)
if (lua_istable(state, 1))
{
Lua::StackUnwinder frame(state);
if (!lua_getmetatable(state, 1) ||
!lua_rawequal(state, -1, lua_upvalueindex(1)))
luaL_argerror(state, 1, "invalid table type");
lua_settop(state, 1);
return persistent_by_struct(state, 1);
}
else
{
const char *str = luaL_checkstring(state, 1);
return Core::getInstance().getWorld()->GetPersistentData(str);
return World::GetPersistentData(str);
}
}
@ -342,7 +344,7 @@ static int dfhack_persistent_delete(lua_State *state)
auto ref = get_persistent(state);
bool ok = Core::getInstance().getWorld()->DeletePersistentData(ref);
bool ok = World::DeletePersistentData(ref);
lua_pushboolean(state, ok);
return 1;
@ -356,7 +358,7 @@ static int dfhack_persistent_get_all(lua_State *state)
bool prefix = (lua_gettop(state)>=2 ? lua_toboolean(state,2) : false);
std::vector<PersistentDataItem> data;
Core::getInstance().getWorld()->GetPersistentData(&data, str, prefix);
World::GetPersistentData(&data, str, prefix);
if (data.empty())
{
@ -396,7 +398,7 @@ static int dfhack_persistent_save(lua_State *state)
if (add)
{
ref = Core::getInstance().getWorld()->AddPersistentData(str);
ref = World::AddPersistentData(str);
added = true;
}
else if (lua_getmetatable(state, 1))
@ -409,13 +411,13 @@ static int dfhack_persistent_save(lua_State *state)
}
else
{
ref = Core::getInstance().getWorld()->GetPersistentData(str);
ref = World::GetPersistentData(str);
}
// Auto-add if not found
if (!ref.isValid())
{
ref = Core::getInstance().getWorld()->AddPersistentData(str);
ref = World::AddPersistentData(str);
if (!ref.isValid())
luaL_error(state, "cannot create persistent entry");
added = true;
@ -446,11 +448,38 @@ static int dfhack_persistent_save(lua_State *state)
return 2;
}
static int dfhack_persistent_getTilemask(lua_State *state)
{
CoreSuspender suspend;
lua_settop(state, 3);
auto ref = get_persistent(state);
auto block = Lua::CheckDFObject<df::map_block>(state, 2);
bool create = lua_toboolean(state, 3);
Lua::PushDFObject(state, World::getPersistentTilemask(ref, block, create));
return 1;
}
static int dfhack_persistent_deleteTilemask(lua_State *state)
{
CoreSuspender suspend;
lua_settop(state, 2);
auto ref = get_persistent(state);
auto block = Lua::CheckDFObject<df::map_block>(state, 2);
lua_pushboolean(state, World::deletePersistentTilemask(ref, block));
return 1;
}
static const luaL_Reg dfhack_persistent_funcs[] = {
{ "get", dfhack_persistent_get },
{ "delete", dfhack_persistent_delete },
{ "get_all", dfhack_persistent_get_all },
{ "save", dfhack_persistent_save },
{ "getTilemask", dfhack_persistent_getTilemask },
{ "deleteTilemask", dfhack_persistent_deleteTilemask },
{ NULL, NULL }
};
@ -471,7 +500,9 @@ static void OpenPersistent(lua_State *state)
* Material info lookup *
************************/
static void push_matinfo(lua_State *state, MaterialInfo &info)
static int DFHACK_MATINFO_TOKEN = 0;
void Lua::Push(lua_State *state, MaterialInfo &info)
{
if (!info.isValid())
{
@ -480,7 +511,7 @@ static void push_matinfo(lua_State *state, MaterialInfo &info)
}
lua_newtable(state);
lua_pushvalue(state, lua_upvalueindex(1));
lua_rawgetp(state, LUA_REGISTRYINDEX, &DFHACK_MATINFO_TOKEN);
lua_setmetatable(state, -2);
lua_pushinteger(state, info.type);
@ -535,7 +566,7 @@ static int dfhack_matinfo_find(lua_State *state)
info.find(tokens);
}
push_matinfo(state, info);
Lua::Push(state, info);
return 1;
}
@ -603,7 +634,7 @@ static int dfhack_matinfo_decode(lua_State *state)
{
MaterialInfo info;
decode_matinfo(state, &info, true);
push_matinfo(state, info);
Lua::Push(state, info);
return 1;
}
@ -682,6 +713,9 @@ static void OpenMatinfo(lua_State *state)
{
luaL_getsubtable(state, lua_gettop(state), "matinfo");
lua_dup(state);
lua_rawsetp(state, LUA_REGISTRYINDEX, &DFHACK_MATINFO_TOKEN);
lua_dup(state);
luaL_setfuncs(state, dfhack_matinfo_funcs, 1);
@ -691,6 +725,316 @@ static void OpenMatinfo(lua_State *state)
lua_pop(state, 1);
}
/**************
* Pen object *
**************/
static int DFHACK_PEN_TOKEN = 0;
void Lua::Push(lua_State *L, const Screen::Pen &info)
{
if (!info.valid())
{
lua_pushnil(L);
return;
}
void *pdata = lua_newuserdata(L, sizeof(Pen));
lua_rawgetp(L, LUA_REGISTRYINDEX, &DFHACK_PEN_TOKEN);
lua_setmetatable(L, -2);
new (pdata) Pen(info);
}
static Pen *check_pen_native(lua_State *L, int index)
{
lua_rawgetp(L, LUA_REGISTRYINDEX, &DFHACK_PEN_TOKEN);
if (!lua_getmetatable(L, index) || !lua_rawequal(L, -1, -2))
luaL_argerror(L, index, "not a pen object");
lua_pop(L, 2);
return (Pen*)lua_touserdata(L, index);
}
void Lua::CheckPen(lua_State *L, Screen::Pen *pen, int index, bool allow_nil, bool allow_color)
{
index = lua_absindex(L, index);
luaL_checkany(L, index);
if (lua_isnil(L, index))
{
if (!allow_nil)
luaL_argerror(L, index, "nil pen not allowed");
*pen = Pen(0,0,0,-1);
}
else if (lua_isuserdata(L, index))
{
*pen = *check_pen_native(L, index);
}
else if (allow_color && lua_isnumber(L, index))
{
*pen = Pen(0, lua_tointeger(L, index)&15, 0);
}
else
{
luaL_checktype(L, index, LUA_TTABLE);
decode_pen(L, *pen, index);
}
}
static int adjust_pen(lua_State *L, bool no_copy)
{
lua_settop(L, 4);
Pen pen;
int iidx = 1;
Lua::CheckPen(L, &pen, 1, true, true);
if (!lua_isnil(L, 2) || !lua_isnil(L, 3) || !lua_isnil(L, 4))
{
if (lua_isnumber(L, 2) || lua_isnil(L, 2))
{
if (!pen.valid())
pen = Pen();
iidx = -1;
pen.fg = luaL_optint(L, 2, pen.fg) & 15;
pen.bg = luaL_optint(L, 3, pen.bg);
if (!lua_isnil(L, 4))
pen.bold = lua_toboolean(L, 4);
else if (!lua_isnil(L, 2))
{
pen.bold = !!(pen.fg & 8);
pen.fg &= 7;
}
}
else
{
iidx = 2;
Lua::CheckPen(L, &pen, 2, false, false);
}
}
if (no_copy && iidx > 0 && lua_isuserdata(L, iidx))
lua_pushvalue(L, iidx);
else
Lua::Push(L, pen);
return 1;
}
static int dfhack_pen_parse(lua_State *L)
{
return adjust_pen(L, true);
}
static int dfhack_pen_make(lua_State *L)
{
return adjust_pen(L, false);
}
static void make_pen_table(lua_State *L, Pen &pen)
{
if (!pen.valid())
luaL_error(L, "invalid pen state");
else
{
lua_newtable(L);
lua_pushinteger(L, (unsigned char)pen.ch); lua_setfield(L, -2, "ch");
lua_pushinteger(L, pen.fg); lua_setfield(L, -2, "fg");
lua_pushinteger(L, pen.bg); lua_setfield(L, -2, "bg");
lua_pushboolean(L, pen.bold); lua_setfield(L, -2, "bold");
if (pen.tile)
{
lua_pushinteger(L, pen.tile); lua_setfield(L, -2, "tile");
}
switch (pen.tile_mode) {
case Pen::CharColor:
lua_pushboolean(L, true); lua_setfield(L, -2, "tile_color");
break;
case Pen::TileColor:
lua_pushinteger(L, pen.tile_fg); lua_setfield(L, -2, "tile_fg");
lua_pushinteger(L, pen.tile_bg); lua_setfield(L, -2, "tile_bg");
break;
default:
lua_pushboolean(L, false); lua_setfield(L, -2, "tile_color");
break;
}
}
}
static void get_pen_mirror(lua_State *L, int idx)
{
lua_getuservalue(L, idx);
if (lua_isnil(L, -1))
{
lua_pop(L, 1);
Pen pen;
Lua::CheckPen(L, &pen, idx, false, false);
make_pen_table(L, pen);
lua_dup(L);
lua_setuservalue(L, idx);
}
}
static int dfhack_pen_index(lua_State *L)
{
lua_settop(L, 2);
luaL_checktype(L, 1, LUA_TUSERDATA);
// check metatable
if (!lua_getmetatable(L, 1))
luaL_argerror(L, 1, "must be a pen");
lua_pushvalue(L, 2);
lua_rawget(L, -2);
if (!lua_isnil(L, -1))
return 1;
// otherwise read from the mirror table, creating it if necessary
lua_settop(L, 2);
get_pen_mirror(L, 1);
lua_pushvalue(L, 2);
lua_rawget(L, -2);
return 1;
}
static int pen_pnext(lua_State *L)
{
lua_settop(L, 2); /* create a 2nd argument if there isn't one */
if (lua_next(L, lua_upvalueindex(1)))
return 2;
lua_pushnil(L);
return 1;
}
static int dfhack_pen_pairs(lua_State *L)
{
luaL_checktype(L, 1, LUA_TUSERDATA);
get_pen_mirror(L, 1);
lua_pushcclosure(L, pen_pnext, 1);
lua_pushnil(L);
lua_pushnil(L);
return 3;
}
const char *const pen_fields[] = {
"ch", "fg", "bold", "bg", "tile", "tile_color", "tile_fg", "tile_bg", NULL
};
static int dfhack_pen_newindex(lua_State *L)
{
lua_settop(L, 3);
luaL_checktype(L, 1, LUA_TUSERDATA);
int id = luaL_checkoption(L, 2, NULL, pen_fields);
int arg = 0;
Pen &pen = *check_pen_native(L, 1);
bool wipe_tile = false, wipe_tc = false;
switch (id) {
case 0:
if (lua_type(L, 3) != LUA_TNUMBER)
arg = (unsigned char)*luaL_checkstring(L, 3);
else
arg = luaL_checkint(L, 3);
pen.ch = arg;
lua_pushinteger(L, (unsigned char)pen.ch);
break;
case 1:
pen.fg = luaL_checkint(L, 3) & 15;
lua_pushinteger(L, pen.fg);
break;
case 2:
pen.bold = lua_toboolean(L, 3);
lua_pushboolean(L, pen.bold);
break;
case 3:
pen.bg = luaL_checkint(L, 3) & 15;
lua_pushinteger(L, pen.bg);
break;
case 4:
arg = lua_isnil(L, 3) ? 0 : luaL_checkint(L, 3);
if (arg < 0)
luaL_argerror(L, 3, "invalid tile index");
pen.tile = arg;
if (pen.tile)
lua_pushinteger(L, pen.tile);
else
lua_pushnil(L);
break;
case 5:
wipe_tile = (pen.tile_mode == Pen::TileColor);
pen.tile_mode = lua_toboolean(L, 3) ? Pen::CharColor : Pen::AsIs;
lua_pushboolean(L, pen.tile_mode == Pen::CharColor);
break;
case 6:
if (pen.tile_mode != Pen::TileColor) { wipe_tc = true; pen.tile_bg = 0; }
pen.tile_fg = luaL_checkint(L, 3) & 15;
pen.tile_mode = Pen::TileColor;
lua_pushinteger(L, pen.tile_fg);
break;
case 7:
if (pen.tile_mode != Pen::TileColor) { wipe_tc = true; pen.tile_fg = 7; }
pen.tile_bg = luaL_checkint(L, 3) & 15;
pen.tile_mode = Pen::TileColor;
lua_pushinteger(L, pen.tile_bg);
break;
}
lua_getuservalue(L, 1);
if (!lua_isnil(L, -1))
{
lua_remove(L, 3);
lua_insert(L, 2);
lua_rawset(L, 2);
if (wipe_tc) {
lua_pushnil(L); lua_setfield(L, 2, "tile_color");
lua_pushinteger(L, pen.tile_fg); lua_setfield(L, 2, "tile_fg");
lua_pushinteger(L, pen.tile_bg); lua_setfield(L, 2, "tile_bg");
}
if (wipe_tile) {
lua_pushnil(L); lua_setfield(L, 2, "tile_fg");
lua_pushnil(L); lua_setfield(L, 2, "tile_bg");
}
}
return 0;
}
static const luaL_Reg dfhack_pen_funcs[] = {
{ "parse", dfhack_pen_parse },
{ "make", dfhack_pen_make },
{ "__index", dfhack_pen_index },
{ "__pairs", dfhack_pen_pairs },
{ "__newindex", dfhack_pen_newindex },
{ NULL, NULL }
};
static void OpenPen(lua_State *state)
{
luaL_getsubtable(state, lua_gettop(state), "pen");
lua_dup(state);
lua_rawsetp(state, LUA_REGISTRYINDEX, &DFHACK_PEN_TOKEN);
luaL_setfuncs(state, dfhack_pen_funcs, 0);
lua_pop(state, 1);
}
/************************
* Wrappers for C++ API *
************************/
@ -777,10 +1121,14 @@ static const LuaWrapper::FunctionReg dfhack_job_module[] = {
WRAPM(Job,cloneJobStruct),
WRAPM(Job,printItemDetails),
WRAPM(Job,printJobDetails),
WRAPM(Job,getGeneralRef),
WRAPM(Job,getSpecificRef),
WRAPM(Job,getHolder),
WRAPM(Job,getWorker),
WRAPM(Job,checkBuildingsNow),
WRAPM(Job,checkDesignationsNow),
WRAPM(Job,isSuitableItem),
WRAPM(Job,isSuitableMaterial),
WRAPN(is_equal, jobEqual),
WRAPN(is_item_equal, jobItemEqual),
{ NULL, NULL }
@ -811,6 +1159,8 @@ static const luaL_Reg dfhack_job_funcs[] = {
/***** Units module *****/
static const LuaWrapper::FunctionReg dfhack_units_module[] = {
WRAPM(Units, getGeneralRef),
WRAPM(Units, getSpecificRef),
WRAPM(Units, getContainer),
WRAPM(Units, setNickname),
WRAPM(Units, getVisibleName),
@ -830,6 +1180,7 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = {
WRAPM(Units, getAge),
WRAPM(Units, getNominalSkill),
WRAPM(Units, getEffectiveSkill),
WRAPM(Units, getExperience),
WRAPM(Units, computeMovementSpeed),
WRAPM(Units, getProfessionName),
WRAPM(Units, getCasteProfessionName),
@ -906,7 +1257,12 @@ static const LuaWrapper::FunctionReg dfhack_items_module[] = {
WRAPM(Items, getOwner),
WRAPM(Items, setOwner),
WRAPM(Items, getContainer),
WRAPM(Items, getHolderBuilding),
WRAPM(Items, getHolderUnit),
WRAPM(Items, getDescription),
WRAPM(Items, isCasteMaterial),
WRAPM(Items, getSubtypeCount),
WRAPM(Items, getSubtypeDef),
WRAPN(moveToGround, items_moveToGround),
WRAPN(moveToContainer, items_moveToContainer),
WRAPN(moveToBuilding, items_moveToBuilding),
@ -937,6 +1293,22 @@ static const luaL_Reg dfhack_items_funcs[] = {
/***** Maps module *****/
static bool hasTileAssignment(df::tile_bitmask *bm) {
return bm && bm->has_assignments();
}
static bool getTileAssignment(df::tile_bitmask *bm, int x, int y) {
return bm && bm->getassignment(x,y);
}
static void setTileAssignment(df::tile_bitmask *bm, int x, int y, bool val) {
CHECK_NULL_POINTER(bm);
bm->setassignment(x,y,val);
}
static void resetTileAssignment(df::tile_bitmask *bm, bool val) {
CHECK_NULL_POINTER(bm);
if (val) bm->set_all();
else bm->clear();
}
static const LuaWrapper::FunctionReg dfhack_maps_module[] = {
WRAPN(getBlock, (df::map_block* (*)(int32_t,int32_t,int32_t))Maps::getBlock),
WRAPM(Maps, enableBlockUpdates),
@ -944,6 +1316,10 @@ static const LuaWrapper::FunctionReg dfhack_maps_module[] = {
WRAPM(Maps, getLocalInitFeature),
WRAPM(Maps, canWalkBetween),
WRAPM(Maps, spawnFlow),
WRAPN(hasTileAssignment, hasTileAssignment),
WRAPN(getTileAssignment, getTileAssignment),
WRAPN(setTileAssignment, setTileAssignment),
WRAPN(resetTileAssignment, resetTileAssignment),
{ NULL, NULL }
};
@ -968,6 +1344,25 @@ static int maps_ensureTileBlock(lua_State *L)
return 1;
}
static int maps_getTileType(lua_State *L)
{
auto pos = CheckCoordXYZ(L, 1, true);
auto ptype = Maps::getTileType(pos);
if (ptype)
lua_pushinteger(L, *ptype);
else
lua_pushnil(L);
return 1;
}
static int maps_getTileFlags(lua_State *L)
{
auto pos = CheckCoordXYZ(L, 1, true);
Lua::PushDFObject(L, Maps::getTileDesignation(pos));
Lua::PushDFObject(L, Maps::getTileOccupancy(pos));
return 2;
}
static int maps_getRegionBiome(lua_State *L)
{
auto pos = CheckCoordXY(L, 1, true);
@ -985,6 +1380,8 @@ static const luaL_Reg dfhack_maps_funcs[] = {
{ "isValidTilePos", maps_isValidTilePos },
{ "getTileBlock", maps_getTileBlock },
{ "ensureTileBlock", maps_ensureTileBlock },
{ "getTileType", maps_getTileType },
{ "getTileFlags", maps_getTileFlags },
{ "getRegionBiome", maps_getRegionBiome },
{ "getTileBiomeRgn", maps_getTileBiomeRgn },
{ NULL, NULL }
@ -1035,6 +1432,8 @@ static bool buildings_containsTile(df::building *bld, int x, int y, bool room) {
}
static const LuaWrapper::FunctionReg dfhack_buildings_module[] = {
WRAPM(Buildings, getGeneralRef),
WRAPM(Buildings, getSpecificRef),
WRAPM(Buildings, setOwner),
WRAPM(Buildings, allocInstance),
WRAPM(Buildings, checkFreeTiles),
@ -1146,6 +1545,7 @@ static const LuaWrapper::FunctionReg dfhack_screen_module[] = {
WRAPM(Screen, inGraphicsMode),
WRAPM(Screen, clear),
WRAPM(Screen, invalidate),
WRAPM(Screen, getKeyDisplay),
{ NULL, NULL }
};
@ -1168,7 +1568,7 @@ static int screen_getWindowSize(lua_State *L)
static int screen_paintTile(lua_State *L)
{
Pen pen;
decode_pen(L, pen, 1);
Lua::CheckPen(L, &pen, 1);
int x = luaL_checkint(L, 2);
int y = luaL_checkint(L, 3);
if (lua_gettop(L) >= 4 && !lua_isnil(L, 4))
@ -1189,44 +1589,14 @@ static int screen_readTile(lua_State *L)
int x = luaL_checkint(L, 1);
int y = luaL_checkint(L, 2);
Pen pen = Screen::readTile(x, y);
if (!pen.valid())
{
lua_pushnil(L);
}
else
{
lua_newtable(L);
lua_pushinteger(L, pen.ch); lua_setfield(L, -2, "ch");
lua_pushinteger(L, pen.fg); lua_setfield(L, -2, "fg");
lua_pushinteger(L, pen.bg); lua_setfield(L, -2, "bg");
lua_pushboolean(L, pen.bold); lua_setfield(L, -2, "bold");
if (pen.tile)
{
lua_pushinteger(L, pen.tile); lua_setfield(L, -2, "tile");
switch (pen.tile_mode) {
case Pen::CharColor:
lua_pushboolean(L, true); lua_setfield(L, -2, "tile_color");
break;
case Pen::TileColor:
lua_pushinteger(L, pen.tile_fg); lua_setfield(L, -2, "tile_fg");
lua_pushinteger(L, pen.tile_bg); lua_setfield(L, -2, "tile_bg");
break;
default:
break;
}
}
}
Lua::Push(L, pen);
return 1;
}
static int screen_paintString(lua_State *L)
{
Pen pen;
decode_pen(L, pen, 1);
Lua::CheckPen(L, &pen, 1);
int x = luaL_checkint(L, 2);
int y = luaL_checkint(L, 3);
const char *text = luaL_checkstring(L, 4);
@ -1237,7 +1607,7 @@ static int screen_paintString(lua_State *L)
static int screen_fillRect(lua_State *L)
{
Pen pen;
decode_pen(L, pen, 1);
Lua::CheckPen(L, &pen, 1);
int x1 = luaL_checkint(L, 2);
int y1 = luaL_checkint(L, 3);
int x2 = luaL_checkint(L, 4);
@ -1355,9 +1725,11 @@ static void *checkaddr(lua_State *L, int idx, bool allow_null = false)
return rv;
}
static uint32_t getImageBase() { return Core::getInstance().p->getBase(); }
static int getRebaseDelta() { return Core::getInstance().vinfo->getRebaseDelta(); }
static const LuaWrapper::FunctionReg dfhack_internal_module[] = {
WRAP(getImageBase),
WRAP(getRebaseDelta),
{ NULL, NULL }
};
@ -1411,6 +1783,18 @@ static int internal_getVTable(lua_State *L)
return 1;
}
static int internal_adjustOffset(lua_State *L)
{
lua_settop(L, 2);
int off = luaL_checkint(L, 1);
int rv = Core::getInstance().p->adjustOffset(off, lua_toboolean(L, 2));
if (rv >= 0)
lua_pushinteger(L, rv);
else
lua_pushnil(L);
return 1;
}
static int internal_getMemRanges(lua_State *L)
{
std::vector<DFHack::t_memrange> ranges;
@ -1454,6 +1838,81 @@ static int internal_patchMemory(lua_State *L)
return 1;
}
static int internal_patchBytes(lua_State *L)
{
luaL_checktype(L, 1, LUA_TTABLE);
lua_settop(L, 2);
MemoryPatcher patcher;
if (!lua_isnil(L, 2))
{
luaL_checktype(L, 2, LUA_TTABLE);
lua_pushnil(L);
while (lua_next(L, 2))
{
uint8_t *addr = (uint8_t*)checkaddr(L, -2, true);
int isnum;
uint8_t value = (uint8_t)lua_tounsignedx(L, -1, &isnum);
if (!isnum)
luaL_error(L, "invalid value in verify table");
lua_pop(L, 1);
if (!patcher.verifyAccess(addr, 1, false))
{
lua_pushnil(L);
lua_pushstring(L, "invalid verify address");
lua_pushvalue(L, -3);
return 3;
}
if (*addr != value)
{
lua_pushnil(L);
lua_pushstring(L, "wrong verify value");
lua_pushvalue(L, -3);
return 3;
}
}
}
lua_pushnil(L);
while (lua_next(L, 1))
{
uint8_t *addr = (uint8_t*)checkaddr(L, -2, true);
int isnum;
uint8_t value = (uint8_t)lua_tounsignedx(L, -1, &isnum);
if (!isnum)
luaL_error(L, "invalid value in write table");
lua_pop(L, 1);
if (!patcher.verifyAccess(addr, 1, true))
{
lua_pushnil(L);
lua_pushstring(L, "invalid write address");
lua_pushvalue(L, -3);
return 3;
}
}
lua_pushnil(L);
while (lua_next(L, 1))
{
uint8_t *addr = (uint8_t*)checkaddr(L, -2, true);
uint8_t value = (uint8_t)lua_tounsigned(L, -1);
lua_pop(L, 1);
*addr = value;
}
lua_pushboolean(L, true);
return 1;
}
static int internal_memmove(lua_State *L)
{
void *dest = checkaddr(L, 1);
@ -1543,8 +2002,10 @@ static const luaL_Reg dfhack_internal_funcs[] = {
{ "getAddress", internal_getAddress },
{ "setAddress", internal_setAddress },
{ "getVTable", internal_getVTable },
{ "adjustOffset", internal_adjustOffset },
{ "getMemRanges", internal_getMemRanges },
{ "patchMemory", internal_patchMemory },
{ "patchBytes", internal_patchBytes },
{ "memmove", internal_memmove },
{ "memcmp", internal_memcmp },
{ "memscan", internal_memscan },
@ -1561,6 +2022,7 @@ void OpenDFHackApi(lua_State *state)
{
OpenPersistent(state);
OpenMatinfo(state);
OpenPen(state);
LuaWrapper::SetFunctionWrappers(state, dfhack_module);
OpenModule(state, "gui", dfhack_gui_module);

@ -107,7 +107,8 @@ static void signal_typeid_error(color_ostream *out, lua_State *state,
type_identity *type, const char *msg,
int val_index, bool perr, bool signal)
{
std::string error = stl_sprintf(msg, type->getFullName().c_str());
std::string typestr = type ? type->getFullName() : "any pointer";
std::string error = stl_sprintf(msg, typestr.c_str());
if (signal)
{
@ -134,6 +135,8 @@ void *DFHack::Lua::CheckDFObject(lua_State *state, type_identity *type, int val_
if (lua_isnil(state, val_index))
return NULL;
if (lua_islightuserdata(state, val_index) && !lua_touserdata(state, val_index))
return NULL;
void *rv = get_object_internal(state, type, val_index, exact_type, false);
@ -1548,6 +1551,10 @@ void DFHack::Lua::Notification::bind(lua_State *state, const char *name)
void OpenDFHackApi(lua_State *state);
namespace DFHack { namespace Lua { namespace Core {
static void InitCoreContext();
}}}
lua_State *DFHack::Lua::Open(color_ostream &out, lua_State *state)
{
if (!state)
@ -1651,6 +1658,10 @@ lua_State *DFHack::Lua::Open(color_ostream &out, lua_State *state)
lua_dup(state);
lua_rawseti(state, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS);
// Init core-context specific stuff before loading dfhack.lua
if (IsCoreContext(state))
Lua::Core::InitCoreContext();
// load dfhack.lua
Require(out, state, "dfhack");
@ -1826,8 +1837,12 @@ void DFHack::Lua::Core::Init(color_ostream &out)
State = luaL_newstate();
// Calls InitCoreContext after checking IsCoreContext
Lua::Open(out, State);
}
static void Lua::Core::InitCoreContext()
{
lua_newtable(State);
lua_rawsetp(State, LUA_REGISTRYINDEX, &DFHACK_TIMEOUTS_TOKEN);

@ -1265,6 +1265,51 @@ static void BuildTypeMetatable(lua_State *state, type_identity *type)
* Recursive walk of scopes to construct the df... tree.
*/
static int wtype_pnext(lua_State *L)
{
lua_settop(L, 2); /* create a 2nd argument if there isn't one */
if (lua_next(L, lua_upvalueindex(1)))
return 2;
lua_pushnil(L);
return 1;
}
static int wtype_pairs(lua_State *state)
{
lua_pushvalue(state, lua_upvalueindex(1));
lua_pushcclosure(state, wtype_pnext, 1);
lua_pushnil(state);
lua_pushnil(state);
return 3;
}
static int wtype_inext(lua_State *L)
{
int i = luaL_checkint(L, 2);
i++; /* next value */
if (i <= lua_tointeger(L, lua_upvalueindex(2)))
{
lua_pushinteger(L, i);
lua_rawgeti(L, lua_upvalueindex(1), i);
return 2;
}
else
{
lua_pushnil(L);
return 1;
}
}
static int wtype_ipairs(lua_State *state)
{
lua_pushvalue(state, lua_upvalueindex(1));
lua_pushvalue(state, lua_upvalueindex(3));
lua_pushcclosure(state, wtype_inext, 2);
lua_pushnil(state);
lua_pushvalue(state, lua_upvalueindex(2));
return 3;
}
static void RenderTypeChildren(lua_State *state, const std::vector<compound_identity*> &children);
void LuaWrapper::AssociateId(lua_State *state, int table, int val, const char *name)
@ -1278,7 +1323,7 @@ void LuaWrapper::AssociateId(lua_State *state, int table, int val, const char *n
lua_rawset(state, table);
}
static void FillEnumKeys(lua_State *state, int ftable, enum_identity *eid)
static void FillEnumKeys(lua_State *state, int ix_meta, int ftable, enum_identity *eid)
{
const char *const *keys = eid->getKeys();
@ -1296,11 +1341,17 @@ static void FillEnumKeys(lua_State *state, int ftable, enum_identity *eid)
if (eid->getFirstItem() <= eid->getLastItem())
{
lua_pushvalue(state, base+1);
lua_pushinteger(state, eid->getFirstItem()-1);
lua_pushinteger(state, eid->getLastItem());
lua_pushcclosure(state, wtype_ipairs, 3);
lua_setfield(state, ix_meta, "__ipairs");
lua_pushinteger(state, eid->getFirstItem());
lua_setfield(state, base+1, "_first_item");
lua_setfield(state, ftable, "_first_item");
lua_pushinteger(state, eid->getLastItem());
lua_setfield(state, base+1, "_last_item");
lua_setfield(state, ftable, "_last_item");
}
SaveInTable(state, eid, &DFHACK_ENUM_TABLE_TOKEN);
@ -1321,7 +1372,7 @@ static void FillEnumKeys(lua_State *state, int ftable, enum_identity *eid)
lua_setmetatable(state, ftable);
}
static void FillBitfieldKeys(lua_State *state, int ftable, bitfield_identity *eid)
static void FillBitfieldKeys(lua_State *state, int ix_meta, int ftable, bitfield_identity *eid)
{
// Create a new table attached to ftable as __index
lua_newtable(state);
@ -1338,11 +1389,17 @@ static void FillBitfieldKeys(lua_State *state, int ftable, bitfield_identity *ei
i += bits[i].size-1;
}
lua_pushvalue(state, base+1);
lua_pushinteger(state, -1);
lua_pushinteger(state, eid->getNumBits()-1);
lua_pushcclosure(state, wtype_ipairs, 3);
lua_setfield(state, ix_meta, "__ipairs");
lua_pushinteger(state, 0);
lua_setfield(state, base+1, "_first_item");
lua_setfield(state, ftable, "_first_item");
lua_pushinteger(state, eid->getNumBits()-1);
lua_setfield(state, base+1, "_last_item");
lua_setfield(state, ftable, "_last_item");
SaveInTable(state, eid, &DFHACK_ENUM_TABLE_TOKEN);
@ -1355,7 +1412,12 @@ static void RenderType(lua_State *state, compound_identity *node)
assert(node->getName());
std::string name = node->getFullName();
int base = lua_gettop(state);
// Frame:
// base+1 - outer table
// base+2 - metatable of outer table
// base+3 - inner table
// base+4 - pairs table
Lua::StackUnwinder base(state);
lua_newtable(state);
if (!lua_checkstack(state, 20))
@ -1365,51 +1427,59 @@ static void RenderType(lua_State *state, compound_identity *node)
// metatable
lua_newtable(state);
int ix_meta = base+2;
lua_dup(state);
lua_setmetatable(state, base+1);
lua_pushstring(state, name.c_str());
lua_setfield(state, base+2, "__metatable");
lua_setfield(state, ix_meta, "__metatable");
lua_getfield(state, LUA_REGISTRYINDEX, DFHACK_TYPE_TOSTRING_NAME);
lua_setfield(state, base+2, "__tostring");
lua_setfield(state, ix_meta, "__tostring");
lua_pushlightuserdata(state, node);
lua_rawsetp(state, base+2, &DFHACK_IDENTITY_FIELD_TOKEN);
lua_rawsetp(state, ix_meta, &DFHACK_IDENTITY_FIELD_TOKEN);
// inner table
lua_newtable(state);
int ftable = base+3;
lua_dup(state);
lua_setfield(state, base+2, "__index");
lua_setfield(state, ix_meta, "__index");
int ftable = base+3;
// pairs table
lua_newtable(state);
int ptable = base+4;
lua_pushvalue(state, ptable);
lua_pushcclosure(state, wtype_pairs, 1);
lua_setfield(state, ix_meta, "__pairs");
switch (node->type())
{
case IDTYPE_STRUCT:
lua_pushstring(state, "struct-type");
lua_setfield(state, ftable, "_kind");
IndexStatics(state, base+2, base+3, (struct_identity*)node);
IndexStatics(state, ix_meta, ftable, (struct_identity*)node);
break;
case IDTYPE_CLASS:
lua_pushstring(state, "class-type");
lua_setfield(state, ftable, "_kind");
IndexStatics(state, base+2, base+3, (struct_identity*)node);
IndexStatics(state, ix_meta, ftable, (struct_identity*)node);
break;
case IDTYPE_ENUM:
lua_pushstring(state, "enum-type");
lua_setfield(state, ftable, "_kind");
FillEnumKeys(state, ftable, (enum_identity*)node);
FillEnumKeys(state, ix_meta, ftable, (enum_identity*)node);
break;
case IDTYPE_BITFIELD:
lua_pushstring(state, "bitfield-type");
lua_setfield(state, ftable, "_kind");
FillBitfieldKeys(state, ftable, (bitfield_identity*)node);
FillBitfieldKeys(state, ix_meta, ftable, (bitfield_identity*)node);
break;
case IDTYPE_GLOBAL:
@ -1425,14 +1495,14 @@ static void RenderType(lua_State *state, compound_identity *node)
BuildTypeMetatable(state, node);
lua_dup(state);
lua_setmetatable(state, base+3);
lua_setmetatable(state, ftable);
lua_getfield(state, -1, "__newindex");
lua_setfield(state, base+2, "__newindex");
lua_setfield(state, ix_meta, "__newindex");
lua_getfield(state, -1, "__pairs");
lua_setfield(state, base+2, "__pairs");
lua_setfield(state, ix_meta, "__pairs");
lua_pop(state, 3);
base += 1;
return;
}
@ -1454,17 +1524,25 @@ static void RenderType(lua_State *state, compound_identity *node)
lua_getfield(state, LUA_REGISTRYINDEX, DFHACK_IS_INSTANCE_NAME);
lua_setfield(state, ftable, "is_instance");
lua_pop(state, 2);
base += 1;
}
static void RenderTypeChildren(lua_State *state, const std::vector<compound_identity*> &children)
{
// fieldtable pairstable |
int base = lua_gettop(state);
for (size_t i = 0; i < children.size(); i++)
{
RenderType(state, children[i]);
lua_pushstring(state, children[i]->getName());
lua_swap(state);
lua_rawset(state, -3);
// save in both tables
lua_pushvalue(state, -2);
lua_pushvalue(state, -2);
lua_rawset(state, base);
lua_rawset(state, base-1);
}
}
@ -1524,10 +1602,13 @@ static int DoAttach(lua_State *state)
{
// Assign df a metatable with read-only contents
lua_newtable(state);
lua_newtable(state);
// Render the type structure
RenderTypeChildren(state, compound_identity::getTopScope());
lua_swap(state); // -> pairstable fieldtable
lua_getfield(state, LUA_REGISTRYINDEX, DFHACK_SIZEOF_NAME);
lua_setfield(state, -2, "sizeof");
lua_getfield(state, LUA_REGISTRYINDEX, DFHACK_NEW_NAME);
@ -1558,7 +1639,15 @@ static int DoAttach(lua_State *state)
lua_pushcfunction(state, meta_isnull);
lua_setfield(state, -2, "isnull");
freeze_table(state, false, "df");
freeze_table(state, true, "df");
// pairstable dftable dfmeta
lua_pushvalue(state, -3);
lua_pushcclosure(state, wtype_pairs, 1);
lua_setfield(state, -2, "__pairs");
lua_pop(state, 1);
lua_remove(state, -2);
}
return 1;

@ -148,6 +148,11 @@ bool prefix_matches(const std::string &prefix, const std::string &key, std::stri
return false;
}
int random_int(int max)
{
return int(int64_t(rand())*max/(int64_t(RAND_MAX)+1));
}
#ifdef LINUX_BUILD // Linux
uint64_t GetTimeMs64()
{

@ -22,6 +22,7 @@ must not be misrepresented as being the original software.
distribution.
*/
#include "modules/EventManager.h"
#include "Internal.h"
#include "Core.h"
#include "MemAccess.h"
@ -211,9 +212,10 @@ bool Plugin::load(color_ostream &con)
}
const char ** plug_name =(const char ** ) LookupPlugin(plug, "name");
const char ** plug_version =(const char ** ) LookupPlugin(plug, "version");
if(!plug_name || !plug_version)
Plugin **plug_self = (Plugin**)LookupPlugin(plug, "plugin_self");
if(!plug_name || !plug_version || !plug_self)
{
con.printerr("Plugin %s has no name or version.\n", filename.c_str());
con.printerr("Plugin %s has no name, version or self pointer.\n", filename.c_str());
ClosePlugin(plug);
RefAutolock lock(access);
state = PS_BROKEN;
@ -228,6 +230,7 @@ bool Plugin::load(color_ostream &con)
state = PS_BROKEN;
return false;
}
*plug_self = this;
RefAutolock lock(access);
plugin_init = (command_result (*)(color_ostream &, std::vector <PluginCommand> &)) LookupPlugin(plug, "plugin_init");
if(!plugin_init)
@ -270,6 +273,7 @@ bool Plugin::unload(color_ostream &con)
// if we are actually loaded
if(state == PS_LOADED)
{
EventManager::unregisterAll(this);
// notify the plugin about an attempt to shutdown
if (plugin_onstatechange &&
plugin_onstatechange(con, SC_BEGIN_UNLOAD) == CR_NOT_FOUND)
@ -598,6 +602,22 @@ void Plugin::push_function(lua_State *state, LuaFunction *fn)
}
PluginManager::PluginManager(Core * core)
{
cmdlist_mutex = new mutex();
eval_ruby = NULL;
}
PluginManager::~PluginManager()
{
for(size_t i = 0; i < all_plugins.size();i++)
{
delete all_plugins[i];
}
all_plugins.clear();
delete cmdlist_mutex;
}
void PluginManager::init(Core * core)
{
#ifdef LINUX_BUILD
string path = core->getHackPath() + "plugins/";
@ -606,8 +626,6 @@ PluginManager::PluginManager(Core * core)
string path = core->getHackPath() + "plugins\\";
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++)
@ -622,16 +640,6 @@ PluginManager::PluginManager(Core * core)
}
}
PluginManager::~PluginManager()
{
for(size_t i = 0; i < all_plugins.size();i++)
{
delete all_plugins[i];
}
all_plugins.clear();
delete cmdlist_mutex;
}
Plugin *PluginManager::getPluginByName (const std::string & name)
{
for(size_t i = 0; i < all_plugins.size(); i++)

@ -220,9 +220,14 @@ void Process::getMemRanges( vector<t_memrange> & ranges )
}*/
}
uint32_t Process::getBase()
uintptr_t Process::getBase()
{
return 0;
return 0x1000000;
}
int Process::adjustOffset(int offset, bool /*to_file*/)
{
return offset;
}
static int getdir (string dir, vector<string> &files)
@ -300,3 +305,28 @@ bool Process::setPermisions(const t_memrange & range,const t_memrange &trgrange)
return result==0;
}
// returns -1 on error
void* Process::memAlloc(const int length)
{
return mmap(0, length, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON, -1, 0);
}
int Process::memDealloc(void *ptr, const int length)
{
return munmap(ptr, length);
}
int Process::memProtect(void *ptr, const int length, const int prot)
{
int prot_native = 0;
if (prot & Process::MemProt::READ)
prot_native |= PROT_READ;
if (prot & Process::MemProt::WRITE)
prot_native |= PROT_WRITE;
if (prot & Process::MemProt::EXEC)
prot_native |= PROT_EXEC;
return mprotect(ptr, length, prot_native);
}

@ -155,9 +155,14 @@ void Process::getMemRanges( vector<t_memrange> & ranges )
fclose(mapFile);
}
uint32_t Process::getBase()
uintptr_t Process::getBase()
{
return 0;
return 0x8048000;
}
int Process::adjustOffset(int offset, bool /*to_file*/)
{
return offset;
}
static int getdir (string dir, vector<string> &files)
@ -231,3 +236,28 @@ bool Process::setPermisions(const t_memrange & range,const t_memrange &trgrange)
return result==0;
}
// returns -1 on error
void* Process::memAlloc(const int length)
{
return mmap(0, length, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
}
int Process::memDealloc(void *ptr, const int length)
{
return munmap(ptr, length);
}
int Process::memProtect(void *ptr, const int length, const int prot)
{
int prot_native = 0;
if (prot & Process::MemProt::READ)
prot_native |= PROT_READ;
if (prot & Process::MemProt::WRITE)
prot_native |= PROT_WRITE;
if (prot & Process::MemProt::EXEC)
prot_native |= PROT_EXEC;
return mprotect(ptr, length, prot_native);
}

@ -160,7 +160,7 @@ Process::Process(VersionInfoFactory * factory)
identified = true;
// give the process a data model and memory layout fixed for the base of first module
my_descriptor = new VersionInfo(*vinfo);
my_descriptor->rebaseTo((uint32_t)d->base);
my_descriptor->rebaseTo(getBase());
for(size_t i = 0; i < threads_ids.size();i++)
{
HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, (DWORD) threads_ids[i]);
@ -394,13 +394,46 @@ void Process::getMemRanges( vector<t_memrange> & ranges )
}
}
uint32_t Process::getBase()
uintptr_t Process::getBase()
{
if(d)
return (uint32_t) d->base;
return (uintptr_t) d->base;
return 0x400000;
}
int Process::adjustOffset(int offset, bool to_file)
{
if (!d)
return -1;
for(int i = 0; i < d->pe_header.FileHeader.NumberOfSections; i++)
{
auto &section = d->sections[i];
if (to_file)
{
unsigned delta = offset - section.VirtualAddress;
if (delta >= section.Misc.VirtualSize)
continue;
if (!section.PointerToRawData || delta >= section.SizeOfRawData)
return -1;
return (int)(section.PointerToRawData + delta);
}
else
{
unsigned delta = offset - section.PointerToRawData;
if (!section.PointerToRawData || delta >= section.SizeOfRawData)
continue;
if (delta >= section.Misc.VirtualSize)
return -1;
return (int)(section.VirtualAddress + delta);
}
}
return -1;
}
string Process::doReadClassName (void * vptr)
{
char * rtti = readPtr((char *)vptr - 0x4);
@ -440,3 +473,42 @@ bool Process::setPermisions(const t_memrange & range,const t_memrange &trgrange)
return result;
}
void* Process::memAlloc(const int length)
{
void *ret;
// returns 0 on error
ret = VirtualAlloc(0, length, MEM_RESERVE|MEM_COMMIT, PAGE_READWRITE);
if (!ret)
ret = (void*)-1;
return ret;
}
int Process::memDealloc(void *ptr, const int length)
{
// can only free the whole region at once
// vfree returns 0 on error
return !VirtualFree(ptr, 0, MEM_RELEASE);
}
int Process::memProtect(void *ptr, const int length, const int prot)
{
int prot_native = 0;
DWORD old_prot = 0;
// only support a few constant combinations
if (prot == 0)
prot_native = PAGE_NOACCESS;
else if (prot == Process::MemProt::READ)
prot_native = PAGE_READONLY;
else if (prot == (Process::MemProt::READ | Process::MemProt::WRITE))
prot_native = PAGE_READWRITE;
else if (prot == (Process::MemProt::READ | Process::MemProt::WRITE | Process::MemProt::EXEC))
prot_native = PAGE_EXECUTE_READWRITE;
else if (prot == (Process::MemProt::READ | Process::MemProt::EXEC))
prot_native = PAGE_EXECUTE_READ;
else
return -1;
return !VirtualProtect(ptr, length, prot_native, &old_prot);
}

@ -292,9 +292,9 @@ void DFHack::describeUnit(BasicUnitInfo *info, df::unit *unit,
if (!unit->custom_profession.empty())
info->set_custom_profession(unit->custom_profession);
if (unit->military.squad_index >= 0)
if (unit->military.squad_id >= 0)
{
info->set_squad_id(unit->military.squad_index);
info->set_squad_id(unit->military.squad_id);
info->set_squad_position(unit->military.squad_position);
}
}

@ -100,6 +100,24 @@ bool DFHack::removeRef(std::vector<df::general_ref*> &vec, df::general_ref_type
return false;
}
df::item *DFHack::findItemRef(std::vector<df::general_ref*> &vec, df::general_ref_type type)
{
auto ref = findRef(vec, type);
return ref ? ref->getItem() : NULL;
}
df::building *DFHack::findBuildingRef(std::vector<df::general_ref*> &vec, df::general_ref_type type)
{
auto ref = findRef(vec, type);
return ref ? ref->getBuilding() : NULL;
}
df::unit *DFHack::findUnitRef(std::vector<df::general_ref*> &vec, df::general_ref_type type)
{
auto ref = findRef(vec, type);
return ref ? ref->getUnit() : NULL;
}
df::specific_ref *DFHack::findRef(std::vector<df::specific_ref*> &vec, df::specific_ref_type type)
{
for (int i = vec.size()-1; i >= 0; i--)

@ -39,15 +39,54 @@ using namespace DFHack;
/*
* Code for accessing method pointers directly. Very compiler-specific.
*
* Pointers to methods in C++ are conceptually similar to pointers to
* functions, but with some complications. Specifically, the target of
* such pointer can be either:
*
* - An ordinary non-virtual method, in which case the pointer behaves
* not much differently from a simple function pointer.
* - A virtual method, in which case calling the pointer must emulate
* an ordinary call to that method, i.e. fetch the real code address
* from the vtable at the appropriate index.
*
* This means that pointers to virtual methods actually have to encode
* the relevant vtable index value in some way. Also, since these two
* types of pointers cannot be distinguished by data type and differ
* only in value, any sane compiler would ensure that any non-virtual
* method that can potentially be called via a pointer uses the same
* parameter passing rules as an equivalent virtual method, so that
* the same parameter passing code would work with both types of pointer.
*
* This means that with a few small low-level compiler-specific wrappers
* to access the data inside such pointers it is possible to:
*
* - Convert a non-virtual method pointer into a code address that
* can be directly put into a vtable.
* - Convert a pointer taken out of a vtable into a fake non-virtual
* method pointer that can be used to easily call the original
* vmethod body.
* - Extract a vtable index out of a virtual method pointer.
*
* Taken together, these features allow delegating all the difficult
* and fragile tasks like passing parameters and calculating the
* vtable index to the C++ compiler.
*/
#if defined(_MSC_VER)
// MSVC may use up to 3 different representations
// based on context, but adding the /vmg /vmm options
// forces it to stick to this one. It can accomodate
// multiple, but not virtual inheritance.
struct MSVC_MPTR {
void *method;
intptr_t this_shift;
};
// Debug builds sometimes use additional thunks that
// just jump to the real one, presumably to attach some
// additional debug info.
static uint32_t *follow_jmp(void *ptr)
{
uint8_t *p = (uint8_t*)ptr;
@ -56,10 +95,10 @@ static uint32_t *follow_jmp(void *ptr)
{
switch (*p)
{
case 0xE9:
case 0xE9: // jmp near rel32
p += 5 + *(int32_t*)(p+1);
break;
case 0xEB:
case 0xEB: // jmp short rel8
p += 2 + *(int8_t*)(p+1);
break;
default:
@ -120,8 +159,10 @@ void DFHack::addr_to_method_pointer_(void *pptr, void *addr)
#elif defined(__GXX_ABI_VERSION)
// GCC seems to always use this structure - possibly unless
// virtual inheritance is involved, but that's irrelevant.
struct GCC_MPTR {
intptr_t method;
intptr_t method; // Code pointer or tagged vtable offset
intptr_t this_shift;
};
@ -166,12 +207,12 @@ void *virtual_identity::get_vmethod_ptr(int idx)
return vtable[idx];
}
bool virtual_identity::set_vmethod_ptr(int idx, void *ptr)
bool virtual_identity::set_vmethod_ptr(MemoryPatcher &patcher, int idx, void *ptr)
{
assert(idx >= 0);
void **vtable = (void**)vtable_ptr;
if (!vtable) return NULL;
return Core::getInstance().p->patchMemory(&vtable[idx], &ptr, sizeof(void*));
return patcher.write(&vtable[idx], &ptr, sizeof(void*));
}
/*
@ -254,6 +295,14 @@ VMethodInterposeLinkBase::VMethodInterposeLinkBase(virtual_identity *host, int v
{
if (vmethod_idx < 0 || interpose_method == NULL)
{
/*
* A failure here almost certainly means a problem in one
* of the pointer-to-method access wrappers above:
*
* - vmethod_idx comes from vmethod_pointer_to_idx_
* - interpose_method comes from method_pointer_to_addr_
*/
fprintf(stderr, "Bad VMethodInterposeLinkBase arguments: %d %08x\n",
vmethod_idx, unsigned(interpose_method));
fflush(stderr);
@ -281,9 +330,10 @@ VMethodInterposeLinkBase *VMethodInterposeLinkBase::get_first_interpose(virtual_
return item;
}
void VMethodInterposeLinkBase::find_child_hosts(virtual_identity *cur, void *vmptr)
bool VMethodInterposeLinkBase::find_child_hosts(virtual_identity *cur, void *vmptr)
{
auto &children = cur->getChildren();
bool found = false;
for (size_t i = 0; i < children.size(); i++)
{
@ -298,17 +348,32 @@ void VMethodInterposeLinkBase::find_child_hosts(virtual_identity *cur, void *vmp
continue;
child_next.insert(base);
found = true;
}
else
else if (child->vtable_ptr)
{
void *cptr = child->get_vmethod_ptr(vmethod_idx);
if (cptr != vmptr)
continue;
child_hosts.insert(child);
found = true;
find_child_hosts(child, vmptr);
}
else
{
// If this vtable is not known, but any of the children
// have the same vmethod, this one definitely does too
if (find_child_hosts(child, vmptr))
{
child_hosts.insert(child);
found = true;
}
}
}
return found;
}
void VMethodInterposeLinkBase::on_host_delete(virtual_identity *from)
@ -328,7 +393,9 @@ void VMethodInterposeLinkBase::on_host_delete(virtual_identity *from)
auto last = this;
while (last->prev) last = last->prev;
from->set_vmethod_ptr(vmethod_idx, last->saved_chain);
MemoryPatcher patcher;
from->set_vmethod_ptr(patcher, vmethod_idx, last->saved_chain);
// Unlink the chains
child_hosts.erase(from);
@ -363,13 +430,15 @@ bool VMethodInterposeLinkBase::apply(bool enable)
assert(old_ptr != NULL && (!old_link || old_link->interpose_method == old_ptr));
// Apply the new method ptr
MemoryPatcher patcher;
set_chain(old_ptr);
if (next_link)
{
next_link->set_chain(interpose_method);
}
else if (!host->set_vmethod_ptr(vmethod_idx, interpose_method))
else if (!host->set_vmethod_ptr(patcher, vmethod_idx, interpose_method))
{
set_chain(NULL);
return false;
@ -443,7 +512,7 @@ bool VMethodInterposeLinkBase::apply(bool enable)
{
auto nhost = *it;
assert(nhost->interpose_list[vmethod_idx] == old_link);
nhost->set_vmethod_ptr(vmethod_idx, interpose_method);
nhost->set_vmethod_ptr(patcher, vmethod_idx, interpose_method);
nhost->interpose_list[vmethod_idx] = this;
}
@ -480,9 +549,11 @@ void VMethodInterposeLinkBase::remove()
}
else
{
MemoryPatcher patcher;
// Remove from the list in the identity and vtable
host->interpose_list[vmethod_idx] = prev;
host->set_vmethod_ptr(vmethod_idx, saved_chain);
host->set_vmethod_ptr(patcher, vmethod_idx, saved_chain);
for (auto it = child_next.begin(); it != child_next.end(); ++it)
{
@ -499,7 +570,7 @@ void VMethodInterposeLinkBase::remove()
auto nhost = *it;
assert(nhost->interpose_list[vmethod_idx] == this);
nhost->interpose_list[vmethod_idx] = prev;
nhost->set_vmethod_ptr(vmethod_idx, saved_chain);
nhost->set_vmethod_ptr(patcher, vmethod_idx, saved_chain);
if (prev)
prev->child_hosts.insert(nhost);
}

@ -103,13 +103,13 @@ void VersionInfoFactory::ParseVersion (TiXmlElement* entry, VersionInfo* mem)
{
mem->setOS(OS_LINUX);
// this is wrong... I'm not going to do base image relocation on linux though.
mem->setBase(0x0);
mem->setBase(0x8048000);
}
else if(os == "darwin")
{
mem->setOS(OS_APPLE);
// this is wrong... I'm not going to do base image relocation on linux though.
mem->setBase(0x0);
mem->setBase(0x1000000);
}
else
{

@ -0,0 +1,315 @@
/*
https://github.com/peterix/dfhack
Copyright (c) 2011 Petr Mrázek <peterix@gmail.com>
A thread-safe logging console with a line editor for windows.
Based on linenoise win32 port,
copyright 2010, Jon Griffiths <jon_p_griffiths at yahoo dot com>.
All rights reserved.
Based on linenoise, copyright 2010, Salvatore Sanfilippo <antirez at gmail dot com>.
The original linenoise can be found at: http://github.com/antirez/linenoise
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of Redis nor the names of its contributors may be used
to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*/
#include <stdarg.h>
#include <errno.h>
#include <stdio.h>
#include <assert.h>
#include <iostream>
#include <fstream>
#include <istream>
#include <string>
#include <stdint.h>
#include <cstdio>
#include <cstdlib>
#include <sstream>
#include <vector>
#include <memory>
#include <md5wrapper.h>
using std::cout;
using std::cerr;
using std::endl;
typedef unsigned char patch_byte;
struct BinaryPatch {
struct Byte {
unsigned offset;
patch_byte old_val, new_val;
};
enum State {
Conflict = 0,
Unapplied = 1,
Applied = 2,
Partial = 3
};
std::vector<Byte> entries;
bool loadDIF(std::string name);
State checkState(const patch_byte *ptr, size_t len);
void apply(patch_byte *ptr, size_t len, bool newv);
};
inline bool is_hex(char c)
{
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
}
bool BinaryPatch::loadDIF(std::string name)
{
entries.clear();
std::ifstream infile(name);
if(infile.bad())
{
cerr << "Cannot open file: " << name << endl;
return false;
}
std::string s;
while(std::getline(infile, s))
{
// Parse lines that begin with "[0-9a-f]+:"
size_t idx = s.find(':');
if (idx == std::string::npos || idx == 0 || idx > 8)
continue;
bool ok = true;
for (size_t i = 0; i < idx; i++)
if (!is_hex(s[i]))
ok = false;
if (!ok)
continue;
unsigned off, oval, nval;
int nchar = 0;
int cnt = sscanf(s.c_str(), "%x: %x %x%n", &off, &oval, &nval, &nchar);
if (cnt < 3)
{
cerr << "Could not parse: " << s << endl;
return false;
}
for (size_t i = nchar; i < s.size(); i++)
{
if (!isspace(s[i]))
{
cerr << "Garbage at end of line: " << s << endl;
return false;
}
}
if (oval >= 256 || nval >= 256)
{
cerr << "Invalid byte values: " << s << endl;
return false;
}
Byte bv = { off, patch_byte(oval), patch_byte(nval) };
entries.push_back(bv);
}
if (entries.empty())
{
cerr << "No lines recognized." << endl;
return false;
}
return true;
}
BinaryPatch::State BinaryPatch::checkState(const patch_byte *ptr, size_t len)
{
int state = 0;
for (size_t i = 0; i < entries.size(); i++)
{
Byte &bv = entries[i];
if (bv.offset >= len)
{
cerr << "Offset out of range: 0x" << std::hex << bv.offset << std::dec << endl;
return Conflict;
}
patch_byte cv = ptr[bv.offset];
if (bv.old_val == cv)
state |= Unapplied;
else if (bv.new_val == cv)
state |= Applied;
else
{
cerr << std::hex << bv.offset << ": "
<< unsigned(bv.old_val) << " " << unsigned(bv.new_val)
<< ", but currently " << unsigned(cv) << std::dec << endl;
return Conflict;
}
}
return State(state);
}
void BinaryPatch::apply(patch_byte *ptr, size_t len, bool newv)
{
for (size_t i = 0; i < entries.size(); i++)
{
Byte &bv = entries[i];
assert (bv.offset < len);
ptr[bv.offset] = (newv ? bv.new_val : bv.old_val);
}
}
bool load_file(std::vector<patch_byte> *pvec, std::string fname)
{
FILE *f = fopen(fname.c_str(), "rb");
if (!f)
{
cerr << "Cannot open file: " << fname << endl;
return false;
}
fseek(f, 0, SEEK_END);
pvec->resize(ftell(f));
fseek(f, 0, SEEK_SET);
size_t cnt = fread(pvec->data(), 1, pvec->size(), f);
fclose(f);
return cnt == pvec->size();
}
bool save_file(const std::vector<patch_byte> &pvec, std::string fname)
{
FILE *f = fopen(fname.c_str(), "wb");
if (!f)
{
cerr << "Cannot open file: " << fname << endl;
return false;
}
size_t cnt = fwrite(pvec.data(), 1, pvec.size(), f);
fclose(f);
return cnt == pvec.size();
}
std::string compute_hash(const std::vector<patch_byte> &pvec)
{
md5wrapper md5;
return md5.getHashFromBytes(pvec.data(), pvec.size());
}
int main (int argc, char *argv[])
{
if (argc <= 3)
{
cerr << "Usage: binpatch check|apply|remove <exe> <patch>" << endl;
return 2;
}
std::string cmd = argv[1];
if (cmd != "check" && cmd != "apply" && cmd != "remove")
{
cerr << "Invalid command: " << cmd << endl;
return 2;
}
std::string exe_file = argv[2];
std::vector<patch_byte> bindata;
if (!load_file(&bindata, exe_file))
return 2;
BinaryPatch patch;
if (!patch.loadDIF(argv[3]))
return 2;
BinaryPatch::State state = patch.checkState(bindata.data(), bindata.size());
if (state == BinaryPatch::Conflict)
return 1;
if (cmd == "check")
{
switch (state)
{
case BinaryPatch::Unapplied:
cout << "Currently not applied." << endl;
break;
case BinaryPatch::Applied:
cout << "Currently applied." << endl;
break;
case BinaryPatch::Partial:
cout << "Currently partially applied." << endl;
break;
default:
break;
}
return 0;
}
else if (cmd == "apply")
{
if (state == BinaryPatch::Applied)
{
cout << "Already applied." << endl;
return 0;
}
patch.apply(bindata.data(), bindata.size(), true);
}
else if (cmd == "remove")
{
if (state == BinaryPatch::Unapplied)
{
cout << "Already removed." << endl;
return 0;
}
patch.apply(bindata.data(), bindata.size(), false);
}
if (!save_file(bindata, exe_file + ".bak"))
{
cerr << "Could not create backup." << endl;
return 1;
}
if (!save_file(bindata, exe_file))
return 1;
cout << "Patched " << patch.entries.size()
<< " bytes, new hash: " << compute_hash(bindata) << endl;
return 0;
}

@ -54,7 +54,6 @@ namespace DFHack
{
class Process;
class Module;
class World;
class Materials;
class Notes;
struct VersionInfo;
@ -120,8 +119,6 @@ namespace DFHack
/// Is everything OK?
bool isValid(void) { return !errorstate; }
/// get the world module
World * getWorld();
/// get the materials module
Materials * getMaterials();
/// get the notes module
@ -205,7 +202,6 @@ namespace DFHack
// Module storage
struct
{
World * pWorld;
Materials * pMaterials;
Notes * pNotes;
Graphic * pGraphic;

@ -61,7 +61,6 @@ distribution.
#include "modules/Translation.h"
#include "modules/World.h"
#include "modules/Items.h"
#include "modules/Vegetation.h"
#include "modules/Maps.h"
#include "modules/Gui.h"

@ -294,6 +294,7 @@ namespace DFHack
#endif
class DFHACK_EXPORT VMethodInterposeLinkBase;
class MemoryPatcher;
class DFHACK_EXPORT virtual_identity : public struct_identity {
static std::map<void*, virtual_identity*> known;
@ -313,7 +314,7 @@ namespace DFHack
bool can_allocate() { return struct_identity::can_allocate() && (vtable_ptr != NULL); }
void *get_vmethod_ptr(int index);
bool set_vmethod_ptr(int index, void *ptr);
bool set_vmethod_ptr(MemoryPatcher &patcher, int index, void *ptr);
public:
virtual_identity(size_t size, TAllocateFn alloc,

@ -36,10 +36,14 @@ distribution.
namespace DFHack {
class function_identity_base;
struct MaterialInfo;
namespace Units {
struct NoblePosition;
}
namespace Screen {
struct Pen;
};
}
namespace DFHack {namespace Lua {
@ -283,6 +287,8 @@ namespace DFHack {namespace Lua {
DFHACK_EXPORT void Push(lua_State *state, df::coord obj);
DFHACK_EXPORT void Push(lua_State *state, df::coord2d obj);
void Push(lua_State *state, const Units::NoblePosition &pos);
DFHACK_EXPORT void Push(lua_State *state, MaterialInfo &info);
DFHACK_EXPORT void Push(lua_State *state, const Screen::Pen &info);
template<class T> inline void Push(lua_State *state, T *ptr) {
PushDFObject(state, ptr);
}
@ -313,6 +319,8 @@ namespace DFHack {namespace Lua {
DFHACK_EXPORT int PushPosXYZ(lua_State *state, df::coord pos);
DFHACK_EXPORT int PushPosXY(lua_State *state, df::coord2d pos);
DFHACK_EXPORT void CheckPen(lua_State *L, Screen::Pen *pen, int index, bool allow_nil = false, bool allow_color = true);
DFHACK_EXPORT bool IsCoreContext(lua_State *state);
namespace Event {
@ -472,3 +480,18 @@ namespace DFHack {namespace Lua {
name##_event.invoke(out, 5); \
} \
}
#define DEFINE_LUA_EVENT_6(name, handler, arg_type1, arg_type2, arg_type3, arg_type4, arg_type5,arg_type6) \
static DFHack::Lua::Notification name##_event(df::wrap_function(handler, true)); \
void name(color_ostream &out, arg_type1 arg1, arg_type2 arg2, arg_type3 arg3, arg_type4 arg4,arg_type5 arg5, arg_type6 arg6) { \
handler(out, arg1, arg2, arg3, arg4, arg5, arg6); \
if (auto state = name##_event.state_if_count()) { \
DFHack::Lua::Push(state, arg1); \
DFHack::Lua::Push(state, arg2); \
DFHack::Lua::Push(state, arg3); \
DFHack::Lua::Push(state, arg4); \
DFHack::Lua::Push(state, arg5); \
DFHack::Lua::Push(state, arg6); \
name##_event.invoke(out, 6); \
} \
}

@ -275,11 +275,13 @@ namespace DFHack
{
return my_descriptor;
};
uint32_t getBase();
uintptr_t getBase();
/// get the DF Process ID
int getPID();
/// get the DF Process FilePath
std::string getPath();
/// Adjust between in-memory and in-file image offset
int adjustOffset(int offset, bool to_file = false);
/// millisecond tick count, exactly as DF uses
uint32_t getTickCount();
@ -289,6 +291,27 @@ namespace DFHack
/// write a possibly read-only memory area
bool patchMemory(void *target, const void* src, size_t count);
/// allocate new memory pages for code or stuff
/// returns -1 on error (0 is a valid address)
void* memAlloc(const int length);
/// free memory pages from memAlloc
/// should have length = alloced length for portability
/// returns 0 on success
int memDealloc(void *ptr, const int length);
/// change memory page permissions
/// prot is a bitwise OR of the MemProt enum
/// returns 0 on success
int memProtect(void *ptr, const int length, const int prot);
enum MemProt {
READ = 1,
WRITE = 2,
EXEC = 4
};
private:
VersionInfo * my_descriptor;
PlatformSpecific *d;
@ -315,5 +338,22 @@ namespace DFHack
// Get list of names given to ClassNameCheck constructors.
static void getKnownClassNames(std::vector<std::string> &names);
};
class DFHACK_EXPORT MemoryPatcher
{
Process *p;
std::vector<t_memrange> ranges, save;
public:
MemoryPatcher(Process *p = NULL);
~MemoryPatcher();
bool verifyAccess(void *target, size_t size, bool write = false);
bool makeWritable(void *target, size_t size) {
return verifyAccess(target, size, true);
}
bool write(void *target, const void *src, size_t size);
void close();
};
}
#endif

@ -331,6 +331,8 @@ inline T clip_range(T a, T1 minv, T2 maxv) {
return a;
}
DFHACK_EXPORT int random_int(int max);
/**
* Returns the amount of milliseconds elapsed since the UNIX epoch.
* Works on both windows and linux.

@ -33,7 +33,6 @@ namespace DFHack
Module* createGui();
Module* createWorld();
Module* createMaterials();
Module* createVegetation();
Module* createNotes();
Module* createGraphic();
}

@ -205,6 +205,7 @@ namespace DFHack
friend class Plugin;
PluginManager(Core * core);
~PluginManager();
void init(Core* core);
void OnUpdate(color_ostream &out);
void OnStateChange(color_ostream &out, state_change_event event);
void registerCommands( Plugin * p );
@ -246,7 +247,8 @@ namespace DFHack
/// You have to have this in every plugin you write - just once. Ideally on top of the main file.
#define DFHACK_PLUGIN(plugin_name) \
DFhackDataExport const char * version = DFHACK_VERSION;\
DFhackDataExport const char * name = plugin_name;
DFhackDataExport const char * name = plugin_name;\
DFhackDataExport Plugin *plugin_self = NULL;
#define DFHACK_PLUGIN_LUA_COMMANDS \
DFhackCExport const DFHack::CommandReg plugin_lua_commands[] =

@ -203,6 +203,12 @@ namespace DFHack
return ENUM_ATTR(tiletype_shape, passable_flow, tileShape(tiletype));
}
inline
bool FlowPassableDown(df::tiletype tiletype)
{
return ENUM_ATTR(tiletype_shape, passable_flow_down, tileShape(tiletype));
}
inline
bool isWalkable(df::tiletype tiletype)
{

@ -74,12 +74,38 @@ namespace DFHack
uint32_t xpNxtLvl;
};
typedef std::pair<df::coord2d, df::coord2d> rect2d;
inline rect2d intersect(rect2d a, rect2d b) {
df::coord2d g1 = a.first, g2 = a.second;
df::coord2d c1 = b.first, c2 = b.second;
df::coord2d rc1 = df::coord2d(std::max(g1.x, c1.x), std::max(g1.y, c1.y));
df::coord2d rc2 = df::coord2d(std::min(g2.x, c2.x), std::min(g2.y, c2.y));
return rect2d(rc1, rc2);
}
inline rect2d mkrect_xy(int x1, int y1, int x2, int y2) {
return rect2d(df::coord2d(x1, y1), df::coord2d(x2, y2));
}
inline rect2d mkrect_wh(int x, int y, int w, int h) {
return rect2d(df::coord2d(x, y), df::coord2d(x+w-1, y+h-1));
}
inline df::coord2d rect_size(const rect2d &rect) {
return rect.second - rect.first + df::coord2d(1,1);
}
DFHACK_EXPORT int getdir(std::string dir, std::vector<std::string> &files);
DFHACK_EXPORT bool hasEnding (std::string const &fullString, std::string const &ending);
DFHACK_EXPORT df::general_ref *findRef(std::vector<df::general_ref*> &vec, df::general_ref_type type);
DFHACK_EXPORT bool removeRef(std::vector<df::general_ref*> &vec, df::general_ref_type type, int id);
DFHACK_EXPORT df::item *findItemRef(std::vector<df::general_ref*> &vec, df::general_ref_type type);
DFHACK_EXPORT df::building *findBuildingRef(std::vector<df::general_ref*> &vec, df::general_ref_type type);
DFHACK_EXPORT df::unit *findUnitRef(std::vector<df::general_ref*> &vec, df::general_ref_type type);
DFHACK_EXPORT df::specific_ref *findRef(std::vector<df::specific_ref*> &vec, df::specific_ref_type type);
DFHACK_EXPORT bool removeRef(std::vector<df::specific_ref*> &vec, df::specific_ref_type type, void *ptr);
}// namespace DFHack

@ -28,6 +28,58 @@ distribution.
namespace DFHack
{
/* VMethod interpose API.
This API allows replacing an entry in the original vtable
with code defined by DFHack, while retaining ability to
call the original code. The API can be safely used from
plugins, and multiple hooks for the same vmethod are
automatically chained (subclass before superclass; at same
level highest priority called first; undefined order otherwise).
Usage:
struct my_hack : df::someclass {
typedef df::someclass interpose_base;
DEFINE_VMETHOD_INTERPOSE(void, foo, (int arg)) {
// If needed by the code, claim the suspend lock.
// DO NOT USE THE USUAL CoreSuspender, OR IT WILL DEADLOCK!
// CoreSuspendClaimer suspend;
...
INTERPOSE_NEXT(foo)(arg) // call the original
...
}
};
IMPLEMENT_VMETHOD_INTERPOSE(my_hack, foo);
or
IMPLEMENT_VMETHOD_INTERPOSE_PRIO(my_hack, foo, priority);
void init() {
if (!INTERPOSE_HOOK(my_hack, foo).apply())
error();
}
void shutdown() {
INTERPOSE_HOOK(my_hack, foo).remove();
}
Important caveat:
This will NOT intercept calls to the superclass vmethod
from overriding vmethod bodies in subclasses, i.e. whenever
DF code contains something like this, the call to "superclass::foo()"
doesn't actually use vtables, and thus will never trigger any hooks:
class superclass { virtual foo() { ... } };
class subclass : superclass { virtual foo() { ... superclass::foo(); ... } };
The only workaround is to implement and apply a second hook for subclass::foo,
and repeat that for any other subclasses and sub-subclasses that override this
vmethod.
*/
template<bool> struct StaticAssert;
template<> struct StaticAssert<true> {};
@ -81,43 +133,6 @@ namespace DFHack
return addr_to_method_pointer<P>(identity.get_vmethod_ptr(idx));
}
/* VMethod interpose API.
This API allows replacing an entry in the original vtable
with code defined by DFHack, while retaining ability to
call the original code. The API can be safely used from
plugins, and multiple hooks for the same vmethod are
automatically chained (subclass before superclass; at same
level highest priority called first; undefined order otherwise).
Usage:
struct my_hack : df::someclass {
typedef df::someclass interpose_base;
DEFINE_VMETHOD_INTERPOSE(void, foo, (int arg)) {
// If needed by the code, claim the suspend lock.
// DO NOT USE THE USUAL CoreSuspender, OR IT WILL DEADLOCK!
// CoreSuspendClaimer suspend;
...
INTERPOSE_NEXT(foo)(arg) // call the original
...
}
};
IMPLEMENT_VMETHOD_INTERPOSE(my_hack, foo);
or
IMPLEMENT_VMETHOD_INTERPOSE_PRIO(my_hack, foo, priority);
void init() {
if (!INTERPOSE_HOOK(my_hack, foo).apply())
error();
}
void shutdown() {
INTERPOSE_HOOK(my_hack, foo).remove();
}
*/
#define DEFINE_VMETHOD_INTERPOSE(rtype, name, args) \
typedef rtype (interpose_base::*interpose_ptr_##name)args; \
@ -142,24 +157,27 @@ namespace DFHack
friend class virtual_identity;
virtual_identity *host; // Class with the vtable
int vmethod_idx;
int vmethod_idx; // Index of the interposed method in the vtable
void *interpose_method; // Pointer to the code of the interposing method
void *chain_mptr; // Pointer to the chain field below
int priority;
void *chain_mptr; // Pointer to the chain field in the subclass below
int priority; // Higher priority hooks are called earlier
bool applied;
void *saved_chain; // Previous pointer to the code
VMethodInterposeLinkBase *next, *prev; // Other hooks for the same method
bool applied; // True if this hook is currently applied
void *saved_chain; // Pointer to the code of the original vmethod or next hook
// inherited vtable members
// Chain of hooks within the same host
VMethodInterposeLinkBase *next, *prev;
// Subclasses that inherit this topmost hook directly
std::set<virtual_identity*> child_hosts;
// Hooks within subclasses that branch off this topmost hook
std::set<VMethodInterposeLinkBase*> child_next;
// (See the cpp file for a more detailed description of these links)
void set_chain(void *chain);
void on_host_delete(virtual_identity *host);
VMethodInterposeLinkBase *get_first_interpose(virtual_identity *id);
void find_child_hosts(virtual_identity *cur, void *vmptr);
bool find_child_hosts(virtual_identity *cur, void *vmptr);
public:
VMethodInterposeLinkBase(virtual_identity *host, int vmethod_idx, void *interpose_method, void *chain_mptr, int priority);
~VMethodInterposeLinkBase();
@ -172,6 +190,9 @@ namespace DFHack
template<class Base, class Ptr>
class VMethodInterposeLink : public VMethodInterposeLinkBase {
public:
// Exactly the same as the saved_chain field of superclass,
// but converted to the appropriate pointer-to-method type.
// Kept up to date via the chain_mptr pointer.
Ptr chain;
operator Ptr () { return chain; }

@ -27,7 +27,7 @@ distribution.
#include "Pragma.h"
#include "Export.h"
#include "Types.h"
/* #include "Types.h" */
#include <map>
#include <sys/types.h>
#include <vector>

@ -1,5 +1,12 @@
bool empty() const { return x.empty(); }
unsigned size() const { return x.size(); }
void clear() {
x.clear();
y.clear();
z.clear();
}
coord operator[] (unsigned idx) const {
if (idx >= x.size())
return coord();

@ -16,7 +16,7 @@ inline bool getassignment( const df::coord2d &xy )
}
inline bool getassignment( int x, int y )
{
return (bits[y] & (1 << x));
return (bits[(y&15)] & (1 << (x&15)));
}
inline void setassignment( const df::coord2d &xy, bool bit )
{
@ -25,9 +25,9 @@ inline void setassignment( const df::coord2d &xy, bool bit )
inline void setassignment( int x, int y, bool bit )
{
if(bit)
bits[y] |= (1 << x);
bits[(y&15)] |= (1 << (x&15));
else
bits[y] &= ~(1 << x);
bits[(y&15)] &= ~(1 << (x&15));
}
bool has_assignments()
{

@ -25,7 +25,9 @@ distribution.
#pragma once
#include "Export.h"
#include "DataDefs.h"
#include "Types.h"
#include "df/building.h"
#include "df/building_type.h"
#include "df/civzone_type.h"
#include "df/furnace_type.h"
#include "df/workshop_type.h"
@ -92,6 +94,9 @@ DFHACK_EXPORT bool Read (const uint32_t index, t_building & building);
*/
DFHACK_EXPORT bool ReadCustomWorkshopTypes(std::map <uint32_t, std::string> & btypes);
DFHACK_EXPORT df::general_ref *getGeneralRef(df::building *building, df::general_ref_type type);
DFHACK_EXPORT df::specific_ref *getSpecificRef(df::building *building, df::specific_ref_type type);
/**
* Sets the owner unit for the building.
*/
@ -178,5 +183,8 @@ DFHACK_EXPORT bool constructWithFilters(df::building *bld, std::vector<df::job_i
*/
DFHACK_EXPORT bool deconstruct(df::building *bld);
void updateBuildings(color_ostream& out, void* ptr);
void clearBuildings(color_ostream& out);
}
}

@ -0,0 +1,60 @@
#pragma once
#ifndef EVENT_MANAGER_H_INCLUDED
#define EVENT_MANAGER_H_INCLUDED
#include "Core.h"
#include "Export.h"
#include "ColorText.h"
#include "PluginManager.h"
#include "Console.h"
namespace DFHack {
namespace EventManager {
namespace EventType {
enum EventType {
TICK,
JOB_INITIATED,
JOB_COMPLETED,
UNIT_DEATH,
ITEM_CREATED,
BUILDING,
CONSTRUCTION,
SYNDROME,
INVASION,
EVENT_MAX
};
}
struct EventHandler {
void (*eventHandler)(color_ostream&, void*); //called when the event happens
int32_t freq;
EventHandler(void (*eventHandlerIn)(color_ostream&, void*), int32_t freqIn): eventHandler(eventHandlerIn), freq(freqIn) {
}
bool operator==(EventHandler& handle) const {
return eventHandler == handle.eventHandler && freq == handle.freq;
}
bool operator!=(EventHandler& handle) const {
return !( *this == handle);
}
};
struct SyndromeData {
int32_t unitId;
int32_t syndromeIndex;
SyndromeData(int32_t unitId_in, int32_t syndromeIndex_in): unitId(unitId_in), syndromeIndex(syndromeIndex_in) {
}
};
DFHACK_EXPORT void registerListener(EventType::EventType e, EventHandler handler, Plugin* plugin);
DFHACK_EXPORT void registerTick(EventHandler handler, int32_t when, Plugin* plugin, bool absolute=false);
DFHACK_EXPORT void unregister(EventType::EventType e, EventHandler handler, Plugin* plugin);
DFHACK_EXPORT void unregisterAll(Plugin* plugin);
void manageEvents(color_ostream& out);
void onStateChange(color_ostream& out, state_change_event event);
}
}
#endif

@ -29,6 +29,8 @@ distribution.
#include "ColorText.h"
#include <string>
#include "Types.h"
#include "DataDefs.h"
#include "df/init.h"
#include "df/ui.h"
@ -116,6 +118,9 @@ namespace DFHack
int map_x1, map_x2, menu_x1, menu_x2, area_x1, area_x2;
int y1, y2;
bool menu_on, area_on, menu_forced;
rect2d map() { return mkrect_xy(map_x1, y1, map_x2, y2); }
rect2d menu() { return mkrect_xy(menu_x1, y1, menu_x2, y2); }
};
DFHACK_EXPORT DwarfmodeDims getDwarfmodeViewDims();

@ -40,6 +40,7 @@ distribution.
#include "df/building_actual.h"
#include "df/body_part_raw.h"
#include "df/unit_inventory_item.h"
#include "df/job_item_vector_id.h"
namespace df
{
@ -86,7 +87,8 @@ namespace DFHack
bool find(const std::string &token);
bool matches(const df::job_item &item, MaterialInfo *mat = NULL);
bool matches(df::job_item_vector_id vec_id);
bool matches(const df::job_item &item, MaterialInfo *mat = NULL, bool skip_vector = false);
};
inline bool operator== (const ItemTypeInfo &a, const ItemTypeInfo &b) {
@ -123,6 +125,10 @@ struct dfh_item
namespace Items
{
DFHACK_EXPORT bool isCasteMaterial(df::item_type itype);
DFHACK_EXPORT int getSubtypeCount(df::item_type itype);
DFHACK_EXPORT df::itemdef *getSubtypeDef(df::item_type itype, int subtype);
/// Look for a particular item by ID
DFHACK_EXPORT df::item * findItemByID(int32_t id);
@ -145,6 +151,11 @@ DFHACK_EXPORT df::item *getContainer(df::item *item);
/// which items does it contain?
DFHACK_EXPORT void getContainedItems(df::item *item, /*output*/ std::vector<df::item*> *items);
/// which building holds it?
DFHACK_EXPORT df::building *getHolderBuilding(df::item *item);
/// which unit holds it?
DFHACK_EXPORT df::unit *getHolderUnit(df::item *item);
/// Returns the true position of the item.
DFHACK_EXPORT df::coord getPosition(df::item *item);
@ -155,7 +166,7 @@ DFHACK_EXPORT bool moveToGround(MapExtras::MapCache &mc, df::item *item, df::coo
DFHACK_EXPORT bool moveToContainer(MapExtras::MapCache &mc, df::item *item, df::item *container);
DFHACK_EXPORT bool moveToBuilding(MapExtras::MapCache &mc, df::item *item, df::building_actual *building,int16_t use_mode);
DFHACK_EXPORT bool moveToInventory(MapExtras::MapCache &mc, df::item *item, df::unit *unit,
df::unit_inventory_item::T_mode mode = df::unit_inventory_item::Carried, int body_part = -1);
df::unit_inventory_item::T_mode mode = df::unit_inventory_item::Hauled, int body_part = -1);
/// Makes the item removed and marked for garbage collection
DFHACK_EXPORT bool remove(MapExtras::MapCache &mc, df::item *item, bool no_uncat = false);

@ -28,10 +28,13 @@ distribution.
#include "Export.h"
#include "Module.h"
#include "Types.h"
#include <ostream>
#include "DataDefs.h"
#include "df/job_item_ref.h"
#include "df/item_type.h"
namespace df
{
@ -46,7 +49,7 @@ namespace DFHack
{
namespace Job {
// Duplicate the job structure. It is not linked into any DF lists.
DFHACK_EXPORT df::job *cloneJobStruct(df::job *job);
DFHACK_EXPORT df::job *cloneJobStruct(df::job *job, bool keepWorkerData=false);
// Delete a cloned structure.
DFHACK_EXPORT void deleteJobStruct(df::job *job);
@ -54,6 +57,9 @@ namespace DFHack
DFHACK_EXPORT void printItemDetails(color_ostream &out, df::job_item *item, int idx);
DFHACK_EXPORT void printJobDetails(color_ostream &out, df::job *job);
DFHACK_EXPORT df::general_ref *getGeneralRef(df::job *job, df::general_ref_type type);
DFHACK_EXPORT df::specific_ref *getSpecificRef(df::job *job, df::specific_ref_type type);
DFHACK_EXPORT df::building *getHolder(df::job *job);
DFHACK_EXPORT df::unit *getWorker(df::job *job);
@ -69,6 +75,9 @@ namespace DFHack
DFHACK_EXPORT bool attachJobItem(df::job *job, df::item *item,
df::job_item_ref::T_role role,
int filter_idx = -1, int insert_idx = -1);
DFHACK_EXPORT bool isSuitableItem(df::job_item *item, df::item_type itype, int isubtype);
DFHACK_EXPORT bool isSuitableMaterial(df::job_item *item, int mat_type, int mat_index);
}
DFHACK_EXPORT bool operator== (const df::job_item &a, const df::job_item &b);

@ -47,14 +47,6 @@ namespace MapExtras
class DFHACK_EXPORT MapCache;
template<class R, class T> inline R index_tile(T &v, df::coord2d p) {
return v[p.x&15][p.y&15];
}
inline bool is_valid_tile_coord(df::coord2d p) {
return (p.x & ~15) == 0 && (p.y & ~15) == 0;
}
class Block;
class BlockInfo
@ -119,8 +111,8 @@ public:
{
if (!basemats) init_tiles(true);
return t_matpair(
index_tile<int16_t>(basemats->mattype,p),
index_tile<int16_t>(basemats->matindex,p)
index_tile<int16_t>(basemats->mat_type,p),
index_tile<int16_t>(basemats->mat_index,p)
);
}
bool isVeinAt(df::coord2d p)
@ -159,8 +151,8 @@ public:
if (!basemats) init_tiles(true);
if (tiles->con_info)
return t_matpair(
index_tile<int16_t>(tiles->con_info->mattype,p),
index_tile<int16_t>(tiles->con_info->matindex,p)
index_tile<int16_t>(tiles->con_info->mat_type,p),
index_tile<int16_t>(tiles->con_info->mat_index,p)
);
return baseMaterialAt(p);
}
@ -292,8 +284,8 @@ private:
struct ConInfo {
df::tile_bitmask constructed;
df::tiletype tiles[16][16];
t_blockmaterials mattype;
t_blockmaterials matindex;
t_blockmaterials mat_type;
t_blockmaterials mat_index;
};
struct TileInfo {
df::tile_bitmask frozen;
@ -312,8 +304,8 @@ private:
};
struct BasematInfo {
df::tile_bitmask dirty;
t_blockmaterials mattype;
t_blockmaterials matindex;
t_blockmaterials mat_type;
t_blockmaterials mat_index;
t_blockmaterials layermat;
BasematInfo();

@ -32,7 +32,6 @@ distribution.
#include "Export.h"
#include "Module.h"
#include "modules/Vegetation.h"
#include <vector>
#include "BitArray.h"
#include "modules/Materials.h"
@ -151,6 +150,21 @@ typedef uint8_t biome_indices40d [9];
*/
typedef uint16_t t_temperatures [16][16];
/**
* Index a tile array by a 2D coordinate, clipping it to mod 16
*/
template<class R, class T> inline R index_tile(T &v, df::coord2d p) {
return v[p.x&15][p.y&15];
}
/**
* Check if a 2D coordinate is in the 0-15 range.
*/
inline bool is_valid_tile_coord(df::coord2d p) {
return (p.x & ~15) == 0 && (p.y & ~15) == 0;
}
/**
* The Maps module
* \ingroup grp_modules
@ -294,6 +308,7 @@ extern DFHACK_EXPORT bool SortBlockEvents(df::map_block *block,
extern DFHACK_EXPORT bool RemoveBlockEvent(uint32_t x, uint32_t y, uint32_t z, df::block_square_event * which );
DFHACK_EXPORT bool canWalkBetween(df::coord pos1, df::coord pos2);
DFHACK_EXPORT bool canStepBetween(df::coord pos1, df::coord pos2);
}
}
#endif

@ -338,10 +338,10 @@ namespace DFHack
*/
struct t_material
{
t_itemType itemType;
t_itemSubtype subType;
t_materialType material;
t_materialIndex index;
t_itemType item_type;
t_itemSubtype item_subtype;
t_materialType mat_type;
t_materialIndex mat_index;
uint32_t flags;
};
/**

@ -27,7 +27,10 @@ distribution.
#include "Module.h"
#include "BitArray.h"
#include "ColorText.h"
#include "Types.h"
#include <string>
#include <set>
#include "DataDefs.h"
#include "df/graphic.h"
@ -50,6 +53,8 @@ namespace DFHack
{
class Core;
typedef std::set<df::interface_key> interface_key_set;
/**
* The Screen module
* \ingroup grp_modules
@ -76,6 +81,8 @@ namespace DFHack
bool valid() const { return tile >= 0; }
bool empty() const { return ch == 0 && tile == 0; }
// NOTE: LuaApi.cpp assumes this struct is plain data and has empty destructor
Pen(char ch = 0, int8_t fg = 7, int8_t bg = 0, int tile = 0, bool color_tile = false)
: ch(ch), fg(fg&7), bg(bg), bold(!!(fg&8)),
tile(tile), tile_mode(color_tile ? CharColor : AsIs), tile_fg(0), tile_bg(0)
@ -92,11 +99,67 @@ namespace DFHack
: ch(ch), fg(fg), bg(bg), bold(bold),
tile(tile), tile_mode(TileColor), tile_fg(tile_fg), tile_bg(tile_bg)
{}
void adjust(int8_t nfg) { fg = nfg&7; bold = !!(nfg&8); }
void adjust(int8_t nfg, bool nbold) { fg = nfg; bold = nbold; }
void adjust(int8_t nfg, int8_t nbg) { adjust(nfg); bg = nbg; }
void adjust(int8_t nfg, bool nbold, int8_t nbg) { adjust(nfg, nbold); bg = nbg; }
Pen color(int8_t nfg) const { Pen cp(*this); cp.adjust(nfg); return cp; }
Pen color(int8_t nfg, bool nbold) const { Pen cp(*this); cp.adjust(nfg, nbold); return cp; }
Pen color(int8_t nfg, int8_t nbg) const { Pen cp(*this); cp.adjust(nfg, nbg); return cp; }
Pen color(int8_t nfg, bool nbold, int8_t nbg) const { Pen cp(*this); cp.adjust(nfg, nbold, nbg); return cp; }
Pen chtile(char ch) { Pen cp(*this); cp.ch = ch; return cp; }
Pen chtile(char ch, int tile) { Pen cp(*this); cp.ch = ch; cp.tile = tile; return cp; }
};
struct DFHACK_EXPORT ViewRect {
rect2d view, clip;
ViewRect(rect2d area) : view(area), clip(area) {}
ViewRect(rect2d area, rect2d clip) : view(area), clip(clip) {}
bool isDefunct() const {
return clip.first.x > clip.second.x || clip.first.y > clip.second.y;
}
int width() const { return view.second.x-view.first.x+1; }
int height() const { return view.second.y-view.first.y+1; }
df::coord2d local(df::coord2d pos) const {
return df::coord2d(pos.x - view.first.x, pos.y - view.first.y);
}
df::coord2d global(df::coord2d pos) const {
return df::coord2d(pos.x + view.first.x, pos.y + view.first.y);
}
df::coord2d global(int x, int y) const {
return df::coord2d(x + view.first.x, y + view.first.y);
}
bool inClipGlobal(int x, int y) const {
return x >= clip.first.x && x <= clip.second.x &&
y >= clip.first.y && y <= clip.second.y;
}
bool inClipGlobal(df::coord2d pos) const {
return inClipGlobal(pos.x, pos.y);
}
bool inClipLocal(int x, int y) const {
return inClipGlobal(x + view.first.x, y + view.first.y);
}
bool inClipLocal(df::coord2d pos) const {
return inClipLocal(pos.x, pos.y);
}
ViewRect viewport(rect2d area) const {
rect2d nview(global(area.first), global(area.second));
return ViewRect(nview, intersect(nview, clip));
}
};
DFHACK_EXPORT df::coord2d getMousePos();
DFHACK_EXPORT df::coord2d getWindowSize();
inline rect2d getScreenRect() {
return rect2d(df::coord2d(0,0), getWindowSize()-df::coord2d(1,1));
}
/// Returns the state of [GRAPHICS:YES/NO]
DFHACK_EXPORT bool inGraphicsMode();
@ -128,6 +191,80 @@ namespace DFHack
DFHACK_EXPORT bool show(df::viewscreen *screen, df::viewscreen *before = NULL);
DFHACK_EXPORT void dismiss(df::viewscreen *screen, bool to_first = false);
DFHACK_EXPORT bool isDismissed(df::viewscreen *screen);
/// Retrieve the string representation of the bound key.
DFHACK_EXPORT std::string getKeyDisplay(df::interface_key key);
/// A painter class that implements a clipping area and cursor/pen state
struct DFHACK_EXPORT Painter : ViewRect {
df::coord2d gcursor;
Pen cur_pen, cur_key_pen;
static const Pen default_pen;
static const Pen default_key_pen;
Painter(const ViewRect &area, const Pen &pen = default_pen, const Pen &kpen = default_key_pen)
: ViewRect(area), gcursor(area.view.first), cur_pen(pen), cur_key_pen(kpen)
{}
df::coord2d cursor() const { return local(gcursor); }
int cursorX() const { return gcursor.x - view.first.x; }
int cursorY() const { return gcursor.y - view.first.y; }
bool isValidPos() const { return inClipGlobal(gcursor); }
Painter viewport(rect2d area) const {
return Painter(ViewRect::viewport(area), cur_pen, cur_key_pen);
}
Painter &seek(df::coord2d pos) { gcursor = global(pos); return *this; }
Painter &seek(int x, int y) { gcursor = global(x,y); return *this; }
Painter &advance(int dx) { gcursor.x += dx; return *this; }
Painter &advance(int dx, int dy) { gcursor.x += dx; gcursor.y += dy; return *this; }
Painter &newline(int dx = 0) { gcursor.y++; gcursor.x = view.first.x + dx; return *this; }
const Pen &pen() const { return cur_pen; }
Painter &pen(const Pen &np) { cur_pen = np; return *this; }
Painter &pen(int8_t fg) { cur_pen.adjust(fg); return *this; }
const Pen &key_pen() const { return cur_key_pen; }
Painter &key_pen(const Pen &np) { cur_key_pen = np; return *this; }
Painter &key_pen(int8_t fg) { cur_key_pen.adjust(fg); return *this; }
Painter &clear() {
fillRect(Pen(' ',0,0,false), clip.first.x, clip.first.y, clip.second.x, clip.second.y);
return *this;
}
Painter &fill(const rect2d &area, const Pen &pen) {
rect2d irect = intersect(area, clip);
fillRect(pen, irect.first.x, irect.first.y, irect.second.x, irect.second.y);
return *this;
}
Painter &fill(const rect2d &area) { return fill(area, cur_pen); }
Painter &tile(const Pen &pen) {
if (isValidPos()) paintTile(pen, gcursor.x, gcursor.y);
return advance(1);
}
Painter &tile() { return tile(cur_pen); }
Painter &tile(char ch) { return tile(cur_pen.chtile(ch)); }
Painter &tile(char ch, int tileid) { return tile(cur_pen.chtile(ch, tileid)); }
Painter &string(const std::string &str, const Pen &pen) {
do_paint_string(str, pen); return advance(str.size());
}
Painter &string(const std::string &str) { return string(str, cur_pen); }
Painter &string(const std::string &str, int8_t fg) { return string(str, cur_pen.color(fg)); }
Painter &key(df::interface_key kc, const Pen &pen) {
return string(getKeyDisplay(kc), pen);
}
Painter &key(df::interface_key kc) { return key(kc, cur_key_pen); }
private:
void do_paint_string(const std::string &str, const Pen &pen);
};
}
class DFHACK_EXPORT dfhack_viewscreen : public df::viewscreen {

@ -205,6 +205,9 @@ DFHACK_EXPORT void CopyNameTo(df::unit *creature, df::language_name * target);
/// Returns the true position of the unit (non-trivial in case of caged).
DFHACK_EXPORT df::coord getPosition(df::unit *unit);
DFHACK_EXPORT df::general_ref *getGeneralRef(df::unit *unit, df::general_ref_type type);
DFHACK_EXPORT df::specific_ref *getSpecificRef(df::unit *unit, df::specific_ref_type type);
DFHACK_EXPORT df::item *getContainer(df::unit *unit);
DFHACK_EXPORT void setNickname(df::unit *unit, std::string nick);
@ -235,6 +238,8 @@ DFHACK_EXPORT double getAge(df::unit *unit, bool true_age = false);
DFHACK_EXPORT int getNominalSkill(df::unit *unit, df::job_skill skill_id, bool use_rust = false);
DFHACK_EXPORT int getEffectiveSkill(df::unit *unit, df::job_skill skill_id);
DFHACK_EXPORT int getExperience(df::unit *unit, df::job_skill skill_id, bool total = false);
DFHACK_EXPORT int computeMovementSpeed(df::unit *unit);
struct NoblePosition {

@ -1,70 +0,0 @@
/*
https://github.com/peterix/dfhack
Copyright (c) 2009-2012 Petr Mrázek (peterix@gmail.com)
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any
damages arising from the use of this software.
Permission is granted to anyone to use this software for any
purpose, including commercial applications, and to alter it and
redistribute it freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must
not claim that you wrote the original software. If you use this
software in a product, an acknowledgment in the product documentation
would be appreciated but is not required.
2. Altered source versions must be plainly marked as such, and
must not be misrepresented as being the original software.
3. This notice may not be removed or altered from any source
distribution.
*/
#pragma once
#ifndef CL_MOD_VEGETATION
#define CL_MOD_VEGETATION
/**
* \defgroup grp_vegetation Vegetation : stuff that grows and gets cut down or trampled by dwarves
* @ingroup grp_modules
*/
#include "Export.h"
#include "DataDefs.h"
#include "df/plant.h"
namespace DFHack
{
namespace Vegetation
{
const uint32_t sapling_to_tree_threshold = 120 * 28 * 12 * 3; // 3 years
// "Simplified" copy of plant
struct t_plant {
df::language_name name;
df::plant_flags flags;
int16_t material;
df::coord pos;
int32_t grow_counter;
uint16_t temperature_1;
uint16_t temperature_2;
int32_t is_burning;
int32_t hitpoints;
int16_t update_order;
//std::vector<void *> unk1;
//int32_t unk2;
//uint16_t temperature_3;
//uint16_t temperature_4;
//uint16_t temperature_5;
// Pointer to original object, in case you want to modify it
df::plant *origin;
};
DFHACK_EXPORT bool isValid();
DFHACK_EXPORT uint32_t getCount();
DFHACK_EXPORT df::plant * getPlant(const int32_t index);
DFHACK_EXPORT bool copyPlant (const int32_t index, t_plant &out);
}
}
#endif

@ -37,6 +37,12 @@ distribution.
#include "DataDefs.h"
namespace df
{
struct tile_bitmask;
struct map_block;
}
namespace DFHack
{
typedef df::game_mode GameMode;
@ -55,8 +61,6 @@ namespace DFHack
class DFContextShared;
class DFHACK_EXPORT PersistentDataItem {
friend class World;
int id;
std::string key_value;
@ -65,13 +69,66 @@ namespace DFHack
public:
static const int NumInts = 7;
bool isValid() { return id != 0; }
int entry_id() { return -id; }
bool isValid() const { return id != 0; }
int entry_id() const { return -id; }
int raw_id() const { return id; }
const std::string &key() { return key_value; }
const std::string &key() const { return key_value; }
std::string &val() { return *str_value; }
const std::string &val() const { return *str_value; }
int &ival(int i) { return int_values[i]; }
int ival(int i) const { return int_values[i]; }
// Pack binary data into string field.
// Since DF serialization chokes on NUL bytes,
// use bit magic to ensure none of the bytes is 0.
// Choose the lowest bit for padding so that
// sign-extend can be used normally.
size_t data_size() const { return str_value->size(); }
bool check_data(size_t off, size_t sz = 1) {
return (str_value->size() >= off+sz);
}
void ensure_data(size_t off, size_t sz = 0) {
if (str_value->size() < off+sz) str_value->resize(off+sz, '\x01');
}
uint8_t *pdata(size_t off) { return (uint8_t*)&(*str_value)[off]; }
static const size_t int7_size = 1;
uint8_t get_uint7(size_t off) {
uint8_t *p = pdata(off);
return p[0]>>1;
}
int8_t get_int7(size_t off) {
uint8_t *p = pdata(off);
return int8_t(p[0])>>1;
}
void set_uint7(size_t off, uint8_t val) {
uint8_t *p = pdata(off);
p[0] = uint8_t((val<<1) | 1);
}
void set_int7(size_t off, int8_t val) { set_uint7(off, val); }
static const size_t int28_size = 4;
uint32_t get_uint28(size_t off) {
uint8_t *p = pdata(off);
return (p[0]>>1) | ((p[1]&~1U)<<6) | ((p[2]&~1U)<<13) | ((p[3]&~1U)<<20);
}
int32_t get_int28(size_t off) {
uint8_t *p = pdata(off);
return (p[0]>>1) | ((p[1]&~1U)<<6) | ((p[2]&~1U)<<13) | ((int8_t(p[3])&~1)<<20);
}
void set_uint28(size_t off, uint32_t val) {
uint8_t *p = pdata(off);
p[0] = uint8_t((val<<1) | 1);
p[1] = uint8_t((val>>6) | 1);
p[2] = uint8_t((val>>13) | 1);
p[3] = uint8_t((val>>20) | 1);
}
void set_int28(size_t off, int32_t val) { set_uint28(off, val); }
PersistentDataItem() : id(0), str_value(0), int_values(0) {}
PersistentDataItem(int id, const std::string &key, std::string *sv, int *iv)
@ -83,54 +140,45 @@ namespace DFHack
* \ingroup grp_modules
* \ingroup grp_world
*/
class DFHACK_EXPORT World : public Module
namespace World
{
public:
World();
~World();
bool Start();
bool Finish();
///true if paused, false if not
bool ReadPauseState();
DFHACK_EXPORT bool ReadPauseState();
///true if paused, false if not
void SetPauseState(bool paused);
uint32_t ReadCurrentTick();
uint32_t ReadCurrentYear();
uint32_t ReadCurrentMonth();
uint32_t ReadCurrentDay();
uint8_t ReadCurrentWeather();
void SetCurrentWeather(uint8_t weather);
bool ReadGameMode(t_gamemodes& rd);
bool WriteGameMode(const t_gamemodes & wr); // this is very dangerous
std::string ReadWorldFolder();
DFHACK_EXPORT void SetPauseState(bool paused);
DFHACK_EXPORT uint32_t ReadCurrentTick();
DFHACK_EXPORT uint32_t ReadCurrentYear();
DFHACK_EXPORT uint32_t ReadCurrentMonth();
DFHACK_EXPORT uint32_t ReadCurrentDay();
DFHACK_EXPORT uint8_t ReadCurrentWeather();
DFHACK_EXPORT void SetCurrentWeather(uint8_t weather);
DFHACK_EXPORT bool ReadGameMode(t_gamemodes& rd);
DFHACK_EXPORT bool WriteGameMode(const t_gamemodes & wr); // this is very dangerous
DFHACK_EXPORT std::string ReadWorldFolder();
// Store data in fake historical figure names.
// This ensures that the values are stored in save games.
PersistentDataItem AddPersistentData(const std::string &key);
PersistentDataItem GetPersistentData(const std::string &key);
PersistentDataItem GetPersistentData(int entry_id);
DFHACK_EXPORT PersistentDataItem AddPersistentData(const std::string &key);
DFHACK_EXPORT PersistentDataItem GetPersistentData(const std::string &key);
DFHACK_EXPORT PersistentDataItem GetPersistentData(int entry_id);
// Calls GetPersistentData(key); if not found, adds and sets added to true.
// The result can still be not isValid() e.g. if the world is not loaded.
PersistentDataItem GetPersistentData(const std::string &key, bool *added);
DFHACK_EXPORT PersistentDataItem GetPersistentData(const std::string &key, bool *added);
// Lists all items with the given key.
// If prefix is true, search for keys starting with key+"/".
// GetPersistentData(&vec,"",true) returns all items.
// Items have alphabetic order by key; same key ordering is undefined.
void GetPersistentData(std::vector<PersistentDataItem> *vec,
DFHACK_EXPORT void GetPersistentData(std::vector<PersistentDataItem> *vec,
const std::string &key, bool prefix = false);
// Deletes the item; returns true if success.
bool DeletePersistentData(const PersistentDataItem &item);
DFHACK_EXPORT bool DeletePersistentData(const PersistentDataItem &item);
void ClearPersistentCache();
DFHACK_EXPORT void ClearPersistentCache();
private:
struct Private;
Private *d;
bool BuildPersistentCache();
};
DFHACK_EXPORT df::tile_bitmask *getPersistentTilemask(const PersistentDataItem &item, df::map_block *block, bool create = false);
DFHACK_EXPORT bool deletePersistentTilemask(const PersistentDataItem &item, df::map_block *block);
}
}
#endif

@ -0,0 +1,123 @@
-- Simple binary patch with IDA dif file support.
local _ENV = mkmodule('binpatch')
local function load_patch(name)
local filename = name
if not string.match(filename, '[./\\]') then
filename = dfhack.getHackPath()..'/patches/'..dfhack.getDFVersion()..'/'..name..'.dif'
end
local file, err = io.open(filename, 'r')
if not file then
if string.match(err, ': No such file or directory') then
return nil, 'patch not found'
end
end
local old_bytes = {}
local new_bytes = {}
for line in file:lines() do
if string.match(line, '^%x+:') then
local offset, oldv, newv = string.match(line, '^(%x+):%s*(%x+)%s+(%x+)%s*$')
if not offset then
file:close()
return nil, 'could not parse: '..line
end
offset, oldv, newv = tonumber(offset,16), tonumber(oldv,16), tonumber(newv,16)
if oldv > 255 or newv > 255 then
file:close()
return nil, 'invalid byte values: '..line
end
old_bytes[offset] = oldv
new_bytes[offset] = newv
end
end
return { name = name, old_bytes = old_bytes, new_bytes = new_bytes }
end
local function rebase_table(input)
local output = {}
local base = dfhack.internal.getImageBase()
for k,v in pairs(input) do
local offset = dfhack.internal.adjustOffset(k)
if not offset then
return nil, string.format('invalid offset: %x', k)
end
output[base + offset] = v
end
return output
end
local function rebase_patch(patch)
local nold, err = rebase_table(patch.old_bytes)
if not nold then return nil, err end
local nnew, err = rebase_table(patch.new_bytes)
if not nnew then return nil, err end
return { name = patch.name, old_bytes = nold, new_bytes = nnew }
end
BinaryPatch = defclass(BinaryPatch)
BinaryPatch.ATTRS {
name = DEFAULT_NIL,
old_bytes = DEFAULT_NIL,
new_bytes = DEFAULT_NIL,
}
function load_dif_file(name)
local patch, err = load_patch(name)
if not patch then return nil, err end
local rpatch, err = rebase_patch(patch)
if not rpatch then return nil, err end
return BinaryPatch(rpatch)
end
function BinaryPatch:status()
local old_ok, err, addr = dfhack.internal.patchBytes({}, self.old_bytes)
if old_ok then
return 'removed'
elseif dfhack.internal.patchBytes({}, self.new_bytes) then
return 'applied'
else
return 'conflict', addr
end
end
function BinaryPatch:isApplied()
return dfhack.internal.patchBytes({}, self.new_bytes)
end
function BinaryPatch:apply()
local ok, err, addr = dfhack.internal.patchBytes(self.new_bytes, self.old_bytes)
if ok then
return true, 'applied the patch'
elseif dfhack.internal.patchBytes({}, self.new_bytes) then
return true, 'patch is already applied'
else
return false, string.format('conflict at address %x', addr)
end
end
function BinaryPatch:isRemoved()
return dfhack.internal.patchBytes({}, self.old_bytes)
end
function BinaryPatch:remove()
local ok, err, addr = dfhack.internal.patchBytes(self.old_bytes, self.new_bytes)
if ok then
return true, 'removed the patch'
elseif dfhack.internal.patchBytes({}, self.old_bytes) then
return true, 'patch is already removed'
else
return false, string.format('conflict at address %x', addr)
end
end
return _ENV

@ -3,16 +3,16 @@
local _ENV = mkmodule('class')
-- Metatable template for a class
class_obj = {} or class_obj
class_obj = class_obj or {}
-- Methods shared by all classes
common_methods = {} or common_methods
common_methods = common_methods or {}
-- Forbidden names for class fields and methods.
reserved_names = { super = true, ATTRS = true }
-- Attribute table metatable
attrs_meta = {} or attrs_meta
attrs_meta = attrs_meta or {}
-- Create or updates a class; a class has metamethods and thus own metatable.
function defclass(class,parent)
@ -65,10 +65,14 @@ end
local function apply_attrs(obj, attrs, init_table)
for k,v in pairs(attrs) do
if v == DEFAULT_NIL then
v = nil
local init_v = init_table[k]
if init_v ~= nil then
obj[k] = init_v
elseif v == DEFAULT_NIL then
obj[k] = nil
else
obj[k] = v
end
obj[k] = init_table[k] or v
end
end
@ -133,6 +137,14 @@ function common_methods:callback(method, ...)
return dfhack.curry(self[method], self, ...)
end
function common_methods:cb_getfield(field)
return function() return self[field] end
end
function common_methods:cb_setfield(field)
return function(val) self[field] = val end
end
function common_methods:assign(data)
for k,v in pairs(data) do
self[k] = v

@ -125,6 +125,10 @@ end
-- Misc functions
NEWLINE = "\n"
COMMA = ","
PERIOD = "."
function printall(table)
local ok,f,t,k = pcall(pairs,table)
if ok then
@ -157,6 +161,14 @@ function xyz2pos(x,y,z)
end
end
function same_xyz(a,b)
return a and b and a.x == b.x and a.y == b.y and a.z == b.z
end
function get_path_xyz(path,i)
return path.x[i], path.y[i], path.z[i]
end
function pos2xy(pos)
if pos then
local x = pos.x
@ -174,6 +186,14 @@ function xy2pos(x,y)
end
end
function same_xy(a,b)
return a and b and a.x == b.x and a.y == b.y
end
function get_path_xy(path,i)
return path.x[i], path.y[i]
end
function safe_index(obj,idx,...)
if obj == nil or idx == nil then
return nil
@ -308,9 +328,11 @@ end
-- Command scripts
dfhack.internal.scripts = dfhack.internal.scripts or {}
local internal = dfhack.internal
local scripts = dfhack.internal.scripts
internal.scripts = internal.scripts or {}
local scripts = internal.scripts
local hack_path = dfhack.getHackPath()
function dfhack.run_script(name,...)
@ -329,5 +351,42 @@ function dfhack.run_script(name,...)
return f(...)
end
-- Per-save init file
function dfhack.getSavePath()
if dfhack.isWorldLoaded() then
return dfhack.getDFPath() .. '/data/save/' .. df.global.world.cur_savegame.save_dir
end
end
if dfhack.is_core_context then
dfhack.onStateChange.DFHACK_PER_SAVE = function(op)
if op == SC_WORLD_LOADED or op == SC_WORLD_UNLOADED then
if internal.save_init then
if internal.save_init.onUnload then
safecall(internal.save_init.onUnload)
end
internal.save_init = nil
end
local path = dfhack.getSavePath()
if path and op == SC_WORLD_LOADED then
local env = setmetatable({ SAVE_PATH = path }, { __index = base_env })
local f,perr = loadfile(path..'/raw/init.lua', 't', env)
if f == nil then
if not string.match(perr, 'No such file or directory') then
dfhack.printerr(perr)
end
elseif safecall(f) then
internal.save_init = env
end
end
elseif internal.save_init and internal.save_init.onStateChange then
safecall(internal.save_init.onStateChange, op)
end
end
end
-- Feed the table back to the require() mechanism.
return dfhack

@ -334,7 +334,22 @@ local trap_inputs = {
},
[df.trap_type.TrackStop] = { { flags2={ building_material=true, non_economic=true } } }
}
local siegeengine_input = {
[df.siegeengine_type.Catapult] = {
{
item_type=df.item_type.CATAPULTPARTS,
vector_id=df.job_item_vector_id.CATAPULTPARTS,
quantity=3
}
},
[df.siegeengine_type.Ballista] = {
{
item_type=df.item_type.BALLISTAPARTS,
vector_id=df.job_item_vector_id.BALLISTAPARTS,
quantity=3
}
},
}
--[[ Functions for lookup in tables. ]]
local function get_custom_inputs(custom)
@ -359,6 +374,8 @@ local function get_inputs_by_type(type,subtype,custom)
end
elseif type == df.building_type.Trap then
return trap_inputs[subtype]
elseif type == df.building_type.SiegeEngine then
return siegeengine_input[subtype]
else
return building_inputs[type]
end

@ -0,0 +1,591 @@
local _ENV = mkmodule('dfhack.workshops')
local utils = require 'utils'
input_filter_defaults = {
item_type = -1,
item_subtype = -1,
mat_type = -1,
mat_index = -1,
flags1 = {},
-- Instead of noting those that allow artifacts, mark those that forbid them.
-- Leaves actually enabling artifacts to the discretion of the API user,
-- which is the right thing because unlike the game UI these filters are
-- used in a way that does not give the user a chance to choose manually.
flags2 = { allow_artifact = true },
flags3 = {},
flags4 = 0,
flags5 = 0,
reaction_class = '',
has_material_reaction_product = '',
metal_ore = -1,
min_dimension = -1,
has_tool_use = -1,
quantity = 1
}
local fuel={item_type=df.item_type.BAR,mat_type=df.builtin_mats.COAL}
jobs_furnace={
[df.furnace_type.Smelter]={
{
name="Melt metal object",
items={fuel,{flags2={allow_melt_dump=true}}},--also maybe melt_designated
job_fields={job_type=df.job_type.MeltMetalObject}
}
},
[df.furnace_type.MagmaSmelter]={
{
name="Melt metal object",
items={{flags2={allow_melt_dump=true}}},--also maybe melt_designated
job_fields={job_type=df.job_type.MeltMetalObject}
}
},
--[[ [df.furnace_type.MetalsmithsForge]={
unpack(concat(furnaces,mechanism,anvil,crafts,coins,flask))
},
]]
--MetalsmithsForge,
--MagmaForge
--[[
forges:
weapons and ammo-> from raws...
armor -> raws
furniture -> builtins?
siege eq-> builtin (only balista head)
trap eq -> from raws+ mechanisms
other object-> anvil, crafts, goblets,toys,instruments,nestbox... (raws?) flask, coins,stud with iron
metal clothing-> raws???
]]
[df.furnace_type.GlassFurnace]={
{
name="collect sand",
items={},
job_fields={job_type=df.job_type.CollectSand}
},
--glass crafts x3
},
[df.furnace_type.WoodFurnace]={
defaults={item_type=df.item_type.WOOD,vector_id=df.job_item_vector_id.WOOD},
{
name="make charcoal",
items={{}},
job_fields={job_type=df.job_type.MakeCharcoal}
},
{
name="make ash",
items={{}},
job_fields={job_type=df.job_type.MakeAsh}
}
},
[df.furnace_type.Kiln]={
{
name="collect clay",
items={},
job_fields={job_type=df.job_type.CollectClay}
}
},
}
jobs_workshop={
[df.workshop_type.Jewelers]={
{
name="cut gems",
items={{item_type=df.item_type.ROUGH,flags1={unrotten=true}}},
job_fields={job_type=df.job_type.CutGems}
},
{
name="encrust finished goods with gems",
items={{item_type=df.item_type.SMALLGEM},{flags1={improvable=true,finished_goods=true}}},
job_fields={job_type=df.job_type.EncrustWithGems}
},
{
name="encrust ammo with gems",
items={{item_type=df.item_type.SMALLGEM},{flags1={improvable=true,ammo=true}}},
job_fields={job_type=df.job_type.EncrustWithGems}
},
{
name="encrust furniture with gems",
items={{item_type=df.item_type.SMALLGEM},{flags1={improvable=true,furniture=true}}},
job_fields={job_type=df.job_type.EncrustWithGems}
},
},
[df.workshop_type.Fishery]={
{
name="prepare raw fish",
items={{item_type=df.item_type.FISH_RAW,flags1={unrotten=true}}},
job_fields={job_type=df.job_type.PrepareRawFish}
},
{
name="extract from raw fish",
items={{flags1={unrotten=true,extract_bearing_fish=true}},{item_type=df.item_type.FLASK,flags1={empty=true,glass=true}}},
job_fields={job_type=df.job_type.ExtractFromRawFish}
},
{
name="catch live fish",
items={},
job_fields={job_type=df.job_type.CatchLiveFish}
}, -- no items?
},
[df.workshop_type.Still]={
{
name="brew drink",
items={{flags1={distillable=true},vector_id=22},{flags1={empty=true},flags3={food_storage=true}}},
job_fields={job_type=df.job_type.BrewDrink}
},
{
name="extract from plants",
items={{item_type=df.item_type.PLANT,flags1={unrotten=true,extract_bearing_plant=true}},{item_type=df.item_type.FLASK,flags1={empty=true}}},
job_fields={job_type=df.job_type.ExtractFromPlants}
},
--mead from raws?
},
[df.workshop_type.Masons]={
defaults={item_type=df.item_type.BOULDER,item_subtype=-1,vector_id=df.job_item_vector_id.BOULDER, mat_type=0,mat_index=-1,flags3={hard=true}},--flags2={non_economic=true},
{
name="construct armor stand",
items={{}},
job_fields={job_type=df.job_type.ConstructArmorStand}
},
{
name="construct blocks",
items={{}},
job_fields={job_type=df.job_type.ConstructBlocks}
},
{
name="construct throne",
items={{}},
job_fields={job_type=df.job_type.ConstructThrone}
},
{
name="construct coffin",
items={{}},
job_fields={job_type=df.job_type.ConstructCoffin}
},
{
name="construct door",
items={{}},
job_fields={job_type=df.job_type.ConstructDoor}
},
{
name="construct floodgate",
items={{}},
job_fields={job_type=df.job_type.ConstructFloodgate}
},
{
name="construct hatch cover",
items={{}},
job_fields={job_type=df.job_type.ConstructHatchCover}
},
{
name="construct grate",
items={{}},
job_fields={job_type=df.job_type.ConstructGrate}
},
{
name="construct cabinet",
items={{}},
job_fields={job_type=df.job_type.ConstructCabinet}
},
{
name="construct chest",
items={{}},
job_fields={job_type=df.job_type.ConstructChest}
},
{
name="construct statue",
items={{}},
job_fields={job_type=df.job_type.ConstructStatue}
},
{
name="construct slab",
items={{}},
job_fields={job_type=df.job_type.ConstructSlab}
},
{
name="construct table",
items={{}},
job_fields={job_type=df.job_type.ConstructTable}
},
{
name="construct weapon rack",
items={{}},
job_fields={job_type=df.job_type.ConstructWeaponRack}
},
{
name="construct quern",
items={{}},
job_fields={job_type=df.job_type.ConstructQuern}
},
{
name="construct millstone",
items={{}},
job_fields={job_type=df.job_type.ConstructMillstone}
},
},
[df.workshop_type.Carpenters]={
--training weapons, wooden shields
defaults={item_type=df.item_type.WOOD,vector_id=df.job_item_vector_id.WOOD},
{
name="make barrel",
items={{}},
job_fields={job_type=df.job_type.MakeBarrel}
},
{
name="make bucket",
items={{}},
job_fields={job_type=df.job_type.MakeBucket}
},
{
name="make animal trap",
items={{}},
job_fields={job_type=df.job_type.MakeAnimalTrap}
},
{
name="make cage",
items={{}},
job_fields={job_type=df.job_type.MakeCage}
},
{
name="construct bed",
items={{}},
job_fields={job_type=df.job_type.ConstructBed}
},
{
name="construct bin",
items={{}},
job_fields={job_type=df.job_type.ConstructBin}
},
{
name="construct armor stand",
items={{}},
job_fields={job_type=df.job_type.ConstructArmorStand}
},
{
name="construct blocks",
items={{}},
job_fields={job_type=df.job_type.ConstructBlocks}
},
{
name="construct throne",
items={{}},
job_fields={job_type=df.job_type.ConstructThrone}
},
{
name="construct coffin",
items={{}},
job_fields={job_type=df.job_type.ConstructCoffin}
},
{
name="construct door",
items={{}},
job_fields={job_type=df.job_type.ConstructDoor}
},
{
name="construct floodgate",
items={{}},
job_fields={job_type=df.job_type.ConstructFloodgate}
},
{
name="construct hatch cover",
items={{}},
job_fields={job_type=df.job_type.ConstructHatchCover}
},
{
name="construct grate",
items={{}},
job_fields={job_type=df.job_type.ConstructGrate}
},
{
name="construct cabinet",
items={{}},
job_fields={job_type=df.job_type.ConstructCabinet}
},
{
name="construct chest",
items={{}},
job_fields={job_type=df.job_type.ConstructChest}
},
{
name="construct statue",
items={{}},
job_fields={job_type=df.job_type.ConstructStatue}
},
{
name="construct table",
items={{}},
job_fields={job_type=df.job_type.ConstructTable}
},
{
name="construct weapon rack",
items={{}},
job_fields={job_type=df.job_type.ConstructWeaponRack}
},
{
name="construct splint",
items={{}},
job_fields={job_type=df.job_type.ConstructSplint}
},
{
name="construct crutch",
items={{}},
job_fields={job_type=df.job_type.ConstructCrutch}
},
},
[df.workshop_type.Kitchen]={
--mat_type=2,3,4
defaults={flags1={unrotten=true,cookable=true}},
{
name="prepare easy meal",
items={{flags1={solid=true}},{}},
job_fields={job_type=df.job_type.PrepareMeal,mat_type=2}
},
{
name="prepare fine meal",
items={{flags1={solid=true}},{},{}},
job_fields={job_type=df.job_type.PrepareMeal,mat_type=3}
},
{
name="prepare lavish meal",
items={{flags1={solid=true}},{},{},{}},
job_fields={job_type=df.job_type.PrepareMeal,mat_type=4}
},
},
[df.workshop_type.Butchers]={
{
name="butcher an animal",
items={{flags1={butcherable=true,unrotten=true,nearby=true}}},
job_fields={job_type=df.job_type.ButcherAnimal}
},
{
name="extract from land animal",
items={{flags1={extract_bearing_vermin=true,unrotten=true}},{item_type=df.item_type.FLASK,flags1={empty=true,glass=true}}},
job_fields={job_type=df.job_type.ExtractFromLandAnimal}
},
{
name="catch live land animal",
items={},
job_fields={job_type=df.job_type.CatchLiveLandAnimal}
},
},
[df.workshop_type.Mechanics]={
{
name="construct mechanisms",
items={{item_type=df.item_type.BOULDER,item_subtype=-1,vector_id=df.job_item_vector_id.BOULDER, mat_type=0,mat_index=-1,quantity=1,
flags3={hard=true}}},
job_fields={job_type=df.job_type.ConstructMechanisms}
},
{
name="construct traction bench",
items={{item_type=df.item_type.TABLE},{item_type=df.item_type.MECHANISM},{item_type=df.item_type.CHAIN}},
job_fields={job_type=df.job_type.ConstructTractionBench}
},
},
[df.workshop_type.Loom]={
{
name="weave plant thread cloth",
items={{item_type=df.item_type.THREAD,quantity=15000,min_dimension=15000,flags1={collected=true},flags2={plant=true}}},
job_fields={job_type=df.job_type.WeaveCloth}
},
{
name="weave silk thread cloth",
items={{item_type=df.item_type.THREAD,quantity=15000,min_dimension=15000,flags1={collected=true},flags2={silk=true}}},
job_fields={job_type=df.job_type.WeaveCloth}
},
{
name="weave yarn cloth",
items={{item_type=df.item_type.THREAD,quantity=15000,min_dimension=15000,flags1={collected=true},flags2={yarn=true}}},
job_fields={job_type=df.job_type.WeaveCloth}
},
{
name="weave inorganic cloth",
items={{item_type=df.item_type.THREAD,quantity=15000,min_dimension=15000,flags1={collected=true},mat_type=0}},
job_fields={job_type=df.job_type.WeaveCloth}
},
{
name="collect webs",
items={{item_type=df.item_type.THREAD,quantity=10,min_dimension=10,flags1={undisturbed=true}}},
job_fields={job_type=df.job_type.CollectWebs}
},
},
[df.workshop_type.Leatherworks]={
defaults={item_type=SKIN_TANNED},
{
name="construct leather bag",
items={{}},
job_fields={job_type=df.job_type.ConstructChest}
},
{
name="construct waterskin",
items={{}},
job_fields={job_type=df.job_type.MakeFlask}
},
{
name="construct backpack",
items={{}},
job_fields={job_type=df.job_type.MakeBackpack}
},
{
name="construct quiver",
items={{}},
job_fields={job_type=df.job_type.MakeQuiver}
},
{
name="sew leather image",
items={{item_type=-1,flags1={empty=true},flags2={sewn_imageless=true}},{}},
job_fields={job_type=df.job_type.SewImage}
},
},
[df.workshop_type.Dyers]={
{
name="dye thread",
items={{item_type=df.item_type.THREAD,quantity=15000,min_dimension=15000,flags1={collected=true},flags2={dyeable=true}},
{flags1={unrotten=true},flags2={dye=true}}},
job_fields={job_type=df.job_type.DyeThread}
},
{
name="dye cloth",
items={{item_type=df.item_type.CLOTH,quantity=10000,min_dimension=10000,flags2={dyeable=true}},
{flags1={unrotten=true},flags2={dye=true}}},
job_fields={job_type=df.job_type.DyeThread}
},
},
[df.workshop_type.Siege]={
{
name="construct balista parts",
items={{item_type=df.item_type.WOOD}},
job_fields={job_type=df.job_type.ConstructBallistaParts}
},
{
name="construct catapult parts",
items={{item_type=df.item_type.WOOD}},
job_fields={job_type=df.job_type.ConstructCatapultParts}
},
{
name="assemble balista arrow",
items={{item_type=df.item_type.WOOD}},
job_fields={job_type=df.job_type.AssembleSiegeAmmo}
},
{
name="assemble tipped balista arrow",
items={{item_type=df.item_type.WOOD},{item_type=df.item_type.BALLISTAARROWHEAD}},
job_fields={job_type=df.job_type.AssembleSiegeAmmo}
},
},
}
local function matchIds(bid1,wid1,cid1,bid2,wid2,cid2)
if bid1~=-1 and bid2~=-1 and bid1~=bid2 then
return false
end
if wid1~=-1 and wid2~=-1 and wid1~=wid2 then
return false
end
if cid1~=-1 and cid2~=-1 and cid1~=cid2 then
return false
end
return true
end
local function scanRawsReaction(buildingId,workshopId,customId)
local ret={}
for idx,reaction in ipairs(df.global.world.raws.reactions) do
for k,v in pairs(reaction.building.type) do
if matchIds(buildingId,workshopId,customId,v,reaction.building.subtype[k],reaction.building.custom[k]) then
table.insert(ret,reaction)
end
end
end
return ret
end
local function reagentToJobItem(reagent,react_id,reagentId)
local ret_item
ret_item=utils.clone_with_default(reagent, input_filter_defaults)
ret_item.reaction_id=react_id
ret_item.reagent_index=reagentId
return ret_item
end
local function addReactionJobs(ret,bid,wid,cid)
local reactions=scanRawsReaction(bid,wid or -1,cid or -1)
for idx,react in pairs(reactions) do
local job={name=react.name,
items={},job_fields={job_type=df.job_type.CustomReaction,reaction_name=react.code}
}
for reagentId,reagent in pairs(react.reagents) do
table.insert(job.items,reagentToJobItem(reagent,idx,reagentId))
end
if react.flags.FUEL then
table.insert(job.items,fuel)
end
table.insert(ret,job)
end
end
local function scanRawsOres()
local ret={}
for idx,ore in ipairs(df.global.world.raws.inorganics) do
if #ore.metal_ore.mat_index~=0 then
ret[idx]=ore
end
end
return ret
end
local function addSmeltJobs(ret,use_fuel)
local ores=scanRawsOres()
for idx,ore in pairs(ores) do
print("adding:",ore.material.state_name.Solid)
printall(ore)
local job={name="smelt "..ore.material.state_name.Solid,job_fields={job_type=df.job_type.SmeltOre,mat_type=df.builtin_mats.INORGANIC,mat_index=idx},items={
{item_type=df.item_type.BOULDER,mat_type=df.builtin_mats.INORGANIC,mat_index=idx,vector_id=df.job_item_vector_id.BOULDER}}}
if use_fuel then
table.insert(job.items,fuel)
end
table.insert(ret,job)
end
return ret
end
function getJobs(buildingId,workshopId,customId)
local ret={}
local c_jobs
if buildingId==df.building_type.Workshop then
c_jobs=jobs_workshop[workshopId]
elseif buildingId==df.building_type.Furnace then
c_jobs=jobs_furnace[workshopId]
if workshopId == df.furnace_type.Smelter or workshopId == df.furnace_type.MagmaSmelter then
c_jobs=utils.clone(c_jobs,true)
addSmeltJobs(c_jobs,workshopId == df.furnace_type.Smelter)
end
else
return nil
end
if c_jobs==nil then
c_jobs={}
else
c_jobs=utils.clone(c_jobs,true)
end
addReactionJobs(c_jobs,buildingId,workshopId,customId)
for jobId,contents in pairs(c_jobs) do
if jobId~="defaults" then
local entry={}
entry.name=contents.name
local lclDefaults=utils.clone(input_filter_defaults,true)
if c_jobs.defaults ~=nil then
utils.assign(lclDefaults,c_jobs.defaults)
end
entry.items={}
for k,item in pairs(contents.items) do
entry.items[k]=utils.clone(lclDefaults,true)
utils.assign(entry.items[k],item)
end
if contents.job_fields~=nil then
entry.job_fields={}
utils.assign(entry.job_fields,contents.job_fields)
end
ret[jobId]=entry
end
end
--get jobs, add in from raws
return ret
end
return _ENV

@ -6,7 +6,17 @@ local dscreen = dfhack.screen
USE_GRAPHICS = dscreen.inGraphicsMode()
CLEAR_PEN = {ch=32,fg=0,bg=0}
local to_pen = dfhack.pen.parse
CLEAR_PEN = to_pen{ch=32,fg=0,bg=0}
local FAKE_INPUT_KEYS = {
_MOUSE_L = true,
_MOUSE_R = true,
_MOUSE_L_DOWN = true,
_MOUSE_R_DOWN = true,
_STRING = true,
}
function simulateInput(screen,...)
local keys = {}
@ -14,7 +24,7 @@ function simulateInput(screen,...)
local kv = arg
if type(arg) == 'string' then
kv = df.interface_key[arg]
if kv == nil then
if kv == nil and not FAKE_INPUT_KEYS[arg] then
error('Invalid keycode: '..arg)
end
end
@ -48,57 +58,140 @@ end
function mkdims_wh(x1,y1,w,h)
return { x1=x1, y1=y1, x2=x1+w-1, y2=y1+h-1, width=w, height=h }
end
function inset(rect,dx1,dy1,dx2,dy2)
return mkdims_xy(
rect.x1+dx1, rect.y1+dy1,
rect.x2-(dx2 or dx1), rect.y2-(dy2 or dy1)
)
end
function is_in_rect(rect,x,y)
return x and y and x >= rect.x1 and x <= rect.x2 and y >= rect.y1 and y <= rect.y2
end
local function to_pen(default, pen, bg, bold)
if pen == nil then
return default or {}
elseif type(pen) ~= 'table' then
return {fg=pen,bg=bg,bold=bold}
local function align_coord(gap,align,lv,rv)
if gap <= 0 then
return 0
end
if not align then
if rv and not lv then
align = 1.0
elseif lv and not rv then
align = 0.0
else
return pen
align = 0.5
end
end
return math.floor(gap*align)
end
----------------------------
-- Clipped painter object --
----------------------------
function compute_frame_rect(wavail,havail,spec,xgap,ygap)
if not spec then
return mkdims_wh(0,0,wavail,havail)
end
Painter = defclass(Painter, nil)
local sw = wavail - (spec.l or 0) - (spec.r or 0)
local sh = havail - (spec.t or 0) - (spec.b or 0)
local rqw = math.min(sw, (spec.w or sw)+xgap)
local rqh = math.min(sh, (spec.h or sh)+ygap)
local ax = align_coord(sw - rqw, spec.xalign, spec.l, spec.r)
local ay = align_coord(sh - rqh, spec.yalign, spec.t, spec.b)
function Painter:init(args)
local rect = mkdims_wh((spec.l or 0) + ax, (spec.t or 0) + ay, rqw, rqh)
rect.wgap = sw - rqw
rect.hgap = sh - rqh
return rect
end
local function parse_inset(inset)
local l,r,t,b
if type(inset) == 'table' then
l,r = inset.l or inset.x, inset.r or inset.x
t,b = inset.t or inset.y, inset.b or inset.y
else
l = inset or 0
t,r,b = l,l,l
end
return l,r,t,b
end
function inset_frame(rect, inset, gap)
gap = gap or 0
local l,t,r,b = parse_inset(inset)
return mkdims_xy(rect.x1+l+gap, rect.y1+t+gap, rect.x2-r-gap, rect.y2-b-gap)
end
function compute_frame_body(wavail, havail, spec, inset, gap, inner_frame)
gap = gap or 0
local l,t,r,b = parse_inset(inset)
local xgap,ygap = 0,0
if inner_frame then
xgap,ygap = gap*2+l+r, gap*2+t+b
end
local rect = compute_frame_rect(wavail, havail, spec, xgap, ygap)
local body = mkdims_xy(rect.x1+l+gap, rect.y1+t+gap, rect.x2-r-gap, rect.y2-b-gap)
return rect, body
end
function blink_visible(delay)
return math.floor(dfhack.getTickCount()/delay) % 2 == 0
end
function getKeyDisplay(code)
if type(code) == 'string' then
code = df.interface_key[code]
end
return dscreen.getKeyDisplay(code)
end
-----------------------------------
-- Clipped view rectangle object --
-----------------------------------
ViewRect = defclass(ViewRect, nil)
function ViewRect:init(args)
if args.view_rect then
self:assign(args.view_rect)
else
local rect = args.rect or mkdims_wh(0,0,dscreen.getWindowSize())
local crect = args.clip_rect or rect
self:assign{
x = rect.x1, y = rect.y1,
x1 = rect.x1, clip_x1 = crect.x1,
y1 = rect.y1, clip_y1 = crect.y1,
x2 = rect.x2, clip_x2 = crect.x2,
y2 = rect.y2, clip_y2 = crect.y2,
width = rect.x2-rect.x1+1,
height = rect.y2-rect.y1+1,
cur_pen = to_pen(nil, args.pen or COLOR_GREY)
}
end
if args.clip_view then
local cr = args.clip_view
self:assign{
clip_x1 = math.max(self.clip_x1, cr.clip_x1),
clip_y1 = math.max(self.clip_y1, cr.clip_y1),
clip_x2 = math.min(self.clip_x2, cr.clip_x2),
clip_y2 = math.min(self.clip_y2, cr.clip_y2),
}
end
end
function Painter.new(rect, pen)
return Painter{ rect = rect, pen = pen }
function ViewRect:isDefunct()
return (self.clip_x1 > self.clip_x2 or self.clip_y1 > self.clip_y2)
end
function Painter:isValidPos()
return self.x >= self.clip_x1 and self.x <= self.clip_x2
and self.y >= self.clip_y1 and self.y <= self.clip_y2
function ViewRect:inClipGlobalXY(x,y)
return x >= self.clip_x1 and x <= self.clip_x2
and y >= self.clip_y1 and y <= self.clip_y2
end
function Painter:viewport(x,y,w,h)
function ViewRect:inClipLocalXY(x,y)
return (x+self.x1) >= self.clip_x1 and (x+self.x1) <= self.clip_x2
and (y+self.y1) >= self.clip_y1 and (y+self.y1) <= self.clip_y2
end
function ViewRect:localXY(x,y)
return x-self.x1, y-self.y1
end
function ViewRect:globalXY(x,y)
return x+self.x1, y+self.y1
end
function ViewRect:viewport(x,y,w,h)
if type(x) == 'table' then
x,y,w,h = x.x1, x.y1, x.width, x.height
end
@ -113,17 +206,59 @@ function Painter:viewport(x,y,w,h)
clip_y1 = math.max(self.clip_y1, y1),
clip_x2 = math.min(self.clip_x2, x2),
clip_y2 = math.min(self.clip_y2, y2),
-- Pen
cur_pen = self.cur_pen
}
return mkinstance(ViewRect, vp)
end
----------------------------
-- Clipped painter object --
----------------------------
Painter = defclass(Painter, ViewRect)
function Painter:init(args)
self.x = self.x1
self.y = self.y1
self.cur_pen = to_pen(args.pen or COLOR_GREY)
self.cur_key_pen = to_pen(args.key_pen or COLOR_LIGHTGREEN)
end
function Painter.new(rect, pen)
return Painter{ rect = rect, pen = pen }
end
function Painter.new_view(view_rect, pen)
return Painter{ view_rect = view_rect, pen = pen }
end
function Painter.new_xy(x1,y1,x2,y2,pen)
return Painter{ rect = mkdims_xy(x1,y1,x2,y2), pen = pen }
end
function Painter.new_wh(x,y,w,h,pen)
return Painter{ rect = mkdims_wh(x,y,w,h), pen = pen }
end
function Painter:isValidPos()
return self:inClipGlobalXY(self.x, self.y)
end
function Painter:viewport(x,y,w,h)
local vp = ViewRect.viewport(x,y,w,h)
vp.cur_pen = self.cur_pen
vp.cur_key_pen = self.cur_key_pen
return mkinstance(Painter, vp):seek(0,0)
end
function Painter:localX()
function Painter:cursor()
return self.x - self.x1, self.y - self.y1
end
function Painter:cursorX()
return self.x - self.x1
end
function Painter:localY()
function Painter:cursorY()
return self.y - self.y1
end
@ -151,10 +286,12 @@ function Painter:pen(pen,...)
end
function Painter:color(fg,bold,bg)
self.cur_pen = copyall(self.cur_pen)
self.cur_pen.fg = fg
self.cur_pen.bold = bold
if bg then self.cur_pen.bg = bg end
self.cur_pen = to_pen(self.cur_pen, fg, bg, bold)
return self
end
function Painter:key_pen(pen,...)
self.cur_key_pen = to_pen(self.cur_key_pen, pen, ...)
return self
end
@ -210,16 +347,150 @@ function Painter:string(text,pen,...)
return self:advance(#text, nil)
end
function Painter:key(code,pen,...)
return self:string(
getKeyDisplay(code),
to_pen(self.cur_key_pen, pen, ...)
)
end
--------------------------
-- Abstract view object --
--------------------------
View = defclass(View)
View.ATTRS {
active = true,
visible = true,
view_id = DEFAULT_NIL,
}
function View:init(args)
self.subviews = {}
end
function View:addviews(list)
if not list then return end
local sv = self.subviews
for _,obj in ipairs(list) do
table.insert(sv, obj)
local id = obj.view_id
if id and type(id) ~= 'number' and sv[id] == nil then
sv[id] = obj
end
end
for _,dir in ipairs(list) do
for id,obj in pairs(dir.subviews) do
if id and type(id) ~= 'number' and sv[id] == nil then
sv[id] = obj
end
end
end
end
function View:getWindowSize()
local rect = self.frame_body
return rect.width, rect.height
end
function View:getMousePos()
local rect = self.frame_body
local x,y = dscreen.getMousePos()
if rect and rect:inClipGlobalXY(x,y) then
return rect:localXY(x,y)
end
end
function View:computeFrame(parent_rect)
return mkdims_wh(0,0,parent_rect.width,parent_rect.height)
end
function View:updateSubviewLayout(frame_body)
for _,child in ipairs(self.subviews) do
child:updateLayout(frame_body)
end
end
function View:updateLayout(parent_rect)
if not parent_rect then
parent_rect = self.frame_parent_rect
else
self.frame_parent_rect = parent_rect
end
self:invoke_before('preUpdateLayout', parent_rect)
local frame_rect,body_rect = self:computeFrame(parent_rect)
self.frame_rect = frame_rect
self.frame_body = parent_rect:viewport(body_rect or frame_rect)
self:invoke_after('postComputeFrame', self.frame_body)
self:updateSubviewLayout(self.frame_body)
self:invoke_after('postUpdateLayout', self.frame_body)
end
function View:renderSubviews(dc)
for _,child in ipairs(self.subviews) do
if child.visible then
child:render(dc)
end
end
end
function View:render(dc)
self:onRenderFrame(dc, self.frame_rect)
local sub_dc = Painter{
view_rect = self.frame_body,
clip_view = dc
}
self:onRenderBody(sub_dc)
self:renderSubviews(sub_dc)
end
function View:onRenderFrame(dc,rect)
end
function View:onRenderBody(dc)
end
function View:inputToSubviews(keys)
local children = self.subviews
for i=#children,1,-1 do
local child = children[i]
if child.visible and child.active and child:onInput(keys) then
return true
end
end
return false
end
function View:onInput(keys)
return self:inputToSubviews(keys)
end
------------------------
-- Base screen object --
------------------------
Screen = defclass(Screen)
Screen = defclass(Screen, View)
Screen.text_input_mode = false
function Screen:postinit()
self:updateLayout()
self:onResize(dscreen.getWindowSize())
end
Screen.isDismissed = dscreen.isDismissed
@ -236,14 +507,6 @@ function Screen:invalidate()
dscreen.invalidate()
end
function Screen:getWindowSize()
return dscreen.getWindowSize()
end
function Screen:getMousePos()
return dscreen.getMousePos()
end
function Screen:renderParent()
if self._native and self._native.parent then
self._native.parent:render()
@ -258,21 +521,22 @@ function Screen:sendInputToParent(...)
end
end
function Screen:show(below)
function Screen:show(parent)
if self._native then
error("This screen is already on display")
end
self:onAboutToShow(below)
if not dscreen.show(self, below) then
parent = parent or dfhack.gui.getCurViewscreen(true)
self:onAboutToShow(parent)
if not dscreen.show(self, parent.child) then
error('Could not show screen')
end
end
function Screen:onAboutToShow()
function Screen:onAboutToShow(parent)
end
function Screen:onShow()
self:updateLayout()
self:onResize(dscreen.getWindowSize())
end
function Screen:dismiss()
@ -288,10 +552,11 @@ function Screen:onDestroy()
end
function Screen:onResize(w,h)
self:updateLayout()
self:updateLayout(ViewRect{ rect = mkdims_wh(0,0,w,h) })
end
function Screen:updateLayout()
function Screen:onRender()
self:render(Painter.new())
end
------------------------
@ -300,28 +565,28 @@ end
-- Plain grey-colored frame.
GREY_FRAME = {
frame_pen = { ch = ' ', fg = COLOR_BLACK, bg = COLOR_GREY },
title_pen = { fg = COLOR_BLACK, bg = COLOR_WHITE },
signature_pen = { fg = COLOR_BLACK, bg = COLOR_GREY },
frame_pen = to_pen{ ch = ' ', fg = COLOR_BLACK, bg = COLOR_GREY },
title_pen = to_pen{ fg = COLOR_BLACK, bg = COLOR_WHITE },
signature_pen = to_pen{ fg = COLOR_BLACK, bg = COLOR_GREY },
}
-- The usual boundary used by the DF screens. Often has fancy pattern in tilesets.
BOUNDARY_FRAME = {
frame_pen = { ch = 0xDB, fg = COLOR_DARKGREY, bg = COLOR_BLACK },
title_pen = { fg = COLOR_BLACK, bg = COLOR_GREY },
signature_pen = { fg = COLOR_BLACK, bg = COLOR_DARKGREY },
frame_pen = to_pen{ ch = 0xDB, fg = COLOR_DARKGREY, bg = COLOR_BLACK },
title_pen = to_pen{ fg = COLOR_BLACK, bg = COLOR_GREY },
signature_pen = to_pen{ fg = COLOR_BLACK, bg = COLOR_DARKGREY },
}
GREY_LINE_FRAME = {
frame_pen = { ch = 206, fg = COLOR_GREY, bg = COLOR_BLACK },
h_frame_pen = { ch = 205, fg = COLOR_GREY, bg = COLOR_BLACK },
v_frame_pen = { ch = 186, fg = COLOR_GREY, bg = COLOR_BLACK },
lt_frame_pen = { ch = 201, fg = COLOR_GREY, bg = COLOR_BLACK },
lb_frame_pen = { ch = 200, fg = COLOR_GREY, bg = COLOR_BLACK },
rt_frame_pen = { ch = 187, fg = COLOR_GREY, bg = COLOR_BLACK },
rb_frame_pen = { ch = 188, fg = COLOR_GREY, bg = COLOR_BLACK },
title_pen = { fg = COLOR_BLACK, bg = COLOR_GREY },
signature_pen = { fg = COLOR_DARKGREY, bg = COLOR_BLACK },
frame_pen = to_pen{ ch = 206, fg = COLOR_GREY, bg = COLOR_BLACK },
h_frame_pen = to_pen{ ch = 205, fg = COLOR_GREY, bg = COLOR_BLACK },
v_frame_pen = to_pen{ ch = 186, fg = COLOR_GREY, bg = COLOR_BLACK },
lt_frame_pen = to_pen{ ch = 201, fg = COLOR_GREY, bg = COLOR_BLACK },
lb_frame_pen = to_pen{ ch = 200, fg = COLOR_GREY, bg = COLOR_BLACK },
rt_frame_pen = to_pen{ ch = 187, fg = COLOR_GREY, bg = COLOR_BLACK },
rb_frame_pen = to_pen{ ch = 188, fg = COLOR_GREY, bg = COLOR_BLACK },
title_pen = to_pen{ fg = COLOR_BLACK, bg = COLOR_GREY },
signature_pen = to_pen{ fg = COLOR_DARKGREY, bg = COLOR_BLACK },
}
function paint_frame(x1,y1,x2,y2,style,title)
@ -353,68 +618,31 @@ FramedScreen.ATTRS{
frame_title = DEFAULT_NIL,
frame_width = DEFAULT_NIL,
frame_height = DEFAULT_NIL,
frame_inset = 0,
frame_background = CLEAR_PEN,
}
local function hint_coord(gap,hint)
if hint and hint > 0 then
return math.min(hint,gap)
elseif hint and hint < 0 then
return math.max(0,gap-hint)
else
return math.floor(gap/2)
end
end
function FramedScreen:getWantedFrameSize()
return self.frame_width, self.frame_height
end
function FramedScreen:updateFrameSize()
local sw, sh = dscreen.getWindowSize()
local iw, ih = sw-2, sh-2
local fw, fh = self:getWantedFrameSize()
local width = math.min(fw or iw, iw)
local height = math.min(fh or ih, ih)
local gw, gh = iw-width, ih-height
local x1, y1 = hint_coord(gw,self.frame_xhint), hint_coord(gh,self.frame_yhint)
self.frame_rect = mkdims_wh(x1+1,y1+1,width,height)
self.frame_opaque = (gw == 0 and gh == 0)
end
function FramedScreen:updateLayout()
self:updateFrameSize()
function FramedScreen:computeFrame(parent_rect)
local sw, sh = parent_rect.width, parent_rect.height
local fw, fh = self:getWantedFrameSize(parent_rect)
return compute_frame_body(sw, sh, { w = fw, h = fh }, self.frame_inset, 1, true)
end
function FramedScreen:getWindowSize()
local rect = self.frame_rect
return rect.width, rect.height
end
function FramedScreen:onRenderFrame(dc, rect)
local x1,y1,x2,y2 = rect.x1, rect.y1, rect.x2, rect.y2
function FramedScreen:getMousePos()
local rect = self.frame_rect
local x,y = dscreen.getMousePos()
if is_in_rect(rect,x,y) then
return x-rect.x1, y-rect.y1
end
end
function FramedScreen:onRender()
local rect = self.frame_rect
local x1,y1,x2,y2 = rect.x1-1, rect.y1-1, rect.x2+1, rect.y2+1
if self.frame_opaque then
dscreen.clear()
if rect.wgap <= 0 and rect.hgap <= 0 then
dc:clear()
else
self:renderParent()
dscreen.fillRect(CLEAR_PEN,x1,y1,x2,y2)
dc:fill(rect, self.frame_background)
end
paint_frame(x1,y1,x2,y2,self.frame_style,self.frame_title)
self:onRenderBody(Painter.new(rect))
end
function FramedScreen:onRenderBody(dc)
end
return _ENV

@ -0,0 +1,285 @@
-- Stock dialog for selecting buildings
local _ENV = mkmodule('gui.buildings')
local gui = require('gui')
local widgets = require('gui.widgets')
local dlg = require('gui.dialogs')
local utils = require('utils')
ARROW = string.char(26)
WORKSHOP_ABSTRACT={
[df.building_type.Civzone]=true,[df.building_type.Stockpile]=true,
}
WORKSHOP_SPECIAL={
[df.building_type.Workshop]=true,[df.building_type.Furnace]=true,[df.building_type.Trap]=true,
[df.building_type.Construction]=true,[df.building_type.SiegeEngine]=true
}
BuildingDialog = defclass(BuildingDialog, gui.FramedScreen)
BuildingDialog.focus_path = 'BuildingDialog'
BuildingDialog.ATTRS{
prompt = 'Type or select a building from this list',
frame_style = gui.GREY_LINE_FRAME,
frame_inset = 1,
frame_title = 'Select Building',
-- new attrs
none_caption = 'none',
hide_none = false,
use_abstract = true,
use_workshops = true,
use_tool_workshop=true,
use_furnace = true,
use_construction = true,
use_siege = true,
use_trap = true,
use_custom = true,
building_filter = DEFAULT_NIL,
on_select = DEFAULT_NIL,
on_cancel = DEFAULT_NIL,
on_close = DEFAULT_NIL,
}
function BuildingDialog:init(info)
self:addviews{
widgets.Label{
text = {
self.prompt, '\n\n',
'Category: ', { text = self:cb_getfield('context_str'), pen = COLOR_CYAN }
},
text_pen = COLOR_WHITE,
frame = { l = 0, t = 0 },
},
widgets.Label{
view_id = 'back',
visible = false,
text = { { key = 'LEAVESCREEN', text = ': Back' } },
frame = { r = 0, b = 0 },
auto_width = true,
},
widgets.FilteredList{
view_id = 'list',
not_found_label = 'No matching buildings',
frame = { l = 0, r = 0, t = 4, b = 2 },
icon_width = 2,
on_submit = self:callback('onSubmitItem'),
},
widgets.Label{
text = { {
key = 'SELECT', text = ': Select',
disabled = function() return not self.subviews.list:canSubmit() end
} },
frame = { l = 0, b = 0 },
}
}
self:initBuiltinMode()
end
function BuildingDialog:getWantedFrameSize(rect)
return math.max(self.frame_width or 40, #self.prompt), math.min(28, rect.height-8)
end
function BuildingDialog:onDestroy()
if self.on_close then
self.on_close()
end
end
function BuildingDialog:initBuiltinMode()
local choices = {}
if not self.hide_none then
table.insert(choices, { text = self.none_caption, type_id = -1, subtype_id = -1, custom_id=-1})
end
if self.use_workshops then
table.insert(choices, {
icon = ARROW, text = 'workshop', key = 'CUSTOM_SHIFT_W',
cb = self:callback('initWorkshopMode')
})
end
if self.use_furnace then
table.insert(choices, {
icon = ARROW, text = 'furnaces', key = 'CUSTOM_SHIFT_F',
cb = self:callback('initFurnaceMode')
})
end
if self.use_trap then
table.insert(choices, {
icon = ARROW, text = 'traps', key = 'CUSTOM_SHIFT_T',
cb = self:callback('initTrapMode')
})
end
if self.use_construction then
table.insert(choices, {
icon = ARROW, text = 'constructions', key = 'CUSTOM_SHIFT_C',
cb = self:callback('initConstructionMode')
})
end
if self.use_siege then
table.insert(choices, {
icon = ARROW, text = 'siege engine', key = 'CUSTOM_SHIFT_S',
cb = self:callback('initSiegeMode')
})
end
if self.use_custom then
table.insert(choices, {
icon = ARROW, text = 'custom workshop', key = 'CUSTOM_SHIFT_U',
cb = self:callback('initCustomMode')
})
end
for i=0,df.building_type._last_item do
if (not WORKSHOP_ABSTRACT[i] or self.use_abstract)and not WORKSHOP_SPECIAL[i] then
self:addBuilding(choices, df.building_type[i], i, -1,-1,nil)
end
end
self:pushContext('Any building', choices)
end
function BuildingDialog:initWorkshopMode()
local choices = {}
for i=0,df.workshop_type._last_item do
if i~=df.workshop_type.Custom and (i~=df.workshop_type.Tool or self.use_tool_workshop) then
self:addBuilding(choices, df.workshop_type[i], df.building_type.Workshop, i,-1,nil)
end
end
self:pushContext('Workshops', choices)
end
function BuildingDialog:initTrapMode()
local choices = {}
for i=0,df.trap_type._last_item do
self:addBuilding(choices, df.trap_type[i], df.building_type.Trap, i,-1,nil)
end
self:pushContext('Traps', choices)
end
function BuildingDialog:initConstructionMode()
local choices = {}
for i=0,df.construction_type._last_item do
self:addBuilding(choices, df.construction_type[i], df.building_type.Construction, i,-1,nil)
end
self:pushContext('Constructions', choices)
end
function BuildingDialog:initFurnaceMode()
local choices = {}
for i=0,df.furnace_type._last_item do
self:addBuilding(choices, df.furnace_type[i], df.building_type.Furnace, i,-1,nil)
end
self:pushContext('Furnaces', choices)
end
function BuildingDialog:initSiegeMode()
local choices = {}
for i=0,df.siegeengine_type._last_item do
self:addBuilding(choices, df.siegeengine_type[i], df.building_type.SiegeEngine, i,-1,nil)
end
self:pushContext('Siege weapons', choices)
end
function BuildingDialog:initCustomMode()
local choices = {}
local raws=df.global.world.raws.buildings.all
for k,v in pairs(raws) do
self:addBuilding(choices, v.name, df.building_type.Workshop,df.workshop_type.Custom,v.id,v)
end
self:pushContext('Custom workshops', choices)
end
function BuildingDialog:addBuilding(choices, name,type_id, subtype_id, custom_id, parent)
-- Check the filter
if self.building_filter and not self.building_filter(name,type_id,subtype_id,custom_id, parent) then
return
end
table.insert(choices, {
text = name:lower(),
customshop = parent,
type_id = type_id, subtype_id = subtype_id, custom_id=custom_id
})
end
function BuildingDialog:pushContext(name, choices)
if not self.back_stack then
self.back_stack = {}
self.subviews.back.visible = false
else
table.insert(self.back_stack, {
context_str = self.context_str,
all_choices = self.subviews.list:getChoices(),
edit_text = self.subviews.list:getFilter(),
selected = self.subviews.list:getSelected(),
})
self.subviews.back.visible = true
end
self.context_str = name
self.subviews.list:setChoices(choices, 1)
end
function BuildingDialog:onGoBack()
local save = table.remove(self.back_stack)
self.subviews.back.visible = (#self.back_stack > 0)
self.context_str = save.context_str
self.subviews.list:setChoices(save.all_choices)
self.subviews.list:setFilter(save.edit_text, save.selected)
end
function BuildingDialog:submitBuilding(type_id,subtype_id,custom_id,choice,index)
self:dismiss()
if self.on_select then
self.on_select(type_id,subtype_id,custom_id,choice,index)
end
end
function BuildingDialog:onSubmitItem(idx, item)
if item.cb then
item:cb(idx)
else
self:submitBuilding(item.type_id, item.subtype_id,item.custom_id,item,idx)
end
end
function BuildingDialog:onInput(keys)
if keys.LEAVESCREEN or keys.LEAVESCREEN_ALL then
if self.subviews.back.visible and not keys.LEAVESCREEN_ALL then
self:onGoBack()
else
self:dismiss()
if self.on_cancel then
self.on_cancel()
end
end
else
self:inputToSubviews(keys)
end
end
function showBuildingPrompt(title, prompt, on_select, on_cancel, build_filter)
BuildingDialog{
frame_title = title,
prompt = prompt,
building_filter = build_filter,
on_select = on_select,
on_cancel = on_cancel,
}:show()
end
return _ENV

@ -3,6 +3,7 @@
local _ENV = mkmodule('gui.dialogs')
local gui = require('gui')
local widgets = require('gui.widgets')
local utils = require('utils')
local dscreen = dfhack.screen
@ -13,48 +14,35 @@ MessageBox.focus_path = 'MessageBox'
MessageBox.ATTRS{
frame_style = gui.GREY_LINE_FRAME,
frame_inset = 1,
-- new attrs
text = {},
on_accept = DEFAULT_NIL,
on_cancel = DEFAULT_NIL,
on_close = DEFAULT_NIL,
text_pen = DEFAULT_NIL,
}
function MessageBox:preinit(info)
if type(info.text) == 'string' then
info.text = utils.split_string(info.text, "\n")
end
function MessageBox:init(info)
self:addviews{
widgets.Label{
view_id = 'label',
text = info.text,
text_pen = info.text_pen,
frame = { l = 0, t = 0 },
auto_height = true
}
}
end
function MessageBox:getWantedFrameSize()
local text = self.text
local w = #(self.frame_title or '') + 4
w = math.max(w, 20)
w = math.max(self.frame_width or w, w)
for _, l in ipairs(text) do
w = math.max(w, #l)
end
local h = #text+1
if h > 1 then
h = h+1
end
return w+2, #text+2
local label = self.subviews.label
local width = math.max(self.frame_width or 0, 20, #(self.frame_title or '') + 4)
return math.max(width, label:getTextWidth()), label:getTextHeight()
end
function MessageBox:onRenderBody(dc)
if #self.text > 0 then
dc:newline(1):pen(self.text_pen or COLOR_GREY)
for _, l in ipairs(self.text or {}) do
dc:string(l):newline(1)
end
end
function MessageBox:onRenderFrame(dc,rect)
MessageBox.super.onRenderFrame(self,dc,rect)
if self.on_accept then
local x,y = self.frame_rect.x1+1, self.frame_rect.y2+1
dscreen.paintString({fg=COLOR_LIGHTGREEN},x,y,'ESC')
dscreen.paintString({fg=COLOR_GREY},x+3,y,'/')
dscreen.paintString({fg=COLOR_LIGHTGREEN},x+4,y,'y')
dc:seek(rect.x1+2,rect.y2):key('LEAVESCREEN'):string('/'):key('MENU_CONFIRM')
end
end
@ -75,6 +63,8 @@ function MessageBox:onInput(keys)
if self.on_cancel then
self.on_cancel()
end
else
self:inputToSubviews(keys)
end
end
@ -102,8 +92,6 @@ InputBox = defclass(InputBox, MessageBox)
InputBox.focus_path = 'InputBox'
InputBox.ATTRS{
input = '',
input_pen = DEFAULT_NIL,
on_input = DEFAULT_NIL,
}
@ -111,46 +99,36 @@ function InputBox:preinit(info)
info.on_accept = nil
end
function InputBox:init(info)
self:addviews{
widgets.EditField{
view_id = 'edit',
text = info.input,
text_pen = info.input_pen,
frame = { l = 0, r = 0, h = 1 },
}
}
end
function InputBox:getWantedFrameSize()
local mw, mh = InputBox.super.getWantedFrameSize(self)
self.subviews.edit.frame.t = mh+1
return mw, mh+2
end
function InputBox:onRenderBody(dc)
InputBox.super.onRenderBody(self, dc)
dc:newline(1)
dc:pen(self.input_pen or COLOR_LIGHTCYAN)
dc:fill(1,dc:localY(),dc.width-2,dc:localY())
local cursor = '_'
if math.floor(dfhack.getTickCount()/300) % 2 == 0 then
cursor = ' '
end
local txt = self.input .. cursor
if #txt > dc.width-2 then
txt = string.char(27)..string.sub(txt, #txt-dc.width+4)
end
dc:string(txt)
end
function InputBox:onInput(keys)
if keys.SELECT then
self:dismiss()
if self.on_input then
self.on_input(self.input)
self.on_input(self.subviews.edit.text)
end
elseif keys.LEAVESCREEN then
self:dismiss()
if self.on_cancel then
self.on_cancel()
end
elseif keys._STRING then
if keys._STRING == 0 then
self.input = string.sub(self.input, 1, #self.input-1)
else
self.input = self.input .. string.char(keys._STRING)
end
self:inputToSubviews(keys)
end
end
@ -171,97 +149,92 @@ ListBox = defclass(ListBox, MessageBox)
ListBox.focus_path = 'ListBox'
ListBox.ATTRS{
selection = 0,
choices = {},
with_filter = false,
cursor_pen = DEFAULT_NIL,
select_pen = DEFAULT_NIL,
on_input = DEFAULT_NIL
on_select = DEFAULT_NIL,
on_select2 = DEFAULT_NIL,
select2_hint = DEFAULT_NIL,
}
function InputBox:preinit(info)
function ListBox:preinit(info)
info.on_accept = nil
end
function ListBox:init(info)
self.page_top = 0
end
function ListBox:getWantedFrameSize()
local mw, mh = ListBox.super.getWantedFrameSize(self)
return mw, mh+#self.choices
end
function ListBox:onRenderBody(dc)
ListBox.super.onRenderBody(self, dc)
local spen = dfhack.pen.parse(COLOR_CYAN, self.select_pen, nil, false)
local cpen = dfhack.pen.parse(COLOR_LIGHTCYAN, self.cursor_pen or self.select_pen, nil, true)
dc:newline(1)
if self.selection>dc.height-3 then
self.page_top=self.selection-(dc.height-3)
elseif self.selection<self.page_top and self.selection >0 then
self.page_top=self.selection-1
end
for i,entry in ipairs(self.choices) do
if type(entry)=="table" then
entry=entry[1]
end
if i>self.page_top then
if i == self.selection then
dc:pen(self.select_pen or COLOR_LIGHTCYAN)
else
dc:pen(self.text_pen or COLOR_GREY)
local list_widget = widgets.List
if self.with_filter then
list_widget = widgets.FilteredList
end
dc:string(entry)
dc:newline(1)
local on_submit2
if self.select2_hint or self.on_select2 then
on_submit2 = function(sel, obj)
self:dismiss()
if self.on_select2 then self.on_select2(sel, obj) end
local cb = obj.on_select2
if cb then cb(obj, sel) end
end
end
self:addviews{
list_widget{
view_id = 'list',
selected = info.selected,
choices = info.choices,
icon_width = info.icon_width,
text_pen = spen,
cursor_pen = cpen,
on_submit = function(sel,obj)
self:dismiss()
if self.on_select then self.on_select(sel, obj) end
local cb = obj.on_select or obj[2]
if cb then cb(obj, sel) end
end,
on_submit2 = on_submit2,
frame = { l = 0, r = 0 },
}
}
end
function ListBox:moveCursor(delta)
local newsel=self.selection+delta
if #self.choices ~=0 then
if newsel<1 or newsel>#self.choices then
newsel=newsel % #self.choices
end
function ListBox:onRenderFrame(dc,rect)
ListBox.super.onRenderFrame(self,dc,rect)
if self.select2_hint then
dc:seek(rect.x1+2,rect.y2):key('SEC_SELECT'):string(': '..self.select2_hint,COLOR_DARKGREY)
end
self.selection=newsel
end
function ListBox:onInput(keys)
if keys.SELECT then
self:dismiss()
local choice=self.choices[self.selection]
if self.on_input then
self.on_input(self.selection,choice)
end
function ListBox:getWantedFrameSize()
local mw, mh = InputBox.super.getWantedFrameSize(self)
local list = self.subviews.list
list.frame.t = mh+1
return math.max(mw, list:getContentWidth()), mh+1+math.min(20,list:getContentHeight())
end
if choice and choice[2] then
choice[2](choice,self.selection) -- maybe reverse the arguments?
end
elseif keys.LEAVESCREEN then
function ListBox:onInput(keys)
if keys.LEAVESCREEN then
self:dismiss()
if self.on_cancel then
self.on_cancel()
end
elseif keys.CURSOR_UP then
self:moveCursor(-1)
elseif keys.CURSOR_DOWN then
self:moveCursor(1)
elseif keys.CURSOR_UP_FAST then
self:moveCursor(-10)
elseif keys.CURSOR_DOWN_FAST then
self:moveCursor(10)
else
self:inputToSubviews(keys)
end
end
function showListPrompt(title, text, tcolor, choices, on_input, on_cancel, min_width)
function showListPrompt(title, text, tcolor, choices, on_select, on_cancel, min_width, filter)
ListBox{
frame_title = title,
text = text,
text_pen = tcolor,
choices = choices,
on_input = on_input,
on_select = on_select,
on_cancel = on_cancel,
frame_width = min_width,
with_filter = filter,
}:show()
end

@ -238,6 +238,19 @@ local function get_movement_delta(key, delta, big_step)
end
end
HOTKEY_KEYS = {}
for i,v in ipairs(df.global.ui.main.hotkeys) do
HOTKEY_KEYS['D_HOTKEY'..(i+1)] = v
end
local function get_hotkey_target(key)
local hk = HOTKEY_KEYS[key]
if hk and hk.cmd == df.ui_hotkey.T_cmd.Zoom then
return xyz2pos(hk.x, hk.y, hk.z)
end
end
function Viewport:scrollByKey(key)
local dx, dy, dz = get_movement_delta(key, 10, 20)
if dx then
@ -246,15 +259,21 @@ function Viewport:scrollByKey(key)
self.y1 + dy,
self.z + dz
)
else
local hk_pos = get_hotkey_target(key)
if hk_pos then
return self:centerOn(hk_pos)
else
return self
end
end
end
DwarfOverlay = defclass(DwarfOverlay, gui.Screen)
function DwarfOverlay:updateLayout()
function DwarfOverlay:updateLayout(parent_rect)
self.df_layout = getPanelLayout()
DwarfOverlay.super.updateLayout(self, parent_rect)
end
function DwarfOverlay:getViewport(old_vp)
@ -285,9 +304,11 @@ function DwarfOverlay:selectBuilding(building,cursor,viewport,gap)
end
function DwarfOverlay:propagateMoveKeys(keys)
for code,_ in pairs(MOVEMENT_KEYS) do
if keys[code] then
for code,_ in pairs(keys) do
if MOVEMENT_KEYS[code] or HOTKEY_KEYS[code] then
if not HOTKEY_KEYS[code] or get_hotkey_target(code) then
self:sendInputToParent(code)
end
return code
end
end
@ -304,8 +325,8 @@ function DwarfOverlay:simulateViewScroll(keys, anchor, no_clip_cursor)
return 'A_MOVE_SAME_SQUARE'
end
for code,_ in pairs(MOVEMENT_KEYS) do
if keys[code] then
for code,_ in pairs(keys) do
if MOVEMENT_KEYS[code] or HOTKEY_KEYS[code] then
local vp = self:getViewport():scrollByKey(code)
if (cursor and not no_clip_cursor) or no_clip_cursor == false then
vp = vp:reveal(anchor,4,20,4,true)
@ -328,37 +349,46 @@ function DwarfOverlay:simulateCursorMovement(keys, anchor)
return 'A_MOVE_SAME_SQUARE'
end
for code,_ in pairs(MOVEMENT_KEYS) do
if keys[code] then
for code,_ in pairs(keys) do
if MOVEMENT_KEYS[code] then
local dx, dy, dz = get_movement_delta(code, 1, 10)
local ncur = xyz2pos(cx+dx, cy+dy, cz+dz)
if dfhack.maps.isValidTilePos(ncur) then
setCursorPos(ncur)
self:getViewport():reveal(ncur,4,10,6,true):set()
end
return code
elseif HOTKEY_KEYS[code] then
local pos = get_hotkey_target(code)
if pos and dfhack.maps.isValidTilePos(pos) then
setCursorPos(pos)
self:getViewport():centerOn(pos):set()
end
return code
end
end
end
function DwarfOverlay:onAboutToShow(below)
local screen = dfhack.gui.getCurViewscreen()
if below then screen = below.parent end
if not df.viewscreen_dwarfmodest:is_instance(screen) then
function DwarfOverlay:onAboutToShow(parent)
if not df.viewscreen_dwarfmodest:is_instance(parent) then
error("This screen requires the main dwarfmode view")
end
end
MenuOverlay = defclass(MenuOverlay, DwarfOverlay)
function MenuOverlay:updateLayout()
MenuOverlay.super.updateLayout(self)
self.frame_rect = self.df_layout.menu
end
MenuOverlay.ATTRS {
frame_inset = 0,
frame_background = CLEAR_PEN,
}
MenuOverlay.getWindowSize = gui.FramedScreen.getWindowSize
MenuOverlay.getMousePos = gui.FramedScreen.getMousePos
function MenuOverlay:computeFrame(parent_rect)
return self.df_layout.menu, gui.inset_frame(self.df_layout.menu, self.frame_inset)
end
function MenuOverlay:onAboutToShow(below)
MenuOverlay.super.onAboutToShow(self,below)
@ -369,7 +399,7 @@ function MenuOverlay:onAboutToShow(below)
end
end
function MenuOverlay:onRender()
function MenuOverlay:render(dc)
self:renderParent()
local menu = self.df_layout.menu
@ -380,7 +410,11 @@ function MenuOverlay:onRender()
menu.x1+1, menu.y2+1, "DFHack"
)
self:onRenderBody(gui.Painter.new(menu))
if self.frame_background then
dc:fill(menu, self.frame_background)
end
MenuOverlay.super.render(self, dc)
end
end

@ -0,0 +1,337 @@
-- Stock dialog for selecting materials
local _ENV = mkmodule('gui.materials')
local gui = require('gui')
local widgets = require('gui.widgets')
local dlg = require('gui.dialogs')
local utils = require('utils')
ARROW = string.char(26)
CREATURE_BASE = 19
PLANT_BASE = 419
MaterialDialog = defclass(MaterialDialog, gui.FramedScreen)
MaterialDialog.focus_path = 'MaterialDialog'
MaterialDialog.ATTRS{
prompt = 'Type or select a material from this list',
frame_style = gui.GREY_LINE_FRAME,
frame_inset = 1,
frame_title = 'Select Material',
-- new attrs
none_caption = 'none',
hide_none = false,
use_inorganic = true,
use_creature = true,
use_plant = true,
mat_filter = DEFAULT_NIL,
on_select = DEFAULT_NIL,
on_cancel = DEFAULT_NIL,
on_close = DEFAULT_NIL,
}
function MaterialDialog:init(info)
self:addviews{
widgets.Label{
text = {
self.prompt, '\n\n',
'Category: ', { text = self:cb_getfield('context_str'), pen = COLOR_CYAN }
},
text_pen = COLOR_WHITE,
frame = { l = 0, t = 0 },
},
widgets.Label{
view_id = 'back',
visible = false,
text = { { key = 'LEAVESCREEN', text = ': Back' } },
frame = { r = 0, b = 0 },
auto_width = true,
},
widgets.FilteredList{
view_id = 'list',
not_found_label = 'No matching materials',
frame = { l = 0, r = 0, t = 4, b = 2 },
icon_width = 2,
on_submit = self:callback('onSubmitItem'),
},
widgets.Label{
text = { {
key = 'SELECT', text = ': Select',
disabled = function() return not self.subviews.list:canSubmit() end
} },
frame = { l = 0, b = 0 },
}
}
self:initBuiltinMode()
end
function MaterialDialog:getWantedFrameSize(rect)
return math.max(self.frame_width or 40, #self.prompt), math.min(28, rect.height-8)
end
function MaterialDialog:onDestroy()
if self.on_close then
self.on_close()
end
end
function MaterialDialog:initBuiltinMode()
local choices = {}
if not self.hide_none then
table.insert(choices, { text = self.none_caption, mat_type = -1, mat_index = -1 })
end
if self.use_inorganic then
table.insert(choices, {
icon = ARROW, text = 'inorganic', key = 'CUSTOM_SHIFT_I',
cb = self:callback('initInorganicMode')
})
end
if self.use_creature then
table.insert(choices, {
icon = ARROW, text = 'creature', key = 'CUSTOM_SHIFT_C',
cb = self:callback('initCreatureMode')
})
end
if self.use_plant then
table.insert(choices, {
icon = ARROW, text = 'plant', key = 'CUSTOM_SHIFT_P',
cb = self:callback('initPlantMode')
})
end
local table = df.global.world.raws.mat_table.builtin
for i=0,df.builtin_mats._last_item do
self:addMaterial(choices, table[i], i, -1, false, nil)
end
self:pushContext('Any material', choices)
end
function MaterialDialog:initInorganicMode()
local choices = {}
for i,mat in ipairs(df.global.world.raws.inorganics) do
self:addMaterial(choices, mat.material, 0, i, false, mat)
end
self:pushContext('Inorganic materials', choices)
end
function MaterialDialog:initCreatureMode()
local choices = {}
for i,v in ipairs(df.global.world.raws.creatures.all) do
self:addObjectChoice(choices, v, v.name[0], CREATURE_BASE, i)
end
self:pushContext('Creature materials', choices)
end
function MaterialDialog:initPlantMode()
local choices = {}
for i,v in ipairs(df.global.world.raws.plants.all) do
self:addObjectChoice(choices, v, v.name, PLANT_BASE, i)
end
self:pushContext('Plant materials', choices)
end
function MaterialDialog:addObjectChoice(choices, obj, name, typ, index)
-- Check if any eligible children
local count = #obj.material
local idx = 0
if self.mat_filter then
count = 0
for i,v in ipairs(obj.material) do
if self.mat_filter(v, obj, typ+i, index) then
count = count + 1
if count > 1 then break end
idx = i
end
end
end
-- Create an entry
if count < 1 then
return
elseif count == 1 then
self:addMaterial(choices, obj.material[idx], typ+idx, index, true, obj)
else
table.insert(choices, {
icon = ARROW, text = name, mat_type = typ, mat_index = index,
obj = obj, cb = self:callback('onSelectObj')
})
end
end
function MaterialDialog:onSelectObj(item)
local choices = {}
for i,v in ipairs(item.obj.material) do
self:addMaterial(choices, v, item.mat_type+i, item.mat_index, false, item.obj)
end
self:pushContext(item.text, choices)
end
function MaterialDialog:addMaterial(choices, mat, typ, idx, pfix, parent)
-- Check the filter
if self.mat_filter and not self.mat_filter(mat, parent, typ, idx) then
return
end
-- Find the material name
local state = 0
if mat.heat.melting_point <= 10015 then
state = 1
end
local name = mat.state_name[state]
name = string.gsub(name, '^frozen ','')
name = string.gsub(name, '^molten ','')
name = string.gsub(name, '^condensed ','')
-- Add prefix if requested
local key
if pfix and mat.prefix ~= '' then
name = mat.prefix .. ' ' .. name
key = mat.prefix
end
table.insert(choices, {
text = name,
search_key = key,
material = mat,
mat_type = typ, mat_index = idx
})
end
function MaterialDialog:pushContext(name, choices)
if not self.back_stack then
self.back_stack = {}
self.subviews.back.visible = false
else
table.insert(self.back_stack, {
context_str = self.context_str,
all_choices = self.subviews.list:getChoices(),
edit_text = self.subviews.list:getFilter(),
selected = self.subviews.list:getSelected(),
})
self.subviews.back.visible = true
end
self.context_str = name
self.subviews.list:setChoices(choices, 1)
end
function MaterialDialog:onGoBack()
local save = table.remove(self.back_stack)
self.subviews.back.visible = (#self.back_stack > 0)
self.context_str = save.context_str
self.subviews.list:setChoices(save.all_choices)
self.subviews.list:setFilter(save.edit_text, save.selected)
end
function MaterialDialog:submitMaterial(typ, index)
self:dismiss()
if self.on_select then
self.on_select(typ, index)
end
end
function MaterialDialog:onSubmitItem(idx, item)
if item.cb then
item:cb(idx)
else
self:submitMaterial(item.mat_type, item.mat_index)
end
end
function MaterialDialog:onInput(keys)
if keys.LEAVESCREEN or keys.LEAVESCREEN_ALL then
if self.subviews.back.visible and not keys.LEAVESCREEN_ALL then
self:onGoBack()
else
self:dismiss()
if self.on_cancel then
self.on_cancel()
end
end
else
self:inputToSubviews(keys)
end
end
function showMaterialPrompt(title, prompt, on_select, on_cancel, mat_filter)
MaterialDialog{
frame_title = title,
prompt = prompt,
mat_filter = mat_filter,
on_select = on_select,
on_cancel = on_cancel,
}:show()
end
function ItemTypeDialog(args)
args.text = args.prompt or 'Type or select an item type'
args.text_pen = COLOR_WHITE
args.with_filter = true
args.icon_width = 2
local choices = {}
if not args.hide_none then
table.insert(choices, {
icon = '?', text = args.none_caption or 'none',
item_type = -1, item_subtype = -1
})
end
local filter = args.item_filter
for itype = 0,df.item_type._last_item do
local attrs = df.item_type.attrs[itype]
local defcnt = dfhack.items.getSubtypeCount(itype)
if not filter or filter(itype,-1) then
local name = attrs.caption or df.item_type[itype]
local icon
if defcnt >= 0 then
name = 'any '..name
icon = '+'
end
table.insert(choices, {
icon = icon, text = string.lower(name), item_type = itype, item_subtype = -1
})
end
if defcnt > 0 then
for subtype = 0,defcnt-1 do
local def = dfhack.items.getSubtypeDef(itype, subtype)
if not filter or filter(itype,subtype,def) then
table.insert(choices, {
icon = '\x1e', text = ' '..def.name, item_type = itype, item_subtype = subtype
})
end
end
end
end
args.choices = choices
if args.on_select then
local cb = args.on_select
args.on_select = function(idx, obj)
return cb(obj.item_type, obj.item_subtype)
end
end
return dlg.ListBox(args)
end
return _ENV

@ -0,0 +1,164 @@
-- Support for scripted interaction sequences via coroutines.
local _ENV = mkmodule('gui.script')
local dlg = require('gui.dialogs')
--[[
Example:
start(function()
sleep(100, 'frames')
print(showYesNoPrompt('test', 'print true?'))
end)
]]
-- Table of running background scripts.
if not scripts then
scripts = {}
setmetatable(scripts, { __mode = 'k' })
end
local function do_resume(inst, ...)
inst.gen = inst.gen + 1
return (dfhack.saferesume(inst.coro, ...))
end
-- Starts a new background script by calling the function.
function start(fn,...)
local coro = coroutine.create(fn)
local inst = {
coro = coro,
gen = 0,
}
scripts[coro] = inst
return do_resume(inst, ...)
end
-- Checks if called from a background script
function in_script()
return scripts[coroutine.running()] ~= nil
end
local function getinst()
local inst = scripts[coroutine.running()]
if not inst then
error('Not in a gui script coroutine.')
end
return inst
end
local function invoke_resume(inst,gen,quiet,...)
local state = coroutine.status(inst.coro)
if state ~= 'suspended' then
if state ~= 'dead' then
dfhack.printerr(debug.traceback('resuming a non-waiting continuation'))
end
elseif inst.gen > gen then
if not quiet then
dfhack.printerr(debug.traceback('resuming an expired continuation'))
end
else
do_resume(inst, ...)
end
end
-- Returns a callback that resumes the script from wait with given return values
function mkresume(...)
local inst = getinst()
return curry(invoke_resume, inst, inst.gen, false, ...)
end
-- Like mkresume, but does not complain if already resumed from this wait
function qresume(...)
local inst = getinst()
return curry(invoke_resume, inst, inst.gen, true, ...)
end
-- Wait until a mkresume callback is called, then return its arguments.
-- Once it returns, all mkresume callbacks created before are invalidated.
function wait()
getinst() -- check that the context is right
return coroutine.yield()
end
-- Wraps dfhack.timeout for coroutines.
function sleep(time, quantity)
if dfhack.timeout(time, quantity, mkresume()) then
wait()
return true
else
return false
end
end
-- Some dialog wrappers:
function showMessage(title, text, tcolor)
dlg.MessageBox{
frame_title = title,
text = text,
text_pen = tcolor,
on_close = qresume(nil)
}:show()
return wait()
end
function showYesNoPrompt(title, text, tcolor)
dlg.MessageBox{
frame_title = title,
text = text,
text_pen = tcolor,
on_accept = mkresume(true),
on_cancel = mkresume(false),
on_close = qresume(nil)
}:show()
return wait()
end
function showInputPrompt(title, text, tcolor, input, min_width)
dlg.InputBox{
frame_title = title,
text = text,
text_pen = tcolor,
input = input,
frame_width = min_width,
on_input = mkresume(true),
on_cancel = mkresume(false),
on_close = qresume(nil)
}:show()
return wait()
end
function showListPrompt(title, text, tcolor, choices, min_width, filter)
dlg.ListBox{
frame_title = title,
text = text,
text_pen = tcolor,
choices = choices,
frame_width = min_width,
with_filter = filter,
on_select = mkresume(true),
on_cancel = mkresume(false),
on_close = qresume(nil)
}:show()
return wait()
end
function showMaterialPrompt(title, prompt)
require('gui.materials').MaterialDialog{
frame_title = title,
prompt = prompt,
on_select = mkresume(true,
on_cancel = mkresume(false),
on_close = qresume(nil)
}:show()
return wait()
end
return _ENV

@ -0,0 +1,783 @@
-- Simple widgets for screens
local _ENV = mkmodule('gui.widgets')
local gui = require('gui')
local utils = require('utils')
local dscreen = dfhack.screen
local function show_view(view,vis)
if view then
view.visible = vis
end
end
local function getval(obj)
if type(obj) == 'function' then
return obj()
else
return obj
end
end
local function map_opttab(tab,idx)
if tab then
return tab[idx]
else
return idx
end
end
------------
-- Widget --
------------
Widget = defclass(Widget, gui.View)
Widget.ATTRS {
frame = DEFAULT_NIL,
frame_inset = DEFAULT_NIL,
frame_background = DEFAULT_NIL,
}
function Widget:computeFrame(parent_rect)
local sw, sh = parent_rect.width, parent_rect.height
return gui.compute_frame_body(sw, sh, self.frame, self.frame_inset)
end
function Widget:onRenderFrame(dc, rect)
if self.frame_background then
dc:fill(rect, self.frame_background)
end
end
-----------
-- Panel --
-----------
Panel = defclass(Panel, Widget)
Panel.ATTRS {
on_render = DEFAULT_NIL,
on_layout = DEFAULT_NIL,
}
function Panel:init(args)
self:addviews(args.subviews)
end
function Panel:onRenderBody(dc)
if self.on_render then self.on_render(dc) end
end
function Panel:postComputeFrame(body)
if self.on_layout then self.on_layout(body) end
end
-----------
-- Pages --
-----------
Pages = defclass(Pages, Panel)
function Pages:init(args)
for _,v in ipairs(self.subviews) do
v.visible = false
end
self:setSelected(args.selected or 1)
end
function Pages:setSelected(idx)
if type(idx) ~= 'number' then
local key = idx
if type(idx) == 'string' then
key = self.subviews[key]
end
idx = utils.linear_index(self.subviews, key)
if not idx then
error('Unknown page: '..key)
end
end
show_view(self.subviews[self.selected], false)
self.selected = math.min(math.max(1, idx), #self.subviews)
show_view(self.subviews[self.selected], true)
end
function Pages:getSelected()
return self.selected, self.subviews[self.selected]
end
----------------
-- Edit field --
----------------
EditField = defclass(EditField, Widget)
EditField.ATTRS{
text = '',
text_pen = DEFAULT_NIL,
on_char = DEFAULT_NIL,
on_change = DEFAULT_NIL,
on_submit = DEFAULT_NIL,
}
function EditField:onRenderBody(dc)
dc:pen(self.text_pen or COLOR_LIGHTCYAN):fill(0,0,dc.width-1,0)
local cursor = '_'
if not self.active or gui.blink_visible(300) then
cursor = ' '
end
local txt = self.text .. cursor
if #txt > dc.width then
txt = string.char(27)..string.sub(txt, #txt-dc.width+2)
end
dc:string(txt)
end
function EditField:onInput(keys)
if self.on_submit and keys.SELECT then
self.on_submit(self.text)
return true
elseif keys._STRING then
local old = self.text
if keys._STRING == 0 then
self.text = string.sub(old, 1, #old-1)
else
local cv = string.char(keys._STRING)
if not self.on_char or self.on_char(cv, old) then
self.text = old .. cv
end
end
if self.on_change and self.text ~= old then
self.on_change(self.text, old)
end
return true
end
end
-----------
-- Label --
-----------
function parse_label_text(obj)
local text = obj.text or {}
if type(text) ~= 'table' then
text = { text }
end
local curline = nil
local out = { }
local active = nil
local idtab = nil
for _,v in ipairs(text) do
local vv
if type(v) == 'string' then
vv = utils.split_string(v, NEWLINE)
else
vv = { v }
end
for i = 1,#vv do
local cv = vv[i]
if i > 1 then
if not curline then
table.insert(out, {})
end
curline = nil
end
if cv ~= '' then
if not curline then
curline = {}
table.insert(out, curline)
end
if type(cv) == 'string' then
table.insert(curline, { text = cv })
else
table.insert(curline, cv)
if cv.on_activate then
active = active or {}
table.insert(active, cv)
end
if cv.id then
idtab = idtab or {}
idtab[cv.id] = cv
end
end
end
end
end
obj.text_lines = out
obj.text_active = active
obj.text_ids = idtab
end
local function is_disabled(token)
return (token.disabled ~= nil and getval(token.disabled)) or
(token.enabled ~= nil and not getval(token.enabled))
end
function render_text(obj,dc,x0,y0,pen,dpen,disabled)
local width = 0
for iline,line in ipairs(obj.text_lines) do
local x = 0
if dc then
dc:seek(x+x0,y0+iline-1)
end
for _,token in ipairs(line) do
token.line = iline
token.x1 = x
if token.gap then
x = x + token.gap
if dc then
dc:advance(token.gap)
end
end
if token.tile then
x = x + 1
if dc then
dc:char(nil, token.tile)
end
end
if token.text or token.key then
local text = ''..(getval(token.text) or '')
local keypen
if dc then
local tpen = getval(token.pen)
if disabled or is_disabled(token) then
dc:pen(getval(token.dpen) or tpen or dpen)
keypen = COLOR_GREEN
else
dc:pen(tpen or pen)
keypen = COLOR_LIGHTGREEN
end
end
local width = getval(token.width)
local padstr
if width then
x = x + width
if #text > width then
text = string.sub(text,1,width)
else
if token.pad_char then
padstr = string.rep(token.pad_char,width-#text)
end
if dc and token.rjustify then
if padstr then dc:string(padstr) else dc:advance(width-#text) end
end
end
else
x = x + #text
end
if token.key then
local keystr = gui.getKeyDisplay(token.key)
local sep = token.key_sep or ''
x = x + #keystr
if sep == '()' then
if dc then
dc:string(text)
dc:string(' ('):string(keystr,keypen):string(')')
end
x = x + 3
else
if dc then
dc:string(keystr,keypen):string(sep):string(text)
end
x = x + #sep
end
else
if dc then
dc:string(text)
end
end
if width and dc and not token.rjustify then
if padstr then dc:string(padstr) else dc:advance(width-#text) end
end
end
token.x2 = x
end
width = math.max(width, x)
end
obj.text_width = width
end
function check_text_keys(self, keys)
if self.text_active then
for _,item in ipairs(self.text_active) do
if item.key and keys[item.key] and not is_disabled(item) then
item.on_activate()
return true
end
end
end
end
Label = defclass(Label, Widget)
Label.ATTRS{
text_pen = COLOR_WHITE,
text_dpen = COLOR_DARKGREY,
disabled = DEFAULT_NIL,
enabled = DEFAULT_NIL,
auto_height = true,
auto_width = false,
}
function Label:init(args)
self:setText(args.text)
end
function Label:setText(text)
self.text = text
parse_label_text(self)
if self.auto_height then
self.frame = self.frame or {}
self.frame.h = self:getTextHeight()
end
end
function Label:preUpdateLayout()
if self.auto_width then
self.frame = self.frame or {}
self.frame.w = self:getTextWidth()
end
end
function Label:itemById(id)
if self.text_ids then
return self.text_ids[id]
end
end
function Label:getTextHeight()
return #self.text_lines
end
function Label:getTextWidth()
render_text(self)
return self.text_width
end
function Label:onRenderBody(dc)
render_text(self,dc,0,0,self.text_pen,self.text_dpen,is_disabled(self))
end
function Label:onInput(keys)
if not is_disabled(self) then
return check_text_keys(self, keys)
end
end
----------
-- List --
----------
List = defclass(List, Widget)
STANDARDSCROLL = {
STANDARDSCROLL_UP = -1,
STANDARDSCROLL_DOWN = 1,
STANDARDSCROLL_PAGEUP = '-page',
STANDARDSCROLL_PAGEDOWN = '+page',
}
SECONDSCROLL = {
SECONDSCROLL_UP = -1,
SECONDSCROLL_DOWN = 1,
SECONDSCROLL_PAGEUP = '-page',
SECONDSCROLL_PAGEDOWN = '+page',
}
List.ATTRS{
text_pen = COLOR_CYAN,
cursor_pen = COLOR_LIGHTCYAN,
inactive_pen = DEFAULT_NIL,
on_select = DEFAULT_NIL,
on_submit = DEFAULT_NIL,
on_submit2 = DEFAULT_NIL,
row_height = 1,
scroll_keys = STANDARDSCROLL,
icon_width = DEFAULT_NIL,
}
function List:init(info)
self.page_top = 1
self.page_size = 1
if info.choices then
self:setChoices(info.choices, info.selected)
else
self.choices = {}
self.selected = 1
end
end
function List:setChoices(choices, selected)
self.choices = choices or {}
for i,v in ipairs(self.choices) do
if type(v) ~= 'table' then
v = { text = v }
self.choices[i] = v
end
v.text = v.text or v.caption or v[1]
parse_label_text(v)
end
self:setSelected(selected)
end
function List:setSelected(selected)
self.selected = selected or self.selected or 1
self:moveCursor(0, true)
return self.selected
end
function List:getChoices()
return self.choices
end
function List:getSelected()
if #self.choices > 0 then
return self.selected, self.choices[self.selected]
end
end
function List:getContentWidth()
local width = 0
for i,v in ipairs(self.choices) do
render_text(v)
local roww = v.text_width
if v.key then
roww = roww + 3 + #gui.getKeyDisplay(v.key)
end
width = math.max(width, roww)
end
return width + (self.icon_width or 0)
end
function List:getContentHeight()
return #self.choices * self.row_height
end
function List:postComputeFrame(body)
self.page_size = math.max(1, math.floor(body.height / self.row_height))
self:moveCursor(0)
end
function List:moveCursor(delta, force_cb)
local page = math.max(1, self.page_size)
local cnt = #self.choices
if cnt < 1 then
self.page_top = 1
self.selected = 1
if force_cb and self.on_select then
self.on_select(nil,nil)
end
return
end
local off = self.selected+delta-1
local ds = math.abs(delta)
if ds > 1 then
if off >= cnt+ds-1 then
off = 0
else
off = math.min(cnt-1, off)
end
if off <= -ds then
off = cnt-1
else
off = math.max(0, off)
end
end
self.selected = 1 + off % cnt
self.page_top = 1 + page * math.floor((self.selected-1) / page)
if (force_cb or delta ~= 0) and self.on_select then
self.on_select(self:getSelected())
end
end
function List:onRenderBody(dc)
local choices = self.choices
local top = self.page_top
local iend = math.min(#choices, top+self.page_size-1)
local iw = self.icon_width
local function paint_icon(icon, obj)
if type(icon) ~= 'string' then
dc:char(nil,icon)
else
if current then
dc:string(icon, obj.icon_pen or self.icon_pen or cur_pen)
else
dc:string(icon, obj.icon_pen or self.icon_pen or cur_dpen)
end
end
end
for i = top,iend do
local obj = choices[i]
local current = (i == self.selected)
local cur_pen = self.cursor_pen
local cur_dpen = self.text_pen
if not self.active then
cur_pen = self.inactive_pen or self.cursor_pen
end
local y = (i - top)*self.row_height
local icon = getval(obj.icon)
if iw and icon then
dc:seek(0, y)
paint_icon(icon, obj)
end
render_text(obj, dc, iw or 0, y, cur_pen, cur_dpen, not current)
local ip = dc.width
if obj.key then
local keystr = gui.getKeyDisplay(obj.key)
ip = ip-2-#keystr
dc:seek(ip,y):pen(self.text_pen)
dc:string('('):string(keystr,COLOR_LIGHTGREEN):string(')')
end
if icon and not iw then
dc:seek(ip-1,y)
paint_icon(icon, obj)
end
end
end
function List:submit()
if self.on_submit and #self.choices > 0 then
self.on_submit(self:getSelected())
end
end
function List:submit2()
if self.on_submit2 and #self.choices > 0 then
self.on_submit2(self:getSelected())
end
end
function List:onInput(keys)
if self.on_submit and keys.SELECT then
self:submit()
return true
elseif self.on_submit2 and keys.SEC_SELECT then
self:submit2()
return true
else
for k,v in pairs(self.scroll_keys) do
if keys[k] then
if v == '+page' then
v = self.page_size
elseif v == '-page' then
v = -self.page_size
end
self:moveCursor(v)
return true
end
end
for i,v in ipairs(self.choices) do
if v.key and keys[v.key] then
self:setSelected(i)
self:submit()
return true
end
end
local current = self.choices[self.selected]
if current then
return check_text_keys(current, keys)
end
end
end
-------------------
-- Filtered List --
-------------------
FilteredList = defclass(FilteredList, Widget)
FilteredList.ATTRS {
edit_below = false,
}
function FilteredList:init(info)
self.edit = EditField{
text_pen = info.edit_pen or info.cursor_pen,
frame = { l = info.icon_width, t = 0, h = 1 },
on_change = self:callback('onFilterChange'),
on_char = self:callback('onFilterChar'),
}
self.list = List{
frame = { t = 2 },
text_pen = info.text_pen,
cursor_pen = info.cursor_pen,
inactive_pen = info.inactive_pen,
icon_pen = info.icon_pen,
row_height = info.row_height,
scroll_keys = info.scroll_keys,
icon_width = info.icon_width,
}
if self.edit_below then
self.edit.frame = { l = info.icon_width, b = 0, h = 1 }
self.list.frame = { t = 0, b = 2 }
end
if info.on_select then
self.list.on_select = function()
return info.on_select(self:getSelected())
end
end
if info.on_submit then
self.list.on_submit = function()
return info.on_submit(self:getSelected())
end
end
if info.on_submit2 then
self.list.on_submit2 = function()
return info.on_submit2(self:getSelected())
end
end
self.not_found = Label{
visible = true,
text = info.not_found_label or 'No matches',
text_pen = COLOR_LIGHTRED,
frame = { l = info.icon_width, t = self.list.frame.t },
}
self:addviews{ self.edit, self.list, self.not_found }
if info.choices then
self:setChoices(info.choices, info.selected)
else
self.choices = {}
end
end
function FilteredList:getChoices()
return self.choices
end
function FilteredList:setChoices(choices, pos)
choices = choices or {}
self.choices = choices
self.edit.text = ''
self.list:setChoices(choices, pos)
self.not_found.visible = (#choices == 0)
end
function FilteredList:submit()
return self.list:submit()
end
function FilteredList:submit2()
return self.list:submit2()
end
function FilteredList:canSubmit()
return not self.not_found.visible
end
function FilteredList:getSelected()
local i,v = self.list:getSelected()
if i then
return map_opttab(self.choice_index, i), v
end
end
function FilteredList:getContentWidth()
return self.list:getContentWidth()
end
function FilteredList:getContentHeight()
return self.list:getContentHeight() + 2
end
function FilteredList:getFilter()
return self.edit.text, self.list.choices
end
function FilteredList:setFilter(filter, pos)
local choices = self.choices
local cidx = nil
filter = filter or ''
self.edit.text = filter
if filter ~= '' then
local tokens = utils.split_string(filter, ' ')
local ipos = pos
choices = {}
cidx = {}
pos = nil
for i,v in ipairs(self.choices) do
local ok = true
local search_key = v.search_key or v.text
for _,key in ipairs(tokens) do
if key ~= '' and not string.match(search_key, '%f[^%s\x00]'..key) then
ok = false
break
end
end
if ok then
table.insert(choices, v)
cidx[#choices] = i
if ipos == i then
pos = #choices
end
end
end
end
self.choice_index = cidx
self.list:setChoices(choices, pos)
self.not_found.visible = (#choices == 0)
end
function FilteredList:onFilterChange(text)
self:setFilter(text)
end
local bad_chars = {
['%'] = true, ['.'] = true, ['+'] = true, ['*'] = true,
['['] = true, [']'] = true, ['('] = true, [')'] = true,
}
function FilteredList:onFilterChar(char, text)
if bad_chars[char] then
return false
end
if char == ' ' then
return string.match(text, '%S$')
end
return true
end
return _ENV

@ -24,7 +24,7 @@ function CheckedArray:__len()
return self.count
end
function CheckedArray:__index(idx)
if type(idx) == number then
if type(idx) == "number" then
if idx >= self.count then
error('Index out of bounds: '..tostring(idx))
end
@ -195,6 +195,26 @@ function MemoryArea:delete()
for k,v in pairs(self) do self[k] = nil end
end
-- Static code segment search
function get_code_segment()
local cstart, cend
for i,mem in ipairs(dfhack.internal.getMemRanges()) do
if mem.read and mem.execute
and (string.match(mem.name,'/dwarfort%.exe$')
or string.match(mem.name,'/Dwarf_Fortress$')
or string.match(mem.name,'Dwarf Fortress%.exe'))
then
cstart = mem.start_addr
cend = mem.end_addr
end
end
if cstart and cend then
return MemoryArea.new(cstart, cend)
end
end
-- Static data segment search
local function find_data_segment()
@ -358,7 +378,7 @@ end
-- Interactive search utility
function DiffSearcher:find_interactive(prompt,data_type,condition_cb)
function DiffSearcher:find_interactive(prompt,data_type,condition_cb,iter_limit)
enum = enum or {}
-- Loop for restarting search from scratch
@ -374,6 +394,11 @@ function DiffSearcher:find_interactive(prompt,data_type,condition_cb)
while true do
print('')
if iter_limit and ccursor >= iter_limit then
dfhack.printerr(' Iteration limit reached without a solution.')
break
end
local ok, value, delta = condition_cb(ccursor)
ccursor = ccursor + 1

@ -283,6 +283,33 @@ function clone_with_default(obj,default,force)
return rv
end
-- Parse an integer value into a bitfield table
function parse_bitfield_int(value, type_ref)
if value == 0 then
return nil
end
local res = {}
for i,v in ipairs(type_ref) do
if bit32.extract(value, i) ~= 0 then
res[v] = true
end
end
return res
end
-- List the enabled flag names in the bitfield table
function list_bitfield_flags(bitfield, list)
list = list or {}
if bitfield then
for name,val in pairs(bitfield) do
if val then
table.insert(list, name)
end
end
end
return list
end
-- Sort a vector or lua table
function sort_vector(vector,field,cmp)
local fcmp = compare_field(field,cmp)
@ -302,6 +329,34 @@ function sort_vector(vector,field,cmp)
return vector
end
-- Linear search
function linear_index(vector,key,field)
local min,max
if df.isvalid(vector) then
min,max = 0,#vector-1
else
min,max = 1,#vector
end
if field then
for i=min,max do
local obj = vector[i]
if obj[field] == key then
return i, obj
end
end
else
for i=min,max do
local obj = vector[i]
if obj == key then
return i, obj
end
end
end
return nil
end
-- Binary search in a vector or lua table
function binsearch(vector,key,field,cmp,min,max)
if not(min and max) then
@ -402,6 +457,7 @@ function getBuildingCenter(building)
return xyz2pos(building.centerx, building.centery, building.z)
end
-- Split the string by the given delimiter
function split_string(self, delimiter)
local result = { }
local from = 1

@ -25,11 +25,16 @@ distribution.
#include "Internal.h"
#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <map>
#include <string>
#include <unordered_map>
#include <vector>
#include <map>
using namespace std;
#include "ColorText.h"
#include "VersionInfo.h"
#include "MemAccess.h"
#include "Types.h"
@ -77,6 +82,14 @@ using df::global::building_next_id;
using df::global::process_jobs;
using df::building_def;
struct CoordHash {
size_t operator()(const df::coord pos) const {
return pos.x*65537 + pos.y*17 + pos.z;
}
};
static unordered_map<df::coord, int32_t, CoordHash> locationToBuilding;
static uint8_t *getExtentTile(df::building_extents &extent, df::coord2d tile)
{
if (!extent.extents)
@ -178,6 +191,20 @@ bool Buildings::ReadCustomWorkshopTypes(map <uint32_t, string> & btypes)
return true;
}
df::general_ref *Buildings::getGeneralRef(df::building *building, df::general_ref_type type)
{
CHECK_NULL_POINTER(building);
return findRef(building->general_refs, type);
}
df::specific_ref *Buildings::getSpecificRef(df::building *building, df::specific_ref_type type)
{
CHECK_NULL_POINTER(building);
return findRef(building->specific_refs, type);
}
bool Buildings::setOwner(df::building *bld, df::unit *unit)
{
CHECK_NULL_POINTER(bld);
@ -222,6 +249,21 @@ df::building *Buildings::findAtTile(df::coord pos)
if (!occ || !occ->bits.building)
return NULL;
// Try cache lookup in case it works:
auto cached = locationToBuilding.find(pos);
if (cached != locationToBuilding.end())
{
auto building = df::building::find(cached->second);
if (building && building->z == pos.z &&
building->isSettingOccupancy() &&
containsTile(building, pos, false))
{
return building;
}
}
// The authentic method, i.e. how the game generally does this:
auto &vec = df::building::get_vector();
for (size_t i = 0; i < vec.size(); i++)
{
@ -762,7 +804,7 @@ static void markBuildingTiles(df::building *bld, bool remove)
static void linkRooms(df::building *bld)
{
auto &vec = world->buildings.other[buildings_other_id::ANY_FREE];
auto &vec = world->buildings.other[buildings_other_id::IN_PLAY];
bool changed = false;
@ -895,7 +937,7 @@ static bool linkForConstruct(df::job* &job, df::building *bld)
job = new df::job();
job->job_type = df::job_type::ConstructBuilding;
job->pos = df::coord(bld->centerx, bld->centery, bld->z);
job->references.push_back(ref);
job->general_refs.push_back(ref);
bld->jobs.push_back(job);
@ -1063,3 +1105,61 @@ bool Buildings::deconstruct(df::building *bld)
return true;
}
static unordered_map<int32_t, df::coord> corner1;
static unordered_map<int32_t, df::coord> corner2;
void Buildings::clearBuildings(color_ostream& out) {
corner1.clear();
corner2.clear();
locationToBuilding.clear();
}
void Buildings::updateBuildings(color_ostream& out, void* ptr)
{
int32_t id = (int32_t)ptr;
auto building = df::building::find(id);
if (building)
{
// Already cached -> weird, so bail out
if (corner1.count(id))
return;
// Civzones cannot be cached because they can
// overlap each other and normal buildings.
if (!building->isSettingOccupancy())
return;
df::coord p1(min(building->x1, building->x2), min(building->y1,building->y2), building->z);
df::coord p2(max(building->x1, building->x2), max(building->y1,building->y2), building->z);
corner1[id] = p1;
corner2[id] = p2;
for ( int32_t x = p1.x; x <= p2.x; x++ ) {
for ( int32_t y = p1.y; y <= p2.y; y++ ) {
df::coord pt(x,y,building->z);
if (containsTile(building, pt, false))
locationToBuilding[pt] = id;
}
}
}
else if (corner1.count(id))
{
//existing building: destroy it
df::coord p1 = corner1[id];
df::coord p2 = corner2[id];
for ( int32_t x = p1.x; x <= p2.x; x++ ) {
for ( int32_t y = p1.y; y <= p2.y; y++ ) {
df::coord pt(x,y,p1.z);
auto cur = locationToBuilding.find(pt);
if (cur != locationToBuilding.end() && cur->second == id)
locationToBuilding.erase(cur);
}
}
corner1.erase(id);
corner2.erase(id);
}
}

@ -0,0 +1,503 @@
#include "Core.h"
#include "Console.h"
#include "modules/Buildings.h"
#include "modules/Constructions.h"
#include "modules/EventManager.h"
#include "modules/Job.h"
#include "modules/World.h"
#include "df/building.h"
#include "df/construction.h"
#include "df/global_objects.h"
#include "df/item.h"
#include "df/job.h"
#include "df/job_list_link.h"
#include "df/ui.h"
#include "df/unit.h"
#include "df/unit_syndrome.h"
#include "df/world.h"
#include <map>
#include <unordered_map>
#include <unordered_set>
using namespace std;
using namespace DFHack;
using namespace EventManager;
/*
* TODO:
* error checking
* consider a typedef instead of a struct for EventHandler
**/
//map<uint32_t, vector<DFHack::EventManager::EventHandler> > tickQueue;
multimap<uint32_t, EventHandler> tickQueue;
//TODO: consider unordered_map of pairs, or unordered_map of unordered_set, or whatever
multimap<Plugin*, EventHandler> handlers[EventType::EVENT_MAX];
uint32_t eventLastTick[EventType::EVENT_MAX];
const uint32_t ticksPerYear = 403200;
void DFHack::EventManager::registerListener(EventType::EventType e, EventHandler handler, Plugin* plugin) {
handlers[e].insert(pair<Plugin*, EventHandler>(plugin, handler));
}
void DFHack::EventManager::registerTick(EventHandler handler, int32_t when, Plugin* plugin, bool absolute) {
uint32_t tick = DFHack::World::ReadCurrentYear()*ticksPerYear
+ DFHack::World::ReadCurrentTick();
if ( !Core::getInstance().isWorldLoaded() ) {
tick = 0;
if ( absolute ) {
Core::getInstance().getConsole().print("Warning: absolute flag will not be honored.\n");
}
}
if ( absolute ) {
tick = 0;
}
tickQueue.insert(pair<uint32_t, EventHandler>(tick+(uint32_t)when, handler));
handlers[EventType::TICK].insert(pair<Plugin*,EventHandler>(plugin,handler));
return;
}
void DFHack::EventManager::unregister(EventType::EventType e, EventHandler handler, Plugin* plugin) {
for ( multimap<Plugin*, EventHandler>::iterator i = handlers[e].find(plugin); i != handlers[e].end(); i++ ) {
if ( (*i).first != plugin )
break;
EventHandler handle = (*i).second;
if ( handle == handler ) {
handlers[e].erase(i);
break;
}
}
return;
}
void DFHack::EventManager::unregisterAll(Plugin* plugin) {
for ( auto i = handlers[EventType::TICK].find(plugin); i != handlers[EventType::TICK].end(); i++ ) {
if ( (*i).first != plugin )
break;
//shenanigans to avoid concurrent modification
EventHandler getRidOf = (*i).second;
bool didSomething;
do {
didSomething = false;
for ( auto j = tickQueue.begin(); j != tickQueue.end(); j++ ) {
EventHandler candidate = (*j).second;
if ( getRidOf != candidate )
continue;
tickQueue.erase(j);
didSomething = true;
break;
}
} while(didSomething);
}
for ( size_t a = 0; a < (size_t)EventType::EVENT_MAX; a++ ) {
handlers[a].erase(plugin);
}
return;
}
static void manageTickEvent(color_ostream& out);
static void manageJobInitiatedEvent(color_ostream& out);
static void manageJobCompletedEvent(color_ostream& out);
static void manageUnitDeathEvent(color_ostream& out);
static void manageItemCreationEvent(color_ostream& out);
static void manageBuildingEvent(color_ostream& out);
static void manageConstructionEvent(color_ostream& out);
static void manageSyndromeEvent(color_ostream& out);
static void manageInvasionEvent(color_ostream& out);
//tick event
static uint32_t lastTick = 0;
//job initiated
static int32_t lastJobId = -1;
//job completed
static unordered_map<int32_t, df::job*> prevJobs;
//unit death
static unordered_set<int32_t> livingUnits;
//item creation
static int32_t nextItem;
//building
static int32_t nextBuilding;
static unordered_set<int32_t> buildings;
//construction
static unordered_set<df::construction*> constructions;
static bool gameLoaded;
//invasion
static int32_t nextInvasion;
void DFHack::EventManager::onStateChange(color_ostream& out, state_change_event event) {
static bool doOnce = false;
if ( !doOnce ) {
//TODO: put this somewhere else
doOnce = true;
EventHandler buildingHandler(Buildings::updateBuildings, 100);
DFHack::EventManager::registerListener(EventType::BUILDING, buildingHandler, NULL);
//out.print("Registered listeners.\n %d", __LINE__);
}
if ( event == DFHack::SC_WORLD_UNLOADED ) {
lastTick = 0;
lastJobId = -1;
for ( auto i = prevJobs.begin(); i != prevJobs.end(); i++ ) {
Job::deleteJobStruct((*i).second);
}
prevJobs.clear();
tickQueue.clear();
livingUnits.clear();
nextItem = -1;
nextBuilding = -1;
buildings.clear();
constructions.clear();
Buildings::clearBuildings(out);
gameLoaded = false;
nextInvasion = -1;
} else if ( event == DFHack::SC_WORLD_LOADED ) {
uint32_t tick = DFHack::World::ReadCurrentYear()*ticksPerYear
+ DFHack::World::ReadCurrentTick();
multimap<uint32_t,EventHandler> newTickQueue;
for ( auto i = tickQueue.begin(); i != tickQueue.end(); i++ ) {
newTickQueue.insert(pair<uint32_t,EventHandler>(tick + (*i).first, (*i).second));
}
tickQueue.clear();
tickQueue.insert(newTickQueue.begin(), newTickQueue.end());
nextItem = 0;
nextBuilding = 0;
lastTick = 0;
nextInvasion = df::global::ui->invasions.next_id;
gameLoaded = true;
}
}
void DFHack::EventManager::manageEvents(color_ostream& out) {
if ( !gameLoaded ) {
return;
}
uint32_t tick = DFHack::World::ReadCurrentYear()*ticksPerYear
+ DFHack::World::ReadCurrentTick();
if ( tick <= lastTick )
return;
lastTick = tick;
int32_t eventFrequency[EventType::EVENT_MAX];
for ( size_t a = 0; a < EventType::EVENT_MAX; a++ ) {
int32_t min = 1000000000;
for ( auto b = handlers[a].begin(); b != handlers[a].end(); b++ ) {
EventHandler bob = (*b).second;
if ( bob.freq < min )
min = bob.freq;
}
eventFrequency[a] = min;
}
manageTickEvent(out);
if ( tick - eventLastTick[EventType::JOB_INITIATED] >= eventFrequency[EventType::JOB_INITIATED] ) {
manageJobInitiatedEvent(out);
eventLastTick[EventType::JOB_INITIATED] = tick;
}
if ( tick - eventLastTick[EventType::JOB_COMPLETED] >= eventFrequency[EventType::JOB_COMPLETED] ) {
manageJobCompletedEvent(out);
eventLastTick[EventType::JOB_COMPLETED] = tick;
}
if ( tick - eventLastTick[EventType::UNIT_DEATH] >= eventFrequency[EventType::UNIT_DEATH] ) {
manageUnitDeathEvent(out);
eventLastTick[EventType::UNIT_DEATH] = tick;
}
if ( tick - eventLastTick[EventType::ITEM_CREATED] >= eventFrequency[EventType::ITEM_CREATED] ) {
manageItemCreationEvent(out);
eventLastTick[EventType::ITEM_CREATED] = tick;
}
if ( tick - eventLastTick[EventType::BUILDING] >= eventFrequency[EventType::BUILDING] ) {
manageBuildingEvent(out);
eventLastTick[EventType::BUILDING] = tick;
}
if ( tick - eventLastTick[EventType::CONSTRUCTION] >= eventFrequency[EventType::CONSTRUCTION] ) {
manageConstructionEvent(out);
eventLastTick[EventType::CONSTRUCTION] = tick;
}
if ( tick - eventLastTick[EventType::SYNDROME] >= eventFrequency[EventType::SYNDROME] ) {
manageSyndromeEvent(out);
eventLastTick[EventType::SYNDROME] = tick;
}
if ( tick - eventLastTick[EventType::INVASION] >= eventFrequency[EventType::INVASION] ) {
manageInvasionEvent(out);
eventLastTick[EventType::INVASION] = tick;
}
return;
}
static void manageTickEvent(color_ostream& out) {
uint32_t tick = DFHack::World::ReadCurrentYear()*ticksPerYear
+ DFHack::World::ReadCurrentTick();
while ( !tickQueue.empty() ) {
if ( tick < (*tickQueue.begin()).first )
break;
EventHandler handle = (*tickQueue.begin()).second;
tickQueue.erase(tickQueue.begin());
handle.eventHandler(out, (void*)tick);
}
}
static void manageJobInitiatedEvent(color_ostream& out) {
if ( handlers[EventType::JOB_INITIATED].empty() )
return;
if ( lastJobId == -1 ) {
lastJobId = *df::global::job_next_id - 1;
return;
}
if ( lastJobId+1 == *df::global::job_next_id ) {
return; //no new jobs
}
multimap<Plugin*,EventHandler> copy(handlers[EventType::JOB_INITIATED].begin(), handlers[EventType::JOB_INITIATED].end());
for ( df::job_list_link* link = &df::global::world->job_list; link != NULL; link = link->next ) {
if ( link->item == NULL )
continue;
if ( link->item->id <= lastJobId )
continue;
for ( auto i = copy.begin(); i != copy.end(); i++ ) {
(*i).second.eventHandler(out, (void*)link->item);
}
}
lastJobId = *df::global::job_next_id - 1;
}
static void manageJobCompletedEvent(color_ostream& out) {
if ( handlers[EventType::JOB_COMPLETED].empty() ) {
return;
}
multimap<Plugin*,EventHandler> copy(handlers[EventType::JOB_COMPLETED].begin(), handlers[EventType::JOB_COMPLETED].end());
map<int32_t, df::job*> nowJobs;
for ( df::job_list_link* link = &df::global::world->job_list; link != NULL; link = link->next ) {
if ( link->item == NULL )
continue;
nowJobs[link->item->id] = link->item;
}
for ( auto i = prevJobs.begin(); i != prevJobs.end(); i++ ) {
if ( nowJobs.find((*i).first) != nowJobs.end() )
continue;
//recently finished or cancelled job!
for ( auto j = copy.begin(); j != copy.end(); j++ ) {
(*j).second.eventHandler(out, (void*)(*i).second);
}
}
//erase old jobs, copy over possibly altered jobs
for ( auto i = prevJobs.begin(); i != prevJobs.end(); i++ ) {
Job::deleteJobStruct((*i).second);
}
prevJobs.clear();
//create new jobs
for ( auto j = nowJobs.begin(); j != nowJobs.end(); j++ ) {
/*map<int32_t, df::job*>::iterator i = prevJobs.find((*j).first);
if ( i != prevJobs.end() ) {
continue;
}*/
df::job* newJob = Job::cloneJobStruct((*j).second, true);
prevJobs[newJob->id] = newJob;
}
/*//get rid of old pointers to deallocated jobs
for ( size_t a = 0; a < toDelete.size(); a++ ) {
prevJobs.erase(a);
}*/
}
static void manageUnitDeathEvent(color_ostream& out) {
if ( handlers[EventType::UNIT_DEATH].empty() ) {
return;
}
multimap<Plugin*,EventHandler> copy(handlers[EventType::UNIT_DEATH].begin(), handlers[EventType::UNIT_DEATH].end());
for ( size_t a = 0; a < df::global::world->units.active.size(); a++ ) {
df::unit* unit = df::global::world->units.active[a];
if ( unit->counters.death_id == -1 ) {
livingUnits.insert(unit->id);
continue;
}
//dead: if dead since last check, trigger events
if ( livingUnits.find(unit->id) == livingUnits.end() )
continue;
for ( auto i = copy.begin(); i != copy.end(); i++ ) {
(*i).second.eventHandler(out, (void*)unit->id);
}
livingUnits.erase(unit->id);
}
}
static void manageItemCreationEvent(color_ostream& out) {
if ( handlers[EventType::ITEM_CREATED].empty() ) {
return;
}
if ( nextItem >= *df::global::item_next_id ) {
return;
}
multimap<Plugin*,EventHandler> copy(handlers[EventType::ITEM_CREATED].begin(), handlers[EventType::ITEM_CREATED].end());
size_t index = df::item::binsearch_index(df::global::world->items.all, nextItem, false);
for ( size_t a = index; a < df::global::world->items.all.size(); a++ ) {
df::item* item = df::global::world->items.all[a];
//invaders
if ( item->flags.bits.foreign )
continue;
//traders who bring back your items?
if ( item->flags.bits.trader )
continue;
//migrants
if ( item->flags.bits.owned )
continue;
//spider webs don't count
if ( item->flags.bits.spider_web )
continue;
for ( auto i = copy.begin(); i != copy.end(); i++ ) {
(*i).second.eventHandler(out, (void*)item->id);
}
}
nextItem = *df::global::item_next_id;
}
static void manageBuildingEvent(color_ostream& out) {
/*
* TODO: could be faster
* consider looking at jobs: building creation / destruction
**/
if ( handlers[EventType::BUILDING].empty() )
return;
multimap<Plugin*,EventHandler> copy(handlers[EventType::BUILDING].begin(), handlers[EventType::BUILDING].end());
//first alert people about new buildings
for ( int32_t a = nextBuilding; a < *df::global::building_next_id; a++ ) {
int32_t index = df::building::binsearch_index(df::global::world->buildings.all, a);
if ( index == -1 ) {
//out.print("%s, line %d: Couldn't find new building with id %d.\n", __FILE__, __LINE__, a);
//the tricky thing is that when the game first starts, it's ok to skip buildings, but otherwise, if you skip buildings, something is probably wrong. TODO: make this smarter
continue;
}
buildings.insert(a);
for ( auto b = copy.begin(); b != copy.end(); b++ ) {
EventHandler bob = (*b).second;
bob.eventHandler(out, (void*)a);
}
}
nextBuilding = *df::global::building_next_id;
//now alert people about destroyed buildings
unordered_set<int32_t> toDelete;
for ( auto a = buildings.begin(); a != buildings.end(); a++ ) {
int32_t id = *a;
int32_t index = df::building::binsearch_index(df::global::world->buildings.all,id);
if ( index != -1 )
continue;
toDelete.insert(id);
for ( auto b = copy.begin(); b != copy.end(); b++ ) {
EventHandler bob = (*b).second;
bob.eventHandler(out, (void*)id);
}
}
for ( auto a = toDelete.begin(); a != toDelete.end(); a++ ) {
int32_t id = *a;
buildings.erase(id);
}
//out.print("Sent building event.\n %d", __LINE__);
}
static void manageConstructionEvent(color_ostream& out) {
if ( handlers[EventType::CONSTRUCTION].empty() )
return;
unordered_set<df::construction*> constructionsNow(df::global::world->constructions.begin(), df::global::world->constructions.end());
multimap<Plugin*,EventHandler> copy(handlers[EventType::CONSTRUCTION].begin(), handlers[EventType::CONSTRUCTION].end());
for ( auto a = constructions.begin(); a != constructions.end(); a++ ) {
df::construction* construction = *a;
if ( constructionsNow.find(construction) != constructionsNow.end() )
continue;
for ( auto b = copy.begin(); b != copy.end(); b++ ) {
EventHandler handle = (*b).second;
handle.eventHandler(out, (void*)construction);
}
}
for ( auto a = constructionsNow.begin(); a != constructionsNow.end(); a++ ) {
df::construction* construction = *a;
if ( constructions.find(construction) != constructions.end() )
continue;
for ( auto b = copy.begin(); b != copy.end(); b++ ) {
EventHandler handle = (*b).second;
handle.eventHandler(out, (void*)construction);
}
}
constructions.clear();
constructions.insert(constructionsNow.begin(), constructionsNow.end());
}
static void manageSyndromeEvent(color_ostream& out) {
if ( handlers[EventType::SYNDROME].empty() )
return;
multimap<Plugin*,EventHandler> copy(handlers[EventType::SYNDROME].begin(), handlers[EventType::SYNDROME].end());
for ( auto a = df::global::world->units.active.begin(); a != df::global::world->units.active.end(); a++ ) {
df::unit* unit = *a;
if ( unit->flags1.bits.dead )
continue;
for ( size_t b = 0; b < unit->syndromes.active.size(); b++ ) {
df::unit_syndrome* syndrome = unit->syndromes.active[b];
uint32_t startTime = syndrome->year*ticksPerYear + syndrome->year_time;
if ( startTime <= eventLastTick[EventType::SYNDROME] )
continue;
SyndromeData data(unit->id, b);
for ( auto c = copy.begin(); c != copy.end(); c++ ) {
EventHandler handle = (*c).second;
handle.eventHandler(out, (void*)&data);
}
}
}
}
static void manageInvasionEvent(color_ostream& out) {
if ( handlers[EventType::INVASION].empty() )
return;
multimap<Plugin*,EventHandler> copy(handlers[EventType::INVASION].begin(), handlers[EventType::INVASION].end());
if ( df::global::ui->invasions.next_id <= nextInvasion )
return;
nextInvasion = df::global::ui->invasions.next_id;
for ( auto a = copy.begin(); a != copy.end(); a++ ) {
EventHandler handle = (*a).second;
handle.eventHandler(out, (void*)nextInvasion);
}
}

@ -85,6 +85,8 @@ using namespace DFHack;
#include "df/assign_trade_status.h"
#include "df/announcement_flags.h"
#include "df/announcements.h"
#include "df/stop_depart_condition.h"
#include "df/route_stockpile_link.h"
using namespace df::enums;
using df::global::gview;
@ -303,6 +305,45 @@ DEFINE_GET_FOCUS_STRING_HANDLER(dwarfmode)
focus += "/List";
break;
case Hauling:
if (ui->hauling.in_assign_vehicle)
{
auto vehicle = vector_get(ui->hauling.vehicles, ui->hauling.cursor_vehicle);
focus += "/AssignVehicle/" + std::string(vehicle ? "Some" : "None");
}
else
{
int idx = ui->hauling.cursor_top;
auto route = vector_get(ui->hauling.view_routes, idx);
auto stop = vector_get(ui->hauling.view_stops, idx);
std::string tag = stop ? "Stop" : (route ? "Route" : "None");
if (ui->hauling.in_name)
focus += "/Rename/" + tag;
else if (ui->hauling.in_stop)
{
int sidx = ui->hauling.cursor_stop;
auto cond = vector_get(ui->hauling.stop_conditions, sidx);
auto link = vector_get(ui->hauling.stop_links, sidx);
focus += "/DefineStop";
if (cond)
focus += "/Cond/" + enum_item_key(cond->mode);
else if (link)
{
focus += "/Link/";
if (link->mode.bits.give) focus += "Give";
if (link->mode.bits.take) focus += "Take";
}
else
focus += "/None";
}
else
focus += "/Select/" + tag;
}
break;
default:
break;
}

@ -109,30 +109,47 @@ using df::global::proj_next_id;
ITEM(PANTS, pants, itemdef_pantsst) \
ITEM(FOOD, food, itemdef_foodst)
bool ItemTypeInfo::decode(df::item_type type_, int16_t subtype_)
int Items::getSubtypeCount(df::item_type itype)
{
using namespace df::enums::item_type;
type = type_;
subtype = subtype_;
custom = NULL;
df::world_raws::T_itemdefs &defs = df::global::world->raws.itemdefs;
switch (type_) {
case NONE:
return false;
switch (itype) {
#define ITEM(type,vec,tclass) \
case type: \
return defs.vec.size();
ITEMDEF_VECTORS
#undef ITEM
default:
return -1;
}
}
df::itemdef *Items::getSubtypeDef(df::item_type itype, int subtype)
{
using namespace df::enums::item_type;
df::world_raws::T_itemdefs &defs = df::global::world->raws.itemdefs;
switch (itype) {
#define ITEM(type,vec,tclass) \
case type: \
custom = vector_get(defs.vec, subtype); \
break;
return vector_get(defs.vec, subtype);
ITEMDEF_VECTORS
#undef ITEM
default:
break;
return NULL;
}
}
bool ItemTypeInfo::decode(df::item_type type_, int16_t subtype_)
{
type = type_;
subtype = subtype_;
custom = Items::getSubtypeDef(type_, subtype_);
return isValid();
}
@ -171,6 +188,10 @@ ITEMDEF_VECTORS
break;
}
const char *name = ENUM_ATTR(item_type, caption, type);
if (name)
return name;
return toLower(ENUM_KEY_STR(item_type, type));
}
@ -219,19 +240,51 @@ ITEMDEF_VECTORS
return (subtype >= 0);
}
bool ItemTypeInfo::matches(const df::job_item &item, MaterialInfo *mat)
bool Items::isCasteMaterial(df::item_type itype)
{
return ENUM_ATTR(item_type, is_caste_mat, itype);
}
bool ItemTypeInfo::matches(df::job_item_vector_id vec_id)
{
auto other_id = ENUM_ATTR(job_item_vector_id, other, vec_id);
auto explicit_item = ENUM_ATTR(items_other_id, item, other_id);
if (explicit_item != item_type::NONE && type != explicit_item)
return false;
auto generic_item = ENUM_ATTR(items_other_id, generic_item, other_id);
if (generic_item.size > 0)
{
for (size_t i = 0; i < generic_item.size; i++)
if (generic_item.items[i] == type)
return true;
return false;
}
return true;
}
bool ItemTypeInfo::matches(const df::job_item &item, MaterialInfo *mat, bool skip_vector)
{
using namespace df::enums::item_type;
if (!isValid())
return mat ? mat->matches(item) : false;
df::job_item_flags1 ok1, mask1, item_ok1, item_mask1;
df::job_item_flags2 ok2, mask2, item_ok2, item_mask2;
if (Items::isCasteMaterial(type) && mat && !mat->isNone())
return false;
if (!skip_vector && !matches(item.vector_id))
return false;
df::job_item_flags1 ok1, mask1, item_ok1, item_mask1, xmask1;
df::job_item_flags2 ok2, mask2, item_ok2, item_mask2, xmask2;
df::job_item_flags3 ok3, mask3, item_ok3, item_mask3;
ok1.whole = mask1.whole = item_ok1.whole = item_mask1.whole = 0;
ok2.whole = mask2.whole = item_ok2.whole = item_mask2.whole = 0;
ok1.whole = mask1.whole = item_ok1.whole = item_mask1.whole = xmask1.whole = 0;
ok2.whole = mask2.whole = item_ok2.whole = item_mask2.whole = xmask2.whole = 0;
ok3.whole = mask3.whole = item_ok3.whole = item_mask3.whole = 0;
if (mat) {
@ -252,12 +305,16 @@ bool ItemTypeInfo::matches(const df::job_item &item, MaterialInfo *mat)
RQ(1,not_bin); RQ(1,lye_bearing);
RQ(2,dye); RQ(2,dyeable); RQ(2,dyed); RQ(2,glass_making); RQ(2,screw);
RQ(2,building_material); RQ(2,fire_safe); RQ(2,magma_safe); RQ(2,non_economic);
RQ(2,building_material); RQ(2,fire_safe); RQ(2,magma_safe);
RQ(2,totemable); RQ(2,plaster_containing); RQ(2,body_part); RQ(2,lye_milk_free);
RQ(2,blunt); RQ(2,unengraved); RQ(2,hair_wool);
RQ(3,any_raw_material); RQ(3,non_pressed); RQ(3,food_storage);
// only checked if boulder
xmask2.bits.non_economic = true;
// Compute the ok mask
OK(1,solid);
@ -277,7 +334,7 @@ bool ItemTypeInfo::matches(const df::job_item &item, MaterialInfo *mat)
case BOULDER:
OK(1,sharpenable);
OK(2,non_economic);
xmask2.bits.non_economic = false;
case BAR:
OK(3,any_raw_material);
case BLOCKS:
@ -305,11 +362,13 @@ bool ItemTypeInfo::matches(const df::job_item &item, MaterialInfo *mat)
case CAGE:
OK(1,milk);
OK(1,milkable);
xmask1.bits.cookable = true;
break;
case BUCKET:
case FLASK:
OK(1,milk);
xmask1.bits.cookable = true;
break;
case TOOL:
@ -317,6 +376,7 @@ bool ItemTypeInfo::matches(const df::job_item &item, MaterialInfo *mat)
OK(1,milk);
OK(2,lye_milk_free);
OK(2,blunt);
xmask1.bits.cookable = true;
if (VIRTUAL_CAST_VAR(def, df::itemdef_toolst, custom)) {
df::tool_uses key(tool_uses::FOOD_STORAGE);
@ -332,11 +392,13 @@ bool ItemTypeInfo::matches(const df::job_item &item, MaterialInfo *mat)
OK(1,milk);
OK(2,lye_milk_free);
OK(3,food_storage);
xmask1.bits.cookable = true;
break;
case BOX:
OK(1,bag); OK(1,sand_bearing); OK(1,milk);
OK(2,dye); OK(2,plaster_containing);
xmask1.bits.cookable = true;
break;
case BIN:
@ -403,6 +465,9 @@ bool ItemTypeInfo::matches(const df::job_item &item, MaterialInfo *mat)
#undef OK
#undef RQ
mask1.whole &= ~xmask1.whole;
mask2.whole &= ~xmask2.whole;
return bits_match(item.flags1.whole, ok1.whole, mask1.whole) &&
bits_match(item.flags2.whole, ok2.whole, mask2.whole) &&
bits_match(item.flags3.whole, ok3.whole, mask3.whole) &&
@ -430,10 +495,10 @@ bool Items::copyItem(df::item * itembase, DFHack::dfh_item &item)
item.id = itreal->id;
item.age = itreal->age;
item.flags = itreal->flags;
item.matdesc.itemType = itreal->getType();
item.matdesc.subType = itreal->getSubtype();
item.matdesc.material = itreal->getMaterial();
item.matdesc.index = itreal->getMaterialIndex();
item.matdesc.item_type = itreal->getType();
item.matdesc.item_subtype = itreal->getSubtype();
item.matdesc.mat_type = itreal->getMaterial();
item.matdesc.mat_index = itreal->getMaterialIndex();
item.wear_level = itreal->getWear();
item.quality = itreal->getQuality();
item.quantity = itreal->getStackSize();
@ -444,7 +509,7 @@ df::general_ref *Items::getGeneralRef(df::item *item, df::general_ref_type type)
{
CHECK_NULL_POINTER(item);
return findRef(item->itemrefs, type);
return findRef(item->general_refs, type);
}
df::specific_ref *Items::getSpecificRef(df::item *item, df::specific_ref_type type)
@ -465,9 +530,9 @@ bool Items::setOwner(df::item *item, df::unit *unit)
{
CHECK_NULL_POINTER(item);
for (int i = item->itemrefs.size()-1; i >= 0; i--)
for (int i = item->general_refs.size()-1; i >= 0; i--)
{
df::general_ref *ref = item->itemrefs[i];
df::general_ref *ref = item->general_refs[i];
if (!strict_virtual_cast<df::general_ref_unit_itemownerst>(ref))
continue;
@ -481,7 +546,7 @@ bool Items::setOwner(df::item *item, df::unit *unit)
}
delete ref;
vector_erase_at(item->itemrefs, i);
vector_erase_at(item->general_refs, i);
}
item->flags.bits.owned = false;
@ -496,7 +561,7 @@ bool Items::setOwner(df::item *item, df::unit *unit)
ref->unit_id = unit->id;
insert_into_vector(unit->owned_items, item->id);
item->itemrefs.push_back(ref);
item->general_refs.push_back(ref);
}
return true;
@ -515,9 +580,9 @@ void Items::getContainedItems(df::item *item, std::vector<df::item*> *items)
items->clear();
for (size_t i = 0; i < item->itemrefs.size(); i++)
for (size_t i = 0; i < item->general_refs.size(); i++)
{
df::general_ref *ref = item->itemrefs[i];
df::general_ref *ref = item->general_refs[i];
if (ref->getType() != general_ref_type::CONTAINS_ITEM)
continue;
@ -527,6 +592,20 @@ void Items::getContainedItems(df::item *item, std::vector<df::item*> *items)
}
}
df::building *Items::getHolderBuilding(df::item * item)
{
auto ref = getGeneralRef(item, general_ref_type::BUILDING_HOLDER);
return ref ? ref->getBuilding() : NULL;
}
df::unit *Items::getHolderUnit(df::item * item)
{
auto ref = getGeneralRef(item, general_ref_type::UNIT_HOLDER);
return ref ? ref->getUnit() : NULL;
}
df::coord Items::getPosition(df::item *item)
{
CHECK_NULL_POINTER(item);
@ -538,9 +617,9 @@ df::coord Items::getPosition(df::item *item)
if (item->flags.bits.in_inventory)
{
for (size_t i = 0; i < item->itemrefs.size(); i++)
for (size_t i = 0; i < item->general_refs.size(); i++)
{
df::general_ref *ref = item->itemrefs[i];
df::general_ref *ref = item->general_refs[i];
switch (ref->getType())
{
@ -637,9 +716,9 @@ static bool detachItem(MapExtras::MapCache &mc, df::item *item)
if (item->world_data_id != -1)
return false;
for (size_t i = 0; i < item->itemrefs.size(); i++)
for (size_t i = 0; i < item->general_refs.size(); i++)
{
df::general_ref *ref = item->itemrefs[i];
df::general_ref *ref = item->general_refs[i];
switch (ref->getType())
{
@ -669,9 +748,9 @@ static bool detachItem(MapExtras::MapCache &mc, df::item *item)
{
bool found = false;
for (int i = item->itemrefs.size()-1; i >= 0; i--)
for (int i = item->general_refs.size()-1; i >= 0; i--)
{
df::general_ref *ref = item->itemrefs[i];
df::general_ref *ref = item->general_refs[i];
switch (ref->getType())
{
@ -688,7 +767,7 @@ static bool detachItem(MapExtras::MapCache &mc, df::item *item)
item2->flags.bits.weight_computed = false;
removeRef(item2->itemrefs, general_ref_type::CONTAINS_ITEM, item->id);
removeRef(item2->general_refs, general_ref_type::CONTAINS_ITEM, item->id);
}
break;
@ -720,7 +799,7 @@ static bool detachItem(MapExtras::MapCache &mc, df::item *item)
}
found = true;
vector_erase_at(item->itemrefs, i);
vector_erase_at(item->general_refs, i);
delete ref;
}
@ -799,10 +878,10 @@ bool DFHack::Items::moveToContainer(MapExtras::MapCache &mc, df::item *item, df:
container->flags.bits.weight_computed = false;
ref1->item_id = item->id;
container->itemrefs.push_back(ref1);
container->general_refs.push_back(ref1);
ref2->item_id = container->id;
item->itemrefs.push_back(ref2);
item->general_refs.push_back(ref2);
return true;
}
@ -833,7 +912,7 @@ bool DFHack::Items::moveToBuilding(MapExtras::MapCache &mc, df::item *item, df::
item->flags.bits.in_building=true;
ref->building_id=building->id;
item->itemrefs.push_back(ref);
item->general_refs.push_back(ref);
auto con=new df::building_actual::T_contained_items;
con->item=item;
@ -876,7 +955,7 @@ bool DFHack::Items::moveToInventory(
unit->inventory.push_back(newInventoryItem);
holderReference->unit_id = unit->id;
item->itemrefs.push_back(holderReference);
item->general_refs.push_back(holderReference);
resetUnitInvFlags(unit, newInventoryItem);
@ -937,7 +1016,7 @@ df::proj_itemst *Items::makeProjectile(MapExtras::MapCache &mc, df::item *item)
proj->item = item;
ref->projectile_id = proj->id;
item->itemrefs.push_back(ref);
item->general_refs.push_back(ref);
linked_list_append(&world->proj_list, proj->link);

@ -35,6 +35,7 @@ using namespace std;
#include "Error.h"
#include "PluginManager.h"
#include "MiscUtils.h"
#include "Types.h"
#include "modules/Job.h"
#include "modules/Materials.h"
@ -54,7 +55,7 @@ using namespace std;
using namespace DFHack;
using namespace df::enums;
df::job *DFHack::Job::cloneJobStruct(df::job *job)
df::job *DFHack::Job::cloneJobStruct(df::job *job, bool keepWorkerData)
{
CHECK_NULL_POINTER(job);
@ -71,14 +72,14 @@ df::job *DFHack::Job::cloneJobStruct(df::job *job)
pnew->specific_refs.clear();
// Clone refs
for (int i = pnew->references.size()-1; i >= 0; i--)
for (int i = pnew->general_refs.size()-1; i >= 0; i--)
{
df::general_ref *ref = pnew->references[i];
df::general_ref *ref = pnew->general_refs[i];
if (virtual_cast<df::general_ref_unit_workerst>(ref))
vector_erase_at(pnew->references, i);
if (!keepWorkerData && virtual_cast<df::general_ref_unit_workerst>(ref))
vector_erase_at(pnew->general_refs, i);
else
pnew->references[i] = ref->clone();
pnew->general_refs[i] = ref->clone();
}
// Clone items
@ -96,8 +97,8 @@ void DFHack::Job::deleteJobStruct(df::job *job)
// Only allow free-floating job structs
assert(!job->list_link && job->items.empty() && job->specific_refs.empty());
for (int i = job->references.size()-1; i >= 0; i--)
delete job->references[i];
for (int i = job->general_refs.size()-1; i >= 0; i--)
delete job->general_refs[i];
for (int i = job->job_items.size()-1; i >= 0; i--)
delete job->job_items[i];
@ -228,13 +229,27 @@ void DFHack::Job::printJobDetails(color_ostream &out, df::job *job)
printItemDetails(out, job->job_items[i], i);
}
df::general_ref *Job::getGeneralRef(df::job *job, df::general_ref_type type)
{
CHECK_NULL_POINTER(job);
return findRef(job->general_refs, type);
}
df::specific_ref *Job::getSpecificRef(df::job *job, df::specific_ref_type type)
{
CHECK_NULL_POINTER(job);
return findRef(job->specific_refs, type);
}
df::building *DFHack::Job::getHolder(df::job *job)
{
CHECK_NULL_POINTER(job);
for (size_t i = 0; i < job->references.size(); i++)
for (size_t i = 0; i < job->general_refs.size(); i++)
{
VIRTUAL_CAST_VAR(ref, df::general_ref_building_holderst, job->references[i]);
VIRTUAL_CAST_VAR(ref, df::general_ref_building_holderst, job->general_refs[i]);
if (ref)
return ref->getBuilding();
}
@ -246,9 +261,9 @@ df::unit *DFHack::Job::getWorker(df::job *job)
{
CHECK_NULL_POINTER(job);
for (size_t i = 0; i < job->references.size(); i++)
for (size_t i = 0; i < job->general_refs.size(); i++)
{
VIRTUAL_CAST_VAR(ref, df::general_ref_unit_workerst, job->references[i]);
VIRTUAL_CAST_VAR(ref, df::general_ref_unit_workerst, job->general_refs[i]);
if (ref)
return ref->getUnit();
}
@ -361,3 +376,29 @@ bool DFHack::Job::attachJobItem(df::job *job, df::item *item,
return true;
}
bool Job::isSuitableItem(df::job_item *item, df::item_type itype, int isubtype)
{
CHECK_NULL_POINTER(item);
if (itype == item_type::NONE)
return true;
ItemTypeInfo iinfo(itype, isubtype);
MaterialInfo minfo(item);
return iinfo.isValid() && iinfo.matches(*item, &minfo);
}
bool Job::isSuitableMaterial(df::job_item *item, int mat_type, int mat_index)
{
CHECK_NULL_POINTER(item);
if (mat_type == -1 && mat_index == -1)
return true;
ItemTypeInfo iinfo(item);
MaterialInfo minfo(mat_type, mat_index);
return minfo.isValid() && iinfo.matches(*item, &minfo);
}

@ -30,10 +30,12 @@ distribution.
#include <map>
#include <set>
#include <cstdlib>
#include <iostream>
using namespace std;
#include "modules/Maps.h"
#include "modules/MapCache.h"
#include "ColorText.h"
#include "Error.h"
#include "VersionInfo.h"
#include "MemAccess.h"
@ -59,6 +61,8 @@ using namespace std;
#include "df/z_level_flags.h"
#include "df/region_map_entry.h"
#include "df/flow_info.h"
#include "df/plant.h"
#include "df/building_type.h"
using namespace DFHack;
using namespace df::enums;
@ -454,7 +458,7 @@ df::coord2d Maps::getBlockTileBiomeRgn(df::map_block *block, df::coord2d pos)
if (!block || !world->world_data)
return df::coord2d();
auto des = MapExtras::index_tile<df::tile_designation>(block->designation,pos);
auto des = index_tile<df::tile_designation>(block->designation,pos);
unsigned idx = des.bits.biome;
if (idx < 9)
{
@ -529,12 +533,135 @@ bool Maps::canWalkBetween(df::coord pos1, df::coord pos2)
if (!block1 || !block2)
return false;
auto tile1 = MapExtras::index_tile<uint16_t>(block1->walkable, pos1);
auto tile2 = MapExtras::index_tile<uint16_t>(block2->walkable, pos2);
auto tile1 = index_tile<uint16_t>(block1->walkable, pos1);
auto tile2 = index_tile<uint16_t>(block2->walkable, pos2);
return tile1 && tile1 == tile2;
}
bool Maps::canStepBetween(df::coord pos1, df::coord pos2)
{
color_ostream& out = Core::getInstance().getConsole();
int32_t dx = pos2.x-pos1.x;
int32_t dy = pos2.y-pos1.y;
int32_t dz = pos2.z-pos1.z;
if ( dx*dx > 1 || dy*dy > 1 || dz*dz > 1 )
return false;
if ( pos2.z < pos1.z ) {
df::coord temp = pos1;
pos1 = pos2;
pos2 = temp;
}
df::map_block* block1 = getTileBlock(pos1);
df::map_block* block2 = getTileBlock(pos2);
if ( !block1 || !block2 )
return false;
if ( !index_tile<uint16_t>(block1->walkable,pos1) || !index_tile<uint16_t>(block2->walkable,pos2) ) {
return false;
}
if ( dz == 0 )
return true;
df::tiletype* type1 = Maps::getTileType(pos1);
df::tiletype* type2 = Maps::getTileType(pos2);
df::tiletype_shape shape1 = ENUM_ATTR(tiletype,shape,*type1);
df::tiletype_shape shape2 = ENUM_ATTR(tiletype,shape,*type2);
if ( dx == 0 && dy == 0 ) {
//check for forbidden hatches and floors and such
df::enums::tile_building_occ::tile_building_occ upOcc = index_tile<df::tile_occupancy>(block2->occupancy,pos2).bits.building;
if ( upOcc == df::enums::tile_building_occ::Impassable || upOcc == df::enums::tile_building_occ::Obstacle || upOcc == df::enums::tile_building_occ::Floored )
return false;
if ( shape1 == tiletype_shape::STAIR_UPDOWN && shape2 == shape1 )
return true;
if ( shape1 == tiletype_shape::STAIR_UPDOWN && shape2 == tiletype_shape::STAIR_DOWN )
return true;
if ( shape1 == tiletype_shape::STAIR_UP && shape2 == tiletype_shape::STAIR_UPDOWN )
return true;
if ( shape1 == tiletype_shape::STAIR_UP && shape2 == tiletype_shape::STAIR_DOWN )
return true;
if ( shape1 == tiletype_shape::RAMP && shape2 == tiletype_shape::RAMP_TOP ) {
//it depends
//there has to be a wall next to the ramp
bool foundWall = false;
for ( int32_t x = -1; x <= 1; x++ ) {
for ( int32_t y = -1; y <= 1; y++ ) {
if ( x == 0 && y == 0 )
continue;
df::tiletype* type = Maps::getTileType(df::coord(pos1.x+x,pos1.y+y,pos1.z));
df::tiletype_shape shape1 = ENUM_ATTR(tiletype,shape,*type);
if ( shape1 == tiletype_shape::WALL ) {
foundWall = true;
x = 2;
break;
}
}
}
if ( !foundWall )
return false; //unusable ramp
//there has to be an unforbidden hatch above the ramp
if ( index_tile<df::tile_occupancy>(block2->occupancy,pos2).bits.building != df::enums::tile_building_occ::Dynamic )
return false;
//note that forbidden hatches have Floored occupancy. unforbidden ones have dynamic occupancy
df::building* building = Buildings::findAtTile(pos2);
if ( building == NULL ) {
out << __FILE__ << ", line " << __LINE__ << ": couldn't find hatch.\n";
return false;
}
if ( building->getType() != df::enums::building_type::Hatch ) {
return false;
}
return true;
}
return false;
}
//diagonal up: has to be a ramp
if ( shape1 == tiletype_shape::RAMP /*&& shape2 == tiletype_shape::RAMP*/ ) {
df::coord up = df::coord(pos1.x,pos1.y,pos1.z+1);
bool foundWall = false;
for ( int32_t x = -1; x <= 1; x++ ) {
for ( int32_t y = -1; y <= 1; y++ ) {
if ( x == 0 && y == 0 )
continue;
df::tiletype* type = Maps::getTileType(df::coord(pos1.x+x,pos1.y+y,pos1.z));
df::tiletype_shape shape1 = ENUM_ATTR(tiletype,shape,*type);
if ( shape1 == tiletype_shape::WALL ) {
foundWall = true;
x = 2;
break;
}
}
}
if ( !foundWall )
return false; //unusable ramp
df::tiletype* typeUp = Maps::getTileType(up);
df::tiletype_shape shapeUp = ENUM_ATTR(tiletype,shape,*typeUp);
if ( shapeUp != tiletype_shape::RAMP_TOP )
return false;
df::map_block* blockUp = getTileBlock(up);
if ( !blockUp )
return false;
df::enums::tile_building_occ::tile_building_occ occupancy = index_tile<df::tile_occupancy>(blockUp->occupancy,up).bits.building;
if ( occupancy == df::enums::tile_building_occ::Obstacle || occupancy == df::enums::tile_building_occ::Floored || occupancy == df::enums::tile_building_occ::Impassable )
return false;
return true;
}
return false;
}
#define COPY(a,b) memcpy(&a,&b,sizeof(a))
MapExtras::Block::Block(MapCache *parent, DFCoord _bcoord) : parent(parent)
@ -650,15 +777,15 @@ void MapExtras::Block::TileInfo::init_coninfo()
con_info = new ConInfo();
con_info->constructed.clear();
COPY(con_info->tiles, base_tiles);
memset(con_info->mattype, -1, sizeof(con_info->mattype));
memset(con_info->matindex, -1, sizeof(con_info->matindex));
memset(con_info->mat_type, -1, sizeof(con_info->mat_type));
memset(con_info->mat_index, -1, sizeof(con_info->mat_index));
}
MapExtras::Block::BasematInfo::BasematInfo()
{
dirty.clear();
memset(mattype,0,sizeof(mattype));
memset(matindex,-1,sizeof(matindex));
memset(mat_type,0,sizeof(mat_type));
memset(mat_index,-1,sizeof(mat_index));
memset(layermat,-1,sizeof(layermat));
}
@ -719,8 +846,8 @@ void MapExtras::Block::ParseTiles(TileInfo *tiles)
is_con = true;
tiles->con_info->constructed.setassignment(x,y,true);
tiles->con_info->tiles[x][y] = tt;
tiles->con_info->mattype[x][y] = con->mat_type;
tiles->con_info->matindex[x][y] = con->mat_index;
tiles->con_info->mat_type[x][y] = con->mat_type;
tiles->con_info->mat_index[x][y] = con->mat_index;
tt = con->original_tile;
}
@ -753,14 +880,14 @@ void MapExtras::Block::ParseBasemats(TileInfo *tiles, BasematInfo *bmats)
auto tt = tiles->base_tiles[x][y];
auto mat = info.getBaseMaterial(tt, df::coord2d(x,y));
bmats->mattype[x][y] = mat.mat_type;
bmats->matindex[x][y] = mat.mat_index;
bmats->mat_type[x][y] = mat.mat_type;
bmats->mat_index[x][y] = mat.mat_index;
// Copy base info back to construction layer
if (tiles->con_info && !tiles->con_info->constructed.getassignment(x,y))
{
tiles->con_info->mattype[x][y] = mat.mat_type;
tiles->con_info->matindex[x][y] = mat.mat_index;
tiles->con_info->mat_type[x][y] = mat.mat_type;
tiles->con_info->mat_index[x][y] = mat.mat_index;
}
}
}

@ -190,6 +190,8 @@ bool MaterialInfo::find(const std::vector<std::string> &items)
}
else if (items.size() == 2)
{
if (items[1] == "NONE" && findBuiltin(items[0]))
return true;
if (findPlant(items[0], items[1]))
return true;
if (findCreature(items[0], items[1]))
@ -210,7 +212,7 @@ bool MaterialInfo::findBuiltin(const std::string &token)
}
df::world_raws &raws = world->raws;
for (int i = 1; i < NUM_BUILTIN; i++)
for (int i = 0; i < NUM_BUILTIN; i++)
{
auto obj = raws.mat_table.builtin[i];
if (obj && obj->id == token)
@ -423,6 +425,8 @@ bool MaterialInfo::matches(const df::dfhack_material_category &cat)
TEST(glass, IS_GLASS);
if (cat.bits.clay && linear_index(material->reaction_product.id, std::string("FIRED_MAT")) >= 0)
return true;
if (cat.bits.milk && linear_index(material->reaction_product.id, std::string("CHEESE_MAT")) >= 0)
return true;
return false;
}
@ -497,7 +501,7 @@ void MaterialInfo::getMatchBits(df::job_item_flags2 &ok, df::job_item_flags2 &ma
TEST(fire_safe, material->heat.melting_point > 11000);
TEST(magma_safe, material->heat.melting_point > 12000);
TEST(deep_material, FLAG(inorganic, inorganic_flags::SPECIAL));
TEST(non_economic, inorganic && !(ui && ui->economic_stone[index]));
TEST(non_economic, !inorganic || !(ui && vector_get(ui->economic_stone, index)));
TEST(plant, plant);
TEST(silk, MAT_FLAG(SILK));
@ -762,7 +766,7 @@ bool Materials::ReadCreatureTypesEx (void)
for(size_t k = 0; k < sizecolormod;k++)
{
// color mod [0] -> color list
auto & indexes = colorings[k]->color_indexes;
auto & indexes = colorings[k]->pattern_index;
size_t sizecolorlist = indexes.size();
caste.ColorModifier[k].colorlist.resize(sizecolorlist);
for(size_t l = 0; l < sizecolorlist; l++)
@ -836,7 +840,7 @@ bool Materials::ReadAllMaterials(void)
std::string Materials::getDescription(const t_material & mat)
{
MaterialInfo mi(mat.material, mat.index);
MaterialInfo mi(mat.mat_type, mat.mat_index);
if (mi.creature)
return mi.creature->creature_id + " " + mi.material->id;
else if (mi.plant)
@ -849,7 +853,7 @@ std::string Materials::getDescription(const t_material & mat)
// This is completely worthless now
std::string Materials::getType(const t_material & mat)
{
MaterialInfo mi(mat.material, mat.index);
MaterialInfo mi(mat.mat_type, mat.mat_index);
switch (mi.mode)
{
case MaterialInfo::Builtin:

@ -28,6 +28,7 @@ distribution.
#include <string>
#include <vector>
#include <map>
#include <set>
using namespace std;
#include "modules/Screen.h"
@ -54,6 +55,7 @@ using namespace DFHack;
#include "df/item.h"
#include "df/job.h"
#include "df/building.h"
#include "df/renderer.h"
using namespace df::enums;
using df::global::init;
@ -64,6 +66,8 @@ using df::global::enabler;
using Screen::Pen;
using std::string;
/*
* Screen painting API.
*/
@ -106,10 +110,10 @@ bool Screen::paintTile(const Pen &pen, int x, int y)
{
if (!gps || !pen.valid()) return false;
int dimx = gps->dimx, dimy = gps->dimy;
if (x < 0 || x >= dimx || y < 0 || y >= dimy) return false;
auto dim = getWindowSize();
if (x < 0 || x >= dim.x || y < 0 || y >= dim.y) return false;
doSetTile(pen, x*dimy + y);
doSetTile(pen, x*dim.y + y);
return true;
}
@ -117,11 +121,11 @@ Pen Screen::readTile(int x, int y)
{
if (!gps) return Pen(0,0,0,-1);
int dimx = gps->dimx, dimy = gps->dimy;
if (x < 0 || x >= dimx || y < 0 || y >= dimy)
auto dim = getWindowSize();
if (x < 0 || x >= dim.x || y < 0 || y >= dim.y)
return Pen(0,0,0,-1);
int index = x*dimy + y;
int index = x*dim.y + y;
auto screen = gps->screen + index*4;
if (screen[3] & 0x80)
return Pen(0,0,0,-1);
@ -150,14 +154,15 @@ Pen Screen::readTile(int x, int y)
bool Screen::paintString(const Pen &pen, int x, int y, const std::string &text)
{
if (!gps || y < 0 || y >= gps->dimy) return false;
auto dim = getWindowSize();
if (!gps || y < 0 || y >= dim.y) return false;
Pen tmp(pen);
bool ok = false;
for (size_t i = -std::min(0,x); i < text.size(); i++)
{
if (x + i >= size_t(gps->dimx))
if (x + i >= size_t(dim.x))
break;
tmp.ch = text[i];
@ -171,17 +176,18 @@ bool Screen::paintString(const Pen &pen, int x, int y, const std::string &text)
bool Screen::fillRect(const Pen &pen, int x1, int y1, int x2, int y2)
{
auto dim = getWindowSize();
if (!gps || !pen.valid()) return false;
if (x1 < 0) x1 = 0;
if (y1 < 0) y1 = 0;
if (x2 >= gps->dimx) x2 = gps->dimx-1;
if (y2 >= gps->dimy) y2 = gps->dimy-1;
if (x2 >= dim.x) x2 = dim.x-1;
if (y2 >= dim.y) y2 = dim.y-1;
if (x1 > x2 || y1 > y2) return false;
for (int x = x1; x <= x2; x++)
{
int index = x*gps->dimy;
int index = x*dim.y;
for (int y = y1; y <= y2; y++)
doSetTile(pen, index+y);
@ -194,32 +200,33 @@ bool Screen::drawBorder(const std::string &title)
{
if (!gps) return false;
int dimx = gps->dimx, dimy = gps->dimy;
auto dim = getWindowSize();
Pen border('\xDB', 8);
Pen text(0, 0, 7);
Pen signature(0, 0, 8);
for (int x = 0; x < dimx; x++)
for (int x = 0; x < dim.x; x++)
{
doSetTile(border, x * dimy + 0);
doSetTile(border, x * dimy + dimy - 1);
doSetTile(border, x * dim.y + 0);
doSetTile(border, x * dim.y + dim.y - 1);
}
for (int y = 0; y < dimy; y++)
for (int y = 0; y < dim.y; y++)
{
doSetTile(border, 0 * dimy + y);
doSetTile(border, (dimx - 1) * dimy + y);
doSetTile(border, 0 * dim.y + y);
doSetTile(border, (dim.x - 1) * dim.y + y);
}
paintString(signature, dimx-8, dimy-1, "DFHack");
paintString(signature, dim.x-8, dim.y-1, "DFHack");
return paintString(text, (dimx - title.length()) / 2, 0, title);
return paintString(text, (dim.x - title.length()) / 2, 0, title);
}
bool Screen::clear()
{
if (!gps) return false;
return fillRect(Pen(' ',0,0,false), 0, 0, gps->dimx-1, gps->dimy-1);
auto dim = getWindowSize();
return fillRect(Pen(' ',0,0,false), 0, 0, dim.x-1, dim.y-1);
}
bool Screen::invalidate()
@ -230,6 +237,21 @@ bool Screen::invalidate()
return true;
}
const Pen Screen::Painter::default_pen(0,COLOR_GREY,0);
const Pen Screen::Painter::default_key_pen(0,COLOR_LIGHTGREEN,0);
void Screen::Painter::do_paint_string(const std::string &str, const Pen &pen)
{
if (gcursor.y < clip.first.y || gcursor.y > clip.second.y)
return;
int dx = std::max(0, int(clip.first.x - gcursor.x));
int len = std::min((int)str.size(), int(clip.second.x - gcursor.x + 1));
if (len > dx)
paintString(pen, gcursor.x + dx, gcursor.y, str.substr(dx, len-dx));
}
bool Screen::findGraphicsTile(const std::string &pagename, int x, int y, int *ptile, int *pgs)
{
if (!gps || !texture || x < 0 || y < 0) return false;
@ -303,6 +325,94 @@ bool Screen::isDismissed(df::viewscreen *screen)
return screen->breakdown_level != interface_breakdown_types::NONE;
}
#ifdef _LINUX
// Link to the libgraphics class directly:
class DFHACK_EXPORT enabler_inputst {
public:
std::string GetKeyDisplay(int binding);
};
class DFHACK_EXPORT renderer {
unsigned char *screen;
long *screentexpos;
char *screentexpos_addcolor;
unsigned char *screentexpos_grayscale;
unsigned char *screentexpos_cf;
unsigned char *screentexpos_cbr;
// For partial printing:
unsigned char *screen_old;
long *screentexpos_old;
char *screentexpos_addcolor_old;
unsigned char *screentexpos_grayscale_old;
unsigned char *screentexpos_cf_old;
unsigned char *screentexpos_cbr_old;
public:
virtual void update_tile(int x, int y) {};
virtual void update_all() {};
virtual void render() {};
virtual void set_fullscreen();
virtual void zoom(df::zoom_commands cmd);
virtual void resize(int w, int h) {};
virtual void grid_resize(int w, int h) {};
renderer() {
screen = NULL;
screentexpos = NULL;
screentexpos_addcolor = NULL;
screentexpos_grayscale = NULL;
screentexpos_cf = NULL;
screentexpos_cbr = NULL;
screen_old = NULL;
screentexpos_old = NULL;
screentexpos_addcolor_old = NULL;
screentexpos_grayscale_old = NULL;
screentexpos_cf_old = NULL;
screentexpos_cbr_old = NULL;
}
virtual ~renderer();
virtual bool get_mouse_coords(int &x, int &y) { return false; }
virtual bool uses_opengl();
};
#else
struct less_sz {
bool operator() (const string &a, const string &b) const {
if (a.size() < b.size()) return true;
if (a.size() > b.size()) return false;
return a < b;
}
};
static std::map<df::interface_key,std::set<string,less_sz> > *keydisplay = NULL;
#endif
void init_screen_module(Core *core)
{
#ifdef _LINUX
renderer tmp;
if (!strict_virtual_cast<df::renderer>((virtual_ptr)&tmp))
cerr << "Could not fetch the renderer vtable." << std::endl;
#else
if (!core->vinfo->getAddress("keydisplay", keydisplay))
keydisplay = NULL;
#endif
}
string Screen::getKeyDisplay(df::interface_key key)
{
#ifdef _LINUX
auto enabler = (enabler_inputst*)df::global::enabler;
if (enabler)
return enabler->GetKeyDisplay(key);
#else
if (keydisplay)
{
auto it = keydisplay->find(key);
if (it != keydisplay->end() && !it->second.empty())
return *it->second.begin();
}
#endif
return "?";
}
/*
* Base DFHack viewscreen.
*/
@ -554,14 +664,24 @@ int dfhack_lua_viewscreen::do_input(lua_State *L)
if (enabler && enabler->tracking_on)
{
if (enabler->mouse_lbut) {
if (enabler->mouse_lbut_down) {
lua_pushboolean(L, true);
lua_setfield(L, -2, "_MOUSE_L");
}
if (enabler->mouse_rbut) {
if (enabler->mouse_rbut_down) {
lua_pushboolean(L, true);
lua_setfield(L, -2, "_MOUSE_R");
}
if (enabler->mouse_lbut) {
lua_pushboolean(L, true);
lua_setfield(L, -2, "_MOUSE_L_DOWN");
enabler->mouse_lbut = 0;
}
if (enabler->mouse_rbut) {
lua_pushboolean(L, true);
lua_setfield(L, -2, "_MOUSE_R_DOWN");
enabler->mouse_rbut = 0;
}
}
lua_call(L, 2, 0);
@ -591,7 +711,12 @@ dfhack_lua_viewscreen::~dfhack_lua_viewscreen()
void dfhack_lua_viewscreen::render()
{
if (Screen::isDismissed(this)) return;
if (Screen::isDismissed(this))
{
if (parent)
parent->render();
return;
}
dfhack_viewscreen::render();

@ -115,6 +115,9 @@ void Translation::setNickname(df::language_name *name, std::string nick)
if (!name->has_name)
{
if (nick.empty())
return;
*name = df::language_name();
name->language = 0;
@ -122,6 +125,18 @@ void Translation::setNickname(df::language_name *name, std::string nick)
}
name->nickname = nick;
// If the nick is empty, check if this made the whole name empty
if (name->nickname.empty() && name->first_name.empty())
{
bool has_words = false;
for (int i = 0; i < 7; i++)
if (name->words[i] >= 0)
has_words = true;
if (!has_words)
name->has_name = false;
}
}
string Translation::TranslateName(const df::language_name * name, bool inEnglish, bool onlyLastPart)

Some files were not shown because too many files have changed in this diff Show More