Protocol¶
Request and response base classes, packets, text codecs, the JavaScript-compatible readers and the connection.
protocol.base¶
Base classes and common types for GGE protocol models.
GGE Protocol Format:
- Request: %xt%{zone}%{command}%{room id}%{params...}%, a JSON command's one param being its JSON
- Response: %xt%{command}%{request id}%{status}%{payload}%
Classes:
| Name | Description |
|---|---|
GGECommand |
Registry of all GGE protocol commands. |
BasePayload |
Base class for all protocol payloads. |
BaseRequest |
Base class for request payloads sent to the server. |
BaseResponse |
Base class for response payloads received from the server. |
Position |
A position on the game map. |
CurrencyTotals |
Coins and rubies after an action, the |
CommanderEffect |
One entry of a commander's |
PlayerInfo |
Basic player information. |
Functions:
| Name | Description |
|---|---|
smartfox_text |
Escape a string command param as the client does: each |
json_text |
|
build_command |
The frame for a command with these params, as the client puts it on the wire. |
get_response_model |
Get the response model class for a command. |
parse_response |
Parse a response payload into the appropriate model. |
object_or_none |
A reply block that is an object, else None: the client reads keys off it only when it is one. |
list_or_empty |
A reply value that is an array, else no entries. |
int_entries |
The int entries of an id array the client looks up as sent in a table keyed by int ids, else no entries. |
readable_list |
Each entry of an array read as |
read_or_none |
|
enum_or_none |
The member of |
Attributes:
| Name | Type | Description |
|---|---|---|
DEFAULT_ZONE |
|
|
NO_ROOM |
|
|
CurrencyBlock |
A |
|
CommanderEffects |
|
CurrencyBlock
module-attribute
¶
CurrencyBlock = Annotated[CurrencyTotals | None, BeforeValidator(object_or_none)]
A gcu block, or None when a reply sends none or something that is not an object.
CommanderEffects
module-attribute
¶
CommanderEffects = Annotated[list[CommanderEffect], BeforeValidator(partial(readable_list, CommanderEffect))]
[effect_id, values, source] rows; unreadable entries are skipped, as the client skips
effects it cannot resolve (LordVO.parseRawEffects, bundle line 26483).
GGECommand
¶
Registry of all GGE protocol commands.
Attributes:
| Name | Type | Description |
|---|---|---|
LLI |
|
|
LRE |
|
|
VPN |
|
|
VLN |
|
|
LPP |
|
|
VCK |
|
|
SLT |
|
|
ACM |
|
|
ACL |
|
|
AIN |
|
|
AKM |
|
|
ARM |
|
|
AIP |
|
|
AAL |
|
|
AAA |
|
|
AQI |
|
|
ADP |
|
|
ARD |
|
|
SAW |
|
|
ANL |
|
|
ADO |
|
|
AHL |
|
|
AHH |
|
|
AHD |
|
|
AHF |
|
|
AHC |
|
|
AHA |
|
|
AHR |
|
|
GCL |
|
|
DCL |
|
|
JCA |
|
|
ARC |
|
|
RST |
|
|
GRC |
|
|
GPA |
|
|
CRM |
|
|
CMI |
|
|
KUT |
|
|
STI |
|
|
KGT |
|
|
MSK |
|
|
GAM |
|
|
GAA |
|
|
FNM |
|
|
GDI |
|
|
WSP |
|
|
ACI |
|
|
ADI |
|
|
ABI |
|
|
ALI |
|
|
AVI |
|
|
AII |
|
|
COI |
|
|
CCI |
|
|
CTI |
|
|
CRA |
|
|
CSM |
|
|
SSI |
|
|
GAS |
|
|
SAS |
|
|
MSD |
|
|
SDC |
|
|
CDS |
|
|
EBU |
|
|
EUP |
|
|
EMO |
|
|
SBD |
|
|
EDO |
|
|
FCO |
|
|
MSB |
|
|
EUD |
|
|
RBU |
|
|
IRA |
|
|
EBE |
|
|
ETC |
|
|
SCL |
|
|
CMR |
|
|
RCC |
|
|
BUP |
|
|
SPL |
|
|
BOU |
|
|
MCU |
|
|
GUI |
|
|
DUP |
|
|
HRU |
|
|
HCS |
|
|
HSS |
|
|
HDU |
|
|
HRA |
|
|
DFC |
|
|
DFK |
|
|
DFW |
|
|
DFM |
|
|
SDI |
|
|
SBP |
|
|
GBC |
|
|
SEI |
|
|
SEE |
|
|
PEP |
|
|
HGH |
|
|
LLSP |
|
|
LLSW |
|
|
SLSE |
|
|
SEDE |
|
|
SNE |
|
|
RMS |
|
|
MMR |
|
|
AMS |
|
|
DMS |
|
|
SMS |
|
|
BSD |
|
|
CLB |
|
|
GPG |
|
|
QSC |
|
|
QDR |
|
|
FCQ |
|
|
CTR |
|
|
GPI |
|
|
VPM |
|
|
GNCI |
|
|
CPNE |
|
|
SCP |
|
|
RMC |
|
|
MNS |
|
|
CMC |
|
|
FCS |
|
|
MVF |
|
|
OPT |
|
|
HFL |
|
|
TXI |
|
|
TXS |
|
|
TXC |
|
|
GBL |
|
|
RUI |
|
|
RMB |
|
|
GLI |
|
|
ARL |
|
|
GEI |
|
|
EEQ |
|
|
GCS |
|
|
SCT |
|
|
SIN |
|
|
SOB |
|
|
SDS |
|
EmpireModel
pydantic-model
¶
Bases: BaseModel
Base of every model with wire keys: a field declares its key once, as Field(validation_alias="X").
The model writes each field back under the key it reads it from: a string validation_alias
is copied to serialization_alias as the class is created, before pydantic collects the
fields, so every later rebuild of the model sees it. The key must be on the assigned
Field(...), not inside Annotated[...]. An AliasChoices or AliasPath field names
its serialization_alias itself.
BasePayload
pydantic-model
¶
Bases: EmpireModel
Base class for all protocol payloads.
model_config
class-attribute
¶
model_config: ConfigDict = ConfigDict(populate_by_name=True, extra='allow')
BaseRequest
pydantic-model
¶
Bases: BasePayload
Base class for request payloads sent to the server.
to_packet
¶
to_packet(zone: str = DEFAULT_ZONE, room_id: int = NO_ROOM) -> str
The frame that sends this request: %xt%{zone}%{command}%{room id}%{json}%.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
zone
|
str
|
Game zone (default: EmpireEx_21) |
DEFAULT_ZONE
|
room_id
|
int
|
The joined room's id (from |
NO_ROOM
|
Client: BasicSmartfoxClient.sendCommandVO (dll line 7177)
BaseResponse
pydantic-model
¶
Bases: BasePayload
Base class for response payloads received from the server.
Responses can be instantiated directly from payload dicts
response = AllianceChatMessageResponse(**payload)
or¶
response = AllianceChatMessageResponse.model_validate(payload)
Each response class should define a command class variable to enable
automatic lookup via get_response_model(). Commands must be unique;
a class that shares a command with another (and is parsed manually
instead) must opt out with class Foo(BaseResponse, register=False).
Fields:
TimedPayload
pydantic-model
¶
Bases: BasePayload
A read-only block whose times count down from when it was read, as the client counts them from its timer.
Fields:
TimedResponse
pydantic-model
¶
Position
pydantic-model
¶
CurrencyTotals
pydantic-model
¶
Bases: BasePayload
Coins and rubies after an action, the gcu block.
Client: CurrencyData.parseGCU (bundle line 141191), which reads
CollectableItemC1VO.SERVER_KEY "C1" (bundle line 7995) and
CollectableItemC2VO.SERVER_KEY "C2" (bundle line 4876).
Fields:
Validators:
CommanderEffect
pydantic-model
¶
Bases: BasePayload
One entry of a commander's E or AE: [effect_id, values, source].
Client: LordVO.parseRawEffects (bundle line 26483), BonusVO.parseFromValueArray
(bundle line 5707).
Fields:
Validators:
-
_source_key→source -
_from_row
PlayerInfo
pydantic-model
¶
Bases: EmpireModel
Basic player information.
Fields:
-
player_id(int) -
player_name(str) -
alliance_id(int | None) -
alliance_name(str | None)
smartfox_text
¶
Escape a string command param as the client does: each % becomes % and each ' is dropped.
Client: TextValide.getValideSmartFoxText (dll line 5816)
js_number_text
¶
String(number): the ECMAScript Number::toString layout of the shortest round-trip digits.
json_text
¶
payload as JSON.stringify writes it: no spaces, non-ASCII kept as is, numbers as JavaScript writes them.
Raises:
| Type | Description |
|---|---|
ValueError
|
For NaN or an infinity, which have no JSON form |
TypeError
|
For a value JSON has no form for |
build_command
¶
build_command(zone: str, command: str, params: list[str | int | float | bool | None], room_id: int = NO_ROOM) -> str
The frame for a command with these params, as the client puts it on the wire.
A 0 goes out as 0; any other falsy param ("", None, False, NaN)
as <RoundHouseKick>; True as true; other numbers as JavaScript
writes them; and each string through smartfox_text.
Client: BasicSmartfoxClient.sendMessage and sendCommand (dll line 7171, 7198)
get_response_model
¶
get_response_model(command: str) -> type[BaseResponse] | None
Get the response model class for a command.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
command
|
str
|
The command code (e.g., "acm", "gam") |
required |
Returns:
| Type | Description |
|---|---|
type[BaseResponse] | None
|
The response model class, or None if not registered |
parse_response
¶
parse_response(command: str, payload: dict[str, Any]) -> BaseResponse | None
Parse a response payload into the appropriate model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
command
|
str
|
The command code |
required |
payload
|
dict[str, Any]
|
The payload dict from the server |
required |
Returns:
| Type | Description |
|---|---|
BaseResponse | None
|
The parsed response model, or None if no model is registered |
object_or_none
¶
A reply block that is an object, else None: the client reads keys off it only when it is one.
int_entries
¶
The int entries of an id array the client looks up as sent in a table keyed by int ids, else no entries.
Any other entry finds no row there, and the client then throws on the missing row; skipping it is a deliberate leniency, counted in one warning that shows the first.
readable_list
¶
readable_list(model: type[_M], value: Any, *, accept: Callable[[Any], Any] | None = None, keep: Callable[[Any], Any] | None = None, parse: Callable[[Any], _M] | None = None, warn: Logger | None = None, what: str = 'entries') -> list[_M]
Each entry of an array read as model, so one unreadable entry costs only itself.
A null entry, or one keep rejects, is skipped quietly. Where the client
would throw on a bad entry instead, skipping it is a deliberate leniency: it
costs only that entry. One accept rejects or that fails validation is unreadable:
it is skipped too, and with warn those are counted in one warning that
shows the first. Anything but an array reads as no entries.
protocol.packet¶
Parsing one server message, XML or %xt%.
Classes:
| Name | Description |
|---|---|
DegradedFrameCounts |
Inbound frames that degraded to a raw wrapper, counted for the whole process. |
Packet |
Base representation of a SmartFoxServer packet. |
Functions:
| Name | Description |
|---|---|
degraded_frame_counts |
How many inbound frames degraded to a raw wrapper (see |
Attributes:
| Name | Type | Description |
|---|---|---|
logger |
|
|
MALFORMED_STATUS_CODE |
|
|
MAX_FRAME_SIZE |
|
|
MAX_XML_SIZE |
|
|
DEGRADED_FRAME_WARN_INTERVAL |
|
DegradedFrameCounts
¶
Bases: NamedTuple
Inbound frames that degraded to a raw wrapper, counted for the whole process.
Attributes:
| Name | Type | Description |
|---|---|---|
total |
int
|
Every one since the process started. |
suppressed |
int
|
Those since the last warning, which no warning has reported yet. |
Packet
dataclass
¶
Packet(raw_data: str, is_xml: bool, command_id: str | None = None, request_id: int = -1, error_code: int = 0, payload: dict[str, Any] | list[Any] | Element | None = None)
Base representation of a SmartFoxServer packet. Can be either XML (Handshake) or XT (Extended/JSON).
error_code is an XT frame's status field, 0 for success.
Methods:
| Name | Description |
|---|---|
build_xt |
The frame for a JSON command, as |
from_bytes |
Parse one message received from the server: an XML |
to_bytes |
The frame as it goes on the wire, with no trailing null byte. |
Attributes:
| Name | Type | Description |
|---|---|---|
raw_data |
str
|
|
is_xml |
bool
|
|
command_id |
str | None
|
|
request_id |
int
|
|
error_code |
int
|
|
payload |
dict[str, Any] | list[Any] | Element | None
|
|
payload
class-attribute
instance-attribute
¶
build_xt
staticmethod
¶
The frame for a JSON command, as BaseRequest.to_packet builds it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
zone
|
str
|
Game zone (e.g., "EmpireEx_21") |
required |
command
|
str
|
Command ID (e.g., "gam") |
required |
payload
|
dict[str, Any]
|
The command's JSON object |
required |
room_id
|
int
|
The joined room's id (from |
NO_ROOM
|
Client: BasicSmartfoxClient.sendCommandVO (dll line 7177)
from_bytes
classmethod
¶
from_bytes(data: bytes) -> 'Packet'
Parse one message received from the server: an XML <msg> or one %xt% command.
Total by design: the receive loop has no per-packet recovery, so a message it cannot make sense of must degrade to a raw wrapper rather than raise and tear down the whole connection.
This reads a single message and strips only trailing null bytes.
Cutting the socket stream into messages is the network layer's job,
as the client does it in BasicSmartfoxClient.onDataReady (dll line
7211): <msg> blocks first, then the rest split on %xt once it
ends in %.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
bytes
|
Raw message bytes (may be truncated, padded or non-UTF-8) |
required |
Returns:
| Type | Description |
|---|---|
'Packet'
|
A Packet. Unparseable input yields a raw wrapper whose |
'Packet'
|
|
degraded_frame_counts
¶
degraded_frame_counts() -> DegradedFrameCounts
How many inbound frames degraded to a raw wrapper (see DegradedFrameCounts).
protocol.auth¶
Authentication protocol models.
Commands:
- lli: Login
- slt: Login token push after a persistent login
- vck: Version check before the login
- lre: Register new account
- vpn: Check a new player name
- vln: Check a login name
- lpp: Password recovery
Classes:
| Name | Description |
|---|---|
LoginRequest |
Log in ( |
LoginResponse |
The reply to |
LoginTokenResponse |
The login token the server pushes after a persistent login ( |
RegisterRequest |
Register a new account ( |
RegisterResponse |
The reply to |
CheckUsernameAvailableRequest |
Check whether a name is free for a new player ( |
CheckUsernameAvailableResponse |
The reply to |
CheckUsernameExistsRequest |
Check a login name ( |
CheckUsernameExistsResponse |
The reply to |
PasswordRecoveryRequest |
Ask for a password recovery email ( |
PasswordRecoveryResponse |
The reply to |
Functions:
| Name | Description |
|---|---|
build_version_check |
The |
Attributes:
| Name | Type | Description |
|---|---|---|
VERSION_CHECK_PLATFORM |
|
LoginRequest
pydantic-model
¶
Bases: BaseRequest
Log in (lli), with a password or with the token an earlier persistent login got.
create encodes the name and password as the login screen does.
With a non-empty password the token goes out as null; without one, the
token logs in and no password is sent.
Client: C2SLoginVO (bundle line 131792), built by CastleLoginCommand (bundle line 131772)
Fields:
-
connection_time(int) -
round_trip_time(int) -
login_id(int) -
persistent_login(bool) -
username(str) -
password(str | None) -
login_token(str | None) -
language(str) -
distributor_id(str) -
account_id(str) -
identity_management_id(str) -
referrer(str) -
gci(str) -
store_id(int) -
platform_id(int) -
recaptcha_token(str | None)
connection_time
pydantic-field
¶
connection_time: int = 0
Wire key CONM
Milliseconds from opening the socket to the server's apiOK
round_trip_time
pydantic-field
¶
round_trip_time: int = 0
Wire key RTM
Milliseconds from sending roundTrip to its answer; 0 before one came back
persistent_login
pydantic-field
¶
persistent_login: bool = False
Wire key PL
Stay logged in; the server then pushes a login token (slt)
password
pydantic-field
¶
password: str | None = None
Wire key PW
The password, encoded; None to log in by token
distributor_id
pydantic-field
¶
distributor_id: str = '0'
Wire key DID
Distributor id of the install
identity_management_id
pydantic-field
¶
identity_management_id: str = ''
Wire key KID
Identity management id, empty outside Korea
recaptcha_token
pydantic-field
¶
recaptcha_token: str | None = None
Wire key RCT
A reCAPTCHA v3 token for the action 'login'
create
classmethod
¶
create(username: str, password: str | None = None, **fields: Any) -> LoginRequest
A login request with the name and password encoded as the login screen encodes them.
LoginResponse
pydantic-model
¶
Bases: BaseResponse
The reply to lli.
A successful login carries nothing; a refused one explains itself in
these keys, depending on its status: IS_BANNED (27),
EXISTING_MAPPING_WRONG_SERVER (368), LOGIN_COOLDOWN_ACTIVE (453)
and NO_KOREA_USER_DATA_AVAILABLE (10003).
Client: LLICommand.executeCommand (bundle line 120651)
Fields:
-
error_code(int) -
account_deleted(bool) -
remaining_ban_seconds(_Number) -
instance_id(_Number) -
remaining_cooldown_seconds(_Number) -
player_id(_Number)
account_deleted
pydantic-field
¶
account_deleted: bool = False
Wire key GDPR
The banned account was deleted
remaining_ban_seconds
pydantic-field
¶
Wire key RS
Seconds until the ban ends
instance_id
pydantic-field
¶
Wire key IID
Instance id of the server the account is on
remaining_cooldown_seconds
pydantic-field
¶
Wire key CD
Seconds until the next login attempt is allowed
LoginTokenResponse
pydantic-model
¶
Bases: BaseResponse
The login token the server pushes after a persistent login (PL 1).
The client saves it and logs in with it later, as LoginRequest's
token with no password.
Client: SLTCommand.executeCommand (bundle line 120936)
Fields:
-
error_code(int) -
login_token(str | None)
RegisterRequest
pydantic-model
¶
Bases: BaseRequest
Register a new account (lre).
Only the keys initialize sets and the reCAPTCHA token RCT are
modelled. The client's object also
carries the helper properties it was filled from (username,
password, campaignVars and so on) and, when it has them, an
inviter code IC and a Steam ticket STK; whether the server needs
any of those is unverified.
Client: C2SRegisterWithNameVO.initialize (bundle line 52392), filled by
BasicRegisterUserCommand.composeRegisterUserVO (dll line 33134) and
sent, with RCT last, by CastleRegisterUserCommand.sendRegisterUserCommandWithRecaptcha
(bundle line 110800)
Fields:
-
distributor_id(int) -
connection_time(int) -
round_trip_time(int) -
campaign_partner_id(int) -
campaign_creative(int) -
campaign_landing_page(int) -
ad_id(int) -
time_zone(int) -
username(str) -
password(str | None) -
referrer(str) -
language(str) -
account_id(str) -
gci(str) -
store_id(int) -
platform_id(int) -
network_id(int) -
recaptcha_token(str | None)
connection_time
pydantic-field
¶
connection_time: int = 0
Wire key CONM
Milliseconds from opening the socket to the server's apiOK
round_trip_time
pydantic-field
¶
round_trip_time: int = 0
Wire key RTM
Milliseconds from sending roundTrip to its answer
campaign_partner_id
pydantic-field
¶
campaign_partner_id: int = 0
Wire key campainPId
Campaign partner id
RegisterResponse
pydantic-model
¶
Bases: BaseResponse
The reply to lre: the new player's id, or name suggestions when the name was refused.
Client: LRECommand.executeCommand (bundle line 120729)
Fields:
-
error_code(int) -
player_id(_Number) -
suggested_names(_Names)
CheckUsernameAvailableRequest
pydantic-model
¶
Bases: BaseRequest
Check whether a name is free for a new player (vpn).
Status 0 means it is; INVALID_NAME, NAME_ALREADY_IN_USE,
NAME_HAS_ONLY_NUMBERS and USAGE_OF_BADWORDS say why not, and
client.send raises them as a CommandError whose payload reads as
CheckUsernameAvailableResponse.
Client: C2SValidateNewPlayerNameVO (bundle line 108936)
Fields:
CheckUsernameAvailableResponse
pydantic-model
¶
Bases: BaseResponse
The reply to vpn; a refusal carries free names like the refused one.
Client: VPNCommand.executeCommand (bundle line 121103)
Fields:
-
error_code(int) -
suggested_names(_Names)
suggested_names
pydantic-field
¶
Wire key NS
Free names like the refused one
CheckUsernameExistsRequest
pydantic-model
¶
Bases: BaseRequest
Check a login name (vln).
Status 0 means the name is valid; any other status is raised by
client.send as a CommandError.
Client: C2SValidateLoginNameVO (bundle line 108927), reply read by
VLNCommand.executeCommand (bundle line 121093)
Fields:
CheckUsernameExistsResponse
pydantic-model
¶
Bases: BaseResponse
The reply to vln, which carries nothing the client reads.
Client: VLNCommand.executeCommand (bundle line 121093)
Fields:
PasswordRecoveryRequest
pydantic-model
¶
Bases: BaseRequest
Ask for a password recovery email (lpp).
Client: BasicLostPasswordCommand.sendMessage (dll line 33056), sent from
CastleLostPasswordDialog (bundle line 34746)
Fields:
PasswordRecoveryResponse
pydantic-model
¶
Bases: BaseResponse
The reply to lpp.
Its status has its own codes, not GGEError's: 0 means sent, 1 a
general error and 2 that no such player exists.
Client: CastleLostPasswordCommand.execute (bundle line 131805)
Fields:
build_version_check
¶
The vck frame the client sends once it has joined the lobby.
The server answers under vck: status 0 lets the login go ahead,
1 says the client's version is too low and 2 too high. The reply's first
field is the server's build number.
Client: BasicJoinedRoomCommand.execute (dll line 33011), reply read by
CastleVCKCommand.executeCommand (bundle line 120444)
protocol.text¶
The client's text codecs for strings it sends in and reads out of JSON commands.
The two are not inverses: decoding turns square brackets into spaces and
reads % before %5C, exactly as the client does.
Functions:
| Name | Description |
|---|---|
encode_json_text |
Encode text the way the client does before it puts it in a command. |
decode_json_text |
Decode server text the way the client does. |
is_smartfox_valid |
Whether the client accepts |
Attributes:
| Name | Type | Description |
|---|---|---|
SMARTFOX_INVALID_CHARS |
tuple[str, ...]
|
Characters the client refuses in a name it checks. |
SMARTFOX_INVALID_CHARS
module-attribute
¶
SMARTFOX_INVALID_CHARS: tuple[str, ...] = ('%', '&', '*', '/', '(', ')', '[', ']', '{', '}', '"', "'", '\\', '´', '`', '^', '°', '§', '€', '²', '³', ',', ';', 'µ', '$')
Characters the client refuses in a name it checks.
Client: TextValide.invalidChars (dll line 5831)
encode_json_text
¶
Encode text the way the client does before it puts it in a command.
Replaces %, ', ", a carriage return, a backslash and a newline, in
that order, and turns tabs into spaces.
Client: TextValide.getValideSmartFoxJSONTextMessage (dll line 5817)
decode_json_text
¶
Decode server text the way the client does.
Replaces %, ", &145;, <br /> and %5C in that
order and turns square brackets into spaces; nothing reads as "".
Client: TextValide.parseChatJSONMessage (dll line 5820)
protocol.js¶
The JavaScript conversions the game client applies to reply values.
The client reads most values through its own int(), through parseInt,
through !!value or by comparing them loosely (1 == value). Each helper
here reproduces one of them for the JSON values a reply can carry.
Functions:
| Name | Description |
|---|---|
js_int |
The client's |
js_number_or_none |
|
js_same_number |
Whether |
row_is_at |
Whether a map row |
movement_targets |
Whether a movement wrapper's target area row ( |
js_parse_int |
JavaScript's |
js_parse_int_or_zero |
|
js_truthy |
JavaScript's |
js_falsy |
JavaScript's |
js_loose_equals |
JavaScript's |
Attributes:
| Name | Type | Description |
|---|---|---|
ClientInt |
An int read through the client's |
|
ParseInt |
An int read through |
|
ClientNumber |
A number read through |
ClientInt
module-attribute
¶
ClientInt = Annotated[int, BeforeValidator(js_int)]
An int read through the client's int().
ParseInt
module-attribute
¶
ParseInt = Annotated[int, BeforeValidator(js_parse_int_or_zero)]
An int read through parseInt, NaN as 0.
ClientNumber
module-attribute
¶
ClientNumber = Annotated[int | float, BeforeValidator(_number_or_zero)]
A number read through Number(), an int kept as is; NaN and infinities as 0, as a time the client counts down.
js_string
¶
String(value): a number as JavaScript writes it, an object as [object Object].
js_int
¶
The client's int(): a #rrggbb string as hex, else Math.trunc(Number(value)), NaN as 0.
An infinity, which no int holds, reads as 0.
Client: int (dll line 16098)
js_number_or_none
¶
Number(value), None read as undefined; None where it gives NaN or an infinity, an int kept as is.
js_same_number
¶
Whether Number(value) is expected; 633.0 is 633.
row_is_at
¶
Whether a map row [area_type, x, y, ...] lies at (x, y).
movement_targets
¶
Whether a movement wrapper's target area row (M.TA), when it has one, lies at (x, y).
js_parse_int
¶
JavaScript's parseInt(value): the leading integer of String(value), None where it gives NaN.
"12abc" reads as 12, "1e3" as 1 and "0x1A" as 26.
js_truthy
¶
JavaScript's !!value: false only for null, false, 0, NaN and "".
network.connection¶
The connection: a receive thread routes each packet to its waiter, its subscribers and state.
It uses websocket-client with its own receive and keepalive threads, so it never competes with a host event loop.
Classes:
| Name | Description |
|---|---|
ResponseWaiter |
A waiter for a specific command response. |
Connection |
Synchronous WebSocket connection with threaded message routing. |
Attributes:
ResponseWaiter
dataclass
¶
ResponseWaiter(event: Event = Event(), result: Packet | None = None, error: Exception | None = None, accepts: Callable[[Packet], bool] | None = None, owed_until: float | None = None)
Connection
¶
Synchronous WebSocket connection with threaded message routing.
Features:
- Request/response pattern via waiters (consumed on match)
- Pub/sub pattern via subscribers (broadcast to all)
- Disconnect notification via
add_disconnect_listener - Automatic keepalive thread
- Thread-safe operations:
connectanddisconnectare serialized against each other and against the receive thread's shutdown by an internal lifecycle lock, so concurrent calls cannot leak a socket or let a dying session tear down its successor.
Correlation
The protocol carries no request id, and the game client never pairs a
reply with its request: every reply goes to the one handler for its
command id. So request runs one request per command id at a
time: a second caller for the same command waits for the first to be
answered (or to time out), and that wait counts against its timeout.
Different commands still run in parallel.
A reply goes to the oldest waiter for its command id that accepts it;
a successful reply goes to a waiter still waiting before one that is
only owed a reply and has a check (see below). A waiter with an
accepts check takes only the successful replies that pass it, plus
any error reply; other replies still reach state and subscribers.
Without a check, a server push with the same command id is taken as
the answer.
A reply nobody waits for any more is still owed: a request that timed
out, and send_and_drop_reply, leave their waiter in place to
take that reply when it comes, so it reaches state and subscribers but
no later request (see request).
Client: BasicSmartfoxClient.onExtensionResponse (ggs.dll line 7151) and
CastleExtensionResponseCommand (bundle line 110733), which hands each reply
to the command registered for its id.
listeners keeps the disconnect listeners, under disconnect; by default a store of their own.
Methods:
| Name | Description |
|---|---|
connect |
Connect to the WebSocket server. |
disconnect |
Disconnect from the server and cleanup resources. |
send |
Send data to the server. |
request |
Send data and wait for the response to |
send_and_drop_reply |
Send data whose reply under |
command_lock |
Hold |
create_waiter |
Register a waiter for the next reply under |
wait_for_result |
Wait for the reply a waiter from |
cancel_waiter |
|
wait_for |
Wait for a response with the given command ID. |
subscribe |
Subscribe to packets with the given command ID. |
add_disconnect_listener |
Register a callback fired on unexpected connection loss. |
remove_disconnect_listener |
Remove a listener added with |
run_if_current |
Run |
unsubscribe |
Remove a subscriber. |
Attributes:
| Name | Type | Description |
|---|---|---|
url |
|
|
keepalive_zone |
|
|
room_id |
|
|
ws |
WebSocket | None
|
|
on_packet |
Callable[[Packet], None] | None
|
|
on_disconnect |
Callable[[int], None] | None
|
|
after_disconnect |
Callable[[int], None] | None
|
|
connected |
bool
|
Check if connection is active. |
close_error |
BaseException | None
|
What ended the last connection, or None while connected or after a clean disconnect(). |
degraded_frames |
DegradedFrameCounts
|
Inbound frames that degraded to a raw wrapper, which match no waiter and are dropped. |
generation |
int
|
Counts the sessions: |
close_error
property
¶
close_error: BaseException | None
What ended the last connection, or None while connected or after a clean disconnect().
degraded_frames
property
¶
degraded_frames: DegradedFrameCounts
Inbound frames that degraded to a raw wrapper, which match no waiter and are dropped.
Counted for the whole process, not for this connection: frames are read without knowing which connection they came in on.
connect
¶
connect(timeout: float = 10.0) -> None
Connect to the WebSocket server.
Holds the lifecycle lock for the whole handshake, so a concurrent
connect() waits and then returns early instead of opening a second
socket that would be leaked when self.ws is overwritten. A
concurrent disconnect() likewise waits, and then tears down the
session this call established.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timeout
|
float
|
Connection timeout in seconds |
10.0
|
Raises:
| Type | Description |
|---|---|
NetworkError
|
The handshake failed (the underlying
websocket/socket error is chained as |
disconnect
¶
Disconnect from the server and cleanup resources.
Safe to call repeatedly, and safe to call from a subscriber callback on the receive thread (that thread is then not joined).
send
¶
send(data: str) -> None
Send data to the server.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
str
|
String data to send |
required |
Raises:
| Type | Description |
|---|---|
NetworkError
|
If not connected or the send fails |
request
¶
request(data: str, cmd_id: str, timeout: float = 5.0, accepts: Callable[[Packet], bool] | None = None) -> Packet
Send data and wait for the response to cmd_id.
Holds cmd_id (see command_lock) from before the send until
the reply, so only one request per command id is in flight; time spent
waiting for the lock counts against timeout. The waiter is
registered before the data is sent, so an immediate reply is not lost.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
str
|
The packet to send |
required |
cmd_id
|
str
|
The command id the reply arrives under |
required |
timeout
|
float
|
Seconds to wait, queueing included |
5.0
|
accepts
|
Callable[[Packet], bool] | None
|
Whether a successful reply is the answer to this request; replies it rejects still reach state and subscribers. Error replies carry nothing to check, so they are always taken. |
None
|
EmpireClient.send and request_packet pass the check of a request
model that defines accepts_reply (gaa, ssi, ain, grc, dfc, mcm, spl,
arc, jaa by position, csm, cra, cds, cat, and llsp, llsw and slse by
list): those replies name what was asked for. Most commands' replies do
not (gam, gcl, dcl, gli, gui, gpa, jca, bsd, wsp, the attack-info family,
chat and every write), and hgh replies name a list and league that need
not be the ones asked for (see GetHighscoreRequest).
A request that times out still owes its reply: its waiter stays
registered for timeout more seconds and takes that reply, or any
error, when it arrives, so no later request does. A later request
without a check sends only once that reply has arrived or the window
has passed, and then still has its whole timeout for its own
reply: after a lost reply it can take up to the earlier request's
timeout longer. One with a check sends at once, as its check already
tells a late success reply from its own, and a successful reply goes
to it before the owed waiter. Two things still get past this. A
server push under the command id is taken as the owed reply, and the
late reply then answers the next request as before. And when the
reply never comes, the owed waiter takes the next request's error, so
that request times out instead. Both assume the server answers one
command's requests in the order sent.
Raises:
| Type | Description |
|---|---|
EmpireTimeoutError
|
No response within |
ConnectionClosedError
|
Connection dropped while waiting |
NetworkError
|
The send itself failed |
ReceiveThreadError
|
Called on the receive thread, which alone could route the reply |
send_and_drop_reply
¶
Send data whose reply under cmd_id nobody waits for, and keep that reply from every request.
The reply is owed as a timed-out request's is (see request): it
reaches state and subscribers, and a request for cmd_id without a
check sends only once it has arrived or window has passed. Off the
receive thread it first takes command_lock, waiting up to
window for a request in flight, so the two replies cannot swap; on
the receive thread, which may not wait, it sends at once.
Raises:
| Type | Description |
|---|---|
NetworkError
|
If not connected or the send fails |
EmpireTimeoutError
|
A request for |
command_lock
¶
Hold cmd_id: no request for it runs on another thread until the block ends.
For work that sends cmd_id itself, several requests at once for
example, so that neither their replies nor their errors can land on a
concurrent request. Reentrant on the holding thread.
Threads waiting for it are not served in arrival order: under contention one can lose the race again and again, until its timeout.
Raises:
| Type | Description |
|---|---|
EmpireTimeoutError
|
Another thread held it for all of |
ReceiveThreadError
|
Called on the receive thread, where waiting for it would stall routing |
create_waiter
¶
create_waiter(cmd_id: str, accepts: Callable[[Packet], bool] | None = None) -> ResponseWaiter
Register a waiter for the next reply under cmd_id that accepts takes (see request).
wait_for_result
¶
wait_for_result(cmd_id: str, waiter: ResponseWaiter, timeout: float = 5.0) -> Packet
Wait for the reply a waiter from create_waiter receives, then drop the waiter.
Raises:
| Type | Description |
|---|---|
EmpireTimeoutError / ConnectionClosedError
|
as |
ReceiveThreadError
|
Called on the receive thread |
wait_for
¶
Wait for a response with the given command ID.
Note: only use this for server-pushed packets. For request/response
round trips use request, which registers the waiter before
sending.
Raises:
| Type | Description |
|---|---|
ReceiveThreadError
|
Called on the receive thread |
subscribe
¶
Subscribe to packets with the given command ID.
Unlike waiters, subscribers receive ALL matching packets and are not consumed.
Callbacks run on the receive thread, which routes every reply: they must
not block, and a call that waits for a reply raises ReceiveThreadError.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cmd_id
|
str
|
Command ID to subscribe to |
required |
callback
|
Callable[[Packet], None]
|
Function to call with matching packets |
required |
add_disconnect_listener
¶
add_disconnect_listener(callback: Callable[[], None]) -> None
Register a callback fired on unexpected connection loss.
Additive, unlike the single-slot on_disconnect attribute that
EmpireClient claims for its own
bookkeeping: several consumers can observe disconnects without
clobbering each other or patching the client's wiring. Registering the
same callback twice is a no-op; the client's
on_disconnect callbacks
are listeners of its connection, in the same list.
Not called by disconnect - only when the session drops on its
own. Callbacks run on the receive thread, outside every internal lock,
so a listener may call connect to reconnect.
remove_disconnect_listener
¶
remove_disconnect_listener(callback: Callable[[], None]) -> None
Remove a listener added with add_disconnect_listener.
No-op if the callback is not registered.
run_if_current
¶
Run action unless a newer session has started since generation; say whether it ran.
Holds the lifecycle lock while it runs, so no connect can start a
session in between: cleanup for a dropped session cannot touch its successor.
network.framing¶
Splitting the server's message stream into packets.
Classes:
| Name | Description |
|---|---|
FrameBuffer |
Turns WebSocket messages into packets, however the server batches or splits them. |
Attributes:
| Name | Type | Description |
|---|---|---|
logger |
|
FrameBuffer
¶
FrameBuffer(limit: int = MAX_FRAME_SIZE)
Turns WebSocket messages into packets, however the server batches or splits them.
A message may hold several packets, or only part of one. Every <msg>...</msg>
system message in the buffer is taken out first; the rest is kept until it ends
with %, then split on %xt into extension packets. Partial data waits for
the next message.
The client's system-message pattern is greedy, so two XML messages in one buffer
reach it as one; here each is its own packet. The buffer is also capped at
MAX_FRAME_SIZE, which the client does not do.
Client: BasicSmartfoxClient.onDataReady and handleExtMessage (ggs.dll lines 7211-7224).
Methods:
| Name | Description |
|---|---|
feed |
Add one message; return the packets it completes, system messages first, each in wire order. |
Attributes:
| Name | Type | Description |
|---|---|---|
pending |
str
|
Data kept for the next message: the start of a packet still arriving. |