Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PreGameContext read implementation #328

Merged
merged 1 commit into from
Aug 7, 2023
Merged
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
2 changes: 1 addition & 1 deletion Daybreak/Daybreak.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<LangVersion>preview</LangVersion>
<ApplicationIcon>Daybreak.ico</ApplicationIcon>
<IncludePackageReferencesDuringMarkupCompilation>true</IncludePackageReferencesDuringMarkupCompilation>
<Version>0.9.8.92</Version>
<Version>0.9.8.93</Version>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<UserSecretsId>cfb2a489-db80-448d-a969-80270f314c46</UserSecretsId>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
Expand Down
9 changes: 9 additions & 0 deletions Daybreak/Models/Guildwars/PreGameData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Collections.Generic;

namespace Daybreak.Models.Guildwars;

public sealed class PreGameData
{
public int ChosenCharacterIndex { get; init; }
public List<string>? Characters { get; init; }
}
5 changes: 5 additions & 0 deletions Daybreak/Models/Interop/GuildwarsPointer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,9 @@ public bool IsValid()
{
return this.Address != 0x0;
}

public override string ToString()
{
return this.Address.ToString("X");
}
}
15 changes: 15 additions & 0 deletions Daybreak/Models/Interop/LoginCharacterContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;

namespace Daybreak.Models.Interop;

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public readonly struct LoginCharacterContext
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 44)]
public readonly byte[] ContextBytes;

public string CharacterName =>
new(Encoding.Unicode.GetString(this.ContextBytes, 4, 40).TakeWhile(c => char.IsLetterOrDigit(c) || char.IsWhiteSpace(c)).ToArray());
}
22 changes: 22 additions & 0 deletions Daybreak/Models/Interop/PreGameContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Runtime.InteropServices;

namespace Daybreak.Models.Interop;

[StructLayout(LayoutKind.Explicit)]
public readonly struct PreGameContext
{
[FieldOffset(0x0000)]
public readonly uint FrameId;

[FieldOffset(0x0124)]
public readonly uint ChosenCharacterIndex;

[FieldOffset(0x0140)]
public readonly uint LoginSelectionIndex;

[FieldOffset(0x0144)]
public readonly uint Index2;

[FieldOffset(0x0148)]
public readonly GuildwarsArray<LoginCharacterContext> LoginCharacters;
}
6 changes: 6 additions & 0 deletions Daybreak/Services/Scanner/GuildwarsMemoryCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public sealed class GuildwarsMemoryCache : IGuildwarsMemoryCache
private readonly CachedData<UserData?> userDataCache = new();
private readonly CachedData<MainPlayerData?> mainPlayerDataCache = new();
private readonly CachedData<ConnectionData?> connectionDataCache = new();
private readonly CachedData<PreGameData?> preGameDataCache = new();

public GuildwarsMemoryCache(
IGuildwarsMemoryReader guildwarsMemoryReader,
Expand Down Expand Up @@ -82,6 +83,11 @@ public GuildwarsMemoryCache(
return this.ReadDataInternal(this.connectionDataCache, this.guildwarsMemoryReader.ReadConnectionData, cancellationToken);
}

public Task<PreGameData?> ReadPreGameData(CancellationToken cancellationToken)
{
return this.ReadDataInternal(this.preGameDataCache, this.guildwarsMemoryReader.ReadPreGameData, cancellationToken);
}

private async Task<T?> ReadDataInternal<T>(CachedData<T?> cachedData, Func<CancellationToken, Task<T?>> task, CancellationToken cancellationToken)
{
if (DateTime.Now - cachedData.SetTime <= TimeSpan.FromMilliseconds(this.liveOptions.Value.MemoryReaderFrequency))
Expand Down
64 changes: 53 additions & 11 deletions Daybreak/Services/Scanner/GuildwarsMemoryReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ public sealed class GuildwarsMemoryReader : IGuildwarsMemoryReader

private uint playerIdPointer;
private uint entityArrayPointer;
private uint titleDataPointer;
private uint targetIdPointer;
private uint instanceInfoPointer;
private uint preGameContextPointer;

public GuildwarsMemoryReader(
IApplicationLauncher applicationLauncher,
Expand Down Expand Up @@ -187,6 +187,16 @@ public void Stop()
return Task.Run(() => this.SafeReadGameMemory(this.ReadConnectionData), cancellationToken);
}

public Task<PreGameData?> ReadPreGameData(CancellationToken cancellationToken)
{
if (this.memoryScanner.Scanning is false)
{
return Task.FromResult<PreGameData?>(default);
}

return Task.Run(() => this.SafeReadGameMemory(this.ReadPreGameData), cancellationToken);
}

private async Task InitializeSafe(Process process, ScopedLogger<GuildwarsMemoryReader> scopedLogger)
{
if (this.memoryScanner.Process is null ||
Expand Down Expand Up @@ -636,6 +646,38 @@ private async Task ResilientBeginScanner(ScopedLogger<GuildwarsMemoryReader> sco
return new ConnectionData { IPAddress = ipAddress };
}

private PreGameData? ReadPreGameData()
{
var preGameContextPtr = this.GetPreGameContextPointer();
if (preGameContextPtr == 0)
{
return default;
}

var preGameContext = this.memoryScanner.ReadPtrChain<PreGameContext>(preGameContextPtr, 0x0, 0x0, 0x0);
if (preGameContext.LoginCharacters.Capacity > 20 ||
preGameContext.LoginCharacters.Size > 20 ||
!preGameContext.LoginCharacters.Buffer.IsValid())
{
return default;
}

// Detect corrupt memory by checking that player names are longer than 3 characters
var loginCharacters = this.memoryScanner.ReadArray(preGameContext.LoginCharacters);
if (loginCharacters.Any(c => c.CharacterName != "" && c.CharacterName.Length < 2))
{
return default;
}

return new PreGameData
{
ChosenCharacterIndex = preGameContext.LoginSelectionIndex > 0 && preGameContext.LoginSelectionIndex < loginCharacters.Length ?
(int)preGameContext.LoginSelectionIndex :
-1,
Characters = loginCharacters.Select(c => c.CharacterName).ToList()
};
}

private uint GetPlayerIdPointer()
{
if (this.playerIdPointer == 0)
Expand All @@ -656,16 +698,6 @@ private uint GetEntityArrayPointer()
return this.entityArrayPointer;
}

private uint GetTitleDataPointer()
{
if (this.titleDataPointer == 0)
{
this.titleDataPointer = this.memoryScanner.ScanForAssertion("p:\\code\\gw\\const\\consttitle.cpp", "index < arrsize(s_titleClientData)") + 0x12;
}

return this.titleDataPointer;
}

private uint GetTargetIdPointer()
{
if (this.targetIdPointer == 0)
Expand All @@ -686,6 +718,16 @@ private uint GetInstanceInfoPointer()
return this.instanceInfoPointer;
}

private uint GetPreGameContextPointer()
{
if (this.preGameContextPointer == 0)
{
this.preGameContextPointer = this.memoryScanner.ScanForAssertion("p:\\code\\gw\\ui\\uipregame.cpp", "!s_scene") + 0x34;
}

return this.preGameContextPointer;
}

private Bag? GetBag(GuildwarsPointer<BagInfo> bagInfoPtr, uint expectedBagType, bool returnEmptyBag)
{
var bagInfo = this.memoryScanner.Read(bagInfoPtr);
Expand Down
1 change: 1 addition & 0 deletions Daybreak/Services/Scanner/IGuildwarsMemoryCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ public interface IGuildwarsMemoryCache
Task<UserData?> ReadUserData(CancellationToken cancellationToken);
Task<MainPlayerData?> ReadMainPlayerData(CancellationToken cancellationToken);
Task<ConnectionData?> ReadConnectionData(CancellationToken cancellationToken);
Task<PreGameData?> ReadPreGameData(CancellationToken cancellationToken);
}
1 change: 1 addition & 0 deletions Daybreak/Services/Scanner/IGuildwarsMemoryReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ public interface IGuildwarsMemoryReader
Task<SessionData?> ReadSessionData(CancellationToken cancellationToken);
Task<MainPlayerData?> ReadMainPlayerData(CancellationToken cancellationToken);
Task<ConnectionData?> ReadConnectionData(CancellationToken cancellationToken);
Task<PreGameData?> ReadPreGameData(CancellationToken cancellationToken);
void Stop();
}
6 changes: 4 additions & 2 deletions Daybreak/Services/Scanner/MemoryScanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ public uint ScanForAssertion(string? assertionFile, string? assertionMessage)
}

var assertionBytes = new byte[] { 0xBA, 0x0, 0x0, 0x0, 0x0, 0xB9, 0x0, 0x0, 0x0, 0x0 };
var assertionMask = "x????x????";
var assertionMask = new StringBuilder("x????x????");
if (assertionMessage is not null)
{
var assertionMessageBytes = Encoding.ASCII.GetBytes(assertionMessage);
Expand All @@ -318,6 +318,7 @@ public uint ScanForAssertion(string? assertionFile, string? assertionMessage)
assertionBytes[7] = (byte)(rdataPtr >> 8);
assertionBytes[8] = (byte)(rdataPtr >> 16);
assertionBytes[9] = (byte)(rdataPtr >> 24);
assertionMask[6] = assertionMask[7] = assertionMask[8] = assertionMask[9] = 'x';
}

if (assertionFile is not null)
Expand All @@ -336,9 +337,10 @@ public uint ScanForAssertion(string? assertionFile, string? assertionMessage)
assertionBytes[2] = (byte)(rdataPtr >> 8);
assertionBytes[3] = (byte)(rdataPtr >> 16);
assertionBytes[4] = (byte)(rdataPtr >> 24);
assertionMask[1] = assertionMask[2] = assertionMask[3] = assertionMask[4] = 'x';
}

return this.ScanForPtr(assertionBytes, assertionMask, false);
return this.ScanForPtr(assertionBytes, assertionMask.ToString(), false);
}

private byte[]? ReadBytesNonLocking(uint address, uint size)
Expand Down
20 changes: 19 additions & 1 deletion Daybreak/Services/Toolbox/ToolboxService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Daybreak.Exceptions;
using Daybreak.Models.Progress;
using Daybreak.Services.Downloads;
using Daybreak.Services.Scanner;
using Daybreak.Utils;
using Microsoft.Extensions.Logging;
using Microsoft.Win32;
Expand All @@ -13,6 +14,7 @@
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Daybreak.Services.Toolbox;
Expand All @@ -24,6 +26,7 @@ public sealed class ToolboxService : IToolboxService
private const string ExecutableName = "GWToolboxpp.exe";
private const string ToolboxDestinationDirectory = "GWToolbox";

private readonly IGuildwarsMemoryReader guildwarsMemoryReader;
private readonly IDownloadService downloadService;
private readonly ILiveOptions<LauncherOptions> launcherOptions;
private readonly ILiveUpdateableOptions<ToolboxOptions> toolboxOptions;
Expand All @@ -41,11 +44,13 @@ public bool IsEnabled
public bool IsInstalled => File.Exists(this.toolboxOptions.Value.Path);

public ToolboxService(
IGuildwarsMemoryReader guildwarsMemoryReader,
IDownloadService downloadService,
ILiveOptions<LauncherOptions> launcherOptions,
ILiveUpdateableOptions<ToolboxOptions> toolboxOptions,
ILogger<ToolboxService> logger)
{
this.guildwarsMemoryReader = guildwarsMemoryReader.ThrowIfNull();
this.downloadService = downloadService.ThrowIfNull();
this.launcherOptions = launcherOptions.ThrowIfNull();
this.toolboxOptions = toolboxOptions.ThrowIfNull();
Expand Down Expand Up @@ -138,7 +143,20 @@ private async Task LaunchToolbox()
return;
}

await Task.Delay(5000);
// Try to detect when Guildwars has successfully launched and is on character selection screen
for (var i = 0; i < 10; i++)
{
await this.guildwarsMemoryReader.EnsureInitialized(CancellationToken.None);
var preGameData = await this.guildwarsMemoryReader.ReadPreGameData(CancellationToken.None);
if (preGameData is null)
{
await Task.Delay(1000);
continue;
}

break;
}

this.logger.LogInformation($"Launching GWToolbox");
var process = new Process()
{
Expand Down
Loading