Skip to content

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 gcu block.

CommanderEffect

One entry of a commander's E or AE: [effect_id, values, source].

PlayerInfo

Basic player information.

Functions:

Name Description
smartfox_text

Escape a string command param as the client does: each % becomes % and each ' is dropped.

json_text

payload as JSON.stringify writes it: no spaces, non-ASCII kept as is, numbers as JavaScript writes them.

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 model, so one unreadable entry costs only itself.

read_or_none

read(value), or None when validation fails, so an unreadable block costs only itself.

enum_or_none

The member of enum for a reply's int, or None when the client defines no such value.

Attributes:

Name Type Description
DEFAULT_ZONE
NO_ROOM
CurrencyBlock

A gcu block, or None when a reply sends none or something that is not an object.

CommanderEffects

[effect_id, values, source] rows; unreadable entries are skipped, as the client skips

T module-attribute

T = TypeVar('T')

DEFAULT_ZONE module-attribute

DEFAULT_ZONE = 'EmpireEx_21'

NO_ROOM module-attribute

NO_ROOM = -1

CurrencyBlock module-attribute

A gcu block, or None when a reply sends none or something that is not an object.

CommanderEffects module-attribute

[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

LLI class-attribute instance-attribute

LLI = 'lli'

LRE class-attribute instance-attribute

LRE = 'lre'

VPN class-attribute instance-attribute

VPN = 'vpn'

VLN class-attribute instance-attribute

VLN = 'vln'

LPP class-attribute instance-attribute

LPP = 'lpp'

VCK class-attribute instance-attribute

VCK = 'vck'

SLT class-attribute instance-attribute

SLT = 'slt'

ACM class-attribute instance-attribute

ACM = 'acm'

ACL class-attribute instance-attribute

ACL = 'acl'

AIN class-attribute instance-attribute

AIN = 'ain'

AKM class-attribute instance-attribute

AKM = 'akm'

ARM class-attribute instance-attribute

ARM = 'arm'

AIP class-attribute instance-attribute

AIP = 'aip'

AAL class-attribute instance-attribute

AAL = 'aal'

AAA class-attribute instance-attribute

AAA = 'aaa'

AQI class-attribute instance-attribute

AQI = 'aqi'

ADP class-attribute instance-attribute

ADP = 'adp'

ARD class-attribute instance-attribute

ARD = 'ard'

SAW class-attribute instance-attribute

SAW = 'saw'

ANL class-attribute instance-attribute

ANL = 'anl'

ADO class-attribute instance-attribute

ADO = 'ado'

AHL class-attribute instance-attribute

AHL = 'ahl'

AHH class-attribute instance-attribute

AHH = 'ahh'

AHD class-attribute instance-attribute

AHD = 'ahd'

AHF class-attribute instance-attribute

AHF = 'ahf'

AHC class-attribute instance-attribute

AHC = 'ahc'

AHA class-attribute instance-attribute

AHA = 'aha'

AHR class-attribute instance-attribute

AHR = 'ahr'

GCL class-attribute instance-attribute

GCL = 'gcl'

DCL class-attribute instance-attribute

DCL = 'dcl'

JCA class-attribute instance-attribute

JCA = 'jca'

ARC class-attribute instance-attribute

ARC = 'arc'

RST class-attribute instance-attribute

RST = 'rst'

GRC class-attribute instance-attribute

GRC = 'grc'

GPA class-attribute instance-attribute

GPA = 'gpa'

CRM class-attribute instance-attribute

CRM = 'crm'

CMI class-attribute instance-attribute

CMI = 'cmi'

KUT class-attribute instance-attribute

KUT = 'kut'

STI class-attribute instance-attribute

STI = 'sti'

KGT class-attribute instance-attribute

KGT = 'kgt'

MSK class-attribute instance-attribute

MSK = 'msk'

GAM class-attribute instance-attribute

GAM = 'gam'

GAA class-attribute instance-attribute

GAA = 'gaa'

FNM class-attribute instance-attribute

FNM = 'fnm'

GDI class-attribute instance-attribute

GDI = 'gdi'

WSP class-attribute instance-attribute

WSP = 'wsp'

ACI class-attribute instance-attribute

ACI = 'aci'

ADI class-attribute instance-attribute

ADI = 'adi'

ABI class-attribute instance-attribute

ABI = 'abi'

ALI class-attribute instance-attribute

ALI = 'ali'

AVI class-attribute instance-attribute

AVI = 'avi'

AII class-attribute instance-attribute

AII = 'aii'

COI class-attribute instance-attribute

COI = 'coi'

CCI class-attribute instance-attribute

CCI = 'cci'

CTI class-attribute instance-attribute

CTI = 'cti'

CRA class-attribute instance-attribute

CRA = 'cra'

CSM class-attribute instance-attribute

CSM = 'csm'

SSI class-attribute instance-attribute

SSI = 'ssi'

GAS class-attribute instance-attribute

GAS = 'gas'

SAS class-attribute instance-attribute

SAS = 'sas'

MSD class-attribute instance-attribute

MSD = 'msd'

SDC class-attribute instance-attribute

SDC = 'sdc'

CDS class-attribute instance-attribute

CDS = 'cds'

EBU class-attribute instance-attribute

EBU = 'ebu'

EUP class-attribute instance-attribute

EUP = 'eup'

EMO class-attribute instance-attribute

EMO = 'emo'

SBD class-attribute instance-attribute

SBD = 'sbd'

EDO class-attribute instance-attribute

EDO = 'edo'

FCO class-attribute instance-attribute

FCO = 'fco'

MSB class-attribute instance-attribute

MSB = 'msb'

EUD class-attribute instance-attribute

EUD = 'eud'

RBU class-attribute instance-attribute

RBU = 'rbu'

IRA class-attribute instance-attribute

IRA = 'ira'

EBE class-attribute instance-attribute

EBE = 'ebe'

ETC class-attribute instance-attribute

ETC = 'etc'

SCL class-attribute instance-attribute

SCL = 'scl'

CMR class-attribute instance-attribute

CMR = 'cmr'

RCC class-attribute instance-attribute

RCC = 'rcc'

BUP class-attribute instance-attribute

BUP = 'bup'

SPL class-attribute instance-attribute

SPL = 'spl'

BOU class-attribute instance-attribute

BOU = 'bou'

MCU class-attribute instance-attribute

MCU = 'mcu'

GUI class-attribute instance-attribute

GUI = 'gui'

DUP class-attribute instance-attribute

DUP = 'dup'

HRU class-attribute instance-attribute

HRU = 'hru'

HCS class-attribute instance-attribute

HCS = 'hcs'

HSS class-attribute instance-attribute

HSS = 'hss'

HDU class-attribute instance-attribute

HDU = 'hdu'

HRA class-attribute instance-attribute

HRA = 'hra'

DFC class-attribute instance-attribute

DFC = 'dfc'

DFK class-attribute instance-attribute

DFK = 'dfk'

DFW class-attribute instance-attribute

DFW = 'dfw'

DFM class-attribute instance-attribute

DFM = 'dfm'

SDI class-attribute instance-attribute

SDI = 'sdi'

SBP class-attribute instance-attribute

SBP = 'sbp'

GBC class-attribute instance-attribute

GBC = 'gbc'

SEI class-attribute instance-attribute

SEI = 'sei'

SEE class-attribute instance-attribute

SEE = 'see'

PEP class-attribute instance-attribute

PEP = 'pep'

HGH class-attribute instance-attribute

HGH = 'hgh'

LLSP class-attribute instance-attribute

LLSP = 'llsp'

LLSW class-attribute instance-attribute

LLSW = 'llsw'

SLSE class-attribute instance-attribute

SLSE = 'slse'

SEDE class-attribute instance-attribute

SEDE = 'sede'

SNE class-attribute instance-attribute

SNE = 'sne'

RMS class-attribute instance-attribute

RMS = 'rms'

MMR class-attribute instance-attribute

MMR = 'mmr'

AMS class-attribute instance-attribute

AMS = 'ams'

DMS class-attribute instance-attribute

DMS = 'dms'

SMS class-attribute instance-attribute

SMS = 'sms'

BSD class-attribute instance-attribute

BSD = 'bsd'

CLB class-attribute instance-attribute

CLB = 'clb'

GPG class-attribute instance-attribute

GPG = 'gpg'

QSC class-attribute instance-attribute

QSC = 'qsc'

QDR class-attribute instance-attribute

QDR = 'qdr'

FCQ class-attribute instance-attribute

FCQ = 'fcq'

CTR class-attribute instance-attribute

CTR = 'ctr'

GPI class-attribute instance-attribute

GPI = 'gpi'

VPM class-attribute instance-attribute

VPM = 'vpm'

GNCI class-attribute instance-attribute

GNCI = 'gnci'

CPNE class-attribute instance-attribute

CPNE = 'cpne'

SCP class-attribute instance-attribute

SCP = 'scp'

RMC class-attribute instance-attribute

RMC = 'rmc'

MNS class-attribute instance-attribute

MNS = 'mns'

CMC class-attribute instance-attribute

CMC = 'cmc'

FCS class-attribute instance-attribute

FCS = 'fcs'

MVF class-attribute instance-attribute

MVF = 'mvf'

OPT class-attribute instance-attribute

OPT = 'opt'

HFL class-attribute instance-attribute

HFL = 'hfl'

TXI class-attribute instance-attribute

TXI = 'txi'

TXS class-attribute instance-attribute

TXS = 'txs'

TXC class-attribute instance-attribute

TXC = 'txc'

GBL class-attribute instance-attribute

GBL = 'gbl'

RUI class-attribute instance-attribute

RUI = 'rui'

RMB class-attribute instance-attribute

RMB = 'rmb'

GLI class-attribute instance-attribute

GLI = 'gli'

ARL class-attribute instance-attribute

ARL = 'arl'

GEI class-attribute instance-attribute

GEI = 'gei'

EEQ class-attribute instance-attribute

EEQ = 'eeq'

GCS class-attribute instance-attribute

GCS = 'gcs'

SCT class-attribute instance-attribute

SCT = 'sct'

SIN class-attribute instance-attribute

SIN = 'sin'

SOB class-attribute instance-attribute

SOB = 'sob'

SDS class-attribute instance-attribute

SDS = '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')

to_payload

to_payload() -> dict[str, Any]

Convert the model to a payload dict suitable for sending.

Uses field aliases (e.g., "M" instead of "message") and excludes None values.

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 joinOK); -1 before one is joined

NO_ROOM

Client: BasicSmartfoxClient.sendCommandVO (dll line 7177)

get_command classmethod

get_command() -> str

Get the command code for this request type.

get_response_command classmethod

get_response_command() -> str

The command the server's answer to this request arrives under.

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:

error_code pydantic-field

error_code: int = 0

Wire key E

success property

success: bool

Whether the server reported no error for this response.

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:

model_config class-attribute

model_config: ConfigDict = ConfigDict(frozen=True)

received_at pydantic-field

received_at: float

When the values were read, in time.monotonic() seconds

TimedResponse pydantic-model

Bases: BaseResponse, TimedPayload

A TimedPayload that is a command's response.

Fields:

Position pydantic-model

Bases: EmpireModel

A position on the game map.

Fields:

x pydantic-field

x: int

Wire key X

y pydantic-field

y: int

Wire key Y

kingdom pydantic-field

kingdom: int = 0

Wire key KID

model_config class-attribute

model_config: ConfigDict = ConfigDict(populate_by_name=True)

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:

coins pydantic-field

coins: int | float | None = None

Wire key C1

Coins; None when the reply has no number for it

rubies pydantic-field

rubies: int | float | None = None

Wire key C2

Rubies; None when the reply has no number for it

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

effect_id pydantic-field

effect_id: int

Effect id

values pydantic-field

values: list[Any]

Value array; its layout depends on the effect type

source pydantic-field

source: str = ''

The key of the effect's source

PlayerInfo pydantic-model

Bases: EmpireModel

Basic player information.

Fields:

player_id pydantic-field

player_id: int

Wire key PID

player_name pydantic-field

player_name: str

Wire key PN

alliance_id pydantic-field

alliance_id: int | None = None

Wire key AID

alliance_name pydantic-field

alliance_name: str | None = None

Wire key AN

model_config class-attribute

model_config: ConfigDict = ConfigDict(populate_by_name=True)

smartfox_text

smartfox_text(text: str) -> str

Escape a string command param as the client does: each % becomes % and each ' is dropped.

Client: TextValide.getValideSmartFoxText (dll line 5816)

js_number_text

js_number_text(number: float) -> str

String(number): the ECMAScript Number::toString layout of the shortest round-trip digits.

json_text

json_text(payload: Any) -> str

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

object_or_none(value: Any) -> Any

A reply block that is an object, else None: the client reads keys off it only when it is one.

list_or_empty

list_or_empty(value: Any) -> Any

A reply value that is an array, else no entries.

int_entries

int_entries(value: Any, *, warn: Logger, what: str) -> list[int]

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.

read_or_none

read_or_none(read: Callable[[Any], _T], value: Any, *, warn: Logger | None = None, what: str = 'an entry') -> _T | None

read(value), or None when validation fails, so an unreadable block costs only itself.

enum_or_none

enum_or_none(enum: type[_E], value: int) -> _E | None

The member of enum for a reply's int, or None when the client defines no such value.

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 DegradedFrameCounts).

Attributes:

Name Type Description
logger
MALFORMED_STATUS_CODE
MAX_FRAME_SIZE
MAX_XML_SIZE
DEGRADED_FRAME_WARN_INTERVAL

logger module-attribute

logger = logging.getLogger(__name__)

MALFORMED_STATUS_CODE module-attribute

MALFORMED_STATUS_CODE = -1000

MAX_FRAME_SIZE module-attribute

MAX_FRAME_SIZE = 16 * 1024 * 1024

MAX_XML_SIZE module-attribute

MAX_XML_SIZE = 1024 * 1024

DEGRADED_FRAME_WARN_INTERVAL module-attribute

DEGRADED_FRAME_WARN_INTERVAL = 60.0

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.

total instance-attribute

total: int

Every one since the process started.

suppressed instance-attribute

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 BaseRequest.to_packet builds it.

from_bytes

Parse one message received from the server: an XML <msg> or one %xt% command.

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

raw_data instance-attribute

raw_data: str

is_xml instance-attribute

is_xml: bool

command_id class-attribute instance-attribute

command_id: str | None = None

request_id class-attribute instance-attribute

request_id: int = -1

error_code class-attribute instance-attribute

error_code: int = 0

payload class-attribute instance-attribute

payload: dict[str, Any] | list[Any] | Element | None = None

build_xt staticmethod

build_xt(zone: str, command: str, payload: dict[str, Any], room_id: int = NO_ROOM) -> str

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 joinOK); -1 before one is joined

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'

command_id is None and whose payload is None.

to_bytes

to_bytes() -> bytes

The frame as it goes on the wire, with no trailing null byte.

Client: BasicSmartfoxClient.sendCommand and sendXMLMessage (dll line 7199, 7203)

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 (lli), with a password or with the token an earlier persistent login got.

LoginResponse

The reply to lli.

LoginTokenResponse

The login token the server pushes after a persistent login (PL 1).

RegisterRequest

Register a new account (lre).

RegisterResponse

The reply to lre: the new player's id, or name suggestions when the name was refused.

CheckUsernameAvailableRequest

Check whether a name is free for a new player (vpn).

CheckUsernameAvailableResponse

The reply to vpn; a refusal carries free names like the refused one.

CheckUsernameExistsRequest

Check a login name (vln).

CheckUsernameExistsResponse

The reply to vln, which carries nothing the client reads.

PasswordRecoveryRequest

Ask for a password recovery email (lpp).

PasswordRecoveryResponse

The reply to lpp.

Functions:

Name Description
build_version_check

The vck frame the client sends once it has joined the lobby.

Attributes:

Name Type Description
VERSION_CHECK_PLATFORM

VERSION_CHECK_PLATFORM module-attribute

VERSION_CHECK_PLATFORM = 'web-html5'

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

login_id pydantic-field

login_id: int = 0

Wire key ID

Always 0

persistent_login pydantic-field

persistent_login: bool = False

Wire key PL

Stay logged in; the server then pushes a login token (slt)

username pydantic-field

username: str

Wire key NOM

The login name, encoded

password pydantic-field

password: str | None = None

Wire key PW

The password, encoded; None to log in by token

login_token pydantic-field

login_token: str | None = None

Wire key LT

A persistent login's token

language pydantic-field

language: str = 'en'

Wire key LANG

Language code of the chosen country

distributor_id pydantic-field

distributor_id: str = '0'

Wire key DID

Distributor id of the install

account_id pydantic-field

account_id: str

Wire key AID

Account (install) id

identity_management_id pydantic-field

identity_management_id: str = ''

Wire key KID

Identity management id, empty outside Korea

referrer pydantic-field

referrer: str = ''

Wire key REF

The page the game was opened from

gci pydantic-field

gci: str = ''

Wire key GCI

The page's gci URL parameter

store_id pydantic-field

store_id: int = 9

Wire key SID

Store id: 9 for the web

platform_id pydantic-field

platform_id: int = 1

Wire key PLFID

Platform id: 1 for the web

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.

to_payload

to_payload() -> dict[str, Any]

The payload in C2SLoginVO's key order: a password nulls the token, a missing key is left out.

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:

account_deleted pydantic-field

account_deleted: bool = False

Wire key GDPR

The banned account was deleted

remaining_ban_seconds pydantic-field

remaining_ban_seconds: _Number = None

Wire key RS

Seconds until the ban ends

instance_id pydantic-field

instance_id: _Number = None

Wire key IID

Instance id of the server the account is on

remaining_cooldown_seconds pydantic-field

remaining_cooldown_seconds: _Number = None

Wire key CD

Seconds until the next login attempt is allowed

player_id pydantic-field

player_id: _Number = None

Wire key PID

The player's id

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:

login_token pydantic-field

login_token: str | None = None

Wire key LT

The token

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

distributor_id: int = 0

Wire key DID

Distributor id of the install

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

campaign_creative pydantic-field

campaign_creative: int = 0

Wire key campainCr

Campaign creative

campaign_landing_page pydantic-field

campaign_landing_page: int = 0

Wire key campainLP

Campaign landing page

ad_id pydantic-field

ad_id: int = 0

Wire key adID

Campaign ad id

time_zone pydantic-field

time_zone: int = 0

Wire key timeZone

UTC offset in hours plus 13

username pydantic-field

username: str

Wire key PN

The new player's name

password pydantic-field

password: str | None = None

Wire key PW

The new account's password

referrer pydantic-field

referrer: str = ''

Wire key REF

The page the game was opened from

language pydantic-field

language: str = 'en'

Wire key LANG

Language code of the chosen country

account_id pydantic-field

account_id: str

Wire key AID

Account (install) id

gci pydantic-field

gci: str = ''

Wire key GCI

The page's gci URL parameter

store_id pydantic-field

store_id: int = 9

Wire key SID

Store id: 9 for the web

platform_id pydantic-field

platform_id: int = 1

Wire key PLFID

Platform id: 1 for the web

network_id pydantic-field

network_id: int

Wire key NID

Network id of the install

recaptcha_token pydantic-field

recaptcha_token: str | None = None

Wire key RCT

A reCAPTCHA v3 token for the action 'submit'

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:

player_id pydantic-field

player_id: _Number = None

Wire key PID

The new player's id

suggested_names pydantic-field

suggested_names: _Names

Wire key NS

Free names like the refused one

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:

username pydantic-field

username: str

Wire key PN

The name to check

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:

suggested_names pydantic-field

suggested_names: _Names

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:

username pydantic-field

username: str

Wire key NOM

The name to check

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:

email pydantic-field

email: str

Wire key MAIL

Email address of the account

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

build_version_check(zone: str, build_number: str, session_id: str, room_id: int) -> str

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 &percnt; 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 text as a name: not blank, and none of SMARTFOX_INVALID_CHARS.

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_json_text(text: str) -> str

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_json_text(text: str | None) -> str

Decode server text the way the client does.

Replaces &percnt;, &quot;, &145;, <br /> and %5C in that order and turns square brackets into spaces; nothing reads as "".

Client: TextValide.parseChatJSONMessage (dll line 5820)

is_smartfox_valid

is_smartfox_valid(text: str) -> bool

Whether the client accepts text as a name: not blank, and none of SMARTFOX_INVALID_CHARS.

Client: TextValide.isSmartFoxValide (dll line 5777)

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 int(): a #rrggbb string as hex, else Math.trunc(Number(value)), NaN as 0.

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.

js_parse_int_or_zero

js_parse_int with NaN read as 0.

js_truthy

JavaScript's !!value: false only for null, false, 0, NaN and "".

js_falsy

JavaScript's !value.

js_loose_equals

JavaScript's number == value, as the client compares a reply value with a literal.

Attributes:

Name Type Description
ClientInt

An int read through the client's int().

ParseInt

An int read through parseInt, NaN as 0.

ClientNumber

A number read through Number(), an int kept as is; NaN and infinities as 0, as a time the client counts down.

ClientInt module-attribute

An int read through the client's int().

ParseInt module-attribute

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

js_string(value: Any) -> str

String(value): a number as JavaScript writes it, an object as [object Object].

js_int

js_int(value: Any) -> 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

js_number(value: Any) -> float

Number(value), NaN and infinities as 0.

js_number_or_none

js_number_or_none(value: Any) -> int | float | None

Number(value), None read as undefined; None where it gives NaN or an infinity, an int kept as is.

js_same_number

js_same_number(value: Any, expected: int) -> bool

Whether Number(value) is expected; 633.0 is 633.

row_is_at

row_is_at(row: Any, x: int, y: int) -> bool

Whether a map row [area_type, x, y, ...] lies at (x, y).

movement_targets

movement_targets(wrapper: Any, x: int, y: int) -> bool

Whether a movement wrapper's target area row (M.TA), when it has one, lies at (x, y).

js_floor

js_floor(value: Any) -> int

Math.floor(Number(value)), NaN and infinities as 0.

js_parse_int

js_parse_int(value: Any) -> int | None

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_parse_int_or_zero

js_parse_int_or_zero(value: Any) -> int

js_parse_int with NaN read as 0.

js_truthy

js_truthy(value: Any) -> bool

JavaScript's !!value: false only for null, false, 0, NaN and "".

js_falsy

js_falsy(value: Any) -> bool

JavaScript's !value.

js_loose_equals

js_loose_equals(value: Any, number: float) -> bool

JavaScript's number == value, as the client compares a reply value with a literal.

null never equals a number; anything else is compared as Number(value), so "1", "1.0", true and [1] all equal 1, and "" equals 0.

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:

Name Type Description
logger
NON_ERROR_COMMANDS
LOGIN_REPLY_COMMANDS
AUTH_COMMANDS
LOG_FRAME_CHARS
DEFAULT_KEEPALIVE_ZONE
SOCKET_POLL_TIMEOUT
KEEPALIVE_INTERVAL
SESSION_IDLE_TIMEOUT
THREAD_JOIN_TIMEOUT
REPLY_CHECK_WARN_INTERVAL

logger module-attribute

logger = logging.getLogger(__name__)

NON_ERROR_COMMANDS module-attribute

NON_ERROR_COMMANDS = {'rlu', 'core_pol'}

LOGIN_REPLY_COMMANDS module-attribute

LOGIN_REPLY_COMMANDS = frozenset({'lli', 'vck'})

AUTH_COMMANDS module-attribute

AUTH_COMMANDS = frozenset({'lli', 'core_reg', 'scp'})

LOG_FRAME_CHARS module-attribute

LOG_FRAME_CHARS = 100

DEFAULT_KEEPALIVE_ZONE module-attribute

DEFAULT_KEEPALIVE_ZONE = 'EmpireEx_21'

SOCKET_POLL_TIMEOUT module-attribute

SOCKET_POLL_TIMEOUT = 1.0

KEEPALIVE_INTERVAL module-attribute

KEEPALIVE_INTERVAL = 60.0

SESSION_IDLE_TIMEOUT module-attribute

SESSION_IDLE_TIMEOUT = KEEPALIVE_INTERVAL * 3

THREAD_JOIN_TIMEOUT module-attribute

THREAD_JOIN_TIMEOUT = 2.0

REPLY_CHECK_WARN_INTERVAL module-attribute

REPLY_CHECK_WARN_INTERVAL = 60.0

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)

A waiter for a specific command response.

Attributes:

Name Type Description
event Event
result Packet | None
error Exception | None
accepts Callable[[Packet], bool] | None
owed_until float | None

event class-attribute instance-attribute

event: Event = field(default_factory=threading.Event)

result class-attribute instance-attribute

result: Packet | None = None

error class-attribute instance-attribute

error: Exception | None = None

accepts class-attribute instance-attribute

accepts: Callable[[Packet], bool] | None = None

owed_until class-attribute instance-attribute

owed_until: float | None = None

Connection

Connection(url: str, keepalive_zone: str | None = None, listeners: Registry | None = None)

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: connect and disconnect are 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 cmd_id.

send_and_drop_reply

Send data whose reply under cmd_id nobody waits for, and keep that reply from every request.

command_lock

Hold cmd_id: no request for it runs on another thread until the block ends.

create_waiter

Register a waiter for the next reply under cmd_id that accepts takes (see request).

wait_for_result

Wait for the reply a waiter from create_waiter receives, then drop the waiter.

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

run_if_current

Run action unless a newer session has started since generation; say whether it ran.

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: connect starts a new one.

url instance-attribute

url = url

keepalive_zone instance-attribute

keepalive_zone = keepalive_zone

room_id instance-attribute

room_id = NO_ROOM

ws instance-attribute

ws: WebSocket | None = None

on_packet instance-attribute

on_packet: Callable[[Packet], None] | None = None

on_disconnect instance-attribute

on_disconnect: Callable[[int], None] | None = None

after_disconnect instance-attribute

after_disconnect: Callable[[int], None] | None = None

connected property

connected: bool

Check if connection is active.

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.

generation property

generation: int

Counts the sessions: connect starts a new one.

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 __cause__)

disconnect

disconnect() -> None

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 timeout

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_and_drop_reply(data: str, cmd_id: str, window: float = 5.0) -> None

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 cmd_id held the lock for all of window

command_lock

command_lock(cmd_id: str, timeout: float = 5.0) -> Iterator[None]

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 timeout

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 request

ReceiveThreadError

Called on the receive thread

cancel_waiter

cancel_waiter(cmd_id: str, waiter: ResponseWaiter) -> None

wait_for

wait_for(cmd_id: str, timeout: float = 5.0) -> Packet

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(cmd_id: str, callback: Callable[[Packet], None]) -> None

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_if_current(generation: int, action: Callable[[], None]) -> bool

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.

unsubscribe

unsubscribe(cmd_id: str, callback: Callable[[Packet], None]) -> None

Remove a subscriber.

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

logger module-attribute

logger = logging.getLogger(__name__)

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.

pending property

pending: str

Data kept for the next message: the start of a packet still arriving.

feed

feed(message: str) -> list[str]

Add one message; return the packets it completes, system messages first, each in wire order.

Extension packets are returned with their %xt prefix, as Packet.from_bytes reads them.