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,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
|
+
]
|