935 lines
38 KiB
Python
935 lines
38 KiB
Python
import base64
|
|
import random
|
|
import time
|
|
from typing import Optional, Dict, List, Tuple, Any
|
|
|
|
from bemani.client.base import BaseClient
|
|
from bemani.common import ID, Time
|
|
from bemani.protocol import Node
|
|
|
|
|
|
def b64str(string: str) -> str:
|
|
return base64.b64encode(string.encode()).decode("ascii")
|
|
|
|
|
|
class DDRAceClient(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", b64str("2.4.0")))
|
|
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_convcardnumber(self, cardno: str) -> None:
|
|
call = self.call_node()
|
|
|
|
# Construct node
|
|
system = Node.void("system")
|
|
call.add_child(system)
|
|
system.set_attribute("method", "convcardnumber")
|
|
info = Node.void("info")
|
|
system.add_child(info)
|
|
info.add_child(Node.s32("version", 1))
|
|
data = Node.void("data")
|
|
system.add_child(data)
|
|
data.add_child(Node.string("card_id", cardno))
|
|
data.add_child(Node.s32("card_type", 1))
|
|
|
|
# Swap with server
|
|
resp = self.exchange("", call)
|
|
|
|
# Verify that response is correct
|
|
self.assert_path(resp, "response/system/data/card_number")
|
|
self.assert_path(resp, "response/system/result")
|
|
|
|
def verify_playerdata_usergamedata_advanced_usernew(self, refid: str) -> int:
|
|
call = self.call_node()
|
|
|
|
# Construct node
|
|
playerdata = Node.void("playerdata")
|
|
call.add_child(playerdata)
|
|
playerdata.set_attribute("method", "usergamedata_advanced")
|
|
playerdata.add_child(Node.u32("retrycnt", 0))
|
|
info = Node.void("info")
|
|
playerdata.add_child(info)
|
|
info.add_child(Node.s32("version", 1))
|
|
data = Node.void("data")
|
|
playerdata.add_child(data)
|
|
data.add_child(Node.string("mode", "usernew"))
|
|
data.add_child(Node.string("shoparea", "."))
|
|
data.add_child(Node.s64("gamesession", 123456))
|
|
data.add_child(Node.string("refid", refid))
|
|
data.add_child(Node.string("dataid", refid))
|
|
data.add_child(Node.string("gamekind", "MDX"))
|
|
data.add_child(Node.string("pcbid", self.pcbid))
|
|
data.add_child(Node.void("record"))
|
|
|
|
# Swap with server
|
|
resp = self.exchange("", call)
|
|
|
|
# Verify that response is correct
|
|
self.assert_path(resp, "response/playerdata/seq")
|
|
self.assert_path(resp, "response/playerdata/code")
|
|
self.assert_path(resp, "response/playerdata/shoparea")
|
|
self.assert_path(resp, "response/playerdata/result")
|
|
|
|
return resp.child_value("playerdata/code")
|
|
|
|
def verify_playerdata_usergamedata_advanced_ghostload(self, refid: str, ghostid: int) -> Dict[str, Any]:
|
|
call = self.call_node()
|
|
|
|
# Construct node
|
|
playerdata = Node.void("playerdata")
|
|
call.add_child(playerdata)
|
|
playerdata.set_attribute("method", "usergamedata_advanced")
|
|
playerdata.add_child(Node.u32("retrycnt", 0))
|
|
info = Node.void("info")
|
|
playerdata.add_child(info)
|
|
info.add_child(Node.s32("version", 1))
|
|
data = Node.void("data")
|
|
playerdata.add_child(data)
|
|
data.add_child(Node.string("mode", "ghostload"))
|
|
data.add_child(Node.s32("ghostid", ghostid))
|
|
data.add_child(Node.s64("gamesession", 123456))
|
|
data.add_child(Node.string("refid", refid))
|
|
data.add_child(Node.string("dataid", refid))
|
|
data.add_child(Node.string("gamekind", "MDX"))
|
|
data.add_child(Node.string("pcbid", self.pcbid))
|
|
data.add_child(Node.void("record"))
|
|
|
|
# Swap with server
|
|
resp = self.exchange("", call)
|
|
|
|
# Verify that response is correct
|
|
self.assert_path(resp, "response/playerdata/ghostdata/code")
|
|
self.assert_path(resp, "response/playerdata/ghostdata/mcode")
|
|
self.assert_path(resp, "response/playerdata/ghostdata/notetype")
|
|
self.assert_path(resp, "response/playerdata/ghostdata/ghostsize")
|
|
self.assert_path(resp, "response/playerdata/ghostdata/ghost")
|
|
|
|
return {
|
|
"extid": resp.child_value("playerdata/ghostdata/code"),
|
|
"id": resp.child_value("playerdata/ghostdata/mcode"),
|
|
"chart": resp.child_value("playerdata/ghostdata/notetype"),
|
|
"ghost": resp.child_value("playerdata/ghostdata/ghost"),
|
|
}
|
|
|
|
def verify_playerdata_usergamedata_advanced_rivalload(self, refid: str, loadflag: int) -> None:
|
|
call = self.call_node()
|
|
|
|
# Construct node
|
|
playerdata = Node.void("playerdata")
|
|
call.add_child(playerdata)
|
|
playerdata.set_attribute("method", "usergamedata_advanced")
|
|
playerdata.add_child(Node.u32("retrycnt", 0))
|
|
info = Node.void("info")
|
|
playerdata.add_child(info)
|
|
info.add_child(Node.s32("version", 1))
|
|
data = Node.void("data")
|
|
playerdata.add_child(data)
|
|
data.add_child(Node.string("mode", "rivalload"))
|
|
data.add_child(Node.u64("targettime", Time.now() * 1000))
|
|
data.add_child(Node.string("shoparea", "."))
|
|
data.add_child(Node.bool("isdouble", False))
|
|
data.add_child(Node.s32("loadflag", loadflag))
|
|
data.add_child(Node.s32("ddrcode", 0))
|
|
data.add_child(Node.s64("gamesession", 123456))
|
|
data.add_child(Node.string("refid", refid))
|
|
data.add_child(Node.string("dataid", refid))
|
|
data.add_child(Node.string("gamekind", "MDX"))
|
|
data.add_child(Node.string("pcbid", self.pcbid))
|
|
data.add_child(Node.void("record"))
|
|
|
|
# Swap with server
|
|
resp = self.exchange("", call)
|
|
|
|
# Verify that response is correct
|
|
self.assert_path(resp, "response/playerdata/data/recordtype")
|
|
if loadflag != 2:
|
|
# As implemented, its possible for a machine not in an arcade to have scores.
|
|
# So, if the test PCBID we're using isn't in an arcade, we won't fetch scores
|
|
# for area records (flag 2), so don't check for these in that case.
|
|
self.assert_path(resp, "response/playerdata/data/record/mcode")
|
|
self.assert_path(resp, "response/playerdata/data/record/notetype")
|
|
self.assert_path(resp, "response/playerdata/data/record/rank")
|
|
self.assert_path(resp, "response/playerdata/data/record/clearkind")
|
|
self.assert_path(resp, "response/playerdata/data/record/flagdata")
|
|
self.assert_path(resp, "response/playerdata/data/record/name")
|
|
self.assert_path(resp, "response/playerdata/data/record/area")
|
|
self.assert_path(resp, "response/playerdata/data/record/code")
|
|
self.assert_path(resp, "response/playerdata/data/record/score")
|
|
self.assert_path(resp, "response/playerdata/data/record/ghostid")
|
|
|
|
if resp.child_value("playerdata/data/recordtype") != loadflag:
|
|
raise Exception("Invalid record type returned!")
|
|
|
|
def verify_playerdata_usergamedata_advanced_userload(self, refid: str) -> Tuple[bool, List[Dict[str, Any]]]:
|
|
call = self.call_node()
|
|
|
|
# Construct node
|
|
playerdata = Node.void("playerdata")
|
|
call.add_child(playerdata)
|
|
playerdata.set_attribute("method", "usergamedata_advanced")
|
|
playerdata.add_child(Node.u32("retrycnt", 0))
|
|
info = Node.void("info")
|
|
playerdata.add_child(info)
|
|
info.add_child(Node.s32("version", 1))
|
|
data = Node.void("data")
|
|
playerdata.add_child(data)
|
|
data.add_child(Node.string("mode", "userload"))
|
|
data.add_child(Node.s64("gamesession", 123456))
|
|
data.add_child(Node.string("refid", refid))
|
|
data.add_child(Node.string("dataid", refid))
|
|
data.add_child(Node.string("gamekind", "MDX"))
|
|
data.add_child(Node.string("pcbid", self.pcbid))
|
|
data.add_child(Node.void("record"))
|
|
|
|
# Swap with server
|
|
resp = self.exchange("", call)
|
|
|
|
# Verify that response is correct
|
|
self.assert_path(resp, "response/playerdata/result")
|
|
self.assert_path(resp, "response/playerdata/is_new")
|
|
|
|
music = []
|
|
for child in resp.child("playerdata").children:
|
|
if child.name != "music":
|
|
continue
|
|
|
|
songid = child.child_value("mcode")
|
|
chart = 0
|
|
for note in child.children:
|
|
if note.name != "note":
|
|
continue
|
|
|
|
if note.child_value("count") != 0:
|
|
# Actual song
|
|
music.append(
|
|
{
|
|
"id": songid,
|
|
"chart": chart,
|
|
"rank": note.child_value("rank"),
|
|
"halo": note.child_value("clearkind"),
|
|
"score": note.child_value("score"),
|
|
"ghostid": note.child_value("ghostid"),
|
|
}
|
|
)
|
|
|
|
chart = chart + 1
|
|
|
|
return (
|
|
resp.child_value("playerdata/is_new"),
|
|
music,
|
|
)
|
|
|
|
def verify_playerdata_usergamedata_advanced_inheritance(self, refid: str, locid: str) -> None:
|
|
call = self.call_node()
|
|
|
|
# Construct node
|
|
playerdata = Node.void("playerdata")
|
|
call.add_child(playerdata)
|
|
playerdata.set_attribute("method", "usergamedata_advanced")
|
|
playerdata.add_child(Node.u32("retrycnt", 0))
|
|
info = Node.void("info")
|
|
playerdata.add_child(info)
|
|
info.add_child(Node.s32("version", 1))
|
|
data = Node.void("data")
|
|
playerdata.add_child(data)
|
|
data.add_child(Node.string("mode", "inheritance"))
|
|
data.add_child(Node.string("locid", locid))
|
|
data.add_child(Node.s64("gamesession", 123456))
|
|
data.add_child(Node.string("refid", refid))
|
|
data.add_child(Node.string("dataid", refid))
|
|
data.add_child(Node.string("gamekind", "MDX"))
|
|
data.add_child(Node.string("pcbid", self.pcbid))
|
|
data.add_child(Node.void("record"))
|
|
|
|
# Swap with server
|
|
resp = self.exchange("", call)
|
|
|
|
# Verify that response is correct
|
|
self.assert_path(resp, "response/playerdata/InheritanceStatus")
|
|
self.assert_path(resp, "response/playerdata/result")
|
|
|
|
def verify_playerdata_usergamedata_advanced_usersave(
|
|
self,
|
|
refid: str,
|
|
extid: int,
|
|
locid: str,
|
|
score: Dict[str, Any],
|
|
scorepos: int = 0,
|
|
) -> None:
|
|
call = self.call_node()
|
|
|
|
# Construct node
|
|
playerdata = Node.void("playerdata")
|
|
call.add_child(playerdata)
|
|
playerdata.set_attribute("method", "usergamedata_advanced")
|
|
playerdata.add_child(Node.u32("retrycnt", 0))
|
|
info = Node.void("info")
|
|
playerdata.add_child(info)
|
|
info.add_child(Node.s32("version", 1))
|
|
data = Node.void("data")
|
|
playerdata.add_child(data)
|
|
data.add_child(Node.string("mode", "usersave"))
|
|
data.add_child(Node.string("name", self.NAME))
|
|
data.add_child(Node.s32("ddrcode", extid))
|
|
data.add_child(Node.s32("playside", 1))
|
|
data.add_child(Node.s32("playstyle", 0))
|
|
data.add_child(Node.s32("area", 58))
|
|
data.add_child(Node.s32("weight100", 0))
|
|
data.add_child(Node.string("shopname", "gmw="))
|
|
data.add_child(Node.bool("ispremium", False))
|
|
data.add_child(Node.bool("iseapass", True))
|
|
data.add_child(Node.bool("istakeover", False))
|
|
data.add_child(Node.bool("isrepeater", False))
|
|
data.add_child(Node.bool("isgameover", scorepos < 0))
|
|
data.add_child(Node.string("locid", locid))
|
|
data.add_child(Node.string("shoparea", "."))
|
|
data.add_child(Node.s64("gamesession", 123456))
|
|
data.add_child(Node.string("refid", refid))
|
|
data.add_child(Node.string("dataid", refid))
|
|
data.add_child(Node.string("gamekind", "MDX"))
|
|
data.add_child(Node.string("pcbid", self.pcbid))
|
|
data.add_child(Node.void("record"))
|
|
|
|
for i in range(5):
|
|
if i == scorepos:
|
|
# Fill in score here
|
|
note = Node.void("note")
|
|
data.add_child(note)
|
|
note.add_child(Node.u8("stagenum", i + 1))
|
|
note.add_child(Node.u32("mcode", score["id"]))
|
|
note.add_child(Node.u8("notetype", score["chart"]))
|
|
note.add_child(Node.u8("rank", score["rank"]))
|
|
note.add_child(Node.u8("clearkind", score["halo"]))
|
|
note.add_child(Node.s32("score", score["score"]))
|
|
note.add_child(Node.s32("exscore", 0))
|
|
note.add_child(Node.s32("maxcombo", 0))
|
|
note.add_child(Node.s32("life", 0))
|
|
note.add_child(Node.s32("fastcount", 0))
|
|
note.add_child(Node.s32("slowcount", 0))
|
|
note.add_child(Node.s32("judge_marvelous", 0))
|
|
note.add_child(Node.s32("judge_perfect", 0))
|
|
note.add_child(Node.s32("judge_great", 0))
|
|
note.add_child(Node.s32("judge_good", 0))
|
|
note.add_child(Node.s32("judge_boo", 0))
|
|
note.add_child(Node.s32("judge_miss", 0))
|
|
note.add_child(Node.s32("judge_ok", 0))
|
|
note.add_child(Node.s32("judge_ng", 0))
|
|
note.add_child(Node.s32("calorie", 0))
|
|
note.add_child(Node.s32("ghostsize", len(score["ghost"])))
|
|
note.add_child(Node.string("ghost", score["ghost"]))
|
|
note.add_child(Node.u8("opt_speed", 0))
|
|
note.add_child(Node.u8("opt_boost", 0))
|
|
note.add_child(Node.u8("opt_appearance", 0))
|
|
note.add_child(Node.u8("opt_turn", 0))
|
|
note.add_child(Node.u8("opt_dark", 0))
|
|
note.add_child(Node.u8("opt_scroll", 0))
|
|
note.add_child(Node.u8("opt_arrowcolor", 0))
|
|
note.add_child(Node.u8("opt_cut", 0))
|
|
note.add_child(Node.u8("opt_freeze", 0))
|
|
note.add_child(Node.u8("opt_jump", 0))
|
|
note.add_child(Node.u8("opt_arrowshape", 0))
|
|
note.add_child(Node.u8("opt_filter", 0))
|
|
note.add_child(Node.u8("opt_guideline", 0))
|
|
note.add_child(Node.u8("opt_gauge", 0))
|
|
note.add_child(Node.u8("opt_judgepriority", 0))
|
|
note.add_child(Node.u8("opt_timing", 0))
|
|
note.add_child(Node.string("basename", ""))
|
|
note.add_child(Node.string("title_b64", ""))
|
|
note.add_child(Node.string("artist_b64", ""))
|
|
note.add_child(Node.u16("bpmMax", 0))
|
|
note.add_child(Node.u16("bpmMin", 0))
|
|
note.add_child(Node.u8("level", 0))
|
|
note.add_child(Node.u8("series", 0))
|
|
note.add_child(Node.u32("bemaniFlag", 0))
|
|
note.add_child(Node.u32("genreFlag", 0))
|
|
note.add_child(Node.u8("limited", 0))
|
|
note.add_child(Node.u8("region", 0))
|
|
note.add_child(Node.s32("gr_voltage", 0))
|
|
note.add_child(Node.s32("gr_stream", 0))
|
|
note.add_child(Node.s32("gr_chaos", 0))
|
|
note.add_child(Node.s32("gr_freeze", 0))
|
|
note.add_child(Node.s32("gr_air", 0))
|
|
note.add_child(Node.bool("share", False))
|
|
note.add_child(Node.u64("endtime", 0))
|
|
note.add_child(Node.s32("folder", 0))
|
|
else:
|
|
note = Node.void("note")
|
|
data.add_child(note)
|
|
note.add_child(Node.u8("stagenum", 0))
|
|
note.add_child(Node.u32("mcode", 0))
|
|
note.add_child(Node.u8("notetype", 0))
|
|
note.add_child(Node.u8("rank", 0))
|
|
note.add_child(Node.u8("clearkind", 0))
|
|
note.add_child(Node.s32("score", 0))
|
|
note.add_child(Node.s32("exscore", 0))
|
|
note.add_child(Node.s32("maxcombo", 0))
|
|
note.add_child(Node.s32("life", 0))
|
|
note.add_child(Node.s32("fastcount", 0))
|
|
note.add_child(Node.s32("slowcount", 0))
|
|
note.add_child(Node.s32("judge_marvelous", 0))
|
|
note.add_child(Node.s32("judge_perfect", 0))
|
|
note.add_child(Node.s32("judge_great", 0))
|
|
note.add_child(Node.s32("judge_good", 0))
|
|
note.add_child(Node.s32("judge_boo", 0))
|
|
note.add_child(Node.s32("judge_miss", 0))
|
|
note.add_child(Node.s32("judge_ok", 0))
|
|
note.add_child(Node.s32("judge_ng", 0))
|
|
note.add_child(Node.s32("calorie", 0))
|
|
note.add_child(Node.s32("ghostsize", 0))
|
|
note.add_child(Node.string("ghost", ""))
|
|
note.add_child(Node.u8("opt_speed", 0))
|
|
note.add_child(Node.u8("opt_boost", 0))
|
|
note.add_child(Node.u8("opt_appearance", 0))
|
|
note.add_child(Node.u8("opt_turn", 0))
|
|
note.add_child(Node.u8("opt_dark", 0))
|
|
note.add_child(Node.u8("opt_scroll", 0))
|
|
note.add_child(Node.u8("opt_arrowcolor", 0))
|
|
note.add_child(Node.u8("opt_cut", 0))
|
|
note.add_child(Node.u8("opt_freeze", 0))
|
|
note.add_child(Node.u8("opt_jump", 0))
|
|
note.add_child(Node.u8("opt_arrowshape", 0))
|
|
note.add_child(Node.u8("opt_filter", 0))
|
|
note.add_child(Node.u8("opt_guideline", 0))
|
|
note.add_child(Node.u8("opt_gauge", 0))
|
|
note.add_child(Node.u8("opt_judgepriority", 0))
|
|
note.add_child(Node.u8("opt_timing", 0))
|
|
note.add_child(Node.string("basename", ""))
|
|
note.add_child(Node.string("title_b64", ""))
|
|
note.add_child(Node.string("artist_b64", ""))
|
|
note.add_child(Node.u16("bpmMax", 0))
|
|
note.add_child(Node.u16("bpmMin", 0))
|
|
note.add_child(Node.u8("level", 0))
|
|
note.add_child(Node.u8("series", 0))
|
|
note.add_child(Node.u32("bemaniFlag", 0))
|
|
note.add_child(Node.u32("genreFlag", 0))
|
|
note.add_child(Node.u8("limited", 0))
|
|
note.add_child(Node.u8("region", 0))
|
|
note.add_child(Node.s32("gr_voltage", 0))
|
|
note.add_child(Node.s32("gr_stream", 0))
|
|
note.add_child(Node.s32("gr_chaos", 0))
|
|
note.add_child(Node.s32("gr_freeze", 0))
|
|
note.add_child(Node.s32("gr_air", 0))
|
|
note.add_child(Node.bool("share", False))
|
|
note.add_child(Node.u64("endtime", 0))
|
|
note.add_child(Node.s32("folder", 0))
|
|
|
|
# Swap with server
|
|
resp = self.exchange("", call)
|
|
|
|
# Verify that response is correct
|
|
self.assert_path(resp, "response/playerdata/result")
|
|
|
|
def verify_usergamedata_send(self, ref_id: str, ext_id: int, msg_type: str, send_only_common: bool = False) -> None:
|
|
call = self.call_node()
|
|
|
|
# Set up profile write
|
|
profiledata = {
|
|
"COMMON": [
|
|
b"1",
|
|
b"0", # shoparea spot, filled in below
|
|
b"3c880f8",
|
|
b"1",
|
|
b"0",
|
|
b"0",
|
|
b"0",
|
|
b"0",
|
|
b"0",
|
|
b"ffffffffffffffff",
|
|
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"", # Name spot, filled in below
|
|
ID.format_extid(ext_id).encode("ascii"),
|
|
b"",
|
|
b"",
|
|
b"",
|
|
b"",
|
|
b"",
|
|
b"",
|
|
],
|
|
"OPTION": [
|
|
b"0",
|
|
b"3",
|
|
b"0",
|
|
b"0",
|
|
b"0",
|
|
b"0",
|
|
b"0",
|
|
b"3",
|
|
b"0",
|
|
b"0",
|
|
b"0",
|
|
b"0",
|
|
b"1",
|
|
b"2",
|
|
b"0",
|
|
b"0",
|
|
b"0",
|
|
b"10.000000",
|
|
b"10.000000",
|
|
b"10.000000",
|
|
b"10.000000",
|
|
b"0.000000",
|
|
b"0.000000",
|
|
b"0.000000",
|
|
b"0.000000",
|
|
b"",
|
|
b"",
|
|
b"",
|
|
b"",
|
|
b"",
|
|
b"",
|
|
b"",
|
|
b"",
|
|
],
|
|
"LAST": [
|
|
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"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"",
|
|
b"",
|
|
b"",
|
|
b"",
|
|
b"",
|
|
b"",
|
|
b"",
|
|
b"",
|
|
],
|
|
"RIVAL": [
|
|
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"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"",
|
|
b"",
|
|
b"",
|
|
b"",
|
|
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["COMMON"][1] = b"0"
|
|
profiledata["COMMON"][25] = b""
|
|
|
|
elif msg_type == "existing":
|
|
# Exiting profile gets our hardcoded name saved.
|
|
profiledata["COMMON"][1] = b"3a"
|
|
profiledata["COMMON"][25] = self.NAME.encode("shift-jis")
|
|
|
|
else:
|
|
raise Exception(f"Unknown message type {msg_type}!")
|
|
|
|
if send_only_common:
|
|
profiledata = {"COMMON": profiledata["COMMON"]}
|
|
|
|
# Construct node
|
|
playerdata = Node.void("playerdata")
|
|
call.add_child(playerdata)
|
|
playerdata.set_attribute("method", "usergamedata_send")
|
|
playerdata.add_child(Node.u32("retrycnt", 0))
|
|
info = Node.void("info")
|
|
playerdata.add_child(info)
|
|
info.add_child(Node.s32("version", 1))
|
|
data = Node.void("data")
|
|
playerdata.add_child(data)
|
|
data.add_child(Node.string("refid", ref_id))
|
|
data.add_child(Node.string("dataid", ref_id))
|
|
data.add_child(Node.string("gamekind", "MDX"))
|
|
data.add_child(Node.u32("datanum", len(profiledata.keys())))
|
|
record = Node.void("record")
|
|
data.add_child(record)
|
|
for ptype in profiledata:
|
|
profile = [b"ffffffff", ptype.encode("ascii")] + profiledata[ptype]
|
|
d = Node.string("d", base64.b64encode(b",".join(profile)).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")
|
|
info = Node.void("info")
|
|
playerdata.add_child(info)
|
|
info.add_child(Node.s32("version", 1))
|
|
data = Node.void("data")
|
|
playerdata.add_child(data)
|
|
data.add_child(Node.string("refid", ref_id))
|
|
data.add_child(Node.string("dataid", ref_id))
|
|
data.add_child(Node.string("gamekind", "MDX"))
|
|
data.add_child(Node.u32("recv_num", 4))
|
|
data.add_child(
|
|
Node.string(
|
|
"recv_csv",
|
|
"COMMON,3fffffffff,OPTION,3fffffffff,LAST,3fffffffff,RIVAL,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")
|
|
|
|
profiles = 0
|
|
name = ""
|
|
for child in resp.child("playerdata/player/record").children:
|
|
if child.name != "d":
|
|
continue
|
|
|
|
if profiles == 0:
|
|
bindata = child.value
|
|
profiledata = base64.b64decode(bindata).split(b",")
|
|
name = profiledata[25].decode("ascii")
|
|
|
|
profiles = profiles + 1
|
|
|
|
if profiles != 4:
|
|
raise Exception("Didn't receive all four profiles in the right order!")
|
|
|
|
return name
|
|
|
|
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)
|
|
|
|
# Verify the game-wide packets Ace insists on sending before profile load
|
|
is_new, music = self.verify_playerdata_usergamedata_advanced_userload("X0000000000000000000000000123456")
|
|
if not is_new:
|
|
raise Exception("Fake profiles should be new!")
|
|
if len(music) > 0:
|
|
raise Exception("Fake profiles should have no scores associated!")
|
|
|
|
# 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)
|
|
self.verify_system_convcardnumber(card)
|
|
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")
|
|
extid = self.verify_playerdata_usergamedata_advanced_usernew(ref_id)
|
|
self.verify_usergamedata_send(ref_id, extid, "new")
|
|
self.verify_playerdata_usergamedata_advanced_inheritance(ref_id, location)
|
|
name = self.verify_usergamedata_recv(ref_id)
|
|
if name != "":
|
|
raise Exception("Name stored on profile we just created!")
|
|
self.verify_usergamedata_send(ref_id, extid, "existing", send_only_common=True)
|
|
name = self.verify_usergamedata_recv(ref_id)
|
|
if name != self.NAME:
|
|
raise Exception("Name stored on profile is incorrect!")
|
|
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:
|
|
is_new, music = self.verify_playerdata_usergamedata_advanced_userload(ref_id)
|
|
if is_new:
|
|
raise Exception("Profile should not be new!")
|
|
if len(music) > 0:
|
|
raise Exception("Created profile should have no scores associated!")
|
|
|
|
# Verify score saving and updating
|
|
for phase in [1, 2]:
|
|
if phase == 1:
|
|
dummyscores = [
|
|
# An okay score on a chart
|
|
{
|
|
"id": 10,
|
|
"chart": 3,
|
|
"rank": 5,
|
|
"halo": 6,
|
|
"score": 765432,
|
|
"ghost": "765432",
|
|
},
|
|
# A good score on an easier chart of the same song
|
|
{
|
|
"id": 10,
|
|
"chart": 2,
|
|
"rank": 2,
|
|
"halo": 8,
|
|
"score": 876543,
|
|
"ghost": "876543",
|
|
},
|
|
# A bad score on a hard chart
|
|
{
|
|
"id": 479,
|
|
"chart": 2,
|
|
"rank": 11,
|
|
"halo": 6,
|
|
"score": 654321,
|
|
"ghost": "654321",
|
|
},
|
|
# A terrible score on an easy chart
|
|
{
|
|
"id": 479,
|
|
"chart": 1,
|
|
"rank": 15,
|
|
"halo": 6,
|
|
"score": 123456,
|
|
"ghost": "123456",
|
|
},
|
|
]
|
|
if phase == 2:
|
|
dummyscores = [
|
|
# A better score on the same chart
|
|
{
|
|
"id": 10,
|
|
"chart": 3,
|
|
"rank": 4,
|
|
"halo": 7,
|
|
"score": 888888,
|
|
"ghost": "888888",
|
|
},
|
|
# A worse score on another same chart
|
|
{
|
|
"id": 10,
|
|
"chart": 2,
|
|
"rank": 3,
|
|
"halo": 7,
|
|
"score": 654321,
|
|
"ghost": "654321",
|
|
"expected_score": 876543,
|
|
"expected_halo": 8,
|
|
"expected_rank": 2,
|
|
"expected_ghost": "876543",
|
|
},
|
|
]
|
|
|
|
pos = 0
|
|
for dummyscore in dummyscores:
|
|
self.verify_playerdata_usergamedata_advanced_usersave(
|
|
ref_id,
|
|
extid,
|
|
location,
|
|
dummyscore,
|
|
pos,
|
|
)
|
|
pos = pos + 1
|
|
|
|
is_new, scores = self.verify_playerdata_usergamedata_advanced_userload(ref_id)
|
|
if is_new:
|
|
raise Exception("Profile should not be new!")
|
|
if len(scores) == 0:
|
|
raise Exception("Expected some scores after saving!")
|
|
|
|
for expected in dummyscores:
|
|
actual = None
|
|
for received in scores:
|
|
if received["id"] == expected["id"] and received["chart"] == expected["chart"]:
|
|
actual = received
|
|
break
|
|
|
|
if actual is None:
|
|
raise Exception(f"Didn't find song {expected['id']} chart {expected['chart']} in response!")
|
|
|
|
if "expected_score" in expected:
|
|
expected_score = expected["expected_score"]
|
|
else:
|
|
expected_score = expected["score"]
|
|
if "expected_rank" in expected:
|
|
expected_rank = expected["expected_rank"]
|
|
else:
|
|
expected_rank = expected["rank"]
|
|
if "expected_halo" in expected:
|
|
expected_halo = expected["expected_halo"]
|
|
else:
|
|
expected_halo = expected["halo"]
|
|
|
|
if actual["score"] != expected_score:
|
|
raise Exception(
|
|
f'Expected a score of \'{expected_score}\' for song \'{expected["id"]}\' chart \'{expected["chart"]}\' but got score \'{actual["score"]}\''
|
|
)
|
|
if actual["rank"] != expected_rank:
|
|
raise Exception(
|
|
f'Expected a rank of \'{expected_rank}\' for song \'{expected["id"]}\' chart \'{expected["chart"]}\' but got rank \'{actual["rank"]}\''
|
|
)
|
|
if actual["halo"] != expected_halo:
|
|
raise Exception(
|
|
f'Expected a halo of \'{expected_halo}\' for song \'{expected["id"]}\' chart \'{expected["chart"]}\' but got halo \'{actual["halo"]}\''
|
|
)
|
|
|
|
# Now verify that the ghost for this score is what we saved
|
|
ghost = self.verify_playerdata_usergamedata_advanced_ghostload(ref_id, received["ghostid"])
|
|
if "expected_ghost" in expected:
|
|
expected_ghost = expected["expected_ghost"]
|
|
else:
|
|
expected_ghost = expected["ghost"]
|
|
|
|
if ghost["id"] != received["id"]:
|
|
raise Exception(
|
|
f'Wrong song ID \'{ghost["id"]}\' returned for ghost, expected ID \'{received["id"]}\''
|
|
)
|
|
if ghost["chart"] != received["chart"]:
|
|
raise Exception(
|
|
f'Wrong song chart \'{ghost["chart"]}\' returned for ghost, expected chart \'{received["chart"]}\''
|
|
)
|
|
if ghost["ghost"] != expected_ghost:
|
|
raise Exception(
|
|
f'Wrong ghost data \'{ghost["ghost"]}\' returned for ghost, expected \'{expected_ghost}\''
|
|
)
|
|
if ghost["extid"] != extid:
|
|
raise Exception(f'Wrong extid \'{ghost["extid"]}\' returned for ghost, expected \'{extid}\'')
|
|
|
|
# Sleep so we don't end up putting in score history on the same second
|
|
time.sleep(1)
|
|
|
|
# Simulate game over conditions
|
|
self.verify_playerdata_usergamedata_advanced_usersave(
|
|
ref_id,
|
|
extid,
|
|
location,
|
|
{},
|
|
-1,
|
|
)
|
|
else:
|
|
print("Skipping score checks for existing card")
|
|
|
|
# Verify global scores now that we've inserted some
|
|
self.verify_playerdata_usergamedata_advanced_rivalload("X0000000000000000000000000123456", 1)
|
|
self.verify_playerdata_usergamedata_advanced_rivalload("X0000000000000000000000000123456", 2)
|
|
self.verify_playerdata_usergamedata_advanced_rivalload("X0000000000000000000000000123456", 4)
|
|
|
|
# Verify paseli handling
|
|
if paseli_enabled:
|
|
print("PASELI enabled for this PCBID, executing PASELI checks")
|
|
else:
|
|
print("PASELI disabled for this PCBID, skipping PASELI checks")
|
|
return
|
|
|
|
sessid, balance = self.verify_eacoin_checkin(card)
|
|
if balance == 0:
|
|
print("Skipping PASELI consume check because card has 0 balance")
|
|
else:
|
|
self.verify_eacoin_consume(sessid, balance, random.randint(0, balance))
|
|
self.verify_eacoin_checkout(sessid)
|