The editor offered to import them as textures on every start; source files for authoring scripts do not belong under Content. create_world.py and generate_heightmap.py read them from RawContent/World/Heightmaps. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
141 lines
10 KiB
Markdown
141 lines
10 KiB
Markdown
# 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 `<Project>`, 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 built by
|
|
`Scripts/Authoring/create_world.py` from PNGs in `RawContent/World/Heightmaps/` (seeded by `generate_heightmap.py`)
|
|
through `ULandscapeAuthoringLibrary` in the `SaltyEditor` module. It uses Elite_RockyMeadows' landscape material.
|
|
No gameplay code references it; the gym is still the test level.
|
|
|
|
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/<Feature>/` | 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/<Feature>.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="<abs>\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.
|