Add feet saber wip changes
This commit is contained in:
@@ -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)
|
||||
@@ -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],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
+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,
|
||||
}
|
||||
|
||||
+47
-45
@@ -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']),
|
||||
|
||||
Reference in New Issue
Block a user