436 lines
13 KiB
Python
436 lines
13 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import math
|
|
import os
|
|
from dataclasses import dataclass
|
|
from dataclasses import field
|
|
|
|
from model.dancedash import DD_LEFT
|
|
from model.dancedash import DD_LINE_LEFT
|
|
from model.dancedash import DD_LINE_RIGHT
|
|
from model.dancedash import DD_RIGHT
|
|
|
|
|
|
@dataclass
|
|
class FSEditor:
|
|
version: str
|
|
|
|
|
|
@dataclass
|
|
class FSEditorMetadata:
|
|
lastEditedBy: str
|
|
editors: dict[str, FSEditor]
|
|
|
|
|
|
@dataclass
|
|
class FSContributor:
|
|
role: str
|
|
name: str
|
|
iconPath: str
|
|
|
|
|
|
@dataclass
|
|
class FSDatCustomData:
|
|
contributors: list[FSContributor]
|
|
editors: FSEditorMetadata
|
|
|
|
|
|
@dataclass
|
|
class FSBeatMapCustomData:
|
|
difficultyLabel: str
|
|
editorOffset: int
|
|
editorOldOffset: int
|
|
suggestions: list[str]
|
|
requirements: list[str]
|
|
|
|
|
|
@dataclass
|
|
class FSBeatmap:
|
|
difficulty: str
|
|
difficultyRank: int
|
|
beatmapFilename: str
|
|
noteJumpMovementSpeed: int
|
|
noteJumpStartBeatOffset: float
|
|
customData: FSBeatMapCustomData
|
|
|
|
def get_beatmap(self, map_dir: str) -> FSBeatMapFile:
|
|
file_name = os.path.join(map_dir, self.beatmapFilename)
|
|
beat_map_dict = json.load(open(file_name, encoding='utf-8'))
|
|
return FSBeatMapFile.from_json_dict(beat_map_dict)
|
|
|
|
|
|
@dataclass
|
|
class FSDifficultyBeatMapSets:
|
|
beatmapCharacteristicName: str
|
|
difficultyBeatmaps: list[FSBeatmap]
|
|
|
|
|
|
@dataclass
|
|
class FSInfoDat:
|
|
version: str
|
|
songName: str
|
|
songSubName: str
|
|
songAuthorName: str
|
|
levelAuthorName: str
|
|
beatsPerMinute: int
|
|
shuffle: int
|
|
shufflePeriod: float
|
|
previewStartTime: int
|
|
previewDuration: int
|
|
songFilename: str
|
|
coverImageFilename: str
|
|
environmentName: str
|
|
allDirectionsEnvironmentName: str
|
|
songTimeOffset: int
|
|
customData: FSDatCustomData
|
|
difficultyBeatmapSets: list[FSDifficultyBeatMapSets]
|
|
|
|
@property
|
|
def bps(self):
|
|
return self.beatsPerMinute / 60
|
|
|
|
@classmethod
|
|
def from_json_dict(cls, json_dict: dict) -> FSInfoDat:
|
|
custom_data_raw = json_dict['_customData']
|
|
|
|
def remove_underscore_from_keys(data):
|
|
return {key.lstrip('_'): value for key, value in data.items()}
|
|
|
|
contributors = [
|
|
FSContributor(**remove_underscore_from_keys(contributor)) for contributor in
|
|
custom_data_raw['_contributors']
|
|
] if '_contributors' in custom_data_raw else []
|
|
|
|
editors = {
|
|
editor: FSEditor(version=data['version']) for editor, data in custom_data_raw['_editors'].items() if
|
|
editor != '_lastEditedBy'
|
|
}
|
|
|
|
custom_data = FSDatCustomData(
|
|
contributors=contributors,
|
|
editors=FSEditorMetadata(
|
|
lastEditedBy=custom_data_raw['_editors']['_lastEditedBy'],
|
|
editors=editors,
|
|
),
|
|
)
|
|
|
|
difficulty_beatmap_sets_raw = json_dict['_difficultyBeatmapSets']
|
|
|
|
difficulty_beatmaps_list = []
|
|
for dbset in difficulty_beatmap_sets_raw:
|
|
beatmaps = [
|
|
FSBeatmap(
|
|
difficulty=beatmap['_difficulty'],
|
|
difficultyRank=beatmap['_difficultyRank'],
|
|
beatmapFilename=beatmap['_beatmapFilename'],
|
|
noteJumpMovementSpeed=beatmap['_noteJumpMovementSpeed'],
|
|
noteJumpStartBeatOffset=beatmap['_noteJumpStartBeatOffset'],
|
|
customData=FSBeatMapCustomData(
|
|
difficultyLabel=beatmap['_customData'].get(
|
|
'_difficultyLabel',
|
|
),
|
|
editorOffset=beatmap['_customData'].get(
|
|
'_editorOffset',
|
|
),
|
|
editorOldOffset=beatmap['_customData'].get(
|
|
'_editorOldOffset',
|
|
),
|
|
suggestions=beatmap['_customData'].get('_suggestions'),
|
|
requirements=beatmap['_customData'].get(
|
|
'_requirements',
|
|
),
|
|
),
|
|
) for beatmap in dbset['_difficultyBeatmaps']
|
|
]
|
|
|
|
difficulty_beatmaps_list.append(
|
|
FSDifficultyBeatMapSets(
|
|
beatmapCharacteristicName=dbset['_beatmapCharacteristicName'],
|
|
difficultyBeatmaps=beatmaps,
|
|
),
|
|
)
|
|
|
|
return cls(
|
|
version=json_dict['_version'],
|
|
songName=json_dict['_songName'],
|
|
songSubName=json_dict['_songSubName'],
|
|
songAuthorName=json_dict['_songAuthorName'],
|
|
levelAuthorName=json_dict['_levelAuthorName'],
|
|
beatsPerMinute=json_dict['_beatsPerMinute'],
|
|
shuffle=json_dict['_shuffle'],
|
|
shufflePeriod=json_dict['_shufflePeriod'],
|
|
previewStartTime=json_dict['_previewStartTime'],
|
|
previewDuration=json_dict['_previewDuration'],
|
|
songFilename=json_dict['_songFilename'],
|
|
coverImageFilename=json_dict['_coverImageFilename'],
|
|
environmentName=json_dict['_environmentName'],
|
|
allDirectionsEnvironmentName=json_dict.get(
|
|
'_allDirectionsEnvironmentName',
|
|
),
|
|
songTimeOffset=json_dict['_songTimeOffset'],
|
|
customData=custom_data,
|
|
difficultyBeatmapSets=difficulty_beatmaps_list,
|
|
)
|
|
|
|
@classmethod
|
|
def from_json_file(cls, json_file: str) -> FSInfoDat:
|
|
import json
|
|
with open(json_file) as f:
|
|
json_dict = json.load(f)
|
|
return cls.from_json_dict(json_dict)
|
|
|
|
|
|
@dataclass
|
|
class FSBeatMapFileBookmark:
|
|
time: int
|
|
name: str
|
|
color: list[float]
|
|
|
|
|
|
@dataclass
|
|
class FSBeatMapFileCustomData:
|
|
time: float
|
|
bookmarks: list[FSBeatMapFileBookmark]
|
|
|
|
|
|
@dataclass
|
|
class FSBeatMapFileEvent:
|
|
time: int
|
|
type: int
|
|
value: int
|
|
|
|
|
|
@dataclass
|
|
class FSBeatMapFileNoteCustomData:
|
|
position: tuple[float, float]
|
|
|
|
|
|
FS_LEFT_NOTE = 0
|
|
FS_RIGHT_NOTE = 1
|
|
|
|
|
|
@dataclass
|
|
class FSBeatMapFileNote:
|
|
time: float
|
|
lineIndex: int
|
|
lineLayer: int
|
|
type: int
|
|
cutDirection: int
|
|
customData: FSBeatMapFileNoteCustomData
|
|
|
|
@property
|
|
def to_dd_x(self) -> 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9:
|
|
if not self.customData:
|
|
if self.lineIndex == 0:
|
|
return 2
|
|
elif self.lineIndex == 1:
|
|
return 4
|
|
elif self.lineIndex == 2:
|
|
return 6
|
|
elif self.lineIndex == 3:
|
|
return 8
|
|
x = self.customData.position[0]
|
|
x_min, x_max = -2, 1
|
|
y_min, y_max = FS_LANE_LEFT, FS_LANE_RIGHT
|
|
y = (x - x_min) * (y_max - y_min) / (x_max - x_min) + y_min
|
|
return round(y)
|
|
|
|
|
|
FS_RIGHT_COLOUR = (0.0, 1.0, 3.0, 1.0)
|
|
FS_LEFT_COLOUR = (2.0, 1.5, 0.0, 1.0)
|
|
|
|
FS_LONG_NOTE_TO_DD_NOTE_TYPE = {
|
|
FS_RIGHT_COLOUR: DD_LINE_RIGHT,
|
|
FS_LEFT_COLOUR: DD_LINE_LEFT,
|
|
}
|
|
|
|
FS_TO_DD_NOTE_TYPE = {
|
|
FS_LEFT_NOTE: DD_LEFT,
|
|
FS_RIGHT_NOTE: DD_RIGHT,
|
|
}
|
|
|
|
|
|
@dataclass
|
|
class FSBeatMapFileObstacleCustomData:
|
|
track: str
|
|
interactable: bool
|
|
fake: bool
|
|
scale: tuple
|
|
position: tuple[float, float] | None
|
|
color: tuple[float, float, float, float] | None
|
|
localRotation: tuple[float, float, float] | None = None
|
|
|
|
@property
|
|
def height(self) -> float:
|
|
return self.scale[1]
|
|
|
|
@property
|
|
def is_fs(self) -> bool:
|
|
if not self.position:
|
|
return False
|
|
return self.height == 0.1 and self.position[1] == -0.25 and self.fake
|
|
|
|
@property
|
|
def is_fs_slider(self) -> bool:
|
|
return self.scale[0] == 1.0 and self.is_fs
|
|
|
|
@property
|
|
def dd_note_type(self) -> DD_LINE_RIGHT | DD_LINE_LEFT | None:
|
|
return FS_LONG_NOTE_TO_DD_NOTE_TYPE.get(self.color)
|
|
|
|
|
|
# dance dash notes can go from 1 to 9, with 1 being the leftmost and 9 being the rightmost
|
|
# these values determine the furthest left and right notes can go when converted to dd
|
|
# reason being that feet saber entire stage is smaller, so we need to scale the notes
|
|
FS_LANE_LEFT = 2
|
|
FS_LANE_RIGHT = 8
|
|
|
|
|
|
@dataclass
|
|
class FSBeatMapFileObstacle:
|
|
time: float
|
|
lineIndex: int
|
|
type: int
|
|
duration: int
|
|
width: int
|
|
customData: FSBeatMapFileObstacleCustomData
|
|
|
|
def to_dd_x(self, x: float = None) -> 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9:
|
|
x_min, x_max = -2, 1
|
|
y_min, y_max = FS_LANE_LEFT, FS_LANE_RIGHT
|
|
if not x:
|
|
x = self.customData.position[0]
|
|
if x < x_min:
|
|
x = -2
|
|
elif x > x_max:
|
|
x = 1
|
|
y = (x - x_min) * (y_max - y_min) / (x_max - x_min) + y_min
|
|
return round(y)
|
|
|
|
@property
|
|
def end_to_dd_x(self) -> 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9:
|
|
opposite_angle = self.customData.localRotation[1]
|
|
if opposite_angle == 0.0:
|
|
return self.to_dd_x()
|
|
# this angle calculation came out of nowhere, fix this with proper math or something idk
|
|
opposite_length = self.duration * \
|
|
math.sin(math.radians(opposite_angle))
|
|
return self.to_dd_x(x=self.customData.position[0] + (opposite_length * 6.4))
|
|
|
|
@property
|
|
def has_rotation(self) -> bool:
|
|
return self.customData.localRotation[1] != 0.0
|
|
|
|
@property
|
|
def end_time(self):
|
|
return self.time + self.duration
|
|
|
|
@property
|
|
def is_down(self) -> bool:
|
|
if not self.customData:
|
|
return False
|
|
if not self.customData.track:
|
|
return False
|
|
return self.customData.track.casefold() == 'DownArch'.casefold()
|
|
|
|
@property
|
|
def is_up(self) -> bool:
|
|
if not self.customData:
|
|
return False
|
|
if not self.customData.track:
|
|
return False
|
|
return self.customData.track.casefold() == 'JumpBar'.casefold()
|
|
|
|
def is_part_of_last_obstacle(self, last_obstacle: FSBeatMapFileObstacle | None) -> bool:
|
|
if not last_obstacle:
|
|
return False
|
|
return last_obstacle.time <= self.time <= last_obstacle.end_time
|
|
|
|
|
|
@dataclass
|
|
class FSBeatMapFile:
|
|
version: str
|
|
customData: FSBeatMapFileCustomData
|
|
notes: list[FSBeatMapFileNote]
|
|
obstacles: list[FSBeatMapFileObstacle]
|
|
events: list[FSBeatMapFileEvent] = field(default_factory=list)
|
|
waypoints: list = field(default_factory=list)
|
|
|
|
@classmethod
|
|
def from_json_dict(cls, json_dict: dict) -> FSBeatMapFile:
|
|
custom_data = FSBeatMapFileCustomData(
|
|
time=json_dict['_customData'].get('_time'),
|
|
bookmarks=[
|
|
FSBeatMapFileBookmark(
|
|
time=bm['_time'],
|
|
name=bm['_name'],
|
|
color=bm.get('_color'),
|
|
) for bm in json_dict['_customData'].get('_bookmarks') or []
|
|
],
|
|
)
|
|
|
|
events = [
|
|
FSBeatMapFileEvent(
|
|
time=event['_time'],
|
|
type=event['_type'],
|
|
value=event['_value'],
|
|
) for event in json_dict['_events']
|
|
]
|
|
|
|
notes = [
|
|
FSBeatMapFileNote(
|
|
time=note['_time'],
|
|
lineIndex=note['_lineIndex'],
|
|
lineLayer=note['_lineLayer'],
|
|
type=note['_type'],
|
|
cutDirection=note['_cutDirection'],
|
|
customData=FSBeatMapFileNoteCustomData(
|
|
position=tuple(note['_customData']['_position']),
|
|
) if '_customData' in note and '_position' in note['_customData'] else None,
|
|
) for note in json_dict['_notes']
|
|
]
|
|
|
|
obstacles = [
|
|
FSBeatMapFileObstacle(
|
|
time=obstacle['_time'],
|
|
lineIndex=obstacle['_lineIndex'],
|
|
type=obstacle['_type'],
|
|
duration=obstacle['_duration'],
|
|
width=obstacle['_width'],
|
|
customData=FSBeatMapFileObstacleCustomData(
|
|
track=obstacle['_customData'].get('_track'),
|
|
interactable=obstacle['_customData'].get('_interactable'),
|
|
fake=obstacle['_customData'].get('_fake'),
|
|
position=tuple(
|
|
obstacle['_customData']['_position'],
|
|
) if '_position' in obstacle['_customData'] else None,
|
|
localRotation=tuple(obstacle['_customData']['_localRotation']) if '_localRotation' in obstacle[
|
|
'_customData'
|
|
] else None,
|
|
scale=tuple(obstacle['_customData']['_scale']),
|
|
# type: ignore
|
|
color=tuple(
|
|
float(c)
|
|
for c in obstacle['_customData']['_color']
|
|
) if '_color' in obstacle['_customData'] else None,
|
|
) if '_customData' in obstacle else None,
|
|
) for obstacle in json_dict['_obstacles']
|
|
]
|
|
|
|
return cls(
|
|
version=json_dict['_version'],
|
|
customData=custom_data,
|
|
events=events,
|
|
notes=notes,
|
|
obstacles=obstacles,
|
|
waypoints=json_dict.get('_waypoints'),
|
|
)
|
|
|
|
@classmethod
|
|
def from_json_file(cls, json_file: str) -> FSBeatMapFile:
|
|
import json
|
|
with open(json_file, encoding='utf-8') as f:
|
|
json_dict = json.load(f)
|
|
return cls.from_json_dict(json_dict)
|