1+ import asyncio
2+ import copy
3+ from dataclasses import dataclass
4+ import time
5+ import traceback
6+ import textwrap
7+ import socket
8+ import struct
9+ import threading
10+ from typing import TYPE_CHECKING , Any , List , Optional
11+
12+ import Utils
13+ from CommonClient import (
14+ ClientCommandProcessor ,
15+ CommonContext ,
16+ get_base_parser ,
17+ gui_enabled ,
18+ logger ,
19+ server_loop ,
20+ )
21+ from NetUtils import ClientStatus , NetworkItem
22+
23+ if TYPE_CHECKING :
24+ import kvui
25+
26+ class AsyncUDPProtocol (asyncio .DatagramProtocol ):
27+ def __init__ (self , client ):
28+ self .client : AsyncWiiMemoryClient = client
29+
30+ def datagram_received (self , data , addr ):
31+ self .client .handle_response (data )
32+
33+ def error_received (self , exc ):
34+ print (f"UDP error: { exc } " )
35+ self .client .established = False
36+
37+ class CommandRequest :
38+ def __init__ (self , command : bytes , timeout : float = 10.0 ):
39+ self .command = command
40+ self .timeout = timeout
41+ self .future = asyncio .Future ()
42+ self .timestamp = time .time ()
43+
44+ class AsyncWiiMemoryClient :
45+ def __init__ (self , wii_ip , port = 51234 ):
46+ self .wii_ip = wii_ip
47+ self .port = port
48+ self .transport = None
49+ self .protocol = None
50+ self .established = False
51+
52+ self .command_queue = asyncio .Queue ()
53+ self .current_request : Optional [CommandRequest ] = None
54+ self .queue_processor_task = None
55+
56+ async def connect (self ):
57+ """Establish connection to the Wii"""
58+ print ("connect function" )
59+ try :
60+ loop = asyncio .get_event_loop ()
61+ self .transport , self .protocol = await loop .create_datagram_endpoint (
62+ lambda : AsyncUDPProtocol (self ),
63+ remote_addr = (self .wii_ip , self .port )
64+ )
65+ sock = self .transport .get_extra_info ('socket' )
66+ self .my_ip = sock .getsockname ()[0 ]
67+ self .my_port = sock .getsockname ()[1 ]
68+
69+ print ("trying to connect..." )
70+
71+ self .queue_processor_task = asyncio .create_task (self ._process_command_queue ())
72+
73+ try :
74+ await self .establish_connections ()
75+ self .established = True
76+ return True
77+ except asyncio .TimeoutError :
78+ self .established = False
79+ return False
80+
81+ except Exception as e :
82+ print (f"Connection failed: { e } " )
83+ self .established = False
84+ return False
85+
86+ async def disconnect (self ):
87+ if self .queue_processor_task :
88+ self .queue_processor_task .cancel ()
89+ try :
90+ await self .queue_processor_task
91+ except asyncio .CancelledError :
92+ pass
93+
94+ if self .transport :
95+ self .transport .close ()
96+
97+ self .established = False
98+
99+ async def establish_connection (self , timeout = 1 ):
100+ """Try to send a packet with IP and Port to establish connection to Wii server"""
101+ command = b'\x00 ' + socket .inet_aton (self .my_ip ) + struct .pack ('>H' , self .my_port )
102+
103+ response = await self ._send_command_queued (command , timeout )
104+
105+ if len (response ) > 0 :
106+ return True
107+ else :
108+ raise Exception (f"Establishing UDP connection failed" )
109+
110+ async def _send_command_queued (self , command : bytes , timeout = 2 ):
111+ """Queue up command to read/write to console"""
112+
113+ request = CommandRequest (command , timeout )
114+ await self .command_queue .put (request )
115+
116+ try :
117+ response = await asyncio .wait_for (request .future , timeout = timeout )
118+ return response
119+ except asyncio .TimeoutError :
120+ print (f"Command { command } timed out" )
121+ self .established = False
122+ raise
123+
124+ async def _process_command_queue (self ):
125+ """Process commands from queue with rate limiting"""
126+ while True :
127+ try :
128+ request = await self .command_queue .get ()
129+
130+ if self .transport and not request .future .cancelled ():
131+ self .current_request = request
132+ self .transport .sendto (request .command )
133+
134+ asyncio .create_task (self ._handle_request_timeout (request ))
135+ elif request .future and not request .future .cancelled ():
136+ request .future .set_exception (ConnectionError ("Not connected" ))
137+
138+ except asyncio .CancelledError :
139+ break
140+ except Exception as e :
141+ print (f"Error in command queue: { e } " )
142+ if self .current_request and not self .current_request .future .cancelled ():
143+ self .current_request .future .set_exception (e )
144+
145+ async def _handle_request_timeout (self , request : CommandRequest ):
146+ """Handle tieout for a specific request"""
147+ try :
148+ await asyncio .sleep (request .timeout )
149+ if self .current_request == request and not request .future .done ():
150+ request .future .set_exception (asyncio .TimeoutError ())
151+ self .current_request = None
152+ except asyncio .CancelledError :
153+ pass
154+
155+ async def signal_dc (self , timeout = 2 ) -> bytes :
156+ """Send a signal to the wii that the client lost connection"""
157+ command = struct .pack ('>B' , 0x05 ) # DISCONNECT - 0x05
158+
159+ response = await self ._send_command_queued (command , timeout )
160+
161+ if len (response ) > 0 :
162+ return response
163+ else :
164+ raise Exception (f"Read failed at address" )
165+
166+ def close (self ):
167+ """Close connection"""
168+ self .established = False
169+ if self .transport :
170+ self .transport .close ()
171+ self .transport = None
172+
173+
174+ class WSRContext (CommonContext ):
175+ """
176+ The context for the WSR client.
177+
178+ Manages the connection between the server and the console.
179+ """
180+
181+ def __init__ (self , server_address : Optional [str ], password : Optional [str ]) -> None :
182+ """
183+ Initialize the WSR context.
184+
185+ @param server_address: Address of the AP server.
186+ @param password: Password for server aunthentication.
187+ """
188+
189+ super ().__init__ (server_address , password )
190+ self .items_rcvd : list [tuple [NetworkItem , int ]] = []
191+ self .sync_task : Optional [asyncio .Task [None ]] = None
192+ self .awaiting_rom : bool = False
193+ self .wii_memory_client : AsyncWiiMemoryClient = None
194+ self .wii_ip : str = "0.0.0.0"
195+ self .socket = None
196+ self .client_socket = None
197+
198+ async def disconnect (self , allow_autoreconnect : bool = False ) -> None :
199+ """
200+ Disconnect the client from the server and reset game state variables
201+
202+ @param allow_autoreconnect: Allow the client to auto-reconnect to the server. Default is false
203+ """
204+ self .auth = None
205+ await super ().disconnect (allow_autoreconnect )
206+
207+
208+ def start_wii_client (self , ip ):
209+ """Initialize the async Wii client"""
210+ if self .wii_memory_client :
211+ self .wii_memory_client .close ()
212+ self .wii_memory_client = AsyncWiiMemoryClient (ip )
213+
214+ def close_wii_client (self ):
215+ if self .wii_memory_client :
216+ if self .wii_memory_client .established :
217+ self .wii_memory_client .signal_dc ()
218+ self .wii_memory_client .close ()
219+ self .wii_memory_client = None
220+
221+ def is_hooked (self ):
222+ return False
223+
224+ def make_gui (self ) -> type ["kvui.GameManager" ]:
225+ """
226+ Initialize the GUI for WSR Client.
227+
228+ Returns the client's GUI.
229+ """
230+ ui = super ().make_gui ()
231+ ui .base_title = "Archipelago Wii Sports Resort Client"
232+ return ui
233+
234+ async def server_auth (self , password_requested : bool = False ) -> None :
235+ """
236+ Authenticate with the Archipelago server.
237+
238+ @param password_requested: Whether the server requires a password. Defaults to `False`.
239+ """
240+ if password_requested and not self .password :
241+ await super ().server_auth (password_requested )
242+ if not self .auth :
243+ if self .awaiting_rom :
244+ return
245+ self .awaiting_rom = True
246+ logger .info ("Awaiting connection to the game to get player information." )
247+ return
248+ await self .send_connect ()
249+
250+ async def do_sync_task (ctx : WSRContext ) -> None :
251+ """
252+ Manages the connection to the game
253+
254+ @param ctx: The WSR client context.
255+ """
256+ while not ctx .exit_event .is_set ():
257+ try :
258+ if ctx .is_hooked ():
259+ await ctx .give_items ()
260+ await ctx .check_locations ()
261+ if ctx .awaiting_rom :
262+ await ctx .server_auth ()
263+ await asyncio .sleep (0.1 )
264+ else :
265+ logger .info ("Attempting to connect to the console..." )
266+ ctx .close_wii_client ()
267+ ctx .start_wii_client (ctx .wii_ip )
268+ await ctx .wii_memory_client .connect ()
269+
270+ if ctx .wii_memory_client .established :
271+ logger .info ("Wii connected successfully" )
272+ ctx .locations_checked = set ()
273+ else :
274+ logger .info (
275+ "Connection to console failed, attempting again..."
276+ )
277+ await ctx .disconnect ()
278+ await asyncio .sleep (5 )
279+ continue
280+ except TimeoutError :
281+ print ("Lost packet from console, attempting to reconnect..." )
282+ ctx .close_wii_client ()
283+ ctx .start_wii_client (ctx .wii_ip )
284+ if not await ctx .wii_memory_client .connect ():
285+ logger .info (
286+ "Lost packet from console and couldn't reconnect. attempting to reconnect..."
287+ )
288+ await ctx .disconnect ()
289+ await asyncio .sleep (5 )
290+ else :
291+ print ("Reconnected to console successfully!" )
292+ continue
293+ except Exception :
294+ ctx .close_wii_client ()
295+ logger .info (
296+ "Connection to console failed, attempting to reconnect..."
297+ )
298+ logger .error (traceback .format_exc ())
299+ await ctx .disconnect ()
300+ await asyncio .sleep (5 )
301+ continue
302+
303+ def main (connect : Optional [str ] = None , password : Optional [str ] = None ) -> None :
304+ """
305+ Run the main async loop for the WSR client.
306+
307+ @param connect: Address of the AP server
308+ @param password: Password for server aunthentication
309+ """
310+ Utils .init_logging ("Wii Sports Resort Client" )
311+
312+ async def _main (connect : Optional [str ], Password : Optional [str ]) -> None :
313+ ctx = WSRContext (connect , password )
314+ ctx .server_task = asyncio .create_task (server_loop (ctx ), name = "ServerLoop" )
315+ if gui_enabled :
316+ ctx .run_gui ()
317+ ctx .run_cli ()
318+ await asyncio .sleep (1 )
319+
320+ ctx .sync_task = asyncio .create_task (
321+ do_sync_task (ctx ), name = "GameSync"
322+ )
323+
324+ await ctx .exit_event .wait ()
325+ ctx .server_address = None
326+
327+ await ctx .shutdown ()
328+
329+ if ctx .sync_task :
330+ await asyncio .sleep (3 )
331+ await ctx .sync_task
332+
333+ import colorama
334+
335+ colorama .just_fix_windows_console ()
336+ colorama .init ()
337+ asyncio .run (_main (connect , password ))
338+ colorama .deinit ()
339+
340+ if __name__ == "__main__" :
341+ parser = get_base_parser ()
342+ args = parser .parse_args ()
343+ main (args .connect , args .password )
0 commit comments