diff --git a/.github/release-notes/3.1.15.md b/.github/release-notes/3.1.15.md new file mode 100644 index 00000000..1cd96aeb --- /dev/null +++ b/.github/release-notes/3.1.15.md @@ -0,0 +1,27 @@ +## Custom NPC Inventory Persistence + +- Custom NPC primary inventories now initialize their native slot collection before saved items are restored, so items given to custom customers and dealers retain their identity, quantity, and slot position across save and reload ([#261](https://github.com/ifBars/S1API/pull/261)). +- Dealer overflow restoration remains separate from primary inventory hydration, and base-game dealers keep their existing load behavior ([#261](https://github.com/ifBars/S1API/pull/261)). + +## Custom NPC Relationships and Deals + +- Custom NPCs now derive a stable native GUID from their existing persistent identity and register before accepted contracts are loaded. Newly scheduled custom-customer deals therefore resolve to the same NPC after a reload ([#262](https://github.com/ifBars/S1API/pull/262)). +- Saved relationship values and unlock state now survive deferred custom-NPC spawning, including explicit zero relationship values and both direct and recommendation unlock paths ([#262](https://github.com/ifBars/S1API/pull/262)). +- Loader phases reuse the prepared custom NPC instance and initialize inactive customer deal-attendance state before assigning restored contracts, preventing prefab defaults or incomplete runtime caches from replacing saved state ([#262](https://github.com/ifBars/S1API/pull/262)). + +## Compatibility and Validation + +- Preserved public API shape, existing custom NPC identity IDs, save schemas, network payloads, dealer overflow behavior, and base-game NPC loading. +- Validated 3.1.15 with zero-warning Mono and IL2CPP builds, 575 passing Mono tests, 564 passing IL2CPP tests, documentation coverage, ApiCompat, and the hosted coverage analyzer. +- Verified live Mono and IL2CPP save-to-menu-to-reload flows for custom inventory contents, customer and dealer relationships, unlock state, and accepted-contract ownership. +- Contracts already saved with a pre-3.1.15 random custom-NPC GUID cannot be migrated because the native contract payload contains the customer GUID but not the custom NPC identity ID. Newly saved and future contracts use the stable identity. + +## Contributors + +- @Diffuin: [custom NPC inventory restoration](https://github.com/ifBars/S1API/pull/261) and [custom NPC deal and relationship persistence](https://github.com/ifBars/S1API/pull/262). +- @ifBars: dual-runtime remediation, live persistence validation, and 3.1.15 release preparation for [#261](https://github.com/ifBars/S1API/pull/261) and [#262](https://github.com/ifBars/S1API/pull/262). + +## Release Links + +- [Download S1API-Forked-3.1.15.zip](https://github.com/ifBars/S1API/releases/download/v3.1.15/S1API-Forked-3.1.15.zip) +- [Full changelog](https://github.com/ifBars/S1API/compare/v3.1.14...v3.1.15) diff --git a/.github/workflows/publish-github-release.yml b/.github/workflows/publish-github-release.yml index aa779e64..b01edbd5 100644 --- a/.github/workflows/publish-github-release.yml +++ b/.github/workflows/publish-github-release.yml @@ -326,14 +326,18 @@ jobs: env: RELEASE_TAG: ${{ steps.metadata.outputs.tag }} RELEASE_NAME: ${{ steps.metadata.outputs.release_name }} + RELEASE_VERSION: ${{ steps.metadata.outputs.release_version }} RELEASE_BRANCH: ${{ steps.metadata.outputs.release_branch }} PRERELEASE: ${{ steps.metadata.outputs.prerelease }} with: script: | + const fs = require("fs"); + const path = require("path"); const owner = context.repo.owner; const repo = context.repo.repo; const tag = process.env.RELEASE_TAG; const releaseName = process.env.RELEASE_NAME; + const releaseVersion = process.env.RELEASE_VERSION; const releaseBranch = process.env.RELEASE_BRANCH; const prerelease = process.env.PRERELEASE === "true"; @@ -371,13 +375,32 @@ jobs: }; const targetCommitish = await resolveTargetCommitish(); - const notes = await github.rest.repos.generateReleaseNotes({ - owner, - repo, - tag_name: tag, - target_commitish: targetCommitish - }); - core.setOutput("release_notes", notes.data.body); + const curatedNotesPath = path.join( + process.env.GITHUB_WORKSPACE, + ".github", + "release-notes", + `${releaseVersion}.md` + ); + let releaseNotes; + if (fs.existsSync(curatedNotesPath)) { + releaseNotes = fs.readFileSync(curatedNotesPath, "utf8").trim(); + if (!releaseNotes) { + throw new Error(`Curated release notes are empty: ${curatedNotesPath}`); + } + core.notice(`Using curated release notes from ${curatedNotesPath}`); + } else { + core.warning( + `No curated release notes found for ${releaseVersion}; using GitHub-generated notes.` + ); + const generatedNotes = await github.rest.repos.generateReleaseNotes({ + owner, + repo, + tag_name: tag, + target_commitish: targetCommitish + }); + releaseNotes = generatedNotes.data.body; + } + core.setOutput("release_notes", releaseNotes); const payload = { owner, @@ -385,7 +408,7 @@ jobs: tag_name: tag, target_commitish: targetCommitish, name: releaseName, - body: notes.data.body, + body: releaseNotes, draft: false, prerelease, make_latest: prerelease ? "false" : "legacy" diff --git a/S1API/S1API.cs b/S1API/S1API.cs index 5dab79f3..e4ebf796 100644 --- a/S1API/S1API.cs +++ b/S1API/S1API.cs @@ -12,7 +12,7 @@ using S1API.Lifecycle; using S1API.Map; -[assembly: MelonInfo(typeof(S1API.S1API), "S1API (Forked by Bars)", "3.1.14", "KaBooMa")] +[assembly: MelonInfo(typeof(S1API.S1API), "S1API (Forked by Bars)", "3.1.15", "KaBooMa")] [assembly: MelonPriority(Int32.MinValue)] #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member namespace S1API diff --git a/S1API/S1API.csproj b/S1API/S1API.csproj index 346633c4..85277c30 100644 --- a/S1API/S1API.csproj +++ b/S1API/S1API.csproj @@ -23,7 +23,7 @@ $(NoWarn);1591 true latest - 3.1.14 + 3.1.15 diff --git a/VERSIONING.md b/VERSIONING.md index 6544d569..98035bdf 100644 --- a/VERSIONING.md +++ b/VERSIONING.md @@ -65,21 +65,23 @@ Patch numbers are ordinary integers, not single digits. That means `2.9.10` is t ### New release line 1. Prepare the release changes and version bump on `releases/X.Y.Z`, based on the intended `stable` commit. -2. Open a PR from `releases/X.Y.Z` into `stable` and complete validation. -3. Merge the PR into `stable` with a merge commit. -4. Fast-forward `releases/X.Y.Z` to that exact stable merge commit. -5. Tag the shared commit as `vX.Y.Z` to publish the stable release. -6. Treat both the tag and release branch as immutable release records. -7. Continue forward development on `stable` toward the next version. +2. Add curated release notes at `.github/release-notes/X.Y.Z.md`, following the grouped format used by recent releases. +3. Open a PR from `releases/X.Y.Z` into `stable` and complete validation. +4. Merge the PR into `stable` with a merge commit. +5. Fast-forward `releases/X.Y.Z` to that exact stable merge commit. +6. Tag the shared commit as `vX.Y.Z` to publish the stable release. +7. Treat both the tag and release branch as immutable release records. +8. Continue forward development on `stable` toward the next version. ### Hotfix release for an existing line 1. Create `releases/X.Y.(Z+1)` from the current stable tree when it still matches the shipped version, or from the `vX.Y.Z` tag when stable has unrelated future work. -2. Apply only the hotfix and the `X.Y.(Z+1)` version bump to that new release branch. -3. Open a PR from `releases/X.Y.(Z+1)` into `stable` and merge it after validation. -4. Fast-forward `releases/X.Y.(Z+1)` to the resulting stable merge commit. -5. Tag that exact commit as `vX.Y.(Z+1)` to publish the new patch. -6. Leave `releases/X.Y.Z` and its tag unchanged as the immutable record of the previous release. +2. Apply only the intended hotfixes, release-tooling changes, and the `X.Y.(Z+1)` version bump to that new release branch. +3. Add curated release notes at `.github/release-notes/X.Y.(Z+1).md` for the exact previous-tag comparison range. +4. Open a PR from `releases/X.Y.(Z+1)` into `stable` and merge it after validation. +5. Fast-forward `releases/X.Y.(Z+1)` to the resulting stable merge commit. +6. Tag that exact commit as `vX.Y.(Z+1)` to publish the new patch. +7. Leave `releases/X.Y.Z` and its tag unchanged as the immutable record of the previous release. Using the new version's branch as the PR head keeps review history attached to the release while ensuring every version branch continues to identify the assembly it shipped. @@ -115,13 +117,15 @@ Before expecting a NuGet package to publish: The GitHub release workflow packages public mod archives and can publish the same release to mod distribution platforms. - `publish-github-release.yml` runs from release tags and can also be rerun with `workflow_dispatch`. +- When `.github/release-notes/X.Y.Z.md` exists at the tagged commit, its curated Markdown is used as the GitHub release body. Historical tags and manual reruns without that file fall back to GitHub-generated notes. +- Curated notes should use concise domain-specific change sections, a compatibility and validation section, PR-linked contributor credits, and release links, matching the structure of recent stable releases. - The GitHub/Nexus archive is `S1API-Forked-x.y.z.zip` and contains `Mods/` and `Plugins/` at the archive root. - GitHub Releases should only publish `S1API-Forked-x.y.z.zip` as a release asset. - The Thunderstore archive is `S1API-TS-x.y.z.zip` and contains `icon.png`, `README.md`, `manifest.json`, `Mods/`, and `Plugins/` at the archive root, but it is only used for Thunderstore publishing. - The uppercase `Mods/` and `Plugins/` paths are intentional so case-sensitive filesystems do not create parallel lowercase install folders. - The GitHub release asset is always uploaded by the workflow. - Nexus Mods upload runs when `NEXUSMODS_API_KEY`, `NEXUSMODS_FILE_GROUP_ID`, and `NEXUSMODS_MOD_ID` are configured. -- Generated release notes are published on GitHub. The Nexus Mods upload intentionally omits the optional changelog input because Nexus handles file versions and changelogs through separate endpoints, and a rejected changelog request would otherwise fail the workflow after a successful file upload. +- The workflow publishes versioned curated Markdown on GitHub when present and falls back to GitHub-generated notes only when it is absent. The Nexus Mods upload intentionally omits the optional changelog input because Nexus handles file versions and changelogs through separate endpoints, and a rejected changelog request would otherwise fail the workflow after a successful file upload. - Thunderstore upload runs when `THUNDERSTORE_TOKEN` is configured. - `workflow_dispatch` exposes `publish_nexus` and `publish_thunderstore` toggles for refreshing GitHub assets without re-publishing external platforms.