Skip to content

Commit 58e6804

Browse files
authored
Merge pull request #5704 from pajawojciech/log-cleaner
Create plugin for clearing of combat, sparring, and hunting reports with configurable filtering and overlay UI
2 parents bbfabe6 + c011d31 commit 58e6804

5 files changed

Lines changed: 338 additions & 0 deletions

File tree

docs/changelog.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ Template for new versions:
5555
# Future
5656

5757
## New Tools
58+
- ``logcleaner``: New plugin for time-triggered clearing of combat, sparring, and hunting reports with configurable filtering and overlay UI.
5859

5960
## New Features
6061
- `orders`: added search overlay to find and navigate to matching manager orders with arrow indicators

docs/plugins/logcleaner.rst

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
logcleaner
2+
==========
3+
.. dfhack-tool::
4+
:summary: Automatically clear combat, sparring, and hunting reports.
5+
:tags: fort auto units
6+
7+
This plugin prevents spam from cluttering your announcement history and filling
8+
the 3000-item reports buffer. It runs every 100 ticks and clears selected report
9+
types from both the global reports buffer and per-unit logs.
10+
11+
Usage
12+
-----
13+
14+
Basic commands
15+
~~~~~~~~~~~~~~
16+
17+
``logcleaner``
18+
Show the current status of the plugin.
19+
``logcleaner enable``
20+
Enable the plugin (persists per save).
21+
``logcleaner disable``
22+
Disable the plugin.
23+
24+
Configuring filters
25+
~~~~~~~~~~~~~~~~~~~
26+
27+
``logcleaner combat``
28+
Clear combat reports (also enables the plugin if disabled).
29+
``logcleaner sparring``
30+
Clear sparring reports.
31+
``logcleaner hunting``
32+
Clear hunting reports.
33+
``logcleaner combat,sparring``
34+
Clear multiple report types (comma-separated).
35+
``logcleaner all``
36+
Enable all three filter types.
37+
``logcleaner none``
38+
Disable all filter types.
39+
40+
Examples
41+
~~~~~~~~
42+
43+
Clear only sparring reports::
44+
45+
logcleaner sparring
46+
47+
Clear combat and hunting, but not sparring::
48+
49+
logcleaner combat,hunting
50+
51+
Overlay UI
52+
----------
53+
54+
Run ``gui/logcleaner`` to open the settings overlay, or access it from the
55+
control panel under the Gameplay tab.
56+
57+
The overlay provides:
58+
59+
- **Enable toggle**: Turn the plugin on or off (``Shift+E``)
60+
- **Combat toggle**: Clear combat reports (``Shift+C``)
61+
- **Sparring toggle**: Clear sparring reports (``Shift+S``)
62+
- **Hunting toggle**: Clear hunting reports (``Shift+H``)

plugins/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ if(BUILD_SUPPORTED)
6666
dfhack_plugin(createitem createitem.cpp)
6767
dfhack_plugin(cursecheck cursecheck.cpp)
6868
dfhack_plugin(cxxrandom cxxrandom.cpp LINK_LIBRARIES lua)
69+
dfhack_plugin(logcleaner logcleaner/logcleaner.cpp LINK_LIBRARIES lua)
6970
dfhack_plugin(deramp deramp.cpp)
7071
dfhack_plugin(debug debug.cpp LINK_LIBRARIES jsoncpp_static)
7172
dfhack_plugin(dig dig.cpp LINK_LIBRARIES lua)

plugins/logcleaner/logcleaner.cpp

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
#include "LuaTools.h"
2+
#include "PluginManager.h"
3+
#include "PluginLua.h"
4+
5+
#include "modules/Persistence.h"
6+
#include "modules/World.h"
7+
8+
#include <df/report.h>
9+
#include <df/unit.h>
10+
#include <df/world.h>
11+
#include <unordered_set>
12+
13+
using namespace DFHack;
14+
15+
DFHACK_PLUGIN("logcleaner");
16+
DFHACK_PLUGIN_IS_ENABLED(is_enabled);
17+
18+
REQUIRE_GLOBAL(world);
19+
20+
static const std::string CONFIG_KEY = std::string(plugin_name) + "/config";
21+
static PersistentDataItem config;
22+
23+
enum ConfigValues {
24+
CONFIG_IS_ENABLED = 0,
25+
CONFIG_CLEAR_COMBAT = 1,
26+
CONFIG_CLEAR_SPARING = 2,
27+
CONFIG_CLEAR_HUNTING = 3,
28+
};
29+
30+
static bool clear_combat = false;
31+
static bool clear_sparring = true;
32+
static bool clear_hunting = false;
33+
34+
static constexpr int32_t CLEANUP_TICK_INTERVAL = 97;
35+
36+
static void cleanupLogs();
37+
static command_result do_command(color_ostream& out, std::vector<std::string>& params);
38+
39+
// Getter functions for Lua
40+
static bool logcleaner_getCombat() { return clear_combat; }
41+
static bool logcleaner_getSparring() { return clear_sparring; }
42+
static bool logcleaner_getHunting() { return clear_hunting; }
43+
44+
// Setter functions for Lua (also persist to config)
45+
static void logcleaner_setCombat(bool val) {
46+
clear_combat = val;
47+
config.set_bool(CONFIG_CLEAR_COMBAT, clear_combat);
48+
}
49+
50+
static void logcleaner_setSparring(bool val) {
51+
clear_sparring = val;
52+
config.set_bool(CONFIG_CLEAR_SPARING, clear_sparring);
53+
}
54+
55+
static void logcleaner_setHunting(bool val) {
56+
clear_hunting = val;
57+
config.set_bool(CONFIG_CLEAR_HUNTING, clear_hunting);
58+
}
59+
60+
DFhackCExport command_result plugin_init(color_ostream& out, std::vector<PluginCommand>& commands) {
61+
commands.push_back(PluginCommand(
62+
plugin_name,
63+
"Prevent report buffer from filling up by clearing selected report types (combat, sparring, hunting).",
64+
do_command));
65+
66+
return CR_OK;
67+
}
68+
69+
static command_result do_command(color_ostream& out, std::vector<std::string>& params) {
70+
if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) {
71+
out.printerr("Cannot use {} without a loaded fort.\n", plugin_name);
72+
return CR_FAILURE;
73+
}
74+
75+
bool show_help = false;
76+
if (!Lua::CallLuaModuleFunction(out, "plugins.logcleaner", "parse_commandline", params,
77+
1, [&](lua_State *L) {
78+
show_help = !lua_toboolean(L, -1);
79+
})) {
80+
return CR_FAILURE;
81+
}
82+
83+
return show_help ? CR_WRONG_USAGE : CR_OK;
84+
}
85+
86+
DFhackCExport command_result plugin_enable(color_ostream& out, bool enable) {
87+
if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) {
88+
out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name);
89+
return CR_FAILURE;
90+
}
91+
92+
if (enable != is_enabled) {
93+
is_enabled = enable;
94+
config.set_bool(CONFIG_IS_ENABLED, is_enabled);
95+
}
96+
return CR_OK;
97+
}
98+
99+
DFhackCExport command_result plugin_shutdown(color_ostream& out) {
100+
return CR_OK;
101+
}
102+
103+
DFhackCExport command_result plugin_load_site_data(color_ostream& out) {
104+
config = World::GetPersistentSiteData(CONFIG_KEY);
105+
106+
if (!config.isValid()) {
107+
config = World::AddPersistentSiteData(CONFIG_KEY);
108+
config.set_bool(CONFIG_IS_ENABLED, is_enabled);
109+
config.set_bool(CONFIG_CLEAR_COMBAT, clear_combat);
110+
config.set_bool(CONFIG_CLEAR_SPARING, clear_sparring);
111+
config.set_bool(CONFIG_CLEAR_HUNTING, clear_hunting);
112+
}
113+
114+
is_enabled = config.get_bool(CONFIG_IS_ENABLED);
115+
clear_combat = config.get_bool(CONFIG_CLEAR_COMBAT);
116+
clear_sparring = config.get_bool(CONFIG_CLEAR_SPARING);
117+
clear_hunting = config.get_bool(CONFIG_CLEAR_HUNTING);
118+
119+
return CR_OK;
120+
}
121+
122+
DFhackCExport command_result plugin_onstatechange(color_ostream& out, state_change_event event) {
123+
if (event == DFHack::SC_WORLD_UNLOADED && is_enabled) {
124+
is_enabled = false;
125+
}
126+
return CR_OK;
127+
}
128+
129+
static void cleanupLogs() {
130+
if (!is_enabled || !world)
131+
return;
132+
133+
if (!clear_combat && !clear_sparring && !clear_hunting)
134+
return;
135+
136+
// Collect all report IDs from unit combat/sparring/hunting logs
137+
std::unordered_set<int32_t> report_ids_to_remove;
138+
bool log_types[] = {clear_combat, clear_sparring, clear_hunting};
139+
140+
for (auto unit : world->units.all) {
141+
for (int log_idx = 0; log_idx < 3; log_idx++) {
142+
if (log_types[log_idx]) {
143+
auto& log = unit->reports.log[log_idx];
144+
for (auto report_id : log) {
145+
report_ids_to_remove.insert(report_id);
146+
}
147+
log.clear();
148+
}
149+
}
150+
}
151+
152+
if (report_ids_to_remove.empty())
153+
return;
154+
155+
// Remove collected reports from global buffers
156+
auto& reports = world->status.reports;
157+
158+
std::erase_if(reports, [&](df::report* report) {
159+
if (!report || !report_ids_to_remove.contains(report->id))
160+
return false;
161+
delete report;
162+
return true;
163+
});
164+
}
165+
166+
DFhackCExport command_result plugin_onupdate(color_ostream& out, state_change_event event) {
167+
static int32_t tick_counter = 0;
168+
169+
if (!is_enabled || !world)
170+
return CR_OK;
171+
172+
tick_counter++;
173+
if (tick_counter >= CLEANUP_TICK_INTERVAL) {
174+
tick_counter = 0;
175+
cleanupLogs();
176+
}
177+
178+
return CR_OK;
179+
}
180+
181+
DFHACK_PLUGIN_LUA_FUNCTIONS {
182+
DFHACK_LUA_FUNCTION(logcleaner_getCombat),
183+
DFHACK_LUA_FUNCTION(logcleaner_getSparring),
184+
DFHACK_LUA_FUNCTION(logcleaner_getHunting),
185+
DFHACK_LUA_FUNCTION(logcleaner_setCombat),
186+
DFHACK_LUA_FUNCTION(logcleaner_setSparring),
187+
DFHACK_LUA_FUNCTION(logcleaner_setHunting),
188+
DFHACK_LUA_END
189+
};

plugins/lua/logcleaner.lua

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
local _ENV = mkmodule('plugins.logcleaner')
2+
3+
local function print_status()
4+
print(('logcleaner is %s'):format(isEnabled() and "enabled" or "disabled"))
5+
print(' Combat: ' .. (logcleaner_getCombat() and 'enabled' or 'disabled'))
6+
print(' Sparring: ' .. (logcleaner_getSparring() and 'enabled' or 'disabled'))
7+
print(' Hunting: ' .. (logcleaner_getHunting() and 'enabled' or 'disabled'))
8+
end
9+
10+
function parse_commandline(...)
11+
local args = {...}
12+
local command = args[1]
13+
14+
-- Show status if no command or "status"
15+
if not command or command == 'status' then
16+
print_status()
17+
return true
18+
end
19+
20+
-- Handle enable/disable commands
21+
if command == 'enable' then
22+
if isEnabled() then
23+
print('logcleaner is already enabled')
24+
else
25+
dfhack.run_command('enable', 'logcleaner')
26+
print('logcleaner enabled')
27+
end
28+
return true
29+
end
30+
31+
if command == 'disable' then
32+
if not isEnabled() then
33+
print('logcleaner is already disabled')
34+
else
35+
dfhack.run_command('disable', 'logcleaner')
36+
print('logcleaner disabled')
37+
end
38+
return true
39+
end
40+
41+
-- Start with all disabled, enable only what's specified
42+
local new_combat, new_sparring, new_hunting = false, false, false
43+
local has_filter = false
44+
45+
if command == 'all' then
46+
new_combat, new_sparring, new_hunting = true, true, true
47+
has_filter = true
48+
elseif command == 'none' then
49+
new_combat, new_sparring, new_hunting = false, false, false
50+
else
51+
-- Split by comma for multiple options in one parameter
52+
for token in command:gmatch('([^,]+)') do
53+
if token == 'combat' then
54+
new_combat = true
55+
has_filter = true
56+
elseif token == 'sparring' then
57+
new_sparring = true
58+
has_filter = true
59+
elseif token == 'hunting' then
60+
new_hunting = true
61+
has_filter = true
62+
else
63+
dfhack.printerr('Unknown option: ' .. token)
64+
return false
65+
end
66+
end
67+
end
68+
69+
-- Auto-enable plugin when filters are being configured
70+
if has_filter and not isEnabled() then
71+
dfhack.run_command('enable', 'logcleaner')
72+
print('logcleaner enabled')
73+
end
74+
75+
logcleaner_setCombat(new_combat)
76+
logcleaner_setSparring(new_sparring)
77+
logcleaner_setHunting(new_hunting)
78+
79+
print('Log cleaning config updated:')
80+
print_status()
81+
82+
return true
83+
end
84+
85+
return _ENV

0 commit comments

Comments
 (0)