From c050add7bc472ec9e6daa8b0e5752120c20dc03d Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 10 Jan 2026 12:57:04 +0100 Subject: [PATCH 1/2] Create plugin for clearing of combat, sparring, and hunting reports with configurable filtering and overlay UI. --- docs/changelog.txt | 1 + docs/plugins/logcleaner.rst | 62 ++++++++ plugins/CMakeLists.txt | 1 + plugins/logcleaner/logcleaner.cpp | 239 ++++++++++++++++++++++++++++++ plugins/lua/logcleaner.lua | 66 +++++++++ 5 files changed, 369 insertions(+) create mode 100644 docs/plugins/logcleaner.rst create mode 100644 plugins/logcleaner/logcleaner.cpp create mode 100644 plugins/lua/logcleaner.lua diff --git a/docs/changelog.txt b/docs/changelog.txt index da2aa1ef4e..377577da84 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -55,6 +55,7 @@ Template for new versions: # Future ## New Tools +- ``logcleaner``: New plugin for time-triggered clearing of combat, sparring, and hunting reports with configurable filtering and overlay UI. ## New Features diff --git a/docs/plugins/logcleaner.rst b/docs/plugins/logcleaner.rst new file mode 100644 index 0000000000..d9d0930d82 --- /dev/null +++ b/docs/plugins/logcleaner.rst @@ -0,0 +1,62 @@ +logcleaner +========== +.. dfhack-tool:: + :summary: Automatically clear combat, sparring, and hunting reports. + :tags: fort auto units + +This plugin prevents spam from cluttering your announcement history and filling +the 3000-item reports buffer. It runs every 100 ticks and clears selected report +types from both the global reports buffer and per-unit logs. + +Usage +----- + +Basic commands +~~~~~~~~~~~~~~ + +``logcleaner`` + Show the current status of the plugin. +``logcleaner enable`` + Enable the plugin (persists per save). +``logcleaner disable`` + Disable the plugin. + +Configuring filters +~~~~~~~~~~~~~~~~~~~ + +``logcleaner combat`` + Clear combat reports (also enables the plugin if disabled). +``logcleaner sparring`` + Clear sparring reports. +``logcleaner hunting`` + Clear hunting reports. +``logcleaner combat,sparring`` + Clear multiple report types (comma-separated). +``logcleaner all`` + Enable all three filter types. +``logcleaner none`` + Disable all filter types. + +Examples +~~~~~~~~ + +Clear only sparring reports:: + + logcleaner sparring + +Clear combat and hunting, but not sparring:: + + logcleaner combat,hunting + +Overlay UI +---------- + +Run ``gui/logcleaner`` to open the settings overlay, or access it from the +control panel under the Gameplay tab. + +The overlay provides: + +- **Enable toggle**: Turn the plugin on or off (``Shift+E``) +- **Combat toggle**: Clear combat reports (``Shift+C``) +- **Sparring toggle**: Clear sparring reports (``Shift+S``) +- **Hunting toggle**: Clear hunting reports (``Shift+H``) diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 355cc0ac4c..4a4423f48f 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -66,6 +66,7 @@ if(BUILD_SUPPORTED) dfhack_plugin(createitem createitem.cpp) dfhack_plugin(cursecheck cursecheck.cpp) dfhack_plugin(cxxrandom cxxrandom.cpp LINK_LIBRARIES lua) + dfhack_plugin(logcleaner logcleaner/logcleaner.cpp LINK_LIBRARIES lua) dfhack_plugin(deramp deramp.cpp) dfhack_plugin(debug debug.cpp LINK_LIBRARIES jsoncpp_static) dfhack_plugin(dig dig.cpp LINK_LIBRARIES lua) diff --git a/plugins/logcleaner/logcleaner.cpp b/plugins/logcleaner/logcleaner.cpp new file mode 100644 index 0000000000..b6be95a70e --- /dev/null +++ b/plugins/logcleaner/logcleaner.cpp @@ -0,0 +1,239 @@ +#include "Debug.h" +#include "LuaTools.h" +#include "PluginManager.h" +#include "PluginLua.h" + +#include "modules/Persistence.h" +#include "modules/World.h" + +#include +#include +#include +#include + +using namespace DFHack; + +DFHACK_PLUGIN("logcleaner"); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); + +REQUIRE_GLOBAL(world); + +static const std::string CONFIG_KEY = std::string(plugin_name) + "/config"; +static PersistentDataItem config; + +enum ConfigValues { + CONFIG_IS_ENABLED = 0, + CONFIG_CLEAR_COMBAT = 1, + CONFIG_CLEAR_SPARING = 2, + CONFIG_CLEAR_HUNTING = 3, +}; + +static bool clear_combat = false; +static bool clear_sparring = true; +static bool clear_hunting = false; + +namespace DFHack { + DBG_DECLARE(logcleaner, control, DebugCategory::LINFO); + DBG_DECLARE(logcleaner, cleanup, DebugCategory::LINFO); +} + +static void cleanupLogs(color_ostream& out); +static command_result do_command(color_ostream& out, std::vector& params); +static void do_enable(); +static void do_disable(); + +// Getter functions for Lua +static bool logcleaner_getCombat() { return clear_combat; } +static bool logcleaner_getSparring() { return clear_sparring; } +static bool logcleaner_getHunting() { return clear_hunting; } + +// Setter functions for Lua (also persist to config) +static void logcleaner_setCombat(color_ostream& out, bool val) { + clear_combat = val; + config.set_bool(CONFIG_CLEAR_COMBAT, clear_combat); +} + +static void logcleaner_setSparring(color_ostream& out, bool val) { + clear_sparring = val; + config.set_bool(CONFIG_CLEAR_SPARING, clear_sparring); +} + +static void logcleaner_setHunting(color_ostream& out, bool val) { + clear_hunting = val; + config.set_bool(CONFIG_CLEAR_HUNTING, clear_hunting); +} + +DFhackCExport command_result plugin_init(color_ostream& out, std::vector& commands) { + commands.push_back(PluginCommand( + plugin_name, + "Prevent report buffer from filling up by clearing selected report types (combat, sparring, hunting).", + do_command)); + + return CR_OK; +} + +static command_result do_command(color_ostream& out, std::vector& params) { + if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { + out.printerr("Cannot use {} without a loaded fort.\n", plugin_name); + return CR_FAILURE; + } + + bool show_help = false; + if (!Lua::CallLuaModuleFunction(out, "plugins.logcleaner", "parse_commandline", params, + 1, [&](lua_State *L) { + show_help = !lua_toboolean(L, -1); + })) { + return CR_FAILURE; + } + + return show_help ? CR_WRONG_USAGE : CR_OK; +} + +static void do_enable() { +} + +static void do_disable() { +} + +DFhackCExport command_result plugin_enable(color_ostream& out, bool enable) { + if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); + return CR_FAILURE; + } + + if (enable != is_enabled) { + is_enabled = enable; + DEBUG(control, out).print("{} from the API; persisting\n", + is_enabled ? "enabled" : "disabled"); + config.set_bool(CONFIG_IS_ENABLED, is_enabled); + if (enable) + do_enable(); + else + do_disable(); + } else { + DEBUG(control, out).print("{} from the API, but already {}; no action\n", + is_enabled ? "enabled" : "disabled", + is_enabled ? "enabled" : "disabled"); + } + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown(color_ostream& out) { + DEBUG(control, out).print("shutting down {}\n", plugin_name); + return CR_OK; +} + +DFhackCExport command_result plugin_load_site_data(color_ostream& out) { + config = World::GetPersistentSiteData(CONFIG_KEY); + + if (!config.isValid()) { + DEBUG(control, out).print("no config found in this save; initializing\n"); + config = World::AddPersistentSiteData(CONFIG_KEY); + config.set_bool(CONFIG_IS_ENABLED, is_enabled); + config.set_bool(CONFIG_CLEAR_COMBAT, clear_combat); + config.set_bool(CONFIG_CLEAR_SPARING, clear_sparring); + config.set_bool(CONFIG_CLEAR_HUNTING, clear_hunting); + } + + is_enabled = config.get_bool(CONFIG_IS_ENABLED); + clear_combat = config.get_bool(CONFIG_CLEAR_COMBAT); + clear_sparring = config.get_bool(CONFIG_CLEAR_SPARING); + clear_hunting = config.get_bool(CONFIG_CLEAR_HUNTING); + + DEBUG(control, out).print("loading persisted enabled state: {}\n", + is_enabled ? "true" : "false"); + if (is_enabled) + do_enable(); + + return CR_OK; +} + +DFhackCExport command_result plugin_onstatechange(color_ostream& out, state_change_event event) { + if (event == DFHack::SC_WORLD_UNLOADED && is_enabled) { + DEBUG(control, out).print("world unloaded; disabling {}\n", plugin_name); + is_enabled = false; + do_disable(); + } + return CR_OK; +} + +static void cleanupLogs(color_ostream& out) { + if (!is_enabled || !world) + return; + + // Collect all report IDs from unit combat/sparring/hunting logs + std::unordered_set report_ids_to_remove; + + for (auto unit : world->units.all) { + // Combat logs (index 0) + if (clear_combat) { + auto& log = unit->reports.log[0]; + for (auto report_id : log) { + report_ids_to_remove.insert(report_id); + } + log.clear(); + } + // Sparring logs (index 1) + if (clear_sparring) { + auto& log = unit->reports.log[1]; + for (auto report_id : log) { + report_ids_to_remove.insert(report_id); + } + log.clear(); + } + // Hunting logs (index 2) + if (clear_hunting) { + auto& log = unit->reports.log[2]; + for (auto report_id : log) { + report_ids_to_remove.insert(report_id); + } + log.clear(); + } + } + + if (report_ids_to_remove.empty()) + return; + + // Remove collected reports from global buffers + auto& reports = world->status.reports; + + int reports_erased = 0; + + for (auto report_id : report_ids_to_remove) { + df::report* report = df::report::find(report_id); + if (!report) + continue; + + auto it = std::find(reports.begin(), reports.end(), report); + if (it != reports.end()) { + delete report; + reports.erase(it); + reports_erased++; + } + } +} + +DFhackCExport command_result plugin_onupdate(color_ostream& out, state_change_event event) { + static int32_t tick_counter = 0; + + if (!is_enabled || !world) + return CR_OK; + + tick_counter++; + if (tick_counter >= 100) { + tick_counter = 0; + cleanupLogs(out); + } + + return CR_OK; +} + +DFHACK_PLUGIN_LUA_FUNCTIONS { + DFHACK_LUA_FUNCTION(logcleaner_getCombat), + DFHACK_LUA_FUNCTION(logcleaner_getSparring), + DFHACK_LUA_FUNCTION(logcleaner_getHunting), + DFHACK_LUA_FUNCTION(logcleaner_setCombat), + DFHACK_LUA_FUNCTION(logcleaner_setSparring), + DFHACK_LUA_FUNCTION(logcleaner_setHunting), + DFHACK_LUA_END +}; diff --git a/plugins/lua/logcleaner.lua b/plugins/lua/logcleaner.lua new file mode 100644 index 0000000000..f7fa328ea6 --- /dev/null +++ b/plugins/lua/logcleaner.lua @@ -0,0 +1,66 @@ +local _ENV = mkmodule('plugins.logcleaner') + +local function print_status() + print(('logcleaner is %s'):format(isEnabled() and "enabled" or "disabled")) + print(' Combat: ' .. (logcleaner_getCombat() and 'enabled' or 'disabled')) + print(' Sparring: ' .. (logcleaner_getSparring() and 'enabled' or 'disabled')) + print(' Hunting: ' .. (logcleaner_getHunting() and 'enabled' or 'disabled')) +end + +function parse_commandline(...) + local args = {...} + local command = args[1] + + -- Show status if no command or "status" + if not command or command == 'status' then + print_status() + return true + end + + -- Start with all disabled, enable only what's specified + local new_combat, new_sparring, new_hunting = false, false, false + local has_filter = false + + for _, param in ipairs(args) do + if param == 'all' then + new_combat, new_sparring, new_hunting = true, true, true + has_filter = true + elseif param == 'none' then + new_combat, new_sparring, new_hunting = false, false, false + else + -- Split by comma for multiple options in one parameter + for token in param:gmatch('([^,]+)') do + if token == 'combat' then + new_combat = true + has_filter = true + elseif token == 'sparring' then + new_sparring = true + has_filter = true + elseif token == 'hunting' then + new_hunting = true + has_filter = true + else + dfhack.printerr('Unknown option: ' .. token) + return false + end + end + end + end + + -- Auto-enable plugin when filters are being configured + if has_filter and not isEnabled() then + dfhack.run_command('enable', 'logcleaner') + print('logcleaner enabled') + end + + logcleaner_setCombat(new_combat) + logcleaner_setSparring(new_sparring) + logcleaner_setHunting(new_hunting) + + print('Log cleaning config updated:') + print_status() + + return true +end + +return _ENV From 67b26213c5c7db21db12f93914c87e53b8150700 Mon Sep 17 00:00:00 2001 From: pajawojciech Date: Sat, 10 Jan 2026 15:08:31 +0100 Subject: [PATCH 2/2] Remove debugs, add enable / disable, refactor --- plugins/logcleaner/logcleaner.cpp | 72 +++++-------------------------- plugins/lua/logcleaner.lua | 63 +++++++++++++++++---------- 2 files changed, 52 insertions(+), 83 deletions(-) diff --git a/plugins/logcleaner/logcleaner.cpp b/plugins/logcleaner/logcleaner.cpp index b6be95a70e..56804613ad 100644 --- a/plugins/logcleaner/logcleaner.cpp +++ b/plugins/logcleaner/logcleaner.cpp @@ -1,4 +1,3 @@ -#include "Debug.h" #include "LuaTools.h" #include "PluginManager.h" #include "PluginLua.h" @@ -32,15 +31,8 @@ static bool clear_combat = false; static bool clear_sparring = true; static bool clear_hunting = false; -namespace DFHack { - DBG_DECLARE(logcleaner, control, DebugCategory::LINFO); - DBG_DECLARE(logcleaner, cleanup, DebugCategory::LINFO); -} - static void cleanupLogs(color_ostream& out); static command_result do_command(color_ostream& out, std::vector& params); -static void do_enable(); -static void do_disable(); // Getter functions for Lua static bool logcleaner_getCombat() { return clear_combat; } @@ -48,17 +40,17 @@ static bool logcleaner_getSparring() { return clear_sparring; } static bool logcleaner_getHunting() { return clear_hunting; } // Setter functions for Lua (also persist to config) -static void logcleaner_setCombat(color_ostream& out, bool val) { +static void logcleaner_setCombat(bool val) { clear_combat = val; config.set_bool(CONFIG_CLEAR_COMBAT, clear_combat); } -static void logcleaner_setSparring(color_ostream& out, bool val) { +static void logcleaner_setSparring(bool val) { clear_sparring = val; config.set_bool(CONFIG_CLEAR_SPARING, clear_sparring); } -static void logcleaner_setHunting(color_ostream& out, bool val) { +static void logcleaner_setHunting(bool val) { clear_hunting = val; config.set_bool(CONFIG_CLEAR_HUNTING, clear_hunting); } @@ -89,12 +81,6 @@ static command_result do_command(color_ostream& out, std::vector& p return show_help ? CR_WRONG_USAGE : CR_OK; } -static void do_enable() { -} - -static void do_disable() { -} - DFhackCExport command_result plugin_enable(color_ostream& out, bool enable) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); @@ -103,23 +89,12 @@ DFhackCExport command_result plugin_enable(color_ostream& out, bool enable) { if (enable != is_enabled) { is_enabled = enable; - DEBUG(control, out).print("{} from the API; persisting\n", - is_enabled ? "enabled" : "disabled"); config.set_bool(CONFIG_IS_ENABLED, is_enabled); - if (enable) - do_enable(); - else - do_disable(); - } else { - DEBUG(control, out).print("{} from the API, but already {}; no action\n", - is_enabled ? "enabled" : "disabled", - is_enabled ? "enabled" : "disabled"); } return CR_OK; } DFhackCExport command_result plugin_shutdown(color_ostream& out) { - DEBUG(control, out).print("shutting down {}\n", plugin_name); return CR_OK; } @@ -127,7 +102,6 @@ DFhackCExport command_result plugin_load_site_data(color_ostream& out) { config = World::GetPersistentSiteData(CONFIG_KEY); if (!config.isValid()) { - DEBUG(control, out).print("no config found in this save; initializing\n"); config = World::AddPersistentSiteData(CONFIG_KEY); config.set_bool(CONFIG_IS_ENABLED, is_enabled); config.set_bool(CONFIG_CLEAR_COMBAT, clear_combat); @@ -140,19 +114,12 @@ DFhackCExport command_result plugin_load_site_data(color_ostream& out) { clear_sparring = config.get_bool(CONFIG_CLEAR_SPARING); clear_hunting = config.get_bool(CONFIG_CLEAR_HUNTING); - DEBUG(control, out).print("loading persisted enabled state: {}\n", - is_enabled ? "true" : "false"); - if (is_enabled) - do_enable(); - return CR_OK; } DFhackCExport command_result plugin_onstatechange(color_ostream& out, state_change_event event) { if (event == DFHack::SC_WORLD_UNLOADED && is_enabled) { - DEBUG(control, out).print("world unloaded; disabling {}\n", plugin_name); is_enabled = false; - do_disable(); } return CR_OK; } @@ -163,31 +130,17 @@ static void cleanupLogs(color_ostream& out) { // Collect all report IDs from unit combat/sparring/hunting logs std::unordered_set report_ids_to_remove; + bool log_types[] = {clear_combat, clear_sparring, clear_hunting}; for (auto unit : world->units.all) { - // Combat logs (index 0) - if (clear_combat) { - auto& log = unit->reports.log[0]; - for (auto report_id : log) { - report_ids_to_remove.insert(report_id); - } - log.clear(); - } - // Sparring logs (index 1) - if (clear_sparring) { - auto& log = unit->reports.log[1]; - for (auto report_id : log) { - report_ids_to_remove.insert(report_id); + for (int log_idx = 0; log_idx < 3; log_idx++) { + if (log_types[log_idx]) { + auto& log = unit->reports.log[log_idx]; + for (auto report_id : log) { + report_ids_to_remove.insert(report_id); + } + log.clear(); } - log.clear(); - } - // Hunting logs (index 2) - if (clear_hunting) { - auto& log = unit->reports.log[2]; - for (auto report_id : log) { - report_ids_to_remove.insert(report_id); - } - log.clear(); } } @@ -197,8 +150,6 @@ static void cleanupLogs(color_ostream& out) { // Remove collected reports from global buffers auto& reports = world->status.reports; - int reports_erased = 0; - for (auto report_id : report_ids_to_remove) { df::report* report = df::report::find(report_id); if (!report) @@ -208,7 +159,6 @@ static void cleanupLogs(color_ostream& out) { if (it != reports.end()) { delete report; reports.erase(it); - reports_erased++; } } } diff --git a/plugins/lua/logcleaner.lua b/plugins/lua/logcleaner.lua index f7fa328ea6..104c23ab88 100644 --- a/plugins/lua/logcleaner.lua +++ b/plugins/lua/logcleaner.lua @@ -17,32 +17,51 @@ function parse_commandline(...) return true end + -- Handle enable/disable commands + if command == 'enable' then + if isEnabled() then + print('logcleaner is already enabled') + else + dfhack.run_command('enable', 'logcleaner') + print('logcleaner enabled') + end + return true + end + + if command == 'disable' then + if not isEnabled() then + print('logcleaner is already disabled') + else + dfhack.run_command('disable', 'logcleaner') + print('logcleaner disabled') + end + return true + end + -- Start with all disabled, enable only what's specified local new_combat, new_sparring, new_hunting = false, false, false local has_filter = false - for _, param in ipairs(args) do - if param == 'all' then - new_combat, new_sparring, new_hunting = true, true, true - has_filter = true - elseif param == 'none' then - new_combat, new_sparring, new_hunting = false, false, false - else - -- Split by comma for multiple options in one parameter - for token in param:gmatch('([^,]+)') do - if token == 'combat' then - new_combat = true - has_filter = true - elseif token == 'sparring' then - new_sparring = true - has_filter = true - elseif token == 'hunting' then - new_hunting = true - has_filter = true - else - dfhack.printerr('Unknown option: ' .. token) - return false - end + if command == 'all' then + new_combat, new_sparring, new_hunting = true, true, true + has_filter = true + elseif command == 'none' then + new_combat, new_sparring, new_hunting = false, false, false + else + -- Split by comma for multiple options in one parameter + for token in command:gmatch('([^,]+)') do + if token == 'combat' then + new_combat = true + has_filter = true + elseif token == 'sparring' then + new_sparring = true + has_filter = true + elseif token == 'hunting' then + new_hunting = true + has_filter = true + else + dfhack.printerr('Unknown option: ' .. token) + return false end end end