tja parser refactor

This commit is contained in:
Anthony Samms
2025-12-02 09:06:55 -05:00
parent 9bfb04419b
commit 00126865b9
3 changed files with 158 additions and 163 deletions

View File

@@ -1,37 +1,19 @@
import bisect import bisect
from enum import IntEnum
import hashlib import hashlib
from dataclasses import dataclass, field, fields from dataclasses import dataclass, field, fields
from libs.tja import NoteList, TJAParser, TimelineObject, get_ms_per_measure from libs.tja import NoteList, ScrollType, TJAParser, TimelineObject, get_ms_per_measure
class NoteType(IntEnum):
NONE = 0
DON = 1
KAT = 2
DON_L = 3
KAT_L = 4
ROLL_HEAD = 5
ROLL_HEAD_L = 6
BALLOON_HEAD = 7
TAIL = 8
KUSUDAMA = 9
class ScrollType(IntEnum):
NMSCROLL = 0
BMSCROLL = 1
HBSCROLL = 2
@dataclass() @dataclass()
class Note: class Note:
type: int = field(init=False) type: int = field(init=False)
hit_ms: float = field(init=False) hit_ms: float = field(init=False)
display: bool = field(init=False)
index: int = field(init=False)
moji: int = field(init=False)
bpm: float = field(init=False) bpm: float = field(init=False)
scroll_x: float = field(init=False) scroll_x: float = field(init=False)
scroll_y: float = field(init=False) scroll_y: float = field(init=False)
display: bool = field(init=False)
index: int = field(init=False)
moji: int = field(init=False)
def __lt__(self, other): def __lt__(self, other):
return self.hit_ms < other.hit_ms return self.hit_ms < other.hit_ms
@@ -135,89 +117,140 @@ class Balloon(Note):
return hash_string.encode('utf-8') return hash_string.encode('utf-8')
class TJAParser2(TJAParser): class TJAParser2(TJAParser):
def notes_to_position(self, diff: int): def _build_command_registry(self):
"""Parse a TJA's notes into a NoteList.""" """Auto-discover command handlers based on naming convention."""
master_notes = NoteList() registry = {}
notes = self.data_to_notes(diff) for name in dir(self):
balloon = self.metadata.course_data[diff].balloon.copy() if name.startswith('handle_'):
count = 0 cmd_name = '#' + name[7:].upper()
index = 0 registry[cmd_name] = getattr(self, name)
time_signature = 4/4 return registry
bpm = self.metadata.bpm
scroll_x_modifier = 1
scroll_y_modifier = 0
barline_display = True
curr_note_list = master_notes.play_notes
curr_draw_list = master_notes.draw_notes
curr_bar_list = master_notes.bars
curr_timeline = master_notes.timeline
init_bpm = TimelineObject()
init_bpm.hit_ms = self.current_ms
init_bpm.bpm = bpm
curr_timeline.append(init_bpm)
prev_note = None
scroll_type = ScrollType.NMSCROLL
bpmchange_last_bpm = bpm def handle_measure(self, part: str, state: dict):
for bar in notes:
bar_length = sum(len(part) for part in bar if '#' not in part)
barline_added = False
for part in bar:
if '#MEASURE' in part:
divisor = part.find('/') divisor = part.find('/')
time_signature = float(part[9:divisor]) / float(part[divisor+1:]) state["time_signature"] = float(part[9:divisor]) / float(part[divisor+1:])
continue
elif '#SCROLL' in part: def handle_scroll(self, part: str, state: dict):
if scroll_type != ScrollType.BMSCROLL:
scroll_value = part[7:] scroll_value = part[7:]
if 'i' in scroll_value: if 'i' in scroll_value:
normalized = scroll_value.replace('.i', 'j').replace('i', 'j') normalized = scroll_value.replace('.i', 'j').replace('i', 'j')
normalized = normalized.replace(',', '') normalized = normalized.replace(',', '')
c = complex(normalized) c = complex(normalized)
scroll_x_modifier = c.real state["scroll_x_modifier"] = c.real
scroll_y_modifier = c.imag state["scroll_y_modifier"] = c.imag
else: else:
scroll_x_modifier = float(scroll_value) state["scroll_x_modifier"] = float(scroll_value)
scroll_y_modifier = 0.0 state["scroll_y_modifier"] = 0.0
continue
elif '#BPMCHANGE' in part: def handle_bpmchange(self, part: str, state: dict):
parsed_bpm = float(part[11:]) parsed_bpm = float(part[11:])
if scroll_type == ScrollType.BMSCROLL or scroll_type == ScrollType.HBSCROLL: if state["scroll_type"] == ScrollType.BMSCROLL or state["scroll_type"] == ScrollType.HBSCROLL:
# Do not modify bpm, it needs to be changed live by bpmchange # Do not modify bpm, it needs to be changed live by bpmchange
bpmchange = parsed_bpm / bpmchange_last_bpm bpmchange = parsed_bpm / state["bpmchange_last_bpm"]
bpmchange_last_bpm = parsed_bpm state["bpmchange_last_bpm"] = parsed_bpm
bpmchange_timeline = TimelineObject() bpmchange_timeline = TimelineObject()
bpmchange_timeline.hit_ms = self.current_ms bpmchange_timeline.hit_ms = self.current_ms
bpmchange_timeline.bpmchange = bpmchange bpmchange_timeline.bpmchange = bpmchange
bisect.insort(curr_timeline, bpmchange_timeline, key=lambda x: x.hit_ms) state["curr_timeline"].append(bpmchange_timeline)
else: else:
timeline_obj = TimelineObject() timeline_obj = TimelineObject()
timeline_obj.hit_ms = self.current_ms timeline_obj.hit_ms = self.current_ms
timeline_obj.bpm = parsed_bpm timeline_obj.bpm = parsed_bpm
bpm = parsed_bpm state["bpm"] = parsed_bpm
bisect.insort(curr_timeline, timeline_obj, key=lambda x: x.hit_ms) state["curr_timeline"].append(timeline_obj)
continue
elif len(part) > 0 and not part[0].isdigit():
continue
ms_per_measure = get_ms_per_measure(bpm, time_signature) def add_bar(self, state: dict):
bar_line = Note() bar_line = Note()
bar_line.hit_ms = self.current_ms bar_line.hit_ms = self.current_ms
bar_line.type = 0 bar_line.type = 0
bar_line.display = barline_display bar_line.display = state["barline_display"]
bar_line.bpm = bpm bar_line.bpm = state["bpm"]
bar_line.scroll_x = scroll_x_modifier bar_line.scroll_x = state["scroll_x_modifier"]
bar_line.scroll_y = scroll_y_modifier bar_line.scroll_y = state["scroll_y_modifier"]
if barline_added: if state["barline_added"]:
bar_line.display = False bar_line.display = False
curr_bar_list.append(bar_line) return bar_line
barline_added = True
def add_note(self, item: str, state: dict):
note = Note()
note.hit_ms = self.current_ms
note.display = True
note.type = int(item)
note.index = state["index"]
note.bpm = state["bpm"]
note.scroll_x = state["scroll_x_modifier"]
note.scroll_y = state["scroll_y_modifier"]
if item in {'5', '6'}:
note = Drumroll(note)
note.color = 255
elif item in {'7', '9'}:
state["balloon_index"] += 1
if state["balloons"] is None:
raise Exception("Balloon note found, but no count was specified")
if item == '9':
note = Balloon(note, is_kusudama=True)
else:
note = Balloon(note)
note.count = 1 if not state["balloons"] else state["balloons"].pop(0)
elif item == '8':
if state["prev_note"] is None:
raise ValueError("No previous note found")
return note
def notes_to_position(self, diff: int):
"""Parse a TJA's notes into a NoteList."""
commands = self._build_command_registry()
master_notes = NoteList()
notes = self.data_to_notes(diff)
state = {
'time_signature': 4/4,
'bpm': self.metadata.bpm,
'scroll_x_modifier': 1,
'scroll_y_modifier': 0,
'scroll_type': ScrollType.NMSCROLL,
'bpmchange_last_bpm': self.metadata.bpm,
'barline_display': True,
'curr_note_list': master_notes.play_notes,
'curr_draw_list': master_notes.draw_notes,
'curr_bar_list': master_notes.bars,
'curr_timeline': master_notes.timeline,
'index': 0,
'balloons': self.metadata.course_data[diff].balloon.copy(),
'balloon_index': 0,
'prev_note': None,
'barline_added': False
}
init_bpm = TimelineObject()
init_bpm.hit_ms = self.current_ms
init_bpm.bpm = state['bpm']
state['curr_timeline'].append(init_bpm)
for bar in notes:
bar_length = sum(len(part) for part in bar if '#' not in part)
state['barline_added'] = False
for part in bar:
if part.startswith('#'):
for cmd_prefix, handler in commands.items():
if part.startswith(cmd_prefix):
handler(part, state)
break
continue
elif len(part) > 0 and not part[0].isdigit():
continue
ms_per_measure = get_ms_per_measure(state["bpm"], state["time_signature"])
bar = self.add_bar(state)
state["curr_bar_list"].append(bar)
state["barline_added"] = True
if len(part) == 0: if len(part) == 0:
self.current_ms += ms_per_measure self.current_ms += ms_per_measure
@@ -230,36 +263,13 @@ class TJAParser2(TJAParser):
self.current_ms += increment self.current_ms += increment
continue continue
note = Note() note = self.add_note(item, state)
note.hit_ms = self.current_ms
note.display = True
note.type = int(item)
note.index = index
note.bpm = bpm
note.scroll_x = scroll_x_modifier
note.scroll_y = scroll_y_modifier
if item in {'5', '6'}:
note = Drumroll(note)
note.color = 255
elif item in {'7', '9'}:
count += 1
if balloon is None:
raise Exception("Balloon note found, but no count was specified")
if item == '9':
note = Balloon(note, is_kusudama=True)
else:
note = Balloon(note)
note.count = 1 if not balloon else balloon.pop(0)
elif item == '8':
if prev_note is None:
raise ValueError("No previous note found")
self.current_ms += increment self.current_ms += increment
curr_note_list.append(note) state["curr_note_list"].append(note)
curr_draw_list.append(note) state["curr_draw_list"].append(note)
self.get_moji(curr_note_list, ms_per_measure) self.get_moji(state["curr_note_list"], ms_per_measure)
index += 1 state["index"] += 1
prev_note = note state["prev_note"] = note
return master_notes, [master_notes], [master_notes], [master_notes] return master_notes, [master_notes], [master_notes], [master_notes]

View File

@@ -59,6 +59,7 @@ class Judgments(IntEnum):
class GameScreen(Screen): class GameScreen(Screen):
JUDGE_X = 414 * tex.screen_scale JUDGE_X = 414 * tex.screen_scale
JUDGE_Y = 256 * tex.screen_scale
def on_screen_start(self): def on_screen_start(self):
super().on_screen_start() super().on_screen_start()
self.mask_shader = ray.load_shader("shader/outline.vs", "shader/mask.fs") self.mask_shader = ray.load_shader("shader/outline.vs", "shader/mask.fs")
@@ -879,8 +880,8 @@ class Player:
if self.is_drumroll: if self.is_drumroll:
self.check_drumroll(drum_type, background, current_time) self.check_drumroll(drum_type, background, current_time)
elif self.is_balloon: elif self.is_balloon:
if not isinstance(curr_note, Balloon): #if not isinstance(curr_note, Balloon):
raise Exception("Balloon mode entered but current note is not balloon") #raise Exception("Balloon mode entered but current note is not balloon")
self.check_balloon(drum_type, curr_note, current_time) self.check_balloon(drum_type, curr_note, current_time)
else: else:
self.curr_drumroll_count = 0 self.curr_drumroll_count = 0

View File

@@ -5,20 +5,16 @@ from enum import IntEnum
import math import math
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Optional
from itertools import chain
import pyray as ray import pyray as ray
from libs.audio import audio from libs.audio import audio
from libs.background import Background
from libs.texture import tex from libs.texture import tex
from libs.tja import ( from libs.tja import calculate_base_score, NoteType
from libs.tja2 import (
Balloon, Balloon,
Drumroll, Drumroll,
Note, Note,
NoteType,
calculate_base_score,
) )
from libs.utils import ( from libs.utils import (
get_current_ms, get_current_ms,
@@ -97,24 +93,21 @@ class Player2(Player):
self.end_time = self.play_notes[-1].hit_ms self.end_time = self.play_notes[-1].hit_ms
def get_position_x(self, note, current_ms): def get_position_x(self, note, current_ms):
judge_line_x = 414 speedx = note.bpm / 240000 * note.scroll_x * (tex.screen_width - GameScreen.JUDGE_X) * tex.screen_scale
return judge_line_x + ((note.hit_ms - current_ms) / 1000.0) * 866 * note.scroll_x return GameScreen.JUDGE_X + (note.hit_ms - current_ms) * speedx
def get_position_y(self): def get_position_y(self, note, current_ms):
return 0 speedy = note.bpm / 240000 * note.scroll_y * (tex.screen_width - GameScreen.JUDGE_Y) * tex.screen_scale
return (note.hit_ms - current_ms) * speedy
def bar_manager(self, current_ms: float): def bar_manager(self, current_ms: float):
"""Manages the bars and removes if necessary """Manages the bars and removes if necessary
Also sets branch conditions""" Also sets branch conditions"""
#Add bar to current_bars list if it is ready to be shown on screen #Add bar to current_bars list if it is ready to be shown on screen
if self.draw_bar_list and current_ms + 1000 > self.draw_bar_list[0].hit_ms: if self.draw_bar_list and current_ms >= self.draw_bar_list[0].hit_ms - 10000:
self.current_bars.append(self.draw_bar_list.popleft()) self.current_bars.append(self.draw_bar_list.popleft())
if self.current_bars and self.current_bars[0].hit_ms < current_ms + 1000:
self.current_bars.pop(0)
def draw_note_manager(self, current_ms: float): def draw_note_manager(self, current_ms: float):
"""Manages the draw_notes and removes if necessary""" """Manages the draw_notes and removes if necessary"""
if self.draw_note_list and current_ms >= self.draw_note_list[0].hit_ms - 10000: if self.draw_note_list and current_ms >= self.draw_note_list[0].hit_ms - 10000:
@@ -139,32 +132,27 @@ class Player2(Player):
note = self.current_notes_draw[0] note = self.current_notes_draw[0]
if note.type in {NoteType.ROLL_HEAD, NoteType.ROLL_HEAD_L, NoteType.BALLOON_HEAD, NoteType.KUSUDAMA} and len(self.current_notes_draw) > 1: if note.type in {NoteType.ROLL_HEAD, NoteType.ROLL_HEAD_L, NoteType.BALLOON_HEAD, NoteType.KUSUDAMA} and len(self.current_notes_draw) > 1:
note = self.current_notes_draw[1] note = self.current_notes_draw[1]
if current_ms > note.hit_ms + 200: if self.get_position_x(note, current_ms) < GameScreen.JUDGE_X:
if note.type == NoteType.TAIL:
self.current_notes_draw.pop(0)
self.current_notes_draw.pop(0) self.current_notes_draw.pop(0)
def draw_drumroll(self, current_ms: float, head: Drumroll, current_eighth: int): def draw_drumroll(self, current_ms: float, head: Drumroll, current_eighth: int):
"""Draws a drumroll in the player's lane""" """Draws a drumroll in the player's lane"""
start_position = self.get_position_x(head, current_ms) start_position = self.get_position_x(head, current_ms)
start_position += self.judge_x
tail = next((note for note in self.current_notes_draw[1:] if note.type == NoteType.TAIL and note.index > head.index), self.current_notes_draw[1]) tail = next((note for note in self.current_notes_draw[1:] if note.type == NoteType.TAIL and note.index > head.index), self.current_notes_draw[1])
is_big = int(head.type == NoteType.ROLL_HEAD_L) is_big = int(head.type == NoteType.ROLL_HEAD_L)
end_position = self.get_position_x(tail, current_ms) end_position = self.get_position_x(tail, current_ms)
end_position += self.judge_x
length = end_position - start_position length = end_position - start_position
color = ray.Color(255, head.color, head.color, 255) color = ray.Color(255, head.color, head.color, 255)
y = tex.skin_config["notes"].y + self.get_position_y() y = tex.skin_config["notes"].y + self.get_position_y(head, current_ms)
moji_y = tex.skin_config["moji"].y moji_y = tex.skin_config["moji"].y
moji_x = -(tex.textures["notes"]["moji"].width//2) + (tex.textures["notes"]["1"].width//2) moji_x = -(tex.textures["notes"]["moji"].width//2) + (tex.textures["notes"]["1"].width//2)
if head.display: if head.display:
if length > 0: tex.draw_texture('notes', "8", frame=is_big, x=start_position, y=y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y, x2=length+tex.skin_config["drumroll_width_offset"].width, color=color)
tex.draw_texture('notes', "8", frame=is_big, x=start_position+(tex.textures["notes"]["5"].width//2), y=y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y, x2=length+tex.skin_config["drumroll_width_offset"].width, color=color)
if is_big: if is_big:
tex.draw_texture('notes', "drumroll_big_tail", x=end_position+tex.textures["notes"]["5"].width//2, y=y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y, color=color) tex.draw_texture('notes', "drumroll_big_tail", x=end_position, y=y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y, color=color)
else: else:
tex.draw_texture('notes', "drumroll_tail", x=end_position+tex.textures["notes"]["5"].width//2, y=y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y, color=color) tex.draw_texture('notes', "drumroll_tail", x=end_position, y=y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y, color=color)
tex.draw_texture('notes', str(head.type), frame=current_eighth % 2, x=start_position, y=y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y, color=color) tex.draw_texture('notes', str(head.type), frame=current_eighth % 2, x=start_position - tex.textures["notes"]["1"].width//2, y=y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y, color=color)
tex.draw_texture('notes', 'moji_drumroll_mid', x=start_position + tex.textures["notes"]["1"].width//2, y=moji_y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y, x2=length) tex.draw_texture('notes', 'moji_drumroll_mid', x=start_position + tex.textures["notes"]["1"].width//2, y=moji_y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y, x2=length)
tex.draw_texture('notes', 'moji', frame=head.moji, x=start_position + moji_x, y=moji_y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y) tex.draw_texture('notes', 'moji', frame=head.moji, x=start_position + moji_x, y=moji_y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y)
@@ -174,12 +162,10 @@ class Player2(Player):
"""Draws a balloon in the player's lane""" """Draws a balloon in the player's lane"""
offset = tex.skin_config["balloon_offset"].x offset = tex.skin_config["balloon_offset"].x
start_position = self.get_position_x(head, current_ms) start_position = self.get_position_x(head, current_ms)
start_position += self.judge_x
tail = next((note for note in self.current_notes_draw[1:] if note.type == NoteType.TAIL and note.index > head.index), self.current_notes_draw[1]) tail = next((note for note in self.current_notes_draw[1:] if note.type == NoteType.TAIL and note.index > head.index), self.current_notes_draw[1])
end_position = self.get_position_x(tail, current_ms) end_position = self.get_position_x(tail, current_ms)
end_position += self.judge_x pause_position = GameScreen.JUDGE_X
pause_position = tex.skin_config["balloon_pause_position"].x + self.judge_x y = tex.skin_config["notes"].y + self.get_position_y(head, current_ms)
y = tex.skin_config["notes"].y + self.get_position_y()
if current_ms >= tail.hit_ms: if current_ms >= tail.hit_ms:
position = end_position position = end_position
elif current_ms >= head.hit_ms: elif current_ms >= head.hit_ms:
@@ -187,8 +173,8 @@ class Player2(Player):
else: else:
position = start_position position = start_position
if head.display: if head.display:
tex.draw_texture('notes', str(head.type), frame=current_eighth % 2, x=position-offset, y=y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y) tex.draw_texture('notes', str(head.type), frame=current_eighth % 2, x=position-offset - tex.textures["notes"]["1"].width//2, y=y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y)
tex.draw_texture('notes', '10', frame=current_eighth % 2, x=position-offset+tex.textures["notes"]["10"].width, y=y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y) tex.draw_texture('notes', '10', frame=current_eighth % 2, x=position-offset+tex.textures["notes"]["10"].width - tex.textures["notes"]["1"].width//2, y=y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y)
def draw_bars(self, current_ms: float): def draw_bars(self, current_ms: float):
"""Draw bars in the player's lane""" """Draw bars in the player's lane"""
@@ -199,12 +185,12 @@ class Player2(Player):
if not bar.display: if not bar.display:
continue continue
x_position = self.get_position_x(bar, current_ms) x_position = self.get_position_x(bar, current_ms)
y_position = self.get_position_y() y_position = self.get_position_y(bar, current_ms)
if y_position != 0: if y_position != 0:
angle = math.degrees(math.atan2(bar.pixels_per_frame_y, bar.pixels_per_frame_x)) angle = math.degrees(math.atan2(bar.scroll_y, bar.scroll_x))
else: else:
angle = 0 angle = 0
tex.draw_texture('notes', str(bar.type), x=x_position+tex.skin_config["moji_drumroll"].x, y=y_position+tex.skin_config["moji_drumroll"].y+(self.is_2p*tex.skin_config["2p_offset"].y), rotation=angle) tex.draw_texture('notes', str(bar.type), x=x_position+tex.skin_config["moji_drumroll"].x- (tex.textures["notes"]["1"].width//2), y=y_position+tex.skin_config["moji_drumroll"].y+(self.is_2p*tex.skin_config["2p_offset"].y), rotation=angle)
def draw_notes(self, current_ms: float, start_ms: float): def draw_notes(self, current_ms: float, start_ms: float):
@@ -220,14 +206,12 @@ class Player2(Player):
current_eighth = 0 current_eighth = 0
x_position = self.get_position_x(note, current_ms) x_position = self.get_position_x(note, current_ms)
y_position = self.get_position_y() y_position = self.get_position_y(note, current_ms)
if isinstance(note, Drumroll): if isinstance(note, Drumroll):
pass self.draw_drumroll(current_ms, note, current_eighth)
#self.draw_drumroll(current_ms, note, current_eighth)
elif isinstance(note, Balloon) and not note.is_kusudama: elif isinstance(note, Balloon) and not note.is_kusudama:
pass self.draw_balloon(current_ms, note, current_eighth)
#self.draw_balloon(current_ms, note, current_eighth) tex.draw_texture('notes', 'moji', frame=note.moji, x=x_position, y=tex.skin_config["moji"].y + y_position+(self.is_2p*tex.skin_config["2p_offset"].y))
#tex.draw_texture('notes', 'moji', frame=note.moji, x=x_position, y=tex.skin_config["moji"].y + y_position+(self.is_2p*tex.skin_config["2p_offset"].y))
else: else:
if note.display: if note.display:
tex.draw_texture('notes', str(note.type), frame=current_eighth % 2, x=x_position - (tex.textures["notes"]["1"].width//2), y=y_position+tex.skin_config["notes"].y+(self.is_2p*tex.skin_config["2p_offset"].y), center=True) tex.draw_texture('notes', str(note.type), frame=current_eighth % 2, x=x_position - (tex.textures["notes"]["1"].width//2), y=y_position+tex.skin_config["notes"].y+(self.is_2p*tex.skin_config["2p_offset"].y), center=True)