-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathwow_object.py
More file actions
417 lines (348 loc) · 21.3 KB
/
wow_object.py
File metadata and controls
417 lines (348 loc) · 21.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
import offsets # Import offsets globally for constants
import time
import logging
import sys
from typing import Optional
import pymem
logger = logging.getLogger(__name__)
class WowObject:
"""Represents a generic World of Warcraft object (Player, NPC, Item, etc.)."""
# Object Types (Keep only Player and Unit)
TYPE_NONE = 0
TYPE_UNIT = 3 # NPCs, Mobs
TYPE_PLAYER = 4
# TYPE_GAMEOBJECT = 5 # Removed
# TYPE_DYNAMICOBJECT = 6 # Removed
# TYPE_CORPSE = 7 # Removed
# --- Class IDs and Power Types for 3.3.5a ---
CLASS_WARRIOR = 1
CLASS_PALADIN = 2
CLASS_HUNTER = 3
CLASS_ROGUE = 4
CLASS_PRIEST = 5
CLASS_DEATH_KNIGHT = 6
CLASS_SHAMAN = 7
CLASS_MAGE = 8
CLASS_WARLOCK = 9
CLASS_DRUID = 11
POWER_MANA = 0
POWER_RAGE = 1
POWER_FOCUS = 2 # Not primary in Wrath for players
POWER_ENERGY = 3
POWER_HAPPINESS = 4 # Pets
POWER_RUNES = 5 # DK resource (Not the main bar)
POWER_RUNIC_POWER = 6 # DK primary bar
# Unit Flags (from Wowhead/common sources for 3.3.5 - Check bits in UNIT_FIELD_FLAGS)
# Simplified relevant flags:
UNIT_FLAG_NONE = 0x00000000
UNIT_FLAG_SERVER_CONTROLLED = 0x00000001 # set only on server side controls
UNIT_FLAG_NON_ATTACKABLE = 0x00000002 # not attackable
UNIT_FLAG_DISABLE_MOVE = 0x00000004
UNIT_FLAG_PVP_ATTACKABLE = 0x00000008 # Subject to PvP rules
UNIT_FLAG_PREPARATION = 0x00000020 # Unit is preparing spell
UNIT_FLAG_OOC_NOT_ATTACKABLE = 0x00000100 # Makes unit unattackable while OOC
UNIT_FLAG_PASSIVE = 0x00000200 # Passive unit (won't aggro)
UNIT_FLAG_LOOTING = 0x00000400 # Player is Looting
UNIT_FLAG_PET_IN_COMBAT = 0x00000800 # Player Pet is in combat
UNIT_FLAG_PVP = 0x00001000 # Player flagged PvP
UNIT_FLAG_SILENCED = 0x00002000 # Unit is silenced
UNIT_FLAG_PACIFIED = 0x00020000 # Unit is pacified
UNIT_FLAG_STUNNED = 0x00040000 # Unit is stunned
UNIT_FLAG_IN_COMBAT = 0x00080000 # Unit is in combat (NOTE: Often unreliable for players!)
UNIT_FLAG_TAXI_FLIGHT = 0x00100000 # Unit is on taxi
UNIT_FLAG_DISARMED = 0x00200000 # Unit is disarmed
UNIT_FLAG_CONFUSED = 0x00400000
UNIT_FLAG_FLEEING = 0x00800000
UNIT_FLAG_PLAYER_CONTROLLED = 0x01000000 # Charmed or Possessed
UNIT_FLAG_NOT_SELECTABLE = 0x02000000
UNIT_FLAG_SKINNABLE = 0x04000000
UNIT_FLAG_MOUNT = 0x08000000 # Unit is mounted
UNIT_FLAG_SHEATHE = 0x40000000 # Unit weapons are sheathed
UNIT_FIELD_TARGET_GUID = 0x1C * 4
def __init__(self, base_address: int, mem_handler, local_player_guid: int = 0):
self.base_address = base_address
self.mem = mem_handler
self.local_player_guid = local_player_guid # Store the local player GUID if this is the local player
# --- Core properties read immediately ---
self.guid: int = 0
self.type: int = WowObject.TYPE_NONE
self.unit_fields_address: int = 0
self.descriptor_address: int = 0
self.target_guid: int = 0 # Read early if Unit/Player
# --- Properties updated dynamically or lazily ---
self.name: str = ""
self.x_pos: float = 0.0
self.y_pos: float = 0.0
self.z_pos: float = 0.0
self.rotation: float = 0.0
self.level: int = 0
self.health: int = 0
self.max_health: int = 0
self.energy: int = 0 # Current primary power
self.max_energy: int = 0 # Max primary power
self.power_type: int = -1 # Enum value (POWER_MANA, POWER_RAGE etc.)
self.unit_flags: int = 0 # Raw flags field
self.summoned_by_guid: int = 0
self.casting_spell_id: int = 0
self.channeling_spell_id: int = 0
self.is_dead: bool = False
self.last_update_time: float = 0.0 # Track last dynamic update
# Read initial essential data if base address is valid
if self.base_address and self.mem and self.mem.is_attached():
self._read_core_data()
def _read_core_data(self):
"""Reads the most essential data (GUID, Type, Field/Descriptor Ptrs, TargetGUID)."""
# import offsets # Usually not needed here if imported globally
self.guid = self.mem.read_ulonglong(self.base_address + offsets.OBJECT_GUID)
self.type = self.mem.read_short(self.base_address + offsets.OBJECT_TYPE) # Use read_short for 2 bytes
if self.type == WowObject.TYPE_UNIT or self.type == WowObject.TYPE_PLAYER:
unit_fields_ptr_addr = self.base_address + offsets.OBJECT_UNIT_FIELDS
self.unit_fields_address = self.mem.read_uint(unit_fields_ptr_addr)
descriptor_ptr_addr = self.base_address + offsets.OBJECT_DESCRIPTOR_OFFSET
self.descriptor_address = self.mem.read_uint(descriptor_ptr_addr)
# Read target GUID immediately if unit/player and fields ptr is valid
if self.unit_fields_address:
target_guid_addr = self.unit_fields_address + offsets.UNIT_FIELD_TARGET_GUID
self.target_guid = self.mem.read_ulonglong(target_guid_addr)
def update_dynamic_data(self, force_update=False):
"""Updates frequently changing data. Optional throttling."""
now = time.time()
# Throttle updates unless forced (e.g., reduce updates for non-target units)
# Add more sophisticated throttling later if needed
# if not force_update and now < self.last_update_time + 0.1: # Update max 10 times/sec
# return
if not self.base_address or not self.mem or not self.mem.is_attached():
return
# import offsets # Local import
# --- Position and Rotation ---
self.x_pos = self.mem.read_float(self.base_address + offsets.OBJECT_POS_X)
self.y_pos = self.mem.read_float(self.base_address + offsets.OBJECT_POS_Y)
self.z_pos = self.mem.read_float(self.base_address + offsets.OBJECT_POS_Z)
self.rotation = self.mem.read_float(self.base_address + offsets.OBJECT_ROTATION)
# --- DEBUG LOG --- Check Position Read
# if self.type in [WowObject.TYPE_UNIT, WowObject.TYPE_PLAYER] and self.guid != self.local_player_guid: # Log only other units/players
# print(f"[DEBUG WOW_OBJ {self.guid:X}] Pos: ({self.x_pos:.1f}, {self.y_pos:.1f}, {self.z_pos:.1f}) from base {self.base_address:X}")
# --- Data primarily from Unit Fields (Check if pointer is valid!) ---
if self.unit_fields_address:
# --- Health and Level ---
self.health = self.mem.read_uint(self.unit_fields_address + offsets.UNIT_FIELD_HEALTH)
self.max_health = self.mem.read_uint(self.unit_fields_address + offsets.UNIT_FIELD_MAXHEALTH)
self.level = self.mem.read_uint(self.unit_fields_address + offsets.UNIT_FIELD_LEVEL)
# --- DEBUG LOG --- Check Health Read
# if self.type in [WowObject.TYPE_UNIT, WowObject.TYPE_PLAYER] and self.guid != self.local_player_guid:
# print(f"[DEBUG WOW_OBJ {self.guid:X}] Health: {self.health}/{self.max_health} from UnitFields {self.unit_fields_address:X}")
# --- Flags ---
self.unit_flags = self.mem.read_uint(self.unit_fields_address + offsets.UNIT_FIELD_FLAGS)
# --- Summoner ---
self.summoned_by_guid = self.mem.read_ulonglong(self.unit_fields_address + offsets.UNIT_FIELD_SUMMONEDBY)
# --- Target (might have changed) ---
self.target_guid = self.mem.read_ulonglong(self.unit_fields_address + offsets.UNIT_FIELD_TARGET_GUID)
# --- Power Reading (Needs Power Type first) ---
# Determine Power Type (Descriptor preferred)
current_power_type = -1
# Try reading power type from UNIT_FIELD_BYTES_0 (Byte 3) first - often reliable
bytes_0_addr = self.unit_fields_address + offsets.UNIT_FIELD_BYTES_0
bytes_0_val = self.mem.read_uint(bytes_0_addr)
current_power_type = (bytes_0_val >> 24) & 0xFF # 4th byte
if current_power_type > 10: # If invalid, try descriptor
current_power_type = -1 # Reset before trying descriptor
if self.descriptor_address:
power_type_addr = self.descriptor_address + offsets.UNIT_FIELD_POWER_TYPE_BYTE_FROM_DESCRIPTOR # Offset 0x47
current_power_type = self.mem.read_uchar(power_type_addr)
if current_power_type > 10: current_power_type = -1 # Sanity check descriptor result
# Fallback if descriptor fails or type invalid (This part is now less likely to be needed)
# if current_power_type == -1:
# # Already tried bytes_0 above, so this fallback is redundant
# pass
self.power_type = current_power_type
# Read Current and Max Power based on determined type
if self.power_type != -1:
# --- Current Power ---
# Reverting to original logic that used specific offsets per type
current_power_addr = 0
if self.power_type == WowObject.POWER_MANA: current_power_addr = self.unit_fields_address + (0x19 * 4) # UNIT_FIELD_POWER1 ?
elif self.power_type == WowObject.POWER_RAGE: current_power_addr = self.unit_fields_address + (0x19 * 4) # UNIT_FIELD_POWER1 ?
elif self.power_type == WowObject.POWER_FOCUS: current_power_addr = self.unit_fields_address + (0x1A * 4) # UF + 0x68 << UNTESTED
elif self.power_type == WowObject.POWER_ENERGY:
# User confirmation: Address UF + 0x70 (calculated MaxEnergy offset) shows current energy
current_power_addr = self.unit_fields_address + 0x70
# current_power_addr = self.unit_fields_address + 0x64 # Tried this - Incorrect
# current_power_addr = self.unit_fields_address + 0x58 # UF + 0x58 << IDA Offset - FAILED
# elif self.power_type == WowObject.POWER_HAPPINESS: current_power_addr = self.unit_fields_address + (0x1C * 4) # UNIT_FIELD_POWER4 ?
# Skip Runes (complex)
elif self.power_type == WowObject.POWER_RUNIC_POWER: current_power_addr = self.unit_fields_address + (0x1E * 4) # UF + 0x78 << UNTESTED
else: current_power_addr = self.unit_fields_address + (0x19 * 4) # Default to POWER1
read_value = 0 # Initialize before read
if current_power_addr:
# Try reading as bytes and converting manually (as per original logic)
raw_bytes = self.mem.read_bytes(current_power_addr, 4)
if raw_bytes and len(raw_bytes) == 4:
try:
read_value = int.from_bytes(raw_bytes, 'little')
except ValueError:
# print(f\"[WOW_OBJECT] Error converting bytes {raw_bytes.hex()} to int at {current_power_addr:X}\", \"ERROR\")
read_value = 0 # Ensure it's zero on conversion failure
else:
read_value = 0 # Ensure it's zero if read fails
else:
read_value = 0 # Ensure it's zero if address was not determined
self.energy = read_value # Assign whatever was read (or 0) to self.energy
# --- Max Power ---
# Using the original logic that was present
if self.power_type == WowObject.POWER_ENERGY:
max_power_addr = self.unit_fields_address + 0x6C
else: # Use the offset that worked for Max Mana
max_power_base_offset = 0x64
max_power_addr = self.unit_fields_address + max_power_base_offset + (self.power_type * 4)
self.max_energy = self.mem.read_uint(max_power_addr)
# --- Fallback for Max Energy (Keep this) ---
if self.power_type == WowObject.POWER_ENERGY and (self.max_energy <= 0 or self.max_energy > 150):
self.max_energy = 100
#print(\"DEBUG: Max Energy read failed or invalid, using fallback 100\", \"DEBUG\")
else: # Invalid or unhandled power type
self.energy = 0
self.max_energy = 0
# --- Casting/Channeling Info (from object base offsets) ---
# These seem more reliable based on common usage
self.casting_spell_id = self.mem.read_uint(self.base_address + offsets.OBJECT_CASTING_SPELL_ID)
self.channeling_spell_id = self.mem.read_uint(self.base_address + offsets.OBJECT_CHANNEL_SPELL_ID)
# --- Derived States ---
self.is_dead = (self.health <= 0) or self.has_flag(WowObject.UNIT_FLAG_SKINNABLE)
self.last_update_time = now # Record update time
# --- Property helpers for Flags ---
def has_flag(self, flag: int) -> bool:
"""Checks if the unit has a specific flag set."""
return bool(self.unit_flags & flag)
@property
def is_player(self) -> bool:
return self.type == WowObject.TYPE_PLAYER
@property
def is_unit(self) -> bool:
return self.type == WowObject.TYPE_UNIT
@property
def is_attackable(self) -> bool:
# Basic check: Not dead, has GUID, not non-attackable flag
# More complex checks involve faction, PvP status etc.
if self.is_dead or self.guid == 0: return False
if self.has_flag(WowObject.UNIT_FLAG_NON_ATTACKABLE): return False
if self.has_flag(WowObject.UNIT_FLAG_OOC_NOT_ATTACKABLE): # Check combat status needed here
# Need reliable IsInCombat check (Lua?)
return False # Assume not attackable if OOC flag is set for simplicity now
return True
# Add more flag properties as needed (is_stunned, is_silenced etc.)
@property
def is_stunned(self) -> bool:
return self.has_flag(WowObject.UNIT_FLAG_STUNNED)
@property
def is_casting(self) -> bool:
return self.casting_spell_id != 0
@property
def is_channeling(self) -> bool:
return self.channeling_spell_id != 0
def get_name(self) -> str:
"""Returns the object's name. Relies on ObjectManager to set it."""
return self.name if self.name else f"Obj_{self.type}@{hex(self.base_address)}"
def get_power_label(self) -> str:
"""Returns the string label for the object's primary power type."""
type_map = {
WowObject.POWER_MANA: "Mana", WowObject.POWER_RAGE: "Rage",
WowObject.POWER_FOCUS: "Focus", WowObject.POWER_ENERGY: "Energy",
WowObject.POWER_HAPPINESS: "Happiness", WowObject.POWER_RUNES: "Runes",
WowObject.POWER_RUNIC_POWER: "RunicPower"
}
return type_map.get(self.power_type, "Power")
def get_type_str(self) -> str:
"""Returns a human-readable string for the object's type."""
type_map = {
WowObject.TYPE_NONE: "None",
WowObject.TYPE_UNIT: "Unit",
WowObject.TYPE_PLAYER: "Player",
# WowObject.TYPE_GAMEOBJECT: "GameObject", # Removed
# WowObject.TYPE_DYNAMICOBJECT: "DynamicObj", # Removed
# WowObject.TYPE_CORPSE: "Corpse" # Removed
}
return type_map.get(self.type, "Unknown")
def __str__(self):
name_str = self.get_name()
type_map = {
WowObject.TYPE_NONE: "None",
WowObject.TYPE_UNIT: "Unit", WowObject.TYPE_PLAYER: "Player",
# WowObject.TYPE_GAMEOBJECT: "GameObject", # Removed
# WowObject.TYPE_DYNAMICOBJECT: "DynObj", # Removed
# WowObject.TYPE_CORPSE: "Corpse", # Removed
}
obj_type_str = type_map.get(self.type, f"Type{self.type}")
guid_hex = f"0x{self.guid:X}"
details = f"<{obj_type_str} '{name_str}' GUID:{guid_hex}"
if self.is_unit or self.is_player:
status = "Dead" if self.is_dead else ("Casting" if self.is_casting else ("Channeling" if self.is_channeling else "Alive"))
details += f", Lvl:{self.level}, HP:{self.health}/{self.max_health}"
power_label = self.get_power_label()
max_display = self.max_energy if self.power_type != WowObject.POWER_ENERGY or self.max_energy > 0 else 100
if max_display > 0 or self.energy > 0: # Show power if relevant
details += f", {power_label}:{self.energy}/{max_display}"
details += f" ({status})"
if self.target_guid != 0: details += f" Target:0x{self.target_guid:X}"
# Add flags if useful f" Flags:{hex(self.unit_flags)}"
details += f", Pos:({self.x_pos:.1f},{self.y_pos:.1f},{self.z_pos:.1f})"
details += ">"
return details
def __repr__(self):
# Provide a concise representation, useful for debugging collections
return f"WowObject(GUID=0x{self.guid:X}, Base=0x{self.base_address:X}, Type={self.type})"
def has_aura_by_id(self, spell_id_to_find: int) -> bool:
"""
Checks if this object has an aura with the specified spell ID by reading memory directly.
Uses the logic derived from the 3.3.5a client's internal functions/structures.
Corrected logic based on disassembly analysis for Table 1 vs Table 2.
"""
# print(f"[AuraCheck DEBUG {self.guid:X}] Checking for SpellID {spell_id_to_find}", file=sys.stderr) # DEBUG START - Keep if needed
if not self.base_address or not self.mem or not self.mem.is_attached() or spell_id_to_find <= 0:
# print(f"[AuraCheck DEBUG {self.guid:X}] Pre-check failed (Base: {self.base_address:X}, Mem: {self.mem is not None}, Attached: {self.mem.is_attached() if self.mem else False}, SpellID: {spell_id_to_find})", file=sys.stderr) # DEBUG
return False
aura_count = 0
aura_table_base_addr = 0 # Corrected variable name for clarity
max_auras_sanity_check = 100 # Reasonable upper limit for auras
try:
# Determine which aura count and table to use based on AURA_COUNT_1
count1_addr = self.base_address + offsets.AURA_COUNT_1_OFFSET
count1 = self.mem.read_uint(count1_addr)
# print(f"[AuraCheck DEBUG {self.guid:X}] Read Count1 from {count1_addr:X}: {count1}", file=sys.stderr) # DEBUG
if count1 == 0xFFFFFFFF:
# Use Table 2 / Count 2 - Logic is pointer-based
count2_addr = self.base_address + offsets.AURA_COUNT_2_OFFSET
table2_ptr_addr = self.base_address + offsets.AURA_TABLE_2_OFFSET
aura_count = self.mem.read_uint(count2_addr)
aura_table_base_addr = self.mem.read_uint(table2_ptr_addr) # Read the pointer
# print(f"[AuraCheck DEBUG {self.guid:X}] Using Table 2. Count={aura_count} from {count2_addr:X}, TableAddr={aura_table_base_addr:X} from {table2_ptr_addr:X}", file=sys.stderr) # DEBUG
else:
# Use Table 1 / Count 1 - Logic is direct offset-based
aura_count = count1
# The base address of the table *is* UnitBase + AURA_TABLE_1_OFFSET
aura_table_base_addr = self.base_address + offsets.AURA_TABLE_1_OFFSET
# print(f"[AuraCheck DEBUG {self.guid:X}] Using Table 1. Count={aura_count}, TableAddr={aura_table_base_addr:X} (Direct Offset)", file=sys.stderr) # DEBUG
# Validate count and pointer/address
if aura_table_base_addr == 0 or aura_count <= 0 or aura_count > max_auras_sanity_check:
# print(f"[AuraCheck DEBUG {self.guid:X}] Validation Failed (Addr: {aura_table_base_addr:X}, Count: {aura_count})", file=sys.stderr) # DEBUG
return False # No auras or invalid data
# Iterate through the aura table
# print(f"[AuraCheck DEBUG {self.guid:X}] Iterating {aura_count} auras from table base {aura_table_base_addr:X}...", file=sys.stderr) # DEBUG
for i in range(aura_count):
aura_struct_addr = aura_table_base_addr + i * offsets.AURA_STRUCT_SIZE
spell_id_offset_addr = aura_struct_addr + offsets.AURA_STRUCT_SPELL_ID_OFFSET
# Read the Spell ID from the aura structure
current_spell_id = self.mem.read_uint(spell_id_offset_addr)
# Optional: Print details for debugging specific spells
# if i < 5 or current_spell_id == spell_id_to_find:
# print(f"[AuraCheck DEBUG {self.guid:X}] Index {i}: Struct@{aura_struct_addr:X}, SpellID@{spell_id_offset_addr:X} -> Found SpellID: {current_spell_id}", file=sys.stderr) # DEBUG
if current_spell_id == spell_id_to_find:
# print(f"[AuraCheck DEBUG {self.guid:X}] Found matching SpellID {spell_id_to_find} at index {i}", file=sys.stderr) # DEBUG FOUND
return True # Found the aura
except pymem.exception.MemoryReadError as e:
# print(f"[AuraCheck ERROR {self.guid:X}] MemoryReadError: {e}", file=sys.stderr) # DEBUG ERROR
return False
except Exception as e:
# print(f"[AuraCheck ERROR {self.guid:X}] Unexpected Error: {e}", file=sys.stderr) # DEBUG ERROR
return False
# print(f"[AuraCheck DEBUG {self.guid:X}] SpellID {spell_id_to_find} not found after checking {aura_count} auras.", file=sys.stderr) # DEBUG NOT FOUND
return False # Aura not found