Skip to content

Fix internal map paths in coop mission .scmap files on deploy#327

Open
TimMasalme wants to merge 2 commits into
FAForever:developfrom
TimMasalme:ci/verify-scmap-fixer
Open

Fix internal map paths in coop mission .scmap files on deploy#327
TimMasalme wants to merge 2 commits into
FAForever:developfrom
TimMasalme:ci/verify-scmap-fixer

Conversation

@TimMasalme

@TimMasalme TimMasalme commented Jul 26, 2026

Copy link
Copy Markdown

Summary by CodeRabbit

  • New Features

    • Added support for updating embedded map paths in .scmap files during legacy deployment packaging.
    • Preserved external and unrelated map references while updating applicable mission-specific paths.
  • Bug Fixes

    • Improved map change detection and packaged ZIP contents for deployments containing .scmap files.
  • Chores

    • Added an automated workflow to compile legacy deployment scripts on code changes or manual runs.

Seraphimcommander and others added 2 commits July 26, 2026 19:57
getFileContent() only rewrote /maps/<folder>/ in text files, so the paths
embedded in the .scmap binary kept pointing at the unversioned folder and the
affected missions lost their custom textures after a release.

ScmapPathFixer reads and rewrites the .scmap byte by byte, ported from speed2's
sc_map_parser.gd, and inserts the release version into the map folder segment.

Only paths that point at the mission's own folder are touched. Several missions
deliberately reference a base game map (/maps/X1CA_001/X1CA_001.scmap) or a
texture of another map, and versioning those would break them. A map file is
only parsed at all when a raw byte scan finds "/maps/<own folder>", which also
keeps the placeholder .scmap files of the missions using a base game map out of
the parser.

Affects 4 of the 42 deployed missions: Operation_Blockade, Tha_Atha_Aez,
Golden_Crystals and Overlord_Surth_Velsok. Their checksums change, so they get
one version bump on the first run after this lands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Not part of the pull request, only used to verify the Kotlin builds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The legacy deployment scripts now detect and selectively rewrite embedded paths in .scmap files. A binary SCMap parser/serializer validates rewrites, while a GitHub Actions workflow compiles the Kotlin scripts with Gradle and JDK 21.

Legacy SCMap deployment

Layer / File(s) Summary
SCMap binary rewriting
apps/faf-legacy-deployment/scripts/ScmapPathFixer.kt
Adds SCMap header detection, binary parsing and serialization, path rewriting, sized-string handling, and version suffix normalization.
Rewrite validation and safeguards
apps/faf-legacy-deployment/scripts/ScmapPathFixer.kt
Validates SCMap input, checks expected byte changes, and verifies rewritten output through round-trip serialization.
Deployment integration and compilation
apps/faf-legacy-deployment/scripts/CoopMapDeployer.kt, .github/workflows/compile-legacy-deployment.yml
Routes qualifying .scmap files through rewriting while preserving other content, and adds automated Kotlin compilation on pushes and manual dispatch.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CoopMapDeployer
  participant ScmapPathFixer
  participant ScmapRewriter
  participant ChecksumAndZip
  CoopMapDeployer->>ScmapPathFixer: detect SCMap and own map-folder reference
  ScmapPathFixer->>ScmapRewriter: rewrite embedded map paths
  ScmapRewriter-->>ScmapPathFixer: return serialized bytes
  ScmapPathFixer-->>CoopMapDeployer: return validated content
  CoopMapDeployer->>ChecksumAndZip: use content for checksums and ZIP output
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly matches the main change: fixing internal map paths in coop mission .scmap files during deployment.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@TimMasalme

Copy link
Copy Markdown
Author

The problem

getFileContent() rewrites /maps/<folder>/ into /maps/<folder>.vNNNN/ for text files
only. The paths embedded in the .scmap binary are not touched, so after a release they
still point at the unversioned folder. The missions that ship their own terrain textures or
decals lose them — the map loads with missing textures.

The change

ScmapPathFixer.kt reads a .scmap and writes it back byte for byte, inserting the
release version into the map folder segment of every embedded path. It is a port of
fix_paths from @speed2's sc_map_parser.gd, which is also what produced the manually
corrected FAF_Coop_Operation_Blockade.v0004 that is live today.

CoopMapDeployer.getFileContent() calls it for .scmap files. Nothing else changes — the
version still comes from coop_map.version + 1, change detection and zip building are
untouched, and build.gradle.kts needs no change because kotlin.srcDirs(".") picks the
new file up.

Only the mission's own folder is versioned

A path is rewritten only when its folder segment equals the mission folder. This is not
cosmetic — versioning everything under /maps/ would break released missions:

  • 13 missions point at a base game map, e.g. FAF_Coop_Fort_Clarke_Assault uses
    map = '/maps/X1CA_001/X1CA_001.scmap'. Those are exactly the missions whose repo
    .scmap is the 18 byte test fake map file placeholder.
  • FAF_Coop_Operation_Red_Revenge references a vault map from inside its binary:
    /maps/seraphim outpost ep.v0002/skycube_tundra02a.dds. Blindly appending would produce
    …ep.v0002.v0003.

Paths that lead somewhere else are left alone and logged at WARN. An already present
.vNNNN on the mission's own folder is replaced rather than appended to, so a re-run
cannot stack suffixes.

Nothing is parsed without a reason

Before parsing, a raw case-insensitive byte scan looks for /maps/<own folder>. Only maps
that actually reference their own folder are parsed at all — 4 of 31 real map files. The
placeholders never reach the parser, and neither do the 27 maps that only use base game
textures.

Self-check before shipping

After rewriting, and before the result can end up in a zip, the fixer asserts that

  • the byte delta equals the number of bytes added to paths, and
  • the rewritten file parses again and reproduces itself byte for byte.

If either fails it throws, main() logs the mission as failed and it drops out of the
release instead of shipping a corrupt map.

Effect on the next release

Mission paths rewritten size delta
FAF_Coop_Operation_Blockade 18 +108 B
FAF_Coop_Operation_Tha_Atha_Aez 18 +108 B
FAF_Coop_Operation_Golden_Crystals 2 +12 B
FAF_Coop_Operation_Overlord_Surth_Velsok 2 +12 B

Their checksums change, so these four get one version bump on the first run after this
lands. The other 38 missions are byte identical and are skipped as before. If a dry run
reports more than these four as changed, something is wrong — unless MAP_DIR is empty, in
which case everything counts as changed because there are no old zips to compare against.

Verification

Run against every .scmap in faf-coop-maps@HEAD — 31 real files, format versions 53, 56
and 60, plus the 13 placeholders:

  • Identity: rewriting with a negative version has to reproduce the input exactly.
    31/31 byte identical. This covers the v53 campaign maps and the single v60 map, not just
    the v56 ones that need the fix.
  • Delta accounting and round trip: the two assertions described above hold for all four
    affected missions.
  • Gate: 4 of 31 real maps are parsed, 0 of 13 placeholders.
  • Golden: the paths produced for FAF_Coop_Operation_Blockade are identical to the
    ones in the manually corrected .v0004 that is deployed today — all 18 of them.

Out of scope

  • Automatic deployment on a git release (the next step).
  • The hardcoded coopMaps list — FAF_Coop_Seabring_Defense and FAF_Coop_Theban_Colony
    are in the repo but not in the list.
  • FAF_Coop_Operation_Red_Revenge: its skycube points at another map while the .dds sits
    in its own folder. Left as is here, since that is a decision about the map, not about the deployment.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
apps/faf-legacy-deployment/scripts/ScmapPathFixer.kt (1)

66-93: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No automated tests for a complex binary format rewriter.

This PR's own goal is to "verify" the scmap fixer, and the runtime round-trip self-check here is a good safety net, but there's still no unit test exercising fixScmapPaths/ScmapRewriter against real .scmap fixtures (own-folder path, base-game-map reference, decal sized-strings, different format versions). A fixture-based test asserting byte-for-byte output would also have caught the case-folding issue flagged below, since that bug slips past both the size-delta and round-trip checks.

Want me to draft a JUnit test with a small synthetic .scmap fixture covering these paths?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/faf-legacy-deployment/scripts/ScmapPathFixer.kt` around lines 66 - 93,
Add automated fixture-based JUnit coverage for fixScmapPaths and ScmapRewriter
using synthetic .scmap inputs that exercise own-folder paths, base-game-map
references, decal sized-strings, and multiple format versions. Assert the
complete output bytes, including the case-folding scenario, rather than relying
only on the runtime size-delta and round-trip checks in fixScmapPaths.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/compile-legacy-deployment.yml:
- Around line 17-19: Harden the checkout step in the workflow by disabling
credential persistence via the checkout action’s persist-credentials setting,
and add explicit workflow permissions granting only contents read access. Update
the existing actions/checkout step; do not alter the compilation flow.

In `@apps/faf-legacy-deployment/scripts/ScmapPathFixer.kt`:
- Around line 254-271: Update both early-return branches in addVersion to return
the original path rather than the lowercased value: the suffix-empty case and
the non-matching/invalid path case. Preserve lowercasing only for internal
comparisons and keep the version-rewrite path unchanged.

---

Nitpick comments:
In `@apps/faf-legacy-deployment/scripts/ScmapPathFixer.kt`:
- Around line 66-93: Add automated fixture-based JUnit coverage for
fixScmapPaths and ScmapRewriter using synthetic .scmap inputs that exercise
own-folder paths, base-game-map references, decal sized-strings, and multiple
format versions. Assert the complete output bytes, including the case-folding
scenario, rather than relying only on the runtime size-delta and round-trip
checks in fixScmapPaths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 50b284da-fbc2-4f29-8463-5d6966c4a1e5

📥 Commits

Reviewing files that changed from the base of the PR and between e472eb6 and bfca087.

📒 Files selected for processing (3)
  • .github/workflows/compile-legacy-deployment.yml
  • apps/faf-legacy-deployment/scripts/CoopMapDeployer.kt
  • apps/faf-legacy-deployment/scripts/ScmapPathFixer.kt

Comment on lines +17 to +19
steps:
- uses: actions/checkout@v6

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Harden checkout: disable credential persistence.

zizmor flags this checkout for credential persistence (artipacked): the default leaves the GITHUB_TOKEN persisted for the rest of the job, even though this workflow only compiles code and never needs to push back. Also consider declaring explicit permissions: contents: read for least privilege.

🔒 Proposed fix
+permissions:
+  contents: read
+
 jobs:
   compile:

     runs-on: ubuntu-latest
     container:
       image: gradle:9.4-jdk21

     steps:
       - uses: actions/checkout@v6
+        with:
+          persist-credentials: false
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
steps:
- uses: actions/checkout@v6
permissions:
contents: read
jobs:
compile:
runs-on: ubuntu-latest
container:
image: gradle:9.4-jdk21
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
🧰 Tools
🪛 zizmor (1.26.1)

[warning] 18-18: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/compile-legacy-deployment.yml around lines 17 - 19, Harden
the checkout step in the workflow by disabling credential persistence via the
checkout action’s persist-credentials setting, and add explicit workflow
permissions granting only contents read access. Update the existing
actions/checkout step; do not alter the compilation flow.

Source: Linters/SAST tools

Comment on lines +254 to +271
private fun addVersion(path: String): String {
if (suffix.isEmpty()) return path
val lower = path.lowercase()
val parts = lower.split('/').filter { it.isNotEmpty() }
// expected: ["maps", "map_name", "env", ...]
if (parts.size < 3 || parts[0] != "maps") return lower

val segment = parts[1].replace(VERSIONED, "")
if (segment != folder) {
skipped += path
return lower
}

rewritten++
addedBytes += suffix.length - (parts[1].length - segment.length)
return "/" + parts.mapIndexed { i, part -> if (i == 1) segment + suffix else part }
.joinToString("/")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

addVersion lowercases paths it's supposed to leave untouched.

The docstring here says "Everything else is returned untouched," and the caller's docstring (line 48) explicitly warns that base-game-map references like /maps/X1CA_001/X1CA_001.scmap must be left alone. But both early-return branches return lower instead of path, so every path that doesn't target the mission's own folder (base-game maps, shared textures, skyboxes, decals, etc.) still gets case-folded to lowercase.

This silently diverges from the documented contract and isn't caught by either safeguard: the size-delta check only detects length changes (lowercasing doesn't change length), and the round-trip re-verification reprocesses the already-lowercased output with suffix = "", which trivially matches itself. If any referenced asset (e.g. a base-game map file) has mixed-case path segments on disk, this rewrite would break that reference.

🐛 Proposed fix
     private fun addVersion(path: String): String {
         if (suffix.isEmpty()) return path
         val lower = path.lowercase()
         val parts = lower.split('/').filter { it.isNotEmpty() }
         // expected: ["maps", "map_name", "env", ...]
-        if (parts.size < 3 || parts[0] != "maps") return lower
+        if (parts.size < 3 || parts[0] != "maps") return path

         val segment = parts[1].replace(VERSIONED, "")
         if (segment != folder) {
             skipped += path
-            return lower
+            return path
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private fun addVersion(path: String): String {
if (suffix.isEmpty()) return path
val lower = path.lowercase()
val parts = lower.split('/').filter { it.isNotEmpty() }
// expected: ["maps", "map_name", "env", ...]
if (parts.size < 3 || parts[0] != "maps") return lower
val segment = parts[1].replace(VERSIONED, "")
if (segment != folder) {
skipped += path
return lower
}
rewritten++
addedBytes += suffix.length - (parts[1].length - segment.length)
return "/" + parts.mapIndexed { i, part -> if (i == 1) segment + suffix else part }
.joinToString("/")
}
private fun addVersion(path: String): String {
if (suffix.isEmpty()) return path
val lower = path.lowercase()
val parts = lower.split('/').filter { it.isNotEmpty() }
// expected: ["maps", "map_name", "env", ...]
if (parts.size < 3 || parts[0] != "maps") return path
val segment = parts[1].replace(VERSIONED, "")
if (segment != folder) {
skipped += path
return path
}
rewritten++
addedBytes += suffix.length - (parts[1].length - segment.length)
return "/" + parts.mapIndexed { i, part -> if (i == 1) segment + suffix else part }
.joinToString("/")
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/faf-legacy-deployment/scripts/ScmapPathFixer.kt` around lines 254 - 271,
Update both early-return branches in addVersion to return the original path
rather than the lowercased value: the suffix-empty case and the
non-matching/invalid path case. Preserve lowercasing only for internal
comparisons and keep the version-rewrite path unchanged.

@TimMasalme TimMasalme changed the title Ci/verify scmap fixer Fix internal map paths in coop mission .scmap files on deploy Jul 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants