361 lines
13 KiB
Python
361 lines
13 KiB
Python
import base64
|
||
import time
|
||
from typing import Optional
|
||
|
||
from bemani.client.base import BaseClient
|
||
from bemani.protocol import Node
|
||
|
||
|
||
class MetalGearArcadeClient(BaseClient):
|
||
NAME = "TEST"
|
||
|
||
def verify_eventlog_write(self, location: str) -> None:
|
||
call = self.call_node()
|
||
|
||
# Construct node
|
||
eventlog = Node.void("eventlog")
|
||
call.add_child(eventlog)
|
||
eventlog.set_attribute("method", "write")
|
||
eventlog.add_child(Node.u32("retrycnt", 0))
|
||
data = Node.void("data")
|
||
eventlog.add_child(data)
|
||
data.add_child(Node.string("eventid", "S_PWRON"))
|
||
data.add_child(Node.s32("eventorder", 0))
|
||
data.add_child(Node.u64("pcbtime", int(time.time() * 1000)))
|
||
data.add_child(Node.s64("gamesession", -1))
|
||
data.add_child(Node.string("strdata1", "1.9.1"))
|
||
data.add_child(Node.string("strdata2", ""))
|
||
data.add_child(Node.s64("numdata1", 1))
|
||
data.add_child(Node.s64("numdata2", 0))
|
||
data.add_child(Node.string("locationid", location))
|
||
|
||
# Swap with server
|
||
resp = self.exchange("", call)
|
||
|
||
# Verify that response is correct
|
||
self.assert_path(resp, "response/eventlog/gamesession")
|
||
self.assert_path(resp, "response/eventlog/logsendflg")
|
||
self.assert_path(resp, "response/eventlog/logerrlevel")
|
||
self.assert_path(resp, "response/eventlog/evtidnosendflg")
|
||
|
||
def verify_system_getmaster(self) -> None:
|
||
call = self.call_node()
|
||
|
||
# Construct node
|
||
system = Node.void("system")
|
||
call.add_child(system)
|
||
system.set_attribute("method", "getmaster")
|
||
data = Node.void("data")
|
||
system.add_child(data)
|
||
data.add_child(Node.string("gamekind", "I36"))
|
||
data.add_child(Node.string("datatype", "S_SRVMSG"))
|
||
data.add_child(Node.string("datakey", "INFO"))
|
||
|
||
# Swap with server
|
||
resp = self.exchange("", call)
|
||
|
||
# Verify that response is correct
|
||
self.assert_path(resp, "response/system/result")
|
||
|
||
def verify_usergamedata_send(self, ref_id: str, msg_type: str) -> None:
|
||
call = self.call_node()
|
||
|
||
# Set up profile write
|
||
profiledata = [
|
||
b"ffffffff",
|
||
b"PLAYDATA",
|
||
b"8",
|
||
b"1",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"ffffffffffffa928",
|
||
b"0.000000",
|
||
b"0.000000",
|
||
b"0.000000",
|
||
b"0.000000",
|
||
b"0.000000",
|
||
b"0.000000",
|
||
b"0.000000",
|
||
b"0.000000",
|
||
b"PLAYER",
|
||
b"JP-13",
|
||
b"ea",
|
||
b"",
|
||
b"JP-13",
|
||
b"",
|
||
b"",
|
||
b"",
|
||
]
|
||
|
||
outfitdata = [
|
||
b"ffffffff",
|
||
b"OUTFIT",
|
||
b"8",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"202000020400800",
|
||
b"1000100",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0.000000",
|
||
b"0.000000",
|
||
b"0.000000",
|
||
b"0.000000",
|
||
b"0.000000",
|
||
b"0.000000",
|
||
b"0.000000",
|
||
b"0.000000",
|
||
b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||
b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||
b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||
b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||
b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||
b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAABwACxEWEUgRWBEuADkRZgBmAAAcAAsRFhFIEVgRLgA5EWYAZgAAHAALERYRSBFYES4AORFmAGYA",
|
||
b"AAAAAA==",
|
||
b"",
|
||
]
|
||
|
||
weapondata = [
|
||
b"ffffffff",
|
||
b"WEAPON",
|
||
b"8",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"201000000003",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0.000000",
|
||
b"0.000000",
|
||
b"0.000000",
|
||
b"0.000000",
|
||
b"0.000000",
|
||
b"0.000000",
|
||
b"0.000000",
|
||
b"0.000000",
|
||
b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||
b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||
b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||
b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||
b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||
b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||
b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||
b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||
]
|
||
|
||
mainruledata = [
|
||
b"ffffffff",
|
||
b"MAINRULE",
|
||
b"8",
|
||
b"6",
|
||
b"800000",
|
||
b"1",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"0",
|
||
b"1.000000",
|
||
b"0.000000",
|
||
b"10.000000",
|
||
b"4.000000",
|
||
b"0.000000",
|
||
b"0.000000",
|
||
b"0.000000",
|
||
b"0.000000",
|
||
b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||
b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||
b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||
b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAA=",
|
||
b"",
|
||
b"",
|
||
b"",
|
||
b"",
|
||
]
|
||
|
||
if msg_type == "new":
|
||
# New profile gets blank name, because we save over it at the end of the round.
|
||
profiledata[27] = b""
|
||
elif msg_type == "existing":
|
||
# Exiting profile gets our hardcoded name saved.
|
||
profiledata[27] = self.NAME.encode("shift-jis")
|
||
|
||
# Construct node
|
||
playerdata = Node.void("playerdata")
|
||
call.add_child(playerdata)
|
||
playerdata.set_attribute("method", "usergamedata_send")
|
||
playerdata.add_child(Node.u32("retrycnt", 0))
|
||
|
||
data = Node.void("data")
|
||
playerdata.add_child(data)
|
||
data.add_child(Node.string("eaid", ref_id))
|
||
data.add_child(Node.string("gamekind", "I36"))
|
||
data.add_child(Node.u32("datanum", 4))
|
||
record = Node.void("record")
|
||
data.add_child(record)
|
||
d = Node.string("d", base64.b64encode(b",".join(profiledata)).decode("ascii"))
|
||
record.add_child(d)
|
||
d.add_child(Node.string("bin1", ""))
|
||
d = Node.string("d", base64.b64encode(b",".join(outfitdata)).decode("ascii"))
|
||
record.add_child(d)
|
||
d.add_child(Node.string("bin1", ""))
|
||
d = Node.string("d", base64.b64encode(b",".join(weapondata)).decode("ascii"))
|
||
record.add_child(d)
|
||
d.add_child(Node.string("bin1", ""))
|
||
d = Node.string("d", base64.b64encode(b",".join(mainruledata)).decode("ascii"))
|
||
record.add_child(d)
|
||
d.add_child(Node.string("bin1", ""))
|
||
|
||
# Swap with server
|
||
resp = self.exchange("", call)
|
||
self.assert_path(resp, "response/playerdata/result")
|
||
|
||
def verify_usergamedata_recv(self, ref_id: str) -> str:
|
||
call = self.call_node()
|
||
|
||
# Construct node
|
||
playerdata = Node.void("playerdata")
|
||
call.add_child(playerdata)
|
||
playerdata.set_attribute("method", "usergamedata_recv")
|
||
data = Node.void("data")
|
||
playerdata.add_child(data)
|
||
data.add_child(Node.string("eaid", ref_id))
|
||
data.add_child(Node.string("gamekind", "I36"))
|
||
data.add_child(Node.u32("recv_num", 4))
|
||
data.add_child(
|
||
Node.string(
|
||
"recv_csv",
|
||
"PLAYDATA,3fffffffff,OUTFIT,3fffffffff,WEAPON,3fffffffff,MAINRULE,3fffffffff",
|
||
)
|
||
)
|
||
|
||
# Swap with server
|
||
resp = self.exchange("", call)
|
||
self.assert_path(resp, "response/playerdata/result")
|
||
self.assert_path(resp, "response/playerdata/player/record/d/bin1")
|
||
self.assert_path(resp, "response/playerdata/player/record_num")
|
||
|
||
# Grab binary data, parse out name
|
||
bindata = resp.child_value("playerdata/player/record/d")
|
||
profiledata = base64.b64decode(bindata).split(b",")
|
||
|
||
# We lob off the first two values in returning profile, so the name is offset by two
|
||
return profiledata[25].decode("shift-jis")
|
||
|
||
def verify(self, cardid: Optional[str]) -> None:
|
||
# Verify boot sequence is okay
|
||
self.verify_services_get(
|
||
expected_services=[
|
||
"pcbtracker",
|
||
"pcbevent",
|
||
"local",
|
||
"message",
|
||
"facility",
|
||
"cardmng",
|
||
"package",
|
||
"posevent",
|
||
"pkglist",
|
||
"dlstatus",
|
||
"eacoin",
|
||
"lobby",
|
||
"ntp",
|
||
"keepalive",
|
||
]
|
||
)
|
||
paseli_enabled = self.verify_pcbtracker_alive()
|
||
self.verify_message_get()
|
||
self.verify_package_list()
|
||
location = self.verify_facility_get()
|
||
self.verify_pcbevent_put()
|
||
self.verify_eventlog_write(location)
|
||
self.verify_system_getmaster()
|
||
|
||
# Verify card registration and profile lookup
|
||
if cardid is not None:
|
||
card = cardid
|
||
else:
|
||
card = self.random_card()
|
||
print(f"Generated random card ID {card} for use.")
|
||
|
||
if cardid is None:
|
||
self.verify_cardmng_inquire(
|
||
card, msg_type="unregistered", paseli_enabled=paseli_enabled
|
||
)
|
||
ref_id = self.verify_cardmng_getrefid(card)
|
||
if len(ref_id) != 16:
|
||
raise Exception(
|
||
f"Invalid refid '{ref_id}' returned when registering card"
|
||
)
|
||
if ref_id != self.verify_cardmng_inquire(
|
||
card, msg_type="new", paseli_enabled=paseli_enabled
|
||
):
|
||
raise Exception(f"Invalid refid '{ref_id}' returned when querying card")
|
||
# MGA doesn't read a new profile, it just writes out CSV for a blank one
|
||
self.verify_usergamedata_send(ref_id, msg_type="new")
|
||
else:
|
||
print("Skipping new card checks for existing card")
|
||
ref_id = self.verify_cardmng_inquire(
|
||
card, msg_type="query", paseli_enabled=paseli_enabled
|
||
)
|
||
|
||
# Verify pin handling and return card handling
|
||
self.verify_cardmng_authpass(ref_id, correct=True)
|
||
self.verify_cardmng_authpass(ref_id, correct=False)
|
||
if ref_id != self.verify_cardmng_inquire(
|
||
card, msg_type="query", paseli_enabled=paseli_enabled
|
||
):
|
||
raise Exception(f"Invalid refid '{ref_id}' returned when querying card")
|
||
|
||
if cardid is None:
|
||
# Verify profile saving
|
||
name = self.verify_usergamedata_recv(ref_id)
|
||
if name != "":
|
||
raise Exception("New profile has a name associated with it!")
|
||
|
||
self.verify_usergamedata_send(ref_id, msg_type="existing")
|
||
name = self.verify_usergamedata_recv(ref_id)
|
||
if name != self.NAME:
|
||
raise Exception("Existing profile has no name associated with it!")
|
||
else:
|
||
print("Skipping score checks for existing card")
|