Compare commits

...

2 Commits

Author SHA1 Message Date
a37f162b21
ruff 2025-05-24 00:39:24 +00:00
5ba816715e
UI 2025-05-23 23:35:46 +00:00
4 changed files with 370 additions and 28 deletions

4
.idea/vcs.xml generated
View File

@ -1,4 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings" defaultProject="true" />
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

289
main.py
View File

@ -1,15 +1,20 @@
from pathlib import Path
import threading
import tkinter as tk
from tkinter import ttk, filedialog, scrolledtext
import queue
import requests
import time
from dataclasses import dataclass
from typing import Generator, Self
DISCORD_WEBHOOK_URL = "HOOOOOOOOOOOOOOOOK"
LOG_FILE_PATH = Path(r"E:\RSI\StarCitizen\LIVE\Game.log")
# Default values
DEFAULT_DISCORD_WEBHOOK_URL = "HOOOOOOOOOOOOOOOOK"
DEFAULT_LOG_FILE_PATH = Path(r"E:\RSI\StarCitizen\LIVE\Game.log")
DEBUG = False
@dataclass(frozen=True)
class Line:
line: str
@ -44,15 +49,20 @@ class App:
discord_webhook_url: str,
do_backread: bool = False,
do_mock_discord: bool = False,
hide_npc_kills: bool = False,
kill_callback=None,
):
self.log_file_path = log_file_path
self.do_backread: bool = do_backread
self.do_mock_discord: bool = do_mock_discord
self.discord_webhook_url: str = discord_webhook_url
self.nickname: str | None = None
self.kill_callback = kill_callback
self.hide_npc_kills = hide_npc_kills
self.running = True
if not (discord_webhook_url.startswith("https://") or do_mock_discord):
msg = f'{discord_webhook_url} не вебхук, так то'
msg = f"{discord_webhook_url} не вебхук, так то"
print(msg)
raise RuntimeError(msg)
@ -71,6 +81,9 @@ class App:
def process_lines(self) -> None:
for line in self.monitor_log_file():
if not self.running:
break
process_important_realtime_events = self.do_backread or line.is_realtime
if "<AccountLoginCharacterStatus_Character>" in line.line:
@ -82,23 +95,27 @@ class App:
and process_important_realtime_events
):
kill = Kill.from_line(line.line)
# if self.nickname in (kill.killer, kill.victim):
# Skip processing if this is an NPC kill and hide_npc_kills is enabled
if self.hide_npc_kills and "_NPC_" in kill.victim:
continue
self.send_kill(kill)
def send_kill(self, kill: Kill) -> None:
if kill.victim == kill.killer:
msg = f"**{kill.victim}** РКН"
msg = f"🤦 **{kill.victim}** РКН"
else:
msg = f"**{kill.victim}** killed by **{kill.killer}**"
msg = f"💀 **{kill.victim}** killed by 🔫 **{kill.killer}**"
self.send(msg)
# Call the callback if provided
if self.kill_callback:
self.kill_callback(kill, msg)
def send(self, message: str) -> None:
try:
if self.do_mock_discord:
print("Sending message to discord", message)
else:
requests.post(self.discord_webhook_url, json={"content": message})
@ -107,31 +124,251 @@ class App:
def monitor_log_file(self) -> Generator[Line, None, None]:
is_realtime = False
with self.log_file_path.open("r", encoding="utf-8") as file:
while True:
current_position = file.tell()
line = file.readline()
try:
with self.log_file_path.open("r", encoding="utf-8") as file:
while self.running:
current_position = file.tell()
line = file.readline()
if not line:
is_realtime = True
time.sleep(5)
file.seek(current_position)
if DEBUG is True:
return
if not line:
is_realtime = True
time.sleep(5)
file.seek(current_position)
if DEBUG is True:
return
line = line.removesuffix("\n")
yield Line(line, is_realtime)
line = line.removesuffix("\n")
yield Line(line, is_realtime)
except FileNotFoundError:
print(f"Log file not found: {self.log_file_path}")
return
except Exception as e:
print(f"Error monitoring log file: {e}")
return
def stop(self):
self.running = False
class DeathLogGUI:
def __init__(self, root):
self.root = root
self.root.title("Star Citizen Death Log Monitor")
self.root.geometry("800x600")
self.root.minsize(600, 400)
self.log_path = tk.StringVar(value=str(DEFAULT_LOG_FILE_PATH))
self.webhook_url = tk.StringVar(value=DEFAULT_DISCORD_WEBHOOK_URL)
self.do_backread = tk.BooleanVar(value=False)
self.do_mock_discord = tk.BooleanVar(value=False)
self.hide_npc_kills = tk.BooleanVar(value=True)
self.app = None
self.monitor_thread = None
self.kill_queue = queue.Queue()
self.recent_kills: list[str] = []
self.max_kills_to_display = 100
self.create_widgets()
self.update_kill_display()
def create_widgets(self):
# Create main frame
main_frame = ttk.Frame(self.root, padding="10")
main_frame.pack(fill=tk.BOTH, expand=True)
# Configuration section
config_frame = ttk.LabelFrame(main_frame, text="Configuration", padding="10")
config_frame.pack(fill=tk.X, pady=5)
# Log file path
ttk.Label(config_frame, text="Log File Path:").grid(
row=0, column=0, sticky=tk.W, pady=2
)
ttk.Entry(config_frame, textvariable=self.log_path, width=50).grid(
row=0, column=1, sticky=tk.W + tk.E, pady=2, padx=5
)
ttk.Button(config_frame, text="Browse", command=self.browse_log_file).grid(
row=0, column=2, pady=2
)
# Discord webhook URL
ttk.Label(config_frame, text="Discord Webhook URL:").grid(
row=1, column=0, sticky=tk.W, pady=2
)
ttk.Entry(config_frame, textvariable=self.webhook_url, width=50).grid(
row=1, column=1, sticky=tk.W + tk.E, pady=2, padx=5
)
# Checkboxes
ttk.Checkbutton(
config_frame, text="Process existing log entries", variable=self.do_backread
).grid(row=2, column=0, columnspan=2, sticky=tk.W, pady=2)
ttk.Checkbutton(
config_frame,
text="Mock Discord (don't send actual messages)",
variable=self.do_mock_discord,
).grid(row=3, column=0, columnspan=2, sticky=tk.W, pady=2)
ttk.Checkbutton(
config_frame,
text="Hide NPC kills (_NPC_ in name)",
variable=self.hide_npc_kills,
).grid(row=4, column=0, columnspan=2, sticky=tk.W, pady=2)
# Control buttons
control_frame = ttk.Frame(main_frame)
control_frame.pack(fill=tk.X, pady=5)
self.start_button = ttk.Button(
control_frame, text="Start Monitoring", command=self.start_monitoring
)
self.start_button.pack(side=tk.LEFT, padx=5)
self.stop_button = ttk.Button(
control_frame,
text="Stop Monitoring",
command=self.stop_monitoring,
state=tk.DISABLED,
)
self.stop_button.pack(side=tk.LEFT, padx=5)
# Status indicator
self.status_var = tk.StringVar(value="Not monitoring")
status_frame = ttk.Frame(main_frame)
status_frame.pack(fill=tk.X, pady=5)
ttk.Label(status_frame, text="Status:").pack(side=tk.LEFT)
self.status_label = ttk.Label(
status_frame, textvariable=self.status_var, foreground="red"
)
self.status_label.pack(side=tk.LEFT, padx=5)
# Kill events display
kills_frame = ttk.LabelFrame(
main_frame, text="Recent Kill Events", padding="10"
)
kills_frame.pack(fill=tk.BOTH, expand=True, pady=5)
self.kill_display = scrolledtext.ScrolledText(
kills_frame, wrap=tk.WORD, height=10
)
self.kill_display.pack(fill=tk.BOTH, expand=True)
self.kill_display.config(state=tk.DISABLED)
def browse_log_file(self):
filename = filedialog.askopenfilename(
title="Select Star Citizen Log File",
filetypes=[("Log Files", "*.log"), ("All Files", "*.*")],
)
if filename:
self.log_path.set(filename)
def start_monitoring(self):
if self.monitor_thread and self.monitor_thread.is_alive():
return
try:
log_path = Path(self.log_path.get())
webhook_url = self.webhook_url.get()
self.app = App(
log_file_path=log_path,
discord_webhook_url=webhook_url,
do_backread=self.do_backread.get(),
do_mock_discord=self.do_mock_discord.get(),
hide_npc_kills=self.hide_npc_kills.get(),
kill_callback=self.on_kill,
)
self.monitor_thread = threading.Thread(
target=self.app.process_lines, daemon=True
)
self.monitor_thread.start()
self.status_var.set("Monitoring active")
self.status_label.config(foreground="green")
self.start_button.config(state=tk.DISABLED)
self.stop_button.config(state=tk.NORMAL)
except Exception as e:
self.status_var.set(f"Error: {str(e)}")
self.status_label.config(foreground="red")
def stop_monitoring(self):
if self.app:
self.app.stop()
if self.monitor_thread:
self.monitor_thread.join(timeout=1.0)
self.status_var.set("Monitoring stopped")
self.status_label.config(foreground="red")
self.start_button.config(state=tk.NORMAL)
self.stop_button.config(state=tk.DISABLED)
def on_kill(self, kill: Kill, message: str):
# No need to filter here as it's already filtered in the App class
self.kill_queue.put((kill, message))
def update_kill_display(self):
while not self.kill_queue.empty():
try:
kill, message = self.kill_queue.get_nowait()
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
# Simplified message with only nicknames and emojis
if kill.victim == kill.killer:
formatted_message = f"[{timestamp}] 🤦 {kill.victim} (self-kill)\n"
else:
formatted_message = (
f"[{timestamp}] 💀 {kill.victim} killed by 🔫 {kill.killer}\n"
)
self.recent_kills.append(formatted_message)
if len(self.recent_kills) > self.max_kills_to_display:
self.recent_kills.pop(0)
self.kill_display.config(state=tk.NORMAL)
self.kill_display.delete(1.0, tk.END)
for msg in self.recent_kills:
self.kill_display.insert(tk.END, msg)
self.kill_display.config(state=tk.DISABLED)
self.kill_display.see(tk.END)
except queue.Empty:
break
self.root.after(100, self.update_kill_display)
def main():
try:
if DEBUG is True:
app = App(Path("./Game.log"), DISCORD_WEBHOOK_URL, do_mock_discord=True, do_backread=True)
# Check if we should run in GUI mode or CLI mode
import sys
if len(sys.argv) > 1 and sys.argv[1] == "--cli":
# CLI mode
if 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:
app = App(LOG_FILE_PATH, DISCORD_WEBHOOK_URL)
app.process_lines()
# GUI mode
root = tk.Tk()
app = DeathLogGUI(root)
root.mainloop()
except KeyboardInterrupt:
print("\nMonitoring stopped")

View File

@ -4,5 +4,6 @@ version = "0.1.0"
description = "Add your description here"
requires-python = ">=3.13"
dependencies = [
"pyinstaller>=6.13.0",
"requests>=2.32.3",
]

104
uv.lock generated
View File

@ -2,6 +2,15 @@ version = 1
revision = 1
requires-python = ">=3.13"
[[package]]
name = "altgraph"
version = "0.17.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/de/a8/7145824cf0b9e3c28046520480f207df47e927df83aa9555fb47f8505922/altgraph-0.17.4.tar.gz", hash = "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406", size = 48418 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/3f/3bc3f1d83f6e4a7fcb834d3720544ca597590425be5ba9db032b2bf322a2/altgraph-0.17.4-py2.py3-none-any.whl", hash = "sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff", size = 21212 },
]
[[package]]
name = "certifi"
version = "2025.4.26"
@ -42,6 +51,86 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
]
[[package]]
name = "macholib"
version = "1.16.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "altgraph" },
]
sdist = { url = "https://files.pythonhosted.org/packages/95/ee/af1a3842bdd5902ce133bd246eb7ffd4375c38642aeb5dc0ae3a0329dfa2/macholib-1.16.3.tar.gz", hash = "sha256:07ae9e15e8e4cd9a788013d81f5908b3609aa76f9b1421bae9c4d7606ec86a30", size = 59309 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/5d/c059c180c84f7962db0aeae7c3b9303ed1d73d76f2bfbc32bc231c8be314/macholib-1.16.3-py2.py3-none-any.whl", hash = "sha256:0e315d7583d38b8c77e815b1ecbdbf504a8258d8b3e17b61165c6feb60d18f2c", size = 38094 },
]
[[package]]
name = "packaging"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 },
]
[[package]]
name = "pefile"
version = "2023.2.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/78/c5/3b3c62223f72e2360737fd2a57c30e5b2adecd85e70276879609a7403334/pefile-2023.2.7.tar.gz", hash = "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc", size = 74854 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/55/26/d0ad8b448476d0a1e8d3ea5622dc77b916db84c6aa3cb1e1c0965af948fc/pefile-2023.2.7-py3-none-any.whl", hash = "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6", size = 71791 },
]
[[package]]
name = "pyinstaller"
version = "6.13.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "altgraph" },
{ name = "macholib", marker = "sys_platform == 'darwin'" },
{ name = "packaging" },
{ name = "pefile", marker = "sys_platform == 'win32'" },
{ name = "pyinstaller-hooks-contrib" },
{ name = "pywin32-ctypes", marker = "sys_platform == 'win32'" },
{ name = "setuptools" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a8/b1/2949fe6d3874e961898ca5cfc1bf2cf13bdeea488b302e74a745bc28c8ba/pyinstaller-6.13.0.tar.gz", hash = "sha256:38911feec2c5e215e5159a7e66fdb12400168bd116143b54a8a7a37f08733456", size = 4276427 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b4/02/d1a347d35b1b627da1e148159e617576555619ac3bb8bbd5fed661fc7bb5/pyinstaller-6.13.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:aa404f0b02cd57948098055e76ee190b8e65ccf7a2a3f048e5000f668317069f", size = 1001923 },
{ url = "https://files.pythonhosted.org/packages/6b/80/6da39f7aeac65c9ca5afad0fac37887d75fdfd480178a7077c9d30b0704c/pyinstaller-6.13.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:92efcf2f09e78f07b568c5cb7ed48c9940f5dad627af4b49bede6320fab2a06e", size = 718135 },
{ url = "https://files.pythonhosted.org/packages/05/2c/d21d31f780a489609e7bf6385c0f7635238dc98b37cba8645b53322b7450/pyinstaller-6.13.0-py3-none-manylinux2014_i686.whl", hash = "sha256:9f82f113c463f012faa0e323d952ca30a6f922685d9636e754bd3a256c7ed200", size = 728543 },
{ url = "https://files.pythonhosted.org/packages/e1/20/e6ca87bbed6c0163533195707f820f05e10b8da1223fc6972cfe3c3c50c7/pyinstaller-6.13.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:db0e7945ebe276f604eb7c36e536479556ab32853412095e19172a5ec8fca1c5", size = 726868 },
{ url = "https://files.pythonhosted.org/packages/20/d5/53b19285f8817ab6c4b07c570208d62606bab0e5a049d50c93710a1d9dc6/pyinstaller-6.13.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:92fe7337c5aa08d42b38d7a79614492cb571489f2cb0a8f91dc9ef9ccbe01ed3", size = 725037 },
{ url = "https://files.pythonhosted.org/packages/84/5b/08e0b305ba71e6d7cb247e27d714da7536895b0283132d74d249bf662366/pyinstaller-6.13.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:bc09795f5954135dd4486c1535650958c8218acb954f43860e4b05fb515a21c0", size = 721027 },
{ url = "https://files.pythonhosted.org/packages/1f/9c/d8d0a7120103471be8dbe1c5419542aa794b9b9ec2ef628b542f9e6f9ef0/pyinstaller-6.13.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:589937548d34978c568cfdc39f31cf386f45202bc27fdb8facb989c79dfb4c02", size = 723443 },
{ url = "https://files.pythonhosted.org/packages/52/c7/8a9d81569dda2352068ecc6ee779d5feff6729569dd1b4ffd1236ecd38fe/pyinstaller-6.13.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:b7260832f7501ba1d2ce1834d4cddc0f2b94315282bc89c59333433715015447", size = 719915 },
{ url = "https://files.pythonhosted.org/packages/d5/e6/cccadb02b90198c7ed4ffb8bc34d420efb72b996f47cbd4738067a602d65/pyinstaller-6.13.0-py3-none-win32.whl", hash = "sha256:80c568848529635aa7ca46d8d525f68486d53e03f68b7bb5eba2c88d742e302c", size = 1294997 },
{ url = "https://files.pythonhosted.org/packages/1a/06/15cbe0e25d1e73d5b981fa41ff0bb02b15e924e30b8c61256f4a28c4c837/pyinstaller-6.13.0-py3-none-win_amd64.whl", hash = "sha256:8d4296236b85aae570379488c2da833b28828b17c57c2cc21fccd7e3811fe372", size = 1352714 },
{ url = "https://files.pythonhosted.org/packages/83/ef/74379298d46e7caa6aa7ceccc865106d3d4b15ac487ffdda2a35bfb6fe79/pyinstaller-6.13.0-py3-none-win_arm64.whl", hash = "sha256:d9f21d56ca2443aa6a1e255e7ad285c76453893a454105abe1b4d45e92bb9a20", size = 1293589 },
]
[[package]]
name = "pyinstaller-hooks-contrib"
version = "2025.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
{ name = "setuptools" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e3/94/dfc5c7903306211798f990e6794c2eb7b8685ac487b26979e9255790419c/pyinstaller_hooks_contrib-2025.4.tar.gz", hash = "sha256:5ce1afd1997b03e70f546207031cfdf2782030aabacc102190677059e2856446", size = 162628 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d3/e1/ed48c7074145898e5c5b0072e87be975c5bd6a1d0f08c27a1daa7064fca0/pyinstaller_hooks_contrib-2025.4-py3-none-any.whl", hash = "sha256:6c2d73269b4c484eb40051fc1acee0beb113c2cfb3b37437b8394faae6f0d072", size = 434451 },
]
[[package]]
name = "pywin32-ctypes"
version = "0.2.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756 },
]
[[package]]
name = "requests"
version = "2.32.3"
@ -62,11 +151,24 @@ name = "sc-death-log"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "pyinstaller" },
{ name = "requests" },
]
[package.metadata]
requires-dist = [{ name = "requests", specifier = ">=2.32.3" }]
requires-dist = [
{ name = "pyinstaller", specifier = ">=6.13.0" },
{ name = "requests", specifier = ">=2.32.3" },
]
[[package]]
name = "setuptools"
version = "80.8.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8d/d2/ec1acaaff45caed5c2dedb33b67055ba9d4e96b091094df90762e60135fe/setuptools-80.8.0.tar.gz", hash = "sha256:49f7af965996f26d43c8ae34539c8d99c5042fbff34302ea151eaa9c207cd257", size = 1319720 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/58/29/93c53c098d301132196c3238c312825324740851d77a8500a2462c0fd888/setuptools-80.8.0-py3-none-any.whl", hash = "sha256:95a60484590d24103af13b686121328cc2736bee85de8936383111e421b9edc0", size = 1201470 },
]
[[package]]
name = "urllib3"