diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d83f421d..207cfdfe 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,29 +22,28 @@ jobs: - os: ubuntu-latest rid: linux-x64 steps: - - uses: actions/checkout@v2 - - uses: actions/setup-dotnet@v1 - with: - dotnet-version: '3.1.x' + - uses: actions/checkout@v4 - name: Build run: | - cd VisualPinball.Engine.Mpf - dotnet build -c Release -r ${{ matrix.rid }} -# - name: Test -# run: | -# cd VisualPinball.Engine.Mpf.Test -# dotnet run -c Release -r ${{ matrix.rid }} - - run: | - mkdir tmp - cp -r VisualPinball.Engine.Mpf.Unity/Plugins/${{ matrix.rid }} tmp - - uses: actions/upload-artifact@v2 + dotnet build -c Release -r ${{ matrix.rid }} -p:InstallYetAnotherHttpHandler=false + - uses: actions/upload-artifact@v4 with: - name: Plugins - path: tmp + name: NuGetDependencies-${{ matrix.rid }} + path: Dependencies/NuGetDependencies/${{ matrix.rid }} + install-yetanotherhttphandler: + name: Install YetAnotherHttpHandler + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: dotnet build /t:InstallYetAnotherHttpHandler + - uses: actions/upload-artifact@v4 + with: + name: YetAnotherHttpHandler + path: Dependencies/YetAnotherHttpHandler dispatch: runs-on: ubuntu-latest - needs: [ build ] + needs: [ build, install-yetanotherhttphandler ] if: github.repository == 'VisualPinball/VisualPinball.Engine.Mpf' && github.ref == 'refs/heads/master' && github.event_name == 'push' steps: - uses: peter-evans/repository-dispatch@v1 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e906f841..a08b3ee5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -7,15 +7,14 @@ jobs: registry: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: dawidd6/action-download-artifact@v2 + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 with: - workflow: build - run_id: ${{ github.event.client_payload.artifacts_run_id }} - path: VisualPinball.Engine.Mpf.Unity + merge-multiple: true + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.client_payload.artifacts_run_id }} - name: Publish run: | - cd VisualPinball.Engine.Mpf.Unity echo "//registry.visualpinball.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc npm publish env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 509e895b..fe2784a7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,19 +7,17 @@ jobs: release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Fetch next version id: nextVersion uses: VisualPinball/next-version-action@v0.1.7 with: - path: VisualPinball.Engine.Mpf.Unity tagPrefix: 'v' - name: Bump if: ${{ steps.nextVersion.outputs.isBump == 'true' }} run: | - cd VisualPinball.Engine.Mpf.Unity npm version ${{ steps.nextVersion.outputs.nextVersion }} --no-git-tag-version - name: Commit id: commit @@ -27,7 +25,7 @@ jobs: run: | git config user.name "github-actions" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add VisualPinball.Engine.Mpf.Unity/package.json + git add package.json git commit -m "release: ${{ steps.nextVersion.outputs.nextTag }}." git push commitish=$(git rev-parse HEAD) diff --git a/.gitignore b/.gitignore index 0496174a..2a922a9b 100644 --- a/.gitignore +++ b/.gitignore @@ -353,3 +353,9 @@ MigrationBackup/ **/protos/*.cs VisualPinball.Engine.Mpf/machine/data/ + +# macOS meta files +*.DS_Store + +Dependencies/ +Dependencies.meta \ No newline at end of file diff --git a/VisualPinball.Engine.Mpf.Unity/Editor.meta b/Editor.meta similarity index 100% rename from VisualPinball.Engine.Mpf.Unity/Editor.meta rename to Editor.meta diff --git a/Editor/BuildPostprocessing.cs b/Editor/BuildPostprocessing.cs new file mode 100644 index 00000000..776579c1 --- /dev/null +++ b/Editor/BuildPostprocessing.cs @@ -0,0 +1,140 @@ +// Visual Pinball Engine +// Copyright (C) 2025 freezy and VPE Team +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using System.IO; +using System.Linq; +using NLog; +using UnityEditor; +using UnityEditor.Build; +using UnityEditor.Build.Reporting; +using Logger = NLog.Logger; + +namespace VisualPinball.Engine.Mpf.Unity.Editor +{ + public class BuildPostprocessing : IPostprocessBuildWithReport + { + int IOrderedCallback.callbackOrder => 0; + + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + private const string _unsupportedPlaformMessage = + "Visual Pinball Engine does not ship with an MPF executable for the build " + + "platform '{}}.' The build will not work unless MPF is installed " + + "on the end-user's device"; + + void IPostprocessBuildWithReport.OnPostprocessBuild(BuildReport report) + { + if ( + report.summary.result == BuildResult.Failed + || report.summary.result == BuildResult.Cancelled + ) + return; + + var streamingAssetsPath = FindStreamingAssets( + report.summary.platform, + report.summary.outputPath + ); + CleanMachineFolder(streamingAssetsPath); + AddMpfBinaries(report.summary.platform, streamingAssetsPath); + } + + private static string FindStreamingAssets(BuildTarget platform, string buildExePath) + { + string dataDir; + + if ( + platform + is BuildTarget.StandaloneWindows + or BuildTarget.StandaloneWindows64 + or BuildTarget.StandaloneLinux64 + ) + { + dataDir = Directory + .GetDirectories(Directory.GetParent(buildExePath).ToString(), "*_Data") + .FirstOrDefault(); + } + else if (platform is BuildTarget.StandaloneOSX) + { + dataDir = Path.Combine(buildExePath, "Contents", "Resources", "Data"); + } + else + { + throw new PlatformNotSupportedException( + string.Format(_unsupportedPlaformMessage, platform) + ); + } + + return Path.Combine(dataDir, "StreamingAssets"); + } + + private static void AddMpfBinaries(BuildTarget platform, string streamingAssetsPath) + { + Logger.Info("Adding MPF binaries to build..."); + // Get the directory of the MPF package from the Unity package manager + var packageInfo = UnityEditor.PackageManager.PackageInfo.FindForAssembly( + typeof(BuildPostprocessing).Assembly + ); + + var binaryDirName = platform switch + { + BuildTarget.StandaloneLinux64 => Constants.MpfBinaryDirLinux, + BuildTarget.StandaloneOSX => Constants.MpfBinaryDirMacOS, + BuildTarget.StandaloneWindows => Constants.MpfBinaryDirWindows, + BuildTarget.StandaloneWindows64 => Constants.MpfBinaryDirWindows, + _ => throw new PlatformNotSupportedException( + string.Format(_unsupportedPlaformMessage, platform) + ), + }; + var sourcePath = Path.Combine( + packageInfo.resolvedPath, + Constants.MpfBinariesDirName, + binaryDirName + ); + + var destPath = Path.Combine( + streamingAssetsPath, + Constants.MpfBinariesDirName, + binaryDirName + ); + + Directory.CreateDirectory(destPath); + CopyUtil.CopyDirectory(sourcePath, destPath, recursive: true, overwrite: true); + + Logger.Info("Successfully added MPF binaries to build."); + } + + private static void CleanMachineFolder(string streamingAssetsPath) + { + Logger.Info("Removing log files and audits from machine folder..."); + + var machineFolders = Directory + .GetDirectories(streamingAssetsPath) + .Where((dir) => File.Exists(Path.Combine(dir, "config", "config.yaml"))); + + foreach (var mf in machineFolders) + { + // Delete log files from previous runs + var logDir = Path.Combine(mf, "logs"); + var logFiles = Directory.GetFiles(logDir, "*.log", SearchOption.TopDirectoryOnly); + foreach (var logFile in logFiles) + File.Delete(logFile); + + // Delete audits file (contains statistics about previous runs) + var auditsFile = Path.Combine(mf, "data", "audits.yaml"); + if (File.Exists(auditsFile)) + File.Delete(auditsFile); + } + + Logger.Info("Successfully removed log files and audits from machine folder."); + } + } +} diff --git a/Editor/BuildPostprocessing.cs.meta b/Editor/BuildPostprocessing.cs.meta new file mode 100644 index 00000000..03e33772 --- /dev/null +++ b/Editor/BuildPostprocessing.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 59dc6ec9a08e68446aa1070e0834b4c6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/CopyUtil.cs b/Editor/CopyUtil.cs new file mode 100644 index 00000000..2cf8c776 --- /dev/null +++ b/Editor/CopyUtil.cs @@ -0,0 +1,53 @@ +// Visual Pinball Engine +// Copyright (C) 2025 freezy and VPE Team +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System.IO; + +namespace VisualPinball.Engine.Mpf.Unity.Editor +{ + public static class CopyUtil + { + // Source: https://learn.microsoft.com/en-us/dotnet/standard/io/how-to-copy-directories + public static void CopyDirectory( + string sourceDir, + string destinationDir, + bool recursive, + bool overwrite + ) + { + var dir = new DirectoryInfo(sourceDir); + + if (!dir.Exists) + throw new DirectoryNotFoundException($"Source directory not found: {dir.FullName}"); + + DirectoryInfo[] dirs = dir.GetDirectories(); + Directory.CreateDirectory(destinationDir); + + foreach (FileInfo file in dir.GetFiles()) + { + if (file.Extension == ".meta") + continue; + string targetFilePath = Path.Combine(destinationDir, file.Name); + if (overwrite || !File.Exists(targetFilePath)) + file.CopyTo(targetFilePath, overwrite); + } + + if (recursive) + { + foreach (DirectoryInfo subDir in dirs) + { + string newDestinationDir = Path.Combine(destinationDir, subDir.Name); + CopyDirectory(subDir.FullName, newDestinationDir, true, overwrite); + } + } + } + } +} diff --git a/Editor/CopyUtil.cs.meta b/Editor/CopyUtil.cs.meta new file mode 100644 index 00000000..7dbbf647 --- /dev/null +++ b/Editor/CopyUtil.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0c9e5d4485c76ac498eee3ccbbded7be +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Inspector.meta b/Editor/Inspector.meta new file mode 100644 index 00000000..257484f3 --- /dev/null +++ b/Editor/Inspector.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f922972ac02300e45a2f23c5d4439bac +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Inspector/MpfGamelogicEngineInspector.cs b/Editor/Inspector/MpfGamelogicEngineInspector.cs new file mode 100644 index 00000000..e439c0ed --- /dev/null +++ b/Editor/Inspector/MpfGamelogicEngineInspector.cs @@ -0,0 +1,301 @@ +// Visual Pinball Engine +// Copyright (C) 2025 freezy and VPE Team +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// ReSharper disable AssignmentInConditionalExpression + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using Grpc.Core; +using Mono.Cecil; +using UnityEditor; +using UnityEditor.UIElements; +using UnityEngine; +using UnityEngine.UIElements; +using VisualPinball.Unity; + +namespace VisualPinball.Engine.Mpf.Unity.Editor +{ + [CustomEditor(typeof(MpfGamelogicEngine))] + public class MpfGamelogicEngineInspector : UnityEditor.Editor + { + [SerializeField] + private VisualTreeAsset _inspectorXml; + + private CancellationTokenSource _getMachineDescCts; + private MpfGamelogicEngine _mpfEngine; + private PropertyField _connectTimeoutField; + private PropertyField _connectDelayField; + private VisualElement _commandLineOptionsContainer; + private VisualElement _startupBehaviorOptionsContainer; + + public override VisualElement CreateInspectorGUI() + { + var root = _inspectorXml.Instantiate(); + _mpfEngine = (MpfGamelogicEngine)serializedObject.targetObject; + var tableComponent = _mpfEngine.GetComponentInParent(); + + var machineFolderField = root.Q("machine-folder"); + var machineFolderInput = machineFolderField.Q(name: "unity-text-input"); + machineFolderInput.RegisterCallback( + (evt) => + { + if (!Directory.Exists(Application.streamingAssetsPath)) + { + Directory.CreateDirectory(Application.streamingAssetsPath); + } + + var path = EditorUtility.OpenFolderPanel( + "Mission Pinball Framework: Choose machine folder", + Application.streamingAssetsPath, + "" + ); + + if (!string.IsNullOrWhiteSpace(path)) + { + path = path.Replace("\\", "/"); + if (path.Contains("StreamingAssets/")) + path = "./StreamingAssets/" + path.Split("StreamingAssets/")[1]; + + var machineFolderProp = serializedObject.FindProperty( + $"_mpfWrangler._machineFolder" + ); + machineFolderProp.stringValue = path; + serializedObject.ApplyModifiedProperties(); + } + } + ); + + var getDescBtn = root.Q