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.
Files changed (67) hide show
  1. empire_core/__init__.py +36 -0
  2. empire_core/_archive/actions.py +511 -0
  3. empire_core/_archive/automation/__init__.py +24 -0
  4. empire_core/_archive/automation/alliance_tools.py +266 -0
  5. empire_core/_archive/automation/battle_reports.py +196 -0
  6. empire_core/_archive/automation/building_queue.py +242 -0
  7. empire_core/_archive/automation/defense_manager.py +124 -0
  8. empire_core/_archive/automation/map_scanner.py +370 -0
  9. empire_core/_archive/automation/multi_account.py +296 -0
  10. empire_core/_archive/automation/quest_automation.py +94 -0
  11. empire_core/_archive/automation/resource_manager.py +380 -0
  12. empire_core/_archive/automation/target_finder.py +153 -0
  13. empire_core/_archive/automation/tasks.py +224 -0
  14. empire_core/_archive/automation/unit_production.py +719 -0
  15. empire_core/_archive/cli.py +68 -0
  16. empire_core/_archive/client_async.py +469 -0
  17. empire_core/_archive/commands.py +201 -0
  18. empire_core/_archive/connection_async.py +228 -0
  19. empire_core/_archive/defense.py +156 -0
  20. empire_core/_archive/events/__init__.py +35 -0
  21. empire_core/_archive/events/base.py +153 -0
  22. empire_core/_archive/events/manager.py +85 -0
  23. empire_core/accounts.py +190 -0
  24. empire_core/client/__init__.py +0 -0
  25. empire_core/client/client.py +459 -0
  26. empire_core/config.py +87 -0
  27. empire_core/exceptions.py +42 -0
  28. empire_core/network/__init__.py +0 -0
  29. empire_core/network/connection.py +378 -0
  30. empire_core/protocol/__init__.py +0 -0
  31. empire_core/protocol/models/__init__.py +339 -0
  32. empire_core/protocol/models/alliance.py +186 -0
  33. empire_core/protocol/models/army.py +444 -0
  34. empire_core/protocol/models/attack.py +229 -0
  35. empire_core/protocol/models/auth.py +216 -0
  36. empire_core/protocol/models/base.py +403 -0
  37. empire_core/protocol/models/building.py +455 -0
  38. empire_core/protocol/models/castle.py +317 -0
  39. empire_core/protocol/models/chat.py +150 -0
  40. empire_core/protocol/models/defense.py +300 -0
  41. empire_core/protocol/models/map.py +269 -0
  42. empire_core/protocol/packet.py +104 -0
  43. empire_core/services/__init__.py +31 -0
  44. empire_core/services/alliance.py +222 -0
  45. empire_core/services/base.py +107 -0
  46. empire_core/services/castle.py +221 -0
  47. empire_core/state/__init__.py +0 -0
  48. empire_core/state/manager.py +398 -0
  49. empire_core/state/models.py +215 -0
  50. empire_core/state/quest_models.py +60 -0
  51. empire_core/state/report_models.py +115 -0
  52. empire_core/state/unit_models.py +75 -0
  53. empire_core/state/world_models.py +269 -0
  54. empire_core/storage/__init__.py +1 -0
  55. empire_core/storage/database.py +237 -0
  56. empire_core/utils/__init__.py +0 -0
  57. empire_core/utils/battle_sim.py +172 -0
  58. empire_core/utils/calculations.py +170 -0
  59. empire_core/utils/crypto.py +8 -0
  60. empire_core/utils/decorators.py +69 -0
  61. empire_core/utils/enums.py +111 -0
  62. empire_core/utils/helpers.py +252 -0
  63. empire_core/utils/response_awaiter.py +153 -0
  64. empire_core/utils/troops.py +93 -0
  65. empire_core-0.7.3.dist-info/METADATA +197 -0
  66. empire_core-0.7.3.dist-info/RECORD +67 -0
  67. 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
+ }