huge refactor

This commit is contained in:
Boki 2026-03-02 23:45:12 -05:00
parent e5ebe05571
commit a8341e8232
29 changed files with 3184 additions and 340 deletions

View file

@ -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; }
}
}