diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100644 index 0000000..1860f3a --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,21 @@ +#!/bin/sh +# Blocks a push that would publish a credential. +# +# This runs here rather than in CI on purpose. A secret-scanning job on GitHub Actions only fires +# after the objects are already on GitHub, where they stay reachable through the API even if the +# branch is deleted, so a red build reports the leak instead of preventing it. CI runs the checks +# that are still useful once the code is public: the dependency audit, the type check, the tests. +# +# Enable in a fresh clone with: git config core.hooksPath .githooks +# Bypass deliberately with: git push --no-verify +set -e + +root=$(git rev-parse --show-toplevel) + +if ! command -v node >/dev/null 2>&1; then + echo "pre-push: node is not on PATH, so the secret scan cannot run." >&2 + echo "pre-push: install Node or push with --no-verify if you have checked the diff yourself." >&2 + exit 1 +fi + +node "$root/backend/scripts/secret-scan.mjs" diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 8a0dc48..364c8a1 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -32,8 +32,8 @@ body: attributes: label: SS2 Revive version description: >- - The first SS2 Revive line in the log says it: `SS2 Revive 0.2.0 starting.` - placeholder: "0.2.0" + The first SS2 Revive line in the log says it: `SS2 Revive 1.2.0 starting.` + placeholder: "1.2.0" validations: required: true @@ -42,10 +42,10 @@ body: attributes: label: Game build description: >- - The line straight after that one, at the end: `Unity ... | product ... | version '1.3.1.276'`. - Anything other than 1.3.1.276 is untested, which is worth knowing rather than a reason not - to report. - placeholder: "1.3.1.276" + The line straight after that one, at the end: `Unity ... | product ... | version '1.3.7.3054'`. + SS2 Revive supports build 1.3.7.3054. Version 1.5.x removed the networking code the mod + restores and cannot work. + placeholder: "1.3.7.3054" validations: required: true @@ -65,7 +65,7 @@ body: placeholder: | 1. Launched the game 2. Opened the party menu - 3. Invited a friend with F10 + 3. Invited a friend through Steam 4. ... validations: required: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b213c9f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,136 @@ +name: CI + +# What this deliberately does not do is scan for committed secrets. That check has to happen before +# the objects reach GitHub, or it is only a notification that the leak already shipped, so it lives +# in .githooks/pre-push instead. Everything here is a check that is still worth running on code that +# is already public. + +on: + push: + branches: ["**"] + # Tagged pushes are the release build's job; running both would duplicate the work. + tags-ignore: ["v*.*.*"] + pull_request: + # Advisories are published against dependencies that have not changed, so the audit needs a clock + # of its own rather than waiting for the next commit. + schedule: + - cron: "23 6 * * 1" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +env: + WRANGLER_SEND_METRICS: "false" + +jobs: + worker: + name: Worker types and tests + runs-on: ubuntu-latest + timeout-minutes: 15 + defaults: + run: + working-directory: backend + + steps: + - name: Check out source + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Set up pnpm + uses: pnpm/action-setup@v4 + with: + # There is no package.json at the repository root, so the version has to be pointed at the + # backend workspace explicitly instead of being discovered. + package_json_file: backend/package.json + + - name: Set up Node + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: pnpm + cache-dependency-path: backend/pnpm-lock.yaml + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Check generated bindings and types + # Regenerates worker-configuration.d.ts from wrangler.jsonc and fails if the committed copy + # has drifted, then type-checks against it. A binding added to the config but never wired up + # is caught here rather than at deploy time. + run: pnpm run check + + - name: Run Worker tests + # Real Miniflare D1 and R2, including the migrations, so a migration that does not apply + # cleanly fails here instead of against production. + run: pnpm test + + - name: Build Worker bundle + # A dry-run deploy. Proves the Worker still bundles without needing any Cloudflare + # credentials in CI. + run: pnpm run build + + audit: + name: Dependency audit + runs-on: ubuntu-latest + timeout-minutes: 10 + defaults: + run: + working-directory: backend + + steps: + - name: Check out source + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Set up pnpm + uses: pnpm/action-setup@v4 + with: + package_json_file: backend/package.json + + - name: Set up Node + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: pnpm + cache-dependency-path: backend/pnpm-lock.yaml + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Audit dependencies + # Kept in its own job so a newly published advisory reports as an advisory, rather than + # turning up as a mystery failure in the middle of the test job. + run: pnpm run security:audit + + data: + name: SS2Revive_Data tests + runs-on: windows-latest + timeout-minutes: 15 + + steps: + - name: Check out source + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Set up .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 8.0.x + + - name: Run data tests + # SS2Revive_Data references no game assembly, and the runner reports SKIP for the checks + # that need an installed copy of the game, so this is meaningful on a bare runner. The + # plugin itself is not built here: SS2Revive.csproj compiles against the game's Managed + # directory, which no hosted runner has. + shell: pwsh + run: | + dotnet run --project tests/DataTests/DataTests.csproj --configuration Release + if ($LASTEXITCODE -ne 0) { throw 'Data tests failed.' } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..4d14528 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,144 @@ +name: Build draft release + +on: + push: + tags: + - "v*.*.*" + +permissions: + contents: write + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + setup: + name: Build Windows setup + runs-on: windows-latest + timeout-minutes: 20 + + steps: + - name: Check out tagged source + uses: actions/checkout@v6 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Set up .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 8.0.x + + - name: Validate tag and project version + id: metadata + shell: pwsh + env: + RELEASE_TAG_INPUT: ${{ github.ref_name }} + run: | + $tag = $env:RELEASE_TAG_INPUT + if ($tag -notmatch '^v(?\d+\.\d+\.\d+)$') { + throw "Release tags must use the exact vMAJOR.MINOR.PATCH form. Received: $tag" + } + + [xml]$props = Get-Content -LiteralPath 'Directory.Build.props' + $projectVersion = [string]$props.Project.PropertyGroup.SS2ReviveVersion + $version = $Matches.version + if ($projectVersion -ne $version) { + throw "Tag $tag does not match SS2ReviveVersion $projectVersion." + } + + "tag=$tag" >> $env:GITHUB_OUTPUT + "version=$version" >> $env:GITHUB_OUTPUT + + - name: Publish single-file setup + shell: pwsh + run: | + $output = Join-Path $env:RUNNER_TEMP 'ss2revive-release' + dotnet publish tools/Setup/SS2Revive.Setup.csproj ` + --configuration Release ` + --runtime win-x64 ` + --self-contained true ` + --output $output + if ($LASTEXITCODE -ne 0) { throw 'Setup publish failed.' } + + $source = Join-Path $output 'SS2Revive.Setup.exe' + $name = 'SS2Revive-Setup-${{ steps.metadata.outputs.version }}.exe' + $destination = Join-Path $output $name + Move-Item -LiteralPath $source -Destination $destination + + & $destination --self-test + if ($LASTEXITCODE -ne 0) { throw 'Published setup self-test failed.' } + + $hash = (Get-FileHash -LiteralPath $destination -Algorithm SHA256).Hash.ToLowerInvariant() + "$hash $name" | Set-Content ` + -LiteralPath (Join-Path $output "$name.sha256") ` + -Encoding ascii ` + -NoNewline + + - name: Prepare draft release notes + shell: pwsh + run: | + @' + # SS2 Revive ${{ steps.metadata.outputs.tag }} + + SS2 Revive restores multiplayer, progression, Creation Mode, and community-made maps for Surgeon Simulator 2 build 1.3.7.3054. + + ## Install + + 1. Download `SS2Revive-Setup-${{ steps.metadata.outputs.version }}.exe`. + 2. Verify it with the attached `.sha256` file. + 3. Run Setup, choose an install location, and enter the Steam login name for an account that owns Surgeon Simulator 2. + 4. Complete the password and Steam Guard prompts in DepotDownloader's separate window. + 5. Start the game with the launcher Setup creates. + + The setup executable is currently unsigned, so Windows may display a SmartScreen warning. + + ## Maintainer checklist before publishing + + - Attach `SS2Revive-${{ steps.metadata.outputs.version }}.zip`, created locally with `./pack.ps1`. + - Confirm the mod ZIP contains `SS2Revive.dll`, `SS2Revive_Data.dll`, and `newsfeed/NewsFeed.json`. + - Confirm the public community service is healthy. + - Replace or expand these draft notes from the local ignored release-description document if needed. + - Publish this draft only after all assets are present. + + ## Compatibility + + Windows x64 and Surgeon Simulator 2 build 1.3.7.3054 are required. Legacy maps are preserved as-is and some may contain bugs, fail to load, or open to a black screen. + '@ | Set-Content -LiteralPath (Join-Path $env:RUNNER_TEMP 'release-notes.md') -Encoding utf8 + + - name: Create or update draft release + shell: pwsh + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ steps.metadata.outputs.tag }} + RELEASE_VERSION: ${{ steps.metadata.outputs.version }} + run: | + $output = Join-Path $env:RUNNER_TEMP 'ss2revive-release' + $exe = Join-Path $output "SS2Revive-Setup-$env:RELEASE_VERSION.exe" + $checksum = "$exe.sha256" + $notes = Join-Path $env:RUNNER_TEMP 'release-notes.md' + + $existing = & gh release view $env:RELEASE_TAG --json isDraft 2>$null + if ($LASTEXITCODE -eq 0) { + $release = $existing | ConvertFrom-Json + if (-not $release.isDraft) { + throw "Release $env:RELEASE_TAG is already public; refusing to replace its assets." + } + + gh release upload $env:RELEASE_TAG $exe $checksum --clobber + if ($LASTEXITCODE -ne 0) { throw 'Uploading setup assets failed.' } + gh release edit $env:RELEASE_TAG ` + --draft ` + --title "SS2 Revive $env:RELEASE_TAG" ` + --notes-file $notes + if ($LASTEXITCODE -ne 0) { throw 'Updating the draft release failed.' } + } + else { + gh release create $env:RELEASE_TAG $exe $checksum ` + --verify-tag ` + --draft ` + --title "SS2 Revive $env:RELEASE_TAG" ` + --notes-file $notes + if ($LASTEXITCODE -ne 0) { throw 'Creating the draft release failed.' } + } diff --git a/.gitignore b/.gitignore index fb208f9..c208032 100644 --- a/.gitignore +++ b/.gitignore @@ -10,8 +10,9 @@ obj/ # Local machine paths live here and must never be published. Directory.Build.user.props -# Working notes and protocol write-ups. Kept on disk, not in the repository. +# Documentation is retained locally and transferred manually, not published from this branch. docs/ +*.md # BepInEx is linked from the README, never vendored here. lib/ @@ -25,3 +26,15 @@ dist/ # Inventory.dat, ProgressionConfig.json and the news tile artwork. *.dat assets/newsfeed/images/*.png + +# Local Cloudflare Worker development. Never commit local state or secrets. +backend/node_modules/ +backend/**/node_modules/ +backend/**/.wrangler/ +backend/**/.dev.vars +backend/**/.env* +backend/**/wrangler.production.jsonc +backend/**/coverage/ +backend/**/dist/ +backend/**/dist-production/ +.pnpm-store/ diff --git a/BUILDING.md b/BUILDING.md deleted file mode 100644 index db539f7..0000000 --- a/BUILDING.md +++ /dev/null @@ -1,63 +0,0 @@ -# Building SS2 Revive from source - -You need the [.NET SDK](https://dotnet.microsoft.com/download) (10 or newer) and a copy of the game -installed. BepInEx and HarmonyX come from NuGet, so there is nothing else to fetch. - -```powershell -dotnet build SS2Revive.sln -c Release -``` - -The build finds the game through Steam. If it cannot, because your library lives on another drive -or you run the game outside Steam, copy `Directory.Build.user.props.example` to -`Directory.Build.user.props` and set `GameDir` in it. That filename is gitignored. You can also -pass the path directly: - -```powershell -dotnet build SS2Revive.sln -c Release -p:GameDir="D:\SteamLibrary\steamapps\common\Surgeon Simulator 2" -``` - -A successful build copies both DLLs and the news feed straight into your `BepInEx\plugins\SS2Revive` -folder, so the edit-build-launch loop needs no copying by hand. - -There is a self-check that runs the backend against your installed game files: - -```powershell -dotnet run --project tests\DataTests -``` - -It prints a line per check and exits non-zero if any fail. Checks that need `Inventory.dat` report -`SKIP` rather than failing when no install is found. - -## Making a release - -```powershell -.\pack.ps1 -``` - -Builds, runs the self-check, and writes `dist\SS2Revive-.zip` containing exactly what the -install step in the README tells you to copy. It refuses to package anything if the self-check -fails. - -Attach that file to the release as it is. `installCurrentVersion.ps1` asks GitHub for the newest -release and takes the `SS2Revive-*.zip` on it, so the version in the name never has to be told to -anything - but the `SS2Revive-` prefix does have to stay. - -It also verifies that the `SS2Revive_Data.dll` going into the zip is the **net472** build. That -project multi-targets, the two outputs look interchangeable, and shipping the netstandard2.0 one -boots the game to a black screen with no main menu - the reason is at the top of -`src/SS2Revive_Data/SS2Revive_Data.csproj`. It is not a mistake you would catch by looking. - -The version comes from `SS2ReviveVersion` in `Directory.Build.props`, which is the only place it -is written down. Both assemblies and the `[BepInPlugin]` attribute are generated from it, so the -number the log prints and the number on the zip cannot drift apart. - -## Layout - -| Path | What it is | -|---|---| -| `src/SS2Revive` | The plugin. Harmony patches, Steam lobbies, Steam P2P transport, news feed. | -| `src/SS2Revive_Data` | The backend, as a plain library. Request router, save file, challenge catalogue, JSON.| -| `tests/DataTests` | A console runner for the library. No test framework, no packages. | -| `assets/newsfeed` | The authored news feed that ships with a release. | -| `installCurrentVersion.ps1` | Installs build 1.3.7, BepInEx and the mod, into a folder of its own. | -| `pack.ps1` | Builds, self-checks and assembles the release zip. | diff --git a/Directory.Build.props b/Directory.Build.props index 6acb9c5..07b1b71 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -9,7 +9,7 @@ zip cannot drift apart. --> - 1.1.0 + 1.2.0 + + $(GameManagedDir)\UnityEngine.IMGUIModule.dll + false + + + $(GameManagedDir)\UnityEngine.UI.dll + false + + + $(GameManagedDir)\UnityEngine.UIModule.dll + false + + + $(GameManagedDir)\Unity.TextMeshPro.dll + false + + + + +