import random import time from typing import Dict, List, Optional from bemani.common import Time from bemani.client.base import BaseClient from bemani.protocol import Node class ReflecBeat(BaseClient): NAME = "TEST" def verify_log_pcb_status(self, loc: str) -> None: call = self.call_node() pcb = Node.void("log") pcb.set_attribute("method", "pcb_status") pcb.add_child(Node.string("lid", loc)) pcb.add_child(Node.u8("type", 0)) call.add_child(pcb) # Swap with server resp = self.exchange("", call) # Verify that response is correct self.assert_path(resp, "response/log/@status") def verify_pcbinfo_get(self, loc: str) -> None: call = self.call_node() pcb = Node.void("pcbinfo") pcb.set_attribute("method", "get") pcb.add_child(Node.string("lid", loc)) call.add_child(pcb) # Swap with server resp = self.exchange("", call) # Verify that response is correct self.assert_path(resp, "response/pcbinfo/info/name") self.assert_path(resp, "response/pcbinfo/info/pref") self.assert_path(resp, "response/pcbinfo/info/close") self.assert_path(resp, "response/pcbinfo/info/hour") self.assert_path(resp, "response/pcbinfo/info/min") def verify_sysinfo_get(self) -> None: call = self.call_node() info = Node.void("sysinfo") info.set_attribute("method", "get") call.add_child(info) # Swap with server resp = self.exchange("", call) # Verify that response is correct self.assert_path(resp, "response/sysinfo/trd") def verify_sysinfo_fan(self, loc: str) -> None: call = self.call_node() info = Node.void("sysinfo") info.set_attribute("method", "fan") info.add_child(Node.u8("pref", 0)) info.add_child(Node.string("lid", loc)) call.add_child(info) # Swap with server resp = self.exchange("", call) # Verify that response is correct self.assert_path(resp, "response/sysinfo/pref") self.assert_path(resp, "response/sysinfo/lid") def verify_player_start(self, refid: str) -> None: call = self.call_node() player = Node.void("player") player.set_attribute("method", "start") player.add_child(Node.string("rid", refid)) player.add_child(Node.s32("ver", 3)) call.add_child(player) # Swap with server resp = self.exchange("", call) # Verify that response is correct self.assert_path(resp, "response/player/is_suc") def verify_player_delete(self, refid: str) -> None: call = self.call_node() player = Node.void("player") player.set_attribute("method", "delete") player.add_child(Node.string("rid", refid)) call.add_child(player) # Swap with server resp = self.exchange("", call) # Verify that response is correct self.assert_path(resp, "response/player/@status") def verify_player_end(self, refid: str) -> None: call = self.call_node() player = Node.void("player") player.set_attribute("method", "end") player.add_child(Node.string("rid", refid)) call.add_child(player) # Swap with server resp = self.exchange("", call) # Verify that response is correct self.assert_path(resp, "response/player") def verify_player_read(self, refid: str, location: str) -> List[Dict[str, int]]: call = self.call_node() player = Node.void("player") player.set_attribute("method", "read") player.add_child(Node.string("rid", refid)) player.add_child(Node.string("lid", location)) player.add_child(Node.s32("ver", 3)) call.add_child(player) # Swap with server resp = self.exchange("", call) # Verify that response is correct self.assert_path(resp, "response/player/pdata/base/uid") self.assert_path(resp, "response/player/pdata/base/name") self.assert_path(resp, "response/player/pdata/base/lv") self.assert_path(resp, "response/player/pdata/base/exp") self.assert_path(resp, "response/player/pdata/base/mg") self.assert_path(resp, "response/player/pdata/base/ap") self.assert_path(resp, "response/player/pdata/base/flag") self.assert_path(resp, "response/player/pdata/con/day") self.assert_path(resp, "response/player/pdata/con/cnt") self.assert_path(resp, "response/player/pdata/con/last") self.assert_path(resp, "response/player/pdata/con/now") self.assert_path(resp, "response/player/pdata/team/id") self.assert_path(resp, "response/player/pdata/team/name") self.assert_path(resp, "response/player/pdata/custom/bgm_m") self.assert_path(resp, "response/player/pdata/custom/st_f") self.assert_path(resp, "response/player/pdata/custom/st_bg") self.assert_path(resp, "response/player/pdata/custom/st_bg_b") self.assert_path(resp, "response/player/pdata/custom/eff_e") self.assert_path(resp, "response/player/pdata/custom/se_s") self.assert_path(resp, "response/player/pdata/custom/se_s_v") self.assert_path(resp, "response/player/pdata/released") self.assert_path(resp, "response/player/pdata/record") self.assert_path(resp, "response/player/pdata/blog") self.assert_path(resp, "response/player/pdata/cmnt") if resp.child_value("player/pdata/base/name") != self.NAME: raise Exception( f'Invalid name {resp.child_value("player/pdata/base/name")} returned on profile read!' ) scores = [] for child in resp.child("player/pdata/record").children: if child.name != "rec": continue score = { "id": child.child_value("mid"), "chart": child.child_value("ng"), "clear_type": child.child_value("ct"), "achievement_rate": child.child_value("ar"), "score": child.child_value("bs"), "combo": child.child_value("mc"), "miss_count": child.child_value("bmc"), } scores.append(score) return scores def verify_player_write( self, refid: str, extid: int, loc: str, records: List[Dict[str, int]], scores: List[Dict[str, int]], ) -> int: call = self.call_node() player = Node.void("player") call.add_child(player) player.set_attribute("method", "write") player.add_child(Node.string("rid", refid)) player.add_child(Node.string("lid", loc)) pdata = Node.void("pdata") player.add_child(pdata) base = Node.void("base") pdata.add_child(base) base.add_child(Node.s32("uid", extid)) base.add_child(Node.string("name", self.NAME)) base.add_child(Node.s16("lv", 1)) base.add_child(Node.s32("exp", 0)) base.add_child(Node.s16("mg", 0)) base.add_child(Node.s16("ap", 0)) base.add_child(Node.s32("flag", 0)) con = Node.void("con") pdata.add_child(con) con.add_child(Node.s32("day", 0)) con.add_child(Node.s32("cnt", 0)) con.add_child(Node.s32("last", 0)) con.add_child(Node.s32("now", 0)) custom = Node.void("custom") pdata.add_child(custom) custom.add_child(Node.u8("bgm_m", 0)) custom.add_child(Node.u8("st_f", 0)) custom.add_child(Node.u8("st_bg", 0)) custom.add_child(Node.u8("st_bg_b", 100)) custom.add_child(Node.u8("eff_e", 0)) custom.add_child(Node.u8("se_s", 0)) custom.add_child(Node.u8("se_s_v", 100)) pdata.add_child(Node.void("released")) # First, filter down to only records that are also in the battle log def key(thing: Dict[str, int]) -> str: return f'{thing["id"]}-{thing["chart"]}' updates = [key(score) for score in scores] sortedrecords = { key(record): record for record in records if key(record) in updates } # Now, see what records need updating and update them for score in scores: if key(score) in sortedrecords: # Had a record, need to merge record = sortedrecords[key(score)] else: # First time playing record = { "clear_type": 0, "achievement_rate": 0, "score": 0, "combo": 0, "miss_count": 999999999, } sortedrecords[key(score)] = { "id": score["id"], "chart": score["chart"], "clear_type": max(record["clear_type"], score["clear_type"]), "achievement_rate": max( record["achievement_rate"], score["achievement_rate"] ), "score": max(record["score"], score["score"]), "combo": max(record["combo"], score["combo"]), "miss_count": min(record["miss_count"], score["miss_count"]), } # Finally, send the records and battle logs recordnode = Node.void("record") pdata.add_child(recordnode) blog = Node.void("blog") pdata.add_child(blog) for _, record in sortedrecords.items(): rec = Node.void("rec") recordnode.add_child(rec) rec.add_child(Node.u16("mid", record["id"])) rec.add_child(Node.u8("ng", record["chart"])) rec.add_child(Node.s32("win", 1)) rec.add_child(Node.s32("lose", 0)) rec.add_child(Node.s32("draw", 0)) rec.add_child(Node.u8("ct", record["clear_type"])) rec.add_child(Node.s16("ar", record["achievement_rate"])) rec.add_child(Node.s16("bs", record["score"])) rec.add_child(Node.s16("mc", record["combo"])) rec.add_child(Node.s16("bmc", record["miss_count"])) scoreid = 0 for score in scores: log = Node.void("log") blog.add_child(log) log.add_child(Node.u8("id", scoreid)) log.add_child(Node.u16("mid", score["id"])) log.add_child(Node.u8("ng", score["chart"])) log.add_child(Node.u8("mt", 0)) log.add_child(Node.u8("rt", 0)) log.add_child(Node.s32("ruid", 0)) myself = Node.void("myself") log.add_child(myself) myself.add_child(Node.s16("mg", 0)) myself.add_child(Node.s16("ap", 0)) myself.add_child(Node.u8("ct", score["clear_type"])) myself.add_child(Node.s16("s", score["score"])) myself.add_child(Node.s16("ar", score["achievement_rate"])) rival = Node.void("rival") log.add_child(rival) rival.add_child(Node.s16("mg", 0)) rival.add_child(Node.s16("ap", 0)) rival.add_child(Node.u8("ct", 2)) rival.add_child(Node.s16("s", 177)) rival.add_child(Node.s16("ar", 500)) log.add_child(Node.s32("time", Time.now())) scoreid = scoreid + 1 # Swap with server resp = self.exchange("", call) # Verify that response is correct self.assert_path(resp, "response/player/uid") self.assert_path(resp, "response/player/time") return resp.child_value("player/uid") def verify_log_play( self, extid: int, loc: str, scores: List[Dict[str, int]] ) -> None: call = self.call_node() log = Node.void("log") call.add_child(log) log.set_attribute("method", "play") log.add_child(Node.s32("uid", extid)) log.add_child(Node.string("lid", loc)) play = Node.void("play") log.add_child(play) play.add_child(Node.s16("stage", len(scores))) play.add_child(Node.s32("sec", 700)) scoreid = 0 for score in scores: rec = Node.void("rec") log.add_child(rec) rec.add_child(Node.s16("idx", scoreid)) rec.add_child(Node.s16("mid", score["id"])) rec.add_child(Node.s16("grade", score["chart"])) rec.add_child(Node.s16("color", 0)) rec.add_child(Node.s16("match", 0)) rec.add_child(Node.s16("res", 0)) rec.add_child(Node.s16("score", score["score"])) rec.add_child(Node.s16("mc", score["combo"])) rec.add_child(Node.s16("jt_jr", 0)) rec.add_child(Node.s16("jt_ju", 0)) rec.add_child(Node.s16("jt_gr", 0)) rec.add_child(Node.s16("jt_gd", 0)) rec.add_child(Node.s16("jt_ms", score["miss_count"])) rec.add_child(Node.s32("sec", 200)) scoreid = scoreid + 1 # Swap with server resp = self.exchange("", call) # Verify that response is correct self.assert_path(resp, "response/log/@status") def verify_lobby_read(self, location: str, extid: int) -> None: call = self.call_node() lobby = Node.void("lobby") lobby.set_attribute("method", "read") lobby.add_child(Node.s32("uid", extid)) lobby.add_child(Node.u8("m_grade", 255)) lobby.add_child(Node.string("lid", location)) lobby.add_child(Node.s32("max", 128)) call.add_child(lobby) # Swap with server resp = self.exchange("", call) # Verify that response is correct self.assert_path(resp, "response/lobby/@status") def verify_lobby_entry(self, location: str, extid: int) -> int: call = self.call_node() lobby = Node.void("lobby") lobby.set_attribute("method", "entry") e = Node.void("e") lobby.add_child(e) e.add_child(Node.s32("eid", 0)) e.add_child(Node.u16("mid", 79)) e.add_child(Node.u8("ng", 0)) e.add_child(Node.s32("uid", extid)) e.add_child(Node.string("pn", self.NAME)) e.add_child(Node.s32("exp", 0)) e.add_child(Node.u8("mg", 0)) e.add_child(Node.s32("tid", 0)) e.add_child(Node.string("tn", "")) e.add_child(Node.string("lid", location)) e.add_child(Node.string("sn", "")) e.add_child(Node.u8("pref", 51)) e.add_child(Node.u8_array("ga", [127, 0, 0, 1])) e.add_child(Node.u16("gp", 10007)) e.add_child(Node.u8_array("la", [16, 0, 0, 0])) call.add_child(lobby) # Swap with server resp = self.exchange("", call) # Verify that response is correct self.assert_path(resp, "response/lobby/eid") self.assert_path(resp, "response/lobby/e/eid") self.assert_path(resp, "response/lobby/e/mid") self.assert_path(resp, "response/lobby/e/ng") self.assert_path(resp, "response/lobby/e/uid") self.assert_path(resp, "response/lobby/e/pn") self.assert_path(resp, "response/lobby/e/exp") self.assert_path(resp, "response/lobby/e/mg") self.assert_path(resp, "response/lobby/e/tid") self.assert_path(resp, "response/lobby/e/tn") self.assert_path(resp, "response/lobby/e/lid") self.assert_path(resp, "response/lobby/e/sn") self.assert_path(resp, "response/lobby/e/pref") self.assert_path(resp, "response/lobby/e/ga") self.assert_path(resp, "response/lobby/e/gp") self.assert_path(resp, "response/lobby/e/la") return resp.child_value("lobby/eid") def verify_lobby_delete(self, eid: int) -> None: call = self.call_node() lobby = Node.void("lobby") lobby.set_attribute("method", "delete") lobby.add_child(Node.s32("eid", eid)) call.add_child(lobby) # Swap with server resp = self.exchange("", call) # Verify that response is correct self.assert_path(resp, "response/lobby") 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_log_pcb_status(location) self.verify_pcbinfo_get(location) self.verify_sysinfo_get() self.verify_sysinfo_fan(location) # 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") # Always get a player start, regardless of new profile or not self.verify_player_start(ref_id) self.verify_player_delete(ref_id) extid = self.verify_player_write( ref_id, 0, location, [], [], ) 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") # Verify lobby functionality self.verify_lobby_read(location, extid) eid = self.verify_lobby_entry(location, extid) self.verify_lobby_delete(eid) # Original reflec is weird and sends only the top record for each song you played, # and then a separate battle log. So, emulating that is kinda hard. scores: List[Dict[str, int]] = [] if cardid is None: # Verify score saving and updating for phase in [1, 2]: if phase == 1: dummyscores = [ # An okay score on a chart { "id": 1, "chart": 1, "clear_type": 2, "achievement_rate": 7543, "score": 432, "combo": 123, "miss_count": 5, }, # A good score on an easier chart of the same song { "id": 1, "chart": 0, "clear_type": 3, "achievement_rate": 9876, "score": 543, "combo": 543, "miss_count": 0, }, # A bad score on a hard chart { "id": 3, "chart": 2, "clear_type": 2, "achievement_rate": 1234, "score": 123, "combo": 42, "miss_count": 54, }, # A terrible score on an easy chart { "id": 3, "chart": 0, "clear_type": 2, "achievement_rate": 1024, "score": 50, "combo": 12, "miss_count": 90, }, ] if phase == 2: dummyscores = [ # A better score on the same chart { "id": 1, "chart": 1, "clear_type": 3, "achievement_rate": 8765, "score": 469, "combo": 468, "miss_count": 1, }, # A worse score on another same chart { "id": 1, "chart": 0, "clear_type": 2, "achievement_rate": 8765, "score": 432, "combo": 321, "miss_count": 15, "expected_score": 543, "expected_clear_type": 3, "expected_achievement_rate": 9876, "expected_combo": 543, "expected_miss_count": 0, }, ] self.verify_player_write(ref_id, extid, location, scores, dummyscores) self.verify_log_play(extid, location, dummyscores) scores = self.verify_player_read(ref_id, location) 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_achievement_rate" in expected: expected_achievement_rate = expected[ "expected_achievement_rate" ] else: expected_achievement_rate = expected["achievement_rate"] if "expected_clear_type" in expected: expected_clear_type = expected["expected_clear_type"] else: expected_clear_type = expected["clear_type"] if "expected_combo" in expected: expected_combo = expected["expected_combo"] else: expected_combo = expected["combo"] if "expected_miss_count" in expected: expected_miss_count = expected["expected_miss_count"] else: expected_miss_count = expected["miss_count"] 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["achievement_rate"] != expected_achievement_rate: raise Exception( f'Expected an achievement rate of \'{expected_achievement_rate}\' for song \'{expected["id"]}\' chart \'{expected["chart"]}\' but got achievement rate \'{actual["achievement_rate"]}\'' ) if actual["clear_type"] != expected_clear_type: raise Exception( f'Expected a clear_type of \'{expected_clear_type}\' for song \'{expected["id"]}\' chart \'{expected["chart"]}\' but got clear_type \'{actual["clear_type"]}\'' ) if actual["combo"] != expected_combo: raise Exception( f'Expected a combo of \'{expected_combo}\' for song \'{expected["id"]}\' chart \'{expected["chart"]}\' but got combo \'{actual["combo"]}\'' ) if actual["miss_count"] != expected_miss_count: raise Exception( f'Expected a miss count of \'{expected_miss_count}\' for song \'{expected["id"]}\' chart \'{expected["chart"]}\' but got miss count \'{actual["miss_count"]}\'' ) # Sleep so we don't end up putting in score history on the same second time.sleep(1) else: print("Skipping score checks for existing card") # Verify ending game self.verify_player_end(ref_id) # 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)