Combine module into single file to follow project pattern

This commit is contained in:
Darren Thompson
2025-08-02 16:27:57 -04:00
parent f4f1817e31
commit 8971f9d346
10 changed files with 349 additions and 377 deletions

View File

@@ -2,14 +2,15 @@
import base64
import hashlib
import html
from typing import Dict, List, Optional, Tuple
import logging
import math
from enum import IntEnum
from typing import Dict, Optional, Tuple
from typing_extensions import Final
from ctypes import *
from bemani.backend.ddr.base import DDRBase
from bemani.backend.ddr.ddrsn2.eventinfo import EventInfo
from bemani.backend.ddr.ddrsn2.playerinfo import PlayerInfo
from bemani.backend.ddr.ddrsn2.scoreinfo import ScoreInfo
from bemani.backend.ddr.stubs import DDRSuperNova
from bemani.backend.ddr.common import (
DDRGameFriendHandler,
@@ -27,10 +28,352 @@ from bemani.backend.ddr.common import (
DDRGameShopHandler,
DDRGameTraceHandler,
)
from bemani.common import Time, VersionConstants, Profile, intish
from bemani.common import VersionConstants, Profile, PlayStatistics, DBConstants, intish
from bemani.data import Score, UserID
from bemani.protocol import Node
from bemani.backend.base import Status
GAME_RANK_AAA = 0
GAME_RANK_AA = 4
GAME_RANK_A = 8
GAME_RANK_B = 13
GAME_RANK_C = 16
GAME_RANK_D = 20
GAME_RANK_E = 24
GAME_RANK_NONE = 0xFF
class ScoreRankRecordStruct(Structure):
_pack_ = 1
_fields_ = [
("lo_score", c_int8),
("score", c_int16),
("rank", c_int8, 5),
("unk1", c_int8, 1),
("yfc", c_int8, 1),
("ofc", c_int8, 1),
]
class ScoreRecordStruct(Structure):
_pack_ = 1
_fields_ = [
("beginner", ScoreRankRecordStruct),
("basic", ScoreRankRecordStruct),
("difficult", ScoreRankRecordStruct),
("expert", ScoreRankRecordStruct),
("challenge", ScoreRankRecordStruct),
("double_basic", ScoreRankRecordStruct),
("double_difficult", ScoreRankRecordStruct),
("double_expert", ScoreRankRecordStruct),
("double_challenge", ScoreRankRecordStruct),
]
class ScoreRecord:
@staticmethod
def blank() -> ScoreRecordStruct:
record = ScoreRecordStruct()
record.beginner.rank = GAME_RANK_NONE
record.basic.rank = GAME_RANK_NONE
record.difficult.rank = GAME_RANK_NONE
record.expert.rank = GAME_RANK_NONE
record.challenge.rank = GAME_RANK_NONE
record.double_basic.rank = GAME_RANK_NONE
record.double_difficult.rank = GAME_RANK_NONE
record.double_expert.rank = GAME_RANK_NONE
record.double_challenge.rank = GAME_RANK_NONE
return record
@staticmethod
def create(scores: Optional[dict[int, Score]]) -> ScoreRecordStruct:
record = ScoreRecord.blank()
if scores is not None:
for chart in scores:
if chart == 0:
chart_rec = record.beginner
elif chart == 1:
chart_rec = record.basic
elif chart == 2:
chart_rec = record.difficult
elif chart == 3:
chart_rec = record.expert
else:
chart_rec = record.challenge
score = scores[chart]
formatted_score = math.floor(score.points / 10)
hi_score = formatted_score >> 8
lo_score = formatted_score ^ (hi_score << 8)
chart_rec.lo_score = lo_score
chart_rec.score = hi_score
rank = score.data.get_int("rank")
if rank == DBConstants.DDR_RANK_AAA:
chart_rec.rank = GAME_RANK_AAA
elif rank == DBConstants.DDR_RANK_AA:
chart_rec.rank = GAME_RANK_AA
elif rank == DBConstants.DDR_RANK_A:
chart_rec.rank = GAME_RANK_A
elif rank == DBConstants.DDR_RANK_B:
chart_rec.rank = GAME_RANK_B
elif rank == DBConstants.DDR_RANK_C:
chart_rec.rank = GAME_RANK_C
elif rank == DBConstants.DDR_RANK_D:
chart_rec.rank = GAME_RANK_D
else:
chart_rec.rank = GAME_RANK_E
halo = score.data.get_int("halo")
if halo == DBConstants.DDR_HALO_PERFECT_FULL_COMBO:
chart_rec.yfc = 1
chart_rec.ofc = 1
elif halo == DBConstants.DDR_HALO_GREAT_FULL_COMBO:
chart_rec.yfc = 1
return record
class BattleRecordStruct(Structure):
_pack_ = 1
_fields_ = [
("name", c_char * 14),
("wins", c_uint16),
("loses", c_uint16),
("draws", c_uint16),
]
class BattleRecord:
@staticmethod
def create(name: str, wins: int, losses: int, draws: int) -> BattleRecordStruct:
this_name = name
if len(this_name) > 8:
this_name = this_name[:8]
logging.warning("name {} too long, truncating to {}", name, this_name)
elif len(this_name) < 8:
logging.warning("name too short, padding with spaces")
this_name = this_name.ljust(8, " ")
return BattleRecordStruct(this_name.encode("ascii"), wins, losses, draws)
class PlayerInfoStruct(Structure):
_pack_ = 1
_fields_ = [
("count", c_uint16), # 0x00
("area", c_uint16), # 0x02
("title", c_uint16), # 0x04
("flag", c_uint16), # 0x06
("id", c_uint32), # 0x08
("exp", c_uint32), # 0x0c
("weight", c_uint16), # 0x10
("last_cate", c_uint8), # 0x12
("last_mode", c_uint8), # 0x13
("last_type", c_uint8), # 0x14
("last_sort", c_uint8), # 0x15
("last_music", c_uint8), # 0x16
("_unused1", c_char * 6), # 0x17 - 0x1b
("team", c_uint8), # 0x1c
("_unused2", c_char * 8), # 0x1d - 0x24
("takeover", c_uint8), # 0x25
("count_b", c_uint8), # 0x26
("_unused3", c_char * 2), # 0x27 - 0x29
("groove_radar", c_uint16 * 5), # 0x2a
("options", c_uint8 * 32), # 0x34
("name", c_char * 14), # 0x54
("_unused4", c_char * 2), # 0x62 - 0x63
# Bit is set to 1 = already displayed, 0 = not yet displayed
("unlock_prompt_bits", c_uint8 * 12), # 0x64 - 0x70
("_unused5", c_char * 20), # 0x70 - 0x83
("course", c_uint32 * 3), # 0x84
("_unused6", c_char * (0x1344 - 0x90)),
("battle_records", BattleRecordStruct * 5), # 0x1344
]
class PlayerInfo:
@staticmethod
def create(play_stats: PlayStatistics, profile: Profile, machine_region: int) -> PlayerInfoStruct:
player = PlayerInfoStruct()
player.count = play_stats.get_int("single_plays")
player.area = profile.get_int("area", machine_region)
player.title = profile.get_int("title")
player.flag = profile.get_int("flag")
player.id = profile.extid
player.exp = play_stats.get_int("exp")
player.weight = profile.get_int("weight")
lastdict = profile.get_dict("last")
player.last_cate = lastdict.get_int("cate")
player.last_mode = lastdict.get_int("mode")
player.last_type = lastdict.get_int("type")
player.last_sort = lastdict.get_int("sort")
player.last_music = lastdict.get_int("music")
player.team = profile.get_int("team")
player.takeover = profile.get_int("takeover")
player.count_b = play_stats.get_int("battle_plays")
idx = 0
for entry in profile.get_int_array("gr_s", 5):
player.groove_radar[idx] = entry
idx += 1
idx = 0
if "opt" not in profile:
profile.replace_int_array("opt", 16, [2, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 2, 0, 0, 0, 0])
for entry in profile.get_int_array("opt", 16):
player.options[idx] = entry
idx += 1
player.name = (profile.get_str("name").ljust(8, " ")[:8]).encode("ascii")
# Default, unlock everything?
for i in range(len(player.unlock_prompt_bits)):
player.unlock_prompt_bits[i] = 0xFF
idx = 0
for entry in profile.get_int_array("course", 3):
player.course[idx] = entry
idx += 1
return player
class ScoreInfoStruct(Structure):
_pack_ = 1
_fields_ = [
("records", ScoreRecordStruct * 200),
]
class ScoreInfo:
@staticmethod
def create(scores: list[Score], part: int) -> ScoreInfoStruct:
info = ScoreInfoStruct()
scores_by_song_id: dict[int, dict[int, Score]] = {}
for score in scores:
if score.id not in scores_by_song_id:
scores_by_song_id[score.id] = {}
scores_by_song_id[score.id][score.chart] = score
for i in range(len(info.records)):
if part == 2:
song_id = i + 200
else:
song_id = i
if song_id in scores_by_song_id:
score_record = ScoreRecord.create(scores_by_song_id[song_id])
else:
score_record = ScoreRecord.create(None)
info.records[i] = score_record
return info
class GlobalRankingStateEntryStruct(Structure):
_pack_ = 1
_fields_ = [
("id", c_int8),
("state", c_int8), # 0 = disabled, 1 = English ("Global Ranking"), 2 = Japanese ("Gachinko Dance Matsuri")
("_unused", c_int8 * 2),
]
class GlobalRankingStateEntry:
@staticmethod
def create(id: int, state: int) -> GlobalRankingStateEntryStruct:
return GlobalRankingStateEntryStruct(id, state)
class ZukinTeams(IntEnum):
GREEN = 1
RED = 2
YELLOW = 3
class UnlockEntryStruct(Structure):
_pack_ = 1
_fields_ = [
("id", c_uint8),
("enabled", c_uint8),
# Set to 1 to enable. This is only read if the other flags check out. Set all other flags to 0 to force unlock.
("flag1", c_uint8), # Must be less than some value passed to check function (always 0x3d6???)
("flag2", c_uint8), # Some kind of "unlock type" flag? If 0 then other flags aren't checked.
("flag3", c_uint8), # Unused??
("flag4", c_uint8),
# If 0, flag2 (if flag2 is non-0) must match the type of unlock looked for by the checker function. If non-0, flag4 * 10 must be <= param4 of checker function (when is this used?)
]
class UnlockEntry:
@staticmethod
def create(id: int, enabled: int, flag1: int, flag2: int, flag3: int, flag4: int) -> UnlockEntryStruct:
return UnlockEntryStruct(id, enabled, flag1, flag2, flag3, flag4)
class EventInfoStruct(Structure):
_pack_ = 1
_fields_ = [
("unlocks", UnlockEntryStruct * 256),
("global_ranking_state", GlobalRankingStateEntryStruct * 8),
("_unused", c_uint8 * 4), # Unused?
("event_ver", c_uint8), # 0x624
("event_end", c_uint8), # 0x625
("event_sel_team", c_uint8), # 0x626
("event_rankings", c_uint8 * 3), # 0x627
("event_avg", c_uint16), # 0x62a
("event_score_green", c_uint32), # 0x62c
("event_score_red", c_uint32), # 0x62c
("event_score_yellow", c_uint32), # 0x62c
("event_border", c_uint16), # 0x638
]
class EventInfo:
@staticmethod
def create() -> EventInfoStruct:
p = EventInfoStruct()
p.event_ver = 5 # Event episode
p.event_end = 1 # Event has ended
p.event_sel_team = ZukinTeams.RED # Winning team
p.event_rankings[0] = ZukinTeams.RED
p.event_rankings[1] = ZukinTeams.GREEN
p.event_rankings[2] = ZukinTeams.YELLOW
p.event_avg = 123 # ?
p.event_score_green = 57312
p.event_score_red = 3516541
p.event_score_yellow = 5631
p.event_border = 123
for i in range(len(p.unlocks)):
p.unlocks[i].id = i & 0xFF
p.unlocks[i].enabled = 1
for i in range(0, len(p.global_ranking_state)):
p.global_ranking_state[i].id = i & 0xFF
p.global_ranking_state[i].state = 2
p.unlocks[0].enabled = 7 # Phase unlock max
return p
class DDRSuperNova2(

View File

@@ -1,3 +0,0 @@
from bemani.backend.ddr.ddrsn2.ddrsn2 import DDRSuperNova2
__all__ = ["DDRSuperNova2"]

View File

@@ -1,26 +0,0 @@
import logging
from ctypes import *
class BattleRecordStruct(Structure):
_pack_ = 1
_fields_ = [
("name", c_char * 14),
("wins", c_uint16),
("loses", c_uint16),
("draws", c_uint16),
]
class BattleRecord:
@staticmethod
def create(name: str, wins: int, losses: int, draws: int) -> BattleRecordStruct:
this_name = name
if len(this_name) > 8:
this_name = this_name[:8]
logging.warning("name {} too long, truncating to {}", name, this_name)
elif len(this_name) < 8:
logging.warning("name too short, padding with spaces")
this_name = this_name.ljust(8, " ")
return BattleRecordStruct(this_name.encode("ascii"), wins, losses, draws)

View File

@@ -1,52 +0,0 @@
from ctypes import *
from bemani.backend.ddr.ddrsn2.globalrankingstateentry import GlobalRankingStateEntryStruct
from bemani.backend.ddr.ddrsn2.unlockentry import UnlockEntryStruct
from bemani.backend.ddr.ddrsn2.zukinteams import ZukinTeams
class EventInfoStruct(Structure):
_pack_ = 1
_fields_ = [
("unlocks", UnlockEntryStruct * 256),
("global_ranking_state", GlobalRankingStateEntryStruct * 8),
("_unused", c_uint8 * 4), # Unused?
("event_ver", c_uint8), # 0x624
("event_end", c_uint8), # 0x625
("event_sel_team", c_uint8), # 0x626
("event_rankings", c_uint8 * 3), # 0x627
("event_avg", c_uint16), # 0x62a
("event_score_green", c_uint32), # 0x62c
("event_score_red", c_uint32), # 0x62c
("event_score_yellow", c_uint32), # 0x62c
("event_border", c_uint16), # 0x638
]
class EventInfo:
@staticmethod
def create() -> EventInfoStruct:
p = EventInfoStruct()
p.event_ver = 5 # Event episode
p.event_end = 1 # Event has ended
p.event_sel_team = ZukinTeams.RED # Winning team
p.event_rankings[0] = ZukinTeams.RED
p.event_rankings[1] = ZukinTeams.GREEN
p.event_rankings[2] = ZukinTeams.YELLOW
p.event_avg = 123 # ?
p.event_score_green = 57312
p.event_score_red = 3516541
p.event_score_yellow = 5631
p.event_border = 123
for i in range(len(p.unlocks)):
p.unlocks[i].id = i & 0xFF
p.unlocks[i].enabled = 1
for i in range(0, len(p.global_ranking_state)):
p.global_ranking_state[i].id = i & 0xFF
p.global_ranking_state[i].state = 2
p.unlocks[0].enabled = 7 # Phase unlock max
return p

View File

@@ -1,16 +0,0 @@
from ctypes import *
class GlobalRankingStateEntryStruct(Structure):
_pack_ = 1
_fields_ = [
("id", c_int8),
("state", c_int8), # 0 = disabled, 1 = English ("Global Ranking"), 2 = Japanese ("Gachinko Dance Matsuri")
("_unused", c_int8 * 2),
]
class GlobalRankingStateEntry:
@staticmethod
def create(id: int, state: int) -> GlobalRankingStateEntryStruct:
return GlobalRankingStateEntryStruct(id, state)

View File

@@ -1,91 +0,0 @@
from ctypes import *
from bemani.backend.ddr.ddrsn2.battlerecord import BattleRecordStruct
from bemani.common import Profile, PlayStatistics
class PlayerInfoStruct(Structure):
_pack_ = 1
_fields_ = [
("count", c_uint16), # 0x00
("area", c_uint16), # 0x02
("title", c_uint16), # 0x04
("flag", c_uint16), # 0x06
("id", c_uint32), # 0x08
("exp", c_uint32), # 0x0c
("weight", c_uint16), # 0x10
("last_cate", c_uint8), # 0x12
("last_mode", c_uint8), # 0x13
("last_type", c_uint8), # 0x14
("last_sort", c_uint8), # 0x15
("last_music", c_uint8), # 0x16
("_unused1", c_char * 6), # 0x17 - 0x1b
("team", c_uint8), # 0x1c
("_unused2", c_char * 8), # 0x1d - 0x24
("takeover", c_uint8), # 0x25
("count_b", c_uint8), # 0x26
("_unused3", c_char * 2), # 0x27 - 0x29
("groove_radar", c_uint16 * 5), # 0x2a
("options", c_uint8 * 32), # 0x34
("name", c_char * 14), # 0x54
("_unused4", c_char * 2), # 0x62 - 0x63
# Bit is set to 1 = already displayed, 0 = not yet displayed
("unlock_prompt_bits", c_uint8 * 12), # 0x64 - 0x70
("_unused5", c_char * 20), # 0x70 - 0x83
("course", c_uint32 * 3), # 0x84
("_unused6", c_char * (0x1344 - 0x90)),
("battle_records", BattleRecordStruct * 5), # 0x1344
]
class PlayerInfo:
@staticmethod
def create(play_stats: PlayStatistics, profile: Profile, machine_region: int) -> PlayerInfoStruct:
player = PlayerInfoStruct()
player.count = play_stats.get_int("single_plays")
player.area = profile.get_int("area", machine_region)
player.title = profile.get_int("title")
player.flag = profile.get_int("flag")
player.id = profile.extid
player.exp = play_stats.get_int("exp")
player.weight = profile.get_int("weight")
lastdict = profile.get_dict("last")
player.last_cate = lastdict.get_int("cate")
player.last_mode = lastdict.get_int("mode")
player.last_type = lastdict.get_int("type")
player.last_sort = lastdict.get_int("sort")
player.last_music = lastdict.get_int("music")
player.team = profile.get_int("team")
player.takeover = profile.get_int("takeover")
player.count_b = play_stats.get_int("battle_plays")
idx = 0
for entry in profile.get_int_array("gr_s", 5):
player.groove_radar[idx] = entry
idx += 1
idx = 0
if "opt" not in profile:
profile.replace_int_array("opt", 16, [2, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 2, 0, 0, 0, 0])
for entry in profile.get_int_array("opt", 16):
player.options[idx] = entry
idx += 1
player.name = (profile.get_str("name").ljust(8, " ")[:8]).encode("ascii")
# Default, unlock everything?
for i in range(len(player.unlock_prompt_bits)):
player.unlock_prompt_bits[i] = 0xFF
idx = 0
for entry in profile.get_int_array("course", 3):
player.course[idx] = entry
idx += 1
return player

View File

@@ -1,40 +0,0 @@
from ctypes import *
from bemani.backend.ddr.ddrsn2.scorerecord import ScoreRecordStruct, ScoreRecord
from bemani.data import Score
class ScoreInfoStruct(Structure):
_pack_ = 1
_fields_ = [
("records", ScoreRecordStruct * 200),
]
class ScoreInfo:
@staticmethod
def create(scores: list[Score], part: int) -> ScoreInfoStruct:
info = ScoreInfoStruct()
scores_by_song_id: dict[int, dict[int, Score]] = {}
for score in scores:
if score.id not in scores_by_song_id:
scores_by_song_id[score.id] = {}
scores_by_song_id[score.id][score.chart] = score
for i in range(len(info.records)):
if part == 2:
song_id = i + 200
else:
song_id = i
if song_id in scores_by_song_id:
score_record = ScoreRecord.create(scores_by_song_id[song_id])
else:
score_record = ScoreRecord.create(None)
info.records[i] = score_record
return info

View File

@@ -1,115 +0,0 @@
import math
from ctypes import *
from typing import Optional
from bemani.common import DBConstants
from bemani.data import Score
class ScoreRankRecordStruct(Structure):
_pack_ = 1
_fields_ = [
("lo_score", c_int8),
("score", c_int16),
("rank", c_int8, 5),
("unk1", c_int8, 1),
("yfc", c_int8, 1),
("ofc", c_int8, 1),
]
class ScoreRecordStruct(Structure):
_pack_ = 1
_fields_ = [
("beginner", ScoreRankRecordStruct),
("basic", ScoreRankRecordStruct),
("difficult", ScoreRankRecordStruct),
("expert", ScoreRankRecordStruct),
("challenge", ScoreRankRecordStruct),
("double_basic", ScoreRankRecordStruct),
("double_difficult", ScoreRankRecordStruct),
("double_expert", ScoreRankRecordStruct),
("double_challenge", ScoreRankRecordStruct),
]
GAME_RANK_AAA = 0
GAME_RANK_AA = 4
GAME_RANK_A = 8
GAME_RANK_B = 13
GAME_RANK_C = 16
GAME_RANK_D = 20
GAME_RANK_E = 24
GAME_RANK_NONE = 0xFF
class ScoreRecord:
@staticmethod
def blank() -> ScoreRecordStruct:
record = ScoreRecordStruct()
record.beginner.rank = GAME_RANK_NONE
record.basic.rank = GAME_RANK_NONE
record.difficult.rank = GAME_RANK_NONE
record.expert.rank = GAME_RANK_NONE
record.challenge.rank = GAME_RANK_NONE
record.double_basic.rank = GAME_RANK_NONE
record.double_difficult.rank = GAME_RANK_NONE
record.double_expert.rank = GAME_RANK_NONE
record.double_challenge.rank = GAME_RANK_NONE
return record
@staticmethod
def create(scores: Optional[dict[int, Score]]) -> ScoreRecordStruct:
record = ScoreRecord.blank()
if scores is not None:
for chart in scores:
if chart == 0:
chart_rec = record.beginner
elif chart == 1:
chart_rec = record.basic
elif chart == 2:
chart_rec = record.difficult
elif chart == 3:
chart_rec = record.expert
else:
chart_rec = record.challenge
score = scores[chart]
formatted_score = math.floor(score.points / 10)
hi_score = formatted_score >> 8
lo_score = formatted_score ^ (hi_score << 8)
chart_rec.lo_score = lo_score
chart_rec.score = hi_score
rank = score.data.get_int("rank")
if rank == DBConstants.DDR_RANK_AAA:
chart_rec.rank = GAME_RANK_AAA
elif rank == DBConstants.DDR_RANK_AA:
chart_rec.rank = GAME_RANK_AA
elif rank == DBConstants.DDR_RANK_A:
chart_rec.rank = GAME_RANK_A
elif rank == DBConstants.DDR_RANK_B:
chart_rec.rank = GAME_RANK_B
elif rank == DBConstants.DDR_RANK_C:
chart_rec.rank = GAME_RANK_C
elif rank == DBConstants.DDR_RANK_D:
chart_rec.rank = GAME_RANK_D
else:
chart_rec.rank = GAME_RANK_E
halo = score.data.get_int("halo")
if halo == DBConstants.DDR_HALO_PERFECT_FULL_COMBO:
chart_rec.yfc = 1
chart_rec.ofc = 1
elif halo == DBConstants.DDR_HALO_GREAT_FULL_COMBO:
chart_rec.yfc = 1
return record

View File

@@ -1,21 +0,0 @@
from ctypes import *
class UnlockEntryStruct(Structure):
_pack_ = 1
_fields_ = [
("id", c_uint8),
("enabled", c_uint8),
# Set to 1 to enable. This is only read if the other flags check out. Set all other flags to 0 to force unlock.
("flag1", c_uint8), # Must be less than some value passed to check function (always 0x3d6???)
("flag2", c_uint8), # Some kind of "unlock type" flag? If 0 then other flags aren't checked.
("flag3", c_uint8), # Unused??
("flag4", c_uint8),
# If 0, flag2 (if flag2 is non-0) must match the type of unlock looked for by the checker function. If non-0, flag4 * 10 must be <= param4 of checker function (when is this used?)
]
class UnlockEntry:
@staticmethod
def create(id: int, enabled: int, flag1: int, flag2: int, flag3: int, flag4: int) -> UnlockEntryStruct:
return UnlockEntryStruct(id, enabled, flag1, flag2, flag3, flag4)

View File

@@ -1,7 +0,0 @@
from enum import IntEnum
class ZukinTeams(IntEnum):
GREEN = 1
RED = 2
YELLOW = 3