Commit f9897fd
Map render rework: correct projection & grid, RustMaps #info map, per-server grid style (#45)
* feat(map): plumb WorldSize through MapDimensions + GetWorldAsync seam
* feat(map): shared MapGrid math; GridReference uses real world size
* feat(map): MapProjection canonical world-to-pixel transform
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(map): MapRenderStyle constants + size-scaled icon cache
* fix(map): correct projection, scaled icons, per-cell grid labels
* feat(events): Moved bucket in marker deltas + nullable marker rotation
* feat(events): active markers track movement + 6-deep history ring
* feat(map): fading motion trails + rotation-aware marker drawing
* feat(map): base-map source chain with per-image projection metadata
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.
* test(map): restore BaseMapCache.Clear eviction coverage
* feat(map): wire marker rotation via RustPlusApi 2.0.0-beta.4
* feat(map): RustMaps preferred base-map source behind optional API key; 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>
* feat(map): RustMaps parity harness + skippable ground-truth test
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>
* fix(map): RustMaps timeout fall-through + doc/config cleanup from branch review
* fix(map): correct grid to whole 146.25 cells (rustplusplus getCorrectedMapSize)
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>
* chore(deps): bump RustMapsApi 1.0.0-beta.1 → beta.2 (tolerant MonumentType enum)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* revert(map): drop RustMaps as a base-map source; keep client for #info
* feat(map): MapComposer.ComposeStaticAsync (grid+monuments static render)
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>
* feat(map): RustMaps generation coordinator (per size+seed state machine)
* feat(map): #info channel locator + info-map attachment poster
* feat(map): credit-safe RustMaps generation driver (limits fail-closed, 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.
* test(map): explicit still-generating poll test + shared IsReady helper
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.
* feat(map): RustMaps generation poll-interval option + config
* feat(map): #info static RustMaps map + credit-safe auto-generate service
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.
* fix(map): only spend a RustMaps credit on genuine NotFound + registered #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.
* fix(map): serialize #info map posting per server (no duplicate messages)
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>
* fix(map): single edited-in-place #info map + use the iconned RustMaps 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>
* refactor(map): #info map is a reconciled top-of-channel message (persistent, 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>
* fix(workspace): reconcile channel message order (map above the #info 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>
* fix(map): register #info map generation from the connection store each 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>
* fix(map): stop #info map flickering to a placeholder on every boot
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>
* fix(map): correct the small oil rig monument token (oilrig_1 → oil_rig_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>
* fix(map): centre grid cell labels (were top-left → "half a row too high")
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>
* revert(map): keep grid labels at the cell top-left corner
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>
* fix(map): render the partial edge grid cell so the grid matches RustMaps
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>
* fix(map): anchor the grid at the world's north-west corner, lattice over 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>
* feat(map): per-server grid style — in-game vs Rust+/RustMaps (100-unit 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>
* fix(map): clip the grid to the labelled cell block, not the whole image
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>
* fix(review): address Copilot comments on PR #45
- 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>
* fix(test): deflake InfoMapHostedService tests (publish-before-subscribe 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>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>1 parent c049fb2 commit f9897fd
119 files changed
Lines changed: 4256 additions & 421 deletions
File tree
- src
- RustPlusBot.Abstractions
- Connections
- Events
- Map
- RustPlusBot.Domain/Map
- RustPlusBot.Features.Commands/Handlers
- RustPlusBot.Features.Connections
- Listening
- Supervisor
- RustPlusBot.Features.Events
- Formatting
- Relaying
- Rendering
- State
- RustPlusBot.Features.Map
- Assets
- Composing
- Hosting
- Rendering
- RustMaps
- RustPlusBot.Features.Players
- Relaying
- Rendering
- RustPlusBot.Features.Workspace
- Gateway
- Hosting
- Messages
- Modules
- Reconciler
- Specs
- RustPlusBot.Host
- RustPlusBot.Localization
- RustPlusBot.Persistence
- Map
- Migrations
- tests
- RustPlusBot.Abstractions.Tests
- Connections
- Events
- RustPlusBot.Features.Commands.Tests
- Handlers
- RustPlusBot.Features.Connections.Tests
- Fakes
- RustPlusBot.Features.Events.Tests
- Classifying
- Formatting
- Relaying
- Rendering
- State
- RustPlusBot.Features.Map.Tests
- Hosting
- RustMaps
- RustPlusBot.Features.Players.Tests
- Hosting
- RustPlusBot.Features.Workspace.Tests
- Fakes
- Messages
- Reconciler
- RustPlusBot.Localization.Tests
- RustPlusBot.Persistence.Tests/Map
- tools/RustPlusBot.MapParity
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
392 | 392 | | |
393 | 393 | | |
394 | 394 | | |
| 395 | + | |
395 | 396 | | |
396 | 397 | | |
397 | 398 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
16 | | - | |
17 | | - | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
18 | 19 | | |
19 | 20 | | |
20 | 21 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
| 24 | + | |
24 | 25 | | |
25 | 26 | | |
26 | 27 | | |
| |||
Lines changed: 7 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
46 | 46 | | |
47 | 47 | | |
48 | 48 | | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
49 | 56 | | |
50 | 57 | | |
51 | 58 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
4 | | - | |
5 | | - | |
6 | | - | |
7 | | - | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
Lines changed: 10 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
| 3 | + | |
| 4 | + | |
3 | 5 | | |
4 | 6 | | |
5 | 7 | | |
6 | 8 | | |
7 | 9 | | |
8 | 10 | | |
9 | | - | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
0 commit comments