Player¶
Player profiles, search, research and the mercenary camp behind client.player.
player.service¶
Other players' details, player search, your research and the mercenary camp.
Classes:
| Name | Description |
|---|---|
PlayerDetailsBulkResult |
What |
PlayerService |
Other players' details, finding a player by name, your research and the mercenary camp's missions. |
PlayerDetailsBulkResult
dataclass
¶
PlayerDetailsBulkResult(found: dict[int, GetPlayerInfoResponse] = dict(), failed: dict[int, CommandError | PacketError] = dict(), timed_out: list[int] = list())
What PlayerService.get_player_details_bulk got for each player.
Attributes:
| Name | Type | Description |
|---|---|---|
found |
dict[int, GetPlayerInfoResponse]
|
The replies, by player id. |
failed |
dict[int, CommandError | PacketError]
|
Players whose request the server refused, or whose reply could not be read. |
timed_out |
list[int]
|
Players whose reply did not come in time. |
complete |
bool
|
Whether every player was found. |
found
class-attribute
instance-attribute
¶
found: dict[int, GetPlayerInfoResponse] = field(default_factory=dict)
The replies, by player id.
failed
class-attribute
instance-attribute
¶
failed: dict[int, CommandError | PacketError] = field(default_factory=dict)
Players whose request the server refused, or whose reply could not be read.
timed_out
class-attribute
instance-attribute
¶
Players whose reply did not come in time.
PlayerService
¶
Bases: BaseService
Other players' details, finding a player by name, your research and the mercenary camp's missions.
Reached as client.player.
Methods:
| Name | Description |
|---|---|
get_player_info |
Get detailed player information (gdi), including castle list with |
get_player_details |
Alias for |
get_player_details_bulk |
Get detailed info for several players, one gdi request after another. |
search_player_by_name |
|
start_research |
Start a research, paying what it costs. |
skip_research |
Shorten the running research with a minute skip from your inventory. |
list_missions |
The mercenary camp's missions, and when new ones come. |
start_mission |
Start an open mercenary mission, paying its price in coins. |
collect_mission |
Collect a finished mercenary mission's rewards. |
get_player_info
¶
get_player_info(player_id: int, timeout: float = 5.0) -> GetPlayerInfoResponse
Get detailed player information (gdi), including castle list with capture info.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
player_id
|
int
|
A player's id: |
required |
timeout
|
float
|
Timeout in seconds |
5.0
|
Raises:
| Type | Description |
|---|---|
CommandError / EmpireTimeoutError / ConnectionClosedError
|
see |
get_player_details
¶
get_player_details(player_id: int, timeout: float = 5.0) -> GetPlayerInfoResponse
Alias for get_player_info (both use the 'gdi' command).
get_player_details_bulk
¶
get_player_details_bulk(player_ids: list[int], timeout: float = 5.0, send_delay: float = 0.0) -> PlayerDetailsBulkResult
Get detailed info for several players, one gdi request after another.
Each request waits for its own reply before the next goes out: a gdi
error reply names no player, so only one request in flight at a time
tells which player it belongs to. Each runs as get_player_info
does, under the gdi command lock and taking only a reply whose
O.OID is the player asked for.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
player_ids
|
list[int]
|
Player ids to fetch, found as for |
required |
timeout
|
float
|
Seconds to wait for each reply |
5.0
|
send_delay
|
float
|
Seconds to wait between two requests; by default none, as each already waits for the one before it to be answered |
0.0
|
Returns:
| Type | Description |
|---|---|
PlayerDetailsBulkResult
|
Which players were found, which failed and why, and which timed out |
Raises:
| Type | Description |
|---|---|
ConnectionClosedError / NetworkError
|
the connection failed; the players fetched so far are lost |
ReceiveThreadError
|
called on the receive thread |
search_player_by_name
¶
search_player_by_name(player_name: str, timeout: float = 5.0) -> SearchPlayerResponse
start_research
¶
start_research(research: Research | int, *, spend_rubies: bool = False, timeout: float = 5.0) -> bool
Start a research, paying what it costs.
Missing resources are never paid with rubies: the request sends PWR 0, as the research
dialog does. Some researches cost rubies themselves (ResearchDef.cost_rubies), and the
client sends the same request for them; this method refuses those unless spend_rubies is
True. Other costs, legendary tokens included, are a normal game currency and are paid. The
reply's research reaches client.state.get_research().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
research
|
Research | int
|
The research to start |
required |
spend_rubies
|
bool
|
Allow a research that costs rubies, on either kind of server |
False
|
timeout
|
float
|
Timeout in seconds |
5.0
|
Raises:
| Type | Description |
|---|---|
GameDataNotLoadedError
|
the game data cannot be loaded |
ValueError
|
The game data has no such research, or it costs rubies and |
Client: ResearchInfo.buyResearch (bundle line 79704) sends C2SResearchStartVO whatever the
costs; updateBuyArea shows the ruby overlay when getFinalCosts holds rubies (bundle lines
79679-79680), and AResearchVO.getBaseCosts takes the temporary server's costs there (bundle
line 61530); RESCommand (bundle line 126872)
skip_research
¶
Shorten the running research with a minute skip from your inventory.
The reply's research reaches client.state.get_research().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
minute_skip
|
Currency | str
|
The minute skip to use, |
required |
timeout
|
float
|
Timeout in seconds |
5.0
|
Raises:
| Type | Description |
|---|---|
ValueError
|
|
Client: ResearchMinuteSkipProperties.getMinuteSkipCommand (bundle line 79784),
CastleMinuteSkipDialog.showLoaded (bundle line 7671), MSRCommand (bundle line 125825)
list_missions
¶
list_missions(timeout: float = 5.0) -> MercenaryMissionsResponse
The mercenary camp's missions, and when new ones come.
Sends mpe with MID -1, as the client does when it opens the camp and when new missions
are due; the reply also reaches client.state.get_mercenary_missions().
Client: CastleMercenaryOverviewDialog.showLoaded and CastleMercenaryData.onNewMissions
(bundle lines 51996, 28851)
start_mission
¶
Start an open mercenary mission, paying its price in coins.
Lists the missions first and decides from that reply, as the client decides from its mission state: only an open mission starts, and only while no mission runs or waits to be collected. While one runs the client would first finish it for rubies; that is left out.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mission_id
|
int
|
A |
required |
timeout
|
float
|
Timeout in seconds, for each request |
5.0
|
Raises:
| Type | Description |
|---|---|
ValueError
|
The missions list no such mission, it is not open, or another mission runs or waits to be collected |
Client: CastleMercenaryMissionItem.startMission, showStartAndRefreshButton and
startMissionCallback (bundle lines 87377, 87361, 87379)
collect_mission
¶
Collect a finished mercenary mission's rewards.
The collect is the same mpe as the ruby skip of a running mission, which costs
ceil(remaining seconds / 60 * 5) rubies. So this lists the missions first and sends the
collect only for one the server lists as collectable, as started with no RD (finished for
the client, and a skip of it would cost 0 rubies), or as started with its time run out at
least two seconds ago, since RD is rounded. A started mission whose RD still counts
down is refused. Another session can still change the mission between the list and the
send; that window is not closed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mission_id
|
int
|
A |
required |
timeout
|
float
|
Timeout in seconds, for each request |
5.0
|
Raises:
| Type | Description |
|---|---|
ValueError
|
The missions list no such mission, or it is not collectable by that rule |
Client: CastleMercenaryMissionItem.showCollectButton and collectMissionRewards
(bundle lines 87365, 87382); CastleMercenaryMissionItemVO.fillFromParamObject sets no end
time without RD (bundle line 81134); CastleMercenarySkipMissionDialog.confirmSkip (bundle
line 87459) sends the same id, MercenaryConst.getSkipC2Cost (dll line 19508)
player.models.account¶
Account sections of the login data, and the pushes and replies that bring them again.
Commands:
- drt: seconds until the daily reset
- gatp: the officers' school training program running now
- bie: the global effects a booster event boosts
- pgl: your gift packages and how many you may still send
The timed models count from received_at, as the client counts from its timer.
Classes:
| Name | Description |
|---|---|
DailyResetResponse |
When the daily reset comes. |
OfficerBonus |
The bonus a training program gives: an effect and its values, |
OfficerTraining |
The officers' school training program running now. |
BoostedGlobalEffectsResponse |
The global effects the running global effect booster event makes stronger. |
PlayerGift |
One gift package you hold: |
PlayerGiftsResponse |
Your gift packages, to send to other players, and how many you may still send. |
DailyResetResponse
pydantic-model
¶
Bases: TimedResponse
When the daily reset comes.
Command: drt, as a login section of gbd only.
Client: CastleTimerData.parse_DRT and timeTillDailyResetInSec (bundle lines 112810-112811)
Fields:
seconds
pydantic-field
¶
seconds: ClientNumber = 0
Wire key STR
Seconds until the daily reset when the values were read
OfficerBonus
pydantic-model
¶
Bases: BasePayload
The bonus a training program gives: an effect and its values, [effect_id, [value, ...]].
Client: OfficersSchoolEffectVO.setBonusVOByValueArray (bundle line 144678), which looks the
effect up by id and reads the values with BonusVO.parseFromValueArray
Fields:
Validators:
-
_from_row
OfficerTraining
pydantic-model
¶
Bases: TimedPayload
The officers' school training program running now.
Not a response model: its E is the program's bonus, not an error code.
Command: gatp, as a login section of gbd and a reply; a gtp updates it.
Client: GATPCommand (bundle line 126126), OfficersSchoolData.parse_GATP (bundle line 144606),
OfficersSchoolEffectVO.setBonusVOByValueArray and setActiveTime (bundle lines 144678, 144681),
OfficersSchoolData.getBonusByEffectType (bundle line 144634)
Fields:
-
received_at(float) -
slot_id(ClientInt) -
bonus(OfficerBonus | None) -
seconds(ClientNumber)
Validators:
-
_bonus→bonus
bonus
pydantic-field
¶
bonus: OfficerBonus | None = None
Wire key E
The program's bonus; None for none
seconds
pydantic-field
¶
seconds: ClientNumber = -1
Wire key RS
Seconds the program runs when the values were read, -1 for none
BoostedGlobalEffectsResponse
pydantic-model
¶
Bases: BaseResponse
The global effects the running global effect booster event makes stronger.
The running global effects themselves come with the global effects event, not here.
Command: bie, as a login section of gbd and a push.
Client: BIECommand (bundle line 122579), GlobalEffectData.parse_GIE (bundle line 143676), which
skips a block without GE, and isEffectBoosted (bundle line 143643)
Fields:
-
error_code(int) -
global_effect_ids(tuple[EnumOrInt['GlobalEffect'], ...])
Validators:
-
_listed→global_effect_ids
PlayerGift
pydantic-model
¶
Bases: BasePayload
One gift package you hold: [package_id, amount].
Client: PlayerGiftVO.parseFromArray (bundle line 52178)
Fields:
Validators:
-
_from_row
PlayerGiftsResponse
pydantic-model
¶
Bases: BaseResponse
Your gift packages, to send to other players, and how many you may still send.
Command: pgl, as a login section of gbd and a reply.
Client: PGLCommand (bundle line 127961), PlayerGiftData.parse_PGL (bundle line 113571), which skips a
block without G; sendablePackageAmount (bundle line 113613), read at bundle line 44174
Fields:
-
error_code(int) -
gifts(tuple[PlayerGift, ...]) -
sendable_amount(ClientNumber)
Validators:
-
_gifts→gifts
sendable_amount
pydantic-field
¶
sendable_amount: ClientNumber = 0
Wire key RA
Gift packages you may still send; none at 0 or less
player.models.economy¶
Loot boxes and mercenary missions, as the login data and their pushes send them.
Commands:
- gls: your loot boxes and key progress, a push before and a section of
gbd - mpe: the mercenary missions, a login section of
gbdand a reply; also the request that lists, starts and collects them
Classes:
| Name | Description |
|---|---|
LootBoxAmount |
One kind of loot box you hold: |
LootBoxKeys |
The keys collected towards one loot box type: |
LootBoxesResponse |
Your loot boxes, and the keys collected towards each loot box type. |
MercenaryMission |
One mercenary mission, an entry of an |
MercenaryMissionsResponse |
The mercenary camp's missions and when new ones come. |
MercenaryPackageRequest |
List the mercenary missions, or start or collect one. |
NEXT_MISSIONS_DELAY
module-attribute
¶
CastleMercenaryData.NEXT_MISSIONS_TIMER_DELAY (bundle line 28881): seconds the client waits past NM
LootBoxAmount
pydantic-model
¶
Bases: BasePayload
One kind of loot box you hold: {"ID": loot box, "AMT": amount}.
Client: CastleLootboxData.parse_GLS (bundle line 112332) reads each as an ACollectableItemLootBoxVO
Fields:
-
loot_box_id(EnumOrInt['LootBox']) -
amount(ClientNumber)
LootBoxKeys
pydantic-model
¶
Bases: BasePayload
The keys collected towards one loot box type: {"ID": loot box type, "AMT": keys}.
Client: CastleLootboxData.parse_GLS and setKeyProgress (bundle lines 112332, 112343)
Fields:
-
loot_box_type_id(EnumOrInt['LootBoxType']) -
keys(ClientNumber)
LootBoxesResponse
pydantic-model
¶
Bases: BaseResponse
Your loot boxes, and the keys collected towards each loot box type.
A gls lists the loot boxes whole, but key progress only for some types; the client keeps the
others as they were, and client.state.get_loot_boxes() does the same.
Command: gls, a push the server sends before gbd and again when they change.
Client: GLSCommand (bundle line 124800), CastleLootboxData.parse_GLS (bundle line 112332)
Fields:
-
error_code(int) -
loot_boxes(tuple[LootBoxAmount, ...]) -
key_progress(tuple[LootBoxKeys, ...])
Validators:
-
_amounts→loot_boxes -
_keys→key_progress
loot_boxes
pydantic-field
¶
loot_boxes: tuple[LootBoxAmount, ...] = ()
Wire key ALL
The loot boxes you hold
key_progress
pydantic-field
¶
key_progress: tuple[LootBoxKeys, ...] = ()
Wire key KEY
Keys collected per loot box type this packet lists
keys
¶
keys(loot_box_type_id: LootBoxType | int) -> int | float
The keys collected towards a loot box type, 0 when none is known.
MercenaryMission
pydantic-model
¶
Bases: TimedPayload
One mercenary mission, an entry of an mpe's M.
Client: CastleMercenaryMissionItemVO.fillFromParamObject and remainingTime (bundle lines 81134-81135)
Fields:
-
received_at(float) -
mission_id(ClientInt) -
rewards(CollectableRows) -
duration_seconds(ClientNumber) -
price(ClientNumber) -
quality(EnumOrInt[MercenaryMissionRarity]) -
state(EnumOrInt[MercenaryMissionState]) -
seconds(ClientNumber)
duration_seconds
pydantic-field
¶
duration_seconds: ClientNumber = 0
Wire key D
How long the mission takes
quality
pydantic-field
¶
quality: EnumOrInt[MercenaryMissionRarity] = MercenaryMissionRarity.FREE
Wire key Q
The mission's rarity
state
pydantic-field
¶
state: EnumOrInt[MercenaryMissionState] = MercenaryMissionState.OPEN
Wire key S
Where the mission stood when the values were read
seconds
pydantic-field
¶
seconds: ClientNumber = 0
Wire key RD
Seconds the running mission still takes when the values were read
remaining_seconds
¶
Seconds the mission still takes, 0 at least.
seconds_past_end
¶
Seconds since the mission's time ran out, below 0 while it still runs.
Client: CastleMercenaryMissionItemVO.remainingTime (bundle line 81135), negated and not clamped
current_state
¶
current_state(now: float | None = None) -> MercenaryMissionState | int
Where the mission stands now: a started mission whose time has run out is collectable.
Client: CastleMercenaryData.parse_MPE and onMissionFinished (bundle lines 28829-28830, 28850)
MercenaryMissionsResponse
pydantic-model
¶
Bases: TimedResponse
The mercenary camp's missions and when new ones come.
Command: mpe, as a login section of gbd and a reply.
Client: MPECommand (bundle line 125109), CastleMercenaryData.parse_MPE (bundle line 28824),
which drops collected missions (state 3 and up)
Fields:
-
received_at(float) -
error_code(int) -
next_missions_seconds(ClientNumber) -
missions(tuple[MercenaryMission, ...])
Validators:
-
_missions→missions
next_missions_seconds
pydantic-field
¶
next_missions_seconds: ClientNumber = 0
Wire key NM
Seconds until new missions come when the values were read
missions
pydantic-field
¶
missions: tuple[MercenaryMission, ...] = ()
Wire key M
The missions not collected
remaining_next_missions_seconds
¶
Seconds until the client asks for new missions, 0 at least.
get_mission
¶
get_mission(mission_id: int) -> MercenaryMission | None
The mission with this id; None when the reply does not list it.
current_mission_state
¶
current_mission_state(now: float | None = None) -> MercenaryMissionState | int
The furthest any mission stands now: OPEN while none runs or waits to be collected.
Client: CastleMercenaryData.currentMissionState (bundle line 28853), the highest state
parse_MPE read (bundle line 28829), collectable once onMissionFinished fired (bundle line 28850)
MercenaryPackageRequest
pydantic-model
¶
Bases: BaseRequest
List the mercenary missions, or start or collect one.
Command: mpe Payload: {"MID": mission_id}
MID -1 lists the missions. A mission's id starts an open mission, collects a collectable one, and
finishes a running one at once for rubies (CastleMercenarySkipMissionDialog.confirmSkip, bundle
line 87459); client.player.start_mission and collect_mission send it only for the first two.
Client: C2SMercenaryPackageVO (bundle line 28889); -1 from CastleMercenaryData.onNewMissions
and CastleMercenaryOverviewDialog.showLoaded (bundle lines 28851, 51996), a mission's id from
CastleMercenaryMissionItem.startMissionCallback and collectMissionRewards (bundle lines 87379, 87382)
Fields:
player.models.info¶
Player protocol models.
Commands:
- gdi: Get detailed player info (including castle list with capture info)
- wsp: World search player (search by name)
Classes:
| Name | Description |
|---|---|
LocationCapture |
One of the player's locations that someone occupies, from the gdi castle list. |
PlayerOwnerInfo |
The gdi reply's |
GetPlayerInfoRequest |
Get detailed player information including castle list. |
GetPlayerInfoResponse |
Response containing detailed player information. |
SearchPlayerRequest |
Find a player by name. |
SearchPlayerResponse |
Response to a player search. |
LocationCapture
pydantic-model
¶
Bases: BasePayload
One of the player's locations that someone occupies, from the gdi castle list.
Built from a CastleInfo that is occupied: its occupier_id is above -1, as the
client's isOccupied checks.
Client: InteractiveMapobjectVO.parseAreaInfo (bundle line 3631) and
CapitalMapobjectVO.parseAreaInfo (bundle line 18729) read the occupier
as occupierID
Fields:
-
location_id(int) -
location_type(MapItemType) -
x(int) -
y(int) -
kingdom(Kingdom) -
capturer_id(int)
location_type
pydantic-field
¶
location_type: MapItemType = MapItemType.EMPTY
The location's area type
PlayerOwnerInfo
pydantic-model
¶
Bases: PlayerProfileBase
The gdi reply's O block: the player's owner record.
Client: GDICommand.executeCommand (bundle line 129458) reads it with
CastleOtherPlayerData.parseOwnerInfo (bundle line 138996)
Fields:
-
player_id(ParseInt) -
name(str) -
emblem(OwnerCrest | None) -
level(ParseInt) -
legendary_level(ParseInt) -
beginner_protection_seconds(ParseInt) -
honor(ParseInt) -
might(ParseInt) -
top_ranking(ParseInt) -
is_ruin(bool) -
alliance_id(int) -
alliance_rank(int | None) -
alliance_name(str) -
is_searching_alliance(bool) -
revenge_protection_seconds(ParseInt) -
castle_positions(list[OwnerCastlePosition]) -
village_positions(list[OwnerCastlePosition]) -
has_premium_flag(bool) -
has_vip_flag(bool) -
is_dummy(bool) -
achievement_points(ParseInt) -
relocation_remaining_seconds(int) -
faction(OwnerFaction | None) -
alliance_emblem(AllianceEmblem | None) -
title_suffix(int | None) -
title_prefix(int | None) -
via_refer_a_friend(bool) -
received_at(float) -
received_at_wall(float)
GetPlayerInfoRequest
pydantic-model
¶
Bases: BaseRequest
Get detailed player information including castle list.
Command: gdi Payload: {"PID": player_id}
Returns owner info (O) and castle list (gcl.C) with capture status.
Client: C2SGetDetailPlayerInfo (bundle line 27021)
Fields:
player_id
pydantic-field
¶
player_id: int
Wire key PID
A player's id, e.g. MapObject.owner_id from a map scan or client.player.search_player_by_name(), or AllianceMember.player_id
accepts_reply
¶
Whether a gdi reply is about this player: its owner record's OID is PID.
Client: GDICommand.executeCommand (bundle line 129458) reads the player from
O.OID; CastleSendGoodsDialog.handleCastlesList (line 27122) keeps only the reply
about the player it asked for.
GetPlayerInfoResponse
pydantic-model
¶
Bases: BaseResponse
Response containing detailed player information.
Command: gdi Response format: { "O": { ...player fields... }, "gcl": {"PID": ..., "C": [{"KID": ..., "AI": [{"AI": [...], ...}]}]} }
Client: GDICommand.executeCommand (bundle line 129458) reads O with
parseOwnerInfo and hands gcl to the same
CastleListVO.parseCastleList as the gcl command.
Fields:
-
error_code(int) -
owner(PlayerOwnerInfo | None) -
castle_list(GetCastlesResponse)
Validators:
-
_fold_landmark_lists_into_gcl -
_castle_list_needs_an_object→castle_list
model_config
class-attribute
¶
model_config: ConfigDict = ConfigDict(populate_by_name=True, extra='allow')
castle_list
pydantic-field
¶
castle_list: GetCastlesResponse
Wire key gcl
The player's castles, outposts and landmarks, shaped as a GetCastlesResponse
alliance_id
property
¶
alliance_id: int
The player's alliance id, -1 when the player is in none or the reply has no owner.
has_bird
property
¶
has_bird: bool
Whether the player has peace protection left now; False once it has run out or without owner.
has_beginner_protection
property
¶
has_beginner_protection: bool
Whether the player has beginner protection left now; False once it has run out or without owner.
revenge_protection_end
property
¶
revenge_protection_end: float | None
When peace protection ends, in time.monotonic() seconds; None without protection or owner.
get_castles
¶
get_castles() -> list[CastleInfo]
All castles, outposts and landmarks across all kingdoms.
get_location_captures
¶
get_location_captures() -> list[LocationCapture]
Extract locations that are being captured.
Returns:
| Type | Description |
|---|---|
list[LocationCapture]
|
List of LocationCapture objects for locations with active captures. |
SearchPlayerRequest
pydantic-model
¶
Bases: BaseRequest
Find a player by name.
Command: wsp Payload: {"PN": player_name}, the name encoded as chat text
Client: C2SSearchPlayerVO (bundle line 66000), sent by
CastleWorldmapData.searchPlayerByName (bundle line 19009)
Fields:
SearchPlayerResponse
pydantic-model
¶
Bases: BaseResponse
Response to a player search.
Command: wsp
Client: WSPCommand.executeCommand (bundle line 131619) reads gaa.OI
with parseOwnerInfoArray and gaa.AI with parseAreaInfos, as the
fnm reply does, then CastleWorldmapData.parseSearchInfos (bundle line
19010) opens the area at X/Y, the found player's castle.
Fields:
Validators:
-
_area_needs_an_object→area
model_config
class-attribute
¶
model_config: ConfigDict = ConfigDict(populate_by_name=True, extra='allow')
player.models.profile¶
The owner record both the alliance member list and the gdi player info send.
AllianceMember (ain, alliance.models.info) and PlayerOwnerInfo (gdi, player.models.info) subclass PlayerProfileBase.
Classes:
| Name | Description |
|---|---|
PlayerProfileBase |
A player's owner record. |
PlayerProfileBase
pydantic-model
¶
Bases: BasePayload
A player's owner record.
Client: WorldMapOwnerInfoVO.fillFromParamObject (bundle line 10794),
which both the ain member list and the gdi owner go through
(CastleOtherPlayerData.parseOwnerInfo, bundle line 138996);
WorldMapOwnerInfoVO.parsePosList (bundle line 10795) for AP and VP.
The protection times count from received_at, as the client counts them from the CachedTimer
reading parseOwnerInfo takes (bundle line 138997).
received_at_wall is stamped with it, for the *_utc ends: the wall clock at reading.
Fields:
-
player_id(ParseInt) -
name(str) -
emblem(OwnerCrest | None) -
level(ParseInt) -
legendary_level(ParseInt) -
beginner_protection_seconds(ParseInt) -
honor(ParseInt) -
might(ParseInt) -
top_ranking(ParseInt) -
is_ruin(bool) -
alliance_id(int) -
alliance_rank(int | None) -
alliance_name(str) -
is_searching_alliance(bool) -
revenge_protection_seconds(ParseInt) -
castle_positions(list[OwnerCastlePosition]) -
village_positions(list[OwnerCastlePosition]) -
has_premium_flag(bool) -
has_vip_flag(bool) -
is_dummy(bool) -
achievement_points(ParseInt) -
relocation_remaining_seconds(int) -
faction(OwnerFaction | None) -
alliance_emblem(AllianceEmblem | None) -
title_suffix(int | None) -
title_prefix(int | None) -
via_refer_a_friend(bool) -
received_at(float) -
received_at_wall(float)
beginner_protection_seconds
pydantic-field
¶
beginner_protection_seconds: ParseInt = 0
Wire key RNP
Seconds of beginner protection left
alliance_id
pydantic-field
¶
alliance_id: int = -1
Wire key AID
Alliance id; -1 when the player is in none
alliance_rank
pydantic-field
¶
alliance_rank: int | None = None
Wire key AR
Rank in the alliance, an AllianceRank value; None when unreadable
is_searching_alliance
pydantic-field
¶
is_searching_alliance: bool = False
Wire key SA
The player is looking for an alliance
revenge_protection_seconds
pydantic-field
¶
revenge_protection_seconds: ParseInt = 0
Wire key RPT
Seconds of peace protection left
village_positions
pydantic-field
¶
village_positions: list[OwnerCastlePosition]
Wire key VP
Villages
has_premium_flag
pydantic-field
¶
has_premium_flag: bool = False
Wire key PF
The player has the premium flag
is_dummy
pydantic-field
¶
is_dummy: bool = False
Wire key DUM
A placeholder record, not a real player
achievement_points
pydantic-field
¶
achievement_points: ParseInt = 0
Wire key AVP
Achievement points
relocation_remaining_seconds
pydantic-field
¶
relocation_remaining_seconds: int = 0
Wire key RRD
Seconds until the player's castle relocation ends
alliance_emblem
pydantic-field
¶
alliance_emblem: AllianceEmblem | None = None
Wire key aee
The alliance's crest; None outside an alliance
title_suffix
pydantic-field
¶
title_suffix: int | None = None
Wire key SUF
Suffix title id; None when unsent
title_prefix
pydantic-field
¶
title_prefix: int | None = None
Wire key PRE
Prefix title id; None when unsent
via_refer_a_friend
pydantic-field
¶
via_refer_a_friend: bool = False
Wire key IRF
The player joined through a referral
received_at
pydantic-field
¶
received_at: float
When the values were read, in time.monotonic() seconds
received_at_wall
pydantic-field
¶
received_at_wall: float
When the values were read, in time.time() seconds
is_in_alliance
property
¶
is_in_alliance: bool
Whether the player is in an alliance.
Client: WorldMapOwnerInfoVO.isInAlliance (bundle line 10834)
alliance_rank_enum
property
¶
alliance_rank_enum: AllianceRank | None
alliance_rank as an AllianceRank, None for a value the client does not define.
is_leader
property
¶
is_leader: bool
Whether the player leads their alliance: rank 0.
Client: AllianceInfoVO.allianceLeader (bundle line 25986) is the member whose allianceRank is 0
has_bird
property
¶
has_bird: bool
Whether the player has peace protection left now; False once it has run out.
Client: WorldMapOwnerInfoVO.isPeaceProtected (bundle line 10808)
has_beginner_protection
property
¶
has_beginner_protection: bool
Whether the player has beginner protection left now; False once it has run out.
Client: WorldMapOwnerInfoVO.isNoobProtected (bundle line 10806)
revenge_protection_end
property
¶
revenge_protection_end: float | None
When peace protection ends, in time.monotonic() seconds; None without protection.
beginner_protection_end
property
¶
beginner_protection_end: float | None
When beginner protection ends, in time.monotonic() seconds; None without protection.
revenge_protection_end_utc
¶
revenge_protection_end_utc() -> datetime | None
When peace protection ends, as a UTC datetime; None without protection.
Counted from received_at_wall, not from now, so it stays the same
for the same record.
beginner_protection_end_utc
¶
beginner_protection_end_utc() -> datetime | None
When beginner protection ends, as a UTC datetime; None without protection.
Counted from received_at_wall, not from now, so it stays the same
for the same record.
remaining_revenge_protection_seconds
¶
Seconds of peace protection left, 0 when none.
Client: WorldMapOwnerInfoVO.remainingPeaceTime (bundle line 10804), counted from the
CachedTimer reading fillFromParamObject keeps (bundle line 10794)
remaining_beginner_protection_seconds
¶
Seconds of beginner protection left, 0 when none.
Client: WorldMapOwnerInfoVO.remainingNoobTime (bundle line 10802), counted from the
CachedTimer reading fillFromParamObject keeps (bundle line 10794)
player.models.progress¶
The player's progress, as the login data and its pushes send it.
Commands:
- rei: research
- boi: boosters, the premium account and production slots (bfs: the festival)
- gmu: might points
- ufa: glory points
- ufp: Berimond (faction) points
- uar: top-X title ranks, the island title and the displayed title systems
- vli: achievements
- gri: relocation
Each model is read once and never changes; the times in it count from received_at.
Classes:
| Name | Description |
|---|---|
ResearchInfoResponse |
Your research: what is finished and what runs now. |
Booster |
One booster of a |
Festival |
The running festival: a |
BoosterInfoResponse |
Your boosters, premium account, production slots and festival. |
MightPointsResponse |
Your might points. |
GloryPointsResponse |
Your glory points, which give the glory titles. |
FactionPointsResponse |
Your Berimond (faction) points, which give the Berimond titles. |
TopTitleRanking |
One title system's top-X standing: a |
IslandTitle |
A |
AllianceCityTitle |
A |
TitleRanksResponse |
Your top-X title ranks, Storm Islands title and the title systems your name shows. |
AchievementProgress |
One achievement's progress, an entry of a |
AchievementsResponse |
Your achievement points, finished achievements and progress. |
RelocationInfoResponse |
Your castle relocations: how many, the one running and the cooldown. |
Attributes:
| Name | Type | Description |
|---|---|---|
PERMANENT_BOOSTER_DURATION |
|
PERMANENT_BOOSTER_DURATION
module-attribute
¶
BoosterConst.PERMANENT_BOOSTER_DURATION (dll): a booster's RT when it never runs out
ResearchInfoResponse
pydantic-model
¶
Bases: TimedResponse
Your research: what is finished and what runs now.
Command: rei, as a login section of gbd, a push, and inside the res and msr replies.
Client: CastleResearchData.parse_REI (bundle line 139330), remainingResearchTimeInSeconds
and isSomeResearchActive (bundle lines 139337-139338)
Fields:
-
received_at(float) -
error_code(int) -
bought_research_ids(tuple[EnumOrInt['Research'], ...]) -
current_research_id(ClientInt) -
research_seconds(ClientNumber)
bought_research_ids
pydantic-field
¶
Wire key BR
Finished researches
current_research_id
pydantic-field
¶
current_research_id: ClientInt = 0
Wire key ARID
The research running now, -1 for none
research_seconds
pydantic-field
¶
research_seconds: ClientNumber = 0
Wire key ARRT
Seconds left on the running research when the values were read
remaining_research_seconds
¶
Whole seconds left on the running research, 0 when none runs.
Booster
pydantic-model
¶
Bases: TimedPayload
One booster of a boi's BO list.
Client: CastleHeroDefaultBoosterShopVO.parseServerInfo (bundle line 8677), remainingTimeInSeconds
and isActive (bundle lines 8678-8682); the subclasses that read B (bundle lines 90338, 90365,
90670, 90864)
Fields:
-
received_at(float) -
booster_id(ClientInt) -
level(ClientInt) -
seconds(ClientInt) -
purchase_count(int | float | None) -
bonus_percent(ClientInt | None)
Validators:
-
_count→purchase_count
seconds
pydantic-field
¶
seconds: ClientInt = 0
Wire key RT
Seconds left when the values were read; PERMANENT_BOOSTER_DURATION for ever
purchase_count
pydantic-field
¶
Wire key PC
How many times in a row it was bought; None when not sent
bonus_percent
pydantic-field
¶
bonus_percent: ClientInt | None = None
Wire key B
Its bonus in percent, if it has one
remaining_seconds
¶
Seconds left, -1 at least; infinite for a permanent booster.
Festival
pydantic-model
¶
Bases: TimedPayload
The running festival: a boi's bfs, or a bfs reply.
Client: FestivalVO.fillFromParamObject and parseDuration (bundle lines 90214-90215)
Fields:
festival_type
pydantic-field
¶
festival_type: ClientInt = 0
Wire key T
The festival's id, -1 for none
seconds
pydantic-field
¶
seconds: ClientNumber = 0
Wire key RT
Seconds left when the values were read
BoosterInfoResponse
pydantic-model
¶
Bases: TimedResponse
Your boosters, premium account, production slots and festival.
A boi lists only some boosters; the client keeps the others as they were, and
client.state.get_boosts() does the same.
Command: boi, as a login section of gbd, a push, and inside the replies to buying a booster
(bcs, bds, bis, bms, brs, ovs, ups, btx).
Client: CastlePremiumBoostData.parse_BOI (bundle line 15202), PremiumAccountVO.parseServerInfo,
isActive, remainingTimeInSeconds and premiumAccountType (bundle lines 90230-90235),
parse_bfs (bundle line 15218)
Fields:
-
received_at(float) -
error_code(int) -
boosters(tuple[Booster, ...]) -
premium_seconds(ClientNumber) -
premium_type(EnumOrInt[PremiumAccountType] | None) -
unit_slots(Numbers) -
tool_slots(Numbers) -
festival(Festival | None)
Validators:
-
_boosters→boosters -
_festival→festival -
_premium_type→premium_type
boosters
pydantic-field
¶
Wire key BO
The boosters this packet lists
premium_seconds
pydantic-field
¶
premium_seconds: ClientNumber = 0
Wire key PA
Seconds of premium account left when the values were read
premium_type
pydantic-field
¶
premium_type: EnumOrInt[PremiumAccountType] | None = None
Wire key PT
The premium account's type; None when the packet names none
unit_slots
pydantic-field
¶
unit_slots: Numbers = ()
Wire key SU
Unit production slots: above 0 bought, below 0 permanent
tool_slots
pydantic-field
¶
tool_slots: Numbers = ()
Wire key ST
Tool production slots: above 0 bought, below 0 permanent
festival
pydantic-field
¶
festival: Festival | None = None
Wire key bfs
The festival; None when not sent
premium_remaining_seconds
¶
Seconds of premium account left, 0 when it is not active.
is_premium_active
¶
Whether the premium account runs.
premium_account_type
¶
premium_account_type(now: float | None = None) -> PremiumAccountType | int | None
The premium account's type while it runs, else None.
MightPointsResponse
pydantic-model
¶
Bases: TimedResponse
Your might points.
Command: gmu, as a login section of gbd and a push. The client reads one only
when it has both MP and HMP.
Client: MightData.parse_GMU (bundle line 113517)
Fields:
GloryPointsResponse
pydantic-model
¶
Bases: TimedResponse
Your glory points, which give the glory titles.
Command: ufa, as a login section of gbd and a push.
Client: CastleTitleData.parseUFA (bundle line 21038), which parseInt s a string and keeps a number
Fields:
-
received_at(float) -
error_code(int) -
glory_points(int | float | None) -
highest_glory_points(int | float | None)
Validators:
-
_points→glory_points,highest_glory_points
FactionPointsResponse
pydantic-model
¶
Bases: TimedResponse
Your Berimond (faction) points, which give the Berimond titles.
Command: ufp, a push. The client ignores the copy in gbd.
Client: UFPCommand (bundle line 121010), CastleTitleData.parseUFP (bundle line 21039)
Fields:
TopTitleRanking
pydantic-model
¶
Bases: TimedPayload
One title system's top-X standing: a uar's FTM (glory) or BTM (Berimond).
Client: CastleTitleData.parseUAR (bundle line 21022)
Fields:
-
received_at(float) -
top_rank(ClientInt) -
reset_seconds(ClientNumber) -
thresholds(tuple[int, ...]) -
top_player_id(int | None)
Validators:
-
_thresholds→thresholds -
_player_id→top_player_id
top_rank
pydantic-field
¶
top_rank: ClientInt = 0
Wire key CTXT
Your rank for the system's top-X titles, -1 or 0 for none
reset_seconds
pydantic-field
¶
reset_seconds: ClientNumber = 0
Wire key RS
Seconds until the top-X titles reset
thresholds
pydantic-field
¶
Wire key NTFP
The points the top-X titles need, highest title first
IslandTitle
pydantic-model
¶
Bases: BasePayload
A uar's ITM. Client: CastleTitleData.parseIslandDataFromServer (bundle lines 21023-21027)
Fields:
Validators:
-
_held→title_id
AllianceCityTitle
pydantic-model
¶
Bases: BasePayload
A uar's ATM. Client: CastleTitleData.parseAllianceCityDataFromServer (bundle lines 21028-21036)
Fields:
Validators:
TitleRanksResponse
pydantic-model
¶
Bases: TimedResponse
Your top-X title ranks, Storm Islands title and the title systems your name shows.
Command: uar, as a login section of gbd and a push.
Client: CastleTitleData.parseUAR (bundle line 21022)
Fields:
-
received_at(float) -
error_code(int) -
glory(TopTitleRanking) -
faction(TopTitleRanking) -
island_title(IslandTitle) -
alliance_city_title(AllianceCityTitle | None) -
prefix_system(EnumOrStr[TitleSystem] | None) -
suffix_system(EnumOrStr[TitleSystem] | None)
Validators:
-
_block→glory,faction,island_title -
_city→alliance_city_title -
_system→prefix_system,suffix_system
alliance_city_title
pydantic-field
¶
alliance_city_title: AllianceCityTitle | None = None
Wire key ATM
The alliance city title; None when not sent
prefix_system
pydantic-field
¶
prefix_system: EnumOrStr[TitleSystem] | None = None
Wire key PFX
The title system shown before your name; None when not sent
suffix_system
pydantic-field
¶
suffix_system: EnumOrStr[TitleSystem] | None = None
Wire key SFX
The title system shown after your name; None when not sent
AchievementProgress
pydantic-model
¶
Bases: BasePayload
One achievement's progress, an entry of a vli's RA.
Client: CastleAchievementData.parse_RA (bundle line 29838), AchievementVO.setProgress (bundle line 92889)
Fields:
AchievementsResponse
pydantic-model
¶
Bases: TimedResponse
Your achievement points, finished achievements and progress.
A vli adds to what the client knows: finished achievements stay finished, and an
achievement it does not list keeps its progress. client.state.get_achievements() does the same.
Command: vli, as a login section of gbd and a push.
Client: CastleAchievementData.parse_vli, parse_RA and parse_FA (bundle lines 29837-29842)
Fields:
-
received_at(float) -
error_code(int) -
achievement_points(ClientInt) -
finished_achievement_ids(tuple[EnumOrInt['Achievement'], ...]) -
progress(tuple[AchievementProgress, ...])
Validators:
-
_progress→progress
achievement_points
pydantic-field
¶
achievement_points: ClientInt = 0
Wire key AVP
Achievement points
finished_achievement_ids
pydantic-field
¶
Wire key FA
Finished achievements
progress
pydantic-field
¶
progress: tuple[AchievementProgress, ...] = ()
Wire key RA
Progress of the achievements this packet lists
RelocationInfoResponse
pydantic-model
¶
Bases: TimedResponse
Your castle relocations: how many, the one running and the cooldown.
Command: gri, as a login section of gbd and a push.
Client: CastleUserData.parse_GRI (bundle line 9910), remainingRelocationDuration and
remainingRelocationCooldown
Fields:
-
received_at(float) -
error_code(int) -
relocation_count(ClientInt) -
relocation_seconds(ClientNumber) -
relocation_mode(int | None) -
cooldown_seconds(ClientNumber) -
destination_x(ClientInt) -
destination_y(ClientInt)
Validators:
-
_mode→relocation_mode
relocation_seconds
pydantic-field
¶
relocation_seconds: ClientNumber = 0
Wire key RD
Seconds left on the running relocation when the values were read
relocation_mode
pydantic-field
¶
relocation_mode: int | None = None
Wire key JM
The relocation's mode: its time counts only while this is 0, and the client redraws the map at OX/OY for 1; None when not sent
cooldown_seconds
pydantic-field
¶
cooldown_seconds: ClientNumber = 0
Wire key RMC
Seconds until you may relocate again, when the values were read
destination_x
pydantic-field
¶
destination_x: ClientInt = 0
Wire key DX
The relocation's destination X
destination_y
pydantic-field
¶
destination_y: ClientInt = 0
Wire key DY
The relocation's destination Y
remaining_relocation_seconds
¶
Seconds left on the running relocation, 0 when none runs.
player.models.research¶
Starting research and shortening it with a minute skip.
Commands:
- res: start a research
- msr: shorten the running research with a minute skip
Finishing a research at once for rubies (C2SResearchFinishInstantVO, bundle line 49093) is left out.
Classes:
| Name | Description |
|---|---|
StartResearchRequest |
Start a research, paying what it costs: resources and coins, or rubies for a research that costs them. |
StartResearchResponse |
Your research, coins and rubies, and the joined castle's resources after starting a research. |
SkipResearchRequest |
Shorten the running research with a minute skip. |
SkipResearchResponse |
Your research after a minute skip. |
StartResearchRequest
pydantic-model
¶
Bases: BaseRequest
Start a research, paying what it costs: resources and coins, or rubies for a research that costs them.
Command: res Payload: {"RID": research_id, "PO": -1, "PWR": 0}
PWR 1 would pay missing resources with rubies (CastleResourceWaitDialogProperties.getResourceSkipCommand,
bundle line 35216); this request always sends 0. A research whose own costs hold rubies
(ResearchDef.cost_rubies) is sent the same way and spends them; client.player.start_research
refuses it unless asked to spend rubies.
Client: C2SResearchStartVO (bundle line 47697), sent by ResearchInfo.buyResearch (bundle line 79704)
with the private offer -1 and PWR 0
Fields:
-
research_id(EnumOrInt['Research']) -
private_offer_id(int) -
pay_with_rubies(Literal[0])
StartResearchResponse
pydantic-model
¶
Bases: BaseResponse
Your research, coins and rubies, and the joined castle's resources after starting a research.
Command: res Payload: {"rei": {..}, "gcu": {..}, "grc": {..}}
Client: RESCommand.executeCommand (bundle line 126876), which parses all three
Fields:
-
error_code(int) -
research(ResearchInfoResponse | None) -
currencies(CurrencyBlock) -
resources(CastleResources | None)
research
pydantic-field
¶
research: ResearchInfoResponse | None = None
Wire key rei
Your research after
resources
pydantic-field
¶
resources: CastleResources | None = None
Wire key grc
The joined castle's resources after
SkipResearchRequest
pydantic-model
¶
Bases: BaseRequest
Shorten the running research with a minute skip.
Command: msr Payload: {"MST": minute_skip}
Client: C2SMinuteSkipResearchVO (bundle line 79810), built by
ResearchMinuteSkipProperties.getMinuteSkipCommand (bundle line 79784); CastleMinuteSkipDialog
passes the currency's jsonKey (bundle line 7722)
Fields:
-
minute_skip(EnumOrStr['Currency'])
SkipResearchResponse
pydantic-model
¶
Bases: BaseResponse
Your research after a minute skip.
Command: msr Payload: {"rei": {..}}
Client: MSRCommand.executeCommand (bundle line 125829)
Fields:
-
error_code(int) -
research(ResearchInfoResponse | None)
research
pydantic-field
¶
research: ResearchInfoResponse | None = None
Wire key rei
Your research after
player.titles¶
The titles you hold, worked out from your points and ranks as the client does.
Client: CastleTitleData (bundle lines 21000-21130)
Functions:
| Name | Description |
|---|---|
titles_in_order |
A title system's title ids, from its first title along each title's next one. |
held_titles |
The glory or Berimond titles you hold, lowest first. |
island_title_chain |
The island titles you hold with one: it and every title below it, lowest first. |
player_title_ids |
Every title you hold, as the client lists them: glory, then Berimond, then Storm Islands. |
titles_in_order
¶
titles_in_order(game_data: GameData, system: TitleSystem) -> list[Title | int]
A title system's title ids, from its first title along each title's next one.
Client: CastleTitleData.setupNextTitles (bundle line 21016) and orderTitlesInSystem
(bundle line 21058)
held_titles
¶
held_titles(game_data: GameData, system: TitleSystem, points: float | None, top_rank: int | None) -> list[Title | int]
The glory or Berimond titles you hold, lowest first.
Each title in order is held while its points threshold is reached, or for a top-X title while your rank is above 0 and within its top X; the first one missed ends the list.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
game_data
|
GameData
|
Loaded tables |
required |
system
|
TitleSystem
|
|
required |
points
|
float | None
|
Your glory points ( |
required |
top_rank
|
int | None
|
The system's |
required |
Client: CastleTitleData.setupThisUsersTitlesinSystem (bundle line 21048), with TitleVO.isTopXTitle
island_title_chain
¶
The island titles you hold with one: it and every title below it, lowest first.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
game_data
|
GameData
|
Loaded tables |
required |
island_title_id
|
int
|
Your current Storm Islands title, -1 or an unknown id for none |
required |
Client: CastleTitleData.getUsersTitleVectorFromSystem for ISLAND_TITLE (bundle line 21119)
player_title_ids
¶
player_title_ids(game_data: GameData, glory: GloryPointsResponse | None, faction: FactionPointsResponse | None, ranks: TitleRanksResponse | None) -> list[Title | int]
Every title you hold, as the client lists them: glory, then Berimond, then Storm Islands.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
game_data
|
GameData
|
Loaded tables |
required |
glory
|
GloryPointsResponse | None
|
|
required |
faction
|
FactionPointsResponse | None
|
|
required |
ranks
|
TitleRanksResponse | None
|
|
required |
Client: CastleTitleData.thisUsersTitles (bundle line 21073)