diff --git a/.gitmodules b/.gitmodules index d95e2e5944..f0ba819812 100644 --- a/.gitmodules +++ b/.gitmodules @@ -55,3 +55,6 @@ path = waterbox/mame-arcade/mame url = https://github.com/TASEmulators/mame.git branch = mamehawk0250 +[submodule "ExternalProjects/librcheevos/rcheevos"] + path = ExternalProjects/librcheevos/rcheevos + url = https://github.com/RetroAchievements/rcheevos.git diff --git a/Assets/dll/librcheevos.dll b/Assets/dll/librcheevos.dll new file mode 100644 index 0000000000..ea41b748df Binary files /dev/null and b/Assets/dll/librcheevos.dll differ diff --git a/Assets/dll/librcheevos.so b/Assets/dll/librcheevos.so new file mode 100644 index 0000000000..3a7c5bc20e Binary files /dev/null and b/Assets/dll/librcheevos.so differ diff --git a/Assets/overlay/acherror.wav b/Assets/overlay/acherror.wav new file mode 100644 index 0000000000..dff2627550 Binary files /dev/null and b/Assets/overlay/acherror.wav differ diff --git a/Assets/overlay/info.wav b/Assets/overlay/info.wav new file mode 100644 index 0000000000..67357e2c76 Binary files /dev/null and b/Assets/overlay/info.wav differ diff --git a/Assets/overlay/lb.wav b/Assets/overlay/lb.wav new file mode 100644 index 0000000000..e7a3944580 Binary files /dev/null and b/Assets/overlay/lb.wav differ diff --git a/Assets/overlay/lbcancel.wav b/Assets/overlay/lbcancel.wav new file mode 100644 index 0000000000..8b62297d01 Binary files /dev/null and b/Assets/overlay/lbcancel.wav differ diff --git a/Assets/overlay/login.wav b/Assets/overlay/login.wav new file mode 100644 index 0000000000..3bbf587f0d Binary files /dev/null and b/Assets/overlay/login.wav differ diff --git a/Assets/overlay/message.wav b/Assets/overlay/message.wav new file mode 100644 index 0000000000..7146107f76 Binary files /dev/null and b/Assets/overlay/message.wav differ diff --git a/Assets/overlay/overlayBG.png b/Assets/overlay/overlayBG.png new file mode 100644 index 0000000000..d0ba403b3d Binary files /dev/null and b/Assets/overlay/overlayBG.png differ diff --git a/Assets/overlay/theme-classic.json b/Assets/overlay/theme-classic.json new file mode 100644 index 0000000000..49af6173be --- /dev/null +++ b/Assets/overlay/theme-classic.json @@ -0,0 +1,47 @@ +{ + "Popup": { + "Font": "Tahoma", + "FontSizes": { + "Title": 26, + "Subtitle": 18, + "Detail": 16, + "LeaderboardTitle": 22, + "LeaderboardEntry": 18, + "LeaderboardTracker": 18 + }, + "Colors": { + "Background": "#FB6600", + "NonHardcoreBackground": "#FB6600", + "MasteryBackground": "#FB6600", + "Border": "#A04020", + "TextShadow": "#B45000", + "Title": "#000000", + "Description": "#000000", + "Detail": "#000000", + "Error": "#E0B080", + "LeaderboardEntry": "#000000", + "LeaderboardPlayer": "#E0B080" + } + }, + "Overlay": { + "Font": "Tahoma", + "FontSizes": { + "Title": 32, + "Header": 26, + "Summary": 22 + }, + "Colors": { + "Panel": "#202020", + "Text": "#1166DD", + "DisabledText": "#747A90", + "SubText": "#8C8C8C", + "DisabledSubText": "#606060", + "SelectionBackground": "#16163C", + "SelectionText": "#FFFFFF", + "SelectionDisabledText": "#C8C8C8", + "ScrollBar": "#404040", + "ScrollBarGripper": "#C0C0C0" + } + }, + "Transparent": true +} \ No newline at end of file diff --git a/Assets/overlay/theme-coloredgrey.json b/Assets/overlay/theme-coloredgrey.json new file mode 100644 index 0000000000..82bd4c6620 --- /dev/null +++ b/Assets/overlay/theme-coloredgrey.json @@ -0,0 +1,47 @@ +{ + "Popup": { + "Font": "Tahoma", + "FontSizes": { + "Title": 26, + "Subtitle": 18, + "Detail": 16, + "LeaderboardTitle": 22, + "LeaderboardEntry": 18, + "LeaderboardTracker": 18 + }, + "Colors": { + "Background": "#404040", + "NonHardcoreBackground": "#405068", + "MasteryBackground": "#685820", + "Border": "#202020", + "TextShadow": "#202020", + "Title": "#FFFFFF", + "Description": "#F0E040", + "Detail": "#40C0E0", + "Error": "#E02020", + "LeaderboardEntry": "#B4B4B4", + "LeaderboardPlayer": "#F0E080" + } + }, + "Overlay": { + "Font": "Tahoma", + "FontSizes": { + "Title": 32, + "Header": 26, + "Summary": 22 + }, + "Colors": { + "Panel": "#202020", + "Text": "#1166DD", + "DisabledText": "#747A90", + "SubText": "#8C8C8C", + "DisabledSubText": "#606060", + "SelectionBackground": "#16163C", + "SelectionText": "#FFFFFF", + "SelectionDisabledText": "#C8C8C8", + "ScrollBar": "#404040", + "ScrollBarGripper": "#C0C0C0" + } + }, + "Transparent": true +} \ No newline at end of file diff --git a/Assets/overlay/unlock.wav b/Assets/overlay/unlock.wav new file mode 100644 index 0000000000..2b0c3b1ea4 Binary files /dev/null and b/Assets/overlay/unlock.wav differ diff --git a/ExternalProjects/librcheevos/Makefile b/ExternalProjects/librcheevos/Makefile new file mode 100644 index 0000000000..94f50b4060 --- /dev/null +++ b/ExternalProjects/librcheevos/Makefile @@ -0,0 +1,79 @@ +ROOT_DIR := $(realpath .) +OUTPUTDLL_DIR := $(realpath $(ROOT_DIR)/../../Assets/dll) +OUTPUTDLLCOPY_DIR := $(realpath $(ROOT_DIR)/../../output/dll) + +OUT_DIR := $(ROOT_DIR)/obj +OBJ_DIR := $(OUT_DIR)/release +DOBJ_DIR := $(OUT_DIR)/debug + +CC := gcc +CCFLAGS := -I$(ROOT_DIR)/rcheevos/include -Wall -Wextra -std=c89 \ + -Wno-implicit-fallthrough -Wno-missing-field-initializers \ + -Wno-unused-parameter -Wno-maybe-uninitialized -DRC_DISABLE_LUA + +ifeq ($(OS),Windows_NT) +TARGET := librcheevos.dll +else +TARGET := librcheevos.so +endif + +# rc_libretro.c wants libretro.h which is expected by the builder to provide (and that file is huge) +# not needed for our purposes anyways, so don't bother +SRCS := $(filter-out %rc_libretro.c,$(shell find $(ROOT_DIR)/rcheevos/src -type f -name '*.c')) + +LDFLAGS := -shared +CCFLAGS_DEBUG := -O0 -g +CCFLAGS_RELEASE := -O3 -flto +LDFLAGS_DEBUG := +LDFLAGS_RELEASE := -s + +_OBJS := $(addsuffix .o,$(realpath $(SRCS))) +OBJS := $(patsubst $(ROOT_DIR)%,$(OBJ_DIR)%,$(_OBJS)) +DOBJS := $(patsubst $(ROOT_DIR)%,$(DOBJ_DIR)%,$(_OBJS)) + +$(OBJ_DIR)/%.c.o: %.c + @echo cc $< + @mkdir -p $(@D) + @$(CC) -c -o $@ $< $(CCFLAGS) $(CCFLAGS_RELEASE) +$(DOBJ_DIR)/%.c.o: %.c + @echo cc $< + @mkdir -p $(@D) + @$(CC) -c -o $@ $< $(CCFLAGS) $(CCFLAGS_DEBUG) + +.DEFAULT_GOAL := install + +TARGET_RELEASE := $(OBJ_DIR)/$(TARGET) +TARGET_DEBUG := $(DOBJ_DIR)/$(TARGET) + +.PHONY: release debug install install-debug + +release: $(TARGET_RELEASE) +debug: $(TARGET_DEBUG) + +$(TARGET_RELEASE): $(OBJS) + @echo ld $@ + @$(CC) -o $@ $(LDFLAGS) $(LDFLAGS_RELEASE) $(CCFLAGS) $(CCFLAGS_RELEASE) $(OBJS) +$(TARGET_DEBUG): $(DOBJS) + @echo ld $@ + @$(CC) -o $@ $(LDFLAGS) $(LDFLAGS_DEBUG) $(CCFLAGS) $(CCFLAGS_DEBUG) $(DOBJS) + +install: $(TARGET_RELEASE) + @cp -f $< $(OUTPUTDLL_DIR) + @cp $(OUTPUTDLL_DIR)/$(TARGET) $(OUTPUTDLLCOPY_DIR)/$(TARGET) || true + @echo Release build of $(TARGET) installed. + +install-debug: $(TARGET_DEBUG) + @cp -f $< $(OUTPUTDLL_DIR) + @cp $(OUTPUTDLL_DIR)/$(TARGET) $(OUTPUTDLLCOPY_DIR)/$(TARGET) || true + @echo Debug build of $(TARGET) installed. + +.PHONY: clean clean-release clean-debug +clean: + rm -rf $(OUT_DIR) +clean-release: + rm -rf $(OUT_DIR)/release +clean-debug: + rm -rf $(OUT_DIR)/debug + +-include $(OBJS:%o=%d) +-include $(DOBJS:%o=%d) diff --git a/ExternalProjects/librcheevos/rcheevos b/ExternalProjects/librcheevos/rcheevos new file mode 160000 index 0000000000..165d1a3b18 --- /dev/null +++ b/ExternalProjects/librcheevos/rcheevos @@ -0,0 +1 @@ +Subproject commit 165d1a3b189c472c4547ac385e59be2f527e688e diff --git a/src/BizHawk.BizInvoke/BizInvokeUtilities.cs b/src/BizHawk.BizInvoke/BizInvokeUtilities.cs index bf9196b803..0b9893bba1 100644 --- a/src/BizHawk.BizInvoke/BizInvokeUtilities.cs +++ b/src/BizHawk.BizInvoke/BizInvokeUtilities.cs @@ -84,6 +84,10 @@ namespace BizHawk.BizInvoke { return new CustomAttributeBuilder(t.GetConstructor(Type.EmptyTypes)!, Array.Empty()); } + else if (t == typeof(MarshalAsAttribute)) + { + return new CustomAttributeBuilder(t.GetConstructor(new[] { typeof(UnmanagedType) })!, new object[] { ((MarshalAsAttribute)o).Value }); + } throw new InvalidOperationException($"Unknown parameter attribute {t.Name}"); } diff --git a/src/BizHawk.BizInvoke/BizInvoker.cs b/src/BizHawk.BizInvoke/BizInvoker.cs index e05e58f86b..56ddf19bec 100644 --- a/src/BizHawk.BizInvoke/BizInvoker.cs +++ b/src/BizHawk.BizInvoke/BizInvoker.cs @@ -388,12 +388,20 @@ namespace BizHawk.BizInvoke pli.EmitLoad(); } + bool WantsWinAPIBool() + { + var attrs = baseMethod.ReturnTypeCustomAttributes.GetCustomAttributes(typeof(MarshalAsAttribute), false); + return attrs.Length > 0 && ((MarshalAsAttribute)attrs[0]).Value is UnmanagedType.Bool; + } + il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldfld, field); il.EmitCalli( OpCodes.Calli, nativeCall, - returnType == typeof(bool) ? typeof(byte) : returnType, // undo winapi style bool garbage + returnType != typeof(bool) || WantsWinAPIBool() + ? returnType + : typeof(byte), // undo winapi style bool garbage by default paramLoadInfos.Select(p => p.NativeType).ToArray()); if (monitorField != null) // monitor: finally exit diff --git a/src/BizHawk.Client.Common/config/Binding.cs b/src/BizHawk.Client.Common/config/Binding.cs index be24a04dbb..4f4a2ad3f9 100644 --- a/src/BizHawk.Client.Common/config/Binding.cs +++ b/src/BizHawk.Client.Common/config/Binding.cs @@ -177,6 +177,15 @@ namespace BizHawk.Client.Common Bind("NDS", "Previous Screen Layout"); Bind("NDS", "Screen Rotate"); + Bind("RAIntegration", "Open RA Overlay", "Escape"); + Bind("RAIntegration", "RA Up", "Up"); + Bind("RAIntegration", "RA Down", "Down"); + Bind("RAIntegration", "RA Left", "Left"); + Bind("RAIntegration", "RA Right", "Right"); + Bind("RAIntegration", "RA Confirm", "X"); + Bind("RAIntegration", "RA Cancel", "Z"); + Bind("RAIntegration", "RA Quit", "Backspace"); + AllHotkeys = dict; Groupings = dict.Values.Select(static info => info.TabGroup).Distinct().ToList(); } diff --git a/src/BizHawk.Client.Common/config/Config.cs b/src/BizHawk.Client.Common/config/Config.cs index 1780a5fbe4..04bdf6ea5a 100644 --- a/src/BizHawk.Client.Common/config/Config.cs +++ b/src/BizHawk.Client.Common/config/Config.cs @@ -358,7 +358,19 @@ namespace BizHawk.Client.Common public int OSDMessageDuration { get; set; } = 2; public Queue RecentCores { get; set; } = new(); - + public Dictionary TrustedExtTools { get; set; } = new(); + + // RetroAchievements settings + public bool SkipRATelemetryWarning { get; set; } + public string RAUsername { get; set; } = ""; + public string RAToken { get; set; } = ""; + public bool RACheevosActive { get; set; } = true; + public bool RALBoardsActive { get; set; } + public bool RARichPresenceActive { get; set; } = true; + public bool RAHardcoreMode { get; set; } + public bool RASoundEffects { get; set; } = true; + public bool RAAllowUnofficialCheevos { get; set; } + public bool RAAutostart { get; set; } } } diff --git a/src/BizHawk.Client.EmuHawk/BizHawk.Client.EmuHawk.csproj b/src/BizHawk.Client.EmuHawk/BizHawk.Client.EmuHawk.csproj index 9a7c16fd59..9b02dd9f6e 100755 --- a/src/BizHawk.Client.EmuHawk/BizHawk.Client.EmuHawk.csproj +++ b/src/BizHawk.Client.EmuHawk/BizHawk.Client.EmuHawk.csproj @@ -297,6 +297,11 @@ + + + + + diff --git a/src/BizHawk.Client.EmuHawk/MainForm.Designer.cs b/src/BizHawk.Client.EmuHawk/MainForm.Designer.cs index 3748cd8e06..0117d5b847 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.Designer.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.Designer.cs @@ -214,6 +214,8 @@ namespace BizHawk.Client.EmuHawk this.ExternalToolMenuItem = new BizHawk.WinForms.Controls.ToolStripMenuItemEx(); this.dummyExternalTool = new BizHawk.WinForms.Controls.ToolStripMenuItemEx(); this.BatchRunnerMenuItem = new BizHawk.WinForms.Controls.ToolStripMenuItemEx(); + this.RetroAchievementsMenuItem = new BizHawk.WinForms.Controls.ToolStripMenuItemEx(); + this.StartRetroAchievementsMenuItem = new BizHawk.WinForms.Controls.ToolStripMenuItemEx(); this.NESSubMenu = new BizHawk.WinForms.Controls.ToolStripMenuItemEx(); this.NESPPUViewerMenuItem = new BizHawk.WinForms.Controls.ToolStripMenuItemEx(); this.NESNametableViewerMenuItem = new BizHawk.WinForms.Controls.ToolStripMenuItemEx(); @@ -1368,6 +1370,7 @@ namespace BizHawk.Client.EmuHawk this.MacroToolMenuItem, this.VirtualPadMenuItem, this.BasicBotMenuItem, + this.RetroAchievementsMenuItem, this.toolStripSeparator11, this.CheatsMenuItem, this.GameSharkConverterMenuItem, @@ -1438,6 +1441,17 @@ namespace BizHawk.Client.EmuHawk this.BasicBotMenuItem.Text = "Basic Bot"; this.BasicBotMenuItem.Click += new System.EventHandler(this.BasicBotMenuItem_Click); // + // RetroAchievementsMenuItem + // + this.RetroAchievementsMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { + StartRetroAchievementsMenuItem}); + this.RetroAchievementsMenuItem.Text = "&RetroAchievements"; + // + // StartRetroAchievementsMenuItem + // + this.StartRetroAchievementsMenuItem.Text = "&Start RetroAchievements"; + this.StartRetroAchievementsMenuItem.Click += new System.EventHandler(this.StartRetroAchievementsMenuItem_Click); + // // CheatsMenuItem // this.CheatsMenuItem.Text = "Cheats"; @@ -2459,6 +2473,8 @@ namespace BizHawk.Client.EmuHawk private BizHawk.WinForms.Controls.ToolStripMenuItemEx ViewSubMenu; private BizHawk.WinForms.Controls.ToolStripMenuItemEx ConfigSubMenu; private BizHawk.WinForms.Controls.ToolStripMenuItemEx ToolsSubMenu; + private BizHawk.WinForms.Controls.ToolStripMenuItemEx RetroAchievementsMenuItem; + private BizHawk.WinForms.Controls.ToolStripMenuItemEx StartRetroAchievementsMenuItem; private BizHawk.WinForms.Controls.ToolStripMenuItemEx HelpSubMenu; private BizHawk.WinForms.Controls.ToolStripMenuItemEx PauseMenuItem; private BizHawk.WinForms.Controls.ToolStripSeparatorEx toolStripSeparator1; diff --git a/src/BizHawk.Client.EmuHawk/MainForm.Events.cs b/src/BizHawk.Client.EmuHawk/MainForm.Events.cs index 7217bfa0d4..3d7ca0a3f4 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.Events.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.Events.cs @@ -1314,6 +1314,11 @@ namespace BizHawk.Client.EmuHawk form.ShowDialog(); } + private void StartRetroAchievementsMenuItem_Click(object sender, EventArgs e) + { + OpenRetroAchievements(); + } + private void NesSubMenu_DropDownOpened(object sender, EventArgs e) { var boardName = Emulator.HasBoardInfo() ? Emulator.AsBoardInfo().BoardName : null; diff --git a/src/BizHawk.Client.EmuHawk/MainForm.Hotkey.cs b/src/BizHawk.Client.EmuHawk/MainForm.Hotkey.cs index 522651e77c..4f5c70bcb0 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.Hotkey.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.Hotkey.cs @@ -589,6 +589,14 @@ namespace BizHawk.Client.EmuHawk case "Turbo": case "Rewind": case "Fast Forward": + case "Open RA Overlay": + case "RA Up": + case "RA Down": + case "RA Left": + case "RA Right": + case "RA Confirm": + case "RA Cancel": + case "RA Quit": return true; } } diff --git a/src/BizHawk.Client.EmuHawk/MainForm.cs b/src/BizHawk.Client.EmuHawk/MainForm.cs index 0da0f315d6..045fcced7f 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.cs @@ -48,7 +48,7 @@ using BizHawk.WinForms.Controls; namespace BizHawk.Client.EmuHawk { - public partial class MainForm : FormBase, IDialogParent, IMainFormForApi, IMainFormForTools + public partial class MainForm : FormBase, IDialogParent, IMainFormForApi, IMainFormForTools, IMainFormForRetroAchievements { private static readonly FilesystemFilterSet EmuHawkSaveStatesFSFilterSet = new(FilesystemFilter.EmuHawkSaveStates); @@ -698,6 +698,11 @@ namespace BizHawk.Client.EmuHawk SynchChrome(); + if (Config.RAAutostart) + { + OpenRetroAchievements(); + } + _presentationPanel.Control.Paint += (o, e) => { // I would like to trigger a repaint here, but this isn't done yet @@ -838,6 +843,9 @@ namespace BizHawk.Client.EmuHawk DisplayManager = null; } + RA?.Dispose(); + RA = null; + if (disposing) { components?.Dispose(); @@ -2970,6 +2978,7 @@ namespace BizHawk.Client.EmuHawk ExtToolManager.Restart(Config); Sound.Config = Config; DisplayManager.UpdateGlobals(Config, Emulator); + RA?.Restart(); AddOnScreenMessage($"Config file loaded: {iniPath}"); } @@ -3010,6 +3019,7 @@ namespace BizHawk.Client.EmuHawk runFrame = true; } + RA?.Update(); bool oldFrameAdvanceCondition = InputManager.ClientControls["Frame Advance"] || PressFrameAdvance || HoldFrameAdvance; if (FrameInch) @@ -3132,6 +3142,8 @@ namespace BizHawk.Client.EmuHawk MovieSession.HandleFrameBefore(); + RA?.OnFrameAdvance(); + if (Config.AutosaveSaveRAM) { if (AutoFlushSaveRamIn-- <= 0) @@ -4006,6 +4018,7 @@ namespace BizHawk.Client.EmuHawk UpdateCoreStatusBarButton(); UpdateDumpInfo(); SetMainformMovieInfo(); + RA?.Restart(); } private void CommitCoreSettingsToConfig() @@ -4071,6 +4084,8 @@ namespace BizHawk.Client.EmuHawk StopMovie(); } + RA?.Stop(); + CheatList.SaveOnClose(); Emulator.Dispose(); Emulator = new NullEmulator(); @@ -4210,6 +4225,7 @@ namespace BizHawk.Client.EmuHawk { OSD.ClearGuiText(); EmuClient.OnStateLoaded(this, userFriendlyStateName); + RA?.OnLoadState(path); if (Tools.Has()) { @@ -4288,6 +4304,7 @@ namespace BizHawk.Client.EmuHawk new SavestateFile(Emulator, MovieSession, QuickBmpFile, MovieSession.UserBag).Create(path, Config.Savestates); EmuClient.OnStateSaved(this, userFriendlyStateName); + RA?.OnSaveState(path); if (!suppressOSD) { @@ -4885,5 +4902,20 @@ namespace BizHawk.Client.EmuHawk } public IQuickBmpFile QuickBmpFile { get; } = EmuHawk.QuickBmpFile.INSTANCE; + + private IRetroAchievements RA { get; set; } + + private void OpenRetroAchievements() + { + RA = RetroAchievements.CreateImpl(this, InputManager, Tools, () => Config, RetroAchievementsMenuItem.DropDownItems, () => + { + RA.Dispose(); + RA = null; + RetroAchievementsMenuItem.DropDownItems.Clear(); + RetroAchievementsMenuItem.DropDownItems.Add(StartRetroAchievementsMenuItem); + }); + + RA?.Restart(); + } } } diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/IMainFormForRetroAchievements.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/IMainFormForRetroAchievements.cs new file mode 100644 index 0000000000..ff3407fdd4 --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/IMainFormForRetroAchievements.cs @@ -0,0 +1,38 @@ +using System; + +using BizHawk.Client.Common; +using BizHawk.Emulation.Common; + +namespace BizHawk.Client.EmuHawk +{ + public interface IMainFormForRetroAchievements : IDialogController + { + LoadRomArgs CurrentlyOpenRomArgs { get; } + + EmuClientApi EmuClient { get; } + + IEmulator Emulator { get; } + + bool FrameInch { get; set; } + + bool FastForward { get; set; } + + GameInfo Game { get; } + + IntPtr Handle { get; } + + IMovieSession MovieSession { get; } + + SettingsAdapter GetSettingsAdapterForLoadedCoreUntyped(); + + bool LoadRom(string path, LoadRomArgs args); + + void PauseEmulator(); + + bool RebootCore(); + + void UpdateWindowTitle(); + + void UnpauseEmulator(); + } +} diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/IRetroAchievements.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/IRetroAchievements.cs new file mode 100644 index 0000000000..916cfed5b8 --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/IRetroAchievements.cs @@ -0,0 +1,43 @@ +using System; + +namespace BizHawk.Client.EmuHawk +{ + public interface IRetroAchievements : IDisposable + { + /// + /// General update, not tied to frame advance + /// Use this for overlay / checking hardcore mode + /// + void Update(); + + /// + /// Called on frame advance, process achievements here + /// + void OnFrameAdvance(); + + /// + /// Call this whenever the ROM changes (and after ctor) + /// Make sure CurrentlyOpenRomArgs is updated before calling this! + /// + void Restart(); + + /// + /// Call this before the emulator is disposed + /// + void Stop(); + + /// + /// Call this when a state is saved + /// In memory states (like with rewind) do not need to call this + /// + /// path to .State file + void OnSaveState(string path); + + /// + /// Call this when a state is loaded + /// In memory states (like with rewind) do not need to call this + /// + /// path to .State file + void OnLoadState(string path); + } +} diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/LibRCheevos.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/LibRCheevos.cs new file mode 100644 index 0000000000..e070ab0867 --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/LibRCheevos.cs @@ -0,0 +1,715 @@ +using System; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Windows.Forms; + +using BizHawk.BizInvoke; +using BizHawk.Common; +using BizHawk.Client.Common; +using BizHawk.Emulation.Common; + +#pragma warning disable IDE1006 // Naming Styles + +namespace BizHawk.Client.EmuHawk +{ + public abstract class LibRCheevos + { + private const CallingConvention cc = CallingConvention.Cdecl; + + public enum rc_error_t : int + { + RC_OK = 0, + RC_INVALID_LUA_OPERAND = -1, + RC_INVALID_MEMORY_OPERAND = -2, + RC_INVALID_CONST_OPERAND = -3, + RC_INVALID_FP_OPERAND = -4, + RC_INVALID_CONDITION_TYPE = -5, + RC_INVALID_OPERATOR = -6, + RC_INVALID_REQUIRED_HITS = -7, + RC_DUPLICATED_START = -8, + RC_DUPLICATED_CANCEL = -9, + RC_DUPLICATED_SUBMIT = -10, + RC_DUPLICATED_VALUE = -11, + RC_DUPLICATED_PROGRESS = -12, + RC_MISSING_START = -13, + RC_MISSING_CANCEL = -14, + RC_MISSING_SUBMIT = -15, + RC_MISSING_VALUE = -16, + RC_INVALID_LBOARD_FIELD = -17, + RC_MISSING_DISPLAY_STRING = -18, + RC_OUT_OF_MEMORY = -19, + RC_INVALID_VALUE_FLAG = -20, + RC_MISSING_VALUE_MEASURED = -21, + RC_MULTIPLE_MEASURED = -22, + RC_INVALID_MEASURED_TARGET = -23, + RC_INVALID_COMPARISON = -24, + RC_INVALID_STATE = -25, + RC_INVALID_JSON = -26 + } + + public enum rc_runtime_event_type_t : byte + { + RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, + RC_RUNTIME_EVENT_ACHIEVEMENT_PAUSED, + RC_RUNTIME_EVENT_ACHIEVEMENT_RESET, + RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, + RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED, + RC_RUNTIME_EVENT_LBOARD_STARTED, + RC_RUNTIME_EVENT_LBOARD_CANCELED, + RC_RUNTIME_EVENT_LBOARD_UPDATED, + RC_RUNTIME_EVENT_LBOARD_TRIGGERED, + RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED, + RC_RUNTIME_EVENT_LBOARD_DISABLED, + RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED + } + + public enum rc_api_image_type_t : int + { + RC_IMAGE_TYPE_GAME = 1, + RC_IMAGE_TYPE_ACHIEVEMENT, + RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED, + RC_IMAGE_TYPE_USER, + } + + public enum rc_runtime_achievement_category_t : int + { + RC_ACHIEVEMENT_CATEGORY_CORE = 3, + RC_ACHIEVEMENT_CATEGORY_UNOFFICIAL = 5, + } + + [StructLayout(LayoutKind.Sequential)] + public struct rc_runtime_t + { + public IntPtr triggers; + public int trigger_count; + public int trigger_capacity; + public IntPtr lboards; + public int lboard_count; + public int lboard_capacity; + public IntPtr richpresence; + public IntPtr memrefs; + public IntPtr next_memref; + public IntPtr variables; + public IntPtr next_variable; + } + + [StructLayout(LayoutKind.Sequential)] + public struct rc_runtime_event_t + { + public int id; + public int value; + public rc_runtime_event_type_t type; + } + + [StructLayout(LayoutKind.Sequential)] + public unsafe struct rc_api_buffer_t + { + public IntPtr write; + public IntPtr end; + public IntPtr next; + public fixed byte data[256]; + } + + [StructLayout(LayoutKind.Sequential)] + public struct rc_api_request_t + { + public IntPtr url; + public IntPtr post_data; + public rc_api_buffer_t buffer; + + public string URL => Mershul.PtrToStringUtf8(url); + public string PostData => Mershul.PtrToStringUtf8(post_data); + } + + [StructLayout(LayoutKind.Sequential)] + public struct rc_api_response_t + { + public int succeeded; + public IntPtr error_message; + public rc_api_buffer_t buffer; + + public string ErrorMessage => Mershul.PtrToStringUtf8(error_message); + } + + [StructLayout(LayoutKind.Sequential)] + public struct rc_api_fetch_user_unlocks_response_t + { + public IntPtr achievement_ids; + public int num_achievement_ids; + public rc_api_response_t response; + } + + [StructLayout(LayoutKind.Sequential)] + public struct rc_api_login_response_t + { + public IntPtr username; + public IntPtr api_token; + public int score; + public int num_unread_messages; + public IntPtr display_name; + public rc_api_response_t response; + + public string Username => Mershul.PtrToStringUtf8(username); + public string ApiToken => Mershul.PtrToStringUtf8(api_token); + public string DisplayName => Mershul.PtrToStringUtf8(display_name); + } + + [StructLayout(LayoutKind.Sequential)] + public struct rc_api_start_session_response_t + { + public rc_api_response_t response; + } + + [StructLayout(LayoutKind.Sequential)] + public struct rc_api_fetch_user_unlocks_request_t + { + public string username; + public string api_token; + public int game_id; + public int hardcore; + + public rc_api_fetch_user_unlocks_request_t(string username, string api_token, int game_id, bool hardcore) + { + this.username = username; + this.api_token = api_token; + this.game_id = game_id; + this.hardcore = hardcore ? 1 : 0; + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct rc_api_login_request_t + { + public string username; + public string api_token; + public string password; + + public rc_api_login_request_t(string username, string api_token, string password) + { + this.username = username; + this.api_token = api_token; + this.password = password; + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct rc_api_start_session_request_t + { + public string username; + public string api_token; + public int game_id; + + public rc_api_start_session_request_t(string username, string api_token, int game_id) + { + this.username = username; + this.api_token = api_token; + this.game_id = game_id; + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct rc_api_award_achievement_response_t + { + public int awarded_achievement_id; + public int new_player_score; + public int achievements_remaining; + public rc_api_response_t response; + } + + [StructLayout(LayoutKind.Sequential)] + public struct rc_api_achievement_definition_t + { + public int id; + public int points; + public rc_runtime_achievement_category_t category; + public IntPtr title; + public IntPtr description; + public IntPtr definition; + public IntPtr author; + public IntPtr badge_name; + public long created; // time_t? + public long updated; // time_t? + + public string Title => Mershul.PtrToStringUtf8(title); + public string Description => Mershul.PtrToStringUtf8(description); + public string Definition => Mershul.PtrToStringUtf8(definition); + public string Author => Mershul.PtrToStringUtf8(author); + public string BadgeName => Mershul.PtrToStringUtf8(badge_name); + } + + [StructLayout(LayoutKind.Sequential)] + public struct rc_api_leaderboard_definition_t + { + public int id; + public int format; + public IntPtr title; + public IntPtr description; + public IntPtr definition; + public int lower_is_better; + public int hidden; + + public string Title => Mershul.PtrToStringUtf8(title); + public string Description => Mershul.PtrToStringUtf8(description); + public string Definition => Mershul.PtrToStringUtf8(definition); + } + + [StructLayout(LayoutKind.Sequential)] + public struct rc_api_fetch_game_data_response_t + { + public int id; + public RetroAchievements.ConsoleID console_id; + public IntPtr title; + public IntPtr image_name; + public IntPtr rich_presence_script; + public IntPtr achievements; + public int num_achievements; + public IntPtr leaderboards; + public int num_leaderboards; + public rc_api_response_t response; + + public string Title => Mershul.PtrToStringUtf8(title); + public string ImageName => Mershul.PtrToStringUtf8(image_name); + public string RichPresenceScript => Mershul.PtrToStringUtf8(rich_presence_script); + } + + [StructLayout(LayoutKind.Sequential)] + public struct rc_api_ping_response_t + { + public rc_api_response_t response; + } + + [StructLayout(LayoutKind.Sequential)] + public struct rc_api_resolve_hash_response_t + { + public int game_id; + public rc_api_response_t response; + } + + [StructLayout(LayoutKind.Sequential)] + public struct rc_api_submit_lboard_entry_response_t + { + public int submitted_score; + public int best_score; + public int new_rank; + public int num_entries; + public IntPtr top_entries; + public int num_top_entries; + public rc_api_response_t response; + } + + [StructLayout(LayoutKind.Sequential)] + public struct rc_api_award_achievement_request_t + { + public string username; + public string api_token; + public int achievement_id; + public int hardcore; + public string game_hash; + + public rc_api_award_achievement_request_t(string username, string api_token, int achievement_id, bool hardcore, string game_hash) + { + this.username = username; + this.api_token = api_token; + this.achievement_id = achievement_id; + this.hardcore = hardcore ? 1 : 0; + this.game_hash = game_hash; + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct rc_api_fetch_game_data_request_t + { + public string username; + public string api_token; + public int game_id; + + public rc_api_fetch_game_data_request_t(string username, string api_token, int game_id) + { + this.username = username; + this.api_token = api_token; + this.game_id = game_id; + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct rc_api_fetch_image_request_t + { + public string image_name; + public rc_api_image_type_t image_type; + + public rc_api_fetch_image_request_t(string image_name, rc_api_image_type_t image_type) + { + this.image_name = image_name; + this.image_type = image_type; + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct rc_api_ping_request_t + { + public string username; + public string api_token; + public int game_id; + public string rich_presence; + + public rc_api_ping_request_t(string username, string api_token, int game_id, string rich_presence) + { + this.username = username; + this.api_token = api_token; + this.game_id = game_id; + this.rich_presence = rich_presence; + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct rc_api_resolve_hash_request_t + { + public string username; // note: not actually used + public string api_token; // note: not actually used + public string game_hash; + + public rc_api_resolve_hash_request_t(string username, string api_token, string game_hash) + { + this.username = username; + this.api_token = api_token; + this.game_hash = game_hash; + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct rc_api_submit_lboard_entry_request_t + { + public string username; + public string api_token; + public int leaderboard_id; + public int score; + public string game_hash; + + public rc_api_submit_lboard_entry_request_t(string username, string api_token, int leaderboard_id, int score, string game_hash) + { + this.username = username; + this.api_token = api_token; + this.leaderboard_id = leaderboard_id; + this.score = score; + this.game_hash = game_hash; + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct rc_api_fetch_achievement_info_response_t + { + public int id; + public int game_id; + public int num_awarded; + public int num_players; + public IntPtr recently_awarded; + public int num_recently_awarded; + public rc_api_response_t response; + } + + [StructLayout(LayoutKind.Sequential)] + public struct rc_api_fetch_games_list_response_t + { + public IntPtr entries; + public int num_entries; + public rc_api_response_t response; + } + + [StructLayout(LayoutKind.Sequential)] + public struct rc_api_fetch_leaderboard_info_response_t + { + public int id; + public int format; + public int lower_is_better; + public IntPtr title; + public IntPtr description; + public IntPtr definition; + public int game_id; + public IntPtr author; + public long created; // time_t? + public long updated; // time_t? + public IntPtr entries; + public int num_entries; + public rc_api_response_t response; + + public string Title => Mershul.PtrToStringUtf8(title); + public string Description => Mershul.PtrToStringUtf8(description); + public string Definition => Mershul.PtrToStringUtf8(definition); + } + + [StructLayout(LayoutKind.Sequential)] + public struct rc_api_fetch_achievement_info_request_t + { + public string username; + public string api_token; + public int achievement_id; + public int first_entry; + public int count; + public int friends_only; + + public rc_api_fetch_achievement_info_request_t(string username, string api_token, int achievement_id, int first_entry, int count, int friends_only) + { + this.username = username; + this.api_token = api_token; + this.achievement_id = achievement_id; + this.first_entry = first_entry; + this.count = count; + this.friends_only = friends_only; + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct rc_api_fetch_games_list_request_t + { + public int console_id; + + public rc_api_fetch_games_list_request_t(int console_id) + { + this.console_id = console_id; + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct rc_api_fetch_leaderboard_info_request_t + { + public int leaderboard_id; + public int count; + public int first_entry; + public string username; + + public rc_api_fetch_leaderboard_info_request_t(int leaderboard_id, int count, int first_entry, string username) + { + this.leaderboard_id = leaderboard_id; + this.count = count; + this.first_entry = first_entry; + this.username = username; + } + } + + [UnmanagedFunctionPointer(cc)] + public delegate void rc_runtime_event_handler_t(IntPtr runtime_event); + + [UnmanagedFunctionPointer(cc)] + public delegate int rc_peek_t(int address, int num_bytes, IntPtr ud); + + [UnmanagedFunctionPointer(cc)] + [return: MarshalAs(UnmanagedType.Bool)] + public delegate bool rc_runtime_validate_address_t(int address); + + [UnmanagedFunctionPointer(cc)] + public delegate void rc_hash_message_callback(string message); + + [BizImport(cc)] + public abstract IntPtr rc_console_memory_regions(RetroAchievements.ConsoleID id); + + [BizImport(cc)] + public abstract IntPtr rc_console_name(RetroAchievements.ConsoleID id); + + [BizImport(cc)] + public abstract IntPtr rc_error_str(rc_error_t error_code); + + [BizImport(cc)] + public abstract void rc_runtime_init(ref rc_runtime_t runtime); + + [BizImport(cc)] + public abstract void rc_runtime_destroy(ref rc_runtime_t runtime); + + [BizImport(cc)] + public abstract void rc_runtime_reset(ref rc_runtime_t runtime); + + [BizImport(cc)] + public abstract void rc_runtime_do_frame(ref rc_runtime_t runtime, rc_runtime_event_handler_t rc_runtime_event_handler_t, rc_peek_t peek, IntPtr ud, IntPtr unused); + + [BizImport(cc)] + public abstract rc_error_t rc_runtime_progress_size(ref rc_runtime_t runtime, IntPtr unused); + + [BizImport(cc)] + public abstract void rc_runtime_serialize_progress(byte[] buffer, ref rc_runtime_t runtime, IntPtr unused); + + [BizImport(cc)] + public abstract rc_error_t rc_runtime_deserialize_progress(ref rc_runtime_t runtime, byte[] serialized, IntPtr unused); + + [BizImport(cc)] + public abstract void rc_runtime_invalidate_address(ref rc_runtime_t runtime, int address); + + [BizImport(cc)] + public abstract void rc_runtime_validate_addresses(ref rc_runtime_t runtime, rc_runtime_event_handler_t event_handler, rc_runtime_validate_address_t validate_handler); + + [BizImport(cc)] + public abstract rc_error_t rc_runtime_activate_achievement(ref rc_runtime_t runtime, int id, string memaddr, IntPtr unused, int unused_idx); + + [BizImport(cc)] + public abstract rc_error_t rc_runtime_activate_lboard(ref rc_runtime_t runtime, int id, string memaddr, IntPtr unused, int unused_idx); + + [BizImport(cc)] + public abstract rc_error_t rc_runtime_activate_richpresence(ref rc_runtime_t runtime, string script, IntPtr unused, int unused_idx); + + [BizImport(cc)] + public abstract IntPtr rc_runtime_get_achievement(ref rc_runtime_t runtime, int id); + + [BizImport(cc)] + public abstract IntPtr rc_runtime_get_lboard(ref rc_runtime_t runtime, int id); + + [BizImport(cc)] + public abstract int rc_runtime_get_richpresence(ref rc_runtime_t runtime, byte[] buffer, int buffersize, rc_peek_t peek, IntPtr ud, IntPtr unused); + + [BizImport(cc)] + [return: MarshalAs(UnmanagedType.Bool)] + public abstract bool rc_runtime_get_achievement_measured(ref rc_runtime_t runtime, int id, out int measured_value, out int measured_target); + + [BizImport(cc)] + public abstract int rc_runtime_format_achievement_measured(ref rc_runtime_t runtime, int id, byte[] buffer, long buffer_size); + + [BizImport(cc)] + public abstract int rc_runtime_format_lboard_value(byte[] buffer, int size, int value, int format); + + [BizImport(cc)] + public abstract void rc_runtime_deactivate_achievement(ref rc_runtime_t runtime, int id); + + [BizImport(cc)] + public abstract void rc_runtime_deactivate_lboard(ref rc_runtime_t runtime, int id); + + [BizImport(cc)] + public abstract void rc_hash_init_error_message_callback(rc_hash_message_callback callback); + + [BizImport(cc)] + public abstract void rc_hash_init_verbose_message_callback(rc_hash_message_callback callback); + + [BizImport(cc)] + public abstract void rc_hash_init_custom_cdreader(IntPtr reader); + + [BizImport(cc)] + public abstract void rc_hash_init_custom_filereader(IntPtr reader); + + [BizImport(cc)] + [return: MarshalAs(UnmanagedType.Bool)] + public abstract bool rc_hash_generate_from_buffer(byte[] hash, RetroAchievements.ConsoleID console_id, byte[] buffer, long buffer_size); + + [BizImport(cc)] + [return: MarshalAs(UnmanagedType.Bool)] + public abstract bool rc_hash_generate_from_file(byte[] hash, RetroAchievements.ConsoleID console_id, string path); + + [BizImport(cc)] + public abstract void rc_hash_initialize_iterator(IntPtr iterator, string path, byte[] buffer, long buffer_size); + + [BizImport(cc)] + [return: MarshalAs(UnmanagedType.Bool)] + public abstract bool rc_hash_iterate(byte[] hash, IntPtr iterator); + + [BizImport(cc)] + public abstract void rc_hash_destroy_iterator(IntPtr iterator); + + [BizImport(cc)] + public abstract void rc_api_set_host(string hostname); + + [BizImport(cc)] + public abstract void rc_api_set_image_host(string hostname); + + [BizImport(cc, Compatibility = true)] + public abstract rc_error_t rc_api_init_login_request(out rc_api_request_t request, ref rc_api_login_request_t api_params); + + [BizImport(cc, Compatibility = true)] + public abstract rc_error_t rc_api_init_start_session_request(out rc_api_request_t request, ref rc_api_start_session_request_t api_params); + + [BizImport(cc, Compatibility = true)] + public abstract rc_error_t rc_api_init_resolve_hash_request(out rc_api_request_t request, ref rc_api_resolve_hash_request_t api_params); + + [BizImport(cc, Compatibility = true)] + public abstract rc_error_t rc_api_init_fetch_game_data_request(out rc_api_request_t request, ref rc_api_fetch_game_data_request_t api_params); + + [BizImport(cc, Compatibility = true)] + public abstract rc_error_t rc_api_init_fetch_user_unlocks_request(out rc_api_request_t request, ref rc_api_fetch_user_unlocks_request_t api_params); + + [BizImport(cc, Compatibility = true)] + public abstract rc_error_t rc_api_init_fetch_achievement_info_request(out rc_api_request_t request, ref rc_api_fetch_achievement_info_request_t api_params); + + [BizImport(cc, Compatibility = true)] + public abstract rc_error_t rc_api_init_fetch_leaderboard_info_request(out rc_api_request_t request, ref rc_api_fetch_leaderboard_info_request_t api_params); + + [BizImport(cc, Compatibility = true)] + public abstract rc_error_t rc_api_init_fetch_image_request(out rc_api_request_t request, ref rc_api_fetch_image_request_t api_params); + + [BizImport(cc, Compatibility = true)] + public abstract rc_error_t rc_api_init_award_achievement_request(out rc_api_request_t request, ref rc_api_award_achievement_request_t api_params); + + [BizImport(cc, Compatibility = true)] + public abstract rc_error_t rc_api_init_submit_lboard_entry_request(out rc_api_request_t request, ref rc_api_submit_lboard_entry_request_t api_params); + + [BizImport(cc, Compatibility = true)] + public abstract rc_error_t rc_api_init_ping_request(out rc_api_request_t request, ref rc_api_ping_request_t api_params); + + [BizImport(cc, Compatibility = true)] + public abstract rc_error_t rc_api_init_fetch_games_list_request(out rc_api_request_t request, ref rc_api_fetch_games_list_request_t api_params); + + [BizImport(cc)] + public abstract void rc_api_destroy_request(ref rc_api_request_t request); + + [BizImport(cc)] + public abstract rc_error_t rc_api_process_login_response(out rc_api_login_response_t response, byte[] server_response); + + [BizImport(cc)] + public abstract rc_error_t rc_api_process_start_session_response(out rc_api_start_session_response_t response, byte[] server_response); + + [BizImport(cc)] + public abstract rc_error_t rc_api_process_resolve_hash_response(out rc_api_resolve_hash_response_t response, byte[] server_response); + + [BizImport(cc)] + public abstract rc_error_t rc_api_process_fetch_game_data_response(out rc_api_fetch_game_data_response_t response, byte[] server_response); + + [BizImport(cc)] + public abstract rc_error_t rc_api_process_fetch_user_unlocks_response(out rc_api_fetch_user_unlocks_response_t response, byte[] server_response); + + [BizImport(cc)] + public abstract rc_error_t rc_api_process_fetch_achievement_info_response(out rc_api_fetch_achievement_info_response_t response, byte[] server_response); + + [BizImport(cc)] + public abstract rc_error_t rc_api_process_fetch_leaderboard_info_response(out rc_api_fetch_leaderboard_info_response_t response, byte[] server_response); + + [BizImport(cc)] + public abstract rc_error_t rc_api_process_award_achievement_response(out rc_api_award_achievement_response_t response, byte[] server_response); + + [BizImport(cc)] + public abstract rc_error_t rc_api_process_submit_lboard_entry_response(out rc_api_submit_lboard_entry_response_t response, byte[] server_response); + + [BizImport(cc)] + public abstract rc_error_t rc_api_process_ping_response(out rc_api_ping_response_t response, byte[] server_response); + + [BizImport(cc)] + public abstract rc_error_t rc_api_process_fetch_games_list_response(out rc_api_fetch_games_list_response_t response, byte[] server_response); + + [BizImport(cc)] + public abstract void rc_api_destroy_login_response(ref rc_api_login_response_t response); + + [BizImport(cc)] + public abstract void rc_api_destroy_start_session_response(ref rc_api_start_session_response_t response); + + [BizImport(cc)] + public abstract void rc_api_destroy_resolve_hash_response(ref rc_api_resolve_hash_response_t response); + + [BizImport(cc)] + public abstract void rc_api_destroy_fetch_game_data_response(ref rc_api_fetch_game_data_response_t response); + + [BizImport(cc)] + public abstract void rc_api_destroy_fetch_user_unlocks_response(ref rc_api_fetch_user_unlocks_response_t response); + + [BizImport(cc)] + public abstract void rc_api_destroy_fetch_achievement_info_response(ref rc_api_fetch_achievement_info_response_t response); + + [BizImport(cc)] + public abstract void rc_api_destroy_fetch_leaderboard_info_response(ref rc_api_fetch_leaderboard_info_response_t response); + + [BizImport(cc)] + public abstract void rc_api_destroy_award_achievement_response(ref rc_api_award_achievement_response_t response); + + [BizImport(cc)] + public abstract void rc_api_destroy_submit_lboard_entry_response(ref rc_api_submit_lboard_entry_response_t response); + + [BizImport(cc)] + public abstract void rc_api_destroy_ping_response(ref rc_api_ping_response_t response); + + [BizImport(cc)] + public abstract void rc_api_destroy_fetch_games_list_response(ref rc_api_fetch_games_list_response_t response); + } +} \ No newline at end of file diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RAIntegration.Update.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RAIntegration.Update.cs new file mode 100644 index 0000000000..8e804bfb97 --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RAIntegration.Update.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +using Newtonsoft.Json; + +using BizHawk.BizInvoke; +using BizHawk.Common; +using BizHawk.Client.Common; + +namespace BizHawk.Client.EmuHawk +{ + public partial class RAIntegration + { + private static DynamicLibraryImportResolver _resolver; + private static Version _version; + + private static void AttachDll() + { + _resolver = new("RA_Integration-x64.dll", hasLimitedLifetime: true); + RA = BizInvoker.GetInvoker(_resolver, CallingConventionAdapters.Native); + _version = new(Marshal.PtrToStringAnsi(RA.IntegrationVersion())); + Console.WriteLine($"Loaded RetroAchievements v{_version}"); + } + + private static void DetachDll() + { + RA?.Shutdown(); + _resolver?.Dispose(); + _resolver = null; + RA = null; + _version = new(0, 0); + } + + private static bool DownloadDll(string url) + { + if (url.StartsWith("http:")) + { + // force https + url = url.Replace("http:", "https:"); + } + + using var downloadForm = new RAIntegrationDownloaderForm(url); + downloadForm.ShowDialog(); + return downloadForm.DownloadSucceeded(); + } + + public static bool CheckUpdateRA(IMainFormForRetroAchievements mainForm) + { + try + { + var http = new HttpCommunication(null, "https://retroachievements.org/dorequest.php?r=latestintegration", null); + var info = JsonConvert.DeserializeObject>(http.ExecGet()); + if (info.TryGetValue("Success", out var success) && (bool)success) + { + var lastestVer = new Version((string)info["LatestVersion"]); + var minVer = new Version((string)info["MinimumVersion"]); + + if (_version < minVer) + { + if (mainForm.ShowMessageBox2( + owner: null, + text: "An update is required to use RetroAchievements. Do you want to download the update now?", + caption: "Update", + icon: EMsgBoxIcon.Question, + useOKCancel: false)) + { + DetachDll(); + var ret = DownloadDll((string)info["LatestVersionUrlX64"]); + AttachDll(); + return ret; + } + else + { + return false; + } + } + else if (_version < lastestVer) + { + if (mainForm.ShowMessageBox2( + owner: null, + text: "An optional update is available for RetroAchievements. Do you want to download the update now?", + caption: "Update", + icon: EMsgBoxIcon.Question, + useOKCancel: false)) + { + DetachDll(); + DownloadDll((string)info["LatestVersionUrlX64"]); + AttachDll(); + return true; // even if this fails, should be OK to use the old dll + } + else + { + // don't have to update in this case + return true; + } + } + else + { + return true; + } + } + else + { + mainForm.ShowMessageBox( + owner: null, + text: "Failed to fetch update information, cannot start RetroAchievements.", + caption: "Error", + icon: EMsgBoxIcon.Error); + + return false; + } + } + catch (Exception ex) + { + // is this needed? + mainForm.ShowMessageBox( + owner: null, + text: $"Exception {ex.Message} occurred when fetching update information, cannot start RetroAchievements.", + caption: "Error", + icon: EMsgBoxIcon.Error); + + DetachDll(); + AttachDll(); + return false; + } + } + } +} diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RAIntegration.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RAIntegration.cs new file mode 100644 index 0000000000..d628e0ed99 --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RAIntegration.cs @@ -0,0 +1,273 @@ +using System; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Windows.Forms; + +using BizHawk.Common; +using BizHawk.Client.Common; +using BizHawk.Emulation.Common; + +namespace BizHawk.Client.EmuHawk +{ + public partial class RAIntegration : RetroAchievements + { + private static RAInterface RA; + public static bool IsAvailable => RA != null; + + static RAIntegration() + { + try + { + if (OSTailoredCode.IsUnixHost) + { + throw new NotSupportedException("RAIntegration is Windows only!"); + } + + AttachDll(); + } + catch + { + DetachDll(); + } + } + + private readonly RAInterface.IsActiveDelegate _isActive; + private readonly RAInterface.UnpauseDelegate _unpause; + private readonly RAInterface.PauseDelegate _pause; + private readonly RAInterface.RebuildMenuDelegate _rebuildMenu; + private readonly RAInterface.EstimateTitleDelegate _estimateTitle; + private readonly RAInterface.ResetEmulatorDelegate _resetEmulator; + private readonly RAInterface.LoadROMDelegate _loadROM; + + private readonly RAInterface.MenuItem[] _menuItems = new RAInterface.MenuItem[40]; + + // Memory may be accessed by another thread (for rich presence) + // and peeks for us are not thread safe, so we need to guard it + private readonly AutoResetEvent _memAccessReady = new(false); + private readonly AutoResetEvent _memAccessGo = new(false); + private readonly AutoResetEvent _memAccessDone = new(false); + private readonly RAMemGuard _memGuard; + + private void RebuildMenu() + { + var numItems = RA.GetPopupMenuItems(_menuItems); + var tsmiddi = _raDropDownItems; + tsmiddi.Clear(); + { + var tsi = new ToolStripMenuItem("Shutdown RetroAchievements"); + tsi.Click += (_, _) => _shutdownRACallback(); + tsmiddi.Add(tsi); + + tsi = new ToolStripMenuItem("Autostart RetroAchievements") + { + Checked = _getConfig().RAAutostart, + CheckOnClick = true, + }; + tsi.CheckedChanged += (_, _) => _getConfig().RAAutostart ^= true; + tsmiddi.Add(tsi); + + var tss = new ToolStripSeparator(); + tsmiddi.Add(tss); + } + for (int i = 0; i < numItems; i++) + { + if (_menuItems[i].Label != IntPtr.Zero) + { + var tsi = new ToolStripMenuItem(Marshal.PtrToStringUni(_menuItems[i].Label)) + { + Checked = _menuItems[i].Checked != 0, + }; + var id = _menuItems[i].ID; + tsi.Click += (_, _) => + { + RA.InvokeDialog(id); + _mainForm.UpdateWindowTitle(); + }; + tsmiddi.Add(tsi); + } + else + { + var tss = new ToolStripSeparator(); + tsmiddi.Add(tss); + } + } + } + + protected override void HandleHardcoreModeDisable(string reason) + { + _mainForm.ShowMessageBox(null, $"{reason} Disabling hardcore mode.", "Warning", EMsgBoxIcon.Warning); + RA.WarnDisableHardcore(null); + } + + protected override int IdentifyHash(string hash) + => RA.IdentifyHash(hash); + + protected override int IdentifyRom(byte[] rom) + => RA.IdentifyRom(rom, rom.Length); + + public RAIntegration(IMainFormForRetroAchievements mainForm, InputManager inputManager, ToolManager tools, + Func getConfig, ToolStripItemCollection raDropDownItems, Action shutdownRACallback) + : base(mainForm, inputManager, tools, getConfig, raDropDownItems, shutdownRACallback) + { + _memGuard = new(_memAccessReady, _memAccessGo, _memAccessDone); + + RA.InitClient(_mainForm.Handle, "BizHawk", VersionInfo.GetEmuVersion()); + + _isActive = () => !Emu.IsNull(); + _unpause = _mainForm.UnpauseEmulator; + _pause = _mainForm.PauseEmulator; + _rebuildMenu = RebuildMenu; + _estimateTitle = buffer => + { + var name = Encoding.UTF8.GetBytes(Game?.Name ?? "No Game Info Available"); + Marshal.Copy(name, 0, buffer, Math.Min(name.Length, 256)); + }; + _resetEmulator = () => _mainForm.RebootCore(); + _loadROM = path => _mainForm.LoadRom(path, new LoadRomArgs { OpenAdvanced = OpenAdvancedSerializer.ParseWithLegacy(path) }); + + RA.InstallSharedFunctionsExt(_isActive, _unpause, _pause, _rebuildMenu, _estimateTitle, _resetEmulator, _loadROM); + + RA.AttemptLogin(true); + } + + public override void Dispose() + { + RA?.Shutdown(); + _memGuard.Dispose(); + } + + public override void OnSaveState(string path) + => RA.OnSaveState(path); + + public override void OnLoadState(string path) + { + if (RA.HardcoreModeIsActive()) + { + HandleHardcoreModeDisable("Loading savestates is not allowed in hardcore mode."); + } + + RA.OnLoadState(path); + } + + public override void Stop() + { + RA.ClearMemoryBanks(); + RA.ActivateGame(0); + } + + public override void Restart() + { + var consoleId = SystemIdToConsoleId(); + RA.SetConsoleID(consoleId); + + RA.ClearMemoryBanks(); + + if (Emu.HasMemoryDomains()) + { + _memFunctions = CreateMemoryBanks(consoleId, Domains, Emu.CanDebug() ? Emu.AsDebuggable() : null); + + for (int i = 0; i < _memFunctions.Count; i++) + { + _memFunctions[i].MemGuard = _memGuard; + RA.InstallMemoryBank(i, _memFunctions[i].ReadFunc, _memFunctions[i].WriteFunc, _memFunctions[i].BankSize); + RA.InstallMemoryBankBlockReader(i, _memFunctions[i].ReadBlockFunc); + } + } + + AllGamesVerified = true; + + if (_mainForm.CurrentlyOpenRomArgs is not null) + { + var ids = GetRAGameIds(_mainForm.CurrentlyOpenRomArgs.OpenAdvanced, consoleId); + + AllGamesVerified = !ids.Contains(0); + + RA.ActivateGame(ids.Count > 0 ? ids[0] : 0); + } + else + { + RA.ActivateGame(0); + } + + Update(); + RebuildMenu(); + + // workaround a bug in RA which will cause the window title to be changed despite us not calling UpdateAppTitle + _mainForm.UpdateWindowTitle(); + + // note: this can only catch quicksaves (probably only case of accidential use from hotkeys) + _mainForm.EmuClient.BeforeQuickLoad += (_, e) => + { + if (RA.HardcoreModeIsActive()) + { + e.Handled = !RA.WarnDisableHardcore("load a quicksave"); + } + }; + } + + public override void Update() + { + if (RA.HardcoreModeIsActive()) + { + CheckHardcoreModeConditions(); + } + + if (_inputManager.ClientControls["Open RA Overlay"]) + { + RA.SetPaused(true); + } + + if (RA.IsOverlayFullyVisible()) + { + var ci = new RAInterface.ControllerInput + { + UpPressed = _inputManager.ClientControls["RA Up"], + DownPressed = _inputManager.ClientControls["RA Down"], + LeftPressed = _inputManager.ClientControls["RA Left"], + RightPressed = _inputManager.ClientControls["RA Right"], + ConfirmPressed = _inputManager.ClientControls["RA Confirm"], + CancelPressed = _inputManager.ClientControls["RA Cancel"], + QuitPressed = _inputManager.ClientControls["RA Quit"], + }; + + RA.NavigateOverlay(ref ci); + + // todo: suppress user inputs with overlay active? + } + + if (_memAccessReady.WaitOne(0)) + { + _memAccessGo.Set(); + _memAccessDone.WaitOne(); + } + } + + public override void OnFrameAdvance() + { + var input = _inputManager.ControllerOutput; + foreach (var resetButton in input.Definition.BoolButtons.Where(b => b.Contains("Power") || b.Contains("Reset"))) + { + if (input.IsPressed(resetButton)) + { + RA.OnReset(); + break; + } + } + + if (Emu.HasMemoryDomains()) + { + // we want to EnterExit to prevent wbx host spam when peeks are spammed + using (Domains.MainMemory.EnterExit()) + { + RA.DoAchievementsFrame(); + } + } + else + { + RA.DoAchievementsFrame(); + } + } + } +} diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RAIntegrationDownloaderForm.Designer.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RAIntegrationDownloaderForm.Designer.cs new file mode 100644 index 0000000000..ad2438afc4 --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RAIntegrationDownloaderForm.Designer.cs @@ -0,0 +1,170 @@ +namespace BizHawk.Client.EmuHawk +{ + partial class RAIntegrationDownloaderForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.btnCancel = new System.Windows.Forms.Button(); + this.btnDownload = new System.Windows.Forms.Button(); + this.label3 = new BizHawk.WinForms.Controls.LocLabelEx(); + this.linkLabel1 = new System.Windows.Forms.LinkLabel(); + this.progressBar1 = new System.Windows.Forms.ProgressBar(); + this.timer1 = new System.Windows.Forms.Timer(this.components); + this.txtLocation = new System.Windows.Forms.TextBox(); + this.label1 = new System.Windows.Forms.Label(); + this.label2 = new System.Windows.Forms.Label(); + this.txtUrl = new System.Windows.Forms.TextBox(); + this.SuspendLayout(); + // + // btnCancel + // + this.btnCancel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.btnCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel; + this.btnCancel.Location = new System.Drawing.Point(667, 86); + this.btnCancel.Name = "btnCancel"; + this.btnCancel.Size = new System.Drawing.Size(75, 23); + this.btnCancel.TabIndex = 7; + this.btnCancel.Text = "Cancel"; + this.btnCancel.UseVisualStyleBackColor = true; + this.btnCancel.Click += new System.EventHandler(this.btnCancel_Click); + // + // btnDownload + // + this.btnDownload.Location = new System.Drawing.Point(9, 86); + this.btnDownload.Name = "btnDownload"; + this.btnDownload.Size = new System.Drawing.Size(186, 23); + this.btnDownload.TabIndex = 6; + this.btnDownload.Text = "Download"; + this.btnDownload.UseVisualStyleBackColor = true; + this.btnDownload.Click += new System.EventHandler(this.btnDownload_Click); + // + // label3 + // + this.label3.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.label3.Location = new System.Drawing.Point(6, -35); + this.label3.MaximumSize = new System.Drawing.Size(260, 0); + this.label3.Name = "label3"; + // + // linkLabel1 + // + this.linkLabel1.AutoSize = true; + this.linkLabel1.Location = new System.Drawing.Point(715, 61); + this.linkLabel1.Name = "linkLabel1"; + this.linkLabel1.Size = new System.Drawing.Size(27, 13); + this.linkLabel1.TabIndex = 12; + this.linkLabel1.TabStop = true; + this.linkLabel1.Text = "Link"; + this.linkLabel1.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.linkLabel1_LinkClicked); + // + // progressBar1 + // + this.progressBar1.Location = new System.Drawing.Point(201, 86); + this.progressBar1.Name = "progressBar1"; + this.progressBar1.Size = new System.Drawing.Size(186, 23); + this.progressBar1.TabIndex = 13; + // + // timer1 + // + this.timer1.Enabled = true; + this.timer1.Tick += new System.EventHandler(this.timer1_Tick); + // + // txtLocation + // + this.txtLocation.Location = new System.Drawing.Point(95, 12); + this.txtLocation.Name = "txtLocation"; + this.txtLocation.ReadOnly = true; + this.txtLocation.Size = new System.Drawing.Size(613, 20); + this.txtLocation.TabIndex = 15; + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(9, 15); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(80, 13); + this.label1.TabIndex = 16; + this.label1.Text = "Local Location:"; + // + // label2 + // + this.label2.AutoSize = true; + this.label2.Location = new System.Drawing.Point(57, 41); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(32, 13); + this.label2.TabIndex = 17; + this.label2.Text = "URL:"; + // + // txtUrl + // + this.txtUrl.Location = new System.Drawing.Point(96, 38); + this.txtUrl.Name = "txtUrl"; + this.txtUrl.ReadOnly = true; + this.txtUrl.Size = new System.Drawing.Size(613, 20); + this.txtUrl.TabIndex = 18; + // + // RAIntegrationDownloaderForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.CancelButton = this.btnCancel; + this.ClientSize = new System.Drawing.Size(754, 121); + this.Controls.Add(this.progressBar1); + this.Controls.Add(this.txtUrl); + this.Controls.Add(this.btnDownload); + this.Controls.Add(this.label2); + this.Controls.Add(this.linkLabel1); + this.Controls.Add(this.label1); + this.Controls.Add(this.txtLocation); + this.Controls.Add(this.btnCancel); + this.Controls.Add(this.label3); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.MinimumSize = new System.Drawing.Size(300, 160); + this.Name = "RAIntegrationDownloaderForm"; + this.ShowIcon = false; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + this.Text = "Download RAIntegration"; + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + private BizHawk.WinForms.Controls.LocLabelEx label3; + private System.Windows.Forms.Button btnCancel; + private System.Windows.Forms.Button btnDownload; + private System.Windows.Forms.LinkLabel linkLabel1; + private System.Windows.Forms.ProgressBar progressBar1; + private System.Windows.Forms.Timer timer1; + private System.Windows.Forms.TextBox txtLocation; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.Label label2; + private System.Windows.Forms.TextBox txtUrl; + } +} \ No newline at end of file diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RAIntegrationDownloaderForm.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RAIntegrationDownloaderForm.cs new file mode 100644 index 0000000000..ee6ed35f47 --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RAIntegrationDownloaderForm.cs @@ -0,0 +1,168 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading; +using System.Windows.Forms; + +using BizHawk.Common; +using BizHawk.Common.PathExtensions; + +namespace BizHawk.Client.EmuHawk +{ + /// + /// Downloads RAIntegration (largely a copy paste of ffmpeg's downloader) + /// + public partial class RAIntegrationDownloaderForm : Form + { + public RAIntegrationDownloaderForm(string url) + { + _path = Path.Combine(PathUtils.DataDirectoryPath, "dll", "RA_Integration-x64.dll"); + _url = url; + + InitializeComponent(); + + txtLocation.Text = _path; + txtUrl.Text = _url; + } + + private readonly string _path; + private readonly string _url; + + private int _pct = 0; + private bool _exiting = false; + private bool _succeeded = false; + private bool _failed = false; + private Thread _thread; + + public bool DownloadSucceeded() + { + // block until the thread dies + while (_thread?.IsAlive ?? false) + { + Thread.Sleep(1); + } + + return _succeeded; + } + + private void ThreadProc() + { + Download(); + } + + private void Download() + { + //the temp file is owned by this thread + var fn = TempFileManager.GetTempFilename("RAIntegration_download", ".dll", false); + + try + { + using (var evt = new ManualResetEvent(false)) + { + using var client = new System.Net.WebClient(); + System.Net.ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Tls12; + client.DownloadFileAsync(new Uri(_url), fn); + client.DownloadProgressChanged += (object sender, System.Net.DownloadProgressChangedEventArgs e) => + { + _pct = e.ProgressPercentage; + }; + client.DownloadFileCompleted += (object sender, System.ComponentModel.AsyncCompletedEventArgs e) => + { + //we don't really need a status. we'll just try to unzip it when it's done + evt.Set(); + }; + + for (; ; ) + { + if (evt.WaitOne(10)) + break; + + //if the gui thread ordered an exit, cancel the download and wait for it to acknowledge + if (_exiting) + { + client.CancelAsync(); + evt.WaitOne(); + break; + } + } + } + + //throw new Exception("test of download failure"); + + //if we were ordered to exit, bail without wasting any more time + if (_exiting) + return; + + //try acquiring file + using (var dll = new HawkFile(fn)) + { + var data = dll!.ReadAllBytes(); + + //last chance. exiting, don't dump the new RAIntegration file + if (_exiting) + return; + + DirectoryInfo parentDir = new(Path.GetDirectoryName(_path)!); + if (!parentDir.Exists) parentDir.Create(); + if (File.Exists(_path)) File.Delete(_path); + File.WriteAllBytes(_path, data); + } + + _succeeded = true; + } + catch + { + _failed = true; + } + finally + { + try { File.Delete(fn); } + catch { } + } + } + + private void btnDownload_Click(object sender, EventArgs e) + { + btnDownload.Text = "Downloading..."; + btnDownload.Enabled = false; + _failed = false; + _succeeded = false; + _pct = 0; + _thread = new Thread(ThreadProc); + _thread.Start(); + } + + private void btnCancel_Click(object sender, EventArgs e) + { + Close(); + } + + protected override void OnClosed(EventArgs e) + { + //inform the worker thread that it needs to try terminating without doing anything else + //(it will linger on in background for a bit til it can service this) + _exiting = true; + } + + private void timer1_Tick(object sender, EventArgs e) + { + //if it's done, close the window. the user will be smart enough to reopen it + if (_succeeded) + Close(); + if (_failed) + { + _failed = false; + _pct = 0; + btnDownload.Text = "FAILED - Download Again"; + btnDownload.Enabled = true; + } + progressBar1.Value = _pct; + } + + private void linkLabel1_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) + { + System.Diagnostics.Process.Start(_url); + } + } +} + diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RAIntegrationDownloaderForm.resx b/src/BizHawk.Client.EmuHawk/RetroAchievements/RAIntegrationDownloaderForm.resx new file mode 100644 index 0000000000..aac33d5a29 --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RAIntegrationDownloaderForm.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + \ No newline at end of file diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RAInterface.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RAInterface.cs new file mode 100644 index 0000000000..c5d7ff7c49 --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RAInterface.cs @@ -0,0 +1,194 @@ +using System; +using System.Runtime.InteropServices; + +using BizHawk.BizInvoke; + +// this is largely a C# mirror of https://github.com/RetroAchievements/RAInterface + +namespace BizHawk.Client.EmuHawk +{ + public abstract class RAInterface + { + private const CallingConvention cc = CallingConvention.Cdecl; + + [BizImport(cc, EntryPoint = "_RA_IntegrationVersion")] + public abstract IntPtr IntegrationVersion(); + + [BizImport(cc, EntryPoint = "_RA_HostName")] + public abstract IntPtr HostName(); + + [BizImport(cc, EntryPoint = "_RA_HostUrl")] + public abstract IntPtr HostUrl(); + + [BizImport(cc, EntryPoint = "_RA_InitI")] + [return: MarshalAs(UnmanagedType.Bool)] + public abstract bool InitI(IntPtr hwnd, int emuID, string clientVer); + + [BizImport(cc, EntryPoint = "_RA_InitOffline")] + [return: MarshalAs(UnmanagedType.Bool)] + public abstract bool InitOffline(IntPtr hwnd, int emuID, string clientVer); + + [BizImport(cc, EntryPoint = "_RA_InitClient")] + [return: MarshalAs(UnmanagedType.Bool)] + public abstract bool InitClient(IntPtr hwnd, string clientName, string clientVer); + + [BizImport(cc, EntryPoint = "_RA_InitClientOffline")] + [return: MarshalAs(UnmanagedType.Bool)] + public abstract bool InitClientOffline(IntPtr hwnd, string clientName, string clientVer); + + [UnmanagedFunctionPointer(cc)] + public delegate bool IsActiveDelegate(); + + [UnmanagedFunctionPointer(cc)] + public delegate void UnpauseDelegate(); + + [UnmanagedFunctionPointer(cc)] + public delegate void PauseDelegate(); + + [UnmanagedFunctionPointer(cc)] + public delegate void RebuildMenuDelegate(); + + [UnmanagedFunctionPointer(cc)] + public delegate void EstimateTitleDelegate(IntPtr buffer); + + [UnmanagedFunctionPointer(cc)] + public delegate void ResetEmulatorDelegate(); + + [UnmanagedFunctionPointer(cc)] + public delegate void LoadROMDelegate(string filename); + + [BizImport(cc, EntryPoint = "_RA_InstallSharedFunctionsExt")] + public abstract void InstallSharedFunctionsExt(IsActiveDelegate isActive, + UnpauseDelegate unpause, PauseDelegate pause, RebuildMenuDelegate rebuildMenu, + EstimateTitleDelegate estimateTitle, ResetEmulatorDelegate resetEmulator, LoadROMDelegate loadROM); + + [BizImport(cc, Compatibility = true, EntryPoint = "_RA_SetForceRepaint")] + public abstract void SetForceRepaint([MarshalAs(UnmanagedType.Bool)] bool enable); + + [BizImport(cc, EntryPoint = "_RA_CreatePopupMenu")] + public abstract IntPtr CreatePopupMenu(); + + [StructLayout(LayoutKind.Sequential)] + public struct MenuItem + { + public IntPtr Label; + public IntPtr ID; + public int Checked; + } + + [BizImport(cc, EntryPoint = "_RA_GetPopupMenuItems")] + public abstract int GetPopupMenuItems(MenuItem[] items); + + [BizImport(cc, EntryPoint = "_RA_InvokeDialog")] + public abstract void InvokeDialog(IntPtr lparam); + + [BizImport(cc, EntryPoint = "_RA_SetUserAgentDetail")] + public abstract void SetUserAgentDetail(string detail); + + [BizImport(cc, Compatibility = true, EntryPoint = "_RA_AttemptLogin")] + public abstract void AttemptLogin([MarshalAs(UnmanagedType.Bool)] bool blocking); + + [BizImport(cc, EntryPoint = "_RA_SetConsoleID")] + public abstract void SetConsoleID(RetroAchievements.ConsoleID consoleID); + + [BizImport(cc, EntryPoint = "_RA_ClearMemoryBanks")] + public abstract void ClearMemoryBanks(); + + [BizImport(cc, EntryPoint = "_RA_InstallMemoryBank")] + public abstract void InstallMemoryBank(int bankID, RetroAchievements.ReadMemoryFunc reader, RetroAchievements.WriteMemoryFunc writer, int bankSize); + + [BizImport(cc, EntryPoint = "_RA_InstallMemoryBankBlockReader")] + public abstract void InstallMemoryBankBlockReader(int bankID, RetroAchievements.ReadMemoryBlockFunc reader); + + [BizImport(cc, EntryPoint = "_RA_Shutdown")] + [return: MarshalAs(UnmanagedType.Bool)] + public abstract bool Shutdown(); + + [BizImport(cc, EntryPoint = "_RA_IsOverlayFullyVisible")] + [return: MarshalAs(UnmanagedType.Bool)] + public abstract bool IsOverlayFullyVisible(); + + [BizImport(cc, Compatibility = true, EntryPoint = "_RA_SetPaused")] + public abstract void SetPaused([MarshalAs(UnmanagedType.Bool)] bool isPaused); + + [StructLayout(LayoutKind.Sequential)] + public struct ControllerInput + { + [MarshalAs(UnmanagedType.Bool)] + public bool UpPressed; + [MarshalAs(UnmanagedType.Bool)] + public bool DownPressed; + [MarshalAs(UnmanagedType.Bool)] + public bool LeftPressed; + [MarshalAs(UnmanagedType.Bool)] + public bool RightPressed; + [MarshalAs(UnmanagedType.Bool)] + public bool ConfirmPressed; + [MarshalAs(UnmanagedType.Bool)] + public bool CancelPressed; + [MarshalAs(UnmanagedType.Bool)] + public bool QuitPressed; + } + + [BizImport(cc, Compatibility = true, EntryPoint = "_RA_NavigateOverlay")] + public abstract void NavigateOverlay(ref ControllerInput input); + + [BizImport(cc, EntryPoint = "_RA_UpdateHWnd")] + public abstract void UpdateHWnd(IntPtr hwnd); + + [BizImport(cc, EntryPoint = "_RA_IdentifyRom")] + public abstract int IdentifyRom(byte[] rom, int romSize); + + [BizImport(cc, EntryPoint = "_RA_IdentifyHash")] + public abstract int IdentifyHash(string hash); + + [BizImport(cc, EntryPoint = "_RA_ActivateGame")] + public abstract void ActivateGame(int gameId); + + [BizImport(cc, EntryPoint = "_RA_OnLoadNewRom")] + [return: MarshalAs(UnmanagedType.Bool)] + public abstract bool OnLoadNewRom(byte[] rom, int romSize); + + [BizImport(cc, Compatibility = true, EntryPoint = "_RA_ConfirmLoadNewRom")] + [return: MarshalAs(UnmanagedType.Bool)] + public abstract bool ConfirmLoadNewRom([MarshalAs(UnmanagedType.Bool)] bool quitting); + + [BizImport(cc, EntryPoint = "_RA_DoAchievementsFrame")] + public abstract void DoAchievementsFrame(); + + [BizImport(cc, EntryPoint = "_RA_SuspendRepaint")] + public abstract void SuspendRepaint(); + + [BizImport(cc, EntryPoint = "_RA_ResumeRepaint")] + public abstract void ResumeRepaint(); + + [BizImport(cc, EntryPoint = "_RA_UpdateAppTitle")] + public abstract void UpdateAppTitle(string message); + + [BizImport(cc, EntryPoint = "_RA_UserName")] + public abstract IntPtr UserName(); + + [BizImport(cc, EntryPoint = "_RA_HardcoreModeIsActive")] + [return: MarshalAs(UnmanagedType.Bool)] + public abstract bool HardcoreModeIsActive(); + + [BizImport(cc, EntryPoint = "_RA_WarnDisableHardcore")] + [return: MarshalAs(UnmanagedType.Bool)] + public abstract bool WarnDisableHardcore(string activity); + + [BizImport(cc, EntryPoint = "_RA_OnReset")] + public abstract void OnReset(); + + [BizImport(cc, EntryPoint = "_RA_OnSaveState")] + public abstract void OnSaveState(string filename); + + [BizImport(cc, EntryPoint = "_RA_OnLoadState")] + public abstract void OnLoadState(string filename); + + [BizImport(cc, EntryPoint = "_RA_CaptureState")] + public abstract int CaptureState(IntPtr buffer, int bufferSize); + + [BizImport(cc, EntryPoint = "_RA_RestoreState")] + public abstract void RestoreState(IntPtr buffer); + } +} diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Achievements.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Achievements.cs new file mode 100644 index 0000000000..233c94a4ec --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Achievements.cs @@ -0,0 +1,190 @@ +using System; +using System.Drawing; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Client.EmuHawk +{ + public partial class RCheevos + { + private readonly RCheevosAchievementListForm _cheevoListForm = new(); + + private bool CheevosActive { get; set; } + private bool AllowUnofficialCheevos { get; set; } + + public class Cheevo + { + public int ID { get; } + public int Points { get; } + public LibRCheevos.rc_runtime_achievement_category_t Category { get; } + public string Title { get; } + public string Description { get; } + public string Definition { get; } + public string Author { get; } + private string BadgeName { get; } + public Bitmap BadgeUnlocked { get; private set; } + public Bitmap BadgeLocked { get; private set; } + public DateTime Created { get; } + public DateTime Updated { get; } + + public bool IsSoftcoreUnlocked { get; set; } + public bool IsHardcoreUnlocked { get; set; } + + public bool IsUnlocked(bool hardcore) + => hardcore ? IsHardcoreUnlocked : IsSoftcoreUnlocked; + + public void SetUnlocked(bool hardcore, bool unlocked) + { + if (hardcore) + { + IsHardcoreUnlocked = unlocked; + } + else + { + IsSoftcoreUnlocked = unlocked; + } + } + + public bool IsPrimed { get; set; } + private Func AllowUnofficialCheevos { get; } + public bool Invalid { get; set; } + public bool IsEnabled => !Invalid && (IsOfficial || AllowUnofficialCheevos()); + public bool IsOfficial => Category is LibRCheevos.rc_runtime_achievement_category_t.RC_ACHIEVEMENT_CATEGORY_CORE; + + public async void LoadImages() + { + BadgeUnlocked = await GetImage(BadgeName, LibRCheevos.rc_api_image_type_t.RC_IMAGE_TYPE_ACHIEVEMENT).ConfigureAwait(false); + BadgeLocked = await GetImage(BadgeName, LibRCheevos.rc_api_image_type_t.RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED).ConfigureAwait(false); + } + + public Cheevo(in LibRCheevos.rc_api_achievement_definition_t cheevo, Func allowUnofficialCheevos) + { + ID = cheevo.id; + Points = cheevo.points; + Category = cheevo.category; + Title = cheevo.Title; + Description = cheevo.Description; + Definition = cheevo.Definition; + Author = cheevo.Author; + BadgeName = cheevo.BadgeName; + BadgeUnlocked = null; + BadgeLocked = null; + Created = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddSeconds(cheevo.created).ToLocalTime(); + Updated = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddSeconds(cheevo.updated).ToLocalTime(); + IsSoftcoreUnlocked = false; + IsHardcoreUnlocked = false; + IsPrimed = false; + AllowUnofficialCheevos = allowUnofficialCheevos; + Invalid = false; + } + + public Cheevo(in Cheevo cheevo, Func allowUnofficialCheevos) + { + ID = cheevo.ID; + Points = cheevo.Points; + Category = cheevo.Category; + Title = cheevo.Title; + Description = cheevo.Description; + Definition = cheevo.Definition; + Author = cheevo.Author; + BadgeName = cheevo.BadgeName; + BadgeUnlocked = null; + BadgeLocked = null; + Created = cheevo.Created; + Updated = cheevo.Updated; + IsSoftcoreUnlocked = false; + IsHardcoreUnlocked = false; + IsPrimed = false; + AllowUnofficialCheevos = allowUnofficialCheevos; + Invalid = false; + } + } + + private readonly byte[] _cheevoFormatBuffer = new byte[1024]; + + public string GetCheevoProgress(int id) + { + var len = _lib.rc_runtime_format_achievement_measured(ref _runtime, id, _cheevoFormatBuffer, _cheevoFormatBuffer.Length); + return Encoding.ASCII.GetString(_cheevoFormatBuffer, 0, len); + } + + private void ToggleUnofficialCheevos() + { + if (_gameData.GameID == 0) + { + AllowUnofficialCheevos ^= true; + return; + } + + var initReady = HardcoreMode ? _gameData.HardcoreInitUnlocksReady : _gameData.SoftcoreInitUnlocksReady; + initReady.WaitOne(); + + foreach (var cheevo in _gameData.CheevoEnumerable) + { + if (cheevo.IsEnabled && !cheevo.IsUnlocked(HardcoreMode)) + { + _lib.rc_runtime_deactivate_achievement(ref _runtime, cheevo.ID); + } + } + + AllowUnofficialCheevos ^= true; + + foreach (var cheevo in _gameData.CheevoEnumerable) + { + if (cheevo.IsEnabled && !cheevo.IsUnlocked(HardcoreMode)) + { + _lib.rc_runtime_activate_achievement(ref _runtime, cheevo.ID, cheevo.Definition, IntPtr.Zero, 0); + } + } + } + + private void ToSoftcoreMode() + { + if (_gameData == null || _gameData.GameID == 0) return; + + _gameData.SoftcoreInitUnlocksReady.WaitOne(); + + foreach (var cheevo in _gameData.CheevoEnumerable) + { + if (cheevo.IsEnabled && !cheevo.IsUnlocked(false)) + { + _lib.rc_runtime_deactivate_achievement(ref _runtime, cheevo.ID); + } + } + + _gameData.HardcoreInitUnlocksReady.WaitOne(); + + foreach (var cheevo in _gameData.CheevoEnumerable) + { + if (cheevo.IsEnabled && !cheevo.IsUnlocked(true)) + { + _lib.rc_runtime_activate_achievement(ref _runtime, cheevo.ID, cheevo.Definition, IntPtr.Zero, 0); + } + } + + Update(); + } + + private static async Task SendUnlockAchievementAsync(string username, string api_token, int id, bool hardcore, string hash) + { + var api_params = new LibRCheevos.rc_api_award_achievement_request_t(username, api_token, id, hardcore, hash); + var res = LibRCheevos.rc_error_t.RC_INVALID_STATE; + if (_lib.rc_api_init_award_achievement_request(out var api_req, ref api_params) == LibRCheevos.rc_error_t.RC_OK) + { + var serv_req = await SendAPIRequest(in api_req).ConfigureAwait(false); + res = _lib.rc_api_process_award_achievement_response(out var resp, serv_req); + _lib.rc_api_destroy_award_achievement_response(ref resp); + } + + _lib.rc_api_destroy_request(ref api_req); + + if (res != LibRCheevos.rc_error_t.RC_OK) + { + // todo: warn user? correct local version of cheevos? + } + } + + private static async void SendUnlockAchievement(string username, string api_token, int id, bool hardcore, string hash) + => await SendUnlockAchievementAsync(username, api_token, id, hardcore, hash).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.GameInfo.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.GameInfo.cs new file mode 100644 index 0000000000..352bd30b8a --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.GameInfo.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace BizHawk.Client.EmuHawk +{ + public partial class RCheevos + { + private readonly RCheevosGameInfoForm _gameInfoForm = new(); + + private ConsoleID _consoleId; + + private string _gameHash; + private readonly Dictionary _cachedGameIds = new(); // keep around IDs per hash to avoid unneeded API calls for a simple RebootCore + + private GameData _gameData; + private readonly Dictionary _cachedGameDatas = new(); // keep game data around to avoid unneeded API calls for a simple RebootCore + + public class GameData + { + public int GameID { get; } + public ConsoleID ConsoleID { get; } + public string Title { get; } + private string ImageName { get; } + public Bitmap GameBadge { get; private set; } + public string RichPresenseScript { get; } + + private readonly IReadOnlyDictionary _cheevos; + private readonly IReadOnlyDictionary _lboards; + + public IEnumerable CheevoEnumerable => _cheevos.Values; + public IEnumerable LBoardEnumerable => _lboards.Values; + + public Cheevo GetCheevoById(int i) => _cheevos[i]; + public LBoard GetLboardById(int i) => _lboards[i]; + + public ManualResetEvent SoftcoreInitUnlocksReady { get; } + public ManualResetEvent HardcoreInitUnlocksReady { get; } + + public async Task InitUnlocks(string username, string api_token, bool hardcore) + { + var api_params = new LibRCheevos.rc_api_fetch_user_unlocks_request_t(username, api_token, GameID, hardcore); + if (_lib.rc_api_init_fetch_user_unlocks_request(out var api_req, ref api_params) == LibRCheevos.rc_error_t.RC_OK) + { + var serv_req = await SendAPIRequest(in api_req).ConfigureAwait(false); + if (_lib.rc_api_process_fetch_user_unlocks_response(out var resp, serv_req) == LibRCheevos.rc_error_t.RC_OK) + { + unsafe + { + var unlocks = (int*)resp.achievement_ids; + for (int i = 0; i < resp.num_achievement_ids; i++) + { + if (_cheevos.TryGetValue(unlocks[i], out var cheevo)) + { + cheevo.SetUnlocked(hardcore, true); + } + } + } + } + + _lib.rc_api_destroy_fetch_user_unlocks_response(ref resp); + } + + _lib.rc_api_destroy_request(ref api_req); + + if (hardcore) + { + HardcoreInitUnlocksReady?.Set(); + } + else + { + SoftcoreInitUnlocksReady?.Set(); + } + } + + public async Task LoadImages() + { + GameBadge = await GetImage(ImageName, LibRCheevos.rc_api_image_type_t.RC_IMAGE_TYPE_GAME).ConfigureAwait(false); + + if (_cheevos is null) return; + + foreach (var cheevo in _cheevos.Values) + { + cheevo.LoadImages(); + } + } + + public int TotalCheevoPoints(bool hardcore) + => _cheevos?.Values.Sum(c => (c.IsEnabled && !c.Invalid && c.IsUnlocked(hardcore)) ? c.Points : 0) ?? 0; + + public unsafe GameData(in LibRCheevos.rc_api_fetch_game_data_response_t resp, Func allowUnofficialCheevos) + { + GameID = resp.id; + ConsoleID = resp.console_id; + Title = resp.Title; + ImageName = resp.ImageName; + GameBadge = null; + RichPresenseScript = resp.RichPresenceScript; + + var cheevos = new Dictionary(); + var cptr = (LibRCheevos.rc_api_achievement_definition_t*)resp.achievements; + for (int i = 0; i < resp.num_achievements; i++) + { + cheevos.Add(cptr[i].id, new(in cptr[i], allowUnofficialCheevos)); + } + + _cheevos = cheevos; + + var lboards = new Dictionary(); + var lptr = (LibRCheevos.rc_api_leaderboard_definition_t*)resp.leaderboards; + for (int i = 0; i < resp.num_leaderboards; i++) + { + lboards.Add(lptr[i].id, new(in lptr[i])); + } + + _lboards = lboards; + + SoftcoreInitUnlocksReady = new(false); + HardcoreInitUnlocksReady = new(false); + } + + public GameData(GameData gameData, Func allowUnofficialCheevos) + { + GameID = gameData.GameID; + ConsoleID = gameData.ConsoleID; + Title = gameData.Title; + ImageName = gameData.ImageName; + GameBadge = null; + RichPresenseScript = gameData.RichPresenseScript; + + var cheevos = new Dictionary(); + foreach (var cheevo in gameData.CheevoEnumerable) + { + cheevos.Add(cheevo.ID, new(in cheevo, allowUnofficialCheevos)); + } + + _cheevos = cheevos; + + var lboards = new Dictionary(); + foreach (var lboard in gameData.LBoardEnumerable) + { + lboards.Add(lboard.ID, new(in lboard)); + } + + _lboards = lboards; + + SoftcoreInitUnlocksReady = new(false); + HardcoreInitUnlocksReady = new(false); + } + + public GameData() + { + GameID = 0; + } + } + + private async Task SendHashAsync(string hash) + { + var api_params = new LibRCheevos.rc_api_resolve_hash_request_t(null, null, hash); + var ret = 0; + if (_lib.rc_api_init_resolve_hash_request(out var api_req, ref api_params) == LibRCheevos.rc_error_t.RC_OK) + { + var serv_req = await SendAPIRequest(in api_req).ConfigureAwait(false); + if (_lib.rc_api_process_resolve_hash_response(out var resp, serv_req) == LibRCheevos.rc_error_t.RC_OK) + { + ret = resp.game_id; + } + + _lib.rc_api_destroy_resolve_hash_response(ref resp); + } + + _lib.rc_api_destroy_request(ref api_req); + return ret; + } + + protected override int IdentifyHash(string hash) + { + _gameHash ??= hash; + + if (_cachedGameIds.ContainsKey(hash)) + { + return _cachedGameIds[hash]; + } + + var ret = SendHashAsync(hash).ConfigureAwait(false).GetAwaiter().GetResult(); + _cachedGameIds.Add(hash, ret); + return ret; + } + + protected override int IdentifyRom(byte[] rom) + { + var hash = new byte[33]; + if (_lib.rc_hash_generate_from_buffer(hash, _consoleId, rom, rom.Length)) + { + return IdentifyHash(Encoding.ASCII.GetString(hash, 0, 32)); + } + + _gameHash ??= "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE"; + return 0; + } + + private static async Task InitGameDataAsync(GameData gameData, string username, string api_token, bool hardcore) + { + await gameData.InitUnlocks(username, api_token, hardcore).ConfigureAwait(false); + await gameData.InitUnlocks(username, api_token, !hardcore).ConfigureAwait(false); + await gameData.LoadImages().ConfigureAwait(false); + } + + private static async void InitGameData(GameData gameData, string username, string api_token, bool hardcore) + => await InitGameDataAsync(gameData, username, api_token, hardcore); + + private static GameData GetGameData(string username, string api_token, int id, Func allowUnofficialCheevos) + { + var api_params = new LibRCheevos.rc_api_fetch_game_data_request_t(username, api_token, id); + var ret = new GameData(); + if (_lib.rc_api_init_fetch_game_data_request(out var api_req, ref api_params) == LibRCheevos.rc_error_t.RC_OK) + { + var serv_req = SendAPIRequest(in api_req).ConfigureAwait(false).GetAwaiter().GetResult(); + if (_lib.rc_api_process_fetch_game_data_response(out var resp, serv_req) == LibRCheevos.rc_error_t.RC_OK) + { + ret = new(in resp, allowUnofficialCheevos); + } + + _lib.rc_api_destroy_fetch_game_data_response(ref resp); + } + + _lib.rc_api_destroy_request(ref api_req); + return ret; + } + + private static async Task GetImage(string image_name, LibRCheevos.rc_api_image_type_t image_type) + { + if (image_name is null) return null; + + var api_params = new LibRCheevos.rc_api_fetch_image_request_t(image_name, image_type); + Bitmap ret = null; + if (_lib.rc_api_init_fetch_image_request(out var api_req, ref api_params) == LibRCheevos.rc_error_t.RC_OK) + { + try + { + var serv_resp = await SendAPIRequest(in api_req).ConfigureAwait(false); + ret = new Bitmap(new MemoryStream(serv_resp)); + } + catch + { + ret = null; + } + } + + _lib.rc_api_destroy_request(ref api_req); + return ret; + } + } +} \ No newline at end of file diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Http.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Http.cs new file mode 100644 index 0000000000..089704b4f9 --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Http.cs @@ -0,0 +1,45 @@ +using System; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Client.EmuHawk +{ + public partial class RCheevos + { + private static readonly HttpClient _http = new(); + + private static async Task HttpGet(string url) + { + _http.DefaultRequestHeaders.ConnectionClose = false; + var response = await _http.GetAsync(url).ConfigureAwait(false); + if (response.IsSuccessStatusCode) + { + return await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false); + } + return null; + } + + private static async Task HttpPost(string url, string post) + { + _http.DefaultRequestHeaders.ConnectionClose = true; + HttpResponseMessage response; + try + { + response = await _http.PostAsync(url + "?" + post, null).ConfigureAwait(false); + } + catch (Exception e) + { + return Encoding.UTF8.GetBytes(e.ToString()); // bleh + } + if (!response.IsSuccessStatusCode) + { + return null; + } + return await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false); + } + + private static Task SendAPIRequest(in LibRCheevos.rc_api_request_t api_req) + => api_req.post_data != IntPtr.Zero ? HttpPost(api_req.URL, api_req.PostData) : HttpGet(api_req.URL); + } +} \ No newline at end of file diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Leaderboards.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Leaderboards.cs new file mode 100644 index 0000000000..7c44d35790 --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Leaderboards.cs @@ -0,0 +1,83 @@ +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Client.EmuHawk +{ + public partial class RCheevos + { + private readonly RCheevosLeaderboardListForm _lboardListForm = new(); + + private bool LBoardsActive { get; set; } + + private LBoard CurrentLboard { get; set; } + + public class LBoard + { + public int ID { get; } + public int Format { get; } + public string Title { get; } + public string Description { get; } + public string Definition { get; } + public bool LowerIsBetter { get; } + public bool Hidden { get; } + public bool Invalid { get; set; } + public string Score { get; private set; } + + private readonly byte[] _scoreFormatBuffer = new byte[1024]; + + public void SetScore(int val) + { + var len = _lib.rc_runtime_format_lboard_value(_scoreFormatBuffer, _scoreFormatBuffer.Length, val, Format); + Score = Encoding.ASCII.GetString(_scoreFormatBuffer, 0, len); + } + + public LBoard(in LibRCheevos.rc_api_leaderboard_definition_t lboard) + { + ID = lboard.id; + Format = lboard.format; + Title = lboard.Title; + Description = lboard.Description; + Definition = lboard.Definition; + LowerIsBetter = lboard.lower_is_better != 0; + Hidden = lboard.hidden != 0; + Invalid = false; + SetScore(0); + } + + public LBoard(in LBoard lboard) + { + ID = lboard.ID; + Format = lboard.Format; + Title = lboard.Title; + Description = lboard.Description; + Definition = lboard.Definition; + LowerIsBetter = lboard.LowerIsBetter; + Hidden = lboard.Hidden; + Invalid = false; + SetScore(0); + } + } + + private static async Task SendTriggerLeaderboardAsync(string username, string api_token, int id, int value, string hash) + { + var api_params = new LibRCheevos.rc_api_submit_lboard_entry_request_t(username, api_token, id, value, hash); + var res = LibRCheevos.rc_error_t.RC_INVALID_STATE; + if (_lib.rc_api_init_submit_lboard_entry_request(out var api_req, ref api_params) == LibRCheevos.rc_error_t.RC_OK) + { + var serv_req = await SendAPIRequest(in api_req).ConfigureAwait(false); + res = _lib.rc_api_process_submit_lboard_entry_response(out var resp, serv_req); + _lib.rc_api_destroy_submit_lboard_entry_response(ref resp); + } + + _lib.rc_api_destroy_request(ref api_req); + + if (res != LibRCheevos.rc_error_t.RC_OK) + { + // todo: warn user? + } + } + + private static async void SendTriggerLeaderboard(string username, string api_token, int id, int value, string hash) + => await SendTriggerLeaderboardAsync(username, api_token, id, value, hash).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Login.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Login.cs new file mode 100644 index 0000000000..5c59de9fdd --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Login.cs @@ -0,0 +1,99 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace BizHawk.Client.EmuHawk +{ + public partial class RCheevos + { + private string Username, ApiToken; + private bool LoggedIn => !string.IsNullOrEmpty(Username) && !string.IsNullOrEmpty(ApiToken); + + private ManualResetEvent InitLoginDone { get; } + + private event Action LoginStatusChanged; + + private async Task LoginCallback(string username, string password) + { + Username = null; + ApiToken = null; + + var api_params = new LibRCheevos.rc_api_login_request_t(username, null, password); + if (_lib.rc_api_init_login_request(out var api_req, ref api_params) == LibRCheevos.rc_error_t.RC_OK) + { + var serv_req = await SendAPIRequest(in api_req).ConfigureAwait(false); + if (_lib.rc_api_process_login_response(out var resp, serv_req) == LibRCheevos.rc_error_t.RC_OK) + { + Username = resp.Username; + ApiToken = resp.ApiToken; + } + + _lib.rc_api_destroy_login_response(ref resp); + } + + _lib.rc_api_destroy_request(ref api_req); + + return LoggedIn; + } + + private async void Login() + { + var config = _getConfig(); + Username = config.RAUsername; + ApiToken = config.RAToken; + + if (LoggedIn) + { + // OK, Username and ApiToken are probably valid, let's ensure they are now + var api_params = new LibRCheevos.rc_api_login_request_t(Username, ApiToken, null); + + Username = null; + ApiToken = null; + + if (_lib.rc_api_init_login_request(out var api_req, ref api_params) == LibRCheevos.rc_error_t.RC_OK) + { + var serv_req = await SendAPIRequest(in api_req).ConfigureAwait(false); + if (_lib.rc_api_process_login_response(out var resp, serv_req) == LibRCheevos.rc_error_t.RC_OK) + { + Username = resp.Username; + ApiToken = resp.ApiToken; + } + + _lib.rc_api_destroy_login_response(ref resp); + } + + _lib.rc_api_destroy_request(ref api_req); + } + + if (LoggedIn) + { + config.RAUsername = Username; + config.RAToken = ApiToken; + InitLoginDone.Set(); + if (EnableSoundEffects) _loginSound.Play(); + return; + } + + using var loginForm = new RCheevosLoginForm(LoginCallback); + loginForm.ShowDialog(); + + config.RAUsername = Username; + config.RAToken = ApiToken; + InitLoginDone.Set(); + + if (LoggedIn && EnableSoundEffects) + { + _loginSound.Play(); + } + } + + private void Logout() + { + var config = _getConfig(); + config.RAUsername = Username = string.Empty; + config.RAToken = ApiToken = string.Empty; + _cachedGameDatas.Clear(); // no longer valid + // should be fine to leave other things be, they'll be reinit on login + } + } +} \ No newline at end of file diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Ping.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Ping.cs new file mode 100644 index 0000000000..a12053eadf --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Ping.cs @@ -0,0 +1,73 @@ +using System; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Client.EmuHawk +{ + public partial class RCheevos + { + private bool RichPresenceActive { get; set; } + + private string CurrentRichPresence { get; set; } + + private static async Task StartGameSessionAsync(string username, string api_token, int id) + { + var api_params = new LibRCheevos.rc_api_start_session_request_t(username, api_token, id); + var res = LibRCheevos.rc_error_t.RC_INVALID_STATE; + if (_lib.rc_api_init_start_session_request(out var api_req, ref api_params) == LibRCheevos.rc_error_t.RC_OK) + { + var serv_req = await SendAPIRequest(in api_req).ConfigureAwait(false); + res = _lib.rc_api_process_start_session_response(out var resp, serv_req); + _lib.rc_api_destroy_start_session_response(ref resp); + } + + _lib.rc_api_destroy_request(ref api_req); + return res == LibRCheevos.rc_error_t.RC_OK; + } + + // todo: warn on failure? + private static async void StartGameSession(string username, string api_token, int id) + => await StartGameSessionAsync(username, api_token, id).ConfigureAwait(false); + + private static async Task SendPingAsync(string username, string api_token, int id, string rich_presence) + { + var api_params = new LibRCheevos.rc_api_ping_request_t(username, api_token, id, rich_presence); + if (_lib.rc_api_init_ping_request(out var api_req, ref api_params) == LibRCheevos.rc_error_t.RC_OK) + { + var serv_req = await SendAPIRequest(in api_req).ConfigureAwait(false); + _lib.rc_api_process_ping_response(out var resp, serv_req); + _lib.rc_api_destroy_ping_response(ref resp); + } + + _lib.rc_api_destroy_request(ref api_req); + } + + private static async void SendPing(string username, string api_token, int id, string rich_presence) + => await SendPingAsync(username, api_token, id, rich_presence).ConfigureAwait(false); + + private readonly byte[] _richPresenceBuffer = new byte[1024]; + + private DateTime _lastPingTime = DateTime.Now; + private static readonly TimeSpan _pingCooldown = new(10000000 * 120); // 2 minutes + + private void CheckPing() + { + if (RichPresenceActive) + { + var len = _lib.rc_runtime_get_richpresence(ref _runtime, _richPresenceBuffer, _richPresenceBuffer.Length, PeekCallback, IntPtr.Zero, IntPtr.Zero); + CurrentRichPresence = Encoding.UTF8.GetString(_richPresenceBuffer, 0, len); + } + else + { + CurrentRichPresence = null; + } + + var now = DateTime.Now; + if ((now - _lastPingTime) >= _pingCooldown) + { + SendPing(Username, ApiToken, _gameData.GameID, CurrentRichPresence); + _lastPingTime = now; + } + } + } +} \ No newline at end of file diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Sound.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Sound.cs new file mode 100644 index 0000000000..8c09fb407d --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Sound.cs @@ -0,0 +1,21 @@ +using System.IO; +using System.Media; + +using BizHawk.Common.PathExtensions; + +namespace BizHawk.Client.EmuHawk +{ + public partial class RCheevos + { + // NOTE: these are net framework only... + // this logic should probably be the main sound class + // this shouldn't be a blocker to moving to net core anyways + private static readonly SoundPlayer _loginSound = new(Path.Combine(PathUtils.ExeDirectoryPath, "overlay/login.wav")); + private static readonly SoundPlayer _unlockSound = new(Path.Combine(PathUtils.ExeDirectoryPath, "overlay/unlock.wav")); + private static readonly SoundPlayer _lboardStartSound = new(Path.Combine(PathUtils.ExeDirectoryPath, "overlay/lb.wav")); + private static readonly SoundPlayer _lboardFailedSound = new(Path.Combine(PathUtils.ExeDirectoryPath, "overlay/lbcancel.wav")); + private static readonly SoundPlayer _infoSound = new(Path.Combine(PathUtils.ExeDirectoryPath, "overlay/info.wav")); + + private bool EnableSoundEffects { get; set; } + } +} \ No newline at end of file diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.cs new file mode 100644 index 0000000000..9d2e605432 --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.cs @@ -0,0 +1,636 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Windows.Forms; + +using BizHawk.BizInvoke; +using BizHawk.Common; +using BizHawk.Common.IOExtensions; +using BizHawk.Client.Common; +using BizHawk.Emulation.Common; + +namespace BizHawk.Client.EmuHawk +{ + public partial class RCheevos : RetroAchievements + { + private static readonly LibRCheevos _lib; + + static RCheevos() + { + var resolver = new DynamicLibraryImportResolver( + OSTailoredCode.IsUnixHost ? "librcheevos.so" : "librcheevos.dll", hasLimitedLifetime: false); + _lib = BizInvoker.GetInvoker(resolver, CallingConventionAdapters.Native); + } + + private LibRCheevos.rc_runtime_t _runtime; + + private readonly Dictionary _readMap = new(); + + private ToolStripMenuItem _hardcoreModeMenuItem; + private bool _hardcoreMode; + + private bool HardcoreMode + { + get => _hardcoreMode; + set => _hardcoreModeMenuItem.Checked = value; + } + + private bool _firstRestart = true; + + private void BuildMenu(ToolStripItemCollection raDropDownItems) + { + raDropDownItems.Clear(); + + var shutDownRAItem = new ToolStripMenuItem("Shutdown RetroAchievements"); + shutDownRAItem.Click += (_, _) => _shutdownRACallback(); + raDropDownItems.Add(shutDownRAItem); + + var autoStartRAItem = new ToolStripMenuItem("Autostart RetroAchievements") + { + Checked = _getConfig().RAAutostart, + CheckOnClick = true, + }; + autoStartRAItem.CheckedChanged += (_, _) => _getConfig().RAAutostart ^= true; + raDropDownItems.Add(autoStartRAItem); + + var loginItem = new ToolStripMenuItem("Login") + { + Visible = !LoggedIn + }; + loginItem.Click += (_, _) => + { + Login(); + _firstRestart = true; // kinda + Restart(); + LoginStatusChanged(); + }; + raDropDownItems.Add(loginItem); + + var logoutItem = new ToolStripMenuItem("Logout") + { + Visible = LoggedIn + }; + logoutItem.Click += (_, _) => + { + Logout(); + LoginStatusChanged(); + }; + raDropDownItems.Add(logoutItem); + + LoginStatusChanged += () => loginItem.Visible = !LoggedIn; + LoginStatusChanged += () => logoutItem.Visible = LoggedIn; + + var tss = new ToolStripSeparator(); + raDropDownItems.Add(tss); + + var enableCheevosItem = new ToolStripMenuItem("Enable Achievements") + { + Checked = CheevosActive, + CheckOnClick = true + }; + enableCheevosItem.CheckedChanged += (_, _) => CheevosActive ^= true; + raDropDownItems.Add(enableCheevosItem); + + var enableLboardsItem = new ToolStripMenuItem("Enable Leaderboards") + { + Checked = LBoardsActive, + CheckOnClick = true, + Enabled = HardcoreMode + }; + enableLboardsItem.CheckedChanged += (_, _) => LBoardsActive ^= true; + raDropDownItems.Add(enableLboardsItem); + + var enableRichPresenceItem = new ToolStripMenuItem("Enable Rich Presence") + { + Checked = RichPresenceActive, + CheckOnClick = true + }; + enableRichPresenceItem.CheckedChanged += (_, _) => RichPresenceActive ^= true; + raDropDownItems.Add(enableRichPresenceItem); + + var enableHardcoreItem = new ToolStripMenuItem("Enable Hardcore Mode") + { + Checked = HardcoreMode, + CheckOnClick = true + }; + enableHardcoreItem.CheckedChanged += (_, _) => + { + _hardcoreMode ^= true; + enableLboardsItem.Enabled = HardcoreMode; + + if (HardcoreMode) + { + _hardcoreMode = _mainForm.RebootCore(); // unset hardcore mode if we fail to reboot core somehow + } + else + { + ToSoftcoreMode(); + } + }; + raDropDownItems.Add(enableHardcoreItem); + + _hardcoreModeMenuItem = enableHardcoreItem; + + var enableSoundEffectsItem = new ToolStripMenuItem("Enable Sound Effects") + { + Checked = EnableSoundEffects, + CheckOnClick = true + }; + enableSoundEffectsItem.CheckedChanged += (_, _) => EnableSoundEffects ^= true; + raDropDownItems.Add(enableSoundEffectsItem); + + var enableUnofficialCheevosItem = new ToolStripMenuItem("Test Unofficial Achievements") + { + Checked = AllowUnofficialCheevos, + CheckOnClick = true + }; + enableUnofficialCheevosItem.CheckedChanged += (_, _) => ToggleUnofficialCheevos(); + raDropDownItems.Add(enableUnofficialCheevosItem); + + tss = new ToolStripSeparator(); + raDropDownItems.Add(tss); + + var viewGameInfoItem = new ToolStripMenuItem("View Game Info"); + viewGameInfoItem.Click += (_, _) => + { + _gameInfoForm.OnFrameAdvance(_gameData.GameBadge, _gameData.TotalCheevoPoints(HardcoreMode), + CurrentLboard is null ? "N/A" : $"{CurrentLboard.Description} ({CurrentLboard.Score})", + CurrentRichPresence ?? "N/A"); + + _gameInfoForm.Show(); + }; + raDropDownItems.Add(viewGameInfoItem); + + var viewCheevoListItem = new ToolStripMenuItem("View Achievement List"); + viewCheevoListItem.Click += (_, _) => + { + _cheevoListForm.OnFrameAdvance(HardcoreMode, true); + _cheevoListForm.Show(); + }; + raDropDownItems.Add(viewCheevoListItem); + + var viewLboardListItem = new ToolStripMenuItem("View Leaderboard List"); + viewLboardListItem.Click += (_, _) => + { + _lboardListForm.OnFrameAdvance(true); + _lboardListForm.Show(); + }; + raDropDownItems.Add(viewLboardListItem); + } + + protected override void HandleHardcoreModeDisable(string reason) + { + _mainForm.ShowMessageBox(null, $"{reason} Disabling hardcore mode.", "Warning", EMsgBoxIcon.Warning); + HardcoreMode = false; + } + + public RCheevos(IMainFormForRetroAchievements mainForm, InputManager inputManager, ToolManager tools, + Func getConfig, ToolStripItemCollection raDropDownItems, Action shutdownRACallback) + : base(mainForm, inputManager, tools, getConfig, raDropDownItems, shutdownRACallback) + { + _runtime = default; + _lib.rc_runtime_init(ref _runtime); + InitLoginDone = new(false); + Login(); + InitLoginDone.WaitOne(); + + var config = _getConfig(); + CheevosActive = config.RACheevosActive; + LBoardsActive = config.RALBoardsActive; + RichPresenceActive = config.RARichPresenceActive; + _hardcoreMode = config.RAHardcoreMode; + EnableSoundEffects = config.RASoundEffects; + AllowUnofficialCheevos = config.RAAllowUnofficialCheevos; + + BuildMenu(raDropDownItems); + } + + public override void Dispose() + { + _lib.rc_runtime_destroy(ref _runtime); + Stop(); + _gameInfoForm.Dispose(); + _cheevoListForm.Dispose(); + _lboardListForm.Dispose(); + } + + public override void OnSaveState(string path) + { + if (!LoggedIn) + { + return; + } + + var size = _lib.rc_runtime_progress_size(ref _runtime, IntPtr.Zero); + if (size > 0) + { + var buffer = new byte[(int)size]; + _lib.rc_runtime_serialize_progress(buffer, ref _runtime, IntPtr.Zero); + using var file = File.OpenWrite(path + ".rap"); + file.Write(buffer, 0, buffer.Length); + } + } + + public override void OnLoadState(string path) + { + if (!LoggedIn) + { + return; + } + + if (HardcoreMode) + { + HandleHardcoreModeDisable("Loading savestates is not allowed in hardcore mode."); + } + + _lib.rc_runtime_reset(ref _runtime); + + if (File.Exists(path + ".rap")) + { + using var file = File.OpenRead(path + ".rap"); + var buffer = file.ReadAllBytes(); + _lib.rc_runtime_deserialize_progress(ref _runtime, buffer, IntPtr.Zero); + } + } + + // not sure if we really need to do anything here... + // nice way to ensure config is written back every so often (and on close) + public override void Stop() + { + var config = _getConfig(); + config.RACheevosActive = CheevosActive; + config.RALBoardsActive = LBoardsActive; + config.RARichPresenceActive = RichPresenceActive; + config.RAHardcoreMode = HardcoreMode; + config.RASoundEffects = EnableSoundEffects; + config.RAAllowUnofficialCheevos = AllowUnofficialCheevos; + } + + public override void Restart() + { + if (_firstRestart) + { + _firstRestart = false; + if (HardcoreMode) + { + HardcoreMode = _mainForm.RebootCore(); // unset hardcore mode if we fail to reboot core somehow + if (HardcoreMode && _mainForm.CurrentlyOpenRomArgs is not null) + { + // if we aren't hardcore anymore, we failed to reboot the core (and didn't call Restart probably) + // if CurrentlyOpenRomArgs is null, then Restart won't be called (as RebootCore returns true immediately), so + return; + } + } + } + + if (!LoggedIn) + { + return; + } + + // reinit the runtime + _lib.rc_runtime_destroy(ref _runtime); + _runtime = default; + _lib.rc_runtime_init(ref _runtime); + + // get console id + _consoleId = SystemIdToConsoleId(); + + // init the read map + _readMap.Clear(); + + if (Emu.HasMemoryDomains()) + { + _memFunctions = CreateMemoryBanks(_consoleId, Domains, Emu.CanDebug() ? Emu.AsDebuggable() : null); + + var addr = 0; + foreach (var memFunctions in _memFunctions) + { + if (memFunctions.ReadFunc is not null) + { + for (int i = 0; i < memFunctions.BankSize; i++) + { + _readMap.Add(addr + i, (memFunctions.ReadFunc, addr)); + } + } + + addr += memFunctions.BankSize; + } + } + + // verify and init whatever is loaded + AllGamesVerified = true; + _gameHash = null; // will be set by first IdentifyHash + + if (_mainForm.CurrentlyOpenRomArgs is not null) + { + var ids = GetRAGameIds(_mainForm.CurrentlyOpenRomArgs.OpenAdvanced, _consoleId); + + AllGamesVerified = !ids.Contains(0); + + var gameId = ids.Count > 0 ? ids[0] : 0; + + if (gameId != 0) + { + if (_cachedGameDatas.TryGetValue(gameId, out var cachedGameData)) + { + _gameData = new GameData(cachedGameData, () => AllowUnofficialCheevos); + } + else + { + _gameData = GetGameData(Username, ApiToken, gameId, () => AllowUnofficialCheevos); + } + + StartGameSession(Username, ApiToken, gameId); + + _cachedGameDatas.Remove(gameId); + _cachedGameDatas.Add(gameId, _gameData); + + InitGameData(_gameData, Username, ApiToken, HardcoreMode); + + foreach (var lboard in _gameData.LBoardEnumerable) + { + _lib.rc_runtime_activate_lboard(ref _runtime, lboard.ID, lboard.Definition, IntPtr.Zero, 0); + } + + if (_gameData.RichPresenseScript is not null) + { + _lib.rc_runtime_activate_richpresence(ref _runtime, _gameData.RichPresenseScript, IntPtr.Zero, 0); + } + + var waitInit = HardcoreMode ? _gameData.HardcoreInitUnlocksReady : _gameData.SoftcoreInitUnlocksReady; + // hopefully not too long, given we spent some time doing other work + waitInit.WaitOne(); + + foreach (var cheevo in _gameData.CheevoEnumerable) + { + if (cheevo.IsEnabled && !cheevo.IsUnlocked(HardcoreMode)) + { + _lib.rc_runtime_activate_achievement(ref _runtime, cheevo.ID, cheevo.Definition, IntPtr.Zero, 0); + } + } + } + else + { + _gameData = new GameData(); + } + } + else + { + _gameData = new GameData(); + } + + // validate addresses now that we have cheevos init + _lib.rc_runtime_validate_addresses(ref _runtime, EventHandlerCallback, address => _readMap.ContainsKey(address)); + + _gameInfoForm.Restart(_gameData.Title, _gameData.TotalCheevoPoints(HardcoreMode), CurrentRichPresence ?? "N/A"); + _cheevoListForm.Restart(_gameData.GameID == 0 ? Array.Empty() : _gameData.CheevoEnumerable, GetCheevoProgress); + _lboardListForm.Restart(_gameData.GameID == 0 ? Array.Empty() : _gameData.LBoardEnumerable); + + Update(); + + // note: this can only catch quicksaves (probably only case of accidential use from hotkeys) + _mainForm.EmuClient.BeforeQuickLoad += (_, e) => + { + if (HardcoreMode) + { + e.Handled = _mainForm.ShowMessageBox2(null, "Loading a quicksave is not allowed in hardcode mode. Abort loading state?", "Warning", EMsgBoxIcon.Warning); + } + }; + } + + public override void Update() + { + if (!LoggedIn) + { + return; + } + + if (HardcoreMode) + { + CheckHardcoreModeConditions(); + } + + if (_gameData.GameID != 0) + { + CheckPing(); + } + } + + private unsafe void EventHandlerCallback(IntPtr runtime_event) + { + var evt = (LibRCheevos.rc_runtime_event_t*)runtime_event; + switch (evt->type) + { + case LibRCheevos.rc_runtime_event_type_t.RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED: + { + if (!CheevosActive) return; + + var cheevo = _gameData.GetCheevoById(evt->id); + if (cheevo.IsEnabled) + { + _lib.rc_runtime_deactivate_achievement(ref _runtime, evt->id); + + cheevo.SetUnlocked(HardcoreMode, true); + var prefix = HardcoreMode ? "[HARDCORE] " : ""; + _mainForm.AddOnScreenMessage($"{prefix}Achievement Unlocked!"); + _mainForm.AddOnScreenMessage(cheevo.Description); + if (EnableSoundEffects) _unlockSound.Play(); + + if (cheevo.IsOfficial) + { + SendUnlockAchievement(Username, ApiToken, evt->id, HardcoreMode, _gameHash); + } + } + + break; + } + case LibRCheevos.rc_runtime_event_type_t.RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED: + { + if (!CheevosActive) return; + + var cheevo = _gameData.GetCheevoById(evt->id); + if (cheevo.IsEnabled) + { + cheevo.IsPrimed = true; + var prefix = HardcoreMode ? "[HARDCORE] " : ""; + _mainForm.AddOnScreenMessage($"{prefix}Achievement Primed!"); + _mainForm.AddOnScreenMessage(cheevo.Description); + if (EnableSoundEffects) _infoSound.Play(); + } + + break; + } + case LibRCheevos.rc_runtime_event_type_t.RC_RUNTIME_EVENT_LBOARD_STARTED: + { + if (!LBoardsActive || !HardcoreMode) return; + + var lboard = _gameData.GetLboardById(evt->id); + if (!lboard.Invalid) + { + lboard.SetScore(evt->value); + + if (!lboard.Hidden) + { + CurrentLboard = lboard; + _mainForm.AddOnScreenMessage($"Leaderboard Attempt Started!"); + _mainForm.AddOnScreenMessage(lboard.Description); + if (EnableSoundEffects) _lboardStartSound.Play(); + } + } + + break; + } + case LibRCheevos.rc_runtime_event_type_t.RC_RUNTIME_EVENT_LBOARD_CANCELED: + { + if (!LBoardsActive || !HardcoreMode) return; + + var lboard = _gameData.GetLboardById(evt->id); + if (!lboard.Invalid) + { + if (!lboard.Hidden) + { + if (lboard == CurrentLboard) + { + CurrentLboard = null; + } + + _mainForm.AddOnScreenMessage($"Leaderboard Attempt Failed! ({lboard.Score})"); + _mainForm.AddOnScreenMessage(lboard.Description); + if (EnableSoundEffects) _lboardFailedSound.Play(); + } + + lboard.SetScore(0); + } + + break; + } + case LibRCheevos.rc_runtime_event_type_t.RC_RUNTIME_EVENT_LBOARD_UPDATED: + { + if (!LBoardsActive || !HardcoreMode) return; + + var lboard = _gameData.GetLboardById(evt->id); + if (!lboard.Invalid) + { + lboard.SetScore(evt->value); + } + + break; + } + case LibRCheevos.rc_runtime_event_type_t.RC_RUNTIME_EVENT_LBOARD_TRIGGERED: + { + if (!LBoardsActive || !HardcoreMode) return; + + var lboard = _gameData.GetLboardById(evt->id); + if (!lboard.Invalid) + { + SendTriggerLeaderboard(Username, ApiToken, evt->id, evt->value, _gameHash); + + if (!lboard.Hidden) + { + if (lboard == CurrentLboard) + { + CurrentLboard = null; + } + + _mainForm.AddOnScreenMessage($"Leaderboard Attempt Complete! ({lboard.Score})"); + _mainForm.AddOnScreenMessage(lboard.Description); + if (EnableSoundEffects) _unlockSound.Play(); + } + } + + break; + } + case LibRCheevos.rc_runtime_event_type_t.RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED: + { + var cheevo = _gameData.GetCheevoById(evt->id); + cheevo.Invalid = true; + break; + } + case LibRCheevos.rc_runtime_event_type_t.RC_RUNTIME_EVENT_LBOARD_DISABLED: + { + var lboard = _gameData.GetLboardById(evt->id); + lboard.Invalid = true; + break; + } + case LibRCheevos.rc_runtime_event_type_t.RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED: + { + var cheevo = _gameData.GetCheevoById(evt->id); + if (cheevo.IsEnabled) + { + cheevo.IsPrimed = false; + var prefix = HardcoreMode ? "[HARDCORE] " : ""; + _mainForm.AddOnScreenMessage($"{prefix}Achievement Unprimed!"); + _mainForm.AddOnScreenMessage(cheevo.Description); + if (EnableSoundEffects) _infoSound.Play(); + } + + break; + } + } + } + + private int PeekCallback(int address, int num_bytes, IntPtr ud) + { + byte Peek(int addr) + => _readMap.TryGetValue(addr, out var reader) ? reader.Func(addr - reader.Start) : (byte)0; + + return num_bytes switch + { + 1 => Peek(address), + 2 => Peek(address) | (Peek(address + 1) << 8), + 4 => Peek(address) | (Peek(address + 1) << 8) | (Peek(address + 2) << 16) | (Peek(address + 3) << 24), + _ => throw new InvalidOperationException($"Requested {num_bytes} in {nameof(PeekCallback)}"), + }; + } + + public override void OnFrameAdvance() + { + if (!LoggedIn) + { + return; + } + + var input = _inputManager.ControllerOutput; + foreach (var resetButton in input.Definition.BoolButtons.Where(b => b.Contains("Power") || b.Contains("Reset"))) + { + if (input.IsPressed(resetButton)) + { + _lib.rc_runtime_reset(ref _runtime); + break; + } + } + + if (Emu.HasMemoryDomains()) + { + // we want to EnterExit to prevent wbx host spam when peeks are spammed + using (Domains.MainMemory.EnterExit()) + { + _lib.rc_runtime_do_frame(ref _runtime, EventHandlerCallback, PeekCallback, IntPtr.Zero, IntPtr.Zero); + } + } + else + { + _lib.rc_runtime_do_frame(ref _runtime, EventHandlerCallback, PeekCallback, IntPtr.Zero, IntPtr.Zero); + } + + if (_gameInfoForm.IsShown) + { + _gameInfoForm.OnFrameAdvance(_gameData.GameBadge, _gameData.TotalCheevoPoints(HardcoreMode), + CurrentLboard is null ? "N/A" : $"{CurrentLboard.Description} ({CurrentLboard.Score})", + CurrentRichPresence ?? "N/A"); + } + + if (_cheevoListForm.IsShown) + { + _cheevoListForm.OnFrameAdvance(HardcoreMode); + } + + if (_lboardListForm.IsShown) + { + _lboardListForm.OnFrameAdvance(); + } + } + } +} \ No newline at end of file diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosAchievementForm.Designer.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosAchievementForm.Designer.cs new file mode 100644 index 0000000000..0e002bcf9f --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosAchievementForm.Designer.cs @@ -0,0 +1,224 @@ +namespace BizHawk.Client.EmuHawk +{ + partial class RCheevosAchievementForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.cheevoBadgeBox = new System.Windows.Forms.PictureBox(); + this.titleLabel = new System.Windows.Forms.Label(); + this.descriptionLabel = new System.Windows.Forms.Label(); + this.titleBox = new System.Windows.Forms.TextBox(); + this.descriptionBox = new System.Windows.Forms.TextBox(); + this.pointsLabel = new System.Windows.Forms.Label(); + this.pointsBox = new System.Windows.Forms.TextBox(); + this.progressBox = new System.Windows.Forms.TextBox(); + this.progressLabel = new System.Windows.Forms.Label(); + this.unofficialCheckBox = new System.Windows.Forms.CheckBox(); + this.hcUnlockedCheckBox = new System.Windows.Forms.CheckBox(); + this.primedCheckBox = new System.Windows.Forms.CheckBox(); + this.scUnlockedCheckBox = new System.Windows.Forms.CheckBox(); + ((System.ComponentModel.ISupportInitialize)(this.cheevoBadgeBox)).BeginInit(); + this.SuspendLayout(); + // + // cheevoBadgeBox + // + this.cheevoBadgeBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left))); + this.cheevoBadgeBox.Location = new System.Drawing.Point(12, 12); + this.cheevoBadgeBox.Name = "cheevoBadgeBox"; + this.cheevoBadgeBox.Size = new System.Drawing.Size(120, 120); + this.cheevoBadgeBox.TabIndex = 0; + this.cheevoBadgeBox.TabStop = false; + // + // titleLabel + // + this.titleLabel.AutoSize = true; + this.titleLabel.Location = new System.Drawing.Point(171, 15); + this.titleLabel.Name = "titleLabel"; + this.titleLabel.Size = new System.Drawing.Size(30, 13); + this.titleLabel.TabIndex = 1; + this.titleLabel.Text = "Title:"; + // + // descriptionLabel + // + this.descriptionLabel.AutoSize = true; + this.descriptionLabel.Location = new System.Drawing.Point(138, 40); + this.descriptionLabel.Name = "descriptionLabel"; + this.descriptionLabel.Size = new System.Drawing.Size(63, 13); + this.descriptionLabel.TabIndex = 2; + this.descriptionLabel.Text = "Description:"; + // + // titleBox + // + this.titleBox.Location = new System.Drawing.Point(207, 12); + this.titleBox.Name = "titleBox"; + this.titleBox.ReadOnly = true; + this.titleBox.Size = new System.Drawing.Size(285, 20); + this.titleBox.TabIndex = 3; + // + // descriptionBox + // + this.descriptionBox.Location = new System.Drawing.Point(207, 37); + this.descriptionBox.Name = "descriptionBox"; + this.descriptionBox.ReadOnly = true; + this.descriptionBox.Size = new System.Drawing.Size(285, 20); + this.descriptionBox.TabIndex = 4; + // + // pointsLabel + // + this.pointsLabel.AutoSize = true; + this.pointsLabel.Location = new System.Drawing.Point(162, 66); + this.pointsLabel.Name = "pointsLabel"; + this.pointsLabel.Size = new System.Drawing.Size(39, 13); + this.pointsLabel.TabIndex = 5; + this.pointsLabel.Text = "Points:"; + // + // pointsBox + // + this.pointsBox.Location = new System.Drawing.Point(207, 63); + this.pointsBox.Name = "pointsBox"; + this.pointsBox.ReadOnly = true; + this.pointsBox.Size = new System.Drawing.Size(285, 20); + this.pointsBox.TabIndex = 6; + // + // progressBox + // + this.progressBox.Location = new System.Drawing.Point(207, 89); + this.progressBox.Name = "progressBox"; + this.progressBox.ReadOnly = true; + this.progressBox.Size = new System.Drawing.Size(285, 20); + this.progressBox.TabIndex = 7; + // + // progressLabel + // + this.progressLabel.AutoSize = true; + this.progressLabel.Location = new System.Drawing.Point(150, 92); + this.progressLabel.Name = "progressLabel"; + this.progressLabel.Size = new System.Drawing.Size(51, 13); + this.progressLabel.TabIndex = 8; + this.progressLabel.Text = "Progress:"; + // + // unofficialCheckBox + // + this.unofficialCheckBox.AutoCheck = false; + this.unofficialCheckBox.AutoSize = true; + this.unofficialCheckBox.CheckAlign = System.Drawing.ContentAlignment.MiddleRight; + this.unofficialCheckBox.Location = new System.Drawing.Point(419, 115); + this.unofficialCheckBox.Name = "unofficialCheckBox"; + this.unofficialCheckBox.RightToLeft = System.Windows.Forms.RightToLeft.No; + this.unofficialCheckBox.Size = new System.Drawing.Size(73, 17); + this.unofficialCheckBox.TabIndex = 9; + this.unofficialCheckBox.Text = "Unofficial:"; + this.unofficialCheckBox.UseVisualStyleBackColor = true; + // + // hcUnlockedCheckBox + // + this.hcUnlockedCheckBox.AutoCheck = false; + this.hcUnlockedCheckBox.AutoSize = true; + this.hcUnlockedCheckBox.CheckAlign = System.Drawing.ContentAlignment.MiddleRight; + this.hcUnlockedCheckBox.Location = new System.Drawing.Point(247, 115); + this.hcUnlockedCheckBox.Name = "hcUnlockedCheckBox"; + this.hcUnlockedCheckBox.RightToLeft = System.Windows.Forms.RightToLeft.No; + this.hcUnlockedCheckBox.Size = new System.Drawing.Size(99, 17); + this.hcUnlockedCheckBox.TabIndex = 10; + this.hcUnlockedCheckBox.Text = "(HC) Unlocked:"; + this.hcUnlockedCheckBox.UseVisualStyleBackColor = true; + // + // primedCheckBox + // + this.primedCheckBox.AutoCheck = false; + this.primedCheckBox.AutoSize = true; + this.primedCheckBox.CheckAlign = System.Drawing.ContentAlignment.MiddleRight; + this.primedCheckBox.Location = new System.Drawing.Point(352, 115); + this.primedCheckBox.Name = "primedCheckBox"; + this.primedCheckBox.RightToLeft = System.Windows.Forms.RightToLeft.No; + this.primedCheckBox.Size = new System.Drawing.Size(61, 17); + this.primedCheckBox.TabIndex = 11; + this.primedCheckBox.Text = "Primed:"; + this.primedCheckBox.UseVisualStyleBackColor = true; + // + // scUnlockedCheckBox + // + this.scUnlockedCheckBox.AutoCheck = false; + this.scUnlockedCheckBox.AutoSize = true; + this.scUnlockedCheckBox.CheckAlign = System.Drawing.ContentAlignment.MiddleRight; + this.scUnlockedCheckBox.Location = new System.Drawing.Point(143, 115); + this.scUnlockedCheckBox.Name = "scUnlockedCheckBox"; + this.scUnlockedCheckBox.RightToLeft = System.Windows.Forms.RightToLeft.No; + this.scUnlockedCheckBox.Size = new System.Drawing.Size(98, 17); + this.scUnlockedCheckBox.TabIndex = 12; + this.scUnlockedCheckBox.Text = "(SC) Unlocked:"; + this.scUnlockedCheckBox.UseVisualStyleBackColor = true; + // + // RCheevosAchievementForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(504, 120); + this.ControlBox = false; + this.Controls.Add(this.scUnlockedCheckBox); + this.Controls.Add(this.primedCheckBox); + this.Controls.Add(this.hcUnlockedCheckBox); + this.Controls.Add(this.unofficialCheckBox); + this.Controls.Add(this.progressLabel); + this.Controls.Add(this.progressBox); + this.Controls.Add(this.pointsBox); + this.Controls.Add(this.pointsLabel); + this.Controls.Add(this.descriptionBox); + this.Controls.Add(this.titleBox); + this.Controls.Add(this.descriptionLabel); + this.Controls.Add(this.titleLabel); + this.Controls.Add(this.cheevoBadgeBox); + this.MaximizeBox = false; + this.MinimizeBox = false; + this.MinimumSize = new System.Drawing.Size(300, 120); + this.Name = "RCheevosAchievementForm"; + this.ShowIcon = false; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + ((System.ComponentModel.ISupportInitialize)(this.cheevoBadgeBox)).EndInit(); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.PictureBox cheevoBadgeBox; + private System.Windows.Forms.Label titleLabel; + private System.Windows.Forms.Label descriptionLabel; + private System.Windows.Forms.TextBox titleBox; + private System.Windows.Forms.TextBox descriptionBox; + private System.Windows.Forms.Label pointsLabel; + private System.Windows.Forms.TextBox pointsBox; + private System.Windows.Forms.TextBox progressBox; + private System.Windows.Forms.Label progressLabel; + private System.Windows.Forms.CheckBox unofficialCheckBox; + private System.Windows.Forms.CheckBox hcUnlockedCheckBox; + private System.Windows.Forms.CheckBox primedCheckBox; + private System.Windows.Forms.CheckBox scUnlockedCheckBox; + } +} \ No newline at end of file diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosAchievementForm.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosAchievementForm.cs new file mode 100644 index 0000000000..db753ffbef --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosAchievementForm.cs @@ -0,0 +1,84 @@ +using System; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Windows.Forms; + +namespace BizHawk.Client.EmuHawk +{ + /// + /// Shows information about a specific achievement + /// + public partial class RCheevosAchievementForm : Form + { + public int OrderByKey() + { + var ret = 0; + ret += hcUnlockedCheckBox.Checked ? 3 : 0; + ret += scUnlockedCheckBox.Checked ? 2 : 0; + ret += primedCheckBox.Checked ? 1 : 0; + ret += string.IsNullOrEmpty(progressBox.Text) ? 0 : 1; + ret += unofficialCheckBox.Checked ? -10 : 0; + return ret; + } + + private Bitmap _unlockedBadge, _lockedBadge; + private readonly RCheevos.Cheevo _cheevo; + private readonly Func _getCheevoProgress; + + public RCheevosAchievementForm(RCheevos.Cheevo cheevo, Func getCheevoProgress) + { + InitializeComponent(); + titleBox.Text = cheevo.Title; + descriptionBox.Text = cheevo.Description; + pointsBox.Text = cheevo.Points.ToString(); + progressBox.Text = getCheevoProgress(cheevo.ID); + unofficialCheckBox.Checked = !cheevo.IsOfficial; + hcUnlockedCheckBox.Checked = cheevo.IsHardcoreUnlocked; + primedCheckBox.Checked = cheevo.IsPrimed; + scUnlockedCheckBox.Checked = cheevo.IsSoftcoreUnlocked; + _cheevo = cheevo; + _getCheevoProgress = getCheevoProgress; + TopLevel = false; + Show(); + } + + private static Bitmap UpscaleBadge(Bitmap src) + { + var ret = new Bitmap(120, 120); + using var g = Graphics.FromImage(ret); + g.InterpolationMode = InterpolationMode.NearestNeighbor; + g.PixelOffsetMode = PixelOffsetMode.Half; + g.DrawImage(src, 0, 0, 120, 120); + return ret; + } + + public void OnFrameAdvance(bool hardcore) + { + var unlockedBadge = _cheevo.BadgeUnlocked; + if (_unlockedBadge is null && unlockedBadge is not null) + { + _unlockedBadge = UpscaleBadge(unlockedBadge); + } + + var lockedBadge = _cheevo.BadgeLocked; + if (_lockedBadge is null && lockedBadge is not null) + { + _lockedBadge = UpscaleBadge(lockedBadge); + } + + var badge = _cheevo.IsUnlocked(hardcore) ? _unlockedBadge : _lockedBadge; + + if (cheevoBadgeBox.Image != badge) + { + cheevoBadgeBox.Image = badge; + } + + pointsBox.Text = _cheevo.Points.ToString(); + progressBox.Text = _getCheevoProgress(_cheevo.ID); + hcUnlockedCheckBox.Checked = _cheevo.IsHardcoreUnlocked; + primedCheckBox.Checked = _cheevo.IsPrimed; + scUnlockedCheckBox.Checked = _cheevo.IsSoftcoreUnlocked; + } + } +} + diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosAchievementForm.resx b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosAchievementForm.resx new file mode 100644 index 0000000000..29dcb1b3a3 --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosAchievementForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosAchievementListForm.Designer.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosAchievementListForm.Designer.cs new file mode 100644 index 0000000000..d671e02f94 --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosAchievementListForm.Designer.cs @@ -0,0 +1,68 @@ +namespace BizHawk.Client.EmuHawk +{ + partial class RCheevosAchievementListForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.flowLayoutPanel1 = new System.Windows.Forms.FlowLayoutPanel(); + this.SuspendLayout(); + // + // flowLayoutPanel1 + // + this.flowLayoutPanel1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.flowLayoutPanel1.AutoScroll = true; + this.flowLayoutPanel1.Location = new System.Drawing.Point(12, 12); + this.flowLayoutPanel1.MinimumSize = new System.Drawing.Size(544, 0); + this.flowLayoutPanel1.Name = "flowLayoutPanel1"; + this.flowLayoutPanel1.Size = new System.Drawing.Size(544, 567); + this.flowLayoutPanel1.TabIndex = 0; + // + // RCheevosAchievementListForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowOnly; + this.ClientSize = new System.Drawing.Size(568, 591); + this.Controls.Add(this.flowLayoutPanel1); + this.MaximizeBox = false; + this.MinimizeBox = false; + this.MinimumSize = new System.Drawing.Size(574, 244); + this.Name = "RCheevosAchievementListForm"; + this.ShowIcon = false; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + this.Text = "Achievement List"; + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.FlowLayoutPanel flowLayoutPanel1; + } +} \ No newline at end of file diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosAchievementListForm.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosAchievementListForm.cs new file mode 100644 index 0000000000..0d5d946753 --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosAchievementListForm.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Forms; + +namespace BizHawk.Client.EmuHawk +{ + /// + /// Shows a list of a user's current achievements + /// + public partial class RCheevosAchievementListForm : Form + { + public bool IsShown { get; private set; } + + private RCheevosAchievementForm[] _cheevoForms; + private int _updateCooldown; + + public RCheevosAchievementListForm() + { + InitializeComponent(); + FormClosing += RCheevosAchievementListForm_FormClosing; + Shown += (_, _) => IsShown = true; + _cheevoForms = Array.Empty(); + _updateCooldown = 5; // only update every 5 frames / 12 fps (as this is rather expensive to update) + } + + private void DisposeCheevoForms() + { + foreach (var cheevoForm in _cheevoForms) + { + cheevoForm.Dispose(); + } + } + + public void Restart(IEnumerable cheevos, Func getCheevoProgress) + { + flowLayoutPanel1.Controls.Clear(); + DisposeCheevoForms(); + var cheevoForms = new List(); + foreach (var cheevo in cheevos) + { + cheevoForms.Add(new(cheevo, getCheevoProgress)); + } + _cheevoForms = cheevoForms.OrderByDescending(f => f.OrderByKey()).ToArray(); + flowLayoutPanel1.Controls.AddRange(_cheevoForms); + } + + public void OnFrameAdvance(bool hardcore, bool forceUpdate = false) + { + _updateCooldown--; + if (_updateCooldown == 0 || forceUpdate) + { + _updateCooldown = 5; + + for (int i = 0; i < _cheevoForms.Length; i++) + { + _cheevoForms[i].OnFrameAdvance(hardcore); + } + + var reorderedForms = _cheevoForms.OrderByDescending(f => f.OrderByKey()).ToArray(); + + for (int i = 0; i < _cheevoForms.Length; i++) + { + if (_cheevoForms[i] != reorderedForms[i]) + { + flowLayoutPanel1.Controls.SetChildIndex(reorderedForms[i], i); + } + } + + _cheevoForms = reorderedForms; + } + } + + private void RCheevosAchievementListForm_FormClosing(object sender, FormClosingEventArgs e) + { + Hide(); + e.Cancel = true; + IsShown = false; + } + } +} + diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosAchievementListForm.resx b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosAchievementListForm.resx new file mode 100644 index 0000000000..29dcb1b3a3 --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosAchievementListForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosGameInfoForm.Designer.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosGameInfoForm.Designer.cs new file mode 100644 index 0000000000..b12467c83f --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosGameInfoForm.Designer.cs @@ -0,0 +1,178 @@ +namespace BizHawk.Client.EmuHawk +{ + partial class RCheevosGameInfoForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.gameIconBox = new System.Windows.Forms.PictureBox(); + this.label2 = new System.Windows.Forms.Label(); + this.label1 = new System.Windows.Forms.Label(); + this.label3 = new System.Windows.Forms.Label(); + this.label5 = new System.Windows.Forms.Label(); + this.titleTextBox = new System.Windows.Forms.TextBox(); + this.totalPointsBox = new System.Windows.Forms.TextBox(); + this.currentLboardBox = new System.Windows.Forms.TextBox(); + this.richPresenceBox = new System.Windows.Forms.TextBox(); + ((System.ComponentModel.ISupportInitialize)(this.gameIconBox)).BeginInit(); + this.SuspendLayout(); + // + // gameIconBox + // + this.gameIconBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left))); + this.gameIconBox.Location = new System.Drawing.Point(12, 12); + this.gameIconBox.Name = "gameIconBox"; + this.gameIconBox.Size = new System.Drawing.Size(100, 100); + this.gameIconBox.TabIndex = 0; + this.gameIconBox.TabStop = false; + // + // label2 + // + this.label2.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.label2.AutoSize = true; + this.label2.Location = new System.Drawing.Point(118, 68); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(107, 13); + this.label2.TabIndex = 2; + this.label2.Text = "Current Leaderboard:"; + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(195, 15); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(30, 13); + this.label1.TabIndex = 1; + this.label1.Text = "Title:"; + // + // label3 + // + this.label3.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.label3.AutoSize = true; + this.label3.Location = new System.Drawing.Point(145, 95); + this.label3.Name = "label3"; + this.label3.Size = new System.Drawing.Size(80, 13); + this.label3.TabIndex = 3; + this.label3.Text = "Rich Presence:"; + // + // label5 + // + this.label5.AutoSize = true; + this.label5.Location = new System.Drawing.Point(122, 41); + this.label5.Name = "label5"; + this.label5.Size = new System.Drawing.Size(103, 13); + this.label5.TabIndex = 5; + this.label5.Text = "Total Earned Points:"; + // + // titleTextBox + // + this.titleTextBox.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.titleTextBox.Location = new System.Drawing.Point(231, 12); + this.titleTextBox.Name = "titleTextBox"; + this.titleTextBox.ReadOnly = true; + this.titleTextBox.Size = new System.Drawing.Size(261, 20); + this.titleTextBox.TabIndex = 6; + // + // totalPointsBox + // + this.totalPointsBox.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.totalPointsBox.Location = new System.Drawing.Point(231, 38); + this.totalPointsBox.Name = "totalPointsBox"; + this.totalPointsBox.ReadOnly = true; + this.totalPointsBox.Size = new System.Drawing.Size(261, 20); + this.totalPointsBox.TabIndex = 7; + // + // currentLboardBox + // + this.currentLboardBox.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.currentLboardBox.Location = new System.Drawing.Point(231, 65); + this.currentLboardBox.Name = "currentLboardBox"; + this.currentLboardBox.ReadOnly = true; + this.currentLboardBox.Size = new System.Drawing.Size(261, 20); + this.currentLboardBox.TabIndex = 8; + // + // richPresenceBox + // + this.richPresenceBox.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.richPresenceBox.Location = new System.Drawing.Point(231, 92); + this.richPresenceBox.Name = "richPresenceBox"; + this.richPresenceBox.ReadOnly = true; + this.richPresenceBox.Size = new System.Drawing.Size(261, 20); + this.richPresenceBox.TabIndex = 9; + // + // RCheevosGameInfoForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(504, 121); + this.Controls.Add(this.richPresenceBox); + this.Controls.Add(this.currentLboardBox); + this.Controls.Add(this.totalPointsBox); + this.Controls.Add(this.titleTextBox); + this.Controls.Add(this.label5); + this.Controls.Add(this.label3); + this.Controls.Add(this.label2); + this.Controls.Add(this.label1); + this.Controls.Add(this.gameIconBox); + this.MaximizeBox = false; + this.MinimizeBox = false; + this.MinimumSize = new System.Drawing.Size(300, 160); + this.Name = "RCheevosGameInfoForm"; + this.ShowIcon = false; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + this.Text = "Game Info"; + ((System.ComponentModel.ISupportInitialize)(this.gameIconBox)).EndInit(); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.PictureBox gameIconBox; + private System.Windows.Forms.Label label2; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.Label label3; + private System.Windows.Forms.Label label5; + private System.Windows.Forms.TextBox titleTextBox; + private System.Windows.Forms.TextBox totalPointsBox; + private System.Windows.Forms.TextBox currentLboardBox; + private System.Windows.Forms.TextBox richPresenceBox; + } +} \ No newline at end of file diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosGameInfoForm.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosGameInfoForm.cs new file mode 100644 index 0000000000..7eb6e44cae --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosGameInfoForm.cs @@ -0,0 +1,53 @@ +using System.Drawing; +using System.Windows.Forms; + +namespace BizHawk.Client.EmuHawk +{ + /// + /// Shows current RetroAchievements game info + /// + public partial class RCheevosGameInfoForm : Form + { + public bool IsShown { get; private set; } + + private bool _iconLoaded; + + public RCheevosGameInfoForm() + { + InitializeComponent(); + FormClosing += RCheevosGameInfoForm_FormClosing; + Shown += (_, _) => IsShown = true; + } + + public void Restart(string gameTitle, int totalPoints, string richPresence) + { + titleTextBox.Text = gameTitle; + totalPointsBox.Text = totalPoints.ToString(); + currentLboardBox.Text = "N/A"; + richPresenceBox.Text = richPresence; + _iconLoaded = false; + } + + public void OnFrameAdvance(Bitmap gameIcon, int totalPoints, string lboardStr, string richPresence) + { + // probably bad idea to set this every frame, so + if (!_iconLoaded && gameIcon is not null) + { + gameIconBox.Image = gameIcon; + _iconLoaded = true; + } + + totalPointsBox.Text = totalPoints.ToString(); + currentLboardBox.Text = lboardStr; + richPresenceBox.Text = richPresence; + } + + private void RCheevosGameInfoForm_FormClosing(object sender, FormClosingEventArgs e) + { + Hide(); + e.Cancel = true; + IsShown = false; + } + } +} + diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosGameInfoForm.resx b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosGameInfoForm.resx new file mode 100644 index 0000000000..29dcb1b3a3 --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosGameInfoForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosLeaderboardForm.Designer.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosLeaderboardForm.Designer.cs new file mode 100644 index 0000000000..fb32444192 --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosLeaderboardForm.Designer.cs @@ -0,0 +1,137 @@ +namespace BizHawk.Client.EmuHawk +{ + partial class RCheevosLeaderboardForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.titleLabel = new System.Windows.Forms.Label(); + this.descriptionLabel = new System.Windows.Forms.Label(); + this.titleBox = new System.Windows.Forms.TextBox(); + this.descriptionBox = new System.Windows.Forms.TextBox(); + this.scoreLabel = new System.Windows.Forms.Label(); + this.scoreBox = new System.Windows.Forms.TextBox(); + this.lowerIsBetterBox = new System.Windows.Forms.CheckBox(); + this.SuspendLayout(); + // + // titleLabel + // + this.titleLabel.AutoSize = true; + this.titleLabel.Location = new System.Drawing.Point(45, 14); + this.titleLabel.Name = "titleLabel"; + this.titleLabel.Size = new System.Drawing.Size(30, 13); + this.titleLabel.TabIndex = 1; + this.titleLabel.Text = "Title:"; + // + // descriptionLabel + // + this.descriptionLabel.AutoSize = true; + this.descriptionLabel.Location = new System.Drawing.Point(12, 40); + this.descriptionLabel.Name = "descriptionLabel"; + this.descriptionLabel.Size = new System.Drawing.Size(63, 13); + this.descriptionLabel.TabIndex = 2; + this.descriptionLabel.Text = "Description:"; + // + // titleBox + // + this.titleBox.Location = new System.Drawing.Point(81, 11); + this.titleBox.Name = "titleBox"; + this.titleBox.ReadOnly = true; + this.titleBox.Size = new System.Drawing.Size(411, 20); + this.titleBox.TabIndex = 3; + // + // descriptionBox + // + this.descriptionBox.Location = new System.Drawing.Point(81, 37); + this.descriptionBox.Name = "descriptionBox"; + this.descriptionBox.ReadOnly = true; + this.descriptionBox.Size = new System.Drawing.Size(411, 20); + this.descriptionBox.TabIndex = 4; + // + // scoreLabel + // + this.scoreLabel.AutoSize = true; + this.scoreLabel.Location = new System.Drawing.Point(37, 63); + this.scoreLabel.Name = "scoreLabel"; + this.scoreLabel.Size = new System.Drawing.Size(38, 13); + this.scoreLabel.TabIndex = 5; + this.scoreLabel.Text = "Score:"; + // + // scoreBox + // + this.scoreBox.Location = new System.Drawing.Point(81, 60); + this.scoreBox.Name = "scoreBox"; + this.scoreBox.ReadOnly = true; + this.scoreBox.Size = new System.Drawing.Size(411, 20); + this.scoreBox.TabIndex = 6; + // + // lowerIsBetterBox + // + this.lowerIsBetterBox.AutoCheck = false; + this.lowerIsBetterBox.AutoSize = true; + this.lowerIsBetterBox.CheckAlign = System.Drawing.ContentAlignment.MiddleRight; + this.lowerIsBetterBox.Location = new System.Drawing.Point(392, 86); + this.lowerIsBetterBox.Name = "lowerIsBetterBox"; + this.lowerIsBetterBox.RightToLeft = System.Windows.Forms.RightToLeft.No; + this.lowerIsBetterBox.Size = new System.Drawing.Size(100, 17); + this.lowerIsBetterBox.TabIndex = 9; + this.lowerIsBetterBox.Text = "Lower Is Better:"; + this.lowerIsBetterBox.UseVisualStyleBackColor = true; + // + // RCheevosLeaderboardForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(504, 94); + this.ControlBox = false; + this.Controls.Add(this.lowerIsBetterBox); + this.Controls.Add(this.scoreBox); + this.Controls.Add(this.scoreLabel); + this.Controls.Add(this.descriptionBox); + this.Controls.Add(this.titleBox); + this.Controls.Add(this.descriptionLabel); + this.Controls.Add(this.titleLabel); + this.MaximizeBox = false; + this.MinimizeBox = false; + this.MinimumSize = new System.Drawing.Size(300, 110); + this.Name = "RCheevosLeaderboardForm"; + this.ShowIcon = false; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + private System.Windows.Forms.Label titleLabel; + private System.Windows.Forms.Label descriptionLabel; + private System.Windows.Forms.TextBox titleBox; + private System.Windows.Forms.TextBox descriptionBox; + private System.Windows.Forms.Label scoreLabel; + private System.Windows.Forms.TextBox scoreBox; + private System.Windows.Forms.CheckBox lowerIsBetterBox; + } +} \ No newline at end of file diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosLeaderboardForm.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosLeaderboardForm.cs new file mode 100644 index 0000000000..79099e7fc2 --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosLeaderboardForm.cs @@ -0,0 +1,29 @@ +using System.Windows.Forms; + +namespace BizHawk.Client.EmuHawk +{ + /// + /// Shows information about a specific leaderboard + /// + public partial class RCheevosLeaderboardForm : Form + { + private readonly RCheevos.LBoard _lboard; + + public RCheevosLeaderboardForm(RCheevos.LBoard lboard) + { + InitializeComponent(); + titleBox.Text = lboard.Title; + descriptionBox.Text = lboard.Description; + scoreBox.Text = lboard.Score; + _lboard = lboard; + TopLevel = false; + Show(); + } + + public void OnFrameAdvance() + { + scoreBox.Text = _lboard.Score; + } + } +} + diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosLeaderboardForm.resx b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosLeaderboardForm.resx new file mode 100644 index 0000000000..29dcb1b3a3 --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosLeaderboardForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosLeaderboardListForm.Designer.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosLeaderboardListForm.Designer.cs new file mode 100644 index 0000000000..77c887dff6 --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosLeaderboardListForm.Designer.cs @@ -0,0 +1,67 @@ +namespace BizHawk.Client.EmuHawk +{ + partial class RCheevosLeaderboardListForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.flowLayoutPanel1 = new System.Windows.Forms.FlowLayoutPanel(); + this.SuspendLayout(); + // + // flowLayoutPanel1 + // + this.flowLayoutPanel1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.flowLayoutPanel1.AutoScroll = true; + this.flowLayoutPanel1.Location = new System.Drawing.Point(12, 12); + this.flowLayoutPanel1.MinimumSize = new System.Drawing.Size(544, 0); + this.flowLayoutPanel1.Name = "flowLayoutPanel1"; + this.flowLayoutPanel1.Size = new System.Drawing.Size(544, 567); + this.flowLayoutPanel1.TabIndex = 0; + // + // RCheevosLeaderboardListForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(568, 591); + this.Controls.Add(this.flowLayoutPanel1); + this.MaximizeBox = false; + this.MinimizeBox = false; + this.MinimumSize = new System.Drawing.Size(574, 244); + this.Name = "RCheevosLeaderboardListForm"; + this.ShowIcon = false; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + this.Text = "Leaderboard List"; + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.FlowLayoutPanel flowLayoutPanel1; + } +} \ No newline at end of file diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosLeaderboardListForm.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosLeaderboardListForm.cs new file mode 100644 index 0000000000..7f52e74915 --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosLeaderboardListForm.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Windows.Forms; + +namespace BizHawk.Client.EmuHawk +{ + /// + /// Shows a list of a user's current leaderboards + /// + public partial class RCheevosLeaderboardListForm : Form + { + public bool IsShown { get; private set; } + + private RCheevosLeaderboardForm[] _lboardForms; + private int _updateCooldown; + + public RCheevosLeaderboardListForm() + { + InitializeComponent(); + FormClosing += RCheevosLeaderboardListForm_FormClosing; + Shown += (_, _) => IsShown = true; + _lboardForms = Array.Empty(); + _updateCooldown = 5; // only update every 5 frames / 12 fps (as this is rather expensive to update) + } + + private void DisposeLboardForms() + { + foreach (var lboardForm in _lboardForms) + { + lboardForm.Dispose(); + } + } + + public void Restart(IEnumerable lboards) + { + flowLayoutPanel1.Controls.Clear(); + DisposeLboardForms(); + var lboardForms = new List(); + foreach (var lboard in lboards) + { + lboardForms.Add(new(lboard)); + } + _lboardForms = lboardForms.ToArray(); + flowLayoutPanel1.Controls.AddRange(_lboardForms); + } + + public void OnFrameAdvance(bool forceUpdate = false) + { + _updateCooldown--; + if (_updateCooldown == 0 || forceUpdate) + { + _updateCooldown = 5; + + for (int i = 0; i < _lboardForms.Length; i++) + { + _lboardForms[i].OnFrameAdvance(); + } + } + } + + private void RCheevosLeaderboardListForm_FormClosing(object sender, FormClosingEventArgs e) + { + Hide(); + e.Cancel = true; + IsShown = false; + } + } +} + diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosLeaderboardListForm.resx b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosLeaderboardListForm.resx new file mode 100644 index 0000000000..29dcb1b3a3 --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosLeaderboardListForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosLoginForm.Designer.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosLoginForm.Designer.cs new file mode 100644 index 0000000000..c978b19d79 --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosLoginForm.Designer.cs @@ -0,0 +1,138 @@ +namespace BizHawk.Client.EmuHawk +{ + partial class RCheevosLoginForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.btnLogin = new System.Windows.Forms.Button(); + this.linkLabel1 = new System.Windows.Forms.LinkLabel(); + this.txtUsername = new System.Windows.Forms.TextBox(); + this.label1 = new System.Windows.Forms.Label(); + this.label2 = new System.Windows.Forms.Label(); + this.txtPassword = new System.Windows.Forms.TextBox(); + this.label3 = new System.Windows.Forms.Label(); + this.SuspendLayout(); + // + // btnLogin + // + this.btnLogin.Location = new System.Drawing.Point(270, 117); + this.btnLogin.Name = "btnLogin"; + this.btnLogin.Size = new System.Drawing.Size(83, 23); + this.btnLogin.TabIndex = 6; + this.btnLogin.Text = "Login"; + this.btnLogin.UseVisualStyleBackColor = true; + this.btnLogin.Click += new System.EventHandler(this.btnLogin_Click); + // + // linkLabel1 + // + this.linkLabel1.AutoSize = true; + this.linkLabel1.Location = new System.Drawing.Point(12, 127); + this.linkLabel1.Name = "linkLabel1"; + this.linkLabel1.Size = new System.Drawing.Size(136, 13); + this.linkLabel1.TabIndex = 12; + this.linkLabel1.TabStop = true; + this.linkLabel1.Text = "No Account? Register here"; + this.linkLabel1.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.linkLabel1_LinkClicked); + // + // txtUsername + // + this.txtUsername.Location = new System.Drawing.Point(73, 64); + this.txtUsername.Name = "txtUsername"; + this.txtUsername.Size = new System.Drawing.Size(280, 20); + this.txtUsername.TabIndex = 15; + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(12, 67); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(58, 13); + this.label1.TabIndex = 16; + this.label1.Text = "Username:"; + // + // label2 + // + this.label2.AutoSize = true; + this.label2.Location = new System.Drawing.Point(14, 94); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(56, 13); + this.label2.TabIndex = 17; + this.label2.Text = "Password:"; + // + // txtPassword + // + this.txtPassword.Location = new System.Drawing.Point(73, 91); + this.txtPassword.Name = "txtPassword"; + this.txtPassword.Size = new System.Drawing.Size(280, 20); + this.txtPassword.TabIndex = 18; + this.txtPassword.UseSystemPasswordChar = true; + // + // label3 + // + this.label3.AutoSize = true; + this.label3.Location = new System.Drawing.Point(19, 9); + this.label3.Name = "label3"; + this.label3.Size = new System.Drawing.Size(334, 39); + this.label3.TabIndex = 19; + this.label3.Text = "Please enter your RetroAchievements username and password.\r\nNote, your password w" + + "ill not be saved, instead a temporary API token\r\nwill be saved and used.\r\n"; + // + // RCheevosLoginForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(367, 150); + this.Controls.Add(this.label3); + this.Controls.Add(this.txtPassword); + this.Controls.Add(this.btnLogin); + this.Controls.Add(this.label2); + this.Controls.Add(this.linkLabel1); + this.Controls.Add(this.label1); + this.Controls.Add(this.txtUsername); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.MinimumSize = new System.Drawing.Size(300, 160); + this.Name = "RCheevosLoginForm"; + this.ShowIcon = false; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + this.Text = "Login to RetroAchievements"; + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + private System.Windows.Forms.Button btnLogin; + private System.Windows.Forms.LinkLabel linkLabel1; + private System.Windows.Forms.TextBox txtUsername; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.Label label2; + private System.Windows.Forms.TextBox txtPassword; + private System.Windows.Forms.Label label3; + } +} \ No newline at end of file diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosLoginForm.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosLoginForm.cs new file mode 100644 index 0000000000..9f5719f672 --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosLoginForm.cs @@ -0,0 +1,41 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace BizHawk.Client.EmuHawk +{ + /// + /// Logs into retroachievements.org for RetroAchievements + /// + public partial class RCheevosLoginForm : Form + { + public RCheevosLoginForm(Func> loginCallback) + { + InitializeComponent(); + _loginCallback = loginCallback; + } + + private readonly Func> _loginCallback; + + private async void btnLogin_Click(object sender, EventArgs e) + { + var res = await _loginCallback(txtUsername.Text, txtPassword.Text); + if (res) + { + MessageBox.Show("Login successful"); + Close(); + } + else + { + MessageBox.Show("Login failed"); + } + } + + private void linkLabel1_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) + { + Process.Start("https://retroachievements.org/createaccount.php"); + } + } +} + diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosLoginForm.resx b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosLoginForm.resx new file mode 100644 index 0000000000..29dcb1b3a3 --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosLoginForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RetroAchievements.ConsoleID.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RetroAchievements.ConsoleID.cs new file mode 100644 index 0000000000..fb86947f80 --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RetroAchievements.ConsoleID.cs @@ -0,0 +1,180 @@ +using BizHawk.Emulation.Common; +using BizHawk.Emulation.Cores.Atari.Jaguar; +using BizHawk.Emulation.Cores.Consoles.Nintendo.Gameboy; +using BizHawk.Emulation.Cores.Consoles.Sega.gpgx; +using BizHawk.Emulation.Cores.Consoles.Sega.PicoDrive; +using BizHawk.Emulation.Cores.Nintendo.BSNES; +using BizHawk.Emulation.Cores.Nintendo.Gameboy; +using BizHawk.Emulation.Cores.Nintendo.GBHawkLink; +using BizHawk.Emulation.Cores.Nintendo.GBHawkLink3x; +using BizHawk.Emulation.Cores.Nintendo.GBHawkLink4x; +using BizHawk.Emulation.Cores.Nintendo.SNES; + +namespace BizHawk.Client.EmuHawk +{ + public abstract partial class RetroAchievements + { + public enum ConsoleID : int + { + UnknownConsoleID = 0, + MegaDrive = 1, + N64 = 2, + SNES = 3, + GB = 4, + GBA = 5, + GBC = 6, + NES = 7, + PCEngine = 8, + SegaCD = 9, + Sega32X = 10, + MasterSystem = 11, + PlayStation = 12, + Lynx = 13, + NeoGeoPocket = 14, + GameGear = 15, + GameCube = 16, + Jaguar = 17, + DS = 18, + WII = 19, + WIIU = 20, + PlayStation2 = 21, + Xbox = 22, + MagnavoxOdyssey = 23, + PokemonMini = 24, + Atari2600 = 25, + MSDOS = 26, + Arcade = 27, + VirtualBoy = 28, + MSX = 29, + C64 = 30, + ZX81 = 31, + Oric = 32, + SG1000 = 33, + VIC20 = 34, + Amiga = 35, + AtariST = 36, + AmstradCPC = 37, + AppleII = 38, + Saturn = 39, + Dreamcast = 40, + PSP = 41, + CDi = 42, + ThreeDO = 43, + Colecovision = 44, + Intellivision = 45, + Vectrex = 46, + PC8800 = 47, + PC9800 = 48, + PCFX = 49, + Atari5200 = 50, + Atari7800 = 51, + X68K = 52, + WonderSwan = 53, + CassetteVision = 54, + SuperCassetteVision = 55, + NeoGeoCD = 56, + FairchildChannelF = 57, + FMTowns = 58, + ZXSpectrum = 59, + GameAndWatch = 60, + NokiaNGage = 61, + Nintendo3DS = 62, + Supervision = 63, + SharpX1 = 64, + Tic80 = 65, + ThomsonTO8 = 66, + PC6000 = 67, + Pico = 68, + MegaDuck = 69, + Zeebo = 70, + Arduboy = 71, + WASM4 = 72, + Arcadia2001 = 73, + IntertonVC4000 = 74, + ElektorTVGamesComputer = 75, + PCEngineCD = 76, + JaguarCD = 77, + + NumConsoleIDs + } + + protected ConsoleID SystemIdToConsoleId() + { + return Emu.SystemId switch + { + VSystemID.Raw.A26 => ConsoleID.Atari2600, + VSystemID.Raw.A78 => ConsoleID.Atari7800, + VSystemID.Raw.Amiga => ConsoleID.Amiga, + VSystemID.Raw.AmstradCPC => ConsoleID.AmstradCPC, + VSystemID.Raw.AppleII => ConsoleID.AppleII, + VSystemID.Raw.Arcade => ConsoleID.Arcade, + VSystemID.Raw.C64 => ConsoleID.C64, + VSystemID.Raw.ChannelF => ConsoleID.FairchildChannelF, + VSystemID.Raw.Coleco => ConsoleID.Colecovision, + VSystemID.Raw.DEBUG => ConsoleID.UnknownConsoleID, + VSystemID.Raw.Dreamcast => ConsoleID.Dreamcast, + VSystemID.Raw.GameCube => ConsoleID.GameCube, + VSystemID.Raw.GB when Emu is IGameboyCommon gb => gb.IsCGBMode() ? ConsoleID.GBC : ConsoleID.GB, + VSystemID.Raw.GBA => ConsoleID.GBA, + VSystemID.Raw.GBC => ConsoleID.GBC, // Not actually used + VSystemID.Raw.GBL => Emu switch // actually can be a mix of GB and GBC + { + // there's probably a better way for all this + GambatteLink gb => gb.IsCGBMode(0) ? ConsoleID.GBC : ConsoleID.GB, + // WHY ARE THESE PUBLIC??? + GBHawkLink gb => gb.L.IsCGBMode() ? ConsoleID.GBC : ConsoleID.GB, + GBHawkLink3x gb => gb.L.IsCGBMode() ? ConsoleID.GBC : ConsoleID.GB, + GBHawkLink4x gb => gb.A.IsCGBMode() ? ConsoleID.GBC : ConsoleID.GB, + _ => ConsoleID.UnknownConsoleID, + }, + VSystemID.Raw.GEN when Emu is GPGX gpgx => gpgx.IsMegaCD ? ConsoleID.SegaCD : ConsoleID.MegaDrive, + VSystemID.Raw.GEN when Emu is PicoDrive pico => pico.Is32XActive ? ConsoleID.Sega32X : ConsoleID.MegaDrive, + VSystemID.Raw.GG => ConsoleID.GameGear, + VSystemID.Raw.GGL => ConsoleID.GameGear, // ??? + VSystemID.Raw.INTV => ConsoleID.Intellivision, + VSystemID.Raw.Jaguar when Emu is VirtualJaguar jaguar => jaguar.IsJaguarCD ? ConsoleID.JaguarCD : ConsoleID.Jaguar, + VSystemID.Raw.Libretro => ConsoleID.UnknownConsoleID, + VSystemID.Raw.Lynx => ConsoleID.Lynx, + VSystemID.Raw.MSX => ConsoleID.MSX, + VSystemID.Raw.N64 => ConsoleID.N64, + VSystemID.Raw.NDS => ConsoleID.DS, + VSystemID.Raw.NeoGeoCD => ConsoleID.NeoGeoCD, + VSystemID.Raw.NES => ConsoleID.NES, + VSystemID.Raw.NGP => ConsoleID.NeoGeoPocket, + VSystemID.Raw.NULL => ConsoleID.UnknownConsoleID, + VSystemID.Raw.O2 => ConsoleID.MagnavoxOdyssey, + VSystemID.Raw.Panasonic3DO => ConsoleID.ThreeDO, + VSystemID.Raw.PCE => ConsoleID.PCEngine, + VSystemID.Raw.PCECD => ConsoleID.PCEngineCD, + VSystemID.Raw.PCFX => ConsoleID.PCFX, + VSystemID.Raw.PhillipsCDi => ConsoleID.CDi, + VSystemID.Raw.Playdia => ConsoleID.UnknownConsoleID, + VSystemID.Raw.PS2 => ConsoleID.PlayStation2, + VSystemID.Raw.PSP => ConsoleID.PSP, + VSystemID.Raw.PSX => ConsoleID.PlayStation, + VSystemID.Raw.SAT => ConsoleID.Saturn, + VSystemID.Raw.Sega32X => ConsoleID.Sega32X, // not actually used + VSystemID.Raw.SG => ConsoleID.SG1000, + VSystemID.Raw.SGB => ConsoleID.GB, + VSystemID.Raw.SGX => ConsoleID.PCEngine, // ??? + VSystemID.Raw.SGXCD => ConsoleID.PCEngineCD, // ??? + VSystemID.Raw.SMS => ConsoleID.MasterSystem, + VSystemID.Raw.SNES => Emu switch + { + LibsnesCore libsnes => libsnes.IsSGB ? ConsoleID.GB : ConsoleID.SNES, + BsnesCore bsnes => bsnes.IsSGB ? ConsoleID.GB : ConsoleID.SNES, + _ => ConsoleID.SNES, + }, + VSystemID.Raw.TI83 => ConsoleID.UnknownConsoleID, + VSystemID.Raw.TIC80 => ConsoleID.Tic80, + VSystemID.Raw.UZE => ConsoleID.UnknownConsoleID, + VSystemID.Raw.VB => ConsoleID.VirtualBoy, + VSystemID.Raw.VEC => ConsoleID.Vectrex, + VSystemID.Raw.Wii => ConsoleID.WII, + VSystemID.Raw.WSWAN => ConsoleID.WonderSwan, + VSystemID.Raw.ZXSpectrum => ConsoleID.ZXSpectrum, + _ => ConsoleID.UnknownConsoleID, + }; + } + } +} diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RetroAchievements.GameVerification.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RetroAchievements.GameVerification.cs new file mode 100644 index 0000000000..ae6098a4ed --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RetroAchievements.GameVerification.cs @@ -0,0 +1,339 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +using BizHawk.Common; +using BizHawk.Client.Common; +using BizHawk.Emulation.DiscSystem; + +namespace BizHawk.Client.EmuHawk +{ + public abstract partial class RetroAchievements + { + protected bool AllGamesVerified { get; set; } + protected abstract int IdentifyHash(string hash); + protected abstract int IdentifyRom(byte[] rom); + + private int? HashDisc(string path, ConsoleID consoleID, int discCount) + { + // this shouldn't throw in practice, this is only called when loading was successful! + using var disc = DiscExtensions.CreateAnyType(path, e => throw new Exception(e)); + var dsr = new DiscSectorReader(disc) + { + Policy = { DeterministicClearBuffer = false } // let's make this a little faster + }; + + var buf2048 = new byte[2048]; + var buffer = new List(); + + switch (consoleID) + { + case ConsoleID.PCEngineCD: + { + dsr.ReadLBA_2048(1, buf2048, 0); + buffer.AddRange(new ArraySegment(buf2048, 128 - 22, 22)); + var bootSector = (buf2048[2] << 16) | (buf2048[1] << 8) | buf2048[0]; + var numSectors = buf2048[3]; + for (int i = 0; i < numSectors; i++) + { + dsr.ReadLBA_2048(bootSector + i, buf2048, 0); + buffer.AddRange(buf2048); + } + break; + } + case ConsoleID.PCFX: + { + dsr.ReadLBA_2048(1, buf2048, 0); + buffer.AddRange(new ArraySegment(buf2048, 0, 128)); + var bootSector = (buf2048[35] << 24) | (buf2048[34] << 16) | (buf2048[33] << 8) | buf2048[32]; + var numSectors = (buf2048[39] << 24) | (buf2048[38] << 16) | (buf2048[37] << 8) | buf2048[36]; + for (int i = 0; i < numSectors; i++) + { + dsr.ReadLBA_2048(bootSector + i, buf2048, 0); + buffer.AddRange(buf2048); + } + break; + } + case ConsoleID.PlayStation: + { + int GetFileSector(string filename, out int filesize) + { + dsr.ReadLBA_2048(16, buf2048, 0); + var sector = (buf2048[160] << 16) | (buf2048[159] << 8) | buf2048[158]; + dsr.ReadLBA_2048(sector, buf2048, 0); + var index = 0; + while ((index + 33 + filename.Length) < 2048) + { + var term = buf2048[index + 33 + filename.Length]; + if (term == ';' || term == '\0') + { + var fn = Encoding.ASCII.GetString(buf2048, index + 33, filename.Length); + if (filename == fn) + { + filesize = (buf2048[index + 13] << 24) | (buf2048[index + 12] << 16) | (buf2048[index + 11] << 8) | buf2048[index + 10]; + return (buf2048[index + 4] << 16) | (buf2048[index + 3] << 8) | buf2048[index + 2]; + } + } + index += buf2048[index]; + } + + filesize = 0; + return -1; + } + + string exePath = "PSX.EXE"; + + // find SYSTEM.CNF sector + var sector = GetFileSector("SYSTEM.CNF", out _); + if (sector > 0) + { + // read SYSTEM.CNF sector + dsr.ReadLBA_2048(sector, buf2048, 0); + exePath = Encoding.ASCII.GetString(buf2048); + + // "BOOT = cdrom:\" precedes the path + var index = exePath.IndexOf("BOOT = cdrom:\\"); + if (index < 0) break; + exePath = exePath.Remove(0, index + 14); + + // end of the path has ; + var end = exePath.IndexOf(';'); + if (end < 0) break; + exePath = exePath.Substring(0, end); + } + + buffer.AddRange(Encoding.ASCII.GetBytes(exePath)); + + // get the filename + // valid too if -1, as that means we already have the filename + var start = exePath.LastIndexOf('\\'); + if (start > 0) + { + exePath = exePath.Remove(0, start + 1); + } + + // get sector for exe + sector = GetFileSector(exePath, out var exeSize); + if (sector < 0) break; + + dsr.ReadLBA_2048(sector++, buf2048, 0); + + if ("PS-X EXE" == Encoding.ASCII.GetString(buf2048, 0, 8)) + { + exeSize = ((buf2048[31] << 24) | (buf2048[30] << 16) | (buf2048[29] << 8) | buf2048[28]) + 2048; + } + + buffer.AddRange(new ArraySegment(buf2048, 0, Math.Min(2048, exeSize))); + exeSize -= 2048; + + while (exeSize > 0) + { + dsr.ReadLBA_2048(sector++, buf2048, 0); + buffer.AddRange(new ArraySegment(buf2048, 0, Math.Min(2048, exeSize))); + exeSize -= 2048; + } + + break; + } + case ConsoleID.SegaCD: + case ConsoleID.Saturn: + dsr.ReadLBA_2048(0, buf2048, 0); + buffer.AddRange(new ArraySegment(buf2048, 0, 512)); + break; + case ConsoleID.JaguarCD: + if (discCount == 2) // we want to hash the second session of the disc (which is hacked to be disc 2) + { + const string _jaguarHeader = "ATARI APPROVED DATA HEADER ATRI "; + const string _jaguarBSHeader = "TARA IPARPVODED TA AEHDAREA RT I"; + var buf2352 = new byte[2352]; + + // find the boot track header + // see https://github.com/TASEmulators/BizHawk/blob/f29113287e88c6a644dbff30f92a9833307aad20/waterbox/virtualjaguar/src/cdhle.cpp#L109-L145 + var startLba = disc.Session1.FirstInformationTrack.LBA; + var numLbas = disc.Session1.FirstInformationTrack.NextTrack.LBA - disc.Session1.FirstInformationTrack.LBA; + int bootLen = 0, bootLba = 0, bootOff = 0; + bool byteswapped = false, foundHeader = false; + for (int i = 0; i < numLbas; i++) + { + dsr.ReadLBA_2352(startLba + i, buf2352, 0); + + for (int j = 0; j < (2352 - 32 - 4 - 4); j++) + { + if (buf2352[j] == _jaguarHeader[0]) + { + if (_jaguarHeader == Encoding.ASCII.GetString(buf2352, j, 32)) + { + bootLen = (buf2352[j + 36] << 24) | (buf2352[j + 37] << 16) | (buf2352[j + 38] << 8) | buf2352[j + 39]; + bootLba = startLba + i; + bootOff = j + 32 + 4 + 4; + byteswapped = false; + foundHeader = true; + break; + } + } + else if (buf2352[j] == _jaguarBSHeader[0]) + { + if (_jaguarBSHeader == Encoding.ASCII.GetString(buf2352, j, 32)) + { + bootLen = (buf2352[j + 37] << 24) | (buf2352[j + 36] << 16) | (buf2352[j + 39] << 8) | buf2352[j + 38]; + bootLba = startLba + i; + bootOff = j + 32 + 4 + 4; + byteswapped = true; + foundHeader = true; + break; + } + } + } + + if (foundHeader) + { + break; + } + } + + if (!foundHeader) + { + return 0; + } + + dsr.ReadLBA_2352(bootLba++, buf2352, 0); + + if (byteswapped) + { + EndiannessUtils.MutatingByteSwap16(buf2352.AsSpan()); + } + + buffer.AddRange(new ArraySegment(buf2352, bootOff, Math.Min(2352 - bootOff, bootLen))); + bootLen -= 2352 - bootOff; + + while (bootLen > 0) + { + dsr.ReadLBA_2352(bootLba++, buf2352, 0); + + if (byteswapped) + { + EndiannessUtils.MutatingByteSwap16(buf2352.AsSpan()); + } + + buffer.AddRange(new ArraySegment(buf2352, 0, Math.Min(2352, bootLen))); + bootLen -= 2352; + } + + break; + } + else + { + return null; // other sessions aren't hashed, ignore them + } + } + + var hash = MD5Checksum.ComputeDigestHex(buffer.ToArray()); + return IdentifyHash(hash); + } + + private int HashArcade(string path) + { + // Arcade wants to just hash the filename (with no extension) + var name = Encoding.UTF8.GetBytes(Path.GetFileNameWithoutExtension(path)); + var hash = MD5Checksum.ComputeDigestHex(name); + return IdentifyHash(hash); + } + + protected IReadOnlyList GetRAGameIds(IOpenAdvanced ioa, ConsoleID consoleID) + { + var ret = new List(); + switch (ioa.TypeName) + { + case OpenAdvancedTypes.OpenRom: + { + var ext = Path.GetExtension(Path.GetExtension(ioa.SimplePath.Replace("|", "")).ToLowerInvariant()); + var discCount = 0; + + if (ext == ".m3u") + { + using var file = new HawkFile(ioa.SimplePath); + using var sr = new StreamReader(file.GetStream()); + var m3u = M3U_File.Read(sr); + m3u.Rebase(Path.GetDirectoryName(ioa.SimplePath)); + foreach (var entry in m3u.Entries) + { + var id = HashDisc(entry.Path, consoleID, ++discCount); + if (id.HasValue) + { + ret.Add(id.Value); + } + } + } + else if (ext == ".xml") + { + var xml = XmlGame.Create(new HawkFile(ioa.SimplePath)); + foreach (var kvp in xml.Assets) + { + if (consoleID is ConsoleID.Arcade) + { + ret.Add(HashArcade(kvp.Key)); + break; + } + + if (Disc.IsValidExtension(Path.GetExtension(kvp.Key))) + { + var id = HashDisc(kvp.Key, consoleID, ++discCount); + if (id.HasValue) + { + ret.Add(id.Value); + } + } + else + { + ret.Add(IdentifyRom(kvp.Value)); + } + } + } + else + { + if (consoleID is ConsoleID.Arcade) + { + ret.Add(HashArcade(ioa.SimplePath)); + break; + } + + if (Disc.IsValidExtension(Path.GetExtension(ext))) + { + var id = HashDisc(ioa.SimplePath, consoleID, ++discCount); + if (id.HasValue) + { + ret.Add(id.Value); + } + } + else + { + using var file = new HawkFile(ioa.SimplePath); + var rom = file.ReadAllBytes(); + ret.Add(IdentifyRom(rom)); + } + } + break; + } + case OpenAdvancedTypes.MAME: + { + ret.Add(HashArcade(ioa.SimplePath)); + break; + } + case OpenAdvancedTypes.LibretroNoGame: + // nothing to hash here + break; + case OpenAdvancedTypes.Libretro: + { + // can't know what's here exactly, so we'll just hash the entire thing + using var file = new HawkFile(ioa.SimplePath); + var rom = file.ReadAllBytes(); + ret.Add(IdentifyRom(rom)); + break; + } + } + + return ret.AsReadOnly(); + } + } +} diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RetroAchievements.Hardcore.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RetroAchievements.Hardcore.cs new file mode 100644 index 0000000000..71ed7d79f5 --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RetroAchievements.Hardcore.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using BizHawk.Client.Common; +using BizHawk.Emulation.Cores.Atari.Atari2600; +using BizHawk.Emulation.Cores.Computers.MSX; +using BizHawk.Emulation.Cores.Consoles.O2Hawk; +using BizHawk.Emulation.Cores.Consoles.Sega.gpgx; +using BizHawk.Emulation.Cores.Nintendo.BSNES; +using BizHawk.Emulation.Cores.Nintendo.Gameboy; +using BizHawk.Emulation.Cores.Nintendo.GBA; +using BizHawk.Emulation.Cores.Nintendo.NES; +using BizHawk.Emulation.Cores.Nintendo.Sameboy; +using BizHawk.Emulation.Cores.Nintendo.SNES; +using BizHawk.Emulation.Cores.Nintendo.SNES9X; +using BizHawk.Emulation.Cores.Nintendo.SubGBHawk; +using BizHawk.Emulation.Cores.Nintendo.SubNESHawk; +using BizHawk.Emulation.Cores.PCEngine; +using BizHawk.Emulation.Cores.Sega.MasterSystem; +using BizHawk.Emulation.Cores.Waterbox; +using BizHawk.Emulation.Cores.WonderSwan; + +namespace BizHawk.Client.EmuHawk +{ + public abstract partial class RetroAchievements + { + // "Hardcore Mode" is a mode intended for RA's leaderboard, and places various restrictions on the emulator + // To keep changes outside this file minimal, we'll simply check if any problematic condition arises and disable hardcore mode + // (with the exception of frame advance and rewind, which we can just suppress) + + private static readonly Type[] HardcoreProhibitedTools = new[] + { + typeof(LuaConsole), typeof(RamWatch), typeof(RamSearch), + typeof(GameShark), typeof(SNESGraphicsDebugger), typeof(PceBgViewer), + typeof(PceTileViewer), typeof(GenVdpViewer), typeof(SmsVdpViewer), + typeof(PCESoundDebugger), typeof(MacroInputTool), typeof(GenericDebugger), + typeof(NESNameTableViewer), typeof(TraceLogger), typeof(CDL), + typeof(Cheats), typeof(NesPPU), typeof(GbaGpuView), + typeof(GbGpuView), typeof(BasicBot), typeof(HexEditor), + typeof(TAStudio), + }; + + private static readonly Dictionary CoreGraphicsLayers = new() + { + [typeof(MSX)] = new[] { "DispBG", "DispOBJ" }, + [typeof(Atari2600)] = new[] { "ShowBG", "ShowPlayer1", "ShowPlayer2", "ShowMissle1", "ShowMissle2", "ShowBall", "ShowPlayfield" }, + [typeof(O2Hawk)] = new[] { "Show_Chars", "Show_Quads", "Show_Sprites", "Show_G7400_Sprites", "Show_G7400_BG" }, + [typeof(BsnesCore)] = new[] { "ShowBG1_0", "ShowBG2_0", "ShowBG3_0", "ShowBG4_0", "ShowBG1_1", "ShowBG2_1", "ShowBG3_1", "ShowBG4_1", "ShowOBJ_0", "ShowOBJ_1", "ShowOBJ_2", "ShowOBJ_3" }, + [typeof(MGBAHawk)] = new[] { "DisplayBG0", "DisplayBG1", "DisplayBG2", "DisplayBG3", "DisplayOBJ" }, + [typeof(NES)] = new[] { "DispBackground", "DispSprites" }, + [typeof(Sameboy)] = new[] { "EnableBGWIN", "EnableOBJ" }, + [typeof(LibsnesCore)] = new[] { "ShowBG1_0", "ShowBG2_0", "ShowBG3_0", "ShowBG4_0", "ShowBG1_1", "ShowBG2_1", "ShowBG3_1", "ShowBG4_1", "ShowOBJ_0", "ShowOBJ_1", "ShowOBJ_2", "ShowOBJ_3" }, + [typeof(Snes9x)] = new[] { "ShowBg0", "ShowBg1", "ShowBg2", "ShowBg3", "ShowSprites0", "ShowSprites1", "ShowSprites2", "ShowSprites3", "ShowWindow", "ShowTransparency" }, + [typeof(PCEngine)] = new[] { "ShowBG1", "ShowOBJ1", "ShowBG2", "ShowOBJ2", }, + [typeof(GPGX)] = new[] { "DrawBGA", "DrawBGB", "DrawBGW", "DrawObj", }, + [typeof(SMS)] = new[] { "DispBG", "DispOBJ," }, + [typeof(WonderSwan)] = new[] { "EnableBG", "EnableFG", "EnableSprites", }, + }; + + private readonly OverrideAdapter _hardcoreHotkeyOverrides = new(); + + protected abstract void HandleHardcoreModeDisable(string reason); + + protected void CheckHardcoreModeConditions() + { + if (!AllGamesVerified) + { + HandleHardcoreModeDisable("All loaded games were not verified."); + return; + } + + if (MovieSession.Movie.IsPlaying()) + { + HandleHardcoreModeDisable("Playing a movie while in hardcore mode is not allowed."); + return; + } + + // suppress rewind and frame advance hotkeys + _hardcoreHotkeyOverrides.FrameTick(); + _hardcoreHotkeyOverrides.SetButton("Frame Advance", false); + _hardcoreHotkeyOverrides.SetButton("Rewind", false); + _inputManager.ClientControls.Overrides(_hardcoreHotkeyOverrides); + _mainForm.FrameInch = false; + + var fastForward = _inputManager.ClientControls["Fast Forward"] || _mainForm.FastForward; + var speedPercent = fastForward ? _getConfig().SpeedPercentAlternate : _getConfig().SpeedPercent; + if (speedPercent < 100) + { + HandleHardcoreModeDisable("Slow motion in hardcore mode is not allowed."); + return; + } + + foreach (var t in HardcoreProhibitedTools) + { + if (_tools.IsLoaded(t)) + { + HandleHardcoreModeDisable($"Using {t.Name} in hardcore mode is not allowed."); + return; + } + } + + // can't know what external tools are doing, so just don't allow them here + if (_tools.IsLoaded()) + { + HandleHardcoreModeDisable($"Using external tools in hardcore mode is not allowed."); + return; + } + + if (Emu is SubNESHawk or SubBsnesCore or SubGBHawk) + { + // this is mostly due to wonkiness with subframes which can be used as pseudo slowdown + HandleHardcoreModeDisable($"Using subframes in hardcore mode is not allowed."); + return; + } + else if (Emu is NymaCore nyma) + { + if (nyma.GetSettings().DisabledLayers.Any()) + { + HandleHardcoreModeDisable($"Disabling {Emu.GetType().Name}'s graphics layers in hardcore mode is not allowed."); + return; + } + } + else if (Emu is GambatteLink gl) + { + foreach (var ss in gl.GetSyncSettings()._linkedSyncSettings) + { + if (!ss.DisplayBG || !ss.DisplayOBJ || !ss.DisplayWindow) + { + HandleHardcoreModeDisable($"Disabling GambatteLink's graphics layers in hardcore mode is not allowed."); + return; + } + } + } + else if (Emu is Gameboy gb) + { + var ss = gb.GetSyncSettings(); + if (!ss.DisplayBG || !ss.DisplayOBJ || !ss.DisplayWindow) + { + HandleHardcoreModeDisable($"Disabling Gambatte's graphics layers in hardcore mode is not allowed."); + return; + } + if (ss.FrameLength is Gameboy.GambatteSyncSettings.FrameLengthType.UserDefinedFrames) + { + HandleHardcoreModeDisable($"Using subframes in hardcore mode is not allowed."); + return; + } + } + else if (CoreGraphicsLayers.TryGetValue(Emu.GetType(), out var layers)) + { + var s = _mainForm.GetSettingsAdapterForLoadedCoreUntyped().GetSettings(); + var t = s.GetType(); + foreach (var layer in layers) + { + // annoyingly NES has fields instead of properties for layers + if (!(bool)(t.GetProperty(layer)?.GetValue(s) ?? t.GetField(layer).GetValue(s))) + { + HandleHardcoreModeDisable($"Disabling {Emu.GetType().Name}'s {layer} in hardcore mode is not allowed."); + return; + } + } + } + } + } +} diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RetroAchievements.Memory.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RetroAchievements.Memory.cs new file mode 100644 index 0000000000..b57860c073 --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RetroAchievements.Memory.cs @@ -0,0 +1,524 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; + +using BizHawk.Common; +using BizHawk.Emulation.Common; + +namespace BizHawk.Client.EmuHawk +{ + public abstract partial class RetroAchievements + { + public struct RAMemGuard : IMonitor, IDisposable + { + private readonly AutoResetEvent _start, _go, _end; + private readonly ThreadLocal _isMainThread; + + private bool IsNotMainThread => !_isMainThread.Value; + + public RAMemGuard(AutoResetEvent start, AutoResetEvent go, AutoResetEvent end) + { + _start = start; + _go = go; + _end = end; + _isMainThread = new() { Value = true }; + } + + public void Dispose() + { + _start.Dispose(); + _go.Dispose(); + _end.Dispose(); + _isMainThread.Dispose(); + } + + public void Enter() + { + if (IsNotMainThread) + { + _start.Set(); + _go.WaitOne(); + } + } + + public void Exit() + { + if (IsNotMainThread) + { + _end.Set(); + } + } + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate byte ReadMemoryFunc(int address); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void WriteMemoryFunc(int address, byte value); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate int ReadMemoryBlockFunc(int address, IntPtr buffer, int bytes); + + public class MemFunctions + { + protected readonly MemoryDomain _domain; + private readonly int _domainAddrStart; // addr of _domain where bank begins + private readonly int _addressMangler; // of course, let's *not* correct internal core byteswapping! + + public ReadMemoryFunc ReadFunc { get; protected init; } + public WriteMemoryFunc WriteFunc { get; protected init; } + public ReadMemoryBlockFunc ReadBlockFunc { get; protected init; } + + public readonly int BankSize; + + public RAMemGuard? MemGuard { get; set; } + + protected virtual int FixAddr(int addr) + => _domainAddrStart + addr; + + protected virtual byte ReadMem(int addr) + { + using (MemGuard?.EnterExit()) + { + return _domain.PeekByte(FixAddr(addr) ^ _addressMangler); + } + } + + protected virtual void WriteMem(int addr, byte val) + { + using (MemGuard?.EnterExit()) + { + _domain.PokeByte(FixAddr(addr) ^ _addressMangler, val); + } + } + + protected virtual int ReadMemBlock(int addr, IntPtr buffer, int bytes) + { + addr = FixAddr(addr); + + if (addr >= (_domainAddrStart + BankSize)) + { + return 0; + } + + using (MemGuard?.EnterExit()) + { + var end = Math.Min(addr + bytes, _domainAddrStart + BankSize); + var length = end - addr; + + if (_addressMangler == 0) + { + var ret = new byte[length]; + _domain.BulkPeekByte(((long)addr).RangeToExclusive(end), ret); + Marshal.Copy(ret, 0, buffer, length); + } + else + { + unsafe + { + for (var i = addr; i < end; i++) + { + ((byte*)buffer)[i - addr] = _domain.PeekByte(i ^ _addressMangler); + } + } + } + + return length; + } + } + + public MemFunctions(MemoryDomain domain, int domainAddrStart, long bankSize, int addressMangler = 0) + { + _domain = domain; + _domainAddrStart = domainAddrStart; + _addressMangler = addressMangler; + + ReadFunc = ReadMem; + WriteFunc = WriteMem; + ReadBlockFunc = ReadMemBlock; + + if (bankSize > int.MaxValue) + { + throw new OverflowException("bankSize is too big!"); + } + + BankSize = (int)bankSize; + } + } + + private class NullMemFunctions : MemFunctions + { + public NullMemFunctions(long bankSize) + : base(null, 0, bankSize) + { + ReadFunc = null; + WriteFunc = null; + ReadBlockFunc = null; + } + } + + // this is a complete hack because the libretro Intelli core sucks and so achievements are made expecting this format + private class IntelliMemFunctions : MemFunctions + { + protected override int FixAddr(int addr) + => (addr >> 1) + (~addr & 1); + + protected override byte ReadMem(int addr) + { + if ((addr & 2) != 0) + { + return 0; + } + + return base.ReadMem(addr); + } + + protected override void WriteMem(int addr, byte val) + { + if ((addr & 2) != 0) + { + return; + } + + base.WriteMem(addr, val); + } + + protected override int ReadMemBlock(int addr, IntPtr buffer, int bytes) + { + if (addr >= BankSize) + { + return 0; + } + + using (MemGuard?.EnterExit()) + { + var end = Math.Min(addr + bytes, BankSize); + var length = end - addr; + + unsafe + { + for (var i = addr; i < end; i++) + { + if ((i & 2) != 0) + { + ((byte*)buffer)[i - addr] = 0; + } + else + { + ((byte*)buffer)[i - addr] = _domain.PeekByte(FixAddr(i)); + } + } + } + + return length; + } + } + + public IntelliMemFunctions(MemoryDomain domain) + : base(domain, 0, 0x40000) + { + } + } + + private class ChanFMemFunctions : MemFunctions + { + private readonly IDebuggable _debuggable; + private readonly MemoryDomain _vram; // our vram is unpacked, but RA expects it packed + + private byte ReadVRAMPacked(int addr) + { + return (byte)(((_vram.PeekByte(addr * 4 + 0) & 3) << 6) + | ((_vram.PeekByte(addr * 4 + 1) & 3) << 4) + | ((_vram.PeekByte(addr * 4 + 2) & 3) << 2) + | ((_vram.PeekByte(addr * 4 + 3) & 3) << 0)); + } + + protected override byte ReadMem(int addr) + { + using (MemGuard?.EnterExit()) + { + if (addr < 0x40) + { + return (byte)_debuggable.GetCpuFlagsAndRegisters()["SPR" + addr].Value; + } + else + { + return ReadVRAMPacked(addr - 0x40); + } + } + } + + protected override void WriteMem(int addr, byte val) + { + using (MemGuard?.EnterExit()) + { + if (addr < 0x40) + { + _debuggable.SetCpuRegister("SPR" + addr, val); + } + else + { + addr -= 0x40; + _vram.PokeByte(addr * 4 + 0, (byte)((val >> 6) & 3)); + _vram.PokeByte(addr * 4 + 1, (byte)((val >> 4) & 3)); + _vram.PokeByte(addr * 4 + 2, (byte)((val >> 2) & 3)); + _vram.PokeByte(addr * 4 + 3, (byte)((val >> 0) & 3)); + } + } + } + + protected override int ReadMemBlock(int addr, IntPtr buffer, int bytes) + { + if (addr >= BankSize) + { + return 0; + } + + using (MemGuard?.EnterExit()) + { + var regs = _debuggable.GetCpuFlagsAndRegisters(); + var end = Math.Min(addr + bytes, BankSize); + for (int i = addr; i < end; i++) + { + byte val; + if (i < 0x40) + { + val = (byte)regs["SPR" + i].Value; + } + else + { + val = ReadVRAMPacked(i - 0x40); + } + + unsafe + { + ((byte*)buffer)[i - addr] = val; + } + } + + return end - addr; + } + } + + public ChanFMemFunctions(IDebuggable debuggable, MemoryDomain vram) + : base(null, 0, 0x840) + { + _debuggable = debuggable; + _vram = vram; + } + } + + // these consoles will use the entire system bus + private static readonly ConsoleID[] UseFullSysBus = new[] + { + ConsoleID.NES, ConsoleID.C64, ConsoleID.AmstradCPC, ConsoleID.Atari7800, + }; + + // these consoles will use the entire main memory domain + private static readonly ConsoleID[] UseFullMainMem = new[] + { + ConsoleID.PlayStation, ConsoleID.Lynx, ConsoleID.Lynx, ConsoleID.NeoGeoPocket, + ConsoleID.Jaguar, ConsoleID.JaguarCD, ConsoleID.DS, ConsoleID.AppleII, + ConsoleID.Vectrex, ConsoleID.Tic80, ConsoleID.PCEngine, + }; + + // these consoles will use part of the system bus at an offset + private static readonly Dictionary UsePartialSysBus = new() + { + [ConsoleID.MasterSystem] = new[] { (0xC000, 0x2000) }, + [ConsoleID.GameGear] = new[] { (0xC000, 0x2000) }, + [ConsoleID.Atari2600] = new[] { (0, 0x80) }, + [ConsoleID.Colecovision] = new[] { (0x6000, 0x400) }, + [ConsoleID.GBA] = new[] { (0x3000000, 0x8000), (0x2000000, 0x40000) }, + [ConsoleID.SG1000] = new[] { (0xC000, 0x2000), (0x2000, 0x2000), (0x8000, 0x2000) }, + }; + + // anything more complicated will be handled accordingly + + protected static IReadOnlyList CreateMemoryBanks(ConsoleID consoleId, IMemoryDomains domains, IDebuggable debuggable) + { + var mfs = new List(); + + void TryAddDomain(string domain, int? size = null, int addressMangler = 0) + { + if (domains.Has(domain)) + { + if (size.HasValue && domains[domain].Size < size.Value) + { + mfs.Add(new(domains[domain], 0, domains[domain].Size, addressMangler)); + mfs.Add(new NullMemFunctions(size.Value - domains[domain].Size)); + } + else + { + mfs.Add(new(domains[domain], 0, size ?? domains[domain].Size, addressMangler)); + } + } + else if (size.HasValue) + { + mfs.Add(new NullMemFunctions(size.Value)); + } + } + + if (Array.Exists(UseFullSysBus, id => id == consoleId)) + { + mfs.Add(new(domains.SystemBus, 0, domains.SystemBus.Size)); + } + else if (Array.Exists(UseFullMainMem, id => id == consoleId)) + { + mfs.Add(new(domains.MainMemory, 0, domains.MainMemory.Size)); + } + else if (UsePartialSysBus.TryGetValue(consoleId, out var pairs)) + { + foreach (var pair in pairs) + { + mfs.Add(new(domains.SystemBus, pair.Start, pair.Size)); + } + } + else + { + switch (consoleId) + { + case ConsoleID.MegaDrive: + case ConsoleID.Sega32X: + mfs.Add(new(domains["68K RAM"], 0, domains["68K RAM"].Size, 1)); + TryAddDomain("32X RAM", addressMangler: 1); + // our picodrive doesn't byteswap its SRAM, so... + TryAddDomain("SRAM", addressMangler: domains["SRAM"] is MemoryDomainIntPtrSwap16Monitor ? 1 : 0); + break; + case ConsoleID.SNES: + mfs.Add(new(domains["WRAM"], 0, domains["WRAM"].Size)); + TryAddDomain("CARTRAM"); + // sufami B sram + // don't think this is actually hooked up at all anyways... + TryAddDomain("CARTRAM B"); // Snes9x + TryAddDomain("SUFAMI TURBO B RAM"); // new BSNES + break; + case ConsoleID.GB: + case ConsoleID.GBC: + if (domains.Has("SGB CARTROM")) + { + // old BSNES doesn't have as many domains + // but it should still suffice in practice + mfs.Add(new(domains["SGB CARTROM"], 0, 0x8000)); + TryAddDomain("SGB VRAM", 0x2000); + TryAddDomain("SGB CARTRAM", 0x2000); + mfs.Add(new(domains["SGB WRAM"], 0, 0x2000)); + mfs.Add(new(domains["SGB WRAM"], 0, 0x1E00)); + TryAddDomain("SGB OAM", 0xA0); + TryAddDomain("SGB System Bus", 0xE0); + TryAddDomain("SGB HRAM", 0x80); + TryAddDomain("SGB IE"); + } + else + { + string sysBus, cartRam, wram; + if (domains.Has("P1 System Bus")) // GambatteLink + { + sysBus = "P1 System Bus"; + cartRam = "P1 CartRAM"; + wram = "P1 WRAM"; + } + else if (domains.Has("System Bus L")) // GBHawkLink / GBHawkLink3x + { + sysBus = "System Bus L"; + cartRam = "Cart RAM L"; + wram = "Main RAM L"; + } + else if (domains.Has("System Bus A")) // GBHawkLink4x + { + sysBus = "System Bus A"; + cartRam = "Cart RAM A"; + wram = "Main RAM A"; + } + else // Gambatte / GBHawk + { + sysBus = "System Bus"; + cartRam = "CartRAM"; + wram = "WRAM"; + } + + mfs.Add(new(domains[sysBus], 0, 0xA000)); + TryAddDomain(cartRam, 0x2000); + mfs.Add(new(domains[wram], 0x0000, 0x2000)); + mfs.Add(new(domains[sysBus], 0xE000, 0x2000)); + if (domains[wram].Size == 0x8000) + { + mfs.Add(new(domains[wram], 0x2000, 0x6000)); + } + } + break; + case ConsoleID.SegaCD: + mfs.Add(new(domains["68K RAM"], 0, domains["68K RAM"].Size, 1)); + mfs.Add(new(domains["CD PRG RAM"], 0, domains["CD PRG RAM"].Size, 1)); + break; + case ConsoleID.MagnavoxOdyssey: + mfs.Add(new(domains["CPU RAM"], 0, domains["CPU RAM"].Size)); + mfs.Add(new(domains["Main RAM"], 0, domains["Main RAM"].Size)); + break; + case ConsoleID.VirtualBoy: + // todo: add System Bus so this isn't needed + mfs.Add(new(domains["WRAM"], 0, domains["WRAM"].Size)); + mfs.Add(new(domains["CARTRAM"], 0, domains["CARTRAM"].Size)); + break; + case ConsoleID.MSX: + // no, can't use MainMemory here, as System Bus is that due to init ordering + // todo: make this MainMemory + mfs.Add(new(domains["RAM"], 0, domains["RAM"].Size)); + break; + case ConsoleID.Saturn: + // todo: add System Bus so this isn't needed + mfs.Add(new(domains["Work Ram Low"], 0, domains["Work Ram Low"].Size)); + mfs.Add(new(domains["Work Ram High"], 0, domains["Work Ram High"].Size)); + break; + case ConsoleID.Intellivision: + // special case + mfs.Add(new NullMemFunctions(0x80)); + mfs.Add(new IntelliMemFunctions(domains.SystemBus)); + break; + case ConsoleID.PCFX: + // todo: add System Bus so this isn't needed + mfs.Add(new(domains["Main RAM"], 0, domains["Main RAM"].Size)); + mfs.Add(new(domains["Backup RAM"], 0, domains["Backup RAM"].Size)); + mfs.Add(new(domains["Extra Backup RAM"], 0, domains["Extra Backup RAM"].Size)); + break; + case ConsoleID.WonderSwan: + mfs.Add(new(domains["RAM"], 0, domains["RAM"].Size)); + TryAddDomain("SRAM"); + TryAddDomain("EEPROM"); + break; + case ConsoleID.FairchildChannelF: + // special case + mfs.Add(new ChanFMemFunctions(debuggable, domains["VRAM"])); + mfs.Add(new(domains.SystemBus, 0, domains.SystemBus.Size)); + break; + case ConsoleID.PCEngineCD: + mfs.Add(new(domains["System Bus (21 bit)"], 0x1F0000, 0x2000)); + mfs.Add(new(domains["System Bus (21 bit)"], 0x100000, 0x10000)); + mfs.Add(new(domains["System Bus (21 bit)"], 0xD0000, 0x30000)); + mfs.Add(new(domains["System Bus (21 bit)"], 0x1EE000, 0x800)); + break; + case ConsoleID.N64: + mfs.Add(new(domains.MainMemory, 0, domains.MainMemory.Size, 3)); + break; + case ConsoleID.Arcade: + foreach (var domain in domains) + { + if (domain.Name.Contains("ram")) + { + mfs.Add(new(domain, 0, domain.Size)); + } + } + break; + case ConsoleID.UnknownConsoleID: + case ConsoleID.ZXSpectrum: // this doesn't actually have anything standardized, so... + default: + mfs.Add(new(domains.MainMemory, 0, domains.MainMemory.Size)); + break; + } + } + + return mfs.AsReadOnly(); + } + } +} diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RetroAchievements.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RetroAchievements.cs new file mode 100644 index 0000000000..8286db7e30 --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RetroAchievements.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Windows.Forms; + +using BizHawk.Client.Common; +using BizHawk.Emulation.Common; + +namespace BizHawk.Client.EmuHawk +{ + public abstract partial class RetroAchievements : IRetroAchievements + { + protected readonly IMainFormForRetroAchievements _mainForm; + protected readonly InputManager _inputManager; + protected readonly ToolManager _tools; + protected readonly Func _getConfig; + protected readonly ToolStripItemCollection _raDropDownItems; + protected readonly Action _shutdownRACallback; + + protected IEmulator Emu => _mainForm.Emulator; + protected IMemoryDomains Domains => Emu.AsMemoryDomains(); + protected IGameInfo Game => _mainForm.Game; + protected IMovieSession MovieSession => _mainForm.MovieSession; + + protected IReadOnlyList _memFunctions; + + protected RetroAchievements(IMainFormForRetroAchievements mainForm, InputManager inputManager, ToolManager tools, + Func getConfig, ToolStripItemCollection raDropDownItems, Action shutdownRACallback) + { + _mainForm = mainForm; + _inputManager = inputManager; + _tools = tools; + _getConfig = getConfig; + _raDropDownItems = raDropDownItems; + _shutdownRACallback = shutdownRACallback; + } + + public static IRetroAchievements CreateImpl(IMainFormForRetroAchievements mainForm, InputManager inputManager, ToolManager tools, + Func getConfig, ToolStripItemCollection raDropDownItems, Action shutdownRACallback) + { + if (getConfig().SkipRATelemetryWarning || mainForm.ShowMessageBox2( + owner: null, + text: "In order to use RetroAchievements, some information needs to be sent to retroachievements.org:\n" + + "\n\u2022 Your RetroAchievements username and password (first login) or token (subsequent logins)." + + "\n\u2022 The hash of the game(s) you have loaded into BizHawk. (for game identification + achievement unlock + leaderboard submission)" + + "\n\u2022 The RetroAchievements game ID(s) of the game(s) you have loaded into BizHawk. (for game information + achievement definitions + leaderboard definitions + rich presence definitions + code notes + achievement badges + user unlocks + leaderboard submission + ticket submission)" + + "\n\u2022 Rich presence data (periodically sent, derived from emulated game memory)." + + "\n\u2022 Whether or not you are currently in \"Hardcore Mode\" (for achievement unlock)." + + "\n\u2022 Ticket submission type and message (when submitting tickets with RAIntegration)." + // todo: add this to our impl? doesn't seem to be supported in rcheevos... + "\n\nDo you agree to send this information to retroachievements.org?", + caption: "Notice", + icon: EMsgBoxIcon.Question, + useOKCancel: false)) + { + getConfig().SkipRATelemetryWarning = true; + + if (RAIntegration.IsAvailable && RAIntegration.CheckUpdateRA(mainForm)) + { + return new RAIntegration(mainForm, inputManager, tools, getConfig, raDropDownItems, shutdownRACallback); + } + else + { + return new RCheevos(mainForm, inputManager, tools, getConfig, raDropDownItems, shutdownRACallback); + } + } + + return null; + } + + public abstract void Update(); + + public abstract void OnFrameAdvance(); + + public abstract void Restart(); + + public abstract void Stop(); + + public abstract void OnSaveState(string path); + + public abstract void OnLoadState(string path); + + public abstract void Dispose(); + } +} diff --git a/src/BizHawk.Client.EmuHawk/config/HotkeyConfig.cs b/src/BizHawk.Client.EmuHawk/config/HotkeyConfig.cs index 18bf6294f3..4ac71f7dad 100644 --- a/src/BizHawk.Client.EmuHawk/config/HotkeyConfig.cs +++ b/src/BizHawk.Client.EmuHawk/config/HotkeyConfig.cs @@ -85,6 +85,11 @@ namespace BizHawk.Client.EmuHawk foreach (var tab in HotkeyInfo.Groupings) { + if (tab == "RAIntegration" && !RAIntegration.IsAvailable) + { + continue; // skip RA hotkeys if it can't be used + } + var tb = new TabPage { Name = tab, Text = tab }; var bindings = HotkeyInfo.AllHotkeys.Where(kvp => kvp.Value.TabGroup == tab) .OrderBy(static kvp => kvp.Value.Ordinal).ThenBy(static kvp => kvp.Value.DisplayName);