Refactor project structure and update documentation. Replace PixiJS with plain DOM for UI rendering, enhance README with game features, and revise protocol documentation for HTTP API. Remove unused files and streamline client code for better maintainability.
This commit is contained in:
@@ -8,16 +8,17 @@ way; this file is *how to work in them*.
|
||||
|
||||
| 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` |
|
||||
| schools, the game clock, game rules | `src/HSchool.Simulation` |
|
||||
| the menu API (list, create, delete) | `src/HSchool.Server/Api` **and** `docs/protocol.md` |
|
||||
| what the socket carries | `src/HSchool.Protocol` **and** `src/HSchool.Client/src/net/protocol.ts` **and** `docs/protocol.md` |
|
||||
| connection handling, the loop | `src/HSchool.Server` |
|
||||
| what runs locally | `src/HSchool.AppHost/AppHost.cs` |
|
||||
| rendering, input, HUD | `src/HSchool.Client/src` |
|
||||
| screens, dialogs, formatting | `src/HSchool.Client/src` |
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
dotnet build HSchool.slnx
|
||||
dotnet build h-school.sln
|
||||
```
|
||||
|
||||
```bash
|
||||
@@ -53,20 +54,22 @@ These are the rules that keep the base coherent. Breaking one is a design decisi
|
||||
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`.
|
||||
logic in `src/HSchool.Client` — not even a local clock that ticks between frames.
|
||||
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.
|
||||
3. **Only the loop thread touches a `School` or the `SchoolRegistry`.** Everything inbound goes
|
||||
through `GameCommandQueue`; menu reads use the immutable state the loop publishes; everything
|
||||
outbound goes through the per-client outbox. No locks around the registry, 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.
|
||||
5. **Fixed timestep.** The clock advances by `SimulationOptions.FixedDeltaTime`, never by
|
||||
wall-clock deltas and never from `DateTime.Now`. Same tick count, same date.
|
||||
6. **Everything from the wire is untrusted.** Validate lengths and ranges before anything reaches
|
||||
the simulation — names, dates and speed indexes all arrive from a browser.
|
||||
7. **One intent per message.** A frame that also resends a neighbouring field overwrites it with a
|
||||
stale client copy; that is why running and speed are separate messages.
|
||||
|
||||
## Conventions
|
||||
|
||||
@@ -76,14 +79,16 @@ say so explicitly in the change description.
|
||||
- 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.
|
||||
- New tunables go on `SimulationOptions` with a default, not as a constant buried in a class.
|
||||
|
||||
**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.
|
||||
- Modules stay thin: `net/` speaks to the server, `ui/` renders, `format/` formats, `main.ts`
|
||||
wires them together.
|
||||
- No framework, plain DOM. `ui/dom.ts` is the whole helper budget.
|
||||
- UI strings are Russian; game dates are formatted through `format/gameTime.ts`, never inline.
|
||||
|
||||
**Both**
|
||||
|
||||
@@ -92,13 +97,15 @@ say so explicitly in the change description.
|
||||
|
||||
## Testing policy
|
||||
|
||||
- Simulation changes need a `GameWorld` test. They are fast, hermetic and do not need a host.
|
||||
- Simulation changes need a `GameClock` or `SchoolRegistry` test. They are fast and need no 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`.
|
||||
- Server wiring, endpoints and the WebSocket 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.
|
||||
- Those tests also share one server, so schools survive between them: start each test by clearing
|
||||
the list (`SchoolApiTests.ResetAsync`) instead of assuming it is empty.
|
||||
- The screens have no unit tests — a DOM environment would cost a dependency the project does not
|
||||
have. Verify UI changes by running the app.
|
||||
|
||||
## Dependencies
|
||||
|
||||
@@ -106,17 +113,24 @@ say so explicitly in the change description.
|
||||
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.
|
||||
- Keep the dependency count low. Arch, Aspire and the test runners are the whole budget; `pixi.js`
|
||||
is installed but not imported yet — it is there for the game view inside a school.
|
||||
|
||||
## Things that will bite you
|
||||
|
||||
- **`<dialog>`'s `close` event is not delivered by every engine** (Chromium 148 fires only
|
||||
`toggle`). `ui/modal.ts` resolves its promise explicitly on every exit for that reason — never go
|
||||
back to awaiting the event, or a dialog will silently freeze the screen that awaited it.
|
||||
- **Game dates are UTC on the wire and UTC when formatted.** A `DateTime` bound from configuration
|
||||
arrives as `Kind=Unspecified`, serializes without a `Z`, and the browser then reads it in its own
|
||||
time zone — `SimulationOptions.DefaultStartDate` forces the kind for exactly that reason.
|
||||
- `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.
|
||||
- Every school runs whether or not a connection is watching it, so anything you hang off the tick
|
||||
runs six times over once six schools exist. `OpenSchool` only subscribes to clock frames.
|
||||
- The menu polls `GET /api/schools` once a second and patches its cards in place. Rebuilding the
|
||||
list on every refresh would drop focus and swallow clicks.
|
||||
- The client outbox drops the oldest frame under pressure. That is correct for clock frames 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