Merge branch 'master' of git://github.com/angavrilov/dfhack

develop
jj 2012-09-19 14:29:09 +02:00
commit 27fd3f5fc7
40 changed files with 2671 additions and 622 deletions

@ -999,6 +999,10 @@ Items module
Move the item to the unit inventory. Returns *false* if impossible.
* ``dfhack.items.remove(item[, no_uncat])``
Removes the item, and marks it for garbage collection unless ``no_uncat`` is true.
* ``dfhack.items.makeProjectile(item)``
Turns the item into a projectile, and returns the new object, or *nil* if impossible.

@ -1213,6 +1213,9 @@ Returns <em>false</em> in case of error.</p>
<li><p class="first"><tt class="docutils literal">dfhack.items.moveToInventory(item,unit,use_mode,body_part)</tt></p>
<p>Move the item to the unit inventory. Returns <em>false</em> if impossible.</p>
</li>
<li><p class="first"><tt class="docutils literal">dfhack.items.remove(item[, no_uncat])</tt></p>
<p>Removes the item, and marks it for garbage collection unless <tt class="docutils literal">no_uncat</tt> is true.</p>
</li>
<li><p class="first"><tt class="docutils literal">dfhack.items.makeProjectile(item)</tt></p>
<p>Turns the item into a projectile, and returns the new object, or <em>nil</em> if impossible.</p>
</li>

30
NEWS

@ -25,6 +25,8 @@ DFHack v0.34.11-r2 (UNRELEASED)
- tweak readable-build-plate: fix unreadable truncation in unit pressure plate build ui.
- tweak stable-temp: fixes bug 6012; may improve FPS by 50-100% on a slow item-heavy fort.
- tweak fast-heat: speeds up item heating & cooling, thus making stable-temp act faster.
- tweak fix-dimensions: fixes subtracting small amounts from stacked liquids etc.
- tweak advmode-contained: fixes UI bug in custom reactions with container inputs in advmode.
New scripts:
- fixnaked: removes thoughts about nakedness.
- setfps: set FPS cap at runtime, in case you want slow motion or speed-up.
@ -52,18 +54,30 @@ DFHack v0.34.11-r2 (UNRELEASED)
New Dwarf Manipulator plugin:
Open the unit list, and press 'l' to access a Dwarf Therapist like UI in the game.
New Steam Engine plugin:
Dwarven Water Reactors don't make any sense whatsoever, so this is a potential
replacement for those concerned by it. The plugin detects if a workshop with a
Dwarven Water Reactors don't make any sense whatsoever and cause lag, so this may be
a replacement for those concerned by it. The plugin detects if a workshop with a
certain name is in the raws used by the current world, and provides the necessary
behavior. See hack/raw/*_steam_engine.txt for the necessary raw definitions.
Note: Stuff like animal treadmills might be more period, but can't be done with dfhack.
Note: Stuff like animal treadmills might be more period, but absolutely can't be
done with tools dfhack has access to.
New Power Meter plugin:
When activated, implements a pressure plate modification that detects power in gear
boxes built on the four adjacent N/S/W/E tiles. The gui/power-meter script implements
the build configuration UI.
New Siege Engine plugin (INCOMPLETE):
the necessary build configuration UI.
New Siege Engine plugin:
When enabled and configured via gui/siege-engine, allows aiming siege engines
at a designated rectangular area across Z levels. Also supports loading catapults
with non-boulder projectiles, taking from a stockpile, and restricting operator
skill range, like with ordinary workshops.
at a designated rectangular area with 360 degree fire range and across Z levels;
this works by rewriting the projectile trajectory immediately after it appears.
Also supports loading catapults with non-boulder projectiles, taking from a stockpile,
and restricting operator skill range like with ordinary workshops.
Disclaimer: not in any way to undermine the future siege update from Toady, but
the aiming logic of existing engines hasn't been updated since 2D, and is almost
useless above ground :(. Again, things like making siegers bring their own engines
is totally out of the scope of dfhack and can only be done by Toady.
New Add Spatter plugin:
Detects reactions with certain names in the raws, and changes them from adding
improvements to adding item contaminants. This allows directly covering items
with poisons. The added spatters are immune both to water and 'clean items'.
Intended to give some use to all those giant cave spider poison barrels brought
by the caravans.

@ -707,6 +707,8 @@ Options
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.
reveal
======
@ -907,6 +909,23 @@ 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.
: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
========
@ -1414,16 +1433,16 @@ using this mode for birds is not recommanded.)
Will target any unit on a revealed tile of the map, including ambushers.
Ex:
::
Ex::
slayrace gob
To kill a single creature, select the unit with the 'v' cursor and:
::
To kill a single creature, select the unit with the 'v' cursor and::
slayrace him
To purify all elves on the map with fire (may have side-effects):
::
To purify all elves on the map with fire (may have side-effects)::
slayrace elve magma
@ -1435,8 +1454,8 @@ This script registers a map tile as a magma source, and every 12 game ticks
that tile receives 1 new unit of flowing magma.
Place the game cursor where you want to create the source (must be a
flow-passable tile, and not too high in the sky) and call
::
flow-passable tile, and not too high in the sky) and call::
magmasource here
To add more than 1 unit everytime, call the command again.
@ -1484,3 +1503,214 @@ drainaquifer
============
Remove all 'aquifer' tag from the map blocks. Irreversible.
=======================
In-game interface tools
=======================
These tools work by displaying dialogs or overlays in the game window, and
are mostly implemented by lua scripts.
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.
Liquids
=======
Implemented by the gui/liquids script. 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.
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
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.
* ``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.
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.
The script lists other rooms owned by the same owner, or by the unit selected in the assign
list, and allows unassigning them.
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 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 reflect if it can be
hit by the selected engine.
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.
Pressing 't' switches to stockpile selection.
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.
Steam Engine
============
The steam-engine plugin detects custom workshops with STEAM_ENGINE in
their token, and turns them into real steam engines.
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
limited in supply, or actually flowing and thus laggy.
Steam engines are an alternative to water reactors that actually makes
sense, and hopefully doesn't lag. Also, unlike e.g. animal treadmills,
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
------------
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.
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.
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.
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
boiler gets filled to capacity, all queued jobs are suspended;
once it drops back to 3+1 or 5+1 items, they are re-enabled.
While the engine is providing power, steam is being consumed.
The consumption speed includes a fixed 10% waste rate, and
the remaining 90% are applied proportionally to the actual
load in the machine. With the engine at nominal 300 power with
150 load in the system, it will consume steam for actual
300*(10% + 90%*150/300) = 165 power.
Masterpiece mechanism and chain will decrease the mechanical
power drawn by the engine itself from 10 to 5. Masterpiece
barrel decreases waste rate by 4%. Masterpiece piston and pipe
decrease it by further 4%, and also decrease the whole steam
use rate by 10%.
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
eventually the engine explodes. It should also explode if
toppled during operation by a building destroyer, or a
tantruming dwarf.
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
to them, or machines they connect to (including by pulling levers),
can easily result in inconsistent state once this plugin is
available again. The effects may be as weird as negative power
being generated.
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.
To be really useful this needs patches from bug 808 and ``tweak fix-dimensions``.

File diff suppressed because it is too large Load Diff

@ -59,6 +59,9 @@ keybinding add Alt-L@dwarfmode/LookAround gui/liquids
# machine power sensitive pressure plate construction
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
############################
# UI and game logic tweaks #
############################
@ -79,3 +82,11 @@ tweak stable-temp
# capping the rate to no less than 1 degree change per 500 frames
# Note: will also cause stuff to melt faster in magma etc
tweak fast-heat 500
# stop stacked liquid/bar/thread/cloth items from lasting forever
# if used in reactions that use only a fraction of the dimension.
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

@ -886,6 +886,12 @@ static bool items_moveToInventory
return Items::moveToInventory(mc, item, unit, mode, body_part);
}
static bool items_remove(df::item *item, bool no_uncat)
{
MapExtras::MapCache mc;
return Items::remove(mc, item, no_uncat);
}
static df::proj_itemst *items_makeProjectile(df::item *item)
{
MapExtras::MapCache mc;
@ -904,6 +910,7 @@ static const LuaWrapper::FunctionReg dfhack_items_module[] = {
WRAPN(moveToBuilding, items_moveToBuilding),
WRAPN(moveToInventory, items_moveToInventory),
WRAPN(makeProjectile, items_makeProjectile),
WRAPN(remove, items_remove),
{ NULL, NULL }
};
@ -1077,7 +1084,9 @@ static int buildings_getCorrectSize(lua_State *state)
return 5;
}
static int buildings_setSize(lua_State *state)
namespace {
int buildings_setSize(lua_State *state)
{
auto bld = Lua::CheckDFObject<df::building>(state, 1);
df::coord2d size(luaL_optint(state, 2, 1), luaL_optint(state, 3, 1));
@ -1098,11 +1107,13 @@ static int buildings_setSize(lua_State *state)
return 1;
}
}
static const luaL_Reg dfhack_buildings_funcs[] = {
{ "findAtTile", buildings_findAtTile },
{ "findCivzonesAt", buildings_findCivzonesAt },
{ "getCorrectSize", buildings_getCorrectSize },
{ "setSize", buildings_setSize },
{ "setSize", &Lua::CallWithCatchWrapper<buildings_setSize> },
{ NULL, NULL }
};

@ -438,6 +438,8 @@ void VMethodInterposeLinkBase::remove()
if (next)
prev->child_next.insert(next);
else
prev->child_hosts.insert(host);
}
}

@ -85,7 +85,7 @@ namespace df {
static const bool is_method = true; \
};
#define INSTANTIATE_WRAPPERS(Count, FArgs, Args, Loads) \
#define INSTANTIATE_WRAPPERS2(Count, FArgs, Args, Loads) \
template<FW_TARGS> struct function_wrapper<void (*) FArgs, true> { \
static const int num_args = Count; \
static void execute(lua_State *state, int base, void (*cb) FArgs) { Loads; INVOKE_VOID(cb Args); } \
@ -105,79 +105,103 @@ namespace df {
LOAD_CLASS(); Loads; INVOKE_RV((self->*cb) Args); } \
};
#define INSTANTIATE_WRAPPERS(Count, FArgs, OFArgs, Args, OArgs, Loads) \
INSTANTIATE_WRAPPERS2(Count, FArgs, Args, Loads) \
INSTANTIATE_WRAPPERS2(Count, OFArgs, OArgs, LOAD_OSTREAM(out); Loads)
#define FW_TARGSC
#define FW_TARGS
INSTANTIATE_RETURN_TYPE(())
INSTANTIATE_WRAPPERS(0, (), (), ;)
INSTANTIATE_WRAPPERS(0, (OSTREAM_ARG), (out), LOAD_OSTREAM(out);)
INSTANTIATE_WRAPPERS(0, (), (OSTREAM_ARG), (), (out), ;)
#undef FW_TARGS
#undef FW_TARGSC
#define FW_TARGSC FW_TARGS,
#define FW_TARGS class A1
INSTANTIATE_RETURN_TYPE((A1))
INSTANTIATE_WRAPPERS(1, (A1), (vA1), LOAD_ARG(A1);)
INSTANTIATE_WRAPPERS(1, (OSTREAM_ARG,A1), (out,vA1), LOAD_OSTREAM(out); LOAD_ARG(A1);)
INSTANTIATE_WRAPPERS(1, (A1), (OSTREAM_ARG,A1), (vA1), (out,vA1), LOAD_ARG(A1);)
#undef FW_TARGS
#define FW_TARGS class A1, class A2
INSTANTIATE_RETURN_TYPE((A1,A2))
INSTANTIATE_WRAPPERS(2, (A1,A2), (vA1,vA2), LOAD_ARG(A1); LOAD_ARG(A2);)
INSTANTIATE_WRAPPERS(2, (OSTREAM_ARG,A1,A2), (out,vA1,vA2),
LOAD_OSTREAM(out); LOAD_ARG(A1); LOAD_ARG(A2);)
INSTANTIATE_WRAPPERS(2, (A1,A2), (OSTREAM_ARG,A1,A2), (vA1,vA2), (out,vA1,vA2),
LOAD_ARG(A1); LOAD_ARG(A2);)
#undef FW_TARGS
#define FW_TARGS class A1, class A2, class A3
INSTANTIATE_RETURN_TYPE((A1,A2,A3))
INSTANTIATE_WRAPPERS(3, (A1,A2,A3), (vA1,vA2,vA3), LOAD_ARG(A1); LOAD_ARG(A2); LOAD_ARG(A3);)
INSTANTIATE_WRAPPERS(3, (OSTREAM_ARG,A1,A2,A3), (out,vA1,vA2,vA3),
LOAD_OSTREAM(out); LOAD_ARG(A1); LOAD_ARG(A2); LOAD_ARG(A3);)
INSTANTIATE_WRAPPERS(3, (A1,A2,A3), (OSTREAM_ARG,A1,A2,A3), (vA1,vA2,vA3), (out,vA1,vA2,vA3),
LOAD_ARG(A1); LOAD_ARG(A2); LOAD_ARG(A3);)
#undef FW_TARGS
#define FW_TARGS class A1, class A2, class A3, class A4
INSTANTIATE_RETURN_TYPE((A1,A2,A3,A4))
INSTANTIATE_WRAPPERS(4, (A1,A2,A3,A4), (vA1,vA2,vA3,vA4),
INSTANTIATE_WRAPPERS(4, (A1,A2,A3,A4), (OSTREAM_ARG,A1,A2,A3,A4),
(vA1,vA2,vA3,vA4), (out,vA1,vA2,vA3,vA4),
LOAD_ARG(A1); LOAD_ARG(A2); LOAD_ARG(A3); LOAD_ARG(A4);)
INSTANTIATE_WRAPPERS(4, (OSTREAM_ARG,A1,A2,A3,A4), (out,vA1,vA2,vA3,vA4),
LOAD_OSTREAM(out); LOAD_ARG(A1); LOAD_ARG(A2); LOAD_ARG(A3); LOAD_ARG(A4);)
#undef FW_TARGS
#define FW_TARGS class A1, class A2, class A3, class A4, class A5
INSTANTIATE_RETURN_TYPE((A1,A2,A3,A4,A5))
INSTANTIATE_WRAPPERS(5, (A1,A2,A3,A4,A5), (vA1,vA2,vA3,vA4,vA5),
LOAD_ARG(A1); LOAD_ARG(A2); LOAD_ARG(A3); LOAD_ARG(A4); LOAD_ARG(A5);)
INSTANTIATE_WRAPPERS(5, (OSTREAM_ARG,A1,A2,A3,A4,A5), (out,vA1,vA2,vA3,vA4,vA5),
LOAD_OSTREAM(out); LOAD_ARG(A1); LOAD_ARG(A2);
LOAD_ARG(A3); LOAD_ARG(A4); LOAD_ARG(A5);)
INSTANTIATE_WRAPPERS(5, (A1,A2,A3,A4,A5), (OSTREAM_ARG,A1,A2,A3,A4,A5),
(vA1,vA2,vA3,vA4,vA5), (out,vA1,vA2,vA3,vA4,vA5),
LOAD_ARG(A1); LOAD_ARG(A2); LOAD_ARG(A3); LOAD_ARG(A4);
LOAD_ARG(A5);)
#undef FW_TARGS
#define FW_TARGS class A1, class A2, class A3, class A4, class A5, class A6
INSTANTIATE_RETURN_TYPE((A1,A2,A3,A4,A5,A6))
INSTANTIATE_WRAPPERS(6, (A1,A2,A3,A4,A5,A6), (vA1,vA2,vA3,vA4,vA5,vA6),
LOAD_ARG(A1); LOAD_ARG(A2); LOAD_ARG(A3);
LOAD_ARG(A4); LOAD_ARG(A5); LOAD_ARG(A6);)
INSTANTIATE_WRAPPERS(6, (OSTREAM_ARG,A1,A2,A3,A4,A5,A6), (out,vA1,vA2,vA3,vA4,vA5,vA6),
LOAD_OSTREAM(out); LOAD_ARG(A1); LOAD_ARG(A2); LOAD_ARG(A3);
LOAD_ARG(A4); LOAD_ARG(A5); LOAD_ARG(A6);)
INSTANTIATE_WRAPPERS(6, (A1,A2,A3,A4,A5,A6), (OSTREAM_ARG,A1,A2,A3,A4,A5,A6),
(vA1,vA2,vA3,vA4,vA5,vA6), (out,vA1,vA2,vA3,vA4,vA5,vA6),
LOAD_ARG(A1); LOAD_ARG(A2); LOAD_ARG(A3); LOAD_ARG(A4);
LOAD_ARG(A5); LOAD_ARG(A6);)
#undef FW_TARGS
#define FW_TARGS class A1, class A2, class A3, class A4, class A5, class A6, class A7
INSTANTIATE_RETURN_TYPE((A1,A2,A3,A4,A5,A6,A7))
INSTANTIATE_WRAPPERS(7, (A1,A2,A3,A4,A5,A6,A7), (vA1,vA2,vA3,vA4,vA5,vA6,vA7),
LOAD_ARG(A1); LOAD_ARG(A2); LOAD_ARG(A3);
LOAD_ARG(A4); LOAD_ARG(A5); LOAD_ARG(A6);
LOAD_ARG(A7);)
INSTANTIATE_WRAPPERS(7, (OSTREAM_ARG,A1,A2,A3,A4,A5,A6,A7), (out,vA1,vA2,vA3,vA4,vA5,vA6,vA7),
LOAD_OSTREAM(out); LOAD_ARG(A1); LOAD_ARG(A2); LOAD_ARG(A3);
LOAD_ARG(A4); LOAD_ARG(A5); LOAD_ARG(A6); LOAD_ARG(A7);)
INSTANTIATE_WRAPPERS(7, (A1,A2,A3,A4,A5,A6,A7), (OSTREAM_ARG,A1,A2,A3,A4,A5,A6,A7),
(vA1,vA2,vA3,vA4,vA5,vA6,vA7), (out,vA1,vA2,vA3,vA4,vA5,vA6,vA7),
LOAD_ARG(A1); LOAD_ARG(A2); LOAD_ARG(A3); LOAD_ARG(A4);
LOAD_ARG(A5); LOAD_ARG(A6); LOAD_ARG(A7);)
#undef FW_TARGS
#define FW_TARGS class A1, class A2, class A3, class A4, class A5, class A6, class A7, class A8
INSTANTIATE_RETURN_TYPE((A1,A2,A3,A4,A5,A6,A7,A8))
INSTANTIATE_WRAPPERS(8, (A1,A2,A3,A4,A5,A6,A7,A8), (OSTREAM_ARG,A1,A2,A3,A4,A5,A6,A7,A8),
(vA1,vA2,vA3,vA4,vA5,vA6,vA7,vA8), (out,vA1,vA2,vA3,vA4,vA5,vA6,vA7,vA8),
LOAD_ARG(A1); LOAD_ARG(A2); LOAD_ARG(A3); LOAD_ARG(A4);
LOAD_ARG(A5); LOAD_ARG(A6); LOAD_ARG(A7); LOAD_ARG(A8);)
#undef FW_TARGS
#define FW_TARGS class A1, class A2, class A3, class A4, class A5, class A6, class A7, class A8, class A9
INSTANTIATE_RETURN_TYPE((A1,A2,A3,A4,A5,A6,A7,A8,A9))
INSTANTIATE_WRAPPERS(9, (A1,A2,A3,A4,A5,A6,A7,A8,A9),
(OSTREAM_ARG,A1,A2,A3,A4,A5,A6,A7,A8,A9),
(vA1,vA2,vA3,vA4,vA5,vA6,vA7,vA8,vA9),
(out,vA1,vA2,vA3,vA4,vA5,vA6,vA7,vA8,vA9),
LOAD_ARG(A1); LOAD_ARG(A2); LOAD_ARG(A3); LOAD_ARG(A4);
LOAD_ARG(A5); LOAD_ARG(A6); LOAD_ARG(A7); LOAD_ARG(A8);
LOAD_ARG(A9);)
#undef FW_TARGS
#define FW_TARGS class A1, class A2, class A3, class A4, class A5, class A6, class A7, class A8, class A9, class A10
INSTANTIATE_RETURN_TYPE((A1,A2,A3,A4,A5,A6,A7,A8,A9,A10))
INSTANTIATE_WRAPPERS(10, (A1,A2,A3,A4,A5,A6,A7,A8,A9,A10),
(OSTREAM_ARG,A1,A2,A3,A4,A5,A6,A7,A8,A9,A10),
(vA1,vA2,vA3,vA4,vA5,vA6,vA7,vA8,vA9,vA10),
(out,vA1,vA2,vA3,vA4,vA5,vA6,vA7,vA8,vA9,vA10),
LOAD_ARG(A1); LOAD_ARG(A2); LOAD_ARG(A3); LOAD_ARG(A4);
LOAD_ARG(A5); LOAD_ARG(A6); LOAD_ARG(A7); LOAD_ARG(A8);
LOAD_ARG(A9); LOAD_ARG(A10);)
#undef FW_TARGS
#define FW_TARGS class A1, class A2, class A3, class A4, class A5, class A6, class A7, class A8, class A9, class A10, class A11
INSTANTIATE_RETURN_TYPE((A1,A2,A3,A4,A5,A6,A7,A8,A9,A10,A11))
#undef FW_TARGS
#undef FW_TARGSC
#undef INSTANTIATE_WRAPPERS
#undef INSTANTIATE_WRAPPERS2
#undef INVOKE_VOID
#undef INVOKE_RV
#undef LOAD_CLASS

@ -157,6 +157,9 @@ DFHACK_EXPORT bool moveToBuilding(MapExtras::MapCache &mc, df::item *item, df::b
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);
/// Makes the item removed and marked for garbage collection
DFHACK_EXPORT bool remove(MapExtras::MapCache &mc, df::item *item, bool no_uncat = false);
/// Detaches the items from its current location and turns it into a projectile
DFHACK_EXPORT df::proj_itemst *makeProjectile(MapExtras::MapCache &mc, df::item *item);
}

@ -131,6 +131,11 @@ namespace DFHack
bool findPlant(const std::string &token, const std::string &subtoken);
bool findCreature(const std::string &token, const std::string &subtoken);
bool findProduct(df::material *material, const std::string &name);
bool findProduct(const MaterialInfo &info, const std::string &name) {
return findProduct(info.material, name);
}
std::string getToken();
std::string toString(uint16_t temp = 10015, bool named = true);

@ -0,0 +1,150 @@
-- A trivial reloadable class system
local _ENV = mkmodule('class')
-- Metatable template for a class
class_obj = {} or class_obj
-- Methods shared by all classes
common_methods = {} or common_methods
-- Forbidden names for class fields and methods.
reserved_names = { super = true, ATTRS = true }
-- Attribute table metatable
attrs_meta = {} or attrs_meta
-- Create or updates a class; a class has metamethods and thus own metatable.
function defclass(class,parent)
class = class or {}
local meta = getmetatable(class)
if not meta then
meta = {}
setmetatable(class, meta)
end
for k,v in pairs(class_obj) do meta[k] = v end
meta.__index = parent or common_methods
local attrs = rawget(class, 'ATTRS') or {}
setmetatable(attrs, attrs_meta)
rawset(class, 'super', parent)
rawset(class, 'ATTRS', attrs)
rawset(class, '__index', rawget(class, '__index') or class)
return class
end
-- An instance uses the class as metatable
function mkinstance(class,table)
table = table or {}
setmetatable(table, class)
return table
end
-- Patch the stubs in the global environment
dfhack.BASE_G.defclass = _ENV.defclass
dfhack.BASE_G.mkinstance = _ENV.mkinstance
-- Just verify the name, and then set.
function class_obj:__newindex(name,val)
if reserved_names[name] or common_methods[name] then
error('Method name '..name..' is reserved.')
end
rawset(self, name, val)
end
function attrs_meta:__call(attrs)
for k,v in pairs(attrs) do
self[k] = v
end
end
local function apply_attrs(obj, attrs, init_table)
for k,v in pairs(attrs) do
if v == DEFAULT_NIL then
v = nil
end
obj[k] = init_table[k] or v
end
end
local function invoke_before_rec(self, class, method, ...)
local meta = getmetatable(class)
if meta then
local fun = rawget(class, method)
if fun then
fun(self, ...)
end
invoke_before_rec(self, meta.__index, method, ...)
end
end
local function invoke_after_rec(self, class, method, ...)
local meta = getmetatable(class)
if meta then
invoke_after_rec(self, meta.__index, method, ...)
local fun = rawget(class, method)
if fun then
fun(self, ...)
end
end
end
local function init_attrs_rec(obj, class, init_table)
local meta = getmetatable(class)
if meta then
init_attrs_rec(obj, meta.__index, init_table)
apply_attrs(obj, rawget(class, 'ATTRS'), init_table)
end
end
-- Call metamethod constructs the object
function class_obj:__call(init_table)
-- The table is assumed to be a scratch temporary.
-- If it is not, copy it yourself before calling.
init_table = init_table or {}
local obj = mkinstance(self)
-- This initialization sequence is broadly based on how the
-- Common Lisp initialize-instance generic function works.
-- preinit screens input arguments in subclass to superclass order
invoke_before_rec(obj, self, 'preinit', init_table)
-- initialize the instance table from init table
init_attrs_rec(obj, self, init_table)
-- init in superclass -> subclass
invoke_after_rec(obj, self, 'init', init_table)
-- postinit in superclass -> subclass
invoke_after_rec(obj, self, 'postinit', init_table)
return obj
end
-- Common methods for all instances:
function common_methods:callback(method, ...)
return dfhack.curry(self[method], self, ...)
end
function common_methods:assign(data)
for k,v in pairs(data) do
self[k] = v
end
end
function common_methods:invoke_before(method, ...)
return invoke_before_rec(self, getmetatable(self), method, ...)
end
function common_methods:invoke_after(method, ...)
return invoke_after_rec(self, getmetatable(self), method, ...)
end
return _ENV

@ -113,26 +113,14 @@ function rawset_default(target,source)
end
end
function defclass(class,parent)
class = class or {}
rawset_default(class, { __index = class })
if parent then
setmetatable(class, parent)
else
rawset_default(class, {
init_fields = rawset_default,
callback = function(self, name, ...)
return dfhack.curry(self[name], self, ...)
end
})
end
return class
DEFAULT_NIL = DEFAULT_NIL or {} -- Unique token
function defclass(...)
return require('class').defclass(...)
end
function mkinstance(class,table)
table = table or {}
setmetatable(table, class)
return table
function mkinstance(...)
return require('class').mkinstance(...)
end
-- Misc functions

@ -74,18 +74,23 @@ end
Painter = defclass(Painter, nil)
function Painter.new(rect, pen)
rect = rect or mkdims_wh(0,0,dscreen.getWindowSize())
local self = {
x1 = rect.x1, clip_x1 = rect.x1,
y1 = rect.y1, clip_y1 = rect.y1,
x2 = rect.x2, clip_x2 = rect.x2,
y2 = rect.y2, clip_y2 = rect.y2,
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, pen or COLOR_GREY)
cur_pen = to_pen(nil, args.pen or COLOR_GREY)
}
return mkinstance(Painter, self):seek(0,0)
end
function Painter.new(rect, pen)
return Painter{ rect = rect, pen = pen }
end
function Painter:isValidPos()
@ -213,9 +218,8 @@ Screen = defclass(Screen)
Screen.text_input_mode = false
function Screen:init()
function Screen:postinit()
self:updateLayout()
return self
end
Screen.isDismissed = dscreen.isDismissed
@ -344,7 +348,12 @@ end
FramedScreen = defclass(FramedScreen, Screen)
FramedScreen.frame_style = BOUNDARY_FRAME
FramedScreen.ATTRS{
frame_style = BOUNDARY_FRAME,
frame_title = DEFAULT_NIL,
frame_width = DEFAULT_NIL,
frame_height = DEFAULT_NIL,
}
local function hint_coord(gap,hint)
if hint and hint > 0 then

@ -10,24 +10,21 @@ local dscreen = dfhack.screen
MessageBox = defclass(MessageBox, gui.FramedScreen)
MessageBox.focus_path = 'MessageBox'
MessageBox.frame_style = gui.GREY_LINE_FRAME
function MessageBox:init(info)
info = info or {}
self:init_fields{
text = info.text or {},
frame_title = info.title,
frame_width = info.frame_width,
on_accept = info.on_accept,
on_cancel = info.on_cancel,
on_close = info.on_close,
text_pen = info.text_pen
}
if type(self.text) == 'string' then
self.text = utils.split_string(self.text, "\n")
MessageBox.ATTRS{
frame_style = gui.GREY_LINE_FRAME,
-- 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
gui.FramedScreen.init(self, info)
return self
end
function MessageBox:getWantedFrameSize()
@ -82,9 +79,8 @@ function MessageBox:onInput(keys)
end
function showMessage(title, text, tcolor, on_close)
mkinstance(MessageBox):init{
text = text,
title = title,
MessageBox{
frame_title = title,
text = text,
text_pen = tcolor,
on_close = on_close
@ -92,8 +88,8 @@ function showMessage(title, text, tcolor, on_close)
end
function showYesNoPrompt(title, text, tcolor, on_accept, on_cancel)
mkinstance(MessageBox):init{
title = title,
MessageBox{
frame_title = title,
text = text,
text_pen = tcolor,
on_accept = on_accept,
@ -105,25 +101,23 @@ InputBox = defclass(InputBox, MessageBox)
InputBox.focus_path = 'InputBox'
function InputBox:init(info)
info = info or {}
self:init_fields{
input = info.input or '',
input_pen = info.input_pen,
on_input = info.on_input,
}
MessageBox.init(self, info)
self.on_accept = nil
return self
InputBox.ATTRS{
input = '',
input_pen = DEFAULT_NIL,
on_input = DEFAULT_NIL,
}
function InputBox:preinit(info)
info.on_accept = nil
end
function InputBox:getWantedFrameSize()
local mw, mh = MessageBox.getWantedFrameSize(self)
local mw, mh = InputBox.super.getWantedFrameSize(self)
return mw, mh+2
end
function InputBox:onRenderBody(dc)
MessageBox.onRenderBody(self, dc)
InputBox.super.onRenderBody(self, dc)
dc:newline(1)
dc:pen(self.input_pen or COLOR_LIGHTCYAN)
@ -161,8 +155,8 @@ function InputBox:onInput(keys)
end
function showInputPrompt(title, text, tcolor, input, on_input, on_cancel, min_width)
mkinstance(InputBox):init{
title = title,
InputBox{
frame_title = title,
text = text,
text_pen = tcolor,
input = input,
@ -176,27 +170,28 @@ ListBox = defclass(ListBox, MessageBox)
ListBox.focus_path = 'ListBox'
ListBox.ATTRS{
selection = 0,
choices = {},
select_pen = DEFAULT_NIL,
on_input = DEFAULT_NIL
}
function InputBox:preinit(info)
info.on_accept = nil
end
function ListBox:init(info)
info = info or {}
self:init_fields{
selection = info.selection or 0,
choices = info.choices or {},
select_pen = info.select_pen,
on_input = info.on_input,
page_top = 0
}
MessageBox.init(self, info)
self.on_accept = nil
return self
self.page_top = 0
end
function ListBox:getWantedFrameSize()
local mw, mh = MessageBox.getWantedFrameSize(self)
local mw, mh = ListBox.super.getWantedFrameSize(self)
return mw, mh+#self.choices
end
function ListBox:onRenderBody(dc)
MessageBox.onRenderBody(self, dc)
ListBox.super.onRenderBody(self, dc)
dc:newline(1)
@ -220,6 +215,7 @@ function ListBox:onRenderBody(dc)
end
end
end
function ListBox:moveCursor(delta)
local newsel=self.selection+delta
if #self.choices ~=0 then
@ -229,6 +225,7 @@ function ListBox:moveCursor(delta)
end
self.selection=newsel
end
function ListBox:onInput(keys)
if keys.SELECT then
self:dismiss()
@ -257,8 +254,8 @@ function ListBox:onInput(keys)
end
function showListPrompt(title, text, tcolor, choices, on_input, on_cancel, min_width)
mkinstance(ListBox):init{
title = title,
ListBox{
frame_title = title,
text = text,
text_pen = tcolor,
choices = choices,

@ -353,7 +353,7 @@ end
MenuOverlay = defclass(MenuOverlay, DwarfOverlay)
function MenuOverlay:updateLayout()
DwarfOverlay.updateLayout(self)
MenuOverlay.super.updateLayout(self)
self.frame_rect = self.df_layout.menu
end
@ -361,7 +361,7 @@ MenuOverlay.getWindowSize = gui.FramedScreen.getWindowSize
MenuOverlay.getMousePos = gui.FramedScreen.getMousePos
function MenuOverlay:onAboutToShow(below)
DwarfOverlay.onAboutToShow(self,below)
MenuOverlay.super.onAboutToShow(self,below)
self:updateLayout()
if not self.df_layout.menu then

@ -54,7 +54,7 @@ using namespace DFHack;
#include "df/viewscreen_joblistst.h"
#include "df/viewscreen_unitlistst.h"
#include "df/viewscreen_itemst.h"
#include "df/viewscreen_layerst.h"
#include "df/viewscreen_layer.h"
#include "df/viewscreen_layer_workshop_profilest.h"
#include "df/viewscreen_layer_noblelistst.h"
#include "df/viewscreen_layer_overall_healthst.h"
@ -95,7 +95,7 @@ using df::global::selection_rect;
using df::global::ui_menu_width;
using df::global::ui_area_map_width;
static df::layer_object_listst *getLayerList(df::viewscreen_layerst *layer, int idx)
static df::layer_object_listst *getLayerList(df::viewscreen_layer *layer, int idx)
{
return virtual_cast<df::layer_object_listst>(vector_get(layer->layer_objects,idx));
}
@ -332,9 +332,9 @@ DEFINE_GET_FOCUS_STRING_HANDLER(layer_military)
focus += "/" + enum_item_key(screen->page);
int cur_list;
if (list1->bright) cur_list = 0;
else if (list2->bright) cur_list = 1;
else if (list3->bright) cur_list = 2;
if (list1->active) cur_list = 0;
else if (list2->active) cur_list = 1;
else if (list3->active) cur_list = 2;
else return;
switch (screen->page)
@ -420,7 +420,7 @@ DEFINE_GET_FOCUS_STRING_HANDLER(layer_assigntrade)
if (unsigned(list_idx) >= num_lists)
return;
if (list1->bright)
if (list1->active)
focus += "/Groups";
else
focus += "/Items";
@ -458,10 +458,10 @@ DEFINE_GET_FOCUS_STRING_HANDLER(layer_stockpile)
focus += "/On";
if (list2->bright || list3->bright || screen->list_ids.empty()) {
if (list2->active || list3->active || screen->list_ids.empty()) {
focus += "/" + enum_item_key(screen->cur_list);
if (list3->bright)
if (list3->active)
focus += (screen->item_names.empty() ? "/None" : "/Item");
}
}
@ -844,7 +844,7 @@ static df::item *getAnyItem(df::viewscreen *top)
{
auto list1 = getLayerList(screen, 0);
auto list2 = getLayerList(screen, 1);
if (!list1 || !list2 || !list2->bright)
if (!list1 || !list2 || !list2->active)
return NULL;
int list_idx = vector_get(screen->visible_lists, list1->cursor, (int16_t)-1);

@ -730,6 +730,18 @@ static bool detachItem(MapExtras::MapCache &mc, df::item *item)
item->flags.bits.in_inventory = false;
return true;
}
else if (item->flags.bits.removed)
{
item->flags.bits.removed = false;
if (item->flags.bits.garbage_collect)
{
item->flags.bits.garbage_collect = false;
item->categorize(true);
}
return true;
}
else
return false;
}
@ -871,6 +883,26 @@ bool DFHack::Items::moveToInventory(
return true;
}
bool Items::remove(MapExtras::MapCache &mc, df::item *item, bool no_uncat)
{
CHECK_NULL_POINTER(item);
auto pos = getPosition(item);
if (!detachItem(mc, item))
return false;
if (pos.isValid())
item->pos = pos;
if (!no_uncat)
item->uncategorize();
item->flags.bits.removed = true;
item->flags.bits.garbage_collect = !no_uncat;
return true;
}
df::proj_itemst *Items::makeProjectile(MapExtras::MapCache &mc, df::item *item)
{
CHECK_NULL_POINTER(item);

@ -283,6 +283,19 @@ bool MaterialInfo::findCreature(const std::string &token, const std::string &sub
return decode(-1);
}
bool MaterialInfo::findProduct(df::material *material, const std::string &name)
{
if (!material || name.empty())
return decode(-1);
auto &pids = material->reaction_product.id;
for (size_t i = 0; i < pids.size(); i++)
if ((*pids[i]) == name)
return decode(material->reaction_product.material, i);
return decode(-1);
}
std::string MaterialInfo::getToken()
{
if (isNone())

@ -1 +1 @@
Subproject commit 2bc8fbdf71143398817d31e06e169a01cce37c50
Subproject commit 8a78bfa218817765b0a80431e0cf25435ffb2179

@ -47,6 +47,9 @@ install(DIRECTORY lua/
install(DIRECTORY raw/
DESTINATION ${DFHACK_DATA_DESTINATION}/raw
FILES_MATCHING PATTERN "*.txt")
install(DIRECTORY raw/
DESTINATION ${DFHACK_DATA_DESTINATION}/raw
FILES_MATCHING PATTERN "*.diff")
# Protobuf
FILE(GLOB PROJECT_PROTOS ${CMAKE_CURRENT_SOURCE_DIR}/proto/*.proto)
@ -119,6 +122,8 @@ if (BUILD_SUPPORTED)
DFHACK_PLUGIN(sort sort.cpp LINK_LIBRARIES lua)
DFHACK_PLUGIN(steam-engine steam-engine.cpp)
DFHACK_PLUGIN(power-meter power-meter.cpp LINK_LIBRARIES lua)
DFHACK_PLUGIN(siege-engine siege-engine.cpp LINK_LIBRARIES lua)
DFHACK_PLUGIN(add-spatter add-spatter.cpp)
# not yet. busy with other crud again...
#DFHACK_PLUGIN(versionosd versionosd.cpp)
endif()

@ -0,0 +1,433 @@
#include "Core.h"
#include <Console.h>
#include <Export.h>
#include <PluginManager.h>
#include <modules/Gui.h>
#include <modules/Screen.h>
#include <modules/Maps.h>
#include <modules/Job.h>
#include <modules/Items.h>
#include <modules/Units.h>
#include <TileTypes.h>
#include <vector>
#include <cstdio>
#include <stack>
#include <string>
#include <cmath>
#include <string.h>
#include <VTableInterpose.h>
#include "df/item_liquid_miscst.h"
#include "df/item_constructed.h"
#include "df/builtin_mats.h"
#include "df/world.h"
#include "df/job.h"
#include "df/job_item.h"
#include "df/job_item_ref.h"
#include "df/ui.h"
#include "df/report.h"
#include "df/reaction.h"
#include "df/reaction_reagent_itemst.h"
#include "df/reaction_product_item_improvementst.h"
#include "df/reaction_product_improvement_flags.h"
#include "df/matter_state.h"
#include "df/contaminant.h"
#include "MiscUtils.h"
using std::vector;
using std::string;
using std::stack;
using namespace DFHack;
using namespace df::enums;
using df::global::gps;
using df::global::world;
using df::global::ui;
typedef df::reaction_product_item_improvementst improvement_product;
DFHACK_PLUGIN("add-spatter");
struct ReagentSource {
int idx;
df::reaction_reagent *reagent;
ReagentSource() : idx(-1), reagent(NULL) {}
};
struct MaterialSource : ReagentSource {
bool product;
std::string product_name;
int mat_type, mat_index;
MaterialSource() : product(false), mat_type(-1), mat_index(-1) {}
};
struct ProductInfo {
df::reaction *react;
improvement_product *product;
ReagentSource object;
MaterialSource material;
bool isValid() {
return object.reagent && (material.mat_type >= 0 || material.reagent);
}
};
struct ReactionInfo {
df::reaction *react;
std::vector<ProductInfo> products;
};
static std::map<std::string, ReactionInfo> reactions;
static std::map<df::reaction_product*, ProductInfo*> products;
static ReactionInfo *find_reaction(const std::string &name)
{
auto it = reactions.find(name);
return (it != reactions.end()) ? &it->second : NULL;
}
static bool is_add_spatter(const std::string &name)
{
return name.size() > 12 && memcmp(name.data(), "SPATTER_ADD_", 12) == 0;
}
static void find_material(int *type, int *index, df::item *input, MaterialSource &mat)
{
if (input && mat.reagent)
{
MaterialInfo info(input);
if (mat.product)
{
if (!info.findProduct(info, mat.product_name))
{
color_ostream_proxy out(Core::getInstance().getConsole());
out.printerr("Cannot find product '%s'\n", mat.product_name.c_str());
}
}
*type = info.type;
*index = info.index;
}
else
{
*type = mat.mat_type;
*index = mat.mat_index;
}
}
static int has_contaminant(df::item_actual *item, int type, int index)
{
auto cont = item->contaminants;
if (!cont)
return 0;
int size = 0;
for (size_t i = 0; i < cont->size(); i++)
{
auto cur = (*cont)[i];
if (cur->mat_type == type && cur->mat_index == index)
size += cur->size;
}
return size;
}
/*
* Hooks
*/
typedef std::map<int, std::vector<df::item*> > item_table;
static void index_items(item_table &table, df::job *job, ReactionInfo *info)
{
for (int i = job->items.size()-1; i >= 0; i--)
{
auto iref = job->items[i];
if (iref->job_item_idx < 0) continue;
auto iitem = job->job_items[iref->job_item_idx];
if (iitem->contains.empty())
{
table[iitem->reagent_index].push_back(iref->item);
}
else
{
std::vector<df::item*> contents;
Items::getContainedItems(iref->item, &contents);
for (int j = contents.size()-1; j >= 0; j--)
{
for (int k = iitem->contains.size()-1; k >= 0; k--)
{
int ridx = iitem->contains[k];
auto reag = info->react->reagents[ridx];
if (reag->matchesChild(contents[j], info->react, iitem->reaction_id))
table[ridx].push_back(contents[j]);
}
}
}
}
}
df::item* find_item(ReagentSource &info, item_table &table)
{
if (!info.reagent)
return NULL;
if (table[info.idx].empty())
return NULL;
return table[info.idx].back();
}
struct item_hook : df::item_constructed {
typedef df::item_constructed interpose_base;
DEFINE_VMETHOD_INTERPOSE(bool, isImprovable, (df::job *job, int16_t mat_type, int32_t mat_index))
{
ReactionInfo *info;
if (job && job->job_type == job_type::CustomReaction &&
(info = find_reaction(job->reaction_name)) != NULL)
{
if (!contaminants || contaminants->empty())
return true;
item_table table;
index_items(table, job, info);
for (size_t i = 0; i < info->products.size(); i++)
{
auto &product = info->products[i];
int mattype, matindex;
auto material = find_item(info->products[i].material, table);
find_material(&mattype, &matindex, material, product.material);
if (mattype < 0 || has_contaminant(this, mattype, matindex) >= 50)
return false;
}
return true;
}
return INTERPOSE_NEXT(isImprovable)(job, mat_type, mat_index);
}
};
IMPLEMENT_VMETHOD_INTERPOSE(item_hook, isImprovable);
df::item* find_item(
ReagentSource &info,
std::vector<df::reaction_reagent*> *in_reag,
std::vector<df::item*> *in_items
) {
if (!info.reagent)
return NULL;
for (int i = in_items->size(); i >= 0; i--)
if ((*in_reag)[i] == info.reagent)
return (*in_items)[i];
return NULL;
}
struct product_hook : improvement_product {
typedef improvement_product interpose_base;
DEFINE_VMETHOD_INTERPOSE(
void, produce,
(df::unit *unit, std::vector<df::item*> *out_items,
std::vector<df::reaction_reagent*> *in_reag,
std::vector<df::item*> *in_items,
int32_t quantity, int16_t skill,
df::historical_entity *entity, df::world_site *site)
) {
if (auto product = products[this])
{
auto object = find_item(product->object, in_reag, in_items);
auto material = find_item(product->material, in_reag, in_items);
if (object && (material || !product->material.reagent))
{
using namespace df::enums::improvement_type;
int mattype, matindex;
find_material(&mattype, &matindex, material, product->material);
df::matter_state state = matter_state::Liquid;
switch (improvement_type)
{
case COVERED:
if (flags.is_set(reaction_product_improvement_flags::GLAZED))
state = matter_state::Solid;
break;
case BANDS:
state = matter_state::Paste;
break;
case SPIKES:
state = matter_state::Powder;
break;
default:
break;
}
int rating = unit ? Units::getEffectiveSkill(unit, df::job_skill(skill)) : 0;
int size = int(probability*(1.0f + 0.06f*rating)); // +90% at legendary
object->addContaminant(
mattype, matindex, state,
object->getTemperature(),
size, -1,
0x8000 // not washed by water, and 'clean items' safe.
);
}
return;
}
INTERPOSE_NEXT(produce)(unit, out_items, in_reag, in_items, quantity, skill, entity, site);
}
};
IMPLEMENT_VMETHOD_INTERPOSE(product_hook, produce);
/*
* Scan raws for matching reactions.
*/
static void find_reagent(
color_ostream &out, ReagentSource &info, df::reaction *react, std::string name
) {
for (size_t i = 0; i < react->reagents.size(); i++)
{
if (react->reagents[i]->code != name)
continue;
info.idx = i;
info.reagent = react->reagents[i];
return;
}
out.printerr("Invalid reagent name '%s' in '%s'\n", name.c_str(), react->code.c_str());
}
static void parse_product(
color_ostream &out, ProductInfo &info, df::reaction *react, improvement_product *prod
) {
using namespace df::enums::reaction_product_improvement_flags;
info.react = react;
info.product = prod;
find_reagent(out, info.object, react, prod->target_reagent);
auto ritem = strict_virtual_cast<df::reaction_reagent_itemst>(info.object.reagent);
if (ritem)
ritem->flags1.bits.improvable = true;
info.material.mat_type = prod->mat_type;
info.material.mat_index = prod->mat_index;
if (prod->flags.is_set(GET_MATERIAL_PRODUCT))
{
find_reagent(out, info.material, react, prod->get_material.reagent_code);
info.material.product = true;
info.material.product_name = prod->get_material.product_code;
}
else if (prod->flags.is_set(GET_MATERIAL_SAME))
{
find_reagent(out, info.material, react, prod->get_material.reagent_code);
}
}
static bool find_reactions(color_ostream &out)
{
reactions.clear();
products.clear();
auto &rlist = world->raws.reactions;
for (size_t i = 0; i < rlist.size(); i++)
{
if (!is_add_spatter(rlist[i]->code))
continue;
reactions[rlist[i]->code].react = rlist[i];
}
for (auto it = reactions.begin(); it != reactions.end(); ++it)
{
auto &prod = it->second.react->products;
auto &out_prod = it->second.products;
for (size_t i = 0; i < prod.size(); i++)
{
auto itprod = strict_virtual_cast<improvement_product>(prod[i]);
if (!itprod) continue;
out_prod.push_back(ProductInfo());
parse_product(out, out_prod.back(), it->second.react, itprod);
}
for (size_t i = 0; i < prod.size(); i++)
{
if (out_prod[i].isValid())
products[out_prod[i].product] = &out_prod[i];
}
}
return !products.empty();
}
static void enable_hooks(bool enable)
{
INTERPOSE_HOOK(item_hook, isImprovable).apply(enable);
INTERPOSE_HOOK(product_hook, produce).apply(enable);
}
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event)
{
switch (event) {
case SC_MAP_LOADED:
if (find_reactions(out))
{
out.print("Detected spatter add reactions - enabling plugin.\n");
enable_hooks(true);
}
else
enable_hooks(false);
break;
case SC_MAP_UNLOADED:
enable_hooks(false);
reactions.clear();
products.clear();
break;
default:
break;
}
return CR_OK;
}
DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <PluginCommand> &commands)
{
if (Core::getInstance().isMapLoaded())
plugin_onstatechange(out, SC_MAP_LOADED);
return CR_OK;
}
DFhackCExport command_result plugin_shutdown ( color_ostream &out )
{
enable_hooks(false);
return CR_OK;
}

@ -81,11 +81,18 @@ command_result cleanitems (color_ostream &out)
df::item_actual *item = (df::item_actual *)world->items.all[i];
if (item->contaminants && item->contaminants->size())
{
std::vector<df::contaminant*> saved;
for (size_t j = 0; j < item->contaminants->size(); j++)
delete item->contaminants->at(j);
{
auto obj = (*item->contaminants)[j];
if (obj->flags.whole & 0x8000) // DFHack-generated contaminant
saved.push_back(obj);
else
delete obj;
}
cleaned_items++;
cleaned_total += item->contaminants->size();
item->contaminants->clear();
cleaned_total += item->contaminants->size() - saved.size();
item->contaminants->swap(saved);
}
}
if (cleaned_total)

@ -18,7 +18,6 @@ DFHACK_PLUGIN(stripcaged stripcaged.cpp)
DFHACK_PLUGIN(rprobe rprobe.cpp)
DFHACK_PLUGIN(nestboxes nestboxes.cpp)
DFHACK_PLUGIN(vshook vshook.cpp)
DFHACK_PLUGIN(siege-engine siege-engine.cpp LINK_LIBRARIES lua)
IF(UNIX)
DFHACK_PLUGIN(ref-index ref-index.cpp)
ENDIF()

@ -11,6 +11,7 @@
#include "df/matter_state.h"
#include "df/descriptor_color.h"
#include "df/item_type.h"
#include "df/strain_type.h"
using std::string;
using std::vector;
@ -195,47 +196,17 @@ command_result df_dumpmats (color_ostream &out, vector<string> &parameters)
if (mat->molar_mass != 0xFBBC7818)
out.print("\t[MOLAR_MASS:%i]\n", mat->molar_mass);
if (mat->strength.impact_yield != 10000)
out.print("\t[IMPACT_YIELD:%i]\n", mat->strength.impact_yield);
if (mat->strength.impact_fracture != 10000)
out.print("\t[IMPACT_FRACTURE:%i]\n", mat->strength.impact_fracture);
if (mat->strength.impact_strain_at_yield != 0)
out.print("\t[IMPACT_STRAIN_AT_YIELD:%i]\n", mat->strength.impact_strain_at_yield);
if (mat->strength.compressive_yield != 10000)
out.print("\t[COMPRESSIVE_YIELD:%i]\n", mat->strength.compressive_yield);
if (mat->strength.compressive_fracture != 10000)
out.print("\t[COMPRESSIVE_FRACTURE:%i]\n", mat->strength.compressive_fracture);
if (mat->strength.compressive_strain_at_yield != 0)
out.print("\t[COMPRESSIVE_STRAIN_AT_YIELD:%i]\n", mat->strength.compressive_strain_at_yield);
if (mat->strength.tensile_yield != 10000)
out.print("\t[TENSILE_YIELD:%i]\n", mat->strength.tensile_yield);
if (mat->strength.tensile_fracture != 10000)
out.print("\t[TENSILE_FRACTURE:%i]\n", mat->strength.tensile_fracture);
if (mat->strength.tensile_strain_at_yield != 0)
out.print("\t[TENSILE_STRAIN_AT_YIELD:%i]\n", mat->strength.tensile_strain_at_yield);
if (mat->strength.torsion_yield != 10000)
out.print("\t[TORSION_YIELD:%i]\n", mat->strength.torsion_yield);
if (mat->strength.torsion_fracture != 10000)
out.print("\t[TORSION_FRACTURE:%i]\n", mat->strength.torsion_fracture);
if (mat->strength.torsion_strain_at_yield != 0)
out.print("\t[TORSION_STRAIN_AT_YIELD:%i]\n", mat->strength.torsion_strain_at_yield);
if (mat->strength.shear_yield != 10000)
out.print("\t[SHEAR_YIELD:%i]\n", mat->strength.shear_yield);
if (mat->strength.shear_fracture != 10000)
out.print("\t[SHEAR_FRACTURE:%i]\n", mat->strength.shear_fracture);
if (mat->strength.shear_strain_at_yield != 0)
out.print("\t[SHEAR_STRAIN_AT_YIELD:%i]\n", mat->strength.shear_strain_at_yield);
if (mat->strength.bending_yield != 10000)
out.print("\t[BENDING_YIELD:%i]\n", mat->strength.bending_yield);
if (mat->strength.bending_fracture != 10000)
out.print("\t[BENDING_FRACTURE:%i]\n", mat->strength.bending_fracture);
if (mat->strength.bending_strain_at_yield != 0)
out.print("\t[BENDING_STRAIN_AT_YIELD:%i]\n", mat->strength.bending_strain_at_yield);
FOR_ENUM_ITEMS(strain_type, strain)
{
auto name = ENUM_KEY_STR(strain_type,strain);
if (mat->strength.yield[strain] != 10000)
out.print("\t[%s_YIELD:%i]\n", name.c_str(), mat->strength.yield[strain]);
if (mat->strength.fracture[strain] != 10000)
out.print("\t[%s_FRACTURE:%i]\n", name.c_str(), mat->strength.fracture[strain]);
if (mat->strength.strain_at_yield[strain] != 0)
out.print("\t[%s_STRAIN_AT_YIELD:%i]\n", name.c_str(), mat->strength.strain_at_yield[strain]);
}
if (mat->strength.max_edge != 0)
out.print("\t[MAX_EDGE:%i]\n", mat->strength.max_edge);

@ -31,6 +31,40 @@ static command_result nestboxes(color_ostream &out, vector <string> & parameters
DFHACK_PLUGIN("nestboxes");
static bool enabled = false;
static void eggscan(color_ostream &out)
{
CoreSuspender suspend;
for (int i = 0; i < world->buildings.all.size(); ++i)
{
df::building *build = world->buildings.all[i];
auto type = build->getType();
if (df::enums::building_type::NestBox == type)
{
bool fertile = false;
df::building_nest_boxst *nb = virtual_cast<df::building_nest_boxst>(build);
if (nb->claimed_by != -1)
{
df::unit* u = df::unit::find(nb->claimed_by);
if (u && u->relations.pregnancy_timer > 0)
fertile = true;
}
for (int j = 1; j < nb->contained_items.size(); j++)
{
df::item* item = nb->contained_items[j]->item;
if (item->flags.bits.forbid != fertile)
{
item->flags.bits.forbid = fertile;
out << item->getStackSize() << " eggs " << (fertile ? "forbidden" : "unforbidden.") << endl;
}
}
}
}
}
DFhackCExport command_result plugin_init (color_ostream &out, std::vector <PluginCommand> &commands)
{
if (world && ui) {
@ -49,6 +83,19 @@ DFhackCExport command_result plugin_shutdown ( color_ostream &out )
return CR_OK;
}
DFhackCExport command_result plugin_onupdate(color_ostream &out)
{
if (!enabled)
return CR_OK;
static unsigned cnt = 0;
if ((++cnt % 5) != 0)
return CR_OK;
eggscan(out);
return CR_OK;
}
static command_result nestboxes(color_ostream &out, vector <string> & parameters)
{
@ -57,60 +104,16 @@ static command_result nestboxes(color_ostream &out, vector <string> & parameters
int dump_count = 0;
int good_egg = 0;
if (parameters.size() == 1 && parameters[0] == "clean")
{
clean = true;
}
for (int i = 0; i < world->buildings.all.size(); ++i)
{
df::building *build = world->buildings.all[i];
auto type = build->getType();
if (df::enums::building_type::NestBox == type)
{
bool needs_clean = false;
df::building_nest_boxst *nb = virtual_cast<df::building_nest_boxst>(build);
out << "Nestbox at (" << nb->x1 << "," << nb->y1 << ","<< nb->z << "): claimed-by " << nb->claimed_by << ", contained item count " << nb->contained_items.size() << " (" << nb->anon_1 << ")" << endl;
if (nb->contained_items.size() > 1)
needs_clean = true;
if (nb->claimed_by != -1)
{
df::unit* u = df::unit::find(nb->claimed_by);
if (u)
{
out << " Claimed by ";
if (u->name.has_name)
out << u->name.first_name << ", ";
df::creature_raw *raw = df::global::world->raws.creatures.all[u->race];
out << raw->creature_id
<< ", pregnancy timer " << u->relations.pregnancy_timer << endl;
if (u->relations.pregnancy_timer > 0)
needs_clean = false;
}
}
for (int j = 1; j < nb->contained_items.size(); j++)
{
df::item* item = nb->contained_items[j]->item;
if (needs_clean) {
if (clean && !item->flags.bits.dump)
{
item->flags.bits.dump = 1;
dump_count += item->getStackSize();
}
} else {
good_egg += item->getStackSize();
}
}
}
}
if (clean)
{
out << dump_count << " eggs dumped." << endl;
}
out << good_egg << " fertile eggs found." << endl;
if (parameters.size() == 1) {
if (parameters[0] == "enable")
enabled = true;
else if (parameters[0] == "disable")
enabled = false;
else
return CR_WRONG_USAGE;
} else {
out << "Plugin " << (enabled ? "enabled" : "disabled") << "." << endl;
}
return CR_OK;
}

@ -8,37 +8,221 @@ local _ENV = mkmodule('plugins.siege-engine')
* clearTargetArea(building)
* setTargetArea(building, point1, point2) -> true/false
--]]
* isLinkedToPile(building,pile) -> true/false
* getStockpileLinks(building) -> {pile}
* addStockpileLink(building,pile) -> true/false
* removeStockpileLink(building,pile) -> true/false
* saveWorkshopProfile(building) -> profile
* getAmmoItem(building) -> item_type
* setAmmoItem(building,item_type) -> true/false
* isPassableTile(pos) -> true/false
* isTreeTile(pos) -> true/false
* isTargetableTile(pos) -> true/false
* getTileStatus(building,pos) -> 'invalid/ok/out_of_range/blocked/semiblocked'
* paintAimScreen(building,view_pos_xyz,left_top_xy,size_xy)
* canTargetUnit(unit) -> true/false
proj_info = { target = pos, [delta = float/pos], [factor = int] }
* projPosAtStep(building,proj_info,step) -> pos
* projPathMetrics(building,proj_info) -> {
hit_type = 'wall/floor/ceiling/map_edge/tree',
collision_step = int,
collision_z_step = int,
goal_distance = int,
goal_step = int/nil,
goal_z_step = int/nil,
status = 'ok/out_of_range/blocked'
}
* adjustToTarget(building,pos) -> pos,ok=true/false
* traceUnitPath(unit) -> { {x=int,y=int,z=int[,from=time][,to=time]} }
* unitPosAtTime(unit, time) -> pos
* proposeUnitHits(building) -> { {
pos=pos, unit=unit, time=float, dist=int,
[lmargin=float,] [rmargin=float,]
} }
* computeNearbyWeight(building,hits,{[id/unit]=score}[,fname])
]]
Z_STEP_COUNT = 15
Z_STEP = 1/31
function getMetrics(engine, path)
path.metrics = path.metrics or projPathMetrics(engine, path)
return path.metrics
end
function findShotHeight(engine, target)
local path = { target = target, delta = 0.0 }
if projPathMetrics(engine, path).goal_step then
if getMetrics(engine, path).goal_step then
return path
end
for i = 1,Z_STEP_COUNT do
path.delta = i*Z_STEP
if projPathMetrics(engine, path).goal_step then
return path
local tpath = { target = target, delta = Z_STEP_COUNT*Z_STEP }
if getMetrics(engine, tpath).goal_step then
for i = 1,Z_STEP_COUNT-1 do
path = { target = target, delta = i*Z_STEP }
if getMetrics(engine, path).goal_step then
return path
end
end
return tpath
end
tpath = { target = target, delta = -Z_STEP_COUNT*Z_STEP }
if getMetrics(engine, tpath).goal_step then
for i = 1,Z_STEP_COUNT-1 do
path = { target = target, delta = -i*Z_STEP }
if getMetrics(engine, path).goal_step then
return path
end
end
return tpath
end
end
function findReachableTargets(engine, targets)
local reachable = {}
for _,tgt in ipairs(targets) do
tgt.path = findShotHeight(engine, tgt.pos)
if tgt.path then
table.insert(reachable, tgt)
end
end
return reachable
end
recent_targets = recent_targets or {}
if dfhack.is_core_context then
dfhack.onStateChange[_ENV] = function(code)
if code == SC_MAP_LOADED then
recent_targets = {}
end
end
end
function saveRecent(unit)
local id = unit.id
local tgt = recent_targets
tgt[id] = (tgt[id] or 0) + 1
dfhack.timeout(3, 'days', function()
tgt[id] = math.max(0, tgt[id]-1)
end)
end
function getBaseUnitWeight(unit)
if dfhack.units.isCitizen(unit) then
return -10
elseif unit.flags1.diplomat or unit.flags1.merchant then
return -2
elseif unit.flags1.tame and unit.civ_id == df.global.ui.civ_id then
return -1
else
local rv = 1
if unit.flags1.marauder then rv = rv + 0.5 end
if unit.flags1.active_invader then rv = rv + 1 end
if unit.flags1.invader_origin then rv = rv + 1 end
if unit.flags1.invades then rv = rv + 1 end
if unit.flags1.hidden_ambusher then rv = rv + 1 end
return rv
end
end
function getUnitWeight(unit)
local base = getBaseUnitWeight(unit)
return base * math.pow(0.7, recent_targets[unit.id] or 0)
end
function unitWeightCache()
local cache = {}
return cache, function(unit)
local id = unit.id
cache[id] = cache[id] or getUnitWeight(unit)
return cache[id]
end
end
function scoreTargets(engine, reachable)
local ucache, get_weight = unitWeightCache()
for _,tgt in ipairs(reachable) do
tgt.score = get_weight(tgt.unit)
if tgt.lmargin and tgt.lmargin < 3 then
tgt.score = tgt.score * tgt.lmargin / 3
end
if tgt.rmargin and tgt.rmargin < 3 then
tgt.score = tgt.score * tgt.rmargin / 3
end
end
computeNearbyWeight(engine, reachable, ucache)
for _,tgt in ipairs(reachable) do
tgt.score = (tgt.score + tgt.nearby_weight*0.7) * math.pow(0.995, tgt.time/3)
end
table.sort(reachable, function(a,b)
return a.score > b.score or (a.score == b.score and a.time < b.time)
end)
end
function pickUniqueTargets(reachable)
local unique = {}
path.delta = -i*Z_STEP
if projPathMetrics(engine, path).goal_step then
return path
if #reachable > 0 then
local pos_table = {}
local first_score = reachable[1].score
for i,tgt in ipairs(reachable) do
if tgt.score < 0 or tgt.score < 0.1*first_score then
break
end
local x,y,z = pos2xyz(tgt.pos)
local key = x..':'..y..':'..z
if pos_table[key] then
table.insert(pos_table[key].units, tgt.unit)
else
table.insert(unique, tgt)
pos_table[key] = tgt
tgt.units = { tgt.unit }
end
end
end
return unique
end
function doAimProjectile(engine, item, target_min, target_max, skill)
print(item, df.skill_rating[skill])
local targets = proposeUnitHits(engine)
if #targets > 0 then
local rnd = math.random(#targets)
return findShotHeight(engine, targets[rnd].pos)
local reachable = findReachableTargets(engine, targets)
scoreTargets(engine, reachable)
local unique = pickUniqueTargets(reachable)
if #unique > 0 then
local cnt = math.max(math.min(#unique,5), math.min(10, math.floor(#unique/2)))
local rnd = math.random(cnt)
for _,u in ipairs(unique[rnd].units) do
saveRecent(u)
end
return unique[rnd].path
end
end

@ -528,6 +528,105 @@ void viewscreen_unitlaborsst::feed(set<df::interface_key> *events)
if (first_column < sel_column - col_widths[DISP_COLUMN_LABORS] + 1)
first_column = sel_column - col_widths[DISP_COLUMN_LABORS] + 1;
// handle mouse input
if (enabler->tracking_on && gps->mouse_x != -1 && gps->mouse_y != -1)
{
int click_header = DISP_COLUMN_MAX; // group ID of the column header clicked
int click_body = DISP_COLUMN_MAX; // group ID of the column body clicked
int click_unit = -1; // Index into units[] (-1 if out of range)
int click_labor = -1; // Index into columns[] (-1 if out of range)
for (int i = 0; i < DISP_COLUMN_MAX; i++)
{
if ((gps->mouse_x >= col_offsets[i]) &&
(gps->mouse_x < col_offsets[i] + col_widths[i]))
{
if ((gps->mouse_y >= 1) && (gps->mouse_y <= 2))
click_header = i;
if ((gps->mouse_y >= 4) && (gps->mouse_y <= 4 + num_rows))
click_body = i;
}
}
if ((gps->mouse_x >= col_offsets[DISP_COLUMN_LABORS]) &&
(gps->mouse_x < col_offsets[DISP_COLUMN_LABORS] + col_widths[DISP_COLUMN_LABORS]))
click_labor = gps->mouse_x - col_offsets[DISP_COLUMN_LABORS] + first_column;
if ((gps->mouse_y >= 4) && (gps->mouse_y <= 4 + num_rows))
click_unit = gps->mouse_y - 4 + first_row;
switch (click_header)
{
case DISP_COLUMN_HAPPINESS:
if (enabler->mouse_lbut || enabler->mouse_rbut)
{
descending = enabler->mouse_lbut;
std::sort(units.begin(), units.end(), sortByHappiness);
}
break;
case DISP_COLUMN_NAME:
if (enabler->mouse_lbut || enabler->mouse_rbut)
{
descending = enabler->mouse_rbut;
std::sort(units.begin(), units.end(), sortByName);
}
break;
case DISP_COLUMN_PROFESSION:
if (enabler->mouse_lbut || enabler->mouse_rbut)
{
descending = enabler->mouse_rbut;
std::sort(units.begin(), units.end(), sortByProfession);
}
break;
case DISP_COLUMN_LABORS:
if (enabler->mouse_lbut || enabler->mouse_rbut)
{
descending = enabler->mouse_lbut;
sort_skill = columns[click_labor].skill;
sort_labor = columns[click_labor].labor;
std::sort(units.begin(), units.end(), sortBySkill);
}
break;
}
switch (click_body)
{
case DISP_COLUMN_HAPPINESS:
// do nothing
break;
case DISP_COLUMN_NAME:
case DISP_COLUMN_PROFESSION:
// left-click to view, right-click to zoom
if (enabler->mouse_lbut)
{
sel_row = click_unit;
events->insert(interface_key::UNITJOB_VIEW);
}
if (enabler->mouse_rbut)
{
sel_row = click_unit;
events->insert(interface_key::UNITJOB_ZOOM_CRE);
}
break;
case DISP_COLUMN_LABORS:
// left-click to toggle, right-click to just highlight
if (enabler->mouse_lbut || enabler->mouse_rbut)
{
sel_row = click_unit;
sel_column = click_labor;
if (enabler->mouse_lbut)
events->insert(interface_key::SELECT);
}
break;
}
enabler->mouse_lbut = enabler->mouse_rbut = 0;
}
UnitInfo *cur = units[sel_row];
if (events->count(interface_key::SELECT) && (cur->allowEdit) && (columns[sel_column].labor != unit_labor::NONE))
{
@ -647,6 +746,11 @@ void viewscreen_unitlaborsst::render()
Screen::clear();
Screen::drawBorder(" Dwarf Manipulator - Manage Labors ");
Screen::paintString(Screen::Pen(' ', 7, 0), col_offsets[DISP_COLUMN_HAPPINESS], 2, "Hap.");
Screen::paintString(Screen::Pen(' ', 7, 0), col_offsets[DISP_COLUMN_NAME], 2, "Name");
Screen::paintString(Screen::Pen(' ', 7, 0), col_offsets[DISP_COLUMN_PROFESSION], 2, "Profession");
for (int col = 0; col < col_widths[DISP_COLUMN_LABORS]; col++)
{
int col_offset = col + first_column;

@ -0,0 +1,29 @@
--- ../objects.old/entity_default.txt 2012-09-17 17:59:28.853898702 +0400
+++ entity_default.txt 2012-09-17 17:59:28.684899429 +0400
@@ -49,6 +49,7 @@
[TRAPCOMP:ITEM_TRAPCOMP_SPIKEDBALL]
[TRAPCOMP:ITEM_TRAPCOMP_LARGESERRATEDDISC]
[TRAPCOMP:ITEM_TRAPCOMP_MENACINGSPIKE]
+ [TRAPCOMP:ITEM_TRAPCOMP_STEAM_PISTON]
[TOY:ITEM_TOY_PUZZLEBOX]
[TOY:ITEM_TOY_BOAT]
[TOY:ITEM_TOY_HAMMER]
@@ -204,6 +205,8 @@
[PERMITTED_JOB:WAX_WORKER]
[PERMITTED_BUILDING:SOAP_MAKER]
[PERMITTED_BUILDING:SCREW_PRESS]
+ [PERMITTED_BUILDING:STEAM_ENGINE]
+ [PERMITTED_BUILDING:MAGMA_STEAM_ENGINE]
[PERMITTED_REACTION:TAN_A_HIDE]
[PERMITTED_REACTION:RENDER_FAT]
[PERMITTED_REACTION:MAKE_SOAP_FROM_TALLOW]
@@ -248,6 +251,9 @@
[PERMITTED_REACTION:ROSE_GOLD_MAKING]
[PERMITTED_REACTION:BISMUTH_BRONZE_MAKING]
[PERMITTED_REACTION:ADAMANTINE_WAFERS]
+ [PERMITTED_REACTION:STOKE_BOILER]
+ [PERMITTED_REACTION:SPATTER_ADD_WEAPON_EXTRACT]
+ [PERMITTED_REACTION:SPATTER_ADD_AMMO_EXTRACT]
[WORLD_CONSTRUCTION:TUNNEL]
[WORLD_CONSTRUCTION:BRIDGE]
[WORLD_CONSTRUCTION:ROAD]

@ -0,0 +1,10 @@
--- ../objects.old/material_template_default.txt 2012-09-17 17:59:28.907898469 +0400
+++ material_template_default.txt 2012-09-17 17:59:28.695899382 +0400
@@ -2374,6 +2374,7 @@
[MAX_EDGE:500]
[ABSORPTION:100]
[LIQUID_MISC_CREATURE]
+ [REACTION_CLASS:CREATURE_EXTRACT]
[ROTS]
This is for creatures that are "made of fire". Right now there isn't a good format for that.

@ -0,0 +1,154 @@
reaction_spatter
[OBJECT:REACTION]
Reaction name must start with 'SPATTER_ADD_':
[REACTION:SPATTER_ADD_OBJECT_LIQUID]
[NAME:coat object with liquid]
[ADVENTURE_MODE_ENABLED]
[SKILL:WAX_WORKING]
[REAGENT:extract:150:LIQUID_MISC:NONE:NONE:NONE]
[MIN_DIMENSION:150]
[DOES_NOT_DETERMINE_PRODUCT_AMOUNT]
[REAGENT:extract container:1:NONE:NONE:NONE:NONE]
[CONTAINS:extract]
[PRESERVE_REAGENT]
[DOES_NOT_DETERMINE_PRODUCT_AMOUNT]
The object to improve must be after the input mat, so that it is known:
[REAGENT:object:1:NONE:NONE:NONE:NONE]
[PRESERVE_REAGENT]
[DOES_NOT_DETERMINE_PRODUCT_AMOUNT]
Need some excuse why the spatter is water-resistant:
[REAGENT:grease:1:GLOB:NONE:NONE:NONE][REACTION_CLASS:FAT][UNROTTEN]
[DOES_NOT_DETERMINE_PRODUCT_AMOUNT]
The probability is used as spatter size; Legendary gives +90%:
COVERED = liquid, GLAZED = solid, BANDS = paste, SPIKES = powder
[IMPROVEMENT:800:object:COVERED:GET_MATERIAL_FROM_REAGENT:extract:NONE]
[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]
[REACTION_CLASS:CREATURE_EXTRACT]
[DOES_NOT_DETERMINE_PRODUCT_AMOUNT]
[REAGENT:extract container:1:NONE:NONE:NONE:NONE]
[CONTAINS:extract]
[PRESERVE_REAGENT]
[DOES_NOT_DETERMINE_PRODUCT_AMOUNT]
The object to improve must be after the input mat, so that it is known:
[REAGENT:object:1:WEAPON:NONE:NONE:NONE]
[PRESERVE_REAGENT]
[DOES_NOT_DETERMINE_PRODUCT_AMOUNT]
Need some excuse why the spatter is water-resistant:
[REAGENT:grease:1:GLOB:NONE:NONE:NONE][REACTION_CLASS:TALLOW][UNROTTEN]
[DOES_NOT_DETERMINE_PRODUCT_AMOUNT]
The probability is used as spatter size; Legendary gives +90%:
COVERED = liquid, GLAZED = solid, BANDS = paste, SPIKES = powder
[IMPROVEMENT:800:object:COVERED:GET_MATERIAL_FROM_REAGENT:extract:NONE]
[REACTION:SPATTER_ADD_AMMO_EXTRACT]
[NAME:coat ammo with extract]
[BUILDING:CRAFTSMAN:NONE]
[SKILL:WAX_WORKING]
[REAGENT:extract:50:LIQUID_MISC:NONE:NONE:NONE]
[MIN_DIMENSION:50]
[REACTION_CLASS:CREATURE_EXTRACT]
[DOES_NOT_DETERMINE_PRODUCT_AMOUNT]
[REAGENT:extract container:1:NONE:NONE:NONE:NONE]
[CONTAINS:extract]
[PRESERVE_REAGENT]
[DOES_NOT_DETERMINE_PRODUCT_AMOUNT]
The object to improve must be after the input mat, so that it is known:
[REAGENT:object:1:AMMO:NONE:NONE:NONE]
[PRESERVE_REAGENT]
[MIN_DIMENSION:5] don't waste materials on single bolts
[DOES_NOT_DETERMINE_PRODUCT_AMOUNT]
Need some excuse why the spatter is water-resistant:
[REAGENT:grease:1:GLOB:NONE:NONE:NONE][REACTION_CLASS:TALLOW][UNROTTEN]
[DOES_NOT_DETERMINE_PRODUCT_AMOUNT]
The probability is used as spatter size; Legendary gives +90%:
COVERED = liquid, GLAZED = solid, BANDS = paste, SPIKES = powder
[IMPROVEMENT:200:object:COVERED:GET_MATERIAL_FROM_REAGENT:extract:NONE]
[REACTION:SPATTER_ADD_WEAPON_GCS]
[NAME:coat weapon with GCS venom]
[BUILDING:CRAFTSMAN:NONE]
[SKILL:WAX_WORKING]
[REAGENT:extract:150:LIQUID_MISC:NONE:CAVE_SPIDER_GIANT:POISON]
[MIN_DIMENSION:150]
[REACTION_CLASS:CREATURE_EXTRACT]
[REAGENT:extract container:1:NONE:NONE:NONE:NONE]
[CONTAINS:extract]
[PRESERVE_REAGENT]
[DOES_NOT_DETERMINE_PRODUCT_AMOUNT]
The object to improve must be after the input mat, so that it is known:
[REAGENT:object:1:WEAPON:NONE:NONE:NONE]
[PRESERVE_REAGENT]
Need some excuse why the spatter is water-resistant:
[REAGENT:grease:1:GLOB:NONE:NONE:NONE][REACTION_CLASS:TALLOW][UNROTTEN]
The probability is used as spatter size; Legendary gives +90%:
COVERED = liquid, GLAZED = solid, BANDS = paste, SPIKES = powder
[IMPROVEMENT:800:object:COVERED:GET_MATERIAL_FROM_REAGENT:extract:NONE]
[REACTION:SPATTER_ADD_AMMO_GCS]
[NAME:coat ammo with GCS venom]
[BUILDING:CRAFTSMAN:NONE]
[SKILL:WAX_WORKING]
[REAGENT:extract:50:LIQUID_MISC:NONE:CAVE_SPIDER_GIANT:POISON]
[MIN_DIMENSION:50]
[REACTION_CLASS:CREATURE_EXTRACT]
[REAGENT:extract container:1:NONE:NONE:NONE:NONE]
[CONTAINS:extract]
[PRESERVE_REAGENT]
[DOES_NOT_DETERMINE_PRODUCT_AMOUNT]
The object to improve must be after the input mat, so that it is known:
[REAGENT:object:1:AMMO:NONE:NONE:NONE]
[PRESERVE_REAGENT]
Need some excuse why the spatter is water-resistant:
[REAGENT:grease:1:GLOB:NONE:NONE:NONE][REACTION_CLASS:TALLOW][UNROTTEN]
The probability is used as spatter size; Legendary gives +90%:
COVERED = liquid, GLAZED = solid, BANDS = paste, SPIKES = powder
[IMPROVEMENT:200:object:COVERED:GET_MATERIAL_FROM_REAGENT:extract:NONE]
[REACTION:SPATTER_ADD_WEAPON_GDS]
[NAME:coat weapon with GDS venom]
[BUILDING:CRAFTSMAN:NONE]
[SKILL:WAX_WORKING]
[REAGENT:extract:150:LIQUID_MISC:NONE:SCORPION_DESERT_GIANT:POISON]
[MIN_DIMENSION:150]
[REACTION_CLASS:CREATURE_EXTRACT]
[REAGENT:extract container:1:NONE:NONE:NONE:NONE]
[CONTAINS:extract]
[PRESERVE_REAGENT]
[DOES_NOT_DETERMINE_PRODUCT_AMOUNT]
The object to improve must be after the input mat, so that it is known:
[REAGENT:object:1:WEAPON:NONE:NONE:NONE]
[PRESERVE_REAGENT]
Need some excuse why the spatter is water-resistant:
[REAGENT:grease:1:GLOB:NONE:NONE:NONE][REACTION_CLASS:TALLOW][UNROTTEN]
The probability is used as spatter size; Legendary gives +90%:
COVERED = liquid, GLAZED = solid, BANDS = paste, SPIKES = powder
[IMPROVEMENT:800:object:COVERED:GET_MATERIAL_FROM_REAGENT:extract:NONE]
[REACTION:SPATTER_ADD_AMMO_GDS]
[NAME:coat ammo with GDS venom]
[BUILDING:CRAFTSMAN:NONE]
[SKILL:WAX_WORKING]
[REAGENT:extract:50:LIQUID_MISC:NONE:SCORPION_DESERT_GIANT:POISON]
[MIN_DIMENSION:50]
[REACTION_CLASS:CREATURE_EXTRACT]
[REAGENT:extract container:1:NONE:NONE:NONE:NONE]
[CONTAINS:extract]
[PRESERVE_REAGENT]
[DOES_NOT_DETERMINE_PRODUCT_AMOUNT]
The object to improve must be after the input mat, so that it is known:
[REAGENT:object:1:AMMO:NONE:NONE:NONE]
[PRESERVE_REAGENT]
Need some excuse why the spatter is water-resistant:
[REAGENT:grease:1:GLOB:NONE:NONE:NONE][REACTION_CLASS:TALLOW][UNROTTEN]
The probability is used as spatter size; Legendary gives +90%:
COVERED = liquid, GLAZED = solid, BANDS = paste, SPIKES = powder
[IMPROVEMENT:200:object:COVERED:GET_MATERIAL_FROM_REAGENT:extract:NONE]

@ -10,6 +10,7 @@
#include <modules/World.h>
#include <modules/Units.h>
#include <modules/Job.h>
#include <modules/Materials.h>
#include <LuaTools.h>
#include <TileTypes.h>
#include <vector>
@ -45,11 +46,14 @@
#include "df/unit_misc_trait.h"
#include "df/job.h"
#include "df/job_item.h"
#include "df/item.h"
#include "df/item_actual.h"
#include "df/items_other_id.h"
#include "df/building_stockpilest.h"
#include "df/stockpile_links.h"
#include "df/workshop_profile.h"
#include "df/strain_type.h"
#include "df/material.h"
#include "df/flow_type.h"
#include "MiscUtils.h"
@ -108,7 +112,7 @@ static bool is_in_range(const coord_range &target, df::coord pos)
static std::pair<int, int> get_engine_range(df::building_siegeenginest *bld)
{
if (bld->type == siegeengine_type::Ballista)
return std::make_pair(0, 200);
return std::make_pair(1, 200);
else
return std::make_pair(30, 100);
}
@ -162,6 +166,63 @@ static void random_direction(float &x, float &y, float &z)
z = 1.0f - 2.0f*d;
}
static const int WEAR_TICKS = 806400;
static bool apply_impact_damage(df::item *item, int minv, int maxv)
{
MaterialInfo info(item);
if (!info.isValid())
{
item->setWear(3);
return false;
}
auto &strength = info.material->strength;
// Use random strain type excluding COMPRESSIVE (conveniently last)
int type = random_int(strain_type::COMPRESSIVE);
int power = minv + random_int(maxv-minv+1);
// High elasticity materials just bend
if (strength.strain_at_yield[type] >= 5000)
return true;
// Instant fracture?
int fracture = strength.fracture[type];
if (fracture <= power)
{
item->setWear(3);
return false;
}
// Impact within elastic strain range?
int yield = strength.yield[type];
if (yield > power)
return true;
// Can wear?
auto actual = virtual_cast<df::item_actual>(item);
if (!actual)
return false;
// Transform plastic deformation to wear
int max_wear = WEAR_TICKS * 4;
int cur_wear = WEAR_TICKS * actual->wear + actual->wear_timer;
cur_wear += int64_t(power - yield)*max_wear/(fracture - yield);
if (cur_wear >= max_wear)
{
actual->wear = 3;
return false;
}
else
{
actual->wear = cur_wear / WEAR_TICKS;
actual->wear_timer = cur_wear % WEAR_TICKS;
return true;
}
}
/*
* Configuration object
*/
@ -230,7 +291,7 @@ static EngineInfo *find_engine(df::building *bld, bool create = false)
);
obj->is_catapult = (ebld->type == siegeengine_type::Catapult);
obj->proj_speed = 2;
obj->hit_delay = 3;
obj->hit_delay = obj->is_catapult ? 2 : -1;
obj->fire_range = get_engine_range(ebld);
obj->ammo_vector_id = job_item_vector_id::BOULDER;
@ -1046,6 +1107,9 @@ struct UnitPath {
float time = unit->counters.job_counter+0.5f;
float speed = Units::computeMovementSpeed(unit)/100.0f;
if (unit->counters.unconscious > 0)
time += unit->counters.unconscious;
for (size_t i = 0; i < upath.size(); i++)
{
df::coord new_pos = upath[i];
@ -1221,10 +1285,84 @@ static int proposeUnitHits(lua_State *L)
return 1;
}
static int computeNearbyWeight(lua_State *L)
{
auto engine = find_engine(L, 1);
luaL_checktype(L, 2, LUA_TTABLE);
luaL_checktype(L, 3, LUA_TTABLE);
const char *fname = luaL_optstring(L, 4, "nearby_weight");
std::vector<UnitPath*> units;
std::vector<float> weights;
lua_pushnil(L);
while (lua_next(L, 3))
{
df::unit *unit;
if (lua_isnumber(L, -2))
unit = df::unit::find(lua_tointeger(L, -2));
else
unit = Lua::CheckDFObject<df::unit>(L, -2);
if (!unit)
continue;
units.push_back(UnitPath::get(unit));
weights.push_back(lua_tonumber(L, -1));
lua_pop(L, 1);
}
lua_pushnil(L);
while (lua_next(L, 2))
{
Lua::StackUnwinder frame(L, 1);
lua_getfield(L, frame[1], "unit");
df::unit *unit = Lua::CheckDFObject<df::unit>(L, -1);
lua_getfield(L, frame[1], "time");
float time = luaL_checknumber(L, lua_gettop(L));
df::coord pos;
lua_getfield(L, frame[1], "pos");
if (lua_isnil(L, -1))
{
if (!unit) luaL_error(L, "either unit or pos is required");
pos = UnitPath::get(unit)->posAtTime(time);
}
else
Lua::CheckDFAssign(L, &pos, -1);
float sum = 0.0f;
for (size_t i = 0; i < units.size(); i++)
{
if (units[i]->unit == unit)
continue;
auto diff = units[i]->posAtTime(time) - pos;
float dist = 1 + sqrtf(diff.x*diff.x + diff.y*diff.y + diff.z*diff.z);
sum += weights[i]/(dist*dist);
}
lua_pushnumber(L, sum);
lua_setfield(L, frame[1], fname);
}
return 0;
}
/*
* Projectile hook
*/
static const int offsets[8][2] = {
{ -1, -1 }, { 0, -1 }, { 1, -1 },
{ -1, 0 }, { 1, 0 },
{ -1, 1 }, { 0, 1 }, { 1, 1 }
};
struct projectile_hook : df::proj_itemst {
typedef df::proj_itemst interpose_base;
@ -1232,6 +1370,9 @@ struct projectile_hook : df::proj_itemst {
{
target_pos = path.target;
// Debug
Maps::getTileOccupancy(path.goal)->bits.arrow_color = COLOR_LIGHTMAGENTA;
PathMetrics raytrace(path);
// Materialize map blocks, or the projectile will crash into them
@ -1259,7 +1400,53 @@ struct projectile_hook : df::proj_itemst {
fall_threshold = std::min(fall_threshold, engine->fire_range.second);
}
void aimAtArea(EngineInfo *engine)
void aimAtPoint(EngineInfo *engine, int skill, const ProjectilePath &path)
{
df::coord fail_target = path.goal;
orient_engine(engine->bld, path.goal);
// Debug
Maps::getTileOccupancy(path.goal)->bits.arrow_color = COLOR_LIGHTRED;
// Dabbling always hit in 7x7 area
if (skill < skill_rating::Novice)
{
fail_target.x += random_int(7)-3;
fail_target.y += random_int(7)-3;
aimAtPoint(engine, ProjectilePath(path.origin, fail_target));
return;
}
// Exact hit chance
float hit_chance = 1.04f - powf(0.8f, skill);
if (float(rand())/RAND_MAX < hit_chance)
{
aimAtPoint(engine, path);
return;
}
// Otherwise perturb
if (skill <= skill_rating::Proficient)
{
// 5x5
fail_target.x += random_int(5)-2;
fail_target.y += random_int(5)-2;
}
else
{
// 3x3
int idx = random_int(8);
fail_target.x += offsets[idx][0];
fail_target.y += offsets[idx][1];
}
ProjectilePath fail(path.origin, fail_target, path.fudge_delta, path.fudge_factor);
aimAtPoint(engine, fail);
}
void aimAtArea(EngineInfo *engine, int skill)
{
df::coord target, last_passable;
df::coord tbase = engine->target.first;
@ -1282,7 +1469,7 @@ struct projectile_hook : df::proj_itemst {
if (raytrace.hits() && engine->isInRange(raytrace.goal_step))
{
aimAtPoint(engine, path);
aimAtPoint(engine, skill, path);
return;
}
}
@ -1290,7 +1477,7 @@ struct projectile_hook : df::proj_itemst {
if (!last_passable.isValid())
last_passable = target;
aimAtPoint(engine, ProjectilePath(engine->center, last_passable));
aimAtPoint(engine, skill, ProjectilePath(engine->center, last_passable));
}
static int safeAimProjectile(lua_State *L)
@ -1312,9 +1499,9 @@ struct projectile_hook : df::proj_itemst {
lua_call(L, 5, 1);
if (lua_isnil(L, -1))
proj->aimAtArea(engine);
proj->aimAtArea(engine, skill);
else
proj->aimAtPoint(engine, decode_path(L, -1, engine->center));
proj->aimAtPoint(engine, skill, decode_path(L, -1, engine->center));
return 0;
}
@ -1335,13 +1522,19 @@ struct projectile_hook : df::proj_itemst {
int skill = getOperatorSkill(engine->bld, true);
lua_pushcfunction(L, safeAimProjectile);
lua_pushlightuserdata(L, this);
lua_pushlightuserdata(L, engine);
lua_pushinteger(L, skill);
// Dabbling can't aim
if (skill < skill_rating::Novice)
aimAtArea(engine, skill);
else
{
lua_pushcfunction(L, safeAimProjectile);
lua_pushlightuserdata(L, this);
lua_pushlightuserdata(L, engine);
lua_pushinteger(L, skill);
if (!Lua::Core::SafeCall(out, 3, 0))
aimAtArea(engine);
if (!Lua::Core::SafeCall(out, 3, 0))
aimAtArea(engine, skill);
}
switch (item->getType())
{
@ -1363,9 +1556,13 @@ struct projectile_hook : df::proj_itemst {
float speed = 100000.0f / (fall_delay + 1);
int min_zspeed = (fall_delay+1)*4900;
float bonus = 1.0f + 0.1f*(origin_pos.z -cur_pos.z);
bonus *= 1.0f + (distance_flown - 60) / 200.0f;
speed *= bonus;
// Flight direction vector
df::coord dist = target_pos - origin_pos;
float vx = dist.x, vy = dist.y, vz = fabs(dist.z);
float vx = dist.x, vy = dist.y, vz = fabs((float)dist.z);
normalize(vx, vy, vz);
int start_z = 0;
@ -1383,10 +1580,28 @@ struct projectile_hook : df::proj_itemst {
for (size_t i = 0; i < contents.size(); i++)
{
auto child = contents[i];
// Liquids are vaporized so that they cover nearby units
if (child->isLiquid())
{
auto flow = Maps::spawnFlow(
cur_pos,
flow_type::MaterialVapor,
child->getMaterial(), child->getMaterialIndex(),
100
);
// should it leave a puddle too?..
if (flow && Items::remove(mc, child))
continue;
}
auto proj = Items::makeProjectile(mc, child);
if (!proj) continue;
proj->flags.bits.no_impact_destroy = true;
bool keep = apply_impact_damage(child, 50000, int(250000*bonus));
proj->flags.bits.no_impact_destroy = keep;
//proj->flags.bits.bouncing = true;
proj->flags.bits.piercing = true;
proj->flags.bits.parabolic = true;
@ -1403,7 +1618,7 @@ struct projectile_hook : df::proj_itemst {
proj->speed_x = int(speed * sx);
proj->speed_y = int(speed * sy);
proj->speed_z = int(speed * sz);
proj->speed_z = std::max(min_zspeed, int(speed * sz));
}
}
@ -1554,6 +1769,7 @@ DFHACK_PLUGIN_LUA_COMMANDS {
DFHACK_LUA_COMMAND(traceUnitPath),
DFHACK_LUA_COMMAND(unitPosAtTime),
DFHACK_LUA_COMMAND(proposeUnitHits),
DFHACK_LUA_COMMAND(computeNearbyWeight),
DFHACK_LUA_END
};

@ -228,7 +228,7 @@ static void sort_null_first(vector<string> &parameters)
vector_insert_at(parameters, 0, std::string("<exists"));
}
static df::layer_object_listst *getLayerList(df::viewscreen_layerst *layer, int idx)
static df::layer_object_listst *getLayerList(df::viewscreen_layer *layer, int idx)
{
return virtual_cast<df::layer_object_listst>(vector_get(layer->layer_objects,idx));
}

@ -29,11 +29,20 @@
#include "df/criminal_case.h"
#include "df/unit_inventory_item.h"
#include "df/viewscreen_dwarfmodest.h"
#include "df/viewscreen_layer_unit_actionst.h"
#include "df/squad_order_trainst.h"
#include "df/ui_build_selector.h"
#include "df/building_trapst.h"
#include "df/item_actual.h"
#include "df/item_liquipowder.h"
#include "df/item_barst.h"
#include "df/item_threadst.h"
#include "df/item_clothst.h"
#include "df/contaminant.h"
#include "df/layer_object.h"
#include "df/reaction.h"
#include "df/reaction_reagent_itemst.h"
#include "df/reaction_reagent_flags.h"
#include <stdlib.h>
@ -93,6 +102,13 @@ DFhackCExport command_result plugin_init (color_ostream &out, std::vector <Plugi
" Further improves temperature updates by ensuring that 1 degree of\n"
" item temperature is crossed in no more than specified number of frames\n"
" when updating from the environment temperature. Use 0 to disable.\n"
" tweak fix-dimensions [disable]\n"
" Fixes subtracting small amount of thread/cloth/liquid from a stack\n"
" by splitting the stack and subtracting from the remaining single item.\n"
" tweak advmode-contained [disable]\n"
" Fixes custom reactions with container inputs in advmode. The issue is\n"
" that the screen tries to force you to select the contents separately\n"
" from the container. This forcefully skips child reagents.\n"
));
return CR_OK;
}
@ -343,6 +359,141 @@ IMPLEMENT_VMETHOD_INTERPOSE(fast_heat_hook, updateTempFromMap);
IMPLEMENT_VMETHOD_INTERPOSE(fast_heat_hook, updateTemperature);
IMPLEMENT_VMETHOD_INTERPOSE(fast_heat_hook, adjustTemperature);
static void correct_dimension(df::item_actual *self, int32_t &delta, int32_t dim)
{
// Zero dimension or remainder?
if (dim <= 0 || self->stack_size <= 1) return;
int rem = delta % dim;
if (rem == 0) return;
// If destroys, pass through
int intv = delta / dim;
if (intv >= self->stack_size) return;
// Subtract int part
delta = rem;
self->stack_size -= intv;
if (self->stack_size <= 1) return;
// If kills the item or cannot split, round up.
if (!self->flags.bits.in_inventory || !Items::getContainer(self))
{
delta = dim;
return;
}
// Otherwise split the stack
color_ostream_proxy out(Core::getInstance().getConsole());
out.print("fix-dimensions: splitting stack #%d for delta %d.\n", self->id, delta);
auto copy = self->splitStack(self->stack_size-1, true);
if (copy) copy->categorize(true);
}
struct dimension_lqp_hook : df::item_liquipowder {
typedef df::item_liquipowder interpose_base;
DEFINE_VMETHOD_INTERPOSE(bool, subtractDimension, (int32_t delta))
{
correct_dimension(this, delta, dimension);
return INTERPOSE_NEXT(subtractDimension)(delta);
}
};
IMPLEMENT_VMETHOD_INTERPOSE(dimension_lqp_hook, subtractDimension);
struct dimension_bar_hook : df::item_barst {
typedef df::item_barst interpose_base;
DEFINE_VMETHOD_INTERPOSE(bool, subtractDimension, (int32_t delta))
{
correct_dimension(this, delta, dimension);
return INTERPOSE_NEXT(subtractDimension)(delta);
}
};
IMPLEMENT_VMETHOD_INTERPOSE(dimension_bar_hook, subtractDimension);
struct dimension_thread_hook : df::item_threadst {
typedef df::item_threadst interpose_base;
DEFINE_VMETHOD_INTERPOSE(bool, subtractDimension, (int32_t delta))
{
correct_dimension(this, delta, dimension);
return INTERPOSE_NEXT(subtractDimension)(delta);
}
};
IMPLEMENT_VMETHOD_INTERPOSE(dimension_thread_hook, subtractDimension);
struct dimension_cloth_hook : df::item_clothst {
typedef df::item_clothst interpose_base;
DEFINE_VMETHOD_INTERPOSE(bool, subtractDimension, (int32_t delta))
{
correct_dimension(this, delta, dimension);
return INTERPOSE_NEXT(subtractDimension)(delta);
}
};
IMPLEMENT_VMETHOD_INTERPOSE(dimension_cloth_hook, subtractDimension);
struct advmode_contained_hook : df::viewscreen_layer_unit_actionst {
typedef df::viewscreen_layer_unit_actionst interpose_base;
DEFINE_VMETHOD_INTERPOSE(void, feed, (set<df::interface_key> *input))
{
auto old_reaction = cur_reaction;
auto old_reagent = reagent;
INTERPOSE_NEXT(feed)(input);
if (cur_reaction && (cur_reaction != old_reaction || reagent != old_reagent))
{
old_reagent = reagent;
// Skip reagents already contained by others
while (reagent < (int)cur_reaction->reagents.size()-1)
{
if (!cur_reaction->reagents[reagent]->flags.bits.IN_CONTAINER)
break;
reagent++;
}
if (old_reagent != reagent)
{
// Reproduces a tiny part of the orginal screen code
choice_items.clear();
auto preagent = cur_reaction->reagents[reagent];
reagent_amnt_left = preagent->quantity;
for (int i = held_items.size()-1; i >= 0; i--)
{
if (!preagent->matchesRoot(held_items[i], cur_reaction->index))
continue;
if (linear_index(sel_items, held_items[i]) >= 0)
continue;
choice_items.push_back(held_items[i]);
}
layer_objects[6]->setListLength(choice_items.size());
if (!choice_items.empty())
{
layer_objects[4]->active = layer_objects[5]->active = false;
layer_objects[6]->active = true;
}
else if (layer_objects[6]->active)
{
layer_objects[6]->active = false;
layer_objects[5]->active = true;
}
}
}
}
};
IMPLEMENT_VMETHOD_INTERPOSE(advmode_contained_hook, feed);
static void enable_hook(color_ostream &out, VMethodInterposeLinkBase &hook, vector <string> &parameters)
{
if (vector_get(parameters, 1) == "disable")
@ -491,6 +642,17 @@ static command_result tweak(color_ostream &out, vector <string> &parameters)
enable_hook(out, INTERPOSE_HOOK(fast_heat_hook, updateTemperature), parameters);
enable_hook(out, INTERPOSE_HOOK(fast_heat_hook, adjustTemperature), parameters);
}
else if (cmd == "fix-dimensions")
{
enable_hook(out, INTERPOSE_HOOK(dimension_lqp_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);
}
else if (cmd == "advmode-contained")
{
enable_hook(out, INTERPOSE_HOOK(advmode_contained_hook, feed), parameters);
}
else
return CR_WRONG_USAGE;

@ -4,19 +4,21 @@ local gui = require 'gui'
local text = 'Woohoo, lua viewscreen :)'
local screen = mkinstance(gui.FramedScreen, {
local screen = gui.FramedScreen{
frame_style = gui.GREY_LINE_FRAME,
frame_title = 'Hello World',
frame_width = #text+6,
frame_height = 3,
onRenderBody = function(self, dc)
dc:seek(3,1):string(text, COLOR_LIGHTGREEN)
end,
onInput = function(self,keys)
if keys.LEAVESCREEN or keys.SELECT then
self:dismiss()
end
}
function screen:onRenderBody(dc)
dc:seek(3,1):string(text, COLOR_LIGHTGREEN)
end
function screen:onInput(keys)
if keys.LEAVESCREEN or keys.SELECT then
self:dismiss()
end
}):init()
end
screen:show()

@ -53,13 +53,7 @@ local permaflows = {
Toggle = defclass(Toggle)
function Toggle:init(items)
self:init_fields{
items = items,
selected = 1
}
return self
end
Toggle.ATTRS{ items = {}, selected = 1 }
function Toggle:get()
return self.items[self.selected]
@ -89,16 +83,14 @@ LiquidsUI = defclass(LiquidsUI, guidm.MenuOverlay)
LiquidsUI.focus_path = 'liquids'
function LiquidsUI:init()
self:init_fields{
brush = mkinstance(Toggle):init(brushes),
paint = mkinstance(Toggle):init(paints),
flow = mkinstance(Toggle):init(flowbits),
set = mkinstance(Toggle):init(setmode),
permaflow = mkinstance(Toggle):init(permaflows),
self:assign{
brush = Toggle{ items = brushes },
paint = Toggle{ items = paints },
flow = Toggle{ items = flowbits },
set = Toggle{ items = setmode },
permaflow = Toggle{ items = permaflows },
amount = 7,
}
guidm.MenuOverlay.init(self)
return self
end
function LiquidsUI:onDestroy()
@ -201,6 +193,7 @@ function LiquidsUI:onRenderBody(dc)
end
function ensure_blocks(cursor, size, cb)
size = size or xyz2pos(1,1,1)
local cx,cy,cz = pos2xyz(cursor)
local all = true
for x=1,size.x or 1,16 do
@ -298,5 +291,5 @@ if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/LookAround') then
qerror("This script requires the main dwarfmode view in 'k' mode")
end
local list = mkinstance(LiquidsUI):init()
local list = LiquidsUI()
list:show()

@ -43,13 +43,11 @@ MechanismList = defclass(MechanismList, guidm.MenuOverlay)
MechanismList.focus_path = 'mechanisms'
function MechanismList:init(building)
self:init_fields{
function MechanismList:init(info)
self:assign{
links = {}, selected = 1
}
guidm.MenuOverlay.init(self)
self:fillList(building)
return self
self:fillList(info.building)
end
function MechanismList:fillList(building)
@ -126,6 +124,6 @@ if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some') t
qerror("This script requires the main dwarfmode view in 'q' mode")
end
local list = mkinstance(MechanismList):init(df.global.world.selected_building)
local list = MechanismList{ building = df.global.world.selected_building }
list:show()
list:changeSelected(1)

@ -13,15 +13,13 @@ PowerMeter = defclass(PowerMeter, guidm.MenuOverlay)
PowerMeter.focus_path = 'power-meter'
function PowerMeter:init()
self:init_fields{
self:assign{
min_power = 0, max_power = -1, invert = false,
}
guidm.MenuOverlay.init(self)
return self
end
function PowerMeter:onShow()
guidm.MenuOverlay.onShow(self)
PowerMeter.super.onShow(self)
-- Send an event to update the errors
bselector.plate_info.flags.whole = 0
@ -112,5 +110,5 @@ then
qerror("This script requires the main dwarfmode view in build pressure plate mode")
end
local list = mkinstance(PowerMeter):init()
local list = PowerMeter()
list:show()

@ -78,15 +78,17 @@ RoomList = defclass(RoomList, guidm.MenuOverlay)
RoomList.focus_path = 'room-list'
function RoomList:init(unit)
RoomList.ATTRS{ unit = DEFAULT_NIL }
function RoomList:init(info)
local unit = info.unit
local base_bld = df.global.world.selected_building
self:init_fields{
unit = unit, base_building = base_bld,
self:assign{
base_building = base_bld,
items = {}, selected = 1,
own_rooms = {}, spouse_rooms = {}
}
guidm.MenuOverlay.init(self)
self.old_viewport = self:getViewport()
self.old_cursor = guidm.getCursorPos()
@ -115,8 +117,6 @@ function RoomList:init(unit)
self.items = concat_lists({self.base_item}, self.items)
::found::
end
return self
end
local sex_char = { [0] = 12, [1] = 11 }
@ -235,12 +235,13 @@ function RoomList:onInput(keys)
end
local focus = dfhack.gui.getCurFocus()
if focus == 'dwarfmode/QueryBuilding/Some' then
local base = df.global.world.selected_building
mkinstance(RoomList):init(base.owner):show()
elseif focus == 'dwarfmode/QueryBuilding/Some/Assign/Unit' then
if focus == 'dwarfmode/QueryBuilding/Some/Assign/Unit' then
local unit = df.global.ui_building_assign_units[df.global.ui_building_item_cursor]
mkinstance(RoomList):init(unit):show()
RoomList{ unit = unit }:show()
elseif string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some') then
local base = df.global.world.selected_building
RoomList{ unit = base.owner }:show()
else
qerror("This script requires the main dwarfmode view in 'q' mode")
end

@ -21,6 +21,7 @@ local item_choices = {
{ caption = 'trap components', item_type = df.item_type.TRAPCOMP },
{ caption = 'bins', item_type = df.item_type.BIN },
{ caption = 'barrels', item_type = df.item_type.BARREL },
{ caption = 'cages', item_type = df.item_type.CAGE },
{ caption = 'anything', item_type = -1 },
}
@ -33,30 +34,29 @@ SiegeEngine = defclass(SiegeEngine, guidm.MenuOverlay)
SiegeEngine.focus_path = 'siege-engine'
function SiegeEngine:init(building)
self:init_fields{
building = building,
center = utils.getBuildingCenter(building),
SiegeEngine.ATTRS{ building = DEFAULT_NIL }
function SiegeEngine:init()
self:assign{
center = utils.getBuildingCenter(self.building),
selected_pile = 1,
mode_main = {
render = self:callback 'onRenderBody_main',
input = self:callback 'onInput_main',
},
mode_aim = {
render = self:callback 'onRenderBody_aim',
input = self:callback 'onInput_aim',
},
mode_pile = {
render = self:callback 'onRenderBody_pile',
input = self:callback 'onInput_pile',
}
}
guidm.MenuOverlay.init(self)
self.mode_main = {
render = self:callback 'onRenderBody_main',
input = self:callback 'onInput_main',
}
self.mode_aim = {
render = self:callback 'onRenderBody_aim',
input = self:callback 'onInput_aim',
}
self.mode_pile = {
render = self:callback 'onRenderBody_pile',
input = self:callback 'onInput_pile',
}
return self
end
function SiegeEngine:onShow()
guidm.MenuOverlay.onShow(self)
SiegeEngine.super.onShow(self)
self.old_cursor = guidm.getCursorPos()
self.old_viewport = self:getViewport()
@ -486,5 +486,5 @@ if not df.building_siegeenginest:is_instance(building) then
qerror("A siege engine must be selected")
end
local list = mkinstance(SiegeEngine):init(df.global.world.selected_building)
local list = SiegeEngine{ building = building }
list:show()