diff --git a/assets/discord/README.md b/assets/discord/README.md new file mode 100644 index 0000000..a23366a --- /dev/null +++ b/assets/discord/README.md @@ -0,0 +1,30 @@ +# Discord Rich Presence art assets + +The three images EDNexus's Discord Rich Presence refers to by key (see `DiscordPresenceMapper`). Each is +1024 x 1024 (Discord's recommended size). They are in the repo, not inside the app: Discord shows images +only from files uploaded to the Discord **application**, so they have to be uploaded once by whoever owns it. + +| File | Asset key | Where it shows | +|---|---|---| +| `ednexus_logo.png` | `ednexus_logo` | The large image on every presence (the app icon). The ship is named in its hover text. | +| `docked.png` | `docked` | The small badge while docked. | +| `cruising.png` | `cruising` | The small badge while in flight. | + +## Uploading + +1. Open the application at (id `1557985945393827911`). +2. Left sidebar -> **Rich Presence** -> **Art Assets** -> **Add Image(s)**. +3. Select the three PNGs. The key defaults to the file name without the extension; Discord lower-cases it, so + leave the names as they are. +4. Click **Save Changes**. New assets can take a few minutes to appear in presence. + +If the sidebar entry is not where you expect, Discord moves things around: look under Rich Presence (older +layouts called the page "Rich Presence Assets") and check the keys afterwards match the table. + +## Notes + +- Earlier the large image was a per-ship key (`federal_corvette`, ...), which only renders if an asset with + exactly that key was uploaded, and the journal's ship names are not stable enough to pre-generate. The large + image is now always `ednexus_logo`; per-ship art is not used. +- The images are drawn from `docs/assets/logo/ednexus-icon.svg` and the brand palette; regenerate rather than + hand-editing if the logo changes. diff --git a/assets/discord/cruising.png b/assets/discord/cruising.png new file mode 100644 index 0000000..9a49103 Binary files /dev/null and b/assets/discord/cruising.png differ diff --git a/assets/discord/docked.png b/assets/discord/docked.png new file mode 100644 index 0000000..baf44f6 Binary files /dev/null and b/assets/discord/docked.png differ diff --git a/assets/discord/ednexus_logo.png b/assets/discord/ednexus_logo.png new file mode 100644 index 0000000..8046335 Binary files /dev/null and b/assets/discord/ednexus_logo.png differ diff --git a/src/EDNexus.Core/Discord/DiscordPresenceMapper.cs b/src/EDNexus.Core/Discord/DiscordPresenceMapper.cs index 2dddee8..5d00061 100644 --- a/src/EDNexus.Core/Discord/DiscordPresenceMapper.cs +++ b/src/EDNexus.Core/Discord/DiscordPresenceMapper.cs @@ -59,7 +59,11 @@ public static DiscordPresencePayload Map( var detailsText = BuildDetails(state, options.ShowCommander); - var largeImageKey = ShipImageKey(state.Ship) ?? DefaultLargeImageKey; + // Always the EDNexus logo. A per-ship key (e.g. "krait_mk_ii") only renders if an asset with exactly + // that key was uploaded to the Discord application, and the journal's localised ship names are not + // stable enough to pre-generate for every ship, so a missing one showed a broken image. The ship is + // still named, in the hover text here and in the details line. + var largeImageKey = DefaultLargeImageKey; var largeImageText = state.Ship ?? DefaultLargeImageText; var smallImageKey = docked ? "docked" : "cruising"; @@ -108,23 +112,4 @@ private static string BuildExploringState(string? system, string? body) ? $"Flying {state.Ship}" : $"Flying {state.Ship} ({state.ShipIdent})"; } - - /// - /// Discord asset keys are lowercase-with-underscores identifiers uploaded to the application's Art - /// Assets page; this derives one from the journal's ship name (e.g. "Federal Corvette" → - /// "federal_corvette") so a matching asset just needs to be uploaded under that key. - /// - private static string? ShipImageKey(string? ship) - { - if (string.IsNullOrWhiteSpace(ship)) return null; - - var sb = new StringBuilder(ship.Length); - foreach (var c in ship.ToLower(CultureInfo.InvariantCulture)) - { - if (char.IsLetterOrDigit(c)) sb.Append(c); - else if (sb.Length > 0 && sb[^1] != '_') sb.Append('_'); - } - var key = sb.ToString().Trim('_'); - return key.Length > 0 ? key : null; - } } diff --git a/src/EDNexus.Core/Discord/DiscordPresenceService.cs b/src/EDNexus.Core/Discord/DiscordPresenceService.cs index bd103d4..34999f8 100644 --- a/src/EDNexus.Core/Discord/DiscordPresenceService.cs +++ b/src/EDNexus.Core/Discord/DiscordPresenceService.cs @@ -18,11 +18,30 @@ namespace EDNexus.Core.Discord; public sealed class DiscordPresenceService : IDisposable { /// - /// Placeholder Discord application (Client) ID. Replace with EDNexus's own application, created at - /// https://discord.com/developers/applications, before shipping — this id has no art assets - /// registered against it, so large/small images will simply not render until it is replaced. + /// EDNexus's own Discord application (Client) ID, registered at + /// https://discord.com/developers/applications. Rich Presence images (ednexus_logo, + /// docked, cruising, ship keys) only render once art assets with those keys are uploaded + /// to that application; presence works without them, just without pictures. /// - public const string DefaultApplicationId = "0000000000000000000"; + public const string DefaultApplicationId = "1557985945393827911"; + + /// + /// The all-zeros placeholder earlier builds shipped as the default. It is not a real application, so + /// Discord rejects it and no presence is ever shown; it was also saved into every commander's + /// settings.json, so it must be recognised and replaced rather than honoured. + /// + public const string PlaceholderApplicationId = "0000000000000000000"; + + /// + /// The application id to connect with: the commander's override when they set a real one, otherwise + /// . A blank value or the old placeholder (persisted by earlier + /// builds) counts as "not set". + /// + public static string ResolveApplicationId(string? configured) + { + var id = configured?.Trim(); + return string.IsNullOrEmpty(id) || id == PlaceholderApplicationId ? DefaultApplicationId : id; + } /// Discord's own guidance: don't push presence updates more than once every 15 seconds. public static readonly TimeSpan DefaultMinInterval = TimeSpan.FromSeconds(15); diff --git a/src/EDNexus.Core/EngineHost.cs b/src/EDNexus.Core/EngineHost.cs index ad785a7..05fdece 100644 --- a/src/EDNexus.Core/EngineHost.cs +++ b/src/EDNexus.Core/EngineHost.cs @@ -227,7 +227,7 @@ public EngineHost( var discord = settings.Discord; _discordPresence = new DiscordPresenceController(State, () => { - try { return new DiscordRpcClientAdapter(discord.ApplicationId); } + try { return new DiscordRpcClientAdapter(DiscordPresenceService.ResolveApplicationId(discord.ApplicationId)); } catch { return NoOpDiscordRpcClient.Instance; } // unsupported platform, etc. }, reportingSuppressed); _discordPresence.Apply(discord); diff --git a/tests/EDNexus.Tests/Discord/DiscordApplicationIdTests.cs b/tests/EDNexus.Tests/Discord/DiscordApplicationIdTests.cs new file mode 100644 index 0000000..24abfc0 --- /dev/null +++ b/tests/EDNexus.Tests/Discord/DiscordApplicationIdTests.cs @@ -0,0 +1,37 @@ +using EDNexus.Core.Discord; +using EDNexus.Core.Settings; +using Xunit; + +namespace EDNexus.Tests.Discord; + +/// +/// Rich Presence only works under a real Discord application id. Earlier builds shipped an all-zeros +/// placeholder and saved it into every settings file, so the default is not enough: the old value has to +/// be recognised and replaced, while a commander's own real override is honoured. +/// +public class DiscordApplicationIdTests +{ + [Fact] + public void The_default_is_the_real_application_and_not_the_placeholder() + { + Assert.Equal("1557985945393827911", DiscordPresenceService.DefaultApplicationId); + Assert.NotEqual(DiscordPresenceService.PlaceholderApplicationId, DiscordPresenceService.DefaultApplicationId); + Assert.Equal(DiscordPresenceService.DefaultApplicationId, new DiscordSettings().ApplicationId); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("0000000000000000000")] // saved by earlier builds + [InlineData(" 0000000000000000000 ")] + public void A_missing_or_placeholder_id_resolves_to_the_default(string? configured) + => Assert.Equal(DiscordPresenceService.DefaultApplicationId, DiscordPresenceService.ResolveApplicationId(configured)); + + [Fact] + public void A_real_override_is_honoured() + { + Assert.Equal("123456789012345678", DiscordPresenceService.ResolveApplicationId("123456789012345678")); + Assert.Equal("123456789012345678", DiscordPresenceService.ResolveApplicationId(" 123456789012345678 ")); + } +} diff --git a/tests/EDNexus.Tests/Discord/DiscordPresenceMapperTests.cs b/tests/EDNexus.Tests/Discord/DiscordPresenceMapperTests.cs index b6532b1..30c0c5d 100644 --- a/tests/EDNexus.Tests/Discord/DiscordPresenceMapperTests.cs +++ b/tests/EDNexus.Tests/Discord/DiscordPresenceMapperTests.cs @@ -106,13 +106,15 @@ public void No_ship_and_no_cargo_gives_null_details() } [Fact] - public void Ship_name_is_slugged_into_a_large_image_key() + public void The_ship_is_named_in_the_hover_text_but_the_large_image_stays_the_logo() { + // A per-ship asset key would render blank unless that exact asset was uploaded, so the large + // image is always the logo that ships with the app; the ship is named in the text. var state = new CommanderState { Ship = "Federal Corvette" }; var payload = DiscordPresenceMapper.Map(state, SessionStart); - Assert.Equal("federal_corvette", payload.LargeImageKey); + Assert.Equal("ednexus_logo", payload.LargeImageKey); Assert.Equal("Federal Corvette", payload.LargeImageText); }