Merge branch 'develop' into automelt

develop
eamondo2 2023-01-22 20:32:43 -05:00
commit 8f7788ec20
12 changed files with 323 additions and 64 deletions

1
build/.gitignore vendored

@ -6,3 +6,4 @@ DF_PATH.txt
_CPack_Packages _CPack_Packages
*.tar.* *.tar.*
.cmake .cmake
win64-cross

@ -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

@ -38,6 +38,9 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
## Fixes ## Fixes
## Misc Improvements ## 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 ## Documentation

@ -284,8 +284,8 @@ addition to the normal ``CC`` and ``CXX`` flags above::
export PATH=/usr/local/bin:$PATH export PATH=/usr/local/bin:$PATH
Windows cross compiling from Linux Windows cross compiling from Linux (running DF inside docker)
================================== =============================================================
.. highlight:: bash .. 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``. If you edit code and need to rebuild, run ``dfhack-make`` and then ``ninja install``.
That will handle all the wineserver management for you. 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: .. _note-offline-builds:
Building DFHack Offline Building DFHack Offline

@ -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 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, that running more often than you really need to will impact game FPS,
especially if your widget can run while the game is unpaused. 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 Registering a widget with the overlay framework
*********************************************** ***********************************************

@ -40,6 +40,17 @@ Examples
Import manager orders from the library that keep your fort stocked with Import manager orders from the library that keep your fort stocked with
basic essentials. 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 The orders library
------------------ ------------------

@ -167,8 +167,8 @@ int linear_index(const std::vector<CT*> &vec, FT CT::*field, FT key)
return -1; return -1;
} }
template <typename CT, typename FT> template <typename CT, typename FT, typename MT>
int binsearch_index(const std::vector<CT*> &vec, FT CT::*field, FT key, bool exact = true) int binsearch_index(const std::vector<CT*> &vec, FT MT::*field, FT key, bool exact = true)
{ {
// Returns the index of the value >= the key // Returns the index of the value >= the key
int min = -1, max = (int)vec.size(); int min = -1, max = (int)vec.size();
@ -245,8 +245,8 @@ unsigned insert_into_vector(std::vector<FT> &vec, FT key, bool *inserted = NULL)
return pos; return pos;
} }
template<typename CT, typename FT> template<typename CT, typename FT, typename MT>
unsigned insert_into_vector(std::vector<CT*> &vec, FT CT::*field, CT *obj, bool *inserted = NULL) unsigned insert_into_vector(std::vector<CT*> &vec, FT MT::*field, CT *obj, bool *inserted = NULL)
{ {
unsigned pos = (unsigned)binsearch_index(vec, field, obj->*field, false); unsigned pos = (unsigned)binsearch_index(vec, field, obj->*field, false);
bool to_ins = (pos >= vec.size() || vec[pos] != obj); bool to_ins = (pos >= vec.size() || vec[pos] != obj);

@ -82,6 +82,7 @@ using namespace DFHack;
#include "df/map_block.h" #include "df/map_block.h"
#include "df/tile_occupancy.h" #include "df/tile_occupancy.h"
#include "df/plotinfost.h" #include "df/plotinfost.h"
#include "df/squad.h"
#include "df/ui_look_list.h" #include "df/ui_look_list.h"
#include "df/unit.h" #include "df/unit.h"
#include "df/unit_relationship_type.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) 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; return;
} }
zone->contained_buildings.push_back(bld); insert_into_vector(zone->contained_buildings, &df::building::id, bld);
std::sort(zone->contained_buildings.begin(), zone->contained_buildings.end(), [](df::building* b1, df::building* b2)
{
return b1->id < b2->id;
});
} }
static void zone_into_building_unidir(df::building* bld, df::building_civzonest* zone) 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; return;
} }
bld->relations.push_back(zone); insert_into_vector(bld->relations, &df::building_civzonest::id, zone);
std::sort(bld->relations.begin(), bld->relations.end(), [](df::building* b1, df::building* b2)
{
return b1->id < b2->id;
});
} }
static bool is_suitable_building_for_zoning(df::building* bld) 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<df::building_civzonest*> cv; std::vector<df::building_civzonest*> cv;
Buildings::findCivzonesAt(&cv, coord); Buildings::findCivzonesAt(&cv, coord);
for (size_t i=0; i < cv.size(); i++) for (auto zone : cv)
{ add_building_to_zone(bld, zone);
add_building_to_zone(bld, cv[i]);
}
} }
static void add_zone_to_all_buildings(df::building* zone_as_building) 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) if (zone == nullptr)
return; return;
auto& vec = world->buildings.other[buildings_other_id::IN_PLAY]; for (auto bld : world->buildings.other.IN_PLAY)
for (size_t i = 0; i < vec.size(); i++)
{ {
auto against = vec[i]; if (zone->z != bld->z)
if (zone->z != against->z)
continue; continue;
if (!is_suitable_building_for_zoning(against)) if (!is_suitable_building_for_zoning(bld))
continue; continue;
int32_t cx = against->centerx; int32_t cx = bld->centerx;
int32_t cy = against->centery; int32_t cy = bld->centery;
df::coord2d coord(cx, cy); df::coord2d coord(cx, cy);
@ -262,7 +247,7 @@ static void add_zone_to_all_buildings(df::building* zone_as_building)
if (!etile || !*etile) if (!etile || !*etile)
continue; 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<df::building_civzonest*> cv; std::vector<df::building_civzonest*> cv;
Buildings::findCivzonesAt(&cv, coord); Buildings::findCivzonesAt(&cv, coord);
for (size_t i=0; i < cv.size(); i++) for (auto zone : cv)
{ remove_building_from_zone(bld, zone);
remove_building_from_zone(bld, cv[i]);
}
} }
static void remove_zone_from_all_buildings(df::building* zone_as_building) 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) if (zone == nullptr)
return; 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 //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 //good for fixing potentially bad game states when deleting a zone
for (size_t i = 0; i < vec.size(); i++) for (auto bld : world->buildings.other.IN_PLAY)
{
df::building* bld = vec[i];
remove_building_from_zone(bld, zone); remove_building_from_zone(bld, zone);
}
} }
uint32_t Buildings::getNumBuildings() uint32_t Buildings::getNumBuildings()
@ -1348,6 +1325,62 @@ bool Buildings::constructWithFilters(df::building *bld, std::vector<df::job_item
return true; return true;
} }
static void delete_civzone_squad_links(df::building_civzonest* zone)
{
for (df::building_civzonest::T_squad_room_info* room_info : zone->squad_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) bool Buildings::deconstruct(df::building *bld)
{ {
using df::global::plotinfo; using df::global::plotinfo;
@ -1386,7 +1419,14 @@ bool Buildings::deconstruct(df::building *bld)
bld->uncategorize(); bld->uncategorize();
remove_building_from_all_zones(bld); remove_building_from_all_zones(bld);
remove_zone_from_all_buildings(bld);
if (bld->getType() == df::building_type::Civzone)
{
auto zone = strict_virtual_cast<df::building_civzonest>(bld);
if (zone)
on_civzone_delete(zone);
}
delete bld; delete bld;

@ -134,7 +134,7 @@ dfhack_plugin(misery misery.cpp)
#dfhack_plugin(mode mode.cpp) #dfhack_plugin(mode mode.cpp)
#dfhack_plugin(mousequery mousequery.cpp) #dfhack_plugin(mousequery mousequery.cpp)
dfhack_plugin(nestboxes nestboxes.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(overlay overlay.cpp LINK_LIBRARIES lua)
dfhack_plugin(pathable pathable.cpp LINK_LIBRARIES lua) dfhack_plugin(pathable pathable.cpp LINK_LIBRARIES lua)
#dfhack_plugin(petcapRemover petcapRemover.cpp) #dfhack_plugin(petcapRemover petcapRemover.cpp)

@ -32,7 +32,7 @@ function HotspotMenuWidget:overlay_onupdate()
end end
function HotspotMenuWidget:overlay_trigger() function HotspotMenuWidget:overlay_trigger()
return MenuScreen{hotspot_frame=self.frame}:show() return MenuScreen{hotspot=self}:show()
end end
local dscreen = dfhack.screen local dscreen = dfhack.screen
@ -74,9 +74,9 @@ local ARROW = string.char(26)
local MAX_LIST_WIDTH = 45 local MAX_LIST_WIDTH = 45
local MAX_LIST_HEIGHT = 15 local MAX_LIST_HEIGHT = 15
Menu = defclass(MenuScreen, widgets.Panel) Menu = defclass(Menu, widgets.Panel)
Menu.ATTRS{ Menu.ATTRS{
hotspot_frame=DEFAULT_NIL, hotspot=DEFAULT_NIL,
} }
-- get a map from the binding string to a list of hotkey strings that all -- get a map from the binding string to a list of hotkey strings that all
@ -136,10 +136,10 @@ end
function Menu:init() function Menu:init()
local hotkeys, bindings = getHotkeys() 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 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 list_widget_frame = {h=math.min(#choices, MAX_LIST_HEIGHT)}
local quickstart_frame = {} local quickstart_frame = {}
list_frame.w = list_width + 2 list_frame.w = list_width + 2
@ -248,7 +248,7 @@ function Menu:onInput(keys)
self:onSubmit2(list:getSelected()) self:onSubmit2(list:getSelected())
return true return true
end end
if not self:getMouseFramePos() then if not self:getMouseFramePos() and not self.hotspot:getMousePos() then
self.parent_view:dismiss() self.parent_view:dismiss()
return true return true
end end
@ -297,12 +297,12 @@ end
MenuScreen = defclass(MenuScreen, gui.ZScreen) MenuScreen = defclass(MenuScreen, gui.ZScreen)
MenuScreen.ATTRS { MenuScreen.ATTRS {
focus_path='hotkeys/menu', focus_path='hotkeys/menu',
hotspot_frame=DEFAULT_NIL, hotspot=DEFAULT_NIL,
} }
function MenuScreen:init() function MenuScreen:init()
self:addviews{ self:addviews{
Menu{hotspot_frame=self.hotspot_frame}, Menu{hotspot=self.hotspot},
} }
end end

@ -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

@ -184,7 +184,6 @@ end
local function do_disable(args, quiet) local function do_disable(args, quiet)
local disable_fn = function(name, db_entry) local disable_fn = function(name, db_entry)
if db_entry.widget.always_enabled then return end
overlay_config[name].enabled = false overlay_config[name].enabled = false
if db_entry.widget.hotspot then if db_entry.widget.hotspot then
active_hotspot_widgets[name] = nil active_hotspot_widgets[name] = nil
@ -252,7 +251,7 @@ local function load_widget(name, widget_class)
local config = overlay_config[name] local config = overlay_config[name]
config.pos = sanitize_pos(config.pos or widget.default_pos) config.pos = sanitize_pos(config.pos or widget.default_pos)
widget.frame = make_frame(config.pos, widget.frame) 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) do_enable(name, true, true)
else else
config.enabled = false config.enabled = false
@ -492,7 +491,6 @@ OverlayWidget.ATTRS{
hotspot=false, -- whether to call overlay_onupdate on all screens hotspot=false, -- whether to call overlay_onupdate on all screens
viewscreens={}, -- override with associated viewscreen or list of viewscrens viewscreens={}, -- override with associated viewscreen or list of viewscrens
overlay_onupdate_max_freq_seconds=5, -- throttle calls to overlay_onupdate overlay_onupdate_max_freq_seconds=5, -- throttle calls to overlay_onupdate
always_enabled=false, -- for overlays that should never be disabled
} }
function OverlayWidget:init() function OverlayWidget:init()