diff --git a/.github/workflows/generate-symbols.yml b/.github/workflows/generate-symbols.yml index 85a0067c85..9f4e32b146 100644 --- a/.github/workflows/generate-symbols.yml +++ b/.github/workflows/generate-symbols.yml @@ -36,6 +36,7 @@ on: default: default options: - default + - experimental - testing - adventure_test - beta diff --git a/.github/workflows/watch-df-release.yml b/.github/workflows/watch-df-release.yml index 6e72333034..77f4947781 100644 --- a/.github/workflows/watch-df-release.yml +++ b/.github/workflows/watch-df-release.yml @@ -15,13 +15,17 @@ jobs: fail-fast: false matrix: # df_steam_branch: which DF Steam branch to watch - # platform: leave blank to default to all + # platform: for symbols generation; leave blank to default to all # structures_ref: leave blank to default to master # dfhack_ref: leave blank if no structures update is desired # steam_branch: leave blank if no DFHack steam push is desired include: - df_steam_branch: public - df_steam_branch: beta + - df_steam_branch: experimental + structures_ref: experimental + dfhack_ref: experimental + steam_branch: experimental steps: - name: Fetch state uses: actions/cache/restore@v4 diff --git a/CMakeLists.txt b/CMakeLists.txt index e133c87acb..22f04fe085 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,9 +6,9 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "51.05") -set(DFHACK_RELEASE "r1") -set(DFHACK_PRERELEASE FALSE) +set(DF_VERSION "51.06") +set(DFHACK_RELEASE "r2rc1") +set(DFHACK_PRERELEASE TRUE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") set(DFHACK_ABI_VERSION 2) @@ -451,7 +451,6 @@ if(INSTALL_DATA_FILES) install(FILES LICENSE.rst DESTINATION ${DFHACK_USERDOC_DESTINATION}) install(FILES docs/changelog-placeholder.txt DESTINATION ${DFHACK_USERDOC_DESTINATION} RENAME changelog.txt) install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/depends/luacov/src/luacov/ DESTINATION ${DFHACK_DATA_DESTINATION}/lua/luacov) - install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/patches/ DESTINATION ${DFHACK_DATA_DESTINATION}/patches) endif() if(INSTALL_SCRIPTS) diff --git a/data/CMakeLists.txt b/data/CMakeLists.txt index 355182bfb7..75146071b8 100644 --- a/data/CMakeLists.txt +++ b/data/CMakeLists.txt @@ -24,6 +24,10 @@ install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/blueprints/ FILES_MATCHING PATTERN "*" PATTERN blueprints/test EXCLUDE) +install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/patches/ + DESTINATION ${DFHACK_DATA_DESTINATION}/patches +) + if(BUILD_TESTS) install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/blueprints/test/ DESTINATION "${DFHACK_DATA_DESTINATION}/data/blueprints/test") diff --git a/data/orders/basic.json b/data/orders/basic.json index 688bdaa7ab..e1cd5ac8ff 100644 --- a/data/orders/basic.json +++ b/data/orders/basic.json @@ -114,27 +114,6 @@ "job" : "CustomReaction", "reaction" : "BREW_DRINK_FROM_PLANT_GROWTH" }, - { - "amount_left" : 1, - "amount_total" : 1, - "frequency" : "Daily", - "id" : 3, - "is_active" : false, - "is_validated" : false, - "item_conditions" : - [ - { - "condition" : "AtLeast", - "flags" : - [ - "unrotten", - "milk" - ], - "value" : 2 - } - ], - "job" : "MakeCheese" - }, { "amount_left" : 1, "amount_total" : 1, diff --git a/data/patches/README.md b/data/patches/README.md new file mode 100644 index 0000000000..599bbf47fb --- /dev/null +++ b/data/patches/README.md @@ -0,0 +1,13 @@ +Place IDA-exported `.dif` files for use by `binpatch` in subdirectories of this +directory. Each `.dif` file must be in a subdirectory named after the full +symbol table version string. For example, for DF version 51.05, you would use +these subdirectories: + +- "v0.51.05 linux64 CLASSIC" +- "v0.51.05 linux64 ITCH" +- "v0.51.05 linux64 STEAM" +- "v0.51.05 win64 CLASSIC" +- "v0.51.05 win64 ITCH" +- "v0.51.05 win64 STEAM" + +See https://docs.dfhack.org/en/stable/docs/dev/Binpatches.html for more details. diff --git a/docs/changelog.txt b/docs/changelog.txt index 7c2b587bda..c883d4868b 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,15 +57,15 @@ Template for new versions: - `spectate`: can now specify number of seconds (in real time) before switching to follow a new unit - `spectate`: new "cinematic-action" mode that dynamically speeds up perspective switches based on intensity of conflict - `spectate`: new global keybinding for toggling spectate mode: Ctrl-Shift-S -- `spectate`: new overlay panel that allows you to cycle through following next/prevous units (regardless of whether spectate mode is enabled) -- ``disable-vanilla-dimensions``: new binpatch to disable the vanilla dimensions tooltip +- `spectate`: new overlay panel that allows you to cycle through following next/previous units (regardless of whether spectate mode is enabled) - `gui/sitemap`: is now the official "go to" tool. new global hotkey for fort and adventure mode: Ctrl-G ## Fixes - Windows console: fix possible hang if the console returns a too-small window width (for any reason) -- `createitem`: output items will now end up at look cursor if active +- `createitem`: produced items will now end up at the look cursor position (if it is active) - `spectate`: don't allow temporarily modified announcement settings to be written to disk when "auto-unpause" mode is enabled - `changevein`: fix a crash that could occur when attempting to change a vein into itself +- `overlay`: reset draw context between rendering widgets so context changes can't propagate from widget to widget ## Misc Improvements - `spectate`: player-set configuration is now stored globally instead of per-fort @@ -77,22 +77,32 @@ Template for new versions: ## API - ``Military::removeFromSquad``: removes unit from any squad assignments -- ``Buildings::checkFreeTiles``: now replaces the extents parameter for a building pointer +- ``Buildings::checkFreeTiles``: now takes a building instead of a pointer to the building extents - ``Units::isUnitInBox``, ``Units::getUnitsInBox``: don't include inactive units +- ``Items::getItemBaseValue``: adjust to the reduced value of prepared meals (changed in DF 51.06) +- ``Items::getValue``: magical powers now correctly contribute to item value ## Lua - ``dfhack.units.setAutomaticProfessions``: sets unit labors according to current work detail settings - ``dfhack.military.removeFromSquad``: Lua API for ``Military::removeFromSquad`` - ``gui.dwarfmode``: adventure mode cursor now supported in ``getCursorPos``, ``setCursorPos``, and ``clearCursorPos`` funcitons -- ``dfhack.buildings.checkFreeTiles``: now replaces the extents parameter for a building pointer +- ``dfhack.buildings.checkFreeTiles``: now takes a building pointer instead of an extents parameter - ``overlay.isOverlayEnabled``: new API for querying whether a given overlay is enabled +- ``overlay``: widgets can now declare ``overlay_onenable`` and ``overlay_ondisable`` functions to hook enable/disable ## Removed +- `orders`: MakeCheese job removed from library/basic orders set. Please use `autocheese` instead! + +# 51.06-r1 + +## Misc Improvements +- Compatibility with DF 51.06 # 51.05-r1 ## Misc Improvements - Compatibility with DF 51.05 + # 51.04-r1.1 ## Fixes diff --git a/docs/dev/Binpatches.rst b/docs/dev/Binpatches.rst index a566576168..d59b1f8482 100644 --- a/docs/dev/Binpatches.rst +++ b/docs/dev/Binpatches.rst @@ -50,10 +50,20 @@ directly in memory at runtime:: If the name of the patch has no extension or directory separators, the script uses :file:`hack/patches//.dif`, thus auto-selecting -the version appropriate for the currently loaded executable. +the version appropriate for the currently loaded executable. The ``df-version`` +is the version string in the loaded symbol table. For example, if you want to +make a patch for all distributed verisons of DF 51.05, you'd provide a ``dif`` +file in each of the following directories: + +- :file:`hack/patches/v0.51.05 linux64 CLASSIC/mypatch.dif` +- :file:`hack/patches/v0.51.05 linux64 ITCH/mypatch.dif` +- :file:`hack/patches/v0.51.05 linux64 STEAM/mypatch.dif` +- :file:`hack/patches/v0.51.05 win64 CLASSIC/mypatch.dif` +- :file:`hack/patches/v0.51.05 win64 ITCH/mypatch.dif` +- :file:`hack/patches/v0.51.05 win64 STEAM/mypatch.dif` This is the preferred method; it's easier to debug, does not cause persistent -problems, and leaves file checksums alone. As with many other commands, users +problems, and leaves file checksums alone. As with many other commands, users can simply add it to `dfhack.init` to reapply the patch every time DF is run. diff --git a/docs/dev/overlay-dev-guide.rst b/docs/dev/overlay-dev-guide.rst index 4302d5d4c8..d87178bfff 100644 --- a/docs/dev/overlay-dev-guide.rst +++ b/docs/dev/overlay-dev-guide.rst @@ -86,6 +86,11 @@ beyond your everyday `widgets.Widget `: end This allows for dynamic updates to UI overlays directly from the CLI. +- If an ``overlay_onenable()`` function is defined, it is called when the + overlay is enabled (including when the persisted state is reloaded at DF + startup). +- If an ``overlay_ondisable()`` function is defined, it is called when the + overlay is disabled. If the widget can take up a variable amount of space on the screen, and you want the widget to adjust its position according to the size of its contents, you can diff --git a/docs/plugins/orders.rst b/docs/plugins/orders.rst index 9ac3009af8..378881d12c 100644 --- a/docs/plugins/orders.rst +++ b/docs/plugins/orders.rst @@ -145,7 +145,14 @@ This collection of orders handles basic fort necessities: You should import it as soon as you have enough dwarves to perform the tasks. Right after the first migration wave is usually a good time. -Note that the jugs are specifically made out of wood. This is so, as long as you don't may any other "Tools" out of wood, you can have a stockpile just for jugs by restricting a finished goods stockpile to only take wooden tools. +These orders do not contain milking, shearing, or cheesemaking jobs since the +game does not provide sufficient order conditions. Please enable ``automilk``, +``autoshear``, and `autocheese` on the DFHack `gui/control-panel` for these +types of jobs. + +Note that the jugs are specifically made out of wood. This is so, as long as +you don't may any other "Tools" out of wood, you can have a stockpile just for +jugs by restricting a finished goods stockpile to only take wooden tools. Armok's additional note: "shleggings? Yes, `shleggings `__." @@ -155,7 +162,7 @@ Armok's additional note: "shleggings? Yes, This collection creates basic items that require heat. It is separated out from ``library/basic`` to give players the opportunity to set up magma furnaces first -in order to save resources. It handles: +(if desired) in order to save resources. It handles: - charcoal (including smelting of bituminous coal and lignite) - pearlash diff --git a/library/Core.cpp b/library/Core.cpp index 9dc2d2e68a..be53fe58fd 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -2987,7 +2987,7 @@ bool MemoryPatcher::verifyAccess(void *target, size_t count, bool write) save.push_back(perms); perms.write = perms.read = true; - if (!p->setPermisions(perms, perms)) + if (!p->setPermissions(perms, perms)) return false; } @@ -3000,13 +3000,15 @@ bool MemoryPatcher::write(void *target, const void *src, size_t size) return false; memmove(target, src, size); + + p->flushCache(target, size); return true; } void MemoryPatcher::close() { for (size_t i = 0; i < save.size(); i++) - p->setPermisions(save[i], save[i]); + p->setPermissions(save[i], save[i]); save.clear(); ranges.clear(); diff --git a/library/DataIdentity.cpp b/library/DataIdentity.cpp index e6b4889cf8..a385915d92 100644 --- a/library/DataIdentity.cpp +++ b/library/DataIdentity.cpp @@ -38,6 +38,7 @@ namespace df { const bool_identity identity_traits::identity; const stl_string_identity identity_traits::identity; + const path_identity identity_traits::identity; const ptr_string_identity identity_traits::identity; const ptr_string_identity identity_traits::identity; const pointer_identity identity_traits::identity; diff --git a/library/LuaTypes.cpp b/library/LuaTypes.cpp index 1aa7711e68..81914a7185 100644 --- a/library/LuaTypes.cpp +++ b/library/LuaTypes.cpp @@ -188,6 +188,24 @@ void df::stl_string_identity::lua_write(lua_State *state, int fname_idx, void *p *(std::string*)ptr = std::string(bytes, size); } +void df::path_identity::lua_read(lua_State* state, int fname_idx, void* ptr) const +{ + auto ppath = (std::filesystem::path*)ptr; + auto str = ppath->u8string(); + lua_pushlstring(state, (char*)str.data(), str.size()); +} + +void df::path_identity::lua_write(lua_State* state, int fname_idx, void* ptr, int val_index) const +{ + size_t size; + const char* bytes = lua_tolstring(state, val_index, &size); + if (!bytes) + field_error(state, fname_idx, "path expected", "write"); + + std::u8string str((char8_t*)bytes, size); + *(std::filesystem::path*)ptr = std::filesystem::path(str); +} + void df::pointer_identity::lua_read(lua_State *state, int fname_idx, void *ptr, const type_identity *target) { push_object_internal(state, target, *(void**)ptr); diff --git a/library/LuaWrapper.cpp b/library/LuaWrapper.cpp index 608ef95683..f9fd0e7832 100644 --- a/library/LuaWrapper.cpp +++ b/library/LuaWrapper.cpp @@ -49,7 +49,7 @@ using namespace DFHack::LuaWrapper; /** * Report an error while accessing a field (index = field name). */ -void LuaWrapper::field_error(lua_State *state, int index, const char *err, const char *mode) +[[noreturn]] void LuaWrapper::field_error(lua_State *state, int index, const char *err, const char *mode) { if (lua_islightuserdata(state, UPVAL_METATABLE)) lua_pushstring(state, "(global)"); @@ -59,6 +59,7 @@ void LuaWrapper::field_error(lua_State *state, int index, const char *err, const const char *fname = index ? lua_tostring(state, index) : "*"; luaL_error(state, "Cannot %s field %s.%s: %s.", mode, (cname ? cname : "?"), (fname ? fname : "?"), err); + std::abort(); // should never be reached but makes gcc happy } /* */ diff --git a/library/Process-linux.cpp b/library/Process-linux.cpp index 85e83f54f6..a3fa9f2d67 100644 --- a/library/Process-linux.cpp +++ b/library/Process-linux.cpp @@ -205,7 +205,7 @@ int Process::getPID() return getpid(); } -bool Process::setPermisions(const t_memrange & range,const t_memrange &trgrange) +bool Process::setPermissions(const t_memrange & range,const t_memrange &trgrange) { int result; int protect=0; @@ -217,6 +217,13 @@ bool Process::setPermisions(const t_memrange & range,const t_memrange &trgrange) return result==0; } +bool Process::flushCache(const void* target, size_t count) +{ + __builtin___clear_cache((char*)target, (char*)target + count - 1); + return true; /* assume always succeeds, as the builtin has no return type */ +} + + // returns -1 on error void* Process::memAlloc(const int length) { diff --git a/library/Process-windows.cpp b/library/Process-windows.cpp index 2450eb87bd..79bbea8002 100644 --- a/library/Process-windows.cpp +++ b/library/Process-windows.cpp @@ -399,7 +399,7 @@ int Process::getPID() } -bool Process::setPermisions(const t_memrange & range,const t_memrange &trgrange) +bool Process::setPermissions(const t_memrange & range,const t_memrange &trgrange) { DWORD newprotect=0; if(trgrange.read && !trgrange.write && !trgrange.execute)newprotect=PAGE_READONLY; @@ -414,6 +414,11 @@ bool Process::setPermisions(const t_memrange & range,const t_memrange &trgrange) return result; } +bool Process::flushCache(const void* target, size_t count) +{ + return 0 != FlushInstructionCache(d->my_handle, (LPCVOID)target, count); +} + void* Process::memAlloc(const int length) { void *ret; diff --git a/library/include/DataIdentity.h b/library/include/DataIdentity.h index 02bc8b3201..7e1f8eaaea 100644 --- a/library/include/DataIdentity.h +++ b/library/include/DataIdentity.h @@ -34,6 +34,7 @@ distribution. #include #include #include +#include #include "DataDefs.h" @@ -298,6 +299,24 @@ namespace df virtual void lua_write(lua_State *state, int fname_idx, void *ptr, int val_index) const; }; + class DFHACK_EXPORT path_identity : public DFHack::constructed_identity { + public: + path_identity() + : constructed_identity(sizeof(std::filesystem::path), &allocator_fn) + { + }; + + const std::string getFullName() const { return "path"; } + + virtual DFHack::identity_type type() const { return DFHack::IDTYPE_PRIMITIVE; } + + virtual bool isPrimitive() const { return true; } + + virtual void lua_read(lua_State* state, int fname_idx, void* ptr) const; + virtual void lua_write(lua_State* state, int fname_idx, void* ptr, int val_index) const; + }; + + class DFHACK_EXPORT stl_ptr_vector_identity : public ptr_container_identity { public: typedef std::vector container; @@ -616,6 +635,11 @@ namespace df static const stl_string_identity *get() { return &identity; } }; + template<> struct DFHACK_EXPORT identity_traits { + static const bool is_primitive = true; + static const path_identity identity; + static const path_identity* get() { return &identity; } + }; template<> struct DFHACK_EXPORT identity_traits { static const bool is_primitive = true; static const ptr_string_identity identity; diff --git a/library/include/LuaWrapper.h b/library/include/LuaWrapper.h index 347945e896..44bc19938c 100644 --- a/library/include/LuaWrapper.h +++ b/library/include/LuaWrapper.h @@ -142,7 +142,7 @@ namespace LuaWrapper { /** * Report an error while accessing a field (index = field name). */ - void field_error(lua_State *state, int index, const char *err, const char *mode); + [[noreturn]] void field_error(lua_State *state, int index, const char *err, const char *mode); /* * If is_method is true, these use UPVAL_TYPETABLE to save a hash lookup. diff --git a/library/include/MemAccess.h b/library/include/MemAccess.h index 083d94434c..bcbab75bd3 100644 --- a/library/include/MemAccess.h +++ b/library/include/MemAccess.h @@ -270,11 +270,14 @@ namespace DFHack uint32_t getTickCount(); /// modify permisions of memory range - bool setPermisions(const t_memrange & range,const t_memrange &trgrange); + bool setPermissions(const t_memrange & range,const t_memrange &trgrange); /// write a possibly read-only memory area bool patchMemory(void *target, const void* src, size_t count); + /// flush cache + bool flushCache(const void* target, size_t count); + /// allocate new memory pages for code or stuff /// returns -1 on error (0 is a valid address) void* memAlloc(const int length); diff --git a/library/modules/Items.cpp b/library/modules/Items.cpp index a99c2bb761..ff92b3cece 100644 --- a/library/modules/Items.cpp +++ b/library/modules/Items.cpp @@ -64,6 +64,7 @@ distribution. #include "df/historical_entity.h" #include "df/item.h" #include "df/item_bookst.h" +#include "df/item_magicalst.h" #include "df/item_plant_growthst.h" #include "df/item_toolst.h" #include "df/item_type.h" @@ -1238,7 +1239,7 @@ int Items::getItemBaseValue(int16_t item_type, int16_t item_subtype, value = craw->misc.petvalue; return value; case FOOD: - return 10; + return 1; case CORPSE: case CORPSEPIECE: case REMAINS: @@ -1645,7 +1646,10 @@ int Items::getValue(df::item *item, df::caravan_state *caravan) { case 3: value = value / 4; break; // (XX) tattered } - // Ignore value bonuses from magic, since that never actually happens + // Magical powers have 500 value each + auto magic = item->getMagic(); + if (magic != NULL) + value += magic->power.size() * 500; // Artifacts have 10x value if (item->flags.bits.artifact_mood) diff --git a/library/xml b/library/xml index 1e61d7f5f3..f5a63e87b3 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 1e61d7f5f36a0bc937985c5b859d8ee152789bde +Subproject commit f5a63e87b3d96c5c435d8737e95a70151bd218c1 diff --git a/patches/CMakeLists.txt b/patches/CMakeLists.txt deleted file mode 100644 index 37ee7c9440..0000000000 --- a/patches/CMakeLists.txt +++ /dev/null @@ -1,3 +0,0 @@ -install(DIRECTORY ${patchdirectories} - DESTINATION ${DFHACK_DATA_DESTINATION}/patches -) diff --git a/patches/v0.51.05 linux64 CLASSIC/disable-vanilla-dimensions.dif b/patches/v0.51.05 linux64 CLASSIC/disable-vanilla-dimensions.dif deleted file mode 100644 index f8c95a2af6..0000000000 --- a/patches/v0.51.05 linux64 CLASSIC/disable-vanilla-dimensions.dif +++ /dev/null @@ -1,6 +0,0 @@ -Dwarf_Fortress -011ba9eb: e8 90 -011ba9ec: c0 90 -011ba9ed: 4a 90 -011ba9ee: e6 90 -011ba9ef: fe 90 diff --git a/patches/v0.51.05 linux64 ITCH/disable-vanilla-dimensions.dif b/patches/v0.51.05 linux64 ITCH/disable-vanilla-dimensions.dif deleted file mode 100644 index 1784576881..0000000000 --- a/patches/v0.51.05 linux64 ITCH/disable-vanilla-dimensions.dif +++ /dev/null @@ -1,6 +0,0 @@ -Dwarf_Fortress -011c605b: e8 90 -011c605c: 50 90 -011c605d: a5 90 -011c605e: e5 90 -011c605f: fe 90 diff --git a/patches/v0.51.05 linux64 STEAM/disable-vanilla-dimensions.dif b/patches/v0.51.05 linux64 STEAM/disable-vanilla-dimensions.dif deleted file mode 100644 index 142e53e4fa..0000000000 --- a/patches/v0.51.05 linux64 STEAM/disable-vanilla-dimensions.dif +++ /dev/null @@ -1,6 +0,0 @@ -Dwarf_Fortress -011c9c3b: e8 90 -011c9c3c: 80 90 -011c9c3d: 6a 90 -011c9c3e: e5 90 -011c9c3f: fe 90 diff --git a/patches/v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif b/patches/v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif deleted file mode 100644 index 2f5cf9e0ae..0000000000 --- a/patches/v0.51.05 win64 CLASSIC/disable-vanilla-dimensions.dif +++ /dev/null @@ -1,6 +0,0 @@ -Dwarf_Fortress -377d5d: e8 90 -377d5e: ee 90 -377d5f: 93 90 -377d60: 68 90 -377d61: 00 90 diff --git a/patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif b/patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif deleted file mode 100644 index e254650761..0000000000 --- a/patches/v0.51.05 win64 ITCH/disable-vanilla-dimensions.dif +++ /dev/null @@ -1,6 +0,0 @@ -Dwarf_Fortress -37af8d: e8 90 -37af8e: 1e 90 -37af8f: b7 90 -37af90: 68 90 -37af91: 00 90 diff --git a/patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif b/patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif deleted file mode 100644 index 44dd5ebb81..0000000000 --- a/patches/v0.51.05 win64 STEAM/disable-vanilla-dimensions.dif +++ /dev/null @@ -1,6 +0,0 @@ -Dwarf_Fortress -37d77d: e8 90 -37d77e: 6e 90 -37d77f: bb 90 -37d780: 68 90 -37d781: 00 90 diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 8f230ff583..f87f9002ce 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -71,6 +71,7 @@ if(BUILD_SUPPORTED) #dfhack_plugin(dwarfmonitor dwarfmonitor.cpp LINK_LIBRARIES lua) #add_subdirectory(embark-assistant) dfhack_plugin(eventful eventful.cpp LINK_LIBRARIES lua) + dfhack_plugin(export-map export-map.cpp COMPILE_FLAGS_GCC -fno-gnu-unique LINK_LIBRARIES gdal) dfhack_plugin(fastdwarf fastdwarf.cpp) dfhack_plugin(filltraffic filltraffic.cpp) dfhack_plugin(fix-occupancy fix-occupancy.cpp LINK_LIBRARIES lua) diff --git a/plugins/export-map.cpp b/plugins/export-map.cpp new file mode 100644 index 0000000000..0891cd5701 --- /dev/null +++ b/plugins/export-map.cpp @@ -0,0 +1,400 @@ +#include "Debug.h" +#include "Error.h" +#include "PluginManager.h" +#include "MiscUtils.h" + +#include "modules/Maps.h" +#include "modules/Translation.h" + +#include "df/entity_raw.h" +#include "df/creature_raw.h" +#include "df/historical_entity.h" +#include "df/world.h" +#include "df/map_block.h" +#include "df/world_data.h" +#include "df/world_site.h" +#include "df/region_map_entry.h" +#include "df/world_region.h" +#include "df/world_landmass.h" +#include "df/world_region_details.h" + +#include "gdal/ogrsf_frmts.h" + +#include +#include +#include + +using std::string; +using std::vector; + +using namespace DFHack; + +DFHACK_PLUGIN("export-map"); + +REQUIRE_GLOBAL(world); + +namespace DFHack { + DBG_DECLARE(exportmap, log); +} + +static command_result do_command(color_ostream &out, vector ¶meters); + +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { + DEBUG(log,out).print("initializing %s\n", plugin_name); + + commands.push_back(PluginCommand( + plugin_name, + "Export the world map.", + do_command)); + + return CR_OK; +} + +auto setGeometry(OGRFeature *feature, double x, double y, double dimx, double dimy = 0) { + if (dimy == 0) { dimy = dimx; } + auto poly = new OGRPolygon(); + auto boundary = new OGRLinearRing(); + y = -y; // in GIS negative y-coordinates mean further south + boundary->addPoint(x,y); + boundary->addPoint(x,y-dimy); + boundary->addPoint(x+dimx,y-dimy); + boundary->addPoint(x+dimx,y); + boundary->closeRings(); + //the "Directly" variants assume ownership of the objects created above + poly->addRingDirectly(boundary); + feature->SetGeometryDirectly( poly ); +} + +auto get_world_index(int world_x, int world_y, int8_t dir) { + switch (dir) { + case 1: world_x-- ; world_y++; break; + case 2: ; world_y++; break; + case 3: world_x++ ; world_y++; break; + case 4: world_x-- ; ; break; + // case 5 induces no change + case 6: world_x++ ; ; break; + case 7: world_x-- ; world_y--; break; + case 8: ; world_y--; break; + case 9: world_x++ ; world_y--; break; + } + world_x = std::min(std::max(0,world_x),world->world_data->world_width - 1); + world_y = std::min(std::max(0,world_y),world->world_data->world_height - 1); + return std::pair(world_x,world_y); +} + +auto create_field(OGRLayer *layer, std::string name, OGRFieldType type, int width = 0, OGRFieldSubType subtype = OFSTNone) { + OGRFieldDefn field( name.c_str() , type ); + if (subtype != OFSTNone) { + field.SetSubType(subtype); + } + if (width != 0) { + field.SetWidth(width); + } + // this should create a copy internally + if( layer->CreateField( &field ) != OGRERR_NONE ){ + throw CR_FAILURE; + } +} + +// PROJ.4 description of EPSG:3857 (https://epsg.io/3857) +static const char* EPSG_3857 = "+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 +units=m +nadgrids=@null +wktext +no_defs +type=crs"; + +const char* describe_surroundings(int savagery, int evilness) { + constexpr std::arraysurroundings{ + "Serene", "Mirthful", "Joyous Wilds", + "Calm", "Wilderness", "Untamed Wilds", + "Sinister", "Haunted", "Terrifying" + }; + auto savagery_index = savagery < 33 ? 0 : (savagery > 65 ? 2 : 1); + auto evilness_index = evilness < 33 ? 0 : (evilness > 65 ? 2 : 1); + return surroundings[3 * evilness_index + savagery_index]; +} + +static int wdim = 768; // dimension of a world tile +static int rdim = 48; // dimension of a region tile + +static command_result export_region_tiles(color_ostream &out); +static command_result export_sites(color_ostream &out); + +static command_result do_command(color_ostream &out, vector ¶meters) +{ + CoreSuspender suspend; + + if (!Core::getInstance().isWorldLoaded()){ + out.printerr("This command requires a world to be loaded\n"); + return CR_WRONG_USAGE; + } + + if (parameters.size() && parameters[0] == "sites") + { + return export_sites(out); + } + else + { + return export_region_tiles(out); + } +} + +static command_result export_sites(color_ostream &out) +{ + out.print("exporting sites... "); + out.flush(); + const auto start{std::chrono::steady_clock::now()}; + + // set up coordinate system + OGRSpatialReference CRS; + if (CRS.importFromProj4(EPSG_3857) != OGRERR_NONE) { + out.printerr("could not set up coordinate system"); + return CR_FAILURE; + } + + // set up output driver + GDALAllRegister(); + const char *driver_name = "SQLite"; + const char *extension = "sqlite"; + auto driver = GetGDALDriverManager()->GetDriverByName(driver_name); + if (!driver) { + out.printerr("could not find sqlite driver"); + return CR_FAILURE; + } + + // create a dataset and associate it to a file + std::string sites("sites."); + sites.append(extension); + const char* options[] = { "SPATIALITE=YES", nullptr }; + auto dataset = driver->Create( sites.c_str(), 0, 0, 0, GDT_Unknown, options); + if (!dataset) { + out.printerr("could not create dataset"); + return CR_FAILURE; + } + + // create a layer for the biome data + // const char* format[] = { "FORMAT=WKT", nullptr }; + auto layer = dataset->CreateLayer( "world_sites", &CRS, wkbPolygon, nullptr ); + if (!layer) { + out.printerr("could not create layer"); + return CR_FAILURE; + } + + try { + create_field(layer, "site_id", OFTInteger); + create_field(layer, "civ_id", OFTInteger); + create_field(layer, "created_year", OFTInteger); + create_field(layer, "cur_owner_id", OFTInteger); + + create_field(layer, "type", OFTString, 15); + + create_field(layer, "site_name_df", OFTString, 100); + create_field(layer, "site_name_en", OFTString, 100); + + create_field(layer, "civ_name_df", OFTString, 100); + create_field(layer, "civ_name_en", OFTString, 100); + + create_field(layer, "site_government_df", OFTString, 100); + create_field(layer, "site_government_en", OFTString, 100); + + create_field(layer, "owner_race", OFTString, 15); + + // create_field(layer, "local_ruler", OFTString, 100); + + } + catch (const DFHack::command_result& r) { + out.printerr("could not create fields for output layer"); + return r; + } + + if (dataset->StartTransaction() != OGRERR_NONE) { + out.printerr("could not start a transaction\n"); + } + + for (auto const site : world->world_data->sites) + { + + auto feature = OGRFeature::CreateFeature( layer->GetLayerDefn() ); + + setGeometry( + feature, + site->global_min_x * rdim, + site->global_min_y * rdim, + (site->global_max_x - site->global_min_x + 1) * rdim, + (site->global_max_y - site->global_min_y + 1) * rdim + ); + feature->SetField( "site_id", site->id ); + feature->SetField( "type", ENUM_KEY_STR(world_site_type, site->type).c_str() ); + #define SET_FIELD(name) feature->SetField( #name, site->name) + SET_FIELD(civ_id); + SET_FIELD(created_year); + SET_FIELD(cur_owner_id); + #undef SET_FIELD + + #define TRANSLATE_NAME(field_name, name_object)\ + feature->SetField((#field_name"_df"), DF2UTF(Translation::translateName(&name_object, false)).c_str());\ + feature->SetField((#field_name"_en"), DF2UTF(Translation::translateName(&name_object, true)).c_str()); + + TRANSLATE_NAME(site_name, site->name) + + auto civ = df::historical_entity::find(site->civ_id); + if (civ) { TRANSLATE_NAME(civ_name,civ->name) } + + auto owner = df::historical_entity::find(site->cur_owner_id); + if (owner) { + TRANSLATE_NAME(site_government,owner->name) + auto race = df::creature_raw::find(owner->race); + if (!race){ + race = df::creature_raw::find(civ->race); + } + if (race) { + feature->SetField( "owner_race", race->name[2].c_str() ); + } + } + + + // this updates the feature with the id it receives in the layer + if( layer->CreateFeature( feature ) != OGRERR_NONE ) + return CR_FAILURE; + // but we don't care and destroy the feature + OGRFeature::DestroyFeature( feature ); + } + + dataset->CommitTransaction(); + + GDALClose( dataset ); + const auto finish{std::chrono::steady_clock::now()}; + const std::chrono::duration elapsed_seconds{finish - start}; + out.print("done in %f ms !\n", elapsed_seconds.count()); + return CR_OK; +} + +static command_result export_region_tiles(color_ostream &out) +{ + out.print("%lu / %d region map tiles loaded\n", + world->world_data->midmap_data.region_details.size(), + world->world_data->world_width * world->world_data->world_height + ); + out.print("exporting map... "); + out.flush(); + const auto start{std::chrono::steady_clock::now()}; + + // set up coordinate system + OGRSpatialReference CRS; + if (CRS.importFromProj4(EPSG_3857) != OGRERR_NONE) { + out.printerr("could not set up coordinate system"); + return CR_FAILURE; + } + + // set up output driver + GDALAllRegister(); + const char *driver_name = "Parquet"; + const char *extension = "parquet"; + auto driver = GetGDALDriverManager()->GetDriverByName(driver_name); + CHECK_NULL_POINTER(driver); + + // create a data set and associate it to a file + std::string map("map."); + map.append(extension); + auto dataset = driver->Create( map.c_str(), 0, 0, 0, GDT_Unknown, NULL ); + + // create a layer for the biome data + auto layer = dataset->CreateLayer( "world_biomes", &CRS, wkbPolygon, NULL ); + + try { + create_field(layer, "region_id", OFTInteger); + create_field(layer, "region_name_en", OFTString, 100); + create_field(layer, "region_name_df", OFTString, 100); + create_field(layer, "landmass_id", OFTInteger); + create_field(layer, "landmass_name_en", OFTString, 100); + create_field(layer, "landmass_name_df", OFTString, 100); + + create_field(layer, "biome_type", OFTString, 32); + create_field(layer, "surroundings", OFTString, 16); + create_field(layer, "elevation", OFTInteger); + + create_field(layer, "evilness", OFTInteger); + create_field(layer, "savagery", OFTInteger); + create_field(layer, "volcanism", OFTInteger); + create_field(layer, "drainage", OFTInteger); + create_field(layer, "temperature", OFTInteger); + create_field(layer, "vegetation", OFTInteger); + create_field(layer, "rainfall", OFTInteger); + create_field(layer, "snowfall", OFTInteger); + create_field(layer, "salinity", OFTInteger); + + create_field(layer, "reanimating", OFTInteger, 0, OFSTBoolean); + create_field(layer, "has_bogeymen", OFTInteger, 0, OFSTBoolean); + + } catch (const DFHack::command_result& r) { + out.printerr("could not create fields for output layer"); + return r; + } + + // iterating over the region details allows the user to do partial map exports + // by manually scrolling on the embark site selection + for (auto const region_details : world->world_data->midmap_data.region_details) { + auto world_x = region_details->pos.x; + auto world_y = region_details->pos.y; + for (int region_x = 0; region_x < 16; ++region_x) { + for (int region_y = 0; region_y < 16; ++region_y) { + + auto feature = OGRFeature::CreateFeature( layer->GetLayerDefn() ); + setGeometry( + feature, + (double)(world_x * wdim + region_x * rdim), + (double)(world_y * wdim + region_y * rdim), + rdim + ); + + // get information from the region details + auto [biome_x,biome_y] = get_world_index(world_x, world_y, region_details->biome[region_x][region_y]); + feature->SetField( "biome_type", ENUM_KEY_STR(biome_type,Maps::getBiomeType(biome_x,biome_y)).c_str() ); + feature->SetField( "elevation", region_details->elevation[region_x][region_y]); + + // gets supplementary information from the world tile + auto& region_map_entry = world->world_data->region_map[biome_x][biome_y]; + #define SET_FIELD(name) feature->SetField( #name, region_map_entry.name) + SET_FIELD(region_id); + SET_FIELD(landmass_id); + SET_FIELD(evilness); + SET_FIELD(savagery); + SET_FIELD(volcanism); + SET_FIELD(drainage); + SET_FIELD(temperature); + SET_FIELD(vegetation); + SET_FIELD(rainfall); + SET_FIELD(snowfall); + SET_FIELD(salinity); + #undef SET_FIELD + + feature->SetField( "surroundings", describe_surroundings(region_map_entry.savagery, region_map_entry.evilness)); + + auto region = df::world_region::find(region_map_entry.region_id); + if (region) { + auto region_name_en = DF2UTF(Translation::translateName(®ion->name, true)); + feature->SetField( "region_name_en", region_name_en.c_str()); + auto region_name_df = DF2UTF(Translation::translateName(®ion->name, false)); + feature->SetField( "region_name_df", region_name_df.c_str()); + feature->SetField("reanimating", region->reanimating); + feature->SetField("has_bogeymen", region->has_bogeymen); + } + auto landmass = df::world_landmass::find(region_map_entry.landmass_id); + if (landmass) { + auto landmass_name_en = DF2UTF(Translation::translateName(&landmass->name, true)); + feature->SetField( "landmass_name_en", landmass_name_en.c_str()); + auto landmass_name_df = DF2UTF(Translation::translateName(&landmass->name, false)); + feature->SetField( "landmass_name_df", landmass_name_df.c_str()); + } + + // this updates the feature with the id it receives in the layer + if( layer->CreateFeature( feature ) != OGRERR_NONE ) + return CR_FAILURE; + // but we don't care and destroy the feature + OGRFeature::DestroyFeature( feature ); + } + } + } + + GDALClose( dataset ); + const auto finish{std::chrono::steady_clock::now()}; + const std::chrono::duration elapsed_seconds{finish - start}; + out.print("done in %f ms !\n", elapsed_seconds.count()); + return CR_OK; +} diff --git a/plugins/lua/overlay.lua b/plugins/lua/overlay.lua index 77df7d47a4..c1d1bcb434 100644 --- a/plugins/lua/overlay.lua +++ b/plugins/lua/overlay.lua @@ -171,6 +171,9 @@ local function do_enable(args, quiet, skip_save) vs_name = normalize_viewscreen_name(vs_name) ensure_key(active_viewscreen_widgets, vs_name)[name] = db_entry end + if db_entry.widget.overlay_onenable then + db_entry.widget.overlay_onenable() + end if not quiet then print(('enabled widget %s'):format(name)) end @@ -202,6 +205,9 @@ local function do_disable(args, quiet) active_viewscreen_widgets[vs_name] = nil end end + if db_entry.widget.overlay_ondisable then + db_entry.widget.overlay_ondisable() + end if not quiet then print(('disabled widget %s'):format(name)) end @@ -532,16 +538,16 @@ function feed_viewscreen_widgets(vs_name, vs, keys) return true end -local function _render_viewscreen_widgets(vs_name, vs, full_dc, scaled_dc) +local function _render_viewscreen_widgets(vs_name, vs) local vs_widgets = active_viewscreen_widgets[vs_name] if not vs_widgets then return end local full, scaled = get_interface_rects() - full_dc = full_dc or gui.Painter.new(full) - scaled_dc = scaled_dc or gui.Painter.new(scaled) for _,db_entry in pairs(vs_widgets) do local w = db_entry.widget if (not vs or matches_focus_strings(db_entry, vs_name, vs)) and utils.getval(w.visible) then - detect_frame_change(w, function() w:render(w.fullscreen and full_dc or scaled_dc) end) + detect_frame_change(w, function() + w:render(w.fullscreen and gui.Painter.new(full) or gui.Painter.new(scaled)) + end) end end return full_dc, scaled_dc @@ -550,8 +556,8 @@ end local force_refresh function render_viewscreen_widgets(vs_name, vs) - local full_dc, scaled_dc = _render_viewscreen_widgets(vs_name, vs, nil, nil) - _render_viewscreen_widgets('all', nil, full_dc, scaled_dc) + _render_viewscreen_widgets(vs_name, vs) + _render_viewscreen_widgets('all', nil) if force_refresh then force_refresh = nil df.global.gps.force_full_display_count = 1 diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua index b3b81ef79a..0bf46cef2b 100644 --- a/plugins/lua/spectate.lua +++ b/plugins/lua/spectate.lua @@ -167,61 +167,64 @@ local function set_setting(args) if n == 0 then qerror('missing key') end - local key = table.remove(args, 1) - if config[key] == nil then - qerror('unknown setting: ' .. key) - end - n = #args - if n == 0 - or (n == 1 and type(config[key]) == 'table') - then - qerror('missing value') - end - if n == 1 then - local value = args[1] - if key == 'follow-seconds' then - value = argparse.positiveInt(value, 'follow-seconds') - elseif key == 'tooltip-follow-blink-milliseconds' then - value = argparse.nonnegativeInt(value, 'tooltip-follow-blink-milliseconds') + local cfg = config + local v + for i = 1, n do + v = cfg[args[i]] + if v == nil then + -- probably an unknown option, but we may allow adding new keys + break + elseif type(v) == 'table' then + if i == n then + -- arrived at the very last argument, but have a table + qerror('missing value for ' .. table.concat(args, '/', 1, i)) + end + cfg = v else - value = argparse.boolean(value, key) - end - - config[key] = value - - if not key:startswith(lua_only_settings_prefix) then - if type(value) == 'boolean' then - value = value and 1 or 0 + -- arrived at something that's not a table + if i == n-1 then + -- if there is exactly 1 argument left, we're good + break + elseif i == n then + qerror('missing value for ' .. table.concat(args, '/', 1, i)) + else -- i < n-1 then + qerror('too many arguments for ' .. table.concat(args, '/', 1, i)) end - spectate_setSetting(key, value) end - else - local errorUnknownSettingIfNil = function(t) - if t == nil then - table.remove(args) - qerror('unknown setting: ' .. key .. '/' .. table.concat(args, '/')) - end + end + if v == nil then + if n == 3 and args[1] == 'tooltip-follow-job-shortenings' then + -- user should be able to add new shortenings, but not other things + else + qerror('unknown option: ' .. table.concat(args, '/', 1, i)) end + end - local t = config[key] - for i = 1, n - 2 do - errorUnknownSettingIfNil(t) - t = t[args[i]] - end - local k = args[n-1] - local v = args[n] - if key ~= 'tooltip-follow-job-shortenings' then - -- user should be able to add new shortenings, but not other things - errorUnknownSettingIfNil(t[k]) - if key:endswith('-stress-levels') and key ~= 'tooltip-stress-levels' then - v = argparse.boolean(v, key .. '/' .. k) - end + local path = table.concat(args, '/', 1, n-1) + local key = args[n-1] + local value = args[n] + local entry_type = type(cfg[key]) + if entry_type == 'table' then + -- here just in case, is already checked in the loop above + qerror('missing value for ' .. path) + elseif entry_type == 'boolean' then + value = argparse.boolean(value, path) + elseif entry_type == 'number' then + if path == 'follow-seconds' then + value = argparse.positiveInt(value, path) + else + value = argparse.nonnegativeInt(value, path) end - if type(t[k]) == 'table' then - qerror('missing value') + end + + cfg[key] = value + + if n == 2 and not key:startswith(lua_only_settings_prefix) then + if type(value) == 'boolean' then + value = value and 1 or 0 end - t[k] = v + spectate_setSetting(key, value) end save_state() @@ -425,6 +428,8 @@ function TooltipOverlay:render_unit_banners(dc) local oneTileOffset = GetScreenCoordinates({x = topleft.x + 1, y = topleft.y + 1, z = topleft.z + 0}) local pen = COLOR_WHITE + local _, screenHeight = dfhack.screen:getWindowSize() + local used_tiles = {} -- reverse order yields better offsets for overlapping texts for i = #units, 1, -1 do @@ -444,6 +449,10 @@ function TooltipOverlay:render_unit_banners(dc) local y = scrPos.y - 1 -- subtract 1 to move the text over the heads local x = scrPos.x + oneTileOffset.x - 1 -- subtract 1 to move the text inside the map tile + -- do not write anything in the top rows, where DF's interface is. + -- todo: use precise rectangles + if y < 4 then goto continue end + -- to resolve overlaps, we'll mark every coordinate we write anything in, -- and then check if the new tooltip will overwrite any used coordinate. -- if it will, try the next row, to a maximum offset of 4. @@ -474,15 +483,19 @@ function TooltipOverlay:render_unit_banners(dc) -- we can't place any useful information, and will ignore it instead. if 0 <= usedAt and usedAt <= 2 then goto continue end - local writer = dc:seek(x, y + dy) + -- do not write anything over DF's interface + -- todo: use precise rectangles + if y + dy > screenHeight - 4 then goto continue end + + dc:seek(x, y + dy) local ix = 0 for _, tok in ipairs(info) do local s if type(tok) == "string" then - writer = writer:pen(pen) + dc:pen(pen) s = tok else - writer = writer:pen(tok.pen) + dc:pen(tok.pen) s = tok.text end @@ -493,10 +506,10 @@ function TooltipOverlay:render_unit_banners(dc) -- we want to replace it with an `_`, so we need another `- 1` s = s:sub(1, usedAt - len - ix - 1 - 1) .. '_' - writer = writer:string(s) + dc:string(s) break -- nothing more will fit else - writer = writer:string(s) + dc:string(s) end ix = ix + len @@ -601,7 +614,7 @@ function FollowPanelOverlay:init() on_click=spectate_followNext, }, widgets.Label{ - frame={l=10, t=0, w=25}, + frame={l=10, t=0, w=14}, text={ ' spectate:', {text=function() return isEnabled() and ' on ' or 'off ' end, diff --git a/plugins/stonesense b/plugins/stonesense index 7badeff963..8fdc2fdb49 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 7badeff963b0494e2e79e49a0dd3555961df5f04 +Subproject commit 8fdc2fdb4939fda074b50626381a43581b0b8479 diff --git a/scripts b/scripts index 557ac5732e..e927734288 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 557ac5732e87d62f3b7970295322ae0c5f7eae2c +Subproject commit e927734288e1676ccb6f25b96228719b79b48f13