Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
62 changes: 62 additions & 0 deletions docs/plugins/logcleaner.rst
Original file line number Diff line number Diff line change
@@ -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``)
1 change: 1 addition & 0 deletions plugins/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
191 changes: 191 additions & 0 deletions plugins/logcleaner/logcleaner.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
#include "LuaTools.h"
#include "PluginManager.h"
#include "PluginLua.h"

#include "modules/Persistence.h"
#include "modules/World.h"

#include <df/report.h>
#include <df/unit.h>
#include <df/world.h>
#include <unordered_set>

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;

static const int32_t CLEANUP_TICK_INTERVAL = 97;

Comment thread
pajawojciech marked this conversation as resolved.
static void cleanupLogs();
static command_result do_command(color_ostream& out, std::vector<std::string>& params);

// 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(bool val) {
clear_combat = val;
config.set_bool(CONFIG_CLEAR_COMBAT, clear_combat);
}

static void logcleaner_setSparring(bool val) {
clear_sparring = val;
config.set_bool(CONFIG_CLEAR_SPARING, clear_sparring);
}

static void logcleaner_setHunting(bool val) {
clear_hunting = val;
config.set_bool(CONFIG_CLEAR_HUNTING, clear_hunting);
}

DFhackCExport command_result plugin_init(color_ostream& out, std::vector<PluginCommand>& 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<std::string>& 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;
}

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;
config.set_bool(CONFIG_IS_ENABLED, is_enabled);
}
return CR_OK;
}

DFhackCExport command_result plugin_shutdown(color_ostream& out) {
return CR_OK;
}

DFhackCExport command_result plugin_load_site_data(color_ostream& out) {
config = World::GetPersistentSiteData(CONFIG_KEY);

if (!config.isValid()) {
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);

return CR_OK;
}

DFhackCExport command_result plugin_onstatechange(color_ostream& out, state_change_event event) {
if (event == DFHack::SC_WORLD_UNLOADED && is_enabled) {
is_enabled = false;
}
return CR_OK;
}

static void cleanupLogs() {
if (!is_enabled || !world)
return;

// Collect all report IDs from unit combat/sparring/hunting logs
std::unordered_set<int32_t> report_ids_to_remove;
bool log_types[] = {clear_combat, clear_sparring, clear_hunting};

for (auto unit : world->units.all) {
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();
}
}
}

if (report_ids_to_remove.empty())
return;

// Remove collected reports from global buffers
Comment thread
pajawojciech marked this conversation as resolved.
auto& reports = world->status.reports;

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);
}
}
}

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 >= CLEANUP_TICK_INTERVAL) {
tick_counter = 0;
cleanupLogs();
}

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
};
85 changes: 85 additions & 0 deletions plugins/lua/logcleaner.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
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

-- 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

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

-- 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