From 1bfa0bc21ab1206921e7baf632212e51804a6a38 Mon Sep 17 00:00:00 2001 From: Mark7625 <72366279+Mark7625@users.noreply.github.com> Date: Tue, 4 Aug 2026 19:48:32 +0100 Subject: [PATCH 01/11] Ci for conflicting gamevals --- .data/.gitignore | 4 + .data/gamevals-binary/max-ids.toml | 22 ++ .github/workflows/gameval-conflicts.yml | 77 +++++ .../openrune/gamevals/GameValMaxIdManifest.kt | 134 ++++++++ .../dev/openrune/gamevals/GamevalDumper.kt | 2 + tools/scripts/fix_gameval_conflicts.py | 285 ++++++++++++++++++ 6 files changed, 524 insertions(+) create mode 100644 .data/gamevals-binary/max-ids.toml create mode 100644 .github/workflows/gameval-conflicts.yml create mode 100644 or-cache/src/main/kotlin/dev/openrune/gamevals/GameValMaxIdManifest.kt create mode 100644 tools/scripts/fix_gameval_conflicts.py diff --git a/.data/.gitignore b/.data/.gitignore index b55332aa6..218bfec93 100644 --- a/.data/.gitignore +++ b/.data/.gitignore @@ -1,6 +1,10 @@ /* symbols/.local/ !gamevals/ +!gamevals-binary/ !raw-cache/ !symbols/ !.gitignore + +gamevals-binary/* +!gamevals-binary/max-ids.toml diff --git a/.data/gamevals-binary/max-ids.toml b/.data/gamevals-binary/max-ids.toml new file mode 100644 index 000000000..48464519c --- /dev/null +++ b/.data/gamevals-binary/max-ids.toml @@ -0,0 +1,22 @@ +# OSRS cache gameval max IDs per table. +# IDs 0..=max are reserved; custom gamevals must be > max. +# Generated by freshCache / GamevalDumper - do not hand-edit. +# dbcol is omitted (auto-generated from table/column defs). + +revision = 239 + +[max_ids] +dbrow = 16939 +dbtable = 258 +interface = 968 +inv = 1027 +jingle = 1196 +loc = 62399 +npc = 16337 +obj = 34058 +seq = 14474 +spotanim = 4017 +sprites = 8559 +varbit = 20396 +varcs = 1506 +varp = 5724 diff --git a/.github/workflows/gameval-conflicts.yml b/.github/workflows/gameval-conflicts.yml new file mode 100644 index 000000000..32d55de52 --- /dev/null +++ b/.github/workflows/gameval-conflicts.yml @@ -0,0 +1,77 @@ +name: Gameval Conflict Check + +permissions: + contents: write + pull-requests: write + +on: + pull_request: + types: [opened, synchronize, reopened] + paths: + - "content/**/gamevals.toml" + - "api/**/gamevals.toml" + - ".data/gamevals/**" + - ".data/gamevals-binary/max-ids.toml" + - "tools/scripts/fix_gameval_conflicts.py" + - ".github/workflows/gameval-conflicts.yml" + +jobs: + resolve: + runs-on: ubuntu-latest + if: > + github.event.pull_request.head.repo.full_name == github.repository && + github.actor != 'github-actions[bot]' + + steps: + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Fix conflicting gamevals + id: resolve + run: | + set +e + python3 tools/scripts/fix_gameval_conflicts.py | tee /tmp/gameval-fix-log.txt + code=${PIPESTATUS[0]} + set -e + if [ "$code" -eq 0 ]; then + echo "fixed=false" >> "$GITHUB_OUTPUT" + elif [ "$code" -eq 2 ]; then + echo "fixed=true" >> "$GITHUB_OUTPUT" + else + exit "$code" + fi + + - name: Commit and push fixes + if: steps.resolve.outputs.fixed == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git add -u -- content api .data/gamevals + + if git diff --cached --quiet; then + echo "Nothing staged." + exit 0 + fi + + git commit -m "fix(gamevals): reassign IDs conflicting with OSRS reserved range" + git push origin "HEAD:${{ github.head_ref }}" + + { + echo "## Gameval conflicts auto-fixed" + echo + echo "Reserved OSRS IDs come from \`.data/gamevals-binary/max-ids.toml\` (written on \`freshCache\`)." + echo "Custom gamevals must use IDs **greater than** each table's max." + echo + echo '```' + cat /tmp/gameval-fix-log.txt + echo '```' + } > /tmp/gameval-pr-body.md + + gh pr comment "${{ github.event.pull_request.number }}" --body-file /tmp/gameval-pr-body.md diff --git a/or-cache/src/main/kotlin/dev/openrune/gamevals/GameValMaxIdManifest.kt b/or-cache/src/main/kotlin/dev/openrune/gamevals/GameValMaxIdManifest.kt new file mode 100644 index 000000000..8c9e4ae77 --- /dev/null +++ b/or-cache/src/main/kotlin/dev/openrune/gamevals/GameValMaxIdManifest.kt @@ -0,0 +1,134 @@ +package dev.openrune.gamevals + +import java.io.DataInputStream +import java.io.File +import java.io.FileInputStream + +/** + * Snapshot of the highest OSRS cache gameval ID per table. + * + * IDs in `0..maxId` (inclusive) are reserved for the official cache. Custom gamevals must use + * IDs strictly greater than the table's max. + * + * Written by [GamevalDumper] during `freshCache`; committed so PR CI can detect conflicts without + * the binary `.dat` dumps. + */ +object GameValMaxIdManifest { + const val RELATIVE_PATH = ".data/gamevals-binary/max-ids.toml" + + data class Manifest( + val revision: Int?, + val maxIds: Map, + ) + + fun path(rootDir: File): File = File(rootDir, RELATIVE_PATH) + + fun write( + rootDir: File, + maxIds: Map, + revision: Int? = null, + ) { + val file = path(rootDir).apply { parentFile?.mkdirs() } + val body = + buildString { + appendLine("# OSRS cache gameval max IDs per table.") + appendLine("# IDs 0..=max are reserved; custom gamevals must be > max.") + appendLine("# Generated by freshCache / GamevalDumper - do not hand-edit.") + appendLine("# dbcol is omitted (auto-generated from table/column defs).") + appendLine() + if (revision != null) { + appendLine("revision = $revision") + appendLine() + } + appendLine("[max_ids]") + maxIds.toSortedMap().forEach { (table, maxId) -> + appendLine("$table = $maxId") + } + } + file.writeText(body) + } + + fun load(rootDir: File): Manifest { + val file = path(rootDir) + require(file.isFile) { + "Missing gameval max-id manifesto at ${file.invariantSeparatorsPath}. " + + "Run :or-cache:freshCache to generate it." + } + return parse(file.readText()) + } + + fun parse(text: String): Manifest { + var revision: Int? = null + val maxIds = linkedMapOf() + var inMaxIds = false + + text.lineSequence().forEach { raw -> + val line = raw.trim() + if (line.isEmpty() || line.startsWith("#")) return@forEach + when { + line == "[max_ids]" -> inMaxIds = true + line.startsWith("[") -> inMaxIds = false + line.startsWith("revision") && '=' in line && !inMaxIds -> { + revision = line.substringAfter('=').trim().toIntOrNull() + } + inMaxIds && '=' in line -> { + val key = line.substringBefore('=').trim() + val value = + line.substringAfter('=').trim().toIntOrNull() + ?: error("Invalid max id for '$key': $line") + maxIds[key] = value + } + } + } + + require(maxIds.isNotEmpty()) { "Gameval max-id manifesto has no [max_ids] entries." } + return Manifest(revision, maxIds) + } + + /** Decode max IDs from `gamevals.dat` only (`dbcol` / columns dat is auto-generated). */ + fun readMaxIdsFromDats(binaryDir: File): Map { + val maxIds = linkedMapOf() + val dat = File(binaryDir, "gamevals.dat") + require(dat.isFile) { "Missing gamevals.dat under ${binaryDir.invariantSeparatorsPath}" } + mergeMaxIdsFromDat(dat, maxIds) + maxIds.remove("dbcol") + return maxIds + } + + fun writeFromDats( + rootDir: File, + revision: Int? = null, + ) { + val binaryDir = File(rootDir, ".data/gamevals-binary") + write(rootDir, readMaxIdsFromDats(binaryDir), revision) + } + + private fun mergeMaxIdsFromDat( + datFile: File, + maxIds: MutableMap, + ) { + DataInputStream(FileInputStream(datFile)).use { input -> + val tableCount = input.readInt() + repeat(tableCount) { + val nameLength = input.readShort().toInt() + val nameBytes = ByteArray(nameLength) + input.readFully(nameBytes) + val tableName = String(nameBytes, Charsets.UTF_8) + + val itemCount = input.readInt() + var max = maxIds[tableName] ?: -1 + repeat(itemCount) { + val itemLength = input.readShort().toInt() + val itemBytes = ByteArray(itemLength) + input.readFully(itemBytes) + val itemString = String(itemBytes, Charsets.UTF_8) + val id = + itemString.substringAfterLast('=').trim().toIntOrNull() + ?: return@repeat + if (id > max) max = id + } + maxIds[tableName] = max + } + } + } +} diff --git a/or-cache/src/main/kotlin/dev/openrune/gamevals/GamevalDumper.kt b/or-cache/src/main/kotlin/dev/openrune/gamevals/GamevalDumper.kt index b78b51641..c4b80d339 100644 --- a/or-cache/src/main/kotlin/dev/openrune/gamevals/GamevalDumper.kt +++ b/or-cache/src/main/kotlin/dev/openrune/gamevals/GamevalDumper.kt @@ -65,6 +65,8 @@ object GamevalDumper { encodeGameValDat(File(outputDir, "gamevals.dat").path, gamevals) dumpCols(cache, rev) + + GameValMaxIdManifest.writeFromDats(File(".."), revision = rev) } fun dumpCols(cache: Cache, rev: Int) { diff --git a/tools/scripts/fix_gameval_conflicts.py b/tools/scripts/fix_gameval_conflicts.py new file mode 100644 index 000000000..358efe612 --- /dev/null +++ b/tools/scripts/fix_gameval_conflicts.py @@ -0,0 +1,285 @@ +#!/usr/bin/env python3 +"""Detect and rewrite custom gamevals that collide with OSRS reserved max IDs. + +Reads `.data/gamevals-binary/max-ids.toml` (written by freshCache). +IDs in 0..=max per table are reserved; custom entries must be > max. + +Exit codes: + 0 - no changes needed + 2 - files were rewritten + 1 - hard failure +""" + +from __future__ import annotations + +import argparse +import re +import sys +from collections import defaultdict +from pathlib import Path + +SECTION_RE = re.compile(r"^\s*\[gamevals\.([^.\]]+)\]\s*$") +KV_RE = re.compile(r"^(\s*)([^=]+?)(\s*=\s*)(-?\d+)(\s*)$") +RSCM_KV_RE = re.compile(r"^([^=]+?)=(-?\d+)\s*$") +MAX_ID = 65535 +# Packed from table/column defs — not authored as custom gamevals. +SKIP_TABLES = frozenset({"dbcol"}) + + +def repo_root() -> Path: + return Path(__file__).resolve().parents[2] + + +def load_max_ids(root: Path) -> dict[str, int]: + path = root / ".data" / "gamevals-binary" / "max-ids.toml" + if not path.is_file(): + raise SystemExit( + f"Missing {path.as_posix()}. Run :or-cache:freshCache to generate it." + ) + max_ids: dict[str, int] = {} + in_section = False + for raw in path.read_text(encoding="utf-8").splitlines(): + line = raw.strip() + if not line or line.startswith("#"): + continue + if line == "[max_ids]": + in_section = True + continue + if line.startswith("["): + in_section = False + continue + if in_section and "=" in line: + key, value = line.split("=", 1) + table = key.strip() + if table in SKIP_TABLES: + continue + max_ids[table] = int(value.strip()) + if not max_ids: + raise SystemExit(f"No [max_ids] entries in {path.as_posix()}") + return max_ids + + +def is_generated(path: Path) -> bool: + parts = {p.lower() for p in path.parts} + return bool(parts & {"build", "out", "target"}) + + +def iter_gameval_files(root: Path) -> list[Path]: + files: list[Path] = [] + for base in (root / "content", root / "api"): + if not base.is_dir(): + continue + for path in base.rglob("gamevals.toml"): + if not is_generated(path): + files.append(path) + gamevals_dir = root / ".data" / "gamevals" + if gamevals_dir.is_dir(): + files.extend( + sorted( + path + for path in gamevals_dir.glob("*.rscm") + if path.stem not in SKIP_TABLES + ) + ) + return files + + +def parse_toml_entries(path: Path) -> list[tuple[int, str, str, int, re.Match[str]]]: + """Return (line_index, table, key, value, match) for toml gamevals.""" + entries: list[tuple[int, str, str, int, re.Match[str]]] = [] + table: str | None = None + lines = path.read_text(encoding="utf-8").splitlines() + for idx, line in enumerate(lines): + section = SECTION_RE.match(line) + if section: + table = section.group(1) + if table in SKIP_TABLES: + table = None + continue + if table is None: + continue + match = KV_RE.match(line) + if not match: + continue + key = match.group(2).strip() + value = int(match.group(4)) + entries.append((idx, table, key, value, match)) + return entries + + +def parse_rscm_entries(path: Path) -> list[tuple[int, str, str, int, re.Match[str]]]: + table = path.stem + entries: list[tuple[int, str, str, int, re.Match[str]]] = [] + lines = path.read_text(encoding="utf-8").splitlines() + for idx, line in enumerate(lines): + if not line.strip() or line.lstrip().startswith("#"): + continue + match = RSCM_KV_RE.match(line.strip()) + if not match: + continue + key = match.group(1).strip() + value = int(match.group(2)) + entries.append((idx, table, key, value, match)) + return entries + + +def collect_entries(root: Path) -> dict[tuple[str, str], list[tuple[Path, int, int, str]]]: + """Map (table, key) -> list of (file, line_index, value, original_line).""" + by_key: dict[tuple[str, str], list[tuple[Path, int, int, str]]] = defaultdict(list) + for path in iter_gameval_files(root): + text_lines = path.read_text(encoding="utf-8").splitlines() + parsed = ( + parse_rscm_entries(path) + if path.suffix.lower() == ".rscm" + else parse_toml_entries(path) + ) + for idx, table, key, value, _match in parsed: + by_key[(table, key)].append((path, idx, value, text_lines[idx])) + return by_key + + +def preferred_value(locations: list[tuple[Path, int, int, str]]) -> int: + def sort_key(item: tuple[Path, int, int, str]) -> tuple[int, str, int]: + path, idx, _value, _line = item + toml_first = 0 if path.name == "gamevals.toml" else 1 + return (toml_first, path.as_posix().lower(), idx) + + return min(locations, key=sort_key)[2] + + +def plan_reassignments( + by_key: dict[tuple[str, str], list[tuple[Path, int, int, str]]], + max_ids: dict[str, int], +) -> dict[tuple[str, str], tuple[int, int, str]]: + """Return (table, key) -> (old_id, new_id, reason).""" + used: dict[str, set[int]] = defaultdict(set) + claimed: dict[str, set[int]] = defaultdict(set) + decisions: list[tuple[str, str, int | None, bool, str | None]] = [] + + for table, key in sorted(by_key.keys(), key=lambda tk: (tk[0], tk[1])): + locations = by_key[(table, key)] + value = preferred_value(locations) + floor = max_ids.get(table, -1) + 1 + + if value == -1: + decisions.append((table, key, None, True, "unassigned")) + elif value < floor: + decisions.append( + (table, key, None, True, f"reserved (id {value} <= max {floor - 1})") + ) + elif value in claimed[table]: + decisions.append((table, key, None, True, f"duplicate id {value}")) + else: + claimed[table].add(value) + used[table].add(value) + decisions.append((table, key, value, False, None)) + + plan: dict[tuple[str, str], tuple[int, int, str]] = {} + targets: dict[tuple[str, str], int] = {} + + for table, key, keep_id, needs_assign, reason in decisions: + table_key = (table, key) + if not needs_assign: + if keep_id is not None: + targets[table_key] = keep_id + continue + + floor = max_ids.get(table, -1) + 1 + new_id = next((i for i in range(MAX_ID, floor - 1, -1) if i not in used[table]), None) + if new_id is None: + raise SystemExit(f"No free IDs for table '{table}' in range [{floor}..{MAX_ID}]") + used[table].add(new_id) + claimed[table].add(new_id) + targets[table_key] = new_id + old_id = preferred_value(by_key[table_key]) + plan[table_key] = (old_id, new_id, reason or "unassigned") + + # Sync divergent copies of the same key to the chosen target. + for table_key, locations in by_key.items(): + target = targets.get(table_key) + if target is None: + continue + if any(value != target for _p, _i, value, _l in locations) and table_key not in plan: + plan[table_key] = (locations[0][2], target, "sync divergent id") + + return plan + + +def rewrite_line(line: str, new_id: int, rscm: bool) -> str: + if rscm: + match = RSCM_KV_RE.match(line.strip()) + if not match: + return line + prefix = line[: len(line) - len(line.lstrip())] + return f"{prefix}{match.group(1).rstrip()}={new_id}" + match = KV_RE.match(line) + if not match: + return line + return f"{match.group(1)}{match.group(2)}{match.group(3)}{new_id}{match.group(5)}" + + +def apply_plan( + root: Path, + by_key: dict[tuple[str, str], list[tuple[Path, int, int, str]]], + plan: dict[tuple[str, str], tuple[int, int, str]], + write: bool, +) -> list[Path]: + file_lines: dict[Path, list[str]] = {} + changed: set[Path] = set() + + for table_key, (_old, new_id, _reason) in plan.items(): + for path, idx, value, _line in by_key[table_key]: + if value == new_id: + continue + if path not in file_lines: + text = path.read_text(encoding="utf-8") + file_lines[path] = text.splitlines() + # Preserve final newline decision later via original text + lines = file_lines[path] + updated = rewrite_line(lines[idx], new_id, path.suffix.lower() == ".rscm") + if updated != lines[idx]: + lines[idx] = updated + changed.add(path) + + if write: + for path, lines in file_lines.items(): + if path not in changed: + continue + original = path.read_text(encoding="utf-8") + ending = "\n" if original.endswith("\n") else "" + path.write_text("\n".join(lines) + ending, encoding="utf-8") + + return sorted(changed, key=lambda p: p.as_posix().lower()) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", type=Path, default=None) + parser.add_argument( + "--check", + action="store_true", + help="Report conflicts without writing files", + ) + args = parser.parse_args() + root = (args.root or repo_root()).resolve() + + max_ids = load_max_ids(root) + by_key = collect_entries(root) + plan = plan_reassignments(by_key, max_ids) + + if not plan: + print("No gameval conflicts or unassigned IDs found.") + return 0 + + changed = apply_plan(root, by_key, plan, write=not args.check) + print(f"Gameval reassignments ({len(plan)}):") + for (table, key), (old_id, new_id, reason) in sorted(plan.items()): + print(f" {table}.{key}: {old_id} -> {new_id} ({reason})") + print(f"{'Would update' if args.check else 'Updated'} {len(changed)} file(s):") + for path in changed: + print(f" {path.relative_to(root).as_posix()}") + return 2 + + +if __name__ == "__main__": + sys.exit(main()) From 9410fdff068eda4ff121b0c1925d79c7e64cb525 Mon Sep 17 00:00:00 2001 From: Mark7625 <72366279+Mark7625@users.noreply.github.com> Date: Tue, 4 Aug 2026 19:49:17 +0100 Subject: [PATCH 02/11] d --- .../other/consumables/src/main/kotlin/resources/gamevals.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/content/other/consumables/src/main/kotlin/resources/gamevals.toml b/content/other/consumables/src/main/kotlin/resources/gamevals.toml index 0c81322f2..a72e715f5 100644 --- a/content/other/consumables/src/main/kotlin/resources/gamevals.toml +++ b/content/other/consumables/src/main/kotlin/resources/gamevals.toml @@ -65,6 +65,7 @@ ugthanki_kebab=64535 kebab=64534 locust_meat=64533 roe=64532 +roedddddddddd=64532 stew=64531 spicy_stew=64530 cooked_rabbit=64529 @@ -480,4 +481,4 @@ effect_toa_tears_of_elidinis=64136 effect_toa_liquid_adrenaline=64135 effect_toa_smelling_salts=64134 effect_toa_silk_dressing=64133 -effect_toa_blessed_crystal_scarab=64132 \ No newline at end of file +effect_toa_blessed_crystal_scarab=64132 From 7b45ba54ada5a7ecbd6e1f63fe1ff5ddb719410b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 4 Aug 2026 18:49:51 +0000 Subject: [PATCH 03/11] fix(gamevals): reassign IDs conflicting with OSRS reserved range --- .../other/consumables/src/main/kotlin/resources/gamevals.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/other/consumables/src/main/kotlin/resources/gamevals.toml b/content/other/consumables/src/main/kotlin/resources/gamevals.toml index a72e715f5..b7e18b2f3 100644 --- a/content/other/consumables/src/main/kotlin/resources/gamevals.toml +++ b/content/other/consumables/src/main/kotlin/resources/gamevals.toml @@ -65,7 +65,7 @@ ugthanki_kebab=64535 kebab=64534 locust_meat=64533 roe=64532 -roedddddddddd=64532 +roedddddddddd=64130 stew=64531 spicy_stew=64530 cooked_rabbit=64529 From abb766333dd989db5a8bc767675ebfa66053aa88 Mon Sep 17 00:00:00 2001 From: Mark7625 <72366279+Mark7625@users.noreply.github.com> Date: Tue, 4 Aug 2026 19:48:32 +0100 Subject: [PATCH 04/11] Ci for conflicting gamevals --- .data/.gitignore | 4 + .data/gamevals-binary/max-ids.toml | 22 ++ .github/workflows/gameval-conflicts.yml | 86 ++++++ .../openrune/gamevals/GameValMaxIdManifest.kt | 134 ++++++++ .../dev/openrune/gamevals/GamevalDumper.kt | 2 + tools/scripts/fix_gameval_conflicts.py | 285 ++++++++++++++++++ 6 files changed, 533 insertions(+) create mode 100644 .data/gamevals-binary/max-ids.toml create mode 100644 .github/workflows/gameval-conflicts.yml create mode 100644 or-cache/src/main/kotlin/dev/openrune/gamevals/GameValMaxIdManifest.kt create mode 100644 tools/scripts/fix_gameval_conflicts.py diff --git a/.data/.gitignore b/.data/.gitignore index b55332aa6..218bfec93 100644 --- a/.data/.gitignore +++ b/.data/.gitignore @@ -1,6 +1,10 @@ /* symbols/.local/ !gamevals/ +!gamevals-binary/ !raw-cache/ !symbols/ !.gitignore + +gamevals-binary/* +!gamevals-binary/max-ids.toml diff --git a/.data/gamevals-binary/max-ids.toml b/.data/gamevals-binary/max-ids.toml new file mode 100644 index 000000000..48464519c --- /dev/null +++ b/.data/gamevals-binary/max-ids.toml @@ -0,0 +1,22 @@ +# OSRS cache gameval max IDs per table. +# IDs 0..=max are reserved; custom gamevals must be > max. +# Generated by freshCache / GamevalDumper - do not hand-edit. +# dbcol is omitted (auto-generated from table/column defs). + +revision = 239 + +[max_ids] +dbrow = 16939 +dbtable = 258 +interface = 968 +inv = 1027 +jingle = 1196 +loc = 62399 +npc = 16337 +obj = 34058 +seq = 14474 +spotanim = 4017 +sprites = 8559 +varbit = 20396 +varcs = 1506 +varp = 5724 diff --git a/.github/workflows/gameval-conflicts.yml b/.github/workflows/gameval-conflicts.yml new file mode 100644 index 000000000..9bd407bd0 --- /dev/null +++ b/.github/workflows/gameval-conflicts.yml @@ -0,0 +1,86 @@ +name: Gameval Conflict Check + +# Runs on every PR so it can be a required status check (blocks merge until it completes). +# Auto-fixes conflicting gamevals and pushes — no environment approval, no PR comments when clean. + +on: + pull_request: + types: [opened, synchronize, reopened] + +concurrency: + group: gameval-conflicts-${{ github.event.pull_request.number }} + cancel-in-progress: true + +permissions: + contents: write + checks: write + +jobs: + gameval-conflicts: + runs-on: ubuntu-latest + # No environment: — do not gate fixes behind approval. + + steps: + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Fix conflicting gamevals + id: resolve + run: | + set +e + python3 tools/scripts/fix_gameval_conflicts.py | tee /tmp/gameval-fix-log.txt + code=${PIPESTATUS[0]} + set -e + if [ "$code" -eq 0 ]; then + echo "fixed=false" >> "$GITHUB_OUTPUT" + elif [ "$code" -eq 2 ]; then + echo "fixed=true" >> "$GITHUB_OUTPUT" + else + exit "$code" + fi + + - name: Commit and push fixes + if: steps.resolve.outputs.fixed == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + SAME_REPO="${{ github.event.pull_request.head.repo.full_name == github.repository }}" + if [ "$SAME_REPO" != "true" ]; then + echo "::error::Conflicting gamevals on a fork PR — push fixes to the branch (or open the PR from this repo)." + cat /tmp/gameval-fix-log.txt + exit 1 + fi + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git add -u -- content api .data/gamevals + + if git diff --cached --quiet; then + echo "Nothing staged." + exit 0 + fi + + git commit -m "fix(gamevals): reassign IDs conflicting with OSRS reserved range" + git push origin "HEAD:${{ github.head_ref }}" + + # GITHUB_TOKEN pushes do not re-trigger workflows; mark the new head so a + # required "gameval-conflicts" check is not stuck waiting on this SHA. + NEW_SHA="$(git rev-parse HEAD)" + jq -n \ + --arg name "Gameval Conflict Check / gameval-conflicts" \ + --arg sha "$NEW_SHA" \ + '{ + name: $name, + head_sha: $sha, + status: "completed", + conclusion: "success", + output: { + title: "Gameval conflicts auto-fixed", + summary: "Conflicting custom gamevals were reassigned and pushed." + } + }' | gh api "repos/${{ github.repository }}/check-runs" --input - diff --git a/or-cache/src/main/kotlin/dev/openrune/gamevals/GameValMaxIdManifest.kt b/or-cache/src/main/kotlin/dev/openrune/gamevals/GameValMaxIdManifest.kt new file mode 100644 index 000000000..8c9e4ae77 --- /dev/null +++ b/or-cache/src/main/kotlin/dev/openrune/gamevals/GameValMaxIdManifest.kt @@ -0,0 +1,134 @@ +package dev.openrune.gamevals + +import java.io.DataInputStream +import java.io.File +import java.io.FileInputStream + +/** + * Snapshot of the highest OSRS cache gameval ID per table. + * + * IDs in `0..maxId` (inclusive) are reserved for the official cache. Custom gamevals must use + * IDs strictly greater than the table's max. + * + * Written by [GamevalDumper] during `freshCache`; committed so PR CI can detect conflicts without + * the binary `.dat` dumps. + */ +object GameValMaxIdManifest { + const val RELATIVE_PATH = ".data/gamevals-binary/max-ids.toml" + + data class Manifest( + val revision: Int?, + val maxIds: Map, + ) + + fun path(rootDir: File): File = File(rootDir, RELATIVE_PATH) + + fun write( + rootDir: File, + maxIds: Map, + revision: Int? = null, + ) { + val file = path(rootDir).apply { parentFile?.mkdirs() } + val body = + buildString { + appendLine("# OSRS cache gameval max IDs per table.") + appendLine("# IDs 0..=max are reserved; custom gamevals must be > max.") + appendLine("# Generated by freshCache / GamevalDumper - do not hand-edit.") + appendLine("# dbcol is omitted (auto-generated from table/column defs).") + appendLine() + if (revision != null) { + appendLine("revision = $revision") + appendLine() + } + appendLine("[max_ids]") + maxIds.toSortedMap().forEach { (table, maxId) -> + appendLine("$table = $maxId") + } + } + file.writeText(body) + } + + fun load(rootDir: File): Manifest { + val file = path(rootDir) + require(file.isFile) { + "Missing gameval max-id manifesto at ${file.invariantSeparatorsPath}. " + + "Run :or-cache:freshCache to generate it." + } + return parse(file.readText()) + } + + fun parse(text: String): Manifest { + var revision: Int? = null + val maxIds = linkedMapOf() + var inMaxIds = false + + text.lineSequence().forEach { raw -> + val line = raw.trim() + if (line.isEmpty() || line.startsWith("#")) return@forEach + when { + line == "[max_ids]" -> inMaxIds = true + line.startsWith("[") -> inMaxIds = false + line.startsWith("revision") && '=' in line && !inMaxIds -> { + revision = line.substringAfter('=').trim().toIntOrNull() + } + inMaxIds && '=' in line -> { + val key = line.substringBefore('=').trim() + val value = + line.substringAfter('=').trim().toIntOrNull() + ?: error("Invalid max id for '$key': $line") + maxIds[key] = value + } + } + } + + require(maxIds.isNotEmpty()) { "Gameval max-id manifesto has no [max_ids] entries." } + return Manifest(revision, maxIds) + } + + /** Decode max IDs from `gamevals.dat` only (`dbcol` / columns dat is auto-generated). */ + fun readMaxIdsFromDats(binaryDir: File): Map { + val maxIds = linkedMapOf() + val dat = File(binaryDir, "gamevals.dat") + require(dat.isFile) { "Missing gamevals.dat under ${binaryDir.invariantSeparatorsPath}" } + mergeMaxIdsFromDat(dat, maxIds) + maxIds.remove("dbcol") + return maxIds + } + + fun writeFromDats( + rootDir: File, + revision: Int? = null, + ) { + val binaryDir = File(rootDir, ".data/gamevals-binary") + write(rootDir, readMaxIdsFromDats(binaryDir), revision) + } + + private fun mergeMaxIdsFromDat( + datFile: File, + maxIds: MutableMap, + ) { + DataInputStream(FileInputStream(datFile)).use { input -> + val tableCount = input.readInt() + repeat(tableCount) { + val nameLength = input.readShort().toInt() + val nameBytes = ByteArray(nameLength) + input.readFully(nameBytes) + val tableName = String(nameBytes, Charsets.UTF_8) + + val itemCount = input.readInt() + var max = maxIds[tableName] ?: -1 + repeat(itemCount) { + val itemLength = input.readShort().toInt() + val itemBytes = ByteArray(itemLength) + input.readFully(itemBytes) + val itemString = String(itemBytes, Charsets.UTF_8) + val id = + itemString.substringAfterLast('=').trim().toIntOrNull() + ?: return@repeat + if (id > max) max = id + } + maxIds[tableName] = max + } + } + } +} diff --git a/or-cache/src/main/kotlin/dev/openrune/gamevals/GamevalDumper.kt b/or-cache/src/main/kotlin/dev/openrune/gamevals/GamevalDumper.kt index b78b51641..c4b80d339 100644 --- a/or-cache/src/main/kotlin/dev/openrune/gamevals/GamevalDumper.kt +++ b/or-cache/src/main/kotlin/dev/openrune/gamevals/GamevalDumper.kt @@ -65,6 +65,8 @@ object GamevalDumper { encodeGameValDat(File(outputDir, "gamevals.dat").path, gamevals) dumpCols(cache, rev) + + GameValMaxIdManifest.writeFromDats(File(".."), revision = rev) } fun dumpCols(cache: Cache, rev: Int) { diff --git a/tools/scripts/fix_gameval_conflicts.py b/tools/scripts/fix_gameval_conflicts.py new file mode 100644 index 000000000..358efe612 --- /dev/null +++ b/tools/scripts/fix_gameval_conflicts.py @@ -0,0 +1,285 @@ +#!/usr/bin/env python3 +"""Detect and rewrite custom gamevals that collide with OSRS reserved max IDs. + +Reads `.data/gamevals-binary/max-ids.toml` (written by freshCache). +IDs in 0..=max per table are reserved; custom entries must be > max. + +Exit codes: + 0 - no changes needed + 2 - files were rewritten + 1 - hard failure +""" + +from __future__ import annotations + +import argparse +import re +import sys +from collections import defaultdict +from pathlib import Path + +SECTION_RE = re.compile(r"^\s*\[gamevals\.([^.\]]+)\]\s*$") +KV_RE = re.compile(r"^(\s*)([^=]+?)(\s*=\s*)(-?\d+)(\s*)$") +RSCM_KV_RE = re.compile(r"^([^=]+?)=(-?\d+)\s*$") +MAX_ID = 65535 +# Packed from table/column defs — not authored as custom gamevals. +SKIP_TABLES = frozenset({"dbcol"}) + + +def repo_root() -> Path: + return Path(__file__).resolve().parents[2] + + +def load_max_ids(root: Path) -> dict[str, int]: + path = root / ".data" / "gamevals-binary" / "max-ids.toml" + if not path.is_file(): + raise SystemExit( + f"Missing {path.as_posix()}. Run :or-cache:freshCache to generate it." + ) + max_ids: dict[str, int] = {} + in_section = False + for raw in path.read_text(encoding="utf-8").splitlines(): + line = raw.strip() + if not line or line.startswith("#"): + continue + if line == "[max_ids]": + in_section = True + continue + if line.startswith("["): + in_section = False + continue + if in_section and "=" in line: + key, value = line.split("=", 1) + table = key.strip() + if table in SKIP_TABLES: + continue + max_ids[table] = int(value.strip()) + if not max_ids: + raise SystemExit(f"No [max_ids] entries in {path.as_posix()}") + return max_ids + + +def is_generated(path: Path) -> bool: + parts = {p.lower() for p in path.parts} + return bool(parts & {"build", "out", "target"}) + + +def iter_gameval_files(root: Path) -> list[Path]: + files: list[Path] = [] + for base in (root / "content", root / "api"): + if not base.is_dir(): + continue + for path in base.rglob("gamevals.toml"): + if not is_generated(path): + files.append(path) + gamevals_dir = root / ".data" / "gamevals" + if gamevals_dir.is_dir(): + files.extend( + sorted( + path + for path in gamevals_dir.glob("*.rscm") + if path.stem not in SKIP_TABLES + ) + ) + return files + + +def parse_toml_entries(path: Path) -> list[tuple[int, str, str, int, re.Match[str]]]: + """Return (line_index, table, key, value, match) for toml gamevals.""" + entries: list[tuple[int, str, str, int, re.Match[str]]] = [] + table: str | None = None + lines = path.read_text(encoding="utf-8").splitlines() + for idx, line in enumerate(lines): + section = SECTION_RE.match(line) + if section: + table = section.group(1) + if table in SKIP_TABLES: + table = None + continue + if table is None: + continue + match = KV_RE.match(line) + if not match: + continue + key = match.group(2).strip() + value = int(match.group(4)) + entries.append((idx, table, key, value, match)) + return entries + + +def parse_rscm_entries(path: Path) -> list[tuple[int, str, str, int, re.Match[str]]]: + table = path.stem + entries: list[tuple[int, str, str, int, re.Match[str]]] = [] + lines = path.read_text(encoding="utf-8").splitlines() + for idx, line in enumerate(lines): + if not line.strip() or line.lstrip().startswith("#"): + continue + match = RSCM_KV_RE.match(line.strip()) + if not match: + continue + key = match.group(1).strip() + value = int(match.group(2)) + entries.append((idx, table, key, value, match)) + return entries + + +def collect_entries(root: Path) -> dict[tuple[str, str], list[tuple[Path, int, int, str]]]: + """Map (table, key) -> list of (file, line_index, value, original_line).""" + by_key: dict[tuple[str, str], list[tuple[Path, int, int, str]]] = defaultdict(list) + for path in iter_gameval_files(root): + text_lines = path.read_text(encoding="utf-8").splitlines() + parsed = ( + parse_rscm_entries(path) + if path.suffix.lower() == ".rscm" + else parse_toml_entries(path) + ) + for idx, table, key, value, _match in parsed: + by_key[(table, key)].append((path, idx, value, text_lines[idx])) + return by_key + + +def preferred_value(locations: list[tuple[Path, int, int, str]]) -> int: + def sort_key(item: tuple[Path, int, int, str]) -> tuple[int, str, int]: + path, idx, _value, _line = item + toml_first = 0 if path.name == "gamevals.toml" else 1 + return (toml_first, path.as_posix().lower(), idx) + + return min(locations, key=sort_key)[2] + + +def plan_reassignments( + by_key: dict[tuple[str, str], list[tuple[Path, int, int, str]]], + max_ids: dict[str, int], +) -> dict[tuple[str, str], tuple[int, int, str]]: + """Return (table, key) -> (old_id, new_id, reason).""" + used: dict[str, set[int]] = defaultdict(set) + claimed: dict[str, set[int]] = defaultdict(set) + decisions: list[tuple[str, str, int | None, bool, str | None]] = [] + + for table, key in sorted(by_key.keys(), key=lambda tk: (tk[0], tk[1])): + locations = by_key[(table, key)] + value = preferred_value(locations) + floor = max_ids.get(table, -1) + 1 + + if value == -1: + decisions.append((table, key, None, True, "unassigned")) + elif value < floor: + decisions.append( + (table, key, None, True, f"reserved (id {value} <= max {floor - 1})") + ) + elif value in claimed[table]: + decisions.append((table, key, None, True, f"duplicate id {value}")) + else: + claimed[table].add(value) + used[table].add(value) + decisions.append((table, key, value, False, None)) + + plan: dict[tuple[str, str], tuple[int, int, str]] = {} + targets: dict[tuple[str, str], int] = {} + + for table, key, keep_id, needs_assign, reason in decisions: + table_key = (table, key) + if not needs_assign: + if keep_id is not None: + targets[table_key] = keep_id + continue + + floor = max_ids.get(table, -1) + 1 + new_id = next((i for i in range(MAX_ID, floor - 1, -1) if i not in used[table]), None) + if new_id is None: + raise SystemExit(f"No free IDs for table '{table}' in range [{floor}..{MAX_ID}]") + used[table].add(new_id) + claimed[table].add(new_id) + targets[table_key] = new_id + old_id = preferred_value(by_key[table_key]) + plan[table_key] = (old_id, new_id, reason or "unassigned") + + # Sync divergent copies of the same key to the chosen target. + for table_key, locations in by_key.items(): + target = targets.get(table_key) + if target is None: + continue + if any(value != target for _p, _i, value, _l in locations) and table_key not in plan: + plan[table_key] = (locations[0][2], target, "sync divergent id") + + return plan + + +def rewrite_line(line: str, new_id: int, rscm: bool) -> str: + if rscm: + match = RSCM_KV_RE.match(line.strip()) + if not match: + return line + prefix = line[: len(line) - len(line.lstrip())] + return f"{prefix}{match.group(1).rstrip()}={new_id}" + match = KV_RE.match(line) + if not match: + return line + return f"{match.group(1)}{match.group(2)}{match.group(3)}{new_id}{match.group(5)}" + + +def apply_plan( + root: Path, + by_key: dict[tuple[str, str], list[tuple[Path, int, int, str]]], + plan: dict[tuple[str, str], tuple[int, int, str]], + write: bool, +) -> list[Path]: + file_lines: dict[Path, list[str]] = {} + changed: set[Path] = set() + + for table_key, (_old, new_id, _reason) in plan.items(): + for path, idx, value, _line in by_key[table_key]: + if value == new_id: + continue + if path not in file_lines: + text = path.read_text(encoding="utf-8") + file_lines[path] = text.splitlines() + # Preserve final newline decision later via original text + lines = file_lines[path] + updated = rewrite_line(lines[idx], new_id, path.suffix.lower() == ".rscm") + if updated != lines[idx]: + lines[idx] = updated + changed.add(path) + + if write: + for path, lines in file_lines.items(): + if path not in changed: + continue + original = path.read_text(encoding="utf-8") + ending = "\n" if original.endswith("\n") else "" + path.write_text("\n".join(lines) + ending, encoding="utf-8") + + return sorted(changed, key=lambda p: p.as_posix().lower()) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", type=Path, default=None) + parser.add_argument( + "--check", + action="store_true", + help="Report conflicts without writing files", + ) + args = parser.parse_args() + root = (args.root or repo_root()).resolve() + + max_ids = load_max_ids(root) + by_key = collect_entries(root) + plan = plan_reassignments(by_key, max_ids) + + if not plan: + print("No gameval conflicts or unassigned IDs found.") + return 0 + + changed = apply_plan(root, by_key, plan, write=not args.check) + print(f"Gameval reassignments ({len(plan)}):") + for (table, key), (old_id, new_id, reason) in sorted(plan.items()): + print(f" {table}.{key}: {old_id} -> {new_id} ({reason})") + print(f"{'Would update' if args.check else 'Updated'} {len(changed)} file(s):") + for path in changed: + print(f" {path.relative_to(root).as_posix()}") + return 2 + + +if __name__ == "__main__": + sys.exit(main()) From c713b2611481f3e7ffae833035bd3249e15afc7b Mon Sep 17 00:00:00 2001 From: Mark7625 <72366279+Mark7625@users.noreply.github.com> Date: Tue, 4 Aug 2026 19:55:25 +0100 Subject: [PATCH 05/11] Update gamevals.toml --- .../consumables/src/main/kotlin/resources/gamevals.toml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/content/other/consumables/src/main/kotlin/resources/gamevals.toml b/content/other/consumables/src/main/kotlin/resources/gamevals.toml index b7e18b2f3..fe7fcf757 100644 --- a/content/other/consumables/src/main/kotlin/resources/gamevals.toml +++ b/content/other/consumables/src/main/kotlin/resources/gamevals.toml @@ -65,7 +65,12 @@ ugthanki_kebab=64535 kebab=64534 locust_meat=64533 roe=64532 -roedddddddddd=64130 +roedddddddddd=64532 +roedddddddddd2=64532 +roedddddddddd3=64532 +roedddddddddd34=64532 +roedddddddddd45=64532 +roedddddddddd55=64532 stew=64531 spicy_stew=64530 cooked_rabbit=64529 From a111f7e436b0c40f976c0fc4c128f191efd16cbf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 4 Aug 2026 18:56:11 +0000 Subject: [PATCH 06/11] fix(gamevals): reassign IDs conflicting with OSRS reserved range --- .../src/main/kotlin/resources/gamevals.toml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/content/other/consumables/src/main/kotlin/resources/gamevals.toml b/content/other/consumables/src/main/kotlin/resources/gamevals.toml index fe7fcf757..985faa758 100644 --- a/content/other/consumables/src/main/kotlin/resources/gamevals.toml +++ b/content/other/consumables/src/main/kotlin/resources/gamevals.toml @@ -65,12 +65,12 @@ ugthanki_kebab=64535 kebab=64534 locust_meat=64533 roe=64532 -roedddddddddd=64532 -roedddddddddd2=64532 -roedddddddddd3=64532 -roedddddddddd34=64532 -roedddddddddd45=64532 -roedddddddddd55=64532 +roedddddddddd=64130 +roedddddddddd2=64129 +roedddddddddd3=64128 +roedddddddddd34=64127 +roedddddddddd45=64126 +roedddddddddd55=64125 stew=64531 spicy_stew=64530 cooked_rabbit=64529 From 94fbc023a80fe9cc5b2c8cfef8854dab637af4f8 Mon Sep 17 00:00:00 2001 From: Mark7625 <72366279+Mark7625@users.noreply.github.com> Date: Tue, 4 Aug 2026 19:57:58 +0100 Subject: [PATCH 07/11] Update gameval-conflicts.yml --- .github/workflows/gameval-conflicts.yml | 34 ++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gameval-conflicts.yml b/.github/workflows/gameval-conflicts.yml index 9bd407bd0..da3eb206e 100644 --- a/.github/workflows/gameval-conflicts.yml +++ b/.github/workflows/gameval-conflicts.yml @@ -1,7 +1,8 @@ name: Gameval Conflict Check # Runs on every PR so it can be a required status check (blocks merge until it completes). -# Auto-fixes conflicting gamevals and pushes — no environment approval, no PR comments when clean. +# Auto-fixes conflicting gamevals and pushes — no environment approval. +# Comments on the PR only when conflicts were found/fixed; silent when clean. on: pull_request: @@ -14,6 +15,7 @@ concurrency: permissions: contents: write checks: write + pull-requests: write jobs: gameval-conflicts: @@ -43,7 +45,7 @@ jobs: exit "$code" fi - - name: Commit and push fixes + - name: Commit, push, and report conflicts if: steps.resolve.outputs.fixed == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -52,6 +54,16 @@ jobs: if [ "$SAME_REPO" != "true" ]; then echo "::error::Conflicting gamevals on a fork PR — push fixes to the branch (or open the PR from this repo)." cat /tmp/gameval-fix-log.txt + { + echo "## Gameval conflicts found" + echo + echo "Could not push fixes (fork PR). Please apply the reassignments below on your branch." + echo + echo '```' + cat /tmp/gameval-fix-log.txt + echo '```' + } > /tmp/gameval-pr-body.md + gh pr comment "${{ github.event.pull_request.number }}" --body-file /tmp/gameval-pr-body.md exit 1 fi @@ -74,6 +86,7 @@ jobs: jq -n \ --arg name "Gameval Conflict Check / gameval-conflicts" \ --arg sha "$NEW_SHA" \ + --arg summary "$(cat /tmp/gameval-fix-log.txt)" \ '{ name: $name, head_sha: $sha, @@ -81,6 +94,21 @@ jobs: conclusion: "success", output: { title: "Gameval conflicts auto-fixed", - summary: "Conflicting custom gamevals were reassigned and pushed." + summary: $summary } }' | gh api "repos/${{ github.repository }}/check-runs" --input - + + { + echo "## Gameval conflicts auto-fixed" + echo + echo "Reserved OSRS IDs come from \`.data/gamevals-binary/max-ids.toml\` (written on \`freshCache\`)." + echo "Custom gamevals must use IDs **greater than** each table's max." + echo + echo "A fix commit was pushed with these reassignments:" + echo + echo '```' + cat /tmp/gameval-fix-log.txt + echo '```' + } > /tmp/gameval-pr-body.md + + gh pr comment "${{ github.event.pull_request.number }}" --body-file /tmp/gameval-pr-body.md From fff7b8df1b114c2676dbaed56ed9178cc1d5de52 Mon Sep 17 00:00:00 2001 From: Mark7625 <72366279+Mark7625@users.noreply.github.com> Date: Tue, 4 Aug 2026 19:58:10 +0100 Subject: [PATCH 08/11] Update gamevals.toml --- .../src/main/kotlin/resources/gamevals.toml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/content/other/consumables/src/main/kotlin/resources/gamevals.toml b/content/other/consumables/src/main/kotlin/resources/gamevals.toml index 985faa758..fe7fcf757 100644 --- a/content/other/consumables/src/main/kotlin/resources/gamevals.toml +++ b/content/other/consumables/src/main/kotlin/resources/gamevals.toml @@ -65,12 +65,12 @@ ugthanki_kebab=64535 kebab=64534 locust_meat=64533 roe=64532 -roedddddddddd=64130 -roedddddddddd2=64129 -roedddddddddd3=64128 -roedddddddddd34=64127 -roedddddddddd45=64126 -roedddddddddd55=64125 +roedddddddddd=64532 +roedddddddddd2=64532 +roedddddddddd3=64532 +roedddddddddd34=64532 +roedddddddddd45=64532 +roedddddddddd55=64532 stew=64531 spicy_stew=64530 cooked_rabbit=64529 From d13f24ab4dca8df7ec43a38487e5f7f2a7c9c3cd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 4 Aug 2026 18:58:20 +0000 Subject: [PATCH 09/11] fix(gamevals): reassign IDs conflicting with OSRS reserved range --- .../src/main/kotlin/resources/gamevals.toml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/content/other/consumables/src/main/kotlin/resources/gamevals.toml b/content/other/consumables/src/main/kotlin/resources/gamevals.toml index fe7fcf757..985faa758 100644 --- a/content/other/consumables/src/main/kotlin/resources/gamevals.toml +++ b/content/other/consumables/src/main/kotlin/resources/gamevals.toml @@ -65,12 +65,12 @@ ugthanki_kebab=64535 kebab=64534 locust_meat=64533 roe=64532 -roedddddddddd=64532 -roedddddddddd2=64532 -roedddddddddd3=64532 -roedddddddddd34=64532 -roedddddddddd45=64532 -roedddddddddd55=64532 +roedddddddddd=64130 +roedddddddddd2=64129 +roedddddddddd3=64128 +roedddddddddd34=64127 +roedddddddddd45=64126 +roedddddddddd55=64125 stew=64531 spicy_stew=64530 cooked_rabbit=64529 From 04a7c26148e1eb036ca2b55321a5a90412b17f8b Mon Sep 17 00:00:00 2001 From: Mark7625 <72366279+Mark7625@users.noreply.github.com> Date: Tue, 4 Aug 2026 20:00:39 +0100 Subject: [PATCH 10/11] Update gamevals.toml --- .../other/consumables/src/main/kotlin/resources/gamevals.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/other/consumables/src/main/kotlin/resources/gamevals.toml b/content/other/consumables/src/main/kotlin/resources/gamevals.toml index 985faa758..549f9a723 100644 --- a/content/other/consumables/src/main/kotlin/resources/gamevals.toml +++ b/content/other/consumables/src/main/kotlin/resources/gamevals.toml @@ -65,7 +65,7 @@ ugthanki_kebab=64535 kebab=64534 locust_meat=64533 roe=64532 -roedddddddddd=64130 +roedddddddddd=64532 roedddddddddd2=64129 roedddddddddd3=64128 roedddddddddd34=64127 From 20d98f162f555ca5f158d30a046b28d77c320b4f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 4 Aug 2026 19:01:01 +0000 Subject: [PATCH 11/11] fix(gamevals): reassign IDs conflicting with OSRS reserved range --- .../other/consumables/src/main/kotlin/resources/gamevals.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/other/consumables/src/main/kotlin/resources/gamevals.toml b/content/other/consumables/src/main/kotlin/resources/gamevals.toml index 549f9a723..985faa758 100644 --- a/content/other/consumables/src/main/kotlin/resources/gamevals.toml +++ b/content/other/consumables/src/main/kotlin/resources/gamevals.toml @@ -65,7 +65,7 @@ ugthanki_kebab=64535 kebab=64534 locust_meat=64533 roe=64532 -roedddddddddd=64532 +roedddddddddd=64130 roedddddddddd2=64129 roedddddddddd3=64128 roedddddddddd34=64127