empire-core 0.7.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- empire_core/__init__.py +36 -0
- empire_core/_archive/actions.py +511 -0
- empire_core/_archive/automation/__init__.py +24 -0
- empire_core/_archive/automation/alliance_tools.py +266 -0
- empire_core/_archive/automation/battle_reports.py +196 -0
- empire_core/_archive/automation/building_queue.py +242 -0
- empire_core/_archive/automation/defense_manager.py +124 -0
- empire_core/_archive/automation/map_scanner.py +370 -0
- empire_core/_archive/automation/multi_account.py +296 -0
- empire_core/_archive/automation/quest_automation.py +94 -0
- empire_core/_archive/automation/resource_manager.py +380 -0
- empire_core/_archive/automation/target_finder.py +153 -0
- empire_core/_archive/automation/tasks.py +224 -0
- empire_core/_archive/automation/unit_production.py +719 -0
- empire_core/_archive/cli.py +68 -0
- empire_core/_archive/client_async.py +469 -0
- empire_core/_archive/commands.py +201 -0
- empire_core/_archive/connection_async.py +228 -0
- empire_core/_archive/defense.py +156 -0
- empire_core/_archive/events/__init__.py +35 -0
- empire_core/_archive/events/base.py +153 -0
- empire_core/_archive/events/manager.py +85 -0
- empire_core/accounts.py +190 -0
- empire_core/client/__init__.py +0 -0
- empire_core/client/client.py +459 -0
- empire_core/config.py +87 -0
- empire_core/exceptions.py +42 -0
- empire_core/network/__init__.py +0 -0
- empire_core/network/connection.py +378 -0
- empire_core/protocol/__init__.py +0 -0
- empire_core/protocol/models/__init__.py +339 -0
- empire_core/protocol/models/alliance.py +186 -0
- empire_core/protocol/models/army.py +444 -0
- empire_core/protocol/models/attack.py +229 -0
- empire_core/protocol/models/auth.py +216 -0
- empire_core/protocol/models/base.py +403 -0
- empire_core/protocol/models/building.py +455 -0
- empire_core/protocol/models/castle.py +317 -0
- empire_core/protocol/models/chat.py +150 -0
- empire_core/protocol/models/defense.py +300 -0
- empire_core/protocol/models/map.py +269 -0
- empire_core/protocol/packet.py +104 -0
- empire_core/services/__init__.py +31 -0
- empire_core/services/alliance.py +222 -0
- empire_core/services/base.py +107 -0
- empire_core/services/castle.py +221 -0
- empire_core/state/__init__.py +0 -0
- empire_core/state/manager.py +398 -0
- empire_core/state/models.py +215 -0
- empire_core/state/quest_models.py +60 -0
- empire_core/state/report_models.py +115 -0
- empire_core/state/unit_models.py +75 -0
- empire_core/state/world_models.py +269 -0
- empire_core/storage/__init__.py +1 -0
- empire_core/storage/database.py +237 -0
- empire_core/utils/__init__.py +0 -0
- empire_core/utils/battle_sim.py +172 -0
- empire_core/utils/calculations.py +170 -0
- empire_core/utils/crypto.py +8 -0
- empire_core/utils/decorators.py +69 -0
- empire_core/utils/enums.py +111 -0
- empire_core/utils/helpers.py +252 -0
- empire_core/utils/response_awaiter.py +153 -0
- empire_core/utils/troops.py +93 -0
- empire_core-0.7.3.dist-info/METADATA +197 -0
- empire_core-0.7.3.dist-info/RECORD +67 -0
- empire_core-0.7.3.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
from typing import Any, Dict, List
|
|
2
|
+
|
|
3
|
+
from empire_core.state.world_models import MapObject
|
|
4
|
+
from pydantic import BaseModel, ConfigDict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Event(BaseModel):
|
|
8
|
+
"""Base class for all events."""
|
|
9
|
+
|
|
10
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PacketEvent(Event):
|
|
14
|
+
"""
|
|
15
|
+
Raw event triggered when a packet is received.
|
|
16
|
+
Useful for debugging or catching unhandled commands.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
command_id: str
|
|
20
|
+
payload: Any
|
|
21
|
+
is_xml: bool
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ============================================================
|
|
25
|
+
# Map Events
|
|
26
|
+
# ============================================================
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MapChunkParsedEvent(Event):
|
|
30
|
+
"""Triggered when a map chunk is parsed."""
|
|
31
|
+
|
|
32
|
+
kingdom_id: int
|
|
33
|
+
map_objects: List[MapObject]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ============================================================
|
|
37
|
+
# Movement Events
|
|
38
|
+
# ============================================================
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class MovementEvent(Event):
|
|
42
|
+
"""Base class for all movement-related events."""
|
|
43
|
+
|
|
44
|
+
movement_id: int
|
|
45
|
+
movement_type: int
|
|
46
|
+
movement_type_name: str
|
|
47
|
+
source_area_id: int
|
|
48
|
+
target_area_id: int
|
|
49
|
+
is_incoming: bool
|
|
50
|
+
is_outgoing: bool
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class MovementStartedEvent(MovementEvent):
|
|
54
|
+
"""
|
|
55
|
+
Triggered when a new movement is detected.
|
|
56
|
+
This includes both our outgoing movements and incoming attacks from others.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
total_time: int # Total travel time in seconds
|
|
60
|
+
unit_count: int # Total units in movement
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class MovementUpdatedEvent(MovementEvent):
|
|
64
|
+
"""
|
|
65
|
+
Triggered when a movement's progress is updated.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
progress_time: int # Time elapsed
|
|
69
|
+
total_time: int # Total travel time
|
|
70
|
+
time_remaining: int # Time left
|
|
71
|
+
progress_percent: float
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class MovementArrivedEvent(MovementEvent):
|
|
75
|
+
"""
|
|
76
|
+
Triggered when a movement arrives at its destination.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
was_incoming: bool # True if it was incoming to us
|
|
80
|
+
was_outgoing: bool # True if it was our movement
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class MovementCancelledEvent(MovementEvent):
|
|
84
|
+
"""
|
|
85
|
+
Triggered when a movement is cancelled/recalled.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class IncomingAttackEvent(Event):
|
|
92
|
+
"""
|
|
93
|
+
Triggered specifically when an incoming attack is detected.
|
|
94
|
+
This is a high-priority alert event.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
movement_id: int
|
|
98
|
+
attacker_id: int
|
|
99
|
+
attacker_name: str
|
|
100
|
+
target_area_id: int
|
|
101
|
+
target_name: str
|
|
102
|
+
time_remaining: int
|
|
103
|
+
unit_count: int
|
|
104
|
+
source_x: int
|
|
105
|
+
source_y: int
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ReturnArrivalEvent(Event):
|
|
109
|
+
"""
|
|
110
|
+
Triggered when returning troops arrive back.
|
|
111
|
+
Useful for tracking loot from attacks.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
movement_id: int
|
|
115
|
+
castle_id: int
|
|
116
|
+
units: Dict[int, int] # UnitID -> Count
|
|
117
|
+
resources_wood: int
|
|
118
|
+
resources_stone: int
|
|
119
|
+
resources_food: int
|
|
120
|
+
total_loot: int
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ============================================================
|
|
124
|
+
# Attack/Battle Events
|
|
125
|
+
# ============================================================
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class AttackSentEvent(Event):
|
|
129
|
+
"""Triggered when we send an attack."""
|
|
130
|
+
|
|
131
|
+
movement_id: int
|
|
132
|
+
origin_castle_id: int
|
|
133
|
+
target_area_id: int
|
|
134
|
+
units: Dict[int, int]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class ScoutSentEvent(Event):
|
|
138
|
+
"""Triggered when we send scouts."""
|
|
139
|
+
|
|
140
|
+
movement_id: int
|
|
141
|
+
origin_castle_id: int
|
|
142
|
+
target_area_id: int
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class TransportSentEvent(Event):
|
|
146
|
+
"""Triggered when we send a transport."""
|
|
147
|
+
|
|
148
|
+
movement_id: int
|
|
149
|
+
origin_castle_id: int
|
|
150
|
+
target_castle_id: int
|
|
151
|
+
resources_wood: int
|
|
152
|
+
resources_stone: int
|
|
153
|
+
resources_food: int
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import inspect
|
|
3
|
+
import logging
|
|
4
|
+
from typing import (
|
|
5
|
+
Any,
|
|
6
|
+
Callable,
|
|
7
|
+
Coroutine,
|
|
8
|
+
Dict,
|
|
9
|
+
List,
|
|
10
|
+
Type,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from empire_core.events.base import Event
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
EventHandler = Callable[[Any], Coroutine[Any, Any, None]]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class EventManager:
|
|
21
|
+
def __init__(self):
|
|
22
|
+
# Map EventType -> List[Handler]
|
|
23
|
+
self._listeners: Dict[Type[Event], List[EventHandler]] = {}
|
|
24
|
+
|
|
25
|
+
def subscribe(self, event_type: Type[Event], handler: EventHandler):
|
|
26
|
+
if event_type not in self._listeners:
|
|
27
|
+
self._listeners[event_type] = []
|
|
28
|
+
self._listeners[event_type].append(handler)
|
|
29
|
+
logger.debug(f"Registered handler {handler.__name__} for {event_type.__name__}")
|
|
30
|
+
|
|
31
|
+
def listen(self, func: EventHandler):
|
|
32
|
+
"""
|
|
33
|
+
Decorator to register an event handler.
|
|
34
|
+
Infers the event type from the first argument's type hint.
|
|
35
|
+
"""
|
|
36
|
+
sig = inspect.signature(func)
|
|
37
|
+
params = list(sig.parameters.values())
|
|
38
|
+
|
|
39
|
+
if not params:
|
|
40
|
+
raise ValueError(f"Handler {func.__name__} must have at least one argument (the event).")
|
|
41
|
+
|
|
42
|
+
# Get type hint of first arg
|
|
43
|
+
event_arg = params[0]
|
|
44
|
+
if event_arg.annotation is inspect.Parameter.empty:
|
|
45
|
+
raise ValueError(
|
|
46
|
+
f"Handler {func.__name__} argument '{event_arg.name}' must have a type hint (e.g. 'event: AttackEvent')."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
event_type = event_arg.annotation
|
|
50
|
+
|
|
51
|
+
# Verify it's an Event subclass (or valid type)
|
|
52
|
+
# We accept any type for flexibility, but ideally it's an Event
|
|
53
|
+
self.subscribe(event_type, func)
|
|
54
|
+
return func
|
|
55
|
+
|
|
56
|
+
async def emit(self, event: Event):
|
|
57
|
+
event_type = type(event)
|
|
58
|
+
# Dispatch to specific listeners
|
|
59
|
+
handlers = self._listeners.get(event_type, [])
|
|
60
|
+
|
|
61
|
+
# Also dispatch to listeners of parent classes?
|
|
62
|
+
# (e.g. PacketEvent listeners might want all PacketEvents)
|
|
63
|
+
# For simplicity, currently exact match.
|
|
64
|
+
# But to support 'Event' (all events), we can check mro.
|
|
65
|
+
|
|
66
|
+
# Check MRO for polymorphism
|
|
67
|
+
for cls in event_type.mro():
|
|
68
|
+
if cls in self._listeners:
|
|
69
|
+
if cls is not event_type:
|
|
70
|
+
handlers.extend(self._listeners[cls])
|
|
71
|
+
|
|
72
|
+
# Dedup handlers?
|
|
73
|
+
handlers = list(set(handlers))
|
|
74
|
+
|
|
75
|
+
if not handlers:
|
|
76
|
+
# logger.debug(f"No listeners for {event_type.__name__}")
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
# Run all listeners in parallel
|
|
80
|
+
tasks: List[Coroutine] = []
|
|
81
|
+
for func in handlers:
|
|
82
|
+
tasks.append(func(event))
|
|
83
|
+
|
|
84
|
+
if tasks:
|
|
85
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
empire_core/accounts.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Account management and configuration system.
|
|
3
|
+
Handles loading credentials from files, environment variables, and provides
|
|
4
|
+
a robust interface for selecting accounts based on aliases or tags.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
from typing import TYPE_CHECKING, List, Optional
|
|
11
|
+
|
|
12
|
+
from dotenv import load_dotenv
|
|
13
|
+
from pydantic import BaseModel, Field, ValidationError
|
|
14
|
+
|
|
15
|
+
from empire_core.config import EmpireConfig
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from empire_core.client.client import EmpireClient
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# Load .env file if present
|
|
23
|
+
load_dotenv()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Account(BaseModel):
|
|
27
|
+
"""
|
|
28
|
+
Represents a single game account configuration.
|
|
29
|
+
Wraps credentials and metadata.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
username: str
|
|
33
|
+
password: str = Field(..., description="Plain text password")
|
|
34
|
+
world: str = Field(default="EmpireEx_21", description="Game world/zone (e.g., EmpireEx_21)")
|
|
35
|
+
alias: Optional[str] = Field(None, description="Short name for this account (e.g., 'main', 'farmer1')")
|
|
36
|
+
tags: List[str] = Field(default_factory=list, description="Categorization tags (e.g., ['farmer', 'k1'])")
|
|
37
|
+
active: bool = Field(default=True, description="Whether this account should be used")
|
|
38
|
+
|
|
39
|
+
def to_empire_config(self) -> EmpireConfig:
|
|
40
|
+
"""Convert to EmpireConfig for client usage."""
|
|
41
|
+
return EmpireConfig(username=self.username, password=self.password, default_zone=self.world)
|
|
42
|
+
|
|
43
|
+
def has_tag(self, tag: str) -> bool:
|
|
44
|
+
"""Check if account has a specific tag (case-insensitive)."""
|
|
45
|
+
return tag.lower() in [t.lower() for t in self.tags]
|
|
46
|
+
|
|
47
|
+
def get_client(self) -> "EmpireClient":
|
|
48
|
+
"""Create and return an EmpireClient for this account."""
|
|
49
|
+
from empire_core.client.client import EmpireClient
|
|
50
|
+
|
|
51
|
+
return EmpireClient(
|
|
52
|
+
username=self.username,
|
|
53
|
+
password=self.password,
|
|
54
|
+
config=self.to_empire_config(),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class AccountRegistry:
|
|
59
|
+
"""
|
|
60
|
+
Central registry for managing game accounts.
|
|
61
|
+
Sources accounts from:
|
|
62
|
+
1. accounts.json (local development)
|
|
63
|
+
2. Environment variables (production/CI)
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self):
|
|
67
|
+
self._accounts: List[Account] = []
|
|
68
|
+
self._loaded = False
|
|
69
|
+
|
|
70
|
+
def load(self, file_path: str = "accounts.json"):
|
|
71
|
+
"""
|
|
72
|
+
Load accounts from all sources.
|
|
73
|
+
Prioritizes environment variables, then file.
|
|
74
|
+
"""
|
|
75
|
+
self._accounts = []
|
|
76
|
+
|
|
77
|
+
# 1. Load from JSON file
|
|
78
|
+
self._load_from_file(file_path)
|
|
79
|
+
|
|
80
|
+
# 2. Load from Environment Variables (EMPIRE_ACCOUNTS_JSON or specific vars)
|
|
81
|
+
self._load_from_env()
|
|
82
|
+
|
|
83
|
+
self._loaded = True
|
|
84
|
+
logger.info(f"AccountRegistry loaded {len(self._accounts)} active accounts.")
|
|
85
|
+
|
|
86
|
+
def _load_from_file(self, path_str: str):
|
|
87
|
+
"""Internal: Load from JSON file."""
|
|
88
|
+
# Resolve path similarly to the old loader
|
|
89
|
+
paths_to_check = [
|
|
90
|
+
path_str,
|
|
91
|
+
os.path.join("..", path_str),
|
|
92
|
+
os.path.join("..", "..", path_str),
|
|
93
|
+
os.path.join(os.getcwd(), path_str),
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
target_path = None
|
|
97
|
+
for p in paths_to_check:
|
|
98
|
+
if os.path.exists(p):
|
|
99
|
+
target_path = p
|
|
100
|
+
break
|
|
101
|
+
|
|
102
|
+
if not target_path:
|
|
103
|
+
logger.debug(f"Account file '{path_str}' not found. Skipping file load.")
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
with open(target_path, "r") as f:
|
|
108
|
+
data = json.load(f)
|
|
109
|
+
|
|
110
|
+
if not isinstance(data, list):
|
|
111
|
+
logger.warning(f"Invalid format in '{target_path}'. Expected a list of accounts.")
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
for entry in data:
|
|
115
|
+
try:
|
|
116
|
+
account = Account(**entry)
|
|
117
|
+
if account.active:
|
|
118
|
+
self._accounts.append(account)
|
|
119
|
+
except ValidationError as e:
|
|
120
|
+
logger.error(f"Skipping invalid account entry in {target_path}: {e}")
|
|
121
|
+
|
|
122
|
+
except Exception as e:
|
|
123
|
+
logger.error(f"Error reading '{target_path}': {e}")
|
|
124
|
+
|
|
125
|
+
def _load_from_env(self):
|
|
126
|
+
"""
|
|
127
|
+
Internal: Load from environment variables.
|
|
128
|
+
Format: EMPIRE_ACCOUNT_<ALIAS>=<USERNAME>,<PASSWORD>,<WORLD>
|
|
129
|
+
"""
|
|
130
|
+
for key, value in os.environ.items():
|
|
131
|
+
if key.startswith("EMPIRE_ACCOUNT_"):
|
|
132
|
+
# Simple parsing: ALIAS=USER,PASS,WORLD
|
|
133
|
+
# Example: EMPIRE_ACCOUNT_MAIN=myuser,mypass,EmpireEx_21
|
|
134
|
+
alias = key.replace("EMPIRE_ACCOUNT_", "").lower()
|
|
135
|
+
parts = value.split(",")
|
|
136
|
+
|
|
137
|
+
if len(parts) >= 2:
|
|
138
|
+
username = parts[0].strip()
|
|
139
|
+
password = parts[1].strip()
|
|
140
|
+
world = parts[2].strip() if len(parts) > 2 else "EmpireEx_21"
|
|
141
|
+
|
|
142
|
+
# Create account
|
|
143
|
+
acc = Account(
|
|
144
|
+
username=username, password=password, world=world, alias=alias, tags=["env"], active=True
|
|
145
|
+
)
|
|
146
|
+
self._accounts.append(acc)
|
|
147
|
+
|
|
148
|
+
# === Query Methods ===
|
|
149
|
+
|
|
150
|
+
def get_all(self) -> List[Account]:
|
|
151
|
+
"""Get all active accounts."""
|
|
152
|
+
if not self._loaded:
|
|
153
|
+
self.load()
|
|
154
|
+
return self._accounts
|
|
155
|
+
|
|
156
|
+
def get_by_alias(self, alias: str) -> Optional[Account]:
|
|
157
|
+
"""Find an account by its alias."""
|
|
158
|
+
if not self._loaded:
|
|
159
|
+
self.load()
|
|
160
|
+
for acc in self._accounts:
|
|
161
|
+
if acc.alias and acc.alias.lower() == alias.lower():
|
|
162
|
+
return acc
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
def get_by_username(self, username: str) -> Optional[Account]:
|
|
166
|
+
"""Find an account by username."""
|
|
167
|
+
if not self._loaded:
|
|
168
|
+
self.load()
|
|
169
|
+
for acc in self._accounts:
|
|
170
|
+
if acc.username.lower() == username.lower():
|
|
171
|
+
return acc
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
def get_by_tag(self, tag: str) -> List[Account]:
|
|
175
|
+
"""Get all accounts with a specific tag."""
|
|
176
|
+
if not self._loaded:
|
|
177
|
+
self.load()
|
|
178
|
+
return [acc for acc in self._accounts if acc.has_tag(tag)]
|
|
179
|
+
|
|
180
|
+
def get_default(self) -> Optional[Account]:
|
|
181
|
+
"""Get the first available account (default)."""
|
|
182
|
+
if not self._loaded:
|
|
183
|
+
self.load()
|
|
184
|
+
if self._accounts:
|
|
185
|
+
return self._accounts[0]
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# Global Singleton
|
|
190
|
+
accounts = AccountRegistry()
|
|
File without changes
|