Map¶
Map areas, map rows and the kingdom scanner behind client.map.
map.service¶
Map areas, kingdom scans and finding map objects, enemy castles and Berimond towers.
Reading the map moves the session off the castle it had joined: a
castle-scoped read after a scan fails with NOT_IN_OWNED_CASTLE until the
castle is joined again. client.army methods join it themselves; anything
else castle-scoped needs client.castle.select first.
Classes:
| Name | Description |
|---|---|
MapService |
Read the world map: areas, kingdom scans and the nearest object of a type. |
MapService
¶
Bases: BaseService
Read the world map: areas, kingdom scans and the nearest object of a type.
Reached as client.map.
Methods:
| Name | Description |
|---|---|
scan_map_area |
Scan a specific area of the map. |
scan_kingdom |
Scan a kingdom map. See MapScanner.scan_kingdom; the session leaves its castle. |
scan_chunks |
Scan an explicit chunk list (no BFS). See MapScanner.scan_chunks; the session leaves its castle. |
find_next |
Find the nearest map object of an area type, with the map rows around it. |
find_next_enemy_castle |
Find an enemy castle near a position, with the map rows around it. |
find_next_tower |
Find the next Berimond tower, with the map rows around it, read in Berimond. |
Attributes:
| Name | Type | Description |
|---|---|---|
pacer |
|
scan_map_area
¶
scan_map_area(x1: int, y1: int, x2: int, y2: int, kingdom: Kingdom = GREEN, timeout: float = 5.0) -> GetMapAreaResponse
Scan a specific area of the map.
The session leaves the castle it had joined; see the module docstring.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x1
|
int
|
Left X coordinate |
required |
y1
|
int
|
Top Y coordinate |
required |
x2
|
int
|
Right X coordinate |
required |
y2
|
int
|
Bottom Y coordinate |
required |
kingdom
|
Kingdom
|
Kingdom to scan |
GREEN
|
timeout
|
float
|
Timeout in seconds |
5.0
|
Raises:
| Type | Description |
|---|---|
CommandError / EmpireTimeoutError / ConnectionClosedError
|
see |
scan_kingdom
¶
scan_kingdom(kingdom: Kingdom = GREEN, item_types: list[MapItemType] | None = None, timeout: float = 300.0, request_timeout: float = 5.0, chunk_delay: float = 0.0, include_unowned_types: set[MapItemType] | None = None, *, cancel: Event | None = None, on_chunk: ChunkHandler | None = None) -> ScanResult
Scan a kingdom map. See MapScanner.scan_kingdom; the session leaves its castle.
scan_chunks
¶
scan_chunks(kingdom: Kingdom, chunks: list[tuple[int, int]], item_types: list[MapItemType] | None = None, timeout: float = 300.0, request_timeout: float = 5.0, chunk_delay: float = 0.0, include_unowned_types: set[MapItemType] | None = None, *, cancel: Event | None = None, on_chunk: ChunkHandler | None = None) -> ScanResult
Scan an explicit chunk list (no BFS). See MapScanner.scan_chunks; the session leaves its castle.
find_next
¶
find_next(area_type: MapItemType, kingdom: Kingdom = GREEN, *, min_level: int = -1, max_level: int = -1, owner_id: int = -1, timeout: float = 5.0) -> FindNextMapObjectResponse | None
Find the nearest map object of an area type, with the map rows around it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
area_type
|
MapItemType
|
The area type to look for |
required |
kingdom
|
Kingdom
|
The kingdom to look in |
GREEN
|
min_level
|
int
|
Lowest level to match, -1 for any |
-1
|
max_level
|
int
|
Highest level to match, -1 for any |
-1
|
owner_id
|
int
|
The NPC owner to match, -1 for any |
-1
|
timeout
|
float
|
Timeout in seconds |
5.0
|
Returns:
| Type | Description |
|---|---|
FindNextMapObjectResponse | None
|
The reply, its rows read in |
Raises:
| Type | Description |
|---|---|
CommandError
|
The server refused for any other reason |
find_next_enemy_castle
¶
find_next_enemy_castle(x: int, y: int, index: int = 0, min_level: int = -1, max_level: int = -1, *, kingdom: Kingdom = GREEN, timeout: float = 5.0) -> FindNextEnemyCastleResponse | None
Find an enemy castle near a position, with the map rows around it.
The game's "search enemy" button asks with index 0, 1, 2 ... 9 and
then 0 again, so each press finds another castle. On a live account
indexes 0 to 2 found three different players' outposts near the castle
searched from; the reply also echoes N, which the client does not read.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
int
|
Map x to search from |
required |
y
|
int
|
Map y to search from |
required |
index
|
int
|
Which castle to find, 0 to 9 |
0
|
min_level
|
int
|
Lowest level to match, -1 for any |
-1
|
max_level
|
int
|
Highest level to match, -1 for any |
-1
|
kingdom
|
Kingdom
|
The kingdom the rows of the reply are read in; the request names none |
GREEN
|
timeout
|
float
|
Timeout in seconds |
5.0
|
Returns:
| Type | Description |
|---|---|
FindNextEnemyCastleResponse | None
|
The reply; None when nothing matches (NO_PLAYER_FOUND) |
Raises:
| Type | Description |
|---|---|
ValueError
|
|
CommandError
|
The server refused for any other reason |
Client: C2SFindNextEnemyCastleVO (bundle line 55304), sent by
SearchEnemyPanelButton.onButtonClicked (bundle line 108075)
find_next_tower
¶
find_next_tower(timeout: float = 5.0) -> FindNextTowerResponse | None
Find the next Berimond tower, with the map rows around it, read in Berimond.
Returns:
| Type | Description |
|---|---|
FindNextTowerResponse | None
|
The reply; None when nothing matches (NO_PLAYER_FOUND) |
Raises:
| Type | Description |
|---|---|
CommandError
|
The server refused for any other reason, for example when no Berimond event runs |
Client: C2SFindNextTowerVO (bundle line 37533), sent by
SearchEnemyPanelButton.onButtonClicked (bundle line 108075) in Berimond
map.scanner¶
Kingdom scans: breadth-first discovery and targeted re-scans of known chunks.
Classes:
| Name | Description |
|---|---|
ScanResult |
What a scan read: |
MapScanner |
Utility class to scan kingdom maps with dynamic boundary detection. |
Functions:
| Name | Description |
|---|---|
scan_kingdom_with |
Scan one kingdom with several logged-in clients at once, each on its own thread. |
Attributes:
| Name | Type | Description |
|---|---|---|
logger |
|
|
ChunkHandler |
Called with a chunk, the items kept from it and its owner records by player id; see |
ChunkHandler
module-attribute
¶
Called with a chunk, the items kept from it and its owner records by player id; see on_chunk.
ScanResult
¶
Bases: NamedTuple
What a scan read: items and objects stay empty when it handed each chunk to on_chunk.
Attributes:
| Name | Type | Description |
|---|---|---|
items |
list[MapAreaItem]
|
|
objects |
dict[int, MapObject]
|
|
kingdom |
Kingdom
|
|
failed_chunks |
tuple[tuple[int, int], ...]
|
|
content_chunks |
tuple[tuple[int, int], ...]
|
|
MapScanner
¶
Utility class to scan kingdom maps with dynamic boundary detection.
Methods:
| Name | Description |
|---|---|
scan_kingdom |
Scan a kingdom map with dynamic boundary detection. |
scan_chunks |
Scan an explicit list of chunks — no BFS discovery. |
chunk_for_position |
Map world coordinates to the chunk that contains them. |
Attributes:
| Name | Type | Description |
|---|---|---|
CHUNK_SIZE |
|
|
MAX_COORD |
|
|
CHUNK_RETRIES |
|
|
RETRY_BACKOFF |
|
|
client |
|
scan_kingdom
¶
scan_kingdom(kingdom: Kingdom = GREEN, item_types: list[MapItemType] | None = None, timeout: float = 300.0, request_timeout: float = 5.0, chunk_delay: float = 0.0, include_unowned_types: set[MapItemType] | None = None, *, cancel: Event | None = None, on_chunk: ChunkHandler | None = None) -> ScanResult
Scan a kingdom map with dynamic boundary detection. Uses BFS expansion from the bot's castle position.
item_types selects which map items are collected, and its two
empty-ish values mean opposite things — the sentinel is inverted,
so read this carefully:
None(the default) is the most restrictive: it collects player main castles only,MapItemType.CASTLEin the green kingdom andMapItemType.KINGDOM_CASTLEin the others.[](empty list) is the least restrictive: it disables filtering and collects every item type.- a non-empty list collects exactly those types.
In every case free castle plots and rows whose owner is an NPC or
nobody (owner_id not above 0, such as an unclaimed outpost) are
skipped unless their type is in include_unowned_types. A castle on
the move (MapAreaItem.is_relocating) is kept, and so is a row
that names no owner at all, such as an NPC camp. Rows shorter than
[type, x, y, id] (placeholders, inactive event camps) are skipped.
Every item and ScanResult.kingdom carry the scanned kingdom.
A scan moves the session off the castle it had joined; see
MapService.
Chunks that fail even after a retry are reported in
ScanResult.failed_chunks so callers can tell a partial scan
from a complete one. The same applies to chunks left unscanned
when the overall timeout expires or the connection drops: an
empty failed_chunks means the scan really did finish. A dropped
connection is logged once, as a warning, by the session; the scan
itself only notes at INFO how many chunks it left.
chunk_delay waits that many seconds before each gaa request;
by default there is no wait, as the client does not pace its map
requests either. A live scan of 289 chunks at about 17 requests a
second saw no refusal and no dropped connection. A chunk that times
out, fails on the network or is refused with a cooldown is asked
again after a short backoff (CHUNK_RETRIES, RETRY_BACKOFF).
Setting cancel stops the scan before its next chunk and returns
what it has, the chunks not scanned in failed_chunks, as a timeout
does. It is looked at between requests, never during one: the chunk
in flight ends with its reply or request_timeout, so its reply
cannot reach the next gaa request, and a cancel takes at most one
chunk.
on_chunk is called with each chunk that answered, empty ones too,
as on_chunk((cx, cy), items, objects), and the scan keeps nothing
of that chunk: ScanResult.items and ScanResult.objects come
back empty. A caller that keeps only what it needs from each chunk
holds that, not the whole kingdom's rows and owner records, which
every full garbage collection walks while they live. A failed chunk
is not passed on; it is in failed_chunks as usual. An exception
raised by on_chunk ends the scan. The client decides nothing
here: it reads each chunk into its world map as it arrives.
scan_chunks
¶
scan_chunks(kingdom: Kingdom, chunks: list[tuple[int, int]], item_types: list[MapItemType] | None = None, timeout: float = 300.0, request_timeout: float = 5.0, chunk_delay: float = 0.0, include_unowned_types: set[MapItemType] | None = None, *, cancel: Event | None = None, on_chunk: ChunkHandler | None = None) -> ScanResult
Scan an explicit list of chunks — no BFS discovery.
Use this to re-scan a known region cheaply: run scan_kingdom()
once to discover the map, then feed its content_chunks back
here on subsequent scans. Also useful for targeted scans (e.g.
only the chunks around known castle coordinates via
chunk_for_position).
item_types behaves exactly as in scan_kingdom(), inverted
sentinel included: None (the default) collects player main
castles only, [] disables filtering and collects every type,
and a non-empty list collects exactly those types.
include_unowned_types also matches scan_kingdom(): types listed
there are collected even when they have no player owner.
Chunks are deduplicated and out-of-range coordinates skipped.
Unscanned chunks left over when timeout hits or cancel is set
are reported in failed_chunks; cancel is looked at between
chunks, and on_chunk takes each chunk in place of the result, as
in scan_kingdom().
scan_kingdom_with
¶
scan_kingdom_with(clients: Sequence[_Client], kingdom: Kingdom = GREEN, item_types: list[MapItemType] | None = None, timeout: float = 300.0, request_timeout: float = 5.0, chunk_delay: float = 0.0, include_unowned_types: set[MapItemType] | None = None, *, chunks: Iterable[tuple[int, int]] | None = None, cancel: Event | None = None, on_chunk: ChunkHandler | None = None) -> ScanResult
Scan one kingdom with several logged-in clients at once, each on its own thread.
Without chunks the clients discover the kingdom together, breadth-first
from the castle there of the first client that has one, as MapScanner.scan_kingdom
does; with chunks (say a discovery's content_chunks) they scan
those, as MapScanner.scan_chunks does. Every other argument means
what it means there. Each client keeps one request in flight and takes the
next chunk not yet taken, so the chunks interleave across the clients and
a slow client takes fewer.
A chunk that fails on one client, after that client's own retries, is
asked again by a client that has not tried it. A client whose session
drops, or whose account has not unlocked the kingdom (a chunk refused with
ADDITIONAL_KINGDOM_NOT_UNLOCKED), leaves the scan and the others take over
its chunk. Only a chunk every remaining client has tried, or one left when
no client remains, the timeout passes or cancel is set, ends in
failed_chunks.
The result merges every client's chunks: items and content_chunks
are in the order the chunks answered, which varies between runs.
on_chunk is called from the clients' threads, one call at a time, so
it needs no lock of its own; an exception it raises stops every client
after its chunk in flight and is raised here, and no chunk reaches
on_chunk after it. An interrupt (KeyboardInterrupt) while waiting
stops the clients the same way, each after the request it has in flight,
and is raised once they have. Every session leaves the castle it had joined.
Parsing a chunk's reply holds the GIL, so threads in one process overlap their waits for replies but not their parsing; see the map-scanning guide. The client decides nothing here: one game client is one session and never spreads a scan.
Raises:
| Type | Description |
|---|---|
ValueError
|
|
CommandError
|
Every client left with ADDITIONAL_KINGDOM_NOT_UNLOCKED |
map.models.areas¶
Map protocol models.
Commands:
- gaa: Get map area/chunk
- fnm: Find the next map object of a type
- fec: Find the next enemy castle
- fnt: Find the next Berimond tower
Classes:
| Name | Description |
|---|---|
GetMapAreaRequest |
Get the map rows of a rectangle of one kingdom. |
MapObject |
An owner record: one player named by the rows of a map reply's |
KingdomProtection |
The player's own protection in one kingdom: a |
MapArea |
Map rows and the owner records of the players they name: the |
GetMapAreaResponse |
The map rows of a rectangle, the owner records of the players they name, and the player's own protection. |
FindNextMapObjectRequest |
Find the nearest map object of one area type, as the client's "show me" and "jump to" buttons do. |
FindNextMapObjectResponse |
Where the object found lies, with the map rows around it. |
FindNextEnemyCastleRequest |
Find an enemy castle near a position, as the client's "search enemy" button does. |
FindNextEnemyCastleResponse |
Where the enemy castle found lies, with the map rows around it. |
FindNextTowerRequest |
Find the next Berimond tower. |
FindNextTowerResponse |
Where the tower found lies, with the map rows around it. |
Attributes:
| Name | Type | Description |
|---|---|---|
MAX_FINDABLE_ENEMY_INDEX |
The highest result index |
MAX_FINDABLE_ENEMY_INDEX
module-attribute
¶
The highest result index fec asks for; the client cycles 0 to 9.
Client: ClientConstCastle.MAX_FINDABLE_ENEMY_COUNTER (bundle line 1004), 10
GetMapAreaRequest
pydantic-model
¶
Bases: BaseRequest
Get the map rows of a rectangle of one kingdom.
Command: gaa Payload: {"KID": kingdom, "AX1": x1, "AY1": y1, "AX2": x2, "AY2": y2}
The client asks for at most 100 tiles a side: it clamps AX2 to
AX1 + 99 once the span passes 100, and AY2 the same way.
Client: C2SGetAreasVO (bundle line 65991), built by
CastleWorldmapData.updateAreaRange (bundle line 19006)
Fields:
accepts_reply
¶
Whether a gaa reply is about this rectangle: this KID and no row outside it.
GAACommand.executeCommand (bundle line 130112) drops a reply whose KID
is not the kingdom on screen, and reads one without KID as that kingdom's;
that every row lies inside the rectangle asked for is how the server answers
(seen live), not something the client checks. Every reply to a request seen
live carried KID, OI and uap; the one-row gaa the server pushes by
itself carried only AI. So a reply without KID is taken only when it
has no rows, whose shape was not seen live. Two requests for one kingdom are
told apart only by their rows, so a reply with none matches either.
MapObject
pydantic-model
¶
Bases: BasePayload
An owner record: one player named by the rows of a map reply's OI list.
A row's owner_id (or a relocating castle's occupier_id) matches a
record's owner_id. The player's castles and villages, wherever they
are, are listed in castle_positions and village_positions.
The client reads most numbers through parseInt and skips a record
with no OID. It reads aee only for a player in an alliance; this
model reads it whenever it is sent.
Client: WorldMapOwnerInfoVO.fillFromParamObject (bundle line 10794),
CastleOtherPlayerData.parseOwnerInfo (bundle line 138996)
Fields:
-
owner_id(int | None) -
is_dummy(bool) -
owner_name(str | None) -
emblem(OwnerCrest | None) -
level(ParseInt) -
legendary_level(ParseInt) -
honor(ParseInt) -
achievement_points(ParseInt) -
prefix_title(int | None) -
suffix_title(int | None) -
current_top_x(ParseInt) -
might_points(ParseInt) -
is_ruin(bool) -
alliance_id(int | None) -
alliance_rank(ParseInt) -
alliance_name(str) -
alliance_emblem(AllianceEmblem | None) -
remaining_protection_time(ParseInt) -
castle_positions(list[OwnerCastlePosition]) -
village_positions(list[OwnerCastlePosition]) -
is_searching_alliance(bool) -
has_vip_flag(bool) -
has_premium_flag(bool) -
remaining_relocation_time(ParseInt) -
remaining_noob_protection(ParseInt) -
faction(OwnerFaction | None) -
via_refer_a_friend(bool)
Validators:
-
_dummy_keeps_only_its_id -
_parse_int_or_none→owner_id,alliance_id -
_one→is_dummy -
_parsed_one→is_ruin -
_truthy→is_searching_alliance,has_vip_flag,has_premium_flag -
_parsed_truthy→via_refer_a_friend -
_name_or_empty→alliance_name -
_block_needs_an_object→emblem,alliance_emblem,faction -
_position_rows→castle_positions,village_positions
is_dummy
pydantic-field
¶
is_dummy: bool = False
Wire key DUM
The record stands in for a player not loaded
achievement_points
pydantic-field
¶
achievement_points: ParseInt = 0
Wire key AVP
Achievement points
prefix_title
pydantic-field
¶
prefix_title: int | None = None
Wire key PRE
Title id shown before the name
suffix_title
pydantic-field
¶
suffix_title: int | None = None
Wire key SUF
Title id shown after the name
current_top_x
pydantic-field
¶
current_top_x: ParseInt = 0
Wire key TOPX
Top-ranking placement marker
alliance_id
pydantic-field
¶
alliance_id: int | None = None
Wire key AID
Alliance id; negative for none
alliance_emblem
pydantic-field
¶
alliance_emblem: AllianceEmblem | None = None
Wire key aee
The alliance's crest
remaining_protection_time
pydantic-field
¶
remaining_protection_time: ParseInt = 0
Wire key RPT
Seconds of peace protection left
castle_positions
pydantic-field
¶
castle_positions: list[OwnerCastlePosition]
Wire key AP
The player's castles
village_positions
pydantic-field
¶
village_positions: list[OwnerCastlePosition]
Wire key VP
The player's villages
is_searching_alliance
pydantic-field
¶
is_searching_alliance: bool = False
Wire key SA
Looking for an alliance
remaining_relocation_time
pydantic-field
¶
remaining_relocation_time: ParseInt = 0
Wire key RRD
Seconds until the player's castle relocation ends
KingdomProtection
pydantic-model
¶
Bases: BasePayload
The player's own protection in one kingdom: a uap block.
PMS and PMT mean the peace mode outside Berimond and the faction
protection in Berimond, as the client reads them. The client acts on the
block only for the kingdom it is in.
Client: CastleUserData.parse_UAP (bundle lines 9899-9906), which hands a
Berimond block to FactionEventVO.parse_uap (bundle line 7366)
Fields:
-
kingdom_id(int | None) -
noob_protection_seconds(ClientInt) -
protection_status(ClientInt) -
protection_seconds(ClientInt)
kingdom_id
pydantic-field
¶
kingdom_id: int | None = None
Wire key KID
The kingdom this protection is for
noob_protection_seconds
pydantic-field
¶
noob_protection_seconds: ClientInt = 0
Wire key NS
Seconds of beginner protection left in that kingdom
protection_status
pydantic-field
¶
protection_status: ClientInt = 0
Wire key PMS
Peace mode status (see peace_mode_status), or in Berimond the faction protection status
protection_seconds
pydantic-field
¶
protection_seconds: ClientInt = 0
Wire key PMT
Seconds left of the peace mode stage, or in Berimond of faction protection
is_noob_protected
property
¶
is_noob_protected: bool
Whether the player still has beginner protection there.
peace_mode_status
property
¶
peace_mode_status: PeaceModeStatus | None
protection_status as a peace mode stage; None for a value the client does not define.
MapArea
pydantic-model
¶
Bases: BasePayload
Map rows and the owner records of the players they name: the gaa block of a find reply.
Client: FNMCommand.executeCommand (bundle line 40185) reads gaa.OI
with parseOwnerInfoArray and gaa.AI with parseAreaInfos.
Fields:
Validators:
GetMapAreaResponse
pydantic-model
¶
Bases: BaseResponse
The map rows of a rectangle, the owner records of the players they name, and the player's own protection.
Command: gaa Response format: {"KID": 0, "AI": [[type, x, y, ...], ...], "OI": [{...}, ...], "uap": {"KID": .., "NS": .., "PMS": .., "PMT": ..}}
Each row is read in the reply's kingdom (see MapAreaItem). A scan
moves the session off the castle it had joined, so castle-scoped reads
after it must join the castle again (client.army methods do).
Client: GAACommand.executeCommand (bundle line 130112) reads uap
with parse_UAP, OI with parseOwnerInfoArray and AI with
parseAreaInfos.
Fields:
-
error_code(int) -
kingdom(Kingdom) -
items(list[MapAreaItem]) -
owners(list[MapObject]) -
protection(KingdomProtection | None)
Validators:
-
_parse_rows→items -
_parse_owners→owners -
_protection_needs_an_object→protection
model_config
class-attribute
¶
model_config: ConfigDict = ConfigDict(populate_by_name=True, extra='allow')
owners
pydantic-field
¶
Wire key OI
Owner records for the players the rows name
protection
pydantic-field
¶
protection: KingdomProtection | None = None
Wire key uap
The player's own protection in the kingdom
get_ruins
¶
Owner records flagged as ruins; their castles are in each record's castle_positions.
get_moving_flags
¶
The castles in this area that are on the move, by the relocating player's id.
See MapAreaItem.is_relocating; the owner record with that id
says how long the relocation has left. Whether (x, y) is where the
castle comes from or where it goes is not settled by the client.
Returns:
| Type | Description |
|---|---|
dict[int, tuple[int, int]]
|
Dict mapping player_id -> (x, y) |
FindNextMapObjectRequest
pydantic-model
¶
Bases: BaseRequest
Find the nearest map object of one area type, as the client's "show me" and "jump to" buttons do.
Command: fnm Payload: {"T": area_type, "KID": kingdom, "LMIN": min_level, "LMAX": max_level, "NID": owner_id}
The client asks this for robber baron camps (DUNGEON with LMIN and LMAX 1, bundle line 143000), alien camps (bundle lines 114204, 117711), faction invasion, samurai and nomad camps and the alliance raid portal, naming the NPC owner of the camps it wants (bundle lines 75451, 91575, 98463, 98544, 98951), and Berimond faction camps with LMIN -3 (bundle line 54675); quest jumps default LMIN to -3 too (bundle line 97692). What LMIN -3 asks for is not settled by the client. The server answers NO_PLAYER_FOUND (153) when nothing matches.
Client: C2SFindNextMapObjectVO (bundle line 8970), whose key order the
fields follow
Fields:
FindNextMapObjectResponse
pydantic-model
¶
Bases: BaseResponse
Where the object found lies, with the map rows around it.
Command: fnm Payload: {"gaa": {"AI": [...], "OI": [...]}, "X": x, "Y": y}
Client: FNMCommand.executeCommand (bundle line 40185), which centres
the map on X/Y with CastleWorldmapData.parseSearchInfos
(bundle line 19010).
Fields:
Validators:
-
_area_needs_an_object→area
FindNextEnemyCastleRequest
pydantic-model
¶
Bases: BaseRequest
Find an enemy castle near a position, as the client's "search enemy" button does.
Command: fec Payload: {"X": x, "Y": y, "N": index, "LMIN": min_level, "LMAX": max_level}
The button sends the selected map object's position and a counter mod 10
as N, so each press finds the next castle; quest help sends N 0
from your main castle with a minimum level. The server answers
NO_PLAYER_FOUND (153) when nothing matches.
Client: C2SFindNextEnemyCastleVO (bundle line 55304), sent by
SearchEnemyPanelButton.onButtonClicked (bundle line 108075) and
ShowHowToDoQuestconditionCommand.toNextEnemy / toNextDifferentEnemy
(bundle lines 97694-97698)
Fields:
FindNextEnemyCastleResponse
pydantic-model
¶
Bases: FindNextMapObjectResponse
Where the enemy castle found lies, with the map rows around it.
Command: fec Payload: {"gaa": {"AI": [...], "OI": [...]}, "X": x, "Y": y}
Client: FECCommand (bundle line 130098) extends FNMCommand
(bundle line 40185) and reads the reply as it does.
Fields:
Validators:
-
_area_needs_an_object→area
FindNextTowerRequest
pydantic-model
¶
Bases: BaseRequest
Find the next Berimond tower.
Command: fnt Payload: {}
The client sends it only in Berimond, from the "search enemy" button and quest help. The server answers NO_PLAYER_FOUND (153) when nothing matches.
Client: C2SFindNextTowerVO (bundle line 37533), sent by
SearchEnemyPanelButton.onButtonClicked (bundle line 108075),
ShowHowToDoQuestconditionCommand.toNextFactionTower (bundle line 97699)
and after switching to Berimond (bundle line 54636)
FindNextTowerResponse
pydantic-model
¶
Bases: FindNextMapObjectResponse
Where the tower found lies, with the map rows around it.
Command: fnt Payload: {"gaa": {"AI": [...], "OI": [...]}, "X": x, "Y": y}
Client: FNTCommand (bundle line 127833) extends FNMCommand
(bundle line 40185) and reads the reply as it does.
Fields:
Validators:
-
_area_needs_an_object→area
map.models.items¶
Map area items: the rows of a map area, one per map object, read by their area type's client parser.
Classes:
| Name | Description |
|---|---|
MapAreaItem |
One map row, read field by field as its area type's client parser reads it. |
Functions:
| Name | Description |
|---|---|
parse_area_rows |
Map rows as |
Attributes:
| Name | Type | Description |
|---|---|---|
ROW_PARSERS |
dict[MapItemType, Callable[[_Row], dict[str, Any]]]
|
|
INVASION_AREA_TYPES |
Invasion event camps: their row gives the camp's own wall, gate and moat protection. |
|
TargetRow |
A pre-calculation's |
ROW_PARSERS
module-attribute
¶
ROW_PARSERS: dict[MapItemType, Callable[[_Row], dict[str, Any]]] = {MapItemType.EMPTY: _position_only, MapItemType.CASTLE: _castle, MapItemType.DUNGEON: _dungeon, MapItemType.CAPITAL: _capital, MapItemType.OUTPOST: _interactive, MapItemType.TREASURE_DUNGEON: _dungeon, MapItemType.TREASURE_CAMP: _treasure_camp, MapItemType.SHADOW_AREA: _attack_area, MapItemType.VILLAGE: _village, MapItemType.BOSS_DUNGEON: _boss_dungeon, MapItemType.KINGDOM_CASTLE: _castle, MapItemType.EVENT_DUNGEON: _event_dungeon, MapItemType.FACTION_CAMP: _faction_camp, MapItemType.FACTION_VILLAGE: _faction_village, MapItemType.FACTION_TOWER: _faction_tower, MapItemType.FACTION_CAPITAL: _faction_capital, MapItemType.PLAGUE_AREA: _attack_area, MapItemType.ALIEN_CAMP: _alien_camp, MapItemType.METROPOL: _metropol, MapItemType.KINGS_TOWER: _kings_tower, MapItemType.ISLE_RESOURCE: _resource_isle, MapItemType.ISLE_DUNGEON: _isle_dungeon, MapItemType.MONUMENT: _monument, MapItemType.NOMAD_CAMP: _invasion_camp, MapItemType.LABORATORY: _laboratory, MapItemType.SAMURAI_CAMP: _invasion_camp, MapItemType.FACTION_INVASION_CAMP: _faction_invasion_camp, MapItemType.DYNAMIC: _position_only, MapItemType.RED_ALIEN_CAMP: _alien_camp, MapItemType.ALLIANCE_NOMAD_CAMP: _alliance_camp, MapItemType.DAIMYO_CASTLE: _daimyo, MapItemType.DAIMYO_TOWNSHIP: _daimyo, MapItemType.ALLIANCE_BATTLE_GROUND_RESOURCE_TOWER: _alliance_camp, MapItemType.ALLIANCE_BATTLE_GROUND_TOWER: _abg_tower, MapItemType.WOLF_KING: _wolf_king, MapItemType.ARE_PORTAL: _position_only}
INVASION_AREA_TYPES
module-attribute
¶
INVASION_AREA_TYPES = frozenset({MapItemType.SAMURAI_CAMP, MapItemType.NOMAD_CAMP, MapItemType.DAIMYO_CASTLE, MapItemType.DAIMYO_TOWNSHIP})
Invasion event camps: their row gives the camp's own wall, gate and moat protection.
TargetRow
module-attribute
¶
TargetRow = Annotated[MapAreaItem | None, BeforeValidator(_target_row)]
A pre-calculation's gaa.AI target row, None when the client could not read it.
Client: WorldmapObjectFactory.parseWorldMapArea (bundle line 5343)
AbgCastleConnection
pydantic-model
¶
Bases: BasePayload
An alliance battle ground castle's line to its tower: a castle row's [x, y, is_attackable, tower_points].
The client reads it only on a battle ground server that scores towers.
Client: CastleMapobjectVO.parseAreaInfo (bundle lines 18911-18912), ABGHelper.isOnABGAndTower
(bundle line 2037)
Fields:
-
x(ClientInt) -
y(ClientInt) -
is_attackable(bool) -
tower_points(ClientInt)
Validators:
-
_from_row
AbgTowerConnection
pydantic-model
¶
Bases: BasePayload
One of an alliance battle ground tower's connections: [x, y, player_name, status].
is_defeated is status 1: the client sums the statuses into _defeatedConnections and draws
status 1 in red.
Client: ABGAllianceTowerMapobjectVO.parseConnections (bundle lines 32285-32291),
ABGTowerConnectionVO.fillFromConnectionValues (bundle line 25482)
Fields:
-
x(ClientInt) -
y(ClientInt) -
player_name(str | None) -
is_defeated(bool)
Validators:
-
_from_row
MapAreaItem
pydantic-model
¶
Bases: BasePayload
One map row, read field by field as its area type's client parser reads it.
Every row starts [area_type, x, y]; what follows depends on the type
(see ROW_PARSERS). A field the type's row does not carry is None. The
client reads some values through int() and stores the rest as sent;
so does this model, so a value of the wrong kind where the client stores
it as sent makes the row unreadable.
Client: WorldmapObjectFactory.parseWorldMapArea (bundle line 5343);
CastleWorldmapData.parseAreaInfos (bundle line 18993) places each row
at int(row[1]), int(row[2]).
Fields:
-
item_type(MapItemType) -
x(int) -
y(int) -
kingdom(Kingdom) -
is_plot_row(bool) -
location_id(int | None) -
owner_id(int | None) -
name(str | None) -
keep_level(int | None) -
wall_level(int | None) -
gate_level(int | None) -
tower_level(int | None) -
moat_level(int | None) -
attack_cooldown_seconds(int | None) -
sabotage_cooldown_seconds(int | None) -
seconds_since_espionage(int | None) -
outpost_type(int | None) -
occupier_id(int | None) -
skin_id(int | None) -
has_sabotage_protection(bool | None) -
abg_tower_connection(AbgCastleConnection | None) -
abg_mine_out_seconds(int | None) -
abg_max_influence_points(int | None) -
is_destroyed(bool | None) -
village_type(int | None) -
monument_type(int | None) -
landmark_level(int | None) -
isle_id(int | None) -
remaining_occupier_seconds(int | None) -
protector_positions(list[Any] | None) -
dungeon_level(int | None) -
attacks_left(int | None) -
special_camp_id(int | None) -
victory_count(int | None) -
defeater_player_id(int | None) -
is_defeated(bool | None) -
map_id(int | None) -
has_peace_mode(bool | None) -
already_rerolled(bool | None) -
scaling_camp_id(int | None) -
dungeon_type(int | None) -
camp_id(int | None) -
total_cooldown_seconds(int | None) -
skip_cost(int | None) -
base_wall_bonus(float | None) -
base_gate_bonus(float | None) -
base_moat_bonus(float | None) -
is_attackable(bool | None) -
alliance_id(int | None) -
alliance_name(str | None) -
alliance_crest(AllianceCrest | None) -
abg_connections(tuple[AbgTowerConnection, ...] | None) -
raw_data(list[Any])
kingdom
pydantic-field
¶
The kingdom the row names, else the kingdom of the reply it came in
is_plot_row
pydantic-field
¶
is_plot_row: bool = False
A castle row of four fields or fewer: a free plot, or a castle on the move
location_id
pydantic-field
¶
location_id: int | None = None
The map object's id; negative for an unclaimed plot
owner_id
pydantic-field
¶
owner_id: int | None = None
Player id of the owner the row names; negative for an NPC owner such as an unclaimed outpost
attack_cooldown_seconds
pydantic-field
¶
attack_cooldown_seconds: int | None = None
Seconds until it can be attacked again; negative once that has passed
sabotage_cooldown_seconds
pydantic-field
¶
sabotage_cooldown_seconds: int | None = None
Seconds until it can be sabotaged again
seconds_since_espionage
pydantic-field
¶
seconds_since_espionage: int | None = None
Seconds since it was last spied; negative when it never was
occupier_id
pydantic-field
¶
occupier_id: int | None = None
Player id of the occupier, -1 for none; on a four-field castle row, the player relocating there
has_sabotage_protection
pydantic-field
¶
has_sabotage_protection: bool | None = None
Temporary sabotage protection is on
abg_tower_connection
pydantic-field
¶
abg_tower_connection: AbgCastleConnection | None = None
On an alliance battle ground: the castle's tower connection
abg_mine_out_seconds
pydantic-field
¶
abg_mine_out_seconds: int | None = None
Alliance battle ground: seconds until the metropolis is mined out
abg_max_influence_points
pydantic-field
¶
abg_max_influence_points: int | None = None
Alliance battle ground: the metropolis's most influence points
landmark_level
pydantic-field
¶
landmark_level: int | None = None
A monument's or laboratory's level
remaining_occupier_seconds
pydantic-field
¶
remaining_occupier_seconds: int | None = None
Seconds left on the occupier's timer
protector_positions
pydantic-field
¶
Positions of the faction object's protectors still standing
dungeon_level
pydantic-field
¶
dungeon_level: int | None = None
The camp's level as its row gives it
defeater_player_id
pydantic-field
¶
defeater_player_id: int | None = None
Player id of whoever defeated the boss dungeon
already_rerolled
pydantic-field
¶
already_rerolled: bool | None = None
The alien camp was already rerolled
scaling_camp_id
pydantic-field
¶
scaling_camp_id: int | None = None
The difficulty-scaling camp that sets the camp's level; -1 or 0 when none
dungeon_type
pydantic-field
¶
dungeon_type: int | None = None
The faction king whose invasion camp this is
camp_id
pydantic-field
¶
camp_id: int | None = None
The camp's row in its event's camp table (a daimyo rank, an alliance camp)
total_cooldown_seconds
pydantic-field
¶
total_cooldown_seconds: int | None = None
The camp's full cooldown
base_wall_bonus
pydantic-field
¶
base_wall_bonus: float | None = None
The camp's wall protection, as a percentage
base_gate_bonus
pydantic-field
¶
base_gate_bonus: float | None = None
The camp's gate protection, as a percentage
base_moat_bonus
pydantic-field
¶
base_moat_bonus: float | None = None
The camp's moat protection, as a percentage
alliance_name
pydantic-field
¶
alliance_name: str | None = None
Name of the alliance holding the tower
alliance_crest
pydantic-field
¶
alliance_crest: AllianceCrest | None = None
Crest of the alliance holding the tower
abg_connections
pydantic-field
¶
abg_connections: tuple[AbgTowerConnection, ...] | None = None
The alliance tower's connections to the castles around it
is_occupied
property
¶
is_occupied: bool
Whether a player occupies it; the client's isOccupied is an occupier id above -1.
is_relocating
property
¶
is_relocating: bool
Whether this is a castle on the move: a four-field castle row naming the relocating player.
occupier_id is that player; the owner record's remaining_relocation_time
says when it arrives. [1, x, y, -1] is a free plot instead.
Client: CastleMapobjectVO.parseAreaInfo (bundle line 18910) queues such a row
as a relocation, and CastleWorldmapData.getExpiredRelocationObject (bundle
line 19023) ends it once that player's relocation time runs out.
has_player_owner
property
¶
has_player_owner: bool
Whether the row names a player as its owner, rather than an NPC or nobody.
is_invasion_camp
property
¶
is_invasion_camp: bool
Whether this row is an invasion event camp, whose row gives its own protection.
is_castle
property
¶
is_castle: bool
Whether this is a castle, capital, outpost, kingdom castle or metropolis.
from_list
classmethod
¶
from_list(data: Any, kingdom: Kingdom = GREEN) -> MapAreaItem
Read a map row; kingdom is the kingdom of the reply it came in.
Raises:
| Type | Description |
|---|---|
ValueError
|
The row is not a list, is empty, or has an area type
the client reads no row of; |
row_values
staticmethod
¶
The values from_list builds a map row from, by field name, before they are validated.
A caller that drops most rows can look at a row's values first and build only the rows it keeps.
Raises:
| Type | Description |
|---|---|
ValueError
|
The row is not a list, is empty, or has an area type the client reads no row of |
parse_area_rows
¶
Map rows as MapAreaItem, with how many rows could not be read.
kingdom is the kingdom of the reply the rows came in. A row that is not
a list, is empty, has an area type the client reads no row of, or has a
value of the wrong kind is counted and skipped, so one bad row costs only
itself. The client instead stops reading at an empty row.
Client: CastleWorldmapData.parseAreaInfos (bundle line 18993) hands
each row to WorldmapObjectFactory.parseWorldMapArea (bundle line 5343).
map.models.owners¶
Owner records and the pieces they share with movements and player profiles.
A player's crest, faction standing, castle positions and alliance crest.
Classes:
| Name | Description |
|---|---|
OwnerCrest |
A player's crest: an owner record's |
OwnerFaction |
Faction event standing: an owner record's |
OwnerCastlePosition |
One of an owner's castles or villages: an entry of |
AllianceCrest |
An alliance's crest: a layout and its colours. |
AllianceEmblem |
The alliance crest block of an owner record: its |
Functions:
| Name | Description |
|---|---|
owner_positions |
An owner record's |
OwnerCrest
pydantic-model
¶
Bases: BasePayload
A player's crest: an owner record's E.
Client: CrestVO.loadFromParamObject (bundle line 10587).
Fields:
-
is_set(bool) -
symbol_type(ClientInt) -
symbol1(ClientInt) -
symbol1_color(ClientInt) -
symbol2(ClientInt) -
symbol2_color(ClientInt) -
background_type(ClientInt) -
background_color1(ClientInt) -
background_color2(ClientInt)
Validators:
-
_truthy→is_set
OwnerFaction
pydantic-model
¶
Bases: BasePayload
Faction event standing: an owner record's FN.
Client: WorldMapOwnerInfoVO.fillFromParamObject (bundle line 10794), which reads each key through parseInt.
Fields:
-
faction_id(ParseInt) -
protection_status(ParseInt) -
protection_end_seconds(ParseInt) -
title_id(EnumOrInt['Title'] | None)
Validators:
-
_title→title_id
OwnerCastlePosition
¶
Bases: NamedTuple
One of an owner's castles or villages: an entry of AP or VP, [kingdom_id, area_id, x, y, area_type].
A plain named tuple rather than a model: a kingdom scan keeps tens of thousands.
kingdom_id and area_id are read through the client's int(); area_type
is None when the entry has none.
Client: MinWorldMapCastleInfoVO.fillFromParamObject (bundle line 18459).
Attributes:
| Name | Type | Description |
|---|---|---|
kingdom_id |
ClientInt
|
|
area_id |
ClientInt
|
|
x |
int
|
|
y |
int
|
|
area_type |
int | None
|
|
AllianceCrest
pydantic-model
¶
Bases: BasePayload
An alliance's crest: a layout and its colours.
Client: AllianceCrestVO.fillWithData (bundle line 11233); fillFromArray
(bundle line 11237) reads the same two values from [layout_id, color_ids]; each colour is
looked up through int() (bundle line 11216).
Fields:
-
layout_id(EnumOrInt['AllianceCrestLayout'] | None) -
color_ids(list[Annotated[EnumOrInt['AllianceCrestColor'], BeforeValidator(js_int)]])
Validators:
AllianceEmblem
pydantic-model
¶
Bases: BasePayload
The alliance crest block of an owner record: its aee.
Client: WorldMapOwnerInfoVO.fillFromParamObject (bundle line 10794)
reads only ACCA, and only for a player in an alliance.
Fields:
-
crest(AllianceCrest | None)
Validators:
-
_crest_needs_an_object→crest
crest
pydantic-field
¶
crest: AllianceCrest | None = None
Wire key ACCA
The alliance's current crest
owner_positions
¶
owner_positions(value: Any) -> list[OwnerCastlePosition]
An owner record's AP or VP rows, as WorldMapOwnerInfoVO.parsePosList (bundle line 10795) reads them.
Only the first five entries of a row are read. A row the server wraps in one extra list (seen on AP in Berimond) is unwrapped, which the client does not do; a row that still cannot be read (fewer than four entries, or a position that is not a number) is skipped instead of failing the record.
A row of plain ints, the shape live replies carry, is taken as it is; only another row goes through pydantic, one call per row.