# NjConsole — condensed reference for AI coding assistants > For coding agents only. Humans start at [ninjadini.github.io/njconsole](https://ninjadini.github.io/njconsole/). In-game + in-editor debug console for Unity 2022.3+ (UI Toolkit). Runtime log viewer with filtering, a cheats/options menu, a command line, an object inspector and a live hierarchy browser. **Nothing to set up.** NjConsole starts itself — no prefab, no GameObject, no init call. Settings live at `Project Settings > Ninjadini ⌨ Console`. Open the editor window from `Window > ⌨ Ninjadini Console (new window)`; in play mode press `` ` `` , or hold/double-tap the top-left screen corner. Two namespaces, two assemblies: | `using` | Contains | Assembly | |---|---|---| | `Ninjadini.Logger` | `NjLogger`, `LogChannel`, `ColoredLogChannel`, `StrValue`, `AsLogRef()` | `Ninjadini.Logger` (+ `ColoredLogChannel`, which sits in `Ninjadini.Console`) | | `Ninjadini.Console` | `NjConsole`, `ConsoleModules`, `ConsoleToasts`, every `IConsole*` interface | `Ninjadini.Console` | `Ninjadini.Logger.Internal` holds `LogsHistory` — only needed if you name the type. ## Logging `NjLogger` is a drop-in alternative to `Debug.Log` that formats arguments with **zero allocations** and captures no stack trace unless configured to. `Debug.Log()` still shows up in the console either way, so replacing existing calls is optional — reach for `NjLogger` where allocations matter. ```csharp using Ninjadini.Logger; NjLogger.Debug("Debug level - auto excluded in release builds"); NjLogger.Info("Info level"); NjLogger.Warn("Warning level"); NjLogger.Error("Error level - raises an alert in the console"); NjLogger.Exception(exception, "optional message"); NjLogger.Log("alias of Info(), reads like Debug.Log()"); // Concatenate by passing values, NOT by interpolating - interpolation allocates. NjLogger.Info("hp:", health, " speed:", speed, " alive:", isAlive); ``` **Value slots.** Each call takes up to **6** values (`v0`..`v5`); a `LogChannel` call takes **5** (the channel name uses one), and `ColoredLogChannel.Debug()`/`Info()` take **4**. `Exception()` takes the exception plus **2**. Every method also ends with `Options options = 0, object context = null`. These types go in without allocating: `string`, `bool`, `int`, `uint`, `long`, `ulong`, `float`, `double`, `DateTime`, `TimeSpan`, `Color`, `Color32`, `Exception`, `UnityEngine.Object`, and anything wrapped with `AsLogRef()` / `AsStrongLogRef()`. Everything else needs one of: ```csharp NjLogger.Info("clickable link to ", playerObj.AsLogRef()); // weak ref, inspectable, no alloc NjLogger.Info("held until the log scrolls out ", obj.AsStrongLogRef()); NjLogger.Info("plain text, no link ", obj.AsString()); // calls ToString() - DOES allocate ``` `AsLogRef()` / `AsStrongLogRef()` are constrained `where T : class` — they will not compile on a struct. A `UnityEngine.Object` passed bare already becomes a link. ```csharp NjLogger.Info(Color.cyan, "a Color in the FIRST slot tints the whole line"); // Replaces the newest line in place with a `123x` counter, instead of adding rows. // Only while that line is still newest - once another log lands, the next one starts fresh. NjLogger.Info("Downloading... ", percent, "%", options: NjLogger.Options.Repeating); ``` Other `NjLogger.Options` flags: `ForceStackTrace`, `ForceNoStackTrace`. ### Channels Group logs so they can be filtered by name. Both channel types are **readonly structs** — keep them `static readonly`. ```csharp static readonly LogChannel Net = new LogChannel("net"); Net.Info("connected"); // Tints its own debug/info lines, so you don't pass a Color every call. static readonly ColoredLogChannel Hints = new ColoredLogChannel("hints", debug: new Color(0.6f, 0.6f, 0.6f), info: new Color(0.72f, 0.92f, 0.80f)); ``` Warn/error/exception are never tinted (the console already colors those rows) and keep all 5 slots. ### Reading the log history `NjLogger.LogsHistory` is a ring buffer of everything captured. Nothing in `Ninjadini.Logger` is stripped by `NJCONSOLE_DISABLE`, which is what makes this usable for crash reports in production. ```csharp var sb = new StringBuilder(); NjLogger.LogsHistory.GenerateHistoryNewestToOldest(sb, maxLogs: 500); NjLogger.LogsHistory.ForEachLogNewestToOldest(log => { var text = log.GetLineString(); var level = log.Level; // NjLogger.Level: Debug | Info | Warn | Error var time = log.Time; var channel = log.GetChannelName(); }, maxLogs: 200); ``` Also `GetLevelCount(level)` (cheap running count, good for an error badge) and `SetMaxHistoryCount(n)`. `LoggerUtils.BorrowStringBuilder()` / `ReturnStringBuilder(sb)` for a scratch builder, and `LoggerUtils.AppendNum()` / `AppendNumWithZeroPadding()` / `AppendDateTime()` for alloc-free formatting. ### Routing (settings, not code) Three independent routes, all under `Project Settings > … > Logging > Logging Paths`, each with a runtime override: `Debug.Log()` → NjConsole (`NjConsole.Settings.UnityDebugLogsToNjLogger`, default on), `Debug.Log()` → Unity (`UnityDebugLogsToUnity`, default on), and NjLogger → Unity (`NjLoggerToUnityMinLevel`, default off — turning it on gives up most of NjLogger's speed). Stack traces are the expensive part, so min level is configured separately for dev build, release build and editor. Runtime override: `NjLogger.Settings.MinStackTraceLevel = NjLogger.Level.Warn;` (`null` disables). To forward NjLogger's output into your own logger, implement `NjLogger.IHandler` and register it with `NjLogger.Settings.AddHandler(...)`. `LogRow` is a `ref struct` valid only inside the callback — copy out what you need. To send *into* NjLogger from your own logger, call `NjLogger.Add(...)`. ## Options menu (cheats / debug tools) Two ways to register. Both build the same items, and every item is also callable from the command line. ### Attribute ```csharp void Start() { NjConsole.Options.CreateCatalogFrom(this, "MyTools"); // 2nd arg = folder, optional // If `this` is a MonoBehaviour the items auto-remove on OnDestroy(). } ``` For static members pass the type instead: `NjConsole.Options.CreateCatalogFrom(typeof(MyCheats));` ```csharp [ConsoleOption] void SayHello() => NjLogger.Info("hi"); // button [ConsoleOption("Folder/Sub/Named Button")] void B2() { } // `/` makes folders [ConsoleOption("Item", header: "My Header")] void B3() { } [ConsoleOption(key: KeyCode.W, keyModifier: ConsoleKeyBindings.Modifier.Shift)] void WinLevel() { } [ConsoleOption(autoClose: true)] void CloseAfterPress() { } [ConsoleOption] bool InfiniteLives; // toggle (field or property) [ConsoleOption] int Health; // number [ConsoleOption(increments: 0.5f)] float Speed; // with -/+ step buttons [ConsoleOption] [Range(1, 5)] int Strength; // clamped [ConsoleOption] [Multiline] string Notes; // text field [ConsoleOption] DeviceOrientation Orientation; // enum dropdown [ConsoleOption] void Introduce(string name, int age) { } // command-line only (see below) ``` Attribute signature: `ConsoleOptionAttribute(string path = null, string header = null, double increments = 0, KeyCode/Key key = 0, ConsoleKeyBindings.Modifier keyModifier = 0, bool autoClose = false)`. `key` is `UnityEngine.KeyCode` on the legacy Input Manager and `UnityEngine.InputSystem.Key` on the new Input System. Key bindings and `autoClose` work on **buttons and toggles only**, and key bindings are editor-only unless `Features > In Player Key Bindings` is enabled. Members the menu cannot render (multiple parameters, unsupported types) are hidden from the menu but stay callable from the command line. ### Programmatic Full control, no reflection, and the only way to build items dynamically. ```csharp var catalog = NjConsole.Options.CreateCatalog(); catalog.AddButton("A Folder / Reload Scene", () => ReloadScene()) .SetHeader("Scene").SetTooltip("Reloads the active scene") .BindToKeyboard(KeyCode.R, ConsoleKeyBindings.Modifier.Shift) .AutoCloseOverlay(); catalog.AddToggle("God Mode", v => godMode = v, () => godMode); catalog.AddNumberPrompt("Coins", v => coins = v, () => coins, btnDeltaSteps: 100); catalog.AddTextPrompt("Player Name", v => name = v, () => name); catalog.AddTextPromptWithValidation("Server", getter: () => url, setter: Accept, validator: Trim); catalog.AddChoice("Difficulty", choices, v => index = v, () => index); catalog.AddEnumChoice("Platform", v => platform = v, () => platform); ``` > **The setter comes before the getter** on every one of these. Getting that backwards compiles in some > cases and silently misbehaves. `AddNumberPrompt` has overloads for `float`, `double`, `int`, `uint`, `long`, `ulong`. ### Removing options A catalog is the unit of cleanup — hold onto it: ```csharp catalog.RemoveAll(); // everything this catalog added catalog.Remove("A Folder / My Button"); // one item, if this catalog added it ``` Both only touch items *this* catalog added; `RemoveIncludingConflicts(path)` / `RemoveAllIncludingConflicts()` clear a path regardless of owner. `CreateCatalogFrom(monoBehaviour)` already removes on destroy — pass `autoRemoveOnMonoBehaviourDestroy: false` to manage it yourself. ### Variants and key bindings - `NjConsole.Options` — play mode. - `NjConsole.EditModeOptions` — works **outside** play mode, shown in the console's `Editor Options` panel. Register from editor-only code (`Editor` assembly or `[InitializeOnLoad]`). Same catalog API. - `NjConsole.CommandLineOptions` — command-line-only, hidden from the menu. Same catalog API. - Bind a key with no menu item: `NjConsole.KeyBindings.BindKeyDown(action, KeyCode.C, modifier)` / `UnbindKeyDown(...)`. `ConsoleKeyBindings.IsUserTyping()` is true while a text field has focus — check it before acting on raw key input. ## Command line Every options item doubles as a command; the path becomes the command name. Open it by pressing any key while the Logs panel has focus, or `` Shift+` `` in the runtime overlay. ``` sayhello // [ConsoleOption] void SayHello() profile/name // read a [ConsoleOption] field profile/name "My Name" // write it demo/introduce "Ninjadini", 30 // params separated by space or comma, quotes for strings math/vector multiply (1 2 3) 1 // parentheses group constructor args, and nest ``` Built-ins live under `/` and always win: `/help`, `/clear logs`, `/store `, `/retrieve `, `/list stored`, `/clear stored`, `/scope [name]`, `/rescope`, `/call [args]`, `/inspect`, `/destroy`, `/find type`, `/filter cmds`, `/filter reset`, `/close`. Command names are **case-insensitive** and **overloads are not supported** — each command must be unique, so give colliding methods distinct paths. Member names passed to `/call` *are* case-sensitive. Returned objects land in `$_`; returning a class object also sets the scope `$@` (previous: `$@prev`). `/store name` keeps one as `$name` for later commands. Read them from code via: ```csharp var storage = NjConsole.Modules.GetOrCreateModule(); var last = storage.GetLastResult(); // same as GetStored("_") var scope = storage.GetScope(); // same as GetStored("@") ``` Stored variables are **strong** references and are not collected until `/clear stored`. Add `[Tooltip("...")]` to any option — it shows in the menu, in autocomplete and in `/help`. Worth doing for commands with constructor-argument syntax, since the generated hint (` `) says nothing about the parentheses. ## Driving the console from code ```csharp NjConsole.Overlay.EnsureStarted(); // start it hidden, waiting for triggers NjConsole.Overlay.ShowWithAccessChallenge(); // show, running the configured passcode gate first NjConsole.Overlay.Show(); // show, skipping the gate NjConsole.Overlay.Hide(); NjConsole.Overlay.Toggle(); NjConsole.Overlay.Showing; // bool NjConsole.Overlay.SetActivePanel("Utilities"); NjConsole.Overlay.Destroy(); ConsoleToasts.TryShow("Save wiped"); // transient message ConsoleToasts.TryShow("Save wiped", () => Reload(), "Reload"); // ...with a button // A page under the Utilities panel. Use += : runs once per console window. NjConsole.Settings.CustomUtilitiesMenus += (context, addMenu) => addMenu("My Tools", () => new Label("Hello")); ``` Prefer `ShowWithAccessChallenge()` over `Show()` so a configured passcode still applies. ## Stripping for production — `NJCONSOLE_DISABLE` Set from `Project Settings > Ninjadini ⌨ Console > Disable NjConsole`, or the define directly, or `ConsoleEditorSettings.AddDefineSymbolToDisableConsole()` from a build processor. Most of the console compiles out; **log history is still collected**, so crash reporting keeps working. This matters when writing code, because only some entry points keep a do-nothing stub: | Safe to call unguarded | Needs `#if !NJCONSOLE_DISABLE` | |---|---| | `NjConsole.*` — Options, KeyBindings, Overlay, Settings, Modules | `ConsoleTextPrompt`, `ConsoleInspector` | | Everything in `Ninjadini.Logger`, incl. `ColoredLogChannel` | `ConsoleGraphingElement`, `FpsMonitorElement`, `MemoryMonitorElement` | | `ConsoleToasts`, `ConsoleModules`, `ConsoleSettings` | `ConsoleWindow`, `ConsoleLogsPanel`, `ConsoleScreenShortcuts` | | `ConsoleContext.SetData()` / `GetData()` / `Storage` | `ConsoleContext.TryGetFocusedContext()` / `RuntimeOverlay` | | `ConsoleKeyBindings`, every `IConsole*` interface you implement | `StringParser`, `ConsoleUIUtils`, `ConsoleUtilitiesElement` | Rule of thumb: reached through the `NjConsole` static, it's stubbed; named as a UI class directly, it isn't. The guard must wrap the **whole declaration**, not just a call — naming a stripped type as a base interface or a generic argument breaks the build the same way. Wrap your own cheat code in the same define so it strips too. Per-feature switches that don't need stripping are fields on `ConsoleSettings` (`inPlayerLogsPanel`, `inPlayerOptionsPanel`, `inPlayerHierarchyPanel`, `inPlayerUtilitiesPanel`, `inPlayerObjectInspector`, `inPlayerCommandLine`, `inPlayerKeyBindings`, `autoStartOverlay`) — all always on in the editor. ## Extending the console Everything pluggable is an `IConsoleModule`. Implementing `IConsoleExtension` as well lets NjConsole construct it for you from `Project Settings > … > Extension Modules` instead of you registering it in code — for that the class must be `[Serializable]` and a plain class (not a `MonoBehaviour` or `ScriptableObject`); its public serializable fields then become editable settings. Press `Apply extension changes` after adding one. | Interface | Adds | |---|---| | `IConsolePanelModule` | A whole panel (UI Toolkit) — `Name`, `SideBarOrder`, `CreateElement(ConsoleContext)` | | `IConsoleIMGUIPanelModule` | A panel drawn with OnGUI/IMGUI | | `IConsoleCommandlineModule` | A custom command executor, or an interactive prompt that locks the input | | `IConsoleOverlayTrigger` | Your own way to open the runtime overlay | | `IConsoleAccessChallenge` | A gate before the console opens (e.g. your own login) | | `IConsoleTimestampFormatter` | Timestamp rendering in the Logs panel | | `IConsoleLogExportFormatter` | Header / footer / line format for exported and emailed logs | | `IConsoleExtension` alone | Anything else — run setup code when the console starts | Registering in code instead: ```csharp if (NjConsole.Modules.GetModule(typeof(MyModule)) == null) NjConsole.Modules.AddModule(new MyModule()); ``` Also on `ConsoleModules`: `GetOrCreateModule()`, `GetModule(includeSubClasses)`, `RemoveModule(type)`, `HasModule(module)`, and the `ModuleAdd` / `ModuleRemoved` events (unsubscribe — `ConsoleModules` outlives play sessions). Two `IConsoleModule` members worth knowing: - `PersistInEditMode` — defaults to `false`. Return `true` for anything that must survive leaving play mode, which includes any panel meant to work in the editor window outside play mode. - `OnAdded(ConsoleModules)` / `Dispose()` — setup and teardown. **Implement `OnAdded` explicitly (`void IConsoleModule.OnAdded(...)`) or make it `public`** — a private `void OnAdded(...)` is silently never called. There is one module instance per type, but a `ConsoleContext` per window (several editor windows and the runtime overlay can be open at once), so panel *state* belongs in the element or in `context.SetData()` / `GetData()`, never in the module. ## Fuller documentation Per-topic pages, with screenshots and longer examples, at [ninjadini.github.io/njconsole](https://ninjadini.github.io/njconsole/): Console Window, Logging, Options Menu, Command Line, Inspector & Utilities, Custom Panels, Build Customization, Extension Modules, Hidden Gems. The `Demo/` folder in this package has a runnable scene covering most of the above.