Client¶
EmpireClient, its configuration, accounts and the account pool, and the base every service shares.
client.client¶
The client: logs in the way the game client does and carries every service.
The connection runs on its own threads, so the client never competes with a host event loop.
Classes:
| Name | Description |
|---|---|
EmpireClient |
Empire client for connecting to GGE game servers. |
Attributes:
| Name | Type | Description |
|---|---|---|
logger |
|
|
LOGIN_SECTION_PUSHES |
|
|
T |
|
EmpireClient
¶
EmpireClient(username: str | None = None, password: str | None = None, config: EmpireConfig | None = None, login_token: str | None = None, keep_session: bool = False)
Empire client for connecting to GGE game servers.
This client uses blocking I/O with a background receive thread, making it safe to use from Discord.py without blocking the event loop (run client operations in a thread pool).
Usage
Keeping the session
With keep_session=True (or the attribute set later), a logged-in
session that drops is logged in again on a thread of its own, on the
same client, so callbacks and listen streams carry on.
is_restoring_session is True while it runs,
on_session_retry fires for each failed attempt,
on_session_restored once it is back, and
on_session_lost when it gives up. close ends it, also
while it waits between attempts, and a login of your own stops
it before it logs in.
The game client does not do this: after a drop
(CastleConnectionLostCommand.execute, bundle line 120253) it
empties the game and shows a reconnect dialog, and logs in again only
when the player clicks it (onReconnect, bundle line 120256). So
the delays are library policy, set on the config: the first attempt comes
config.relogin_first_delay (5) seconds after the drop, a failed one
is retried after twice the last delay, up to config.relogin_max_delay
(300) seconds. A longer first delay leaves a person who logged the same
account in elsewhere, and so dropped this session, time to play before
the re-login drops theirs. login(retry=True) retries a first login
on the same delays.
A LoginCooldownError counts as a
failed attempt too, and is retried no sooner than the seconds the
refusal named (see remaining_login_cooldown): after
the longer of that cooldown and the delay. A refusal that waiting does
not cure (a ban, the wrong server, the client version, credentials, or
any other refusal) ends the attempts, logged as an error and reported
to on_session_lost; the client then stays logged out. A
re-login sends no reCAPTCHA token.
Methods:
| Name | Description |
|---|---|
on_disconnect |
Register a callback for the session dropping on its own; |
listen |
Stream callback calls to the running event loop: every registration's, or those of |
close_streams |
End every stream of |
login |
Log in the way the game client does. |
remaining_login_cooldown |
Seconds until the login cooldown the server last named is over; 0 when there is none. |
on_session_lost |
Register a callback for a |
on_session_retry |
Register a callback for a failed login attempt that is tried again. |
on_session_restored |
Register a callback for a |
close |
Disconnect from the server and release background resources. |
load_game_data |
Load the static game data (unit and tool stats) now; optional. |
frame |
The request as this session sends it: in its zone and the room it joined. |
request_packet |
Send a request and return the raw reply packet for |
send |
Send a request to the server using protocol models. |
request |
Send a request and return its typed response. |
Attributes:
| Name | Type | Description |
|---|---|---|
alliance |
AllianceService
|
|
castle |
CastleService
|
|
army |
ArmyService
|
|
attack |
AttackService
|
|
commanders |
CommandersService
|
|
equipment |
EquipmentService
|
|
skills |
SkillsService
|
|
spy |
SpyService
|
|
ranking |
RankingService
|
|
map |
MapService
|
|
messages |
MessagesService
|
|
movements |
MovementsService
|
|
defense |
DefenseService
|
|
player |
PlayerService
|
|
events |
EventsService
|
|
rewards |
RewardsService
|
|
config |
|
|
username |
|
|
password |
|
|
login_token |
|
|
session_id |
|
|
connection |
|
|
state |
|
|
is_logged_in |
|
|
keep_session |
|
|
is_restoring_session |
bool
|
Whether a |
game_data |
GameData | None
|
The static game data the services read, or None while none is loaded. |
connection
instance-attribute
¶
connection = Connection(self.config.game_url, keepalive_zone=self.config.default_zone, listeners=self._registry)
is_restoring_session
property
¶
is_restoring_session: bool
Whether a keep_session re-login is running: logging in, or waiting between attempts.
True from just after the on_disconnect callbacks of a
logged-in session that dropped until the session is back (before
on_session_restored fires), the attempts give up (before
on_session_lost fires), close or a login of
your own ends them. False during login(retry=True), which is not a
restore.
It is still False inside the on_disconnect callbacks, as the
re-login starts after them. To follow a restore, register
on_session_retry, on_session_restored and
on_session_lost instead.
Library bookkeeping: the client restores no session by itself.
game_data
property
writable
¶
game_data: GameData | None
The static game data the services read, or None while none is loaded.
The data the process loaded (GameData.loaded, which
load_game_data, every GameData.load and the first API that
needs the data fill), the same instance the troop counts read. Reading it
never loads anything. Setting it attaches data of your own, e.g.
GameData.parse of a saved payload, which this client then reads
instead; setting None goes back to the process's data.
on_disconnect
¶
Register a callback for the session dropping on its own; close does not fire it.
Runs on the receive thread as it shuts down, after is_logged_in is
cleared, and fires once per dropped session. Keep it short and hand a
re-login to another thread, or leave it to keep_session, which
starts its own once these callbacks have run. Registering the same
callback twice is a no-op, and so is removing one not registered.
The callbacks are the connection's disconnect listeners
(:meth:Connection.add_disconnect_listener
<empire_core.network.connection.Connection.add_disconnect_listener>):
one list, run in the order registered through either, and either
on_disconnect.remove or remove_disconnect_listener takes one out.
Unregister a callback with on_disconnect.remove(callback).
listen
¶
listen(*sources: Callable[..., object], names: Iterable[str] | None = None, maxsize: int = 0) -> EventStream[Unpack[tuple[Any, ...]]]
Stream callback calls to the running event loop: every registration's, or those of sources and names.
Call it from a coroutine; the stream delivers on that coroutine's loop. sources
are the registration methods themselves (client.state.on_incoming_attack,
client.alliance.on_chat_message, ...): any on_<name> event of the client, its
state or a service. Each event is a
ClientEvent named after its registration. A stream
of one registration types its events' args as that registration's callback
parameters: client.listen(client.state.on_incoming_attack_updated) yields
ClientEvent[Movement, Movement]. The movement callbacks, which take either
(movement_id) or (movement_id, movement), and streams of several
registrations yield untyped args.
names picks registrations by the event names instead, for subscriptions kept as
data: client.listen(names={"incoming_attack", "chat_message"}). A name is the
registration's without on_, as ClientEvent has
it, and an unknown one raises ValueError here, not later. A stream by name has
untyped args. sources and names can be given together; the stream then has
the registrations of both. Only with neither does it have every registration: an
empty names alone picks none.
The stream listens inside async with only: entering it subscribes, leaving it
stops listening, and iterating a stream that was not entered raises RuntimeError.
Inside the block it outlives sessions, as callbacks do: a dropped session is a
"disconnect" event, and after close and a new login the stream
delivers the new session's events. Only leaving the block, stream.close() and
close_streams end it.
Nothing is dropped while the loop keeps up. A maxsize above 0 caps the events
waiting unread: one more stops the stream, which raises
EventStreamOverflowError once the consumer reaches it.
Raises:
| Type | Description |
|---|---|
RuntimeError
|
No event loop is running. |
ValueError
|
A source is not a callback registration of this client, a name is not
one's, or |
Example::
close_streams
¶
End every stream of listen still listening, after the events already on their way.
close leaves streams listening, as it leaves callbacks registered. A client
handed on to other code, as an AccountPool release does,
calls this so no stream of the previous holder keeps queueing.
login
¶
Log in the way the game client does.
- Connect the WebSocket
verChk, answered byapiOK- Zone login with the build number, answered by
rlu autoJoin, answered byjoinOKwith the room idroundTripand the version checkvcklli, with the measured connection and round-trip times
With password set it logs in by password; without one, by the
login_token an earlier login got. After a login the server
pushes a fresh token, kept in login_token; the push comes after
gbd, so it lands shortly after this method returns.
A keep_session re-login still under way, or waiting between
attempts, is ended first, so only this login runs:
is_restoring_session turns False, and neither
on_session_restored nor on_session_lost fires for it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
recaptcha_token
|
str | Callable[[], str] | None
|
A reCAPTCHA v3 token for the action |
None
|
retry
|
bool
|
Retry a failed login, as |
False
|
Raises:
| Type | Description |
|---|---|
NetworkError
|
The WebSocket connection could not be established, or the live
client's version could not be downloaded ( |
EmpireTimeoutError
|
A required login step timed out |
ClientVersionError
|
The version check says the client version is too low or too high |
LoginCooldownError
|
The server is rate-limiting this account |
AccountBannedError
|
The account is banned or deleted |
WrongServerError
|
The account is on another server |
LoginError
|
Username and password or token missing, the live client's files name no version, the version check failed, or the server rejected the login |
Every failure mode is an EmpireError subclass, so
except EmpireError catches all of them.
On any of these, the connection and its background threads are closed before the error propagates, so a failed login leaks nothing.
Client: BasicSmartfoxClient (dll line 7130), BasicJoinedRoomCommand (dll line 33011),
CastleLoginCommand (bundle line 131762)
remaining_login_cooldown
¶
remaining_login_cooldown() -> float
Seconds until the login cooldown the server last named is over; 0 when there is none.
Only what the server last said: the CD of the last refused login of
this client, less the time since, cleared by a login that holds. The
server may start a cooldown this client has not been told about yet.
Client: LLICommand.executeCommand (bundle line 120673) shows the
refusal's REMAINING_COOLDOWN in a timer dialog.
on_session_lost
¶
on_session_lost(callback: Callable[[Exception], object]) -> None
Register a callback for a keep_session re-login that gives up.
callback(error) fires once, with the refusal that ended the attempts
(a ban, the wrong server, the client version, credentials, or any other
refusal waiting does not cure); the client then stays logged out until
a login of your own. Neither close nor a login
of your own that stops the re-login fires it; that login raises its own
failure. Runs on the callback thread (see
GameState). Registering the same
callback twice is a no-op, and so is removing one not registered.
Unregister a callback with on_session_lost.remove(callback).
on_session_retry
¶
Register a callback for a failed login attempt that is tried again.
callback(attempt, wait, error) fires for each failed attempt of a
keep_session re-login or of login(retry=True): the attempts
so far, the seconds until the next (the backoff delay, or the server's
login cooldown if that is longer), and the attempt's failure. It fires
once the failed attempt's connection is closed, on the callback thread
(see GameState), which the attempts
keep, so the callbacks run in order. An attempt that close or a
login of your own ended does not fire it.
Registering the same callback twice is a no-op, and so is removing one
not registered.
Unregister a callback with on_session_retry.remove(callback).
on_session_restored
¶
Register a callback for a keep_session re-login that holds.
Fires once per restored session, after the login data and the movement
list the server pushes after it have reached state (or that list did
not come within config.request_timeout), so the events of that
state come first. Runs on the callback thread (see
GameState). A login of
your own does not fire it. Registering the same callback twice is a no-op,
and so is removing one not registered.
Unregister a callback with on_session_restored.remove(callback).
close
¶
Disconnect from the server and release background resources.
Safe to call more than once, and safe to call after a failed login.
Registered callbacks and the streams of listen stay, as on a
dropped session: they keep working after the next login.
Ends a keep_session re-login, also one waiting between
attempts, and returns once it has stopped: no re-login follows a
close until the next login. The game client's logout
likewise marks the disconnect as the user's (userForcedDisconnect,
ggs.dll line 7170), and with it set does not log in again by the stored
token (VCKLegacyCommand.executeCommand, bundle line 121066).
load_game_data
¶
Load the static game data (unit and tool stats) now; optional.
The APIs that need the data load it on first use, so the first of them
may wait on the disk cache or, on a cache miss, a large download. Call
this before timing-sensitive work to take that cost up front, or with
refresh=True to download again. It is GameData.load, so the
data is kept for the process and cached on disk per game version, and
this is cheap after the first call; client.game_data and the troop
counts read what it loaded.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
refresh
|
bool
|
Ignore any cached copy and any failure backoff, and re-download |
False
|
cache_dir
|
str | Path | None
|
Where to keep trimmed data (default: XDG cache dir) |
None
|
Returns:
| Type | Description |
|---|---|
GameData
|
The loaded data, also available as |
GameData
|
your own is attached there |
Raises:
| Type | Description |
|---|---|
NetworkError
|
Nothing is loaded (or refresh is set) and the CDN could not be reached, now or less than five minutes ago |
frame
¶
frame(request: BaseRequest) -> str
The request as this session sends it: in its zone and the room it joined.
Client: BasicSmartfoxClient.sendMessage and sendCommand (ggs.dll lines 7174 and 7198)
request_packet
¶
request_packet(request: BaseRequest, response_command: str, timeout: float = 5.0) -> Packet
Send a request and return the raw reply packet for response_command.
For replies whose command id the response registry gives to another
model (hgh answers both alliance search and highscores), or that a
caller reads itself. Raises as Connection.request does.
send
¶
send(request: BaseRequest, wait: bool = False, timeout: float = 5.0) -> BaseResponse | None
Send a request to the server using protocol models.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
BaseRequest
|
The request model to send |
required |
wait
|
bool
|
Whether to wait for a response |
False
|
timeout
|
float
|
Timeout in seconds when waiting |
5.0
|
Returns:
| Type | Description |
|---|---|
BaseResponse | None
|
The parsed response if wait=True, otherwise None |
With wait=True, requests for one command run one at a time across
threads, and the wait for an earlier one counts against timeout.
With wait=False the reply, if the command has one, is not paired
with this send: it can answer a concurrent waiting request for that
command that has no reply check (see Connection.request).
Raises:
| Type | Description |
|---|---|
CommandError
|
The server answered with a non-zero error code |
PacketError
|
The response payload did not match the response model |
EmpireTimeoutError
|
No response within |
ConnectionClosedError
|
Connection dropped while waiting |
NetworkError
|
The send itself failed |
ReceiveThreadError
|
Called with |
request
¶
request(request: BaseRequest, response_type: type[T], timeout: float = 5.0) -> T
Send a request and return its typed response.
Like send(request, wait=True) but verifies the parsed response
is of the expected type, so callers get a non-optional result.
Raises:
| Type | Description |
|---|---|
PacketError
|
The response could not be parsed as |
CommandError / EmpireTimeoutError / ConnectionClosedError / NetworkError
|
See |
client.session¶
client.stream¶
Every client callback as one asyncio stream, for code that runs on an event loop.
The library stays threaded; a stream of EmpireClient.listen subscribes to the
callbacks inside async with and hands each call to a loop.
The game client decides none of this: it is library threading.
Classes:
| Name | Description |
|---|---|
ClientEvent |
One callback call, as an |
CallbackSource |
An event the client, its state or a service declares as |
EventStream |
The calls of some callback registrations, delivered on an event loop in packet order. |
Functions:
| Name | Description |
|---|---|
callback_sources |
Every event the client, its state and its services declare ( |
Attributes:
| Name | Type | Description |
|---|---|---|
Args |
|
ClientEvent
dataclass
¶
One callback call, as an EventStream delivers it.
name is the registration's name without on_: "incoming_attack" for
client.state.on_incoming_attack, "chat_message" for client.alliance.on_chat_message.
args are what a callback registered there is called with: one model for most,
(old, new) for "incoming_attack_updated" and "occupation_updated",
(movement, captured) for "occupation_ended",
(movement_id, movement) for the movement callbacks, (error,) for "session_lost",
(attempt, wait, error) for "session_retry",
none for "disconnect" and "session_restored". From a stream of one registration,
args is typed as that registration's callback parameters.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
|
args |
tuple[Unpack[Args]]
|
|
CallbackSource
dataclass
¶
CallbackSource(name: str, register: Callable[[Callable[..., Any]], object], unregister: Callable[[Callable[..., Any]], None], on_callback_thread: bool)
EventStream
¶
EventStream(client: EmpireClient, sources: list[CallbackSource], loop: AbstractEventLoop, maxsize: int)
The calls of some callback registrations, delivered on an event loop in packet order.
Made by EmpireClient.listen. Iterate it with async for; each item is a
ClientEvent. Every call reaches the loop through the state callback thread,
the state's own callbacks as they run there and the others queued behind them from
the receive thread, so the events come in the order their packets came, whatever
registration they belong to.
It listens for the length of one async with: entering subscribes, leaving (or
close) stops listening, and a stream not entered refuses to be iterated.
EmpireClient.close_streams ends it after the events already on their way.
Sessions do not: a session that drops is a "disconnect" event, and after
EmpireClient.close the stream keeps listening, so it delivers the next
login's events, as the callbacks do.
Methods:
| Name | Description |
|---|---|
close |
Stop listening and end the stream after the events it already holds. Safe from any thread. |
close
¶
Stop listening and end the stream after the events it already holds. Safe from any thread.
callback_sources
¶
callback_sources(client: EmpireClient) -> dict[str, CallbackSource]
Every event the client, its state and its services declare (Event), by name.
config¶
Client configuration and the game's server list.
Classes:
| Name | Description |
|---|---|
NetworkInstance |
One game server (world) of a network, as |
EmpireConfig |
Configuration for EmpireCore. |
Functions:
| Name | Description |
|---|---|
fetch_client_version |
The live game client's version, read from the client's own files. |
network_config_url |
The URL of a network's |
parse_network_instances |
The servers a |
fetch_network_instances |
Download a network's |
instance_for_zone |
The server a world (zone) lives on, from the network's |
Attributes:
| Name | Type | Description |
|---|---|---|
GAME_CLIENT_URL |
Where the HTML5 game client is served from: its |
|
EMPIRE_GAME_ID |
Goodgame Empire's game id, as in its |
|
EMPIRE_NETWORK_ID |
The network of Goodgame Empire's international worlds ( |
|
CDN_SUB_DOMAIN |
The CDN host the client loads files from: |
|
FALLBACK_CDN_SUB_DOMAIN |
The host the client falls back to when its CDN fails ( |
GAME_CLIENT_URL
module-attribute
¶
Where the HTML5 game client is served from: its index.html and bundles.
LOGIN_DEFAULTS
module-attribute
¶
LOGIN_DEFAULTS: dict[str, Any] = {'ID': 0, 'PL': 1, 'LANG': 'en', 'DID': '0', 'AID': AID, 'KID': '', 'REF': 'https://empire.goodgamestudios.com', 'GCI': '', 'SID': 9, 'PLFID': 1}
EMPIRE_GAME_ID
module-attribute
¶
Goodgame Empire's game id, as in its games-netconf and language file URLs.
EMPIRE_NETWORK_ID
module-attribute
¶
The network of Goodgame Empire's international worlds (EmpireEx_*).
CDN_SUB_DOMAIN
module-attribute
¶
The CDN host the client loads files from: files- and the active CDN's abbreviation (Akamai, ak).
Client: cdnSubDomain (dll line 34406), CDN.abbreviation (dll lines 10973-10978)
FALLBACK_CDN_SUB_DOMAIN
module-attribute
¶
The host the client falls back to when its CDN fails (_useFallbackCDN, dll line 34406).
NetworkInstance
pydantic-model
¶
Bases: EmpireModel
One game server (world) of a network, as network.xml lists it.
Client: NetworkXMLParser (dll line 20264)
Fields:
-
instance_id(int) -
server(str) -
port(int) -
zone(str) -
zone_id(int) -
instance_number(int) -
is_international(bool) -
is_favorite(bool) -
default_country(str) -
instance_loca_id(str) -
countries(list[str])
EmpireConfig
pydantic-model
¶
Bases: EmpireModel
Configuration for EmpireCore. Defaults can be overridden by passing arguments to EmpireClient or (in the future) loading from environment variables/files.
Instances are mutable: build one and assign to it, or pass field values to
the constructor. The one exception is the shared default_config
below, which is frozen. An assignment is validated as the constructor
validates, and a value the field does not take raises ValidationError.
Fields:
-
game_url(str) -
default_zone(str) -
game_version(str) -
client_version(str | None) -
connection_timeout(float) -
login_timeout(float) -
request_timeout(float) -
map_tiles_per_second(float) -
relogin_first_delay(float) -
relogin_max_delay(float) -
username(str | None) -
password(str | None)
Validators:
game_version
pydantic-field
¶
game_version: str = '166'
SmartFox API version the verChk handshake sends
client_version
pydantic-field
¶
client_version: str | None = None
Game client version whose build number the login and version check send; None reads the live client's version at each login (fetch_client_version)
map_tiles_per_second
pydantic-field
¶
map_tiles_per_second: float = 25000.0
Map tiles an account reads per second at most; the server bans an account for 12 hours past about 810,000 tiles in 30 seconds, about 27,000 a second (see empire_core.map.pacing)
relogin_first_delay
pydantic-field
¶
relogin_first_delay: float = 5.0
Seconds from a drop to the first keep_session re-login, and from a failed login(retry=True) attempt to the next; above 0 and at most relogin_max_delay
relogin_max_delay
pydantic-field
¶
relogin_max_delay: float = 300.0
The longest wait between two login attempts in seconds; each failure doubles it. At least relogin_first_delay: raise this one first when raising both on an existing config
relogin_delays_in_order
pydantic-validator
¶
relogin_delays_in_order() -> EmpireConfig
Refuse a first re-login delay longer than the cap on the delays.
for_instance
classmethod
¶
for_instance(instance: NetworkInstance, **fields: Any) -> EmpireConfig
A config for one server of network.xml: its URL and zone, and fields for the rest.
generate_aid
¶
generate_aid() -> str
A fresh account (install) id, made as the client makes one for a new install.
Epoch milliseconds followed by a random number below 999999, not padded.
Client: AccountCookie (dll line 9088)
resolve_aid
¶
resolve_aid() -> str
Return the AID to send to the server.
Reads $EMPIRE_AID if it is set to something non-blank, otherwise
generates one. The server treats this as a stable per-install identifier, so
a long-lived deployment should generate one once, store it, and pin it via
EMPIRE_AID; otherwise every process start looks like a brand new device.
Persisting it here is deliberately not done: a library must not write files
or touch os.environ as an import side effect.
generate_session_id
¶
generate_session_id() -> str
A session id as the client makes one per page load; the version check sends it.
Client: BasicEnvironmentGlobals.sessionId (dll line 34455)
build_number
¶
The build number of a client version: "1.169.11" is "1169011".
The minor and patch parts are padded to three digits; a patch suffix after
- is dropped.
Client: CastleVersionInformation.buildNumberGame (bundle line 10446)
fetch_client_version
¶
The live game client's version, read from the client's own files.
Downloads index.html every call, so a game update is picked up at once,
and the small CacheBreaker bundle it names only when the bundle is new
to this process.
Client: index.html preloads CacheBreaker.bundle.<hash>.js, which
CastleLoader loads first (loadCacheBreaker); in it
CastleVersionInformation reads appVersion from the
TranspilationEmpire package (module 858), the version
buildNumberGame is made from
Raises:
| Type | Description |
|---|---|
NetworkError
|
|
LoginError
|
The files name no bundle or no version |
network_config_url
¶
network_config_url(game_id: int, network_id: int, *, test_servers: bool = False, cdn_sub_domain: str = CDN_SUB_DOMAIN, domain: str = 'goodgamestudios.com') -> str
The URL of a network's network.xml.
https://{cdn_sub_domain}.{domain}/games-netconf/{game_id}/{network_id}.xml,
or with test_servers (the client's forceToShowTestServers)
https://files.{domain}/games-netconf-test/{game_id}/{network_id}.xml.
The ids come from the page the game is embedded in, not from the client;
the host defaults are the client's.
Client: LiveEnvironment.initPatterns (dll line 3894-3900); cdnSubDomain
and domain from BasicEnvironmentGlobals (dll line 34178, 34223)
parse_network_instances
¶
parse_network_instances(xml_text: str, include_test: bool = False) -> list[NetworkInstance]
The servers a network.xml lists under instances, and under test-instances too with include_test.
Country codes are kept as the file gives them (the client also drops the ones it has no country for), and a countries list that is not JSON reads as none, where the client would fail the whole file.
Raises:
| Type | Description |
|---|---|
ValueError
|
The text is not XML, or declares a DTD |
Client: NetworkXMLParser.parseInstances and parseTestInstances (dll line 20254),
NetworkInstancesController.loadNetworkInstances (dll line 20233)
fetch_network_instances
¶
fetch_network_instances(game_id: int, network_id: int, include_test: bool = False, timeout: float = 10.0, **url_parts: Any) -> list[NetworkInstance]
Download a network's network.xml and read its servers.
url_parts go to network_config_url (test_servers,
cdn_sub_domain, domain).
Raises:
| Type | Description |
|---|---|
NetworkError
|
The file could not be downloaded |
ValueError
|
The file is not XML |
instance_for_zone
¶
instance_for_zone(zone: str, *, game_id: int = EMPIRE_GAME_ID, network_id: int = EMPIRE_NETWORK_ID) -> NetworkInstance
The server a world (zone) lives on, from the network's network.xml, read once per process.
Raises:
| Type | Description |
|---|---|
NetworkError
|
The server list could not be downloaded |
ValueError
|
The network lists no world of that zone |
accounts¶
Accounts, loaded from a file and from the environment. Handles loading credentials from files, environment variables, and provides a robust interface for selecting accounts based on aliases or tags.
Classes:
| Name | Description |
|---|---|
Account |
Represents a single game account configuration. |
AccountRegistry |
Central registry for managing game accounts. |
Attributes:
| Name | Type | Description |
|---|---|---|
account_registry |
The shared registry, loaded from the environment and |
DEFAULT_ACCOUNTS_FILE
module-attribute
¶
The file AccountRegistry.load reads when given no path; it may be absent.
account_registry
module-attribute
¶
account_registry = AccountRegistry()
The shared registry, loaded from the environment and accounts.json on first use.
Account
pydantic-model
¶
Bases: EmpireModel
Represents a single game account configuration. Wraps credentials and metadata.
Fields:
-
username(str) -
password(str) -
world(str) -
alias(str | None) -
tags(list[str]) -
active(bool) -
client_version(str | None)
alias
pydantic-field
¶
alias: str | None = None
Short name for this account (e.g., 'main', 'farmer1')
client_version
pydantic-field
¶
client_version: str | None = None
Game client version to log in with; None reads the live client's at each login
to_empire_config
¶
to_empire_config() -> EmpireConfig
A config that logs in to this account's world: its server's URL and its zone.
Each world lives on its own server, so the zone alone would log in to the
default server, which then never answers for a zone it does not serve. A
world other than the default one is looked up in network.xml (once per
process).
Raises:
| Type | Description |
|---|---|
NetworkError
|
The server list could not be downloaded |
ValueError
|
No world of that name is listed |
AccountRegistry
¶
Central registry for managing game accounts. Sources accounts from:
- accounts.json (local development)
- Environment variables (production/CI)
Security
accounts.json holds passwords in plain text. Keep it out of version
control and readable only by its owner (chmod 600 accounts.json).
This is not enforced; the file is read whatever its permissions are.
Thread Safety
The implicit lazy load performed by the query methods is guarded by a
lock, so concurrent first-time readers cannot observe a partially
loaded registry. Explicit calls to load are not synchronized.
Methods:
| Name | Description |
|---|---|
load |
Load accounts from all sources. |
get_all |
Get all active accounts. |
get_by_alias |
Find an account by its alias. |
get_by_username |
Find an account by username. |
get_by_tag |
Get all accounts with a specific tag. |
get_default |
Get the first available account (default). |
load
¶
Load accounts from all sources. Prioritizes environment variables, then file: an account whose username appears in both sources keeps the env definition.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
file_path
|
str | PathLike[str] | None
|
Path to the accounts JSON file, relative to the current working
directory unless absolute; it must exist. When None, |
None
|
load_env_file
|
bool
|
When True, first merge a |
False
|
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
|
ValueError
|
The file is not a JSON list of accounts |
pool¶
A pool that leases one logged-in client per account.
Provides lease/release semantics for account management, automatic cooldown handling, and tag-based filtering for different use cases (e.g., tracking, scanning, alerts).
Classes:
| Name | Description |
|---|---|
PoolExhaustedError |
Raised when the pool has no candidate account to hand out. |
AccountPool |
Manages a pool of accounts for concurrent GGE operations. |
PoolExhaustedError
¶
Bases: EmpireError
Raised when the pool has no candidate account to hand out.
Distinct from LoginError: nothing was tried
and nothing failed, there was simply nothing free (none configured, all
busy or logging back in, all inactive, or none matching the requested
username/tag). Callers should back off and retry rather than treat it as
a credential problem.
AccountPool
¶
AccountPool(registry: AccountRegistry, keep_alive: bool = False)
Manages a pool of accounts for concurrent GGE operations.
Allows callers to 'lease' accounts so they aren't used by multiple operations simultaneously. Implements automatic cycling and cooldown handling.
Usage
registry = AccountRegistry()
registry.load(file_path="farmers.json")
pool = AccountPool(registry)
# Scoped lease: released even if the body raises (preferred)
with pool.leased(tag="tracking") as client:
...
# Manual lease/release, for callers that cannot use a with-block
client = pool.lease()
try:
...
finally:
pool.release(client)
Keeping clients logged in
With keep_alive=True, release hands the client back to the
pool still logged in, and the next lease of that account reuses it
without a login. A kept client whose session dropped meanwhile is
closed and replaced by a fresh login, so a cooldown on that login sends
the lease on to the next candidate as usual. One still logging back in
by itself (EmpireClient.is_restoring_session, with
keep_session) is left to it: its account is not available until
the restore ends, so a lease moves on to the next candidate, and a
release keeps it. To force a fresh login instead, close it yourself:
pool.get_client(username).close(); the next lease of the account
then logs in a new client. While
kept, the connection pings every 60 seconds as the game client does,
which keeps an idle session open. release_all closes kept clients too.
Client: BasicSmartfoxClient.onJoinRoom (dll line 7165) and
activatePing (dll line 7167).
Two moments still treat a client that is logging back in as dropped,
closing it and logging in a fresh one: between its drop and the start
of its re-login (while its on_disconnect callbacks run, it is
neither logged in nor restoring), and when it drops after a lease saw
it logged in but before the lease takes it over.
The next leaseholder gets the same client object. leased
releases only its own lease, but release cannot tell one
holder's handle from the next one's: release a manual handle once, and
never after its lease ended.
Release ends the streams a leaseholder opened with
EmpireClient.listen. Otherwise the client is handed over as it
is: callbacks a leaseholder registered
(on_disconnect, on_incoming_attack and the like) stay
registered and fire during the next lease. Remove them before
releasing, or release with logout=True to close the client.
Thread Safety
Every method may be called from any thread. An account is marked busy before its login starts, so two concurrent leases never get the same account; the login itself runs outside the pool's lock, so leases of different accounts log in in parallel.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
registry
|
AccountRegistry
|
Where this pool's accounts come from. |
required |
keep_alive
|
bool
|
Keep released clients logged in for the next lease instead of closing them. |
False
|
Methods:
| Name | Description |
|---|---|
get_available |
Get list of available accounts: not leased, and not waiting for a kept client to log back in. |
lease |
Lease an account from the pool. |
leased |
Lease an account for the duration of a |
release |
Release an account back to the pool. |
release_all |
Release all leased accounts and, with |
get_client |
Get a leased or kept client by username. |
Attributes:
| Name | Type | Description |
|---|---|---|
registry |
|
|
keep_alive |
|
|
all_accounts |
list[Account]
|
Get all configured accounts. |
busy_count |
int
|
Number of currently leased accounts. |
available_count |
int
|
Number of available accounts. |
get_available
¶
lease
¶
lease(username: str | None = None, tag: str | None = None, login: bool = True) -> EmpireClient | None
Lease an account from the pool.
Marks the account as busy and optionally logs in. If a specific account
is on cooldown, automatically tries the next available account. A
client kept by a keep_alive pool is reused while it is still
logged in, also when login is False; while it is logging back in
by itself its account is skipped.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
username
|
str | None
|
Specific username to lease (optional). |
None
|
tag
|
str | None
|
Tag to filter accounts (optional). |
None
|
login
|
bool
|
Whether to login the client (default True). |
True
|
Returns:
| Type | Description |
|---|---|
EmpireClient | None
|
Connected EmpireClient, or None if there were no candidate accounts |
EmpireClient | None
|
to try (none configured, all busy or logging back in, or none |
EmpireClient | None
|
matching username/tag). |
Raises:
| Type | Description |
|---|---|
LoginError
|
Every candidate was tried and every one failed. The last
failure is attached as |
leased
¶
leased(username: str | None = None, tag: str | None = None, login: bool = True) -> Iterator[EmpireClient]
Lease an account for the duration of a with block.
Preferred over lease/release: the release happens in a
finally, so a caller exception cannot leak the busy slot and the live
client. The pool has no lease timeout or reaper, so a leaked slot means
the account is unavailable until the process restarts.
Usage::
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
username
|
str | None
|
Specific username to lease (optional). |
None
|
tag
|
str | None
|
Tag to filter accounts (optional). |
None
|
login
|
bool
|
Whether to login the client (default True). |
True
|
Yields:
| Type | Description |
|---|---|
EmpireClient
|
The leased, connected client. |
Raises:
| Type | Description |
|---|---|
PoolExhaustedError
|
No candidate account was available to try. |
LoginError
|
Every candidate was tried and every one failed. |
release
¶
release(client: EmpireClient, logout: bool | None = None) -> None
Release an account back to the pool.
Only a client the account is leased with now is released. Any other
leaves the pool as it is, with a warning: a client the pool did not
hand out, or one whose lease already ended. Such a client is still
closed as logout says, unless it is the client a keep_alive
pool holds for its account, which a later leaseholder may be using.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
client
|
EmpireClient
|
The client to release. |
required |
logout
|
bool | None
|
Whether to close the client. The default closes it, unless
the pool was made with |
None
|
release_all
¶
release_all(logout: bool = True) -> None
Release all leased accounts and, with logout, close the clients a keep_alive pool kept.
get_client
¶
get_client(username: str) -> EmpireClient | None
Get a leased or kept client by username.
Closing a kept client that is logging back in ends its restore; the next lease of its account then logs in a new client.
services.base¶
The base every service builds on: typed requests and action results.
Classes:
| Name | Description |
|---|---|
BaseService |
Base class for all services. |
BaseService
¶
Base class for all services.
Services provide high-level APIs for game domains and use
protocol models for type-safe request/response handling. The client
builds one of each in EmpireClient.__init__.
Every service method reports failure by one rule:
- transport errors and unexpected server errors raise (
EmpireTimeoutError,ConnectionClosedError,CommandError); Noneonly for a not-found (or nothing-to-do) outcome mapped from a namedGGEError, matched onCommandError.errorand documented on the method;- an empty collection only when the collection really is empty;
- invalid arguments, and a call the client would not make, raise
ValueErroror a typed error fromempire_core.exceptions; - a malformed reply raises
PacketError; - an action the server refuses returns False (see
execute).
Four kinds of method follow rules of their own, each documented on the method:
- state read without a request returns
Nonewhile the login data or push it comes from has not arrived yet (client.castle.get_horsesbefore agpc); - a call made of several requests returns a result object that names each outcome,
such as
SpyOutcome.COMMAND_FAILEDon aSpyResultor thefailedandtimed_outplayers ofget_player_details_bulk; - a best-effort fill goes on without a read the server refuses and names it,
as
fill_attackdoes inFilledAttack.unread; - a request the server never answers, waiting for a push instead, returns
Nonewhen no push came in time (open_activity_chest).
Methods:
| Name | Description |
|---|---|
send |
Send a request to the server. |
request |
Send a request and return its typed response. |
execute |
Send an action request and report whether the server accepted it. |
on_response |
Register a handler for a specific response type. |
Attributes:
| Name | Type | Description |
|---|---|---|
client |
|
|
zone |
str
|
Get the game zone from client config. |
send
¶
send(request: BaseRequest, wait: bool = False, timeout: float = 5.0) -> BaseResponse | None
Send a request to the server.
With wait=True, raises the same typed exceptions as
EmpireClient.send (CommandError, EmpireTimeoutError, ...).
request
¶
request(request: BaseRequest, response_type: type[R], timeout: float = 5.0) -> R
Send a request and return its typed response.
Raises:
| Type | Description |
|---|---|
CommandError
|
The server answered with a non-zero error code |
EmpireTimeoutError / ConnectionClosedError / NetworkError
|
transport failures |
PacketError
|
The response could not be parsed as |
execute
¶
execute(request: BaseRequest, timeout: float = 5.0, *, raise_on: Collection[GGEError] = ()) -> bool
Send an action request and report whether the server accepted it.
Returns False when the server rejects the action with an error code (logged at warning level). Transport failures (timeout, disconnect) still raise, so infrastructure problems are never mistaken for a game-rule rejection.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
raise_on
|
Collection[GGEError]
|
Errors that raise their |
()
|
on_response
¶
on_response(command: str, handler: Callable[[BaseResponse], None]) -> None
Register a handler for a specific response type.
Handlers are registered with the client for efficient routing. Only commands with registered handlers will be parsed.
Handlers 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 |
|---|---|---|---|
command
|
str
|
The command code to handle (e.g., "acm") |
required |
handler
|
Callable[[BaseResponse], None]
|
Callback function that receives the parsed response |
required |