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,300 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Defense protocol models.
|
|
3
|
+
|
|
4
|
+
Commands:
|
|
5
|
+
- dfc: Get defense configuration
|
|
6
|
+
- dfk: Change keep defense
|
|
7
|
+
- dfw: Change wall defense
|
|
8
|
+
- dfm: Change moat defense
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from pydantic import ConfigDict, Field
|
|
14
|
+
|
|
15
|
+
from .base import BaseRequest, BaseResponse, UnitCount
|
|
16
|
+
|
|
17
|
+
# =============================================================================
|
|
18
|
+
# DFC - Get Defense Configuration
|
|
19
|
+
# =============================================================================
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class GetDefenseRequest(BaseRequest):
|
|
23
|
+
"""
|
|
24
|
+
Get defense configuration for a castle.
|
|
25
|
+
|
|
26
|
+
Command: dfc
|
|
27
|
+
Payload: {"CID": castle_id}
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
command = "dfc"
|
|
31
|
+
|
|
32
|
+
castle_id: int = Field(alias="CID")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class DefenseConfiguration(BaseResponse):
|
|
36
|
+
"""Defense configuration for a location (keep, wall, moat)."""
|
|
37
|
+
|
|
38
|
+
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
39
|
+
|
|
40
|
+
units: list[UnitCount] = Field(alias="U", default_factory=list)
|
|
41
|
+
tools: list[UnitCount] = Field(alias="T", default_factory=list)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class GetDefenseResponse(BaseResponse):
|
|
45
|
+
"""
|
|
46
|
+
Response containing defense configuration.
|
|
47
|
+
|
|
48
|
+
Command: dfc
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
command = "dfc"
|
|
52
|
+
|
|
53
|
+
keep: DefenseConfiguration | None = Field(alias="K", default=None)
|
|
54
|
+
wall: DefenseConfiguration | None = Field(alias="W", default=None)
|
|
55
|
+
moat: DefenseConfiguration | None = Field(alias="M", default=None)
|
|
56
|
+
courtyard: DefenseConfiguration | None = Field(alias="C", default=None)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# =============================================================================
|
|
60
|
+
# DFK - Change Keep Defense
|
|
61
|
+
# =============================================================================
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ChangeKeepDefenseRequest(BaseRequest):
|
|
65
|
+
"""
|
|
66
|
+
Change keep defense configuration.
|
|
67
|
+
|
|
68
|
+
Command: dfk
|
|
69
|
+
Payload: {
|
|
70
|
+
"CID": castle_id,
|
|
71
|
+
"U": [{"UID": unit_id, "C": count}, ...],
|
|
72
|
+
"T": [{"TID": tool_id, "C": count}, ...]
|
|
73
|
+
}
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
command = "dfk"
|
|
77
|
+
|
|
78
|
+
castle_id: int = Field(alias="CID")
|
|
79
|
+
units: list[UnitCount] = Field(alias="U", default_factory=list)
|
|
80
|
+
tools: list[UnitCount] = Field(alias="T", default_factory=list)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ChangeKeepDefenseResponse(BaseResponse):
|
|
84
|
+
"""
|
|
85
|
+
Response to changing keep defense.
|
|
86
|
+
|
|
87
|
+
Command: dfk
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
command = "dfk"
|
|
91
|
+
|
|
92
|
+
success: bool = Field(default=True)
|
|
93
|
+
error_code: int = Field(alias="E", default=0)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# =============================================================================
|
|
97
|
+
# DFW - Change Wall Defense
|
|
98
|
+
# =============================================================================
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class ChangeWallDefenseRequest(BaseRequest):
|
|
102
|
+
"""
|
|
103
|
+
Change wall defense configuration.
|
|
104
|
+
|
|
105
|
+
Command: dfw
|
|
106
|
+
Payload: {
|
|
107
|
+
"CID": castle_id,
|
|
108
|
+
"U": [{"UID": unit_id, "C": count}, ...],
|
|
109
|
+
"T": [{"TID": tool_id, "C": count}, ...]
|
|
110
|
+
}
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
command = "dfw"
|
|
114
|
+
|
|
115
|
+
castle_id: int = Field(alias="CID")
|
|
116
|
+
units: list[UnitCount] = Field(alias="U", default_factory=list)
|
|
117
|
+
tools: list[UnitCount] = Field(alias="T", default_factory=list)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class ChangeWallDefenseResponse(BaseResponse):
|
|
121
|
+
"""
|
|
122
|
+
Response to changing wall defense.
|
|
123
|
+
|
|
124
|
+
Command: dfw
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
command = "dfw"
|
|
128
|
+
|
|
129
|
+
success: bool = Field(default=True)
|
|
130
|
+
error_code: int = Field(alias="E", default=0)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# =============================================================================
|
|
134
|
+
# DFM - Change Moat Defense
|
|
135
|
+
# =============================================================================
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class ChangeMoatDefenseRequest(BaseRequest):
|
|
139
|
+
"""
|
|
140
|
+
Change moat defense configuration.
|
|
141
|
+
|
|
142
|
+
Command: dfm
|
|
143
|
+
Payload: {
|
|
144
|
+
"CID": castle_id,
|
|
145
|
+
"U": [{"UID": unit_id, "C": count}, ...],
|
|
146
|
+
"T": [{"TID": tool_id, "C": count}, ...]
|
|
147
|
+
}
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
command = "dfm"
|
|
151
|
+
|
|
152
|
+
castle_id: int = Field(alias="CID")
|
|
153
|
+
units: list[UnitCount] = Field(alias="U", default_factory=list)
|
|
154
|
+
tools: list[UnitCount] = Field(alias="T", default_factory=list)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class ChangeMoatDefenseResponse(BaseResponse):
|
|
158
|
+
"""
|
|
159
|
+
Response to changing moat defense.
|
|
160
|
+
|
|
161
|
+
Command: dfm
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
command = "dfm"
|
|
165
|
+
|
|
166
|
+
success: bool = Field(default=True)
|
|
167
|
+
error_code: int = Field(alias="E", default=0)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# =============================================================================
|
|
171
|
+
# SDI - Support Defense Info (Alliance Member Castle Defense)
|
|
172
|
+
# =============================================================================
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class GetSupportDefenseRequest(BaseRequest):
|
|
176
|
+
"""
|
|
177
|
+
Get defense info for an alliance member's castle.
|
|
178
|
+
|
|
179
|
+
Command: sdi
|
|
180
|
+
Payload: {"TX": target_x, "TY": target_y, "SX": source_x, "SY": source_y}
|
|
181
|
+
|
|
182
|
+
Note: Can only query castles of players in the same alliance.
|
|
183
|
+
TX/TY = Target castle coordinates (the one being attacked)
|
|
184
|
+
SX/SY = Source castle coordinates (your castle sending support)
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
command = "sdi"
|
|
188
|
+
|
|
189
|
+
target_x: int = Field(alias="TX")
|
|
190
|
+
target_y: int = Field(alias="TY")
|
|
191
|
+
source_x: int = Field(alias="SX")
|
|
192
|
+
source_y: int = Field(alias="SY")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class GetSupportDefenseResponse(BaseResponse):
|
|
196
|
+
"""
|
|
197
|
+
Response containing defense information for an alliance member's castle.
|
|
198
|
+
|
|
199
|
+
Command: sdi
|
|
200
|
+
|
|
201
|
+
The response contains:
|
|
202
|
+
- SCID: Castle ID queried
|
|
203
|
+
- S: List of 6 defense positions, each containing [[unit_id, count], ...] pairs
|
|
204
|
+
- B: Commander/Lord info
|
|
205
|
+
- gui: Unit inventory
|
|
206
|
+
- gli: Lords info
|
|
207
|
+
- UYL: Total yard limit (max troops in courtyard)
|
|
208
|
+
- AUYL: Available yard limit
|
|
209
|
+
- UWL: Wall limit
|
|
210
|
+
|
|
211
|
+
To get total defenders, sum all unit counts across all positions in S.
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
command = "sdi"
|
|
215
|
+
|
|
216
|
+
castle_id: int = Field(alias="SCID", default=0)
|
|
217
|
+
|
|
218
|
+
# S contains 6 arrays (defense positions), each with [unit_id, count] pairs
|
|
219
|
+
# e.g. [[[487, 5174], [488, 20]], [[487, 347]], ...]
|
|
220
|
+
defense_positions: list = Field(alias="S", default_factory=list)
|
|
221
|
+
|
|
222
|
+
# Commander/Lord info (optional, not always present)
|
|
223
|
+
commander_info: dict | None = Field(alias="B", default=None)
|
|
224
|
+
|
|
225
|
+
# Unit inventory info
|
|
226
|
+
unit_inventory: dict | None = Field(alias="gui", default=None)
|
|
227
|
+
|
|
228
|
+
# Lords info
|
|
229
|
+
lords_info: dict | None = Field(alias="gli", default=None)
|
|
230
|
+
|
|
231
|
+
# Capacity limits
|
|
232
|
+
yard_limit: int = Field(alias="UYL", default=0) # Total yard/courtyard limit
|
|
233
|
+
available_yard_limit: int = Field(alias="AUYL", default=0) # Available yard space
|
|
234
|
+
wall_limit: int = Field(alias="UWL", default=0) # Wall limit
|
|
235
|
+
|
|
236
|
+
def get_total_defenders(self) -> int:
|
|
237
|
+
"""
|
|
238
|
+
Calculate total number of defending troops.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Total count of all units across all defense positions.
|
|
242
|
+
"""
|
|
243
|
+
total = 0
|
|
244
|
+
for position in self.defense_positions:
|
|
245
|
+
if isinstance(position, list):
|
|
246
|
+
for unit_pair in position:
|
|
247
|
+
if isinstance(unit_pair, list) and len(unit_pair) >= 2:
|
|
248
|
+
# unit_pair is [unit_id, count]
|
|
249
|
+
total += unit_pair[1]
|
|
250
|
+
return total
|
|
251
|
+
|
|
252
|
+
def get_max_defense(self) -> int:
|
|
253
|
+
"""
|
|
254
|
+
Get the maximum defense capacity for this castle.
|
|
255
|
+
|
|
256
|
+
UYL (yard_limit) represents the total capacity including
|
|
257
|
+
courtyard limit plus room for alliance support.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Maximum number of troops that can defend this castle.
|
|
261
|
+
"""
|
|
262
|
+
return self.yard_limit
|
|
263
|
+
|
|
264
|
+
def get_units_by_position(self) -> list[dict[int, int]]:
|
|
265
|
+
"""
|
|
266
|
+
Get unit counts grouped by defense position.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
List of 6 dicts, each mapping unit_id -> count for that position.
|
|
270
|
+
"""
|
|
271
|
+
result = []
|
|
272
|
+
for position in self.defense_positions:
|
|
273
|
+
units: dict[int, int] = {}
|
|
274
|
+
if isinstance(position, list):
|
|
275
|
+
for unit_pair in position:
|
|
276
|
+
if isinstance(unit_pair, list) and len(unit_pair) >= 2:
|
|
277
|
+
unit_id, count = unit_pair[0], unit_pair[1]
|
|
278
|
+
units[unit_id] = units.get(unit_id, 0) + count
|
|
279
|
+
result.append(units)
|
|
280
|
+
return result
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
__all__ = [
|
|
284
|
+
# DFC - Get Defense
|
|
285
|
+
"GetDefenseRequest",
|
|
286
|
+
"GetDefenseResponse",
|
|
287
|
+
"DefenseConfiguration",
|
|
288
|
+
# DFK - Keep Defense
|
|
289
|
+
"ChangeKeepDefenseRequest",
|
|
290
|
+
"ChangeKeepDefenseResponse",
|
|
291
|
+
# DFW - Wall Defense
|
|
292
|
+
"ChangeWallDefenseRequest",
|
|
293
|
+
"ChangeWallDefenseResponse",
|
|
294
|
+
# DFM - Moat Defense
|
|
295
|
+
"ChangeMoatDefenseRequest",
|
|
296
|
+
"ChangeMoatDefenseResponse",
|
|
297
|
+
# SDI - Support Defense Info
|
|
298
|
+
"GetSupportDefenseRequest",
|
|
299
|
+
"GetSupportDefenseResponse",
|
|
300
|
+
]
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Map protocol models.
|
|
3
|
+
|
|
4
|
+
Commands:
|
|
5
|
+
- gaa: Get map area/chunk
|
|
6
|
+
- gam: Get active movements
|
|
7
|
+
- fnm: Find NPC on map
|
|
8
|
+
- adi: Get area/target detailed info
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from pydantic import ConfigDict, Field
|
|
14
|
+
|
|
15
|
+
from .base import BaseRequest, BaseResponse, PlayerInfo, Position
|
|
16
|
+
|
|
17
|
+
# =============================================================================
|
|
18
|
+
# GAA - Get Map Area
|
|
19
|
+
# =============================================================================
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class GetMapAreaRequest(BaseRequest):
|
|
23
|
+
"""
|
|
24
|
+
Get a chunk of the map.
|
|
25
|
+
|
|
26
|
+
Command: gaa
|
|
27
|
+
Payload: {"X": x, "Y": y, "W": width, "H": height, "KID": kingdom_id}
|
|
28
|
+
|
|
29
|
+
Returns information about all objects in the specified area.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
command = "gaa"
|
|
33
|
+
|
|
34
|
+
x: int = Field(alias="X")
|
|
35
|
+
y: int = Field(alias="Y")
|
|
36
|
+
width: int = Field(alias="W", default=10)
|
|
37
|
+
height: int = Field(alias="H", default=10)
|
|
38
|
+
kingdom_id: int = Field(alias="KID", default=0)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class MapObject(BaseResponse):
|
|
42
|
+
"""An object on the map (castle, NPC, resource, etc.)."""
|
|
43
|
+
|
|
44
|
+
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
45
|
+
|
|
46
|
+
x: int = Field(alias="X")
|
|
47
|
+
y: int = Field(alias="Y")
|
|
48
|
+
object_type: int = Field(alias="OT") # Type of object
|
|
49
|
+
object_id: int = Field(alias="OID", default=0)
|
|
50
|
+
owner_id: int | None = Field(alias="PID", default=None)
|
|
51
|
+
owner_name: str | None = Field(alias="PN", default=None)
|
|
52
|
+
alliance_id: int | None = Field(alias="AID", default=None)
|
|
53
|
+
alliance_name: str | None = Field(alias="AN", default=None)
|
|
54
|
+
level: int = Field(alias="L", default=0)
|
|
55
|
+
name: str | None = Field(alias="N", default=None)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def position(self) -> Position:
|
|
59
|
+
"""Get object position."""
|
|
60
|
+
return Position(X=self.x, Y=self.y)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class GetMapAreaResponse(BaseResponse):
|
|
64
|
+
"""
|
|
65
|
+
Response containing map area data.
|
|
66
|
+
|
|
67
|
+
Command: gaa
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
command = "gaa"
|
|
71
|
+
|
|
72
|
+
objects: list[MapObject] = Field(alias="O", default_factory=list)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# =============================================================================
|
|
76
|
+
# GAM - Get Active Movements
|
|
77
|
+
# =============================================================================
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class GetMovementsRequest(BaseRequest):
|
|
81
|
+
"""
|
|
82
|
+
Get all active troop movements.
|
|
83
|
+
|
|
84
|
+
Command: gam
|
|
85
|
+
Payload: {} (empty) or {"CID": castle_id}
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
command = "gam"
|
|
89
|
+
|
|
90
|
+
castle_id: int | None = Field(alias="CID", default=None)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class Movement(BaseResponse):
|
|
94
|
+
"""An active troop movement."""
|
|
95
|
+
|
|
96
|
+
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
97
|
+
|
|
98
|
+
movement_id: int = Field(alias="MID")
|
|
99
|
+
movement_type: int = Field(alias="MT") # 1=attack, 2=support, 3=spy, 4=trade, etc.
|
|
100
|
+
|
|
101
|
+
# Source
|
|
102
|
+
source_x: int = Field(alias="SX")
|
|
103
|
+
source_y: int = Field(alias="SY")
|
|
104
|
+
source_castle_id: int = Field(alias="SCID", default=0)
|
|
105
|
+
source_player_id: int = Field(alias="SPID", default=0)
|
|
106
|
+
|
|
107
|
+
# Target
|
|
108
|
+
target_x: int = Field(alias="TX")
|
|
109
|
+
target_y: int = Field(alias="TY")
|
|
110
|
+
target_castle_id: int | None = Field(alias="TCID", default=None)
|
|
111
|
+
target_player_id: int | None = Field(alias="TPID", default=None)
|
|
112
|
+
|
|
113
|
+
# Timing
|
|
114
|
+
start_time: int = Field(alias="ST") # Unix timestamp
|
|
115
|
+
arrival_time: int = Field(alias="AT") # Unix timestamp
|
|
116
|
+
return_time: int | None = Field(alias="RT", default=None)
|
|
117
|
+
|
|
118
|
+
# Status
|
|
119
|
+
is_returning: bool = Field(alias="IR", default=False)
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def source_position(self) -> Position:
|
|
123
|
+
"""Get source position."""
|
|
124
|
+
return Position(X=self.source_x, Y=self.source_y)
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def target_position(self) -> Position:
|
|
128
|
+
"""Get target position."""
|
|
129
|
+
return Position(X=self.target_x, Y=self.target_y)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class GetMovementsResponse(BaseResponse):
|
|
133
|
+
"""
|
|
134
|
+
Response containing active movements.
|
|
135
|
+
|
|
136
|
+
Command: gam
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
command = "gam"
|
|
140
|
+
|
|
141
|
+
movements: list[Movement] = Field(alias="M", default_factory=list)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# =============================================================================
|
|
145
|
+
# FNM - Find NPC
|
|
146
|
+
# =============================================================================
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class FindNPCRequest(BaseRequest):
|
|
150
|
+
"""
|
|
151
|
+
Find NPC targets on the map.
|
|
152
|
+
|
|
153
|
+
Command: fnm
|
|
154
|
+
Payload: {"NT": npc_type, "L": level, "KID": kingdom_id}
|
|
155
|
+
|
|
156
|
+
NPC types vary by game version.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
command = "fnm"
|
|
160
|
+
|
|
161
|
+
npc_type: int = Field(alias="NT")
|
|
162
|
+
level: int | None = Field(alias="L", default=None)
|
|
163
|
+
kingdom_id: int = Field(alias="KID", default=0)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class NPCLocation(BaseResponse):
|
|
167
|
+
"""An NPC location on the map."""
|
|
168
|
+
|
|
169
|
+
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
170
|
+
|
|
171
|
+
x: int = Field(alias="X")
|
|
172
|
+
y: int = Field(alias="Y")
|
|
173
|
+
npc_type: int = Field(alias="NT")
|
|
174
|
+
level: int = Field(alias="L")
|
|
175
|
+
npc_id: int = Field(alias="NID", default=0)
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def position(self) -> Position:
|
|
179
|
+
"""Get NPC position."""
|
|
180
|
+
return Position(X=self.x, Y=self.y)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class FindNPCResponse(BaseResponse):
|
|
184
|
+
"""
|
|
185
|
+
Response containing NPC locations.
|
|
186
|
+
|
|
187
|
+
Command: fnm
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
command = "fnm"
|
|
191
|
+
|
|
192
|
+
npcs: list[NPCLocation] = Field(alias="N", default_factory=list)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# =============================================================================
|
|
196
|
+
# ADI - Get Area/Target Detailed Info
|
|
197
|
+
# =============================================================================
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class GetTargetInfoRequest(BaseRequest):
|
|
201
|
+
"""
|
|
202
|
+
Get detailed info about a specific map location/target.
|
|
203
|
+
|
|
204
|
+
Command: adi
|
|
205
|
+
Payload: {"X": x, "Y": y, "KID": kingdom_id}
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
command = "adi"
|
|
209
|
+
|
|
210
|
+
x: int = Field(alias="X")
|
|
211
|
+
y: int = Field(alias="Y")
|
|
212
|
+
kingdom_id: int = Field(alias="KID", default=0)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class TargetInfo(BaseResponse):
|
|
216
|
+
"""Detailed information about a target location."""
|
|
217
|
+
|
|
218
|
+
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
219
|
+
|
|
220
|
+
x: int = Field(alias="X")
|
|
221
|
+
y: int = Field(alias="Y")
|
|
222
|
+
object_type: int = Field(alias="OT")
|
|
223
|
+
object_id: int = Field(alias="OID", default=0)
|
|
224
|
+
|
|
225
|
+
# Owner info (if owned)
|
|
226
|
+
owner: PlayerInfo | None = Field(alias="O", default=None)
|
|
227
|
+
|
|
228
|
+
# Castle-specific
|
|
229
|
+
castle_name: str | None = Field(alias="CN", default=None)
|
|
230
|
+
castle_level: int | None = Field(alias="CL", default=None)
|
|
231
|
+
|
|
232
|
+
# NPC-specific
|
|
233
|
+
npc_type: int | None = Field(alias="NT", default=None)
|
|
234
|
+
npc_level: int | None = Field(alias="NL", default=None)
|
|
235
|
+
|
|
236
|
+
# Resources (for resource nodes)
|
|
237
|
+
resources: int | None = Field(alias="R", default=None)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class GetTargetInfoResponse(BaseResponse):
|
|
241
|
+
"""
|
|
242
|
+
Response containing target information.
|
|
243
|
+
|
|
244
|
+
Command: adi
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
command = "adi"
|
|
248
|
+
|
|
249
|
+
target: TargetInfo | None = Field(alias="T", default=None)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
__all__ = [
|
|
253
|
+
# GAA - Map Area
|
|
254
|
+
"GetMapAreaRequest",
|
|
255
|
+
"GetMapAreaResponse",
|
|
256
|
+
"MapObject",
|
|
257
|
+
# GAM - Movements
|
|
258
|
+
"GetMovementsRequest",
|
|
259
|
+
"GetMovementsResponse",
|
|
260
|
+
"Movement",
|
|
261
|
+
# FNM - Find NPC
|
|
262
|
+
"FindNPCRequest",
|
|
263
|
+
"FindNPCResponse",
|
|
264
|
+
"NPCLocation",
|
|
265
|
+
# ADI - Target Info
|
|
266
|
+
"GetTargetInfoRequest",
|
|
267
|
+
"GetTargetInfoResponse",
|
|
268
|
+
"TargetInfo",
|
|
269
|
+
]
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import xml.etree.ElementTree as ET
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Dict, Optional, Union
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class Packet:
|
|
9
|
+
"""
|
|
10
|
+
Base representation of a SmartFoxServer packet.
|
|
11
|
+
Can be either XML (Handshake) or XT (Extended/JSON).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
raw_data: str
|
|
15
|
+
is_xml: bool
|
|
16
|
+
|
|
17
|
+
command_id: Optional[str] = None
|
|
18
|
+
request_id: int = -1
|
|
19
|
+
error_code: int = 0 # New field for XT status/error code
|
|
20
|
+
payload: Union[Dict[str, Any], ET.Element, None] = None
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def build_xt(zone: str, command: str, payload: Dict[str, Any], request_id: int = 1) -> str:
|
|
24
|
+
"""
|
|
25
|
+
Build an XT (Extended) packet string.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
zone: Game zone (e.g., "EmpireEx_21")
|
|
29
|
+
command: Command ID (e.g., "att", "tra", "bui")
|
|
30
|
+
payload: Dictionary payload to JSON encode
|
|
31
|
+
request_id: Request ID (default 1)
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Formatted XT packet string
|
|
35
|
+
"""
|
|
36
|
+
return f"%xt%{zone}%{command}%{request_id}%{json.dumps(payload)}%"
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def from_bytes(cls, data: bytes) -> "Packet":
|
|
40
|
+
decoded = data.decode("utf-8").rstrip("\x00")
|
|
41
|
+
if not decoded:
|
|
42
|
+
raise ValueError("Empty packet")
|
|
43
|
+
|
|
44
|
+
if decoded.startswith("<"):
|
|
45
|
+
return cls._parse_xml(decoded)
|
|
46
|
+
elif decoded.startswith("%xt%"):
|
|
47
|
+
return cls._parse_xt(decoded)
|
|
48
|
+
|
|
49
|
+
# Unknown or junk, return raw wrapper
|
|
50
|
+
return cls(raw_data=decoded, is_xml=False)
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def _parse_xml(cls, data: str) -> "Packet":
|
|
54
|
+
try:
|
|
55
|
+
root = ET.fromstring(data)
|
|
56
|
+
# Structure: <msg t='sys'><body action='verChk' ...>
|
|
57
|
+
body = root.find("body")
|
|
58
|
+
cmd = body.get("action") if body is not None else None
|
|
59
|
+
|
|
60
|
+
# Fallback: Use root tag if no action (e.g. <cross-domain-policy>)
|
|
61
|
+
if cmd is None:
|
|
62
|
+
cmd = root.tag
|
|
63
|
+
|
|
64
|
+
return cls(raw_data=data, is_xml=True, command_id=cmd, payload=root)
|
|
65
|
+
except ET.ParseError:
|
|
66
|
+
return cls(raw_data=data, is_xml=True)
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def _parse_xt(cls, data: str) -> "Packet":
|
|
70
|
+
# Format: %xt%{Command}%{RequestId}%{Status}%{Payload}%
|
|
71
|
+
parts = data.split("%")
|
|
72
|
+
if len(parts) < 5:
|
|
73
|
+
return cls(raw_data=data, is_xml=False)
|
|
74
|
+
|
|
75
|
+
cmd = parts[2]
|
|
76
|
+
req_id = int(parts[3]) if parts[3].isdigit() else -1
|
|
77
|
+
|
|
78
|
+
error_code = 0
|
|
79
|
+
if parts[4].isdigit() or (parts[4].startswith("-") and parts[4][1:].isdigit()):
|
|
80
|
+
error_code = int(parts[4])
|
|
81
|
+
|
|
82
|
+
raw_payload = parts[5] if len(parts) > 5 else ""
|
|
83
|
+
|
|
84
|
+
# Optimization: Only parse JSON if it looks like JSON
|
|
85
|
+
payload_data = {}
|
|
86
|
+
if raw_payload.startswith("{") or raw_payload.startswith("["):
|
|
87
|
+
try:
|
|
88
|
+
payload_data = json.loads(raw_payload)
|
|
89
|
+
except json.JSONDecodeError:
|
|
90
|
+
payload_data = {"raw": raw_payload}
|
|
91
|
+
else:
|
|
92
|
+
payload_data = {"raw": raw_payload}
|
|
93
|
+
|
|
94
|
+
return cls(
|
|
95
|
+
raw_data=data,
|
|
96
|
+
is_xml=False,
|
|
97
|
+
command_id=cmd,
|
|
98
|
+
request_id=req_id,
|
|
99
|
+
error_code=error_code,
|
|
100
|
+
payload=payload_data,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def to_bytes(self) -> bytes:
|
|
104
|
+
return (self.raw_data + "\x00").encode("utf-8")
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Service layer for EmpireCore.
|
|
3
|
+
|
|
4
|
+
Services provide high-level APIs for different game domains (alliance, castle, etc.)
|
|
5
|
+
and are auto-registered with the EmpireClient.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
@register_service("alliance")
|
|
9
|
+
class AllianceService(BaseService):
|
|
10
|
+
def send_chat(self, message: str):
|
|
11
|
+
request = AllianceChatMessageRequest.create(message)
|
|
12
|
+
self.send(request)
|
|
13
|
+
|
|
14
|
+
# Client auto-discovers services:
|
|
15
|
+
client = EmpireClient(...)
|
|
16
|
+
client.alliance.send_chat("Hello!")
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# Import services to trigger registration
|
|
20
|
+
from .alliance import AllianceService
|
|
21
|
+
from .base import BaseService, get_registered_services, register_service
|
|
22
|
+
from .castle import CastleService
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"BaseService",
|
|
26
|
+
"register_service",
|
|
27
|
+
"get_registered_services",
|
|
28
|
+
# Services
|
|
29
|
+
"AllianceService",
|
|
30
|
+
"CastleService",
|
|
31
|
+
]
|