diff --git a/build/.gitignore b/build/.gitignore index 3c85ab4df..f25d10ca7 100644 --- a/build/.gitignore +++ b/build/.gitignore @@ -6,3 +6,4 @@ DF_PATH.txt _CPack_Packages *.tar.* .cmake +win64-cross diff --git a/build/build-win64-from-linux.sh b/build/build-win64-from-linux.sh new file mode 100755 index 000000000..08ea88793 --- /dev/null +++ b/build/build-win64-from-linux.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +set -e +# Number of jobs == core count +jobs=$(grep -c ^processor /proc/cpuinfo) + +# Calculate absolute paths for docker to do mounts +srcdir=$(realpath "$(dirname "$(readlink -f "$0")")"/..) + +cd "$srcdir"/build + +builder_uid=$(id -u) + +mkdir -p win64-cross +mkdir -p win64-cross/output + +# Check for sudo; we want to use the real user +if [[ $(id -u) -eq 0 ]]; then + if [[ -z "$SUDO_UID" || "$SUDO_UID" -eq 0 ]]; then + echo "Please don't run this script directly as root, use sudo instead:" + echo + echo " sudo $0" + # This is because we can't change the buildmaster UID in the container to 0 -- + # that's already taken by root. + exit 1 + fi + + # If this was run using sudo, let's make sure the directories are owned by the + # real user (and set the BUILDER_UID to it) + builder_uid=$SUDO_UID + chown $builder_uid win64-cross win64-cross/output +fi + +# Assumes you built a container image called dfhack-build-msvc from +# https://github.com/BenLubar/build-env/tree/master/msvc, see +# docs/dev/compile/Compile.rst. +# +# NOTE: win64-cross is mounted in /src/build due to the hardcoded `cmake ..` in +# the Dockerfile +if ! docker run --rm -it -v "$srcdir":/src -v "$srcdir/build/win64-cross/":/src/build \ + -e BUILDER_UID=$builder_uid \ + --name dfhack-win \ + dfhack-build-msvc bash -c "cd /src/build && dfhack-configure windows 64 Release -DCMAKE_INSTALL_PREFIX=/src/build/output cmake .. -DBUILD_DOCS=1 && dfhack-make -j$jobs install" \ + ; then + echo + echo "Build failed" + exit 1 +else + echo + echo "Windows artifacts are at win64-cross/output. Copy or symlink them to" + echo "your steam DF directory to install dfhack (and optionally delete the" + echo "hack/ directory already present)" + echo + echo "Typically this can be done like this:" + echo " cp -r win64-cross/output/* ~/.local/share/Steam/steamapps/common/\"Dwarf Fortress\"" +fi diff --git a/docs/changelog.txt b/docs/changelog.txt index 94b7d61c9..f83892e69 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -38,6 +38,9 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## Fixes ## Misc Improvements +- A new cross-compile build script was added for building the Windows files from a Linux Docker builder (see the Compile instructions in the docs) +- `hotkeys`: clicking on the DFHack logo no longer closes the popup menu +- `orders`: orders plugin functionality is now offered via an overlay widget when the manager orders screen is open ## Documentation diff --git a/docs/dev/compile/Compile.rst b/docs/dev/compile/Compile.rst index 032eb7524..f91c43c07 100644 --- a/docs/dev/compile/Compile.rst +++ b/docs/dev/compile/Compile.rst @@ -284,8 +284,8 @@ addition to the normal ``CC`` and ``CXX`` flags above:: export PATH=/usr/local/bin:$PATH -Windows cross compiling from Linux -================================== +Windows cross compiling from Linux (running DF inside docker) +============================================================= .. highlight:: bash @@ -368,6 +368,60 @@ host when you want to reattach:: If you edit code and need to rebuild, run ``dfhack-make`` and then ``ninja install``. That will handle all the wineserver management for you. +Cross-compiling windows files for running DF in Steam for Linux +=============================================================== + +.. highlight:: bash + +If you wish, you can use Docker to build just the Windows files to copy to your +existing Steam installation on Linux. + +.. contents:: + :local: + :depth: 1 + +Step 1: Build the MSVC builder image +------------------------------------ + +It'll be called ``dfhack-build-msvc:latest`` after it's done building:: + + git clone https://github.com/BenLubar/build-env.git + cd build-env/msvc + docker build -t dfhack-build-msvc . + +The docker build takes a while, but only needs to be done once, unless the build +environment changes. + +Step 2: Get dfhack, and run the build script +-------------------------------------------- + +Check out ``dfhack`` into another directory, and run the build script:: + + git clone https://github.com/DFHack/dfhack.git + cd dfhack + git submodule update --init --recursive + cd build + ./build-win64-from-linux.sh + +The script will mount your host's ``dfhack`` directory to docker, use it to +build the artifacts in ``build/win64-cross``, and put all the files needed to +install in ``build/win64-cross/output``. + +If you need to run ``docker`` using ``sudo``, run the script using ``sudo`` +rather than directly:: + + sudo ./build-win64-from-linux.sh + +Step 3: install dfhack to your Steam DF install +----------------------------------------------- +As the script will tell you, you can then copy the files into your DF folder:: + + # Optional -- remove the old hack directory in case we leave files behind + rm ~/.local/share/Steam/steamapps/common/"Dwarf Fortress"/hack + cp -r win64-cross/output/* ~/.local/share/Steam/steamapps/common/"Dwarf Fortress"/ + +Afterward, just run DF as normal. + .. _note-offline-builds: Building DFHack Offline diff --git a/docs/dev/overlay-dev-guide.rst b/docs/dev/overlay-dev-guide.rst index bce623180..50998530a 100644 --- a/docs/dev/overlay-dev-guide.rst +++ b/docs/dev/overlay-dev-guide.rst @@ -125,10 +125,6 @@ The ``overlay.OverlayWidget`` superclass defines the following class attributes: not annoy the player. Set to 0 to be called at the maximum rate. Be aware that running more often than you really need to will impact game FPS, especially if your widget can run while the game is unpaused. -- ``always_enabled`` (default: ``false``) - Set this to ``true`` if you don't want to let the user disable the widget. - This is useful for widgets that are controlled purely through their - triggers. See `gui/pathable` for an example. Registering a widget with the overlay framework *********************************************** diff --git a/docs/plugins/orders.rst b/docs/plugins/orders.rst index 8beb246e4..5dfe23758 100644 --- a/docs/plugins/orders.rst +++ b/docs/plugins/orders.rst @@ -40,6 +40,17 @@ Examples Import manager orders from the library that keep your fort stocked with basic essentials. +Overlay +------- + +Orders plugin functionality is directly available when the manager orders screen +is open via an `overlay` widget. There are hotkeys assigned to export, import, +sort, and clear. You can also click on the hotkey hints as if they were buttons. +Clearing will ask for confirmation before acting. + +If you want to change where the hotkey hints appear, you can move them via +`gui/overlay`. + The orders library ------------------ diff --git a/library/include/MiscUtils.h b/library/include/MiscUtils.h index 124987535..c9a5f66d6 100644 --- a/library/include/MiscUtils.h +++ b/library/include/MiscUtils.h @@ -167,8 +167,8 @@ int linear_index(const std::vector &vec, FT CT::*field, FT key) return -1; } -template -int binsearch_index(const std::vector &vec, FT CT::*field, FT key, bool exact = true) +template +int binsearch_index(const std::vector &vec, FT MT::*field, FT key, bool exact = true) { // Returns the index of the value >= the key int min = -1, max = (int)vec.size(); @@ -245,8 +245,8 @@ unsigned insert_into_vector(std::vector &vec, FT key, bool *inserted = NULL) return pos; } -template -unsigned insert_into_vector(std::vector &vec, FT CT::*field, CT *obj, bool *inserted = NULL) +template +unsigned insert_into_vector(std::vector &vec, FT MT::*field, CT *obj, bool *inserted = NULL) { unsigned pos = (unsigned)binsearch_index(vec, field, obj->*field, false); bool to_ins = (pos >= vec.size() || vec[pos] != obj); diff --git a/library/modules/Buildings.cpp b/library/modules/Buildings.cpp index 653f8bccf..5077b5b52 100644 --- a/library/modules/Buildings.cpp +++ b/library/modules/Buildings.cpp @@ -82,6 +82,7 @@ using namespace DFHack; #include "df/map_block.h" #include "df/tile_occupancy.h" #include "df/plotinfost.h" +#include "df/squad.h" #include "df/ui_look_list.h" #include "df/unit.h" #include "df/unit_relationship_type.h" @@ -168,34 +169,24 @@ void buildings_onUpdate(color_ostream &out) static void building_into_zone_unidir(df::building* bld, df::building_civzonest* zone) { - for (size_t bid = 0; bid < zone->contained_buildings.size(); bid++) + for (auto contained_building : zone->contained_buildings) { - if (zone->contained_buildings[bid] == bld) + if (contained_building == bld) return; } - zone->contained_buildings.push_back(bld); - - std::sort(zone->contained_buildings.begin(), zone->contained_buildings.end(), [](df::building* b1, df::building* b2) - { - return b1->id < b2->id; - }); + insert_into_vector(zone->contained_buildings, &df::building::id, bld); } static void zone_into_building_unidir(df::building* bld, df::building_civzonest* zone) { - for (size_t bid = 0; bid < bld->relations.size(); bid++) + for (auto relation : bld->relations) { - if (bld->relations[bid] == zone) + if (relation == zone) return; } - bld->relations.push_back(zone); - - std::sort(bld->relations.begin(), bld->relations.end(), [](df::building* b1, df::building* b2) - { - return b1->id < b2->id; - }); + insert_into_vector(bld->relations, &df::building_civzonest::id, zone); } static bool is_suitable_building_for_zoning(df::building* bld) @@ -222,10 +213,8 @@ static void add_building_to_all_zones(df::building* bld) std::vector cv; Buildings::findCivzonesAt(&cv, coord); - for (size_t i=0; i < cv.size(); i++) - { - add_building_to_zone(bld, cv[i]); - } + for (auto zone : cv) + add_building_to_zone(bld, zone); } static void add_zone_to_all_buildings(df::building* zone_as_building) @@ -238,20 +227,16 @@ static void add_zone_to_all_buildings(df::building* zone_as_building) if (zone == nullptr) return; - auto& vec = world->buildings.other[buildings_other_id::IN_PLAY]; - - for (size_t i = 0; i < vec.size(); i++) + for (auto bld : world->buildings.other.IN_PLAY) { - auto against = vec[i]; - - if (zone->z != against->z) + if (zone->z != bld->z) continue; - if (!is_suitable_building_for_zoning(against)) + if (!is_suitable_building_for_zoning(bld)) continue; - int32_t cx = against->centerx; - int32_t cy = against->centery; + int32_t cx = bld->centerx; + int32_t cy = bld->centery; df::coord2d coord(cx, cy); @@ -262,7 +247,7 @@ static void add_zone_to_all_buildings(df::building* zone_as_building) if (!etile || !*etile) continue; - add_building_to_zone(against, zone); + add_building_to_zone(bld, zone); } } } @@ -295,10 +280,8 @@ static void remove_building_from_all_zones(df::building* bld) std::vector cv; Buildings::findCivzonesAt(&cv, coord); - for (size_t i=0; i < cv.size(); i++) - { - remove_building_from_zone(bld, cv[i]); - } + for (auto zone : cv) + remove_building_from_zone(bld, zone); } static void remove_zone_from_all_buildings(df::building* zone_as_building) @@ -311,16 +294,10 @@ static void remove_zone_from_all_buildings(df::building* zone_as_building) if (zone == nullptr) return; - auto& vec = world->buildings.other[buildings_other_id::IN_PLAY]; - //this is a paranoid check and slower than it could be. Zones contain a list of children //good for fixing potentially bad game states when deleting a zone - for (size_t i = 0; i < vec.size(); i++) - { - df::building* bld = vec[i]; - + for (auto bld : world->buildings.other.IN_PLAY) remove_building_from_zone(bld, zone); - } } uint32_t Buildings::getNumBuildings() @@ -1348,6 +1325,62 @@ bool Buildings::constructWithFilters(df::building *bld, std::vectorsquad_room_info) + { + int32_t squad_id = room_info->squad_id; + + df::squad* squad = df::squad::find(squad_id); + + //if this is null, something has gone just *terribly* wrong + if (squad) + { + for (int i=(int)squad->rooms.size() - 1; i >= 0; i--) + { + if (squad->rooms[i]->building_id == zone->id) + { + auto room = squad->rooms[i]; + squad->rooms.erase(squad->rooms.begin() + i); + delete room; + } + } + } + + delete room_info; + } + + zone->squad_room_info.clear(); +} + +//unit owned_building pointers are known-bad as of 50.05 and dangle on zone delete +//do not use anything that touches anything other than the pointer value +//this means also that if dwarf fortress reuses a memory allocation, we will end up with duplicates +//this vector is also not sorted by id +//it also turns out that multiple units eg (solely?) spouses can point to one room +static void delete_assigned_unit_links(df::building_civzonest* zone) +{ + //not clear if this is always true + /*if (zone->assigned_unit_id == -1) + return;*/ + + for (df::unit* unit : world->units.active) + { + for (int i=(int)unit->owned_buildings.size() - 1; i >= 0; i--) + { + if (unit->owned_buildings[i] == zone) + unit->owned_buildings.erase(unit->owned_buildings.begin() + i); + } + } +} + +static void on_civzone_delete(df::building_civzonest* civzone) +{ + remove_zone_from_all_buildings(civzone); + delete_civzone_squad_links(civzone); + delete_assigned_unit_links(civzone); +} + bool Buildings::deconstruct(df::building *bld) { using df::global::plotinfo; @@ -1386,7 +1419,14 @@ bool Buildings::deconstruct(df::building *bld) bld->uncategorize(); remove_building_from_all_zones(bld); - remove_zone_from_all_buildings(bld); + + if (bld->getType() == df::building_type::Civzone) + { + auto zone = strict_virtual_cast(bld); + + if (zone) + on_civzone_delete(zone); + } delete bld; diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index e92bfaac7..95b1d9ba8 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -134,7 +134,7 @@ dfhack_plugin(misery misery.cpp) #dfhack_plugin(mode mode.cpp) #dfhack_plugin(mousequery mousequery.cpp) dfhack_plugin(nestboxes nestboxes.cpp) -dfhack_plugin(orders orders.cpp LINK_LIBRARIES jsoncpp_static) +dfhack_plugin(orders orders.cpp LINK_LIBRARIES jsoncpp_static lua) dfhack_plugin(overlay overlay.cpp LINK_LIBRARIES lua) dfhack_plugin(pathable pathable.cpp LINK_LIBRARIES lua) #dfhack_plugin(petcapRemover petcapRemover.cpp) diff --git a/plugins/lua/hotkeys.lua b/plugins/lua/hotkeys.lua index 2ec38fff6..a6eaf7d81 100644 --- a/plugins/lua/hotkeys.lua +++ b/plugins/lua/hotkeys.lua @@ -32,7 +32,7 @@ function HotspotMenuWidget:overlay_onupdate() end function HotspotMenuWidget:overlay_trigger() - return MenuScreen{hotspot_frame=self.frame}:show() + return MenuScreen{hotspot=self}:show() end local dscreen = dfhack.screen @@ -74,9 +74,9 @@ local ARROW = string.char(26) local MAX_LIST_WIDTH = 45 local MAX_LIST_HEIGHT = 15 -Menu = defclass(MenuScreen, widgets.Panel) +Menu = defclass(Menu, widgets.Panel) Menu.ATTRS{ - hotspot_frame=DEFAULT_NIL, + hotspot=DEFAULT_NIL, } -- get a map from the binding string to a list of hotkey strings that all @@ -136,10 +136,10 @@ end function Menu:init() local hotkeys, bindings = getHotkeys() - local is_inverted = not not self.hotspot_frame.b + local is_inverted = not not self.hotspot.frame.b local choices,list_width = get_choices(hotkeys, bindings, is_inverted) - local list_frame = copyall(self.hotspot_frame) + local list_frame = copyall(self.hotspot.frame) local list_widget_frame = {h=math.min(#choices, MAX_LIST_HEIGHT)} local quickstart_frame = {} list_frame.w = list_width + 2 @@ -248,7 +248,7 @@ function Menu:onInput(keys) self:onSubmit2(list:getSelected()) return true end - if not self:getMouseFramePos() then + if not self:getMouseFramePos() and not self.hotspot:getMousePos() then self.parent_view:dismiss() return true end @@ -297,12 +297,12 @@ end MenuScreen = defclass(MenuScreen, gui.ZScreen) MenuScreen.ATTRS { focus_path='hotkeys/menu', - hotspot_frame=DEFAULT_NIL, + hotspot=DEFAULT_NIL, } function MenuScreen:init() self:addviews{ - Menu{hotspot_frame=self.hotspot_frame}, + Menu{hotspot=self.hotspot}, } end diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua new file mode 100644 index 000000000..aecd940d6 --- /dev/null +++ b/plugins/lua/orders.lua @@ -0,0 +1,100 @@ +local _ENV = mkmodule('plugins.orders') + +local dialogs = require('gui.dialogs') +local gui = require('gui') +local overlay = require('plugins.overlay') +local widgets = require('gui.widgets') + +-- +-- OrdersOverlay +-- + +local function is_orders_panel_visible() + local info = df.global.game.main_interface.info + return info.open and info.current_mode == df.info_interface_mode_type.WORK_ORDERS +end + +local function do_sort() + dfhack.run_command('orders', 'sort') +end + +local function do_clear() + dialogs.showYesNoPrompt('Clear manager orders?', + 'Are you sure you want to clear the manager orders?', nil, + function() dfhack.run_command('orders', 'clear') end) +end + +local function do_import() + local output = dfhack.run_command_silent('orders', 'list') + dialogs.ListBox{ + frame_title='Import Manager Orders', + with_filter=true, + choices=output:split('\n'), + on_select=function(idx, choice) + dfhack.run_command('orders', 'import', choice.text) + end, + }:show() +end + +local function do_export() + dialogs.InputBox{ + frame_title='Export Manager Orders', + on_input=function(text) + dfhack.run_command('orders', 'export', text) + end + }:show() +end + +OrdersOverlay = defclass(OrdersOverlay, overlay.OverlayWidget) +OrdersOverlay.ATTRS{ + default_pos={x=61,y=-6}, + viewscreens='dwarfmode', + frame={w=30, h=4}, + frame_style=gui.GREY_LINE_FRAME, + frame_background=gui.CLEAR_PEN, +} + +function OrdersOverlay:init() + self:addviews{ + widgets.HotkeyLabel{ + frame={t=0, l=0}, + label='import', + key='CUSTOM_CTRL_I', + on_activate=do_import, + }, + widgets.HotkeyLabel{ + frame={t=1, l=0}, + label='export', + key='CUSTOM_CTRL_E', + on_activate=do_export, + }, + widgets.HotkeyLabel{ + frame={t=0, l=15}, + label='sort', + key='CUSTOM_CTRL_O', + on_activate=do_sort, + }, + widgets.HotkeyLabel{ + frame={t=1, l=15}, + label='clear', + key='CUSTOM_CTRL_C', + on_activate=do_clear, + }, + } +end + +function OrdersOverlay:render(dc) + if not is_orders_panel_visible() then return false end + OrdersOverlay.super.render(self, dc) +end + +function OrdersOverlay:onInput(keys) + if not is_orders_panel_visible() then return false end + OrdersOverlay.super.onInput(self, keys) +end + +OVERLAY_WIDGETS = { + overlay=OrdersOverlay, +} + +return _ENV diff --git a/plugins/lua/overlay.lua b/plugins/lua/overlay.lua index cf599fd08..b90c1c651 100644 --- a/plugins/lua/overlay.lua +++ b/plugins/lua/overlay.lua @@ -184,7 +184,6 @@ end local function do_disable(args, quiet) local disable_fn = function(name, db_entry) - if db_entry.widget.always_enabled then return end overlay_config[name].enabled = false if db_entry.widget.hotspot then active_hotspot_widgets[name] = nil @@ -252,7 +251,7 @@ local function load_widget(name, widget_class) local config = overlay_config[name] config.pos = sanitize_pos(config.pos or widget.default_pos) widget.frame = make_frame(config.pos, widget.frame) - if config.enabled or widget.always_enabled then + if config.enabled then do_enable(name, true, true) else config.enabled = false @@ -492,7 +491,6 @@ OverlayWidget.ATTRS{ hotspot=false, -- whether to call overlay_onupdate on all screens viewscreens={}, -- override with associated viewscreen or list of viewscrens overlay_onupdate_max_freq_seconds=5, -- throttle calls to overlay_onupdate - always_enabled=false, -- for overlays that should never be disabled } function OverlayWidget:init()