Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions Component.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
using LiveSplit.Model;
using LiveSplit.Options;
using LiveSplit.SonicFrontiers;
using LiveSplit.UI;
using LiveSplit.UI.Components;
using LiveSplit.SonicFrontiers;
using System;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Xml;
Expand All @@ -20,8 +23,15 @@ public SonicFrontiersComponent(LiveSplitState state)
{
AutosplitterLogic(state, cancelToken.Token);
}
catch { }
});
catch (OperationCanceledException e) {
File.WriteAllText("LiveSplit.SonicFrontiers.ErrorLog.txt", "Cancelled intentionally\n" + e.StackTrace + "\n" + e.Message);
Log.Error("Something broke :(\n" + e.StackTrace + "\n" + e.Message);
}
catch (Exception e) {
File.WriteAllText("LiveSplit.SonicFrontiers.ErrorLog.txt", "Something broke :(\n" + e.StackTrace + "\n" + e.Message);
Log.Error("Something broke :(\n" + e.StackTrace + "\n" + e.Message);
}
});

if (state.CurrentTimingMethod == TimingMethod.RealTime)
AskGameTime(state);
Expand Down
Binary file modified Components/LiveSplit.SonicFrontiers.dll
Binary file not shown.
8 changes: 8 additions & 0 deletions Components/update.LiveSplit.SonicFrontiers.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<updates>
<update version="2.0.4">
<files>
<file path="Components/LiveSplit.SonicFrontiers.dll" status="changed"/>
</files>
<changelog>
<change>Various bugfixes</change>
</changelog>
</update>
<update version="2.0.3">
<files>
<file path="Components/LiveSplit.SonicFrontiers.dll" status="changed"/>
Expand Down
2,011 changes: 976 additions & 1,035 deletions Game/GameMemory.cs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion JHelper.ProcessMemory
2 changes: 1 addition & 1 deletion LiveSplit.SonicFrontiers.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<OutputType>Library</OutputType>
<ImplicitUsings>disable</ImplicitUsings>
<PlatformTarget>AnyCPU</PlatformTarget>
<Configurations>Release</Configurations>
<Configurations>Release;Debug</Configurations>
<ProduceReferenceAssembly>False</ProduceReferenceAssembly>
<GenerateDocumentationFile>False</GenerateDocumentationFile>
<SignAssembly>False</SignAssembly>
Expand Down
7 changes: 5 additions & 2 deletions LiveSplit.SonicFrontiers.sln
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.3.11520.95 d18.3
# Visual Studio Version 17
VisualStudioVersion = 17.14.36327.8 d17.14
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiveSplit.SonicFrontiers", "LiveSplit.SonicFrontiers.csproj", "{E62550EC-582A-4FBF-B139-EAF962D91F08}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{E62550EC-582A-4FBF-B139-EAF962D91F08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E62550EC-582A-4FBF-B139-EAF962D91F08}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E62550EC-582A-4FBF-B139-EAF962D91F08}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E62550EC-582A-4FBF-B139-EAF962D91F08}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
Expand Down
262 changes: 124 additions & 138 deletions Logic.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using JHelper.Common.ProcessInterop;
using LiveSplit.Model;
using LiveSplit.Options;
using LiveSplit.UI.Components;
using System;
using System.Collections.Generic;
Expand All @@ -11,142 +12,127 @@

namespace LiveSplit.SonicFrontiers;

internal partial class SonicFrontiersComponent : 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 = ["SonicFrontiers.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 = new Memory(process, state);
}
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, state);

// 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.IGT.Current;
if (gameTime is not null && memory.GameMode.Current != SonicFrontiers.GameMode.Story)
timer.CurrentState.SetGameTime(gameTime.Value);

// Check if the game conditions require a timer reset or split
/*
if (memory.Reset(Settings))
timer.Reset();
*/
else if (timer.CurrentState.CurrentTime.RealTime > new TimeSpan(0, 0, 2) && 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))
{
//band-aid for igt preservation
memory.AccumulatedIGT = TimeSpan.Zero;
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;
}
}
internal partial class SonicFrontiersComponent : LogicComponent {
/// <summary>
/// Name of the component as it appears in the LiveSplit UI.
/// </summary>
public override string ComponentName => "Sonic Frontiers - 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 = ["SonicFrontiers.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 = new Memory(process, state);
Log.Info("memory created");
}
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, state);

// 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();
if (gameTime is not null && memory.GameMode.Current != SonicFrontiers.GameMode.Story)
timer.CurrentState.SetGameTime(gameTime.Value);

// Check if the game conditions require a timer reset or split
/*
if (memory.Reset(Settings))
timer.Reset();
*/
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)) {
//band-aid for igt preservation
memory.AccumulatedIGT = TimeSpan.Zero;
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;
}
}
}
4 changes: 2 additions & 2 deletions Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]

[assembly: AssemblyVersion("2.0.3")]
[assembly: AssemblyFileVersion("2.0.3")]
[assembly: AssemblyVersion("2.0.4")]
[assembly: AssemblyFileVersion("2.0.4")]