diff --git a/GameMemory.cs b/GameMemory.cs new file mode 100644 index 0000000..adc6bf9 --- /dev/null +++ b/GameMemory.cs @@ -0,0 +1,345 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace LiveSplit.HaloSplit +{ + class GameMemory + { + public delegate void MapChangedEventHandler(object sender, string map); + public event MapChangedEventHandler OnMapChanged; + public event EventHandler OnGainControl; + public event EventHandler OnLostControl; + public event EventHandler OnReset; + + private Task _thread; + private CancellationTokenSource _cancelSource; + + private DeepPointer _currentMapPtr; + private DeepPointer _playerPosPtr; + private DeepPointer _playerFrozenPtr; + private DeepPointer _difficultyPtr; + + public GameMemory() + { + _difficultyPtr = new DeepPointer(0x290354); + } + + public void StartReading() + { + if (_thread != null && _thread.Status == TaskStatus.Running) + throw new InvalidOperationException(); + + _cancelSource = new CancellationTokenSource(); + _thread = Task.Factory.StartNew(MemoryReadThread); + } + + public void Stop() + { + if (_cancelSource == null || _thread == null) + throw new InvalidOperationException(); + + if (_thread.Status != TaskStatus.Running) + return; + + _cancelSource.Cancel(); + _thread.Wait(); + } + + Process GetGameProcess() + { + Process gameProcess = Process.GetProcesses() + .FirstOrDefault(p => p.ProcessName.ToLower() == "halo" && !p.HasExited); + + if (gameProcess != null) + { + if (gameProcess.MainModule.FileVersionInfo.FileVersion == "01.00.00.0564") + { + _currentMapPtr = new DeepPointer(0x30B4B9); + _playerFrozenPtr = new DeepPointer(0x46B838, 0x11); + _playerPosPtr = new DeepPointer(0x21A508, 0x77c); + return gameProcess; + } + else if (gameProcess.MainModule.FileVersionInfo.FileVersion == "01.00.01.0580") + { + _currentMapPtr = new DeepPointer(0x30B6C1); + _playerFrozenPtr = new DeepPointer(0x46BA58, 0x11); + _playerPosPtr = new DeepPointer(0x21A278, 0x77c); + return gameProcess; + } + } + + return null; + } + + void MemoryReadThread() + { + while (!_cancelSource.IsCancellationRequested) + { + try + { + Process gameProcess; + while ((gameProcess = this.GetGameProcess()) == null) + { + Thread.Sleep(250); + if (_cancelSource.IsCancellationRequested) + return; + } + + string prevCurrentMap = String.Empty; + bool prevPlayerFrozen = false; + while (!gameProcess.HasExited) + { + string currentMap; + _currentMapPtr.Deref(gameProcess, out currentMap, 255); + if (currentMap != prevCurrentMap) + { + if (this.OnMapChanged != null) + this.OnMapChanged(this, currentMap); + } + + bool playerFrozen; + _playerFrozenPtr.Deref(gameProcess, out playerFrozen); + if (playerFrozen != prevPlayerFrozen) + { + if (!playerFrozen && currentMap == @"levels\a10\a10") + { + Vector3f pos; + _playerPosPtr.Deref(gameProcess, out pos); + + const int DIFFICULTY_LEGENDARY = 3; + int difficulty; + _difficultyPtr.Deref(gameProcess, out difficulty); + + // a bunch of hacks, but it works + var chamberPos = new Vector3f(-0.0989f, 0.436f, 0); + float dist = pos.DistanceXY(chamberPos); + if ((difficulty == DIFFICULTY_LEGENDARY && dist < 0.001) + || (difficulty != DIFFICULTY_LEGENDARY && dist > 1.0f && dist < 55.4f)) + { + if (this.OnGainControl != null) + this.OnGainControl(this, EventArgs.Empty); + } + else if (dist > 55.3f && dist < 55.5f) + { + if (this.OnReset != null) + this.OnReset(this, EventArgs.Empty); + } + } + else if (playerFrozen && currentMap == @"levels\d40\d40") + { + if (this.OnLostControl != null) + this.OnLostControl(this, EventArgs.Empty); + } + } + + prevCurrentMap = currentMap; + prevPlayerFrozen = playerFrozen; + + Thread.Sleep(15); + + if (_cancelSource.IsCancellationRequested) + return; + } + } + catch (Exception ex) + { + Trace.WriteLine(ex.ToString()); + Thread.Sleep(1000); + } + } + } + } + + class DeepPointer + { + private List _offsets; + private int _base; + + public DeepPointer(int base_, params int[] offsets) + { + _base = base_; + _offsets = new List(); + _offsets.Add(0); // deref base first + _offsets.AddRange(offsets); + } + + public bool Deref(Process process, out T value) where T: struct + { + int offset = _offsets[_offsets.Count - 1]; + IntPtr ptr; + if (!this.DerefOffsets(process, out ptr) + || !ReadProcessValue(process, ptr + offset, out value)) + { + value = default(T); + return false; + } + + return true; + } + + public bool Deref(Process process, out Vector3f value) + { + int offset = _offsets[_offsets.Count - 1]; + IntPtr ptr; + float x, y, z; + if (!this.DerefOffsets(process, out ptr) + || !ReadProcessValue(process, ptr + offset + 0, out x) + || !ReadProcessValue(process, ptr + offset + 4, out y) + || !ReadProcessValue(process, ptr + offset + 8, out z)) + { + value = new Vector3f(); + return false; + } + + value = new Vector3f(x, y, z); + return true; + } + + public bool Deref(Process process, out string str, int max) + { + var sb = new StringBuilder(max); + + IntPtr ptr; + if (!this.DerefOffsets(process, out ptr) + || !ReadProcessASCIIString(process, ptr, sb)) + { + str = String.Empty; + return false; + } + + str = sb.ToString(); + return true; + } + + bool DerefOffsets(Process process, out IntPtr ptr) + { + ptr = process.MainModule.BaseAddress + _base; + for (int i = 0; i < _offsets.Count - 1; i++) + { + if (!ReadProcessPtr32(process, ptr + _offsets[i], out ptr) + || ptr == IntPtr.Zero) + { + return false; + } + } + + return true; + } + + static bool ReadProcessValue(Process process, IntPtr addr, out T val) where T : struct + { + Type type = typeof(T); + + var bytes = new byte[Marshal.SizeOf(type)]; + + int read; + val = default(T); + if (!SafeNativeMethods.ReadProcessMemory(process.Handle, addr, bytes, bytes.Length, out read) || read != bytes.Length) + return false; + + if (type == typeof(int)) + { + val = (T)(object)BitConverter.ToInt32(bytes, 0); + } + else if (type == typeof(uint)) + { + val = (T)(object)BitConverter.ToUInt32(bytes, 0); + } + else if (type == typeof(float)) + { + val = (T)(object)BitConverter.ToSingle(bytes, 0); + } + else if (type == typeof(byte)) + { + val = (T)(object)bytes[0]; + } + else if (type == typeof(bool)) + { + val = (T)(object)BitConverter.ToBoolean(bytes, 0); + } + else + { + throw new Exception("Type not supported."); + } + + return true; + } + + static bool ReadProcessPtr32(Process process, IntPtr addr, out IntPtr val) + { + byte[] bytes = new byte[4]; + int read; + val = IntPtr.Zero; + if (!SafeNativeMethods.ReadProcessMemory(process.Handle, addr, bytes, bytes.Length, out read) || read != bytes.Length) + return false; + val = (IntPtr)BitConverter.ToInt32(bytes, 0); + return true; + } + + static bool ReadProcessASCIIString(Process process, IntPtr addr, StringBuilder sb) + { + byte[] bytes = new byte[sb.Capacity]; + int read; + if (!SafeNativeMethods.ReadProcessMemory(process.Handle, addr, bytes, bytes.Length, out read) || read != bytes.Length) + return false; + sb.Append(Encoding.ASCII.GetString(bytes)); + + for (int i = 0; i < sb.Length; i++) + { + if (sb[i] == '\0') + { + sb.Remove(i, sb.Length - i); + break; + } + } + + return true; + } + } + + public class Vector3f + { + public float X { get; set; } + public float Y { get; set; } + public float Z { get; set; } + + public int IX { get { return (int)this.X; } } + public int IY { get { return (int)this.Y; } } + public int IZ { get { return (int)this.Z; } } + + public Vector3f() { } + + public Vector3f(float x, float y, float z) + { + this.X = x; + this.Y = y; + this.Z = z; + } + + public float Distance(Vector3f other) + { + float result = (this.X - other.X) * (this.X - other.X) + + (this.Y - other.Y) * (this.Y - other.Y) + + (this.Z - other.Z) * (this.Z - other.Z); + return (float)Math.Sqrt(result); + } + + public float DistanceXY(Vector3f other) + { + float result = (this.X - other.X) * (this.X - other.X) + + (this.Y - other.Y) * (this.Y - other.Y); + return (float)Math.Sqrt(result); + } + + public override string ToString() + { + return this.X + " " + this.Y + " " + this.Z; + } + } +} diff --git a/HaloSplitComponent.cs b/HaloSplitComponent.cs new file mode 100644 index 0000000..9ecc4e9 --- /dev/null +++ b/HaloSplitComponent.cs @@ -0,0 +1,86 @@ +using LiveSplit.Model; +using LiveSplit.UI.Components; +using LiveSplit.UI; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Xml; +using System.Windows.Forms; + +namespace LiveSplit.HaloSplit +{ + class HaloSplitComponent : IComponent + { + public string ComponentName + { + get { return "HaloSplit"; } + } + + public IDictionary ContextMenuControls { get; protected set; } + + private TimerModel _timer; + private LiveSplitState _state; + private GameMemory _gameMemory; + + public HaloSplitComponent(LiveSplitState state) + { + this.ContextMenuControls = new Dictionary(); + + _timer = new TimerModel(); + _timer.CurrentState = state; + _state = state; + + _gameMemory = new GameMemory(); + // possible thread safety issues in all of these event handlers + // invoking on main thread could lose a few frames of timer accuracy + _gameMemory.OnMapChanged += gameMemory_OnMapChanged; + _gameMemory.OnGainControl += gameMemory_OnGainControl; + _gameMemory.OnLostControl += gameMemory_OnLostControl; + _gameMemory.OnReset += gameMemory_OnReset; + _gameMemory.StartReading(); + } + + void gameMemory_OnMapChanged(object sender, string map) + { + if (map != @"levels\a10\a10") + _timer.Split(); + } + + void gameMemory_OnReset(object sender, EventArgs e) + { + _timer.Reset(); + } + + void gameMemory_OnGainControl(object sender, EventArgs e) + { + _timer.Start(); + } + + void gameMemory_OnLostControl(object sender, EventArgs eventArgs) + { + _timer.Split(); + } + + ~HaloSplitComponent() + { + // TODO: in LiveSplit 1.4, components will be IDisposable + //_gameMemory.Stop(); + } + + public XmlNode GetSettings(XmlDocument document) { return document.CreateElement("Settings"); } + public Control GetSettingsControl(LayoutMode mode) { return null; } + public void SetSettings(XmlNode settings) { } + public void Update(IInvalidator invalidator, LiveSplitState state, float width, float height, LayoutMode mode) { } + public void DrawVertical(Graphics g, LiveSplitState state, float width, Region region) { } + public void DrawHorizontal(Graphics g, LiveSplitState state, float height, Region region) { } + public void RenameComparison(string oldName, string newName) { } + public float VerticalHeight { get { return 0; } } + public float MinimumWidth { get { return 0; } } + public float HorizontalWidth { get { return 0; } } + public float MinimumHeight { get { return 0; } } + public float PaddingLeft { get { return 0; } } + public float PaddingRight { get { return 0; } } + public float PaddingTop { get { return 0; } } + public float PaddingBottom { get { return 0; } } + } +} diff --git a/HaloSplitFactory.cs b/HaloSplitFactory.cs new file mode 100644 index 0000000..5a019d7 --- /dev/null +++ b/HaloSplitFactory.cs @@ -0,0 +1,46 @@ +using System.Reflection; +using LiveSplit.UI.Components; +using System; +using LiveSplit.Model; + +namespace LiveSplit.HaloSplit +{ + public class HaloSplitFactory : IComponentFactory + { + private HaloSplitComponent _instance; + + public string ComponentName + { + get { return "HaloSplit"; } + } + + public IComponent Create(LiveSplitState state) + { + // TODO: in LiveSplit 1.4, components will be IDisposable + // this assumes the passed state is always the same one, until then + return _instance ?? (_instance = new HaloSplitComponent(state)); + + // return new SourceSplitComponent(state); + } + + public string UpdateName + { + get { return this.ComponentName; } + } + + public string UpdateURL + { + get { return "http://fatalis.hive.ai/livesplit/update/"; } + } + + public Version Version + { + get { return Assembly.GetExecutingAssembly().GetName().Version; } + } + + public string XMLURL + { + get { return this.UpdateURL + "Components/update.LiveSplit.HaloSplit.xml"; } + } + } +} diff --git a/LiveSplit.HaloSplit.csproj b/LiveSplit.HaloSplit.csproj new file mode 100644 index 0000000..8b46188 --- /dev/null +++ b/LiveSplit.HaloSplit.csproj @@ -0,0 +1,68 @@ + + + + + Debug + AnyCPU + {8FB279B4-D808-4FFF-9418-87A6FD447EF1} + Library + Properties + LiveSplit.HaloSplit + LiveSplit.HaloSplit + v4.0 + 512 + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + C:\Files\Apps\Gaming\LiveSplit 1.3\LiveSplit.Core.dll + False + + + + + + + C:\Files\Apps\Gaming\LiveSplit 1.3\UpdateManager.dll + False + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LiveSplit.HaloSplit.sln b/LiveSplit.HaloSplit.sln new file mode 100644 index 0000000..ca75cd0 --- /dev/null +++ b/LiveSplit.HaloSplit.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2013 +VisualStudioVersion = 12.0.30110.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiveSplit.HaloSplit", "LiveSplit.HaloSplit.csproj", "{8FB279B4-D808-4FFF-9418-87A6FD447EF1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8FB279B4-D808-4FFF-9418-87A6FD447EF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8FB279B4-D808-4FFF-9418-87A6FD447EF1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8FB279B4-D808-4FFF-9418-87A6FD447EF1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8FB279B4-D808-4FFF-9418-87A6FD447EF1}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..e7c0cf3 --- /dev/null +++ b/Properties/AssemblyInfo.cs @@ -0,0 +1,40 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +using LiveSplit.HaloSplit; +using LiveSplit.UI.Components; + +[assembly: AssemblyTitle("LiveSplit.HaloSplit")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Fatalis")] +[assembly: AssemblyProduct("LiveSplit.HaloSplit")] +[assembly: AssemblyCopyright("")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("118c6880-89e1-462a-a26a-10ef8a710db3")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] + +[assembly: ComponentFactory(typeof(HaloSplitFactory))] \ No newline at end of file diff --git a/SafeNativeMethods.cs b/SafeNativeMethods.cs new file mode 100644 index 0000000..d7f3aa2 --- /dev/null +++ b/SafeNativeMethods.cs @@ -0,0 +1,16 @@ +using System; +using System.Runtime.InteropServices; + +namespace LiveSplit.HaloSplit +{ + static class SafeNativeMethods + { + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool ReadProcessMemory( + IntPtr hProcess, + IntPtr lpBaseAddress, + [Out] byte[] lpBuffer, + int dwSize, // should be IntPtr if we ever need to read a size bigger than 32 bit address space + out int lpNumberOfBytesRead); + } +} diff --git a/license.txt b/license.txt new file mode 100644 index 0000000..220e2fc --- /dev/null +++ b/license.txt @@ -0,0 +1,14 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2004 Sam Hocevar + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. + diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000..075add9 --- /dev/null +++ b/readme.txt @@ -0,0 +1,24 @@ +HaloSplit is a LiveSplit (livesplit.org) component for Halo: Combat Evolved. + +Features: + +Automatically start/stop the timer when player control is gained/lost. +Automatically split when the level changes. +Automatically reset when the first level starts. + +Requirements: + +Halo CE 1.0 or 1.01 +LiveSplit 1.3+ + +Install: + +Extract LiveSplit.HaloSplit.dll to your LiveSplit\Components folder. Restart +LiveSplit. Add HaloSplit in LiveSplit's Layout Editor. It will not be visible +on your splits and will run in the background. + + +@fatalis_ +twitch.tv/fatalis_ +Fatalis @ irc2.speedrunslive.com IRC +fatalis.twitch@gmail.com