diff --git a/NEWS b/NEWS
index fd92f620a..4e1736a86 100644
--- a/NEWS
+++ b/NEWS
@@ -5,6 +5,9 @@ DFHack future
Fixes:
- autotrade: "Mark all" no longer double-marks bin contents
+ New plugins:
+ - trackstop: Shows track stop friction and dump direction in its 'q' menu
+
DFHack 0.40.13-r1
Internals:
- unified spatter structs
diff --git a/Readme.html b/Readme.html
index bd9ea6aa5..4a20a419c 100644
--- a/Readme.html
+++ b/Readme.html
@@ -529,6 +529,7 @@ access DF memory and allow for easier development of new tools.
When querying a stockpile an option will appear to toggle automelt for this stockpile.
Any items placed in this stockpile will be designated to be melted.
+
This script allows to perform jobs in adventure mode. For more complete help
diff --git a/Readme.rst b/Readme.rst
index 2eb75b6b1..fb5d39df3 100644
--- a/Readme.rst
+++ b/Readme.rst
@@ -2655,6 +2655,19 @@ Enable the automelt plugin in your dfhack.init with::
When querying a stockpile an option will appear to toggle automelt for this stockpile.
Any items placed in this stockpile will be designated to be melted.
+Track Stop Menu
+===============
+
+The `q` menu of track stops is completely blank by default. To enable one::
+
+ enable trackstop
+
+This allows you to view and/or change the track stop's friction and dump direction settings.
+It re-uses the keybindings from the track stop building interface:
+
+* BUILDING_TRACK_STOP_FRICTION_UP
+* BUILDING_TRACK_STOP_FRICTION_DOWN
+* BUILDING_TRACK_STOP_DUMP
gui/advfort
===========
diff --git a/dfhack.init-example b/dfhack.init-example
index f0f380095..5338f9448 100644
--- a/dfhack.init-example
+++ b/dfhack.init-example
@@ -183,7 +183,7 @@ enable search
enable automaterial
# Other interface improvement tools
-enable dwarfmonitor mousequery automelt autotrade buildingplan resume zone
+enable dwarfmonitor mousequery automelt autotrade buildingplan resume trackstop zone
# allow the fortress bookkeeper to queue jobs through the manager
stockflow enable
diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt
index 6ebe3ad6d..35b7ca52d 100644
--- a/plugins/CMakeLists.txt
+++ b/plugins/CMakeLists.txt
@@ -156,6 +156,7 @@ if (BUILD_SUPPORTED)
DFHACK_PLUGIN(stocks stocks.cpp)
DFHACK_PLUGIN(strangemood strangemood.cpp)
DFHACK_PLUGIN(tiletypes tiletypes.cpp Brushes.h)
+ DFHACK_PLUGIN(trackstop trackstop.cpp)
# DFHACK_PLUGIN(treefarm treefarm.cpp)
DFHACK_PLUGIN(tubefill tubefill.cpp)
DFHACK_PLUGIN(tweak tweak.cpp)
diff --git a/plugins/trackstop.cpp b/plugins/trackstop.cpp
new file mode 100644
index 000000000..4676a1b52
--- /dev/null
+++ b/plugins/trackstop.cpp
@@ -0,0 +1,301 @@
+/*
+ * Trackstop plugin.
+ * Shows track stop friction and direction in its 'q' menu.
+ */
+
+#include "uicommon.h"
+#include "LuaTools.h"
+
+#include "df/building_rollersst.h"
+#include "df/building_trapst.h"
+#include "df/viewscreen_dwarfmodest.h"
+
+#include "modules/Gui.h"
+
+using namespace DFHack;
+using namespace std;
+
+using df::global::world;
+using df::global::ui;
+using df::building_rollersst;
+using df::building_trapst;
+using df::enums::trap_type::trap_type;
+using df::enums::screw_pump_direction::screw_pump_direction;
+
+DFHACK_PLUGIN("trackstop");
+
+#define AUTOENABLE false
+DFHACK_PLUGIN_IS_ENABLED(enabled);
+
+
+/*
+ * Interface hooks
+ */
+struct trackstop_hook : public df::viewscreen_dwarfmodest {
+ typedef df::viewscreen_dwarfmodest interpose_base;
+
+ enum Friction {
+ Lowest = 10,
+ Low = 50,
+ Medium = 500,
+ High = 10000,
+ Highest = 50000
+ };
+
+ building_trapst *get_selected_trackstop() {
+ if (!Gui::dwarfmode_hotkey(Core::getTopViewscreen()) || ui->main.mode != ui_sidebar_mode::QueryBuilding) {
+ return nullptr;
+ }
+
+ building_trapst *ts = virtual_cast(world->selected_building);
+ if (ts && ts->trap_type == trap_type::TrackStop && ts->construction_stage) {
+ return ts;
+ }
+
+ return nullptr;
+ }
+
+ bool handleInput(set *input) {
+ building_trapst *ts = get_selected_trackstop();
+ if (!ts) {
+ return false;
+ }
+
+ if (input->count(interface_key::BUILDING_TRACK_STOP_DUMP)) {
+ // Change track stop dump direction.
+ // There might be a more elegant way to do this.
+
+ if (!ts->use_dump) {
+ // No -> North
+ ts->use_dump = 1;
+ ts->dump_x_shift = 0;
+ ts->dump_y_shift = -1;
+ } else if (ts->dump_x_shift == 0 && ts->dump_y_shift == -1) {
+ // North -> South
+ ts->dump_x_shift = 0;
+ ts->dump_y_shift = 1;
+ } else if (ts->dump_x_shift == 0 && ts->dump_y_shift == 1) {
+ // South -> East
+ ts->dump_x_shift = 1;
+ ts->dump_y_shift = 0;
+ } else if (ts->dump_x_shift == 1 && ts->dump_y_shift == 0) {
+ // East -> West
+ ts->dump_x_shift = -1;
+ ts->dump_y_shift = 0;
+ } else {
+ // West (or Elsewhere) -> No
+ ts->use_dump = 0;
+ ts->dump_x_shift = 0;
+ ts->dump_y_shift = 0;
+ }
+
+ return true;
+ } else if (input->count(interface_key::BUILDING_TRACK_STOP_FRICTION_UP)) {
+ ts->friction = (
+ (ts->friction < Friction::Lowest)? Friction::Lowest:
+ (ts->friction < Friction::Low)? Friction::Low:
+ (ts->friction < Friction::Medium)? Friction::Medium:
+ (ts->friction < Friction::High)? Friction::High:
+ (ts->friction < Friction::Highest)? Friction::Highest:
+ ts->friction
+ );
+
+ return true;
+ } else if (input->count(interface_key::BUILDING_TRACK_STOP_FRICTION_DOWN)) {
+ ts->friction = (
+ (ts->friction > Friction::Highest)? Friction::Highest:
+ (ts->friction > Friction::High)? Friction::High:
+ (ts->friction > Friction::Medium)? Friction::Medium:
+ (ts->friction > Friction::Low)? Friction::Low:
+ (ts->friction > Friction::Lowest)? Friction::Lowest:
+ ts->friction
+ );
+
+ return true;
+ }
+
+ return false;
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) {
+ if (!handleInput(input)) {
+ INTERPOSE_NEXT(feed)(input);
+ }
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(void, render, ()) {
+ INTERPOSE_NEXT(render)();
+
+ building_trapst *ts = get_selected_trackstop();
+ if (ts) {
+ auto dims = Gui::getDwarfmodeViewDims();
+ int left_margin = dims.menu_x1 + 1;
+ int x = left_margin;
+ int y = dims.y1 + 1;
+
+ OutputString(COLOR_WHITE, x, y, "Track Stop", true, left_margin);
+
+ y += 3;
+ OutputString(COLOR_WHITE, x, y, "Friction: ", false);
+ OutputString(COLOR_WHITE, x, y, (
+ (ts->friction <= Friction::Lowest)? "Lowest":
+ (ts->friction <= Friction::Low)? "Low":
+ (ts->friction <= Friction::Medium)? "Medium":
+ (ts->friction <= Friction::High)? "High":
+ "Highest"
+ ), true, left_margin);
+ OutputString(COLOR_LIGHTRED, x, y, Screen::getKeyDisplay(interface_key::BUILDING_TRACK_STOP_FRICTION_DOWN));
+ OutputString(COLOR_LIGHTRED, x, y, Screen::getKeyDisplay(interface_key::BUILDING_TRACK_STOP_FRICTION_UP));
+ OutputString(COLOR_WHITE, x, y, ": Change Friction", true, left_margin);
+
+ y += 1;
+
+ OutputString(COLOR_WHITE, x, y, "Dump on arrival: ", false);
+ OutputString(COLOR_WHITE, x, y, (
+ (!ts->use_dump)? "No":
+ (ts->dump_x_shift == 0 && ts->dump_y_shift == -1)? "North":
+ (ts->dump_x_shift == 0 && ts->dump_y_shift == 1)? "South":
+ (ts->dump_x_shift == 1 && ts->dump_y_shift == 0)? "East":
+ (ts->dump_x_shift == -1 && ts->dump_y_shift == 0)? "West":
+ "Elsewhere"
+ ), true, left_margin);
+ OutputString(COLOR_LIGHTRED, x, y, Screen::getKeyDisplay(interface_key::BUILDING_TRACK_STOP_DUMP));
+ OutputString(COLOR_WHITE, x, y, ": Activate/change direction", true, left_margin);
+ }
+ }
+};
+
+struct roller_hook : public df::viewscreen_dwarfmodest {
+ typedef df::viewscreen_dwarfmodest interpose_base;
+
+ enum Speed {
+ Lowest = 10000,
+ Low = 20000,
+ Medium = 30000,
+ High = 40000,
+ Highest = 50000
+ };
+
+ building_rollersst *get_selected_roller() {
+ if (!Gui::dwarfmode_hotkey(Core::getTopViewscreen()) || ui->main.mode != ui_sidebar_mode::QueryBuilding) {
+ return nullptr;
+ }
+
+ building_rollersst *roller = virtual_cast(world->selected_building);
+ if (roller && roller->construction_stage) {
+ return roller;
+ }
+
+ return nullptr;
+ }
+
+ bool handleInput(set *input) {
+ building_rollersst *roller = get_selected_roller();
+ if (!roller) {
+ return false;
+ }
+
+ if (input->count(interface_key::BUILDING_ORIENT_NONE)) {
+ // Flip roller orientation.
+ // Long rollers can only be oriented along their length.
+ // Todo: Only add 1 to 1x1 rollers: x ^= ((x&1)<<1)|1
+ // Todo: This could have been elegant without all the casting,
+ // but as an enum it might be better off listing each case.
+ roller->direction = (df::enums::screw_pump_direction::screw_pump_direction)(((int8_t)roller->direction) ^ 2);
+ return true;
+ } else if (input->count(interface_key::BUILDING_ROLLERS_SPEED_UP)) {
+ if (roller->speed < Speed::Highest) {
+ roller->speed += Speed::Lowest;
+ }
+
+ return true;
+ } else if (input->count(interface_key::BUILDING_ROLLERS_SPEED_DOWN)) {
+ if (roller->speed > Speed::Lowest) {
+ roller->speed -= Speed::Lowest;
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) {
+ if (!handleInput(input)) {
+ INTERPOSE_NEXT(feed)(input);
+ }
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(void, render, ()) {
+ INTERPOSE_NEXT(render)();
+
+ building_rollersst *roller = get_selected_roller();
+ if (roller) {
+ auto dims = Gui::getDwarfmodeViewDims();
+ int left_margin = dims.menu_x1 + 1;
+ int x = left_margin;
+ int y = dims.y1 + 6;
+
+ OutputString(COLOR_LIGHTRED, x, y, Screen::getKeyDisplay(interface_key::BUILDING_ORIENT_NONE));
+ OutputString(COLOR_WHITE, x, y, ": Rolls ", false);
+ OutputString(COLOR_WHITE, x, y, (
+ (roller->direction == screw_pump_direction::FromNorth)? "Southward":
+ (roller->direction == screw_pump_direction::FromEast)? "Westward":
+ (roller->direction == screw_pump_direction::FromSouth)? "Northward":
+ (roller->direction == screw_pump_direction::FromWest)? "Eastward":
+ ""
+ ), true, left_margin);
+
+ OutputString(COLOR_LIGHTRED, x, y, Screen::getKeyDisplay(interface_key::BUILDING_ROLLERS_SPEED_DOWN));
+ OutputString(COLOR_LIGHTRED, x, y, Screen::getKeyDisplay(interface_key::BUILDING_ROLLERS_SPEED_UP));
+ OutputString(COLOR_WHITE, x, y, ": ");
+ OutputString(COLOR_WHITE, x, y, (
+ (roller->speed <= Speed::Lowest)? "Lowest":
+ (roller->speed <= Speed::Low)? "Low":
+ (roller->speed <= Speed::Medium)? "Medium":
+ (roller->speed <= Speed::High)? "High":
+ "Highest"
+ ));
+ OutputString(COLOR_WHITE, x, y, " Speed", true, left_margin);
+ }
+ }
+};
+
+IMPLEMENT_VMETHOD_INTERPOSE(trackstop_hook, feed);
+IMPLEMENT_VMETHOD_INTERPOSE(trackstop_hook, render);
+IMPLEMENT_VMETHOD_INTERPOSE(roller_hook, feed);
+IMPLEMENT_VMETHOD_INTERPOSE(roller_hook, render);
+
+
+DFhackCExport command_result plugin_enable(color_ostream& out, bool enable) {
+ // Accept the "enable trackstop" / "disable trackstop" commands.
+ if (enable != enabled) {
+ // Check for global variables that, if missing, result in total failure.
+ // Missing enabler and ui_menu_width also produce visible effects, but not nearly as severe.
+ // This could be moved to the plugin_init step, but that's louder for no real benefit.
+ if (!(gps && ui && world)) {
+ out.printerr("trackstop: Missing required global variables.\n");
+ return CR_FAILURE;
+ }
+
+ if (!INTERPOSE_HOOK(trackstop_hook, feed).apply(enable) ||
+ !INTERPOSE_HOOK(trackstop_hook, render).apply(enable) ||
+ !INTERPOSE_HOOK(roller_hook, feed).apply(enable) ||
+ !INTERPOSE_HOOK(roller_hook, render).apply(enable)) {
+ out.printerr("Could not %s trackstop hooks!\n", enable? "insert": "remove");
+ return CR_FAILURE;
+ }
+
+ enabled = enable;
+ }
+
+ return CR_OK;
+}
+
+DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) {
+ return plugin_enable(out, AUTOENABLE);
+}
+
+DFhackCExport command_result plugin_shutdown(color_ostream &out) {
+ return plugin_enable(out, false);
+}