From ef62df2ead1606e13e21a48f3dc81617fa5ed368 Mon Sep 17 00:00:00 2001 From: BroBordd Date: Thu, 6 Nov 2025 12:30:07 +0200 Subject: [PATCH 1/4] Updated Replay to v3.0 --- plugins/utilities.json | 5 +- plugins/utilities/camera.py | 0 plugins/utilities/fileman.py | 0 plugins/utilities/finder.py | 0 plugins/utilities/path.py | 0 plugins/utilities/plugtools.py | 0 plugins/utilities/power.py | 0 plugins/utilities/replay.py | 2142 +++++++++++++++++--------------- plugins/utilities/topmsg.py | 0 9 files changed, 1158 insertions(+), 989 deletions(-) mode change 100755 => 100644 plugins/utilities/camera.py mode change 100755 => 100644 plugins/utilities/fileman.py mode change 100755 => 100644 plugins/utilities/finder.py mode change 100755 => 100644 plugins/utilities/path.py mode change 100755 => 100644 plugins/utilities/plugtools.py mode change 100755 => 100644 plugins/utilities/power.py mode change 100755 => 100644 plugins/utilities/replay.py mode change 100755 => 100644 plugins/utilities/topmsg.py diff --git a/plugins/utilities.json b/plugins/utilities.json index c339e9b..a1f7f4c 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -300,7 +300,8 @@ "commit_sha": "99611b9", "released_on": "10-08-2025", "md5sum": "e97e88170046b4822067f3164ee1dcf5" - } + }, + "3.0": null } }, "topmsg": { @@ -2322,4 +2323,4 @@ } } } -} \ No newline at end of file +} diff --git a/plugins/utilities/camera.py b/plugins/utilities/camera.py old mode 100755 new mode 100644 diff --git a/plugins/utilities/fileman.py b/plugins/utilities/fileman.py old mode 100755 new mode 100644 diff --git a/plugins/utilities/finder.py b/plugins/utilities/finder.py old mode 100755 new mode 100644 diff --git a/plugins/utilities/path.py b/plugins/utilities/path.py old mode 100755 new mode 100644 diff --git a/plugins/utilities/plugtools.py b/plugins/utilities/plugtools.py old mode 100755 new mode 100644 diff --git a/plugins/utilities/power.py b/plugins/utilities/power.py old mode 100755 new mode 100644 diff --git a/plugins/utilities/replay.py b/plugins/utilities/replay.py old mode 100755 new mode 100644 index 0fb2d88..9327962 --- a/plugins/utilities/replay.py +++ b/plugins/utilities/replay.py @@ -3,7 +3,7 @@ # Bug? Feedback? Telegram >> @BroBordd """ -Replay v2.5 - Simple replay player +Replay v3.0 - Simple replay player Experimental. Feedback is appreciated. Adds a button to pause menu and watch menu. @@ -12,63 +12,29 @@ Features: - Common features (pause/play/seek/speed/replay) - Press on progress bar to seek anywhere - Advanced free camera target control -- Ability to zoom in/out to target -- Uses pybrp to display how long a replay is +- Ability to zoom in/out anywhere in the 3D world +- Instant replay start with progressive duration scanning +- Stream-like buffering (like YouTube) which plays while loading - Good UI with detailed toast pop ups - Ability to show/hide UI -- Uses threading everywhere +- Uses threading for non-blocking duration calculation +- Dynamic progress bar that grows with estimated duration +- Visual scan progress indicator with alternating % and time display """ from babase import Plugin -from bauiv1 import ( - get_virtual_screen_size as res, - get_special_widget as gsw, - clipboard_set_text as COPY, - get_replays_dir as rdir, - containerwidget as ocw, - screenmessage as push, - spinnerwidget as spin, - buttonwidget as obw, - fade_screen as fade, - scrollwidget as sw, - SpecialChar as sc, - imagewidget as iw, - textwidget as otw, - gettexture as gt, - apptimer as teck, - AppTimer as tock, - getsound as gs, - UIScale as UIS, - charstr as cs, - Call, - app -) -from bascenev1 import ( - set_replay_speed_exponent as SET, - get_replay_speed_exponent as GET, - new_replay_session as PLAY, - resume_replay as RESUME, - pause_replay as PAUSE, - seek_replay as SEEK, - is_in_replay as ON -) -from _babase import ( - set_camera_position as SCP, - get_camera_position as GCP, - set_camera_manual as SCM, - set_camera_target as SCT, - get_camera_target as GCT -) -from os.path import join, dirname, getsize, basename -from time import time, strftime, gmtime -from random import uniform as uf +import bauiv1 as bui +from bauiv1 import CallPartial +import bascenev1 as bs +import _babase as _ba +import os +from time import strftime, gmtime, time +from random import uniform from threading import Thread -from os import listdir as ls from struct import unpack - class Replay: - VER = '2.5' + VER = '3.0' COL1 = (0.18, 0.18, 0.18) COL2 = (1, 1, 1) COL3 = (0, 1, 0) @@ -76,237 +42,253 @@ class Replay: BUSY = False @classmethod - def BUS(c, b=None): - if b is None: - return c.BUSY - c.BUSY = b + def is_busy(cls, value=None): + if value is None: + return cls.BUSY + cls.BUSY = value - def __init__(s, source=None): - s.sl = s.rn = s.buf = None - s.ohno = False - s._h = _H() - s.p = s.cw( + def __init__(self, source=None): + self.selected = self.replay_name = self.buffer = None + self.error = False + self.huffman = _Huffman() + self.parent = self.create_container( src=source.get_screen_space_center(), - p=GOS(), + p=get_overlay_stack(), size=(400, 500), - oac=lambda: (ocw(s.p, transition='out_scale' if source and source.exists() - else 'out_left'), s.snd('laser'), s.trs.stop()) + oac=lambda: ( + bui.containerwidget(self.parent, transition='out_scale' if source and source.exists() else 'out_left'), + self.play_sound('laser'), + self.transition_sound.stop() + ) ) - s.trs = s.snd('powerup01') - s.tw( - p=s.p, + self.transition_sound = self.play_sound('powerup01') + self.create_text( + p=self.parent, h_align='center', text='Replay', pos=(175, 460), scale=2 ) - sy = 360 - p1 = sw( - parent=s.p, - size=(sy, sy), + scroll_y = 360 + scroll_parent = bui.scrollwidget( + parent=self.parent, + size=(scroll_y, scroll_y), position=(25, 80) ) - s.rd = rdir() - a = [_ for _ in ls(s.rd) if _.endswith('.brp')] - v = 30*len(a) - p2 = ocw( - parent=p1, + self.replays_dir = bui.get_replays_dir() + replays = [f for f in os.listdir(self.replays_dir) if f.endswith('.brp')] + scroll_height = 30 * len(replays) + scroll_content = bui.containerwidget( + parent=scroll_parent, background=False, - size=(sy, v) + size=(scroll_y, scroll_height) ) - s.kids = [] - for i, _ in enumerate(a): - t = s.tw( - p=p2, + self.replay_widgets = [] + for idx, replay_file in enumerate(replays): + widget = self.create_text( + p=scroll_content, click_activate=True, selectable=True, - pos=(0, v-30*i-30), - text=_, - maxwidth=sy, - size=(sy, 30), - color=s.COL2, - oac=Call(s.hl, i, _) + pos=(0, scroll_height - 30 * idx - 30), + text=replay_file, + maxwidth=scroll_y, + size=(scroll_y, 30), + color=self.COL2, + oac=CallPartial(self.highlight, idx, replay_file) ) - s.kids.append(t) - s.psrc = None - for _ in range(3): - b = s.bw( - p=s.p, - pos=(25+120*_, 30), + self.replay_widgets.append(widget) + self.play_source = None + for i in range(3): + btn = self.create_button( + p=self.parent, + pos=(25 + 120 * i, 30), size=(120, 40), - label=['Show', 'Copy', 'Run'][_], - oac=Call(s.con, [s.show, s.copy, s.play][_]), - icon=gt(['folder', 'file', 'nextLevelIcon'][_]) + label=['Show', 'Copy', 'Run'][i], + oac=CallPartial(self.execute, [self.show, self.copy, self.play][i]), + icon=bui.gettexture(['folder', 'file', 'nextLevelIcon'][i]) ) - if _ == 2: - s.psrc = b + if i == 2: + self.play_source = btn - def snd(s, t): - h = gs(t) - h.play() - teck(uf(0.14, 0.17), h.stop) - return h + def play_sound(self, name): + sound = bui.getsound(name) + sound.play() + bui.apptimer(uniform(0.14, 0.17), sound.stop) + return sound - def get(s): - return join(s.rd, s.rn) + def get_replay_path(self): + return os.path.join(self.replays_dir, self.replay_name) - def copy(s): - s.snd('dingSmallHigh') - COPY(s.get()) - push('Copied replay path to clipboard!', color=s.COL3) + def copy(self): + self.play_sound('dingSmallHigh') + bui.clipboard_set_text(self.get_replay_path()) + bui.screenmessage('Copied replay path to clipboard!', color=self.COL3) - def show(s): - gs('ding').play() - push(s.get(), color=s.COL3) + def show(self): + bui.getsound('ding').play() + bui.screenmessage(self.get_replay_path(), color=self.COL3) - def con(s, f): - if s.sl is None: - BTW('Select a replay!') + def execute(self, func): + if self.selected is None: + show_warning('Select a replay!') return - if ON(): - BTW('A replay is already running!') + if bs.is_in_replay(): + show_warning('A replay is already running!') return - return f() + return func() - def hl(s, i, n): - if s.sl == i: - s.psrc = s.kids[i] - s.play() + def highlight(self, idx, name): + if self.selected == idx: + self.play_source = self.replay_widgets[idx] + self.play() return - s.sl = i - s.rn = n - [otw(_, color=s.COL2) for _ in s.kids] - otw(s.kids[i], color=s.COL3) + self.selected = idx + self.replay_name = name + [bui.textwidget(w, color=self.COL2) for w in self.replay_widgets] + bui.textwidget(self.replay_widgets[idx], color=self.COL3) - def play(s): - if s.BUS(): + def play(self): + """Start replay immediately without waiting for duration calculation""" + if self.is_busy(): return - s.BUS(True) - gs('deek').play() - s.load() + self.is_busy(True) + bui.getsound('deek').play() + + # Start replay immediately + bs.set_replay_speed_exponent(0) + bui.fade_screen(1) + + # Create player with unknown duration + Player(path=self.get_replay_path(), duration=None) + self.is_busy(False) - def load(s): - src = s.psrc.get_screen_space_center() - if s.psrc.get_widget_type() == 'text': - src = (src[0]-170, src[1]) - s.parc = c = s.cw( + def load(self): + src = self.play_source.get_screen_space_center() + if self.play_source.get_widget_type() == 'text': + src = (src[0] - 170, src[1]) + self.parent_container = container = self.create_container( src=src, size=(300, 200), - p=GOS() + p=get_overlay_stack() ) - s.tw( - p=c, + self.create_text( + p=container, text='Player', pos=(125, 150), h_align='center', scale=1.4 ) - spin( - parent=c, + bui.spinnerwidget( + parent=container, size=60, position=(75, 100) ) - s.st = s.tw( - p=c, + self.status_text = self.create_text( + p=container, text='Reading...', pos=(115, 87) ) - s.tpar = s.tw( - p=c, + self.progress_text = self.create_text( + p=container, pos=(125, 30), maxwidth=240, - text=f'{s.rn} with total of {getsize(s.get())} bytes\nstreaming bytes to pybrp_stream', + text=f'{self.replay_name} with total of {os.path.getsize(self.get_replay_path())} bytes\nstreaming bytes to pybrp_stream', h_align='center' ) - s.tpar2 = s.tw( - p=c, + self.progress_text2 = self.create_text( + p=container, maxwidth=240, pos=(30, 20), v_align='bottom' ) - s.par = [0, 1] - teck(0.5, Thread(target=s.calc).start) - teck(0.5, s.fpar) - s.spy(s.calc2) + self.progress = [0, 1] + bui.apptimer(0.5, Thread(target=self.calculate).start) + bui.apptimer(0.5, self.update_progress) + self.wait_for_calculation(self.finish_calculation) - def fpar(s): - a, b = s.par - teck(0.1, s.fpar) if (a != b) and (not s.ohno) else 0 - if not a: + def update_progress(self): + current, total = self.progress + bui.apptimer(0.1, self.update_progress) if (current != total) and (not self.error) else 0 + if not current: return - p = a/b*100 - t = '\u2588'*int(p)+'\u2591'*int(100-p) - if not s.ohno: + percent = current / total * 100 + bar = '\u2588' * int(percent) + '\u2591' * int(100 - percent) + if not self.error: try: - otw(s.tpar, text=t) - otw(s.tpar2, text=f'{a} of {b} bytes read') + bui.textwidget(self.progress_text, text=bar) + bui.textwidget(self.progress_text2, text=f'{current} of {total} bytes read') except: return - def calc(s): + def calculate(self): try: - s.buf = GMS(s._h, s.get(), s.par) + self.buffer = get_replay_duration(self.huffman, self.get_replay_path(), self.progress) except: - s.buf = 0 + self.buffer = 0 - def calc2(s, t): - otw(s.st, text='Starting...' if t else 'Wait what?') - otw(s.tpar2, text=f'result was {t} milleseconds') if t else t - if not t: - s.ohno = True - otw(s.tpar, text='pybrp returned zero duration, error?\nclosing this window in 5 seconds') - otw(s.tpar2, text='') - teck(1 if t else 5, Call(s._play, t)) + def finish_calculation(self, duration): + bui.textwidget(self.status_text, text='Starting...' if duration else 'Wait what?') + bui.textwidget(self.progress_text2, text=f'result was {duration} milleseconds') if duration else duration + if not duration: + self.error = True + bui.textwidget(self.progress_text, text='pybrp returned zero duration, error?\nclosing this window in 5 seconds') + bui.textwidget(self.progress_text2, text='') + bui.apptimer(1 if duration else 5, CallPartial(self.start_player, duration)) - def spy(s, f, i=60): - if not i: - s.buf = None - f(None) + def wait_for_calculation(self, callback, iterations=60): + if not iterations: + self.buffer = None + callback(None) return - if s.buf is not None: - b = s.buf - s.buf = None - f(b) + if self.buffer is not None: + result = self.buffer + self.buffer = None + callback(result) return - teck(0.5, Call(s.spy, f, i-1)) + bui.apptimer(0.5, CallPartial(self.wait_for_calculation, callback, iterations - 1)) - def _play(s, t): - if t == 0: - BTW("Couldn't load replay!") - ocw(s.parc, transition='out_scale') - s.BUS(False) + def start_player(self, duration): + if duration == 0: + show_warning("Couldn't load replay!") + bui.containerwidget(self.parent_container, transition='out_scale') + self.is_busy(False) return - SET(0) - fade(1) - Player(path=s.get(), duration=t) - s.BUS(False) - bw = lambda s, p=None, oac=None, pos=None, **k: obw( - parent=p, - color=s.COL1, - textcolor=s.COL2, - on_activate_call=oac, - position=pos, - button_type='square', - enable_sound=False, - **k - ) - cw = lambda s, p=None, pos=None, src=None, oac=None, **k: ocw( - color=s.COL1, - parent=p, - position=pos, - scale_origin_stack_offset=src, - transition='in_scale', - on_outside_click_call=oac, - **k - ) - tw = lambda s, color=None, oac=None, p=None, pos=None, **k: otw( - parent=p, - position=pos, - color=color or s.COL2, - on_activate_call=oac, - **k - ) + bs.set_replay_speed_exponent(0) + bui.fade_screen(1) + Player(path=self.get_replay_path(), duration=duration) + self.is_busy(False) + def create_button(self, p=None, oac=None, pos=None, **kwargs): + return bui.buttonwidget( + parent=p, + color=self.COL1, + textcolor=self.COL2, + on_activate_call=oac, + position=pos, + button_type='square', + enable_sound=False, + **kwargs + ) + + def create_container(self, p=None, pos=None, src=None, oac=None, **kwargs): + return bui.containerwidget( + color=self.COL1, + parent=p, + position=pos, + scale_origin_stack_offset=src, + transition='in_scale', + on_outside_click_call=oac, + **kwargs + ) + + def create_text(self, color=None, oac=None, p=None, pos=None, **kwargs): + return bui.textwidget( + parent=p, + position=pos, + color=color or self.COL2, + on_activate_call=oac, + **kwargs + ) class Player: TICK = 0.01 @@ -328,550 +310,718 @@ class Player: COL15 = (1, 1, 1) COL16 = (0.1, 0.2, 0.4) COL17 = (1, 1.7, 2) + COL18 = (0.45, 0.45, 0.45) + COL19 = (1, 0.8, 0) - def __init__(s, path, duration): - s.path = path - s.du = duration - s.ds = s.du / 1000 - s.ps = s.nah = s.camon = s.snma = s.gay = False - s.caml = None - s.rn = s.st = s.pr = 0 - s.camz = 1 - [setattr(s, _, []) for _ in ['kids', 'camkids', 'hdkids', 'snkids', 'snuikids']] - PLAY(path) - x, y = res() - s.sy = 80 - s.p = ocw( - size=(x, s.sy), - stack_offset=(0, -y/2+s.sy/2), + def __init__(self, path, duration): + self.path = path + self.duration_ms = duration # Will be None initially + self.duration_sec = None if duration is None else duration / 1000 + self.scanning = duration is None + self.scan_progress = [0, 1] + self.estimated_duration = 10.0 # Start with 10 second estimate + self.confirmed_duration_sec = 0 + self.paused = self.ui_hidden = self.camera_on = self.cinema_mode = self.manual_zoom = False + self.camera_look = None + self.replay_time = self.start_time = self.progress_val = 0 + self.camera_zoom = 1 + [setattr(self, attr, []) for attr in ['ui_widgets', 'camera_widgets', 'hide_widgets', 'cinema_widgets', 'cinema_ui_widgets']] + + # Start replay immediately + bs.new_replay_session(path) + + width, height = bui.get_virtual_screen_size() + self.bar_height = 80 + self.parent = bui.containerwidget( + size=(width, self.bar_height), + stack_offset=(0, -height / 2 + self.bar_height / 2), background=False ) - s.bg = iw( - parent=s.p, - texture=gt('black'), - size=(x+3, s.sy+5), + self.background = bui.imagewidget( + parent=self.parent, + texture=bui.gettexture('black'), + size=(width + 3, self.bar_height + 5), position=(0, -2), opacity=0.4 ) - s.mkui() - s.mkhd() - # finally - s.sp = 1 - s.foc() - s.play() - def mkhd(s): - f = s.hdkids.append - s.tex = ['\u25bc', '\u25b2'] - s.kekb = s.bw( - p=s.p, + self.create_ui() + self.create_hide_button() + self.speed = 1 + self.start_focus() + self.play() + + if self.scanning: + self.huffman = _Huffman() + self.scan_cycle = 0 # For alternating display + Thread(target=self.calculate_duration).start() + self.scan_timer = bui.AppTimer(0.1, self.update_scan_progress, repeat=True) + + def calculate_duration(self): + """Calculate duration in background thread with reduced load""" + import time as pytime + + try: + total_ms = 0 + last_yield = pytime.time() + + with open(self.path, 'rb') as f: + f.seek(0, 2) + file_size = f.tell() + self.scan_progress[1] = file_size + f.seek(6) + + while True: + current_pos = f.tell() + self.scan_progress[0] = current_pos + + # Yield control every 10ms to prevent freezing + current_time = pytime.time() + if current_time - last_yield > 0.01: + pytime.sleep(0.005) # Small sleep to reduce CPU load + last_yield = current_time + + b_data = f.read(1) + if not b_data: + break + + b1, comp_len = b_data[0], 0 + if b1 < 254: + comp_len = b1 + elif b1 == 254: + comp_len = int.from_bytes(f.read(2), 'little') + else: + comp_len = int.from_bytes(f.read(4), 'little') + + if comp_len == 0: + continue + + raw_msg = self.huffman.decompress(f.read(comp_len)) + + if not raw_msg or raw_msg[0] != 1: + continue + + sub_off = 1 + while sub_off + 2 <= len(raw_msg): + sub_size_bytes = raw_msg[sub_off:sub_off+2] + if len(sub_size_bytes) < 2: + break + sub_size = int.from_bytes(sub_size_bytes, 'little') + sub_off += 2 + if sub_off + sub_size > len(raw_msg): + break + sub_data = raw_msg[sub_off:sub_off+sub_size] + if len(sub_data) >= 2 and sub_data[0] == 0: + total_ms += sub_data[1] + sub_off += sub_size + + # Update confirmed duration (this is safe to seek to!) + self.confirmed_duration_sec = total_ms / 1000 + + # Update estimated duration based on progress + if current_pos > 1000: # After reading at least 1KB + progress_percent = current_pos / file_size + if progress_percent > 0: + estimated_total_ms = total_ms / progress_percent + self.estimated_duration = max(10.0, estimated_total_ms / 1000) + + self.scan_progress[0] = self.scan_progress[1] + self.duration_ms = total_ms + self.duration_sec = total_ms / 1000 + + except Exception as e: + self.duration_ms = 0 + self.duration_sec = 0 + finally: + self.scanning = False + + def update_scan_progress(self): + """Update duration text with scan progress - alternates between % and estimated time""" + if not self.scanning: + self.scan_timer = None + # Fade out loading bar + if hasattr(self, 'loading_bar') and self.loading_bar.exists(): + bui.imagewidget(self.loading_bar, opacity=0) + if self.main_bar.exists(): + bui.imagewidget(self.main_bar, opacity=0.6) + + # Update to show final duration with normal color + if hasattr(self, 'duration_time_text') and self.duration_time_text.exists(): + bui.textwidget( + self.duration_time_text, + text=format_time(self.duration_sec), + color=self.COL6 + ) + return + + # Show scanning progress + if hasattr(self, 'duration_time_text') and self.duration_time_text.exists(): + current, total = self.scan_progress + if total > 0: + percent = int((current / total) * 100) + + # Cycle between showing percentage and estimated time + self.scan_cycle += 1 + show_percent = (self.scan_cycle // 10) % 2 == 0 # Switch every second + + if show_percent: + # Show scan percentage + bui.textwidget( + self.duration_time_text, + text=f'Scan {percent}%', + color=self.COL6 + ) + else: + # Show estimated duration in orange + est_mins = int(self.estimated_duration // 60) + est_secs = int(self.estimated_duration % 60) + bui.textwidget( + self.duration_time_text, + text=format_time(self.estimated_duration), + color=self.COL19 # Orange color + ) + + # Update loading bar width based on scan progress + if hasattr(self, 'loading_bar') and self.loading_bar.exists(): + loading_width = (current / total) * self.progress_width + bui.imagewidget(self.loading_bar, size=(loading_width, 5)) + + def create_hide_button(self): + widgets = self.hide_widgets.append + self.hide_icons = ['\u25bc', '\u25b2'] + self.hide_button = self.create_button( + p=self.parent, pos=(20, 15), size=(50, 50), - oac=s.kek, - color=s.COL10 + oac=self.toggle_hide, + color=self.COL10 ) - f(s.kekb) - s.kekt = otw( - parent=s.p, - text=s.tex[s.nah], + widgets(self.hide_button) + self.hide_text = bui.textwidget( + parent=self.parent, + text=self.hide_icons[self.ui_hidden], position=(44, 30), scale=2, shadow=0.4, - color=s.COL11 + color=self.COL11 ) - f(s.kekt) - f(iw( - parent=s.p, + widgets(self.hide_text) + widgets(bui.imagewidget( + parent=self.parent, position=(18, 13), size=(54, 54), - color=s.COL10, - texture=gt('white'), + color=self.COL10, + texture=bui.gettexture('white'), opacity=0.4 )) - def killhd(s): - [_.delete() for _ in s.hdkids] - s.hdkids.clear() + def destroy_hide_button(self): + [w.delete() for w in self.hide_widgets] + self.hide_widgets.clear() - def mkui(s): - s.up = True - f = s.kids.append - x, y = res() - sy = s.sy - p = s.p - # exit - f(s.bw( - p=p, - pos=(x-65, 15), + def create_ui(self): + """Modified to handle unknown duration initially""" + self.ui_visible = True + widgets = self.ui_widgets.append + width, height = bui.get_virtual_screen_size() + bar_height = self.bar_height + parent = self.parent + + # Exit button + widgets(self.create_button( + p=parent, + pos=(width - 65, 15), size=(50, 50), - color=s.COL0, - oac=s.bye + color=self.COL0, + oac=self.exit )) - c = s.COL1 - f(iw( - parent=p, - texture=gt('crossOut'), - color=(c[0]*10, c[1]*10, c[2]*10), - position=(x-60, 20), + color = self.COL1 + widgets(bui.imagewidget( + parent=parent, + texture=bui.gettexture('crossOut'), + color=(color[0] * 10, color[1] * 10, color[2] * 10), + position=(width - 60, 20), size=(40, 40) )) - # speed - for _ in range(2): - a = [ - 'FAST_FORWARD_BUTTON', - 'REWIND_BUTTON' - ][_] - pos = (x-130-260*_, 15) - f(s.bw( - p=p, + + # Speed buttons + for i in range(2): + arrow = ['FAST_FORWARD_BUTTON', 'REWIND_BUTTON'][i] + pos = (width - 130 - 260 * i, 15) + widgets(self.create_button( + p=parent, pos=pos, size=(50, 50), - color=s.COL2, - oac=Call(s.boost, [1, -1][_]), + color=self.COL2, + oac=CallPartial(self.change_speed, [1, -1][i]), repeat=True )) - f(otw( - parent=p, - text=cs(getattr(sc, a)), - color=s.COL3, - position=(pos[0]-2, pos[1]+13), + widgets(bui.textwidget( + parent=parent, + text=bui.charstr(getattr(bui.SpecialChar, arrow)), + color=self.COL3, + position=(pos[0] - 2, pos[1] + 13), h_align='center', v_align='center', scale=1.8, shadow=0.3 )) - # seek - for _ in range(2): - a = [ - 'RIGHT_ARROW', - 'LEFT_ARROW' - ][_] - pos = (x-195-130*_, 15) - f(s.bw( - p=p, + + # Seek buttons + for i in range(2): + arrow = ['RIGHT_ARROW', 'LEFT_ARROW'][i] + pos = (width - 195 - 130 * i, 15) + widgets(self.create_button( + p=parent, pos=pos, size=(50, 50), - color=s.COL4, - oac=Call(s.seek, [1, -1][_]), + color=self.COL4, + oac=CallPartial(self.seek, [1, -1][i]), repeat=True )) - f(otw( - parent=p, - text=cs(getattr(sc, a)), - color=s.COL5, - position=(pos[0]-1, pos[1]+12), + widgets(bui.textwidget( + parent=parent, + text=bui.charstr(getattr(bui.SpecialChar, arrow)), + color=self.COL5, + position=(pos[0] - 1, pos[1] + 12), h_align='center', v_align='center', scale=1.7, shadow=0.2 )) - # pause - pos = (x-260, 15) - f(s.bw( - p=p, + + # Pause button + pos = (width - 260, 15) + widgets(self.create_button( + p=parent, pos=pos, size=(50, 50), - color=s.COL6, - oac=s.toggle + color=self.COL6, + oac=self.toggle_pause )) - s.tt = otw( - parent=p, - color=s.COL7, - position=(pos[0]+12, pos[1]+11), + self.pause_text = bui.textwidget( + parent=parent, + color=self.COL7, + position=(pos[0] + 12, pos[1] + 11), scale=1.5, shadow=0.3 ) - f(s.tt) - s.toggle(dry=True) - # replay - pos = (x-455, 15) - f(s.bw( - p=p, + widgets(self.pause_text) + self.toggle_pause(dry=True) + + # Restart button + pos = (width - 455, 15) + widgets(self.create_button( + p=parent, pos=pos, size=(50, 50), - color=s.COL12, - oac=s.rloop + color=self.COL12, + oac=self.restart )) - c = s.COL13 - sk = 1.5 - f(iw( - parent=p, - texture=gt('replayIcon'), - color=(c[0]*sk, c[1]*sk, c[2]*sk), - position=(pos[0]+2, pos[1]+1), + color = self.COL13 + scale = 1.5 + widgets(bui.imagewidget( + parent=parent, + texture=bui.gettexture('replayIcon'), + color=(color[0] * scale, color[1] * scale, color[2] * scale), + position=(pos[0] + 2, pos[1] + 1), size=(47, 47), )) - # progress - pos = (285, sy/2-2) - s.px = x-790 - f(iw( - parent=p, - texture=gt('white'), - size=(s.px, 5), + + # Progress bar + pos = (285, bar_height / 2 - 2) + self.progress_width = width - 790 + self.main_bar = bui.imagewidget( + parent=parent, + texture=bui.gettexture('white'), + size=(self.progress_width, 5), position=pos, - opacity=0.4, - color=s.COL8 - )) - s.nbp = (pos[0]-24, pos[1]-22) - s.nb = iw( - parent=p, - texture=gt('nub'), + opacity=0.2 if self.scanning else 0.6, + color=self.COL8 + ) + widgets(self.main_bar) + + # Secondary progress bar + self.loading_bar = bui.imagewidget( + parent=parent, + texture=bui.gettexture('white'), + size=(0, 5), + position=pos, + opacity=0.5, + color=self.COL18 + ) + widgets(self.loading_bar) + + # Nub + self.nub_pos = (pos[0] - 24, pos[1] - 22) + self.nub = bui.imagewidget( + parent=parent, + texture=bui.gettexture('nub'), size=(50, 50), - position=s.nbp, + position=self.nub_pos, opacity=0.4, - color=s.COL9 + color=self.COL9 ) - f(s.nb) - # timestamp - s.ct = otw( - parent=p, + widgets(self.nub) + + # Time displays + self.current_time_text = bui.textwidget( + parent=parent, position=(155, 40), - color=s.COL7, - text=FOR(s.rn-s.st) + color=self.COL7, + text=format_time(self.replay_time - self.start_time), + maxwidth=100 ) - f(s.ct) - f(otw( - parent=p, + widgets(self.current_time_text) + + # Duration display - alternates between scan% and estimated time while scanning + self.duration_time_text = bui.textwidget( + parent=parent, position=(155, 11), - text=FOR(s.ds), - color=s.COL6 - )) - # sensor - sx, sy = (285, 15) - n = 100 - tp = s.px/n - for _ in range(n): - f(obw( + text='Scan 0%' if self.scanning else format_time(self.duration_sec), + color=self.COL6, + maxwidth=100 + ) + widgets(self.duration_time_text) + + # Seekbar sensors + sensor_x, sensor_y = (285, 15) + sensor_count = 100 + tile_width = self.progress_width / sensor_count + for i in range(sensor_count): + widgets(bui.buttonwidget( label='', - parent=p, - position=(sx+tp*_, sy), - size=(tp, 50), - texture=gt('empty'), + parent=parent, + position=(sensor_x + tile_width * i, sensor_y), + size=(tile_width, 50), + texture=bui.gettexture('empty'), enable_sound=False, - on_activate_call=Call(s.jump, _/n), + on_activate_call=CallPartial(self.jump, i / sensor_count), selectable=False )) - # camera - f(s.bw( - p=s.p, + + # Camera button + widgets(self.create_button( + p=self.parent, pos=(85, 15), size=(50, 50), - color=s.COL14, - oac=s.cam + color=self.COL14, + oac=self.toggle_camera )) - c = s.COL15 - sk = 1.5 - f(iw( - parent=s.p, - texture=gt('achievementOutline'), + color = self.COL15 + scale = 1.5 + widgets(bui.imagewidget( + parent=self.parent, + texture=bui.gettexture('achievementOutline'), position=(88, 18), - color=(c[0]*sk, c[1]*sk, c[2]*sk), + color=(color[0] * scale, color[1] * scale, color[2] * scale), size=(45, 45) )) - # info - ix, iy = (443, 98) - s.ok = iw( - texture=gt('white'), - position=(x-456, 100), - parent=p, - size=(ix, iy), + + # Info panel + info_w, info_h = (443, 98) + self.info_bg = bui.imagewidget( + texture=bui.gettexture('white'), + position=(width - 456, 100), + parent=parent, + size=(info_w, info_h), opacity=0 ) - f(s.ok) - s.ok2 = otw( - position=(x-ix+182.5, iy+64), + widgets(self.info_bg) + self.info_title = bui.textwidget( + position=(width - info_w + 182.5, info_h + 64), h_align='center', scale=1.2, - parent=p, - maxwidth=ix-20 + parent=parent, + maxwidth=info_w - 20 ) - f(s.ok2) - s.ok3 = otw( - position=(x-ix+182.5, iy+10), + widgets(self.info_title) + self.info_text = bui.textwidget( + position=(width - info_w + 182.5, info_h + 10), h_align='center', - parent=p, - maxwidth=ix-20 + parent=parent, + maxwidth=info_w - 20 ) - f(s.ok3) + widgets(self.info_text) - def mkcamui(s): - f = s.camkids.append - x, y = (19, 100) - s.cambg = iw( - parent=s.p, + def create_camera_ui(self): + widgets = self.camera_widgets.append + cam_x, cam_y = (19, 100) + self.camera_bg = bui.imagewidget( + parent=self.parent, size=(235, 260), - position=(x, y), - texture=gt('white'), - color=s.COL14, + position=(cam_x, cam_y), + texture=bui.gettexture('white'), + color=self.COL14, opacity=0.05 ) - f(s.cambg) - # tip - s.cambg2 = iw( - parent=s.p, + widgets(self.camera_bg) + + self.camera_bg2 = bui.imagewidget( + parent=self.parent, size=(235, 100), opacity=0.05, - position=(x, y+267), - color=s.COL14, - texture=gt('white') + position=(cam_x, cam_y + 267), + color=self.COL14, + texture=bui.gettexture('white') ) - f(s.cambg2) - s.fcam() - f(otw( - parent=s.p, + widgets(self.camera_bg2) + self.fade_camera() + widgets(bui.textwidget( + parent=self.parent, h_align='center', v_align='center', text='To maintain animated\nsmooth camera, keep\nzoom at auto.', maxwidth=205, max_height=190, - position=(x+92, y+300), - color=s.COL15 + position=(cam_x + 92, cam_y + 300), + color=self.COL15 )) - # reset - f(s.bw( - p=s.p, + + widgets(self.create_button( + p=self.parent, label='Reset', - color=s.COL14, - textcolor=s.COL15, + color=self.COL14, + textcolor=self.COL15, size=(205, 30), - pos=(x+15, y+7), - oac=s.camr + pos=(cam_x + 15, cam_y + 7), + oac=self.reset_camera )) - # seperator - f(iw( - parent=s.p, + + widgets(bui.imagewidget( + parent=self.parent, size=(219, 2), - position=(x+8, y+44), - texture=gt('white'), - color=s.COL15, + position=(cam_x + 8, cam_y + 44), + texture=bui.gettexture('white'), + color=self.COL15, opacity=0.6 )) - # look - f(s.bw( - p=s.p, + + widgets(self.create_button( + p=self.parent, label='Look', - pos=(x+15, y+53), - color=s.COL14, - textcolor=s.COL15, + pos=(cam_x + 15, cam_y + 53), + color=self.COL14, + textcolor=self.COL15, size=(205, 30), - oac=s.look + oac=self.look )) - s.ltw = otw( - parent=s.p, - text=str(RND(s.caml) if s.caml else 'players'), + self.look_text = bui.textwidget( + parent=self.parent, + text=str(round_tuple(self.camera_look) if self.camera_look else 'players'), v_align='center', h_align='center', - position=(x+92, y+90), - color=s.COL14, + position=(cam_x + 92, cam_y + 90), + color=self.COL14, maxwidth=205, max_height=40 ) - f(s.ltw) - f(otw( - parent=s.p, + widgets(self.look_text) + widgets(bui.textwidget( + parent=self.parent, text='Currently looking at:', v_align='center', h_align='center', - position=(x+92, y+120), - color=s.COL15, + position=(cam_x + 92, cam_y + 120), + color=self.COL15, maxwidth=205, max_height=40 )) - # seperator - f(iw( - parent=s.p, + + widgets(bui.imagewidget( + parent=self.parent, size=(219, 2), - position=(x+8, y+154), - texture=gt('white'), - color=s.COL15, + position=(cam_x + 8, cam_y + 154), + texture=bui.gettexture('white'), + color=self.COL15, opacity=0.6 )) - # zoom - [f(s.bw( - p=s.p, - label=['-', '+'][_], - pos=(x+13+113*_, y+163), - color=s.COL14, - textcolor=s.COL15, + + [widgets(self.create_button( + p=self.parent, + label=['-', '+'][i], + pos=(cam_x + 13 + 113 * i, cam_y + 163), + color=self.COL14, + textcolor=self.COL15, size=(98, 30), repeat=True, - oac=Call(s.zoom, [1, -1][_]) - )) for _ in [0, 1]] - s.ztw = otw( - parent=s.p, - text=f'x{round(0.5**(s.camz-1), 2)}' if s.camz != 1 else 'x1.0' if s.gay else 'auto', + oac=CallPartial(self.zoom, [1, -1][i]) + )) for i in [0, 1]] + self.zoom_text = bui.textwidget( + parent=self.parent, + text=f'x{round(0.5 ** (self.camera_zoom - 1), 2)}' if self.camera_zoom != 1 else 'x1.0' if self.manual_zoom else 'auto', v_align='center', h_align='center', - position=(x+92, y+200), - color=s.COL14, + position=(cam_x + 92, cam_y + 200), + color=self.COL14, maxwidth=205, max_height=40 ) - f(s.ztw) - f(otw( - parent=s.p, + widgets(self.zoom_text) + widgets(bui.textwidget( + parent=self.parent, text='Current zoom:', v_align='center', h_align='center', - position=(x+92, y+227), - color=s.COL15, + position=(cam_x + 92, cam_y + 227), + color=self.COL15, maxwidth=205, max_height=40 )) - def zoom(s, i): - n = round(s.camz+i*0.05, 2) - if s.camz == 1 and not s.gay: - SCM(True) - s.camp = GCP() - s.caml = GCT() - otw(s.ltw, text=str(RND(s.caml))) - if n == 1 and not s.gay: - SCM(False) - s.caml = None - otw(s.ltw, text='players') - s.camz = n - otw(s.ztw, text=f'x{round(0.5**(n-1), 2)}' if n != 1 else 'x1.0' if s.gay else 'auto') - s.zom() + def zoom(self, direction): + new_zoom = round(self.camera_zoom + direction * 0.05, 2) + if self.camera_zoom == 1 and not self.manual_zoom: + _ba.set_camera_manual(True) + self.camera_pos = _ba.get_camera_position() + self.camera_look = _ba.get_camera_target() + bui.textwidget(self.look_text, text=str(round_tuple(self.camera_look))) + if new_zoom == 1 and not self.manual_zoom: + _ba.set_camera_manual(False) + self.camera_look = None + bui.textwidget(self.look_text, text='players') + self.camera_zoom = new_zoom + bui.textwidget(self.zoom_text, text=f'x{round(0.5 ** (new_zoom - 1), 2)}' if new_zoom != 1 else 'x1.0' if self.manual_zoom else 'auto') + self.apply_zoom() - def look(s): - s.killui() - s.killhd() - s.fkek(0.4, -0.1) - s.camlo = s.caml - s.campo = GCP() - s.gayo = s.gay - s.mksns() - s.mksnui() - s.mksnb() - s.mksni() + def look(self): + self.destroy_ui() + self.destroy_hide_button() + self.fade_hide_button(0.4, -0.1) + self.look_backup = self.camera_look + self.pos_backup = _ba.get_camera_position() + self.manual_backup = self.manual_zoom + self.create_cinema_sensors() + self.create_cinema_ui() + self.create_cinema_button() + self.create_cinema_indicator() - def _look(s, x, y): - o = s.caml or GCT() - sk = 0.7*s.camz - s.caml = n = (o[0]+x*sk, o[1]+y*sk, o[2]) - 0 if s.snma else otw(s.snt, text=str(RND(n))) - s.camp = GCP() - if s.camz != 1: - s.gay = True - s.camz = 1 + def update_look(self, dx, dy): + origin = self.camera_look or _ba.get_camera_target() + scale = 0.7 * self.camera_zoom + self.camera_look = new_look = (origin[0] + dx * scale, origin[1] + dy * scale, origin[2]) + 0 if self.cinema_mode else bui.textwidget(self.cinema_text, text=str(round_tuple(new_look))) + self.camera_pos = _ba.get_camera_position() + if self.camera_zoom != 1: + self.manual_zoom = True + self.camera_zoom = 1 - def foc(s): - s.tfoc = tock(0.01, s.focus, repeat=True) + def start_focus(self): + self.focus_timer = bui.AppTimer(0.01, self.focus, repeat=True) - def focus(s): - SCT(*s.caml) if s.caml else 0 + def focus(self): + _ba.set_camera_target(*self.camera_look) if self.camera_look else 0 - def zom(s): - if s.camz == 1 and not s.gay: + def apply_zoom(self): + if self.camera_zoom == 1 and not self.manual_zoom: return - z = s.camz - tx, ty, tz = GCT() - px, py, pz = s.camp - npx = tx+(px-tx)*z - npy = ty+(py-ty)*z - npz = tz+(pz-tz)*z - SCP(npx, npy, npz) + zoom = self.camera_zoom + target_x, target_y, target_z = _ba.get_camera_target() + pos_x, pos_y, pos_z = self.camera_pos + new_pos_x = target_x + (pos_x - target_x) * zoom + new_pos_y = target_y + (pos_y - target_y) * zoom + new_pos_z = target_z + (pos_z - target_z) * zoom + _ba.set_camera_position(new_pos_x, new_pos_y, new_pos_z) - def fockill(s): - s.tfoc = None + def stop_focus(self): + self.focus_timer = None - def snsave(s): - s.snbye() + def save_cinema(self): + self.exit_cinema() - def snbye(s): - s.killsn() - s.mkui() - s.mkhd() - s.fkek(0, 0.1) - s.pro() + def exit_cinema(self): + self.destroy_cinema() + self.create_ui() + self.create_hide_button() + self.fade_hide_button(0, 0.1) + self.update_progress_ui() - def mksns(s): - x, y = res() - sz = 50 - a = int(x/sz) - b = int(y/sz) - ha = a/2 - hb = b/2 - [s.snkids.append(obw( - parent=s.p, - size=(sz, sz), - position=(i*sz, j*sz), - texture=gt('empty'), + def create_cinema_sensors(self): + width, height = bui.get_virtual_screen_size() + tile = 50 + cols = int(width / tile) + rows = int(height / tile) + half_cols = cols / 2 + half_rows = rows / 2 + [self.cinema_widgets.append(bui.buttonwidget( + parent=self.parent, + size=(tile, tile), + position=(i * tile, j * tile), + texture=bui.gettexture('empty'), enable_sound=False, - on_activate_call=Call(s._look, i-ha, j-hb), + on_activate_call=CallPartial(self.update_look, i - half_cols, j - half_rows), label='', repeat=True )) - for i in range(a) - for j in range(b)] + for i in range(cols) + for j in range(rows)] - def mksnui(s): - f = s.snuikids.append - f(iw( - parent=s.p, + def create_cinema_ui(self): + widgets = self.cinema_ui_widgets.append + widgets(bui.imagewidget( + parent=self.parent, position=(0, 3), - color=s.COL14, + color=self.COL14, opacity=0.4, - texture=gt('white'), + texture=bui.gettexture('white'), size=(232, 190) )) - # buttons - f(s.bw( - p=s.p, + + widgets(self.create_button( + p=self.parent, pos=(14, 50), size=(204, 30), label='Target Players', - color=s.COL14, - textcolor=s.COL15, - oac=s.sntar + color=self.COL14, + textcolor=self.COL15, + oac=self.target_players )) - f(s.bw( - p=s.p, + widgets(self.create_button( + p=self.parent, pos=(10, 90), - color=s.COL14, + color=self.COL14, label='Cancel', size=(99, 30), - textcolor=s.COL15, - oac=s.sncancel + textcolor=self.COL15, + oac=self.cancel_cinema )) - f(s.bw( - p=s.p, + widgets(self.create_button( + p=self.parent, pos=(123, 90), - color=s.COL14, + color=self.COL14, label='Save', size=(99, 30), - textcolor=s.COL15, - oac=s.snsave + textcolor=self.COL15, + oac=self.save_cinema )) - # info - f(otw( - parent=s.p, + + widgets(bui.textwidget( + parent=self.parent, position=(90, 160), - color=s.COL15, + color=self.COL15, text='Currently looking at:', h_align='center', maxwidth=220 )) - s.snt = otw( - parent=s.p, + self.cinema_text = bui.textwidget( + parent=self.parent, position=(90, 130), - color=s.COL14, + color=self.COL14, h_align='center', - text=str(RND(s.caml) if s.caml else 'players') + text=str(round_tuple(self.camera_look) if self.camera_look else 'players') ) - f(s.snt) - # tip - f(iw( - parent=s.p, + widgets(self.cinema_text) + + widgets(bui.imagewidget( + parent=self.parent, position=(0, 200), - color=s.COL14, + color=self.COL14, opacity=0.4, - texture=gt('white'), + texture=bui.gettexture('white'), size=(232, 110) )) - f(otw( - parent=s.p, + widgets(bui.textwidget( + parent=self.parent, position=(90, 240), text='Longpress anywhere\nto look around. Tap on \nsomething to look at it.\nPause for calmer control!', h_align='center', @@ -879,455 +1029,474 @@ class Player: maxwidth=225, max_height=105 )) - # crosshair - x, y = res() - h = 20 - [f(iw( - parent=s.p, - position=(x/2, y/2-h/2+h*0.1) if _ else (x/2-h/2, y/2+h*0.1), - size=(3, h*1.15) if _ else (h*1.15, 3), - color=s.COL1, - texture=gt('white') - )) for _ in [0, 1]] - # top - k = 60 + + width, height = bui.get_virtual_screen_size() + crosshair = 20 + [widgets(bui.imagewidget( + parent=self.parent, + position=(width / 2, height / 2 - crosshair / 2 + crosshair * 0.1) if i else (width / 2 - crosshair / 2, height / 2 + crosshair * 0.1), + size=(3, crosshair * 1.15) if i else (crosshair * 1.15, 3), + color=self.COL1, + texture=bui.gettexture('white') + )) for i in [0, 1]] + + offset = 60 for j in range(2): - f(iw( - parent=s.p, - texture=gt('white'), - color=s.COL1, - position=(x/2+[-k, k-h][j], y/2+k), - size=(h*1.1, 3) - )) - # right - for j in range(2): - f(iw( - parent=s.p, - texture=gt('white'), - color=s.COL1, - position=(x/2+k, y/2+[k-h, -k+h*0.3][j]), - size=(3, h*+1.1) - )) - # bottom - for j in range(2): - f(iw( - parent=s.p, - texture=gt('white'), - color=s.COL1, - position=(x/2+[-k, k-h][j], y/2-h/2-k+h*0.8), - size=(h*1.1, 3) - )) - # left - for j in range(2): - f(iw( - parent=s.p, - texture=gt('white'), - color=s.COL1, - position=(x/2-k, y/2+[k-h, -k+h*0.3][j]), - size=(3, h*1.1) + widgets(bui.imagewidget( + parent=self.parent, + texture=bui.gettexture('white'), + color=self.COL1, + position=(width / 2 + [-offset, offset - crosshair][j], height / 2 + offset), + size=(crosshair * 1.1, 3) )) - def killsnui(s): - [_.delete() for _ in s.snuikids] + for j in range(2): + widgets(bui.imagewidget( + parent=self.parent, + texture=bui.gettexture('white'), + color=self.COL1, + position=(width / 2 + offset, height / 2 + [offset - crosshair, -offset + crosshair * 0.3][j]), + size=(3, crosshair * +1.1) + )) - def snhide(s): - if getattr(s, 'snbusy', 0): + for j in range(2): + widgets(bui.imagewidget( + parent=self.parent, + texture=bui.gettexture('white'), + color=self.COL1, + position=(width / 2 + [-offset, offset - crosshair][j], height / 2 - crosshair / 2 - offset + crosshair * 0.8), + size=(crosshair * 1.1, 3) + )) + + for j in range(2): + widgets(bui.imagewidget( + parent=self.parent, + texture=bui.gettexture('white'), + color=self.COL1, + position=(width / 2 - offset, height / 2 + [offset - crosshair, -offset + crosshair * 0.3][j]), + size=(3, crosshair * 1.1) + )) + + def destroy_cinema_ui(self): + [w.delete() for w in self.cinema_ui_widgets] + + def toggle_cinema_hide(self): + if getattr(self, 'cinema_busy', 0): return - s.snma = not s.snma - s.snbusy = True - if s.snma: - s.snanim(204, 14, -1) - obw(s.snbtn, label=cs(sc.UP_ARROW)) - s.killsnui() + self.cinema_mode = not self.cinema_mode + self.cinema_busy = True + if self.cinema_mode: + self.animate_cinema(204, 14, -1) + bui.buttonwidget(self.cinema_button, label=bui.charstr(bui.SpecialChar.UP_ARROW)) + self.destroy_cinema_ui() else: - obw(s.snbtn, texture=gt('white')) - s.snanim(36, 7, 1) - iw(s.sni, opacity=0) + bui.buttonwidget(self.cinema_button, texture=bui.gettexture('white')) + self.animate_cinema(36, 7, 1) + bui.imagewidget(self.cinema_indicator, opacity=0) - def mksni(s): - s.sni = iw( - parent=s.p, + def create_cinema_indicator(self): + self.cinema_indicator = bui.imagewidget( + parent=self.parent, position=(7, 8), - color=s.COL14, + color=self.COL14, opacity=0, size=(36, 33), - texture=gt('white') + texture=bui.gettexture('white') ) - s.snkids.append(s.sni) + self.cinema_widgets.append(self.cinema_indicator) - def mksnb(s): - s.snbtn = s.bw( - p=s.p, + def create_cinema_button(self): + self.cinema_button = self.create_button( + p=self.parent, pos=(14, 10), - color=s.COL14, + color=self.COL14, label='Cinema Mode', size=(204, 30), - textcolor=s.COL15, - oac=s.snhide + textcolor=self.COL15, + oac=self.toggle_cinema_hide ) - s.snkids.append(s.snbtn) + self.cinema_widgets.append(self.cinema_button) - def snanim(s, a, b, i): - a += (163/35)*i - b += 0.2*i - obw(s.snbtn, size=(a, 30), position=(b, 10)) - if not (14 >= b >= 7): - s.snbusy = False - if s.snma: - obw(s.snbtn, texture=gt('empty')) - iw(s.sni, opacity=0.4) + def animate_cinema(self, width_val, x_val, direction): + width_val += (163 / 35) * direction + x_val += 0.2 * direction + bui.buttonwidget(self.cinema_button, size=(width_val, 30), position=(x_val, 10)) + if not (14 >= x_val >= 7): + self.cinema_busy = False + if self.cinema_mode: + bui.buttonwidget(self.cinema_button, texture=bui.gettexture('empty')) + bui.imagewidget(self.cinema_indicator, opacity=0.4) else: - s.mksnui() - s.snbtn.delete() - s.mksnb() - obw(s.snbtn, label='Cinema Mode') + self.create_cinema_ui() + self.cinema_button.delete() + self.create_cinema_button() + bui.buttonwidget(self.cinema_button, label='Cinema Mode') return - teck(0.004, Call(s.snanim, a, b, i)) + bui.apptimer(0.004, CallPartial(self.animate_cinema, width_val, x_val, direction)) - def killsn(s): - s.killsnui() - [_.delete() for _ in s.snkids] + def destroy_cinema(self): + self.destroy_cinema_ui() + [w.delete() for w in self.cinema_widgets] - def all(s): - return s.trash()+s.hdkids+s.snkids + def get_all_widgets(self): + return self.get_deletable_widgets() + self.hide_widgets + self.cinema_widgets - def sntar(s): - s.caml = None - otw(s.snt, text='players') - if s.camz != 1 or s.gay: - s.camz = 1 - s.gay = False - SCM(False) + def target_players(self): + self.camera_look = None + bui.textwidget(self.cinema_text, text='players') + if self.camera_zoom != 1 or self.manual_zoom: + self.camera_zoom = 1 + self.manual_zoom = False + _ba.set_camera_manual(False) - def sncancel(s): - s.caml = s.camlo - s.camp = s.campo - s.gay = s.gayo - if s.camz != 1 or s.gay: - SCM(True) - SCP(*s.camp) - s.snbye() + def cancel_cinema(self): + self.camera_look = self.look_backup + self.camera_pos = self.pos_backup + self.manual_zoom = self.manual_backup + if self.camera_zoom != 1 or self.manual_zoom: + _ba.set_camera_manual(True) + _ba.set_camera_position(*self.camera_pos) + self.exit_cinema() - def cam(s): - if s.camon: - s.camon = False - [_.delete() for _ in s.camkids] - s.camkids.clear() - s.fcam(0.4, -0.1) + def toggle_camera(self): + if self.camera_on: + self.camera_on = False + [w.delete() for w in self.camera_widgets] + self.camera_widgets.clear() + self.fade_camera(0.4, -0.1) else: - s.camon = True - s.mkcamui() + self.camera_on = True + self.create_camera_ui() - def camr(s): - SCM(False) - s.caml = None - s.gay = False - s.camz = 1 - otw(s.ltw, text='players') - otw(s.ztw, text='auto') + def reset_camera(self): + _ba.set_camera_manual(False) + self.camera_look = None + self.manual_zoom = False + self.camera_zoom = 1 + bui.textwidget(self.look_text, text='players') + bui.textwidget(self.zoom_text, text='auto') - def fcam(s, i=0, a=0.1): - if i > 0.4 or i < 0: - if a < 0: - s.cambg.delete() + def fade_camera(self, opacity=0, delta=0.1): + if opacity > 0.4 or opacity < 0: + if delta < 0: + self.camera_bg.delete() return - if not s.cambg.exists(): + if not self.camera_bg.exists(): return - iw(s.cambg, opacity=i) - iw(s.cambg2, opacity=i) - teck(0.02, Call(s.fcam, i+a, a)) + bui.imagewidget(self.camera_bg, opacity=opacity) + bui.imagewidget(self.camera_bg2, opacity=opacity) + bui.apptimer(0.02, CallPartial(self.fade_camera, opacity + delta, delta)) - def rloop(s): - s.loop() - s.fixps() - s.hm('Replay', f'Version {Replay.VER} BETA', s.COL12, s.COL13) + def restart(self): + self.loop() + self.fix_pause() + self.show_message('Replay', f'Version {Replay.VER} BETA', self.COL12, self.COL13) - def killui(s): - s.up = s.camon = False - [_.delete() for _ in s.trash()] - s.kids.clear() - s.camkids.clear() + def destroy_ui(self): + self.ui_visible = self.camera_on = False + [w.delete() for w in self.get_deletable_widgets()] + self.ui_widgets.clear() + self.camera_widgets.clear() - def trash(s): - return s.kids+s.camkids + def get_deletable_widgets(self): + return self.ui_widgets + self.camera_widgets - def kek(s): - if getattr(s, 'kekbusy', 0): + def toggle_hide(self): + if getattr(self, 'hide_busy', 0): return - s.kekbusy = True - if getattr(s, 'tbye', 0) and getattr(s, 'frbro', 0): - s.frbro = s.tbye = False - s.okt = None - s.nah = b = not s.nah - otw(s.kekt, text=s.tex[b]) - if b: - teck(0.2, lambda: obw(s.kekb, texture=gt('empty'))) - s.fkek(0.4, -0.05) - s.killui() + self.hide_busy = True + if getattr(self, 'exit_timer', 0) and getattr(self, 'exit_ready', 0): + self.exit_ready = self.exit_timer = False + self.info_timer = None + self.ui_hidden = hidden = not self.ui_hidden + bui.textwidget(self.hide_text, text=self.hide_icons[hidden]) + if hidden: + bui.apptimer(0.2, lambda: bui.buttonwidget(self.hide_button, texture=bui.gettexture('empty'))) + self.fade_hide_button(0.4, -0.05) + self.destroy_ui() else: - obw(s.kekb, texture=gt('white')) - s.fkek(0, 0.05) - s.mkui() - s.pro() - teck(0.21, Call(setattr, s, 'kekbusy', 0)) + bui.buttonwidget(self.hide_button, texture=bui.gettexture('white')) + self.fade_hide_button(0, 0.05) + self.create_ui() + self.update_progress_ui() + bui.apptimer(0.21, CallPartial(setattr, self, 'hide_busy', 0)) - def fkek(s, i=0, a=0.1): - if i > 0.4 or i < 0: + def fade_hide_button(self, opacity=0, delta=0.1): + if opacity > 0.4 or opacity < 0: return - if not s.bg.exists(): + if not self.background.exists(): return - iw(s.bg, opacity=i) - teck(0.02, Call(s.fkek, i+a, a)) + bui.imagewidget(self.background, opacity=opacity) + bui.apptimer(0.02, CallPartial(self.fade_hide_button, opacity + delta, delta)) - def hm(s, t1, t2, c1, c2): - if getattr(s, 'tbye', 0) and getattr(s, 'frbro', 0): - s.frbro = s.tbye = False - s.okt = None - iw(s.ok, color=c1) - otw(s.ok2, text=t1, color=c2) - otw(s.ok3, text=t2, color=c2) - s.fok() - s.okt = tock(1.5, s.unhm) + def show_message(self, title, text, color1, color2): + if getattr(self, 'exit_timer', 0) and getattr(self, 'exit_ready', 0): + self.exit_ready = self.exit_timer = False + self.info_timer = None + bui.imagewidget(self.info_bg, color=color1) + bui.textwidget(self.info_title, text=title, color=color2) + bui.textwidget(self.info_text, text=text, color=color2) + self.fade_info() + self.info_timer = bui.AppTimer(1.5, self.hide_message) - def unhm(s): - s.fok(0.7, -0.1) - [otw(_, text='') for _ in [s.ok2, s.ok3] if _.exists()] + def hide_message(self): + self.fade_info(0.7, -0.1) + [bui.textwidget(w, text='') for w in [self.info_title, self.info_text] if w.exists()] - def fok(s, i=0, a=0.1): - if i > 0.7 or i < 0: + def fade_info(self, opacity=0, delta=0.1): + if opacity > 0.7 or opacity < 0: return - if not s.ok.exists(): + if not self.info_bg.exists(): return - iw(s.ok, opacity=i) - teck(0.02, Call(s.fok, i+a, a)) + bui.imagewidget(self.info_bg, opacity=opacity) + bui.apptimer(0.02, CallPartial(self.fade_info, opacity + delta, delta)) - def toggle(s, dry=False, shut=False): + def toggle_pause(self, dry=False, silent=False): if not dry: - s.ps = not s.ps - t = cs(getattr(sc, ['PAUSE', 'PLAY'][s.ps]+'_BUTTON')) - otw(s.tt, text=t) + self.paused = not self.paused + icon = bui.charstr(getattr(bui.SpecialChar, ['PAUSE', 'PLAY'][self.paused] + '_BUTTON')) + bui.textwidget(self.pause_text, text=icon) if not dry: - if not shut: - s.hm(['Resume', 'Pause'][s.ps], basename(s.path) + - f' of {getsize(s.path)} bytes', s.COL6, s.COL7) - if s.ps: - s.stop() - PAUSE() + if not silent: + self.show_message(['Resume', 'Pause'][self.paused], os.path.basename(self.path) + f' of {os.path.getsize(self.path)} bytes', self.COL6, self.COL7) + if self.paused: + self.stop() + bs.pause_replay() else: - s.play() - RESUME() + self.play() + bs.resume_replay() - def fixps(s): - if not s.ps: + def fix_pause(self): + if not self.paused: return - s.toggle(shut=True) - teck(0.02, Call(s.toggle, shut=True)) + self.toggle_pause(silent=True) + bui.apptimer(0.02, CallPartial(self.toggle_pause, silent=True)) - def clock(s): - t = time() - r = t - s.rt - s.rt = t - s.rn += r * s.sp + def update_clock(self): + current = time() + elapsed = current - self.real_time + self.real_time = current + self.replay_time += elapsed * self.speed - def boost(s, i): - n = GET()+i - SET(n) - s.sp = 2**n - h = 'Snail Mode' if s.sp == 0.0625 else 'Slow Motion' if s.sp < 1 else 'Quake Pro' if s.sp == 16 else 'Fast Motion' if s.sp > 1 else 'Normal Speed' - s.hm(h, f'Current exponent: x{s.sp}', s.COL2, s.COL3) + def change_speed(self, direction): + new_exp = bs.get_replay_speed_exponent() + direction + bs.set_replay_speed_exponent(new_exp) + self.speed = 2 ** new_exp + label = 'Snail Mode' if self.speed == 0.0625 else 'Slow Motion' if self.speed < 1 else 'Quake Pro' if self.speed == 16 else 'Fast Motion' if self.speed > 1 else 'Normal Speed' + self.show_message(label, f'Current exponent: x{self.speed}', self.COL2, self.COL3) - def play(s): - s.rt = time() - s.clock() - s.ptplay() - s.clt = tock(s.TICK, s.clock, repeat=True) + def play(self): + self.real_time = time() + self.update_clock() + self.start_progress_timer() + self.clock_timer = bui.AppTimer(self.TICK, self.update_clock, repeat=True) - def stop(s): - s.clt = None - s.ptkill() + def stop(self): + self.clock_timer = None + self.stop_progress_timer() - def ptkill(s): - s.pt = None + def stop_progress_timer(self): + self.progress_timer = None - def ptplay(s): - s.pt = tock(s.TICK, s.pro, repeat=True) + def start_progress_timer(self): + self.progress_timer = bui.AppTimer(self.TICK, self.update_progress_ui, repeat=True) - def seek(s, i): - h = ['Forward by', 'Rewind by'][i == -1] - i = i * s.sp - i = (s.ds/20)*i - t = (s.rn-s.st)+i - if (t >= s.ds) or (t <= 0): - s.loop() + def seek(self, direction): + """Seek with confirmed duration checking""" + label = ['Forward by', 'Rewind by'][direction == -1] + amount = direction * self.speed + + # Use estimated duration for calculating seek amount + if self.scanning: + seek_base = self.estimated_duration else: - s.st = s.rn-t - s.replay() - SEEK(t) - s.rt = time() - s.fixps() - i = abs(round(i, 2)) - s.hm('Seek', h+f" {i} second{['s', ''][i == 1]}", s.COL4, s.COL5) - - def jump(s, p): - t = s.ds * p - s.st = s.rn-t - s.replay() - SEEK(t) - s.rt = time() - s.fixps() - - def bye(s): - if getattr(s, 'frbro', 0): - s._bye() + seek_base = self.duration_sec + + amount = (seek_base / 20) * amount + new_time = (self.replay_time - self.start_time) + amount + + # Check if trying to seek beyond confirmed duration while scanning + if self.scanning and new_time > self.confirmed_duration_sec: + current, total = self.scan_progress + percent = int((current / total) * 100) if total > 0 else 0 + self.show_message('Buffering...', f'{percent}% loaded', self.COL2, self.COL3) return - s.hm('Exit', 'Press again to confirm', s.COL0, s.COL1) - s.frbro = True - s.tbye = tock(1.5, Call(setattr, s, 'frbro', False)) + + # Use appropriate duration for loop check + max_duration = self.confirmed_duration_sec if self.scanning else self.duration_sec + + if (new_time >= max_duration) or (new_time <= 0): + self.loop() + else: + self.start_time = self.replay_time - new_time + self.reset_replay() + bs.seek_replay(new_time) + + self.real_time = time() + self.fix_pause() + amount = abs(round(amount, 2)) + self.show_message('Seek', label + f" {amount} second{['s', ''][amount == 1]}", self.COL4, self.COL5) + + def jump(self, percent): + """Jump with WYSIWYG behavior - percent is based on ESTIMATED duration, checked against CONFIRMED""" + # Calculate target time based on ESTIMATED duration (what user sees on bar) + if self.scanning: + target_time = self.estimated_duration * percent + else: + target_time = self.duration_sec * percent + + # Check if trying to jump beyond CONFIRMED duration while scanning + if self.scanning and target_time > self.confirmed_duration_sec: + current, total = self.scan_progress + scan_percent = int((current / total) * 100) if total > 0 else 0 + self.show_message('Buffering...', f'{scan_percent}% loaded', self.COL2, self.COL3) + return + + self.start_time = self.replay_time - target_time + self.reset_replay() + bs.seek_replay(target_time) + self.real_time = time() + self.fix_pause() - def _bye(s): - fade(0, time=0.75, endcall=Call(fade, 1, time=0.75)) - gs('deek').play() - BYE() - s.stop() - s.fockill() - s.tbye = None - SCM(False) + def exit(self): + if getattr(self, 'exit_ready', 0): + self.confirm_exit() + return + self.show_message('Exit', 'Press again to confirm', self.COL0, self.COL1) + self.exit_ready = True + self.exit_timer = bui.AppTimer(1.5, CallPartial(setattr, self, 'exit_ready', False)) - def pro(s): - t = s.rn-s.st - if s.rn-s.st >= s.ds: - s.loop() - x, y = s.nbp - p = (t/s.ds)*s.px + def confirm_exit(self): + bui.fade_screen(0, time=0.75, endcall=CallPartial(bui.fade_screen, 1, time=0.75)) + bui.getsound('deek').play() + return_to_menu() + self.stop() + self.stop_focus() + self.exit_timer = None + _ba.set_camera_manual(False) + + def update_progress_ui(self): + """Modified to handle scanning state with growing progress bar""" + elapsed = self.replay_time - self.start_time + + # Check for loop only if we know the duration + if not self.scanning and self.duration_sec and elapsed >= self.duration_sec: + self.loop() + + nub_x, nub_y = self.nub_pos + + # Calculate progress + if self.scanning: + # Use estimated duration for progress bar + # Progress bar "grows" as estimate increases + if elapsed < self.estimated_duration: + progress = (elapsed / self.estimated_duration) * self.progress_width + else: + # If elapsed exceeds estimate, show at 95% until scan completes + progress = self.progress_width * 0.95 + elif not self.duration_sec: + # Fallback if scan failed + progress = min(elapsed / max(elapsed, 1) * self.progress_width * 0.1, self.progress_width * 0.1) + else: + # Normal progress calculation + progress = (elapsed / self.duration_sec) * self.progress_width + try: - iw(s.nb, position=(x+p, y)) - otw(s.ct, text=FOR(t)) + bui.imagewidget(self.nub, position=(nub_x + progress, nub_y)) + bui.textwidget(self.current_time_text, text=format_time(elapsed)) except ReferenceError: pass - def replay(s): - SEEK(-10**10) + def reset_replay(self): + bs.seek_replay(-10 ** 10) - def loop(s): - s.st = s.rn = 0 - s.replay() - bw = lambda s, label='', p=None, oac=None, pos=None, texture='white', **k: obw( - parent=p, - on_activate_call=oac, - position=pos, - label=label, - texture=gt(texture), - enable_sound=False, - **k - ) + def loop(self): + self.start_time = self.replay_time = 0 + self.reset_replay() + def create_button(self, label='', p=None, oac=None, pos=None, texture='white', **kwargs): + return bui.buttonwidget( + parent=p, + on_activate_call=oac, + position=pos, + label=label, + texture=bui.gettexture(texture), + enable_sound=False, + **kwargs + ) # Tools -def BYE(): return app.classic.return_to_main_menu_session_gracefully(reset_ui=False) - - -def BTW(t): return (gs('block').play() or 1) and push(t, color=(1, 1, 0)) -def GOS(): return gsw('overlay_stack') - - -def FOR(t): return strftime('%H:%M:%S', gmtime(t)) -def SCL(a, b, c=None): return ((s := app.ui_v1.uiscale), - a if s is UIS.SMALL else b if s is UIS.MEDIUM else (c or b))[1] - - -def RND(t): return type(t)([round(_, 1) for _ in t]) +def get_ui_scale(small, medium, large=None): + scale = bui.app.ui_v1.uiscale + return small if scale is bui.UIScale.SMALL else medium if scale is bui.UIScale.MEDIUM else (large or medium) +return_to_menu = lambda: bui.app.classic.return_to_main_menu_session_gracefully(reset_ui=False) +show_warning = lambda text: (bui.getsound('block').play() or 1) and bui.screenmessage(text, color=(1, 1, 0)) +format_time = lambda seconds: strftime('%H:%M:%S', gmtime(seconds)) +get_overlay_stack = lambda: bui.get_special_widget('overlay_stack') +round_tuple = lambda tup: type(tup)([round(val, 1) for val in tup]) # pybrp - - -def Z(_): return [0]*_ - - -def G_FREQS(): return [ - 101342, 9667, 3497, 1072, 0, 3793, *Z(2), 2815, 5235, *Z(3), 3570, *Z(3), - 1383, *Z(3), 2970, *Z(2), 2857, *Z(8), 1199, *Z(30), - 1494, 1974, *Z(12), 1351, *Z(122), 1475, *Z(65) +Z = lambda _:[0]*_ +G_FREQS = lambda: [ + 101342,9667,3497,1072,0,3793,*Z(2),2815,5235,*Z(3),3570,*Z(3), + 1383,*Z(3),2970,*Z(2),2857,*Z(8),1199,*Z(30), + 1494,1974,*Z(12),1351,*Z(122),1475,*Z(65) ] - - -def CMD_NAMES(): return {0: 'BaseTimeStep', 1: 'StepSceneGraph', 2: 'AddSceneGraph', 3: 'RemoveSceneGraph', 4: 'AddNode', 5: 'NodeOnCreate', 6: 'SetForegroundScene', 7: 'RemoveNode', 8: 'AddMaterial', 9: 'RemoveMaterial', 10: 'AddMaterialComponent', 11: 'AddTexture', 12: 'RemoveTexture', 13: 'AddMesh', 14: 'RemoveMesh', 15: 'AddSound', 16: 'RemoveSound', 17: 'AddCollisionMesh', 18: 'RemoveCollisionMesh', 19: 'ConnectNodeAttribute', 20: 'NodeMessage', 21: 'SetNodeAttrFloat', 22: 'SetNodeAttrInt32', 23: 'SetNodeAttrBool', 24: 'SetNodeAttrFloats', 25: 'SetNodeAttrInt32s', 26: 'SetNodeAttrString', 27: 'SetNodeAttrNode', 28: 'SetNodeAttrNodeNull', - 29: 'SetNodeAttrNodes', 30: 'SetNodeAttrPlayer', 31: 'SetNodeAttrPlayerNull', 32: 'SetNodeAttrMaterials', 33: 'SetNodeAttrTexture', 34: 'SetNodeAttrTextureNull', 35: 'SetNodeAttrTextures', 36: 'SetNodeAttrSound', 37: 'SetNodeAttrSoundNull', 38: 'SetNodeAttrSounds', 39: 'SetNodeAttrMesh', 40: 'SetNodeAttrMeshNull', 41: 'SetNodeAttrMeshes', 42: 'SetNodeAttrCollisionMesh', 43: 'SetNodeAttrCollisionMeshNull', 44: 'SetNodeAttrCollisionMeshes', 45: 'PlaySoundAtPosition', 46: 'PlaySound', 47: 'EmitBGDynamics', 48: 'EndOfFile', 49: 'DynamicsCorrection', 50: 'ScreenMessageBottom', 51: 'ScreenMessageTop', 52: 'AddData', 53: 'RemoveData', 54: 'CameraShake'} - - -class _H: +class _Huffman: class _N: def __init__(self): - self.l, self.r, self.p, self.f = -1, -1, 0, 0 - + self.l,self.r,self.p,self.f=-1,-1,0,0 def __init__(self): - gf, self.nodes = G_FREQS(), [self._N()for _ in range(511)] - for i in range(256): - self.nodes[i].f = gf[i] - nc = 256 - while nc < 511: - s1, s2 = -1, -1 - i = 0 - while self.nodes[i].p != 0: - i += 1 - s1 = i - i += 1 - while self.nodes[i].p != 0: - i += 1 - s2 = i - i += 1 - while i < nc: - if self.nodes[i].p == 0: - if self.nodes[s1].f > self.nodes[s2].f: - if self.nodes[i].f < self.nodes[s1].f: - s1 = i - elif self.nodes[i].f < self.nodes[s2].f: - s2 = i - i += 1 - self.nodes[nc].f = self.nodes[s1].f+self.nodes[s2].f - self.nodes[s1].p = self.nodes[s2].p = nc-255 - self.nodes[nc].r, self.nodes[nc].l = s1, s2 - nc += 1 - - def decompress(self, src): - if not src: - return b'' - rem, comp = src[0] & 15, src[0] >> 7 - if not comp: - return src - out, ptr, l = bytearray(), src[1:], len(src) - bl = ((l-1)*8)-rem - bit = 0 - while bit < bl: - m_bit = (ptr[bit >> 3] >> (bit & 7)) & 1 - bit += 1 + self.nodes=[self._N()for _ in range(511)] + gf = G_FREQS() + for i in range(256):self.nodes[i].f=gf[i] + nc=256 + while nc<511: + s1,s2=-1,-1 + i=0 + while self.nodes[i].p!=0:i+=1 + s1=i;i+=1 + while self.nodes[i].p!=0:i+=1 + s2=i;i+=1 + while iself.nodes[s2].f: + if self.nodes[i].f>7 + if not comp:return src + out,ptr,l=bytearray(),src[1:],len(src) + bl=((l-1)*8)-rem;bit=0 + while bit>3]>>(bit&7))&1;bit+=1 if m_bit: - n = 510 - while n >= 256: - if bit >= bl: - raise ValueError("Incomplete Huffman code") - p_bit = (ptr[bit >> 3] >> (bit & 7)) & 1 - bit += 1 - n = self.nodes[n].l if p_bit == 0 else self.nodes[n].r + n=510 + while n>=256: + if bit>=bl:raise ValueError("Incomplete Huffman code") + p_bit=(ptr[bit>>3]>>(bit&7))&1;bit+=1 + n=self.nodes[n].l if p_bit==0 else self.nodes[n].r out.append(n) else: - if bit+8 > bl: - break - bi, b_in_b = bit >> 3, bit & 7 - val = ptr[bi]if b_in_b == 0 else (ptr[bi] >> b_in_b) | (ptr[bi+1] << (8-b_in_b)) - out.append(val & 255) - bit += 8 + if bit+8>bl:break + bi,b_in_b=bit>>3,bit&7 + val=ptr[bi]if b_in_b==0 else(ptr[bi]>>b_in_b)|(ptr[bi+1]<<(8-b_in_b)) + out.append(val&255);bit+=8 return bytes(out) - - -def GMS(_h, brp_path, par): +def get_replay_duration(_h, brp_path, progress): total_ms = 0 with open(brp_path, 'rb') as f: - f.seek(0, 2) - par[1] = f.tell() + f.seek(0,2) + progress[1] = f.tell() f.seek(6) while True: - if par: - par[0] = f.tell() + progress[0] = f.tell() b_data = f.read(1) if not b_data: break @@ -1336,7 +1505,7 @@ def GMS(_h, brp_path, par): comp_len = b1 elif b1 == 254: comp_len = int.from_bytes(f.read(2), 'little') - else: # 255 + else: comp_len = int.from_bytes(f.read(4), 'little') if comp_len == 0: continue @@ -1344,48 +1513,47 @@ def GMS(_h, brp_path, par): if not raw_msg or raw_msg[0] != 1: continue sub_off = 1 - while sub_off < len(raw_msg): - try: - sub_size = int.from_bytes(raw_msg[sub_off:sub_off+2], 'little') - except IndexError: + while sub_off + 2 <= len(raw_msg): + sub_size_bytes = raw_msg[sub_off:sub_off+2] + if len(sub_size_bytes) < 2: break - except ValueError: + sub_size = int.from_bytes(sub_size_bytes, 'little') + sub_off += 2 + if sub_off + sub_size > len(raw_msg): break - sub_data = raw_msg[sub_off+2:sub_off+2+sub_size] - if sub_data and sub_data[0] == 0: + sub_data = raw_msg[sub_off:sub_off+sub_size] + if len(sub_data) >= 2 and sub_data[0] == 0: total_ms += sub_data[1] - sub_off += 2 + sub_size - if par: - par[0] = par[1] + sub_off += sub_size + progress[0] = progress[1] return total_ms # brobord collide grass # ba_meta require api 9 # ba_meta export babase.Plugin - - class byBordd(Plugin): - def __init__(s): - from bauiv1lib.ingamemenu import InGameMenuWindow as m - a = '_refresh_in_game' - o = getattr(m, a) - setattr(m, a, lambda v, *a, **k: (s.mk(v), o(v, *a, **k))[1]) - from bauiv1lib.watch import WatchWindow as n - b = '__init__' - p = getattr(n, b) - setattr(n, b, lambda v, *a, **k: (p(v, *a, **k), s.mk(v, 1))[0]) + has_settings_ui = lambda c=0: True + show_settings_ui = lambda c=0, w=None: Replay(source=w) + def __init__(self): + from bauiv1lib.ingamemenu import InGameMenuWindow as ingame + orig_refresh = getattr(ingame, '_refresh_in_game') + setattr(ingame, '_refresh_in_game', lambda window, *args, **kwargs: (self.add_button(window), orig_refresh(window, *args, **kwargs))[1]) + from bauiv1lib.watch import WatchWindow as watch + orig_init = getattr(watch, '__init__') + setattr(watch, '__init__', lambda window, *args, **kwargs: (orig_init(window, *args, **kwargs), self.add_button(window, 1))[0]) - def mk(s, v, i=0): - if i: - x = v._width/2+SCL(v._scroll_width*-0.5+93, 0)+100 - y = v.yoffs-SCL(63, 10)-25 - s.b = Replay.bw( + def add_button(self, window, watch_mode=0): + if watch_mode: + btn_x = window._width / 2 + get_ui_scale(window._scroll_width * -0.5 + 93, 0) + 100 + btn_y = window.yoffs - get_ui_scale(63, 10) - 25 + self.button = Replay.create_button( Replay, - p=v._root_widget, + p=window._root_widget, label='Replay', - pos=(x, y) if i else (-70, 0), - icon=gt('replayIcon'), - iconscale=1.6 if i else 0.8, - size=(140, 50) if i else (90, 35), - oac=lambda: Replay(source=s.b) + pos=(btn_x, btn_y) if watch_mode else (-70, 0), + icon=bui.gettexture('replayIcon'), + iconscale=1.6 if watch_mode else 0.8, + size=(140, 50) if watch_mode else (90, 35), + oac=lambda: Replay(source=self.button), + id='Replay' ) diff --git a/plugins/utilities/topmsg.py b/plugins/utilities/topmsg.py old mode 100755 new mode 100644 From d25a14bd2e11f2aa31a482cf2173f86ca5c1b076 Mon Sep 17 00:00:00 2001 From: BroBordd Date: Thu, 6 Nov 2025 16:39:37 +0200 Subject: [PATCH 2/4] bugfix --- plugins/utilities/replay.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/plugins/utilities/replay.py b/plugins/utilities/replay.py index 9327962..b440d8c 100644 --- a/plugins/utilities/replay.py +++ b/plugins/utilities/replay.py @@ -1379,27 +1379,35 @@ class Player: """Modified to handle scanning state with growing progress bar""" elapsed = self.replay_time - self.start_time - # Check for loop only if we know the duration - if not self.scanning and self.duration_sec and elapsed >= self.duration_sec: + # Determine the current max duration to check against + if self.scanning: + max_duration = self.estimated_duration + elif self.duration_sec: + max_duration = self.duration_sec + else: + max_duration = None + + # Check if elapsed exceeds duration and loop if needed + if max_duration and elapsed >= max_duration: self.loop() + elapsed = 0 # Reset elapsed after loop nub_x, nub_y = self.nub_pos # Calculate progress if self.scanning: # Use estimated duration for progress bar - # Progress bar "grows" as estimate increases if elapsed < self.estimated_duration: progress = (elapsed / self.estimated_duration) * self.progress_width else: - # If elapsed exceeds estimate, show at 95% until scan completes - progress = self.progress_width * 0.95 + # Cap at 100% if somehow elapsed exceeds estimate + progress = self.progress_width elif not self.duration_sec: # Fallback if scan failed progress = min(elapsed / max(elapsed, 1) * self.progress_width * 0.1, self.progress_width * 0.1) else: - # Normal progress calculation - progress = (elapsed / self.duration_sec) * self.progress_width + # Normal progress calculation, cap at 100% + progress = min((elapsed / self.duration_sec) * self.progress_width, self.progress_width) try: bui.imagewidget(self.nub, position=(nub_x + progress, nub_y)) From fd4bcc1f72b251eedac694cd4b05ae0c69d17cf8 Mon Sep 17 00:00:00 2001 From: BroBordd <192699453+BroBordd@users.noreply.github.com> Date: Thu, 6 Nov 2025 14:40:23 +0000 Subject: [PATCH 3/4] [ci] auto-format --- plugins/utilities/replay.py | 265 +++++++++++++++++++++--------------- 1 file changed, 158 insertions(+), 107 deletions(-) diff --git a/plugins/utilities/replay.py b/plugins/utilities/replay.py index b440d8c..106ec95 100644 --- a/plugins/utilities/replay.py +++ b/plugins/utilities/replay.py @@ -33,6 +33,7 @@ from random import uniform from threading import Thread from struct import unpack + class Replay: VER = '3.0' COL1 = (0.18, 0.18, 0.18) @@ -56,7 +57,8 @@ class Replay: p=get_overlay_stack(), size=(400, 500), oac=lambda: ( - bui.containerwidget(self.parent, transition='out_scale' if source and source.exists() else 'out_left'), + bui.containerwidget( + self.parent, transition='out_scale' if source and source.exists() else 'out_left'), self.play_sound('laser'), self.transition_sound.stop() ) @@ -153,11 +155,11 @@ class Replay: return self.is_busy(True) bui.getsound('deek').play() - + # Start replay immediately bs.set_replay_speed_exponent(0) bui.fade_screen(1) - + # Create player with unknown duration Player(path=self.get_replay_path(), duration=None) self.is_busy(False) @@ -228,10 +230,12 @@ class Replay: def finish_calculation(self, duration): bui.textwidget(self.status_text, text='Starting...' if duration else 'Wait what?') - bui.textwidget(self.progress_text2, text=f'result was {duration} milleseconds') if duration else duration + bui.textwidget(self.progress_text2, + text=f'result was {duration} milleseconds') if duration else duration if not duration: self.error = True - bui.textwidget(self.progress_text, text='pybrp returned zero duration, error?\nclosing this window in 5 seconds') + bui.textwidget( + self.progress_text, text='pybrp returned zero duration, error?\nclosing this window in 5 seconds') bui.textwidget(self.progress_text2, text='') bui.apptimer(1 if duration else 5, CallPartial(self.start_player, duration)) @@ -290,6 +294,7 @@ class Replay: **kwargs ) + class Player: TICK = 0.01 COL0 = (0.5, 0, 0) @@ -325,11 +330,12 @@ class Player: self.camera_look = None self.replay_time = self.start_time = self.progress_val = 0 self.camera_zoom = 1 - [setattr(self, attr, []) for attr in ['ui_widgets', 'camera_widgets', 'hide_widgets', 'cinema_widgets', 'cinema_ui_widgets']] - + [setattr(self, attr, []) for attr in ['ui_widgets', 'camera_widgets', + 'hide_widgets', 'cinema_widgets', 'cinema_ui_widgets']] + # Start replay immediately bs.new_replay_session(path) - + width, height = bui.get_virtual_screen_size() self.bar_height = 80 self.parent = bui.containerwidget( @@ -360,31 +366,31 @@ class Player: def calculate_duration(self): """Calculate duration in background thread with reduced load""" import time as pytime - + try: total_ms = 0 last_yield = pytime.time() - + with open(self.path, 'rb') as f: f.seek(0, 2) file_size = f.tell() self.scan_progress[1] = file_size f.seek(6) - + while True: current_pos = f.tell() self.scan_progress[0] = current_pos - + # Yield control every 10ms to prevent freezing current_time = pytime.time() if current_time - last_yield > 0.01: pytime.sleep(0.005) # Small sleep to reduce CPU load last_yield = current_time - + b_data = f.read(1) if not b_data: break - + b1, comp_len = b_data[0], 0 if b1 < 254: comp_len = b1 @@ -392,15 +398,15 @@ class Player: comp_len = int.from_bytes(f.read(2), 'little') else: comp_len = int.from_bytes(f.read(4), 'little') - + if comp_len == 0: continue - + raw_msg = self.huffman.decompress(f.read(comp_len)) - + if not raw_msg or raw_msg[0] != 1: continue - + sub_off = 1 while sub_off + 2 <= len(raw_msg): sub_size_bytes = raw_msg[sub_off:sub_off+2] @@ -414,27 +420,27 @@ class Player: if len(sub_data) >= 2 and sub_data[0] == 0: total_ms += sub_data[1] sub_off += sub_size - + # Update confirmed duration (this is safe to seek to!) self.confirmed_duration_sec = total_ms / 1000 - + # Update estimated duration based on progress if current_pos > 1000: # After reading at least 1KB progress_percent = current_pos / file_size if progress_percent > 0: estimated_total_ms = total_ms / progress_percent self.estimated_duration = max(10.0, estimated_total_ms / 1000) - + self.scan_progress[0] = self.scan_progress[1] self.duration_ms = total_ms self.duration_sec = total_ms / 1000 - + except Exception as e: self.duration_ms = 0 self.duration_sec = 0 finally: self.scanning = False - + def update_scan_progress(self): """Update duration text with scan progress - alternates between % and estimated time""" if not self.scanning: @@ -444,7 +450,7 @@ class Player: bui.imagewidget(self.loading_bar, opacity=0) if self.main_bar.exists(): bui.imagewidget(self.main_bar, opacity=0.6) - + # Update to show final duration with normal color if hasattr(self, 'duration_time_text') and self.duration_time_text.exists(): bui.textwidget( @@ -453,17 +459,17 @@ class Player: color=self.COL6 ) return - + # Show scanning progress if hasattr(self, 'duration_time_text') and self.duration_time_text.exists(): current, total = self.scan_progress if total > 0: percent = int((current / total) * 100) - + # Cycle between showing percentage and estimated time self.scan_cycle += 1 show_percent = (self.scan_cycle // 10) % 2 == 0 # Switch every second - + if show_percent: # Show scan percentage bui.textwidget( @@ -480,7 +486,7 @@ class Player: text=format_time(self.estimated_duration), color=self.COL19 # Orange color ) - + # Update loading bar width based on scan progress if hasattr(self, 'loading_bar') and self.loading_bar.exists(): loading_width = (current / total) * self.progress_width @@ -673,7 +679,7 @@ class Player: maxwidth=100 ) widgets(self.current_time_text) - + # Duration display - alternates between scan% and estimated time while scanning self.duration_time_text = bui.textwidget( parent=parent, @@ -881,7 +887,8 @@ class Player: self.camera_look = None bui.textwidget(self.look_text, text='players') self.camera_zoom = new_zoom - bui.textwidget(self.zoom_text, text=f'x{round(0.5 ** (new_zoom - 1), 2)}' if new_zoom != 1 else 'x1.0' if self.manual_zoom else 'auto') + bui.textwidget( + self.zoom_text, text=f'x{round(0.5 ** (new_zoom - 1), 2)}' if new_zoom != 1 else 'x1.0' if self.manual_zoom else 'auto') self.apply_zoom() def look(self): @@ -953,8 +960,8 @@ class Player: label='', repeat=True )) - for i in range(cols) - for j in range(rows)] + for i in range(cols) + for j in range(rows)] def create_cinema_ui(self): widgets = self.cinema_ui_widgets.append @@ -1034,7 +1041,8 @@ class Player: crosshair = 20 [widgets(bui.imagewidget( parent=self.parent, - position=(width / 2, height / 2 - crosshair / 2 + crosshair * 0.1) if i else (width / 2 - crosshair / 2, height / 2 + crosshair * 0.1), + position=(width / 2, height / 2 - crosshair / 2 + crosshair * + 0.1) if i else (width / 2 - crosshair / 2, height / 2 + crosshair * 0.1), size=(3, crosshair * 1.15) if i else (crosshair * 1.15, 3), color=self.COL1, texture=bui.gettexture('white') @@ -1055,7 +1063,8 @@ class Player: parent=self.parent, texture=bui.gettexture('white'), color=self.COL1, - position=(width / 2 + offset, height / 2 + [offset - crosshair, -offset + crosshair * 0.3][j]), + position=(width / 2 + offset, height / 2 + + [offset - crosshair, -offset + crosshair * 0.3][j]), size=(3, crosshair * +1.1) )) @@ -1064,7 +1073,8 @@ class Player: parent=self.parent, texture=bui.gettexture('white'), color=self.COL1, - position=(width / 2 + [-offset, offset - crosshair][j], height / 2 - crosshair / 2 - offset + crosshair * 0.8), + position=(width / 2 + [-offset, offset - crosshair][j], + height / 2 - crosshair / 2 - offset + crosshair * 0.8), size=(crosshair * 1.1, 3) )) @@ -1073,7 +1083,8 @@ class Player: parent=self.parent, texture=bui.gettexture('white'), color=self.COL1, - position=(width / 2 - offset, height / 2 + [offset - crosshair, -offset + crosshair * 0.3][j]), + position=(width / 2 - offset, height / 2 + + [offset - crosshair, -offset + crosshair * 0.3][j]), size=(3, crosshair * 1.1) )) @@ -1211,7 +1222,8 @@ class Player: self.ui_hidden = hidden = not self.ui_hidden bui.textwidget(self.hide_text, text=self.hide_icons[hidden]) if hidden: - bui.apptimer(0.2, lambda: bui.buttonwidget(self.hide_button, texture=bui.gettexture('empty'))) + bui.apptimer(0.2, lambda: bui.buttonwidget( + self.hide_button, texture=bui.gettexture('empty'))) self.fade_hide_button(0.4, -0.05) self.destroy_ui() else: @@ -1258,7 +1270,8 @@ class Player: bui.textwidget(self.pause_text, text=icon) if not dry: if not silent: - self.show_message(['Resume', 'Pause'][self.paused], os.path.basename(self.path) + f' of {os.path.getsize(self.path)} bytes', self.COL6, self.COL7) + self.show_message(['Resume', 'Pause'][self.paused], os.path.basename( + self.path) + f' of {os.path.getsize(self.path)} bytes', self.COL6, self.COL7) if self.paused: self.stop() bs.pause_replay() @@ -1305,38 +1318,39 @@ class Player: """Seek with confirmed duration checking""" label = ['Forward by', 'Rewind by'][direction == -1] amount = direction * self.speed - + # Use estimated duration for calculating seek amount if self.scanning: seek_base = self.estimated_duration else: seek_base = self.duration_sec - + amount = (seek_base / 20) * amount new_time = (self.replay_time - self.start_time) + amount - + # Check if trying to seek beyond confirmed duration while scanning if self.scanning and new_time > self.confirmed_duration_sec: current, total = self.scan_progress percent = int((current / total) * 100) if total > 0 else 0 self.show_message('Buffering...', f'{percent}% loaded', self.COL2, self.COL3) return - + # Use appropriate duration for loop check max_duration = self.confirmed_duration_sec if self.scanning else self.duration_sec - + if (new_time >= max_duration) or (new_time <= 0): self.loop() else: self.start_time = self.replay_time - new_time self.reset_replay() bs.seek_replay(new_time) - + self.real_time = time() self.fix_pause() amount = abs(round(amount, 2)) - self.show_message('Seek', label + f" {amount} second{['s', ''][amount == 1]}", self.COL4, self.COL5) - + self.show_message( + 'Seek', label + f" {amount} second{['s', ''][amount == 1]}", self.COL4, self.COL5) + def jump(self, percent): """Jump with WYSIWYG behavior - percent is based on ESTIMATED duration, checked against CONFIRMED""" # Calculate target time based on ESTIMATED duration (what user sees on bar) @@ -1344,14 +1358,14 @@ class Player: target_time = self.estimated_duration * percent else: target_time = self.duration_sec * percent - + # Check if trying to jump beyond CONFIRMED duration while scanning if self.scanning and target_time > self.confirmed_duration_sec: current, total = self.scan_progress scan_percent = int((current / total) * 100) if total > 0 else 0 self.show_message('Buffering...', f'{scan_percent}% loaded', self.COL2, self.COL3) return - + self.start_time = self.replay_time - target_time self.reset_replay() bs.seek_replay(target_time) @@ -1378,7 +1392,7 @@ class Player: def update_progress_ui(self): """Modified to handle scanning state with growing progress bar""" elapsed = self.replay_time - self.start_time - + # Determine the current max duration to check against if self.scanning: max_duration = self.estimated_duration @@ -1386,14 +1400,14 @@ class Player: max_duration = self.duration_sec else: max_duration = None - + # Check if elapsed exceeds duration and loop if needed if max_duration and elapsed >= max_duration: self.loop() elapsed = 0 # Reset elapsed after loop - + nub_x, nub_y = self.nub_pos - + # Calculate progress if self.scanning: # Use estimated duration for progress bar @@ -1404,11 +1418,12 @@ class Player: progress = self.progress_width elif not self.duration_sec: # Fallback if scan failed - progress = min(elapsed / max(elapsed, 1) * self.progress_width * 0.1, self.progress_width * 0.1) + progress = min(elapsed / max(elapsed, 1) * self.progress_width * + 0.1, self.progress_width * 0.1) else: # Normal progress calculation, cap at 100% progress = min((elapsed / self.duration_sec) * self.progress_width, self.progress_width) - + try: bui.imagewidget(self.nub, position=(nub_x + progress, nub_y)) bui.textwidget(self.current_time_text, text=format_time(elapsed)) @@ -1434,73 +1449,104 @@ class Player: ) # Tools + + def get_ui_scale(small, medium, large=None): scale = bui.app.ui_v1.uiscale return small if scale is bui.UIScale.SMALL else medium if scale is bui.UIScale.MEDIUM else (large or medium) -return_to_menu = lambda: bui.app.classic.return_to_main_menu_session_gracefully(reset_ui=False) -show_warning = lambda text: (bui.getsound('block').play() or 1) and bui.screenmessage(text, color=(1, 1, 0)) -format_time = lambda seconds: strftime('%H:%M:%S', gmtime(seconds)) -get_overlay_stack = lambda: bui.get_special_widget('overlay_stack') -round_tuple = lambda tup: type(tup)([round(val, 1) for val in tup]) + + +def return_to_menu(): return bui.app.classic.return_to_main_menu_session_gracefully(reset_ui=False) +def show_warning(text): return (bui.getsound('block').play() + or 1) and bui.screenmessage(text, color=(1, 1, 0)) + + +def format_time(seconds): return strftime('%H:%M:%S', gmtime(seconds)) +def get_overlay_stack(): return bui.get_special_widget('overlay_stack') +def round_tuple(tup): return type(tup)([round(val, 1) for val in tup]) + # pybrp -Z = lambda _:[0]*_ -G_FREQS = lambda: [ - 101342,9667,3497,1072,0,3793,*Z(2),2815,5235,*Z(3),3570,*Z(3), - 1383,*Z(3),2970,*Z(2),2857,*Z(8),1199,*Z(30), - 1494,1974,*Z(12),1351,*Z(122),1475,*Z(65) +def Z(_): return [0]*_ + + +def G_FREQS(): return [ + 101342, 9667, 3497, 1072, 0, 3793, *Z(2), 2815, 5235, *Z(3), 3570, *Z(3), + 1383, *Z(3), 2970, *Z(2), 2857, *Z(8), 1199, *Z(30), + 1494, 1974, *Z(12), 1351, *Z(122), 1475, *Z(65) ] + + class _Huffman: class _N: def __init__(self): - self.l,self.r,self.p,self.f=-1,-1,0,0 + self.l, self.r, self.p, self.f = -1, -1, 0, 0 + def __init__(self): - self.nodes=[self._N()for _ in range(511)] + self.nodes = [self._N()for _ in range(511)] gf = G_FREQS() - for i in range(256):self.nodes[i].f=gf[i] - nc=256 - while nc<511: - s1,s2=-1,-1 - i=0 - while self.nodes[i].p!=0:i+=1 - s1=i;i+=1 - while self.nodes[i].p!=0:i+=1 - s2=i;i+=1 - while iself.nodes[s2].f: - if self.nodes[i].f>7 - if not comp:return src - out,ptr,l=bytearray(),src[1:],len(src) - bl=((l-1)*8)-rem;bit=0 - while bit>3]>>(bit&7))&1;bit+=1 + for i in range(256): + self.nodes[i].f = gf[i] + nc = 256 + while nc < 511: + s1, s2 = -1, -1 + i = 0 + while self.nodes[i].p != 0: + i += 1 + s1 = i + i += 1 + while self.nodes[i].p != 0: + i += 1 + s2 = i + i += 1 + while i < nc: + if self.nodes[i].p == 0: + if self.nodes[s1].f > self.nodes[s2].f: + if self.nodes[i].f < self.nodes[s1].f: + s1 = i + elif self.nodes[i].f < self.nodes[s2].f: + s2 = i + i += 1 + self.nodes[nc].f = self.nodes[s1].f+self.nodes[s2].f + self.nodes[s1].p = self.nodes[s2].p = nc-255 + self.nodes[nc].r, self.nodes[nc].l = s1, s2 + nc += 1 + + def decompress(self, src): + if not src: + return b'' + rem, comp = src[0] & 15, src[0] >> 7 + if not comp: + return src + out, ptr, l = bytearray(), src[1:], len(src) + bl = ((l-1)*8)-rem + bit = 0 + while bit < bl: + m_bit = (ptr[bit >> 3] >> (bit & 7)) & 1 + bit += 1 if m_bit: - n=510 - while n>=256: - if bit>=bl:raise ValueError("Incomplete Huffman code") - p_bit=(ptr[bit>>3]>>(bit&7))&1;bit+=1 - n=self.nodes[n].l if p_bit==0 else self.nodes[n].r + n = 510 + while n >= 256: + if bit >= bl: + raise ValueError("Incomplete Huffman code") + p_bit = (ptr[bit >> 3] >> (bit & 7)) & 1 + bit += 1 + n = self.nodes[n].l if p_bit == 0 else self.nodes[n].r out.append(n) else: - if bit+8>bl:break - bi,b_in_b=bit>>3,bit&7 - val=ptr[bi]if b_in_b==0 else(ptr[bi]>>b_in_b)|(ptr[bi+1]<<(8-b_in_b)) - out.append(val&255);bit+=8 + if bit+8 > bl: + break + bi, b_in_b = bit >> 3, bit & 7 + val = ptr[bi]if b_in_b == 0 else (ptr[bi] >> b_in_b) | (ptr[bi+1] << (8-b_in_b)) + out.append(val & 255) + bit += 8 return bytes(out) + + def get_replay_duration(_h, brp_path, progress): total_ms = 0 with open(brp_path, 'rb') as f: - f.seek(0,2) + f.seek(0, 2) progress[1] = f.tell() f.seek(6) while True: @@ -1539,16 +1585,21 @@ def get_replay_duration(_h, brp_path, progress): # brobord collide grass # ba_meta require api 9 # ba_meta export babase.Plugin + + class byBordd(Plugin): - has_settings_ui = lambda c=0: True - show_settings_ui = lambda c=0, w=None: Replay(source=w) + def has_settings_ui(c=0): return True + def show_settings_ui(c=0, w=None): return Replay(source=w) + def __init__(self): from bauiv1lib.ingamemenu import InGameMenuWindow as ingame orig_refresh = getattr(ingame, '_refresh_in_game') - setattr(ingame, '_refresh_in_game', lambda window, *args, **kwargs: (self.add_button(window), orig_refresh(window, *args, **kwargs))[1]) + setattr(ingame, '_refresh_in_game', lambda window, *args, ** + kwargs: (self.add_button(window), orig_refresh(window, *args, **kwargs))[1]) from bauiv1lib.watch import WatchWindow as watch orig_init = getattr(watch, '__init__') - setattr(watch, '__init__', lambda window, *args, **kwargs: (orig_init(window, *args, **kwargs), self.add_button(window, 1))[0]) + setattr(watch, '__init__', lambda window, *args, ** + kwargs: (orig_init(window, *args, **kwargs), self.add_button(window, 1))[0]) def add_button(self, window, watch_mode=0): if watch_mode: From 9b6a098e52b720349d0ee633163b1f347dcddbd9 Mon Sep 17 00:00:00 2001 From: BroBordd <192699453+BroBordd@users.noreply.github.com> Date: Thu, 6 Nov 2025 14:40:24 +0000 Subject: [PATCH 4/4] [ci] apply-version-metadata --- plugins/utilities.json | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/plugins/utilities.json b/plugins/utilities.json index a1f7f4c..0882250 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -295,13 +295,18 @@ } ], "versions": { + "3.0": { + "api_version": 9, + "commit_sha": "fd4bcc1", + "released_on": "06-11-2025", + "md5sum": "456b1e24a55a0d08b35246e1c69f5acd" + }, "1.5": { "api_version": 9, "commit_sha": "99611b9", "released_on": "10-08-2025", "md5sum": "e97e88170046b4822067f3164ee1dcf5" - }, - "3.0": null + } } }, "topmsg": { @@ -2323,4 +2328,4 @@ } } } -} +} \ No newline at end of file