Skip to content

Latest commit

 

History

History
227 lines (159 loc) · 24.5 KB

File metadata and controls

227 lines (159 loc) · 24.5 KB

AGENTS.md

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.

Agent Config Layout

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.

Human-Facing Documentation

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.

Commit Message Style

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.

Project Overview

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.

Building and Running

Development workflow:

  • Open the project in Unity 6000.3.20f1 (see ProjectSettings/ProjectVersion.txt for 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.sln for 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.

Unity CLI (preferred automation path)

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.xml
  • unity open — open the project with the correct Editor version
  • unity 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.

Agentic Verification

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:

  1. Compile check (connected Editor via unity command, or unity test --mode EditMode batch) — catches syntax/type errors.
  2. EditMode tests (unity test --mode EditMode batch, or run_tests on the connected Editor) when the change touches covered logic.
  3. 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.
  4. 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.

Architecture

Dependency Injection & MVP Pattern

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

Core Data Model

  • Map (Domain/Map*.cs) — central data structure, recently split into:
    • MapTileGrid — 2D grid of tiles
    • MapLevelRenderer — camera-based hiding and fading applied/restored for every camera; active view stores editing context only
    • MapBridgesController — bridge logic (lives in the Bridges/ module)
    • MapDockCollection — owns dock registration, removal, and active state; factories create unattached docks
    • MapRoofCalculator — owned by each map; pending roof work runs from Map.LateUpdate
    • MapHeightTracker — 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

Data Definitions: Single Source of Truth

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.

Tab-Based Updater Pattern

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.

Camera System

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.

Screen-Space Outline System

Custom screen-space selection outline unified under Rendering/Outline/:

  • ScreenSpaceOutlineFeature — ScriptableRendererFeature; renders outlined objects to a mask RT, dilates, composites border over scene
  • OutlineCoordinator — plain C# class with one registration per object containing its priority, outline type, and renderers; subscribes once per object
  • OutlineFeatureBridge — IInitializable+IDisposable, bound NonLazy; discovers and wires the feature on startup via reflection
  • OutlineEntry — readonly struct grouping renderers and outline type
  • Auto-setup: Editor/OutlineFeatureSetup.cs uses [InitializeOnLoad] + EditorApplication.update

Command Pattern (Undo/Redo)

All map edits are implemented as IReversibleCommand objects managed by CommandManager. Never modify map state directly — always go through commands.

Async / Reactive

  • Modern async/await throughout (converted from coroutines — async methods use the Async suffix per C# convention)
  • R3 (Cysharp reactive extensions) used for streaming updates and observable texture loading
  • TextureLoader, WurmModelLoader, MaterialLoader load Wurm assets asynchronously

Save/Load

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.

Settings & Features

  • DeedPlannerSettings declares the unified registry and typed module settings (CameraSettings, EditingSettings, UiSettings, GraphicsOptions); project-scoped InputSettings owns the shared DPInput; MapRenderSettings remains session-only
  • DPFeatureStateRepository — feature flags for experimental features

Key Namespaces

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

Coding Conventions

  • Private fields: _camelCase prefix
  • Methods/Properties/Classes: PascalCase
  • Async methods: must end with Async suffix
  • Avoid FindObjectOfType or manual component wiring — use VContainer injection instead
  • Prefer [SerializeField] for inspector-assigned component references over GetComponent calls
  • 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. The View suffix is the one intentional exception — MonoBehaviour view classes in the MVP pattern must end with View (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 splits get/set onto their own lines. If get or set contains 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 named struct or 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()). See IDataCatalog/Database for the reference shape.
  • Logging: log through the logging system in Warlander.Deedplanner.Logging, never via Debug.Log* directly. One static LogCategory per system, declared in the system's root class. The root takes ILoggerSource, creates the logger once (_logger = loggerSource.Create(Category);), and passes the ICategoryLogger to other classes in its system — they never reference another class's Category and never take ILoggerSource themselves. 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 own LoggerSource with a private LogLevelFilter. Per-category level tuning lives exclusively in LoggingConfigurator (runtime) or the utility's own filter.
  • UI arrangement: ui routes to the active Unity UI system and ui-ugui owns generic Canvas/layout mechanics. For DeedPlanner UI, also load unity-ui-screenshot before design/mockup work and unity-ui-build before implementation; these project overlays own capture, prefab reuse, MVP wiring, and mockup fidelity, and their project-specific rules take precedence.

After Code Changes

Verify compilation after every code edit before considering the task done:

  1. If an Editor is connected (unity status): trigger a refresh/compile via unity command (discover exact command names with unity list) and read the console/compile status back the same way.
  2. If no Editor is connected: unity test --mode EditMode forces a full compile and surfaces errors, or check Editor.log after a domain reload.
  3. 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.
  4. Do not consider a task finished until compilation is clean (errors AND new warnings).

Changelog

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 - Unreleased on 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.

Honesty About Feasibility

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.

Notable Third-Party Packages

  • VContainer — DI container
  • R3 — reactive extensions
  • Unity InputSystem 1.19.0
  • TextMesh Pro — UI text
  • Steamworks.NET — Steam distribution/achievements