diff --git a/.gitignore b/.gitignore index bdb9474bb..9f2b009c6 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,9 @@ build/tools build/plugins build/depends build/install_manifest.txt +build/README.html +build/LUA_API.html +build/COMPILE.html #ignore Kdevelop stuff .kdev4 diff --git a/CMake/Modules/FindDocutils.cmake b/CMake/Modules/FindDocutils.cmake new file mode 100644 index 000000000..8103628df --- /dev/null +++ b/CMake/Modules/FindDocutils.cmake @@ -0,0 +1,3 @@ +FIND_PROGRAM(RST2HTML_EXECUTABLE NAMES rst2html rst2html.py) +INCLUDE(FindPackageHandleStandardArgs) +FIND_PACKAGE_HANDLE_STANDARD_ARGS(Docutils DEFAULT_MSG RST2HTML_EXECUTABLE) \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index dfb13cd5d..41c38bd44 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -63,7 +63,7 @@ set(DF_VERSION_MINOR "34") set(DF_VERSION_PATCH "11") set(DF_VERSION "${DF_VERSION_MAJOR}.${DF_VERSION_MINOR}.${DF_VERSION_PATCH}") -SET(DFHACK_RELEASE "r1" 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}") add_definitions(-DDFHACK_VERSION="${DFHACK_VERSION}") @@ -145,12 +145,33 @@ include_directories(depends/clsocket/src) add_subdirectory(depends) +#find_package(Docutils) + +#set (RST_FILES +#"Readme" +#"Compile" +#"LUA Api" +#"Contributors" +#) + +#set (RST_PROCESSED_FILES "") +#IF(RST2HTML_EXECUTABLE) +# foreach(F ${RST_FILES}) +# add_custom_command( +# OUTPUT "${dfhack_BINARY_DIR}/${F}.html" +# COMMAND ${RST2HTML_EXECUTABLE} "${dfhack_SOURCE_DIR}/${F}.rst" "${dfhack_BINARY_DIR}/${F}.html" +# COMMENT "Translating ${F} to html" +# DEPENDS "${dfhack_SOURCE_DIR}/${F}.rst") +# list (APPEND RST_PROCESSED_FILES "${dfhack_BINARY_DIR}/${F}.html") +# endforeach() +# add_custom_target(HTML_DOCS ALL DEPENDS ${RST_PROCESSED_FILES}) +#ENDIF() # build the lib itself IF(BUILD_LIBRARY) add_subdirectory (library) ## install the default documentation files - install(FILES LICENSE Readme.html Compile.html Lua\ API.html DESTINATION ${DFHACK_USERDOC_DESTINATION}) + install(FILES LICENSE "Lua API.html" Readme.html Compile.html Contributors.html DESTINATION ${DFHACK_USERDOC_DESTINATION}) endif() #build the plugins diff --git a/Compile.html b/Compile.html index e17e57e22..e5f6ca679 100644 --- a/Compile.html +++ b/Compile.html @@ -3,13 +3,13 @@ - + Building DFHACK + + +
+

Contributors

+ +

If you belong here and are missing, please add yourself and send me (peterix) a pull request :-)

+

The following is a list of people who have contributed to DFHack.

+ +

And those are the cool people who made stonesense.

+ +
+ + diff --git a/Contributors.rst b/Contributors.rst new file mode 100644 index 000000000..b10877718 --- /dev/null +++ b/Contributors.rst @@ -0,0 +1,74 @@ +Contributors +============ +If you belong here and are missing, please add yourself and send me (peterix) a pull request :-) + +The following is a list of people who have contributed to **DFHack**. + +- Petr Mrázek +- Alexander Gavrilov +- doomchild +- Quietust +- jj +- Warmist +- Robert Heinrich +- simon +- Kelly Martin +- mizipzor +- Simon Jackson +- belal +- RusAnon +- Raoul XQ +- Matthew Cline +- Mike Stewart +- Timothy Collett +- RossM +- Tom Prince +- Jared Adams +- expwnent +- Erik Youngren +- Espen Wiborg +- Tim Walberg +- Mikko Juola +- rampaging-poet +- U-glouglou\\simon +- Clayton Hughes +- zilpin +- Will Rogers +- NMLittle +- root +- reverb +- Zhentar +- Valentin Ochs +- Priit Laes +- kmartin +- Neil Little +- rout +- rofl0r +- harlanplayford +- gsvslto +- sami +- potato +- playfordh +- feng1st +- comestible +- Rumrusher +- Rinin +- Raoul van Putten +- John Shade +- John Beisley +- Feng +- Donald Ruegsegger +- Caldfir +- Antalia +- Angus Mezick + +And those are the cool people who made **stonesense**. + +- Kris Parker +- Japa +- Jonas Ask +- Petr Mrázek +- Caldfir +- 8Z +- Alexander Gavrilov +- Timothy Collett diff --git a/LICENSE b/LICENSE index 96ab022d9..2a9a5513b 100644 --- a/LICENSE +++ b/LICENSE @@ -2,7 +2,7 @@ License of dfhack https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/Lua API.html b/Lua API.html index 07f038bc4..a52347104 100644 --- a/Lua API.html +++ b/Lua API.html @@ -3,13 +3,13 @@ - + DFHack Lua API -
- +
+

DFHack Readme

-

Introduction

+

Introduction

DFHack is a Dwarf Fortress memory access library and a set of basic tools that use it. Tools come in the form of plugins or (not yet) external tools. It is an attempt to unite the various ways tools @@ -326,253 +334,222 @@ access DF memory and allow for easier development of new tools.

Contents

-

Getting DFHack

+

Getting DFHack

The project is currently hosted on github, for both source and binaries at http://github.com/peterix/dfhack

Releases can be downloaded from here: https://github.com/peterix/dfhack/downloads

All new releases are announced in the bay12 thread: http://tinyurl.com/dfhack-ng

-

Compatibility

+

Compatibility

DFHack works on Windows XP, Vista, 7 or any modern Linux distribution. OSX is not supported due to lack of developers with a Mac.

-

Currently, versions 0.34.08 - 0.34.11 are supported. If you need DFHack +

Currently, version 0.34.11 is supported (and tested). If you need DFHack for older versions, look for older releases.

On Windows, you have to use the SDL version of DF.

It is possible to use the Windows DFHack under wine/OSX.

-

Installation/Removal

+

Installation/Removal

Installing DFhack involves copying files into your DF folder. Copy the files from a release archive so that:

@@ -594,7 +571,7 @@ remove the other DFHack files file created in your DF folder.

-

Using DFHack

+

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 @@ -616,9 +593,37 @@ of the game, so it becomes necessary to use the dfhack.init file to ensure that they are re-created every time it is loaded.

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.

+
-

Something doesn't work, help!

+

Something doesn't work, help!

First, don't panic :) Second, dfhack keeps a few log files in DF's folder - stderr.log and stdout.log. You can look at those and possibly find out what's happening. @@ -627,24 +632,181 @@ the issues tracker on github, contact me ( -

The init file

-

If your DF folder contains a file named dfhack.init, its contents will be run +

The init file

+

If your DF folder contains a file named dfhack.init, its contents will be run every time you start DF. This allows setting up keybindings. An example file -is provided as dfhack.init-example - you can tweak it and rename to dfhack.init +is provided as dfhack.init-example - you can tweak it and rename to dfhack.init if you want to use this functionality.

+
+

Setting keybindings

+

To set keybindings, use the built-in keybinding command. Like any other +command it can be used at any time from the console, but it is also meaningful +in the DFHack init file.

+

Currently it supports any combination of Ctrl/Alt/Shift with F1-F9, or A-Z.

+

Possible ways to call the command:

+ +++ + + + + + + + + + + + + + +
keybinding list <key>:
 List bindings active for the key combination.
keybinding clear <key> <key>...:
 Remove bindings for the specified keys.
keybinding add <key> "cmdline" "cmdline"...:
 Add bindings for the specified +key.
keybinding set <key> "cmdline" "cmdline"...:
 Clear, and then add bindings for +the specified key.
+

The <key> parameter above has the following case-sensitive syntax:

+
+[Ctrl-][Alt-][Shift-]KEY[@context]
+
+

where the KEY part can be F1-F9 or A-Z, and [] denote optional parts.

+

When multiple commands are bound to the same key combination, DFHack selects +the first applicable one. Later 'add' commands, and earlier entries within one +'add' command have priority. Commands that are not specifically intended for use +as a hotkey are always considered applicable.

+

The context part in the key specifier above can be used to explicitly restrict +the UI state where the binding would be applicable. If called without parameters, +the keybinding command among other things prints the current context string. +Only bindings with a context tag that either matches the current context fully, +or is a prefix ending at a '/' boundary would be considered for execution, i.e. +for context foo/bar/baz, possible matches are any of @foo/bar/baz, @foo/bar, +@foo or none.

+
-

Commands

+

Commands

+

DFHack command syntax consists of a command name, followed by arguments separated +by whitespace. To include whitespace in an argument, quote it in double quotes. +To include a double quote character, use \" inside double quotes.

+

If the first non-whitespace character of a line is #, the line is treated +as a comment, i.e. a silent no-op command.

+

If the first non-whitespace character is :, the command is parsed in a special +alternative mode: first, non-whitespace characters immediately following the : +are used as the command name; the remaining part of the line, starting with the first +non-whitespace character after the command name, is used verbatim as the first argument. +The following two command lines are exactly equivalent:

+
+
    +
  • :foo a b "c d" e f
  • +
  • foo "a b \"c d\" e f"
  • +
+
+

This is intended for commands like rb_eval that evaluate script language statements.

Almost all the commands support using the 'help <command-name>' built-in command to retrieve further help without having to look at this document. Alternatively, some accept a 'help'/'?' option on their command line.

+
+

Game progress

+
+

die

+

Instantly kills DF without saving.

+
+
+

forcepause

+

Forces DF to pause. This is useful when your FPS drops below 1 and you lose +control of the game.

+
+
    +
  • Activate with 'forcepause 1'
  • +
  • Deactivate with 'forcepause 0'
  • +
+
+
+
+

nopause

+

Disables pausing (both manual and automatic) with the exception of pause forced +by 'reveal hell'. This is nice for digging under rivers.

+
+
+

fastdwarf

+

Controls speedydwarf and teledwarf. Speedydwarf makes dwarves move quickly and perform tasks quickly. Teledwarf makes dwarves move instantaneously, but do jobs at the same speed.

+
+
    +
  • 'fastdwarf 0 0' disables both
  • +
  • 'fastdwarf 0 1' disables speedydwarf and enables teledwarf
  • +
  • 'fastdwarf 1 0' enables speedydwarf and disables teledwarf
  • +
  • '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

+
+

follow

+

Makes the game view follow the currently highlighted unit after you exit from +current menu/cursor mode. Handy for watching dwarves running around. Deactivated +by moving the view manually.

+
+
+

tidlers

+

Toggle between all possible positions where the idlers count can be placed.

+
+
+

twaterlvl

+

Toggle between displaying/not displaying liquid depth as numbers.

+
+
+

copystock

+

Copies the parameters of the currently highlighted stockpile to the custom +stockpile settings and switches to custom stockpile placement mode, effectively +allowing you to copy/paste stockpiles easily.

+
+
+

rename

+

Allows renaming various things.

+

Options:

+
+ +++ + + + + + + + + + + + + + + + + +
rename squad <index> "name":
 Rename squad by index to 'name'.
rename hotkey <index> "name":
 Rename hotkey by index. This allows assigning +longer commands to the DF hotkeys.
rename unit "nickname":
 Rename a unit/creature highlighted in the DF user +interface.
rename unit-profession "custom profession":
 Change proffession name of the +highlighted unit/creature.
rename building "name":
 Set a custom name for the selected building. +The building must be one of stockpile, workshop, furnace, trap, +siege engine or an activity zone.
+
+
+
+
+

Adventure mode

-

adv-bodyswap

+

adv-bodyswap

This allows taking control over your followers and other creatures in adventure mode. For example, you can make them pick up new arms and armor and equip them properly.

-
-

Usage

+

Usage:

  • When viewing unit details, body-swaps into that unit.
  • @@ -652,12 +814,11 @@ properly.

-
-

advtools

+

advtools

A package of different adventure mode tools (currently just one)

-
-

Usage

+

Usage:

+
@@ -673,10 +834,13 @@ on item type and being in shop.
+
+
+

Map modification

-

changelayer

+

changelayer

Changes material of the geology layer under cursor to the specified inorganic RAW material. Can have impact on all surrounding regions, not only your embark! By default changing stone to soil and vice versa is not allowed. By default @@ -687,8 +851,8 @@ as well, though. Mineral veins and gem clusters will stay on the map. Use 'changevein' for them.

tl;dr: You will end up with changing quite big areas in one go, especially if you use it in lower z levels. Use with care.

-
-

Options

+

Options:

+
@@ -720,9 +884,9 @@ soil.
-
-
-

Examples:

+ +

Examples:

+
changelayer GRANITE
Convert layer at cursor position into granite.
@@ -731,6 +895,7 @@ soil.
changelayer MARBLE all_biomes all_layers
Convert all layers of all biomes which are not soil into marble.
+

Note

    @@ -749,22 +914,21 @@ You did save your game, right?
-
-

changevein

+

changevein

Changes material of the vein under cursor to the specified inorganic RAW material. Only affects tiles within the current 16x16 block - for veins and large clusters, you will need to use this command multiple times.

-
-

Example:

+

Example:

+
changevein NATIVE_PLATINUM
Convert vein at cursor position into platinum ore.
-
+
-

changeitem

+

changeitem

Allows changing item material and base quality. By default the item currently selected in the UI will be changed (you can select items in the 'k' list or inside containers/inventory). By default change is only allowed if materials @@ -774,8 +938,8 @@ with 'force'. Note that some attributes will not be touched, possibly resulting in weirdness. To get an idea how the RAW id should look like, check some items with 'info'. Using 'force' might create items which are not touched by crafters/haulers.

-
-

Options

+

Options:

+
@@ -792,358 +956,592 @@ crafters/haulers.

-
-
-

Examples:

+ +

Examples:

+
changeitem m INORGANIC:GRANITE here
Change material of all items under the cursor to granite.
changeitem q 5
Change currently selected item to masterpiece quality.
+
-
-
-

cursecheck

-

Checks a single map tile or the whole map/world for cursed creatures (ghosts, -vampires, necromancers, werebeasts, zombies).

-

With an active in-game cursor only the selected tile will be observed. -Without a cursor the whole map will be checked.

-

By default cursed creatures will be only counted in case you just want to find -out if you have any of them running around in your fort. Dead and passive -creatures (ghosts who were put to rest, killed vampires, ...) are ignored. -Undead skeletons, corpses, bodyparts and the like are all thrown into the curse -category "zombie". Anonymous zombies and resurrected body parts will show -as "unnamed creature".

-
-

Options

+
+

colonies

+

Allows listing all the vermin colonies on the map and optionally turning them into honey bee colonies.

+

Options:

+
- - - - - - - +
detail:Print full name, date of birth, date of curse and some status -info (some vampires might use fake identities in-game, though).
nick:Set the type of curse as nickname (does not always show up -in-game, some vamps don't like nicknames).
all:Include dead and passive cursed creatures (can result in a quite -long list after having FUN with necromancers).
verbose:Print all curse tags (if you really want to know it all).
bees:turn colonies into honey bee colonies
+
-
-

Examples:

-
-
cursecheck detail all
-
Give detailed info about all cursed creatures including deceased ones (no -in-game cursor).
-
cursecheck nick
-
Give a nickname all living/active cursed creatures on the map(no in-game -cursor).
-
-
-

Note

-
    -
  • If you do a full search (with the option "all") former ghosts will show up -with the cursetype "unknown" because their ghostly flag is not set -anymore. But if you happen to find a living/active creature with cursetype -"unknown" please report that in the dfhack thread on the modding forum or -per irc. This is likely to happen with mods which introduce new types -of curses, for example.
  • -
-
-
-
-
-

follow

-

Makes the game view follow the currently highlighted unit after you exit from -current menu/cursor mode. Handy for watching dwarves running around. Deactivated -by moving the view manually.

+
+

deramp (by zilpin)

+

Removes all ramps designated for removal from the map. This is useful for replicating the old channel digging designation. +It also removes any and all 'down ramps' that can remain after a cave-in (you don't have to designate anything for that to happen).

-
-

forcepause

-

Forces DF to pause. This is useful when your FPS drops below 1 and you lose -control of the game.

-
+
+

feature

+

Enables management of map features.

    -
  • Activate with 'forcepause 1'
  • -
  • Deactivate with 'forcepause 0'
  • +
  • Discovering a magma feature (magma pool, volcano, magma sea, or curious +underground structure) permits magma workshops and furnaces to be built.
  • +
  • Discovering a cavern layer causes plants (trees, shrubs, and grass) from +that cavern to grow within your fortress.
-
-
-
-

nopause

-

Disables pausing (both manual and automatic) with the exception of pause forced -by 'reveal hell'. This is nice for digging under rivers.

-
-
-

die

-

Instantly kills DF without saving.

-
-
-

autodump

-

This utility lets you quickly move all items designated to be dumped. -Items are instantly moved to the cursor position, the dump flag is unset, -and the forbid flag is set, as if it had been dumped normally. -Be aware that any active dump item tasks still point at the item.

-

Cursor must be placed on a floor tile so the items can be dumped there.

-
-

Options

+

Options:

+
- - - - - + - + - +
destroy:Destroy instead of dumping. Doesn't require a cursor.
destroy-here:Destroy items only under the cursor.
visible:Only process items that are not hidden.
list:Lists all map features in your current embark by index.
hidden:Only process hidden items.
show X:Marks the selected map feature as discovered.
forbidden:Only process forbidden items (default: only unforbidden).
hide X:Marks the selected map feature as undiscovered.
+
+
+

liquids

+

Allows adding magma, water and obsidian to the game. It replaces the normal +dfhack command line and can't be used from a hotkey. Settings will be remembered +as long as dfhack runs. Intended for use in combination with the command +liquids-here (which can be bound to a hotkey).

+

For more information, refer to the command's internal help.

+
+

Note

+

Spawning and deleting liquids can F up pathing data and +temperatures (creating heat traps). You've been warned.

-
-

autodump-destroy-here

-

Destroy items marked for dumping under cursor. Identical to autodump -destroy-here, but intended for use as keybinding.

-
-
-

autodump-destroy-item

-

Destroy the selected item. The item may be selected in the 'k' list, or inside -a container. If called again before the game is resumed, cancels destroy.

-
-
-

burrow

-

Miscellaneous burrow control. Allows manipulating burrows and automated burrow -expansion while digging.

-
-

Options

- --- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
enable feature ...:
 
disable feature ...:
 Enable or Disable features of the plugin.
clear-unit burrow burrow ...:
 
clear-tiles burrow burrow ...:
 Removes all units or tiles from the burrows.
set-units target-burrow src-burrow ...:
 
add-units target-burrow src-burrow ...:
 
remove-units target-burrow src-burrow ...:
 Adds or removes units in source -burrows to/from the target burrow. Set is equivalent to clear and add.
set-tiles target-burrow src-burrow ...:
 
add-tiles target-burrow src-burrow ...:
 
remove-tiles target-burrow src-burrow ...:
 Adds or removes tiles in source -burrows to/from the target burrow. In place of a source burrow it is -possible to use one of the following keywords: ABOVE_GROUND, -SUBTERRANEAN, INSIDE, OUTSIDE, LIGHT, DARK, HIDDEN, REVEALED
-
-

Features

- --- - - - -
auto-grow:When a wall inside a burrow with a name ending in '+' is dug -out, the burrow is extended to newly-revealed adjacent walls. -This final '+' may be omitted in burrow name args of commands above. -Digging 1-wide corridors with the miner inside the burrow is SLOW.
+
+

liquids-here

+

Run the liquid spawner with the current/last settings made in liquids (if no +settings in liquids were made it paints a point of 7/7 magma by default).

+

Intended to be used as keybinding. Requires an active in-game cursor.

+
+
+

tiletypes

+

Can be used for painting map tiles and is an interactive command, much like +liquids.

+

The tool works with two set of options and a brush. The brush determines which +tiles will be processed. First set of options is the filter, which can exclude +some of the tiles from the brush by looking at the tile properties. The second +set of options is the paint - this determines how the selected tiles are +changed.

+

Both paint and filter can have many different properties including things like +general shape (WALL, FLOOR, etc.), general material (SOIL, STONE, MINERAL, +etc.), state of 'designated', 'hidden' and 'light' flags.

+

The properties of filter and paint can be partially defined. This means that +you can for example do something like this:

+
+filter material STONE
+filter shape FORTIFICATION
+paint shape FLOOR
+
+

This will turn all stone fortifications into floors, preserving the material.

+

Or this:

+
+filter shape FLOOR
+filter material MINERAL
+paint shape WALL
+
+

Turning mineral vein floors back into walls.

+

The tool also allows tweaking some tile flags:

+

Or this:

+
+paint hidden 1
+paint hidden 0
+
+

This will hide previously revealed tiles (or show hidden with the 0 option).

+

Any paint or filter option (or the entire paint or filter) can be disabled entirely by using the ANY keyword:

+
+paint hidden ANY
+paint shape ANY
+filter material any
+filter shape any
+filter any
+
+
+
You can use several different brushes for painting tiles:
+
    +
  • Point. (point)
  • +
  • Rectangular range. (range)
  • +
  • A column ranging from current cursor to the first solid tile above. (column)
  • +
  • DF map block - 16x16 tiles, in a regular grid. (block)
  • +
+
+
+

Example:

+
+range 10 10 1
+
+

This will change the brush to a rectangle spanning 10x10 tiles on one z-level. +The range starts at the position of the cursor and goes to the east, south and +up.

+

For more details, see the 'help' command while using this.

+
+

tiletypes-commands

+

Runs tiletypes commands, separated by ;. This makes it possible to change +tiletypes modes from a hotkey.

-
-

catsplosion

-

Makes cats just multiply. It is not a good idea to run this more than once or -twice.

+
+

tiletypes-here

+

Apply the current tiletypes options at the in-game cursor position, including +the brush. Can be used from a hotkey.

-
-

clean

-

Cleans all the splatter that get scattered all over the map, items and -creatures. In an old fortress, this can significantly reduce FPS lag. It can -also spoil your !!FUN!!, so think before you use it.

-
-

Options

+
+

tiletypes-here-point

+

Apply the current tiletypes options at the in-game cursor position to a single +tile. Can be used from a hotkey.

+
+
+

tubefill

+

Fills all the adamantine veins again. Veins that were empty will be filled in +too, but might still trigger a demon invasion (this is a known bug).

+
+
+

extirpate

+

A tool for getting rid of trees and shrubs. By default, it only kills +a tree/shrub under the cursor. The plants are turned into ashes instantly.

+

Options:

+
- + - + - +
map:Clean the map tiles. By default, it leaves mud and snow alone.
shrubs:affect all shrubs on the map
units:Clean the creatures. Will also clean hostiles.
trees:affect all trees on the map
items:Clean all the items. Even a poisoned blade.
all:affect every plant!
+
+
+
+

grow

+

Makes all saplings present on the map grow into trees (almost) instantly.

+
+
+

immolate

+

Very similar to extirpate, but additionally sets the plants on fire. The fires +can and will spread ;)

+
+
+

regrass

+

Regrows grass. Not much to it ;)

-
-

Extra options for 'map'

+
+

weather

+

Prints the current weather map by default.

+

Also lets you change the current weather to 'clear sky', 'rainy' or 'snowing'.

+

Options:

+
- + - + + +
mud:Remove mud in addition to the normal stuff.
snow:make it snow everywhere.
snow:Also remove snow coverings.
rain:make it rain.
clear:clear the sky.
+
-
-

spotclean

-

Works like 'clean map snow mud', but only for the tile under the cursor. Ideal -if you want to keep that bloody entrance 'clean map' would clean up.

-
-
-

cleanowned

-

Confiscates items owned by dwarfs. By default, owned food on the floor -and rotten items are confistacted and dumped.

-
-

Options

+
+

Map inspection

+
+

cursecheck

+

Checks a single map tile or the whole map/world for cursed creatures (ghosts, +vampires, necromancers, werebeasts, zombies).

+

With an active in-game cursor only the selected tile will be observed. +Without a cursor the whole map will be checked.

+

By default cursed creatures will be only counted in case you just want to find +out if you have any of them running around in your fort. Dead and passive +creatures (ghosts who were put to rest, killed vampires, ...) are ignored. +Undead skeletons, corpses, bodyparts and the like are all thrown into the curse +category "zombie". Anonymous zombies and resurrected body parts will show +as "unnamed creature".

+

Options:

+
- - - + - + - + - +
all:confiscate all owned items
scattered:confiscated and dump all items scattered on the floor
detail:Print full name, date of birth, date of curse and some status +info (some vampires might use fake identities in-game, though).
x:confiscate/dump items with wear level 'x' and more
nick:Set the type of curse as nickname (does not always show up +in-game, some vamps don't like nicknames).
X:confiscate/dump items with wear level 'X' and more
all:Include dead and passive cursed creatures (can result in a quite +long list after having FUN with necromancers).
dryrun:a dry run. combine with other options to see what will happen -without it actually happening.
verbose:Print all curse tags (if you really want to know it all).
+
+

Examples:

+
+
+
cursecheck detail all
+
Give detailed info about all cursed creatures including deceased ones (no +in-game cursor).
+
cursecheck nick
+
Give a nickname all living/active cursed creatures on the map(no in-game +cursor).
+
+
+
+

Note

+
    +
  • If you do a full search (with the option "all") former ghosts will show up +with the cursetype "unknown" because their ghostly flag is not set +anymore. But if you happen to find a living/active creature with cursetype +"unknown" please report that in the dfhack thread on the modding forum or +per irc. This is likely to happen with mods which introduce new types +of curses, for example.
  • +
+
-
-

Example:

-

cleanowned scattered X : This will confiscate rotten and dropped food, garbage on the floors and any worn items with 'X' damage and above.

+
+

flows

+

A tool for checking how many tiles contain flowing liquids. If you suspect that +your magma sea leaks into HFS, you can use this tool to be sure without +revealing the map.

+
+

probe

+

Can be used to determine tile properties like temperature.

-
-

colonies

-

Allows listing all the vermin colonies on the map and optionally turning them into honey bee colonies.

-
-

Options

+
+

prospect

+

Prints a big list of all the present minerals and plants. By default, only +the visible part of the map is scanned.

+

Options:

+
- + + + + +
bees:turn colonies into honey bee colonies
all:Scan the whole map, as if it was revealed.
value:Show material value in the output. Most useful for gems.
hell:Show the Z range of HFS tubes. Implies 'all'.
+
+
+

Pre-embark estimate

+

If prospect is called during the embark selection screen, it displays an estimate of +layer stone availability.

+
+

Note

+

The results of pre-embark prospect are an estimate, and can at best be expected +to be somewhere within +/- 30% of the true amount; sometimes it does a lot worse. +Especially, it is not clear how to precisely compute how many soil layers there +will be in a given embark tile, so it can report a whole extra layer, or omit one +that is actually present.

-
-
-

deramp (by zilpin)

-

Removes all ramps designated for removal from the map. This is useful for replicating the old channel digging designation. -It also removes any and all 'down ramps' that can remain after a cave-in (you don't have to designate anything for that to happen).

-
-
-

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

-
-

Confirmed working DFusion plugins:

+

Options:

+
- +
simple_embark:allows changing the number of dwarves available on embark.
all:Also estimate vein mineral amounts.
-
-

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.
  • -
-
+
-
-

drybuckets

-

This utility removes water from all buckets in your fortress, allowing them to be safely used for making lye.

+
+

reveal

+

This reveals the map. By default, HFS will remain hidden so that the demons +don't spawn. You can use 'reveal hell' to reveal everything. With hell revealed, +you won't be able to unpause until you hide the map again. If you really want +to unpause with hell revealed, use 'reveal demons'.

+

Reveal also works in adventure mode, but any of its effects are negated once +you move. When you use it this way, you don't need to run 'unreveal'.

-
-

fastdwarf

-

Makes your minions move at ludicrous speeds.

+
+

unreveal

+

Reverts the effects of 'reveal'.

+
+
+

revtoggle

+

Switches between 'reveal' and 'unreveal'.

+
+
+

revflood

+

This command will hide the whole map and then reveal all the tiles that have +a path to the in-game cursor.

+
+
+

revforget

+

When you use reveal, it saves information about what was/wasn't visible before +revealing everything. Unreveal uses this information to hide things again. +This command throws away the information. For example, use in cases where +you abandoned with the fort revealed and no longer want the data.

+
+
+

showmood

+

Shows all items needed for the currently active strange mood.

+
+
+
+

Designations

+
+

burrow

+

Miscellaneous burrow control. Allows manipulating burrows and automated burrow +expansion while digging.

+

Options:

-
    -
  • Activate with 'fastdwarf 1'
  • -
  • Deactivate with 'fastdwarf 0'
  • +
    +
    enable feature ...
    +
    Enable features of the plugin.
    +
    disable feature ...
    +
    Disable features of the plugin.
    +
    clear-unit burrow burrow ...
    +
    Remove all units from the burrows.
    +
    clear-tiles burrow burrow ...
    +
    Remove all tiles from the burrows.
    +
    set-units target-burrow src-burrow ...
    +
    Clear target, and adds units from source burrows.
    +
    add-units target-burrow src-burrow ...
    +
    Add units from the source burrows to the target.
    +
    remove-units target-burrow src-burrow ...
    +
    Remove units in source burrows from the target.
    +
    set-tiles target-burrow src-burrow ...
    +
    Clear target and adds tiles from the source burrows.
    +
    add-tiles target-burrow src-burrow ...
    +
    Add tiles from the source burrows to the target.
    +
    remove-tiles target-burrow src-burrow ...
    +

    Remove tiles in source burrows from the target.

    +

    For these three options, in place of a source burrow it is +possible to use one of the following keywords: ABOVE_GROUND, +SUBTERRANEAN, INSIDE, OUTSIDE, LIGHT, DARK, HIDDEN, REVEALED

    +
    +
    +
+

Features:

+
+ +++ + + + +
auto-grow:When a wall inside a burrow with a name ending in '+' is dug +out, the burrow is extended to newly-revealed adjacent walls. +This final '+' may be omitted in burrow name args of commands above. +Digging 1-wide corridors with the miner inside the burrow is SLOW.
+
+
+
+

digv

+

Designates a whole vein for digging. Requires an active in-game cursor placed +over a vein tile. With the 'x' option, it will traverse z-levels (putting stairs +between the same-material tiles).

+
+
+

digvx

+

A permanent alias for 'digv x'.

+
+
+

digl

+

Designates layer stone for digging. Requires an active in-game cursor placed +over a layer stone tile. With the 'x' option, it will traverse z-levels +(putting stairs between the same-material tiles). With the 'undo' option it +will remove the dig designation instead (if you realize that digging out a 50 +z-level deep layer was not such a good idea after all).

+
+
+

diglx

+

A permanent alias for 'digl x'.

+
+
+

digexp

+

This command can be used for exploratory mining.

+

See: http://df.magmawiki.com/index.php/DF2010:Exploratory_mining

+

There are two variables that can be set: pattern and filter.

+

Patterns:

+
+ +++ + + + + + + + + + + + + + +
diag5:diagonals separated by 5 tiles
diag5r:diag5 rotated 90 degrees
ladder:A 'ladder' pattern
ladderr:ladder rotated 90 degrees
clear:Just remove all dig designations
cross:A cross, exactly in the middle of the map.
+
+

Filters:

+
+ +++ + + + + + + + +
all:designate whole z-level
hidden:designate only hidden tiles of z-level (default)
designated:Take current designation and apply pattern to it.
+
+

After you have a pattern set, you can use 'expdig' to apply it again.

+

Examples:

+
+
+
designate the diagonal 5 patter over all hidden tiles:
+
    +
  • expdig diag5 hidden
  • +
+
+
apply last used pattern and filter:
+
    +
  • expdig
  • +
+
+
Take current designations and replace them with the ladder pattern:
+
    +
  • expdig ladder designated
+
+
-
-

feature

-

Enables management of map features.

+
+

digcircle

+

A command for easy designation of filled and hollow circles. +It has several types of options.

+

Shape:

+
+ +++ + + + + + + + +
hollow:Set the circle to hollow (default)
filled:Set the circle to filled
#:Diameter in tiles (default = 0, does nothing)
+
+

Action:

+
+ +++ + + + + + + + +
set:Set designation (default)
unset:Unset current designation
invert:Invert designations already present
+
+

Designation types:

+
+ +++ + + + + + + + + + + + + + +
dig:Normal digging designation (default)
ramp:Ramp digging
ustair:Staircase up
dstair:Staircase down
xstair:Staircase up/down
chan:Dig channel
+
+

After you have set the options, the command called with no options +repeats with the last selected parameters.

+

Examples:

    -
  • Discovering a magma feature (magma pool, volcano, magma sea, or curious -underground structure) permits magma workshops and furnaces to be built.
  • -
  • Discovering a cavern layer causes plants (trees, shrubs, and grass) from -that cavern to grow within your fortress.
  • +
  • 'digcircle filled 3' = Dig a filled circle with radius = 3.
  • +
  • 'digcircle' = Do it again.
- +
+

digtype

+

For every tile on the map of the same vein type as the selected tile, this command designates it to have the same designation as the selected tile. If the selected tile has no designation, they will be dig designated. +If an argument is given, the designation of the selected tile is ignored, and all appropriate tiles are set to the specified designation.

+

Options:

+
- + - + - + + + + + + + + +
list:Lists all map features in your current embark by index.
dig:
show X:Marks the selected map feature as discovered.
channel:
hide X:Marks the selected map feature as undiscovered.
ramp:
updown:up/down stairs
up:up stairs
down:down stairs
clear:clear designation
-
+
-

filltraffic

+

filltraffic

Set traffic designations using flood-fill starting at the cursor.

-
-

Traffic Type Codes:

+

Traffic Type Codes:

+
@@ -1158,9 +1556,9 @@ that cavern to grow within your fortress.
-
-
-

Other Options:

+ +

Other Options:

+
@@ -1173,17 +1571,16 @@ that cavern to grow within your fortress.
-
-
-

Example:

-

'filltraffic H' - When used in a room with doors, it will set traffic to HIGH in just that room.

-
+ +

Example:

+
+'filltraffic H' - When used in a room with doors, it will set traffic to HIGH in just that room.
-

alltraffic

+

alltraffic

Set traffic designations for every single tile of the map (useful for resetting traffic designations).

-
-

Traffic Type Codes:

+

Traffic Type Codes:

+
@@ -1198,46 +1595,18 @@ that cavern to grow within your fortress.
-
-
-

Example:

-

'alltraffic N' - Set traffic to 'normal' for all tiles.

-
-
-
-

fixdiplomats

-

Up to version 0.31.12, Elves only sent Diplomats to your fortress to propose -tree cutting quotas due to a bug; once that bug was fixed, Elves stopped caring -about excess tree cutting. This command adds a Diplomat position to all Elven -civilizations, allowing them to negotiate tree cutting quotas (and allowing you -to violate them and potentially start wars) in case you haven't already modified -your raws accordingly.

-
-
-

fixmerchants

-

This command adds the Guild Representative position to all Human civilizations, -allowing them to make trade agreements (just as they did back in 0.28.181.40d -and earlier) in case you haven't already modified your raws accordingly.

-
-
-

fixveins

-

Removes invalid references to mineral inclusions and restores missing ones. -Use this if you broke your embark with tools like tiletypes, or if you -accidentally placed a construction on top of a valuable mineral floor.

-
-
-

flows

-

A tool for checking how many tiles contain flowing liquids. If you suspect that -your magma sea leaks into HFS, you can use this tool to be sure without -revealing the map.

+ +

Example:

+
+'alltraffic N' - Set traffic to 'normal' for all tiles.
-

getplants

+

getplants

This tool allows plant gathering and tree cutting by RAW ID. Specify the types of trees to cut down and/or shrubs to gather by their plant names, separated by spaces.

-
-

Options

+

Options:

+
@@ -1253,398 +1622,154 @@ selection)
+

Specifying both -t and -s will have no effect. If no plant IDs are specified, all valid plant IDs will be listed.

-
-

tidlers

-

Toggle between all possible positions where the idlers count can be placed.

-
-
-

twaterlvl

-

Toggle between displaying/not displaying liquid depth as numbers.

-
-
-

job

-

Command for general job query and manipulation.

-
-
Options:
-
    -
  • no extra options - Print details of the current job. The job can be selected -in a workshop, or the unit/jobs screen.
  • -
  • list - Print details of all jobs in the selected workshop.
  • -
  • item-material <item-idx> <material[:subtoken]> - Replace the exact material -id in the job item.
  • -
  • item-type <item-idx> <type[:subtype]> - Replace the exact item type id in -the job item.
  • -
-
-
-
-
-

job-material

-

Alter the material of the selected job.

-

Invoked as: job-material <inorganic-token>

-
-
Intended to be used as a keybinding:
-
    -
  • In 'q' mode, when a job is highlighted within a workshop or furnace, -changes the material of the job. Only inorganic materials can be used -in this mode.
  • -
  • In 'b' mode, during selection of building components positions the cursor -over the first available choice with the matching material.
  • -
-
-
-
-
-

job-duplicate

-
-
Duplicate the selected job in a workshop:
-
    -
  • In 'q' mode, when a job is highlighted within a workshop or furnace building, -instantly duplicates the job.
  • -
-
-
-
-
-

keybinding

-

Manages DFHack keybindings.

-

Currently it supports any combination of Ctrl/Alt/Shift with F1-F9, or A-Z.

-
-

Options

+
+

Cleanup and garbage disposal

+
+

clean

+

Cleans all the splatter that get scattered all over the map, items and +creatures. In an old fortress, this can significantly reduce FPS lag. It can +also spoil your !!FUN!!, so think before you use it.

+

Options:

+
- - - - - + - - + - - +
keybinding list <key>:
 List bindings active for the key combination.
keybinding clear <key> <key>...:
 Remove bindings for the specified keys.
map:Clean the map tiles. By default, it leaves mud and snow alone.
keybinding add <key> "cmdline" "cmdline"...:
 Add bindings for the specified -key.
units:Clean the creatures. Will also clean hostiles.
keybinding set <key> "cmdline" "cmdline"...:
 Clear, and then add bindings for -the specified key.
items:Clean all the items. Even a poisoned blade.
-

When multiple commands are bound to the same key combination, DFHack selects -the first applicable one. Later 'add' commands, and earlier entries within one -'add' command have priority. Commands that are not specifically intended for use -as a hotkey are always considered applicable.

-
-
-
-

liquids

-

Allows adding magma, water and obsidian to the game. It replaces the normal -dfhack command line and can't be used from a hotkey. Settings will be remembered -as long as dfhack runs. Intended for use in combination with the command -liquids-here (which can be bound to a hotkey).

-

For more information, refer to the command's internal help.

-
-

Note

-

Spawning and deleting liquids can F up pathing data and -temperatures (creating heat traps). You've been warned.

-
-
-
-

liquids-here

-

Run the liquid spawner with the current/last settings made in liquids (if no -settings in liquids were made it paints a point of 7/7 magma by default).

-

Intended to be used as keybinding. Requires an active in-game cursor.

-
-
-

mode

-

This command lets you see and change the game mode directly. -Not all combinations are good for every situation and most of them will -produce undesirable results. There are a few good ones though.

-
-

Example

-

You are in fort game mode, managing your fortress and paused. -You switch to the arena game mode, assume control of a creature and then -switch to adventure game mode(1). -You just lost a fortress and gained an adventurer. -You could also do this. -You are in fort game mode, managing your fortress and paused at the esc menu. -You switch to the adventure game mode, then use Dfusion to assume control of a creature and then -save or retire. -You just created a returnable mountain home and gained an adventurer.

-
-

I take no responsibility of anything that happens as a result of using this tool

-
-
-

extirpate

-

A tool for getting rid of trees and shrubs. By default, it only kills -a tree/shrub under the cursor. The plants are turned into ashes instantly.

-
-

Options

+ +

Extra options for 'map':

+
- - - + - +
shrubs:affect all shrubs on the map
trees:affect all trees on the map
mud:Remove mud in addition to the normal stuff.
all:affect every plant!
snow:Also remove snow coverings.
+
+
+

spotclean

+

Works like 'clean map snow mud', but only for the tile under the cursor. Ideal +if you want to keep that bloody entrance 'clean map' would clean up.

-
-

grow

-

Makes all saplings present on the map grow into trees (almost) instantly.

-
-
-

immolate

-

Very similar to extirpate, but additionally sets the plants on fire. The fires -can and will spread ;)

-
-
-

probe

-

Can be used to determine tile properties like temperature.

-
-
-

prospect

-

Prints a big list of all the present minerals and plants. By default, only -the visible part of the map is scanned.

-
-

Options

+
+

autodump

+

This utility lets you quickly move all items designated to be dumped. +Items are instantly moved to the cursor position, the dump flag is unset, +and the forbid flag is set, as if it had been dumped normally. +Be aware that any active dump item tasks still point at the item.

+

Cursor must be placed on a floor tile so the items can be dumped there.

+

Options:

+
- + - + - + - -
all:Scan the whole map, as if it was revealed.
destroy:Destroy instead of dumping. Doesn't require a cursor.
value:Show material value in the output. Most useful for gems.
destroy-here:Destroy items only under the cursor.
hell:Show the Z range of HFS tubes. Implies 'all'.
visible:Only process items that are not hidden.
-
-
-

Pre-embark estimate

-

If called during the embark selection screen, displays an estimate of layer -stone availability. If the 'all' option is specified, also estimates veins. -The estimate is computed either for 1 embark tile of the blinking biome, or -for all tiles of the embark rectangle.

-
-
-

Options

- --- - + + +
all:processes all tiles, even hidden ones.
hidden:Only process hidden items.
forbidden:Only process forbidden items (default: only unforbidden).
+
+
+

autodump-destroy-here

+

Destroy items marked for dumping under cursor. Identical to autodump +destroy-here, but intended for use as keybinding.

-
-

regrass

-

Regrows grass. Not much to it ;)

+
+

autodump-destroy-item

+

Destroy the selected item. The item may be selected in the 'k' list, or inside +a container. If called again before the game is resumed, cancels destroy.

-
-

rename

-

Allows renaming various things.

-
-

Options

+
+

cleanowned

+

Confiscates items owned by dwarfs. By default, owned food on the floor +and rotten items are confistacted and dumped.

+

Options:

+
- - - - - - - - + - - + - - + - -
rename squad <index> "name":
 Rename squad by index to 'name'.
rename hotkey <index> "name":
 Rename hotkey by index. This allows assigning -longer commands to the DF hotkeys.
rename unit "nickname":
 Rename a unit/creature highlighted in the DF user -interface.
all:confiscate all owned items
rename unit-profession "custom profession":
 Change proffession name of the -highlighted unit/creature.
scattered:confiscated and dump all items scattered on the floor
rename building "name":
 Set a custom name for the selected building. -The building must be one of stockpile, workshop, furnace, trap or siege engine.
x:confiscate/dump items with wear level 'x' and more
-
-
-
-

reveal

-

This reveals the map. By default, HFS will remain hidden so that the demons -don't spawn. You can use 'reveal hell' to reveal everything. With hell revealed, -you won't be able to unpause until you hide the map again. If you really want -to unpause with hell revealed, use 'reveal demons'.

-

Reveal also works in adventure mode, but any of its effects are negated once -you move. When you use it this way, you don't need to run 'unreveal'.

-
-
-

unreveal

-

Reverts the effects of 'reveal'.

-
-
-

revtoggle

-

Switches between 'reveal' and 'unreveal'.

-
-
-

revflood

-

This command will hide the whole map and then reveal all the tiles that have -a path to the in-game cursor.

-
-
-

revforget

-

When you use reveal, it saves information about what was/wasn't visible before -revealing everything. Unreveal uses this information to hide things again. -This command throws away the information. For example, use in cases where -you abandoned with the fort revealed and no longer want the data.

-
-
-

lair

-

This command allows you to mark the map as 'monster lair', preventing item -scatter on abandon. When invoked as 'lair reset', it does the opposite.

-

Unlike reveal, this command doesn't save the information about tiles - you -won't be able to restore state of real monster lairs using 'lair reset'.

-
-

Options

- --- - + - +
lair:Mark the map as monster lair
X:confiscate/dump items with wear level 'X' and more
lair reset:Mark the map as ordinary (not lair)
dryrun:a dry run. combine with other options to see what will happen +without it actually happening.
-
-
-
-

seedwatch

-

Tool for turning cooking of seeds and plants on/off depending on how much you -have of them.

-

See 'seedwatch help' for detailed description.

-
-
-

showmood

-

Shows all items needed for the currently active strange mood.

-
-
-

copystock

-

Copies the parameters of the currently highlighted stockpile to the custom -stockpile settings and switches to custom stockpile placement mode, effectively -allowing you to copy/paste stockpiles easily.

-
-
-

ssense / stonesense

-

An isometric visualizer that runs in a second window. This requires working -graphics acceleration and at least a dual core CPU (otherwise it will slow -down DF).

-

All the data resides in the 'stonesense' directory. For detailed instructions, -see stonesense/README.txt

-

Compatible with Windows > XP SP3 and most modern Linux distributions.

-

Older versions, support and extra graphics can be found in the bay12 forum -thread: http://www.bay12forums.com/smf/index.php?topic=43260.0

-

Some additional resources: -http://df.magmawiki.com/index.php/Utility:Stonesense/Content_repository

-
-
-

tiletypes

-

Can be used for painting map tiles and is an interactive command, much like -liquids.

-

The tool works with two set of options and a brush. The brush determines which -tiles will be processed. First set of options is the filter, which can exclude -some of the tiles from the brush by looking at the tile properties. The second -set of options is the paint - this determines how the selected tiles are -changed.

-

Both paint and filter can have many different properties including things like -general shape (WALL, FLOOR, etc.), general material (SOIL, STONE, MINERAL, -etc.), state of 'designated', 'hidden' and 'light' flags.

-

The properties of filter and paint can be partially defined. This means that -you can for example do something like this:

-
-filter material STONE
-filter shape FORTIFICATION
-paint shape FLOOR
-
-

This will turn all stone fortifications into floors, preserving the material.

-

Or this:

-
-filter shape FLOOR
-filter material MINERAL
-paint shape WALL
-
-

Turning mineral vein floors back into walls.

-

The tool also allows tweaking some tile flags:

-

Or this:

-
-paint hidden 1
-paint hidden 0
-
-

This will hide previously revealed tiles (or show hidden with the 0 option).

-

Any paint or filter option (or the entire paint or filter) can be disabled entirely by using the ANY keyword:

-
-paint hidden ANY
-paint shape ANY
-filter material any
-filter shape any
-filter any
-
+ +

Example:

+
-
You can use several different brushes for painting tiles:
-
    -
  • Point. (point)
  • -
  • Rectangular range. (range)
  • -
  • A column ranging from current cursor to the first solid tile above. (column)
  • -
  • DF map block - 16x16 tiles, in a regular grid. (block)
  • -
-
+
cleanowned scattered X
+
This will confiscate rotten and dropped food, garbage on the floors and any +worn items with 'X' damage and above.
-

Example:

-
-range 10 10 1
-
-

This will change the brush to a rectangle spanning 10x10 tiles on one z-level. -The range starts at the position of the cursor and goes to the east, south and -up.

-

For more details, see the 'help' command while using this.

+
-
-

tiletypes-commands

-

Runs tiletypes commands, separated by ;. This makes it possible to change -tiletypes modes from a hotkey.

-
-

tiletypes-here

-

Apply the current tiletypes options at the in-game cursor position, including -the brush. Can be used from a hotkey.

+
+

Bugfixes

+
+

drybuckets

+

This utility removes water from all buckets in your fortress, allowing them to be safely used for making lye.

-
-

tiletypes-here-point

-

Apply the current tiletypes options at the in-game cursor position to a single -tile. Can be used from a hotkey.

+
+

fixdiplomats

+

Up to version 0.31.12, Elves only sent Diplomats to your fortress to propose +tree cutting quotas due to a bug; once that bug was fixed, Elves stopped caring +about excess tree cutting. This command adds a Diplomat position to all Elven +civilizations, allowing them to negotiate tree cutting quotas (and allowing you +to violate them and potentially start wars) in case you haven't already modified +your raws accordingly.

+
+
+

fixmerchants

+

This command adds the Guild Representative position to all Human civilizations, +allowing them to make trade agreements (just as they did back in 0.28.181.40d +and earlier) in case you haven't already modified your raws accordingly.

+
+
+

fixveins

+

Removes invalid references to mineral inclusions and restores missing ones. +Use this if you broke your embark with tools like tiletypes, or if you +accidentally placed a construction on top of a valuable mineral floor.

-

tweak

-

Contains various tweaks for minor bugs (currently just one).

-
-

Options

+

tweak

+

Contains various tweaks for minor bugs.

+

One-shot subcommands:

@@ -1666,230 +1791,243 @@ Intended to fix bugged migrants/traders who stay at the map edge and don't enter your fort. Only works for dwarves (or generally the player's race in modded games). Do NOT abuse this for 'real' caravan merchants (if you -really want to kidnap them, use 'tweak makeown' instead, -otherwise they will have their clothes set to forbidden etc). - - - - - - - - - - - - - - - - - - - - -
makeown:Force selected unit to become a member of your fort. -Can be abused to grab caravan merchants and escorts, even if -they don't belong to the player's race. Foreign sentients -(humans, elves) can be put to work, but you can't assign rooms -to them and they don't show up in DwarfTherapist because the -game treats them like pets. Grabbing draft animals from -a caravan can result in weirdness (animals go insane or berserk -and are not flagged as tame), but you are allowed to mark them -for slaughter. Grabbing wagons results in some funny spam, then -they are scuttled.
stable-cursor:Saves the exact cursor position between t/q/k/d/etc menus of dwarfmode.
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.
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 -of item temperature is crossed in no more than specified number of frames -when updating from the environment temperature. This reduces the time it -takes for stable-temp to stop updates again when equilibrium is disturbed.
fix-dimensions:Fixes subtracting small amount of thread/cloth/liquid from a stack -by splitting the stack and subtracting from the remaining single item. -This is a necessary addition to the binary patch in bug 808.
advmode-contained:
 Works around bug 6202, i.e. custom reactions with container inputs -in advmode. The issue is that the screen tries to force you to select -the contents separately from the container. This forcefully skips child -reagents.
-
-
-
-

tubefill

-

Fills all the adamantine veins again. Veins that were empty will be filled in -too, but might still trigger a demon invasion (this is a known bug).

-
-
-

digv

-

Designates a whole vein for digging. Requires an active in-game cursor placed -over a vein tile. With the 'x' option, it will traverse z-levels (putting stairs -between the same-material tiles).

-
-
-

digvx

-

A permanent alias for 'digv x'.

-
-
-

digl

-

Designates layer stone for digging. Requires an active in-game cursor placed -over a layer stone tile. With the 'x' option, it will traverse z-levels -(putting stairs between the same-material tiles). With the 'undo' option it -will remove the dig designation instead (if you realize that digging out a 50 -z-level deep layer was not such a good idea after all).

-
-
-

diglx

-

A permanent alias for 'digl x'.

-
-
-

digexp

-

This command can be used for exploratory mining.

-

See: http://df.magmawiki.com/index.php/DF2010:Exploratory_mining

-

There are two variables that can be set: pattern and filter.

-
-

Patterns:

- --- - - - - - - - - - - - - - -
diag5:diagonals separated by 5 tiles
diag5r:diag5 rotated 90 degrees
ladder:A 'ladder' pattern
ladderr:ladder rotated 90 degrees
clear:Just remove all dig designations
cross:A cross, exactly in the middle of the map.
-
-
-

Filters:

- --- - - - - - - - -
all:designate whole z-level
hidden:designate only hidden tiles of z-level (default)
designated:Take current designation and apply pattern to it.
-

After you have a pattern set, you can use 'expdig' to apply it again.

-
-
-

Examples:

-
-
designate the diagonal 5 patter over all hidden tiles:
-
    -
  • expdig diag5 hidden
  • -
-
-
apply last used pattern and filter:
-
    -
  • expdig
  • -
-
-
Take current designations and replace them with the ladder pattern:
-
    -
  • expdig ladder designated
  • -
-
-
-
-
-
-

digcircle

-

A command for easy designation of filled and hollow circles. -It has several types of options.

-
-

Shape:

- --- - - - +really want to kidnap them, use 'tweak makeown' instead, +otherwise they will have their clothes set to forbidden etc). - +
hollow:Set the circle to hollow (default)
filled:Set the circle to filled
#:Diameter in tiles (default = 0, does nothing)
makeown:Force selected unit to become a member of your fort. +Can be abused to grab caravan merchants and escorts, even if +they don't belong to the player's race. Foreign sentients +(humans, elves) can be put to work, but you can't assign rooms +to them and they don't show up in DwarfTherapist because the +game treats them like pets. Grabbing draft animals from +a caravan can result in weirdness (animals go insane or berserk +and are not flagged as tame), but you are allowed to mark them +for slaughter. Grabbing wagons results in some funny spam, then +they are scuttled.
-
-
-

Action:

+

Subcommands that persist until disabled or DF quit:

- + - + - + + - -
set:Set designation (default)
stable-cursor:Saves the exact cursor position between t/q/k/d/etc menus of dwarfmode.
unset:Unset current designation
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).
invert:Invert designations already present
readable-build-plate:
 Fixes rendering of creature weight limits in pressure plate build menu.
-
-
-

Designation types:

- --- - + - + - + - + + - + - + + + + +
dig:Normal digging designation (default)
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%
ramp:Ramp digging
fast-heat:Further improves temperature update performance by ensuring that 1 degree +of item temperature is crossed in no more than specified number of frames +when updating from the environment temperature. This reduces the time it +takes for stable-temp to stop updates again when equilibrium is disturbed.
ustair:Staircase up
fix-dimensions:Fixes subtracting small amount of thread/cloth/liquid from a stack +by splitting the stack and subtracting from the remaining single item. +This is a necessary addition to the binary patch in bug 808.
dstair:Staircase down
advmode-contained:
 Works around bug 6202, i.e. custom reactions with container inputs +in advmode. The issue is that the screen tries to force you to select +the contents separately from the container. This forcefully skips child +reagents.
xstair:Staircase up/down
fast-trade:Makes Shift-Enter in the Move Goods to Depot and Trade screens select +the current item (fully, in case of a stack), and scroll down one line.
chan:Dig channel
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 +to make them stand out more in the list.
-

After you have set the options, the command called with no options -repeats with the last selected parameters.

-
-

Examples:

+
+

fix-armory

+

Enables a fix for storage of squad equipment in barracks.

+

Specifically, it prevents your haulers from moving that 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 check out +http://www.bay12games.com/dwarves/mantisbt/view.php?id=1445 +for a patch addressing that too.

+
+

Note that the buildings in the armory are used as follows:

    -
  • 'digcircle filled 3' = Dig a filled circle with radius = 3.
  • -
  • 'digcircle' = Do it again.
  • +
  • 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.

-
-

weather

-

Prints the current weather map by default.

-

Also lets you change the current weather to 'clear sky', 'rainy' or 'snowing'.

-
-

Options:

+
+

Mode switch and reclaim

+
+

lair

+

This command allows you to mark the map as 'monster lair', preventing item +scatter on abandon. When invoked as 'lair reset', it does the opposite.

+

Unlike reveal, this command doesn't save the information about tiles - you +won't be able to restore state of real monster lairs using 'lair reset'.

+

Options:

+
- - - + - +
snow:make it snow everywhere.
rain:make it rain.
lair:Mark the map as monster lair
clear:clear the sky.
lair reset:Mark the map as ordinary (not lair)
+
+
+
+

mode

+

This command lets you see and change the game mode directly. +Not all combinations are good for every situation and most of them will +produce undesirable results. There are a few good ones though.

+
+

Example

+

You are in fort game mode, managing your fortress and paused. +You switch to the arena game mode, assume control of a creature and then +switch to adventure game mode(1). +You just lost a fortress and gained an adventurer. +You could also do this. +You are in fort game mode, managing your fortress and paused at the esc menu. +You switch to the adventure game mode, then use Dfusion to assume control of a creature and then +save or retire. +You just created a returnable mountain home and gained an adventurer.

+
+

I take no responsibility of anything that happens as a result of using this tool

+
+
+
+

Visualizer and data export

+
+

ssense / stonesense

+

An isometric visualizer that runs in a second window. This requires working +graphics acceleration and at least a dual core CPU (otherwise it will slow +down DF).

+

All the data resides in the 'stonesense' directory. For detailed instructions, +see stonesense/README.txt

+

Compatible with Windows > XP SP3 and most modern Linux distributions.

+

Older versions, support and extra graphics can be found in the bay12 forum +thread: http://www.bay12forums.com/smf/index.php?topic=43260.0

+

Some additional resources: +http://df.magmawiki.com/index.php/Utility:Stonesense/Content_repository

+
+
+

mapexport

+

Export the current loaded map as a file. This will be eventually usable +with visualizers.

+
+
+

dwarfexport

+

Export dwarves to RuneSmith-compatible XML.

+
+
+
+

Job management

+
+

job

+

Command for general job query and manipulation.

+
+
Options:
+
+
no extra options
+
Print details of the current job. The job can be selected +in a workshop, or the unit/jobs screen.
+
list
+
Print details of all jobs in the selected workshop.
+
item-material <item-idx> <material[:subtoken]>
+
Replace the exact material id in the job item.
+
item-type <item-idx> <type[:subtype]>
+
Replace the exact item type id in the job item.
+
+
+
+
+
+

job-material

+

Alter the material of the selected job.

+

Invoked as:

+
+job-material <inorganic-token>
+
+

Intended to be used as a keybinding:

+
+
    +
  • In 'q' mode, when a job is highlighted within a workshop or furnace, +changes the material of the job. Only inorganic materials can be used +in this mode.
  • +
  • In 'b' mode, during selection of building components positions the cursor +over the first available choice with the matching material.
  • +
+
+
+

job-duplicate

+
+
Duplicate the selected job in a workshop:
+
    +
  • In 'q' mode, when a job is highlighted within a workshop or furnace building, +instantly duplicates the job.
  • +
+
+
-

workflow

+

workflow

Manage control of repeat jobs.

-
-

Usage

+

Usage:

+
workflow enable [option...], workflow disable [option...]

If no options are specified, enables or disables the plugin. @@ -1908,9 +2046,9 @@ Otherwise, enables or disables any of the following options:

workflow unlimit <constraint-spec>
Delete a constraint.
-
+
-

Function

+

Function

When the plugin is enabled, it protects all repeat jobs from removal. If they do disappear due to any cause, they are immediately re-added to their workshop and suspended.

@@ -1919,9 +2057,11 @@ produce that kind of item are automatically suspended and resumed as the item 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 examples

+

Constraint examples

Keep metal bolts within 900-1000, and wood/bone within 150-200.

 workflow amount AMMO:ITEM_AMMO_BOLTS/METAL 1000 100
@@ -1958,20 +2098,20 @@ command.
 
-
-

mapexport

-

Export the current loaded map as a file. This will be eventually usable -with visualizers.

-
-

dwarfexport

-

Export dwarves to RuneSmith-compatible XML.

+
+

Fortress activity management

+
+

seedwatch

+

Tool for turning cooking of seeds and plants on/off depending on how much you +have of them.

+

See 'seedwatch help' for detailed description.

-

zone

+

zone

Helps a bit with managing activity zones (pens, pastures and pits) and cages.

-
-

Options:

+

Options:

+
@@ -2008,9 +2148,9 @@ the cursor are listed.
-
-
-

Filters:

+ +

Filters:

+
@@ -2065,9 +2205,9 @@ for war/hunt). Negatable.
-
+
-

Usage with single units

+

Usage with single units

One convenient way to use the zone tool is to bind the command 'zone assign' to a hotkey, maybe also the command 'zone set'. Place the in-game cursor over a pen/pasture or pit, use 'zone set' to mark it. Then you can select units @@ -2076,7 +2216,7 @@ and use 'zone assign' to assign them to their new home. Allows pitting your own dwarves, by the way.

-

Usage with filters

+

Usage with filters

All filters can be used together with the 'assign' command.

Restrictions: It's not possible to assign units who are inside built cages or chained because in most cases that won't be desirable anyways. @@ -2094,14 +2234,14 @@ are not properly added to your own stocks; slaughtering them should work).

Most filters can be negated (e.g. 'not grazer' -> race is not a grazer).

-

Mass-renaming

+

Mass-renaming

Using the 'nick' command you can set the same nickname for multiple units. If used without 'assign', 'all' or 'count' it will rename all units in the current default target zone. Combined with 'assign', 'all' or 'count' (and further optional filters) it will rename units matching the filter conditions.

-

Cage zones

+

Cage zones

Using the 'tocages' command you can assign units to a set of cages, for example a room next to your butcher shop(s). They will be spread evenly among available cages to optimize hauling to and butchering from them. For this to work you need @@ -2111,8 +2251,8 @@ all cages you want to use. Then use 'zone set' (like with 'assign') and use would make no sense, but can be used together with 'nick' or 'remnick' and all the usual filters.

-
-

Examples

+
+

Examples

zone assign all own ALPACA minage 3 maxage 10
Assign all own alpacas who are between 3 and 10 years old to the selected @@ -2138,7 +2278,7 @@ on the current default zone.
-

autonestbox

+

autonestbox

Assigns unpastured female egg-layers to nestbox zones. Requires that you create pen/pasture zones above nestboxes. If the pen is bigger than 1x1 the nestbox must be in the top left corner. Only 1 unit will be assigned per pen, regardless @@ -2148,8 +2288,8 @@ to a 1x1 pasture is not a good idea. Only tame and domesticated own units are processed since pasturing half-trained wild egglayers could destroy your neat nestbox zones when they revert to wild. When called without options autonestbox will instantly run once.

-
-

Options:

+

Options:

+
@@ -2164,10 +2304,10 @@ frames between runs.
-
+
-

autobutcher

+

autobutcher

Assigns lifestock for slaughter once it reaches a specific count. Requires that you add the target race(s) to a watch list. Only tame units will be processed.

Named units will be completely ignored (to protect specific animals from @@ -2180,8 +2320,8 @@ units or with 'zone nick' to mass-rename units in pastures and cages).

Once you have too much kids, the youngest will be butchered first. If you don't set any target count the following default will be used: 1 male kid, 5 female kids, 1 male adult, 5 female adults.

-
-

Options:

+

Options:

+
@@ -2232,9 +2372,8 @@ for future entries.
-
-
-

Examples:

+ +

Examples:

You want to keep max 7 kids (4 female, 3 male) and max 3 adults (2 female, 1 male) of the race alpaca. Once the kids grow up the oldest adults will get slaughtered. Excess kids will get slaughtered starting with the youngest @@ -2263,9 +2402,7 @@ add some new races with 'watch'. If you simply want to stop it completely use

 autobutcher unwatch ALPACA CAT
 
-
-
-

Note:

+

Note:

Settings and watchlist are stored in the savegame, so that you can have different settings for each world. If you want to copy your watchlist to another savegame you can use the command list_export:

@@ -2277,9 +2414,8 @@ Load the savegame where you want to copy the settings to, run the batch file (fr autobutcher.bat
-
-

autolabor

+

autolabor

Automatically manage dwarf labors.

When enabled, autolabor periodically checks your dwarves and enables or disables labors. It tries to keep as many dwarves as possible busy but @@ -2291,8 +2427,120 @@ while it is enabled.

For detailed usage information, see 'help autolabor'.

+
+
+

Other

+
+

catsplosion

+

Makes cats just multiply. It is not a good idea to run this more than once or +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

+

Confirmed working DFusion plugins:

+ +++ + + + +
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.
  • +
+
+
+
+

misery

+

When enabled, every new negative dwarven thought will be multiplied by a factor (2 by default).

+

Usage:

+ +++ + + + + + + + + + + + + +
misery enable n:
 enable misery with optional magnitude n. If specified, n must be positive.
misery n:same as "misery enable n"
misery enable:same as "misery enable 2"
misery disable:stop adding new negative thoughts. This will not remove existing duplicated thoughts. Equivalent to "misery 1"
misery clear:remove fake thoughts added in this session of DF. Saving makes them permanent! Does not change factor.
+
+
+
+
+

Scripts

+

Lua or ruby scripts placed in the hack/scripts/ directory are considered for +execution as if they were native DFHack commands. They are listed at the end +of the 'ls' command output.

+

Note: scripts in subdirectories of hack/scripts/ can still be called, but will +only be listed by ls if called as 'ls -a'. This is intended as a way to hide +scripts that are obscure, developer-oriented, or should be used as keybindings.

+

Some notable scripts:

+
+

fix/*

+

Scripts in this subdirectory fix various bugs and issues, some of them obscure.

+
    +
  • fix/dead-units

    +

    Removes uninteresting dead units from the unit list. Doesn't seem to give any +noticeable performance gain, but migrants normally stop if the unit list grows +to around 3000 units, and this script reduces it back.

    +
  • +
  • fix/population-cap

    +

    Run this after every migrant wave to ensure your population cap is not exceeded. +The issue with the cap is that it is compared to the population number reported +by the last caravan, so once it drops below the cap, migrants continue to come +until that number is updated again.

    +
  • +
  • fix/stable-temp

    +

    Instantly sets the temperature of all free-lying items to be in equilibrium with +the environment and stops temperature updates. In order to maintain this efficient +state however, use tweak stable-temp and tweak fast-heat.

    +
  • +
  • fix/item-occupancy

    +

    Diagnoses and fixes issues with nonexistant 'items occupying site', usually +caused by autodump bugs or other hacking mishaps.

    +
  • +
+
+
+

gui/*

+

Scripts that implement dialogs inserted into the main game window are put in this +directory.

+
+
+

quicksave

+

If called in dwarf mode, makes DF immediately auto-save the game by setting a flag +normally used in seasonal auto-save.

+
+
+

setfps

+

Run setfps <number> to set the FPS cap at runtime, in case you want to watch +combat in slow motion or something :)

+
+
+

siren

+

Wakes up sleeping units, cancels breaks and stops parties either everywhere, +or in the burrows given as arguments. In return, adds bad thoughts about +noise, tiredness and lack of protection. Also, the units with interrupted +breaks will go on break again a lot sooner. The script is intended for +emergencies, e.g. when a siege appears, and all your military is partying.

+
-

growcrops

+

growcrops

Instantly grow seeds inside farming plots.

With no argument, this command list the various seed types currently in use in your farming plots. @@ -2304,23 +2552,24 @@ growcrops plump 40

-

removebadthoughts

+

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.

+but in the short term your dwarves will get much more joyful.

+

Internals: the thoughts are set to be very old, so that the game remove them +quickly after you unpause.

-

slayrace

+

slayrace

Kills any unit of a given race.

With no argument, lists the available races.

-

With the special argument 'him', targets only the selected creature.

+

With the special argument him, targets only the selected creature.

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.

@@ -2343,7 +2592,7 @@ slayrace elve magma
-

magmasource

+

magmasource

Create an infinite magma source on a tile.

This script registers a map tile as a magma source, and every 12 game ticks that tile receives 1 new unit of flowing magma.

@@ -2357,13 +2606,57 @@ magmasource here To remove all placed sources, call magmasource stop.

With no argument, this command shows an help message and list existing sources.

+
+

digfort

+

A script to designate an area for digging according to a plan in csv format.

+

This script, inspired from quickfort, can designate an area for digging. +Your plan should be stored in a .csv file like this:

+
+# this is a comment
+d;d;u;d;d;skip this tile;d
+d;d;d;i
+
+

Available tile shapes are named after the 'dig' menu shortcuts: +d for dig, u for upstairs, d downstairs, i updown, +h channel, r upward ramp, x remove designation. +Unrecognized characters are ignored (eg the 'skip this tile' in the sample).

+

Empty lines and data after a # are ignored as comments. +To skip a row in your design, use a single ;.

+

The script takes the plan filename, starting from the root df folder.

+
+
+

superdwarf

+

Similar to fastdwarf, per-creature.

+

To make any creature superfast, target it ingame using 'v' and:

+
+superdwarf add
+
+

Other options available: del, clear, list.

+

This plugin also shortens the 'sleeping' and 'on break' periods of targets.

+
+
+

drainaquifer

+

Remove all 'aquifer' tag from the map blocks. Irreversible.

+
+
+

deathcause

+

Focus a body part ingame, and this script will display the cause of death of +the creature.

+
-

In-game interface tools

+

In-game interface tools

These tools work by displaying dialogs or overlays in the game window, and are mostly implemented by lua scripts.

+
+

Note

+

In order to avoid user confusion, as a matter of policy all these tools +display the word "DFHack" on the screen somewhere while active.

+

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.

+
-

Dwarf Manipulator

+

Dwarf Manipulator

Implemented by the manipulator plugin. To activate, open the unit screen and press 'l'.

This tool implements a Dwarf Therapist-like interface within the game UI. The @@ -2375,12 +2668,13 @@ denote skills not controlled by labors.

Use the arrow keys or number pad to move the cursor around, holding Shift to move 10 tiles at a time.

Press the Z-Up (<) and Z-Down (>) keys to move quickly between labor/skill -categories.

+categories. The numpad Z-Up and Z-Down keys seek to the first or last unit +in the list. Backspace seeks to the top left corner.

Press Enter to toggle the selected labor for the selected unit, or Shift+Enter to toggle all labors within the selected category.

Press the +- keys to sort the unit list according to the currently selected skill/labor, and press the */ keys to sort the unit list by Name, Profession, -or Happiness (using Tab to select which sort method to use here).

+Happiness, or Arrival order (using Tab to select which sort method to use here).

With a unit selected, you can press the "v" key to view its properties (and possibly set a custom nickname or profession) or the "c" key to exit Manipulator and zoom to its position within your fortress.

@@ -2394,62 +2688,169 @@ cursor onto that cell instead of toggling it.
  • Left-click on a unit's name or profession to view its properties.
  • Right-click on a unit's name or profession to zoom to it.
  • +

    Pressing ESC normally returns to the unit screen, but Shift-ESC would exit +directly to the main dwarf mode screen.

    -
    -

    Liquids

    -

    Implemented by the gui/liquids script. To use, bind to a key and activate in the 'k' mode.

    +
    +

    gui/liquids

    +

    To use, bind to a key and activate in the 'k' mode.

    While active, use the suggested keys to switch the usual liquids parameters, and Enter to select the target area and apply changes.

    -
    -

    Mechanisms

    -

    Implemented by the gui/mechanims script. To use, bind to a key and activate in the 'q' mode.

    +
    +

    gui/mechanisms

    +

    To use, bind to a key and activate in the 'q' mode.

    Lists mechanisms connected to the building, and their links. Navigating the list centers the view on the relevant linked buildings.

    To exit, press ESC or Enter; ESC recenters on the original building, while Enter leaves focus on the current one. Shift-Enter has an effect equivalent to pressing Enter, and then re-entering the mechanisms ui.

    -
    -

    Power Meter

    -

    Front-end to the power-meter plugin implemented by the gui/power-meter script. Bind to a -key and activate after selecting Pressure Plate in the build menu.

    -

    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.

    -
    -
    -

    Rename

    -

    Backed by the rename plugin, the gui/rename script allows entering the desired name +

    +

    gui/rename

    +

    Backed by the rename plugin, this script allows entering the desired name via a simple dialog in the game ui.

    • gui/rename [building] in 'q' mode changes the name of a building.

      -

      The selected building must be one of stockpile, workshop, furnace, trap or siege engine.

      +

      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.

    • gui/rename unit-profession changes the selected unit's custom profession name.

    -

    The building or unit are automatically assumed when in relevant ui state.

    +

    The building or unit options are automatically assumed when in relevant ui state.

    -
    -

    Room List

    -

    Implemented by the gui/room-list script. To use, bind to a key and activate in the 'q' mode, -either immediately or after opening the assign owner page.

    +
    +

    gui/room-list

    +

    To use, bind to a key and activate in the 'q' mode, either immediately or after opening +the assign owner page.

    The script lists other rooms owned by the same owner, or by the unit selected in the assign list, and allows unassigning them.

    +
    +

    gui/choose-weapons

    +

    Bind to a key, 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 +unit's top skill. If the cursor is in the rightmost list over a weapon entry, it rewrites +only that entry, and does it even if it is not 'individual choice'.

    +

    Rationale: individual choice seems to be unreliable when there is a weapon shortage, +and may lead to inappropriate weapons being selected.

    +
    +
    +

    gui/guide-path

    +

    Bind to a key, and activate in the Hauling menu with the cursor over a Guide order.

    +

    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, and activate with a job selected in a workshop in the 'q' mode.

    +

    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. Pressing 'm', unless the item type does not allow a material, +lets you choose a material.

    +
    +

    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, and activate with a job selected in a workshop in the 'q' mode.

    +

    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 'c' switches the current constraint between counting stacks or items. +Pressing 'm' lets you input the range directly; 'e', 'r', 'd', 'f' adjust the +bounds by 1, 5, or 25 depending on the direction and the 'c' setting (counting +items and expanding the range each gives a 5x bonus).

    +

    Pressing 'n' produces a list of possible outputs of this job as guessed by +workflow, and lets you create a new constraint by just choosing one. 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.

    +
    +
    +

    gui/assign-rack

    +

    Bind to a key, and activate when viewing a weapon rack in the 'q' mode.

    +

    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. +Check the comments for this bug to get it: +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.

    +
    +
    +
    +

    Behavior Mods

    +

    These plugins, when activated via configuration UI or by detecting certain +structures in RAWs, modify the game engine behavior concerning the target +objects to add features not otherwise present.

    +
    +

    DISCLAIMER

    +

    The plugins in this section have mostly been created for fun as an interesting +technical challenge, and do not represent any long-term plans to produce more +similar modifications of the game.

    +
    -

    Siege Engine

    -

    Front-end to the siege-engine plugin implemented by the gui/siege-engine script. Bind to a -key and activate after selecting a siege engine in 'q' mode.

    +

    Siege Engine

    +

    The siege-engine plugin enables siege engines to be linked to stockpiles, and +aimed at an arbitrary rectangular area across Z levels, instead of the original +four directions. Also, catapults can be ordered to load arbitrary objects, not +just stones.

    +
    +

    Rationale

    +

    Siege engines are a very interesting feature, but sadly almost useless in the current state +because they haven't been updated since 2D and can only aim in four directions. This is an +attempt to bring them more up to date until Toady has time to work on it. Actual improvements, +e.g. like making siegers bring their own, are something only Toady can do.

    +
    +
    +

    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.

    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 hit by the selected engine: green for fully reachable, blue for out of range, red for blocked, yellow for partially blocked.

    Pressing 'r' changes into the target selection mode, which works by highlighting two points with Enter like all designations. When a target area is set, the engine projectiles are -aimed at that area, or units within it, instead of the vanilla four directions.

    +aimed at that area, or units within it (this doesn't actually change the original aiming +code, instead the projectile trajectory parameters are rewritten as soon as it appears).

    After setting the target in this way for one engine, you can 'paste' the same area into others just by pressing 'p' in the main page of this script. The area to paste is kept until you quit DF, or select another area manually.

    @@ -2457,21 +2858,23 @@ DF, or select another area manually.

    Exiting from the siege engine script via ESC reverts the view to the state prior to starting the script. Shift-ESC retains the current viewport, and also exits from the 'q' mode to main menu.

    -

    DISCLAIMER: Siege engines are a very interesting feature, but currently nearly useless -because they haven't been updated since 2D and can only aim in four directions. This is an -attempt to bring them more up to date until Toady has time to work on it. Actual improvements, -e.g. like making siegers bring their own, are something only Toady can do.

    -
    -

    RAW hacks

    -

    These plugins detect certain structures in RAWs, and enhance them in various ways.

    +
    +

    Power Meter

    +

    The power-meter plugin implements a modified pressure plate that detects power being +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.

    +

    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.

    +
    -

    Steam Engine

    +

    Steam Engine

    The steam-engine plugin detects custom workshops with STEAM_ENGINE in their token, and turns them into real steam engines.

    -
    -

    Rationale

    +
    +

    Rationale

    The vanilla game contains only water wheels and windmills as sources of power, but windmills give relatively little power, and water wheels require flowing water, which must either be a real river and thus immovable and @@ -2482,31 +2885,43 @@ it can be done just by combining existing features of the game engine in a new way with some glue code and a bit of custom logic.

    -

    Construction

    +

    Construction

    The workshop needs water as its input, which it takes via a passable floor tile below it, like usual magma workshops do. The magma version also needs magma.

    -

    ISSUE: Since this building is a machine, and machine collapse -code cannot be modified, it would collapse over true open space. +

    +

    ISSUE

    +

    Since this building is a machine, and machine collapse +code cannot be hooked, it would collapse over true open space. As a loophole, down stair provides support to machines, while being passable, so use them.

    +

    After constructing the building itself, machines can be connected to the edge tiles that look like gear boxes. Their exact position is extracted from the workshop raws.

    -

    ISSUE: Like with collapse above, part of the code involved in -machine connection cannot be modified. As a result, the workshop +

    +

    ISSUE

    +

    Like with collapse above, part of the code involved in +machine connection cannot be hooked. As a result, the workshop can only immediately connect to machine components built AFTER it. This also means that engines cannot be chained without intermediate -short axles that can be built later.

    +short axles that can be built later than both of the engines.

    +
    -

    Operation

    +

    Operation

    In order to operate the engine, queue the Stoke Boiler job (optionally on repeat). A furnace operator will come, possibly bringing a bar of fuel, and perform it. As a result, a "boiling water" item will appear in the 't' view of the workshop.

    -

    NOTE: The completion of the job will actually consume one unit -of the appropriate liquids from below the workshop.

    +
    +

    Note

    +

    The completion of the job will actually consume one unit +of the appropriate liquids from below the workshop. This means +that you cannot just raise 7 units of magma with a piston and +have infinite power. However, liquid consumption should be slow +enough that water can be supplied by a pond zone bucket chain.

    +

    Every such item gives 100 power, up to a limit of 300 for coal, and 500 for a magma engine. The building can host twice that amount of items to provide longer autonomous running. When the @@ -2525,7 +2940,7 @@ decrease it by further 4%, and also decrease the whole steam use rate by 10%.

    -

    Explosions

    +

    Explosions

    The engine must be constructed using barrel, pipe and piston from fire-safe, or in the magma version magma-safe metals.

    During operation weak parts get gradually worn out, and @@ -2534,7 +2949,7 @@ toppled during operation by a building destroyer, or a tantruming dwarf.

    -

    Save files

    +

    Save files

    It should be safe to load and view engine-using fortresses from a DF version without DFHack installed, except that in such case the engines won't work. However actually making modifications @@ -2545,10 +2960,13 @@ being generated.

    -

    Add Spatter

    +

    Add Spatter

    This plugin makes reactions with names starting with SPATTER_ADD_ -produce contaminants on the items instead of improvements.

    -

    Intended to give some use to all those poisons that can be bought from caravans.

    +produce contaminants on the items instead of improvements. The produced +contaminants are immune to being washed away by water or destroyed by +the clean items command.

    +

    The plugin is intended to give some use to all those poisons that can +be bought from caravans. :)

    To be really useful this needs patches from bug 808, tweak fix-dimensions and tweak advmode-contained.

    diff --git a/README.rst b/Readme.rst similarity index 58% rename from README.rst rename to Readme.rst index 589451a9a..0b2fbc378 100644 --- a/README.rst +++ b/Readme.rst @@ -1,3 +1,7 @@ +############# +DFHack Readme +############# + ============ Introduction ============ @@ -27,7 +31,7 @@ Compatibility DFHack works on Windows XP, Vista, 7 or any modern Linux distribution. OSX is not supported due to lack of developers with a Mac. -Currently, versions 0.34.08 - 0.34.11 are supported. If you need DFHack +Currently, version 0.34.11 is supported (and tested). If you need DFHack for older versions, look for older releases. On Windows, you have to use the SDL version of DF. @@ -84,6 +88,36 @@ 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:: + + + +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. + ============================= Something doesn't work, help! ============================= @@ -97,46 +131,183 @@ the issues tracker on github, contact me (peterix@gmail.com) or visit the ============= The init file ============= -If your DF folder contains a file named dfhack.init, its contents will be run +If your DF folder contains a file named ``dfhack.init``, its contents will be run every time you start DF. This allows setting up keybindings. An example file -is provided as dfhack.init-example - you can tweak it and rename to dfhack.init +is provided as ``dfhack.init-example`` - you can tweak it and rename to dfhack.init if you want to use this functionality. +Setting keybindings +=================== + +To set keybindings, use the built-in ``keybinding`` command. Like any other +command it can be used at any time from the console, but it is also meaningful +in the DFHack init file. + +Currently it supports any combination of Ctrl/Alt/Shift with F1-F9, or A-Z. + +Possible ways to call the command: + +:keybinding list : List bindings active for the key combination. +:keybinding clear ...: Remove bindings for the specified keys. +:keybinding add "cmdline" "cmdline"...: Add bindings for the specified + key. +:keybinding set "cmdline" "cmdline"...: Clear, and then add bindings for + the specified key. + +The ** parameter above has the following *case-sensitive* syntax:: + + [Ctrl-][Alt-][Shift-]KEY[@context] + +where the *KEY* part can be F1-F9 or A-Z, and [] denote optional parts. + +When multiple commands are bound to the same key combination, DFHack selects +the first applicable one. Later 'add' commands, and earlier entries within one +'add' command have priority. Commands that are not specifically intended for use +as a hotkey are always considered applicable. + +The *context* part in the key specifier above can be used to explicitly restrict +the UI state where the binding would be applicable. If called without parameters, +the ``keybinding`` command among other things prints the current context string. +Only bindings with a *context* tag that either matches the current context fully, +or is a prefix ending at a '/' boundary would be considered for execution, i.e. +for context ``foo/bar/baz``, possible matches are any of ``@foo/bar/baz``, ``@foo/bar``, +``@foo`` or none. + + ======== Commands ======== +DFHack command syntax consists of a command name, followed by arguments separated +by whitespace. To include whitespace in an argument, quote it in double quotes. +To include a double quote character, use ``\"`` inside double quotes. + +If the first non-whitespace character of a line is ``#``, the line is treated +as a comment, i.e. a silent no-op command. + +If the first non-whitespace character is ``:``, the command is parsed in a special +alternative mode: first, non-whitespace characters immediately following the ``:`` +are used as the command name; the remaining part of the line, starting with the first +non-whitespace character *after* the command name, is used verbatim as the first argument. +The following two command lines are exactly equivalent: + + * ``:foo a b "c d" e f`` + * ``foo "a b \"c d\" e f"`` + +This is intended for commands like ``rb_eval`` that evaluate script language statements. + Almost all the commands support using the 'help ' built-in command to retrieve further help without having to look at this document. Alternatively, some accept a 'help'/'?' option on their command line. + +Game progress +============= + +die +--- +Instantly kills DF without saving. + +forcepause +---------- +Forces DF to pause. This is useful when your FPS drops below 1 and you lose +control of the game. + + * Activate with 'forcepause 1' + * Deactivate with 'forcepause 0' + +nopause +------- +Disables pausing (both manual and automatic) with the exception of pause forced +by 'reveal hell'. This is nice for digging under rivers. + +fastdwarf +--------- +Controls speedydwarf and teledwarf. Speedydwarf makes dwarves move quickly and perform tasks quickly. Teledwarf makes dwarves move instantaneously, but do jobs at the same speed. + + * 'fastdwarf 0 0' disables both + * 'fastdwarf 0 1' disables speedydwarf and enables teledwarf + * 'fastdwarf 1 0' enables speedydwarf and disables teledwarf + * '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 +============== + +follow +------ +Makes the game view follow the currently highlighted unit after you exit from +current menu/cursor mode. Handy for watching dwarves running around. Deactivated +by moving the view manually. + +tidlers +------- +Toggle between all possible positions where the idlers count can be placed. + +twaterlvl +--------- +Toggle between displaying/not displaying liquid depth as numbers. + +copystock +---------- +Copies the parameters of the currently highlighted stockpile to the custom +stockpile settings and switches to custom stockpile placement mode, effectively +allowing you to copy/paste stockpiles easily. + +rename +------ +Allows renaming various things. + +Options: + + :rename squad "name": Rename squad by index to 'name'. + :rename hotkey \"name\": Rename hotkey by index. This allows assigning + longer commands to the DF hotkeys. + :rename unit "nickname": Rename a unit/creature highlighted in the DF user + interface. + :rename unit-profession "custom profession": Change proffession name of the + highlighted unit/creature. + :rename building "name": Set a custom name for the selected building. + The building must be one of stockpile, workshop, furnace, trap, + siege engine or an activity zone. + + +Adventure mode +============== + adv-bodyswap -============ +------------ This allows taking control over your followers and other creatures in adventure mode. For example, you can make them pick up new arms and armor and equip them properly. -Usage ------ +Usage: + * When viewing unit details, body-swaps into that unit. * In the main adventure mode screen, reverts transient swap. advtools -======== +-------- A package of different adventure mode tools (currently just one) - -Usage ------ -:list-equipped [all]: List armor and weapons equipped by your companions. - If all is specified, also lists non-metal clothing. -:metal-detector [all-types] [non-trader]: Reveal metal armor and weapons in - shops. The options disable the checks - on item type and being in shop. +Usage: + + :list-equipped [all]: List armor and weapons equipped by your companions. + If all is specified, also lists non-metal clothing. + :metal-detector [all-types] [non-trader]: Reveal metal armor and weapons in + shops. The options disable the checks + on item type and being in shop. + + +Map modification +================ changelayer -=========== +----------- Changes material of the geology layer under cursor to the specified inorganic RAW material. Can have impact on all surrounding regions, not only your embark! By default changing stone to soil and vice versa is not allowed. By default @@ -149,18 +320,18 @@ as well, though. Mineral veins and gem clusters will stay on the map. Use tl;dr: You will end up with changing quite big areas in one go, especially if you use it in lower z levels. Use with care. -Options -------- -:all_biomes: Change selected layer for all biomes on your map. +Options: + + :all_biomes: Change selected layer for all biomes on your map. Result may be undesirable since the same layer can AND WILL be on different z-levels for different biomes. Use the tool 'probe' to get an idea how layers and biomes are distributed on your map. -:all_layers: Change all layers on your map (only for the selected biome + :all_layers: Change all layers on your map (only for the selected biome unless 'all_biomes' is added). Candy mountain, anyone? Will make your map quite boring, but tidy. -:force: Allow changing stone to soil and vice versa. !!THIS CAN HAVE + :force: Allow changing stone to soil and vice versa. !!THIS CAN HAVE WEIRD EFFECTS, USE WITH CARE!! Note that soil will not be magically replaced with stone. You will, however, get a stone floor after digging so it @@ -169,16 +340,16 @@ Options You will, however, get a soil floor after digging so it could be helpful for creating farm plots on maps with no soil. -:verbose: Give some details about what is being changed. -:trouble: Give some advice about known problems. + :verbose: Give some details about what is being changed. + :trouble: Give some advice about known problems. Examples: ---------- -``changelayer GRANITE`` + + ``changelayer GRANITE`` Convert layer at cursor position into granite. -``changelayer SILTY_CLAY force`` + ``changelayer SILTY_CLAY force`` Convert layer at cursor position into clay even if it's stone. -``changelayer MARBLE all_biomes all_layers`` + ``changelayer MARBLE all_biomes all_layers`` Convert all layers of all biomes which are not soil into marble. .. note:: @@ -197,18 +368,18 @@ Examples: You did save your game, right? changevein -========== +---------- Changes material of the vein under cursor to the specified inorganic RAW material. Only affects tiles within the current 16x16 block - for veins and large clusters, you will need to use this command multiple times. Example: --------- -``changevein NATIVE_PLATINUM`` + + ``changevein NATIVE_PLATINUM`` Convert vein at cursor position into platinum ore. changeitem -========== +---------- Allows changing item material and base quality. By default the item currently selected in the UI will be changed (you can select items in the 'k' list or inside containers/inventory). By default change is only allowed if materials @@ -219,494 +390,289 @@ in weirdness. To get an idea how the RAW id should look like, check some items with 'info'. Using 'force' might create items which are not touched by crafters/haulers. -Options -------- -:info: Don't change anything, print some info instead. -:here: Change all items at the cursor position. Requires in-game cursor. -:material, m: Change material. Must be followed by valid material RAW id. -:quality, q: Change base quality. Must be followed by number (0-5). -:force: Ignore subtypes, force change to new material. +Options: + + :info: Don't change anything, print some info instead. + :here: Change all items at the cursor position. Requires in-game cursor. + :material, m: Change material. Must be followed by valid material RAW id. + :quality, q: Change base quality. Must be followed by number (0-5). + :force: Ignore subtypes, force change to new material. Examples: ---------- -``changeitem m INORGANIC:GRANITE here`` + + ``changeitem m INORGANIC:GRANITE here`` Change material of all items under the cursor to granite. -``changeitem q 5`` + ``changeitem q 5`` Change currently selected item to masterpiece quality. -cursecheck -========== -Checks a single map tile or the whole map/world for cursed creatures (ghosts, -vampires, necromancers, werebeasts, zombies). +colonies +-------- +Allows listing all the vermin colonies on the map and optionally turning them into honey bee colonies. -With an active in-game cursor only the selected tile will be observed. -Without a cursor the whole map will be checked. +Options: -By default cursed creatures will be only counted in case you just want to find -out if you have any of them running around in your fort. Dead and passive -creatures (ghosts who were put to rest, killed vampires, ...) are ignored. -Undead skeletons, corpses, bodyparts and the like are all thrown into the curse -category "zombie". Anonymous zombies and resurrected body parts will show -as "unnamed creature". + :bees: turn colonies into honey bee colonies + +deramp (by zilpin) +------------------ +Removes all ramps designated for removal from the map. This is useful for replicating the old channel digging designation. +It also removes any and all 'down ramps' that can remain after a cave-in (you don't have to designate anything for that to happen). -Options +feature ------- -:detail: Print full name, date of birth, date of curse and some status - info (some vampires might use fake identities in-game, though). -:nick: Set the type of curse as nickname (does not always show up - in-game, some vamps don't like nicknames). -:all: Include dead and passive cursed creatures (can result in a quite - long list after having FUN with necromancers). -:verbose: Print all curse tags (if you really want to know it all). +Enables management of map features. -Examples: ---------- -``cursecheck detail all`` - Give detailed info about all cursed creatures including deceased ones (no - in-game cursor). -``cursecheck nick`` - Give a nickname all living/active cursed creatures on the map(no in-game - cursor). +* Discovering a magma feature (magma pool, volcano, magma sea, or curious + underground structure) permits magma workshops and furnaces to be built. +* Discovering a cavern layer causes plants (trees, shrubs, and grass) from + that cavern to grow within your fortress. -.. note:: +Options: - * If you do a full search (with the option "all") former ghosts will show up - with the cursetype "unknown" because their ghostly flag is not set - anymore. But if you happen to find a living/active creature with cursetype - "unknown" please report that in the dfhack thread on the modding forum or - per irc. This is likely to happen with mods which introduce new types - of curses, for example. + :list: Lists all map features in your current embark by index. + :show X: Marks the selected map feature as discovered. + :hide X: Marks the selected map feature as undiscovered. -follow -====== -Makes the game view follow the currently highlighted unit after you exit from -current menu/cursor mode. Handy for watching dwarves running around. Deactivated -by moving the view manually. +liquids +------- +Allows adding magma, water and obsidian to the game. It replaces the normal +dfhack command line and can't be used from a hotkey. Settings will be remembered +as long as dfhack runs. Intended for use in combination with the command +liquids-here (which can be bound to a hotkey). -forcepause -========== -Forces DF to pause. This is useful when your FPS drops below 1 and you lose -control of the game. +For more information, refer to the command's internal help. - * Activate with 'forcepause 1' - * Deactivate with 'forcepause 0' +.. note:: -nopause -======= -Disables pausing (both manual and automatic) with the exception of pause forced -by 'reveal hell'. This is nice for digging under rivers. + Spawning and deleting liquids can F up pathing data and + temperatures (creating heat traps). You've been warned. -die -=== -Instantly kills DF without saving. +liquids-here +------------ +Run the liquid spawner with the current/last settings made in liquids (if no +settings in liquids were made it paints a point of 7/7 magma by default). -autodump -======== -This utility lets you quickly move all items designated to be dumped. -Items are instantly moved to the cursor position, the dump flag is unset, -and the forbid flag is set, as if it had been dumped normally. -Be aware that any active dump item tasks still point at the item. +Intended to be used as keybinding. Requires an active in-game cursor. -Cursor must be placed on a floor tile so the items can be dumped there. -Options -------- -:destroy: Destroy instead of dumping. Doesn't require a cursor. -:destroy-here: Destroy items only under the cursor. -:visible: Only process items that are not hidden. -:hidden: Only process hidden items. -:forbidden: Only process forbidden items (default: only unforbidden). +tiletypes +--------- +Can be used for painting map tiles and is an interactive command, much like +liquids. -autodump-destroy-here -===================== -Destroy items marked for dumping under cursor. Identical to autodump -destroy-here, but intended for use as keybinding. +The tool works with two set of options and a brush. The brush determines which +tiles will be processed. First set of options is the filter, which can exclude +some of the tiles from the brush by looking at the tile properties. The second +set of options is the paint - this determines how the selected tiles are +changed. -autodump-destroy-item -===================== -Destroy the selected item. The item may be selected in the 'k' list, or inside -a container. If called again before the game is resumed, cancels destroy. +Both paint and filter can have many different properties including things like +general shape (WALL, FLOOR, etc.), general material (SOIL, STONE, MINERAL, +etc.), state of 'designated', 'hidden' and 'light' flags. -burrow -====== -Miscellaneous burrow control. Allows manipulating burrows and automated burrow -expansion while digging. +The properties of filter and paint can be partially defined. This means that +you can for example do something like this: -Options -------- -:enable feature ...: -:disable feature ...: Enable or Disable features of the plugin. -:clear-unit burrow burrow ...: -:clear-tiles burrow burrow ...: Removes all units or tiles from the burrows. -:set-units target-burrow src-burrow ...: -:add-units target-burrow src-burrow ...: -:remove-units target-burrow src-burrow ...: Adds or removes units in source - burrows to/from the target burrow. Set is equivalent to clear and add. -:set-tiles target-burrow src-burrow ...: -:add-tiles target-burrow src-burrow ...: -:remove-tiles target-burrow src-burrow ...: Adds or removes tiles in source - burrows to/from the target burrow. In place of a source burrow it is - possible to use one of the following keywords: ABOVE_GROUND, - SUBTERRANEAN, INSIDE, OUTSIDE, LIGHT, DARK, HIDDEN, REVEALED - -Features --------- -:auto-grow: When a wall inside a burrow with a name ending in '+' is dug - out, the burrow is extended to newly-revealed adjacent walls. - This final '+' may be omitted in burrow name args of commands above. - Digging 1-wide corridors with the miner inside the burrow is SLOW. +:: -catsplosion -=========== -Makes cats just *multiply*. It is not a good idea to run this more than once or -twice. + filter material STONE + filter shape FORTIFICATION + paint shape FLOOR -clean -===== -Cleans all the splatter that get scattered all over the map, items and -creatures. In an old fortress, this can significantly reduce FPS lag. It can -also spoil your !!FUN!!, so think before you use it. +This will turn all stone fortifications into floors, preserving the material. -Options -------- -:map: Clean the map tiles. By default, it leaves mud and snow alone. -:units: Clean the creatures. Will also clean hostiles. -:items: Clean all the items. Even a poisoned blade. +Or this: +:: -Extra options for 'map' ------------------------ -:mud: Remove mud in addition to the normal stuff. -:snow: Also remove snow coverings. + filter shape FLOOR + filter material MINERAL + paint shape WALL -spotclean -========= -Works like 'clean map snow mud', but only for the tile under the cursor. Ideal -if you want to keep that bloody entrance 'clean map' would clean up. +Turning mineral vein floors back into walls. -cleanowned -========== -Confiscates items owned by dwarfs. By default, owned food on the floor -and rotten items are confistacted and dumped. - -Options -------- -:all: confiscate all owned items -:scattered: confiscated and dump all items scattered on the floor -:x: confiscate/dump items with wear level 'x' and more -:X: confiscate/dump items with wear level 'X' and more -:dryrun: a dry run. combine with other options to see what will happen - without it actually happening. - -Example: --------- -``cleanowned scattered X`` : This will confiscate rotten and dropped food, garbage on the floors and any worn items with 'X' damage and above. - -colonies -======== -Allows listing all the vermin colonies on the map and optionally turning them into honey bee colonies. - -Options -------- -:bees: turn colonies into honey bee colonies +The tool also allows tweaking some tile flags: -deramp (by zilpin) -================== -Removes all ramps designated for removal from the map. This is useful for replicating the old channel digging designation. -It also removes any and all 'down ramps' that can remain after a cave-in (you don't have to designate anything for that to happen). +Or this: -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 + paint hidden 1 + paint hidden 0 -Confirmed working DFusion plugins: ----------------------------------- -:simple_embark: allows changing the number of dwarves available on embark. +This will hide previously revealed tiles (or show hidden with the 0 option). -.. note:: +Any paint or filter option (or the entire paint or filter) can be disabled entirely by using the ANY keyword: - * 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. +:: -drybuckets -========== -This utility removes water from all buckets in your fortress, allowing them to be safely used for making lye. + paint hidden ANY + paint shape ANY + filter material any + filter shape any + filter any -fastdwarf -========= -Makes your minions move at ludicrous speeds. +You can use several different brushes for painting tiles: + * Point. (point) + * Rectangular range. (range) + * A column ranging from current cursor to the first solid tile above. (column) + * DF map block - 16x16 tiles, in a regular grid. (block) - * Activate with 'fastdwarf 1' - * Deactivate with 'fastdwarf 0' +Example: -feature -======= -Enables management of map features. +:: -* Discovering a magma feature (magma pool, volcano, magma sea, or curious - underground structure) permits magma workshops and furnaces to be built. -* Discovering a cavern layer causes plants (trees, shrubs, and grass) from - that cavern to grow within your fortress. + range 10 10 1 -Options -------- -:list: Lists all map features in your current embark by index. -:show X: Marks the selected map feature as discovered. -:hide X: Marks the selected map feature as undiscovered. +This will change the brush to a rectangle spanning 10x10 tiles on one z-level. +The range starts at the position of the cursor and goes to the east, south and +up. -filltraffic -=========== -Set traffic designations using flood-fill starting at the cursor. +For more details, see the 'help' command while using this. -Traffic Type Codes: -------------------- -:H: High Traffic -:N: Normal Traffic -:L: Low Traffic -:R: Restricted Traffic +tiletypes-commands +------------------ +Runs tiletypes commands, separated by ;. This makes it possible to change +tiletypes modes from a hotkey. -Other Options: +tiletypes-here -------------- -:X: Fill accross z-levels. -:B: Include buildings and stockpiles. -:P: Include empty space. - -Example: --------- -'filltraffic H' - When used in a room with doors, it will set traffic to HIGH in just that room. - -alltraffic -========== -Set traffic designations for every single tile of the map (useful for resetting traffic designations). +Apply the current tiletypes options at the in-game cursor position, including +the brush. Can be used from a hotkey. -Traffic Type Codes: -------------------- -:H: High Traffic -:N: Normal Traffic -:L: Low Traffic -:R: Restricted Traffic +tiletypes-here-point +-------------------- +Apply the current tiletypes options at the in-game cursor position to a single +tile. Can be used from a hotkey. -Example: +tubefill -------- -'alltraffic N' - Set traffic to 'normal' for all tiles. +Fills all the adamantine veins again. Veins that were empty will be filled in +too, but might still trigger a demon invasion (this is a known bug). -fixdiplomats -============ -Up to version 0.31.12, Elves only sent Diplomats to your fortress to propose -tree cutting quotas due to a bug; once that bug was fixed, Elves stopped caring -about excess tree cutting. This command adds a Diplomat position to all Elven -civilizations, allowing them to negotiate tree cutting quotas (and allowing you -to violate them and potentially start wars) in case you haven't already modified -your raws accordingly. +extirpate +--------- +A tool for getting rid of trees and shrubs. By default, it only kills +a tree/shrub under the cursor. The plants are turned into ashes instantly. -fixmerchants -============ -This command adds the Guild Representative position to all Human civilizations, -allowing them to make trade agreements (just as they did back in 0.28.181.40d -and earlier) in case you haven't already modified your raws accordingly. +Options: -fixveins -======== -Removes invalid references to mineral inclusions and restores missing ones. -Use this if you broke your embark with tools like tiletypes, or if you -accidentally placed a construction on top of a valuable mineral floor. + :shrubs: affect all shrubs on the map + :trees: affect all trees on the map + :all: affect every plant! -flows -===== -A tool for checking how many tiles contain flowing liquids. If you suspect that -your magma sea leaks into HFS, you can use this tool to be sure without -revealing the map. +grow +---- +Makes all saplings present on the map grow into trees (almost) instantly. -getplants -========= -This tool allows plant gathering and tree cutting by RAW ID. Specify the types -of trees to cut down and/or shrubs to gather by their plant names, separated -by spaces. +immolate +-------- +Very similar to extirpate, but additionally sets the plants on fire. The fires +can and *will* spread ;) -Options +regrass ------- -:-t: Select trees only (exclude shrubs) -:-s: Select shrubs only (exclude trees) -:-c: Clear designations instead of setting them -:-x: Apply selected action to all plants except those specified (invert - selection) - -Specifying both -t and -s will have no effect. If no plant IDs are specified, -all valid plant IDs will be listed. - -tidlers -======= -Toggle between all possible positions where the idlers count can be placed. +Regrows grass. Not much to it ;) -twaterlvl -========= -Toggle between displaying/not displaying liquid depth as numbers. +weather +------- +Prints the current weather map by default. -job -=== -Command for general job query and manipulation. +Also lets you change the current weather to 'clear sky', 'rainy' or 'snowing'. Options: - * no extra options - Print details of the current job. The job can be selected - in a workshop, or the unit/jobs screen. - * list - Print details of all jobs in the selected workshop. - * item-material - Replace the exact material - id in the job item. - * item-type - Replace the exact item type id in - the job item. -job-material -============ -Alter the material of the selected job. + :snow: make it snow everywhere. + :rain: make it rain. + :clear: clear the sky. -Invoked as: job-material - -Intended to be used as a keybinding: - * In 'q' mode, when a job is highlighted within a workshop or furnace, - changes the material of the job. Only inorganic materials can be used - in this mode. - * In 'b' mode, during selection of building components positions the cursor - over the first available choice with the matching material. -job-duplicate -============= -Duplicate the selected job in a workshop: - * In 'q' mode, when a job is highlighted within a workshop or furnace building, - instantly duplicates the job. +Map inspection +============== -keybinding -========== +cursecheck +---------- +Checks a single map tile or the whole map/world for cursed creatures (ghosts, +vampires, necromancers, werebeasts, zombies). -Manages DFHack keybindings. +With an active in-game cursor only the selected tile will be observed. +Without a cursor the whole map will be checked. -Currently it supports any combination of Ctrl/Alt/Shift with F1-F9, or A-Z. +By default cursed creatures will be only counted in case you just want to find +out if you have any of them running around in your fort. Dead and passive +creatures (ghosts who were put to rest, killed vampires, ...) are ignored. +Undead skeletons, corpses, bodyparts and the like are all thrown into the curse +category "zombie". Anonymous zombies and resurrected body parts will show +as "unnamed creature". -Options -------- -:keybinding list : List bindings active for the key combination. -:keybinding clear ...: Remove bindings for the specified keys. -:keybinding add "cmdline" "cmdline"...: Add bindings for the specified - key. -:keybinding set "cmdline" "cmdline"...: Clear, and then add bindings for - the specified key. +Options: -When multiple commands are bound to the same key combination, DFHack selects -the first applicable one. Later 'add' commands, and earlier entries within one -'add' command have priority. Commands that are not specifically intended for use -as a hotkey are always considered applicable. + :detail: Print full name, date of birth, date of curse and some status + info (some vampires might use fake identities in-game, though). + :nick: Set the type of curse as nickname (does not always show up + in-game, some vamps don't like nicknames). + :all: Include dead and passive cursed creatures (can result in a quite + long list after having FUN with necromancers). + :verbose: Print all curse tags (if you really want to know it all). -liquids -======= -Allows adding magma, water and obsidian to the game. It replaces the normal -dfhack command line and can't be used from a hotkey. Settings will be remembered -as long as dfhack runs. Intended for use in combination with the command -liquids-here (which can be bound to a hotkey). +Examples: -For more information, refer to the command's internal help. + ``cursecheck detail all`` + Give detailed info about all cursed creatures including deceased ones (no + in-game cursor). + ``cursecheck nick`` + Give a nickname all living/active cursed creatures on the map(no in-game + cursor). .. note:: - Spawning and deleting liquids can F up pathing data and - temperatures (creating heat traps). You've been warned. - -liquids-here -============ -Run the liquid spawner with the current/last settings made in liquids (if no -settings in liquids were made it paints a point of 7/7 magma by default). - -Intended to be used as keybinding. Requires an active in-game cursor. - -mode -==== -This command lets you see and change the game mode directly. -Not all combinations are good for every situation and most of them will -produce undesirable results. There are a few good ones though. - -.. admonition:: Example - - You are in fort game mode, managing your fortress and paused. - You switch to the arena game mode, *assume control of a creature* and then - switch to adventure game mode(1). - You just lost a fortress and gained an adventurer. - You could also do this. - You are in fort game mode, managing your fortress and paused at the esc menu. - You switch to the adventure game mode, then use Dfusion to *assume control of a creature* and then - save or retire. - You just created a returnable mountain home and gained an adventurer. - - -I take no responsibility of anything that happens as a result of using this tool - -extirpate -========= -A tool for getting rid of trees and shrubs. By default, it only kills -a tree/shrub under the cursor. The plants are turned into ashes instantly. - -Options -------- -:shrubs: affect all shrubs on the map -:trees: affect all trees on the map -:all: affect every plant! - -grow -==== -Makes all saplings present on the map grow into trees (almost) instantly. + * If you do a full search (with the option "all") former ghosts will show up + with the cursetype "unknown" because their ghostly flag is not set + anymore. But if you happen to find a living/active creature with cursetype + "unknown" please report that in the dfhack thread on the modding forum or + per irc. This is likely to happen with mods which introduce new types + of curses, for example. -immolate -======== -Very similar to extirpate, but additionally sets the plants on fire. The fires -can and *will* spread ;) +flows +----- +A tool for checking how many tiles contain flowing liquids. If you suspect that +your magma sea leaks into HFS, you can use this tool to be sure without +revealing the map. probe -===== +----- Can be used to determine tile properties like temperature. prospect -======== +-------- Prints a big list of all the present minerals and plants. By default, only the visible part of the map is scanned. -Options -------- -:all: Scan the whole map, as if it was revealed. -:value: Show material value in the output. Most useful for gems. -:hell: Show the Z range of HFS tubes. Implies 'all'. +Options: + + :all: Scan the whole map, as if it was revealed. + :value: Show material value in the output. Most useful for gems. + :hell: Show the Z range of HFS tubes. Implies 'all'. Pre-embark estimate -------------------- -If called during the embark selection screen, displays an estimate of layer -stone availability. If the 'all' option is specified, also estimates veins. -The estimate is computed either for 1 embark tile of the blinking biome, or -for all tiles of the embark rectangle. +................... -Options -------- -:all: processes all tiles, even hidden ones. +If prospect is called during the embark selection screen, it displays an estimate of +layer stone availability. -regrass -======= -Regrows grass. Not much to it ;) +.. note:: -rename -====== -Allows renaming various things. + The results of pre-embark prospect are an *estimate*, and can at best be expected + to be somewhere within +/- 30% of the true amount; sometimes it does a lot worse. + Especially, it is not clear how to precisely compute how many soil layers there + will be in a given embark tile, so it can report a whole extra layer, or omit one + that is actually present. -Options -------- -:rename squad "name": Rename squad by index to 'name'. -:rename hotkey \"name\": Rename hotkey by index. This allows assigning - longer commands to the DF hotkeys. -:rename unit "nickname": Rename a unit/creature highlighted in the DF user - interface. -:rename unit-profession "custom profession": Change proffession name of the - highlighted unit/creature. -:rename building "name": Set a custom name for the selected building. - The building must be one of stockpile, workshop, furnace, trap or siege engine. +Options: + + :all: Also estimate vein mineral amounts. reveal -====== +------ This reveals the map. By default, HFS will remain hidden so that the demons don't spawn. You can use 'reveal hell' to reveal everything. With hell revealed, you won't be able to unpause until you hide the map again. If you really want @@ -716,167 +682,341 @@ Reveal also works in adventure mode, but any of its effects are negated once you move. When you use it this way, you don't need to run 'unreveal'. unreveal -======== +-------- Reverts the effects of 'reveal'. revtoggle -========= +--------- Switches between 'reveal' and 'unreveal'. revflood -======== +-------- This command will hide the whole map and then reveal all the tiles that have a path to the in-game cursor. revforget -========= +--------- When you use reveal, it saves information about what was/wasn't visible before revealing everything. Unreveal uses this information to hide things again. This command throws away the information. For example, use in cases where you abandoned with the fort revealed and no longer want the data. -lair -==== -This command allows you to mark the map as 'monster lair', preventing item -scatter on abandon. When invoked as 'lair reset', it does the opposite. +showmood +-------- +Shows all items needed for the currently active strange mood. -Unlike reveal, this command doesn't save the information about tiles - you -won't be able to restore state of real monster lairs using 'lair reset'. -Options +Designations +============ + +burrow +------ +Miscellaneous burrow control. Allows manipulating burrows and automated burrow +expansion while digging. + +Options: + + **enable feature ...** + Enable features of the plugin. + **disable feature ...** + Disable features of the plugin. + **clear-unit burrow burrow ...** + Remove all units from the burrows. + **clear-tiles burrow burrow ...** + Remove all tiles from the burrows. + **set-units target-burrow src-burrow ...** + Clear target, and adds units from source burrows. + **add-units target-burrow src-burrow ...** + Add units from the source burrows to the target. + **remove-units target-burrow src-burrow ...** + Remove units in source burrows from the target. + **set-tiles target-burrow src-burrow ...** + Clear target and adds tiles from the source burrows. + **add-tiles target-burrow src-burrow ...** + Add tiles from the source burrows to the target. + **remove-tiles target-burrow src-burrow ...** + Remove tiles in source burrows from the target. + + For these three options, in place of a source burrow it is + possible to use one of the following keywords: ABOVE_GROUND, + SUBTERRANEAN, INSIDE, OUTSIDE, LIGHT, DARK, HIDDEN, REVEALED + +Features: + + :auto-grow: When a wall inside a burrow with a name ending in '+' is dug + out, the burrow is extended to newly-revealed adjacent walls. + This final '+' may be omitted in burrow name args of commands above. + Digging 1-wide corridors with the miner inside the burrow is SLOW. + +digv +---- +Designates a whole vein for digging. Requires an active in-game cursor placed +over a vein tile. With the 'x' option, it will traverse z-levels (putting stairs +between the same-material tiles). + +digvx +----- +A permanent alias for 'digv x'. + +digl +---- +Designates layer stone for digging. Requires an active in-game cursor placed +over a layer stone tile. With the 'x' option, it will traverse z-levels +(putting stairs between the same-material tiles). With the 'undo' option it +will remove the dig designation instead (if you realize that digging out a 50 +z-level deep layer was not such a good idea after all). + +diglx +----- +A permanent alias for 'digl x'. + +digexp +------ +This command can be used for exploratory mining. + +See: http://df.magmawiki.com/index.php/DF2010:Exploratory_mining + +There are two variables that can be set: pattern and filter. + +Patterns: + + :diag5: diagonals separated by 5 tiles + :diag5r: diag5 rotated 90 degrees + :ladder: A 'ladder' pattern + :ladderr: ladder rotated 90 degrees + :clear: Just remove all dig designations + :cross: A cross, exactly in the middle of the map. + +Filters: + + :all: designate whole z-level + :hidden: designate only hidden tiles of z-level (default) + :designated: Take current designation and apply pattern to it. + +After you have a pattern set, you can use 'expdig' to apply it again. + +Examples: + + designate the diagonal 5 patter over all hidden tiles: + * expdig diag5 hidden + apply last used pattern and filter: + * expdig + Take current designations and replace them with the ladder pattern: + * expdig ladder designated + +digcircle +--------- +A command for easy designation of filled and hollow circles. +It has several types of options. + +Shape: + + :hollow: Set the circle to hollow (default) + :filled: Set the circle to filled + :#: Diameter in tiles (default = 0, does nothing) + +Action: + + :set: Set designation (default) + :unset: Unset current designation + :invert: Invert designations already present + +Designation types: + + :dig: Normal digging designation (default) + :ramp: Ramp digging + :ustair: Staircase up + :dstair: Staircase down + :xstair: Staircase up/down + :chan: Dig channel + +After you have set the options, the command called with no options +repeats with the last selected parameters. + +Examples: + +* 'digcircle filled 3' = Dig a filled circle with radius = 3. +* 'digcircle' = Do it again. + + +digtype ------- -:lair: Mark the map as monster lair -:lair reset: Mark the map as ordinary (not lair) +For every tile on the map of the same vein type as the selected tile, this command designates it to have the same designation as the selected tile. If the selected tile has no designation, they will be dig designated. +If an argument is given, the designation of the selected tile is ignored, and all appropriate tiles are set to the specified designation. + +Options: + + :dig: + :channel: + :ramp: + :updown: up/down stairs + :up: up stairs + :down: down stairs + :clear: clear designation + + +filltraffic +----------- +Set traffic designations using flood-fill starting at the cursor. + +Traffic Type Codes: + + :H: High Traffic + :N: Normal Traffic + :L: Low Traffic + :R: Restricted Traffic + +Other Options: + + :X: Fill accross z-levels. + :B: Include buildings and stockpiles. + :P: Include empty space. -seedwatch -========= -Tool for turning cooking of seeds and plants on/off depending on how much you -have of them. +Example: -See 'seedwatch help' for detailed description. + 'filltraffic H' - When used in a room with doors, it will set traffic to HIGH in just that room. -showmood -======== -Shows all items needed for the currently active strange mood. +alltraffic +---------- +Set traffic designations for every single tile of the map (useful for resetting traffic designations). -copystock -========== -Copies the parameters of the currently highlighted stockpile to the custom -stockpile settings and switches to custom stockpile placement mode, effectively -allowing you to copy/paste stockpiles easily. +Traffic Type Codes: -ssense / stonesense -=================== -An isometric visualizer that runs in a second window. This requires working -graphics acceleration and at least a dual core CPU (otherwise it will slow -down DF). + :H: High Traffic + :N: Normal Traffic + :L: Low Traffic + :R: Restricted Traffic -All the data resides in the 'stonesense' directory. For detailed instructions, -see stonesense/README.txt +Example: -Compatible with Windows > XP SP3 and most modern Linux distributions. + 'alltraffic N' - Set traffic to 'normal' for all tiles. -Older versions, support and extra graphics can be found in the bay12 forum -thread: http://www.bay12forums.com/smf/index.php?topic=43260.0 +getplants +--------- +This tool allows plant gathering and tree cutting by RAW ID. Specify the types +of trees to cut down and/or shrubs to gather by their plant names, separated +by spaces. -Some additional resources: -http://df.magmawiki.com/index.php/Utility:Stonesense/Content_repository +Options: -tiletypes -========= -Can be used for painting map tiles and is an interactive command, much like -liquids. + :-t: Select trees only (exclude shrubs) + :-s: Select shrubs only (exclude trees) + :-c: Clear designations instead of setting them + :-x: Apply selected action to all plants except those specified (invert + selection) -The tool works with two set of options and a brush. The brush determines which -tiles will be processed. First set of options is the filter, which can exclude -some of the tiles from the brush by looking at the tile properties. The second -set of options is the paint - this determines how the selected tiles are -changed. +Specifying both -t and -s will have no effect. If no plant IDs are specified, +all valid plant IDs will be listed. -Both paint and filter can have many different properties including things like -general shape (WALL, FLOOR, etc.), general material (SOIL, STONE, MINERAL, -etc.), state of 'designated', 'hidden' and 'light' flags. -The properties of filter and paint can be partially defined. This means that -you can for example do something like this: +Cleanup and garbage disposal +============================ -:: +clean +----- +Cleans all the splatter that get scattered all over the map, items and +creatures. In an old fortress, this can significantly reduce FPS lag. It can +also spoil your !!FUN!!, so think before you use it. - filter material STONE - filter shape FORTIFICATION - paint shape FLOOR +Options: -This will turn all stone fortifications into floors, preserving the material. + :map: Clean the map tiles. By default, it leaves mud and snow alone. + :units: Clean the creatures. Will also clean hostiles. + :items: Clean all the items. Even a poisoned blade. -Or this: -:: +Extra options for 'map': - filter shape FLOOR - filter material MINERAL - paint shape WALL + :mud: Remove mud in addition to the normal stuff. + :snow: Also remove snow coverings. -Turning mineral vein floors back into walls. +spotclean +--------- +Works like 'clean map snow mud', but only for the tile under the cursor. Ideal +if you want to keep that bloody entrance 'clean map' would clean up. -The tool also allows tweaking some tile flags: +autodump +-------- +This utility lets you quickly move all items designated to be dumped. +Items are instantly moved to the cursor position, the dump flag is unset, +and the forbid flag is set, as if it had been dumped normally. +Be aware that any active dump item tasks still point at the item. -Or this: +Cursor must be placed on a floor tile so the items can be dumped there. -:: +Options: - paint hidden 1 - paint hidden 0 + :destroy: Destroy instead of dumping. Doesn't require a cursor. + :destroy-here: Destroy items only under the cursor. + :visible: Only process items that are not hidden. + :hidden: Only process hidden items. + :forbidden: Only process forbidden items (default: only unforbidden). -This will hide previously revealed tiles (or show hidden with the 0 option). +autodump-destroy-here +--------------------- +Destroy items marked for dumping under cursor. Identical to autodump +destroy-here, but intended for use as keybinding. -Any paint or filter option (or the entire paint or filter) can be disabled entirely by using the ANY keyword: +autodump-destroy-item +--------------------- +Destroy the selected item. The item may be selected in the 'k' list, or inside +a container. If called again before the game is resumed, cancels destroy. -:: +cleanowned +---------- +Confiscates items owned by dwarfs. By default, owned food on the floor +and rotten items are confistacted and dumped. - paint hidden ANY - paint shape ANY - filter material any - filter shape any - filter any +Options: -You can use several different brushes for painting tiles: - * Point. (point) - * Rectangular range. (range) - * A column ranging from current cursor to the first solid tile above. (column) - * DF map block - 16x16 tiles, in a regular grid. (block) + :all: confiscate all owned items + :scattered: confiscated and dump all items scattered on the floor + :x: confiscate/dump items with wear level 'x' and more + :X: confiscate/dump items with wear level 'X' and more + :dryrun: a dry run. combine with other options to see what will happen + without it actually happening. Example: -:: + ``cleanowned scattered X`` + This will confiscate rotten and dropped food, garbage on the floors and any + worn items with 'X' damage and above. - range 10 10 1 -This will change the brush to a rectangle spanning 10x10 tiles on one z-level. -The range starts at the position of the cursor and goes to the east, south and -up. -For more details, see the 'help' command while using this. +Bugfixes +======== -tiletypes-commands -================== -Runs tiletypes commands, separated by ;. This makes it possible to change -tiletypes modes from a hotkey. +drybuckets +---------- +This utility removes water from all buckets in your fortress, allowing them to be safely used for making lye. -tiletypes-here -============== -Apply the current tiletypes options at the in-game cursor position, including -the brush. Can be used from a hotkey. +fixdiplomats +------------ +Up to version 0.31.12, Elves only sent Diplomats to your fortress to propose +tree cutting quotas due to a bug; once that bug was fixed, Elves stopped caring +about excess tree cutting. This command adds a Diplomat position to all Elven +civilizations, allowing them to negotiate tree cutting quotas (and allowing you +to violate them and potentially start wars) in case you haven't already modified +your raws accordingly. -tiletypes-here-point -==================== -Apply the current tiletypes options at the in-game cursor position to a single -tile. Can be used from a hotkey. +fixmerchants +------------ +This command adds the Guild Representative position to all Human civilizations, +allowing them to make trade agreements (just as they did back in 0.28.181.40d +and earlier) in case you haven't already modified your raws accordingly. + +fixveins +-------- +Removes invalid references to mineral inclusions and restores missing ones. +Use this if you broke your embark with tools like tiletypes, or if you +accidentally placed a construction on top of a valuable mineral floor. tweak -===== -Contains various tweaks for minor bugs (currently just one). +----- +Contains various tweaks for minor bugs. + +One-shot subcommands: -Options -------- :clear-missing: Remove the missing status from the selected unit. This allows engraving slabs for ghostly, but not yet found, creatures. @@ -904,6 +1044,9 @@ Options and are not flagged as tame), but you are allowed to mark them for slaughter. Grabbing wagons results in some funny spam, then they are scuttled. + +Subcommands that persist until disabled or DF quit: + :stable-cursor: Saves the exact cursor position between t/q/k/d/etc menus of dwarfmode. :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). @@ -921,137 +1064,208 @@ Options in advmode. The issue is that the screen tries to force you to select the contents separately from the container. This forcefully skips child reagents. +:fast-trade: Makes Shift-Enter in the Move Goods to Depot and Trade screens select + the current item (fully, in case of a stack), and scroll down one line. +: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 + to make them stand out more in the list. + +fix-armory +---------- -tubefill -======== -Fills all the adamantine veins again. Veins that were empty will be filled in -too, but might still trigger a demon invasion (this is a known bug). +Enables a fix for storage of squad equipment in barracks. -digv -==== -Designates a whole vein for digging. Requires an active in-game cursor placed -over a vein tile. With the 'x' option, it will traverse z-levels (putting stairs -between the same-material tiles). +Specifically, it prevents your haulers from moving that equipment +to stockpiles, and instead queues jobs to store it on weapon racks, +armor stands, and in containers. -digvx -===== -A permanent alias for 'digv x'. +.. note:: -digl -==== -Designates layer stone for digging. Requires an active in-game cursor placed -over a layer stone tile. With the 'x' option, it will traverse z-levels -(putting stairs between the same-material tiles). With the 'undo' option it -will remove the dig designation instead (if you realize that digging out a 50 -z-level deep layer was not such a good idea after all). + 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. -diglx -===== -A permanent alias for 'digl x'. + Also, the default capacity of armor stands is way too low, so check out + http://www.bay12games.com/dwarves/mantisbt/view.php?id=1445 + for a patch addressing that too. -digexp -====== -This command can be used for exploratory mining. +Note that the buildings in the armory are used as follows: -See: http://df.magmawiki.com/index.php/DF2010:Exploratory_mining +* 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. -There are two variables that can be set: pattern and filter. +* 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. -Patterns: ---------- -:diag5: diagonals separated by 5 tiles -:diag5r: diag5 rotated 90 degrees -:ladder: A 'ladder' pattern -:ladderr: ladder rotated 90 degrees -:clear: Just remove all dig designations -:cross: A cross, exactly in the middle of the map. +* Cabinets are used to store assigned clothing for a specific squad member. + They are **never** used to store owned clothing. -Filters: --------- -:all: designate whole z-level -:hidden: designate only hidden tiles of z-level (default) -:designated: Take current designation and apply pattern to it. +* 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. -After you have a pattern set, you can use 'expdig' to apply it again. +.. warning:: -Examples: + 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 +======================= + +lair +---- +This command allows you to mark the map as 'monster lair', preventing item +scatter on abandon. When invoked as 'lair reset', it does the opposite. + +Unlike reveal, this command doesn't save the information about tiles - you +won't be able to restore state of real monster lairs using 'lair reset'. + +Options: + + :lair: Mark the map as monster lair + :lair reset: Mark the map as ordinary (not lair) + +mode +---- +This command lets you see and change the game mode directly. +Not all combinations are good for every situation and most of them will +produce undesirable results. There are a few good ones though. + +.. admonition:: Example + + You are in fort game mode, managing your fortress and paused. + You switch to the arena game mode, *assume control of a creature* and then + switch to adventure game mode(1). + You just lost a fortress and gained an adventurer. + You could also do this. + You are in fort game mode, managing your fortress and paused at the esc menu. + You switch to the adventure game mode, then use Dfusion to *assume control of a creature* and then + save or retire. + You just created a returnable mountain home and gained an adventurer. + + +I take no responsibility of anything that happens as a result of using this tool + + +Visualizer and data export +========================== + +ssense / stonesense +------------------- +An isometric visualizer that runs in a second window. This requires working +graphics acceleration and at least a dual core CPU (otherwise it will slow +down DF). + +All the data resides in the 'stonesense' directory. For detailed instructions, +see stonesense/README.txt + +Compatible with Windows > XP SP3 and most modern Linux distributions. + +Older versions, support and extra graphics can be found in the bay12 forum +thread: http://www.bay12forums.com/smf/index.php?topic=43260.0 + +Some additional resources: +http://df.magmawiki.com/index.php/Utility:Stonesense/Content_repository + +mapexport --------- -designate the diagonal 5 patter over all hidden tiles: - * expdig diag5 hidden -apply last used pattern and filter: - * expdig -Take current designations and replace them with the ladder pattern: - * expdig ladder designated +Export the current loaded map as a file. This will be eventually usable +with visualizers. -digcircle -========= -A command for easy designation of filled and hollow circles. -It has several types of options. +dwarfexport +----------- +Export dwarves to RuneSmith-compatible XML. -Shape: --------- -:hollow: Set the circle to hollow (default) -:filled: Set the circle to filled -:#: Diameter in tiles (default = 0, does nothing) -Action: -------- -:set: Set designation (default) -:unset: Unset current designation -:invert: Invert designations already present +Job management +============== -Designation types: ------------------- -:dig: Normal digging designation (default) -:ramp: Ramp digging -:ustair: Staircase up -:dstair: Staircase down -:xstair: Staircase up/down -:chan: Dig channel +job +--- +Command for general job query and manipulation. -After you have set the options, the command called with no options -repeats with the last selected parameters. +Options: + *no extra options* + Print details of the current job. The job can be selected + in a workshop, or the unit/jobs screen. + **list** + Print details of all jobs in the selected workshop. + **item-material ** + Replace the exact material id in the job item. + **item-type ** + Replace the exact item type id in the job item. -Examples: ---------- -* 'digcircle filled 3' = Dig a filled circle with radius = 3. -* 'digcircle' = Do it again. +job-material +------------ +Alter the material of the selected job. -weather -======= -Prints the current weather map by default. +Invoked as:: -Also lets you change the current weather to 'clear sky', 'rainy' or 'snowing'. + job-material -Options: --------- -:snow: make it snow everywhere. -:rain: make it rain. -:clear: clear the sky. +Intended to be used as a keybinding: + + * In 'q' mode, when a job is highlighted within a workshop or furnace, + changes the material of the job. Only inorganic materials can be used + in this mode. + * In 'b' mode, during selection of building components positions the cursor + over the first available choice with the matching material. + +job-duplicate +------------- +Duplicate the selected job in a workshop: + * In 'q' mode, when a job is highlighted within a workshop or furnace building, + instantly duplicates the job. workflow -======== +-------- Manage control of repeat jobs. -Usage ------ -``workflow enable [option...], workflow disable [option...]`` +Usage: + + ``workflow enable [option...], workflow disable [option...]`` If no options are specified, enables or disables the plugin. Otherwise, enables or disables any of the following options: - drybuckets: Automatically empty abandoned water buckets. - auto-melt: Resume melt jobs when there are objects to melt. -``workflow jobs`` + ``workflow jobs`` List workflow-controlled jobs (if in a workshop, filtered by it). -``workflow list`` + ``workflow list`` List active constraints, and their job counts. -``workflow count [cnt-gap], workflow amount [cnt-gap]`` + ``workflow count [cnt-gap], workflow amount [cnt-gap]`` Set a constraint. The first form counts each stack as only 1 item. -``workflow unlimit `` + ``workflow unlimit `` Delete a constraint. Function --------- +........ + When the plugin is enabled, it protects all repeat jobs from removal. If they do disappear due to any cause, they are immediately re-added to their workshop and suspended. @@ -1062,9 +1276,13 @@ 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 examples -------------------- +................... + Keep metal bolts within 900-1000, and wood/bone within 150-200. :: @@ -1105,73 +1323,76 @@ Make sure there are always 80-100 units of dimple dye. on the Mill Plants job to MUSHROOM_CUP_DIMPLE using the 'job item-material' command. -mapexport -========= -Export the current loaded map as a file. This will be eventually usable -with visualizers. -dwarfexport -=========== -Export dwarves to RuneSmith-compatible XML. +Fortress activity management +============================ + +seedwatch +--------- +Tool for turning cooking of seeds and plants on/off depending on how much you +have of them. + +See 'seedwatch help' for detailed description. zone -==== +---- Helps a bit with managing activity zones (pens, pastures and pits) and cages. Options: --------- -:set: Set zone or cage under cursor as default for future assigns. -:assign: Assign unit(s) to the pen or pit marked with the 'set' command. + + :set: Set zone or cage under cursor as default for future assigns. + :assign: Assign unit(s) to the pen or pit marked with the 'set' command. If no filters are set a unit must be selected in the in-game ui. Can also be followed by a valid zone id which will be set instead. -:unassign: Unassign selected creature from it's zone. -:nick: Mass-assign nicknames, must be followed by the name you want + :unassign: Unassign selected creature from it's zone. + :nick: Mass-assign nicknames, must be followed by the name you want to set. -:remnick: Mass-remove nicknames. -:tocages: Assign unit(s) to cages inside a pasture. -:uinfo: Print info about unit(s). If no filters are set a unit must + :remnick: Mass-remove nicknames. + :tocages: Assign unit(s) to cages inside a pasture. + :uinfo: Print info about unit(s). If no filters are set a unit must be selected in the in-game ui. -:zinfo: Print info about zone(s). If no filters are set zones under + :zinfo: Print info about zone(s). If no filters are set zones under the cursor are listed. -:verbose: Print some more info. -:filters: Print list of valid filter options. -:examples: Print some usage examples. -:not: Negates the next filter keyword. + :verbose: Print some more info. + :filters: Print list of valid filter options. + :examples: Print some usage examples. + :not: Negates the next filter keyword. Filters: --------- -:all: Process all units (to be used with additional filters). -:count: Must be followed by a number. Process only n units (to be used - with additional filters). -:unassigned: Not assigned to zone, chain or built cage. -:minage: Minimum age. Must be followed by number. -:maxage: Maximum age. Must be followed by number. -:race: Must be followed by a race RAW ID (e.g. BIRD_TURKEY, ALPACA, - etc). Negatable. -:caged: In a built cage. Negatable. -:own: From own civilization. Negatable. -:merchant: Is a merchant / belongs to a merchant. Should only be used for - pitting, not for stealing animals (slaughter should work). -:war: Trained war creature. Negatable. -:hunting: Trained hunting creature. Negatable. -:tamed: Creature is tame. Negatable. -:trained: Creature is trained. Finds war/hunting creatures as well as - creatures who have a training level greater than 'domesticated'. - If you want to specifically search for war/hunting creatures use - 'war' or 'hunting' Negatable. -:trainablewar: Creature can be trained for war (and is not already trained for - war/hunt). Negatable. -:trainablehunt: Creature can be trained for hunting (and is not already trained - for war/hunt). Negatable. -:male: Creature is male. Negatable. -:female: Creature is female. Negatable. -:egglayer: Race lays eggs. Negatable. -:grazer: Race is a grazer. Negatable. -:milkable: Race is milkable. Negatable. + + :all: Process all units (to be used with additional filters). + :count: Must be followed by a number. Process only n units (to be used + with additional filters). + :unassigned: Not assigned to zone, chain or built cage. + :minage: Minimum age. Must be followed by number. + :maxage: Maximum age. Must be followed by number. + :race: Must be followed by a race RAW ID (e.g. BIRD_TURKEY, ALPACA, + etc). Negatable. + :caged: In a built cage. Negatable. + :own: From own civilization. Negatable. + :merchant: Is a merchant / belongs to a merchant. Should only be used for + pitting, not for stealing animals (slaughter should work). + :war: Trained war creature. Negatable. + :hunting: Trained hunting creature. Negatable. + :tamed: Creature is tame. Negatable. + :trained: Creature is trained. Finds war/hunting creatures as well as + creatures who have a training level greater than 'domesticated'. + If you want to specifically search for war/hunting creatures use + 'war' or 'hunting' Negatable. + :trainablewar: Creature can be trained for war (and is not already trained for + war/hunt). Negatable. + :trainablehunt: Creature can be trained for hunting (and is not already trained + for war/hunt). Negatable. + :male: Creature is male. Negatable. + :female: Creature is female. Negatable. + :egglayer: Race lays eggs. Negatable. + :grazer: Race is a grazer. Negatable. + :milkable: Race is milkable. Negatable. Usage with single units ------------------------ +....................... + One convenient way to use the zone tool is to bind the command 'zone assign' to a hotkey, maybe also the command 'zone set'. Place the in-game cursor over a pen/pasture or pit, use 'zone set' to mark it. Then you can select units @@ -1180,7 +1401,8 @@ and use 'zone assign' to assign them to their new home. Allows pitting your own dwarves, by the way. Usage with filters ------------------- +.................. + All filters can be used together with the 'assign' command. Restrictions: It's not possible to assign units who are inside built cages @@ -1201,14 +1423,16 @@ are not properly added to your own stocks; slaughtering them should work). Most filters can be negated (e.g. 'not grazer' -> race is not a grazer). Mass-renaming -------------- +............. + Using the 'nick' command you can set the same nickname for multiple units. If used without 'assign', 'all' or 'count' it will rename all units in the current default target zone. Combined with 'assign', 'all' or 'count' (and further optional filters) it will rename units matching the filter conditions. Cage zones ----------- +.......... + Using the 'tocages' command you can assign units to a set of cages, for example a room next to your butcher shop(s). They will be spread evenly among available cages to optimize hauling to and butchering from them. For this to work you need @@ -1219,7 +1443,8 @@ would make no sense, but can be used together with 'nick' or 'remnick' and all the usual filters. Examples --------- +........ + ``zone assign all own ALPACA minage 3 maxage 10`` Assign all own alpacas who are between 3 and 10 years old to the selected pasture. @@ -1242,7 +1467,7 @@ Examples on the current default zone. autonestbox -=========== +----------- Assigns unpastured female egg-layers to nestbox zones. Requires that you create pen/pasture zones above nestboxes. If the pen is bigger than 1x1 the nestbox must be in the top left corner. Only 1 unit will be assigned per pen, regardless @@ -1254,15 +1479,15 @@ nestbox zones when they revert to wild. When called without options autonestbox will instantly run once. Options: --------- -:start: Start running every X frames (df simulation ticks). - Default: X=6000, which would be every 60 seconds at 100fps. -:stop: Stop running automatically. -:sleep: Must be followed by number X. Changes the timer to sleep X - frames between runs. + + :start: Start running every X frames (df simulation ticks). + Default: X=6000, which would be every 60 seconds at 100fps. + :stop: Stop running automatically. + :sleep: Must be followed by number X. Changes the timer to sleep X + frames between runs. autobutcher -=========== +----------- Assigns lifestock for slaughter once it reaches a specific count. Requires that you add the target race(s) to a watch list. Only tame units will be processed. @@ -1281,41 +1506,41 @@ If you don't set any target count the following default will be used: 1 male kid, 5 female kids, 1 male adult, 5 female adults. Options: --------- -:start: Start running every X frames (df simulation ticks). - Default: X=6000, which would be every 60 seconds at 100fps. -:stop: Stop running automatically. -:sleep: Must be followed by number X. Changes the timer to sleep - X frames between runs. -:watch R: Start watching a race. R can be a valid race RAW id (ALPACA, - BIRD_TURKEY, etc) or a list of ids seperated by spaces or - the keyword 'all' which affects all races on your current - watchlist. -:unwatch R: Stop watching race(s). The current target settings will be - remembered. R can be a list of ids or the keyword 'all'. -:forget R: Stop watching race(s) and forget it's/their target settings. - R can be a list of ids or the keyword 'all'. -:autowatch: Automatically adds all new races (animals you buy from merchants, - tame yourself or get from migrants) to the watch list using - default target count. -:noautowatch: Stop auto-adding new races to the watchlist. -:list: Print the current status and watchlist. -:list_export: Print status and watchlist in a format which can be used - to import them to another savegame (see notes). -:target fk mk fa ma R: Set target count for specified race(s). - fk = number of female kids, - mk = number of male kids, - fa = number of female adults, - ma = number of female adults. - R can be a list of ids or the keyword 'all' or 'new'. - R = 'all': change target count for all races on watchlist - and set the new default for the future. R = 'new': don't touch - current settings on the watchlist, only set the new default - for future entries. -:example: Print some usage examples. + + :start: Start running every X frames (df simulation ticks). + Default: X=6000, which would be every 60 seconds at 100fps. + :stop: Stop running automatically. + :sleep: Must be followed by number X. Changes the timer to sleep + X frames between runs. + :watch R: Start watching a race. R can be a valid race RAW id (ALPACA, + BIRD_TURKEY, etc) or a list of ids seperated by spaces or + the keyword 'all' which affects all races on your current + watchlist. + :unwatch R: Stop watching race(s). The current target settings will be + remembered. R can be a list of ids or the keyword 'all'. + :forget R: Stop watching race(s) and forget it's/their target settings. + R can be a list of ids or the keyword 'all'. + :autowatch: Automatically adds all new races (animals you buy from merchants, + tame yourself or get from migrants) to the watch list using + default target count. + :noautowatch: Stop auto-adding new races to the watchlist. + :list: Print the current status and watchlist. + :list_export: Print status and watchlist in a format which can be used + to import them to another savegame (see notes). + :target fk mk fa ma R: Set target count for specified race(s). + fk = number of female kids, + mk = number of male kids, + fa = number of female adults, + ma = number of female adults. + R can be a list of ids or the keyword 'all' or 'new'. + R = 'all': change target count for all races on watchlist + and set the new default for the future. R = 'new': don't touch + current settings on the watchlist, only set the new default + for future entries. + :example: Print some usage examples. Examples: ---------- + You want to keep max 7 kids (4 female, 3 male) and max 3 adults (2 female, 1 male) of the race alpaca. Once the kids grow up the oldest adults will get slaughtered. Excess kids will get slaughtered starting with the youngest @@ -1347,8 +1572,8 @@ add some new races with 'watch'. If you simply want to stop it completely use autobutcher unwatch ALPACA CAT -Note: ------ +**Note:** + Settings and watchlist are stored in the savegame, so that you can have different settings for each world. If you want to copy your watchlist to another savegame you can use the command list_export: @@ -1362,7 +1587,7 @@ another savegame you can use the command list_export: autolabor -========= +--------- Automatically manage dwarf labors. When enabled, autolabor periodically checks your dwarves and enables or @@ -1376,6 +1601,112 @@ also tries to have dwarves specialize in specific skills. For detailed usage information, see 'help autolabor'. +Other +===== + +catsplosion +----------- +Makes cats just *multiply*. It is not a good idea to run this more than once or +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 + +Confirmed working DFusion plugins: + +: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. + +misery +------ +When enabled, every new negative dwarven thought will be multiplied by a factor (2 by default). + +Usage: + +:misery enable n: enable misery with optional magnitude n. If specified, n must be positive. +:misery n: same as "misery enable n" +:misery enable: same as "misery enable 2" +:misery disable: stop adding new negative thoughts. This will not remove existing duplicated thoughts. Equivalent to "misery 1" +:misery clear: remove fake thoughts added in this session of DF. Saving makes them permanent! Does not change factor. + +======= +Scripts +======= + +Lua or ruby scripts placed in the hack/scripts/ directory are considered for +execution as if they were native DFHack commands. They are listed at the end +of the 'ls' command output. + +Note: scripts in subdirectories of hack/scripts/ can still be called, but will +only be listed by ls if called as 'ls -a'. This is intended as a way to hide +scripts that are obscure, developer-oriented, or should be used as keybindings. + +Some notable scripts: + +fix/* +===== + +Scripts in this subdirectory fix various bugs and issues, some of them obscure. + +* fix/dead-units + + Removes uninteresting dead units from the unit list. Doesn't seem to give any + noticeable performance gain, but migrants normally stop if the unit list grows + to around 3000 units, and this script reduces it back. + +* fix/population-cap + + Run this after every migrant wave to ensure your population cap is not exceeded. + The issue with the cap is that it is compared to the population number reported + by the last caravan, so once it drops below the cap, migrants continue to come + until that number is updated again. + +* fix/stable-temp + + Instantly sets the temperature of all free-lying items to be in equilibrium with + the environment and stops temperature updates. In order to maintain this efficient + state however, use ``tweak stable-temp`` and ``tweak fast-heat``. + +* fix/item-occupancy + + Diagnoses and fixes issues with nonexistant 'items occupying site', usually + caused by autodump bugs or other hacking mishaps. + + +gui/* +===== + +Scripts that implement dialogs inserted into the main game window are put in this +directory. + +quicksave +========= + +If called in dwarf mode, makes DF immediately auto-save the game by setting a flag +normally used in seasonal auto-save. + +setfps +====== + +Run ``setfps `` to set the FPS cap at runtime, in case you want to watch +combat in slow motion or something :) + +siren +===== + +Wakes up sleeping units, cancels breaks and stops parties either everywhere, +or in the burrows given as arguments. In return, adds bad thoughts about +noise, tiredness and lack of protection. Also, the units with interrupted +breaks will go on break again a lot sooner. The script is intended for +emergencies, e.g. when a siege appears, and all your military is partying. growcrops ========= @@ -1397,16 +1728,18 @@ 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 @@ -1415,7 +1748,7 @@ Kills any unit of a given race. With no argument, lists the available races. -With the special argument 'him', targets only the selected creature. +With the special argument ``him``, targets only the selected creature. 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 @@ -1461,6 +1794,49 @@ To remove all placed sources, call ``magmasource stop``. With no argument, this command shows an help message and list existing sources. +digfort +======= +A script to designate an area for digging according to a plan in csv format. + +This script, inspired from quickfort, can designate an area for digging. +Your plan should be stored in a .csv file like this:: + + # this is a comment + d;d;u;d;d;skip this tile;d + d;d;d;i + +Available tile shapes are named after the 'dig' menu shortcuts: +``d`` for dig, ``u`` for upstairs, ``d`` downstairs, ``i`` updown, +``h`` channel, ``r`` upward ramp, ``x`` remove designation. +Unrecognized characters are ignored (eg the 'skip this tile' in the sample). + +Empty lines and data after a ``#`` are ignored as comments. +To skip a row in your design, use a single ``;``. + +The script takes the plan filename, starting from the root df folder. + +superdwarf +========== +Similar to fastdwarf, per-creature. + +To make any creature superfast, target it ingame using 'v' and:: + + superdwarf add + +Other options available: ``del``, ``clear``, ``list``. + +This plugin also shortens the 'sleeping' and 'on break' periods of targets. + +drainaquifer +============ +Remove all 'aquifer' tag from the map blocks. Irreversible. + +deathcause +========== +Focus a body part ingame, and this script will display the cause of death of +the creature. + + ======================= In-game interface tools ======================= @@ -1468,6 +1844,14 @@ In-game interface tools These tools work by displaying dialogs or overlays in the game window, and are mostly implemented by lua scripts. +.. note:: + + In order to avoid user confusion, as a matter of policy all these tools + display the word "DFHack" on the screen somewhere while active. + + 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. + Dwarf Manipulator ================= @@ -1486,14 +1870,15 @@ Use the arrow keys or number pad to move the cursor around, holding Shift to move 10 tiles at a time. Press the Z-Up (<) and Z-Down (>) keys to move quickly between labor/skill -categories. +categories. The numpad Z-Up and Z-Down keys seek to the first or last unit +in the list. Backspace seeks to the top left corner. Press Enter to toggle the selected labor for the selected unit, or Shift+Enter to toggle all labors within the selected category. Press the ``+-`` keys to sort the unit list according to the currently selected skill/labor, and press the ``*/`` keys to sort the unit list by Name, Profession, -or Happiness (using Tab to select which sort method to use here). +Happiness, or Arrival order (using Tab to select which sort method to use here). With a unit selected, you can press the "v" key to view its properties (and possibly set a custom nickname or profession) or the "c" key to exit @@ -1509,19 +1894,23 @@ The following mouse shortcuts are also available: * Left-click on a unit's name or profession to view its properties. * Right-click on a unit's name or profession to zoom to it. -Liquids -======= +Pressing ESC normally returns to the unit screen, but Shift-ESC would exit +directly to the main dwarf mode screen. + + +gui/liquids +=========== -Implemented by the gui/liquids script. To use, bind to a key and activate in the 'k' mode. +To use, bind to a key and activate in the 'k' mode. While active, use the suggested keys to switch the usual liquids parameters, and Enter to select the target area and apply changes. -Mechanisms -========== +gui/mechanisms +============== -Implemented by the gui/mechanims script. To use, bind to a key and activate in the 'q' mode. +To use, bind to a key and activate in the 'q' mode. Lists mechanisms connected to the building, and their links. Navigating the list centers the view on the relevant linked buildings. @@ -1531,48 +1920,180 @@ focus on the current one. Shift-Enter has an effect equivalent to pressing Enter re-entering the mechanisms ui. -Power Meter -=========== - -Front-end to the power-meter plugin implemented by the gui/power-meter script. Bind to a -key and activate after selecting Pressure Plate in the build menu. - -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. - - -Rename -====== +gui/rename +========== -Backed by the rename plugin, the gui/rename script allows entering the desired name +Backed by the rename plugin, this script allows entering the desired name via a simple dialog in the game ui. * ``gui/rename [building]`` in 'q' mode changes the name of a building. - The selected building must be one of stockpile, workshop, furnace, trap or siege engine. + 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. * ``gui/rename unit-profession`` changes the selected unit's custom profession name. -The ``building`` or ``unit`` are automatically assumed when in relevant ui state. +The ``building`` or ``unit`` options are automatically assumed when in relevant ui state. -Room List -========= +gui/room-list +============= -Implemented by the gui/room-list script. 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 and activate in the 'q' mode, either immediately or after opening +the assign owner page. The script lists other rooms owned by the same owner, or by the unit selected in the assign list, and allows unassigning them. +gui/choose-weapons +================== + +Bind to a key, 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 +unit's top skill. If the cursor is in the rightmost list over a weapon entry, it rewrites +only that entry, and does it even if it is not 'individual choice'. + +Rationale: individual choice seems to be unreliable when there is a weapon shortage, +and may lead to inappropriate weapons being selected. + + +gui/guide-path +============== + +Bind to a key, and activate in the Hauling menu with the cursor over a Guide order. + +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, and activate with a job selected in a workshop in the 'q' mode. + +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. Pressing 'm', unless the item type does not allow a material, +lets you choose a material. + +.. 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, and activate with a job selected in a workshop in the 'q' mode. + +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 'c' switches the current constraint between counting stacks or items. +Pressing 'm' lets you input the range directly; 'e', 'r', 'd', 'f' adjust the +bounds by 1, 5, or 25 depending on the direction and the 'c' setting (counting +items and expanding the range each gives a 5x bonus). + +Pressing 'n' produces a list of possible outputs of this job as guessed by +workflow, and lets you create a new constraint by just choosing one. 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. + + +gui/assign-rack +=============== + +Bind to a key, and activate when viewing a weapon rack in the 'q' mode. + +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**. + Check the comments for this bug to get it: + 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. + + +============= +Behavior Mods +============= + +These plugins, when activated via configuration UI or by detecting certain +structures in RAWs, modify the game engine behavior concerning the target +objects to add features not otherwise present. + +.. admonition:: DISCLAIMER + + The plugins in this section have mostly been created for fun as an interesting + technical challenge, and do not represent any long-term plans to produce more + similar modifications of the game. + + Siege Engine ============ -Front-end to the siege-engine plugin implemented by the gui/siege-engine script. Bind to a -key and activate after selecting a siege engine in 'q' mode. +The siege-engine plugin enables siege engines to be linked to stockpiles, and +aimed at an arbitrary rectangular area across Z levels, instead of the original +four directions. Also, catapults can be ordered to load arbitrary objects, not +just stones. + +Rationale +--------- + +Siege engines are a very interesting feature, but sadly almost useless in the current state +because they haven't been updated since 2D and can only aim in four directions. This is an +attempt to bring them more up to date until Toady has time to work on it. Actual improvements, +e.g. like making siegers bring their own, are something only Toady can do. + +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. 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 @@ -1581,7 +2102,8 @@ yellow for partially blocked. Pressing 'r' changes into the target selection mode, which works by highlighting two points with Enter like all designations. When a target area is set, the engine projectiles are -aimed at that area, or units within it, instead of the vanilla four directions. +aimed at that area, or units within it (this doesn't actually change the original aiming +code, instead the projectile trajectory parameters are rewritten as soon as it appears). After setting the target in this way for one engine, you can 'paste' the same area into others just by pressing 'p' in the main page of this script. The area to paste is kept until you quit @@ -1593,17 +2115,18 @@ Exiting from the siege engine script via ESC reverts the view to the state prior the script. Shift-ESC retains the current viewport, and also exits from the 'q' mode to main menu. -**DISCLAIMER**: Siege engines are a very interesting feature, but currently nearly useless -because they haven't been updated since 2D and can only aim in four directions. This is an -attempt to bring them more up to date until Toady has time to work on it. Actual improvements, -e.g. like making siegers bring their own, are something only Toady can do. +Power Meter +=========== -========= -RAW hacks -========= +The power-meter plugin implements a modified pressure plate that detects power being +supplied to gear boxes built in the four adjacent N/S/W/E tiles. -These plugins detect certain structures in RAWs, and enhance them in various ways. +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. + +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. Steam Engine @@ -1632,20 +2155,24 @@ The workshop needs water as its input, which it takes via a passable floor tile below it, like usual magma workshops do. The magma version also needs magma. -**ISSUE**: Since this building is a machine, and machine collapse -code cannot be modified, it would collapse over true open space. -As a loophole, down stair provides support to machines, while -being passable, so use them. +.. admonition:: ISSUE + + Since this building is a machine, and machine collapse + code cannot be hooked, it would collapse over true open space. + As a loophole, down stair provides support to machines, while + being passable, so use them. After constructing the building itself, machines can be connected to the edge tiles that look like gear boxes. Their exact position is extracted from the workshop raws. -**ISSUE**: Like with collapse above, part of the code involved in -machine connection cannot be modified. As a result, the workshop -can only immediately connect to machine components built AFTER it. -This also means that engines cannot be chained without intermediate -short axles that can be built later. +.. admonition:: ISSUE + + Like with collapse above, part of the code involved in + machine connection cannot be hooked. As a result, the workshop + can only immediately connect to machine components built AFTER it. + This also means that engines cannot be chained without intermediate + short axles that can be built later than both of the engines. Operation --------- @@ -1655,8 +2182,13 @@ on repeat). A furnace operator will come, possibly bringing a bar of fuel, and perform it. As a result, a "boiling water" item will appear in the 't' view of the workshop. -**NOTE**: The completion of the job will actually consume one unit -of the appropriate liquids from below the workshop. +.. note:: + + The completion of the job will actually consume one unit + of the appropriate liquids from below the workshop. This means + that you cannot just raise 7 units of magma with a piston and + have infinite power. However, liquid consumption should be slow + enough that water can be supplied by a pond zone bucket chain. Every such item gives 100 power, up to a limit of 300 for coal, and 500 for a magma engine. The building can host twice that @@ -1704,9 +2236,12 @@ Add Spatter =========== This plugin makes reactions with names starting with ``SPATTER_ADD_`` -produce contaminants on the items instead of improvements. +produce contaminants on the items instead of improvements. The produced +contaminants are immune to being washed away by water or destroyed by +the ``clean items`` command. -Intended to give some use to all those poisons that can be bought from caravans. +The plugin is intended to give some use to all those poisons that can +be bought from caravans. :) To be really useful this needs patches from bug 808, ``tweak fix-dimensions`` and ``tweak advmode-contained``. diff --git a/depends/clsocket b/depends/clsocket index d0b2d0750..178a4916d 160000 --- a/depends/clsocket +++ b/depends/clsocket @@ -1 +1 @@ -Subproject commit d0b2d0750dc2d529a152eba4f3f519f69ff7eab0 +Subproject commit 178a4916da838e46e46e769e566e47fff6eff8f8 diff --git a/depends/md5/md5wrapper.cpp b/depends/md5/md5wrapper.cpp index e12b65780..d9f857c5d 100644 --- a/depends/md5/md5wrapper.cpp +++ b/depends/md5/md5wrapper.cpp @@ -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 diff --git a/depends/md5/md5wrapper.h b/depends/md5/md5wrapper.h index 1a41192a1..0b534b61d 100644 --- a/depends/md5/md5wrapper.h +++ b/depends/md5/md5wrapper.h @@ -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(data),size); + } + /* * creates a MD5 hash from * a file specified in "filename" and diff --git a/dfhack.init-example b/dfhack.init-example index 83c3641b6..d7f3f5399 100644 --- a/dfhack.init-example +++ b/dfhack.init-example @@ -18,7 +18,7 @@ keybinding add Ctrl-Alt-S@dwarfmode/Default quicksave # gui/rename script keybinding add Ctrl-Shift-N gui/rename -keybinding add Ctrl-Shift-P "gui/rename unit-profession" +keybinding add Alt-Shift-P "gui/rename unit-profession" ############################## # Generic adv mode bindings # @@ -45,8 +45,15 @@ keybinding add Shift-R "job-material RHYOLITE" keybinding add Shift-I "job-material CINNABAR" keybinding add Shift-B "job-material COBALTITE" 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 +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" +keybinding add Alt-Shift-Q "sort-units squad_position" "sort-items quality" + # browse linked mechanisms keybinding add Ctrl-M@dwarfmode/QueryBuilding/Some gui/mechanisms @@ -62,6 +69,21 @@ keybinding add Ctrl-Shift-M@dwarfmode/Build/Position/Trap gui/power-meter # siege engine control 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 + +# 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 # ############################ @@ -90,3 +112,12 @@ tweak fix-dimensions # make reactions requiring containers usable in advmode - the issue is # that the screen asks for those reagents to be selected directly tweak advmode-contained + +# support Shift-Enter in Trade and Move Goods to Depot screens for faster +# selection; it selects the current item or stack and scrolls down one line +tweak fast-trade + +# stop the right list in military->positions from resetting to top all the time +tweak military-stable-assign +# in same list, color units already assigned to squads in brown & green +tweak military-color-assigned diff --git a/fixTexts.sh b/fixTexts.sh index 669fa068b..ff277338c 100755 --- a/fixTexts.sh +++ b/fixTexts.sh @@ -1,5 +1,18 @@ #!/bin/bash -rst2html README.rst > Readme.html -rst2html COMPILE.rst > Compile.html -rst2html DEVEL.rst > Devel.html -rst2html LUA_API.rst > Lua\ API.html +# regenerate documentation after editing the .rst files. Requires python and docutils. + +cd `dirname $0` + +function process() { + if [ "$1" -nt "$2" ]; then + rst2html --no-generator --no-datestamp "$1" "$2" + else + echo "$2 - up to date." + fi +} + +process Readme.rst Readme.html +process Compile.rst Compile.html +process Lua\ API.rst Lua\ API.html +process Contributors.rst Contributors.html + diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index 536f4d34d..6f33d5c8a 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -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}) diff --git a/library/Core.cpp b/library/Core.cpp index 735359a7f..26c0acbb0 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 @@ -126,8 +126,34 @@ struct Core::Private void Core::cheap_tokenise(string const& input, vector &output) { string *cur = NULL; + size_t i = 0; - for (size_t i = 0; i < input.size(); i++) { + // Check the first non-space character + while (i < input.size() && isspace(input[i])) i++; + + // Special verbatim argument mode? + if (i < input.size() && input[i] == ':') + { + // Read the command + std::string cmd; + i++; + while (i < input.size() && !isspace(input[i])) + cmd.push_back(input[i++]); + if (!cmd.empty()) + output.push_back(cmd); + + // Find the argument + while (i < input.size() && isspace(input[i])) i++; + + if (i < input.size()) + output.push_back(input.substr(i)); + + return; + } + + // Otherwise, parse in the regular quoted mode + for (; i < input.size(); i++) + { unsigned char c = input[i]; if (isspace(c)) { cur = NULL; @@ -348,6 +374,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) { @@ -601,8 +629,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") @@ -795,6 +822,8 @@ std::string Core::getHackPath() #endif } +void init_screen_module(Core *); + bool Core::Init() { if(started) @@ -865,6 +894,7 @@ bool Core::Init() */ // initialize data defs virtual_identity::Init(this); + init_screen_module(this); // initialize common lua context Lua::Core::Init(con); @@ -1096,7 +1126,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) @@ -1114,7 +1144,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); } } @@ -1585,15 +1615,27 @@ void ClassNameCheck::getKnownClassNames(std::vector &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 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++; @@ -1616,23 +1658,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; - for (unsigned i = start; i < end && ok; i++) - setPermisions(ranges[i], ranges[i]); + memmove(target, src, size); + return true; +} + +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); } /******************************************************************************* @@ -1652,7 +1716,6 @@ TYPE * Core::get##TYPE() \ return s_mods.p##TYPE;\ } -MODULE_GETTER(World); MODULE_GETTER(Materials); MODULE_GETTER(Notes); MODULE_GETTER(Graphic); diff --git a/library/DataDefs.cpp b/library/DataDefs.cpp index fa2aacf78..93e75e280 100644 --- a/library/DataDefs.cpp +++ b/library/DataDefs.cpp @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/Hooks-darwin.cpp b/library/Hooks-darwin.cpp index 190616b86..31e191e1b 100644 --- a/library/Hooks-darwin.cpp +++ b/library/Hooks-darwin.cpp @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/Hooks-egg.cpp b/library/Hooks-egg.cpp index 8abd0ae8a..842573eca 100644 --- a/library/Hooks-egg.cpp +++ b/library/Hooks-egg.cpp @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/Hooks-linux.cpp b/library/Hooks-linux.cpp index ef1a765cb..31c0323f6 100644 --- a/library/Hooks-linux.cpp +++ b/library/Hooks-linux.cpp @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/Hooks-windows.cpp b/library/Hooks-windows.cpp index a586a0e85..1e6679855 100644 --- a/library/Hooks-windows.cpp +++ b/library/Hooks-windows.cpp @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index d73d14cf9..e7424ad50 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1,6 +1,6 @@ -/* +/* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 @@ -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 #include @@ -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 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(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(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); @@ -760,6 +794,7 @@ static const LuaWrapper::FunctionReg dfhack_gui_module[] = { WRAPM(Gui, getSelectedJob), WRAPM(Gui, getSelectedUnit), WRAPM(Gui, getSelectedItem), + WRAPM(Gui, getSelectedBuilding), WRAPM(Gui, showAnnouncement), WRAPM(Gui, showZoomAnnouncement), WRAPM(Gui, showPopupAnnouncement), @@ -780,6 +815,8 @@ static const LuaWrapper::FunctionReg dfhack_job_module[] = { 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 } @@ -827,6 +864,7 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = { WRAPM(Units, isDwarf), WRAPM(Units, isCitizen), WRAPM(Units, getAge), + WRAPM(Units, getNominalSkill), WRAPM(Units, getEffectiveSkill), WRAPM(Units, computeMovementSpeed), WRAPM(Units, getProfessionName), @@ -904,7 +942,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), @@ -935,6 +978,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), @@ -942,6 +1001,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 } }; @@ -966,6 +1029,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); @@ -983,6 +1065,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 } @@ -1144,6 +1228,7 @@ static const LuaWrapper::FunctionReg dfhack_screen_module[] = { WRAPM(Screen, inGraphicsMode), WRAPM(Screen, clear), WRAPM(Screen, invalidate), + WRAPM(Screen, getKeyDisplay), { NULL, NULL } }; @@ -1452,6 +1537,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,6 +1703,7 @@ static const luaL_Reg dfhack_internal_funcs[] = { { "getVTable", internal_getVTable }, { "getMemRanges", internal_getMemRanges }, { "patchMemory", internal_patchMemory }, + { "patchBytes", internal_patchBytes }, { "memmove", internal_memmove }, { "memcmp", internal_memcmp }, { "memscan", internal_memscan }, diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp index a283d215c..c052b88aa 100644 --- a/library/LuaTools.cpp +++ b/library/LuaTools.cpp @@ -1,6 +1,6 @@ -/* +/* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 @@ -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); @@ -1814,7 +1817,9 @@ void DFHack::Lua::Core::onUpdate(color_ostream &out) lua_rawgetp(State, LUA_REGISTRYINDEX, &DFHACK_TIMEOUTS_TOKEN); run_timers(out, State, frame_timers, frame[1], ++frame_idx); - run_timers(out, State, tick_timers, frame[1], world->frame_counter); + + if (world) + run_timers(out, State, tick_timers, frame[1], world->frame_counter); } void DFHack::Lua::Core::Init(color_ostream &out) diff --git a/library/LuaTypes.cpp b/library/LuaTypes.cpp index 9f2689fa9..877609e56 100644 --- a/library/LuaTypes.cpp +++ b/library/LuaTypes.cpp @@ -1,6 +1,6 @@ -/* +/* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/LuaWrapper.cpp b/library/LuaWrapper.cpp index 9f2fc2f16..1ce405c29 100644 --- a/library/LuaWrapper.cpp +++ b/library/LuaWrapper.cpp @@ -1,6 +1,6 @@ -/* +/* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 @@ -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 &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 &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; diff --git a/library/MiscUtils.cpp b/library/MiscUtils.cpp index b73d730c1..53c403026 100644 --- a/library/MiscUtils.cpp +++ b/library/MiscUtils.cpp @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 @@ -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() { diff --git a/library/PlugLoad-windows.cpp b/library/PlugLoad-windows.cpp index 714960e64..df8397f96 100644 --- a/library/PlugLoad-windows.cpp +++ b/library/PlugLoad-windows.cpp @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/PluginManager.cpp b/library/PluginManager.cpp index ceb644e60..0c80639b4 100644 --- a/library/PluginManager.cpp +++ b/library/PluginManager.cpp @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/Process-darwin.cpp b/library/Process-darwin.cpp index 3893cfc5f..c5e4e4b85 100644 --- a/library/Process-darwin.cpp +++ b/library/Process-darwin.cpp @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/Process-linux.cpp b/library/Process-linux.cpp index 1fecbab78..f88279b3f 100644 --- a/library/Process-linux.cpp +++ b/library/Process-linux.cpp @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/Process-windows.cpp b/library/Process-windows.cpp index db58c4d33..966468fb5 100644 --- a/library/Process-windows.cpp +++ b/library/Process-windows.cpp @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/RemoteTools.cpp b/library/RemoteTools.cpp index b371d60fa..718d1b181 100644 --- a/library/RemoteTools.cpp +++ b/library/RemoteTools.cpp @@ -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); } } diff --git a/library/TileTypes.cpp b/library/TileTypes.cpp index 5564e5e3f..804d5e772 100644 --- a/library/TileTypes.cpp +++ b/library/TileTypes.cpp @@ -1,6 +1,6 @@ -/* +/* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/Types.cpp b/library/Types.cpp index 72f8bdb8c..1ba87f839 100644 --- a/library/Types.cpp +++ b/library/Types.cpp @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/VTableInterpose.cpp b/library/VTableInterpose.cpp index 046425653..3f9423b45 100644 --- a/library/VTableInterpose.cpp +++ b/library/VTableInterpose.cpp @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 @@ -166,12 +166,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*)); } /* @@ -247,8 +247,9 @@ void VMethodInterposeLinkBase::set_chain(void *chain) addr_to_method_pointer_(chain_mptr, chain); } -VMethodInterposeLinkBase::VMethodInterposeLinkBase(virtual_identity *host, int vmethod_idx, void *interpose_method, void *chain_mptr) - : host(host), vmethod_idx(vmethod_idx), interpose_method(interpose_method), chain_mptr(chain_mptr), +VMethodInterposeLinkBase::VMethodInterposeLinkBase(virtual_identity *host, int vmethod_idx, void *interpose_method, void *chain_mptr, int priority) + : host(host), vmethod_idx(vmethod_idx), interpose_method(interpose_method), + chain_mptr(chain_mptr), priority(priority), applied(false), saved_chain(NULL), next(NULL), prev(NULL) { if (vmethod_idx < 0 || interpose_method == NULL) @@ -280,9 +281,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++) { @@ -297,17 +299,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) @@ -327,7 +344,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); @@ -349,15 +368,28 @@ bool VMethodInterposeLinkBase::apply(bool enable) return false; // Retrieve the current vtable entry - void *old_ptr = host->get_vmethod_ptr(vmethod_idx); VMethodInterposeLinkBase *old_link = host->interpose_list[vmethod_idx]; + VMethodInterposeLinkBase *next_link = NULL; + + while (old_link && old_link->host == host && old_link->priority > priority) + { + next_link = old_link; + old_link = old_link->prev; + } + void *old_ptr = next_link ? next_link->saved_chain : host->get_vmethod_ptr(vmethod_idx); assert(old_ptr != NULL && (!old_link || old_link->interpose_method == old_ptr)); // Apply the new method ptr + MemoryPatcher patcher; + set_chain(old_ptr); - if (!host->set_vmethod_ptr(vmethod_idx, interpose_method)) + if (next_link) + { + next_link->set_chain(interpose_method); + } + else if (!host->set_vmethod_ptr(patcher, vmethod_idx, interpose_method)) { set_chain(NULL); return false; @@ -365,8 +397,13 @@ bool VMethodInterposeLinkBase::apply(bool enable) // Push the current link into the home host applied = true; - host->interpose_list[vmethod_idx] = this; prev = old_link; + next = next_link; + + if (next_link) + next_link->prev = this; + else + host->interpose_list[vmethod_idx] = this; child_hosts.clear(); child_next.clear(); @@ -374,13 +411,22 @@ bool VMethodInterposeLinkBase::apply(bool enable) if (old_link && old_link->host == host) { // If the old link is home, just push into the plain chain - assert(old_link->next == NULL); + assert(old_link->next == next_link); old_link->next = this; // Child links belong to the topmost local entry child_hosts.swap(old_link->child_hosts); child_next.swap(old_link->child_next); } + else if (next_link) + { + if (old_link) + { + assert(old_link->child_next.count(next_link)); + old_link->child_next.erase(next_link); + old_link->child_next.insert(this); + } + } else { // If creating a new local chain, find children with same vmethod @@ -401,6 +447,8 @@ bool VMethodInterposeLinkBase::apply(bool enable) } } + assert (!next_link || (child_next.empty() && child_hosts.empty())); + // Chain subclass hooks for (auto it = child_next.begin(); it != child_next.end(); ++it) { @@ -415,7 +463,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; } @@ -452,9 +500,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) { @@ -471,7 +521,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); } diff --git a/library/VersionInfoFactory.cpp b/library/VersionInfoFactory.cpp index 4ac53dd1a..e8c0561cd 100644 --- a/library/VersionInfoFactory.cpp +++ b/library/VersionInfoFactory.cpp @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/binpatch.cpp b/library/binpatch.cpp new file mode 100644 index 000000000..60e2c4ca7 --- /dev/null +++ b/library/binpatch.cpp @@ -0,0 +1,315 @@ +/* +https://github.com/peterix/dfhack +Copyright (c) 2011 Petr Mrázek + +A thread-safe logging console with a line editor for windows. + +Based on linenoise win32 port, +copyright 2010, Jon Griffiths . +All rights reserved. +Based on linenoise, copyright 2010, Salvatore Sanfilippo . +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 +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include + +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 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 *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 &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 &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 " << 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 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; +} diff --git a/library/include/BitArray.h b/library/include/BitArray.h index ff68ea1d1..a507a38a4 100644 --- a/library/include/BitArray.h +++ b/library/include/BitArray.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/include/ColorText.h b/library/include/ColorText.h index 50d1f3623..006fa119d 100644 --- a/library/include/ColorText.h +++ b/library/include/ColorText.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/include/Console.h b/library/include/Console.h index ba59324e9..8d848135a 100644 --- a/library/include/Console.h +++ b/library/include/Console.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/include/Core.h b/library/include/Core.h index e1f1cf3fb..b3db50c74 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 @@ -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; diff --git a/library/include/DFHack.h b/library/include/DFHack.h index ac5d78cf0..d606df94b 100644 --- a/library/include/DFHack.h +++ b/library/include/DFHack.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index 61d5dec41..32ffabc32 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 @@ -294,6 +294,7 @@ namespace DFHack #endif class DFHACK_EXPORT VMethodInterposeLinkBase; + class MemoryPatcher; class DFHACK_EXPORT virtual_identity : public struct_identity { static std::map 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, @@ -707,6 +708,9 @@ namespace DFHack { // Global object pointers #include "df/global_objects.h" +#define DF_GLOBAL_VALUE(name,defval) (df::global::name ? *df::global::name : defval) +#define DF_GLOBAL_FIELD(name,fname,defval) (df::global::name ? df::global::name->fname : defval) + // A couple of headers that have to be included at once #include "df/coord2d.h" #include "df/coord.h" diff --git a/library/include/DataFuncs.h b/library/include/DataFuncs.h index 01a798e34..f34b9cc73 100644 --- a/library/include/DataFuncs.h +++ b/library/include/DataFuncs.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/include/DataIdentity.h b/library/include/DataIdentity.h index 21dc68d1a..0b88e0195 100644 --- a/library/include/DataIdentity.h +++ b/library/include/DataIdentity.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/include/Error.h b/library/include/Error.h index 448c1a4f2..4e3ff269c 100644 --- a/library/include/Error.h +++ b/library/include/Error.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/include/Export.h b/library/include/Export.h index 8e00dbaed..c3d007117 100644 --- a/library/include/Export.h +++ b/library/include/Export.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/include/Hooks.h b/library/include/Hooks.h index 325af43eb..7f522a33d 100644 --- a/library/include/Hooks.h +++ b/library/include/Hooks.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/include/Internal.h b/library/include/Internal.h index 12b7852a1..f92814cd4 100644 --- a/library/include/Internal.h +++ b/library/include/Internal.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h index e956a65d5..14397128d 100644 --- a/library/include/LuaTools.h +++ b/library/include/LuaTools.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 @@ -36,6 +36,7 @@ distribution. namespace DFHack { class function_identity_base; + struct MaterialInfo; namespace Units { struct NoblePosition; @@ -283,6 +284,7 @@ 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); template inline void Push(lua_State *state, T *ptr) { PushDFObject(state, ptr); } @@ -486,4 +488,4 @@ namespace DFHack {namespace Lua { DFHack::Lua::Push(state, arg6); \ name##_event.invoke(out, 6); \ } \ -} \ No newline at end of file +} diff --git a/library/include/LuaWrapper.h b/library/include/LuaWrapper.h index 9e4490220..ab694c4a8 100644 --- a/library/include/LuaWrapper.h +++ b/library/include/LuaWrapper.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/include/MemAccess.h b/library/include/MemAccess.h index a226018a6..1b8e687b9 100644 --- a/library/include/MemAccess.h +++ b/library/include/MemAccess.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 @@ -315,5 +315,22 @@ namespace DFHack // Get list of names given to ClassNameCheck constructors. static void getKnownClassNames(std::vector &names); }; + + class DFHACK_EXPORT MemoryPatcher + { + Process *p; + std::vector 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 diff --git a/library/include/MiscUtils.h b/library/include/MiscUtils.h index bbc88a2bb..f5b4e8ded 100644 --- a/library/include/MiscUtils.h +++ b/library/include/MiscUtils.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 @@ -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. diff --git a/library/include/Module.h b/library/include/Module.h index 7433fce49..639edea6a 100644 --- a/library/include/Module.h +++ b/library/include/Module.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/include/ModuleFactory.h b/library/include/ModuleFactory.h index 276ccabb3..1f3d4222a 100644 --- a/library/include/ModuleFactory.h +++ b/library/include/ModuleFactory.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/include/PluginManager.h b/library/include/PluginManager.h index 38f0e2e50..9ef16703a 100644 --- a/library/include/PluginManager.h +++ b/library/include/PluginManager.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/include/Pragma.h b/library/include/Pragma.h index e688810d2..b6f776725 100644 --- a/library/include/Pragma.h +++ b/library/include/Pragma.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/include/RemoteClient.h b/library/include/RemoteClient.h index 37aaea9b1..1e8264cec 100644 --- a/library/include/RemoteClient.h +++ b/library/include/RemoteClient.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/include/RemoteServer.h b/library/include/RemoteServer.h index d89b44959..df2f42712 100644 --- a/library/include/RemoteServer.h +++ b/library/include/RemoteServer.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 @@ -65,6 +65,7 @@ namespace DFHack RPCService *owner, const char *name, int flags) : RPCFunctionBase(in, out), name(name), flags(flags), owner(owner), id(-1) {} + virtual ~ServerFunctionBase() {} RPCService *owner; int16_t id; diff --git a/library/include/RemoteTools.h b/library/include/RemoteTools.h index e87e8026b..e70cefc00 100644 --- a/library/include/RemoteTools.h +++ b/library/include/RemoteTools.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/include/TileTypes.h b/library/include/TileTypes.h index 55628b3df..d21fb3c17 100644 --- a/library/include/TileTypes.h +++ b/library/include/TileTypes.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/include/Types.h b/library/include/Types.h index 1f211649d..3b9bf00b6 100644 --- a/library/include/Types.h +++ b/library/include/Types.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/include/VTableInterpose.h b/library/include/VTableInterpose.h index 7ba6b67aa..bd3c23036 100644 --- a/library/include/VTableInterpose.h +++ b/library/include/VTableInterpose.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 @@ -87,7 +87,8 @@ namespace DFHack 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 in undefined order. + automatically chained (subclass before superclass; at same + level highest priority called first; undefined order otherwise). Usage: @@ -105,6 +106,8 @@ namespace DFHack }; IMPLEMENT_VMETHOD_INTERPOSE(my_hack, foo); + or + IMPLEMENT_VMETHOD_INTERPOSE_PRIO(my_hack, foo, priority); void init() { if (!INTERPOSE_HOOK(my_hack, foo).apply()) @@ -121,9 +124,11 @@ namespace DFHack static DFHack::VMethodInterposeLink interpose_##name; \ rtype interpose_fn_##name args -#define IMPLEMENT_VMETHOD_INTERPOSE(class,name) \ +#define IMPLEMENT_VMETHOD_INTERPOSE_PRIO(class,name,priority) \ DFHack::VMethodInterposeLink \ - class::interpose_##name(&class::interpose_base::name, &class::interpose_fn_##name); + class::interpose_##name(&class::interpose_base::name, &class::interpose_fn_##name, priority); + +#define IMPLEMENT_VMETHOD_INTERPOSE(class,name) IMPLEMENT_VMETHOD_INTERPOSE_PRIO(class,name,0) #define INTERPOSE_NEXT(name) (this->*interpose_##name.chain) #define INTERPOSE_HOOK(class, name) (class::interpose_##name) @@ -140,6 +145,7 @@ namespace DFHack int vmethod_idx; void *interpose_method; // Pointer to the code of the interposing method void *chain_mptr; // Pointer to the chain field below + int priority; bool applied; void *saved_chain; // Previous pointer to the code @@ -153,9 +159,9 @@ namespace DFHack 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); + VMethodInterposeLinkBase(virtual_identity *host, int vmethod_idx, void *interpose_method, void *chain_mptr, int priority); ~VMethodInterposeLinkBase(); bool is_applied() { return applied; } @@ -171,12 +177,13 @@ namespace DFHack operator Ptr () { return chain; } template - VMethodInterposeLink(Ptr target, Ptr2 src) + VMethodInterposeLink(Ptr target, Ptr2 src, int priority) : VMethodInterposeLinkBase( &Base::_identity, vmethod_pointer_to_idx(target), method_pointer_to_addr(src), - &chain + &chain, + priority ) { src = target; /* check compatibility */ } }; diff --git a/library/include/VersionInfo.h b/library/include/VersionInfo.h index 5ff20516e..2ea4170fa 100644 --- a/library/include/VersionInfo.h +++ b/library/include/VersionInfo.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/include/VersionInfoFactory.h b/library/include/VersionInfoFactory.h index 08b1ea76e..f69f37fee 100644 --- a/library/include/VersionInfoFactory.h +++ b/library/include/VersionInfoFactory.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/include/df/custom/coord_path.methods.inc b/library/include/df/custom/coord_path.methods.inc index 9acebb82a..5421796e3 100644 --- a/library/include/df/custom/coord_path.methods.inc +++ b/library/include/df/custom/coord_path.methods.inc @@ -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(); diff --git a/library/include/df/custom/tile_bitmask.methods.inc b/library/include/df/custom/tile_bitmask.methods.inc index 18b312a08..e3414e334 100644 --- a/library/include/df/custom/tile_bitmask.methods.inc +++ b/library/include/df/custom/tile_bitmask.methods.inc @@ -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() { diff --git a/library/include/modules/Buildings.h b/library/include/modules/Buildings.h index 639df6865..266aadcb8 100644 --- a/library/include/modules/Buildings.h +++ b/library/include/modules/Buildings.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/include/modules/Burrows.h b/library/include/modules/Burrows.h index e26a2dc20..49b7df3c1 100644 --- a/library/include/modules/Burrows.h +++ b/library/include/modules/Burrows.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/include/modules/Constructions.h b/library/include/modules/Constructions.h index f8a3e5c61..41959b2d7 100644 --- a/library/include/modules/Constructions.h +++ b/library/include/modules/Constructions.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/include/modules/Engravings.h b/library/include/modules/Engravings.h index ea5eae62c..bf30c62a8 100644 --- a/library/include/modules/Engravings.h +++ b/library/include/modules/Engravings.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/include/modules/Graphic.h b/library/include/modules/Graphic.h index 6c003b4bc..a4b2dad10 100644 --- a/library/include/modules/Graphic.h +++ b/library/include/modules/Graphic.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mr�zek (peterix@gmail.com) +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 diff --git a/library/include/modules/Gui.h b/library/include/modules/Gui.h index 97e8bd422..147f27883 100644 --- a/library/include/modules/Gui.h +++ b/library/include/modules/Gui.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 @@ -91,6 +91,10 @@ namespace DFHack DFHACK_EXPORT bool any_item_hotkey(df::viewscreen *top); DFHACK_EXPORT df::item *getSelectedItem(color_ostream &out, bool quiet = false); + // A building is selected via 'q', 't' or 'i' (civzone) + DFHACK_EXPORT bool any_building_hotkey(df::viewscreen *top); + DFHACK_EXPORT df::building *getSelectedBuilding(color_ostream &out, bool quiet = false); + // Show a plain announcement, or a titan-style popup message DFHACK_EXPORT void showAnnouncement(std::string message, int color = 7, bool bright = true); DFHACK_EXPORT void showZoomAnnouncement(df::announcement_type type, df::coord pos, std::string message, int color = 7, bool bright = true); diff --git a/library/include/modules/Items.h b/library/include/modules/Items.h index 81c8e1285..34ca98162 100644 --- a/library/include/modules/Items.h +++ b/library/include/modules/Items.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 @@ -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 *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); diff --git a/library/include/modules/Job.h b/library/include/modules/Job.h index 9ce72c326..853813073 100644 --- a/library/include/modules/Job.h +++ b/library/include/modules/Job.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 @@ -32,6 +32,7 @@ distribution. #include "DataDefs.h" #include "df/job_item_ref.h" +#include "df/item_type.h" namespace df { @@ -69,6 +70,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); diff --git a/library/include/modules/MapCache.h b/library/include/modules/MapCache.h index 262e70bbf..c1c478bc6 100644 --- a/library/include/modules/MapCache.h +++ b/library/include/modules/MapCache.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 @@ -47,14 +47,6 @@ namespace MapExtras class DFHACK_EXPORT MapCache; -template 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 diff --git a/library/include/modules/Maps.h b/library/include/modules/Maps.h index 869b21580..632e8ec13 100644 --- a/library/include/modules/Maps.h +++ b/library/include/modules/Maps.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 @@ -151,6 +151,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 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 diff --git a/library/include/modules/Materials.h b/library/include/modules/Materials.h index fb5a6353c..e44ca89ed 100644 --- a/library/include/modules/Materials.h +++ b/library/include/modules/Materials.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/include/modules/Screen.h b/library/include/modules/Screen.h index 4f47205f2..ccd7f2f8d 100644 --- a/library/include/modules/Screen.h +++ b/library/include/modules/Screen.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 @@ -33,6 +33,14 @@ distribution. #include "df/graphic.h" #include "df/viewscreen.h" +namespace df +{ + struct job; + struct item; + struct unit; + struct building; +} + /** * \defgroup grp_screen utilities for painting to the screen * @ingroup grp_screen @@ -120,6 +128,9 @@ 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); } class DFHACK_EXPORT dfhack_viewscreen : public df::viewscreen { @@ -134,6 +145,7 @@ namespace DFHack virtual ~dfhack_viewscreen(); static bool is_instance(df::viewscreen *screen); + static dfhack_viewscreen *try_cast(df::viewscreen *screen); virtual void logic(); virtual void render(); @@ -146,6 +158,10 @@ namespace DFHack virtual std::string getFocusString() = 0; virtual void onShow() {}; virtual void onDismiss() {}; + virtual df::unit *getSelectedUnit() { return NULL; } + virtual df::item *getSelectedItem() { return NULL; } + virtual df::job *getSelectedJob() { return NULL; } + virtual df::building *getSelectedBuilding() { return NULL; } }; class DFHACK_EXPORT dfhack_lua_viewscreen : public dfhack_viewscreen { @@ -178,5 +194,10 @@ namespace DFHack virtual void onShow(); virtual void onDismiss(); + + virtual df::unit *getSelectedUnit(); + virtual df::item *getSelectedItem(); + virtual df::job *getSelectedJob(); + virtual df::building *getSelectedBuilding(); }; } diff --git a/library/include/modules/Translation.h b/library/include/modules/Translation.h index f75bf6339..03521797a 100644 --- a/library/include/modules/Translation.h +++ b/library/include/modules/Translation.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/include/modules/Units.h b/library/include/modules/Units.h index 65f0b58a0..ab1a5f63a 100644 --- a/library/include/modules/Units.h +++ b/library/include/modules/Units.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 @@ -233,6 +233,7 @@ DFHACK_EXPORT bool isDwarf(df::unit *unit); 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 computeMovementSpeed(df::unit *unit); diff --git a/library/include/modules/Vegetation.h b/library/include/modules/Vegetation.h index 83a192b98..89ba5ff6c 100644 --- a/library/include/modules/Vegetation.h +++ b/library/include/modules/Vegetation.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/include/modules/Windows.h b/library/include/modules/Windows.h index 7dbc9f1ad..700b88851 100644 --- a/library/include/modules/Windows.h +++ b/library/include/modules/Windows.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/include/modules/World.h b/library/include/modules/World.h index 8b0fe3f59..a945c4e72 100644 --- a/library/include/modules/World.h +++ b/library/include/modules/World.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 @@ -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,17 @@ 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; } - const std::string &key() { return key_value; } + int raw_id() const { return id; } + + 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]; } PersistentDataItem() : id(0), str_value(0), int_values(0) {} PersistentDataItem(int id, const std::string &key, std::string *sv, int *iv) @@ -83,54 +91,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 *vec, - const std::string &key, bool prefix = false); + DFHACK_EXPORT void GetPersistentData(std::vector *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 diff --git a/library/include/modules/kitchen.h b/library/include/modules/kitchen.h index f9d9d3458..ca88e9163 100644 --- a/library/include/modules/kitchen.h +++ b/library/include/modules/kitchen.h @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/lua/class.lua b/library/lua/class.lua index 7b142e499..e18bad9a9 100644 --- a/library/lua/class.lua +++ b/library/lua/class.lua @@ -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 diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua index ce3be5a87..3f57e5722 100644 --- a/library/lua/dfhack.lua +++ b/library/lua/dfhack.lua @@ -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 diff --git a/library/lua/gui.lua b/library/lua/gui.lua index 6eaa98606..15d03742f 100644 --- a/library/lua/gui.lua +++ b/library/lua/gui.lua @@ -48,17 +48,75 @@ 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) +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 + align = 0.5 + end + end + return math.floor(gap*align) +end + +function compute_frame_rect(wavail,havail,spec,xgap,ygap) + if not spec then + return mkdims_wh(0,0,wavail,havail) + end + + 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) + + 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) + gap = gap or 0 + local l,t,r,b = parse_inset(inset) + local rect = compute_frame_rect(wavail, havail, spec, gap*2+l+r, gap*2+t+b) + 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 to_pen(default, pen, bg, bold) if pen == nil then return default or {} elseif type(pen) ~= 'table' then @@ -68,37 +126,68 @@ local function to_pen(default, pen, bg, bold) end end ----------------------------- --- Clipped painter object -- ----------------------------- +function getKeyDisplay(code) + if type(code) == 'string' then + code = df.interface_key[code] + end + return dscreen.getKeyDisplay(code) +end -Painter = defclass(Painter, nil) +----------------------------------- +-- Clipped view rectangle object -- +----------------------------------- -function Painter:init(args) - 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) - } +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{ + 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, + } + 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 +202,57 @@ 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(nil, args.pen or COLOR_GREY) +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 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 @@ -210,16 +339,150 @@ function Painter:string(text,pen,...) return self:advance(#text, nil) end +function Painter:key(code,pen,bg,...) + return self:string( + getKeyDisplay(code), + pen or COLOR_LIGHTGREEN, bg or self.cur_pen.bg, ... + ) +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 +499,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 +513,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 +544,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 ------------------------ @@ -353,68 +610,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) 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 diff --git a/library/lua/gui/dialogs.lua b/library/lua/gui/dialogs.lua index b1a96a558..5811e94e6 100644 --- a/library/lua/gui/dialogs.lua +++ b/library/lua/gui/dialogs.lua @@ -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 + else + self:inputToSubviews(keys) end end @@ -171,97 +149,72 @@ 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 } -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) - - dc:newline(1) + local spen = gui.to_pen(COLOR_CYAN, self.select_pen, nil, false) + local cpen = gui.to_pen(COLOR_LIGHTCYAN, self.cursor_pen or self.select_pen, nil, true) - if self.selection>dc.height-3 then - self.page_top=self.selection-(dc.height-3) - elseif self.selection0 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) - end - dc:string(entry) - dc:newline(1) - end + local list_widget = widgets.List + if self.with_filter then + list_widget = widgets.FilteredList 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, + 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 - end - self.selection=newsel +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 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 - - if choice and choice[2] then - choice[2](choice,self.selection) -- maybe reverse the arguments? - end - elseif keys.LEAVESCREEN then + 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 diff --git a/library/lua/gui/dwarfmode.lua b/library/lua/gui/dwarfmode.lua index ba3cfbe6c..8c537639b 100644 --- a/library/lua/gui/dwarfmode.lua +++ b/library/lua/gui/dwarfmode.lua @@ -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 @@ -247,14 +260,20 @@ function Viewport:scrollByKey(key) self.z + dz ) else - return self + 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 - self:sendInputToParent(code) + 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() - return code 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 diff --git a/library/lua/gui/materials.lua b/library/lua/gui/materials.lua new file mode 100644 index 000000000..871c60014 --- /dev/null +++ b/library/lua/gui/materials.lua @@ -0,0 +1,329 @@ +-- 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', + 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(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 = { + { text = self.none_caption, mat_type = -1, mat_index = -1 }, + } + + 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 = { { + icon = '?', text = args.none_caption or 'none', item_type = -1, item_subtype = -1 + } } + 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 diff --git a/library/lua/gui/script.lua b/library/lua/gui/script.lua new file mode 100644 index 000000000..e15f6c1b9 --- /dev/null +++ b/library/lua/gui/script.lua @@ -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 diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua new file mode 100644 index 000000000..e731af068 --- /dev/null +++ b/library/lua/gui/widgets.lua @@ -0,0 +1,707 @@ +-- 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, +} + +function Panel:init(args) + self:addviews(args.subviews) +end + +function Panel:onRenderBody(dc) + if self.on_render then self.on_render(dc) 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 + + x = x + #text + + 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 + 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, + row_height = 1, + scroll_keys = STANDARDSCROLL, + icon_width = DEFAULT_NIL, +} + +function List:init(info) + self.page_top = 1 + self.page_size = 1 + self:setChoices(info.choices, info.selected) +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 + 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 + + 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 + + if iw and obj.icon then + local icon = getval(obj.icon) + if icon then + dc:seek(0, y) + if type(icon) == 'table' 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 + end + + render_text(obj, dc, iw or 0, y, cur_pen, cur_dpen, not current) + + if obj.key then + local keystr = gui.getKeyDisplay(obj.key) + dc:seek(dc.width-2-#keystr,y):pen(self.text_pen) + dc:string('('):string(keystr,COLOR_LIGHTGREEN):string(')') + end + end +end + +function List:submit() + if self.on_submit and #self.choices > 0 then + self.on_submit(self:getSelected()) + end +end + +function List:onInput(keys) + if self.on_submit and keys.SELECT then + self:submit() + 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) + +function FilteredList:init(info) + self.edit = EditField{ + text_pen = info.cursor_pen, + frame = { l = info.icon_width, t = 0 }, + 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, + row_height = info.row_height, + scroll_keys = info.scroll_keys, + icon_width = info.icon_width, + } + 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 + self.not_found = Label{ + visible = false, + text = info.not_found_label or 'No matches', + text_pen = COLOR_LIGHTRED, + frame = { l = info.icon_width, t = 2 }, + } + self:addviews{ self.edit, self.list, self.not_found } + self:setChoices(info.choices, info.selected) +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: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 diff --git a/library/lua/memscan.lua b/library/lua/memscan.lua index 6796b3563..ce7d2f1e5 100644 --- a/library/lua/memscan.lua +++ b/library/lua/memscan.lua @@ -378,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 @@ -394,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 diff --git a/library/lua/utils.lua b/library/lua/utils.lua index 9fa473ed8..e7267038c 100644 --- a/library/lua/utils.lua +++ b/library/lua/utils.lua @@ -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 @@ -361,6 +416,27 @@ function insert_or_update(vector,item,field,cmp) return added,cur,pos end +-- Binary search and erase +function erase_sorted_key(vector,key,field,cmp) + local cur,found,pos = binsearch(vector,key,field,cmp) + if found then + if df.isvalid(vector) then + vector:erase(pos) + else + table.remove(vector, pos) + end + end + return found,cur,pos +end + +function erase_sorted(vector,item,field,cmp) + local key = item + if field and item then + key = item[field] + end + return erase_sorted_key(vector,key,field,cmp) +end + -- Calls a method with a string temporary function call_with_string(obj,methodname,...) return dfhack.with_temp_object( diff --git a/library/modules/Buildings.cpp b/library/modules/Buildings.cpp index 9e78edd3e..6421114a1 100644 --- a/library/modules/Buildings.cpp +++ b/library/modules/Buildings.cpp @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 @@ -762,7 +762,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; diff --git a/library/modules/Burrows.cpp b/library/modules/Burrows.cpp index 62f62779a..48a2ca3a8 100644 --- a/library/modules/Burrows.cpp +++ b/library/modules/Burrows.cpp @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/modules/Constructions.cpp b/library/modules/Constructions.cpp index 2d61c447a..16c1f1b89 100644 --- a/library/modules/Constructions.cpp +++ b/library/modules/Constructions.cpp @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/modules/Engravings.cpp b/library/modules/Engravings.cpp index bff17ef5f..c2e0e6fce 100644 --- a/library/modules/Engravings.cpp +++ b/library/modules/Engravings.cpp @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/modules/Graphic.cpp b/library/modules/Graphic.cpp index 2f490c233..4188df2b8 100644 --- a/library/modules/Graphic.cpp +++ b/library/modules/Graphic.cpp @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mr�zek (peterix@gmail.com) +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 diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 1662f4467..04a453c06 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 @@ -53,6 +53,7 @@ using namespace DFHack; #include "df/viewscreen_dungeon_monsterstatusst.h" #include "df/viewscreen_joblistst.h" #include "df/viewscreen_unitlistst.h" +#include "df/viewscreen_buildinglistst.h" #include "df/viewscreen_itemst.h" #include "df/viewscreen_layer.h" #include "df/viewscreen_layer_workshop_profilest.h" @@ -84,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; @@ -302,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; } @@ -346,6 +388,37 @@ DEFINE_GET_FOCUS_STRING_HANDLER(layer_military) break; } + case df::viewscreen_layer_militaryst::Equip: + { + focus += "/" + enum_item_key(screen->equip.mode); + + switch (screen->equip.mode) + { + case df::viewscreen_layer_militaryst::T_equip::Customize: + { + if (screen->equip.edit_mode < 0) + focus += "/View"; + else + focus += "/" + enum_item_key(screen->equip.edit_mode); + break; + } + case df::viewscreen_layer_militaryst::T_equip::Uniform: + break; + case df::viewscreen_layer_militaryst::T_equip::Priority: + { + if (screen->equip.prio_in_move >= 0) + focus += "/Move"; + else + focus += "/View"; + break; + } + } + + static const char *lists[] = { "/Squads", "/Positions", "/Choices" }; + focus += lists[cur_list]; + break; + } + default: break; } @@ -691,6 +764,8 @@ df::job *Gui::getSelectedJob(color_ostream &out, bool quiet) return job; } + else if (auto dfscreen = dfhack_viewscreen::try_cast(top)) + return dfscreen->getSelectedJob(); else return getSelectedWorkshopJob(out, quiet); } @@ -763,7 +838,7 @@ static df::unit *getAnyUnit(df::viewscreen *top) { case df::viewscreen_petst::List: if (!vector_get(screen->is_vermin, screen->cursor)) - return (df::unit*)vector_get(screen->animal, screen->cursor); + return vector_get(screen->animal, screen->cursor).unit; return NULL; case df::viewscreen_petst::SelectTrainer: @@ -781,6 +856,9 @@ static df::unit *getAnyUnit(df::viewscreen *top) return NULL; } + if (auto dfscreen = dfhack_viewscreen::try_cast(top)) + return dfscreen->getSelectedUnit(); + if (!Gui::dwarfmode_hotkey(top)) return NULL; @@ -875,6 +953,9 @@ static df::item *getAnyItem(df::viewscreen *top) return NULL; } + if (auto dfscreen = dfhack_viewscreen::try_cast(top)) + return dfscreen->getSelectedItem(); + if (!Gui::dwarfmode_hotkey(top)) return NULL; @@ -933,6 +1014,70 @@ df::item *Gui::getSelectedItem(color_ostream &out, bool quiet) return item; } +static df::building *getAnyBuilding(df::viewscreen *top) +{ + using namespace ui_sidebar_mode; + using df::global::ui; + using df::global::ui_look_list; + using df::global::ui_look_cursor; + using df::global::world; + using df::global::ui_sidebar_menus; + + if (auto screen = strict_virtual_cast(top)) + return vector_get(screen->buildings, screen->cursor); + + if (auto dfscreen = dfhack_viewscreen::try_cast(top)) + return dfscreen->getSelectedBuilding(); + + if (!Gui::dwarfmode_hotkey(top)) + return NULL; + + switch (ui->main.mode) { + case LookAround: + { + if (!ui_look_list || !ui_look_cursor) + return NULL; + + auto item = vector_get(ui_look_list->items, *ui_look_cursor); + if (item && item->type == df::ui_look_list::T_items::Building) + return item->building; + else + return NULL; + } + case QueryBuilding: + case BuildingItems: + { + return world->selected_building; + } + case Zones: + case ZonesPenInfo: + case ZonesPitInfo: + case ZonesHospitalInfo: + { + if (ui_sidebar_menus) + return ui_sidebar_menus->zone.selected; + return NULL; + } + default: + return NULL; + } +} + +bool Gui::any_building_hotkey(df::viewscreen *top) +{ + return getAnyBuilding(top) != NULL; +} + +df::building *Gui::getSelectedBuilding(color_ostream &out, bool quiet) +{ + df::building *building = getAnyBuilding(Core::getTopViewscreen()); + + if (!building && !quiet) + out.printerr("No building is selected in the UI.\n"); + + return building; +} + // static void doShowAnnouncement( diff --git a/library/modules/Items.cpp b/library/modules/Items.cpp index b8c697a48..877f8abe0 100644 --- a/library/modules/Items.cpp +++ b/library/modules/Items.cpp @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 @@ -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) && @@ -527,6 +592,20 @@ void Items::getContainedItems(df::item *item, std::vector *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); diff --git a/library/modules/Job.cpp b/library/modules/Job.cpp index b74a4b73f..def3b4192 100644 --- a/library/modules/Job.cpp +++ b/library/modules/Job.cpp @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 @@ -361,3 +361,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); +} diff --git a/library/modules/Maps.cpp b/library/modules/Maps.cpp index f1f40f19c..482b950ba 100644 --- a/library/modules/Maps.cpp +++ b/library/modules/Maps.cpp @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 @@ -454,7 +454,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(block->designation,pos); + auto des = index_tile(block->designation,pos); unsigned idx = des.bits.biome; if (idx < 9) { @@ -529,8 +529,8 @@ bool Maps::canWalkBetween(df::coord pos1, df::coord pos2) if (!block1 || !block2) return false; - auto tile1 = MapExtras::index_tile(block1->walkable, pos1); - auto tile2 = MapExtras::index_tile(block2->walkable, pos2); + auto tile1 = index_tile(block1->walkable, pos1); + auto tile2 = index_tile(block2->walkable, pos2); return tile1 && tile1 == tile2; } @@ -585,7 +585,7 @@ bool MapExtras::Block::Allocate() if (!block) return false; - delete item_counts; + delete[] item_counts; delete tiles; delete basemats; init(); diff --git a/library/modules/Materials.cpp b/library/modules/Materials.cpp index db9c9c7df..454fdf66d 100644 --- a/library/modules/Materials.cpp +++ b/library/modules/Materials.cpp @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 @@ -312,7 +312,7 @@ std::string MaterialInfo::getToken() else if (index == 1) return "COAL:CHARCOAL"; } - return material->id; + return material->id + ":NONE"; case Inorganic: return "INORGANIC:" + inorganic->id; case Creature: @@ -497,7 +497,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)); diff --git a/library/modules/Screen.cpp b/library/modules/Screen.cpp index 9f258fe02..e8d23261b 100644 --- a/library/modules/Screen.cpp +++ b/library/modules/Screen.cpp @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 @@ -28,6 +28,7 @@ distribution. #include #include #include +#include using namespace std; #include "modules/Screen.h" @@ -50,6 +51,11 @@ using namespace DFHack; #include "df/tile_page.h" #include "df/interfacest.h" #include "df/enabler.h" +#include "df/unit.h" +#include "df/item.h" +#include "df/job.h" +#include "df/building.h" +#include "df/renderer.h" using namespace df::enums; using df::global::init; @@ -60,6 +66,8 @@ using df::global::enabler; using Screen::Pen; +using std::string; + /* * Screen painting API. */ @@ -191,7 +199,7 @@ bool Screen::drawBorder(const std::string &title) if (!gps) return false; int dimx = gps->dimx, dimy = gps->dimy; - Pen border(0xDB, 8); + Pen border('\xDB', 8); Pen text(0, 0, 7); Pen signature(0, 0, 8); @@ -299,6 +307,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 > *keydisplay = NULL; +#endif + +void init_screen_module(Core *core) +{ +#ifdef _LINUX + renderer tmp; + if (!strict_virtual_cast((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. */ @@ -322,6 +418,11 @@ bool dfhack_viewscreen::is_instance(df::viewscreen *screen) return dfhack_screens.count(screen) != 0; } +dfhack_viewscreen *dfhack_viewscreen::try_cast(df::viewscreen *screen) +{ + return is_instance(screen) ? static_cast(screen) : NULL; +} + void dfhack_viewscreen::check_resize() { auto size = Screen::getWindowSize(); @@ -582,7 +683,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(); @@ -637,3 +743,35 @@ void dfhack_lua_viewscreen::onDismiss() lua_pushstring(Lua::Core::State, "onDismiss"); safe_call_lua(do_notify, 1, 0); } + +df::unit *dfhack_lua_viewscreen::getSelectedUnit() +{ + Lua::StackUnwinder frame(Lua::Core::State); + lua_pushstring(Lua::Core::State, "onGetSelectedUnit"); + safe_call_lua(do_notify, 1, 1); + return Lua::GetDFObject(Lua::Core::State, -1); +} + +df::item *dfhack_lua_viewscreen::getSelectedItem() +{ + Lua::StackUnwinder frame(Lua::Core::State); + lua_pushstring(Lua::Core::State, "onGetSelectedItem"); + safe_call_lua(do_notify, 1, 1); + return Lua::GetDFObject(Lua::Core::State, -1); +} + +df::job *dfhack_lua_viewscreen::getSelectedJob() +{ + Lua::StackUnwinder frame(Lua::Core::State); + lua_pushstring(Lua::Core::State, "onGetSelectedJob"); + safe_call_lua(do_notify, 1, 1); + return Lua::GetDFObject(Lua::Core::State, -1); +} + +df::building *dfhack_lua_viewscreen::getSelectedBuilding() +{ + Lua::StackUnwinder frame(Lua::Core::State); + lua_pushstring(Lua::Core::State, "onGetSelectedBuilding"); + safe_call_lua(do_notify, 1, 1); + return Lua::GetDFObject(Lua::Core::State, -1); +} diff --git a/library/modules/Translation.cpp b/library/modules/Translation.cpp index db1305161..6f4ca2b04 100644 --- a/library/modules/Translation.cpp +++ b/library/modules/Translation.cpp @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 01b7b50f4..7a0a7549b 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 @@ -876,7 +876,7 @@ inline void adjust_skill_rating(int &rating, bool is_adventure, int value, int d } } -int Units::getEffectiveSkill(df::unit *unit, df::job_skill skill_id) +int Units::getNominalSkill(df::unit *unit, df::job_skill skill_id, bool use_rust) { CHECK_NULL_POINTER(unit); @@ -889,12 +889,26 @@ int Units::getEffectiveSkill(df::unit *unit, df::job_skill skill_id) // Retrieve skill from unit soul: - df::enum_field key(skill_id); - auto skill = binsearch_in_vector(unit->status.current_soul->skills, &df::unit_skill::id, key); + auto skill = binsearch_in_vector(unit->status.current_soul->skills, &df::unit_skill::id, skill_id); - int rating = 0; if (skill) - rating = std::max(0, int(skill->rating) - skill->rusty); + { + int rating = int(skill->rating); + if (use_rust) + rating -= skill->rusty; + return std::max(0, rating); + } + + return 0; +} + +int Units::getEffectiveSkill(df::unit *unit, df::job_skill skill_id) +{ + /* + * This is 100% reverse-engineered from DF code. + */ + + int rating = getNominalSkill(unit, skill_id, true); // Apply special states diff --git a/library/modules/Vegetation.cpp b/library/modules/Vegetation.cpp index a76c3a2cd..1a6779553 100644 --- a/library/modules/Vegetation.cpp +++ b/library/modules/Vegetation.cpp @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/modules/Windows.cpp b/library/modules/Windows.cpp index 218b6f7e0..4e749322f 100644 --- a/library/modules/Windows.cpp +++ b/library/modules/Windows.cpp @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 diff --git a/library/modules/World.cpp b/library/modules/World.cpp index 67b8c1236..f3283c3a3 100644 --- a/library/modules/World.cpp +++ b/library/modules/World.cpp @@ -1,6 +1,6 @@ /* https://github.com/peterix/dfhack -Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) +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 @@ -38,99 +38,49 @@ using namespace std; #include "ModuleFactory.h" #include "Core.h" +#include "modules/Maps.h" + #include "MiscUtils.h" #include "DataDefs.h" #include "df/world.h" #include "df/historical_figure.h" +#include "df/map_block.h" +#include "df/block_square_event_world_constructionst.h" using namespace DFHack; +using namespace df::enums; using df::global::world; -Module* DFHack::createWorld() -{ - return new World(); -} - -struct World::Private -{ - Private() - { - Inited = PauseInited = StartedWeather = StartedMode = false; - next_persistent_id = 0; - } - bool Inited; - - bool PauseInited; - bool StartedWeather; - bool StartedMode; - - int next_persistent_id; - std::multimap persistent_index; - - Process * owner; -}; - +static int next_persistent_id = 0; +static std::multimap persistent_index; typedef std::pair T_persistent_item; -World::World() -{ - Core & c = Core::getInstance(); - d = new Private; - d->owner = c.p; - - if(df::global::pause_state) - d->PauseInited = true; - - if(df::global::current_weather) - d->StartedWeather = true; - if (df::global::gamemode && df::global::gametype) - d->StartedMode = true; - - d->Inited = true; -} - -World::~World() -{ - delete d; -} - -bool World::Start() -{ - return true; -} - -bool World::Finish() -{ - return true; -} - bool World::ReadPauseState() { - if(!d->PauseInited) return false; - return *df::global::pause_state; + return DF_GLOBAL_VALUE(pause_state, false); } void World::SetPauseState(bool paused) { - if (d->PauseInited) - *df::global::pause_state = paused; + bool dummy; + DF_GLOBAL_VALUE(pause_state, dummy) = paused; } uint32_t World::ReadCurrentYear() { - return *df::global::cur_year; + return DF_GLOBAL_VALUE(cur_year, 0); } uint32_t World::ReadCurrentTick() { - return *df::global::cur_year_tick; + return DF_GLOBAL_VALUE(cur_year_tick, 0); } bool World::ReadGameMode(t_gamemodes& rd) { - if(d->Inited && d->StartedMode) + if(df::global::gamemode && df::global::gametype) { rd.g_mode = (DFHack::GameMode)*df::global::gamemode; rd.g_type = (DFHack::GameType)*df::global::gametype; @@ -140,7 +90,7 @@ bool World::ReadGameMode(t_gamemodes& rd) } bool World::WriteGameMode(const t_gamemodes & wr) { - if(d->Inited && d->StartedMode) + if(df::global::gamemode && df::global::gametype) { *df::global::gamemode = wr.g_mode; *df::global::gametype = wr.g_type; @@ -173,24 +123,24 @@ specified by memory.xml gets me the current month/date. */ uint32_t World::ReadCurrentMonth() { - return this->ReadCurrentTick() / 1200 / 28; + return ReadCurrentTick() / 1200 / 28; } uint32_t World::ReadCurrentDay() { - return ((this->ReadCurrentTick() / 1200) % 28) + 1; + return ((ReadCurrentTick() / 1200) % 28) + 1; } uint8_t World::ReadCurrentWeather() { - if (d->Inited && d->StartedWeather) + if (df::global::current_weather) return (*df::global::current_weather)[2][2]; return 0; } void World::SetCurrentWeather(uint8_t weather) { - if (d->Inited && d->StartedWeather) + if (df::global::current_weather) memset(df::global::current_weather, weather, 25); } @@ -206,13 +156,13 @@ static PersistentDataItem dataFromHFig(df::historical_figure *hfig) void World::ClearPersistentCache() { - d->next_persistent_id = 0; - d->persistent_index.clear(); + next_persistent_id = 0; + persistent_index.clear(); } -bool World::BuildPersistentCache() +static bool BuildPersistentCache() { - if (d->next_persistent_id) + if (next_persistent_id) return true; if (!Core::getInstance().isWorldLoaded()) return false; @@ -220,20 +170,20 @@ bool World::BuildPersistentCache() std::vector &hfvec = df::historical_figure::get_vector(); // Determine the next entry id as min(-100, lowest_id-1) - d->next_persistent_id = -100; + next_persistent_id = -100; if (hfvec.size() > 0 && hfvec[0]->id <= -100) - d->next_persistent_id = hfvec[0]->id-1; + next_persistent_id = hfvec[0]->id-1; // Add the entries to the lookup table - d->persistent_index.clear(); + persistent_index.clear(); for (size_t i = 0; i < hfvec.size() && hfvec[i]->id <= -100; i++) { if (!hfvec[i]->name.has_name || hfvec[i]->name.first_name.empty()) continue; - d->persistent_index.insert(T_persistent_item(hfvec[i]->name.first_name, -hfvec[i]->id)); + persistent_index.insert(T_persistent_item(hfvec[i]->name.first_name, -hfvec[i]->id)); } return true; @@ -247,14 +197,14 @@ PersistentDataItem World::AddPersistentData(const std::string &key) std::vector &hfvec = df::historical_figure::get_vector(); df::historical_figure *hfig = new df::historical_figure(); - hfig->id = d->next_persistent_id--; + hfig->id = next_persistent_id--; hfig->name.has_name = true; hfig->name.first_name = key; memset(hfig->name.words, 0xFF, sizeof(hfig->name.words)); hfvec.insert(hfvec.begin(), hfig); - d->persistent_index.insert(T_persistent_item(key, -hfig->id)); + persistent_index.insert(T_persistent_item(key, -hfig->id)); return dataFromHFig(hfig); } @@ -264,8 +214,8 @@ PersistentDataItem World::GetPersistentData(const std::string &key) if (!BuildPersistentCache()) return PersistentDataItem(); - auto it = d->persistent_index.find(key); - if (it != d->persistent_index.end()) + auto it = persistent_index.find(key); + if (it != persistent_index.end()) return GetPersistentData(it->second); return PersistentDataItem(); @@ -305,24 +255,24 @@ void World::GetPersistentData(std::vector *vec, const std::s if (!BuildPersistentCache()) return; - auto eqrange = d->persistent_index.equal_range(key); + auto eqrange = persistent_index.equal_range(key); if (prefix) { if (key.empty()) { - eqrange.first = d->persistent_index.begin(); - eqrange.second = d->persistent_index.end(); + eqrange.first = persistent_index.begin(); + eqrange.second = persistent_index.end(); } else { std::string bound = key; if (bound[bound.size()-1] != '/') bound += "/"; - eqrange.first = d->persistent_index.lower_bound(bound); + eqrange.first = persistent_index.lower_bound(bound); bound[bound.size()-1]++; - eqrange.second = d->persistent_index.lower_bound(bound); + eqrange.second = persistent_index.lower_bound(bound); } } @@ -336,25 +286,26 @@ void World::GetPersistentData(std::vector *vec, const std::s bool World::DeletePersistentData(const PersistentDataItem &item) { - if (item.id > -100) + int id = item.raw_id(); + if (id > -100) return false; if (!BuildPersistentCache()) return false; std::vector &hfvec = df::historical_figure::get_vector(); - auto eqrange = d->persistent_index.equal_range(item.key_value); + auto eqrange = persistent_index.equal_range(item.key()); for (auto it2 = eqrange.first; it2 != eqrange.second; ) { auto it = it2; ++it2; - if (it->second != -item.id) + if (it->second != -id) continue; - d->persistent_index.erase(it); + persistent_index.erase(it); - int idx = binsearch_index(hfvec, item.id); + int idx = binsearch_index(hfvec, id); if (idx >= 0) { delete hfvec[idx]; @@ -366,3 +317,63 @@ bool World::DeletePersistentData(const PersistentDataItem &item) return false; } + +df::tile_bitmask *World::getPersistentTilemask(const PersistentDataItem &item, df::map_block *block, bool create) +{ + if (!block) + return NULL; + + int id = item.raw_id(); + if (id > -100) + return NULL; + + for (size_t i = 0; i < block->block_events.size(); i++) + { + auto ev = block->block_events[i]; + if (ev->getType() != block_square_event_type::world_construction) + continue; + auto wcsev = strict_virtual_cast(ev); + if (!wcsev || wcsev->construction_id != id) + continue; + return &wcsev->tile_bitmask; + } + + if (!create) + return NULL; + + auto ev = df::allocate(); + if (!ev) + return NULL; + + ev->construction_id = id; + ev->tile_bitmask.clear(); + vector_insert_at(block->block_events, 0, (df::block_square_event*)ev); + + return &ev->tile_bitmask; +} + +bool World::deletePersistentTilemask(const PersistentDataItem &item, df::map_block *block) +{ + if (!block) + return false; + int id = item.raw_id(); + if (id > -100) + return false; + + bool found = false; + for (int i = block->block_events.size()-1; i >= 0; i--) + { + auto ev = block->block_events[i]; + if (ev->getType() != block_square_event_type::world_construction) + continue; + auto wcsev = strict_virtual_cast(ev); + if (!wcsev || wcsev->construction_id != id) + continue; + + delete wcsev; + vector_erase_at(block->block_events, i); + found = true; + } + + return found; +} diff --git a/library/xml b/library/xml index 8a78bfa21..fcacacce7 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 8a78bfa218817765b0a80431e0cf25435ffb2179 +Subproject commit fcacacce7cf09cf70f011fea87b5be416da73457 diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 4d4f7493f..14e07562d 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -100,7 +100,7 @@ if (BUILD_SUPPORTED) DFHACK_PLUGIN(stockpiles stockpiles.cpp) DFHACK_PLUGIN(rename rename.cpp LINK_LIBRARIES lua PROTOBUFS rename) DFHACK_PLUGIN(jobutils jobutils.cpp) - DFHACK_PLUGIN(workflow workflow.cpp) + DFHACK_PLUGIN(workflow workflow.cpp LINK_LIBRARIES lua) DFHACK_PLUGIN(showmood showmood.cpp) DFHACK_PLUGIN(fixveins fixveins.cpp) DFHACK_PLUGIN(fixpositions fixpositions.cpp) @@ -125,8 +125,11 @@ if (BUILD_SUPPORTED) DFHACK_PLUGIN(siege-engine siege-engine.cpp LINK_LIBRARIES lua) DFHACK_PLUGIN(reactionhooks reactionhooks.cpp LINK_LIBRARIES lua) DFHACK_PLUGIN(add-spatter add-spatter.cpp) + DFHACK_PLUGIN(fix-armory fix-armory.cpp) # not yet. busy with other crud again... #DFHACK_PLUGIN(versionosd versionosd.cpp) + DFHACK_PLUGIN(misery misery.cpp) + #DFHACK_PLUGIN(dfstream dfstream.cpp LINK_LIBRARIES clsocket dfhack-tinythread) endif() diff --git a/plugins/add-spatter.cpp b/plugins/add-spatter.cpp index 425ffe9d0..ca37c8ee3 100644 --- a/plugins/add-spatter.cpp +++ b/plugins/add-spatter.cpp @@ -246,7 +246,7 @@ struct product_hook : improvement_product { (df::unit *unit, std::vector *out_items, std::vector *in_reag, std::vector *in_items, - int32_t quantity, int16_t skill, + int32_t quantity, df::job_skill skill, df::historical_entity *entity, df::world_site *site) ) { if (auto product = products[this]) @@ -279,7 +279,7 @@ struct product_hook : improvement_product { break; } - int rating = unit ? Units::getEffectiveSkill(unit, df::job_skill(skill)) : 0; + int rating = unit ? Units::getEffectiveSkill(unit, skill) : 0; int size = int(probability*(1.0f + 0.06f*rating)); // +90% at legendary object->addContaminant( diff --git a/plugins/advtools.cpp b/plugins/advtools.cpp index d674f5528..59b88087b 100644 --- a/plugins/advtools.cpp +++ b/plugins/advtools.cpp @@ -30,6 +30,7 @@ #include "df/viewscreen_optionst.h" #include "df/viewscreen_dungeonmodest.h" #include "df/viewscreen_dungeon_monsterstatusst.h" +#include "df/nemesis_flags.h" #include @@ -320,7 +321,7 @@ std::string getUnitNameProfession(df::unit *unit) } enum InventoryMode { - INV_CARRIED, + INV_HAULED, INV_WEAPON, INV_WORN, INV_IN_CONTAINER @@ -354,8 +355,8 @@ void listUnitInventory(std::vector *list, df::unit *unit) InventoryMode mode; switch (item->mode) { - case df::unit_inventory_item::Carried: - mode = INV_CARRIED; + case df::unit_inventory_item::Hauled: + mode = INV_HAULED; break; case df::unit_inventory_item::Weapon: mode = INV_WEAPON; @@ -683,17 +684,19 @@ command_result adv_bodyswap (color_ostream &out, std::vector & par // Permanently re-link everything if (permanent) { + using namespace df::enums::nemesis_flags; + ui_advmode->player_id = linear_index(world->nemesis.all, new_nemesis); // Flag 0 appears to be the 'active adventurer' flag, and // the player_id field above seems to be computed using it // when a savegame is loaded. // Also, unless this is set, it is impossible to retire. - real_nemesis->flags.set(0, false); - new_nemesis->flags.set(0, true); + real_nemesis->flags.set(ACTIVE_ADVENTURER, false); + new_nemesis->flags.set(ACTIVE_ADVENTURER, true); - real_nemesis->flags.set(1, true); // former retired adventurer - new_nemesis->flags.set(2, true); // blue color in legends + real_nemesis->flags.set(RETIRED_ADVENTURER, true); // former retired adventurer + new_nemesis->flags.set(ADVENTURER, true); // blue color in legends // Reassign companions and acquaintances if (!no_make_leader) diff --git a/plugins/autolabor.cpp b/plugins/autolabor.cpp index c39b126c9..44984cfdb 100644 --- a/plugins/autolabor.cpp +++ b/plugins/autolabor.cpp @@ -547,9 +547,7 @@ static void reset_labor(df::unit_labor labor) static void init_state() { - auto pworld = Core::getInstance().getWorld(); - - config = pworld->GetPersistentData("autolabor/config"); + config = World::GetPersistentData("autolabor/config"); if (config.isValid() && config.ival(0) == -1) config.ival(0) = 0; @@ -558,7 +556,7 @@ static void init_state() if (!enable_autolabor) return; - auto cfg_haulpct = pworld->GetPersistentData("autolabor/haulpct"); + auto cfg_haulpct = World::GetPersistentData("autolabor/haulpct"); if (cfg_haulpct.isValid()) { hauler_pct = cfg_haulpct.ival(0); @@ -572,7 +570,7 @@ static void init_state() labor_infos.resize(ARRAY_COUNT(default_labor_infos)); std::vector items; - pworld->GetPersistentData(&items, "autolabor/labors/", true); + World::GetPersistentData(&items, "autolabor/labors/", true); for (auto p = items.begin(); p != items.end(); p++) { @@ -594,7 +592,7 @@ static void init_state() std::stringstream name; name << "autolabor/labors/" << i; - labor_infos[i].config = pworld->AddPersistentData(name.str()); + labor_infos[i].config = World::AddPersistentData(name.str()); labor_infos[i].is_exclusive = default_labor_infos[i].is_exclusive; labor_infos[i].active_dwarfs = 0; @@ -633,11 +631,9 @@ static void generate_labor_to_skill_map() static void enable_plugin(color_ostream &out) { - auto pworld = Core::getInstance().getWorld(); - if (!config.isValid()) { - config = pworld->AddPersistentData("autolabor/config"); + config = World::AddPersistentData("autolabor/config"); config.ival(0) = 0; } @@ -1551,7 +1547,7 @@ static int stockcheck(color_ostream &out, vector & parameters) } - std::vector &items = world->items.other[items_other_id::ANY_FREE]; + std::vector &items = world->items.other[items_other_id::IN_PLAY]; // Precompute a bitmask with the bad flags df::item_flags bad_flags; diff --git a/plugins/burrows.cpp b/plugins/burrows.cpp index 0b66a7b9a..edcc01ecf 100644 --- a/plugins/burrows.cpp +++ b/plugins/burrows.cpp @@ -281,7 +281,7 @@ static void reset_tracking() static void init_map(color_ostream &out) { - auto config = Core::getInstance().getWorld()->GetPersistentData("burrows/config"); + auto config = World::GetPersistentData("burrows/config"); if (config.isValid()) { auto_grow = !!(config.ival(0) & 1); @@ -307,7 +307,7 @@ static void deinit_map(color_ostream &out) static PersistentDataItem create_config(color_ostream &out) { bool created; - auto rv = Core::getInstance().getWorld()->GetPersistentData("burrows/config", &created); + auto rv = World::GetPersistentData("burrows/config", &created); if (created && rv.isValid()) rv.ival(0) = 0; if (!rv.isValid()) diff --git a/plugins/devel/stockcheck.cpp b/plugins/devel/stockcheck.cpp index 8ace55cdc..452a637fc 100644 --- a/plugins/devel/stockcheck.cpp +++ b/plugins/devel/stockcheck.cpp @@ -135,7 +135,7 @@ static command_result stockcheck(color_ostream &out, vector & parameter } - std::vector &items = world->items.other[items_other_id::ANY_FREE]; + std::vector &items = world->items.other[items_other_id::IN_PLAY]; // Precompute a bitmask with the bad flags df::item_flags bad_flags; diff --git a/plugins/dfstream.cpp b/plugins/dfstream.cpp new file mode 100644 index 000000000..1ed906881 --- /dev/null +++ b/plugins/dfstream.cpp @@ -0,0 +1,362 @@ +#include "Core.h" +#include "Console.h" +#include "Export.h" +#include "PluginManager.h" + +#include "DataDefs.h" +#include "df/graphic.h" +#include "df/enabler.h" +#include "df/renderer.h" + +#include +#include +#include "PassiveSocket.h" +#include "tinythread.h" + +using namespace DFHack; +using namespace df::enums; + +using std::string; +using std::vector; +using df::global::gps; +using df::global::enabler; + +// The error messages are taken from the clsocket source code +const char * translate_socket_error(CSimpleSocket::CSocketError err) { + switch (err) { + case CSimpleSocket::SocketError: + return "Generic socket error translates to error below."; + case CSimpleSocket::SocketSuccess: + return "No socket error."; + case CSimpleSocket::SocketInvalidSocket: + return "Invalid socket handle."; + case CSimpleSocket::SocketInvalidAddress: + return "Invalid destination address specified."; + case CSimpleSocket::SocketInvalidPort: + return "Invalid destination port specified."; + case CSimpleSocket::SocketConnectionRefused: + return "No server is listening at remote address."; + case CSimpleSocket::SocketTimedout: + return "Timed out while attempting operation."; + case CSimpleSocket::SocketEwouldblock: + return "Operation would block if socket were blocking."; + case CSimpleSocket::SocketNotconnected: + return "Currently not connected."; + case CSimpleSocket::SocketEinprogress: + return "Socket is non-blocking and the connection cannot be completed immediately"; + case CSimpleSocket::SocketInterrupted: + return "Call was interrupted by a signal that was caught before a valid connection arrived."; + case CSimpleSocket::SocketConnectionAborted: + return "The connection has been aborted."; + case CSimpleSocket::SocketProtocolError: + return "Invalid protocol for operation."; + case CSimpleSocket::SocketFirewallError: + return "Firewall rules forbid connection."; + case CSimpleSocket::SocketInvalidSocketBuffer: + return "The receive buffer point outside the process's address space."; + case CSimpleSocket::SocketConnectionReset: + return "Connection was forcibly closed by the remote host."; + case CSimpleSocket::SocketAddressInUse: + return "Address already in use."; + case CSimpleSocket::SocketInvalidPointer: + return "Pointer type supplied as argument is invalid."; + case CSimpleSocket::SocketEunknown: + return "Unknown error please report to mark@carrierlabs.com"; + default: + return "No such CSimpleSocket error"; + } +} + +// Owns the thread that accepts TCP connections and forwards messages to clients; +// has a mutex +class client_pool { + typedef tthread::mutex mutex; + + mutex clients_lock; + std::vector clients; + + // TODO - delete this at some point + tthread::thread * accepter; + + static void accept_clients(void * client_pool_pointer) { + client_pool * p = reinterpret_cast(client_pool_pointer); + CPassiveSocket socket; + socket.Initialize(); + if (socket.Listen((const uint8_t *)"0.0.0.0", 8008)) { + std::cout << "Listening on a socket" << std::endl; + } else { + std::cout << "Not listening: " << socket.GetSocketError() << std::endl; + std::cout << translate_socket_error(socket.GetSocketError()) << std::endl; + } + while (true) { + CActiveSocket * client = socket.Accept(); + if (client != 0) { + lock l(*p); + p->clients.push_back(client); + } + } + } + +public: + class lock { + tthread::lock_guard l; + public: + lock(client_pool & p) + : l(p.clients_lock) + { + } + }; + friend class client_pool::lock; + + client_pool() { + accepter = new tthread::thread(accept_clients, this); + } + + // MUST have lock + bool has_clients() { + return !clients.empty(); + } + + // MUST have lock + void add_client(CActiveSocket * sock) { + clients.push_back(sock); + } + + // MUST have lock + void broadcast(const std::string & message) { + unsigned int sz = htonl(message.size()); + for (size_t i = 0; i < clients.size(); ++i) { + clients[i]->Send(reinterpret_cast(&sz), sizeof(sz)); + clients[i]->Send((const uint8_t *) message.c_str(), message.size()); + } + } +}; + +// A decorator (in the design pattern sense) of the DF renderer class. +// Sends the screen contents to a client_pool. +class renderer_decorator : public df::renderer { + // the renderer we're decorating + df::renderer * inner; + + // how many frames have passed since we last sent a frame + int framesNotPrinted; + + // set to false in the destructor + bool * alive; + + // clients to which we send the frame + client_pool clients; + + // The following three methods facilitate copying of state to the inner object + void set_to_null() { + 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; + } + + void copy_from_inner() { + screen = inner->screen; + screentexpos = inner->screentexpos; + screentexpos_addcolor = inner->screentexpos_addcolor; + screentexpos_grayscale = inner->screentexpos_grayscale; + screentexpos_cf = inner->screentexpos_cf; + screentexpos_cbr = inner->screentexpos_cbr; + screen_old = inner->screen_old; + screentexpos_old = inner->screentexpos_old; + screentexpos_addcolor_old = inner->screentexpos_addcolor_old; + screentexpos_grayscale_old = inner->screentexpos_grayscale_old; + screentexpos_cf_old = inner->screentexpos_cf_old; + screentexpos_cbr_old = inner->screentexpos_cbr_old; + } + + void copy_to_inner() { + inner->screen = screen; + inner->screentexpos = screentexpos; + inner->screentexpos_addcolor = screentexpos_addcolor; + inner->screentexpos_grayscale = screentexpos_grayscale; + inner->screentexpos_cf = screentexpos_cf; + inner->screentexpos_cbr = screentexpos_cbr; + inner->screen_old = screen_old; + inner->screentexpos_old = screentexpos_old; + inner->screentexpos_addcolor_old = screentexpos_addcolor_old; + inner->screentexpos_grayscale_old = screentexpos_grayscale_old; + inner->screentexpos_cf_old = screentexpos_cf_old; + inner->screentexpos_cbr_old = screentexpos_cbr_old; + } + +public: + renderer_decorator(df::renderer * inner, bool * alive) + : inner(inner) + , framesNotPrinted(0) + , alive(alive) + { + copy_from_inner(); + } + virtual void update_tile(int x, int y) { + copy_to_inner(); + inner->update_tile(x, y); + } + virtual void update_all() { + copy_to_inner(); + inner->update_all(); + } + virtual void render() { + copy_to_inner(); + inner->render(); + + ++framesNotPrinted; + int gfps = enabler->calculated_gfps; + if (gfps == 0) gfps = 1; + // send a frame roughly every 128 mibiseconds (1 second = 1024 mibiseconds) + if ((framesNotPrinted * 1024) / gfps <= 128) return; + + client_pool::lock lock(clients); + if (!clients.has_clients()) return; + framesNotPrinted = 0; + std::stringstream frame; + frame << gps->dimx << ' ' << gps->dimy << " 0 0 " << gps->dimx << ' ' << gps->dimy << '\n'; + unsigned char * sc_ = gps->screen; + for (int y = 0; y < gps->dimy; ++y) { + unsigned char * sc = sc_; + for (int x = 0; x < gps->dimx; ++x) { + unsigned char ch = sc[0]; + unsigned char bold = (sc[3] != 0) * 8; + unsigned char translate[] = + { 0, 4, 2, 6, 1, 5, 3, 7, 8, 12, 10, 14, 9, 13, 11, 15 }; + unsigned char fg = translate[(sc[1] + bold) % 16]; + unsigned char bg = translate[sc[2] % 16]*16; + frame.put(ch); + frame.put(fg+bg); + sc += 4*gps->dimy; + } + sc_ += 4; + } + clients.broadcast(frame.str()); + } + virtual void set_fullscreen() { inner->set_fullscreen(); } + virtual void zoom(df::zoom_commands cmd) { + copy_to_inner(); + inner->zoom(cmd); + } + virtual void resize(int w, int h) { + copy_to_inner(); + inner->resize(w, h); + copy_from_inner(); + } + virtual void grid_resize(int w, int h) { + copy_to_inner(); + inner->grid_resize(w, h); + copy_from_inner(); + } + virtual ~renderer_decorator() { + *alive = false; + if (inner) { + copy_to_inner(); + delete inner; + inner = 0; + } + set_to_null(); + } + virtual bool get_mouse_coords(int *x, int *y) { return inner->get_mouse_coords(x, y); } + virtual bool uses_opengl() { return inner->uses_opengl(); } + + static renderer_decorator * hook(df::renderer *& ptr, bool * alive) { + renderer_decorator * r = new renderer_decorator(ptr, alive); + ptr = r; + return r; + } + + static void unhook(df::renderer *& ptr, renderer_decorator * dec, color_ostream & out) { + dec->copy_to_inner(); + ptr = dec->inner; + dec->inner = 0; + delete dec; + } +}; + +DFHACK_PLUGIN("dfstream"); + +inline df::renderer *& active_renderer() { + return enabler->renderer; +} + +// This class is a smart pointer around a renderer_decorator. +// It should only be assigned r_d pointers that use the alive-pointer of this +// instance. +// If the r_d has been deleted by an external force, this smart pointer doesn't +// redelete it. +class auto_renderer_decorator { + renderer_decorator * p; +public: + // pass this member to the ctor of renderer_decorator + bool alive; + + auto_renderer_decorator() + : p(0) + { + } + + ~auto_renderer_decorator() { + reset(); + } + + void reset() { + if (*this) { + delete p; + p = 0; + } + } + + operator bool() { + return (p != 0) && alive; + } + + auto_renderer_decorator & operator=(renderer_decorator *p) { + reset(); + this->p = p; + return *this; + } + + renderer_decorator * get() { + return p; + } + + renderer_decorator * operator->() { + return get(); + } +}; + +auto_renderer_decorator decorator; + +DFhackCExport command_result plugin_init ( color_ostream &out, vector &commands) +{ + if (!df::renderer::_identity.can_instantiate()) + { + out.printerr("Cannot allocate a renderer\n"); + return CR_OK; + } + if (!decorator) { + decorator = renderer_decorator::hook(active_renderer(), &decorator.alive); + } + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown ( color_ostream &out ) +{ + if (decorator && active_renderer() == decorator.get()) + { + renderer_decorator::unhook(active_renderer(), decorator.get(), out); + } + decorator.reset(); + return CR_OK; +} +// vim:set sw=4 sts=4 et: diff --git a/plugins/dig.cpp b/plugins/dig.cpp index 21c199102..5e2bb9f89 100644 --- a/plugins/dig.cpp +++ b/plugins/dig.cpp @@ -24,7 +24,7 @@ command_result diglx (color_ostream &out, vector & parameters); command_result digauto (color_ostream &out, vector & parameters); command_result digexp (color_ostream &out, vector & parameters); command_result digcircle (color_ostream &out, vector & parameters); - +command_result digtype (color_ostream &out, vector & parameters); DFHACK_PLUGIN("dig"); @@ -57,6 +57,18 @@ DFhackCExport command_result plugin_init ( color_ostream &out, std::vector & parameters) mx.setDesignationAt(pos,des); } } - mx.WriteAll(); + mx.WriteAll(); } else for(uint32_t x = 0; x < x_max; x++) { @@ -1129,6 +1141,7 @@ command_result digv (color_ostream &out, vector & parameters) } } MCache->WriteAll(); + delete MCache; return CR_OK; } @@ -1342,6 +1355,7 @@ command_result digl (color_ostream &out, vector & parameters) } } MCache->WriteAll(); + delete MCache; return CR_OK; } @@ -1350,3 +1364,120 @@ command_result digauto (color_ostream &out, vector & parameters) { return CR_NOT_IMPLEMENTED; } + +command_result digtype (color_ostream &out, vector & parameters) +{ + //mostly copy-pasted from digv + CoreSuspender suspend; + if ( parameters.size() > 1 ) + { + out.printerr("Too many parameters.\n"); + return CR_FAILURE; + } + + uint32_t targetDigType; + if ( parameters.size() == 1 ) + { + string parameter = parameters[0]; + if ( parameter == "clear" ) + targetDigType = tile_dig_designation::No; + else if ( parameter == "dig" ) + targetDigType = tile_dig_designation::Default; + else if ( parameter == "updown" ) + targetDigType = tile_dig_designation::UpDownStair; + else if ( parameter == "channel" ) + targetDigType = tile_dig_designation::Channel; + else if ( parameter == "ramp" ) + targetDigType = tile_dig_designation::Ramp; + else if ( parameter == "down" ) + targetDigType = tile_dig_designation::DownStair; + else if ( parameter == "up" ) + targetDigType = tile_dig_designation::UpStair; + else + { + out.printerr("Invalid parameter.\n"); + return CR_FAILURE; + } + } + else + { + targetDigType = -1; + } + + if (!Maps::IsValid()) + { + out.printerr("Map is not available!\n"); + return CR_FAILURE; + } + + int32_t cx, cy, cz; + uint32_t xMax,yMax,zMax; + Maps::getSize(xMax,yMax,zMax); + uint32_t tileXMax = xMax * 16; + uint32_t tileYMax = yMax * 16; + Gui::getCursorCoords(cx,cy,cz); + if (cx == -30000) + { + out.printerr("Cursor is not active. Point the cursor at a vein.\n"); + return CR_FAILURE; + } + DFHack::DFCoord xy ((uint32_t)cx,(uint32_t)cy,cz); + MapExtras::MapCache * mCache = new MapExtras::MapCache; + df::tile_designation baseDes = mCache->designationAt(xy); + df::tiletype tt = mCache->tiletypeAt(xy); + int16_t veinmat = mCache->veinMaterialAt(xy); + if( veinmat == -1 ) + { + out.printerr("This tile is not a vein.\n"); + delete mCache; + return CR_FAILURE; + } + out.print("(%d,%d,%d) tiletype: %d, veinmat: %d, designation: 0x%x ... DIGGING!\n", cx,cy,cz, tt, veinmat, baseDes.whole); + + if ( targetDigType != -1 ) + { + baseDes.bits.dig = (tile_dig_designation::tile_dig_designation)targetDigType; + } + else + { + if ( baseDes.bits.dig == tile_dig_designation::No ) + { + baseDes.bits.dig = tile_dig_designation::Default; + } + } + + for( uint32_t z = 0; z < zMax; z++ ) + { + for( uint32_t x = 1; x < tileXMax-1; x++ ) + { + for( uint32_t y = 1; y < tileYMax-1; y++ ) + { + DFHack::DFCoord current(x,y,z); + int16_t vmat2 = mCache->veinMaterialAt(current); + if ( vmat2 != veinmat ) + continue; + tt = mCache->tiletypeAt(current); + if (!DFHack::isWallTerrain(tt)) + continue; + + //designate it for digging + df::tile_designation des = mCache->designationAt(current); + if ( !mCache->testCoord(current) ) + { + out.printerr("testCoord failed at (%d,%d,%d)\n", x, y, z); + delete mCache; + return CR_FAILURE; + } + + df::tile_designation designation = mCache->designationAt(current); + designation.bits.dig = baseDes.bits.dig; + mCache->setDesignationAt(current, designation); + } + } + } + + mCache->WriteAll(); + delete mCache; + return CR_OK; +} + diff --git a/plugins/fastdwarf.cpp b/plugins/fastdwarf.cpp index 5a2e2f206..6292fc85e 100644 --- a/plugins/fastdwarf.cpp +++ b/plugins/fastdwarf.cpp @@ -2,76 +2,179 @@ #include "Console.h" #include "Export.h" #include "PluginManager.h" +#include "modules/Units.h" +#include "modules/Maps.h" #include "DataDefs.h" -#include "df/ui.h" #include "df/world.h" #include "df/unit.h" +#include "df/map_block.h" using std::string; using std::vector; using namespace DFHack; using df::global::world; -using df::global::ui; // dfhack interface DFHACK_PLUGIN("fastdwarf"); +static bool enable_fastdwarf = false; +static bool enable_teledwarf = false; + DFhackCExport command_result plugin_shutdown ( color_ostream &out ) { + if (df::global::debug_turbospeed) + *df::global::debug_turbospeed = false; return CR_OK; } -static int enable_fastdwarf = false; - DFhackCExport command_result plugin_onupdate ( color_ostream &out ) { - // check run conditions - if(!world || !world->map.block_index || !enable_fastdwarf) + // do we even need to do anything at all? + if (!enable_fastdwarf && !enable_teledwarf) + return CR_OK; + + // make sure the world is actually loaded + if (!world || !world->map.block_index) { - // give up if we shouldn't be running' + enable_fastdwarf = enable_teledwarf = false; return CR_OK; } - int32_t race = ui->race_id; - int32_t civ = ui->civ_id; - for (size_t i = 0; i < world->units.all.size(); i++) + for (size_t i = 0; i < world->units.active.size(); i++) { - df::unit *unit = world->units.all[i]; + df::unit *unit = world->units.active[i]; + // citizens only + if (!Units::isCitizen(unit)) + continue; + + if (enable_fastdwarf) + { + if (unit->counters.job_counter > 0) + unit->counters.job_counter = 0; + // could also patch the unit->job.current_job->completion_timer + } + + if (enable_teledwarf) do + { + // don't do anything if the dwarf isn't going anywhere + if (!unit->path.dest.isValid()) + break; + + // skip dwarves that are dragging creatures or being dragged + if ((unit->relations.draggee_id != -1) || (unit->relations.dragger_id != -1)) + break; + + // skip dwarves that are following other units + if (unit->relations.following != 0) + break; + + // skip unconscious units + if (unit->counters.unconscious > 0) + break; + + // make sure source and dest map blocks are valid + auto old_occ = Maps::getTileOccupancy(unit->pos); + auto new_occ = Maps::getTileOccupancy(unit->path.dest); + if (!old_occ || !new_occ) + break; - if (unit->race == race && unit->civ_id == civ && unit->counters.job_counter > 0) - unit->counters.job_counter = 0; - // could also patch the unit->job.current_job->completion_timer + // clear appropriate occupancy flags at old tile + if (unit->flags1.bits.on_ground) + // this is technically wrong, but the game will recompute this as needed + old_occ->bits.unit_grounded = 0; + else + old_occ->bits.unit = 0; + + // if there's already somebody standing at the destination, then force the unit to lay down + if (new_occ->bits.unit) + unit->flags1.bits.on_ground = 1; + + // set appropriate occupancy flags at new tile + if (unit->flags1.bits.on_ground) + new_occ->bits.unit_grounded = 1; + else + new_occ->bits.unit = 1; + + // move unit to destination + unit->pos = unit->path.dest; + unit->path.path.clear(); + } while (0); } return CR_OK; } static command_result fastdwarf (color_ostream &out, vector & parameters) { - if (parameters.size() == 1 && (parameters[0] == "0" || parameters[0] == "1")) + if (parameters.size() > 2) + return CR_WRONG_USAGE; + + if ((parameters.size() == 1) || (parameters.size() == 2)) { + if (parameters.size() == 2) + { + if (parameters[1] == "0") + enable_teledwarf = false; + else if (parameters[1] == "1") + enable_teledwarf = true; + else + return CR_WRONG_USAGE; + } + else + enable_teledwarf = false; if (parameters[0] == "0") - enable_fastdwarf = 0; + { + enable_fastdwarf = false; + if (df::global::debug_turbospeed) + *df::global::debug_turbospeed = false; + } + else if (parameters[0] == "1") + { + enable_fastdwarf = true; + if (df::global::debug_turbospeed) + *df::global::debug_turbospeed = false; + } + else if (parameters[0] == "2") + { + if (df::global::debug_turbospeed) + { + enable_fastdwarf = false; + *df::global::debug_turbospeed = true; + } + else + { + out.print("Speed level 2 not available.\n"); + return CR_FAILURE; + } + } else - enable_fastdwarf = 1; - out.print("fastdwarf %sactivated.\n", (enable_fastdwarf ? "" : "de")); - } - else - { - out.print("Makes your minions move at ludicrous speeds.\n" - "Activate with 'fastdwarf 1', deactivate with 'fastdwarf 0'.\n" - "Current state: %d.\n", enable_fastdwarf); + return CR_WRONG_USAGE; } + out.print("Current state: fast = %d, teleport = %d.\n", + (df::global::debug_turbospeed && *df::global::debug_turbospeed) ? 2 : (enable_fastdwarf ? 1 : 0), + enable_teledwarf ? 1 : 0); + return CR_OK; } DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) { commands.push_back(PluginCommand("fastdwarf", - "enable/disable fastdwarf (parameter=0/1)", - fastdwarf)); - + "enable/disable fastdwarf and teledwarf (parameters=0/1)", + fastdwarf, false, + "fastdwarf: make dwarves faster.\n" + "Usage:\n" + " fastdwarf (tele)\n" + "Valid values for speed:\n" + " * 0 - Make dwarves move and work at standard speed.\n" + " * 1 - Make dwarves move and work at maximum speed.\n" + " * 2 - Make ALL creatures move and work at maximum speed.\n" + "Valid values for (tele):\n" + " * 0 - Disable dwarf teleportation (default)\n" + " * 1 - Make dwarves teleport to their destinations instantly.\n" + )); + return CR_OK; } diff --git a/plugins/fix-armory.cpp b/plugins/fix-armory.cpp new file mode 100644 index 000000000..ade9e4252 --- /dev/null +++ b/plugins/fix-armory.cpp @@ -0,0 +1,800 @@ +// Fixes containers in barracks to actually work as intended. + +#include "Core.h" +#include "Console.h" +#include "Export.h" +#include "PluginManager.h" + +#include "modules/Gui.h" +#include "modules/Screen.h" +#include "modules/Units.h" +#include "modules/Items.h" +#include "modules/Job.h" +#include "modules/World.h" +#include "modules/Maps.h" + +#include "MiscUtils.h" + +#include "DataDefs.h" +#include +#include "df/ui.h" +#include "df/world.h" +#include "df/squad.h" +#include "df/unit.h" +#include "df/squad_position.h" +#include "df/squad_ammo_spec.h" +#include "df/items_other_id.h" +#include "df/item_weaponst.h" +#include "df/item_armorst.h" +#include "df/item_helmst.h" +#include "df/item_pantsst.h" +#include "df/item_shoesst.h" +#include "df/item_glovesst.h" +#include "df/item_shieldst.h" +#include "df/item_flaskst.h" +#include "df/item_backpackst.h" +#include "df/item_quiverst.h" +#include "df/item_ammost.h" +#include "df/building_weaponrackst.h" +#include "df/building_armorstandst.h" +#include "df/building_cabinetst.h" +#include "df/building_boxst.h" +#include "df/building_squad_use.h" +#include "df/job.h" +#include "df/general_ref_building_holderst.h" +#include "df/general_ref_building_destinationst.h" +#include "df/barrack_preference_category.h" + +#include + +using std::vector; +using std::string; +using std::endl; +using namespace DFHack; +using namespace df::enums; + +using df::global::ui; +using df::global::world; +using df::global::gamemode; +using df::global::ui_build_selector; +using df::global::ui_menu_width; +using df::global::ui_area_map_width; + +using namespace DFHack::Gui; +using Screen::Pen; + +static command_result fix_armory(color_ostream &out, vector & parameters); + +DFHACK_PLUGIN("fix-armory"); + +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event); + +DFhackCExport command_result plugin_init (color_ostream &out, std::vector &commands) +{ + commands.push_back(PluginCommand( + "fix-armory", "Enables or disables the fix-armory plugin.", fix_armory, false, + " fix-armory enable\n" + " Enables the tweaks.\n" + " fix-armory disable\n" + " Disables the tweaks. All equipment will be hauled off to stockpiles.\n" + )); + + if (Core::getInstance().isMapLoaded()) + plugin_onstatechange(out, SC_MAP_LOADED); + + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown (color_ostream &out) +{ + return CR_OK; +} + +/* + * PART 1 - Stop restockpiling of items stored in the armory. + * + * For everything other than ammo this is quite straightforward, + * since the uniform switch code already tries to store items + * in barracks containers, and it is thus known what the intention + * is. Moreover these containers know which squad and member they + * belong to. + * + * For ammo there is no such code (in fact, ammo is never removed + * from a quiver, except when it is dropped itself), so I had to + * apply some improvisation. There is one place where BOX containers + * with Squad Equipment set are used as an anchor location for a + * pathfinding check when assigning ammo, so presumably that's + * the correct place. I however wanted to also differentiate + * training ammo, so came up with the following rules: + * + * 1. Combat ammo and ammo without any allowed use can be stored + * in BOXes marked for Squad Equipment, either directly or via + * containing room. No-allowed-use ammo is assumed to be reserved + * for emergency combat use, or something like that. + * 1a. If assigned to a squad position, that box can be used _only_ + * for ammo assigned to that specific _squad_. Otherwise, if + * multiple squads can use this room, they will store their + * ammo all mixed up. + * 2. Training ammo can be stored in BOXes within archery ranges + * (designated from archery target) that are enabled for Training. + * Train-only ammo in particular can _only_ be stored in such + * boxes. The inspiration for this comes from some broken code + * for weapon racks in Training rooms. + * + * As an additional feature (partially needed due to the constraints + * of working from an external hack), this plugin also blocks instant + * queueing of stockpiling jobs for items blocked on the ground, if + * these items are assigned to any squad. + * + * Since there apparently still are bugs that cause uniform items to be + * momentarily dropped on ground, this delay is set not to the minimally + * necessary 50 ticks, but to 0.5 - 1.0 in-game days, so as to provide a + * grace period during which the items can be instantly picked up again. + */ + +// Check if the item is assigned to any use controlled by the military tab +static bool is_assigned_item(df::item *item) +{ + if (!ui) + return false; + + auto type = item->getType(); + int idx = binsearch_index(ui->equipment.items_assigned[type], item->id); + if (idx < 0) + return false; + + // Exclude weapons used by miners, wood cutters etc + switch (type) { + case item_type::WEAPON: + // the game code also checks this for ammo, funnily enough + // maybe it's not just for weapons?.. + if (binsearch_index(ui->equipment.work_weapons, item->id) >= 0) + return false; + break; + + default: + break; + } + + return true; +} + +// Check if this ammo item is assigned to this squad with one of the specified uses +static bool is_squad_ammo(df::item *item, df::squad *squad, bool train, bool combat) +{ + for (size_t i = 0; i < squad->ammunition.size(); i++) + { + auto spec = squad->ammunition[i]; + bool cs = spec->flags.bits.use_combat; + bool ts = spec->flags.bits.use_training; + + // no-use ammo assumed to be combat + if (((cs || !ts) && combat) || (ts && train)) + { + if (binsearch_index(spec->assigned, item->id) >= 0) + return true; + } + } + + return false; +} + +// Recursively check room parents to find out if this ammo item is allowed here +static bool can_store_ammo_rec(df::item *item, df::building *holder, int squad_id) +{ + auto squads = holder->getSquads(); + + if (squads) + { + bool target = holder->getType() == building_type::ArcheryTarget; + + for (size_t i = 0; i < squads->size(); i++) + { + auto use = (*squads)[i]; + + // For containers assigned to a squad, only consider that squad + if (squad_id >= 0 && use->squad_id != squad_id) + continue; + + // Squad Equipment -> combat + bool combat = use->mode.bits.squad_eq; + // Archery target with Train -> training + bool train = target && use->mode.bits.train; + + if (combat || train) + { + auto squad = df::squad::find(use->squad_id); + + if (squad && is_squad_ammo(item, squad, combat, train)) + return true; + } + } + } + + for (size_t i = 0; i < holder->parents.size(); i++) + if (can_store_ammo_rec(item, holder->parents[i], squad_id)) + return true; + + return false; +} + +// Check if the ammo item can be stored in this container +static bool can_store_ammo(df::item *item, df::building *holder) +{ + // Only chests + if (holder->getType() != building_type::Box) + return false; + + // with appropriate flags set + return can_store_ammo_rec(item, holder, holder->getSpecificSquad()); +} + +// Check if the item is assigned to the squad member who owns this armory building +static bool belongs_to_position(df::item *item, df::building *holder) +{ + int sid = holder->getSpecificSquad(); + if (sid < 0) + return false; + + auto squad = df::squad::find(sid); + if (!squad) + return false; + + int position = holder->getSpecificPosition(); + + // Weapon racks belong to the whole squad, i.e. can be used by any position + if (position == -1 && holder->getType() == building_type::Weaponrack) + { + for (size_t i = 0; i < squad->positions.size(); i++) + { + if (binsearch_index(squad->positions[i]->assigned_items, item->id) >= 0) + return true; + } + } + else + { + auto cpos = vector_get(squad->positions, position); + if (cpos && binsearch_index(cpos->assigned_items, item->id) >= 0) + return true; + } + + return false; +} + +// Check if the item is appropriately stored in an armory building +static bool is_in_armory(df::item *item) +{ + if (item->flags.bits.in_inventory || item->flags.bits.on_ground) + return false; + + auto holder = Items::getHolderBuilding(item); + if (!holder) + return false; + + // If indeed in a building, check if it is the right one + if (item->getType() == item_type::AMMO) + return can_store_ammo(item, holder); + else + return belongs_to_position(item, holder); +} + +/* + * Hooks used to affect stockpiling code as it runs, and prevent it + * from doing unwanted stuff. + * + * Toady can simply add these checks directly to the stockpiling code; + * we have to abuse some handy item vmethods. + */ + +template struct armory_hook : Item { + typedef Item interpose_base; + + /* + * This vmethod is called by the actual stockpiling code before it + * tries to queue a job, and is normally used to prevent stockpiling + * of uncollected webs. + */ + DEFINE_VMETHOD_INTERPOSE(bool, isCollected, ()) + { + // Block stockpiling of items in the armory. + if (is_in_armory(this)) + return false; + + /* + * When an item is removed from inventory due to Pickup Equipment + * process, the unit code directly invokes the stockpiling code + * and thus creates the job even before the item is actually dropped + * on the ground. We don't want this at all, especially due to the + * grace period idea. + * + * With access to source, that code can just be changed to simply + * drop the item on ground, without running stockpiling code. + */ + if (this->flags.bits.in_inventory) + { + auto holder = Items::getHolderUnit(this); + + // When that call happens, the item is still in inventory + if (holder && is_assigned_item(this)) + { + // And its ID is is this vector + if (::binsearch_index(holder->military.uniform_drop, this->id) >= 0) + return false; + } + } + + // Call the original vmethod + return INTERPOSE_NEXT(isCollected)(); + } + + /* + * This vmethod is used to actually put the item on the ground. + * When it does that, it also adds it to a vector of items to be + * instanly restockpiled by a loop in another part of the code. + * + * We don't want this either, even more than when removing from + * uniform, because this can happen in lots of various situations, + * including deconstructed containers etc, and we want our own + * armory code below to have a chance to look at the item. + * + * The logical place for this code is in the loop that processes + * that vector, but that part is not virtual. + */ + DEFINE_VMETHOD_INTERPOSE(bool, moveToGround, (int16_t x, int16_t y, int16_t z)) + { + // First, let it do its work + bool rv = INTERPOSE_NEXT(moveToGround)(x, y, z); + + // Prevent instant restockpiling of dropped assigned items. + if (is_assigned_item(this)) + { + // The original vmethod adds the item to this vector to force instant check + auto &ovec = world->items.other[items_other_id::ANY_RECENTLY_DROPPED]; + + // If it is indeed there, remove it + if (erase_from_vector(ovec, &df::item::id, this->id)) + { + // and queue it to be checked normally in 0.5-1 in-game days + // (this is a grace period in case the uniform is dropped just + // for a moment due to a momentary glitch) + this->stockpile_countdown = 12 + random_int(12); + this->stockpile_delay = 0; + } + } + + return rv; + } +}; + +template<> IMPLEMENT_VMETHOD_INTERPOSE(armory_hook, isCollected); +template<> IMPLEMENT_VMETHOD_INTERPOSE(armory_hook, isCollected); +template<> IMPLEMENT_VMETHOD_INTERPOSE(armory_hook, isCollected); +template<> IMPLEMENT_VMETHOD_INTERPOSE(armory_hook, isCollected); +template<> IMPLEMENT_VMETHOD_INTERPOSE(armory_hook, isCollected); +template<> IMPLEMENT_VMETHOD_INTERPOSE(armory_hook, isCollected); +template<> IMPLEMENT_VMETHOD_INTERPOSE(armory_hook, isCollected); +template<> IMPLEMENT_VMETHOD_INTERPOSE(armory_hook, isCollected); +template<> IMPLEMENT_VMETHOD_INTERPOSE(armory_hook, isCollected); +template<> IMPLEMENT_VMETHOD_INTERPOSE(armory_hook, isCollected); +template<> IMPLEMENT_VMETHOD_INTERPOSE(armory_hook, isCollected); + +template<> IMPLEMENT_VMETHOD_INTERPOSE(armory_hook, moveToGround); +template<> IMPLEMENT_VMETHOD_INTERPOSE(armory_hook, moveToGround); +template<> IMPLEMENT_VMETHOD_INTERPOSE(armory_hook, moveToGround); +template<> IMPLEMENT_VMETHOD_INTERPOSE(armory_hook, moveToGround); +template<> IMPLEMENT_VMETHOD_INTERPOSE(armory_hook, moveToGround); +template<> IMPLEMENT_VMETHOD_INTERPOSE(armory_hook, moveToGround); +template<> IMPLEMENT_VMETHOD_INTERPOSE(armory_hook, moveToGround); +template<> IMPLEMENT_VMETHOD_INTERPOSE(armory_hook, moveToGround); +template<> IMPLEMENT_VMETHOD_INTERPOSE(armory_hook, moveToGround); +template<> IMPLEMENT_VMETHOD_INTERPOSE(armory_hook, moveToGround); +template<> IMPLEMENT_VMETHOD_INTERPOSE(armory_hook, moveToGround); + +/* + * PART 2 - Actively queue jobs to haul assigned items to the armory. + * + * The logical place for this of course is in the same code that decides + * to put stuff in stockpiles, alongside the checks to prevent moving + * stuff away from the armory. We just run it independently every 50 + * simulation frames. + */ + +// Check if this item is loose and can be moved to armory +static bool can_store_item(df::item *item) +{ + // bad id, or cooldown timer still counting + if (!item || item->stockpile_countdown > 0) + return false; + + // bad flags? + if (item->flags.bits.in_job || + item->flags.bits.removed || + item->flags.bits.in_building || + item->flags.bits.encased || + item->flags.bits.owned || + item->flags.bits.forbid || + item->flags.bits.on_fire) + return false; + + // in unit inventory? + auto top = item; + + while (top->flags.bits.in_inventory) + { + auto parent = Items::getContainer(top); + if (!parent) break; + top = parent; + } + + if (Items::getGeneralRef(top, general_ref_type::UNIT_HOLDER)) + return false; + + // already in armory? + if (is_in_armory(item)) + return false; + + return true; +} + +// Queue a job to store the item in the building, if possible +static bool try_store_item(df::building *target, df::item *item) +{ + // Check if the dwarves can path between the target and the item + df::coord tpos(target->centerx, target->centery, target->z); + df::coord ipos = Items::getPosition(item); + + if (!Maps::canWalkBetween(tpos, ipos)) + return false; + + // Check if the target has enough space left + if (!target->canStoreItem(item, true)) + return false; + + // Create the job + auto href = df::allocate(); + if (!href) + return false; + + auto job = new df::job(); + + job->pos = tpos; + + bool dest = false; + + // Choose the job type - correct matching is needed so that + // later canStoreItem calls would take the job into account. + switch (target->getType()) { + case building_type::Weaponrack: + job->job_type = job_type::StoreWeapon; + // Without this flag dwarves will pick up the item, and + // then dismiss the job and put it back into the stockpile: + job->flags.bits.specific_dropoff = true; + break; + case building_type::Armorstand: + job->job_type = job_type::StoreArmor; + job->flags.bits.specific_dropoff = true; + break; + case building_type::Cabinet: + job->job_type = job_type::StoreItemInCabinet; + dest = true; + break; + default: + job->job_type = job_type::StoreItemInChest; + dest = true; + break; + } + + // job <-> item link + if (!Job::attachJobItem(job, item, df::job_item_ref::Hauled)) + { + delete job; + delete href; + return false; + } + + // job <-> building link + href->building_id = target->id; + target->jobs.push_back(job); + job->references.push_back(href); + + // Two of the jobs need this link to find the job in canStoreItem(). + // They also don't actually need BUILDING_HOLDER, but it doesn't hurt. + if (dest) + { + auto rdest = df::allocate(); + + if (rdest) + { + rdest->building_id = target->id; + job->references.push_back(rdest); + } + } + + // add to job list + Job::linkIntoWorld(job); + return true; +} + +// Store the item into the first building in the list that would accept it. +static void try_store_item(std::vector &vec, df::item *item) +{ + for (size_t i = 0; i < vec.size(); i++) + { + auto target = df::building::find(vec[i]); + if (!target) + continue; + + if (try_store_item(target, item)) + return; + } +} + +// Store the items into appropriate armory buildings +static void try_store_item_set(std::vector &items, df::squad *squad, df::squad_position *pos) +{ + for (size_t j = 0; j < items.size(); j++) + { + auto item = df::item::find(items[j]); + + // not loose + if (!can_store_item(item)) + continue; + + // queue jobs to put it in the appropriate container + if (item->isWeapon()) + try_store_item(squad->rack_combat, item); + else if (item->isClothing()) + try_store_item(pos->preferences[barrack_preference_category::Cabinet], item); + else if (item->isArmorNotClothing()) + try_store_item(pos->preferences[barrack_preference_category::Armorstand], item); + else + try_store_item(pos->preferences[barrack_preference_category::Box], item); + } +} + +// For storing ammo, use a data structure sorted by free space, to even out the load +typedef std::map > ammo_box_set; + +// Enumerate boxes in the room, adding them to the set +static void index_boxes(df::building *root, ammo_box_set &group, int squad_id) +{ + if (root->getType() == building_type::Box) + { + int id = root->getSpecificSquad(); + + if (id < 0 || id == squad_id) + { + //color_ostream_proxy out(Core::getInstance().getConsole()); + //out.print("%08x %d\n", unsigned(root), root->getFreeCapacity(true)); + + group[root->getFreeCapacity(true)].insert(root); + } + } + + for (size_t i = 0; i < root->children.size(); i++) + index_boxes(root->children[i], group, squad_id); +} + +// Loop over the set from most empty to least empty +static bool try_store_ammo(df::item *item, ammo_box_set &group) +{ + int volume = item->getVolume(); + + for (auto it = group.rbegin(); it != group.rend(); ++it) + { + if (it->first < volume) + break; + + for (auto it2 = it->second.begin(); it2 != it->second.end(); ++it2) + { + auto bld = *it2; + + if (try_store_item(bld, item)) + { + it->second.erase(bld); + group[bld->getFreeCapacity(true)].insert(bld); + return true; + } + } + } + + return false; +} + +// Collect chests for ammo storage +static void index_ammo_boxes(df::squad *squad, ammo_box_set &train_set, ammo_box_set &combat_set) +{ + for (size_t j = 0; j < squad->rooms.size(); j++) + { + auto room = squad->rooms[j]; + auto bld = df::building::find(room->building_id); + + // Chests in rooms marked for Squad Equipment used for combat ammo + if (room->mode.bits.squad_eq) + index_boxes(bld, combat_set, squad->id); + + // Chests in archery ranges used for training ammo + if (room->mode.bits.train && bld->getType() == building_type::ArcheryTarget) + index_boxes(bld, train_set, squad->id); + } +} + +// Store ammo into appropriate chests +static void try_store_ammo(df::squad *squad) +{ + bool indexed = false; + ammo_box_set train_set, combat_set; + + for (size_t i = 0; i < squad->ammunition.size(); i++) + { + auto spec = squad->ammunition[i]; + bool cs = spec->flags.bits.use_combat; + bool ts = spec->flags.bits.use_training; + + for (size_t j = 0; j < spec->assigned.size(); j++) + { + auto item = df::item::find(spec->assigned[j]); + + // not loose + if (!can_store_item(item)) + continue; + + // compute the maps lazily + if (!indexed) + { + indexed = true; + index_ammo_boxes(squad, train_set, combat_set); + } + + // BUG: if the same container is in both sets, + // when a job is queued, the free space in the other + // set will not be updated, which could lead to uneven + // loading - but not to overflowing the container! + + // As said above, combat goes into Squad Equipment + if (cs && try_store_ammo(item, combat_set)) + continue; + // Training goes into Archery Range with Train + if (ts && try_store_ammo(item, train_set)) + continue; + // No use goes into combat + if (!(ts || cs) && try_store_ammo(item, combat_set)) + continue; + } + } +} + +static bool is_enabled = false; + +DFhackCExport command_result plugin_onupdate(color_ostream &out, state_change_event event) +{ + if (!is_enabled) + return CR_OK; + + // Process every 50th frame, sort of like regular stockpiling does + if (DF_GLOBAL_VALUE(cur_year_tick,1) % 50 != 0) + return CR_OK; + + // Loop over squads + auto &squads = df::global::world->squads.all; + + for (size_t si = 0; si < squads.size(); si++) + { + auto squad = squads[si]; + + for (size_t i = 0; i < squad->positions.size(); i++) + { + auto pos = squad->positions[i]; + + try_store_item_set(pos->assigned_items, squad, pos); + } + + try_store_ammo(squad); + } + + return CR_OK; +} + +static void enable_hook(color_ostream &out, VMethodInterposeLinkBase &hook, bool enable) +{ + if (!hook.apply(enable)) + out.printerr("Could not %s hook.\n", enable?"activate":"deactivate"); +} + +static void enable_hooks(color_ostream &out, bool enable) +{ + is_enabled = enable; + + enable_hook(out, INTERPOSE_HOOK(armory_hook, isCollected), enable); + enable_hook(out, INTERPOSE_HOOK(armory_hook, isCollected), enable); + enable_hook(out, INTERPOSE_HOOK(armory_hook, isCollected), enable); + enable_hook(out, INTERPOSE_HOOK(armory_hook, isCollected), enable); + enable_hook(out, INTERPOSE_HOOK(armory_hook, isCollected), enable); + enable_hook(out, INTERPOSE_HOOK(armory_hook, isCollected), enable); + enable_hook(out, INTERPOSE_HOOK(armory_hook, isCollected), enable); + enable_hook(out, INTERPOSE_HOOK(armory_hook, isCollected), enable); + enable_hook(out, INTERPOSE_HOOK(armory_hook, isCollected), enable); + enable_hook(out, INTERPOSE_HOOK(armory_hook, isCollected), enable); + enable_hook(out, INTERPOSE_HOOK(armory_hook, isCollected), enable); + + enable_hook(out, INTERPOSE_HOOK(armory_hook, moveToGround), enable); + enable_hook(out, INTERPOSE_HOOK(armory_hook, moveToGround), enable); + enable_hook(out, INTERPOSE_HOOK(armory_hook, moveToGround), enable); + enable_hook(out, INTERPOSE_HOOK(armory_hook, moveToGround), enable); + enable_hook(out, INTERPOSE_HOOK(armory_hook, moveToGround), enable); + enable_hook(out, INTERPOSE_HOOK(armory_hook, moveToGround), enable); + enable_hook(out, INTERPOSE_HOOK(armory_hook, moveToGround), enable); + enable_hook(out, INTERPOSE_HOOK(armory_hook, moveToGround), enable); + enable_hook(out, INTERPOSE_HOOK(armory_hook, moveToGround), enable); + enable_hook(out, INTERPOSE_HOOK(armory_hook, moveToGround), enable); + enable_hook(out, INTERPOSE_HOOK(armory_hook, moveToGround), enable); +} + +static void enable_plugin(color_ostream &out) +{ + auto entry = World::GetPersistentData("fix-armory/enabled", NULL); + if (!entry.isValid()) + { + out.printerr("Could not save the status.\n"); + return; + } + + enable_hooks(out, true); +} + +static void disable_plugin(color_ostream &out) +{ + auto entry = World::GetPersistentData("fix-armory/enabled"); + World::DeletePersistentData(entry); + + enable_hooks(out, false); +} + +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) +{ + switch (event) { + case SC_MAP_LOADED: + if (!gamemode || *gamemode == game_mode::DWARF) + { + bool enable = World::GetPersistentData("fix-armory/enabled").isValid(); + + if (enable) + { + out.print("Enabling the fix-armory plugin.\n"); + enable_hooks(out, true); + } + else + enable_hooks(out, false); + } + break; + case SC_MAP_UNLOADED: + enable_hooks(out, false); + default: + break; + } + + return CR_OK; +} + +static command_result fix_armory(color_ostream &out, vector ¶meters) +{ + CoreSuspender suspend; + + if (parameters.empty()) + return CR_WRONG_USAGE; + + string cmd = parameters[0]; + + if (cmd == "enable") + { + enable_plugin(out); + } + else if (cmd == "disable") + { + disable_plugin(out); + } + else + return CR_WRONG_USAGE; + + return CR_OK; +} diff --git a/plugins/follow.cpp b/plugins/follow.cpp index 96693df8d..5c14780a3 100644 --- a/plugins/follow.cpp +++ b/plugins/follow.cpp @@ -65,8 +65,7 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) { if (!followedUnit) return CR_OK; //Don't do anything if we're not following a unit - DFHack::World *world = Core::getInstance().getWorld(); - if (world->ReadPauseState() && prevX==-1) return CR_OK; //Wait until the game is unpaused after first running "follow" to begin following + if (World::ReadPauseState() && prevX==-1) return CR_OK; //Wait until the game is unpaused after first running "follow" to begin following df::coord &unitPos = followedUnit->pos; @@ -120,7 +119,7 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) Gui::setViewCoords(x, y, z); //If, for some reason, the cursor is active and the screen is still moving, move the cursor along with the screen - if (c_x != -30000 && !world->ReadPauseState()) + if (c_x != -30000 && !World::ReadPauseState()) Gui::setCursorCoords(c_x - (prevX-x), c_y - (prevY-y), z); //Save this round's stuff for next time so we can monitor for changes made by the user diff --git a/plugins/lua/sort/items.lua b/plugins/lua/sort/items.lua index 13b62ff9b..5c6d4a5ba 100644 --- a/plugins/lua/sort/items.lua +++ b/plugins/lua/sort/items.lua @@ -23,12 +23,18 @@ orders.description = { end } -orders.quality = { +orders.base_quality = { key = function(item) return item:getQuality() end } +orders.quality = { + key = function(item) + return item:getOverallQuality() + end +} + orders.improvement = { key = function(item) return item:getImprovementQuality() diff --git a/plugins/lua/sort/units.lua b/plugins/lua/sort/units.lua index d8ae83a38..c124e52d9 100644 --- a/plugins/lua/sort/units.lua +++ b/plugins/lua/sort/units.lua @@ -93,7 +93,7 @@ orders.race = { orders.squad = { key = function(unit) - local sidx = unit.military.squad_index + local sidx = unit.military.squad_id if sidx >= 0 then return sidx end @@ -102,7 +102,7 @@ orders.squad = { orders.squad_position = { key = function(unit) - local sidx = unit.military.squad_index + local sidx = unit.military.squad_id if sidx >= 0 then return sidx * 1000 + unit.military.squad_position end @@ -115,4 +115,4 @@ orders.happiness = { end } -return _ENV \ No newline at end of file +return _ENV diff --git a/plugins/lua/workflow.lua b/plugins/lua/workflow.lua new file mode 100644 index 000000000..4c011b24c --- /dev/null +++ b/plugins/lua/workflow.lua @@ -0,0 +1,326 @@ +local _ENV = mkmodule('plugins.workflow') + +local utils = require 'utils' + +--[[ + + Native functions: + + * isEnabled() + * setEnabled(enable) + * listConstraints([job]) -> {...} + * setConstraint(token[, by_count, goal, gap]) -> {...} + * deleteConstraint(token) -> true/false + +--]] + +local reaction_id_cache = nil + +if dfhack.is_core_context then + dfhack.onStateChange[_ENV] = function(code) + if code == SC_MAP_LOADED then + reaction_id_cache = nil + end + end +end + +local function get_reaction(name) + if not reaction_id_cache then + reaction_id_cache = {} + for i,v in ipairs(df.global.world.raws.reactions) do + reaction_id_cache[v.code] = i + end + end + local id = reaction_id_cache[name] or -1 + return id, df.reaction.find(id) +end + +local job_outputs = {} + +function job_outputs.CustomReaction(callback, job) + local rid, r = get_reaction(job.reaction_name) + + if not r then + return + end + + for i,prod in ipairs(r.products) do + if df.reaction_product_itemst:is_instance(prod) then + local mat_type, mat_index = prod.mat_type, prod.mat_index + local mat_mask + + local get_mat_prod = prod.flags.GET_MATERIAL_PRODUCT + if get_mat_prod or prod.flags.GET_MATERIAL_SAME then + local reagent_code = prod.get_material.reagent_code + local reagent_idx, src = utils.linear_index(r.reagents, reagent_code, 'code') + if not reagent_idx then goto continue end + + local item_idx, jitem = utils.linear_index(job.job_items, reagent_idx, 'reagent_index') + if jitem then + mat_type, mat_index = jitem.mat_type, jitem.mat_index + else + if not df.reaction_reagent_itemst:is_instance(src) then goto continue end + mat_type, mat_index = src.mat_type, src.mat_index + end + + if get_mat_prod then + local p_code = prod.get_material.product_code + local mat = dfhack.matinfo.decode(mat_type, mat_index) + + mat_type, mat_index = -1, -1 + + if mat then + local rp = mat.material.reaction_product + local idx = utils.linear_index(rp.id, p_code) + if not idx then + goto continue + end + mat_type, mat_index = rp.material.mat_type[idx], rp.material.mat_index[idx] + else + if p_code == "SOAP_MAT" then + mat_mask = { soap = true } + end + end + end + end + + callback{ + is_craft = prod.flags.CRAFTS, + item_type = prod.item_type, item_subtype = prod.item_subtype, + mat_type = mat_type, mat_index = mat_index, mat_mask = mat_mask + } + end + ::continue:: + end +end + +local function guess_job_material(job) + if job.job_type == df.job_type.PrepareMeal then + return -1, -1, nil + end + + local mat_type, mat_index = job.mat_type, job.mat_index + local mask_whole = job.material_category.whole + local mat_mask + + local jmat = df.job_type.attrs[job.job_type].material + if jmat then + mat_type, mat_index = df.builtin_mats[jmat] or -1, -1 + if mat_type < 0 and df.dfhack_material_category[jmat] then + mat_mask = { [jmat] = true } + end + end + + if not mat_mask and mask_whole ~= 0 then + mat_mask = utils.parse_bitfield_int(mask_whole, df.job_material_category) + if mat_mask.wood2 then + mat_mask.wood = true + mat_mask.wood2 = nil + end + end + + if mat_type < 0 and #job.job_items > 0 then + local item0 = job.job_items[0] + if #job.job_items == 1 or item0.item_type == df.item_type.PLANT then + mat_type, mat_index = item0.mat_type, item0.mat_index + + if item0.item_type == df.item_type.WOOD then + mat_mask = mat_mask or {} + mat_mask.wood = true + end + end + end + + return mat_type, mat_index, mat_mask +end + +function default_output(callback, job, mat_type, mat_index, mat_mask) + local itype = df.job_type.attrs[job.job_type].item + if itype >= 0 then + local subtype = nil + if df.item_type.attrs[itype].is_rawable then + subtype = job.item_subtype + end + callback{ + item_type = itype, item_subtype = subtype, + mat_type = mat_type, mat_index = mat_index, mat_mask = mat_mask + } + end +end + +function job_outputs.SmeltOre(callback, job) + local mat = dfhack.matinfo.decode(job.job_items[0]) + if mat and mat.inorganic then + for i,v in ipairs(mat.inorganic.metal_ore.mat_index) do + callback{ item_type = df.item_type.BAR, mat_type = 0, mat_index = v } + end + else + callback{ item_type = df.item_type.BAR, mat_type = 0 } + end +end + +function job_outputs.ExtractMetalStrands(callback, job) + local mat = dfhack.matinfo.decode(job.job_items[0]) + if mat and mat.inorganic then + for i,v in ipairs(mat.inorganic.thread_metal.mat_index) do + callback{ item_type = df.item_type.THREAD, mat_type = 0, mat_index = v } + end + else + callback{ item_type = df.item_type.THREAD, mat_type = 0 } + end +end + +function job_outputs.PrepareMeal(callback, job) + if job.mat_type ~= -1 then + for i,v in ipairs(df.global.world.raws.itemdefs.food) do + if v.level == job.mat_type then + callback{ item_type = df.item_type.FOOD, item_subtype = i } + end + end + else + callback{ item_type = df.item_type.FOOD } + end +end + +function job_outputs.MakeCrafts(callback, job) + local mat_type, mat_index, mat_mask = guess_job_material(job) + callback{ is_craft = true, mat_type = mat_type, mat_index = mat_index, mat_mask = mat_mask } +end + +local plant_products = { + BrewDrink = 'DRINK', + MillPlants = 'MILL', + ProcessPlants = 'THREAD', + ProcessPlantsBag = 'LEAVES', + ProcessPlantsBarrel = 'EXTRACT_BARREL', + ProcessPlantsVial = 'EXTRACT_VIAL', + ExtractFromPlants = 'EXTRACT_STILL_VIAL', +} + +for job,flag in pairs(plant_products) do + local ttag = 'type_'..string.lower(flag) + local itag = 'idx_'..string.lower(flag) + job_outputs[job] = function(callback, job) + local mat_type, mat_index = -1, -1 + local mat = dfhack.matinfo.decode(job.job_items[0]) + if mat and mat.plant and mat.plant.flags[flag] then + mat_type = mat.plant.material_defs[ttag] + mat_index = mat.plant.material_defs[itag] + end + default_output(callback, job, mat_type, mat_index) + end +end + +local function enum_job_outputs(callback, job) + local handler = job_outputs[df.job_type[job.job_type]] + if handler then + handler(callback, job) + else + default_output(callback, job, guess_job_material(job)) + end +end + +function doEnumJobOutputs(native_cb, job) + local function cb(info) + native_cb( + info.item_type, info.item_subtype, + info.mat_mask, info.mat_type, info.mat_index, + info.is_craft + ) + end + + enum_job_outputs(cb, job) +end + +function listJobOutputs(job) + local res = {} + enum_job_outputs(curry(table.insert, res), job) + return res +end + +function constraintToToken(cspec) + local token + if cspec.is_craft then + token = 'CRAFTS' + else + token = df.item_type[cspec.item_type] or error('invalid item type: '..cspec.item_type) + if cspec.item_subtype and cspec.item_subtype >= 0 then + local def = dfhack.items.getSubtypeDef(cspec.item_type, cspec.item_subtype) + if def then + token = token..':'..def.id + else + error('invalid subtype '..cspec.item_subtype..' of '..token) + end + end + end + local mask_part + if cspec.mat_mask then + mask_part = table.concat(utils.list_bitfield_flags(cspec.mat_mask), ',') + end + local mat_part + if cspec.mat_type and cspec.mat_type >= 0 then + local mat = dfhack.matinfo.decode(cspec.mat_type, cspec.mat_index or -1) + if mat then + mat_part = mat:getToken() + else + error('invalid material: '..cspec.mat_type..':'..(cspec.mat_index or -1)) + end + end + local qpart + if cspec.quality and cspec.quality > 0 then + qpart = df.item_quality[cspec.quality] or error('invalid quality: '..cspec.quality) + end + + if mask_part or mat_part or qpart then + token = token .. '/' .. (mask_part or '') + if mat_part or qpart then + token = token .. '/' .. (mat_part or '') + if qpart then + token = token .. '/' .. (qpart or '') + end + end + end + + return token +end + +function listWeakenedConstraints(outputs) + local variants = {} + local known = {} + local function register(cons) + cons.token = constraintToToken(cons) + if not known[cons.token] then + known[cons.token] = true + table.insert(variants, cons) + end + end + + local generic = {} + local anymat = {} + for i,cons in ipairs(outputs) do + local mask = cons.mat_mask + if (cons.mat_type or -1) >= 0 then + cons.mat_mask = nil + end + register(cons) + if mask then + table.insert(generic, { + item_type = cons.item_type, + item_subtype = cons.item_subtype, + is_craft = cons.is_craft, + mat_mask = mask + }) + end + table.insert(anymat, { + item_type = cons.item_type, + item_subtype = cons.item_subtype, + is_craft = cons.is_craft + }) + end + for i,cons in ipairs(generic) do register(cons) end + for i,cons in ipairs(anymat) do register(cons) end + + return variants +end + +return _ENV diff --git a/plugins/manipulator.cpp b/plugins/manipulator.cpp index 2b3fc86fc..b6d30ab1e 100644 --- a/plugins/manipulator.cpp +++ b/plugins/manipulator.cpp @@ -247,12 +247,14 @@ struct UnitInfo string transname; string profession; int8_t color; + int active_index; }; enum altsort_mode { ALTSORT_NAME, ALTSORT_PROFESSION, ALTSORT_HAPPINESS, + ALTSORT_ARRIVAL, ALTSORT_MAX }; @@ -284,12 +286,20 @@ bool sortByHappiness (const UnitInfo *d1, const UnitInfo *d2) return (d1->unit->status.happiness < d2->unit->status.happiness); } +bool sortByArrival (const UnitInfo *d1, const UnitInfo *d2) +{ + if (descending) + return (d1->active_index > d2->active_index); + else + return (d1->active_index < d2->active_index); +} + bool sortBySkill (const UnitInfo *d1, const UnitInfo *d2) { if (sort_skill != job_skill::NONE) { - df::unit_skill *s1 = binsearch_in_vector>(d1->unit->status.current_soul->skills, &df::unit_skill::id, sort_skill); - df::unit_skill *s2 = binsearch_in_vector>(d2->unit->status.current_soul->skills, &df::unit_skill::id, sort_skill); + df::unit_skill *s1 = binsearch_in_vector(d1->unit->status.current_soul->skills, &df::unit_skill::id, sort_skill); + df::unit_skill *s2 = binsearch_in_vector(d2->unit->status.current_soul->skills, &df::unit_skill::id, sort_skill); int l1 = s1 ? s1->rating : 0; int l2 = s2 ? s2->rating : 0; int e1 = s1 ? s1->experience : 0; @@ -331,6 +341,12 @@ class viewscreen_unitlaborsst : public dfhack_viewscreen { public: void feed(set *events); + void logic() { + dfhack_viewscreen::logic(); + if (do_refresh_names) + refreshNames(); + } + void render(); void resize(int w, int h) { calcSize(); } @@ -338,6 +354,8 @@ public: std::string getFocusString() { return "unitlabors"; } + df::unit *getSelectedUnit(); + viewscreen_unitlaborsst(vector &src, int cursor_pos); ~viewscreen_unitlaborsst() { }; @@ -345,23 +363,39 @@ protected: vector units; altsort_mode altsort; + bool do_refresh_names; int first_row, sel_row, num_rows; int first_column, sel_column; int col_widths[DISP_COLUMN_MAX]; int col_offsets[DISP_COLUMN_MAX]; + void refreshNames(); void calcSize (); }; viewscreen_unitlaborsst::viewscreen_unitlaborsst(vector &src, int cursor_pos) { + std::map active_idx; + auto &active = world->units.active; + for (size_t i = 0; i < active.size(); i++) + active_idx[active[i]] = i; + for (size_t i = 0; i < src.size(); i++) { - UnitInfo *cur = new UnitInfo; df::unit *unit = src[i]; + if (!unit) + { + if (cursor_pos > i) + cursor_pos--; + continue; + } + + UnitInfo *cur = new UnitInfo; + cur->unit = unit; cur->allowEdit = true; + cur->active_index = active_idx[unit]; if (unit->race != ui->race_id) cur->allowEdit = false; @@ -375,9 +409,6 @@ viewscreen_unitlaborsst::viewscreen_unitlaborsst(vector &src, int cur if (!ENUM_ATTR(profession, can_assign_labor, unit->profession)) cur->allowEdit = false; - cur->name = Translation::TranslateName(&unit->name, false); - cur->transname = Translation::TranslateName(&unit->name, true); - cur->profession = Units::getProfessionName(unit); cur->color = Units::getProfessionColor(unit); units.push_back(cur); @@ -385,6 +416,8 @@ viewscreen_unitlaborsst::viewscreen_unitlaborsst(vector &src, int cur altsort = ALTSORT_NAME; first_column = sel_column = 0; + refreshNames(); + first_row = 0; sel_row = cursor_pos; calcSize(); @@ -401,6 +434,22 @@ viewscreen_unitlaborsst::viewscreen_unitlaborsst(vector &src, int cur first_row = units.size() - num_rows; } +void viewscreen_unitlaborsst::refreshNames() +{ + do_refresh_names = false; + + for (size_t i = 0; i < units.size(); i++) + { + UnitInfo *cur = units[i]; + df::unit *unit = cur->unit; + + cur->name = Translation::TranslateName(Units::getVisibleName(unit), false); + cur->transname = Translation::TranslateName(Units::getVisibleName(unit), true); + cur->profession = Units::getProfessionName(unit); + } + calcSize(); +} + void viewscreen_unitlaborsst::calcSize() { num_rows = gps->dimy - 10; @@ -408,39 +457,92 @@ void viewscreen_unitlaborsst::calcSize() num_rows = units.size(); int num_columns = gps->dimx - DISP_COLUMN_MAX - 1; + + // min/max width of columns + int col_minwidth[DISP_COLUMN_MAX]; + int col_maxwidth[DISP_COLUMN_MAX]; + col_minwidth[DISP_COLUMN_HAPPINESS] = 4; + col_maxwidth[DISP_COLUMN_HAPPINESS] = 4; + col_minwidth[DISP_COLUMN_NAME] = 0; + col_maxwidth[DISP_COLUMN_NAME] = 0; + col_minwidth[DISP_COLUMN_PROFESSION] = 0; + col_maxwidth[DISP_COLUMN_PROFESSION] = 0; + col_minwidth[DISP_COLUMN_LABORS] = num_columns*3/5; // 60% + col_maxwidth[DISP_COLUMN_LABORS] = NUM_COLUMNS; + + // get max_name/max_prof from strings length + for (size_t i = 0; i < units.size(); i++) + { + if (col_maxwidth[DISP_COLUMN_NAME] < units[i]->name.size()) + col_maxwidth[DISP_COLUMN_NAME] = units[i]->name.size(); + if (col_maxwidth[DISP_COLUMN_PROFESSION] < units[i]->profession.size()) + col_maxwidth[DISP_COLUMN_PROFESSION] = units[i]->profession.size(); + } + + // check how much room we have + int width_min = 0, width_max = 0; for (int i = 0; i < DISP_COLUMN_MAX; i++) - col_widths[i] = 0; - while (num_columns > 0) { - num_columns--; - // need at least 4 digits for happiness - if (col_widths[DISP_COLUMN_HAPPINESS] < 4) + width_min += col_minwidth[i]; + width_max += col_maxwidth[i]; + } + + if (width_max <= num_columns) + { + // lots of space, distribute leftover (except last column) + int col_margin = (num_columns - width_max) / (DISP_COLUMN_MAX-1); + int col_margin_r = (num_columns - width_max) % (DISP_COLUMN_MAX-1); + for (int i = DISP_COLUMN_MAX-1; i>=0; i--) { - col_widths[DISP_COLUMN_HAPPINESS]++; - continue; + col_widths[i] = col_maxwidth[i]; + + if (i < DISP_COLUMN_MAX-1) + { + col_widths[i] += col_margin; + + if (col_margin_r) + { + col_margin_r--; + col_widths[i]++; + } + } } - // of remaining, 20% for Name, 20% for Profession, 60% for Labors - switch (num_columns % 5) + } + else if (width_min <= num_columns) + { + // constrained, give between min and max to every column + int space = num_columns - width_min; + // max size columns not yet seen may consume + int next_consume_max = width_max - width_min; + + for (int i = 0; i < DISP_COLUMN_MAX; i++) { - case 0: case 2: case 4: - col_widths[DISP_COLUMN_LABORS]++; - break; - case 1: - col_widths[DISP_COLUMN_NAME]++; - break; - case 3: - col_widths[DISP_COLUMN_PROFESSION]++; - break; + // divide evenly remaining space + int col_margin = space / (DISP_COLUMN_MAX-i); + + // take more if the columns after us cannot + next_consume_max -= (col_maxwidth[i]-col_minwidth[i]); + if (col_margin < space-next_consume_max) + col_margin = space - next_consume_max; + + // no more than maxwidth + if (col_margin > col_maxwidth[i] - col_minwidth[i]) + col_margin = col_maxwidth[i] - col_minwidth[i]; + + col_widths[i] = col_minwidth[i] + col_margin; + + space -= col_margin; } } - - while (col_widths[DISP_COLUMN_LABORS] > NUM_COLUMNS) + else { - col_widths[DISP_COLUMN_LABORS]--; - if (col_widths[DISP_COLUMN_LABORS] & 1) - col_widths[DISP_COLUMN_NAME]++; - else - col_widths[DISP_COLUMN_PROFESSION]++; + // should not happen, min screen is 80x25 + int space = num_columns; + for (int i = 0; i < DISP_COLUMN_MAX; i++) + { + col_widths[i] = space / (DISP_COLUMN_MAX-i); + space -= col_widths[i]; + } } for (int i = 0; i < DISP_COLUMN_MAX; i++) @@ -474,16 +576,28 @@ void viewscreen_unitlaborsst::calcSize() void viewscreen_unitlaborsst::feed(set *events) { - if (events->count(interface_key::LEAVESCREEN)) + bool leave_all = events->count(interface_key::LEAVESCREEN_ALL); + if (leave_all || events->count(interface_key::LEAVESCREEN)) { events->clear(); Screen::dismiss(this); + if (leave_all) + { + events->insert(interface_key::LEAVESCREEN); + parent->feed(events); + events->clear(); + } return; } if (!units.size()) return; + if (do_refresh_names) + refreshNames(); + + int old_sel_row = sel_row; + if (events->count(interface_key::CURSOR_UP) || events->count(interface_key::CURSOR_UPLEFT) || events->count(interface_key::CURSOR_UPRIGHT)) sel_row--; if (events->count(interface_key::CURSOR_UP_FAST) || events->count(interface_key::CURSOR_UPLEFT_FAST) || events->count(interface_key::CURSOR_UPRIGHT_FAST)) @@ -493,10 +607,33 @@ void viewscreen_unitlaborsst::feed(set *events) if (events->count(interface_key::CURSOR_DOWN_FAST) || events->count(interface_key::CURSOR_DOWNLEFT_FAST) || events->count(interface_key::CURSOR_DOWNRIGHT_FAST)) sel_row += 10; - if (sel_row < 0) + if ((sel_row > 0) && events->count(interface_key::CURSOR_UP_Z_AUX)) + { sel_row = 0; + } + if ((sel_row < units.size()-1) && events->count(interface_key::CURSOR_DOWN_Z_AUX)) + { + sel_row = units.size()-1; + } + + if (sel_row < 0) + { + if (old_sel_row == 0 && events->count(interface_key::CURSOR_UP)) + sel_row = units.size() - 1; + else + sel_row = 0; + } + if (sel_row > units.size() - 1) - sel_row = units.size() - 1; + { + if (old_sel_row == units.size()-1 && events->count(interface_key::CURSOR_DOWN)) + sel_row = 0; + else + sel_row = units.size() - 1; + } + + if (events->count(interface_key::STRING_A000)) + sel_row = 0; if (sel_row < first_row) first_row = sel_row; @@ -522,18 +659,34 @@ void viewscreen_unitlaborsst::feed(set *events) } if ((sel_column != NUM_COLUMNS - 1) && events->count(interface_key::CURSOR_DOWN_Z)) { - // go to end of current column group; if already at the end, go to the end of the next one - sel_column++; + // go to beginning of next group int cur = columns[sel_column].group; - while ((sel_column < NUM_COLUMNS - 1) && columns[sel_column + 1].group == cur) - sel_column++; + int next = sel_column+1; + while ((next < NUM_COLUMNS) && (columns[next].group == cur)) + next++; + if ((next < NUM_COLUMNS) && (columns[next].group != cur)) + sel_column = next; } + if (events->count(interface_key::STRING_A000)) + sel_column = 0; + if (sel_column < 0) sel_column = 0; if (sel_column > NUM_COLUMNS - 1) sel_column = NUM_COLUMNS - 1; + if (events->count(interface_key::CURSOR_DOWN_Z) || events->count(interface_key::CURSOR_UP_Z)) + { + // when moving by group, ensure the whole group is shown onscreen + int endgroup_column = sel_column; + while ((endgroup_column < NUM_COLUMNS-1) && columns[endgroup_column+1].group == columns[sel_column].group) + endgroup_column++; + + if (first_column < endgroup_column - col_widths[DISP_COLUMN_LABORS] + 1) + first_column = endgroup_column - col_widths[DISP_COLUMN_LABORS] + 1; + } + if (sel_column < first_column) first_column = sel_column; if (first_column < sel_column - col_widths[DISP_COLUMN_LABORS] + 1) @@ -726,6 +879,9 @@ void viewscreen_unitlaborsst::feed(set *events) case ALTSORT_HAPPINESS: std::sort(units.begin(), units.end(), sortByHappiness); break; + case ALTSORT_ARRIVAL: + std::sort(units.begin(), units.end(), sortByArrival); + break; } } if (events->count(interface_key::CHANGETAB)) @@ -739,6 +895,9 @@ void viewscreen_unitlaborsst::feed(set *events) altsort = ALTSORT_HAPPINESS; break; case ALTSORT_HAPPINESS: + altsort = ALTSORT_ARRIVAL; + break; + case ALTSORT_ARRIVAL: altsort = ALTSORT_NAME; break; } @@ -756,6 +915,8 @@ void viewscreen_unitlaborsst::feed(set *events) unitlist->feed(events); if (Screen::isDismissed(unitlist)) Screen::dismiss(this); + else + do_refresh_names = true; break; } } @@ -869,7 +1030,7 @@ void viewscreen_unitlaborsst::render() fg = 9; if (columns[col_offset].skill != job_skill::NONE) { - df::unit_skill *skill = binsearch_in_vector>(unit->status.current_soul->skills, &df::unit_skill::id, columns[col_offset].skill); + df::unit_skill *skill = binsearch_in_vector(unit->status.current_soul->skills, &df::unit_skill::id, columns[col_offset].skill); if ((skill != NULL) && (skill->rating || skill->experience)) { int level = skill->rating; @@ -925,7 +1086,7 @@ void viewscreen_unitlaborsst::render() } else { - df::unit_skill *skill = binsearch_in_vector>(unit->status.current_soul->skills, &df::unit_skill::id, columns[sel_column].skill); + df::unit_skill *skill = binsearch_in_vector(unit->status.current_soul->skills, &df::unit_skill::id, columns[sel_column].skill); if (skill) { int level = skill->rating; @@ -944,30 +1105,30 @@ void viewscreen_unitlaborsst::render() } int x = 2; - OutputString(10, x, gps->dimy - 3, "Enter"); // SELECT key + OutputString(10, x, gps->dimy - 3, Screen::getKeyDisplay(interface_key::SELECT)); OutputString(canToggle ? 15 : 8, x, gps->dimy - 3, ": Toggle labor, "); - OutputString(10, x, gps->dimy - 3, "Shift+Enter"); // SELECT_ALL key + OutputString(10, x, gps->dimy - 3, Screen::getKeyDisplay(interface_key::SELECT_ALL)); OutputString(canToggle ? 15 : 8, x, gps->dimy - 3, ": Toggle Group, "); - OutputString(10, x, gps->dimy - 3, "v"); // UNITJOB_VIEW key + OutputString(10, x, gps->dimy - 3, Screen::getKeyDisplay(interface_key::UNITJOB_VIEW)); OutputString(15, x, gps->dimy - 3, ": ViewCre, "); - OutputString(10, x, gps->dimy - 3, "c"); // UNITJOB_ZOOM_CRE key + OutputString(10, x, gps->dimy - 3, Screen::getKeyDisplay(interface_key::UNITJOB_ZOOM_CRE)); OutputString(15, x, gps->dimy - 3, ": Zoom-Cre"); x = 2; - OutputString(10, x, gps->dimy - 2, "Esc"); // LEAVESCREEN key + OutputString(10, x, gps->dimy - 2, Screen::getKeyDisplay(interface_key::LEAVESCREEN)); OutputString(15, x, gps->dimy - 2, ": Done, "); - OutputString(10, x, gps->dimy - 2, "+"); // SECONDSCROLL_DOWN key - OutputString(10, x, gps->dimy - 2, "-"); // SECONDSCROLL_UP key + OutputString(10, x, gps->dimy - 2, Screen::getKeyDisplay(interface_key::SECONDSCROLL_DOWN)); + OutputString(10, x, gps->dimy - 2, Screen::getKeyDisplay(interface_key::SECONDSCROLL_UP)); OutputString(15, x, gps->dimy - 2, ": Sort by Skill, "); - OutputString(10, x, gps->dimy - 2, "*"); // SECONDSCROLL_PAGEDOWN key - OutputString(10, x, gps->dimy - 2, "/"); // SECONDSCROLL_PAGEUP key + OutputString(10, x, gps->dimy - 2, Screen::getKeyDisplay(interface_key::SECONDSCROLL_PAGEDOWN)); + OutputString(10, x, gps->dimy - 2, Screen::getKeyDisplay(interface_key::SECONDSCROLL_PAGEUP)); OutputString(15, x, gps->dimy - 2, ": Sort by ("); - OutputString(10, x, gps->dimy - 2, "Tab"); // CHANGETAB key + OutputString(10, x, gps->dimy - 2, Screen::getKeyDisplay(interface_key::CHANGETAB)); OutputString(15, x, gps->dimy - 2, ") "); switch (altsort) { @@ -980,12 +1141,23 @@ void viewscreen_unitlaborsst::render() case ALTSORT_HAPPINESS: OutputString(15, x, gps->dimy - 2, "Happiness"); break; + case ALTSORT_ARRIVAL: + OutputString(15, x, gps->dimy - 2, "Arrival"); + break; default: OutputString(15, x, gps->dimy - 2, "Unknown"); break; } } +df::unit *viewscreen_unitlaborsst::getSelectedUnit() +{ + // This query might be from the rename plugin + do_refresh_names = true; + + return units[sel_row]->unit; +} + struct unitlist_hook : df::viewscreen_unitlistst { typedef df::viewscreen_unitlistst interpose_base; @@ -1010,7 +1182,7 @@ struct unitlist_hook : df::viewscreen_unitlistst if (units[page].size()) { int x = 2; - OutputString(12, x, gps->dimy - 2, "l"); // UNITVIEW_PRF_PROF key + OutputString(12, x, gps->dimy - 2, Screen::getKeyDisplay(interface_key::UNITVIEW_PRF_PROF)); OutputString(15, x, gps->dimy - 2, ": Manage labors (DFHack)"); } } diff --git a/plugins/misery.cpp b/plugins/misery.cpp new file mode 100644 index 000000000..a0c0a2390 --- /dev/null +++ b/plugins/misery.cpp @@ -0,0 +1,183 @@ +#include "PluginManager.h" +#include "Export.h" + +#include "DataDefs.h" +#include "df/world.h" +#include "df/ui.h" +#include "df/unit.h" +#include "df/unit_thought.h" +#include "df/unit_thought_type.h" + +#include +#include +#include +using namespace std; + +using namespace DFHack; + +static int factor = 1; +static map processedThoughtCountTable; + +//keep track of fake thoughts so you can remove them if requested +static vector > fakeThoughts; +static int count; +const int maxCount = 1000; + +DFHACK_PLUGIN("misery"); + +command_result misery(color_ostream& out, vector& parameters); + +DFhackCExport command_result plugin_shutdown(color_ostream& out) { + factor = 1; + return CR_OK; +} + +DFhackCExport command_result plugin_onupdate(color_ostream& out) { + static bool wasLoaded = false; + if ( factor == 1 || !df::global::world || !df::global::world->map.block_index ) { + if ( wasLoaded ) { + //we just unloaded the game: clear all data + factor = 1; + processedThoughtCountTable.clear(); + fakeThoughts.clear(); + wasLoaded = false; + } + return CR_OK; + } + + if ( !wasLoaded ) { + wasLoaded = true; + } + + if ( count < maxCount ) { + count++; + return CR_OK; + } + count = 0; + + int32_t race_id = df::global::ui->race_id; + int32_t civ_id = df::global::ui->civ_id; + for ( size_t a = 0; a < df::global::world->units.all.size(); a++ ) { + df::unit* unit = df::global::world->units.all[a]; //TODO: consider units.active + //living, native units only + if ( unit->race != race_id || unit->civ_id != civ_id ) + continue; + if ( unit->flags1.bits.dead ) + continue; + + int processedThoughtCount; + map::iterator i = processedThoughtCountTable.find(unit->id); + if ( i == processedThoughtCountTable.end() ) { + processedThoughtCount = unit->status.recent_events.size(); + processedThoughtCountTable[unit->id] = processedThoughtCount; + } else { + processedThoughtCount = (*i).second; + } + + if ( processedThoughtCount == unit->status.recent_events.size() ) { + continue; + } else if ( processedThoughtCount > unit->status.recent_events.size() ) { + processedThoughtCount = unit->status.recent_events.size(); + } + + //don't reprocess any old thoughts + vector newThoughts; + for ( size_t b = processedThoughtCount; b < unit->status.recent_events.size(); b++ ) { + df::unit_thought* oldThought = unit->status.recent_events[b]; + const char* bob = ENUM_ATTR(unit_thought_type, value, oldThought->type); + if ( bob[0] != '-' ) { + //out.print("unit %4d: old thought value = %s\n", unit->id, bob); + continue; + } + /*out.print("unit %4d: Duplicating thought type %d (%s), value %s, age %d, subtype %d, severity %d\n", + unit->id, + oldThought->type.value, + ENUM_ATTR(unit_thought_type, caption, (oldThought->type)), + //df::enum_traits::attr_table[oldThought->type].caption + bob, + oldThought->age, + oldThought->subtype, + oldThought->severity + );*/ + //add duplicate thoughts to the temp list + for ( size_t c = 0; c < factor; c++ ) { + df::unit_thought* thought = new df::unit_thought; + thought->type = unit->status.recent_events[b]->type; + thought->age = unit->status.recent_events[b]->age; + thought->subtype = unit->status.recent_events[b]->subtype; + thought->severity = unit->status.recent_events[b]->severity; + newThoughts.push_back(thought); + } + } + for ( size_t b = 0; b < newThoughts.size(); b++ ) { + fakeThoughts.push_back(std::pair(a, unit->status.recent_events.size())); + unit->status.recent_events.push_back(newThoughts[b]); + } + processedThoughtCountTable[unit->id] = unit->status.recent_events.size(); + } + + return CR_OK; +} + +DFhackCExport command_result plugin_init(color_ostream& out, vector &commands) { + commands.push_back(PluginCommand("misery", "increase the intensity of negative dwarven thoughts", + &misery, false, + "misery: When enabled, every new negative dwarven thought will be multiplied by a factor (2 by default).\n" + "Usage:\n" + " misery enable n\n" + " enable misery with optional magnitude n. If specified, n must be positive.\n" + " misery n\n" + " same as \"misery enable n\"\n" + " misery enable\n" + " same as \"misery enable 2\"\n" + " misery disable\n" + " stop adding new negative thoughts. This will not remove existing duplicated thoughts. Equivalent to \"misery 1\"\n" + " misery clear\n" + " remove fake thoughts added in this session of DF. Saving makes them permanent! Does not change factor.\n\n" + )); + return CR_OK; +} + +command_result misery(color_ostream &out, vector& parameters) { + if ( !df::global::world || !df::global::world->map.block_index ) { + out.printerr("misery can only be enabled in fortress mode with a fully-loaded game.\n"); + return CR_FAILURE; + } + + if ( parameters.size() < 1 || parameters.size() > 2 ) { + return CR_WRONG_USAGE; + } + + if ( parameters[0] == "disable" ) { + if ( parameters.size() > 1 ) { + return CR_WRONG_USAGE; + } + factor = 1; + return CR_OK; + } else if ( parameters[0] == "enable" ) { + factor = 2; + if ( parameters.size() == 2 ) { + factor = atoi(parameters[1].c_str()); + if ( factor < 1 ) { + out.printerr("Second argument must be a positive integer.\n"); + return CR_WRONG_USAGE; + } + } + } else if ( parameters[0] == "clear" ) { + for ( size_t a = 0; a < fakeThoughts.size(); a++ ) { + int dorfIndex = fakeThoughts[a].first; + int thoughtIndex = fakeThoughts[a].second; + df::global::world->units.all[dorfIndex]->status.recent_events[thoughtIndex]->age = 1000000; + } + fakeThoughts.clear(); + } else { + int a = atoi(parameters[0].c_str()); + if ( a < 1 ) { + return CR_WRONG_USAGE; + } + factor = a; + } + + return CR_OK; +} + diff --git a/plugins/mode.cpp b/plugins/mode.cpp index f9e6cd10c..d18cf7529 100644 --- a/plugins/mode.cpp +++ b/plugins/mode.cpp @@ -117,13 +117,9 @@ command_result mode (color_ostream &out_, vector & parameters) return CR_WRONG_USAGE; } - World *world; - { CoreSuspender suspend; - world = Core::getInstance().getWorld(); - world->Start(); - world->ReadGameMode(gm); + World::ReadGameMode(gm); } printCurrentModes(gm, out); @@ -202,7 +198,7 @@ command_result mode (color_ostream &out_, vector & parameters) { CoreSuspender suspend; - world->WriteGameMode(gm); + World::WriteGameMode(gm); } out << endl; diff --git a/plugins/power-meter.cpp b/plugins/power-meter.cpp index 17261adb2..8db6c6f30 100644 --- a/plugins/power-meter.cpp +++ b/plugins/power-meter.cpp @@ -177,8 +177,7 @@ static bool makePowerMeter(df::pressure_plate_info *info, int min_power, int max if (!enabled) { - auto pworld = Core::getInstance().getWorld(); - auto entry = pworld->GetPersistentData("power-meter/enabled", NULL); + auto entry = World::GetPersistentData("power-meter/enabled", NULL); if (!entry.isValid()) return false; @@ -202,8 +201,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan switch (event) { case SC_WORLD_LOADED: { - auto pworld = Core::getInstance().getWorld(); - bool enable = pworld->GetPersistentData("power-meter/enabled").isValid(); + bool enable = World::GetPersistentData("power-meter/enabled").isValid(); if (enable) { diff --git a/plugins/raw/building_spatter.txt b/plugins/raw/building_spatter.txt new file mode 100644 index 000000000..7bde8297b --- /dev/null +++ b/plugins/raw/building_spatter.txt @@ -0,0 +1,22 @@ +building_spatter + +[OBJECT:BUILDING] + +[BUILDING_WORKSHOP:GREASING_STATION] + [NAME:Greasing Station] + [NAME_COLOR:2:0:1] + [DIM:1:1] + [WORK_LOCATION:1:1] + [BUILD_LABOR:DYER] + [BUILD_KEY:CUSTOM_ALT_G] + [BLOCK:1:0] + [TILE:0:1:150] + [COLOR:0:1:0:0:1] + [TILE:1:1:150] + [COLOR:1:1:MAT] + [TILE:2:1:8] + [COLOR:2:1:MAT] + [TILE:3:1:8] + [COLOR:3:1:7:5:0] + [BUILD_ITEM:1:BUCKET:NONE:NONE:NONE][CAN_USE_ARTIFACT] + [BUILD_ITEM:1:NONE:NONE:NONE:NONE][BUILDMAT] diff --git a/plugins/raw/reaction_spatter.txt b/plugins/raw/reaction_spatter.txt index 81f9a2b62..95b9f1d33 100644 --- a/plugins/raw/reaction_spatter.txt +++ b/plugins/raw/reaction_spatter.txt @@ -7,7 +7,7 @@ Reaction name must start with 'SPATTER_ADD_': [REACTION:SPATTER_ADD_OBJECT_LIQUID] [NAME:coat object with liquid] [ADVENTURE_MODE_ENABLED] - [SKILL:WAX_WORKING] + [SKILL:DYER] [REAGENT:extract:150:LIQUID_MISC:NONE:NONE:NONE] [MIN_DIMENSION:150] [DOES_NOT_DETERMINE_PRODUCT_AMOUNT] @@ -28,10 +28,10 @@ Reaction name must start with 'SPATTER_ADD_': [REACTION:SPATTER_ADD_WEAPON_EXTRACT] [NAME:coat weapon with extract] - [BUILDING:CRAFTSMAN:NONE] - [SKILL:WAX_WORKING] - [REAGENT:extract:150:LIQUID_MISC:NONE:NONE:NONE] - [MIN_DIMENSION:150] + [BUILDING:GREASING_STATION:CUSTOM_W] + [SKILL:DYER] + [REAGENT:extract:100:LIQUID_MISC:NONE:NONE:NONE] + [MIN_DIMENSION:100] [REACTION_CLASS:CREATURE_EXTRACT] [DOES_NOT_DETERMINE_PRODUCT_AMOUNT] [REAGENT:extract container:1:NONE:NONE:NONE:NONE] @@ -51,8 +51,8 @@ Reaction name must start with 'SPATTER_ADD_': [REACTION:SPATTER_ADD_AMMO_EXTRACT] [NAME:coat ammo with extract] - [BUILDING:CRAFTSMAN:NONE] - [SKILL:WAX_WORKING] + [BUILDING:GREASING_STATION:CUSTOM_A] + [SKILL:DYER] [REAGENT:extract:50:LIQUID_MISC:NONE:NONE:NONE] [MIN_DIMENSION:50] [REACTION_CLASS:CREATURE_EXTRACT] @@ -75,8 +75,8 @@ Reaction name must start with 'SPATTER_ADD_': [REACTION:SPATTER_ADD_WEAPON_GCS] [NAME:coat weapon with GCS venom] - [BUILDING:CRAFTSMAN:NONE] - [SKILL:WAX_WORKING] + [BUILDING:GREASING_STATION:NONE] + [SKILL:DYER] [REAGENT:extract:150:LIQUID_MISC:NONE:CAVE_SPIDER_GIANT:POISON] [MIN_DIMENSION:150] [REACTION_CLASS:CREATURE_EXTRACT] @@ -95,8 +95,8 @@ Reaction name must start with 'SPATTER_ADD_': [REACTION:SPATTER_ADD_AMMO_GCS] [NAME:coat ammo with GCS venom] - [BUILDING:CRAFTSMAN:NONE] - [SKILL:WAX_WORKING] + [BUILDING:GREASING_STATION:NONE] + [SKILL:DYER] [REAGENT:extract:50:LIQUID_MISC:NONE:CAVE_SPIDER_GIANT:POISON] [MIN_DIMENSION:50] [REACTION_CLASS:CREATURE_EXTRACT] @@ -115,8 +115,8 @@ Reaction name must start with 'SPATTER_ADD_': [REACTION:SPATTER_ADD_WEAPON_GDS] [NAME:coat weapon with GDS venom] - [BUILDING:CRAFTSMAN:NONE] - [SKILL:WAX_WORKING] + [BUILDING:GREASING_STATION:NONE] + [SKILL:DYER] [REAGENT:extract:150:LIQUID_MISC:NONE:SCORPION_DESERT_GIANT:POISON] [MIN_DIMENSION:150] [REACTION_CLASS:CREATURE_EXTRACT] @@ -135,8 +135,8 @@ Reaction name must start with 'SPATTER_ADD_': [REACTION:SPATTER_ADD_AMMO_GDS] [NAME:coat ammo with GDS venom] - [BUILDING:CRAFTSMAN:NONE] - [SKILL:WAX_WORKING] + [BUILDING:GREASING_STATION:NONE] + [SKILL:DYER] [REAGENT:extract:50:LIQUID_MISC:NONE:SCORPION_DESERT_GIANT:POISON] [MIN_DIMENSION:50] [REACTION_CLASS:CREATURE_EXTRACT] diff --git a/plugins/rename.cpp b/plugins/rename.cpp index 99dc6848a..bffb33111 100644 --- a/plugins/rename.cpp +++ b/plugins/rename.cpp @@ -10,9 +10,11 @@ #include "modules/Translation.h" #include "modules/Units.h" #include "modules/World.h" +#include "modules/Screen.h" #include #include "df/ui.h" +#include "df/ui_sidebar_menus.h" #include "df/world.h" #include "df/squad.h" #include "df/unit.h" @@ -27,6 +29,8 @@ #include "df/building_furnacest.h" #include "df/building_trapst.h" #include "df/building_siegeenginest.h" +#include "df/building_civzonest.h" +#include "df/viewscreen_dwarfmodest.h" #include "RemoteServer.h" #include "rename.pb.h" @@ -43,6 +47,7 @@ using namespace df::enums; using namespace dfproto; using df::global::ui; +using df::global::ui_sidebar_menus; using df::global::world; DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event); @@ -66,8 +71,8 @@ DFhackCExport command_result plugin_init (color_ostream &out, std::vector main.mode == ui_sidebar_mode::Zones && + ui_sidebar_menus && ui_sidebar_menus->zone.selected && + !ui_sidebar_menus->zone.selected->name.empty()) + { + auto dims = Gui::getDwarfmodeViewDims(); + int width = dims.menu_x2 - dims.menu_x1 - 1; + + Screen::Pen pen(' ',COLOR_WHITE); + Screen::fillRect(pen, dims.menu_x1, dims.y1+1, dims.menu_x2, dims.y1+1); + + std::string name; + ui_sidebar_menus->zone.selected->getName(&name); + Screen::paintString(pen, dims.menu_x1+1, dims.y1+1, name.substr(0, width)); + } + } +}; + +IMPLEMENT_VMETHOD_INTERPOSE(dwarf_render_zone_hook, render); + static char getBuildingCode(df::building *bld) { CHECK_NULL_POINTER(bld); @@ -142,6 +174,9 @@ KNOWN_BUILDINGS static bool enable_building_rename(char code, bool enable) { + if (code == 'Z') + INTERPOSE_HOOK(dwarf_render_zone_hook, render).apply(enable); + switch (code) { #define BUILDING(code, cname, tag) \ case code: return INTERPOSE_HOOK(cname##_hook, getName).apply(enable); @@ -154,6 +189,8 @@ KNOWN_BUILDINGS static void disable_building_rename() { + INTERPOSE_HOOK(dwarf_render_zone_hook, render).remove(); + #define BUILDING(code, cname, tag) \ INTERPOSE_HOOK(cname##_hook, getName).remove(); KNOWN_BUILDINGS @@ -178,8 +215,7 @@ static void init_buildings(bool enable) if (enable) { - auto pworld = Core::getInstance().getWorld(); - auto entry = pworld->GetPersistentData("rename/building_types"); + auto entry = World::GetPersistentData("rename/building_types"); if (entry.isValid()) { @@ -208,8 +244,7 @@ static bool renameBuilding(df::building *bld, std::string name) if (!name.empty() && !is_enabled_building(code)) { - auto pworld = Core::getInstance().getWorld(); - auto entry = pworld->GetPersistentData("rename/building_types", NULL); + auto entry = World::GetPersistentData("rename/building_types", NULL); if (!entry.isValid()) return false; @@ -357,10 +392,11 @@ static command_result rename(color_ostream &out, vector ¶meters) if (parameters.size() != 2) return CR_WRONG_USAGE; - if (ui->main.mode != ui_sidebar_mode::QueryBuilding) + df::building *bld = Gui::getSelectedBuilding(out, true); + if (!bld) return CR_WRONG_USAGE; - if (!renameBuilding(world->selected_building, parameters[1])) + if (!renameBuilding(bld, parameters[1])) { out.printerr("This type of building is not supported.\n"); return CR_FAILURE; diff --git a/plugins/reveal.cpp b/plugins/reveal.cpp index f35424309..8db7c9112 100644 --- a/plugins/reveal.cpp +++ b/plugins/reveal.cpp @@ -87,19 +87,18 @@ DFhackCExport command_result plugin_init ( color_ostream &out, vector ReadGameMode(gm); + World::ReadGameMode(gm); if(gm.g_mode == game_mode::DWARF) { // if the map is revealed and we're in fortress mode, force the game to pause. if(revealed == REVEALED) { - World->SetPauseState(true); + World::SetPauseState(true); } else if(nopause_state) { - World->SetPauseState(false); + World::SetPauseState(false); } } return CR_OK; @@ -185,14 +184,13 @@ command_result reveal(color_ostream &out, vector & params) CoreSuspender suspend; - World *World = Core::getInstance().getWorld(); if (!Maps::IsValid()) { out.printerr("Map is not available!\n"); return CR_FAILURE; } t_gamemodes gm; - World->ReadGameMode(gm); + World::ReadGameMode(gm); if(gm.g_mode == game_mode::ADVENTURE) { revealAdventure(out); @@ -234,7 +232,7 @@ command_result reveal(color_ostream &out, vector & params) if(pause) { revealed = REVEALED; - World->SetPauseState(true); + World::SetPauseState(true); } else revealed = DEMON_REVEALED; @@ -264,14 +262,13 @@ command_result unreveal(color_ostream &out, vector & params) } CoreSuspender suspend; - World *World = Core::getInstance().getWorld(); if (!Maps::IsValid()) { out.printerr("Map is not available!\n"); return CR_FAILURE; } t_gamemodes gm; - World->ReadGameMode(gm); + World::ReadGameMode(gm); if(gm.g_mode != game_mode::DWARF) { con.printerr("Only in fortress mode.\n"); @@ -337,7 +334,6 @@ command_result revflood(color_ostream &out, vector & params) } CoreSuspender suspend; uint32_t x_max,y_max,z_max; - World * World = Core::getInstance().getWorld(); if (!Maps::IsValid()) { out.printerr("Map is not available!\n"); @@ -349,7 +345,7 @@ command_result revflood(color_ostream &out, vector & params) return CR_FAILURE; } t_gamemodes gm; - World->ReadGameMode(gm); + World::ReadGameMode(gm); if(gm.g_type != game_type::DWARF_MAIN && gm.g_mode != game_mode::DWARF ) { out.printerr("Only in proper dwarf mode.\n"); diff --git a/plugins/ruby/CMakeLists.txt b/plugins/ruby/CMakeLists.txt index e10ee38bf..9d821f941 100644 --- a/plugins/ruby/CMakeLists.txt +++ b/plugins/ruby/CMakeLists.txt @@ -20,7 +20,9 @@ ENDIF(DL_RUBY AND NOT APPLE) ADD_CUSTOM_COMMAND( OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/ruby-autogen.rb COMMAND ${PERL_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/codegen.pl ${dfhack_SOURCE_DIR}/library/include/df/codegen.out.xml ${CMAKE_CURRENT_BINARY_DIR}/ruby-autogen.rb - DEPENDS ${dfhack_SOURCE_DIR}/library/include/df/codegen.out.xml ${CMAKE_CURRENT_SOURCE_DIR}/codegen.pl + # cmake quirk: depending on codegen.out.xml or generate_headers only is not enough, needs both + # test by manually patching any library/xml/moo.xml, run make ruby-autogen-rb -j2, and check build/plugins/ruby/ruby-autogen.rb for patched xml data + DEPENDS generate_headers ${dfhack_SOURCE_DIR}/library/include/df/codegen.out.xml ${CMAKE_CURRENT_SOURCE_DIR}/codegen.pl COMMENT ruby-autogen.rb ) ADD_CUSTOM_TARGET(ruby-autogen-rb DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/ruby-autogen.rb) @@ -32,8 +34,8 @@ ADD_DEPENDENCIES(ruby ruby-autogen-rb) INSTALL(FILES ${RUBYLIB} DESTINATION ${DFHACK_LIBRARY_DESTINATION}) -INSTALL(FILES ${CMAKE_CURRENT_BINARY_DIR}/ruby-autogen.rb DESTINATION hack/ruby) - INSTALL(DIRECTORY . DESTINATION hack/ruby FILES_MATCHING PATTERN "*.rb") + +INSTALL(FILES ${CMAKE_CURRENT_BINARY_DIR}/ruby-autogen.rb DESTINATION hack/ruby) diff --git a/plugins/ruby/README b/plugins/ruby/README index c9a84fb37..9246fec88 100644 --- a/plugins/ruby/README +++ b/plugins/ruby/README @@ -177,7 +177,7 @@ Symbol. This works for array indexes too. ex: df.unit_find(:selected).status.labors[:HAUL_FOOD] = true df.map_tile_at(df.cursor).designation.liquid_type = :Water -Virtual method calls are supported for C++ objects, with a maximum of 4 +Virtual method calls are supported for C++ objects, with a maximum of 6 arguments. Arguments / return value are interpreted as Compound/Enums as specified in the vmethod definition in the xmls. diff --git a/plugins/ruby/building.rb b/plugins/ruby/building.rb index af152e198..e863ec5d5 100644 --- a/plugins/ruby/building.rb +++ b/plugins/ruby/building.rb @@ -8,6 +8,8 @@ module DFHack k.building if k.type == :Building when :BuildingItems, :QueryBuilding world.selected_building + when :Zones, :ZonesPenInfo, :ZonesPitInfo, :ZonesHospitalInfo + ui_sidebar_menus.zone.selected end elsif what.kind_of?(Integer) @@ -21,8 +23,8 @@ module DFHack if b.room.extents dx = x - b.room.x dy = y - b.room.y - dx >= 0 and dx <= b.room.width and - dy >= 0 and dy <= b.room.height and + dx >= 0 and dx < b.room.width and + dy >= 0 and dy < b.room.height and b.room.extents[ dy*b.room.width + dx ] > 0 else b.x1 <= x and b.x2 >= x and @@ -46,12 +48,17 @@ module DFHack raise "invalid building type #{type.inspect}" if not cls bld = cls.cpp_new bld.race = ui.race_id - bld.setSubtype(subtype) if subtype != -1 - bld.setCustomType(custom) if custom != -1 + subtype = WorkshopType.int(subtype) if subtype.kind_of?(::Symbol) and type == :Workshop + subtype = FurnaceType.int(subtype) if subtype.kind_of?(::Symbol) and type == :Furnace + subtype = CivzoneType.int(subtype) if subtype.kind_of?(::Symbol) and type == :Civzone + subtype = TrapType.int(subtype) if subtype.kind_of?(::Symbol) and type == :Trap + bld.setSubtype(subtype) + bld.setCustomType(custom) case type when :Furnace; bld.melt_remainder[world.raws.inorganics.length] = 0 when :Coffin; bld.initBurialFlags when :Trap; bld.unk_cc = 500 if bld.trap_type == :PressurePlate + when :Floodgate; bld.gate_flags.closed = true end bld end @@ -176,9 +183,20 @@ module DFHack # set building at position, with optional width/height def building_position(bld, pos, w=nil, h=nil) - bld.x1 = pos.x - bld.y1 = pos.y - bld.z = pos.z + if pos.respond_to?(:x1) + x, y, z = pos.x1, pos.y1, pos.z + w ||= pos.x2-pos.x1+1 if pos.respond_to?(:x2) + h ||= pos.y2-pos.y1+1 if pos.respond_to?(:y2) + elsif pos.respond_to?(:x) + x, y, z = pos.x, pos.y, pos.z + else + x, y, z = pos + end + w ||= pos.w if pos.respond_to?(:w) + h ||= pos.h if pos.respond_to?(:h) + bld.x1 = x + bld.y1 = y + bld.z = z bld.x2 = bld.x1+w-1 if w bld.y2 = bld.y1+h-1 if h building_setsize(bld) @@ -193,7 +211,7 @@ module DFHack z = bld.z (bld.x1..bld.x2).each { |x| (bld.y1..bld.y2).each { |y| - next if !extents or bld.room.extents[bld.room.width*(y-bld.room.y)+(x-bld.room.x)] == 0 + next if extents and bld.room.extents[bld.room.width*(y-bld.room.y)+(x-bld.room.x)] == 0 next if not mb = map_block_at(x, y, z) des = mb.designation[x%16][y%16] des.pile = stockpile @@ -207,17 +225,24 @@ module DFHack } end - # link bld into other rooms if it is inside their extents + # link bld into other rooms if it is inside their extents or vice versa def building_linkrooms(bld) - didstuff = false - world.buildings.other[:ANY_FREE].each { |ob| - next if !ob.is_room or ob.z != bld.z - next if !ob.room.extents or !ob.isExtentShaped or ob.room.extents[ob.room.width*(bld.y1-ob.room.y)+(bld.x1-ob.room.x)] == 0 - didstuff = true - ob.children << bld - bld.parents << ob + world.buildings.other[:IN_PLAY].each { |ob| + next if ob.z != bld.z + if bld.is_room and bld.room.extents + next if ob.is_room or ob.x1 < bld.room.x or ob.x1 >= bld.room.x+bld.room.width or ob.y1 < bld.room.y or ob.y1 >= bld.room.y+bld.room.height + next if bld.room.extents[bld.room.width*(ob.y1-bld.room.y)+(ob.x1-bld.room.x)] == 0 + ui.equipment.update.buildings = true + bld.children << ob + ob.parents << bld + elsif ob.is_room and ob.room.extents + next if bld.is_room or bld.x1 < ob.room.x or bld.x1 >= ob.room.x+ob.room.width or bld.y1 < ob.room.y or bld.y1 >= ob.room.y+ob.room.height + next if ob.room.extents[ob.room.width*(bld.y1-ob.room.y)+(bld.x1-ob.room.x)].to_i == 0 + ui.equipment.update.buildings = true + ob.children << bld + bld.parents << ob + end } - ui.equipment.update.buildings = true if didstuff end # link the building into the world, set map data, link rooms, bld.id @@ -274,6 +299,47 @@ module DFHack building_createdesign(bld, rough) end + # construct an abstract building (stockpile, farmplot, ...) + def building_construct_abstract(bld) + case bld.getType + when :Stockpile + max = df.world.buildings.other[:STOCKPILE].map { |s| s.stockpile_number }.max + bld.stockpile_number = max.to_i + 1 + when :Civzone + max = df.world.buildings.other[:ANY_ZONE].map { |z| z.zone_num }.max + bld.zone_num = max.to_i + 1 + end + building_link bld + if !bld.flags.exists + bld.flags.exists = true + bld.initFarmSeasons + end + end + + def building_setowner(bld, unit) + return unless bld.is_room + return if bld.owner == unit + + if bld.owner + if idx = bld.owner.owned_buildings.index { |ob| ob.id == bld.id } + bld.owner.owned_buildings.delete_at(idx) + end + if spouse = bld.owner.relations.spouse_tg and + idx = spouse.owned_buildings.index { |ob| ob.id == bld.id } + spouse.owned_buildings.delete_at(idx) + end + end + bld.owner = unit + if unit + unit.owned_buildings << bld + if spouse = bld.owner.relations.spouse_tg and + !spouse.owned_buildings.index { |ob| ob.id == bld.id } and + bld.canUseSpouseRoom + spouse.owned_buildings << bld + end + end + end + # creates a job to deconstruct the building def building_deconstruct(bld) job = Job.cpp_new @@ -286,23 +352,13 @@ module DFHack job end - # check item flags to see if it is suitable for use as a building material - def building_isitemfree(i) - !i.flags.in_job and - !i.flags.in_inventory and - !i.flags.removed and - !i.flags.in_building and - !i.flags.owned and - !i.flags.forbid - end - # exemple usage def buildbed(pos=cursor) raise 'where to ?' if pos.x < 0 item = world.items.all.find { |i| i.kind_of?(ItemBedst) and - building_isitemfree(i) + item_isfree(i) } raise 'no free bed, build more !' if not item diff --git a/plugins/ruby/codegen.pl b/plugins/ruby/codegen.pl index 593216d71..ff69853af 100755 --- a/plugins/ruby/codegen.pl +++ b/plugins/ruby/codegen.pl @@ -232,7 +232,11 @@ sub render_global_class { render_struct_fields($type); my $vms = $type->findnodes('child::virtual-methods')->[0]; - render_class_vmethods($vms) if $vms; + if ($vms) + { + my $voff = render_class_vmethods_voff($parent); + render_class_vmethods($vms, $voff); + } }; push @lines_rb, "end\n"; } @@ -275,7 +279,6 @@ sub render_struct_field_refs { my ($parent, $field, $name) = @_; my $reftg = $field->getAttribute('ref-target'); - render_field_reftarget($parent, $field, $name, $reftg) if ($reftg); my $refto = $field->getAttribute('refers-to'); render_field_refto($parent, $name, $refto) if ($refto); @@ -285,6 +288,8 @@ sub render_struct_field_refs { if ($meta and $meta eq 'container' and $item) { my $itemreftg = $item->getAttribute('ref-target'); render_container_reftarget($parent, $item, $name, $itemreftg) if $itemreftg; + } elsif ($reftg) { + render_field_reftarget($parent, $field, $name, $reftg); } } @@ -368,9 +373,29 @@ sub render_container_reftarget { } } +# return the size of the parent's vtables +sub render_class_vmethods_voff { + my ($name) = @_; + + return 0 if !$name; + + my $type = $global_types{$name}; + my $parent = $type->getAttribute('inherits-from'); + + my $voff = render_class_vmethods_voff($parent); + my $vms = $type->findnodes('child::virtual-methods')->[0]; + + for my $meth ($vms->findnodes('child::vmethod')) + { + $voff += 4 if $meth->getAttribute('is-destructor') and $os eq 'linux'; + $voff += 4; + } + + return $voff; +} + sub render_class_vmethods { - my ($vms) = @_; - my $voff = 0; + my ($vms, $voff) = @_; for my $meth ($vms->findnodes('child::vmethod')) { @@ -788,6 +813,7 @@ sub render_item_number { my $subtype = $item->getAttribute('ld:subtype'); my $meta = $item->getAttribute('ld:meta'); my $initvalue = $item->getAttribute('init-value'); + $initvalue ||= -1 if $item->getAttribute('refers-to') or $item->getAttribute('ref-target'); my $typename = $item->getAttribute('type-name'); undef $typename if ($meta and $meta eq 'bitfield-type'); my $g = $global_types{$typename} if ($typename); @@ -964,7 +990,7 @@ sub render_item_bytes { my $subtype = $item->getAttribute('ld:subtype'); if ($subtype eq 'padding') { } elsif ($subtype eq 'static-string') { - my $size = $item->getAttribute('size'); + my $size = $item->getAttribute('size') || -1; push @lines_rb, "static_string($size)"; } else { print "no render bytes $subtype\n"; diff --git a/plugins/ruby/item.rb b/plugins/ruby/item.rb index 34b404505..469ec7449 100644 --- a/plugins/ruby/item.rb +++ b/plugins/ruby/item.rb @@ -4,9 +4,18 @@ module DFHack # arg similar to unit.rb/unit_find; no arg = 'k' menu def item_find(what=:selected, y=nil, z=nil) if what == :selected - if curview._rtti_classname == :viewscreen_itemst - ref = curview.entry_ref[curview.cursor_pos] - ref.item_tg if ref.kind_of?(GeneralRefItem) + case curview._rtti_classname + when :viewscreen_itemst + if ref = curview.entry_ref[curview.cursor_pos] + ref.item_tg if ref.kind_of?(GeneralRefItem) + else + # not a container + curview.item + end + when :viewscreen_storesst # z/stocks + if curview.in_group_mode == 0 and curview.in_right_list == 1 + curview.items[curview.item_cursor] + end else case ui.main.mode when :LookAround @@ -26,6 +35,8 @@ module DFHack u = world.units.active[ui_selected_unit] u.inventory[ui_look_cursor].item if u and u.pos.z == cursor.z and ui_unit_view_mode.value == :Inventory and u.inventory[ui_look_cursor] + else + ui.follow_item_tg if ui.follow_item != -1 end end elsif what.kind_of?(Integer) @@ -40,5 +51,17 @@ module DFHack raise "what what?" end end + + # check item flags to see if it is suitable for use as a job input material + def item_isfree(i) + !i.flags.trader and + !i.flags.in_job and + !i.flags.in_inventory and + !i.flags.removed and + !i.flags.in_building and + !i.flags.owned and + !i.flags.forbid + end + end end diff --git a/plugins/ruby/map.rb b/plugins/ruby/map.rb index dccea7291..7c616d722 100644 --- a/plugins/ruby/map.rb +++ b/plugins/ruby/map.rb @@ -26,7 +26,7 @@ module DFHack end end - def map_tile_at(x, y=nil, z=nil) + def map_tile_at(x=df.cursor, y=nil, z=nil) x = x.pos if x.respond_to?(:pos) x, y, z = x.x, x.y, x.z if x.respond_to?(:x) b = map_block_at(x, y, z) @@ -67,6 +67,14 @@ module DFHack @mapblock = b end + def offset(dx, dy=nil, dz=0) + if dx.respond_to?(:x) + dz = dx.z if dx.respond_to?(:z) + dx, dy = dx.x, dx.y + end + df.map_tile_at(@x+dx, @y+dy, @z+dz) + end + def designation @mapblock.designation[@dx][@dy] end @@ -189,8 +197,25 @@ module DFHack end def dig(mode=:Default) - designation.dig = mode - mapblock.flags.designated = true + if mode == :Smooth + if (tilemat == :STONE or tilemat == :MINERAL) and caption !~ /smooth|pillar|fortification/i and # XXX caption.. + designation.smooth == 0 and (designation.hidden or not df.world.job_list.find { |j| + # the game removes 'smooth' designation as soon as it assigns a job, if we + # re-set it the game may queue another :DetailWall that will carve a fortification + (j.job_type == :DetailWall or j.job_type == :DetailFloor) and df.same_pos?(j, self) + }) + designation.dig = :No + designation.smooth = 1 + mapblock.flags.designated = true + end + else + return if mode != :No and designation.dig == :No and not designation.hidden and df.world.job_list.find { |j| + # someone already enroute to dig here, avoid 'Inappropriate dig square' spam + JobType::Type[j.job_type] == :Digging and df.same_pos?(j, self) + } + designation.dig = mode + mapblock.flags.designated = true if mode != :No + end end def spawn_liquid(quantity, is_magma=false, flowing=true) @@ -215,5 +240,35 @@ module DFHack def spawn_magma(quantity=7) spawn_liquid(quantity, true) end + + # yield a serie of tiles until the block returns true, returns the matching tile + # the yielded tiles form a (squared) spiral centered here in the current zlevel + # eg for radius 4, yields (-4, -4), (-4, -3), .., (-4, 3), + # (-4, 4), (-3, 4), .., (4, 4), .., (4, -4), .., (-3, -4) + # then move on to radius 5 + def spiral_search(maxradius=100, minradius=0, step=1) + if minradius == 0 + return self if yield self + minradius += step + end + + sides = [[0, 1], [1, 0], [0, -1], [-1, 0]] + (minradius..maxradius).step(step) { |r| + sides.length.times { |s| + dxr, dyr = sides[(s-1) % sides.length] + dx, dy = sides[s] + (-r...r).step(step) { |v| + t = offset(dxr*r + dx*v, dyr*r + dy*v) + return t if t and yield t + } + } + } + nil + end + + # returns dx^2+dy^2+dz^2 + def distance_to(ot) + (x-ot.x)**2 + (y-ot.y)**2 + (z-ot.z)**2 + end end end diff --git a/plugins/ruby/material.rb b/plugins/ruby/material.rb index 4a92118d6..ca0a64779 100644 --- a/plugins/ruby/material.rb +++ b/plugins/ruby/material.rb @@ -189,6 +189,10 @@ module DFHack end def to_s ; token ; end + + def ===(other) + other.mat_index == mat_index and other.mat_type == mat_type + end end class << self diff --git a/plugins/ruby/plant.rb b/plugins/ruby/plant.rb index 5d6b9d724..2f5a1c7c4 100644 --- a/plugins/ruby/plant.rb +++ b/plugins/ruby/plant.rb @@ -51,7 +51,7 @@ module DFHack end SaplingToTreeAge = 120960 - def cuttrees(material=nil, count_max=100) + def cuttrees(material=nil, count_max=100, quiet=false) if !material # list trees cnt = Hash.new(0) @@ -62,7 +62,7 @@ module DFHack } cnt.sort_by { |mat, c| c }.each { |mat, c| name = @raws_tree_name[mat] - puts " #{name} #{c}" + puts " #{name} #{c}" unless quiet } else cnt = 0 @@ -78,11 +78,11 @@ module DFHack break if cnt == count_max end } - puts "Updated #{cnt} plant designations" + puts "Updated #{cnt} plant designations" unless quiet end end - def growtrees(material=nil, count_max=100) + def growtrees(material=nil, count_max=100, quiet=false) if !material # list plants cnt = Hash.new(0) @@ -93,7 +93,7 @@ module DFHack } cnt.sort_by { |mat, c| c }.each { |mat, c| name = @raws_tree_name[mat] - puts " #{name} #{c}" + puts " #{name} #{c}" unless quiet } else cnt = 0 @@ -104,7 +104,7 @@ module DFHack cnt += 1 break if cnt == count_max } - puts "Grown #{cnt} saplings" + puts "Grown #{cnt} saplings" unless quiet end end end diff --git a/plugins/ruby/ruby-autogen-defs.rb b/plugins/ruby/ruby-autogen-defs.rb index 0cee6426f..d5bcb08f4 100644 --- a/plugins/ruby/ruby-autogen-defs.rb +++ b/plugins/ruby/ruby-autogen-defs.rb @@ -124,15 +124,17 @@ module DFHack case h when Hash; h.each { |k, v| send("#{k}=", v) } when Array; names = _field_names ; raise 'bad size' if names.length != h.length ; names.zip(h).each { |n, a| send("#{n}=", a) } - when Compound; _field_names.each { |n| send("#{n}=", h.send(n)) } - else raise 'wut?' + else _field_names.each { |n| send("#{n}=", h.send(n)) } end end def _fields ; self.class._fields.to_a ; end def _fields_ancestors ; self.class._fields_ancestors.to_a ; end def _field_names ; _fields_ancestors.map { |n, o, s| n } ; end def _rtti_classname ; self.class._rtti_classname ; end + def _raw_rtti_classname ; df.get_rtti_classname(df.get_vtable_ptr(@_memaddr)) if self.class._rtti_classname ; end def _sizeof ; self.class._sizeof ; end + def ==(o) ; o.kind_of?(Compound) and o._memaddr == _memaddr ; end + @@inspecting = {} # avoid infinite recursion on mutually-referenced objects def inspect cn = self.class.name.sub(/^DFHack::/, '') @@ -280,6 +282,10 @@ module DFHack DFHack.memory_read_int32(@_memaddr) & 0xffffffff end + def _setp(v) + DFHack.memory_write_int32(@_memaddr, v) + end + def _get addr = _getp return if addr == 0 @@ -289,12 +295,20 @@ module DFHack # XXX shaky... def _set(v) - if v.kind_of?(Pointer) - DFHack.memory_write_int32(@_memaddr, v._getp) - elsif v.kind_of?(MemStruct) - DFHack.memory_write_int32(@_memaddr, v._memaddr) - else - _get._set(v) + case v + when Pointer; DFHack.memory_write_int32(@_memaddr, v._getp) + when MemStruct; DFHack.memory_write_int32(@_memaddr, v._memaddr) + when Integer + if @_tg and @_tg.kind_of?(MemHack::Number) + if _getp == 0 + _setp(DFHack.malloc(@_tg._bits/8)) + end + @_tg._at(_getp)._set(v) + else + DFHack.memory_write_int32(@_memaddr, v) + end + when nil; DFHack.memory_write_int32(@_memaddr, 0) + else _get._set(v) end end @@ -328,6 +342,16 @@ module DFHack self end + def _set(v) + case v + when Pointer; DFHack.memory_write_int32(@_memaddr, v._getp) + when MemStruct; DFHack.memory_write_int32(@_memaddr, v._memaddr) + when Integer; DFHack.memory_write_int32(@_memaddr, v) + when nil; DFHack.memory_write_int32(@_memaddr, 0) + else raise "cannot PointerAry._set(#{v.inspect})" + end + end + def [](i) addr = _getp(i) return if addr == 0 @@ -411,11 +435,20 @@ module DFHack def initialize(length) @_length = length end + def length + if @_length == -1 + maxlen = 4096 - (@_memaddr & 0xfff) + maxlen += 4096 until len = DFHack.memory_read(@_memaddr, maxlen).index(?\0) + len + else + @_length + end + end def _get - DFHack.memory_read(@_memaddr, @_length) + DFHack.memory_read(@_memaddr, length) end def _set(v) - DFHack.memory_write(@_memaddr, v[0, @_length]) + DFHack.memory_write(@_memaddr, v[0, length]) end end @@ -464,7 +497,7 @@ module DFHack def []=(idx, v) idx += length if idx < 0 if idx >= length - insert_at(idx, 0) + insert_at(length, 0) while idx >= length elsif idx < 0 raise 'index out of bounds' end @@ -698,8 +731,8 @@ module DFHack return if not addr @_tg._at(addr)._get end - alias next= _next= - alias prev= _prev= + alias next= _next= + alias prev= _prev= include Enumerable def each @@ -747,6 +780,55 @@ module DFHack end end + class StlSet + attr_accessor :_memaddr, :_enum + def self.cpp_new(init=nil, enum=nil) + ret = new DFHack.memory_stlset_new, enum + init.each { |k| ret.set(k) } if init + ret + end + + def initialize(addr, enum=nil) + addr = nil if addr == 0 + @_memaddr = addr + @_enum = enum + end + + def isset(key) + raise unless @_memaddr + key = @_enum.int(key) if _enum + raise "unknown key #{key.inspect}" if key.kind_of?(::Symbol) + DFHack.memory_stlset_isset(@_memaddr, key) + end + alias is_set? isset + + def set(key) + raise unless @_memaddr + key = @_enum.int(key) if _enum + raise "unknown key #{key.inspect}" if key.kind_of?(::Symbol) + DFHack.memory_stlset_set(@_memaddr, key) + end + + def delete(key) + raise unless @_memaddr + key = @_enum.int(key) if _enum + raise "unknown key #{key.inspect}" if key.kind_of?(::Symbol) + DFHack.memory_stlset_deletekey(@_memaddr, key) + end + + def clear + raise unless @_memaddr + DFHack.memory_stlset_clear(@_memaddr) + end + + def _cpp_delete + raise unless @_memaddr + DFHack.memory_stlset_delete(@_memaddr) + @_memaddr = nil + end + end + + # cpp rtti name -> rb class @rtti_n2c = {} @rtti_c2n = {} @@ -795,8 +877,12 @@ module DFHack v if v != 0 end - def self.vmethod_call(obj, voff, a0=0, a1=0, a2=0, a3=0, a4=0) - vmethod_do_call(obj._memaddr, voff, vmethod_arg(a0), vmethod_arg(a1), vmethod_arg(a2), vmethod_arg(a3)) + def self.vmethod_call(obj, voff, a0=0, a1=0, a2=0, a3=0, a4=0, a5=0) + this = obj._memaddr + vt = df.get_vtable_ptr(this) + fptr = df.memory_read_int32(vt + voff) & 0xffffffff + vmethod_do_call(this, fptr, vmethod_arg(a0), vmethod_arg(a1), vmethod_arg(a2), + vmethod_arg(a3), vmethod_arg(a4), vmethod_arg(a5)) end def self.vmethod_arg(arg) @@ -805,7 +891,7 @@ module DFHack when true; 1 when Integer; arg #when String; [arg].pack('p').unpack('L')[0] # raw pointer to buffer - when MemHack::Compound; arg._memaddr + when MemHack::Compound, StlSet; arg._memaddr else raise "bad vmethod arg #{arg.class}" end end diff --git a/plugins/ruby/ruby.cpp b/plugins/ruby/ruby.cpp index 482714d2a..13190f70d 100644 --- a/plugins/ruby/ruby.cpp +++ b/plugins/ruby/ruby.cpp @@ -118,18 +118,8 @@ DFhackCExport command_result plugin_shutdown ( color_ostream &out ) return CR_OK; } -// send a single ruby line to be evaluated by the ruby thread -DFhackCExport command_result plugin_eval_ruby( color_ostream &out, const char *command) +static command_result do_plugin_eval_ruby(color_ostream &out, const char *command) { - // if dlopen failed - if (!r_thread) - return CR_FAILURE; - - // wrap all ruby code inside a suspend block - // if we dont do that and rely on ruby code doing it, we'll deadlock in - // onupdate - CoreSuspender suspend; - command_result ret; // ensure ruby thread is idle @@ -159,6 +149,27 @@ DFhackCExport command_result plugin_eval_ruby( color_ostream &out, const char *c return ret; } +// send a single ruby line to be evaluated by the ruby thread +DFhackCExport command_result plugin_eval_ruby( color_ostream &out, const char *command) +{ + // if dlopen failed + if (!r_thread) + return CR_FAILURE; + + if (!strncmp(command, "nolock ", 7)) { + // debug only! + // run ruby commands without locking the main thread + // useful when the game is frozen after a segfault + return do_plugin_eval_ruby(out, command+7); + } else { + // wrap all ruby code inside a suspend block + // if we dont do that and rely on ruby code doing it, we'll deadlock in + // onupdate + CoreSuspender suspend; + return do_plugin_eval_ruby(out, command); + } +} + DFhackCExport command_result plugin_onupdate ( color_ostream &out ) { if (!r_thread) @@ -205,8 +216,6 @@ DFhackCExport command_result plugin_onstatechange ( color_ostream &out, state_ch static command_result df_rubyeval(color_ostream &out, std::vector & parameters) { - command_result ret; - if (parameters.size() == 1 && (parameters[0] == "help" || parameters[0] == "?")) { out.print("This command executes an arbitrary ruby statement.\n"); @@ -632,7 +641,7 @@ static VALUE rb_dfmemory_patch(VALUE self, VALUE addr, VALUE raw) bool ret; ret = Core::getInstance().p->patchMemory((void*)rb_num2ulong(addr), - rb_string_value_ptr(&raw), strlen); + rb_string_value_ptr(&raw), strlen); return ret ? Qtrue : Qfalse; } @@ -835,11 +844,54 @@ static VALUE rb_dfmemory_bitarray_set(VALUE self, VALUE addr, VALUE idx, VALUE v return Qtrue; } +// add basic support for std::set used for passing keyboard keys to viewscreens +#include +static VALUE rb_dfmemory_set_new(VALUE self) +{ + std::set *ptr = new std::set; + return rb_uint2inum((uint32_t)ptr); +} + +static VALUE rb_dfmemory_set_delete(VALUE self, VALUE set) +{ + std::set *ptr = (std::set*)rb_num2ulong(set); + if (ptr) + delete ptr; + return Qtrue; +} + +static VALUE rb_dfmemory_set_set(VALUE self, VALUE set, VALUE key) +{ + std::set *ptr = (std::set*)rb_num2ulong(set); + ptr->insert(rb_num2ulong(key)); + return Qtrue; +} + +static VALUE rb_dfmemory_set_isset(VALUE self, VALUE set, VALUE key) +{ + std::set *ptr = (std::set*)rb_num2ulong(set); + return ptr->count(rb_num2ulong(key)) ? Qtrue : Qfalse; +} + +static VALUE rb_dfmemory_set_deletekey(VALUE self, VALUE set, VALUE key) +{ + std::set *ptr = (std::set*)rb_num2ulong(set); + ptr->erase(rb_num2ulong(key)); + return Qtrue; +} + +static VALUE rb_dfmemory_set_clear(VALUE self, VALUE set) +{ + std::set *ptr = (std::set*)rb_num2ulong(set); + ptr->clear(); + return Qtrue; +} + /* call an arbitrary object virtual method */ #ifdef WIN32 -__declspec(naked) static int raw_vcall(char **that, unsigned long voff, unsigned long a0, - unsigned long a1, unsigned long a2, unsigned long a3) +__declspec(naked) static int raw_vcall(void *that, void *fptr, unsigned long a0, + unsigned long a1, unsigned long a2, unsigned long a3, unsigned long a4, unsigned long a5) { // __thiscall requires that the callee cleans up the stack // here we dont know how many arguments it will take, so @@ -848,6 +900,8 @@ __declspec(naked) static int raw_vcall(char **that, unsigned long voff, unsigned push ebp mov ebp, esp + push a5 + push a4 push a3 push a2 push a1 @@ -855,9 +909,7 @@ __declspec(naked) static int raw_vcall(char **that, unsigned long voff, unsigned mov ecx, that - mov eax, [ecx] - add eax, voff - call [eax] + call fptr mov esp, ebp pop ebp @@ -865,25 +917,25 @@ __declspec(naked) static int raw_vcall(char **that, unsigned long voff, unsigned } } #else -static int raw_vcall(char **that, unsigned long voff, unsigned long a0, - unsigned long a1, unsigned long a2, unsigned long a3) +static int raw_vcall(void *that, void *fptr, unsigned long a0, + unsigned long a1, unsigned long a2, unsigned long a3, unsigned long a4, unsigned long a5) { - int (*fptr)(char **me, int, int, int, int); - fptr = (decltype(fptr))*(void**)(*that + voff); - return fptr(that, a0, a1, a2, a3); + int (*t_fptr)(void *me, int, int, int, int, int, int); + t_fptr = (decltype(t_fptr))fptr; + return t_fptr(that, a0, a1, a2, a3, a4, a5); } #endif // call an arbitrary vmethod, convert args/ret to native values for raw_vcall -static VALUE rb_dfvcall(VALUE self, VALUE cppobj, VALUE cppvoff, VALUE a0, VALUE a1, VALUE a2, VALUE a3) +static VALUE rb_dfvcall(VALUE self, VALUE cppobj, VALUE fptr, VALUE a0, VALUE a1, VALUE a2, VALUE a3, VALUE a4, VALUE a5) { - return rb_int2inum(raw_vcall((char**)rb_num2ulong(cppobj), rb_num2ulong(cppvoff), + return rb_int2inum(raw_vcall((void*)rb_num2ulong(cppobj), (void*)rb_num2ulong(fptr), rb_num2ulong(a0), rb_num2ulong(a1), - rb_num2ulong(a2), rb_num2ulong(a3))); + rb_num2ulong(a2), rb_num2ulong(a3), + rb_num2ulong(a4), rb_num2ulong(a5))); } - // define module DFHack and its methods static void ruby_bind_dfhack(void) { rb_cDFHack = rb_define_module("DFHack"); @@ -902,7 +954,7 @@ static void ruby_bind_dfhack(void) { rb_define_singleton_method(rb_cDFHack, "print_err", RUBY_METHOD_FUNC(rb_dfprint_err), 1); rb_define_singleton_method(rb_cDFHack, "malloc", RUBY_METHOD_FUNC(rb_dfmalloc), 1); rb_define_singleton_method(rb_cDFHack, "free", RUBY_METHOD_FUNC(rb_dffree), 1); - rb_define_singleton_method(rb_cDFHack, "vmethod_do_call", RUBY_METHOD_FUNC(rb_dfvcall), 6); + rb_define_singleton_method(rb_cDFHack, "vmethod_do_call", RUBY_METHOD_FUNC(rb_dfvcall), 8); rb_define_singleton_method(rb_cDFHack, "memory_read", RUBY_METHOD_FUNC(rb_dfmemory_read), 2); rb_define_singleton_method(rb_cDFHack, "memory_read_int8", RUBY_METHOD_FUNC(rb_dfmemory_read_int8), 1); @@ -950,4 +1002,10 @@ static void ruby_bind_dfhack(void) { rb_define_singleton_method(rb_cDFHack, "memory_bitarray_resize", RUBY_METHOD_FUNC(rb_dfmemory_bitarray_resize), 2); rb_define_singleton_method(rb_cDFHack, "memory_bitarray_isset", RUBY_METHOD_FUNC(rb_dfmemory_bitarray_isset), 2); rb_define_singleton_method(rb_cDFHack, "memory_bitarray_set", RUBY_METHOD_FUNC(rb_dfmemory_bitarray_set), 3); + rb_define_singleton_method(rb_cDFHack, "memory_stlset_new", RUBY_METHOD_FUNC(rb_dfmemory_set_new), 0); + rb_define_singleton_method(rb_cDFHack, "memory_stlset_delete", RUBY_METHOD_FUNC(rb_dfmemory_set_delete), 1); + rb_define_singleton_method(rb_cDFHack, "memory_stlset_set", RUBY_METHOD_FUNC(rb_dfmemory_set_set), 2); + rb_define_singleton_method(rb_cDFHack, "memory_stlset_isset", RUBY_METHOD_FUNC(rb_dfmemory_set_isset), 2); + rb_define_singleton_method(rb_cDFHack, "memory_stlset_deletekey", RUBY_METHOD_FUNC(rb_dfmemory_set_deletekey), 2); + rb_define_singleton_method(rb_cDFHack, "memory_stlset_clear", RUBY_METHOD_FUNC(rb_dfmemory_set_clear), 1); } diff --git a/plugins/ruby/ruby.rb b/plugins/ruby/ruby.rb index 8c2c97969..ab095e8d8 100644 --- a/plugins/ruby/ruby.rb +++ b/plugins/ruby/ruby.rb @@ -25,11 +25,11 @@ end module DFHack class OnupdateCallback attr_accessor :callback, :timelimit, :minyear, :minyeartick - def initialize(cb, tl) + def initialize(cb, tl, initdelay=0) @callback = cb @ticklimit = tl @minyear = (tl ? df.cur_year : 0) - @minyeartick = (tl ? df.cur_year_tick : 0) + @minyeartick = (tl ? df.cur_year_tick+initdelay : 0) end # run callback if timedout @@ -48,6 +48,7 @@ module DFHack end end rescue + df.onupdate_unregister self puts_err "onupdate cb #$!", $!.backtrace end @@ -61,9 +62,9 @@ module DFHack # register a callback to be called every gframe or more # ex: DFHack.onupdate_register { DFHack.world.units[0].counters.job_counter = 0 } - def onupdate_register(ticklimit=nil, &b) + def onupdate_register(ticklimit=nil, initialtickdelay=0, &b) @onupdate_list ||= [] - @onupdate_list << OnupdateCallback.new(b, ticklimit) + @onupdate_list << OnupdateCallback.new(b, ticklimit, initialtickdelay) DFHack.onupdate_active = true if onext = @onupdate_list.sort.first DFHack.onupdate_minyear = onext.minyear @@ -81,6 +82,13 @@ module DFHack end end + # same as onupdate_register, but remove the callback once it returns true + def onupdate_register_once(*a) + handle = onupdate_register(*a) { + onupdate_unregister(handle) if yield + } + end + TICKS_PER_YEAR = 1200*28*12 # this method is called by dfhack every 'onupdate' if onupdate_active is true def onupdate @@ -112,6 +120,14 @@ module DFHack @onstatechange_list.delete b end + # same as onstatechange_register, but auto-unregisters if the block returns true + def onstatechange_register_once + handle = onstatechange_register { |st| + onstatechange_unregister(handle) if yield(st) + } + end + + # this method is called by dfhack every 'onstatechange' def onstatechange(newstate) @onstatechange_list ||= [] diff --git a/plugins/ruby/ui.rb b/plugins/ruby/ui.rb index 9dded66c2..a9dd05437 100644 --- a/plugins/ruby/ui.rb +++ b/plugins/ruby/ui.rb @@ -66,11 +66,12 @@ module DFHack world.status.reports << rep world.status.announcements << rep world.status.display_timer = 2000 + yield rep if block_given? end end - # add an announcement to display in a game popup message - # (eg "the megabeast foobar arrived") + # add an announcement to display in a game popup message + # (eg "the megabeast foobar arrived") def popup_announcement(str, color=nil, bright=nil) pop = PopupMessage.cpp_new(:text => str) pop.color = color if color @@ -78,4 +79,13 @@ module DFHack world.status.popups << pop end end + + class Viewscreen + def feed_keys(*keys) + keyset = StlSet.cpp_new(keys, InterfaceKey) + ret = feed(keyset) + keyset._cpp_delete + ret + end + end end diff --git a/plugins/ruby/unit.rb b/plugins/ruby/unit.rb index 1a619c5ce..4fbf75d8d 100644 --- a/plugins/ruby/unit.rb +++ b/plugins/ruby/unit.rb @@ -12,9 +12,15 @@ module DFHack ref.unit_tg if ref.kind_of?(GeneralRefUnit) when :viewscreen_unitlistst v = curview - # TODO fix xml to use enums everywhere - page = DFHack::ViewscreenUnitlistst_TPage.int(v.page) - v.units[page][v.cursor_pos[page]] + v.units[v.page][v.cursor_pos[v.page]] + when :viewscreen_petst + v = curview + case v.mode + when :List + v.animal[v.cursor].unit if !v.is_vermin[v.cursor] + when :SelectTrainer + v.trainer_unit[v.trainer_cursor] + end else case ui.main.mode when :ViewUnits @@ -24,6 +30,8 @@ module DFHack when :LookAround k = ui_look_list.items[ui_look_cursor] k.unit if k.type == :Unit + else + ui.follow_unit_tg if ui.follow_unit != -1 end end elsif what.kind_of?(Integer) @@ -46,8 +54,8 @@ module DFHack } end - def unit_iscitizen(u) - u.race == ui.race_id and u.civ_id == ui.civ_id and !u.flags1.dead and !u.flags1.merchant and + def unit_iscitizen(u) + u.race == ui.race_id and u.civ_id == ui.civ_id and !u.flags1.dead and !u.flags1.merchant and !u.flags1.forest and !u.flags1.diplomat and !u.flags2.resident and !u.flags3.ghostly and !u.curse.add_tags1.OPPOSED_TO_LIFE and !u.curse.add_tags1.CRAZED and u.mood != :Berserk @@ -84,7 +92,7 @@ module DFHack # filter 'attend meeting' not u.specific_refs.find { |s| s.type == :ACTIVITY } and # filter soldiers (TODO check schedule) - u.military.squad_index == -1 and + u.military.squad_id == -1 and # filter 'on break' not u.status.misc_traits.find { |t| t.id == :OnBreak } end @@ -104,7 +112,7 @@ module DFHack end class LanguageName - def to_s(english=true) + def to_s(english=false) df.translate_name(self, english) end end diff --git a/plugins/seedwatch.cpp b/plugins/seedwatch.cpp index 676c02ed5..ae87e4381 100755 --- a/plugins/seedwatch.cpp +++ b/plugins/seedwatch.cpp @@ -106,9 +106,8 @@ command_result df_seedwatch(color_ostream &out, vector& parameters) materialsReverser[world->raws.plants.all[i]->id] = i; } - World *w = Core::getInstance().getWorld(); t_gamemodes gm; - w->ReadGameMode(gm);// FIXME: check return value + World::ReadGameMode(gm);// FIXME: check return value // if game mode isn't fortress mode if(gm.g_mode != game_mode::DWARF || @@ -296,9 +295,8 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) return CR_OK; counter = 0; - World *w = Core::getInstance().getWorld(); t_gamemodes gm; - w->ReadGameMode(gm);// FIXME: check return value + World::ReadGameMode(gm);// FIXME: check return value // if game mode isn't fortress mode if(gm.g_mode != game_mode::DWARF || !(gm.g_type == game_type::DWARF_MAIN || gm.g_type == game_type::DWARF_RECLAIM)) diff --git a/plugins/siege-engine.cpp b/plugins/siege-engine.cpp index 2e362afec..4b2060e99 100644 --- a/plugins/siege-engine.cpp +++ b/plugins/siege-engine.cpp @@ -132,11 +132,6 @@ static void orient_engine(df::building_siegeenginest *bld, df::coord target) df::building_siegeenginest::Up; } -static int random_int(int val) -{ - return int(int64_t(rand())*val/RAND_MAX); -} - static int point_distance(df::coord speed) { return std::max(abs(speed.x), std::max(abs(speed.y), abs(speed.z))); @@ -347,10 +342,9 @@ static void load_engines() { clear_engines(); - auto pworld = Core::getInstance().getWorld(); std::vector vec; - pworld->GetPersistentData(&vec, "siege-engine/target/", true); + World::GetPersistentData(&vec, "siege-engine/target/", true); for (auto it = vec.begin(); it != vec.end(); ++it) { auto engine = find_engine(df::building::find(it->ival(0)), true); @@ -359,7 +353,7 @@ static void load_engines() engine->target.second = df::coord(it->ival(4), it->ival(5), it->ival(6)); } - pworld->GetPersistentData(&vec, "siege-engine/ammo/", true); + World::GetPersistentData(&vec, "siege-engine/ammo/", true); for (auto it = vec.begin(); it != vec.end(); ++it) { auto engine = find_engine(df::building::find(it->ival(0)), true); @@ -368,7 +362,7 @@ static void load_engines() engine->ammo_item_type = (df::item_type)it->ival(2); } - pworld->GetPersistentData(&vec, "siege-engine/stockpiles/", true); + World::GetPersistentData(&vec, "siege-engine/stockpiles/", true); for (auto it = vec.begin(); it != vec.end(); ++it) { auto engine = find_engine(df::building::find(it->ival(0)), true); @@ -377,14 +371,14 @@ static void load_engines() auto pile = df::building::find(it->ival(1)); if (!pile || pile->getType() != building_type::Stockpile) { - pworld->DeletePersistentData(*it); + World::DeletePersistentData(*it); continue;; } engine->stockpiles.insert(it->ival(1)); } - pworld->GetPersistentData(&vec, "siege-engine/profiles/", true); + World::GetPersistentData(&vec, "siege-engine/profiles/", true); for (auto it = vec.begin(); it != vec.end(); ++it) { auto engine = find_engine(df::building::find(it->ival(0)), true); @@ -393,7 +387,7 @@ static void load_engines() engine->profile.max_level = it->ival(2); } - pworld->GetPersistentData(&vec, "siege-engine/profile-workers/", true); + World::GetPersistentData(&vec, "siege-engine/profile-workers/", true); for (auto it = vec.begin(); it != vec.end(); ++it) { auto engine = find_engine(df::building::find(it->ival(0)), true); @@ -402,7 +396,7 @@ static void load_engines() auto unit = df::unit::find(it->ival(1)); if (!unit || !Units::isCitizen(unit)) { - pworld->DeletePersistentData(*it); + World::DeletePersistentData(*it); continue; } engine->profile.permitted_workers.push_back(it->ival(1)); @@ -434,9 +428,8 @@ static void clearTargetArea(df::building_siegeenginest *bld) if (auto engine = find_engine(bld)) engine->target = coord_range(); - auto pworld = Core::getInstance().getWorld(); auto key = stl_sprintf("siege-engine/target/%d", bld->id); - pworld->DeletePersistentData(pworld->GetPersistentData(key)); + World::DeletePersistentData(World::GetPersistentData(key)); } static bool setTargetArea(df::building_siegeenginest *bld, df::coord target_min, df::coord target_max) @@ -447,9 +440,8 @@ static bool setTargetArea(df::building_siegeenginest *bld, df::coord target_min, if (!enable_plugin()) return false; - auto pworld = Core::getInstance().getWorld(); auto key = stl_sprintf("siege-engine/target/%d", bld->id); - auto entry = pworld->GetPersistentData(key, NULL); + auto entry = World::GetPersistentData(key, NULL); if (!entry.isValid()) return false; @@ -491,13 +483,12 @@ static int setAmmoItem(lua_State *L) if (!is_valid_enum_item(item_type)) luaL_argerror(L, 2, "invalid item type"); - auto pworld = Core::getInstance().getWorld(); auto key = stl_sprintf("siege-engine/ammo/%d", engine->id); - auto entry = pworld->GetPersistentData(key, NULL); + auto entry = World::GetPersistentData(key, NULL); if (!entry.isValid()) return 0; - engine->ammo_vector_id = job_item_vector_id::ANY_FREE; + engine->ammo_vector_id = job_item_vector_id::IN_PLAY; engine->ammo_item_type = item_type; FOR_ENUM_ITEMS(job_item_vector_id, id) @@ -523,9 +514,8 @@ static void forgetStockpileLink(EngineInfo *engine, int pile_id) { engine->stockpiles.erase(pile_id); - auto pworld = Core::getInstance().getWorld(); auto key = stl_sprintf("siege-engine/stockpiles/%d/%d", engine->id, pile_id); - pworld->DeletePersistentData(pworld->GetPersistentData(key)); + World::DeletePersistentData(World::GetPersistentData(key)); } static void update_stockpile_links(EngineInfo *engine) @@ -583,9 +573,8 @@ static bool addStockpileLink(df::building_siegeenginest *bld, df::building_stock if (!enable_plugin()) return false; - auto pworld = Core::getInstance().getWorld(); auto key = stl_sprintf("siege-engine/stockpiles/%d/%d", bld->id, pile->id); - auto entry = pworld->GetPersistentData(key, NULL); + auto entry = World::GetPersistentData(key, NULL); if (!entry.isValid()) return false; @@ -620,9 +609,8 @@ static df::workshop_profile *saveWorkshopProfile(df::building_siegeenginest *bld return NULL; // Save skill limits - auto pworld = Core::getInstance().getWorld(); auto key = stl_sprintf("siege-engine/profiles/%d", bld->id); - auto entry = pworld->GetPersistentData(key, NULL); + auto entry = World::GetPersistentData(key, NULL); if (!entry.isValid()) return NULL; @@ -637,18 +625,18 @@ static df::workshop_profile *saveWorkshopProfile(df::building_siegeenginest *bld auto &workers = engine->profile.permitted_workers; key = stl_sprintf("siege-engine/profile-workers/%d", bld->id); - pworld->GetPersistentData(&vec, key, true); + World::GetPersistentData(&vec, key, true); for (auto it = vec.begin(); it != vec.end(); ++it) { if (linear_index(workers, it->ival(1)) < 0) - pworld->DeletePersistentData(*it); + World::DeletePersistentData(*it); } for (size_t i = 0; i < workers.size(); i++) { key = stl_sprintf("siege-engine/profile-workers/%d/%d", bld->id, workers[i]); - entry = pworld->GetPersistentData(key, NULL); + entry = World::GetPersistentData(key, NULL); if (!entry.isValid()) continue; entry.ival(0) = engine->id; @@ -1573,6 +1561,8 @@ struct projectile_hook : df::proj_itemst { if (next_pos.z == cur_pos.z && !isPassableTile(next_pos)) start_z = 49000; + bool forbid_ammo = DF_GLOBAL_VALUE(standing_orders_forbid_used_ammo, false); + MapExtras::MapCache mc; std::vector contents; Items::getContainedItems(item, &contents); @@ -1581,6 +1571,9 @@ struct projectile_hook : df::proj_itemst { { auto child = contents[i]; + if (forbid_ammo) + child->flags.bits.forbid = true; + // Liquids are vaporized so that they cover nearby units if (child->isLiquid()) { @@ -1797,8 +1790,7 @@ static bool enable_plugin() if (is_enabled) return true; - auto pworld = Core::getInstance().getWorld(); - auto entry = pworld->GetPersistentData("siege-engine/enabled", NULL); + auto entry = World::GetPersistentData("siege-engine/enabled", NULL); if (!entry.isValid()) return false; @@ -1823,8 +1815,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan case SC_MAP_LOADED: if (!gamemode || *gamemode == game_mode::DWARF) { - auto pworld = Core::getInstance().getWorld(); - bool enable = pworld->GetPersistentData("siege-engine/enabled").isValid(); + bool enable = World::GetPersistentData("siege-engine/enabled").isValid(); if (enable) { diff --git a/plugins/sort.cpp b/plugins/sort.cpp index 4b2bf7bbd..95ae109a8 100644 --- a/plugins/sort.cpp +++ b/plugins/sort.cpp @@ -356,7 +356,7 @@ DEFINE_SORT_HANDLER(unit_sorters, pet, "/List", animals) std::vector units; for (size_t i = 0; i < animals->animal.size(); i++) - units.push_back(animals->is_vermin[i] ? NULL : (df::unit*)animals->animal[i]); + units.push_back(animals->is_vermin[i] ? NULL : animals->animal[i].unit); if (compute_order(*pout, L, top, &order, units)) { diff --git a/plugins/stonesense b/plugins/stonesense index 37a823541..75df76626 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 37a823541538023b9f3d0d1e8039cf32851de68d +Subproject commit 75df766263b23182820a1e07b330e64f87d5c9b7 diff --git a/plugins/tweak.cpp b/plugins/tweak.cpp index fb286e0d7..93e5ab3bc 100644 --- a/plugins/tweak.cpp +++ b/plugins/tweak.cpp @@ -10,6 +10,7 @@ #include "modules/Screen.h" #include "modules/Units.h" #include "modules/Items.h" +#include "modules/Job.h" #include "MiscUtils.h" @@ -34,7 +35,8 @@ #include "df/ui_build_selector.h" #include "df/building_trapst.h" #include "df/item_actual.h" -#include "df/item_liquipowder.h" +#include "df/item_liquid_miscst.h" +#include "df/item_powder_miscst.h" #include "df/item_barst.h" #include "df/item_threadst.h" #include "df/item_clothst.h" @@ -43,6 +45,12 @@ #include "df/reaction.h" #include "df/reaction_reagent_itemst.h" #include "df/reaction_reagent_flags.h" +#include "df/viewscreen_layer_assigntradest.h" +#include "df/viewscreen_tradegoodsst.h" +#include "df/viewscreen_layer_militaryst.h" +#include "df/squad_position.h" +#include "df/job.h" +#include "df/general_ref_building_holderst.h" #include @@ -59,6 +67,7 @@ using df::global::ui_menu_width; using df::global::ui_area_map_width; using namespace DFHack::Gui; +using Screen::Pen; static command_result tweak(color_ostream &out, vector & parameters); @@ -109,6 +118,16 @@ DFhackCExport command_result plugin_init (color_ostream &out, std::vector categorize(true); } -struct dimension_lqp_hook : df::item_liquipowder { - typedef df::item_liquipowder interpose_base; +struct dimension_liquid_hook : df::item_liquid_miscst { + typedef df::item_liquid_miscst interpose_base; DEFINE_VMETHOD_INTERPOSE(bool, subtractDimension, (int32_t delta)) { @@ -398,7 +417,19 @@ struct dimension_lqp_hook : df::item_liquipowder { } }; -IMPLEMENT_VMETHOD_INTERPOSE(dimension_lqp_hook, subtractDimension); +IMPLEMENT_VMETHOD_INTERPOSE(dimension_liquid_hook, subtractDimension); + +struct dimension_powder_hook : df::item_powder_miscst { + typedef df::item_powder_miscst interpose_base; + + DEFINE_VMETHOD_INTERPOSE(bool, subtractDimension, (int32_t delta)) + { + correct_dimension(this, delta, dimension); + return INTERPOSE_NEXT(subtractDimension)(delta); + } +}; + +IMPLEMENT_VMETHOD_INTERPOSE(dimension_powder_hook, subtractDimension); struct dimension_bar_hook : df::item_barst { typedef df::item_barst interpose_base; @@ -494,6 +525,143 @@ struct advmode_contained_hook : df::viewscreen_layer_unit_actionst { IMPLEMENT_VMETHOD_INTERPOSE(advmode_contained_hook, feed); +struct fast_trade_assign_hook : df::viewscreen_layer_assigntradest { + typedef df::viewscreen_layer_assigntradest interpose_base; + + DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) + { + if (layer_objects[1]->active && input->count(interface_key::SELECT_ALL)) + { + set tmp; tmp.insert(interface_key::SELECT); + INTERPOSE_NEXT(feed)(&tmp); + tmp.clear(); tmp.insert(interface_key::STANDARDSCROLL_DOWN); + INTERPOSE_NEXT(feed)(&tmp); + } + else + INTERPOSE_NEXT(feed)(input); + } +}; + +IMPLEMENT_VMETHOD_INTERPOSE(fast_trade_assign_hook, feed); + +struct fast_trade_select_hook : df::viewscreen_tradegoodsst { + typedef df::viewscreen_tradegoodsst interpose_base; + + DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) + { + if (!(is_unloading || !has_traders || in_edit_count) + && input->count(interface_key::SELECT_ALL)) + { + set tmp; tmp.insert(interface_key::SELECT); + INTERPOSE_NEXT(feed)(&tmp); + if (in_edit_count) + INTERPOSE_NEXT(feed)(&tmp); + tmp.clear(); tmp.insert(interface_key::STANDARDSCROLL_DOWN); + INTERPOSE_NEXT(feed)(&tmp); + } + else + INTERPOSE_NEXT(feed)(input); + } +}; + +IMPLEMENT_VMETHOD_INTERPOSE(fast_trade_select_hook, feed); + +struct military_assign_hook : df::viewscreen_layer_militaryst { + typedef df::viewscreen_layer_militaryst interpose_base; + + inline bool inPositionsMode() { + return page == Positions && !(in_create_squad || in_new_squad); + } + + DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) + { + if (inPositionsMode() && !layer_objects[0]->active) + { + auto pos_list = layer_objects[1]; + auto plist = layer_objects[2]; + auto &cand = positions.candidates; + + // Save the candidate list and cursors + std::vector copy = cand; + int cursor = plist->getListCursor(); + int pos_cursor = pos_list->getListCursor(); + + INTERPOSE_NEXT(feed)(input); + + if (inPositionsMode() && !layer_objects[0]->active) + { + bool is_select = input->count(interface_key::SELECT); + + // Resort the candidate list and restore cursor + // on add to squad OR scroll in the position list. + if (!plist->active || is_select) + { + // Since we don't know the actual sorting order, preserve + // the ordering of the items in the list before keypress. + // This does the right thing even if the list was sorted + // with sort-units. + std::set prev, next; + prev.insert(copy.begin(), copy.end()); + next.insert(cand.begin(), cand.end()); + std::vector out; + + // (old-before-cursor) (new) |cursor| (old-after-cursor) + for (int i = 0; i < cursor && i < (int)copy.size(); i++) + if (next.count(copy[i])) out.push_back(copy[i]); + for (size_t i = 0; i < cand.size(); i++) + if (!prev.count(cand[i])) out.push_back(cand[i]); + int new_cursor = out.size(); + for (int i = cursor; i < (int)copy.size(); i++) + if (next.count(copy[i])) out.push_back(copy[i]); + + cand.swap(out); + plist->setListLength(cand.size()); + if (new_cursor < (int)cand.size()) + plist->setListCursor(new_cursor); + } + + // Preserve the position list index on remove from squad + if (pos_list->active && is_select) + pos_list->setListCursor(pos_cursor); + } + } + else + INTERPOSE_NEXT(feed)(input); + } + + DEFINE_VMETHOD_INTERPOSE(void, render, ()) + { + INTERPOSE_NEXT(render)(); + + if (inPositionsMode()) + { + auto plist = layer_objects[2]; + int x1 = plist->getX1(), y1 = plist->getY1(); + int x2 = plist->getX2(), y2 = plist->getY2(); + int i1 = plist->getFirstVisible(), i2 = plist->getLastVisible(); + int si = plist->getListCursor(); + + for (int y = y1, i = i1; i <= i2; i++, y++) + { + auto unit = vector_get(positions.candidates, i); + if (!unit || unit->military.squad_id < 0) + continue; + + for (int x = x1; x <= x2; x++) + { + Pen cur_tile = Screen::readTile(x, y); + if (!cur_tile.valid()) continue; + cur_tile.fg = (i == si) ? COLOR_BROWN : COLOR_GREEN; + Screen::paintTile(cur_tile, x, y); + } + } + } + } +}; + +IMPLEMENT_VMETHOD_INTERPOSE(military_assign_hook, feed); +IMPLEMENT_VMETHOD_INTERPOSE(military_assign_hook, render); + static void enable_hook(color_ostream &out, VMethodInterposeLinkBase &hook, vector ¶meters) { if (vector_get(parameters, 1) == "disable") @@ -644,7 +812,8 @@ static command_result tweak(color_ostream &out, vector ¶meters) } else if (cmd == "fix-dimensions") { - enable_hook(out, INTERPOSE_HOOK(dimension_lqp_hook, subtractDimension), parameters); + enable_hook(out, INTERPOSE_HOOK(dimension_liquid_hook, subtractDimension), parameters); + enable_hook(out, INTERPOSE_HOOK(dimension_powder_hook, subtractDimension), parameters); enable_hook(out, INTERPOSE_HOOK(dimension_bar_hook, subtractDimension), parameters); enable_hook(out, INTERPOSE_HOOK(dimension_thread_hook, subtractDimension), parameters); enable_hook(out, INTERPOSE_HOOK(dimension_cloth_hook, subtractDimension), parameters); @@ -653,6 +822,19 @@ static command_result tweak(color_ostream &out, vector ¶meters) { enable_hook(out, INTERPOSE_HOOK(advmode_contained_hook, feed), parameters); } + else if (cmd == "fast-trade") + { + enable_hook(out, INTERPOSE_HOOK(fast_trade_assign_hook, feed), parameters); + enable_hook(out, INTERPOSE_HOOK(fast_trade_select_hook, feed), parameters); + } + else if (cmd == "military-stable-assign") + { + enable_hook(out, INTERPOSE_HOOK(military_assign_hook, feed), parameters); + } + else if (cmd == "military-color-assigned") + { + enable_hook(out, INTERPOSE_HOOK(military_assign_hook, render), parameters); + } else return CR_WRONG_USAGE; diff --git a/plugins/weather.cpp b/plugins/weather.cpp index 33fa45fd3..98476ef2c 100644 --- a/plugins/weather.cpp +++ b/plugins/weather.cpp @@ -84,7 +84,6 @@ command_result weather (color_ostream &con, vector & parameters) CoreSuspender suspend; - DFHack::World * w = Core::getInstance().getWorld(); if(!df::global::current_weather) { con << "Weather support seems broken :(" << std::endl; @@ -123,22 +122,22 @@ command_result weather (color_ostream &con, vector & parameters) if(rain) { con << "Here comes the rain." << std::endl; - w->SetCurrentWeather(weather_type::Rain); + World::SetCurrentWeather(weather_type::Rain); } if(snow) { con << "Snow everywhere!" << std::endl; - w->SetCurrentWeather(weather_type::Snow); + World::SetCurrentWeather(weather_type::Snow); } if(clear) { con << "Suddenly, sunny weather!" << std::endl; - w->SetCurrentWeather(weather_type::None); + World::SetCurrentWeather(weather_type::None); } if(val_override != -1) { con << "I have no damn idea what this is... " << val_override << std::endl; - w->SetCurrentWeather(val_override); + World::SetCurrentWeather(val_override); } // FIXME: weather lock needs map ID to work reliably... needs to be implemented. } diff --git a/plugins/workflow.cpp b/plugins/workflow.cpp index 98258682e..d46daed44 100644 --- a/plugins/workflow.cpp +++ b/plugins/workflow.cpp @@ -4,6 +4,9 @@ #include "PluginManager.h" #include "MiscUtils.h" +#include "LuaTools.h" +#include "DataFuncs.h" + #include "modules/Materials.h" #include "modules/Items.h" #include "modules/Gui.h" @@ -37,6 +40,7 @@ #include "df/plant_raw.h" #include "df/inorganic_raw.h" #include "df/builtin_mats.h" +#include "df/vehicle.h" using std::vector; using std::string; @@ -303,8 +307,9 @@ public: void setGoalCount(int v) { config.ival(0) = v; } int goalGap() { - int gcnt = std::max(1, goalCount()/2); - return std::min(gcnt, config.ival(1) <= 0 ? 5 : config.ival(1)); + int cval = (config.ival(1) <= 0) ? 5 : config.ival(1); + int cmax = std::max(goalCount()-5, goalCount()/2); + return std::max(1, std::min(cmax, cval)); } void setGoalGap(int v) { config.ival(1) = v; } @@ -319,6 +324,7 @@ public: void init(const std::string &str) { config.val() = str; + config.ival(0) = 10; config.ival(2) = 0; } @@ -374,13 +380,6 @@ static bool isSupportedJob(df::job *job) job->job_type == job_type::CollectSand); } -static void enumLiveJobs(std::map &rv) -{ - df::job_list_link *p = world->job_list.next; - for (; p; p = p->next) - rv[p->item->id] = p->item; -} - static bool isOptionEnabled(unsigned flag) { return config.isValid() && (config.ival(0) & flag) != 0; @@ -438,9 +437,7 @@ static void start_protect(color_ostream &out) static void init_state(color_ostream &out) { - auto pworld = Core::getInstance().getWorld(); - - config = pworld->GetPersistentData("workflow/config"); + config = World::GetPersistentData("workflow/config"); if (config.isValid() && config.ival(0) == -1) config.ival(0) = 0; @@ -448,14 +445,14 @@ static void init_state(color_ostream &out) // Parse constraints std::vector items; - pworld->GetPersistentData(&items, "workflow/constraints"); + World::GetPersistentData(&items, "workflow/constraints"); for (int i = items.size()-1; i >= 0; i--) { if (get_constraint(out, items[i].val(), &items[i])) continue; out.printerr("Lost constraint %s\n", items[i].val().c_str()); - pworld->DeletePersistentData(items[i]); + World::DeletePersistentData(items[i]); } last_tick_frame_count = world->frame_counter; @@ -469,11 +466,9 @@ static void init_state(color_ostream &out) static void enable_plugin(color_ostream &out) { - auto pworld = Core::getInstance().getWorld(); - if (!config.isValid()) { - config = pworld->AddPersistentData("workflow/config"); + config = World::AddPersistentData("workflow/config"); config.ival(0) = 0; } @@ -729,7 +724,7 @@ static ItemConstraint *get_constraint(color_ostream &out, const std::string &str nct->config = *cfg; else { - nct->config = Core::getInstance().getWorld()->AddPersistentData("workflow/constraints"); + nct->config = World::AddPersistentData("workflow/constraints"); nct->init(str); } @@ -743,10 +738,24 @@ static void delete_constraint(ItemConstraint *cv) if (idx >= 0) vector_erase_at(constraints, idx); - Core::getInstance().getWorld()->DeletePersistentData(cv->config); + World::DeletePersistentData(cv->config); delete cv; } +static bool deleteConstraint(std::string name) +{ + for (size_t i = 0; i < constraints.size(); i++) + { + if (constraints[i]->config.val() != name) + continue; + + delete_constraint(constraints[i]); + return true; + } + + return false; +} + /****************************** * JOB-CONSTRAINT MAPPING * ******************************/ @@ -813,71 +822,6 @@ static void link_job_constraint(ProtectedJob *pj, df::item_type itype, int16_t i } } -static void compute_custom_job(ProtectedJob *pj, df::job *job) -{ - if (pj->reaction_id < 0) - pj->reaction_id = linear_index(df::reaction::get_vector(), - &df::reaction::code, job->reaction_name); - - df::reaction *r = df::reaction::find(pj->reaction_id); - if (!r) - return; - - for (size_t i = 0; i < r->products.size(); i++) - { - using namespace df::enums::reaction_product_item_flags; - - VIRTUAL_CAST_VAR(prod, df::reaction_product_itemst, r->products[i]); - if (!prod || (prod->item_type < (df::item_type)0 && !prod->flags.is_set(CRAFTS))) - continue; - - MaterialInfo mat(prod); - df::dfhack_material_category mat_mask(0); - - bool get_mat_prod = prod->flags.is_set(GET_MATERIAL_PRODUCT); - if (get_mat_prod || prod->flags.is_set(GET_MATERIAL_SAME)) - { - int reagent_idx = linear_index(r->reagents, &df::reaction_reagent::code, - prod->get_material.reagent_code); - if (reagent_idx < 0) - continue; - - int item_idx = linear_index(job->job_items, &df::job_item::reagent_index, reagent_idx); - if (item_idx >= 0) - mat.decode(job->job_items[item_idx]); - else - { - VIRTUAL_CAST_VAR(src, df::reaction_reagent_itemst, r->reagents[reagent_idx]); - if (!src) - continue; - mat.decode(src); - } - - if (get_mat_prod) - { - std::string code = prod->get_material.product_code; - - if (mat.isValid()) - { - int idx = linear_index(mat.material->reaction_product.id, code); - if (idx < 0) - continue; - - mat.decode(mat.material->reaction_product.material, idx); - } - else - { - if (code == "SOAP_MAT") - mat_mask.bits.soap = true; - } - } - } - - link_job_constraint(pj, prod->item_type, prod->item_subtype, - mat_mask, mat.type, mat.index, prod->flags.is_set(CRAFTS)); - } -} - static void guess_job_material(df::job *job, MaterialInfo &mat, df::dfhack_material_category &mat_mask) { using namespace df::enums::job_type; @@ -919,100 +863,24 @@ static void guess_job_material(df::job *job, MaterialInfo &mat, df::dfhack_mater } } -static void compute_job_outputs(color_ostream &out, ProtectedJob *pj) +static int cbEnumJobOutputs(lua_State *L) { - using namespace df::enums::job_type; - - // Custom reactions handled in another function - df::job *job = pj->job_copy; - - if (job->job_type == CustomReaction) - { - compute_custom_job(pj, job); - return; - } - - // Item type & subtype - df::item_type itype = ENUM_ATTR(job_type, item, job->job_type); - int16_t isubtype = job->item_subtype; - - if (itype == item_type::NONE && job->job_type != MakeCrafts) - return; - - // Item material & material category - MaterialInfo mat; - df::dfhack_material_category mat_mask; - guess_job_material(job, mat, mat_mask); - - // Job-specific code - switch (job->job_type) - { - case SmeltOre: - if (mat.inorganic) - { - std::vector &ores = mat.inorganic->metal_ore.mat_index; - for (size_t i = 0; i < ores.size(); i++) - link_job_constraint(pj, item_type::BAR, -1, 0, 0, ores[i]); - } - return; + auto pj = (ProtectedJob*)lua_touserdata(L, lua_upvalueindex(1)); - case ExtractMetalStrands: - if (mat.inorganic) - { - std::vector &threads = mat.inorganic->thread_metal.mat_index; - for (size_t i = 0; i < threads.size(); i++) - link_job_constraint(pj, item_type::THREAD, -1, 0, 0, threads[i]); - } - return; + lua_settop(L, 6); - case PrepareMeal: - if (job->mat_type != -1) - { - std::vector &food = df::itemdef_foodst::get_vector(); - for (size_t i = 0; i < food.size(); i++) - if (food[i]->level == job->mat_type) - link_job_constraint(pj, item_type::FOOD, i, 0, -1, -1); - return; - } - break; - - case MakeCrafts: - link_job_constraint(pj, item_type::NONE, -1, mat_mask, mat.type, mat.index, true); - return; - -#define PLANT_PROCESS_MAT(flag, tag) \ - if (mat.plant && mat.plant->flags.is_set(plant_raw_flags::flag)) \ - mat.decode(mat.plant->material_defs.type_##tag, \ - mat.plant->material_defs.idx_##tag); \ - else mat.decode(-1); - case BrewDrink: - PLANT_PROCESS_MAT(DRINK, drink); - break; - case MillPlants: - PLANT_PROCESS_MAT(MILL, mill); - break; - case ProcessPlants: - PLANT_PROCESS_MAT(THREAD, thread); - break; - case ProcessPlantsBag: - PLANT_PROCESS_MAT(LEAVES, leaves); - break; - case ProcessPlantsBarrel: - PLANT_PROCESS_MAT(EXTRACT_BARREL, extract_barrel); - break; - case ProcessPlantsVial: - PLANT_PROCESS_MAT(EXTRACT_VIAL, extract_vial); - break; - case ExtractFromPlants: - PLANT_PROCESS_MAT(EXTRACT_STILL_VIAL, extract_still_vial); - break; -#undef PLANT_PROCESS_MAT + df::dfhack_material_category mat_mask(0); + if (!lua_isnil(L, 3)) + Lua::CheckDFAssign(L, &mat_mask, 3); - default: - break; - } + link_job_constraint( + pj, + (df::item_type)luaL_optint(L, 1, -1), luaL_optint(L, 2, -1), + mat_mask, luaL_optint(L, 4, -1), luaL_optint(L, 5, -1), + lua_toboolean(L, 6) + ); - link_job_constraint(pj, itype, isubtype, mat_mask, mat.type, mat.index); + return 0; } static void map_job_constraints(color_ostream &out) @@ -1025,19 +893,32 @@ static void map_job_constraints(color_ostream &out) constraints[i]->is_active = false; } + auto L = Lua::Core::State; + Lua::StackUnwinder frame(L); + + bool ok = Lua::PushModulePublic(out, L, "plugins.workflow", "doEnumJobOutputs"); + if (!ok) + out.printerr("The workflow lua module is not available.\n"); + for (TKnownJobs::const_iterator it = known_jobs.begin(); it != known_jobs.end(); ++it) { ProtectedJob *pj = it->second; pj->constraints.clear(); - if (!pj->isLive()) + if (!ok || !pj->isLive()) continue; if (!melt_active && pj->actual_job->job_type == job_type::MeltMetalObject) melt_active = pj->isResumed(); - compute_job_outputs(out, pj); + // Call the lua module + lua_pushvalue(L, -1); + lua_pushlightuserdata(L, pj); + lua_pushcclosure(L, cbEnumJobOutputs, 1); + Lua::PushDFObject(L, pj->job_copy); + + Lua::SafeCall(out, L, 2, 0); } } @@ -1114,6 +995,15 @@ static bool itemInRealJob(df::item *item) != job_type_class::Hauling; } +static bool isRouteVehicle(df::item *item) +{ + int id = item->getVehicleID(); + if (id < 0) return false; + + auto vehicle = df::vehicle::find(id); + return vehicle && vehicle->route_id >= 0; +} + static void map_job_items(color_ostream &out) { for (size_t i = 0; i < constraints.size(); i++) @@ -1137,7 +1027,7 @@ static void map_job_items(color_ostream &out) bool dry_buckets = isOptionEnabled(CF_DRYBUCKETS); - std::vector &items = world->items.other[items_other_id::ANY_FREE]; + std::vector &items = world->items.other[items_other_id::IN_PLAY]; for (size_t i = 0; i < items.size(); i++) { @@ -1165,6 +1055,8 @@ static void map_job_items(color_ostream &out) break; case item_type::THREAD: + if (item->flags.bits.spider_web) + continue; if (item->getTotalDimension() < 15000) is_invalid = true; break; @@ -1223,6 +1115,7 @@ static void map_job_items(color_ostream &out) item->flags.bits.owned || item->flags.bits.in_chest || item->isAssignedToStockpile() || + isRouteVehicle(item) || itemInRealJob(item) || itemBusy(item)) { @@ -1351,6 +1244,160 @@ static void process_constraints(color_ostream &out) update_jobs_by_constraints(out); } +static void update_data_structures(color_ostream &out) +{ + if (enabled) { + check_lost_jobs(out, 0); + recover_jobs(out); + update_job_data(out); + map_job_constraints(out); + map_job_items(out); + } +} + +/************* + * LUA API * + *************/ + +static bool isEnabled() { return enabled; } + +static void setEnabled(color_ostream &out, bool enable) +{ + if (enable && !enabled) + { + enable_plugin(out); + } + else if (!enable && enabled) + { + enabled = false; + setOptionEnabled(CF_ENABLED, false); + stop_protect(out); + } +} + +static void push_constraint(lua_State *L, ItemConstraint *cv) +{ + lua_newtable(L); + int ctable = lua_gettop(L); + + Lua::SetField(L, cv->config.entry_id(), ctable, "id"); + Lua::SetField(L, cv->config.val(), ctable, "token"); + + // Constraint key + + Lua::SetField(L, cv->item.type, ctable, "item_type"); + Lua::SetField(L, cv->item.subtype, ctable, "item_subtype"); + + Lua::SetField(L, cv->is_craft, ctable, "is_craft"); + + Lua::PushDFObject(L, &cv->mat_mask); + lua_setfield(L, -2, "mat_mask"); + + Lua::SetField(L, cv->material.type, ctable, "mat_type"); + Lua::SetField(L, cv->material.index, ctable, "mat_index"); + + Lua::SetField(L, (int)cv->min_quality, ctable, "min_quality"); + + // Constraint value + + Lua::SetField(L, cv->goalByCount(), ctable, "goal_by_count"); + Lua::SetField(L, cv->goalCount(), ctable, "goal_value"); + Lua::SetField(L, cv->goalGap(), ctable, "goal_gap"); + + Lua::SetField(L, cv->item_amount, ctable, "cur_amount"); + Lua::SetField(L, cv->item_count, ctable, "cur_count"); + Lua::SetField(L, cv->item_inuse, ctable, "cur_in_use"); + + // Current state value + + if (cv->request_resume) + Lua::SetField(L, "resume", ctable, "request"); + else if (cv->request_suspend) + Lua::SetField(L, "suspend", ctable, "request"); + + lua_newtable(L); + + for (size_t i = 0, j = 0; i < cv->jobs.size(); i++) + { + if (!cv->jobs[i]->isLive()) continue; + Lua::PushDFObject(L, cv->jobs[i]->actual_job); + lua_rawseti(L, -2, ++j); + } + + lua_setfield(L, ctable, "jobs"); +} + +static int listConstraints(lua_State *L) +{ + auto job = Lua::CheckDFObject(L, 1); + + lua_pushnil(L); + + if (!enabled || (job && !isSupportedJob(job))) + return 1; + + color_ostream &out = *Lua::GetOutput(L); + update_data_structures(out); + + ProtectedJob *pj = NULL; + if (job) + { + pj = get_known(job->id); + if (!pj) + return 1; + } + + lua_newtable(L); + + auto &vec = (pj ? pj->constraints : constraints); + + for (size_t i = 0; i < vec.size(); i++) + { + push_constraint(L, vec[i]); + lua_rawseti(L, -2, i+1); + } + + return 1; +} + +static int setConstraint(lua_State *L) +{ + auto token = luaL_checkstring(L, 1); + bool by_count = lua_toboolean(L, 2); + int count = luaL_optint(L, 3, -1); + int gap = luaL_optint(L, 4, -1); + + color_ostream &out = *Lua::GetOutput(L); + + ItemConstraint *icv = get_constraint(out, token); + if (!icv) + luaL_error(L, "invalid constraint: %s", token); + + if (!lua_isnil(L, 2)) + icv->setGoalByCount(by_count); + if (!lua_isnil(L, 3)) + icv->setGoalCount(count); + if (!lua_isnil(L, 4)) + icv->setGoalGap(gap); + + process_constraints(out); + push_constraint(L, icv); + return 1; +} + +DFHACK_PLUGIN_LUA_FUNCTIONS { + DFHACK_LUA_FUNCTION(isEnabled), + DFHACK_LUA_FUNCTION(setEnabled), + DFHACK_LUA_FUNCTION(deleteConstraint), + DFHACK_LUA_END +}; + +DFHACK_PLUGIN_LUA_COMMANDS { + DFHACK_LUA_COMMAND(listConstraints), + DFHACK_LUA_COMMAND(setConstraint), + DFHACK_LUA_END +}; + /****************************** * PRINTING AND THE COMMAND * ******************************/ @@ -1494,13 +1541,7 @@ static command_result workflow_cmd(color_ostream &out, vector & paramet return CR_FAILURE; } - if (enabled) { - check_lost_jobs(out, 0); - recover_jobs(out); - update_job_data(out); - map_job_constraints(out); - map_job_items(out); - } + update_data_structures(out); df::building *workshop = NULL; //FIXME: unused variable! @@ -1518,18 +1559,11 @@ static command_result workflow_cmd(color_ostream &out, vector & paramet if (cmd == "enable" || cmd == "disable") { bool enable = (cmd == "enable"); - if (enable && !enabled) - { - enable_plugin(out); - } - else if (!enable && parameters.size() == 1) + if (enable) + setEnabled(out, true); + else if (parameters.size() == 1) { - if (enabled) - { - enabled = false; - setOptionEnabled(CF_ENABLED, false); - stop_protect(out); - } + setEnabled(out, false); out << "The plugin is disabled." << endl; return CR_OK; @@ -1647,14 +1681,8 @@ static command_result workflow_cmd(color_ostream &out, vector & paramet if (parameters.size() != 2) return CR_WRONG_USAGE; - for (size_t i = 0; i < constraints.size(); i++) - { - if (constraints[i]->config.val() != parameters[1]) - continue; - - delete_constraint(constraints[i]); + if (deleteConstraint(parameters[1])) return CR_OK; - } out.printerr("Constraint not found: %s\n", parameters[1].c_str()); return CR_FAILURE; diff --git a/plugins/zone.cpp b/plugins/zone.cpp index c496f49b6..2c63b6af7 100644 --- a/plugins/zone.cpp +++ b/plugins/zone.cpp @@ -2769,10 +2769,7 @@ public: if(!rconfig.isValid()) { string keyname = "autobutcher/watchlist/" + getRaceName(raceId); - auto pworld = Core::getInstance().getWorld(); - rconfig = pworld->GetPersistentData(keyname); - if(!rconfig.isValid()) - rconfig = pworld->AddPersistentData(keyname); + rconfig = World::GetPersistentData(keyname, NULL); } if(rconfig.isValid()) { @@ -2795,7 +2792,7 @@ public: { if(!rconfig.isValid()) return; - Core::getInstance().getWorld()->DeletePersistentData(rconfig); + World::DeletePersistentData(rconfig); } void SortUnitsByAge() @@ -3405,13 +3402,18 @@ command_result autoButcher( color_ostream &out, bool verbose = false ) command_result start_autobutcher(color_ostream &out) { - auto pworld = Core::getInstance().getWorld(); - enable_autobutcher = true; + if (!config_autobutcher.isValid()) { - config_autobutcher = pworld->AddPersistentData("autobutcher/config"); - config_autobutcher.ival(0) = enable_autobutcher; + config_autobutcher = World::AddPersistentData("autobutcher/config"); + + if (!config_autobutcher.isValid()) + { + out << "Cannot enable autobutcher without a world!" << endl; + return CR_OK; + } + config_autobutcher.ival(1) = sleep_autobutcher; config_autobutcher.ival(2) = enable_autobutcher_autowatch; config_autobutcher.ival(3) = default_fk; @@ -3420,6 +3422,8 @@ command_result start_autobutcher(color_ostream &out) config_autobutcher.ival(6) = default_ma; } + config_autobutcher.ival(0) = enable_autobutcher; + out << "Starting autobutcher." << endl; init_autobutcher(out); return CR_OK; @@ -3428,14 +3432,8 @@ command_result start_autobutcher(color_ostream &out) command_result init_autobutcher(color_ostream &out) { cleanup_autobutcher(out); - auto pworld = Core::getInstance().getWorld(); - if(!pworld) - { - out << "Autobutcher has no world to read from!" << endl; - return CR_OK; - } - config_autobutcher = pworld->GetPersistentData("autobutcher/config"); + config_autobutcher = World::GetPersistentData("autobutcher/config"); if(config_autobutcher.isValid()) { if (config_autobutcher.ival(0) == -1) @@ -3467,7 +3465,7 @@ command_result init_autobutcher(color_ostream &out) // read watchlist from save std::vector items; - pworld->GetPersistentData(&items, "autobutcher/watchlist/", true); + World::GetPersistentData(&items, "autobutcher/watchlist/", true); for (auto p = items.begin(); p != items.end(); p++) { string key = p->key(); @@ -3498,14 +3496,23 @@ command_result cleanup_autobutcher(color_ostream &out) command_result start_autonestbox(color_ostream &out) { - auto pworld = Core::getInstance().getWorld(); enable_autonestbox = true; - if (!config_autobutcher.isValid()) + + if (!config_autonestbox.isValid()) { - config_autonestbox = pworld->AddPersistentData("autonestbox/config"); - config_autonestbox.ival(0) = enable_autonestbox; + config_autonestbox = World::AddPersistentData("autonestbox/config"); + + if (!config_autonestbox.isValid()) + { + out << "Cannot enable autonestbox without a world!" << endl; + return CR_OK; + } + config_autonestbox.ival(1) = sleep_autonestbox; } + + config_autonestbox.ival(0) = enable_autonestbox; + out << "Starting autonestbox." << endl; init_autonestbox(out); return CR_OK; @@ -3514,14 +3521,8 @@ command_result start_autonestbox(color_ostream &out) command_result init_autonestbox(color_ostream &out) { cleanup_autonestbox(out); - auto pworld = Core::getInstance().getWorld(); - if(!pworld) - { - out << "Autonestbox has no world to read from!" << endl; - return CR_OK; - } - config_autonestbox = pworld->GetPersistentData("autonestbox/config"); + config_autonestbox = World::GetPersistentData("autonestbox/config"); if(config_autonestbox.isValid()) { if (config_autonestbox.ival(0) == -1) diff --git a/scripts/deathcause.rb b/scripts/deathcause.rb new file mode 100644 index 000000000..0ed54d81a --- /dev/null +++ b/scripts/deathcause.rb @@ -0,0 +1,36 @@ +# show death cause of a creature + +def display_death_event(e) + str = "The #{e.victim_hf_tg.race_tg.name[0]} #{e.victim_hf_tg.name} died in year #{e.year}" + str << " (cause: #{e.death_cause.to_s.downcase})," + str << " killed by the #{e.slayer_race_tg.name[0]} #{e.slayer_hf_tg.name}" if e.slayer_hf != -1 + str << " using a #{df.world.raws.itemdefs.weapons[e.weapon.item_subtype].name}" if e.weapon.item_type == :WEAPON + str << ", shot by a #{df.world.raws.itemdefs.weapons[e.weapon.bow_item_subtype].name}" if e.weapon.bow_item_type == :WEAPON + + puts str.chomp(',') + '.' +end + +item = df.item_find(:selected) + +if !item or !item.kind_of?(DFHack::ItemBodyComponent) + item = df.world.items.other[:ANY_CORPSE].find { |i| df.at_cursor?(i) } +end + +if !item or !item.kind_of?(DFHack::ItemBodyComponent) + puts "Please select a corpse in the loo'k' menu" +else + hf = item.hist_figure_id + if hf == -1 + # TODO try to retrieve info from the unit (u = item.unit_tg) + puts "Not a historical figure, cannot death find info" + else + events = df.world.history.events + (0...events.length).reverse_each { |i| + if events[i].kind_of?(DFHack::HistoryEventHistFigureDiedst) and events[i].victim_hf == hf + display_death_event(events[i]) + break + end + } + end +end + diff --git a/scripts/devel/find-offsets.lua b/scripts/devel/find-offsets.lua index 6fc127351..2f7f1287c 100644 --- a/scripts/devel/find-offsets.lua +++ b/scripts/devel/find-offsets.lua @@ -2,6 +2,7 @@ local utils = require 'utils' local ms = require 'memscan' +local gui = require 'gui' local is_known = dfhack.internal.getAddress @@ -53,6 +54,35 @@ end local searcher = ms.DiffSearcher.new(data) +local function get_screen(class, prompt) + if not is_known('gview') then + print('Please navigate to '..prompt) + if not utils.prompt_yes_no('Proceed?', true) then + return nil + end + return true + end + + while true do + local cs = dfhack.gui.getCurViewscreen(true) + if not df.is_instance(class, cs) then + print('Please navigate to '..prompt) + if not utils.prompt_yes_no('Proceed?', true) then + return nil + end + else + return cs + end + end +end + +local function screen_title() + return get_screen(df.viewscreen_titlest, 'the title screen') +end +local function screen_dwarfmode() + return get_screen(df.viewscreen_dwarfmodest, 'the main dwarf mode screen') +end + local function validate_offset(name,validator,addr,tname,...) local obj = data:object_by_field(addr,tname,...) if obj and not validator(obj) then @@ -136,13 +166,134 @@ local function list_index_choices(length_func) end end +local function can_feed() + return not force_scan['nofeed'] and is_known 'gview' +end + +local function dwarfmode_feed_input(...) + local screen = screen_dwarfmode() + if not df.isvalid(screen) then + qerror('could not retrieve dwarfmode screen') + end + for _,v in ipairs({...}) do + gui.simulateInput(screen, v) + end +end + +local function dwarfmode_step_frames(count) + local screen = screen_dwarfmode() + if not df.isvalid(screen) then + qerror('could not retrieve dwarfmode screen') + end + + for i = 1,(count or 1) do + gui.simulateInput(screen, 'D_ONESTEP') + if screen.keyRepeat ~= 1 then + qerror('Could not step one frame: . did not work') + end + screen:logic() + end +end + +local function dwarfmode_to_top() + if not can_feed() then + return false + end + + local screen = screen_dwarfmode() + if not df.isvalid(screen) then + return false + end + + for i=0,10 do + if is_known 'ui' and df.global.ui.main.mode == df.ui_sidebar_mode.Default then + break + end + gui.simulateInput(screen, 'LEAVESCREEN') + end + + -- force pause just in case + screen.keyRepeat = 1 + return true +end + +local function feed_menu_choice(catnames,catkeys,enum) + return function (idx) + idx = idx % #catnames + 1 + dwarfmode_feed_input(catkeys[idx]) + if enum then + return true, enum[catnames[idx]] + else + return true, catnames[idx] + end + end +end + +local function feed_list_choice(count,upkey,downkey) + return function(idx) + if idx > 0 then + local ok, len + if type(count) == 'number' then + ok, len = true, count + else + ok, len = pcall(count) + end + if not ok then + len = 5 + elseif len > 10 then + len = 10 + end + + local hcnt = len-1 + local rix = 1 + (idx-1) % (hcnt*2) + + if rix >= hcnt then + dwarfmode_feed_input(upkey or 'SECONDSCROLL_UP') + return true, hcnt*2 - rix + else + dwarfmode_feed_input(donwkey or 'SECONDSCROLL_DOWN') + return true, rix + end + else + print(' Please select the first list item.') + if not utils.prompt_yes_no(' Proceed?', true) then + return false + end + return true, 0 + end + end +end + +local function feed_menu_bool(enter_seq, exit_seq) + return function(idx) + if idx == 0 then + if not utils.prompt_yes_no(' Proceed?', true) then + return false + end + return true, 0 + end + if idx == 5 then + print(' Please resize the game window.') + if not utils.prompt_yes_no(' Proceed?', true) then + return false + end + end + if idx%2 == 1 then + dwarfmode_feed_input(table.unpack(enter_seq)) + return true, 1 + else + dwarfmode_feed_input(table.unpack(exit_seq)) + return true, 0 + end + end +end + -- -- Cursor group -- local function find_cursor() - print('\nPlease navigate to the title screen to find cursor.') - if not utils.prompt_yes_no('Proceed?', true) then + if not screen_title() then return false end @@ -354,13 +505,35 @@ local function is_valid_world(world) end local function find_world() - local addr = searcher:find_menu_cursor([[ + local catnames = { + 'Corpses', 'Refuse', 'Stone', 'Wood', 'Gems', 'Bars', 'Cloth', 'Leather', 'Ammo', 'Coins' + } + local catkeys = { + 'STOCKPILE_GRAVEYARD', 'STOCKPILE_REFUSE', 'STOCKPILE_STONE', 'STOCKPILE_WOOD', + 'STOCKPILE_GEM', 'STOCKPILE_BARBLOCK', 'STOCKPILE_CLOTH', 'STOCKPILE_LEATHER', + 'STOCKPILE_AMMO', 'STOCKPILE_COINS' + } + local addr + + if dwarfmode_to_top() then + dwarfmode_feed_input('D_STOCKPILES') + + addr = searcher:find_interactive( + 'Auto-searching for world.', + 'int32_t', + feed_menu_choice(catnames, catkeys, df.stockpile_category), + 20 + ) + end + + if not addr then + addr = searcher:find_menu_cursor([[ Searching for world. Please open the stockpile creation menu, and select different types as instructed below:]], - 'int32_t', - { 'Corpses', 'Refuse', 'Stone', 'Wood', 'Gems', 'Bars', 'Cloth', 'Leather', 'Ammo', 'Coins' }, - df.stockpile_category - ) + 'int32_t', catnames, df.stockpile_category + ) + end + validate_offset('world', is_valid_world, addr, df.world, 'selected_stockpile_type') end @@ -385,14 +558,37 @@ local function is_valid_ui(ui) end local function find_ui() - local addr = searcher:find_menu_cursor([[ + local catnames = { + 'DesignateMine', 'DesignateChannel', 'DesignateRemoveRamps', + 'DesignateUpStair', 'DesignateDownStair', 'DesignateUpDownStair', + 'DesignateUpRamp', 'DesignateChopTrees' + } + local catkeys = { + 'DESIGNATE_DIG', 'DESIGNATE_CHANNEL', 'DESIGNATE_DIG_REMOVE_STAIRS_RAMPS', + 'DESIGNATE_STAIR_UP', 'DESIGNATE_STAIR_DOWN', 'DESIGNATE_STAIR_UPDOWN', + 'DESIGNATE_RAMP', 'DESIGNATE_CHOP' + } + local addr + + if dwarfmode_to_top() then + dwarfmode_feed_input('D_DESIGNATE') + + addr = searcher:find_interactive( + 'Auto-searching for ui.', + 'int16_t', + feed_menu_choice(catnames, catkeys, df.ui_sidebar_mode), + 20 + ) + end + + if not addr then + addr = searcher:find_menu_cursor([[ Searching for ui. Please open the designation menu, and switch modes as instructed below:]], - 'int16_t', - { 'DesignateMine', 'DesignateChannel', 'DesignateRemoveRamps', 'DesignateUpStair', - 'DesignateDownStair', 'DesignateUpDownStair', 'DesignateUpRamp', 'DesignateChopTrees' }, - df.ui_sidebar_mode - ) + 'int16_t', catnames, df.ui_sidebar_mode + ) + end + validate_offset('ui', is_valid_ui, addr, df.ui, 'main', 'mode') end @@ -421,14 +617,32 @@ local function is_valid_ui_sidebar_menus(usm) end local function find_ui_sidebar_menus() - local addr = searcher:find_menu_cursor([[ + local addr + + if dwarfmode_to_top() then + dwarfmode_feed_input('D_BUILDJOB') + + addr = searcher:find_interactive([[ +Auto-searching for ui_sidebar_menus. Please select a Mason, +Craftsdwarfs, or Carpenters workshop, open the Add Job +menu, and move the cursor within:]], + 'int32_t', + feed_list_choice(7), + 20 + ) + end + + if not addr then + addr = searcher:find_menu_cursor([[ Searching for ui_sidebar_menus. Please switch to 'q' mode, select a Mason, Craftsdwarfs, or Carpenters workshop, open the Add Job menu, and move the cursor within:]], - 'int32_t', - { 0, 1, 2, 3, 4, 5, 6 }, - ordinal_names - ) + 'int32_t', + { 0, 1, 2, 3, 4, 5, 6 }, + ordinal_names + ) + end + validate_offset('ui_sidebar_menus', is_valid_ui_sidebar_menus, addr, df.ui_sidebar_menus, 'workshop_job', 'cursor') end @@ -455,14 +669,41 @@ local function is_valid_ui_build_selector(ubs) end local function find_ui_build_selector() - local addr = searcher:find_menu_cursor([[ + local addr + + if dwarfmode_to_top() then + addr = searcher:find_interactive([[ +Auto-searching for ui_build_selector. This requires mechanisms.]], + 'int32_t', + function(idx) + if idx == 0 then + dwarfmode_to_top() + dwarfmode_feed_input( + 'D_BUILDING', + 'HOTKEY_BUILDING_TRAP', + 'HOTKEY_BUILDING_TRAP_TRIGGER', + 'BUILDING_TRIGGER_ENABLE_CREATURE' + ) + else + dwarfmode_feed_input('BUILDING_TRIGGER_MIN_SIZE_DOWN') + end + return true, 50000 - 1000*idx + end, + 20 + ) + end + + if not addr then + addr = searcher:find_menu_cursor([[ Searching for ui_build_selector. Please start constructing a pressure plate, and enable creatures. Then change the min weight as requested, remembering that the ui truncates the number, so when it shows "Min (5000df", it means 50000:]], - 'int32_t', - { 50000, 49000, 48000, 47000, 46000, 45000, 44000 } - ) + 'int32_t', + { 50000, 49000, 48000, 47000, 46000, 45000, 44000 } + ) + end + validate_offset('ui_build_selector', is_valid_ui_build_selector, addr, df.ui_build_selector, 'plate_info', 'unit_min') end @@ -601,13 +842,33 @@ end -- local function find_ui_unit_view_mode() - local addr = searcher:find_menu_cursor([[ + local catnames = { 'General', 'Inventory', 'Preferences', 'Wounds' } + local catkeys = { 'UNITVIEW_GEN', 'UNITVIEW_INV', 'UNITVIEW_PRF', 'UNITVIEW_WND' } + local addr + + if dwarfmode_to_top() and is_known('ui_selected_unit') then + dwarfmode_feed_input('D_VIEWUNIT') + + if df.global.ui_selected_unit < 0 then + df.global.ui_selected_unit = 0 + end + + addr = searcher:find_interactive( + 'Auto-searching for ui_unit_view_mode.', + 'int32_t', + feed_menu_choice(catnames, catkeys, df.ui_unit_view_mode.T_value), + 10 + ) + end + + if not addr then + addr = searcher:find_menu_cursor([[ Searching for ui_unit_view_mode. Having selected a unit with 'v', switch the pages as requested:]], - 'int32_t', - { 'General', 'Inventory', 'Preferences', 'Wounds' }, - df.ui_unit_view_mode.T_value - ) + 'int32_t', catnames, df.ui_unit_view_mode.T_value + ) + end + ms.found_offset('ui_unit_view_mode', addr) end @@ -620,14 +881,32 @@ local function look_item_list_count() end local function find_ui_look_cursor() - local addr = searcher:find_menu_cursor([[ + local addr + + if dwarfmode_to_top() then + dwarfmode_feed_input('D_LOOK') + + addr = searcher:find_interactive([[ +Auto-searching for ui_look_cursor. Please select a tile +with at least 5 items or units on the ground, and move +the cursor as instructed:]], + 'int32_t', + feed_list_choice(look_item_list_count), + 20 + ) + end + + if not addr then + addr = searcher:find_menu_cursor([[ Searching for ui_look_cursor. Please activate the 'k' mode, find a tile with many items or units on the ground, and select list entries as instructed:]], - 'int32_t', - list_index_choices(look_item_list_count), - ordinal_names - ) + 'int32_t', + list_index_choices(look_item_list_count), + ordinal_names + ) + end + ms.found_offset('ui_look_cursor', addr) end @@ -640,14 +919,32 @@ local function building_item_list_count() end local function find_ui_building_item_cursor() - local addr = searcher:find_menu_cursor([[ + local addr + + if dwarfmode_to_top() then + dwarfmode_feed_input('D_BUILDITEM') + + addr = searcher:find_interactive([[ +Auto-searching for ui_building_item_cursor. Please highlight a +workshop, trade depot or other building with at least 5 contained +items, and select as instructed:]], + 'int32_t', + feed_list_choice(building_item_list_count), + 20 + ) + end + + if not addr then + addr = searcher:find_menu_cursor([[ Searching for ui_building_item_cursor. Please activate the 't' mode, find a cluttered workshop, trade depot, or other building with many contained items, and select as instructed:]], - 'int32_t', - list_index_choices(building_item_list_count), - ordinal_names - ) + 'int32_t', + list_index_choices(building_item_list_count), + ordinal_names + ) + end + ms.found_offset('ui_building_item_cursor', addr) end @@ -656,17 +953,37 @@ end -- local function find_ui_workshop_in_add() - local addr = searcher:find_menu_cursor([[ + local addr + + if dwarfmode_to_top() then + dwarfmode_feed_input('D_BUILDJOB') + + addr = searcher:find_interactive([[ +Auto-searching for ui_workshop_in_add. Please select a +workshop, e.g. Carpenters or Masons.]], + 'int8_t', + feed_menu_bool( + { 'BUILDJOB_CANCEL', 'BUILDJOB_ADD' }, + { 'SELECT', 'SELECT', 'SELECT', 'SELECT', 'SELECT' } + ), + 20 + ) + end + + if not addr then + addr = searcher:find_menu_cursor([[ Searching for ui_workshop_in_add. Please activate the 'q' mode, find a workshop without jobs (or delete jobs), and do as instructed below. NOTE: If not done after first 3-4 steps, resize the game window.]], - 'int8_t', - { 1, 0 }, - { [1] = 'enter the add job menu', - [0] = 'add job, thus exiting the menu' } - ) + 'int8_t', + { 1, 0 }, + { [1] = 'enter the add job menu', + [0] = 'add job, thus exiting the menu' } + ) + end + ms.found_offset('ui_workshop_in_add', addr) end @@ -679,13 +996,30 @@ local function workshop_job_list_count() end local function find_ui_workshop_job_cursor() - local addr = searcher:find_menu_cursor([[ + local addr + + if dwarfmode_to_top() then + dwarfmode_feed_input('D_BUILDJOB') + + addr = searcher:find_interactive([[ +Auto-searching for ui_workshop_job_cursor. Please highlight a +workshop with at least 5 contained jobs, and select as instructed:]], + 'int32_t', + feed_list_choice(workshop_job_list_count), + 20 + ) + end + + if not addr then + addr = searcher:find_menu_cursor([[ Searching for ui_workshop_job_cursor. Please activate the 'q' mode, find a workshop with many jobs, and select as instructed:]], - 'int32_t', - list_index_choices(workshop_job_list_count), - ordinal_names - ) + 'int32_t', + list_index_choices(workshop_job_list_count), + ordinal_names + ) + end + ms.found_offset('ui_workshop_job_cursor', addr) end @@ -694,17 +1028,39 @@ end -- local function find_ui_building_in_assign() - local addr = searcher:find_menu_cursor([[ + local addr + + if dwarfmode_to_top() then + dwarfmode_feed_input('D_BUILDJOB') + + addr = searcher:find_interactive([[ +Auto-searching for ui_building_in_assign. Please select a room, +i.e. a bedroom, tomb, office, dining room or statue garden.]], + 'int8_t', + feed_menu_bool( + { { 'BUILDJOB_STATUE_ASSIGN', 'BUILDJOB_COFFIN_ASSIGN', + 'BUILDJOB_CHAIR_ASSIGN', 'BUILDJOB_TABLE_ASSIGN', + 'BUILDJOB_BED_ASSIGN' } }, + { 'LEAVESCREEN' } + ), + 20 + ) + end + + if not addr then + addr = searcher:find_menu_cursor([[ Searching for ui_building_in_assign. Please activate the 'q' mode, select a room building (e.g. a bedroom) and do as instructed below. NOTE: If not done after first 3-4 steps, resize the game window.]], - 'int8_t', - { 1, 0 }, - { [1] = 'enter the Assign owner menu', - [0] = 'press Esc to exit assign' } - ) + 'int8_t', + { 1, 0 }, + { [1] = 'enter the Assign owner menu', + [0] = 'press Esc to exit assign' } + ) + end + ms.found_offset('ui_building_in_assign', addr) end @@ -713,17 +1069,39 @@ end -- local function find_ui_building_in_resize() - local addr = searcher:find_menu_cursor([[ + local addr + + if dwarfmode_to_top() then + dwarfmode_feed_input('D_BUILDJOB') + + addr = searcher:find_interactive([[ +Auto-searching for ui_building_in_resize. Please select a room, +i.e. a bedroom, tomb, office, dining room or statue garden.]], + 'int8_t', + feed_menu_bool( + { { 'BUILDJOB_STATUE_SIZE', 'BUILDJOB_COFFIN_SIZE', + 'BUILDJOB_CHAIR_SIZE', 'BUILDJOB_TABLE_SIZE', + 'BUILDJOB_BED_SIZE' } }, + { 'LEAVESCREEN' } + ), + 20 + ) + end + + if not addr then + addr = searcher:find_menu_cursor([[ Searching for ui_building_in_resize. Please activate the 'q' mode, select a room building (e.g. a bedroom) and do as instructed below. NOTE: If not done after first 3-4 steps, resize the game window.]], - 'int8_t', - { 1, 0 }, - { [1] = 'enter the Resize room mode', - [0] = 'press Esc to exit resize' } - ) + 'int8_t', + { 1, 0 }, + { [1] = 'enter the Resize room mode', + [0] = 'press Esc to exit resize' } + ) + end + ms.found_offset('ui_building_in_resize', addr) end @@ -731,13 +1109,40 @@ end -- window_x -- +local function feed_window_xyz(dec,inc,step) + return function(idx) + if idx == 0 then + for i = 1,30 do dwarfmode_feed_input(dec) end + else + dwarfmode_feed_input(inc) + end + return true, nil, step + end +end + local function find_window_x() - local addr = searcher:find_counter([[ + local addr + + if dwarfmode_to_top() then + addr = searcher:find_interactive( + 'Auto-searching for window_x.', + 'int32_t', + feed_window_xyz('CURSOR_LEFT_FAST', 'CURSOR_RIGHT', 10), + 20 + ) + + dwarfmode_feed_input('D_HOTKEY1') + end + + if not addr then + addr = searcher:find_counter([[ Searching for window_x. Please exit to main dwarfmode menu, scroll to the LEFT edge, then do as instructed:]], - 'int32_t', 10, - 'Please press Right to scroll right one step.' - ) + 'int32_t', 10, + 'Please press Right to scroll right one step.' + ) + end + ms.found_offset('window_x', addr) end @@ -746,12 +1151,28 @@ end -- local function find_window_y() - local addr = searcher:find_counter([[ + local addr + + if dwarfmode_to_top() then + addr = searcher:find_interactive( + 'Auto-searching for window_y.', + 'int32_t', + feed_window_xyz('CURSOR_UP_FAST', 'CURSOR_DOWN', 10), + 20 + ) + + dwarfmode_feed_input('D_HOTKEY1') + end + + if not addr then + addr = searcher:find_counter([[ Searching for window_y. Please exit to main dwarfmode menu, scroll to the TOP edge, then do as instructed:]], - 'int32_t', 10, - 'Please press Down to scroll down one step.' - ) + 'int32_t', 10, + 'Please press Down to scroll down one step.' + ) + end + ms.found_offset('window_y', addr) end @@ -760,14 +1181,30 @@ end -- local function find_window_z() - local addr = searcher:find_counter([[ + local addr + + if dwarfmode_to_top() then + addr = searcher:find_interactive( + 'Auto-searching for window_z.', + 'int32_t', + feed_window_xyz('CURSOR_UP_Z', 'CURSOR_DOWN_Z', -1), + 30 + ) + + dwarfmode_feed_input('D_HOTKEY1') + end + + if not addr then + addr = searcher:find_counter([[ Searching for window_z. Please exit to main dwarfmode menu, scroll to a Z level near surface, then do as instructed below. NOTE: If not done after first 3-4 steps, resize the game window.]], - 'int32_t', -1, - "Please press '>' to scroll one Z level down." - ) + 'int32_t', -1, + "Please press '>' to scroll one Z level down." + ) + end + ms.found_offset('window_z', addr) end @@ -803,6 +1240,40 @@ end -- cur_year_tick -- +function stop_autosave() + if is_known 'd_init' then + local f = df.global.d_init.flags4 + if f.AUTOSAVE_SEASONAL or f.AUTOSAVE_YEARLY then + f.AUTOSAVE_SEASONAL = false + f.AUTOSAVE_YEARLY = false + print('Disabled seasonal and yearly autosave.') + end + else + dfhack.printerr('Could not disable autosave!') + end +end + +function step_n_frames(cnt, feed) + local world = df.global.world + local ctick = world.frame_counter + + if feed then + print(" Auto-stepping "..cnt.." frames.") + dwarfmode_step_frames(cnt) + return world.frame_counter-ctick + end + + local more = '' + while world.frame_counter-ctick < cnt do + print(" Please step the game "..(cnt-world.frame_counter+ctick)..more.." frames.") + more = ' more' + if not utils.prompt_yes_no(' Proceed?', true) then + return nil + end + end + return world.frame_counter-ctick +end + local function find_cur_year_tick() local zone if os_type == 'windows' then @@ -815,15 +1286,87 @@ local function find_cur_year_tick() return end - local addr = zone:find_counter([[ -Searching for cur_year_tick. Please exit to main dwarfmode -menu, then do as instructed below:]], - 'int32_t', 1, - "Please press '.' to step the game one frame." + stop_autosave() + + local feed = dwarfmode_to_top() + local addr = zone:find_interactive( + 'Searching for cur_year_tick.', + 'int32_t', + function(idx) + if idx > 0 then + if not step_n_frames(1, feed) then + return false + end + end + return true, nil, 1 + end, + 20 ) + ms.found_offset('cur_year_tick', addr) end +-- +-- cur_season_tick +-- + +local function find_cur_season_tick() + if not (is_known 'cur_year_tick') then + dfhack.printerr('Cannot search for cur_season_tick - prerequisites missing.') + return + end + + stop_autosave() + + local feed = dwarfmode_to_top() + local addr = searcher:find_interactive([[ +Searching for cur_season_tick. Please exit to main dwarfmode +menu, then do as instructed below:]], + 'int32_t', + function(ccursor) + if ccursor > 0 then + if not step_n_frames(10, feed) then + return false + end + end + return true, math.floor(((df.global.cur_year_tick+10)%100800)/10) + end + ) + ms.found_offset('cur_season_tick', addr) +end + +-- +-- cur_season +-- + +local function find_cur_season() + if not (is_known 'cur_year_tick' and is_known 'cur_season_tick') then + dfhack.printerr('Cannot search for cur_season - prerequisites missing.') + return + end + + stop_autosave() + + local feed = dwarfmode_to_top() + local addr = searcher:find_interactive([[ +Searching for cur_season. Please exit to main dwarfmode +menu, then do as instructed below:]], + 'int8_t', + function(ccursor) + if ccursor > 0 then + local cst = df.global.cur_season_tick + df.global.cur_season_tick = 10079 + df.global.cur_year_tick = df.global.cur_year_tick + (10079-cst)*10 + if not step_n_frames(10, feed) then + return false + end + end + return true, math.floor((df.global.cur_year_tick+10)/100800)%4 + end + ) + ms.found_offset('cur_season', addr) +end + -- -- process_jobs -- @@ -839,6 +1382,8 @@ end local function find_process_jobs() local zone = get_process_zone() or searcher + stop_autosave() + local addr = zone:find_menu_cursor([[ Searching for process_jobs. Please do as instructed below:]], 'int8_t', @@ -856,6 +1401,8 @@ end local function find_process_dig() local zone = get_process_zone() or searcher + stop_autosave() + local addr = zone:find_menu_cursor([[ Searching for process_dig. Please do as instructed below:]], 'int8_t', @@ -871,7 +1418,7 @@ end -- local function find_pause_state() - local zone + local zone, addr if os_type == 'linux' or os_type == 'darwin' then zone = zoomed_searcher('ui_look_cursor', 32) elseif os_type == 'windows' then @@ -879,13 +1426,35 @@ local function find_pause_state() end zone = zone or searcher - local addr = zone:find_menu_cursor([[ + stop_autosave() + + if dwarfmode_to_top() then + addr = zone:find_interactive( + 'Auto-searching for pause_state', + 'int8_t', + function(idx) + if idx%2 == 0 then + dwarfmode_feed_input('D_ONESTEP') + return true, 0 + else + screen_dwarfmode():logic() + return true, 1 + end + end, + 20 + ) + end + + if not addr then + addr = zone:find_menu_cursor([[ Searching for pause_state. Please do as instructed below:]], - 'int8_t', - { 1, 0 }, - { [1] = 'PAUSE the game', - [0] = 'UNPAUSE the game' } - ) + 'int8_t', + { 1, 0 }, + { [1] = 'PAUSE the game', + [0] = 'UNPAUSE the game' } + ) + end + ms.found_offset('pause_state', addr) end @@ -895,10 +1464,10 @@ end print('\nInitial globals (need title screen):\n') +exec_finder(find_gview, 'gview') exec_finder(find_cursor, { 'cursor', 'selection_rect', 'gamemode', 'gametype' }) exec_finder(find_announcements, 'announcements') exec_finder(find_d_init, 'd_init') -exec_finder(find_gview, 'gview') exec_finder(find_enabler, 'enabler') exec_finder(find_gps, 'gps') @@ -930,6 +1499,8 @@ print('\nUnpausing globals:\n') exec_finder(find_cur_year, 'cur_year') exec_finder(find_cur_year_tick, 'cur_year_tick') +exec_finder(find_cur_season_tick, 'cur_season_tick') +exec_finder(find_cur_season, 'cur_season') exec_finder(find_process_jobs, 'process_jobs') exec_finder(find_process_dig, 'process_dig') exec_finder(find_pause_state, 'pause_state') diff --git a/scripts/devel/inject-raws.lua b/scripts/devel/inject-raws.lua new file mode 100644 index 000000000..a4ebeec1d --- /dev/null +++ b/scripts/devel/inject-raws.lua @@ -0,0 +1,178 @@ +-- Injects new reaction, item and building defs into the world. + +-- The savegame contains a list of the relevant definition tokens in +-- the right order, but all details are read from raws every time. +-- This allows just adding stub definitions, and simply saving and +-- reloading the game. + +local utils = require 'utils' + +local raws = df.global.world.raws + +print[[ +WARNING: THIS SCRIPT CAN PERMANENLY DAMAGE YOUR SAVE. + +This script attempts to inject new raw objects into your +world. If the injected references do not match the actual +edited raws, your save will refuse to load, or load but crash. +]] + +if not utils.prompt_yes_no('Did you make a backup?') then + qerror('Not backed up.') +end + +df.global.pause_state = true + +local changed = false + +function inject_reaction(name) + for _,v in ipairs(raws.reactions) do + if v.code == name then + print('Reaction '..name..' already exists.') + return + end + end + + print('Injecting reaction '..name) + changed = true + + raws.reactions:insert('#', { + new = true, + code = name, + name = 'Dummy reaction '..name, + index = #raws.reactions, + }) +end + +local building_types = { + workshop = { df.building_def_workshopst, raws.buildings.workshops }, + furnace = { df.building_def_furnacest, raws.buildings.furnaces }, +} + +function inject_building(btype, name) + for _,v in ipairs(raws.buildings.all) do + if v.code == name then + print('Building '..name..' already exists.') + return + end + end + + print('Injecting building '..name) + changed = true + + local typeinfo = building_types[btype] + + local id = raws.buildings.next_id + raws.buildings.next_id = id+1 + + raws.buildings.all:insert('#', { + new = typeinfo[1], + code = name, + name = 'Dummy '..btype..' '..name, + id = id, + }) + + typeinfo[2]:insert('#', raws.buildings.all[#raws.buildings.all-1]) +end + +local itemdefs = raws.itemdefs +local item_types = { + weapon = { df.itemdef_weaponst, itemdefs.weapons, 'weapon_type' }, + trainweapon = { df.itemdef_weaponst, itemdefs.weapons, 'training_weapon_type' }, + pick = { df.itemdef_weaponst, itemdefs.weapons, 'digger_type' }, + trapcomp = { df.itemdef_trapcompst, itemdefs.trapcomps, 'trapcomp_type' }, + toy = { df.itemdef_toyst, itemdefs.toys, 'toy_type' }, + tool = { df.itemdef_toolst, itemdefs.tools, 'tool_type' }, + instrument = { df.itemdef_instrumentst, itemdefs.instruments, 'instrument_type' }, + armor = { df.itemdef_armorst, itemdefs.armor, 'armor_type' }, + ammo = { df.itemdef_ammost, itemdefs.ammo, 'ammo_type' }, + siegeammo = { df.itemdef_siegeammost, itemdefs.siege_ammo, 'siegeammo_type' }, + gloves = { df.itemdef_glovest, itemdefs.gloves, 'gloves_type' }, + shoes = { df.itemdef_shoest, itemdefs.shoes, 'shoes_type' }, + shield = { df.itemdef_shieldst, itemdefs.shields, 'shield_type' }, + helm = { df.itemdef_helmst, itemdefs.helms, 'helm_type' }, + pants = { df.itemdef_pantsst, itemdefs.pants, 'pants_type' }, + food = { df.itemdef_foodst, itemdefs.food }, +} + +function add_to_civ(entity, bvec, id) + for _,v in ipairs(entity.resources[bvec]) do + if v == id then + return + end + end + + entity.resources[bvec]:insert('#', id) +end + +function add_to_dwarf_civs(btype, id) + local typeinfo = item_types[btype] + if not typeinfo[3] then + print('Not adding to civs.') + end + + for _,entity in ipairs(df.global.world.entities.all) do + if entity.race == df.global.ui.race_id then + add_to_civ(entity, typeinfo[3], id) + end + end +end + +function inject_item(btype, name) + for _,v in ipairs(itemdefs.all) do + if v.id == name then + print('Itemdef '..name..' already exists.') + return + end + end + + print('Injecting item '..name) + changed = true + + local typeinfo = item_types[btype] + local vec = typeinfo[2] + local id = #vec + + vec:insert('#', { + new = typeinfo[1], + id = name, + subtype = id, + name = name, + name_plural = name, + }) + + itemdefs.all:insert('#', vec[id]) + + add_to_dwarf_civs(btype, id) +end + +local args = {...} +local mode = nil +local ops = {} + +for _,kv in ipairs(args) do + if mode and string.match(kv, '^[%u_]+$') then + table.insert(ops, curry(mode, kv)) + elseif kv == 'reaction' then + mode = inject_reaction + elseif building_types[kv] then + mode = curry(inject_building, kv) + elseif item_types[kv] then + mode = curry(inject_item, kv) + else + qerror('Invalid option: '..kv) + end +end + +if #ops > 0 then + print('') + for _,v in ipairs(ops) do + v() + end +end + +if changed then + print('\nNow without unpausing save and reload the game to re-read raws.') +else + print('\nNo changes made.') +end diff --git a/scripts/digfort.rb b/scripts/digfort.rb new file mode 100644 index 000000000..6d609f9af --- /dev/null +++ b/scripts/digfort.rb @@ -0,0 +1,38 @@ +# designate an area for digging according to a plan in csv format + +raise "usage: digfort " if not $script_args[0] +planfile = File.read($script_args[0]) + +if df.cursor.x == -30000 + raise "place the game cursor to the top-left corner of the design" +end + +tiles = planfile.lines.map { |l| + l.sub(/#.*/, '').split(';').map { |t| t.strip } +} + +x = x0 = df.cursor.x +y = df.cursor.y +z = df.cursor.z + +tiles.each { |line| + next if line.empty? or line == [''] + line.each { |tile| + t = df.map_tile_at(x, y, z) + s = t.shape_basic + case tile + when 'd'; t.dig(:Default) if s == :Wall + when 'u'; t.dig(:UpStair) if s == :Wall + when 'j'; t.dig(:DownStair) if s == :Wall or s == :Floor + when 'i'; t.dig(:UpDownStair) if s == :Wall + when 'h'; t.dig(:Channel) if s == :Wall or s == :Floor + when 'r'; t.dig(:Ramp) if s == :Wall + when 'x'; t.dig(:No) + end + x += 1 + } + x = x0 + y += 1 +} + +puts 'done' diff --git a/scripts/drainaquifer.rb b/scripts/drainaquifer.rb new file mode 100644 index 000000000..4e2ae4ff1 --- /dev/null +++ b/scripts/drainaquifer.rb @@ -0,0 +1,11 @@ +# remove all aquifers from the map + +count = 0 +df.each_map_block { |b| + if b.designation[0][0].water_table or b.designation[15][15].water_table + count += 1 + b.designation.each { |dx| dx.each { |dy| dy.water_table = false } } + end +} + +puts "cleared #{count} map blocks" diff --git a/scripts/gui/assign-rack.lua b/scripts/gui/assign-rack.lua new file mode 100644 index 000000000..d358dfff1 --- /dev/null +++ b/scripts/gui/assign-rack.lua @@ -0,0 +1,202 @@ +-- Assign weapon racks to squads. Requires patch from bug 1445. + +--[[ + + Required patches: + + v0.34.11 linux: http://pastebin.com/mt5EUgFZ + v0.34.11 windows: http://pastebin.com/09nRCybe + +]] + +local utils = require 'utils' +local gui = require 'gui' +local guidm = require 'gui.dwarfmode' +local widgets = require 'gui.widgets' +local dlg = require 'gui.dialogs' + +AssignRack = defclass(AssignRack, guidm.MenuOverlay) + +AssignRack.focus_path = 'assign-rack' + +AssignRack.ATTRS { + building = DEFAULT_NIL, + frame_inset = 1, + frame_background = COLOR_BLACK, +} + +function list_squads(building,squad_table,squad_list) + local sqlist = building:getSquads() + if not sqlist then + return + end + + for i,v in ipairs(sqlist) do + local obj = df.squad.find(v.squad_id) + if obj then + if not squad_table[v.squad_id] then + squad_table[v.squad_id] = { id = v.squad_id, obj = obj } + table.insert(squad_list, squad_table[v.squad_id]) + end + + -- Set specific use flags + for n,ok in pairs(v.mode) do + if ok then + squad_table[v.squad_id][n] = true + end + end + + -- Check if any use is possible + local btype = building:getType() + if btype == df.building_type.Bed then + if v.mode.sleep then + squad_table[v.squad_id].any = true + end + elseif btype == df.building.Weaponrack then + if v.mode.train or v.mode.indiv_eq then + squad_table[v.squad_id].any = true + end + else + if v.mode.indiv_eq then + squad_table[v.squad_id].any = true + end + end + end + end + + for i,v in ipairs(building.parents) do + list_squads(v, squad_table, squad_list) + end +end + +function filter_invalid(list, id) + for i=#list-1,0,-1 do + local bld = df.building.find(list[i]) + if not bld or bld:getSpecificSquad() ~= id then + list:erase(i) + end + end +end + +function AssignRack:init(args) + self.squad_table = {} + self.squad_list = {} + list_squads(self.building, self.squad_table, self.squad_list) + table.sort(self.squad_list, function(a,b) return a.id < b.id end) + + self.choices = {} + for i,v in ipairs(self.squad_list) do + if v.any and (v.train or v.indiv_eq) then + local name = v.obj.alias + if name == '' then + name = dfhack.TranslateName(v.obj.name, true) + end + + filter_invalid(v.obj.rack_combat, v.id) + filter_invalid(v.obj.rack_training, v.id) + + table.insert(self.choices, { + icon = self:callback('isSelected', v), + icon_pen = COLOR_LIGHTGREEN, + obj = v, + text = { + name, NEWLINE, ' ', + { text = function() + return string.format('%d combat, %d training', #v.obj.rack_combat, #v.obj.rack_training) + end } + } + }) + end + end + + self:addviews{ + widgets.Label{ + frame = { l = 0, t = 0 }, + text = { + 'Assign Weapon Rack' + } + }, + widgets.List{ + view_id = 'list', + frame = { t = 2, b = 2 }, + icon_width = 2, row_height = 2, + scroll_keys = widgets.SECONDSCROLL, + choices = self.choices, + on_submit = self:callback('onSubmit'), + }, + widgets.Label{ + frame = { l = 0, t = 2 }, + text_pen = COLOR_LIGHTRED, + text = 'No appropriate barracks\n\nNote: weapon racks use the\nIndividual equipment flag', + visible = (#self.choices == 0), + }, + widgets.Label{ + frame = { l = 0, b = 0 }, + text = { + { key = 'LEAVESCREEN', text = ': Back', + on_activate = self:callback('dismiss') } + } + }, + } +end + +function AssignRack:isSelected(info) + if self.building.specific_squad == info.id then + return '\xfb' + else + return nil + end +end + +function AssignRack:onSubmit(idx, choice) + local rid = self.building.id + local curid = self.building.specific_squad + + local cur = df.squad.find(curid) + if cur then + utils.erase_sorted(cur.rack_combat, rid) + utils.erase_sorted(cur.rack_training, rid) + end + + self.building.specific_squad = -1 + df.global.ui.equipment.update.buildings = true + + local new = df.squad.find(choice.obj.id) + if new and choice.obj.id ~= curid then + self.building.specific_squad = choice.obj.id + + if choice.obj.indiv_eq then + utils.insert_sorted(new.rack_combat, rid) + end + if choice.obj.train then + utils.insert_sorted(new.rack_training, rid) + end + end +end + +function AssignRack:onInput(keys) + if self:propagateMoveKeys(keys) then + if df.global.world.selected_building ~= self.building then + self:dismiss() + end + else + AssignRack.super.onInput(self, keys) + end +end + +if dfhack.gui.getCurFocus() ~= 'dwarfmode/QueryBuilding/Some/Weaponrack' then + qerror("This script requires a weapon rack selected in the 'q' mode") +end + +AssignRack{ building = dfhack.gui.getSelectedBuilding() }:show() + +if not already_warned then + already_warned = true + dlg.showMessage( + 'BUG ALERT', + { 'This script requires a binary patch from', NEWLINE, + 'bug 1445 on the tracker. Otherwise the game', NEWLINE, + 'will lose your settings due to a bug.' }, + COLOR_YELLOW + ) +end diff --git a/scripts/gui/choose-weapons.lua b/scripts/gui/choose-weapons.lua new file mode 100644 index 000000000..85ad62b6e --- /dev/null +++ b/scripts/gui/choose-weapons.lua @@ -0,0 +1,154 @@ +-- Rewrite individual choice weapons into specific types. + +local utils = require 'utils' +local dlg = require 'gui.dialogs' + +local defs = df.global.world.raws.itemdefs +local entity = df.global.ui.main.fortress_entity +local tasks = df.global.ui.tasks +local equipment = df.global.ui.equipment + +function find_best_weapon(unit,mode) + local best = nil + local skill = nil + local skill_level = nil + local count = 0 + local function try(id,iskill) + local slevel = dfhack.units.getNominalSkill(unit,iskill) + -- Choose most skill + if (skill ~= nil and slevel > skill_level) + or (skill == nil and slevel > 0) then + best,skill,skill_level,count = id,iskill,slevel,0 + end + -- Then most produced within same skill + if skill == iskill then + local cnt = tasks.created_weapons[id] + if cnt > count then + best,count = id,cnt + end + end + end + for _,id in ipairs(entity.resources.weapon_type) do + local def = defs.weapons[id] + if def.skill_ranged >= 0 then + if mode == nil or mode == 'ranged' then + try(id, def.skill_ranged) + end + else + if mode == nil or mode == 'melee' then + try(id, def.skill_melee) + end + end + end + return best +end + +function unassign_wrong_items(unit,position,spec,subtype) + for i=#spec.assigned-1,0,-1 do + local id = spec.assigned[i] + local item = df.item.find(id) + + if item.subtype.subtype ~= subtype then + spec.assigned:erase(i) + + -- TODO: somewhat unexplored area; maybe missing some steps + utils.erase_sorted(position.assigned_items,id) + if utils.erase_sorted(equipment.items_assigned.WEAPON,item,'id') then + utils.insert_sorted(equipment.items_unassigned.WEAPON,item,'id') + end + equipment.update.weapon = true + unit.military.pickup_flags.update = true + end + end +end + +local count = 0 + +function adjust_uniform_spec(unit,position,spec,force) + if not unit then return end + local best + if spec.indiv_choice.melee then + best = find_best_weapon(unit, 'melee') + elseif spec.indiv_choice.ranged then + best = find_best_weapon(unit, 'ranged') + elseif spec.indiv_choice.any or force then + best = find_best_weapon(unit, nil) + end + if best then + count = count + 1 + spec.item_filter.item_subtype = best + spec.indiv_choice.any = false + spec.indiv_choice.melee = false + spec.indiv_choice.ranged = false + unassign_wrong_items(unit, position, spec, best) + end +end + +function adjust_position(unit,position,force) + if not unit then + local fig = df.historical_figure.find(position.occupant) + if not fig then return end + unit = df.unit.find(fig.unit_id) + end + + for _,v in ipairs(position.uniform.weapon) do + adjust_uniform_spec(unit, position, v, force) + end +end + +function adjust_squad(squad, force) + for _,pos in ipairs(squad.positions) do + adjust_position(nil, pos, force) + end +end + +local args = {...} +local vs = dfhack.gui.getCurViewscreen() +local vstype = df.viewscreen_layer_militaryst +if not vstype:is_instance(vs) then + qerror('Call this from the military screen') +end + +if vs.page == vstype.T_page.Equip +and vs.equip.mode == vstype.T_equip.T_mode.Customize then + local slist = vs.layer_objects[0] + local squad = vs.equip.squads[slist:getListCursor()] + + if slist.active then + print('Adjusting squad.') + adjust_squad(squad) + else + local plist = vs.layer_objects[1] + local pidx = plist:getListCursor() + local pos = squad.positions[pidx] + local unit = vs.equip.units[pidx] + + if plist.active then + print('Adjusting position.') + adjust_position(unit, pos) + elseif unit and vs.equip.edit_mode < 0 then + local wlist = vs.layer_objects[2] + local idx = wlist:getListCursor() + local cat = vs.equip.assigned.category[idx] + + if wlist.active and cat == df.uniform_category.weapon then + print('Adjusting spec.') + adjust_uniform_spec(unit, pos, vs.equip.assigned.spec[idx], true) + end + end + end +else + qerror('Call this from the Equip page of military screen') +end + +if count > 1 then + dlg.showMessage( + 'Choose Weapons', + 'Updated '..count..' uniform entries.', COLOR_GREEN + ) +elseif count == 0 then + dlg.showMessage( + 'Choose Weapons', + 'Did not find any entries to update.', COLOR_YELLOW + ) +end diff --git a/scripts/gui/guide-path.lua b/scripts/gui/guide-path.lua new file mode 100644 index 000000000..a807e032d --- /dev/null +++ b/scripts/gui/guide-path.lua @@ -0,0 +1,193 @@ +-- Show and manipulate the path used by Guide Cart orders. + +local utils = require 'utils' +local gui = require 'gui' +local guidm = require 'gui.dwarfmode' +local dlg = require 'gui.dialogs' + +local tile_attrs = df.tiletype.attrs + +GuidePathUI = defclass(GuidePathUI, guidm.MenuOverlay) + +GuidePathUI.focus_path = 'guide-path' + +GuidePathUI.ATTRS { + route = DEFAULT_NIL, + stop = DEFAULT_NIL, + order = DEFAULT_NIL, +} + +function GuidePathUI:init() + self.saved_mode = df.global.ui.main.mode + + for i=0,#self.route.stops-1 do + if self.route.stops[i] == self.stop then + self.stop_idx = i + break + end + end + + if not self.stop_idx then + error('Could not find stop index') + end + + self.next_stop = self.route.stops[(self.stop_idx+1)%#self.route.stops] +end + +function GuidePathUI:onShow() + -- with cursor, but without those ugly lines from native hauling mode + df.global.ui.main.mode = df.ui_sidebar_mode.Stockpiles +end + +function GuidePathUI:onDestroy() + df.global.ui.main.mode = self.saved_mode +end + +local function getTileType(cursor) + local block = dfhack.maps.getTileBlock(cursor) + if block then + return block.tiletype[cursor.x%16][cursor.y%16] + else + return 0 + end +end + +local function isTrackTile(tile) + return tile_attrs[tile].special == df.tiletype_special.TRACK +end + +local function paintMapTile(dc, vp, cursor, pos, ...) + if not same_xyz(cursor, pos) then + local stile = vp:tileToScreen(pos) + if stile.z == 0 then + dc:seek(stile.x,stile.y):char(...) + end + end +end + +local function get_path_point(gpath,i) + return xyz2pos(gpath.x[i], gpath.y[i], gpath.z[i]) +end + +function GuidePathUI:renderPath(cursor) + local gpath = self.order.guide_path + local startp = self.stop.pos + local endp = self.next_stop.pos + local vp = self:getViewport() + local dc = gui.Painter.new(self.df_layout.map) + local visible = gui.blink_visible(500) + + if visible then + paintMapTile(dc, vp, cursor, endp, '+', COLOR_LIGHTGREEN) + end + + local ok = nil + local pcnt = #gpath.x + if pcnt > 0 then + ok = true + + for i = 0,pcnt-1 do + local pt = get_path_point(gpath, i) + if i == 0 and not same_xyz(startp,pt) then + ok = false + end + if i == pcnt-1 and not same_xyz(endp,pt) then + ok = false + end + local tile = getTileType(pt) + if not isTrackTile(tile) then + ok = false + end + if visible then + local char = '+' + if i < pcnt-1 then + local npt = get_path_point(gpath, i+1) + if npt.x == pt.x+1 then + char = 26 + elseif npt.x == pt.x-1 then + char = 27 + elseif npt.y == pt.y+1 then + char = 25 + elseif npt.y == pt.y-1 then + char = 24 + end + end + local color = COLOR_LIGHTGREEN + if not ok then color = COLOR_LIGHTRED end + paintMapTile(dc, vp, cursor, pt, char, color) + end + end + end + + if gui.blink_visible(120) then + paintMapTile(dc, vp, cursor, startp, 240, COLOR_LIGHTGREEN, COLOR_GREEN) + end + + return ok +end + +function GuidePathUI:onRenderBody(dc) + dc:clear():seek(1,1):pen(COLOR_WHITE):string("Guide Path") + + dc:seek(2,3) + + local cursor = guidm.getCursorPos() + local path_ok = self:renderPath(cursor) + + if path_ok == nil then + dc:string('No saved path', COLOR_DARKGREY) + elseif path_ok then + dc:string('Valid path', COLOR_GREEN) + else + dc:string('Invalid path', COLOR_RED) + end + + dc:newline():newline(1) + dc:key('CUSTOM_Z'):string(": Reset path",COLOR_GREY,nil,path_ok~=nil) + --dc:key('CUSTOM_P'):string(": Find path",COLOR_GREY,nil,false) + dc:newline(1) + dc:key('CUSTOM_C'):string(": Zoom cur, ") + dc:key('CUSTOM_N'):string(": Zoom next") + + dc:newline():newline(1):string('At cursor:') + dc:newline():newline(2) + + local tile = getTileType(cursor) + if isTrackTile(tile) then + dc:string('Track '..tile_attrs[tile].direction, COLOR_GREEN) + else + dc:string('No track', COLOR_DARKGREY) + end + + dc:newline():newline(1) + dc:key('LEAVESCREEN'):string(": Back") +end + +function GuidePathUI:onInput(keys) + if keys.CUSTOM_C then + self:moveCursorTo(copyall(self.stop.pos)) + elseif keys.CUSTOM_N then + self:moveCursorTo(copyall(self.next_stop.pos)) + elseif keys.CUSTOM_Z then + local gpath = self.order.guide_path + gpath.x:resize(0) + gpath.y:resize(0) + gpath.z:resize(0) + elseif keys.LEAVESCREEN then + self:dismiss() + elseif self:propagateMoveKeys(keys) then + return + end +end + +if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/Hauling/DefineStop/Cond/Guide') then + qerror("This script requires the main dwarfmode view in 'h' mode over a Guide order") +end + +local hauling = df.global.ui.hauling +local route = hauling.view_routes[hauling.cursor_top] +local stop = hauling.view_stops[hauling.cursor_top] +local order = hauling.stop_conditions[hauling.cursor_stop] + +local list = GuidePathUI{ route = route, stop = stop, order = order } +list:show() diff --git a/scripts/gui/hello-world.lua b/scripts/gui/hello-world.lua index c8cd3bd01..122f2cd38 100644 --- a/scripts/gui/hello-world.lua +++ b/scripts/gui/hello-world.lua @@ -7,12 +7,13 @@ local text = 'Woohoo, lua viewscreen :)' local screen = gui.FramedScreen{ frame_style = gui.GREY_LINE_FRAME, frame_title = 'Hello World', - frame_width = #text+6, - frame_height = 3, + frame_width = #text, + frame_height = 1, + frame_inset = 1, } function screen:onRenderBody(dc) - dc:seek(3,1):string(text, COLOR_LIGHTGREEN) + dc:string(text, COLOR_LIGHTGREEN) end function screen:onInput(keys) diff --git a/scripts/gui/liquids.lua b/scripts/gui/liquids.lua index cddb9f01d..322ae6952 100644 --- a/scripts/gui/liquids.lua +++ b/scripts/gui/liquids.lua @@ -17,8 +17,8 @@ local brushes = { } local paints = { - { tag = 'water', caption = 'Water', liquid = true, flow = true, key = 'w' }, - { tag = 'magma', caption = 'Magma', liquid = true, flow = true, key = 'l' }, + { tag = 'water', caption = 'Water', liquid = true, flow = true, key = 'D_LOOK_ARENA_WATER' }, + { tag = 'magma', caption = 'Magma', liquid = true, flow = true, key = 'D_LOOK_ARENA_MAGMA' }, { tag = 'obsidian', caption = 'Obsidian Wall' }, { tag = 'obsidian_floor', caption = 'Obsidian Floor' }, { tag = 'riversource', caption = 'River Source' }, @@ -64,7 +64,7 @@ function Toggle:render(dc) if item then dc:string(item.caption) if item.key then - dc:string(" ("):string(item.key, COLOR_LIGHTGREEN):string(")") + dc:string(" ("):key(item.key):string(")") end else dc:string('NONE', COLOR_RED) @@ -160,9 +160,9 @@ function LiquidsUI:onRenderBody(dc) dc:newline():pen(COLOR_GREY) - dc:newline(1):string("b", COLOR_LIGHTGREEN):string(": ") + dc:newline(1):key('CUSTOM_B'):string(": ") self.brush:render(dc) - dc:newline(1):string("p", COLOR_LIGHTGREEN):string(": ") + dc:newline(1):key('CUSTOM_P'):string(": ") self.paint:render(dc) local paint = self.paint:get() @@ -170,8 +170,8 @@ function LiquidsUI:onRenderBody(dc) dc:newline() if paint.liquid then dc:newline(1):string("Amount: "..self.amount) - dc:advance(1):string("("):string("-+", COLOR_LIGHTGREEN):string(")") - dc:newline(3):string("s", COLOR_LIGHTGREEN):string(": ") + dc:advance(1):string("("):key('SECONDSCROLL_UP'):key('SECONDSCROLL_DOWN'):string(")") + dc:newline(3):key('CUSTOM_S'):string(": ") self.set:render(dc) else dc:advance(0,2) @@ -179,17 +179,17 @@ function LiquidsUI:onRenderBody(dc) dc:newline() if paint.flow then - dc:newline(1):string("f", COLOR_LIGHTGREEN):string(": ") + dc:newline(1):key('CUSTOM_F'):string(": ") self.flow:render(dc) - dc:newline(1):string("r", COLOR_LIGHTGREEN):string(": ") + dc:newline(1):key('CUSTOM_R'):string(": ") self.permaflow:render(dc) else dc:advance(0,2) end dc:newline():newline(1):pen(COLOR_WHITE) - dc:string("Esc", COLOR_LIGHTGREEN):string(": Back, ") - dc:string("Enter", COLOR_LIGHTGREEN):string(": Paint") + dc:key('LEAVESCREEN'):string(": Back, ") + dc:key('SELECT'):string(": Paint") end function ensure_blocks(cursor, size, cb) diff --git a/scripts/gui/mechanisms.lua b/scripts/gui/mechanisms.lua index d1e8ec803..607625524 100644 --- a/scripts/gui/mechanisms.lua +++ b/scripts/gui/mechanisms.lua @@ -89,8 +89,8 @@ function MechanismList:onRenderBody(dc) end dc:newline():newline(1):pen(COLOR_WHITE) - dc:string("Esc", COLOR_LIGHTGREEN):string(": Back, ") - dc:string("Enter", COLOR_LIGHTGREEN):string(": Switch") + dc:key('LEAVESCREEN'):string(": Back, ") + dc:key('SELECT'):string(": Switch") end function MechanismList:changeSelected(delta) diff --git a/scripts/gui/power-meter.lua b/scripts/gui/power-meter.lua index 6c2f699ac..ac30c9273 100644 --- a/scripts/gui/power-meter.lua +++ b/scripts/gui/power-meter.lua @@ -12,6 +12,10 @@ PowerMeter = defclass(PowerMeter, guidm.MenuOverlay) PowerMeter.focus_path = 'power-meter' +PowerMeter.ATTRS { + frame_background = false +} + function PowerMeter:init() self:assign{ min_power = 0, max_power = -1, invert = false, @@ -34,7 +38,8 @@ function PowerMeter:onRenderBody(dc) dc:string("Excess power range:") - dc:newline(3):string("as", COLOR_LIGHTGREEN) + dc:newline(3):key('BUILDING_TRIGGER_MIN_WATER_DOWN') + dc:key('BUILDING_TRIGGER_MIN_WATER_UP') dc:string(": Min ") if self.min_power <= 0 then dc:string("(any)") @@ -42,7 +47,8 @@ function PowerMeter:onRenderBody(dc) dc:string(''..self.min_power) end - dc:newline(3):string("zx", COLOR_LIGHTGREEN) + dc:newline(3):key('BUILDING_TRIGGER_MAX_WATER_DOWN') + dc:key('BUILDING_TRIGGER_MAX_WATER_UP') dc:string(": Max ") if self.max_power < 0 then dc:string("(any)") @@ -51,7 +57,7 @@ function PowerMeter:onRenderBody(dc) end dc:newline():newline(1) - dc:string("i",COLOR_LIGHTGREEN):string(": ") + dc:key('CUSTOM_I'):string(": ") if self.invert then dc:string("Inverted") else diff --git a/scripts/gui/rename.lua b/scripts/gui/rename.lua index a457a0bfd..70a09b2fa 100644 --- a/scripts/gui/rename.lua +++ b/scripts/gui/rename.lua @@ -13,10 +13,12 @@ local function verify_mode(expected) end end -if string.match(focus, '^dwarfmode/QueryBuilding/Some') then +local unit = dfhack.gui.getSelectedUnit(true) +local building = dfhack.gui.getSelectedBuilding(true) + +if building and (not unit or mode == 'building') then verify_mode('building') - local building = df.global.world.selected_building if plugin.canRenameBuilding(building) then dlg.showInputPrompt( 'Rename Building', @@ -30,9 +32,7 @@ if string.match(focus, '^dwarfmode/QueryBuilding/Some') then 'Cannot rename this type of building.', COLOR_LIGHTRED ) end -elseif dfhack.gui.getSelectedUnit(true) then - local unit = dfhack.gui.getSelectedUnit(true) - +elseif unit then if mode == 'unit-profession' then dlg.showInputPrompt( 'Rename Unit', diff --git a/scripts/gui/room-list.lua b/scripts/gui/room-list.lua index 0de82db5f..1f3e663da 100644 --- a/scripts/gui/room-list.lua +++ b/scripts/gui/room-list.lua @@ -185,10 +185,10 @@ function RoomList:onRenderBody(dc) end dc:newline():newline(1):pen(COLOR_WHITE) - dc:string("Esc", COLOR_LIGHTGREEN):string(": Back") + dc:key('LEAVESCREEN'):string(": Back") if can_modify(sel_item) then - dc:string(", "):string("Enter", COLOR_LIGHTGREEN) + dc:string(", "):key('SELECT') if sel_item.obj.owner == sel_item.owner then dc:string(": Unassign") else diff --git a/scripts/gui/siege-engine.lua b/scripts/gui/siege-engine.lua index c98cb1676..9a5d76066 100644 --- a/scripts/gui/siege-engine.lua +++ b/scripts/gui/siege-engine.lua @@ -74,6 +74,10 @@ function SiegeEngine:onDestroy() end end +function SiegeEngine:onGetSelectedBuilding() + return df.global.world.selected_building +end + function SiegeEngine:showCursor(enable) local cursor = guidm.getCursorPos() if cursor and not enable then @@ -200,14 +204,14 @@ function SiegeEngine:onRenderBody_main(dc) dc:string("None (default)") end - dc:newline(3):string("r",COLOR_LIGHTGREEN):string(": Rectangle") + dc:newline(3):key('CUSTOM_R'):string(": Rectangle") if last_target_min then - dc:string(", "):string("p",COLOR_LIGHTGREEN):string(": Paste") + dc:string(", "):key('CUSTOM_P'):string(": Paste") end dc:newline(3) if target_min then - dc:string("x",COLOR_LIGHTGREEN):string(": Clear, ") - dc:string("z",COLOR_LIGHTGREEN):string(": Zoom") + dc:key('CUSTOM_X'):string(": Clear, ") + dc:key('CUSTOM_Z'):string(": Zoom") end dc:newline():newline(1) @@ -215,7 +219,7 @@ function SiegeEngine:onRenderBody_main(dc) dc:string("Uses ballista arrows") else local item = plugin.getAmmoItem(self.building) - dc:string("u",COLOR_LIGHTGREEN):string(": Use ") + dc:key('CUSTOM_U'):string(": Use ") if item_choice_idx[item] then dc:string(item_choices[item_choice_idx[item]].caption) else @@ -224,18 +228,20 @@ function SiegeEngine:onRenderBody_main(dc) end dc:newline():newline(1) - dc:string("t",COLOR_LIGHTGREEN):string(": Take from stockpile"):newline(3) + dc:key('CUSTOM_T'):string(": Take from stockpile"):newline(3) local links = plugin.getStockpileLinks(self.building) local bottom = dc.height - 5 if links then - dc:string("d",COLOR_LIGHTGREEN):string(": Delete, ") - dc:string("o",COLOR_LIGHTGREEN):string(": Zoom"):newline() - self:renderStockpiles(dc, links, bottom-2-dc:localY()) + dc:key('CUSTOM_D'):string(": Delete, ") + dc:key('CUSTOM_O'):string(": Zoom"):newline() + self:renderStockpiles(dc, links, bottom-2-dc:cursorY()) dc:newline():newline() end local prof = self.building:getWorkshopProfile() or {} - dc:seek(1,math.max(dc:localY(),19)):string('ghjk',COLOR_LIGHTGREEN)dc:string(': ') + dc:seek(1,math.max(dc:cursorY(),19)) + dc:key('CUSTOM_G'):key('CUSTOM_H'):key('CUSTOM_J'):key('CUSTOM_K') + dc:string(': ') dc:string(df.skill_rating.attrs[prof.min_level or 0].caption):string('-') dc:string(df.skill_rating.attrs[math.min(LEGENDARY,prof.max_level or 3000)].caption) dc:newline():newline() @@ -353,7 +359,7 @@ function SiegeEngine:onRenderBody_aim(dc) dc:newline(2):string('ERROR', COLOR_RED) end - dc:newline():newline(1):string("Enter",COLOR_LIGHTGREEN) + dc:newline():newline(1):key('SELECT') if first then dc:string(": Finish rectangle") else @@ -363,7 +369,7 @@ function SiegeEngine:onRenderBody_aim(dc) local target_min, target_max = plugin.getTargetArea(self.building) if target_min then - dc:newline(1):string("z",COLOR_LIGHTGREEN):string(": Zoom to current target") + dc:newline(1):key('CUSTOM_Z'):string(": Zoom to current target") end if first then @@ -408,9 +414,9 @@ function SiegeEngine:onRenderBody_pile(dc) if plugin.isLinkedToPile(self.building, sel) then dc:string("Already taking from here"):newline():newline(2) - dc:string("d", COLOR_LIGHTGREEN):string(": Delete link") + dc:key('CUSTOM_D'):string(": Delete link") else - dc:string("Enter",COLOR_LIGHTGREEN):string(": Take from this pile") + dc:key('SELECT'):string(": Take from this pile") end elseif sel then dc:string(utils.getBuildingName(sel), COLOR_DARKGREY) @@ -455,9 +461,9 @@ function SiegeEngine:onRenderBody(dc) self.mode.render(dc) - dc:seek(1, math.max(dc:localY(), 21)):pen(COLOR_WHITE) - dc:string("ESC", COLOR_LIGHTGREEN):string(": Back, ") - dc:string("c", COLOR_LIGHTGREEN):string(": Recenter") + dc:seek(1, math.max(dc:cursorY(), 21)):pen(COLOR_WHITE) + dc:key('LEAVESCREEN'):string(": Back, ") + dc:key('CUSTOM_C'):string(": Recenter") end function SiegeEngine:onInput(keys) diff --git a/scripts/gui/workflow.lua b/scripts/gui/workflow.lua new file mode 100644 index 000000000..8dc958062 --- /dev/null +++ b/scripts/gui/workflow.lua @@ -0,0 +1,358 @@ +-- A GUI front-end for the workflow plugin. + +local utils = require 'utils' +local gui = require 'gui' +local guidm = require 'gui.dwarfmode' +local guimat = require 'gui.materials' +local widgets = require 'gui.widgets' +local dlg = require 'gui.dialogs' + +local workflow = require 'plugins.workflow' + +function check_enabled(cb) + if workflow.isEnabled() then + return cb() + else + dlg.showYesNoPrompt( + 'Enable Plugin', + { 'The workflow plugin is not enabled currently.', NEWLINE, NEWLINE, + 'Press ', { key = 'MENU_CONFIRM' }, ' to enable it.' }, + COLOR_YELLOW, + function() + workflow.setEnabled(true) + return cb() + end + ) + end +end + +function check_repeat(job, cb) + if job.flags['repeat'] then + return cb() + else + dlg.showYesNoPrompt( + 'Not Repeat Job', + { 'Workflow only tracks repeating jobs.', NEWLINE, NEWLINE, + 'Press ', { key = 'MENU_CONFIRM' }, ' to make this one repeat.' }, + COLOR_YELLOW, + function() + job.flags['repeat'] = true + return cb() + end + ) + end +end + +JobConstraints = defclass(JobConstraints, guidm.MenuOverlay) + +JobConstraints.focus_path = 'workflow-job' + +JobConstraints.ATTRS { + job = DEFAULT_NIL, + frame_inset = 1, + frame_background = COLOR_BLACK, +} + +local null_cons = { goal_value = 0, goal_gap = 0, goal_by_count = false } + +function JobConstraints:init(args) + self.building = dfhack.job.getHolder(self.job) + + self:addviews{ + widgets.Label{ + frame = { l = 0, t = 0 }, + text = { + 'Workflow Constraints' + } + }, + widgets.List{ + view_id = 'list', + frame = { t = 2, b = 6 }, + row_height = 4, + scroll_keys = widgets.SECONDSCROLL, + }, + widgets.Label{ + frame = { l = 0, b = 3 }, + enabled = self:callback('isAnySelected'), + text = { + { key = 'BUILDING_TRIGGER_ENABLE_CREATURE', + text = function() + local cons = self:getCurConstraint() or null_cons + if cons.goal_by_count then + return ': Count stacks ' + else + return ': Count items ' + end + end, + on_activate = self:callback('onChangeUnit') }, + { key = 'BUILDING_TRIGGER_ENABLE_MAGMA', text = ': Modify', + on_activate = self:callback('onEditRange') }, + NEWLINE, ' ', + { key = 'BUILDING_TRIGGER_MIN_SIZE_DOWN', + on_activate = self:callback('onIncRange', 'goal_gap', 5) }, + { key = 'BUILDING_TRIGGER_MIN_SIZE_UP', + on_activate = self:callback('onIncRange', 'goal_gap', -1) }, + { text = function() + local cons = self:getCurConstraint() or null_cons + return string.format(': Min %-4d ', cons.goal_value - cons.goal_gap) + end }, + { key = 'BUILDING_TRIGGER_MAX_SIZE_DOWN', + on_activate = self:callback('onIncRange', 'goal_value', -1) }, + { key = 'BUILDING_TRIGGER_MAX_SIZE_UP', + on_activate = self:callback('onIncRange', 'goal_value', 5) }, + { text = function() + local cons = self:getCurConstraint() or null_cons + return string.format(': Max %-4d', cons.goal_value) + end }, + } + }, + widgets.Label{ + frame = { l = 0, b = 0 }, + text = { + { key = 'CUSTOM_N', text = ': New limit, ', + on_activate = self:callback('onNewConstraint') }, + { key = 'CUSTOM_X', text = ': Delete', + enabled = self:callback('isAnySelected'), + on_activate = self:callback('onDeleteConstraint') }, + NEWLINE, NEWLINE, + { key = 'LEAVESCREEN', text = ': Back', + on_activate = self:callback('dismiss') } + } + }, + } + + self:initListChoices(args.clist) +end + +function JobConstraints:onGetSelectedBuilding() + return self.building +end + +function JobConstraints:onGetSelectedJob() + return self.job +end + +function describe_item_type(iobj) + local itemline = 'any item' + if iobj.is_craft then + itemline = 'any craft' + elseif iobj.item_type >= 0 then + itemline = df.item_type.attrs[iobj.item_type].caption or iobj.item_type + local subtype = iobj.item_subtype or -1 + local def = dfhack.items.getSubtypeDef(iobj.item_type, subtype) + local count = dfhack.items.getSubtypeCount(iobj.item_type, subtype) + if def then + itemline = def.name + elseif count >= 0 then + itemline = 'any '..itemline + end + end + return itemline +end + +function is_caste_mat(iobj) + return dfhack.items.isCasteMaterial(iobj.item_type or -1) +end + +function describe_material(iobj) + local matline = 'any material' + if is_caste_mat(iobj) then + matline = 'no material' + elseif (iobj.mat_type or -1) >= 0 then + local info = dfhack.matinfo.decode(iobj.mat_type, iobj.mat_index) + if info then + matline = info:toString() + else + matline = iobj.mat_type..':'..iobj.mat_index + end + end + return matline +end + +function JobConstraints:initListChoices(clist, sel_token) + clist = clist or workflow.listConstraints(self.job) + + local choices = {} + + for i,cons in ipairs(clist) do + local goal = (cons.goal_value-cons.goal_gap)..'-'..cons.goal_value + local curval + if cons.goal_by_count then + goal = goal .. ' stacks' + curval = cons.cur_count + else + goal = goal .. ' items' + curval = cons.cur_amount + end + local order_pen = COLOR_GREY + if cons.request == 'resume' then + order_pen = COLOR_GREEN + elseif cons.request == 'suspend' then + order_pen = COLOR_BLUE + end + local itemstr = describe_item_type(cons) + if cons.min_quality > 0 then + itemstr = itemstr .. ' ('..df.item_quality[cons.min_quality]..')' + end + local matstr = describe_material(cons) + local matflagstr = '' + local matflags = utils.list_bitfield_flags(cons.mat_mask) + if #matflags > 0 then + matflags[1] = 'any '..matflags[1] + if matstr == 'any material' then + matstr = table.concat(matflags, ', ') + matflags = {} + end + end + if #matflags > 0 then + matflagstr = table.concat(matflags, ', ') + end + + table.insert(choices, { + text = { + goal, ' ', { text = '(now '..curval..')', pen = order_pen }, NEWLINE, + ' ', itemstr, NEWLINE, ' ', matstr, NEWLINE, ' ', matflagstr + }, + token = cons.token, + obj = cons + }) + end + + local selidx = nil + if sel_token then + selidx = utils.linear_index(choices, sel_token, 'token') + end + + self.subviews.list:setChoices(choices, selidx) +end + +function JobConstraints:isAnySelected() + return self.subviews.list:getSelected() ~= nil +end + +function JobConstraints:getCurConstraint() + local i,v = self.subviews.list:getSelected() + if v then return v.obj end +end + +function JobConstraints:saveConstraint(cons) + local out = workflow.setConstraint(cons.token, cons.goal_by_count, cons.goal_value, cons.goal_gap) + self:initListChoices(nil, out.token) +end + +function JobConstraints:onChangeUnit() + local cons = self:getCurConstraint() + cons.goal_by_count = not cons.goal_by_count + self:saveConstraint(cons) +end + +function JobConstraints:onEditRange() + local cons = self:getCurConstraint() + dlg.showInputPrompt( + 'Input Range', + 'Enter the new constraint range:', + COLOR_WHITE, + (cons.goal_value-cons.goal_gap)..'-'..cons.goal_value, + function(text) + local maxv = string.match(text, '^%s*(%d+)%s*$') + if maxv then + cons.goal_value = maxv + return self:saveConstraint(cons) + end + local minv,maxv = string.match(text, '^%s*(%d+)-(%d+)%s*$') + if minv and maxv and minv ~= maxv then + cons.goal_value = math.max(minv,maxv) + cons.goal_gap = math.abs(maxv-minv) + return self:saveConstraint(cons) + end + dlg.showMessage('Invalid Range', 'This range is invalid: '..text, COLOR_LIGHTRED) + end + ) +end + +function JobConstraints:onIncRange(field, delta) + local cons = self:getCurConstraint() + if not cons.goal_by_count then + delta = delta * 5 + end + cons[field] = math.max(1, cons[field] + delta) + self:saveConstraint(cons) +end + +function JobConstraints:onNewConstraint() + local outputs = workflow.listJobOutputs(self.job) + if #outputs == 0 then + dlg.showMessage('Unsupported', 'Workflow cannot guess the outputs of this job.', COLOR_LIGHTRED) + return + end + + local variants = workflow.listWeakenedConstraints(outputs) + + local choices = {} + for i,cons in ipairs(variants) do + local itemstr = describe_item_type(cons) + local matstr = describe_material(cons) + local matflags = utils.list_bitfield_flags(cons.mat_mask) + if #matflags > 0 then + local fstr = table.concat(matflags, '/') + if matstr == 'any material' then + matstr = 'any '..fstr + else + matstr = 'any '..fstr..' '..matstr + end + end + + table.insert(choices, { text = itemstr..' of '..matstr, obj = cons }) + end + + dlg.showListPrompt( + 'Job Outputs', + 'Select one of the possible outputs:', + COLOR_WHITE, + choices, + function(idx,item) + self:saveConstraint(item.obj) + end + ) +end + +function JobConstraints:onDeleteConstraint() + local cons = self:getCurConstraint() + dlg.showYesNoPrompt( + 'Delete Constraint', + 'Really delete the current constraint?', + COLOR_YELLOW, + function() + workflow.deleteConstraint(cons.token) + self:initListChoices() + end + ) +end + +function JobConstraints:onInput(keys) + if self:propagateMoveKeys(keys) then + if df.global.world.selected_building ~= self.building then + self:dismiss() + end + else + JobConstraints.super.onInput(self, keys) + end +end + +if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some/Workshop/Job') then + qerror("This script requires a workshop job selected in the 'q' mode") +end + +local job = dfhack.gui.getSelectedJob() + +check_enabled(function() + check_repeat(job, function() + local clist = workflow.listConstraints(job) + if not clist then + dlg.showMessage('Not Supported', 'This type of job is not supported by workflow.', COLOR_LIGHTRED) + return + end + JobConstraints{ job = job, clist = clist }:show() + end) +end) + diff --git a/scripts/gui/workshop-job.lua b/scripts/gui/workshop-job.lua new file mode 100644 index 000000000..c4e203614 --- /dev/null +++ b/scripts/gui/workshop-job.lua @@ -0,0 +1,284 @@ +-- Show and modify properties of jobs in a workshop. + +local utils = require 'utils' +local gui = require 'gui' +local guidm = require 'gui.dwarfmode' +local guimat = require 'gui.materials' +local widgets = require 'gui.widgets' +local dlg = require 'gui.dialogs' + +JobDetails = defclass(JobDetails, guidm.MenuOverlay) + +JobDetails.focus_path = 'workshop-job' + +JobDetails.ATTRS { + job = DEFAULT_NIL, + frame_inset = 1, + frame_background = COLOR_BLACK, +} + +function JobDetails:init(args) + self.building = dfhack.job.getHolder(self.job) + + local status = { text = 'No worker', pen = COLOR_DARKGREY } + local worker = dfhack.job.getWorker(self.job) + if self.job.flags.suspend then + status = { text = 'Suspended', pen = COLOR_RED } + elseif worker then + status = { text = dfhack.TranslateName(dfhack.units.getVisibleName(worker)), pen = COLOR_GREEN } + end + + self:addviews{ + widgets.Label{ + frame = { l = 0, t = 0 }, + text = { + { text = df.job_type.attrs[self.job.job_type].caption }, NEWLINE, NEWLINE, + ' ', status + } + }, + widgets.Label{ + frame = { l = 0, t = 4 }, + text = { + { key = 'CUSTOM_I', text = ': Input item, ', + enabled = self:callback('canChangeIType'), + on_activate = self:callback('onChangeIType') }, + { key = 'CUSTOM_M', text = ': Material', + enabled = self:callback('canChangeMat'), + on_activate = self:callback('onChangeMat') } + } + }, + widgets.List{ + view_id = 'list', + frame = { t = 6, b = 2 }, + row_height = 4, + scroll_keys = widgets.SECONDSCROLL, + }, + widgets.Label{ + frame = { l = 0, b = 0 }, + text = { + { key = 'LEAVESCREEN', text = ': Back', + on_activate = self:callback('dismiss') } + } + }, + } + + self:initListChoices() +end + +function JobDetails:onGetSelectedBuilding() + return self.building +end + +function JobDetails:onGetSelectedJob() + return self.job +end + +function describe_item_type(iobj) + local itemline = 'any item' + if iobj.item_type >= 0 then + itemline = df.item_type.attrs[iobj.item_type].caption or iobj.item_type + local def = dfhack.items.getSubtypeDef(iobj.item_type, iobj.item_subtype) + local count = dfhack.items.getSubtypeCount(iobj.item_type, iobj.item_subtype) + if def then + itemline = def.name + elseif count >= 0 then + itemline = 'any '..itemline + end + end + return itemline +end + +function is_caste_mat(iobj) + return dfhack.items.isCasteMaterial(iobj.item_type) +end + +function describe_material(iobj) + local matline = 'any material' + if is_caste_mat(iobj) then + matline = 'material not applicable' + elseif iobj.mat_type >= 0 then + local info = dfhack.matinfo.decode(iobj.mat_type, iobj.mat_index) + if info then + matline = info:toString() + else + matline = iobj.mat_type..':'..iobj.mat_index + end + end + return matline +end + +function list_flags(list, bitfield) + for name,val in pairs(bitfield) do + if val then + table.insert(list, name) + end + end +end + +function JobDetails:initListChoices() + local items = {} + for i,ref in ipairs(self.job.items) do + local idx = ref.job_item_idx + if idx >= 0 then + items[idx] = (items[idx] or 0) + 1 + end + end + + local choices = {} + for i,iobj in ipairs(self.job.job_items) do + local head = 'Item '..(i+1)..': '..(items[i] or 0)..' of '..iobj.quantity + if iobj.min_dimension > 0 then + head = head .. '(size '..iobj.min_dimension..')' + end + + local line1 = {} + local reaction = df.reaction.find(iobj.reaction_id) + if reaction and #iobj.contains > 0 then + for _,ri in ipairs(iobj.contains) do + table.insert(line1, 'has '..utils.call_with_string( + reaction.reagents[ri],'getDescription',iobj.reaction_id + )) + end + end + if iobj.metal_ore >= 0 then + local ore = dfhack.matinfo.decode(0, iobj.metal_ore) + if ore then + table.insert(line1, 'ore of '..ore:toString()) + end + end + if iobj.has_material_reaction_product ~= '' then + table.insert(line1, 'product '..iobj.has_material_reaction_product) + end + if iobj.reaction_class ~= '' then + table.insert(line1, 'class '..iobj.reaction_class) + end + if iobj.has_tool_use >= 0 then + table.insert(line1, 'has use '..df.tool_uses[iobj.has_tool_use]) + end + list_flags(line1, iobj.flags1) + list_flags(line1, iobj.flags2) + list_flags(line1, iobj.flags3) + if #line1 == 0 then + table.insert(line1, 'no flags') + end + + table.insert(choices, { + index = i, + iobj = iobj, + text = { + head, NEWLINE, + ' ', { text = curry(describe_item_type, iobj) }, NEWLINE, + ' ', { text = curry(describe_material, iobj) }, NEWLINE, + ' ', table.concat(line1, ', '), NEWLINE + } + }) + end + + self.subviews.list:setChoices(choices) +end + +function JobDetails:canChangeIType() + local idx, obj = self.subviews.list:getSelected() + return obj ~= nil +end + +function JobDetails:setItemType(obj, item_type, item_subtype) + obj.iobj.item_type = item_type + obj.iobj.item_subtype = item_subtype + + if is_caste_mat(obj.iobj) then + self:setMaterial(obj, -1, -1) + end +end + +function JobDetails:onChangeIType() + local idx, obj = self.subviews.list:getSelected() + guimat.ItemTypeDialog{ + prompt = 'Please select a new item type for input '..idx, + none_caption = 'any item', + item_filter = curry(dfhack.job.isSuitableItem, obj.iobj), + on_select = self:callback('setItemType', obj) + }:show() +end + +function JobDetails:canChangeMat() + local idx, obj = self.subviews.list:getSelected() + return obj ~= nil and not is_caste_mat(obj.iobj) +end + +function JobDetails:setMaterial(obj, mat_type, mat_index) + if obj.index == 0 + and self.job.mat_type == obj.iobj.mat_type + and self.job.mat_index == obj.iobj.mat_index + and self.job.job_type ~= df.job_type.PrepareMeal + then + self.job.mat_type = mat_type + self.job.mat_index = mat_index + end + + obj.iobj.mat_type = mat_type + obj.iobj.mat_index = mat_index +end + +function JobDetails:findUnambiguousItem(iobj) + local count = 0 + local itype + + for i = 0,df.item_type._last_item do + if dfhack.job.isSuitableItem(iobj, i, -1) then + count = count + 1 + if count > 1 then return nil end + itype = i + end + end + + return itype +end + +function JobDetails:onChangeMat() + local idx, obj = self.subviews.list:getSelected() + + if obj.iobj.item_type == -1 and obj.iobj.mat_type == -1 then + -- If the job allows only one specific item type, use it + local vitype = self:findUnambiguousItem(obj.iobj) + + if vitype then + obj.iobj.item_type = vitype + else + dlg.showMessage( + 'Bug Alert', + { 'Please set a specific item type first.\n\n', + 'Otherwise the material will be matched\n', + 'incorrectly due to a limitation in DF code.' }, + COLOR_YELLOW + ) + return + end + end + + guimat.MaterialDialog{ + prompt = 'Please select a new material for input '..idx, + none_caption = 'any material', + mat_filter = function(mat,parent,mat_type,mat_index) + return dfhack.job.isSuitableMaterial(obj.iobj, mat_type, mat_index) + end, + on_select = self:callback('setMaterial', obj) + }:show() +end + +function JobDetails:onInput(keys) + if self:propagateMoveKeys(keys) then + if df.global.world.selected_building ~= self.building then + self:dismiss() + end + else + JobDetails.super.onInput(self, keys) + end +end + +if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some/Workshop/Job') then + qerror("This script requires a workshop job selected in the 'q' mode") +end + +local dlg = JobDetails{ job = dfhack.gui.getSelectedJob() } +dlg:show() diff --git a/scripts/removebadthoughts.rb b/scripts/removebadthoughts.rb index 99b742643..ff98f8f65 100644 --- a/scripts/removebadthoughts.rb +++ b/scripts/removebadthoughts.rb @@ -1,27 +1,43 @@ # remove bad thoughts for the selected unit or the whole fort -# with removebadthoughts -v, dump the bad thoughts types we removed -verbose = $script_args.delete('-v') +dry_run = $script_args.delete('--dry-run') || $script_args.delete('-n') -if u = df.unit_find(:selected) - targets = [u] -else - targets = df.unit_citizens -end +$script_args << 'all' if dry_run and $script_args.empty? seenbad = Hash.new(0) -targets.each { |u| +clear_mind = lambda { |u| u.status.recent_events.each { |e| next if DFHack::UnitThoughtType::Value[e.type].to_s[0, 1] != '-' seenbad[e.type] += 1 - e.age = 0x1000_0000 + e.age = 0x1000_0000 unless dry_run } } -if verbose - seenbad.sort_by { |k, v| v }.each { |k, v| puts " #{v} #{k}" } -end +summary = lambda { + seenbad.sort_by { |thought, cnt| cnt }.each { |thought, cnt| + puts " #{thought} #{cnt}" + } + count = seenbad.values.inject(0) { |sum, cnt| sum+cnt } + puts "Removed #{count} bad thought#{'s' if count != 1}." if count > 0 and not dry_run +} -count = seenbad.values.inject(0) { |s, v| s+v } -puts "removed #{count} bad thought#{'s' if count != 1}" +case $script_args[0] +when 'him' + if u = df.unit_find + clear_mind[u] + summary[] + else + puts 'Please select a dwarf ingame' + end + +when 'all' + df.unit_citizens.each { |uu| + clear_mind[uu] + } + summary[] + +else + puts "Usage: removebadthoughts [--dry-run] " + +end diff --git a/scripts/siren.lua b/scripts/siren.lua new file mode 100644 index 000000000..4fc574ed0 --- /dev/null +++ b/scripts/siren.lua @@ -0,0 +1,122 @@ +-- Wakes up the sleeping, breaks up parties and stops breaks. + +local utils = require 'utils' + +local args = {...} +local burrows = {} +local bnames = {} + +if not dfhack.isMapLoaded() then + qerror('Map is not loaded.') +end + +for _,v in ipairs(args) do + local b = dfhack.burrows.findByName(v) + if not b then + qerror('Unknown burrow: '..v) + end + table.insert(bnames, v) + table.insert(burrows, b) +end + +local in_siege = false + +function is_in_burrows(pos) + if #burrows == 0 then + return true + end + for _,v in ipairs(burrows) do + if dfhack.burrows.isAssignedTile(v, pos) then + return true + end + end +end + +function add_thought(unit, code) + for _,v in ipairs(unit.status.recent_events) do + if v.type == code then + v.age = 0 + return + end + end + + unit.status.recent_events:insert('#', { new = true, type = code }) +end + +function wake_unit(unit) + local job = unit.job.current_job + if not job or job.job_type ~= df.job_type.Sleep then + return + end + + if job.completion_timer > 0 then + unit.counters.unconscious = 0 + add_thought(unit, df.unit_thought_type.SleepNoiseWake) + elseif job.completion_timer < 0 then + add_thought(unit, df.unit_thought_type.Tired) + end + + job.pos:assign(unit.pos) + + job.completion_timer = 0 + + unit.path.dest:assign(unit.pos) + unit.path.path.x:resize(0) + unit.path.path.y:resize(0) + unit.path.path.z:resize(0) + + unit.counters.job_counter = 0 +end + +function stop_break(unit) + local counter = dfhack.units.getMiscTrait(unit, df.misc_trait_type.OnBreak) + if counter then + counter.id = df.misc_trait_type.TimeSinceBreak + counter.value = 100800 - 30*1200 + add_thought(unit, df.unit_thought_type.Tired) + end +end + +-- Check siege for thought purpose +for _,v in ipairs(df.global.ui.invasions.list) do + if v.flags.siege and v.flags.active then + in_siege = true + break + end +end + +-- Stop rest +for _,v in ipairs(df.global.world.units.active) do + local x,y,z = dfhack.units.getPosition(v) + if x and dfhack.units.isCitizen(v) and is_in_burrows(xyz2pos(x,y,z)) then + if not in_siege and v.military.squad_id < 0 then + add_thought(v, df.unit_thought_type.LackProtection) + end + wake_unit(v) + stop_break(v) + end +end + +-- Stop parties +for _,v in ipairs(df.global.ui.parties) do + local pos = utils.getBuildingCenter(v.location) + if is_in_burrows(pos) then + v.timer = 0 + for _, u in ipairs(v.units) do + add_thought(unit, df.unit_thought_type.Tired) + end + end +end + +local place = 'the halls and tunnels' +if #bnames > 0 then + if #bnames == 1 then + place = bnames[1] + else + place = table.concat(bnames,', ',1,#bnames-1)..' and '..bnames[#bnames] + end +end +dfhack.gui.showAnnouncement( + 'A loud siren sounds throughout '..place..', waking the sleeping and startling the awake.', + COLOR_BROWN, true +) diff --git a/scripts/slayrace.rb b/scripts/slayrace.rb index b1fc24e35..749d0189b 100644 --- a/scripts/slayrace.rb +++ b/scripts/slayrace.rb @@ -33,12 +33,14 @@ slayit = lambda { |u| end } -all_races = df.world.units.active.map { |u| - u.race_tg.creature_id if checkunit[u] -}.compact.uniq.sort +all_races = Hash.new(0) + +df.world.units.active.map { |u| + all_races[u.race_tg.creature_id] += 1 if checkunit[u] +} if !race - puts all_races + all_races.sort_by { |race, cnt| [cnt, race] }.each{ |race, cnt| puts " #{race} #{cnt}" } elsif race == 'him' if him = df.unit_find slayit[him] @@ -46,7 +48,7 @@ elsif race == 'him' puts "Choose target" end else - raw_race = df.match_rawname(race, all_races) + raw_race = df.match_rawname(race, all_races.keys) raise 'invalid race' if not raw_race race_nr = df.world.raws.creatures.all.index { |cr| cr.creature_id == raw_race } diff --git a/scripts/superdwarf.rb b/scripts/superdwarf.rb new file mode 100644 index 000000000..7f5296b74 --- /dev/null +++ b/scripts/superdwarf.rb @@ -0,0 +1,61 @@ +# give super-dwarven speed to an unit + +$superdwarf_onupdate ||= nil +$superdwarf_ids ||= [] + +case $script_args[0] +when 'add' + if u = df.unit_find + $superdwarf_ids |= [u.id] + + $superdwarf_onupdate ||= df.onupdate_register(1) { + if $superdwarf_ids.empty? + df.onupdate_unregister($superdwarf_onupdate) + $superdwarf_onupdate = nil + else + $superdwarf_ids.each { |id| + if u = df.unit_find(id) and not u.flags1.dead + # faster walk/work + if u.counters.job_counter > 0 + u.counters.job_counter = 0 + end + + # no sleep + if u.counters2.sleepiness_timer > 10000 + u.counters2.sleepiness_timer = 1 + end + + # no break + if b = u.status.misc_traits.find { |t| t.id == :OnBreak } + b.value = 500_000 + end + else + $superdwarf_ids.delete id + end + } + end + } + else + puts "Select a creature using 'v'" + end + +when 'del' + if u = df.unit_find + $superdwarf_ids.delete u.id + else + puts "Select a creature using 'v'" + end + +when 'clear' + $superdwarf_ids.clear + +when 'list' + puts "current superdwarves:", $superdwarf_ids.map { |id| df.unit_find(id).name } + +else + puts "Usage:", + " - superdwarf add: give superspeed to currently selected creature", + " - superdwarf del: remove superspeed to current creature", + " - superdwarf clear: remove all superpowers", + " - superdwarf list: list super-dwarves" +end