Measure: all 4,769 file names from notes/sha1-all.txt, extensions recorded
as spelt; all 1,321 data files parsed with a strict XML parser; every - Copy
file diffed against its original.
$ awk -F'\t' '{print $4}' notes/sha1-all.txt | sort | uniq -c | sort -rn
2323 .meta
1150 .XML
1035 .bk2
176 .xml
1,150 files are spelt .XML and 176 are spelt .xml. One of the 176 is
Mono's mconfig.xml, outside the game's data; the other 175 are in
ExternalData, mixed in with the 1,150 in the same two directories:
| directory | .XML |
.xml |
|---|---|---|
Data/CutsceneSubtitlesData |
976 | 83 |
Data/Conversations |
174 | 81 |
Data/GUI/* |
0 | 6 |
Localization/Strings |
0 | 5 |
total under ExternalData |
1,150 | 175 |
Plus one .txt: CutsceneSubtitlesData/StGermaine_1_IA_Tex6_PIECE_A.txt, 279
bytes, which is a CaptionData file that lost its extension:
<CaptionData>
<caption>
<stringID>StGermaine_1_IA_Tex6_PIECE_A</stringID>
<timeStart>0</timeStart><timeEnd>4</timeEnd>
</caption>
</CaptionData>Same shape as its neighbours, wrong extension. On Windows it does not matter,
because the game builds stem + '.xml' and NTFS does not care about case — and
because if the loader globs the directory, .txt will not be in the glob at
all and one caption never appears.
The code builds file names by concatenation (12). Among the assembly's literals:
'/ExternalData/Data/CutsceneSubtitlesData/'
'.xml'
Lower case. So on a case-sensitive filesystem — Linux, which is what Proton
on a Steam Deck runs — a request for Mojo_1_18.xml will not find
Mojo_1_18.XML, and 1,150 of 1,325 caption files are spelt the second way.
Whether that breaks anything depends on whether the runtime layer folds case,
which is a question about Proton's filesystem shim and not about this object.
What this object contributes is the count: 1,150 files at risk, 175 not.
The same trap reaches the keys, not just the file names:
CutsceneSubtitlesData/Resort_SC87_3pt13_3K.XML Resort_SC87_3pt13_3K_PIECE_A
Conversations/Rook_AA_7yrGap.xml Rook_AA_7yrGap_Piece_B
_PIECE_ and _Piece_. Case-folding the closure test in
07 recovers 16 lines in every one of the five
languages — 16 subtitles whose appearance depends on whether a dictionary
lookup is case-sensitive.
Two sessions ago, in pc-megaman3-doc, verifying that a .gitignore written
for upper-case extensions also caught lower-case ones, the session created
test files with lower-case names and then removed them with rm -f. The
filesystem is case-insensitive. rm -f mm.exe deleted MM.EXE, the real
executable, and rm -f dyna.scn deleted DYNA.SCN. Two of that object's
twenty-nine files were destroyed by the command that was verifying they would
be ignored.
So this session's .gitignore was verified with no file created and no file
removed, on paths that do not exist:
$ git check-ignore -v --no-index \
TeslaEffect.exe teslaeffect.exe foo.bk2 FOO.BK2 bar.XML bar.xml \
x.meta X.META x.assets x.resS level0 mainData output_log.txt \
appmanifest_261510.acf DefaultWsdlHelpGenerator.aspx Tesla_Promo_1.ogv \
notes/sha1-all.txt notes/census.txt README.md docs/00-predictions.md \
tools/census.py _work/anything.bin prompt.txt
.gitignore:23:*.[Ee][Xx][Ee] TeslaEffect.exe
.gitignore:23:*.[Ee][Xx][Ee] teslaeffect.exe
.gitignore:17:*.[Bb][Kk]2 foo.bk2
.gitignore:17:*.[Bb][Kk]2 FOO.BK2
.gitignore:18:*.[Xx][Mm][Ll] bar.XML
.gitignore:18:*.[Xx][Mm][Ll] bar.xml
...
.gitignore:47:!notes/*.TXT notes/sha1-all.txt
.gitignore:12:_work/ _work/anything.bin
README.md, docs/00-predictions.md and tools/census.py produce no output,
which is the correct answer: they are not ignored. No file was created, no
file was deleted, and the object is on a drive nothing in this session wrote
to.
Every rule is a bracket class — *.[Xx][Mm][Ll], *.[Bb][Kk]2,
*.[Mm][Ee][Tt][Aa] — rather than a bare extension, so the rules do not depend
on core.ignorecase being true. The one place they do is the exception
!notes/*.txt, which is why it is written twice, once for each case.
$ python - <<'…' # ET.parse on all 1,321 data files
data files parsed 1321
not well-formed XML 4
!! CutsceneSubtitlesData/Mantus_2_Sc_Cinematic.XML mismatched tag: line 113
!! CutsceneSubtitlesData/Rook_AA_PhotoMLeonard_triggered.XML mismatched tag: line 4
!! CutsceneSubtitlesData/StGermaine_2_4 .XML no element found: line 7
!! CutsceneSubtitlesData/Zack_1_3c .XML mismatched tag: line 15
Zack_1_3c .XML, entire:
<CaptionData>
<caption>
<stringID>Zack_1_3c_PIECE_A</stringID>
<timeStart>0</timeStart><timeEnd>3</timeEnd>
</caption>
<caption>
<stringID>Zack_1_3c_PIECE_B</stringID>
<timeStart>3</timeStart><timeEnd>6</timeEnd>
</caption>
<stringID>Zack_1_3c_PIECE_C</stringID>
<timeStart>6</timeStart><timeEnd>12</timeEnd>
</caption>
</CaptionData>The third caption lost its opening tag, leaving an unbalanced </caption>.
That is the file behind the <caption> 3,889 versus <stringID> 3,890
discrepancy in 07.
Four files of 1,321 are rejected by a conforming XML parser, which means
the game's loader is not one — it is lenient, or it is a regex. tools/closure.py
reads these files with a regular expression for exactly that reason, and says
so in its docstring: 1,321 files whose entire grammar is three tags do not need
a parser, and one of them is not even called .xml.
StGermaine_1_4 .XML 273 B
StGermaine_2_4 .XML 273 B
Zack_1_3c .XML 709 B
A trailing space in the stem. On Windows these are hard to create by accident
and hard to type; the shell in this session broke on one of them until the
tooling stopped globbing and started walking with os.walk.
And four with a duplicate suffix:
StGermaine_2_4 (2).XML 273 B
StGermaine_2_4 (3).XML 273 B
StGermaine_2_5D8_PIECE_A (2).XML 767 B
StGermaine_2_9G (2).XML 519 B
StGermaine_2_4 .XML, StGermaine_2_4 (2).XML and StGermaine_2_4 (3).XML
— note the two spaces in the last two — are three copies of a 273-byte file,
one of which is the one that fails to parse.
$ grep -ci " - copy" notes/sha1-all.txt
34
Seventeen XML files and their seventeen .meta. All seventeen are
Mojo_1_*:
Mojo_1_10F Mojo_1_11F Mojo_1_12F Mojo_1_18 Mojo_1_19I
Mojo_1_1B Mojo_1_20I Mojo_1_21I Mojo_1_22 Mojo_1_23
Mojo_1_25K Mojo_1_26L Mojo_1_2B_PIECE_A Mojo_1_3B
Mojo_1_A Mojo_1_H Mojo_1_J
(The briefing said eighteen. It is seventeen; see 19.)
One person, one afternoon, one conversation, selecting seventeen files in
Windows Explorer and pressing Ctrl+C, Ctrl+V. The - Copy suffix is Explorer's
and nothing else produces it.
Diffing each against its original is where it gets interesting:
identical to the original 11
different 6
original missing 0
Six of the seventeen are not backups. They are older versions.
Mojo_1_18 vs Mojo_1_18 - Copy
-<stringID>Mojo_1_18_PIECE_A</stringID>
+<stringID>Mojo_1_18</stringID>
Mojo_1_22 vs Mojo_1_22 - Copy
-<stringID>Mojo_1_22_PIECE_A</stringID>
+<stringID>Mojo_1_22</stringID>
Mojo_1_J vs Mojo_1_J - Copy
-<stringID>Mojo_1_J_PIECE_B</stringID> +<stringID>Mojo_1_J_PIECE_A</stringID>
-<timeEnd>7</timeEnd> +<timeEnd>1</timeEnd>
-<stringID>Mojo_1_J_PIECE_C</stringID> +<stringID>Mojo_1_J_PIECE_B</stringID>
-<stringID>Mojo_1_J_PIECE_D</stringID> +<stringID>Mojo_1_J_PIECE_C</stringID>
The - Copy versions predate the _PIECE_A naming convention and predate a
caption being renumbered. They are a snapshot taken before an edit, kept in
the directory the game ships, for two years. Version control implemented as
Ctrl+C.
And every one of the seventeen has its own .meta with its own distinct GUID —
so at some point the Unity editor saw all seventeen, imported them, and gave
each an identity. Nothing rejected them, because nothing was checking.
The same hand appears once more, in 06: four
.meta files in the object share a GUID with another .meta, and all four are
the Ault_5A / Ault_5BC pair — a folder copied with its sidecars, which is
the one thing a Unity project must never do.
17 XML + 17 .meta named "- Copy" 8,164 + 1,581 = 9,745 B
4 duplicate-suffix XML + their 4 .meta 1,832 + 372 = 2,204 B
3 trailing-space XML 1,255 B
1 .txt among the .XML 279 B
total 13,483 B
13,483 bytes. Nothing, against 17.5 gigabytes, and that is exactly why they are still there: nobody's disk-space alarm was ever going to go off over nine kilobytes of subtitle backups sitting next to a 560-megabyte video file. They shipped because the thing that would have caught them — a build step that looks at the data directory — did not exist, and 13 KB is under every threshold anybody would have set.