Skip to content

Commit 9dd39c0

Browse files
AlexMacocianAlexandru Macocian
andcommitted
Rework character select (#1201)
* Rework character select * Change index by frames * Fixes --------- Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
1 parent 2f5108f commit 9dd39c0

File tree

8 files changed

+199
-145
lines changed

8 files changed

+199
-145
lines changed

Daybreak.API/Interop/GuildWars/Frame.cs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Daybreak.API.Models;
2+
using System.Extensions;
23
using System.Runtime.InteropServices;
34

45
namespace Daybreak.API.Interop.GuildWars;
@@ -50,7 +51,7 @@ public readonly unsafe struct FrameRelation
5051
public readonly uint FrameHashId;
5152
}
5253

53-
[StructLayout(LayoutKind.Sequential, Pack =1)]
54+
[StructLayout(LayoutKind.Sequential, Pack = 1)]
5455
public readonly unsafe struct InteractionMessage
5556
{
5657
public readonly uint FrameId;
@@ -66,3 +67,23 @@ public unsafe readonly struct FrameInteractionCallback
6667
public readonly void* Callback; // UIInteractionCallback
6768
public readonly void* UiCtl_Context;
6869
}
70+
71+
[StructLayout(LayoutKind.Explicit, Pack = 1)]
72+
public readonly unsafe struct CharSelectorContext
73+
{
74+
[FieldOffset(0x0000)]
75+
public readonly uint Vtable;
76+
77+
[FieldOffset(0x0004)]
78+
public readonly uint FrameId;
79+
80+
[FieldOffset(0x0008)]
81+
public readonly GuildWarsArray<WrappedPointer<CharSelectorChar>> Chars;
82+
}
83+
84+
[StructLayout(LayoutKind.Explicit, Pack = 1)]
85+
public readonly struct CharSelectorChar
86+
{
87+
[FieldOffset(0x0020)]
88+
public readonly Array20Char Name;
89+
}

Daybreak.API/Models/UIMessage.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ public enum UIMessage : uint
1010
MouseClick = 0x24, // wparam = UIPacket::kMouseClick*
1111
MouseClick2 = 0x31, // wparam = UIPacket::kMouseAction*
1212
MouseAction = 0x32, // wparam = UIPacket::kMouseAction*
13+
FrameMessage_QuerySelectedIndex = 0x58, // Used to query selected index in character selector
1314
WriteToChatLog = 0x10000000 | 0x7F, // wparam = UIPacket::kWriteToChatLog*. Triggered by the game when it wants to add a new message to chat.
1415
WriteToChatLogWithSender = 0x10000000 | 0x80, // wparam = UIPacket::kWriteToChatLogWithSender*. Triggered by the game when it wants to add a new message to chat.
16+
CheckUIState = 0x10000000 | 0x118, // lparam = uint32_t* out state (2 = char select ready)
1517
Logout = 0x10000000 | 0x9D, // wparam = { bool unknown, bool character_select }
16-
};
18+
}

Daybreak.API/Models/UIPackets.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,11 @@ public readonly struct UIChatMessage(Channel channel, string message, Channel ch
3939
}
4040

4141
[StructLayout(LayoutKind.Sequential, Pack = 1)]
42-
public readonly struct KeyAction(uint key)
42+
public readonly struct KeyAction(uint key, uint wParam = 0x4000, uint lParam = 0x0006)
4343
{
4444
public readonly uint Key = key;
45-
public readonly uint WParam = 0x4000;
46-
public readonly uint LParam = 0x0006;
45+
public readonly uint WParam = wParam;
46+
public readonly uint LParam = lParam;
4747
}
4848

4949
[StructLayout(LayoutKind.Sequential, Pack = 1)]

Daybreak.API/Services/CharacterSelectService.cs

Lines changed: 124 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Daybreak.API.Interop.GuildWars;
2+
using Daybreak.API.Models;
23
using Daybreak.API.Services.Interop;
34
using Daybreak.Shared.Models.Api;
45
using System.Core.Extensions;
@@ -54,7 +55,7 @@ private readonly struct LogOutMessage(uint unknown, uint characterSelect)
5455

5556
var currentUuid = gameContext.Pointer->CharContext->PlayerUuid.ToString();
5657
var availableChars = new List<CharacterSelectEntry>((int)availableCharsContext.Pointer->Size);
57-
foreach(var charContext in *availableCharsContext.Pointer)
58+
foreach (var charContext in *availableCharsContext.Pointer)
5859
{
5960
var nameSpan = charContext.Name.AsSpan();
6061
var name = new string(nameSpan[..nameSpan.IndexOf('\0')]);
@@ -80,7 +81,6 @@ private readonly struct LogOutMessage(uint unknown, uint characterSelect)
8081
var currentCharacter = availableChars.FirstOrDefault();
8182
return new CharacterSelectInformation(currentCharacter, availableChars);
8283
}
83-
8484
}
8585
}, cancellationToken);
8686
}
@@ -96,20 +96,10 @@ public async Task<bool> ChangeCharacterByName(string characterName, Cancellation
9696

9797
await this.TriggerLogOut(cancellationToken);
9898

99-
var indexResult = await this.WaitForLoginScreenAndGetCurrentAndDesiredIndex(characterName, cancellationToken);
100-
if (indexResult is null)
101-
{
102-
scopedLogger.LogError("Failed to find index by name {name}", characterName);
103-
return false;
104-
}
105-
106-
var currentIndex = indexResult.Value.CurrentIndex;
107-
var desiredIndex = indexResult.Value.DesiredIndex;
108-
scopedLogger.LogInformation("Changing character to {name} with index {index}", characterName, desiredIndex);
109-
var navigateResult = await this.NavigateToCharAndPlay(currentIndex, desiredIndex, cancellationToken);
110-
if (!navigateResult)
99+
var selectResult = await this.WaitForCharSelectAndSelectCharacter(characterName, cancellationToken);
100+
if (!selectResult)
111101
{
112-
scopedLogger.LogError("Failed to navigate to character {name} with index {index}", characterName, desiredIndex);
102+
scopedLogger.LogError("Failed to select character {name}", characterName);
113103
return false;
114104
}
115105

@@ -134,86 +124,138 @@ public async Task<bool> ChangeCharacterByUuid(string uuid, CancellationToken can
134124

135125
await this.TriggerLogOut(cancellationToken);
136126

137-
var indexResult = await this.WaitForLoginScreenAndGetCurrentAndDesiredIndex(desiredCharName, cancellationToken);
138-
if (indexResult is null)
139-
{
140-
scopedLogger.LogError("Resolved character by uuid {uuid} but failed to find index by name {name}", uuid, desiredCharName);
141-
return false;
142-
}
143-
144-
var currentIndex = indexResult.Value.CurrentIndex;
145-
var desiredIndex = indexResult.Value.DesiredIndex;
146-
scopedLogger.LogInformation("Changing character to {name} with index {index} and uuid {uuid}", desiredCharName, desiredIndex, uuid);
147-
var navigateResult = await this.NavigateToCharAndPlay(currentIndex, desiredIndex, cancellationToken);
148-
if (!navigateResult)
127+
var selectResult = await this.WaitForCharSelectAndSelectCharacter(desiredCharName, cancellationToken);
128+
if (!selectResult)
149129
{
150-
scopedLogger.LogError("Failed to navigate to character {name} with index {index} and uuid {uuid}", desiredCharName, desiredIndex, uuid);
130+
scopedLogger.LogError("Failed to select character {name} with uuid {uuid}", desiredCharName, uuid);
151131
return false;
152132
}
153133

154134
return true;
155135
}
156136

157-
private async Task<bool> ValidateState(CancellationToken cancellationToken)
158-
{
159-
return await this.gameThreadService.QueueOnGameThread(() =>
160-
{
161-
return this.instanceContextService.GetInstanceType() is not API.Interop.GuildWars.InstanceType.Loading;
162-
}, cancellationToken);
163-
}
164-
165-
private async Task<bool> NavigateToCharAndPlay(uint currentIndex, uint desiredIndex, CancellationToken cancellationToken)
137+
private async Task<bool> SelectCharacterToPlay(string characterName, bool play, CancellationToken cancellationToken)
166138
{
167139
var scopedLogger = this.logger.CreateScopedLogger();
168-
while (!cancellationToken.IsCancellationRequested)
140+
141+
return await this.gameThreadService.QueueOnGameThread(() =>
169142
{
170-
await Task.Delay(100, cancellationToken);
171-
var result = await this.gameThreadService.QueueOnGameThread(() =>
143+
unsafe
172144
{
173-
unsafe
145+
var selectorFrame = this.GetCharSelectorFrame();
146+
if (selectorFrame is null)
174147
{
175-
var preGameContext = this.gameContextService.GetPreGameContext();
176-
if (preGameContext.IsNull)
177-
{
178-
scopedLogger.LogError("Pre-game context is not initialized");
179-
return false;
180-
}
148+
scopedLogger.LogError("Character selector frame not found");
149+
return false;
150+
}
151+
152+
var ctx = this.uiContextService.GetFrameContext<CharSelectorContext>(selectorFrame);
153+
if (ctx.IsNull)
154+
{
155+
scopedLogger.LogError("Character selector context not found");
156+
return false;
157+
}
158+
159+
var panesFrame = this.uiContextService.GetChildFrame(selectorFrame, 0);
160+
if (panesFrame.IsNull)
161+
{
162+
scopedLogger.LogError("Character panes frame not found");
163+
return false;
164+
}
181165

182-
var hwnd = this.platformContextService.GetWindowHandle();
183-
if (hwnd is null or 0)
166+
// Get current selected index
167+
var selectedIdx = 0;
168+
this.uiContextService.SendFrameUIMessage(panesFrame, UIMessage.FrameMessage_QuerySelectedIndex, null, &selectedIdx);
169+
170+
var chosen = false;
171+
for (uint i = 0; !chosen && i < ctx.Pointer->Chars.Size; i++)
172+
{
173+
var c = ctx.Pointer->Chars.Buffer[i];
174+
var charNameSpan = c.Pointer->Name.AsSpan();
175+
var nullIdx = charNameSpan.IndexOf('\0');
176+
if (nullIdx <= 0)
184177
{
185-
scopedLogger.LogError("Failed to get window handle");
186-
return false;
178+
continue;
187179
}
188180

189-
if (preGameContext.Pointer->Index1 == currentIndex)
181+
var charName = new string(charNameSpan[..nullIdx]);
182+
if (!charName.StartsWith(characterName, StringComparison.OrdinalIgnoreCase))
190183
{
191-
return false; //Not moved yet
184+
continue;
192185
}
193186

194-
if (preGameContext.Pointer->Index1 == desiredIndex)
187+
while (selectedIdx != i)
195188
{
196-
// We're on the desired character. Trigger play
197-
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_KEYDOWN, 0x50, 0x00190001);
198-
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_CHAR, 0x70, 0x00190001);
199-
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_KEYUP, 0x50, 0x00190001);
200-
return true;
189+
var keyAction = new UIPackets.KeyAction(0x1c);
190+
this.uiContextService.SendFrameUIMessage(panesFrame, UIMessage.KeyDown, &keyAction);
191+
192+
var newIdx = selectedIdx;
193+
this.uiContextService.SendFrameUIMessage(panesFrame, UIMessage.FrameMessage_QuerySelectedIndex, null, &newIdx);
194+
195+
if (newIdx == selectedIdx)
196+
{
197+
break; // This shouldn't happen - the character should have changed
198+
}
199+
200+
selectedIdx = newIdx;
201201
}
202202

203-
currentIndex = preGameContext.Pointer->Index1;
204-
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_KEYDOWN, NativeMethods.VK_RIGHT, 0x014D0001);
205-
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_KEYUP, NativeMethods.VK_RIGHT, 0x014D0001);
203+
chosen = selectedIdx == i;
204+
break;
205+
}
206+
207+
if (!chosen)
208+
{
209+
scopedLogger.LogError("Failed to select character {name}", characterName);
206210
return false;
207211
}
208-
}, cancellationToken);
209212

210-
if (result)
211-
{
213+
// TODO: This needs to be reworked to use UI messages to click on Play
214+
var hwnd = this.platformContextService.GetWindowHandle();
215+
if (!hwnd.HasValue)
216+
{
217+
scopedLogger.LogError("Failed to get game window handle");
218+
return false;
219+
}
220+
221+
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_KEYDOWN, 0x50, 0x00190001);
222+
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_CHAR, 0x70, 0x00190001);
223+
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_KEYUP, 0x50, 0x00190001);
212224
return true;
213225
}
226+
}, cancellationToken);
227+
}
228+
229+
private async Task<bool> WaitForCharSelectAndSelectCharacter(string characterName, CancellationToken cancellationToken)
230+
{
231+
var scopedLogger = this.logger.CreateScopedLogger();
232+
233+
// Wait for character select to be ready
234+
while (!cancellationToken.IsCancellationRequested)
235+
{
236+
try
237+
{
238+
if (await this.gameThreadService.QueueOnGameThread(this.IsCharSelectReady, cancellationToken))
239+
{
240+
break;
241+
}
242+
243+
await Task.Delay(100, cancellationToken);
244+
}
245+
catch(Exception e)
246+
{
247+
scopedLogger.LogError(e, "Error while waiting for character select to be ready");
248+
throw;
249+
}
214250
}
215251

216-
return false;
252+
if (cancellationToken.IsCancellationRequested)
253+
{
254+
return false;
255+
}
256+
257+
scopedLogger.LogInformation("Character select ready, selecting {name}", characterName);
258+
return await this.SelectCharacterToPlay(characterName, play: true, cancellationToken);
217259
}
218260

219261
private async Task<string?> GetCharNameByUuid(string uuid, CancellationToken cancellationToken)
@@ -258,68 +300,35 @@ private async Task TriggerLogOut(CancellationToken cancellationToken)
258300
{
259301
await this.gameThreadService.QueueOnGameThread(() =>
260302
{
261-
var logoutMessage = new LogOutMessage(0, 0);
303+
var logoutMessage = new LogOutMessage(0, 1); // Changed to 1 for character select
262304
unsafe
263305
{
264306
this.uiContextService.SendMessage(Models.UIMessage.Logout, (uint)&logoutMessage, 0);
265307
}
266308
}, cancellationToken);
267309
}
268310

269-
private async Task<(uint DesiredIndex, uint CurrentIndex)?> WaitForLoginScreenAndGetCurrentAndDesiredIndex(string desiredCharName, CancellationToken cancellationToken)
311+
private async Task<bool> ValidateState(CancellationToken cancellationToken)
270312
{
271-
uint? desiredIndex = default;
272-
uint? currentIndex = default;
273-
while (!cancellationToken.IsCancellationRequested)
313+
return await this.gameThreadService.QueueOnGameThread(() =>
274314
{
275-
await Task.Delay(100, cancellationToken);
276-
var ready = await this.gameThreadService.QueueOnGameThread(() =>
277-
{
278-
unsafe
279-
{
280-
var preGameContext = this.gameContextService.GetPreGameContext();
281-
if (preGameContext.IsNull)
282-
{
283-
return false;
284-
}
285-
286-
//TODO: Re-enable login screen check once we have a reliable way to detect it
287-
//var uiState = 10U;
288-
//this.uiContextService.SendMessage(Models.UIMessage.CheckUIState, 0, (uint)&uiState);
289-
//var loginScreen = uiState == 2;
290-
//if (!loginScreen)
291-
//{
292-
// return false;
293-
//}
294-
295-
for (var i = 0U; i < preGameContext.Pointer->LoginCharacters.Size; i++)
296-
{
297-
var loginCharacter = preGameContext.Pointer->LoginCharacters.Buffer[i];
298-
var charNameSpan = loginCharacter.CharacterName.AsSpan();
299-
var charName = new string(charNameSpan[..charNameSpan.IndexOf('\0')]);
300-
if (charName == desiredCharName)
301-
{
302-
desiredIndex = i;
303-
currentIndex = 0xffffffdd;
304-
return true;
305-
}
306-
}
307-
308-
return false;
309-
}
310-
}, cancellationToken);
315+
return this.instanceContextService.GetInstanceType() is not API.Interop.GuildWars.InstanceType.Loading;
316+
}, cancellationToken);
317+
}
311318

312-
if (ready)
313-
{
314-
break;
315-
}
316-
}
319+
private unsafe bool IsCharSelectReady()
320+
{
321+
return this.GetCharSelectorFrame() is not null;
322+
}
317323

318-
if (desiredIndex is not null && currentIndex is not null)
324+
private unsafe Frame* GetCharSelectorFrame()
325+
{
326+
var selectorFrame = this.uiContextService.GetFrameByLabel("Selector");
327+
if (selectorFrame.IsNull)
319328
{
320-
return (desiredIndex.Value, currentIndex.Value);
329+
return null;
321330
}
322331

323-
return default;
332+
return selectorFrame.Pointer;
324333
}
325334
}

0 commit comments

Comments
 (0)