Skip to content

Army

Units, production and the hospital behind client.army.

army.service

Units, recruitment and the hospital of your castles.

Every command here acts on the castle the session has joined, so each method joins the castle first (jca), as the client is inside a castle before it shows the recruit or hospital dialog.

Action methods return True when the server accepted the action and False when it rejected it with an error code; transport failures (timeout, disconnect) raise. Query methods raise on any failure. A refused join raises before anything is sent.

Classes:

Name Description
ArmyService

Units, recruitment and the hospital of one of your castles.

ArmyService

ArmyService(client: 'EmpireClient')

Bases: BaseService

Units, recruitment and the hospital of one of your castles.

Reached as client.army.

Each method joins the castle it is given first (jca), as the client acts on the castle it is in. It joins in that castle's Castle.kingdom_id from client.state.get_castles(), and raises UnknownCastleError for a castle not listed there. A castle id listed in several of your kingdoms raises AmbiguousCastleError.

Usage
client = EmpireClient(...)

# Get units
units = client.army.get_units(castle_id=123)

# Produce 50 of wod id 620 in the soldier list
client.army.produce_units(123, ProductionListId.SOLDIERS, wod_id=620, amount=50)

Methods:

Name Description
get_units

Get the available unit inventory for a castle.

get_units_response

Get every unit inventory a castle reports.

dismiss_units

Dismiss units of the castle, or of its stronghold.

produce_units

Produce units or tools.

get_production_list

Get one production list of a castle: the slot producing now and the queue,

cancel_production

Cancel a production slot.

double_production_slot

Double the units of a production slot, for rubies; see "Spending rubies" in the guides.

heal_units

Queue wounded units for healing.

heal_all

Heal every wounded unit at once, for rubies; see "Spending rubies" in the guides.

cancel_heal

Cancel a hospital slot, by its HospitalSlot.position.

skip_heal

Finish a hospital slot now, by its HospitalSlot.position, for rubies; see "Spending

dismiss_wounded

Dismiss wounded units of one type instead of healing them.

dismiss_wounded_units

Dismiss wounded units of several types at once, as {wod_id: amount}.

get_units

get_units(castle_id: int, timeout: float = 5.0) -> dict[UnitOrTool, int]

Get the available unit inventory for a castle.

Covers soldiers and tools. Units in production, in the stronghold or in the hospital are reported separately - use get_units_response() for those.

Parameters:

Name Type Description Default
castle_id int

One of your castles, a Castle.id from client.state.get_castles()

required
timeout float

Timeout in seconds

5.0

Returns:

Type Description
dict[UnitOrTool, int]

The castle's units and tools, {Unit or Tool: amount}

get_units_response

get_units_response(castle_id: int, timeout: float = 5.0) -> GetUnitsResponse

Get every unit inventory a castle reports.

Parameters:

Name Type Description Default
castle_id int

One of your castles, a Castle.id from client.state.get_castles()

required
timeout float

Timeout in seconds

5.0

Returns:

Type Description
GetUnitsResponse

The full gui response: available, in production, stronghold, hospital

Raises:

Type Description
CommandError

The server refused to join the castle, so gui would have answered for another one

dismiss_units

dismiss_units(castle_id: int, wod_id: int, amount: int, from_stronghold: bool = False, timeout: float = 5.0) -> bool

Dismiss units of the castle, or of its stronghold.

Parameters:

Name Type Description Default
castle_id int

One of your castles, a Castle.id from client.state.get_castles()

required

Client: CastleRecruitDismissUnitsDialog.dismissUnits (bundle line 84330)

produce_units

produce_units(castle_id: int, list_id: ProductionListId, wod_id: int, amount: int, *, spend_rubies: bool = False, private_offer_id: int = -1, timeout: float = 5.0) -> bool

Produce units or tools.

Spends rubies when the unit or tool has a ruby price (cost_rubies or, on a temporary server, temp_server_cost_rubies in the game data, refused on either kind of server), and with spend_rubies on missing resources; see "Spending rubies" in the guides.

Parameters:

Name Type Description Default
castle_id int

One of your castles, a Castle.id from client.state.get_castles()

required
list_id ProductionListId

SOLDIERS, TOOLS or AUXILIARIES

required
wod_id int

Unit or tool wod id

required
amount int

How many to produce

required
spend_rubies bool

Allow a ruby price, and pay rubies for missing resources as the client's resource wait dialog does

False
private_offer_id int

With spend_rubies, the id of the active resource merchant private offer; -1 when there is none

-1
timeout float

Timeout in seconds

5.0

Raises:

Type Description
ValueError

The list is the hospital, or the unit has a ruby price or is not in the game data, and spend_rubies is False

GameDataNotLoadedError

spend_rubies is False and the game data cannot be loaded

Client: C2SBuyUnitPackageVO (bundle line 35277); BasicUnitVO.basicCostC2 (bundle line 19248); the ruby path is CastleResourceWaitDialogProperties.getResourceSkipCommand (bundle line 35218)

get_production_list

get_production_list(castle_id: int, list_id: ProductionListId, timeout: float = 5.0) -> ProductionList

Get one production list of a castle: the slot producing now and the queue, or the hospital slots for ProductionListId.HOSPITAL.

Parameters:

Name Type Description Default
castle_id int

One of your castles, a Castle.id from client.state.get_castles()

required

Client: C2SShowPackageListVO (bundle line 22860)

cancel_production

cancel_production(castle_id: int, list_id: ProductionListId, slot_type: SlotType, position: int, timeout: float = 5.0) -> bool

Cancel a production slot.

Parameters:

Name Type Description Default
castle_id int

One of your castles, a Castle.id from client.state.get_castles()

required
slot_type SlotType

PRODUCTION for the slot producing now, QUEUE for a queued one

required
position int

0 for the slot producing now, else the slot's ProductionSlot.position

required

Client: CastleRecruitDialogUnits.onCancelCurrentSlotConfirmed (bundle line 23655)

double_production_slot

double_production_slot(castle_id: int, list_id: ProductionListId, slot_type: SlotType, position: int, *, spend_rubies: bool = False, timeout: float = 5.0) -> bool

Double the units of a production slot, for rubies; see "Spending rubies" in the guides.

Parameters:

Name Type Description Default
castle_id int

One of your castles, a Castle.id from client.state.get_castles()

required
slot_type SlotType

PRODUCTION for the slot producing now, QUEUE for a queued one

required
position int

0 for the slot producing now, else the slot's ProductionSlot.position

required
spend_rubies bool

Allow the rubies it costs; without it nothing is sent

False
timeout float

Timeout in seconds

5.0

Raises:

Type Description
ValueError

spend_rubies is False

Client: RecruitmentHelper.boostCurrentSlot (bundle line 23165), after CastleRecruitDialogUnits.handleBoostSlot (bundle line 23692) prices it with getUnitDoublingCosts (bundle line 23546), always in rubies

heal_units

heal_units(castle_id: int, wod_id: int, amount: int, *, spend_rubies: bool = False, timeout: float = 5.0) -> bool

Queue wounded units for healing.

A unit whose UnitStats.healing_cost_rubies is above 0 heals for rubies; see "Spending rubies" in the guides. A subscription that waives it is not checked.

Parameters:

Name Type Description Default
castle_id int

One of your castles, a Castle.id from client.state.get_castles()

required
wod_id int

The wounded unit's wod id

required
amount int

How many to heal

required
spend_rubies bool

Allow healing a unit that costs rubies

False
timeout float

Timeout in seconds

5.0

Raises:

Type Description
ValueError

The unit heals for rubies, or is not in the game data, and spend_rubies is False

GameDataNotLoadedError

spend_rubies is False and the game data cannot be loaded

Client: CastleRecruitSelectedUnitComponent.onReviveClick (bundle line 51105), priced at healingCostC2 per unit (bundle line 51110)

heal_all

heal_all(castle_id: int, ruby_cost: int, *, spend_rubies: bool = False, timeout: float = 5.0) -> bool

Heal every wounded unit at once, for rubies; see "Spending rubies" in the guides.

Parameters:

Name Type Description Default
castle_id int

One of your castles, a Castle.id from client.state.get_castles()

required
ruby_cost int

The price the client would show (see HealAllRequest); the server refuses a price that no longer matches the hospital

required
spend_rubies bool

Allow a ruby_cost above 0

False
timeout float

Timeout in seconds

5.0

Raises:

Type Description
ValueError

ruby_cost is above 0 and spend_rubies is False

Client: CastleHospitalReviveAllDialog.reviveAll (bundle line 83667), with the price from CastleRecruitDialogHospital.openReviveAllDialog (bundle lines 83509-83513)

cancel_heal

cancel_heal(castle_id: int, position: int, timeout: float = 5.0) -> bool

Cancel a hospital slot, by its HospitalSlot.position.

Parameters:

Name Type Description Default
castle_id int

One of your castles, a Castle.id from client.state.get_castles()

required

Client: CastleRecruitDialogHospital.onCurrentSlotCancelled (bundle line 83508)

skip_heal

skip_heal(castle_id: int, position: int, *, spend_rubies: bool = False, timeout: float = 5.0) -> bool

Finish a hospital slot now, by its HospitalSlot.position, for rubies; see "Spending rubies" in the guides.

Parameters:

Name Type Description Default
castle_id int

One of your castles, a Castle.id from client.state.get_castles()

required
position int

The slot's HospitalSlot.position

required
spend_rubies bool

Allow the rubies it costs; without it nothing is sent

False
timeout float

Timeout in seconds

5.0

Raises:

Type Description
ValueError

spend_rubies is False

Client: CastleRecruitDialogHospital.skipCurrentSlot (bundle line 83504), priced in rubies by updateSkipTooltip (bundle line 83582)

dismiss_wounded

dismiss_wounded(castle_id: int, wod_id: int, amount: int, timeout: float = 5.0) -> bool

Dismiss wounded units of one type instead of healing them.

Parameters:

Name Type Description Default
castle_id int

One of your castles, a Castle.id from client.state.get_castles()

required

Client: CastleHospitalDismissUnitsDialog.dismissUnits (bundle line 83737)

dismiss_wounded_units

dismiss_wounded_units(castle_id: int, units: Mapping[int, int], timeout: float = 5.0) -> bool

Dismiss wounded units of several types at once, as {wod_id: amount}.

Parameters:

Name Type Description Default
castle_id int

One of your castles, a Castle.id from client.state.get_castles()

required

Client: CastleRecruitDialogHospital.onConfirmDeleteAll (bundle line 83498)

army.spy_army

Positional army data from a spy report.

The report's S block is consumed positionally by the game client (CastleSpyArmyInfoVO.parseArmyInfo), which shifts one entry per position in this order:

left, middle, right, keep, stronghold, support, reserve (optional)

Each entry is a list of [wod_id, amount] pairs. Summing the whole block flattens the three wall flanks together with keep, stronghold, support and reserve troops — a number the game never shows and which says nothing about where a castle is actually strong.

Classes:

Name Description
SpyArmy

A spied castle's defenders, by the position they hold.

Attributes:

Name Type Description
SpyStacks

One position of a spied army: its stacks in the order sent, an id sent twice kept twice.

SpyArmyBlock

A reply's S block as a SpyArmy, None when it is missing or empty (no spy report).

SpyStacks module-attribute

SpyStacks = Annotated[tuple[WodAmount, ...], BeforeValidator(_stacks)]

One position of a spied army: its stacks in the order sent, an id sent twice kept twice.

SpyArmyBlock module-attribute

SpyArmyBlock = Annotated[SpyArmy | None, BeforeValidator(_spied)]

A reply's S block as a SpyArmy, None when it is missing or empty (no spy report).

SpyArmy pydantic-model

Bases: EmpireModel

A spied castle's defenders, by the position they hold.

Reads a report's S block: SpyArmy.model_validate(report_s). A block shorter than the full seven positions is normal (the server omits trailing ones), so a missing position is empty.

Client: CastleSpyArmyInfoVO.parseArmyInfo (bundle line 30699)

Fields:

Validators:

  • _by_position

model_config class-attribute

model_config: ConfigDict = ConfigDict(frozen=True)

left pydantic-field

left: SpyStacks = ()

Defenders on the left wall flank

middle pydantic-field

middle: SpyStacks = ()

Defenders on the middle wall flank

right pydantic-field

right: SpyStacks = ()

Defenders on the right wall flank

keep pydantic-field

keep: SpyStacks = ()

Defenders in the keep (courtyard)

stronghold pydantic-field

stronghold: SpyStacks = ()

Units in the stronghold

support pydantic-field

support: SpyStacks = ()

Alliance support, which holds every flank

reserve pydantic-field

reserve: SpyStacks = ()

The reserve, when the report lists one

section

section(section: SpyArmySection) -> tuple[WodAmount, ...]

The stacks at one position.

sections

sections() -> list[tuple[SpyArmySection, tuple[WodAmount, ...]]]

Every position in wire order, for display.

total

total() -> int

Every defender in the castle, wherever they stand.

wall_total

wall_total() -> int

Defenders on the wall: the flanks an attack meets first.

army.models.hospital

Hospital protocol models.

Commands:

  • hru: Heal wounded units
  • hcs: Cancel a hospital slot
  • hss: Skip a hospital slot (rubies)
  • hdu: Dismiss wounded units
  • hra: Heal all wounded units (rubies)

These act on the castle the session has joined (jca).

Classes:

Name Description
HealUnitsRequest

Queue wounded units of the joined castle for healing.

HealUnitsResponse

The joined castle after an hru.

CancelHealRequest

Cancel a hospital slot of the joined castle.

CancelHealResponse

The joined castle after an hcs.

SkipHealRequest

Finish a hospital slot of the joined castle now, for rubies.

SkipHealResponse

Currencies after an hss.

DismissWoundedRequest

Dismiss wounded units of one type in the joined castle.

WoundedUnits

One UT entry of a many-type hdu.

DismissManyWoundedRequest

Dismiss wounded units of several types in the joined castle.

DismissWoundedResponse

The joined castle after an hdu.

HealAllRequest

Heal every wounded unit of the joined castle at once, for rubies.

HealAllResponse

The joined castle after an hra.

HealUnitsRequest pydantic-model

Bases: BaseRequest

Queue wounded units of the joined castle for healing.

Command: hru Payload: {"U": wod_id, "A": amount}

Client: C2SReviveUnitPackageVO (bundle line 84175), built by CastleRecruitSelectedUnitComponent.onReviveClick (bundle line 51105)

Fields:

wod_id pydantic-field

wod_id: int

Wire key U

amount pydantic-field

amount: int

Wire key A

HealUnitsResponse pydantic-model

Bases: BaseResponse

The joined castle after an hru.

Command: hru

Client: HRUCommand.executeCommand (bundle line 124532)

Fields:

production_list pydantic-field

production_list: ProductionListBlock = None

Wire key spl

currencies pydantic-field

currencies: CurrencyBlock = None

Wire key gcu

Coins and rubies after the change

unit_inventory pydantic-field

unit_inventory: UnitInventoryBlock = None

Wire key gui

CancelHealRequest pydantic-model

Bases: BaseRequest

Cancel a hospital slot of the joined castle.

Command: hcs Payload: {"S": position}

Client: C2SCancelHospitalSlot (bundle line 83616), built by CastleRecruitDialogHospital.onCurrentSlotCancelled (bundle line 83508)

Fields:

position pydantic-field

position: int

Wire key S

The slot's index in the hospital list

CancelHealResponse pydantic-model

Bases: BaseResponse

The joined castle after an hcs.

Command: hcs

Client: HCSCommand.executeCommand (bundle line 124454)

Fields:

production_list pydantic-field

production_list: ProductionListBlock = None

Wire key spl

unit_inventory pydantic-field

unit_inventory: UnitInventoryBlock = None

Wire key gui

SkipHealRequest pydantic-model

Bases: BaseRequest

Finish a hospital slot of the joined castle now, for rubies.

Command: hss Payload: {"S": position}

Client: C2SSkipHospitalSlot (bundle line 83625), built by CastleRecruitDialogHospital (bundle line 83504)

Fields:

position pydantic-field

position: int

Wire key S

The slot's index in the hospital list

SkipHealResponse pydantic-model

Bases: BaseResponse

Currencies after an hss.

Command: hss

Client: HSSCommand.executeCommand (bundle line 50856)

Fields:

currencies pydantic-field

currencies: CurrencyBlock = None

Wire key gcu

Coins and rubies after the change

DismissWoundedRequest pydantic-model

Bases: BaseRequest

Dismiss wounded units of one type in the joined castle.

Command: hdu Payload: {"U": wod_id, "A": amount}

Client: C2SDismissHospitalUnits (bundle line 83748), built by CastleHospitalDismissUnitsDialog.dismissUnits (bundle line 83737)

Fields:

wod_id pydantic-field

wod_id: int

Wire key U

amount pydantic-field

amount: int

Wire key A

WoundedUnits pydantic-model

Bases: BasePayload

One UT entry of a many-type hdu.

Fields:

wod_id pydantic-field

wod_id: int

Wire key U

amount pydantic-field

amount: int

Wire key A

DismissManyWoundedRequest pydantic-model

Bases: BaseRequest

Dismiss wounded units of several types in the joined castle.

Command: hdu Payload: {"UT": [{"U": wod_id, "A": amount}, ...]}

Client: C2SDismissManyHospitalUnits (bundle line 83834), built by CastleRecruitDialogHospital.onConfirmDeleteAll (bundle line 83498)

Fields:

units pydantic-field

Wire key UT

DismissWoundedResponse pydantic-model

Bases: BaseResponse

The joined castle after an hdu.

Command: hdu

Client: HDUCommand.executeCommand (bundle line 124470)

Fields:

unit_inventory pydantic-field

unit_inventory: UnitInventoryBlock = None

Wire key gui

HealAllRequest pydantic-model

Bases: BaseRequest

Heal every wounded unit of the joined castle at once, for rubies.

Command: hra Payload: {"C2": ruby_cost}

C2 is the price the client shows: the sum over the hospital of int(amount * reviveAllCostC2), less the active prime-sales revive-all discount percent, rounded up. The server answers INVALID_AMOUNT when the hospital changed ("alert_hospital_amountOutdated").

Client: C2SReviveAllHospitalUnits (bundle line 83677), sent by CastleHospitalReviveAllDialog.reviveAll (bundle line 83667) with the price from CastleRecruitDialogHospital.openReviveAllDialog (bundle line 83509)

Fields:

ruby_cost pydantic-field

ruby_cost: int

Wire key C2

HealAllResponse pydantic-model

Bases: BaseResponse

The joined castle after an hra.

Command: hra

Client: HRACommand.executeCommand (bundle line 124513)

Fields:

currencies pydantic-field

currencies: CurrencyBlock = None

Wire key gcu

Coins and rubies after the change

unit_inventory pydantic-field

unit_inventory: UnitInventoryBlock = None

Wire key gui

production_area pydantic-field

production_area: RawBlock = None

Wire key gpa

The castle's production and storage figures, as a raw block

army.models.production

Unit and tool production models.

Commands:

  • bup: Produce units or tools
  • spl: Get a production list
  • bou: Double the units of a production slot
  • mcu: Cancel a production slot

These act on the castle the session has joined (jca); bup and bou also name that castle and its kingdom.

Classes:

Name Description
ProductionSlot

One slot of a soldier, tool or auxiliary list: a QS entry or the PS block.

CurrentProductionSlot

The slot producing now, the PS block: free until the block says otherwise.

HospitalSlot

One slot of the hospital list, a positional PIDL entry.

ProductionList

One production list, the spl block.

ProduceUnitsRequest

Produce units or tools in the joined castle.

AddedUnit

Units bup put straight into the inventory, the O block.

ProduceUnitsResponse

The joined castle after a bup.

GetProductionListRequest

Get one production list of the joined castle.

GetProductionListResponse

One production list of the joined castle.

DoubleProductionSlotRequest

Double the units of a production slot, for rubies.

DoubleProductionSlotResponse

The production list after a bou.

CancelProductionRequest

Cancel a production slot of the joined castle.

CancelProductionResponse

The production list after an mcu.

ProductionListBlock module-attribute

ProductionListBlock = Annotated[ProductionList | None, BeforeValidator(object_or_none)]

ProductionSlot pydantic-model

Bases: BasePayload

One slot of a soldier, tool or auxiliary list: a QS entry or the PS block.

The client reads three optional parts of the same object: the top-level keys only when ICT is set, then P (which overrides the unit, amount, boosts, alliance help and recruitment id), then SI (the lock state). Numbers go through int(), RAH and VIP through !!, and RUT is kept as sent. An entry that is not an object reads as an empty slot.

Client: UnitPackageSlotVO.fillFromParamObject (bundle line 61416); PS first goes through initProductionSlot (bundle line 61417)

Fields:

Validators:

  • _fill_as_the_client_does

starts_free class-attribute

starts_free: bool = False

position pydantic-field

position: int = 0

Index in the queue, the position boost and cancel requests take; 0 for the slot producing now

wod_id pydantic-field

wod_id: int = 0

Wire key WID

Unit or tool wod id

amount pydantic-field

amount: int = 0

Wire key TUA

Units in the slot

boost_count pydantic-field

boost_count: int = 0

Wire key CBS

How often the slot's units were doubled

received_alliance_help pydantic-field

received_alliance_help: bool = False

Wire key RAH

recruitment_id pydantic-field

recruitment_id: int = 0

Wire key PID

remaining_seconds pydantic-field

remaining_seconds: int = 0

Wire key RCT

Seconds left on the slot

production_seconds pydantic-field

production_seconds: int = 0

Wire key ICT

Total production time, in seconds

source_recruitment_id pydantic-field

source_recruitment_id: int = 0

Wire key SPID

Source recruitment id

seconds_till_locked pydantic-field

seconds_till_locked: int | float | str | None = 0

Seconds until the slot locks while it is empty, else -1; 0 means locked

is_vip pydantic-field

is_vip: bool = False

The slot is a VIP slot

is_locked pydantic-field

is_locked: bool = False

is_free pydantic-field

is_free: bool = False

Unlocked and empty

CurrentProductionSlot pydantic-model

Bases: ProductionSlot

The slot producing now, the PS block: free until the block says otherwise.

Client: UnitPackageList.parseCurrentProductionSlot (bundle line 138906)

Fields:

Validators:

  • _fill_as_the_client_does

starts_free class-attribute

starts_free: bool = True

HospitalSlot pydantic-model

Bases: BasePayload

One slot of the hospital list, a positional PIDL entry.

[wod_id, amount, remaining_seconds, speed * 100, heal_time_reduction, recruitment_id, seconds_till_locked], which the client reads in that order with no int(); here each goes through js_int so the fields are ints, and a missing value reads as 0. An entry that is not an array reads as an empty slot here, where the client would stop reading the list.

Client: UnitHealPackageSlotVO.fillFromParamArray (bundle line 138964)

Fields:

position pydantic-field

position: int = 0

Index in the hospital list, the position cancel and skip requests take

wod_id pydantic-field

wod_id: int = 0

Unit wod id; -1 for a free slot, -2 for a locked one

amount pydantic-field

amount: int = 0

remaining_seconds pydantic-field

remaining_seconds: int = 0

recruitment_speed pydantic-field

recruitment_speed: float = 0.0

Recruitment speed

heal_time_reduction pydantic-field

heal_time_reduction: int = 0

recruitment_id pydantic-field

recruitment_id: int = 0

seconds_till_locked pydantic-field

seconds_till_locked: int = 0

is_free property

is_free: bool

ConstructionConst.SLOTSTATEUNLOCKED -1 (dll line 18983).

is_locked property

is_locked: bool

ConstructionConst.SLOTSTATELOCKED -2 (dll line 18983).

ProductionList pydantic-model

Bases: BasePayload

One production list, the spl block.

The client ignores a block without LID. The hospital list (ProductionListId.HOSPITAL) sends PIDL and ASI; every other list sends QS, PS, RM and TCT.

Client: CastleMilitaryData.parse_SPL (bundle line 138830), UnitPackageList.parseList / parseMilitaryList (bundle lines 138899, 138907); RecruitmentConst (dll line 19660) for the RM modes

Fields:

Validators:

  • _client_ints

list_id pydantic-field

list_id: int | None = None

Wire key LID

A ProductionListId; None when missing

queue pydantic-field

Wire key QS

Queued slots, by position

current pydantic-field

Wire key PS

The slot producing now

recruitment_mode pydantic-field

recruitment_mode: int = 0

Wire key RM

0 finishes the first stack first, 1 finishes the stacks equally

remaining_seconds pydantic-field

remaining_seconds: int = 0

Wire key TCT

Seconds until the list's production is done

hospital_slots pydantic-field

hospital_slots: list[HospitalSlot]

Wire key PIDL

Hospital list only: slots by position

active_slot_index pydantic-field

active_slot_index: ParseInt = 0

Wire key ASI

Index of the active hospital slot; hospital list only

ProduceUnitsRequest pydantic-model

Bases: BaseRequest

Produce units or tools in the joined castle.

Command: bup Payload: {"LID": list_id, "WID": wod_id, "AMT": amount, "PO": -1, "PWR": 0, "SK": 73, "SID": kingdom_id, "AID": castle_id}

PWR 1 pays rubies for missing resources: the client sends it from the resource wait dialog's skip (CastleResourceWaitDialog.skipForRubies, bundle line 32069), with PO set to the resource merchant's private offer id (CastlePrivateOfferData.getPrivateOfferMerchantID, bundle line 131979, -1 when there is none). SK is always 73 (BUY_UNIT_PACKAGE_SK).

Client: C2SBuyUnitPackageVO (bundle line 35277), built by CastleRecruitSelectedUnitComponent (bundle line 51107) and CastleResourceWaitDialogProperties.getResourceSkipCommand (bundle line 35218)

Fields:

list_id pydantic-field

Wire key LID

The list to produce into

wod_id pydantic-field

wod_id: int

Wire key WID

Unit or tool wod id

amount pydantic-field

amount: int

Wire key AMT

How many to produce

private_offer_id pydantic-field

private_offer_id: int = -1

Wire key PO

Resource merchant offer id, -1 for none

pay_with_rubies pydantic-field

pay_with_rubies: int = 0

Wire key PWR

1 to pay rubies for missing resources

sk pydantic-field

Wire key SK

Always 73

kingdom_id pydantic-field

kingdom_id: Kingdom

Wire key SID

The joined castle's kingdom

castle_id pydantic-field

castle_id: int

Wire key AID

The castle the session is in, a Castle.id from client.state.get_castles(); ArmyService joins it first

AddedUnit pydantic-model

Bases: BasePayload

Units bup put straight into the inventory, the O block.

Client: BUPCommand.executeCommand (bundle line 125531) calls unitInventory.addUnit(O.W, O.AMT) when O is set and not 0

Fields:

Validators:

  • _client_ints

wod_id pydantic-field

wod_id: int = 0

Wire key W

amount pydantic-field

amount: int = 0

Wire key AMT

ProduceUnitsResponse pydantic-model

Bases: BaseResponse

The joined castle after a bup.

Command: bup Payload: {"spl": {...}, "grc": {...}, "gcu": {...}, "gui": {...}, "O": {"W": wod_id, "AMT": amount}}

Client: BUPCommand.executeCommand (bundle line 125531): spl to parse_SPL, grc to AreaDataUpdater.parseGRC, gcu to CurrencyData.parseGCU, gui to parse_GUI

Fields:

production_list pydantic-field

production_list: ProductionListBlock = None

Wire key spl

resources pydantic-field

resources: RawBlock = None

Wire key grc

The castle's resources, as a raw block

currencies pydantic-field

currencies: CurrencyBlock = None

Wire key gcu

Coins and rubies after the change

unit_inventory pydantic-field

unit_inventory: UnitInventoryBlock = None

Wire key gui

added_unit pydantic-field

added_unit: AddedUnit | None = None

Wire key O

GetProductionListRequest pydantic-model

Bases: BaseRequest

Get one production list of the joined castle.

Command: spl Payload: {"LID": list_id}

Client: C2SShowPackageListVO (bundle line 22860)

Fields:

list_id pydantic-field

Wire key LID

accepts_reply

accepts_reply(payload: Any) -> bool

Whether an spl reply is about this list: its LID is the one asked for, or it is empty.

Client: CastleMilitaryData.parse_SPL (bundle line 138830) updates the list named by the reply's LID and ignores a reply without one. The server answers {} for a list the castle cannot produce (seen live for the auxiliaries list on a castle without that building), which the client never asks for; an empty reply names no list, so it cannot be another request's.

GetProductionListResponse pydantic-model

Bases: BaseResponse, ProductionList

One production list of the joined castle.

Command: spl

Client: SPLCommand.executeCommand (bundle line 125676) hands the reply to CastleMilitaryData.parse_SPL

Fields:

Validators:

  • _client_ints

DoubleProductionSlotRequest pydantic-model

Bases: BaseRequest

Double the units of a production slot, for rubies.

Command: bou Payload: {"LID": list_id, "S": position, "AID": castle_id, "SID": kingdom_id, "ST": slot_type}

S is the slot's position: 0 for the slot producing now, its index in QS for a queued one.

Client: C2SBoostUnitPackageVO (bundle line 79040), built by RecruitmentHelper.boostCurrentSlot (bundle line 23165) from the recruit dialog's boost button (onDoubleUnitsConfirmation, bundle line 23694)

Fields:

list_id pydantic-field

Wire key LID

The list the slot belongs to

position pydantic-field

position: int

Wire key S

0 for the slot producing now, its index in QS for a queued one

castle_id pydantic-field

castle_id: int

Wire key AID

The castle the session is in, a Castle.id from client.state.get_castles(); ArmyService joins it first

kingdom_id pydantic-field

kingdom_id: Kingdom

Wire key SID

The joined castle's kingdom

slot_type pydantic-field

slot_type: SlotType

Wire key ST

Whether the slot is producing now or queued

DoubleProductionSlotResponse pydantic-model

Bases: BaseResponse

The production list after a bou.

Command: bou

Client: BOUCommand.executeCommand (bundle line 125515)

Fields:

production_list pydantic-field

production_list: ProductionListBlock = None

Wire key spl

currencies pydantic-field

currencies: CurrencyBlock = None

Wire key gcu

Coins and rubies after the change

CancelProductionRequest pydantic-model

Bases: BaseRequest

Cancel a production slot of the joined castle.

Command: mcu Payload: {"LID": list_id, "S": position, "ST": slot_type}

Client: C2SCancelUnitPackageVO (bundle line 83843), built by CastleRecruitDialogUnits.onCancelCurrentSlotConfirmed (bundle line 23655)

Fields:

list_id pydantic-field

Wire key LID

position pydantic-field

position: int

Wire key S

0 for the slot producing now, else its index in the queue

slot_type pydantic-field

slot_type: SlotType

Wire key ST

CancelProductionResponse pydantic-model

Bases: BaseResponse

The production list after an mcu.

Command: mcu

Client: MCUCommand.executeCommand (bundle line 125616)

Fields:

production_list pydantic-field

production_list: ProductionListBlock = None

Wire key spl

army.models.units

Unit inventory, dismissing units, and attack waves.

Commands:

  • gui: Get units inventory
  • dup: Dismiss units

AttackWave and WaveFlank are the waves a cra attack sends.

Classes:

Name Description
GetUnitsRequest

Get the unit inventories of the castle the session is in.

UnitInventory

A castle's unit inventories, the gui block.

GetUnitsResponse

The unit inventories of the castle the session is in.

DismissUnitsRequest

Dismiss units of the joined castle.

DismissUnitsResponse

The joined castle after a dup.

WaveFlank

One flank of an attack wave.

AttackWave

A single attack wave: left, right and middle flank.

Attributes:

Name Type Description
BUY_UNIT_PACKAGE_SK

SK of every bup: each client caller leaves C2SBuyUnitPackageVO's

BUY_UNIT_PACKAGE_SK module-attribute

BUY_UNIT_PACKAGE_SK = 73

SK of every bup: each client caller leaves C2SBuyUnitPackageVO's default; nothing in the client reads it or says what it means.

RawBlock module-attribute

A reply block this module keeps as sent; the docstring names the client parser.

UnitInventoryBlock module-attribute

UnitInventoryBlock = Annotated[UnitInventory | None, BeforeValidator(object_or_none)]

GetUnitsRequest pydantic-model

Bases: BaseRequest

Get the unit inventories of the castle the session is in.

Command: gui Payload: {}

The request names no castle: the server answers for the joined castle, and with NOT_IN_OWNED_CASTLE once a map scan has moved the session away.

Client: C2SGetUnitInventoryVO (no fields)

UnitInventory pydantic-model

Bases: BasePayload

A castle's unit inventories, the gui block.

Client: CastleMilitaryData.parse_GUI, which fills each inventory with AUnitInventory.fillFromWodAmountArray (bundle line 42572)

Fields:

units pydantic-field

units: WodAmounts

Wire key I

Units and tools in the castle

in_production pydantic-field

in_production: WodAmounts

Wire key TU

Units on their way in

stronghold pydantic-field

stronghold: WodAmounts

Wire key SHI

Units stored in the stronghold

hospital pydantic-field

hospital: WodAmounts

Wire key HI

Wounded units in the hospital

GetUnitsResponse pydantic-model

Bases: BaseResponse, UnitInventory

The unit inventories of the castle the session is in.

Command: gui Payload: {"I": [[wod_id, amount], ...], "TU": [...], "SHI": [...], "HI": [...]}

Client: GUICommand.exec (bundle line 125600) hands the reply to CastleMilitaryData.parse_GUI, which reads I, TU, SHI and HI and nothing else.

Fields:

DismissUnitsRequest pydantic-model

Bases: BaseRequest

Dismiss units of the joined castle.

Command: dup Payload: {"WID": wod_id, "A": amount, "S": 0 or 1}

Client: C2SDismissUnitPackageVO (bundle line 84341), built by CastleRecruitDismissUnitsDialog.dismissUnits (bundle line 84330)

Fields:

wod_id pydantic-field

wod_id: int

Wire key WID

amount pydantic-field

amount: int

Wire key A

from_stronghold pydantic-field

from_stronghold: int = 0

Wire key S

1 to dismiss from the stronghold

DismissUnitsResponse pydantic-model

Bases: BaseResponse

The joined castle after a dup.

Command: dup

Client: DUPCommand.executeCommand (bundle line 125585)

Fields:

production_area pydantic-field

production_area: RawBlock = None

Wire key gpa

The castle's production and storage figures, as a raw block

unit_inventory pydantic-field

unit_inventory: UnitInventoryBlock = None

Wire key gui

WaveFlank pydantic-model

WaveFlank(*, tools: WodAmountSlotsInput = (), units: WodAmountSlotsInput = ())

Bases: BasePayload

One flank of an attack wave.

Payload: {"T": [[tool_id, count], ...], "U": [[unit_id, count], ...]}, one pair per slot

Client: CastleAttackWaveVO.getWaveInfoObject (bundle line 99930) sends each container's getSlotList() (bundle line 20573)

Fields:

tools pydantic-field

tools: WodAmountSlots = ()

Wire key T

The tool slots, [-1, 0] for an empty one

units pydantic-field

units: WodAmountSlots = ()

Wire key U

The unit slots, [-1, 0] for an empty one

AttackWave pydantic-model

Bases: BasePayload

A single attack wave: left, right and middle flank.

Payload: {"L": flank, "R": flank, "M": flank}, in the client's key order.

Client: CastleAttackWaveVO.getWaveInfoObject (bundle line 99930)

Fields:

left pydantic-field

left: WaveFlank

Wire key L

right pydantic-field

right: WaveFlank

Wire key R

middle pydantic-field

middle: WaveFlank

Wire key M

unit_count

unit_count() -> int

Total units across all three flanks.

is_complete

is_complete() -> bool

Whether the client would send this wave.

The game drops any wave without units, tools included.