Movements¶
Army movements behind client.movements.
movements.service¶
The army movements the state tracks.
Classes:
| Name | Description |
|---|---|
MovementsService |
Army movements: listing them and recalling your own. |
MovementsService
¶
Bases: BaseService
Army movements: listing them and recalling your own.
Reached as client.movements.
Methods:
| Name | Description |
|---|---|
get_movements |
Request army movements from server. |
recall |
Recall one of your movements ( |
get_incoming_attacks |
Get all incoming attack movements. |
get_incoming_movements |
Get all incoming movements. |
get_outgoing_movements |
Get all outgoing movements. |
get_movements
¶
Request army movements from server.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
wait
|
bool
|
If True, wait for response before returning |
True
|
timeout
|
float
|
Timeout in seconds when waiting; with |
5.0
|
Returns:
| Type | Description |
|---|---|
list[Movement]
|
List of Movement objects, read from state after the response has |
list[Movement]
|
been applied. With |
list[Movement]
|
right now, which is not yet the answer to this request. |
Raises:
| Type | Description |
|---|---|
CommandError
|
|
EmpireTimeoutError
|
|
recall
¶
recall(movement_id: int, timeout: float = 5.0) -> MovementWrapper
Recall one of your movements (mcm), as the client's retreat and "send home" buttons do.
Which movements the client lets you recall is listed on
CancelMovementRequest: in short, your own that are still
heading to their target (attacks only within 600 seconds of leaving,
with exceptions) and your supports in either direction. The reply
also updates state and fires GameState.on_movement_recalled.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
movement_id
|
int
|
The movement to recall ( |
required |
timeout
|
float
|
Timeout in seconds |
5.0
|
Returns:
| Type | Description |
|---|---|
MovementWrapper
|
The movement as the reply has it now, normally on its way home |
Raises:
| Type | Description |
|---|---|
CommandError
|
The server refused the recall, such as INVALID_STARTAREA_FOR_CANCEL_MOVEMENT (189) |
Client: CastleAskRetreatDialog.onClick (bundle line 33234),
MCMCommand.executeCommand (bundle line 126041)
movements.tracked¶
A tracked army movement, with its timing and troops.
Classes:
| Name | Description |
|---|---|
MovementResources |
Resources a movement carries: the goods among its market goods or travel loot, added up. |
Movement |
A live army movement (Attack, Support, Transport, ...) tracked in state. |
Attributes:
| Name | Type | Description |
|---|---|---|
logger |
|
|
DUNGEON_OWNER_IDS |
Owner ids the game counts as dungeon owners. Robber barons, kingdom dungeons, |
DUNGEON_OWNER_IDS
module-attribute
¶
DUNGEON_OWNER_IDS = frozenset({*(NPCOwner.ROBBER_BARON - i for i in range(13)), *(NPCOwner.DESERT_DUNGEON - i for i in range(5)), NPCOwner.DESERT_BOSS_DUNGEON, NPCOwner.ICE_BOSS_DUNGEON, NPCOwner.VOLCANO_BOSS_DUNGEON, *range(NPCOwner.ISLAND_VILLAGE, NPCOwner.ISLAND_VILLAGE + 5), NPCOwner.BLUE_FACTION_KING, NPCOwner.RED_FACTION_KING, NPCOwner.KINGS_TOWER, NPCOwner.MONUMENT, NPCOwner.CLASSIC_LABORATORY, NPCOwner.ICE_LABORATORY, NPCOwner.DESERT_LABORATORY, NPCOwner.VOLCANO_LABORATORY, NPCOwner.RANDOM_DUNGEON_EVENT, NPCOwner.APRIL_DUNGEON_EVENT, NPCOwner.ST_PATRICKS_DAY_DUNGEON_EVENT, NPCOwner.EASTER_DUNGEON_EVENT, *(NPCOwner.NOMAD_CAMP - i for i in range(4)), NPCOwner.SAMURAI_CAMP, NPCOwner.TUTORIAL_DUNGEON, NPCOwner.THORNKING_DUNGEON, NPCOwner.THORNKING_VILLAGE, NPCOwner.THORNKING_COW_DUNGEON, NPCOwner.SEA_QUEEN_DUNGEON, NPCOwner.SEA_QUEEN_SHIPS, NPCOwner.TREASURE_HUNT_DUNGEON, NPCOwner.UNDERWORLD_DUNGEON, NPCOwner.UNDERWORLD_VILLAGE, NPCOwner.ALLIANCE_NOMAD_CAMP, NPCOwner.DAIMYO_CASTLE, NPCOwner.ALIEN_INVASION, NPCOwner.RED_ALIEN_INVASION, *(owner for owner in NPCOwner if owner.name.startswith('COLLECTOR_')), NPCOwner.WOLF_KING, NPCOwner.ARE_PORTAL, NPCOwner.UNKNOWN_EVENT_OWNER})
Owner ids the game counts as dungeon owners. Robber barons, kingdom dungeons,
villages and nomad camps are runs of ids (see NPCOwner).
MovementResources
pydantic-model
¶
Bases: EmpireModel
Resources a movement carries: the goods among its market goods or travel loot, added up.
Keys are the client's collectable server keys (CollectableItem*VO.SERVER_KEY). Goods sent
as an old-style amounts list count as well; coins, rubies and currencies are in Movement.goods
only.
Fields:
-
wood(int) -
stone(int) -
food(int) -
coal(int) -
oil(int) -
glass(int) -
iron(int) -
aquamarine(int) -
honey(int) -
mead(int) -
beef(int)
model_config
class-attribute
¶
model_config: ConfigDict = ConfigDict(extra='ignore', populate_by_name=True)
Movement
pydantic-model
¶
Bases: EmpireModel
A live army movement (Attack, Support, Transport, ...) tracked in state.
This is the movement type the client returns from
get_movements and passes to
on_incoming_attack callbacks, and the one exported as
empire_core.Movement.
The raw gam reply is modelled as empire_core.movements.GetMovementsResponse:
a list of MovementWrapper entries, each holding a MovementRecord. Use those
only when parsing packets by hand.
Whether a movement is yours or aimed at you depends on the local player's
id, which state stamps on every movement as local_player_id.
Client: BasicMapmovementVO, ArmyAttackMapmovementVO.
Fields are snake_case with the wire key as the alias (movement_id is
MID), so a raw gam record validates unchanged and either name
works when building one.
Fields:
-
movement_id(int) -
movement_type(int) -
progress_time(int) -
total_time(int) -
direction(int) -
target_id(int) -
kingdom_id(int) -
source_id(int) -
owner_id(int) -
horse_booster(EnumOrInt[Horse] | None) -
target_area(MovementArea | None) -
source_area(MovementArea | None) -
target_area_id(int) -
source_area_id(int) -
target_x(int) -
target_y(int) -
source_x(int) -
source_y(int) -
target_type(int) -
local_player_id(int) -
units(WodAmounts) -
estimated_size(int) -
resources(MovementResources) -
target_name(str) -
source_name(str) -
target_player_name(str) -
source_player_name(str) -
target_alliance_name(str) -
source_alliance_name(str) -
created_at(float) -
last_updated(float) -
commander(Commander | None) -
wait_total(int) -
wait_passed(int) -
force_cancelable(bool) -
spy(MovementSpy | None) -
owner(MovementOwner | None) -
target_owner(MovementOwner | None) -
attack_type(int | None) -
is_shadow(bool) -
support_tools(SupportToolSlots) -
auto_skip_cooldown_type(EnumOrInt[AutoSkipCooldownType]) -
advisor_type(EnumOrInt[AttackAdvisorType]) -
advisor_movement_count(int) -
advisor_movement_number(int) -
advisor_is_last(bool) -
market_carriages(int) -
goods(CollectableRows)
Validators:
-
_horse→horse_booster -
_readable_area→target_area,source_area
model_config
class-attribute
¶
model_config: ConfigDict = ConfigDict(extra='ignore', populate_by_name=True)
direction
pydantic-field
¶
direction: int = 0
Wire key D
1 = returning home, 0 = heading to the target
horse_booster
pydantic-field
¶
Wire key HBW
The horse booster; None for none or when paid with feathers
target_area
pydantic-field
¶
target_area: MovementArea | None = None
Wire key TA
Target area row; None when there is none
source_area
pydantic-field
¶
source_area: MovementArea | None = None
Wire key SA
Source area row; None when there is none
local_player_id
pydantic-field
¶
local_player_id: int = -1
Player id of the receiving account, -1 if unknown
target_alliance_name
pydantic-field
¶
target_alliance_name: str = ''
Alliance name of the target's owner
source_alliance_name
pydantic-field
¶
source_alliance_name: str = ''
Alliance name of the movement's owner
commander
pydantic-field
¶
commander: Commander | None = None
The commander leading the army, as the UM block sends it; None when there is none
force_cancelable
pydantic-field
¶
force_cancelable: bool = False
The movement can be force-cancelled
spy
pydantic-field
¶
spy: MovementSpy | None = None
A spy mission's type, accuracy or damage, spy count and risk; None for any other
target_owner
pydantic-field
¶
target_owner: MovementOwner | None = None
Owner record of the target's owner
support_tools
pydantic-field
¶
Support tools sent along, as the attack sent them; None for an empty slot
auto_skip_cooldown_type
pydantic-field
¶
auto_skip_cooldown_type: EnumOrInt[AutoSkipCooldownType] = AutoSkipCooldownType.OFF
How the target's cooldown is skipped on arrival
advisor_type
pydantic-field
¶
advisor_type: EnumOrInt[AttackAdvisorType] = AttackAdvisorType.NONE
The attack advisor that sent the attack
advisor_movement_count
pydantic-field
¶
advisor_movement_count: int = 0
Attacks in the advisor series
advisor_movement_number
pydantic-field
¶
advisor_movement_number: int = 0
This attack's place in the series
movement_type_enum
property
¶
movement_type_enum: MovementType | None
The movement's type (T) as a MovementType, None for an id the client does not define.
target_type_enum
property
¶
target_type_enum: MapItemType | None
The target area's type (TA[0]) as a MapItemType, None for an id the client does not define.
time_remaining
property
¶
time_remaining: int
Seconds until arrival, advancing with wall-clock time.
Extrapolated from the last packet snapshot (total_time - progress_time at
last_updated), so it keeps counting down between updates.
estimated_end
property
¶
estimated_end: float
When the movement is over: arrival plus whatever wait at the target is left.
Client: BasicMapmovementVO._endWaitTimeStamp.
owner_alliance_id
property
¶
owner_alliance_id: int
Alliance of the movement's owner, -1 if none or unknown.
target_alliance_id
property
¶
target_alliance_id: int
Alliance of the target's owner, -1 if none or unknown.
battle_time
property
¶
battle_time: float
When the battle starts (Unix time): arrival plus the wait at the target.
Client: BasicMapmovementVO.parseUnitMovement.
is_stationed
property
¶
is_stationed: bool
The army has arrived and is waiting at its target, as a support does.
Client: SupportDefenceMapmovementVO.isStationed.
is_incoming
property
¶
is_incoming: bool
Another player's army heading to one of the local player's areas.
The daimyo township counts as yours, as in the client, except for an
alien attack, which comes only at you. Armies moving between your own
areas count as outgoing, not incoming, and so does no occupation
(is_occupation): the forces holding an area are not on their way to it.
Client: ArmyAttackMapmovementVO.isAttackingMovement (bundle line 14389),
AlienAttackMovementVO.isAttackingMovement (bundle line 33073). An
occupation is a SiegeMapmovementVO, which keeps
BasicMapmovementVO.isAttackingMovement (bundle line 19438, always
false); the movement overview's attack category (FilterAttack,
dialog_moveOverview_catAttack, bundle line 67996) adds one on your
area by a clause of its own, and lists it as "Occupying forces"
(RenderSiege, bundle line 67408), not as an attack under way.
is_attack
property
¶
is_attack: bool
Any attack type, including NPC, alien, faction and event attacks.
is_occupation
property
¶
is_occupation: bool
An occupation: the forces holding an area a capture attack won, until it is captured or they are driven off.
is_transport
property
¶
is_transport: bool
A market transport of resources. Troops moved between own castles are is_travel.
is_travel
property
¶
is_travel: bool
Troops moved between the owner's own areas.
An army's way home also arrives as a new TRAVEL movement, with D == 1.
unit_count
property
¶
unit_count: int
Total number of units in this movement (includes all unit types).
movements.models¶
Army movement models: gam, the recall (mcm), and the movement wrappers pushed with abr and asr.
Classes:
| Name | Description |
|---|---|
GetMovementsRequest |
Get all active troop movements. |
MovementArea |
A movement's |
MovementRecord |
The movement itself: the |
MovementArmy |
A visible army: three flanks plus the courtyard wave, each |
MovementUnitInfo |
Commander and wait details: a wrapper's |
MovementMarket |
A market transport's cargo: a wrapper's |
MovementSpy |
A spy mission's details: a wrapper's |
MovementWrapper |
One entry of |
MovementOwner |
An owner record: one entry of |
GetMovementsResponse |
Response containing active movements. |
CancelMovementRequest |
Recall one of your movements, turning it back home. |
CancelMovementResponse |
The reply to a recall: the movement as it now stands. |
GetMovementsRequest
pydantic-model
¶
Bases: BaseRequest
Get all active troop movements.
Command: gam Payload: {}
Client: C2SGetAllMovementsVO (bundle line 129713, no fields)
MovementArea
pydantic-model
¶
Bases: BasePayload
A movement's TA or SA area row, read as a map row.
area_type, x and y are the first three positions, which every area type has;
item is the whole row read by the area type's own parser, None when the client reads
no row of that type, or (with a warning) when a value is of the wrong kind. The row carries no kingdom of its own,
so it is read in the movement's KID: build one from {"row": [...], "kingdom": KID},
or from the bare row, read in the green kingdom.
Client: BasicMapmovementVO.loadFromParamObject reads SA and TA with
WorldmapObjectFactory.parseWorldMapArea (bundle lines 19383, 5343), as the map reads its rows
Fields:
Validators:
-
_from_row
item
pydantic-field
¶
item: MapAreaItem | None = None
The row as its area type's parser reads it; None when it cannot be read
MovementRecord
pydantic-model
¶
Bases: BasePayload
The movement itself: the M inside each gam wrapper.
Client: BasicMapmovementVO.loadFromParamObject.
Fields:
-
movement_id(int) -
movement_type(int) -
progress_time(int) -
total_time(int) -
direction(int) -
target_id(int) -
kingdom_id(int) -
source_id(int) -
owner_id(int) -
horse_booster(EnumOrInt['Horse'] | None) -
target_area(MovementArea | None) -
source_area(MovementArea | None)
Validators:
-
_areas_in_the_movements_kingdom -
_horse→horse_booster
progress_time
pydantic-field
¶
progress_time: int = 0
Wire key PT
Seconds travelled when the reply was sent
direction
pydantic-field
¶
direction: int = 0
Wire key D
1 = returning home, 0 = heading to the target
MovementArmy
pydantic-model
¶
Bases: BasePayload
A visible army: three flanks plus the courtyard wave, each {Unit or Tool: amount}.
Client: CastleCompactArmyVO.parseSimpleArmy / parseYardWave (bundle lines 67525,
67529) read each into a UnitInventoryDictionary, which adds up an id sent twice.
Fields:
MovementUnitInfo
pydantic-model
¶
Bases: BasePayload
Commander and wait details: a wrapper's UM.
Client: BasicMapmovementVO.parseUnitMovement (bundle line 19385), which hands L to
LordFactory.createLord (bundle line 26399).
Fields:
-
commander(Commander | None) -
wait_passed(int) -
wait_total(int) -
advisor_type(EnumOrInt[AttackAdvisorType]) -
advisor_movement_count(int) -
advisor_movement_number(int) -
advisor_is_last(int)
Validators:
-
_readable_commander→commander
commander
pydantic-field
¶
commander: Commander | None = None
Wire key L
Commander leading the army; None when there is none
wait_passed
pydantic-field
¶
wait_passed: int = 0
Wire key PWD
Seconds of the wait at the target already passed
advisor_type
pydantic-field
¶
advisor_type: EnumOrInt[AttackAdvisorType] = AttackAdvisorType.NONE
Wire key AAT
The attack advisor that sent the attack
advisor_movement_count
pydantic-field
¶
advisor_movement_count: int = 0
Wire key AAC
Attacks in the advisor series
MovementMarket
pydantic-model
¶
Bases: BasePayload
A market transport's cargo: a wrapper's MM.
Client: MarketMapmovementVO.parse_MM (bundle line 43706), which reads G with
CollectableParserS2CParamList.createList (bundle line 40560)
Fields:
MovementSpy
pydantic-model
¶
Bases: BasePayload
A spy mission's details: a wrapper's S.
Client: SpyMapmovementVO.parseSpyInfo (bundle line 43748), which reads each through int().
Fields:
MovementWrapper
pydantic-model
¶
Bases: BasePayload
One entry of gam's M list, and the A of an abr/asr push or an mcm reply.
Which of the optional blocks are present depends on the movement type and on what the receiving player may see. Keys this model does not name are kept, as on every payload.
Client: MapmovementFactory.parseMapMovement and the loadFromParamObject
of each movement class.
Fields:
-
movement(MovementRecord) -
full_army(MovementArmy | None) -
army(MovementArmy | None) -
army_size(int | None) -
unit_info(MovementUnitInfo | None) -
attack_type(int | None) -
is_shadow(bool) -
force_cancelable(bool) -
support_tools(SupportToolSlots) -
auto_skip_cooldown_type(EnumOrInt[AutoSkipCooldownType]) -
travel_units(WodAmounts) -
travel_goods(CollectableRows) -
market(MovementMarket | None) -
spy(MovementSpy | None)
Validators:
-
_no_spy_details→spy -
_auto_skip→auto_skip_cooldown_type
full_army
pydantic-field
¶
full_army: MovementArmy | None = None
Wire key FA
The full army, preferred over army
army_size
pydantic-field
¶
army_size: int | None = None
Wire key GS
Estimated army size when the army is hidden
unit_info
pydantic-field
¶
unit_info: MovementUnitInfo | None = None
Wire key UM
Commander and wait details
attack_type
pydantic-field
¶
attack_type: int | None = None
Wire key ATT
AttackType value of an attack
force_cancelable
pydantic-field
¶
force_cancelable: bool = False
Wire key FC
The movement can be force-cancelled
support_tools
pydantic-field
¶
Wire key AST
Support tools sent along, as the attack sent them; None for an empty slot
auto_skip_cooldown_type
pydantic-field
¶
auto_skip_cooldown_type: EnumOrInt[AutoSkipCooldownType] = AutoSkipCooldownType.OFF
Wire key ASCT
How the target's cooldown is skipped on arrival
travel_units
pydantic-field
¶
Wire key A
Units and tools of a travel movement
travel_goods
pydantic-field
¶
travel_goods: CollectableRows = ()
Wire key G
Loot a travel movement carries (ArmyTravelMapMovementVO.loadFromParamObject, bundle line 26798)
MovementOwner
pydantic-model
¶
Bases: BasePayload
An owner record: one entry of O in gam, abr and asr.
Client: WorldMapOwnerInfoVO.fillFromParamObject.
Fields:
-
player_id(int) -
name(str) -
crest(OwnerCrest | None) -
level(int) -
legend_level(int) -
beginner_protection_seconds(int) -
honor(int) -
might(int) -
top_x(int) -
is_ruin(bool) -
alliance_id(int) -
alliance_rank(int) -
alliance_name(str) -
is_searching_alliance(bool) -
peace_seconds(int) -
castle_positions(list[OwnerCastlePosition]) -
village_positions(list[OwnerCastlePosition]) -
has_premium(bool) -
has_vip(bool) -
is_dummy(bool) -
achievement_points(int) -
relocation_seconds(int) -
faction(OwnerFaction | None) -
title_prefix_id(EnumOrInt['Title'] | None) -
title_suffix_id(EnumOrInt['Title'] | None) -
via_refer_a_friend(bool)
Validators:
-
_title→title_prefix_id,title_suffix_id -
_truthy_flag→is_searching_alliance,has_premium,has_vip -
_parsed_one_flag→is_ruin -
_position_rows→castle_positions,village_positions -
_one_flag→is_dummy -
_parsed_truthy_flag→via_refer_a_friend
beginner_protection_seconds
pydantic-field
¶
beginner_protection_seconds: int = -1
Wire key RNP
-1 when not protected
relocation_seconds
pydantic-field
¶
relocation_seconds: int = 0
Wire key RRD
Seconds until a relocation ends
title_prefix_id
pydantic-field
¶
title_prefix_id: EnumOrInt['Title'] | None = None
Wire key PRE
The title shown before the name; None for none
GetMovementsResponse
pydantic-model
¶
Bases: BaseResponse
Response containing active movements.
Command: gam Payload: {"M": [wrapper, ...], "O": [owner record, ...]}
Client: CastleArmyData.parse_GAM.
Fields:
owners
pydantic-field
¶
owners: list[MovementOwner]
Wire key O
Owner records for every player the movements name
CancelMovementRequest
pydantic-model
¶
Bases: BaseRequest
Recall one of your movements, turning it back home.
Command: mcm Payload: {"MID": movement_id}
The client sends it from its retreat dialog, from "send home" for your supports stationed at an area (one per support), and from the attack advisor's cancel dialog (one per advisor attack). The retreat button is enabled only where the movement's class allows it:
- attacks (every attack type but alien attacks, which never): your own,
heading to the target, from level 5, and no more than 600 seconds after
leaving, unless the movement is force-cancelable (
FC), attacks nomads while the alliance nomad invasion is not running, or attacks a faction tower - supports: your own, in either direction
- travel between your areas: your own, heading to the target, from a source area that is still yours
- spies, market transports and occupations: your own, heading to the target
- treasure hunts and plague monks: never
The movement list also disables it while the source area is under conquer control. The advisor's overview cancels every advisor attack that is not the series' last, returning ones too, without these checks. The server's refusals include INVALID_STARTAREA_FOR_CANCEL_MOVEMENT (189); the client handles none itself and shows its generic server error.
Client: C2SCancelMovementVO (bundle line 33049); sent by
CastleAskRetreatDialog.onClick (bundle line 33234),
SupportOverviewDialog.onClickRetreat (bundle line 33984) and
AdvisorAttackOverviewCancelDialog.cancelAttacks (bundle line 21965),
fed by AdvisorAttackOverviewDialog.getCancellableAdvisorMovements
(bundle line 57303); canBeRetreated on ArmyAttackMapmovementVO
(bundle line 14379), ArmyTravelMapMovementVO (bundle line 26818),
SupportDefenceMapmovementVO (bundle line 43782), SpyMapmovementVO
(bundle line 43759), MarketMapmovementVO (bundle line 43707),
SiegeMapmovementVO (bundle line 33183) and the always-false ones
(bundle lines 33079, 43733, 43805); ArmyAttackMapmovementVO.tooLateToBeRetreated
(bundle line 14382) with TravelConst.MAX_FALLBACK_TIME (ggs.dll line
19828); ClientConstLevelRestrictions.MIN_LEVEL_RETREAT_MOVEMENTS (bundle
line 2139) through CastleUserData.hasLevelFor (bundle line 10027);
AMovementRenderStrategy.initRetreatButton (bundle line 14347);
MapmovementFactory.parseMapMovement (bundle line 133793)
Fields:
CancelMovementResponse
pydantic-model
¶
Bases: BaseResponse
The reply to a recall: the movement as it now stands.
Command: mcm Payload: {"A": wrapper}
A is read as one gam entry, replacing the tracked movement. The
client reads it unconditionally, so a reply without one is malformed.
Client: MCMCommand.executeCommand (bundle line 126041), which hands
[A] to CastleArmyData.parseMapMovementArray (bundle line 133626)
Fields: