11using Daybreak . API . Interop . GuildWars ;
2+ using Daybreak . API . Models ;
23using Daybreak . API . Services . Interop ;
34using Daybreak . Shared . Models . Api ;
45using 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