Compare commits

..

2 Commits

Author SHA1 Message Date
d8bf7239f6
Best effor scaling for windows 2025-05-25 17:53:38 +00:00
eabd2f0f78
Implement filtering 2025-05-25 17:51:29 +00:00
5 changed files with 110 additions and 100 deletions

View File

@ -5,8 +5,6 @@ import sqlite3
APP_NAME = "SC KF" APP_NAME = "SC KF"
DEBUG = os.getenv("DEBUG") is not None
class PersistentConfig: class PersistentConfig:
def __init__(self, db_path: Path): def __init__(self, db_path: Path):

View File

@ -1,10 +1,12 @@
import requests import requests
from monitor import Event, SubscriberABC from monitor import Event, SubscriberABC
from loguru import logger from loguru import logger
from monitor import Filter
class DiscordSubscriber(SubscriberABC): class DiscordSubscriber(SubscriberABC):
def __init__(self): def __init__(self, event_filter: Filter):
super().__init__(event_filter)
self.webhook_url: str | None = None self.webhook_url: str | None = None
self.enabled: bool = True self.enabled: bool = True
@ -18,10 +20,15 @@ class DiscordSubscriber(SubscriberABC):
if not self.enabled: if not self.enabled:
return return
try: if not self.filter.filter_event(event):
requests.post( return
self.webhook_url, json={"content": event.to_str(include_ts=False)}
)
except Exception as e: try:
r = requests.post(
self.webhook_url,
json={"content": event.to_str(include_ts=False, rich_text=True)},
)
r.raise_for_status()
except Exception:
logger.opt(exception=True).warning(f"Error sending to Discord: {event}") logger.opt(exception=True).warning(f"Error sending to Discord: {event}")

View File

@ -1,8 +1,14 @@
from ui import DeathLogGUI from ui import DeathLogGUI
import tkinter as tk import tkinter as tk
from contextlib import suppress
def main(): def main():
with suppress(Exception):
from ctypes import windll
windll.shcore.SetProcessDpiAwareness(1)
root = tk.Tk() root = tk.Tk()
app = DeathLogGUI(root) app = DeathLogGUI(root)
root.mainloop() root.mainloop()

View File

@ -4,8 +4,6 @@ from pathlib import Path
import time import time
from dataclasses import dataclass from dataclasses import dataclass
from typing import Generator, Self from typing import Generator, Self
import config
from loguru import logger from loguru import logger
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
@ -40,16 +38,10 @@ class Line:
class Event: class Event:
timestamp: datetime.datetime | None timestamp: datetime.datetime | None
def to_str(self, include_ts: bool = False) -> str: def to_str(self, include_ts: bool = False, rich_text: bool = False) -> str:
return str(self) return str(self)
class SubscriberABC(ABC):
@abstractmethod
def consume_event(self, event: Event) -> None:
raise NotImplementedError
@dataclass(frozen=True) @dataclass(frozen=True)
class Kill(Event): class Kill(Event):
killer: str killer: str
@ -71,13 +63,51 @@ class Kill(Event):
damage_type=splited[21], damage_type=splited[21],
) )
def to_str(self, include_ts: bool = False) -> str: def to_str(self, include_ts: bool = False, rich_text: bool = True) -> str:
ts = (self.timestamp.strftime("%Y-%m-%d %H:%M:%S") + ": ") * include_ts ts = (self.timestamp.strftime("%Y-%m-%d %H:%M:%S") + ": ") * include_ts
if self.victim == self.killer: if self.victim == self.killer:
return f"{ts}**{self.victim}** РКН 🤦" return f"{ts}{'**' * rich_text}{self.victim}{'**' * rich_text} РКН{' 🤦' * rich_text}"
emoji = damage_type_to_emoji.get(self.damage_type, "") emoji = " " + damage_type_to_emoji.get(self.damage_type, "")
return f"{ts}**{self.victim}** killed by 🔫 **{self.killer}** {emoji}" return f"{ts}{'**' * rich_text}{self.victim}{'**' * rich_text} killed by {'🔫 ' * rich_text}{'**' * rich_text}{self.killer}{'**' * rich_text}{emoji * rich_text}"
@dataclass
class Filter:
only_personal_feed: bool
include_npc: bool
nickname: str | None = None
def filter_event(self, event: Event) -> bool:
if isinstance(event, Kill):
if "_NPC_" in event.killer + event.victim and not self.include_npc:
return False
if all(
[
self.nickname not in (event.killer, event.victim),
self.only_personal_feed is True,
self.nickname is not None,
]
):
return False
return True
else:
logger.warning(
f"Don't know how to filter event {type(event)}, letting through"
)
return True
class SubscriberABC(ABC):
def __init__(self, event_filter: Filter):
self.filter = event_filter
@abstractmethod
def consume_event(self, event: Event) -> None:
raise NotImplementedError
@dataclass @dataclass
@ -126,16 +156,6 @@ class Monitor:
and process_important_realtime_events and process_important_realtime_events
): ):
kill = Kill.from_line(line.line) kill = Kill.from_line(line.line)
# if "_NPC_" in kill.killer + kill.victim and not self.include_npc: # TODO:
# continue
#
# if (
# self.nickname not in (kill.killer, kill.victim)
# and self.only_personal_feed is True
# ):
# continue
self.call_subscribers(kill) self.call_subscribers(kill)
def monitor_log_file(self) -> Generator[Line, None, None]: def monitor_log_file(self) -> Generator[Line, None, None]:
@ -152,31 +172,8 @@ class Monitor:
is_realtime = True is_realtime = True
time.sleep(0.15) time.sleep(0.15)
file.seek(current_position) file.seek(current_position)
if config.DEBUG is True:
return
line = line.removesuffix("\n") line = line.removesuffix("\n")
yield Line(line, is_realtime) yield Line(line, is_realtime)
logger.info("Exiting reading loop") logger.info("Exiting reading loop")
# def main():
# try:
# if config.DEBUG is True:
# app = Monitor(
# log_file_path=Path("./Game.log"),
# do_backread=True,
# )
#
# else:
# app = Monitor(config.LOG_FILE_PATH, config.DISCORD_WEBHOOK_URL)
#
# app.process_lines()
#
# except KeyboardInterrupt:
# print("\nMonitoring stopped")
#
#
# if __name__ == "__main__":
# main()

94
ui.py
View File

@ -1,11 +1,9 @@
from contextlib import suppress
from functools import partial from functools import partial
from collections import deque from collections import deque
import pystray import pystray
from pystray import Menu, MenuItem from pystray import Menu, MenuItem
from PIL import Image, ImageDraw from PIL import Image, ImageDraw
import config
import threading
from loguru import logger
import sv_ttk import sv_ttk
from pathlib import Path from pathlib import Path
import threading import threading
@ -13,14 +11,17 @@ import tkinter as tk
from tkinter import ttk, filedialog, scrolledtext from tkinter import ttk, filedialog, scrolledtext
import config import config
from discord_subscriber import DiscordSubscriber from discord_subscriber import DiscordSubscriber
from monitor import Monitor, Event, SubscriberABC from monitor import Monitor, Event, SubscriberABC, Filter
from loguru import logger from loguru import logger
from typing import Callable from typing import Callable
class Feed(SubscriberABC): class Feed(SubscriberABC):
def __init__(self, callback_data_changed: Callable | None = None): def __init__(
self.ring_buffer: deque[Event] = deque(maxlen=150) self, event_filter: Filter, callback_data_changed: Callable | None = None
):
super().__init__(event_filter)
self.ring_buffer: deque[Event] = deque(maxlen=512)
self.ring_buffer_lock = threading.Lock() self.ring_buffer_lock = threading.Lock()
self.callback = callback_data_changed self.callback = callback_data_changed
@ -38,8 +39,9 @@ class Feed(SubscriberABC):
def render(self) -> str: def render(self) -> str:
with self.ring_buffer_lock: with self.ring_buffer_lock:
return "\n".join( return "\n".join(
event.to_str(include_ts=self.include_ts.get()) event.to_str(include_ts=self.include_ts.get(), rich_text=False)
for event in self.ring_buffer for event in self.ring_buffer
if self.filter.filter_event(event)
) )
def clear(self) -> None: def clear(self) -> None:
@ -68,9 +70,10 @@ class DeathLogGUI:
def __init__(self, root: tk.Tk): def __init__(self, root: tk.Tk):
self.root = root self.root = root
self.root.title(config.APP_NAME) self.root.title(config.APP_NAME)
self.root.geometry("850x600") self.root.geometry(config.db.get_str("GEOMETRY", "850x600"))
self.root.minsize(850, 600) self.root.minsize(850, 600)
self.root.protocol("WM_DELETE_WINDOW", self.handle_close_window) self.root.protocol("WM_DELETE_WINDOW", self.handle_close_window)
theme_mapping = {True: "dark", False: "light"} theme_mapping = {True: "dark", False: "light"}
sv_ttk.set_theme(theme_mapping.get(config.db.get_bool("IS_DARK_MODE", False))) sv_ttk.set_theme(theme_mapping.get(config.db.get_bool("IS_DARK_MODE", False)))
@ -79,7 +82,23 @@ class DeathLogGUI:
) )
self.do_backread = config.db.get_tk_boolean("DO_BACKREAD", False) self.do_backread = config.db.get_tk_boolean("DO_BACKREAD", False)
self.include_npc_kills = config.db.get_tk_boolean("INCLUDE_NPC_KILLS", True) self.include_npc_kills = config.db.get_tk_boolean("INCLUDE_NPC_KILLS", True)
self.personal_feed = config.db.get_tk_boolean("PERSONAL_FEED", False)
self.filter = Filter(
only_personal_feed=self.personal_feed.get(),
include_npc=self.include_npc_kills.get(),
)
def filter_changed(a, b, c) -> None:
self.filter.include_npc = self.include_npc_kills.get()
self.filter.only_personal_feed = self.personal_feed.get()
logger.debug("Filter props changed")
self.feed.callback()
self.include_npc_kills.trace_add("write", filter_changed)
self.personal_feed.trace_add("write", filter_changed)
self.is_dark_mode = config.db.get_tk_boolean("IS_DARK_MODE", False) self.is_dark_mode = config.db.get_tk_boolean("IS_DARK_MODE", False)
self.is_dark_mode.trace_add( self.is_dark_mode.trace_add(
@ -90,13 +109,14 @@ class DeathLogGUI:
) )
self.feed = Feed( self.feed = Feed(
callback_data_changed=partial(self.root.after, 0, self.update_feed_display) callback_data_changed=partial(self.root.after, 0, self.update_feed_display),
event_filter=self.filter,
) )
self.enable_discord = config.db.get_tk_boolean("ENABLE_DISCORD", False) self.enable_discord = config.db.get_tk_boolean("ENABLE_DISCORD", False)
self.webhook_url = config.db.get_tk_str( self.webhook_url = config.db.get_tk_str(
"DISCORD_WEBHOOK_URL", "https://discord.com/..." "DISCORD_WEBHOOK_URL", "https://discord.com/..."
) )
self.discord = DiscordSubscriber() self.discord = DiscordSubscriber(event_filter=self.filter)
self.discord.enabled = self.enable_discord.get() self.discord.enabled = self.enable_discord.get()
self.discord.webhook_url = self.webhook_url.get() self.discord.webhook_url = self.webhook_url.get()
@ -105,7 +125,7 @@ class DeathLogGUI:
) )
self.enable_discord.trace_add( self.enable_discord.trace_add(
"read", lambda a, b, c: self.discord.set_enabled(self.enable_discord.get()) "write", lambda a, b, c: self.discord.set_enabled(self.enable_discord.get())
) )
self.monitor: Monitor | None = None self.monitor: Monitor | None = None
@ -132,7 +152,10 @@ class DeathLogGUI:
def shutdown(self): def shutdown(self):
logger.info("Beginning GUI shutdown") logger.info("Beginning GUI shutdown")
self.stop_monitoring() self.stop_monitoring()
logger.info(self.root.winfo_geometry())
config.db.set_str("GEOMETRY", self.root.winfo_geometry())
self.tray.stop() self.tray.stop()
config.db.db.close()
self.root.quit() self.root.quit()
def handle_close_window(self) -> None: def handle_close_window(self) -> None:
@ -181,22 +204,29 @@ class DeathLogGUI:
text="Do send events to discord", text="Do send events to discord",
variable=self.enable_discord, variable=self.enable_discord,
).grid(row=3, column=0, columnspan=2, sticky=tk.W, pady=2) ).grid(row=3, column=0, columnspan=2, sticky=tk.W, pady=2)
ttk.Checkbutton(
config_frame,
text="Only personal events",
variable=self.personal_feed,
).grid(row=4, column=0, columnspan=2, sticky=tk.W, pady=2)
ttk.Checkbutton( ttk.Checkbutton(
config_frame, config_frame,
text="Hide NPC kills (_NPC_ in name)", text="Hide NPC kills (_NPC_ in name)",
variable=self.include_npc_kills, variable=self.include_npc_kills,
).grid(row=4, column=0, columnspan=2, sticky=tk.W, pady=2) ).grid(row=5, column=0, columnspan=2, sticky=tk.W, pady=2)
ttk.Checkbutton( ttk.Checkbutton(
config_frame, config_frame,
text="Include timestamps", text="Include timestamps",
variable=self.feed.include_ts, variable=self.feed.include_ts,
).grid(row=5, column=0, sticky=tk.W, pady=2) ).grid(row=6, column=0, sticky=tk.W, pady=2)
ttk.Checkbutton( ttk.Checkbutton(
config_frame, config_frame,
text="Enable dark mode", text="Enable dark mode",
variable=self.is_dark_mode, variable=self.is_dark_mode,
).grid(row=6, column=0, sticky=tk.W, pady=2) ).grid(row=7, column=0, sticky=tk.W, pady=2)
# Control buttons # Control buttons
control_frame = ttk.Frame(main_frame) control_frame = ttk.Frame(main_frame)
@ -252,7 +282,7 @@ class DeathLogGUI:
self.monitor = Monitor( self.monitor = Monitor(
log_file_path=Path(self.log_path.get()), log_file_path=Path(self.log_path.get()),
do_backread=self.do_backread.get(), do_backread=self.do_backread.get(),
subscribers={self.feed}, subscribers={self.feed, self.discord},
) )
self.monitor_thread = threading.Thread( self.monitor_thread = threading.Thread(
@ -282,39 +312,11 @@ class DeathLogGUI:
self.stop_button.config(state=tk.DISABLED) self.stop_button.config(state=tk.DISABLED)
def update_feed_display(self): def update_feed_display(self):
with suppress(AttributeError):
self.filter.nickname = self.monitor.nickname
self.feed_display.config(state=tk.NORMAL) self.feed_display.config(state=tk.NORMAL)
self.feed_display.delete("1.0", tk.END) self.feed_display.delete("1.0", tk.END)
self.feed_display.insert("1.0", self.feed.render()) self.feed_display.insert("1.0", self.feed.render())
self.feed_display.config(state=tk.DISABLED) self.feed_display.config(state=tk.DISABLED)
self.feed_display.see(tk.END) self.feed_display.see(tk.END)
# try:
# # Check if we should run in GUI mode or CLI mode
# import sys
#
# # CLI mode
# if config.DEBUG is True:
# app = App(
# Path("./Game.log"),
# DEFAULT_DISCORD_WEBHOOK_URL,
# do_mock_discord=True,
# do_backread=True,
# hide_npc_kills=False,
# )
#
# else:
# app = App(
# DEFAULT_LOG_FILE_PATH,
# DEFAULT_DISCORD_WEBHOOK_URL,
# hide_npc_kills=False,
# )
# app.process_lines()
#
# else:
# # GUI mode
# root = tk.Tk()
# app = DeathLogGUI(root)
# root.mainloop()
#
# except KeyboardInterrupt:
# print("\nMonitoring stopped")