Skip to content

Commit b369e41

Browse files
jamesbrqSilvrisnicholassaylorNewSoupViExempt-Medic
authored andcommitted
Mario & Luigi: Superstar Saga: Implement New Game (ArchipelagoMW#2754)
* Commit for PR * Commit for PR * Update worlds/mlss/Client.py Co-authored-by: Silvris <[email protected]> * Update worlds/mlss/__init__.py Co-authored-by: Silvris <[email protected]> * Update worlds/mlss/__init__.py Co-authored-by: Silvris <[email protected]> * Update worlds/mlss/docs/setup_en.md Co-authored-by: Silvris <[email protected]> * Remove deprecated import. Updated settings and romfile syntax * Updated Options to new system. Changed all references from MultiWorld to World * Changed switch statements to if else * Update en_Mario & Luigi Superstar Saga.md * Updated client.py * Update Client.py * Update worlds/mlss/docs/en_Mario & Luigi Superstar Saga.md Co-authored-by: Nicholas Saylor <[email protected]> * Updated logic, Updated patch implementation, Removed unused imports, Cleaned up Code * Update __init__.py * Changed reference from world to mlssworld * Update worlds/mlss/docs/en_Mario & Luigi Superstar Saga.md Co-authored-by: Nicholas Saylor <[email protected]> * Update worlds/mlss/docs/en_Mario & Luigi Superstar Saga.md Co-authored-by: Nicholas Saylor <[email protected]> * Update worlds/mlss/docs/en_Mario & Luigi Superstar Saga.md Co-authored-by: Nicholas Saylor <[email protected]> * Update worlds/mlss/docs/en_Mario & Luigi Superstar Saga.md Co-authored-by: Nicholas Saylor <[email protected]> * Update worlds/mlss/docs/en_Mario & Luigi Superstar Saga.md Co-authored-by: Nicholas Saylor <[email protected]> * Update worlds/mlss/docs/en_Mario & Luigi Superstar Saga.md Co-authored-by: Nicholas Saylor <[email protected]> * Fix merge conflict + update prep * v1.2 * Leftover print commands * Update basepatch.bsdiff * Update basepatch.bsdiff * v1.3 * Update Rom.py * Change tracker locations to serverside, no longer locations. Various code cleanup and logic changes. * Event removal continuation. * Partial Implementation of APPP (Incomplete)) * v1.4 Implemented APPP * Docs Updated * Update Rom.py * Update setup_en.md * Update Rom.py * Update Rules.py * Fix for APPP being broken on webhost * Update Rom.py * Update Rom.py * Location name fixes + pants color fixes * Update Rules.py * Fix for ultra hammer cutscene * Fixed compat. issues with python ver. 3.8 * Updated hidden block yaml option * pre-v1.5 * Update Client.py * Update basepatch.bsdiff * v1.5 * Update XP multiplier to have a minimum of 0 * Update 'Beanfruit' to 'Bean Fruit' * v1.6 * Update Rom.py * Update basepatch.bsdiff * Initial review refactor * Revert state logic changes. Continuation of refactor. * Fixed failed generations. Finished refactor. * Reworked colors. Removed all .txt files * Actually removed the .txt files this time * Update Rom.py * Update README.md Co-authored-by: Exempt-Medic <[email protected]> * Update worlds/mlss/Options.py Co-authored-by: Exempt-Medic <[email protected]> * Update worlds/mlss/Client.py Co-authored-by: Exempt-Medic <[email protected]> * Update worlds/mlss/docs/en_Mario & Luigi Superstar Saga.md Co-authored-by: Exempt-Medic <[email protected]> * Update worlds/mlss/__init__.py Co-authored-by: Exempt-Medic <[email protected]> * Update worlds/mlss/docs/en_Mario & Luigi Superstar Saga.md Co-authored-by: Exempt-Medic <[email protected]> * Update worlds/mlss/Data.py Co-authored-by: Exempt-Medic <[email protected]> * Review refactor. * Update README.md Co-authored-by: Exempt-Medic <[email protected]> * Update worlds/mlss/Rules.py Co-authored-by: Exempt-Medic <[email protected]> * Add coin blocks to LocationName * Refactor. * Update Items.py * Delete mlss.apworld * Small asm bugfix * Update basepatch.bsdiff * Client sends less messages to server * Update basepatch.bsdiff --------- Co-authored-by: Silvris <[email protected]> Co-authored-by: Nicholas Saylor <[email protected]> Co-authored-by: NewSoupVi <[email protected]> Co-authored-by: Exempt-Medic <[email protected]>
1 parent 139336f commit b369e41

16 files changed

+10027
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ Currently, the following games are supported:
6565
* Castlevania 64
6666
* A Short Hike
6767
* Yoshi's Island
68+
* Mario & Luigi: Superstar Saga
6869

6970
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
7071
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled

docs/CODEOWNERS

+3
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@
9292
/worlds/lufia2ac/ @el-u
9393
/worlds/lufia2ac/docs/ @wordfcuk @el-u
9494

95+
# Mario & Luigi: Superstar Saga
96+
/worlds/mlss/ @jamesbrq
97+
9598
# Meritous
9699
/worlds/meritous/ @FelicitusNeko
97100

worlds/mlss/Client.py

+297
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
from typing import TYPE_CHECKING, Optional, Set, List, Dict
2+
import struct
3+
4+
from NetUtils import ClientStatus
5+
from .Locations import roomCount, nonBlock, beanstones, roomException, shop, badge, pants, eReward
6+
from .Items import items_by_id
7+
8+
import asyncio
9+
10+
import worlds._bizhawk as bizhawk
11+
from worlds._bizhawk.client import BizHawkClient
12+
13+
if TYPE_CHECKING:
14+
from worlds._bizhawk.context import BizHawkClientContext
15+
16+
ROOM_ARRAY_POINTER = 0x51FA00
17+
18+
19+
class MLSSClient(BizHawkClient):
20+
game = "Mario & Luigi Superstar Saga"
21+
system = "GBA"
22+
patch_suffix = ".apmlss"
23+
local_checked_locations: Set[int]
24+
goal_flag: int
25+
rom_slot_name: Optional[str]
26+
eUsed: List[int]
27+
room: int
28+
local_events: List[int]
29+
player_name: Optional[str]
30+
checked_flags: Dict[int, list] = {}
31+
32+
def __init__(self) -> None:
33+
super().__init__()
34+
self.local_checked_locations = set()
35+
self.local_set_events = {}
36+
self.local_found_key_items = {}
37+
self.rom_slot_name = None
38+
self.seed_verify = False
39+
self.eUsed = []
40+
self.room = 0
41+
self.local_events = []
42+
43+
async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
44+
from CommonClient import logger
45+
46+
try:
47+
# Check ROM name/patch version
48+
rom_name_bytes = await bizhawk.read(ctx.bizhawk_ctx, [(0xA0, 14, "ROM")])
49+
rom_name = bytes([byte for byte in rom_name_bytes[0] if byte != 0]).decode("UTF-8")
50+
if not rom_name.startswith("MARIO&LUIGIU"):
51+
return False
52+
if rom_name == "MARIO&LUIGIUA8":
53+
logger.info(
54+
"ERROR: You appear to be running an unpatched version of Mario & Luigi Superstar Saga. "
55+
"You need to generate a patch file and use it to create a patched ROM."
56+
)
57+
return False
58+
if rom_name != "MARIO&LUIGIUAP":
59+
logger.info(
60+
"ERROR: The patch file used to create this ROM is not compatible with "
61+
"this client. Double check your client version against the version being "
62+
"used by the generator."
63+
)
64+
return False
65+
except UnicodeDecodeError:
66+
return False
67+
except bizhawk.RequestFailedError:
68+
return False # Should verify on the next pass
69+
70+
ctx.game = self.game
71+
ctx.items_handling = 0b101
72+
ctx.want_slot_data = True
73+
ctx.watcher_timeout = 0.125
74+
self.rom_slot_name = rom_name
75+
self.seed_verify = False
76+
name_bytes = (await bizhawk.read(ctx.bizhawk_ctx, [(0xDF0000, 16, "ROM")]))[0]
77+
name = bytes([byte for byte in name_bytes if byte != 0]).decode("UTF-8")
78+
self.player_name = name
79+
80+
for i in range(59):
81+
self.checked_flags[i] = []
82+
83+
return True
84+
85+
async def set_auth(self, ctx: "BizHawkClientContext") -> None:
86+
ctx.auth = self.player_name
87+
88+
def on_package(self, ctx, cmd, args) -> None:
89+
if cmd == "RoomInfo":
90+
ctx.seed_name = args["seed_name"]
91+
92+
async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
93+
from CommonClient import logger
94+
95+
try:
96+
if ctx.seed_name is None:
97+
return
98+
if not self.seed_verify:
99+
seed = await bizhawk.read(ctx.bizhawk_ctx, [(0xDF00A0, len(ctx.seed_name), "ROM")])
100+
seed = seed[0].decode("UTF-8")
101+
if seed != ctx.seed_name:
102+
logger.info(
103+
"ERROR: The ROM you loaded is for a different game of AP. "
104+
"Please make sure the host has sent you the correct patch file,"
105+
"and that you have opened the correct ROM."
106+
)
107+
raise bizhawk.ConnectorError("Loaded ROM is for Incorrect lobby.")
108+
self.seed_verify = True
109+
110+
read_state = await bizhawk.read(
111+
ctx.bizhawk_ctx,
112+
[
113+
(0x4564, 59, "EWRAM"),
114+
(0x2330, 2, "IWRAM"),
115+
(0x3FE0, 1, "IWRAM"),
116+
(0x304A, 1, "EWRAM"),
117+
(0x304B, 1, "EWRAM"),
118+
(0x304C, 4, "EWRAM"),
119+
(0x3060, 6, "EWRAM"),
120+
(0x4808, 2, "EWRAM"),
121+
(0x4407, 1, "EWRAM"),
122+
(0x2339, 1, "IWRAM"),
123+
]
124+
)
125+
flags = read_state[0]
126+
current_room = int.from_bytes(read_state[1], "little")
127+
shop_init = read_state[2][0]
128+
shop_scroll = read_state[3][0] & 0x1F
129+
is_buy = read_state[4][0] != 0
130+
shop_address = (struct.unpack("<I", read_state[5])[0]) & 0xFFFFFF
131+
logo = bytes([byte for byte in read_state[6] if byte < 0x70]).decode("UTF-8")
132+
received_index = (read_state[7][0] << 8) + read_state[7][1]
133+
cackletta = read_state[8][0] & 0x40
134+
shopping = read_state[9][0] & 0xF
135+
136+
if logo != "MLSSAP":
137+
return
138+
139+
locs_to_send = set()
140+
141+
# Checking shop purchases
142+
if is_buy:
143+
await bizhawk.write(ctx.bizhawk_ctx, [(0x304A, [0x0, 0x0], "EWRAM")])
144+
if shop_address != 0x3C0618 and shop_address != 0x3C0684:
145+
location = shop[shop_address][shop_scroll]
146+
else:
147+
if shop_init & 0x1 != 0:
148+
location = badge[shop_address][shop_scroll]
149+
else:
150+
location = pants[shop_address][shop_scroll]
151+
if location in ctx.server_locations:
152+
locs_to_send.add(location)
153+
154+
# Loop for receiving items. Item is written as an ID into 0x3057.
155+
# ASM reads the ID in a loop and give the player the item before resetting the RAM address to 0x0.
156+
# If RAM address isn't 0x0 yet break out and try again later to give the rest of the items
157+
for i in range(len(ctx.items_received) - received_index):
158+
item_data = items_by_id[ctx.items_received[received_index + i].item]
159+
b = await bizhawk.guarded_read(ctx.bizhawk_ctx, [(0x3057, 1, "EWRAM")], [(0x3057, [0x0], "EWRAM")])
160+
if b is None:
161+
break
162+
await bizhawk.write(
163+
ctx.bizhawk_ctx,
164+
[
165+
(0x3057, [id_to_RAM(item_data.itemID)], "EWRAM"),
166+
(0x4808, [(received_index + i + 1) // 0x100, (received_index + i + 1) % 0x100], "EWRAM"),
167+
],
168+
)
169+
await asyncio.sleep(0.1)
170+
171+
# Early return and location send if you are currently in a shop,
172+
# since other flags aren't going to change
173+
if shopping & 0x3 == 0x3:
174+
if locs_to_send != self.local_checked_locations:
175+
self.local_checked_locations = locs_to_send
176+
177+
if locs_to_send is not None:
178+
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": list(locs_to_send)}])
179+
return
180+
181+
# Checking flags that aren't digspots or blocks
182+
for item in nonBlock:
183+
address, mask, location = item
184+
if location in self.local_checked_locations:
185+
continue
186+
flag_bytes = await bizhawk.read(ctx.bizhawk_ctx, [(address, 1, "EWRAM"), (0x3060, 6, "EWRAM")])
187+
flag_byte = flag_bytes[0][0]
188+
backup_logo = bytes([byte for byte in flag_bytes[1] if byte < 0x70]).decode("UTF-8")
189+
if backup_logo != "MLSSAP":
190+
return
191+
if flag_byte & mask != 0:
192+
if location >= 0xDA0000 and location not in self.local_events:
193+
self.local_events += [location]
194+
await ctx.send_msgs(
195+
[
196+
{
197+
"cmd": "Set",
198+
"key": f"mlss_flag_{ctx.team}_{ctx.slot}",
199+
"default": 0,
200+
"want_reply": False,
201+
"operations": [{"operation": "or", "value": 1 << (location - 0xDA0000)}],
202+
}
203+
]
204+
)
205+
continue
206+
if location in roomException:
207+
if current_room not in roomException[location]:
208+
exception = True
209+
else:
210+
exception = False
211+
else:
212+
exception = True
213+
214+
if location in eReward:
215+
if location not in self.eUsed:
216+
self.eUsed += [location]
217+
location = eReward[len(self.eUsed) - 1]
218+
else:
219+
continue
220+
if (location in ctx.server_locations) and exception:
221+
locs_to_send.add(location)
222+
223+
# Check for set location flags.
224+
for byte_i, byte in enumerate(bytearray(flags)):
225+
for j in range(8):
226+
if j in self.checked_flags[byte_i]:
227+
continue
228+
and_value = 1 << j
229+
if byte & and_value != 0:
230+
flag_id = byte_i * 8 + (j + 1)
231+
room, item = find_key(roomCount, flag_id)
232+
pointer_arr = await bizhawk.read(
233+
ctx.bizhawk_ctx, [(ROOM_ARRAY_POINTER + ((room - 1) * 4), 4, "ROM")]
234+
)
235+
pointer = struct.unpack("<I", pointer_arr[0])[0]
236+
pointer = pointer & 0xFFFFFF
237+
offset = await bizhawk.read(ctx.bizhawk_ctx, [(pointer, 1, "ROM")])
238+
offset = offset[0][0]
239+
if offset != 0:
240+
offset = 2
241+
pointer += (item * 8) + 1 + offset
242+
for key, value in beanstones.items():
243+
if pointer == value:
244+
pointer = key
245+
break
246+
if pointer in ctx.server_locations:
247+
self.checked_flags[byte_i] += [j]
248+
locs_to_send.add(pointer)
249+
250+
if not ctx.finished_game and cackletta != 0 and current_room == 0x1C7:
251+
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
252+
253+
if self.room != current_room:
254+
self.room = current_room
255+
await ctx.send_msgs(
256+
[
257+
{
258+
"cmd": "Set",
259+
"key": f"mlss_room_{ctx.team}_{ctx.slot}",
260+
"default": 0,
261+
"want_reply": False,
262+
"operations": [{"operation": "replace", "value": current_room}],
263+
}
264+
]
265+
)
266+
267+
# Send locations if there are any to send.
268+
if locs_to_send != self.local_checked_locations:
269+
self.local_checked_locations = locs_to_send
270+
271+
if locs_to_send is not None:
272+
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": list(locs_to_send)}])
273+
274+
except bizhawk.RequestFailedError:
275+
# Exit handler and return to main loop to reconnect.
276+
pass
277+
except bizhawk.ConnectorError:
278+
pass
279+
280+
281+
def find_key(dictionary, target):
282+
leftover = target
283+
284+
for key, value in dictionary.items():
285+
if leftover > value:
286+
leftover -= value
287+
else:
288+
return key, leftover
289+
290+
291+
def id_to_RAM(id_: int):
292+
code = id_
293+
if 0x1C <= code <= 0x1F:
294+
code += 0xE
295+
if 0x20 <= code <= 0x26:
296+
code -= 0x4
297+
return code

0 commit comments

Comments
 (0)