Spy¶
Spy missions, risk planning and reports behind client.spy.
spy.service¶
Espionage: spy missions, sabotage and spy reports.
Classes:
| Name | Description |
|---|---|
SpyOutcome |
How a spy mission ended. |
SpyStep |
The command a spy mission was at when it ended. |
SpyResult |
Outcome of a spy mission. |
SpyHandle |
A spy mission sent by |
SpyService |
Spy missions planned for risk, sabotage, and reading spy reports. |
SpyOutcome
¶
How a spy mission ended.
Attributes:
| Name | Type | Description |
|---|---|---|
SUCCESS |
The report was read. |
|
SENT |
The mission was sent; its report is not waited for. |
|
NO_SPIES_AVAILABLE |
No spy was free, after polling |
|
RISK_OVER_BUDGET |
Even the whole pool stays above the risk ceiling, so nothing was sent. |
|
COMMAND_FAILED |
A request failed; |
|
TIMEOUT |
No report for this mission arrived before the deadline. |
|
REPORT_TARGET_MISMATCH |
Only reports for other areas of the target's owner arrived before the deadline. |
|
DISCONNECTED |
The connection dropped while waiting for the report. |
|
CANCELLED |
The caller cancelled; |
|
SPY_CAUGHT |
The spies were caught or kept away. |
|
NO_SPY_DATA |
The mission brought no army back, or the server has no report ( |
SENT
class-attribute
instance-attribute
¶
The mission was sent; its report is not waited for.
NO_SPIES_AVAILABLE
class-attribute
instance-attribute
¶
No spy was free, after polling ssi for returning spies.
RISK_OVER_BUDGET
class-attribute
instance-attribute
¶
Even the whole pool stays above the risk ceiling, so nothing was sent.
COMMAND_FAILED
class-attribute
instance-attribute
¶
A request failed; SpyResult.error holds why and step which one.
TIMEOUT
class-attribute
instance-attribute
¶
No report for this mission arrived before the deadline.
REPORT_TARGET_MISMATCH
class-attribute
instance-attribute
¶
Only reports for other areas of the target's owner arrived before the deadline.
DISCONNECTED
class-attribute
instance-attribute
¶
The connection dropped while waiting for the report.
CANCELLED
class-attribute
instance-attribute
¶
The caller cancelled; SpyResult.mission is set when the spies had
already left, and they keep going until recalled.
SPY_CAUGHT
class-attribute
instance-attribute
¶
The spies were caught or kept away.
NO_SPY_DATA
class-attribute
instance-attribute
¶
The mission brought no army back, or the server has no report (bsd
error 130 or 66, then in SpyResult.error); the area is not known to
be empty.
SpyStep
¶
SpyResult
dataclass
¶
SpyResult(outcome: SpyOutcome, step: SpyStep | None = None, error: EmpireError | None = None, message_id: int | None = None, report: SpyReportResponse | None = None, mission: SendSpyResponse | None = None)
Outcome of a spy mission.
Attributes:
| Name | Type | Description |
|---|---|---|
outcome |
SpyOutcome
|
How the mission ended; |
step |
SpyStep | None
|
The command the mission ended at; None when it ended between commands (no spies, risk) or was read to the end. |
error |
EmpireError | None
|
The exception that ended the mission, for |
message_id |
int | None
|
The report's message id, once a report for the mission arrived. |
report |
SpyReportResponse | None
|
The spy report, when one was read. |
mission |
SendSpyResponse | None
|
The csm reply for the mission sent, with its movement and risk. |
SpyHandle
dataclass
¶
SpyHandle(target_x: int, target_y: int, target_kingdom: Kingdom, spy_type: SpyType, cancel_event: Event = Event(), mission: SendSpyResponse | None = None, arrival_eta: float | None = None, result: SpyResult | None = None)
A spy mission sent by SpyService.send_instant_spy, to pass to SpyService.await_report.
Until it is awaited or cancelled the service listens to sne and
keeps the spy logs that may be its report; a handle left alone stops
listening 10 minutes after its report was due.
Attributes:
| Name | Type | Description |
|---|---|---|
target_x |
int
|
Target X coordinate |
target_y |
int
|
Target Y coordinate |
target_kingdom |
Kingdom
|
Target kingdom |
spy_type |
SpyType
|
MILITARY or ECO |
cancel_event |
Event
|
Set to cancel the mission's wait, as |
mission |
SendSpyResponse | None
|
The csm reply; None when nothing was sent |
arrival_eta |
float | None
|
|
result |
SpyResult | None
|
How the mission ended: set when nothing was sent, when it was
cancelled before sending, and by |
Methods:
| Name | Description |
|---|---|
cancel |
Stop waiting for this mission: |
cancel_event
class-attribute
instance-attribute
¶
movement_id
property
¶
movement_id: int | None
The spy movement's id, for client.movements.recall; None when nothing was sent.
target
property
¶
target: MovementRecord | None
The csm reply's movement, whose target area a report is matched against.
cancel
¶
Stop waiting for this mission: await_report returns CANCELLED at once.
The spies are not recalled. They arrive and their report comes as
usual: until 10s after their arrival the mission keeps its place in
the order reports are given out, and the sne subscription, so its
report is dropped rather than read as another mission's. To turn them back, recall the movement with
client.movements.recall(handle.movement_id), which the client
allows only for your own spies still heading to the target.
Client: SpyMapmovementVO.canBeRetreated (bundle line 43759),
CastleAskRetreatDialog.onClick (bundle line 33234)
SpyService
¶
SpyService(client: EmpireClient)
Bases: BaseService
Spy missions planned for risk, sabotage, and reading spy reports.
Methods:
| Name | Description |
|---|---|
forward_report |
Share a spy report with other players in game. |
get_screen_info |
What a mission against a target would face: its guards, your free spies and the target's map row. |
get_report |
Read any spy report in the mailbox by its message id. |
auto_spy |
Spy a target at once with the auto-spy subscription: no spies, no movement, no mail. |
spies_in_use |
Spies on your own spy movements, out or on their way home, as the state tracks them. |
total_spies |
All your spies, home or out, counted as the client does: the |
available_spies |
Spies at home: |
send_spy_mission |
Send one spy mission as given, without planning it or waiting for its report. |
send_instant_spy |
Plan and send a military or economy spy mission, without waiting for its report. |
await_report |
Wait for a sent mission's report and read it. |
execute_instant_spy |
Send a military or economy spy mission and read its report. |
send_sabotage |
Send a sabotage mission with the fewest spies that reach the pool's lowest risk. |
forward_report
¶
Share a spy report with other players in game.
Returns False when the server rejects it — a report can age out of the mailbox, and the recipients may no longer be reachable. A recipient who has the report already (error 167) counts as done, as in the client.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
message_id
|
int
|
The report's message id: |
required |
player_ids
|
list[int]
|
The recipients. The client offers the members of your alliance
other than you, |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
|
Client: CastleForwardMessageDialog.fillList and sendMessage (bundle lines 60719, 60740),
checkEnableForwardButton (bundle line 60753), MFSCommand.executeCommand (bundle line 125409)
get_screen_info
¶
get_screen_info(target_x: int, target_y: int, target_kingdom: Kingdom = GREEN) -> SpyScreenInfoResponse
What a mission against a target would face: its guards, your free spies and the target's map row.
Raises:
| Type | Description |
|---|---|
CommandError
|
The server refused, e.g. for a target you cannot spy |
Client: C2SGetSpyInfo (bundle line 22504), CastleSpyData.parse_SSI (bundle line 139962)
get_report
¶
get_report(message_id: int) -> SpyReportResponse | None
Read any spy report in the mailbox by its message id.
Returns None when the server has no report for it (error 130 NO_SPY_DATA or 66 NO_SUCH_MESSAGE), which the client shows as "no spy data".
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
message_id
|
int
|
A spy log's |
required |
Raises:
| Type | Description |
|---|---|
CommandError
|
The server refused for another reason |
Client: CastleSpyData.getSpyLog (bundle line 139963), BSDCommand.executeCommand
(bundle line 125223)
auto_spy
¶
auto_spy(target_x: int, target_y: int) -> AutoSpyResponse
Spy a target at once with the auto-spy subscription: no spies, no movement, no mail.
The client offers this only while the subscription covers the target; what the server answers without it is untested.
Raises:
| Type | Description |
|---|---|
CommandError
|
The server refused |
Client: ButtonAutoSpyComponent.onClick (bundle line 109977), SSUCommand.executeCommand
(bundle line 128568)
spies_in_use
¶
spies_in_use() -> int
Spies on your own spy movements, out or on their way home, as the state tracks them.
Client: CastleSpyData.getNumAvailableSpies (bundle line 139970), which counts the
spyCount of every SpyMapmovementVO you own
total_spies
¶
total_spies(*, research_ids: Iterable[int] | None = None, legend_skill_ids: Iterable[int] | None = None, title_ids: Iterable[int] | None = None, island_title_id: int | None = None, legend_target: bool = False) -> int | None
All your spies, home or out, counted as the client does: the gms count plus boosts.
The count is for the whole account, not one castle. Each boost left as None is read
from state: your finished research (rei), legend skills (skl), and the glory,
Berimond and Storm Islands titles your points and ranks give (ufa, ufp, uar).
Pass a value to count that instead, () for none. A boost reads
the game data, loaded on first use; after login the state's titles always count.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
research_ids
|
Iterable[int] | None
|
Your finished research ids, the |
None
|
legend_skill_ids
|
Iterable[int] | None
|
|
None
|
title_ids
|
Iterable[int] | None
|
Every glory and Berimond title you hold, each one below your current title included, as the client lists them |
None
|
island_title_id
|
int | None
|
Your Storm Islands title, -1 for none; the titles below it count too |
None
|
legend_target
|
bool
|
Add legend skills: the client does for a target whose owner is a legend or that is a landmark, for your own legend status, and always in the attack screen's spy alert |
False
|
Returns:
| Type | Description |
|---|---|
int | None
|
The count, or None before the login gbd brought |
Raises:
| Type | Description |
|---|---|
GameDataNotLoadedError
|
A boost counts and the game data cannot be loaded |
Client: CastleSpyData.getNumAllSpies (bundle line 139976), called with the target's
ownerInfo.isLegend (bundle lines 34325, 34342, 127082) or userData.isLegend
(bundle line 102169); CastleTitleSystemHelper.returnTitleEffectValue (bundle line 4420)
over CastleTitleData.thisUsersTitles (bundle line 21073)
available_spies
¶
available_spies(*, research_ids: Iterable[int] | None = None, legend_skill_ids: Iterable[int] | None = None, title_ids: Iterable[int] | None = None, island_title_id: int | None = None, legend_target: bool = False) -> int | None
Spies at home: total_spies less spies_in_use, as the client counts them.
The client uses this count to offer the spy button. The spy dialog
itself sends with the ssi reply's available_spies, as
execute_instant_spy does.
Takes the arguments of total_spies, read from state the same way.
Returns:
| Type | Description |
|---|---|
int | None
|
The count, which can be negative as in the client, or None before |
Client: CastleSpyData.getNumAvailableSpies (bundle line 139970)
send_spy_mission
¶
send_spy_mission(source_castle_id: int, target_x: int, target_y: int, target_kingdom: Kingdom = GREEN, *, spy_type: SpyType, spies: int, accuracy_or_damage: int, horse_booster_id: int = -1, feathers: bool = False, slowdown: int = 0, spend_rubies: bool = False) -> SendSpyResponse
Send one spy mission as given, without planning it or waiting for its report.
A horse that costs rubies, unless paid with feathers, and a slowdown spend rubies; see "Spending rubies" in the guides.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source_castle_id
|
int
|
The castle the spies leave from, one of yours |
required |
target_x
|
int
|
Target X coordinate |
required |
target_y
|
int
|
Target Y coordinate |
required |
target_kingdom
|
Kingdom
|
Target kingdom |
GREEN
|
spy_type
|
SpyType
|
MILITARY, ECO or SABOTAGE; plague monks are not sent this way |
required |
spies
|
int
|
How many spies to send |
required |
accuracy_or_damage
|
int
|
Accuracy percent (50-100), or damage percent (10-50) for sabotage |
required |
horse_booster_id
|
int
|
A horse's wod id to speed the spies up (-1 = none) |
-1
|
feathers
|
bool
|
Use the instant spy horse and pay for it with feathers; wins over |
False
|
slowdown
|
int
|
Seconds to delay the arrival by; costs rubies |
0
|
spend_rubies
|
bool
|
Allow a horse or slowdown that costs rubies |
False
|
Raises:
| Type | Description |
|---|---|
ValueError
|
For |
ValueError
|
The horse or slowdown costs rubies and |
GameDataNotLoadedError
|
A horse is picked without feathers and the game data cannot be loaded |
CommandError
|
The server refused the mission |
Client: CastlePostSpyDialog.spyCastle (bundle line 38459), C2SCreateSpyMovementVO
(bundle line 100126)
send_instant_spy
¶
send_instant_spy(source_castle_id: int, target_x: int, target_y: int, target_kingdom: Kingdom = GREEN, risk_tolerance: int | None = None, accuracy: int = MAX_ACCURACY, *, spy_type: SpyType = MILITARY, horse_booster_id: int = -1, feathers: bool = False, slowdown: int = 0, spend_rubies: bool = False, wait_for_spies: float | None = None, cancel: Event | None = None) -> SpyHandle
Plan and send a military or economy spy mission, without waiting for its report.
Plans and sends as execute_instant_spy does, which is this and
await_report. The handle listens for the report from before
csm is sent, so a report that comes before await_report is called
is kept for it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source_castle_id
|
int
|
The castle the spies leave from, one of yours |
required |
target_x
|
int
|
Target X coordinate |
required |
target_y
|
int
|
Target Y coordinate |
required |
target_kingdom
|
Kingdom
|
Target kingdom |
GREEN
|
risk_tolerance
|
int | None
|
Ceiling on the chance of being caught, as a percentage |
None
|
accuracy
|
int
|
Spy accuracy (50-100) |
MAX_ACCURACY
|
spy_type
|
SpyType
|
MILITARY or ECO |
MILITARY
|
horse_booster_id
|
int
|
A horse's wod id to speed the spies up (-1 = none) |
-1
|
feathers
|
bool
|
Use the instant spy horse and pay for it with feathers |
False
|
slowdown
|
int
|
Seconds to delay the arrival by; costs rubies |
0
|
spend_rubies
|
bool
|
Allow a horse or slowdown that costs rubies; see "Spending rubies" in the guides |
False
|
wait_for_spies
|
float | None
|
Most seconds to keep asking |
None
|
cancel
|
Event | None
|
Ends the mission's wait when set, as |
None
|
Returns:
| Type | Description |
|---|---|
SpyHandle
|
The mission's handle; when nothing was sent, |
Raises:
| Type | Description |
|---|---|
ValueError
|
For a spy type other than MILITARY or ECO, or the horse or slowdown costs
rubies and |
GameDataNotLoadedError
|
A horse is picked without feathers and the game data cannot be loaded |
await_report
¶
Wait for a sent mission's report and read it.
Blocks until the spies arrive plus 10s for the report, or max_wait
if that is shorter; do not call this from a state callback. Returns
CANCELLED once the handle is cancelled: at once through
SpyHandle.cancel, within a second when its event is set
directly. A bsd already asked for is read to the end, so a report
read while the cancel comes is returned. The handle stops listening
when this returns, and its result is kept in handle.result: a
later call returns it at once, and a call made while another waits
returns what that one does, or TIMEOUT after its own max_wait.
sne carries no mission id. A spy log whose header names a
mission's target (kingdom, owner, area type and name, from the csm
reply's movement) goes to the waiting mission for that target whose
spies arrive first, and one whose report is for another position is
handed on to the next. Two missions to one target so get the reports
in the order their spies arrive. A mission that stopped waiting
without its report, cancelled or at max_wait, keeps its place
until 10s after its spies arrive, and the report given to it is
dropped. That is a client-side heuristic: no field ties a report to
its mission.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
handle
|
SpyHandle
|
From |
required |
max_wait
|
float | None
|
Most seconds to wait from now; None waits for the trip plus 10s |
None
|
Returns:
| Type | Description |
|---|---|
SpyResult
|
SpyResult with the report or why there is none; |
SpyResult
|
as it is when the mission ended before its report wait. |
execute_instant_spy
¶
execute_instant_spy(source_castle_id: int, target_x: int, target_y: int, target_kingdom: Kingdom = GREEN, risk_tolerance: int | None = None, accuracy: int = MAX_ACCURACY, *, spy_type: SpyType = MILITARY, horse_booster_id: int = -1, feathers: bool = False, slowdown: int = 0, spend_rubies: bool = False, max_wait: float | None = None, wait_for_spies: float | None = None, cancel: Event | None = None) -> SpyResult
Send a military or economy spy mission and read its report.
This is send_instant_spy then await_report; call those
two to do something else while the spies travel.
Nothing is paid unless asked: by default the spies travel without a
horse. feathers uses the instant spy horse, paid with feathers,
which the client sends as HBW -1 with PTT 1 and which wins over
horse_booster_id, as in the client. A horse that costs rubies and a
slowdown need spend_rubies; see "Spending rubies" in the guides.
The mission is costed with the client's risk floor for the target:
none for an NPC area such as a robber baron camp, 5% for a player's
area and for NPCs the game fights like players. Where the ssi
reply does not tell whose area it is (see
row_risk_flags), the 5% floor is kept:
the mission may then be planned at a higher risk than the client
shows, and a risk_tolerance under 5 skips it.
The report arrives as an sne push once the spies get there. Every
sne in the wait is looked at without being taken from other
listeners; only a spy log whose header names this mission's target
(kingdom, owner, area type and name, from the csm reply's movement)
and whose report, a caught mission's too, is for the target's position
counts; the rest are skipped. Two missions to one target at once get
their reports in arrival order (see await_report).
Asks ssi once: with no spy at home, or no mission within
risk_tolerance, it returns at once, as the client's spy dialog
shows the pool as it is. wait_for_spies asks again every 2s for up
to that many seconds, for spies still on their way home. Then it
blocks until the spies arrive (the csm reply's travel time) plus 10s
for the report, or max_wait if that is shorter; do not call this
from a state callback.
Setting cancel ends the call with CANCELLED. It is looked at
between requests, never during one: a request in flight ends with its
own reply or timeout, so no waiter is abandoned and no late reply can
reach the next caller of that command. Spies already sent keep going
(result.mission is set); client.movements.recall with its
movement_id turns them back.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source_castle_id
|
int
|
The castle the spies leave from, one of yours: |
required |
target_x
|
int
|
Target X coordinate |
required |
target_y
|
int
|
Target Y coordinate |
required |
target_kingdom
|
Kingdom
|
Target kingdom |
GREEN
|
risk_tolerance
|
int | None
|
Ceiling on the chance of being caught, as a percentage. Missions always run at the lowest risk the spy pool allows; this only decides whether to send at all, so a target that stays above it is skipped rather than spied badly. |
None
|
accuracy
|
int
|
Spy accuracy (50-100). Lower values need fewer spies for the same risk but return a less complete report. |
MAX_ACCURACY
|
spy_type
|
SpyType
|
MILITARY for the army, ECO for the resources. The client offers ECO only for castles and other players' outposts. |
MILITARY
|
horse_booster_id
|
int
|
A horse's wod id to speed the spies up (-1 = none); sent as -1 whenever feathers are used, as the client does |
-1
|
feathers
|
bool
|
Use the instant spy horse and pay for it with feathers |
False
|
slowdown
|
int
|
Seconds to delay the arrival by; costs rubies |
0
|
spend_rubies
|
bool
|
Allow a horse or slowdown that costs rubies |
False
|
max_wait
|
float | None
|
Most seconds to wait for the report after the csm reply; None waits for the trip plus 10s |
None
|
wait_for_spies
|
float | None
|
Most seconds to keep asking |
None
|
cancel
|
Event | None
|
Ends the call with |
None
|
Client: CastlePostSpyDialog.spyCastle (bundle line 38459),
CastleStartSpyVO.setSpyValues (bundle line 140004),
CastleSpyDialogSpyState.fillSpyTypeList (bundle line 72238),
HorseTravelboosterVO.isPayedWithPegasusTickets (bundle line 118825)
Returns:
| Type | Description |
|---|---|
SpyResult
|
SpyResult with the report or why there is none. |
Raises:
| Type | Description |
|---|---|
ValueError / GameDataNotLoadedError
|
See |
send_sabotage
¶
send_sabotage(source_castle_id: int, target_x: int, target_y: int, target_kingdom: Kingdom = GREEN, damage: int = MIN_DAMAGE, risk_tolerance: int | None = None, *, horse_booster_id: int = -1, feathers: bool = False, slowdown: int = 0, spend_rubies: bool = False) -> SpyResult
Send a sabotage mission with the fewest spies that reach the pool's lowest risk.
Sabotage spies travel at a ninth of a spy's speed (TRAVELSPEED_SABOTAGE
50 against 450). The report is not waited for: the outcome is
SENT with the csm reply in mission. The damage is checked
against what the target's owner level allows when the ssi reply
carries the owner's record; the client's other refusals (target kind,
protection, cooldown) are left to the server.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source_castle_id
|
int
|
The castle the spies leave from, one of yours |
required |
target_x
|
int
|
Target X coordinate |
required |
target_y
|
int
|
Target Y coordinate |
required |
target_kingdom
|
Kingdom
|
Target kingdom |
GREEN
|
damage
|
int
|
Damage percent, 10 up to |
MIN_DAMAGE
|
risk_tolerance
|
int | None
|
Ceiling on the chance of being caught, as a percentage |
None
|
horse_booster_id
|
int
|
A horse's wod id to speed the spies up (-1 = none) |
-1
|
feathers
|
bool
|
Use the instant spy horse and pay for it with feathers |
False
|
slowdown
|
int
|
Seconds to delay the arrival by; costs rubies |
0
|
spend_rubies
|
bool
|
Allow a horse or slowdown that costs rubies; see "Spending rubies" in the guides |
False
|
Raises:
| Type | Description |
|---|---|
ValueError
|
A damage the client would not send, or the horse or slowdown costs rubies
and |
GameDataNotLoadedError
|
A horse is picked without feathers and the game data cannot be loaded |
Client: CastleSpyDialogSabotageState.updateSpyVO and spyCastle (bundle lines
72203-72205), ACastleSpyDialogState.updateSliderForDamage (bundle line 34360),
CastleStartSpyVO.setSabotageValues (bundle line 140006)
spy.pool¶
How many spies you have: the gms count plus research, legend skill and title boosts.
Client: CastleSpyData.getNumAllSpies and getNumAvailableSpies (bundle lines 139970-139978)
Functions:
| Name | Description |
|---|---|
island_title_chain |
The island titles you hold with one: it and every title below it, lowest first. |
total_spies |
The client's count of all your spies, home or out. |
research_spy_bonus |
Extra spies from your finished research, uncapped as the client totals it. |
legend_spy_bonus |
Extra spies from your legend skills. |
title_spy_percent |
Your titles' spy boost in percent, uncapped as the client totals it. |
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)
total_spies
¶
total_spies(max_spies: int, *, research_bonus: float = 0, legend_bonus: float = 0, title_percent: float = 0) -> int
The client's count of all your spies, home or out.
int((max_spies + int(research_bonus) + int(legend_bonus)) * (1 + title_percent / 100)),
each int truncating. The client adds legend_bonus only for a target
whose owner is a legend; pass 0 otherwise.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
max_spies
|
int
|
|
required |
research_bonus
|
float
|
|
0
|
legend_bonus
|
float
|
|
0
|
title_percent
|
float
|
|
0
|
Client: CastleSpyData.getNumAllSpies (bundle line 139976)
research_spy_bonus
¶
Extra spies from your finished research, uncapped as the client totals it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
game_data
|
GameData
|
Loaded tables |
required |
research_ids
|
Iterable[int]
|
Your finished research ids, the |
required |
Client: CastleResearchData.getResearchEffectValue and getResearchEffectsByType
(bundle lines 139343-139357), CastleEffectsHelper.getTotalEffectValue with ignoreCap
(bundle line 4139)
legend_spy_bonus
¶
Extra spies from your legend skills.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
game_data
|
GameData
|
Loaded tables |
required |
legend_skill_ids
|
Iterable[int]
|
|
required |
Client: CastleLegendSkillData.getTotalValueOfLegendSkillEffect (bundle line 112086)
title_spy_percent
¶
Your titles' spy boost in percent, uncapped as the client totals it.
Each title counts its first spy boost only.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
game_data
|
GameData
|
Loaded tables |
required |
title_ids
|
Iterable[int]
|
Every title you hold; for a Storm Islands title, |
required |
Client: CastleTitleSystemHelper.returnTitleEffectValue and returnTitleEffectsByType
(bundle lines 4420-4428), EffectsHandlerVO.getBonusVOByEffectType (bundle line 25098)
spy.risk¶
Espionage risk model, ported from the game client's SpyConst.
Constants and formulas are taken verbatim from SpyConst (dll lines
19738-19757), so a mission can be costed before it is sent. getSpyRisk
reads:
ratioGuardSpy = MAX_GUARD / MAX_SPY
ratioAccuracy = 50 - (MAX_ACCURACY + MIN_ACCURACY) / 2
r = clamp(-(ratioGuardSpy * spies - guards) + ratioAccuracy + accuracy, floor, MAX_RISK_SPY)
o = clamp(floor(guards / spies / ratioGuardSpy * (ratioAccuracy + accuracy)), floor, MAX_RISK_SPY)
risk = round((r + o) / 2)
The floor is MIN_RISK_SPY_PLAYER unless the target is a dungeon or not a
player's, then MIN_RISK_SPY_DUNGEON; see risk_target_flags.
getSabotageRisk is the same with the damage for the accuracy, its own
ratio and the fixed MIN_RISK_SABOTAGE..MAX_RISK_SABOTAGE range.
Classes:
| Name | Description |
|---|---|
SpyPlan |
How a mission should be sent: spies, the risk it buys, and at what detail. |
SabotagePlan |
How a sabotage should be sent: spies, the risk it buys, and the damage. |
Functions:
| Name | Description |
|---|---|
spy_risk |
The percentage chance of a military or economy mission being caught, as the client shows it. |
sabotage_risk |
The percentage chance of a sabotage mission being caught, as the client shows it. |
max_damaged_buildings |
How many buildings a sabotage of an owner at this level can damage. |
max_sabotage_damage |
The most damage the client lets a sabotage of an owner at this level do, 0 when it offers no sabotage. |
risk_target_flags |
The client's |
row_risk_flags |
The client's |
plan_mission |
The library's cheapest military or economy mission inside the risk ceiling. |
plan_sabotage |
The library's cheapest sabotage at this damage inside the risk ceiling. |
Attributes:
| Name | Type | Description |
|---|---|---|
SPY_VALIDITY |
|
|
MAX_GUARD |
|
|
MAX_SPY |
|
|
MIN_RISK_SPY_PLAYER |
|
|
MIN_RISK_SPY_DUNGEON |
|
|
MAX_RISK_SPY |
|
|
MIN_ACCURACY |
|
|
MAX_ACCURACY |
|
|
MIN_RISK_SABOTAGE |
|
|
MAX_RISK_SABOTAGE |
|
|
MIN_DAMAGE |
|
|
MAX_DAMAGE |
|
|
DAMAGE_PER_BUILDING |
|
|
MAX_SABOTAGE_COOLDOWN |
|
|
SABOTAGE_PROTECTION_WINDOW |
|
|
TRAVELSPEED_SPY |
|
|
TRAVELSPEED_SABOTAGE |
|
|
DAMAGE_PER_SPY |
|
|
SABOTAGE_PROTECTION_THRESHOLD |
|
|
MAX_DEPLOYABLE_PLAGUEMONKS |
|
|
MAX_OWNABLE_PLAGUEMONKS |
|
|
OUTPOST_AREA_TYPES |
Area types built as an |
SPY_VALIDITY
module-attribute
¶
SpyConst.SPY_VALIDITY (dll line 19739): seconds a spy report's army stays current.
SABOTAGE_PROTECTION_THRESHOLD
module-attribute
¶
SpyConst.SABTOAGE_PROTECTION_THRESHOLD, spelled that way in the client.
OUTPOST_AREA_TYPES
module-attribute
¶
OUTPOST_AREA_TYPES = frozenset({int(MapItemType.OUTPOST), int(MapItemType.CAPITAL), int(MapItemType.METROPOL)})
Area types built as an OutpostMapobjectVO: the outpost and its capital and
metropolis subclasses (WorldmapObjectFactory.mapObjectVOs, bundle line 5357;
CapitalMapobjectVO bundle line 18760, MetropolMapobjectVO 21637).
SpyPlan
dataclass
¶
SpyPlan(spies: int, risk: int, accuracy: int = MAX_ACCURACY)
SabotagePlan
dataclass
¶
spy_risk
¶
spy_risk(spies: int, guards: int, accuracy: int | float = MAX_ACCURACY, *, player_target: bool = True, dungeon: bool = False) -> int
The percentage chance of a military or economy mission being caught, as the client shows it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
spies
|
int
|
Spies sent, at least 1 |
required |
guards
|
int
|
Guards at the target, |
required |
accuracy
|
int | float
|
Accuracy percent, 50-100 |
MAX_ACCURACY
|
player_target
|
bool
|
The client's |
True
|
dungeon
|
bool
|
The client's |
False
|
Client: SpyConst.getSpyRisk (dll lines 19745-19748)
sabotage_risk
¶
The percentage chance of a sabotage mission being caught, as the client shows it.
The client costs a plague monk mission with the same formula.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
spies
|
int
|
Spies sent, at least 1 |
required |
guards
|
int
|
Guards at the target, |
required |
damage
|
int | float
|
Damage percent, 10-50 |
required |
Client: SpyConst.getSabotageRisk (dll line 19749), CastleStartSpyVO.setSabotageValues
and setPlagueValues (bundle lines 140006-140007)
max_damaged_buildings
¶
How many buildings a sabotage of an owner at this level can damage.
Client: CombatConst.getMaxDamagedBuildings (dll line 18896)
max_sabotage_damage
¶
The most damage the client lets a sabotage of an owner at this level do, 0 when it offers no sabotage.
The damage slider runs from MIN_DAMAGE to MAX_DAMAGE and stops at ten per building the target can lose; the sabotage tab is disabled when that is no building.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
owner_level
|
int
|
The target owner's level |
required |
Client: ACastleSpyDialogState.updateSliderForDamage (bundle line 34360),
the sabotage tab check (bundle line 34338)
risk_target_flags
¶
The client's (isDungeon, isPlayer) for a target, in the order spy_risk takes them.
isDungeon is an owner below 0 that is no alien invasion or collector;
isPlayer is anything but an outpost, or an outpost a player owns.
Pass them as spy_risk(..., dungeon=..., player_target=...).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
owner_id
|
int
|
The target owner's player id, e.g. |
required |
area_type
|
int | None
|
The target's area type |
required |
Client: CastleStartSpyVO.setSpyValues (bundle line 140004),
PlayerHelper.isNpcPvpPlayer (bundle line 4689)
row_risk_flags
¶
The client's (isDungeon, isPlayer) for a target's map row, or None where its owner is not traced here.
NPC dungeons and camps have fixed owners below 0 that the game does not
fight like players; a faction invasion camp names its owner in the row;
alien camps and the areas whose row names the owner are read with
owner_id_from_row. Not traced: treasure
dungeons and camps, shadow areas (whose owner may be a collector) and any
other type; a samurai alien camp has no map object in the client.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
row
|
list[Any] | None
|
The target's raw map row, e.g. |
required |
Client: WorldmapObjectFactory.parseWorldMapArea (bundle line 5343) and the
parseAreaInfo of each map object
plan_mission
¶
plan_mission(guards: int, available: int, accuracy: int = MAX_ACCURACY, max_risk: int = MAX_RISK_SPY, *, player_target: bool = True, dungeon: bool = False) -> SpyPlan | None
The library's cheapest military or economy mission inside the risk ceiling.
Risk falls as accuracy falls, so accuracy is walked down from accuracy
to MIN_ACCURACY until the whole pool's risk fits the ceiling. The most
detailed report that fits is chosen, then the fewest spies that reach the
same risk, leaving the rest of the pool for other targets. This search is
the library's own: the client's dialog lets the player pick both values and
only colours its slider by risk (ACastleSpyDialogState.getAccuracyByRisk,
bundle line 34363).
None means even the least accurate mission with every spy available stays above the ceiling: a target to skip rather than spy badly.
plan_sabotage
¶
plan_sabotage(guards: int, available: int, damage: int, max_risk: int = MAX_RISK_SABOTAGE) -> SabotagePlan | None
The library's cheapest sabotage at this damage inside the risk ceiling.
The damage is kept as asked; the fewest spies that reach the whole pool's risk are sent. None when even the whole pool stays above the ceiling.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
guards
|
int
|
Guards at the target |
required |
available
|
int
|
Spies at hand |
required |
damage
|
int
|
Damage percent, MIN_DAMAGE up to |
required |
max_risk
|
int
|
Ceiling on the risk, percent |
MAX_RISK_SABOTAGE
|
spy.models¶
Spy protocol models.
Commands:
- csm: Send spy mission
- ssi: Spy screen info
- ssu: Auto-spy, a report without sending spies
- gms: Maximum spies, a login section and push
Classes:
| Name | Description |
|---|---|
SendSpyRequest |
Send a spy mission to a target. |
SendSpyResponse |
The spy mission the server created. |
SpyScreenInfoRequest |
Get what a spy mission against a target would face: its guards and the spies at hand. |
SpyTargetArea |
The target's map rows and owner records, and your own protection: the |
SpyScreenInfoResponse |
What a spy mission against a target would face, and the spies at hand. |
AutoSpyRequest |
Spy a target at once, without sending spies: the auto-spy subscription's button. |
AutoSpyResponse |
An auto-spy report, the same shape as a |
MaxSpiesResponse |
How many spies you have, before research, title and legend skill boosts. |
PlagueMonkInfoResponse |
Your plague monks. |
SendSpyRequest
pydantic-model
¶
Bases: BaseRequest
Send a spy mission to a target.
Command: csm Payload: {"SID": castle_id, "TX": target_x, "TY": target_y, "SC": spy_count, "ST": spy_type, "SE": accuracy_or_damage, "HBW": horse_booster_id, "KID": target_kingdom, "PTT": feathers, "SD": slowdown}
The keys follow the client's order. SE is the damage (10-50) for a
sabotage mission and the accuracy (50-100) for any other; the client
sends the slider's value unrounded. A horse paid with feathers goes out as
HBW -1 with PTT 1: with feathers 1 the horse is sent as
-1, as the client's constructor does. Plague monks are not sent with
csm.
Client: C2SCreateSpyMovementVO (bundle lines 100126-100127), built by
CastlePostSpyDialog.spyCastle (bundle line 38459)
Fields:
-
castle_id(int) -
target_x(int) -
target_y(int) -
spy_count(int) -
spy_type(SpyType) -
accuracy_or_damage(int) -
horse_booster_id(int) -
target_kingdom(Kingdom) -
feathers(int) -
slowdown(int)
Validators:
-
_feathers_send_no_horse
castle_id
pydantic-field
¶
castle_id: int
Wire key SID
One of your castles, CastleInfo.castle_id from client.castle.get_all() or Castle.id from client.state.get_castles()
spy_type
pydantic-field
¶
Wire key ST
What the spies are sent to do
accuracy_or_damage
pydantic-field
¶
accuracy_or_damage: int = 100
Wire key SE
Damage percent for a sabotage mission, accuracy percent for any other
horse_booster_id
pydantic-field
¶
horse_booster_id: int = -1
Wire key HBW
The horse booster's wod id, -1 for none or when paid with feathers
target_kingdom
pydantic-field
¶
Wire key KID
The target's kingdom
accepts_reply
¶
Whether a csm reply is the movement this sent: its target area (A.M.TA) is TX/TY.
Client: CSMCommand (bundle line 125996) reads the new movement from A,
whose TA is the target area (BasicMapmovementVO). The spies' way home,
which the server pushes as a csm too (seen live), targets your own castle and
is not taken.
SendSpyResponse
pydantic-model
¶
Bases: BaseResponse
The spy mission the server created.
Command: csm Payload:
{"A": {"M": {movement}, "S": {"ST": spy_type, "SA": accuracy, "SC": spies, "SR": risk}},
"O": [owner record, ...], "gcu": {currencies}}
A is read like a gam entry; gcu may be missing.
Client: CSMCommand.executeCommand (bundle line 125997), which passes
[i.A] to CastleArmyData.parseMapMovementArray (bundle line 133626),
i.O to CastleOtherPlayerData.parseOwnerInfoArray (bundle line 139005)
and i.gcu to CurrencyData.parseGCU (bundle line 141191);
SpyMapmovementVO.loadFromParamObject (bundle line 43747)
Fields:
-
error_code(int) -
spy_movement(MovementWrapper | None) -
owners(list[MovementOwner]) -
currencies(CurrencyBlock)
Validators:
-
_movement_or_none→spy_movement -
_readable_owners→owners
spy_movement
pydantic-field
¶
spy_movement: MovementWrapper | None = None
Wire key A
The spy movement; None when there is none
owners
pydantic-field
¶
owners: list[MovementOwner]
Wire key O
Owner records for the movement's areas
currencies
pydantic-field
¶
currencies: CurrencyBlock = None
Wire key gcu
Coins and rubies after the send; None when the reply has none
movement_id
property
¶
movement_id: int | None
The spy movement's id, or None when the reply has no movement.
seconds_until_arrival
property
¶
seconds_until_arrival: int | None
Seconds from the reply until the spies arrive: TT - PT, never below 0.
Client: BasicMapmovementVO.loadFromParamObject (bundle line 19382)
spy
property
¶
spy: MovementSpy | None
The mission's spy type, accuracy or damage, spy count and risk.
SpyScreenInfoRequest
pydantic-model
¶
Bases: BaseRequest
Get what a spy mission against a target would face: its guards and the spies at hand.
Command: ssi Payload: {"TX": target_x, "TY": target_y, "KID": target_kingdom}
Client: C2SGetSpyInfo (bundle lines 22504-22505), sent with the target's
absAreaPos and kingdomID when the spy dialog opens (bundle line 14800)
Fields:
target_kingdom
pydantic-field
¶
Wire key KID
The target's kingdom
accepts_reply
¶
Whether an ssi reply is about this target: its TX/TY, when both are set, are the ones asked for.
Client: CastleSpyData.parse_SSI (bundle line 139962) passes the reply's
position only when TX and TY are both truthy, and
CastleSpyDialog.onPreSpyInfoUpdate (bundle line 16398) ignores a reply
whose position is not the target's.
SpyTargetArea
pydantic-model
¶
Bases: BasePayload
The target's map rows and owner records, and your own protection: the gaa block of ssi.
Client: CastleSpyData.parse_SSI (bundle line 139962), which reads
uap with parse_UAP, OI with parseOwnerInfoArray and AI
with parseAreaInfos, as a map area reply
Fields:
-
kingdom_id(int | None) -
protection(KingdomProtection | None) -
owners(list[MapObject]) -
rows(list[MapAreaItem])
Validators:
-
_protection_needs_an_object→protection -
_readable_owners→owners -
_readable_rows→rows
protection
pydantic-field
¶
protection: KingdomProtection | None = None
Wire key uap
Your own protection
SpyScreenInfoResponse
pydantic-model
¶
Bases: BaseResponse
What a spy mission against a target would face, and the spies at hand.
Command: ssi Payload:
{"AS": available_spies, "GC": guards, "APM": available_plague_monks,
"TPM": total_plague_monks, "TX": x, "TY": y,
"gaa": {"KID": kingdom, "uap": {protection}, "OI": [owner records], "AI": [map rows]}}
The client reads gaa.uap without a check; a reply without gaa
still parses here.
Client: SSICommand.executeCommand (bundle line 128554),
CastleSpyData.parse_SSI (bundle line 139962)
Fields:
-
error_code(int) -
available_spies(ClientInt) -
guard_count(ClientInt) -
available_plague_monks(ClientInt) -
total_plague_monks(ClientInt) -
target_x(int | None) -
target_y(int | None) -
target_area(SpyTargetArea)
Validators:
-
_position_needs_both -
_area_needs_an_object→target_area
available_plague_monks
pydantic-field
¶
available_plague_monks: ClientInt = 0
Wire key APM
Plague monks free to send
total_plague_monks
pydantic-field
¶
total_plague_monks: ClientInt = 0
Wire key TPM
Plague monks owned
target_x
pydantic-field
¶
target_x: int | None = None
Wire key TX
The target's map x; None when not sent
target_y
pydantic-field
¶
target_y: int | None = None
Wire key TY
The target's map y; None when not sent
target_area
pydantic-field
¶
target_area: SpyTargetArea
Wire key gaa
The target's map rows and owner records
target_row
¶
target_row(x: int | None = None, y: int | None = None) -> MapAreaItem | None
The target's map row: the one at x/y, by default the reply's own position.
Falls back to the only row when there is exactly one, and None otherwise.
target_owner
¶
The owner record of the target's owner, when the target row names a player that has one.
AutoSpyRequest
pydantic-model
¶
Bases: BaseRequest
Spy a target at once, without sending spies: the auto-spy subscription's button.
Command: ssu Payload: {"TX": target_x, "TY": target_y}
The client offers it only while subscriptionData.isAutoSpyActiveForArea
holds for the target.
Client: C2SSpySpyUnits (bundle lines 42828-42829), sent by
ButtonAutoSpyComponent.onClick (bundle line 109977) and
CastleMapobjectInfoComponent.onClickSpyIcon (bundle line 66479)
Fields:
AutoSpyResponse
pydantic-model
¶
Bases: SpyReportResponse
An auto-spy report, the same shape as a bsd spy report.
Command: ssu
The client builds it with CastleSpyLogVO.parseSpyLog as bsd does,
but does not store the OI and SO owner records.
Client: SSUCommand.executeCommand (bundle lines 128568-128573)
Fields:
-
error_code(int) -
message_id(int | None) -
area_object_id(ClientInt) -
spy_owner_id(ClientInt) -
owner_id(ClientInt) -
spy_count(ClientInt) -
guard_count(ClientInt) -
accuracy_or_damage(ClientInt) -
risk(ClientInt) -
army(SpyArmyBlock) -
seconds_since_spy(int) -
defending_castellan(Castellan | None) -
legend_skill_ids(tuple[EnumOrInt['LegendSkill'], ...]) -
resources(CollectableRows) -
dungeon_cooldown_seconds(int | None) -
owner(MapObject | None) -
spy_owner(MapObject | None) -
area(SpyReportArea | None)
Validators:
-
_daimyo_rank_into_the_area -
_skill_ids→legend_skill_ids -
_castellan_or_none→defending_castellan -
_owner_or_none→owner,spy_owner -
_area_or_none→area -
_no_age_without_an_army
MaxSpiesResponse
pydantic-model
¶
Bases: BaseResponse
How many spies you have, before research, title and legend skill boosts.
Command: gms, as a login section of gbd and as a push. The client
never sends a gms request.
Client: GMSCommand.executeCommand (bundle line 120555),
CastleSpyData.parse_GMS (bundle line 139979), read from gbd
(bundle line 129381)
Fields:
PlagueMonkInfoResponse
pydantic-model
¶
Bases: BaseResponse
Your plague monks.
Command: cpi, as a login section of gbd, a push, and inside the cpm and sbp replies.
Client: CPICommand (bundle line 128524), CastleSpyData.parse_CPI (bundle line 139967),
which skips a falsy section
Fields: