Compare commits

..

No commits in common. "d8bf7239f6403afdd0ce837fb823d92f7a9f78f9" and "25da1593d1551c345a0bdda77adc6f6196dc7e98" have entirely different histories.

5 changed files with 98 additions and 108 deletions

View File

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

View File

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

View File

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

View File

@ -4,6 +4,8 @@ from pathlib import Path
import time
from dataclasses import dataclass
from typing import Generator, Self
import config
from loguru import logger
from abc import ABC, abstractmethod
@ -38,10 +40,16 @@ class Line:
class Event:
timestamp: datetime.datetime | None
def to_str(self, include_ts: bool = False, rich_text: bool = False) -> str:
def to_str(self, include_ts: bool = False) -> str:
return str(self)
class SubscriberABC(ABC):
@abstractmethod
def consume_event(self, event: Event) -> None:
raise NotImplementedError
@dataclass(frozen=True)
class Kill(Event):
killer: str
@ -63,51 +71,13 @@ class Kill(Event):
damage_type=splited[21],
)
def to_str(self, include_ts: bool = False, rich_text: bool = True) -> str:
def to_str(self, include_ts: bool = False) -> str:
ts = (self.timestamp.strftime("%Y-%m-%d %H:%M:%S") + ": ") * include_ts
if self.victim == self.killer:
return f"{ts}{'**' * rich_text}{self.victim}{'**' * rich_text} РКН{' 🤦' * rich_text}"
return f"{ts}**{self.victim}** РКН 🤦"
emoji = " " + damage_type_to_emoji.get(self.damage_type, "")
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
emoji = damage_type_to_emoji.get(self.damage_type, "")
return f"{ts}**{self.victim}** killed by 🔫 **{self.killer}** {emoji}"
@dataclass
@ -156,6 +126,16 @@ class Monitor:
and process_important_realtime_events
):
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)
def monitor_log_file(self) -> Generator[Line, None, None]:
@ -172,8 +152,31 @@ class Monitor:
is_realtime = True
time.sleep(0.15)
file.seek(current_position)
if config.DEBUG is True:
return
line = line.removesuffix("\n")
yield Line(line, is_realtime)
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,9 +1,11 @@
from contextlib import suppress
from functools import partial
from collections import deque
import pystray
from pystray import Menu, MenuItem
from PIL import Image, ImageDraw
import config
import threading
from loguru import logger
import sv_ttk
from pathlib import Path
import threading
@ -11,17 +13,14 @@ import tkinter as tk
from tkinter import ttk, filedialog, scrolledtext
import config
from discord_subscriber import DiscordSubscriber
from monitor import Monitor, Event, SubscriberABC, Filter
from monitor import Monitor, Event, SubscriberABC
from loguru import logger
from typing import Callable
class Feed(SubscriberABC):
def __init__(
self, event_filter: Filter, callback_data_changed: Callable | None = None
):
super().__init__(event_filter)
self.ring_buffer: deque[Event] = deque(maxlen=512)
def __init__(self, callback_data_changed: Callable | None = None):
self.ring_buffer: deque[Event] = deque(maxlen=150)
self.ring_buffer_lock = threading.Lock()
self.callback = callback_data_changed
@ -39,9 +38,8 @@ class Feed(SubscriberABC):
def render(self) -> str:
with self.ring_buffer_lock:
return "\n".join(
event.to_str(include_ts=self.include_ts.get(), rich_text=False)
event.to_str(include_ts=self.include_ts.get())
for event in self.ring_buffer
if self.filter.filter_event(event)
)
def clear(self) -> None:
@ -70,10 +68,9 @@ class DeathLogGUI:
def __init__(self, root: tk.Tk):
self.root = root
self.root.title(config.APP_NAME)
self.root.geometry(config.db.get_str("GEOMETRY", "850x600"))
self.root.geometry("850x600")
self.root.minsize(850, 600)
self.root.protocol("WM_DELETE_WINDOW", self.handle_close_window)
theme_mapping = {True: "dark", False: "light"}
sv_ttk.set_theme(theme_mapping.get(config.db.get_bool("IS_DARK_MODE", False)))
@ -82,23 +79,7 @@ class DeathLogGUI:
)
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.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.trace_add(
@ -109,14 +90,13 @@ class DeathLogGUI:
)
self.feed = Feed(
callback_data_changed=partial(self.root.after, 0, self.update_feed_display),
event_filter=self.filter,
callback_data_changed=partial(self.root.after, 0, self.update_feed_display)
)
self.enable_discord = config.db.get_tk_boolean("ENABLE_DISCORD", False)
self.webhook_url = config.db.get_tk_str(
"DISCORD_WEBHOOK_URL", "https://discord.com/..."
)
self.discord = DiscordSubscriber(event_filter=self.filter)
self.discord = DiscordSubscriber()
self.discord.enabled = self.enable_discord.get()
self.discord.webhook_url = self.webhook_url.get()
@ -125,7 +105,7 @@ class DeathLogGUI:
)
self.enable_discord.trace_add(
"write", lambda a, b, c: self.discord.set_enabled(self.enable_discord.get())
"read", lambda a, b, c: self.discord.set_enabled(self.enable_discord.get())
)
self.monitor: Monitor | None = None
@ -152,10 +132,7 @@ class DeathLogGUI:
def shutdown(self):
logger.info("Beginning GUI shutdown")
self.stop_monitoring()
logger.info(self.root.winfo_geometry())
config.db.set_str("GEOMETRY", self.root.winfo_geometry())
self.tray.stop()
config.db.db.close()
self.root.quit()
def handle_close_window(self) -> None:
@ -204,29 +181,22 @@ class DeathLogGUI:
text="Do send events to discord",
variable=self.enable_discord,
).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(
config_frame,
text="Hide NPC kills (_NPC_ in name)",
variable=self.include_npc_kills,
).grid(row=5, column=0, columnspan=2, sticky=tk.W, pady=2)
).grid(row=4, column=0, columnspan=2, sticky=tk.W, pady=2)
ttk.Checkbutton(
config_frame,
text="Include timestamps",
variable=self.feed.include_ts,
).grid(row=6, column=0, sticky=tk.W, pady=2)
).grid(row=5, column=0, sticky=tk.W, pady=2)
ttk.Checkbutton(
config_frame,
text="Enable dark mode",
variable=self.is_dark_mode,
).grid(row=7, column=0, sticky=tk.W, pady=2)
).grid(row=6, column=0, sticky=tk.W, pady=2)
# Control buttons
control_frame = ttk.Frame(main_frame)
@ -282,7 +252,7 @@ class DeathLogGUI:
self.monitor = Monitor(
log_file_path=Path(self.log_path.get()),
do_backread=self.do_backread.get(),
subscribers={self.feed, self.discord},
subscribers={self.feed},
)
self.monitor_thread = threading.Thread(
@ -312,11 +282,39 @@ class DeathLogGUI:
self.stop_button.config(state=tk.DISABLED)
def update_feed_display(self):
with suppress(AttributeError):
self.filter.nickname = self.monitor.nickname
self.feed_display.config(state=tk.NORMAL)
self.feed_display.delete("1.0", tk.END)
self.feed_display.insert("1.0", self.feed.render())
self.feed_display.config(state=tk.DISABLED)
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")