Feet Saber is real! #5
@@ -59,5 +59,7 @@ 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.
|
||||
.venv/Scripts/python fs2dd.py --fs-map-dir "path/to/map/folder"
|
||||
.venv/Scripts/python fs2dd.py --fs-map-id 229ed
|
||||
.venv/Scripts/python fs2dd.py --fs-playlist-id 3474
|
||||
```
|
||||
|
||||
@@ -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],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,89 +1,197 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import concurrent
|
||||
import os
|
||||
import shutil
|
||||
import zipfile
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
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 DDAlbumInfo
|
||||
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
|
||||
from util import download_and_extract_zip
|
||||
from util import download_image_from_url
|
||||
from util import get_feet_saber_map_from_id
|
||||
from util import get_feet_saber_maps_from_playlist
|
||||
from util import get_first_image_file_in_folder
|
||||
from util import random_9_digit_int
|
||||
from util import yyyymmdd_to_ticks
|
||||
from util import zipdir
|
||||
|
||||
|
||||
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 and 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
|
||||
o for o in fs_beat_map.obstacles if o.customData and o.customData.is_fs
|
||||
]
|
||||
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
|
||||
|
||||
obstacles_by_type = {
|
||||
DD_LINE_LEFT: [
|
||||
o for o in line_obstacles if o.customData.dd_note_type == DD_LINE_LEFT
|
||||
],
|
||||
DD_LINE_RIGHT: [
|
||||
o for o in line_obstacles if o.customData.dd_note_type == DD_LINE_RIGHT
|
||||
],
|
||||
}
|
||||
|
||||
index_in_line = 0
|
||||
for idx, obstacle in enumerate(left_obstacles):
|
||||
line_group_id = 1
|
||||
last_obstacle = None
|
||||
for note_type, obstacles in obstacles_by_type.items():
|
||||
for obstacle in obstacles:
|
||||
if obstacle.is_part_of_last_obstacle(last_obstacle) and not obstacle.customData.is_fs_slider:
|
||||
index_in_line += 1
|
||||
is_left = obstacle.customData.position[0] < last_obstacle.customData.position[0]
|
||||
last_obstacle_x = lines[-1].position.x
|
||||
lines.append(
|
||||
DDLineNode(
|
||||
lineGroupId=line_group_id,
|
||||
indexInLine=index_in_line,
|
||||
noteOrder=round(obstacle.time * ORDER_COUNT_PER_BEAT),
|
||||
time=(fs_info_dat.bps * obstacle.time) /
|
||||
total_time_seconds,
|
||||
position=X_Y(
|
||||
last_obstacle_x +
|
||||
(-1 if is_left else 1), y=0,
|
||||
),
|
||||
noteType=note_type,
|
||||
),
|
||||
)
|
||||
index_in_line += 1
|
||||
lines.append(
|
||||
DDLineNode(
|
||||
lineGroupId=line_group_id,
|
||||
indexInLine=index_in_line,
|
||||
noteOrder=round(
|
||||
obstacle.time * ORDER_COUNT_PER_BEAT,
|
||||
) + ORDER_COUNT_PER_BEAT / 4,
|
||||
time=(fs_info_dat.bps * obstacle.time) /
|
||||
total_time_seconds,
|
||||
position=X_Y(
|
||||
last_obstacle_x +
|
||||
(-1 if is_left else 1), y=0,
|
||||
),
|
||||
noteType=note_type,
|
||||
),
|
||||
)
|
||||
last_obstacle = obstacle
|
||||
continue
|
||||
|
||||
for multiplier in (0, obstacle.duration): # line start and line end
|
||||
if not obstacle.is_part_of_last_obstacle(last_obstacle):
|
||||
index_in_line = 0
|
||||
line_group_id += 1
|
||||
|
||||
lines.append(
|
||||
DDLineNode(
|
||||
lineGroupId=line_group_id,
|
||||
indexInLine=index_in_line,
|
||||
noteOrder=round(obstacle.time * ORDER_COUNT_PER_BEAT),
|
||||
time=(fs_info_dat.bps * obstacle.time) /
|
||||
total_time_seconds,
|
||||
position=X_Y(x=obstacle.to_dd_x(), y=0),
|
||||
noteType=note_type,
|
||||
),
|
||||
)
|
||||
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,
|
||||
noteOrder=round(obstacle.end_time * ORDER_COUNT_PER_BEAT),
|
||||
time=(fs_info_dat.bps * obstacle.time) /
|
||||
total_time_seconds,
|
||||
position=X_Y(x=obstacle.end_to_dd_x, y=0),
|
||||
noteType=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
|
||||
last_obstacle = obstacle
|
||||
|
||||
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:
|
||||
def create_dd_tracks_from_fs(fs_map_dir: str, prefix_dir: str = '', ost_id: int | None = None) -> DDBeatMapInfoFile:
|
||||
info_file_path = os.path.join(fs_map_dir, 'Info.dat')
|
||||
fs_info = FSInfoDat.from_json_file(info_file_path)
|
||||
|
||||
@@ -92,7 +200,7 @@ def create_dd_tracks_from_fs(fs_map_dir: str) -> DDBeatMapInfoFile:
|
||||
fs_map_dir,
|
||||
) if f.endswith('.egg')
|
||||
][0]
|
||||
target_dir = f'{fs_info.songName} - {fs_info.levelAuthorName}'
|
||||
target_dir = f'{prefix_dir}/{fs_info.songName} - {fs_info.levelAuthorName}'
|
||||
if not os.path.exists(target_dir):
|
||||
os.makedirs(target_dir)
|
||||
|
||||
@@ -112,19 +220,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 +241,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',
|
||||
),
|
||||
)
|
||||
|
||||
@@ -152,7 +261,7 @@ def create_dd_tracks_from_fs(fs_map_dir: str) -> DDBeatMapInfoFile:
|
||||
|
||||
create_ticks = yyyymmdd_to_ticks(datetime.now().strftime('%Y%m%d'))
|
||||
dd_beat_map_info = DDBeatMapInfoFile(
|
||||
OstId=random_9_digit_int(),
|
||||
OstId=ost_id,
|
||||
CreateTicks=create_ticks,
|
||||
CreateTime=str(create_ticks),
|
||||
BeatMapId=random_9_digit_int(),
|
||||
@@ -180,13 +289,78 @@ if __name__ == '__main__':
|
||||
description='Create Dance Dash tracks from Feet Saber maps',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--fs-map-dir',
|
||||
type=str,
|
||||
help='The directory containing the Feet Saber map files',
|
||||
required=True,
|
||||
'--fs-map-dir', type=str, help='The directory containing the Feet Saber map files', required=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
'--fs-map-id', type=str,
|
||||
help='The ID to fetch the Feet Saber map zip', required=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
'--fs-playlist-id', type=str,
|
||||
help='The ID to fetch the Feet Saber playlist', required=False,
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.fs_playlist_id:
|
||||
ost_id = int(args.fs_playlist_id) if args.fs_playlist_id.isdigit(
|
||||
) else random_9_digit_int()
|
||||
target_dir = f'bin/{ost_id}'
|
||||
|
||||
if not os.path.exists(target_dir):
|
||||
os.makedirs(target_dir)
|
||||
|
||||
fs_map_urls, album_cover_url, author = get_feet_saber_maps_from_playlist(
|
||||
args.fs_playlist_id,
|
||||
)
|
||||
album_cover_path = None
|
||||
if album_cover_url:
|
||||
album_cover_path = download_image_from_url(
|
||||
album_cover_url, f'{target_dir}/{ost_id}',
|
||||
)
|
||||
|
||||
def download_and_process_track(url, target_dir, ost_id):
|
||||
fs_map_dir = download_and_extract_zip(url)
|
||||
track = create_dd_tracks_from_fs(fs_map_dir, target_dir, ost_id)
|
||||
return track
|
||||
|
||||
tracks = []
|
||||
with ThreadPoolExecutor() as executor:
|
||||
future_to_url = {
|
||||
executor.submit(download_and_process_track, url, target_dir, ost_id): url for url in
|
||||
fs_map_urls
|
||||
}
|
||||
for future in concurrent.futures.as_completed(future_to_url):
|
||||
track = future.result()
|
||||
tracks.append(track)
|
||||
|
||||
album_info = DDAlbumInfo(
|
||||
OstName=f'Feet Saber - {args.fs_playlist_id} by {author}',
|
||||
BeatMapIdList=sorted([track.BeatMapId for track in tracks]),
|
||||
OstId=ost_id,
|
||||
CoverPath=os.path.basename(album_cover_path),
|
||||
CreateTime=yyyymmdd_to_ticks(datetime.now().strftime('%Y%m%d')),
|
||||
).save_to_file(target_dir)
|
||||
print(f'Created album info file: {album_info}')
|
||||
|
||||
print('Zipping tracks...')
|
||||
with zipfile.ZipFile(f'bin/{ost_id}.zip', 'w', zipfile.ZIP_DEFLATED) as zipf:
|
||||
zipdir(
|
||||
target_dir, zipf,
|
||||
f'Dance Dash_Data/StreamingAssets/NewDLC/{ost_id}',
|
||||
)
|
||||
|
||||
print(f'Created bin/{ost_id}.zip')
|
||||
raise SystemExit(0)
|
||||
|
||||
if not args.fs_map_dir and not args.fs_map_id:
|
||||
print('Either --fs-map-dir or --fs-map-id must be provided.')
|
||||
raise SystemExit(1)
|
||||
|
||||
if args.fs_map_id:
|
||||
url = get_feet_saber_map_from_id(args.fs_map_id)
|
||||
args.fs_map_dir = download_and_extract_zip(url)
|
||||
|
||||
if not os.path.exists(args.fs_map_dir):
|
||||
print(f'Invalid Feet Saber map directory: {args.fs_map_dir}')
|
||||
raise SystemExit(1)
|
||||
|
||||
+2
-2
@@ -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,
|
||||
}
|
||||
|
||||
+91
-47
@@ -1,12 +1,15 @@
|
||||
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
|
||||
@@ -124,10 +127,12 @@ class FSInfoDat:
|
||||
noteJumpMovementSpeed=beatmap['_noteJumpMovementSpeed'],
|
||||
noteJumpStartBeatOffset=beatmap['_noteJumpStartBeatOffset'],
|
||||
customData=FSBeatMapCustomData(
|
||||
difficultyLabel=beatmap['_customData']['_difficultyLabel'],
|
||||
difficultyLabel=beatmap['_customData'].get(
|
||||
'_difficultyLabel',
|
||||
),
|
||||
editorOffset=beatmap['_customData']['_editorOffset'],
|
||||
editorOldOffset=beatmap['_customData']['_editorOldOffset'],
|
||||
suggestions=beatmap['_customData']['_suggestions'],
|
||||
suggestions=beatmap['_customData'].get('_suggestions'),
|
||||
requirements=beatmap['_customData']['_requirements'],
|
||||
),
|
||||
) for beatmap in dbset['_difficultyBeatmaps']
|
||||
@@ -193,6 +198,10 @@ class FSBeatMapFileNoteCustomData:
|
||||
position: tuple[float, float]
|
||||
|
||||
|
||||
FS_LEFT_NOTE = 0
|
||||
FS_RIGHT_NOTE = 1
|
||||
|
||||
|
||||
@dataclass
|
||||
class FSBeatMapFileNote:
|
||||
time: float
|
||||
@@ -202,6 +211,23 @@ class FSBeatMapFileNote:
|
||||
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 = 2, 8
|
||||
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)
|
||||
@@ -211,9 +237,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 +254,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 +280,56 @@ class FSBeatMapFileObstacle:
|
||||
width: int
|
||||
customData: FSBeatMapFileObstacleCustomData
|
||||
|
||||
def is_part_of_last_obstacle(self, last_obstacle: FSBeatMapFileObstacle) -> bool:
|
||||
if self.time < last_obstacle.time:
|
||||
return False
|
||||
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 = 2, 8
|
||||
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)
|
||||
|
||||
if self.time > last_obstacle.time + last_obstacle.duration:
|
||||
return False
|
||||
@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))
|
||||
|
||||
return True
|
||||
@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
|
||||
@@ -307,7 +350,7 @@ class FSBeatMapFile:
|
||||
time=bm['_time'],
|
||||
name=bm['_name'],
|
||||
color=bm['_color'],
|
||||
) for bm in json_dict['_customData']['_bookmarks']
|
||||
) for bm in json_dict['_customData'].get('_bookmarks') or []
|
||||
],
|
||||
)
|
||||
|
||||
@@ -328,7 +371,7 @@ class FSBeatMapFile:
|
||||
cutDirection=note['_cutDirection'],
|
||||
customData=FSBeatMapFileNoteCustomData(
|
||||
position=tuple(note['_customData']['_position']),
|
||||
),
|
||||
) if '_customData' in note else None,
|
||||
) for note in json_dict['_notes']
|
||||
]
|
||||
|
||||
@@ -340,6 +383,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']),
|
||||
@@ -352,7 +396,7 @@ class FSBeatMapFile:
|
||||
float(c)
|
||||
for c in obstacle['_customData']['_color']
|
||||
),
|
||||
),
|
||||
) if '_customData' in obstacle else None,
|
||||
) for obstacle in json_dict['_obstacles']
|
||||
]
|
||||
|
||||
|
||||
@@ -3,9 +3,14 @@ from __future__ import annotations
|
||||
import contextlib
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import zipfile
|
||||
from datetime import datetime
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
def get_drs_ogg_and_duration(folder_path) -> tuple[str, str] | tuple[None, None]:
|
||||
ogg_files = [f for f in os.listdir(folder_path) if f.endswith('.ogg')]
|
||||
@@ -81,7 +86,7 @@ def zipdir(path, ziph, archiveroot):
|
||||
|
||||
|
||||
def random_9_digit_int():
|
||||
return random.randint(10**8, 10**9 - 1)
|
||||
return random.randint(10 ** 8, 10 ** 9 - 1)
|
||||
|
||||
|
||||
def convert_egg_to_ogg_and_get_length(egg_path: str) -> tuple[str, float]:
|
||||
@@ -101,3 +106,67 @@ def convert_egg_to_ogg_and_get_length(egg_path: str) -> tuple[str, float]:
|
||||
ffprobe_cmd, text=True,
|
||||
).strip()
|
||||
return os.path.basename(ogg_path), float(duration_str)
|
||||
|
||||
|
||||
def get_feet_saber_map_from_id(beat_saber_map_id: str) -> str:
|
||||
response = requests.get(
|
||||
f'https://beatsaver.com/api/maps/id/{beat_saber_map_id}',
|
||||
)
|
||||
response.raise_for_status()
|
||||
response_dict = response.json()
|
||||
return response_dict['versions'][0]['downloadURL']
|
||||
|
||||
|
||||
def get_feet_saber_maps_from_playlist(playlist_id: str) -> tuple[list[str], str, str]:
|
||||
response = requests.get(
|
||||
f'https://beatsaver.com/api/playlists/id/{playlist_id}/0',
|
||||
)
|
||||
response.raise_for_status()
|
||||
response_dict = response.json()
|
||||
download_urls = [
|
||||
song['map']['versions'][0]['downloadURL']
|
||||
for song in response_dict['maps']
|
||||
]
|
||||
image_url = response_dict['playlist']['playlistImage']
|
||||
author = response_dict['playlist']['owner']['name']
|
||||
return download_urls, image_url, author
|
||||
|
||||
|
||||
def download_image_from_url(url, target_path):
|
||||
response = requests.get(url, stream=True)
|
||||
response.raise_for_status() # Raise exception for bad responses
|
||||
|
||||
content_type = response.headers['content-type']
|
||||
if 'jpeg' in content_type or 'jpg' in content_type:
|
||||
ext = '.jpg'
|
||||
elif 'png' in content_type:
|
||||
ext = '.png'
|
||||
else:
|
||||
ext = '.jpg'
|
||||
|
||||
with open(f'{target_path}{ext}', 'wb') as file:
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
file.write(chunk)
|
||||
|
||||
print(f'Downloaded image to {target_path}{ext}')
|
||||
|
||||
return f'{target_path}{ext}'
|
||||
|
||||
|
||||
def download_and_extract_zip(url):
|
||||
tmpdirname = tempfile.mkdtemp() # Manually creating a temporary directory
|
||||
|
||||
try:
|
||||
response = requests.get(url, stream=True)
|
||||
zip_path = os.path.join(tmpdirname, 'downloaded.zip')
|
||||
with open(zip_path, 'wb') as out_file:
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
out_file.write(chunk)
|
||||
|
||||
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||
zip_ref.extractall(tmpdirname)
|
||||
|
||||
return tmpdirname
|
||||
except Exception as e:
|
||||
shutil.rmtree(tmpdirname)
|
||||
raise e
|
||||
|
||||
Reference in New Issue
Block a user