using WhiteMagic.Native; using System.Runtime.InteropServices; using System.Text; namespace WhiteMagic; /// /// Abstract base for all memory-access readers and writers. Provides typed /// /, array IO, string IO, and /// relative/absolute addressing. Subclasses implement the concrete /// and methods. /// public abstract class MemoryBase : IDisposable { /// The base address of the target process's main module. public abstract IntPtr ImageBase { get; } /// The native handle to the target process. public abstract SafeMemoryHandle Handle { get; } // ── Raw byte IO ──────────────────────────────────────────────────────── /// Reads a sequence of bytes from the target address. public abstract byte[] ReadBytes(IntPtr address, int count, bool isRelative = false); /// Writes a sequence of bytes to the target address. /// The number of bytes written. public abstract int WriteBytes(IntPtr address, ReadOnlySpan bytes, bool isRelative = false); // ── Typed IO ─────────────────────────────────────────────────────────── /// Reads a value of type from the target address. public T Read(IntPtr address, bool isRelative = false) where T : struct { if (isRelative) address = GetAbsolute(address); int size = MarshalCache.Size; byte[] raw = ReadBytes(address, size); if (MarshalCache.TypeRequiresMarshal) { return MarshalByteArrayToStructure(raw); } return MemoryMarshal.Read(raw.AsSpan()); } /// Writes a value of type to the target address. /// if all bytes were written. public bool Write(IntPtr address, T value, bool isRelative = false) where T : struct { if (isRelative) address = GetAbsolute(address); int size = MarshalCache.Size; byte[] raw; if (MarshalCache.TypeRequiresMarshal) { raw = StructureToByteArray(value, size); } else { raw = new byte[size]; MemoryMarshal.Write(raw.AsSpan(), in value); } int written = WriteBytes(address, raw, false); return written == size; } /// Reads an array of values of type from the target address. public T[] Read(IntPtr address, int count, bool isRelative = false) where T : struct { if (isRelative) address = GetAbsolute(address); int elementSize = MarshalCache.Size; int totalSize = elementSize * count; byte[] raw = ReadBytes(address, totalSize); var result = new T[count]; if (MarshalCache.TypeRequiresMarshal) { for (int i = 0; i < count; i++) { var elementBytes = new ReadOnlySpan(raw, i * elementSize, elementSize); result[i] = MarshalByteArrayToStructure(elementBytes.ToArray()); } } else { ReadOnlySpan span = raw; for (int i = 0; i < count; i++) { result[i] = MemoryMarshal.Read(span.Slice(i * elementSize, elementSize)); } } return result; } /// Writes an array of values of type to the target address. /// if all bytes were written. public bool Write(IntPtr address, T[] values, bool isRelative = false) where T : struct { if (isRelative) address = GetAbsolute(address); if (values is null || values.Length == 0) return true; int elementSize = MarshalCache.Size; int totalSize = elementSize * values.Length; byte[] raw = new byte[totalSize]; Span span = raw; for (int i = 0; i < values.Length; i++) { Span slice = span.Slice(i * elementSize, elementSize); if (MarshalCache.TypeRequiresMarshal) { StructureToByteArray(values[i], slice, elementSize); } else { MemoryMarshal.Write(slice, in values[i]); } } int written = WriteBytes(address, raw, false); return written == totalSize; } // ── String IO ────────────────────────────────────────────────────────── /// Reads a null-terminated string from the target address. public virtual string ReadString(IntPtr address, Encoding encoding, int maxLength = 512, bool relative = false) { byte[] buffer = ReadBytes(address, maxLength, relative); string decoded = encoding.GetString(buffer); int nullIndex = decoded.IndexOf('\0'); if (nullIndex >= 0) { return decoded[..nullIndex]; } return decoded; } /// Writes a null-terminated string to the target address. public virtual bool WriteString(IntPtr address, string value, Encoding encoding, bool relative = false) { if (value.Length == 0 || value[^1] != '\0') value += '\0'; byte[] bytes = encoding.GetBytes(value); int written = WriteBytes(address, bytes, relative); return written == bytes.Length; } // ── Addressing ───────────────────────────────────────────────────────── /// Converts a relative offset to an absolute address relative to . public IntPtr GetAbsolute(IntPtr relative) { return ImageBase + (nint)relative; } /// Converts an absolute address to a relative offset from . public IntPtr GetRelative(IntPtr absolute) { return (IntPtr)((nint)ImageBase - (nint)absolute); } // ── Lifecycle ────────────────────────────────────────────────────────── /// public virtual void Dispose() { Handle?.Dispose(); } // ── Private helpers ──────────────────────────────────────────────────── private static T MarshalByteArrayToStructure(byte[] bytes) where T : struct { GCHandle pin = GCHandle.Alloc(bytes, GCHandleType.Pinned); try { return Marshal.PtrToStructure(pin.AddrOfPinnedObject()); } finally { pin.Free(); } } private static byte[] StructureToByteArray(T value, int size) where T : struct { byte[] bytes = new byte[size]; GCHandle pin = GCHandle.Alloc(bytes, GCHandleType.Pinned); try { Marshal.StructureToPtr(value, pin.AddrOfPinnedObject(), false); } finally { pin.Free(); } return bytes; } private static void StructureToByteArray(T value, Span destination, int size) where T : struct { byte[] temp = destination.ToArray(); GCHandle pin = GCHandle.Alloc(temp, GCHandleType.Pinned); try { Marshal.StructureToPtr(value, pin.AddrOfPinnedObject(), false); temp.CopyTo(destination); } finally { pin.Free(); } } }