import random import time from typing import Optional, Dict, List, Tuple, Any from bemani.client.base import BaseClient from bemani.protocol import Node class PopnMusicSunnyParkClient(BaseClient): NAME = "TEST" def verify_game_active(self) -> None: call = self.call_node() # Construct node game = Node.void("game") call.add_child(game) game.set_attribute("method", "active") # Add minimum amount of stuff so server accepts game.add_child(Node.s8("event", 0)) # Swap with server resp = self.exchange("pnm20/game", call) # Verify that response is correct self.assert_path(resp, "response/game/@status") def verify_game_get(self) -> None: call = self.call_node() # Construct node game = Node.void("game") call.add_child(game) game.set_attribute("location_id", "JP-1") game.set_attribute("method", "get") game.add_child(Node.s8("event", 0)) # Swap with server resp = self.exchange("pnm20/game", call) # Verify that response is correct self.assert_path(resp, "response/game") for name in [ "ir_phase", "music_open_phase", "collabo_phase", "personal_event_phase", "shop_event_phase", "netvs_phase", "card_phase", "other_phase", "local_matching_enable", "n_matching_sec", "l_matching_sec", "is_check_cpu", "week_no", ]: node = resp.child("game").child(name) if node is None: raise Exception(f"Missing node '{name}' in response!") if node.data_type != "s32": raise Exception(f"Node '{name}' has wrong data type!") sel_ranking = resp.child("game").child("sel_ranking") up_ranking = resp.child("game").child("up_ranking") for nodepair in [("sel_ranking", sel_ranking), ("up_ranking", up_ranking)]: name = nodepair[0] node = nodepair[1] if node is None: raise Exception(f"Missing node '{name}' in response!") if node.data_type != "s16": raise Exception(f"Node '{name}' has wrong data type!") if not node.is_array: raise Exception(f"Node '{name}' is not array!") if len(node.value) != 5: raise Exception(f"Node '{name}' is wrong array length!") def verify_playerdata_get( self, ref_id: str, msg_type: str ) -> Optional[Dict[str, Any]]: call = self.call_node() # Construct node playerdata = Node.void("playerdata") call.add_child(playerdata) playerdata.set_attribute("method", "get") if msg_type == "new": playerdata.set_attribute( "model", self.config["old_profile_model"].split(":")[0] ) playerdata.add_child(Node.string("ref_id", ref_id)) playerdata.add_child(Node.string("shop_name", "")) playerdata.add_child(Node.s8("pref", 51)) if msg_type == "new": playerdata.add_child(Node.s32("ir_num", 0)) elif msg_type == "query": playerdata.add_child(Node.s32("gakuen", 2)) playerdata.add_child(Node.s32("zoo", 1)) playerdata.add_child(Node.s32("floor_infection", 1)) playerdata.add_child(Node.s32("triple_journey", 1)) playerdata.add_child(Node.s32("baseball", 1)) # Swap with server resp = self.exchange("pnm20/playerdata", call) if msg_type == "new": # Verify that response is correct self.assert_path(resp, "response/playerdata/@status") status = int(resp.child("playerdata").attribute("status")) if status != 109: raise Exception( f"Reference ID '{ref_id}' returned invalid status '{status}'" ) # No score data return None elif msg_type == "query": # Verify that the response is correct self.assert_path(resp, "response/playerdata/base/name") self.assert_path(resp, "response/playerdata/base/g_pm_id") self.assert_path(resp, "response/playerdata/base/my_best") self.assert_path(resp, "response/playerdata/base/latest_music") self.assert_path(resp, "response/playerdata/avatar") self.assert_path(resp, "response/playerdata/avatar_add") self.assert_path(resp, "response/playerdata/netvs") self.assert_path(resp, "response/playerdata/sp_data") self.assert_path(resp, "response/playerdata/hiscore") name = resp.child("playerdata").child("base").child("name").value if name != self.NAME: raise Exception(f"Invalid name '{name}' returned for Ref ID '{ref_id}'") # Extract and return score data self.assert_path(resp, "response/playerdata/base/clear_medal") def transform_medals(medal: int) -> Tuple[int, int, int, int]: return ( (medal >> 0) & 0xF, (medal >> 4) & 0xF, (medal >> 8) & 0xF, (medal >> 12) & 0xF, ) medals = [ transform_medals(medal) for medal in resp.child("playerdata") .child("base") .child("clear_medal") .value ] hiscore = resp.child("playerdata").child("hiscore").value hiscores = [] for i in range(0, len(hiscore) * 8, 17): byte_offset = int(i / 8) bit_offset = int(i % 8) value = hiscore[byte_offset] value = value + (hiscore[byte_offset + 1] << 8) value = value + (hiscore[byte_offset + 2] << 16) value = value >> bit_offset hiscores.append(value & 0x1FFFF) scores = [ (hiscores[x], hiscores[x + 1], hiscores[x + 2], hiscores[x + 3]) for x in range(0, len(hiscores), 4) ] return {"medals": medals, "scores": scores} else: raise Exception(f"Unrecognized message type '{msg_type}'") def verify_playerdata_set(self, ref_id: str, scores: List[Dict[str, Any]]) -> None: call = self.call_node() # Construct node playerdata = Node.void("playerdata") call.add_child(playerdata) playerdata.set_attribute("method", "set") playerdata.set_attribute("ref_id", ref_id) playerdata.set_attribute("shop_name", "") # Add required children playerdata.add_child(Node.s16("chara", 1543)) # Add requested scores for score in scores: stage = Node.void("stage") playerdata.add_child(stage) stage.add_child(Node.s16("no", score["id"])) stage.add_child(Node.u8("sheet", score["chart"])) stage.add_child( Node.u16("n_data", (score["medal"] << (4 * score["chart"]))) ) stage.add_child(Node.s32("score", score["score"])) # Swap with server resp = self.exchange("pnm20/playerdata", call) # Verify nodes that cause crashes if they don't exist self.assert_path(resp, "response/playerdata/name") name = resp.child("playerdata").child("name").value if name != self.NAME: raise Exception(f"Invalid name '{name}' returned for Ref ID '{ref_id}'") def verify_playerdata_new(self, ref_id: str) -> None: call = self.call_node() # Construct node playerdata = Node.void("playerdata") call.add_child(playerdata) playerdata.set_attribute("method", "new") playerdata.add_child(Node.string("ref_id", ref_id)) playerdata.add_child(Node.string("name", self.NAME)) playerdata.add_child(Node.string("shop_name", "")) playerdata.add_child(Node.s8("pref", 51)) playerdata.add_child(Node.s8("gakuen", 2)) playerdata.add_child(Node.s8("zoo", 1)) playerdata.add_child(Node.s8("floor_infection", 1)) playerdata.add_child(Node.s8("triple_journey", 1)) playerdata.add_child(Node.s8("baseball", 1)) # Swap with server resp = self.exchange("pnm20/playerdata", call) # Verify nodes that cause crashes if they don't exist self.assert_path(resp, "response/playerdata/base") 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() self.verify_facility_get() self.verify_pcbevent_put() self.verify_game_active() self.verify_game_get() # 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") self.verify_playerdata_get(ref_id, msg_type="new") self.verify_playerdata_new(ref_id) 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 score handling scores = self.verify_playerdata_get(ref_id, msg_type="query") if scores is None: raise Exception("Expected to get scores back, didn't get anything!") for medal in scores["medals"]: for i in range(4): if medal[i] != 0: raise Exception("Got nonzero medals count on a new card!") for score in scores["scores"]: for i in range(4): if score[i] != 0: raise Exception("Got nonzero scores count on a new card!") for phase in [1, 2]: if phase == 1: dummyscores = [ # An okay score on a chart { "id": 987, "chart": 2, "medal": 5, "score": 76543, }, # A good score on an easier chart of the same song { "id": 987, "chart": 0, "medal": 6, "score": 99999, }, # A bad score on a hard chart { "id": 741, "chart": 3, "medal": 2, "score": 45000, }, # A terrible score on an easy chart { "id": 742, "chart": 1, "medal": 2, "score": 1, }, ] # Random score to add in songid = random.randint(907, 950) chartid = random.randint(0, 3) score = random.randint(0, 100000) medal = random.choice([1, 2, 3, 5, 6, 7, 9, 10, 11, 15]) dummyscores.append( { "id": songid, "chart": chartid, "medal": medal, "score": score, } ) if phase == 2: dummyscores = [ # A better score on the same chart { "id": 987, "chart": 2, "medal": 5, "score": 98765, }, # A worse score on another same chart { "id": 987, "chart": 0, "medal": 3, "score": 12345, "expected_score": 99999, "expected_medal": 6, }, ] self.verify_playerdata_set(ref_id, dummyscores) scores = self.verify_playerdata_get(ref_id, msg_type="query") for score in dummyscores: newscore = scores["scores"][score["id"]][score["chart"]] newmedal = scores["medals"][score["id"]][score["chart"]] if "expected_score" in score: expected_score = score["expected_score"] else: expected_score = score["score"] if "expected_medal" in score: expected_medal = score["expected_medal"] else: expected_medal = score["medal"] if newscore != expected_score: raise Exception( f'Expected a score of \'{expected_score}\' for song \'{score["id"]}\' chart \'{score["chart"]}\' but got score \'{newscore}\'' ) if newmedal != expected_medal: raise Exception( f'Expected a medal of \'{expected_medal}\' for song \'{score["id"]}\' chart \'{score["chart"]}\' but got medal \'{newmedal}\'' ) # 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 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)