Merge branch 'develop' into ccompile-script

develop
Myk 2023-01-22 17:16:19 -08:00 committed by GitHub
commit f805f5b67e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 209 additions and 62 deletions

@ -39,6 +39,8 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
## 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) - 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

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