diff --git a/.github/actions/compatibility-check/action.yml b/.github/actions/compatibility-check/action.yml new file mode 100644 index 00000000..bf84e3f6 --- /dev/null +++ b/.github/actions/compatibility-check/action.yml @@ -0,0 +1,183 @@ +name: 'Compatibility Check' +description: 'Check for breaking changes against the original Legally-Distinct-Missile repository' + +inputs: + original-repo: + description: 'Original repository to compare against' + required: false + default: 'SmartlyDressedGames/Legally-Distinct-Missile' + original-branch: + description: 'Branch of original repository to compare against' + required: false + default: 'master' + runtime-version: + description: 'Runtime version for build paths' + required: false + default: 'linux-x64' + fail-on-breaking-changes: + description: 'Whether to fail the workflow if breaking changes are detected' + required: false + default: 'true' + +outputs: + has-breaking-changes: + description: 'Whether breaking changes were detected' + value: ${{ steps.check.outputs.has-breaking-changes }} + compatibility-report: + description: 'Path to the compatibility report' + value: ${{ steps.check.outputs.report-path }} + +runs: + using: 'composite' + steps: + - name: Checkout original repository + uses: actions/checkout@v5 + with: + repository: ${{ inputs.original-repo }} + ref: ${{ inputs.original-branch }} + path: original-repo + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.x + + - name: Build original repo's Rocket.API + run: | + cd original-repo + dotnet build Rocket/Rocket.API/Rocket.API.csproj --configuration Release --verbosity minimal + shell: bash + + - name: Build original repo's Rocket.Core + run: | + cd original-repo + dotnet build Rocket/Rocket.Core/Rocket.Core.csproj --configuration Release --verbosity minimal + shell: bash + + - name: Build original repo's Rocket.Unturned + run: | + cd original-repo + dotnet build Rocket.Unturned/Rocket.Unturned.csproj --configuration Release --verbosity minimal + shell: bash + + - name: Install API compatibility tools + shell: bash + run: | + dotnet tool install --global Microsoft.DotNet.ApiCompat.Tool + + - name: Run compatibility checks + id: check + shell: bash + run: | + # Create reports directory + mkdir -p compatibility-reports + + # Check Rocket.API compatibility + apicompat \ + --left ./original-repo/Rocket/Rocket.API/bin/Release/Rocket.API.dll \ + --right ./Rocket/Rocket.API/bin/Release/net461/${{ inputs.runtime-version }}/Rocket.API.dll \ + > ./compatibility-reports/api-compat-report.txt 2>&1 || true + + # Check Rocket.Core compatibility + apicompat \ + --left ./original-repo/Rocket/Rocket.Core/bin/Release/Rocket.Core.dll \ + --right ./Rocket/Rocket.Core/bin/Release/net461/${{ inputs.runtime-version }}/Rocket.Core.dll \ + > ./compatibility-reports/core-compat-report.txt 2>&1 || true + + # Check Rocket.Unturned compatibility + apicompat \ + --left ./original-repo/Rocket.Unturned/bin/Release/Rocket.Unturned.dll \ + --right ./Rocket.Unturned/bin/Release/net461/${{ inputs.runtime-version }}/Rocket.Unturned.dll \ + > ./compatibility-reports/unturned-compat-report.txt 2>&1 || true + + # Check for breaking changes + has_breaking_changes=false + + for report in ./compatibility-reports/*.txt; do + if grep -q "Breaking Changes\|Error" "$report"; then + has_breaking_changes=true + break + fi + done + + echo "has-breaking-changes=$has_breaking_changes" >> $GITHUB_OUTPUT + echo "report-path=./compatibility-reports" >> $GITHUB_OUTPUT + + # Create summary report + cat > ./compatibility-reports/summary.md << EOF + # Compatibility Check Summary + + ## Results + - **Breaking Changes Detected**: $has_breaking_changes + - **Check Date**: $(date -u) + - **Original Repository**: ${{ inputs.original-repo }} + - **Original Branch**: ${{ inputs.original-branch }} + + ## Detailed Reports + - [API Compatibility](./api-compat-report.txt) + - [Core Compatibility](./core-compat-report.txt) + - [Unturned Compatibility](./unturned-compat-report.txt) + + ## Recommendations + EOF + + if [ "$has_breaking_changes" = "true" ]; then + cat >> ./compatibility-reports/summary.md << EOF + + ⚠️ **BREAKING CHANGES DETECTED** + + Your changes may break existing plugins. Please review the detailed reports above. + Consider: + 1. Reverting breaking changes if possible + 2. Adding compatibility layers + 3. Documenting migration steps for plugin developers + EOF + else + cat >> ./compatibility-reports/summary.md << EOF + + ✅ **No breaking changes detected** + + Your changes appear to be compatible with the original repository. + Existing plugins should continue to work without modification. + EOF + fi + + - name: Upload compatibility reports + uses: actions/upload-artifact@v4 + with: + name: compatibility-reports + path: ./compatibility-reports + + - name: Comment on PR if breaking changes detected + if: github.event_name == 'pull_request' && steps.check.outputs.has-breaking-changes == 'true' + uses: actions/github-script@v7 + with: + script: | + const workflowRunId = context.runId; + const repoOwner = context.repo.owner; + const repoName = context.repo.repo; + + const comment = `❌ **Breaking changes detected** + + Your changes may break existing plugins. Check the [compatibility reports](https://github.com/${repoOwner}/${repoName}/actions/runs/${workflowRunId}/artifacts) for details. + + ### Detailed Reports + - [API Compatibility Report](./api-compat-report.txt) + - [Core Compatibility Report](./core-compat-report.txt) + - [Unturned Compatibility Report](./unturned-compat-report.txt) + `; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: comment + }); + + - name: Fail on breaking changes + if: inputs.fail-on-breaking-changes == 'true' && steps.check.outputs.has-breaking-changes == 'true' + shell: bash + run: | + echo "❌ Breaking changes detected! Check the compatibility reports for details." + exit 1 diff --git a/.github/actions/nuget-pack/action.yaml b/.github/actions/nuget-pack/action.yaml index a139a899..66a0030a 100644 --- a/.github/actions/nuget-pack/action.yaml +++ b/.github/actions/nuget-pack/action.yaml @@ -19,6 +19,6 @@ runs: shell: bash - name: Push to NuGet (Release) run: if ${{ inputs.nuget_push == 'true' }}; then - dotnet nuget push *.nupkg --skip-duplicate --api-key ${{ inputs.nuget_key }} --source https://api.nuget.org/v3/index.json; + dotnet nuget push *.nupkg --api-key ${{ inputs.nuget_key }} --source https://api.nuget.org/v3/index.json; fi shell: bash \ No newline at end of file diff --git a/.github/actions/project-build-windows/action.yaml b/.github/actions/project-build-windows/action.yaml new file mode 100644 index 00000000..f8c69807 --- /dev/null +++ b/.github/actions/project-build-windows/action.yaml @@ -0,0 +1,96 @@ +name: 'Project Build Windows' +description: 'Builds RocketModFix projects on Windows runners (for .NET Framework compatibility)' +inputs: + project_path: + description: 'Path to project folder' + required: true + runtime_version: + description: 'Build runtime version default win-x64' + required: false + default: 'win-x64' + nuget_push: + description: 'Push to Nuget on release?' + required: false + default: false + nuget_key: + description: 'NuGet deploy key' + required: false + github_token: + description: 'GitHub token' + required: false +outputs: + version: + description: "Generated version (SemVersion compatible)" + value: ${{ steps.get-version.outputs.version }} + is_prerelease: + description: 'Gets if the version is a prerelease' + value: ${{ steps.check-prerelease.outputs.is_prerelease }} +runs: + using: "composite" + steps: + # Generate semver compatible version from current tag and commit hash + - name: Create version + id: get-version + run: echo "version=$(git describe --tags `git rev-list --tags --max-count=1`)+$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + shell: bash + + - name: Check Prerelease + id: check-prerelease + run: "if ${{ contains(steps.get-version.outputs.version, '-') }}; then + echo is_prerelease=true >> $GITHUB_OUTPUT; + else + echo is_prerelease=false >> $GITHUB_OUTPUT; + fi" + shell: bash + + # Commands that are used multiple times. + # Placed in one place to make sure that the arguments would always be the same + - name: Common commands + id: common-commands + run: | + echo "dotnet-restore=dotnet restore \$PROJECT_PATH --runtime ${{ inputs.runtime_version }}" >> $GITHUB_OUTPUT + echo "dotnet-build=dotnet build \$PROJECT_PATH --configuration Release --no-restore -p:RocketModFixVersion=${{ steps.get-version.outputs.version }} --runtime ${{ inputs.runtime_version }}" >> $GITHUB_OUTPUT + echo "dotnet-test=dotnet test \$PROJECT_PATH --configuration Release --no-restore --no-build --runtime ${{ inputs.runtime_version }}" >> $GITHUB_OUTPUT + shell: bash + + # Install dependencies (this needs to be a separate step from build for caching) + - name: Install dependencies + run: | + ${{ steps.common-commands.outputs.dotnet-restore }} + pwsh -File .github/actions/project-build/run-command-for-every-tests-project.ps1 "$PROJECT_PATH" '${{ steps.common-commands.outputs.dotnet-restore }}' + env: + PROJECT_PATH: ${{ inputs.project_path }} # Used by commands in `common-commands` step + shell: bash + + # Build project + - name: Build + run: | + ${{ steps.common-commands.outputs.dotnet-build }} + pwsh -File .github/actions/project-build/run-command-for-every-tests-project.ps1 "$PROJECT_PATH" '${{ steps.common-commands.outputs.dotnet-build }}' + env: + PROJECT_PATH: ${{ inputs.project_path }} # Used by commands in `common-commands` step + shell: bash + + # Test project + - name: Test + run: | + pwsh -File .github/actions/project-build/run-command-for-every-tests-project.ps1 "$PROJECT_PATH" '${{ steps.common-commands.outputs.dotnet-test }}' + env: + PROJECT_PATH: ${{ inputs.project_path }} # Used by commands in `common-commands` step + shell: bash + + # Push to GitHub packages on each commit and release + - name: Push to NuGet (Nightly) + if: | + inputs.nuget_push == 'true' && (github.event_name == 'push' || + github.event_name == 'create' && github.event.ref_type == 'tag') + run: dotnet nuget push ${{ inputs.project_path }}/bin/Release/*.nupkg --api-key ${{ inputs.github_token }} --source https://nuget.pkg.github.com/RocketModFix/index.json; + shell: bash + + # Push to NuGet on each tag, but only if the tag is not a pre-release version + - name: Push to NuGet (Release) + if: | + inputs.nuget_push == 'true' && steps.check-prerelease.outputs.is_prerelease == 'false' && + github.event_name == 'create' && github.event.ref_type == 'tag' + run: dotnet nuget push ${{ inputs.project_path }}/bin/Release/*.nupkg --api-key ${{ inputs.nuget_key }} --source https://api.nuget.org/v3/index.json; + shell: bash diff --git a/.github/actions/project-build/action.yaml b/.github/actions/project-build/action.yaml index 452c8b77..9813f6a6 100644 --- a/.github/actions/project-build/action.yaml +++ b/.github/actions/project-build/action.yaml @@ -28,6 +28,28 @@ outputs: runs: using: "composite" steps: + # Install .NET Framework reference assemblies for .NET Framework 4.8 + - name: Install .NET Framework Reference Assemblies + run: | + # Download and install .NET Framework 4.8 reference assemblies from official source + mkdir -p /tmp/net48-refs + curl -L -o /tmp/net48.zip "https://api.nuget.org/v3-flatcontainer/microsoft.netframework.referenceassemblies/1.0.3/microsoft.netframework.referenceassemblies.1.0.3.nupkg" + unzip -q /tmp/net48.zip -d /tmp/net48-refs/ + + # Create the target directory + mkdir -p /usr/share/dotnet/reference-assemblies/microsoft/framework/.netframework/v4.8 + + # Copy reference assemblies to the expected location + if [ -d "/tmp/net48-refs/build/.NETFramework/v4.8" ]; then + cp -r /tmp/net48-refs/build/.NETFramework/v4.8/* /usr/share/dotnet/reference-assemblies/microsoft/framework/.netframework/v4.8/ + elif [ -d "/tmp/net48-refs/ref/net48" ]; then + cp -r /tmp/net48-refs/ref/net48/* /usr/share/dotnet/reference-assemblies/microsoft/framework/.netframework/v4.8/ + fi + + # Clean up + rm -rf /tmp/net48-refs /tmp/net48.zip + shell: bash + # Generate semver compatible version from current tag and commit hash - name: Create version id: get-version @@ -71,6 +93,8 @@ runs: PROJECT_PATH: ${{ inputs.project_path }} # Used by commands in `common-commands` step shell: bash + + # Test project - name: Test run: | @@ -84,7 +108,7 @@ runs: if: | inputs.nuget_push == 'true' && (github.event_name == 'push' || github.event_name == 'create' && github.event.ref_type == 'tag') - run: dotnet nuget push ${{ inputs.project_path }}/bin/Release/*.nupkg --api-key ${{ inputs.github_token }} --skip-duplicate --source https://nuget.pkg.github.com/RocketModFix/index.json; + run: dotnet nuget push ${{ inputs.project_path }}/bin/Release/*.nupkg --api-key ${{ inputs.github_token }} --source https://nuget.pkg.github.com/RocketModFix/index.json; shell: bash # Push to NuGet on each tag, but only if the tag is not a pre-release version @@ -92,5 +116,5 @@ runs: if: | inputs.nuget_push == 'true' && steps.check-prerelease.outputs.is_prerelease == 'false' && github.event_name == 'create' && github.event.ref_type == 'tag' - run: dotnet nuget push ${{ inputs.project_path }}/bin/Release/*.nupkg --api-key ${{ inputs.nuget_key }} --skip-duplicate --source https://api.nuget.org/v3/index.json; + run: dotnet nuget push ${{ inputs.project_path }}/bin/Release/*.nupkg --api-key ${{ inputs.nuget_key }} --source https://api.nuget.org/v3/index.json; shell: bash \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8095c401..4fb4812f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,8 +7,7 @@ updates: - package-ecosystem: "nuget" directory: "/" schedule: - interval: "weekly" - day: "monday" + interval: "daily" time: "09:00" timezone: "UTC" open-pull-requests-limit: 10 @@ -31,8 +30,7 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "weekly" - day: "monday" + interval: "daily" time: "09:00" timezone: "UTC" open-pull-requests-limit: 5 diff --git a/.github/workflows/Rocket.API.yaml b/.github/workflows/Rocket.API.yaml index 7e877509..e2a27852 100644 --- a/.github/workflows/Rocket.API.yaml +++ b/.github/workflows/Rocket.API.yaml @@ -16,16 +16,39 @@ on: - 'Rocket/Rocket.API/**' jobs: + Compatibility-Check: + runs-on: windows-latest + + steps: + - name: Checkout code + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - uses: actions/setup-dotnet@v6 + name: Setup .NET + with: + dotnet-version: 8.x + + - name: Check compatibility with original repository + uses: ./.github/actions/compatibility-check + with: + fail-on-breaking-changes: true + build: name: "Rocket.API Build" runs-on: ubuntu-latest + needs: Compatibility-Check + permissions: + contents: read + packages: write steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 with: fetch-depth: 0 - - uses: actions/setup-dotnet@v4 + - uses: actions/setup-dotnet@v6 name: Setup .NET with: dotnet-version: 8.x @@ -34,6 +57,6 @@ jobs: id: project-build with: project_path: Rocket/Rocket.API - github_token: ${{ secrets.PAT }} + github_token: ${{ secrets.GITHUB_TOKEN }} nuget_key: ${{ secrets.NUGET_DEPLOY_KEY }} nuget_push: true \ No newline at end of file diff --git a/.github/workflows/Rocket.Core.yaml b/.github/workflows/Rocket.Core.yaml index edf05083..76529bd6 100644 --- a/.github/workflows/Rocket.Core.yaml +++ b/.github/workflows/Rocket.Core.yaml @@ -16,16 +16,39 @@ on: - 'Rocket/Rocket.Core/**' jobs: + Compatibility-Check: + runs-on: windows-latest + + steps: + - name: Checkout code + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - uses: actions/setup-dotnet@v6 + name: Setup .NET + with: + dotnet-version: 8.x + + - name: Check compatibility with original repository + uses: ./.github/actions/compatibility-check + with: + fail-on-breaking-changes: true + build: name: "Rocket.Core Build" runs-on: ubuntu-latest + needs: Compatibility-Check + permissions: + contents: read + packages: write steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 with: fetch-depth: 0 - - uses: actions/setup-dotnet@v4 + - uses: actions/setup-dotnet@v6 name: Setup .NET with: dotnet-version: 8.x @@ -34,6 +57,6 @@ jobs: id: project-build with: project_path: Rocket/Rocket.Core - github_token: ${{ secrets.PAT }} + github_token: ${{ secrets.GITHUB_TOKEN }} nuget_key: ${{ secrets.NUGET_DEPLOY_KEY }} nuget_push: true \ No newline at end of file diff --git a/.github/workflows/Rocket.Unturned.yaml b/.github/workflows/Rocket.Unturned.yaml index 8a3916a5..150bb376 100644 --- a/.github/workflows/Rocket.Unturned.yaml +++ b/.github/workflows/Rocket.Unturned.yaml @@ -16,16 +16,39 @@ on: - 'Rocket.Unturned/**' jobs: + Compatibility-Check: + runs-on: windows-latest + + steps: + - name: Checkout code + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - uses: actions/setup-dotnet@v6 + name: Setup .NET + with: + dotnet-version: 8.x + + - name: Check compatibility with original repository + uses: ./.github/actions/compatibility-check + with: + fail-on-breaking-changes: true + build: name: "Rocket.Unturned Build" runs-on: ubuntu-latest + needs: Compatibility-Check + permissions: + contents: read + packages: write steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 with: fetch-depth: 0 - - uses: actions/setup-dotnet@v4 + - uses: actions/setup-dotnet@v6 name: Setup .NET with: dotnet-version: 8.x @@ -34,6 +57,6 @@ jobs: id: project-build with: project_path: Rocket.Unturned - github_token: ${{ secrets.PAT }} + github_token: ${{ secrets.GITHUB_TOKEN }} nuget_key: ${{ secrets.NUGET_DEPLOY_KEY }} nuget_push: true \ No newline at end of file diff --git a/.github/workflows/RocketModFix.yaml b/.github/workflows/RocketModFix.yaml index c1df839d..1509d32c 100644 --- a/.github/workflows/RocketModFix.yaml +++ b/.github/workflows/RocketModFix.yaml @@ -19,16 +19,38 @@ on: - 'Rocket.Unturned/**' jobs: + Compatibility-Check: + runs-on: windows-latest + + steps: + - name: Checkout code + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - uses: actions/setup-dotnet@v6 + name: Setup .NET + with: + dotnet-version: 8.x + + - name: Check compatibility with original repository + uses: ./.github/actions/compatibility-check + with: + fail-on-breaking-changes: true + Build: runs-on: ubuntu-latest + needs: Compatibility-Check + permissions: + contents: write steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v7 with: fetch-depth: 0 - - uses: actions/setup-dotnet@v4 + - uses: actions/setup-dotnet@v6 name: Setup .NET with: dotnet-version: 8.x @@ -40,13 +62,7 @@ jobs: project_path: Rocket.AutoInstaller nuget_key: ${{ secrets.NUGET_DEPLOY_KEY }} nuget_push: false - github_token: ${{ secrets.PAT }} - - - name: Install zip - run: sudo apt-get install zip - - - name: Zip Rocket.AutoInstaller artifacts - run: "cd ./Rocket.AutoInstaller/bin/Release/net461/linux-x64/Rocket.AutoInstaller && zip -qq -r ./Rocket.AutoInstaller.zip *" + github_token: ${{ secrets.GITHUB_TOKEN }} - name: Build Rocket.Unturned.Module uses: ./.github/actions/project-build @@ -55,35 +71,38 @@ jobs: project_path: Rocket.Unturned nuget_key: ${{ secrets.NUGET_DEPLOY_KEY }} nuget_push: false - github_token: ${{ secrets.PAT }} - - - name: Zip Rocket.Unturned artifacts - run: "cd ./Rocket.Unturned/bin/Release/net461/linux-x64/Rocket.Unturned && zip -qq -r ./Rocket.Unturned.Module.zip *" + github_token: ${{ secrets.GITHUB_TOKEN }} - name: Upload Rocket.AutoInstaller - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: - name: Rocket.AutoInstaller.zip - path: "./Rocket.AutoInstaller/bin/Release/net461/linux-x64/Rocket.AutoInstaller/Rocket.AutoInstaller.zip" + name: Rocket.AutoInstaller + path: "./Rocket.AutoInstaller/bin/Release/net461/linux-x64/Rocket.AutoInstaller/" if-no-files-found: error - + - name: Upload Rocket.Unturned.Module - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: - name: Rocket.Unturned.Module.zip - path: "./Rocket.Unturned/bin/Release/net461/linux-x64/Rocket.Unturned/Rocket.Unturned.Module.zip" + name: Rocket.Unturned.Module + path: "./Rocket.Unturned/bin/Release/net461/linux-x64/Rocket.Unturned/" if-no-files-found: error + - name: Zip Rocket.AutoInstaller artifacts + run: "cd ./Rocket.AutoInstaller/bin/Release/net461/linux-x64/Rocket.AutoInstaller && zip -qq -r ./Rocket.AutoInstaller.zip *" + + - name: Zip Rocket.Unturned artifacts + run: "cd ./Rocket.Unturned/bin/Release/net461/linux-x64/Rocket.Unturned && zip -qq -r ./Rocket.Unturned.Module.zip *" + - name: Create Release if: github.event_name == 'create' && github.event.ref_type == 'tag' - uses: ncipollo/release-action@v1.18.0 + uses: ncipollo/release-action@v1.21.0 with: name: RocketModFix Release v${{ steps.auto-installer-build.outputs.version }} tag: ${{ steps.auto-installer-build.outputs.version }} artifacts: | ./Rocket.AutoInstaller/bin/Release/net461/linux-x64/Rocket.AutoInstaller/Rocket.AutoInstaller.zip ./Rocket.Unturned/bin/Release/net461/linux-x64/Rocket.Unturned/Rocket.Unturned.Module.zip - token: ${{ secrets.PAT }} + token: ${{ secrets.GITHUB_TOKEN }} prerelease: ${{ steps.auto-installer-build.outputs.is_prerelease }} allowUpdates: true draft: true \ No newline at end of file diff --git a/.github/workflows/api-suppressions.xml b/.github/workflows/api-suppressions.xml new file mode 100644 index 00000000..70d5802f --- /dev/null +++ b/.github/workflows/api-suppressions.xml @@ -0,0 +1,34 @@ + + + + + CP0001 + T:Rocket.Unturned.Commands.CommandCompass + ./original-repo/Rocket.Unturned/bin/Release/Rocket.Unturned.dll + ./Rocket.Unturned/bin/Release/net461/Rocket.Unturned.dll + + + CP0003 + Rocket.Unturned, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + ./original-repo/Rocket.Unturned/bin/Release/Rocket.Unturned.dll + ./Rocket.Unturned/bin/Release/net461/Rocket.Unturned.dll + + + CP0003 + Rocket.API, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + ./original-repo/Rocket/Rocket.API/bin/Release/Rocket.API.dll + ./Rocket/Rocket.API/bin/Release/net461/Rocket.API.dll + + + CP0003 + Rocket.Core, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + ./original-repo/Rocket/Rocket.Core/bin/Release/Rocket.Core.dll + ./Rocket/Rocket.Core/bin/Release/net461/Rocket.Core.dll + + + CP0021 + M:Rocket.Unturned.Player.UnturnedPlayer.GetComponent``1``0:!:UnityEngine.Component + ./original-repo/Rocket.Unturned/bin/Release/Rocket.Unturned.dll + ./Rocket.Unturned/bin/Release/net461/Rocket.Unturned.dll + + \ No newline at end of file diff --git a/.github/workflows/compatibility-check.yaml b/.github/workflows/compatibility-check.yaml new file mode 100644 index 00000000..ac5fef7e --- /dev/null +++ b/.github/workflows/compatibility-check.yaml @@ -0,0 +1,147 @@ +name: Compatibility-Check + +on: + pull_request: + branches: [ master ] + push: + branches: [ master ] + workflow_dispatch: + schedule: + - cron: "0 3 * * *" # every day at 03:00 UTC + +jobs: + api-compatibility-check: + name: API Compatibility Check + runs-on: windows-latest + + steps: + - name: Checkout current fork + uses: actions/checkout@v7 + + - name: Checkout original repository + uses: actions/checkout@v7 + with: + repository: SmartlyDressedGames/Legally-Distinct-Missile + path: original-repo + + - name: Setup dotnet + uses: actions/setup-dotnet@v6 + with: + dotnet-version: 8.x + + - name: Build current fork (RocketModFix) + run: | + dotnet restore Rocket/Rocket.API/Rocket.API.csproj + dotnet restore Rocket/Rocket.Core/Rocket.Core.csproj + dotnet restore Rocket.Unturned/Rocket.Unturned.csproj + dotnet build Rocket/Rocket.API/Rocket.API.csproj --configuration Release --no-restore + dotnet build Rocket/Rocket.Core/Rocket.Core.csproj --configuration Release --no-restore + dotnet build Rocket.Unturned/Rocket.Unturned.csproj --configuration Release --no-restore + + - name: Build original repository (Legally-Distinct-Missile) + run: | + cd original-repo + dotnet build Rocket/Rocket.API/Rocket.API.csproj --configuration Release --verbosity minimal + dotnet build Rocket/Rocket.Core/Rocket.Core.csproj --configuration Release --verbosity minimal + dotnet build Rocket.Unturned/Rocket.Unturned.csproj --configuration Release --verbosity minimal + + - name: Verify assemblies exist + shell: cmd + run: | + echo "=== Checking if assemblies exist ===" + + REM Define list of required assemblies + set "assemblies=" + set "assemblies=%assemblies%Rocket\Rocket.API\bin\Release\net461\Rocket.API.dll " + set "assemblies=%assemblies%Rocket\Rocket.Core\bin\Release\net461\Rocket.Core.dll " + set "assemblies=%assemblies%Rocket.Unturned\bin\Release\net461\Rocket.Unturned.dll " + set "assemblies=%assemblies%original-repo\Rocket\Rocket.API\bin\Release\Rocket.API.dll " + set "assemblies=%assemblies%original-repo\Rocket\Rocket.Core\bin\Release\Rocket.Core.dll " + set "assemblies=%assemblies%original-repo\Rocket.Unturned\bin\Release\Rocket.Unturned.dll" + + REM Check each assembly + for %%f in (%assemblies%) do ( + if exist "%%f" ( + echo "✅ %%~nxf exists" + ) else ( + echo "❌ %%~nxf not found" + echo "Expected path: %%f" + dir "%%~dpf" 2>nul || echo "Directory does not exist" + exit /b 1 + ) + ) + + - name: Install API compatibility tools + run: dotnet tool install --global Microsoft.DotNet.ApiCompat.Tool + + - name: Generate suppression file for all breaking changes + id: generate + shell: cmd + run: | + echo "Generating suppression file for all breaking changes..." + + apicompat ^ + --left ./original-repo/Rocket/Rocket.API/bin/Release/Rocket.API.dll ^ + --right ./Rocket/Rocket.API/bin/Release/net461/Rocket.API.dll ^ + --left ./original-repo/Rocket/Rocket.Core/bin/Release/Rocket.Core.dll ^ + --right ./Rocket/Rocket.Core/bin/Release/net461/Rocket.Core.dll ^ + --left ./original-repo/Rocket.Unturned/bin/Release/Rocket.Unturned.dll ^ + --right ./Rocket.Unturned/bin/Release/net461/Rocket.Unturned.dll ^ + --generate-suppression-file ^ + --suppression-output-file ./generated-api-suppressions.xml + + - name: Upload generated suppression file + uses: actions/upload-artifact@v7 + with: + name: generated-api-suppressions + path: ./generated-api-suppressions.xml + retention-days: 30 + + - name: Check API compatibility with manual suppressions + shell: pwsh + run: | + Write-Host "Checking API compatibility using manual suppression file..." + + # Run apicompat and capture output directly to variables + $output = & apicompat @( + "--left", "./original-repo/Rocket/Rocket.API/bin/Release/Rocket.API.dll", + "--right", "./Rocket/Rocket.API/bin/Release/net461/Rocket.API.dll", + "--left", "./original-repo/Rocket/Rocket.Core/bin/Release/Rocket.Core.dll", + "--right", "./Rocket/Rocket.Core/bin/Release/net461/Rocket.Core.dll", + "--left", "./original-repo/Rocket.Unturned/bin/Release/Rocket.Unturned.dll", + "--right", "./Rocket.Unturned/bin/Release/net461/Rocket.Unturned.dll", + "--suppression-file", ".github\workflows\api-suppressions.xml" + ) 2>&1 + + $exitCode = $LASTEXITCODE + $fullOutput = $output | Out-String + + # Always show apicompat output + Write-Host "=== apicompat output ===" + Write-Host $fullOutput + Write-Host "=== end of apicompat output ===" + Write-Host "Exit code: $exitCode" + + # Check output content instead of exit code, because for some reason it always returns 0, instead of 1! + if ($fullOutput -match "APICompat ran successfully without finding any breaking changes\.") { + Write-Host "✅ API compatibility check passed!" -ForegroundColor Green + Write-Host "No breaking changes found." + } elseif ($fullOutput -match "API breaking changes found") { + Write-Host "❌ API compatibility check failed!" -ForegroundColor Red + Write-Host "Breaking changes detected that are not covered by your suppression file." + Write-Host "Please either fix breaking change or update .github/workflows/api-suppressions.xml with generated-api-suppressions.xml from artifacts and commit if it's not important breaking change." + exit 1 + } else { + Write-Host "❌ API compatibility check failed!" -ForegroundColor Red + Write-Host "Unexpected output from apicompat. Please check the output above." + exit 1 + } + + workflow-keepalive: + if: github.event_name == 'schedule' + runs-on: ubuntu-latest + permissions: + actions: write + steps: + - uses: liskin/gh-workflow-keepalive@v1 + \ No newline at end of file diff --git a/README.md b/README.md index 290e65fc..3d6c0dca 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ We're not planning to make any breaking changes with API. - [x] Update MSBuild to the `Microsoft.NET.Sdk`, because current MSBuild in RocketMod is outdated and its hard to support and understand what's going on inside. - [x] RocketMod NuGet Package containing all required libraries for RockeMod API usage. - [x] CI/CD and nightly builds with RocketMod .dlls. +- [x] Breaking changes detector ([see #126](https://github.com/RocketModFix/RocketModFix/issues/126)). - [x] Automatic Release on Tag creation (with RocketMod Module). - [x] Rocket.Unturned.Module Artifacts on PR. - [x] Rocket.Unturned NuGet Package. @@ -101,7 +102,9 @@ See here details [how to contribute][contributing]. ### Redist -[![RocketModFix.Unturned.Redist][badge_RocketModFix.Unturned.Redist]][nuget_package_RocketModFix.Unturned.Redist] +[![RocketModFix.Unturned.Redist.Client][badge_RocketModFix.Unturned.Redist.Client]][nuget_package_RocketModFix.Unturned.Redist.Client] + +[![RocketModFix.Unturned.Redist.Server][badge_RocketModFix.Unturned.Redist.Server]][nuget_package_RocketModFix.Unturned.Redist.Server] [![RocketModFix.UnityEngine.Redist][badge_RocketModFix.UnityEngine.Redist]][nuget_package_RocketModFix.UnityEngine.Redist] @@ -140,8 +143,10 @@ On the 2nd of June 2020 fr34kyn01535 requested the fork be rebranded to help dis [openmod_github_repository]: https://github.com/openmod/openmod [ldm_github_repository]: https://github.com/SmartlyDressedGames/Legally-Distinct-Missile -[nuget_package_RocketModFix.Unturned.Redist]: https://www.nuget.org/packages/RocketModFix.Unturned.Redist -[badge_RocketModFix.Unturned.Redist]: https://img.shields.io/nuget/v/RocketModFix.Unturned.Redist?label=RocketModFix.Unturned.Redist&link=https%3A%2F%2Fwww.nuget.org%2Fpackages%2FRocketModFix.Unturned.Redist +[nuget_package_RocketModFix.Unturned.Redist.Client]: https://www.nuget.org/packages/RocketModFix.Unturned.Redist.Client +[badge_RocketModFix.Unturned.Redist.Client]: https://img.shields.io/nuget/v/RocketModFix.Unturned.Redist.Client?label=RocketModFix.Unturned.Redist.Client&link=https%3A%2F%2Fwww.nuget.org%2Fpackages%2FRocketModFix.Unturned.Redist.Client +[nuget_package_RocketModFix.Unturned.Redist.Server]: https://www.nuget.org/packages/RocketModFix.Unturned.Redist.Server +[badge_RocketModFix.Unturned.Redist.Server]: https://img.shields.io/nuget/v/RocketModFix.Unturned.Redist.Server?label=RocketModFix.Unturned.Redist.Server&link=https%3A%2F%2Fwww.nuget.org%2Fpackages%2FRocketModFix.Unturned.Redist.Server [nuget_package_RocketModFix.UnityEngine.Redist]: https://www.nuget.org/packages/RocketModFix.UnityEngine.Redist [badge_RocketModFix.UnityEngine.Redist]: https://img.shields.io/nuget/v/RocketModFix.UnityEngine.Redist?label=RocketModFix.UnityEngine.Redist&link=https%3A%2F%2Fwww.nuget.org%2Fpackages%2FRocketModFix.UnityEngine.Redist [nuget_package_RocketModFix.Rocket.API]: https://www.nuget.org/packages/RocketModFix.Rocket.API @@ -149,4 +154,4 @@ On the 2nd of June 2020 fr34kyn01535 requested the fork be rebranded to help dis [nuget_package_RocketModFix.Rocket.Core]: https://www.nuget.org/packages/RocketModFix.Rocket.Core [badge_RocketModFix.Rocket.Core]: https://img.shields.io/nuget/v/RocketModFix.Rocket.Core?label=RocketModFix.Rocket.Core&link=https%3A%2F%2Fwww.nuget.org%2Fpackages%2FRocketModFix.Rocket.Core [nuget_package_RocketModFix.Rocket.Unturned]: https://www.nuget.org/packages/RocketModFix.Rocket.Unturned -[badge_RocketModFix.Rocket.Unturned]: https://img.shields.io/nuget/v/RocketModFix.Rocket.Unturned?label=RocketModFix.Rocket.Unturned&link=https%3A%2F%2Fwww.nuget.org%2Fpackages%2FRocketModFix.Rocket.Unturned \ No newline at end of file +[badge_RocketModFix.Rocket.Unturned]: https://img.shields.io/nuget/v/RocketModFix.Rocket.Unturned?label=RocketModFix.Rocket.Unturned&link=https%3A%2F%2Fwww.nuget.org%2Fpackages%2FRocketModFix.Rocket.Unturned diff --git a/Rocket.AutoInstaller/Installation/DownloadHelper.cs b/Rocket.AutoInstaller/Installation/DownloadHelper.cs new file mode 100644 index 00000000..c8192299 --- /dev/null +++ b/Rocket.AutoInstaller/Installation/DownloadHelper.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections; +using SDG.Unturned; +using UnityEngine; +using UnityEngine.Networking; + +namespace Rocket.AutoInstaller.Installation +{ + public static class DownloadHelper + { + public static bool IsUri(string path) + { + if (string.IsNullOrWhiteSpace(path)) + return false; + + return Uri.TryCreate(path, UriKind.Absolute, out _); + } + + public static IEnumerator Download( + string url, + Action? onSuccess, + Action? onError, + Config config, + float delaySeconds = 5.0f) + { + if (!IsUri(url)) + { + onError?.Invoke("Invalid URI format."); + yield break; + } + + CommandWindow.Log($"Downloading: {url}"); + + var maxRetries = config.EnableRetry ? 5 : 1; + for (int attempt = 1; attempt <= maxRetries; attempt++) + { + if (attempt > 1) + { + yield return new WaitForSeconds(delaySeconds); + } + + bool downloadSucceeded = false; + yield return DownloadSingleAttempt(url, + data => { + onSuccess?.Invoke(data); + downloadSucceeded = true; + }, + error => { + onError?.Invoke(error); + downloadSucceeded = false; + }); + + if (downloadSucceeded) + { + yield break; + } + } + + CommandWindow.LogError($"Download failed after {maxRetries} attempts: {url}"); + } + + private static IEnumerator DownloadSingleAttempt( + string url, + Action? onSuccess, + Action? onError) + { + var request = UnityWebRequest.Get(url); + request.SetRequestHeader("User-Agent", "RocketModFix"); + request.redirectLimit = 5; + + yield return request.SendWebRequest(); + + if (request.result != UnityWebRequest.Result.Success) + { + var errorMessage = $"Download failed: {request.error} (HTTP {request.responseCode})"; + onError?.Invoke(errorMessage); + throw new Exception(errorMessage); + } + + var data = request.downloadHandler.data; + onSuccess?.Invoke(data); + } + } +} diff --git a/Rocket.AutoInstaller/Installation/ExtrasInstaller.cs b/Rocket.AutoInstaller/Installation/ExtrasInstaller.cs new file mode 100644 index 00000000..3745a37d --- /dev/null +++ b/Rocket.AutoInstaller/Installation/ExtrasInstaller.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections; +using System.IO; +using System.IO.Compression; +using SDG.Unturned; + +namespace Rocket.AutoInstaller.Installation +{ + public static class ExtrasInstaller + { + public static IEnumerator InstallRocketFromExtras() + { + CommandWindow.Log("Installing from Extras..."); + + var extrasPath = Path.Combine(ReadWrite.PATH, "Extras"); + if (!Directory.Exists(extrasPath)) + { + CommandWindow.LogError("Extras directory not found"); + yield break; + } + + var moduleZipPath = Path.Combine(extrasPath, "Rocket.Unturned.Module.zip"); + if (!File.Exists(moduleZipPath)) + { + CommandWindow.LogError("Rocket.Unturned.Module.zip not found in Extras"); + yield break; + } + + CommandWindow.Log($"Found module: {moduleZipPath}"); + + var modulesPath = Path.Combine(ReadWrite.PATH, "Modules"); + var targetPath = Path.Combine(modulesPath, "Rocket.Unturned.Module.zip"); + + try + { + File.Copy(moduleZipPath, targetPath, true); + CommandWindow.Log("Copied module to Modules directory"); + } + catch (Exception ex) + { + CommandWindow.LogError($"Failed to copy module: {ex}"); + yield break; + } + + ExtractModule(targetPath, modulesPath); + } + + private static void ExtractModule(string zipPath, string extractPath) + { + try + { + using var archive = ZipFile.OpenRead(zipPath); + var extractDir = Path.Combine(extractPath, "Rocket.Unturned"); + + if (Directory.Exists(extractDir)) + { + Directory.Delete(extractDir, true); + } + Directory.CreateDirectory(extractDir); + foreach (var entry in archive.Entries) + { + if (string.IsNullOrEmpty(entry.Name)) + continue; + + var entryPath = Path.Combine(extractDir, entry.FullName); + var entryDir = Path.GetDirectoryName(entryPath); + + if (!string.IsNullOrEmpty(entryDir) && !Directory.Exists(entryDir)) + { + Directory.CreateDirectory(entryDir); + } + + if (!string.IsNullOrEmpty(entry.Name)) + { + entry.ExtractToFile(entryPath, true); + } + } + + File.Delete(zipPath); + } + catch (Exception ex) + { + CommandWindow.LogError($"Failed to extract: {ex}"); + } + } + } +} diff --git a/Rocket.AutoInstaller/Installation/GitHubAsset.cs b/Rocket.AutoInstaller/Installation/GitHubAsset.cs index 95d72ddc..1b8239f0 100644 --- a/Rocket.AutoInstaller/Installation/GitHubAsset.cs +++ b/Rocket.AutoInstaller/Installation/GitHubAsset.cs @@ -1,3 +1,4 @@ +using System; using Newtonsoft.Json; namespace Rocket.AutoInstaller.Installation @@ -19,4 +20,31 @@ public class GitHubAsset [JsonProperty("browser_download_url")] public string BrowserDownloadUrl { get; set; } } + + public class GitHubRelease + { + [JsonProperty("url")] + public string Url { get; set; } + + [JsonProperty("tag_name")] + public string TagName { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("body")] + public string Body { get; set; } + + [JsonProperty("draft")] + public bool Draft { get; set; } + + [JsonProperty("prerelease")] + public bool Prerelease { get; set; } + + [JsonProperty("published_at")] + public DateTime PublishedAt { get; set; } + + [JsonProperty("assets")] + public GitHubAsset[] Assets { get; set; } + } } \ No newline at end of file diff --git a/Rocket.AutoInstaller/Installation/GitHubRelease.cs b/Rocket.AutoInstaller/Installation/GitHubRelease.cs deleted file mode 100644 index ca34f496..00000000 --- a/Rocket.AutoInstaller/Installation/GitHubRelease.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using Newtonsoft.Json; - -namespace Rocket.AutoInstaller.Installation -{ - public class GitHubRelease - { - [JsonProperty("url")] - public string Url { get; set; } - - [JsonProperty("tag_name")] - public string TagName { get; set; } - - [JsonProperty("name")] - public string Name { get; set; } - - [JsonProperty("body")] - public string Body { get; set; } - - [JsonProperty("draft")] - public bool Draft { get; set; } - - [JsonProperty("prerelease")] - public bool Prerelease { get; set; } - - [JsonProperty("published_at")] - public DateTime PublishedAt { get; set; } - - [JsonProperty("assets")] - public GitHubAsset[] Assets { get; set; } - } -} \ No newline at end of file diff --git a/Rocket.AutoInstaller/Installation/Installer.cs b/Rocket.AutoInstaller/Installation/Installer.cs index 5dc495aa..e75e4ad5 100644 --- a/Rocket.AutoInstaller/Installation/Installer.cs +++ b/Rocket.AutoInstaller/Installation/Installer.cs @@ -16,8 +16,29 @@ public class Installer { public static IEnumerator Install(Config config) { - var request = UnityWebRequest.Get( - "https://api.github.com/repos/RocketModFix/RocketModFix/releases"); + if (config.EnableCustomInstall == true && !string.IsNullOrWhiteSpace(config.CustomInstallPath)) + { + CommandWindow.Log("Installing from local path..."); + yield return LocalInstaller.Install(config.CustomInstallPath!, config); + yield break; + } + + if (config.AutoInstallRocketFromExtras) + { + CommandWindow.Log("Installing from Extras..."); + yield return ExtrasInstaller.InstallRocketFromExtras(); + yield break; + } + + var modulesDirectory = Path.Combine(ReadWrite.PATH, "Modules"); + var releaseCache = new ReleaseCache(modulesDirectory); + + yield return FetchAndInstallLatestRelease(releaseCache, config); + } + + private static IEnumerator FetchAndInstallLatestRelease(ReleaseCache releaseCache, Config config) + { + var request = UnityWebRequest.Get("https://api.github.com/repos/RocketModFix/RocketModFix/releases"); request.SetRequestHeader("User-Agent", "RocketModFix"); request.redirectLimit = 5; @@ -25,8 +46,36 @@ public static IEnumerator Install(Config config) if (request.result != UnityWebRequest.Result.Success) { - CommandWindow.LogError($"Installation Failed: An error occured while installing RocketModFix, response code: {request.responseCode}, error: {request.error}"); - yield break; + var errorMessage = $"Failed to fetch releases: {request.error} (Status: {request.responseCode})"; + CommandWindow.LogError(errorMessage); + + if (config.EnableCaching) + { + var cachedEntry = releaseCache.GetCachedEntry(); + if (cachedEntry != null && releaseCache.IsFileCached(cachedEntry.TagName)) + { + CommandWindow.LogWarning("Using cached version due to network error"); + var cachedData = releaseCache.LoadCachedFile(cachedEntry.TagName); + if (cachedData != null) + { + var mockRelease = new GitHubRelease + { + TagName = cachedEntry.TagName, + Name = cachedEntry.Name, + PublishedAt = cachedEntry.PublishedAt + }; + var mockAsset = new GitHubAsset + { + BrowserDownloadUrl = cachedEntry.DownloadUrl, + Size = cachedEntry.FileSize + }; + yield return ProcessModuleData(cachedData, mockRelease, mockAsset, releaseCache); + yield break; + } + } + } + + throw new Exception(errorMessage); } var responseContent = request.downloadHandler.text; @@ -34,36 +83,71 @@ public static IEnumerator Install(Config config) var latestRelease = releases!.FirstOrDefault(); if (latestRelease == null) { - CommandWindow.LogError("Installation Failed: No Release Found."); - yield break; + CommandWindow.LogError("No release found"); + throw new Exception("No releases found"); } if (string.IsNullOrWhiteSpace(latestRelease.TagName)) { - CommandWindow.LogError("Installation Failed: Release Tag Name doesn't seems to be valid."); - yield break; + CommandWindow.LogError("Invalid release tag"); + throw new Exception("Invalid release tag name"); } var moduleAsset = latestRelease.Assets.FirstOrDefault(IsRocketModFixModule); if (moduleAsset == null) { - CommandWindow.LogError("Installation Failed: Module not found."); + CommandWindow.LogError("Module not found"); + throw new Exception("Module asset not found"); + } + + if (config.EnableCaching && releaseCache.IsFileCached(latestRelease.TagName)) + { + CommandWindow.Log($"Using cached version: {latestRelease.TagName}"); + var cachedData = releaseCache.LoadCachedFile(latestRelease.TagName); + if (cachedData != null) + { + yield return ProcessModuleData(cachedData, latestRelease, moduleAsset, releaseCache); + yield break; + } + } + + if (!releaseCache.IsNewerReleaseAvailable(latestRelease.TagName, latestRelease.PublishedAt)) + { + var cachedEntry = releaseCache.GetCachedEntry(); + if (cachedEntry != null) + { + CommandWindow.Log($"Already up to date: {cachedEntry.TagName}"); + } + else + { + CommandWindow.Log($"Already up to date: {latestRelease.TagName}"); + } yield break; } - CommandWindow.LogWarning($"Preparing to install: " + - $"RocketModFix {latestRelease.TagName} Released: {latestRelease.PublishedAt}"); + CommandWindow.LogWarning($"Installing RocketModFix {latestRelease.TagName} ({latestRelease.PublishedAt:yyyy-MM-dd})"); - request = UnityWebRequest.Get(moduleAsset.BrowserDownloadUrl); - request.SetRequestHeader("User-Agent", "RocketModFix"); - request.redirectLimit = 5; - yield return request.SendWebRequest(); + byte[]? rawData = null; + yield return DownloadHelper.Download( + moduleAsset.BrowserDownloadUrl, + data => rawData = data, + error => throw new Exception(error), + config, + delaySeconds: 5.0f); - if (request.result != UnityWebRequest.Result.Success) + if (rawData == null) { - CommandWindow.LogError($"Installation Failed: An error occured while getting raw data of module {moduleAsset.Name}, response code: {request.responseCode}, error: {request.error}"); - yield break; + throw new Exception("Failed to download module data"); + } + + if (config.EnableCaching) + { + releaseCache.SaveCachedFile(latestRelease.TagName, rawData); } - var rawData = request.downloadHandler.data; + yield return ProcessModuleData(rawData, latestRelease, moduleAsset, releaseCache); + } + + private static IEnumerator ProcessModuleData(byte[] rawData, GitHubRelease latestRelease, GitHubAsset moduleAsset, ReleaseCache releaseCache) + { var releaseEntries = GetReleaseEntries(rawData); byte[]? rocketModuleData = null; List rocketLibraries = []; @@ -84,7 +168,7 @@ public static IEnumerator Install(Config config) if (rocketModuleData == null) { - CommandWindow.LogError($"Installation Failed: {rocketEntryPointFileName} Rocket Entry Point was not found."); + CommandWindow.LogError($"{rocketEntryPointFileName} not found"); yield break; } @@ -96,11 +180,10 @@ public static IEnumerator Install(Config config) var rocketModule = Assembly.Load(rocketModuleData); var types = GetLoadableTypes(rocketModule); - var moduleType = types.FirstOrDefault( - x => x.IsAbstract == false && typeof(IModuleNexus).IsAssignableFrom(x)); + var moduleType = types.FirstOrDefault(x => !x.IsAbstract && typeof(IModuleNexus).IsAssignableFrom(x)); if (moduleType == null) { - CommandWindow.LogError("Installation Failed: Rocket Module Type cannot be found!"); + CommandWindow.LogError("Rocket module type not found"); yield break; } @@ -108,7 +191,7 @@ public static IEnumerator Install(Config config) { if (Activator.CreateInstance(moduleType) is not IModuleNexus plugin) { - CommandWindow.LogError("Unable to create UnturnedGuard Module!"); + CommandWindow.LogError("Failed to create module"); yield break; } @@ -116,18 +199,28 @@ public static IEnumerator Install(Config config) } catch (Exception ex) { - CommandWindow.LogError("An error occured while creating UnturnedGuard Module! \n" + ex); - yield break; + CommandWindow.LogError($"Error creating module: {ex}"); + throw ex; } - CommandWindow.LogWarning( - $"Successfully installed: RocketModFix v{latestRelease.TagName}"); + var cacheEntry = new CacheEntry + { + TagName = latestRelease.TagName, + Name = latestRelease.Name, + PublishedAt = latestRelease.PublishedAt, + DownloadUrl = moduleAsset.BrowserDownloadUrl, + FileSize = moduleAsset.Size + }; + releaseCache.SaveCacheEntry(cacheEntry); + + CommandWindow.LogWarning($"Installed RocketModFix v{latestRelease.TagName}"); } private static bool IsRocketModFixModule(GitHubAsset asset) { return asset.Name.Equals("Rocket.Unturned.Module.zip", StringComparison.Ordinal); } + private static List GetReleaseEntries(byte[] assetData) { var entries = new List(); @@ -149,7 +242,7 @@ private static List GetReleaseEntries(byte[] assetData) } catch (Exception ex) { - CommandWindow.LogError($"Error while collecting info about release entry: {entry.FullName} \n{ex}"); + CommandWindow.LogError($"Error reading {entry.FullName}: {ex}"); } } return entries; @@ -180,7 +273,7 @@ private static IEnumerable GetLoadableTypes(Assembly assembly) } catch (ReflectionTypeLoadException ex) { - CommandWindow.LogError($"An error occured while getting types for assembly: {assembly} \n" + ex); + CommandWindow.LogError($"Error getting types from {assembly}: {ex}"); return ex.Types.Where(x => x is not null); } } diff --git a/Rocket.AutoInstaller/Installation/LocalInstaller.cs b/Rocket.AutoInstaller/Installation/LocalInstaller.cs new file mode 100644 index 00000000..e728a123 --- /dev/null +++ b/Rocket.AutoInstaller/Installation/LocalInstaller.cs @@ -0,0 +1,240 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Reflection; +using SDG.Framework.Modules; +using SDG.Unturned; + +namespace Rocket.AutoInstaller.Installation +{ + public class LocalInstaller + { + public static IEnumerator Install(string path, Config config) + { + if (!PathValidator.ValidatePath(path, out var errorMessage)) + { + CommandWindow.LogError($"Local install failed: {errorMessage}"); + yield break; + } + + CommandWindow.Log($"Installing from: {path}"); + + List releaseEntries = []; + + if (DownloadHelper.IsUri(path)) + { + yield return DownloadAndProcessGenericUri(path, entries => releaseEntries = entries, config); + } + else + { + try + { + if (File.Exists(path) && path.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) + { + var zipData = File.ReadAllBytes(path); + releaseEntries = GetReleaseEntries(zipData); + } + else + { + releaseEntries = GetReleaseEntriesFromDirectory(path); + } + } + catch (Exception ex) + { + CommandWindow.LogError($"Failed to read module zip: {ex}"); + yield break; + } + } + + byte[]? rocketModuleData = null; + List rocketLibraries = []; + const string rocketEntryPointFileName = "Rocket.Unturned.dll"; + + foreach (var releaseEntry in releaseEntries) + { + if (releaseEntry.Name == rocketEntryPointFileName) + { + rocketModuleData = releaseEntry.Content; + continue; + } + if (releaseEntry.Name != rocketEntryPointFileName && releaseEntry.FileExtension == ".dll") + { + rocketLibraries.Add(releaseEntry.Content); + continue; + } + } + + if (rocketModuleData == null) + { + CommandWindow.LogError($"{rocketEntryPointFileName} not found in zip"); + yield break; + } + + foreach (var rocketLibrary in rocketLibraries) + { + try + { + Assembly.Load(rocketLibrary); + } + catch (Exception ex) + { + CommandWindow.LogWarning($"Failed to load library: {ex}"); + } + } + + var rocketModule = Assembly.Load(rocketModuleData); + var types = GetLoadableTypes(rocketModule); + var moduleType = types.FirstOrDefault(x => !x.IsAbstract && typeof(IModuleNexus).IsAssignableFrom(x)); + + if (moduleType == null) + { + CommandWindow.LogError("Rocket module type not found"); + yield break; + } + + IModuleNexus plugin; + try + { + if (Activator.CreateInstance(moduleType) is not IModuleNexus createdPlugin) + { + CommandWindow.LogError("Failed to create rocket module"); + yield break; + } + plugin = createdPlugin; + } + catch (Exception ex) + { + CommandWindow.LogError($"Error creating rocket module: {ex}"); + yield break; + } + + plugin.initialize(); + CommandWindow.LogWarning($"Installed from local build: {path}"); + } + + private static List GetReleaseEntries(byte[] assetData) + { + var entries = new List(); + using var memoryStream = new MemoryStream(assetData); + using var zipArchive = new ZipArchive(memoryStream); + foreach (var entry in zipArchive.Entries) + { + try + { + var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(entry.FullName); + var fileExtension = Path.GetExtension(entry.FullName); + var entryDirectoryName = Path.GetDirectoryName(entry.FullName)!; + + using var stream = entry.Open(); + var entryData = stream.CopyToArray(); + + entries.Add(new ReleaseEntry(entry.Name, entry.FullName, fileNameWithoutExtension, fileExtension, + entryDirectoryName, entryData)); + } + catch (Exception ex) + { + CommandWindow.LogError($"Error reading {entry.FullName}: {ex}"); + } + } + return entries; + } + + private static List GetReleaseEntriesFromDirectory(string directoryPath) + { + var entries = new List(); + + var allFiles = Directory.GetFiles(directoryPath, "*", SearchOption.AllDirectories); + + foreach (var filePath in allFiles) + { + try + { + var relativePath = GetRelativePath(directoryPath, filePath); + var fileName = Path.GetFileName(filePath); + var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(filePath); + var fileExtension = Path.GetExtension(filePath); + var entryDirectoryName = Path.GetDirectoryName(relativePath) ?? string.Empty; + + var fileData = File.ReadAllBytes(filePath); + + entries.Add(new ReleaseEntry(fileName, relativePath, fileNameWithoutExtension, fileExtension, + entryDirectoryName, fileData)); + } + catch (Exception ex) + { + CommandWindow.LogError($"Error reading {filePath}: {ex}"); + } + } + + return entries; + } + + private static IEnumerator DownloadAndProcessGenericUri(string uri, Action> onComplete, Config config) + { + var entries = new List(); + var downloadSucceeded = false; + var downloadError = string.Empty; + + yield return DownloadHelper.Download(uri, + data => { + entries = GetReleaseEntries(data); + downloadSucceeded = true; + }, + error => { + downloadError = error; + downloadSucceeded = false; + }, + config); + + if (!downloadSucceeded) + { + CommandWindow.LogError($"URI download failed: {downloadError}"); + yield break; + } + + onComplete?.Invoke(entries); + } + + private static string GetRelativePath(string basePath, string targetPath) + { + var baseUri = new Uri(basePath.EndsWith(Path.DirectorySeparatorChar.ToString()) ? basePath : basePath + Path.DirectorySeparatorChar); + var targetUri = new Uri(targetPath); + var relativeUri = baseUri.MakeRelativeUri(targetUri); + return Uri.UnescapeDataString(relativeUri.ToString().Replace('/', Path.DirectorySeparatorChar)); + } + + /// + /// Safely returns the set of loadable types from an assembly. + /// Algorithm from StackOverflow answer here: + /// https://stackoverflow.com/questions/7889228/how-to-prevent-reflectiontypeloadexception-when-calling-assembly-gettypes + /// + /// The from which to load types. + /// + /// The set of types from the , or the subset + /// of types that could be loaded if there was any error. + /// + /// + /// Thrown if is . + /// + private static IEnumerable GetLoadableTypes(Assembly assembly) + { + if (assembly == null) + { + throw new ArgumentNullException(nameof(assembly)); + } + + try + { + return assembly.DefinedTypes.Select(x => x.AsType()); + } + catch (ReflectionTypeLoadException ex) + { + CommandWindow.LogError($"Error getting types from {assembly}: {ex}"); + return ex.Types.Where(x => x is not null); + } + } + } +} diff --git a/Rocket.AutoInstaller/Installation/PathValidator.cs b/Rocket.AutoInstaller/Installation/PathValidator.cs new file mode 100644 index 00000000..ff0a0479 --- /dev/null +++ b/Rocket.AutoInstaller/Installation/PathValidator.cs @@ -0,0 +1,128 @@ +using System; +using System.IO; +using SDG.Unturned; + +namespace Rocket.AutoInstaller.Installation +{ + public static class PathValidator + { + public static bool ValidatePath(string path, out string errorMessage) + { + errorMessage = string.Empty; + + if (string.IsNullOrWhiteSpace(path)) + { + errorMessage = "Path is null or empty."; + return false; + } + + if (DownloadHelper.IsUri(path)) + { + return ValidateUri(path, out errorMessage); + } + + if (File.Exists(path) && path.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) + { + return ValidateZipFile(path, out errorMessage); + } + + if (Directory.Exists(path)) + { + return ValidateDirectory(path, out errorMessage); + } + + errorMessage = $"Path does not exist (neither file, directory, nor valid URL): {path}"; + return false; + } + + private static bool ValidateUri(string uri, out string errorMessage) + { + errorMessage = string.Empty; + + if (!Uri.TryCreate(uri, UriKind.Absolute, out var parsedUri)) + { + errorMessage = "Invalid URI format."; + return false; + } + + if (parsedUri.Scheme != "http" && parsedUri.Scheme != "https") + { + errorMessage = "Only HTTP and HTTPS URIs are supported."; + return false; + } + + // Note: We can't validate the actual content without making a network request + // This validation will be done during the download process + return true; + } + + private static bool ValidateZipFile(string zipPath, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + var fileInfo = new FileInfo(zipPath); + if (fileInfo.Length == 0) + { + errorMessage = "Zip file is empty."; + return false; + } + + using var zipArchive = System.IO.Compression.ZipFile.OpenRead(zipPath); + var hasRocketDll = false; + foreach (var entry in zipArchive.Entries) + { + if (entry.Name.Equals("Rocket.Unturned.dll", StringComparison.OrdinalIgnoreCase)) + { + hasRocketDll = true; + break; + } + } + + if (!hasRocketDll) + { + errorMessage = "Rocket.Unturned.dll not found in the zip file."; + return false; + } + } + catch (Exception ex) + { + errorMessage = $"Invalid or corrupted zip file: {ex}"; + return false; + } + + return true; + } + + private static bool ValidateDirectory(string directoryPath, out string errorMessage) + { + errorMessage = string.Empty; + + var rocketDllPath = Path.Combine(directoryPath, "Rocket.Unturned.dll"); + if (File.Exists(rocketDllPath)) + { + return true; + } + + var moduleZipPath = Path.Combine(directoryPath, "Rocket.Unturned.Module.zip"); + if (File.Exists(moduleZipPath)) + { + return ValidateZipFile(moduleZipPath, out errorMessage); + } + + var subdirectories = Directory.GetDirectories(directoryPath, "*", SearchOption.AllDirectories); + foreach (var subdir in subdirectories) + { + var subdirRocketDll = Path.Combine(subdir, "Rocket.Unturned.dll"); + if (File.Exists(subdirRocketDll)) + { + return true; + } + } + + errorMessage = $"Neither Rocket.Unturned.dll nor Rocket.Unturned.Module.zip found in: {directoryPath}"; + return false; + } + } +} diff --git a/Rocket.AutoInstaller/Installation/ReleaseCache.cs b/Rocket.AutoInstaller/Installation/ReleaseCache.cs new file mode 100644 index 00000000..a7b74752 --- /dev/null +++ b/Rocket.AutoInstaller/Installation/ReleaseCache.cs @@ -0,0 +1,211 @@ +using System; +using System.IO; +using System.Linq; +using Newtonsoft.Json; +using SDG.Unturned; + +namespace Rocket.AutoInstaller.Installation +{ + public class ReleaseCache + { + private const string CacheFileName = "rocket_cache.json"; + private const string CacheDirectoryName = "cache"; + private readonly string _cacheFilePath; + private readonly string _modulesDirectory; + private readonly string _cacheDirectory; + + public ReleaseCache(string modulesDirectory) + { + _modulesDirectory = modulesDirectory; + + var autoInstallerDirectory = FindAutoInstallerDirectory(modulesDirectory); + if (autoInstallerDirectory == null) + { + throw new Exception("Rocket.AutoInstaller module directory not found"); + } + + _cacheFilePath = Path.Combine(autoInstallerDirectory, CacheFileName); + _cacheDirectory = Path.Combine(autoInstallerDirectory, CacheDirectoryName); + + if (!Directory.Exists(_cacheDirectory)) + { + Directory.CreateDirectory(_cacheDirectory); + } + } + + private static string? FindAutoInstallerDirectory(string modulesDirectory) + { + try + { + var autoInstallerFiles = Directory.GetFiles(modulesDirectory, "Rocket.AutoInstaller.dll", SearchOption.AllDirectories); + if (autoInstallerFiles.Length > 0) + { + return Path.GetDirectoryName(autoInstallerFiles[0]); + } + } + catch (Exception ex) + { + CommandWindow.LogWarning($"Failed to find Rocket.AutoInstaller directory: {ex}"); + } + return null; + } + + public CacheEntry? GetCachedEntry() + { + if (!File.Exists(_cacheFilePath)) + return null; + + try + { + var cacheContent = File.ReadAllText(_cacheFilePath); + var cache = JsonConvert.DeserializeObject(cacheContent); + return cache; + } + catch (Exception ex) + { + CommandWindow.LogWarning($"Cache read failed: {ex}"); + return null; + } + } + + public void SaveCacheEntry(CacheEntry entry) + { + try + { + var cacheContent = JsonConvert.SerializeObject(entry, Formatting.Indented); + File.WriteAllText(_cacheFilePath, cacheContent); + CommandWindow.Log($"Cached: {entry.TagName} ({entry.PublishedAt:yyyy-MM-dd})"); + } + catch (Exception ex) + { + CommandWindow.LogWarning($"Cache save failed: {ex}"); + } + } + + public bool IsNewerReleaseAvailable(string latestTagName, DateTime latestPublishedAt) + { + var cachedEntry = GetCachedEntry(); + if (cachedEntry == null) + { + CommandWindow.Log("No cached release found"); + return true; + } + if (latestPublishedAt > cachedEntry.PublishedAt) + { + CommandWindow.Log($"Newer release: {latestTagName} vs {cachedEntry.TagName}"); + return true; + } + + if (latestPublishedAt == cachedEntry.PublishedAt && latestTagName != cachedEntry.TagName) + { + CommandWindow.Log($"Different tag: {latestTagName} vs {cachedEntry.TagName}"); + return true; + } + + CommandWindow.Log($"No newer release: {cachedEntry.TagName}"); + return false; + } + + public void ClearCache() + { + try + { + if (File.Exists(_cacheFilePath)) + { + File.Delete(_cacheFilePath); + } + ClearCachedFiles(); + } + catch (Exception ex) + { + CommandWindow.LogWarning($"Cache clear failed: {ex}"); + } + } + + public bool IsRocketInstalled() + { + var rocketFiles = Directory.GetFiles(_modulesDirectory, "Rocket.Unturned.dll", SearchOption.AllDirectories); + return rocketFiles.Any(); + } + + public string GetCachedFilePath(string tagName) + { + var fileName = $"Rocket.Unturned.Module.{tagName}.zip"; + return Path.Combine(_cacheDirectory, fileName); + } + + public bool IsFileCached(string tagName) + { + var cachedFilePath = GetCachedFilePath(tagName); + return File.Exists(cachedFilePath); + } + + public void SaveCachedFile(string tagName, byte[] fileData) + { + try + { + var cachedFilePath = GetCachedFilePath(tagName); + File.WriteAllBytes(cachedFilePath, fileData); + CommandWindow.Log($"Cached file: {Path.GetFileName(cachedFilePath)}"); + } + catch (Exception ex) + { + CommandWindow.LogWarning($"Failed to cache file: {ex}"); + } + } + + public byte[]? LoadCachedFile(string tagName) + { + try + { + var cachedFilePath = GetCachedFilePath(tagName); + if (File.Exists(cachedFilePath)) + { + var fileData = File.ReadAllBytes(cachedFilePath); + CommandWindow.Log($"Loaded from cache: {Path.GetFileName(cachedFilePath)}"); + return fileData; + } + } + catch (Exception ex) + { + CommandWindow.LogWarning($"Failed to load cached file: {ex}"); + } + return null; + } + + public void ClearCachedFiles() + { + try + { + if (Directory.Exists(_cacheDirectory)) + { + var files = Directory.GetFiles(_cacheDirectory, "*.zip"); + foreach (var file in files) + { + File.Delete(file); + } + CommandWindow.Log($"Cleared {files.Length} cached files"); + } + } + catch (Exception ex) + { + CommandWindow.LogWarning($"Failed to clear cached files: {ex}"); + } + } + } + + public class CacheEntry + { + public string TagName { get; set; } + public string Name { get; set; } + public DateTime PublishedAt { get; set; } + public string DownloadUrl { get; set; } + public int FileSize { get; set; } + public DateTime CachedAt { get; set; } + + public CacheEntry() + { + CachedAt = DateTime.UtcNow; + } + } +} diff --git a/Rocket.AutoInstaller/Module.cs b/Rocket.AutoInstaller/Module.cs index f3707bee..68e467aa 100644 --- a/Rocket.AutoInstaller/Module.cs +++ b/Rocket.AutoInstaller/Module.cs @@ -16,6 +16,9 @@ public class Config public bool? EnableCustomInstall { get; set; } public string? CustomInstallPath { get; set; } public bool BlockIfRocketInstalled { get; set; } + public bool AutoInstallRocketFromExtras { get; set; } + public bool EnableRetry { get; set; } + public bool EnableCaching { get; set; } } /// @@ -33,7 +36,7 @@ public void initialize() var assembly = typeof(Module).Assembly; var assemblyName = assembly.GetName(); - CommandWindow.Log($"Loading Rocket.AutoInstaller... {assemblyName.Name} {assemblyName.Version} this could take for a while!"); + CommandWindow.Log($"Loading {assemblyName.Name} {assemblyName.Version}..."); CommandWindow.Log($"Discord Support: {DiscordSupportURL}"); var modulesDirectory = Path.Combine(ReadWrite.PATH, "Modules"); @@ -55,6 +58,9 @@ public void initialize() EnableCustomInstall = false, CustomInstallPath = null, BlockIfRocketInstalled = true, + AutoInstallRocketFromExtras = false, + EnableRetry = true, + EnableCaching = true, }; File.WriteAllText(configPath, JsonConvert.SerializeObject(config, Formatting.Indented)); } @@ -62,20 +68,28 @@ public void initialize() if (config.BlockIfRocketInstalled) { const string rocketUnturnedDll = "Rocket.Unturned.dll"; - // TODO: Once we do a caching make sure to ignore the own directory in Module code, cuz it will block the load process (see `BlockIfRocketInstalled`)! - var rocketPath = Path.GetDirectoryName(Directory - .GetFiles(modulesDirectory, rocketUnturnedDll, SearchOption.AllDirectories) - .FirstOrDefault()); + var rocketFiles = Directory.GetFiles(modulesDirectory, rocketUnturnedDll, SearchOption.AllDirectories); + var rocketPath = rocketFiles.FirstOrDefault(); + if (rocketPath != null) { - CommandWindow.Log($"Rocket.Unturned is already installed in the Modules directory: \"{rocketPath}\". Please remove it to prevent potential issues."); - if (config.BlockIfRocketInstalled) + var rocketDirectory = Path.GetDirectoryName(rocketPath); + + if (string.Equals(rocketDirectory, workingDirectory, StringComparison.OrdinalIgnoreCase)) + { + CommandWindow.Log("Ignoring self-directory to prevent blocking"); + } + else { - CommandWindow.Log( - "Installation via Rocket.AutoInstaller has been stopped because Rocket is already installed. " + - "To proceed, either delete Rocket.Unturned from the Modules directory or set `BlockIfRocketInstalled` to `false` in the configuration." - ); - return; + CommandWindow.Log($"Rocket already installed: {rocketDirectory}"); + if (config.BlockIfRocketInstalled) + { + CommandWindow.Log( + "Installation via Rocket.AutoInstaller has been stopped because Rocket is already installed. " + + "To proceed, either delete Rocket.Unturned from the Modules directory or set `BlockIfRocketInstalled` to `false` in the configuration." + ); + return; + } } } } diff --git a/Rocket.AutoInstaller/Module/RuntimeLibs/System.IO.Compression.FileSystem.dll b/Rocket.AutoInstaller/Module/RuntimeLibs/System.IO.Compression.FileSystem.dll index 59e95a23..50b74f81 100644 Binary files a/Rocket.AutoInstaller/Module/RuntimeLibs/System.IO.Compression.FileSystem.dll and b/Rocket.AutoInstaller/Module/RuntimeLibs/System.IO.Compression.FileSystem.dll differ diff --git a/Rocket.AutoInstaller/Module/RuntimeLibs/System.IO.Compression.dll b/Rocket.AutoInstaller/Module/RuntimeLibs/System.IO.Compression.dll index 751ef2a6..835c025a 100644 Binary files a/Rocket.AutoInstaller/Module/RuntimeLibs/System.IO.Compression.dll and b/Rocket.AutoInstaller/Module/RuntimeLibs/System.IO.Compression.dll differ diff --git a/Rocket.AutoInstaller/README.md b/Rocket.AutoInstaller/README.md index 3f77d29c..8232587b 100644 --- a/Rocket.AutoInstaller/README.md +++ b/Rocket.AutoInstaller/README.md @@ -1,19 +1,79 @@ # Rocket.AutoInstaller -It's a Module auto-installer (loader/bootstrapper), which installs RocketModFix automatically from GitHub, you can still use a manual way of installation, however, this one is more modern, simple and faster. +Auto-installer for RocketModFix. Downloads and installs the latest version from GitHub, or installs from local builds for development. + +## Quick Start + +Just drop the module in your `Modules` folder and start your server. It will automatically download and install the latest RocketModFix. + +**Important:** Before using the auto-installer, remove any manually installed `Rocket.Unturned` from your `Modules` directory to avoid conflicts. + +## Configuration + +Edit `config.json` to customize installation behavior: + +```json +{ + "EnableCustomInstall": false, + "CustomInstallPath": "", + "BlockIfRocketInstalled": true, + "AutoInstallRocketFromExtras": false, + "EnableRetry": true, + "EnableCaching": true +} +``` + +### Options + +**For regular users:** +- `BlockIfRocketInstalled` - Prevent installation if Rocket is already installed +- `EnableRetry` - Enable retry mechanism for failed downloads (5 attempts, 5s delay) +- `EnableCaching` - Enable file caching for faster startup and offline support + +**For developers:** +- `EnableCustomInstall` - Enable local installation instead of GitHub +- `CustomInstallPath` - Path to your local build (see examples below) +- `AutoInstallRocketFromExtras` - Auto-install from Extras folder + +### Local Installation Examples + +For developers who want to test their own builds without manually copying files every time. The `CustomInstallPath` can point to: + +1. A zip file - Direct path to Rocket.Unturned.Module.zip + ``` + "CustomInstallPath": "C:\\Builds\\Rocket.Unturned.Module.zip" + ``` + +2. An unzipped folder - Directory containing the module files + ``` + "CustomInstallPath": "C:\\Builds\\RocketModFix\\Rocket.Unturned" + ``` + + The folder should contain either: + - Rocket.Unturned.dll directly, or + - Rocket.Unturned.Module.zip file, or + - Rocket.Unturned.dll in any subdirectory + +3. Direct URI - Any HTTP/HTTPS URL pointing to a zip file + ``` + "CustomInstallPath": "https://example.com/downloads/Rocket.Unturned.Module.zip" + ``` + + **Note:** GitHub Actions artifacts are currently not supported cause require authentication and aren't suitable for public downloads. + See [GitHub issue #51](https://github.com/actions/upload-artifact/issues/51) for details. ## Plans -If you want to implement one of the plans or have better ideas or ideas to change, feel free to know us about that! - -- [x] Auto-Install from GitHub Releases. -- [ ] Auto-Install Local Build (so, no need to manually install RocketModFix everytime you test/update it, 1 click to build, restart server, and you're testing!). - - [x] Json (or other type of config) Config with Options: - - [x] `EnableCustomInstall`: false/true (default is false). - - [x] `CustomInstallPath`: path to a build of RocketModFix. - - [ ] (Not 100% yet) We can also an option in config (`AutoInstallRocketFromExtras`) to make auto installation of Rocket (LDM) from Extras. - - [ ] Once we do a caching make sure to ignore the own directory in Module code, cuz it will block the load process (see `BlockIfRocketInstalled`)! -- [ ] Caching. - - [ ] Check if GitHub release is newer than current cached version and ONLY then install new version, also use Retry (5 seconds, 5 attempts or so) in case of GitHub's down or problems with internet. - - [ ] For safe usage without Internet Connection. -- [x] ([Details](https://github.com/RocketModFix/RocketModFix/issues/119)) Block installation if Rocket already installed (`BlockIfRocketInstalled` in config). \ No newline at end of file +If you want to implement one of the plans or have better ideas, feel free to let us know! + +- [x] Auto-Install from GitHub Releases +- [x] Auto-Install Local Build (no need to manually install RocketModFix every time you test/update it, 1 click to build, restart server, and you're testing!) + - [x] Json config with options: + - [x] `EnableCustomInstall`: false/true (default is false) + - [x] `CustomInstallPath`: path to a build of RocketModFix + - [x] `AutoInstallRocketFromExtras`: false/true (default is false) - Auto-install Rocket (LDM) from Extras + - [x] Fixed self-directory exclusion in Module code to prevent blocking the load process (see `BlockIfRocketInstalled`)! +- [x] Caching + - [x] Check if GitHub release is newer than current cached version and ONLY then install new version, also use Retry (5 seconds, 5 attempts or so) in case of GitHub's down or problems with internet + - [x] For safe usage without Internet Connection +- [x] ([Details](https://github.com/RocketModFix/RocketModFix/issues/119)) Block installation if Rocket already installed (`BlockIfRocketInstalled` in config) \ No newline at end of file diff --git a/Rocket.AutoInstaller/Rocket.AutoInstaller.csproj b/Rocket.AutoInstaller/Rocket.AutoInstaller.csproj index 10902fba..7e310061 100644 --- a/Rocket.AutoInstaller/Rocket.AutoInstaller.csproj +++ b/Rocket.AutoInstaller/Rocket.AutoInstaller.csproj @@ -12,21 +12,15 @@ - + All - + All - - - Module\RuntimeLibs\System.IO.Compression.dll - - - Always diff --git a/Rocket.AutoInstaller/config.json b/Rocket.AutoInstaller/config.json index 961e1cc7..ed6995db 100644 --- a/Rocket.AutoInstaller/config.json +++ b/Rocket.AutoInstaller/config.json @@ -1,5 +1,8 @@ { "EnableCustomInstall": false, "CustomInstallPath": "", - "BlockIfRocketInstalled": true + "BlockIfRocketInstalled": true, + "AutoInstallRocketFromExtras": false, + "EnableRetry": true, + "EnableCaching": true } \ No newline at end of file diff --git a/Rocket.Unturned.sln b/Rocket.Unturned.sln index 7f7d2278..dcce1a2e 100644 --- a/Rocket.Unturned.sln +++ b/Rocket.Unturned.sln @@ -16,6 +16,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rocket.Core", "Rocket\Rocke EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rocket.API", "Rocket\Rocket.API\Rocket.API.csproj", "{FB0FDFB9-D500-4E0F-9467-CA404E64C1E0}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rocket.AutoInstaller", "Rocket.AutoInstaller\Rocket.AutoInstaller.csproj", "{F7B14393-5418-4D51-B342-5EC2A51FC62B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -101,6 +103,24 @@ Global {FB0FDFB9-D500-4E0F-9467-CA404E64C1E0}.Release|x64.Build.0 = Release|Any CPU {FB0FDFB9-D500-4E0F-9467-CA404E64C1E0}.Release|x86.ActiveCfg = Release|Any CPU {FB0FDFB9-D500-4E0F-9467-CA404E64C1E0}.Release|x86.Build.0 = Release|Any CPU + {F7B14393-5418-4D51-B342-5EC2A51FC62B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F7B14393-5418-4D51-B342-5EC2A51FC62B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F7B14393-5418-4D51-B342-5EC2A51FC62B}.Debug|x64.ActiveCfg = Debug|Any CPU + {F7B14393-5418-4D51-B342-5EC2A51FC62B}.Debug|x64.Build.0 = Debug|Any CPU + {F7B14393-5418-4D51-B342-5EC2A51FC62B}.Debug|x86.ActiveCfg = Debug|Any CPU + {F7B14393-5418-4D51-B342-5EC2A51FC62B}.Debug|x86.Build.0 = Debug|Any CPU + {F7B14393-5418-4D51-B342-5EC2A51FC62B}.Local|Any CPU.ActiveCfg = Debug|Any CPU + {F7B14393-5418-4D51-B342-5EC2A51FC62B}.Local|Any CPU.Build.0 = Debug|Any CPU + {F7B14393-5418-4D51-B342-5EC2A51FC62B}.Local|x64.ActiveCfg = Debug|Any CPU + {F7B14393-5418-4D51-B342-5EC2A51FC62B}.Local|x64.Build.0 = Debug|Any CPU + {F7B14393-5418-4D51-B342-5EC2A51FC62B}.Local|x86.ActiveCfg = Debug|Any CPU + {F7B14393-5418-4D51-B342-5EC2A51FC62B}.Local|x86.Build.0 = Debug|Any CPU + {F7B14393-5418-4D51-B342-5EC2A51FC62B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F7B14393-5418-4D51-B342-5EC2A51FC62B}.Release|Any CPU.Build.0 = Release|Any CPU + {F7B14393-5418-4D51-B342-5EC2A51FC62B}.Release|x64.ActiveCfg = Release|Any CPU + {F7B14393-5418-4D51-B342-5EC2A51FC62B}.Release|x64.Build.0 = Release|Any CPU + {F7B14393-5418-4D51-B342-5EC2A51FC62B}.Release|x86.ActiveCfg = Release|Any CPU + {F7B14393-5418-4D51-B342-5EC2A51FC62B}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Rocket.Unturned/Module/RuntimeLibs/System.IO.Compression.FileSystem.dll b/Rocket.Unturned/Module/RuntimeLibs/System.IO.Compression.FileSystem.dll index 59e95a23..50b74f81 100644 Binary files a/Rocket.Unturned/Module/RuntimeLibs/System.IO.Compression.FileSystem.dll and b/Rocket.Unturned/Module/RuntimeLibs/System.IO.Compression.FileSystem.dll differ diff --git a/Rocket.Unturned/Module/RuntimeLibs/System.IO.Compression.dll b/Rocket.Unturned/Module/RuntimeLibs/System.IO.Compression.dll index 751ef2a6..835c025a 100644 Binary files a/Rocket.Unturned/Module/RuntimeLibs/System.IO.Compression.dll and b/Rocket.Unturned/Module/RuntimeLibs/System.IO.Compression.dll differ diff --git a/Rocket.Unturned/Rocket.Unturned.csproj b/Rocket.Unturned/Rocket.Unturned.csproj index b0cbef7c..e41c4b17 100644 --- a/Rocket.Unturned/Rocket.Unturned.csproj +++ b/Rocket.Unturned/Rocket.Unturned.csproj @@ -13,10 +13,10 @@ - + All - + All diff --git a/Rocket/Rocket.API/Rocket.API.csproj b/Rocket/Rocket.API/Rocket.API.csproj index e775a643..b4b5196a 100644 --- a/Rocket/Rocket.API/Rocket.API.csproj +++ b/Rocket/Rocket.API/Rocket.API.csproj @@ -7,10 +7,10 @@ - + All - + All diff --git a/Rocket/Rocket.Core.Tests/Rocket.Core.Tests.csproj b/Rocket/Rocket.Core.Tests/Rocket.Core.Tests.csproj index b3c5be20..df54900d 100644 --- a/Rocket/Rocket.Core.Tests/Rocket.Core.Tests.csproj +++ b/Rocket/Rocket.Core.Tests/Rocket.Core.Tests.csproj @@ -12,10 +12,10 @@ - + All - + All diff --git a/Rocket/Rocket.Core/Plugins/RocketPluginManager.cs b/Rocket/Rocket.Core/Plugins/RocketPluginManager.cs index 403d641f..c9ec8b40 100644 --- a/Rocket/Rocket.Core/Plugins/RocketPluginManager.cs +++ b/Rocket/Rocket.Core/Plugins/RocketPluginManager.cs @@ -45,18 +45,39 @@ private Assembly OnAssemblyResolve(object sender, ResolveEventArgs args) try { AssemblyName requestedName = new AssemblyName(args.Name); - var bestMatch = libraries.FirstOrDefault(lib => string.Equals(lib.Key.Name, requestedName.Name) && lib.Key.Version >= requestedName.Version); + var matchesByName = libraries.Where(lib => string.Equals(lib.Key.Name, requestedName.Name)); + + // Prefer exactly-matching version if possible. + var bestMatch = matchesByName.FirstOrDefault(lib => lib.Key.Version == requestedName.Version); + if (string.IsNullOrEmpty(bestMatch.Value)) + { + // Otherwise, fallback to highest version. + bestMatch = matchesByName.OrderByDescending(lib => lib.Key.Version).FirstOrDefault(); + } if (!string.IsNullOrEmpty(bestMatch.Value)) { + // https://github.com/SmartlyDressedGames/Legally-Distinct-Missile/issues/84 + if (requestedName.Version != null) + { + if (bestMatch.Key.Version == null) + { + Logging.Logger.LogWarning($"Rocket best match for dependency {requestedName} has no configued version: {bestMatch.Key} at {bestMatch.Value}"); + } + else if (bestMatch.Key.Version < requestedName.Version) + { + Logging.Logger.LogWarning($"Rocket best match for dependency {requestedName} version is older than requested: {bestMatch.Key} at {bestMatch.Value}"); + } + } + return Assembly.Load(File.ReadAllBytes(bestMatch.Value)); } } catch (Exception ex) { - Logging.Logger.LogException(ex, "Caught exception resolving dependency: " + args.Name); + Logging.Logger.LogException(ex, "Rocket caught exception resolving dependency: " + args.Name); } - Logging.Logger.LogError("Could not find dependency: " + args.Name); + Logging.Logger.LogError("Rocket could not find dependency: " + args.Name); return null; } @@ -84,6 +105,11 @@ private void loadPlugins() libraries.Add(pair.Key,pair.Value); } + foreach (KeyValuePair pair in libraries) + { + Logging.Logger.Log($"Rocket dependency registered: {pair.Key} at {pair.Value}"); + } + pluginAssemblies = LoadAssembliesFromDirectory(Environment.PluginsDirectory); List pluginImplemenations = RocketHelper.GetTypesFromInterface(pluginAssemblies, "IRocketPlugin"); foreach (Type pluginType in pluginImplemenations) diff --git a/Rocket/Rocket.Core/Rocket.Core.csproj b/Rocket/Rocket.Core/Rocket.Core.csproj index e8967341..2d8ace35 100644 --- a/Rocket/Rocket.Core/Rocket.Core.csproj +++ b/Rocket/Rocket.Core/Rocket.Core.csproj @@ -18,10 +18,10 @@ - + All - + All