diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 0f93882..e6a8a0f 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -19,6 +19,7 @@ on: env: PROJECT_PATH: ./Source/HedgeModManager.UI/HedgeModManager.UI.csproj FLATPAK_ID: io.github.hedge_dev.hedgemodmanager + MACOS_BUILD_SCRIPT: ./macos/generate-bundle.bash GENERATOR_URL: https://raw.githubusercontent.com/flatpak/flatpak-builder-tools/refs/heads/master/dotnet/flatpak-dotnet-generator.py DOTNET_VERSION: 8 DOTNET_CLI_HOME: /tmp/.dotnet @@ -82,12 +83,19 @@ jobs: - name: Build Flatpak Bundle run: flatpak build-bundle repo ./flatpak/${{env.FLATPAK_ID}}.flatpak ${{env.FLATPAK_ID}} + + - name: osx-arm64 Build + run: dotnet publish -p:PublishProfile=osx-arm64 -c Release -p:AssemblyVersion=${{ github.event.inputs.version }} -p:FileVersion=${{ github.event.inputs.version }} -o ./output/macos/osx-arm64 ${{env.PROJECT_PATH}} -p:UseAppHost=true + + - name: Build osx-arm64 Bundle + run: /bin/bash ${{env.MACOS_BUILD_SCRIPT}} ${{ github.event.inputs.version }} - name: Prepare Release run: | mkdir -p ./release mv ./output/win-x64/HedgeModManager.UI.exe ./release/HedgeModManager.exe mv ./flatpak/${{env.FLATPAK_ID}}.flatpak ./release/${{env.FLATPAK_ID}}.flatpak + mv ./output/macos/HedgeModManager.app ./release/HedgeModManager.app - name: Create Release uses: softprops/action-gh-release@v2.2.1 diff --git a/.gitignore b/.gitignore index f740d3a..a221143 100644 --- a/.gitignore +++ b/.gitignore @@ -24,10 +24,14 @@ bld/ bin/ [Bb]in/ [Oo]bj/ +output/ # Visual Studio 2015 cache/options directory .vs/ +Source/.idea + + # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* @@ -215,3 +219,4 @@ _Pvt_Extensions/ ModelManifest.xml Ignored/ +.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index 1c06756..92db837 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ A mod manager for Hedgehog Engine games on PC. - [Installation](#installation) - [Windows](#windows) - [Linux/Steam Deck](#linuxsteam-deck) +- [Building](#building) - [Frequently Asked Questions](#frequently-asked-questions) - [Bug/Issue Reporting](#bugissue-reporting) - [Contribute](#contribute) @@ -62,6 +63,9 @@ Flatpak bundles are also available from the [releases page](https://github.com/h > > For Heroic, it must be a flatpak install within the home directory. +### Building +For building from source, please check the [building instructions](/docs/BUILDING.md) page. + ### Frequently Asked Questions - Where can I install mods? diff --git a/Source/HedgeModManager.Console/HedgeModManager.Console.csproj b/Source/HedgeModManager.Console/HedgeModManager.Console.csproj index 6acb2e7..040f275 100644 --- a/Source/HedgeModManager.Console/HedgeModManager.Console.csproj +++ b/Source/HedgeModManager.Console/HedgeModManager.Console.csproj @@ -12,4 +12,4 @@ - + \ No newline at end of file diff --git a/Source/HedgeModManager.UI/App.axaml b/Source/HedgeModManager.UI/App.axaml index 392e294..476c95f 100644 --- a/Source/HedgeModManager.UI/App.axaml +++ b/Source/HedgeModManager.UI/App.axaml @@ -4,7 +4,8 @@ xmlns:local="using:HedgeModManager.UI" xmlns:themes="using:HedgeModManager.UI.Themes" xmlns:materialIcons="using:Material.Icons.Avalonia" - RequestedThemeVariant="{x:Static themes:Themes.Darker}"> + RequestedThemeVariant="{x:Static themes:Themes.Darker}" + Name="Hedge Mod Manager"> diff --git a/Source/HedgeModManager.UI/HedgeModManager.UI.csproj b/Source/HedgeModManager.UI/HedgeModManager.UI.csproj index be08e2f..6032e8e 100644 --- a/Source/HedgeModManager.UI/HedgeModManager.UI.csproj +++ b/Source/HedgeModManager.UI/HedgeModManager.UI.csproj @@ -60,4 +60,4 @@ Resources.Designer.cs - + \ No newline at end of file diff --git a/Source/HedgeModManager.UI/Program.cs b/Source/HedgeModManager.UI/Program.cs index 27ce37e..f23aa2a 100644 --- a/Source/HedgeModManager.UI/Program.cs +++ b/Source/HedgeModManager.UI/Program.cs @@ -6,6 +6,7 @@ using System.Runtime.InteropServices; using System.Text.Json; using System.Runtime.Versioning; +using Avalonia.Controls.ApplicationLifetimes; #if !DEBUG using System.Diagnostics; @@ -176,6 +177,28 @@ static void installToRegistery(string schema, string args) } } + [SupportedOSPlatform("macos")] + public static void ListenForUriSchemeMac(Action> callback) + { + // Use Avalonia IActivatableLifetime to listen for OpenUri events on macOS + if (Application.Current?.TryGetFeature(typeof(IActivatableLifetime)) is { } activatableLifetime) + { + ((IActivatableLifetime)activatableLifetime).Activated += async (s, a) => + { + if (a is ProtocolActivatedEventArgs protocolArgs && protocolArgs.Kind == ActivationKind.OpenUri) + { + var uri = protocolArgs.Uri.ToString(); + var args = CommandLine.ParseArguments(["--schema", uri]); + var (_, commands) = CommandLine.ExecuteArguments(args); + if (commands.Count > 0) + { + callback(commands); + } + } + }; + } + } + public static string GetFormattedAppVersion() { string unstableType = "beta"; diff --git a/Source/HedgeModManager.UI/Properties/PublishProfiles/osx-arm64.pubxml b/Source/HedgeModManager.UI/Properties/PublishProfiles/osx-arm64.pubxml new file mode 100644 index 0000000..097e12a --- /dev/null +++ b/Source/HedgeModManager.UI/Properties/PublishProfiles/osx-arm64.pubxml @@ -0,0 +1,8 @@ + + + osx-arm64 + true + true + true + + diff --git a/Source/HedgeModManager.UI/Properties/PublishProfiles/osx-x64.pubxml b/Source/HedgeModManager.UI/Properties/PublishProfiles/osx-x64.pubxml new file mode 100644 index 0000000..ca16eae --- /dev/null +++ b/Source/HedgeModManager.UI/Properties/PublishProfiles/osx-x64.pubxml @@ -0,0 +1,8 @@ + + + osx-x64 + true + true + false + + diff --git a/Source/HedgeModManager.UI/ViewModels/MainWindowViewModel.cs b/Source/HedgeModManager.UI/ViewModels/MainWindowViewModel.cs index 652a2e5..b12c8f9 100644 --- a/Source/HedgeModManager.UI/ViewModels/MainWindowViewModel.cs +++ b/Source/HedgeModManager.UI/ViewModels/MainWindowViewModel.cs @@ -945,4 +945,4 @@ protected override void OnPropertyChanged(PropertyChangedEventArgs e) OnPropertyChanged(nameof(IsFullscreen)); base.OnPropertyChanged(e); } -} +} \ No newline at end of file diff --git a/Source/HedgeModManager.UI/Views/MainWindow.axaml.cs b/Source/HedgeModManager.UI/Views/MainWindow.axaml.cs index 6e4ba1c..a91fb70 100644 --- a/Source/HedgeModManager.UI/Views/MainWindow.axaml.cs +++ b/Source/HedgeModManager.UI/Views/MainWindow.axaml.cs @@ -40,7 +40,7 @@ public void LoadGames() // Select the last selected game or first game ViewModel.SelectedGame = ViewModel.Games .FirstOrDefault(x => x != null && Path.Combine(x.Game.Root, x.Game.Executable ?? "") - == ViewModel.Config.LastSelectedPath, ViewModel.Games.FirstOrDefault()); + == ViewModel.Config.LastSelectedPath, ViewModel.Games.FirstOrDefault()); } // Set to true until we create a setup @@ -70,6 +70,8 @@ private void Window_Loaded(object? sender, RoutedEventArgs e) Logger.Information($"Loading URI handlers..."); Program.InstallURIHandler(); + if (OperatingSystem.IsMacOS()) + Program.ListenForUriSchemeMac((commands) => _ = ViewModel.ProcessCommandsAsync(commands)); LoadGames(); @@ -148,6 +150,7 @@ private void OnKeyDown(object? sender, KeyEventArgs e) }); } } + break; case Key.F11: toggleFullscreen = true; @@ -207,6 +210,7 @@ private void OnKeyDown(object? sender, KeyEventArgs e) if (button != Buttons.None) _ = Dispatcher.UIThread.Invoke(async () => await ViewModel.OnInputDownAsync(button)); } + break; } diff --git a/Source/HedgeModManager/ModdableGameLocator.cs b/Source/HedgeModManager/ModdableGameLocator.cs index dd2ce3f..4d79811 100644 --- a/Source/HedgeModManager/ModdableGameLocator.cs +++ b/Source/HedgeModManager/ModdableGameLocator.cs @@ -1,4 +1,5 @@ namespace HedgeModManager; + using Foundation; using HedgeModManager.Epic; using HedgeModManager.Properties; @@ -17,7 +18,7 @@ public class ModdableGameLocator ID = "SonicGenerations", ModLoaderName = "HE1ModLoader", ModLoaderFileName = "d3d9.dll", - ModLoaderIncompatibleFileNames = [ "dinput8.dll" ], + ModLoaderIncompatibleFileNames = ["dinput8.dll"], ModLoaderDownloadURL = Resources.HE1MLDownloadURL, Is64Bit = false, PlatformInfos = new() @@ -30,7 +31,7 @@ public class ModdableGameLocator ID = "SonicLostWorld", ModLoaderName = "HE1ModLoader", ModLoaderFileName = "d3d9.dll", - ModLoaderIncompatibleFileNames = [ "dinput8.dll" ], + ModLoaderIncompatibleFileNames = ["dinput8.dll"], ModLoaderDownloadURL = Resources.HE1MLDownloadURL, Is64Bit = false, PlatformInfos = new() { { "Steam", [new ("329440", "slw.exe")] } } @@ -40,7 +41,7 @@ public class ModdableGameLocator ID = "SonicForces", ModLoaderName = "HE2ModLoader", ModLoaderFileName = "d3d11.dll", - ModLoaderIncompatibleFileNames = [ "dinput8.dll" ], + ModLoaderIncompatibleFileNames = ["dinput8.dll"], ModLoaderDownloadURL = Resources.HE2MLDownloadURL, PlatformInfos = new() { { "Steam", [new ("637100", Path.Combine("build", "main", "projects", "exec", "Sonic Forces.exe"))] } } }, @@ -132,7 +133,8 @@ public class ModdableGameLocator { { "Windows", [new(string.Empty, "SOFTWARE\\UnleashedRecomp")] }, { "Flatpak", [new("io.github.hedge_dev.unleashedrecomp", "unleashedrecomp")] }, - { "Desktop", [new("io.github.hedge_dev.unleashedrecomp", "unleashedrecomp"), new("UnleashedRecomp", "unleashedrecomp")] } + { "Desktop", [new("io.github.hedge_dev.unleashedrecomp", "unleashedrecomp"), new("UnleashedRecomp", "unleashedrecomp")] }, + { "macOS", [new("io.github.hedge_dev.unleashedrecomp", "unleashedrecomp"), new("UnleashedRecomp", "unleashedrecomp")] } } } ]; @@ -390,6 +392,27 @@ public static List LocateGames() } } } + if (OperatingSystem.IsMacOS() && gameInfo.PlatformInfos.TryGetValue("macOS", out var macOsInfo)) + { + foreach (var entry in macOsInfo) + { + string root = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + "Library", "Application Support", entry.Executable); + if (Directory.Exists(root)) + { + var gameSimple = new GameSimple( + "macOS", entry.ID, gameInfo.ID, + root, Path.GetFileName(entry.Executable), "macOS", + null, $"open -a {entry.Executable}", null); + var game = new ModdableGameGeneric(gameSimple) + { + Is64Bit = gameInfo.Is64Bit + }; + game.ModDatabase.SupportsCodeCompilation = gameInfo.SupportsCodes; + games.Add(game); + } + } + } } return games; diff --git a/docs/BUILDING.md b/docs/BUILDING.md new file mode 100644 index 0000000..576be7e --- /dev/null +++ b/docs/BUILDING.md @@ -0,0 +1,26 @@ +# Building from source + +## 1. Clone the repository +```zsh +git clone https://github.com/hedge-dev/HedgeModManager.git +``` + +## 2. Build the project + +### macOS + +1. Navigate to the root of the project +2. Run the build command +```zsh +dotnet publish -p:PublishProfile=osx-arm64 -c Release -o ./output/macos/osx-arm64 ./Source/HedgeModManager.UI/HedgeModManager.UI.csproj -p:UseAppHost=true +``` +3. Navigate to `macos` +```zsh +cd macos +``` +4. Run the following command to create the app bundle. +```zsh +/bin/bash generate-bundle.bash +``` +5. Navigate to `../output/macos` +6. Run `HedgeModManager.app` diff --git a/macos/AppIcon.icns b/macos/AppIcon.icns new file mode 100644 index 0000000..e85f5d7 Binary files /dev/null and b/macos/AppIcon.icns differ diff --git a/macos/Info.plist b/macos/Info.plist new file mode 100644 index 0000000..9fce9e4 --- /dev/null +++ b/macos/Info.plist @@ -0,0 +1,43 @@ + + + + + CFBundleURLTypes + + + CFBundleURLSchemes + + hedgemmswa + hedgemmgens + hedgemmlw + hedgemmforces + hedgemmtenpex + hedgemmmusashi + hedgemmrainbow + hedgemmhite + hedgemmrangers + hedgemmmillersonic + hedgemmmillershadow + + + + CFBundleIconFile + AppIcon.icns + CFBundleIdentifier + io.github.hedge_dev.hedgemodmanager + CFBundleName + HedgeModManager + CFBundleVersion1.0.0 + LSMinimumSystemVersion + 12.0 + CFBundleExecutable + HedgeModManager.UI + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + APPL + CFBundleShortVersionString1.0.0 + NSHighResolutionCapable + + + \ No newline at end of file diff --git a/macos/generate-bundle.bash b/macos/generate-bundle.bash new file mode 100644 index 0000000..c8ad69d --- /dev/null +++ b/macos/generate-bundle.bash @@ -0,0 +1,31 @@ +#!/bin/bash +VERSION=${1:-"1.0.0"} + +APP_NAME="../output/macos/HedgeModManager.app" +PLIST_PATH="./Info.plist" +PUBLISH_OUTPUT_DIRECTORY="../output/macos/osx-arm64/." + +ICON_FILE="./AppIcon.icns" + +if [ -d "$APP_NAME" ] +then + rm -rf "$APP_NAME" +fi + +mkdir "$APP_NAME" + +mkdir "$APP_NAME/Contents" +mkdir "$APP_NAME/Contents/MacOS" +mkdir "$APP_NAME/Contents/Resources" + +cp "$ICON_FILE" "$APP_NAME/Contents/Resources/AppIcon.icns" +cp -a "$PUBLISH_OUTPUT_DIRECTORY" "$APP_NAME/Contents/MacOS" +cp -a "$PLIST_PATH" "$APP_NAME/Contents/Info.plist" + +PLIST_PATH="$APP_NAME/Contents/Info.plist" + +# Update CFBundleShortVersionString using sed +sed -i '' -e "s/CFBundleShortVersionString<\/key>\s*[^<]*<\/string>/CFBundleShortVersionString<\/key>${VERSION}<\/string>/g" "$PLIST_PATH" + +# Update CFBundleVersion using sed +sed -i '' -e "s/CFBundleVersion<\/key>\s*[^<]*<\/string>/CFBundleVersion<\/key>${VERSION}<\/string>/g" "$PLIST_PATH"