Merge remote-tracking branch 'cppcooper/unit-testing' into myk_ctest
commit
f921b02a1b
@ -1,5 +1,5 @@
|
||||
This folder contains blueprints that can be applied by the `quickfort` script. For more information, see:
|
||||
|
||||
* [Quickfort command reference](https://docs.dfhack.org/en/stable/docs/_auto/base.html#quickfort)
|
||||
* [Quickfort command reference](https://docs.dfhack.org/en/stable/docs/tools/quickfort.html)
|
||||
* [Quickfort blueprint guide](https://docs.dfhack.org/en/stable/docs/guides/quickfort-user-guide.html)
|
||||
* [Quickfort library guide](https://docs.dfhack.org/en/stable/docs/guides/quickfort-library-guide.html)
|
||||
|
Can't render this file because it has a wrong number of fields in line 56.
|
@ -0,0 +1 @@
|
||||
Subproject commit 2fe3bd994b3189899d93f1d5a881e725e046fdc2
|
@ -1,21 +1,3 @@
|
||||
{
|
||||
"widgets": [
|
||||
{
|
||||
"type": "weather",
|
||||
"x": 22,
|
||||
"y": -1
|
||||
},
|
||||
{
|
||||
"type": "date",
|
||||
"x": -30,
|
||||
"y": 0,
|
||||
"format": "Y-M-D"
|
||||
},
|
||||
{
|
||||
"type": "misery",
|
||||
"x": -2,
|
||||
"y": -1,
|
||||
"anchor": "right"
|
||||
}
|
||||
]
|
||||
"date_format": "Y-M-D"
|
||||
}
|
||||
|
@ -0,0 +1,17 @@
|
||||
{
|
||||
"dwarfmonitor.date": {
|
||||
"enabled": true
|
||||
},
|
||||
"dwarfmonitor.misery": {
|
||||
"enabled": true
|
||||
},
|
||||
"dwarfmonitor.weather": {
|
||||
"enabled": true
|
||||
},
|
||||
"hotkeys.menu": {
|
||||
"enabled": true
|
||||
},
|
||||
"unsuspend.overlay": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
@ -0,0 +1,302 @@
|
||||
.. _overlay-dev-guide:
|
||||
|
||||
DFHack overlay dev guide
|
||||
=========================
|
||||
|
||||
.. highlight:: lua
|
||||
|
||||
This guide walks you through how to build overlay widgets and register them with
|
||||
the `overlay` framework for injection into Dwarf Fortress viewscreens.
|
||||
|
||||
Why would I want to create an overlay widget?
|
||||
---------------------------------------------
|
||||
|
||||
There are both C++ and Lua APIs for creating viewscreens and drawing to the
|
||||
screen. If you need very specific low-level control, those APIs might be the
|
||||
right choice for you. However, here are some reasons you might want to implement
|
||||
an overlay widget instead:
|
||||
|
||||
1. You can draw directly to an existing viewscreen instead of creating an
|
||||
entirely new screen on the viewscreen stack. This allows the original
|
||||
viewscreen to continue processing uninterrupted and keybindings bound to
|
||||
that viewscreen will continue to function. This was previously only
|
||||
achievable by C++ plugins.
|
||||
|
||||
1. You'll get a free UI for enabling/disabling your widget and repositioning it
|
||||
on the screen. Widget state is saved for you and is automatically restored
|
||||
when the game is restarted.
|
||||
|
||||
1. You don't have to manage the C++ interposing logic yourself and can focus on
|
||||
the business logic, writing purely in Lua if desired.
|
||||
|
||||
In general, if you are writing a plugin or script and have anything you'd like
|
||||
to add to an existing screen (including live updates of map tiles while the game
|
||||
is unpaused), an overlay widget is probably your easiest path to get it done. If
|
||||
your plugin or script doesn't otherwise need to be enabled to function, using
|
||||
the overlay allows you to avoid writing any of the enable management code that
|
||||
would normally be required for you to show info in the UI.
|
||||
|
||||
Overlay widget API
|
||||
------------------
|
||||
|
||||
Overlay widgets are Lua classes that inherit from ``overlay.OverlayWidget``
|
||||
(which itself inherits from `widgets.Widget <widget>`). The regular
|
||||
``onInput(keys)``, ``onRenderFrame(dc, frame_rect)``, and ``onRenderBody(dc)``
|
||||
functions work as normal, and they are called when the viewscreen that the
|
||||
widget is associated with does its usual input and render processing. The widget
|
||||
gets first dibs on input processing. If a widget returns ``true`` from its
|
||||
``onInput()`` function, the viewscreen will not receive the input.
|
||||
|
||||
Overlay widgets can contain other Widgets and be as simple or complex as you
|
||||
need them to be, just like you're building a regular UI element.
|
||||
|
||||
There are a few extra capabilities that overlay widgets have that take them
|
||||
beyond your everyday ``Widget``:
|
||||
|
||||
- If an ``overlay_onupdate(viewscreen)`` function is defined, it will be called
|
||||
just after the associated viewscreen's ``logic()`` function is called (i.e.
|
||||
a "tick" or a (non-graphical) "frame"). For hotspot widgets, this function
|
||||
will also get called after the top viewscreen's ``logic()`` function is
|
||||
called, regardless of whether the widget is associated with that viewscreen.
|
||||
If this function returns ``true``, then the widget's ``overlay_trigger()``
|
||||
function is immediately called. Note that the ``viewscreen`` parameter will
|
||||
be ``nil`` for hotspot widgets that are not also associated with the current
|
||||
viewscreen.
|
||||
- If an ``overlay_trigger()`` function is defined, will be called when the
|
||||
widget's ``overlay_onupdate`` callback returns true or when the player uses
|
||||
the CLI (or a keybinding calling the CLI) to trigger the widget. The
|
||||
function must return either ``nil`` or the ``gui.Screen`` object that the
|
||||
widget code has allocated, shown, and now owns. Hotspot widgets will receive
|
||||
no callbacks from unassociated viewscreens until the returned screen is
|
||||
dismissed. Unbound hotspot widgets **must** allocate a Screen with this
|
||||
function if they want to react to the ``onInput()`` feed or be rendered. The
|
||||
widgets owned by the overlay framework must not be attached to that new
|
||||
screen, but the returned screen can instantiate and configure any new views
|
||||
that it wants to.
|
||||
|
||||
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
|
||||
modify ``self.frame.w`` and ``self.frame.h`` at any time -- in ``init()`` or in
|
||||
any of the callbacks -- to indicate a new size. The overlay framework will
|
||||
detect the size change and adjust the widget position and layout.
|
||||
|
||||
If you don't need to dynamically resize, just set ``self.frame.w`` and
|
||||
``self.frame.h`` once in ``init()``.
|
||||
|
||||
Widget attributes
|
||||
*****************
|
||||
|
||||
The ``overlay.OverlayWidget`` superclass defines the following class attributes:
|
||||
|
||||
- ``name``
|
||||
This will be filled in with the display name of your widget, in case you
|
||||
have multiple widgets with the same implementation but different
|
||||
configurations.
|
||||
- ``default_pos`` (default: ``{x=-2, y=-2}``)
|
||||
Override this attribute with your desired default widget position. See
|
||||
the `overlay` docs for information on what positive and negative numbers
|
||||
mean for the position. Players can change the widget position at any time
|
||||
via the `overlay position <overlay>` command, so don't assume that your
|
||||
widget will always be at the default position.
|
||||
- ``viewscreens`` (default: ``{}``)
|
||||
The list of viewscreens that this widget should be associated with. When
|
||||
one of these viewscreens is on top of the viewscreen stack, your widget's
|
||||
callback functions for update, input, and render will be interposed into the
|
||||
viewscreen's call path. The name of the viewscreen is the name of the DFHack
|
||||
class that represents the viewscreen, minus the ``viewscreen_`` prefix and
|
||||
``st`` suffix. For example, the fort mode main map viewscreen would be
|
||||
``dwarfmode`` and the adventure mode map viewscreen would be
|
||||
``dungeonmode``. If there is only one viewscreen that this widget is
|
||||
associated with, it can be specified as a string instead of a list of
|
||||
strings with a single element.
|
||||
- ``hotspot`` (default: ``false``)
|
||||
If set to ``true``, your widget's ``overlay_onupdate`` function will be
|
||||
called whenever the `overlay` plugin's ``plugin_onupdate()`` function is
|
||||
called (which corresponds to one call per call to the current top
|
||||
viewscreen's ``logic()`` function). This call to ``overlay_onupdate`` is in
|
||||
addition to any calls initiated from associated interposed viewscreens and
|
||||
will come after calls from associated viewscreens.
|
||||
- ``overlay_onupdate_max_freq_seconds`` (default: ``5``)
|
||||
This throttles how often a widget's ``overlay_onupdate`` function can be
|
||||
called (from any source). Set this to the largest amount of time (in
|
||||
seconds) that your widget can take to react to changes in information and
|
||||
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.
|
||||
|
||||
Registering a widget with the overlay framework
|
||||
***********************************************
|
||||
|
||||
Anywhere in your code after the widget classes are declared, define a table
|
||||
named ``OVERLAY_WIDGETS``. The keys are the display names for your widgets and
|
||||
the values are the widget classes. For example, the `dwarfmonitor` widgets are
|
||||
declared like this::
|
||||
|
||||
OVERLAY_WIDGETS = {
|
||||
cursor=CursorWidget,
|
||||
date=DateWidget,
|
||||
misery=MiseryWidget,
|
||||
weather=WeatherWidget,
|
||||
}
|
||||
|
||||
When the `overlay` plugin is enabled, it scans all plugins and scripts for
|
||||
this table and registers the widgets on your behalf. The widget is enabled if it
|
||||
was enabled the last time the `overlay` plugin was loaded and the widget's
|
||||
position is restored according to the state saved in the
|
||||
:file:`dfhack-config/overlay.json` file.
|
||||
|
||||
The overlay framework will instantiate widgets from the named classes and own
|
||||
the resulting objects. The instantiated widgets must not be added as subviews to
|
||||
any other View, including the Screen views that can be returned from the
|
||||
``overlay_trigger()`` function.
|
||||
|
||||
Widget example 1: adding text to a DF screen
|
||||
--------------------------------------------
|
||||
|
||||
This is a simple widget that displays a message at its position. The message
|
||||
text is retrieved from the host script or plugin every ~20 seconds or when
|
||||
the :kbd:`Alt`:kbd:`Z` hotkey is hit::
|
||||
|
||||
local overlay = require('plugins.overlay')
|
||||
local widgets = require('gui.widgets')
|
||||
|
||||
MessageWidget = defclass(MessageWidget, overlay.OverlayWidget)
|
||||
MessageWidget.ATTRS{
|
||||
default_pos={x=5,y=-2},
|
||||
viewscreens={'dwarfmode', 'dungeonmode'},
|
||||
overlay_onupdate_max_freq_seconds=20,
|
||||
}
|
||||
|
||||
function MessageWidget:init()
|
||||
self.label = widgets.Label{text=''}
|
||||
self:addviews{self.label}
|
||||
end
|
||||
|
||||
function MessageWidget:overlay_onupdate()
|
||||
local text = getImportantMessage() -- defined in the host script/plugin
|
||||
self.label:setText(text)
|
||||
self.frame.w = #text
|
||||
end
|
||||
|
||||
function MessageWidget:onInput(keys)
|
||||
if keys.CUSTOM_ALT_Z then
|
||||
self:overlay_onupdate()
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
OVERLAY_WIDGETS = {message=MessageWidget}
|
||||
|
||||
Widget example 2: highlighting artifacts on the live game map
|
||||
-------------------------------------------------------------
|
||||
|
||||
This widget is not rendered at its "position" at all, but instead monitors the
|
||||
map and overlays information about where artifacts are located. Scanning for
|
||||
which artifacts are visible on the map can slow, so that is only done every 10
|
||||
seconds to avoid slowing down the entire game on every frame.
|
||||
|
||||
::
|
||||
|
||||
local overlay = require('plugins.overlay')
|
||||
local widgets = require('gui.widgets')
|
||||
|
||||
ArtifactRadarWidget = defclass(ArtifactRadarWidget, overlay.OverlayWidget)
|
||||
ArtifactRadarWidget.ATTRS{
|
||||
viewscreens={'dwarfmode', 'dungeonmode'},
|
||||
overlay_onupdate_max_freq_seconds=10,
|
||||
}
|
||||
|
||||
function ArtifactRadarWidget:overlay_onupdate()
|
||||
self.visible_artifacts_coords = getVisibleArtifactCoords()
|
||||
end
|
||||
|
||||
function ArtifactRadarWidget:onRenderFrame()
|
||||
for _,pos in ipairs(self.visible_artifacts_coords) do
|
||||
-- highlight tile at given coordinates
|
||||
end
|
||||
end
|
||||
|
||||
OVERLAY_WIDGETS = {radar=ArtifactRadarWidget}
|
||||
|
||||
Widget example 3: corner hotspot
|
||||
--------------------------------
|
||||
|
||||
This hotspot reacts to mouseover events and launches a screen that can react to
|
||||
input events. The hotspot area is a 2x2 block near the lower right corner of the
|
||||
screen (by default, but the player can move it wherever).
|
||||
|
||||
::
|
||||
|
||||
local overlay = require('plugins.overlay')
|
||||
local widgets = require('gui.widgets')
|
||||
|
||||
HotspotMenuWidget = defclass(HotspotMenuWidget, overlay.OverlayWidget)
|
||||
HotspotMenuWidget.ATTRS{
|
||||
default_pos={x=-3,y=-3},
|
||||
frame={w=2, h=2},
|
||||
hotspot=true,
|
||||
viewscreens='dwarfmode',
|
||||
overlay_onupdate_max_freq_seconds=0, -- check for mouseover every tick
|
||||
}
|
||||
|
||||
function HotspotMenuWidget:init()
|
||||
-- note this label only gets rendered on the associated viewscreen
|
||||
-- (dwarfmode), but the hotspot is active on all screens
|
||||
self:addviews{widgets.Label{text={'!!', NEWLINE, '!!'}}}
|
||||
self.mouseover = false
|
||||
end
|
||||
|
||||
function HotspotMenuWidget:overlay_onupdate()
|
||||
local hasMouse = self:getMousePos()
|
||||
if hasMouse and not self.mouseover then -- only trigger on mouse entry
|
||||
self.mouseover = true
|
||||
return true
|
||||
end
|
||||
self.mouseover = hasMouse
|
||||
end
|
||||
|
||||
function HotspotMenuWidget:overlay_trigger()
|
||||
return MenuScreen{hotspot_frame=self.frame}:show()
|
||||
end
|
||||
|
||||
OVERLAY_WIDGETS = {menu=HotspotMenuWidget}
|
||||
|
||||
MenuScreen = defclass(MenuScreen, gui.Screen)
|
||||
MenuScreen.ATTRS{
|
||||
focus_path='hotspot/menu',
|
||||
hotspot_frame=DEFAULT_NIL,
|
||||
}
|
||||
|
||||
function MenuScreen:init()
|
||||
self.mouseover = false
|
||||
|
||||
-- derrive the menu frame from the hotspot frame so it
|
||||
-- can appear in a nearby location
|
||||
local frame = copyall(self.hotspot_frame)
|
||||
-- ...
|
||||
|
||||
self:addviews{
|
||||
widgets.ResizingPanel{
|
||||
autoarrange_subviews=true,
|
||||
frame=frame,
|
||||
frame_style=gui.GREY_LINE_FRAME,
|
||||
frame_background=gui.CLEAR_PEN,
|
||||
subviews={
|
||||
-- ...
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
function MenuScreen:onInput(keys)
|
||||
if keys.LEAVESCREEN then
|
||||
self:dismiss()
|
||||
return true
|
||||
end
|
||||
return self:inputToSubviews(keys)
|
||||
end
|
||||
|
||||
function MenuScreen:onRenderFrame(dc, rect)
|
||||
self:renderParent()
|
||||
end
|
Binary file not shown.
Before Width: | Height: | Size: 32 KiB |
@ -0,0 +1,76 @@
|
||||
channel-safely
|
||||
==============
|
||||
|
||||
.. dfhack-tool::
|
||||
:summary: Auto-manage channel designations to keep dwarves safe.
|
||||
:tags: fort auto
|
||||
|
||||
Multi-level channel projects can be dangerous, and managing the safety of your
|
||||
dwarves throughout the completion of such projects can be difficult and time
|
||||
consuming. This plugin keeps your dwarves safe (at least while channeling) so you don't
|
||||
have to. Now you can focus on designing your dwarven cities with the deep chasms
|
||||
they were meant to have.
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
::
|
||||
|
||||
enable channel-safely
|
||||
channel-safely set <setting> <value>
|
||||
channel-safely enable|disable <feature>
|
||||
channel-safely <command>
|
||||
|
||||
When enabled the map will be scanned for channel designations which will be grouped
|
||||
together based on adjacency and z-level. These groups will then be analyzed for safety
|
||||
and designations deemed unsafe will be put into :wiki:`Marker Mode <Designations_menu#Marker_Mode>`.
|
||||
Each time a channel designation is completed its group status is checked, and if the group
|
||||
is complete pending groups below are made active again.
|
||||
|
||||
Features and settings once set will persist until you change them, even if you save and reload your game.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
``channel-safely``
|
||||
The plugin reports its configured status.
|
||||
|
||||
``channel-safely runonce``
|
||||
Runs the safety procedures once. You can use this if you prefer initiating scans manually.
|
||||
|
||||
``channel-safely disable require-vision``
|
||||
Allows the plugin to read all tiles, including the ones your dwarves know nothing about.
|
||||
|
||||
``channel-safely enable monitor``
|
||||
Enables monitoring active channel digging jobs. Meaning that if another unit it present
|
||||
or the tile below becomes open space the job will be paused or canceled (respectively).
|
||||
|
||||
``channel-safely set ignore-threshold 3``
|
||||
Configures the plugin to ignore designations equal to or above priority 3 designations.
|
||||
|
||||
Commands
|
||||
--------
|
||||
|
||||
:runonce: Run the safety procedures once to set the marker mode of designations.
|
||||
:rebuild: Rebuild the designation group data. Intended for to be used in the event
|
||||
the marker mode isn't being set correctly (mostly for debugging).
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
:require-vision: Toggle whether the dwarves need vision of a tile before channeling to it can be deemed unsafe. (default: enabled)
|
||||
:monitor: Toggle whether to monitor the conditions of active digs. (default: disabled)
|
||||
:resurrect: Toggle whether to resurrect units involved in cave-ins, and if monitor is enabled
|
||||
units who die while digging. (default: disabled)
|
||||
:insta-dig: Toggle whether to use insta-digging on unreachable designations.
|
||||
Runs on the refresh cycles. (default: disabled)
|
||||
|
||||
Settings
|
||||
--------
|
||||
|
||||
:refresh-freq: The rate at which full refreshes are performed.
|
||||
This can be expensive if you're undertaking many mega projects. (default:600, twice a day)
|
||||
:monitor-freq: The rate at which active jobs are monitored. (default:1)
|
||||
:ignore-threshold: Sets the priority threshold below which designations are processed. You can set to 1 or 0 to
|
||||
effectively disable the scanning. (default: 5)
|
||||
:fall-threshold: Sets the fall threshold beyond which is considered unsafe. (default: 1)
|
@ -1,23 +0,0 @@
|
||||
resume
|
||||
======
|
||||
|
||||
.. dfhack-tool::
|
||||
:summary: Color planned buildings based on their suspend status.
|
||||
:tags: fort productivity interface jobs
|
||||
:no-command:
|
||||
|
||||
.. dfhack-command:: resume
|
||||
:summary: Resume all suspended building jobs.
|
||||
|
||||
When enabled, this plugin will display a colored 'X' over suspended buildings.
|
||||
When run as a command, it can resume all suspended building jobs, allowing you
|
||||
to quickly recover if a bunch of jobs were suspended due to the workers getting
|
||||
scared off by wildlife or items temporarily blocking building sites.
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
::
|
||||
|
||||
enable resume
|
||||
resume all
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,7 @@
|
||||
file(GLOB_RECURSE TEST_SOURCES LIST_DIRECTORIES false *test.cpp)
|
||||
dfhack_test(test-library "${TEST_SOURCES}")
|
||||
|
||||
# How to get `test` to ensure everything is up to date before running
|
||||
# tests? This add_dependencies() fails with:
|
||||
# Cannot add target-level dependencies to non-existent target "test".
|
||||
#add_dependencies(test MiscUtils.test)
|
@ -0,0 +1,19 @@
|
||||
|
||||
#include "MiscUtils.h"
|
||||
#include <gtest/gtest.h>
|
||||
#include <string>
|
||||
|
||||
TEST(MiscUtils, wordwrap) {
|
||||
std::vector<std::string> result;
|
||||
|
||||
word_wrap(&result, "123", 3);
|
||||
ASSERT_EQ(result.size(), 1);
|
||||
|
||||
result.clear();
|
||||
word_wrap(&result, "12345", 3);
|
||||
ASSERT_EQ(result.size(), 2);
|
||||
|
||||
result.clear();
|
||||
word_wrap(&result, "1234567", 3);
|
||||
ASSERT_EQ(result.size(), 3);
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
@ -1 +1 @@
|
||||
Subproject commit ea78ed8bf70c3e75b8fba90cdc61cab34788899e
|
||||
Subproject commit 4c5697dcb060d645849327410b8ecce6880053d4
|
@ -0,0 +1,16 @@
|
||||
project(autolahor)
|
||||
# A list of source files
|
||||
set(COMMON_SRCS
|
||||
)
|
||||
# A list of headers
|
||||
set(COMMON_HDRS laborstatemap.h
|
||||
)
|
||||
set_source_files_properties(${COMMON_HDRS} PROPERTIES HEADER_FILE_ONLY TRUE)
|
||||
|
||||
# mash them together (headers are marked as headers and nothing will try to compile them)
|
||||
list(APPEND COMMON_SRCS ${COMMON_HDRS})
|
||||
|
||||
dfhack_plugin(labormanager labormanager.cpp joblabormapper.cpp ${COMMON_SRCS})
|
||||
|
||||
dfhack_plugin(autohauler autohauler.cpp ${COMMON_SRCS})
|
||||
dfhack_plugin(autolabor autolabor.cpp ${COMMON_SRCS})
|
@ -0,0 +1,294 @@
|
||||
#pragma once
|
||||
|
||||
#include <Core.h>
|
||||
|
||||
#include "df/job.h"
|
||||
#include "df/job_type.h"
|
||||
#include "df/unit_labor.h"
|
||||
|
||||
using namespace DFHack;
|
||||
using namespace df::enums;
|
||||
|
||||
enum dwarf_state : int {
|
||||
// Ready for a new task
|
||||
IDLE=0,
|
||||
|
||||
// Busy with a useful task
|
||||
BUSY,
|
||||
|
||||
// Busy with a useful task that requires a tool
|
||||
EXCLUSIVE,
|
||||
|
||||
// In the military, can't work
|
||||
MILITARY,
|
||||
|
||||
// Child or noble, can't work
|
||||
CHILD,
|
||||
|
||||
// Doing something that precludes working, may be busy for a while
|
||||
OTHER
|
||||
};
|
||||
|
||||
const int NUM_STATE = 6;
|
||||
|
||||
char const* state_names[] {
|
||||
"IDLE",
|
||||
"BUSY",
|
||||
"EXCLUSIVE",
|
||||
"MILITARY",
|
||||
"CHILD",
|
||||
"OTHER",
|
||||
};
|
||||
|
||||
const dwarf_state dwarf_states[] = {
|
||||
dwarf_state::BUSY /* CarveFortification */,
|
||||
dwarf_state::BUSY /* DetailWall */,
|
||||
dwarf_state::BUSY /* DetailFloor */,
|
||||
dwarf_state::EXCLUSIVE /* Dig */,
|
||||
dwarf_state::EXCLUSIVE /* CarveUpwardStaircase */,
|
||||
dwarf_state::EXCLUSIVE /* CarveDownwardStaircase */,
|
||||
dwarf_state::EXCLUSIVE /* CarveUpDownStaircase */,
|
||||
dwarf_state::EXCLUSIVE /* CarveRamp */,
|
||||
dwarf_state::EXCLUSIVE /* DigChannel */,
|
||||
dwarf_state::EXCLUSIVE /* FellTree */,
|
||||
dwarf_state::BUSY /* GatherPlants */,
|
||||
dwarf_state::BUSY /* RemoveConstruction */,
|
||||
dwarf_state::BUSY /* CollectWebs */,
|
||||
dwarf_state::BUSY /* BringItemToDepot */,
|
||||
dwarf_state::BUSY /* BringItemToShop */,
|
||||
dwarf_state::OTHER /* Eat */,
|
||||
dwarf_state::OTHER /* GetProvisions */,
|
||||
dwarf_state::OTHER /* Drink */,
|
||||
dwarf_state::OTHER /* Drink2 */,
|
||||
dwarf_state::OTHER /* FillWaterskin */,
|
||||
dwarf_state::OTHER /* FillWaterskin2 */,
|
||||
dwarf_state::OTHER /* Sleep */,
|
||||
dwarf_state::BUSY /* CollectSand */,
|
||||
dwarf_state::BUSY /* Fish */,
|
||||
dwarf_state::EXCLUSIVE /* Hunt */,
|
||||
dwarf_state::OTHER /* HuntVermin */,
|
||||
dwarf_state::BUSY /* Kidnap */,
|
||||
dwarf_state::BUSY /* BeatCriminal */,
|
||||
dwarf_state::BUSY /* StartingFistFight */,
|
||||
dwarf_state::BUSY /* CollectTaxes */,
|
||||
dwarf_state::BUSY /* GuardTaxCollector */,
|
||||
dwarf_state::BUSY /* CatchLiveLandAnimal */,
|
||||
dwarf_state::BUSY /* CatchLiveFish */,
|
||||
dwarf_state::BUSY /* ReturnKill */,
|
||||
dwarf_state::BUSY /* CheckChest */,
|
||||
dwarf_state::BUSY /* StoreOwnedItem */,
|
||||
dwarf_state::BUSY /* PlaceItemInTomb */,
|
||||
dwarf_state::BUSY /* StoreItemInStockpile */,
|
||||
dwarf_state::BUSY /* StoreItemInBag */,
|
||||
dwarf_state::BUSY /* StoreItemInHospital */,
|
||||
dwarf_state::BUSY /* StoreItemInChest */,
|
||||
dwarf_state::BUSY /* StoreItemInCabinet */,
|
||||
dwarf_state::BUSY /* StoreWeapon */,
|
||||
dwarf_state::BUSY /* StoreArmor */,
|
||||
dwarf_state::BUSY /* StoreItemInBarrel */,
|
||||
dwarf_state::BUSY /* StoreItemInBin */,
|
||||
dwarf_state::BUSY /* SeekArtifact */,
|
||||
dwarf_state::BUSY /* SeekInfant */,
|
||||
dwarf_state::OTHER /* AttendParty */,
|
||||
dwarf_state::OTHER /* GoShopping */,
|
||||
dwarf_state::OTHER /* GoShopping2 */,
|
||||
dwarf_state::BUSY /* Clean */,
|
||||
dwarf_state::OTHER /* Rest */,
|
||||
dwarf_state::EXCLUSIVE /* PickupEquipment */,
|
||||
dwarf_state::BUSY /* DumpItem */,
|
||||
dwarf_state::OTHER /* StrangeMoodCrafter */,
|
||||
dwarf_state::OTHER /* StrangeMoodJeweller */,
|
||||
dwarf_state::OTHER /* StrangeMoodForge */,
|
||||
dwarf_state::OTHER /* StrangeMoodMagmaForge */,
|
||||
dwarf_state::OTHER /* StrangeMoodBrooding */,
|
||||
dwarf_state::OTHER /* StrangeMoodFell */,
|
||||
dwarf_state::OTHER /* StrangeMoodCarpenter */,
|
||||
dwarf_state::OTHER /* StrangeMoodMason */,
|
||||
dwarf_state::OTHER /* StrangeMoodBowyer */,
|
||||
dwarf_state::OTHER /* StrangeMoodTanner */,
|
||||
dwarf_state::OTHER /* StrangeMoodWeaver */,
|
||||
dwarf_state::OTHER /* StrangeMoodGlassmaker */,
|
||||
dwarf_state::OTHER /* StrangeMoodMechanics */,
|
||||
dwarf_state::BUSY /* ConstructBuilding */,
|
||||
dwarf_state::BUSY /* ConstructDoor */,
|
||||
dwarf_state::BUSY /* ConstructFloodgate */,
|
||||
dwarf_state::BUSY /* ConstructBed */,
|
||||
dwarf_state::BUSY /* ConstructThrone */,
|
||||
dwarf_state::BUSY /* ConstructCoffin */,
|
||||
dwarf_state::BUSY /* ConstructTable */,
|
||||
dwarf_state::BUSY /* ConstructChest */,
|
||||
dwarf_state::BUSY /* ConstructBin */,
|
||||
dwarf_state::BUSY /* ConstructArmorStand */,
|
||||
dwarf_state::BUSY /* ConstructWeaponRack */,
|
||||
dwarf_state::BUSY /* ConstructCabinet */,
|
||||
dwarf_state::BUSY /* ConstructStatue */,
|
||||
dwarf_state::BUSY /* ConstructBlocks */,
|
||||
dwarf_state::BUSY /* MakeRawGlass */,
|
||||
dwarf_state::BUSY /* MakeCrafts */,
|
||||
dwarf_state::BUSY /* MintCoins */,
|
||||
dwarf_state::BUSY /* CutGems */,
|
||||
dwarf_state::BUSY /* CutGlass */,
|
||||
dwarf_state::BUSY /* EncrustWithGems */,
|
||||
dwarf_state::BUSY /* EncrustWithGlass */,
|
||||
dwarf_state::BUSY /* DestroyBuilding */,
|
||||
dwarf_state::BUSY /* SmeltOre */,
|
||||
dwarf_state::BUSY /* MeltMetalObject */,
|
||||
dwarf_state::BUSY /* ExtractMetalStrands */,
|
||||
dwarf_state::BUSY /* PlantSeeds */,
|
||||
dwarf_state::BUSY /* HarvestPlants */,
|
||||
dwarf_state::BUSY /* TrainHuntingAnimal */,
|
||||
dwarf_state::BUSY /* TrainWarAnimal */,
|
||||
dwarf_state::BUSY /* MakeWeapon */,
|
||||
dwarf_state::BUSY /* ForgeAnvil */,
|
||||
dwarf_state::BUSY /* ConstructCatapultParts */,
|
||||
dwarf_state::BUSY /* ConstructBallistaParts */,
|
||||
dwarf_state::BUSY /* MakeArmor */,
|
||||
dwarf_state::BUSY /* MakeHelm */,
|
||||
dwarf_state::BUSY /* MakePants */,
|
||||
dwarf_state::BUSY /* StudWith */,
|
||||
dwarf_state::BUSY /* ButcherAnimal */,
|
||||
dwarf_state::BUSY /* PrepareRawFish */,
|
||||
dwarf_state::BUSY /* MillPlants */,
|
||||
dwarf_state::BUSY /* BaitTrap */,
|
||||
dwarf_state::BUSY /* MilkCreature */,
|
||||
dwarf_state::BUSY /* MakeCheese */,
|
||||
dwarf_state::BUSY /* ProcessPlants */,
|
||||
dwarf_state::BUSY /* ProcessPlantsBag */,
|
||||
dwarf_state::BUSY /* ProcessPlantsVial */,
|
||||
dwarf_state::BUSY /* ProcessPlantsBarrel */,
|
||||
dwarf_state::BUSY /* PrepareMeal */,
|
||||
dwarf_state::BUSY /* WeaveCloth */,
|
||||
dwarf_state::BUSY /* MakeGloves */,
|
||||
dwarf_state::BUSY /* MakeShoes */,
|
||||
dwarf_state::BUSY /* MakeShield */,
|
||||
dwarf_state::BUSY /* MakeCage */,
|
||||
dwarf_state::BUSY /* MakeChain */,
|
||||
dwarf_state::BUSY /* MakeFlask */,
|
||||
dwarf_state::BUSY /* MakeGoblet */,
|
||||
dwarf_state::BUSY /* MakeInstrument */,
|
||||
dwarf_state::BUSY /* MakeToy */,
|
||||
dwarf_state::BUSY /* MakeAnimalTrap */,
|
||||
dwarf_state::BUSY /* MakeBarrel */,
|
||||
dwarf_state::BUSY /* MakeBucket */,
|
||||
dwarf_state::BUSY /* MakeWindow */,
|
||||
dwarf_state::BUSY /* MakeTotem */,
|
||||
dwarf_state::BUSY /* MakeAmmo */,
|
||||
dwarf_state::BUSY /* DecorateWith */,
|
||||
dwarf_state::BUSY /* MakeBackpack */,
|
||||
dwarf_state::BUSY /* MakeQuiver */,
|
||||
dwarf_state::BUSY /* MakeBallistaArrowHead */,
|
||||
dwarf_state::BUSY /* AssembleSiegeAmmo */,
|
||||
dwarf_state::BUSY /* LoadCatapult */,
|
||||
dwarf_state::BUSY /* LoadBallista */,
|
||||
dwarf_state::BUSY /* FireCatapult */,
|
||||
dwarf_state::BUSY /* FireBallista */,
|
||||
dwarf_state::BUSY /* ConstructMechanisms */,
|
||||
dwarf_state::BUSY /* MakeTrapComponent */,
|
||||
dwarf_state::BUSY /* LoadCageTrap */,
|
||||
dwarf_state::BUSY /* LoadStoneTrap */,
|
||||
dwarf_state::BUSY /* LoadWeaponTrap */,
|
||||
dwarf_state::BUSY /* CleanTrap */,
|
||||
dwarf_state::BUSY /* CastSpell */,
|
||||
dwarf_state::BUSY /* LinkBuildingToTrigger */,
|
||||
dwarf_state::BUSY /* PullLever */,
|
||||
dwarf_state::BUSY /* BrewDrink */,
|
||||
dwarf_state::BUSY /* ExtractFromPlants */,
|
||||
dwarf_state::BUSY /* ExtractFromRawFish */,
|
||||
dwarf_state::BUSY /* ExtractFromLandAnimal */,
|
||||
dwarf_state::BUSY /* TameVermin */,
|
||||
dwarf_state::BUSY /* TameAnimal */,
|
||||
dwarf_state::BUSY /* ChainAnimal */,
|
||||
dwarf_state::BUSY /* UnchainAnimal */,
|
||||
dwarf_state::BUSY /* UnchainPet */,
|
||||
dwarf_state::BUSY /* ReleaseLargeCreature */,
|
||||
dwarf_state::BUSY /* ReleasePet */,
|
||||
dwarf_state::BUSY /* ReleaseSmallCreature */,
|
||||
dwarf_state::BUSY /* HandleSmallCreature */,
|
||||
dwarf_state::BUSY /* HandleLargeCreature */,
|
||||
dwarf_state::BUSY /* CageLargeCreature */,
|
||||
dwarf_state::BUSY /* CageSmallCreature */,
|
||||
dwarf_state::BUSY /* RecoverWounded */,
|
||||
dwarf_state::BUSY /* DiagnosePatient */,
|
||||
dwarf_state::BUSY /* ImmobilizeBreak */,
|
||||
dwarf_state::BUSY /* DressWound */,
|
||||
dwarf_state::BUSY /* CleanPatient */,
|
||||
dwarf_state::BUSY /* Surgery */,
|
||||
dwarf_state::BUSY /* Suture */,
|
||||
dwarf_state::BUSY /* SetBone */,
|
||||
dwarf_state::BUSY /* PlaceInTraction */,
|
||||
dwarf_state::BUSY /* DrainAquarium */,
|
||||
dwarf_state::BUSY /* FillAquarium */,
|
||||
dwarf_state::BUSY /* FillPond */,
|
||||
dwarf_state::BUSY /* GiveWater */,
|
||||
dwarf_state::BUSY /* GiveFood */,
|
||||
dwarf_state::BUSY /* GiveWater2 */,
|
||||
dwarf_state::BUSY /* GiveFood2 */,
|
||||
dwarf_state::BUSY /* RecoverPet */,
|
||||
dwarf_state::BUSY /* PitLargeAnimal */,
|
||||
dwarf_state::BUSY /* PitSmallAnimal */,
|
||||
dwarf_state::BUSY /* SlaughterAnimal */,
|
||||
dwarf_state::BUSY /* MakeCharcoal */,
|
||||
dwarf_state::BUSY /* MakeAsh */,
|
||||
dwarf_state::BUSY /* MakeLye */,
|
||||
dwarf_state::BUSY /* MakePotashFromLye */,
|
||||
dwarf_state::BUSY /* FertilizeField */,
|
||||
dwarf_state::BUSY /* MakePotashFromAsh */,
|
||||
dwarf_state::BUSY /* DyeThread */,
|
||||
dwarf_state::BUSY /* DyeCloth */,
|
||||
dwarf_state::BUSY /* SewImage */,
|
||||
dwarf_state::BUSY /* MakePipeSection */,
|
||||
dwarf_state::BUSY /* OperatePump */,
|
||||
dwarf_state::OTHER /* ManageWorkOrders */,
|
||||
dwarf_state::OTHER /* UpdateStockpileRecords */,
|
||||
dwarf_state::OTHER /* TradeAtDepot */,
|
||||
dwarf_state::BUSY /* ConstructHatchCover */,
|
||||
dwarf_state::BUSY /* ConstructGrate */,
|
||||
dwarf_state::BUSY /* RemoveStairs */,
|
||||
dwarf_state::BUSY /* ConstructQuern */,
|
||||
dwarf_state::BUSY /* ConstructMillstone */,
|
||||
dwarf_state::BUSY /* ConstructSplint */,
|
||||
dwarf_state::BUSY /* ConstructCrutch */,
|
||||
dwarf_state::BUSY /* ConstructTractionBench */,
|
||||
dwarf_state::BUSY /* CleanSelf */,
|
||||
dwarf_state::BUSY /* BringCrutch */,
|
||||
dwarf_state::BUSY /* ApplyCast */,
|
||||
dwarf_state::BUSY /* CustomReaction */,
|
||||
dwarf_state::BUSY /* ConstructSlab */,
|
||||
dwarf_state::BUSY /* EngraveSlab */,
|
||||
dwarf_state::BUSY /* ShearCreature */,
|
||||
dwarf_state::BUSY /* SpinThread */,
|
||||
dwarf_state::BUSY /* PenLargeAnimal */,
|
||||
dwarf_state::BUSY /* PenSmallAnimal */,
|
||||
dwarf_state::BUSY /* MakeTool */,
|
||||
dwarf_state::BUSY /* CollectClay */,
|
||||
dwarf_state::BUSY /* InstallColonyInHive */,
|
||||
dwarf_state::BUSY /* CollectHiveProducts */,
|
||||
dwarf_state::OTHER /* CauseTrouble */,
|
||||
dwarf_state::OTHER /* DrinkBlood */,
|
||||
dwarf_state::OTHER /* ReportCrime */,
|
||||
dwarf_state::OTHER /* ExecuteCriminal */,
|
||||
dwarf_state::BUSY /* TrainAnimal */,
|
||||
dwarf_state::BUSY /* CarveTrack */,
|
||||
dwarf_state::BUSY /* PushTrackVehicle */,
|
||||
dwarf_state::BUSY /* PlaceTrackVehicle */,
|
||||
dwarf_state::BUSY /* StoreItemInVehicle */,
|
||||
dwarf_state::BUSY /* GeldAnimal */,
|
||||
dwarf_state::BUSY /* MakeFigurine */,
|
||||
dwarf_state::BUSY /* MakeAmulet */,
|
||||
dwarf_state::BUSY /* MakeScepter */,
|
||||
dwarf_state::BUSY /* MakeCrown */,
|
||||
dwarf_state::BUSY /* MakeRing */,
|
||||
dwarf_state::BUSY /* MakeEarring */,
|
||||
dwarf_state::BUSY /* MakeBracelet */,
|
||||
dwarf_state::BUSY /* MakeGem */,
|
||||
dwarf_state::BUSY /* PutItemOnDisplay */,
|
||||
dwarf_state::OTHER /* unk_fake_no_job */,
|
||||
dwarf_state::OTHER /* InterrogateSubject */,
|
||||
dwarf_state::OTHER /* unk_fake_no_activity */,
|
||||
};
|
||||
|
||||
#define ARRAY_COUNT(array) (sizeof(array)/sizeof((array)[0]))
|
||||
|
||||
const int dwarf_state_count = ARRAY_COUNT(dwarf_states);
|
||||
|
||||
#undef ARRAY_COUNT
|
@ -0,0 +1,9 @@
|
||||
project(channel-safely)
|
||||
|
||||
include_directories(include)
|
||||
SET(SOURCES
|
||||
channel-groups.cpp
|
||||
channel-manager.cpp
|
||||
channel-safely-plugin.cpp)
|
||||
|
||||
dfhack_plugin(${PROJECT_NAME} ${SOURCES} LINK_LIBRARIES lua)
|
@ -0,0 +1,290 @@
|
||||
#include <channel-groups.h>
|
||||
#include <tile-cache.h>
|
||||
#include <inlines.h>
|
||||
#include <modules/Maps.h>
|
||||
#include <df/block_square_event_designation_priorityst.h>
|
||||
|
||||
#include <random>
|
||||
|
||||
// iterates the DF job list and adds channel jobs to the `jobs` container
|
||||
void ChannelJobs::load_channel_jobs() {
|
||||
locations.clear();
|
||||
df::job_list_link* node = df::global::world->jobs.list.next;
|
||||
while (node) {
|
||||
df::job* job = node->item;
|
||||
node = node->next;
|
||||
if (is_dig_job(job)) {
|
||||
locations.emplace(job->pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// adds map_pos to a group if an adjacent one exists, or creates one if none exist... if multiple exist they're merged into the first found
|
||||
void ChannelGroups::add(const df::coord &map_pos) {
|
||||
// if we've already added this, we don't need to do it again
|
||||
if (groups_map.count(map_pos)) {
|
||||
return;
|
||||
}
|
||||
/* We need to add map_pos to an existing group if possible...
|
||||
* So what we do is we look at neighbours to see if they belong to one or more existing groups
|
||||
* If there is more than one group, we'll be merging them
|
||||
*/
|
||||
df::coord neighbors[8];
|
||||
get_neighbours(map_pos, neighbors);
|
||||
Group* group = nullptr;
|
||||
int group_index = -1;
|
||||
|
||||
DEBUG(groups).print(" add(" COORD ")\n", COORDARGS(map_pos));
|
||||
// and so we begin iterating the neighbours
|
||||
for (auto &neighbour: neighbors) {
|
||||
// go to the next neighbour if this one doesn't have a group
|
||||
if (!groups_map.count(neighbour)) {
|
||||
TRACE(groups).print(" -> neighbour is not designated\n");
|
||||
continue;
|
||||
}
|
||||
// get the group, since at least one exists... then merge any additional into that one
|
||||
if (!group){
|
||||
TRACE(groups).print(" -> group* has no valid state yet\n");
|
||||
group_index = groups_map.find(neighbour)->second;
|
||||
group = &groups.at(group_index);
|
||||
} else {
|
||||
TRACE(groups).print(" -> group* has an existing state\n");
|
||||
|
||||
// we don't do anything if the found group is the same as the existing group
|
||||
auto index2 = groups_map.find(neighbour)->second;
|
||||
if (group_index != index2) {
|
||||
// we already have group "prime" if you will, so we're going to merge the new find into prime
|
||||
Group &group2 = groups.at(index2);
|
||||
// merge
|
||||
TRACE(groups).print(" -> merging two groups. group 1 size: %zu. group 2 size: %zu\n", group->size(),
|
||||
group2.size());
|
||||
for (auto pos2: group2) {
|
||||
group->emplace(pos2);
|
||||
groups_map[pos2] = group_index;
|
||||
}
|
||||
group2.clear();
|
||||
free_spots.emplace(index2);
|
||||
TRACE(groups).print(" merged size: %zu\n", group->size());
|
||||
}
|
||||
}
|
||||
}
|
||||
// if we haven't found at least one group by now we need to create/get one
|
||||
if (!group) {
|
||||
TRACE(groups).print(" -> no merging took place\n");
|
||||
// first we check if we can re-use a group that's been freed
|
||||
if (!free_spots.empty()) {
|
||||
TRACE(groups).print(" -> use recycled old group\n");
|
||||
// first element in a set is always the lowest value, so we re-use from the front of the vector
|
||||
group_index = *free_spots.begin();
|
||||
group = &groups[group_index];
|
||||
free_spots.erase(free_spots.begin());
|
||||
} else {
|
||||
TRACE(groups).print(" -> brand new group\n");
|
||||
// we create a brand-new group to use
|
||||
group_index = groups.size();
|
||||
groups.push_back(Group());
|
||||
group = &groups[group_index];
|
||||
}
|
||||
}
|
||||
// puts the "add" in "ChannelGroups::add"
|
||||
group->emplace(map_pos);
|
||||
DEBUG(groups).print(" = group[%d] of (" COORD ") is size: %zu\n", group_index, COORDARGS(map_pos), group->size());
|
||||
|
||||
// we may have performed a merge, so we update all the `coord -> group index` mappings
|
||||
for (auto &wpos: *group) {
|
||||
groups_map[wpos] = group_index;
|
||||
}
|
||||
DEBUG(groups).print(" <- add() exits, there are %zu mappings\n", groups_map.size());
|
||||
}
|
||||
|
||||
// scans a single tile for channel designations
|
||||
void ChannelGroups::scan_one(const df::coord &map_pos) {
|
||||
df::map_block* block = Maps::getTileBlock(map_pos);
|
||||
int16_t lx = map_pos.x % 16;
|
||||
int16_t ly = map_pos.y % 16;
|
||||
if (is_dig_designation(block->designation[lx][ly])) {
|
||||
for (df::block_square_event* event: block->block_events) {
|
||||
if (auto evT = virtual_cast<df::block_square_event_designation_priorityst>(event)) {
|
||||
// we want to let the user keep some designations free of being managed
|
||||
if (evT->priority[lx][ly] < 1000 * config.ignore_threshold) {
|
||||
TRACE(groups).print(" adding (" COORD ")\n", COORDARGS(map_pos));
|
||||
add(map_pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (TileCache::Get().hasChanged(map_pos, block->tiletype[lx][ly])) {
|
||||
TileCache::Get().uncache(map_pos);
|
||||
remove(map_pos);
|
||||
}
|
||||
}
|
||||
|
||||
// builds groupings of adjacent channel designations
|
||||
void ChannelGroups::scan() {
|
||||
// save current jobs, then clear and load the current jobs
|
||||
std::set<df::coord> last_jobs;
|
||||
for (auto &pos : jobs) {
|
||||
last_jobs.emplace(pos);
|
||||
}
|
||||
jobs.load_channel_jobs();
|
||||
// transpose channel jobs to
|
||||
std::set<df::coord> new_jobs;
|
||||
std::set<df::coord> gone_jobs;
|
||||
set_difference(last_jobs, jobs, gone_jobs);
|
||||
set_difference(jobs, last_jobs, new_jobs);
|
||||
for (auto &pos : new_jobs) {
|
||||
add(pos);
|
||||
}
|
||||
for (auto &pos : gone_jobs){
|
||||
remove(pos);
|
||||
}
|
||||
|
||||
static std::default_random_engine RNG(0);
|
||||
static std::bernoulli_distribution optimizing(0.75); // fixing OpenSpace as designated
|
||||
|
||||
DEBUG(groups).print(" scan()\n");
|
||||
// foreach block
|
||||
for (int32_t z = mapz - 1; z >= 0; --z) {
|
||||
for (int32_t by = 0; by < mapy; ++by) {
|
||||
for (int32_t bx = 0; bx < mapx; ++bx) {
|
||||
// the block
|
||||
if (df::map_block* block = Maps::getBlock(bx, by, z)) {
|
||||
// skip this block?
|
||||
if (!block->flags.bits.designated && !group_blocks.count(block) && optimizing(RNG)) {
|
||||
continue;
|
||||
}
|
||||
// foreach tile
|
||||
bool empty_group = true;
|
||||
for (int16_t lx = 0; lx < 16; ++lx) {
|
||||
for (int16_t ly = 0; ly < 16; ++ly) {
|
||||
// the tile, check if it has a channel designation
|
||||
df::coord map_pos((bx * 16) + lx, (by * 16) + ly, z);
|
||||
if (TileCache::Get().hasChanged(map_pos, block->tiletype[lx][ly])) {
|
||||
TileCache::Get().uncache(map_pos);
|
||||
remove(map_pos);
|
||||
if (jobs.count(map_pos)) {
|
||||
jobs.erase(map_pos);
|
||||
}
|
||||
block->designation[lx][ly].bits.dig = df::tile_dig_designation::No;
|
||||
} else if (is_dig_designation(block->designation[lx][ly])) {
|
||||
for (df::block_square_event* event: block->block_events) {
|
||||
if (auto evT = virtual_cast<df::block_square_event_designation_priorityst>(event)) {
|
||||
// we want to let the user keep some designations free of being managed
|
||||
TRACE(groups).print(" tile designation priority: %d\n", evT->priority[lx][ly]);
|
||||
if (evT->priority[lx][ly] < 1000 * config.ignore_threshold) {
|
||||
if (empty_group) {
|
||||
group_blocks.emplace(block);
|
||||
empty_group = false;
|
||||
}
|
||||
TRACE(groups).print(" adding (" COORD ")\n", COORDARGS(map_pos));
|
||||
add(map_pos);
|
||||
} else if (groups_map.count(map_pos)) {
|
||||
remove(map_pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// erase the block if we didn't find anything iterating through it
|
||||
if (empty_group) {
|
||||
group_blocks.erase(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
INFO(groups).print("scan() exits\n");
|
||||
}
|
||||
|
||||
// clears out the containers for unloading maps or disabling the plugin
|
||||
void ChannelGroups::clear() {
|
||||
debug_map();
|
||||
WARN(groups).print(" <- clearing groups\n");
|
||||
group_blocks.clear();
|
||||
free_spots.clear();
|
||||
groups_map.clear();
|
||||
for(size_t i = 0; i < groups.size(); ++i) {
|
||||
groups[i].clear();
|
||||
free_spots.emplace(i);
|
||||
}
|
||||
}
|
||||
|
||||
// erases map_pos from its group, and deletes mappings IFF the group is empty
|
||||
void ChannelGroups::remove(const df::coord &map_pos) {
|
||||
// we don't need to do anything if the position isn't in a group (granted, that should never be the case)
|
||||
INFO(groups).print(" remove()\n");
|
||||
if (groups_map.count(map_pos)) {
|
||||
INFO(groups).print(" -> found group\n");
|
||||
// get the group, and map_pos' block*
|
||||
int group_index = groups_map.find(map_pos)->second;
|
||||
Group &group = groups[group_index];
|
||||
// erase map_pos from the group
|
||||
INFO(groups).print(" -> erase(" COORD ")\n", COORDARGS(map_pos));
|
||||
group.erase(map_pos);
|
||||
groups_map.erase(map_pos);
|
||||
// clean up if the group is empty
|
||||
if (group.empty()) {
|
||||
WARN(groups).print(" -> group is empty\n");
|
||||
// erase `coord -> group group_index` mappings
|
||||
for (auto iter = groups_map.begin(); iter != groups_map.end();) {
|
||||
if (group_index == iter->second) {
|
||||
iter = groups_map.erase(iter);
|
||||
continue;
|
||||
}
|
||||
++iter;
|
||||
}
|
||||
// flag the `groups` group_index as available
|
||||
free_spots.insert(group_index);
|
||||
}
|
||||
}
|
||||
INFO(groups).print(" remove() exits\n");
|
||||
}
|
||||
|
||||
// finds a group corresponding to a map position if one exists
|
||||
Groups::const_iterator ChannelGroups::find(const df::coord &map_pos) const {
|
||||
const auto iter = groups_map.find(map_pos);
|
||||
if (iter != groups_map.end()) {
|
||||
return groups.begin() + iter->second;
|
||||
}
|
||||
return groups.end();
|
||||
}
|
||||
|
||||
// returns an iterator to the first element stored
|
||||
Groups::const_iterator ChannelGroups::begin() const {
|
||||
return groups.begin();
|
||||
}
|
||||
|
||||
// returns an iterator to after the last element stored
|
||||
Groups::const_iterator ChannelGroups::end() const {
|
||||
return groups.end();
|
||||
}
|
||||
|
||||
// returns a count of 0 or 1 depending on whether map_pos is mapped to a group
|
||||
size_t ChannelGroups::count(const df::coord &map_pos) const {
|
||||
return groups_map.count(map_pos);
|
||||
}
|
||||
|
||||
// prints debug info about the groups stored, and their members
|
||||
void ChannelGroups::debug_groups() {
|
||||
if (DFHack::debug_groups.isEnabled(DebugCategory::LTRACE)) {
|
||||
int idx = 0;
|
||||
TRACE(groups).print(" debugging group data\n");
|
||||
for (auto &group: groups) {
|
||||
TRACE(groups).print(" group %d (size: %zu)\n", idx, group.size());
|
||||
for (auto &pos: group) {
|
||||
TRACE(groups).print(" (%d,%d,%d)\n", pos.x, pos.y, pos.z);
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// prints debug info group mappings
|
||||
void ChannelGroups::debug_map() {
|
||||
if (DFHack::debug_groups.isEnabled(DebugCategory::LDEBUG)) {
|
||||
INFO(groups).print("Group Mappings: %zu\n", groups_map.size());
|
||||
for (auto &pair: groups_map) {
|
||||
DEBUG(groups).print(" map[" COORD "] = %d\n", COORDARGS(pair.first), pair.second);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
#include <channel-manager.h>
|
||||
#include <tile-cache.h>
|
||||
#include <inlines.h>
|
||||
|
||||
#include <modules/EventManager.h> //hash function for df::coord
|
||||
#include <df/block_square_event_designation_priorityst.h>
|
||||
|
||||
|
||||
// sets mark flags as necessary, for all designations
|
||||
void ChannelManager::manage_groups() {
|
||||
INFO(manager).print("manage_groups()\n");
|
||||
// make sure we've got a fort map to analyze
|
||||
if (World::isFortressMode() && Maps::IsValid()) {
|
||||
// iterate the groups we built/updated
|
||||
for (const auto &group: groups) {
|
||||
manage_group(group, true, has_any_groups_above(groups, group));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChannelManager::manage_group(const df::coord &map_pos, bool set_marker_mode, bool marker_mode) {
|
||||
INFO(manager).print("manage_group(" COORD ")\n ", COORDARGS(map_pos));
|
||||
if (!groups.count(map_pos)) {
|
||||
groups.scan_one(map_pos);
|
||||
}
|
||||
auto iter = groups.find(map_pos);
|
||||
if (iter != groups.end()) {
|
||||
manage_group(*iter, set_marker_mode, marker_mode);
|
||||
}
|
||||
INFO(manager).print("manage_group() is done\n");
|
||||
}
|
||||
|
||||
void ChannelManager::manage_group(const Group &group, bool set_marker_mode, bool marker_mode) {
|
||||
INFO(manager).print("manage_group()\n");
|
||||
if (!set_marker_mode) {
|
||||
if (has_any_groups_above(groups, group)) {
|
||||
marker_mode = true;
|
||||
} else {
|
||||
marker_mode = false;
|
||||
}
|
||||
}
|
||||
for (auto &designation: group) {
|
||||
manage_one(group, designation, true, marker_mode);
|
||||
}
|
||||
INFO(manager).print("manage_group() is done\n");
|
||||
}
|
||||
|
||||
bool ChannelManager::manage_one(const Group &group, const df::coord &map_pos, bool set_marker_mode, bool marker_mode) {
|
||||
if (Maps::isValidTilePos(map_pos)) {
|
||||
INFO(manager).print("manage_one(" COORD ")\n", COORDARGS(map_pos));
|
||||
df::map_block* block = Maps::getTileBlock(map_pos);
|
||||
// we calculate the position inside the block*
|
||||
df::coord local(map_pos);
|
||||
local.x = local.x % 16;
|
||||
local.y = local.y % 16;
|
||||
df::tile_occupancy &tile_occupancy = block->occupancy[Coord(local)];
|
||||
// ensure that we aren't on the top-most layers
|
||||
if (map_pos.z < mapz - 3) {
|
||||
// do we already know whether to set marker mode?
|
||||
if (set_marker_mode) {
|
||||
DEBUG(manager).print(" -> marker_mode\n");
|
||||
// if enabling marker mode, just do it
|
||||
if (marker_mode) {
|
||||
tile_occupancy.bits.dig_marked = marker_mode;
|
||||
return true;
|
||||
}
|
||||
// if activating designation, check if it is safe to dig or not a channel designation
|
||||
if (!is_channel_designation(block->designation[Coord(local)]) || is_safe_to_dig_down(map_pos)) {
|
||||
if (!block->flags.bits.designated) {
|
||||
block->flags.bits.designated = true;
|
||||
}
|
||||
tile_occupancy.bits.dig_marked = false;
|
||||
TileCache::Get().cache(map_pos, block->tiletype[Coord(local)]);
|
||||
}
|
||||
return false;
|
||||
|
||||
} else {
|
||||
// next search for the designation priority
|
||||
DEBUG(manager).print(" if(has_groups_above())\n");
|
||||
// check that the group has no incomplete groups directly above it
|
||||
if (has_group_above(groups, map_pos) || !is_safe_to_dig_down(map_pos)) {
|
||||
DEBUG(manager).print(" has_groups_above: setting marker mode\n");
|
||||
tile_occupancy.bits.dig_marked = true;
|
||||
if (jobs.count(map_pos)) {
|
||||
jobs.erase(map_pos);
|
||||
}
|
||||
WARN(manager).print(" <- manage_one() exits normally\n");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// if we are though, it should be totally safe to dig
|
||||
tile_occupancy.bits.dig_marked = false;
|
||||
}
|
||||
WARN(manager).print(" <- manage_one() exits normally\n");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void ChannelManager::mark_done(const df::coord &map_pos) {
|
||||
groups.remove(map_pos);
|
||||
jobs.erase(map_pos);
|
||||
CSP::dignow_queue.erase(map_pos);
|
||||
TileCache::Get().uncache(map_pos);
|
||||
}
|
@ -0,0 +1,668 @@
|
||||
/* Prevent channeling down into known open space.
|
||||
Author: Josh Cooper
|
||||
Created: Aug. 4 2020
|
||||
Updated: Nov. 6 2022
|
||||
|
||||
Enable plugin:
|
||||
-> build groups
|
||||
-> manage designations
|
||||
|
||||
Unpause event:
|
||||
-> build groups
|
||||
-> manage designations
|
||||
|
||||
Manage Designation(s):
|
||||
-> for each group in groups:
|
||||
-> does any tile in group have a group above
|
||||
-> Yes: set entire group to marker mode
|
||||
-> No: activate entire group (still checks is_safe_to_dig_down before activating each designation)
|
||||
|
||||
Job started event:
|
||||
-> validate job type (channel)
|
||||
-> check pathing:
|
||||
-> Can: add job/worker to tracking
|
||||
-> Can: set tile to restricted
|
||||
-> Cannot: remove worker
|
||||
-> Cannot: insta-dig & delete job
|
||||
-> Cannot: set designation to Marker Mode (no insta-digging)
|
||||
|
||||
OnUpdate:
|
||||
-> check worker location:
|
||||
-> CanFall: check if a fall would be safe:
|
||||
-> Safe: do nothing
|
||||
-> Unsafe: remove worker
|
||||
-> Unsafe: insta-dig & delete job (presumes the job is only accessible from directly on the tile)
|
||||
-> Unsafe: set designation to Marker Mode (no insta-digging)
|
||||
-> check tile occupancy:
|
||||
-> HasUnit: check if a fall would be safe:
|
||||
-> Safe: do nothing, let them fall
|
||||
-> Unsafe: remove worker for 1 tick (test if this "pauses" or cancels the job)
|
||||
-> Unsafe: Add feature to teleport unit?
|
||||
|
||||
Job completed event:
|
||||
-> validate job type (channel)
|
||||
-> verify completion:
|
||||
-> IsOpenSpace: mark done
|
||||
-> IsOpenSpace: manage tile below
|
||||
-> NotOpenSpace: check for designation
|
||||
-> HasDesignation: do nothing
|
||||
-> NoDesignation: mark done (erases from group)
|
||||
-> NoDesignation: manage tile below
|
||||
*/
|
||||
|
||||
#include <plugin.h>
|
||||
#include <inlines.h>
|
||||
#include <channel-manager.h>
|
||||
#include <tile-cache.h>
|
||||
|
||||
#include <Debug.h>
|
||||
#include <LuaTools.h>
|
||||
#include <LuaWrapper.h>
|
||||
#include <PluginManager.h>
|
||||
#include <modules/EventManager.h>
|
||||
#include <modules/Units.h>
|
||||
#include <df/world.h>
|
||||
#include <df/report.h>
|
||||
#include <df/tile_traffic.h>
|
||||
#include <df/block_square_event_designation_priorityst.h>
|
||||
|
||||
#include <cinttypes>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
// Debugging
|
||||
namespace DFHack {
|
||||
DBG_DECLARE(channelsafely, plugin, DebugCategory::LINFO);
|
||||
DBG_DECLARE(channelsafely, monitor, DebugCategory::LERROR);
|
||||
DBG_DECLARE(channelsafely, manager, DebugCategory::LERROR);
|
||||
DBG_DECLARE(channelsafely, groups, DebugCategory::LERROR);
|
||||
DBG_DECLARE(channelsafely, jobs, DebugCategory::LERROR);
|
||||
}
|
||||
|
||||
DFHACK_PLUGIN("channel-safely");
|
||||
DFHACK_PLUGIN_IS_ENABLED(enabled);
|
||||
REQUIRE_GLOBAL(world);
|
||||
|
||||
namespace EM = EventManager;
|
||||
using namespace DFHack;
|
||||
using namespace EM::EventType;
|
||||
|
||||
int32_t mapx, mapy, mapz;
|
||||
Configuration config;
|
||||
PersistentDataItem psetting;
|
||||
PersistentDataItem pfeature;
|
||||
const std::string FCONFIG_KEY = std::string(plugin_name) + "/feature";
|
||||
const std::string SCONFIG_KEY = std::string(plugin_name) + "/setting";
|
||||
|
||||
enum FeatureConfigData {
|
||||
VISION,
|
||||
MONITOR,
|
||||
RESURRECT,
|
||||
INSTADIG
|
||||
};
|
||||
|
||||
enum SettingConfigData {
|
||||
REFRESH_RATE,
|
||||
MONITOR_RATE,
|
||||
IGNORE_THRESH,
|
||||
FALL_THRESH
|
||||
};
|
||||
|
||||
// dig-now.cpp
|
||||
df::coord simulate_fall(const df::coord &pos) {
|
||||
df::coord resting_pos(pos);
|
||||
|
||||
while (Maps::ensureTileBlock(resting_pos)) {
|
||||
df::tiletype tt = *Maps::getTileType(resting_pos);
|
||||
df::tiletype_shape_basic basic_shape = tileShapeBasic(tileShape(tt));
|
||||
if (isWalkable(tt) && basic_shape != df::tiletype_shape_basic::Open)
|
||||
break;
|
||||
--resting_pos.z;
|
||||
}
|
||||
|
||||
return resting_pos;
|
||||
}
|
||||
|
||||
df::coord simulate_area_fall(const df::coord &pos) {
|
||||
df::coord neighbours[8]{};
|
||||
get_neighbours(pos, neighbours);
|
||||
df::coord lowest = simulate_fall(pos);
|
||||
for (auto p : neighbours) {
|
||||
auto nlow = simulate_fall(p);
|
||||
if (nlow.z < lowest.z) {
|
||||
lowest = nlow;
|
||||
}
|
||||
}
|
||||
return lowest;
|
||||
}
|
||||
|
||||
// executes dig designations for the specified tile coordinates
|
||||
inline bool dig_now(color_ostream &out, const df::coord &map_pos) {
|
||||
bool ret = false;
|
||||
|
||||
lua_State* state = Lua::Core::State;
|
||||
static const char* module_name = "plugins.dig-now";
|
||||
static const char* fn_name = "dig_now_tile";
|
||||
// the stack layout isn't likely to change, ever
|
||||
static auto args_lambda = [&map_pos](lua_State* L) {
|
||||
Lua::Push(L, map_pos);
|
||||
};
|
||||
static auto res_lambda = [&ret](lua_State* L) {
|
||||
ret = lua_toboolean(L, -1);
|
||||
};
|
||||
|
||||
Lua::StackUnwinder top(state);
|
||||
Lua::CallLuaModuleFunction(out, state, module_name, fn_name, 1, 1, args_lambda, res_lambda);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// fully heals the unit specified, resurrecting if need be
|
||||
inline void resurrect(color_ostream &out, const int32_t &unit) {
|
||||
std::vector<std::string> params{"-r", "--unit", std::to_string(unit)};
|
||||
Core::getInstance().runCommand(out,"full-heal", params);
|
||||
}
|
||||
|
||||
namespace CSP {
|
||||
std::unordered_map<df::unit*, int32_t> endangered_units;
|
||||
std::unordered_map<df::job*, int32_t> job_id_map;
|
||||
std::unordered_map<int32_t, df::job*> active_jobs;
|
||||
std::unordered_map<int32_t, df::unit*> active_workers;
|
||||
|
||||
std::unordered_map<int32_t, df::coord> last_safe;
|
||||
std::unordered_set<df::coord> dignow_queue;
|
||||
|
||||
void ClearData() {
|
||||
ChannelManager::Get().destroy_groups();
|
||||
dignow_queue.clear();
|
||||
last_safe.clear();
|
||||
endangered_units.clear();
|
||||
active_workers.clear();
|
||||
active_jobs.clear();
|
||||
job_id_map.clear();
|
||||
}
|
||||
|
||||
void SaveSettings() {
|
||||
if (pfeature.isValid() && psetting.isValid()) {
|
||||
try {
|
||||
pfeature.ival(MONITOR) = config.monitor_active;
|
||||
pfeature.ival(VISION) = config.require_vision;
|
||||
pfeature.ival(INSTADIG) = config.insta_dig;
|
||||
pfeature.ival(RESURRECT) = config.resurrect;
|
||||
|
||||
psetting.ival(REFRESH_RATE) = config.refresh_freq;
|
||||
psetting.ival(MONITOR_RATE) = config.monitor_freq;
|
||||
psetting.ival(IGNORE_THRESH) = config.ignore_threshold;
|
||||
psetting.ival(FALL_THRESH) = config.fall_threshold;
|
||||
} catch (std::exception &e) {
|
||||
ERR(plugin).print("%s\n", e.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LoadSettings() {
|
||||
pfeature = World::GetPersistentData(FCONFIG_KEY);
|
||||
psetting = World::GetPersistentData(SCONFIG_KEY);
|
||||
|
||||
if (!pfeature.isValid() || !psetting.isValid()) {
|
||||
pfeature = World::AddPersistentData(FCONFIG_KEY);
|
||||
psetting = World::AddPersistentData(SCONFIG_KEY);
|
||||
SaveSettings();
|
||||
} else {
|
||||
try {
|
||||
config.monitor_active = pfeature.ival(MONITOR);
|
||||
config.require_vision = pfeature.ival(VISION);
|
||||
config.insta_dig = pfeature.ival(INSTADIG);
|
||||
config.resurrect = pfeature.ival(RESURRECT);
|
||||
|
||||
config.ignore_threshold = psetting.ival(IGNORE_THRESH);
|
||||
config.fall_threshold = psetting.ival(FALL_THRESH);
|
||||
config.refresh_freq = psetting.ival(REFRESH_RATE);
|
||||
config.monitor_freq = psetting.ival(MONITOR_RATE);
|
||||
} catch (std::exception &e) {
|
||||
ERR(plugin).print("%s\n", e.what());
|
||||
}
|
||||
}
|
||||
active_workers.clear();
|
||||
}
|
||||
|
||||
void UnpauseEvent(){
|
||||
CoreSuspender suspend; // we need exclusive access to df memory and this call stack doesn't already have a lock
|
||||
INFO(monitor).print("UnpauseEvent()\n");
|
||||
ChannelManager::Get().build_groups();
|
||||
ChannelManager::Get().manage_groups();
|
||||
ChannelManager::Get().debug();
|
||||
INFO(monitor).print("UnpauseEvent() exits\n");
|
||||
}
|
||||
|
||||
void JobStartedEvent(color_ostream &out, void* j) {
|
||||
if (enabled && World::isFortressMode() && Maps::IsValid()) {
|
||||
INFO(jobs).print("JobStartedEvent()\n");
|
||||
auto job = (df::job*) j;
|
||||
// validate job type
|
||||
if (ChannelManager::Get().exists(job->pos)) {
|
||||
WARN(jobs).print(" valid channel job:\n");
|
||||
df::unit* worker = Job::getWorker(job);
|
||||
// there is a valid worker (living citizen) on the job? right..
|
||||
if (worker && Units::isAlive(worker) && Units::isCitizen(worker)) {
|
||||
DEBUG(jobs).print(" valid worker:\n");
|
||||
// track workers on jobs
|
||||
df::coord &pos = job->pos;
|
||||
WARN(jobs).print(" -> Starting job at (" COORD ")\n", COORDARGS(pos));
|
||||
if (config.monitor_active || config.resurrect) {
|
||||
job_id_map.emplace(job, job->id);
|
||||
active_jobs.emplace(job->id, job);
|
||||
active_workers[job->id] = worker;
|
||||
if (config.resurrect) {
|
||||
// this is the only place we can be 100% sure of "safety"
|
||||
// (excluding deadly enemies that will have arrived)
|
||||
last_safe[worker->id] = worker->pos;
|
||||
}
|
||||
}
|
||||
// set tile to restricted
|
||||
TRACE(jobs).print(" setting job tile to restricted\n");
|
||||
Maps::getTileDesignation(job->pos)->bits.traffic = df::tile_traffic::Restricted;
|
||||
}
|
||||
}
|
||||
INFO(jobs).print(" <- JobStartedEvent() exits normally\n");
|
||||
}
|
||||
}
|
||||
|
||||
void JobCompletedEvent(color_ostream &out, void* j) {
|
||||
if (enabled && World::isFortressMode() && Maps::IsValid()) {
|
||||
INFO(jobs).print("JobCompletedEvent()\n");
|
||||
auto job = (df::job*) j;
|
||||
// we only care if the job is a channeling one
|
||||
if (ChannelManager::Get().exists(job->pos)) {
|
||||
// check job outcome
|
||||
auto block = Maps::getTileBlock(job->pos);
|
||||
df::coord local(job->pos);
|
||||
local.x = local.x % 16;
|
||||
local.y = local.y % 16;
|
||||
// verify completion
|
||||
if (TileCache::Get().hasChanged(job->pos, block->tiletype[Coord(local)])) {
|
||||
// the job can be considered done
|
||||
df::coord below(job->pos);
|
||||
below.z--;
|
||||
WARN(jobs).print(" -> (" COORD ") is marked done, managing group below.\n", COORDARGS(job->pos));
|
||||
// mark done and manage below
|
||||
block->designation[Coord(local)].bits.traffic = df::tile_traffic::Normal;
|
||||
ChannelManager::Get().mark_done(job->pos);
|
||||
ChannelManager::Get().manage_group(below);
|
||||
ChannelManager::Get().debug();
|
||||
if (config.resurrect) {
|
||||
// this is the only place we can be 100% sure of "safety"
|
||||
// (excluding deadly enemies that will have arrived)
|
||||
if (active_workers.count(job->id)) {
|
||||
df::unit* worker = active_workers[job->id];
|
||||
last_safe[worker->id] = worker->pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
// clean up
|
||||
auto jp = active_jobs[job->id];
|
||||
job_id_map.erase(jp);
|
||||
active_workers.erase(job->id);
|
||||
active_jobs.erase(job->id);
|
||||
}
|
||||
INFO(jobs).print("JobCompletedEvent() exits\n");
|
||||
}
|
||||
}
|
||||
|
||||
void NewReportEvent(color_ostream &out, void* r) {
|
||||
int32_t tick = df::global::world->frame_counter;
|
||||
auto report_id = (int32_t)(intptr_t(r));
|
||||
if (df::global::world) {
|
||||
std::vector<df::report*> &reports = df::global::world->status.reports;
|
||||
size_t idx = -1;
|
||||
idx = df::report::binsearch_index(reports, report_id);
|
||||
df::report* report = reports.at(idx);
|
||||
switch (report->type) {
|
||||
case announcement_type::CANCEL_JOB:
|
||||
if (config.insta_dig) {
|
||||
if (report->text.find("cancels Dig") != std::string::npos) {
|
||||
dignow_queue.emplace(report->pos);
|
||||
} else if (report->text.find("path") != std::string::npos) {
|
||||
dignow_queue.emplace(report->pos);
|
||||
}
|
||||
DEBUG(plugin).print("%d, pos: " COORD ", pos2: " COORD "\n%s\n", report_id, COORDARGS(report->pos),
|
||||
COORDARGS(report->pos2), report->text.c_str());
|
||||
}
|
||||
break;
|
||||
case announcement_type::CAVE_COLLAPSE:
|
||||
if (config.resurrect) {
|
||||
DEBUG(plugin).print("CAVE IN\n%d, pos: " COORD ", pos2: " COORD "\n%s\n", report_id, COORDARGS(report->pos),
|
||||
COORDARGS(report->pos2), report->text.c_str());
|
||||
|
||||
df::coord below = report->pos;
|
||||
below.z -= 1;
|
||||
below = simulate_area_fall(below);
|
||||
df::coord areaMin{report->pos};
|
||||
df::coord areaMax{areaMin};
|
||||
areaMin.x -= 15;
|
||||
areaMin.y -= 15;
|
||||
areaMax.x += 15;
|
||||
areaMax.y += 15;
|
||||
areaMin.z = below.z;
|
||||
areaMax.z += 1;
|
||||
std::vector<df::unit*> units;
|
||||
Units::getUnitsInBox(units, COORDARGS(areaMin), COORDARGS(areaMax));
|
||||
for (auto unit: units) {
|
||||
endangered_units[unit] = tick;
|
||||
DEBUG(plugin).print(" [id %d] was near a cave in.\n", unit->id);
|
||||
}
|
||||
for (auto unit : world->units.all) {
|
||||
if (last_safe.count(unit->id)) {
|
||||
endangered_units[unit] = tick;
|
||||
DEBUG(plugin).print(" [id %d] is/was a worker, we'll track them too.\n", unit->id);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OnUpdate(color_ostream &out) {
|
||||
static auto print_res_msg = [](df::unit* unit) {
|
||||
WARN(plugin).print("Channel-Safely: Resurrecting..\n [id: %d]\n", unit->id);
|
||||
};
|
||||
if (enabled && World::isFortressMode() && Maps::IsValid() && !World::ReadPauseState()) {
|
||||
static int32_t last_tick = df::global::world->frame_counter;
|
||||
static int32_t last_monitor_tick = df::global::world->frame_counter;
|
||||
static int32_t last_refresh_tick = df::global::world->frame_counter;
|
||||
static int32_t last_resurrect_tick = df::global::world->frame_counter;
|
||||
int32_t tick = df::global::world->frame_counter;
|
||||
|
||||
// Refreshing the group data with full scanning
|
||||
if (tick - last_refresh_tick >= config.refresh_freq) {
|
||||
last_refresh_tick = tick;
|
||||
TRACE(monitor).print("OnUpdate() refreshing now\n");
|
||||
if (config.insta_dig) {
|
||||
TRACE(monitor).print(" -> evaluate dignow queue\n");
|
||||
for (auto iter = dignow_queue.begin(); iter != dignow_queue.end();) {
|
||||
dig_now(out, *iter); // teleports units to the bottom of a simulated fall
|
||||
iter = dignow_queue.erase(iter);
|
||||
DEBUG(plugin).print(">INSTA-DIGGING<\n");
|
||||
}
|
||||
}
|
||||
UnpauseEvent();
|
||||
TRACE(monitor).print("OnUpdate() refresh done\n");
|
||||
}
|
||||
|
||||
// Clean up stale df::job*
|
||||
if ((config.monitor_active || config.resurrect) && tick - last_tick >= 1) {
|
||||
last_tick = tick;
|
||||
// make note of valid jobs
|
||||
std::unordered_map<int32_t, df::job*> valid_jobs;
|
||||
for (df::job_list_link* link = &df::global::world->jobs.list; link != nullptr; link = link->next) {
|
||||
df::job* job = link->item;
|
||||
if (job && active_jobs.count(job->id)) {
|
||||
valid_jobs.emplace(job->id, job);
|
||||
}
|
||||
}
|
||||
|
||||
// erase the active jobs that aren't valid
|
||||
std::unordered_set<df::job*> erase;
|
||||
map_value_difference(active_jobs, valid_jobs, erase);
|
||||
for (auto j : erase) {
|
||||
auto id = job_id_map[j];
|
||||
job_id_map.erase(j);
|
||||
active_jobs.erase(id);
|
||||
active_workers.erase(id);
|
||||
}
|
||||
}
|
||||
|
||||
// Monitoring Active and Resurrecting Dead
|
||||
if (config.monitor_active && tick - last_monitor_tick >= config.monitor_freq) {
|
||||
last_monitor_tick = tick;
|
||||
TRACE(monitor).print("OnUpdate() monitoring now\n");
|
||||
|
||||
// iterate active jobs
|
||||
for (auto pair: active_jobs) {
|
||||
df::job* job = pair.second;
|
||||
df::unit* unit = active_workers[job->id];
|
||||
if (!unit) continue;
|
||||
if (!Maps::isValidTilePos(job->pos)) continue;
|
||||
TRACE(monitor).print(" -> check for job in tracking\n");
|
||||
if (Units::isAlive(unit)) {
|
||||
if (!config.monitor_active) continue;
|
||||
TRACE(monitor).print(" -> compare positions of worker and job\n");
|
||||
|
||||
// save position
|
||||
if (unit->pos != job->pos && isFloorTerrain(*Maps::getTileType(unit->pos))) {
|
||||
// worker is probably safe right now
|
||||
continue;
|
||||
}
|
||||
|
||||
// check for fall safety
|
||||
if (unit->pos == job->pos && !is_safe_fall(job->pos)) {
|
||||
// unsafe
|
||||
WARN(monitor).print(" -> unsafe job\n");
|
||||
Job::removeWorker(job);
|
||||
|
||||
// decide to insta-dig or marker mode
|
||||
if (config.insta_dig) {
|
||||
// delete the job
|
||||
Job::removeJob(job);
|
||||
// queue digging the job instantly
|
||||
dignow_queue.emplace(job->pos);
|
||||
DEBUG(monitor).print(" -> insta-dig\n");
|
||||
} else if (config.resurrect) {
|
||||
endangered_units.emplace(unit, tick);
|
||||
} else {
|
||||
// set marker mode
|
||||
Maps::getTileOccupancy(job->pos)->bits.dig_marked = true;
|
||||
|
||||
// prevent algorithm from re-enabling designation
|
||||
for (auto &be: Maps::getBlock(job->pos)->block_events) { ;
|
||||
if (auto bsedp = virtual_cast<df::block_square_event_designation_priorityst>(
|
||||
be)) {
|
||||
df::coord local(job->pos);
|
||||
local.x = local.x % 16;
|
||||
local.y = local.y % 16;
|
||||
bsedp->priority[Coord(local)] = config.ignore_threshold * 1000 + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
DEBUG(monitor).print(" -> set marker mode\n");
|
||||
}
|
||||
}
|
||||
} else if (config.resurrect) {
|
||||
resurrect(out, unit->id);
|
||||
if (last_safe.count(unit->id)) {
|
||||
df::coord lowest = simulate_fall(last_safe[unit->id]);
|
||||
Units::teleport(unit, lowest);
|
||||
}
|
||||
print_res_msg(unit);
|
||||
}
|
||||
}
|
||||
TRACE(monitor).print("OnUpdate() monitoring done\n");
|
||||
}
|
||||
|
||||
// Resurrect Dead Workers
|
||||
if (config.resurrect && tick - last_resurrect_tick >= 1) {
|
||||
last_resurrect_tick = tick;
|
||||
|
||||
// clean up any "endangered" workers that have been tracked 100 ticks or more
|
||||
for (auto iter = endangered_units.begin(); iter != endangered_units.end();) {
|
||||
if (tick - iter->second >= 1200) { //keep watch 1 day
|
||||
DEBUG(plugin).print("It has been one day since [id %d]'s last incident.\n", iter->first->id);
|
||||
iter = endangered_units.erase(iter);
|
||||
continue;
|
||||
}
|
||||
++iter;
|
||||
}
|
||||
|
||||
// resurrect any dead units
|
||||
for (auto pair : endangered_units) {
|
||||
auto unit = pair.first;
|
||||
if (!Units::isAlive(unit)) {
|
||||
resurrect(out, unit->id);
|
||||
if (last_safe.count(unit->id)) {
|
||||
df::coord lowest = simulate_fall(last_safe[unit->id]);
|
||||
Units::teleport(unit, lowest);
|
||||
}
|
||||
print_res_msg(unit);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
command_result channel_safely(color_ostream &out, std::vector<std::string> ¶meters);
|
||||
|
||||
DFhackCExport command_result plugin_init(color_ostream &out, std::vector<PluginCommand> &commands) {
|
||||
commands.push_back(PluginCommand("channel-safely",
|
||||
"Automatically manage channel designations.",
|
||||
channel_safely,
|
||||
false));
|
||||
return CR_OK;
|
||||
}
|
||||
|
||||
DFhackCExport command_result plugin_shutdown(color_ostream &out) {
|
||||
EM::unregisterAll(plugin_self);
|
||||
return CR_OK;
|
||||
}
|
||||
|
||||
DFhackCExport command_result plugin_load_data (color_ostream &out) {
|
||||
CSP::LoadSettings();
|
||||
if (enabled) {
|
||||
std::vector<std::string> params;
|
||||
channel_safely(out, params);
|
||||
}
|
||||
return DFHack::CR_OK;
|
||||
}
|
||||
|
||||
DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) {
|
||||
if (enable && !enabled) {
|
||||
// register events to check jobs / update tracking
|
||||
EM::EventHandler jobStartHandler(CSP::JobStartedEvent, 0);
|
||||
EM::EventHandler jobCompletionHandler(CSP::JobCompletedEvent, 0);
|
||||
EM::EventHandler reportHandler(CSP::NewReportEvent, 0);
|
||||
EM::registerListener(EventType::REPORT, reportHandler, plugin_self);
|
||||
EM::registerListener(EventType::JOB_STARTED, jobStartHandler, plugin_self);
|
||||
EM::registerListener(EventType::JOB_COMPLETED, jobCompletionHandler, plugin_self);
|
||||
// manage designations to start off (first time building groups [very important])
|
||||
out.print("channel-safely: enabled!\n");
|
||||
CSP::UnpauseEvent();
|
||||
} else if (!enable) {
|
||||
// don't need the groups if the plugin isn't going to be enabled
|
||||
EM::unregisterAll(plugin_self);
|
||||
out.print("channel-safely: disabled!\n");
|
||||
}
|
||||
enabled = enable;
|
||||
return CR_OK;
|
||||
}
|
||||
|
||||
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) {
|
||||
switch (event) {
|
||||
case SC_UNPAUSED:
|
||||
if (enabled && World::isFortressMode() && Maps::IsValid()) {
|
||||
// manage all designations on unpause
|
||||
CSP::UnpauseEvent();
|
||||
}
|
||||
break;
|
||||
case SC_MAP_LOADED:
|
||||
// cache the map size
|
||||
Maps::getSize(mapx, mapy, mapz);
|
||||
case SC_WORLD_LOADED:
|
||||
case SC_WORLD_UNLOADED:
|
||||
case SC_MAP_UNLOADED:
|
||||
CSP::ClearData();
|
||||
break;
|
||||
default:
|
||||
return DFHack::CR_OK;
|
||||
}
|
||||
return DFHack::CR_OK;
|
||||
}
|
||||
|
||||
DFhackCExport command_result plugin_onupdate(color_ostream &out, state_change_event event) {
|
||||
CSP::OnUpdate(out);
|
||||
return DFHack::CR_OK;
|
||||
}
|
||||
|
||||
command_result channel_safely(color_ostream &out, std::vector<std::string> ¶meters) {
|
||||
if (!parameters.empty()) {
|
||||
if (parameters[0] == "runonce") {
|
||||
CSP::UnpauseEvent();
|
||||
return DFHack::CR_OK;
|
||||
} else if (parameters[0] == "rebuild") {
|
||||
ChannelManager::Get().destroy_groups();
|
||||
ChannelManager::Get().build_groups();
|
||||
}
|
||||
if (parameters.size() >= 2 && parameters.size() <= 3) {
|
||||
bool state = false;
|
||||
bool set = false;
|
||||
if (parameters[0] == "enable") {
|
||||
state = true;
|
||||
} else if (parameters[0] == "disable") {
|
||||
state = false;
|
||||
} else if (parameters[0] == "set") {
|
||||
set = true;
|
||||
} else {
|
||||
return DFHack::CR_WRONG_USAGE;
|
||||
}
|
||||
try {
|
||||
if(parameters[1] == "monitor"){
|
||||
if (state != config.monitor_active) {
|
||||
config.monitor_active = state;
|
||||
// if this is a fresh start
|
||||
if (state && !config.resurrect) {
|
||||
// we need a fresh start
|
||||
CSP::active_workers.clear();
|
||||
}
|
||||
}
|
||||
} else if (parameters[1] == "require-vision") {
|
||||
config.require_vision = state;
|
||||
} else if (parameters[1] == "insta-dig") {
|
||||
config.insta_dig = state;
|
||||
} else if (parameters[1] == "resurrect") {
|
||||
if (state != config.resurrect) {
|
||||
config.resurrect = state;
|
||||
// if this is a fresh start
|
||||
if (state && !config.monitor_active) {
|
||||
// we need a fresh start
|
||||
CSP::active_workers.clear();
|
||||
}
|
||||
}
|
||||
} else if (parameters[1] == "refresh-freq" && set && parameters.size() == 3) {
|
||||
config.refresh_freq = std::abs(std::stol(parameters[2]));
|
||||
} else if (parameters[1] == "monitor-freq" && set && parameters.size() == 3) {
|
||||
config.monitor_freq = std::abs(std::stol(parameters[2]));
|
||||
} else if (parameters[1] == "ignore-threshold" && set && parameters.size() == 3) {
|
||||
config.ignore_threshold = std::abs(std::stol(parameters[2]));
|
||||
} else if (parameters[1] == "fall-threshold" && set && parameters.size() == 3) {
|
||||
uint8_t t = std::abs(std::stol(parameters[2]));
|
||||
if (t > 0) {
|
||||
config.fall_threshold = t;
|
||||
} else {
|
||||
out.printerr("fall-threshold must have a value greater than 0 or the plugin does a lot of nothing.\n");
|
||||
return DFHack::CR_FAILURE;
|
||||
}
|
||||
} else {
|
||||
return DFHack::CR_WRONG_USAGE;
|
||||
}
|
||||
} catch (const std::exception &e) {
|
||||
out.printerr("%s\n", e.what());
|
||||
return DFHack::CR_FAILURE;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out.print("Channel-Safely is %s\n", enabled ? "ENABLED." : "DISABLED.");
|
||||
out.print(" FEATURES:\n");
|
||||
out.print(" %-20s\t%s\n", "monitor-active: ", config.monitor_active ? "on." : "off.");
|
||||
out.print(" %-20s\t%s\n", "require-vision: ", config.require_vision ? "on." : "off.");
|
||||
out.print(" %-20s\t%s\n", "insta-dig: ", config.insta_dig ? "on." : "off.");
|
||||
out.print(" %-20s\t%s\n", "resurrect: ", config.resurrect ? "on." : "off.");
|
||||
out.print(" SETTINGS:\n");
|
||||
out.print(" %-20s\t%" PRIi32 "\n", "refresh-freq: ", config.refresh_freq);
|
||||
out.print(" %-20s\t%" PRIi32 "\n", "monitor-freq: ", config.monitor_freq);
|
||||
out.print(" %-20s\t%" PRIu8 "\n", "ignore-threshold: ", config.ignore_threshold);
|
||||
out.print(" %-20s\t%" PRIu8 "\n", "fall-threshold: ", config.fall_threshold);
|
||||
}
|
||||
CSP::SaveSettings();
|
||||
return DFHack::CR_OK;
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
#include "plugin.h"
|
||||
#include "channel-jobs.h"
|
||||
|
||||
#include <df/map_block.h>
|
||||
#include <df/coord.h>
|
||||
#include <modules/EventManager.h> //hash functions (they should probably get moved at this point, the ones that aren't specifically for EM anyway)
|
||||
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
using namespace DFHack;
|
||||
|
||||
using Group = std::unordered_set<df::coord>;
|
||||
using Groups = std::vector<Group>;
|
||||
|
||||
/* Used to build groups of adjacent channel designations/jobs
|
||||
* groups_map: maps coordinates to a group index in `groups`
|
||||
* groups: list of Groups
|
||||
* Group: used to track designations which are connected through adjacency to one another (a group cannot span Z)
|
||||
* Note: a designation plan may become unsafe if the jobs aren't completed in a specific order;
|
||||
* the easiest way to programmatically ensure safety is to..
|
||||
* lock overlapping groups directly adjacent across Z until the above groups are complete, or no longer overlap
|
||||
* groups may no longer overlap if the adjacent designations are completed, but requires a rebuild of groups
|
||||
* jobs: list of coordinates with channel jobs associated to them
|
||||
*/
|
||||
class ChannelGroups {
|
||||
private:
|
||||
using GroupBlocks = std::unordered_set<df::map_block*>;
|
||||
using GroupsMap = std::unordered_map<df::coord, int>;
|
||||
GroupBlocks group_blocks;
|
||||
GroupsMap groups_map;
|
||||
Groups groups;
|
||||
ChannelJobs &jobs;
|
||||
std::set<int> free_spots;
|
||||
protected:
|
||||
void add(const df::coord &map_pos);
|
||||
public:
|
||||
explicit ChannelGroups(ChannelJobs &jobs) : jobs(jobs) { groups.reserve(200); }
|
||||
void scan_one(const df::coord &map_pos);
|
||||
void scan();
|
||||
void clear();
|
||||
void remove(const df::coord &map_pos);
|
||||
Groups::const_iterator find(const df::coord &map_pos) const;
|
||||
Groups::const_iterator begin() const;
|
||||
Groups::const_iterator end() const;
|
||||
size_t count(const df::coord &map_pos) const;
|
||||
void debug_groups();
|
||||
void debug_map();
|
||||
};
|
@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
#include <PluginManager.h>
|
||||
#include <modules/Job.h>
|
||||
#include <modules/EventManager.h> //hash functions (they should probably get moved at this point, the ones that aren't specifically for EM anyway)
|
||||
#include <df/world.h>
|
||||
#include <df/job.h>
|
||||
|
||||
#include <unordered_set>
|
||||
|
||||
using namespace DFHack;
|
||||
|
||||
/* Used to read/store/iterate channel digging jobs
|
||||
* jobs: list of coordinates with channel jobs associated to them
|
||||
* load_channel_jobs: iterates world->jobs.list to find channel jobs and adds them into the `jobs` map
|
||||
* clear: empties the container
|
||||
* erase: finds a job corresponding to a coord, removes the mapping in jobs, and calls Job::removeJob, then returns an iterator following the element removed
|
||||
* find: returns an iterator to a job if one exists for a map coordinate
|
||||
* begin: returns jobs.begin()
|
||||
* end: returns jobs.end()
|
||||
*/
|
||||
class ChannelJobs {
|
||||
private:
|
||||
friend class ChannelGroup;
|
||||
|
||||
using Jobs = std::unordered_set<df::coord>; // job* will exist until it is complete, and likely beyond
|
||||
Jobs locations;
|
||||
public:
|
||||
void load_channel_jobs();
|
||||
void clear() {
|
||||
locations.clear();
|
||||
}
|
||||
int count(const df::coord &map_pos) const { return locations.count(map_pos); }
|
||||
Jobs::iterator erase(const df::coord &map_pos) {
|
||||
auto iter = locations.find(map_pos);
|
||||
if (iter != locations.end()) {
|
||||
return locations.erase(iter);
|
||||
}
|
||||
return iter;
|
||||
}
|
||||
Jobs::const_iterator find(const df::coord &map_pos) const { return locations.find(map_pos); }
|
||||
Jobs::const_iterator begin() const { return locations.begin(); }
|
||||
Jobs::const_iterator end() const { return locations.end(); }
|
||||
};
|
@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
#include <PluginManager.h>
|
||||
#include <modules/World.h>
|
||||
#include <modules/Maps.h>
|
||||
#include <modules/Job.h>
|
||||
#include <df/map_block.h>
|
||||
#include "channel-groups.h"
|
||||
#include "plugin.h"
|
||||
|
||||
using namespace DFHack;
|
||||
|
||||
// Uses GroupData to detect an unsafe work environment
|
||||
class ChannelManager {
|
||||
private:
|
||||
ChannelJobs jobs;
|
||||
ChannelManager()= default;
|
||||
protected:
|
||||
public:
|
||||
ChannelGroups groups = ChannelGroups(jobs);
|
||||
|
||||
static ChannelManager& Get(){
|
||||
static ChannelManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
void build_groups() { groups.scan(); debug(); }
|
||||
void destroy_groups() { groups.clear(); debug(); }
|
||||
void manage_groups();
|
||||
void manage_group(const df::coord &map_pos, bool set_marker_mode = false, bool marker_mode = false);
|
||||
void manage_group(const Group &group, bool set_marker_mode = false, bool marker_mode = false);
|
||||
bool manage_one(const Group &group, const df::coord &map_pos, bool set_marker_mode = false, bool marker_mode = false);
|
||||
void mark_done(const df::coord &map_pos);
|
||||
bool exists(const df::coord &map_pos) const { return groups.count(map_pos); }
|
||||
void debug() {
|
||||
DEBUG(groups).print(" DEBUGGING GROUPS:\n");
|
||||
groups.debug_groups();
|
||||
groups.debug_map();
|
||||
}
|
||||
};
|
@ -0,0 +1,196 @@
|
||||
#pragma once
|
||||
#include "plugin.h"
|
||||
#include "channel-manager.h"
|
||||
|
||||
#include <modules/Maps.h>
|
||||
#include <df/job.h>
|
||||
#include <TileTypes.h>
|
||||
|
||||
#include <cinttypes>
|
||||
#include <unordered_set>
|
||||
|
||||
#define Coord(id) id.x][id.y
|
||||
#define COORD "%" PRIi16 " %" PRIi16 " %" PRIi16
|
||||
#define COORDARGS(id) id.x, id.y, id.z
|
||||
|
||||
namespace CSP {
|
||||
extern std::unordered_set<df::coord> dignow_queue;
|
||||
}
|
||||
|
||||
inline void get_neighbours(const df::coord &map_pos, df::coord(&neighbours)[8]) {
|
||||
neighbours[0] = map_pos;
|
||||
neighbours[1] = map_pos;
|
||||
neighbours[2] = map_pos;
|
||||
neighbours[3] = map_pos;
|
||||
neighbours[4] = map_pos;
|
||||
neighbours[5] = map_pos;
|
||||
neighbours[6] = map_pos;
|
||||
neighbours[7] = map_pos;
|
||||
neighbours[0].x--; neighbours[0].y--;
|
||||
neighbours[1].y--;
|
||||
neighbours[2].x++; neighbours[2].y--;
|
||||
neighbours[3].x--;
|
||||
neighbours[4].x++;
|
||||
neighbours[5].x--; neighbours[5].y++;
|
||||
neighbours[6].y++;
|
||||
neighbours[7].x++; neighbours[7].y++;
|
||||
}
|
||||
|
||||
inline bool is_dig_job(const df::job* job) {
|
||||
return job->job_type == df::job_type::Dig || job->job_type == df::job_type::DigChannel;
|
||||
}
|
||||
|
||||
inline bool is_channel_job(const df::job* job) {
|
||||
return job->job_type == df::job_type::DigChannel;
|
||||
}
|
||||
|
||||
inline bool is_group_job(const ChannelGroups &groups, const df::job* job) {
|
||||
return groups.count(job->pos);
|
||||
}
|
||||
|
||||
inline bool is_dig_designation(const df::tile_designation &designation) {
|
||||
return designation.bits.dig != df::tile_dig_designation::No;
|
||||
}
|
||||
|
||||
inline bool is_channel_designation(const df::tile_designation &designation) {
|
||||
return designation.bits.dig != df::tile_dig_designation::Channel;
|
||||
}
|
||||
|
||||
inline bool is_safe_fall(const df::coord &map_pos) {
|
||||
df::coord below(map_pos);
|
||||
for (uint8_t zi = 0; zi < config.fall_threshold; ++zi) {
|
||||
below.z--;
|
||||
if (config.require_vision && Maps::getTileDesignation(below)->bits.hidden) {
|
||||
return true; //we require vision, and we can't see below.. so we gotta assume it's safe
|
||||
}
|
||||
df::tiletype type = *Maps::getTileType(below);
|
||||
if (!DFHack::isOpenTerrain(type)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
inline bool is_safe_to_dig_down(const df::coord &map_pos) {
|
||||
df::coord pos(map_pos);
|
||||
|
||||
for (uint8_t zi = 0; zi <= config.fall_threshold; ++zi) {
|
||||
// assume safe if we can't see and need vision
|
||||
if (config.require_vision && Maps::getTileDesignation(pos)->bits.hidden) {
|
||||
return true;
|
||||
}
|
||||
df::tiletype type = *Maps::getTileType(pos);
|
||||
if (zi == 0 && DFHack::isOpenTerrain(type)) {
|
||||
// the starting tile is open space, that's obviously not safe
|
||||
return false;
|
||||
} else if (!DFHack::isOpenTerrain(type)) {
|
||||
// a tile after the first one is not open space
|
||||
return true;
|
||||
}
|
||||
pos.z--;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
inline bool can_reach_designation(const df::coord &start, const df::coord &end) {
|
||||
if (start != end) {
|
||||
if (!Maps::canWalkBetween(start, end)) {
|
||||
df::coord neighbours[8];
|
||||
get_neighbours(end, neighbours);
|
||||
for (auto &pos: neighbours) {
|
||||
if (Maps::isValidTilePos(pos) && Maps::canWalkBetween(start, pos)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool has_unit(const df::tile_occupancy* occupancy) {
|
||||
return occupancy->bits.unit || occupancy->bits.unit_grounded;
|
||||
}
|
||||
|
||||
inline bool has_group_above(const ChannelGroups &groups, const df::coord &map_pos) {
|
||||
df::coord above(map_pos);
|
||||
above.z++;
|
||||
if (groups.count(above)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
inline bool has_any_groups_above(const ChannelGroups &groups, const Group &group) {
|
||||
// for each designation in the group
|
||||
for (auto &pos : group) {
|
||||
df::coord above(pos);
|
||||
above.z++;
|
||||
if (groups.count(above)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// if there are no incomplete groups above this group, then this group is ready
|
||||
return false;
|
||||
}
|
||||
|
||||
inline void cancel_job(df::job* job) {
|
||||
if (job != nullptr) {
|
||||
df::coord &pos = job->pos;
|
||||
df::map_block* job_block = Maps::getTileBlock(pos);
|
||||
uint16_t x, y;
|
||||
x = pos.x % 16;
|
||||
y = pos.y % 16;
|
||||
df::tile_designation &designation = job_block->designation[x][y];
|
||||
auto type = job->job_type;
|
||||
Job::removeJob(job);
|
||||
switch (type) {
|
||||
case job_type::Dig:
|
||||
designation.bits.dig = df::tile_dig_designation::Default;
|
||||
break;
|
||||
case job_type::CarveUpwardStaircase:
|
||||
designation.bits.dig = df::tile_dig_designation::UpStair;
|
||||
break;
|
||||
case job_type::CarveDownwardStaircase:
|
||||
designation.bits.dig = df::tile_dig_designation::DownStair;
|
||||
break;
|
||||
case job_type::CarveUpDownStaircase:
|
||||
designation.bits.dig = df::tile_dig_designation::UpDownStair;
|
||||
break;
|
||||
case job_type::CarveRamp:
|
||||
designation.bits.dig = df::tile_dig_designation::Ramp;
|
||||
break;
|
||||
case job_type::DigChannel:
|
||||
designation.bits.dig = df::tile_dig_designation::Channel;
|
||||
break;
|
||||
default:
|
||||
designation.bits.dig = df::tile_dig_designation::No;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template<class Ctr1, class Ctr2, class Ctr3>
|
||||
void set_difference(const Ctr1 &c1, const Ctr2 &c2, Ctr3 &c3) {
|
||||
for (const auto &a : c1) {
|
||||
if (!c2.count(a)) {
|
||||
c3.emplace(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template<class Ctr1, class Ctr2, class Ctr3>
|
||||
void map_value_difference(const Ctr1 &c1, const Ctr2 &c2, Ctr3 &c3) {
|
||||
for (const auto &a : c1) {
|
||||
bool matched = false;
|
||||
for (const auto &b : c2) {
|
||||
if (a.second == b.second) {
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matched) {
|
||||
c3.emplace(a.second);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
#include <Debug.h>
|
||||
|
||||
namespace DFHack {
|
||||
DBG_EXTERN(channelsafely, monitor);
|
||||
DBG_EXTERN(channelsafely, manager);
|
||||
DBG_EXTERN(channelsafely, groups);
|
||||
DBG_EXTERN(channelsafely, jobs);
|
||||
}
|
||||
|
||||
struct Configuration {
|
||||
bool monitor_active = false;
|
||||
bool require_vision = true;
|
||||
bool insta_dig = false;
|
||||
bool resurrect = false;
|
||||
int32_t refresh_freq = 600;
|
||||
int32_t monitor_freq = 1;
|
||||
uint8_t ignore_threshold = 5;
|
||||
uint8_t fall_threshold = 1;
|
||||
};
|
||||
|
||||
extern Configuration config;
|
||||
extern int32_t mapx, mapy, mapz;
|
@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <modules/Maps.h>
|
||||
#include <df/coord.h>
|
||||
#include <df/tiletype.h>
|
||||
#include <modules/EventManager.h> //hash functions (they should probably get moved at this point, the ones that aren't specifically for EM anyway)
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
class TileCache {
|
||||
private:
|
||||
TileCache() = default;
|
||||
std::unordered_map<df::coord, df::tiletype> locations;
|
||||
public:
|
||||
static TileCache& Get() {
|
||||
static TileCache instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
void cache(const df::coord &pos, df::tiletype type) {
|
||||
locations.emplace(pos, type);
|
||||
}
|
||||
|
||||
void uncache(const df::coord &pos) {
|
||||
locations.erase(pos);
|
||||
}
|
||||
|
||||
bool hasChanged(const df::coord &pos, const df::tiletype &type) {
|
||||
return locations.count(pos) && type != locations[pos];
|
||||
}
|
||||
};
|
@ -1,17 +0,0 @@
|
||||
project(labormanager)
|
||||
# A list of source files
|
||||
set(PROJECT_SRCS
|
||||
labormanager.cpp
|
||||
joblabormapper.cpp
|
||||
)
|
||||
# A list of headers
|
||||
set(PROJECT_HDRS
|
||||
labormanager.h
|
||||
joblabormapper.h
|
||||
)
|
||||
set_source_files_properties(${PROJECT_HDRS} PROPERTIES HEADER_FILE_ONLY TRUE)
|
||||
|
||||
# mash them together (headers are marked as headers and nothing will try to compile them)
|
||||
list(APPEND PROJECT_SRCS ${PROJECT_HDRS})
|
||||
|
||||
dfhack_plugin(labormanager ${PROJECT_SRCS})
|
@ -1,175 +1,186 @@
|
||||
local _ENV = mkmodule('plugins.dwarfmonitor')
|
||||
|
||||
local gps = df.global.gps
|
||||
local gui = require 'gui'
|
||||
local json = require('json')
|
||||
local guidm = require('gui.dwarfmode')
|
||||
local overlay = require('plugins.overlay')
|
||||
|
||||
config = {}
|
||||
widgets = {}
|
||||
local DWARFMONITOR_CONFIG_FILE = 'dfhack-config/dwarfmonitor.json'
|
||||
|
||||
function dmerror(desc)
|
||||
qerror('dwarfmonitor: ' .. tostring(desc))
|
||||
end
|
||||
|
||||
Widget = defclass(Widget)
|
||||
function Widget:init(opts)
|
||||
self.opts = opts
|
||||
end
|
||||
function Widget:get_pos()
|
||||
local x = self.opts.x >= 0 and self.opts.x or gps.dimx + self.opts.x
|
||||
local y = self.opts.y >= 0 and self.opts.y or gps.dimy + self.opts.y
|
||||
if self.opts.anchor == 'right' then
|
||||
x = x - (self:get_width() or 0) + 1
|
||||
end
|
||||
return x, y
|
||||
end
|
||||
function Widget:render()
|
||||
if monitor_state(self.opts.type) == false then
|
||||
return
|
||||
end
|
||||
self:update()
|
||||
local x, y = self:get_pos()
|
||||
local p = gui.Painter.new_xy(x, y, gps.dimx - 1, y)
|
||||
self:render_body(p)
|
||||
end
|
||||
function Widget:update() end
|
||||
function Widget:get_width() end
|
||||
function Widget:render_body() end
|
||||
-- ------------- --
|
||||
-- WeatherWidget --
|
||||
-- ------------- --
|
||||
|
||||
Widget_weather = defclass(Widget_weather, Widget)
|
||||
WeatherWidget = defclass(WeatherWidget, overlay.OverlayWidget)
|
||||
WeatherWidget.ATTRS{
|
||||
default_pos={x=15,y=-1},
|
||||
viewscreens={'dungeonmode', 'dwarfmode'},
|
||||
}
|
||||
|
||||
function Widget_weather:update()
|
||||
self.counts = get_weather_counts()
|
||||
function WeatherWidget:init()
|
||||
self.rain = false
|
||||
self.snow = false
|
||||
end
|
||||
|
||||
function Widget_weather:get_width()
|
||||
if self.counts.rain > 0 then
|
||||
if self.counts.snow > 0 then
|
||||
return 9
|
||||
function WeatherWidget:overlay_onupdate()
|
||||
local rain, snow = false, false
|
||||
local cw = df.global.current_weather
|
||||
for i=0,4 do
|
||||
for j=0,4 do
|
||||
weather = cw[i][j]
|
||||
if weather == df.weather_type.Rain then rain = true end
|
||||
if weather == df.weather_type.Snow then snow = true end
|
||||
end
|
||||
return 4
|
||||
elseif self.counts.snow > 0 then
|
||||
return 4
|
||||
end
|
||||
return 0
|
||||
self.frame.w = (rain and 4 or 0) + (snow and 4 or 0) +
|
||||
((snow and rain) and 1 or 0)
|
||||
self.rain, self.snow = rain, snow
|
||||
end
|
||||
|
||||
function Widget_weather:render_body(p)
|
||||
if self.counts.rain > 0 then
|
||||
p:string('Rain', COLOR_LIGHTBLUE):advance(1)
|
||||
function WeatherWidget:onRenderBody(dc)
|
||||
if self.rain then dc:string('Rain', COLOR_LIGHTBLUE):advance(1) end
|
||||
if self.snow then dc:string('Snow', COLOR_WHITE) end
|
||||
end
|
||||
if self.counts.snow > 0 then
|
||||
p:string('Snow', COLOR_WHITE)
|
||||
|
||||
-- ---------- --
|
||||
-- DateWidget --
|
||||
-- ---------- --
|
||||
|
||||
local function get_date_format()
|
||||
local ok, config = pcall(json.decode_file, DWARFMONITOR_CONFIG_FILE)
|
||||
if not ok or not config.date_format then
|
||||
return 'Y-M-D'
|
||||
end
|
||||
return config.date_format
|
||||
end
|
||||
|
||||
Widget_date = defclass(Widget_date, Widget)
|
||||
Widget_date.ATTRS = {
|
||||
output = ''
|
||||
DateWidget = defclass(DateWidget, overlay.OverlayWidget)
|
||||
DateWidget.ATTRS{
|
||||
default_pos={x=-16,y=1},
|
||||
viewscreens={'dungeonmode', 'dwarfmode'},
|
||||
}
|
||||
|
||||
function Widget_date:update()
|
||||
if not self.opts.format then
|
||||
self.opts.format = 'Y-M-D'
|
||||
function DateWidget:init()
|
||||
self.datestr = ''
|
||||
self.fmt = get_date_format()
|
||||
end
|
||||
|
||||
function DateWidget:overlay_onupdate()
|
||||
local year = dfhack.world.ReadCurrentYear()
|
||||
local month = dfhack.world.ReadCurrentMonth() + 1
|
||||
local day = dfhack.world.ReadCurrentDay()
|
||||
self.output = 'Date:'
|
||||
for i = 1, #self.opts.format do
|
||||
local c = self.opts.format:sub(i, i)
|
||||
|
||||
local fmt = self.fmt
|
||||
local datestr = 'Date:'
|
||||
for i=1,#fmt do
|
||||
local c = fmt:sub(i, i)
|
||||
if c == 'y' or c == 'Y' then
|
||||
self.output = self.output .. year
|
||||
datestr = datestr .. year
|
||||
elseif c == 'm' or c == 'M' then
|
||||
if c == 'M' and month < 10 then
|
||||
self.output = self.output .. '0'
|
||||
datestr = datestr .. '0'
|
||||
end
|
||||
self.output = self.output .. month
|
||||
datestr = datestr .. month
|
||||
elseif c == 'd' or c == 'D' then
|
||||
if c == 'D' and day < 10 then
|
||||
self.output = self.output .. '0'
|
||||
datestr = datestr .. '0'
|
||||
end
|
||||
self.output = self.output .. day
|
||||
datestr = datestr .. day
|
||||
else
|
||||
self.output = self.output .. c
|
||||
end
|
||||
datestr = datestr .. c
|
||||
end
|
||||
end
|
||||
|
||||
function Widget_date:get_width()
|
||||
return #self.output
|
||||
self.frame.w = #datestr
|
||||
self.datestr = datestr
|
||||
end
|
||||
|
||||
function Widget_date:render_body(p)
|
||||
p:string(self.output, COLOR_GREY)
|
||||
function DateWidget:onRenderBody(dc)
|
||||
dc:string(self.datestr, COLOR_GREY)
|
||||
end
|
||||
|
||||
Widget_misery = defclass(Widget_misery, Widget)
|
||||
-- ------------ --
|
||||
-- MiseryWidget --
|
||||
-- ------------ --
|
||||
|
||||
function Widget_misery:update()
|
||||
self.data = get_misery_data()
|
||||
end
|
||||
MiseryWidget = defclass(MiseryWidget, overlay.OverlayWidget)
|
||||
MiseryWidget.ATTRS{
|
||||
default_pos={x=-2,y=-1},
|
||||
viewscreens={'dwarfmode'},
|
||||
}
|
||||
|
||||
function Widget_misery:get_width()
|
||||
local w = 2 + 6
|
||||
for k, v in pairs(self.data) do
|
||||
w = w + #tostring(v.value)
|
||||
end
|
||||
return w
|
||||
function MiseryWidget:init()
|
||||
self.colors = getStressCategoryColors()
|
||||
self.stress_category_counts = {}
|
||||
end
|
||||
|
||||
function Widget_misery:render_body(p)
|
||||
p:string("H:", COLOR_WHITE)
|
||||
for i = 0, 6 do
|
||||
local v = self.data[i]
|
||||
p:string(tostring(v.value), v.color)
|
||||
if not v.last then
|
||||
p:string("/", COLOR_WHITE)
|
||||
end
|
||||
function MiseryWidget:overlay_onupdate()
|
||||
local counts, num_colors = {}, #self.colors
|
||||
for _,unit in ipairs(df.global.world.units.active) do
|
||||
if not dfhack.units.isCitizen(unit, true) then goto continue end
|
||||
local stress_category = math.min(num_colors,
|
||||
dfhack.units.getStressCategory(unit))
|
||||
counts[stress_category] = (counts[stress_category] or 0) + 1
|
||||
::continue::
|
||||
end
|
||||
|
||||
local width = 2 + num_colors - 1 -- 'H:' plus the slashes
|
||||
for i=1,num_colors do
|
||||
width = width + #tostring(counts[i] or 0)
|
||||
end
|
||||
|
||||
Widget_cursor = defclass(Widget_cursor, Widget)
|
||||
self.stress_category_counts = counts
|
||||
self.frame.w = width
|
||||
end
|
||||
|
||||
function Widget_cursor:update()
|
||||
if gps.mouse_x == -1 and not self.opts.show_invalid then
|
||||
self.output = ''
|
||||
return
|
||||
function MiseryWidget:onRenderBody(dc)
|
||||
dc:string('H:', COLOR_WHITE)
|
||||
local counts = self.stress_category_counts
|
||||
for i,color in ipairs(self.colors) do
|
||||
dc:string(tostring(counts[i] or 0), color)
|
||||
if i < #self.colors then dc:string('/', COLOR_WHITE) end
|
||||
end
|
||||
self.output = (self.opts.format or '(x,y)'):gsub('[xX]', gps.mouse_x):gsub('[yY]', gps.mouse_y)
|
||||
end
|
||||
|
||||
function Widget_cursor:get_width()
|
||||
return #self.output
|
||||
end
|
||||
-- ------------ --
|
||||
-- CursorWidget --
|
||||
-- ------------ --
|
||||
|
||||
function Widget_cursor:render_body(p)
|
||||
p:string(self.output)
|
||||
end
|
||||
CursorWidget = defclass(CursorWidget, overlay.OverlayWidget)
|
||||
CursorWidget.ATTRS{
|
||||
default_pos={x=2,y=-4},
|
||||
viewscreens={'dungeonmode', 'dwarfmode'},
|
||||
}
|
||||
|
||||
function render_all()
|
||||
for _, w in pairs(widgets) do
|
||||
w:render()
|
||||
end
|
||||
end
|
||||
function CursorWidget:onRenderBody(dc)
|
||||
local screenx, screeny = dfhack.screen.getMousePos()
|
||||
local mouse_map = dfhack.gui.getMousePos()
|
||||
local keyboard_map = guidm.getCursorPos()
|
||||
|
||||
function load_config()
|
||||
config = require('json').decode_file('dfhack-config/dwarfmonitor.json')
|
||||
if not config.widgets then
|
||||
dmerror('No widgets enabled')
|
||||
local text = {}
|
||||
table.insert(text, ('mouse UI grid (%d,%d)'):format(screenx, screeny))
|
||||
if mouse_map then
|
||||
table.insert(text, ('mouse map coord (%d,%d,%d)')
|
||||
:format(mouse_map.x, mouse_map.y, mouse_map.z))
|
||||
end
|
||||
if type(config.widgets) ~= 'table' then
|
||||
dmerror('"widgets" is not a table')
|
||||
if keyboard_map then
|
||||
table.insert(text, ('kbd cursor coord (%d,%d,%d)')
|
||||
:format(keyboard_map.x, keyboard_map.y, keyboard_map.z))
|
||||
end
|
||||
widgets = {}
|
||||
for _, opts in pairs(config.widgets) do
|
||||
if type(opts) ~= 'table' then dmerror('"widgets" is not an array') end
|
||||
if not opts.type then dmerror('Widget missing type field') end
|
||||
local cls = _ENV['Widget_' .. opts.type]
|
||||
if not cls then
|
||||
dmerror('Invalid widget type: ' .. opts.type)
|
||||
end
|
||||
table.insert(widgets, cls(opts))
|
||||
local width = 0
|
||||
for i,line in ipairs(text) do
|
||||
dc:seek(0, i-1):string(line)
|
||||
width = math.max(width, #line)
|
||||
end
|
||||
self.frame.w = width
|
||||
self.frame.h = #text
|
||||
end
|
||||
|
||||
-- register our widgets with the overlay
|
||||
OVERLAY_WIDGETS = {
|
||||
cursor=CursorWidget,
|
||||
date=DateWidget,
|
||||
misery=MiseryWidget,
|
||||
weather=WeatherWidget,
|
||||
}
|
||||
|
||||
return _ENV
|
||||
|
@ -0,0 +1,244 @@
|
||||
local _ENV = mkmodule('plugins.hotkeys')
|
||||
|
||||
local gui = require('gui')
|
||||
local helpdb = require('helpdb')
|
||||
local overlay = require('plugins.overlay')
|
||||
local widgets = require('gui.widgets')
|
||||
|
||||
-- ----------------- --
|
||||
-- HotspotMenuWidget --
|
||||
-- ----------------- --
|
||||
|
||||
HotspotMenuWidget = defclass(HotspotMenuWidget, overlay.OverlayWidget)
|
||||
HotspotMenuWidget.ATTRS{
|
||||
default_pos={x=1,y=3},
|
||||
hotspot=true,
|
||||
viewscreens={'dwarfmode'},
|
||||
overlay_onupdate_max_freq_seconds=0,
|
||||
frame={w=2, h=1}
|
||||
}
|
||||
|
||||
function HotspotMenuWidget:init()
|
||||
self:addviews{widgets.Label{text='!!'}}
|
||||
self.mouseover = false
|
||||
end
|
||||
|
||||
function HotspotMenuWidget:overlay_onupdate()
|
||||
local hasMouse = self:getMousePos()
|
||||
if hasMouse and not self.mouseover then
|
||||
self.mouseover = true
|
||||
return true
|
||||
end
|
||||
self.mouseover = hasMouse
|
||||
end
|
||||
|
||||
function HotspotMenuWidget:overlay_trigger()
|
||||
local hotkeys, bindings = getHotkeys()
|
||||
return MenuScreen{
|
||||
hotspot_frame=self.frame,
|
||||
hotkeys=hotkeys,
|
||||
bindings=bindings}:show()
|
||||
end
|
||||
|
||||
-- register the menu hotspot with the overlay
|
||||
OVERLAY_WIDGETS = {menu=HotspotMenuWidget}
|
||||
|
||||
-- ---------- --
|
||||
-- MenuScreen --
|
||||
-- ---------- --
|
||||
|
||||
local ARROW = string.char(26)
|
||||
local MAX_LIST_WIDTH = 45
|
||||
local MAX_LIST_HEIGHT = 15
|
||||
|
||||
MenuScreen = defclass(MenuScreen, gui.Screen)
|
||||
MenuScreen.ATTRS{
|
||||
focus_path='hotkeys/menu',
|
||||
hotspot_frame=DEFAULT_NIL,
|
||||
hotkeys=DEFAULT_NIL,
|
||||
bindings=DEFAULT_NIL,
|
||||
}
|
||||
|
||||
-- get a map from the binding string to a list of hotkey strings that all
|
||||
-- point to that binding
|
||||
local function get_bindings_to_hotkeys(hotkeys, bindings)
|
||||
local bindings_to_hotkeys = {}
|
||||
for _,hotkey in ipairs(hotkeys) do
|
||||
local binding = bindings[hotkey]
|
||||
table.insert(ensure_key(bindings_to_hotkeys, binding), hotkey)
|
||||
end
|
||||
return bindings_to_hotkeys
|
||||
end
|
||||
|
||||
-- number of non-text tiles: icon, space, space between cmd and hk, scrollbar
|
||||
local LIST_BUFFER = 2 + 1 + 1
|
||||
|
||||
local function get_choices(hotkeys, bindings, is_inverted)
|
||||
local choices, max_width, seen = {}, 0, {}
|
||||
local bindings_to_hotkeys = get_bindings_to_hotkeys(hotkeys, bindings)
|
||||
|
||||
-- build list choices
|
||||
for _,hotkey in ipairs(hotkeys) do
|
||||
local command = bindings[hotkey]
|
||||
if seen[command] then goto continue end
|
||||
seen[command] = true
|
||||
local hk_width, tokens = 0, {}
|
||||
for _,hk in ipairs(bindings_to_hotkeys[command]) do
|
||||
if hk_width ~= 0 then
|
||||
table.insert(tokens, ', ')
|
||||
hk_width = hk_width + 2
|
||||
end
|
||||
table.insert(tokens, {text=hk, pen=COLOR_LIGHTGREEN})
|
||||
hk_width = hk_width + #hk
|
||||
end
|
||||
local command_str = command
|
||||
if hk_width + #command + LIST_BUFFER > MAX_LIST_WIDTH then
|
||||
local max_command_len = MAX_LIST_WIDTH - hk_width - LIST_BUFFER
|
||||
command_str = command:sub(1, max_command_len - 3) .. '...'
|
||||
end
|
||||
table.insert(tokens, 1, {text=command_str})
|
||||
local choice = {icon=ARROW, command=command, text=tokens,
|
||||
hk_width=hk_width}
|
||||
max_width = math.max(max_width, hk_width + #command_str + LIST_BUFFER)
|
||||
table.insert(choices, is_inverted and 1 or #choices + 1, choice)
|
||||
::continue::
|
||||
end
|
||||
|
||||
-- adjust width of command fields so the hotkey tokens are right justified
|
||||
for _,choice in ipairs(choices) do
|
||||
local command_token = choice.text[1]
|
||||
command_token.width = max_width - choice.hk_width - 3
|
||||
end
|
||||
|
||||
return choices, max_width
|
||||
end
|
||||
|
||||
function MenuScreen:init()
|
||||
self.mouseover = false
|
||||
|
||||
local choices,list_width = get_choices(self.hotkeys, self.bindings,
|
||||
self.hotspot_frame.b)
|
||||
|
||||
local list_frame = copyall(self.hotspot_frame)
|
||||
list_frame.w = list_width + 2
|
||||
list_frame.h = math.min(#choices, MAX_LIST_HEIGHT) + 2
|
||||
if list_frame.t then
|
||||
list_frame.t = math.max(0, list_frame.t - 1)
|
||||
else
|
||||
list_frame.b = math.max(0, list_frame.b - 1)
|
||||
end
|
||||
if list_frame.l then
|
||||
list_frame.l = math.max(0, list_frame.l - 1)
|
||||
else
|
||||
list_frame.r = math.max(0, list_frame.r - 1)
|
||||
end
|
||||
|
||||
local help_frame = {w=list_frame.w, l=list_frame.l, r=list_frame.r}
|
||||
if list_frame.t then
|
||||
help_frame.t = list_frame.t + list_frame.h + 1
|
||||
else
|
||||
help_frame.b = list_frame.b + list_frame.h + 1
|
||||
end
|
||||
|
||||
self:addviews{
|
||||
widgets.ResizingPanel{
|
||||
view_id='list_panel',
|
||||
autoarrange_subviews=true,
|
||||
frame=list_frame,
|
||||
frame_style=gui.GREY_LINE_FRAME,
|
||||
frame_background=gui.CLEAR_PEN,
|
||||
subviews={
|
||||
widgets.List{
|
||||
view_id='list',
|
||||
choices=choices,
|
||||
icon_width=2,
|
||||
on_select=self:callback('onSelect'),
|
||||
on_submit=self:callback('onSubmit'),
|
||||
on_submit2=self:callback('onSubmit2'),
|
||||
},
|
||||
},
|
||||
},
|
||||
widgets.ResizingPanel{
|
||||
view_id='help_panel',
|
||||
autoarrange_subviews=true,
|
||||
frame=help_frame,
|
||||
frame_style=gui.GREY_LINE_FRAME,
|
||||
frame_background=gui.CLEAR_PEN,
|
||||
subviews={
|
||||
widgets.WrappedLabel{
|
||||
view_id='help',
|
||||
text_to_wrap='',
|
||||
scroll_keys={},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
function MenuScreen:onDismiss()
|
||||
cleanupHotkeys()
|
||||
end
|
||||
|
||||
function MenuScreen:onSelect(_, choice)
|
||||
if not choice or #self.subviews == 0 then return end
|
||||
local first_word = choice.command:trim():split(' +')[1]
|
||||
if first_word:startswith(':') then first_word = first_word:sub(2) end
|
||||
self.subviews.help.text_to_wrap = helpdb.is_entry(first_word) and
|
||||
helpdb.get_entry_short_help(first_word) or 'Command not found'
|
||||
self.subviews.help_panel:updateLayout()
|
||||
end
|
||||
|
||||
function MenuScreen:onSubmit(_, choice)
|
||||
if not choice then return end
|
||||
dfhack.screen.hideGuard(self, dfhack.run_command, choice.command)
|
||||
self:dismiss()
|
||||
end
|
||||
|
||||
function MenuScreen:onSubmit2(_, choice)
|
||||
if not choice then return end
|
||||
self:dismiss()
|
||||
dfhack.run_script('gui/launcher', choice.command)
|
||||
end
|
||||
|
||||
function MenuScreen:onInput(keys)
|
||||
if keys.LEAVESCREEN then
|
||||
self:dismiss()
|
||||
return true
|
||||
elseif keys.STANDARDSCROLL_RIGHT then
|
||||
self:onSubmit2(self.subviews.list:getSelected())
|
||||
return true
|
||||
elseif keys._MOUSE_L_DOWN then
|
||||
local list = self.subviews.list
|
||||
local x = list:getMousePos()
|
||||
if x == 0 then -- clicked on icon
|
||||
self:onSubmit2(list:getSelected())
|
||||
return true
|
||||
end
|
||||
end
|
||||
return self:inputToSubviews(keys)
|
||||
end
|
||||
|
||||
function MenuScreen:onRenderFrame(dc, rect)
|
||||
self:renderParent()
|
||||
end
|
||||
|
||||
function MenuScreen:onRenderBody(dc)
|
||||
local panel = self.subviews.list_panel
|
||||
local list = self.subviews.list
|
||||
local idx = list:getIdxUnderMouse()
|
||||
if idx and idx ~= self.last_mouse_idx then
|
||||
-- focus follows mouse, but if cursor keys were used to change the
|
||||
-- selection, don't override the selection until the mouse moves to
|
||||
-- another item
|
||||
list:setSelected(idx)
|
||||
self.mouseover = true
|
||||
self.last_mouse_idx = idx
|
||||
elseif not panel:getMousePos(gui.ViewRect{rect=panel.frame_rect})
|
||||
and self.mouseover then
|
||||
-- once the mouse has entered the list area, leaving the frame should
|
||||
-- close the menu screen
|
||||
self:dismiss()
|
||||
end
|
||||
end
|
||||
|
||||
return _ENV
|
@ -0,0 +1,485 @@
|
||||
local _ENV = mkmodule('plugins.overlay')
|
||||
|
||||
local gui = require('gui')
|
||||
local json = require('json')
|
||||
local utils = require('utils')
|
||||
local widgets = require('gui.widgets')
|
||||
|
||||
local OVERLAY_CONFIG_FILE = 'dfhack-config/overlay.json'
|
||||
local OVERLAY_WIDGETS_VAR = 'OVERLAY_WIDGETS'
|
||||
|
||||
local DEFAULT_X_POS, DEFAULT_Y_POS = -2, -2
|
||||
|
||||
-- ---------------- --
|
||||
-- state and config --
|
||||
-- ---------------- --
|
||||
|
||||
local active_triggered_widget = nil
|
||||
local active_triggered_screen = nil -- if non-nil, hotspots will not get updates
|
||||
local widget_db = {} -- map of widget name to ephermeral state
|
||||
local widget_index = {} -- ordered list of widget names
|
||||
local overlay_config = {} -- map of widget name to persisted state
|
||||
local active_hotspot_widgets = {} -- map of widget names to the db entry
|
||||
local active_viewscreen_widgets = {} -- map of vs_name to map of w.names -> db
|
||||
|
||||
local function reset()
|
||||
if active_triggered_screen then
|
||||
active_triggered_screen:dismiss()
|
||||
end
|
||||
active_triggered_widget = nil
|
||||
active_triggered_screen = nil
|
||||
|
||||
widget_db = {}
|
||||
widget_index = {}
|
||||
|
||||
local ok, config = pcall(json.decode_file, OVERLAY_CONFIG_FILE)
|
||||
overlay_config = ok and config or {}
|
||||
|
||||
active_hotspot_widgets = {}
|
||||
active_viewscreen_widgets = {}
|
||||
end
|
||||
|
||||
local function save_config()
|
||||
if not safecall(json.encode_file, overlay_config, OVERLAY_CONFIG_FILE) then
|
||||
dfhack.printerr(('failed to save overlay config file: "%s"')
|
||||
:format(path))
|
||||
end
|
||||
end
|
||||
|
||||
local function triggered_screen_has_lock()
|
||||
if not active_triggered_screen then return false end
|
||||
if active_triggered_screen:isActive() then return true end
|
||||
active_triggered_widget = nil
|
||||
active_triggered_screen = nil
|
||||
return false
|
||||
end
|
||||
|
||||
-- ----------- --
|
||||
-- utility fns --
|
||||
-- ----------- --
|
||||
|
||||
local function normalize_list(element_or_list)
|
||||
if type(element_or_list) == 'table' then return element_or_list end
|
||||
return {element_or_list}
|
||||
end
|
||||
|
||||
-- normalize "short form" viewscreen names to "long form"
|
||||
local function normalize_viewscreen_name(vs_name)
|
||||
if vs_name:match('viewscreen_.*st') then return vs_name end
|
||||
return 'viewscreen_' .. vs_name .. 'st'
|
||||
end
|
||||
|
||||
-- reduce "long form" viewscreen names to "short form"
|
||||
local function simplify_viewscreen_name(vs_name)
|
||||
_,_,short_name = vs_name:find('^viewscreen_(.*)st$')
|
||||
if short_name then return short_name end
|
||||
return vs_name
|
||||
end
|
||||
|
||||
local function is_empty(tbl)
|
||||
for _ in pairs(tbl) do
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function sanitize_pos(pos)
|
||||
local x = math.floor(tonumber(pos.x) or DEFAULT_X_POS)
|
||||
local y = math.floor(tonumber(pos.y) or DEFAULT_Y_POS)
|
||||
-- if someone accidentally uses 0-based instead of 1-based indexing, fix it
|
||||
if x == 0 then x = 1 end
|
||||
if y == 0 then y = 1 end
|
||||
return {x=x, y=y}
|
||||
end
|
||||
|
||||
local function make_frame(pos, old_frame)
|
||||
old_frame = old_frame or {}
|
||||
local frame = {w=old_frame.w, h=old_frame.h}
|
||||
if pos.x < 0 then frame.r = math.abs(pos.x) - 1 else frame.l = pos.x - 1 end
|
||||
if pos.y < 0 then frame.b = math.abs(pos.y) - 1 else frame.t = pos.y - 1 end
|
||||
return frame
|
||||
end
|
||||
|
||||
local function get_screen_rect()
|
||||
local w, h = dfhack.screen.getWindowSize()
|
||||
return gui.ViewRect{rect=gui.mkdims_wh(0, 0, w, h)}
|
||||
end
|
||||
|
||||
-- ------------- --
|
||||
-- CLI functions --
|
||||
-- ------------- --
|
||||
|
||||
local function get_name(name_or_number)
|
||||
local num = tonumber(name_or_number)
|
||||
if num and widget_index[num] then
|
||||
return widget_index[num]
|
||||
end
|
||||
return tostring(name_or_number)
|
||||
end
|
||||
|
||||
local function do_by_names_or_numbers(args, fn)
|
||||
local arglist = normalize_list(args)
|
||||
if #arglist == 0 then
|
||||
dfhack.printerr('please specify a widget name or list number')
|
||||
return
|
||||
end
|
||||
for _,name_or_number in ipairs(arglist) do
|
||||
local name = get_name(name_or_number)
|
||||
local db_entry = widget_db[name]
|
||||
if not db_entry then
|
||||
dfhack.printerr(('widget not found: "%s"'):format(name))
|
||||
else
|
||||
fn(name, db_entry)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function do_enable(args, quiet, skip_save)
|
||||
local enable_fn = function(name, db_entry)
|
||||
overlay_config[name].enabled = true
|
||||
if db_entry.widget.hotspot then
|
||||
active_hotspot_widgets[name] = db_entry
|
||||
end
|
||||
for _,vs_name in ipairs(normalize_list(db_entry.widget.viewscreens)) do
|
||||
vs_name = normalize_viewscreen_name(vs_name)
|
||||
ensure_key(active_viewscreen_widgets, vs_name)[name] = db_entry
|
||||
end
|
||||
if not quiet then
|
||||
print(('enabled widget %s'):format(name))
|
||||
end
|
||||
end
|
||||
if args[1] == 'all' then
|
||||
for name,db_entry in pairs(widget_db) do
|
||||
if not overlay_config[name].enabled then
|
||||
enable_fn(name, db_entry)
|
||||
end
|
||||
end
|
||||
else
|
||||
do_by_names_or_numbers(args, enable_fn)
|
||||
end
|
||||
if not skip_save then
|
||||
save_config()
|
||||
end
|
||||
end
|
||||
|
||||
local function do_disable(args)
|
||||
local disable_fn = function(name, db_entry)
|
||||
overlay_config[name].enabled = false
|
||||
if db_entry.widget.hotspot then
|
||||
active_hotspot_widgets[name] = nil
|
||||
end
|
||||
for _,vs_name in ipairs(normalize_list(db_entry.widget.viewscreens)) do
|
||||
vs_name = normalize_viewscreen_name(vs_name)
|
||||
ensure_key(active_viewscreen_widgets, vs_name)[name] = nil
|
||||
if is_empty(active_viewscreen_widgets[vs_name]) then
|
||||
active_viewscreen_widgets[vs_name] = nil
|
||||
end
|
||||
end
|
||||
print(('disabled widget %s'):format(name))
|
||||
end
|
||||
if args[1] == 'all' then
|
||||
for name,db_entry in pairs(widget_db) do
|
||||
if overlay_config[name].enabled then
|
||||
disable_fn(name, db_entry)
|
||||
end
|
||||
end
|
||||
else
|
||||
do_by_names_or_numbers(args, disable_fn)
|
||||
end
|
||||
save_config()
|
||||
end
|
||||
|
||||
local function do_list(args)
|
||||
local filter = args and #args > 0
|
||||
local num_filtered = 0
|
||||
for i,name in ipairs(widget_index) do
|
||||
if filter then
|
||||
local passes = false
|
||||
for _,str in ipairs(args) do
|
||||
if name:find(str) then
|
||||
passes = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if not passes then
|
||||
num_filtered = num_filtered + 1
|
||||
goto continue
|
||||
end
|
||||
end
|
||||
local db_entry = widget_db[name]
|
||||
local enabled = overlay_config[name].enabled
|
||||
dfhack.color(enabled and COLOR_LIGHTGREEN or COLOR_YELLOW)
|
||||
dfhack.print(enabled and '[enabled] ' or '[disabled]')
|
||||
dfhack.color()
|
||||
print((' %d) %s'):format(i, name))
|
||||
::continue::
|
||||
end
|
||||
if num_filtered > 0 then
|
||||
print(('(%d widgets filtered out)'):format(num_filtered))
|
||||
end
|
||||
end
|
||||
|
||||
local function load_widget(name, widget_class)
|
||||
local widget = widget_class{name=name}
|
||||
widget_db[name] = {
|
||||
widget=widget,
|
||||
next_update_ms=widget.overlay_onupdate and 0 or math.huge,
|
||||
}
|
||||
if not overlay_config[name] then overlay_config[name] = {} end
|
||||
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 then
|
||||
do_enable(name, true, true)
|
||||
else
|
||||
config.enabled = false
|
||||
end
|
||||
end
|
||||
|
||||
local function load_widgets(env_prefix, provider, env_fn)
|
||||
local env_name = env_prefix .. provider
|
||||
local ok, provider_env = pcall(env_fn, env_name)
|
||||
if not ok or not provider_env[OVERLAY_WIDGETS_VAR] then return end
|
||||
local overlay_widgets = provider_env[OVERLAY_WIDGETS_VAR]
|
||||
if type(overlay_widgets) ~= 'table' then
|
||||
dfhack.printerr(
|
||||
('error loading overlay widgets from "%s": %s map is malformed')
|
||||
:format(env_name, OVERLAY_WIDGETS_VAR))
|
||||
return
|
||||
end
|
||||
for widget_name,widget_class in pairs(overlay_widgets) do
|
||||
local name = provider .. '.' .. widget_name
|
||||
if not safecall(load_widget, name, widget_class) then
|
||||
dfhack.printerr(('error loading overlay widget "%s"'):format(name))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- called directly from cpp on plugin enable
|
||||
function reload()
|
||||
reset()
|
||||
|
||||
for _,plugin in ipairs(dfhack.internal.listPlugins()) do
|
||||
load_widgets('plugins.', plugin, require)
|
||||
end
|
||||
for _,script_path in ipairs(dfhack.internal.getScriptPaths()) do
|
||||
local files = dfhack.filesystem.listdir_recursive(
|
||||
script_path, nil, false)
|
||||
if not files then goto skip_path end
|
||||
for _,f in ipairs(files) do
|
||||
if not f.isdir and
|
||||
f.path:endswith('.lua') and
|
||||
not f.path:startswith('test/') and
|
||||
not f.path:startswith('internal/') then
|
||||
local script_name = f.path:sub(1, #f.path - 4) -- remove '.lua'
|
||||
load_widgets('', script_name, reqscript)
|
||||
end
|
||||
end
|
||||
::skip_path::
|
||||
end
|
||||
|
||||
for name in pairs(widget_db) do
|
||||
table.insert(widget_index, name)
|
||||
end
|
||||
table.sort(widget_index)
|
||||
|
||||
reposition_widgets()
|
||||
end
|
||||
|
||||
local function dump_widget_config(name, widget)
|
||||
local pos = overlay_config[name].pos
|
||||
print(('widget %s is positioned at x=%d, y=%d'):format(name, pos.x, pos.y))
|
||||
local viewscreens = normalize_list(widget.viewscreens)
|
||||
if #viewscreens > 0 then
|
||||
print(' it will be attached to the following viewscreens:')
|
||||
for _,vs in ipairs(viewscreens) do
|
||||
print((' %s'):format(simplify_viewscreen_name(vs)))
|
||||
end
|
||||
end
|
||||
if widget.hotspot then
|
||||
print(' it will act as a hotspot on all screens')
|
||||
end
|
||||
end
|
||||
|
||||
local function do_position(args)
|
||||
local name_or_number, x, y = table.unpack(args)
|
||||
local name = get_name(name_or_number)
|
||||
if not widget_db[name] then
|
||||
if not name_or_number then
|
||||
dfhack.printerr('please specify a widget name or list number')
|
||||
else
|
||||
dfhack.printerr(('widget not found: "%s"'):format(name))
|
||||
end
|
||||
return
|
||||
end
|
||||
local widget = widget_db[name].widget
|
||||
local pos
|
||||
if x == 'default' then
|
||||
pos = sanitize_pos(widget.default_pos)
|
||||
else
|
||||
x, y = tonumber(x), tonumber(y)
|
||||
if not x or not y then
|
||||
dump_widget_config(name, widget)
|
||||
return
|
||||
end
|
||||
pos = sanitize_pos{x=x, y=y}
|
||||
end
|
||||
overlay_config[name].pos = pos
|
||||
widget.frame = make_frame(pos, widget.frame)
|
||||
widget:updateLayout(get_screen_rect())
|
||||
save_config()
|
||||
print(('repositioned widget %s to x=%d, y=%d'):format(name, pos.x, pos.y))
|
||||
end
|
||||
|
||||
-- note that the widget does not have to be enabled to be triggered
|
||||
local function do_trigger(args)
|
||||
if triggered_screen_has_lock() then
|
||||
dfhack.printerr(('cannot trigger widget; widget "%s" is already active')
|
||||
:format(active_triggered_widget))
|
||||
return
|
||||
end
|
||||
do_by_names_or_numbers(args[1], function(name, db_entry)
|
||||
local widget = db_entry.widget
|
||||
if widget.overlay_trigger then
|
||||
active_triggered_screen = widget:overlay_trigger()
|
||||
if active_triggered_screen then
|
||||
active_triggered_widget = name
|
||||
end
|
||||
print(('triggered widget %s'):format(name))
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local command_fns = {
|
||||
enable=do_enable,
|
||||
disable=do_disable,
|
||||
list=do_list,
|
||||
position=do_position,
|
||||
trigger=do_trigger,
|
||||
}
|
||||
|
||||
local HELP_ARGS = utils.invert{'help', '--help', '-h'}
|
||||
|
||||
function overlay_command(args)
|
||||
local command = table.remove(args, 1) or 'help'
|
||||
if HELP_ARGS[command] or not command_fns[command] then return false end
|
||||
command_fns[command](args)
|
||||
return true
|
||||
end
|
||||
|
||||
-- ---------------- --
|
||||
-- event management --
|
||||
-- ---------------- --
|
||||
|
||||
local function detect_frame_change(widget, fn)
|
||||
local frame = widget.frame
|
||||
local w, h = frame.w, frame.h
|
||||
local ret = fn()
|
||||
if w ~= frame.w or h ~= frame.h then
|
||||
widget:updateLayout()
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
local function get_next_onupdate_timestamp(now_ms, widget)
|
||||
local freq_s = widget.overlay_onupdate_max_freq_seconds
|
||||
if freq_s == 0 then
|
||||
return now_ms
|
||||
end
|
||||
local freq_ms = math.floor(freq_s * 1000)
|
||||
local jitter = math.random(0, freq_ms // 8) -- up to ~12% jitter
|
||||
return now_ms + freq_ms - jitter
|
||||
end
|
||||
|
||||
-- reduces the next call by a small random amount to introduce jitter into the
|
||||
-- widget processing timings
|
||||
local function do_update(name, db_entry, now_ms, vs)
|
||||
if db_entry.next_update_ms > now_ms then return end
|
||||
local w = db_entry.widget
|
||||
db_entry.next_update_ms = get_next_onupdate_timestamp(now_ms, w)
|
||||
if detect_frame_change(w, function() return w:overlay_onupdate(vs) end) then
|
||||
active_triggered_screen = w:overlay_trigger()
|
||||
if active_triggered_screen then
|
||||
active_triggered_widget = name
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function update_hotspot_widgets()
|
||||
if triggered_screen_has_lock() then return end
|
||||
local now_ms = dfhack.getTickCount()
|
||||
for name,db_entry in pairs(active_hotspot_widgets) do
|
||||
if do_update(name, db_entry, now_ms) then return end
|
||||
end
|
||||
end
|
||||
|
||||
-- not subject to trigger lock since these widgets are already filtered by
|
||||
-- viewscreen
|
||||
function update_viewscreen_widgets(vs_name, vs)
|
||||
local vs_widgets = active_viewscreen_widgets[vs_name]
|
||||
if not vs_widgets then return end
|
||||
local now_ms = dfhack.getTickCount()
|
||||
for name,db_entry in pairs(vs_widgets) do
|
||||
if do_update(name, db_entry, now_ms, vs) then return end
|
||||
end
|
||||
end
|
||||
|
||||
function feed_viewscreen_widgets(vs_name, keys)
|
||||
local vs_widgets = active_viewscreen_widgets[vs_name]
|
||||
if not vs_widgets then return false end
|
||||
for _,db_entry in pairs(vs_widgets) do
|
||||
local w = db_entry.widget
|
||||
if detect_frame_change(w, function() return w:onInput(keys) end) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function render_viewscreen_widgets(vs_name)
|
||||
local vs_widgets = active_viewscreen_widgets[vs_name]
|
||||
if not vs_widgets then return false end
|
||||
local dc = gui.Painter.new()
|
||||
for _,db_entry in pairs(vs_widgets) do
|
||||
local w = db_entry.widget
|
||||
detect_frame_change(w, function() w:render(dc) end)
|
||||
end
|
||||
end
|
||||
|
||||
-- called when the DF window is resized
|
||||
function reposition_widgets()
|
||||
local sr = get_screen_rect()
|
||||
for _,db_entry in pairs(widget_db) do
|
||||
db_entry.widget:updateLayout(sr)
|
||||
end
|
||||
end
|
||||
|
||||
-- ------------------------------------------------- --
|
||||
-- OverlayWidget (base class of all overlay widgets) --
|
||||
-- ------------------------------------------------- --
|
||||
|
||||
OverlayWidget = defclass(OverlayWidget, widgets.Widget)
|
||||
OverlayWidget.ATTRS{
|
||||
name=DEFAULT_NIL, -- this is set by the framework to the widget name
|
||||
default_pos={x=DEFAULT_X_POS, y=DEFAULT_Y_POS}, -- 1-based widget screen pos
|
||||
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
|
||||
}
|
||||
|
||||
function OverlayWidget:init()
|
||||
if self.overlay_onupdate_max_freq_seconds < 0 then
|
||||
error(('overlay_onupdate_max_freq_seconds must be >= 0: %s')
|
||||
:format(tostring(self.overlay_onupdate_max_freq_seconds)))
|
||||
end
|
||||
|
||||
-- set defaults for frame. the widget is expected to keep these up to date
|
||||
-- when display contents change so the widget position can shift if the
|
||||
-- frame is relative to the right or bottom edges.
|
||||
self.frame = self.frame or {}
|
||||
self.frame.w = self.frame.w or 5
|
||||
self.frame.h = self.frame.h or 1
|
||||
end
|
||||
|
||||
return _ENV
|
@ -1,325 +0,0 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
|
||||
#include "Core.h"
|
||||
#include <Console.h>
|
||||
#include <Export.h>
|
||||
#include <PluginManager.h>
|
||||
#include <VTableInterpose.h>
|
||||
|
||||
|
||||
// DF data structure definition headers
|
||||
#include "DataDefs.h"
|
||||
#include "LuaTools.h"
|
||||
#include "MiscUtils.h"
|
||||
#include "Types.h"
|
||||
#include "df/viewscreen_dwarfmodest.h"
|
||||
#include "df/world.h"
|
||||
#include "df/building_constructionst.h"
|
||||
#include "df/building.h"
|
||||
#include "df/job.h"
|
||||
#include "df/job_item.h"
|
||||
|
||||
#include "modules/Gui.h"
|
||||
#include "modules/Screen.h"
|
||||
#include "modules/Buildings.h"
|
||||
#include "modules/Maps.h"
|
||||
|
||||
#include "modules/World.h"
|
||||
|
||||
#include "uicommon.h"
|
||||
|
||||
using std::map;
|
||||
using std::string;
|
||||
using std::vector;
|
||||
|
||||
using namespace DFHack;
|
||||
using namespace df::enums;
|
||||
|
||||
DFHACK_PLUGIN("resume");
|
||||
#define PLUGIN_VERSION 0.2
|
||||
|
||||
REQUIRE_GLOBAL(gps);
|
||||
REQUIRE_GLOBAL(process_jobs);
|
||||
REQUIRE_GLOBAL(ui);
|
||||
REQUIRE_GLOBAL(world);
|
||||
|
||||
#ifndef HAVE_NULLPTR
|
||||
#define nullptr 0L
|
||||
#endif
|
||||
|
||||
DFhackCExport command_result plugin_shutdown ( color_ostream &out )
|
||||
{
|
||||
return CR_OK;
|
||||
}
|
||||
|
||||
df::job *get_suspended_job(df::building *bld)
|
||||
{
|
||||
if (bld->getBuildStage() != 0)
|
||||
return nullptr;
|
||||
|
||||
if (bld->jobs.size() == 0)
|
||||
return nullptr;
|
||||
|
||||
auto job = bld->jobs[0];
|
||||
if (job->flags.bits.suspend)
|
||||
return job;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
struct SuspendedBuilding
|
||||
{
|
||||
df::building *bld;
|
||||
df::coord pos;
|
||||
bool was_resumed;
|
||||
bool is_planned;
|
||||
|
||||
SuspendedBuilding(df::building *bld_) : bld(bld_), was_resumed(false), is_planned(false)
|
||||
{
|
||||
pos = df::coord(bld->centerx, bld->centery, bld->z);
|
||||
}
|
||||
|
||||
bool isValid()
|
||||
{
|
||||
return bld && Buildings::findAtTile(pos) == bld && get_suspended_job(bld);
|
||||
}
|
||||
};
|
||||
|
||||
static bool is_planned_building(df::building *bld)
|
||||
{
|
||||
auto L = Lua::Core::State;
|
||||
color_ostream_proxy out(Core::getInstance().getConsole());
|
||||
Lua::StackUnwinder top(L);
|
||||
|
||||
if (!lua_checkstack(L, 2) ||
|
||||
!Lua::PushModulePublic(
|
||||
out, L, "plugins.buildingplan", "isPlannedBuilding"))
|
||||
return false;
|
||||
|
||||
Lua::Push(L, bld);
|
||||
|
||||
if (!Lua::SafeCall(out, L, 1, 1))
|
||||
return false;
|
||||
|
||||
return lua_toboolean(L, -1);
|
||||
}
|
||||
|
||||
DFHACK_PLUGIN_IS_ENABLED(enabled);
|
||||
static bool buildings_scanned = false;
|
||||
static vector<SuspendedBuilding> suspended_buildings, resumed_buildings;
|
||||
|
||||
void scan_for_suspended_buildings()
|
||||
{
|
||||
if (buildings_scanned)
|
||||
return;
|
||||
|
||||
for (auto b = world->buildings.all.begin(); b != world->buildings.all.end(); b++)
|
||||
{
|
||||
auto bld = *b;
|
||||
auto job = get_suspended_job(bld);
|
||||
if (job)
|
||||
{
|
||||
SuspendedBuilding sb(bld);
|
||||
sb.is_planned = is_planned_building(bld);
|
||||
|
||||
auto it = resumed_buildings.begin();
|
||||
|
||||
for (; it != resumed_buildings.end(); ++it)
|
||||
if (it->bld == bld) break;
|
||||
|
||||
sb.was_resumed = it != resumed_buildings.end();
|
||||
|
||||
suspended_buildings.push_back(sb);
|
||||
}
|
||||
}
|
||||
|
||||
buildings_scanned = true;
|
||||
}
|
||||
|
||||
void show_suspended_buildings()
|
||||
{
|
||||
int32_t vx, vy, vz;
|
||||
if (!Gui::getViewCoords(vx, vy, vz))
|
||||
return;
|
||||
|
||||
auto dims = Gui::getDwarfmodeViewDims();
|
||||
int left_margin = vx + dims.map_x2;
|
||||
int bottom_margin = vy + dims.map_y2 - 1;
|
||||
|
||||
for (auto sb = suspended_buildings.begin(); sb != suspended_buildings.end();)
|
||||
{
|
||||
if (!sb->isValid())
|
||||
{
|
||||
sb = suspended_buildings.erase(sb);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sb->bld->z == vz && sb->bld->centerx >= vx && sb->bld->centerx <= left_margin &&
|
||||
sb->bld->centery >= vy && sb->bld->centery <= bottom_margin)
|
||||
{
|
||||
int x = sb->bld->centerx - vx + 1;
|
||||
int y = sb->bld->centery - vy + 1;
|
||||
auto color = COLOR_YELLOW;
|
||||
if (sb->is_planned)
|
||||
color = COLOR_GREEN;
|
||||
else if (sb->was_resumed)
|
||||
color = COLOR_RED;
|
||||
|
||||
OutputString(color, x, y, "X", false, 0, 0, true /* map */);
|
||||
}
|
||||
|
||||
sb++;
|
||||
}
|
||||
}
|
||||
|
||||
void clear_scanned()
|
||||
{
|
||||
buildings_scanned = false;
|
||||
suspended_buildings.clear();
|
||||
}
|
||||
|
||||
void resume_suspended_buildings(color_ostream &out)
|
||||
{
|
||||
out << "Resuming all buildings." << endl;
|
||||
|
||||
for (auto isb = resumed_buildings.begin(); isb != resumed_buildings.end();)
|
||||
{
|
||||
if (isb->isValid())
|
||||
{
|
||||
isb++;
|
||||
continue;
|
||||
}
|
||||
|
||||
isb = resumed_buildings.erase(isb);
|
||||
}
|
||||
|
||||
scan_for_suspended_buildings();
|
||||
for (auto sb = suspended_buildings.begin(); sb != suspended_buildings.end(); sb++)
|
||||
{
|
||||
if (sb->is_planned)
|
||||
continue;
|
||||
|
||||
resumed_buildings.push_back(*sb);
|
||||
sb->bld->jobs[0]->flags.bits.suspend = false;
|
||||
}
|
||||
|
||||
clear_scanned();
|
||||
|
||||
out << resumed_buildings.size() << " buildings resumed" << endl;
|
||||
}
|
||||
|
||||
|
||||
//START Viewscreen Hook
|
||||
struct resume_hook : public df::viewscreen_dwarfmodest
|
||||
{
|
||||
//START UI Methods
|
||||
typedef df::viewscreen_dwarfmodest interpose_base;
|
||||
|
||||
DEFINE_VMETHOD_INTERPOSE(void, render, ())
|
||||
{
|
||||
INTERPOSE_NEXT(render)();
|
||||
|
||||
if (enabled && DFHack::World::ReadPauseState() && ui->main.mode == ui_sidebar_mode::Default)
|
||||
{
|
||||
if (*process_jobs)
|
||||
{
|
||||
// something just created some buildings. rescan.
|
||||
clear_scanned();
|
||||
}
|
||||
scan_for_suspended_buildings();
|
||||
show_suspended_buildings();
|
||||
}
|
||||
else
|
||||
{
|
||||
clear_scanned();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
IMPLEMENT_VMETHOD_INTERPOSE(resume_hook, render);
|
||||
|
||||
DFhackCExport command_result plugin_enable ( color_ostream &out, bool enable)
|
||||
{
|
||||
if (!gps)
|
||||
return CR_FAILURE;
|
||||
|
||||
if (enabled != enable)
|
||||
{
|
||||
clear_scanned();
|
||||
|
||||
if (!INTERPOSE_HOOK(resume_hook, render).apply(enable))
|
||||
return CR_FAILURE;
|
||||
|
||||
enabled = enable;
|
||||
}
|
||||
|
||||
return CR_OK;
|
||||
}
|
||||
|
||||
static command_result resume_cmd(color_ostream &out, vector <string> & parameters)
|
||||
{
|
||||
bool show_help = false;
|
||||
if (parameters.empty())
|
||||
{
|
||||
show_help = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
auto cmd = parameters[0][0];
|
||||
if (cmd == 'v')
|
||||
{
|
||||
out << "Resume" << endl << "Version: " << PLUGIN_VERSION << endl;
|
||||
}
|
||||
else if (cmd == 's')
|
||||
{
|
||||
plugin_enable(out, true);
|
||||
out << "Overlay enabled" << endl;
|
||||
}
|
||||
else if (cmd == 'h')
|
||||
{
|
||||
plugin_enable(out, false);
|
||||
out << "Overlay disabled" << endl;
|
||||
}
|
||||
else if (cmd == 'a')
|
||||
{
|
||||
resume_suspended_buildings(out);
|
||||
}
|
||||
else
|
||||
{
|
||||
show_help = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (show_help)
|
||||
return CR_WRONG_USAGE;
|
||||
|
||||
return CR_OK;
|
||||
}
|
||||
|
||||
DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <PluginCommand> &commands)
|
||||
{
|
||||
commands.push_back(
|
||||
PluginCommand(
|
||||
"resume",
|
||||
"Mark suspended constructions on the map and easily resume them.",
|
||||
resume_cmd));
|
||||
|
||||
return CR_OK;
|
||||
}
|
||||
|
||||
|
||||
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event)
|
||||
{
|
||||
switch (event) {
|
||||
case SC_MAP_LOADED:
|
||||
suspended_buildings.clear();
|
||||
resumed_buildings.clear();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return CR_OK;
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
User-agent: *
|
||||
|
||||
Allow: /en/stable/
|
||||
|
||||
Sitemap: https://docs.dfhack.org/sitemap.xml
|
@ -1 +1 @@
|
||||
Subproject commit 020f2466bc4462e59c1c16c036881907cad9718e
|
||||
Subproject commit 727e4921c00e260d7c8d1112daf77115ce3960ee
|
Loading…
Reference in New Issue