lots working good, minimap / rotation / follow / entities

This commit is contained in:
Boki 2026-03-01 21:29:44 -05:00
parent 69a8eaea62
commit 1ba7c39c30
11 changed files with 2496 additions and 99 deletions

View file

@ -0,0 +1,201 @@
namespace Automata.Memory;
public enum EntityType
{
Unknown,
Player,
Monster,
Npc,
Effect,
WorldItem,
MiscellaneousObject,
Terrain,
Critter,
Chest,
Shrine,
Portal,
TownPortal,
Waypoint,
AreaTransition,
Door,
}
public enum MonsterRarity
{
White,
Magic,
Rare,
Unique,
}
public class Entity
{
public nint Address { get; }
public uint Id { get; }
public string? Path { get; }
public string? Metadata { get; }
// Position (from Render component)
public bool HasPosition { get; internal set; }
public float X { get; internal set; }
public float Y { get; internal set; }
public float Z { get; internal set; }
// Vitals (from Life component — only populated when explicitly read)
public bool HasVitals { get; internal set; }
public int LifeCurrent { get; internal set; }
public int LifeTotal { get; internal set; }
public int ManaCurrent { get; internal set; }
public int ManaTotal { get; internal set; }
public int EsCurrent { get; internal set; }
public int EsTotal { get; internal set; }
// Component info
public int ComponentCount { get; internal set; }
public HashSet<string>? Components { get; internal set; }
// Component-based properties (populated by GameMemoryReader)
public bool IsTargetable { get; internal set; }
public bool IsOpened { get; internal set; }
public bool IsAvailable { get; internal set; }
public MonsterRarity Rarity { get; internal set; }
// Derived properties
public bool IsAlive => HasVitals && LifeCurrent > 0;
public bool IsDead => HasVitals && LifeCurrent <= 0;
public bool IsHostile => Type == EntityType.Monster;
public bool IsNpc => Type == EntityType.Npc;
public bool IsPlayer => Type == EntityType.Player;
public bool HasComponent(string name) => Components?.Contains(name) == true;
/// <summary>
/// Grid-plane distance to another point (ignores Z).
/// </summary>
public float DistanceTo(float px, float py)
{
var dx = X - px;
var dy = Y - py;
return MathF.Sqrt(dx * dx + dy * dy);
}
/// <summary>
/// Grid-plane distance to another entity.
/// </summary>
public float DistanceTo(Entity other) => DistanceTo(other.X, other.Y);
/// <summary>
/// Short category string derived from path (e.g. "Monsters", "Effects", "NPC").
/// </summary>
public string Category
{
get
{
if (Path is null) return "?";
var parts = Path.Split('/');
return parts.Length >= 2 ? parts[1] : "?";
}
}
public EntityType Type { get; internal set; }
internal Entity(nint address, uint id, string? path)
{
Address = address;
Id = id;
Path = path;
Metadata = ExtractMetadata(path);
Type = ClassifyType(path);
}
/// <summary>
/// Reclassify entity type using component names (called after components are read).
/// Component-based classification is more reliable than path-based.
/// </summary>
internal void ReclassifyFromComponents()
{
if (Components is null || Components.Count == 0) return;
// Priority order matching ExileCore's ParseType logic
if (Components.Contains("Monster")) { Type = EntityType.Monster; return; }
if (Components.Contains("Chest")) { Type = EntityType.Chest; return; }
if (Components.Contains("Shrine")) { Type = EntityType.Shrine; return; }
if (Components.Contains("Waypoint")) { Type = EntityType.Waypoint; return; }
if (Components.Contains("AreaTransition")) { Type = EntityType.AreaTransition; return; }
if (Components.Contains("Portal")) { Type = EntityType.Portal; return; }
if (Components.Contains("TownPortal")) { Type = EntityType.TownPortal; return; }
if (Components.Contains("NPC")) { Type = EntityType.Npc; return; }
if (Components.Contains("Player")) { Type = EntityType.Player; return; }
// Don't override path-based classification for Effects/Terrain/etc.
}
/// <summary>
/// Strips the "@N" instance suffix from the path.
/// "Metadata/Monsters/Wolves/RottenWolf1_@2" → "Metadata/Monsters/Wolves/RottenWolf1_"
/// </summary>
private static string? ExtractMetadata(string? path)
{
if (path is null) return null;
var atIndex = path.LastIndexOf('@');
return atIndex > 0 ? path[..atIndex] : path;
}
private static EntityType ClassifyType(string? path)
{
if (path is null) return EntityType.Unknown;
// Check second path segment: "Metadata/<Category>/..."
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":
// Sub-classify: some "monsters" are actually NPCs or critters
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;
}
}
public override string ToString()
{
var pos = HasPosition ? $"({X:F0},{Y:F0})" : "no pos";
return $"[{Id}] {Type} {Path ?? "?"} {pos}";
}
}