Skip to content

State

The thread-safe game state that server pushes keep current.

state.manager

The game state, kept current from server packets.

Classes:

Name Description
GameState

Manages game state parsed from server packets.

Attributes:

Name Type Description
MovementEventCallback
EventCallback
EventsCallback
MOVEMENT_PARSE_WARN_INTERVAL

MovementEventCallback module-attribute

MovementEventCallback = Callable[[int], Any] | Callable[[int, Movement | None], Any]

EventCallback module-attribute

EventCallback = Callable[[SpecialEvent], Any]

EventsCallback module-attribute

EventsCallback = Callable[[dict[int, SpecialEvent]], Any]

MOVEMENT_PARSE_WARN_INTERVAL module-attribute

MOVEMENT_PARSE_WARN_INTERVAL = 60.0

GameState

GameState()

Bases: MovementState, CastleState, AreaState, PlayerState, EventState, CommanderState, AllianceState, ProgressState, QuestState, AccountState, InventoryState

Manages game state parsed from server packets.

State is mutated by the network receive thread and read from user threads, so all mutation and snapshot reads are guarded by a lock.

The public attributes stay readable directly, but callers that read several fields at once (or iterate a container) should use the snapshot accessors — get_local_player(), get_special_currencies(), get_castles(), get_all_movements(), get_events(), get_alliance_chat(), get_commanders(), get_skills(), get_own_alliance(), get_quests() — which copy under the lock, or hand out read-only models (get_research(), get_boosts() and the other progress accessors). Mutation paths swap containers instead of editing them in place, so an unlocked reader that already holds one never sees it change underneath.

Callbacks (the on_* registrations) run on one callback thread, never on the receive thread: one at a time, in the order their packets were applied, so two callbacks never run at once and the events for one movement arrive in order. A callback may wait for a reply, but everything queued behind it waits too, so hand long work to another thread.

Freshness

Nothing here polls the server: every field is as old as the last packet that carried it, and some packets arrive only once per session. Cached values are therefore not automatically current:

===================================  ==========================  ======================================
State                                Refreshed by                Force a refresh with
===================================  ==========================  ======================================
castle name/coords, castle list      ``gcl``, ``mir`` (pushed)    re-login
castle resources/units/details       ``dcl``                      ``client.castle.get_details(id)``
castle units (also)                  ``rue`` (pushed)             ``client.castle.get_details(id)``
castle resources (also)              ``grc``, the replies that    ``client.castle.get_resources(id)``
                                     carry one (``ebu``, ...)
castle open-gate counter             ``gcl``, ``kik`` (pushed)    re-login
castle unlocked units and horses     ``gpc`` (pushed)             re-login
joined area, slum level, discount    ``jaa``, ``csl``/``gab``     ``client.castle.join(id)``
                                     (pushed)
joined castle's mines                ``gsm`` (pushed), ``jaa``,   ``client.castle.join(id)``
                                     ``cmr``
joined castle's resource carts       ``rci`` (pushed), ``jaa``,   ``client.castle.join(id)``
                                     ``rcc``
player identity/level/XP             ``gpi``/``gxp``/``glu``      re-login
player coins/rubies, VIP, alliance    ``gcu``/``vip``/``gal``      re-login
honor, beginner protection           ``gho``/``uap``              re-login
special currencies                   ``sce`` (pushed)             --
spies owned, before boosts           ``gms`` (pushed)             re-login
commanders and castellans            ``gli``, the replies that    ``client.commanders.get_all()``
                                     carry one (``arl``, ...)
legend and sceat skills              ``skl``, ``ego``             ``client.skills.get_skills()``
your alliance's details, members     ``ain``, ``acn``, ``akm``,   ``client.alliance.get_alliance_info(id)``
                                     ..., ``acm`` (online)
alliance chat history                ``acl``, ``acm`` (pushed)    --
research                             ``rei``, ``res``, ``msr``    re-login
boosters, premium, slots, festival   ``boi``, booster replies,    re-login
                                     ``bfs``
might points                         ``gmu`` (pushed)             re-login
glory points                         ``ufa`` (pushed)             re-login
Berimond points                      ``ufp`` (pushed only)        --
top-X ranks, Storm Islands title     ``uar`` (pushed)             re-login
achievements                         ``vli`` (pushed)             re-login
relocation                           ``gri`` (pushed)             re-login
plague monks                         ``cpi``, ``cpm``, ``sbp``    re-login
daily reset                          ``drt`` (login only)         re-login
attack counter                       ``gai`` (pushed)             re-login
officers' school training            ``gatp``, ``gtp``            re-login
boosted global effects               ``bie`` (pushed)             re-login
gift packages                        ``pgl``                      re-login
ruby wishing well                    ``rww``                      re-login
new relics flag                      ``nrf`` (pushed)             re-login
gems and relic gems                  ``ggm``, ``gec`` (pushed)    re-login
loot boxes, key progress             ``gls`` (pushed)             re-login
inventory space                      ``esl``, the crafting and    re-login
                                     selling replies
kingdoms, transfers between them     ``kpi``, the transfer        re-login
                                     replies
mercenary missions                   ``mpe``                      re-login
tax collection                       ``txi``, ``txs``, ``txc``,   ``client.castle.get_tax_info()``
                                     ``btx``
construction item expiry             ``nec`` (pushed)             re-login
resource citizen                     ``irc`` (pushed)             --
running events, scores, ends         ``sei``/``tei`` (pushed),    ``client.events.refresh()``
                                     ``see``/``tee``, ``pep``,
                                     ``fjf``, ``bst``, ``cqs``
active quests, quest book            ``qli`` (pushed after        --
                                     login), ``qst``, ``qfi``,
                                     ``msp``
daily quests                         ``dql`` (pushed)             re-login
movements                            ``gam``, ``abr``/``asr``,    ``client.movements.get_movements()``
                                     your sends' replies
                                     (``cra``, ``cds``, ...)
===================================  ==========================  ======================================

Every player section above is sent inside the login gbd and again as a push of its own when it changes. When the connection is lost, or the client is closed, everything is reset (see reset). The next login's gbd refills the player and castles, and the gam the server pushes after it (seen live) the movements; get_last_packet_time("gbd") and get_last_packet_time("gam") say when.

In practice a castle's resources often reflects login time and nothing else, so use the freshness accessors before trusting them: get_castle_last_updated / get_castle_age, get_player_last_updated, get_events_last_updated, and get_last_packet_time / get_packet_times for per-packet timestamps. All timestamps are wall-clock (time.time()) seconds, and None means "never seen", which is different from "seen and empty".

Methods:

Name Description
update_from_packet

Central update router — parses packet and updates state.

get_last_packet_time

When a packet (or gbd sub-packet) of this kind was last applied.

get_packet_times

Snapshot of every tracked packet/sub-packet timestamp.

update_from_packet

update_from_packet(cmd_id: str, payload: dict[str, Any], error_code: int = 0) -> None

Central update router — parses packet and updates state.

error_code is the packet's status; a command the client reads only from a successful reply is skipped, unstamped, when it is not 0.

Every packet, handled or not, also advances movements, as the movement clock does between packets.

get_last_packet_time

get_last_packet_time(cmd_id: str) -> float | None

When a packet (or gbd sub-packet) of this kind was last applied.

Accepts the wire ids this manager tracks — "gbd", "gam", "dcl", "abr", "asr", the send replies ("cra", "cam", "abgcam", "cds", "csm", "cat", "crm", "css", "tde", "cdd", "cpm", "thm", "ldt"), "mcm", "mrm", "mfc", "glu", "mir", "fjf", "bst", "sce", "see", "tee", "pep", "acm", "aqi", "acn", "cal", "ufp", "bfs", the officers' school's "gtp" (only a successful one), the quest pushes "qli", "qst", "qpg", "qfi", "msp" and the campaign's "cqs", the castle pushes "rue", "kik", "fbe", "cbx", "gdb", "gcb", the gem push "gec", the joined area's "jaa", "cmr", "rcc" and the map read "gaa" (refused too) — and the login sections "gpi", "gxp", "gcu", "vip", "gal", "gcl", "gho", "uap", "gpc", "gms", "sei", "tei", "gli", "skl", "ain", "acl", "rei", "boi", "gmu", "ufa", "uar", "vli", "gri", "cpi", "dql", "gai", "gatp", "bie", "pgl", "rww", "nrf", "ggm", "gls", "esl", "kpi", "mpe", "txi", "nec", "irc" and "grc", stamped whether they came inside a gbd, as a push of their own or inside a reply that carries one ("sei" from a fjf or bst, "gli" from an arl, "ain" from an akm, "rei" from a res, "kpi" from a kut, ...), plus "gac" and "drt", which only come inside a gbd. "gsm" and "rci" are stamped whenever mines or resource carts are applied, from their push or a jaa, cmr or rcc reply; "csl" and "gab" from their push or a jaa, the push even when no area was joined to apply it to. A send reply is stamped even when the server refused the send; the commands the client reads only from a successful reply (the event ones, the login section ones, the castle ones, the quest ones, "gec", "acm", "aqi", "ufp" and "bfs") are not, nor is a login section that was not applied: unreadable, not valid, or another alliance's "ain". None means none was ever seen; packets this manager ignores are never recorded.

get_packet_times

get_packet_times() -> dict[str, float]

Snapshot of every tracked packet/sub-packet timestamp.

state.base

State shared by the GameState mixins: the lock, tracked data and callback dispatch.

Classes:

Name Description
StateBase

The data every GameState mixin reads and writes, created once in __init__.

Functions:

Name Description
read_section

A section block read as model; None, with a warning, when it is no object or does not validate.

Attributes:

Name Type Description
logger
CALLBACK_QUEUE_WARN_DEPTH
CALLBACK_QUEUE_WARN_INTERVAL
MovementEventCallback

logger module-attribute

logger = logging.getLogger(__name__)

CALLBACK_QUEUE_WARN_DEPTH module-attribute

CALLBACK_QUEUE_WARN_DEPTH = 1000

CALLBACK_QUEUE_WARN_INTERVAL module-attribute

CALLBACK_QUEUE_WARN_INTERVAL = 60.0

MovementEventCallback module-attribute

MovementEventCallback = Callable[[int], Any] | Callable[[int, Movement | None], Any]

StateBase

StateBase()

The data every GameState mixin reads and writes, created once in __init__.

Methods:

Name Description
reset

Forget all the session sent: player, castles, joined area, movements, events, login sections, timestamps.

shutdown

Stop the movement clock and shut the callback executor down. Call when done with the client.

Attributes:

Name Type Description
callback_queue_depth int

Callbacks queued on the callback thread and not finished yet, the running one included.

callback_queue_depth property

callback_queue_depth: int

Callbacks queued on the callback thread and not finished yet, the running one included.

reset

reset() -> None

Forget all the session sent: player, castles, joined area, movements, events, login sections, timestamps.

Registered callbacks stay, and so does the record of attacks and occupations already announced to on_incoming_attack and on_occupation_started. Fires no callback: a movement that is dropped here was not seen to arrive or be removed. The client resets its data the same way when the connection is lost; the next login's gbd, and the gam the server pushes after it (seen live), rebuild it.

Client: CastleConnectionLostCommand.execute (bundle line 120254) runs CastleDestroyGameCommand, whose destroyGameSpecificObjects (line 120270) calls CastleModel.resetModels; CastleArmyData.reset (line 133620) starts the movement map over.

shutdown

shutdown() -> None

Stop the movement clock and shut the callback executor down. Call when done with the client.

Each starts again when next needed, so a client logged in again keeps working. Until this runs (client.close() runs it), the movement clock's thread keeps the state and its callbacks in memory until the last movement it knows of has passed.

read_section

read_section(model: type[_M], body: Any, what: str) -> _M | None

A section block read as model; None, with a warning, when it is no object or does not validate.

state.movements

Movement tracking: gam and its pushes, arrival, removal and the movement queries.

Classes:

Name Description
MovementState

Attributes:

Name Type Description
logger
MOVEMENT_PARSE_WARN_INTERVAL
MOVEMENT_CLOCK_MAX_WAIT

logger module-attribute

logger = logging.getLogger(__name__)

MOVEMENT_PARSE_WARN_INTERVAL module-attribute

MOVEMENT_PARSE_WARN_INTERVAL = 60.0

MOVEMENT_CLOCK_MAX_WAIT module-attribute

MOVEMENT_CLOCK_MAX_WAIT = 3600.0

MovementState

MovementState()

Bases: StateBase

Methods:

Name Description
on_incoming_attack

Register a callback for new hostile attack movements.

on_incoming_attack_updated

Register a callback for changes to an attack on_incoming_attack announced.

on_incoming_attack_withdrawn

Register a callback for announced attacks the server removes before they arrive.

on_occupation_started

Register a callback for occupations of your areas or your alliance members'.

on_occupation_updated

Register a callback for changes to an occupation on_occupation_started announced.

on_occupation_ended

Register a callback for occupations on_occupation_started announced leaving state.

on_movement_recalled

Register a callback for your own recalled movements.

on_movement_arrived

Register a callback for movements reaching their target.

on_movement_removed

Register a callback for movements the server removes (mrm).

get_all_movements

Get all tracked movements (stale ones pruned first).

get_incoming_movements

Other players' armies heading to the local player (see Movement.is_incoming); occupations are not.

get_outgoing_movements

The local player's armies heading to their targets, returns excluded.

get_incoming_attacks

Get all incoming attack movements.

get_announced_attacks

The attacks on_incoming_attack announced that are still on their way, in the order announced.

get_occupations

The occupations on_occupation_started announced that have not ended, in the order announced.

reannounce

Fire on_incoming_attack or on_occupation_started again for an announced movement.

get_movement_by_id

Get a tracked movement by its Movement.movement_id, as client.movements.get_movements() lists it.

on_incoming_attack

on_incoming_attack(callback: Callable[[Movement], object]) -> None

Register a callback for new hostile attack movements.

Fires once per newly seen attack that is not the local player's own, is not on its way home and had not already landed when first seen, and is aimed at you or the daimyo township (an alien attack only at you), whoever sends it, or at another member of your alliance. An attack on an alliance member fires when a player the movement names sends it, when an alien attack or the alliance nomad camp does, or when an NPC that is not a dungeon owner does (an outpost, capital or metropolis owner, the plague monk, or an NPC id the client does not know); robber barons, camps, event dungeons and the other dungeon owners do not fire it. An alliance member's attack on someone outside the alliance does not fire. Each packet that carries an attack judges it again, with the owner records seen so far, so one whose attacker's record comes later fires then.

Fires once per attack movement id, also across a reconnect: an attack still on its way when the connection drops is not announced again when the next login lists it. The record of an attack is dropped when it arrives, when the server removes it, or once its travel time is over.

Runs on the callback thread, in packet order (see GameState).

Unregister a callback with on_incoming_attack.remove(callback).

on_incoming_attack_updated

on_incoming_attack_updated(callback: Callable[[Movement, Movement], object]) -> None

Register a callback for changes to an attack on_incoming_attack announced.

callback(old, new) fires on a later packet for the same attack that changes its army (units, estimated_size), its arrival (estimated_arrival, by two seconds or more, as a speed-up does), its target (target_id, target_area_id, target_x, target_y) or its commander (commander, its equipment and area effects included). A packet that changes none of these does not fire it, nor does the packet that announces the attack, nor any packet once the attack has arrived or been removed. After a reconnect the first listing of an attack has nothing to compare with, so it does not fire either. old is the Movement as state had it before the packet, new the one it holds now.

The client keeps no history to compare with: parseMapMovementArray (bundle line 133626) replaces a movement's object with each packet, so this is derived by comparing the two.

Runs on the callback thread, in packet order (see GameState).

Unregister a callback with on_incoming_attack_updated.remove(callback).

on_incoming_attack_withdrawn

on_incoming_attack_withdrawn(callback: Callable[[Movement], object]) -> None

Register a callback for announced attacks the server removes before they arrive.

Fires once, with the attack as state last had it, when mrm removes an attack that on_incoming_attack announced while its travel time is not up yet. An attack removed within two seconds of its estimated arrival (estimated_arrival can run a second late), at or after it, or one state no longer tracks, does not fire. on_movement_removed still fires for the same mrm, first.

Derived, not reported: the server does not say why it removes a movement, and neither does the client. CastleArmyData.parse_MRM (bundle line 133633) only drops it, and the client removes an arrived movement on its own timer (updateMapmovements, bundle line 133670, once currentProgress reaches 1), so a removal before arrival is the attack turning back or being called off.

Runs on the callback thread, in packet order (see GameState).

Unregister a callback with on_incoming_attack_withdrawn.remove(callback).

on_occupation_started

on_occupation_started(callback: Callable[[Movement], object]) -> None

Register a callback for occupations of your areas or your alliance members'.

An occupation (MovementType.SIEGE or OCCUPY_FACTION, both parsed as SiegeMapmovementVO, bundle line 133809) follows a capture attack that landed and won: the game's movement overview lists it as "Occupying forces" (RenderSiege, bundle line 67408, dialog_moveOverview_siege). Its travel time is the time the occupier must hold the area (the battle log's dialog_battleLog_youSiege24h); once it runs out the area is captured. It is no attack, so on_incoming_attack never reports it; the capture attack before it is an ordinary attack (isConquerMovement, bundle line 14404).

Fires once per newly seen occupation that is not the local player's own, is not on its way home and had not already ended when first seen, and holds an area of yours or the daimyo township, or of another member of your alliance, whoever sends it. One on someone outside the alliance does not fire. As for attacks, each packet judges it again, and it fires once per movement id, also across a reconnect.

The client raises no attack warning for an occupation: SiegeMapmovementVO (bundle line 33173) is a BasicMapmovementVO, and CastleArmyData.checkAllAttackMovements (bundle line 133659) counts only ArmyAttackMapmovementVO (its type check, 133664). These are the occupations its movement list shows: FilterAttack (bundle line 67995) those whose target owner is you, which the daimyo township's is (getOwnerInfoVO, bundle line 138994), and FilterAllianceIncoming (bundle line 67971) those, not yours, whose target owner is another member of your alliance. Leaving out your own and returning occupations is the library's choice, as for attacks: neither filter checks the direction, and FilterAttack (67996) does not check the sender either.

Runs on the callback thread, in packet order (see GameState).

Unregister a callback with on_occupation_started.remove(callback).

on_occupation_updated

on_occupation_updated(callback: Callable[[Movement, Movement], object]) -> None

Register a callback for changes to an occupation on_occupation_started announced.

callback(old, new) fires on the same changes, and with the same exceptions, as on_incoming_attack_updated does for an attack: a changed estimated_arrival is a changed end of the occupation time. An occupation's army is its A block (SiegeMapmovementVO.parseArmy, bundle line 33176), read into units.

Runs on the callback thread, in packet order (see GameState).

Unregister a callback with on_occupation_updated.remove(callback).

on_occupation_ended

on_occupation_ended(callback: Callable[[Movement, bool], object]) -> None

Register a callback for occupations on_occupation_started announced leaving state.

callback(movement, captured) fires once per announced occupation, with it as state last had it. captured is True when its time ran out: at its arrival, after on_movement_arrived, or when mrm removes it within two seconds of its estimated arrival or later, after on_movement_removed. The game then reports the area captured ("managed to occupy your outpost for long enough and has now captured it", dialog_siegeMessage_yourOutpostWasConquered). captured is False when mrm removes it earlier, after on_movement_removed: the occupation was broken ("Occupying forces driven off!", dialog_messageHeader_siegeCancelledByPlayer, dialog_siegeMessage_siegeCancelled). One that ends while no session is logged in is not reported: state drops it with the session (see reset) and the next login does not list it.

Derived, not reported, as on_incoming_attack_withdrawn is: the server does not say why it removes a movement, and the reports above come as messages of their own.

Runs on the callback thread, in packet order (see GameState).

Unregister a callback with on_occupation_ended.remove(callback).

on_movement_recalled

on_movement_recalled(callback: MovementEventCallback) -> None

Register a callback for your own recalled movements.

Fires on the mcm reply to a recall, with the movement as it now stands: on its way home (is_returning). A plain removal (mrm) fires on_movement_removed instead.

Accepts either signature (see on_movement_arrived):

def on_recalled(movement_id: int) -> None: ...
def on_recalled(movement_id: int, movement: Movement | None) -> None: ...

Runs on the callback thread, in packet order (see GameState).

Unregister a callback with on_movement_recalled.remove(callback).

on_movement_arrived

on_movement_arrived(callback: MovementEventCallback) -> None

Register a callback for movements reaching their target.

The server sends no arrival packet: as in the game client, a movement arrives once its travel time is up (estimated_arrival). A timer of the state's own fires it then, with no packet needed, and every packet and movement query checks too. It fires once per movement, and not for a movement first seen after it had already arrived.

The client checks on its update tick (CastleArmyData.executeUpdate, bundle line 133669, running updateMapmovements, line 133670); the timer is the library's way to the same moment.

An army that stays at its target (a stationed support) is kept in state until its wait is over (estimated_end); every other movement is removed before callbacks run. An army's way home is a movement too, so it fires when the army gets back; check movement.is_returning to tell the two apart.

Two signatures are supported, picked per callback from its own parameter list:

def on_arrived(movement_id: int) -> None: ...
def on_arrived(movement_id: int, movement: Movement | None) -> None: ...

Prefer the second form: the id alone says nothing about what arrived.

Runs on the callback thread, in packet order (see GameState).

Unregister a callback with on_movement_arrived.remove(callback).

on_movement_removed

on_movement_removed(callback: MovementEventCallback) -> None

Register a callback for movements the server removes (mrm).

The server does not say why: a battle ending, a finished recall and a support sent home all look the same; on_incoming_attack_withdrawn tells an announced attack removed before it arrives, and on_occupation_ended an announced occupation leaving. movement is None if state was not tracking it. Accepts either signature (see on_movement_arrived).

Runs on the callback thread, in packet order (see GameState).

Unregister a callback with on_movement_removed.remove(callback).

get_all_movements

get_all_movements() -> list[Movement]

Get all tracked movements (stale ones pruned first).

get_incoming_movements

get_incoming_movements() -> list[Movement]

Other players' armies heading to the local player (see Movement.is_incoming); occupations are not.

get_occupations lists those the library announced.

get_outgoing_movements

get_outgoing_movements() -> list[Movement]

The local player's armies heading to their targets, returns excluded.

get_incoming_attacks

get_incoming_attacks() -> list[Movement]

Get all incoming attack movements.

Attacks whose travel time is up are dropped first, so this never reports one that has landed.

get_announced_attacks

get_announced_attacks() -> list[Movement]

The attacks on_incoming_attack announced that are still on their way, in the order announced.

Each is the Movement as state holds it now, from the latest packet that carried it. Unlike get_incoming_attacks, which lists the attacks aimed at you, this is what the callback reported, attacks on alliance members included. An attack leaves it when it arrives or the server removes it. After a reconnect, or a close() and login(), an attack is listed again once a packet lists it, without being announced again (see reannounce): a client handed to a new holder does not announce to it what it announced before.

Library bookkeeping: the client keeps no record of what it announced.

get_occupations

get_occupations() -> list[Movement]

The occupations on_occupation_started announced that have not ended, in the order announced.

Listed as get_announced_attacks lists attacks; an occupation leaves the list as on_occupation_ended reports it.

reannounce

reannounce(movement_id: int, *, when_listed: bool = False) -> bool

Fire on_incoming_attack or on_occupation_started again for an announced movement.

For a consumer whose callback could not act on the announcement (an alert that failed to send). The callbacks get the Movement as state holds it now, on the callback thread, behind those already queued. Nothing else changes: the movement stays announced, so no packet announces it again. A client.listen() stream gets it as an ordinary incoming_attack or occupation_started event.

After a reconnect, a movement announced before it is known again only once a packet lists it: the movement list the server pushes after the login. on_session_restored fires once that list has come, or once config.request_timeout passed without it (a warning is logged), so even there it may not be listed yet. Until then the callbacks cannot fire. With when_listed they are queued instead, and fire once, with the Movement as the next packet that lists it has it, however often this was called. The queued call is dropped without firing if no packet lists the movement before its travel time is over, the server removes it, or it is listed again with its arrival behind it. An AccountPool release drops it too.

Parameters:

Name Type Description Default
movement_id int

The movement to announce again.

required
when_listed bool

Queue the callbacks for an announced movement no packet has listed since a reconnect, instead of returning False.

False

Returns:

Type Description
bool

True if the callbacks were queued, now or with when_listed for

bool

the next listing; False if movement_id is not announced (never

bool

announced, arrived or ended, or removed), or without

bool

when_listed not listed again since a reconnect.

Library bookkeeping: the client announces nothing (see on_incoming_attack).

get_movement_by_id

get_movement_by_id(movement_id: int) -> Movement | None

Get a tracked movement by its Movement.movement_id, as client.movements.get_movements() lists it.

state.announcer

Announcing movements: which attacks and occupations count as on you, and the events they fire.

Classes:

Name Description
AttackEvents

Incoming attacks: only one removed before it arrives is reported, as withdrawn.

OccupationEvents

Occupations: each one leaving is reported as ended, captured when its time ran out.

MovementAnnouncer

The attacks and occupations a state announced, judged as its store reports movements stored, arrived, removed.

Attributes:

Name Type Description
ETA_CHANGE_THRESHOLD
UPDATED_ATTACK_FIELDS
QueuedCall

ETA_CHANGE_THRESHOLD module-attribute

ETA_CHANGE_THRESHOLD = 2.0

UPDATED_ATTACK_FIELDS module-attribute

UPDATED_ATTACK_FIELDS = ('units', 'estimated_size', 'target_id', 'target_area_id', 'target_x', 'target_y', 'commander')

QueuedCall module-attribute

QueuedCall = tuple[Callable[..., object], tuple[Any, ...]]

AttackEvents dataclass

AttackEvents(announced: BoundEvent[Callable[[Movement], object]], updated: BoundEvent[Callable[[Movement, Movement], object]], withdrawn: BoundEvent[Callable[[Movement], object]])

Incoming attacks: only one removed before it arrives is reported, as withdrawn.

Methods:

Name Description
leaving

The calls for an announced attack leaving state, at its arrival or removed before it.

Attributes:

Name Type Description
announced BoundEvent[Callable[[Movement], object]]
updated BoundEvent[Callable[[Movement, Movement], object]]
withdrawn BoundEvent[Callable[[Movement], object]]

announced instance-attribute

announced: BoundEvent[Callable[[Movement], object]]

updated instance-attribute

updated: BoundEvent[Callable[[Movement, Movement], object]]

withdrawn instance-attribute

withdrawn: BoundEvent[Callable[[Movement], object]]

leaving

leaving(mov: Movement, arrived: bool) -> list[QueuedCall]

The calls for an announced attack leaving state, at its arrival or removed before it.

OccupationEvents dataclass

OccupationEvents(announced: BoundEvent[Callable[[Movement], object]], updated: BoundEvent[Callable[[Movement, Movement], object]], ended: BoundEvent[Callable[[Movement, bool], object]])

Occupations: each one leaving is reported as ended, captured when its time ran out.

Methods:

Name Description
leaving

The calls for an announced occupation leaving state, at its arrival or removed before it.

Attributes:

Name Type Description
announced BoundEvent[Callable[[Movement], object]]
updated BoundEvent[Callable[[Movement, Movement], object]]
ended BoundEvent[Callable[[Movement, bool], object]]

announced instance-attribute

announced: BoundEvent[Callable[[Movement], object]]

updated instance-attribute

updated: BoundEvent[Callable[[Movement, Movement], object]]

ended instance-attribute

ended: BoundEvent[Callable[[Movement, bool], object]]

leaving

leaving(mov: Movement, arrived: bool) -> list[QueuedCall]

The calls for an announced occupation leaving state, at its arrival or removed before it.

MovementAnnouncer

MovementAnnouncer(attacks: AttackEvents, occupations: OccupationEvents, fire: Callable[..., None])

The attacks and occupations a state announced, judged as its store reports movements stored, arrived, removed.

The store calls it under the state lock, and fire queues an event's callbacks on the callback thread. It lives as long as the state, so a reconnect does not announce the same movement again.

Methods:

Name Description
events_for

The events of the kind of announced movement mov is, None if it is neither attack nor occupation.

stored

Fire the callbacks for a movement a packet stored, old being the one it replaces.

arrived

Forget an arrived movement; the calls to queue after its arrival if it was announced.

removed

Forget a movement mrm removed; the calls to queue after its removal if it was announced.

prune

Forget announced movements whose end has passed while no session tracked them.

listed

The movements of one kind still announced, in the order announced.

reannounce

Fire the announced callbacks again for an announced movement; False if it is not one.

Attributes:

Name Type Description
attacks
occupations
announced dict[int, float]
reannounce_when_listed set[int]

attacks instance-attribute

attacks = attacks

occupations instance-attribute

occupations = occupations

announced instance-attribute

announced: dict[int, float] = {}

reannounce_when_listed instance-attribute

reannounce_when_listed: set[int] = set()

events_for

events_for(mov: Movement) -> AttackEvents | OccupationEvents | None

The events of the kind of announced movement mov is, None if it is neither attack nor occupation.

stored

stored(old: Movement | None, new: Movement, *, arrived: bool, alliance_id: int | None) -> None

Fire the callbacks for a movement a packet stored, old being the one it replaces.

arrived says its arrival is behind it (reported, or past when it was first seen); alliance_id is the local player's alliance, None outside one.

An attack is judged on every packet that carries it, as the client counts its attack warnings anew (CastleArmyData.checkAllAttackMovements, bundle line 133659), so one whose owner record comes later still fires, once; an occupation likewise. A later packet for an announced attack or occupation fires the updated callback when it changes what that callback reports, and the announced callbacks if reannounce queued it and it has not arrived.

arrived

arrived(mov: Movement) -> list[QueuedCall]

Forget an arrived movement; the calls to queue after its arrival if it was announced.

removed

removed(mid: int, mov: Movement | None, *, arrived: bool) -> list[QueuedCall]

Forget a movement mrm removed; the calls to queue after its removal if it was announced.

arrived says its arrival is behind it, as for stored. One removed within two seconds of its estimated arrival or later counts as arrived.

prune

prune(now: float) -> None

Forget announced movements whose end has passed while no session tracked them.

listed

listed(events: AttackEvents | OccupationEvents, movements: Mapping[int, Movement]) -> list[Movement]

The movements of one kind still announced, in the order announced.

reannounce

reannounce(mid: int, mov: Movement | None, *, when_listed: bool) -> bool

Fire the announced callbacks again for an announced movement; False if it is not one.

mov is the movement as tracked, None while no packet since a reconnect listed it; when_listed then queues the callbacks for stored.

state.castles

Castle tracking: the castle list (gcl), castle details (dcl), castle resources (grc), units received (rue), the open-gate counter reset (kik) and unlocked units and horses (gpc).

Classes:

Name Description
CastleState

Attributes:

Name Type Description
logger

logger module-attribute

logger = logging.getLogger(__name__)

CastleState

CastleState()

Bases: StateBase

Methods:

Name Description
get_permanent_castle

One of your castles' unlocked units and horses, from the last gpc that named it.

get_castle_horse_ids

The horses one of your castles can send movements with, as gpc sent them.

get_castles

Get a snapshot of the player's castles.

get_castle_last_updated

When this castle's detail data (resources, units) was last refreshed.

get_castle_age

Seconds since this castle's detail data was refreshed, or None.

get_permanent_castle

get_permanent_castle(castle_id: int) -> PermanentCastle | None

One of your castles' unlocked units and horses, from the last gpc that named it.

The kingdom is the castle's own, from the castle list. None when the castle is not in the castle list or no gpc named it.

Raises:

Type Description
AmbiguousCastleError

the id repeats across your kingdoms

Client: CastlePermanentCastleData.getCastleByWorldAreaId (bundle line 139145)

get_castle_horse_ids

get_castle_horse_ids(castle_id: int) -> list[EnumOrInt[Horse]] | None

The horses one of your castles can send movements with, as gpc sent them.

Look each up with GameData.require().get_horse, or use client.castle.get_horses. None when get_permanent_castle is.

get_castles

get_castles() -> list[Castle]

Get a snapshot of the player's castles.

The list itself is current, but each castle's resources, units and other dcl-only detail fields are as old as the last dcl packet for that castle — frequently the one from login, and all-zero when no dcl ever arrived. Check get_castle_last_updated / get_castle_age before treating them as live.

get_castle_last_updated

get_castle_last_updated(castle_id: int) -> float | None

When this castle's detail data (resources, units) was last refreshed.

Only a dcl stamps it. A reply carrying the castle's grc (client.castle.get_resources, building, recruiting, starting research, ...) updates its resources without stamping this; get_last_packet_time("grc") says when the last one came.

Returns a wall-clock time.time() timestamp, or None if no dcl packet ever refreshed this castle — in which case resources and units are defaults, not measurements. Refresh with client.castle.get_details(castle_id).

Raises:

Type Description
AmbiguousCastleError

the id is listed in several of your kingdoms

get_castle_age

get_castle_age(castle_id: int) -> float | None

Seconds since this castle's detail data was refreshed, or None.

None means never refreshed (see get_castle_last_updated), so treat it as infinitely stale rather than as zero.

state.area

The joined area: the jaa reply, its mines (gsm) and resource carts (rci), slum level (csl) and builder discount (gab), and the building pushes (fbe, cbx, gdb, gcb) as callbacks.

Classes:

Name Description
AreaState

Attributes:

Name Type Description
logger

logger module-attribute

logger = logging.getLogger(__name__)

AreaState

AreaState()

Bases: StateBase

Methods:

Name Description
on_building_finished

Register a callback for a building in the joined castle that finished (fbe).

on_building_xp

Register a callback for XP a building in the joined castle gave (cbx).

on_buildings_changed

Register a callback for buildings of the joined castle that were damaged (gdb) or

get_joined_area

The area joined, with its slum level and builder discount; None before any join.

get_mines

The joined castle's mines by object id, as the last gsm sent them.

get_resource_carts

The joined castle's resource carts, wood, stone and food, as the last rci sent them.

get_resource_cart

The cart of one resource in the last rci; None before any.

on_building_finished

on_building_finished(callback: Callable[[BuildingFinished], object]) -> None

Register a callback for a building in the joined castle that finished (fbe).

Called with the building's object id and the XP it gave. Runs on the callback thread, in packet order (see GameState).

Client: FBECommand.exec (bundle line 122879), AreaDataUpdater.parseCBX (bundle line 131521)

Unregister a callback with on_building_finished.remove(callback).

on_building_xp

on_building_xp(callback: Callable[[BuildingXP], object]) -> None

Register a callback for XP a building in the joined castle gave (cbx).

Client: CBXCommand.executeCommand (bundle line 123345)

Unregister a callback with on_building_xp.remove(callback).

on_buildings_changed

on_buildings_changed(callback: Callable[[DamagedBuildings], object]) -> None

Register a callback for buildings of the joined castle that were damaged (gdb) or whose efficiency changed (gcb, a BuildingEfficiencyChanged).

Called with the buildings' new rows. State does not keep the joined castle's buildings: the build, upgrade and repair replies change them too.

Client: GDBCommand and GCBCommand (bundle lines 122998, 122968), IsoUpdaterData.updateMultipleObjectInfos (bundle line 130259)

Unregister a callback with on_buildings_changed.remove(callback).

get_joined_area

get_joined_area() -> JoinedArea | None

The area joined, with its slum level and builder discount; None before any join.

client.castle.join joins one. Login joins none. A map read (gaa) moves the session off the castle but keeps this, as the client keeps its active area; join again before anything castle-scoped.

get_mines

get_mines() -> dict[int, MineStatus]

The joined castle's mines by object id, as the last gsm sent them.

next_collect_seconds counted from get_last_packet_time ("gsm"): the client counts it down from then. Empty before any gsm and after a map read (gaa); refreshed by client.castle.join and client.castle.collect_mine.

get_resource_carts

get_resource_carts() -> list[ResourceCart]

The joined castle's resource carts, wood, stone and food, as the last rci sent them.

remaining_seconds counts from get_last_packet_time ("rci"). Empty before any rci; a map read keeps them, as the client does; refreshed by client.castle.join and client.castle.collect_resource_cart.

get_resource_cart

get_resource_cart(cart_type: ResourceCartType) -> ResourceCart | None

The cart of one resource in the last rci; None before any.

The client looks a cart up by its place in RC; this matches each cart's own RT, so an entry that could not be read moves no other cart.

Client: CastleResourceCartsData.getResourceCartData (bundle line 28802)

state.player

Local player tracking: gpi/gxp/gcu/vip/gho/uap, alliance (gal), special currencies (sce) and spies.

Classes:

Name Description
PlayerState

Functions:

Name Description
xp_for_level

Total XP at which level starts. Client: PlayerConst.getXPFromLevel.

legend_level_for_xp

Legend level reached with xp total XP. Client: PlayerConst.getLegendLevelFromXP.

xp_for_legend_level

Total XP at which legend_level starts. Client: PlayerConst.getXPFromLegendLevel.

level_progress

Legend level and the XP at which the current and next level start.

Attributes:

Name Type Description
logger
LEVEL_CAP
LEGEND_LEVEL_CAP

logger module-attribute

logger = logging.getLogger(__name__)

LEVEL_CAP module-attribute

LEVEL_CAP = 70

LEGEND_LEVEL_CAP module-attribute

LEGEND_LEVEL_CAP = 950

PlayerState

PlayerState()

Bases: StateBase

Methods:

Name Description
get_local_player

Get a snapshot of the local player, or None before login.

get_special_currencies

Get a snapshot of the special currencies (Currency -> amount).

get_max_spies

The spies you own before boosts, from gms; None until the login gbd brings it.

get_player_last_updated

When any local-player field was last refreshed, or None.

get_local_player

get_local_player() -> Player | None

Get a snapshot of the local player, or None before login.

Returns a copy taken under the lock, with detached special_currencies, castles and beginner_protection containers, so several fields can be read consistently while the receive thread is updating state. state.local_player remains available for direct access but is a live object.

The Castle objects inside the snapshot are the live ones, as with get_castles().

get_special_currencies

get_special_currencies() -> dict[Currency | str, int]

Get a snapshot of the special currencies (Currency -> amount).

These are the generic currencies from sce (Currency.FAST_TRAVEL_FEATHERS, Currency.SKIP_1_MINUTE, ...), not items; a key newer than the generated enum is kept as its str. Empty before login, or if no sce packet has arrived yet — use get_last_packet_time("sce") to tell those apart from "empty".

get_max_spies

get_max_spies() -> MaxSpiesResponse | None

The spies you own before boosts, from gms; None until the login gbd brings it.

The model is replaced, never edited, on each gms, so the one returned stays as it was. client.spy.total_spies() adds the boosts, read from state.

get_player_last_updated

get_player_last_updated() -> float | None

When any local-player field was last refreshed, or None.

Player fields arrive in the login gbd and again as a push whenever the server changes them (gpi, gxp, gcu, vip, gal, gcl, gho, uap, glu, mir, sce). A stamp far in the past means no push has arrived since, not that the values were re-confirmed.

xp_for_level

xp_for_level(level: int) -> int

Total XP at which level starts. Client: PlayerConst.getXPFromLevel.

legend_level_for_xp

legend_level_for_xp(xp: int) -> int

Legend level reached with xp total XP. Client: PlayerConst.getLegendLevelFromXP.

xp_for_legend_level

xp_for_legend_level(legend_level: int) -> int

Total XP at which legend_level starts. Client: PlayerConst.getXPFromLegendLevel.

level_progress

level_progress(level: int, xp: int) -> tuple[int, int, int]

Legend level and the XP at which the current and next level start.

Client: CastleUserData.parse_GXP, which ignores the LL, XPFCL and XPTNL the server sends alongside and computes them from LVL and XP.

state.events

The running events: sei and tei entries, see and tee ends, pep scores, cqs campaigns, and their callbacks.

Classes:

Name Description
EventState

Attributes:

Name Type Description
logger
EventCallback
EventsCallback

logger module-attribute

logger = logging.getLogger(__name__)

EventCallback module-attribute

EventCallback = Callable[[SpecialEvent], Any]

EventsCallback module-attribute

EventsCallback = Callable[[dict[int, SpecialEvent]], Any]

EventState

EventState()

Bases: StateBase

Methods:

Name Description
on_event_added

Register a callback for an event that starts: a sei or tei entry for an event not running.

on_event_removed

Register a callback for an event that ends: a see or tee, or its time running out.

on_events_updated

Register a callback for every sei, non-empty tei, pep and cqs applied.

get_events

The running events by id, in the order they started; a copy.

get_event

A running event, or None when it is not running.

is_event_active

Whether an event runs and has not ended.

get_events_last_updated

When a sei, tei, see, tee or pep was last applied, wall clock; None before any.

on_event_added

on_event_added(callback: Callable[[SpecialEvent], object]) -> None

Register a callback for an event that starts: a sei or tei entry for an event not running.

Called with the event as that entry left it. Runs on the callback thread, in packet order (see GameState).

Client: CastleSpecialEventEvent.ADD_SPECIALEVENT from parseServerEventData (bundle line 139814)

Unregister a callback with on_event_added.remove(callback).

on_event_removed

on_event_removed(callback: Callable[[SpecialEvent], object]) -> None

Register a callback for an event that ends: a see or tee, or its time running out.

Called with the event as it last stood. A disconnect (see reset) fires none.

Client: CastleSpecialEventEvent.REMOVE_SPECIALEVENT from removeEventById (bundle line 139840), reached from parse_SEE, parseTEE and executeUpdateForEvents (bundle lines 139826, 139915, 139836)

Unregister a callback with on_event_removed.remove(callback).

on_events_updated

on_events_updated(callback: Callable[[dict[int, SpecialEvent]], object]) -> None

Register a callback for every sei, non-empty tei, pep and cqs applied.

Called with every running event (a copy of get_events), after the on_event_added and on_event_removed callbacks of the same packet.

Client: onEventDataParsed (bundle line 139818) sends REFRESH_SPECIALEVENT for every event and SERVER_DATA_PARSED; AScoreEventVO.setRankAndPoints (bundle line 15044) sends UPDATE_POINTS for a pep; CQSCommand.exec (bundle line 126495) sends CAMPAIGN_UPDATED for a cqs

Unregister a callback with on_events_updated.remove(callback).

get_events

get_events() -> dict[int, SpecialEvent]

The running events by id, in the order they started; a copy.

Each is the model its type has (EVENT_CLASSES), or a plain SpecialEvent. The models never change: a later packet replaces them.

get_event

get_event(event: Literal[FACTION]) -> BerimondEvent | None
get_event(event: Literal[POINT_EVENT]) -> PointEvent | None
get_event(event: Literal[BEGGING_KNIGHTS]) -> BeggingKnightsEvent | None
get_event(event: Literal[LONG_TERM_POINT_EVENT]) -> LongTermPointEvent | None
get_event(event: Literal[ALLIANCE_ALIEN_INVASION, RED_ALLIANCE_ALIEN_INVASION]) -> AlienInvasionEvent | None
get_event(event: Literal[ALLIANCE_NOMAD_INVASION]) -> NomadInvasionEvent | None
get_event(event: Literal[SAMURAI_INVASION]) -> SamuraiInvasionEvent | None
get_event(event: Literal[FACTION_INVASION]) -> FactionInvasionEvent | None
get_event(event: Literal[ALLIANCE_RAIDBOSS_EVENT]) -> RaidBossEvent | None
get_event(event: Literal[TEMP_SERVER]) -> TempServerEvent | None
get_event(event: Literal[DONATION_EVENT]) -> DonationEvent | None
get_event(event: Literal[GACHA_DECO2X2, CHRISTMAS_GACHA, EASTER_GACHA, SUMMER_GACHA, ANNIVERSARY_GACHA, HALLOWEEN_GACHA, BLACK_FRIDAY_GACHA, CARNIVAL_GACHA]) -> GachaEvent | None
get_event(event: Literal[SEASON_LEAGUE]) -> KingdomsLeagueEvent | None
get_event(event: Literal[GLOBAL_EFFECT]) -> GlobalEffectEvent | None
get_event(event: Literal[GLOBAL_EFFECT_BUFF]) -> GlobalEffectBuffEvent | None
get_event(event: Literal[ALLI_TOURNAMENT]) -> AllianceTournamentEvent | None
get_event(event: Literal[ALLIANCE_MOBILIZATION_EVENT]) -> AllianceMobilizationEvent | None
get_event(event: Literal[LUCKY_WHEEL, SALE_DAYS_LUCKY_WHEEL]) -> LuckyWheelEvent | None
get_event(event: Literal[ARTIFACT_19, ARTIFACT_23, ARTIFACT_29, ARTIFACT_30, ARTIFACT_67]) -> ArtifactEvent | None
get_event(event: Literal[THORNKING, SEAQUEEN, UNDERWORLD]) -> SeasonEvent | None
get_event(event: Literal[FAMEBOOSTER]) -> FameBoosterEvent | None
get_event(event: Literal[PRIME_ALLI_BONUS, ALLI_PAYMENT_BONUS]) -> AllianceBonusEvent | None
get_event(event: Literal[RELIC_ENCHANTER_PRIME_SALE, SEASON_PASS_PRIME_SALE]) -> DiscountSaleEvent | None
get_event(event: Literal[SKIP_FOR_FREE]) -> SkipForFreeEvent | None
get_event(event: Literal[GGS_GIFT]) -> GiftEvent | None
get_event(event: Literal[FORTUNE_TELLER]) -> FortuneTellerEvent | None
get_event(event: Literal[TOURNAMENT]) -> TournamentEvent | None
get_event(event: Literal[TIME_LIMITED_CAMPAIGN_EVENT]) -> CampaignEvent | None
get_event(event: Literal[TIME_LIMITED_CAMPAIGN_QUEST_EVENT]) -> CampaignQuestEvent | None
get_event(event: Event | int) -> SpecialEvent | None
get_event(event: Event | int) -> SpecialEvent | None

A running event, or None when it is not running.

Typed as the model of its type (EVENT_CLASSES) when event names one, so get_event(Event.SAMURAI_INVASION) is a SamuraiInvasionEvent | None; any other event or a bare id is a SpecialEvent.

is_event_active

is_event_active(event: Event | int) -> bool

Whether an event runs and has not ended.

Client: CastleSpecialEventData.isEventActive (bundle line 139892); the client also waits for some events' graphics to load, which the library has none of.

get_events_last_updated

get_events_last_updated() -> float | None

When a sei, tei, see, tee or pep was last applied, wall clock; None before any.

state.commanders

The player's commanders and castellans (gli) and legend and sceat skills (skl).

Classes:

Name Description
CommanderState

Attributes:

Name Type Description
logger

logger module-attribute

logger = logging.getLogger(__name__)

CommanderState

CommanderState()

Bases: StateBase

Methods:

Name Description
get_commanders

Your commanders and castellans as the last gli listed them; None until one arrived.

get_skills

Your legend and sceat skills as the last skl listed them; None until one arrived.

get_commanders

get_commanders() -> CommanderRoster | None

Your commanders and castellans as the last gli listed them; None until one arrived.

The login gbd brings them, and so does every reply that carries the list (gli, arl, gla, seq, sdi, sti and the attack and conquer info replies). A copy: changing it changes nothing in state.

get_skills

get_skills() -> SkillList | None

Your legend and sceat skills as the last skl listed them; None until one arrived.

The login gbd brings them, and so do a skl reply or push and an ego push that carries one. A copy: changing it changes nothing in state.

state.alliance

Your own alliance's details (ain) and the alliance chat history (acl, acm).

Classes:

Name Description
AllianceState

Attributes:

Name Type Description
logger

logger module-attribute

logger = logging.getLogger(__name__)

AllianceState

AllianceState()

Bases: StateBase

Methods:

Name Description
get_own_alliance

Your alliance's details and members, from the last ain about it; None in no alliance or before one.

get_alliance_chat

The alliance chat history, oldest first; a copy of the list, of read-only messages.

get_own_alliance

get_own_alliance() -> AllianceInfo | None

Your alliance's details and members, from the last ain about it; None in no alliance or before one.

The login gbd brings it, and so do client.alliance.get_alliance_info(your_id) and the replies that carry it (acd, acn, ado, akm, arm, cal). An acm marks its sender online here. A copy: changing it changes nothing in state.

get_alliance_chat

get_alliance_chat() -> list[ChatMessageData]

The alliance chat history, oldest first; a copy of the list, of read-only messages.

The login gbd's acl starts it, an acl reply (client.alliance.get_chat_log()) replaces it, and every acm adds one. Leaving the alliance, a gal with no alliance and a disconnect start it over.

state.progress

The player's progress: research, boosts, might, points, titles, achievements, relocation and plague monks.

Classes:

Name Description
ProgressState

ProgressState

ProgressState()

Bases: StateBase

Methods:

Name Description
get_research

Your finished and running research, from the last rei; None until the login data brought one.

get_boosts

Your boosters, premium account, production slots and festival; None until a boi arrived.

get_might

Your might points, from the last gmu; None until one arrived.

get_glory_points

Your glory points, from the last ufa; None until one arrived.

get_faction_points

Your Berimond points, from the last ufp push; None until one arrived (the login data has none).

get_title_ranks

Your top-X ranks, Storm Islands title and displayed title systems, from the last uar; None before one.

get_achievements

Your achievement points, finished achievements and progress; None until a vli arrived.

get_relocation

Your castle relocations, from the last gri; None until one arrived.

get_plague_monks

Your plague monks, from the last cpi; None until one arrived.

get_research

get_research() -> ResearchInfoResponse | None

Your finished and running research, from the last rei; None until the login data brought one.

get_boosts

get_boosts() -> BoosterInfoResponse | None

Your boosters, premium account, production slots and festival; None until a boi arrived.

Every booster any boi listed is there, as the last one that listed it left it.

get_might

get_might() -> MightPointsResponse | None

Your might points, from the last gmu; None until one arrived.

get_glory_points

get_glory_points() -> GloryPointsResponse | None

Your glory points, from the last ufa; None until one arrived.

get_faction_points

get_faction_points() -> FactionPointsResponse | None

Your Berimond points, from the last ufp push; None until one arrived (the login data has none).

get_title_ranks

get_title_ranks() -> TitleRanksResponse | None

Your top-X ranks, Storm Islands title and displayed title systems, from the last uar; None before one.

get_achievements

get_achievements() -> AchievementsResponse | None

Your achievement points, finished achievements and progress; None until a vli arrived.

Finished achievements from every vli are there, and each achievement's progress as the last vli that listed it left it.

get_relocation

get_relocation() -> RelocationInfoResponse | None

Your castle relocations, from the last gri; None until one arrived.

get_plague_monks

get_plague_monks() -> PlagueMonkInfoResponse | None

Your plague monks, from the last cpi; None until one arrived.

state.account

Account sections: the daily reset, attack counter, officers' school, boosted global effects, gifts, wishing well and new relics.

Classes:

Name Description
AccountState

Attributes:

Name Type Description
ACCOUNT_SECTIONS

ACCOUNT_SECTIONS module-attribute

ACCOUNT_SECTIONS = ('drt', 'gai', 'gatp', 'bie', 'pgl', 'rww', 'nrf')

AccountState

AccountState()

Bases: StateBase

Methods:

Name Description
get_daily_reset

When the daily reset comes, from the login data's drt; None until it arrived.

get_attack_counter

Your attack counter, from the last gai; None until one arrived.

get_officer_training

The officers' school training program running, from the last gatp and the gtp since.

get_boosted_global_effects

The global effects the running booster event boosts, from the last bie; None until one arrived.

get_player_gifts

Your gift packages and how many you may still send, from the last pgl; None until one arrived.

get_wishing_well

The ruby wishing well, from the last rww; None until one arrived.

get_new_relics

Whether new relics wait to be seen, from the last nrf; None until one arrived.

get_daily_reset

get_daily_reset() -> DailyResetResponse | None

When the daily reset comes, from the login data's drt; None until it arrived.

get_attack_counter

get_attack_counter() -> AttackCounterResponse | None

Your attack counter, from the last gai; None until one arrived.

get_officer_training

get_officer_training() -> OfficerTraining | None

The officers' school training program running, from the last gatp and the gtp since.

None when none runs; get_last_packet_time("gatp") tells "none runs" from "none arrived yet".

get_boosted_global_effects

get_boosted_global_effects() -> BoostedGlobalEffectsResponse | None

The global effects the running booster event boosts, from the last bie; None until one arrived.

get_player_gifts

get_player_gifts() -> PlayerGiftsResponse | None

Your gift packages and how many you may still send, from the last pgl; None until one arrived.

get_wishing_well

get_wishing_well() -> WishingWellResponse | None

The ruby wishing well, from the last rww; None until one arrived.

get_new_relics

get_new_relics() -> NewRelicsResponse | None

Whether new relics wait to be seen, from the last nrf; None until one arrived.

state.inventory

Inventory and economy sections: gems, loot boxes, inventory space, kingdoms, mercenaries, tax, construction item expiry and the resource citizen.

Classes:

Name Description
InventoryState

Attributes:

Name Type Description
INVENTORY_SECTIONS

INVENTORY_SECTIONS module-attribute

INVENTORY_SECTIONS = ('ggm', 'gls', 'esl', 'kpi', 'mpe', 'txi', 'nec', 'irc')

InventoryState

InventoryState()

Bases: StateBase

Methods:

Name Description
get_gems

The gems and relic gems in your inventory; None until a ggm or gec arrived.

get_loot_boxes

Your loot boxes and key progress; None until a gls arrived.

get_inventory_space

The equipment and gem inventory space, from the last esl; None until one arrived.

get_kingdoms

Your kingdoms and the transfers to them; None until a kpi arrived.

get_mercenary_missions

The mercenary camp's missions, from the last mpe; None until one arrived.

get_tax

The tax collection, from the last txi; None until one arrived.

get_construction_item_expiry

When the next construction item expires, from the last nec; None until one arrived.

get_resource_pool

What the citizen in your castle carries, from the last irc push; None until one arrived.

get_gems

get_gems() -> GemInventoryResponse | None

The gems and relic gems in your inventory; None until a ggm or gec arrived.

A ggm lists them whole, and each gec push adds or takes some away.

get_loot_boxes

get_loot_boxes() -> LootBoxesResponse | None

Your loot boxes and key progress; None until a gls arrived.

The loot boxes are the last gls's, and each type's key progress as the last gls that listed it left it.

get_inventory_space

get_inventory_space() -> InventorySpace | None

The equipment and gem inventory space, from the last esl; None until one arrived.

get_kingdoms

get_kingdoms() -> KingdomInfoResponse | None

Your kingdoms and the transfers to them; None until a kpi arrived.

Every kingdom any kpi listed is there, as the last one that listed it left it; the transfers are the last kpi's.

get_mercenary_missions

get_mercenary_missions() -> MercenaryMissionsResponse | None

The mercenary camp's missions, from the last mpe; None until one arrived.

get_tax

get_tax() -> TaxInfo | None

The tax collection, from the last txi; None until one arrived.

Its remaining_seconds is as of that packet, get_last_packet_time("txi"). A copy: changing it changes nothing in state.

get_construction_item_expiry

get_construction_item_expiry() -> ConstructionItemExpiryResponse | None

When the next construction item expires, from the last nec; None until one arrived.

get_resource_pool

get_resource_pool() -> ResourcePoolResponse | None

What the citizen in your castle carries, from the last irc push; None until one arrived.

Joining a castle drops it, as in the client (JAACommand, bundle line 130194); the citizen being collected or leaving does not, so it can be stale until the next irc.

state.models

The state's models: the local player, castles and resources.

Classes:

Name Description
Resources

A castle's resources: stock, storage capacity, hourly production and plunder-safe amount.

Alliance

The local player's alliance membership, from the gal login section.

Castle

A castle, outpost or metropolis owned by the logged-in player.

Player

The logged-in player, as tracked by StateManager.

JoinedArea

The castle or area last joined (jaa, the reply to client.castle.join), and the

Attributes:

Name Type Description
CastleKey

A castle's (kingdom, object id): ids repeat across kingdoms, as the client's castle list is kept per kingdom.

CastleKey module-attribute

CastleKey = tuple[Kingdom, int]

A castle's (kingdom, object id): ids repeat across kingdoms, as the client's castle list is kept per kingdom.

Resources pydantic-model

Bases: EmpireModel

A castle's resources: stock, storage capacity, hourly production and plunder-safe amount.

Stock comes from the castle's dcl entry; capacity, production and the plunder-safe amount from its gpa production area.

Client: DetailedCastleVO.parseData

Fields:

wood pydantic-field

wood: int = 0

Wood in stock

stone pydantic-field

stone: int = 0

Stone in stock

food pydantic-field

food: int = 0

Food in stock

coal pydantic-field

coal: int = 0

Coal in stock

oil pydantic-field

oil: int = 0

Oil in stock

glass pydantic-field

glass: int = 0

Glass in stock

iron pydantic-field

iron: int = 0

Iron in stock

aquamarine pydantic-field

aquamarine: int = 0

Aquamarine in stock

honey pydantic-field

honey: int = 0

Honey in stock

mead pydantic-field

mead: int = 0

Mead in stock

beef pydantic-field

beef: int = 0

Beef in stock

capacity pydantic-field

capacity: StorageCapacity

Storage capacity per resource

production pydantic-field

production: ResourceProduction

Production per hour, per resource

safe pydantic-field

safe: SafeAmount

Amount per resource safe from plunder

wood_cap property

wood_cap: int | float

stone_cap property

stone_cap: int | float

food_cap property

food_cap: int | float

wood_rate property

wood_rate: float

stone_rate property

stone_rate: float

food_rate property

food_rate: float

wood_safe property

wood_safe: float

stone_safe property

stone_safe: float

food_safe property

food_safe: float

Alliance pydantic-model

Bases: EmpireModel

The local player's alliance membership, from the gal login section.

Client: CastleUserData.parse_GAL.

Fields:

Validators:

model_config class-attribute

model_config: ConfigDict = ConfigDict(extra='ignore', populate_by_name=True)

id pydantic-field

id: int = -1

Wire key AID

Alliance id; 0 or less means no alliance

name pydantic-field

name: str = ''

Read from AliasChoices('AN', 'N', 'name')

Alliance name

rank pydantic-field

rank: int = 0

Wire key R

The player's rank in the alliance

current_fame pydantic-field

current_fame: int = 0

Wire key ACF

The alliance's current fame

is_searching pydantic-field

is_searching: bool = False

Wire key SA

The player is looking for an alliance

Castle pydantic-model

Bases: EmpireModel

A castle, outpost or metropolis owned by the logged-in player.

Name, position, kingdom and the open-gate counter come from the castle list (gcl); everything else from the castle's dcl entry, kept whole as details and None until one has been received. units also follows each rue push (units received), which details does not; a kik push on a Monday sets every open_gate_counter to 0.

Client: CastleListVO.parseCastleList (bundle line 13698), DetailedCastleVO.parseData, CastleUserCastleListDetailed.parse_rue (bundle line 140918), CastleListVO.resetOpenGateCounter (bundle line 13728)

Not to be confused with empire_core.castle.models.castles.CastleInfo, which is the parsed protocol model for another player's castle.

Fields:

model_config class-attribute

model_config: ConfigDict = ConfigDict(extra='ignore', populate_by_name=True)

id pydantic-field

id: int = -1

Wire key OID

Castle (area) id

name pydantic-field

name: str = 'Unknown'

Wire key N

Castle name

kingdom_id pydantic-field

kingdom_id: Kingdom = Kingdom.GREEN

Wire key KID

The kingdom the castle is in

x pydantic-field

x: int = 0

Wire key X

Map x

y pydantic-field

y: int = 0

Wire key Y

Map y

resources pydantic-field

resources: Resources

The castle's resources

units pydantic-field

units: WodAmounts

Units and tools stationed here

open_gate_counter pydantic-field

open_gate_counter: int = 0

Wire key OGC

How often the gate has been opened since the last Monday reset

details pydantic-field

details: DetailedCastleInfo | None = None

The castle's latest details; None until they arrive

raw_data pydantic-field

raw_data: dict[str, Any]

population property

population: int

gpa P.

neutral_deco_points property

neutral_deco_points: int

gpa NDP.

defence property

defence: int

dcl D.

market_carriages property

market_carriages: int

dcl MC: the castle's total market carriages.

has_barracks property

has_barracks: bool

has_siege_workshop property

has_siege_workshop: bool

has_defense_workshop property

has_defense_workshop: bool

has_hospital property

has_hospital: bool

stronghold_units property

stronghold_units: dict[int, int]

Units in the stronghold, dcl SHI.

from_game_data classmethod

from_game_data(data: dict[str, Any]) -> Castle

Player pydantic-model

Bases: EmpireModel

The logged-in player, as tracked by StateManager.

Some fields are still stored under their raw wire keys (PID, PN, ...), each with a snake_case read-only property (id, name, ...); the rest are snake_case fields aliased to their wire keys. Prefer the snake_case names.

Client: CastleUserData (parse_GPI, parse_GXP, parse_GHO, parse_UAP, parse_GAL), CurrencyData.parseGCU / parseSCE and CastleVIPData.parse_VIP.

Fields:

model_config class-attribute

model_config: ConfigDict = ConfigDict(extra='ignore', populate_by_name=True)

PID pydantic-field

PID: int = -1

PN pydantic-field

PN: str = 'Unknown'

AID pydantic-field

AID: int | None = None

level pydantic-field

level: int = 0

Wire key LVL

Level; 70 is the cap before legend levels

xp pydantic-field

xp: int = 0

Wire key XP

Total XP

legendary_level pydantic-field

legendary_level: int = 0

Wire key LL

Legend level, computed from XP once level reaches 70, else 0

xp_for_current_level pydantic-field

xp_for_current_level: int = 0

Wire key XPFCL

Total XP at which the current level (or legend level) starts, computed from level and XP

xp_to_next_level pydantic-field

xp_to_next_level: int = 0

Wire key XPTNL

Total XP at which the next level (or legend level) starts, computed from level and XP

coins pydantic-field

coins: int = 0

Coins

rubies pydantic-field

rubies: int = 0

Rubies

special_currencies pydantic-field

special_currencies: dict[EnumOrStr[Currency], int]

Special currency -> amount (Currency.FAST_TRAVEL_FEATHERS, Currency.SKIP_1_MINUTE, ...), generic currencies in the item data, not items; a key newer than the generated enum stays a str

vip_points pydantic-field

vip_points: int = 0

Wire key VP

VIP points

vip_level pydantic-field

vip_level: int = 0

Wire key VRL

The highest VIP level reached

vip_time_left pydantic-field

vip_time_left: int = 0

Wire key VRS

Seconds of VIP time left when last read

used_premium_commanders pydantic-field

used_premium_commanders: int = 0

Wire key UPG

Premium commanders used today from the VIP level's free ones

alliance pydantic-field

alliance: Alliance | None = None

honor pydantic-field

honor: int = 0

Wire key H

Honor

ranking pydantic-field

ranking: int = 0

Wire key RP

Ranking points

beginner_protection pydantic-field

beginner_protection: dict[Kingdom, bool]

Whether the player is under beginner protection, by kingdom

PF pydantic-field

PF: int = 0

Premium flag, 1 when set; see premium_flag. Filled only from a gpi carrying PF, where the client does not read it (it reads PF from gpf)

VF pydantic-field

VF: int = 0

VIP flag, 1 when set; see vip_flag. Filled only from a gpi carrying VF, where the client does not read it (it reads VF only on map owner info)

id property

id: int

name property

name: str

alliance_id property

alliance_id: int | None

premium_flag property

premium_flag: int

vip_flag property

vip_flag: int

xp_progress property

xp_progress: float

How far through the current level the player is, as a percentage (0-100).

0 when the level has no XP range: before any gxp, or at the legend level cap. Clamped, because the client puts XP just past level 70 in legend level 1, whose range starts 250 XP later.

castles pydantic-field

castles: dict[CastleKey, Castle]

The player's castles by (kingdom, castle id)

E pydantic-field

E: str | None = None

email property

email: str | None

is_premium property

is_premium: bool

Check if user has active VIP time.

JoinedArea pydantic-model

Bases: EmpireModel

The castle or area last joined (jaa, the reply to client.castle.join), and the slum level and builder discount it was sent with or pushed since (csl, gab).

Client: JAACommand.executeCommand (bundle line 130190), AreaFactory.parseAreaInfo (bundle line 130226), AreaDataUpdater.parseJAA (bundle line 131496), AreaDataSlum (bundle line 131303), AreaDataCommonInfo.parseGAB (bundle line 130992)

Fields:

model_config class-attribute

model_config: ConfigDict = ConfigDict(frozen=True)

kingdom_id pydantic-field

kingdom_id: Kingdom | None

The area's kingdom; None for one Kingdom lacks

castle_id pydantic-field

castle_id: int | None = None

The area's object id; None when its map row is not one of a castle list's

slum_level pydantic-field

slum_level: int = -1

The slum level, -1 for none

builder_discount pydantic-field

builder_discount: int | float = 0

The builder discount