Pack Config¶
config.json sits at the root of a mod folder and declares what the pack is, what it needs, and what rules it plays by.
{
"id": "3f6c2a9e-8b1d-4c57-9e2f-7a0d5b4c1e88",
"isChronicle": true,
"description": "My Streets of Rage 2 New Era mod.",
"dependencies": ["Classic"],
"dependenciesChecksums": [""],
"settings": {
"hasWideScreen": true,
"hasImprovements": true,
"bufferGlitchFixMode": 2,
"scoreForLives": [20000, 50000, 150000],
"difficultySettings": []
}
}
Every field is optional. An omitted field takes its type default.
Top level¶
| Field | Type | Meaning |
|---|---|---|
id |
string | The mod's stable unique identifier: a random UUID, created once with the mod and never changed. The engine identifies the pack by it, so renaming the mod keeps everything that refers to it. See Mod identity. |
isChronicle |
bool | Marks the pack as a full Chronicle: a standalone campaign that appears as its own game, rather than a skin layered over another pack. Set during export when applicable. |
expandHudSpacing |
bool | In widescreen, reserve a 32px lane beside the timer for custom HUD art. Defaults to false. If the campaign or any active character pack enables it, all player HUDs use the expanded layout. |
hudLayout |
object | Optional normal and widescreen profiles configuring player columns, timer offsets, and lives counters in 8px tiles. See below. |
description |
string | Text shown in mod listings. |
dependencies |
string array | Names of packs this pack needs at load time. A Classic reskin must list "Classic". |
dependenciesChecksums |
string array | Checksum of each dependency, positionally matched to dependencies. Editable source packs have no checksum, so empty strings are normal; the exporter fills them in. |
dependenciesIds |
string array | The id of each dependency, positionally matched to dependencies; null for a dependency without one. Optional. When present, the dependency is found by its id, so it survives being renamed. The editor writes it when it saves the pack. |
preEntityUpdateScripts |
ScriptData array |
Lua scripts run once per frame before any entity updates. |
postEntityUpdateScripts |
ScriptData array |
Lua scripts run once per frame after all entity updates. |
settings |
object | Session rules. The rest of this page. |
Mod identity¶
A pack whose config.json has an id is identified by that id. A pack without one is identified by its name (its folder or file name), as before, and keeps working unchanged.
- Create it once, never change it. The id is a random UUID (version 4), such as
3f6c2a9e-8b1d-4c57-9e2f-7a0d5b4c1e88. It is not derived from the name or anything else. The editor creates one for every new mod. Changing an id makes the mod a different mod: replays, saves, netplay and other mods' references to the old id stop finding it. - Copying a mod folder copies its id. Give the copy a new id (or remove the field). When two loaded mods declare the same id, the second one is identified by its name and an error is logged.
- Built-in packs (
Classic,30th Anniversary,Ninpo Kage Bunshin) are always identified by name; anidon them is ignored. - References by name keep working. Everywhere a pack is referenced by name —
dependencies, apackNamein entity, level or script data, an audio replacement or palette folder,additionalCharacters, a Lua argument — the name resolves to the pack's id. Any of them may also be written as the pack'sidinstead, which survives a rename of that pack. Folder names (Palettes/<pack>,Audio Replacements/<pack>,Entity Replacements/Classic) stay names. - Recorded hashes. Fields that store a pack as a number, such as
settings.allowedCharacters[].packHash, hold the hash of the id when the pack has one. Data recorded before a pack declared an id — old replays, saves, preferences,packHashvalues — holds the hash of its name and is translated to the id automatically. - Netplay compares mods by id when they have one, so players may know the same mod under different names.
HUD layout¶
hudLayout contains independent normal and widescreen profiles. The campaign's profile takes precedence; otherwise the first existing player (in slot order) whose character pack supplies that profile selects it for the whole session. Profiles are not merged. A missing profile keeps the built-in layout, including expandHudSpacing if enabled.
Set "hidden": true (beside the profiles) on the campaign to turn the built-in HUD off entirely: names, health bars, scores, lives, the clock (including TIME OVER) and the continue/game-over prompts. Characters' hudScript drawings still show, so the pack can draw its own HUD; they receive score and lives.
Each profile accepts:
| Field | Meaning |
|---|---|
leftPlayerX, rightPlayerX |
Absolute X tile coordinates of the left and right player HUD origins. Omitted/null values retain the built-in columns. Row placement and automatic slot relocation still apply. |
timerOffsetX, timerOffsetY |
Offsets from the built-in clock position, in tiles; default 0. Move both its frame and digits. |
lives |
Optional single-row lives counter layout. Omitted/null keeps the built-in large counter. |
lives accepts leftX, rightX, and y, relative to the player HUD origin (default 0), plus width (2 or 3 tiles, default 3). It displays total lives, clamped to 0–99, with an icon when space permits. Viewer-driven inactive counters remain hidden. Drawing and clearing use the same configured bounds, including when a player HUD moves to another slot.
For example, a pack can move player columns outward and place its own art in the resulting space:
{
"hudLayout": {
"widescreen": {
"leftPlayerX": -4,
"rightPlayerX": 27,
"timerOffsetX": -1,
"lives": { "leftX": 18, "rightX": -6, "y": 2, "width": 3 }
},
"normal": {
"lives": { "leftX": 15, "rightX": 15, "y": 3, "width": 2 }
}
}
}
Custom meter sprites and their relative positions belong to the entity's hudScript; the engine interprets only these layout coordinates.
A ScriptData is { "packName": "...", "scriptName": "..." }, resolving to Scripts/<scriptName>.lua inside packName. The .lua extension is optional.
Every pack you reference anywhere — a sprite reference, a logic reference, a spawned entity, a replacement target, a borrowed sound — should appear in dependencies. A reference to a pack that is not loaded is not a load error; it fails later, silently, as a missing sprite or sound.
Session settings¶
settings is a GameSessionData. It is what makes a Chronicle play differently from Classic.
Core rules¶
| Field | Type | Default | Meaning |
|---|---|---|---|
hasWideScreen |
bool | false | Allow the wider-than-original playfield. |
hasImprovements |
bool | false | Enable the New Era quality-of-life changes over original ROM behaviour. Turning this off is what "Classic rules" means. |
bufferGlitchFixMode |
int | 0 | How far to go fixing the original sprite buffer glitch: 0 NoFix, 1 AllButGroundUpper, 2 All. |
emulateClassicLag |
bool | false | Reproduce the original hardware slowdown. |
useSmoothControls |
bool | true | Use modern smoothed input handling rather than the original stepped one. |
useCoupleEnemiesAdvanceCheck |
bool | true | Keep the original rule that the camera only advances once few enough enemies remain. |
initialPlayerHealth |
int | max | Starting health for players. |
scoreForLives |
int array | none | Score thresholds granting an extra life, ascending. |
Player count¶
| Field | Type | Default | Meaning |
|---|---|---|---|
minimumPlayers |
int | 1 | Fewest players this pack supports. |
maximumPlayers |
int | -1 | Most players. -1 means the engine maximum. |
Optional mechanics¶
| Field | Type | Default | Meaning |
|---|---|---|---|
hasEnemiesHittingEachOther |
bool | false | Enemy attacks damage other enemies. |
hasFriendlyDamage |
bool | false | Players can damage each other. Without it, players also cannot grab each other, and a thrown body never hits a player. |
thrownCharactersSparePlayers |
bool | false | With hasFriendlyDamage on, a character thrown by a player does not hit or knock down other players; players can still hit and grab each other. No effect without hasFriendlyDamage. |
keepDirectionOnHit |
bool | false | Do not force the victim to turn towards the attacker when hit. |
hasNinpoKageBunshin |
bool | false | Enable the ninja shadow-clone mechanic. |
hasKillerEnemies |
bool | false | Killer enemies: attack frames without a hitbox play for one update, and an attack that hits a player stuns and restarts instead of knocking down, so it chains. See Killer Mode. |
hasExplodingEnemies |
bool | false | Enemies explode on death. |
hasInstantSpawns |
bool | false | Skip spawn introduction sequences. |
hasInfiniteContinuesOverrider |
bool | false | Force infinite continues regardless of difficulty. |
Global multipliers¶
These override the corresponding per-difficulty multipliers for the whole pack.
| Field | Applies to |
|---|---|
enemiesHealthMultiplierOverrider |
Enemy health. |
enemiesDamageMultiplierOverrider |
Damage enemies deal. |
enemiesVelocityMultiplierOverrider |
Enemy movement speed. |
playersDamageMultiplierOverrider |
Damage players deal. |
scrollSpeedMultiplier |
Level scroll speed. |
A multiplier is a rational number, not a float:
"enemiesHealthMultiplier": { "numerator": 3, "denominator": 2 }
The game uses integer maths so that replays and netplay stay deterministic across machines. denominator must not be zero.
Randomisation¶
| Field | Type | Default | Meaning |
|---|---|---|---|
enemiesRandomizerMode |
int | 0 | Enemy randomiser mode. 0 disables randomisation. |
randomValue |
int | 0 | Seed used by the randomiser modes. |
enemiesMultiplier |
int | 1 | Multiplies how many enemies each wave spawns. |
Borrowing from other packs¶
| Field | Type | Meaning |
|---|---|---|
additionalCharacters |
string array | Names of other packs whose playable characters are offered alongside this pack's own. |
additionalMechanics |
string array | Names of other packs whose gameplay mechanics are layered onto this pack. |
Packs listed here should also be in the top-level dependencies.
Restricting character select¶
allowedCharacters limits character select to a specific list. An empty list means no restriction.
"allowedCharacters": [
{ "packHash": -346761093, "entityType": 1531339648 }
]
Unlike everywhere else in these formats, this identifies characters by hash, not name: packHash is the hash of the pack's id when it has one (the lowercase, hyphenated UUID string), otherwise of the pack name, and entityType is the hash of the entity folder name. See the name hash for how to compute them. -346761093 is "Classic".
Difficulty settings¶
difficultySettings is an ordered list. Index 0 is the easiest, and the in-game difficulty selection indexes into it.
Per difficulty (DifficultySetting):
| Field | Type | Meaning |
|---|---|---|
name |
string | Label on the difficulty screen. |
enemiesDifficulty |
int | Difficulty level passed to enemy AI, and the value a loadcue's minimumDifficulty is compared against. |
maxInitialLives |
int | Highest number of starting lives selectable. |
initialCredits |
int | Continues granted at the start of a run. |
hasInfiniteContinues |
bool | Never run out of continues. |
hasInstantEnemiesPulling |
bool | Enemies close distance immediately instead of easing in. |
settingsPerNumPlayers |
array | One entry per player count. |
settingsPerNumPlayers index n-1 applies to an n player game. Provide as many entries as maximumPlayers allows; the engine reads the entry matching the current player count.
Per player count (DifficultySettingPerNumberOfPlayers):
| Field | Type | Meaning |
|---|---|---|
maxNumEnemies |
int | Most enemies allowed on screen at once. |
enemiesHealthMultiplier |
Multiplier | Enemy health. |
bossesHealthMultiplier |
Multiplier | Boss health, separately from regular enemies. |
enemiesDamageMultiplier |
Multiplier | Damage enemies deal. |
enemiesVelocityMultiplier |
Multiplier | Enemy movement speed. |
playersDamageMultiplier |
Multiplier | Damage players deal. |
enemiesOnScreenActivationMultiplier |
Multiplier | Scales the on-screen enemy count that wave triggers compare against. |
timeActivationMultiplier |
Multiplier | Scales wave timer thresholds. |
useNextSpawnsDifficulty |
bool | Draw spawns from the next difficulty's loadcue set instead of this one's. |
{
"name": "Very Easy",
"maxInitialLives": 9,
"initialCredits": 3,
"settingsPerNumPlayers": [
{
"maxNumEnemies": 5,
"enemiesHealthMultiplier": { "numerator": 1, "denominator": 2 },
"bossesHealthMultiplier": { "numerator": 1, "denominator": 2 },
"enemiesDamageMultiplier": { "numerator": 1, "denominator": 1 },
"enemiesVelocityMultiplier": { "numerator": 1, "denominator": 1 },
"playersDamageMultiplier": { "numerator": 1, "denominator": 1 },
"enemiesOnScreenActivationMultiplier": { "numerator": 1, "denominator": 1 },
"timeActivationMultiplier": { "numerator": 1, "denominator": 1 }
}
]
}
An editor-created pack ships a large, fully populated difficultySettings. Keep it unless you know which behaviour you are changing: an empty or short list narrows how many difficulties players can pick.