Skip to content

Alliance

client.alliance covers your own alliance and looks up others.

Chat

client.alliance.send_chat("Hello alliance!")   # special characters are encoded for you

for entry in client.alliance.get_chat_log():
    print(f"{entry.player_name}: {entry.decoded_text}")

State keeps the chat history from the login data on, with every message the server sends after it, oldest first, so reading it costs no request:

for entry in client.state.get_alliance_chat():
    print(entry.sent_at, entry.player_name, entry.decoded_text)

Subscribe to new messages with a typed callback, and detach it again with client.alliance.on_chat_message.remove(on_message):

def on_message(msg):                  # an AllianceChatMessageResponse
    print(f"[{msg.player_name}] {msg.decoded_text}")

client.alliance.on_chat_message(on_message)

Chat callbacks run on the receive thread

Unlike the state callbacks, chat callbacks are called on the thread that reads the socket. Keep them short: a call that waits for a reply raises ReceiveThreadError there. Hand real work to another thread.

Help requests

client.alliance.help_requests is the alliance help list. The login data fills it and the server's pushes keep it current, so reading it costs no request.

client.alliance.help_all()            # help every request on the list

To pick requests yourself, skip the ones the client skips: those you already helped, your own, and finished ones. A request is finished once its progress reaches its help type's maxHelpersCount in the items data's alliancehelprequests table (3 for most types, 5 for healing and 20 for loop recruiting in items version 786.03):

max_helpers = {1: 3, 2: 5, 3: 3, 4: 3, 5: 20, 6: 3}
player = client.state.get_local_player()
my_id = player.id if player else None

for request in client.alliance.help_requests:
    finished = request.progress >= max_helpers.get(request.help_type, 0)
    if not (request.already_confirmed or finished or request.player_id == my_id):
        client.alliance.help_member(request)

on_help_update(callback) calls you after each change to the list. Ask for help yourself with request_build_help, request_repair_help, request_recruit_help and request_heal_help.

Members and other alliances

members = client.alliance.get_local_members()
online = client.alliance.get_local_online_members()

for result in client.alliance.search_alliances("PACT"):
    info = client.alliance.get_alliance_info(result.alliance_id)

get_members(alliance_id) and get_online_members(alliance_id) do the same for any alliance, and client.alliance.local_alliance_id is your own alliance's id, or None outside one. Outside an alliance the two get_local_* calls raise NotInAllianceError without sending anything, so "no alliance" is never mistaken for an alliance without members. search_alliances returns an empty list when nothing matches.

A member's protection times count from when the reply was read (received_at, in time.monotonic() seconds): revenge_protection_end and beginner_protection_end stay fixed, and remaining_revenge_protection_seconds() counts down; has_bird and has_beginner_protection turn False once the time runs out. To store or show an end, revenge_protection_end_utc() and beginner_protection_end_utc() give it as a UTC datetime (or None), counted from the wall-clock time stamped at the same read (received_at_wall), so they do not move between calls. A player from client.player.get_player_info has the same on its owner.

client.state.get_own_alliance() is your alliance's details as the login data and the replies since left them (a chat message marks its sender online), or None outside one, without a request.

Applications, ranks and the treasury

from empire_core.alliance import AllianceDonation, AllianceRank

for application in client.alliance.get_applications().applications:
    client.alliance.answer_application(application.player_id, accept=True)

client.alliance.set_rank(player_id, AllianceRank.SERGEANT)
client.alliance.donate(castle_id, AllianceDonation(wood=1000))
client.alliance.donate(castle_id, AllianceDonation(rubies=50), spend_rubies=True)

Also on the service: invite, kick_member, leave, change_diplomacy, refuse_diplomacy, set_auto_war and send_newsletter.

All of these, and get_chronicle, are about your own alliance, which the client only offers inside one. Outside an alliance they raise NotInAllianceError without sending anything, and so does a server answer of ALLI_NOT_FOUND (your alliance id was stale). set_rank returns None for NO_CHANGE, the rank the member already has; kick_member, set_rank and refuse_diplomacy otherwise return the alliance the reply carries. get_subscriber_count is sent outside an alliance too, as the client sends it.

The chronicle

get_chronicle returns the entries of your alliance's chronicle, newest first. What an entry's action_values hold depends on its action; details reads them as the client does, typed: a ChronicleDiplomacy (the other alliance's name and a DiplomacyStatus), a ChronicleBuff (an AllianceBuffType, and the smithy's level for its upgrade), a ChronicleAmount, ChronicleLevel, ChroniclePlace, ChronicleTournamentPrize, ChronicleText, ChronicleMember or ChronicleDaimyoContract, or None where the entry carries nothing (the entries about a player name them in player_name). describe() writes the line the overview shows.

from empire_core.alliance import ChronicleAmount, ChronicleDiplomacy

for entry in client.alliance.get_chronicle():
    print(entry.describe())             # "1,500 coins have been donated to the alliance funds"
    match entry.details:
        case ChronicleAmount(amount=amount):
            ...
        case ChronicleDiplomacy(alliance_name=name, status=status):
            ...

describe() fetches the language file on first use, as empire_core.texts does, and takes lang. A completed daimyo contract reads its rank and level from the game data, loaded on first use or passed as entry.describe(data). On a special server with a skin (such as "Maya" on an alliance battle ground), pass skin for the texts the client shows there. An entry no player made (player_id -1) names an alliance instead: named_alliance_id and named_alliance_name.

Map bookmarks

get_bookmarks lists your own bookmarks and your alliance's. Your own are a friend or an enemy; the alliance ones (free attack, defend, attack order) need the right to manage bookmarks. A name is 1 to 30 characters (BOOKMARK_NAME_MAX_LENGTH) and goes out as typed.

from empire_core.alliance import BookmarkType

added = client.alliance.add_bookmark(640, 655, "Farm", BookmarkType.PLAYER_FRIEND)
client.alliance.change_bookmark(640, 655, "Barn", BookmarkType.PLAYER_FRIEND)
client.alliance.delete_bookmark(added)

add_bookmark and change_bookmark return the bookmark the reply carries and raise CommandError when the server refuses, such as BOOKMARK_ALREADY_ADDED, BOOKMARK_MAX_ENTRYS (50 own) or NO_SELF_TARGET; the alliance list of 20 has ALLIANCE_BOOKMARK_MAX_ENTRYS. change_bookmark changes only your own bookmarks, as the client does. delete_bookmark deletes an own bookmark by its position and an alliance one by its id, and returns False when the server refuses. An alliance attack order takes attack_in_seconds (an hour to just under a day) and attacker_ids. Adding or deleting an alliance bookmark outside an alliance raises NotInAllianceError without sending anything.

Runnable example

examples/alliance_chat.py
"""Read alliance chat history and follow new messages as they arrive.

Demonstrates the two halves of the chat API: a request/response history fetch,
and a callback subscription served by the library's background receive thread.

    export GGE_USERNAME=your_user
    export GGE_PASSWORD=your_pass
    python examples/alliance_chat.py [seconds_to_listen]
"""

import logging
import os
import sys
import time

from empire_core import EmpireClient, EmpireError
from empire_core.alliance import AllianceChatMessageResponse

logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
logging.getLogger("websocket").setLevel(logging.WARNING)

LISTEN_SECONDS = 60.0


def main() -> int:
    username = os.getenv("GGE_USERNAME")
    password = os.getenv("GGE_PASSWORD")
    if not username or not password:
        print("Set GGE_USERNAME and GGE_PASSWORD first.", file=sys.stderr)
        return 2

    listen_for = float(sys.argv[1]) if len(sys.argv) > 1 else LISTEN_SECONDS

    client = EmpireClient(username=username, password=password)
    try:
        client.login()

        print("\n--- recent history ---")
        for entry in client.alliance.get_chat_log():
            print(f"  {entry.player_name}: {entry.decoded_text}")

        def on_message(message: AllianceChatMessageResponse) -> None:
            # Called on the receive thread: keep it quick and do not block.
            print(f"  [live] {message.player_name}: {message.decoded_text}")

        client.alliance.on_chat_message(on_message)

        print(f"\n--- listening for {listen_for:.0f}s (Ctrl+C to stop) ---")
        deadline = time.monotonic() + listen_for
        while time.monotonic() < deadline:
            time.sleep(0.5)

        # To send a message:
        # client.alliance.send_chat("Hello from EmpireCore")

    except KeyboardInterrupt:
        print("\nStopped.")
    except EmpireError as e:
        print(f"{type(e).__name__}: {e}", file=sys.stderr)
        return 1
    finally:
        client.close()

    return 0


if __name__ == "__main__":
    sys.exit(main())

API: AllianceService