Skip to content
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
730 changes: 547 additions & 183 deletions Daybreak.API/Interop/GWCA.cs

Large diffs are not rendered by default.

31 changes: 29 additions & 2 deletions Daybreak.API/Interop/GuildWars/GuildWarsArray.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,44 @@ namespace Daybreak.API.Interop.GuildWars;
public readonly uint Size;
public readonly uint Param;

/// <summary>
/// Upper bound on a believable element count. Guild Wars' largest arrays hold at most a
/// few thousand entries, so anything beyond this is uninitialised or misresolved memory
/// rather than a real array.
/// </summary>
private const uint MaxPlausibleCapacity = 0x10000;

/// <summary>
/// Mirrors <c>GW::BaseArray::valid()</c> - the buffer must be null or aligned and the size
/// must fit within the capacity - plus a bound on the capacity itself. Guild Wars leaves
/// these fields transiently inconsistent while it (re)allocates an array, and a
/// misresolved GWCA scan can point this struct at arbitrary bytes that still satisfy
/// <c>size &lt;= capacity</c>.
/// </summary>
public bool IsValid =>
(this.Buffer is null || ((nuint)this.Buffer & 0x3) == 0) &&
this.Size <= this.Capacity &&
this.Capacity <= MaxPlausibleCapacity;

/// <summary>
/// Number of elements that can safely be read. Zero whenever the array is not backed by
/// a buffer, matching <c>GW::BaseArray::get()</c>, which returns null instead of
/// dereferencing. Reading past this is an access violation, and because NativeAOT cannot
/// throw <c>AccessViolationException</c> it fail-fasts and takes Guild Wars down with it.
/// </summary>
public uint Count => this.Buffer is not null && this.IsValid ? this.Size : 0;

public T this[int index]
{
get
{
ArgumentOutOfRangeException.ThrowIfNegative(index);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, this.Size);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, this.Count);
return this.Buffer[index];
}
}

public Enumerator GetEnumerator() => new(this.Buffer, this.Size);
public Enumerator GetEnumerator() => new(this.Buffer, this.Count);

IEnumerator<T> IEnumerable<T>.GetEnumerator() => this.GetEnumerator();

Expand Down
12 changes: 12 additions & 0 deletions Daybreak.API/Interop/GuildWars/WrappedPointer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ public readonly unsafe struct WrappedPointer<T>(T* pointer)

public bool IsNull => this.Pointer is null;

/// <summary>
/// True when the pointer is non-null and plausibly dereferenceable.
/// </summary>
/// <remarks>
/// Guild Wars structures are 4-byte aligned on x86, so a misaligned address is never a
/// real structure. When a GWCA pattern scan fails to resolve, it can hand back an
/// address inside Gw.exe's code section (for example 0x0048CA72); reading through that
/// yields nonsense field values and eventually an access violation. NativeAOT cannot
/// throw <c>AccessViolationException</c>, so it fail-fasts and terminates Guild Wars.
/// </remarks>
public bool IsValid => this.Pointer is not null && ((nuint)this.Pointer & 0x3) == 0;

public static implicit operator WrappedPointer<T>(T* pointer) => new(pointer);

public static implicit operator T*(WrappedPointer<T> wrappedPointer) => wrappedPointer.Pointer;
Expand Down
19 changes: 14 additions & 5 deletions Daybreak.API/Services/CharacterSelectService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,28 @@ public sealed class CharacterSelectService(
{
var gameContext = this.gameContextService.GetGameContext();
if (gameContext.IsNull ||
gameContext.Pointer->World is null)
gameContext.Pointer->World is null ||
gameContext.Pointer->Character is null)
{
scopedLogger.LogError("Game context is not initialized");
return default;
}

var availableCharsContext = this.gameContextService.GetAvailableChars();
if (availableCharsContext.IsNull)
if (!availableCharsContext.IsValid ||
!availableCharsContext.Pointer->IsValid)
{
scopedLogger.LogError("Available characters context is not initialized");
scopedLogger.LogError(
"Available characters context is not initialized (array@0x{Array:X8} buffer=0x{Buffer:X8} capacity={Capacity} size={Size})",
(nuint)availableCharsContext.Pointer,
availableCharsContext.IsValid ? (nuint)availableCharsContext.Pointer->Buffer : 0,
availableCharsContext.IsValid ? availableCharsContext.Pointer->Capacity : 0,
availableCharsContext.IsValid ? availableCharsContext.Pointer->Size : 0);
return default;
}

var currentUuid = (*(Uuid*)gameContext.Pointer->Character->PlayerUuid).ToString();
var availableChars = new List<CharacterSelectEntry>((int)availableCharsContext.Pointer->Size);
var availableChars = new List<CharacterSelectEntry>((int)availableCharsContext.Pointer->Count);
foreach (var charContext in *availableCharsContext.Pointer)
{
var name = new string(charContext.Name);
Expand Down Expand Up @@ -196,7 +203,9 @@ private async Task<bool> SelectCharacterToPlay(string characterName, bool play,

// Find target character index
uint targetIdx = 0xFFFF;
var charCount = ctx.Pointer->Chars.Size;
// Count (not Size) so a not-yet-allocated roster yields zero iterations
// instead of indexing through a null buffer.
var charCount = ctx.Pointer->Chars.Count;

for (uint i = 0; i < charCount; i++)
{
Expand Down
1 change: 1 addition & 0 deletions Daybreak.Core/Services/DXVK/DXVKService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ private async Task<bool> DownloadAndExtractDXVK(IProgress<ProgressUpdate> progre
{
Directory.Delete(x32Target, recursive: true);
}

if (Directory.Exists(x64Target))
{
Directory.Delete(x64Target, recursive: true);
Expand Down
4 changes: 3 additions & 1 deletion Daybreak.Core/Views/EventCalendarView.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using TrailBlazr.ViewModels;

namespace Daybreak.Views;

public sealed class EventCalendarViewModel(IEventService eventService)
: ViewModelBase<EventCalendarViewModel, EventCalendarView>
{
Expand Down Expand Up @@ -92,7 +93,7 @@ public List<DateTime> GetCalendarDates()
{
dates.Add(new DateTime(today.Year, today.Month, day));
}

return dates;
}

Expand All @@ -107,6 +108,7 @@ public List<Event> GetEventsForDate(DateTime date)
events.Add(eventItem);
}
}

return events;
}
}
45 changes: 33 additions & 12 deletions Daybreak.Generators/CppHeaderParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ public static void Parse(string headerText, ConstantsNode root)
{
inMultiLineConstexpr = false;
}

continue;
}

Expand All @@ -189,6 +190,7 @@ public static void Parse(string headerText, ConstantsNode root)
{
inSkipBlock = false;
}

continue;
}

Expand All @@ -211,6 +213,7 @@ public static void Parse(string headerText, ConstantsNode root)
{
ParseEnumMembersFromLine(trimmed.Substring(0, closeBraceIdx), currentEnum);
}

inEnum = false;
var node = namespaceStack.Peek();
node.Enums.Add(currentEnum);
Expand All @@ -231,11 +234,11 @@ public static void Parse(string headerText, ConstantsNode root)
int closeBraces = CountChar(lineWithoutComments, '}');
int prevDepth = structBraceDepth;
structBraceDepth += openBraces - closeBraces;

// Debug: Track brace changes for AgentLiving
if (currentStruct.Name == "AgentLiving" && (openBraces > 0 || closeBraces > 0))
{
DebugLines.Add($"[BRACE] line {i+1}: {currentStruct.Name} depth {prevDepth}->{structBraceDepth} (open={openBraces}, close={closeBraces})");
DebugLines.Add($"[BRACE] line {i + 1}: {currentStruct.Name} depth {prevDepth}->{structBraceDepth} (open={openBraces}, close={closeBraces})");
}

// If we hit the closing brace of the struct
Expand All @@ -252,8 +255,8 @@ public static void Parse(string headerText, ConstantsNode root)
pathParts.Add(n.Name);
pathParts.Reverse();
currentStruct.DebugNamespacePath = string.Join(".", pathParts);
DebugLines.Add($"[STRUCT-END] line {i+1}: {currentStruct.Name} with {currentStruct.Fields.Count} fields");

DebugLines.Add($"[STRUCT-END] line {i + 1}: {currentStruct.Name} with {currentStruct.Fields.Count} fields");
node.Structs.Add(currentStruct);
currentStruct = null;
inStruct = false;
Expand All @@ -278,6 +281,7 @@ public static void Parse(string headerText, ConstantsNode root)
if (field is not null)
currentStruct.Fields.Add(field);
}

continue;
}

Expand All @@ -295,7 +299,7 @@ public static void Parse(string headerText, ConstantsNode root)
var trailingCommentIdx = trimmed.IndexOf("//");
if (trailingCommentIdx > 0)
trimmedWithoutTrailingComment = trimmed.Substring(0, trailingCommentIdx).TrimEnd();

if (trimmedWithoutTrailingComment.Contains("(") && (trimmedWithoutTrailingComment.Contains(")") || trimmedWithoutTrailingComment.Contains("{")))
continue;

Expand Down Expand Up @@ -339,6 +343,7 @@ public static void Parse(string headerText, ConstantsNode root)
};
currentStruct.Fields.Add(field);
}

continue;
}

Expand All @@ -354,6 +359,7 @@ public static void Parse(string headerText, ConstantsNode root)
var child = current.GetOrCreateChild(part);
namespaceStack.Push(child);
}

continue;
}

Expand All @@ -366,13 +372,14 @@ public static void Parse(string headerText, ConstantsNode root)
freeBraceDepth--;
continue;
}

if (namespaceStack.Count > 1)
{
// Debug: Track where namespace was popped
var poppedNode = namespaceStack.Pop();
poppedNode.DebugPopLine = i + 1;
}

continue;
}

Expand All @@ -390,6 +397,7 @@ public static void Parse(string headerText, ConstantsNode root)
}
}
}

continue;
}

Expand Down Expand Up @@ -436,6 +444,7 @@ public static void Parse(string headerText, ConstantsNode root)
inSkipBlock = true;
skipBlockBraceCount = braces;
}

continue;
}

Expand All @@ -448,24 +457,26 @@ public static void Parse(string headerText, ConstantsNode root)
inSkipBlock = true;
skipBlockBraceCount = braces;
}

continue;
}

// ── Skip out-of-line method definitions (Type* Class::Method() {) ─
if (OutOfLineMethodRegex.IsMatch(trimmed))
{
int braces = CountChar(trimmed, '{') - CountChar(trimmed, '}');
DebugLines.Add($"[OUT-OF-LINE] line {i+1}: {trimmed.Substring(0, Math.Min(50, trimmed.Length))}... braces={braces}");
DebugLines.Add($"[OUT-OF-LINE] line {i + 1}: {trimmed.Substring(0, Math.Min(50, trimmed.Length))}... braces={braces}");
if (braces > 0)
{
inSkipBlock = true;
skipBlockBraceCount = braces;
}

continue;
}
else if (trimmed.Contains("::") && trimmed.Contains("(") && trimmed.Contains("{"))
{
DebugLines.Add($"[UNMATCHED-METHOD] line {i+1}: {trimmed.Substring(0, Math.Min(60, trimmed.Length))}");
DebugLines.Add($"[UNMATCHED-METHOD] line {i + 1}: {trimmed.Substring(0, Math.Min(60, trimmed.Length))}");
}

// ── Track function declarations with body on separate line ─
Expand All @@ -492,6 +503,7 @@ public static void Parse(string headerText, ConstantsNode root)
inMultiLineConstexpr = true;
multiLineBraceCount = braces;
}

continue;
}

Expand All @@ -501,7 +513,7 @@ public static void Parse(string headerText, ConstantsNode root)
{
var structName = structMatch.Groups[1].Value;
var baseType = structMatch.Groups[2].Success ? structMatch.Groups[2].Value : null;
DebugLines.Add($"[STRUCT-START] line {i+1}: {structName} (inSkip={inSkipBlock}, freeBrace={freeBraceDepth})");
DebugLines.Add($"[STRUCT-START] line {i + 1}: {structName} (inSkip={inSkipBlock}, freeBrace={freeBraceDepth})");

// Skip some special cases
if (structName.StartsWith("__") || structName == "Packet")
Expand All @@ -512,6 +524,7 @@ public static void Parse(string headerText, ConstantsNode root)
inSkipBlock = true;
skipBlockBraceCount = braces;
}

continue;
}

Expand All @@ -530,7 +543,7 @@ public static void Parse(string headerText, ConstantsNode root)
if (structNobraceMatch.Success)
{
var structName = structNobraceMatch.Groups[1].Value;

// Skip forward declarations (just struct Name;)
if (trimmed.EndsWith(";"))
continue;
Expand Down Expand Up @@ -559,8 +572,10 @@ public static void Parse(string headerText, ConstantsNode root)
i = j;
break;
}

break; // unexpected content
}

continue;
}

Expand Down Expand Up @@ -593,9 +608,11 @@ public static void Parse(string headerText, ConstantsNode root)
{
ParseEnumMembersFromLine(rest, enumDef);
}

currentEnum = enumDef;
inEnum = true;
}

continue;
}

Expand Down Expand Up @@ -637,14 +654,18 @@ public static void Parse(string headerText, ConstantsNode root)
{
ParseEnumMembersFromLine(rest, enumDef);
}

currentEnum = enumDef;
inEnum = true;
}

i = j;
break;
}

break; // unexpected content
}

continue;
}

Expand Down Expand Up @@ -794,7 +815,7 @@ private static string StripComments(string line)
int slashSlash = line.IndexOf("//");
if (slashSlash >= 0)
line = line.Substring(0, slashSlash);

// Remove /* */ style comments (simple - doesn't handle nested)
int blockStart = line.IndexOf("/*");
while (blockStart >= 0)
Expand All @@ -806,7 +827,7 @@ private static string StripComments(string line)
line = line.Substring(0, blockStart);
blockStart = line.IndexOf("/*");
}

return line;
}

Expand Down
Loading
Loading