-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathLogic.cs
152 lines (130 loc) · 6.14 KB
/
Logic.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
using System.Threading.Tasks;
using System.Threading;
using LiveSplit.UI.Components;
using JHelper.Common.ProcessInterop;
using LiveSplit.Model;
using LiveSplit.SonicXShadowGenerations.Game;
using LiveSplit.SonicXShadowGenerations.Game.Shadow;
using LiveSplit.SonicXShadowGenerations.Game.Sonic;
using System.Diagnostics;
using System.Linq;
using System;
namespace LiveSplit.SonicXShadowGenerations;
internal partial class AutosplitterComponent : LogicComponent
{
/// <summary>
/// Name of the component as it appears in the LiveSplit UI.
/// </summary>
public override string ComponentName => "Sonic X Shadow Generations - Autosplitter";
private readonly Task autosplitterTask;
private readonly CancellationTokenSource cancelToken = new();
/// <summary>
/// Main task method for the autosplitter that monitors the game process and updates the LiveSplit timer based on in-game events.
/// </summary>
/// <param name="state">Current state of the LiveSplit timer.</param>
/// <param name="canceltoken">Token for cancelling the task.</param>
private void AutosplitterLogic(LiveSplitState state, CancellationToken canceltoken)
{
// Array of target game process names to hook into
string[] gameProcessNames = ["SONIC_GENERATIONS.exe", "SONIC_X_SHADOW_GENERATIONS.exe"];
// Interval for autosplitter updates (about 60 times per second)
TimeSpan updateInterval = TimeSpan.FromMilliseconds(1000d / 60d);
// Timer model connected to LiveSplit state, allowing timer control
TimerModel timer = new() { CurrentState = state };
// Stopwatch to track time for each update cycle
Stopwatch clock = new();
// Main loop that will continue running until cancellation is requested
while (!canceltoken.IsCancellationRequested)
{
// Try to hook into the game process
using ProcessMemory? process = ProcessHook(gameProcessNames);
// If the process is not available, wait and retry
if (process is null)
{
Task.Delay(1500, canceltoken).Wait(canceltoken);
continue;
}
// Attempt to locate necessary memory addresses in the hooked process
// This loop will retry if memory scanning fails, catching any exceptions
Memory? memory = null;
while (!canceltoken.IsCancellationRequested && process.IsOpen && memory is null)
{
try
{
memory = process.ProcessName switch
{
"SONIC_GENERATIONS.exe" => new MemorySonic(process),
_ => new MemoryShadow(process),
};
}
catch
{
Task.Delay(1500, canceltoken).Wait(canceltoken);
}
}
// If memory is still null, exit the loop to retry from the start
if (memory is null)
continue;
// Primary loop for updating and checking game memory while the process remains open
while (!canceltoken.IsCancellationRequested && process.IsOpen)
{
clock.Start();
// Memory update cycle for the current game process and settings
memory.Update(process, Settings);
// Timer logic to manage game-time, loading status, and potential resets or splits
if (timer.CurrentState.CurrentPhase == TimerPhase.Running || timer.CurrentState.CurrentPhase == TimerPhase.Paused)
{
// Check if the game is in a loading state and pause timer if so
bool? isLoading = memory.IsLoading(Settings);
if (isLoading is not null)
state.IsGameTimePaused = isLoading.Value;
// Update in-game time if available
TimeSpan? gameTime = memory.GameTime(Settings);
if (gameTime is not null)
timer.CurrentState.SetGameTime(gameTime.Value);
// Check if the game conditions require a timer reset or split
if (memory.Reset(Settings))
timer.Reset();
else if (memory.Split(Settings))
timer.Split();
}
// Start the timer if game start conditions are met and the timer is not running
if (timer.CurrentState.CurrentPhase == TimerPhase.NotRunning && memory.Start(Settings))
{
timer.Start();
state.IsGameTimePaused = true;
// Re-check if the game is loading to update the paused status
bool? isLoading = memory.IsLoading(Settings);
if (isLoading is not null)
state.IsGameTimePaused = isLoading.Value;
}
// Calculate elapsed time for this update cycle
TimeSpan elapsed = clock.Elapsed;
clock.Reset();
// Wait if the cycle completed faster than the desired update interval
if (elapsed < updateInterval)
canceltoken.WaitHandle.WaitOne(updateInterval - elapsed);
}
}
}
/// <summary>
/// Attempts to hook into one of the target game processes specified in <paramref name="exeNames"/>.
/// </summary>
/// <param name="exeNames">Array of executable names to try hooking into.</param>
/// <returns>A <see cref="ProcessMemory"/> object representing the hooked process, or null if no process could be hooked.</returns>
private static ProcessMemory? ProcessHook(string[] exeNames)
{
try
{
// Attempt to hook into each process in exeNames and return the first successful match
return exeNames
.Select(ProcessMemory.HookProcess)
.FirstOrDefault(p => p is not null);
}
catch
{
// Return null if any errors occur during hooking
return null;
}
}
}