Skip to content

Map render rework: correct projection & grid, RustMaps #info map, per-server grid style - #45

Merged
HandyS11 merged 40 commits into
developfrom
feat/map-render-rework
Jul 10, 2026
Merged

Map render rework: correct projection & grid, RustMaps #info map, per-server grid style#45
HandyS11 merged 40 commits into
developfrom
feat/map-render-rework

Conversation

@HandyS11

@HandyS11 HandyS11 commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Summary

Full rework of the Map feature (38 commits): fixes every reported render defect (projection, grid, icons, rotation, movement tracking), adds a static RustMaps map to #info, and introduces a per-server grid style option after discovering the in-game map and Rust+/RustMaps use two different grid conventions.

1 — Render correctness (slice 1)

  • Canonical world→pixel projection (MapProjection): the official-app/rustplusplus transform, with WorldSize plumbed through MapDimensions + a GetWorldAsync seam (was previously image-pixel based → everything misplaced).
  • Shared grid math (MapGrid) used by both the renderer and event grid references — no more drift between the drawn grid and !cargo-style callouts.
  • Size-scaled icons (MapRenderStyle + scaled icon cache) replacing native-size PNG pastes (the giant-icon bug).
  • Marker rotation wired via RustPlusApi 2.0.0-beta.4 — cargo/heli/chinook icons now face their heading like the official app.
  • Movement tracking: Moved bucket in marker deltas + 6-deep position history → fading motion trails behind moving markers.
  • Base-map source chain with per-image projection metadata (BaseMapImage), RustMapsApi bumped to 1.0.0-beta.2 (tolerant MonumentType deserialization).
  • tools/RustPlusBot.MapParity: dev CLI that renders our real grid/projection over a RustMaps ground-truth render for visual parity checks (GET-only, no generation credits).

2 — #info static RustMaps map (slice 2)

A high-quality RustMaps monument render shown as a static, top-of-channel message in #info:

  • Credit-safe generation pipeline: per-(size, seed) coordinator state machine + one-shot driver — CreateMap only on genuine NotFound, limits checked fail-closed, generation advanced strictly sequentially so a credit can never be double-spent.
  • Uses the imageIconUrl render (the one with monument icons — verified empirically against the RustMaps CDN).
  • Delivered as a reconciled Workspace message with a DB-persisted message id → survives restarts, edits in place, never re-posts.
  • Channel-order repair in the reconciler: the map message claims the slot above the status embed even in guilds that already had one (new gateway DeleteMessageAsync + spec-order re-materialization).
  • No boot flicker: renderer returns an empty payload until the render is ready, so the existing image is left untouched across restarts.
  • Registration runs from the connection store every tick (not just the live-only connect event), so generation starts even when the event is missed at startup.

3 — The grid: partial cell + two real-world conventions

  • Partial edge cell restored: a 1500 world is 11×11 (A–K, 0–10)getCorrectedMapSize-style snapping deleted the 11th partial row/column. CellCount = ceil(worldSize / 146.25).
  • Anchoring measured, not guessed — from RustMaps' own pre-rendered grid tile pyramid (content.rustmaps.com/grids/{size}/…, Leaflet CRS.Simple) across sizes 1500–4500:
    • In-game (F1) map: rows anchored at the world's north edge (live-verified).
    • Rust+ app / rustmaps.com: rows anchored exactly 100 game-units south of it — a constant, not size-dependent.
    • Columns are west-anchored in both (identical).
  • Per-server MapGridStyle option (default: In-game) governing both the rendered grid and every event grid reference — event embeds, in-game team-chat callouts, player transitions, and !cargo/!heli/!chinook/!events:
    • Two picker buttons on the #map control message (ManageGuild), active style highlighted, with a help line explaining the difference.
    • ServerMapSettings.GridStyle + MapGridStyle EF migration; style flows through MapComposerMapRenderer and GridReference.From(...).
  • Grid clipped to the labelled block (A0 → last cell) — no lattice across the ocean margin.
  • Labels stay at each cell's top-left corner (companion-app placement).

4 — Small oil rig fixed

The protobuf token is oil_rig_small, not oilrig_1 (confirmed vs rustplusplus). With the wrong token the small rig matched nowhere: no icon, no rig placement, no CH47-proximity activation. Fixed in the icon map, composer, and connection-supervisor rig detection.

Verification

  • Build -warnaserror: 0/0 · jb cleanupcode clean · EF migration included.
  • Full suite: 17 projects, 916 tests, 0 failed (1 intentionally skipped ground-truth test).
  • Grid geometry verified pixel-exact against RustMaps' own grid tiles and blended over ground-truth terrain renders (1500/1234 and 4500/12345); rig token verified against rustplusplus.
  • Live-verified on a real server: render matches the in-game F1 map; RustPlus style matches rustmaps.com; #info map static across reboots.

Notes for reviewers

  • MapGrid.RustPlusRowInset = 100f is an empirical constant measured from RustMaps' tiles (their per-size cell sizes drift 146.25/150/152 by render era on their CDN; we deliberately keep 146.25 everywhere).
  • Existing servers keep the in-game style until someone presses the new button (migration default 0).

🤖 Generated with Claude Code

HandyS11 and others added 30 commits July 7, 2026 22:05
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Generalize base-map fetch into a source chain (IBaseMapSource, tried in
registration order) so a future RustMaps source can slot ahead of the
Rust+ JPEG fallback. BaseMapImage carries the projection metadata of the
specific fetched image (pixel width/height + ocean margin); BaseMapCache
now caches BaseMapImage per (guild, server) instead of raw bytes, trying
each source on a miss and not caching null. MapComposer builds its
MapProjection from the fetched image's own metadata instead of
MapDimensions, fulfilling the Task 5 interim note.
…; 30s refresh

Adds RustMapsBaseMapSource ahead of the Rust+ JPEG fallback in the base-map
source chain: fetches the RustMaps procedural render (clean terrain, no baked
icons) by world size + seed, active only when Map:RustMaps:ApiKey is
configured. Any failure (no key, unpublished map, HTTP error) falls through
to the existing Rust+ source. Also bumps the default map refresh interval
45s -> 30s.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dev-only CLI (tools/RustPlusBot.MapParity) fetches a RustMaps render + monument
list, projects each monument through our MapProjection, and draws crosshairs for
eyeball diffing against RustMaps' own icon placement. RustMapsParityTests reads
a committed fixture and asserts pixel-cell == world-cell for every monument;
skips until the user generates and commits a fixture with their API key.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…edMapSize)

Snap the world size to a whole multiple of 146.25 before any grid math
(MapGrid.CorrectedWorldSize, ported from rustplusplus getCorrectedMapSize),
so the north-edge row and east-edge column are no longer a smaller partial
cell. CellCount and LabelFor route through the corrected size; DrawGrid's
line/label boundaries do too, with a small epsilon nudge on the label Y
probe so it agrees with LabelFor's row binning (see grid-fix-report.md for
the trace).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tType enum)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extracts a shared private ComposeWithLayersAsync(guild, server, MapLayerSet,
ct) core from ComposeAsync; ComposeAsync now reads the per-server toggle
settings then delegates to it, and the new ComposeStaticAsync calls it with
a fixed grid+monuments-only layer set for the #info static fallback render.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…, one-shot)

Advances one (size,seed) map key per tick: GET -> Ready if ImageUrl present,
else pre-check GetLimitsAsync (fail closed on error) before a single
CreateMapAsync, then poll GetMapById until ready and download the image.
CreateMapAsync is guaranteed to fire at most once per key. Poll responses
that aren't a clean success are null-tolerant so a transient miss retries
next tick instead of tripping the key to Failed.
Add Poll_still_generating_stays_generating_without_spending, which
stubs a real Queued/409 GetMapByIdAsync failure (not NSubstitute's
default null) and asserts the key stays Generating with no
CreateMapAsync call — pinning the PollAsync null-tolerance fix.

Extract IsReady(Result<MapInfo>?) and use it at both the StartAsync
and PollAsync "is the GET ready" checks, replacing two divergent
idioms with one null-tolerant predicate. No behavior change.
InfoMapHostedService owns the #info surface: tracks connected servers via
ConnectionStatusChangedEvent, drives RustMapsGenerationDriver.AdvanceAsync
strictly sequentially (one key at a time, single tick loop — never
concurrent, since the driver's check-then-spend path is not atomic across
awaits), and posts via IInfoMapPoster (RustMaps bytes when Ready, else the
grid+monuments MapComposer.ComposeStaticAsync fallback). Posts only on
state change (None→Fallback→Ready) to avoid per-tick repost churn.

Registers the RustMaps coordinator/driver/poster/hosted-service in AddMap
when Map:RustMaps:ApiKey is configured. Adds the map.info.* EN/FR strings
and swept a pre-existing jb formatting drift in DiscordInfoMapPoster.
…ed #info channel

StartAsync fell through to GetLimits/CreateMap on any non-Queued GET result
(Transport, RateLimited, Forbidden, Validation, Unknown), wrongly spending a
credit on transient GET failures instead of only a genuine NotFound. Gate
added right after the Queued check; non-NotFound now bails, leaving the key
Idle so the free GET retries next tick.

EnsureInfoMapAsync registered the (size,seed) key with the coordinator before
confirming the #info channel exists, letting a connected server with no
provisioned #info channel enroll a pending key the tick loop could still
spend a credit generating. Channel resolution now happens before Register.
The connection-status loop and the periodic tick loop both call EnsureInfoMapAsync
for the same server. On connect one composes+posts the fallback while the other
detects the RustMaps render is ready and posts it; both delete-prior-image scans
run before either message commits, so neither deletes the other and two #info map
messages survive. Serialize EnsureInfoMapAsync with a per-server SemaphoreSlim so
the fallback fully posts before the RustMaps post runs its delete.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… render

Two live-run fixes for the #info map:

1. Edit in place, never duplicate. The map is now one tracked message: the
   poster edits it (embed + attachment swapped via ModifyAsync) instead of
   delete+reposting, so the channel never shows two maps. On first post (or if
   the message was deleted / an edit fails) it sweeps stray prior posts and
   posts one fresh, tracking its id. Combined with the per-server serialization,
   the fallback->RustMaps transition is a clean in-place swap.

2. Download imageIconUrl (map_icons.png) instead of imageUrl
   (map_raw_normalized.png). Verified against the live API: imageUrl is plain
   terrain with no markers; imageIconUrl carries the monument icons.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…istent, iconned)

Replaces the home-grown #info map poster (in-memory message-id tracking, new image
posted on every restart) with a reconciled Workspace message: DB-persisted id via
ProvisionedMessage, edited in place across restarts, declared first so it renders
above the #info status embed.

- Abstractions: IInfoMapReadModel/InfoMapView read seam + InfoMapReadyEvent, so
  Features.Workspace (which cannot reference Features.Map) can read RustMaps
  generation state and Features.Map can signal readiness across the boundary.
- Features.Map: the coordinator now also implements IInfoMapReadModel (same
  singleton instance as IRustMapsMapCoordinator); the driver stores the RustMaps
  imageIconUrl directly instead of downloading bytes; InfoMapHostedService drops
  all posting/composing and just drives generation + publishes InfoMapReadyEvent
  once per (size, seed). Poster, #info channel locator, and the MapComposer byte
  fallback (ComposeStaticAsync) are deleted.
- Features.Workspace: new ServerInfoMapMessageRenderer shows the RustMaps render
  via WithImageUrl (or a "preparing" footer) and always returns a non-empty embed
  so it claims the top slot on the first reconcile; WorkspaceHostedService gains
  an InfoMapReadyEvent consumer that reconciles the owning server.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…status embed)

EnsureMessagesAsync only edited-in-place or posted-if-missing per spec, never
reordering, so on an already-provisioned guild a newly-declared earlier message
(server.info.map) posted after an existing later one (server.info) since Discord
orders by creation time. Group specs by channel, detect a to-post message that
precedes an already-live one, and delete+repost the trailing live messages so
they re-materialize fresh, below it, in declaration order. In-order and
single-message channels are unaffected (still edit-in-place).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…h tick

The RustMaps generation was registered only from ConnectionStatusChangedEvent,
which the in-process bus delivers live-only (no replay). Because InfoMapHostedService
is registered after Connections in Program.cs, the connection can publish the connect
event before this service has subscribed, so it is missed — the (size,seed) key is never
registered, PendingKeys() stays empty, generation never runs, and #info shows the
'preparing' placeholder forever. Register connected servers from IConnectionStore each
tick (GetWorldAsync returns null for non-connected), independent of the event. Adds a
regression test that generates with no connect event ever published.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
HandyS11 and others added 8 commits July 9, 2026 21:20
The RustMaps "ready" state lives only in memory (RustMapsMapCoordinator),
so on every restart it is empty until generation re-completes ~20s later.
The renderer previously returned a "preparing…" placeholder embed while not
ready, which overwrote the already-posted image on each boot and swapped it
back once generation finished — the visible flicker/refresh.

Return an EMPTY payload until the render is ready instead. The reconciler
skips empty payloads, so the existing map message (with its last image) is
left untouched across restarts. Positioning above the status embed is still
handled by the reconciler's channel-order repair when the map first becomes
non-empty, so the message no longer needs to be non-empty on first reconcile.

Removes the now-dead map.info.generating string (EN/FR) and updates the
renderer + localization parity tests accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…g_small)

The Rust+ protobuf token for the small oil rig is `oil_rig_small` (confirmed
against rustplusplus Map.js/MapMarkers.js and rustplus-desktop), not
`oilrig_1`. With the wrong token the small rig never matched anywhere, so:
its icon never drew, no RigPlacement was created, and its CH47-proximity
activation was never detected.

Fixes the token in all three production sites that switch on it — the icon
map, the map-composer rig placement, and the connection-supervisor rig-state
detection — plus the doc-comment examples and the tests that used it.
The large oil rig token (`large_oil_rig`) was already correct.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gh")

Grid labels were anchored at each cell's top-left corner. Because text flows
right and down from its origin, column letters read as roughly in-column
(fine) but row numbers hugged the cell's top edge, appearing half a row too
high versus the in-game map — which centres its labels.

Probe the cell centre ((col+0.5, row+0.5) cells) and centre the text on it in
both axes, matching the in-game grid. Drops the now-unneeded LabelRowEpsilon
boundary nudge (centre placement is always safely inside the cell).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reverts the label-centring change (507eef6): the top-left corner placement
was the intended one and should not have been touched. The real grid
displacement is a separate, still-open issue tracked below.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The grid dropped a row and a column versus RustMaps and the in-game map: a
1500 world shows 11×11 (A–K, rows 0–10), but we drew 10×10 (A–J, 0–9). A
prior change snapped the world size to a whole multiple of the 146.25 cell
size (rustplusplus getCorrectedMapSize), which *deletes* the partial edge
cell — the "displacement" the user reported.

Rust, the Rust+ companion app and RustMaps all keep that partial edge cell as
a (narrower) labelled cell. So:
- MapGrid.CellCount is now ceil(worldSize / CellSize) — the whole world,
  partial edge cell included — and CorrectedWorldSize is removed.
- MapGrid.LabelFor bins straight off the raw world size from the SW corner
  (the actual Rust grid formula), so grid references match the in-game grid.
- MapRenderer.DrawGrid covers [0, worldSize], clamping the final boundary to
  worldSize so the last cell is the partial one; labels stay at the cell
  top-left corner. Drops the now-unneeded LabelRowEpsilon nudge.

Verified against RustMaps 1500/1234: 11×11, and monuments land in the same
cells RustMaps shows (small oil rig F5, lighthouse I3, substation I2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ver the whole image

The previous fix kept the partial edge cell but anchored rows at the SOUTH
edge, leaving the remainder as a squished sliver row at the top — rows sat
visibly displaced versus RustMaps and the companion app (columns were fine
because they were already west-anchored).

Rust, the app and RustMaps anchor the grid at the world's NW corner: rows
step south from the top and the partial cell sits at the south/east, bleeding
past the world edge so every visible cell looks full-size. They also draw the
grid lattice across the WHOLE map image (ocean margin included), not clipped
to the world square.

- MapRenderer.DrawGrid: lattice anchored at the NW world corner, lines
  spanning the full image in all directions; labels (A0..) only for the
  world's ceil-count cells, top-left corner placement unchanged.
- MapGrid.LabelFor: rows bin from the north edge (floor((worldSize - y) /
  CellSize)) so grid references match the visible grid; columns unchanged.
- Regression tests lock the north anchoring (1500 world: y=1360 → A0,
  y=10 → partial row A10).

Verified against RustMaps 1500/1234 (tmp/images/image.png) and the
companion-app geometry with an ocean-margin base tile.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t row offset)

The in-game (F1) map and the Rust+ companion app / rustmaps.com use DIFFERENT
grid conventions: both anchor columns at the world's west edge, but
Rust+/RustMaps rows start exactly 100 game-units south of the world's north
edge (measured from RustMaps' own pre-rendered grid tiles across sizes
1500–4500), while the in-game map starts them at the edge itself. That's why
the render could never match both references at once.

Add a per-server MapGridStyle option (default: InGame) governing BOTH the
rendered #map grid and every event grid reference, so callouts read
consistently with whichever map the team uses:
- InGame — matches the F1 map; event callouts read like in-game grid refs.
- RustPlus — matches the companion app and rustmaps.com.

Plumbing:
- MapGridStyle enum + MapGrid.RowInset/LabelFor(style); MapRenderer.DrawGrid
  anchors the lattice at (worldSize − inset).
- ServerMapSettings.GridStyle + MapGridStyle EF migration; MapLayerSettings
  carries it; IMapSettingsStore.SetGridStyleAsync.
- #map control message gains a third row with the two style buttons (active
  = Primary) and a help line explaining the difference; new
  workspace:map:gridstyle:* component handled by MapComponentModule
  (ManageGuild, reconcile + repaint like the layer toggles).
- Style threads through EventRelay/EventEmbedRenderer, PlayerEventRelay/
  PlayerEventRenderer, and the !cargo/!heli/!chinook/!events handlers via
  GridReference.From(x, y, dims, style).
- Localization: map.gridstyle.{ingame,rustplus,help} EN/FR.

Full suite green (17 projects, 916 tests, 1 skipped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Draw the grid lattice only over the labelled cells (A0 through the last
ceil-count cell) instead of extending the lines across the entire image and
its ocean margin. The anchoring (per grid style) and label placement are
unchanged; the partial edge cell still bleeds past the world's south/east
edge inside the block, so every labelled cell renders full-size.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 9, 2026 22:12

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR performs a large-scale rework of the bot’s Map feature to make world→pixel projection and grid references correct and consistent across rendering, event embeds, and command outputs, while also adding a RustMaps-backed static map message in #info and introducing a per-server grid convention setting.

Changes:

  • Replaces the prior image-based mapping with a canonical MapProjection and shared MapGrid math (including partial edge cell support and configurable row anchoring).
  • Improves rendering fidelity (scaled icon rendering, marker rotation, movement trails) and aligns event/command grid references with the selected grid style.
  • Adds RustMaps integration for a reconciled, persistent #info map message plus supporting generation coordination/driver, and extends the workspace reconciler to repair within-channel message ordering.

Reviewed changes

Copilot reviewed 118 out of 119 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tools/RustPlusBot.MapParity/RustPlusBot.MapParity.csproj Adds a dev CLI tool project for projection/grid parity checks.
tools/RustPlusBot.MapParity/README.md Documents parity tool purpose and how to generate fixtures for tests.
tools/RustPlusBot.MapParity/Program.cs Implements the parity CLI that overlays projected monument points onto RustMaps renders.
tests/RustPlusBot.Persistence.Tests/Map/MapSettingsStoreTests.cs Adds tests for default grid style and grid style persistence.
tests/RustPlusBot.Localization.Tests/StringsResourceParityTests.cs Updates expected resource key count after adding new strings.
tests/RustPlusBot.Features.Workspace.Tests/Reconciler/WorkspaceReconcilerMessageTests.cs Adds tests for channel-order repair behavior (delete/repost sequencing).
tests/RustPlusBot.Features.Workspace.Tests/Reconciler/ReconcilerHarness.cs Extends test harness with simple message spec/renderer helpers.
tests/RustPlusBot.Features.Workspace.Tests/Messages/ServerInfoMapMessageRendererTests.cs Adds tests for the #info static map message rendering behavior.
tests/RustPlusBot.Features.Workspace.Tests/Messages/MapControlMessageRendererTests.cs Updates tests for grid style buttons and toggle filtering.
tests/RustPlusBot.Features.Workspace.Tests/Fakes/FakeWorkspaceGateway.cs Adds fake gateway tracking for deleted IDs and posted payloads; implements DeleteMessageAsync.
tests/RustPlusBot.Features.Players.Tests/PlayerEventRendererTests.cs Updates for new MapDimensions shape (includes WorldSize).
tests/RustPlusBot.Features.Players.Tests/PlayerEventRelayTests.cs Wires IMapSettingsStore into player event relaying tests for grid style.
tests/RustPlusBot.Features.Players.Tests/PlayerEventRegistrationTests.cs Ensures DI registers required map settings dependency in player feature tests.
tests/RustPlusBot.Features.Players.Tests/PlayerEventEndToEndTests.cs Updates for new map dimensions signature.
tests/RustPlusBot.Features.Players.Tests/Hosting/PlayersHostedServiceTests.cs Updates for new map dimensions signature and DI requirements.
tests/RustPlusBot.Features.Map.Tests/WorldToPixelTests.cs Removes obsolete tests after replacing WorldToPixel with MapProjection.
tests/RustPlusBot.Features.Map.Tests/RustPlusBot.Features.Map.Tests.csproj Adds skippable facts package and copies fixture content to output.
tests/RustPlusBot.Features.Map.Tests/RustPlusBaseMapSourceTests.cs Adds tests for Rust+ base map source behavior (bytes + dimensions).
tests/RustPlusBot.Features.Map.Tests/RustMapsParityTests.cs Adds fixture-based parity test for projection/grid consistency.
tests/RustPlusBot.Features.Map.Tests/RustMaps/RustMapsMapCoordinatorTests.cs Tests for coordinator lifecycle, readiness, and idempotent registration.
tests/RustPlusBot.Features.Map.Tests/RustMaps/RustMapsGenerationDriverTests.cs Tests for credit-safe RustMaps generation driver and ready URL selection.
tests/RustPlusBot.Features.Map.Tests/MapRendererTests.cs Expands renderer tests for scaling, trails, rotation, and grid style diffs.
tests/RustPlusBot.Features.Map.Tests/MapRegistrationTests.cs Updates DI registration tests (configuration-driven RustMaps integration).
tests/RustPlusBot.Features.Map.Tests/MapProjectionTests.cs Adds unit tests covering canonical projection behavior.
tests/RustPlusBot.Features.Map.Tests/MapOptionsTests.cs Adds tests for MapOptions defaults including RustMaps polling interval.
tests/RustPlusBot.Features.Map.Tests/MapIconsTests.cs Updates icon token mapping and adds tests for sized icon caching/scaling.
tests/RustPlusBot.Features.Map.Tests/MapComposerTests.cs Updates composer tests for new base map source chain and trail forwarding.
tests/RustPlusBot.Features.Map.Tests/Hosting/InfoMapHostedServiceTests.cs Adds tests ensuring generation registration works via tick and connect paths.
tests/RustPlusBot.Features.Map.Tests/BaseMapCacheTests.cs Updates base map cache tests for multi-source behavior and caching semantics.
tests/RustPlusBot.Features.Events.Tests/State/EventStateStoreTests.cs Adds moved-marker and capped history ring behavior tests.
tests/RustPlusBot.Features.Events.Tests/Rendering/EventEmbedRendererTests.cs Updates for new dimensions/world size and grid style support.
tests/RustPlusBot.Features.Events.Tests/Relaying/EventRelayTests.cs Wires IMapSettingsStore into event relay tests for grid style.
tests/RustPlusBot.Features.Events.Tests/Formatting/GridReferenceTests.cs Adds regression coverage for world-size-based grid reference.
tests/RustPlusBot.Features.Events.Tests/EventRegistrationTests.cs Ensures DI registers required map settings dependency in events feature tests.
tests/RustPlusBot.Features.Events.Tests/Classifying/MarkerEventClassifierTests.cs Updates marker delta event construction with moved bucket.
tests/RustPlusBot.Features.Connections.Tests/Fakes/FakeRustSocketSource.cs Adds GetWorldAsync support and updates default dimensions to include world size.
tests/RustPlusBot.Features.Connections.Tests/ConnectionSupervisorTests.cs Adds moved-marker delta test and updates small rig token.
tests/RustPlusBot.Features.Commands.Tests/Handlers/EventHandlersTests.cs Updates command handlers to use map settings for grid style.
tests/RustPlusBot.Features.Commands.Tests/CommandRegistrationTests.cs Ensures DI registers IMapSettingsStore needed by command handlers.
tests/RustPlusBot.Abstractions.Tests/Events/RigStateChangedEventTests.cs Updates test token to oil_rig_small and dimensions signature.
tests/RustPlusBot.Abstractions.Tests/Connections/MapMarkerSnapshotTests.cs Updates dimensions signature expectations.
tests/RustPlusBot.Abstractions.Tests/Connections/MapGridTests.cs Adds comprehensive tests for new shared MapGrid math and styles.
src/RustPlusBot.Persistence/Migrations/BotDbContextModelSnapshot.cs Updates EF snapshot for new ServerMapSettings.GridStyle column.
src/RustPlusBot.Persistence/Migrations/20260709213331_MapGridStyle.cs Adds EF migration adding GridStyle to ServerMapSettings.
src/RustPlusBot.Persistence/Map/MapSettingsStore.cs Persists/reads grid style and adds SetGridStyleAsync.
src/RustPlusBot.Persistence/Map/MapLayer.cs Extends MapLayerSettings with GridStyle defaulting to InGame.
src/RustPlusBot.Persistence/Map/IMapSettingsStore.cs Extends store interface with SetGridStyleAsync.
src/RustPlusBot.Localization/Strings.resx Adds strings for grid style picker and #info map embed fields.
src/RustPlusBot.Localization/Strings.fr.resx Adds French equivalents for new map/grid strings.
src/RustPlusBot.Host/Program.cs Validates RustMaps poll interval and passes configuration into AddMap.
src/RustPlusBot.Host/appsettings.json Adds RustMaps configuration section with API key + poll interval.
src/RustPlusBot.Features.Workspace/WorkspaceServiceCollectionExtensions.cs Registers ServerInfoMapMessageRenderer and related renderers.
src/RustPlusBot.Features.Workspace/WorkspaceKeys.cs Adds workspace message key for server.info.map.
src/RustPlusBot.Features.Workspace/WorkspaceComponentIds.cs Adds component prefix for grid style buttons.
src/RustPlusBot.Features.Workspace/Specs/ServerWorkspaceSpecProvider.cs Declares server.info.map before status message to enforce ordering.
src/RustPlusBot.Features.Workspace/Reconciler/WorkspaceReconciler.cs Reworks message provisioning to repair within-channel message ordering using delete/repost.
src/RustPlusBot.Features.Workspace/Modules/MapComponentModule.cs Adds interaction handler for per-server grid style selection.
src/RustPlusBot.Features.Workspace/Messages/ServerInfoMapMessageRenderer.cs Renders the RustMaps-backed static #info map embed (empty payload until ready).
src/RustPlusBot.Features.Workspace/Messages/MapControlMessageRenderer.cs Adds grid style picker buttons to #map control message.
src/RustPlusBot.Features.Workspace/Hosting/WorkspaceHostedService.cs Subscribes to InfoMapReadyEvent to reconcile the #info map when ready.
src/RustPlusBot.Features.Workspace/Gateway/IWorkspaceGateway.cs Adds DeleteMessageAsync to support order repair.
src/RustPlusBot.Features.Workspace/Gateway/DiscordWorkspaceGateway.cs Implements message deletion with NotFound tolerance.
src/RustPlusBot.Features.Players/Rendering/PlayerEventRenderer.cs Threads grid style through player event rendering and line rendering.
src/RustPlusBot.Features.Players/Relaying/PlayerEventRelay.cs Reads per-server grid style from settings for event relay outputs.
src/RustPlusBot.Features.Map/RustPlusBot.Features.Map.csproj Adds RustMapsApi package reference.
src/RustPlusBot.Features.Map/RustMaps/RustMapsReadyMap.cs Adds ready map view model (image URL + page URL).
src/RustPlusBot.Features.Map/RustMaps/RustMapsMapSnapshot.cs Adds immutable snapshot for coordinator state.
src/RustPlusBot.Features.Map/RustMaps/RustMapsMapKey.cs Adds (size, seed) key type for RustMaps generations.
src/RustPlusBot.Features.Map/RustMaps/RustMapsMapCoordinator.cs Implements thread-safe generation coordination and IInfoMapReadModel.
src/RustPlusBot.Features.Map/RustMaps/RustMapsGenerationState.cs Defines RustMaps generation lifecycle states.
src/RustPlusBot.Features.Map/RustMaps/RustMapsGenerationDriver.cs Implements credit-safe GET→(limits)→CreateMap→poll workflow.
src/RustPlusBot.Features.Map/RustMaps/IRustMapsMapCoordinator.cs Adds coordinator interface for generation tracking + requesters.
src/RustPlusBot.Features.Map/Rendering/WorldToPixel.cs Removes old projection helper in favor of MapProjection.
src/RustPlusBot.Features.Map/Rendering/MarkerPlacement.cs Extends marker placement with rotation and trail points.
src/RustPlusBot.Features.Map/Rendering/MapRenderStyle.cs Centralizes render sizing and styling constants for 1024px output.
src/RustPlusBot.Features.Map/Rendering/MapRenderer.cs Reworks renderer to use MapProjection, MapGrid, scaled icons, trails, and grid style.
src/RustPlusBot.Features.Map/Rendering/MapProjection.cs Introduces canonical world→pixel projection model.
src/RustPlusBot.Features.Map/MapServiceCollectionExtensions.cs Makes AddMap configuration-driven and registers RustMaps generation components when keyed.
src/RustPlusBot.Features.Map/MapOptions.cs Updates defaults and adds RustMaps poll configuration.
src/RustPlusBot.Features.Map/Hosting/MapHostedService.cs Updates docs to reflect moved-marker deltas and tick behavior changes.
src/RustPlusBot.Features.Map/Hosting/InfoMapHostedService.cs Adds hosted service driving RustMaps generation and publishing readiness events.
src/RustPlusBot.Features.Map/Composing/RustPlusBaseMapSource.cs Implements base map source backed by Rust+ server tile.
src/RustPlusBot.Features.Map/Composing/MapComposer.cs Uses base map sources + MapProjection; forwards marker history into trails; applies grid style.
src/RustPlusBot.Features.Map/Composing/IBaseMapSource.cs Adds base map source abstraction.
src/RustPlusBot.Features.Map/Composing/BaseMapImage.cs Adds base map bytes with per-image projection metadata.
src/RustPlusBot.Features.Map/Composing/BaseMapCache.cs Updates cache to store BaseMapImage and try sources in order.
src/RustPlusBot.Features.Map/Assets/MonumentIconMap.cs Fixes small oil rig token and icon key mapping.
src/RustPlusBot.Features.Map/Assets/MapIcons.cs Adds scaled icon caching and new APIs for size-specific icons.
src/RustPlusBot.Features.Events/State/TrailPoint.cs Adds marker trail history point record.
src/RustPlusBot.Features.Events/State/EventStateStore.cs Tracks moved markers, rotation changes, and capped position history.
src/RustPlusBot.Features.Events/State/ActiveMarker.cs Extends active marker state with history and rotation.
src/RustPlusBot.Features.Events/Rendering/EventEmbedRenderer.cs Threads grid style through grid reference formatting.
src/RustPlusBot.Features.Events/Relaying/EventRelay.cs Reads per-server grid style from settings for relay outputs.
src/RustPlusBot.Features.Events/Formatting/GridReference.cs Switches to shared MapGrid and uses world size rather than image pixels.
src/RustPlusBot.Features.Connections/Supervisor/ConnectionSupervisor.cs Adds moved-marker bucket in marker delta publishing; fixes rig token mapping; adds GetWorldAsync query.
src/RustPlusBot.Features.Connections/Listening/RustPlusSocketSource.cs Adds rotation extraction, world snapshot query, and world size plumbing into dimensions.
src/RustPlusBot.Features.Connections/Listening/IRustServerConnection.cs Adds GetWorldAsync contract.
src/RustPlusBot.Features.Commands/Handlers/MarkerReply.cs Uses map settings to choose grid style for marker replies.
src/RustPlusBot.Features.Commands/Handlers/HeliCommandHandler.cs Injects map settings and uses async marker reply.
src/RustPlusBot.Features.Commands/Handlers/EventsCommandHandler.cs Uses map settings to choose grid style for recent events output.
src/RustPlusBot.Features.Commands/Handlers/ChinookCommandHandler.cs Injects map settings and uses async marker reply.
src/RustPlusBot.Features.Commands/Handlers/CargoCommandHandler.cs Injects map settings and uses async marker reply.
src/RustPlusBot.Domain/Map/ServerMapSettings.cs Adds persisted GridStyle property (default InGame).
src/RustPlusBot.Abstractions/Map/InfoMapView.cs Adds cross-assembly view model for ready RustMaps map.
src/RustPlusBot.Abstractions/Map/IInfoMapReadModel.cs Adds read seam for workspace renderer to access ready RustMaps map.
src/RustPlusBot.Abstractions/Events/RigKind.cs Updates documentation for small rig token.
src/RustPlusBot.Abstractions/Events/MapMarkersChangedEvent.cs Adds moved marker bucket to delta event contract.
src/RustPlusBot.Abstractions/Events/InfoMapReadyEvent.cs Adds event used to trigger reconciliation when RustMaps map becomes ready.
src/RustPlusBot.Abstractions/Connections/WorldSnapshot.cs Adds world identity record (size, seed).
src/RustPlusBot.Abstractions/Connections/MonumentSnapshot.cs Updates documentation for small rig token.
src/RustPlusBot.Abstractions/Connections/MapMarkerSnapshot.cs Adds rotation field with default null.
src/RustPlusBot.Abstractions/Connections/MapGridStyle.cs Adds enum representing in-game vs Rust+/RustMaps grid conventions.
src/RustPlusBot.Abstractions/Connections/MapGrid.cs Adds shared grid math implementation (cell count, labels, row inset).
src/RustPlusBot.Abstractions/Connections/MapDimensions.cs Redefines dimensions to include image pixels + ocean margin + world size.
src/RustPlusBot.Abstractions/Connections/IRustServerQuery.cs Adds GetWorldAsync contract.
RustPlusBot.slnx Adds the new MapParity tool project to the solution.
Directory.Packages.props Adds RustMapsApi and bumps RustPlusApi to beta.4.
.editorconfig Disables CA1054 to allow URL strings in RustMaps integration.
Files not reviewed (1)
  • src/RustPlusBot.Persistence/Migrations/20260709213331_MapGridStyle.Designer.cs: Generated file

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


try
{
await channel.DeleteMessageAsync(messageId).ConfigureAwait(false);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e2d069c — the token is now forwarded via RequestOptions.CancelToken.

Comment on lines +45 to +47
// ServerInfoMapMessageRenderer takes IInfoMapReadModel as an OPTIONAL dependency: it only resolves
// when Features.Map registered it (RustMaps API key present); otherwise the renderer shows the
// "preparing" placeholder forever.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e2d069c — comment now says the renderer returns an empty payload (no message) when no API key is configured.

Comment on lines +20 to +22
var fixture = Directory.Exists(FixtureDir)
? Directory.EnumerateFiles(FixtureDir, "rustmaps-*.json").FirstOrDefault()
: null;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e2d069c — the enumeration is now ordered (StringComparer.Ordinal) so the selected fixture is deterministic.

Comment thread tools/RustPlusBot.MapParity/Program.cs Outdated
Comment on lines +17 to +20
var size = int.Parse(args[0], CultureInfo.InvariantCulture);
var seed = int.Parse(args[1], CultureInfo.InvariantCulture);
var apiKey = args[2];
var outDir = Directory.CreateDirectory(args[3]).FullName;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e2d069c — numeric args use TryParse and invalid input prints the usage line.

HandyS11 and others added 2 commits July 10, 2026 00:22
- DiscordWorkspaceGateway.DeleteMessageAsync forwards the cancellation token
  via RequestOptions.CancelToken instead of ignoring it.
- Correct the stale DI comment: with no RustMaps API key the #info map
  renderer returns an empty payload (no message), not a placeholder.
- RustMapsParityTests orders the fixture enumeration so the selected fixture
  is deterministic across filesystems.
- MapParity CLI uses TryParse for numeric args and prints the usage line on
  invalid input instead of throwing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…be race)

Ready_key_publishes_InfoMapReadyEvent_once_per_requester failed on the Linux
CI runner with 0 events received. Root cause: the test collected events by
subscribing to the real InMemoryEventBus from a Task.Run — that bus drops
events published before a subscriber is active, and the service's first tick
(10 ms poll) can announce the key before the background subscription starts
on a loaded runner. The key is announced at most once, so the events are
gone for good and the test times out at 0.

Replace the racy live-bus collector with a small CapturingEventBus fake that
records every published InfoMapReadyEvent synchronously — deterministic, and
the tests now finish in ~170 ms instead of riding the 5 s deadline. The
Connect_registers test keeps the real bus (it re-publishes in a loop, so the
subscribe race self-heals there).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@HandyS11
HandyS11 merged commit f9897fd into develop Jul 10, 2026
3 checks passed
@HandyS11
HandyS11 deleted the feat/map-render-rework branch July 10, 2026 14:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants