Update .gitignore to exclude TypeScript build info and add dist directory. Expand README with project overview, technology stack, prerequisites, and instructions for running and testing the application.
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user