arma-reforger-script-analysis
Analyse Arma Reforger Enforce Script code - what a class or method actually does, which file and line declares it in the installed build, which mods replace it with modded class, and why an override c
SKILL.mdskills/arma-reforger-script-analysis/SKILL.md
GitHubAnalysing Arma Reforger script code
This repository has two jobs around .c files and they are not the same one. Reading a
script — finding it in a .pak, listing a class's members, printing a signature — is
the game-data skill §5, which owns the reader's
reference: every command, every flag, the source kinds and the parser's measured behaviour.
Analysing script code is this skill: deciding which copy of the code is the one that runs,
reading a declaration and then the body behind it, and reasoning about what happens when
several mods replace the same class.
The distinction is not pedantry. Script analysis fails in a particular way: it produces a
plausible description of a file nobody ran. Four downloaded addons each override
SCR_CharacterCameraHandlerComponent.CameraSelector; read one copy alone and you have a
correct description of a method that may never be the one that decides. §4 works that example
through.
0. Three questions, three shapes of answer
| The question | What actually settles it | What the answer must carry |
|---|---|---|
| What does this class or method do? | the declaration, then the body behind it | a behaviour summary and file:line-line |
| Why does my mod's change do nothing? | the chain, the module, the signature, and whether the mod is loaded | the step where it breaks, named |
| Two mods change the same method — what happens? | every copy of the class, the order they run in, and which of them call super |
a chain, with the order called out as the one assumption |
Two rules that keep the answer honest.
- A statement about code carries the file and the lines.
scripts/Game/Player/SCR_PlayerController.c:30is checkable; "in SCR_PlayerController" is not. - A statement about runtime behaviour is an inference, and is labelled as one. The text says what will happen if the code runs; whether it runs, and with what values, is decided by the build flags, the data files and the load order.
A comment is its author's claim, not evidence. WCS_Core guards CameraSelector against
null m_AnimationComponent / m_ControllerComponent and says those are "the members vanilla
actually dereferences". That is true — vanilla dereferences them at
scripts/Game/Character/CharacterCameraHandlerComponent.c:179 and :190 — but the comment is
what prompted the check, not what settled it. A mod's comment is also where a wrong assumption
usually hides.
1. Which copy of the code is the one that runs
Vanilla. The game's script code is in two addons of the install, and each is its own
archive: <install>/addons/data (the game's own scripts, every module) and
<install>/addons/core (the engine-facing layer: Managed, IEntity, Widget, BaseContainer,
and GameLib). The client keeps the game scripts in one archive, data/data007.pak; the dedicated
server keeps the same 5 809 files in data/data002.pak, the other resources in data.pak and
data001.pak beside it. Core is always core/data.pak — 759 .c, in every install. Measured on
1.8.0.13 with stats: 6 568 .c files, 28.7 MB, 94 568 declarations, spread over nine
modules — Game 81 120 declarations, GameLib 4 950, Core 3 079, WorkbenchGame 2 810,
WorkbenchGameCommon 1 145, GameCode 976, Autotest 297, WorkbenchCommon 147, Workbench 44.
core and data conflict as resources, and both are needed as code
The two addons are separate mounts, and their resource dictionaries refuse to merge: they share
151 GUIDs and 136 of those name different paths — core's configs/ResourceTypes/… against
data's Configs/System/ResourceTypes/… — so rdb verify-merge reports 136
sameGuidDifferentPaths and ebin --rdb refuses the pair
(game-data skill §4). Both paths are real entries in their
own archive; the disagreement is between the two dictionaries, not a damaged database.
That refusal is a rule about GUID -> path, and it does not reach scripts. cscript merges
nothing: it searches each --pak on its own, so both are passed together whenever an analysis
crosses the layers — which it almost always does, because core's pak is where the engine's script
objects are declared. stats --pak "<install>/addons/core" on 1.8.0.13: 759 .c, 1.7 MB,
9 051 declarations, and 3 600 of its 5 231 methods are proto/native — 3 600 of the 6 873
in the whole corpus (§3), so over half of "the body does not exist in script" is a declaration that
is right there, with its signature, attributes and docstring. Core also carries 4 680 of the
4 950 GameLib declarations against data's 270, and GameLib is the layer every Game class
builds on:
| What core's pak declares | What the analysis loses without it |
|---|---|
scripts/Core/generated/Entities/IEntity.c:12-540 |
the event protected void EOn* surface — EOnInit, EOnTouch, EOnFrame, EOnVisible — which is the answer to "who calls this hook" (§5) |
scripts/GameLib/generated/Components/ScriptComponent.c:23-158 |
the base 172 vanilla classes name directly; without it schema --inherited ends with inherited from ScriptComponent (NOT FOUND -- pass --pak/--addon for it) |
scripts/GameLib/generated/Entities/GenericEntity.c, …/GenericController.c |
the middle of every entity's chain — SCR_PlayerController reaches IEntity through them |
scripts/Core/generated/UI/Widget.c, …/Containers/BaseContainer.c, scripts/Core/proto/Types.c |
Widget, BaseContainer, Managed: the types a signature or a data value refers to |
The price of leaving it out is a chain that stops at depth 1:
$ node tools/cscript.mjs inheritance-chain "<install>/addons/data/data007.pak:scripts/Game/Player/SCR_PlayerController.c" \
SCR_PlayerController --pak "<install>/addons/data"
0 class SCR_PlayerController : PlayerController
0 <install>/addons/data/data007.pak:scripts/Game/Player/SCR_PlayerController.c:30-1182
1 class PlayerController : GenericController
1 <install>/addons/data/data007.pak:scripts/Game/generated/Player/PlayerController.c:16-81
[warning] base class GenericController is not in the searched sources (pass --pak or --addon for it)
2 step(s) in the chain
The same command with --pak "<install>/addons/core" added is six steps, and every line names its
archive — the last four are core's data.pak, which is how the missing half identifies itself:
2 class GenericController : GenericEntity
2 <install>/addons/core/data.pak:scripts/GameLib/generated/Entities/GenericController.c:16-18
3 class GenericEntity : IEntity
3 <install>/addons/core/data.pak:scripts/GameLib/generated/Entities/GenericEntity.c:15-194 Base entity class.
4 class IEntity : Managed
4 <install>/addons/core/data.pak:scripts/Core/generated/Entities/IEntity.c:12-540
5 class Managed
5 <install>/addons/core/data.pak:scripts/Core/proto/Types.c:112-116
6 step(s) in the chain
So the standing pair for a vanilla analysis is --pak "<install>/addons/data" --pak "<install>/addons/core", and check needs the same pair before it stops calling vanilla base
classes missing. Neither pak alone reaches the top of a chain: schema --inherited of
BaseWeaponComponent (scripts/Game/generated/Weapon/BaseWeaponComponent.c) ends at inherited from GenericComponent (NOT FOUND …) with data alone, at inherited from GameComponent (NOT FOUND …)
with core alone, and answers GameComponent (2) and then GenericComponent (43) with both — each
pak supplies a different half of the same hierarchy. What must not be taken from that pair is
the database: pass one .rdb — the data one — to rdb/ebin, because two dictionaries that do
not merge resolve nothing. Code and GUIDs are two questions with two answers here; a reader that
conflates them loses either the chain or the GUID.
The module is the segment after scripts/ in the entry path, and it is not decoration: a
modded class has to live in the same module as the original ([wiki], measured consistent —
all 203 modded declarations in the analysed addons sit in their original's module). That is why
a mod that mods scripts/Game/... puts its file under Scripts/Game/..., as all four camera
addons do.
Mods come in two shapes, and the difference decides how you read one. Every mod count in
this skill comes from ten downloaded addons whose scripts were read for it: ACECore,
AdvancedZeroingSystem, EchoReports, EnfusionDatabaseFramework, GRS-Patches, RHS-StatusQuo,
ServerAdminTools, TacticalFlava-Core, WCS_Core and WhereAmI — 971 .c, 15 711 declarations and
203 modded declarations between them, which is the sample the conflict numbers below are
measured on. The whole folder is much larger: stats --addons <addons root> on this machine's
231-addon Workshop folder reports 4 242 .c, 28.0 MB, 93 541 declarations, 1 072 of them
modded (977 classes, 95 enums). A different ten will give different counts; nothing below
depends on these exact numbers.
| Shape on disk | How the reader sees it | What a path means |
|---|---|---|
Workbench project: Scripts/**/*.c unpacked |
--addon <project dir> |
an ordinary file path |
Downloaded addon: addon.gproj + data.pak, no unpacked tree |
--addon <addon dir> (its *.pak is opened) |
a pak entry, so cat needs <addon>/data.pak:<entry> |
That second row is where a first attempt used to die: an entry path is not a file on disk, and on
its own it does not say which archive holds the file — every downloaded addon ships a data.pak,
so one virtual path can stand for several different files. The reader prints the archive with the
entry, so what find-defs shows is already the <pak>:<entry> form cat takes:
$ node tools/cscript.mjs find-defs SCR_CharacterCameraHandlerComponent --addons "<addons>"
SCR_CharacterCameraHandlerComponent: 15 definition(s) in 4242 file(s)
1. class SCR_CharacterCameraHandlerComponent [modded]
modded class SCR_CharacterCameraHandlerComponent : CameraHandlerComponent
<addons>/ACECore_60C4CE4888FF4621/data.pak:scripts/Game/ACE_Core/Character/SCR_CharacterCameraHandlerComponent.c:3-14 (signature 3)
2. class SCR_CharacterCameraHandlerComponent [modded]
modded class SCR_CharacterCameraHandlerComponent : CameraHandlerComponent
<addons>/ACECoreDev_65AD7D0D9941A380/data.pak:scripts/Game/ACE_Core/Character/SCR_CharacterCameraHandlerComponent.c:3-14 (signature 3)
Those two rows carry the same virtual path and are two different files; the archive is the whole
difference, and it is what cat needs:
node tools/cscript.mjs cat "<addons>/ACECore_60C4CE4888FF4621/data.pak:scripts/Game/ACE_Core/Character/SCR_CharacterCameraHandlerComponent.c" --plain
Two further constraints, both measured. --addon is one addon: passing the parent
.../ArmaReforger/addons directory to it fails with check needs a source: a .c file, a directory, or --pak/--addon, because that directory holds no .c and its *.pak files are one level down in
each addon. The option for that folder is --addons <root>, which reads every directory under it as
one addon — 231 addons and 4 242 .c on this machine, read in one scan (stats takes about nine
seconds). The two vanilla paks go
together, --pak "<install>/addons/data" --pak "<install>/addons/core" (§1 above: core is an addon
whose database does not merge with data's, and whose scripts the chain needs).
2. The loop: find, then look, then read the body
Every analysis of existing code is the same three steps. The reader answers the first two; the third is the only place behaviour comes from, because bodies are skipped by the parser and known only as a range.
- Find every copy.
find-defs <Class|Class.Method|Enum|MEMBER>over the vanilla paks and the addons involved. The original is marked[original], each replacement[modded], and every row carries the file and its line range. Without--original/--only-moddedyou get both, which is what an analysis wants: the question is almost never "where is it" but "how many are there". - Look at the declaration.
schemafor a class (fields and methods with visibility,static, decorators, docstring, ranges),signaturefor one method,inheritance-chainfor what it derives from.schema --inheritedappends the base classes' members, which is how a method that is not in the file you are reading comes into view. - Read the body.
cat <source> <from>-<to>with the range a previous command printed. Inclusive on both ends, reads out of the archive without extracting 2 GB,--plainwhen the text is going into an answer.
find-defs over vanilla plus the four camera addons takes about 0.6 s (6 691 files
searched), so there is no reason to skip step 1 and read a single copy.
A worked pass, quoting real output:
$ node tools/cscript.mjs find-defs SCR_PlayerController --pak "<install>/addons/data"
SCR_PlayerController: 1 definition(s) in 5809 file(s)
1. class SCR_PlayerController [original]
class SCR_PlayerController : PlayerController
<install>/addons/data/data007.pak:scripts/Game/Player/SCR_PlayerController.c:30-1182 (signature 30)
$ node tools/cscript.mjs schema "<install>/addons/data/data007.pak:scripts/Game/Player/SCR_PlayerController.c" SCR_PlayerController --members methods --limit 5
class SCR_PlayerController : PlayerController lines 30-1182
source <install>/addons/data/data007.pak:scripts/Game/Player/SCR_PlayerController.c
fields (28)
methods (50)
OnOwnershipChangedInvoker GetOnOwnershipChangedInvoker() lines 71-74
override void OnOwnershipChanged(bool changing, bool becameOwner) lines 80-109
override void OnInit(IEntity owner) lines 112-119
void OnPlayerRegistered(SCR_EditorManagerEntity managerEntity) lines 122-131
void OnBlockedListUpdated(bool success) lines 134-153
... 45 more (--limit 0)
$ node tools/cscript.mjs signature "<install>/addons/data/data007.pak:scripts/Game/Player/SCR_PlayerController.c" SCR_PlayerController.OnOwnershipChanged
override void OnOwnershipChanged(bool changing, bool becameOwner)
<install>/addons/data/data007.pak:scripts/Game/Player/SCR_PlayerController.c:80-109 (signature 80)
in class SCR_PlayerController lines 30-1182
doc lines 77-79
\see PlayerController.OnOwnershipChanged for more information.
The two ranges in the last block are what makes a method hit usable without opening the whole
class: 30 lines inside a 1 153-line one. inheritance-chain on the same class prints each link
and, at its head, every modded copy that replaces it — each line naming its own archive:
0 class SCR_PlayerController : PlayerController
0 <install>/addons/data/data007.pak:scripts/Game/Player/SCR_PlayerController.c:30-1182
0 modded class SCR_PlayerController : PlayerController [modded]
0 <addons>/ACECore_60C4CE4888FF4621/data.pak:scripts/Game/ACE_Core/Player/SCR_PlayerController.c:2-106
0 modded class SCR_PlayerController [modded]
0 <addons>/RHS-StatusQuo_595F2BF2F44836FB/data.pak:Scripts/Game/Player/RHS_M_CallInPlayerController.c:1-129
# ... every other modded copy, then ...
1 class PlayerController : GenericController
1 <install>/addons/data/data007.pak:scripts/Game/generated/Player/PlayerController.c:16-81
The leading number is the depth: the class itself and every modded copy that replaces it sit
at 0, and the base classes follow at 1, 2, 3. A chain this class prints for
SCR_PlayerController is 6 steps long (SCR_PlayerController → PlayerController →
GenericController → GenericEntity → IEntity → Managed), so "what does it inherit" is a
bigger question than
the file's : PlayerController suggests. The archive is part of every line because it is part
of the answer: two modded copies of one class in two downloaded addons both declare
scripts/Game/.../SCR_PlayerController.c, and only the archive tells them apart.
Size, before you parse. A schema is a page, not the whole class: schema --inherited of
CharacterControllerComponent answers with 9.5 KB of text, 57 KB of --json (25 rows) or 33 KB of
--json --compact, and the envelope's counts says the real total (242 methods here) with
truncated set. --limit 0 is the whole class — 517 KB as JSON, 22 KB even as text — so ask for it
when you mean it, and use --members methods to spend the page on the half you need. Read the text
unless something has to parse it.
3. What a declaration tells you, and what it does not
A schema is a map of the interface, not a description of the behaviour. These shapes decide what can be concluded; all counts are from the 1.8.0.13 corpus.
| Shape | What it means for the analysis | How much of it |
|---|---|---|
proto / native / external |
engine side: the body does not exist in script, so the analysis stops at the signature. The declaration is still text — signature, [Attribute]s, docstring — and 3 600 of the 6 873 are in core's pak (§1) |
6 873 proto/native methods |
event |
an engine-called hook. A bodyless event int CameraSelector(); means script does not call it; the engine does |
15 bodyless event void OnInit(IEntity owner); declarations, and EOnInit in the same shape |
static |
dispatched on the declared type, not on the object (the wiki's GetAge example), so an override does not apply through a base-typed static call |
— |
private / protected |
private members are not moddable; protected is the hook a mod is meant to use |
— |
sealed |
a promise, not a fact: 355 sealed class declarations ship and 137 declarations still name a sealed class as their base, so "sealed, therefore no subclass" is not a safe inference | 355 / 137 |
[Attribute(...)] |
editor and serialisation metadata. It says what the Workbench offers and what a config may set, not what the code does | — |
#ifdef around a declaration |
the declaration may not be compiled at all. The row carries its conditions |
5 892 declarations, under 53 distinct flags (WORKBENCH, ENABLE_DIAG, AI_DEBUG among them) |
| an empty body | a data holder (2 093 class declarations have no members), a class completed by a modded class elsewhere, or a placeholder |
2 093 |
modded |
not an interface detail at all — see §4 | 203 declarations |
Which flags a shipping build defines cannot be read from the scripts. When a declaration is
inside #ifdef ENABLE_DIAG, the honest answer is "this exists when ENABLE_DIAG is defined",
not "this exists".
4. modded is composition, not replacement
A modded class does not replace a file; it is spliced into the class's hierarchy at load
time. Two consequences follow, and they carry most of the analysis value in this skill.
supercalls the previous copy in the chain, not the original. With one mod they are the same thing. With two, the later-loaded copy'ssuperreaches the earlier mod, and onlyvanilla.X()reaches the unmodded code ([wiki]; the keyword exists for exactly this).- The order is dependency-driven. If mod2 requires mod1, mod1 loads first; between
independent mods the order is not guaranteed ([wiki]). Whether the
mods[]order inserver.jsonalso influences it is not established here — do not assert it either way.
super is what mods actually use: 1 173 occurrences in the ten analysed addons (3 955 in the
whole folder), against 8 code hits for vanilla. there (and none of them reaches the engine —
all eight are one file's variable named vanilla). So a mod that calls super
is assuming something about what sits below it, and a mod that returns without calling super
cuts everything below out of that call.
One method, four mods. SCR_CharacterCameraHandlerComponent (vanilla
scripts/Game/Character/CharacterCameraHandlerComponent.c:6-1169) is replaced by 13 addons in the
whole folder; the four below are from the ten-addon sample, and all four override CameraSelector:
| Copy | File and lines | Condition that returns early | Calls super? |
|---|---|---|---|
| vanilla | scripts/Game/Character/CharacterCameraHandlerComponent.c:177-331 |
diag view, unconscious, swimming, ADS and so on | it is the bottom of the chain — its base declares event int CameraSelector(); |
| ACECore | scripts/Game/ACE_Core/Character/SCR_CharacterCameraHandlerComponent.c:3-14 |
ACE_CharacterCameraCustom.IsToggledOn() → its own camera id |
only when the toggle is off |
| RHS-StatusQuo | Scripts/Game/Camera/CustomCamera/RHS_M_CharacterCameraHandlerComponent.c:6-82 |
a custom point's look-at / instant flags | always |
| TacticalFlava-Core | Scripts/Game/Game/UI/MFD/Modded/TF_SCR_SCR_CharacterCameraHandlerComponent.c:26-85 |
m_CameraPoint → its own camera id |
always |
| WCS_Core | Scripts/Game/Character/WCS_Core_CharacterCameraHandlerComponent.c:1-15 |
a null m_OwnerCharacter/m_AnimationComponent/m_ControllerComponent → plain first person |
only when all three are non-null |
Put any two of them in one server and the method becomes a chain, whose behaviour depends on
the order: if WCS's guard sits above ACE's toggle, a null member returns first person and ACE's
custom camera is never asked; if ACE sits above RHS, a toggled-on ACE returns before RHS can
rewrite the turret camera. Neither is a bug in either mod. This is the answer to "two mods
fight over one method": quote each copy, name the branches that skip super, and state the
order as the assumption the conclusion rests on.
The scale, for context: those 203 modded declarations are 179 classes and 24 enums, and 18
classes are modded by more than one of the ten (this camera class by four,
SCR_PlayerController, SCR_BaseGameMode, ArmaReforgerScripted and others by three). Over the
whole folder it is 104 classes, and SCR_CharacterControllerComponent is modded by 21 addons —
so "who else touches this" is not a hypothetical question on a real server.
The members of a modded class. 614 methods are declared inside those 179 modded classes:
353 add a name the original does not have and need no override; of the 261 that redeclare an
existing name, 8 omit override and all 8 are constructors (the class's own, e.g.
SCR_BaseGameMode()). That matches the wiki: constructors and destructors belong to each copy,
everything else must be an exact-signature override. The folder-wide numbers are the same in
shape and less tidy: 13 of 1 012 redeclarations omit override and only 5 of the 13 are
constructors — the rest are mods that got away with it, which is not the same as being right.
Details worth knowing when the base clause is missing. Three of the four modded copies
declare modded class SCR_CharacterCameraHandlerComponent with no : Base at all; only ACE
repeats : CameraHandlerComponent. Absence of a base clause does not mean "inherits nothing" —
the original's base governs — so do not read a missing clause as a difference. A base clause
that disagrees with the original is a different matter and is what check reports:
[warning] modded class SCR_PlayerController declares base SCR_PlayerController but the original declares PlayerController
That one is real: Scripts/Game/Player/RHS_TVGuidanceRelay.c:4 declares modded class SCR_PlayerController extends SCR_PlayerController, a self-base. The same shape appears in
another addon with the : spelling, and it is one of the six defects the corpus check reports.
5. What the text cannot answer, and what to do instead
| Question | Why the text cannot answer it | What to do |
|---|---|---|
| Who calls this method? | there is no call graph, and some callers are not in script at all | refs <Method> --code-only over the same sources: it prints every use and marks a [comment] or [string] mention as one. Read the event/proto declaration too — CameraSelector has no call site in 6 568 vanilla files (refs --code-only finds only its two declarations) and is called by the engine through event int CameraSelector();, and the only script calls to it anywhere are the super.CameraSelector() lines inside the mods that override it |
| Which override runs at runtime? | it depends on the object's runtime class, on the load order (§4) and on the branch taken | reconstruct the chain from every copy, then say which order the conclusion assumes |
| What is this field's value? | the code's initialiser is only the fallback; the prefab or config may override it | read the .conf/.et with etconf (game-data skill §3), and the class default with schema |
| Is this method server-side or client-side? | the side is decided by the replication attribute and by who owns the entity, not by the name | look for [RplRpc(RplChannel..., RplRcver...)] above the method and Rpc(...) at its call site: 497 [RplRpc] and 163 [RplProp] attributes in vanilla. The RpcAsk_/RpcDo_ prefix is a strong hint but not the binding — 212 of the 217 Rpc*_ methods carry the attribute and 5 do not |
Which #ifdef flags are defined in this build? |
the flags are a build input, not a resource | treat a conditional declaration as conditional, and check whether the behaviour is observable in game |
| Is this dead code? | nothing in the sources marks a method as unused, and the engine, a .conf or another script can call it |
search for the name, then check event/proto/[RplRpc]/override; absence of a script call site proves nothing by itself |
What happens during OnInit across components? |
component initialisation order is engine-side | read each component's OnInit and report the order dependency as unresolved |
Searching for callers. Use refs <name> --code-only over the paks and the addons; it searches
inside the archives, so nothing has to be extracted, and it separates a use from a mention. Two
things still make the list readable: expect the same name in several classes (Init is declared in
410 classes, OnInit in 242), and remember that a name in a string literal is exactly what
--code-only drops — the flag exists because SCR_Target appears three times in one line and only
two of those are uses. What remains is still a text search: a call through a variable
(controller.SomeMethod()) is found, but which class that variable holds is a question for §4.
6. Recipes
a. "What does this class do?"
find-defs <Class>over the paks, plus the addons the user runs.schema <source> <Class> --inherited— own members first, then the bases.signatureon the two or three methods that carry the behaviour.catthose ranges; write down what the methods do, quoting lines.- If a base class matters,
inheritance-chain.
b. "Where is the original behind this modded class?"
find-defs <Class> --only-moddedwith the addons in the search, to see the copies.find-defs <Class> --originalfor the one they replace. Ask for--only-moddedwithout an addon and the answer is0 definition(s)with exit 1 — correct, and easy to misread as "the class is not modded".- Read both: the mod's copy often adds a docstring the original does not have, and the
original's body is what
superreaches when the mod is the only one loaded.
c. "My override does nothing." Check in this order, because each step is cheaper than the next and a failure at one makes the rest irrelevant:
- The class name and file: is the mod's file in the same module as the original
(
Scripts/Game/...forscripts/Game/...)? §1. - Is the method's name and signature exactly the original's?
signatureboth. - Is the member
privatein the original? Then it cannot be overridden — the wiki says so and the schema prints the visibility. - Does the original method exist at all, in the sources you gave
check? Runcheck --pak ... --addon ...:modded class X replaces a class that is not in the searched sourcesmeans the search was too narrow, not that the class is absent. - Is the method actually called on the object you think? If it is an
eventhook the engine calls, or a[RplRpc]method invoked throughRpc(...), a script search will not show it. - Is another mod loaded after yours, above it in the chain, returning without
super? §4.
d. "Two mods change the same method — who wins?" Every copy with find-defs, read the
bodies, list the branches that skip super, then state the order. Answer with the chain and
the assumption, never with "mod B wins".
e. "Is this mod consistent with this build?" check --pak "<install>/addons/data" --pak "<install>/addons/core" --addons "<addons root>" for the whole collection, or the same with
--addon "<one mod>" for one. It exits non-zero on errors; warnings only fail it under
--strict, so read the report rather than the exit code: the addons folder alone has 982 warnings
(and 34 errors), and one addon plus the vanilla paks adds 20 more.
f. "How does this class differ between two builds, or between vanilla and a mod?" Put both
sides in the same cat form (a file, or <pak>:<entry>), read the same range from each, and
diff the two blocks. The ranges will differ; compare the bodies, not the line numbers.
g. "What is affected if this method changes?"
subclass-tree <source> <Class> --depth N, with the addons included: every subclass, and the[modded]rows, which are the copies that replace the class itself.find-defs <Class.Method>for every copy that redeclares it — each one is a signature that has to keep matching.signatureon each copy: anoverridewhose signature differs is not an override at all.
h. "Who uses this name?"
refs <name> --code-onlyover the paks and the addons: the uses, each with its line.refs <name>without the flag: the comments that explain it — often the cheapest documentation a mod ships, and the answer to why a workaround exists.find-defs <name>for the declarations inside those hits. A name with one declaration and no use at all is dead code or engine-called, and §5 is the difference.
7. Traps, each measured
| Trap | What it looks like | Why it bites |
|---|---|---|
| reading one copy of a modded class | a file that reads as the definition | four copies exist; the chain, not the file, decides |
super assumed to be vanilla |
super.CameraSelector() |
with two mods it reaches the other mod, which may return before your code |
a modded class with no base clause |
modded class SCR_CharacterCameraHandlerComponent |
not a difference — three of the four copies write it that way |
| a self-base | modded class SCR_PlayerController extends SCR_PlayerController |
present in a real addon; check warns — do not read the class name on both sides as harmless |
proto/event methods with no body |
event int CameraSelector(); |
the engine is the caller; a search finds nothing and that is the answer |
declarations inside #ifdef |
#ifdef ENABLE_DIAG in SCR_BaseGameMode.c:140-142 |
5 892 declarations are conditional |
| a pak entry treated as a path | cat "<addon>/Scripts/.../X.c" → no such file |
a downloaded addon keeps its scripts inside data.pak |
vanilla. matched in prose |
"Leave empty for vanilla." | the 4 hits in the vanilla paks are comments; the addons folder's 8 code hits are one file's variable named vanilla |
| 2 093 class bodies with no members | class SCR_Foo {} |
the members are added by a mod, or the class is a data holder |
| a comment asserting vanilla behaviour | WCS's "members vanilla actually dereferences" | true here, but it had to be checked against the body |
| a ternary operator | int n = a > 0 ? a : -a; |
not the language: the Workbench compiler rejects ? : as a syntax error, and check reports it. The vanilla corpus has 767 question marks and all of them are in comments or strings — 0 in code — so any ? you write is the mistake, not a shorthand |
| core's pak left out because its database does not merge with data's | [warning] base class … is not in the searched sources, or inherited from ScriptComponent (NOT FOUND …) |
the merge rule is about GUIDs, not scripts: core is where half the proto/native declarations and all of GameLib live (§1) |
8. Before you hand the answer over
- Every claim about code names
file:line-line, and the file is the one in the build the user runs. - The copy count is stated: how many declarations of the class were found, in which paks and addons.
- Both vanilla paks were passed —
dataandcore— so no base class is reported missing only because the second addon was skipped (§1). - If behaviour is claimed, the body was read (
cat), not inferred from a name or a docstring. - If two mods are involved, each copy is quoted and the load order is named as the assumption.
- Anything the text cannot settle is listed as unresolved (§5) instead of guessed.
-
checkwas run on the mod if the answer says the mod is fine.
9. Where the rest is
- The game-data skill §5 — the reader's reference:
every command and flag, the source kinds,
moddedreporting modes, the parser traps and the corpus counts. - The same skill §3 and §4 — prefabs/
etconfand resource GUIDs, for the data a script reads. - The same skill §6 — localization, for the
#AR-…keys a method returns. - tools/cscript.mjs — the reader itself, with its library exports
(
parseScript,findDefinitions,schemaOf,ScriptUniverse) beside the CLI. - AGENTS.md — the repository's router, including which document answers which server-side question.