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,201 @@
1
+ """
2
+ Additional game commands.
3
+ """
4
+
5
+ import asyncio
6
+ import logging
7
+ from typing import TYPE_CHECKING, Any, Dict
8
+
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 GameCommandsMixin:
19
+ """Mixin for additional game commands."""
20
+
21
+ async def _send_command_generic(
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.
31
+
32
+ Args:
33
+ command: Command ID
34
+ payload: Command payload
35
+ action_name: Human-readable name for logging/errors
36
+ wait_for_response: If True, waits for server response
37
+ timeout: Timeout for response in seconds
38
+
39
+ Returns:
40
+ True if successful (fire-and-forget), or response payload if waiting
41
+
42
+ Raises:
43
+ ActionError: If command fails
44
+ TimeoutError: If response times out
45
+ """
46
+ # self is EmpireClient
47
+ client: "EmpireClient" = self # type: ignore
48
+
49
+ if wait_for_response:
50
+ client.response_awaiter.create_waiter(command)
51
+
52
+ packet = Packet.build_xt(client.config.default_zone, command, payload)
53
+ try:
54
+ await client.connection.send(packet)
55
+ logger.info(f"{action_name} sent")
56
+
57
+ if wait_for_response:
58
+ try:
59
+ response = await client.response_awaiter.wait_for(command, timeout=timeout)
60
+
61
+ # Check for Packet object with error code
62
+ if hasattr(response, "error_code"):
63
+ if response.error_code != 0:
64
+ raise ActionError(f"{action_name} failed with error code {response.error_code}")
65
+
66
+ logger.info(f"{action_name} response received (OK)")
67
+ return response.payload
68
+
69
+ # Legacy/fallback behavior (if response is just payload)
70
+ logger.info(f"{action_name} response received")
71
+ return response
72
+
73
+ except asyncio.TimeoutError:
74
+ raise ActionError(f"{action_name} timed out waiting for response")
75
+
76
+ return True
77
+ except Exception as e:
78
+ logger.error(f"Failed to {action_name.lower()}: {e}")
79
+ raise ActionError(f"{action_name} failed: {e}")
80
+
81
+ async def cancel_building(self, castle_id: int, queue_id: int) -> bool:
82
+ """Cancel building upgrade."""
83
+ return await self._send_command_generic(
84
+ "cbu",
85
+ {"AID": castle_id, "QID": queue_id},
86
+ f"Cancel building in castle {castle_id}",
87
+ )
88
+
89
+ async def speed_up_building(self, castle_id: int, queue_id: int) -> bool:
90
+ """Speed up building with rubies."""
91
+ return await self._send_command_generic(
92
+ "sbu",
93
+ {"AID": castle_id, "QID": queue_id},
94
+ f"Speed up building in castle {castle_id}",
95
+ )
96
+
97
+ async def collect_quest_reward(self, quest_id: int) -> bool:
98
+ """Collect quest reward."""
99
+ return await self._send_command_generic("cqr", {"QID": quest_id}, f"Collect quest {quest_id} reward")
100
+
101
+ async def recall_army(self, movement_id: int) -> bool:
102
+ """Recall/cancel army movement."""
103
+ return await self._send_command_generic("cam", {"MID": movement_id}, f"Recall movement {movement_id}")
104
+
105
+ async def get_battle_reports(self, count: int = 10) -> bool:
106
+ """Get battle reports."""
107
+ return await self._send_command_generic("rep", {"C": count}, f"Get {count} battle reports")
108
+
109
+ async def get_battle_report_details(self, report_id: int) -> bool:
110
+ """Get detailed battle report."""
111
+ return await self._send_command_generic("red", {"RID": report_id}, f"Get battle report {report_id} details")
112
+
113
+ async def send_message(self, player_id: int, subject: str, message: str) -> bool:
114
+ """Send message to player."""
115
+ return await self._send_command_generic(
116
+ "smg",
117
+ {"RID": player_id, "S": subject, "M": message},
118
+ f"Send message to player {player_id}",
119
+ )
120
+
121
+ async def read_mail(self, mail_id: int) -> bool:
122
+ """Mark mail as read."""
123
+ return await self._send_command_generic("rma", {"MID": mail_id}, f"Read mail {mail_id}")
124
+
125
+ async def delete_mail(self, mail_id: int) -> bool:
126
+ """Delete mail."""
127
+ return await self._send_command_generic("dma", {"MID": mail_id}, f"Delete mail {mail_id}")
128
+
129
+ async def send_direct_message(self, recipient_name: str, subject: str, body: str) -> bool:
130
+ """
131
+ Send a direct message (mail) to a player by name.
132
+
133
+ Args:
134
+ recipient_name: Name of the player
135
+ subject: Subject of the message
136
+ body: Message content
137
+
138
+ Returns:
139
+ True if command sent successfully and confirmed by server
140
+ """
141
+ # This command requires verification
142
+ await self._send_command_generic(
143
+ "sms",
144
+ {"RN": recipient_name, "MH": subject, "TXT": body},
145
+ f"Send DM to '{recipient_name}'",
146
+ wait_for_response=True,
147
+ )
148
+
149
+ return True
150
+
151
+ async def search_player(self, name: str) -> bool:
152
+ """
153
+ Search for a player by name.
154
+
155
+ Args:
156
+ name: Player name to search for
157
+
158
+ Returns:
159
+ True if command sent successfully
160
+ """
161
+ return await self._send_command_generic("wsp", {"PN": name}, f"Search player '{name}'")
162
+
163
+ async def get_player_details(self, player_id: int) -> bool:
164
+ """
165
+ Get detailed public profile of a player.
166
+
167
+ Args:
168
+ player_id: Player ID
169
+
170
+ Returns:
171
+ True if command sent successfully
172
+ """
173
+ return await self._send_command_generic("gdi", {"PID": player_id}, f"Get details for player {player_id}")
174
+
175
+ async def get_attack_info(self, origin_id: int, target_id: int, units: Dict[int, int]) -> bool:
176
+ """
177
+ Get attack pre-calculation info (travel time, loot, etc.).
178
+
179
+ Args:
180
+ origin_id: Origin castle ID
181
+ target_id: Target castle/area ID
182
+ units: Unit dictionary {unit_id: count}
183
+
184
+ Returns:
185
+ True if command sent successfully
186
+ """
187
+ return await self._send_command_generic(
188
+ "gai", {"OID": origin_id, "TID": target_id, "UN": units}, f"Get attack info from {origin_id} to {target_id}"
189
+ )
190
+
191
+ async def get_castle_defense_info(self, target_id: int) -> bool:
192
+ """
193
+ Get defense information for a target castle.
194
+
195
+ Args:
196
+ target_id: Target castle/area ID
197
+
198
+ Returns:
199
+ True if command sent successfully
200
+ """
201
+ return await self._send_command_generic("aci", {"TID": target_id}, f"Get defense info for {target_id}")
@@ -0,0 +1,228 @@
1
+ import asyncio
2
+ import logging
3
+ from typing import Awaitable, Callable, Dict, Optional, Set, Tuple, Union
4
+
5
+ import aiohttp
6
+ from empire_core.protocol.packet import Packet
7
+ from empire_core.utils.decorators import handle_errors
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class SFSConnection:
13
+ """
14
+ Manages the WebSocket connection to the SmartFoxServer.
15
+ Handles async reading, writing, and packet dispatching.
16
+ """
17
+
18
+ def __init__(self, url: str):
19
+ self.url = url
20
+ self._session: Optional[aiohttp.ClientSession] = None
21
+ self._ws: Optional[aiohttp.ClientWebSocketResponse] = None
22
+ self._read_task: Optional[asyncio.Task] = None
23
+ self._disconnecting = False
24
+
25
+ # Waiters: cmd_id -> Set of (Future, Predicate)
26
+ self._waiters: Dict[str, Set[Tuple[asyncio.Future, Callable[[Packet], bool]]]] = {}
27
+
28
+ # Callbacks
29
+ self.packet_handler: Optional[Callable[[Packet], Awaitable[None]]] = None
30
+ self.on_close: Optional[Callable[[], Awaitable[None]]] = None
31
+
32
+ @property
33
+ def connected(self) -> bool:
34
+ return self._ws is not None and not self._ws.closed
35
+
36
+ @handle_errors(log_msg="Connection failed", cleanup_method="_close_resources")
37
+ async def connect(self):
38
+ if self.connected:
39
+ return
40
+
41
+ logger.info(f"Connecting to {self.url}...")
42
+
43
+ # Ensure previous resources are cleaned up
44
+ await self._close_resources()
45
+
46
+ self._session = aiohttp.ClientSession()
47
+ try:
48
+ # Add timeout to connection attempt
49
+ self._ws = await asyncio.wait_for(self._session.ws_connect(self.url, heartbeat=30.0), timeout=10.0)
50
+ self._read_task = asyncio.create_task(self._read_loop())
51
+ logger.info("Connected.")
52
+ except Exception as e:
53
+ await self._close_resources()
54
+ raise e
55
+
56
+ async def disconnect(self):
57
+ """Cleanly disconnect and close resources."""
58
+ if self._disconnecting:
59
+ return
60
+
61
+ self._disconnecting = True
62
+ logger.info("Disconnecting...")
63
+
64
+ try:
65
+ if self._read_task and not self._read_task.done():
66
+ self._read_task.cancel()
67
+ try:
68
+ # Wait for read loop to finish its finally block
69
+ await asyncio.wait_for(self._read_task, timeout=2.0)
70
+ except (asyncio.CancelledError, asyncio.TimeoutError):
71
+ pass
72
+
73
+ await self._close_resources()
74
+ self._cancel_all_waiters()
75
+ logger.info("Disconnected.")
76
+ finally:
77
+ self._disconnecting = False
78
+
79
+ async def _close_resources(self):
80
+ """Close WebSocket and Session with timeouts."""
81
+ if self._ws:
82
+ try:
83
+ if not self._ws.closed:
84
+ await asyncio.wait_for(self._ws.close(), timeout=2.0)
85
+ except Exception:
86
+ pass
87
+ self._ws = None
88
+
89
+ if self._session:
90
+ try:
91
+ if not self._session.closed:
92
+ await asyncio.wait_for(self._session.close(), timeout=2.0)
93
+ except Exception:
94
+ pass
95
+ self._session = None
96
+
97
+ def _cancel_all_waiters(self):
98
+ """Cancel all pending futures with a descriptive exception."""
99
+ for waiters in list(self._waiters.values()):
100
+ for fut, _ in list(waiters):
101
+ if not fut.done():
102
+ fut.set_exception(asyncio.CancelledError("Connection closed"))
103
+ self._waiters.clear()
104
+
105
+ async def send(self, data: Union[bytes, str]):
106
+ """Send data to the server."""
107
+ if not self.connected:
108
+ raise RuntimeError("Not connected")
109
+
110
+ if self._ws is None:
111
+ raise RuntimeError("WebSocket not initialized")
112
+
113
+ if isinstance(data, bytes):
114
+ data = data.decode("utf-8")
115
+
116
+ if data.endswith("\x00"):
117
+ data = data[:-1]
118
+
119
+ try:
120
+ await self._ws.send_str(data)
121
+ except Exception as e:
122
+ logger.error(f"Error sending data: {e}")
123
+ # Don't call disconnect() here to avoid potential recursion if disconnect calls send
124
+ # Instead, let the read loop handle the failure or the next operation
125
+ raise e
126
+
127
+ def create_waiter(self, cmd_id: str, predicate: Optional[Callable[[Packet], bool]] = None) -> asyncio.Future:
128
+ """Create a future that will be resolved when a matching packet arrives."""
129
+ loop = asyncio.get_running_loop()
130
+ fut = loop.create_future()
131
+
132
+ if cmd_id not in self._waiters:
133
+ self._waiters[cmd_id] = set()
134
+
135
+ if predicate is None:
136
+
137
+ def predicate(p):
138
+ return True
139
+
140
+ entry = (fut, predicate)
141
+ self._waiters[cmd_id].add(entry)
142
+
143
+ def _cleanup(_):
144
+ if cmd_id in self._waiters:
145
+ try:
146
+ self._waiters[cmd_id].remove(entry)
147
+ if not self._waiters[cmd_id]:
148
+ del self._waiters[cmd_id]
149
+ except (KeyError, ValueError):
150
+ pass
151
+
152
+ fut.add_done_callback(_cleanup)
153
+ return fut
154
+
155
+ async def wait_for(
156
+ self,
157
+ cmd_id: str,
158
+ predicate: Optional[Callable[[Packet], bool]] = None,
159
+ timeout: float = 5.0,
160
+ ) -> Packet:
161
+ """Wait for a specific packet from the server."""
162
+ fut = self.create_waiter(cmd_id, predicate)
163
+ try:
164
+ return await asyncio.wait_for(fut, timeout)
165
+ except asyncio.TimeoutError:
166
+ from empire_core.exceptions import TimeoutError
167
+
168
+ raise TimeoutError(f"Timed out waiting for command '{cmd_id}'")
169
+
170
+ async def _read_loop(self):
171
+ """Continuous loop reading from the WebSocket."""
172
+ logger.debug("Read loop started.")
173
+
174
+ try:
175
+ if not self._ws:
176
+ return
177
+
178
+ async for msg in self._ws:
179
+ if msg.type == aiohttp.WSMsgType.TEXT:
180
+ await self._process_message(msg.data.encode("utf-8"))
181
+ elif msg.type == aiohttp.WSMsgType.BINARY:
182
+ await self._process_message(msg.data)
183
+ elif msg.type == aiohttp.WSMsgType.ERROR:
184
+ logger.error("WS connection error: %s", self._ws.exception())
185
+ break
186
+ elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.CLOSING):
187
+ break
188
+ except Exception as e:
189
+ if not isinstance(e, asyncio.CancelledError):
190
+ logger.error(f"Error in read loop: {e}")
191
+ finally:
192
+ logger.warning("Connection lost.")
193
+ await self._close_resources()
194
+ self._cancel_all_waiters()
195
+
196
+ # Notify client of disconnection in a safe way
197
+ if self.on_close and not self._disconnecting:
198
+
199
+ async def _trigger_close():
200
+ try:
201
+ if self.on_close:
202
+ await self.on_close()
203
+ except Exception as e:
204
+ logger.error(f"Error in on_close callback: {e}")
205
+
206
+ asyncio.create_task(_trigger_close())
207
+
208
+ @handle_errors(log_msg="Failed to parse packet", re_raise=False)
209
+ async def _process_message(self, data: bytes):
210
+ packet = Packet.from_bytes(data)
211
+ await self._dispatch_packet(packet)
212
+
213
+ @handle_errors(log_msg="Error dispatching packet", re_raise=False)
214
+ async def _dispatch_packet(self, packet: Packet):
215
+ # 1. Notify Waiters
216
+ if packet.command_id and packet.command_id in self._waiters:
217
+ current_waiters = list(self._waiters[packet.command_id])
218
+ for fut, predicate in current_waiters:
219
+ if not fut.done():
220
+ try:
221
+ if predicate(packet):
222
+ fut.set_result(packet)
223
+ except Exception as e:
224
+ logger.error(f"Error in waiter predicate: {e}")
225
+
226
+ # 2. Global Handler
227
+ if self.packet_handler:
228
+ await self.packet_handler(packet)
@@ -0,0 +1,156 @@
1
+ """
2
+ Defense management mixin for deploying defense units and tools.
3
+ """
4
+
5
+ import logging
6
+ from typing import TYPE_CHECKING, Dict, List, Optional
7
+
8
+ # Assuming GameCommandsMixin provides _send_command_generic
9
+
10
+ if TYPE_CHECKING:
11
+ from empire_core.client.client import EmpireClient
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class DefenseService:
17
+ """Service for managing castle defenses."""
18
+
19
+ def __init__(self, client: "EmpireClient"):
20
+ self.client = client
21
+
22
+ def _build_defense_slot_payload(self, items: Optional[Dict[int, int]]) -> List[List[int]]:
23
+ """Helper to build tool/unit list for defense payloads."""
24
+ if not items:
25
+ # Common pattern is to send [[-1, 0]] for empty slots or specific padding
26
+ # Assuming up to 5 slots based on khan.py example for dfw
27
+ return [[-1, 0]] * 5
28
+
29
+ payload = []
30
+ for item_id, count in items.items():
31
+ payload.append([item_id, count])
32
+
33
+ # Pad with [-1, 0] if less than expected slots
34
+ while len(payload) < 5: # Assuming 5 slots based on typical defense setups
35
+ payload.append([-1, 0])
36
+
37
+ return payload[:5] # Ensure max 5 items for wall slots
38
+
39
+ async def set_wall_defense(
40
+ self,
41
+ castle_id: int,
42
+ castle_x: int,
43
+ castle_y: int,
44
+ left_tools: Optional[Dict[int, int]] = None,
45
+ middle_tools: Optional[Dict[int, int]] = None,
46
+ right_tools: Optional[Dict[int, int]] = None,
47
+ left_units_up: int = 0, # UP field, assuming Units Placed
48
+ left_units_count: int = 0, # UC field, assuming Unit Count
49
+ middle_units_up: int = 0,
50
+ middle_units_count: int = 0,
51
+ right_units_up: int = 0,
52
+ right_units_count: int = 0,
53
+ wait_for_response: bool = False,
54
+ timeout: float = 5.0,
55
+ ) -> bool:
56
+ """
57
+ Deploy defense tools and units on castle walls.
58
+
59
+ Args:
60
+ castle_id: ID of the castle.
61
+ castle_x: Castle's X coordinate.
62
+ castle_y: Castle's Y coordinate.
63
+ left_tools: Tools for the left wall flank ({tool_id: count}).
64
+ middle_tools: Tools for the middle wall flank.
65
+ right_tools: Tools for the right wall flank.
66
+ left_units_up: Units Placed on left wall (UP).
67
+ left_units_count: Units Count on left wall (UC).
68
+ middle_units_up: Units Placed on middle wall (UP).
69
+ middle_units_count: Units Count on middle wall (UC).
70
+ right_units_up: Units Placed on right wall (UP).
71
+ right_units_count: Units Count on right wall (UC).
72
+ wait_for_response: Whether to wait for server confirmation.
73
+ timeout: Response timeout in seconds.
74
+
75
+ Returns:
76
+ bool: True if defense set successfully.
77
+
78
+ Raises:
79
+ ActionError: If setting defense fails.
80
+ """
81
+ logger.info(f"Setting wall defense for castle {castle_id} at ({castle_x},{castle_y})")
82
+
83
+ payload = {
84
+ "CX": castle_x,
85
+ "CY": castle_y,
86
+ "AID": castle_id,
87
+ "L": {
88
+ "S": self._build_defense_slot_payload(left_tools),
89
+ "UP": left_units_up,
90
+ "UC": left_units_count,
91
+ },
92
+ "M": {
93
+ "S": self._build_defense_slot_payload(middle_tools),
94
+ "UP": middle_units_up,
95
+ "UC": middle_units_count,
96
+ },
97
+ "R": {
98
+ "S": self._build_defense_slot_payload(right_tools),
99
+ "UP": right_units_up,
100
+ "UC": right_units_count,
101
+ },
102
+ }
103
+
104
+ # Use client's command sender
105
+ await self.client._send_command_generic("dfw", payload, "Set Wall Defense", wait_for_response, timeout)
106
+
107
+ # Use client's response parser if available or implemented here?
108
+ # The Mixin had _parse_action_response as a stub.
109
+ # Checking if client has it. GameActionsMixin usually has it.
110
+ # If not, we just return True for now if response is valid.
111
+ return True
112
+
113
+ async def set_moat_defense(
114
+ self,
115
+ castle_id: int,
116
+ castle_x: int,
117
+ castle_y: int,
118
+ left_slots: Optional[Dict[int, int]] = None,
119
+ middle_slots: Optional[Dict[int, int]] = None,
120
+ right_slots: Optional[Dict[int, int]] = None,
121
+ wait_for_response: bool = False,
122
+ timeout: float = 5.0,
123
+ ) -> bool:
124
+ """
125
+ Deploy defense tools in castle moat/field.
126
+
127
+ Args:
128
+ castle_id: ID of the castle.
129
+ castle_x: Castle's X coordinate.
130
+ castle_y: Castle's Y coordinate.
131
+ left_slots: Tools for the left moat/field flank ({tool_id: count}).
132
+ middle_slots: Tools for the middle moat/field flank.
133
+ right_slots: Tools for the right moat/field flank.
134
+ wait_for_response: Whether to wait for server confirmation.
135
+ timeout: Response timeout in seconds.
136
+
137
+ Returns:
138
+ bool: True if defense set successfully.
139
+
140
+ Raises:
141
+ ActionError: If setting defense fails.
142
+ """
143
+ logger.info(f"Setting moat defense for castle {castle_id} at ({castle_x},{castle_y})")
144
+
145
+ payload = {
146
+ "CX": castle_x,
147
+ "CY": castle_y,
148
+ "AID": castle_id,
149
+ "LS": self._build_defense_slot_payload(left_slots),
150
+ "MS": self._build_defense_slot_payload(middle_slots),
151
+ "RS": self._build_defense_slot_payload(right_slots),
152
+ }
153
+
154
+ await self.client._send_command_generic("dfm", payload, "Set Moat Defense", wait_for_response, timeout)
155
+
156
+ return True
@@ -0,0 +1,35 @@
1
+ from empire_core.events.base import (
2
+ # Action events
3
+ AttackSentEvent,
4
+ Event,
5
+ IncomingAttackEvent,
6
+ MovementArrivedEvent,
7
+ MovementCancelledEvent,
8
+ # Movement events
9
+ MovementEvent,
10
+ MovementStartedEvent,
11
+ MovementUpdatedEvent,
12
+ PacketEvent,
13
+ ReturnArrivalEvent,
14
+ ScoutSentEvent,
15
+ TransportSentEvent,
16
+ )
17
+ from empire_core.events.manager import EventManager
18
+
19
+ __all__ = [
20
+ "Event",
21
+ "PacketEvent",
22
+ "EventManager",
23
+ # Movement events
24
+ "MovementEvent",
25
+ "MovementStartedEvent",
26
+ "MovementUpdatedEvent",
27
+ "MovementArrivedEvent",
28
+ "MovementCancelledEvent",
29
+ "IncomingAttackEvent",
30
+ "ReturnArrivalEvent",
31
+ # Action events
32
+ "AttackSentEvent",
33
+ "ScoutSentEvent",
34
+ "TransportSentEvent",
35
+ ]