from __future__ import annotations import argparse import json import os import shutil from dataclasses import asdict from drsxml2json import get_songdata_from_track_id from drsxml2json import TRACK_ID_TO_PATH from model.dancedash import DDBeatMap from model.dancedash import DDBeatMapData from model.dancedash import DDBeatMapInfoFile from model.dancedash import DDLineNode from model.dancedash import DDSphereNode from model.dancedash import DRS_TO_DDS_LINE_NOTE_TYPE from model.dancedash import DRS_TO_DDS_NOTE_TYPE from model.dancedash import X_Y from model.dancedash import X_Y_Z from model.dancerush import DRS_DOWN from model.dancerush import DRS_JUMP from model.dancerush import DRSSongData from model.dancerush import DRSSongDifficulty from model.dancerush import DRSTrackPoint from model.dancerush import DRSTrackStep from util import get_m4a_and_duration from util import get_song_cover_path from util import ORDER_COUNT_PER_BEAT def map_sphere_nodes( difficulty: DRSSongDifficulty, total_time_seconds: float, bpm: int, ) -> list[DDSphereNode]: spheres = [] for track_step in difficulty.track.sequence_data: track_step: DRSTrackStep = track_step # Long note get mapped elsewhere if track_step.long_point or track_step.kind in (DRS_DOWN, DRS_JUMP): continue bps = bpm / 60 ticks_per_second = difficulty.track.info.end_tick / total_time_seconds time = track_step.tick_info.end_tick / ticks_per_second sphere = DDSphereNode( noteOrder=round(bps * time * ORDER_COUNT_PER_BEAT), time=time / total_time_seconds, position=X_Y(x=track_step.position_info.to_dance_dash_x, y=0), position2D=X_Y(x=0, y=0), size=X_Y_Z(x=1, y=1, z=1), noteType=DRS_TO_DDS_NOTE_TYPE[track_step.kind], postionOffset=None, isPlayAudio=False, ) spheres.append(sphere) return spheres def map_line_nodes( difficulty: DRSSongDifficulty, total_time_seconds: float, bpm: int, ) -> list[DDLineNode]: lines = [] for line_group_id, track_step in enumerate(difficulty.track.sequence_data, start=1): track_step: DRSTrackStep = track_step if not track_step.long_point or track_step.kind in (DRS_DOWN, DRS_JUMP): continue bps = bpm / 60 ticks_per_second = difficulty.track.info.end_tick / total_time_seconds # DANCERUSH defines the first line step as a point, with the first point being the start of the line. # So we need to add a point at the start of the line for Dance Dash. initial_track_point = DRSTrackPoint( tick=track_step.tick_info.start_tick, left_pos=track_step.position_info.left_pos, right_pos=track_step.position_info.right_pos, ) track_step.long_point.insert(0, initial_track_point) for index_in_line, drs_track_point in enumerate(track_step.long_point): drs_track_point: DRSTrackPoint = drs_track_point if drs_track_point.left_end_pos or drs_track_point.right_end_pos: continue # handle shuffles later time = drs_track_point.tick / ticks_per_second line = DDLineNode( lineGroupId=line_group_id, indexInLine=index_in_line, isSliding=False, noteOrder=round(bps * time * ORDER_COUNT_PER_BEAT), time=time / total_time_seconds, position=X_Y(x=drs_track_point.to_dance_dash_x, y=0), position2D=X_Y(x=0, y=0), size=X_Y_Z(x=1, y=1, z=1), noteType=DRS_TO_DDS_LINE_NOTE_TYPE[track_step.kind], postionOffset=None, isPlayAudio=False, ) lines.append(line) return lines def create_dd_tracks_from_DRSSongData( drs_song_data: DRSSongData, target_dir: str, ) -> bool: if not os.path.exists(target_dir): os.makedirs(target_dir) print(f'Created directory: {target_dir}') dd_bmp = int(drs_song_data.info.bpm_max / 100) folder_path = TRACK_ID_TO_PATH.get(drs_song_data.song_id) song_path, song_length = get_m4a_and_duration(folder_path) if song_path and song_length: shutil.copy(song_path, target_dir) if song_cover_path := get_song_cover_path(folder_path): shutil.copy(song_cover_path, target_dir) if not song_path or not song_length: print( f'Could not find song path or song length for {drs_song_data.info.title_name}', ) return False song_paths = [] for attr, difficulty in drs_song_data.difficulties.with_attrs_as_str.items(): target_difficulty_path = os.path.join( target_dir, f'{drs_song_data.song_id}_{attr}.json', ) song_paths.append(target_difficulty_path) drs_beat_map = DDBeatMap( data=DDBeatMapData( name=f'{drs_song_data.info.title_name} {attr}', intervalPerSecond=0.0, gridSize=X_Y(x=0, y=0), planeSize=X_Y(x=0, y=0), orderCountPerBeat=ORDER_COUNT_PER_BEAT, sphereNodes=map_sphere_nodes( difficulty, # 10 seconds because the songs are 10 seconds longer... ? float(song_length) - 10, dd_bmp, ), lineNodes=map_line_nodes( difficulty, # 10 seconds because the songs are 10 seconds longer... ? float(song_length) - 10, dd_bmp, ), effectNodes=[], roadBlockNodes=[], trapNodes=[], ), beatSubs=1, BPM=dd_bmp, songStartOffset=0.0, NPS='0.0', developerMode=False, noteSpeed=1.0, noteJumpOffset=0.0, interval=1.0, info='', ) with open(target_difficulty_path, 'w') as f: json.dump(asdict(drs_beat_map), f, indent=4) print(f'Created file: {target_difficulty_path}') drs_song_info_json = DDBeatMapInfoFile( EditorVersion='1.3.2', BeatMapId=drs_song_data.song_id, OstId=0, CreateTicks=0, CreateTime='', SongName=drs_song_data.info.title_name, SongLength=song_length or '-1', SongAuthorName=drs_song_data.info.artist_name, LevelAuthorName='https://github.com/thomasasfk/drs2dd', SongPreviewSection=0, Bpm=str(dd_bmp), SongPath=os.path.basename(song_path) if song_path else None, OstName=None, CoverPath=os.path.basename( song_cover_path, ) if song_cover_path else None, # TODO PROPERLY MAP DIFFICULTIES BASED ON DIF NUMBERS # ALSO REMOVE 2 PLAYER DIFFICULTIES DRS_Easy=os.path.basename(song_paths[0]) if len( song_paths, ) > 0 else None, DRS_Normal=os.path.basename(song_paths[1]) if len( song_paths, ) > 1 else None, DRS_Hard=os.path.basename(song_paths[2]) if len( song_paths, ) > 2 else None, DRS_Expert=os.path.basename(song_paths[3]) if len( song_paths, ) > 3 else None, ) info_file_path = os.path.join(target_dir, 'info.json') with open(info_file_path, 'w') as f: json.dump(asdict(drs_song_info_json), f, indent=4) print(f'Created file: {info_file_path}') return True if __name__ == '__main__': parser = argparse.ArgumentParser( description='Create DD tracks from DRS Song Data', ) parser.add_argument( '--song-id', type=int, help='ID of the song to process (number of folder)', required=True, ) args = parser.parse_args() song_data = get_songdata_from_track_id(args.song_id) if not song_data: print(f'No song data found for song id {args.song_id}') raise SystemExit(1) created = create_dd_tracks_from_DRSSongData( song_data, song_data.info.title_name, ) print(f'Song {args.song_id} created?: {created}')