Skip to content

Commit a5980b0

Browse files
committed
temp
1 parent 00b4a68 commit a5980b0

6 files changed

Lines changed: 398 additions & 35 deletions

File tree

worlds/wsr/WSRClient.py

Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
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)

worlds/wsr/__init__.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,42 @@
1111
from worlds.LauncherComponents import Component, components, icon_paths, launch_subprocess, Type
1212
from Utils import local_path
1313

14+
def run_client() -> None:
15+
"""
16+
Launch the Wii Sports Resort Client
17+
"""
18+
print("Running WSR Client")
19+
from .WSRClient import main
20+
21+
launch_subprocess(main, name="WSRClient")
22+
23+
components.append(
24+
Component(
25+
"Wii Sports Resort Client",
26+
func=run_client,
27+
component_type=Type.CLIENT,
28+
file_identifier=(".apwsr"),
29+
icon="Wii Sports Resort"
30+
)
31+
)
32+
icon_paths["Wii Sports Resort"] = "ap:worlds.wsr/assets/icon.png"
33+
1434
class WSRWeb(WebWorld):
15-
#TODO
16-
pass
35+
"""
36+
This class handles the web interface.
37+
38+
The web interface includes the setup guide and the options page for generating YAMLs.
39+
"""
40+
tutorials = [Tutorial(
41+
"Wii Sports Resort Setup Guide",
42+
"A guide to settup up Wii Sports Resort for archipelago on your computer.",
43+
"English",
44+
"setup_en.md",
45+
"setup/en",
46+
["Kiwi", "Plyd", "Cyndifusic", "Dragonz"]
47+
)]
48+
theme = "ice"
49+
rich_text_options_doc = True
1750

1851
class WSRWorld(World):
1952
#VARS

worlds/wsr/assets/icon.png

5.25 KB
Loading

0 commit comments

Comments
 (0)