diff --git a/LUA_API.rst b/LUA_API.rst index 4d9170d6e..c5f9a1c58 100644 --- a/LUA_API.rst +++ b/LUA_API.rst @@ -868,6 +868,26 @@ Units module Returns the nemesis record of the unit if it has one, or *nil*. +* ``dfhack.units.isHidingCurse(unit)`` + + Checks if the unit hides improved attributes from its curse. + +* ``dfhack.units.getPhysicalAttrValue(unit, attr_type)`` +* ``dfhack.units.getMentalAttrValue(unit, attr_type)`` + + Computes the effective attribute value, including curse effect. + +* ``dfhack.units.isCrazed(unit)`` +* ``dfhack.units.isOpposedToLife(unit)`` +* ``dfhack.units.hasExtravision(unit)`` +* ``dfhack.units.isBloodsucker(unit)`` + + Simple checks of caste attributes that can be modified by curses. + +* ``dfhack.units.getMiscTrait(unit, type[, create])`` + + Finds (or creates if requested) a misc trait object with the given id. + * ``dfhack.units.isDead(unit)`` The unit is completely dead and passive, or a ghost. @@ -894,6 +914,14 @@ Units module Returns the age of the unit in years as a floating-point value. If ``true_age`` is true, ignores false identities. +* ``dfhack.units.getEffectiveSkill(unit, skill)`` + + Computes the effective rating for the given skill, taking into account exhaustion, pain etc. + +* ``dfhack.units.computeMovementSpeed(unit)`` + + Computes number of frames * 100 it takes the unit to move in its current state of mind and body. + * ``dfhack.units.getNoblePositions(unit)`` Returns a list of tables describing noble position assignments, or *nil*. @@ -971,6 +999,14 @@ 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. + Maps module ----------- diff --git a/Lua API.html b/Lua API.html index dc9c8d73e..07f038bc4 100644 --- a/Lua API.html +++ b/Lua API.html @@ -1106,6 +1106,26 @@ a lua list containing them.

  • dfhack.units.getNemesis(unit)

    Returns the nemesis record of the unit if it has one, or nil.

  • +
  • dfhack.units.isHidingCurse(unit)

    +

    Checks if the unit hides improved attributes from its curse.

    +
  • +
  • dfhack.units.getPhysicalAttrValue(unit, attr_type)

    +
  • +
  • dfhack.units.getMentalAttrValue(unit, attr_type)

    +

    Computes the effective attribute value, including curse effect.

    +
  • +
  • dfhack.units.isCrazed(unit)

    +
  • +
  • dfhack.units.isOpposedToLife(unit)

    +
  • +
  • dfhack.units.hasExtravision(unit)

    +
  • +
  • dfhack.units.isBloodsucker(unit)

    +

    Simple checks of caste attributes that can be modified by curses.

    +
  • +
  • dfhack.units.getMiscTrait(unit, type[, create])

    +

    Finds (or creates if requested) a misc trait object with the given id.

    +
  • dfhack.units.isDead(unit)

    The unit is completely dead and passive, or a ghost.

  • @@ -1126,6 +1146,12 @@ same checks the game uses to decide game-over by extinction.

    Returns the age of the unit in years as a floating-point value. If true_age is true, ignores false identities.

    +
  • dfhack.units.getEffectiveSkill(unit, skill)

    +

    Computes the effective rating for the given skill, taking into account exhaustion, pain etc.

    +
  • +
  • dfhack.units.computeMovementSpeed(unit)

    +

    Computes number of frames * 100 it takes the unit to move in its current state of mind and body.

    +
  • dfhack.units.getNoblePositions(unit)

    Returns a list of tables describing noble position assignments, or nil. Every table has fields entity, assignment and position.

    @@ -1187,6 +1213,12 @@ Returns false in case of error.

  • dfhack.items.moveToInventory(item,unit,use_mode,body_part)

    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.

    +
  • diff --git a/NEWS b/NEWS new file mode 100644 index 000000000..22f64d7d6 --- /dev/null +++ b/NEWS @@ -0,0 +1,75 @@ +DFHack v0.34.11-r2 (UNRELEASED) + + Internals: + - full support for Mac OS X. + - a plugin that adds scripting in ruby. + - support for interposing virtual methods in DF from C++ plugins. + - support for creating new interface screens from C++ and lua. + - added various other API functions. + Notable bugfixes: + - better terminal reset after exit on linux. + - seedwatch now works on reclaim. + - the sort plugin won't crash on cages anymore. + Misc improvements: + - autodump: can move items to any walkable tile, not just floors. + - stripcaged: by default keep armor, new dumparmor option. + - zone: allow non-domesticated birds in nestboxes. + - workflow: quality range in constraints. + - cleanplants: new command to remove rain water from plants. + - liquids: can paint permaflow, i.e. what makes rivers power water wheels. + - prospect: pre-embark prospector accounts for caves & magma sea in its estimate. + - rename: supports renaming stockpiles, workshops, traps, siege engines. + New tweaks: + - tweak stable-cursor: keeps exact cursor position between d/k/t/q/v etc menus. + - tweak patrol-duty: makes Train orders reduce patrol timer, like the binary patch does. + - 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. + - fix/population-cap: run after every migrant wave to prevent exceeding the cap. + - fix/stable-temp: counts items with temperature updates; does instant one-shot stable-temp. + New GUI scripts: + - gui/mechanisms: browse mechanism links of the current building. + - gui/room-list: browse other rooms owned by the unit when assigning one. + - gui/liquids: a GUI front-end for the liquids plugin. + - gui/rename: renaming stockpiles, workshops and units via an in-game dialog. + - gui/power-meter: front-end for the Power Meter plugin. + - gui/siege-engine: front-end for the Siege Engine plugin. + Autolabor plugin: + - can set nonidle hauler percentage. + - broker excluded from all labors when needed at depot. + - likewise, anybody with a scheduled diplomat meeting. + 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 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 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 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 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. + diff --git a/depends/protobuf/CMakeLists.txt b/depends/protobuf/CMakeLists.txt index 24c4b275a..5034f00f4 100644 --- a/depends/protobuf/CMakeLists.txt +++ b/depends/protobuf/CMakeLists.txt @@ -7,10 +7,10 @@ IF(CMAKE_COMPILER_IS_GNUCC) STRING(REGEX MATCHALL "[0-9]+" GCC_VERSION_COMPONENTS ${GCC_VERSION}) LIST(GET GCC_VERSION_COMPONENTS 0 GCC_MAJOR) LIST(GET GCC_VERSION_COMPONENTS 1 GCC_MINOR) - IF(GCC_MAJOR LESS 4 OR (GCC_MAJOR EQUAL 4 AND GCC_MINOR LESS 2)) + #IF(GCC_MAJOR LESS 4 OR (GCC_MAJOR EQUAL 4 AND GCC_MINOR LESS 2)) #GCC is too old - SET(STL_HASH_OLD_GCC 1) - ENDIF() + # SET(STL_HASH_OLD_GCC 1) + #ENDIF() #SET(CMAKE_CXX_FLAGS "-std=c++0x") SET(HAVE_HASH_MAP 0) diff --git a/dfhack.init-example b/dfhack.init-example index 658f6e9be..83c3641b6 100644 --- a/dfhack.init-example +++ b/dfhack.init-example @@ -51,7 +51,7 @@ keybinding add Shift-G "job-material GLASS_GREEN" keybinding add Ctrl-M@dwarfmode/QueryBuilding/Some gui/mechanisms # browse rooms of same owner -keybinding add Alt-R@dwarfmode/QueryBuilding/Some gui/room-list.work +keybinding add Alt-R@dwarfmode/QueryBuilding/Some gui/room-list # interface for the liquids plugin keybinding add Alt-L@dwarfmode/LookAround gui/liquids @@ -59,12 +59,12 @@ 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 -# interface for universal game master's editor -keybinding add Alt-Shift-E gui/gm-editor +# siege engine control +keybinding add Alt-A@dwarfmode/QueryBuilding/Some/SiegeEngine gui/siege-engine -################### -# UI logic tweaks # -################### +############################ +# UI and game logic tweaks # +############################ # stabilize the cursor of dwarfmode when switching menus tweak stable-cursor @@ -77,3 +77,16 @@ tweak readable-build-plate # improve FPS by squashing endless item temperature update loops tweak stable-temp + +# speed up items reaching temp equilibrium with environment by +# 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 diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index 109a97e7c..536f4d34d 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -286,6 +286,10 @@ SET_TARGET_PROPERTIES(dfhack PROPERTIES LINK_INTERFACE_LIBRARIES "") TARGET_LINK_LIBRARIES(dfhack-client protobuf-lite clsocket) TARGET_LINK_LIBRARIES(dfhack-run dfhack-client) +if(APPLE) + add_custom_command(TARGET dfhack-run COMMAND ${dfhack_SOURCE_DIR}/package/darwin/fix-libs.sh WORKING_DIRECTORY ../ COMMENT "Fixing library dependencies...") +endif() + IF(UNIX) if (APPLE) install(PROGRAMS ${dfhack_SOURCE_DIR}/package/darwin/dfhack diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 807cbf539..d73d14cf9 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -79,6 +79,8 @@ distribution. #include "df/building_civzonest.h" #include "df/region_map_entry.h" #include "df/flow_info.h" +#include "df/unit_misc_trait.h" +#include "df/proj_itemst.h" #include #include @@ -813,12 +815,20 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = { WRAPM(Units, getVisibleName), WRAPM(Units, getIdentity), WRAPM(Units, getNemesis), + WRAPM(Units, isCrazed), + WRAPM(Units, isOpposedToLife), + WRAPM(Units, hasExtravision), + WRAPM(Units, isBloodsucker), + WRAPM(Units, isMischievous), + WRAPM(Units, getMiscTrait), WRAPM(Units, isDead), WRAPM(Units, isAlive), WRAPM(Units, isSane), WRAPM(Units, isDwarf), WRAPM(Units, isCitizen), WRAPM(Units, getAge), + WRAPM(Units, getEffectiveSkill), + WRAPM(Units, computeMovementSpeed), WRAPM(Units, getProfessionName), WRAPM(Units, getCasteProfessionName), WRAPM(Units, getProfessionColor), @@ -876,6 +886,18 @@ 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; + return Items::makeProjectile(mc, item); +} + static const LuaWrapper::FunctionReg dfhack_items_module[] = { WRAPM(Items, getGeneralRef), WRAPM(Items, getSpecificRef), @@ -887,6 +909,8 @@ static const LuaWrapper::FunctionReg dfhack_items_module[] = { WRAPN(moveToContainer, items_moveToContainer), WRAPN(moveToBuilding, items_moveToBuilding), WRAPN(moveToInventory, items_moveToInventory), + WRAPN(makeProjectile, items_makeProjectile), + WRAPN(remove, items_remove), { NULL, NULL } }; @@ -1060,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(state, 1); df::coord2d size(luaL_optint(state, 2, 1), luaL_optint(state, 3, 1)); @@ -1081,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 }, { NULL, NULL } }; diff --git a/library/Process-linux.cpp b/library/Process-linux.cpp index 4a66470f9..1fecbab78 100644 --- a/library/Process-linux.cpp +++ b/library/Process-linux.cpp @@ -127,6 +127,9 @@ void Process::getMemRanges( vector & ranges ) char permissions[5]; // r/-, w/-, x/-, p/s, 0 FILE *mapFile = ::fopen("/proc/self/maps", "r"); + if (!mapFile) + return; + size_t start, end, offset, device1, device2, node; while (fgets(buffer, 1024, mapFile)) @@ -148,6 +151,8 @@ void Process::getMemRanges( vector & ranges ) temp.valid = true; ranges.push_back(temp); } + + fclose(mapFile); } uint32_t Process::getBase() diff --git a/library/RemoteClient.cpp b/library/RemoteClient.cpp index 4d30988c6..09861ad5f 100644 --- a/library/RemoteClient.cpp +++ b/library/RemoteClient.cpp @@ -394,7 +394,7 @@ command_result RemoteFunctionBase::execute(color_ostream &out, //out.print("Received %d:%d\n", header.id, header.size); - if (header.id == RPC_REPLY_FAIL) + if ((DFHack::DFHackReplyCode)header.id == RPC_REPLY_FAIL) return header.size == CR_OK ? CR_FAILURE : command_result(header.size); if (header.size < 0 || header.size > RPCMessageHeader::MAX_MESSAGE_SIZE) diff --git a/library/RemoteServer.cpp b/library/RemoteServer.cpp index 53428f2bd..06a9f859c 100644 --- a/library/RemoteServer.cpp +++ b/library/RemoteServer.cpp @@ -250,7 +250,7 @@ void ServerConnection::threadFn() break; } - if (header.id == RPC_REQUEST_QUIT) + if ((DFHack::DFHackReplyCode)header.id == RPC_REQUEST_QUIT) break; if (header.size < 0 || header.size > RPCMessageHeader::MAX_MESSAGE_SIZE) diff --git a/library/RemoteTools.cpp b/library/RemoteTools.cpp index 95c495e93..b371d60fa 100644 --- a/library/RemoteTools.cpp +++ b/library/RemoteTools.cpp @@ -287,7 +287,7 @@ void DFHack::describeUnit(BasicUnitInfo *info, df::unit *unit, if (mask && mask->profession()) { - if (unit->profession >= 0) + if (unit->profession >= (df::profession)0) info->set_profession(unit->profession); if (!unit->custom_profession.empty()) info->set_custom_profession(unit->custom_profession); diff --git a/library/VTableInterpose.cpp b/library/VTableInterpose.cpp index 583ef5184..046425653 100644 --- a/library/VTableInterpose.cpp +++ b/library/VTableInterpose.cpp @@ -438,6 +438,8 @@ void VMethodInterposeLinkBase::remove() if (next) prev->child_next.insert(next); + else + prev->child_hosts.insert(host); } } diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index 591a0c3ff..61d5dec41 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -518,7 +518,7 @@ namespace DFHack { template inline const char *enum_item_raw_key(T val) { typedef df::enum_traits traits; - return traits::is_valid(val) ? traits::key_table[val - traits::first_item_value] : NULL; + return traits::is_valid(val) ? traits::key_table[(short)val - traits::first_item_value] : NULL; } /** diff --git a/library/include/DataFuncs.h b/library/include/DataFuncs.h index 52039566c..01a798e34 100644 --- a/library/include/DataFuncs.h +++ b/library/include/DataFuncs.h @@ -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 struct function_wrapper { \ 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 diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h index 6b1afb88b..3330e23e7 100644 --- a/library/include/LuaTools.h +++ b/library/include/LuaTools.h @@ -287,6 +287,11 @@ namespace DFHack {namespace Lua { PushDFObject(state, ptr); } + template inline void SetField(lua_State *L, T val, int idx, const char *name) { + if (idx < 0) idx = lua_absindex(L, idx); + Push(L, val); lua_setfield(L, idx, name); + } + template void PushVector(lua_State *state, const T &pvec, bool addn = false) { diff --git a/library/include/RemoteTools.h b/library/include/RemoteTools.h index 65884badc..e87e8026b 100644 --- a/library/include/RemoteTools.h +++ b/library/include/RemoteTools.h @@ -88,7 +88,7 @@ namespace DFHack { typedef df::enum_traits traits; int base = traits::first_item; - int size = traits::last_item - base + 1; + int size = (int)traits::last_item - base + 1; describeEnum(pf, base, size, traits::key_table); } diff --git a/library/include/modules/Items.h b/library/include/modules/Items.h index 4236f068a..81c8e1285 100644 --- a/library/include/modules/Items.h +++ b/library/include/modules/Items.h @@ -44,6 +44,7 @@ distribution. namespace df { struct itemdef; + struct proj_itemst; } namespace MapExtras { @@ -155,5 +156,11 @@ DFHACK_EXPORT bool moveToContainer(MapExtras::MapCache &mc, df::item *item, df:: DFHACK_EXPORT bool moveToBuilding(MapExtras::MapCache &mc, df::item *item, df::building_actual *building,int16_t use_mode); DFHACK_EXPORT bool moveToInventory(MapExtras::MapCache &mc, df::item *item, df::unit *unit, df::unit_inventory_item::T_mode mode = df::unit_inventory_item::Carried, int body_part = -1); + +/// 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); } } diff --git a/library/include/modules/Materials.h b/library/include/modules/Materials.h index 76c89de30..fb5a6353c 100644 --- a/library/include/modules/Materials.h +++ b/library/include/modules/Materials.h @@ -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); diff --git a/library/include/modules/Units.h b/library/include/modules/Units.h index 9003dc3af..65f0b58a0 100644 --- a/library/include/modules/Units.h +++ b/library/include/modules/Units.h @@ -32,6 +32,10 @@ distribution. #include "modules/Items.h" #include "DataDefs.h" #include "df/unit.h" +#include "df/misc_trait_type.h" +#include "df/physical_attribute_type.h" +#include "df/mental_attribute_type.h" +#include "df/job_skill.h" namespace df { @@ -41,6 +45,7 @@ namespace df struct historical_entity; struct entity_position_assignment; struct entity_position; + struct unit_misc_trait; } /** @@ -208,6 +213,18 @@ DFHACK_EXPORT df::language_name *getVisibleName(df::unit *unit); DFHACK_EXPORT df::assumed_identity *getIdentity(df::unit *unit); DFHACK_EXPORT df::nemesis_record *getNemesis(df::unit *unit); +DFHACK_EXPORT bool isHidingCurse(df::unit *unit); +DFHACK_EXPORT int getPhysicalAttrValue(df::unit *unit, df::physical_attribute_type attr); +DFHACK_EXPORT int getMentalAttrValue(df::unit *unit, df::mental_attribute_type attr); + +DFHACK_EXPORT bool isCrazed(df::unit *unit); +DFHACK_EXPORT bool isOpposedToLife(df::unit *unit); +DFHACK_EXPORT bool hasExtravision(df::unit *unit); +DFHACK_EXPORT bool isBloodsucker(df::unit *unit); +DFHACK_EXPORT bool isMischievous(df::unit *unit); + +DFHACK_EXPORT df::unit_misc_trait *getMiscTrait(df::unit *unit, df::misc_trait_type type, bool create = false); + DFHACK_EXPORT bool isDead(df::unit *unit); DFHACK_EXPORT bool isAlive(df::unit *unit); DFHACK_EXPORT bool isSane(df::unit *unit); @@ -216,6 +233,9 @@ DFHACK_EXPORT bool isDwarf(df::unit *unit); DFHACK_EXPORT double getAge(df::unit *unit, bool true_age = false); +DFHACK_EXPORT int getEffectiveSkill(df::unit *unit, df::job_skill skill_id); +DFHACK_EXPORT int computeMovementSpeed(df::unit *unit); + struct NoblePosition { df::historical_entity *entity; df::entity_position_assignment *assignment; diff --git a/library/lua/class.lua b/library/lua/class.lua new file mode 100644 index 000000000..7b142e499 --- /dev/null +++ b/library/lua/class.lua @@ -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 diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua index e96bb0f4b..ce3be5a87 100644 --- a/library/lua/dfhack.lua +++ b/library/lua/dfhack.lua @@ -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 @@ -169,6 +157,23 @@ function xyz2pos(x,y,z) end end +function pos2xy(pos) + if pos then + local x = pos.x + if x and x ~= -30000 then + return x, pos.y + end + end +end + +function xy2pos(x,y) + if x then + return {x=x,y=y} + else + return {x=-30000,y=-30000} + end +end + function safe_index(obj,idx,...) if obj == nil or idx == nil then return nil diff --git a/library/lua/gui.lua b/library/lua/gui.lua index f9b6ab6d2..6eaa98606 100644 --- a/library/lua/gui.lua +++ b/library/lua/gui.lua @@ -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 diff --git a/library/lua/gui/dialogs.lua b/library/lua/gui/dialogs.lua index c4f15c9ac..b1a96a558 100644 --- a/library/lua/gui/dialogs.lua +++ b/library/lua/gui/dialogs.lua @@ -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, @@ -172,5 +166,103 @@ function showInputPrompt(title, text, tcolor, input, on_input, on_cancel, min_wi }:show() end +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) + self.page_top = 0 +end + +function ListBox:getWantedFrameSize() + local mw, mh = ListBox.super.getWantedFrameSize(self) + return mw, mh+#self.choices +end + +function ListBox:onRenderBody(dc) + ListBox.super.onRenderBody(self, dc) + + dc:newline(1) + + if self.selection>dc.height-3 then + self.page_top=self.selection-(dc.height-3) + elseif self.selection0 then + self.page_top=self.selection-1 + end + for i,entry in ipairs(self.choices) do + if type(entry)=="table" then + entry=entry[1] + end + if i>self.page_top then + if i == self.selection then + dc:pen(self.select_pen or COLOR_LIGHTCYAN) + else + dc:pen(self.text_pen or COLOR_GREY) + end + dc:string(entry) + dc:newline(1) + end + end +end + +function ListBox:moveCursor(delta) + local newsel=self.selection+delta + if #self.choices ~=0 then + if newsel<1 or newsel>#self.choices then + newsel=newsel % #self.choices + end + end + self.selection=newsel +end + +function ListBox:onInput(keys) + if keys.SELECT then + self:dismiss() + local choice=self.choices[self.selection] + if self.on_input then + self.on_input(self.selection,choice) + end + + if choice and choice[2] then + choice[2](choice,self.selection) -- maybe reverse the arguments? + end + elseif keys.LEAVESCREEN then + self:dismiss() + if self.on_cancel then + self.on_cancel() + end + elseif keys.CURSOR_UP then + self:moveCursor(-1) + elseif keys.CURSOR_DOWN then + self:moveCursor(1) + elseif keys.CURSOR_UP_FAST then + self:moveCursor(-10) + elseif keys.CURSOR_DOWN_FAST then + self:moveCursor(10) + end +end + +function showListPrompt(title, text, tcolor, choices, on_input, on_cancel, min_width) + ListBox{ + frame_title = title, + text = text, + text_pen = tcolor, + choices = choices, + on_input = on_input, + on_cancel = on_cancel, + frame_width = min_width, + }:show() +end return _ENV diff --git a/library/lua/gui/dwarfmode.lua b/library/lua/gui/dwarfmode.lua index 1f7ae1b03..ba3cfbe6c 100644 --- a/library/lua/gui/dwarfmode.lua +++ b/library/lua/gui/dwarfmode.lua @@ -46,7 +46,7 @@ function getPanelLayout() end function getCursorPos() - if g_cursor ~= -30000 then + if g_cursor.x ~= -30000 then return copyall(g_cursor) end end @@ -136,6 +136,14 @@ function Viewport:set() return vp end +function Viewport:getPos() + return xyz2pos(self.x1, self.y1, self.z) +end + +function Viewport:getSize() + return xy2pos(self.width, self.height) +end + function Viewport:clip(x,y,z) return self:make( math.max(0, math.min(x or self.x1, world_map.x_count-self.width)), @@ -159,6 +167,18 @@ function Viewport:isVisible(target,gap) return self:isVisibleXY(target,gap) and target.z == self.z end +function Viewport:tileToScreen(coord) + return xyz2pos(coord.x - self.x1, coord.y - self.y1, coord.z - self.z) +end + +function Viewport:getCenter() + return xyz2pos( + math.floor((self.x2+self.x1)/2), + math.floor((self.y2+self.y1)/2), + self.z + ) +end + function Viewport:centerOn(target) return self:clip( target.x - math.floor(self.width/2), @@ -207,16 +227,24 @@ MOVEMENT_KEYS = { CURSOR_UP_Z_AUX = { 0, 0, 1 }, CURSOR_DOWN_Z_AUX = { 0, 0, -1 }, } -function Viewport:scrollByKey(key) +local function get_movement_delta(key, delta, big_step) local info = MOVEMENT_KEYS[key] if info then - local delta = 10 - if info[4] then delta = 20 end + if info[4] then + delta = big_step + end + + return delta*info[1], delta*info[2], info[3] + end +end +function Viewport:scrollByKey(key) + local dx, dy, dz = get_movement_delta(key, 10, 20) + if dx then return self:clip( - self.x1 + delta*info[1], - self.y1 + delta*info[2], - self.z + info[3] + self.x1 + dx, + self.y1 + dy, + self.z + dz ) else return self @@ -237,16 +265,23 @@ function DwarfOverlay:getViewport(old_vp) end end -function DwarfOverlay:moveCursorTo(cursor,viewport) +function DwarfOverlay:moveCursorTo(cursor,viewport,gap) setCursorPos(cursor) - self:getViewport(viewport):reveal(cursor, 5, 0, 10):set() + self:zoomViewportTo(cursor,viewport,gap) +end + +function DwarfOverlay:zoomViewportTo(target, viewport, gap) + if gap and self:getViewport():isVisible(target, gap) then + return + end + self:getViewport(viewport):reveal(target, 5, 0, 10):set() end -function DwarfOverlay:selectBuilding(building,cursor,viewport) +function DwarfOverlay:selectBuilding(building,cursor,viewport,gap) cursor = cursor or utils.getBuildingCenter(building) df.global.world.selected_building = building - self:moveCursorTo(cursor, viewport) + self:moveCursorTo(cursor, viewport, gap) end function DwarfOverlay:propagateMoveKeys(keys) @@ -282,6 +317,31 @@ function DwarfOverlay:simulateViewScroll(keys, anchor, no_clip_cursor) end end +function DwarfOverlay:simulateCursorMovement(keys, anchor) + local layout = self.df_layout + local cursor = getCursorPos() + local cx, cy, cz = pos2xyz(cursor) + + if anchor and keys.A_MOVE_SAME_SQUARE then + setCursorPos(anchor) + self:getViewport():centerOn(anchor):set() + return 'A_MOVE_SAME_SQUARE' + end + + for code,_ in pairs(MOVEMENT_KEYS) do + if keys[code] then + local dx, dy, dz = get_movement_delta(code, 1, 10) + local ncur = xyz2pos(cx+dx, cy+dy, cz+dz) + + if dfhack.maps.isValidTilePos(ncur) then + setCursorPos(ncur) + self:getViewport():reveal(ncur,4,10,6,true):set() + return code + end + end + end +end + function DwarfOverlay:onAboutToShow(below) local screen = dfhack.gui.getCurViewscreen() if below then screen = below.parent end @@ -293,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 @@ -301,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 diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 8de908734..1662f4467 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -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(vector_get(layer->layer_objects,idx)); } @@ -173,10 +173,9 @@ DEFINE_GET_FOCUS_STRING_HANDLER(dwarfmode) else if (id == &df::building_trapst::_identity) { auto trap = (df::building_trapst*)selected; - if (trap->trap_type == trap_type::Lever) { - focus += "/Lever"; + focus += "/" + enum_item_key(trap->trap_type); + if (trap->trap_type == trap_type::Lever) jobs = true; - } } else if (ui_building_in_assign && *ui_building_in_assign && ui_building_assign_type && ui_building_assign_units && @@ -189,6 +188,8 @@ DEFINE_GET_FOCUS_STRING_HANDLER(dwarfmode) focus += unit ? "/Unit" : "/None"; } } + else + focus += "/" + enum_item_key(selected->getType()); if (jobs) { @@ -331,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) @@ -419,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"; @@ -457,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"); } } @@ -843,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); diff --git a/library/modules/Items.cpp b/library/modules/Items.cpp index dc64143c9..b8c697a48 100644 --- a/library/modules/Items.cpp +++ b/library/modules/Items.cpp @@ -72,8 +72,11 @@ using namespace std; #include "df/general_ref_contains_itemst.h" #include "df/general_ref_contained_in_itemst.h" #include "df/general_ref_building_holderst.h" +#include "df/general_ref_projectile.h" #include "df/viewscreen_itemst.h" #include "df/vermin.h" +#include "df/proj_itemst.h" +#include "df/proj_list_link.h" #include "df/unit_inventory_item.h" #include "df/body_part_raw.h" @@ -88,6 +91,7 @@ using namespace df::enums; using df::global::world; using df::global::ui; using df::global::ui_selected_unit; +using df::global::proj_next_id; #define ITEMDEF_VECTORS \ ITEM(WEAPON, weapons, itemdef_weaponst) \ @@ -726,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; } @@ -866,3 +882,64 @@ 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); + + if (!world || !proj_next_id) + return NULL; + + auto pos = getPosition(item); + if (!pos.isValid()) + return NULL; + + auto ref = df::allocate(); + if (!ref) + return NULL; + + if (!detachItem(mc, item)) + { + delete ref; + return NULL; + } + + item->pos = pos; + item->flags.bits.in_job = true; + + auto proj = new df::proj_itemst(); + proj->link = new df::proj_list_link(); + proj->link->item = proj; + proj->id = (*proj_next_id)++; + + proj->origin_pos = proj->target_pos = pos; + proj->cur_pos = proj->prev_pos = pos; + proj->item = item; + + ref->projectile_id = proj->id; + item->itemrefs.push_back(ref); + + linked_list_append(&world->proj_list, proj->link); + + return proj; +} diff --git a/library/modules/Job.cpp b/library/modules/Job.cpp index 54b4eb27e..b74a4b73f 100644 --- a/library/modules/Job.cpp +++ b/library/modules/Job.cpp @@ -181,7 +181,7 @@ void DFHack::Job::printItemDetails(color_ostream &out, df::job_item *item, int i out << " reaction class: " << item->reaction_class << endl; if (!item->has_material_reaction_product.empty()) out << " reaction product: " << item->has_material_reaction_product << endl; - if (item->has_tool_use >= 0) + if (item->has_tool_use >= (df::tool_uses)0) out << " tool use: " << ENUM_KEY_STR(tool_uses, item->has_tool_use) << endl; } diff --git a/library/modules/Maps.cpp b/library/modules/Maps.cpp index d0401164a..f1f40f19c 100644 --- a/library/modules/Maps.cpp +++ b/library/modules/Maps.cpp @@ -307,7 +307,7 @@ df::feature_init *Maps::getLocalInitFeature(df::coord2d rgn_pos, int32_t index) df::coord2d bigregion = rgn_pos / 16; // bigregion is 16x16 regions. for each bigregion in X dimension: - auto fptr = data->unk_204[bigregion.x][bigregion.y].features; + auto fptr = data->feature_map[bigregion.x][bigregion.y].features; if (!fptr) return NULL; diff --git a/library/modules/Materials.cpp b/library/modules/Materials.cpp index 50cf21a9c..db9c9c7df 100644 --- a/library/modules/Materials.cpp +++ b/library/modules/Materials.cpp @@ -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()) diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 874dabc3d..01b7b50f4 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -63,11 +63,15 @@ using namespace std; #include "df/burrow.h" #include "df/creature_raw.h" #include "df/caste_raw.h" +#include "df/game_mode.h" +#include "df/unit_misc_trait.h" +#include "df/unit_skill.h" using namespace DFHack; using namespace df::enums; using df::global::world; using df::global::ui; +using df::global::gamemode; bool Units::isValid() { @@ -613,6 +617,58 @@ df::nemesis_record *Units::getNemesis(df::unit *unit) return NULL; } + +bool Units::isHidingCurse(df::unit *unit) +{ + if (!unit->job.hunt_target) + { + auto identity = Units::getIdentity(unit); + if (identity && identity->unk_4c == 0) + return true; + } + + return false; +} + +int Units::getPhysicalAttrValue(df::unit *unit, df::physical_attribute_type attr) +{ + auto &aobj = unit->body.physical_attrs[attr]; + int value = std::max(0, aobj.value - aobj.soft_demotion); + + if (auto mod = unit->curse.attr_change) + { + int mvalue = (value * mod->phys_att_perc[attr] / 100) + mod->phys_att_add[attr]; + + if (isHidingCurse(unit)) + value = std::min(value, mvalue); + else + value = mvalue; + } + + return std::max(0, value); +} + +int Units::getMentalAttrValue(df::unit *unit, df::mental_attribute_type attr) +{ + auto soul = unit->status.current_soul; + if (!soul) return 0; + + auto &aobj = soul->mental_attrs[attr]; + int value = std::max(0, aobj.value - aobj.soft_demotion); + + if (auto mod = unit->curse.attr_change) + { + int mvalue = (value * mod->ment_att_perc[attr] / 100) + mod->ment_att_add[attr]; + + if (isHidingCurse(unit)) + value = std::min(value, mvalue); + else + value = mvalue; + } + + return std::max(0, value); +} + static bool casteFlagSet(int race, int caste, df::caste_raw_flags flag) { auto creature = df::creature_raw::find(race); @@ -626,8 +682,9 @@ static bool casteFlagSet(int race, int caste, df::caste_raw_flags flag) return craw->flags.is_set(flag); } -static bool isCrazed(df::unit *unit) +bool Units::isCrazed(df::unit *unit) { + CHECK_NULL_POINTER(unit); if (unit->flags3.bits.scuttle) return false; if (unit->curse.rem_tags1.bits.CRAZED) @@ -637,13 +694,64 @@ static bool isCrazed(df::unit *unit) return casteFlagSet(unit->race, unit->caste, caste_raw_flags::CRAZED); } -static bool isOpposedToLife(df::unit *unit) +bool Units::isOpposedToLife(df::unit *unit) { + CHECK_NULL_POINTER(unit); if (unit->curse.rem_tags1.bits.OPPOSED_TO_LIFE) return false; if (unit->curse.add_tags1.bits.OPPOSED_TO_LIFE) return true; - return casteFlagSet(unit->race, unit->caste, caste_raw_flags::CANNOT_UNDEAD); + return casteFlagSet(unit->race, unit->caste, caste_raw_flags::OPPOSED_TO_LIFE); +} + +bool Units::hasExtravision(df::unit *unit) +{ + CHECK_NULL_POINTER(unit); + if (unit->curse.rem_tags1.bits.EXTRAVISION) + return false; + if (unit->curse.add_tags1.bits.EXTRAVISION) + return true; + return casteFlagSet(unit->race, unit->caste, caste_raw_flags::EXTRAVISION); +} + +bool Units::isBloodsucker(df::unit *unit) +{ + CHECK_NULL_POINTER(unit); + if (unit->curse.rem_tags1.bits.BLOODSUCKER) + return false; + if (unit->curse.add_tags1.bits.BLOODSUCKER) + return true; + return casteFlagSet(unit->race, unit->caste, caste_raw_flags::BLOODSUCKER); +} + +bool Units::isMischievous(df::unit *unit) +{ + CHECK_NULL_POINTER(unit); + if (unit->curse.rem_tags1.bits.MISCHIEVOUS) + return false; + if (unit->curse.add_tags1.bits.MISCHIEVOUS) + return true; + return casteFlagSet(unit->race, unit->caste, caste_raw_flags::MISCHIEVOUS); +} + +df::unit_misc_trait *Units::getMiscTrait(df::unit *unit, df::misc_trait_type type, bool create) +{ + CHECK_NULL_POINTER(unit); + + auto &vec = unit->status.misc_traits; + for (size_t i = 0; i < vec.size(); i++) + if (vec[i]->id == type) + return vec[i]; + + if (create) + { + auto obj = new df::unit_misc_trait(); + obj->id = type; + vec.push_back(obj); + return obj; + } + + return NULL; } bool DFHack::Units::isDead(df::unit *unit) @@ -753,6 +861,371 @@ double DFHack::Units::getAge(df::unit *unit, bool true_age) return cur_time - birth_time; } +inline void adjust_skill_rating(int &rating, bool is_adventure, int value, int dwarf3_4, int dwarf1_2, int adv9_10, int adv3_4, int adv1_2) +{ + if (is_adventure) + { + if (value >= adv1_2) rating >>= 1; + else if (value >= adv3_4) rating = rating*3/4; + else if (value >= adv9_10) rating = rating*9/10; + } + else + { + if (value >= dwarf1_2) rating >>= 1; + else if (value >= dwarf3_4) rating = rating*3/4; + } +} + +int Units::getEffectiveSkill(df::unit *unit, df::job_skill skill_id) +{ + CHECK_NULL_POINTER(unit); + + /* + * This is 100% reverse-engineered from DF code. + */ + + if (!unit->status.current_soul) + return 0; + + // Retrieve skill from unit soul: + + df::enum_field key(skill_id); + auto skill = binsearch_in_vector(unit->status.current_soul->skills, &df::unit_skill::id, key); + + int rating = 0; + if (skill) + rating = std::max(0, int(skill->rating) - skill->rusty); + + // Apply special states + + if (unit->counters.soldier_mood == df::unit::T_counters::None) + { + if (unit->counters.nausea > 0) rating >>= 1; + if (unit->counters.winded > 0) rating >>= 1; + if (unit->counters.stunned > 0) rating >>= 1; + if (unit->counters.dizziness > 0) rating >>= 1; + if (unit->counters2.fever > 0) rating >>= 1; + } + + if (unit->counters.soldier_mood != df::unit::T_counters::MartialTrance) + { + if (!unit->flags3.bits.ghostly && !unit->flags3.bits.scuttle && + !unit->flags2.bits.vision_good && !unit->flags2.bits.vision_damaged && + !hasExtravision(unit)) + { + rating >>= 2; + } + if (unit->counters.pain >= 100 && unit->mood == -1) + { + rating >>= 1; + } + if (unit->counters2.exhaustion >= 2000) + { + rating = rating*3/4; + if (unit->counters2.exhaustion >= 4000) + { + rating = rating*3/4; + if (unit->counters2.exhaustion >= 6000) + rating = rating*3/4; + } + } + } + + // Hunger etc timers + + bool is_adventure = (gamemode && *gamemode == game_mode::ADVENTURE); + + if (!unit->flags3.bits.scuttle && isBloodsucker(unit)) + { + using namespace df::enums::misc_trait_type; + + if (auto trait = getMiscTrait(unit, TimeSinceSuckedBlood)) + { + adjust_skill_rating( + rating, is_adventure, trait->value, + 302400, 403200, // dwf 3/4; 1/2 + 1209600, 1209600, 2419200 // adv 9/10; 3/4; 1/2 + ); + } + } + + adjust_skill_rating( + rating, is_adventure, unit->counters2.thirst_timer, + 50000, 50000, 115200, 172800, 345600 + ); + adjust_skill_rating( + rating, is_adventure, unit->counters2.hunger_timer, + 75000, 75000, 172800, 1209600, 2592000 + ); + if (is_adventure && unit->counters2.sleepiness_timer >= 846000) + rating >>= 2; + else + adjust_skill_rating( + rating, is_adventure, unit->counters2.sleepiness_timer, + 150000, 150000, 172800, 259200, 345600 + ); + + return rating; +} + +inline void adjust_speed_rating(int &rating, bool is_adventure, int value, int dwarf100, int dwarf200, int adv50, int adv75, int adv100, int adv200) +{ + if (is_adventure) + { + if (value >= adv200) rating += 200; + else if (value >= adv100) rating += 100; + else if (value >= adv75) rating += 75; + else if (value >= adv50) rating += 50; + } + else + { + if (value >= dwarf200) rating += 200; + else if (value >= dwarf100) rating += 100; + } +} + +static int calcInventoryWeight(df::unit *unit) +{ + int armor_skill = Units::getEffectiveSkill(unit, job_skill::ARMOR); + int armor_mul = 15 - std::min(15, armor_skill); + + int inv_weight = 0, inv_weight_fraction = 0; + + for (size_t i = 0; i < unit->inventory.size(); i++) + { + auto item = unit->inventory[i]->item; + if (!item->flags.bits.weight_computed) + continue; + + int wval = item->weight; + int wfval = item->weight_fraction; + auto mode = unit->inventory[i]->mode; + + if ((mode == df::unit_inventory_item::Worn || + mode == df::unit_inventory_item::WrappedAround) && + item->isArmor() && armor_skill > 1) + { + wval = wval * armor_mul / 16; + wfval = wfval * armor_mul / 16; + } + + inv_weight += wval; + inv_weight_fraction += wfval; + } + + return inv_weight*100 + inv_weight_fraction/10000; +} + +int Units::computeMovementSpeed(df::unit *unit) +{ + using namespace df::enums::physical_attribute_type; + + /* + * Pure reverse-engineered computation of unit _slowness_, + * i.e. number of ticks to move * 100. + */ + + // Base speed + + auto creature = df::creature_raw::find(unit->race); + if (!creature) + return 0; + + auto craw = vector_get(creature->caste, unit->caste); + if (!craw) + return 0; + + int speed = craw->misc.speed; + + if (unit->flags3.bits.ghostly) + return speed; + + // Curse multiplier + + if (unit->curse.speed_mul_percent != 100) + { + speed *= 100; + if (unit->curse.speed_mul_percent != 0) + speed /= unit->curse.speed_mul_percent; + } + + speed += unit->curse.speed_add; + + // Swimming + + auto cur_liquid = unit->status2.liquid_type.bits.liquid_type; + bool in_magma = (cur_liquid == tile_liquid::Magma); + + if (unit->flags2.bits.swimming) + { + speed = craw->misc.swim_speed; + if (in_magma) + speed *= 2; + + if (craw->flags.is_set(caste_raw_flags::SWIMS_LEARNED)) + { + int skill = Units::getEffectiveSkill(unit, job_skill::SWIMMING); + + // Originally a switch: + if (skill > 1) + speed = speed * std::max(6, 21-skill) / 20; + } + } + else + { + int delta = 150*unit->status2.liquid_depth; + if (in_magma) + delta *= 2; + speed += delta; + } + + // General counters and flags + + if (unit->profession == profession::BABY) + speed += 3000; + + if (unit->flags3.bits.unk15) + speed /= 20; + + if (unit->counters2.exhaustion >= 2000) + { + speed += 200; + if (unit->counters2.exhaustion >= 4000) + { + speed += 200; + if (unit->counters2.exhaustion >= 6000) + speed += 200; + } + } + + if (unit->flags2.bits.gutted) speed += 2000; + + if (unit->counters.soldier_mood == df::unit::T_counters::None) + { + if (unit->counters.nausea > 0) speed += 1000; + if (unit->counters.winded > 0) speed += 1000; + if (unit->counters.stunned > 0) speed += 1000; + if (unit->counters.dizziness > 0) speed += 1000; + if (unit->counters2.fever > 0) speed += 1000; + } + + if (unit->counters.soldier_mood != df::unit::T_counters::MartialTrance) + { + if (unit->counters.pain >= 100 && unit->mood == -1) + speed += 1000; + } + + // Hunger etc timers + + bool is_adventure = (gamemode && *gamemode == game_mode::ADVENTURE); + + if (!unit->flags3.bits.scuttle && Units::isBloodsucker(unit)) + { + using namespace df::enums::misc_trait_type; + + if (auto trait = Units::getMiscTrait(unit, TimeSinceSuckedBlood)) + { + adjust_speed_rating( + speed, is_adventure, trait->value, + 302400, 403200, // dwf 100; 200 + 1209600, 1209600, 1209600, 2419200 // adv 50; 75; 100; 200 + ); + } + } + + adjust_speed_rating( + speed, is_adventure, unit->counters2.thirst_timer, + 50000, 0x7fffffff, 172800, 172800, 172800, 345600 + ); + adjust_speed_rating( + speed, is_adventure, unit->counters2.hunger_timer, + 75000, 0x7fffffff, 1209600, 1209600, 1209600, 2592000 + ); + adjust_speed_rating( + speed, is_adventure, unit->counters2.sleepiness_timer, + 57600, 150000, 172800, 259200, 345600, 864000 + ); + + // Activity state + + if (unit->relations.draggee_id != -1) speed += 1000; + + if (unit->flags1.bits.on_ground) + speed += 2000; + else if (unit->flags3.bits.on_crutch) + { + int skill = Units::getEffectiveSkill(unit, job_skill::CRUTCH_WALK); + speed += 2000 - 100*std::min(20, skill); + } + + if (unit->flags1.bits.hidden_in_ambush && !Units::isMischievous(unit)) + { + int skill = Units::getEffectiveSkill(unit, job_skill::SNEAK); + speed += 2000 - 100*std::min(20, skill); + } + + if (unsigned(unit->counters2.paralysis-1) <= 98) + speed += unit->counters2.paralysis*10; + if (unsigned(unit->counters.webbed-1) <= 8) + speed += unit->counters.webbed*100; + + // Muscle weight vs vascular tissue (?) + + auto &attr_tissue = unit->body.physical_attr_tissues; + int muscle = attr_tissue[STRENGTH]; + int blood = attr_tissue[AGILITY]; + speed = std::max(speed*3/4, std::min(speed*3/2, int(int64_t(speed)*muscle/blood))); + + // Attributes + + int strength_attr = Units::getPhysicalAttrValue(unit, STRENGTH); + int agility_attr = Units::getPhysicalAttrValue(unit, AGILITY); + + int total_attr = std::max(200, std::min(3800, strength_attr + agility_attr)); + speed = ((total_attr-200)*(speed/2) + (3800-total_attr)*(speed*3/2))/3600; + + // Stance + + if (!unit->flags1.bits.on_ground && unit->status2.able_stand > 2) + { + // WTF + int as = unit->status2.able_stand; + int x = (as-1) - (as>>1); + int y = as - unit->status2.able_stand_impair; + if (unit->flags3.bits.on_crutch) y--; + y = y * 500 / x; + if (y > 0) speed += y; + } + + // Mood + + if (unit->mood == mood_type::Melancholy) speed += 8000; + + // Inventory encumberance + + int total_weight = calcInventoryWeight(unit); + int free_weight = std::max(1, muscle/10 + strength_attr*3); + + if (free_weight < total_weight) + { + int delta = (total_weight - free_weight)/10 + 1; + if (!is_adventure) + delta = std::min(5000, delta); + speed += delta; + } + + // skipped: unknown loop on inventory items that amounts to 0 change + + if (is_adventure) + { + auto player = vector_get(world->units.active, 0); + if (player && player->id == unit->relations.group_leader_id) + speed = std::min(speed, computeMovementSpeed(player)); + } + + return std::min(10000, std::max(0, speed)); +} + static bool noble_pos_compare(const Units::NoblePosition &a, const Units::NoblePosition &b) { if (a.position->precedence < b.position->precedence) @@ -838,7 +1311,7 @@ std::string DFHack::Units::getCasteProfessionName(int race, int casteid, df::pro { std::string prof, race_prefix; - if (pid < 0 || !is_valid_enum_item(pid)) + if (pid < (df::profession)0 || !is_valid_enum_item(pid)) return ""; bool use_race_prefix = (race >= 0 && race != df::global::ui->race_id); diff --git a/library/modules/World.cpp b/library/modules/World.cpp index e14aa02a0..67b8c1236 100644 --- a/library/modules/World.cpp +++ b/library/modules/World.cpp @@ -300,6 +300,8 @@ PersistentDataItem World::GetPersistentData(const std::string &key, bool *added) void World::GetPersistentData(std::vector *vec, const std::string &key, bool prefix) { + vec->clear(); + if (!BuildPersistentCache()) return; @@ -343,8 +345,10 @@ bool World::DeletePersistentData(const PersistentDataItem &item) auto eqrange = d->persistent_index.equal_range(item.key_value); - for (auto it = eqrange.first; it != eqrange.second; ++it) + for (auto it2 = eqrange.first; it2 != eqrange.second; ) { + auto it = it2; ++it2; + if (it->second != -item.id) continue; diff --git a/library/modules/kitchen.cpp b/library/modules/kitchen.cpp index 4300d63df..aa235780d 100644 --- a/library/modules/kitchen.cpp +++ b/library/modules/kitchen.cpp @@ -114,7 +114,7 @@ void Kitchen::fillWatchMap(std::map& watchMap) watchMap.clear(); for(std::size_t i = 0; i < size(); ++i) { - if(ui->kitchen.item_subtypes[i] == limitType && ui->kitchen.item_subtypes[i] == limitSubtype && ui->kitchen.exc_types[i] == limitExclusion) + if(ui->kitchen.item_subtypes[i] == (short)limitType && ui->kitchen.item_subtypes[i] == (short)limitSubtype && ui->kitchen.exc_types[i] == limitExclusion) { watchMap[ui->kitchen.mat_indices[i]] = (unsigned int) ui->kitchen.mat_types[i]; } diff --git a/library/xml b/library/xml index df8178a98..a914f3b75 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit df8178a989373ec7868d9195d82ae5f85145ef81 +Subproject commit a914f3b7558335d53c0ac93f6e7267906a33cd29 diff --git a/package/darwin/dfhack-run b/package/darwin/dfhack-run index 865c8bd21..cc69db964 100755 --- a/package/darwin/dfhack-run +++ b/package/darwin/dfhack-run @@ -3,7 +3,6 @@ DF_DIR=$(dirname "$0") cd "${DF_DIR}" -export DYLD_LIBRARY_PATH=${PWD}/hack:${PWD}/libs -export DYLD_FRAMEWORK_PATH=${PWD}/hack${PWD}/libs +export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:"./stonesense/deplibs":"./hack" exec hack/dfhack-run "$@" diff --git a/package/darwin/fix-libs.sh b/package/darwin/fix-libs.sh new file mode 100755 index 000000000..cff98b6a6 --- /dev/null +++ b/package/darwin/fix-libs.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +BUILD_DIR=`pwd` + +echo "Fixing library dependencies in $BUILD_DIR/library" + +install_name_tool -change $BUILD_DIR/library/libdfhack.1.0.0.dylib @executable_path/hack/libdfhack.1.0.0.dylib library/libdfhack.1.0.0.dylib +install_name_tool -change $BUILD_DIR/library/libdfhack-client.dylib @executable_path/hack/libdfhack-client.dylib library/libdfhack-client.dylib +install_name_tool -change $BUILD_DIR/library/libdfhack-client.dylib @executable_path/hack/libdfhack-client.dylib library/dfhack-run +install_name_tool -change $BUILD_DIR/depends/protobuf/libprotobuf-lite.dylib @executable_path/hack/libprotobuf-lite.dylib library/libdfhack.1.0.0.dylib +install_name_tool -change $BUILD_DIR/depends/protobuf/libprotobuf-lite.dylib @executable_path/hack/libprotobuf-lite.dylib library/libdfhack-client.dylib +install_name_tool -change $BUILD_DIR/depends/protobuf/libprotobuf-lite.dylib @executable_path/hack/libprotobuf-lite.dylib library/dfhack-run +install_name_tool -change $BUILD_DIR/depends/lua/liblua.dylib @executable_path/hack/liblua.dylib library/libdfhack.1.0.0.dylib +install_name_tool -change @executable_path/../Frameworks/SDL.framework/Versions/A/SDL @executable_path/libs/SDL.framework/Versions/A/SDL library/libdfhack.1.0.0.dylib +install_name_tool -change /usr/local/lib/libstdc++.6.dylib @executable_path/libs/libstdc++.6.dylib library/libdfhack.1.0.0.dylib +install_name_tool -change /opt/local/lib/i386/libstdc++.6.dylib @executable_path/libs/libstdc++.6.dylib library/libdfhack.1.0.0.dylib +install_name_tool -change /opt/local/lib/i386/libstdc++.6.dylib @executable_path/libs/libstdc++.6.dylib library/libdfhack-client.dylib +install_name_tool -change /opt/local/lib/i386/libstdc++.6.dylib @executable_path/libs/libstdc++.6.dylib library/dfhack-run +install_name_tool -change /opt/local/lib/i386/libgcc_s.1.dylib @executable_path/libs/libgcc_s.1.dylib library/libdfhack.1.0.0.dylib +install_name_tool -change /opt/local/lib/i386/libgcc_s.1.dylib @executable_path/libs/libgcc_s.1.dylib library/libdfhack-client.dylib +install_name_tool -change /opt/local/lib/i386/libgcc_s.1.dylib @executable_path/libs/libgcc_s.1.dylib library/dfhack-run \ No newline at end of file diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 04da3e6c8..0b0ad0461 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -44,6 +44,12 @@ endif() install(DIRECTORY lua/ DESTINATION ${DFHACK_LUA_DESTINATION}/plugins FILES_MATCHING PATTERN "*.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) @@ -114,6 +120,10 @@ if (BUILD_SUPPORTED) # this one exports functions to lua DFHACK_PLUGIN(burrows burrows.cpp LINK_LIBRARIES lua) 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() diff --git a/plugins/add-spatter.cpp b/plugins/add-spatter.cpp new file mode 100644 index 000000000..35ea11ef5 --- /dev/null +++ b/plugins/add-spatter.cpp @@ -0,0 +1,433 @@ +#include "Core.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#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 products; +}; + +static std::map reactions; +static std::map 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 > 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 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 *in_reag, + std::vector *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 *out_items, + std::vector *in_reag, + std::vector *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(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(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 &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; +} diff --git a/plugins/autolabor.cpp b/plugins/autolabor.cpp index c3a2b313e..c39b126c9 100644 --- a/plugins/autolabor.cpp +++ b/plugins/autolabor.cpp @@ -9,6 +9,7 @@ #include #include +#include "modules/Units.h" #include "modules/World.h" // DF data structure definition headers @@ -358,11 +359,11 @@ static const dwarf_state dwarf_states[] = { OTHER /* DrinkBlood */, OTHER /* ReportCrime */, OTHER /* ExecuteCriminal */, - BUSY /* TrainAnimal */, - BUSY /* CarveTrack */, - BUSY /* PushTrackVehicle */, - BUSY /* PlaceTrackVehicle */, - BUSY /* StoreItemInVehicle */ + BUSY /* TrainAnimal */, + BUSY /* CarveTrack */, + BUSY /* PushTrackVehicle */, + BUSY /* PlaceTrackVehicle */, + BUSY /* StoreItemInVehicle */ }; struct labor_info @@ -397,108 +398,108 @@ static int hauler_pct = 33; static std::vector labor_infos; static const struct labor_default default_labor_infos[] = { - /* MINE */ {AUTOMATIC, true, 2, 200, 0}, - /* HAUL_STONE */ {HAULERS, false, 1, 200, 0}, - /* HAUL_WOOD */ {HAULERS, false, 1, 200, 0}, - /* HAUL_BODY */ {HAULERS, false, 1, 200, 0}, - /* HAUL_FOOD */ {HAULERS, false, 1, 200, 0}, - /* HAUL_REFUSE */ {HAULERS, false, 1, 200, 0}, - /* HAUL_ITEM */ {HAULERS, false, 1, 200, 0}, - /* HAUL_FURNITURE */ {HAULERS, false, 1, 200, 0}, - /* HAUL_ANIMAL */ {HAULERS, false, 1, 200, 0}, - /* CLEAN */ {HAULERS, false, 1, 200, 0}, - /* CUTWOOD */ {AUTOMATIC, true, 1, 200, 0}, - /* CARPENTER */ {AUTOMATIC, false, 1, 200, 0}, - /* DETAIL */ {AUTOMATIC, false, 1, 200, 0}, - /* MASON */ {AUTOMATIC, false, 1, 200, 0}, - /* ARCHITECT */ {AUTOMATIC, false, 1, 200, 0}, - /* ANIMALTRAIN */ {AUTOMATIC, false, 1, 200, 0}, - /* ANIMALCARE */ {AUTOMATIC, false, 1, 200, 0}, - /* DIAGNOSE */ {AUTOMATIC, false, 1, 200, 0}, - /* SURGERY */ {AUTOMATIC, false, 1, 200, 0}, - /* BONE_SETTING */ {AUTOMATIC, false, 1, 200, 0}, - /* SUTURING */ {AUTOMATIC, false, 1, 200, 0}, - /* DRESSING_WOUNDS */ {AUTOMATIC, false, 1, 200, 0}, - /* FEED_WATER_CIVILIANS */ {AUTOMATIC, false, 200, 200, 0}, - /* RECOVER_WOUNDED */ {HAULERS, false, 1, 200, 0}, - /* BUTCHER */ {AUTOMATIC, false, 1, 200, 0}, - /* TRAPPER */ {AUTOMATIC, false, 1, 200, 0}, - /* DISSECT_VERMIN */ {AUTOMATIC, false, 1, 200, 0}, - /* LEATHER */ {AUTOMATIC, false, 1, 200, 0}, - /* TANNER */ {AUTOMATIC, false, 1, 200, 0}, - /* BREWER */ {AUTOMATIC, false, 1, 200, 0}, - /* ALCHEMIST */ {AUTOMATIC, false, 1, 200, 0}, - /* SOAP_MAKER */ {AUTOMATIC, false, 1, 200, 0}, - /* WEAVER */ {AUTOMATIC, false, 1, 200, 0}, - /* CLOTHESMAKER */ {AUTOMATIC, false, 1, 200, 0}, - /* MILLER */ {AUTOMATIC, false, 1, 200, 0}, - /* PROCESS_PLANT */ {AUTOMATIC, false, 1, 200, 0}, - /* MAKE_CHEESE */ {AUTOMATIC, false, 1, 200, 0}, - /* MILK */ {AUTOMATIC, false, 1, 200, 0}, - /* COOK */ {AUTOMATIC, false, 1, 200, 0}, - /* PLANT */ {AUTOMATIC, false, 1, 200, 0}, - /* HERBALIST */ {AUTOMATIC, false, 1, 200, 0}, - /* FISH */ {AUTOMATIC, false, 1, 1, 0}, - /* CLEAN_FISH */ {AUTOMATIC, false, 1, 200, 0}, - /* DISSECT_FISH */ {AUTOMATIC, false, 1, 200, 0}, - /* HUNT */ {AUTOMATIC, true, 1, 1, 0}, - /* SMELT */ {AUTOMATIC, false, 1, 200, 0}, - /* FORGE_WEAPON */ {AUTOMATIC, false, 1, 200, 0}, - /* FORGE_ARMOR */ {AUTOMATIC, false, 1, 200, 0}, - /* FORGE_FURNITURE */ {AUTOMATIC, false, 1, 200, 0}, - /* METAL_CRAFT */ {AUTOMATIC, false, 1, 200, 0}, - /* CUT_GEM */ {AUTOMATIC, false, 1, 200, 0}, - /* ENCRUST_GEM */ {AUTOMATIC, false, 1, 200, 0}, - /* WOOD_CRAFT */ {AUTOMATIC, false, 1, 200, 0}, - /* STONE_CRAFT */ {AUTOMATIC, false, 1, 200, 0}, - /* BONE_CARVE */ {AUTOMATIC, false, 1, 200, 0}, - /* GLASSMAKER */ {AUTOMATIC, false, 1, 200, 0}, - /* EXTRACT_STRAND */ {AUTOMATIC, false, 1, 200, 0}, - /* SIEGECRAFT */ {AUTOMATIC, false, 1, 200, 0}, - /* SIEGEOPERATE */ {AUTOMATIC, false, 1, 200, 0}, - /* BOWYER */ {AUTOMATIC, false, 1, 200, 0}, - /* MECHANIC */ {AUTOMATIC, false, 1, 200, 0}, - /* POTASH_MAKING */ {AUTOMATIC, false, 1, 200, 0}, - /* LYE_MAKING */ {AUTOMATIC, false, 1, 200, 0}, - /* DYER */ {AUTOMATIC, false, 1, 200, 0}, - /* BURN_WOOD */ {AUTOMATIC, false, 1, 200, 0}, - /* OPERATE_PUMP */ {AUTOMATIC, false, 1, 200, 0}, - /* SHEARER */ {AUTOMATIC, false, 1, 200, 0}, - /* SPINNER */ {AUTOMATIC, false, 1, 200, 0}, - /* POTTERY */ {AUTOMATIC, false, 1, 200, 0}, - /* GLAZING */ {AUTOMATIC, false, 1, 200, 0}, - /* PRESSING */ {AUTOMATIC, false, 1, 200, 0}, - /* BEEKEEPING */ {AUTOMATIC, false, 1, 1, 0}, // reduce risk of stuck beekeepers (see http://www.bay12games.com/dwarves/mantisbt/view.php?id=3981) - /* WAX_WORKING */ {AUTOMATIC, false, 1, 200, 0}, + /* MINE */ {AUTOMATIC, true, 2, 200, 0}, + /* HAUL_STONE */ {HAULERS, false, 1, 200, 0}, + /* HAUL_WOOD */ {HAULERS, false, 1, 200, 0}, + /* HAUL_BODY */ {HAULERS, false, 1, 200, 0}, + /* HAUL_FOOD */ {HAULERS, false, 1, 200, 0}, + /* HAUL_REFUSE */ {HAULERS, false, 1, 200, 0}, + /* HAUL_ITEM */ {HAULERS, false, 1, 200, 0}, + /* HAUL_FURNITURE */ {HAULERS, false, 1, 200, 0}, + /* HAUL_ANIMAL */ {HAULERS, false, 1, 200, 0}, + /* CLEAN */ {HAULERS, false, 1, 200, 0}, + /* CUTWOOD */ {AUTOMATIC, true, 1, 200, 0}, + /* CARPENTER */ {AUTOMATIC, false, 1, 200, 0}, + /* DETAIL */ {AUTOMATIC, false, 1, 200, 0}, + /* MASON */ {AUTOMATIC, false, 1, 200, 0}, + /* ARCHITECT */ {AUTOMATIC, false, 1, 200, 0}, + /* ANIMALTRAIN */ {AUTOMATIC, false, 1, 200, 0}, + /* ANIMALCARE */ {AUTOMATIC, false, 1, 200, 0}, + /* DIAGNOSE */ {AUTOMATIC, false, 1, 200, 0}, + /* SURGERY */ {AUTOMATIC, false, 1, 200, 0}, + /* BONE_SETTING */ {AUTOMATIC, false, 1, 200, 0}, + /* SUTURING */ {AUTOMATIC, false, 1, 200, 0}, + /* DRESSING_WOUNDS */ {AUTOMATIC, false, 1, 200, 0}, + /* FEED_WATER_CIVILIANS */ {AUTOMATIC, false, 200, 200, 0}, + /* RECOVER_WOUNDED */ {HAULERS, false, 1, 200, 0}, + /* BUTCHER */ {AUTOMATIC, false, 1, 200, 0}, + /* TRAPPER */ {AUTOMATIC, false, 1, 200, 0}, + /* DISSECT_VERMIN */ {AUTOMATIC, false, 1, 200, 0}, + /* LEATHER */ {AUTOMATIC, false, 1, 200, 0}, + /* TANNER */ {AUTOMATIC, false, 1, 200, 0}, + /* BREWER */ {AUTOMATIC, false, 1, 200, 0}, + /* ALCHEMIST */ {AUTOMATIC, false, 1, 200, 0}, + /* SOAP_MAKER */ {AUTOMATIC, false, 1, 200, 0}, + /* WEAVER */ {AUTOMATIC, false, 1, 200, 0}, + /* CLOTHESMAKER */ {AUTOMATIC, false, 1, 200, 0}, + /* MILLER */ {AUTOMATIC, false, 1, 200, 0}, + /* PROCESS_PLANT */ {AUTOMATIC, false, 1, 200, 0}, + /* MAKE_CHEESE */ {AUTOMATIC, false, 1, 200, 0}, + /* MILK */ {AUTOMATIC, false, 1, 200, 0}, + /* COOK */ {AUTOMATIC, false, 1, 200, 0}, + /* PLANT */ {AUTOMATIC, false, 1, 200, 0}, + /* HERBALIST */ {AUTOMATIC, false, 1, 200, 0}, + /* FISH */ {AUTOMATIC, false, 1, 1, 0}, + /* CLEAN_FISH */ {AUTOMATIC, false, 1, 200, 0}, + /* DISSECT_FISH */ {AUTOMATIC, false, 1, 200, 0}, + /* HUNT */ {AUTOMATIC, true, 1, 1, 0}, + /* SMELT */ {AUTOMATIC, false, 1, 200, 0}, + /* FORGE_WEAPON */ {AUTOMATIC, false, 1, 200, 0}, + /* FORGE_ARMOR */ {AUTOMATIC, false, 1, 200, 0}, + /* FORGE_FURNITURE */ {AUTOMATIC, false, 1, 200, 0}, + /* METAL_CRAFT */ {AUTOMATIC, false, 1, 200, 0}, + /* CUT_GEM */ {AUTOMATIC, false, 1, 200, 0}, + /* ENCRUST_GEM */ {AUTOMATIC, false, 1, 200, 0}, + /* WOOD_CRAFT */ {AUTOMATIC, false, 1, 200, 0}, + /* STONE_CRAFT */ {AUTOMATIC, false, 1, 200, 0}, + /* BONE_CARVE */ {AUTOMATIC, false, 1, 200, 0}, + /* GLASSMAKER */ {AUTOMATIC, false, 1, 200, 0}, + /* EXTRACT_STRAND */ {AUTOMATIC, false, 1, 200, 0}, + /* SIEGECRAFT */ {AUTOMATIC, false, 1, 200, 0}, + /* SIEGEOPERATE */ {AUTOMATIC, false, 1, 200, 0}, + /* BOWYER */ {AUTOMATIC, false, 1, 200, 0}, + /* MECHANIC */ {AUTOMATIC, false, 1, 200, 0}, + /* POTASH_MAKING */ {AUTOMATIC, false, 1, 200, 0}, + /* LYE_MAKING */ {AUTOMATIC, false, 1, 200, 0}, + /* DYER */ {AUTOMATIC, false, 1, 200, 0}, + /* BURN_WOOD */ {AUTOMATIC, false, 1, 200, 0}, + /* OPERATE_PUMP */ {AUTOMATIC, false, 1, 200, 0}, + /* SHEARER */ {AUTOMATIC, false, 1, 200, 0}, + /* SPINNER */ {AUTOMATIC, false, 1, 200, 0}, + /* POTTERY */ {AUTOMATIC, false, 1, 200, 0}, + /* GLAZING */ {AUTOMATIC, false, 1, 200, 0}, + /* PRESSING */ {AUTOMATIC, false, 1, 200, 0}, + /* BEEKEEPING */ {AUTOMATIC, false, 1, 1, 0}, // reduce risk of stuck beekeepers (see http://www.bay12games.com/dwarves/mantisbt/view.php?id=3981) + /* WAX_WORKING */ {AUTOMATIC, false, 1, 200, 0}, /* PUSH_HAUL_VEHICLES */ {HAULERS, false, 1, 200, 0} }; static const int responsibility_penalties[] = { - 0, /* LAW_MAKING */ - 0, /* LAW_ENFORCEMENT */ - 3000, /* RECEIVE_DIPLOMATS */ - 0, /* MEET_WORKERS */ - 1000, /* MANAGE_PRODUCTION */ - 3000, /* TRADE */ - 1000, /* ACCOUNTING */ - 0, /* ESTABLISH_COLONY_TRADE_AGREEMENTS */ - 0, /* MAKE_INTRODUCTIONS */ - 0, /* MAKE_PEACE_AGREEMENTS */ - 0, /* MAKE_TOPIC_AGREEMENTS */ - 0, /* COLLECT_TAXES */ - 0, /* ESCORT_TAX_COLLECTOR */ - 0, /* EXECUTIONS */ - 0, /* TAME_EXOTICS */ - 0, /* RELIGION */ - 0, /* ATTACK_ENEMIES */ - 0, /* PATROL_TERRITORY */ - 0, /* MILITARY_GOALS */ - 0, /* MILITARY_STRATEGY */ - 0, /* UPGRADE_SQUAD_EQUIPMENT */ - 0, /* EQUIPMENT_MANIFESTS */ - 0, /* SORT_AMMUNITION */ - 0, /* BUILD_MORALE */ - 5000 /* HEALTH_MANAGEMENT */ + 0, /* LAW_MAKING */ + 0, /* LAW_ENFORCEMENT */ + 3000, /* RECEIVE_DIPLOMATS */ + 0, /* MEET_WORKERS */ + 1000, /* MANAGE_PRODUCTION */ + 3000, /* TRADE */ + 1000, /* ACCOUNTING */ + 0, /* ESTABLISH_COLONY_TRADE_AGREEMENTS */ + 0, /* MAKE_INTRODUCTIONS */ + 0, /* MAKE_PEACE_AGREEMENTS */ + 0, /* MAKE_TOPIC_AGREEMENTS */ + 0, /* COLLECT_TAXES */ + 0, /* ESCORT_TAX_COLLECTOR */ + 0, /* EXECUTIONS */ + 0, /* TAME_EXOTICS */ + 0, /* RELIGION */ + 0, /* ATTACK_ENEMIES */ + 0, /* PATROL_TERRITORY */ + 0, /* MILITARY_GOALS */ + 0, /* MILITARY_STRATEGY */ + 0, /* UPGRADE_SQUAD_EQUIPMENT */ + 0, /* EQUIPMENT_MANIFESTS */ + 0, /* SORT_AMMUNITION */ + 0, /* BUILD_MORALE */ + 5000 /* HEALTH_MANAGEMENT */ }; struct dwarf_info_t @@ -537,7 +538,7 @@ static void cleanup_state() labor_infos.clear(); } -static void reset_labor(df::enums::unit_labor::unit_labor labor) +static void reset_labor(df::unit_labor labor) { labor_infos[labor].set_minimum_dwarfs(default_labor_infos[labor].minimum_dwarfs); labor_infos[labor].set_maximum_dwarfs(default_labor_infos[labor].maximum_dwarfs); @@ -576,7 +577,7 @@ static void init_state() for (auto p = items.begin(); p != items.end(); p++) { string key = p->key(); - df::enums::unit_labor::unit_labor labor = (df::enums::unit_labor::unit_labor) atoi(key.substr(strlen("autolabor/labors/")).c_str()); + df::unit_labor labor = (df::unit_labor) atoi(key.substr(strlen("autolabor/labors/")).c_str()); if (labor >= 0 && labor <= labor_infos.size()) { labor_infos[labor].config = *p; @@ -597,7 +598,7 @@ static void init_state() labor_infos[i].is_exclusive = default_labor_infos[i].is_exclusive; labor_infos[i].active_dwarfs = 0; - reset_labor((df::enums::unit_labor::unit_labor) i); + reset_labor((df::unit_labor) i); } generate_labor_to_skill_map(); @@ -611,12 +612,12 @@ static void generate_labor_to_skill_map() // Generate labor -> skill mapping for (int i = 0; i <= ENUM_LAST_ITEM(unit_labor); i++) - labor_to_skill[i] = df::enums::job_skill::NONE; + labor_to_skill[i] = job_skill::NONE; FOR_ENUM_ITEMS(job_skill, skill) { int labor = ENUM_ATTR(job_skill, labor, skill); - if (labor != df::enums::unit_labor::NONE) + if (labor != unit_labor::NONE) { /* assert(labor >= 0); @@ -779,7 +780,7 @@ static void assign_labor(unit_labor::unit_labor labor, int value = dwarf_info[dwarf].mastery_penalty; - if (skill != df::enums::job_skill::NONE) + if (skill != job_skill::NONE) { int skill_level = 0; int skill_experience = 0; @@ -843,9 +844,9 @@ static void assign_labor(unit_labor::unit_labor labor, int max_dwarfs = labor_infos[labor].maximum_dwarfs(); // Special - don't assign hunt without a butchers, or fish without a fishery - if (df::enums::unit_labor::HUNT == labor && !has_butchers) + if (unit_labor::HUNT == labor && !has_butchers) min_dwarfs = max_dwarfs = 0; - if (df::enums::unit_labor::FISH == labor && !has_fishery) + if (unit_labor::FISH == labor && !has_fishery) min_dwarfs = max_dwarfs = 0; bool want_idle_dwarf = true; @@ -956,15 +957,15 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) { df::building *build = world->buildings.all[i]; auto type = build->getType(); - if (df::enums::building_type::Workshop == type) + if (building_type::Workshop == type) { - auto subType = build->getSubtype(); - if (df::enums::workshop_type::Butchers == subType) + df::workshop_type subType = (df::workshop_type)build->getSubtype(); + if (workshop_type::Butchers == subType) has_butchers = true; - if (df::enums::workshop_type::Fishery == subType) + if (workshop_type::Fishery == subType) has_fishery = true; } - else if (df::enums::building_type::TradeDepot == type) + else if (building_type::TradeDepot == type) { df::building_tradedepotst* depot = (df::building_tradedepotst*) build; trader_requested = depot->trade_flags.bits.trader_requested; @@ -978,11 +979,10 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) } } - for (int i = 0; i < world->units.all.size(); ++i) + for (int i = 0; i < world->units.active.size(); ++i) { - df::unit* cre = world->units.all[i]; - if (cre->race == race && cre->civ_id == civ && !cre->flags1.bits.marauder && !cre->flags1.bits.diplomat && !cre->flags1.bits.merchant && - !cre->flags1.bits.dead && !cre->flags1.bits.forest) + df::unit* cre = world->units.active[i]; + if (Units::isCitizen(cre)) { if (cre->burrows.size() > 0) continue; // dwarfs assigned to burrows are skipped entirely @@ -1003,9 +1003,6 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) { dwarf_info[dwarf].single_labor = -1; -// assert(dwarfs[dwarf]->status.souls.size() > 0); -// assert fails can cause DF to crash, so don't do that - if (dwarfs[dwarf]->status.souls.size() <= 0) continue; @@ -1076,7 +1073,7 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) // Track total & highest skill among normal/medical skills. (We don't care about personal or social skills.) - if (skill_class != df::enums::job_skill_class::Normal && skill_class != df::enums::job_skill_class::Medical) + if (skill_class != job_skill_class::Normal && skill_class != job_skill_class::Medical) continue; if (dwarf_info[dwarf].highest_skill < skill_level) @@ -1093,16 +1090,11 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) dwarf_info[dwarf].mastery_penalty -= 10 * dwarf_info[dwarf].total_skill; dwarf_info[dwarf].mastery_penalty -= dwarf_info[dwarf].noble_penalty; - for (int labor = ENUM_FIRST_ITEM(unit_labor); labor <= ENUM_LAST_ITEM(unit_labor); labor++) + FOR_ENUM_ITEMS(unit_labor, labor) { - if (labor == df::enums::unit_labor::NONE) + if (labor == unit_labor::NONE) continue; - /* - assert(labor >= 0); - assert(labor < ARRAY_COUNT(labor_infos)); - */ - if (labor_infos[labor].is_exclusive && dwarfs[dwarf]->status.labors[labor]) dwarf_info[dwarf].mastery_penalty -= 100; } @@ -1120,15 +1112,13 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) for (auto p = dwarfs[dwarf]->status.misc_traits.begin(); p < dwarfs[dwarf]->status.misc_traits.end(); p++) { - // 7 / 0x7 = Newly arrived migrant, will not work yet - // 17 / 0x11 = On break - if ((*p)->id == 0x07 || (*p)->id == 0x11) + if ((*p)->id == misc_trait_type::Migrant || (*p)->id == misc_trait_type::OnBreak) is_on_break = true; } - if (dwarfs[dwarf]->profession == df::enums::profession::BABY || - dwarfs[dwarf]->profession == df::enums::profession::CHILD || - dwarfs[dwarf]->profession == df::enums::profession::DRUNK) + if (dwarfs[dwarf]->profession == profession::BABY || + dwarfs[dwarf]->profession == profession::CHILD || + dwarfs[dwarf]->profession == profession::DRUNK) { dwarf_info[dwarf].state = CHILD; } @@ -1146,18 +1136,13 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) else { int job = dwarfs[dwarf]->job.current_job->job_type; - - /* - assert(job >= 0); - assert(job < ARRAY_COUNT(dwarf_states)); - */ - if (job >= 0 && job < ARRAY_COUNT(dwarf_states)) - dwarf_info[dwarf].state = dwarf_states[job]; - else - { - out.print("Dwarf %i \"%s\" has unknown job %i\n", dwarf, dwarfs[dwarf]->name.first_name.c_str(), job); - dwarf_info[dwarf].state = OTHER; - } + if (job >= 0 && job < ARRAY_COUNT(dwarf_states)) + dwarf_info[dwarf].state = dwarf_states[job]; + else + { + out.print("Dwarf %i \"%s\" has unknown job %i\n", dwarf, dwarfs[dwarf]->name.first_name.c_str(), job); + dwarf_info[dwarf].state = OTHER; + } } state_count[dwarf_info[dwarf].state]++; @@ -1170,14 +1155,9 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) FOR_ENUM_ITEMS(unit_labor, labor) { - if (labor == df::enums::unit_labor::NONE) + if (labor == unit_labor::NONE) continue; - /* - assert(labor >= 0); - assert(labor < ARRAY_COUNT(labor_infos)); - */ - labor_infos[labor].active_dwarfs = 0; labors.push_back(labor); @@ -1217,11 +1197,6 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) { auto labor = *lp; - /* - assert(labor >= 0); - assert(labor < ARRAY_COUNT(labor_infos)); - */ - assign_labor(labor, n_dwarfs, dwarf_info, trader_requested, dwarfs, has_butchers, has_fishery, out); } @@ -1241,7 +1216,7 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) { FOR_ENUM_ITEMS(unit_labor, labor) { - if (labor == df::enums::unit_labor::NONE) + if (labor == unit_labor::NONE) continue; if (labor_infos[labor].mode() != HAULERS) continue; @@ -1264,14 +1239,9 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) FOR_ENUM_ITEMS(unit_labor, labor) { - if (labor == df::enums::unit_labor::NONE) + if (labor == unit_labor::NONE) continue; - /* - assert(labor >= 0); - assert(labor < ARRAY_COUNT(labor_infos)); - */ - if (labor_infos[labor].mode() != HAULERS) continue; @@ -1311,7 +1281,7 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) return CR_OK; } -void print_labor (df::enums::unit_labor::unit_labor labor, color_ostream &out) +void print_labor (df::unit_labor labor, color_ostream &out) { string labor_name = ENUM_KEY_STR(unit_labor, labor); out << labor_name << ": "; @@ -1358,7 +1328,6 @@ command_result autolabor (color_ostream &out, std::vector & parame return CR_OK; } - else if (parameters.size() == 2 && parameters[0] == "haulpct") { if (!enable_autolabor) @@ -1371,15 +1340,15 @@ command_result autolabor (color_ostream &out, std::vector & parame hauler_pct = pct; return CR_OK; } - else if (parameters.size() == 2 || parameters.size() == 3) { - + else if (parameters.size() == 2 || parameters.size() == 3) + { if (!enable_autolabor) { out << "Error: The plugin is not enabled." << endl; return CR_FAILURE; } - df::enums::unit_labor::unit_labor labor = df::enums::unit_labor::NONE; + df::unit_labor labor = unit_labor::NONE; FOR_ENUM_ITEMS(unit_labor, test_labor) { @@ -1387,7 +1356,7 @@ command_result autolabor (color_ostream &out, std::vector & parame labor = test_labor; } - if (labor == df::enums::unit_labor::NONE) + if (labor == unit_labor::NONE) { out.printerr("Could not find labor %s.\n", parameters[0].c_str()); return CR_WRONG_USAGE; @@ -1430,7 +1399,8 @@ command_result autolabor (color_ostream &out, std::vector & parame return CR_OK; } - else if (parameters.size() == 1 && parameters[0] == "reset-all") { + else if (parameters.size() == 1 && parameters[0] == "reset-all") + { if (!enable_autolabor) { out << "Error: The plugin is not enabled." << endl; @@ -1439,12 +1409,13 @@ command_result autolabor (color_ostream &out, std::vector & parame for (int i = 0; i < labor_infos.size(); i++) { - reset_labor((df::enums::unit_labor::unit_labor) i); + reset_labor((df::unit_labor) i); } out << "All labors reset." << endl; return CR_OK; } - else if (parameters.size() == 1 && parameters[0] == "list" || parameters[0] == "status") { + else if (parameters.size() == 1 && parameters[0] == "list" || parameters[0] == "status") + { if (!enable_autolabor) { out << "Error: The plugin is not enabled." << endl; @@ -1467,7 +1438,7 @@ command_result autolabor (color_ostream &out, std::vector & parame { FOR_ENUM_ITEMS(unit_labor, labor) { - if (labor == df::enums::unit_labor::NONE) + if (labor == unit_labor::NONE) continue; print_labor(labor, out); @@ -1571,7 +1542,7 @@ static int stockcheck(color_ostream &out, vector & parameters) { df::building *build = world->buildings.all[i]; auto type = build->getType(); - if (df::enums::building_type::Stockpile == type) + if (building_type::Stockpile == type) { df::building_stockpilest *sp = virtual_cast(build); StockpileInfo *spi = new StockpileInfo(sp); @@ -1580,7 +1551,7 @@ static int stockcheck(color_ostream &out, vector & parameters) } - std::vector &items = world->items.other[df::enums::items_other_id::ANY_FREE]; + std::vector &items = world->items.other[items_other_id::ANY_FREE]; // Precompute a bitmask with the bad flags df::item_flags bad_flags; @@ -1602,13 +1573,13 @@ static int stockcheck(color_ostream &out, vector & parameters) // we really only care about MEAT, FISH, FISH_RAW, PLANT, CHEESE, FOOD, and EGG df::item_type typ = item->getType(); - if (typ != df::enums::item_type::MEAT && - typ != df::enums::item_type::FISH && - typ != df::enums::item_type::FISH_RAW && - typ != df::enums::item_type::PLANT && - typ != df::enums::item_type::CHEESE && - typ != df::enums::item_type::FOOD && - typ != df::enums::item_type::EGG) + if (typ != item_type::MEAT && + typ != item_type::FISH && + typ != item_type::FISH_RAW && + typ != item_type::PLANT && + typ != item_type::CHEESE && + typ != item_type::FOOD && + typ != item_type::EGG) continue; df::item *container = 0; @@ -1673,11 +1644,11 @@ static int stockcheck(color_ostream &out, vector & parameters) if (building) { df::building_type btype = building->getType(); - if (btype == df::enums::building_type::TradeDepot || - btype == df::enums::building_type::Wagon) + if (btype == building_type::TradeDepot || + btype == building_type::Wagon) continue; // items in trade depot or the embark wagon do not rot - if (typ == df::enums::item_type::EGG && btype ==df::enums::building_type::NestBox) + if (typ == item_type::EGG && btype ==building_type::NestBox) continue; // eggs in nest box do not rot } diff --git a/plugins/cleaners.cpp b/plugins/cleaners.cpp index de204f611..319b83c1f 100644 --- a/plugins/cleaners.cpp +++ b/plugins/cleaners.cpp @@ -50,12 +50,12 @@ command_result cleanmap (color_ostream &out, bool snow, bool mud) // filter snow if(!snow && spatter->mat_type == builtin_mats::WATER - && spatter->mat_state == matter_state::Powder) + && spatter->mat_state == (short)matter_state::Powder) continue; // filter mud if(!mud && spatter->mat_type == builtin_mats::MUD - && spatter->mat_state == matter_state::Solid) + && spatter->mat_state == (short)matter_state::Solid) continue; delete evt; @@ -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 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) diff --git a/plugins/cleanowned.cpp b/plugins/cleanowned.cpp index c1521b8de..cd01fd616 100644 --- a/plugins/cleanowned.cpp +++ b/plugins/cleanowned.cpp @@ -116,7 +116,7 @@ command_result df_cleanowned (color_ostream &out, vector & parameters) } else if (item->flags.bits.on_ground) { - int32_t type = item->getType(); + df::item_type type = item->getType(); if(type == item_type::MEAT || type == item_type::FISH || type == item_type::VERMIN || diff --git a/plugins/devel/dumpmats.cpp b/plugins/devel/dumpmats.cpp index ba888e7cf..0af1fce50 100644 --- a/plugins/devel/dumpmats.cpp +++ b/plugins/devel/dumpmats.cpp @@ -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 ¶meters) 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); diff --git a/plugins/devel/nestboxes.cpp b/plugins/devel/nestboxes.cpp index b3d24cd92..42c3c0660 100644 --- a/plugins/devel/nestboxes.cpp +++ b/plugins/devel/nestboxes.cpp @@ -31,6 +31,40 @@ static command_result nestboxes(color_ostream &out, vector & 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(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 &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 & parameters) { @@ -57,60 +104,16 @@ static command_result nestboxes(color_ostream &out, vector & 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(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; } diff --git a/plugins/jobutils.cpp b/plugins/jobutils.cpp index 24ad4170e..dbfe26b90 100644 --- a/plugins/jobutils.cpp +++ b/plugins/jobutils.cpp @@ -372,7 +372,7 @@ static command_result job_cmd(color_ostream &out, vector & parameters) out << "Job item updated." << endl; - if (item->item_type < 0 && minfo.isValid()) + if (item->item_type < (df::item_type)0 && minfo.isValid()) out.printerr("WARNING: Due to a probable bug, creature & plant material subtype\n" " is ignored unless the item type is also specified.\n"); diff --git a/plugins/lua/siege-engine.lua b/plugins/lua/siege-engine.lua new file mode 100644 index 000000000..33e120feb --- /dev/null +++ b/plugins/lua/siege-engine.lua @@ -0,0 +1,229 @@ +local _ENV = mkmodule('plugins.siege-engine') + +--[[ + + Native functions: + + * getTargetArea(building) -> point1, point2 + * 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 getMetrics(engine, path).goal_step then + return path + end + + 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 = {} + + 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) + 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 + +return _ENV \ No newline at end of file diff --git a/plugins/manipulator.cpp b/plugins/manipulator.cpp index 1a90d2eea..f40969655 100644 --- a/plugins/manipulator.cpp +++ b/plugins/manipulator.cpp @@ -252,6 +252,7 @@ struct UnitInfo enum altsort_mode { ALTSORT_NAME, ALTSORT_PROFESSION, + ALTSORT_HAPPINESS, ALTSORT_MAX }; @@ -275,6 +276,14 @@ bool sortByProfession (const UnitInfo *d1, const UnitInfo *d2) return (d1->profession < d2->profession); } +bool sortByHappiness (const UnitInfo *d1, const UnitInfo *d2) +{ + if (descending) + return (d1->unit->status.happiness > d2->unit->status.happiness); + else + return (d1->unit->status.happiness < d2->unit->status.happiness); +} + bool sortBySkill (const UnitInfo *d1, const UnitInfo *d2) { if (sort_skill != job_skill::NONE) @@ -310,6 +319,14 @@ bool sortBySkill (const UnitInfo *d1, const UnitInfo *d2) return sortByName(d1, d2); } +enum display_columns { + DISP_COLUMN_HAPPINESS, + DISP_COLUMN_NAME, + DISP_COLUMN_PROFESSION, + DISP_COLUMN_LABORS, + DISP_COLUMN_MAX, +}; + class viewscreen_unitlaborsst : public dfhack_viewscreen { public: void feed(set *events); @@ -328,10 +345,11 @@ protected: vector units; altsort_mode altsort; - int first_row, sel_row; + int first_row, sel_row, num_rows; int first_column, sel_column; - int height, name_width, prof_width, labors_width; + int col_widths[DISP_COLUMN_MAX]; + int col_offsets[DISP_COLUMN_MAX]; void calcSize (); }; @@ -374,34 +392,52 @@ viewscreen_unitlaborsst::viewscreen_unitlaborsst(vector &src) void viewscreen_unitlaborsst::calcSize() { - height = gps->dimy - 10; - if (height > units.size()) - height = units.size(); - - name_width = prof_width = labors_width = 0; - for (int i = 4; i < gps->dimx; i++) + num_rows = gps->dimy - 10; + if (num_rows > units.size()) + num_rows = units.size(); + + int num_columns = gps->dimx - DISP_COLUMN_MAX - 1; + for (int i = 0; i < DISP_COLUMN_MAX; i++) + col_widths[i] = 0; + while (num_columns > 0) { - // 20% for Name, 20% for Profession, 60% for Labors - switch ((i - 4) % 5) + num_columns--; + // need at least 4 digits for happiness + if (col_widths[DISP_COLUMN_HAPPINESS] < 4) + { + col_widths[DISP_COLUMN_HAPPINESS]++; + continue; + } + // of remaining, 20% for Name, 20% for Profession, 60% for Labors + switch (num_columns % 5) { case 0: case 2: case 4: - labors_width++; + col_widths[DISP_COLUMN_LABORS]++; break; case 1: - name_width++; + col_widths[DISP_COLUMN_NAME]++; break; case 3: - prof_width++; + col_widths[DISP_COLUMN_PROFESSION]++; break; } } - while (labors_width > NUM_COLUMNS) + + while (col_widths[DISP_COLUMN_LABORS] > NUM_COLUMNS) + { + col_widths[DISP_COLUMN_LABORS]--; + if (col_widths[DISP_COLUMN_LABORS] & 1) + col_widths[DISP_COLUMN_NAME]++; + else + col_widths[DISP_COLUMN_PROFESSION]++; + } + + for (int i = 0; i < DISP_COLUMN_MAX; i++) { - if (labors_width & 1) - name_width++; + if (i == 0) + col_offsets[i] = 1; else - prof_width++; - labors_width--; + col_offsets[i] = col_offsets[i - 1] + col_widths[i - 1] + 1; } // don't adjust scroll position immediately after the window opened @@ -409,20 +445,20 @@ void viewscreen_unitlaborsst::calcSize() return; // if the window grows vertically, scroll upward to eliminate blank rows from the bottom - if (first_row > units.size() - height) - first_row = units.size() - height; + if (first_row > units.size() - num_rows) + first_row = units.size() - num_rows; // if it shrinks vertically, scroll downward to keep the cursor visible - if (first_row < sel_row - height + 1) - first_row = sel_row - height + 1; + if (first_row < sel_row - num_rows + 1) + first_row = sel_row - num_rows + 1; // if the window grows horizontally, scroll to the left to eliminate blank columns from the right - if (first_column > NUM_COLUMNS - labors_width) - first_column = NUM_COLUMNS - labors_width; + if (first_column > NUM_COLUMNS - col_widths[DISP_COLUMN_LABORS]) + first_column = NUM_COLUMNS - col_widths[DISP_COLUMN_LABORS]; // if it shrinks horizontally, scroll to the right to keep the cursor visible - if (first_column < sel_column - labors_width + 1) - first_column = sel_column - labors_width + 1; + if (first_column < sel_column - col_widths[DISP_COLUMN_LABORS] + 1) + first_column = sel_column - col_widths[DISP_COLUMN_LABORS] + 1; } void viewscreen_unitlaborsst::feed(set *events) @@ -453,8 +489,8 @@ void viewscreen_unitlaborsst::feed(set *events) if (sel_row < first_row) first_row = sel_row; - if (first_row < sel_row - height + 1) - first_row = sel_row - height + 1; + if (first_row < sel_row - num_rows + 1) + first_row = sel_row - num_rows + 1; if (events->count(interface_key::CURSOR_LEFT) || events->count(interface_key::CURSOR_UPLEFT) || events->count(interface_key::CURSOR_DOWNLEFT)) sel_column--; @@ -489,8 +525,8 @@ void viewscreen_unitlaborsst::feed(set *events) if (sel_column < first_column) first_column = sel_column; - if (first_column < sel_column - labors_width + 1) - first_column = sel_column - labors_width + 1; + if (first_column < sel_column - col_widths[DISP_COLUMN_LABORS] + 1) + first_column = sel_column - col_widths[DISP_COLUMN_LABORS] + 1; UnitInfo *cur = units[sel_row]; if (events->count(interface_key::SELECT) && (cur->allowEdit) && (columns[sel_column].labor != unit_labor::NONE)) @@ -556,6 +592,9 @@ void viewscreen_unitlaborsst::feed(set *events) case ALTSORT_PROFESSION: std::sort(units.begin(), units.end(), sortByProfession); break; + case ALTSORT_HAPPINESS: + std::sort(units.begin(), units.end(), sortByHappiness); + break; } } if (events->count(interface_key::CHANGETAB)) @@ -566,6 +605,9 @@ void viewscreen_unitlaborsst::feed(set *events) altsort = ALTSORT_PROFESSION; break; case ALTSORT_PROFESSION: + altsort = ALTSORT_HAPPINESS; + break; + case ALTSORT_HAPPINESS: altsort = ALTSORT_NAME; break; } @@ -603,9 +645,9 @@ void viewscreen_unitlaborsst::render() dfhack_viewscreen::render(); Screen::clear(); - Screen::drawBorder(" Manage Labors "); + Screen::drawBorder(" Dwarf Manipulator - Manage Labors "); - for (int col = 0; col < labors_width; col++) + for (int col = 0; col < col_widths[DISP_COLUMN_LABORS]; col++) { int col_offset = col + first_column; if (col_offset >= NUM_COLUMNS) @@ -620,21 +662,21 @@ void viewscreen_unitlaborsst::render() bg = 7; } - Screen::paintTile(Screen::Pen(columns[col_offset].label[0], fg, bg), 1 + name_width + 1 + prof_width + 1 + col, 1); - Screen::paintTile(Screen::Pen(columns[col_offset].label[1], fg, bg), 1 + name_width + 1 + prof_width + 1 + col, 2); + Screen::paintTile(Screen::Pen(columns[col_offset].label[0], fg, bg), col_offsets[DISP_COLUMN_LABORS] + col, 1); + Screen::paintTile(Screen::Pen(columns[col_offset].label[1], fg, bg), col_offsets[DISP_COLUMN_LABORS] + col, 2); df::profession profession = columns[col_offset].profession; - if (profession != profession::NONE) + if ((profession != profession::NONE) && (ui->race_id != -1)) { auto graphics = world->raws.creatures.all[ui->race_id]->graphics; Screen::paintTile( Screen::Pen(' ', fg, 0, graphics.profession_add_color[creature_graphics_role::DEFAULT][profession], graphics.profession_texpos[creature_graphics_role::DEFAULT][profession]), - 1 + name_width + 1 + prof_width + 1 + col, 3); + col_offsets[DISP_COLUMN_LABORS] + col, 3); } } - for (int row = 0; row < height; row++) + for (int row = 0; row < num_rows; row++) { int row_offset = row + first_row; if (row_offset >= units.size()) @@ -643,6 +685,26 @@ void viewscreen_unitlaborsst::render() UnitInfo *cur = units[row_offset]; df::unit *unit = cur->unit; int8_t fg = 15, bg = 0; + + int happy = cur->unit->status.happiness; + string happiness = stl_sprintf("%4i", happy); + if (happy == 0) // miserable + fg = 13; // 5:1 + else if (happy <= 25) // very unhappy + fg = 12; // 4:1 + else if (happy <= 50) // unhappy + fg = 4; // 4:0 + else if (happy < 75) // fine + fg = 14; // 6:1 + else if (happy < 125) // quite content + fg = 6; // 6:0 + else if (happy < 150) // happy + fg = 2; // 2:0 + else // ecstatic + fg = 10; // 2:1 + Screen::paintString(Screen::Pen(' ', fg, bg), col_offsets[DISP_COLUMN_HAPPINESS], 4 + row, happiness); + + fg = 15; if (row_offset == sel_row) { fg = 0; @@ -650,18 +712,18 @@ void viewscreen_unitlaborsst::render() } string name = cur->name; - name.resize(name_width); - Screen::paintString(Screen::Pen(' ', fg, bg), 1, 4 + row, name); + name.resize(col_widths[DISP_COLUMN_NAME]); + Screen::paintString(Screen::Pen(' ', fg, bg), col_offsets[DISP_COLUMN_NAME], 4 + row, name); string profession = cur->profession; - profession.resize(prof_width); + profession.resize(col_widths[DISP_COLUMN_PROFESSION]); fg = cur->color; bg = 0; - Screen::paintString(Screen::Pen(' ', fg, bg), 1 + name_width + 1, 4 + row, profession); + Screen::paintString(Screen::Pen(' ', fg, bg), col_offsets[DISP_COLUMN_PROFESSION], 4 + row, profession); // Print unit's skills and labor assignments - for (int col = 0; col < labors_width; col++) + for (int col = 0; col < col_widths[DISP_COLUMN_LABORS]; col++) { int col_offset = col + first_column; fg = 15; @@ -693,7 +755,7 @@ void viewscreen_unitlaborsst::render() } else bg = 4; - Screen::paintTile(Screen::Pen(c, fg, bg), 1 + name_width + 1 + prof_width + 1 + col, 4 + row); + Screen::paintTile(Screen::Pen(c, fg, bg), col_offsets[DISP_COLUMN_LABORS] + col, 4 + row); } } @@ -703,17 +765,17 @@ void viewscreen_unitlaborsst::render() { df::unit *unit = cur->unit; int x = 1; - Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + height + 2, cur->transname); + Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + num_rows + 2, cur->transname); x += cur->transname.length(); if (cur->transname.length()) { - Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + height + 2, ", "); + Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + num_rows + 2, ", "); x += 2; } - Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + height + 2, cur->profession); + Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + num_rows + 2, cur->profession); x += cur->profession.length(); - Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + height + 2, ": "); + Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + num_rows + 2, ": "); x += 2; string str; @@ -740,7 +802,7 @@ void viewscreen_unitlaborsst::render() else str = stl_sprintf("Not %s (0/500)", ENUM_ATTR_STR(job_skill, caption_noun, columns[sel_column].skill)); } - Screen::paintString(Screen::Pen(' ', 9, 0), x, 3 + height + 2, str); + Screen::paintString(Screen::Pen(' ', 9, 0), x, 3 + num_rows + 2, str); canToggle = (cur->allowEdit) && (columns[sel_column].labor != unit_labor::NONE); } @@ -779,6 +841,9 @@ void viewscreen_unitlaborsst::render() case ALTSORT_PROFESSION: OutputString(15, x, gps->dimy - 2, "Profession"); break; + case ALTSORT_HAPPINESS: + OutputString(15, x, gps->dimy - 2, "Happiness"); + break; default: OutputString(15, x, gps->dimy - 2, "Unknown"); break; @@ -810,7 +875,7 @@ struct unitlist_hook : df::viewscreen_unitlistst { int x = 2; OutputString(12, x, gps->dimy - 2, "l"); // UNITVIEW_PRF_PROF key - OutputString(15, x, gps->dimy - 2, ": Manage labors"); + OutputString(15, x, gps->dimy - 2, ": Manage labors (DFHack)"); } } }; diff --git a/plugins/raw/entity_default.diff b/plugins/raw/entity_default.diff new file mode 100644 index 000000000..a99f8ebba --- /dev/null +++ b/plugins/raw/entity_default.diff @@ -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_EXTRACT_WEAPON] ++ [PERMITTED_REACTION:SPATTER_ADD_EXTRACT_AMMO] + [WORLD_CONSTRUCTION:TUNNEL] + [WORLD_CONSTRUCTION:BRIDGE] + [WORLD_CONSTRUCTION:ROAD] diff --git a/plugins/raw/material_template_default.diff b/plugins/raw/material_template_default.diff new file mode 100644 index 000000000..8b6ef327b --- /dev/null +++ b/plugins/raw/material_template_default.diff @@ -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. diff --git a/plugins/raw/reaction_spatter.txt b/plugins/raw/reaction_spatter.txt new file mode 100644 index 000000000..085be7fdd --- /dev/null +++ b/plugins/raw/reaction_spatter.txt @@ -0,0 +1,144 @@ +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] + [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] + Need some excuse why the spatter is water-resistant: + [REAGENT:grease:1:GLOB:NONE:NONE:NONE][REACTION_CLASS:FAT][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_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] + [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_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] + [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_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] diff --git a/plugins/siege-engine.cpp b/plugins/siege-engine.cpp new file mode 100644 index 000000000..3b95aba35 --- /dev/null +++ b/plugins/siege-engine.cpp @@ -0,0 +1,1865 @@ +#include "Core.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include "df/graphic.h" +#include "df/building_siegeenginest.h" +#include "df/builtin_mats.h" +#include "df/world.h" +#include "df/buildings_other_id.h" +#include "df/job.h" +#include "df/building_drawbuffer.h" +#include "df/ui.h" +#include "df/viewscreen_dwarfmodest.h" +#include "df/ui_build_selector.h" +#include "df/flow_info.h" +#include "df/report.h" +#include "df/proj_itemst.h" +#include "df/unit.h" +#include "df/unit_soul.h" +#include "df/unit_skill.h" +#include "df/physical_attribute_type.h" +#include "df/creature_raw.h" +#include "df/caste_raw.h" +#include "df/caste_raw_flags.h" +#include "df/assumed_identity.h" +#include "df/game_mode.h" +#include "df/unit_misc_trait.h" +#include "df/job.h" +#include "df/job_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" + +using std::vector; +using std::string; +using std::stack; +using namespace DFHack; +using namespace df::enums; + +using df::global::gamemode; +using df::global::gps; +using df::global::world; +using df::global::ui; +using df::global::ui_build_selector; + +using Screen::Pen; + +DFHACK_PLUGIN("siege-engine"); + +/* + * Misc. utils + */ + +typedef std::pair coord_range; + +static void set_range(coord_range *target, df::coord p1, df::coord p2) +{ + if (!p1.isValid() || !p2.isValid()) + { + *target = coord_range(); + } + else + { + target->first.x = std::min(p1.x, p2.x); + target->first.y = std::min(p1.y, p2.y); + target->first.z = std::min(p1.z, p2.z); + target->second.x = std::max(p1.x, p2.x); + target->second.y = std::max(p1.y, p2.y); + target->second.z = std::max(p1.z, p2.z); + } +} + +static bool is_range_valid(const coord_range &target) +{ + return target.first.isValid() && target.second.isValid(); +} + +static bool is_in_range(const coord_range &target, df::coord pos) +{ + return target.first.isValid() && target.second.isValid() && + target.first.x <= pos.x && pos.x <= target.second.x && + target.first.y <= pos.y && pos.y <= target.second.y && + target.first.z <= pos.z && pos.z <= target.second.z; +} + +static std::pair get_engine_range(df::building_siegeenginest *bld) +{ + if (bld->type == siegeengine_type::Ballista) + return std::make_pair(1, 200); + else + return std::make_pair(30, 100); +} + +static void orient_engine(df::building_siegeenginest *bld, df::coord target) +{ + int dx = target.x - bld->centerx; + int dy = target.y - bld->centery; + + if (abs(dx) > abs(dy)) + bld->facing = (dx > 0) ? + df::building_siegeenginest::Right : + df::building_siegeenginest::Left; + else + bld->facing = (dy > 0) ? + df::building_siegeenginest::Down : + df::building_siegeenginest::Up; +} + +static int random_int(int val) +{ + return int(int64_t(rand())*val/RAND_MAX); +} + +static int point_distance(df::coord speed) +{ + return std::max(abs(speed.x), std::max(abs(speed.y), abs(speed.z))); +} + +inline void normalize(float &x, float &y, float &z) +{ + float dist = sqrtf(x*x + y*y + z*z); + if (dist == 0.0f) return; + x /= dist; y /= dist; z /= dist; +} + +static void random_direction(float &x, float &y, float &z) +{ + float a, b, d; + for (;;) { + a = (rand() + 0.5f)*2.0f/RAND_MAX - 1.0f; + b = (rand() + 0.5f)*2.0f/RAND_MAX - 1.0f; + d = a*a + b*b; + if (d < 1.0f) + break; + } + + float sq = sqrtf(1-d); + x = 2.0f*a*sq; + y = 2.0f*b*sq; + 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(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 + */ + +static bool enable_plugin(); + +struct EngineInfo { + int id; + df::building_siegeenginest *bld; + + df::coord center; + coord_range building_rect; + + bool is_catapult; + int proj_speed, hit_delay; + std::pair fire_range; + + coord_range target; + + df::job_item_vector_id ammo_vector_id; + df::item_type ammo_item_type; + + int operator_id, operator_frame; + + std::set stockpiles; + df::stockpile_links links; + df::workshop_profile profile; + + bool hasTarget() { return is_range_valid(target); } + bool onTarget(df::coord pos) { return is_in_range(target, pos); } + df::coord getTargetSize() { return target.second - target.first; } + + bool isInRange(int dist) { + return dist >= fire_range.first && dist <= fire_range.second; + } +}; + +static std::map engines; +static std::map coord_engines; + +static EngineInfo *find_engine(df::building *bld, bool create = false) +{ + auto ebld = strict_virtual_cast(bld); + if (!ebld) + return NULL; + + auto &obj = engines[bld]; + + if (obj) + { + obj->bld = ebld; + return obj; + } + + if (!create) + return NULL; + + obj = new EngineInfo(); + + obj->id = bld->id; + obj->bld = ebld; + obj->center = df::coord(bld->centerx, bld->centery, bld->z); + obj->building_rect = coord_range( + df::coord(bld->x1, bld->y1, bld->z), + df::coord(bld->x2, bld->y2, bld->z) + ); + obj->is_catapult = (ebld->type == siegeengine_type::Catapult); + obj->proj_speed = 2; + obj->hit_delay = obj->is_catapult ? 2 : -1; + obj->fire_range = get_engine_range(ebld); + + obj->ammo_vector_id = job_item_vector_id::BOULDER; + obj->ammo_item_type = item_type::BOULDER; + + obj->operator_id = obj->operator_frame = -1; + + coord_engines[obj->center] = bld; + return obj; +} + +static EngineInfo *find_engine(lua_State *L, int idx, bool create = false, bool silent = false) +{ + auto bld = Lua::CheckDFObject(L, idx); + + auto engine = find_engine(bld, create); + if (!engine && !silent) + luaL_error(L, "no such engine"); + + return engine; +} + +static EngineInfo *find_engine(df::coord pos) +{ + auto engine = find_engine(coord_engines[pos]); + + if (engine) + { + auto bld0 = df::building::find(engine->id); + auto bld = strict_virtual_cast(bld0); + if (!bld) + return NULL; + + engine->bld = bld; + } + + return engine; +} + +/* + * Configuration management + */ + +static void clear_engines() +{ + for (auto it = engines.begin(); it != engines.end(); ++it) + delete it->second; + engines.clear(); + coord_engines.clear(); +} + +static void load_engines() +{ + clear_engines(); + + auto pworld = Core::getInstance().getWorld(); + std::vector vec; + + pworld->GetPersistentData(&vec, "siege-engine/target/", true); + for (auto it = vec.begin(); it != vec.end(); ++it) + { + auto engine = find_engine(df::building::find(it->ival(0)), true); + if (!engine) continue; + engine->target.first = df::coord(it->ival(1), it->ival(2), it->ival(3)); + engine->target.second = df::coord(it->ival(4), it->ival(5), it->ival(6)); + } + + pworld->GetPersistentData(&vec, "siege-engine/ammo/", true); + for (auto it = vec.begin(); it != vec.end(); ++it) + { + auto engine = find_engine(df::building::find(it->ival(0)), true); + if (!engine) continue; + engine->ammo_vector_id = (df::job_item_vector_id)it->ival(1); + engine->ammo_item_type = (df::item_type)it->ival(2); + } + + pworld->GetPersistentData(&vec, "siege-engine/stockpiles/", true); + for (auto it = vec.begin(); it != vec.end(); ++it) + { + auto engine = find_engine(df::building::find(it->ival(0)), true); + if (!engine) + continue; + auto pile = df::building::find(it->ival(1)); + if (!pile || pile->getType() != building_type::Stockpile) + { + pworld->DeletePersistentData(*it); + continue;; + } + + engine->stockpiles.insert(it->ival(1)); + } + + pworld->GetPersistentData(&vec, "siege-engine/profiles/", true); + for (auto it = vec.begin(); it != vec.end(); ++it) + { + auto engine = find_engine(df::building::find(it->ival(0)), true); + if (!engine) continue; + engine->profile.min_level = it->ival(1); + engine->profile.max_level = it->ival(2); + } + + pworld->GetPersistentData(&vec, "siege-engine/profile-workers/", true); + for (auto it = vec.begin(); it != vec.end(); ++it) + { + auto engine = find_engine(df::building::find(it->ival(0)), true); + if (!engine) + continue; + auto unit = df::unit::find(it->ival(1)); + if (!unit || !Units::isCitizen(unit)) + { + pworld->DeletePersistentData(*it); + continue; + } + engine->profile.permitted_workers.push_back(it->ival(1)); + } +} + +static int getTargetArea(lua_State *L) +{ + auto engine = find_engine(L, 1, false, true); + + if (engine && engine->hasTarget()) + { + Lua::Push(L, engine->target.first); + Lua::Push(L, engine->target.second); + } + else + { + lua_pushnil(L); + lua_pushnil(L); + } + + return 2; +} + +static void clearTargetArea(df::building_siegeenginest *bld) +{ + CHECK_NULL_POINTER(bld); + + if (auto engine = find_engine(bld)) + engine->target = coord_range(); + + auto pworld = Core::getInstance().getWorld(); + auto key = stl_sprintf("siege-engine/target/%d", bld->id); + pworld->DeletePersistentData(pworld->GetPersistentData(key)); +} + +static bool setTargetArea(df::building_siegeenginest *bld, df::coord target_min, df::coord target_max) +{ + CHECK_NULL_POINTER(bld); + CHECK_INVALID_ARGUMENT(target_min.isValid() && target_max.isValid()); + + if (!enable_plugin()) + return false; + + auto pworld = Core::getInstance().getWorld(); + auto key = stl_sprintf("siege-engine/target/%d", bld->id); + auto entry = pworld->GetPersistentData(key, NULL); + if (!entry.isValid()) + return false; + + auto engine = find_engine(bld, true); + + set_range(&engine->target, target_min, target_max); + + entry.ival(0) = bld->id; + entry.ival(1) = engine->target.first.x; + entry.ival(2) = engine->target.first.y; + entry.ival(3) = engine->target.first.z; + entry.ival(4) = engine->target.second.x; + entry.ival(5) = engine->target.second.y; + entry.ival(6) = engine->target.second.z; + + df::coord sum = target_min + target_max; + orient_engine(bld, df::coord(sum.x/2, sum.y/2, sum.z/2)); + + return true; +} + +static int getAmmoItem(lua_State *L) +{ + auto engine = find_engine(L, 1, false, true); + if (!engine) + Lua::Push(L, item_type::BOULDER); + else + Lua::Push(L, engine->ammo_item_type); + return 1; +} + +static int setAmmoItem(lua_State *L) +{ + if (!enable_plugin()) + return 0; + + auto engine = find_engine(L, 1, true); + auto item_type = (df::item_type)luaL_optint(L, 2, item_type::BOULDER); + if (!is_valid_enum_item(item_type)) + luaL_argerror(L, 2, "invalid item type"); + + auto pworld = Core::getInstance().getWorld(); + auto key = stl_sprintf("siege-engine/ammo/%d", engine->id); + auto entry = pworld->GetPersistentData(key, NULL); + if (!entry.isValid()) + return 0; + + engine->ammo_vector_id = job_item_vector_id::ANY_FREE; + engine->ammo_item_type = item_type; + + FOR_ENUM_ITEMS(job_item_vector_id, id) + { + auto other = ENUM_ATTR(job_item_vector_id, other, id); + auto type = ENUM_ATTR(items_other_id, item, other); + if (type == item_type) + { + engine->ammo_vector_id = id; + break; + } + } + + entry.ival(0) = engine->id; + entry.ival(1) = engine->ammo_vector_id; + entry.ival(2) = engine->ammo_item_type; + + lua_pushboolean(L, true); + return 1; +} + +static void forgetStockpileLink(EngineInfo *engine, int pile_id) +{ + engine->stockpiles.erase(pile_id); + + auto pworld = Core::getInstance().getWorld(); + auto key = stl_sprintf("siege-engine/stockpiles/%d/%d", engine->id, pile_id); + pworld->DeletePersistentData(pworld->GetPersistentData(key)); +} + +static void update_stockpile_links(EngineInfo *engine) +{ + engine->links.take_from_pile.clear(); + + for (auto it = engine->stockpiles.begin(); it != engine->stockpiles.end(); ) + { + int id = *it; ++it; + auto pile = df::building::find(id); + + if (!pile || pile->getType() != building_type::Stockpile) + forgetStockpileLink(engine, id); + else + // The vector is sorted, but we are iterating through a sorted set + engine->links.take_from_pile.push_back(pile); + } +} + +static int getStockpileLinks(lua_State *L) +{ + auto engine = find_engine(L, 1, false, true); + if (!engine || engine->stockpiles.empty()) + return 0; + + update_stockpile_links(engine); + + auto &links = engine->links.take_from_pile; + lua_createtable(L, links.size(), 0); + + for (size_t i = 0; i < links.size(); i++) + { + Lua::Push(L, links[i]); + lua_rawseti(L, -2, i+1); + } + + return 1; +} + +static bool isLinkedToPile(df::building_siegeenginest *bld, df::building_stockpilest *pile) +{ + CHECK_NULL_POINTER(bld); + CHECK_NULL_POINTER(pile); + + auto engine = find_engine(bld); + + return engine && engine->stockpiles.count(pile->id); +} + +static bool addStockpileLink(df::building_siegeenginest *bld, df::building_stockpilest *pile) +{ + CHECK_NULL_POINTER(bld); + CHECK_NULL_POINTER(pile); + + if (!enable_plugin()) + return false; + + auto pworld = Core::getInstance().getWorld(); + auto key = stl_sprintf("siege-engine/stockpiles/%d/%d", bld->id, pile->id); + auto entry = pworld->GetPersistentData(key, NULL); + if (!entry.isValid()) + return false; + + auto engine = find_engine(bld, true); + + entry.ival(0) = bld->id; + entry.ival(1) = pile->id; + + engine->stockpiles.insert(pile->id); + return true; +} + +static bool removeStockpileLink(df::building_siegeenginest *bld, df::building_stockpilest *pile) +{ + CHECK_NULL_POINTER(bld); + CHECK_NULL_POINTER(pile); + + if (auto engine = find_engine(bld)) + { + forgetStockpileLink(engine, pile->id); + return true; + } + + return false; +} + +static df::workshop_profile *saveWorkshopProfile(df::building_siegeenginest *bld) +{ + CHECK_NULL_POINTER(bld); + + if (!enable_plugin()) + return NULL; + + // Save skill limits + auto pworld = Core::getInstance().getWorld(); + auto key = stl_sprintf("siege-engine/profiles/%d", bld->id); + auto entry = pworld->GetPersistentData(key, NULL); + if (!entry.isValid()) + return NULL; + + auto engine = find_engine(bld, true); + + entry.ival(0) = engine->id; + entry.ival(1) = engine->profile.min_level; + entry.ival(2) = engine->profile.max_level; + + // Save worker list + std::vector vec; + auto &workers = engine->profile.permitted_workers; + + key = stl_sprintf("siege-engine/profile-workers/%d", bld->id); + pworld->GetPersistentData(&vec, key, true); + + for (auto it = vec.begin(); it != vec.end(); ++it) + { + if (linear_index(workers, it->ival(1)) < 0) + pworld->DeletePersistentData(*it); + } + + for (size_t i = 0; i < workers.size(); i++) + { + key = stl_sprintf("siege-engine/profile-workers/%d/%d", bld->id, workers[i]); + entry = pworld->GetPersistentData(key, NULL); + if (!entry.isValid()) + continue; + entry.ival(0) = engine->id; + entry.ival(1) = workers[i]; + } + + return &engine->profile; +} + +static int getOperatorSkill(df::building_siegeenginest *bld, bool force = false) +{ + CHECK_NULL_POINTER(bld); + + auto engine = find_engine(bld); + if (!engine) + return 0; + + if (engine->operator_id != -1 && + (world->frame_counter - engine->operator_frame) <= 5) + { + auto op_unit = df::unit::find(engine->operator_id); + if (op_unit) + return Units::getEffectiveSkill(op_unit, job_skill::SIEGEOPERATE); + } + + if (force) + { + color_ostream_proxy out(Core::getInstance().getConsole()); + out.print("Forced siege operator search\n"); + + auto &active = world->units.active; + for (size_t i = 0; i < active.size(); i++) + if (active[i]->pos == engine->center && Units::isCitizen(active[i])) + return Units::getEffectiveSkill(active[i], job_skill::SIEGEOPERATE); + } + + return 0; +} + +/* + * Trajectory raytracing + */ + +struct ProjectilePath { + static const int DEFAULT_FUDGE = 31; + + df::coord origin, goal, target, fudge_delta; + int divisor, fudge_factor; + df::coord speed, direction; + + ProjectilePath(df::coord origin, df::coord goal) : + origin(origin), goal(goal), fudge_factor(1) + { + fudge_delta = df::coord(0,0,0); + calc_line(); + } + + ProjectilePath(df::coord origin, df::coord goal, df::coord delta, int factor) : + origin(origin), goal(goal), fudge_delta(delta), fudge_factor(factor) + { + calc_line(); + } + + ProjectilePath(df::coord origin, df::coord goal, float zdelta, int factor = DEFAULT_FUDGE) : + origin(origin), goal(goal), fudge_factor(factor) + { + fudge_delta = df::coord(0,0,int(factor * zdelta)); + calc_line(); + } + + void calc_line() + { + speed = goal - origin; + speed.x *= fudge_factor; + speed.y *= fudge_factor; + speed.z *= fudge_factor; + speed = speed + fudge_delta; + target = origin + speed; + divisor = point_distance(speed); + if (divisor <= 0) divisor = 1; + direction = df::coord(speed.x>=0?1:-1,speed.y>=0?1:-1,speed.z>=0?1:-1); + } + + df::coord operator[] (int i) const + { + int div2 = divisor * 2; + int bias = divisor-1; + return origin + df::coord( + (2*speed.x*i + direction.x*bias)/div2, + (2*speed.y*i + direction.y*bias)/div2, + (2*speed.z*i + direction.z*bias)/div2 + ); + } +}; + +static ProjectilePath decode_path(lua_State *L, int idx, df::coord origin) +{ + idx = lua_absindex(L, idx); + + Lua::StackUnwinder frame(L); + df::coord goal; + + lua_getfield(L, idx, "target"); + Lua::CheckDFAssign(L, &goal, frame[1]); + + lua_getfield(L, idx, "delta"); + + if (!lua_isnil(L, frame[2])) + { + lua_getfield(L, idx, "factor"); + int factor = luaL_optnumber(L, frame[3], ProjectilePath::DEFAULT_FUDGE); + + if (lua_isnumber(L, frame[2])) + return ProjectilePath(origin, goal, lua_tonumber(L, frame[2]), factor); + + df::coord delta; + Lua::CheckDFAssign(L, &delta, frame[2]); + + return ProjectilePath(origin, goal, delta, factor); + } + + return ProjectilePath(origin, goal); +} + +static int projPosAtStep(lua_State *L) +{ + auto engine = find_engine(L, 1); + auto path = decode_path(L, 2, engine->center); + int step = luaL_checkint(L, 3); + Lua::Push(L, path[step]); + return 1; +} + +static bool isPassableTile(df::coord pos) +{ + auto ptile = Maps::getTileType(pos); + + return !ptile || FlowPassable(*ptile); +} + +static bool isTargetableTile(df::coord pos) +{ + auto ptile = Maps::getTileType(pos); + + return ptile && FlowPassable(*ptile) && !isOpenTerrain(*ptile); +} + +static bool isTreeTile(df::coord pos) +{ + auto ptile = Maps::getTileType(pos); + + return ptile && tileShape(*ptile) == tiletype_shape::TREE; +} + +static bool adjustToTarget(EngineInfo *engine, df::coord *pos) +{ + if (isTargetableTile(*pos)) + return true; + + for (df::coord fudge = *pos; + fudge.z <= engine->target.second.z; fudge.z++) + { + if (!isTargetableTile(fudge)) + continue; + *pos = fudge; + return true; + } + + for (df::coord fudge = *pos; + fudge.z >= engine->target.first.z; fudge.z--) + { + if (!isTargetableTile(fudge)) + continue; + *pos = fudge; + return true; + } + + return false; +} + +static int adjustToTarget(lua_State *L) +{ + auto engine = find_engine(L, 1, true); + df::coord pos; + Lua::CheckDFAssign(L, &pos, 2); + bool ok = adjustToTarget(engine, &pos); + Lua::Push(L, pos); + Lua::Push(L, ok); + return 2; +} + +static const char* const hit_type_names[] = { + "wall", "floor", "ceiling", "map_edge", "tree" +}; + +struct PathMetrics { + enum CollisionType { + Impassable, + Floor, + Ceiling, + MapEdge, + Tree + } hit_type; + + int collision_step, collision_z_step; + int goal_step, goal_z_step, goal_distance; + + bool hits() const { return collision_step > goal_step; } + + PathMetrics(const ProjectilePath &path) + { + compute(path); + } + + void compute(const ProjectilePath &path) + { + collision_step = goal_step = goal_z_step = 1000000; + collision_z_step = 0; + + goal_distance = point_distance(path.origin - path.goal); + + int step = 0; + df::coord prev_pos = path.origin; + + for (;;) { + df::coord cur_pos = path[++step]; + if (cur_pos == prev_pos) + break; + + if (cur_pos.z == path.goal.z) + { + goal_z_step = std::min(step, goal_z_step); + if (cur_pos == path.goal) + goal_step = step; + } + + if (!Maps::isValidTilePos(cur_pos)) + { + hit_type = PathMetrics::MapEdge; + break; + } + + if (!isPassableTile(cur_pos)) + { + if (isTreeTile(cur_pos)) + { + // The projectile code has a bug where it will + // hit a tree on the same tick as a Z level change. + if (cur_pos.z != prev_pos.z) + { + hit_type = Tree; + break; + } + } + else + { + hit_type = Impassable; + break; + } + } + + if (cur_pos.z != prev_pos.z) + { + int top_z = std::max(prev_pos.z, cur_pos.z); + auto ptile = Maps::getTileType(cur_pos.x, cur_pos.y, top_z); + + if (ptile && !LowPassable(*ptile)) + { + hit_type = (cur_pos.z > prev_pos.z ? Ceiling : Floor); + break; + } + + collision_z_step = step; + } + + prev_pos = cur_pos; + } + + collision_step = step; + } +}; + +enum TargetTileStatus { + TARGET_OK, TARGET_RANGE, TARGET_BLOCKED, TARGET_SEMIBLOCKED +}; +static const char* const target_tile_type_names[] = { + "ok", "out_of_range", "blocked", "semi_blocked" +}; + +static TargetTileStatus calcTileStatus(EngineInfo *engine, const PathMetrics &raytrace) +{ + if (raytrace.hits()) + { + if (engine->isInRange(raytrace.goal_step)) + return TARGET_OK; + else + return TARGET_RANGE; + } + else + return TARGET_BLOCKED; +} + +static int projPathMetrics(lua_State *L) +{ + auto engine = find_engine(L, 1); + auto path = decode_path(L, 2, engine->center); + + PathMetrics info(path); + + lua_createtable(L, 0, 7); + Lua::SetField(L, hit_type_names[info.hit_type], -1, "hit_type"); + Lua::SetField(L, info.collision_step, -1, "collision_step"); + Lua::SetField(L, info.collision_z_step, -1, "collision_z_step"); + Lua::SetField(L, info.goal_distance, -1, "goal_distance"); + if (info.goal_step < info.collision_step) + Lua::SetField(L, info.goal_step, -1, "goal_step"); + if (info.goal_z_step < info.collision_step) + Lua::SetField(L, info.goal_z_step, -1, "goal_z_step"); + Lua::SetField(L, target_tile_type_names[calcTileStatus(engine, info)], -1, "status"); + return 1; +} + +static TargetTileStatus calcTileStatus(EngineInfo *engine, df::coord target, float zdelta) +{ + ProjectilePath path(engine->center, target, zdelta); + PathMetrics raytrace(path); + return calcTileStatus(engine, raytrace); +} + +static TargetTileStatus calcTileStatus(EngineInfo *engine, df::coord target) +{ + auto status = calcTileStatus(engine, target, 0.0f); + + if (status == TARGET_BLOCKED) + { + if (calcTileStatus(engine, target, 0.5f) < TARGET_BLOCKED) + return TARGET_SEMIBLOCKED; + + if (calcTileStatus(engine, target, -0.5f) < TARGET_BLOCKED) + return TARGET_SEMIBLOCKED; + } + + return status; +} + +static std::string getTileStatus(df::building_siegeenginest *bld, df::coord tile_pos) +{ + auto engine = find_engine(bld, true); + if (!engine) + return "invalid"; + + return target_tile_type_names[calcTileStatus(engine, tile_pos)]; +} + +static void paintAimScreen(df::building_siegeenginest *bld, df::coord view, df::coord2d ltop, df::coord2d size) +{ + auto engine = find_engine(bld, true); + CHECK_NULL_POINTER(engine); + + for (int x = 0; x < size.x; x++) + { + for (int y = 0; y < size.y; y++) + { + df::coord tile_pos = view + df::coord(x,y,0); + if (is_in_range(engine->building_rect, tile_pos)) + continue; + + Pen cur_tile = Screen::readTile(ltop.x+x, ltop.y+y); + if (!cur_tile.valid()) + continue; + + int color; + + switch (calcTileStatus(engine, tile_pos)) + { + case TARGET_OK: + color = COLOR_GREEN; + break; + case TARGET_RANGE: + color = COLOR_CYAN; + break; + case TARGET_BLOCKED: + color = COLOR_RED; + break; + case TARGET_SEMIBLOCKED: + color = COLOR_BROWN; + break; + } + + if (cur_tile.fg && cur_tile.ch != ' ') + { + cur_tile.fg = color; + cur_tile.bg = 0; + } + else + { + cur_tile.fg = 0; + cur_tile.bg = color; + } + + cur_tile.bold = engine->onTarget(tile_pos); + + if (cur_tile.tile) + cur_tile.tile_mode = Pen::CharColor; + + Screen::paintTile(cur_tile, ltop.x+x, ltop.y+y); + } + } +} + +/* + * Unit tracking + */ + +static const float MAX_TIME = 1000000.0f; + +struct UnitPath { + df::unit *unit; + std::map path; + + struct Hit { + UnitPath *path; + df::coord pos; + int dist; + float time, lmargin, rmargin; + }; + + static std::map cache; + + static UnitPath *get(df::unit *unit) + { + auto &cv = cache[unit]; + if (!cv) cv = new UnitPath(unit); + return cv; + }; + + UnitPath(df::unit *unit) : unit(unit) + { + if (unit->flags1.bits.rider) + { + auto mount = df::unit::find(unit->relations.rider_mount_id); + + if (mount) + { + path = get(mount)->path; + return; + } + } + + df::coord pos = unit->pos; + df::coord dest = unit->path.dest; + auto &upath = unit->path.path; + + if (dest.isValid() && !upath.x.empty()) + { + 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]; + if (new_pos == pos) + continue; + + float delay = speed; + if (new_pos.x != pos.x && new_pos.y != pos.y) + delay *= 362.0/256.0; + + path[time] = pos; + pos = new_pos; + time += delay + 1; + } + } + + path[MAX_TIME] = pos; + } + + void get_margin(std::map::iterator &it, float time, float *lmargin, float *rmargin) + { + auto it2 = it; + *lmargin = (it == path.begin()) ? MAX_TIME : time - (--it2)->first; + *rmargin = (it->first == MAX_TIME) ? MAX_TIME : it->first - time; + } + + df::coord posAtTime(float time, float *lmargin = NULL, float *rmargin = NULL) + { + CHECK_INVALID_ARGUMENT(time < MAX_TIME); + + auto it = path.upper_bound(time); + if (lmargin) + get_margin(it, time, lmargin, rmargin); + return it->second; + } + + bool findHits(EngineInfo *engine, std::vector *hit_points, float bias) + { + df::coord origin = engine->center; + + Hit info; + info.path = this; + + for (auto it = path.begin(); it != path.end(); ++it) + { + info.pos = it->second; + info.dist = point_distance(origin - info.pos); + info.time = float(info.dist)*(engine->proj_speed+1) + engine->hit_delay + bias; + get_margin(it, info.time, &info.lmargin, &info.rmargin); + + if (info.lmargin > 0 && info.rmargin > 0) + { + if (engine->onTarget(info.pos) && engine->isInRange(info.dist)) + hit_points->push_back(info); + } + } + + return !hit_points->empty(); + } +}; + +std::map UnitPath::cache; + +static void push_margin(lua_State *L, float margin) +{ + if (margin == MAX_TIME) + lua_pushnil(L); + else + lua_pushnumber(L, margin); +} + +static int traceUnitPath(lua_State *L) +{ + auto unit = Lua::CheckDFObject(L, 1); + + CHECK_NULL_POINTER(unit); + + size_t idx = 1; + auto info = UnitPath::get(unit); + lua_createtable(L, info->path.size(), 0); + + float last_time = 0.0f; + for (auto it = info->path.begin(); it != info->path.end(); ++it) + { + Lua::Push(L, it->second); + if (idx > 1) + { + lua_pushnumber(L, last_time); + lua_setfield(L, -2, "from"); + } + if (idx < info->path.size()) + { + lua_pushnumber(L, it->first); + lua_setfield(L, -2, "to"); + } + lua_rawseti(L, -2, idx++); + last_time = it->first; + } + + return 1; +} + +static int unitPosAtTime(lua_State *L) +{ + auto unit = Lua::CheckDFObject(L, 1); + float time = luaL_checknumber(L, 2); + + CHECK_NULL_POINTER(unit); + + float lmargin, rmargin; + auto info = UnitPath::get(unit); + + Lua::Push(L, info->posAtTime(time, &lmargin, &rmargin)); + push_margin(L, lmargin); + push_margin(L, rmargin); + return 3; +} + +static bool canTargetUnit(df::unit *unit) +{ + CHECK_NULL_POINTER(unit); + + if (unit->flags1.bits.dead || + unit->flags3.bits.ghostly || + unit->flags1.bits.caged || + unit->flags1.bits.hidden_in_ambush) + return false; + + return true; +} + +static void proposeUnitHits(EngineInfo *engine, std::vector *hits, float bias) +{ + auto &active = world->units.active; + + for (size_t i = 0; i < active.size(); i++) + { + auto unit = active[i]; + + if (!canTargetUnit(unit)) + continue; + + UnitPath::get(unit)->findHits(engine, hits, bias); + } +} + +static int proposeUnitHits(lua_State *L) +{ + auto engine = find_engine(L, 1); + float bias = luaL_optnumber(L, 2, 0); + + if (!engine->hasTarget()) + luaL_error(L, "target not set"); + + std::vector hits; + proposeUnitHits(engine, &hits, bias); + + lua_createtable(L, hits.size(), 0); + + for (size_t i = 0; i < hits.size(); i++) + { + auto &hit = hits[i]; + lua_createtable(L, 0, 6); + Lua::SetField(L, hit.path->unit, -1, "unit"); + Lua::SetField(L, hit.pos, -1, "pos"); + Lua::SetField(L, hit.dist, -1, "dist"); + Lua::SetField(L, hit.time, -1, "time"); + push_margin(L, hit.lmargin); lua_setfield(L, -2, "lmargin"); + push_margin(L, hit.rmargin); lua_setfield(L, -2, "rmargin"); + lua_rawseti(L, -2, i+1); + } + + 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 units; + std::vector 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(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(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; + + void aimAtPoint(EngineInfo *engine, const ProjectilePath &path) + { + 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 + for (int i = 0; i < raytrace.collision_step; i++) + Maps::ensureTileBlock(path[i]); + + // Find valid hit point for catapult stones + if (flags.bits.high_flying) + { + if (raytrace.hits()) + fall_threshold = raytrace.goal_step; + else + fall_threshold = (raytrace.collision_z_step+raytrace.collision_step-1)/2; + + while (fall_threshold < raytrace.collision_step-1) + { + if (isTargetableTile(path[fall_threshold])) + break; + + fall_threshold++; + } + } + + fall_threshold = std::max(fall_threshold, engine->fire_range.first); + fall_threshold = std::min(fall_threshold, engine->fire_range.second); + } + + 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; + df::coord tsize = engine->getTargetSize(); + bool success = false; + + for (int i = 0; i < 50; i++) + { + target = tbase + df::coord( + random_int(tsize.x), random_int(tsize.y), random_int(tsize.z) + ); + + if (adjustToTarget(engine, &target)) + last_passable = target; + else + continue; + + ProjectilePath path(engine->center, target, engine->is_catapult ? 0.5f : 0.0f); + PathMetrics raytrace(path); + + if (raytrace.hits() && engine->isInRange(raytrace.goal_step)) + { + aimAtPoint(engine, skill, path); + return; + } + } + + if (!last_passable.isValid()) + last_passable = target; + + aimAtPoint(engine, skill, ProjectilePath(engine->center, last_passable)); + } + + static int safeAimProjectile(lua_State *L) + { + color_ostream &out = *Lua::GetOutput(L); + auto proj = (projectile_hook*)lua_touserdata(L, 1); + auto engine = (EngineInfo*)lua_touserdata(L, 2); + int skill = lua_tointeger(L, 3); + + if (!Lua::PushModulePublic(out, L, "plugins.siege-engine", "doAimProjectile")) + luaL_error(L, "Projectile aiming AI not available"); + + Lua::PushDFObject(L, engine->bld); + Lua::Push(L, proj->item); + Lua::Push(L, engine->target.first); + Lua::Push(L, engine->target.second); + Lua::Push(L, skill); + + lua_call(L, 5, 1); + + if (lua_isnil(L, -1)) + proj->aimAtArea(engine, skill); + else + proj->aimAtPoint(engine, skill, decode_path(L, -1, engine->center)); + + return 0; + } + + void doCheckMovement() + { + if (flags.bits.parabolic || distance_flown != 0 || + fall_counter != fall_delay || item == NULL) + return; + + auto engine = find_engine(origin_pos); + if (!engine || !engine->hasTarget()) + return; + + auto L = Lua::Core::State; + CoreSuspendClaimer suspend; + color_ostream_proxy out(Core::getInstance().getConsole()); + + int skill = getOperatorSkill(engine->bld, true); + + // 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, skill); + } + + switch (item->getType()) + { + case item_type::CAGE: + flags.bits.bouncing = false; + break; + case item_type::BIN: + case item_type::BARREL: + flags.bits.bouncing = false; + break; + default: + break; + } + } + + void doLaunchContents() + { + // Translate cartoon flight speed to parabolic + 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((float)dist.z); + normalize(vx, vy, vz); + + int start_z = 0; + + // Start at tile top, if hit a wall + ProjectilePath path(origin_pos, target_pos); + auto next_pos = path[distance_flown+1]; + if (next_pos.z == cur_pos.z && !isPassableTile(next_pos)) + start_z = 49000; + + MapExtras::MapCache mc; + std::vector contents; + Items::getContainedItems(item, &contents); + + 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; + + 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; + proj->flags.bits.unk9 = true; + proj->flags.bits.no_collide = true; + + proj->pos_z = start_z; + + float sx, sy, sz; + random_direction(sx, sy, sz); + sx += vx*0.7; sy += vy*0.7; sz += vz*0.7; + if (sz < 0) sz = -sz; + normalize(sx, sy, sz); + + proj->speed_x = int(speed * sx); + proj->speed_y = int(speed * sy); + proj->speed_z = std::max(min_zspeed, int(speed * sz)); + } + } + + DEFINE_VMETHOD_INTERPOSE(bool, checkMovement, ()) + { + if (flags.bits.high_flying || flags.bits.piercing) + doCheckMovement(); + + return INTERPOSE_NEXT(checkMovement)(); + } + + DEFINE_VMETHOD_INTERPOSE(bool, checkImpact, (bool no_damage_floor)) + { + if (!flags.bits.has_hit_ground && !flags.bits.parabolic && + flags.bits.high_flying && !flags.bits.bouncing && + !flags.bits.no_impact_destroy && target_pos != origin_pos && + item && item->flags.bits.container) + { + doLaunchContents(); + } + + return INTERPOSE_NEXT(checkImpact)(no_damage_floor); + } +}; + +IMPLEMENT_VMETHOD_INTERPOSE(projectile_hook, checkMovement); +IMPLEMENT_VMETHOD_INTERPOSE(projectile_hook, checkImpact); + +/* + * Building hook + */ + +struct building_hook : df::building_siegeenginest { + typedef df::building_siegeenginest interpose_base; + + DEFINE_VMETHOD_INTERPOSE(df::workshop_profile*, getWorkshopProfile, ()) + { + if (auto engine = find_engine(this)) + return &engine->profile; + + return INTERPOSE_NEXT(getWorkshopProfile)(); + } + + DEFINE_VMETHOD_INTERPOSE(df::stockpile_links*, getStockpileLinks, ()) + { + if (auto engine = find_engine(this)) + { + update_stockpile_links(engine); + return &engine->links; + } + + return INTERPOSE_NEXT(getStockpileLinks)(); + } + + DEFINE_VMETHOD_INTERPOSE(void, updateAction, ()) + { + INTERPOSE_NEXT(updateAction)(); + + if (jobs.empty()) + return; + + if (auto engine = find_engine(this)) + { + auto job = jobs[0]; + bool save_op = false; + + switch (job->job_type) + { + case job_type::LoadCatapult: + if (!job->job_items.empty()) + { + auto item = job->job_items[0]; + item->item_type = engine->ammo_item_type; + item->vector_id = engine->ammo_vector_id; + + switch (item->item_type) + { + case item_type::NONE: + case item_type::BOULDER: + case item_type::BLOCKS: + item->mat_type = 0; + break; + + case item_type::BIN: + case item_type::BARREL: + item->mat_type = -1; + // A hack to make it take objects assigned to stockpiles. + // Since reaction_id is not set, the actual value is not used. + item->contains.resize(1); + break; + + default: + item->mat_type = -1; + break; + } + } + // fallthrough + + case job_type::LoadBallista: + case job_type::FireCatapult: + case job_type::FireBallista: + if (auto worker = Job::getWorker(job)) + { + engine->operator_id = worker->id; + engine->operator_frame = world->frame_counter; + } + break; + + default: + break; + } + } + } +}; + +IMPLEMENT_VMETHOD_INTERPOSE(building_hook, getWorkshopProfile); +IMPLEMENT_VMETHOD_INTERPOSE(building_hook, getStockpileLinks); +IMPLEMENT_VMETHOD_INTERPOSE(building_hook, updateAction); + +/* + * Initialization + */ + +DFHACK_PLUGIN_LUA_FUNCTIONS { + DFHACK_LUA_FUNCTION(clearTargetArea), + DFHACK_LUA_FUNCTION(setTargetArea), + DFHACK_LUA_FUNCTION(isLinkedToPile), + DFHACK_LUA_FUNCTION(addStockpileLink), + DFHACK_LUA_FUNCTION(removeStockpileLink), + DFHACK_LUA_FUNCTION(saveWorkshopProfile), + DFHACK_LUA_FUNCTION(getTileStatus), + DFHACK_LUA_FUNCTION(paintAimScreen), + DFHACK_LUA_FUNCTION(canTargetUnit), + DFHACK_LUA_FUNCTION(isPassableTile), + DFHACK_LUA_FUNCTION(isTreeTile), + DFHACK_LUA_FUNCTION(isTargetableTile), + DFHACK_LUA_END +}; + +DFHACK_PLUGIN_LUA_COMMANDS { + DFHACK_LUA_COMMAND(getTargetArea), + DFHACK_LUA_COMMAND(getAmmoItem), + DFHACK_LUA_COMMAND(setAmmoItem), + DFHACK_LUA_COMMAND(getStockpileLinks), + DFHACK_LUA_COMMAND(projPosAtStep), + DFHACK_LUA_COMMAND(projPathMetrics), + DFHACK_LUA_COMMAND(adjustToTarget), + DFHACK_LUA_COMMAND(traceUnitPath), + DFHACK_LUA_COMMAND(unitPosAtTime), + DFHACK_LUA_COMMAND(proposeUnitHits), + DFHACK_LUA_COMMAND(computeNearbyWeight), + DFHACK_LUA_END +}; + +static bool is_enabled = false; + +static void enable_hooks(bool enable) +{ + is_enabled = enable; + + INTERPOSE_HOOK(projectile_hook, checkMovement).apply(enable); + INTERPOSE_HOOK(projectile_hook, checkImpact).apply(enable); + + INTERPOSE_HOOK(building_hook, getWorkshopProfile).apply(enable); + INTERPOSE_HOOK(building_hook, getStockpileLinks).apply(enable); + INTERPOSE_HOOK(building_hook, updateAction).apply(enable); + + if (enable) + load_engines(); + else + clear_engines(); +} + +static bool enable_plugin() +{ + if (is_enabled) + return true; + + auto pworld = Core::getInstance().getWorld(); + auto entry = pworld->GetPersistentData("siege-engine/enabled", NULL); + if (!entry.isValid()) + return false; + + enable_hooks(true); + return true; +} + +static void clear_caches(color_ostream &out) +{ + if (!UnitPath::cache.empty()) + { + for (auto it = UnitPath::cache.begin(); it != UnitPath::cache.end(); ++it) + delete it->second; + + UnitPath::cache.clear(); + } +} + +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) +{ + switch (event) { + case SC_MAP_LOADED: + { + auto pworld = Core::getInstance().getWorld(); + bool enable = pworld->GetPersistentData("siege-engine/enabled").isValid(); + + if (enable) + { + out.print("Enabling the siege engine plugin.\n"); + enable_hooks(true); + } + else + enable_hooks(false); + } + break; + case SC_MAP_UNLOADED: + enable_hooks(false); + break; + default: + break; + } + + return CR_OK; +} + +DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &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; +} + +DFhackCExport command_result plugin_onupdate ( color_ostream &out ) +{ + clear_caches(out); + return CR_OK; +} diff --git a/plugins/sort.cpp b/plugins/sort.cpp index ff51fc773..4b2bf7bbd 100644 --- a/plugins/sort.cpp +++ b/plugins/sort.cpp @@ -228,7 +228,7 @@ static void sort_null_first(vector ¶meters) vector_insert_at(parameters, 0, std::string("(vector_get(layer->layer_objects,idx)); } diff --git a/plugins/stonesense b/plugins/stonesense index 2a62ba5ed..37a823541 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 2a62ba5ed2607f4dbf0473e77502d4e19c19678e +Subproject commit 37a823541538023b9f3d0d1e8039cf32851de68d diff --git a/plugins/tiletypes.cpp b/plugins/tiletypes.cpp index 190bda7cd..6af94f2ee 100644 --- a/plugins/tiletypes.cpp +++ b/plugins/tiletypes.cpp @@ -767,7 +767,7 @@ command_result executePaintJob(color_ostream &out) } // Remove liquid from walls, etc - if (type != -1 && !DFHack::FlowPassable(type)) + if (type != (df::tiletype)-1 && !DFHack::FlowPassable(type)) { des.bits.flow_size = 0; //des.bits.liquid_type = DFHack::liquid_water; diff --git a/plugins/tweak.cpp b/plugins/tweak.cpp index fbea30231..fb286e0d7 100644 --- a/plugins/tweak.cpp +++ b/plugins/tweak.cpp @@ -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 @@ -89,6 +98,17 @@ DFhackCExport command_result plugin_init (color_ostream &out, std::vector \n" + " 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; } @@ -293,6 +313,187 @@ struct stable_temp_hook : df::item_actual { IMPLEMENT_VMETHOD_INTERPOSE(stable_temp_hook, adjustTemperature); IMPLEMENT_VMETHOD_INTERPOSE(stable_temp_hook, updateContaminants); +static int map_temp_mult = -1; +static int max_heat_ticks = 0; + +struct fast_heat_hook : df::item_actual { + typedef df::item_actual interpose_base; + + DEFINE_VMETHOD_INTERPOSE( + bool, updateTempFromMap, + (bool local, bool contained, bool adjust, int32_t rate_mult) + ) { + int cmult = map_temp_mult; + map_temp_mult = rate_mult; + + bool rv = INTERPOSE_NEXT(updateTempFromMap)(local, contained, adjust, rate_mult); + map_temp_mult = cmult; + return rv; + } + + DEFINE_VMETHOD_INTERPOSE( + bool, updateTemperature, + (uint16_t temp, bool local, bool contained, bool adjust, int32_t rate_mult) + ) { + // Some items take ages to cross the last degree, so speed them up + if (map_temp_mult > 0 && temp != temperature && max_heat_ticks > 0) + { + int spec = getSpecHeat(); + if (spec != 60001) + rate_mult = std::max(map_temp_mult, spec/max_heat_ticks/abs(temp - temperature)); + } + + return INTERPOSE_NEXT(updateTemperature)(temp, local, contained, adjust, rate_mult); + } + + DEFINE_VMETHOD_INTERPOSE(bool, adjustTemperature, (uint16_t temp, int32_t rate_mult)) + { + if (map_temp_mult > 0) + rate_mult = map_temp_mult; + + return INTERPOSE_NEXT(adjustTemperature)(temp, rate_mult); + } +}; + +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 *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 ¶meters) { if (vector_get(parameters, 1) == "disable") @@ -430,6 +631,28 @@ static command_result tweak(color_ostream &out, vector ¶meters) enable_hook(out, INTERPOSE_HOOK(stable_temp_hook, adjustTemperature), parameters); enable_hook(out, INTERPOSE_HOOK(stable_temp_hook, updateContaminants), parameters); } + else if (cmd == "fast-heat") + { + if (parameters.size() < 2) + return CR_WRONG_USAGE; + max_heat_ticks = atoi(parameters[1].c_str()); + if (max_heat_ticks <= 0) + parameters[1] = "disable"; + enable_hook(out, INTERPOSE_HOOK(fast_heat_hook, updateTempFromMap), 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; diff --git a/plugins/workflow.cpp b/plugins/workflow.cpp index 639e7c56a..98258682e 100644 --- a/plugins/workflow.cpp +++ b/plugins/workflow.cpp @@ -828,7 +828,7 @@ static void compute_custom_job(ProtectedJob *pj, df::job *job) using namespace df::enums::reaction_product_item_flags; VIRTUAL_CAST_VAR(prod, df::reaction_product_itemst, r->products[i]); - if (!prod || (prod->item_type < 0 && !prod->flags.is_set(CRAFTS))) + if (!prod || (prod->item_type < (df::item_type)0 && !prod->flags.is_set(CRAFTS))) continue; MaterialInfo mat(prod); diff --git a/plugins/zone.cpp b/plugins/zone.cpp index fc89fecc1..c496f49b6 100644 --- a/plugins/zone.cpp +++ b/plugins/zone.cpp @@ -792,7 +792,7 @@ void unitInfo(color_ostream & out, df::unit* unit, bool verbose = false) bool isActivityZone(df::building * building) { if( building->getType() == building_type::Civzone - && building->getSubtype() == civzone_type::ActivityZone) + && building->getSubtype() == (short)civzone_type::ActivityZone) return true; else return false; @@ -1603,7 +1603,7 @@ void zoneInfo(color_ostream & out, df::building* building, bool verbose) if(building->getType()!= building_type::Civzone) return; - if(building->getSubtype() != civzone_type::ActivityZone) + if(building->getSubtype() != (short)civzone_type::ActivityZone) return; string name; diff --git a/scripts/devel/pop-screen.lua b/scripts/devel/pop-screen.lua new file mode 100644 index 000000000..f1ee072c5 --- /dev/null +++ b/scripts/devel/pop-screen.lua @@ -0,0 +1,3 @@ +-- For killing bugged out gui script screens. + +dfhack.screen.dismiss(dfhack.gui.getCurViewscreen()) diff --git a/scripts/fix/stable-temp.lua b/scripts/fix/stable-temp.lua index d06d0fcce..27a88ef7b 100644 --- a/scripts/fix/stable-temp.lua +++ b/scripts/fix/stable-temp.lua @@ -1,5 +1,9 @@ -- Reset item temperature to the value of their tile. +local args = {...} + +local apply = (args[1] == 'apply') + local count = 0 local types = {} @@ -9,13 +13,16 @@ local function update_temp(item,btemp) local tid = item:getType() types[tid] = (types[tid] or 0) + 1 end - item.temperature = btemp - item.temperature_fraction = 0 - if item.contaminants then - for _,c in ipairs(item.contaminants) do - c.temperature = btemp - c.temperature_fraction = 0 + if apply then + item.temperature = btemp + item.temperature_fraction = 0 + + if item.contaminants then + for _,c in ipairs(item.contaminants) do + c.temperature = btemp + c.temperature_fraction = 0 + end end end @@ -23,7 +30,9 @@ local function update_temp(item,btemp) update_temp(sub,btemp) end - item:checkTemperatureDamage() + if apply then + item:checkTemperatureDamage() + end end local last_frame = df.global.world.frame_counter-1 @@ -39,7 +48,11 @@ for _,item in ipairs(df.global.world.items.all) do end end -print('Items updated: '..count) +if apply then + print('Items updated: '..count) +else + print('Items not in equilibrium: '..count) +end local tlist = {} for k,_ in pairs(types) do tlist[#tlist+1] = k end @@ -47,3 +60,7 @@ table.sort(tlist, function(a,b) return types[a] > types[b] end) for _,k in ipairs(tlist) do print(' '..df.item_type[k]..':', types[k]) end + +if not apply then + print("Use 'fix/stable-temp apply' to force-change temperature.") +end diff --git a/scripts/gui/hello-world.lua b/scripts/gui/hello-world.lua index 80986bbf6..c8cd3bd01 100644 --- a/scripts/gui/hello-world.lua +++ b/scripts/gui/hello-world.lua @@ -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() diff --git a/scripts/gui/liquids.lua b/scripts/gui/liquids.lua index 89f08b7cf..cddb9f01d 100644 --- a/scripts/gui/liquids.lua +++ b/scripts/gui/liquids.lua @@ -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() diff --git a/scripts/gui/mechanisms.lua b/scripts/gui/mechanisms.lua index 4468e1dcb..d1e8ec803 100644 --- a/scripts/gui/mechanisms.lua +++ b/scripts/gui/mechanisms.lua @@ -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) @@ -122,10 +120,10 @@ function MechanismList:onInput(keys) end end -if not string.find(dfhack.gui.getCurFocus(), 'dwarfmode/QueryBuilding/Some') then +if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some') then 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) diff --git a/scripts/gui/power-meter.lua b/scripts/gui/power-meter.lua index 8baf43e7e..6c2f699ac 100644 --- a/scripts/gui/power-meter.lua +++ b/scripts/gui/power-meter.lua @@ -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() diff --git a/scripts/gui/room-list.lua b/scripts/gui/room-list.lua index a4507466f..0de82db5f 100644 --- a/scripts/gui/room-list.lua +++ b/scripts/gui/room-list.lua @@ -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 diff --git a/scripts/gui/siege-engine.lua b/scripts/gui/siege-engine.lua new file mode 100644 index 000000000..c98cb1676 --- /dev/null +++ b/scripts/gui/siege-engine.lua @@ -0,0 +1,490 @@ +-- Front-end for the siege engine plugin. + +local utils = require 'utils' +local gui = require 'gui' +local guidm = require 'gui.dwarfmode' +local dlg = require 'gui.dialogs' + +local plugin = require 'plugins.siege-engine' +local wmap = df.global.world.map + +local LEGENDARY = df.skill_rating.Legendary + +-- Globals kept between script calls +last_target_min = last_target_min or nil +last_target_max = last_target_max or nil + +local item_choices = { + { caption = 'boulders (default)', item_type = df.item_type.BOULDER }, + { caption = 'blocks', item_type = df.item_type.BLOCKS }, + { caption = 'weapons', item_type = df.item_type.WEAPON }, + { 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 }, +} + +local item_choice_idx = {} +for i,v in ipairs(item_choices) do + item_choice_idx[v.item_type] = i +end + +SiegeEngine = defclass(SiegeEngine, guidm.MenuOverlay) + +SiegeEngine.focus_path = 'siege-engine' + +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', + } + } +end + +function SiegeEngine:onShow() + SiegeEngine.super.onShow(self) + + self.old_cursor = guidm.getCursorPos() + self.old_viewport = self:getViewport() + + self.mode = self.mode_main + self:showCursor(false) +end + +function SiegeEngine:onDestroy() + if self.save_profile then + plugin.saveWorkshopProfile(self.building) + end + if not self.no_select_building then + self:selectBuilding(self.building, self.old_cursor, self.old_viewport, 10) + end +end + +function SiegeEngine:showCursor(enable) + local cursor = guidm.getCursorPos() + if cursor and not enable then + self.cursor = cursor + self.target_select_first = nil + guidm.clearCursorPos() + elseif not cursor and enable then + local view = self:getViewport() + cursor = self.cursor + if not cursor or not view:isVisible(cursor) then + cursor = view:getCenter() + end + self.cursor = nil + guidm.setCursorPos(cursor) + end +end + +function SiegeEngine:centerViewOn(pos) + local cursor = guidm.getCursorPos() + if cursor then + guidm.setCursorPos(pos) + else + self.cursor = pos + end + self:getViewport():centerOn(pos):set() +end + +function SiegeEngine:zoomToTarget() + local target_min, target_max = plugin.getTargetArea(self.building) + if target_min then + local cx = math.floor((target_min.x + target_max.x)/2) + local cy = math.floor((target_min.y + target_max.y)/2) + local cz = math.floor((target_min.z + target_max.z)/2) + local pos = plugin.adjustToTarget(self.building, xyz2pos(cx,cy,cz)) + self:centerViewOn(pos) + end +end + +function paint_target_grid(dc, view, origin, p1, p2) + local r1, sz, r2 = guidm.getSelectionRange(p1, p2) + + if view.z < r1.z or view.z > r2.z then + return + end + + local p1 = view:tileToScreen(r1) + local p2 = view:tileToScreen(r2) + local org = view:tileToScreen(origin) + dc:pen{ fg = COLOR_CYAN, bg = COLOR_CYAN, ch = '+', bold = true } + + -- Frame + dc:fill(p1.x,p1.y,p1.x,p2.y) + dc:fill(p1.x,p1.y,p2.x,p1.y) + dc:fill(p2.x,p1.y,p2.x,p2.y) + dc:fill(p1.x,p2.y,p2.x,p2.y) + + -- Grid + local gxmin = org.x+10*math.ceil((p1.x-org.x)/10) + local gxmax = org.x+10*math.floor((p2.x-org.x)/10) + local gymin = org.y+10*math.ceil((p1.y-org.y)/10) + local gymax = org.y+10*math.floor((p2.y-org.y)/10) + for x = gxmin,gxmax,10 do + for y = gymin,gymax,10 do + dc:fill(p1.x,y,p2.x,y) + dc:fill(x,p1.y,x,p2.y) + end + end +end + +function SiegeEngine:renderTargetView(target_min, target_max) + local view = self:getViewport() + local map = self.df_layout.map + local map_dc = gui.Painter.new(map) + + plugin.paintAimScreen( + self.building, view:getPos(), + xy2pos(map.x1, map.y1), view:getSize() + ) + + if target_min and math.floor(dfhack.getTickCount()/500) % 2 == 0 then + paint_target_grid(map_dc, view, self.center, target_min, target_max) + end + + local cursor = guidm.getCursorPos() + if cursor then + local cx, cy, cz = pos2xyz(view:tileToScreen(cursor)) + if cz == 0 then + map_dc:seek(cx,cy):char('X', COLOR_YELLOW) + end + end +end + +function SiegeEngine:scrollPiles(delta) + local links = plugin.getStockpileLinks(self.building) + if links then + self.selected_pile = 1+(self.selected_pile+delta-1) % #links + return links[self.selected_pile] + end +end + +function SiegeEngine:renderStockpiles(dc, links, nlines) + local idx = (self.selected_pile-1) % #links + local page = math.floor(idx/nlines) + for i = page*nlines,math.min(#links,(page+1)*nlines)-1 do + local color = COLOR_BROWN + if i == idx then + color = COLOR_YELLOW + end + dc:newline(2):string(utils.getBuildingName(links[i+1]), color) + end +end + +function SiegeEngine:onRenderBody_main(dc) + dc:newline(1):pen(COLOR_WHITE):string("Target: ") + + local target_min, target_max = plugin.getTargetArea(self.building) + if target_min then + dc:string( + (target_max.x-target_min.x+1).."x".. + (target_max.y-target_min.y+1).."x".. + (target_max.z-target_min.z+1).." Rect" + ) + else + dc:string("None (default)") + end + + dc:newline(3):string("r",COLOR_LIGHTGREEN):string(": Rectangle") + if last_target_min then + dc:string(", "):string("p",COLOR_LIGHTGREEN):string(": Paste") + end + dc:newline(3) + if target_min then + dc:string("x",COLOR_LIGHTGREEN):string(": Clear, ") + dc:string("z",COLOR_LIGHTGREEN):string(": Zoom") + end + + dc:newline():newline(1) + if self.building.type == df.siegeengine_type.Ballista then + dc:string("Uses ballista arrows") + else + local item = plugin.getAmmoItem(self.building) + dc:string("u",COLOR_LIGHTGREEN):string(": Use ") + if item_choice_idx[item] then + dc:string(item_choices[item_choice_idx[item]].caption) + else + dc:string(df.item_type[item]) + end + end + + dc:newline():newline(1) + dc:string("t",COLOR_LIGHTGREEN):string(": Take from stockpile"):newline(3) + local links = plugin.getStockpileLinks(self.building) + local bottom = dc.height - 5 + if links then + dc:string("d",COLOR_LIGHTGREEN):string(": Delete, ") + dc:string("o",COLOR_LIGHTGREEN):string(": Zoom"):newline() + self:renderStockpiles(dc, links, bottom-2-dc:localY()) + dc:newline():newline() + end + + local prof = self.building:getWorkshopProfile() or {} + dc:seek(1,math.max(dc:localY(),19)):string('ghjk',COLOR_LIGHTGREEN)dc:string(': ') + dc:string(df.skill_rating.attrs[prof.min_level or 0].caption):string('-') + dc:string(df.skill_rating.attrs[math.min(LEGENDARY,prof.max_level or 3000)].caption) + dc:newline():newline() + + if self.target_select_first then + self:renderTargetView(self.target_select_first, guidm.getCursorPos()) + else + self:renderTargetView(target_min, target_max) + end +end + +function SiegeEngine:setTargetArea(p1, p2) + self.target_select_first = nil + + if not plugin.setTargetArea(self.building, p1, p2) then + dlg.showMessage( + 'Set Target Area', + 'Could not set the target area', COLOR_LIGHTRED + ) + else + last_target_min = p1 + last_target_max = p2 + end +end + +function SiegeEngine:setAmmoItem(choice) + if self.building.type == df.siegeengine_type.Ballista then + return + end + + if not plugin.setAmmoItem(self.building, choice.item_type) then + dlg.showMessage( + 'Set Ammo Item', + 'Could not set the ammo item', COLOR_LIGHTRED + ) + end +end + +function SiegeEngine:onInput_main(keys) + if keys.CUSTOM_R then + self:showCursor(true) + self.target_select_first = nil + self.mode = self.mode_aim + elseif keys.CUSTOM_P and last_target_min then + self:setTargetArea(last_target_min, last_target_max) + elseif keys.CUSTOM_U then + local item = plugin.getAmmoItem(self.building) + local idx = 1 + (item_choice_idx[item] or 0) % #item_choices + self:setAmmoItem(item_choices[idx]) + elseif keys.CUSTOM_Z then + self:zoomToTarget() + elseif keys.CUSTOM_X then + plugin.clearTargetArea(self.building) + elseif keys.SECONDSCROLL_UP then + self:scrollPiles(-1) + elseif keys.SECONDSCROLL_DOWN then + self:scrollPiles(1) + elseif keys.CUSTOM_D then + local pile = self:scrollPiles(0) + if pile then + plugin.removeStockpileLink(self.building, pile) + end + elseif keys.CUSTOM_O then + local pile = self:scrollPiles(0) + if pile then + self:centerViewOn(utils.getBuildingCenter(pile)) + end + elseif keys.CUSTOM_T then + self:showCursor(true) + self.mode = self.mode_pile + self:sendInputToParent('CURSOR_DOWN_Z') + self:sendInputToParent('CURSOR_UP_Z') + elseif keys.CUSTOM_G then + local prof = plugin.saveWorkshopProfile(self.building) + prof.min_level = math.max(0, prof.min_level-1) + plugin.saveWorkshopProfile(self.building) + elseif keys.CUSTOM_H then + local prof = plugin.saveWorkshopProfile(self.building) + prof.min_level = math.min(LEGENDARY, prof.min_level+1) + plugin.saveWorkshopProfile(self.building) + elseif keys.CUSTOM_J then + local prof = plugin.saveWorkshopProfile(self.building) + prof.max_level = math.max(0, math.min(LEGENDARY,prof.max_level)-1) + plugin.saveWorkshopProfile(self.building) + elseif keys.CUSTOM_K then + local prof = plugin.saveWorkshopProfile(self.building) + prof.max_level = math.min(LEGENDARY, prof.max_level+1) + if prof.max_level >= LEGENDARY then prof.max_level = 3000 end + plugin.saveWorkshopProfile(self.building) + elseif self:simulateViewScroll(keys) then + self.cursor = nil + else + return false + end + return true +end + +local status_table = { + ok = { pen = COLOR_GREEN, msg = "Target accessible" }, + out_of_range = { pen = COLOR_CYAN, msg = "Target out of range" }, + blocked = { pen = COLOR_RED, msg = "Target obstructed" }, + semi_blocked = { pen = COLOR_BROWN, msg = "Partially obstructed" }, +} + +function SiegeEngine:onRenderBody_aim(dc) + local cursor = guidm.getCursorPos() + local first = self.target_select_first + + dc:newline(1):string('Select target rectangle'):newline() + + local info = status_table[plugin.getTileStatus(self.building, cursor)] + if info then + dc:newline(2):string(info.msg, info.pen) + else + dc:newline(2):string('ERROR', COLOR_RED) + end + + dc:newline():newline(1):string("Enter",COLOR_LIGHTGREEN) + if first then + dc:string(": Finish rectangle") + else + dc:string(": Start rectangle") + end + dc:newline() + + local target_min, target_max = plugin.getTargetArea(self.building) + if target_min then + dc:newline(1):string("z",COLOR_LIGHTGREEN):string(": Zoom to current target") + end + + if first then + self:renderTargetView(first, cursor) + else + local target_min, target_max = plugin.getTargetArea(self.building) + self:renderTargetView(target_min, target_max) + end +end + +function SiegeEngine:onInput_aim(keys) + if keys.SELECT then + local cursor = guidm.getCursorPos() + if self.target_select_first then + self:setTargetArea(self.target_select_first, cursor) + + self.mode = self.mode_main + self:showCursor(false) + else + self.target_select_first = cursor + end + elseif keys.CUSTOM_Z then + self:zoomToTarget() + elseif keys.LEAVESCREEN then + self.mode = self.mode_main + self:showCursor(false) + elseif self:simulateCursorMovement(keys) then + self.cursor = nil + else + return false + end + return true +end + +function SiegeEngine:onRenderBody_pile(dc) + dc:newline(1):string('Select pile to take from'):newline():newline(2) + + local sel = df.global.world.selected_building + + if df.building_stockpilest:is_instance(sel) then + dc:string(utils.getBuildingName(sel), COLOR_GREEN):newline():newline(1) + + if plugin.isLinkedToPile(self.building, sel) then + dc:string("Already taking from here"):newline():newline(2) + dc:string("d", COLOR_LIGHTGREEN):string(": Delete link") + else + dc:string("Enter",COLOR_LIGHTGREEN):string(": Take from this pile") + end + elseif sel then + dc:string(utils.getBuildingName(sel), COLOR_DARKGREY) + dc:newline():newline(1) + dc:string("Not a stockpile",COLOR_LIGHTRED) + else + dc:string("No building selected", COLOR_DARKGREY) + end +end + +function SiegeEngine:onInput_pile(keys) + if keys.SELECT then + local sel = df.global.world.selected_building + if df.building_stockpilest:is_instance(sel) + and not plugin.isLinkedToPile(self.building, sel) then + plugin.addStockpileLink(self.building, sel) + + df.global.world.selected_building = self.building + self.mode = self.mode_main + self:showCursor(false) + end + elseif keys.CUSTOM_D then + local sel = df.global.world.selected_building + if df.building_stockpilest:is_instance(sel) then + plugin.removeStockpileLink(self.building, sel) + end + elseif keys.LEAVESCREEN then + df.global.world.selected_building = self.building + self.mode = self.mode_main + self:showCursor(false) + elseif self:propagateMoveKeys(keys) then + -- + else + return false + end + return true +end + +function SiegeEngine:onRenderBody(dc) + dc:clear() + dc:seek(1,1):pen(COLOR_WHITE):string(utils.getBuildingName(self.building)):newline() + + self.mode.render(dc) + + dc:seek(1, math.max(dc:localY(), 21)):pen(COLOR_WHITE) + dc:string("ESC", COLOR_LIGHTGREEN):string(": Back, ") + dc:string("c", COLOR_LIGHTGREEN):string(": Recenter") +end + +function SiegeEngine:onInput(keys) + if self.mode.input(keys) then + -- + elseif keys.CUSTOM_C then + self:centerViewOn(self.center) + elseif keys.LEAVESCREEN then + self:dismiss() + elseif keys.LEAVESCREEN_ALL then + self:dismiss() + self.no_select_building = true + guidm.clearCursorPos() + df.global.ui.main.mode = df.ui_sidebar_mode.Default + df.global.world.selected_building = nil + end +end + +if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some/SiegeEngine') then + qerror("This script requires a siege engine selected in 'q' mode") +end + +local building = df.global.world.selected_building + +if not df.building_siegeenginest:is_instance(building) then + qerror("A siege engine must be selected") +end + +local list = SiegeEngine{ building = building } +list:show() diff --git a/scripts/setfps.lua b/scripts/setfps.lua new file mode 100644 index 000000000..690f82702 --- /dev/null +++ b/scripts/setfps.lua @@ -0,0 +1,10 @@ +-- Set the FPS cap at runtime. + +local cap = ... +local capnum = tonumber(cap) + +if not capnum or capnum < 1 then + qerror('Invalid FPS cap value: '..cap) +end + +df.global.enabler.fps = capnum