123 lines
5.6 KiB
Markdown
123 lines
5.6 KiB
Markdown
# Working agreements
|
|
|
|
Read this before changing anything. It is written for coding agents, and it is just as valid for
|
|
humans. [`docs/architecture.md`](docs/architecture.md) explains *why* the pieces are shaped this
|
|
way; this file is *how to work in them*.
|
|
|
|
## Orientation
|
|
|
|
| I want to change… | Go to |
|
|
| --- | --- |
|
|
| game rules, movement, entities | `src/HSchool.Simulation` |
|
|
| what the client receives | `src/HSchool.Protocol` **and** `src/HSchool.Client/src/net/protocol.ts` **and** `docs/protocol.md` |
|
|
| connection handling, endpoints | `src/HSchool.Server` |
|
|
| what runs locally | `src/HSchool.AppHost/AppHost.cs` |
|
|
| rendering, input, HUD | `src/HSchool.Client/src` |
|
|
|
|
## Commands
|
|
|
|
```bash
|
|
dotnet build HSchool.slnx
|
|
```
|
|
|
|
```bash
|
|
dotnet test
|
|
```
|
|
|
|
```bash
|
|
npm --prefix src/HSchool.Client test
|
|
```
|
|
|
|
```bash
|
|
npm --prefix src/HSchool.Client run build
|
|
```
|
|
|
|
```bash
|
|
dotnet run --project src/HSchool.AppHost
|
|
```
|
|
|
|
`run-aspire.cmd` is the same command for Windows users who want a double-clickable entry point —
|
|
keep the two in sync if the AppHost path ever moves.
|
|
|
|
`dotnet run --project src/HSchool.AppHost` starts the server *and* the Vite dev server and opens
|
|
the Aspire dashboard. The Vite port is assigned per run (`npm run dev -- --port <random>`), so read
|
|
the client URL off the dashboard instead of assuming 5173.
|
|
|
|
Do not start a dev server with a bare `npm run dev` when you meant to run the whole app — the
|
|
client only finds the backend through the Aspire-injected `SERVER_HTTP` environment variable, or
|
|
the `localhost:5180` fallback that matches the server's own launch profile.
|
|
|
|
## Invariants
|
|
|
|
These are the rules that keep the base coherent. Breaking one is a design decision, not a detail —
|
|
say so explicitly in the change description.
|
|
|
|
1. **The server is authoritative.** The client sends intents and draws what it is told. No game
|
|
logic in `src/HSchool.Client`.
|
|
2. **The protocol lives in three places at once.** `ProtocolCodec.cs`, `protocol.ts` and
|
|
`docs/protocol.md` change in the same commit. A layout change bumps
|
|
`ProtocolConstants.Version` / `PROTOCOL_VERSION`. Tests on both sides assert byte offsets —
|
|
if one of them has to change, so do the other two.
|
|
3. **Only the loop thread touches `GameWorld`.** Everything inbound goes through
|
|
`GameCommandQueue`; everything outbound goes through the per-client outbox. No locks around the
|
|
ECS world, no `Task.Run` into it.
|
|
4. **The simulation knows nothing about the network.** `HSchool.Simulation` must not reference
|
|
ASP.NET Core, sockets or logging infrastructure. It stays testable without a host.
|
|
5. **Fixed timestep.** Systems get `SimulationContext.DeltaTime`, never wall-clock time and never
|
|
`DateTime.Now`. Same inputs, same results — `Simulation_IsDeterministicForTheSameInputs` guards it.
|
|
6. **Everything from the wire is untrusted.** Validate lengths and ranges in the handler before
|
|
anything reaches the simulation.
|
|
|
|
## Conventions
|
|
|
|
**C#**
|
|
|
|
- File-scoped namespaces, `var` where the type is obvious, primary constructors for services.
|
|
- Private fields are `_camelCase`; `.editorconfig` enforces it.
|
|
- Nullable is on everywhere. Don't add `!` to silence it; fix the flow.
|
|
- Internal by default in `HSchool.Server`; public only where another project consumes it.
|
|
- New tunables go on `SimulationOptions` with a default, not as a constant buried in a system.
|
|
|
|
**TypeScript**
|
|
|
|
- `strict` is on, no `any`, no non-null `!` assertions.
|
|
- Relative imports carry the `.ts` extension (bundler resolution is configured for it).
|
|
- Modules stay thin: `net/` speaks protocol, `game/` renders, `main.ts` wires them together.
|
|
- No framework. If a UI need appears, plain DOM first.
|
|
|
|
**Both**
|
|
|
|
- Comments explain *why*, not *what*. Assume the reader can read code.
|
|
- Match the surrounding style rather than introducing a new one.
|
|
|
|
## Testing policy
|
|
|
|
- Simulation changes need a `GameWorld` test. They are fast, hermetic and do not need a host.
|
|
- Protocol changes need a round-trip test **and** a byte-layout assertion on both sides.
|
|
- Server wiring, endpoints and the WebSocket handshake belong in `tests/HSchool.AppHost.Tests`.
|
|
That suite shares one AppHost across all tests (`AppHostFixture`) — keep it that way, booting
|
|
per test costs about ten seconds each.
|
|
- Never assert on the *first* snapshot after a join without checking the entity is in it; the
|
|
frame in flight may predate the spawn. Use `ReceiveSnapshotWithAsync`.
|
|
|
|
## Dependencies
|
|
|
|
- NuGet versions are centrally managed in `Directory.Packages.props`. Add the version there and a
|
|
bare `<PackageReference Include="..." />` in the project.
|
|
- Transitive pinning is on, so a downgrade warning means you bump the central version rather than
|
|
adding a per-project override.
|
|
- Keep the dependency count low. Arch, PixiJS, Aspire and the test runners are the whole budget;
|
|
anything new needs a reason in the change description.
|
|
|
|
## Things that will bite you
|
|
|
|
- `PeriodicTimer` does not catch up on its own. The accumulator in `GameLoopService` does, capped
|
|
at 5 steps — do not "simplify" it away.
|
|
- Arch recycles entity ids. Replicate `NetworkId`, never `Entity.Id`.
|
|
- Snapshots are full-state: an entity missing from a frame is *despawned* by the client. Filtering
|
|
entities out of a snapshot is how you accidentally delete them on screen.
|
|
- The client outbox drops the oldest frame under pressure. That is correct for snapshots and wrong
|
|
for anything that must arrive exactly once — such a message would need its own path.
|
|
- `erasableSyntaxOnly` is off in `tsconfig.app.json` on purpose: constructor parameter properties
|
|
are used throughout.
|