Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 9 additions & 9 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ Liberation Sans (Regular)
The OFL permits embedding and redistribution in software products.

-------------------------------------------------------------------------------
MAP MARKER / MONUMENT ICONS (future 2b-ii slice)
MAP MARKER / MONUMENT ICONS
-------------------------------------------------------------------------------

The current 2b slice draws all map markers programmatically as glyphs; NO
third-party image assets are bundled.
Map marker and monument icons under src/RustPlusBot.Features.Map/Assets/icons/
originate from Rust (© Facepunch Studios) and are reused here under the same
companion-app fair-use basis as the official Rust+ application. They were
sourced via the rustplus-desktop project, which redistributes the game art;
the icons are Facepunch's game assets, not original GPL-licensed work, and are
not treated as GPL-licensed nor as encumbering this MIT-licensed project.

When marker and monument icons are added in the 2b-ii slice, they will
originate as Facepunch / Rust game art. They are used under the same
companion-app norms that govern the official Rust+ app and established
reference bots — NOT under the GPL of the third-party repositories that
merely redistribute them. Attribution for those assets will be added to this
file at that time.
The player position icon (player.png) was sourced from the rustplusplus project
on the same Facepunch-game-art fair-use basis described above.
10 changes: 10 additions & 0 deletions src/RustPlusBot.Abstractions/Connections/IRustServerQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,14 @@ public interface IRustServerQuery
/// <param name="cancellationToken">A cancellation token.</param>
/// <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 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>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>The monuments, or an empty list when there is no live socket.</returns>
Task<IReadOnlyList<MonumentSnapshot>> GetMonumentsAsync(
ulong guildId,
Guid serverId,
CancellationToken cancellationToken);
}
3 changes: 3 additions & 0 deletions src/RustPlusBot.Abstractions/Connections/MarkerKind.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,7 @@ public enum MarkerKind

/// <summary>A locked crate.</summary>
Crate = 4,

/// <summary>The travelling vendor.</summary>
TravellingVendor = 5,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace RustPlusBot.Abstractions.Events;

/// <summary>Published when a server's map layer settings change, so the map image repaints immediately.</summary>
/// <param name="GuildId">The owning guild snowflake.</param>
/// <param name="ServerId">The target server id.</param>
public sealed record MapSettingsChangedEvent(ulong GuildId, Guid ServerId);
29 changes: 29 additions & 0 deletions src/RustPlusBot.Domain/Map/ServerMapSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace RustPlusBot.Domain.Map;

/// <summary>Per-(guild, server) rendered-map layer settings; one row per server. Layers default on.</summary>
public sealed class ServerMapSettings
{
/// <summary>The owning guild snowflake.</summary>
public ulong GuildId { get; set; }

/// <summary>The server id (FK to RustServer; primary key, one row per server).</summary>
public Guid ServerId { get; set; }

/// <summary>Whether the grid layer is drawn.</summary>
public bool ShowGrid { get; set; } = true;

/// <summary>Whether live cargo/heli/chinook markers are drawn.</summary>
public bool ShowMarkers { get; set; } = true;

/// <summary>Whether monument icons are drawn.</summary>
public bool ShowMonuments { get; set; } = true;

/// <summary>Whether the travelling-vendor marker is drawn.</summary>
public bool ShowVendor { get; set; } = true;

/// <summary>Whether teammate position markers are drawn.</summary>
public bool ShowPlayers { get; set; } = true;

/// <summary>Whether oil rigs are styled by activation state.</summary>
public bool ShowRigs { get; set; } = true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -324,9 +324,10 @@ public async Task<IReadOnlyList<MapMarkerSnapshot>> GetMapMarkersAsync(
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(timeout);
// CONFIRMED (2.0.0-beta.1): GetMapMarkersAsync returns Task<Response<RustPlusApi.Data.MapMarkers>>.
// MapMarkers has typed dictionaries CargoShipMarkers/PatrolHelicopterMarkers/Ch47Markers
// MapMarkers has typed dictionaries CargoShipMarkers/PatrolHelicopterMarkers/Ch47Markers/TravellingVendorMarkers
// (Dictionary<ulong, XMarker>); each marker exposes Nullable<ulong> Id, Nullable<float> X/Y.
// No flat list, no Type field, NO crate bucket (the game stopped sending crate markers) → core-3.
// Surfaced buckets: CargoShip, PatrolHelicopter, Chinook, TravellingVendor.
var response = await _rustPlus.GetMapMarkersAsync(timeoutCts.Token).WaitAsync(timeoutCts.Token)
.ConfigureAwait(false);
if (!response.IsSuccess || response.Data is null)
Expand All @@ -339,6 +340,7 @@ public async Task<IReadOnlyList<MapMarkerSnapshot>> GetMapMarkersAsync(
AddMarkers(markers, data.CargoShipMarkers, MarkerKind.CargoShip);
AddMarkers(markers, data.PatrolHelicopterMarkers, MarkerKind.PatrolHelicopter);
AddMarkers(markers, data.Ch47Markers, MarkerKind.Chinook);
AddMarkers(markers, data.TravellingVendorMarkers, MarkerKind.TravellingVendor);
return markers;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,21 @@ public async Task<bool> PromoteToLeaderAsync(
.ConfigureAwait(false);
}

/// <inheritdoc />
public async Task<IReadOnlyList<MonumentSnapshot>> GetMonumentsAsync(
ulong guildId,
Guid serverId,
CancellationToken cancellationToken)
{
if (!_liveSockets.TryGetValue((guildId, serverId), out var live))
{
return [];
}

return await live.Connection.GetMonumentsAsync(_options.HeartbeatTimeout, cancellationToken)
.ConfigureAwait(false);
}

/// <inheritdoc />
public async Task<TeamChatSendResult> SendAsync(
ulong guildId,
Expand Down
75 changes: 75 additions & 0 deletions src/RustPlusBot.Features.Map/Assets/MapIcons.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System.Collections.Concurrent;
using RustPlusBot.Abstractions.Events;
using RustPlusBot.Features.Connections.Listening;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;

namespace RustPlusBot.Features.Map.Assets;

/// <summary>
/// Loads vendored icon assets (embedded PNGs) once and serves them by marker kind or monument token.
/// Cached images are immutable inputs the renderer draws from; never mutate them.
/// </summary>
/// <remarks>
/// <c>MapIcons.Marker</c> includes a <c>TravellingVendor</c> arm that returns the embedded vendor.png.
/// <see cref="Vendor"/> is retained as a bridge accessor for callers that reference it directly.
/// </remarks>
public static class MapIcons
{
private const string ResourcePrefix = "RustPlusBot.Features.Map.Assets.icons.";

private static readonly ConcurrentDictionary<string, Image<Rgba32>?> Cache = new(StringComparer.Ordinal);

/// <summary>Gets the icon for a marker kind, or null when there is no icon for that kind.</summary>
/// <param name="kind">The marker kind.</param>
/// <returns>The cached icon image, or null.</returns>
public static Image<Rgba32>? Marker(MarkerKind kind) => kind switch
{
MarkerKind.CargoShip => Load("cargo"),
MarkerKind.PatrolHelicopter => Load("patrol"),
MarkerKind.Chinook => Load("ch47"),
MarkerKind.TravellingVendor => Load("vendor"),
_ => null,
};

/// <summary>Gets the rig icon for a rig kind, or null when there is no icon for that kind.</summary>
/// <param name="kind">Which rig.</param>
/// <param name="active">Whether the rig is currently active (reserved; activation styling is applied by the renderer).</param>
/// <returns>The cached rig icon image, or null.</returns>
/// <remarks>
/// The <paramref name="active"/> parameter is reserved for future use: the renderer is responsible
/// for overlaying activation styling; this method always returns the base icon regardless of state.
/// </remarks>
#pragma warning disable RCS1163 // Unused parameter — 'active' is part of the public API contract; activation styling is applied by the renderer, not here.
public static Image<Rgba32>? Rig(RigKind kind, bool active) => kind switch
{
RigKind.Small => Load("oilrig"),
RigKind.Large => Load("largeoilrig"),
_ => null,
};
#pragma warning restore RCS1163

/// <summary>Gets the travelling vendor icon.</summary>
/// <returns>The cached vendor icon image, or null.</returns>
public static Image<Rgba32>? Vendor() => Load("vendor");

/// <summary>Gets the player position icon, or null when the asset is missing.</summary>
/// <returns>The player icon, or null.</returns>
public static Image<Rgba32>? Player() => Load("player");

/// <summary>Gets the icon for a monument token, or null when the token is unmapped or the file is missing.</summary>
/// <param name="token">The Rust+ monument protobuf token.</param>
/// <returns>The cached icon image, or null.</returns>
public static Image<Rgba32>? Monument(string token)
{
var key = MonumentIconMap.IconKeyFor(token);
return key is null ? null : Load(key);
}

private static Image<Rgba32>? Load(string key) => Cache.GetOrAdd(key, static k =>
{
var asm = typeof(MapIcons).Assembly;
using var stream = asm.GetManifestResourceStream(ResourcePrefix + k + ".png");
return stream is null ? null : Image.Load<Rgba32>(stream);
});
}
19 changes: 0 additions & 19 deletions src/RustPlusBot.Features.Map/Assets/MarkerGlyphs.cs

This file was deleted.

49 changes: 49 additions & 0 deletions src/RustPlusBot.Features.Map/Assets/MonumentIconMap.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
namespace RustPlusBot.Features.Map.Assets;

/// <summary>Maps a Rust+ monument protobuf token to a vendored icon key (filename without extension). Unknown tokens return null and are skipped.</summary>
public static class MonumentIconMap
{
private static readonly Dictionary<string, string> Map = new(StringComparer.Ordinal)
{
["AbandonedMilitaryBase"] = "militarybase",
["airfield_display_name"] = "airfield",
["arctic_base_a"] = "arcticresearch",
["bandit_camp"] = "banditcamp",
["dome_monument_name"] = "dome",
["excavator"] = "excavator",
["ferryterminal"] = "ferryterminal",
["fishing_village_display_name"] = "fishingvillage",
["large_fishing_village_display_name"] = "fishingvillagelarge",
["gas_station"] = "gasstation",
["harbor_display_name"] = "harbour",
["harbor_2_display_name"] = "harbour2",
["junkyard_display_name"] = "junkyard",
["large_oil_rig"] = "largeoilrig",
["oilrig_1"] = "oilrig",
["launchsite"] = "launchsite",
["lighthouse_display_name"] = "lighthouse",
["military_tunnels_display_name"] = "militarytunnel",
["mining_outpost_display_name"] = "miningoutpost",
["mining_quarry_hqm_display_name"] = "hqmquarry",
["mining_quarry_stone_display_name"] = "stonequarry",
["mining_quarry_sulfur_display_name"] = "sulfurquarry",
["missile_silo_monument"] = "missilesilo",
["outpost"] = "outpost",
["power_plant_display_name"] = "powerplant",
["satellite_dish_display_name"] = "satellitedish",
["sewer_display_name"] = "sewerbranch",
["stables_a"] = "stable",
["stables_b"] = "stable",
["supermarket"] = "supermarket",
["swamp_c"] = "swamp",
["train_yard_display_name"] = "trainyard",
["train_tunnel_display_name"] = "traintunnel",
["water_treatment_plant_display_name"] = "watertreatment",
};

/// <summary>Gets the icon key for a monument token, or null when unmapped.</summary>
/// <param name="token">The Rust+ monument protobuf token.</param>
/// <returns>The icon key (filename without extension), or null.</returns>
public static string? IconKeyFor(string token) =>
token is not null && Map.TryGetValue(token, out var key) ? key : null;
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading