Skip to content

Commit

Permalink
Add Discord Rich Presence support
Browse files Browse the repository at this point in the history
Off by default
Checks in fork of https://github.com/Lachee/discord-rpc-csharp, needed as the project used Newtonsoft.Json which isn't NativeAOT compatible. Some optimizations have also been put in place to take advantage of newer .NET
  • Loading branch information
CasualPokePlayer committed Jan 2, 2025
1 parent 6047280 commit 5e04dcd
Show file tree
Hide file tree
Showing 62 changed files with 5,703 additions and 12 deletions.
2 changes: 1 addition & 1 deletion GSE.Audio/BlipBuffer.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2024 CasualPokePlayer & Shay Green & EkeEke
// Copyright (c) 2024 Shay Green & EkeEke & CasualPokePlayer
// SPDX-License-Identifier: LGPL-2.1-or-later

using System;
Expand Down
18 changes: 18 additions & 0 deletions GSE.sln
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GSE.Android", "GSE.Android\
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SDL2-CS", "externals\SDL2-CS\SDL2-CS.csproj", "{1C593A7D-00A9-46B3-8C6D-A06761900ED4}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscordRPC", "externals\DiscordRPC\DiscordRPC.csproj", "{1C593A7D-00A9-46B3-8C6D-3BD044401A1B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Expand Down Expand Up @@ -51,6 +53,10 @@ Global
{1C593A7D-00A9-46B3-8C6D-A06761900ED4}.Debug|x64.Build.0 = Debug|x64
{1C593A7D-00A9-46B3-8C6D-A06761900ED4}.Release|x64.ActiveCfg = Release|x64
{1C593A7D-00A9-46B3-8C6D-A06761900ED4}.Release|x64.Build.0 = Release|x64
{1C593A7D-00A9-46B3-8C6D-3BD044401A1B}.Debug|x64.ActiveCfg = Debug|x64
{1C593A7D-00A9-46B3-8C6D-3BD044401A1B}.Debug|x64.Build.0 = Debug|x64
{1C593A7D-00A9-46B3-8C6D-3BD044401A1B}.Release|x64.ActiveCfg = Release|x64
{1C593A7D-00A9-46B3-8C6D-3BD044401A1B}.Release|x64.Build.0 = Release|x64
{F38AAF32-C1FB-41EC-9BCB-F9035F5790AB}.Debug|ARM64.ActiveCfg = Debug|ARM64
{F38AAF32-C1FB-41EC-9BCB-F9035F5790AB}.Debug|ARM64.Build.0 = Debug|ARM64
{F38AAF32-C1FB-41EC-9BCB-F9035F5790AB}.Release|ARM64.ActiveCfg = Release|ARM64
Expand All @@ -75,6 +81,10 @@ Global
{1C593A7D-00A9-46B3-8C6D-A06761900ED4}.Debug|ARM64.Build.0 = Debug|ARM64
{1C593A7D-00A9-46B3-8C6D-A06761900ED4}.Release|ARM64.ActiveCfg = Release|ARM64
{1C593A7D-00A9-46B3-8C6D-A06761900ED4}.Release|ARM64.Build.0 = Release|ARM64
{1C593A7D-00A9-46B3-8C6D-3BD044401A1B}.Debug|ARM64.ActiveCfg = Debug|ARM64
{1C593A7D-00A9-46B3-8C6D-3BD044401A1B}.Debug|ARM64.Build.0 = Debug|ARM64
{1C593A7D-00A9-46B3-8C6D-3BD044401A1B}.Release|ARM64.ActiveCfg = Release|ARM64
{1C593A7D-00A9-46B3-8C6D-3BD044401A1B}.Release|ARM64.Build.0 = Release|ARM64
{F38AAF32-C1FB-41EC-9BCB-F9035F5790AB}.Debug|x86.ActiveCfg = Debug|x86
{F38AAF32-C1FB-41EC-9BCB-F9035F5790AB}.Debug|x86.Build.0 = Debug|x86
{F38AAF32-C1FB-41EC-9BCB-F9035F5790AB}.Release|x86.ActiveCfg = Release|x86
Expand All @@ -99,6 +109,10 @@ Global
{1C593A7D-00A9-46B3-8C6D-A06761900ED4}.Debug|x86.Build.0 = Debug|x86
{1C593A7D-00A9-46B3-8C6D-A06761900ED4}.Release|x86.ActiveCfg = Release|x86
{1C593A7D-00A9-46B3-8C6D-A06761900ED4}.Release|x86.Build.0 = Release|x86
{1C593A7D-00A9-46B3-8C6D-3BD044401A1B}.Debug|x86.ActiveCfg = Debug|x86
{1C593A7D-00A9-46B3-8C6D-3BD044401A1B}.Debug|x86.Build.0 = Debug|x86
{1C593A7D-00A9-46B3-8C6D-3BD044401A1B}.Release|x86.ActiveCfg = Release|x86
{1C593A7D-00A9-46B3-8C6D-3BD044401A1B}.Release|x86.Build.0 = Release|x86
{F38AAF32-C1FB-41EC-9BCB-F9035F5790AB}.Debug|ARM.ActiveCfg = Debug|ARM
{F38AAF32-C1FB-41EC-9BCB-F9035F5790AB}.Debug|ARM.Build.0 = Debug|ARM
{F38AAF32-C1FB-41EC-9BCB-F9035F5790AB}.Release|ARM.ActiveCfg = Release|ARM
Expand All @@ -123,6 +137,10 @@ Global
{1C593A7D-00A9-46B3-8C6D-A06761900ED4}.Debug|ARM.Build.0 = Debug|ARM
{1C593A7D-00A9-46B3-8C6D-A06761900ED4}.Release|ARM.ActiveCfg = Release|ARM
{1C593A7D-00A9-46B3-8C6D-A06761900ED4}.Release|ARM.Build.0 = Release|ARM
{1C593A7D-00A9-46B3-8C6D-3BD044401A1B}.Debug|ARM.ActiveCfg = Debug|ARM
{1C593A7D-00A9-46B3-8C6D-3BD044401A1B}.Debug|ARM.Build.0 = Debug|ARM
{1C593A7D-00A9-46B3-8C6D-3BD044401A1B}.Release|ARM.ActiveCfg = Release|ARM
{1C593A7D-00A9-46B3-8C6D-3BD044401A1B}.Release|ARM.Build.0 = Release|ARM
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
1 change: 1 addition & 0 deletions GSE/Config.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ internal sealed class Config
public bool HideSgbBorder { get; set; }
public bool DarkMode { get; set; } = true;
public bool DisableWin11RoundCorners { get; set; }
public bool EnableDiscordRichPresence { get; set; }

public bool HideStatusBar { get; set; }
public bool HideStatePreviews { get; set; }
Expand Down
3 changes: 2 additions & 1 deletion GSE/GSE.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ static GSE()
/// <summary>
/// OSD manager, may be a status bar or an overlay
/// </summary>
private readonly OSDManager _osdManager;
private readonly OSDManager _osdManager;

/// <summary>
/// The GB controller, used for GB games
Expand Down Expand Up @@ -220,6 +220,7 @@ public void Dispose()
AndroidInput.InputManager = null;
#endif
_postProcessor?.Dispose();
_osdManager?.Dispose();
_emuManager?.Dispose();
_audioManager?.Dispose();
_inputManager?.Dispose();
Expand Down
1 change: 1 addition & 0 deletions GSE/GSE.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
<ProjectReference Include="$(ProjectDir)../GSE.Audio/GSE.Audio.csproj" />
<ProjectReference Include="$(ProjectDir)../GSE.Emu/GSE.Emu.csproj" />
<ProjectReference Include="$(ProjectDir)../GSE.Input/GSE.Input.csproj" />
<ProjectReference Condition="'$(GSE_ANDROID)' != 'true'" Include="$(ProjectDir)../externals/DiscordRPC/DiscordRPC.csproj" />
<ProjectReference Condition="'$(GSE_ANDROID)' == 'true'" Include="$(ProjectDir)../GSE.Android/GSE.Android.csproj" />
</ItemGroup>
<PropertyGroup>
Expand Down
1 change: 1 addition & 0 deletions GSE/Gui/ImGuiMenuBar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public void RunMenuBar()
if (ImGui.MenuItem("Close ROM", emuManager.RomIsLoaded))
{
emuManager.UnloadRom();
osdManager.OnRomUnloaded();
}
#if !GSE_ANDROID
ImGui.Separator();
Expand Down
9 changes: 9 additions & 0 deletions GSE/Gui/ImGuiModals.cs
Original file line number Diff line number Diff line change
Expand Up @@ -806,6 +806,15 @@ static string AddBiosPathButton(string system, string biosPathConfig, ImGuiWindo
}
#endif

#if !GSE_ANDROID
var enableDiscordRichPresence = _config.EnableDiscordRichPresence;
if (ImGui.Checkbox("Enable Discord Rich Presence", ref enableDiscordRichPresence))
{
_config.EnableDiscordRichPresence = enableDiscordRichPresence;
_osdManager.ResetDiscordRichPresence();
}
#endif

ImGui.EndPopup();
}

Expand Down
5 changes: 3 additions & 2 deletions GSE/Gui/LicenseInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,16 @@ public record CopyrightInfo(string Product, string ProductUrl, string CopyrightH
new("GSE", "https://github.com/CasualPokePlayer/GSE", "CasualPokePlayer", MPL20),
new("Gambatte", "https://github.com/pokemon-speedrunning/gambatte-core", "sinamas", GPL20ONLY),
new("mGBA", "https://github.com/mgba-emu/mgba", "Jeffrey Pfau", MPL20),
new("SDL2-CS", "https://github.com/CasualPokePlayer/GSE/blob/master/externals/SDL2-CS/SDL2.cs", "Ethan Lee & CasualPokePlayer", ZLIB),
new("SDL2-CS (forked)", "https://github.com/CasualPokePlayer/GSE/blob/master/externals/SDL2-CS/SDL2.cs", "Ethan Lee & CasualPokePlayer", ZLIB),
new("SDL2", "https://github.com/libsdl-org/SDL", "Sam Lantinga", ZLIB),
new("libusb", "https://github.com/libusb/libusb", "libusb contributors", LGPL21LATER),
new("ImGui.NET", "https://github.com/ImGuiNET/ImGui.NET", "Eric Mellino and ImGui.NET contributors", MIT),
new("cimgui", "https://github.com/cimgui/cimgui", "Stephan Dilly", MIT),
new("Dear ImGui", "https://github.com/ocornut/imgui", "Omar Cornut", MIT),
new("Noto Sans Mono", "https://github.com/notofonts/latin-greek-cyrillic", "The Noto Project Authors", OFL11),
new("SameBoy", "https://github.com/LIJI32/SameBoy", "Lior Halphon", EXPAT),
new("blip_buf", "https://github.com/CasualPokePlayer/GSE/blob/master/GSE.Audio/BlipBuffer.cs", "CasualPokePlayer & Shay Green & EkeEke", LGPL21LATER),
new("blip_buf (forked)", "https://github.com/CasualPokePlayer/GSE/blob/master/GSE.Audio/BlipBuffer.cs", "Shay Green & EkeEke & CasualPokePlayer", LGPL21LATER),
new("DiscordRPC (forked)", "https://github.com/CasualPokePlayer/GSE/blob/master/externals/DiscordRPC", "Lachee & CasualPokePlayer", MIT),
new("SharpCompress", "https://github.com/adamhathcock/sharpcompress", "Adam Hathcock", MIT),
new("ZstdSharp", "https://github.com/oleg-st/ZstdSharp", "Oleg Stepanischev", MIT),
new("CsWin32", "https://github.com/microsoft/CsWin32", "Microsoft Corporation", MIT),
Expand Down
92 changes: 84 additions & 8 deletions GSE/Gui/OSDManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
using System.Diagnostics;
using System.Numerics;

#if !GSE_ANDROID
using DiscordRPC;
#endif
using ImGuiNET;

using static SDL2.SDL;
Expand All @@ -18,27 +21,94 @@ namespace GSE.Gui;
/// <summary>
/// Manages the OSD. Can be done on a status bar or a transparent overlay
/// </summary>
internal sealed class OSDManager(Config config, EmuManager emuManager, SDLRenderer sdlRenderer)
internal sealed class OSDManager : IDisposable
{
// we want an OSD message to stay for around 3 seconds
private static readonly long _osdMessageTime = 3 * Stopwatch.Frequency;

private readonly Config _config;
private readonly EmuManager _emuManager;
#if !GSE_ANDROID
private readonly DiscordRpcClient _discordRpc;
#endif

private readonly ConcurrentQueue<string> _osdMessages = new();
private string _currentOsdMessage;
private long _osdMessageEndTime;
private string _currentRomHash;
private bool _isPsrRom;

private readonly SDLTexture _statePreview = new(sdlRenderer, SDL_PIXELFORMAT_ARGB8888,
SDL_TextureAccess.SDL_TEXTUREACCESS_STREAMING, SDL_ScaleMode.SDL_ScaleModeNearest, SDL_BlendMode.SDL_BLENDMODE_BLEND);
private string _lastRomName;
private DateTime _discordTimestampStart;

private readonly SDLTexture _statePreview;
private long _statePreviewEndTime;
private int _statePreviewSlot;

public bool StatePreviewActive { get; private set; }

public OSDManager(Config config, EmuManager emuManager, SDLRenderer sdlRenderer)
{
_config = config;
_emuManager = emuManager;
_statePreview = new(sdlRenderer, SDL_PIXELFORMAT_ARGB8888,
SDL_TextureAccess.SDL_TEXTUREACCESS_STREAMING, SDL_ScaleMode.SDL_ScaleModeNearest, SDL_BlendMode.SDL_BLENDMODE_BLEND);
#if !GSE_ANDROID
try
{
_discordRpc = new("1323613302793699329");
_discordRpc.Initialize();
ResetDiscordRichPresence();
}
catch
{
Dispose();
throw;
}
#endif
}

public void Dispose()
{
#if !GSE_ANDROID
_discordRpc?.Dispose();
#endif
}

#if !GSE_ANDROID
private void UpdateDiscordRichPresence(string romName)
{
_lastRomName = romName;
if (!_config.EnableDiscordRichPresence)
{
return;
}

var richPresence = new RichPresence
{
Details = romName ?? "No Game Loaded",
Timestamps = new(_discordTimestampStart),
};
_discordRpc.SetPresence(richPresence);
}

public void ResetDiscordRichPresence()
{
if (_config.EnableDiscordRichPresence)
{
_discordTimestampStart = DateTime.UtcNow;
UpdateDiscordRichPresence(_lastRomName);
}
else
{
_discordRpc.SetPresence(null);
}
}
#endif

private string RomInfoPrefix()
{
return $"{(_isPsrRom ? "<PSR> | " : string.Empty)}{emuManager.CurrentGbPlatform} | {_currentRomHash}";
return $"{(_isPsrRom ? "<PSR> | " : string.Empty)}{_emuManager.CurrentGbPlatform} | {_currentRomHash}";
}

public void OnRomLoaded(string romName, ReadOnlySpan<byte> romData)
Expand All @@ -47,13 +117,19 @@ public void OnRomLoaded(string romName, ReadOnlySpan<byte> romData)
var sha256 = Convert.ToHexString(GSEHash.HashDataSHA256(romData));
_isPsrRom = PSRData.GoodRoms.Contains(sha256);
_osdMessages.Enqueue($"{(_isPsrRom ? "<PSR> | " : string.Empty)}{_currentRomHash} | Loaded {romName}");
#if !GSE_ANDROID
UpdateDiscordRichPresence(romName);
#endif
}

public void OnRomUnloaded()
{
_currentRomHash = null;
_isPsrRom = false;
_osdMessages.Enqueue("Unloaded ROM");
#if !GSE_ANDROID
UpdateDiscordRichPresence(null);
#endif
}

public void OnHardReset()
Expand Down Expand Up @@ -103,10 +179,10 @@ public void RunStatusBar()
else
{
// normal status if no OSD message was displayed
if (emuManager.RomIsLoaded)
if (_emuManager.RomIsLoaded)
{
ImGui.TextUnformatted($"v{GSEVersion.FullSemVer} | {RomInfoPrefix()}");
var cycleCountStr = $"{emuManager.GetCycleCount()}";
var cycleCountStr = $"{_emuManager.GetCycleCount()}";
ImGui.SameLine(ImGui.GetWindowWidth() - ImGui.CalcTextSize(cycleCountStr).X - ImGui.GetTextLineHeight());
ImGui.TextUnformatted(cycleCountStr);
}
Expand Down Expand Up @@ -163,7 +239,7 @@ public void RunStatePreviewOverlay()

// we want the preview width to be decently wide
// but we also want the height to cover a percentage of the screen
var previewHeight = (float)Math.Round((bottomSide - topSide) * config.StatePreviewScale / 100.0f);
var previewHeight = (float)Math.Round((bottomSide - topSide) * _config.StatePreviewScale / 100.0f);
var previewWidth = (float)Math.Round(previewHeight * _statePreview.Width / _statePreview.Height);

// the X pos should shift left according to the state slot
Expand All @@ -177,7 +253,7 @@ public void RunStatePreviewOverlay()
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(0, 0));
if (ImGui.Begin("State Preview", ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoBackground))
{
var opacity = config.StatePreviewOpacity / 100.0f;
var opacity = _config.StatePreviewOpacity / 100.0f;
ImGui.Image(_statePreview.TextureId, new(previewWidth, previewHeight), new(0, 0), new(1, 1), new(1, 1, 1, opacity));
}
ImGui.PopStyleVar(3);
Expand Down
27 changes: 27 additions & 0 deletions externals/DiscordRPC/Configuration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Text.Json.Serialization;

namespace DiscordRPC;

/// <summary>
/// Configuration of the current RPC connection
/// </summary>
public sealed class Configuration
{
/// <summary>
/// The Discord API endpoint that should be used.
/// </summary>
[JsonPropertyName("api_endpoint")]
public string ApiEndpoint { get; set; }

/// <summary>
/// The CDN endpoint
/// </summary>
[JsonPropertyName("cdn_host")]
public string CdnHost { get; set; }

/// <summary>
/// The type of environment the connection on. Usually Production.
/// </summary>
[JsonPropertyName("environment")]
public string Environment { get; set; }
}
3 changes: 3 additions & 0 deletions externals/DiscordRPC/DiscordRPC.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../../GSECommon.props" />
</Project>
Loading

0 comments on commit 5e04dcd

Please sign in to comment.