Game Bundle Analysis¶
Checked against client release: a4a25ae6
- Source:
Game.bundle.a4a25ae6d735e29092f6.js(Empire HTML5 client), plusdll/ggs.dll.145565eddbcbe244aab0.jsfor 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.htmllists 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:
2. Terminology: commander, baron, general¶
Three distinct things share the internal name "lord":
- Commander —
CommanderVO extends LordVO. Returned bygliunder keyC(CommKeys.COMMANDERS = "C"). This is what a player picks when sending an attack, and whatLIDrefers to on movement commands. The UI names them via the loca keycommander_index("Commander 1", "Commander 2", ...). - Castellan —
BaronVO extends LordVO,isBaronin the client. Also returned bygli, under keyB. The UI calls it a castellan (CastleEquipmentDialog.CASTELLAN,isBaron ? "castellan" : "general"). - General — a separate, newer system (
gielists them,glaassigns one,gaaesets its abilities). A general is assigned to a commander; the two are not interchangeable.gcsbelongs to the same screen but carries no general: it is the generals hub's quest progress, oneCHRentry 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:
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¶
- Find the VO — search
function C2S...VOfor the class definition; the constructor body lists every JSON key it emits. - Map arguments — minified parameter names are positional, so read the
this.XX = <arg>assignments in order. - Find usage — search
new ...C2S...VO(for a call site to learn what each argument actually is. - Resolve key constants —
CommKeys.FOOvalues live inggs.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) setsisRuinfrom1 == parseInt(e.R), so the flag lives on the owner record: a map scan reports it asRon the entries of itsOIlist. An owner record has noX/Ykeys. Its player's castles and villages are listed underAPandVP, 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 areMapObject.is_ruin,castle_positionsandvillage_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: