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 callsPlacementLogger.initialize— becauseinitializeonly 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 preferpush_scope/pop_scopeso 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.