Check in initial implementation of Dance Evolution. No events or server settings yet, but basic profile works.

This commit is contained in:
Jennifer Taylor 2025-08-14 02:04:31 +00:00
parent 3435f61189
commit 51d67ca2b8
11 changed files with 247 additions and 7 deletions

View File

@ -0,0 +1,8 @@
from bemani.backend.danevo.factory import DanceEvolutionFactory
from bemani.backend.danevo.base import DanceEvolutionBase
__all__ = [
"DanceEvolutionFactory",
"DanceEvolutionBase",
]

View File

@ -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

View File

@ -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", "<NODATA>"))
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

View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -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:

View File

@ -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,
}
)

View File

@ -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:

View File

@ -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}")

View File

@ -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.