Skip to content

Commit c050add

Browse files
committed
Create plugin for clearing of combat, sparring, and hunting reports with configurable filtering and overlay UI.
1 parent 652b792 commit c050add

5 files changed

Lines changed: 369 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

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: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
#include "Debug.h"
2+
#include "LuaTools.h"
3+
#include "PluginManager.h"
4+
#include "PluginLua.h"
5+
6+
#include "modules/Persistence.h"
7+
#include "modules/World.h"
8+
9+
#include <df/report.h>
10+
#include <df/unit.h>
11+
#include <df/world.h>
12+
#include <unordered_set>
13+
14+
using namespace DFHack;
15+
16+
DFHACK_PLUGIN("logcleaner");
17+
DFHACK_PLUGIN_IS_ENABLED(is_enabled);
18+
19+
REQUIRE_GLOBAL(world);
20+
21+
static const std::string CONFIG_KEY = std::string(plugin_name) + "/config";
22+
static PersistentDataItem config;
23+
24+
enum ConfigValues {
25+
CONFIG_IS_ENABLED = 0,
26+
CONFIG_CLEAR_COMBAT = 1,
27+
CONFIG_CLEAR_SPARING = 2,
28+
CONFIG_CLEAR_HUNTING = 3,
29+
};
30+
31+
static bool clear_combat = false;
32+
static bool clear_sparring = true;
33+
static bool clear_hunting = false;
34+
35+
namespace DFHack {
36+
DBG_DECLARE(logcleaner, control, DebugCategory::LINFO);
37+
DBG_DECLARE(logcleaner, cleanup, DebugCategory::LINFO);
38+
}
39+
40+
static void cleanupLogs(color_ostream& out);
41+
static command_result do_command(color_ostream& out, std::vector<std::string>& params);
42+
static void do_enable();
43+
static void do_disable();
44+
45+
// Getter functions for Lua
46+
static bool logcleaner_getCombat() { return clear_combat; }
47+
static bool logcleaner_getSparring() { return clear_sparring; }
48+
static bool logcleaner_getHunting() { return clear_hunting; }
49+
50+
// Setter functions for Lua (also persist to config)
51+
static void logcleaner_setCombat(color_ostream& out, bool val) {
52+
clear_combat = val;
53+
config.set_bool(CONFIG_CLEAR_COMBAT, clear_combat);
54+
}
55+
56+
static void logcleaner_setSparring(color_ostream& out, bool val) {
57+
clear_sparring = val;
58+
config.set_bool(CONFIG_CLEAR_SPARING, clear_sparring);
59+
}
60+
61+
static void logcleaner_setHunting(color_ostream& out, bool val) {
62+
clear_hunting = val;
63+
config.set_bool(CONFIG_CLEAR_HUNTING, clear_hunting);
64+
}
65+
66+
DFhackCExport command_result plugin_init(color_ostream& out, std::vector<PluginCommand>& commands) {
67+
commands.push_back(PluginCommand(
68+
plugin_name,
69+
"Prevent report buffer from filling up by clearing selected report types (combat, sparring, hunting).",
70+
do_command));
71+
72+
return CR_OK;
73+
}
74+
75+
static command_result do_command(color_ostream& out, std::vector<std::string>& params) {
76+
if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) {
77+
out.printerr("Cannot use {} without a loaded fort.\n", plugin_name);
78+
return CR_FAILURE;
79+
}
80+
81+
bool show_help = false;
82+
if (!Lua::CallLuaModuleFunction(out, "plugins.logcleaner", "parse_commandline", params,
83+
1, [&](lua_State *L) {
84+
show_help = !lua_toboolean(L, -1);
85+
})) {
86+
return CR_FAILURE;
87+
}
88+
89+
return show_help ? CR_WRONG_USAGE : CR_OK;
90+
}
91+
92+
static void do_enable() {
93+
}
94+
95+
static void do_disable() {
96+
}
97+
98+
DFhackCExport command_result plugin_enable(color_ostream& out, bool enable) {
99+
if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) {
100+
out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name);
101+
return CR_FAILURE;
102+
}
103+
104+
if (enable != is_enabled) {
105+
is_enabled = enable;
106+
DEBUG(control, out).print("{} from the API; persisting\n",
107+
is_enabled ? "enabled" : "disabled");
108+
config.set_bool(CONFIG_IS_ENABLED, is_enabled);
109+
if (enable)
110+
do_enable();
111+
else
112+
do_disable();
113+
} else {
114+
DEBUG(control, out).print("{} from the API, but already {}; no action\n",
115+
is_enabled ? "enabled" : "disabled",
116+
is_enabled ? "enabled" : "disabled");
117+
}
118+
return CR_OK;
119+
}
120+
121+
DFhackCExport command_result plugin_shutdown(color_ostream& out) {
122+
DEBUG(control, out).print("shutting down {}\n", plugin_name);
123+
return CR_OK;
124+
}
125+
126+
DFhackCExport command_result plugin_load_site_data(color_ostream& out) {
127+
config = World::GetPersistentSiteData(CONFIG_KEY);
128+
129+
if (!config.isValid()) {
130+
DEBUG(control, out).print("no config found in this save; initializing\n");
131+
config = World::AddPersistentSiteData(CONFIG_KEY);
132+
config.set_bool(CONFIG_IS_ENABLED, is_enabled);
133+
config.set_bool(CONFIG_CLEAR_COMBAT, clear_combat);
134+
config.set_bool(CONFIG_CLEAR_SPARING, clear_sparring);
135+
config.set_bool(CONFIG_CLEAR_HUNTING, clear_hunting);
136+
}
137+
138+
is_enabled = config.get_bool(CONFIG_IS_ENABLED);
139+
clear_combat = config.get_bool(CONFIG_CLEAR_COMBAT);
140+
clear_sparring = config.get_bool(CONFIG_CLEAR_SPARING);
141+
clear_hunting = config.get_bool(CONFIG_CLEAR_HUNTING);
142+
143+
DEBUG(control, out).print("loading persisted enabled state: {}\n",
144+
is_enabled ? "true" : "false");
145+
if (is_enabled)
146+
do_enable();
147+
148+
return CR_OK;
149+
}
150+
151+
DFhackCExport command_result plugin_onstatechange(color_ostream& out, state_change_event event) {
152+
if (event == DFHack::SC_WORLD_UNLOADED && is_enabled) {
153+
DEBUG(control, out).print("world unloaded; disabling {}\n", plugin_name);
154+
is_enabled = false;
155+
do_disable();
156+
}
157+
return CR_OK;
158+
}
159+
160+
static void cleanupLogs(color_ostream& out) {
161+
if (!is_enabled || !world)
162+
return;
163+
164+
// Collect all report IDs from unit combat/sparring/hunting logs
165+
std::unordered_set<int32_t> report_ids_to_remove;
166+
167+
for (auto unit : world->units.all) {
168+
// Combat logs (index 0)
169+
if (clear_combat) {
170+
auto& log = unit->reports.log[0];
171+
for (auto report_id : log) {
172+
report_ids_to_remove.insert(report_id);
173+
}
174+
log.clear();
175+
}
176+
// Sparring logs (index 1)
177+
if (clear_sparring) {
178+
auto& log = unit->reports.log[1];
179+
for (auto report_id : log) {
180+
report_ids_to_remove.insert(report_id);
181+
}
182+
log.clear();
183+
}
184+
// Hunting logs (index 2)
185+
if (clear_hunting) {
186+
auto& log = unit->reports.log[2];
187+
for (auto report_id : log) {
188+
report_ids_to_remove.insert(report_id);
189+
}
190+
log.clear();
191+
}
192+
}
193+
194+
if (report_ids_to_remove.empty())
195+
return;
196+
197+
// Remove collected reports from global buffers
198+
auto& reports = world->status.reports;
199+
200+
int reports_erased = 0;
201+
202+
for (auto report_id : report_ids_to_remove) {
203+
df::report* report = df::report::find(report_id);
204+
if (!report)
205+
continue;
206+
207+
auto it = std::find(reports.begin(), reports.end(), report);
208+
if (it != reports.end()) {
209+
delete report;
210+
reports.erase(it);
211+
reports_erased++;
212+
}
213+
}
214+
}
215+
216+
DFhackCExport command_result plugin_onupdate(color_ostream& out, state_change_event event) {
217+
static int32_t tick_counter = 0;
218+
219+
if (!is_enabled || !world)
220+
return CR_OK;
221+
222+
tick_counter++;
223+
if (tick_counter >= 100) {
224+
tick_counter = 0;
225+
cleanupLogs(out);
226+
}
227+
228+
return CR_OK;
229+
}
230+
231+
DFHACK_PLUGIN_LUA_FUNCTIONS {
232+
DFHACK_LUA_FUNCTION(logcleaner_getCombat),
233+
DFHACK_LUA_FUNCTION(logcleaner_getSparring),
234+
DFHACK_LUA_FUNCTION(logcleaner_getHunting),
235+
DFHACK_LUA_FUNCTION(logcleaner_setCombat),
236+
DFHACK_LUA_FUNCTION(logcleaner_setSparring),
237+
DFHACK_LUA_FUNCTION(logcleaner_setHunting),
238+
DFHACK_LUA_END
239+
};

plugins/lua/logcleaner.lua

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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+
-- Start with all disabled, enable only what's specified
21+
local new_combat, new_sparring, new_hunting = false, false, false
22+
local has_filter = false
23+
24+
for _, param in ipairs(args) do
25+
if param == 'all' then
26+
new_combat, new_sparring, new_hunting = true, true, true
27+
has_filter = true
28+
elseif param == 'none' then
29+
new_combat, new_sparring, new_hunting = false, false, false
30+
else
31+
-- Split by comma for multiple options in one parameter
32+
for token in param:gmatch('([^,]+)') do
33+
if token == 'combat' then
34+
new_combat = true
35+
has_filter = true
36+
elseif token == 'sparring' then
37+
new_sparring = true
38+
has_filter = true
39+
elseif token == 'hunting' then
40+
new_hunting = true
41+
has_filter = true
42+
else
43+
dfhack.printerr('Unknown option: ' .. token)
44+
return false
45+
end
46+
end
47+
end
48+
end
49+
50+
-- Auto-enable plugin when filters are being configured
51+
if has_filter and not isEnabled() then
52+
dfhack.run_command('enable', 'logcleaner')
53+
print('logcleaner enabled')
54+
end
55+
56+
logcleaner_setCombat(new_combat)
57+
logcleaner_setSparring(new_sparring)
58+
logcleaner_setHunting(new_hunting)
59+
60+
print('Log cleaning config updated:')
61+
print_status()
62+
63+
return true
64+
end
65+
66+
return _ENV

0 commit comments

Comments
 (0)