From 1479281ae7adaecc8f66807f9f4463bf6ba13ad3 Mon Sep 17 00:00:00 2001 From: 544146 Date: Sun, 8 Oct 2023 18:36:35 +0100 Subject: [PATCH] Add feet saber wip changes --- README.md | 8 ++- drs2dd.py | 8 +-- fs2dd.py | 143 +++++++++++++++++++++++++++------------------ model/dancedash.py | 4 +- model/feetsaber.py | 92 +++++++++++++++-------------- 5 files changed, 145 insertions(+), 110 deletions(-) diff --git a/README.md b/README.md index 7a979640..a25a74e4 100644 --- a/README.md +++ b/README.md @@ -59,5 +59,11 @@ Generate full DD Beat Map from json files in repository (no --song-id does all) Generate full DD Beat Map from Feet Saber directory (WIP) ```bash -.venv/Scripts/python fs2dd.py ... # WIP when I have time. +# WIP when I have time. https://beatsaver.com/maps/229ed is what i've been using to test. +.venv/Scripts/python fs2dd.py --fs-map-dir "229ed (Yell! (DJ Shimamura Remix) [feat. Moimoi] [Feet saber] - KikaeAeon)" ``` + +### Feet Saber TODO: + +- Map 1-9 x position of notes (model/feetsaber.py:213) +- Map timings and logic for line notes (fs2dd.py:74) \ No newline at end of file diff --git a/drs2dd.py b/drs2dd.py index 14b01d0e..7e7b1081 100644 --- a/drs2dd.py +++ b/drs2dd.py @@ -22,8 +22,8 @@ from model.dancedash import DDLineNode from model.dancedash import DDRoadBlockNode from model.dancedash import DDSphereNode from model.dancedash import DRS2DD_MAP_PREFIX -from model.dancedash import DRS_TO_DDS_LINE_NOTE_TYPE -from model.dancedash import DRS_TO_DDS_NOTE_TYPE +from model.dancedash import DRS_TO_DD_LINE_NOTE_TYPE +from model.dancedash import DRS_TO_DD_NOTE_TYPE from model.dancedash import ORDER_COUNT_PER_BEAT from model.dancedash import X_Y from model.dancerush import ALBUM_NAME @@ -63,7 +63,7 @@ def map_sphere_nodes( noteOrder=round(bps * seconds * ORDER_COUNT_PER_BEAT), time=seconds / total_time_seconds, position=X_Y(x=track_step.position_info.to_dance_dash_x, y=0), - noteType=DRS_TO_DDS_NOTE_TYPE[track_step.kind], + noteType=DRS_TO_DD_NOTE_TYPE[track_step.kind], ), ) return spheres @@ -108,7 +108,7 @@ def map_line_nodes( noteOrder=round(bps * seconds * ORDER_COUNT_PER_BEAT), time=seconds / total_time_seconds, position=X_Y(x=drs_track_point.to_dance_dash_x, y=0), - noteType=DRS_TO_DDS_LINE_NOTE_TYPE[track_step.kind], + noteType=DRS_TO_DD_LINE_NOTE_TYPE[track_step.kind], ), ) diff --git a/fs2dd.py b/fs2dd.py index 490f1245..a2af73c0 100644 --- a/fs2dd.py +++ b/fs2dd.py @@ -5,15 +5,23 @@ import os import shutil from datetime import datetime -from model.dancedash import DD_LEFT +from model.dancedash import DD_LINE_LEFT +from model.dancedash import DD_LINE_RIGHT from model.dancedash import DDBeatMap from model.dancedash import DDBeatMapData from model.dancedash import DDBeatMapInfoFile +from model.dancedash import DDDownPos +from model.dancedash import DDDownPos2D +from model.dancedash import DDJumpPos +from model.dancedash import DDJumpPos2D from model.dancedash import DDLineNode from model.dancedash import DDRoadBlockNode from model.dancedash import DDSphereNode from model.dancedash import ORDER_COUNT_PER_BEAT from model.dancedash import X_Y +from model.feetsaber import FS_LEFT_NOTE +from model.feetsaber import FS_RIGHT_NOTE +from model.feetsaber import FS_TO_DD_NOTE_TYPE from model.feetsaber import FSBeatMapFile from model.feetsaber import FSInfoDat from util import convert_egg_to_ogg_and_get_length @@ -22,65 +30,83 @@ from util import random_9_digit_int from util import yyyymmdd_to_ticks -def map_sphere_nodes(fs_beat_map: FSBeatMapFile, total_time_seconds: float) -> list[DDSphereNode]: - spheres = [] - ... - return spheres +def map_sphere_nodes( + fs_beat_map: FSBeatMapFile, + fs_info_dat: FSInfoDat, + total_time_seconds: float, +) -> list[DDSphereNode]: + + def process_notes(note_type: FS_LEFT_NOTE | FS_RIGHT_NOTE, line_type: DD_LINE_LEFT | DD_LINE_RIGHT): + all_lines_of_type = [ + o for o in fs_beat_map.obstacles if o.customData.dd_note_type == line_type + ] + all_notes = [ + n for n in fs_beat_map.notes if n.type == note_type + ] + non_overlapping_notes = [ + note for note in all_notes + if not any(obstacle.time <= note.time < obstacle.end_time for obstacle in all_lines_of_type) + ] + return [ + DDSphereNode( + noteOrder=round(note.time * ORDER_COUNT_PER_BEAT), + time=(fs_info_dat.bps * note.time) / total_time_seconds, + position=X_Y(x=note.to_dd_x, y=0), + noteType=FS_TO_DD_NOTE_TYPE[note.type], + ) + for note in non_overlapping_notes + ] + + spheres = process_notes(FS_LEFT_NOTE, DD_LINE_LEFT) + \ + process_notes(FS_RIGHT_NOTE, DD_LINE_RIGHT) + return sorted(spheres, key=lambda s: s.noteOrder) -def map_line_nodes(fs_beat_map: FSBeatMapFile, total_time_seconds: float) -> list[DDLineNode]: +def map_line_nodes( + fs_beat_map: FSBeatMapFile, + fs_info_dat: FSInfoDat, + total_time_seconds: float, +) -> list[DDLineNode]: lines = [] - line_obstacles = [ - o for o in fs_beat_map.obstacles if o.customData.is_fs_slider - ] - left_obstacles = [ - o for o in line_obstacles if o.customData.dd_note_type == DD_LEFT - ] - right_obstacles = [ # noqa - o for o in line_obstacles if o.customData.dd_note_type != DD_LEFT - ] # noqa - line_group_id = 1 + # line_obstacles = [ + # o for o in fs_beat_map.obstacles if o.customData.is_fs_slider + # ] + # left_obstacles = [ + # o for o in line_obstacles if o.customData.dd_note_type == DD_LEFT + # ] + # right_obstacles = [ + # o for o in line_obstacles if o.customData.dd_note_type == DD_RIGHT + # ] - index_in_line = 0 - for idx, obstacle in enumerate(left_obstacles): - - for multiplier in (0, obstacle.duration): # line start and line end - index_in_line += 1 - lines.append( - DDLineNode( - lineGroupId=line_group_id, - indexInLine=index_in_line, - noteOrder=round( - ( - (obstacle.time + multiplier) * - ORDER_COUNT_PER_BEAT - ) - ORDER_COUNT_PER_BEAT, - ), - time=fs_beat_map.customData.time / total_time_seconds, - position=X_Y(x=obstacle.customData.dd_x, y=0), - noteType=obstacle.customData.dd_note_type, - ), - ) - - # todo(aggg figure this out i give up for today) - - is_last_obstacle = idx == len(left_obstacles) - 1 - next_obstacle = left_obstacles[ - idx + - 1 - ] if not is_last_obstacle else None - if next_obstacle and not next_obstacle.is_part_of_last_obstacle(obstacle): - line_group_id += 1 - index_in_line = 0 + # todo: implement lines return lines -def map_down_and_jump_notes(fs_beat_map: FSBeatMapFile, total_time_seconds: float) -> list[DDRoadBlockNode]: - spheres = [] - ... - return spheres +def map_down_and_jump_notes( + fs_beat_map: FSBeatMapFile, + fs_info_dat: FSInfoDat, + total_time_seconds: float, +) -> list[DDRoadBlockNode]: + def create_block(obstacle, position, position2D): + return DDRoadBlockNode( + noteOrder=round(obstacle.time * ORDER_COUNT_PER_BEAT) + + (ORDER_COUNT_PER_BEAT / 16), + time=(fs_info_dat.bps * obstacle.time) / total_time_seconds, + position=position, + position2D=position2D, + ) + + downs = [ + create_block(o, DDDownPos, DDDownPos2D) + for o in fs_beat_map.obstacles if o.is_down + ] + ups = [ + create_block(o, DDJumpPos, DDJumpPos2D) + for o in fs_beat_map.obstacles if o.is_up + ] + return sorted(downs + ups, key=lambda r: r.noteOrder) def create_dd_tracks_from_fs(fs_map_dir: str) -> DDBeatMapInfoFile: @@ -112,19 +138,19 @@ def create_dd_tracks_from_fs(fs_map_dir: str) -> DDBeatMapInfoFile: song_paths = [] for difficulty_set in fs_info.difficultyBeatmapSets: - for difficulty_set in difficulty_set.difficultyBeatmaps: - beat_map: FSBeatMapFile = difficulty_set.get_beatmap(fs_map_dir) + for difficulty in difficulty_set.difficultyBeatmaps: + beat_map: FSBeatMapFile = difficulty.get_beatmap(fs_map_dir) - sphere_notes = map_sphere_nodes(beat_map, song_length) - line_notes = map_line_nodes(beat_map, song_length) + sphere_notes = map_sphere_nodes(beat_map, fs_info, song_length) + line_notes = map_line_nodes(beat_map, fs_info, song_length) road_block_notes = map_down_and_jump_notes( - beat_map, song_length, + beat_map, fs_info, song_length, ) total_note_count = len(sphere_notes + line_notes + road_block_notes) # noqa dd_beat_map = DDBeatMap( data=DDBeatMapData( - name=f'{fs_info.songName}', + name=fs_info.songName, sphereNodes=sphere_notes, lineNodes=line_notes, roadBlockNodes=road_block_notes, @@ -133,9 +159,10 @@ def create_dd_tracks_from_fs(fs_map_dir: str) -> DDBeatMapInfoFile: NPS=str(round(total_note_count / song_length, 2)), ) + difficulty_name = f'{difficulty_set.beatmapCharacteristicName}_{difficulty.difficulty}' song_paths.append( dd_beat_map.save_to_file( - target_dir, 'a.json', + target_dir, f'{difficulty_name}.json', ), ) diff --git a/model/dancedash.py b/model/dancedash.py index 2d0f8691..e849334c 100644 --- a/model/dancedash.py +++ b/model/dancedash.py @@ -23,12 +23,12 @@ ORDER_COUNT_PER_BEAT = 24 DRS2DD_MAP_PREFIX = 44_52_53_000 # 44 = D, 52 = R, 53 = S -DRS_TO_DDS_NOTE_TYPE = { +DRS_TO_DD_NOTE_TYPE = { DRS_LEFT: DD_LEFT, DRS_RIGHT: DD_RIGHT, } -DRS_TO_DDS_LINE_NOTE_TYPE = { +DRS_TO_DD_LINE_NOTE_TYPE = { DRS_LEFT: DD_LINE_LEFT, DRS_RIGHT: DD_LINE_RIGHT, } diff --git a/model/feetsaber.py b/model/feetsaber.py index 03ac69c5..bca794fc 100644 --- a/model/feetsaber.py +++ b/model/feetsaber.py @@ -5,8 +5,10 @@ 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 @@ -193,6 +195,10 @@ class FSBeatMapFileNoteCustomData: position: tuple[float, float] +FS_LEFT_NOTE = 0 +FS_RIGHT_NOTE = 1 + + @dataclass class FSBeatMapFileNote: time: float @@ -202,6 +208,10 @@ class FSBeatMapFileNote: cutDirection: int customData: FSBeatMapFileNoteCustomData + @property + def to_dd_x(self): + return 0 # TODO + FS_RIGHT_COLOUR = (0.0, 1.0, 3.0, 1.0) FS_LEFT_COLOUR = (2.0, 1.5, 0.0, 1.0) @@ -211,9 +221,15 @@ FS_LONG_NOTE_TO_DD_NOTE_TYPE = { 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 position: tuple[float, float] @@ -222,52 +238,21 @@ class FSBeatMapFileObstacleCustomData: localRotation: tuple[float, float, float] | None = None @property - def height(self): + def height(self) -> float: return self.scale[1] @property - def y(self): + def is_fs(self) -> bool: _, y = self.position - return y - - @property - def x(self): - x, _ = self.position - return x - - @property - def is_fs(self): - return all( - [ - self.height == 0.1, - self.y == -0.25, - self.fake, - ], - ) + return self.height == 0.1 and y == -0.25 and self.fake @property def is_fs_slider(self) -> bool: return self.scale[0] == 1.0 and self.is_fs @property - def is_tail(self) -> bool: - return self.scale[0] == 1.5 and self.is_fs - - @property - def dd_x(self): - if not self.is_fs: - raise ValueError('This is not a FS note') - - if self.x < -2.5 or self.x > 1.5: - raise ValueError('Input should be between -1.5 and 1.5') - - normalized = (self.x + 2.5) / 4.0 - mapped_value = round(normalized * 8 + 1) - return int(mapped_value) - - @property - def dd_note_type(self): - return FS_LONG_NOTE_TO_DD_NOTE_TYPE[self.color] + def dd_note_type(self) -> DD_LINE_RIGHT | DD_LINE_LEFT | None: + return FS_LONG_NOTE_TO_DD_NOTE_TYPE.get(self.color) @dataclass @@ -279,14 +264,24 @@ class FSBeatMapFileObstacle: width: int customData: FSBeatMapFileObstacleCustomData + @property + def end_time(self): + return self.time + self.duration + + @property + def is_down(self) -> bool: + if not self.customData.track: + return False + return self.customData.track.casefold() == 'DownArch'.casefold() + + @property + def is_up(self) -> bool: + if not self.customData.track: + return False + return self.customData.track.casefold() == 'JumpBar'.casefold() + def is_part_of_last_obstacle(self, last_obstacle: FSBeatMapFileObstacle) -> bool: - if self.time < last_obstacle.time: - return False - - if self.time > last_obstacle.time + last_obstacle.duration: - return False - - return True + return last_obstacle.time <= self.time <= last_obstacle.end_time @dataclass @@ -322,8 +317,14 @@ class FSBeatMapFile: notes = [ FSBeatMapFileNote( time=note['_time'], - lineIndex=note['_lineIndex'], - lineLayer=note['_lineLayer'], + lineIndex=note['_lineIndex'] * + 1000 if len( + str(note['_lineIndex']), + ) == 1 else note['_lineIndex'], + lineLayer=note['_lineLayer'] * + 1000 if len( + str(note['_lineLayer']), + ) == 1 else note['_lineLayer'], type=note['_type'], cutDirection=note['_cutDirection'], customData=FSBeatMapFileNoteCustomData( @@ -340,6 +341,7 @@ class FSBeatMapFile: duration=obstacle['_duration'], width=obstacle['_width'], customData=FSBeatMapFileObstacleCustomData( + track=obstacle['_customData'].get('_track'), interactable=obstacle['_customData']['_interactable'], fake=obstacle['_customData']['_fake'], position=tuple(obstacle['_customData']['_position']),