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
30 changes: 30 additions & 0 deletions assets/discord/README.md
Original file line number Diff line number Diff line change
@@ -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 <https://discord.com/developers/applications> (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.
Binary file added assets/discord/cruising.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/discord/docked.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/discord/ednexus_logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 5 additions & 20 deletions src/EDNexus.Core/Discord/DiscordPresenceMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -108,23 +112,4 @@ private static string BuildExploringState(string? system, string? body)
? $"Flying {state.Ship}"
: $"Flying {state.Ship} ({state.ShipIdent})";
}

/// <summary>
/// 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.
/// </summary>
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;
}
}
27 changes: 23 additions & 4 deletions src/EDNexus.Core/Discord/DiscordPresenceService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,30 @@ namespace EDNexus.Core.Discord;
public sealed class DiscordPresenceService : IDisposable
{
/// <summary>
/// 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 (<c>ednexus_logo</c>,
/// <c>docked</c>, <c>cruising</c>, ship keys) only render once art assets with those keys are uploaded
/// to that application; presence works without them, just without pictures.
/// </summary>
public const string DefaultApplicationId = "0000000000000000000";
public const string DefaultApplicationId = "1557985945393827911";

/// <summary>
/// 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
/// <c>settings.json</c>, so it must be recognised and replaced rather than honoured.
/// </summary>
public const string PlaceholderApplicationId = "0000000000000000000";

/// <summary>
/// The application id to connect with: the commander's override when they set a real one, otherwise
/// <see cref="DefaultApplicationId"/>. A blank value or the old placeholder (persisted by earlier
/// builds) counts as "not set".
/// </summary>
public static string ResolveApplicationId(string? configured)
{
var id = configured?.Trim();
return string.IsNullOrEmpty(id) || id == PlaceholderApplicationId ? DefaultApplicationId : id;
}

/// <summary>Discord's own guidance: don't push presence updates more than once every 15 seconds.</summary>
public static readonly TimeSpan DefaultMinInterval = TimeSpan.FromSeconds(15);
Expand Down
2 changes: 1 addition & 1 deletion src/EDNexus.Core/EngineHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
37 changes: 37 additions & 0 deletions tests/EDNexus.Tests/Discord/DiscordApplicationIdTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using EDNexus.Core.Discord;
using EDNexus.Core.Settings;
using Xunit;

namespace EDNexus.Tests.Discord;

/// <summary>
/// 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.
/// </summary>
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 "));
}
}
6 changes: 4 additions & 2 deletions tests/EDNexus.Tests/Discord/DiscordPresenceMapperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Loading