Skip to content

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

MapService(client)

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

pacer instance-attribute

pacer = MapAreaPacer(lambda: client.config.map_tiles_per_second)

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 EmpireClient.send

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 kingdom; None when nothing matches (NO_PLAYER_FOUND)

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

index is not 0 to 9

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: items and objects stay empty when it handed each chunk to on_chunk.

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 on_chunk.

logger module-attribute

logger = logging.getLogger(__name__)

ChunkHandler module-attribute

ChunkHandler = Callable[[tuple[int, int], list[MapAreaItem], dict[int, MapObject]], None]

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], ...]

items instance-attribute

items: list[MapAreaItem]

objects instance-attribute

objects: dict[int, MapObject]

kingdom instance-attribute

kingdom: Kingdom

failed_chunks class-attribute instance-attribute

failed_chunks: tuple[tuple[int, int], ...] = ()

content_chunks class-attribute instance-attribute

content_chunks: tuple[tuple[int, int], ...] = ()

MapScanner

MapScanner(client: _Client)

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

CHUNK_SIZE class-attribute instance-attribute

CHUNK_SIZE = 90

MAX_COORD class-attribute instance-attribute

MAX_COORD = 20

CHUNK_RETRIES class-attribute instance-attribute

CHUNK_RETRIES = 2

RETRY_BACKOFF class-attribute instance-attribute

RETRY_BACKOFF = 0.5

client instance-attribute

client = 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.CASTLE in the green kingdom and MapItemType.KINGDOM_CASTLE in 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().

chunk_for_position

chunk_for_position(x: int, y: int) -> tuple[int, int]

Map world coordinates to the chunk that contains them.

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

clients is empty, or names one client twice

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 OI list.

KingdomProtection

The player's own protection in one kingdom: a uap block.

MapArea

Map rows and the owner records of the players they name: the gaa block of a find reply.

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 fec asks for; the client cycles 0 to 9.

logger module-attribute

logger = logging.getLogger(__name__)

MAX_FINDABLE_ENEMY_INDEX module-attribute

MAX_FINDABLE_ENEMY_INDEX = 9

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:

kingdom pydantic-field

kingdom: Kingdom = Kingdom.GREEN

Wire key KID

The kingdom to read

x1 pydantic-field

x1: int

Wire key AX1

First corner's map x

y1 pydantic-field

y1: int

Wire key AY1

First corner's map y

x2 pydantic-field

x2: int

Wire key AX2

Second corner's map x

y2 pydantic-field

y2: int

Wire key AY2

Second corner's map y

tiles property

tiles: int

The number of map tiles the rectangle covers, corners included.

accepts_reply

accepts_reply(payload: Any) -> bool

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:

Validators:

owner_id pydantic-field

owner_id: int | None = None

Wire key OID

Player id; negative for an NPC

is_dummy pydantic-field

is_dummy: bool = False

Wire key DUM

The record stands in for a player not loaded

owner_name pydantic-field

owner_name: str | None = None

Wire key N

Player name

emblem pydantic-field

emblem: OwnerCrest | None = None

Wire key E

The player's crest

level pydantic-field

level: ParseInt = 0

Wire key L

Player level

legendary_level pydantic-field

legendary_level: ParseInt = 0

Wire key LL

Legendary level

honor pydantic-field

honor: ParseInt = 0

Wire key H

Honor

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

might_points pydantic-field

might_points: ParseInt = 0

Wire key MP

Might points

is_ruin pydantic-field

is_ruin: bool = False

Wire key R

The player's castles are ruins

alliance_id pydantic-field

alliance_id: int | None = None

Wire key AID

Alliance id; negative for none

alliance_rank pydantic-field

alliance_rank: ParseInt = 0

Wire key AR

Rank in the alliance

alliance_name pydantic-field

alliance_name: str = ''

Wire key AN

Alliance name

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

has_vip_flag pydantic-field

has_vip_flag: bool = False

Wire key VF

VIP flag

has_premium_flag pydantic-field

has_premium_flag: bool = False

Wire key PF

Premium flag

remaining_relocation_time pydantic-field

remaining_relocation_time: ParseInt = 0

Wire key RRD

Seconds until the player's castle relocation ends

remaining_noob_protection pydantic-field

remaining_noob_protection: ParseInt = 0

Wire key RNP

Seconds of beginner protection left

faction pydantic-field

faction: OwnerFaction | None = None

Wire key FN

Faction event standing

via_refer_a_friend pydantic-field

via_refer_a_friend: bool = False

Wire key IRF

Joined through refer-a-friend

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 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:

items pydantic-field

items: list[MapAreaItem]

Wire key AI

The map rows

owners pydantic-field

owners: list[MapObject]

Wire key OI

Owner records for the rows

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:

Validators:

model_config class-attribute

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

kingdom pydantic-field

kingdom: Kingdom = Kingdom.GREEN

Wire key KID

The kingdom the area lies in

items pydantic-field

items: list[MapAreaItem]

Wire key AI

The area's map rows

owners pydantic-field

owners: list[MapObject]

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

get_ruins() -> list[MapObject]

Owner records flagged as ruins; their castles are in each record's castle_positions.

get_moving_flags

get_moving_flags() -> dict[int, tuple[int, int]]

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:

area_type pydantic-field

area_type: MapItemType

Wire key T

The area type to look for

kingdom pydantic-field

kingdom: Kingdom = Kingdom.GREEN

Wire key KID

The kingdom to look in

min_level pydantic-field

min_level: int = -1

Wire key LMIN

Lowest level to match, -1 for any

max_level pydantic-field

max_level: int = -1

Wire key LMAX

Highest level to match, -1 for any

owner_id pydantic-field

owner_id: int = -1

Wire key NID

The NPC owner to match, -1 for any

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

x pydantic-field

x: int = 0

Wire key X

Map x of the object found

y pydantic-field

y: int = 0

Wire key Y

Map y of the object found

area pydantic-field

area: MapArea

Wire key gaa

The map rows around it

found

found() -> MapAreaItem | None

The row at (x, y), when the reply includes it.

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:

x pydantic-field

x: int

Wire key X

Map x to search from

y pydantic-field

y: int

Wire key Y

Map y to search from

index pydantic-field

index: int = 0

Wire key N

Which castle to find, 0 for the first, up to 9

min_level pydantic-field

min_level: int = -1

Wire key LMIN

Lowest level to match, -1 for any

max_level pydantic-field

max_level: int = -1

Wire key LMAX

Highest level to match, -1 for any

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 MapAreaItem, with how many rows could not be read.

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 gaa.AI target row, None when the client could not read it.

logger module-attribute

logger = logging.getLogger(__name__)

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:

Validators:

  • _from_row

x pydantic-field

x: ClientInt = 0

Map x of the other end

y pydantic-field

y: ClientInt = 0

Map y of the other end

is_attackable pydantic-field

is_attackable: bool = False

The castle can be attacked for tower points

tower_points pydantic-field

tower_points: ClientInt = 0

The player's tower points

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:

Validators:

  • _from_row

x pydantic-field

x: ClientInt = 0

Map x of the connected castle

y pydantic-field

y: ClientInt = 0

Map y of the connected castle

player_name pydantic-field

player_name: str | None = None

The connected castle's player; None when not sent

is_defeated pydantic-field

is_defeated: bool = False

The connection is defeated

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 pydantic-field

item_type: MapItemType

The row's area type

x pydantic-field

x: int = 0

Map x

y pydantic-field

y: int = 0

Map y

kingdom pydantic-field

kingdom: Kingdom = Kingdom.GREEN

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

name pydantic-field

name: str | None = None

The object's name

keep_level pydantic-field

keep_level: int | None = None

Keep level

wall_level pydantic-field

wall_level: int | None = None

Wall level

gate_level pydantic-field

gate_level: int | None = None

Gate level

tower_level pydantic-field

tower_level: int | None = None

Tower level

moat_level pydantic-field

moat_level: int | None = None

Moat level

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

outpost_type pydantic-field

outpost_type: int | None = None

Outpost type

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

skin_id pydantic-field

skin_id: int | None = None

Unique id of the castle skin equipment it shows

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

is_destroyed pydantic-field

is_destroyed: bool | None = None

The faction object is destroyed

village_type pydantic-field

village_type: int | None = None

Village type

monument_type pydantic-field

monument_type: int | None = None

Monument type

landmark_level pydantic-field

landmark_level: int | None = None

A monument's or laboratory's level

isle_id pydantic-field

isle_id: int | None = None

Isle blueprint id

remaining_occupier_seconds pydantic-field

remaining_occupier_seconds: int | None = None

Seconds left on the occupier's timer

protector_positions pydantic-field

protector_positions: list[Any] | None = None

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

attacks_left pydantic-field

attacks_left: int | None = None

Attacks the faction tower has left

special_camp_id pydantic-field

special_camp_id: int | None = None

The faction event camp id

victory_count pydantic-field

victory_count: int | None = None

How often the camp has been beaten

defeater_player_id pydantic-field

defeater_player_id: int | None = None

Player id of whoever defeated the boss dungeon

is_defeated pydantic-field

is_defeated: bool | None = None

The dungeon is defeated

map_id pydantic-field

map_id: int | None = None

The treasure map a treasure camp leads to

has_peace_mode pydantic-field

has_peace_mode: bool | None = None

The alien camp is in peace mode

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

skip_cost pydantic-field

skip_cost: int | None = None

What skipping the cooldown costs

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

is_attackable pydantic-field

is_attackable: bool | None = None

The alliance tower can be attacked

alliance_id pydantic-field

alliance_id: int | None = None

Id of the alliance holding the tower

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

raw_data pydantic-field

raw_data: list[Any]

The whole row as sent

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; ValidationError is one, for a value of the wrong kind

row_values staticmethod

row_values(data: Any, kingdom: Kingdom = GREEN) -> dict[str, Any]

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

parse_area_rows(value: Any, kingdom: Kingdom = GREEN) -> tuple[list[MapAreaItem], int]

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 E.

OwnerFaction

Faction event standing: an owner record's FN.

OwnerCastlePosition

One of an owner's castles or villages: an entry of AP or VP, [kingdom_id, area_id, x, y, area_type].

AllianceCrest

An alliance's crest: a layout and its colours.

AllianceEmblem

The alliance crest block of an owner record: its aee.

Functions:

Name Description
owner_positions

An owner record's AP or VP rows, as WorldMapOwnerInfoVO.parsePosList (bundle line 10795) reads them.

OwnerCrest pydantic-model

Bases: BasePayload

A player's crest: an owner record's E.

Client: CrestVO.loadFromParamObject (bundle line 10587).

Fields:

Validators:

is_set pydantic-field

is_set: bool = False

Wire key IS

False means the tutorial crest is shown

symbol_type pydantic-field

symbol_type: ClientInt = 0

Wire key SPT

Symbol layout type

symbol1 pydantic-field

symbol1: ClientInt = 0

Wire key S1

First symbol id

symbol1_color pydantic-field

symbol1_color: ClientInt = 0

Wire key SC1

First symbol's colour

symbol2 pydantic-field

symbol2: ClientInt = 0

Wire key S2

Second symbol id

symbol2_color pydantic-field

symbol2_color: ClientInt = 0

Wire key SC2

Second symbol's colour

background_type pydantic-field

background_type: ClientInt = 0

Wire key BGT

Background layout type

background_color1 pydantic-field

background_color1: ClientInt = 0

Wire key BGC1

First background colour

background_color2 pydantic-field

background_color2: ClientInt = 0

Wire key BGC2

Second background colour

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:

Validators:

faction_id pydantic-field

faction_id: ParseInt = 0

Wire key FID

Faction id

protection_status pydantic-field

protection_status: ParseInt = 0

Wire key PMS

Faction protection status

protection_end_seconds pydantic-field

protection_end_seconds: ParseInt = 0

Wire key PMT

Seconds until faction protection ends

title_id pydantic-field

title_id: EnumOrInt['Title'] | None = None

Wire key TID

The faction title; None when TID is -1 or not a number

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

kingdom_id instance-attribute

kingdom_id: ClientInt

area_id instance-attribute

area_id: ClientInt

x instance-attribute

x: int

y instance-attribute

y: int

area_type class-attribute instance-attribute

area_type: int | None = 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:

Validators:

layout_id pydantic-field

layout_id: EnumOrInt['AllianceCrestLayout'] | None = None

Wire key ACLI

The crest layout; None when the crest names none (0)

color_ids pydantic-field

color_ids: list[Annotated[EnumOrInt['AllianceCrestColor'], BeforeValidator(js_int)]]

Wire key ACCS

The colours, one per layout colour

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:

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.