Skip to content

Commit 713e95d

Browse files
committed
Initial push
1 parent 48cbc05 commit 713e95d

File tree

5 files changed

+1423
-0
lines changed

5 files changed

+1423
-0
lines changed

DK64Client.py

Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
import ModuleUpdate
2+
3+
ModuleUpdate.update()
4+
5+
import Utils
6+
7+
if __name__ == "__main__":
8+
Utils.init_logging("DK64Context", exception_logger="Client")
9+
10+
import asyncio
11+
import colorama
12+
import time
13+
import typing
14+
from worlds.dk64.client.common import N64Exception, DK64MemoryMap, create_task_log_exception
15+
from worlds.dk64.client.pj64 import PJ64Client
16+
from worlds.dk64.client.id_data import item_ids
17+
18+
from CommonClient import CommonContext, get_base_parser, gui_enabled, logger, server_loop
19+
from NetUtils import ClientStatus
20+
21+
22+
class DK64Client:
23+
n64_client = PJ64Client()
24+
tracker = None
25+
game = None
26+
auth = None
27+
recvd_checks: dict = {}
28+
29+
stop_bizhawk_spam = False
30+
31+
async def wait_for_pj64(self):
32+
clear_waiting_message = True
33+
if not self.stop_bizhawk_spam:
34+
logger.info("Waiting on connection to PJ64...")
35+
self.stop_bizhawk_spam = True
36+
37+
while True:
38+
try:
39+
socket_connected = False
40+
valid_rom = self.n64_client.validate_rom(self.game)
41+
if self.n64_client.socket is not None and not socket_connected:
42+
logger.info("Connected to PJ64")
43+
socket_connected = True
44+
while not valid_rom:
45+
if self.n64_client.socket is not None and not socket_connected:
46+
logger.info("Connected to PJ64")
47+
socket_connected = True
48+
if clear_waiting_message:
49+
logger.info("Waiting on valid ROM...")
50+
clear_waiting_message = False
51+
await asyncio.sleep(1.0)
52+
valid_rom = self.n64_client.validate_rom(self.game)
53+
self.stop_bizhawk_spam = False
54+
logger.info("PJ64 Connected to ROM!")
55+
return
56+
except (N64Exception, BlockingIOError, TimeoutError, ConnectionResetError):
57+
await asyncio.sleep(1.0)
58+
pass
59+
60+
async def reset_auth(self):
61+
memory_location = self.n64_client.read_u32(DK64MemoryMap.memory_pointer)
62+
self.n64_client.write_u8(memory_location + DK64MemoryMap.connection, [0xFF])
63+
64+
async def wait_and_init_tracker(self):
65+
await self.wait_for_game_ready()
66+
# self.tracker = LocationTracker(self.n64_client)
67+
# self.item_tracker = ItemTracker(self.n64_client)
68+
69+
async def recved_item_from_ap(self, item_id, from_player, next_index):
70+
# Don't allow getting an item until you've got your first check
71+
if not self.tracker.has_start_item():
72+
return
73+
74+
# Spin until we either:
75+
# get an exception from a bad read (emu shut down or reset)
76+
# beat the game
77+
# the client handles the last pending item
78+
status = self.safe_to_send()
79+
while not (await self.is_victory()) and status != 0:
80+
time.sleep(0.1)
81+
status = self.safe_to_send()
82+
# TODO: not sure why we need this
83+
# item_id -= LABaseID
84+
# The player name table only goes up to 100, so don't go past that
85+
# Even if it didn't, the remote player _index_ byte is just a byte, so 255 max
86+
if from_player > 100:
87+
from_player = 100
88+
89+
# next_index += 1
90+
# self.n64_client.write_memory(LAClientConstants.wLinkGiveItem, [
91+
# item_id, from_player])
92+
# status |= 1
93+
# status = self.n64_client.write_memory(LAClientConstants.wLinkStatusBits, [status])
94+
# self.n64_client.write_memory(LAClientConstants.wRecvIndex, struct.pack(">H", next_index))
95+
# self.setFlag(item_ids[14041094].get("flag_id"))
96+
# memory_location = self.client.n64_client.read_u32(DK64MemoryMap.memory_pointer)
97+
# example_string = "ExampleString Multi Spaced\0"
98+
# self.client.n64_client.write_bytestring(memory_location + DK64MemoryMap.fed_string, example_string)
99+
def check_safe_gameplay(self):
100+
current_gamemode = self.n64_client.read_u8(DK64MemoryMap.CurrentGamemode)
101+
next_gamemode = self.n64_client.read_u8(DK64MemoryMap.NextGamemode)
102+
return current_gamemode in [6, 0xD] and next_gamemode in [6, 0xD]
103+
104+
def safe_to_send(self):
105+
memory_location = self.n64_client.read_u32(DK64MemoryMap.memory_pointer)
106+
countdown_value = self.n64_client.read_u8(memory_location + DK64MemoryMap.safety_text_timer)
107+
return countdown_value == 0
108+
109+
async def readChecks(self, cb):
110+
new_checks = []
111+
for check in self.remaining_checks:
112+
addresses = [check.address]
113+
if check.alternateAddress:
114+
addresses.append(check.alternateAddress)
115+
bytes = await self.gameboy.read_memory_cache(addresses)
116+
if not bytes:
117+
return False
118+
check.set(list(bytes.values()))
119+
120+
if check.value:
121+
self.remaining_checks.remove(check)
122+
new_checks.append(check)
123+
if new_checks:
124+
cb(new_checks)
125+
return True
126+
127+
def has_start_item(self):
128+
# Checks to see if the file has been started
129+
return self.readFlag(0) == 1
130+
131+
should_reset_auth = False
132+
133+
def setFlag(self, index: int) -> int:
134+
byte_index = index >> 3
135+
shift = index & 7
136+
offset = DK64MemoryMap.EEPROM + byte_index
137+
val = self.n64_client.read_u8(offset)
138+
self.n64_client.write_u8(offset, [val | (1 << shift)])
139+
140+
def readFlag(self, index: int) -> int:
141+
byte_index = index >> 3
142+
shift = index & 7
143+
offset = DK64MemoryMap.EEPROM + byte_index
144+
val = self.n64_client.read_u8(offset)
145+
return (val >> shift) & 1
146+
147+
async def wait_for_game_ready(self):
148+
logger.info("Waiting on game to be in valid state...")
149+
while not self.check_safe_gameplay():
150+
if self.should_reset_auth:
151+
self.should_reset_auth = False
152+
raise N64Exception("Resetting due to wrong archipelago server")
153+
logger.info("Game connection ready!")
154+
155+
async def is_victory(self):
156+
memory_location = self.n64_client.read_u32(DK64MemoryMap.memory_pointer)
157+
return self.n64_client.read_u8(memory_location + DK64MemoryMap.end_credits) == 1
158+
159+
async def main_tick(self, item_get_cb, win_cb):
160+
await self.readChecks(item_get_cb)
161+
# await self.item_tracker.readItems()
162+
if await self.is_victory():
163+
await win_cb()
164+
165+
# recv_index = struct.unpack(">H", await self.n64_client.async_read_memory(LAClientConstants.wRecvIndex, 2))[0]
166+
167+
# # Play back one at a time
168+
# if recv_index in self.recvd_checks:
169+
# item = self.recvd_checks[recv_index]
170+
# await self.recved_item_from_ap(item.item, item.player, recv_index)
171+
172+
173+
class DK64Context(CommonContext):
174+
tags = {"AP"}
175+
game = "Donkey Kong 64"
176+
la_task = None
177+
found_checks = []
178+
last_resend = time.time()
179+
180+
won = False
181+
182+
def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str]) -> None:
183+
self.client = DK64Client()
184+
self.client.game = self.game.upper()
185+
self.slot_data = {}
186+
187+
super().__init__(server_address, password)
188+
189+
def run_gui(self) -> None:
190+
from kvui import GameManager
191+
192+
class DK64Manager(GameManager):
193+
logging_pairs = [
194+
("Client", "Archipelago"),
195+
("Tracker", "Tracker"),
196+
]
197+
base_title = "Archipelago Donkey Kong 64 Client"
198+
199+
def build(self):
200+
b = super().build()
201+
return b
202+
203+
self.ui = DK64Manager(self)
204+
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
205+
206+
async def send_checks(self):
207+
message = [{"cmd": "LocationChecks", "locations": self.found_checks}]
208+
await self.send_msgs(message)
209+
210+
had_invalid_slot_data: typing.Optional[bool] = None
211+
212+
def event_invalid_slot(self):
213+
# The next time we try to connect, reset the game loop for new auth
214+
self.had_invalid_slot_data = True
215+
self.auth = None
216+
# Don't try to autoreconnect, it will just fail
217+
self.disconnected_intentionally = True
218+
CommonContext.event_invalid_slot(self)
219+
220+
async def send_victory(self):
221+
if not self.won:
222+
message = [{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]
223+
logger.info("victory!")
224+
await self.send_msgs(message)
225+
self.won = True
226+
227+
def new_checks(self, item_ids):
228+
self.found_checks += item_ids
229+
create_task_log_exception(self.send_checks())
230+
231+
async def server_auth(self, password_requested: bool = False):
232+
if password_requested and not self.password:
233+
await super(DK64Context, self).server_auth(password_requested)
234+
235+
if self.had_invalid_slot_data:
236+
# We are connecting when previously we had the wrong ROM or server - just in case
237+
# re-read the ROM so that if the user had the correct address but wrong ROM, we
238+
# allow a successful reconnect
239+
self.client.should_reset_auth = True
240+
self.had_invalid_slot_data = False
241+
242+
while self.client.auth == None:
243+
await asyncio.sleep(0.1)
244+
245+
# Just return if we're closing
246+
if self.exit_event.is_set():
247+
return
248+
self.auth = self.client.auth
249+
await self.send_connect()
250+
251+
def on_package(self, cmd: str, args: dict):
252+
if cmd == "Connected":
253+
if self.slot is not None:
254+
self.game = self.slot_info[self.slot].game
255+
self.slot_data = args.get("slot_data", {})
256+
257+
if cmd == "ReceivedItems":
258+
for index, item in enumerate(args["items"], start=args["index"]):
259+
self.client.recvd_checks[index] = item
260+
261+
async def sync(self):
262+
sync_msg = [{"cmd": "Sync"}]
263+
await self.send_msgs(sync_msg)
264+
265+
async def run_game_loop(self):
266+
async def victory():
267+
await self.send_victory()
268+
269+
def on_item_get(dk64_checks):
270+
return
271+
# TODO: implement this
272+
# checks = [item_ids[check.id] for check in dk64_checks]
273+
# self.new_checks(checks)
274+
275+
# yield to allow UI to start
276+
await asyncio.sleep(0)
277+
while True:
278+
await asyncio.sleep(0.1)
279+
280+
try:
281+
if not self.client.stop_bizhawk_spam:
282+
logger.info("(Re)Starting game loop")
283+
self.found_checks.clear()
284+
# On restart of game loop, clear all checks, just in case we swapped ROMs
285+
# this isn't totally neccessary, but is extra safety against cross-ROM contamination
286+
self.client.recvd_checks.clear()
287+
await self.client.wait_for_pj64()
288+
await self.client.reset_auth()
289+
290+
# If we find ourselves with new auth after the reset, reconnect
291+
if self.auth and self.client.auth != self.auth:
292+
# It would be neat to reconnect here, but connection needs this loop to be running
293+
logger.info("Detected new ROM, disconnecting...")
294+
await self.disconnect()
295+
continue
296+
if not self.client.recvd_checks:
297+
await self.sync()
298+
299+
# await self.client.wait_and_init_tracker()
300+
await asyncio.sleep(1.0)
301+
while True:
302+
await self.client.reset_auth()
303+
await self.client.main_tick(on_item_get, victory)
304+
await asyncio.sleep(0.1)
305+
now = time.time()
306+
if self.last_resend + 5.0 < now:
307+
self.last_resend = now
308+
await self.send_checks()
309+
if self.client.should_reset_auth:
310+
self.client.should_reset_auth = False
311+
raise Exception("Resetting due to wrong archipelago server")
312+
except (asyncio.TimeoutError, TimeoutError, ConnectionResetError):
313+
await asyncio.sleep(1.0)
314+
315+
316+
async def main():
317+
parser = get_base_parser(description="Donkey Kong 64 Client.")
318+
parser.add_argument("--url", help="Archipelago connection url")
319+
320+
args = parser.parse_args()
321+
322+
ctx = DK64Context(args.connect, args.password)
323+
324+
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
325+
326+
# TODO: nothing about the lambda about has to be in a lambda
327+
ctx.la_task = create_task_log_exception(ctx.run_game_loop())
328+
if gui_enabled:
329+
ctx.run_gui()
330+
ctx.run_cli()
331+
332+
await ctx.exit_event.wait()
333+
await ctx.shutdown()
334+
335+
336+
if __name__ == "__main__":
337+
colorama.init()
338+
asyncio.run(main())
339+
colorama.deinit()

0 commit comments

Comments
 (0)