Skip to content

Commit 3b2bb89

Browse files
committed
Refactors and redesigns
- Added an option to count server initialization ticks - Added an option to add extra ticks per game load - Added an option to Auto-Split instead of Auto-Resetting the timer - Redesigned timer monitoring to no longer rely on memory injection - Fixed some issues with time keeping, particularly with decimal precision - Split Auto Start / Stop / Reset into 3 separate settings - Cleaned up Setting-handling code - Minor adjustments to settings UI
1 parent 1b4b423 commit 3b2bb89

19 files changed

+651
-592
lines changed

Extensions/ProcExtensions.cs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using LiveSplit.ComponentUtil;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.ComponentModel;
5+
using System.Diagnostics;
6+
using System.Linq;
7+
using System.Runtime.InteropServices;
8+
using System.Text;
9+
using System.Threading.Tasks;
10+
11+
namespace LiveSplit.SourceSplit.Extensions
12+
{
13+
static class ProcExtensions
14+
{
15+
16+
public static ProcessModuleWow64Safe[] ModulesWow64SafeNoCache(this Process p)
17+
{
18+
const int LIST_MODULES_ALL = 3;
19+
const int MAX_PATH = 260;
20+
21+
var hModules = new IntPtr[1024];
22+
23+
uint cb = (uint)IntPtr.Size * (uint)hModules.Length;
24+
uint cbNeeded;
25+
26+
if (!WinAPI.EnumProcessModulesEx(p.Handle, hModules, cb, out cbNeeded, LIST_MODULES_ALL))
27+
throw new Win32Exception();
28+
uint numMods = cbNeeded / (uint)IntPtr.Size;
29+
30+
var ret = new List<ProcessModuleWow64Safe>();
31+
32+
// everything below is fairly expensive, which is why we cache!
33+
var sb = new StringBuilder(MAX_PATH);
34+
for (int i = 0; i < numMods; i++)
35+
{
36+
sb.Clear();
37+
if (WinAPI.GetModuleFileNameEx(p.Handle, hModules[i], sb, (uint)sb.Capacity) == 0)
38+
throw new Win32Exception();
39+
string fileName = sb.ToString();
40+
41+
sb.Clear();
42+
if (WinAPI.GetModuleBaseName(p.Handle, hModules[i], sb, (uint)sb.Capacity) == 0)
43+
throw new Win32Exception();
44+
string baseName = sb.ToString();
45+
46+
var moduleInfo = new WinAPI.MODULEINFO();
47+
if (!WinAPI.GetModuleInformation(p.Handle, hModules[i], out moduleInfo, (uint)Marshal.SizeOf(moduleInfo)))
48+
throw new Win32Exception();
49+
50+
ret.Add(new ProcessModuleWow64Safe()
51+
{
52+
FileName = fileName,
53+
BaseAddress = moduleInfo.lpBaseOfDll,
54+
ModuleMemorySize = (int)moduleInfo.SizeOfImage,
55+
EntryPointAddress = moduleInfo.EntryPoint,
56+
ModuleName = baseName
57+
});
58+
}
59+
60+
return ret.ToArray();
61+
}
62+
}
63+
}

GameMemory.cs

Lines changed: 35 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -69,17 +69,8 @@ class GameMemory
6969

7070
private SourceSplitSettings _settings;
7171

72-
private ValueWatcher<int> _custTimeCountWatcher;
72+
private ValueWatcher<long> _custTimeCountWatcher;
7373
private IntPtr _custTimeCountPtr;
74-
private Detour _custTimeInjection;
75-
private void ResetTimeInjection(Process game)
76-
{
77-
if (game != null)
78-
{
79-
_custTimeInjection?.Restore(game);
80-
AppData.DeleteData("Detours", "host_runframe");
81-
}
82-
}
8374

8475
public GameState _state;
8576

@@ -497,8 +488,6 @@ void MemoryReadThread(CancellationTokenSource cts)
497488
{
498489
try
499490
{
500-
ResetTimeInjection(game);
501-
502491
GameOffsets offsets;
503492
while (!this.TryGetGameProcess(out game, out offsets))
504493
{
@@ -513,25 +502,21 @@ void MemoryReadThread(CancellationTokenSource cts)
513502
if (cts.IsCancellationRequested)
514503
goto ret;
515504
}
516-
catch (Win32Exception win32ex)
505+
catch (Exception ex) when (ex is InvalidOperationException || ex is Win32Exception)
517506
{
518-
Trace.WriteLine(win32ex.ToString());
507+
Trace.WriteLine(ex.ToString());
519508
Thread.Sleep(1000);
520509
}
521-
catch (Exception ex) // probably a Win32Exception on access denied to a process
510+
catch (Exception ex)
522511
{
523512
Trace.WriteLine(ex.ToString());
513+
new ErrorDialog($"Main:\n{ex}\n\nInner:\n{ex.InnerException?.ToString()}");
524514

525-
var errorWindow = new ErrorDialog($"Main:\n{ex}\n\nInner:\n{ex.InnerException?.ToString()}");
526-
errorWindow.ShowDialog();
527515
Thread.Sleep(1000);
528-
529-
ResetTimeInjection(game);
530516
}
531517
}
532518

533519
ret:
534-
ResetTimeInjection(game);
535520
Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.Normal;
536521
timeEndPeriod(1);
537522
}
@@ -568,8 +553,8 @@ bool TryGetGameProcess(out Process p, out GameOffsets offsets)
568553
return false;
569554

570555
// process is up, check if engine and server are both loaded yet
571-
ProcessModuleWow64Safe engine = p.ModulesWow64Safe().FirstOrDefault(x => x.ModuleName.ToLower() == "engine.dll");
572-
ProcessModuleWow64Safe server = p.ModulesWow64Safe().FirstOrDefault(x => x.ModuleName.ToLower() == "server.dll");
556+
ProcessModuleWow64Safe engine = p.ModulesWow64SafeNoCache().FirstOrDefault(x => x.ModuleName.ToLower() == "engine.dll");
557+
ProcessModuleWow64Safe server = p.ModulesWow64SafeNoCache().FirstOrDefault(x => x.ModuleName.ToLower() == "server.dll");
573558

574559
if (engine == null || server == null)
575560
return false;
@@ -599,34 +584,8 @@ bool scanForPtr(ref IntPtr ptr, SigScanTarget target, SignatureScanner scanner,
599584
&& !scanForPtr(ref offsets.SignOnStatePtr, _signOnStateTarget2, scanner, "CBaseClientState::m_nSignonState", "2"))
600585
return false;
601586

602-
#region HOST_RUNFRAME TICK COUNT DETOUR
587+
#region HOST_RUNFRAME TICK COUNT
603588

604-
const int timeCountPtrOff = 0x8;
605-
606-
string serialzedFilePath = AppData.GetDataPath("Detours", "host_runframe");
607-
_custTimeCountWatcher = new ValueWatcher<int>(0);
608-
if (File.Exists(serialzedFilePath))
609-
{
610-
Stream dStream = File.Open(serialzedFilePath, FileMode.Open);
611-
try
612-
{
613-
BinaryFormatter dBinary = new BinaryFormatter();
614-
dBinary.Binder = new SerialBinder();
615-
Detour dDetour = (Detour)dBinary.Deserialize(dStream);
616-
bool good = dDetour.VerifyIntegrity(p);
617-
if (good)
618-
{
619-
_custTimeInjection = dDetour;
620-
_custTimeCountPtr = _custTimeInjection.Destination - timeCountPtrOff;
621-
goto skipTimeHooks;
622-
}
623-
}
624-
finally
625-
{
626-
dStream.Close();
627-
}
628-
}
629-
630589
// find the beginning of _host_runframe
631590
// find string pointer and reference
632591
SigScanTarget _hrfStringTarg = new SigScanTarget("_Host_RunFrame (top): _heapchk() != _HEAPOK\n".ConvertToHex() + "00");
@@ -640,82 +599,33 @@ bool scanForPtr(ref IntPtr ptr, SigScanTarget target, SignatureScanner scanner,
640599
"host_runframe target jump"))
641600
return false;
642601

643-
// we can't detour the jl directly due to weird performance issues so back track until we
644-
// found our target cmp instruction
645-
646-
// find out where the jl goes to
602+
// find out where the jl goes to, which should be the top of the update loop
647603
IntPtr loopTo = _hrfStringPtr + p.ReadValue<int>(_hrfStringPtr + 0x2) + 0x6;
648-
// find cmp instruction
649-
int i = 0;
650-
// should never be more than 15 bytes away...
651-
for (i = 0; i < 15; i++)
652-
{
653-
byte curByte = p.ReadValue<byte>(_hrfStringPtr - i);
654-
// we are cmp'ing 2 registeres normally, so use 0x39 and 0x3B
655-
if (new byte[] { 0x39, 0x3B }.Contains(curByte))
656-
{
657-
byte prev = p.ReadValue<byte>(_hrfStringPtr - i - 1);
658-
// ignore this byte if this is part of a mov
659-
if ((prev <= 0x8E && prev >= 0x89))
660-
continue;
661-
662-
_hrfStringPtr -= i;
663-
Debug.WriteLine($"host_runframe target cmp at 0x{_hrfStringPtr.ToString("X")}");
664-
goto success;
665-
}
666-
}
667-
return false;
668604

669-
success:
670-
// allocate memory for our detour
671-
IntPtr workSpace = p.AllocateMemory(0x100);
672-
// bytes of the ptr to our custom tick count var
673-
byte[] timePtrBytes = BitConverter.GetBytes(workSpace.ToInt64());
674-
_custTimeInjection = new Detour(
675-
p,
676-
_hrfStringPtr,
677-
workSpace + timeCountPtrOff,
678-
6 + i,
679-
new byte[]
680-
{
681-
// inc [tick count val]
682-
0xFF, 0x05, timePtrBytes[0], timePtrBytes[1], timePtrBytes[2], timePtrBytes[3],
683-
},
684-
true);
685-
// because the detour just simply copies bytes over, we'll have to rewrite the jl instruction
686-
// to correct the offset back to the start of the loop
687-
// bytes of the ptr from the detoured jl to the start of the loop
688-
byte[] loopToBytes = BitConverter.GetBytes((int)loopTo - (int)(workSpace + 0x8 + 0x6 + i + 0x6));
689-
p.WriteBytes(workSpace + 0x8 + 6 + i, new byte[]
690-
{
691-
0x0F, 0x8C, loopToBytes[0], loopToBytes[1], loopToBytes[2], loopToBytes[3],
692-
});
693-
// final memory layout should look like this:
694-
// workspace + 0x0 [tick count val]
695-
// workspace + 0x8 inc [tick count val]
696-
// workspace + 0xE (cmp and along with any other bytes in between that and the jl)
697-
// workspace + 0xE + i jl [loop start]
698-
// workspace + 0xE + i + 6 jmp [loop end]
699-
_custTimeCountPtr = workSpace;
700-
701-
// serialize data and store it off in case livesplit crashes and we can't restore the original bytes
605+
while ((long)loopTo <= (long)_hrfStringPtr)
702606
{
703-
Stream s = File.Create(serialzedFilePath);
704-
try
705-
{
706-
BinaryFormatter bf = new BinaryFormatter();
707-
bf.Binder = new SerialBinder();
708-
bf.Serialize(s, _custTimeInjection);
709-
}
710-
finally
607+
loopTo = loopTo + 1;
608+
uint candidateHostFrameCount = p.ReadValue<uint>(loopTo);
609+
610+
if (scanner.IsWithin(candidateHostFrameCount))
711611
{
712-
s.Close();
612+
for (int i = 1; i <= 2; i++)
613+
{
614+
uint candidateNextPtr = p.ReadValue<uint>(loopTo + 4 + i);
615+
if (scanner.IsWithin(candidateNextPtr) && candidateNextPtr - candidateHostFrameCount <= 0x8)
616+
{
617+
_custTimeCountPtr = (IntPtr)candidateHostFrameCount;
618+
Debug.WriteLine($"host_runframe host_tickcount ptr is 0x{_custTimeCountPtr.ToString("X")}");
619+
goto skipTimeHooks;
620+
}
621+
}
713622
}
714623

715624
}
625+
return false;
716626

717627
skipTimeHooks:
718-
628+
_custTimeCountWatcher = new ValueWatcher<long>(0);
719629
#endregion
720630

721631
// get the game dir now to evaluate game-specific stuff
@@ -742,7 +652,7 @@ bool scanForPtr(ref IntPtr ptr, SigScanTarget target, SignatureScanner scanner,
742652

743653
#region CLIENT
744654
// optional client fade list
745-
ProcessModuleWow64Safe client = p.ModulesWow64Safe().FirstOrDefault(x => x.ModuleName.ToLower() == "client.dll");
655+
ProcessModuleWow64Safe client = p.ModulesWow64SafeNoCache().FirstOrDefault(x => x.ModuleName.ToLower() == "client.dll");
746656
if (client != null)
747657
{
748658
var clientScanner = new SignatureScanner(p, client.BaseAddress, client.ModuleMemorySize);
@@ -1109,7 +1019,7 @@ void CheckGameState(GameState state)
11091019
{
11101020
// note: seems to be slow sometimes. ~3ms
11111021

1112-
if (state.TickCount > 0)
1022+
if (_settings.ServerInitialTicks.Value || state.TickCount > 0)
11131023
{
11141024
if (state.ServerState.Current == ServerState.Paused)
11151025
this.SendMiscTimeEvent(_custTimeCountWatcher.Current - _custTimeCountWatcher.Old, MiscTimeType.PauseTime);
@@ -1194,7 +1104,7 @@ void CheckGameState(GameState state)
11941104
this.SendNewGameStartedEvent(levelName);
11951105

11961106
if (!string.IsNullOrWhiteSpace(levelName) &&
1197-
(levelName == _settings.StartMap
1107+
(levelName == _settings.StartMap.Value
11981108
|| state.GameSupport.StartOnFirstLoadMaps.Contains(levelName)
11991109
|| state.GameSupport.AdditionalGameSupport.Any(x => x.StartOnFirstLoadMaps.Contains(levelName))))
12001110
{
@@ -1262,13 +1172,13 @@ public void SendMapChangedEvent(string mapName, string prevMapName, bool isGener
12621172

12631173
public class SessionTicksUpdateEventArgs : EventArgs
12641174
{
1265-
public int TickDifference { get; private set; }
1266-
public SessionTicksUpdateEventArgs(int tickDifference)
1175+
public long TickDifference { get; private set; }
1176+
public SessionTicksUpdateEventArgs(long tickDifference)
12671177
{
12681178
this.TickDifference = tickDifference;
12691179
}
12701180
}
1271-
public void SendSessionTimeUpdateEvent(int tickDifference)
1181+
public void SendSessionTimeUpdateEvent(long tickDifference)
12721182
{
12731183
// note: sometimes this takes a few ms
12741184
_uiThread.Post(d => {
@@ -1336,9 +1246,9 @@ public void SendNewGameStartedEvent(string map)
13361246

13371247
public class MiscTimeEventArgs : EventArgs
13381248
{
1339-
public int TickDifference { get; private set; }
1249+
public long TickDifference { get; private set; }
13401250
public MiscTimeType Type { get; private set; }
1341-
public MiscTimeEventArgs(int tickDiff, MiscTimeType type)
1251+
public MiscTimeEventArgs(long tickDiff, MiscTimeType type)
13421252
{
13431253
this.TickDifference = tickDiff;
13441254
this.Type = type;
@@ -1351,7 +1261,7 @@ public enum MiscTimeType
13511261
PauseTime,
13521262
ClientDisconnectTime
13531263
}
1354-
public void SendMiscTimeEvent(int tickDiff, MiscTimeType type)
1264+
public void SendMiscTimeEvent(long tickDiff, MiscTimeType type)
13551265
{
13561266
_uiThread.Post(d => {
13571267
this.OnMiscTime?.Invoke(this, new MiscTimeEventArgs(tickDiff, type));

GameSpecific/PortalMods/PortalMods_TheFlashVersion.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Diagnostics;
33
using System.Linq;
44
using LiveSplit.ComponentUtil;
5+
using LiveSplit.SourceSplit.Extensions;
56

67
namespace LiveSplit.SourceSplit.GameSpecific
78
{
@@ -26,7 +27,7 @@ public PortalMods_TheFlashVersion()
2627

2728
public override void OnGameAttached(GameState state)
2829
{
29-
ProcessModuleWow64Safe server = state.GameProcess.ModulesWow64Safe().FirstOrDefault(x => x.ModuleName.ToLower() == "server.dll");
30+
ProcessModuleWow64Safe server = state.GameProcess.ModulesWow64SafeNoCache().FirstOrDefault(x => x.ModuleName.ToLower() == "server.dll");
3031
Trace.Assert(server != null);
3132

3233
var scanner = new SignatureScanner(state.GameProcess, server.BaseAddress, server.ModuleMemorySize);

GameState.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,7 @@ public int GetIndexOfEHANDLE(uint EHANDLE)
384384

385385
public ProcessModuleWow64Safe GetModule(string name)
386386
{
387-
var proc = GameProcess.ModulesWow64Safe().FirstOrDefault(x => x.ModuleName.ToLower() == name.ToLower());
387+
var proc = GameProcess.ModulesWow64SafeNoCache().FirstOrDefault(x => x.ModuleName.ToLower() == name.ToLower());
388388
Trace.Assert(proc != null);
389389
return proc;
390390
}

LiveSplit.SourceSplit.csproj

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@
5656
</Reference>
5757
</ItemGroup>
5858
<ItemGroup>
59-
<Compile Include="Utils\Detour.cs" />
59+
<Compile Include="Extensions\ProcExtensions.cs" />
60+
<Compile Include="SettingEntry.cs" />
6061
<Compile Include="GameSpecific\HL2Mods\HL2Mods_SouthernmostCombine.cs" />
61-
<Compile Include="Utils\AppData.cs" />
6262
<Compile Include="Utils\CustomCommand.cs" />
6363
<Compile Include="Extensions\NumericsExtensions.cs" />
6464
<Compile Include="Extensions\SignatureScannerExtensions.cs" />
@@ -162,6 +162,7 @@
162162
<Compile Include="Utils\Util.cs" />
163163
<Compile Include="Utils\ValueWatcher.cs" />
164164
<Compile Include="Utils\WinAPI.cs" />
165+
<Compile Include="Utils\XMLOperations.cs" />
165166
</ItemGroup>
166167
<ItemGroup>
167168
<EmbeddedResource Include="MapTimesForm.resx">

Properties/AssemblyInfo.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,5 @@
3131
// You can specify all the values or you can default the Build and Revision Numbers
3232
// by using the '*' as shown below:
3333
// [assembly: AssemblyVersion("1.0.*")]
34-
[assembly: AssemblyVersion("3.2.2.2")]
35-
[assembly: AssemblyFileVersion("3.2.2.2")]
34+
[assembly: AssemblyVersion("3.2.3")]
35+
[assembly: AssemblyFileVersion("3.2.3")]

0 commit comments

Comments
 (0)