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