No description
  • C# 96.4%
  • Python 1.8%
  • HLSL 0.9%
  • Lua 0.4%
  • Shell 0.2%
  • Other 0.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Andrea 527761287a
All checks were successful
ci / Lua content lint (push) Successful in 18s
ci / Lint & Test (push) Successful in 5m8s
feat: cut the terrain structurally, and judge authored terrain with the game's own rule (#284)
## Summary

The client removes the layers above you **only when the world's own structure says you are inside it** —
never because something happens to be drawn in front of you.

That distinction is the whole PR, and it was arrived at by getting it wrong first, in the open. Cutting
on *occlusion* — "something is between the camera and the body" — is true on any slope with a hill in
front of it, and it blacked out half the screen in an open forest. Cutting on "there is higher ground
nearby" is the same mistake spelled differently; the forest has that too. What separates a crater from a
forest is **measured**: of the eight compass directions, how many still offer a way out (0-1 against 4-5,
at every radius tried).

So there are exactly two ways in:

- **UNDER** — your column has another interval above the one you stand on: a cave, a tunnel, an overhang;
- **SHUT IN** — at most one of the eight directions offers a way out within `EscapeRadiusCells`.

A canyon has two ways out along its length and is deliberately left alone; so is a plain with a hill on
it. **Nothing in it is absolute**, so a canyon at z −80 behaves exactly like the same canyon at z +80 and
terrain authored below zero needs no special case — the owner's requirement, in his words: *"van scritte
regole strutturali precise, un domani creerò canyon e valli sotto z0, non voglio ripetere sta
discussione"*. And nothing in it is a camera, so the answer is identical from every viewing angle.

What is then removed is everything above a **structural height** — the bottom of the ceiling you are
under, or head height over the floor you are shut in on — and never a shape derived from where the player
stands. A boundary anchored to the walker slides across the landscape at every step (*"il problema non è
la zona nera, è che si muove"*); one anchored to a height is a contour of the terrain and holds still.

The full model lives in the header of `TerrainCutaway` (Client.Core) — it is the reference, and
`CLAUDE.md` carries the invariant so it cannot quietly drift back to occlusion.

## Screenshots / recording

**Not attached, and I am not going to pretend otherwise.** This behaviour was developed and judged
interactively with the owner over several rounds on his own world, against his screenshots — that is how
the occlusion version was caught and rejected. What I can attach now would be a re-staging after the fact,
not the evidence that drove the decisions.

What replaces them here, and is stronger for a reviewer: `TerrainCutaway` is **pure and unit-tested**, and
the landform contract is pinned as a table — cave → cut, crater (0-1 ways out) → cut, canyon (2 ways out)
→ no cut, valley and plain → no cut. `CutawayState` also exposes `Under`, `ShutIn`, `CutMode` and a
one-word `Shape` to the admin overlay, so the decision is legible in-game rather than being a black screen
with no explanation.

Three known gaps are filed rather than hidden: **#279** (a v3 that preserved a reachable region and cut by
screen-space occlusion — built, judged worse by the owner, reverted; the revert pair is not in this branch
because the two cancel exactly), **#280** (the character reads as detached from the terrain when the cut is
active) and **#281** (silhouette in the open).

## How it was tested

- Unit tests in `IsoMmo.Client.Core.Tests` over `TerrainCutaway`: the two entries (UNDER / SHUT IN), the
  eight-direction escape count, and the landform table above.
- The Z fed to it is the **authoritative** surface, never the smoothed `DisplayZ` — asserted, because that
  is the one substitution that would make the answer depend on presentation.
- Gates on this exact commit: CSharpier clean (580 files), `dotnet test` green (10 test projects), whole
  solution builds with zero warnings.

## Checklist

- [x] `just lint` passes (CSharpier + analyzers, zero warnings)
- [x] `just test` is green
- [x] The whole solution builds (client and tools included)
- [x] Multi-platform preserved (server on Win/macOS/Linux, client on Win/macOS)
- [x] Tests added/updated for this change
- [x] Linked the related issues (#279, #280, #281 stay open as follow-ups)

---

**Stack:** 2 of 3, based on `feat/volume-world-f1` (#283). Merge that one first; this PR's base can then be
retargeted to `main`.

---

# Added by the merge of #285: the map editor redesign (closes #282)

While this PR was being rebased onto the squashed `main`, **#285 was merged into this branch**, so it now
carries the editor work as well. Nothing was lost or changed by that: the tree here is byte-for-byte
identical to the original 43-commit branch, verified by diff. The two halves stay independent — the
cutaway touches the client renderer, the editor touches `tools/IsoMmo.MapEditor*` — so they can still be
read separately.

## Summary of the editor half

The height tools were producing terrain the game could not use, and the editor could not show it. They are
rebuilt around one idea: **nothing reaches the map until it has been judged, and it is judged with the
game's own rule** (`StepRule`) rather than with arithmetic of the editor's own — an editor that scores
terrain its own way ends up promising ground the server refuses.

- **Preview**: every height tool proposes. Enter commits it as one undoable step, Escape and the map is
  byte-for-byte as it was. The 3D view carries the proposal too — a road is a shape, and a top-down canvas
  cannot show a shape.
- **Overlays** score slope / walkability / reachability, keeping a *route* (`SustainableRisePerStep`) and a
  single *ledge* (`MaxWalkStepZ`) apart: a mountain built of legal single steps is a wall you can stroll up.
- **Ridges and calderas** blend into the ground they land on, so a shape has no seam by construction. The
  caldera gained an explicit `CliffWall` because a smooth cone turned out to be *walkable* — measured, not
  deduced.
- **Roads**, driven to three revisions by the owner: one centreline and one baker for freehand and routed
  alike; routing that prices each step's rise by its **square** and earthwork at 25 a Z, so it takes the
  gentlest line and accepts being longer (*"come una vera strada di montagna"*); and the routed line
  rounded off, because a grid path at an angle **is** a staircase (107 direction changes in 272 cells).
  How much to round is the ground's call — `RoadPlanner` keeps the smoothest line that costs the road
  nothing.
- The baker stopped blaming a road for terrain it was laid across: it measured the *absolute* step at the
  edge of its earthwork, so a flat road on flat ground was refused for a 131 Z escarpment seventy cells
  away. It now measures the delta.

Measured on the authored massif, 116 Z over 260 cells: **272 cells instead of 262, steepest steps 6
instead of 55, no earthwork at all**; turning 369° → 206° after rounding, shoulder 140 cells → 26. The
baked road is walkable by `StepRule` from foot to summit, **984/984 paved cells reachable**, 0 seams.

## Screenshots (editor half)

Driven through the editor's own automation channel on a **copy** of the authored world:

```
ui off · view 2d · goto 7449 4230 · zoom 2.6 · overlay slope
route road_dirt 3 96 7449 4356 7449 4096
screenshot <path>
```

Instradata contro linea dritta — verde = pendenza sostenibile, giallo = gradino, rosso = muro:

![routed vs straight](https://git.homelab.devncode.it/attachments/ec8e1ffe-b975-40d3-9ed0-1c93136414e3)

Prima e dopo l'arrotondamento, stesso tratto ingrandito 3x:

![before and after](https://git.homelab.devncode.it/attachments/1b977346-7092-4144-920b-eefc153f02c7)

A 7 px/cella, mentre infila la sella fra due masse invalicabili:

![close up](https://git.homelab.devncode.it/attachments/3c0c0c89-6416-4d09-a738-a42885a79041)

## Gates, on this exact head

CSharpier clean (608 files), `dotnet test` green — **1,200 tests across 10 projects** — whole solution
builds with zero warnings. 192 of those tests are the editor's, all pure, including the road regressions
pinned from real failures: a wide road climbing a hill is not a conflict with itself; a road beside a
natural cliff is not blamed for the cliff; a gentle detour beats a steep short line; a road over walkable
ground moves no earth at all.

Reviewed-on: #284
2026-08-08 15:49:28 +02:00
.claude/skills docs(workflow): add a Verification plan to critical-design-review (#166) 2026-07-23 19:22:42 +02:00
.forgejo chore(deps): update https://github.com/docker/setup-buildx-action action to v4 (#210) 2026-07-25 23:21:16 +02:00
.vscode feat(scripting): Lua (MoonSharp) content-scripting layer — migrate rat, sword, bow (#193) 2026-07-24 18:56:18 +02:00
art feat(client): animated terrain fluids + viewport-windowed ground blend on a 3x world (#222) 2026-07-31 10:04:52 +02:00
client feat: cut the terrain structurally, and judge authored terrain with the game's own rule (#284) 2026-08-08 15:49:28 +02:00
content feat: cut the terrain structurally, and judge authored terrain with the game's own rule (#284) 2026-08-08 15:49:28 +02:00
docs feat: cut the terrain structurally, and judge authored terrain with the game's own rule (#284) 2026-08-08 15:49:28 +02:00
scripts feat(client): terrain Z rendering, foot points, height-aware picking + fixture (Slice B of #224) (#227) 2026-07-31 20:14:39 +02:00
spikes fix(client): copertura del terreno consapevole della quota, ricostruita in background (#274) 2026-08-06 12:55:59 +02:00
src feat: cut the terrain structurally, and judge authored terrain with the game's own rule (#284) 2026-08-08 15:49:28 +02:00
tests feat: cut the terrain structurally, and judge authored terrain with the game's own rule (#284) 2026-08-08 15:49:28 +02:00
tools feat: cut the terrain structurally, and judge authored terrain with the game's own rule (#284) 2026-08-08 15:49:28 +02:00
.csharpierignore build: CSharpier + Roslynator/Sonar analyzers with a phased .editorconfig policy 2026-07-17 12:44:09 +02:00
.dockerignore chore(deploy): containerize services + Forgejo release pipeline (#33) (#98) 2026-07-19 20:25:42 +02:00
.editorconfig build: CSharpier + Roslynator/Sonar analyzers with a phased .editorconfig policy 2026-07-17 12:44:09 +02:00
.gitattributes feat(shared): terrain as a column of solid intervals, and movement onto a surface (#283) 2026-08-08 15:35:57 +02:00
.gitignore build(combat): numbers foundation — formulas→Lua hot-reload + CombatSim + a CI band (#217) 2026-08-02 00:41:31 +02:00
.luacheckrc feat(scripting): typed layer/skill constants + auto-derived kind; fix GameServer image build (#212) 2026-07-25 23:23:28 +02:00
CLAUDE.md feat: cut the terrain structurally, and judge authored terrain with the game's own rule (#284) 2026-08-08 15:49:28 +02:00
CONTRIBUTING.md docs: add CONTRIBUTING (#22) 2026-07-17 13:08:51 +02:00
Directory.Build.props chore(deps): update dependency sonaranalyzer.csharp to 10.31.0.145097 (#220) 2026-08-02 01:15:37 +02:00
dotnet-tools.json feat(client): animated terrain fluids + viewport-windowed ground blend on a 3x world (#222) 2026-07-31 10:04:52 +02:00
IsoMmo.slnLaunch.user build: zero-warning policy and one-command dev startup 2026-07-16 21:55:44 +02:00
IsoMmo.slnx build(combat): numbers foundation — formulas→Lua hot-reload + CombatSim + a CI band (#217) 2026-08-02 00:41:31 +02:00
justfile build(combat): numbers foundation — formulas→Lua hot-reload + CombatSim + a CI band (#217) 2026-08-02 00:41:31 +02:00
README.md chore(auth): drop the Postgres provider, run SQLite only (#44) (#45) 2026-07-18 17:02:16 +02:00
renovate.json fix(ci): pin actions/cache to v4 to unblock Forgejo builds (#213) 2026-07-25 23:12:46 +02:00

IsoMmo

A small isometric MMO in the spirit of Ultima Online. .NET 10 on the server, MonoGame (DesktopGL) on the client — .NET all the way down, no game engine or editor to install.

Log in from the client (accounts are created silently on first login), pick a character name once (min 3 chars, unique in the world), then walk an endless green field in eight directions and see everyone else doing the same — rendered with the legacy UO human animations.

Layout

src/IsoMmo.Shared            protocol contracts, grid math (shared by server and client)
src/IsoMmo.Auth              accounts + characters, issues JWTs          http://localhost:5080
src/IsoMmo.GameServer        authoritative world, WebSocket /ws          http://localhost:5100
client/IsoMmo.Client         MonoGame (DesktopGL) C# client (.NET 10)
client/IsoMmo.Client.Core    engine-independent client logic (unit-tested)
tools/IsoMmo.AssetExtractor  exports UO human art from a legacy client
tests/                       xUnit (unit + integration)

Getting started (development)

Prerequisites: the .NET 10 SDK. Optionally just as a task runner, and a legacy UO client install for the sprites (only anim.mul/anim.idx are read). There is no engine or editor: the client is a plain MonoGame app run with dotnet run, and its native dependencies (SDL2/OpenAL) come from NuGet.

With just:

just test                    # build + run all tests
just extract "D:\Games\UO"   # extract human sprites once (output is git-ignored)
just dev                     # Auth + GameServer + client (Ctrl+C stops everything)
just client                  # just the client (servers already running)

Plain dotnet (no just):

dotnet test
dotnet run --project src/IsoMmo.Auth         # :5080
dotnet run --project src/IsoMmo.GameServer   # :5100
dotnet run --project client/IsoMmo.Client
# servers-only helper: ./scripts/dev.sh   (add --game to also launch the client)

In the client: on the login screen enter any username + a password of at least 8 characters — the account is created on the fly. Pick a character name, then hold the right mouse button to walk toward the cursor in eight directions; the camera follows you. Press Enter to chat (Enter sends, Esc cancels) — messages appear above the speaker's head. Launch a second client with a different username to see other players move and talk in real time.

Without extracted art the client falls back to placeholder diamonds and logs a warning. A bundled font (Roboto, Apache-2.0) and the sprite PNGs are loaded at runtime — there is no MonoGame content-pipeline build step.

The auth database is a local SQLite file (isommo-auth.db), created and kept up to date by EF migrations at startup; delete it to start fresh. Schema changes need one migration — see CLAUDE.md (or just migrate-auth <Name>).

Multi-platform

The server (Auth + GameServer) runs on Windows, macOS, and Linux; the client runs on Windows and macOS (Linux is a bonus). It is pure .NET / MonoGame DesktopGL — a single build runs everywhere, launched with dotnet run (no code signing needed for development). Extract the UO art once on any machine and copy client/IsoMmo.Client/assets/ across by hand; it is deliberately not in git.

Development rules

  • Definition of done — a feature is complete if and only if: dotnet test is fully green, the whole solution builds (client and tools included), and there is not a single warning (warnings are errors via Directory.Build.props).
  • Authoritative server. Clients send intents, never state. Movement rules are enforced server-side; the client only renders.
  • Protocol lives in IsoMmo.Shared only. JSON with a type discriminator, camelCase everywhere, enums as strings.
  • Tests are not optional: unit tests for game logic, WebApplicationFactory integration tests for HTTP and WebSocket flows. Coverage >85% per project.
  • Conventional Commits, one commit per feature.
  • Extracted UO art never enters git (client/IsoMmo.Client/assets/ is ignored). The repo must always work without it.

See CLAUDE.md for the full invariants list.