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
from enum import IntEnum
import hashlib
from dataclasses import dataclass, field, fields
from libs.tja import NoteList, 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
from libs.tja import NoteList, ScrollType, TJAParser, TimelineObject, get_ms_per_measure
@dataclass()
class Note:
type: int = 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)
scroll_x: 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):
return self.hit_ms < other.hit_ms
@@ -135,89 +117,140 @@ class Balloon(Note):
return hash_string.encode('utf-8')
class TJAParser2(TJAParser):
def notes_to_position(self, diff: int):
"""Parse a TJA's notes into a NoteList."""
master_notes = NoteList()
notes = self.data_to_notes(diff)
balloon = self.metadata.course_data[diff].balloon.copy()
count = 0
index = 0
time_signature = 4/4
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
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
bpmchange_last_bpm = bpm
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:
def handle_measure(self, part: str, state: dict):
divisor = part.find('/')
time_signature = float(part[9:divisor]) / float(part[divisor+1:])
continue
elif '#SCROLL' in part:
if scroll_type != ScrollType.BMSCROLL:
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)
scroll_x_modifier = c.real
scroll_y_modifier = c.imag
state["scroll_x_modifier"] = c.real
state["scroll_y_modifier"] = c.imag
else:
scroll_x_modifier = float(scroll_value)
scroll_y_modifier = 0.0
continue
elif '#BPMCHANGE' in part:
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 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
bpmchange = parsed_bpm / bpmchange_last_bpm
bpmchange_last_bpm = parsed_bpm
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
bisect.insort(curr_timeline, bpmchange_timeline, key=lambda x: x.hit_ms)
state["curr_timeline"].append(bpmchange_timeline)
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
elif len(part) > 0 and not part[0].isdigit():
continue
state["bpm"] = parsed_bpm
state["curr_timeline"].append(timeline_obj)
ms_per_measure = get_ms_per_measure(bpm, time_signature)
def add_bar(self, state: dict):
bar_line = Note()
bar_line.hit_ms = self.current_ms
bar_line.type = 0
bar_line.display = barline_display
bar_line.bpm = bpm
bar_line.scroll_x = scroll_x_modifier
bar_line.scroll_y = scroll_y_modifier
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 barline_added:
if state["barline_added"]:
bar_line.display = False
curr_bar_list.append(bar_line)
barline_added = True
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):
"""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:
self.current_ms += ms_per_measure
@@ -230,36 +263,13 @@ class TJAParser2(TJAParser):
self.current_ms += increment
continue
note = Note()
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")
note = self.add_note(item, state)
self.current_ms += increment
curr_note_list.append(note)
curr_draw_list.append(note)
self.get_moji(curr_note_list, ms_per_measure)
index += 1
prev_note = note
state["curr_note_list"].append(note)
state["curr_draw_list"].append(note)
self.get_moji(state["curr_note_list"], ms_per_measure)
state["index"] += 1
state["prev_note"] = note
return master_notes, [master_notes], [master_notes], [master_notes]

View File

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

View File

@@ -5,20 +5,16 @@ from enum import IntEnum
import math
import logging
from pathlib import Path
from typing import Optional
from itertools import chain
import pyray as ray
from libs.audio import audio
from libs.background import Background
from libs.texture import tex
from libs.tja import (
from libs.tja import calculate_base_score, NoteType
from libs.tja2 import (
Balloon,
Drumroll,
Note,
NoteType,
calculate_base_score,
)
from libs.utils import (
get_current_ms,
@@ -97,24 +93,21 @@ class Player2(Player):
self.end_time = self.play_notes[-1].hit_ms
def get_position_x(self, note, current_ms):
judge_line_x = 414
return judge_line_x + ((note.hit_ms - current_ms) / 1000.0) * 866 * note.scroll_x
speedx = note.bpm / 240000 * note.scroll_x * (tex.screen_width - GameScreen.JUDGE_X) * tex.screen_scale
return GameScreen.JUDGE_X + (note.hit_ms - current_ms) * speedx
def get_position_y(self):
return 0
def get_position_y(self, note, current_ms):
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):
"""Manages the bars and removes if necessary
Also sets branch conditions"""
#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())
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):
"""Manages the draw_notes and removes if necessary"""
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]
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]
if current_ms > note.hit_ms + 200:
if note.type == NoteType.TAIL:
self.current_notes_draw.pop(0)
if self.get_position_x(note, current_ms) < GameScreen.JUDGE_X:
self.current_notes_draw.pop(0)
def draw_drumroll(self, current_ms: float, head: Drumroll, current_eighth: int):
"""Draws a drumroll in the player's lane"""
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])
is_big = int(head.type == NoteType.ROLL_HEAD_L)
end_position = self.get_position_x(tail, current_ms)
end_position += self.judge_x
length = end_position - start_position
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_x = -(tex.textures["notes"]["moji"].width//2) + (tex.textures["notes"]["1"].width//2)
if head.display:
if length > 0:
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)
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)
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:
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, 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 - 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', 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"""
offset = tex.skin_config["balloon_offset"].x
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])
end_position = self.get_position_x(tail, current_ms)
end_position += self.judge_x
pause_position = tex.skin_config["balloon_pause_position"].x + self.judge_x
y = tex.skin_config["notes"].y + self.get_position_y()
pause_position = GameScreen.JUDGE_X
y = tex.skin_config["notes"].y + self.get_position_y(head, current_ms)
if current_ms >= tail.hit_ms:
position = end_position
elif current_ms >= head.hit_ms:
@@ -187,8 +173,8 @@ class Player2(Player):
else:
position = start_position
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', '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', 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 - 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):
"""Draw bars in the player's lane"""
@@ -199,12 +185,12 @@ class Player2(Player):
if not bar.display:
continue
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:
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:
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):
@@ -220,14 +206,12 @@ class Player2(Player):
current_eighth = 0
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):
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:
pass
#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))
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))
else:
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)