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,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)
@@ -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