diff --git a/src/Roboto.Memory/ComponentReader.cs b/src/Roboto.Memory/ComponentReader.cs index 4303895..e84018d 100644 --- a/src/Roboto.Memory/ComponentReader.cs +++ b/src/Roboto.Memory/ComponentReader.cs @@ -1,4 +1,6 @@ using System.Text; +using Roboto.GameOffsets.Components; +using Roboto.GameOffsets.Natives; using Serilog; namespace Roboto.Memory; @@ -146,19 +148,21 @@ public sealed class ComponentReader if (high == 0 || high >= 0x7FFF) continue; if ((compPtr & 0x3) != 0) continue; - var hpTotal = mem.Read(compPtr + offsets.LifeHealthOffset + offsets.VitalTotalOffset); + var life = mem.Read(compPtr); + + var hpTotal = life.Health.Total; if (hpTotal < 20 || hpTotal > 200000) continue; - var hpCurrent = mem.Read(compPtr + offsets.LifeHealthOffset + offsets.VitalCurrentOffset); + var hpCurrent = life.Health.Current; if (hpCurrent < 0 || hpCurrent > hpTotal + 1000) continue; - var manaTotal = mem.Read(compPtr + offsets.LifeManaOffset + offsets.VitalTotalOffset); + var manaTotal = life.Mana.Total; if (manaTotal < 0 || manaTotal > 200000) continue; - var manaCurrent = mem.Read(compPtr + offsets.LifeManaOffset + offsets.VitalCurrentOffset); + var manaCurrent = life.Mana.Current; if (manaCurrent < 0 || manaCurrent > manaTotal + 1000) continue; - var esTotal = mem.Read(compPtr + offsets.LifeEsOffset + offsets.VitalTotalOffset); + var esTotal = life.EnergyShield.Total; if (manaTotal == 0 && esTotal == 0) continue; _cachedLifeIndex = i; @@ -174,15 +178,14 @@ public sealed class ComponentReader /// public bool TryReadVitals(GameStateSnapshot snap, nint lifeComp) { - var mem = _ctx.Memory; - var offsets = _ctx.Offsets; + var life = _ctx.Memory.Read(lifeComp); - var hp = mem.Read(lifeComp + offsets.LifeHealthOffset + offsets.VitalCurrentOffset); - var hpMax = mem.Read(lifeComp + offsets.LifeHealthOffset + offsets.VitalTotalOffset); - var mana = mem.Read(lifeComp + offsets.LifeManaOffset + offsets.VitalCurrentOffset); - var manaMax = mem.Read(lifeComp + offsets.LifeManaOffset + offsets.VitalTotalOffset); - var es = mem.Read(lifeComp + offsets.LifeEsOffset + offsets.VitalCurrentOffset); - var esMax = mem.Read(lifeComp + offsets.LifeEsOffset + offsets.VitalTotalOffset); + var hp = life.Health.Current; + var hpMax = life.Health.Total; + var mana = life.Mana.Current; + var manaMax = life.Mana.Total; + var es = life.EnergyShield.Current; + var esMax = life.EnergyShield.Total; if (hpMax <= 0 || hpMax > 200000 || hp < 0 || hp > hpMax + 1000) return false; if (manaMax < 0 || manaMax > 200000 || mana < 0) return false; @@ -256,22 +259,17 @@ public sealed class ComponentReader /// public bool TryReadPosition(GameStateSnapshot snap, nint renderComp) { - var mem = _ctx.Memory; - var offsets = _ctx.Offsets; + var pos = _ctx.Memory.Read>(renderComp + _ctx.Offsets.PositionXOffset); - var x = mem.Read(renderComp + offsets.PositionXOffset); - var y = mem.Read(renderComp + offsets.PositionYOffset); - var z = mem.Read(renderComp + offsets.PositionZOffset); - - if (float.IsNaN(x) || float.IsNaN(y) || float.IsNaN(z)) return false; - if (float.IsInfinity(x) || float.IsInfinity(y) || float.IsInfinity(z)) return false; - if (x < 50 || x > 50000 || y < 50 || y > 50000) return false; - if (MathF.Abs(z) > 5000) return false; + if (float.IsNaN(pos.X) || float.IsNaN(pos.Y) || float.IsNaN(pos.Z)) return false; + if (float.IsInfinity(pos.X) || float.IsInfinity(pos.Y) || float.IsInfinity(pos.Z)) return false; + if (pos.X < 50 || pos.X > 50000 || pos.Y < 50 || pos.Y > 50000) return false; + if (MathF.Abs(pos.Z) > 5000) return false; snap.HasPosition = true; - snap.PlayerX = x; - snap.PlayerY = y; - snap.PlayerZ = z; + snap.PlayerX = pos.X; + snap.PlayerY = pos.Y; + snap.PlayerZ = pos.Z; CachedRenderComponentAddr = renderComp; return true; } @@ -281,12 +279,10 @@ public sealed class ComponentReader /// public bool TryReadPositionRaw(nint comp, out float x, out float y, out float z) { - var mem = _ctx.Memory; - var offsets = _ctx.Offsets; - - x = mem.Read(comp + offsets.PositionXOffset); - y = mem.Read(comp + offsets.PositionYOffset); - z = mem.Read(comp + offsets.PositionZOffset); + var pos = _ctx.Memory.Read>(comp + _ctx.Offsets.PositionXOffset); + x = pos.X; + y = pos.Y; + z = pos.Z; if (float.IsNaN(x) || float.IsNaN(y) || float.IsNaN(z)) return false; if (float.IsInfinity(x) || float.IsInfinity(y) || float.IsInfinity(z)) return false; diff --git a/src/Roboto.Memory/EntityReader.cs b/src/Roboto.Memory/EntityReader.cs index 3cccf1c..eeb6fcf 100644 --- a/src/Roboto.Memory/EntityReader.cs +++ b/src/Roboto.Memory/EntityReader.cs @@ -1,3 +1,5 @@ +using Roboto.GameOffsets.Components; +using Roboto.GameOffsets.Entities; using Serilog; namespace Roboto.Memory; @@ -36,15 +38,15 @@ public sealed class EntityReader var hasComponentLookup = offsets.ComponentLookupEntrySize > 0; var dirty = false; - WalkTreeInOrder(sentinel, root, maxNodes, node => + WalkTreeInOrder(sentinel, root, maxNodes, (_, treeNode) => { - var entityPtr = mem.ReadPointer(node + offsets.EntityNodeValueOffset); + var entityPtr = treeNode.Data.EntityPtr; if (entityPtr == 0) return; var high = (ulong)entityPtr >> 32; if (high == 0 || high >= 0x7FFF || (entityPtr & 0x3) != 0) return; - var entityId = mem.Read(entityPtr + offsets.EntityIdOffset); + var entityId = treeNode.Data.Key.EntityId; var path = TryReadEntityPath(entityPtr); var entity = new Entity(entityPtr, entityId, path); @@ -84,13 +86,13 @@ public sealed class EntityReader var lifeComp = mem.ReadPointer(compFirst + lifeIdx * 8); if (lifeComp != 0) { - var hp = mem.Read(lifeComp + offsets.LifeHealthOffset + offsets.VitalCurrentOffset); - var hpMax = mem.Read(lifeComp + offsets.LifeHealthOffset + offsets.VitalTotalOffset); - if (hpMax > 0 && hpMax < 200000 && hp >= 0 && hp <= hpMax + 1000) + var life = mem.Read(lifeComp); + if (life.Health.Total > 0 && life.Health.Total < 200000 && + life.Health.Current >= 0 && life.Health.Current <= life.Health.Total + 1000) { entity.HasVitals = true; - entity.LifeCurrent = hp; - entity.LifeTotal = hpMax; + entity.LifeCurrent = life.Health.Current; + entity.LifeTotal = life.Health.Total; } } } @@ -109,14 +111,24 @@ public sealed class EntityReader /// /// Iterative in-order traversal of an MSVC std::map red-black tree. + /// Backward-compatible overload — delegates to the struct-based version. /// public void WalkTreeInOrder(nint sentinel, nint root, int maxNodes, Action visitor) + { + WalkTreeInOrder(sentinel, root, maxNodes, (addr, _) => visitor(addr)); + } + + /// + /// Iterative in-order traversal using Read<EntityTreeNode> — 1 kernel call per node + /// instead of separate Left/Right reads. The visitor receives both the node address + /// and the parsed struct data. + /// + public void WalkTreeInOrder(nint sentinel, nint root, int maxNodes, Action visitor) { if (root == 0 || root == sentinel) return; - var offsets = _ctx.Offsets; var mem = _ctx.Memory; - var stack = new Stack(); + var stack = new Stack<(nint Addr, EntityTreeNode Node)>(); var current = root; var count = 0; var visited = new HashSet { sentinel }; @@ -130,18 +142,19 @@ public sealed class EntityReader current = sentinel; break; } - stack.Push(current); - current = mem.ReadPointer(current + offsets.EntityNodeLeftOffset); + var node = mem.Read(current); + stack.Push((current, node)); + current = node.Left; } if (stack.Count == 0) break; - current = stack.Pop(); - visitor(current); + var (nodeAddr, treeNode) = stack.Pop(); + visitor(nodeAddr, treeNode); count++; if (count >= maxNodes) break; - current = mem.ReadPointer(current + offsets.EntityNodeRightOffset); + current = treeNode.Right; } } diff --git a/src/Roboto.Memory/GameMemoryReader.cs b/src/Roboto.Memory/GameMemoryReader.cs index 179c7de..307fef8 100644 --- a/src/Roboto.Memory/GameMemoryReader.cs +++ b/src/Roboto.Memory/GameMemoryReader.cs @@ -258,15 +258,8 @@ public class GameMemoryReader : IDisposable // Cache the resolved address for fast per-frame reads _cachedCameraMatrixAddr = matrixAddr; - // Read 64-byte Matrix4x4 as 16 floats - var bytes = mem.ReadBytes(matrixAddr, 64); - if (bytes is null || bytes.Length < 64) return; - - var m = new Matrix4x4( - BitConverter.ToSingle(bytes, 0), BitConverter.ToSingle(bytes, 4), BitConverter.ToSingle(bytes, 8), BitConverter.ToSingle(bytes, 12), - BitConverter.ToSingle(bytes, 16), BitConverter.ToSingle(bytes, 20), BitConverter.ToSingle(bytes, 24), BitConverter.ToSingle(bytes, 28), - BitConverter.ToSingle(bytes, 32), BitConverter.ToSingle(bytes, 36), BitConverter.ToSingle(bytes, 40), BitConverter.ToSingle(bytes, 44), - BitConverter.ToSingle(bytes, 48), BitConverter.ToSingle(bytes, 52), BitConverter.ToSingle(bytes, 56), BitConverter.ToSingle(bytes, 60)); + // Read 64-byte Matrix4x4 as a single struct (System.Numerics.Matrix4x4 is already unmanaged/sequential) + var m = mem.Read(matrixAddr); // Quick sanity check if (float.IsNaN(m.M11) || float.IsInfinity(m.M11)) return; diff --git a/src/Roboto.Memory/TerrainReader.cs b/src/Roboto.Memory/TerrainReader.cs index 0d30b20..36eac67 100644 --- a/src/Roboto.Memory/TerrainReader.cs +++ b/src/Roboto.Memory/TerrainReader.cs @@ -1,4 +1,6 @@ +using Roboto.GameOffsets.States; using Serilog; +using Terrain = Roboto.GameOffsets.States.Terrain; namespace Roboto.Memory; @@ -64,9 +66,12 @@ public sealed class TerrainReader } // Inline mode: TerrainStruct is inline at AreaInstance + TerrainListOffset + // Single Read (0x1B0 = 432 bytes) replaces 5 individual reads var terrainBase = areaInstance + offsets.TerrainListOffset; - var cols = (int)mem.Read(terrainBase + offsets.TerrainDimensionsOffset); - var rows = (int)mem.Read(terrainBase + offsets.TerrainDimensionsOffset + 8); + var t = mem.Read(terrainBase); + + var cols = (int)t.Dimensions.X; + var rows = (int)t.Dimensions.Y; if (cols <= 0 || cols >= 1000 || rows <= 0 || rows >= 1000) return; @@ -99,10 +104,9 @@ public sealed class TerrainReader return; } - // Read GridWalkableData StdVector (begin/end/cap pointers) - var gridVecOffset = offsets.TerrainWalkableGridOffset; - var gridBegin = mem.ReadPointer(terrainBase + gridVecOffset); - var gridEnd = mem.ReadPointer(terrainBase + gridVecOffset + 8); + // Grid vector pointers already available from the Terrain struct read + var gridBegin = t.WalkableGrid.First; + var gridEnd = t.WalkableGrid.Last; if (gridBegin == 0 || gridEnd <= gridBegin) return; @@ -110,7 +114,7 @@ public sealed class TerrainReader if (gridDataSize <= 0 || gridDataSize > 16 * 1024 * 1024) return; - var bytesPerRow = mem.Read(terrainBase + offsets.TerrainBytesPerRowOffset); + var bytesPerRow = t.BytesPerRow; if (bytesPerRow <= 0 || bytesPerRow > 0x10000) return;