Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
85ce4bf
feat(map): plumb WorldSize through MapDimensions + GetWorldAsync seam
HandyS11 Jul 7, 2026
775052b
feat(map): shared MapGrid math; GridReference uses real world size
HandyS11 Jul 7, 2026
1544d7e
feat(map): MapProjection canonical world-to-pixel transform
HandyS11 Jul 7, 2026
d8a56b6
feat(map): MapRenderStyle constants + size-scaled icon cache
HandyS11 Jul 7, 2026
78f4fd1
fix(map): correct projection, scaled icons, per-cell grid labels
HandyS11 Jul 7, 2026
0ffcddb
feat(events): Moved bucket in marker deltas + nullable marker rotation
HandyS11 Jul 7, 2026
53966c3
feat(events): active markers track movement + 6-deep history ring
HandyS11 Jul 8, 2026
a6ab825
feat(map): fading motion trails + rotation-aware marker drawing
HandyS11 Jul 8, 2026
b6ac80e
feat(map): base-map source chain with per-image projection metadata
HandyS11 Jul 8, 2026
a8cf346
test(map): restore BaseMapCache.Clear eviction coverage
HandyS11 Jul 8, 2026
68e32b4
feat(map): wire marker rotation via RustPlusApi 2.0.0-beta.4
HandyS11 Jul 8, 2026
c1dc6a9
feat(map): RustMaps preferred base-map source behind optional API key…
HandyS11 Jul 8, 2026
15c11f2
feat(map): RustMaps parity harness + skippable ground-truth test
HandyS11 Jul 8, 2026
e1cdd6d
fix(map): RustMaps timeout fall-through + doc/config cleanup from bra…
HandyS11 Jul 8, 2026
b9c37cf
fix(map): correct grid to whole 146.25 cells (rustplusplus getCorrect…
HandyS11 Jul 8, 2026
b18fb59
chore(deps): bump RustMapsApi 1.0.0-beta.1 → beta.2 (tolerant Monumen…
HandyS11 Jul 8, 2026
de3065b
revert(map): drop RustMaps as a base-map source; keep client for #info
HandyS11 Jul 8, 2026
99ff0c2
feat(map): MapComposer.ComposeStaticAsync (grid+monuments static render)
HandyS11 Jul 8, 2026
c00befb
feat(map): RustMaps generation coordinator (per size+seed state machine)
HandyS11 Jul 8, 2026
df52287
feat(map): #info channel locator + info-map attachment poster
HandyS11 Jul 8, 2026
5219f59
feat(map): credit-safe RustMaps generation driver (limits fail-closed…
HandyS11 Jul 8, 2026
a5e558e
test(map): explicit still-generating poll test + shared IsReady helper
HandyS11 Jul 9, 2026
ad18511
feat(map): RustMaps generation poll-interval option + config
HandyS11 Jul 9, 2026
5868b8d
feat(map): #info static RustMaps map + credit-safe auto-generate service
HandyS11 Jul 9, 2026
8856b62
fix(map): only spend a RustMaps credit on genuine NotFound + register…
HandyS11 Jul 9, 2026
23bba48
fix(map): serialize #info map posting per server (no duplicate messages)
HandyS11 Jul 9, 2026
9a3a114
fix(map): single edited-in-place #info map + use the iconned RustMaps…
HandyS11 Jul 9, 2026
701263c
refactor(map): #info map is a reconciled top-of-channel message (pers…
HandyS11 Jul 9, 2026
4c2a1d9
fix(workspace): reconcile channel message order (map above the #info …
HandyS11 Jul 9, 2026
3fb282a
fix(map): register #info map generation from the connection store eac…
HandyS11 Jul 9, 2026
a2ea4fe
fix(map): stop #info map flickering to a placeholder on every boot
HandyS11 Jul 9, 2026
f26485b
fix(map): correct the small oil rig monument token (oilrig_1 → oil_ri…
HandyS11 Jul 9, 2026
507eef6
fix(map): centre grid cell labels (were top-left → "half a row too hi…
HandyS11 Jul 9, 2026
8122e2a
revert(map): keep grid labels at the cell top-left corner
HandyS11 Jul 9, 2026
0326ff7
fix(map): render the partial edge grid cell so the grid matches RustMaps
HandyS11 Jul 9, 2026
9c55231
fix(map): anchor the grid at the world's north-west corner, lattice o…
HandyS11 Jul 9, 2026
47f6f1c
feat(map): per-server grid style — in-game vs Rust+/RustMaps (100-uni…
HandyS11 Jul 9, 2026
93126ff
fix(map): clip the grid to the labelled cell block, not the whole image
HandyS11 Jul 9, 2026
e2d069c
fix(review): address Copilot comments on PR #45
HandyS11 Jul 9, 2026
9b2b957
fix(test): deflake InfoMapHostedService tests (publish-before-subscri…
HandyS11 Jul 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@ roslynator_compiler_diagnostic_fixes.enabled = true
dotnet_diagnostic.CA1002.severity = none # Do not expose generic lists — API uses List<T> intentionally for protobuf collections
dotnet_diagnostic.CA1008.severity = none # Enums should have zero value
dotnet_diagnostic.CA1031.severity = none # Do not catch general exception types (intentional for fail-safe CLI resilience)
dotnet_diagnostic.CA1054.severity = none # Uri parameters should not be strings — same call as CA1056 below; RustMaps URLs travel as plain strings end to end
dotnet_diagnostic.CA1056.severity = none # Uri properties should not be strings
dotnet_diagnostic.CA1303.severity = none # Do not pass literals as localized parameters (literal strings in console output — CLI tool not localized)
dotnet_diagnostic.CA1308.severity = none # Normalize strings to uppercase
Expand Down
5 changes: 3 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" />
<PackageVersion Include="Persistord.Core" Version="1.0.0-beta.1" />
<PackageVersion Include="RustPlusApi" Version="2.0.0-beta.3" />
<PackageVersion Include="RustPlusApi.Fcm" Version="2.0.0-beta.3" />
<PackageVersion Include="RustMapsApi" Version="1.0.0-beta.2" />
<PackageVersion Include="RustPlusApi" Version="2.0.0-beta.4" />
<PackageVersion Include="RustPlusApi.Fcm" Version="2.0.0-beta.4" />
<PackageVersion Include="Serilog.Extensions.Hosting" Version="10.0.0" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="10.0.1" />
<PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1" />
Expand Down
1 change: 1 addition & 0 deletions RustPlusBot.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
</Folder>
<Folder Name="/tools/">
<Project Path="tools/RustPlusBot.ItemData.Generator/RustPlusBot.ItemData.Generator.csproj" />
<Project Path="tools/RustPlusBot.MapParity/RustPlusBot.MapParity.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/RustPlusBot.Abstractions.Tests/RustPlusBot.Abstractions.Tests.csproj" />
Expand Down
7 changes: 7 additions & 0 deletions src/RustPlusBot.Abstractions/Connections/IRustServerQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ public interface IRustServerQuery
/// <returns>The map dimensions, or null when there is no live socket.</returns>
Task<MapDimensions?> GetMapDimensionsAsync(ulong guildId, Guid serverId, CancellationToken cancellationToken);

/// <summary>Gets the world size and seed of a connected server, or null when unavailable.</summary>
/// <param name="guildId">The owning guild snowflake.</param>
/// <param name="serverId">The target server id.</param>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>The world snapshot, or null.</returns>
Task<WorldSnapshot?> GetWorldAsync(ulong guildId, Guid serverId, CancellationToken cancellationToken);

/// <summary>Gets the server's monuments, or an empty list when there is no live socket.</summary>
/// <param name="guildId">The owning guild snowflake.</param>
/// <param name="serverId">The target server id.</param>
Expand Down
11 changes: 6 additions & 5 deletions src/RustPlusBot.Abstractions/Connections/MapDimensions.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
namespace RustPlusBot.Abstractions.Connections;

/// <summary>The static-per-wipe map size needed to convert world coordinates to a grid reference.</summary>
/// <param name="Width">Map image width.</param>
/// <param name="Height">Map image height.</param>
/// <param name="OceanMargin">The ocean margin around the playable area.</param>
public sealed record MapDimensions(uint Width, uint Height, int OceanMargin);
/// <summary>Dimensions of the server-rendered map tile plus the world size.</summary>
/// <param name="Width">Width of the base map image, in pixels.</param>
/// <param name="Height">Height of the base map image, in pixels.</param>
/// <param name="OceanMargin">Ocean border baked into the base map image, in pixels.</param>
/// <param name="WorldSize">Size of the playable world, in game units (from server info).</param>
public sealed record MapDimensions(uint Width, uint Height, int OceanMargin, uint WorldSize);
77 changes: 77 additions & 0 deletions src/RustPlusBot.Abstractions/Connections/MapGrid.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System.Globalization;
using System.Text;

namespace RustPlusBot.Abstractions.Connections;

/// <summary>
/// Rust map grid math shared by the map renderer and grid-reference formatting.
/// One cell is 146.25 game units; the grid is anchored at the world's NORTH-WEST corner
/// (Rust / RustMaps / companion-app behaviour): columns are lettered west→east from A, rows are
/// numbered north→south from 0, and the partial edge cell (when the world size is not a whole
/// multiple of the cell size) sits along the south and east edges.
/// </summary>
public static class MapGrid
{
/// <summary>Edge length of one (whole) grid cell, in game units.</summary>
public const float CellSize = 146.25f;

/// <summary>
/// How far south of the world's north edge the Rust+/RustMaps grid rows start, in game units.
/// Measured exactly (100.0) from RustMaps' own pre-rendered grid tiles across map sizes
/// 1500–4500; the in-game (F1) map uses no inset. Columns have no inset in either style.
/// </summary>
public const float RustPlusRowInset = 100f;

/// <summary>Gets the row inset (south of the world's north edge) for a grid style.</summary>
/// <param name="style">The grid style.</param>
/// <returns>The inset in game units.</returns>
public static float RowInset(MapGridStyle style) =>
style == MapGridStyle.RustPlus ? RustPlusRowInset : 0f;

/// <summary>
/// Number of grid cells per axis, covering the whole world size — <c>ceil(worldSize / CellSize)</c>.
/// The last cell (east-most column / south-most row) is a partial edge cell whenever the world size
/// is not a whole multiple of <see cref="CellSize"/>; it is still a labelled cell, matching how Rust,
/// the Rust+ companion app and RustMaps draw the grid. (A 1500 world → 11 cells: A–K, rows 0–10.)
/// </summary>
/// <param name="worldSize">The world size in game units.</param>
/// <returns>The cell count (at least 1).</returns>
public static int CellCount(uint worldSize)
{
var full = (int)(worldSize / CellSize);
var hasPartial = worldSize - (full * CellSize) > 0.5f;
return Math.Max(1, hasPartial ? full + 1 : full);
}

/// <summary>Formats a spreadsheet-style column label (0→A … 25→Z, 26→AA …).</summary>
/// <param name="index">The zero-based column index.</param>
/// <returns>The column letters.</returns>
public static string ColumnLetters(int index)
{
var sb = new StringBuilder();
var n = index;
do
{
sb.Insert(0, (char)('A' + (n % 26)));
n = (n / 26) - 1;
} while (n >= 0);

return sb.ToString();
}

/// <summary>Formats the grid label ("D7") for a world coordinate.</summary>
/// <param name="x">World X (west→east).</param>
/// <param name="y">World Y (south→north).</param>
/// <param name="worldSize">The world size in game units.</param>
/// <param name="style">Which grid convention to bin against (defaults to the in-game map).</param>
/// <returns>The grid label, rows numbered from the top; out-of-world coordinates clamp to the edge cell.</returns>
public static string LabelFor(float x, float y, uint worldSize, MapGridStyle style = MapGridStyle.InGame)
{
// Rows bin from the style's row anchor (north edge in-game; 100 units south of it for
// Rust+/RustMaps), not the south — with a partial edge cell the two directions disagree.
var cells = CellCount(worldSize);
var col = Math.Clamp((int)MathF.Floor(x / CellSize), 0, cells - 1);
var row = Math.Clamp((int)MathF.Floor((worldSize - RowInset(style) - y) / CellSize), 0, cells - 1);
return string.Create(CultureInfo.InvariantCulture, $"{ColumnLetters(col)}{row}");
}
}
15 changes: 15 additions & 0 deletions src/RustPlusBot.Abstractions/Connections/MapGridStyle.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace RustPlusBot.Abstractions.Connections;

/// <summary>
/// Which map-grid convention to use. The in-game (F1) map and the Rust+ companion app / RustMaps
/// website disagree on where grid ROWS sit: the in-game map anchors row 0 at the world's north edge,
/// while Rust+/RustMaps draw the rows 100 game-units further south. Columns are identical in both.
/// </summary>
public enum MapGridStyle
{
/// <summary>Match the in-game (F1) map — grid references read exactly like in-game callouts.</summary>
InGame = 0,

/// <summary>Match the Rust+ companion app and rustmaps.com — rows sit 100 game-units south of in-game.</summary>
RustPlus = 1,
}
11 changes: 10 additions & 1 deletion src/RustPlusBot.Abstractions/Connections/MapMarkerSnapshot.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
namespace RustPlusBot.Abstractions.Connections;

// Rotation defaults to null so existing call sites stay valid and "no heading" is distinguishable
// from "heading north" once RustPlusApi beta.4 supplies values.
/// <summary>One map marker observed in a <c>GetMapMarkers</c> poll.</summary>
/// <param name="Id">The stable marker id (used to diff polls).</param>
/// <param name="Kind">The classified marker kind.</param>
/// <param name="X">World X coordinate.</param>
/// <param name="Y">World Y coordinate.</param>
/// <param name="Name">The marker name, if any.</param>
public sealed record MapMarkerSnapshot(ulong Id, MarkerKind Kind, float X, float Y, string? Name);
/// <param name="Rotation">Heading in degrees as sent by the server, or null when unavailable.</param>
public sealed record MapMarkerSnapshot(
ulong Id,
MarkerKind Kind,
float X,
float Y,
string? Name,
float? Rotation = null);
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
namespace RustPlusBot.Abstractions.Connections;

/// <summary>One named monument observed in a <c>GetMap</c> response.</summary>
/// <param name="Token">The monument token (e.g. <c>oilrig_1</c>, <c>large_oil_rig</c>).</param>
/// <param name="Token">The monument token (e.g. <c>oil_rig_small</c>, <c>large_oil_rig</c>).</param>
/// <param name="X">World X coordinate.</param>
/// <param name="Y">World Y coordinate.</param>
public sealed record MonumentSnapshot(string Token, float X, float Y);
6 changes: 6 additions & 0 deletions src/RustPlusBot.Abstractions/Connections/WorldSnapshot.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace RustPlusBot.Abstractions.Connections;

/// <summary>The world identity of a connected server, used to resolve external map imagery.</summary>
/// <param name="WorldSize">Size of the playable world, in game units.</param>
/// <param name="Seed">Procedural map generation seed.</param>
public sealed record WorldSnapshot(uint WorldSize, uint Seed);
6 changes: 6 additions & 0 deletions src/RustPlusBot.Abstractions/Events/InfoMapReadyEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace RustPlusBot.Abstractions.Events;

/// <summary>Published when a server's RustMaps #info map becomes ready, so its reconciled message repaints.</summary>
/// <param name="GuildId">The owning guild snowflake.</param>
/// <param name="ServerId">The target server id.</param>
public sealed record InfoMapReadyEvent(ulong GuildId, Guid ServerId);
6 changes: 4 additions & 2 deletions src/RustPlusBot.Abstractions/Events/MapMarkersChangedEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@

namespace RustPlusBot.Abstractions.Events;

/// <summary>Published when a marker poll detects markers that appeared or disappeared since the previous poll.</summary>
/// <summary>Published when a marker poll detects markers that appeared, disappeared, or moved since the previous poll.</summary>
/// <param name="GuildId">The owning guild snowflake.</param>
/// <param name="ServerId">The target server id.</param>
/// <param name="Dimensions">Map dimensions for grid-reference rendering, or null if unavailable.</param>
/// <param name="Added">Markers present now that were absent in the previous poll.</param>
/// <param name="Removed">Markers absent now that were present in the previous poll.</param>
/// <param name="Moved">Markers present in both polls whose position or rotation changed.</param>
public sealed record MapMarkersChangedEvent(
ulong GuildId,
Guid ServerId,
MapDimensions? Dimensions,
IReadOnlyList<MapMarkerSnapshot> Added,
IReadOnlyList<MapMarkerSnapshot> Removed);
IReadOnlyList<MapMarkerSnapshot> Removed,
IReadOnlyList<MapMarkerSnapshot> Moved);
2 changes: 1 addition & 1 deletion src/RustPlusBot.Abstractions/Events/RigKind.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ namespace RustPlusBot.Abstractions.Events;
/// <summary>Which oil rig a rig event refers to.</summary>
public enum RigKind
{
/// <summary>The small oil rig (monument token <c>oilrig_1</c>).</summary>
/// <summary>The small oil rig (monument token <c>oil_rig_small</c>).</summary>
Small = 0,

/// <summary>The large oil rig (monument token <c>large_oil_rig</c>).</summary>
Expand Down
11 changes: 11 additions & 0 deletions src/RustPlusBot.Abstractions/Map/IInfoMapReadModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace RustPlusBot.Abstractions.Map;

/// <summary>Read seam over the RustMaps generation state, for the #info map renderer (cross-assembly).</summary>
public interface IInfoMapReadModel
{
/// <summary>The ready RustMaps map for a (size, seed), or null when it is not generated/ready yet.</summary>
/// <param name="size">The world size.</param>
/// <param name="seed">The map generation seed.</param>
/// <returns>The ready map view, or null.</returns>
InfoMapView? GetReady(int size, int seed);
}
6 changes: 6 additions & 0 deletions src/RustPlusBot.Abstractions/Map/InfoMapView.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace RustPlusBot.Abstractions.Map;

/// <summary>A ready RustMaps map for the #info map message: its hosted image URL plus its page link.</summary>
/// <param name="ImageUrl">The RustMaps-hosted image URL (the monument-icon render).</param>
/// <param name="RustMapsPageUrl">The RustMaps page URL for the map, when available.</param>
public sealed record InfoMapView(string ImageUrl, string? RustMapsPageUrl);
5 changes: 5 additions & 0 deletions src/RustPlusBot.Domain/Map/ServerMapSettings.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using RustPlusBot.Abstractions.Connections;

namespace RustPlusBot.Domain.Map;

/// <summary>Per-(guild, server) rendered-map layer settings; one row per server. Layers default on.</summary>
Expand Down Expand Up @@ -26,4 +28,7 @@ public sealed class ServerMapSettings

/// <summary>Whether oil rigs are styled by activation state.</summary>
public bool ShowRigs { get; set; } = true;

/// <summary>Which grid convention the rendered map and event grid references use.</summary>
public MapGridStyle GridStyle { get; set; } = MapGridStyle.InGame;
}
16 changes: 12 additions & 4 deletions src/RustPlusBot.Features.Commands/Handlers/CargoCommandHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,32 @@
using RustPlusBot.Features.Commands.Dispatching;
using RustPlusBot.Features.Events.State;
using RustPlusBot.Localization;
using RustPlusBot.Persistence.Map;

namespace RustPlusBot.Features.Commands.Handlers;

/// <summary>!cargo — reports the cargo ship's current grid position, if any.</summary>
/// <param name="state">The live event state.</param>
/// <param name="localizer">The reply localizer.</param>
/// <param name="clock">For "how long ago".</param>
internal sealed class CargoCommandHandler(IEventState state, ILocalizer localizer, IClock clock)
/// <param name="mapSettings">Supplies the server's grid style.</param>
internal sealed class CargoCommandHandler(
IEventState state,
ILocalizer localizer,
IClock clock,
IMapSettingsStore mapSettings)
: ICommandHandler
{
/// <inheritdoc />
public string Name => "cargo";

/// <inheritdoc />
public Task<string?> ExecuteAsync(CommandContext context, CancellationToken cancellationToken)
public async Task<string?> ExecuteAsync(CommandContext context, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(context);
return Task.FromResult<string?>(
MarkerReply.For(state, context, MarkerKind.CargoShip, "command.cargo", localizer, clock));
return await MarkerReply
.ForAsync(state, context, MarkerKind.CargoShip, "command.cargo", localizer, clock, mapSettings,
cancellationToken)
.ConfigureAwait(false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,32 @@
using RustPlusBot.Features.Commands.Dispatching;
using RustPlusBot.Features.Events.State;
using RustPlusBot.Localization;
using RustPlusBot.Persistence.Map;

namespace RustPlusBot.Features.Commands.Handlers;

/// <summary>!chinook — reports the Chinook helicopter's current grid position, if any.</summary>
/// <param name="state">The live event state.</param>
/// <param name="localizer">The reply localizer.</param>
/// <param name="clock">For "how long ago".</param>
internal sealed class ChinookCommandHandler(IEventState state, ILocalizer localizer, IClock clock)
/// <param name="mapSettings">Supplies the server's grid style.</param>
internal sealed class ChinookCommandHandler(
IEventState state,
ILocalizer localizer,
IClock clock,
IMapSettingsStore mapSettings)
: ICommandHandler
{
/// <inheritdoc />
public string Name => "chinook";

/// <inheritdoc />
public Task<string?> ExecuteAsync(CommandContext context, CancellationToken cancellationToken)
public async Task<string?> ExecuteAsync(CommandContext context, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(context);
return Task.FromResult<string?>(
MarkerReply.For(state, context, MarkerKind.Chinook, "command.chinook", localizer, clock));
return await MarkerReply
.ForAsync(state, context, MarkerKind.Chinook, "command.chinook", localizer, clock, mapSettings,
cancellationToken)
.ConfigureAwait(false);
}
}
18 changes: 12 additions & 6 deletions src/RustPlusBot.Features.Commands/Handlers/EventsCommandHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,38 @@
using RustPlusBot.Features.Events.Formatting;
using RustPlusBot.Features.Events.State;
using RustPlusBot.Localization;
using RustPlusBot.Persistence.Map;

namespace RustPlusBot.Features.Commands.Handlers;

/// <summary>!events — lists the most recent live events.</summary>
/// <param name="state">The live event state.</param>
/// <param name="localizer">The reply localizer.</param>
internal sealed class EventsCommandHandler(IEventState state, ILocalizer localizer) : ICommandHandler
/// <param name="mapSettings">Supplies the server's grid style.</param>
internal sealed class EventsCommandHandler(
IEventState state,
ILocalizer localizer,
IMapSettingsStore mapSettings) : ICommandHandler
{
/// <inheritdoc />
public string Name => "events";

/// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">A recent event has an unsupported <see cref="MapEventKind"/>.</exception>
public Task<string?> ExecuteAsync(CommandContext context, CancellationToken cancellationToken)
public async Task<string?> ExecuteAsync(CommandContext context, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(context);
var events = state.GetRecentEvents(context.GuildId, context.ServerId);
if (events.Count == 0)
{
return Task.FromResult<string?>(localizer.Get("command.events.none", context.Culture));
return localizer.Get("command.events.none", context.Culture);
}

var settings = await mapSettings.GetAsync(context.GuildId, context.ServerId, cancellationToken)
.ConfigureAwait(false);
var parts = events.Select(e =>
{
var grid = GridReference.From(e.X, e.Y, e.Dimensions);
var grid = GridReference.From(e.X, e.Y, e.Dimensions, settings.GridStyle);
var key = e.Kind switch
{
MapEventKind.CargoEntered => "command.event.cargoentered",
Expand All @@ -40,7 +47,6 @@ internal sealed class EventsCommandHandler(IEventState state, ILocalizer localiz
return localizer.Get(key, context.Culture, grid);
});

return Task.FromResult<string?>(
localizer.Get("command.events.ok", context.Culture, string.Join(", ", parts)));
return localizer.Get("command.events.ok", context.Culture, string.Join(", ", parts));
}
}
Loading
Loading