Skip to content

Commit 7df0bd0

Browse files
Check the coop map path fixer in CI
Compiles the deployment scripts and runs the .scmap path fixer over every mission in faf-coop-maps, in the same image the CronJobs use. Only runs when the scripts change. The load bearing assertion is that rewriting with a negative version reproduces the input byte for byte. It runs over all missions, not only the four that need the fix, so it also covers the old file formats - the coop missions use three of them (53, 56 and 60) and scoping a section to the wrong one breaks exactly those and nothing else. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 92e5625 commit 7df0bd0

3 files changed

Lines changed: 154 additions & 0 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: Coop deployment scripts
2+
3+
on:
4+
push:
5+
paths:
6+
- 'apps/faf-legacy-deployment/scripts/**'
7+
- '.github/workflows/coop-deployment-scripts.yml'
8+
pull_request:
9+
paths:
10+
- 'apps/faf-legacy-deployment/scripts/**'
11+
- '.github/workflows/coop-deployment-scripts.yml'
12+
13+
jobs:
14+
verify:
15+
16+
runs-on: ubuntu-latest
17+
container:
18+
# same image the deployment CronJobs use
19+
image: gradle:9.4-jdk21
20+
21+
steps:
22+
- uses: actions/checkout@v6
23+
24+
- name: Check out the coop missions
25+
uses: actions/checkout@v6
26+
with:
27+
repository: FAForever/faf-coop-maps
28+
path: faf-coop-maps
29+
30+
- name: Compile
31+
working-directory: apps/faf-legacy-deployment/scripts
32+
run: gradle --no-daemon compileKotlin
33+
34+
- name: Round trip the path fixer over every mission map
35+
working-directory: apps/faf-legacy-deployment/scripts
36+
env:
37+
MAPS_REPO: ${{ github.workspace }}/faf-coop-maps
38+
run: gradle --no-daemon verifyScmapFixer
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
@file:Suppress("PackageDirectoryMismatch")
2+
3+
package com.faforever
4+
5+
import java.io.File
6+
import kotlin.system.exitProcess
7+
8+
/**
9+
* Runs [fixScmapPaths] over every map file in a checkout of the coop missions and fails if
10+
* anything is off. Meant for CI, so a change to the path rewriting cannot quietly break a
11+
* map file.
12+
*
13+
* The important one is the identity check: rewriting with a negative version has to
14+
* reproduce the input byte for byte. It covers every mission, not only the handful that
15+
* need the fix, and therefore also the old file formats. Scoping a section to the wrong
16+
* file version breaks exactly those and nothing else.
17+
*
18+
* Point MAPS_REPO at a checkout of https://github.com/FAForever/faf-coop-maps.
19+
*/
20+
fun main() {
21+
val repo = File(System.getenv("MAPS_REPO") ?: "/tmp/faf-coop-maps")
22+
require(repo.isDirectory) { "MAPS_REPO does not point at a directory: $repo" }
23+
24+
val maps = repo.walkTopDown()
25+
.filter { it.isFile && it.name.lowercase().endsWith(".scmap") }
26+
.sortedBy { it.path }
27+
.toList()
28+
require(maps.isNotEmpty()) { "no .scmap files found below $repo" }
29+
30+
val failures = mutableListOf<String>()
31+
var placeholders = 0
32+
var identical = 0
33+
var rewrittenMaps = 0
34+
35+
println("%-44s %-8s %-10s %s".format("mission", "format", "size", "result"))
36+
37+
maps.forEach { file ->
38+
val folder = file.parentFile.name
39+
val bytes = file.readBytes()
40+
41+
if (!bytes.isScmap()) {
42+
placeholders++
43+
println("%-44s %-8s %-10s %s".format(folder, "-", bytes.size, "not a map file, skipped"))
44+
return@forEach
45+
}
46+
47+
val notes = mutableListOf<String>()
48+
49+
try {
50+
val copy = fixScmapPaths(bytes, folder, -1).bytes
51+
if (copy.contentEquals(bytes)) {
52+
identical++
53+
notes += "identical"
54+
} else {
55+
failures += "$folder: copy is ${copy.size} bytes, input is ${bytes.size}"
56+
notes += "NOT IDENTICAL"
57+
}
58+
} catch (e: Exception) {
59+
failures += "$folder: ${e.message}"
60+
notes += "FAILED: ${e.message}"
61+
}
62+
63+
if (bytes.referencesOwnMapFolder(folder)) {
64+
try {
65+
// delta accounting and the round trip are asserted inside fixScmapPaths
66+
val fix = fixScmapPaths(bytes, folder, 4)
67+
rewrittenMaps++
68+
notes += "${fix.rewritten.size} path(s) rewritten"
69+
70+
fix.rewritten.filterNot { it.contains(".v0004/") }
71+
.forEach { failures += "$folder: not versioned: $it" }
72+
fix.rewritten.filter { it != it.lowercase() }
73+
.forEach { failures += "$folder: not lower cased: $it" }
74+
fix.rewritten.filter { DOUBLE_VERSION.containsMatchIn(it) }
75+
.forEach { failures += "$folder: version added twice: $it" }
76+
} catch (e: Exception) {
77+
failures += "$folder: ${e.message}"
78+
notes += "FAILED: ${e.message}"
79+
}
80+
}
81+
82+
println("%-44s %-8s %-10s %s".format(folder, "v" + formatVersion(bytes), bytes.size, notes.joinToString(", ")))
83+
}
84+
85+
println()
86+
println("$identical of ${maps.size - placeholders} map file(s) reproduced byte for byte")
87+
println("$rewrittenMaps map file(s) reference their own folder and were rewritten")
88+
println("$placeholders placeholder file(s) skipped")
89+
90+
if (failures.isNotEmpty()) {
91+
println()
92+
failures.forEach { println("FAIL $it") }
93+
println("${failures.size} failure(s)")
94+
exitProcess(1)
95+
}
96+
println("all good")
97+
}
98+
99+
private val DOUBLE_VERSION = Regex("""\.v\d{4}\.v\d{4}""")
100+
101+
/** Map file format version, stored behind the preview image. */
102+
private fun formatVersion(bytes: ByteArray): Int {
103+
fun int(at: Int) = (bytes[at].toInt() and 0xFF) or
104+
((bytes[at + 1].toInt() and 0xFF) shl 8) or
105+
((bytes[at + 2].toInt() and 0xFF) shl 16) or
106+
((bytes[at + 3].toInt() and 0xFF) shl 24)
107+
return int(34 + int(30))
108+
}

apps/faf-legacy-deployment/scripts/build.gradle.kts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,12 @@ tasks.register<JavaExec>("deployCoopMaps") {
3636

3737
classpath = sourceSets.main.get().runtimeClasspath
3838
mainClass.set("com.faforever.coopmapdeployer.CoopMapDeployerKt")
39+
}
40+
41+
tasks.register<JavaExec>("verifyScmapFixer") {
42+
group = "verification"
43+
description = "Run the .scmap path fixer over every map in a faf-coop-maps checkout (MAPS_REPO)"
44+
45+
classpath = sourceSets.main.get().runtimeClasspath
46+
mainClass.set("com.faforever.ScmapPathFixerCheckKt")
3947
}

0 commit comments

Comments
 (0)