Skip to content

Commanders

Commanders, castellans, equipment and generals behind client.commanders, client.equipment and client.skills.

commanders.service

Commanders and castellans, their equipment, and generals and skills.

  • CommandersService reads and renames the player's commanders and castellans, and says whether the premium commander leads for free.
  • EquipmentService reads the equipment inventory and moves items on and off commanders and castellans.
  • SkillsService reads a general's unlocked skills and the player's own, and changes generals: assigning one to a commander, choosing its abilities, unlocking and resetting its skills, and feeding it xp items. A general's skills widen the flanks whatever the target is; the player's legend skills widen them further and add waves, but only when both sides are at the level cap.

Classes:

Name Description
CommandersService

Your commanders and castellans, and renaming them.

EquipmentService

The equipment inventory, and putting items on and off commanders and castellans.

SkillsService

Generals, their abilities and skills, and the player's legend and sceat skills.

CommandersService

CommandersService(client: EmpireClient)

Bases: BaseService

Your commanders and castellans, and renaming them.

Reached as client.commanders.

Methods:

Name Description
get_all

Get commanders and castellans in one request.

get_commanders

Get all available commanders.

get_castellans

Get all available castellans.

rename

Rename a commander or castellan.

free_premium_commanders

How many more times today the premium commander (PREMIUM_COMMANDER_ID) can lead for free.

premium_commander_is_free

Whether leading with the premium commander now costs no rubies: a premium account

premium_send

Run send, refusing it first when the premium commander leads and may cost rubies, unless spend_rubies.

get_all

get_all(timeout: float = 5.0) -> GetCommandersResponse

Get commanders and castellans in one request.

Parameters:

Name Type Description Default
timeout float

Timeout in seconds

5.0

Returns:

Type Description
GetCommandersResponse

The full gli response

Raises:

Type Description
CommandError / EmpireTimeoutError / ConnectionClosedError

The request failed

get_commanders

get_commanders(timeout: float = 5.0) -> list[Commander]

Get all available commanders.

Parameters:

Name Type Description Default
timeout float

Timeout in seconds

5.0

Returns:

Type Description
list[Commander]

List of Commander objects

Raises:

Type Description
CommandError / EmpireTimeoutError / ConnectionClosedError

The request failed

get_castellans

get_castellans(timeout: float = 5.0) -> list[Castellan]

Get all available castellans.

Parameters:

Name Type Description Default
timeout float

Timeout in seconds

5.0

Returns:

Type Description
list[Castellan]

List of Castellan objects

Raises:

Type Description
CommandError / EmpireTimeoutError / ConnectionClosedError

The request failed

rename

rename(commander_id: int, name: str, timeout: float = 5.0) -> RenameCommanderResponse

Rename a commander or castellan.

Parameters:

Name Type Description Default
commander_id int

The leader's commander_id, a Commander or Castellan from client.commanders.get_all()

required
name str

The new name; the game's dialog allows 3 to 15 characters

required
timeout float

Timeout in seconds

5.0

Returns:

Type Description
RenameCommanderResponse

The arl response, which carries the updated commander list

Raises:

Type Description
CommandError / EmpireTimeoutError / ConnectionClosedError

The request failed

free_premium_commanders

free_premium_commanders(now: float | None = None) -> int | None

How many more times today the premium commander (PREMIUM_COMMANDER_ID) can lead for free.

Your VIP level's free_premium_commanders_per_day less those used today, and 0 while no VIP time runs. It comes from state, kept current by the login data's vip and every vip the server pushes. A premium send this client made (premium_send) counts as used until the next vip arrives. A premium account makes the premium commander free on top of this: premium_commander_is_free counts both.

Example
if client.commanders.premium_commander_is_free():
    client.castle.send_support(..., commander_id=PREMIUM_COMMANDER_ID, use_premium_commander=True)

Parameters:

Name Type Description Default
now float | None

Wall-clock time to measure the VIP time left at, time.time() when not given

None

Returns:

Type Description
int | None

The count, 0 at least, or None before the login data brought your VIP

Raises:

Type Description
GameDataNotLoadedError

VIP time runs and the game data cannot be loaded

Client: CastleVIPData.remainingPremiumCommanders (bundle line 47554), the freePremiumCommandersPerDay of currentActiveVIPLevel (bundle line 47546), which is a level with none unless vipModeActive (bundle line 47550); parse_VIP (bundle line 47527) reads UPG, from GBDCommand (bundle line 129381), VIPCommand (bundle line 129941) and SBPCommand (bundle line 128320)

premium_commander_is_free

premium_commander_is_free() -> bool

Whether leading with the premium commander now costs no rubies: a premium account runs, or free_premium_commanders is 1 or more.

False when neither is known yet.

Raises:

Type Description
GameDataNotLoadedError

No premium account runs, VIP time does, and the game data cannot be loaded

Client: CastlePostAttackDialog.startAttack (bundle line 38360), which sends with a free premium commander left and otherwise asks for getFinalCostsC2(TravelConst.TRAVEL_PREMIUM_COMMANDER_COSTS_C2) rubies, 0 while premiumAccountVO.isActive; TRAVEL_PREMIUM_COMMANDER_COSTS_C2 is 125 (dll line 19872)

premium_send

premium_send(commander_id: int, use_premium_commander: bool, send: Callable[[], bool], *, spend_rubies: bool = False) -> bool

Run send, refusing it first when the premium commander leads and may cost rubies, unless spend_rubies.

The army sends go through this: client.castle.send_support, client.castle.send_troops and client.attack.send_attack. The premium commander leads when use_premium_commander is set or commander_id is PREMIUM_COMMANDER_ID; the client always sends the two together.

Library policy, not client behaviour: the server sends no vip with the send's reply (CRACommand, bundle line 125954; CDSCommand, bundle line 125939; CATCommand, bundle line 125924), so a premium send that send reports accepted while no premium account runs counts as one used in free_premium_commanders until the next vip arrives. The check and the send hold one lock, so two premium sends at once cannot both take the last free one.

Parameters:

Name Type Description Default
commander_id int

The leader's commander_id

required
use_premium_commander bool

The send's BPC flag

required
send Callable[[], bool]

Sends the request; True when the server accepted it

required
spend_rubies bool

Send even when the premium commander may cost rubies

False

Returns:

Type Description
bool

What send returned

Raises:

Type Description
PremiumCommanderCostError

The premium commander leads, premium_commander_is_free is False and spend_rubies is False

GameDataNotLoadedError

See premium_commander_is_free

Client: CastlePostAttackDialog.startAttack (bundle line 38360) sends BPC 1 exactly when the selected commander is TravelConst.COMMANDER_PREMIUM

EquipmentService

EquipmentService(client: 'EmpireClient')

Bases: BaseService

The equipment inventory, and putting items on and off commanders and castellans.

Reached as client.equipment. What a leader wears comes with client.commanders (gli EQ).

Methods:

Name Description
get_inventory

The items in the player's inventory, the ones no leader wears.

equip

Put an item on a commander or castellan.

unequip

Take an item off a commander or castellan.

get_inventory

get_inventory(timeout: float = 5.0) -> list[Equipment]

The items in the player's inventory, the ones no leader wears.

Raises:

Type Description
CommandError / EmpireTimeoutError / ConnectionClosedError

The request failed

equip

equip(equipment_id: int, commander_id: int, timeout: float = 5.0) -> bool

Put an item on a commander or castellan.

The client moves an item between leaders by taking it off the first and then putting it on the second. The reply carries no data; re-read client.commanders.get_all() to see the change.

Parameters:

Name Type Description Default
equipment_id int

An item in the inventory, its Equipment.equipment_id from get_inventory

required
commander_id int

The leader's commander_id, a Commander or Castellan from client.commanders.get_all()

required
timeout float

Timeout in seconds

5.0

Returns:

Type Description
bool

False when the server rejects the request

unequip

unequip(equipment_id: int, commander_id: int, timeout: float = 5.0) -> bool

Take an item off a commander or castellan.

Parameters:

Name Type Description Default
equipment_id int

An item the leader wears, the Equipment.equipment_id of an entry in its equipment list from client.commanders.get_all()

required
commander_id int

The leader's commander_id, a Commander or Castellan from client.commanders.get_all()

required
timeout float

Timeout in seconds

5.0

Returns:

Type Description
bool

False when the server rejects the request

SkillsService

SkillsService(client: EmpireClient)

Bases: BaseService

Generals, their abilities and skills, and the player's legend and sceat skills.

Reached as client.skills.

Methods:

Name Description
get_generals

Every general the player owns, with the skills each has unlocked.

assign_general

Assign a general to a commander; general_id=-1 takes its general away.

set_abilities

Choose a general's abilities.

unlock_skill

Unlock a general skill; the skill id names the general.

reset_skills

Reset a general's skill tree.

add_xp

Feed a general xp items.

get_skills

The player's legend and sceat skills.

on_skill_list

Register a callback for every skill list the server sends.

get_generals

get_generals(timeout: float = 5.0) -> GetGeneralsResponse

Every general the player owns, with the skills each has unlocked.

Parameters:

Name Type Description Default
timeout float

Timeout in seconds

5.0

Returns:

Type Description
GetGeneralsResponse

The gie response

Raises:

Type Description
CommandError / EmpireTimeoutError / ConnectionClosedError

The request failed

assign_general

assign_general(commander_id: int, general_id: int, timeout: float = 5.0) -> AssignGeneralResponse

Assign a general to a commander; general_id=-1 takes its general away.

Parameters:

Name Type Description Default
commander_id int

A Commander.commander_id from client.commanders.get_commanders()

required
general_id int

An OwnedGeneral.general_id from get_generals; GameData.require().general(name) finds its generalID by name

required
timeout float

Timeout in seconds

5.0

Returns:

Type Description
AssignGeneralResponse

The gla response, with the commander list after the change

Raises:

Type Description
CommandError / EmpireTimeoutError / ConnectionClosedError

The request failed

set_abilities

set_abilities(general_id: int, abilities: Iterable[tuple[int, int | None]], timeout: float = 5.0) -> bool

Choose a general's abilities.

Parameters:

Name Type Description Default
general_id int

An OwnedGeneral.general_id from get_generals; GameData.require().general(name) finds its generalID by name

required
abilities Iterable[tuple[int, int | None]]

(slot_id, ability_id) pairs, -1 or None to clear a slot. The client sends every slot it shows. An ability id comes from GameData.require().general_ability(name, level)

required
timeout float

Timeout in seconds

5.0

Returns:

Type Description
bool

True if the server accepted the change

unlock_skill

unlock_skill(skill_id: int, timeout: float = 5.0) -> bool

Unlock a general skill; the skill id names the general.

The general's new skills arrive with the next gie.

Parameters:

Name Type Description Default
skill_id int

The skill level's skillID, from GameData.require().general_skill(general_id, name, level) (GeneralVO.unlockSkill, bundle line 26773)

required
timeout float

Timeout in seconds

5.0

Returns:

Type Description
bool

True if the server accepted the unlock

reset_skills

reset_skills(general_id: int, timeout: float = 5.0) -> bool

Reset a general's skill tree.

Parameters:

Name Type Description Default
general_id int

An OwnedGeneral.general_id from get_generals; GameData.require().general(name) finds its generalID by name

required
timeout float

Timeout in seconds

5.0

Returns:

Type Description
bool

True if the server accepted the reset

add_xp

add_xp(general_id: int, currency_id: int, amount: int, timeout: float = 5.0) -> bool

Feed a general xp items.

Parameters:

Name Type Description Default
general_id int

An OwnedGeneral.general_id from get_generals; GameData.require().general(name) finds its generalID by name

required
currency_id int

The xp item's currencyID, e.g. GameData.require().currency("GXP1").currency_id (GeneralsLevelUpDialogListItem.sendXPSelected, bundle line 74485)

required
amount int

How many of the item to use

required
timeout float

Timeout in seconds

5.0

Returns:

Type Description
bool

True if the server accepted the items

get_skills

get_skills(timeout: float = 5.0) -> GetSkillsResponse

The player's legend and sceat skills.

Parameters:

Name Type Description Default
timeout float

Timeout in seconds

5.0

Returns:

Type Description
GetSkillsResponse

The skl response

Raises:

Type Description
CommandError / EmpireTimeoutError / ConnectionClosedError

The request failed

on_skill_list

on_skill_list(callback: Callable[[SkillList], object]) -> None

Register a callback for every skill list the server sends.

That is each skl packet, including the reply to get_skills, and the skl block of an ego push.

Client: SKLCommand.executeCommand (bundle line 129742) and EGOCommand.executeCommand (bundle line 122801) both call parse_SKL.

Removing a callback not registered is a no-op.

Unregister a callback with on_skill_list.remove(callback).

commanders.models.equipment

Equipment models and equipment inventory commands.

Commands:

  • gei: Get Equipment Inventory - the items no commander or castellan wears
  • eeq: Equip Equipment - put an item on a commander or castellan, or take it off

Classes:

Name Description
EquipmentBonus

One bonus of an item that is not a relic: [effect_id, values].

RelicBonus

One bonus of a relic item: [relic_effect_id, power, values].

RelicGem

The gem set in a relic item: [gem_id, relic_type_id, relic_category_id, might, bonuses, enchantment_level].

RelicInfo

What a relic item carries at index 12: [relic_type_id, relic_category_id, might, gem].

Equipment

An equipment item worn by a commander or castellan.

GetEquipmentInventoryRequest

Request the player's equipment inventory.

GetEquipmentInventoryResponse

The player's equipment inventory.

EquipEquipmentRequest

Put an item on a commander or castellan (equip=1), or take it off (equip=0).

Attributes:

Name Type Description
logger
NO_GEM_ID

logger module-attribute

logger = logging.getLogger(__name__)

NO_GEM_ID module-attribute

NO_GEM_ID = -1

EquipmentBonus pydantic-model

Bases: BasePayload

One bonus of an item that is not a relic: [effect_id, values].

Client: BasicEquipmentVO.parseBonuses (bundle line 7135), which wraps a value that is not an array into one.

Fields:

Validators:

  • _from_row

effect_id pydantic-field

effect_id: int

Equipment effect id

values pydantic-field

values: list[Any]

Value array; its layout depends on the effect type

RelicBonus pydantic-model

Bases: BasePayload

One bonus of a relic item: [relic_effect_id, power, values].

Client: RelicItemInfoVO.parseRelicBoni (bundle line 33743), RelicBonusVO.parseRelicFromValueArray (bundle line 45132), which looks the id up in the relic effect table and parses row[2].toString(), so a value and a one-value array read the same.

Fields:

Validators:

  • _from_row

relic_effect_id pydantic-field

relic_effect_id: int

Relic effect id

power pydantic-field

power: float = 0

Power

values pydantic-field

values: list[Any]

Value array

RelicGem pydantic-model

Bases: BasePayload

The gem set in a relic item: [gem_id, relic_type_id, relic_category_id, might, bonuses, enchantment_level].

Client: RelicGemVO.parseServerObject (bundle line 22222)

Fields:

Validators:

  • _from_row

gem_id pydantic-field

gem_id: int

Gem id

relic_type_id pydantic-field

relic_type_id: int = 0

Relic type id

relic_category_id pydantic-field

relic_category_id: int = 0

Relic category id

might pydantic-field

might: int | float = 0

Might

bonuses pydantic-field

bonuses: list[RelicBonus]

The gem's relic bonuses

enchantment_level pydantic-field

enchantment_level: ClientInt = 0

Enchantment level

RelicInfo pydantic-model

Bases: BasePayload

What a relic item carries at index 12: [relic_type_id, relic_category_id, might, gem].

Client: RelicEquipmentVO.parseEquipFromArray (bundle line 25039)

Fields:

Validators:

  • _from_row
  • _gem_or_none → gem

relic_type_id pydantic-field

relic_type_id: int = 0

Relic type id

relic_category_id pydantic-field

relic_category_id: int = 0

Relic category id

might pydantic-field

might: int | float = 0

Might

gem pydantic-field

gem: RelicGem | None = None

The gem set in the relic; None when there is none

Equipment pydantic-model

Bases: BasePayload

An equipment item worn by a commander or castellan.

Parsed from a positional EQ entry: [id, slot, wearer, rarity, graphic, bonuses, unique_id, set_id, enchantment_level, duration_seconds, gem_id, equipment_type] Entries are truncated by the server when trailing fields do not apply.

Index 5 holds the bonuses: a relic item (index 11 is 3) lists them as relic_bonuses ([relic_effect_id, power, values]), any other item as bonuses ([effect_id, values]). A hero item (slot 6, not a relic) also keeps index 11 as alien_string, which the client matches against the unique heroes (CastleEquipmentData.getUniqueHerosByAlienString, bundle line 143843); the type is still read from it through int().

Client: BasicEquipmentVO.parseEquipFromArray (bundle line 7115), CastleEquipmentFactory.createEquipmentVO (bundle line 18134), RelicEquipmentVO.parseEquipFromArray (bundle line 25039), CastleHeroVO.parseEquipFromArray (bundle line 40585), BasicEquipmentVO.hasSetbonus (bundle line 7213).

Fields:

Validators:

equipment_id pydantic-field

equipment_id: int = 0

Item id

slot pydantic-field

slot: int = 0

Slot type id

wearer_type pydantic-field

wearer_type: int = WearerType.UNDEFINED

Who can wear it (WearerType)

rarity_id pydantic-field

rarity_id: ClientInt = 0

Rarity id

graphic pydantic-field

graphic: int | str = 0

The item's graphic

bonuses pydantic-field

bonuses: list[EquipmentBonus]

Bonuses of an item that is not a relic

relic_bonuses pydantic-field

relic_bonuses: list[RelicBonus]

Bonuses of a relic item

unique_id pydantic-field

unique_id: ClientInt = 0

Unique item id

set_id pydantic-field

set_id: ClientInt = 0

Equipment set id, -1 for none; 0, which no set uses, when the row has none

enchantment_level pydantic-field

enchantment_level: ClientInt = 0

Enchantment level

duration_seconds pydantic-field

duration_seconds: int | float = 0

Seconds until the item expires; below 1 it is permanent

gem_id pydantic-field

gem_id: EnumOrInt['Gem'] | None = None

The slotted gem; None for none

equipment_type pydantic-field

equipment_type: ClientInt = EquipmentType.GENERATED

EquipmentType value

relic_info pydantic-field

relic_info: RelicInfo | None = None

A relic item's type, category, might and gem; None for other items

alien_string pydantic-field

alien_string: str | None = None

A hero item's alien string, "effect_id&value" pairs joined by ","; None for other items. A number the server sends there is kept as its text

is_permanent property

is_permanent: bool

True when the item does not expire (the server sends -1).

has_gem property

has_gem: bool

True when a gem is slotted.

has_set property

has_set: bool

True unless set_id is -1, as the client's hasSetbonus reads it.

is_relic property

is_relic: bool

True for a relic item, whose bonuses index the relic effect table.

rarity_enum property

rarity_enum: Rareness | None

rarity_id as a Rareness, None for a value the client does not define.

from_list classmethod

from_list(data: list[Any]) -> 'Equipment'

Parse from an EQ array entry, tolerating short entries.

GetEquipmentInventoryRequest pydantic-model

Bases: BaseRequest

Request the player's equipment inventory.

Command: gei Payload: {} Client: C2SGetEquipmentInventory (bundle line 19854)

GetEquipmentInventoryResponse pydantic-model

Bases: BaseResponse

The player's equipment inventory.

Command: gei Payload: {"I": [EQ entry, ...]}, each entry laid out as a gli EQ entry.

Client: CastleEquipmentData.parse_GEI (bundle line 143731), which builds every entry with CastleEquipmentFactory.createEquipmentVO (bundle line 18134).

Fields:

Validators:

  • _readable_items → items

items pydantic-field

items: list[Equipment]

Wire key I

Inventory items

EquipEquipmentRequest pydantic-model

Bases: BaseRequest

Put an item on a commander or castellan (equip=1), or take it off (equip=0).

Moving an item between two leaders takes two requests: the client takes it off the first, then puts it on the second. The reply has no body.

Command: eeq Payload: {"EID": equipment_id, "LID": commander_id, "E": 0 or 1} Client: C2SEquipEquipmentVO (bundle line 143914) sends E as int(n?1:0); CastleEquipmentData.startDrag (bundle line 143801) sends 0 when an item is lifted off a leader's slot, stopDrag (bundle line 143807) sends 1 when it is dropped on one, and EquipmentEquipmentClickHandler.init (bundle line 65626) handles the 0 reply as an unequip.

Fields:

Validators:

equipment_id pydantic-field

equipment_id: int

Wire key EID

Equipment.equipment_id: of an inventory item from client.equipment.get_inventory() to equip, of a worn one from client.commanders.get_all() to take off

commander_id pydantic-field

commander_id: int

Wire key LID

The commander_id of a Commander or Castellan from client.commanders.get_all()

equip pydantic-field

equip: int

Wire key E

1 puts the item on the leader, 0 takes it off

commanders.models.inventory

The equipment and gem inventory, as the login data and its pushes send it.

Commands:

  • ggm: the gems and relic gems you hold (gec: a change to the gems)
  • esl: the equipment and gem inventory space
  • nrf: whether new relics wait to be seen

Classes:

Name Description
GemStack

How many of one gem you hold.

GemInventoryResponse

The gems and relic gems in your inventory, not set in any equipment.

InventorySpace

Free and total space in the equipment and gem inventory.

NewRelicsResponse

Whether new relic equipment waits to be seen.

GemStack pydantic-model

Bases: BasePayload

How many of one gem you hold.

Fields:

model_config class-attribute

model_config: ConfigDict = ConfigDict(frozen=True)

gem_id pydantic-field

gem_id: EnumOrInt['Gem']

The gem

amount pydantic-field

amount: int

How many you hold

GemInventoryResponse pydantic-model

Bases: BaseResponse

The gems and relic gems in your inventory, not set in any equipment.

Command: ggm, as a login section of gbd and a reply. A gec push changes the gems; client.state.get_gems() applies it with changed.

Client: GGMCommand (bundle line 123957), CastleGemData.parseGGM, updateInventory and updateRelicInventory (bundle lines 144327, 144334, 144427); GECCommand (bundle line 123927) and parse_GEC (bundle line 144326). The client keeps one entry per gem and takes a negative amount off from the first entry of that gem on; here the gems are counted per id.

Fields:

Validators:

model_config class-attribute

model_config: ConfigDict = ConfigDict(frozen=True)

gems pydantic-field

gems: tuple[GemStack, ...] = ()

Wire key GEM

The gems you hold, per gem id

relic_gems pydantic-field

relic_gems: tuple[RelicGem, ...] = ()

Wire key RGEM

The relic gems you hold

amount

amount(gem_id: int) -> int

How many of a gem you hold.

changed

changed(rows: Any) -> GemInventoryResponse

This inventory after a gec's GEM rows: [gem_id, amount], a negative amount taken away.

InventorySpace pydantic-model

Bases: BasePayload

Free and total space in the equipment and gem inventory.

Not a response model: its E is the equipment space, not an error code.

Command: esl, as a login section of gbd, a push and a reply, and inside the bgm, ceq, cge, frc and seq replies.

Client: ESLCommand (bundle line 123895), CastleEquipmentData.parse_ESL (bundle line 143746) and CastleGemData.parse_ESL (bundle line 144410). The client recounts the free gem space from the total and the gems it holds whenever they change (updateInventorySpace, bundle line 144417); gem_space is the value the server sent. The equipment inventory counts as full at equipment_space 0 or less, and holds equipment_total_space - equipment_space items (isInventoryFull and filledInventorySpace, bundle lines 143839, 143841).

Fields:

model_config class-attribute

model_config: ConfigDict = ConfigDict(frozen=True)

equipment_space pydantic-field

equipment_space: ParseInt = 0

Wire key E

Free equipment inventory space; negative when you hold more than fits, full at 0 or less

equipment_total_space pydantic-field

equipment_total_space: ParseInt = 0

Wire key TE

Total equipment inventory space

gem_space pydantic-field

gem_space: ClientInt = 0

Wire key G

Free gem inventory space, as sent

gem_total_space pydantic-field

gem_total_space: ClientInt = 0

Wire key TG

Total gem inventory space

NewRelicsResponse pydantic-model

Bases: BaseResponse

Whether new relic equipment waits to be seen.

Command: nrf, as a login section of gbd and a push.

Client: NRFCommand (bundle line 124002), CastleEquipmentData.parseNRF (bundle line 143756)

Fields:

Validators:

model_config class-attribute

model_config: ConfigDict = ConfigDict(frozen=True)

has_new_relics pydantic-field

has_new_relics: bool = False

Wire key NR

Whether new relics wait to be seen

commanders.models.roster

Commander protocol models.

Commands:

  • gli: Get Lords Info - the server name for the commander/castellan list
  • arl: rename a commander or castellan

Classes:

Name Description
SelectedAbility

One ability slot of a general: a [slot_id, ability_id] pair, as GASAIDS sends it and SAIDS takes it.

AlienEquipment

Alien (AIE) or temporary (TAE) equipment: [effect_id, values] rows, or

LeaderBase

Fields shared by every gli entry.

Commander

A commander - the leader assigned to an attack or support movement.

Castellan

A castellan - the defensive counterpart of a commander (BaronVO).

GetCommandersRequest

Request the commander and castellan list.

CommanderRoster

A player's commanders and castellans, the gli block.

GetCommandersResponse

Response containing commanders (C) and castellans (B).

RenameCommanderRequest

Rename a commander or castellan.

RenameCommanderResponse

Reply to a rename: the full commander and castellan list.

Attributes:

Name Type Description
logger
PICTURE_FACTION_CASTELLAN

EquipmentConst.PICK_BARON_FACTION (dll line 19249)

PICTURE_ISLAND_CASTELLAN

EquipmentConst.PICK_BARON_ISLAND (dll line 19249)

FACTION_BARON_ID

FactionConst.BARON_ID (dll line 19333)

PREMIUM_COMMANDER_ID

TravelConst.COMMANDER_PREMIUM (dll line 19872): the premium commander's commander_id;

CASTELLAN_PICTURE_ORDER

BaronVO.PIC_ID_ORDER (bundle line 43551): castellans are listed in this portrait order

logger module-attribute

logger = logging.getLogger(__name__)

PICTURE_FACTION_CASTELLAN module-attribute

PICTURE_FACTION_CASTELLAN = 5

EquipmentConst.PICK_BARON_FACTION (dll line 19249)

PICTURE_ISLAND_CASTELLAN module-attribute

PICTURE_ISLAND_CASTELLAN = 13

EquipmentConst.PICK_BARON_ISLAND (dll line 19249)

FACTION_BARON_ID module-attribute

FACTION_BARON_ID = -16

FactionConst.BARON_ID (dll line 19333)

PREMIUM_COMMANDER_ID module-attribute

PREMIUM_COMMANDER_ID = -14

TravelConst.COMMANDER_PREMIUM (dll line 19872): the premium commander's commander_id; the client offers it from ClientConstLevelRestrictions.MIN_LEVEL_SELECT_PREMIUM_COMMANDER, level 10 (bundle line 2139)

CASTELLAN_PICTURE_ORDER module-attribute

CASTELLAN_PICTURE_ORDER = (0, 6, 7, 8, 1, 13, 2, 3, 4, 10, 11, 12, 9, 5)

BaronVO.PIC_ID_ORDER (bundle line 43551): castellans are listed in this portrait order

SelectedAbility pydantic-model

Bases: BasePayload

One ability slot of a general: a [slot_id, ability_id] pair, as GASAIDS sends it and SAIDS takes it.

It serializes back to the pair, with -1 for an empty slot.

Client: GeneralVO.parseData (bundle line 26666) keeps the pairs, and GeneralVO.getSelectedAbilities (bundle lines 26769-26771) reads [0] as the slot and [1] as the ability, counting it only above 0; GeneralsAbilityDialog.onSave (bundle line 27850) sends -1 for a cleared slot.

Fields:

Validators:

slot_id pydantic-field

slot_id: int

The slot, matched against the general's attack and defense slots

ability_id pydantic-field

ability_id: EnumOrInt['GeneralAbility'] | None = None

The ability; None for an empty slot

AlienEquipment pydantic-model

Bases: BasePayload

Alien (AIE) or temporary (TAE) equipment: [effect_id, values] rows, or [hero_rows, equipment_rows] when it carries a hero.

The block is split in two when it has two entries that are each empty or start with an array; otherwise every row is an equipment bonus.

Client: LordVO.parseLord (bundle lines 26471-26477), AlienLordHeroVO.parseAlienBoniData (bundle line 67502), AlienLordEquipmentVO.parseAlienBoniData (bundle line 67479)

Fields:

Validators:

hero_bonuses pydantic-field

hero_bonuses: tuple[EquipmentBonus, ...] = ()

The hero's bonuses

bonuses pydantic-field

bonuses: tuple[EquipmentBonus, ...] = ()

The equipment's bonuses

LeaderBase pydantic-model

Bases: BasePayload

Fields shared by every gli entry.

The wire protocol calls both kinds "lords" (command gli, field LID on movement commands); the game UI says commander and castellan.

AIE (alien) or TAE (temporary) equipment stands in for EQ: the client reads the first of them that is present, and only when EQ is empty. See alien_bonuses.

Client: LordFactory.createLord (bundle line 26399), LordVO.parseLord (bundle line 26451), LordVO.parseGeneral (bundle line 26480) and GeneralVO.parseData (bundle line 26666) for ST, L and the general's XP, OXP, IN, LU, SIDS and GASAIDS. The client reads those six only when the entry names a general above 0 and is a default commander or was created with LordFactory.createLord(e, true), as a battle log's are. An entry cannot tell the second case, so they are None unless sent, or filled with the client's defaults for a default commander with a general; elsewhere the client ignores them.

Fields:

Validators:

commander_id pydantic-field

commander_id: int

Wire key ID

Commander id; for a default commander, its default-commander id

wearer_id pydantic-field

wearer_id: ClientInt | None = None

Wire key WID

2 for a commander, 1 for a castellan

picture_id pydantic-field

picture_id: ClientInt = 0

Wire key VIS

Portrait id

name pydantic-field

name: str = ''

Wire key N

Name

wins pydantic-field

wins: ClientInt = 0

Wire key W

Battles won

defeats pydantic-field

defeats: ClientInt = 0

Wire key D

Battles lost

win_spree pydantic-field

win_spree: ClientInt = 0

Wire key SPR

Current winning streak

effects pydantic-field

Wire key E

The commander's own effects

area_effects pydantic-field

area_effects: CommanderEffects

Wire key AE

Area effects

equipment pydantic-field

equipment: list[Equipment]

Wire key EQ

Equipped items

alien_equipment pydantic-field

alien_equipment: AlienEquipment | None = None

Wire key AIE

Alien equipment; applies when equipment is empty

temporary_equipment pydantic-field

temporary_equipment: AlienEquipment | None = None

Wire key TAE

Temporary equipment; applies when equipment is empty and alien_equipment absent

alien_gem_ids pydantic-field

alien_gem_ids: tuple[EnumOrInt['Gem'], ...] = ()

Wire key GEM

The gems added to the alien or temporary equipment

general_id pydantic-field

general_id: ClientInt | None = None

Wire key GID

The assigned general's id; -1 or None for none

star_level pydantic-field

star_level: ClientInt = 0

Wire key ST

The general's star level when the entry doubles as its general: a default commander with a general_id above 0

level pydantic-field

level: ClientInt = 0

Wire key L

The general's level when the entry doubles as its general: a default commander with a general_id above 0

general_xp pydantic-field

general_xp: ClientInt | None = None

Wire key XP

The general's experience; None when the entry does not carry it

general_old_xp pydantic-field

general_old_xp: ClientInt | None = None

Wire key OXP

The general's experience before the battle; None when the entry does not carry it

general_is_new pydantic-field

general_is_new: bool | None = None

Wire key IN

The general is newly unlocked; None when the entry does not say

general_has_level_up pydantic-field

general_has_level_up: bool | None = None

Wire key LU

The general gained a level; None when the entry does not say

general_skill_ids pydantic-field

general_skill_ids: tuple[EnumOrInt['GeneralSkill'], ...] | None = None

Wire key SIDS

The general's unlocked skills; None when the entry has none

general_selected_abilities pydantic-field

general_selected_abilities: tuple[SelectedAbility, ...] | None = None

Wire key GASAIDS

The general's ability slots, filled or empty; None when the entry has none

uses_alien_equipment property

uses_alien_equipment: bool

Whether AIE/TAE stand in for an empty EQ, as LordVO.parseLord decides.

active_alien_equipment property

active_alien_equipment: AlienEquipment | None

The alien or temporary equipment standing in for an empty EQ, None when none does.

alien_hero_bonuses property

alien_hero_bonuses: list[EquipmentBonus]

The hero bonuses of the active alien or temporary equipment.

alien_bonuses property

alien_bonuses: list[EquipmentBonus]

The equipment bonuses of the active alien or temporary equipment, empty when EQ has items.

worn_items

worn_items() -> list[Equipment]

The items the client puts in the commander's slots, in slot order.

LordVO.parseLord (bundle line 26451) creates the helmet, armor, weapon, artifact, skin and hero slots in that order and puts each EQ item in its slot by row[1], so a later item replaces an earlier one and an item for any other slot is not worn.

Commander pydantic-model

Bases: LeaderBase

A commander - the leader assigned to an attack or support movement.

Client: CommanderVO, built by LordFactory.createLord (bundle line 26399)

Fields:

Validators:

Castellan pydantic-model

Bases: LeaderBase

A castellan - the defensive counterpart of a commander (BaronVO).

Client: BaronVO.parseLord (bundle line 43534)

Fields:

Validators:

locked_in_castle_id pydantic-field

locked_in_castle_id: ClientInt = 0

Wire key LICID

Castle the castellan is locked in, -1 for none; 0 when the entry has none

is_locked_in_castle property

is_locked_in_castle: bool

True when locked_in_castle_id is 0 or more.

is_available_for_movement

is_available_for_movement(kingdom_id: int) -> bool

Whether the client offers this castellan for a movement in kingdom_id.

Not when it is locked in a castle. A faction-portrait castellan (VIS 5) is compared with FactionConst.BARON_ID (-16), not a kingdom id, so it is never available in a real kingdom; an island-portrait one (VIS 13) only in the storm islands (4). The client also refuses a castellan that already leads one of the player's movements, which this model cannot see.

Client: BaronVO.isAvailableForMovement (bundle line 43535), LordVO.isAvailableForMovement (bundle line 26607)

GetCommandersRequest pydantic-model

Bases: BaseRequest

Request the commander and castellan list.

Command: gli Payload: {}

Client: C2SGetLordsInfoVO (bundle line 32779, no fields)

CommanderRoster pydantic-model

Bases: BasePayload

A player's commanders and castellans, the gli block.

The lists are in the client's order: commanders by commander_id, castellans by their portrait's place in CASTELLAN_PICTURE_ORDER, an unlisted portrait first.

Client: CastleLordData.parse_GLI (bundle line 38553), onSortLord and onSortBaron (bundle lines 38572-38573), BaronVO.parseLord (bundle line 43534)

Fields:

Validators:

commanders pydantic-field

commanders: list[Commander]

Wire key C

Commanders

castellans pydantic-field

castellans: list[Castellan]

Wire key B

Castellans

GetCommandersResponse pydantic-model

Bases: BaseResponse, CommanderRoster

Response containing commanders (C) and castellans (B).

Command: gli

Client: GLICommand.executeCommand (bundle line 123976) hands it to CastleLordData.parse_GLI

Fields:

Validators:

RenameCommanderRequest pydantic-model

Bases: BaseRequest

Rename a commander or castellan.

Command: arl Payload: {"LID": commander_id, "N": name}

The game's dialog allows 3 to 15 characters (EquipmentConst.LORD_NAME_MIN_LENGTH and LORD_NAME_MAX_LENGTH, dll line 19249); the server's own rules were not traced.

Client: C2SRenameLordVO (bundle line 65748), sent by CastleRenameLordDialog.sendCommand (bundle line 65738)

Fields:

commander_id pydantic-field

commander_id: int

Wire key LID

The commander_id of a Commander or Castellan from client.commanders.get_all()

name pydantic-field

name: str

Wire key N

The new name; the game allows 3 to 15 characters

RenameCommanderResponse pydantic-model

Bases: BaseResponse

Reply to a rename: the full commander and castellan list.

Command: arl

Client: ARLCommand.executeCommand (bundle line 123657), which passes gli to CastleLordData.parse_GLI

Fields:

commander_roster pydantic-field

commander_roster: CommanderRoster

Wire key gli

Commanders and castellans after the rename

commanders.models.skills

Generals and legend skills: the two attacker-side inputs that size a wave.

A general's unlocked skills widen the flanks; the player's legend skills widen them further and add waves, but only in a legendary fight. Both are read with their own command and neither arrives with the commander list.

Classes:

Name Description
SelectedAbility

One ability slot of a general: a [slot_id, ability_id] pair, as GASAIDS sends it and SAIDS takes it.

GetGeneralsRequest

Request every general the player owns.

OwnedGeneral

One general, as GeneralVO.parseData reads it.

AssignGeneralRequest

Assign a general to a commander, or take the commander's general away.

AssignGeneralResponse

Reply to a general assignment: the full commander list.

SetGeneralAbilitiesRequest

Choose a general's abilities, one per slot.

UnlockGeneralSkillRequest

Unlock a general skill. The skill id names the general, so none is sent.

ResetGeneralSkillsRequest

Reset a general's skill tree.

AddGeneralXpRequest

Feed a general xp items.

GetGeneralsResponse

Response listing the player's generals.

GetSkillsRequest

Request the player's own skill lists.

ActivatingSceatSkill

A Hall of Legends sceat skill still being activated, an entry of SSA.

SkillList

The player's legend and sceat skills, the skl block.

GetSkillsResponse

Response listing the player's legend and sceat skills.

ObjectUpdateEvent

An object update pushed by the server.

logger module-attribute

logger = logging.getLogger(__name__)

SelectedAbility pydantic-model

Bases: BasePayload

One ability slot of a general: a [slot_id, ability_id] pair, as GASAIDS sends it and SAIDS takes it.

It serializes back to the pair, with -1 for an empty slot.

Client: GeneralVO.parseData (bundle line 26666) keeps the pairs, and GeneralVO.getSelectedAbilities (bundle lines 26769-26771) reads [0] as the slot and [1] as the ability, counting it only above 0; GeneralsAbilityDialog.onSave (bundle line 27850) sends -1 for a cleared slot.

Fields:

Validators:

slot_id pydantic-field

slot_id: int

The slot, matched against the general's attack and defense slots

ability_id pydantic-field

ability_id: EnumOrInt['GeneralAbility'] | None = None

The ability; None for an empty slot

GetGeneralsRequest pydantic-model

Bases: BaseRequest

Request every general the player owns.

Command: gie Payload: {}

Client: C2SGetGeneralsInfoVO (bundle line 56405, no fields)

OwnedGeneral pydantic-model

Bases: BasePayload

One general, as GeneralVO.parseData reads it.

skill_ids is the useful part: the skills it has unlocked, which is what the attack dialog accumulates for the flank and front unit bonuses.

Client: GeneralVO.parseData (bundle line 26666)

Fields:

Validators:

general_id pydantic-field

general_id: int = -1

Wire key GID

General id

experience pydantic-field

experience: int = 0

Wire key XP

Experience, 0 when missing

star_level pydantic-field

star_level: int = 0

Wire key ST

Star level; when none is set, derived from fixed_level if that is a multiple of 10

is_new pydantic-field

is_new: bool = False

Wire key IN

The general is new

has_level_up pydantic-field

has_level_up: bool = False

Wire key LU

The general has levelled up

skill_ids pydantic-field

skill_ids: tuple[EnumOrInt['GeneralSkill'], ...] = ()

Wire key SIDS

Unlocked skills

selected_abilities pydantic-field

selected_abilities: list[SelectedAbility]

Wire key GASAIDS

The general's ability slots, filled or empty

fixed_level pydantic-field

fixed_level: int = -1

Wire key L

Fixed level; -1 when there is none

old_experience pydantic-field

old_experience: int = 0

Wire key OXP

The xp before the last change

wins pydantic-field

wins: int = 0

Wire key W

Battles won, 0 when missing

defeats pydantic-field

defeats: int = 0

Wire key D

Battles lost, 0 when missing

ability_ids property

ability_ids: list[int]

The abilities this general has selected, one id per filled slot, from either side.

Client: GeneralVO.getSelectedAbilities (bundle line 26769) skips a slot whose ability id is not above 0.

attack_ability_ids

attack_ability_ids(general_def: GeneralDef) -> list[int]

The abilities selected in this general's attack slots.

Client: GeneralVO.getSelectedAbilities(true) (bundle line 26769)

defense_ability_ids

defense_ability_ids(general_def: GeneralDef) -> list[int]

The abilities selected in this general's defense slots.

Client: GeneralVO.getSelectedAbilities(false) (bundle line 26769)

AssignGeneralRequest pydantic-model

Bases: BaseRequest

Assign a general to a commander, or take the commander's general away.

Command: gla Payload: {"LID": commander_id, "GID": general_id}

Client: C2SGeneralAssignLord (bundle line 101985)

Fields:

commander_id pydantic-field

commander_id: int

Wire key LID

The commander to give the general to, from client.commanders.get_commanders()

general_id pydantic-field

general_id: int = -1

Wire key GID

The general, an OwnedGeneral.general_id from client.skills.get_generals(); -1 unassigns the commander's general

AssignGeneralResponse pydantic-model

Bases: BaseResponse

Reply to a general assignment: the full commander list.

Command: gla Payload: {"gli": {"C": [..], "B": [..]}}

Client: GLACommand.executeCommand (bundle line 124218) passes gli to parse_GLI.

Fields:

commander_roster pydantic-field

commander_roster: CommanderRoster

Wire key gli

The commanders and castellans after the change

SetGeneralAbilitiesRequest pydantic-model

Bases: BaseRequest

Choose a general's abilities, one per slot.

Command: gaae Payload: {"GID": general_id, "SAIDS": [[slot_id, ability_id], ..]}

The ability dialog sends every slot it shows, with -1 for a cleared one. The server replies with no body.

Client: C2SGeneralSelectAbilities (bundle line 70767), GeneralsAbilityDialog.onSave (bundle line 27846)

Fields:

general_id pydantic-field

general_id: int

Wire key GID

The general whose abilities are chosen, an OwnedGeneral.general_id from client.skills.get_generals()

abilities pydantic-field

abilities: list[SelectedAbility]

Wire key SAIDS

Every slot the dialog shows, each sent as [slot_id, ability_id], -1 for none

UnlockGeneralSkillRequest pydantic-model

Bases: BaseRequest

Unlock a general skill. The skill id names the general, so none is sent.

Command: guse Payload: {"ID": skill_id}

The server replies with no body.

Client: C2SGeneralUnlockSkillVO (bundle line 67469), GUSECommand (bundle line 124280)

Fields:

skill_id pydantic-field

skill_id: int

Wire key ID

The skill to unlock

ResetGeneralSkillsRequest pydantic-model

Bases: BaseRequest

Reset a general's skill tree.

Command: grs Payload: {"GID": general_id}

The server replies with no body.

Client: C2SGeneralResetSkills (bundle line 67460), GRSCommand (bundle line 124242)

Fields:

general_id pydantic-field

general_id: int

Wire key GID

The general whose skills are reset, an OwnedGeneral.general_id from client.skills.get_generals()

AddGeneralXpRequest pydantic-model

Bases: BaseRequest

Feed a general xp items.

Command: gaxp Payload: {"GID": general_id, "CID": currency_id, "AMT": amount}

The server replies with no body.

Client: C2SGeneralAddXpVO (bundle line 74507), GAXPCommand (bundle line 124176)

Fields:

general_id pydantic-field

general_id: int

Wire key GID

The general to give xp to, an OwnedGeneral.general_id from client.skills.get_generals()

currency_id pydantic-field

currency_id: int

Wire key CID

The xp item, a currency

amount pydantic-field

amount: int

Wire key AMT

How many of the xp item to use

GetGeneralsResponse pydantic-model

Bases: BaseResponse

Response listing the player's generals.

Command: gie Payload: {"G": [{"GID": .., "SIDS": [skill ids], ..}, ..]}

Client: GIECommand.executeCommand (bundle line 124204) hands it to GeneralsData.parse_GIE (bundle line 113069), which updates each general by GID.

Fields:

Validators:

generals pydantic-field

generals: list[OwnedGeneral]

Wire key G

The player's generals

skill_ids

skill_ids(general_id: int) -> tuple[EnumOrInt['GeneralSkill'], ...]

The skills one general has unlocked, empty when it is not listed.

GetSkillsRequest pydantic-model

Bases: BaseRequest

Request the player's own skill lists.

Command: skl Payload: {}

Client: C2SGetSkillListVO (bundle line 112219, no fields)

ActivatingSceatSkill pydantic-model

Bases: BasePayload

A Hall of Legends sceat skill still being activated, an entry of SSA.

Client: CastleLegendSkillData.parse_SKL (bundle line 112051)

Fields:

skill_id pydantic-field

skill_id: EnumOrInt['SceatSkill'] | None = None

Wire key ID

The sceat skill being activated; None when the entry names none

remaining_seconds pydantic-field

remaining_seconds: ClientInt = 0

Wire key RS

Seconds until the skill is active

SkillList pydantic-model

Bases: BasePayload

The player's legend and sceat skills, the skl block.

SID are the legend skills, which apply only in a legendary fight, and SIDS the Hall of Legends sceat skills, which always apply.

Client: CastleLegendSkillData.parse_SKL (bundle line 112051)

Fields:

Validators:

legend_skill_ids pydantic-field

legend_skill_ids: tuple[EnumOrInt['LegendSkill'], ...] = ()

Wire key SID

Legend skills, which apply only in a legendary fight

sceat_skill_ids pydantic-field

sceat_skill_ids: tuple[EnumOrInt['SceatSkill'], ...] = ()

Wire key SIDS

Hall of Legends sceat skills, which always apply

total_points pydantic-field

total_points: ClientInt = 0

Wire key SP

Skill points

seconds_until_reset pydantic-field

seconds_until_reset: int | float = 0

Wire key RS

Seconds until the skills can be reset, fractions included

reset_count pydantic-field

reset_count: ClientInt = 0

Wire key RC

How many times the skills have been reset

activating pydantic-field

Wire key SSA

Sceat skills still being activated

GetSkillsResponse pydantic-model

Bases: BaseResponse, SkillList

Response listing the player's legend and sceat skills.

Command: skl Payload: {"SID": [legend skill ids], "SIDS": [sceat skill ids], "SP": total_points, "RS": seconds_until_reset, "RC": reset_count, "SSA": [{"ID": .., "RS": ..}, ..]}

Client: SKLCommand.executeCommand (bundle line 129742)

Fields:

Validators:

ObjectUpdateEvent pydantic-model

Bases: BaseResponse

An object update pushed by the server.

Only the skill list is read here; the area update the client also takes from it stays in the extra fields.

Command: ego Client: EGOCommand.executeCommand (bundle line 122801)

Fields:

Validators:

skills pydantic-field

skills: SkillList | None = None

Wire key skl

A new skill list, when one was sent