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; an id on them is ignored.
  • References by name keep working. Everywhere a pack is referenced by name — dependencies, a packName in 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's id instead, 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, packHash values — 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.