mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-25 23:11:15 +02:00
Add custom carts: named, version-pinned mod sets that play as their own game
A custom cart pairs an identity (title, shell colour, label art) with a base game, a list of mods pinned to exact builds with their option values frozen, a load order, and a seal. It ships no code of its own: every mod it names is a separately published mod, which is what keeps a cart auditable before it runs and reproducible after an author's repo disappears. Format and storage: - src/carts/CartManifest.lua parses and validates cart.json, canonicalises it for hashing and reads/writes the .g1rcart bundle. The bundle is a data-only serialised table read through SaveSerializer, so an imported cart can never execute code. Canonical strings are length-prefixed because option keys and values are author-controlled and could otherwise forge a record boundary and collide two different carts onto one hash. - Pins name a public source: a GitHub release with its sha256, a GameBanana file id with its md5, or "local" for a capture that only exists on this install. A local pin is unpublishable by construction, which is what makes "build it here, publish later" possible without inventing a hash. - Label art rides alongside the manifest rather than inside its identity, so re-arting a cart does not tell every player their run is out of date. src/core/Base64.lua decodes it; strict, with no whitespace tolerance. Saves: - Cart playthroughs live in the cart's own slot namespace (saves/cart_<id>/), so a cart's file never sits beside a vanilla one and uninstalling a cart never orphans a save. Every save records the cart build it was made under. The seal: - A sealed cart loads its pinned list, in its order, with its options, and nothing else. A pinned mod with no frozen options gets an empty bucket so unfrozen keys fall to schema defaults, identical for everyone; otherwise two players on one cart quietly run different games. - A sealed cart refuses to load when a pin is missing or installed at another version. Playing a subset of the cart is the exact dishonesty the seal exists to prevent, so the refusal loads nothing at all. - Breaking the seal is permanent, marked per save slot, and downgrades that playthrough to open behaviour. It cannot be cleared through any public API. Launcher: - A game's page carries a Custom Carts control and a picker; choosing a cart turns the page into that cart's page, with its own cartridge, title and save slots. The rail of five games never grows and a cart id never reaches imp.tab or imp.panelVersion. - Loader.planCart runs before boot so a refusal is visible on the page instead of being discovered as an error after launch. - Save as cart captures the enabled mods for a game and names, before the player confirms, every mod that could only be pinned to this install and whether the result can be shared at all. Authoring: - tools/cartkit.py scaffolds, validates, pins and packs a cart, and installs a release workflow. Its writer is byte-identical to the engine's serialiser.
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Existing v<version> tag to build and publish."
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: release
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
CART_ID: "{{CART_ID}}"
|
||||
CARTKIT_REPO: bryanthaboi/gen1recomp
|
||||
CARTKIT_REF: dev
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.tag || github.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Fetch cartkit
|
||||
run: |
|
||||
set -euo pipefail
|
||||
curl -fsSL --retry 3 -o "$RUNNER_TEMP/cartkit.py" \
|
||||
"https://raw.githubusercontent.com/${CARTKIT_REPO}/${CARTKIT_REF}/tools/cartkit.py"
|
||||
python3 "$RUNNER_TEMP/cartkit.py" selftest --quiet
|
||||
|
||||
- name: Check the tag against cart.json
|
||||
id: cart
|
||||
env:
|
||||
TAG: ${{ github.event.inputs.tag || github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 - <<'PY' >> "$GITHUB_OUTPUT"
|
||||
import json, os, sys
|
||||
|
||||
with open("cart.json", encoding="utf-8") as fh:
|
||||
cart = json.load(fh)
|
||||
version = str(cart.get("version", ""))
|
||||
cart_id = str(cart.get("id", ""))
|
||||
tag = os.environ["TAG"]
|
||||
if tag != f"v{version}":
|
||||
print(f"::error::tag {tag} does not match cart.json version "
|
||||
f"{version} (expected v{version})", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
stamped = os.environ["CART_ID"]
|
||||
if cart_id != stamped:
|
||||
print(f"::error::cart.json id is {cart_id}, but this workflow "
|
||||
f"was stamped for {stamped}; rerun cartkit "
|
||||
"add-release-workflow", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
print(f"version={version}")
|
||||
print(f"id={cart_id}")
|
||||
print(f"tag={tag}")
|
||||
PY
|
||||
|
||||
- name: Validate every pin
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: python3 "$RUNNER_TEMP/cartkit.py" validate . --online --strict
|
||||
|
||||
- name: Pack the cart
|
||||
env:
|
||||
CART_VERSION: ${{ steps.cart.outputs.version }}
|
||||
CART_NAME: ${{ steps.cart.outputs.id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
out="$GITHUB_WORKSPACE/dist"
|
||||
rm -rf "$out"
|
||||
mkdir -p "$out"
|
||||
python3 "$RUNNER_TEMP/cartkit.py" pack . \
|
||||
-o "$out/${CART_NAME}-${CART_VERSION}.g1rcart"
|
||||
(cd "$out" && sha256sum ./*.g1rcart > sha256sums.txt)
|
||||
cat "$out/sha256sums.txt"
|
||||
|
||||
- name: Publish GitHub Release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
CART_VERSION: ${{ steps.cart.outputs.version }}
|
||||
CART_NAME: ${{ steps.cart.outputs.id }}
|
||||
TAG: ${{ steps.cart.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
prev="$(git tag -l 'v*' --sort=-v:refname | grep -v "^${TAG}$" | head -1 || true)"
|
||||
range="${prev:+${prev}..}${TAG}"
|
||||
changes="$(git log --no-merges --pretty='- %s' "$range" | head -50 || true)"
|
||||
|
||||
notes=$'Download the .g1rcart and open it from the game to install this cart.'
|
||||
notes+=$'\n\nThe cart is a manifest: it pins each mod to the exact build listed in cart.json and ships no code of its own.'
|
||||
if [ -n "$changes" ]; then
|
||||
notes+=$'\n\n## Changes\n\n'"$changes"
|
||||
fi
|
||||
|
||||
asset="dist/${CART_NAME}-${CART_VERSION}.g1rcart"
|
||||
if gh release view "$TAG" >/dev/null 2>&1; then
|
||||
gh release upload "$TAG" "$asset" "dist/sha256sums.txt" --clobber
|
||||
else
|
||||
gh release create "$TAG" \
|
||||
--target "$GITHUB_SHA" \
|
||||
--title "$CART_VERSION" \
|
||||
--notes "$notes" \
|
||||
"$asset" \
|
||||
"dist/sha256sums.txt"
|
||||
fi
|
||||
|
||||
echo "Published $TAG"
|
||||
+1749
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user