From 70cf317295e199823234d774fc0c3fc1c685e3d1 Mon Sep 17 00:00:00 2001 From: Fatalis Date: Sat, 3 May 2014 13:10:53 -0500 Subject: [PATCH] Initial commit. --- LiveSplit.MirrorsEdge.sln | 56 ++ LiveSplit.MirrorsEdge/GameProcess.cs | 161 ++++ .../LiveSplit.MirrorsEdgeNoLoads.csproj | 63 ++ LiveSplit.MirrorsEdge/MirrorsEdgeComponent.cs | 123 +++ LiveSplit.MirrorsEdge/MirrorsEdgeFactory.cs | 54 ++ .../Properties/AssemblyInfo.cs | 39 + LiveSplit.MirrorsEdge/SafeNativeMethods.cs | 79 ++ .../LiveSplit32BitPatcher.csproj | 50 ++ LiveSplit32BitPatcher/Program.cs | 63 ++ .../Properties/AssemblyInfo.cs | 36 + license.txt | 14 + menl_hooks/dll.def | 17 + menl_hooks/dllmain.d | 786 ++++++++++++++++++ menl_hooks/hook.d | 47 ++ menl_hooks/menl_hooks.visualdproj | 197 +++++ menl_hooks/win32.d | 33 + readme.txt | 54 ++ 17 files changed, 1872 insertions(+) create mode 100644 LiveSplit.MirrorsEdge.sln create mode 100644 LiveSplit.MirrorsEdge/GameProcess.cs create mode 100644 LiveSplit.MirrorsEdge/LiveSplit.MirrorsEdgeNoLoads.csproj create mode 100644 LiveSplit.MirrorsEdge/MirrorsEdgeComponent.cs create mode 100644 LiveSplit.MirrorsEdge/MirrorsEdgeFactory.cs create mode 100644 LiveSplit.MirrorsEdge/Properties/AssemblyInfo.cs create mode 100644 LiveSplit.MirrorsEdge/SafeNativeMethods.cs create mode 100644 LiveSplit32BitPatcher/LiveSplit32BitPatcher.csproj create mode 100644 LiveSplit32BitPatcher/Program.cs create mode 100644 LiveSplit32BitPatcher/Properties/AssemblyInfo.cs create mode 100644 license.txt create mode 100644 menl_hooks/dll.def create mode 100644 menl_hooks/dllmain.d create mode 100644 menl_hooks/hook.d create mode 100644 menl_hooks/menl_hooks.visualdproj create mode 100644 menl_hooks/win32.d create mode 100644 readme.txt diff --git a/LiveSplit.MirrorsEdge.sln b/LiveSplit.MirrorsEdge.sln new file mode 100644 index 0000000..507f8fd --- /dev/null +++ b/LiveSplit.MirrorsEdge.sln @@ -0,0 +1,56 @@ + +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.MirrorsEdgeNoLoads", "LiveSplit.MirrorsEdge\LiveSplit.MirrorsEdgeNoLoads.csproj", "{6F40899A-6B45-4827-A3E7-1344DA09C4B8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiveSplit32BitPatcher", "LiveSplit32BitPatcher\LiveSplit32BitPatcher.csproj", "{25B82856-5429-4646-8E21-0E30A00D2AE1}" +EndProject +Project("{002A2DE9-8BB6-484D-9802-7E4AD4084715}") = "menl_hooks", "menl_hooks\menl_hooks.visualdproj", "{0595D913-5F62-4D51-928C-8742CDE2F243}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|Mixed Platforms = Debug|Mixed Platforms + Debug|Win32 = Debug|Win32 + Release|Any CPU = Release|Any CPU + Release|Mixed Platforms = Release|Mixed Platforms + Release|Win32 = Release|Win32 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6F40899A-6B45-4827-A3E7-1344DA09C4B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6F40899A-6B45-4827-A3E7-1344DA09C4B8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6F40899A-6B45-4827-A3E7-1344DA09C4B8}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {6F40899A-6B45-4827-A3E7-1344DA09C4B8}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {6F40899A-6B45-4827-A3E7-1344DA09C4B8}.Debug|Win32.ActiveCfg = Debug|Any CPU + {6F40899A-6B45-4827-A3E7-1344DA09C4B8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6F40899A-6B45-4827-A3E7-1344DA09C4B8}.Release|Any CPU.Build.0 = Release|Any CPU + {6F40899A-6B45-4827-A3E7-1344DA09C4B8}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {6F40899A-6B45-4827-A3E7-1344DA09C4B8}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {6F40899A-6B45-4827-A3E7-1344DA09C4B8}.Release|Win32.ActiveCfg = Release|Any CPU + {25B82856-5429-4646-8E21-0E30A00D2AE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {25B82856-5429-4646-8E21-0E30A00D2AE1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {25B82856-5429-4646-8E21-0E30A00D2AE1}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {25B82856-5429-4646-8E21-0E30A00D2AE1}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {25B82856-5429-4646-8E21-0E30A00D2AE1}.Debug|Win32.ActiveCfg = Debug|Any CPU + {25B82856-5429-4646-8E21-0E30A00D2AE1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {25B82856-5429-4646-8E21-0E30A00D2AE1}.Release|Any CPU.Build.0 = Release|Any CPU + {25B82856-5429-4646-8E21-0E30A00D2AE1}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {25B82856-5429-4646-8E21-0E30A00D2AE1}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {25B82856-5429-4646-8E21-0E30A00D2AE1}.Release|Win32.ActiveCfg = Release|Any CPU + {0595D913-5F62-4D51-928C-8742CDE2F243}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {0595D913-5F62-4D51-928C-8742CDE2F243}.Debug|Mixed Platforms.ActiveCfg = Debug|Win32 + {0595D913-5F62-4D51-928C-8742CDE2F243}.Debug|Mixed Platforms.Build.0 = Debug|Win32 + {0595D913-5F62-4D51-928C-8742CDE2F243}.Debug|Win32.ActiveCfg = Debug|Win32 + {0595D913-5F62-4D51-928C-8742CDE2F243}.Debug|Win32.Build.0 = Debug|Win32 + {0595D913-5F62-4D51-928C-8742CDE2F243}.Release|Any CPU.ActiveCfg = Release|Win32 + {0595D913-5F62-4D51-928C-8742CDE2F243}.Release|Mixed Platforms.ActiveCfg = Release|Win32 + {0595D913-5F62-4D51-928C-8742CDE2F243}.Release|Mixed Platforms.Build.0 = Release|Win32 + {0595D913-5F62-4D51-928C-8742CDE2F243}.Release|Win32.ActiveCfg = Release|Win32 + {0595D913-5F62-4D51-928C-8742CDE2F243}.Release|Win32.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/LiveSplit.MirrorsEdge/GameProcess.cs b/LiveSplit.MirrorsEdge/GameProcess.cs new file mode 100644 index 0000000..b0088ad --- /dev/null +++ b/LiveSplit.MirrorsEdge/GameProcess.cs @@ -0,0 +1,161 @@ +using System; +using System.ComponentModel; +using System.IO; +using System.IO.Pipes; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace LiveSplit.MirrorsEdge +{ + class GameProcess + { + public event EventHandler OnPause; + public event EventHandler OnUnpause; + + private Task _thread; + private CancellationTokenSource _cancelSource; + private const string GAMEDLL = "menl_hooks.dll"; + private const string PIPE_NAME = "LiveSplit.MirrorsEdge"; + + public void Run() + { + if (_thread != null && _thread.Status == TaskStatus.Running) + throw new InvalidOperationException(); + + _cancelSource = new CancellationTokenSource(); + _thread = Task.Factory.StartNew(NamedPipeThread); + } + + public void Stop() + { + if (_cancelSource == null || _thread == null) + throw new InvalidOperationException(); + + if (_thread.Status != TaskStatus.Running) + return; + + _cancelSource.Cancel(); + _thread.Wait(); + } + + void NamedPipeThread() + { + while (!_cancelSource.IsCancellationRequested) + { + try + { + Process gameProcess; + while ((gameProcess = GetGameProcess()) == null) + { + Thread.Sleep(250); + if (_cancelSource.IsCancellationRequested) + return; + } + + Debug.WriteLine("got process"); + + if (!ProcessHasModule(gameProcess, GAMEDLL)) + InjectDLL(gameProcess, GetGameDLLPath()); + + Debug.WriteLine("dll injected"); + + using (var pipe = new NamedPipeClientStream(".", PIPE_NAME, PipeDirection.In)) + using (var sr = new StreamReader(pipe)) + { + while (!gameProcess.HasExited) + { + try + { + pipe.Connect(250); + break; + } + catch (TimeoutException) { } + catch (IOException) { } + } + if (gameProcess.HasExited || !pipe.IsConnected) + continue; + + Debug.WriteLine("pipe connected"); + + string line; + // TODO: readline blocks so when cancellation is supported in 1.4, go async + while ((line = sr.ReadLine()) != null) + { + if (line == "pause" && this.OnPause != null) + this.OnPause(this, EventArgs.Empty); + else if (line == "unpause") + this.OnUnpause(this, EventArgs.Empty); + } + + Debug.WriteLine("pipe disconnected"); + } + } + catch (Exception ex) + { + Trace.WriteLine(ex.ToString()); + Thread.Sleep(1000); + } + } + } + + static Process GetGameProcess() + { + return Process.GetProcesses() + .FirstOrDefault(p => p.ProcessName.ToLower() == "mirrorsedge" && !p.HasExited); + } + + static string GetGameDLLPath() + { + string dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? String.Empty; + return Path.Combine(dir, GAMEDLL); + } + + static bool ProcessHasModule(Process process, string module) + { + return process.Modules.Cast().Any(m => Path.GetFileName(m.FileName).ToLower() == module); + } + + static void InjectDLL(Process process, string path) + { + IntPtr loadLibraryAddr = SafeNativeMethods.GetProcAddress(SafeNativeMethods.GetModuleHandle("kernel32.dll"), "LoadLibraryA"); + if (loadLibraryAddr == IntPtr.Zero) + throw new Exception("Couldn't locate LoadLibraryA"); + + IntPtr mem = IntPtr.Zero; + IntPtr hThread = IntPtr.Zero; + uint len = 0; + + try + { + if ((mem = SafeNativeMethods.VirtualAllocEx(process.Handle, IntPtr.Zero, (uint)path.Length, + SafeNativeMethods.AllocationType.Commit | SafeNativeMethods.AllocationType.Reserve, + SafeNativeMethods.MemoryProtection.ReadWrite)) == IntPtr.Zero) + throw new Win32Exception(Marshal.GetLastWin32Error()); + + byte[] bytes = Encoding.ASCII.GetBytes(path + "\0"); + len = (uint)bytes.Length; + uint written; + if (!SafeNativeMethods.WriteProcessMemory(process.Handle, mem, bytes, len, out written)) + throw new Win32Exception(Marshal.GetLastWin32Error()); + + if ((hThread = SafeNativeMethods.CreateRemoteThread(process.Handle, IntPtr.Zero, 0, loadLibraryAddr, mem, 0, IntPtr.Zero)) + == IntPtr.Zero) + throw new Win32Exception(Marshal.GetLastWin32Error()); + + SafeNativeMethods.WaitForSingleObject(hThread, 0xFFFFFFFF); // INFINITE + } + finally + { + if (mem != IntPtr.Zero && len > 0) + SafeNativeMethods.VirtualFreeEx(process.Handle, mem, len, SafeNativeMethods.FreeType.Release); + if (hThread != IntPtr.Zero) + SafeNativeMethods.CloseHandle(hThread); + } + } + } +} diff --git a/LiveSplit.MirrorsEdge/LiveSplit.MirrorsEdgeNoLoads.csproj b/LiveSplit.MirrorsEdge/LiveSplit.MirrorsEdgeNoLoads.csproj new file mode 100644 index 0000000..d41cebe --- /dev/null +++ b/LiveSplit.MirrorsEdge/LiveSplit.MirrorsEdgeNoLoads.csproj @@ -0,0 +1,63 @@ + + + + + Debug + AnyCPU + {6F40899A-6B45-4827-A3E7-1344DA09C4B8} + Library + Properties + LiveSplit.MirrorsEdge + LiveSplit.MirrorsEdgeNoLoads + v4.0 + 512 + + + true + full + false + ..\bin\ + DEBUG;TRACE + prompt + 4 + AnyCPU + + + pdbonly + true + ..\bin\ + TRACE + prompt + 4 + AnyCPU + + + + 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.MirrorsEdge/MirrorsEdgeComponent.cs b/LiveSplit.MirrorsEdge/MirrorsEdgeComponent.cs new file mode 100644 index 0000000..f6766eb --- /dev/null +++ b/LiveSplit.MirrorsEdge/MirrorsEdgeComponent.cs @@ -0,0 +1,123 @@ +using LiveSplit.Model; +using LiveSplit.TimeFormatters; +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.MirrorsEdge +{ + class MirrorsEdgeComponent : IComponent + { + public string ComponentName + { + get { return "Mirror's Edge No Loads"; } + } + + public IDictionary ContextMenuControls { get; protected set; } + protected InfoTimeComponent InternalComponent { get; set; } + + private LiveSplitState _state; + private GameProcess _gameProcess; + private TimeSpan _loadTime; + private TripleDateTime _pauseStartTime; + private TimeSpan _timeAtPause; + private bool _isPaused; + private GraphicsCache _cache; + + public MirrorsEdgeComponent(LiveSplitState state) + { + this.ContextMenuControls = new Dictionary(); + + this.InternalComponent = new InfoTimeComponent(null, null, new RegularTimeFormatter(TimeAccuracy.Hundredths)); + + _cache = new GraphicsCache(); + _timeAtPause = new TimeSpan(); + _loadTime = new TimeSpan(); + + _state = state; + _state.OnReset += state_OnReset; + + _gameProcess = new GameProcess(); + _gameProcess.OnPause += gameProcess_OnPause; + _gameProcess.OnUnpause += gameProcess_OnUnpause; + _gameProcess.Run(); + } + + public void Update(IInvalidator invalidator, LiveSplitState state, float width, float height, LayoutMode mode) + { + if (_isPaused) + this.InternalComponent.TimeValue = (_timeAtPause - _loadTime); + else + this.InternalComponent.TimeValue = (_state.CurrentTime.Value - _loadTime); + + _cache.Restart(); + _cache["TimeValue"] = this.InternalComponent.ValueLabel.Text; + if (invalidator != null && _cache.HasChanged) + invalidator.Invalidate(0f, 0f, width, height); + } + + public void DrawVertical(Graphics g, LiveSplitState state, float width, Region region) + { + this.InternalComponent.NameLabel.Text = "Without Loads"; + this.InternalComponent.NameLabel.ForeColor = state.LayoutSettings.TextColor; + this.InternalComponent.ValueLabel.ForeColor = state.LayoutSettings.TextColor; + this.InternalComponent.DrawVertical(g, state, width, region); + } + + public void DrawHorizontal(Graphics g, LiveSplitState state, float height, Region region) + { + this.InternalComponent.NameLabel.Text = "Without Loads"; + this.InternalComponent.NameLabel.ForeColor = state.LayoutSettings.TextColor; + this.InternalComponent.ValueLabel.ForeColor = state.LayoutSettings.TextColor; + this.InternalComponent.DrawHorizontal(g, state, height, region); + } + + void state_OnReset(object sender, EventArgs e) + { + _loadTime = new TimeSpan(); + _isPaused = false; + } + + void gameProcess_OnPause(object sender, EventArgs e) + { + if (!_isPaused && _state.CurrentPhase == TimerPhase.Running) + { + _pauseStartTime = TripleDateTime.Now; + _timeAtPause = _state.CurrentTime.Value; + _isPaused = true; + } + } + + void gameProcess_OnUnpause(object sender, EventArgs e) + { + if (_isPaused && _state.CurrentPhase == TimerPhase.Running) + { + _loadTime = _loadTime.Add(TripleDateTime.Now - _pauseStartTime); + _isPaused = false; + } + } + + ~MirrorsEdgeComponent() + { + // TODO: in LiveSplit 1.4, components will be IDisposable + //_gameProcess.Stop(); + } + + public XmlNode GetSettings(XmlDocument document) { return document.CreateElement("Settings"); } + public Control GetSettingsControl(LayoutMode mode) { return null; } + public void SetSettings(XmlNode settings) { } + public void RenameComparison(string oldName, string newName) { } + public float VerticalHeight { get { return this.InternalComponent.VerticalHeight; } } + public float MinimumWidth { get { return this.InternalComponent.MinimumWidth; } } + public float HorizontalWidth { get { return this.InternalComponent.HorizontalWidth; } } + public float MinimumHeight { get { return this.InternalComponent.MinimumHeight; } } + public float PaddingLeft { get { return this.InternalComponent.PaddingLeft; } } + public float PaddingRight { get { return this.InternalComponent.PaddingRight; } } + public float PaddingTop { get { return this.InternalComponent.PaddingTop; } } + public float PaddingBottom { get { return this.InternalComponent.PaddingBottom; } } + } +} diff --git a/LiveSplit.MirrorsEdge/MirrorsEdgeFactory.cs b/LiveSplit.MirrorsEdge/MirrorsEdgeFactory.cs new file mode 100644 index 0000000..08b78cd --- /dev/null +++ b/LiveSplit.MirrorsEdge/MirrorsEdgeFactory.cs @@ -0,0 +1,54 @@ +using System.Reflection; +using System.Windows.Forms; +using LiveSplit.UI.Components; +using System; +using LiveSplit.Model; + +namespace LiveSplit.MirrorsEdge +{ + public class MirrorsEdgeFactory : IComponentFactory + { + private MirrorsEdgeComponent _instance; + + public string ComponentName + { + get { return "Mirror's Edge No Loads"; } + } + + public IComponent Create(LiveSplitState state) + { + if (Environment.Is64BitProcess) + { + MessageBox.Show("LiveSplit.MirrorsEdgeNoLoads doesn't support x64 LiveSplit! Please run LiveSplit32BitPatcher at least once.", + "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + throw new Exception("x64 not supported."); + } + + // 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 MirrorsEdgeComponent(state)); + + // return new MirrorsEdgeComponent(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.MirrorsEdgeNoLoads.xml"; } + } + } +} diff --git a/LiveSplit.MirrorsEdge/Properties/AssemblyInfo.cs b/LiveSplit.MirrorsEdge/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..3670800 --- /dev/null +++ b/LiveSplit.MirrorsEdge/Properties/AssemblyInfo.cs @@ -0,0 +1,39 @@ +using System.Reflection; +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.MirrorsEdge; +using LiveSplit.UI.Components; + +[assembly: AssemblyTitle("LiveSplit.MirrorsEdge")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Fatalis")] +[assembly: AssemblyProduct("LiveSplit.MirrorsEdge")] +[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("b185a2c5-6874-4ca6-a971-6bc4a9af1f4d")] + +// 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(MirrorsEdgeFactory))] \ No newline at end of file diff --git a/LiveSplit.MirrorsEdge/SafeNativeMethods.cs b/LiveSplit.MirrorsEdge/SafeNativeMethods.cs new file mode 100644 index 0000000..706c656 --- /dev/null +++ b/LiveSplit.MirrorsEdge/SafeNativeMethods.cs @@ -0,0 +1,79 @@ +using System; +using System.Runtime.InteropServices; + +namespace LiveSplit.MirrorsEdge +{ + static class SafeNativeMethods + { + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool ReadProcessMemory( + IntPtr hProcess, + IntPtr lpBaseAddress, + [Out] byte[] lpBuffer, + uint nSize, // should be IntPtr if we ever need to read a size bigger than 32 bit address space + out uint lpNumberOfBytesRead); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, [In] byte[] lpBuffer, uint nSize, out uint lpNumberOfBytesWritten); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern IntPtr GetModuleHandle(string lpModuleName); + + [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)] + public static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, AllocationType flAllocationType, MemoryProtection flProtect); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool VirtualFreeEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, FreeType dwFreeType); + + [DllImport("kernel32.dll")] + public static extern IntPtr CreateRemoteThread(IntPtr hProcess, + IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, + IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern UInt32 WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool CloseHandle(IntPtr hObject); + + [Flags] + public enum AllocationType + { + Commit = 0x1000, + Reserve = 0x2000, + Decommit = 0x4000, + Release = 0x8000, + Reset = 0x80000, + Physical = 0x400000, + TopDown = 0x100000, + WriteWatch = 0x200000, + LargePages = 0x20000000 + } + + [Flags] + public enum MemoryProtection + { + Execute = 0x10, + ExecuteRead = 0x20, + ExecuteReadWrite = 0x40, + ExecuteWriteCopy = 0x80, + NoAccess = 0x01, + ReadOnly = 0x02, + ReadWrite = 0x04, + WriteCopy = 0x08, + GuardModifierflag = 0x100, + NoCacheModifierflag = 0x200, + WriteCombineModifierflag = 0x400 + } + + [Flags] + public enum FreeType + { + Decommit = 0x4000, + Release = 0x8000, + } + } +} diff --git a/LiveSplit32BitPatcher/LiveSplit32BitPatcher.csproj b/LiveSplit32BitPatcher/LiveSplit32BitPatcher.csproj new file mode 100644 index 0000000..a2c98a3 --- /dev/null +++ b/LiveSplit32BitPatcher/LiveSplit32BitPatcher.csproj @@ -0,0 +1,50 @@ + + + + + Debug + AnyCPU + {25B82856-5429-4646-8E21-0E30A00D2AE1} + WinExe + Properties + LiveSplit32BitPatcher + LiveSplit32BitPatcher + v4.0 + 512 + + + AnyCPU + true + full + false + ..\bin\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + ..\bin\ + TRACE + prompt + 4 + + + + + + + + + + + + \ No newline at end of file diff --git a/LiveSplit32BitPatcher/Program.cs b/LiveSplit32BitPatcher/Program.cs new file mode 100644 index 0000000..210039a --- /dev/null +++ b/LiveSplit32BitPatcher/Program.cs @@ -0,0 +1,63 @@ +using System; +using System.IO; +using System.Windows.Forms; + +namespace LiveSplit32BitPatcher +{ + static class Program + { + [STAThread] + static void Main() + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + + try + { + string path = "LiveSplit.exe"; + if (!File.Exists(path)) + { + ShowMessage("LiveSplit.exe couldn't be found. Please browse to it.", MessageBoxIcon.Exclamation); + + using (var fd = new OpenFileDialog()) + { + fd.Filter = "LiveSplit.exe|LiveSplit.exe"; + if (fd.ShowDialog() != DialogResult.OK) + return; + path = fd.FileName; + } + } + + using (var fs = new FileStream(path, FileMode.Open, FileAccess.ReadWrite)) + { + const byte FLAG_32_BIT_REQUIRED = 0x02; + + // .NET Directory - Flags + fs.Seek(0x218, SeekOrigin.Begin); + var flags = (byte)fs.ReadByte(); + + if ((flags & FLAG_32_BIT_REQUIRED) != 0) + { + ShowMessage("The patch is already installed! You only need to do this once.", MessageBoxIcon.Exclamation); + } + else + { + fs.Seek(-1, SeekOrigin.Current); + fs.WriteByte((byte)(flags | FLAG_32_BIT_REQUIRED)); + + ShowMessage("Patch successful!", MessageBoxIcon.Information); + } + } + } + catch (Exception ex) + { + ShowMessage("Error!" + Environment.NewLine + ex, MessageBoxIcon.Error); + } + } + + static void ShowMessage(string message, MessageBoxIcon icon) + { + MessageBox.Show(message, "LiveSplit 32-Bit Patcher", MessageBoxButtons.OK, icon); + } + } +} diff --git a/LiveSplit32BitPatcher/Properties/AssemblyInfo.cs b/LiveSplit32BitPatcher/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..d4e35c6 --- /dev/null +++ b/LiveSplit32BitPatcher/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +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. +[assembly: AssemblyTitle("LiveSplit32BitPatcher")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("LiveSplit32BitPatcher")] +[assembly: AssemblyCopyright("Copyright © 2014")] +[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("9f2dc2cd-0109-4f8f-9ace-43d928723f2d")] + +// 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")] diff --git a/license.txt b/license.txt new file mode 100644 index 0000000..5a8e332 --- /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/menl_hooks/dll.def b/menl_hooks/dll.def new file mode 100644 index 0000000..e8f6682 --- /dev/null +++ b/menl_hooks/dll.def @@ -0,0 +1,17 @@ +; linker definition file +; +; when creating DLLs a definition file should always be specified because of +; http://d.puremagic.com/issues/show_bug.cgi?id=8130 + +EXETYPE NT +LIBRARY "menl_hooks.dll" +CODE PRELOAD DISCARDABLE +DATA PRELOAD MULTIPLE + +; there's also bug http://d.puremagic.com/issues/show_bug.cgi?id=3956 causing +; inconsistent naming for symbols with "export" specifier +; The workaround is to list the names in the EXPORT section translating the name to itself: +; EXPORTS +; Symbol Symbol + +EXPORTS \ No newline at end of file diff --git a/menl_hooks/dllmain.d b/menl_hooks/dllmain.d new file mode 100644 index 0000000..81bd80d --- /dev/null +++ b/menl_hooks/dllmain.d @@ -0,0 +1,786 @@ +// this is the dll that gets injected into the game process and hooks engine functions +// it communicates with livesplit over a named pipe + +import std.c.windows.windows; +import core.sys.windows.dll; +import std.c.stdio: freopen; +import std.cstream; +import std.math; +import std.string; +import std.conv; +import std.file; + +import hook; +import win32; + +export void stub() { }; + +__gshared { // don't put globals in Thread Local Storage + +HMODULE g_base; +HINSTANCE g_hInst; +HANDLE g_hPipe; +LevelStreamData[] g_levelStreamData; +Thread g_thread; +OffsetDB g_offsetDB; +GameVersion g_version; +string g_waitForSublevel; +void* g_player; +bool g_consoleInitialized; + +} + +const string PIPE_NAME = "LiveSplit.MirrorsEdge"; + +enum GameVersion +{ + Unknown, + Steam101, + RELOADED101, + RELOADED100, + OriginOrDVD101, + DVD100, +} + +enum OffsetName +{ + LevelStreamStartFunc, + StringTablePtr, + StaticLevelLoadFunc, + SublevelFinishedLoadingFunc, + MidLoadingStartFunc, + MidLoadingEndFunc, + DeathLoadingStartFunc, + DeathLoadingEndFunc, + UnknownImportantPtr, + UnknownPlayerFunc, +} + +extern (Windows) +bool DllMain(HINSTANCE hInstance, uint ulReason, void* pvReserved) +{ + final switch (ulReason) + { + case DLL_PROCESS_ATTACH: + dll_process_attach(hInstance, true); + + g_hInst = hInstance; + + if (g_thread is null) + { + g_thread = new Thread(&MainThread); + g_thread.start(); + } + + break; + case DLL_PROCESS_DETACH: + dll_process_detach(hInstance, true); + break; + case DLL_THREAD_ATTACH: + dll_thread_attach(true, true); + break; + case DLL_THREAD_DETACH: + dll_thread_detach(true, true); + break; + } + + return true; +} + +void MainThread() +{ + g_base = GetModuleHandleA(null); + + g_version = DetectGameVersion(); + if (g_version == GameVersion.Unknown) + return; + + // show the debug console if compiled for debug or if holding F10 during startup + if (GetAsyncKeyState(VK_F10)) + InitConsole(); + else + debug InitConsole(); + + WriteConsole("debug: mirrorsedge_hooks.dll loaded into MirrorsEdge.exe successfully"); + WriteConsole(format("game version detected = %s", g_version)); + + // the offsets are identical + if (g_version == GameVersion.OriginOrDVD101) + g_version = GameVersion.RELOADED101; + else if (g_version == GameVersion.DVD100) + g_version = GameVersion.RELOADED100; + + InitData(); + InstallHooks(); + + WriteConsole("hooks: installed"); + + RunNamedPipe(); +} + +// detect game version by file size +GameVersion DetectGameVersion() +{ + try + { + auto path = new wchar[MAX_PATH]; + GetModuleFileNameW(g_base, path.ptr, path.length); + + auto entry = DirEntry(to!string(path)); + + final switch (entry.size) + { + case 60167392: + return GameVersion.RELOADED101; + case 31946072: + return GameVersion.Steam101; + case 60298504: + return GameVersion.RELOADED100; + case 36466688: + return GameVersion.DVD100; + case 36484440: + return GameVersion.OriginOrDVD101; + } + } + catch { } + + return GameVersion.Unknown; +} + +void InitConsole() +{ + AllocConsole(); + freopen("CONOUT$", "w", dout.file); + freopen("CONIN$", "r", din.file); + g_consoleInitialized = true; +} + +void InitData() +{ + // elevators + g_levelStreamData ~= new LevelStreamData("Escape_Intro", 7, "Escape_Off-R1_Slc_Lgts" ); // Ch1 A + g_levelStreamData ~= new LevelStreamData("Escape_Off_Bac", 13, "Edge_SB02_Mus" ); // Ch1 C + g_levelStreamData ~= new LevelStreamData("Stormdrain_StdE", 8, "Stormdrain_Roof_boss_Bac" ); // Ch2 E + g_levelStreamData ~= new LevelStreamData("Stormdrain_Roof_spt", 8, "Stormdrain_boss_Spt" ); // Ch2 G + g_levelStreamData ~= new LevelStreamData("Cranes_Off-Roof_Building", 8, "Cranes_Plaza_LW" ); // Ch3 C + g_levelStreamData ~= new LevelStreamData("Mall_HW-R1_Slc", 3, "Mall_R1-R2_Lgts" ); // Ch5 A + g_levelStreamData ~= new LevelStreamData("Mall_R1-R2_Slc", 12, "Mall_Mall_Lgts_Pt1" ); // Ch5 C + g_levelStreamData ~= new LevelStreamData("Factory_Arena_Spt", 6, "Factory_Bac" ); // Ch6 D + g_levelStreamData ~= new LevelStreamData("Boat_Ind-Cont_Slc", 13, "Boat_Deck_Spt" ); // Ch7 A + g_levelStreamData ~= new LevelStreamData("Convoy_Roof", 9, "Convoy_Chase_Bac2" ); // Ch8 A + g_levelStreamData ~= new LevelStreamData("Convoy_Conv", 8, "Convoy_Snipe-Chase_Aud" ); // Ch8 B + g_levelStreamData ~= new LevelStreamData("Scraper_Deck_Spt", 8, "Scraper_Roof_Bac2" ); // Ch9 B + g_levelStreamData ~= new LevelStreamData("Scraper_Lobby", 14, "Scraper_Duct-Roof_Aud" ); // Ch9 C + g_levelStreamData ~= new LevelStreamData("Scraper_Roof_Spt", 16, "Scraper_Ext_Lgts" ); // Ch9 E + + // special cases + g_levelStreamData ~= new LevelStreamData("Stormdrain_Std", 15, "Stormdrain_StdP-StdE_slc_Lgts", + Vector3f(1321f, -30039f, -6635f), 150f); + g_levelStreamData ~= new LevelStreamData("Stormdrain_StdP", 10, "Stormdrain_StdE-Out_Blding_slc", + Vector3f(1488f, -10488f, -7267f), 70f); + + // OoB + g_levelStreamData ~= new LevelStreamData("Subway_Stat_Spt", 12, "Subway_Plat_Spt" ); + g_levelStreamData ~= new LevelStreamData("Factory_Lbay_Spt", 7, "Factory_Facto" ); + g_levelStreamData ~= new LevelStreamData("Scraper_Lobby", 13, "Scraper_Duct" ); + + g_offsetDB = new OffsetDB(); + g_offsetDB.Add(GameVersion.RELOADED101, OffsetName.LevelStreamStartFunc, 0xA0F260); + g_offsetDB.Add(GameVersion.RELOADED101, OffsetName.StringTablePtr, 0x1C67898); + g_offsetDB.Add(GameVersion.RELOADED101, OffsetName.StaticLevelLoadFunc, 0xDC7650); + g_offsetDB.Add(GameVersion.RELOADED101, OffsetName.SublevelFinishedLoadingFunc, 0x784970); // sig: 33 50 60 83 E2 01 31 50 60 + g_offsetDB.Add(GameVersion.RELOADED101, OffsetName.MidLoadingStartFunc, 0xAC54B0); + g_offsetDB.Add(GameVersion.RELOADED101, OffsetName.MidLoadingEndFunc, 0xAC5C80); + //g_offsetDB.Add(GameVersion.RELOADED101, OffsetName.DeathLoadingStartFunc, 0xDC4E10); + g_offsetDB.Add(GameVersion.RELOADED101, OffsetName.DeathLoadingStartFunc, 0xDC4DE0); + g_offsetDB.Add(GameVersion.RELOADED101, OffsetName.DeathLoadingEndFunc, 0xDC5420); + g_offsetDB.Add(GameVersion.RELOADED101, OffsetName.UnknownImportantPtr, 0x1C14D64); + g_offsetDB.Add(GameVersion.RELOADED101, OffsetName.UnknownPlayerFunc, 0xE68CF0); // sig: 8b 86 9c 00 00 00 8b 88 + + g_offsetDB.Add(GameVersion.Steam101, OffsetName.LevelStreamStartFunc, 0xA0F190); + g_offsetDB.Add(GameVersion.Steam101, OffsetName.StringTablePtr, 0x1C4E7D8); + g_offsetDB.Add(GameVersion.Steam101, OffsetName.StaticLevelLoadFunc, 0xDC6A70); + g_offsetDB.Add(GameVersion.Steam101, OffsetName.SublevelFinishedLoadingFunc, 0x7848A0); + g_offsetDB.Add(GameVersion.Steam101, OffsetName.MidLoadingStartFunc, 0xAC53E0); + g_offsetDB.Add(GameVersion.Steam101, OffsetName.MidLoadingEndFunc, 0xAC5BB0); + //g_offsetDB.Add(GameVersion.Steam101, OffsetName.DeathLoadingStartFunc, 0xDC4C30); + g_offsetDB.Add(GameVersion.Steam101, OffsetName.DeathLoadingStartFunc, 0xDC4C00); + g_offsetDB.Add(GameVersion.Steam101, OffsetName.DeathLoadingEndFunc, 0xDC5010); + g_offsetDB.Add(GameVersion.Steam101, OffsetName.UnknownImportantPtr, 0x1BFBCA4); + g_offsetDB.Add(GameVersion.Steam101, OffsetName.UnknownPlayerFunc, 0xE679D0); + + g_offsetDB.Add(GameVersion.RELOADED100, OffsetName.LevelStreamStartFunc, 0xA0EE60); + g_offsetDB.Add(GameVersion.RELOADED100, OffsetName.StringTablePtr, 0x1C67898); + g_offsetDB.Add(GameVersion.RELOADED100, OffsetName.StaticLevelLoadFunc, 0xDC7050); + g_offsetDB.Add(GameVersion.RELOADED100, OffsetName.SublevelFinishedLoadingFunc, 0x784740); + g_offsetDB.Add(GameVersion.RELOADED100, OffsetName.MidLoadingStartFunc, 0xAC50B0); + g_offsetDB.Add(GameVersion.RELOADED100, OffsetName.MidLoadingEndFunc, 0xAC5880); + //g_offsetDB.Add(GameVersion.RELOADED100, OffsetName.DeathLoadingStartFunc, 0xDC4810); + g_offsetDB.Add(GameVersion.RELOADED100, OffsetName.DeathLoadingStartFunc, 0xDC47E0); + g_offsetDB.Add(GameVersion.RELOADED100, OffsetName.DeathLoadingEndFunc, 0xDC4E20); + g_offsetDB.Add(GameVersion.RELOADED100, OffsetName.UnknownImportantPtr, 0x1C14D5C); + g_offsetDB.Add(GameVersion.RELOADED100, OffsetName.UnknownPlayerFunc, 0xE686F0); +} + +void InstallHooks() +{ + TrampolineHook( + cast(ubyte*)g_base+g_offsetDB.Get(g_version, OffsetName.StaticLevelLoadFunc), + cast(ubyte*)&StaticLevelLoadHook, + cast(ubyte*)&StaticLevelLoadGate, + JMP_SIZE+2); + + TrampolineHook( + cast(ubyte*)g_base+g_offsetDB.Get(g_version, OffsetName.LevelStreamStartFunc), + cast(ubyte*)&LevelStreamStartHook, + cast(ubyte*)&LevelStreamStartGate, + JMP_SIZE+2); + + TrampolineHook( + cast(ubyte*)g_base+g_offsetDB.Get(g_version, OffsetName.SublevelFinishedLoadingFunc), + cast(ubyte*)&SublevelFinishedLoadingHook, + cast(ubyte*)&SublevelFinishedLoadingGate, + JMP_SIZE+1); + + TrampolineHook( + cast(ubyte*)g_base+g_offsetDB.Get(g_version, OffsetName.MidLoadingStartFunc), + cast(ubyte*)&MidLoadingStartHook, + cast(ubyte*)&MidLoadingStartGate, + JMP_SIZE+5); + + TrampolineHook( + cast(ubyte*)g_base+g_offsetDB.Get(g_version, OffsetName.MidLoadingEndFunc), + cast(ubyte*)&MidLoadingEndHook, + cast(ubyte*)&MidLoadingEndGate, + JMP_SIZE+1); + + TrampolineHook( + cast(ubyte*)g_base+g_offsetDB.Get(g_version, OffsetName.DeathLoadingStartFunc), + cast(ubyte*)&DeathLoadingStartHook, + cast(ubyte*)&DeathLoadingStartGate, + JMP_SIZE+5); + + TrampolineHook( + cast(ubyte*)g_base+g_offsetDB.Get(g_version, OffsetName.DeathLoadingEndFunc), + cast(ubyte*)&DeathLoadingEndHook, + cast(ubyte*)&DeathLoadingEndGate, + JMP_SIZE+1); + + TrampolineHook( + cast(ubyte*)g_base+g_offsetDB.Get(g_version, OffsetName.UnknownPlayerFunc), + cast(ubyte*)&UnknownPlayerFuncHook, + cast(ubyte*)&UnknownPlayerFuncGate, + JMP_SIZE); +} + +void RunNamedPipe() +{ + HANDLE hPipe = CreateNamedPipeA( + (r"\\.\pipe\" ~ PIPE_NAME).toStringz(), + PIPE_ACCESS_DUPLEX, + PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_NOWAIT, + 1, + 1024, 1024, + 0, + null); + + if (hPipe == INVALID_HANDLE_VALUE) + return; + + scope(exit) CloseHandle(hPipe); + g_hPipe = hPipe; + + while (true) + { + WriteConsole("named pipe: waiting connection"); + + DisconnectNamedPipe(hPipe); + while (!ConnectNamedPipe(hPipe, null) && GetLastError() != ERROR_PIPE_CONNECTED) + { + Sleep(1); + } + + WriteConsole("named pipe: connected"); + + byte tmp; + uint read; + while (ReadFile(hPipe, &tmp, 1, &read, null) || GetLastError() != ERROR_BROKEN_PIPE) + { + // ERROR_BROKEN_PIPE on disconnect + // ERROR_NO_DATA nothing to read + Sleep(1); + } + + WriteConsole("named pipe: disconnected"); + } +} + +// TODO: the original function doesn't return until the cutscene has been skipped, +// so frames of accuracy are lost until the player cancels the cutscene. +// we need to detect when the cutscene is skippable. +extern(Windows) +int StaticLevelLoadHook(void* levelInfo, int unk, void* unk2) +{ + void* this_; asm { mov this_, ECX; } + + wchar* wc = *cast(wchar**)(levelInfo+0x1C); + string name = to!string(wc[0..wcslen(wc)]); + + WriteConsole(format("static level load started: %s", name)); + SetPausedState(true); + + // they quit out, wipe the state + if (name == "TdMainMenu") + g_waitForSublevel = null; + + WriteConsole("resetting level stream data"); + foreach (LevelStreamData d; g_levelStreamData) + { + d.Reset(); + } + + asm { mov ECX, this_; } + int ret = StaticLevelLoadGate(levelInfo, unk, unk2); + + SetPausedState(false); + + WriteConsole("static level load finished"); + + return ret; +} + +extern(Windows) +int StaticLevelLoadGate(void* levelInfo, int unk, void* unk2) { + asm { naked; + nop; nop; nop; nop; nop; nop; nop; // overwritten bytes + nop; nop; nop; nop; nop; } // jmp +} + +extern(C) +int MidLoadingStartHook() +{ + int ret = MidLoadingStartGate(); + + WriteConsole("mid loading start detected"); + SetPausedState(true); + + return ret; +} + +extern(C) +int MidLoadingStartGate() { + asm { naked; + nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; // overwritten bytes + nop; nop; nop; nop; nop; } // jmp +} + +extern(C) +int MidLoadingEndHook() +{ + int ret = MidLoadingEndGate(); + + WriteConsole("mid loading end detected"); + SetPausedState(false); + + return ret; +} + +extern(C) +int MidLoadingEndGate() { + asm { naked; + nop; nop; nop; nop; nop; nop; // overwritten bytes + nop; nop; nop; nop; nop; } // jmp +} + +extern(C) +int DeathLoadingStartHook() +{ + int ret = DeathLoadingStartGate(); + + WriteConsole("death loading start detected"); + SetPausedState(true); + + return ret; +} + +extern(C) +int DeathLoadingStartGate() { + asm { naked; + nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; // overwritten bytes + nop; nop; nop; nop; nop; } // jmp +} + +extern(C) +int DeathLoadingEndHook() +{ + int ret = DeathLoadingEndGate(); + + WriteConsole("death loading end detected"); + SetPausedState(false); + + return ret; +} + +extern(C) +int DeathLoadingEndGate() { + asm { naked; + nop; nop; nop; nop; nop; nop; // overwritten bytes + nop; nop; nop; nop; nop; } // jmp +} + + +extern(Windows) +void SublevelFinishedLoadingHook(void* levelInfo, void* unk) +{ + SublevelFinishedLoadingGate(levelInfo, unk); + + ubyte flags = *cast(ubyte*)(levelInfo+0x60); + if (flags & 0x80) + { + int sublevelStrID = *cast(int*)(levelInfo+0x3C); + string sublevelName = GetStringByID(sublevelStrID); + + WriteConsole(format("sublevel finished loading: %s", sublevelName)); + + foreach (LevelStreamData d; g_levelStreamData) + { + if (d.LastLoadSublevel == sublevelName) + { + WriteConsole("cancelled waiting to reach required pos because the target sublevel finished loading"); + d.Reset(); + break; + } + } + + if (g_waitForSublevel is null) + return; + // hack fix for ch6d elevator in SS + if (g_waitForSublevel == sublevelName || (g_waitForSublevel == "Factory_Bac" && sublevelName == "Factory_Pursu_lgts")) + { + g_waitForSublevel = null;; + WriteConsole("--elevator/oob finished loading--"); + SetPausedState(false); + } + } +} + +extern(Windows) +void SublevelFinishedLoadingGate(void* levelInfo, void* unk) { + asm { naked; + nop; nop; nop; nop; nop; nop; // overwritten bytes + nop; nop; nop; nop; nop; } // jmp +} + +extern(Windows) +int LevelStreamStartHook() +{ + void* this_; asm { mov this_, ECX; } + + const byte LOADTYPE_UNLOADING = 0; + const byte LOADTYPE_LOADING = 1; + const byte FLAG_LOADED = 1; + + int ret = LevelStreamStartGate(); + + // get the list of sublevels to be unloaded/loaded + string[] sublevels; + int sublevelCount = *cast(int*)(this_+0xF4); + for (int i = 0; i < sublevelCount; i++) + { + ubyte* ptr = *cast(ubyte**)(this_+0xF0); + ptr += (i * 12); + int sublevelStringID = *cast(int*)(ptr+4); + sublevels ~= GetStringByID(sublevelStringID); + } + + byte loadingType = *(*cast(byte**)(this_+0x88)+0xC); // 1 = loading, 0 = unloading + + WriteConsole(loadingType == LOADTYPE_LOADING ? "-load list-" : "-unload list-"); + foreach (string sublevel; sublevels) + { + WriteConsole(format("%s: %X", sublevel, GetSublevelStatusFlag(sublevel))); + } + + foreach (LevelStreamData d; g_levelStreamData) + { + if (loadingType == LOADTYPE_UNLOADING && d.UnloadCount == sublevels.length && d.FirstUnloadSublevel == sublevels[0]) + { + // check if this is a real unload + int numLoaded = 0; + foreach (string sublevel; sublevels) + { + if ((GetSublevelStatusFlag(sublevel) & FLAG_LOADED)) + numLoaded++; + } + // if at least one sublevel in the unload list is currently loaded, it's a real unload + // ch4 oob is an exception because none of the items on the unload list are loaded + if (numLoaded < 1 && sublevels[0] != "Factory_Lbay_Spt") + break; + + if (d.IsPositional() && !d.RequiredPositionReached) + { + WriteConsole("load started but required pos hasnt been reached yet. waiting until required area is reached before pausing"); + d.LoadingBeforeRequiredPosition = true; + } + else + { + SetPausedState(true); + WriteConsole("--elevator/oob load start detected--"); + g_waitForSublevel = d.LastLoadSublevel; + } + + WriteConsole(format("num loaded on unload list: %d/%d", numLoaded, sublevels.length)); + + break; + } + } + + return ret; +} + +extern(Windows) +int LevelStreamStartGate() { + asm { naked; + nop; nop; nop; nop; nop; nop; nop; // overwritten bytes + nop; nop; nop; nop; nop; } // jmp +} + +// some function that runs every frame and it's this ptr can be used to find player position +// use this to find player ptr and do stuff we need to do once per frame +float g_test; +extern(Windows) +int UnknownPlayerFuncHook(float frametime) +{ + asm { mov g_player, ECX; } + + int ret = UnknownPlayerFuncGate(frametime); + + Vector3f* pos = GetPlayerPos(); + if (pos is null) + return ret; + + foreach (LevelStreamData d; g_levelStreamData) + { + if (d.CheckPosition(pos)) + { + WriteConsole("pausing because reached required pos and level not finished streaming yet"); + SetPausedState(true); + g_waitForSublevel = d.LastLoadSublevel; + break; + } + } + + debug + { + //string test = format("%f %f %f", pos.X, pos.Y, pos.Z); + //SetConsoleTitleA(test.toStringz()); + } + + return ret; +} + +extern(Windows) +bool UnknownPlayerFuncGate(float frametime) { + asm { naked; + nop; nop; nop; nop; nop; // overwritten bytes + nop; nop; nop; nop; nop; } // jmp +} + +void WriteConsole(string message) +{ + if (g_consoleInitialized) + std.stdio.writeln(message); +} + +void SetPausedState(bool paused) +{ + paused ? WritePipe("pause") : WritePipe("unpause"); +} + +bool WritePipe(string message) +{ + if (g_hPipe is null) + return false; + + message ~= "\n"; + + uint written; + if (!WriteFile(g_hPipe, message.ptr, message.length, &written, null) || written != message.length) + return false; + FlushFileBuffers(g_hPipe); + + return true; +} + +string GetStringByID(int id) +{ + if (id == 0) + return ""; + + void* ptr = *cast(void**)(g_base+g_offsetDB.Get(g_version, OffsetName.StringTablePtr)); + ptr = *cast(void**)(ptr + (id*4)); + ptr += 0x10; + + wchar* wc = cast(wchar*)ptr; + + return to!string(wc[0..wcslen(wc)]); +} + +byte GetSublevelStatusFlag(string level) +{ + void* ptr = g_base+g_offsetDB.Get(g_version, OffsetName.UnknownImportantPtr); + ptr = *cast(void**)ptr; + ptr = *cast(void**)(ptr+0x50); + ptr = **cast(void***)(ptr+0x3C); + + int numSublevels = *cast(int*)(ptr+0xBF0); + void* sublevelsBase = *cast(void**)(ptr+0xBEC); + + for (int i = 0; i < numSublevels; i++) + { + void* sublevel = *cast(void**)(sublevelsBase+(i*4)); + if (sublevel is null) + continue; + int sublevelStrID = *cast(int*)(sublevel+0x3C); + string sublevelStr = GetStringByID(sublevelStrID); + if (sublevelStr == level) + return *cast(byte*)(sublevel+0x60); + } + return 0; +} + +Vector3f* GetPlayerPos() +{ + if (g_player is null) + return null; + + void* ptr = *cast(void**)(g_player + 0x4a4); + ptr = *cast(void**)(ptr + 0x214); + if (ptr is null) // time trial mode crash fix + return null; + ptr += 0xE8; + + return cast(Vector3f*)ptr; +} + +class LevelStreamData +{ + string FirstUnloadSublevel; + int UnloadCount; + string LastLoadSublevel; + string AltLastLoadSublevel; + + bool LoadingBeforeRequiredPosition; + bool RequiredPositionReached; + Vector3f RequiredPosition; + float RequiredPositionRadius; + + this(string firstUnloadSublevel, int unloadCount, string lastLoadSublevel) + { + this.FirstUnloadSublevel = firstUnloadSublevel; + this.UnloadCount = unloadCount; + this.LastLoadSublevel = lastLoadSublevel; + } + + this(string firstUnloadSublevel, int unloadCount, string lastLoadSublevel, Vector3f requiredPos, float requiredPosRadius) + { + this(firstUnloadSublevel, unloadCount, lastLoadSublevel); + this.RequiredPosition = requiredPos; + this.RequiredPositionRadius = requiredPosRadius; + } + + bool IsPositional() + { + return this.RequiredPosition.Initialized; + } + + void Reset() + { + this.LoadingBeforeRequiredPosition = false; + this.RequiredPositionReached = false; + } + + bool CheckPosition(Vector3f* player) + { + if (!this.IsPositional()) + return false; + + if (!this.RequiredPositionReached && player.Distance(&this.RequiredPosition) < this.RequiredPositionRadius) + { + WriteConsole("required position reached"); + this.RequiredPositionReached = true; + + if (this.LoadingBeforeRequiredPosition) + return true; + } + + return false; + } +} + +class OffsetDB +{ + private Offset[] _offsets; + + void Add(GameVersion ver, OffsetName name, uint offset) + { + _offsets ~= new Offset(ver, name, offset); + } + + uint Get(GameVersion ver, OffsetName name) + { + foreach (Offset offset; _offsets) + { + if (offset.Version == ver && offset.Name == name) + return offset.Offset; + } + + throw new Exception("Offset not in DB."); + } + + private class Offset + { + GameVersion Version; + OffsetName Name; + uint Offset; + + this(GameVersion ver, OffsetName name, uint offset) + { + this.Version = ver; + this.Name = name; + this.Offset = offset; + } + } +} + +struct Vector3f +{ + float X; + float Y; + float Z; + + bool Initialized; + + this(float x, float y, float z) + { + this.X = x; + this.Y = y; + this.Z = z; + this.Initialized = true; + } + + float Distance(const 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 sqrt(result); + } + + float DistanceXY(const Vector3f* other) + { + float result = (this.X - other.X) * (this.X - other.X) + + (this.Y - other.Y) * (this.Y - other.Y); + return sqrt(result); + } +} diff --git a/menl_hooks/hook.d b/menl_hooks/hook.d new file mode 100644 index 0000000..fc160b6 --- /dev/null +++ b/menl_hooks/hook.d @@ -0,0 +1,47 @@ +import std.c.windows.windows; +import std.c.string: memcpy; + +const int JMP_SIZE = 5; + +bool memcpy_protected(void* dest, void* src, size_t size) +{ + uint oldProtect; + if (VirtualProtect(dest, size, PAGE_EXECUTE_READWRITE, &oldProtect)) + { + memcpy(dest, src, size); + VirtualProtect(dest, size, oldProtect, &oldProtect); + return true; + } + + return false; +} + +void JMP(ubyte* src, void* dest, int nops) +{ + uint oldProtect; + if (VirtualProtect(src, JMP_SIZE+nops, PAGE_EXECUTE_READWRITE, &oldProtect)) + { + // JMP instruction + *src = 0xE9; + // encode the address + *cast(void**)(src+1) = cast(void*)(dest - (src+JMP_SIZE)); + + for (int i = 0; i < nops; i++) + *(src + JMP_SIZE + i) = 0x90; + + VirtualProtect(src, JMP_SIZE+nops, oldProtect, &oldProtect); + } +} + +void TrampolineHook(ubyte* src, ubyte* dest, ubyte* gate, int overwritten) +{ + uint oldProtect; + if (VirtualProtect(gate, overwritten, PAGE_EXECUTE_READWRITE, &oldProtect)) + { + memcpy(gate, src, overwritten); + VirtualProtect(gate, overwritten, oldProtect, &oldProtect); + } + + JMP(gate+overwritten, src+overwritten, 0); + JMP(src, dest, (overwritten > JMP_SIZE ? overwritten - JMP_SIZE : 0)); +} diff --git a/menl_hooks/menl_hooks.visualdproj b/menl_hooks/menl_hooks.visualdproj new file mode 100644 index 0000000..848f3df --- /dev/null +++ b/menl_hooks/menl_hooks.visualdproj @@ -0,0 +1,197 @@ + + {0595D913-5F62-4D51-928C-8742CDE2F243} + + 0 + 0 + 2 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 2.043 + 0 + 0 + 0 + $(DMDInstallDir)windows\bin\dmd.exe + + + $(ConfigurationName) + $(OutDir) + + + 0 + + + + + 0 + + + 1 + $(IntDir)\$(TargetName).json + 0 + + 0 + + 0 + 0 + 0 + + + + 0 + + 1 + $(VisualDInstallDir)cv2pdb\cv2pdb.exe + 0 + 0 + 0 + + + + + + + + $(SolutionDir)\bin\$(ProjectName).dll + 1 + + + + *.obj;*.cmd;*.build;*.json;*.dep + + + 0 + 0 + 2 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 2 + 0 + 0 + 0 + $(DMDInstallDir)windows\bin\dmd.exe + + + $(ConfigurationName) + $(OutDir) + + + 0 + + + + + 0 + + + 1 + $(IntDir)\$(TargetName).json + 0 + + 0 + + 0 + 0 + 0 + + + + 0 + + 0 + $(VisualDInstallDir)cv2pdb\cv2pdb.exe + 0 + 0 + 0 + + + + + + + + $(SolutionDir)\bin\$(ProjectName).dll + 1 + + + + *.obj;*.cmd;*.build;*.json;*.dep + + + + + + + + diff --git a/menl_hooks/win32.d b/menl_hooks/win32.d new file mode 100644 index 0000000..d518874 --- /dev/null +++ b/menl_hooks/win32.d @@ -0,0 +1,33 @@ +import std.c.windows.windows; + +extern(Windows) { +void OutputDebugStringA(LPCSTR lpPathName); +short GetAsyncKeyState(int vKey); + +const int PIPE_ACCESS_INBOUND = 0x00000001; +const int PIPE_ACCESS_OUTBOUND = 0x00000002; +const int PIPE_ACCESS_DUPLEX = 0x00000003; +const int PIPE_WAIT = 0x00000000; +const int PIPE_NOWAIT = 0x00000001; +const int PIPE_READMODE_BYTE = 0x00000000; +const int PIPE_READMODE_MESSAGE = 0x00000002; +const int PIPE_TYPE_BYTE = 0x00000000; +const int PIPE_TYPE_MESSAGE = 0x00000004; +const int PIPE_UNLIMITED_INSTANCES = 255; +const int ERROR_PIPE_LISTENING = 536; +const int ERROR_BROKEN_PIPE = 109; +const int ERROR_PIPE_CONNECTED = 535; + +HANDLE CreateNamedPipeA( + LPCSTR lpName, + DWORD dwOpenMode, + DWORD dwPipeMode, + DWORD nMaxInstances, + DWORD nOutBufferSize, + DWORD nInBufferSize, + DWORD nDefaultTimeOut, + LPSECURITY_ATTRIBUTES lpSecurityAttributes); + +bool ConnectNamedPipe(HANDLE hNamedPipe, LPOVERLAPPED lpOverlapped); +bool DisconnectNamedPipe(HANDLE hNamedPipe); +} diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000..4b3742a --- /dev/null +++ b/readme.txt @@ -0,0 +1,54 @@ +MENL (Mirror's Edge No Loads) is an automatic load time remover for Mirror's +Edge. It's both easier and more accurate than manually editing a video to cut +out loads. It is designed to be RTA Without Loads; only loading is removed. +Types of loading removed: static load screens (starting a level, dying), +mid-game loading (as seen after the Ropeburn death cutscene on some PCs), +level streaming (the thing it does in elevator rides and Out of Bounds +glitches). + +Requirements: + + LiveSplit 1.3+ (if enough people yell at me, I'll make a standalone version) + Mirror's Edge (PC) - Steam, Origin, DVD, or No-CD crack + +Install: + + Extract the contents of the zip file to your LiveSplit folder and overwrite + everything. + + 64-bit users only: Close LiveSplit and run LiveSplit32BitPatcher. This will not + be required in the next version of LiveSplit. + + Restart LiveSplit. Add MENL in LiveSplit's Layout Editor. + +Notes: + + If you need auto-splitting, you can use the one chillmastor made: + https://www.dropbox.com/s/98k4o1ssvqq5ad8/Autosplit.7z + + If you hold F10 while starting the game up, a debug console will appear. If + something isn't working you can copy the output of it. + +Changelog: + + 1.0 + First public release. Don't get mad if it makes the game crash :^) + +Authors: + + Fatalis - Code + chillmastor - Data Gathering, Testing + +Thanks: + + Testing - qqzzy, Keelshing + Game Info - nulaft, naechster + + +@fatalis_ +twitch.tv/fatalis_ +fatalis.twitch@gmail.com + +twitch.tv/chillmastor + +#mirrorsedge @ irc2.speedrunslive.com IRC