Skip to content

Commit 46020df

Browse files
committed
Add WIP script for automated board testing
1 parent 2a41d60 commit 46020df

File tree

3 files changed

+261
-0
lines changed

3 files changed

+261
-0
lines changed

_scripts/addressTranslator.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
class AddressSectionMapper:
2+
def __init__(self, sections):
3+
self.sections = sections
4+
5+
@staticmethod
6+
def sectionContainsAddress(section, address) -> bool:
7+
if section["off_beg"] <= address and address <= section["off_end"]:
8+
return True
9+
return False
10+
11+
def map(self, address):
12+
for section in self.sections:
13+
if AddressSectionMapper.sectionContainsAddress(section, address):
14+
return address - section["delta"]
15+
return None
16+
17+
def inverseMap(self, address):
18+
for section in self.sections:
19+
mappedAddress = address + section["delta"]
20+
if AddressSectionMapper.sectionContainsAddress(section, mappedAddress):
21+
return mappedAddress
22+
return None
23+
24+
# Fortune Street Virtual to Fortune Street File
25+
fsvirt_to_fsfile = AddressSectionMapper([
26+
{"off_beg": 0x80004000, "off_end": 0x80006720, "delta": 0x80003f00},
27+
{"off_beg": 0x80006720, "off_end": 0x80006c80, "delta": 0x7fbfda40},
28+
{"off_beg": 0x80006c80, "off_end": 0x80007480, "delta": 0x7fbfda40},
29+
{"off_beg": 0x80007480, "off_end": 0x8040d940, "delta": 0x80004c60},
30+
{"off_beg": 0x8040d940, "off_end": 0x8040de80, "delta": 0x80003f00},
31+
{"off_beg": 0x8040de80, "off_end": 0x8040dea0, "delta": 0x80003f00},
32+
{"off_beg": 0x8040dec0, "off_end": 0x8044ea60, "delta": 0x80003f20},
33+
{"off_beg": 0x8044ea60, "off_end": 0x804ac680, "delta": 0x80003f20},
34+
{"off_beg": 0x80814a80, "off_end": 0x808171c0, "delta": 0x8036c320},
35+
{"off_beg": 0x80818da0, "off_end": 0x8081ede0, "delta": 0x8036df00}
36+
])
37+
38+
# Boom Street Virtual to Boom Street File
39+
bsvirt_to_bsfile = AddressSectionMapper([
40+
{"off_beg": 0x80004000, "off_end": 0x80006720, "delta": 0x80003f00},
41+
{"off_beg": 0x80006720, "off_end": 0x80006c80, "delta": 0x7fbfda00},
42+
{"off_beg": 0x80006c80, "off_end": 0x80007480, "delta": 0x7fbfda00},
43+
{"off_beg": 0x80007480, "off_end": 0x8040d980, "delta": 0x80004c60},
44+
{"off_beg": 0x8040d980, "off_end": 0x8040dec0, "delta": 0x80003f00},
45+
{"off_beg": 0x8040dec0, "off_end": 0x8040dee0, "delta": 0x80003f00},
46+
{"off_beg": 0x8040df00, "off_end": 0x8044ec00, "delta": 0x80003f20},
47+
{"off_beg": 0x8044ec00, "off_end": 0x804ac820, "delta": 0x80003f20},
48+
{"off_beg": 0x80814c80, "off_end": 0x808173c0, "delta": 0x8036c380},
49+
{"off_beg": 0x80818fa0, "off_end": 0x8081efe0, "delta": 0x8036df60}
50+
])
51+
52+
# Boom Street Virtual to Fortune Street Virtual
53+
bsvirt_to_fsvirt = AddressSectionMapper([
54+
{"off_beg": 0x80000100, "off_end": 0x8007a283, "delta": 0x0},
55+
{"off_beg": 0x8007a2f4, "off_end": 0x80268717, "delta": 0x54},
56+
{"off_beg": 0x80268720, "off_end": 0x8040d97b, "delta": 0x50},
57+
{"off_beg": 0x8040d980, "off_end": 0x8041027f, "delta": 0x40},
58+
{"off_beg": 0x804105f0, "off_end": 0x8044ebe7, "delta": 0x188},
59+
{"off_beg": 0x8044ec00, "off_end": 0x804ac804, "delta": 0x1A0},
60+
{"off_beg": 0x804ac880, "off_end": 0x8081f013, "delta": 0x200}
61+
])

_scripts/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ jsonschema
44
pyyaml
55
bytechomp
66
requests
7+
dolphin-memory-engine

_scripts/testBoard.py

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
from tempfile import TemporaryDirectory
2+
import dolphin_memory_engine
3+
import sys
4+
import os
5+
from subprocess import run
6+
from pathlib import Path
7+
import colorama
8+
import argparse
9+
import asyncio
10+
import time
11+
import struct
12+
import addressTranslator
13+
14+
repo_root = Path(__file__).parent.parent
15+
16+
def formatConfig() -> list[str]:
17+
config = []
18+
config.append("--config=Dolphin.Display.RenderToMain=False")
19+
config.append("--config=Dolphin.Display.Fullscreen=False")
20+
config.append("--config=Dolphin.Analytics.PermissionAsked=True")
21+
config.append("--config=Dolphin.Interface.ShowActiveTitle=True")
22+
config.append("--config=GFX.Settings.BorderlessFullscreen=True")
23+
config.append("--config=Dolphin.Core.WiiSDCardAllowWrites=False")
24+
config.append("--config=Dolphin.Input.BackgroundInput=False")
25+
config.append("--config=Dolphin.Core.FastDiscSpeed=True") # Speed up loading game files
26+
config.append("--config=Dolphin.Core.OverclockEnable=True") # Enable Emulated CPU Clock override
27+
config.append("--config=Dolphin.Core.Overclock=4.") # Set overclock to 400%
28+
config.append("--config=Dolphin.Core.GFXBackend=Null") # Do not render anything
29+
return config
30+
31+
def unsigned_to_signed_32bit(value):
32+
if value & 0x80000000: # Check if the MSB is set (value is negative in signed interpretation)
33+
return value - 0x100000000
34+
else:
35+
return value
36+
37+
def hack_start_immediately_yoshi_island(boom_to_standard):
38+
dolphin_memory_engine.write_word(boom_to_standard(0x80218508), 0x3800000e) # skip all the splash screens
39+
dolphin_memory_engine.write_word(boom_to_standard(0x80216e6c), 0x48000020) # automatically select to not create a save file, if asked
40+
dolphin_memory_engine.write_word(boom_to_standard(0x802174a8), 0x48000014) # automatically select to confirm to not create a save file, if asked
41+
dolphin_memory_engine.write_word(boom_to_standard(0x801e8fb4), 0x48000510) # when in title screen, start immediately
42+
dolphin_memory_engine.write_word(boom_to_standard(0x801f969c), 0x480000b0) # skip all board settings and start board immediately
43+
dolphin_memory_engine.write_word(boom_to_standard(0x8020cf9c), 0x38600000) # When starting tour mode all players are CPU
44+
dolphin_memory_engine.write_word(boom_to_standard(0x80011678), 0x38000001) # Set Player Order as picked instead of deciding randomly
45+
dolphin_memory_engine.write_word(boom_to_standard(0x80011628), 0x38000000) # Set map id to 0
46+
dolphin_memory_engine.write_word(boom_to_standard(0x80189de8), 0x380000d8) # Skip "This is the target amount for this board" dialog when starting board
47+
48+
def hack_speedup_game(boom_to_standard):
49+
dolphin_memory_engine.write_word(boom_to_standard(0x800197c8), 0x38040009) # Maximum Speedup
50+
dolphin_memory_engine.write_word(boom_to_standard(0x80099e4c), 0x38600001) # AI instantly chooses square
51+
dolphin_memory_engine.write_word(boom_to_standard(0x80011688), 0x380000ff) # Set Game Speed to Super Fast
52+
dolphin_memory_engine.write_word(boom_to_standard(0x80011690), 0x38000002) # Switch off dialogs
53+
dolphin_memory_engine.write_word(boom_to_standard(0x80818fa8), 0x41200069) # Multiply Game Speed by 10x
54+
55+
async def install_hacks(boom_to_standard):
56+
dolphin_memory_engine.assert_hooked()
57+
hacked = False
58+
while not hacked:
59+
try:
60+
hack_start_immediately_yoshi_island(boom_to_standard)
61+
hack_speedup_game(boom_to_standard)
62+
print("Hacked!")
63+
hacked = True
64+
except RuntimeError as e:
65+
print("Attempting to hack...")
66+
await asyncio.sleep(1)
67+
68+
async def run_single_board(csmm_executable: str, dolphin_executable: str, game_dir: str, board: str):
69+
# patch the board in
70+
board_yaml_file = list(Path(repo_root, "_maps", board).glob("*.y*ml"))[0].as_posix()
71+
#run([csmm_executable, "discard", game_dir], check=True)
72+
#run([csmm_executable, "import", game_dir, board_yaml_file, "--id", "0"], check=True)
73+
#run([csmm_executable, "save", game_dir], check=True)
74+
75+
mainDol = Path(game_dir, "sys", "main.dol")
76+
77+
boom_to_standard = None
78+
# check if boom street or fortune street
79+
with open(mainDol, "rb") as stream:
80+
stream.seek(0x756b4)
81+
b = stream.read(4)
82+
v = struct.unpack(">I", b)[0]
83+
if v == 0x800dab84:
84+
# boom street
85+
boom_to_standard = lambda address: address
86+
else:
87+
# fortune street
88+
boom_to_standard = addressTranslator.bsvirt_to_fsvirt
89+
90+
# create temporary directory
91+
with TemporaryDirectory() as td:
92+
# start dolphin
93+
#my_env = os.environ.copy()
94+
#my_env["DME_DOLPHIN_PROCESS_NAME"] = Path(dolphin_executable).with_suffix("").name
95+
args = ["--exec", mainDol, "--video_backend=null", "--user", td, *formatConfig()]
96+
proc = await asyncio.create_subprocess_exec(dolphin_executable, *args, cwd=game_dir)
97+
try:
98+
# hook the memory engine
99+
for i in range(1, 20):
100+
await asyncio.sleep(0.5)
101+
print(f"Attempt {i}/20 to hook dolphin memory engine...")
102+
dolphin_memory_engine.hook()
103+
if dolphin_memory_engine.is_hooked():
104+
print("Dolphin memory engine hooked")
105+
break
106+
if not dolphin_memory_engine.is_hooked():
107+
print("Failed to hook dolphin. Make sure dolpin is running and a game has been started.")
108+
sys.exit(1)
109+
# install hacks
110+
await install_hacks(boom_to_standard)
111+
# print current turn
112+
previous_turn = 0
113+
current_turn = 0
114+
target_amount = 0
115+
while target_amount == 0:
116+
target_amount = dolphin_memory_engine.read_word(boom_to_standard(0x80552424))
117+
await asyncio.sleep(1)
118+
print(f"Target amount: {target_amount}")
119+
while True:
120+
player_1_ready_cash = unsigned_to_signed_32bit(dolphin_memory_engine.read_word(boom_to_standard(0x805503c0)))
121+
player_2_ready_cash = unsigned_to_signed_32bit(dolphin_memory_engine.read_word(boom_to_standard(0x8055089c)))
122+
player_3_ready_cash = unsigned_to_signed_32bit(dolphin_memory_engine.read_word(boom_to_standard(0x80550d78)))
123+
player_4_ready_cash = unsigned_to_signed_32bit(dolphin_memory_engine.read_word(boom_to_standard(0x80551254)))
124+
player_1_net_worth = unsigned_to_signed_32bit(dolphin_memory_engine.read_word(boom_to_standard(0x80550468)))
125+
player_2_net_worth = unsigned_to_signed_32bit(dolphin_memory_engine.read_word(boom_to_standard(0x80550944)))
126+
player_3_net_worth = unsigned_to_signed_32bit(dolphin_memory_engine.read_word(boom_to_standard(0x80550e20)))
127+
player_4_net_worth = unsigned_to_signed_32bit(dolphin_memory_engine.read_word(boom_to_standard(0x805512fc)))
128+
player_1_str = "*P1:" if (current_turn%4)+1 == 1 else " P1:"
129+
player_2_str = "*P2:" if (current_turn%4)+1 == 2 else " P2:"
130+
player_3_str = "*P3:" if (current_turn%4)+1 == 3 else " P3:"
131+
player_4_str = "*P4:" if (current_turn%4)+1 == 4 else " P4:"
132+
print(f"Turn: {int(current_turn/4)+1:>2} {player_1_str}{player_1_ready_cash:>5} {player_1_net_worth:>5} {player_2_str}{player_2_ready_cash:>5} {player_2_net_worth:>5} {player_3_str}{player_3_ready_cash:>5} {player_3_net_worth:>5} {player_4_str}{player_4_ready_cash:>5} {player_4_net_worth:>5}")
133+
while current_turn == previous_turn:
134+
current_turn = dolphin_memory_engine.read_word(boom_to_standard(0x80552414))
135+
await asyncio.sleep(1)
136+
previous_turn = current_turn
137+
138+
await asyncio.sleep(99999)
139+
finally:
140+
# kill dolphin
141+
proc.kill()
142+
await proc.wait()
143+
print(f"Subprocess {proc.pid} killed")
144+
145+
async def run_all_boards(csmm_executable: str, dolphin_executable: str, game_dir: str):
146+
print("testing all boards not yet implemented")
147+
pass
148+
149+
async def run_modpack(csmm_executable: str, dolphin_executable: str, game_dir: str, modpack: str):
150+
print("testing modpack not yet implemented")
151+
pass
152+
153+
if __name__ == "__main__":
154+
colorama.init()
155+
156+
asyncio.run(run_single_board("csmm", "/mnt/workspace/fortunestreet/dolphin-emu/dolphin-emu", "/mnt/workspace/fortunestreet/boom_street_dev", "AlchemistsHouse"))
157+
sys.exit(0)
158+
159+
parser = argparse.ArgumentParser()
160+
parser.add_argument('--csmm-executable', default='csmm', action='store', help='The path to the csmm executable')
161+
parser.add_argument('--dolphin-executable', default='dolphin-emu', action='store', help='The path to the dolphin emulator executable')
162+
parser.add_argument('--game-dir', action='store', help='The path to an extracted clean vanilla Fortune Street or Boom Street.')
163+
parser.add_argument('--modpack', action='store', help='The yaml file which contains the boards that should be tested')
164+
parser.add_argument('--board', action='store', help='The yaml file of the board to be tested')
165+
parser.add_argument('--all', type=bool, help='If given, all boards in the repository are tested')
166+
args = parser.parse_args()
167+
try:
168+
version = run([args.csmm_executable, "--version"], check=True, text=True, capture_output=True)
169+
if not version:
170+
print("Failed to get csmm version")
171+
sys.exit(1)
172+
except FileNotFoundError:
173+
print("Csmm executable not found. Please provide it with --csmm-executable")
174+
sys.exit(1)
175+
try:
176+
version = run([args.dolphin_executable, "--version"], check=True, text=True, capture_output=True)
177+
if not version:
178+
print("Failed to get dolphin version")
179+
sys.exit(1)
180+
except FileNotFoundError:
181+
print("Dolphin executable not found. Please provide it with --dolphin-executable")
182+
sys.exit(1)
183+
game_dir_path = Path(args.game_dir)
184+
if not game_dir_path.exists() or not game_dir_path.is_dir():
185+
print("Game directory not found. Please provide it with --game-dir")
186+
sys.exit(1)
187+
main_dol_path = Path(args.game_dir, "sys", "main.dol")
188+
if not main_dol_path.exists() or not main_dol_path.is_file():
189+
print(f"{main_dol_path.as_posix()} not found. Make sure you provide a proper game directory with --game-dir")
190+
sys.exit(1)
191+
if not args.modpack and not args.board and not args.all:
192+
print("Must specify at least one of --modpack, --board, or --all")
193+
sys.exit(1)
194+
if args.all:
195+
run_all_boards(args.csmm_executable, args.dolphin_executable, args.image)
196+
if args.modpack:
197+
run_modpack(args.csmm_executable, args.dolphin_executable, args.image, args.modpack)
198+
if args.board:
199+
run_single_board(args.csmm_executable, args.dolphin_executable, args.image, args.board)

0 commit comments

Comments
 (0)