Closed myk002 closed 1 year ago
Additional random thoughts:
?
button to the right of each menu item that, when clicked, runs gui/launcher
with the command pre-populated so its help is displayedhotkeys
command to run overlay trigger hotkeys_menu
when run with no arguments (this will work even if overlay is disabled)hotkeys list
to output hotkeys to the console
Motivation
The current implementation of the overlay has some issues:
This proposal seeks to solve these shortcomings. In addition, by supplying an overlay framework that other plugins/scripts can use to display information and respond to hotkeys, we can simplify many other DFHack tools and make it easier for new tools to supply viewscreen-bound functionality. Among other things, this solves the issue where scripts that want to overlay information over an existing screen have to add a new viewscreen to the stack, potentially breaking keybindings attached to that screen.
UX
The "[ DFHack Launcher ]" overlay button can be reduced to a 2x2 hotspot (in the upper left corner by default, but the position will be user-configurable). The hotspot could have a rendered image on some screens (title screen, main map screens, etc) and be invisible on others, but will always be reactive as a hotspot. Once we know more about how the steam renderer works, this can be changed to always rendering a semi-transparent DFHack icon, but we'll take what we can get for this version.
When the mouse rolls over that tile (or Ctrl-Shift-C is hit), a widget grows to show the tools whose hotkeys are associated with the current screen. This list will always include the launcher and (when it's ready) the control panel since their hotkeys are global. The hotkey for the menu itself would be filtered out.
The list will be scrollable with the mouse and keyboard and a short description of the selected tool (selected either by up/down arrow keys or by hovering the mouse cursor over the tool name) will be shown in an adjacent text box.
When the player clicks on a tool, hits enter, or hits a hotkey listed next to one of the tools, that tool is invoked and the widget disappears.
If the player moves the mouse from inside the widget area to outside the widget area, hits the menu hotkey, or presses Esc, then the menu closes without running a tool. If the mouse is still within the hotspot area, it will be ignored until it moves.
On the DF title screen we can display some text indicating where the hotspot is and how to use it. Something like:
Overlay implementation
Overlay will be a framework that will manage overlay widgets provided by other tools (including 3rd party tools). A widget is registered with overlay via a json metadata file in the
dfhack-config/overlay/
directory (see a few sections down for the metadata file format).Overlay will provide two primary services:
The unbound widgets will never have their
render()
oronInput()
methods called they will not be interposed into any viewscreen's context. Even though we have access to the raw renderer, hooking into the raw renderer without interposing a screen'srender()
function is too risky at this point (that is, it's not well understood or trusted, doesn't play well with other renderers that want to hook, etc.). Widgets that are tied to a particular viewscreen will be interposed for render and feed when those viewscreens are active.Overlay will track the mouse in its
plugin_onupdate()
and notify widgets when the mouse enters/leaves their bounds. The widgets can render differently on hover (become larger, display different information, etc.) or even spawn Screens to take over the viewscreen stack if they want to (the menu will do this to gain access to user input). See the Widget API section below for more details.Menu widget
The menu widget metadata will be defined in
dfhack-config/overlay/hotkeys_menu.json
and widget code inplugins/lua/hotkeys.lua
. The menu will be associated with thehotkeys
plugin since it basically does the same thing, just in widget form. It will use all the same techniques and algorithms as thehotkeys
plugin. It seems to make sense to pair them.When the mouse enters the hotspot, the menu widget will appear. It can animate when it is shown to evoke the sense that it is growing from the hotspot. I found a public domain easing library we can use: https://luapower.com/easing The light reading I've done indicates that a ~100ms animation should be appropriate so that it smooths the appearance of the widget without feeling jarring nor time-wasting.
We want to optimize for opening gui/launcher, so that item should appear under the cursor when the menu appears. Since the widget is repositionable, we'll need to place the list of tools dynamically so the launcher item always appears under the mouse cursor. We might have to add the items in reverse and start at the bottom of the list if the widget is too close to the bottom of the screen.
The Lua widget will query its parent
hotkeys
plugin for relevant keybindings and commands to populate its list, and will use the existing technique (and code) for launching context-specific commands by their hotkeys, even when the widget's DFHack Lua screen is the top viewscreen:dfhack/lua/overlay_menu
) with a command of the formoverlay menuselect <index>
hotkeys
doesn't clean up registered keybindings, and it might be hard to do so. Even if we implement a "clear keybindings by command", there's not an easy way to determine if the player has set a similar keybinding that we would erroneously clear. It might be possible to get a token back from the keybindingset
command that we could use to clear it later, but the benefit may not be worth it.Widget config
Each widget will be defined in a different json file in
dfhack-config/overlay/
so new widgets can be added easily, including by 3rd party tools. The dwarfmonitor widgets can move to this framework (with their backend logic staying in the dwarfmonitor plugin).The name of the widget will be the name of the json file (without the .json extension) to ensure there are no naming conflicts. The suggested naming scheme will be
<pluginorscriptname>_<identifier>
.The json file will keep configuration and state:
require
orreqscript
it, depending on type)Widget API
Overlay widgets must inherit from
overlay.OverlayWidget
. Theframe
attribute will be set upon init to position the widget on the screen. The overlay widget can modifyself.frame.w
andself.frame.h
at any time (ininit()
or in any of the callbacks) to indicate a new size. Overlay widgets can override theoverlay_onupdate_max_freq_seconds
attribute (default value: 5.0) if they want to have theiroverlay_onupdate()
callback called more (or less) frequently. Set it to 0 to get called at the maximum rate. Be aware that running more often than you really need to will impact game FPS, especially if your widget is bound to the main map screen.Overlay will instantiate and own the widgets. The instantiated widgets must not be added as subviews to any other View.
A widget class can define the following functions for integration with the overlay framework:
bool = overlay_onupdate(viewscreen)
if defined, will be called on every viewscreenlogic()
execution, but no more frequently than what is specified in theoverlay_onupdate_max_freq_seconds
class attribute. Widgets that need to update their state according to game changes can do it here. Theviewscreen
parameter is the viewscreen that this widget is attached to at the moment. For hotspot widgets,viewscreen
will benil
. Returns whether overlay should subsequently call the widget'soverlay_trigger()
function.screen = overlay_trigger()
if defined, will be called when theoverlay_onupdate
callback returnstrue
or when the player uses the CLI (or a keybinding calling the CLI) to trigger the widget. must return eithernil
or the Screen object that the widget code has allocated, shown, and now owns. Overlay widgets will receive no callbacks until the returned screen is dismissed. Unbound hotspot widgets must allocate a Screen if they want to react to theonInput()
feed or be rendered. The widgets owned by the overlay must not be attached to that new screen, but the returned screen can instantiate and configure new views.overlay_onupdate()
will always get called for hotspots. Un-hotspotted widgets bound to particular viewscreens only get callbacks called when the relevant functions of the viewscreen are called (that is, the widget will be rendered when that viewscreen'srender()
function is run; the widget will get itsonInput(keys)
function called when the viewscreen'sfeed()
function is run; theoverlay_onupdate(viewscreen)
function is called when that viewscren'slogic()
function is run).Performance considerations
The overlay will be interposing every DF viewscreen and calling out to Lua on every call to
logic()
,render()
, andfeed()
. We need to be aware of processing times so we don't cause fps drain.We will measure overlay processing times with high resolution timers and report SLO violations as warnings in the debug log. More detailed performance statistics will be accessible via some CLI command.
The goal is to avoid noticeably changing display and response latency at 100 FPS on average.
According to a handy online reference, 1 frame at 100fps is 10ms (yay math!).
So let's say initial targets (aggregated across all widgets) are:
If we find ourselves going over these limits, then players are being negatively impacted and we need to make changes. We can tune how much processing is done per render/logic and how often that processing is done in offending widgets by adjusting the
overlay_onupdate_max_freq_seconds
attribute.To get per-widget performance characteristics, we can potentially use the chronos Lua library (MIT license). If that turns out to be infeasible, we can measure from C++, but we may not be able to get per-widget timings (unless we call into lua individually for each widget, which may come with its own performance penalty).
Overlay CLI
overlay
(as a command) will provide the following subcommands:trigger <widget name>
(only if the widget implements theoverlay_trigger()
function and its associated screen (if any) is currently on top)list
(also indicates which can be triggered and which can be triggered right now)enable|disable <widget name>
(persisted to disk)reposition <widget name> <screen position>
(persisted to disk)stealth
to temporarily not render the widgets until the next screen transition (triggered widgets can still render)Overlay GUI config
gui/overlay
can be the config screen for theoverlay
plugin. It shows the list of widgets and allows them to be enabled/disabled. As a widget is selected in the list, its on-screen position will be highlighted. Enabled widgets can be clicked and dragged around the screen to reposition them, or a "reposition" button can be selected (via mouse or keyboard) that will allow a widget to be moved with the keyboard arrow keys (enter to confirm, esc to cancel). Widgets will be rendered, but will not be sent callback events while the gui config screen is active. Invisible hotspots will have a representative outline rendered. The json files will be updated accordingly.