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 _build_command_registry(self):
"""Auto-discover command handlers based on naming convention."""
registry = {}
for name in dir(self):
if name.startswith('handle_'):
cmd_name = '#' + name[7:].upper()
registry[cmd_name] = getattr(self, name)
return registry
def handle_measure(self, part: str, state: dict):
divisor = part.find('/')
state["time_signature"] = float(part[9:divisor]) / float(part[divisor+1:])
def handle_scroll(self, part: str, state: dict):
scroll_value = part[7:]
if 'i' in scroll_value:
normalized = scroll_value.replace('.i', 'j').replace('i', 'j')
normalized = normalized.replace(',', '')
c = complex(normalized)
state["scroll_x_modifier"] = c.real
state["scroll_y_modifier"] = c.imag
else:
state["scroll_x_modifier"] = float(scroll_value)
state["scroll_y_modifier"] = 0.0
def handle_bpmchange(self, part: str, state: dict):
parsed_bpm = float(part[11:])
if state["scroll_type"] == ScrollType.BMSCROLL or state["scroll_type"] == ScrollType.HBSCROLL:
# Do not modify bpm, it needs to be changed live by bpmchange
bpmchange = parsed_bpm / state["bpmchange_last_bpm"]
state["bpmchange_last_bpm"] = parsed_bpm
bpmchange_timeline = TimelineObject()
bpmchange_timeline.hit_ms = self.current_ms
bpmchange_timeline.bpmchange = bpmchange
state["curr_timeline"].append(bpmchange_timeline)
else:
timeline_obj = TimelineObject()
timeline_obj.hit_ms = self.current_ms
timeline_obj.bpm = parsed_bpm
state["bpm"] = parsed_bpm
state["curr_timeline"].append(timeline_obj)
def add_bar(self, state: dict):
bar_line = Note()
bar_line.hit_ms = self.current_ms
bar_line.type = 0
bar_line.display = state["barline_display"]
bar_line.bpm = state["bpm"]
bar_line.scroll_x = state["scroll_x_modifier"]
bar_line.scroll_y = state["scroll_y_modifier"]
if state["barline_added"]:
bar_line.display = False
return bar_line
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): def notes_to_position(self, diff: int):
"""Parse a TJA's notes into a NoteList.""" """Parse a TJA's notes into a NoteList."""
commands = self._build_command_registry()
master_notes = NoteList() master_notes = NoteList()
notes = self.data_to_notes(diff) notes = self.data_to_notes(diff)
balloon = self.metadata.course_data[diff].balloon.copy()
count = 0 state = {
index = 0 'time_signature': 4/4,
time_signature = 4/4 'bpm': self.metadata.bpm,
bpm = self.metadata.bpm 'scroll_x_modifier': 1,
scroll_x_modifier = 1 'scroll_y_modifier': 0,
scroll_y_modifier = 0 'scroll_type': ScrollType.NMSCROLL,
barline_display = True 'bpmchange_last_bpm': self.metadata.bpm,
curr_note_list = master_notes.play_notes 'barline_display': True,
curr_draw_list = master_notes.draw_notes 'curr_note_list': master_notes.play_notes,
curr_bar_list = master_notes.bars 'curr_draw_list': master_notes.draw_notes,
curr_timeline = master_notes.timeline '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 = TimelineObject()
init_bpm.hit_ms = self.current_ms init_bpm.hit_ms = self.current_ms
init_bpm.bpm = bpm init_bpm.bpm = state['bpm']
curr_timeline.append(init_bpm) state['curr_timeline'].append(init_bpm)
prev_note = None
scroll_type = ScrollType.NMSCROLL
bpmchange_last_bpm = bpm
for bar in notes: for bar in notes:
bar_length = sum(len(part) for part in bar if '#' not in part) bar_length = sum(len(part) for part in bar if '#' not in part)
barline_added = False state['barline_added'] = False
for part in bar: for part in bar:
if '#MEASURE' in part: if part.startswith('#'):
divisor = part.find('/') for cmd_prefix, handler in commands.items():
time_signature = float(part[9:divisor]) / float(part[divisor+1:]) if part.startswith(cmd_prefix):
continue handler(part, state)
elif '#SCROLL' in part: break
if scroll_type != ScrollType.BMSCROLL:
scroll_value = part[7:]
if 'i' in scroll_value:
normalized = scroll_value.replace('.i', 'j').replace('i', 'j')
normalized = normalized.replace(',', '')
c = complex(normalized)
scroll_x_modifier = c.real
scroll_y_modifier = c.imag
else:
scroll_x_modifier = float(scroll_value)
scroll_y_modifier = 0.0
continue
elif '#BPMCHANGE' in part:
parsed_bpm = float(part[11:])
if scroll_type == ScrollType.BMSCROLL or scroll_type == ScrollType.HBSCROLL:
# Do not modify bpm, it needs to be changed live by bpmchange
bpmchange = parsed_bpm / bpmchange_last_bpm
bpmchange_last_bpm = parsed_bpm
bpmchange_timeline = TimelineObject()
bpmchange_timeline.hit_ms = self.current_ms
bpmchange_timeline.bpmchange = bpmchange
bisect.insort(curr_timeline, bpmchange_timeline, key=lambda x: x.hit_ms)
else:
timeline_obj = TimelineObject()
timeline_obj.hit_ms = self.current_ms
timeline_obj.bpm = parsed_bpm
bpm = parsed_bpm
bisect.insort(curr_timeline, timeline_obj, key=lambda x: x.hit_ms)
continue continue
elif len(part) > 0 and not part[0].isdigit(): elif len(part) > 0 and not part[0].isdigit():
continue continue
ms_per_measure = get_ms_per_measure(bpm, time_signature) ms_per_measure = get_ms_per_measure(state["bpm"], state["time_signature"])
bar_line = Note()
bar_line.hit_ms = self.current_ms bar = self.add_bar(state)
bar_line.type = 0 state["curr_bar_list"].append(bar)
bar_line.display = barline_display state["barline_added"] = True
bar_line.bpm = bpm
bar_line.scroll_x = scroll_x_modifier
bar_line.scroll_y = scroll_y_modifier
if barline_added:
bar_line.display = False
curr_bar_list.append(bar_line)
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, 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+tex.textures["notes"]["5"].width//2, 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, 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+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', 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', 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', '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)