diff --git a/bemani/backend/danevo/__init__.py b/bemani/backend/danevo/__init__.py new file mode 100644 index 0000000..a51458f --- /dev/null +++ b/bemani/backend/danevo/__init__.py @@ -0,0 +1,8 @@ +from bemani.backend.danevo.factory import DanceEvolutionFactory +from bemani.backend.danevo.base import DanceEvolutionBase + + +__all__ = [ + "DanceEvolutionFactory", + "DanceEvolutionBase", +] diff --git a/bemani/backend/danevo/base.py b/bemani/backend/danevo/base.py new file mode 100644 index 0000000..389d6d4 --- /dev/null +++ b/bemani/backend/danevo/base.py @@ -0,0 +1,23 @@ +# vim: set fileencoding=utf-8 +from typing import Optional + +from bemani.backend.base import Base +from bemani.backend.core import CoreHandler, CardManagerHandler, PASELIHandler +from bemani.common import ( + GameConstants, +) + + +class DanceEvolutionBase(CoreHandler, CardManagerHandler, PASELIHandler, Base): + """ + Base game class for all Dance Evolution version that we support. + """ + + game: GameConstants = GameConstants.DANCE_EVOLUTION + + def previous_version(self) -> Optional["DanceEvolutionBase"]: + """ + Returns the previous version of the game, based on this game. Should + be overridden. + """ + return None diff --git a/bemani/backend/danevo/danevo.py b/bemani/backend/danevo/danevo.py new file mode 100644 index 0000000..fa2d0be --- /dev/null +++ b/bemani/backend/danevo/danevo.py @@ -0,0 +1,146 @@ +import base64 +from typing import Any, Dict + +from bemani.backend.ess import EventLogHandler +from bemani.backend.danevo.base import DanceEvolutionBase +from bemani.common import VersionConstants, Profile, CardCipher, Time +from bemani.protocol import Node + + +class DanceEvolution( + EventLogHandler, + DanceEvolutionBase, +): + name: str = "Dance Evolution" + version: int = VersionConstants.DANCE_EVOLUTION + + @classmethod + def get_settings(cls) -> Dict[str, Any]: + """ + Return all of our front-end modifiably settings. + """ + return {} + + def handle_tax_get_phase_request(self, request: Node) -> Node: + tax = Node.void("tax") + tax.add_child(Node.s32("phase", 0)) + return tax + + def handle_system_convcardnumber_request(self, request: Node) -> Node: + cardid = request.child_value("data/card_id") + cardnumber = CardCipher.encode(cardid) + + system = Node.void("system") + data = Node.void("data") + system.add_child(data) + + system.add_child(Node.s32("result", 0)) + data.add_child(Node.string("card_number", cardnumber)) + return system + + def handle_system_getmaster_request(self, request: Node) -> Node: + # See if we can grab the request + data = request.child("data") + if not data: + root = Node.void("system") + root.add_child(Node.s32("result", 0)) + return root + + # Figure out what type of messsage this is + reqtype = data.child_value("datatype") + reqkey = data.child_value("datakey") # noqa + + # System message + root = Node.void("system") + + if reqtype == "S_SRVMSG": + # Known keys include: INFO, ARK_ARR0, ARK_HAS0, SONGOPEN, IRDATA, EVTMSG3, WEEKLYSO + root.add_child(Node.string("strdata1", "")) + root.add_child(Node.string("strdata2", "")) + root.add_child(Node.u64("updatedate", Time.now() * 1000)) + root.add_child(Node.s32("result", 1)) + else: + # Unknown message. + root.add_child(Node.s32("result", 0)) + + return root + + def handle_playerdata_usergamedata_recv_request(self, request: Node) -> Node: + playerdata = Node.void("playerdata") + + player = Node.void("player") + playerdata.add_child(player) + + refid = request.child_value("data/refid") + userid = self.data.remote.user.from_refid(self.game, self.version, refid) + if userid is not None: + profile = self.get_profile(userid) + records = 0 + + record = Node.void("record") + player.add_child(record) + + def danevohex(val: int) -> str: + return hex(val)[2:] + + if profile is None: + # Just return a default empty node + record.add_child(Node.string("d", "")) + records = 1 + else: + # Figure out what profiles are being requested + profiletypes = request.child_value("data/recv_csv").split(",")[::2] + usergamedata = profile.get_dict("usergamedata") + for ptype in profiletypes: + if ptype in usergamedata: + records = records + 1 + + dnode = Node.string( + "d", + base64.b64encode(usergamedata[ptype]["strdata"]).decode("ascii"), + ) + dnode.add_child( + Node.string( + "bin1", + base64.b64encode(usergamedata[ptype]["bindata"]).decode("ascii"), + ) + ) + record.add_child(dnode) + + player.add_child(Node.u32("record_num", records)) + + playerdata.add_child(Node.s32("result", 0)) + return playerdata + + def handle_playerdata_usergamedata_send_request(self, request: Node) -> Node: + playerdata = Node.void("playerdata") + refid = request.child_value("data/refid") + + userid = self.data.remote.user.from_refid(self.game, self.version, refid) + if userid is not None: + profile = self.get_profile(userid) or Profile(self.game, self.version, refid, 0) + usergamedata = profile.get_dict("usergamedata") + + for record in request.child("data/record").children: + if record.name != "d": + continue + + strdata = base64.b64decode(record.value) + bindata = base64.b64decode(record.child_value("bin1")) + + # Grab and format the profile objects + strdatalist = strdata.split(b",") + profiletype = strdatalist[1].decode("utf-8") + strdatalist = strdatalist[2:] + + usergamedata[profiletype] = { + "strdata": b",".join(strdatalist), + "bindata": bindata, + } + + profile.replace_dict("usergamedata", usergamedata) + profile.replace_int("write_time", Time.now()) + self.put_profile(userid, profile) + + playerdata.add_child(Node.s32("result", 0)) + return playerdata diff --git a/bemani/backend/danevo/factory.py b/bemani/backend/danevo/factory.py new file mode 100644 index 0000000..ef86060 --- /dev/null +++ b/bemani/backend/danevo/factory.py @@ -0,0 +1,28 @@ +from typing import List, Optional, Type + +from bemani.backend.base import Base, Factory +from bemani.backend.danevo.danevo import DanceEvolution +from bemani.common import Model +from bemani.data import Config, Data + + +class DanceEvolutionFactory(Factory): + MANAGED_CLASSES: List[Type[Base]] = [ + DanceEvolution, + ] + + @classmethod + def register_all(cls) -> None: + for gamecode in ["KDM"]: + Base.register(gamecode, DanceEvolutionFactory) + + @classmethod + def create( + cls, + data: Data, + config: Config, + model: Model, + parentmodel: Optional[Model] = None, + ) -> Optional[Base]: + # There is only one Dance Evolution. + return DanceEvolution(data, config, model) diff --git a/bemani/common/constants.py b/bemani/common/constants.py index 5c34158..696d4ea 100644 --- a/bemani/common/constants.py +++ b/bemani/common/constants.py @@ -32,6 +32,8 @@ class VersionConstants: BISHI_BASHI_TSBB: Final[int] = 1 + DANCE_EVOLUTION: Final[int] = 1 + DDR_1STMIX: Final[int] = 1 DDR_2NDMIX: Final[int] = 2 DDR_3RDMIX: Final[int] = 3 diff --git a/bemani/utils/config.py b/bemani/utils/config.py index 8380ba0..17820f5 100644 --- a/bemani/utils/config.py +++ b/bemani/utils/config.py @@ -6,6 +6,7 @@ from bemani.backend.iidx import IIDXFactory from bemani.backend.popn import PopnMusicFactory from bemani.backend.jubeat import JubeatFactory from bemani.backend.bishi import BishiBashiFactory +from bemani.backend.danevo import DanceEvolutionFactory from bemani.backend.ddr import DDRFactory from bemani.backend.sdvx import SoundVoltexFactory from bemani.backend.reflec import ReflecBeatFactory @@ -78,3 +79,5 @@ def register_games(config: Config) -> None: MusecaFactory.register_all() if GameConstants.MGA in config.support: MetalGearArcadeFactory.register_all() + if GameConstants.DANCE_EVOLUTION in config.support: + DanceEvolutionFactory.register_all() diff --git a/bemani/utils/frontend.py b/bemani/utils/frontend.py index 5d23443..dcc66e4 100644 --- a/bemani/utils/frontend.py +++ b/bemani/utils/frontend.py @@ -47,6 +47,7 @@ def register_blueprints() -> None: app.register_blueprint(reflec_pages) if GameConstants.MUSECA in config.support: app.register_blueprint(museca_pages) + # TODO: DanEvo frontend here. def register_games() -> None: diff --git a/bemani/utils/read.py b/bemani/utils/read.py index c6cbc42..fa70f2c 100644 --- a/bemani/utils/read.py +++ b/bemani/utils/read.py @@ -6106,28 +6106,46 @@ class ImportDanceEvolution(ImportBase): for i in range(numsongs): offset = (i * 128) + 16 - songcode = get_string(offset + 0) # noqa: F841 + songcode = get_string(offset + 0) songres1 = get_string(offset + 4) # noqa: F841 songres2 = get_string(offset + 8) # noqa: F841 bpm_min = get_int(offset + 12) bpm_max = get_int(offset + 16) + + # Unknown 4 byte value at offset 20. + copyright = get_string(offset + 24, "") + + # Unknown 4 byte value at offset 28, 36, 40, 44, 48. + title = get_string(offset + 52, "Unknown song") artist = get_string(offset + 56, "Unknown artist") + + # Unknown 4 byte value at offset 60. + level = get_int(offset + 64) + + # Unknown 4 byte value at offset 68. + charares1 = get_string(offset + 72) # noqa: F841 charares2 = get_string(offset + 76) # noqa: F841 + + # Unknown 4 byte values at offset 80, 84, 88, 92, 96, 100, 104. + kana_sort = get_string(offset + 108) - flag1 = data[offset + 33] != 0x00 # noqa: F841 - flag2 = data[offset + 34] == 0x01 # noqa: F841 - flag3 = data[offset + 34] == 0x02 # noqa: F841 - flag4 = data[offset + 116] != 0x00 # noqa: F841 + # Unknown 4 byte value at offset 112. + + flag1 = data[offset + 33] != 0x00 + flag2 = data[offset + 34] == 0x01 + flag3 = data[offset + 34] == 0x02 + flag4 = data[offset + 35] != 0x00 # TODO: Get the real music ID from the data, once we have in-game traffic. retval.append( { "id": i, + "code": songcode, "title": title, "artist": artist, "copyright": copyright or None, @@ -6135,6 +6153,10 @@ class ImportDanceEvolution(ImportBase): "bpm_min": bpm_min, "bpm_max": bpm_max, "level": level, + "flag1": flag1, + "flag2": flag2, + "flag3": flag3, + "flag4": flag4, } ) diff --git a/bemani/utils/scheduler.py b/bemani/utils/scheduler.py index 1dd31f3..60f9cea 100644 --- a/bemani/utils/scheduler.py +++ b/bemani/utils/scheduler.py @@ -10,6 +10,7 @@ from bemani.backend.ddr import DDRFactory from bemani.backend.sdvx import SoundVoltexFactory from bemani.backend.reflec import ReflecBeatFactory from bemani.backend.museca import MusecaFactory +from bemani.backend.danevo import DanceEvolutionFactory from bemani.frontend.popn import PopnMusicCache from bemani.frontend.iidx import IIDXCache from bemani.frontend.jubeat import JubeatCache @@ -57,6 +58,9 @@ def run_scheduled_work(config: Config) -> None: if GameConstants.MUSECA in config.support: enabled_factories.append(MusecaFactory) enabled_caches.append(MusecaCache) + if GameConstants.DANCE_EVOLUTION in config.support: + enabled_factories.append(DanceEvolutionFactory) + # TODO: Frontend cache here. # First, run any backend scheduled work for factory in enabled_factories: diff --git a/bemani/utils/trafficgen.py b/bemani/utils/trafficgen.py index 01449b1..f67a648 100644 --- a/bemani/utils/trafficgen.py +++ b/bemani/utils/trafficgen.py @@ -315,6 +315,7 @@ def get_client(proto: ClientProtocol, pcbid: str, game: str, config: Dict[str, A pcbid, config, ) + # TODO: DanEvo client here. raise Exception(f"Unknown game {game}") diff --git a/config/server.yaml b/config/server.yaml index eb2b46c..897904f 100644 --- a/config/server.yaml +++ b/config/server.yaml @@ -71,14 +71,16 @@ paseli: support: # Bishi Bashi frontend/backend enabled. bishi: True - # Metal Gear Arcade frontend/backend enabled - mga: True + # Dance Evolution frontend/backend enable. + danevo: True # DDR frontend/backend enabled ddr: True # IIDX frontend/backend enabled. iidx: True # Jubeat frontend/backend enabled. jubeat: True + # Metal Gear Arcade frontend/backend enabled + mga: True # Museca frontend/backend enabled. museca: True # Pop'n Music frontend/backend enabled.