Files
h-school/AGENTS.md
T
Leonid Pershin 5a400be792 Add SwarmUI settings management and portrait generation enhancements
- Introduced new API endpoints for managing SwarmUI settings, including fetching and saving presets and age rules.
- Updated the portrait generation logic to utilize the new settings structure, allowing for dynamic preset selection based on age.
- Enhanced UI components to support SwarmUI settings, including localization for new strings and improved styling for settings sections.
- Added tests to verify the functionality of new settings endpoints and portrait generation behavior.

This commit lays the groundwork for more flexible and user-friendly portrait generation options.
2026-08-20 06:06:45 +03:00

186 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 |
| --- | --- |
| schools, the game clock, game rules | `src/HSchool.Simulation` |
| defs, JSONC catalog, map validation | `src/HSchool.Content` |
| skills, traits, body, needs, countries | `src/HSchool.Content` |
| people generation, families, roster records, yearly intake | `src/HSchool.People` |
| timetable planning | `src/HSchool.Schedule` |
| routes, day plans, who comes today | `src/HSchool.Ai` |
| people in a school's World, need decay, walking | `src/HSchool.Simulation` |
| the menu API (list, create, delete, mods, catalog) and the people list/card | `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, workers, saves | `src/HSchool.Server` |
| what runs locally | `src/HSchool.AppHost/AppHost.cs` |
| screens, dialogs, formatting, UI language | `src/HSchool.Client/src` |
| clothing, inventory, climate, dress codes | [`docs/design/inventory.md`](docs/design/inventory.md) — slice 7, phases 2937; country and wardrobes are live, weather is not |
| login, who owns a school, watching others | [`docs/design/session.md`](docs/design/session.md) — slice 8, phases 3839 |
| clock speed buttons and high-speed stride | `src/HSchool.Simulation/ClockSpeed.cs` **and** [`docs/design/session.md`](docs/design/session.md) — phase 40 |
| talks, opinions, fights, romance pack | [`docs/design/social.md`](docs/design/social.md) — slice 9, phases 4147 |
## Commands
```bash
dotnet build h-school.sln
```
```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` — 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 a school's worker thread touches that `School` or its `World`.** The supervisor
owns the mailbox table and never calls into a world. Create/delete/name suggestions go
through `GameCommandQueue`; open/close/running/speed go to that school's mailbox; menu
reads use the snapshots workers publish; everything outbound goes through the per-client
outbox. No locks around a school, 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.** 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
**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 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 to the server, `ui/` renders, `format/` formats, `i18n/`
holds the RU/EN dictionaries, `main.ts` wires them together.
- No framework, plain DOM. `ui/dom.ts` is the whole helper budget.
- UI strings go through `t(...)` in `i18n/strings.ts`, never inline. Game dates go through
`format/gameTime.ts`, which follows the active locale.
**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 `GameClock` or `SchoolRegistry` test. They are fast and need no host.
Putting a roster into `World` and ticking needs belongs there too.
- People generation belongs in `tests/HSchool.People.Tests`. Same seed, map, country and
native language must produce the same roster; the suite does not boot a host.
- Timetable planning belongs in `tests/HSchool.Schedule.Tests`. Same staff, map and locks must
produce the same table; the suite does not boot a host.
- Walking and day plans belong in `tests/HSchool.Ai.Tests`. Same seed and map must produce the
same route and commute; the suite does not boot a host, and it does not reference Arch.
- Catalog, inheritance, patches and map validation belong in `tests/HSchool.Content.Tests`.
Feed the loader documents, not disk paths.
- Protocol changes need a round-trip test **and** a byte-layout assertion on both sides.
- 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 (and now on disk too):
start each test by clearing the list (`SchoolApiTests.ResetAsync`) instead of assuming it
is empty. Reset deletes through the API, which deletes the save files. Headless AppHost
sets `HSchool:AllowSaveReload` so tests can `POST /api/dev/reload-schools` without killing
the shared fixture.
- Screen logic is covered in Vitest under happy-dom (`ui/*.test.ts`): people filters and the
pager, the create dialog (core stays on, map reset, submit busy), planner rejection text, and
the payroll-cap message. Layout and styles are still verified by running the app. Dictionaries
and date formatting stay in `i18n/strings.test.ts` and `format/gameTime.test.ts`.
## 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, Aspire and the test runners are the whole budget.
## 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.
- **A modal `<dialog>` is absolutely positioned with `inset: 0`,** so `height: auto` stretches it
to its `max-height` instead of shrink-wrapping. `.dialog--screen` uses `height: fit-content` for
that reason — the map editor sized itself to 800px around a two-room school before it did.
- **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 each `SchoolWorker` does,
capped at 5 steps — do not "simplify" it away.
- 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.
- **An abandoned `WaitToReadAsync` stays queued on its channel.** `GameClient.RunSendLoopAsync`
keeps its two wait tasks across iterations for that reason; recreating both on every idle pass
leaked the loser of `Task.WhenAny` once per clock frame.
- **A map snapshot is not bounded by `MaxMessageSize`.** That constant limits what the server
reads. Outbound snapshots are sized with `ProtocolCodec.MapSnapshotSize`, because a school the
player enlarged in the create editor passes 8 KiB at roughly sixty furnished rooms.
- **A worker that dies must tell the supervisor** (`GameCommand.WorkerFailed`). It owns the table,
so a school that removed itself would break invariant 3 — and a school that removes nothing
leaves a card in the menu whose clock never moves again.
- 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.
- **A large one-shot `Tick(week)` jumps the clock, then applies all those minutes at the new
time.** Presence equality tests must loop `FixedDeltaTime` or one game minute. `string.GetHashCode`
is randomized — commute slack goes through `Seed.Mix`, never that.
- **The school seed used to be its id.** A test that needs a specific family shape is green on
one machine and red on a clean clone. Assert a property, not the first pupil of school 1.
- **Host tests share one server and one save folder.** Start each test by clearing the list
through the API (`SchoolApiTests.ResetAsync`).
- **A paused school still emits presence frames.** The worker loop ticks the mailbox even when
the clock does not advance, so occupancy (often empty) and lesson labels from the timetable
keep going out. Opening sends one extra snapshot. A test that waits for *people* on pause
will hang; a test that waits for a lesson label will not.