This file provides guidance to AI coding agents (OpenAI Codex and others) when working with this repository. It is the canonical instruction file: CLAUDE.md is a fixed stub that imports this file, and the pre-commit hook rejects edits to the stub. Always edit here.
Skills live canonically in .agents/skills/. .claude/skills/ is a generated mirror: the pre-commit hook regenerates it and rejects commits that edit only the mirror. Never edit .claude/skills/ directly. Fresh clones need git config core.hooksPath .githooks once to enable the hook. The hook also runs .githooks/check-meta-pairing.sh, which rejects commits where a tracked file/dir under Assets/ or an embedded Packages/<pkg>/ lacks its Unity .meta (or a .meta lost its base file); fix by staging the Unity-generated meta or removing the orphan. A same-named CI workflow alarms on violations that bypass the local hook.
AgentDocs are vault artifacts, not repository files. Create .agentdoc sources outside the repository (for example, in a system temporary directory), upload them with vault add <file> --project DeedPlanner-3, and do not create Documentation/, docs/, or another repo-facing documentation directory unless the developer explicitly requests repository documentation.
Use a short, plain, capitalized imperative subject without a Conventional Commit prefix, for example Sync Unity agent skills. Check recent commit history before committing and follow it if the dominant convention changes.
DeedPlanner 3 is a 3D deed/house planning tool for Wurm Online and Wurm Unlimited, built with Unity 6000.3.20f1. It runs as a standalone application (Windows/Linux/Mac) and WebGL version.
Development workflow:
- Open the project in Unity 6000.3.20f1 (see
ProjectSettings/ProjectVersion.txtfor the exact version) - Main scenes:
Assets/Scenes/LoadingScene.unity(startup) →Assets/Scenes/MainScene.unity - Use Unity Editor Play mode to run
- Automated build logic is in
Assets/Warlander/Deedplanner/Editor/BuildSystem.cs - Solution file
DeedPlanner-3.slnfor IDE (VSCode/Rider/Visual Studio)
Never run builds (unity build or any other build path) unless the user explicitly asks for a build in their prompt — releases are handled by the developer.
The Unity CLI (unity, v1.0.0-beta.3+) is the primary way to interact with Unity from the terminal. Use the unity-cli skill for guidance. Always prefer CLI over MCP.
Availability: the CLI is expected on every dev machine but is a separate install. Check it exists (unity --version) before relying on it. If missing, ask the developer to install it and fall back to manual/Editor-side verification — NEVER install it automatically or silently.
Useful commands:
unity status— is an Editor connected? Which project/PID/state?unity list— commands the connected Editor exposes (Pipeline package)unity command <cmd> [args]— execute a command on the connected Editor (domain reload, play mode, scene queries, etc.)unity test --mode EditMode— run tests, NUnit XML to test-results.xmlunity open— open the project with the correct Editor versionunity build— full batch-mode build (see the build restriction above)
Add --json for machine-readable output in agentic loops.
Unity MCP is intentionally disabled. Do not configure or run the official unity mcp adapter, and do not add a community MCP-for-Unity package. The project retains com.unity.pipeline because it powers the Unity CLI commands and the custom agent commands in Platform/AgentCommands.cs.
Opening the project: if unity status shows no connected Editor, use unity open to launch the project — after startup, the CLI commands become usable. For automation workflows, launch with the -automated flag:
unity open "E:/Unity/DeedPlanner-3" --args "-automated"
On SteamOS/Linux, add -noaudio for automated Play-mode sessions (--args "-automated -noaudio"). Unity 6000.3.20f1 otherwise crashes in the FMOD audio thread while entering Play mode; edit-mode compilation and tests are unaffected.
Without it, play mode ENTRY stalls indefinitely while the Editor window is unfocused (in-play behavior is unaffected).
CLI call latency / polling pitfall: unity open stays attached for the Editor's lifetime — always run it as a background task and never wait on its completion. Every CLI invocation on this machine also pays a multi-second telemetry fetch timeout (no external network), so tight until unity status | grep -q ready polling loops effectively hang: each iteration is slow and the Editor typically becomes ready long before the loop notices. Instead, launch, do other work, then check unity status once when needed — treat "hangs on status polling" as "Editor is probably already up, just check it".
Code evaluation: the Pipeline package ships a CodeEval command (edit-mode AND runtime — unity command / unity list can also attach to a running Player via --runtime). This lets the CLI execute arbitrary C# in the live Editor or in the running app — usable both for manipulating the Editor (scenes, assets, play mode) and for testing the app's behavior from the outside. Discover exact command names with unity list.
eval_file quirks (verified): eval rejects bare expressions; use eval_file with an explicit return. eval_file wraps the file in an Execute() method body: statements only (no using directives, no class/method definitions), fully-qualified names (UnityEngine.Object, System.IO.Path — Object collides with object). Write eval scripts to system temp, never under Assets/ (stray .cs files break compilation).
Driving the running app (project commands, Platform/AgentCommands.cs): prefer these over eval for app-level actions. app_await_ready (blocks in-game until playing + MainScene active + map present; pass CLI --timeout above --timeoutSeconds), app_status (instant snapshot: scene/map/tab/camera/save state), map_new/map_load <path>/map_save (quicksave only)/map_info (compact entity counts), tab_select <name>, camera_set <fpp|wurmian|top|iso> [level] (no position control — controllers own it), await_idle [frames] (wait out saves + rendered frames before screenshots), edit_undo/edit_redo. Typical loop: editor_play → app_await_ready --timeout 120 → act → await_idle → screenshot --output <file> → editor_stop. For screenshots use screenshot --output <path> — bare capture_game_view returns huge inline base64.
The project iterates fast — domain reload and play-mode enter/exit each take only a few seconds. After triggering a recompile or play-mode change, allow a ~5 second buffer, then poll unity status for the Editor state before issuing the next command.
Unit tests are EditMode only — even for non-editor code. No PlayMode tests. Test files live in a Tests/Editor/ folder next to the tested code (e.g. Persistence/Compression/Tests/Editor/): the Editor folder compiles them into Assembly-CSharp-Editor, which the Test Framework discovers and runs without any asmdef. Do not add test asmdefs.
Never recompile or run tests while in play mode. Before recompile, run_tests, or any action that triggers an assembly reload (package add/remove/resolve, script creation), verify the Editor is in edit mode (unity status) — if it is playing, editor_stop first. A domain reload mid-play silently breaks the session: the running app loses its state (map references go null, evals fail with NullReferenceException) while the Editor reports itself ready, and an EditMode test run submitted mid-play hard-fails (RestoreSceneSetupTask: "This cannot be used during play mode"). Always editor_stop → recompile/test → editor_play → re-run the scenario.
Pipeline wedge detection and recovery. Signature: every unity command times out while unity status still reports "ready" (status reads the port file, never touches the pipeline). It may self-recover after minutes, but do not wait it out — restart the Editor (kill PID, unity open again). While wedged, cancel_tests/editor_stop cannot reach the Editor either. Note unity command --timeout is in seconds (default 30) — do not pass millisecond values.
Run tests detached, then wait on the job. unity command run_tests --mode EditMode --detach returns a job ID; unity job wait <id> blocks until done and prints the pass/fail summary. Job records do not survive domain reloads — if a reload lands mid-run the job vanishes ("Job Not Found"), so re-run after recompile settles. Blocking run_tests couples the CLI call to a full test+reload cycle and turns any editor-side failure into a CLI hang.
Suggested verification ladder, cheapest first:
- Compile check (connected Editor via
unity command, orunity test --mode EditModebatch) — catches syntax/type errors. - EditMode tests (
unity test --mode EditModebatch, orrun_testson the connected Editor) when the change touches covered logic. - Play mode enter/exit on the connected Editor to catch startup/initialization exceptions (VContainer wiring, scene load). While in play mode, use the Pipeline CodeEval command to execute C# against the running app — assert state, drive interactions, and read back results, giving real behavioral testing without a test assembly.
- Manual/QA pass by the developer for visual or gameplay behavior — agents cannot judge rendering correctness.
For UI interaction bugs, reproduce through the real UI event path whenever practical and inexpensive. Direct model commands are useful for setup, but they do not replace clicking the affected controls and checking the actual visible elements rather than only their backing state.
When adding testable plain-C# logic (presenters, data model, commands), prefer code that could be covered by EditMode tests later — keep it free of UnityEngine.Object dependencies where practical.
The project uses VContainer as its IoC container. Scope classes in Assets/Warlander/Deedplanner/Composition/ (VContainer LifetimeScope subclasses) wire bindings.
The UI follows a strict MVP (Model-View-Presenter) pattern:
- View (MonoBehaviour): thin view only — handles its own internal visual state at most; all logic belongs in the presenter; no container access allowed
- Presenter (plain C# class): mediates between view and model; container use is discouraged — prefer pure DI (constructor injection) except in exceptional situations
- Each presenter handles exactly one view. When multiple instances of a view type can exist simultaneously, each instance gets its own presenter. When such presenters need to share model state, the approach is decided case by case.
Injection style:
- Plain C# classes (presenters, etc.): constructor injection (preferred)
- MonoBehaviours (views): no container injection — the presenter receives the view, not the other way around; views must not reference their presenter
- Map (
Domain/Map*.cs) — central data structure, recently split into:MapTileGrid— 2D grid of tilesMapLevelRenderer— camera-based hiding and fading applied/restored for every camera; active view stores editing context onlyMapBridgesController— bridge logic (lives in theBridges/module)MapDockCollection— owns dock registration, removal, and active state; factories create unattached docksMapRoofCalculator— owned by each map; pending roof work runs fromMap.LateUpdateMapHeightTracker— owned by each map; tracks that map's height bounds and grids
- Tile (
Domain/Tile.cs) — individual grid cell containing ground, walls, floors, roof, decorations, cave data - Database (
Domain/Database.cs) — static dictionaries for all game asset metadata (ground/floor/wall/roof/decoration types) - Materials (
Domain/Materials.cs) — material costs are unit counts, not weights; weight-based goods use template-weight units (Mortar unit = 2 kg, Tar unit = 1 kg), matching the game's build-list convention
Assets/StreamingAssets/objects.xml is the canonical source of truth for authored application data and how that data is interpreted. Treat every data-related concern as part of this schema: entity definitions, categories, relationships, asset associations, material properties, costs, capabilities, overrides, and data-specific loading or rendering behavior. Existing custom objects*.xml files extend the same syntax and loading path.
- Express new data requirements in the existing XML structure, next to the entity or asset they describe; extend the syntax coherently when needed.
- Do not create parallel catalogs, sidecar mappings, hardcoded asset lists, filename-based guesses, or entity-specific rules in loaders, shaders, previews, editor tools, or build scripts.
- Runtime code implements generic interpretation and algorithms. Data-specific choices belong in XML, not in code branches keyed by names, paths, or identifiers.
- All consumers use the same definitions. Generated caches, previews, and validation artifacts must derive from XML and must not become independently maintained sources of truth.
- Update the custom XML template and relevant validation when extending the schema. Preserve the meaning of existing definitions and document any intentional compatibility change.
Each editing mode maps to a UI tab and a corresponding *Updater class in Assets/Warlander/Deedplanner/Editing/<Tab>/ (updater, I*UpdaterView interface, and view MonoBehaviour co-located per tab). Updaters are plain C# classes implementing IUpdater (TargetTab, Initialize, Enable, Disable, Tick), constructor-injected, and registered in the VContainer scope. UpdaterCoordinator (Editing/UpdaterCoordinator.cs) receives them as IReadOnlyList<IUpdater>, initializes all, and on TabContext.TabChanged disables the active updater, enables the one whose TargetTab matches, and ticks only the active one.
CameraCoordinator in Cameras/ manages four modes: Perspective (FPP), Wurmian, Isometric (ISO), and Top. Each camera renders a specific level via independent camera controllers implementing ICameraController.
Custom screen-space selection outline unified under Rendering/Outline/:
ScreenSpaceOutlineFeature—ScriptableRendererFeature; renders outlined objects to a mask RT, dilates, composites border over sceneOutlineCoordinator— plain C# class with one registration per object containing its priority, outline type, and renderers; subscribes once per objectOutlineFeatureBridge—IInitializable+IDisposable, bound NonLazy; discovers and wires the feature on startup via reflectionOutlineEntry— readonly struct grouping renderers and outline type- Auto-setup:
Editor/OutlineFeatureSetup.csuses[InitializeOnLoad]+EditorApplication.update
All map edits are implemented as IReversibleCommand objects managed by CommandManager. Never modify map state directly — always go through commands.
- Modern
async/awaitthroughout (converted from coroutines — async methods use theAsyncsuffix per C# convention) - R3 (Cysharp reactive extensions) used for streaming updates and observable texture loading
TextureLoader,WurmModelLoader,MaterialLoaderload Wurm assets asynchronously
Map serialization uses a custom IXmlSerializable interface. MapHandler orchestrates load/save (backed by MapLoader and MapFactory); StartupMapLoader (plain C# class) handles initial load on startup.
DeedPlannerSettingsdeclares the unified registry and typed module settings (CameraSettings,EditingSettings,UiSettings,GraphicsOptions); project-scopedInputSettingsowns the sharedDPInput;MapRenderSettingsremains session-onlyDPFeatureStateRepository— feature flags for experimental features
Namespaces are vertical system modules, not technical layers. Each module owns its model, presenters, views, and commands; cross-module access goes through interfaces at the module root (e.g. IWaterFacade, IScreenshotFacade, IMapProjectorFacade). Dependency rule: downward only — Composition → everything; Editing/Bridges/Docks/Ui/Screenshots → Domain, Rendering, Persistence, Cameras; Domain → Rendering(assets), Logging; Persistence → Domain; Platform/Settings/Logging → nothing internal.
Warlander.Deedplanner.Domain # Map, Tile, Database, IDataCatalog, Materials, Summary; entities in Domain/Entities/<Type>/
Warlander.Deedplanner.Editing # IUpdater, UpdaterCoordinator, TileSelection, TabContext, CommandManager; tab pairs in Editing/<Tab>/ (updater+view co-located)
Warlander.Deedplanner.Bridges # Bridge model, types, commands, widgets (Bridges/Widgets/)
Warlander.Deedplanner.Docks # Dock model, commands, support resolver
Warlander.Deedplanner.Persistence # Save backends, SaveCoordinator, MapHandler/MapLoader/MapFactory, Compression/
Warlander.Deedplanner.Rendering # Assets/ (Wurm loaders, caches), Outline/, Projectors/, Water/
Warlander.Deedplanner.Cameras # CameraCoordinator, MultiCamera, controllers, camera UI
Warlander.Deedplanner.Screenshots # Screenshot captures/renderer/saver behind IScreenshotFacade
Warlander.Deedplanner.Ui # Layout, tabs, windows, widgets, tooltips, home screen
Warlander.Deedplanner.Platform # Steam/, Debugging/, Features/, Web/ interop, error reporting
Warlander.Deedplanner.Composition # VContainer LifetimeScopes (DI wiring)
Warlander.Deedplanner.Settings # Application settings
Warlander.Deedplanner.Logging # Selective logging (categories, levels, runtime configurator)
Warlander.Deedplanner.Editor # Editor-only tooling (build system, preview generation, feature setup)
Warlander.Deedplanner.Inputs # Generated input actions (DPInput)
Promotion rule: an entity type inside Domain/Entities/ graduates to its own module when it grows its own commands, UI, and controller (the path Bridges took).
- Private fields:
_camelCaseprefix - Methods/Properties/Classes: PascalCase
- Async methods: must end with
Asyncsuffix - Avoid
FindObjectOfTypeor manual component wiring — use VContainer injection instead - Prefer
[SerializeField]for inspector-assigned component references overGetComponentcalls - Input handling uses the modern Unity Input System; input definitions are in
Assets/Prefabs/Input/DPInput.inputactions - Class naming: avoid generic, undescriptive suffixes —
Manager,Handler,Controller,Helper,Util,Service,Provider,Processor, and similar vague nouns. These say where something lives but not what it does. Prefer names that describe the specific responsibility (e.g.WaterFacade,WaterObjectContainer,MapHeightTracker). Existing legacy names (CommandManager) are grandfathered in; new classes must follow this rule. TheViewsuffix is the one intentional exception — MonoBehaviour view classes in the MVP pattern must end withView(e.g.GroundPainterView,TileSelectionView) to distinguish them from their presenter counterparts. - Property formatting: auto-properties and single-expression
get-only properties stay on one line. Anything more complex splitsget/setonto their own lines. Ifgetorsetcontains more than one statement, use expanded block syntax ({ ... }) rather than expression-body (=>) shorthand. - No tuples: do not use tuples — neither implicit (
(int x, string y)) nor explicit (Tuple<int, string>). Define a namedstructor value class instead. Named types are self-documenting, refactorable, and avoid accidental structural coupling. - System boundaries: systems declare only well-thought, necessary dependencies and communicate with other systems through deliberate APIs. No reaching into a neighbor's internals and no convenience couplings — if two systems keep touching each other, the API between them is wrong.
- Interfaces expose behavior, not storage: interfaces (and their implementations) expose public methods, never publicly accessible collections or mutable state. Internal storage (dictionaries, lists) stays private; expose it through purpose-named methods (
AddGround(data)deriving the key from the data,GetGround(shortName),GetAllGrounds()). SeeIDataCatalog/Databasefor the reference shape. - Logging: log through the logging system in
Warlander.Deedplanner.Logging, never viaDebug.Log*directly. One staticLogCategoryper system, declared in the system's root class. The root takesILoggerSource, creates the logger once (_logger = loggerSource.Create(Category);), and passes theICategoryLoggerto other classes in its system — they never reference another class'sCategoryand never takeILoggerSourcethemselves. Systems with multiple container-created consumers get a composition-root facade registered once (WurmAssetFacade,UiLog) that creates the pieces internally and shares its logger. Static utilities do not log — they return failure and the caller logs. Editor-only tools build their ownLoggerSourcewith a privateLogLevelFilter. Per-category level tuning lives exclusively inLoggingConfigurator(runtime) or the utility's own filter. - UI arrangement:
uiroutes to the active Unity UI system andui-uguiowns generic Canvas/layout mechanics. For DeedPlanner UI, also loadunity-ui-screenshotbefore design/mockup work andunity-ui-buildbefore implementation; these project overlays own capture, prefab reuse, MVP wiring, and mockup fidelity, and their project-specific rules take precedence.
Verify compilation after every code edit before considering the task done:
- If an Editor is connected (
unity status): trigger a refresh/compile viaunity command(discover exact command names withunity list) and read the console/compile status back the same way. - If no Editor is connected:
unity test --mode EditModeforces a full compile and surfaces errors, or checkEditor.logafter a domain reload. - If the Unity CLI itself is missing: ask the developer to install it (never install it yourself) and ask them to confirm compilation in the Editor.
- Do not consider a task finished until compilation is clean (errors AND new warnings).
CHANGELOG.md (repo root) records user-facing changes, newest first, with the top section being the unreleased version. Update it as part of any task that changes something a user can see or do: features, UI changes, fixes, new content (models, textures, items), platform/behavior differences. Skip purely internal work (refactors, code health, CI, tests) unless it has a user-observable effect (e.g. performance).
Rules:
- Format:
## [x.y.z] - YYYY-MM-DD(or- Unreleasedon top) followed directly by a flat dash-bullet list. No preamble paragraphs, no summary text, no Added/Changed/Fixed subsections, no known-issues blocks. - Match the existing entry style: one concise dash bullet per change, written for players, not developers. Fixes start with "Fixed ...".
- Add entries to the current unreleased section only. Never edit released sections — they are historical record sourced from published GitHub releases.
- Keep the section coherent: it describes the delta users will experience on upgrade. Do not add fix/tweak entries for a feature introduced in the same unreleased version — fold the improvement into the feature's entry (or drop it); that bug never reached users. When editing an existing entry of yours, prefer updating it over adding a corrective bullet.
- On release, the developer renames the unreleased section to the version and date; agents never create new version sections on their own.
If a proposed approach is architecturally poor, has no clean implementation path, or would require unreasonable workarounds — say so clearly and explain why. Do not attempt to implement it anyway. Proposing a better alternative or declining with reasoning is preferable to producing bad code.
- VContainer — DI container
- R3 — reactive extensions
- Unity InputSystem 1.19.0
- TextMesh Pro — UI text
- Steamworks.NET — Steam distribution/achievements