Co-op rogue-lite logistics game (Arcade v1).
Implementation specs are in spec/.
PerformativeMail.sln holds Sim, Server, Client, App, ContentValidator, BotClient, Sim.Tests, and Net.Tests.
Sim targets netstandard2.1 and has no Godot references. Server and Client are net8.0 class libraries that reference Sim. App is a net8.0 class library that boots a listen-server pair over LoopbackTransport. The Godot 4.7.2 C# shell lives in game/ and is not in the solution, so dotnet test never loads GodotSharp.
Godot 4.7.2 .NET. Friend A hosts; Friend B joins by LAN IP. Default port is UDP 7777. Allow inbound UDP 7777 on the host. No NAT traversal in this slice.
godot --path game- Friend A clicks Host. Host/Join chrome hides; Esc pause shows the LAN join address.
- Friend B types that address (
192.168.1.20or192.168.1.20:7777) and clicks Join. - First-person WASD + mouse look. Shift sprints. E interacts. Esc opens Leave / Controls / Options.
- The live world labels the Post Office, Mail intake, house addresses, and mailboxes.
export PATH=$HOME/.dotnet:$PATH
dotnet test PerformativeMail.slnPM Playtest / routines: bash tools/playtest/run.sh (headless). Optional --gui / --gui-only on DISPLAY (default :19). Check IDs and report.json are in tools/playtest/README.md.
Godot integration is tested in GitHub Actions inside barichello/godot-ci:mono-4.7.2 (official Godot 4.7.2 .NET / C# editor). The godot job runs tools/godot/ci.sh once per command.
verifychecks Godot 4.7.2 .NET,godot --headless --quit, anddotnet8.x.importimportsgame/and runsdotnet build.bootboots the main scene headless and asserts the C#_Readymarker.hudbinds HudFrame and reads Control text.overlayopens InventoryOverlay and reads cell text.mapopens the Map Control fromWorldTablesand reads district/street layers, filter chips, and a placed ping.lobbybinds LobbyFrame and reads Control text.overlaysbinds payday, draft, and results frames and reads Control text.shopbinds ShopFrame fromcontent/shop/*prices and reads Control text.debugopens DebugMenu and reads inspect and cheat labels.buildbinds the build-mode bar and reads category, selected building, and ghost reason text.joinruns a two-process LAN host and join on127.0.0.1:7777.playruns a solo Host play report with the golden worldHash, HUD, and world entity counts listed intools/godot/report.md.debug-worldruns a solo Host play report with--debug-world. It asserts Playing, PREP, two houses, and debug worldHash0x4CF184F2FA4D4EEE.debug-helpersruns--host --debug-world --debug-helper=intakeand asserts the local pawn is at Intake (1100,500). DebugMenu (F3 or backtick) buttons also teleport to Intake or a mailbox, give mail if the hotbar is empty, and open the inventory overlay.worldstageruns--host --debug-world --report= --world-dump=and asserts live WorldStage Label3D text for Post Office, Mail, and the debug addresses, plus ConstructStage labels for the seeded wall/chest/belts/sorter and SmokeReportconstructCounts.packed-beltsruns--host --packed-belts --report= --frame-dump=. It asserts Playing, SmokeReportconstructCounts.beltsis400, and the FRAME_DUMP sidecar hasinstancesat least 400,pass=true, andavgMsat most 16.7.interactruns--host --debug-world --debug-helper=interact. It stocks Intake, teleports, holds Interact through pickup and mailbox deliver, and asserts SmokeReportwalletis8.live-overlayruns--host --debug-world --debug-helper=live-overlaywith--overlay-dump=. It picks up Intake mail, opens the inventory overlay on the Playing replica, and asserts live hotbar cell text, not the OverlayBootReplica placeholder.live-hudruns--host --debug-world --report= --hud-dump=. It asserts HUD Control text came from Playing /HudSnapshot(phase PREP, shift, timer matching the replica), notHudBoot.Placeholder.live-mapruns--host --debug-world --debug-helper=mapwith--map-dump=. It opens the Map Control on the PlayingWorldTables(M path) and asserts district/street layers plus filter chips from Debug Lane, not MapBoot placeholder streets.live-shopruns--host --debug-world --debug-helper=shopwith--shop-dump=. It credits the wallet, buysbandage_x3throughShopSession, opens the shop UI, and asserts catalog prices plus wallet$9.20.live-buildruns--host --debug-world --debug-helper=buildwith--build-dump=. It enters build mode, placeswall_woodintoConstructRegistry, and asserts the live dump hasconstructs=1.leaveruns--host --debug-world --debug-helper=leave. It opens pause (OpenPause, same path as Esc), activates Leave then confirm, and asserts SmokeReportstateisMenu. Host/Join chrome is shown again on the Menu render.
all runs every command above. all is the default when you omit the argument. The command list and the steps to add a Control-text smoke are in tools/godot/README.md.
# Same script CI runs (skip the 8.x pin if the image only has SDK 9):
docker run --rm -e REQUIRE_DOTNET_8=0 -v "$PWD":/src -w /src \
barichello/godot-ci:mono-4.7.2 bash tools/godot/ci.shFrom the repo root, ContentValidator loads the M0 test map plus streets.json, world/archetypes.json, balance.json, unlocks.json, and the item, mail, building, recipe, shop, perk, and stamp defs. Unknown ids, unknown Stat keys, unknown RuleFlag values, and district-house sums outside the grown town band fail the process. A recipe whose produces.building does not name a loaded building also fails.
A new content/items/*.json row appears in the host F3 spawn list with no menu code change. A new ContentBundle property needs a spawn projection or an explicit Deferred reason, or DebugSpawnCoverage.RequireComplete and ContentValidator fail. See docs/debug-spawns.md.
dotnet run --project tools/ContentValidator -- content