huge refactor
This commit is contained in:
parent
e5ebe05571
commit
a8341e8232
29 changed files with 3184 additions and 340 deletions
|
|
@ -49,7 +49,11 @@ public sealed class EntityReader
|
|||
var entityId = treeNode.Data.Key.EntityId;
|
||||
var path = TryReadEntityPath(entityPtr);
|
||||
|
||||
// Never process doodads — they are decorative and waste RPM calls
|
||||
if (IsDoodadPath(path)) return;
|
||||
|
||||
var entity = new Entity(entityPtr, entityId, path);
|
||||
entity.Type = ClassifyType(path);
|
||||
|
||||
if (registry["entities"].Register(entity.Metadata))
|
||||
dirty = true;
|
||||
|
|
@ -62,26 +66,24 @@ public sealed class EntityReader
|
|||
entity.Z = z;
|
||||
}
|
||||
|
||||
// Read component names for non-trivial entities
|
||||
if (hasComponentLookup &&
|
||||
entity.Type != EntityType.Effect &&
|
||||
entity.Type != EntityType.Terrain &&
|
||||
entity.Type != EntityType.Critter)
|
||||
// Read component names for non-trivial entities (skip effects, terrain, critters)
|
||||
if (hasComponentLookup && !IsLowPriorityPath(entity.Type))
|
||||
{
|
||||
var lookup = _components.ReadComponentLookup(entityPtr);
|
||||
if (lookup is not null)
|
||||
{
|
||||
entity.Components = new HashSet<string>(lookup.Keys);
|
||||
entity.ReclassifyFromComponents();
|
||||
ReclassifyFromComponents(entity);
|
||||
|
||||
if (registry["components"].Register(lookup.Keys))
|
||||
dirty = true;
|
||||
|
||||
// Read HP for monsters to determine alive/dead
|
||||
if (entity.Type == EntityType.Monster && lookup.TryGetValue("Life", out var lifeIdx))
|
||||
var (compFirst, compCount) = _components.FindComponentList(entityPtr);
|
||||
|
||||
// Read HP/Actor/Mods for monsters
|
||||
if (entity.Components.Contains("Monster"))
|
||||
{
|
||||
var (compFirst, compCount) = _components.FindComponentList(entityPtr);
|
||||
if (lifeIdx >= 0 && lifeIdx < compCount)
|
||||
if (lookup.TryGetValue("Life", out var lifeIdx) && lifeIdx >= 0 && lifeIdx < compCount)
|
||||
{
|
||||
var lifeComp = mem.ReadPointer(compFirst + lifeIdx * 8);
|
||||
if (lifeComp != 0)
|
||||
|
|
@ -96,6 +98,34 @@ public sealed class EntityReader
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Read Actor AnimationId — offset 0x370 confirmed from ExileCore2
|
||||
if (lookup.TryGetValue("Actor", out var actorIdx) && actorIdx >= 0 && actorIdx < compCount)
|
||||
{
|
||||
var actorComp = mem.ReadPointer(compFirst + actorIdx * 8);
|
||||
if (actorComp != 0)
|
||||
{
|
||||
var animId = mem.Read<int>(actorComp + ActorOffsets.AnimationId);
|
||||
entity.ActionId = (short)(animId & 0xFFFF);
|
||||
}
|
||||
}
|
||||
|
||||
// Read Mods — rarity + explicit mod names
|
||||
if (lookup.TryGetValue("Mods", out var modsIdx) && modsIdx >= 0 && modsIdx < compCount)
|
||||
{
|
||||
var modsComp = mem.ReadPointer(compFirst + modsIdx * 8);
|
||||
if (modsComp != 0)
|
||||
ReadEntityMods(entity, modsComp);
|
||||
}
|
||||
}
|
||||
|
||||
// Read AreaTransition destination name
|
||||
if (entity.Components.Contains("AreaTransition") &&
|
||||
lookup.TryGetValue("AreaTransition", out var atIdx) && atIdx >= 0 && atIdx < compCount)
|
||||
{
|
||||
var atComp = mem.ReadPointer(compFirst + atIdx * 8);
|
||||
if (atComp != 0)
|
||||
entity.TransitionName = ReadAreaTransitionName(atComp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -158,6 +188,93 @@ public sealed class EntityReader
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads area transition destination by scanning the AreaTransition component
|
||||
/// for pointer chains leading to a readable string. Returns the raw area ID
|
||||
/// (e.g. "G1_4"); display name resolution is done in the Data layer.
|
||||
/// </summary>
|
||||
private string? ReadAreaTransitionName(nint atComp)
|
||||
{
|
||||
var mem = _ctx.Memory;
|
||||
|
||||
var data = mem.ReadBytes(atComp, 0x80);
|
||||
if (data is null) return null;
|
||||
|
||||
for (var off = 0x10; off + 8 <= data.Length; off += 8)
|
||||
{
|
||||
var ptr = (nint)BitConverter.ToInt64(data, off);
|
||||
if (ptr == 0) continue;
|
||||
if (((ulong)ptr >> 32) is 0 or >= 0x7FFF) continue;
|
||||
|
||||
var raw = _strings.ReadNullTermWString(ptr);
|
||||
if (raw is not null && raw.Length >= 3)
|
||||
return raw;
|
||||
|
||||
var inner = mem.ReadPointer(ptr);
|
||||
if (inner != 0 && ((ulong)inner >> 32) is > 0 and < 0x7FFF)
|
||||
{
|
||||
raw = _strings.ReadNullTermWString(inner);
|
||||
if (raw is not null && raw.Length >= 3)
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads Mods component: rarity from ObjectMagicProperties, mod names from ExplicitMods vector.
|
||||
/// ModPtr in each mod entry → .dat row → first field is a wchar* mod identifier.
|
||||
/// </summary>
|
||||
private void ReadEntityMods(Entity entity, nint modsComp)
|
||||
{
|
||||
var mem = _ctx.Memory;
|
||||
|
||||
var mods = mem.Read<Mods>(modsComp);
|
||||
|
||||
// Read rarity from ObjectMagicProperties
|
||||
if (mods.ObjectMagicPropertiesPtr != 0 &&
|
||||
((ulong)mods.ObjectMagicPropertiesPtr >> 32) is > 0 and < 0x7FFF)
|
||||
{
|
||||
var props = mem.Read<ObjectMagicProperties>(mods.ObjectMagicPropertiesPtr);
|
||||
if (props.Rarity is >= 0 and <= 3)
|
||||
entity.Rarity = props.Rarity;
|
||||
}
|
||||
|
||||
// Read explicit mod names from AllModsPtr
|
||||
if (mods.AllModsPtr == 0 || ((ulong)mods.AllModsPtr >> 32) is 0 or >= 0x7FFF)
|
||||
return;
|
||||
|
||||
var allMods = mem.Read<AllModsType>(mods.AllModsPtr);
|
||||
var explicitCount = (int)allMods.ExplicitMods.TotalElements(16); // ModArrayStruct = 16 bytes
|
||||
if (explicitCount <= 0 || explicitCount > 20) return;
|
||||
|
||||
var modNames = new List<string>();
|
||||
for (var i = 0; i < explicitCount; i++)
|
||||
{
|
||||
var modEntry = mem.Read<ModArrayStruct>(allMods.ExplicitMods.First + i * 16);
|
||||
if (modEntry.ModPtr == 0) continue;
|
||||
if (((ulong)modEntry.ModPtr >> 32) is 0 or >= 0x7FFF) continue;
|
||||
|
||||
// Mods.dat row: first field is typically a wchar* or std::wstring key
|
||||
// Try reading as wchar* first (null-terminated UTF-16)
|
||||
var name = _strings.ReadNullTermWString(modEntry.ModPtr);
|
||||
if (name is not null)
|
||||
{
|
||||
modNames.Add(name);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try as std::wstring
|
||||
name = _strings.ReadMsvcWString(modEntry.ModPtr);
|
||||
if (name is not null)
|
||||
modNames.Add(name);
|
||||
}
|
||||
|
||||
if (modNames.Count > 0)
|
||||
entity.ModNames = modNames;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads entity path string via EntityDetailsPtr → std::wstring.
|
||||
/// </summary>
|
||||
|
|
@ -210,4 +327,93 @@ public sealed class EntityReader
|
|||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── Classification helpers (Memory-internal) ─────────────────────────
|
||||
|
||||
private static bool IsDoodadPath(string? path)
|
||||
{
|
||||
if (path is null) return false;
|
||||
return path.Contains("Doodad", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true for entity types that don't need component reading (effects, terrain, critters).
|
||||
/// </summary>
|
||||
private static bool IsLowPriorityPath(EntityType type)
|
||||
=> type is EntityType.Effect or EntityType.Terrain or EntityType.Critter;
|
||||
|
||||
/// <summary>
|
||||
/// Path-based entity type classification. Mirrors the logic previously in Entity.ClassifyType.
|
||||
/// </summary>
|
||||
private static EntityType ClassifyType(string? path)
|
||||
{
|
||||
if (path is null) return EntityType.Unknown;
|
||||
|
||||
var firstSlash = path.IndexOf('/');
|
||||
if (firstSlash < 0) return EntityType.Unknown;
|
||||
|
||||
var secondSlash = path.IndexOf('/', firstSlash + 1);
|
||||
var category = secondSlash > 0
|
||||
? path[(firstSlash + 1)..secondSlash]
|
||||
: path[(firstSlash + 1)..];
|
||||
|
||||
switch (category)
|
||||
{
|
||||
case "Characters":
|
||||
return EntityType.Player;
|
||||
|
||||
case "Monsters":
|
||||
if (path.Contains("/Critters/", StringComparison.OrdinalIgnoreCase))
|
||||
return EntityType.Critter;
|
||||
if (path.Contains("/NPC/", StringComparison.OrdinalIgnoreCase) ||
|
||||
path.Contains("/TownNPC/", StringComparison.OrdinalIgnoreCase))
|
||||
return EntityType.Npc;
|
||||
return EntityType.Monster;
|
||||
|
||||
case "NPC":
|
||||
return EntityType.Npc;
|
||||
|
||||
case "Effects":
|
||||
return EntityType.Effect;
|
||||
|
||||
case "MiscellaneousObjects":
|
||||
if (path.Contains("/Chest", StringComparison.OrdinalIgnoreCase) ||
|
||||
path.Contains("/Stash", StringComparison.OrdinalIgnoreCase))
|
||||
return EntityType.Chest;
|
||||
if (path.Contains("/Shrine", StringComparison.OrdinalIgnoreCase))
|
||||
return EntityType.Shrine;
|
||||
if (path.Contains("/Portal", StringComparison.OrdinalIgnoreCase))
|
||||
return EntityType.Portal;
|
||||
return EntityType.MiscellaneousObject;
|
||||
|
||||
case "Terrain":
|
||||
return EntityType.Terrain;
|
||||
|
||||
case "Items":
|
||||
return EntityType.WorldItem;
|
||||
|
||||
default:
|
||||
return EntityType.Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reclassify entity type using component names (called after components are read).
|
||||
/// Component-based classification is more reliable than path-based.
|
||||
/// </summary>
|
||||
private static void ReclassifyFromComponents(Entity entity)
|
||||
{
|
||||
var components = entity.Components;
|
||||
if (components is null || components.Count == 0) return;
|
||||
|
||||
if (components.Contains("Monster")) { entity.Type = EntityType.Monster; return; }
|
||||
if (components.Contains("Chest")) { entity.Type = EntityType.Chest; return; }
|
||||
if (components.Contains("Shrine")) { entity.Type = EntityType.Shrine; return; }
|
||||
if (components.Contains("Waypoint")) { entity.Type = EntityType.Waypoint; return; }
|
||||
if (components.Contains("AreaTransition")) { entity.Type = EntityType.AreaTransition; return; }
|
||||
if (components.Contains("Portal")) { entity.Type = EntityType.Portal; return; }
|
||||
if (components.Contains("TownPortal")) { entity.Type = EntityType.TownPortal; return; }
|
||||
if (components.Contains("NPC")) { entity.Type = EntityType.Npc; return; }
|
||||
if (components.Contains("Player")) { entity.Type = EntityType.Player; return; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue