From 414de0f850715ed13b22a43c05ad724fb27bfe94 Mon Sep 17 00:00:00 2001 From: Dregu Date: Tue, 10 Oct 2023 09:10:08 +0300 Subject: [PATCH] Added all timers tracker --- src/modlunky2/config.py | 12 ++ src/modlunky2/mem/state.py | 3 + src/modlunky2/ui/trackers/__init__.py | 2 + src/modlunky2/ui/trackers/common.py | 25 ++-- src/modlunky2/ui/trackers/pacifist.py | 2 +- src/modlunky2/ui/trackers/timer.py | 197 ++++++++++++++++++++++++++ 6 files changed, 232 insertions(+), 9 deletions(-) create mode 100644 src/modlunky2/ui/trackers/timer.py diff --git a/src/modlunky2/config.py b/src/modlunky2/config.py index 762d887d9..bcb1d2132 100644 --- a/src/modlunky2/config.py +++ b/src/modlunky2/config.py @@ -142,12 +142,24 @@ class PacifistTrackerConfig(CommonTrackerConfig): show_kill_count: bool = field(default=False, skip_if_default=True) +@serialize(rename_all="kebabcase") +@deserialize(rename_all="kebabcase") +@dataclass +class TimerTrackerConfig(CommonTrackerConfig): + show_total: bool = field(default=True, skip_if_default=True) + show_level: bool = field(default=True, skip_if_default=True) + show_last_level: bool = field(default=True, skip_if_default=True) + show_tutorial: bool = field(default=True, skip_if_default=True) + show_startup: bool = field(default=True, skip_if_default=True) + + @serialize(rename_all="kebabcase") @deserialize(rename_all="kebabcase") @dataclass class TrackersConfig: category: CategoryTrackerConfig = field(default_factory=CategoryTrackerConfig) pacifist: PacifistTrackerConfig = field(default_factory=PacifistTrackerConfig) + timer: TimerTrackerConfig = field(default_factory=TimerTrackerConfig) @serialize # Note: these fields aren't renamed for historical reasons diff --git a/src/modlunky2/mem/state.py b/src/modlunky2/mem/state.py index 62388fbd8..3b72f3810 100644 --- a/src/modlunky2/mem/state.py +++ b/src/modlunky2/mem/state.py @@ -173,10 +173,13 @@ class State: arena_state: ArenaState = struct_field(0x95C, dc_struct, default_factory=ArenaState) run_recap_flags: RunRecapFlags = struct_field(0xA34, sc_uint32, default=0) hud_flags: HudFlags = struct_field(0xA50, sc_uint32, default=0) + time_last_level: int = struct_field(0xA40, sc_uint32, default=0) time_level: int = struct_field(0xA44, sc_uint32, default=0) + time_tutorial: int = struct_field(0xA48, sc_uint32, default=0) presence_flags: PresenceFlags = struct_field(0xA54, sc_uint32, default=0) next_entity_uid: int = struct_field(0x12E0, sc_uint32, default=0) items: Optional[Items] = struct_field(0x12F0, pointer(dc_struct), default=None) instance_id_to_pointer: UidEntityMap = struct_field( 0x1348, uid_entity_map, default_factory=DictMap ) + time_startup: int = struct_field(0x13A0, sc_uint32, default=0) diff --git a/src/modlunky2/ui/trackers/__init__.py b/src/modlunky2/ui/trackers/__init__.py index 928edcce6..799d8f263 100644 --- a/src/modlunky2/ui/trackers/__init__.py +++ b/src/modlunky2/ui/trackers/__init__.py @@ -7,6 +7,7 @@ from .options import OptionsFrame from .pacifist import PacifistButtons +from .timer import TimerButtons logger = logging.getLogger(__name__) @@ -21,6 +22,7 @@ def __init__(self, parent, ml_config: Config, *args, **kwargs): self.add_button(PacifistButtons(self, self.ml_config)) self.add_button(CategoryButtons(self, self.ml_config)) + self.add_button(TimerButtons(self, self.ml_config)) self.rowconfigure(self.button_index, weight=1) diff --git a/src/modlunky2/ui/trackers/common.py b/src/modlunky2/ui/trackers/common.py index 880a4f7ef..fdff595f0 100644 --- a/src/modlunky2/ui/trackers/common.py +++ b/src/modlunky2/ui/trackers/common.py @@ -218,16 +218,23 @@ def __init__( self.text = "Connecting..." self.label = tk.Label( - self, text=self.text, bg=self.color_key, fg="white", font=font + self, + text=self.text, + bg=self.color_key, + fg="white", + font=font, + justify="right", ) self.label.columnconfigure(0, weight=1) self.label.rowconfigure(0, weight=1) self.label.grid(row=0, column=0, padx=5, pady=5, sticky="nsew") TRACKERS_DIR.mkdir(parents=True, exist_ok=True) - self.text_file = TRACKERS_DIR / file_name - with self.text_file.open("w", encoding="utf-8") as handle: - handle.write(self.text) + self.text_file = None + if file_name: + self.text_file = TRACKERS_DIR / file_name + with self.text_file.open("w", encoding="utf-8") as handle: + handle.write(self.text) self.watcher_thread.start() self.after(self.POLL_INTERVAL, self.after_watcher_thread) @@ -252,8 +259,9 @@ def update_text(self, new_text): return self.text = new_text self.label.configure(text=self.text) - with self.text_file.open("w", encoding="utf-8") as handle: - handle.write(new_text) + if self.text_file: + with self.text_file.open("w", encoding="utf-8") as handle: + handle.write(new_text) def shut_down(self, level, message): logger.log(level, "%s", message) @@ -297,7 +305,8 @@ def destroy(self): if self.on_close: self.on_close() - with self.text_file.open("w", encoding="utf-8") as handle: - handle.write("Not running") + if self.text_file: + with self.text_file.open("w", encoding="utf-8") as handle: + handle.write("Not running") return super().destroy() diff --git a/src/modlunky2/ui/trackers/pacifist.py b/src/modlunky2/ui/trackers/pacifist.py index f17fe31d1..bce435de1 100644 --- a/src/modlunky2/ui/trackers/pacifist.py +++ b/src/modlunky2/ui/trackers/pacifist.py @@ -40,7 +40,7 @@ def __init__(self, parent, modlunky_config: Config, *args, **kwargs): offvalue=False, command=self.toggle_show_kill_count, ) - self.show_kill_count_checkbox.grid(row=0, column=1, pady=5, padx=5, sticky="nw") + self.show_kill_count_checkbox.grid(row=0, column=1, pady=5, padx=5, sticky="w") def toggle_show_kill_count(self): self.modlunky_config.trackers.pacifist.show_kill_count = ( diff --git a/src/modlunky2/ui/trackers/timer.py b/src/modlunky2/ui/trackers/timer.py new file mode 100644 index 000000000..25103ed62 --- /dev/null +++ b/src/modlunky2/ui/trackers/timer.py @@ -0,0 +1,197 @@ +import logging +import tkinter as tk +from tkinter import ttk +import datetime + +from modlunky2.config import Config, TimerTrackerConfig +from modlunky2.mem import Spel2Process + +from modlunky2.ui.trackers.common import ( + Tracker, + TrackerWindow, + WindowData, +) + +logger = logging.getLogger(__name__) + + +class TimerButtons(ttk.Frame): + def __init__(self, parent, modlunky_config: Config, *args, **kwargs): + super().__init__(parent, *args, **kwargs) + self.modlunky_config = modlunky_config + self.columnconfigure(0, weight=1) + self.rowconfigure(0, minsize=60) + self.window = None + self.modlunky_config.trackers.timer.anchor = "w" + + self.timer_button = ttk.Button( + self, + text="Timer", + command=self.launch, + ) + self.timer_button.grid(row=0, column=0, pady=5, padx=5, sticky="nswe") + + self.show_total = tk.BooleanVar() + self.show_total.set(self.modlunky_config.trackers.timer.show_total) + self.show_total_checkbox = ttk.Checkbutton( + self, + text="Total", + variable=self.show_total, + onvalue=True, + offvalue=False, + command=self.toggle_show_total, + ) + self.show_total_checkbox.grid(row=0, column=1, pady=5, padx=5, sticky="w") + + self.show_level = tk.BooleanVar() + self.show_level.set(self.modlunky_config.trackers.timer.show_level) + self.show_level_checkbox = ttk.Checkbutton( + self, + text="Level", + variable=self.show_level, + onvalue=True, + offvalue=False, + command=self.toggle_show_level, + ) + self.show_level_checkbox.grid(row=0, column=2, pady=5, padx=5, sticky="w") + + self.show_last_level = tk.BooleanVar() + self.show_last_level.set(self.modlunky_config.trackers.timer.show_last_level) + self.show_last_level_checkbox = ttk.Checkbutton( + self, + text="Last Level", + variable=self.show_last_level, + onvalue=True, + offvalue=False, + command=self.toggle_show_last_level, + ) + self.show_last_level_checkbox.grid(row=0, column=3, pady=5, padx=5, sticky="w") + + self.show_tutorial = tk.BooleanVar() + self.show_tutorial.set(self.modlunky_config.trackers.timer.show_tutorial) + self.show_tutorial_checkbox = ttk.Checkbutton( + self, + text="Tutorial", + variable=self.show_tutorial, + onvalue=True, + offvalue=False, + command=self.toggle_show_tutorial, + ) + self.show_tutorial_checkbox.grid(row=0, column=4, pady=5, padx=5, sticky="w") + + self.show_startup = tk.BooleanVar() + self.show_startup.set(self.modlunky_config.trackers.timer.show_startup) + self.show_startup_checkbox = ttk.Checkbutton( + self, + text="Session", + variable=self.show_startup, + onvalue=True, + offvalue=False, + command=self.toggle_show_startup, + ) + self.show_startup_checkbox.grid(row=0, column=5, pady=5, padx=5, sticky="w") + + def toggle_show_total(self): + self.modlunky_config.trackers.timer.show_total = self.show_total.get() + self.modlunky_config.save() + if self.window: + self.window.update_config(self.modlunky_config.trackers.timer) + + def toggle_show_level(self): + self.modlunky_config.trackers.timer.show_level = self.show_level.get() + self.modlunky_config.save() + if self.window: + self.window.update_config(self.modlunky_config.trackers.timer) + + def toggle_show_last_level(self): + self.modlunky_config.trackers.timer.show_last_level = self.show_last_level.get() + self.modlunky_config.save() + if self.window: + self.window.update_config(self.modlunky_config.trackers.timer) + + def toggle_show_tutorial(self): + self.modlunky_config.trackers.timer.show_tutorial = self.show_tutorial.get() + self.modlunky_config.save() + if self.window: + self.window.update_config(self.modlunky_config.trackers.timer) + + def toggle_show_startup(self): + self.modlunky_config.trackers.timer.show_startup = self.show_startup.get() + self.modlunky_config.save() + if self.window: + self.window.update_config(self.modlunky_config.trackers.timer) + + def launch(self): + color_key = self.modlunky_config.tracker_color_key + self.disable_button() + self.window = TrackerWindow( + title="Timer Tracker", + color_key=color_key, + on_close=self.window_closed, + file_name="", + tracker=TimerTracker(), + config=self.modlunky_config.trackers.timer, + ) + + def window_closed(self): + self.window = None + # If we're in the midst of destroy() the button might not exist + if self.timer_button.winfo_exists(): + self.timer_button["state"] = tk.NORMAL + + def disable_button(self): + self.timer_button["state"] = tk.DISABLED + + +class TimerTracker(Tracker[TimerTrackerConfig, WindowData]): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.time_total = 0 + self.time_level = 0 + self.time_last_level = 0 + self.time_tutorial = 0 + self.time_startup = 0 + + def initialize(self): + self.time_total = 0 + self.time_level = 0 + self.time_last_level = 0 + self.time_tutorial = 0 + self.time_startup = 0 + + def poll(self, proc: Spel2Process, config: TimerTrackerConfig) -> WindowData: + game_state = proc.get_state() + if game_state is None: + return None + + self.time_total = game_state.time_total + self.time_level = game_state.time_level + self.time_last_level = game_state.time_last_level + self.time_tutorial = game_state.time_tutorial + self.time_startup = game_state.time_startup + + label = self.get_text(config) + return WindowData(label) + + def format(self, frames): + return datetime.datetime.utcfromtimestamp(frames / 60).strftime("%H:%M:%S.%f")[ + :-3 + ] + + def get_text( + self, + config: TimerTrackerConfig, + ): + out = [] + if config.show_total: + out.append(f"Total: {self.format(self.time_total)}") + if config.show_level: + out.append(f"Level: {self.format(self.time_level)}") + if config.show_last_level: + out.append(f"Last level: {self.format(self.time_last_level)}") + if config.show_tutorial: + out.append(f"Tutorial: {self.format(self.time_tutorial)}") + if config.show_startup: + out.append(f"Session: {self.format(self.time_startup)}") + + return "\n".join(out)