Skip to content

Game Bundle Analysis

Checked against client release: a4a25ae6

  • Source: Game.bundle.a4a25ae6d735e29092f6.js (Empire HTML5 client), plus dll/ggs.dll.145565eddbcbe244aab0.js for the shared protocol key constants.
  • Line numbers: "bundle line N" below is line N of the bundle split with sed 's/;/;\n/g; s/}/}\n/g'. They drift with every release; search by name.
  • Date: 2026-10-01
  • Where they come from: https://empire-html5.goodgamestudios.com/default/index.html lists the current bundle hashes; they change on every client release.

The client uses SmartFoxServer (SFS) with a mix of XML (handshake) and JSON-over-string (extended protocol %xt%). Command IDs live in ClientConstSF and payload key names in CommKeys.

1. Command IDs

Extracted from ClientConstSF (bundle line 71).

Category Constant Command Description
Auth C2S_LOGIN lli Login
Auth C2S_VERSION_CHECK vck Pre-login version check
Attack C2S_CREATE_ARMY_ATTACK_MOVEMENT cra Send attack
Attack C2S_GET_ATTACK_INFO gai Attack counter, not a pre-calculation - see combat_effects.md
Attack C2S_GET_ATTACK_CASTLE_INFOS aci The real attack pre-calculation: target area, defender composition, wall/gate/moat bonuses
Commanders C2S_GET_LORDS_INFO gli Commander + baron list
Commanders C2S_RENAME_LORD_EVENT arl Rename a commander
Generals C2S_GET_GENERALS_INFO gie General list
Generals C2S_GENERAL_ASSIGN_LORD gla Assign a general to a commander
Generals C2S_GENERALS_HUB_STATUS gcs Generals hub quest status, per character (GeneralsData.parse_GCS, bundle line 113080)
Generals C2S_GENERALS_SET_ABILITIES gaae Set general abilities
Player C2S_GET_DETAILPLAYERINFO gdi Player profile
Player C2S_SEARCH_PLAYER wsp Search player by name
Castle C2S_RENAME_CASTLE arc Rename castle

To extract more:

grep -oE 'C2S_[A-Z_]+\s*=\s*"[a-z]+"' Game.bundle.js | sort -u

2. Terminology: commander, baron, general

Three distinct things share the internal name "lord":

  • Commander — CommanderVO extends LordVO. Returned by gli under key C (CommKeys.COMMANDERS = "C"). This is what a player picks when sending an attack, and what LID refers to on movement commands. The UI names them via the loca key commander_index ("Commander 1", "Commander 2", ...).
  • Castellan — BaronVO extends LordVO, isBaron in the client. Also returned by gli, under key B. The UI calls it a castellan (CastleEquipmentDialog.CASTELLAN, isBaron ? "castellan" : "general").
  • General — a separate, newer system (gie lists them, gla assigns one, gaae sets its abilities). A general is assigned to a commander; the two are not interchangeable. gcs belongs to the same screen but carries no general: it is the generals hub's quest progress, one CHR entry per character id (CID).

EmpireCore names the Python API after the commander, and keeps the server's own key and command names (gli, LID, ID) on the wire.

3. gli — commander and castellan list

CastleLordData.parse_GLI (bundle line 38553) reads:

{"B": [ ...castellans... ], "C": [ ...commanders... ]}

Both entry kinds parse through LordVO.parseLord (bundle line 26451):

Key Meaning
ID Commander/castellan ID
N Name the player gave it. A commander shows it in commander_index ("Commander 2: name"); a castellan's name getter ignores it
E Raw effects (list)
AE Area effects (list)
W Wins
D Defeats
SPR Win spree
EQ Equipment entries; entry[1] is the slot type (helmet/armor/weapon/artifact/skin/hero)
VIS Portrait id (picID), read through int()
AIE / TAE Alien / temporary equipment, whichever is present, read only when EQ is empty: a list of [effect_id, values] rows, or [hero_rows, equipment_rows]
GEM Gem ids for the AIE/TAE equipment
GID The assigned general's id (LordVO.parseGeneral, bundle line 26480). For a gli entry the client looks it up among the player's generals (generalsData.playerGenerals)
ST, L, XP, ... Not read from a gli entry. GeneralVO.parseData reads them from the lord entry itself only when parseLord's second argument is true, which LordFactory.createLord (bundle line 26401) passes for a default lord (negative id); parse_GLI never does
LICID Castellans only (BaronVO.parseLord): the castle it is locked in, read through int(); >= 0 keeps it off movements

Commanders are sorted, then given playerIndex = position + 1 for display.

4. cra — send attack

C2SCreateArmyAttackMovementVO (bundle line 60851), built by CastleAttackData.sendAttack (bundle line 133852):

C2SCreateArmyAttackMovementVO(srcPos, tgtPos, armyData, waitTime, horses, bpc, attackType, av, lp, lordId, fc, kingdomId, ptt, sd, isCollector, collectorBooster, toolsSupportWodIds, yardWaveSlotList, autoSkipCooldownType)

Key Meaning
SX, SY Source absolute map coordinates
TX, TY Target absolute map coordinates
A Wave list (see below)
KID Source kingdom ID
LID Commander leading the attack. Every id in the gli C list is a real commander, 0 included (the free starting one); the server echoes the chosen one back under AAM.UM.L. An id outside the list is INVALID_LORD_ID (219) and a castellan already posted to a castle is LORD_IS_USED (256), both checked before the army is. -14 is the premium commander (TravelConst.COMMANDER_PREMIUM), not a no-commander value. The client leads with it only when the player picks it, and then sends BPC 1, which uses a premium commander or, when none are left, costs rubies after a confirmation (CastlePostAttackDialog.startAttack)
WT Wait time
HBW Horse type, forced to -1 when the feather flag is set
BPC Premium commander: 1 when the premium commander (LID -14) leads, which uses a premium commander or costs rubies (CastlePostAttackDialog.startAttack); 0 for any other commander
ATT CombatConst.ATTACK_TYPE_ATTACK or ATTACK_TYPE_CONQUER
AV Bool flag (0/1)
LP Int
FC Send anyway: 1 when resending after ATTACK_IN_PROGRESS (234), from CastlePostPostAttackFactionDialog's yes button; the attack dialog itself passes false
PTT Feathers (0/1)
SD Slowdown
ICA Collector attack (0/1)
BKS Collector booster
AST Support tool WOD IDs
CD Hardcoded 99
RW Yard wave slot list (yardWaveContainer.getSlotList(true))
ASCT Auto-skip cooldown type

C2SCreateArmyAttackMovementAdvisorVO extends this with advisor fields when the player has advisor attacks left.

Verified against a live server: an accepted cra answers with the created movement under AAM.M (MID, travel time, source and target areas), and the movement carries the leading commander's equipment and effects. An army built only from tools or event boosts is rejected with MOVEMENT_HAS_NO_UNITS (100), so the wave's U slots must reference actual barracks units.

The A army payload

CastleAttackArmyVO.getArmyData() returns one entry per wave, skipping any wave whose unit total is zero (isWaveComplete). Each entry is:

{"L": {"T": [[wodId, amount]], "U": [[wodId, amount]]},
 "M": {"T": [], "U": []},
 "R": {"T": [], "U": []}}

L/M/R are the left, middle and right flank; T is tools and U is units. Slot lists come from CastleFightItemContainer.getSlotList.

Wave count is CombatConst.getMaxWaveCountWithBonus(userLevel, areaType, bonus), where the bonus is the ADDITIONAL_WAVE legend skill.

5. Fill Waves

The attack dialog's "Fill waves" button (dialog_attack_autofill_fillWaves_button) runs entirely on the client. AttackDialogAutoFill.autoFillSelectedWaves (bundle line 101320) calls, per selected wave:

new StrongestDefenceCounterWaveStrategy().fillWave(
    attackInfo, options, wave,
    FightScreenHelper.getAttackerEffectVO(wave, selectedLord, isLegendaryFight, attackInfo),
    FightScreenHelper.getDefenderEffectVO(attackInfo))

AFillWaveStrategy.fillWave fills each enabled flank in turn — tools first, then soldiers, then checkFlank — from a filtered copy of the unit inventory, and writes the inventory back at the end. Options are fillLeftFlank, fillRightFlank, fillMiddleFlank plus the unit/tool filters. The yard wave goes through fillYardContainer with the FLANK_YARD defender effects.

Reproducing it needs unit and tool stats, which the client gets from its items.xml-derived config (ITEM_XML_LOADER, read by CastleGame.parseConfigXML at bundle line 13426, → CastleModel.xmlPropertyData, built with @goodgamestudios/itemsxml2json). That blob's URL is injected through the loader parameters, not hardcoded in any bundle.

6. Reverse-engineering recipe

  1. Find the VO — search function C2S...VO for the class definition; the constructor body lists every JSON key it emits.
  2. Map arguments — minified parameter names are positional, so read the this.XX = <arg> assignments in order.
  3. Find usage — search new ...C2S...VO( for a call site to learn what each argument actually is.
  4. Resolve key constants — CommKeys.FOO values live in ggs.dll.js (grep -o 'FOO="[^"]*"' ggs.dll.js).

See combat_effects.md in the repository for the full effect and bonus catalogue behind an attack.

7. Map object types

WorldConst.AREA_TYPE_* (in the dll) gives the values; the client's area-type-to-map-object registration (bundle line 5357) confirms what each one is. The table lists the common ones, not every type:

ID Constant Map object
1 CASTLE CastleMapobjectVO
2 DUNGEON DungeonMapobjectVO - the robber baron camp
3 CAPITAL CapitalMapobjectVO
4 OUTPOST OutpostMapobjectVO
7 TREASURE_DUNGEON TreasureDungeonMapObjectVO
11 BOSS_DUNGEON BossdungeonMapobjectVO
12 KINGDOM_CASTLE KingdomCastleMapobjectVO
27 NOMAD_CAMP NomadCampMapObjectVO
31 DYNAMIC PlaceholderMapobjectVO
35 ALLIANCE_NOMAD_CAMP NomadKhanCampMapObjectVO - the khan camp

Two things are not area types:

  • Ruins. No ruin map object is registered. WorldMapOwnerInfoVO.fillFromParamObject (bundle line 10794) sets isRuin from 1 == parseInt(e.R), so the flag lives on the owner record: a map scan reports it as R on the entries of its OI list. An owner record has no X/Y keys. Its player's castles and villages are listed under AP and VP, each entry [kingdom, object id, x, y, area type] (MinWorldMapCastleInfoVO.fillFromParamObject, bundle line 18459), so a ruin's castles are placed from those lists. In EmpireCore they are MapObject.is_ruin, castle_positions and village_positions.
  • Khan camps. They appear during the nomad event as ALLIANCE_NOMAD_CAMP (35), not under a type of their own.

Extract the full table with:

grep -oE 'AREA_TYPE_[A-Z_]+\s*=\s*-?[0-9]+' ggs.dll.js | sort -u