Skip to content
ct

Grid Placement v6.0

Logger Configuration

Configure and override the PlacementLogger singleton without modifying plugin source.

Status
Draft
Version
v6.0
Updated
Development docs generated from GDScript source

This is unreleased documentation in active development. APIs, class names, and behavior may change before the final release.

The Grid Placement plugin uses a static singleton logger (PlacementLogger) that can be configured and overridden at any point without modifying plugin source code.

Version note: This guide is validated for Grid Placement 6.0.0.

Quick Start

The logger is initialized automatically during plugin setup (via PlacementSession composition). No action is needed for basic logging output.

# Anywhere in the plugin — zero boilerplate, zero null checks:
PlacementLogger.debug("Terrain painted at cell%s" % str(cell))
PlacementLogger.trace("Validation details:%s" % str(details))
PlacementLogger.warn("Deprecated API called")
PlacementLogger.error("Failed to place:%s" % error)

Two Composition Roots

The plugin maintains two distinct logger channels that are coordinated by PlacementLogger._resolve_instance. They serve different jobs and are not interchangeable, but they compose cleanly so test scopes always win routing precedence over the production root.

Production root — a single-slot weakref (_production_root_ref) set by PlacementLogger.initialize at game boot. Anywhere in the plugin tree, PlacementLogger.debug(...) / PlacementLogger.warn(...) / PlacementLogger.error(...) route through this logger when no test scope is active. Game/integration code only writes through this root — they never construct a logger. Re-invoking initialize from a system-under-test mid-test replaces the production root in place without disturbing the test scope stack above it.

Test scope stack — a list of weakrefs (_scope_stack) manipulated by PlacementLogger.push_scope and PlacementLogger.pop_scope. Tests substitute the production emitter with a muted logger (to keep test output clean) or a spy logger (to read back emitted lines for assertions). For convenience, TestLoggerFixture.install_spy(...) does the push for you and TestLoggerFixture.clear_after_test(spy) does the pop + clear. The scoped frame is pop'd in after_test, so each test starts from a clean scope stack. Resolution order: _resolve_instance walks the scope stack back-to-front and returns the first non-null weakref (test scopes win), falling back to the production root only when the scope stack is empty.

Channel Job When
PlacementLogger.debug(...) / .warn(...) / ... Emit through the active logger (test scope if any, else production root) Game/integration code
PlacementLogger.initialize(ds) Build a logger + set it as the production root (replaces any previous root; does NOT touch test scope stack) Production installer at game boot; system-under-test bootstrap; create_with_injection path
PlacementLogger.push_scope(spy) / pop_scope() Push/pop a scoped override of the production emitter on the scope stack Test setUp/teardown (mute / spy); the canonical test-side channel after PR-E

The previous per-tree container.set_logger / container.get_logger (legacy per-tree seams) were removed in PR-E. The allowlist now lists zero legacy entries. The two-slot design (production root + scope stack) is what allows tests to push a spy first and still have the spy win routing when the SUT later calls PlacementLogger.initialize — because initialize only replaces the production root, not the scope stack.


Controlling Log Verbosity

Adjust the log level on the PlacementDebugSettings resource held by the logger instance. The level can be changed at runtime by calling set_log_level() on the logger you registered via PlacementLogger.set_instance():

Level What you see
ERROR Only PlacementLogger.error()
WARNING Errors + warnings
INFO Errors, warnings, and PlacementLogger.info()
DEBUG All of the above + debug messages (default for development)
VERBOSE Includes setup reports and detailed placement reports
TRACE Everything — including per-frame diagnostics (very noisy)
# Reduce noise at runtime by reconfiguring the registered logger:
var current:= PlacementLogger.initialize(PlacementDebugSettings.new())# canonical factory call
current.set_log_level(PlacementLogger.LogLevel.WARNING)

Throttling and "Once" Logging (instance API)

To prevent console spam during high-frequency operations (like _process or _input), the logger provides instance-based throttling on the logger held in PlacementLogger. These live on the instance, not the static class — register a logger via PlacementLogger.set_instance() if you want to use them, and call them as instance methods.

Throttled Logs

Prints at most once every 250ms per object instance.

# Inside a node's _process()
var logger:= PlacementLogger.initialize(PlacementDebugSettings.new())
logger.log_debug_throttled(self,"Current position:%s" % str(global_position))

Log Once

Prints exactly once per object instance per session.

logger.log_info_once(self,"System successfully initialized.")

If you need these throttled / once-per-instance semantics, use the instance methods. The plain static PlacementLogger.debug() / PlacementLogger.warn() helpers (in the Quick Start section above) are sufficient for the vast majority of plugin call sites and have zero dependency-injection cost.


Overriding the Logger at Game Time

You can replace the plugin's logger entirely at any point. All existing PlacementLogger.debug() calls throughout the plugin automatically pick up the new instance.

Replace with a custom logger

# Create your own logger with custom settings
var my_debug_settings:= PlacementDebugSettings.new()
my_debug_settings.level= PlacementLogger.LogLevel.WARNING
var my_logger:= PlacementLogger.initialize(my_debug_settings)

# Optional: wire up a custom log sink (e.g., to your game's HUD or file)
my_logger.set_log_sink(func(level:int, context:String, message:String)-> void:
    my_game_hud.log("[GB]%s:%s" % [context, message])
)

# Swap it in — all plugin logging now routes through your logger
PlacementLogger.set_instance(my_logger)

Completely disable plugin logging

PlacementLogger.clear_instance()
# All PlacementLogger.debug/trace/warn/error calls become no-ops.

Restore the original logger

# Keep a reference before replacing:
var original_logger:= PlacementLogger.initialize(original_settings)
# ... later ...
PlacementLogger.set_instance(original_logger)

Custom Log Sink

Route all plugin log output to a custom destination (console overlay, file, analytics):

# logger was created in the "Replace with a custom logger" section above.
logger.set_log_sink(func(level:int, context:String, message:String)-> void:
    match level:
        PlacementLogger.LogLevel.ERROR:
            your_error_handler.push(message)
        PlacementLogger.LogLevel.WARNING:
            your_warning_handler.push(message)
        _:
            your_debug_handler.append("[%s]%s" % [context, message])
)

When a log sink is set, the default push_error/push_warning/print output is suppressed.

Direct Godot Logging Policy

Runtime diagnostics should use PlacementLogger by default:

PlacementLogger.warn("Placement target map is missing")
PlacementLogger.error("PlacementActionHistory failed to save:%s" % path)

Direct push_warning, push_error, and print calls are intentionally limited to these categories:

Category Why direct Godot logging is allowed
Logger sinks and logger fallback code These are the implementation behind PlacementLogger; routing back into the logger would recurse.
Deprecated alias constructors These warnings are user-facing migration notices that should appear even before a session logger is initialized.
Abstract base guards and contract stubs These catch incorrect subclass/contract usage at the exact call site.
Editor/resource validation that must be visible before runtime composition These warnings help users repair scenes and resources before the plugin host is fully running.
Temporary diagnostic traces in tests or quarantined debugging work These should be removed or converted before release-quality runtime code depends on them.

When adding new plugin runtime code, prefer PlacementLogger.warn(...) or PlacementLogger.error(...) unless the call clearly fits one of the allowed direct categories above.

Test Isolation

In GdUnit4 test suites, push a scoped override of the production emitter at the start of each test and pop it at the end. The TestLoggerFixture helper centralizes this:

extends GdUnitTestSuite

var _spy_logger:TestLoggerFixture.Spy

func before_test()-> void:
    # Pushes a scoped spy onto the static stack — overrides the production
    # emitter for the duration of one test.
    _spy_logger= TestLoggerFixture.install_spy(LogLevel.WARNING)

func after_test()-> void:
    # Pops the scoped frame, drops any leftover references, and clears the
    # spy. Subsequent tests see the production root again.
    TestLoggerFixture.clear_after_test(_spy_logger)

For mute-only tests (no assertion on emitted content):

func before_test()-> void:
    TestLoggerFixture.install_muted_singleton()
func after_test()-> void:
    TestLoggerFixture.clear_after_test()

After PR-E, PlacementLogger.set_instance(...) is retained only as a backward-compat wrapper that clears the stack and pushes a single frame. Test code should prefer push_scope / pop_scope so multiple scoped overrides within a test (or a framework that wraps the test body) compose cleanly.

API Reference

Static Methods

Method Description
PlacementLogger.initialize(ds) Build a logger with the given debug settings; sets it as the production root. Returns the new logger.
PlacementLogger.set_instance(logger) Register a plugin-wide logger instance (replaces the production root; does NOT touch the test scope stack).
PlacementLogger.push_scope(logger) Push a scoped override onto the scope stack (test/plugin overrides).
PlacementLogger.pop_scope() Pop the most recent push_scope frame. No-op if the stack is empty.
PlacementLogger.clear_instance() Clear BOTH the production root AND the scope stack (full reset, used between tests).
PlacementLogger.debug(msg) Log at DEBUG level
PlacementLogger.trace(msg) Log at TRACE level
PlacementLogger.info(msg) Log at INFO level
PlacementLogger.warn(msg) Log at WARNING level
PlacementLogger.error(msg) Log at ERROR level
PlacementLogger.verbose(msg) Log at VERBOSE level

All static methods silently no-op when no instance is registered (i.e., after clear_instance()).

Level constants

The level enum is re-exported under PlacementLogger.LogLevel for convenience: ERROR, WARNING, INFO, DEBUG, VERBOSE, TRACE.