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,36 @@
1
+ """
2
+ EmpireCore - Python library for Goodgame Empire automation.
3
+ """
4
+
5
+ from importlib.metadata import version
6
+
7
+ from empire_core.client.client import EmpireClient
8
+ from empire_core.config import EmpireConfig
9
+ from empire_core.state.models import Alliance, Building, Castle, Player, Resources
10
+ from empire_core.state.unit_models import UNIT_IDS, Army, UnitStats
11
+ from empire_core.state.world_models import MapObject, Movement, MovementResources
12
+ from empire_core.utils.enums import KingdomType, MapObjectType, MovementType
13
+
14
+ __version__ = version(__package__)
15
+
16
+ __all__ = [
17
+ "EmpireClient",
18
+ "EmpireConfig",
19
+ # Models
20
+ "Player",
21
+ "Castle",
22
+ "Resources",
23
+ "Building",
24
+ "Alliance",
25
+ "Movement",
26
+ "MovementResources",
27
+ "MapObject",
28
+ "Army",
29
+ "UnitStats",
30
+ # Enums
31
+ "MovementType",
32
+ "MapObjectType",
33
+ "KingdomType",
34
+ # Constants
35
+ "UNIT_IDS",
36
+ ]
@@ -0,0 +1,511 @@
1
+ """
2
+ Action commands for performing game actions (attack, transport, build, etc.)
3
+ """
4
+
5
+ import logging
6
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional
7
+
8
+ from empire_core.config import ResourceType, TroopActionType
9
+ from empire_core.exceptions import ActionError
10
+ from empire_core.protocol.packet import Packet
11
+
12
+ if TYPE_CHECKING:
13
+ from empire_core.client.client import EmpireClient
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class GameActionsMixin:
19
+ """Mixin for game action commands."""
20
+
21
+ async def _send_command_action(
22
+ self,
23
+ command: str,
24
+ payload: Dict[str, Any],
25
+ action_name: str,
26
+ wait_for_response: bool = False,
27
+ timeout: float = 5.0,
28
+ ) -> Any:
29
+ """
30
+ Send a command packet and optionally wait for response.
31
+
32
+ Args:
33
+ command: Command ID (e.g., 'att', 'tra', 'bui')
34
+ payload: Command payload dictionary
35
+ action_name: Human-readable action name for logging
36
+ wait_for_response: Whether to wait for server response
37
+ timeout: Response timeout in seconds
38
+
39
+ Returns:
40
+ Response payload if wait_for_response, else True
41
+
42
+ Raises:
43
+ ActionError: If command fails
44
+ """
45
+ # self.client is now self (EmpireClient)
46
+ client: "EmpireClient" = self # type: ignore
47
+
48
+ if wait_for_response:
49
+ client.response_awaiter.create_waiter(command)
50
+
51
+ packet = Packet.build_xt(client.config.default_zone, command, payload)
52
+
53
+ try:
54
+ await client.connection.send(packet)
55
+ logger.info(f"{action_name} command sent successfully")
56
+
57
+ if wait_for_response:
58
+ logger.debug(f"Waiting for {command} response...")
59
+ response = await client.response_awaiter.wait_for(command, timeout)
60
+ logger.info(f"{action_name} response received: {response}")
61
+ return response
62
+
63
+ return True
64
+ except Exception as e:
65
+ if wait_for_response:
66
+ client.response_awaiter.cancel_command(command)
67
+ logger.error(f"Failed to {action_name.lower()}: {e}")
68
+ raise ActionError(f"{action_name} failed: {e}")
69
+
70
+ def _parse_action_response(self, response: Any, action_name: str) -> bool:
71
+ """
72
+ Parse a standard action response from server.
73
+
74
+ Args:
75
+ response: Server response
76
+ action_name: Action name for error messages
77
+
78
+ Returns:
79
+ True if successful
80
+
81
+ Raises:
82
+ ActionError: If server rejected the action
83
+ """
84
+ if isinstance(response, dict):
85
+ if response.get("success") or response.get("MID"):
86
+ return True
87
+ if response.get("error"):
88
+ raise ActionError(f"Server rejected {action_name}: {response.get('error')}")
89
+ return True
90
+
91
+ def _build_unit_or_tool_payload(self, items: Optional[Dict[int, int]]) -> List[List[int]]:
92
+ """Helper to build unit or tool list for cra payload."""
93
+ if not items:
94
+ return [[-1, 0]] * 6 # Fill with dummy values if empty, matches khan.py pattern
95
+
96
+ payload = []
97
+ for item_id, count in items.items():
98
+ payload.append([item_id, count])
99
+
100
+ # Pad with [-1, 0] if less than 6 items to match common packet structure
101
+ while len(payload) < 6:
102
+ payload.append([-1, 0])
103
+
104
+ return payload[:6] # Ensure max 6 items
105
+
106
+ def _build_flank_payload(self, units: Optional[Dict[int, int]], tools: Optional[Dict[int, int]]) -> Dict[str, Any]:
107
+ """Helper to build flank payload for cra command."""
108
+ return {"T": self._build_unit_or_tool_payload(tools), "U": self._build_unit_or_tool_payload(units)}
109
+
110
+ async def send_attack(
111
+ self,
112
+ origin_x: int,
113
+ origin_y: int,
114
+ target_x: int,
115
+ target_y: int,
116
+ target_area_id: int, # LID
117
+ kingdom_id: int = 0,
118
+ attack_type: int = 0, # ATT
119
+ world_type: int = 0, # WT
120
+ middle_units: Optional[Dict[int, int]] = None,
121
+ left_units: Optional[Dict[int, int]] = None,
122
+ right_units: Optional[Dict[int, int]] = None,
123
+ middle_tools: Optional[Dict[int, int]] = None,
124
+ left_tools: Optional[Dict[int, int]] = None,
125
+ right_tools: Optional[Dict[int, int]] = None,
126
+ wait_for_response: bool = False,
127
+ timeout: float = 5.0,
128
+ ) -> bool:
129
+ """
130
+ Send a detailed attack using the 'cra' command, allowing flank deployment.
131
+
132
+ Args:
133
+ origin_x: Source castle X coordinate.
134
+ origin_y: Source castle Y coordinate.
135
+ target_x: Target X coordinate.
136
+ target_y: Target Y coordinate.
137
+ target_area_id: Target Location ID (LID).
138
+ kingdom_id: Kingdom ID (KID, default 0).
139
+ attack_type: Type of attack (ATT, default 0 for regular attack).
140
+ world_type: World type (WT, default 0).
141
+ middle_units: Units for the middle flank ({unit_id: count}).
142
+ left_units: Units for the left flank.
143
+ right_units: Units for the right flank.
144
+ middle_tools: Tools for the middle flank.
145
+ left_tools: Tools for the left flank.
146
+ right_tools: Tools for the right flank.
147
+ wait_for_response: Whether to wait for server confirmation.
148
+ timeout: Response timeout in seconds.
149
+
150
+ Returns:
151
+ bool: True if attack sent successfully.
152
+
153
+ Raises:
154
+ ActionError: If attack fails or no units/tools are specified.
155
+ """
156
+ logger.info(
157
+ f"Sending detailed attack from ({origin_x},{origin_y}) to ({target_x},{target_y}) LID:{target_area_id}"
158
+ )
159
+
160
+ # Ensure at least some units or tools are being sent
161
+ if not any([middle_units, left_units, right_units, middle_tools, left_tools, right_tools]):
162
+ raise ActionError("Must specify at least one unit or tool for attack.")
163
+
164
+ # Construct the 'A' (Army) payload
165
+ army_payload = {
166
+ "L": self._build_flank_payload(left_units, left_tools),
167
+ "R": self._build_flank_payload(right_units, right_tools),
168
+ "M": self._build_flank_payload(middle_units, middle_tools),
169
+ }
170
+
171
+ # The 'A' field can be a list of these, representing waves. For now, a single wave.
172
+ full_army_payload = [army_payload]
173
+
174
+ payload = {
175
+ "SX": origin_x,
176
+ "SY": origin_y,
177
+ "TX": target_x,
178
+ "TY": target_y,
179
+ "KID": kingdom_id,
180
+ "LID": target_area_id,
181
+ "WT": world_type,
182
+ "ATT": attack_type,
183
+ "A": full_army_payload,
184
+ # Other fields from khan.py example can be added as needed or set to default/0
185
+ "HBW": 0,
186
+ "BPC": 0,
187
+ "AV": 0,
188
+ "LP": 1,
189
+ "FC": 0,
190
+ "PTT": 0,
191
+ "SD": 0,
192
+ "ICA": 0,
193
+ "CD": 99,
194
+ "BKS": [],
195
+ "AST": [-1, -1, -1],
196
+ "RW": [[-1, 0], [-1, 0], [-1, 0], [-1, 0], [-1, 0], [-1, 0], [-1, 0], [-1, 0]],
197
+ }
198
+
199
+ response = await self._send_command_action("cra", payload, "Detailed Attack", wait_for_response, timeout)
200
+
201
+ if wait_for_response:
202
+ return self._parse_action_response(response, "detailed attack")
203
+ return True
204
+
205
+ async def send_transport(
206
+ self,
207
+ origin_castle_id: int,
208
+ target_area_id: int,
209
+ wood: int = 0,
210
+ stone: int = 0,
211
+ food: int = 0,
212
+ wait_for_response: bool = False,
213
+ timeout: float = 5.0,
214
+ ) -> bool:
215
+ """
216
+ Send resources from one castle to another.
217
+
218
+ Args:
219
+ origin_castle_id: ID of sending castle
220
+ target_area_id: ID of receiving area
221
+ wood: Amount of wood
222
+ stone: Amount of stone
223
+ food: Amount of food
224
+ wait_for_response: Wait for server confirmation (default False)
225
+ timeout: Response timeout in seconds (default 5.0)
226
+
227
+ Returns:
228
+ bool: True if transport sent successfully
229
+
230
+ Raises:
231
+ ActionError: If transport fails
232
+ """
233
+ logger.info(f"Sending transport from {origin_castle_id} to {target_area_id}")
234
+
235
+ if wood <= 0 and stone <= 0 and food <= 0:
236
+ raise ActionError("Must send at least one resource")
237
+
238
+ payload = {
239
+ "OID": origin_castle_id,
240
+ "TID": target_area_id,
241
+ "RES": {
242
+ ResourceType.WOOD: wood,
243
+ ResourceType.STONE: stone,
244
+ ResourceType.FOOD: food,
245
+ },
246
+ }
247
+
248
+ response = await self._send_command_action("tra", payload, "Transport", wait_for_response, timeout)
249
+
250
+ if wait_for_response:
251
+ return self._parse_action_response(response, "transport")
252
+ return True
253
+
254
+ async def send_spy(
255
+ self,
256
+ origin_castle_id: int,
257
+ target_area_id: int,
258
+ units: Dict[int, int],
259
+ kingdom_id: int = 0,
260
+ wait_for_response: bool = False,
261
+ timeout: float = 5.0,
262
+ ) -> bool:
263
+ """
264
+ Send spies to a target.
265
+
266
+ Args:
267
+ origin_castle_id: ID of attacking castle
268
+ target_area_id: ID of target area
269
+ units: Dictionary of {unit_id: count} (e.g., spies)
270
+ kingdom_id: Kingdom ID (default 0)
271
+ wait_for_response: Wait for server confirmation (default False)
272
+ timeout: Response timeout in seconds (default 5.0)
273
+
274
+ Returns:
275
+ bool: True if spies sent successfully
276
+
277
+ Raises:
278
+ ActionError: If spy action fails
279
+ """
280
+ logger.info(f"Sending spies from {origin_castle_id} to {target_area_id}")
281
+
282
+ if not units or all(count <= 0 for count in units.values()):
283
+ raise ActionError("Must specify at least one unit")
284
+
285
+ payload = {
286
+ "OID": origin_castle_id,
287
+ "TID": target_area_id,
288
+ "UN": units,
289
+ "TT": TroopActionType.SPY,
290
+ "KID": kingdom_id,
291
+ }
292
+
293
+ response = await self._send_command_action("scl", payload, "Spy", wait_for_response, timeout)
294
+
295
+ if wait_for_response:
296
+ return self._parse_action_response(response, "spy")
297
+ return True
298
+
299
+ async def collect_taxes(
300
+ self,
301
+ castle_id: int,
302
+ wait_for_response: bool = False,
303
+ timeout: float = 5.0,
304
+ ) -> bool:
305
+ """
306
+ Collect taxes/harvest resources from castle.
307
+
308
+ Args:
309
+ castle_id: ID of castle
310
+ wait_for_response: Wait for server confirmation (default False)
311
+ timeout: Response timeout in seconds (default 5.0)
312
+
313
+ Returns:
314
+ bool: True if collection successful
315
+ """
316
+ logger.info(f"Collecting taxes from castle {castle_id}")
317
+
318
+ payload = {"AID": castle_id}
319
+
320
+ response = await self._send_command_action("har", payload, "Harvest", wait_for_response, timeout)
321
+
322
+ if wait_for_response:
323
+ return self._parse_action_response(response, "harvest")
324
+ return True
325
+
326
+ async def use_item(
327
+ self,
328
+ castle_id: int,
329
+ item_id: int,
330
+ count: int = 1,
331
+ target_id: int = 0,
332
+ wait_for_response: bool = False,
333
+ timeout: float = 5.0,
334
+ ) -> bool:
335
+ """
336
+ Use a consumable item.
337
+
338
+ Args:
339
+ castle_id: ID of castle context
340
+ item_id: ID of item to use
341
+ count: Number of items to use
342
+ target_id: Optional target ID (e.g. for specific building or unit boost)
343
+ wait_for_response: Wait for server confirmation
344
+ timeout: Response timeout
345
+
346
+ Returns:
347
+ bool: True if item used successfully
348
+ """
349
+ logger.info(f"Using item {item_id} (x{count}) in castle {castle_id}")
350
+
351
+ payload = {
352
+ "AID": castle_id,
353
+ "IID": item_id,
354
+ "C": count,
355
+ }
356
+ if target_id:
357
+ payload["TID"] = target_id
358
+
359
+ response = await self._send_command_action("itu", payload, "Use Item", wait_for_response, timeout)
360
+
361
+ if wait_for_response:
362
+ return self._parse_action_response(response, "use_item")
363
+ return True
364
+
365
+ async def upgrade_building(self, castle_id: int, building_id: int, building_type: Optional[int] = None) -> bool:
366
+ """
367
+ Upgrade or build a building in a castle.
368
+
369
+ Args:
370
+ castle_id: ID of castle
371
+ building_id: ID of building to upgrade
372
+ building_type: Type of building (if constructing new)
373
+
374
+ Returns:
375
+ bool: True if upgrade started successfully
376
+
377
+ Raises:
378
+ ActionError: If upgrade fails
379
+ """
380
+ logger.info(f"Upgrading building {building_id} in castle {castle_id}")
381
+
382
+ payload = {"AID": castle_id, "BID": building_id}
383
+ if building_type is not None:
384
+ payload["BTYP"] = building_type
385
+
386
+ await self._send_command_action("bui", payload, "Building upgrade")
387
+ return True
388
+
389
+ async def recruit_units(self, castle_id: int, unit_id: int, count: int) -> bool:
390
+ """
391
+ Recruit/train units in a castle.
392
+
393
+ Args:
394
+ castle_id: ID of castle
395
+ unit_id: ID of unit type to recruit
396
+ count: Number of units to recruit
397
+
398
+ Returns:
399
+ bool: True if recruitment started successfully
400
+
401
+ Raises:
402
+ ActionError: If recruitment fails
403
+ """
404
+ logger.info(f"Recruiting {count}x unit {unit_id} in castle {castle_id}")
405
+
406
+ if count <= 0:
407
+ raise ActionError("Must recruit at least one unit")
408
+
409
+ payload = {"AID": castle_id, "UID": unit_id, "C": count}
410
+
411
+ await self._send_command_action("tru", payload, "Unit recruitment")
412
+ return True
413
+
414
+ async def send_support(
415
+ self,
416
+ origin_castle_id: int,
417
+ target_x: int,
418
+ target_y: int,
419
+ kingdom_id: int,
420
+ units: List[List[int]],
421
+ target_location_id: int = -14,
422
+ world_type: int = 12,
423
+ wait_for_response: bool = False,
424
+ timeout: float = 5.0,
425
+ ) -> bool:
426
+ """
427
+ Send troops as support to a location (used for birding troops).
428
+
429
+ This uses the 'cds' command which sends troops to a map location
430
+ for support/defense. Commonly used for sending troops to bird
431
+ protection bookmarks.
432
+
433
+ Args:
434
+ origin_castle_id: ID of sending castle (SID)
435
+ target_x: Target X coordinate
436
+ target_y: Target Y coordinate
437
+ kingdom_id: Kingdom ID (0=Green, 2=Ice, 1=Sands, 3=Storm, 4=Fire)
438
+ units: List of [unit_id, count] pairs, max 10 per request
439
+ target_location_id: Target location ID (default -14 for bird)
440
+ world_type: World type (default 12)
441
+ wait_for_response: Wait for server confirmation
442
+ timeout: Response timeout in seconds
443
+
444
+ Returns:
445
+ bool: True if support sent successfully
446
+
447
+ Raises:
448
+ ActionError: If support fails
449
+
450
+ Example:
451
+ # Send 100 soldiers (unit 1) and 50 archers (unit 2) to bird
452
+ await client.send_support(
453
+ origin_castle_id=12345,
454
+ target_x=100,
455
+ target_y=200,
456
+ kingdom_id=0,
457
+ units=[[1, 100], [2, 50]]
458
+ )
459
+ """
460
+ logger.info(f"Sending support from castle {origin_castle_id} to ({target_x},{target_y})")
461
+
462
+ if not units or len(units) == 0:
463
+ raise ActionError("Must specify at least one unit for support")
464
+
465
+ # Limit to 10 units per request (game limitation)
466
+ if len(units) > 10:
467
+ logger.warning(f"Truncating units list from {len(units)} to 10 (max per request)")
468
+ units = units[:10]
469
+
470
+ payload = {
471
+ "SID": origin_castle_id,
472
+ "TX": target_x,
473
+ "TY": target_y,
474
+ "LID": target_location_id,
475
+ "WT": world_type,
476
+ "HBW": -1,
477
+ "BPC": 1,
478
+ "PTT": 1,
479
+ "SD": 0,
480
+ "A": units,
481
+ "KID": kingdom_id,
482
+ }
483
+
484
+ response = await self._send_command_action("cds", payload, "Support", wait_for_response, timeout)
485
+
486
+ if wait_for_response:
487
+ return self._parse_action_response(response, "support")
488
+ return True
489
+
490
+ async def get_bookmarks(
491
+ self,
492
+ wait_for_response: bool = True,
493
+ timeout: float = 5.0,
494
+ ) -> Any:
495
+ """
496
+ Get alliance bookmarks.
497
+
498
+ This uses the 'gbl' command to retrieve alliance bookmarks,
499
+ which can include bird protection locations.
500
+
501
+ Args:
502
+ wait_for_response: Wait for server response
503
+ timeout: Response timeout in seconds
504
+
505
+ Returns:
506
+ Bookmark data if wait_for_response, else True
507
+ """
508
+ logger.info("Getting alliance bookmarks")
509
+
510
+ response = await self._send_command_action("gbl", {}, "Get bookmarks", wait_for_response, timeout)
511
+ return response
@@ -0,0 +1,24 @@
1
+ """Automation modules for EmpireCore."""
2
+
3
+ from . import tasks
4
+ from .alliance_tools import AllianceService, ChatService
5
+ from .battle_reports import BattleReportService
6
+ from .building_queue import BuildingManager
7
+ from .defense_manager import DefenseManager
8
+ from .map_scanner import MapScanner
9
+ from .quest_automation import QuestService
10
+ from .resource_manager import ResourceManager
11
+ from .unit_production import UnitManager
12
+
13
+ __all__ = [
14
+ "QuestService",
15
+ "BattleReportService",
16
+ "AllianceService",
17
+ "ChatService",
18
+ "MapScanner",
19
+ "ResourceManager",
20
+ "BuildingManager",
21
+ "UnitManager",
22
+ "DefenseManager",
23
+ "tasks",
24
+ ]