# Unreal prototype An Unreal Engine 5 project, C++ first, that builds and proves three things in order: a movement controller, fighting, and crafting. **Engine: Unreal Engine 5.8, launcher build at `D:\UE_5.8` (D-39). Project: `Salty` (D-40).** Where an older doc says ``, read `Salty`. ## Before you start Read [`Docs/Spec/README.md`](Docs/Spec/README.md) (the notation and the twelve cross-cutting rules) and [`Docs/Spec/Architecture.md`](Docs/Spec/Architecture.md) before writing or changing any code. Nearly every wiring decision follows from them. For *what* to build next, read [`Docs/Steps.md`](Docs/Steps.md). It is a ladder, not a roadmap: each rung says what exists afterwards, links the spec sections that define it, and states the proof that closes it. The spec docs under `Docs/Spec/` are the specification; each is C++-shaped pseudocode plus the reasoning, and the pseudocode is meant to be implemented rather than admired. If a step and its spec disagree, the spec wins and the step is corrected. The human summary is [`Docs/Design.md`](Docs/Design.md). Ideas from the two earlier projects that are not built are in [`Docs/Ideas.md`](Docs/Ideas.md); do not build one inside a step about something else. ## Where the project actually is Step 1 is built (see `Docs/Worklog.md`): `Salty.uproject` at the root, the `SaltyCore` and `Salty` modules, the placeholder test, the tag namespaces, LFS, the scripts and `L_Gym`. It was created from the engine's Third Person template; the plain template classes are `ATemplate*` and are the placeholder body until step 3. The template's `Variant_*` folders under `Source/Salty/` and `Content/` are reference only (D-41): do not add code there, do not depend on them. Four Fab asset packs sit in `Content/` (`HouseForge_01`, `Medieval_Weapons`, `Elite_RockyMeadows`, `RPGEnvironmentVFX`); none is referenced yet, and a pack is used only when a step's spec names the asset. Step 2 is built: `UTelemetrySubsystem` on the game instance with the null, log and JSON Lines sinks (`-telemetry` or `telemetry.File 1` writes `Saved/Telemetry/session_*.jsonl`), the four session events, `ASaltyGameMode` minting the session id into `ASaltyGameState`, and `bs.TelemetryTest` in `Source/Salty/Core/SaltyCheats.cpp`, the home of every later `bs.*` command. Emit with a name from `TelemetryEvents` and a `FTelemetryPayload`. The editor serves the engine's MCP plugin on `127.0.0.1:8000/mcp` while it is open (D-43); `.mcp.json` points Claude Code at it. Reconnect with `/mcp` once the editor is up. Close the editor and its Live Coding console before a shell build. Off the ladder: `Content/Maps/L_World`, a ~200 km² world-partitioned landscape, is a product of three inputs and nothing else: the manifest `RawContent/World/World.json` (size, what a heightmap value means in metres, the height source, the paint-layer rules), the PNGs `Scripts/Authoring/generate_heightmap.py` writes from it into `RawContent/World/Heightmaps/`, and `Scripts/Authoring/create_world.py`, which imports them through `ULandscapeAuthoringLibrary` in the `SaltyEditor` module and dresses the level as Elite_RockyMeadows dresses its demo maps (landscape material and layers, sun with cloud shadows, skybox, fog, grade; numbers read with `dump_level.py`). The height source is seeded noise today, weathered and carved by `heightmap_erosion.py` (hydraulic droplets, thermal weathering, strata; D-46); a real heightmap is a manifest edit and a rerun (`RawContent/World/README.md`). Run `create_world.py` detached, never under a tool timeout, and never while the editor has `L_World` open. No gameplay code references the world; the gym is still the test level. Where the generator is going is settled in [`Docs/Terrain.md`](Docs/Terrain.md) (D-47, D-48): a Go core in `Tools/Terrain/`, stream-power fluvial erosion instead of droplets as the thing that shapes the land, a canvas of 7141 vertices at 200 cm over −512…1536 m, and a `Generated` edit layer so sculpting survives a rerun. Read it before touching the generator, and the numpy pipeline is not to be extended. The Go core now exists and works: `Tools/Terrain/` builds an uplift *rate* field and lets a Braun-Willett stream-power solve produce the terrain from it, with a Roering nonlinear hillslope law and per-uplift-class statistics. The coast is a pass of its own now (D-51, `internal/coast`), running after the solve: a continental shelf whose width follows the relief behind the shore, a surf that planes a shore platform and leaves a cliff where its reach ends, and a sediment budget carried along the shore into the bays and out of the river mouths. It stops at the geology grid, so the detail passes are unbuilt and `L_World` still comes from the numpy pipeline. **[`Docs/Terrain-Next.md`](Docs/Terrain-Next.md) is the working brief** — how to run it, what each output map is for, what still looks wrong, and what has already been measured and rejected. Start there. Where it is going: finish the detail passes so there is a full-resolution output at all, then make the source a **painted map** — an author paints the uplift, the mask and the erodibility, never the height, and the solve turns that intent into terrain with real drainage. Worlds are far larger than today's 14 km canvas, so the geology solve stays whole (it cannot be tiled: drainage area is global) and only the detail passes tile. That makes two rules binding on anything written now: index every noise and hash by **absolute world coordinates** rather than grid index, and give every tiled pass an overlap margin. Composition tuning is parked. Next: step 3, the body, the camera, the input map and the gym. Step 1's PIE proof (two clients, dedicated server, two pawns in the gym) is still a person's to tick. Update this section when a step closes. ## Layout The `.uproject` sits at the repository root, which is what the `.gitignore` assumes. | Path | What it is | | --- | --- | | `Salty.uproject`, `Source/Salty.Target.cs`, `SaltyEditor.Target.cs`, `SaltyServer.Target.cs` | The project and its three build targets. The server target needs a source-built engine (OD-04) | | `Source/SaltyCore/` | Rules, data types, tags, the telemetry contract. Knows no `AActor`. Tests in `Tests/` | | `Source/Salty/` | Gameplay: `Core/`, `Stats/`, `Movement/`, `Interaction/`, `Combat/`, `Crafting/`, `UI/`, one folder per feature. `Variant_*` is template reference (D-41) | | `Content//` | Assets per feature: `Definitions/` for data assets, Blueprints, meshes, montages | | `Content/Maps/L_Gym` | The movement test level, authored by `Scripts/Authoring/create_gym.py`. Never leaves the project | | `Content/Tests/` | Functional test maps | | `Config/Tags/.ini` | Gameplay tag source of truth, one file per top-level namespace | | `Scripts/` | `run-tests.sh`, `build.sh`, and `Authoring/` for editor Python scripts. Run by hand; there is no CI. `UE_ROOT` overrides the engine path | | `Docs/` | The documentation set. Update it when you change how something works | | `Docs/Worklog.md` | One terse line per step closed or wall hit | Module dependency is one way: `Salty` depends on `SaltyCore`, never the reverse. If the core module seems to need an actor, the thing it needs is data. ## Conventions - **The server decides.** Every mutation is a `Server*` path that validates its instigator and re-checks its preconditions, even in standalone play. The client predicts its own movement and its own animation, nothing else. Never write a listen-server assumption or an "is this the host" branch. - **Rules are pure C++ in the core module; actors and components adapt them.** A preview and the real thing call the same function. Every pure rule has automation tests, one per rejection reason. - **One funnel per consequence.** All damage through `UDamageExecution`; all interactions through `UInteractionSubsystem`; all activity quality through `FActivityResult`. Do not add a second path. - **One stat block; everything is an effect on it.** Every body has `UStatBlockAttributeSet`; every buff, debuff, surface, weight, gear and base value is a gameplay effect applied through `ApplyStatus` or `ApplyDefaults`. Never add a multiplier, a modifier list or a status timer to a component. Counters are `Immune.*` tags and resist attributes on the receiver. See `Docs/Spec/Stats.md`. - **Content is data.** `UPrimaryDataAsset` subclasses, addressed by gameplay tag or primary asset id, art through soft references. No string ids. If a variant needs code, the model is wrong. - **Gameplay Tags are the vocabulary.** Defined in `Config/Tags/`; tags code references are declared natively with `UE_DECLARE_GAMEPLAY_TAG_EXTERN` / `UE_DEFINE_GAMEPLAY_TAG`. No string literal tag at a call site. - **C++ owns rules, replicated properties and RPCs. Blueprints own composition, tuning and cosmetics.** A Blueprint is a child of a C++ class that sets assets and numbers and wires `*_Cosmetic` hooks. - **Lifetimes are the engine's.** Game-instance subsystems for the application, world subsystems for a map, the player state for a player across bodies, the pawn for a body. Nothing about a player that would matter tomorrow lives on the character actor. - **No world searches in gameplay code.** No `GetAllActorsOfClass`, no static gameplay singletons. Subsystems are the registries; actors register in `BeginPlay` and unregister in `EndPlay`. - **Every rejection carries a reason tag** a player can read. **Nothing is destroyed silently.** - **Emit telemetry through `UTelemetrySubsystem` with names from `TelemetryEvents`**, never a literal, never a static helper. Every feature emits; the catalogue in `Docs/Spec/Telemetry.md` and the feature spec agree. - **Naming is Unreal's:** `A`/`U`/`F`/`E`/`I` prefixes, no project prefix on classes, `PascalCase` members, asset prefixes `BP_ DA_ DT_ IA_ IMC_ GA_ GE_ GC_ ABP_ AM_ SK_ SM_ M_ MI_ WBP_ T_ L_`. Placeholders carry `_Proto`. - **Units are centimetres and seconds.** `FText` for anything a player reads. `TObjectPtr` for object properties. - **The design word "material" is "substance"** in code and docs. `UMaterial` is the engine's. - Do not add an interface, a module or an abstraction for one implementation with no test double. The two source projects were each trimmed of speculative abstraction once already; the interfaces that exist here are the ones with several implementations named in the specs. ## Commands ```bash Scripts/run-tests.sh # headless: UnrealEditor-Cmd Salty.uproject -ExecCmds="Automation RunTests Salty; Quit" -unattended -nopause -NullRHI Scripts/run-tests.sh Core # a filter: Salty.Core.* Scripts/build.sh Win64 Development # UAT BuildCookRun for the game target; add -server for the server target once the engine is a source build D:/UE_5.8/Engine/Build/BatchFiles/Build.bat SaltyEditor Win64 Development -Project="\Salty.uproject" -WaitMutex # compile the editor from a shell UnrealEditor-Cmd.exe Salty.uproject -run=pythonscript -script=Scripts/Authoring/create_gym.py # rerun an authoring script ``` There is no CI, deliberately, and there will not be until there is a reason. Run the tests yourself before saying a step is done. Every replicated step is also played in the editor with two clients, "Run Dedicated Server" on, and network emulation at 100 ms and 5 % loss with `p.NetShowCorrections 1`. ## Working with the editor - Prefer editing C++ and `.ini` files directly. Blueprints and maps are binary; keep them thin and keep logic out of them so a diff can be reviewed. - Batch authoring (a folder of definition assets, a greybox level from a table) goes through the editor's Python API in `Scripts/Authoring/`, idempotent, kept so it can be rerun. Do not hand-edit `.uasset` files. - Blueprint-side wiring an authoring script cannot express is done once in the editor and recorded in the step's worklog line so it can be redone. - Watch for the compile trap: a C++ error that stops the editor launching is fixed in the files, with the build output, not by guessing. The editor's Live Coding is fine for iteration and unreliable for header changes; a header change means a full rebuild. - Every `.uasset` and `.umap` goes through Git LFS; `.gitattributes` is created in step 1. Run `git lfs install` once per machine. Never commit `Saved/`, `Intermediate/`, `DerivedDataCache/` or `Binaries/`; the `.gitignore` covers them. ## Documentation upkeep - A step that lands updates: its status in `Docs/Steps.md`; a **What was built, and where it differs** section at the end of the spec it built from; a line in `Docs/Worklog.md`; a line in `Docs/Decisions.md` if a decision was taken; and the "Where the project actually is" section above. - A design change that contradicts a decision names the decision it supersedes in the log. Never delete a log line. - Do not turn `Docs/Steps.md` into a roadmap. Detail the next step when the current one closes, not before.