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,296 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Multi-account management and account pooling.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Dict, List, Optional, Set
|
|
9
|
+
|
|
10
|
+
from empire_core.accounts import Account, accounts
|
|
11
|
+
from empire_core.client.client import EmpireClient
|
|
12
|
+
from empire_core.config import EmpireConfig
|
|
13
|
+
from empire_core.exceptions import LoginCooldownError
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class AccountConfig:
|
|
20
|
+
"""Configuration for a single account."""
|
|
21
|
+
|
|
22
|
+
username: str
|
|
23
|
+
password: str
|
|
24
|
+
enabled: bool = True
|
|
25
|
+
farm_interval: int = 300
|
|
26
|
+
collect_interval: int = 600
|
|
27
|
+
tags: Optional[List[str]] = None # e.g., ["farmer", "fighter"]
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_account(cls, acc: Account) -> "AccountConfig":
|
|
31
|
+
return cls(username=acc.username, password=acc.password, enabled=acc.active, tags=acc.tags)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AccountPool:
|
|
35
|
+
"""
|
|
36
|
+
Manages a pool of accounts loaded from the central registry.
|
|
37
|
+
Allows bots to 'lease' accounts so they aren't used by multiple processes simultaneously.
|
|
38
|
+
Implements automatic cycling and cooldown handling.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self):
|
|
42
|
+
self._busy_accounts: Set[str] = set() # Set of usernames currently in use
|
|
43
|
+
self._clients: Dict[str, EmpireClient] = {} # Active clients
|
|
44
|
+
self._last_leased_index = -1
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def all_accounts(self) -> List[AccountConfig]:
|
|
48
|
+
"""Get all accounts wrapped in AccountConfig."""
|
|
49
|
+
return [AccountConfig.from_account(acc) for acc in accounts.get_all()]
|
|
50
|
+
|
|
51
|
+
def get_available_accounts(self, tag: Optional[str] = None) -> List[AccountConfig]:
|
|
52
|
+
"""Returns a list of idle accounts, optionally filtered by tag."""
|
|
53
|
+
available = []
|
|
54
|
+
all_accs = self.all_accounts
|
|
55
|
+
|
|
56
|
+
# Sort/Cycle logic: Start from the next index after the last leased one
|
|
57
|
+
num_accs = len(all_accs)
|
|
58
|
+
if num_accs == 0:
|
|
59
|
+
return []
|
|
60
|
+
|
|
61
|
+
start_idx = (self._last_leased_index + 1) % num_accs
|
|
62
|
+
cycled_indices = [(start_idx + i) % num_accs for i in range(num_accs)]
|
|
63
|
+
|
|
64
|
+
for idx in cycled_indices:
|
|
65
|
+
acc = all_accs[idx]
|
|
66
|
+
if acc.username not in self._busy_accounts and acc.enabled:
|
|
67
|
+
if tag is None or (acc.tags and tag in acc.tags):
|
|
68
|
+
available.append(acc)
|
|
69
|
+
return available
|
|
70
|
+
|
|
71
|
+
async def lease_account(self, username: Optional[str] = None, tag: Optional[str] = None) -> Optional[EmpireClient]:
|
|
72
|
+
"""
|
|
73
|
+
Leases an account from the pool, logs it in, and returns the client.
|
|
74
|
+
Automatically cycles through available accounts if one is on cooldown.
|
|
75
|
+
"""
|
|
76
|
+
candidates = []
|
|
77
|
+
|
|
78
|
+
if username:
|
|
79
|
+
for acc in self.all_accounts:
|
|
80
|
+
if acc.username == username and acc.username not in self._busy_accounts:
|
|
81
|
+
candidates.append(acc)
|
|
82
|
+
else:
|
|
83
|
+
candidates = self.get_available_accounts(tag)
|
|
84
|
+
|
|
85
|
+
if not candidates:
|
|
86
|
+
logger.warning(f"AccountPool: No idle accounts found (User: {username}, Tag: {tag})")
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
for target_account in candidates:
|
|
90
|
+
# Update last leased index to ensure true cycling
|
|
91
|
+
all_accs = self.all_accounts
|
|
92
|
+
for i, acc in enumerate(all_accs):
|
|
93
|
+
if acc.username == target_account.username:
|
|
94
|
+
self._last_leased_index = i
|
|
95
|
+
break
|
|
96
|
+
|
|
97
|
+
# Mark as busy immediately
|
|
98
|
+
self._busy_accounts.add(target_account.username)
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
# Create and login client
|
|
102
|
+
config = EmpireConfig(username=target_account.username, password=target_account.password)
|
|
103
|
+
client = EmpireClient(config)
|
|
104
|
+
await client.login()
|
|
105
|
+
|
|
106
|
+
# Cache the client
|
|
107
|
+
self._clients[target_account.username] = client
|
|
108
|
+
logger.info(f"AccountPool: Leased and logged in {target_account.username}")
|
|
109
|
+
return client
|
|
110
|
+
|
|
111
|
+
except LoginCooldownError as e:
|
|
112
|
+
logger.warning(f"AccountPool: {target_account.username} on cooldown ({e.cooldown}s). Trying next...")
|
|
113
|
+
self._busy_accounts.remove(target_account.username)
|
|
114
|
+
await client.close()
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
except Exception as e:
|
|
118
|
+
logger.error(f"AccountPool: Failed to login {target_account.username}: {e}")
|
|
119
|
+
self._busy_accounts.remove(target_account.username)
|
|
120
|
+
await client.close()
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
logger.error("AccountPool: All available candidate accounts failed to login.")
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
async def release_account(self, client: EmpireClient):
|
|
127
|
+
"""Logs out and returns the account to the pool."""
|
|
128
|
+
if not client.username:
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
username = client.username
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
if client.is_logged_in:
|
|
135
|
+
await client.close()
|
|
136
|
+
except Exception as e:
|
|
137
|
+
logger.error(f"Error closing client for {username}: {e}")
|
|
138
|
+
finally:
|
|
139
|
+
if username in self._clients:
|
|
140
|
+
del self._clients[username]
|
|
141
|
+
if username in self._busy_accounts:
|
|
142
|
+
self._busy_accounts.remove(username)
|
|
143
|
+
logger.info(f"AccountPool: Released {username}")
|
|
144
|
+
|
|
145
|
+
async def release_all(self):
|
|
146
|
+
"""Releases all active accounts."""
|
|
147
|
+
active_usernames = list(self._clients.keys())
|
|
148
|
+
for username in active_usernames:
|
|
149
|
+
client = self._clients[username]
|
|
150
|
+
await self.release_account(client)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class MultiAccountManager:
|
|
154
|
+
"""Manage multiple active game sessions."""
|
|
155
|
+
|
|
156
|
+
def __init__(self):
|
|
157
|
+
self.clients: Dict[str, EmpireClient] = {}
|
|
158
|
+
|
|
159
|
+
def load_from_registry(self, tag: Optional[str] = None):
|
|
160
|
+
"""
|
|
161
|
+
Load accounts from the central registry into the manager.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
tag: Optional tag to filter accounts (e.g., 'farmer').
|
|
165
|
+
"""
|
|
166
|
+
target_accounts = accounts.get_by_tag(tag) if tag else accounts.get_all()
|
|
167
|
+
count = 0
|
|
168
|
+
for acc in target_accounts:
|
|
169
|
+
if acc.active and acc.username not in self.clients:
|
|
170
|
+
self.clients[acc.username] = acc.get_client()
|
|
171
|
+
count += 1
|
|
172
|
+
logger.info(f"Manager: Loaded {count} accounts (Total: {len(self.clients)})")
|
|
173
|
+
|
|
174
|
+
async def login_all(self):
|
|
175
|
+
"""Login to all managed accounts."""
|
|
176
|
+
if not self.clients:
|
|
177
|
+
logger.warning("No accounts loaded in manager.")
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
logger.info(f"Logging in to {len(self.clients)} accounts...")
|
|
181
|
+
|
|
182
|
+
tasks = []
|
|
183
|
+
for username, client in self.clients.items():
|
|
184
|
+
if not client.is_logged_in:
|
|
185
|
+
tasks.append(self._login_client(username, client))
|
|
186
|
+
|
|
187
|
+
if tasks:
|
|
188
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
189
|
+
success = sum(1 for r in results if r is True)
|
|
190
|
+
logger.info(f"Logged in {success}/{len(tasks)} accounts")
|
|
191
|
+
|
|
192
|
+
async def _login_client(self, username: str, client: EmpireClient) -> bool:
|
|
193
|
+
"""Login to single client."""
|
|
194
|
+
try:
|
|
195
|
+
await client.login()
|
|
196
|
+
# staggered delay to avoid burst
|
|
197
|
+
await asyncio.sleep(1)
|
|
198
|
+
|
|
199
|
+
# Get initial state
|
|
200
|
+
await client.get_detailed_castle_info()
|
|
201
|
+
|
|
202
|
+
logger.info(f"✅ {username} logged in")
|
|
203
|
+
return True
|
|
204
|
+
|
|
205
|
+
except Exception as e:
|
|
206
|
+
logger.error(f"❌ {username} login failed: {e}")
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
async def logout_all(self):
|
|
210
|
+
"""Logout all accounts."""
|
|
211
|
+
for username, client in self.clients.items():
|
|
212
|
+
try:
|
|
213
|
+
await client.close()
|
|
214
|
+
logger.info(f"Logged out: {username}")
|
|
215
|
+
except Exception as e:
|
|
216
|
+
logger.error(f"Error logging out {username}: {e}")
|
|
217
|
+
|
|
218
|
+
self.clients.clear()
|
|
219
|
+
|
|
220
|
+
def get_client(self, username: str) -> Optional[EmpireClient]:
|
|
221
|
+
"""Get client for username."""
|
|
222
|
+
return self.clients.get(username)
|
|
223
|
+
|
|
224
|
+
def get_all_clients(self) -> List[EmpireClient]:
|
|
225
|
+
"""Get all managed clients."""
|
|
226
|
+
return list(self.clients.values())
|
|
227
|
+
|
|
228
|
+
async def execute_on_all(self, func, *args, **kwargs):
|
|
229
|
+
"""
|
|
230
|
+
Execute an async function on all clients.
|
|
231
|
+
The function must accept 'client' as the first argument.
|
|
232
|
+
"""
|
|
233
|
+
tasks = []
|
|
234
|
+
|
|
235
|
+
for client in self.clients.values():
|
|
236
|
+
if client.is_logged_in:
|
|
237
|
+
task = func(client, *args, **kwargs)
|
|
238
|
+
tasks.append(task)
|
|
239
|
+
|
|
240
|
+
if not tasks:
|
|
241
|
+
return []
|
|
242
|
+
|
|
243
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
244
|
+
return results
|
|
245
|
+
|
|
246
|
+
def get_total_resources(self) -> Dict[str, int]:
|
|
247
|
+
"""Get total resources across all accounts."""
|
|
248
|
+
total_wood = 0
|
|
249
|
+
total_stone = 0
|
|
250
|
+
total_food = 0
|
|
251
|
+
total_gold = 0
|
|
252
|
+
total_rubies = 0
|
|
253
|
+
|
|
254
|
+
for client in self.clients.values():
|
|
255
|
+
player = client.state.local_player
|
|
256
|
+
if not player:
|
|
257
|
+
continue
|
|
258
|
+
|
|
259
|
+
total_gold += player.gold
|
|
260
|
+
total_rubies += player.rubies
|
|
261
|
+
|
|
262
|
+
for castle in player.castles.values():
|
|
263
|
+
total_wood += castle.resources.wood
|
|
264
|
+
total_stone += castle.resources.stone
|
|
265
|
+
total_food += castle.resources.food
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
"wood": total_wood,
|
|
269
|
+
"stone": total_stone,
|
|
270
|
+
"food": total_food,
|
|
271
|
+
"gold": total_gold,
|
|
272
|
+
"rubies": total_rubies,
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
def get_stats(self) -> Dict:
|
|
276
|
+
"""Get statistics for all accounts."""
|
|
277
|
+
resources = self.get_total_resources()
|
|
278
|
+
total_castles = 0
|
|
279
|
+
total_population = 0
|
|
280
|
+
logged_in_count = 0
|
|
281
|
+
|
|
282
|
+
for client in self.clients.values():
|
|
283
|
+
if client.is_logged_in:
|
|
284
|
+
logged_in_count += 1
|
|
285
|
+
player = client.state.local_player
|
|
286
|
+
if player:
|
|
287
|
+
total_castles += len(player.castles)
|
|
288
|
+
for castle in player.castles.values():
|
|
289
|
+
total_population += castle.population
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
"logged_in": logged_in_count,
|
|
293
|
+
"total_castles": total_castles,
|
|
294
|
+
"total_population": total_population,
|
|
295
|
+
"resources": resources,
|
|
296
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Quest automation for daily quests and achievements.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from empire_core.protocol.packet import Packet
|
|
9
|
+
from empire_core.state.quest_models import DailyQuest, Quest
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from empire_core.client.client import EmpireClient
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class QuestService:
|
|
18
|
+
"""Service for quest automation and rewards."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, client: "EmpireClient"):
|
|
21
|
+
self.client = client
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def daily_quests(self) -> Optional[DailyQuest]:
|
|
25
|
+
"""Get current daily quests."""
|
|
26
|
+
return self.client.state.daily_quests
|
|
27
|
+
|
|
28
|
+
async def refresh_quests(self) -> bool:
|
|
29
|
+
"""Refresh quest data from server."""
|
|
30
|
+
packet = Packet.build_xt(self.client.config.default_zone, "dql", {})
|
|
31
|
+
await self.client.connection.send(packet)
|
|
32
|
+
return True
|
|
33
|
+
|
|
34
|
+
async def collect_available_rewards(self) -> List[int]:
|
|
35
|
+
"""Collect rewards for completed quests. Returns list of collected quest IDs."""
|
|
36
|
+
if not self.daily_quests:
|
|
37
|
+
logger.debug("No daily quests data available")
|
|
38
|
+
return []
|
|
39
|
+
|
|
40
|
+
collected = []
|
|
41
|
+
for quest_id in self.daily_quests.finished_quests:
|
|
42
|
+
try:
|
|
43
|
+
# Using client method
|
|
44
|
+
success = await self.client.collect_quest_reward(quest_id)
|
|
45
|
+
if success:
|
|
46
|
+
collected.append(quest_id)
|
|
47
|
+
logger.info(f"Collected reward for quest {quest_id}")
|
|
48
|
+
else:
|
|
49
|
+
logger.warning(f"Failed to collect reward for quest {quest_id}")
|
|
50
|
+
except Exception as e:
|
|
51
|
+
logger.error(f"Error collecting quest {quest_id} reward: {e}")
|
|
52
|
+
|
|
53
|
+
return collected
|
|
54
|
+
|
|
55
|
+
def get_completed_quests(self) -> List[int]:
|
|
56
|
+
"""Get list of completed quest IDs."""
|
|
57
|
+
if not self.daily_quests:
|
|
58
|
+
return []
|
|
59
|
+
return self.daily_quests.finished_quests.copy()
|
|
60
|
+
|
|
61
|
+
def get_active_quests(self) -> List[Quest]:
|
|
62
|
+
"""Get list of active quests."""
|
|
63
|
+
if not self.daily_quests:
|
|
64
|
+
return []
|
|
65
|
+
return self.daily_quests.active_quests.copy()
|
|
66
|
+
|
|
67
|
+
def get_quest_progress(self, quest_id: int) -> Optional[List[int]]:
|
|
68
|
+
"""Get progress for a specific quest."""
|
|
69
|
+
active_quests = self.get_active_quests()
|
|
70
|
+
for quest in active_quests:
|
|
71
|
+
if quest.quest_id == quest_id:
|
|
72
|
+
return quest.progress.copy()
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
async def auto_collect_rewards(self) -> int:
|
|
76
|
+
"""Automatically collect all available quest rewards. Returns count collected."""
|
|
77
|
+
collected = await self.collect_available_rewards()
|
|
78
|
+
if collected:
|
|
79
|
+
logger.info(f"Auto-collected rewards for {len(collected)} quests: {collected}")
|
|
80
|
+
return len(collected)
|
|
81
|
+
|
|
82
|
+
def get_daily_quest_summary(self) -> Dict[str, Any]:
|
|
83
|
+
"""Get summary of daily quest status."""
|
|
84
|
+
if not self.daily_quests:
|
|
85
|
+
return {"available": False}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
"available": True,
|
|
89
|
+
"level": self.daily_quests.level,
|
|
90
|
+
"active_count": len(self.daily_quests.active_quests),
|
|
91
|
+
"completed_count": len(self.daily_quests.finished_quests),
|
|
92
|
+
"active_quests": [q.quest_id for q in self.daily_quests.active_quests],
|
|
93
|
+
"completed_quests": self.daily_quests.finished_quests.copy(),
|
|
94
|
+
}
|