Einführung

[!CAUTION] Hinweis zur Aktualität und Genauigkeit dieser Dokumentation

Ich bemühe mich, diese Entwicklerdokumentation so aktuell wie möglich zu halten. Da ich gleichzeitig am eigentlichen Projekt weiterarbeite und die Dokumentation allein pflege, ist es nicht immer möglich, jede Änderung sofort nachzutragen.

Bitte beachte daher Folgendes:

  • Kennzeichnung veralteter Kapitel Kapitel die veraltet sind werden nach Möglichkeit gekennzeichnet. Dennoch kann es vorkommen, dass auch unmarkierte Abschnitte nicht mehr dem aktuellen Stand des Codes entsprechen.
  • Nutzung von KI: Einige Textpassagen wurden mit Hilfe von KI erstellt. Ich prüfe diese, kann aber nicht garantieren, dass keine Fehler enthalten sind.
  • Allgemeine Fehler wie Tippfehler, ungenaue Beschreibungen oder veraltete Codebeispiele können vorkommen – auch in manuell verfassten und unmarkierten Abschnitten.
  • Deine Hilfe zählt, wenn dir falsche oder veraltete Kapitel auffallen, melde es gerne über ein GitHub Issue – das hilft, die Dokumentation für alle zu verbessern.

Sieh diese Dokumentation als hilfreichen Leitfaden, aber wenn Dokumentation und Quellcode widersprechen hat der Quellcode immer recht.

Willkommen zur Entwicklerdokumentation des Streaming Tools – ein Projekt, das TikTok-Events mit Minecraft verbindet.

Diese Dokumentation richtet sich an Entwickler, die verstehen wollen:

  • Wie das System intern funktioniert
  • Wie die Daten fließen (TikTok → Verarbeitung → Minecraft)
  • Wie man das Projekt erweitert (eigene Plugins schreiben, Features anpassen)

Wo Starten?

ProfilEmpfohlener Einstieg
Python-Grundlagen vorhandenStart mit Grundkonzepte, dann Setup
Erweiterte-Python-Kenntnisse vorhandenSystem-ÜberblickPython in diesem Projekt
System erweitern & anpassen mit PythonDirekt zu Plugin-Entwicklung oder Eigenen $ Befehle
Debuggen / TroubleshootingDebugging & Troubleshooting

[!NOTE] Wenn du Kenntnise in anderen Programmiersprachen als Python hast, dann kannst du gerne direkt zu Plugin ohne Python erstellen gehen. Trozdem solltest du dir einige Kapitel anschauen um besser zu verstehen wie das System Arbeitet. Dies hilft dir weiter auch wenn du kein Python kannst. Ich Empfehle dir auch Eigenes Plugin erstellen anzuschauen auch wenn dies Primär für Python geschrieben ist gibt es dort einige Infos die wichtig sind.


Umfang & Fokus

Das Projekt umfasst etwa 3.000–4.000 Zeilen Python-Code. Wir analysieren nicht jede einzelne Zeile – das würde sinnlos sein.

Stattdessen konzentrieren wir uns auf:

  • Architektonische Kernkomponenten – Wie ist das System aufgebaut?
  • Datenflüsse – Wie bewegen sich Daten durch das System?
  • Muster & Best Practices – Worauf solltest du achten?
  • Praktische Anwendung – Wie schreibe ich mein eigenes Plugin?

Im Code selbst findest du zusätzliche Kommentare, die als Wegweiser für spezifische Details dienen.


Voraussetzungen

[!NOTE] Diese Dokumentation erklärt die Funktion des Streaming-Tool-System – nicht aber die Python-Grundlagen selbst.

Du brauchst folgende Vorkenntnisse:

  • Grund-Begriffe der Python-Programmierung (Funktionen, Klassen, Loops)
  • Command-Line / Terminal Navigation
  • Dateisystem Grundverständnis

Wenn dir diese Konzepte neu sind: Bearbeite zuerst einen Anfänger-Python-Kurs, z.B. Python.org Tutorial oder Codecademy Python Course. Da wir diese Grundlagen hier voraussetzen, sparen wir Platz für Tiefe statt für Wiederholung.

Zusätzlich brauchst du:

  • Python 3.12+
  • Git (zum Klonen des Repositories)
  • Einen Editor oder IDE (VS Code, PyCharm, etc.)

Alles Setup erforderlich wird in Lokale Entwicklung einrichten Schritt-für-Schritt erklärt.


Struktur der Dokumentation

00 GRUNDLAGEN
  ├─ Fundamentals & Concepts (Was ist dieses System?)
  └─ Local Development Setup (Wie richte ich das auf?)

01 SYSTEM-ÜBERBLICK
  ├─ Wie das System zusammenarbeitet
  ├─ Daten von TikTok empfangen
  ├─ Daten verarbeiten
  └─ Daten an Minecraft senden

02 PYTHON & EVENTS (Kernlogik)
  ├─ Python in diesem Projekt
  ├─ Die main.py Datei
  ├─ TikTok-Client & Event-Handler
  │  └─ Gift-Events, Follow-Events, Like-Events
  └─ Threading & Warteschlangen

03 MINECRAFT-INTEGRATION
  ├─ Von Event zum Command
  ├─ Die actions.mca Datei
  ├─ Mapping-Logik
  └─ mcfunction Dateien

04 SYSTEM-ARCHITEKTUR
  ├─ Modulare Struktur
  ├─ Control Methods (DCS vs. ICS)
  ├─ PLUGIN_REGISTRY
  └─ Integration mit Streaming-Software

05 PLUGIN-ENTWICKLUNG
  ├─ Plugin-Struktur & Setup
  ├─ Events & Webhooks
  ├─ Config & Datenspeicherung
  ├─ GUI mit pywebview
  ├─ Inter-Plugin-Kommunikation
  └─ Fehlerbehandlung & Best Practices

06 ADVANCED
  └─ Debugging & Troubleshooting (Problem-Lösung)

ANHANG
  ├─ Projektstruktur
  ├─ Config-Details
  ├─ Update-Prozess
  └─ Glossar (Begriffe erklärt)

Die Dokumentation ist progressiv aufgebaut: Jedes Kapitel baut auf den vorherigen auf. Du kannst aber jederzeit zu Themen springen, die dich interessieren.


Empfohlene Lesereihenfolge

Option 1: Kompletter Durchgang (beste Vorbereitung)

  1. Grundkonzepte
  2. Setup
  3. System-Überblick
  4. Event-Verarbeitung
  5. Minecraft-Integration
  6. System-Architektur
  7. Plugin-Entwicklung

Option 2: Schnelleinstieg für Erfahrene

  1. Grundkonzepte (10 Minuten)
  2. Plugin-Entwicklung
  3. Dann: Spezifische Kapitel je nach Interesse

Der Anhang

Zusätzlich zu den Hauptkapiteln gibt es einen Anhang. Der Anhang enthält:

  • Projektstruktur: Dateien & Ordner im Detail
  • Config-Details: Konfigurationsdatei verstehen & erweitern
  • Update-Prozess: Wie Updates funktionieren (für Maintainer)
  • Glossar: Alle Fachbegriffe erklärt

Der Anhang ist ein Nachschlagewerk – du musst ihn nicht linear lesen.


Wie du diese Dokumentation am besten nutzt

  1. Finde dein Level: Anfänger? Dann start mit Grundkonzepte & Begriffe.
  2. Lies progressiv: Kapitel bauen aufeinander auf.
  3. Überspringe nichts leicht: Wenn etwas unklar ist, geh zurück zu vorherigen Kapiteln.
  4. Nutze das Glossar: Unbekannte Begriffe? Glossar.
  5. Experimentiere: Lesen ist wichtig, aber selbst programmieren ist entscheidend.

Code-Beispiele in dieser Dokumentation

Wo wir Code-Beispiele zeigen, nutzen wir diese Formatierung:

# Beispiel-Python-Code
from TikTokLive import TikTokLiveClient

client = TikTokLiveClient(unique_id="my_account")

Info-Blöcke

[!TIP] Praktische Empfehlung, Trick oder Best Practice, um die Arbeit zu erleichtern oder zu verbessern.

[!NOTE] Ergänzende Information oder Hintergrundwissen. Nicht kritisch, aber oft hilfreich zum besseren Verständnis.

[!IMPORTANT] Pflicht-Information oder harte Voraussetzung. Muss zwingend beachtet werden, damit etwas funktioniert. Nicht optional.

[!WARNING] Hier besonders aufmerksam sein! Kann zu Fehlern, Problemen oder unerwartetem Verhalten führen, aber nichts wird dauerhaft beschädigt.

[!CAUTION] Kritischer Hinweis! Falsche Anwendung kann zu Datenverlust, Systemfehlern oder nicht rückgängig machbaren Schäden führen.


Fehler gefunden? Fragen?

Diese Dokumentation wird ständig verbessert. Falls du:

  • Fehler findest → GitHub Issue öffnen
  • Etwas unklar ist → Frag eine KI oder andere Entwickler
  • Ideen hast → Feedback geben im Repository

Viel Erfolg!

Du bist bereit. Starten wir:

Grundkonzepte & Begriffe

oder

Lokale Entwicklung einrichten

Grundkonzepte & Begriffe

Bevor wir in die Architektur und den Code einsteigen, müssen wir die Kernideen verstehen. Dieses Kapitel behandelt nur die essentiellen Konzepte – alles andere wird in den Folgenden Kapiteln detailliert erklärt.


Was ist dieses Streaming Tool?

Das Streaming Tool verbindet TikTok-Events mit Minecraft – in Echtzeit.

Der Ablauf:

Zuschauer auf TikTok
    ↓
sendet Gift / folgt / liked
    ↓
TikTok-Server benachrichtigt das Tool
    ↓
Das Tool führt einen Minecraft-Befehl aus
    ↓
Etwas passiert zwischen Minecraft Spiel

Praktisches Beispiel:

  • Streamer ist live auf TikTok
  • Zuschauer sendet ein Gift
  • Minecraft-Server erhält: /say "Danke für das Gift!"
  • Alle Spieler sehen die Nachricht

Kernidee: Events → Aktionen

Das ganze System basiert auf einem einfachen Prinzip:

EVENT (Etwas ist passiert)
    ↓
VERARBEITUNG (Was bedeutet das?)
    ↓
AKTION (Was soll passieren?)

Drei zentrale Konzepte:

1. Events – Das Eingangssignal

Ein Event bedeutet: Etwas ist passiert.

Beispiele:

  • User sendet ein Gift
  • User folgt dem Kanal
  • User liked den Stream

[!NOTE] Events sind strukturierte Daten – sie haben Eigenschaften wie "Wer?", "Wann?", "Was?", "Wie viel?". Mehr in Wie das System Arbeitet.

2. Verarbeitung – Das "verstehen"

Das Programm nimmt das Event und fragt:

  • "Was ist das für ein Event?" (Gift / Follow / Like?)
  • "Von wem kommt es?"
  • "Ist das wichtig?"

3. Warteschlange (Queue) – Die Ordnung

Wenn 100 Events gleichzeitig eintreffen, können sie nicht alle sofort an Minecraft gehen. Stattdessen:

Events kommen an → werden in Queue gelegt → nacheinander verarbeitet

Die Queue verhindert Chaos und Überlastung.

[!NOTE] Stell dir den Supermarkt vor: Alle Kunden stellen sich an der Kasse an. Einer nach dem anderen wird bedient – faire, geordnete Verarbeitung.


Die 3 Phasen (Überblick)

Das System arbeitet in 3 Phasen (Details in Wie das System Arbeitet):

PhaseWas passiertErgebnis
1. EmpfangenTikTok-Events werden empfangenStrukturierte Event-Daten
2. VerarbeitenEvents werden klassifiziertKlare Kategorien (Gift/Follow/Like/...)
3. AusführenBefehl wird an Minecraft gesendetMinecraft-Aktion wird ausgelöst

Konfiguration vs Code

Eine wichtige Idee: Konfiguration ist getrennt vom Code.

Das bedeutet:

  • Code: Die Logik "wie funktioniert das Tool?"
  • Konfiguration: Die Regeln "was soll passieren, wenn geschieht?"

Du kannst neue Aktionen hinzufügen ohne eine einzige Codezeile zu ändern – nur durch Bearbeitung der Konfiguration.

[!NOTE] Details dazu in späteren Kapiteln


Zusammenfassung: Die Grundidee

Das System funktioniert nach diesem Muster:

EVENT eintreffen
    ↓
In Warteschlange legen
    ↓
Eine nach dem anderen verarbeiten
    ↓
Entsprechende Aktion ausführen
    ↓
Minecraft reagiert

Das ist das ganze Konzept. Alles andere sind Details und Implementierungen.


Wo geht es weiter?

Jetzt, da du die Grundidee kennst:

Danach werden wir tiefer in Code, Konfiguration und spezifische Features gehen.

[!NOTE] Keine Sorge, wenn nicht alles sofort klar ist. Jede Idee wird später mit Beispielen und Details erklärt!

Lokale Entwicklung einrichten

In diesem Kapitel richtest du deine lokale Entwicklungsumgebung ein. Das ist eine einmalige Aufgabe – danach kannst du direkt mit der Entwicklung starten.


Anforderungen

Windows

  • Windows 10 oder 11
  • Python 3.12+ (empfohlen Python 3.12)
  • Git (zum Repository klonen)
  • PowerShell 7

Java & Minecraft Server

  • Java-Laufzeitumgebung: Der Ordner tools/Java/ muss vorhanden sein (entweder mit Java-Dateien oder deiner eigenen Java-Installation).
  • Minecraft Server: Die Datei tools/server.jar wird benötigt (Minecraft-Server-JAR-Datei).

[!IMPORTANT] Stelle sicher, dass sich sowohl der Ordner tools/Java/ als auch die Datei tools/server.jar im Projekt befinden. Ohne diese Komponenten funktionieren einige Features (z.B. Minecraft-Integration) nicht!

macOS / Linux

  • Python 3.12+ (empfohlen Python 3.12)
  • Git
  • Bash oder ähnliche Shell

[!WARNING] Das Projekt wird hauptsächlich auf Windows entwickelt. Auf macOS/Linux können einzelne Features eingeschränkt sein. In Zukünftigen Version soll allerdings macOS/Linux voll untersützt werden.


Schritt 1: Python installieren

Windows

  1. Besuche https://www.python.org/downloads/
  2. Lade die aktuelle Python 3.X herunter (Windows x86-64)
  3. Wichtig: Beim Installer aktiviere die Option "Add Python to PATH"
  4. Klicke "Install Now"

Überprüfung: Öffne PowerShell und tippe:

python --version

Du solltest sehen: Python 3.12.x (oder deine Version)

macOS

brew install python@3.X

Linux (Ubuntu/Debian)

sudo apt update
sudo apt install python3.X python3.X-venv

Schritt 2: Git installieren

Windows

  1. Besuche https://git-scm.com/download/win oder https://desktop.github.com/download/
  2. Lade den Installer herunter
  3. Führe den Installer aus (Standard-Einstellungen sind OK)

Überprüfung:

git --version

macOS

brew install git

Linux

sudo apt install git

Schritt 3: Repository klonen

Das Repository ist dein lokales Projekt. Du speicherst deine Arbeitung dort.

[!TIP] Prüfe nach dem Klonen, ob der Ordner tools/Java/ und die Datei tools/server.jar vorhanden sind. Falls nicht, musst du sie selbst hinzufügen.

Es gibt zwei Möglichkeiten:

Option 1: Mit Git klonen (empfohlen)

git clone https://github.com/TechnikLey/Streaming_Tool.git
cd Streaming_Tool

Das braucht ein paar Sekunden. Danach solltest du alle Dateien lokal haben.

Vorteil: Du kannst später Updates mit git pull herunterladen.


Option 2: Als ZIP-Datei herunterladen

Falls du Git nicht verwenden möchtest:

  1. Besuche das Repository: https://github.com/TechnikLey/Streaming_Tool

  2. Klicke auf den grünen Button "Code" (oben rechts)

  3. Wähle "Download ZIP"

  4. Entpacke die ZIP-Datei an einem geeigneten Ort (z.B. C:\Users\dein_name\Streaming_Tool)

  5. Öffne PowerShell und navigiere dorthin:

cd C:\Users\dein_name\Streaming_Tool

Hinweis: Mit ZIP-Methode musst du später Updates manuell herunterladen (nicht ideal für Entwicklung).


Schritt 4: Virtuelle Python-Umgebung erstellen

Eine virtuelle Umgebung ist wie ein isolierter Python "Container" für dieses Projekt. Das verhindert Konflikte mit anderen Python-Projekten auf deinem System.

[!NOTE] Virtuelle Umgebung ist optional!

Falls du anfängst, Fehler bekommst, oder es dir zu kompliziert ist: Du kannst auch direkt ohne venv arbeiten (siehe unten). Wir empfehlen venv für erfahrenere Entwickler, aber es ist nicht zwingend notwendig.


Option A: Mit virtueller Umgebung (empfohlen)

Windows

python -m venv venv
.\venv\Scripts\Activate.ps1

macOS / Linux

python3.12 -m venv venv
source venv/bin/activate

[!NOTE] Falls die Aktivierung in PowerShell fehler macht, führe erst aus:

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

Überprüfung: Deine Shell-Zeile sollte sich verändern und (venv) zeigen:

(venv) C:\Streaming_Tool>

Das bedeutet, du bist in der virtuellen Umgebung. ✓

Vorteile:

  • ✓ Saubere Isolation (keine Konflikte mit anderen Projekten)
  • ✓ Professionelle Entwicklung
  • ✓ Einfache Deinstallation (einfach venv-Ordner löschen)

Nachteile:

  • ✗ Ein paar Extra-Schritte beim Setup

Option B: Ohne virtuelle Umgebung (schneller, aber weniger sauber)

Falls du venv überspringen möchtest, gehe direkt zu Schritt 5: Abhängigkeiten installieren.

Führe dann aus:

pip install -r requirements.txt

Die Pakete werden global auf deinem System installiert.

Vorteile:

  • ✓ Schneller Setup
  • ✓ Weniger zu verstehen

Nachteile:

  • ✗ Pakete auch von anderen Projekten beeinflussen sich gegenseitig
  • ✗ Schwerer zu deinstallieren/aufzuräumen
  • ✗ Nicht ideal für mehrere Python-Projekte

Schritt 5: Abhängigkeiten installieren

Jetzt installieren wir alle Python-Pakete, die das Projekt braucht:

[!NOTE] Für die Minecraft-Integration wird zusätzlich eine Java-Laufzeitumgebung im Ordner tools/Java/ und die Datei tools/server.jar benötigt.

pip install --upgrade pip
pip install -r requirements.txt

Das braucht ein paar Minuten. Beispiel-Output:

Collecting TikTokLive==0.8.0
  Using cached ...
Collecting Flask==3.0.0
  Using cached ...
...
Successfully installed TikTokLive-0.8.0 Flask-3.0.0 ...

Was wird installiert?

  • TikTokLive: Die API zum Empfangen von TikTok-Events
  • Flask: Ein Webframework (für Webhooks & GUIs)
  • pywebview: Für GUIs (Desktop-Fenster)
  • PyYAML: Für Config-Dateien
  • Und mehr...

Schritt 6: Konfiguration initialisieren

Das System braucht eine config.yaml, um zu starten.

Falls sie noch nicht existiert:

cp defaults/config.default.yaml config/config.yaml

Das erstellt eine Standard-Konfiguration.

Grundlegende Einstellungen

Öffne config/config.yaml in deinem Editor (z.B. VS Code) und passe an:

# Dein TikTok Live-Account Name
tiktok_user: "dein_tiktok_name"

# Ports für verschiedene Plugins
MinecraftServerAPI:
  Enable: true
  WebServerPort: 8888
  
Timer:
  Enable: true
  StartTime: 10
  
WinCounter:
  Enable: true
  
DeathCounter:
  Enable: true

[!TIP] Du musst nicht alles verstehen. Wichtig ist nur:

  1. tiktok_user: Dein TikTok-Kanal-Name
  2. Die Ports dürfen nicht in Benutzung sein

Schritt 7: Erstes Plugin erstellen (Optional)

Falls du schnell ein kleines Test-Plugin erstellen möchtest:

# Windows
.\create_plugin.ps1

# macOS / Linux
bash create_plugin.ps1

Das Skript fragt dich nach einer Plugin-ID. Gib z.B. testplugin ein.

Danach findest du unter src/plugins/testplugin/ dein Plugin mit Boilerplate-Code.


Virtuelle Umgebung aktivieren / deaktivieren

Beim nächsten Mal (Virtual Environment reaktivieren)

Windows:

.\venv\Scripts\Activate.ps1

macOS / Linux:

source venv/bin/activate

Wenn du fertig bist (Virtual Environment verlassen)

deactivate

Häufige Probleme & Lösungen

ProblemLösung
python: command not foundPython nicht im PATH. Neu installieren und "Add Python to PATH" aktivieren
ModuleNotFoundError: No module named 'TikTokLive'pip install -r requirements.txt noch nicht ausgeführt
Port 8080 already in useAndere Anwendung nutzt den Port. In config.yaml einen anderen Port wählen
Permission denied (macOS/Linux)chmod +x create_plugin.ps1 ausführen
venv aktiviert sich nicht (PowerShell)Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser ausführen
pip: The term 'pip' is not recognized as a name of a cmdlet, function, script file, or executable program.Führe python -m pip aus dies kann helfen

VS Code Setup (Empfohlen)

Falls du VS Code nutzt (kostenlos, sehr beliebt), kannst du es so konfigurieren:

  1. Lade VS Code herunter: https://code.visualstudio.com/

  2. Öffne den Streaming_Tool-Ordner: File → Open Folder

  3. Installiere diese Erweiterungen (Extensions):

    • Python (Microsoft)
    • Pylance (Microsoft)
  4. Wende den Python-Interpreter auf deine venv an:

    • Ctrl+Shift+P → "Python: Select Interpreter"
    • Wähle ./venv/bin/python (oder .\venv\Scripts\python.exe auf Windows)
    • Allternativ direkt Python auswählen wenn du kein venv nutzt

Das war's! Jetzt hast du Syntax-Highlighting, Autocomplete und Debugging.

[!TIP] Solltest du bei einem der Schritte auf Probleme stoßen, dann schau im Internet oder auf YouTube nach einer Lösung.


Nächste Schritte

Du hast jetzt:

✓ Python installiert
✓ Das Repository geklont
✓ Dependencies installiert
✓ Die Config angepasst
✓ Basis-Tests durchgeführt

Du bist ready!

Nächstes Kapitel: Wie das System zusammenarbeitet

Dort lernen wir, wie die einzelnen Komponenten zusammenspielen.

Wie das System zusammenarbeitet

In diesem Kapitel verstehst du die Architektur des Streaming Tools – also wie die einzelnen Komponenten zusammenpassen und Daten von TikTok bis zu Minecraft fließen.


Das große Bild: Der Datenflusss

Das System arbeitet nach einem klaren, dreiphasigen Muster:

┌─────────────────────────────────────────────────────────────────┐
│                    DIE 3 PHASEN DES SYSTEMS                     │
└─────────────────────────────────────────────────────────────────┘

Phase 1: EMPFANGEN
    TikTok-Events
        ↓
    TikTokLive API
        ↓
    "User XY hat gefolgt"

           ↓ ↓ ↓

Phase 2: VERARBEITEN
    Event analysieren
        ↓
    Daten filtern & sortieren
        ↓
    "Ist das ein Follow? Von wem? Wann?"

           ↓ ↓ ↓

Phase 3: AUSFÜHREN
    Aktion triggern
        ↓
    RCON-Befehl senden
        ↓
    Minecraft führt aus

Jede Phase hat eine klare Aufgabe:

PhaseAufgabeWer macht es?
1. EmpfangenDaten von TikTok-Servern abholenTikTokLive API (in unserem Programm)
2. VerarbeitenEvents verstehen & dokumentierenPython-Skripte analyzieren die Rohdaten
3. AusführenBefehl an Minecraft schickenRCON über Netzwerk-Verbindung

Phase 1: Daten von TikTok empfangen

Das Problem: Wie kriegen wir überhaupt mit, wenn jemand auf TikTok ein Gift sendet?

TikTok hat keine offene offizielle API für Entwickler. Deswegen verwendet wir die TikTokLive-Bibliothek

Das Programm verbindet sich mit TikToks Servern und lauscht.

Sobald eine Aktion passiert (Gift, Follow, Like), bekommen wir eine Nachricht. Diese Nachricht ist wie folgt strukturiert:

{
  "Event": "Gift",
  "Von": "BenutzerName",
  "GiftID": "12345",
}

Was passiert danach? Die Daten gehen direkt zur nächsten Phase → Verarbeiten

[!NOTE] Für Anfänger: Stell dir vor, du liest eine Nachricht. Der Text ist noch nicht sortiert – du muss zuerst verstehen, wer schreibt, was wird geschrieben, wann wurde es geschrieben.


Phase 2: Events verarbeiten und analysieren

Das Problem: Die Rohdaten von TikTok sind unstrukturiert. Wir müssen sie klassifizieren und strukturieren, bevor wir damit etwas anfangen können.

Das Programm nimmt das empfangene Event und fragt:

  • "Was ist passiert?" (Gift / Follow / Like / Etc.)
  • "Wer war es?" (Benutzername, User-ID)
  • "Wie viel?" (Anzahl der Gifts, Größe des Gifts)
  • "Was soll jetzt passieren?" (Welche Minecraft-Aktion triggert das?)

Das System kategorisiert Events intern und speichert wichtige Metadaten. Diese werden dann in eine Warteschlange (Queue) gelegt, damit sie nacheinander verarbeitet werden können.

Warum eine Warteschlange?

Wenn 100 Zuschauer gleichzeitig Gifts senden, können nicht alle Events gleichzeitig an Minecraft gehen. Das würde den Server überlasten. Stattdessen werden sie aufgereiht und der Reihe nach abgearbeitet.

[!NOTE] Für Anfänger: Denk an den Supermarkt-Kassierer. Wenn 10 Leute gleichzeitig kommen, stellt man sich an. Einer nach dem anderen zahlt. Die Warteschlange sorgt für Ordnung.


Phase 3: Daten an Minecraft senden

Das Problem: Wie sagen wir dem Minecraft-Server, was passieren soll?

Minecraft hat ein Remote Control System namens RCON (Remote Console). Das ist wie eine Fernbedienung für den Minecraft-Server.

Über RCON können wir Befehle senden:

  • /say "Danke für das Gift!"
  • /give @s diamond 5
  • /function my_namespace:special_event

Das Programm:

  1. Bestimmt, welcher Befehl nötig ist (basierend auf dem Event)
  2. Sendet ihn über RCON an Minecraft
  3. Der Minecraft-Server führt den Befehl aus

Das alles passiert in Echtzeit – normalerweise in Millisekunden.


Zusammenhang: So hängt alles zusammen

Das Wichtigste: Die 3 Phasen sind voneinander abhängig:

(1) Empfangen  →  (2) Verarbeiten  →  (3) Ausführen
      ↓               ↓                   ↓
  TikTok API     Python-Logik         RCON-Befehl

Wenn eine Phase ausfällt, funktioniert die ganze Kette nicht mehr:

Wenn Phase ausfälltFolge
1 brichtKeine Events von TikTok → Nichts passiert
2 brichtEvents werden nicht verstanden → Falsche Aktion oder gar keine
3 brichtBefehl erreicht Minecraft nicht → Game hat keine Reaktion

Nächste Schritte

Jede dieser 3 Phasen wird in diesem Kapitel im Detail erklärt:

Wenn du die Grundidee verstanden hast, kannst du die Unterkapitel lesen, um tiefer einzusteigen.

[!NOTE] Konzepte: Grundideen (Events, Queue, etc.) wurden bereits kurz in Grundkonzepte & Begriffe erklärt. Dieses Kapitel zeigt die Architektur.

Code & Details: Wie Handler funktionieren (@client.on), wie actions.mca geschrieben wird, Control Methods (DCS/ICS), etc. – das kommt in späteren Kapiteln.

[!TIP] Falls dir die Architektur noch nicht 100% klar ist: Das ist völlig normal. Viele Konzepte werden in den kommenden Kapiteln noch konkreter.

Daten von TikTok empfangen (Phase 1)

In diesem Kapitel verstehst du, wie das System überhaupt mitbekommt, dass etwas auf TikTok passiert. Das ist die erste Komponente der Datenkette.


Das Problem: Wie hören wir TikTok ab?

Die Herausforderung:

TikTok ist eine geschlossene Plattform. Es gibt keine offizielle, kostenlose API für Streamer, die in Echtzeit Events liefert. Wir müssen kreativ werden.

Wir nutzen also Reverse Engineering – wir beobachten, wie die TikTok-App selbst mit den Servern kommuniziert, und bauen darauf auf.


Lösung: Die TikTokLive API

Wir verwenden die TikTokLive Bibliothek (Open Source), die genau das macht: Sie imitiert die TikTok-App und empfängt Events direkt.

Wie funktioniert TikTokLive?

TikTok-Server
    ↓
    ├─→ (sendet Events zu Millionen Apps)
    ├─→ Offizielle TikTok App
    ├─→ Andere Live-Tool-Apps
    └─→ TikTokLive Library (unser Tool)
         ↓
    Wir sehen die Events LIVE

Die Bibliothek:

  1. Verbindet sich mit TikTok-Servern (wie die Mobile App)
  2. Hört zu auf dem WebSocket-Stream
  3. Empfängt Events (Gifts, Follows, Likes) in Echtzeit
  4. Übergebe sie an unser Python-Programm

Was sind Events?

Ein Event ist eine strukturierte Nachricht, die TikTok sendet:

Event-Typ:    "Gift"
Benutzer:     "streamer_fan_123"
Gift-Anzahl:   5
Gift-Wert:     1000 Coins

Jeden dieser Daten kommt an. Das Programm kennt die Struktur und weiß, wie man sie ausliest.


Womit verbinden wir uns? WebSocket vs HTTP

WebSocket (wir nutzen das)

┌─ Verbindung ─┐
│  offen und   │  Beide Seiten können jederzeit
│  persistent  │  Daten senden. Perfekt für Echtzeit.
└──────────────┘

Vorteile:

  • ✓ Echtzeit (sofortige Events)
  • ✓ Effizient (ständig offen, nicht ständig neue Verbindungen)
  • ✓ Bidirektional (wir können auch Daten zurückschicken)

Nachteil:

  • ✗ Komplexer als HTTP

HTTP (alte Alternative)

Wir fragen:  "Gibt es neue Events?"
Server:      "Ja hier!"
Wir fragen:  "Gibt es neue Events?"
Server:      "Nein"
Wir fragen:  "Gibt es neue Events?"
...

Problem: Ständiges Fragen ist ineffizient. Das ist wie ständig zu fragen "Bist du jetzt wach?" statt zu warten, bis dich die Person anruft.


Der interne Ablauf: Wie Events ins Programm kommen

1. START: Programm startet
   ↓
2. CONNECT: Programm verbindet sich mit TikTok via WebSocket
   "Hallo TikTok, es ist dein Client"
   ↓
3. LISTEN: WebSocket bleibt offen
   Programm wartet auf Events
   ↓
4. EVENT KOMMT AN: User sendet Gift
   TikTok sendet: { "type": "gift", "user": "xyz", ... }
   ↓
5. EVENT EMPFANGEN: Unser Programm nimmt es entgegen
   ↓
6. EVENT WEITERGELEITET: An Phase 2 (Verarbeitung)


Zusammenfassung Phase 1

Was passiert hier:

  • TikTokLive Library verbindet sich mit TikTok
  • Sie empfängt Events als strukturierte Daten
  • Diese werden sofort an Phase 2 weitergeleitet

Was du wissen solltest:

  • Events kommen LIVE (WebSocket, nicht HTTP)
  • Ein Event hat Struktur: Typ, User, Daten
  • Das Konzept von Events wurde bereits in Grundkonzepte & Begriffe erklärt

Was nicht passiert hier:

  • Wir analysieren Events nicht (das ist Phase 2)
  • Wir senden nichts an Minecraft (das ist Phase 3)
  • Wir speichern Events nicht dauerhaft

[!TIP] Die TikTokLive Bibliothek ist nicht unbediengt sehr zuverlässig, aber es ist die beste Kostenlose Option. Gerne kannst du dir selbst mal alternativen anschauen.

Die Code-Implementierung der TikTokLive Bibliothek lernst du in Python in diesem Projekt.


Nächstes Kapitel: Events verarbeiten – Jetzt schauen wir, was das Programm MIT den Events macht.

Events verarbeiten (Phase 2)

Jetzt sind die Rohdaten angekommen. Aber sind sie auch nutzbar? In diesem Kapitel lernst du, wie das System Events "versteht" und für die nächste Phase vorbereitet.


Das Problem: Daten sind roh

Von Phase 1 bekommen wir Rohdata von TikTok:

{
  "type": "gift",
  "user": { "id": 12345, "name": "xyz" },
  "gift": { "id": 5, "count": 10, "repeatCount": 1 },
}

Fragen, die wir beantworten müssen:

  • ✓ "Was ist das für ein Event?" (Gift / Follow / Like?)
  • ✓ "Von wem kommt es?" (Welcher User?)
  • ✓ "Wie viel ist es?" (Wert, Anzahl, Menge?)
  • ✓ "Ist es wichtig?" (Sollte das Spiel reagieren?)
  • ✓ "Was ist die nächste Aktion?" (Welcher Befehl soll an Minecraft?)

Lösung: Das Event-Processing-System

Schritt 1: Klassifizieren

Das Programm sortiert das Event in eine Kategorie:

Rohdaten ankommen
    ↓
"Ist das ein Gift?"
    ↓
"Ja → Das ist die 'Gift'-Kategorie"
    ↓
Event wird als "GiftEvent" registriert

Das System kennt verschiedene Typen:

TypBeispiel
GiftUser sendet 5x Gifts
FollowUser folgt dem Kanal
LikeUser liked einen Stream
ShareUser teilt den Stream
CommentUser kommentiert

Schritt 2: Daten extrahieren

Aus den Rohdata werden die wichtigen Informationen herausgezogen:

Rohdaten: {
  "type": "gift",
  "user": { "id": 12345, "name": "streamer_fan_xyz", ... },
  "gift": { "id": 5, "count": 3, ... }
}

        ↓ [EXTRAHIERT]

Strukturierte Daten:
├─ Event-Typ: "gift"
├─ User-Name: "streamer_fan_xyz"
├─ Gift-Anzahl: 3

Nur die Daten, die interessant sind, werden behalten.

Schritt 3: In Warteschlange legen

Das Problem: Wenn 100 Zuschauer gleichzeitig Gifts senden, können nicht alle gleichzeitig an Minecraft gehen.

Die Lösung: Eine Queue (Warteschlange) – wie schon in Grundkonzepte erklärt.

Die Queue sorgt dafür, dass alles der Reihe nach verarbeitet wird – fair und ordentlich.


Der interne Ablauf: Wie Events verarbeitet werden

1. EVENT ANKOMMT von Phase 1
   ↓
2. KLASSIFIZIEREN
   "Das ist ein Gift"
   ↓
3. DATEN EXTRAHIEREN
   User = "xyz", Anzahl = 5
   ↓
4. IN QUEUE ENQUEUE
   Eintrag in der Warteschlange
   ↓
5. QUEUE ARBEITET AB
   Ein Event nach dem anderen
   ↓
6. AN PHASE 3 WEITERGEBEN
   "Gift von xyz, 5x → Minecraft!"

Mehrere Events gleichzeitig (Concurrency)

Das System muss mit vielen Events gleichzeitig umgehen. Die Queue ist die Lösung.

Die Queue ist normalerweise sehr klein – Events werden sofort weiterverarbeitet. Sie ist kein Speicher, sondern eine Warteschlange.


Spezial-Filter: Nicht alle Events sind gleich

Das System kann Events priorisieren – Gifts sind wichtiger als Likes, zum Beispiel.

Diese Prioritäten werden in der Konfiguration festgelegt, nicht im Code.


Fehlerszenarien: Was kann schiefgehen?

ProblemFolgeLösung
Event-Struktur unbekanntKlassifizierung fehlgeschlagenError-Log, Event wird verworfen
Queue läuft überSpeicher-Problem (sehr selten)Ältere Events löschen
Zu viele Events pro SekundeBacklog aufgebautMinecraft braucht länger zu reagieren
Event kommt beschädigt anDaten-Parse-FehlerValidierung im System, fehlerhafte Events ignoriert

Zusammenfassung Phase 2

Was passiert hier:

  • Raw Events werden klassifiziert
  • Daten werden extrahiert und strukturiert
  • Events werden in eine Warteschlange gelegt
  • Queue verarbeitet sie der Reihe nach

Was du wissen solltest:

  • Die Queue sorgt für Ordnung
  • Mehrere Events werden nacheinander, nicht parallel verarbeitet

Was nicht passiert hier:

  • Wir schreiben Events nicht zur Festplatte (optional)
  • Wir senden nichts an Minecraft (nächste Phase)
  • Wir zeigen nichts in der GUI (wird separat gemacht)

Nächstes Kapitel: Daten an Minecraft senden – Jetzt wird's konkret: Der Befehl geht ans Spiel!

Daten an Minecraft senden (Phase 3)

Jetzt kommt die Aktion: Das Event ist verarbeitet, und jetzt muss Minecraft es ausführen. Wie funktioniert diese Kommunikation?


Das Problem: Wie sagen wir Minecraft, was es tun soll?

Minecraft ist ein Spiel auf einem Server. Wir sind ein Python-Programm. Wie kommunizieren wir?

Optionen:

  • ✗ Direkt in den Spiele-Code schreiben? (Zu kompliziert, tausende Zeilen Code nötig)
  • ✗ Chat-Nachrichten? (Funktioniert technisch nicht)
  • Commands / Befehle! (Das ist die Lösung)

Minecraft hat eine Console, wo man Befehle eingeben kann:

/say "Hallo Welt!"
/give @s diamond 5
/function my_namespace:special_event
/tp @a 100 64 200

Wir müssen diese Befehle nur remote (von außen) ausführen können. Dafür gibt es RCON.


Lösung: RCON (Remote Console)

Was ist RCON?

RCON = Remote Console – eine Art "Fernbedienung" für Minecraft-Server.

┌─────────────────────────────┐
│  Unser Python-Programm      │
│  "Mach /say Danke!"         │
└──────────────┬──────────────┘
               │ RCON-Befehl über Netzwerk
               ↓
┌──────────────────────────────┐
│  Minecraft-Server            │
│  Console empfängt Befehl     │
│  Führt /say aus              │
│  Result: Chat zeigt "Danke!" │
└──────────────────────────────┘

Wie RCON funktioniert

  1. Verbindung aufbauen

    Programm → "Ich bin Admin, hier ist das Passwort"
    Server → Authentifizierung OK, Verbindung offen
    
  2. Befehl schicken

    Programm → "/say User XY hat gefolgt!"
    Server → Befehl wird ausgeführt (im Spiel sichtbar)
    
  3. Bestätigung (optional)

    Server → Kurze Antwort (normalerweise leer oder "ok")
    Programm → Befehl verarbeitet
    

[!NOTE] Vereinfachte Darstellung!

Die obige Erklärung ist zur Veranschaulichung vereinfacht. In der Realität:

  • RCON sendet keine aussagekräftigen Antworten wie "5 Spieler haben eine Nachricht bekommen"
  • Die Antwort ist normalerweise leer
  • Wir wissen nicht, ob etwas Ausgeführt wurde
  • Wir wissen nicht, welcher Fehler auftrat, wenn der Befehl fehlschlägt

Das ist eine Limitation von RCON. Für die Praxis: Wir senden den Befehl, hoffen, dass es funktioniert.

RCON ist TCP/IP

RCON nutzt das TCP/IP-Protokoll – das ist das Internet-Protokoll, das auch E-Mail, Webseiten, etc. nutzen.

Unser Programm      Netzwerk          Minecraft-Server
(Port XYZ)  ←------TCP/IP----→  (üblicherweise Port 25575)

Wichtige Details:

  • Standardverhalten: RCON verbindet sich normalerweise für jeden Befehl einzeln – Verbindung auf, Befehl senden, Verbindung zu.
  • Das Streaming Tool: Verwendet eine persistent Connection – die Verbindung bleibt offen. Das muss aber speziell eingerichtet werden und ist nicht der Standard.
  • Zuverlässigkeit: RCON ist nicht garantiert zuverlässig. Befehle können verloren gehen, Verbindungen können abbrechen. Das ist eine bekannte Limitation.

[!WARNING] RCON hat Limitations:

  • Nicht garantiert zuverlässig (Befehle können verloren gehen)
  • Persistent connections müssen manuell implementiert werden
  • Keine aussagekräftigen Fehler-Responses
  • Wenn ein Befehl fehlschlägt, wissen wir es oft nicht

Das Streaming Tool handhabt das durch:

  • Eigenes persistentes Connection Management
  • Logging und Retry-Mechanismen
  • Hoffen, dass es funktioniert

Von Event zu Minecraft-Befehl

Beispiel: User sendet 5 Gifts

1. PHASE 1: Event kommt an
   TikTok: "User 'Streamer_Fan_123' hat 5x Gift gesendet"
   
   ↓
   
2. PHASE 2: Event wird verarbeitet
   System: "Das ist ein Gift-Event, 5x"
   Daten extrahiert: User = "Streamer_Fan_123", Menge = 5
   In Queue gelegt → Wartet bis es dran ist
   
   ↓
   
3. PHASE 3: Befehl wird an Minecraft gesendet
   System: "Welcher Befehl soll für 5x Gift ausgelöst werden?"
   (aus `actions.mca` nachschlagen)
   
   Befehl: /say "Danke an Streamer_Fan_123 für 5x Gift! "
   
   RCON: Befehl an Minecraft senden
   
   ↓
   
4. RESULT: Minecraft führt aus
   Server: Chat zeigt "Danke an Streamer_Fan_123 für 5x Gift! "
   Alle Spieler sehen es

Der interne Ablauf: Wie Befehle abgearbeitet werden

1. EVENT AUS QUEUE NEHMEN
   Gift-Event von Streamer_Fan_123
   
   ↓
   
2. ACTION NACHSCHLAGEN
   "Was soll bei einem Gift passieren?"
   → actions.mca Datei prüfen
   → Befehl: /say "Danke für Gift!"
   
   ↓
   
3. RCON-VERBINDUNG PRÜFEN
   Passwort: OK
   Port 25575: Erreichbar
   Server: Erreichbar
   
   ↓
   
4. BEFEHL SENDEN
   Befehl: /say "Danke für Gift!"
   
   ↓
   
5. HOFFEN DAS BEFEHL ANKOMMT
   Server: ...
   
   ↓
   
6. LOGGING & ABSCHLUSS
   Log: "Gift-Event verarbeitet, Befehl erfolgreich"
   Event ist erledigt

Fehlerszenarien: Was kann schiefgehen?

ProblemFolgeLösung
RCON-Server nicht erreichbarBefehle können nicht gesendet werdenMinecraft-Server prüfen, Firewall-Einstellungen
Falsches PasswortAuthentifizierung fehlgeschlagenPasswort in config.yaml überprüfen
Falscher PortVerbindung schlägt fehlStandard: 25575, in config.yaml überprüfen
Befehl syntaktisch falschMinecraft lehnt abBefehl in actions.mca überprüfen
Zu viele Befehle pro SekundeMinecraft kann nicht alle abarbeitenBacklog aufgebaut, Spieler sieht zeitverzögerte Reaktion
Minecraft-Server crashtRCON-Verbindung bricht abAuto-Reconnect nach Neustart

Zusammenfassung Phase 3

Was passiert hier:

  • Event wird aus der Queue genommen
  • Passender Minecraft-Befehl wird ermittelt
  • Befehl wird über RCON an Minecraft gesendet
  • Minecraft führt den Befehl aus
  • Befehle werden nacheinander abgearbeitet (nicht parallel)

Was nicht passiert hier:

  • Wir ändern den Spiele-Code (würde nicht funktionieren)
  • Wir speichern Events (das ist eine separate Komponente)
  • Wir zeigen etwas in der TikTok-App (das ist nicht möglich)

[!NOTE] RCON ist synchron und blockiert. Bei vielen Befehlen pro Sekunde kann es zu Bottlenecks kommen. Deshalb verwenden manche Tools Minecraft-Funktionen (.mcfunction) für Batch-Operationen. (Macht das Streaming Tool auch kommen wir später drauf zu sprechen)


Das ganze System zusammen

Die 3 Phasen funktionieren nur zusammen:

Phase 1           Phase 2              Phase 3
EMPFANGEN    →   VERARBEITEN     →    AUSFÜHREN
  ↓                 ↓                    ↓
TikTokLive       Queue-System         RCON-Befehle
  ↓                 ↓                    ↓
Events rein      Events sortieren     Minecraft reagiert

Wenn eine Phase ausfällt:

  • Keine Phase 1 → Keine Events
  • Keine Phase 2 → Befehle sind falsch
  • Keine Phase 3 → Minecraft reagiert nicht

[!TIP] Wenn du verstehst, wie diese 3 Phasen zusammenhängen, verstehst du das ganze System. Die Details (wie RCON genau funktioniert, wie man Befehle schreibt, etc.) lernst du in den nächsten Kapiteln.


Nächstes Kapitel: Python in diesem Projekt – Jetzt schauen wir, wie der Code diese 3 Phasen umsetzt.

Eigenes Plugin erstellen

[!WARNING] Aktuell nutzen alle Plugins die config.yaml. In Zukunft wird sich das jedoch ändern, da diese Datei ausschließlich für das Hauptprogramm vorgesehen sein soll. Die genaue Umsetzung wird in zukünftigen Updates eingeführt, und das Kapitel wird entsprechend angepasst. Behalte dies im Hinterkopf und halte Ausschau nach Änderungen.

Du kannst bereits jetzt eine eigene config.yaml im Plugin-Ordner erstellen und diese für Einstellungen nutzen. So sparst du dir später Anpassungen am Code, wenn die globale config.yaml nicht mehr für Plugins verwendet werden darf.

Was ist ein Plugin?

Ein Plugin ist ein unabhängiges Python-Programm, das sich in das Streaming Tool integriert. Jedes Plugin:

  • Läuft als separater Prozess (in der Registry registriert)
  • Kommuniziert über DCS (HTTP) mit anderen Plugins
  • Hat optional eine GUI (ICS) mit pywebview
  • Wird zentral in config.yaml konfiguriert
  • Hat Zugriff auf Events via Webhook

Built-in Plugins (Beispiele):

  • DeathCounter: Zählt Tode, sendet an Minecraft
  • LikeGoal: Verwaltet Like-Ziele
  • Timer: Countdown-Timer
  • WinCounter: Siege zählen

(Alle Build-in Plugins untersützen ICS)

Plugin-Lifecycle

1. Plugin-Ordner erstellen (mit create_plugin.ps1) (plugins/myPlugin/)
   ↓
2. main.py schreiben (HTTP-Server, Event-Handler)
   ↓
3. In registry.py registrieren (PLUGIN_REGISTRY)
   ↓
4. config.yaml bearbeiten (Benutzerkonfiguration)
   ↓
5. In Start.py laden (Prozess starten)
   ↓
6. Events kommen via /webhook Endpoint
   ↓
7. Plugin verarbeitet, sendet Befehle

Roadmap dieses Kapitels

  1. Plugin-Struktur verstehen (Ordner, Dateien, Config)
  2. HTTP-Server mit Flask erstellen (Events empfangen)
  3. Minecraft-Befehle senden (RCON-Kommunikation)
  4. Datenspeicherung & Konfiguration (Benutzerdaten)
  5. GUI mit pywebview (Visuelles Interface optional)
  6. Zwischen Plugins kommunizieren (HTTP + Fehlerbehandlung)
  7. Best Practices & Fehlerbehandlung (Production-Ready)

Start: Plugin-Struktur & Setup

  • Logging in logs/ Ordner
  • Typische Fehler vermeiden
  • Resource Management (Memory & Thread Leaks)

Programmiersprachen: Python oder etwas anderes?

Ein Plugin muss nicht zwingend in Python geschrieben sein. Du kannst es grundsätzlich in jeder Programmiersprache entwickeln. Mehr dazu erfährst du im Kapitel Plugin erstellen ohne Python.

Aber Achtung: Mit Python brauchst du etwa 20 Zeilen Code für die Basis. Mit anderen Sprachen (Java, C++, Rust, etc.) können das schnell mehrere hundert Zeilen werden, da du vieles selbst implementieren musst.

In Zukunft werden möglicherweise weitere Programmiersprachen direkt unterstützt. Python bleibt jedoch die primär unterstützte Sprache: mit den meisten Hilfestellungen, Integrationen und regelmäßigen Updates.

Los geht's!

Bist du bereit? Dann starten wir mit Plugin-Aufbau und Struktur.

Nach dem Durcharbeiten dieser Kapitel wirst du verstehen:

  • Wie Plugins technisch aufgebaut sind
  • Wie Events dich erreichen und wie du darauf reagierst
  • Wie du Konfiguration und Daten verwaltest
  • Wie du GUIs mit pywebview baust
  • Wie Plugins sich untereinander austauschen
  • Wie du dein Plugin stabil hältst

Die konkrete Umsetzung und die kreative Nutzung dieser Tools – das ist dein Part!

Plugin-Struktur & Setup

Aufbau eines Plugins

Jedes Plugin ist ein isoliertes Python-Programm mit Standardstruktur. Benefits:

  • Boilerplate-Code schon vorbereitet
  • Core-Module für häufige Tasks (Config, Logging, Pfade)
  • Automatische Registrierung in PLUGIN_REGISTRY

Ordnerstruktur

Automatisch-erstellt per PowerShell-Skript (create_plugin.ps1):

src/plugins/
└── my_plugin/
    ├── main.py           ← Plugin-Kern
    ├── README.md        
    └── version.txt       

Plugin erstellen: 2 Schritte

Wenn du das PowerShell-Skript create_plugin.ps1 ausführst, fragt es dich nach dem Namen deines Plugins. Danach erstellt es automatisch die komplette Ordnerstruktur für dich. Diese sieht dann so aus:

.
├── dein_plugin_name
│   ├── main.py
│   ├── README.md
│   └── version.txt

Der neue Ordner wird unter src/plugins/ erstellt und mit dem Namen benannt, den du während der Erstellung angegeben hast.

Die einzelnen Dateien

main.py – Das Herz deines Plugins

Das ist die wichtigste Datei! Hier schreibst du die eigentliche Logik deines Plugins. Wenn du mit create_plugin.ps1 ein Plugin erstellst, bekommst du automatisch einen Basis-Code eingefügt. Der sieht ungefähr so aus:

from core import load_config, parse_args, get_root_dir, get_base_dir, get_base_file, register_plugin, AppConfig
import sys

BASE_DIR = get_base_dir()
ROOT_DIR = get_root_dir()
CONFIG_FILE = ROOT_DIR / "config" / "config.yaml"
DATA_DIR = ROOT_DIR / "data"
MAIN_FILE = get_base_file()
args = parse_args()

cfg = load_config(CONFIG_FILE)

gui_hidden = args.gui_hidden
register_only = args.register_only

if register_only:
    register_plugin(AppConfig(
        name="test",
        path=MAIN_FILE,
        enable=True,
        level=4,
        ics=False
    ))
    sys.exit(0)

[!TIP] Wenn du die config.yaml Datei direkt im Plugin Ordner nutzen willst dann ersetze:

CONFIG_FILE = ROOT_DIR / "config" / "config.yaml"

Durch diesen Code:

CONFIG_FILE = BASE_DIR / "config.yaml"
CONFIG_FILE.touch(exist_ok=True) # Legt die Datei an wenn du es nicht schon selbst gemacht hast.

Was passiert da genau?

Importe
Du importierst Funktionen und Klassen aus dem core-Modul. Das erspart dir viel Schreibarbeit:

  • load_config: Lädt die Konfigurationsdatei
  • parse_args: Liest Command-Line-Argumente
  • get_root_dir, get_base_dir, get_base_file: Ermitteln wichtige Verzeichnisse und Dateipfade
  • register_plugin: Registriert dein Plugin
  • AppConfig: Eine Klasse, die die Plugin-Konfiguration speichert

Wichtige Pfade einrichten

BASE_DIR = get_base_dir()           # Der Basis-Ordner der Anwendung
ROOT_DIR = get_root_dir()           # Der Wurzelpfad, zwei Ebenen über BASE_DIR
CONFIG_FILE = ROOT_DIR / "config" / "config.yaml"  # Pfad zur Konfiguration
DATA_DIR = ROOT_DIR / "data"        # Ordner für User-Daten
MAIN_FILE = get_base_file()         # Der Pfad zur main.exe (main.py im dev Ordner)

Diese Variablen brauchst du später in deinem Code – zum Beispiel um Dateien zu speichern oder die Config zu laden.

Startargumente auslesen

args = parse_args()
gui_hidden = args.gui_hidden        # War die --gui-hidden Flag gesetzt?
register_only = args.register_only  # War die --register-only Flag gesetzt?

Das Programm kann dein Plugin mit bestimmten Flags starten:

  • --gui-hidden: Die GUI wird versteckt gestartet
  • --register-only: Das Plugin wird nur registriert, aber nicht ausgeführt

Plugin registrieren (wenn --register-only gesetzt ist)

if register_only:
    register_plugin(AppConfig(
        name="test",
        path=MAIN_FILE,
        enable=True,
        level=4,
        ics=False
    ))
    sys.exit(0)

Wenn das Plugin nur registriert werden soll, passiert folgendes:

  • name: Der Name deines Plugins (z.B. "test")

  • path: Der Pfad zur ausführbaren Datei

  • enable: True = Plugin ist aktiv, False = Plugin ist deaktiviert
    Tipp: Statt True/False zu hardcodieren, kannst du auch Config-Werte nutzen:

    enable=cfg.get("custom_name", {}).get("enable", True)
    

    So können Nutzer dein Plugin in der config.yaml ein- und ausschalten!

  • level: Bestimmt ab wann das Terminal sichtbar ist (abhängig vom log_level in der config.yaml):

    • Level 0: Deaktiviert alles (sollte nicht verwendet werden)
    • Level 1: Terminal sichtbar ab log_level: 1
    • Level 2: Hauptprogramme (log_level: 2)
    • Level 3: Hintergrund-Dienste (z.B. Checks, Listener)
    • Level 4: Debug/Entwicklung
    • Level 5: Überschreibt andere Einstellungen (sollte nicht verwendet werden)
  • ics: Interface Control System – gibt an, ob die GUI unterstützt wird

    • True = GUI wird unterstützt
    • False = GUI wird NICHT unterstützt (Direct Control System / DCS)

Nach der Registrierung endet das Programm mit sys.exit(0).


[!WARNING] Plugin-Registrierung: Reihenfolge und Laufzeitbeschränkung

Der Aufruf von register_plugin(...) muss so früh wie möglich im Programm erfolgen. Vor der Registrierung darf kein ausführbarer Code stehen – ausgenommen sind:

  • Imports
  • Konfigurations- und Pfaddefinitionen
  • Argument-Parsing (z. B. parse_args())

Nicht erlaubt vor der Registrierung:

  • Logik mit Seiteneffekten
  • Netzwerkzugriffe oder Dateizugriffe
  • Initialisierungen mit externer Abhängigkeit
  • print-Ausgaben oder sonstige I/O-Operationen

Hintergrund: Die Registrierungsroutine läuft in einer strikt begrenzten Umgebung und kann andernfalls fehlschlagen.


Unmittelbares Beenden erforderlich

Nach erfolgreichem Aufruf von register_plugin(...) muss das Programm sofort mit sys.exit(0) beendet werden.

if register_only:
    register_plugin(AppConfig(
        name="test",
        path=MAIN_FILE,
        enable=True,
        level=4,
        ics=False
    ))
    sys.exit(0)

Ohne dieses sofortige Beenden besteht die Gefahr, dass nachgelagerter Code ausgeführt wird, was die Registrierung beeinträchtigen oder ungültig machen kann.


Zeitlimit beachten

Der Registrierungsprozess hat ein hartes Zeitlimit von 5 Sekunden. Wird dieses überschritten, wird das Programm extern beendet.


Konfiguration laden

cfg = load_config(CONFIG_FILE)

Hier wird die config.yaml geladen. Sie enthält alle Einstellungen für dein Plugin. cfg ist jetzt ein Dictionary, auf das du zugreifen kannst:

# Beispiel: Config-Wert auslesen mit Standard-Wert
enable=cfg.get("custom_name", {}).get("enable", True)

So muss das dann in der config.yaml aussehen:

custom_name:
  enable: True

README.md – Dokumentiere dein Plugin

Diese Datei ist deine Chance, anderen Entwicklern zu zeigen, was dein Plugin macht. Schreib hier auf:

  • Was macht das Plugin? – Eine kurze Beschreibung
  • Anforderungen – Welche Voraussetzungen muss der Nutzer erfüllen?
  • Konfiguration – Welche Optionen gibt es in der config.yaml?
  • Verwendung – Wie wird das Plugin verwendet?

Ein gutes README macht es dir selbst und anderen später leichter!

version.txt – Die Versionsnummer

In dieser Datei speicherst du die aktuelle Version deines Plugins. Wenn du ein neues Plugin erstellst, steht dort standardmäßig:

v1.0.0

Wichtig: Halte dich an dieses Format! Es befolgt das Semantic Versioning-Standard:

  • v1.0.0 = Major.Minor.Patch
  • Major: Breaking Changes (große Änderungen)
  • Minor: Neue Features (rückwärts-kompatibel)
  • Patch: Bugfixes

Beispiele:

  • v1.0.0 → v1.0.1 (kleiner Bugfix)
  • v1.0.1 → v1.1.0 (neue Funktion hinzugefügt)
  • v1.1.0 → v2.0.0 (großer Umbau, nicht mehr kompatibel)

Plugins in anderen Programmiersprachen

Kann ich mein Plugin auch in Java, C++, JavaScript etc. schreiben? Ja, aber...

Wenn du Python verlässt, musst du viel selbst machen, das Python-Module dir abnehmen. Nur um dir eine Idee zu geben:

  • Config laden
  • Startargumente auslesen
  • Pfade ermitteln
  • Plugin registrieren
  • Fehlerbehandlung

Der Grundaufbau kann je nach Sprache schnell mehrere hundert Zeilen Code brauchen – deutlich mehr als die ~20 Zeilen Python oben.

Faustregel: Python ist der beste Startpunkt. Wenn du später mehr Performance brauchst, kannst du Performance-kritische Teile später immer noch optimieren oder in eine andere Sprache erstellen.


Nächstes Kapitel: Webhook-Events und Minecraft-Integration

Events empfangen: Webhook-System

Event-Datenfluss

Wenn in Minecraft etwas passiert (Spieler stirbt, Login, etc.), sendet das Spiel-Plugin einen HTTP-POST-Request an dein Plugin. Das nennt sich Webhook.

Flow:

Minecraft Event (player_death)
        ↓
Minecraft-Plugin prüft configServerAPI.yml
        ↓
Sendet HTTP-POST → http://localhost:PORT/webhook
        ↓
Dein Plugin (@app.route("/webhook")) empfängt
        ↓
Dein Plugin verarbeitet & reagiert

Verfügbare Events

[!NOTE] Eine Komplette Liste aller Events findest du in der configServerAPI.yml im Projekt. Hier ein paar Beispiele:

  • player_death
  • player_respawn
  • player_join
  • player_quit
  • block_break
  • entity_death

Webhook-Implementation: 3 Schritte

1. Flask Server starten (im Main Thread)

from flask import Flask, request
app = Flask(__name__)

def start_server():
    app.run(host="127.0.0.1", port=8001, debug=False, threaded=True)

import threading
srv = threading.Thread(target=start_server, daemon=True)
srv.start()

2. /webhook Endpoint definieren

@app.route("/webhook", methods=['POST'])
def webhook():
    try:
        data = request.json
        event_type = data.get("event")
        
        if event_type == "player_death":
            print(f"Spieler gestorben: {data.get('player')}")
        elif event_type == "block_break":
            print(f"Block abgebaut: {data.get('block')}")
        
        return {"status": "ok"}, 200
    except Exception as e:
        print(f"Webhook-Fehler: {e}")
        return {"status": "error"}, 500

3. In config.yaml registrieren

MyPlugin:
  Enable: true
  WebServerPort: 8001

und in der configServerAPI.yml:

  urls:
    - "http://localhost:7777/webhook"
    - "http://localhost:7878/webhook"
    - "http://localhost:7979/webhook"
    - "http://localhost:8080/webhook"
    - "http://localhost:8001/webhook" # Dein webhook

Komplettes Beispiel: DeathCounter

from flask import Flask, request
import json
from pathlib import Path

app = Flask(__name__)
DATA_DIR = Path(".") / "data"
DEATHS_FILE = DATA_DIR / "deathcount.json"

# Zähler laden
if DEATHS_FILE.exists():
    with open(DEATHS_FILE) as f:
        death_count = json.load(f).get("count", 0)
else:
    death_count = 0

@app.route("/")
def index():
    return f"<h1>Tod-Zähler: {death_count}</h1>"

@app.route("/webhook", methods=['POST'])
def webhook():
    global death_count
    data = request.json
    
    if data.get("event") == "player_death":
        death_count += 1
        # Speichern
        with open(DEATHS_FILE, "w") as f:
            json.dump({"count": death_count}, f)
        print(f"[+] Tode: {death_count}")
    
    return {"status": "ok"}, 200

if __name__ == '__main__':
    import threading
    threading.Thread(target=lambda: app.run(port=8001), daemon=True).start()
    input("Server läuft. Enter zum Stoppen...")

Webhook in dein Plugin integrieren

Um Webhooks zu empfangen, brauchst du einen HTTP-Server in deinem Plugin. Flask ist dafür perfekt geeignet:

from flask import Flask, request

app = Flask(__name__)

@app.route('/webhook', methods=['POST'])
def webhook():
    event_data = request.json
    event_type = event_data.get("event")
    
    if event_type == "player_death":
        print("Spieler ist gestorben!")
    elif event_type == "player_respawn":
        print("Spieler ist respawnt!")
    
    return "OK", 200

Das ist das Minimum. Dein Plugin muss:

  1. Flask starten und auf Port X lauschen
  2. Den /webhook Endpoint bereitstellen
  3. Den POST-Request verarbeiten und reagieren

Praktisches Beispiel: Der Timer

Der Timer-Plugin reagiert auf zwei Events:

@app.route('/webhook', methods=['POST'])
def webhook():
    ev = request.json.get("event")
    if ev == "player_death":
        window.evaluate_js("resetTimer(); setPaused(true);")
    elif ev == "player_respawn":
        window.evaluate_js("setPaused(false);")
    return "OK"

Wenn ein Spieler stirbt:

  • Timer wird zurückgesetzt
  • Timer wird pausiert

Wenn ein Spieler respawnt:

  • Timer läuft weiter

Die Event-Payload verstehen

Wenn ein Event ankommt, sieht der Request so aus:

{
    "load_type": "INGAME_GAMEPLAY",
    "event": "player_death",
    "message": "Player died from fall damage"
}

Je nach load_type kannst du unterschiedliche Verhaltensweisen programmieren:

  • INGAME_GAMEPLAY: Das Event ist vom laufenden Spiel
  • STARTUP: Das Event ist beim Server-Start
  • Andere: siehe configServerAPI.yml

Webhook-URL konfigurieren

Dein Plugin muss in der config.yaml eine Port-Einstellung haben. Die configServerAPI.yml ruft dann diese URL auf:

# config.yaml
MinecraftServerAPI:
  WebServerPortDeathCounter: 7979
  WebServerPortTimer: 7878

Die Webhook-URLs werden dann so konfiguriert:

# configServerAPI.yml
webhooks:
  urls:
    - "http://localhost:7979/webhook"    # DeathCounter
    - "http://localhost:7878/webhook"    # Timer

[!IMPORTANT] Der Port muss eindeutig sein! Kein anderes Plugin darf den gleichen Port nutzen.

Threading: Flask im Hintergrund starten

Ein wichtiger Punkt: Dein Plugin läuft nach der Registrierung weiter. Wenn du Flask direkt mit app.run() aufrufst, blockiert das alles danach.

Die Lösung: Flask in einem Thread starten:

import threading

def start_flask_server():
    app.run(host='127.0.0.1', port=7878, debug=False)

# Im Hauptprogramm:
flask_thread = threading.Thread(target=start_flask_server, daemon=True)
flask_thread.start()

# Dein restlicher Code läuft parallel weiter...

Mit daemon=True wird der Thread automatisch beendet, wenn dein Plugin beendet wird.

Fehlerbehandlung

Nicht immer funktioniert alles. Ein paar wichtige Punkte:

  1. Webhook funktioniert nicht?

    • Prüf, dass dein Port in config.yaml eingestellt ist
    • Prüf, dass die URL in configServerAPI.yml korrekt ist
    • Schau in deine Log-Datei
  2. Port schon in Verwendung?

    • Wähle einen anderen Port oder beende das andere Programm

Zusammenfassung

  • Webhooks sind HTTP POST-Requests von Minecraft zu deinem Plugin
  • Flask macht die Implementierung einfach
  • Der /webhook Endpoint verarbeitet eingehende Events
  • Der Port muss in config.yaml und configServerAPI.yml synchronisiert sein
  • Threading ist wichtig, damit der Flask-Server nicht alles blockiert

Nächstes Kapitel: Konfiguration und Datenspeicherung

Konfiguration & Datenspeicherung

Config + State Trennung

config.yaml (vom User bearbeitet):

  • Port-Nummern, Enable-Flags, UI-Theme
  • Menschen-lesbares YAML-Format
  • Wird beim Plugin-Start geladen

DATA_DIR:

  • Zähler-States, Window-Positionen, Benutzerdaten
  • JSON-Format (strukturiert)
  • Bleibt über Updates erhalten

Architektur

config/
└── config.yaml
    └── MyPlugin:
        ├── Enable: true
        ├── WebServerPort: 8001
        └── Theme: "dark"
                ↓
          [Plugin startet]
                ↓
data/
└── my_plugin_state.json
    └── {
          "counter": 42,
          "window_x": 100,
          "last_updated": 1234567890
        }

Config laden (3 Schritte)

Schritt 1: YAML öffnen

from pathlib import Path
import yaml

CONFIG_FILE = ROOT_DIR / "config" / "config.yaml"

# Mit Error-Handling
try:
    with open(CONFIG_FILE) as f:
        config = yaml.safe_load(f) or {}
except Exception as e:
    print(f"Config-Fehler: {e}")
    config = {}

Schritt 2: Werte mit Defaults auslesen

# Mit .get() bleibst du sicher vor KeyErrors
port = config.get("MyPlugin", {}).get("WebServerPort", 8001)
enabled = config.get("MyPlugin", {}).get("Enable", True)
theme = config.get("MyPlugin", {}).get("Theme", "light")

Schritt 3: Im Plugin nutzen

if enabled:
    app.run(port=port)
    set_theme(theme)

Praktisches Beispiel: Daten speichern

import json
from pathlib import Path

# Pfade
DATA_DIR = ROOT_DIR / "data"
STATE_FILE = DATA_DIR / "myplugin_state.json"

# Daten laden (oder Default)
def load_state():
    if STATE_FILE.exists():
        with open(STATE_FILE) as f:
            return json.load(f)
    return {"counter": 0, "wins": 0}

# Daten speichern
def save_state(state):
    with open(STATE_FILE, "w") as f:
        json.dump(state, f, indent=2)

# Nutzung
state = load_state()
state["counter"] += 1
save_state(state)
import yaml
from pathlib import Path

CONFIG_FILE = (ROOT_DIR / "config" / "config.yaml").resolve()

try:
    with CONFIG_FILE.open("r", encoding="utf-8") as f:
        cfg = yaml.safe_load(f) or {}
except Exception as e:
    print(f"Config konnte nicht geladen werden: {e}")
    cfg = {}

# Wert auslesen mit Default-Wert
port = cfg.get("MeinPlugin", {}).get("WebServerPort", 8000)
enable = cfg.get("MeinPlugin", {}).get("Enable", True)

Mit .get() Methode kannst du sicher Werte abfragen, ohne dass dein Programm crasht, wenn der Schlüssel fehlt.

Datenspeicherung: Wo speichere ich meine Daten?

Es gibt mehrere Möglichkeiten, je nach deinem Use-Case:

1. DATA_DIR (empfohlen für Plugin-spezifische Daten)

ROOT_DIR/
    data/
        window_state_timer.json
        window_state_deathcounter.json

Verwendung: Persistente Daten wie Zählerstände, Window-Positionen, Benutzervorgaben.

from pathlib import Path

DATA_DIR = ROOT_DIR / "data"
STATE_FILE = DATA_DIR / "my_plugin_state.json"

# Daten speichern
import json
state = {"counter": 42, "width": 800}
with STATE_FILE.open("w") as f:
    json.dump(state, f)

# Daten laden
if STATE_FILE.exists():
    with STATE_FILE.open("r") as f:
        state = json.load(f)
else:
    state = {"counter": 0, "width": 800}

Vorteil: Data-Ordner bleibt bei Updates erhalten.

2. Im Plugin-Ordner selbst (Alternative)

src/plugins/mein_plugin/
    main.py
    README.md
    version.txt
    plugin_data.json  ← direkt hier speichern
PLUGIN_DIR = get_base_dir()
MY_DATA_FILE = PLUGIN_DIR / "plugin_data.json"

3. runtime Ordner (nicht empfohlen)

build/release/core/runtime/
    meine_daten.json

WARNUNG: Der runtime Ordner wird bei jedem Update überschrieben! Nutze ihn nur für temporäre Daten, die neu generiert werden können.

Praktisches Beispiel: Window-Zustand speichern

Der Timer und DeathCounter speichern ihre Fenster-Größe/Position:

# Laden beim Start
def load_win_size():
    if STATE_FILE.exists():
        try:
            with STATE_FILE.open("r") as f:
                return json.load(f)
        except:
            pass
    return {"width": 400, "height": 200}

# Speichern wenn Fenster sich ändert
@app.route("/save_dims", methods=["POST"])
def save_dims():
    with STATE_FILE.open("w") as f:
        json.dump(request.json, f)
    return "OK"

Das Frontend ruft /save_dims auf, sobald der Nutzer das Fenster verschiebt oder vergrößert.

YAML vs. JSON

JSON: Schneller zum Laden, einfache Struktur

import json
data = json.load(f)

YAML: Lesbar für Menschen, komplexere Strukturen

import yaml
data = yaml.safe_load(f)

Empfehlung: Für Plugin-Daten JSON verwenden (schneller, weniger Dependencies). YAML nur für die Haupt-Config.

Daten zwischen Plugins teilen

Plugins können sich über Dateien austauschen:

# Plugin A speichert Daten
shared_data = {"wins": 10, "deaths": 3}
with (DATA_DIR / "shared_counter.json").open("w") as f:
    json.dump(shared_data, f)

# Plugin B liest Daten
with (DATA_DIR / "shared_counter.json").open("r") as f:
    data = json.load(f)
    print(data["wins"])

Aber: Achte auf Race Conditions! Wenn beide Plugins gleichzeitig schreiben, kann es zu Datenverlust kommen. Nutze stattdessen HTTP-Kommunikation (siehe nächstes Kapitel).


Zusammenfassung

  • Config laden: yaml.safe_load() aus config.yaml
  • Daten speichern: JSON in DATA_DIR für Persistenz
  • Fallback-Werte: Immer .get() mit Defaults verwenden
  • Teilen: über Dateien (Vorsicht Race Conditions) oder HTTP
  • Runtime Ordner: Nur für temporäre Daten, wird überschrieben

Nächstes Kapitel: GUI mit pywebview

GUI mit Flask + pywebview

GUI = Backend + Frontend

Plugins mit Benutzer-Interface brauchen zwei Layer:

  1. Flask-Backend (Python) → HTTP-Server, Verarbeitung, Daten
  2. HTML/CSS/JS Frontend → User-Interface, Visuals

pywebview: Öffnet ein Desktop-Fenster, der dann Flask-UI lädt.

Architektur

┌───────────────────────────────┐
│ pywebview Fenster             │
│ ┌─────────────────────────┐   │
│ │ HTML/CSS/JavaScript     │   │
│ │ (User sieht das)        │   │
│ └──────┬──────────────────┘   │
│        │ (HTTP GET/POST)      │
│ ┌──────▼──────────────────┐   │
│ │ Flask Backend           │   │
│ │ /api/status             │   │
│ │ /webhook                │   │
│ │ (Datenverarbeitung)     │   │
│ └─────────────────────────┘   │
└───────────────────────────────┘
     ↓ localhost:PORT

Minimal GUI: Setup (3 Schritte)

Schritt 1: HTML als String definieren

HTML = """
<!DOCTYPE html>
<html>
<head>
    <title>Mein Plugin</title>
    <style>
        body { background: #000; color: #0f0; font-size: 16px; }
        #counter { font-size: 48px; text-align: center; }
        button { padding: 10px 20px; margin: 10px; }
    </style>
</head>
<body>
    <h1>Counter: <span id="counter">0</span></h1>
    <button onclick="increment()">+1</button>
    <script>
        let count = 0;
        function increment() {
            count++;
            document.getElementById('counter').innerText = count;
            fetch('/api/count', { method: 'POST', 
                body: JSON.stringify({value: count}),
                headers: {'Content-Type': 'application/json'} });
        }
    </script>
</body>
</html>
"""

Schritt 2: Flask Route + HTML

from flask import Flask, render_template_string

app = Flask(__name__)

@app.route('/')
def index():
    return render_template_string(HTML)

@app.route('/api/count', methods=['POST'])
def update_count():
    data = request.json
    print(f"Counter: {data.get('value')}")
    return {"status": "ok"}

Schritt 3: pywebview starten

import webview
import threading

def start_server():
    app.run(port=8001, debug=False)

# Flask im Thread starten
flask_thread = threading.Thread(target=start_server, daemon=True)
flask_thread.start()

# pywebview öffnet Fenster (zeigt localhost:8001)
webview.create_window('Mein Plugin', 'http://localhost:8001', width=600, height=400)
webview.start()

Komplettes Beispiel: Counter-GUI

from flask import Flask, request, render_template_string
import webview
import threading
import json
from pathlib import Path

app = Flask(__name__)
DATA_DIR = Path("data")
DATA_DIR.mkdir(exist_ok=True)

state = {"counter": 0}

HTML_TEMPLATE = """
<html><head>
    <title>Counter</title>
    <style>
        body { background: #222; color: #fff; font-family: Arial; text-align: center; padding: 20px; }
        #display { font-size: 72px; font-weight: bold; margin: 20px 0; }
        button { padding: 15px 30px; font-size: 18px; cursor: pointer; }
    </style>
</head><body>
    <h1>Counter GUI</h1>
    <div id="display">0</div>
    <button onclick="send('/inc')">Increment</button>
    <button onclick="send('/dec')">Decrement</button>
    <script>
        function send(path) {
            fetch(path).then(r => r.json()).then(d => {
                document.getElementById('display').innerText = d.value;
            });
        }
        setInterval(() => send('/get'), 500);  // Sync every 500ms
    </script>
</body></html>
"""

@app.route('/')
def index():
    return render_template_string(HTML_TEMPLATE)

@app.route('/get')
def get_value():
    return {"value": state["counter"]}

@app.route('/inc')
def increment():
    state["counter"] += 1
    save_state()
    return {"value": state["counter"]}

@app.route('/dec')
def decrement():
    state["counter"] = max(0, state["counter"] - 1)
    save_state()
    return {"value": state["counter"]}

def save_state():
    with open(DATA_DIR / "counter.json", "w") as f:
        json.dump(state, f)

if __name__ == '__main__':
    # Flask im Thread
    threading.Thread(target=lambda: app.run(port=8001), daemon=True).start()
    
    # pywebview
    webview.create_window('Counter', 'http://localhost:8001', width=400, height=300)
    webview.start()
┌─────────────────────────────┐
│     pywebview-Fenster       │
│  (HTML/CSS/JS - Frontend)   │
└──────────────┬──────────────┘
               │ (JavaScript Bridge)
               │
┌──────────────▼──────────────┐
│  Flask oder FastAPI         │
│  (Python - Backend)         │
└─────────────────────────────┘

Einfaches Fenster öffnen

import webview
import threading

HTML = """
<html>
    <body style="background: #000; color: #0f0; font-size: 24px;">
        <h1>Mein Plugin</h1>
        <p>Dies ist eine einfache GUI</p>
    </body>
</html>
"""

def start_gui():
    webview.create_window('Mein Plugin', html=HTML)
    webview.start()

# Im Hauptprogramm:
gui_thread = threading.Thread(target=start_gui, daemon=True)
gui_thread.start()

Flask + pywebview kombinieren

Die meisten Plugins kombinieren Flask (für REST-Endpoints) mit pywebview (für die GUI). Das Frontend und Backend können dann kommunizieren:

from flask import Flask, render_template_string
import webview
import threading

app = Flask(__name__)

HTML_TEMPLATE = """
<!DOCTYPE html>
<html>
<head>
    <style>
        body { background: #000; color: #0f0; margin: 0; }
        #counter { font-size: 72px; text-align: center; }
        button { padding: 10px 20px; margin: 10px; }
    </style>
</head>
<body>
    <h1>Counter</h1>
    <div id="counter">0</div>
    <button onclick="add()">+1</button>
    <script>
        let count = 0;
        function add() {
            count++;
            document.getElementById('counter').innerText = count;
            fetch('/api/count?value=' + count);
        }
    </script>
</body>
</html>
"""

@app.route('/')
def index():
    return render_template_string(HTML_TEMPLATE)

@app.route('/api/count')
def update_count():
    value = request.args.get('value')
    # Speichere oder verarbeite den neuen Wert
    return "OK"

def start_flask():
    app.run(host='127.0.0.1', port=7777, debug=False)

def start_gui():
    flask_thread = threading.Thread(target=start_flask, daemon=True)
    flask_thread.start()
    
    webview.create_window('Counter Plugin', 'http://127.0.0.1:7777')
    webview.start()

# Im Hauptprogramm:
gui_thread = threading.Thread(target=start_gui, daemon=True)
gui_thread.start()

Praktisches Beispiel: DeathCounter

Der DeathCounter zeigt die Anzahl der Tode in Echtzeit an. Das funktioniert übers Server-Sent Events (SSE):

@app.route('/stream')
def stream():
    def event_stream():
        while True:
            yield f'data: {{"deaths": {death_manager.count}}}\n\n'
            time.sleep(0.5)
    
    return Response(stream(), mimetype='text/event-stream')

Das Frontend abonniert den Stream:

const es = new EventSource("/stream");
es.onmessage = (e) => {
    const deaths = JSON.parse(e.data).deaths;
    document.getElementById('counter').innerText = deaths;
};

So wird die GUI automatisch aktualisiert, wenn sich die Zahl ändert.

Fenster-Position und -Größe speichern

Nutzer mögen es, wenn ihre Fenster wieder an der gleichen Position entstehen:

import json

STATE_FILE = DATA_DIR / "window_state.json"

def load_win_size():
    if STATE_FILE.exists():
        try:
            with STATE_FILE.open("r") as f:
                return json.load(f)
        except:
            pass
    return {"x": 100, "y": 100, "width": 600, "height": 400}

@app.route('/save_dims', methods=['POST'])
def save_dims():
    data = request.json
    with STATE_FILE.open("w") as f:
        json.dump(data, f)
    return "OK"

Das Frontend speichert nach jeder Größenänderung:

window.addEventListener('resize', () => {
    fetch('/save_dims', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({
            width: window.innerWidth,
            height: window.innerHeight
        })
    });
});

Python ↔ JavaScript Kommunikation

Mit pywebview kannst du auch direkt Python-Funktionen aus JavaScript aufrufen:

api = webview.api

class API:
    def set_brightness(self, level):
        print(f"Helligkeit auf {level} gesetzt")
        return f"OK: {level}"

webview.create_window('Plugin', 'index.html', js_api=API())

Im Frontend:

async function changeBrightness() {
    const result = await pywebview.api.set_brightness(50);
    console.log(result); // "OK: 50"
}

CSS für Streaming-Overlays

Wenn dein Plugin in OBS eingebettet wird (Browser-Source), brauchst du spezielle CSS:

/* Transparenter Background */
body {
    background: transparent !important;
    margin: 0;
    padding: 0;
}

/* Font für hohe Auflösungen */
* {
    font-family: 'Inter', 'Segoe UI', sans-serif;
    -webkit-font-smoothing: antialiased;
}

/* Keine Rahmen/Scrollbars */
::-webkit-scrollbar {
    display: none;
}

Zusammenfassung

  • pywebview: GUI mit HTML/CSS/JavaScript + Python
  • Flask: REST-API für Frontend-Backend-Kommunikation
  • Server-Sent Events: Für Echtzeit-Updates vom Backend
  • State persistieren: Fenster-Position und -Größe speichern
  • Threading: GUI läuft in seperatem Thread

Nächstes Kapitel: Plugins kommunizieren miteinander

Interne-Plugin Kommunikation

Plugins sprechen miteinander

Plugins laufen parallel. Manchmal braucht eines Daten von einem anderen:

  • Timer fragt DeathCounter: "Wie viele Tode?"
  • WinCounter triggert Timer: "Reset jetzt"

Kommunikationswege:

  1. HTTP-Requests (Clean, async-ready) EMPFOHLEN
  2. Datei-Austausch (Einfach, aber Race Conditions)
  3. WebSockets (Realtime, komplex)

HTTP-Pattern: Client-Server

┌──────────────┐ POST /api/action ┌──────────────┐
│   Plugin A   ├────────────────> │   Plugin B   │
│  (Client)    │  {action: "add"} │   (Server)   │
│   Port 8001  │                  │   Port 8002  │
│              │<─────────────────┤              │
│              │ {status: ok}     │              │
└──────────────┘                  └──────────────┘

HTTP-Request: 3 Schritte

Schritt 1: Server-Plugin (WinCounter)

Szenario: Timer ruft WinCounter auf

WinCounter (Server):

@app.route('/add', methods=['POST'])
@app.route('/add', methods=['GET'])
def add_wins():
    amount = request.args.get('amount', 1, type=int)
    global win_count
    win_count += amount
    return json.dumps({"wins": win_count})

Timer (Client):

import requests

WIN_PORT = cfg.get("WinCounter", {}).get("WebServerPort", 8080)
WIN_URL = f"http://localhost:{WIN_PORT}/add?amount=1"

try:
    response = requests.post(WIN_URL, timeout=3)
    if response.status_code == 200:
        print("Win hinzugefügt!")
except requests.exceptions.Timeout:
    print("WinCounter antwortet nicht")
except Exception as e:
    print(f"Fehler: {e}")

Wichtige Punkte

Ports in config.yaml definieren:

WinCounter:
  Enable: true
  WebServerPort: 8080

Timer:
  Enable: true
  WebServerPortTimer: 7878

Timeout setzen: Wenn das andere Plugin nicht lädt, wartet man nicht ewig.

Fehlerbehandlung: Das andere Plugin kann offline sein.

2. Datei-basierte Kommunikation

Plugins können sich über gemeinsame Dateien austauschen – z.B. eine JSON-Datei mit aktuellen Daten.

# Plugin A schreibt:
data = {"total_wins": 42, "timestamp": time.time()}
with (DATA_DIR / "shared_state.json").open("w") as f:
    json.dump(data, f)

# Plugin B liest:
if (DATA_DIR / "shared_state.json").exists():
    with (DATA_DIR / "shared_state.json").open("r") as f:
        data = json.load(f)
        print(data["total_wins"])

Vorteil: Einfach, keine Netzwerk-Dependencies.

Nachteil: Race Conditions möglich! Wenn beide Plugins gleichzeitig schreiben, geht eine Änderung verloren.

Best Practice: Nur für selten geschriebene Daten oder Read-Only Zugriffe.

3. WebSockets (für Echtzeit-Kommunikation)

Wenn realtime Daten nötig sind, können Plugins über WebSockets kommunizieren. Das ist aber komplexer.

# Mit python-socketio
from socketio import Server

sio = Server(async_mode='threading')

@sio.event
def send_update(data):
    print(f"Daten empfangen: {data}")

# In anderem Plugin:
import socketio
sio_client = socketio.Client()
sio_client.connect('http://localhost:9000')
sio_client.emit('send_update', {'kills': 5})

Wann nutzen? Nur wenn echte Echtzeit-Synchronisation wichtig ist.

4. Webhook-Kommunikation

Ein Plugin kann ein anderes Plugin vor bestimmten Events benachrichtigen, indem es seinen Webhook aufruft.

# Plugin A sendet Event an Plugin B:
requests.post("http://localhost:7777/webhook", json={
    "event": "custom_event",
    "data": {"some": "data"}
})

# Plugin B empfängt:
@app.route('/webhook', methods=['POST'])
def webhook():
    event = request.json.get("event")
    if event == "custom_event":
        handle_custom_event(request.json.get("data"))
    return "OK"

Vorteil: Asynchron, flexibel. Nachteil: Komplexer zu debuggen.

Best Practice Patterns

Pattern 1: Request-Response (synchron)

# Client wartet auf Antwort
try:
    r = requests.get(f"http://localhost:8080/stats", timeout=2)
    stats = r.json()
except:
    stats = {}  # Fallback

Nutzen: Einfache, synchrone Abfragen (Zählerstände, Status, etc.)

Pattern 2: Fire-and-Forget (asynchron)

# Client sendet, wartet nicht auf Antwort
threading.Thread(
    target=requests.post,
    args=(f"http://localhost:8080/trigger", ),
    daemon=True
).start()

Nutzen: Wenn die Antwort egal ist (z.B. Events triggern).

Pattern 3: Polling (regelmäßig abfragen)

def poll_other_plugin():
    while running:
        try:
            r = requests.get(f"http://localhost:8080/status")
            process_status(r.json())
        except:
            pass
        time.sleep(5)  # Alle 5 Sekunden abfragen

threading.Thread(target=poll_other_plugin, daemon=True).start()

Nutzen: Regelmäßige, nicht-ständige Synchronisation.

Error Handling Best Practices

import requests

def call_other_plugin(url, data=None, timeout=3):
    try:
        if data:
            r = requests.post(url, json=data, timeout=timeout)
        else:
            r = requests.get(url, timeout=timeout)
        
        if r.status_code == 200:
            return r.json()
        else:
            print(f"Server antwortete mit {r.status_code}")
            
    except requests.exceptions.ConnectTimeout:
        print(f"Timeout: {url} antwortet nicht")
    except requests.exceptions.ConnectionError:
        print(f"Connection Error: {url} nicht erreichbar")
    except Exception as e:
        print(f"Unerwarteter Fehler: {e}")
    
    return None  # Fallback bei Fehler

Zusammenfassung

  • HTTP-Requests: Standard für Plugin-Kommunikation (synchron, zuverlässig)
  • Dateien: Für persistente Daten, aber Vorsicht vor Race Conditions
  • WebSockets: Nur wenn echte Echtzeit-Sync nötig
  • Fehlerbehandlung: Immer timeout setzen und Fehler abfangen
  • Ports in config.yaml: Zentral definieren, nicht hardcoden

Nächstes Kapitel: Fehlerbehandlung und Best Practices

Error Handling & Best Practices

Du bist selbst verantwortlich

[!WARNING] Das Haupt-System kümmert sich NICHT um Fehler deines Plugins.

Main Streaming Tool
├─ Plugin A (crasht → für immer tot)
├─ Plugin B (läuft normal)
└─ Plugin C (hängt sich auf → für immer dunkel)

Konsequenz: Dein Plugin muss 100% eigenen Error-Handling haben.

Error-Handling Strategien

PhaseFehlerHandling
StartupConfig fehltDefaults nutzen + Log
Flask ServerPort bereits in BenutzungAlternative Port + Error-Message
HTTP-RequestsTimeout/ConnectionRetry-Logic + Fallback
Datei-I/OPermission deniedTry-except + Logging
Unbekannt???Global try-except + Log + Exit

Fehler-Handling: Schichten-Modell

Schicht 1: Startup Protection

import sys
import logging

logging.basicConfig(
    level=logging.INFO,
    filename="logs/plugin.log",
    format='%(asctime)s [%(levelname)s] %(message)s'
)

try:
    # Config laden
    cfg = load_config()
except Exception as e:
    logging.critical(f"Config-Fehler: {e}")
    cfg = {}  # Defaults!

try:
    # Flask starten
    threading.Thread(target=lambda: app.run(port=cfg.get("port", 8001)), 
                     daemon=True).start()
except Exception as e:
    logging.critical(f"Flask-Fehler: {e}")
    sys.exit(1)

Schicht 2: Route Protection

@app.route("/webhook", methods=['POST'])
def webhook():
    try:
        data = request.json
        # Deine Logik
        return {"status": "ok"}, 200
    except Exception as e:
        logging.error(f"Webhook-Fehler: {e}", exc_info=True)
        return {"status": "error", "message": str(e)}, 500

@app.route("/api/add", methods=['POST'])
def add():
    try:
        amount = request.json.get("amount", 1)
        # Logic
        return {"result": result}
    except Exception as e:
        logging.error(f"Add-Fehler: {e}")
        return {"status": "error"}, 500

Schicht 3: Global Wrapper

def main():
    try:
        # Alles, was dein Plugin macht
        threading.Thread(target=start_flask, daemon=True).start()
        webview.create_window(...)
        webview.start()
    except KeyboardInterrupt:
        logging.info("Plugin stopped by user")
    except Exception as e:
        logging.critical(f"Plugin crashed: {e}", exc_info=True)
        sys.exit(1)

if __name__ == '__main__':
    main()

Logging Best Practices

import logging
from pathlib import Path

# Setup
LOGS_DIR = Root_DIR / "logs"
LOGS_DIR.mkdir(exist_ok=True)

logging.basicConfig(
    level=logging.DEBUG,
    handlers=[
        logging.FileHandler(LOGS_DIR / "myplugin.log"),
        logging.StreamHandler()  # Auch Console
    ],
    format='%(asctime)s [%(levelname)s] %(name)s: %(message)s'
)

logger = logging.getLogger(__name__)

# Nutzung:
logger.debug("Debug-Info für Entwickler")
logger.info("Plugin started successfully")
logger.warning("Config missing, using default")
logger.error("HTTP request failed", exc_info=True)
logger.critical("Plugin cannot recover from this error!")

Typische Fehler + Fixes

FehlerUrsacheFix
Port already in usePort 8001 belegtAlternative Port in config.yaml
Connection refusedAnderes Plugin offlinetry-except + Fallback
TimeoutRequest zu langsamtimeout=5 erhöhen
JSON decode errorMalformed responsejson.JSONDecodeError fangen
FileNotFoundErrorConfig-Datei fehlt.exists() checken vor Read

Plugin-Ready Checkliste

  • ☑ Global try-except wrapper um main()
  • ☑ Logging auf File + Console
  • ☑ Alle config-Keys mit .get() + defaults
  • ☑ HTTP-Requests in Threads
  • ☑ HTTP-Requests mit timeout + try-except
  • ☑ Alle Dateien mit .exists() checken
  • ☑ Graceful shutdown bei Ctrl+C

Gratuliere! Du kennst jetzt alles, um ein production-ready Plugin zu bauen.

┌─────────────────────────────────────────────┐
│         Main Streaming Tool                 │
│         (kümmert sich NICHT um Crashes)     │
└────────────┬────────────────────────────────┘
             │
             │── Plugin A (crasht → ignoriert)
             │── Plugin B (läuft normal)
             │── Plugin C (hängt sich auf → ignoriert)

Wenn dein Plugin crasht, ist es weg. Das System stellt es nicht wieder her.

Globales Error Handling

Wrapple deine gesamte Main-Logik in try-except:

import logging

logging.basicConfig(
    level=logging.INFO,
    filename=ROOT_DIR / "logs" / "my_plugin.log",
    format='%(asctime)s - %(levelname)s - %(message)s'
)

logger = logging.getLogger(__name__)

try:
    # Deine gesamte Plugin-Logik hier
    flask_thread = threading.Thread(target=start_flask)
    flask_thread.start()
    
    webview.create_window('Plugin', 'http://127.0.0.1:7777')
    webview.start()

except Exception as e:
    logger.critical(f"Plugin ist gecrasht: {e}", exc_info=True)
    sys.exit(1)

Das loggt den Fehler und beendet das Plugin sauber.

Logging – Deine beste Freundin

Logging ist essentiell zum Debuggen. Nutze logs/ Ordner:

import logging
from pathlib import Path

LOGS_DIR = ROOT_DIR / "logs"
LOGS_DIR.mkdir(exist_ok=True)

logging.basicConfig(
    level=logging.DEBUG,
    handlers=[
        logging.FileHandler(LOGS_DIR / "plugin.log"),
        logging.StreamHandler()  # Auch in Console
    ],
    format='%(asctime)s [%(levelname)s] %(message)s'
)

logger = logging.getLogger(__name__)

# Nutzen:
logger.info("Plugin gestartet")
logger.warning("Das könnte problematisch sein")
logger.error("Kritischer Fehler aufgetreten")
logger.debug("Debug-Informationen")

Log-Level verstehen

# In deiner config.yaml für das Haupt-System
log_level: 2

# Dein Plugin:
# Level 1: ERROR/CRITICAL
# Level 2: WARNING
# Level 3: INFO
# Level 4: DEBUG

Mit level=4 bei der Registrierung ist dein Debug-Output sichtbar, sobald log_level:4 ist. Das log_level muss >= deinem Registrierten level sein damit das Terminal angezigt wird.

Typische Fehler vermeiden

1. Hardcodierte Pfade

FALSCH:

cfg_file = "C:\\Users\\Admin\\Documents\\config.yaml"

RICHTIG:

cfg_file = ROOT_DIR / "config" / "config.yaml"

Nutze immer get_root_dir(), get_base_dir(), etc.

2. Encoding-Fehler bei Datei-Lesen

FALSCH:

with open(cfg_file) as f:  # Default encoding kann unterschiedlich sein
    data = yaml.safe_load(f)

RICHTIG:

with open(cfg_file, "r", encoding="utf-8") as f:
    data = yaml.safe_load(f)

3. Blockierende Operationen in der Main-Loop

Wenn du eine lange Operation machst (Netzwerk-Request, Datei-Verarbeitung), blockiert alles danach:

# FALSCH:
requests.get("http://API.com/data")  # Kann 10 Sekunden dauern
app.run()  # Flask startet erst nach dem Request

RICHTIG:

# In Thread starten
def fetch_data():
    requests.get("http://API.com/data")

threading.Thread(target=fetch_data, daemon=True).start()
app.run()  # Flask läuft parallel

4. Nicht auf Konfiguration Fehler prüfen

# FALSCH:
port = cfg["MyPlugin"]["port"]  # KeyError wenn nicht vorhanden!

# RICHTIG:
port = cfg.get("MyPlugin", {}).get("port", 8000)  # Mit Default

5. Race Conditions bei Datei-Zugriff

# FALSCH - zwei Plugins schreiben gleichzeitig:
with STATE_FILE.open("w") as f:
    json.dump(data, f)

# BETTER - Temporary File + Rename:
import tempfile
with tempfile.NamedTemporaryFile(mode="w", dir=DATA_DIR, delete=False) as tmp:
    json.dump(data, tmp)
    tmp.flush()
    tmp_path = tmp.name

import shutil
shutil.move(tmp_path, STATE_FILE)  # Atomic operation

6. Timeout vergessen bei Netzwerk-Requests

# FALSCH - hängt für immer:
response = requests.get("http://localhost:9999")

# RICHTIG:
try:
    response = requests.get("http://localhost:9999", timeout=3)
except requests.Timeout:
    logger.error("Request timed out")

Graceful Shutdown

Falls der Nutzer "exit" in der Start-Datei eingibt, wird dein Plugin mit SIGTERM beendet. Nutze das:

import signal

def handle_shutdown(sig, frame):
    logger.info("Plugin wird beendet...")
    # Cleanup hier
    sys.exit(0)

signal.signal(signal.SIGTERM, handle_shutdown)
signal.signal(signal.SIGINT, handle_shutdown)

Monitoring und Health Checks

Wenn anderer Plugins mit dir kommunizieren, kann es von vorteil sein, ein health checker zu haben:

@app.route('/health')
def health():
    return json.dumps({"status": "ok", "version": "1.0.0"})

Andere Plugins können checken, ob du noch läufst:

try:
    r = requests.get("http://localhost:7878/health", timeout=1)
    if r.status_code == 200:
        print("Plugin läuft")
except:
    print("Plugin nicht erreichbar")

Resource Management

Memory Leaks vermeiden

# FALSCH - infinite list:
all_events = []
@app.route('/webhook', methods=['POST'])
def webhook():
    all_events.append(request.json)  # Wird immer größer!

# RICHTIG - begrenzte Queue:
from collections import deque
events = deque(maxlen=1000)  # Max 1000 Einträge

@app.route('/webhook', methods=['POST'])
def webhook():
    events.append(request.json)  # Älteste Einträge werden automatisch gelöscht

Thread-Leaks vermeiden

# FALSCH - neue Threads für jeden Request:
@app.route('/process', methods=['POST'])
def process():
    threading.Thread(target=heavy_work).start()  # Memory leak!

# RICHTIG - Thread Pool:
from concurrent.futures import ThreadPoolExecutor
executor = ThreadPoolExecutor(max_workers=5)

@app.route('/process', methods=['POST'])
def process():
    executor.submit(heavy_work)  # Max 5 parallele Tasks

Testing vor dem Release

# test_plugin.py
import requests
import time

def test_basic():
    # Plugin sollte auf Port 7878 laufen
    r = requests.get("http://localhost:7878/health")
    assert r.status_code == 200

def test_webhook():
    r = requests.post("http://localhost:7878/webhook", json={
        "event": "player_death",
        "message": "Test"
    })
    assert r.status_code == 200
    time.sleep(1)
    # Prüfe ob Effekt sichtbar ist...

if __name__ == "__main__":
    test_basic()
    test_webhook()
    print("Alle Tests bestanden!")

Dann tests starten:

python test_plugin.py

[!TIP] Es wird empfohlen das Projekt zu bauen mit denn build.ps1 script und es dann im release Ordner zu testen weil gewisse Plugins/Hauptprogramm abhänigkeiten haben die nur im realse ordner richtig vorhanden sind.

Checkliste vor Release

  • Globales try-except um Main-Logik
  • Logging auf Critical/Error/Info Ebene
  • Alle Config-Zugriffe mit .get() + Fallback
  • Timeouts bei allen Netzwerk-Requests
  • /health Endpoint
  • Paths mit get_root_dir() etc., nicht hardcoded
  • Encoding utf-8 bei Datei-Operationen
  • Threading statt blockierende Ops in Main
  • Memory + Thread Leaks minimiert
  • README dokumentiert und aktuell

Zusammenfassung

  • Dein Plugin ist allein – Kein automatisches Crash-Handling
  • Logging: Nutze logs/ Ordner, speichere Debug-Infos
  • Fehlerbehandlung: Try-except global + bei jeder Netzwerk-Op
  • Timeouts: Immer setzen bei Requests
  • Threading: Blockierende Ops in Threads auslagern
  • Testing: Vor Release manuell/automatisch testen

Das war's für die Grundlagen! Von hier an geht es um deine Kreativität und die speziellen Anforderungen deines Plugins. Viel Erfolg!

Mapping zwischen Events und Minecraft

Das große Rätsel: Wie verbindet sich TikTok mit Minecraft?

Du weißt jetzt, wie Events in Python verarbeitet werden. Aber:

Wie sagt das Programm Minecraft, was es tun soll?

Event-Handler queued eine Aktion, aber: Was ist eine Aktion? Wie wird sie zu einem Minecraft-Befehl?

Die Antwort: Ein Mapping-System das Events zu Minecraft-Commands übersetzt.

TikTok-Event (Gift, Follow, Like)
        ↓
Event-Handler
        ↓
Queue: ("GIFT_ROSE", "anna_xyz")
        ↓
MAPPING-SYSTEM (dieses Kapitel!)
        ↓
Minecraft-Command ausgeführt
        ↓
Etwas passiert im Spiel!

Das Mapping visualisiert

Das Mapping funktioniert wie ein großes Nachschlagewerk:

TRIGGER (aus Event)  →  MINECRAFT COMMAND
─────────────────────────────────────────────────────
"GIFT_ROSE"          →       /give @s rose
"GIFT_DIAMOND"       →       /give @s diamond
"FOLLOW"             →       /say Willkommen!
"LIKE_GOAL_100"      →       /summon firework_rocket

Die Datei actions.mca: Das ist unsere Mapping-Tabelle! Sie definiert was passiert, wenn ein Trigger kommt.


Der komplette Ablauf (Überblick)

Wenn jemand auf TikTok folgt, passiert:

1. TikTok sendet: FollowEvent
2. Python-Handler: on_follow() aufgerufen
3. Handler queuet: ("FOLLOW", username)
4. Worker-Thread: `for trigger, user in trigger_queue.get():`
5. Worker-Thread: `action = ACTIONS_MAP["FOLLOW"]`
6. Worker-Thread: `send_command_to_minecraft(action)`
7. RCON-Protokoll: Command wird gesendet (über Netzwerk)
8. Minecraft-Server: `/say Willkommen, anna_xyz!`
9. **Ergebnis:** Im Chat erscheint die Nachricht

Dieser Ablauf kann bei einem Follow < 100ms dauern! Von TikTok bis Minecraft!


Wichtig zu verstehen

3 Dinge zusammen machen das System:

  1. actions.mca – Die Datei mit allen Mappings (statisch)
  2. Code in main.py – Liest die Datei beim Start
  3. RCON-Protokoll – Sendet Commands an Minecraft (Netzwerk)

Warum diese Aufteilung?

  • ✓ User können die actions.mca bearbeiten ohne Code zu ändern
  • ✓ Fehler in der Datei werden beim Start erkannt
  • ✓ Commands können dynamisch generiert werden

Nach diesem Kapitel wirst du verstehen:

  • Wie eine .mca Datei aufgebaut ist
  • Warum Validator wichtig ist
  • Wie man eigene Commands hinzufügt
  • Warum RCON "insecure" ist (und warum das OK ist)
  • Wann man .mcfunction-Dateien braucht

Weiter zu: → Die actions.mca Datei

Die Datei actions.mca

[!NOTE] In dieser und auch in anderen Dateien verwenden wir den Begriff „Plugin". Dabei gibt es zwei Bedeutungen: Plugins für Minecraft und Plugins für das Streaming-Tool. Welches Plugin gemeint ist, ergibt sich jeweils aus dem Kontext.

Was ist die actions.mca?

Die actions.mca ist eine einfache Text-Datei, die festlegt, was in Minecraft passiert, wenn ein bestimmtes TikTok-Event eintrifft. Jede Zeile ist ein Mapping von einem Trigger zu einem oder mehreren Commands:

TRIGGER:<TYPE>COMMAND xANZAHL
  • TRIGGER = Name oder ID (z.B. follow, 8913)
  • TYPE = Präfix: / (Vanilla), ! (Plugin/Custom), $ (Spezialfunktion)
  • COMMAND = Der auszuführende Befehl
  • xANZAHL = Optional: Command N-mal wiederholen

Die vollständige Syntax-Referenz findest du im nächsten Kapitel → Syntax & Befehle


Wo liegt die Datei?

PfadZweck
defaults/actions.mcaVorlage mit Beispiel-Mappings
data/actions.mcaWird tatsächlich geladen und genutzt

Beim ersten Start wird defaults/actions.mca nach data/actions.mca kopiert. Ab dann wird nur noch data/actions.mca verwendet.


Validierung: Fehler werden früh erkannt

Beim Start parst generate_datapack() in main.py jede Zeile der actions.mca:

# main.py - generate_datapack():
for line_num, original_line in enumerate(f, 1):
    line = original_line.split("#", 1)[0].strip()  # Kommentare entfernen
    if not line or ":" not in line:
        continue
    trigger, full_cmd_line = map(str.strip, line.split(":", 1))
    # ...
    if not cmd.startswith(("/", "!", "$")):
        print(f"[ERROR] Ungültiger Befehl ohne Präfix in Zeile {line_num}: {cmd}")

Jeder Command braucht ein Präfix (/, ! oder $). Zeilen ohne gültiges Präfix werden beim Start mit einer Fehlermeldung übersprungen.


Ein echtes Beispiel

Aus defaults/actions.mca (verkürzt):

# Basic Events
follow:/give @a minecraft:golden_apple 7
like_2:/clear @a *; /kill @a
likes:/execute at @a run summon minecraft:creeper ~ ~ ~ x2

# Gifts (numerische TikTok Gift-IDs)
5655:!tnt 2 0.1 2 Notch
16111:/give @a minecraft:diamond
5487:/give @a minecraft:totem_of_undying

# Special ($random wählt zufällig einen anderen Trigger)
16071:$random

# Complex (mehrere Command-Typen gemischt)
11046:/clear @a *; /execute at @a run summon minecraft:wither ~ ~ ~ x20; !tnt 20 0.1 2 Notch

Was hier passiert:

  • follow → Goldene Äpfel für alle Spieler
  • like_2 → Inventar leeren + alle Spieler töten
  • likes → 2 Creeper spawnen (Vanilla mit x2)
  • 16071 → Zufälliger Trigger ($random)
  • 11046 → Drei Commands nacheinander: Clear, 20 Wither, TNT

Der Ablauf: Vom Event zum Minecraft-Befehl

Event-Handler:
  trigger_queue.put_nowait(
    ("follow", "anna_123")
  )                                ← TRIGGER in Queue!
        ↓
Worker-Thread:
  trigger, user = trigger_queue.get()
  → "follow" ist in valid_functions  ← Trigger bekannt!
        ↓
Datapack / RCON:
  follow.mcfunction enthält:
  give @a minecraft:golden_apple 7   ← Ausführung!
        ↓
ERGEBNIS: Alle bekommen goldene Äpfel

Zusammenfassung

KonzeptErklärung
FormatTRIGGER:<TYPE>COMMAND xANZAHL
Dateiendefaults/actions.mca (Vorlage) → data/actions.mca (aktiv)
Validierunggenerate_datapack() prüft Syntax beim Start
AblaufEvent → Queue → Worker → RCON/Datapack → Minecraft

Nächstes Kapitel: Syntax & Befehle)

Syntax & Befehle

Aufbau einer Zeile

TRIGGER:<TYPE>COMMAND xANZAHL
trigger_name:<type>command xAnzahl
│            │     │       │
│            │     │       └─→ Wiederholung (optional)
│            │     └───────→ Der eigentliche Befehl
│            └───────────→ Präfix: / ! oder $
└─────────────→ Der Name, den Event-Handler nutzen

Jeder Teil ist wichtig:

TeilBedeutungBeispiel
TRIGGEREindeutiger Name oder ID8913, follow, likes
:Trennzeichen: (immer erforderlich)
<TYPE>Art des Commands/, !, $, >>
COMMANDWas soll passieren?give @a diamond, tnt 2 0.1 2
xANZAHLWie oft? (Optional)x3, x10 (ohne x = 1×)

Praktische Beispiele mit Erklärung

Beispiel 1: Einfaches Gift mit Wiederholung

8913:/execute at @a run summon minecraft:evoker ~ ~ ~ x3
  • 8913 = Gift-ID (evoker Gift)
  • : = Trennzeichen
  • / = Vanilla Minecraft Command
  • execute at @a run summon minecraft:evoker ~ ~ ~ = Was soll passieren
  • x3 = Diese Aktion 3x hintereinander

Resultat: Der Command wird 3x ausgeführt = 3 Evoker spawnen!


Beispiel 2: Follow ohne Wiederholung

follow:/give @a minecraft:golden_apple 7
  • follow = Spezial-Trigger für Follow-Events
  • : = Trennzeichen
  • / = Vanilla Command
  • give @a minecraft:golden_apple 7 = Was soll passieren
  • (kein x) = Nur 1x ausführen

Resultat: Alle Spieler bekommen 1x 7 goldene Äpfel.


Beispiel 3: Custom Command (Plugin)

6267:!tnt 600 0.1 2 Notch
  • 6267 = Gift-ID (TNT Gift)
  • : = Trennzeichen
  • ! = Custom/Plugin Command (nicht Vanilla)
  • tnt 600 0.1 2 Notch = Was soll passieren (custom Syntax!)
  • (kein x) = 1x ausführen

Resultat: Plugin-Command !tnt wird ausgeführt.


Beispiel 4: Spezial-Funktion

16071:$random
  • 16071 = Gift-ID
  • : = Trennzeichen
  • $ = Spezial-Funktion (kein normaler Command!)
  • random = Wähle zufällig einen anderen Trigger
  • (kein x) = 1x ausführen

Resultat: Ein zufällig gewählter anderer Trigger wird ausgeführt!

Beispiel 5: Overlay-Ausgabe (Bildschirmtext)

follow: >>Neuer Follower!|{user} folgt dir jetzt!|5
  • follow = Spezial-Trigger für Follow-Events
  • : = Trennzeichen
  • >> = Overlay-Ausgabe (Text wird im Stream eingeblendet, kein Minecraft-Command!)
  • Neuer Follower!|{user} folgt dir jetzt!|5 = Text, Untertitel und Dauer (Sekunden), durch | getrennt
  • x wird hier nicht unterstützt

Resultat: Im Stream erscheint ein Overlay mit dem Text Neuer Follower! und dem Untertitel {user} folgt dir jetzt! für 5 Sekunden.

[!NOTE] Die Dauer ist optional, Standard sind 3 Sekunden. {user} wird automatisch durch den Namen des Auslösers ersetzt (bei Likes z.B. Community).


Trigger-Typen erklärt

1. Gift-IDs (Zahlen)

5655:!tnt 2 0.1 2 Notch
8913:/execute at @a run summon minecraft:evoker ~ ~ ~ x3

Gift-IDs sind numerisch und eindeutig für jeden Geschenktyp auf TikTok. Die komplette Liste aller Gift-IDs findest du in core/gifts.json.


2. Special Trigger: follow

follow:/give @a minecraft:golden_apple 7

Reserviertes Wort für Follow-Events. Wird immer als follow geschrieben (Kleinbuchstaben).


3. Like-Trigger

likes:/execute at @a run summon minecraft:creeper ~ ~ ~ x2
like_2:/clear @a *; /kill @a

Vordefinierte Trigger für Like-Events:

  • likes = Standard-Like-Event (Häufigkeit konfigurierbar in config.yaml)
  • like_2 = Zusätzlicher Like-Trigger (z.B. für Meilensteine)

Können in config.yaml konfiguriert werden.


Command-Typen erklärt

Typ 1: Vanilla Commands (/)

/give @a minecraft:diamond
/execute at @a run summon minecraft:wither ~ ~ ~
/clear @a *
/say Willkommen!

Beginnt mit / → Standard Minecraft-Befehl. Wird in eine .mcfunction-Datei geschrieben und über das Datapack ausgeführt.


Typ 2: Plugin Commands (!)

5655:!tnt 2 0.1 2 Notch
6267:!tnt 600 0.1 2 Notch

Beginnt mit ! → Custom-Befehl. Wird direkt per RCON an den Server gesendet, nicht in eine .mcfunction-Datei geschrieben.

Warum der Unterschied? → Der Nutzen der .mcfunction-Dateien


Typ 3: Spezial-Funktionen ($)

16071:$random

Beginnt mit $ → Interne Spezialfunktion des Streaming-Tools. Aktuell ist nur $random implementiert.

$random wählt einen zufälligen anderen Trigger aus und führt ihn aus. Dabei werden Endlos-Schleifen verhindert: Trigger mit $random sowie Basis-Trigger wie likes, like_2 und follow werden automatisch ausgeschlossen.

Details zu $random → Funktion des $random Commands


Wiederholungen: Das xANZAHL-System

x3    = 3× hintereinander
x10   = 10× hintereinander
x100  = 100× hintereinander

Ohne x wird der Command genau ausgeführt.

8913:/execute at @a run summon minecraft:evoker ~ ~ ~ x3

Ist äquivalent zu:

/execute at @a run summon minecraft:evoker ~ ~ ~
/execute at @a run summon minecraft:evoker ~ ~ ~
/execute at @a run summon minecraft:evoker ~ ~ ~

→ 3 Evoker spawnen statt 1.


Mehrere Commands mit ; kombinieren:

11046:/clear @a *; /execute at @a run summon minecraft:wither ~ ~ ~ x20; !tnt 20 0.1 2 Notch

Commands mit ; trennen → alle werden der Reihe nach ausgeführt!
Von Links nach Rechts


Kommentare und Deaktivieren

Was nach # kommt, wird ignoriert:

#8913:/give @a minecraft:diamond
5655:/give @a minecraft:emerald
  • Zeile 1 ist auskommentiert (wird nicht gelesen)
  • Zeile 2 ist aktiv (wird gelesen)

Nutzen: Zum Deaktivieren ohne zu löschen oder um etwas dazu zu schreiben


Gültige und ungültige Syntax

Gültige Trigger-Namen:

✓ Buchstaben (a-z, A-Z)
✓ Zahlen (0-9)
✓ Unterstriche (_)

✗ Leerzeichen
✗ Sonderzeichen außer _
✗ Umlaute (ä, ö, ü)

✓ RICHTIG:

follow:/give @a minecraft:golden_apple 7
8913:/execute at @a run summon minecraft:evoker ~ ~ ~ x3
16071:$random
5655:!tnt 2 0.1 2 Notch

✗ FALSCH:

follow /give @a diamond           # ← Fehlt :
8913:               /give @a diamond  # ← Leerzeichen nach :
likes $random                        # ← Fehlt :
follow:/give @a diamond x          # ← x ohne Zahl

Zusammenfassung

ConceptErklärung
TriggerGift-ID, follow, likes, like_2
Command-Typen/ (Vanilla → mcfunction), ! (Plugin → RCON), $ (Spezial)
xANZAHLCommand N-mal wiederholen
SemikolonMehrere Commands in einer Zeile
Kommentare# zum Deaktivieren/Dokumentieren

Nächstes Kapitel: Design-Entscheidungen

Warum ist das Format so aufgebaut?

Design-Entscheidung

Das Format TRIGGER:<TYPE>COMMAND xREPEAT ist nicht zufällig gewählt. Es ist ein Kompromiss zwischen:

  • Einfachheit (User können es verstehen)
  • Maschinenlesbarkeit (Code kann es parsen)
  • Flexibilität (verschiedene Command-Typen)

Alternativen (und warum sie nicht gewählt wurden)

Alternative 1: JSON-Format

{
  "triggers": [
    {
      "id": "follow",
      "command": "/give @a diamond",
      "repeat": 1
    }
  ]
}

Pro: Strukturiert, leicht zu parsen Con: Zu streng, User machen viele Fehler mit Klammern/Kommas


Alternative 2: Config-basiert (YAML)

actions:
  follow:
    command: /give @a diamond
    repeat: 1

Pro: Lesbar Con: Zu viel Setup


Alternative 3: SQL-Datenbank

INSERT INTO actions VALUES ('follow', '/give @a diamond', 1);

Pro: Mächtig, Daten persistent Con: Overkill, braucht externe Tools


Das gewählte Format: Warum es besser ist

follow:/give @a diamond x1

Vorteile:

  1. Eine Zeile pro Aktion – Einfach zu verstehen
  2. Trennung mit : und x – Klare Struktur ohne Klammern
  3. Minimal, prägnant – Anfänger verstehen es schnell
  4. Nicht zu streng (Optional: x kann fehlen)
  5. Kommentar-Support (#) – Lines einfach deaktivieren
  6. Kompatibel – Funktioniert in regulären Text-Editoren

Parsen ist einfach

Für den Code:

# Beispiel: "follow:/give @a diamond x3"
parts = line.split(":")
trigger = parts[0]           # "follow"
cmd_with_repeat = parts[1]   # "/give @a diamond x3"

# Repeat extrahieren
if "x" in cmd_with_repeat:
    cmd, repeat = cmd_with_repeat.rsplit("x", 1)
    repeat_count = int(repeat)
else:
    cmd = cmd_with_repeat
    repeat_count = 1

# Typ bestimmen
if cmd.startswith("/"):
    kind = "vanilla"
elif cmd.startswith("!"):
    kind = "plugin"
elif cmd.startswith("$"):
    kind = "built_in"

Einfach, effizient, robust!

Wie wird das Format verarbeitet?

Der Ablauf beim Programmstart

Die Verarbeitung der actions.mca Datei findet beim Start statt:

1. Program Start
   ↓
2. Datei "actions.mca" laden
   ↓
3. Validator prüft auf Fehler
   ↓
4. Parser zerlegt jede Zeile:
   - Trigger auslesen
   - Command-Typ bestimmen (/, !, $)
   - Wiederholungen (x) extrahieren
   ↓
5. In-Memory-Dictionary aufbauen:
   ACTIONS = {
     "follow": [...],
     "8913": [...],
     "likes": [...]
   }
   ↓
6. Fertig! Datei ist in den RAM geladen
   ↓
7. Worker-Thread: Nachschlage ist sehr schnell!

Code-Beispiel: Parser Logik

def parse_actions(filename):
    actions = {}
    
    with open(filename) as f:
        for line_num, line in enumerate(f, 1):
            line = line.strip()
            
            # Kommentare & leere Zeilen überspringen
            if not line or line.startswith("#"):
                continue
            
            # Format parsen: "trigger:command x repeat"
            if ":" not in line:
                print(f"[ERROR] Zeile {line_num} hat kein ':' Trennzeichen")
                continue
            
            trigger, cmd_part = line.split(":", 1)
            trigger = trigger.strip()
            
            # Wiederholungen extrahieren
            repeat = 1
            if " x" in cmd_part:
                cmd_part, repeat_str = cmd_part.rsplit(" x", 1)
                try:
                    repeat = int(repeat_str)
                except ValueError:
                    print(f"[ERROR] Zeile {line_num}: x nach keine Zahl")
                    continue
            
            # Command-Typ bestimmen
            cmd = cmd_part.strip()
            if cmd.startswith("/"):
                cmd_type = "vanilla"
                body = cmd[1:].strip()
            elif cmd.startswith("!"):
                cmd_type = "plugin"
                body = cmd[1:].strip()
            elif cmd.startswith("$"):
                cmd_type = "built_in"
                body = cmd[1:].strip()
            else:
                print(f"[ERROR] Zeile {line_num}: Ungültiger Prefix")
                continue
            
            # Speichern
            if trigger not in actions:
                actions[trigger] = []
            
            actions[trigger].append({
                "type": cmd_type,
                "command": body,
                "repeat": repeat
            })
    
    return actions

Command-Typ Differenzierung

Beim Parsen wird unterschieden zwischen 3 Typen:

Typ 1: Vanilla (/)

if cmd.startswith("/"):
    kind = "vanilla"
    body = cmd[1:]  # "/" entfernen

→ Wird in .mcfunction-Datei gespeichert → Kann vom Minecraft-Server direkt ausgeführt werden


Typ 2: Plugin (!)

elif cmd.startswith("!"):
    kind = "plugin"
    body = cmd[1:]  # "!" entfernen

→ Wird via RCON an Minecraft gesendet → Ist ein Custom/Plugin-Command (nicht Vanilla)


Typ 3: Built-in ($)

elif cmd.startswith("$"):
    kind = "built_in"
    body = cmd[1:]  # "$" entfernen

→ Wird vom Programm selbst verarbeitet → Beispiel: $random wählt anderen Trigger


Runtime: Nachschlagen sehr schnell

Wenn zur Laufzeit ein Event kommt:

# Event-Handler sendet Trigger
trigger = "follow"

# Worker-Thread macht Nachschlag:
if trigger in ACTIONS:
    for action in ACTIONS[trigger]:
        execute(action["command"], repeat=action["repeat"])

Super schnell! Dictionary-Lookup ist sehr schnell.

Egal ob 10 oder 10.000 Actions definiert sind – das Nachschlagen ist gleich schnell!


Warum kind wichtig ist

# Kind bestimmt die Verarbeitung:

if kind == "vanilla":
    # Speichern in .mcfunction-Datei
    # Wird vom Server nativ ausgeführt
    save_to_mcfunction(body)

elif kind == "plugin":
    # Direkt via RCON an Server senden
    rcon_execute(body)

elif kind == "script":
    # Vom Programm interpretiert (z.B. $random)
    execute_built_in(body, source_user)

Die kind-Unterscheidung bestimmt, wie der Command ausgeführt wird!


Performance-Hinweis

Alle Command-Typen haben die gleiche Performance!

Die Kosten einer Aktion hängen ab von:

  • Was der Command macht (nicht welcher Type)
  • Z.B. /summon dauert länger als /say
  • Z.B. !tnt 1000 dauert länger als !tnt 1

Type (/, !, $) ist aus Performence sicht egal!
Es kommt auf denn Command an.


Zusammenfassung

  1. Parser zerlegt actions.mca einmal beim Start
  2. In-Memory-Dictionary wird aufgebaut
  3. Command-Typen werden klassifiziert (/, !, $)
  4. Runtime = schnelle Dictionary-Lookups
  5. Keine Parsing zur Laufzeit = mehr Performance

Funktion des $random-Befehls

Was ist $random?

$random ist ein eingebauter spezieller Command, der zufällig einen anderen Trigger ausführt.

Beispiel:

likes:$random

Wenn ein Like-Event kommt → statt immer das Gleiche zu tun → wähle zufällig einen anderen Trigger!


Praktischer Use-Case

Du magst chaotische Live-Streams? Dann:

likes:$random         # Jedes Like-Event einen ZUFÄLLIGEN Effekt!

Resultat: Der Stream ist unvorhersehbar und lustig!


Wie funktioniert $random intern?

# 1. Parser sieht: "likes:$random"
# → Speichert: kind = "built_in", body = "random"

# 2. Zur Laufzeit kommt Like-Event:
actions = ACTIONS["likes"]
for action in actions:
    if action["kind"] == "script":
        if action["body"] == "random":
            # Sammle alle möglichen Trigger
            possible_triggers = get_all_triggers_except("likes")
            
            # Wähle EINEN zufällig aus
            chosen = random.choice(possible_triggers)
            
            # Führe DIESEN aus
            execute_trigger(chosen)

Beispiel: Zufälliger Trigger Pool

# Definition
follow:/say Willkommen!
5655:/give @a diamond
8913:/summon minecraft:evoker
likes:$random  ← Startet die Random auswahl

# Wenn likes:$random kommt:
# 0% Chance: /say Willkommen!
# 50% Chance: /give @a diamond
# 50% Chance: /summon minecraft:evoker
# 0% Chance: $random

[!NOTE] Der Befehl /say Willkommen! wird niemals ausgeführt, da alle follow-, like- sowie der $random-Trigger selbst von der Zufallsauswahl ausgeschlossen sind.


Besonderheiten

1. Selbst-Rekursion vermieden

# $random wird NICHT in die Liste aufgenommen
possible_triggers = get_all_triggers() # Filtert selbst $random aus!

Sonst: likes:$random könnte $random wieder wählen = Endlosschleife!

2. All Trigger sind gleich wahrscheinlich

chosen = random.choice(possible_triggers)  # Gleichverteilung

Jeder Trigger hat die gleiche Chance gewählt zu werden.


Wann brauchst du das?

  • Chaos-Events auf dem Stream
  • Überraschungs-Effekte bei Milestones
  • Gameplay-Variabilität (nicht immer das Gleiche)
  • Mini-Games (zufällige Belohnungen)

Zusammenfassung

$random ist ein Meta-Command, der:

  • Zufällig einen anderen Trigger wählt
  • Zur Laufzeit evaluiert wird (nicht beim Start!)

[!NOTE] In Zukunft werden voraussichtlich weitere $-Commands hinzugefügt. Diese werden jedoch nicht mehr in der Entwicklerdokumentation beschrieben, sondern nur noch kurz in der Nutzerdokumentation erwähnt. Wenn du dich für ihre Funktionsweise interessierst, musst du den Code selbst einsehen und nachvollziehen.

Nächstes Kapitel: Wie schreibst du deinen eigenen $-Command?

Eigenen $ Command

Eigenem $-Befehl erstellen

Das Streaming Tool besitzt ein Event-Hook-System, mit dem Entwickler eigene $-Commands schreiben können — ganz ohne main.py zu verändern.

Du legst eine .py-Datei im Ordner src/event_hooks/ an, definierst dort eine register(api)-Funktion und trägst den zugehörigen $-Command in der actions.mca-Datei ein. Beim nächsten Start des Bots wird dein Hook automatisch geladen und ist sofort einsatzbereit.

[!WARNING] Eigene Imports sind nur eingeschränkt erlaubt.

Hook-Skripte laufen innerhalb der gebündelten Anwendung (app.exe). Du darfst deshalb keine beliebigen Module importieren — es sind nur die folgenden erlaubt:

  • random
  • time
  • (mehr in Zukunft)

Alle anderen Funktionen stehen dir über das api-Objekt zur Verfügung (z. B. RCON-Befehle senden, Trigger auslösen, Logging, Config lesen). Verwende keine eigenen import-Anweisungen für externe Pakete wie requests, flask, aiohttp usw. — diese stehen in Hook-Skripten nicht zur Verfügung und führen zu einem Ladefehler.


Wie es funktioniert

Beim Start des Bots werden alle .py-Dateien im Ordner event_hooks/ automatisch geladen und in den laufenden Prozess integriert. Es wird kein eigener Prozess und kein eigenes Executable pro Hook gestartet — alles läuft direkt in der Hauptanwendung.

[!NOTE] Entwicklung vs. Release: Während der Entwicklung liegt der Ordner unter src/event_hooks/. Im Release-Build wird er vom Build-Skript nach event_hooks/ kopiert. Der Bot lädt die Hooks immer aus dem Release-Pfad (event_hooks/), nicht aus src/event_hooks.

Die Ladereihenfolge im Detail:

  1. Parsing: generate_datapack() liest actions.mca und sammelt alle $-Command-Namen (z. B. $begruessungbegruessung)
  2. Import: Alle .py-Dateien in event_hooks/ werden via importlib dynamisch importiert
  3. Registrierung: Für jedes geladene Modul wird register(api) aufgerufen — dort werden die Handler registriert
  4. Ausführung: Wenn ein TikTok-Event eintrifft und der zugehörige Trigger auf einen $-Command zeigt, wird der passende Handler sofort ausgeführt

Einen Hook erstellen

Schritt 1: Hook-Skript anlegen

Erstelle eine neue .py-Datei im Ordner src/event_hooks/.

Beispiel: src/event_hooks/begruessung.py

def register(api):
    def begruessung(user, trigger, context):
        api.rcon_enqueue([
            f"say {user} folgt jetzt!",
            "effect give @a minecraft:glowing 5 0 true",
        ])

    api.register_action("begruessung", begruessung)

Was passiert hier?

  • register(api) ist die Pflichtfunktion, die vom System aufgerufen wird. Ohne sie wird das Skript ignoriert.
  • Innerhalb von register() definierst du deinen Handler als Closure — dadurch hat er automatisch Zugriff auf api.
  • api.register_action("begruessung", begruessung) meldet den Handler unter dem Namen begruessung an. Dieser Name muss exakt mit dem $-Command in der actions.mca übereinstimmen.

Schritt 2: In actions.mca eintragen

Der User (oder du als Entwickler zum Testen) trägt den Command in der actions.mca ein. Der Trigger-Key links vom : bestimmt, bei welchem TikTok-Event der Hook ausgelöst wird:

follow: $begruessung

In diesem Beispiel: Jedes Mal, wenn jemand auf TikTok folgt, wird der Hook begruessung aufgerufen.

Schritt 3: Bot starten oder neu starten

Starte den Bot — oder starte ihn neu, falls er bereits läuft. Der Hook wird automatisch geladen und ist sofort aktiv.


Die register(api)-Funktion

Jede Hook-Datei muss eine register(api)-Funktion auf oberster Ebene bereitstellen. Fehlt sie, wird das Skript beim Laden übersprungen und eine Fehlermeldung ausgegeben.

register() wird genau einmal beim Start des Bots aufgerufen. Definiere alle deine Handler innerhalb dieser Funktion als Closures — so haben sie automatisch Zugriff auf das api-Objekt, ohne dass du globale Variablen brauchst.

def register(api):
    def mein_handler(user, trigger, context):
        # Deine Logik hier
        api.log(f"{user} hat {trigger} ausgelöst")

    api.register_action("meine_aktion", mein_handler)

[!WARNING] Dateien ohne register()-Funktion werden nicht geladen. In der Konsole erscheint dann:

[HOOK] [ERROR] dateiname.py has no register() function — skipped.

Handler-Signatur

Jeder Handler muss genau drei Argumente akzeptieren:

ArgumentTypBeschreibung
userstrDer TikTok-Benutzername, der das Event ausgelöst hat (z. B. "max_mustermann")
triggerstrDer Name des $-Commands, der gerade ausgeführt wird (z. B. "begruessung")
contextdictReserviert für zukünftige Erweiterungen — aktuell wird immer ein leeres {} übergeben
def mein_handler(user, trigger, context):
    api.rcon_enqueue([f"say Hallo {user}, Trigger war: {trigger}"])

Fehlende oder zu viele Argumente führen dazu, dass der Handler beim Aufruf einen Fehler wirft (der Bot bleibt aber stabil — siehe Fehlerbehandlung).


Die HookAPI

Das api-Objekt, das an register() übergeben wird, ist die einzige Schnittstelle zwischen deinem Hook und dem Hauptsystem. Es stellt folgende Methoden bereit:

api.register_action(name, fn)

Registriert einen Handler unter dem angegebenen Namen. Der Name muss exakt mit dem $-Command in der actions.mca übereinstimmen.

api.register_action("begruessung", begruessung)

[!WARNING] Wird derselbe Name zweimal registriert (z. B. in zwei verschiedenen Hook-Dateien), gewinnt die erste Registrierung. Die zweite wird ignoriert und eine Warnung ausgegeben:

[HOOK] [WARN] Duplicate action 'begruessung' — first registration kept.

api.rcon_enqueue(commands)

Sendet eine Liste von Minecraft-Commands an die RCON-Queue. Die Commands werden der Reihe nach an in die RCON-Queue geschoben und von dort abgearbeitet.

api.rcon_enqueue([
    "effect give @a minecraft:speed 10 2 true",
    f"say {user} hat einen Speed-Boost ausgelöst!",
])

Jeder Eintrag ist ein vollständiger Command als String — ohne führenden /.

[!NOTE] Vanilla- und Plugin-Commands sind beide erlaubt.

Da alles über RCON läuft, kannst du nicht nur Vanilla-Minecraft-Commands senden, sondern auch Commands von installierten Server-Plugins (z. B. Bukkit/Paper/Spigot-Plugins). Der Server empfängt sie genau so, als hättest du sie in der Server-Konsole eingegeben worden.

api.rcon_enqueue([
    "tnt 2 0.1 2 Notch",  # Beispiel: Plugin-Command
    "say Boost aktiv!",   # Vanilla-Command
])

api.enqueue_trigger(trigger, user)

Schiebt einen Trigger in die Trigger-Queue. Der Bot verarbeitet ihn exakt so, als wäre ein TikTok-Event mit diesem Trigger eingetroffen — inklusive aller Aktionen, die dem Trigger in der actions.mca zugeordnet sind (Vanilla, RCON, Overlay, weitere $-Commands).

[!WARNING] Das erste Argument ist ein Trigger — kein $-Command-Name.

Ein Trigger ist das, was links vom : in der actions.mca steht. Das sind z. B. Gift-IDs (5655), reservierte Event-Namen (follow, likes) oder eigene Trigger, die du selbst angelegt hast.

Was rechts vom : hinter dem $ steht — also der Command-Name wie begruessung oder superjump — ist kein gültiger Trigger.

follow: $begruessung
│         │
│         └── $-Command-Name (KEIN gültiger Trigger)
└──────────── Trigger (das musst du an enqueue_trigger übergeben)

api.enqueue_trigger("begruessung", user) tut nichts — still ignoriert, kein Fehler, keine Warnung. api.enqueue_trigger("follow", user) funktioniert — follow steht links in der actions.mca.

[!NOTE] Der Trigger wird asynchron in die Queue gestellt — er wird nicht sofort im selben Handler-Aufruf abgearbeitet. Die RCON-Commands deines aktuellen Handlers laufen zuerst, dann kommt der weitergeleitete Trigger dran.


Variante A — Einen bestehenden Trigger weiterleiten

Du kannst aus einem Hook heraus einen Trigger auslösen, der in der actions.mca bereits für ein anderes TikTok-Event eingetragen ist.

actions.mca:

follow: $begruessung; /give @a minecraft:golden_apple 7
5655:   $grosses_geschenk
def register(api):
    def grosses_geschenk(user, trigger, context):
        api.rcon_enqueue(f"say Riesiges Geschenk von {user}!")
        # Löst zusätzlich den "follow"-Trigger aus.
        # → Der User bekommt die Begrüßung + Golden Apples on top.
        api.enqueue_trigger("follow", user)

    def begruessung(user, trigger, context):
        api.rcon_enqueue(f"say Hallo {user}")

    api.register_action("grosses_geschenk", grosses_geschenk)
    api.register_action("begruessung", begruessung)

Was passiert bei Gift 5655?

  1. TikTok meldet Gift 5655 → Trigger 5655 wird abgearbeitet
  2. execute_global_command("5655", …) findet $grosses_geschenk → ruft deinen Handler auf
  3. Handler sendet die RCON-Nachricht und schiebt "follow" in die Queue
  4. Kurz darauf: execute_global_command("follow", …) läuft — führt $begruessung und /give … aus

So bekommt ein Geschenke-Sender automatisch dieselbe Behandlung wie ein neuer Follower, ohne dass du den Follow-Code duplizieren musst.

[!WARNING] Achtung: Endlosschleifen!

Leite niemals auf den Trigger zurück, der deinen eigenen Handler ausgelöst hat:

follow: $begruessung
def begruessung(user, trigger, context):
    api.enqueue_trigger("follow", user)  # ← Loop!

Sobald jemand auf TikTok folgt, löst das den follow-Trigger aus. Dessen Handler schiebt follow wieder in die Queue, der Handler feuert erneut, schiebt wieder follow hinein — und so weiter.

Das System erkennt das zur Laufzeit automatisch. Nach 3 Ketten-Schritten wird der Trigger geblockt und dauerhaft für die laufende Session gesperrt:

[HOOK] [ERROR] enqueue_trigger('follow') blocked — chain depth 4 exceeds maximum (3).
               Trigger 'follow' is now permanently banned for this session. Possible infinite loop.

Jeder weitere enqueue_trigger("follow", ...)-Aufruf — egal von welchem Hook — wird dann sofort abgewiesen:

[HOOK] [ERROR] enqueue_trigger('follow') permanently blocked — trigger was banned after loop detection.

Was wird noch ausgeführt?

enqueue_trigger wirft keine Exception — es gibt nur ein stilles return zurück zur aufrufenden Stelle. Das bedeutet:

  • Der Rest des Handlers läuft normal weiter. Hat begruessung nach dem enqueue_trigger-Aufruf noch weiteren Code (z. B. weitere rcon_enqueue-Aufrufe, Logging, etc.), wird der vollständig ausgeführt. Nur der Eine enqueue_trigger-Aufruf ist blockiert.

  • Die restlichen Aktionen aus der actions.mca-Zeile laufen ebenfalls normal. Angenommen, follow ist so eingetragen:

    follow: $begruessung; /give @a minecraft:golden_apple 7
    

    Der Handler begruessung wird aufgerufen (inklusive allem Code dahinter), und der /give-Command wird danach normal ausgeführt — der Ban betrifft nur den enqueue_trigger-Aufruf, nichts sonst.


Variante B — Einen eigenen Trigger anlegen

Trigger in der actions.mca müssen keine echten TikTok-Events sein. Du kannst dir eigene Trigger anlegen — sie sind genauso gültig wie follow oder eine Gift-ID, werden aber nie automatisch von TikTok ausgelöst. Sie feuern nur, wenn du sie per enqueue_trigger anstößt.

actions.mca:

5655:        $geschenk_klein
8913:        $geschenk_gross
dankeschoen: $dankeschoen

Hier ist dankeschoen ein eigener Schlüssel. Kein TikTok-Event heißt so — er dient rein als interner Ketten-Schritt, den deine Hooks über enqueue_trigger aufrufen.

def register(api):
    def geschenk_klein(user, trigger, context):
        api.rcon_enqueue(["effect give @a minecraft:speed 5 1 true"])
        api.enqueue_trigger("dankeschoen", user)

    def geschenk_gross(user, trigger, context):
        api.rcon_enqueue(["effect give @a minecraft:speed 20 3 true"])
        api.enqueue_trigger("dankeschoen", user)

    def dankeschoen(user, trigger, context):
        api.rcon_enqueue([f"say Danke an {user} für das Geschenk!"])

    api.register_action("geschenk_klein", geschenk_klein)
    api.register_action("geschenk_gross", geschenk_gross)
    api.register_action("dankeschoen", dankeschoen)

Was passiert bei Gift 5655?

  1. execute_global_command("5655", …) → findet $geschenk_klein → ruft deinen Handler auf
  2. Handler gibt Speed-Effekt und schiebt "dankeschoen" in die Queue
  3. execute_global_command("dankeschoen", …) → findet $dankeschoen → ruft dankeschoen-Handler auf
  4. Handler sendet die Danke-Nachricht

Bei Gift 8913 passiert dasselbe über geschenk_gross, aber am Ende läuft dieselbe dankeschoen-Aktion. Die Logik steht nur einmal im Code.


Warum lohnt sich ein eigener Trigger?

Ein eigener Trigger ist ein vollwertiger actions.mca-Eintrag. Das heißt: du kannst ihm nicht nur einen $-Hook zuweisen, sondern die gesamte Palette der actions.mca-Syntax nutzen — Vanilla-Commands, RCON, Overlay-Text, alles auf einmal:

dankeschoen: $dankeschoen; /playsound minecraft:entity.player.levelup master @a; >>Danke!|{user} hat gesponsert!|4

Wenn jetzt ein Hook api.enqueue_trigger("dankeschoen", user) aufruft, passiert alles zusammen: dein Python-Handler läuft, der Sound wird über das Datapack abgespielt, und der Overlay-Text erscheint im Stream.

Die konkreten Vorteile:

  • Wiederverwendbarkeit: Beliebig viele Hooks können denselben Trigger aufrufen. Die gemeinsame Logik steht einmal — im Hook und/oder in der actions.mca.
  • Trennung von Code und Konfiguration: Der Streamer kann in der actions.mca Commands, Sounds oder Overlay-Text an den Trigger anhängen, ohne die Python-Datei anfassen zu müssen. Du als Entwickler schreibst die Logik, der Streamer konfiguriert den Rest.
  • Verkettung: Trigger können andere Trigger auslösen — so baust du komplexe Abläufe aus einfachen, testbaren Bausteinen zusammen.
  • Später erweiterbar: Wenn der Streamer irgendwann noch einen Firework-Effekt dranhängen will, ändert er nur die eine Zeile in actions.mca — fertig. Kein Code-Deployment nötig.

[!TIP] Du möchtest testen, ob deine Trigger funktionieren? Dann wirf einen Blick in die GUIDE.md, Kapitel "Test Your Triggers Without TikTok". Dort findest du ein Test-Tool, mit dem du deine Trigger ganz ohne TikTok-Verbindung ausprobieren kannst.

Wenn du Python installiert hast, kannst du in der Entwicklungsstruktur auch direkt die Datei tests/send_trigger.py starten und so Trigger bequem aus der Konsole testen – ganz ohne die .exe zu verwenden. Beachte aber: Auch beim Testen mit send_trigger.py muss das Projekt gebaut sein und alle anderen Komponenten in der Release-Umgebung laufen.

api.log(msg)

Gibt eine Nachricht in der Konsole aus, mit automatischem [HOOK]-Prefix. Nützlich zum Debuggen.

api.log("Hook erfolgreich geladen")
# Ausgabe: [HOOK] Hook erfolgreich geladen

api.config

Lesezugriff auf die geladenen Werte aus config.yaml. Gibt ein verschachteltes Dictionary zurück.

port = api.config.get("RCON", {}).get("Port", 25575)

Einen Handler für mehrere $-Commands verwenden

Manchmal sollen mehrere $-Commands ähnlich reagieren, aber mit leichten Unterschieden. In diesem Fall registrierst du dieselbe Funktion unter mehreren Namen und unterscheidest im Handler über das trigger-Argument, welcher Command gerade aktiv ist.

def register(api):
    def power_up(user, trigger, context):
        effects = {
            "superjump": "minecraft:jump_boost",
            "superrun":  "minecraft:speed",
            "superheal": "minecraft:regeneration",
        }
        effect = effects.get(trigger)
        if effect:
            api.rcon_enqueue([f"effect give @a {effect} 10 5 true"])

    api.register_action("superjump", power_up)
    api.register_action("superrun", power_up)
    api.register_action("superheal", power_up)

Der User trägt dann in actions.mca ein:

5655: $superjump
16111: $superrun
7934: $superheal

So brauchst du nur einen Handler für beliebig viele verwandte Commands.


Mehrere Aktionen in einer Datei

Eine einzelne .py-Datei kann beliebig viele Aktionen registrieren. Du bist nicht auf einen Handler pro Datei beschränkt:

def register(api):
    def bei_follow(user, trigger, context):
        api.rcon_enqueue([f"say {user} folgt jetzt!"])

    def bei_grossem_geschenk(user, trigger, context):
        api.rcon_enqueue([
            "summon minecraft:firework_rocket ~ ~ ~",
            f"say Danke, {user}!",
        ])

    api.register_action("follow_effekt", bei_follow)
    api.register_action("geschenk_effekt", bei_grossem_geschenk)

Fehlerbehandlung

Fehler innerhalb eines Hook-Handlers crashen den Bot nicht. Sie werden abgefangen und mit einem [HOOK]-Prefix in der Konsole ausgegeben:

[HOOK] [WARN] Error in action 'begruessung': name 'undefined_var' is not defined

Auch beim Laden gibt es ein Sicherheitsnetz:

  • Syntaxfehler in einer Hook-Datei → nur diese Datei wird übersprungen, alle anderen laden normal weiter
  • Fehlende register()-Funktion → Datei wird übersprungen mit Fehlermeldung
  • Exception in register() → Datei wird übersprungen, Fehler wird geloggt
  • Exception im Handler zur Laufzeit → wird geloggt, Bot läuft weiter

Build-in Commands können nicht überschrieben werden

[!WARNING] Bestimmte $-Commands sind fest im System eingebaut und können nicht durch eigene Hooks überschrieben werden.

Aktuell reservierte Namen:

  • random

Wenn du versuchst, einen dieser Namen mit api.register_action("random", ...) zu registrieren, erscheint beim Laden folgende Fehlermeldung:

[HOOK] [ERROR] 'random' is a reserved built-in command — cannot be overridden by a hook.

Diese Commands werden intern von main.py behandelt und sind für eigene Hooks gesperrt.

Nächstes Kapitel: RCON und seine Grenzen

​RCON und seine Limitierungen

Was ist RCON?

RCON = Remote Console – Ein Protokoll um von außen Commands an Minecraft zu senden.

Es funktioniert über TCP/Port 25575 (standardmäßig). Der Minecraft-Server muss vorher enable-rcon=true in server.properties haben.


Das Problem: Beschränkte Bandbreite

Stell dir RCON wie ein dünnes Rohr vor:

TikTok-Befehle
        ↓ (viele kommen an)
    RCON-Rohr  ← Begrenzte Kapazität!
        ↓ (muss der Reihe nach raus)
  Minecraft

Das Problem: Wenn zu viele Commands gleichzeitig ankommen → Überlastung!

Die Lösung: Queues (Warteschlangen) – Commands der Reihe nach abarbeiten!


Queue-Limits

trigger_queue = Queue(maxsize=10_000)  # Max 10k eingehende Events
rcon_queue = Queue(maxsize=10_000)     # Max 10k Commands an Minecraft
like_queue = Queue()                   # ∞ (unbegrenzt!)

Warum keine Limits bei like_queue?

Likes sind klein und kommen oft → viele in der Queue ist OK. Like-Daten sind nur delta (Integer), nicht volle Commands!


Dynamisches Throttling

Das System passt die Sendgeschwindigkeit an:

q_size = rcon_queue.qsize()
        wait_time = THROTTLE_TIME
        inner_pause = 0.01 

if q_size > 100:
    wait_time, inner_pause = 0.01, 0.001
elif q_size > 50:
    wait_time, inner_pause = 0.05, 0.005
elif q_size > 20:
    wait_time, inner_pause = 0.1, 0.01

Effekt:

  • Wenn Queue groß: schneller verarbeiten
  • Wenn Queue leer: langsamer senden (Ressourcen sparen)

Limitierungen & Edge Cases

ProblemFolgeLösung
Queue vollEvents gehen verlorenput_nowait() mit Exception-Handling
Verbindung bricht abEs kommen keine Commands anAuto-reconnect
Command zu großRCON-ErrorCommand splitten
Zu schnell sendenMinecraft-CrashThrottling anpassen

Best Practice

# DO: Befehle nacheinander ausführen
while True:
    command = rcon_queue.get()
    minecraft_server.execute(command)
    time.sleep(0.05)  # Kurze Pause für Stabilität

# DON'T: Befehle parallel ausführen (führt zu Instabilität!)
for command in large_command_batch:
    minecraft_server.execute(command)  # ← Zu schnell!

[!NOTE] Dieses Beispiel ist stark vereinfacht. Im Hauptprogramm sind mehrere hundert Zeilen notwendig, um RCON stabil zu betreiben, Fehler sauber abzufangen und alle Befehle zuverlässig der Reihe nach abzuarbeiten.


Zusammenfassung

  • RCON = Netzwerk-Protokoll für Commands
  • Queued = Um nicht zu überlasten
  • Rate-Limiting = Dynamisch angepasst

Nächstes Kapitel: mcfunction-Dateien

Der Nutzen von mcfunction-Dateien

Das Problem: RCON ist das Engpas

Wenn jemand ein extremes Event definiert:

7168:/execute at @a run summon minecraft:wither ~ ~ ~ x500

Das bedeutet: 500 Wither spawnen!

Ohne Optimierung:

  • 500 einzelne RCON-Befehle senden → Verbindung bricht ab!
  • Mit Throttling: ~5 Sekunden Verzögerung nur für dieses Event
  • Andere Events müssen warten → Queue läuft voll

Die Lösung: .mcfunction-Dateien!


Was sind .mcfunction-Dateien?

.mcfunction-Dateien speichern eine Liste von Vanilla-Befehlen, die der Minecraft-Server intern ausführt.

Statt:

RCON: /summon wither x1
RCON: /summon wither x1
...  (500x!)

Jetzt:

# 7168.mcfunction (gespeichert datei)
/execute at @a run summon minecraft:wither ~ ~ ~
/execute at @a run summon minecraft:wither ~ ~ ~
... (500x als Text!)

Und dann nur:

RCON: function namespace:7168

Ein Befehl statt 500!


Der Prozess

1. Programmstart
   ↓
2. actions.mca wird gelesen
   ↓
3. Für jeden `/`-Befehl mit `xN` (N > 1):
   → Schreibe N Zeilen in eine .mcfunction-Datei
   ↓
4. Laufzeit:
   Event kommt an
   → Sende nur: "function namespace:7168"
   ↓
5. Minecraft Server:
   → Führt alle 500 Befehle in einem Tick aus!
   (1/20 Sekunde = super schnell)

Vorteile

AspektOhne .mcfunctionMit .mcfunction
RCON-Last500 Pakete1 Paket
Geschwindigkeit5+ Sekunden~1 Tick
Queue-BelastungHochMinimal
DatendurchsatzRiesigWinzig

Limitierungen

1. Nur Vanilla-Commands

✓ /summon, /give, /execute  (OK!)
✗ /mods-custom-command      (Nicht OK!)

Plugin-Commands können nicht in Dateien stehen.

Lösung: Nutze ! Prefix statt / um RCON direct zu senden.


2. Statische Generierung (beim Start)

# Beim Programmstart:
for trigger, command in actions.mca:
    write_to_mcfunction(trigger, command)

# Änderungen an actions.mca NICHT Live!
# Du musst neu starten, um Änderungen zu laden!

Wichtig: actions.mca Änderungen werden erst ab nächstem Start aktiv!


3. Server-Performance Warnung

x500 bedeutet: Der Server muss 500 Befehle in 1 Tick ausführen!

✓ x10, x50 = OK
x100+ = Warnung aus dem Programm
✗ x500+ = Wahrscheinlich Server-Crash oder starke lags

Nicht Übertreiben!


Beispiel

# Einfach (RCON direct)
follow:/give @a diamond

# Komplex (wird zu .mcfunction)
7168:/summon minecraft:wither ~ ~ ~ x50

Das Programm erstellt:

# 7168.mcfunction
/summon minecraft:wither ~ ~ ~
/summon minecraft:wither ~ ~ ~
...
(50x repeat)

Beim Event 7168:

  • Sendet NUR: /function namespace:7168
  • Server führt Datei aus (50 Befehle in einem Tick!)

Zusammenfassung

  • Vanilla Commands(/) mit xN → werden in .mcfunction-Dateien ausgelagert
  • Plugin Commands(!) & Built-in($) → RCON direct sent
  • .mcfunction-Dateien werden beim Start generiert, nicht live updatet
  • Performance: xN sollte ≤ 100 sein um Server nicht zu überlasten

Ende! Jetzt verstehst du: TikTok → Events → Minecraft-Commands!


Nächstes Kapitel: System-Architektur

System-Module und Integration

Modularität: Das Geheimnis der Skalierbarkeit

Das Streaming-Tool ist nicht ein riesiger monolithischer Block, sondern besteht aus unabhängigen Komponenten:

Streaming-Tool
  │
  ├─ Core (Module - Infrastruktur)
  │   ├─ validator.py (Validierung)
  │   ├─ models.py (Datentypen)
  │   ├─ utils.py (Hilfsfunktionen)
  │   ├─ paths.py (Pfad-Management)
  │   └─ cli.py (Command-Line Interface)
  │
  ├─ Built-in Plugins (Standard-Funktionen)
  │   ├─ Timer (Countdown-Tracker)
  │   ├─ DeathCounter (Tod-Zähler)
  │   ├─ WinCounter (Sieg-Zähler)
  │   ├─ LikeGoal (Like-Milestone-Tracker)
  │   └─ OverlayTxt (Text-Overlay für OBS)
  │
  └─ Custom Plugins (Benutzer-definiert)
      └─ Deine eigenen Plugins

Das Geheimnis: Jedes Plugin ist von den anderen unabhängig, kann sich aber über HTTP (DCS) mit anderen verbinden!


Module vs. Plugins

KategorieSpeicherortZweckBeispiele
Module (Core)src/core/Infrastruktur & Kernlogikvalidator, models, utils, paths, cli
Built-in Pluginssrc/plugins/Standard-FunktionenTimer, DeathCounter, WinCounter, LikeGoal, OverlayTxt
Custom Pluginsplugins/ (Benutzer)Benutzerdefinierte ErweiterungenDeine eigenen Plugins

Die drei Kern-Konzepte

1. Registry (Zentrale Verwaltung)

Alle Plugins registrieren sich beim Start über --register-only:

register_plugin(AppConfig(
    name="Timer",
    path=MAIN_FILE,
    enable=True,
    level=4,
    ics=True
))

start.py weiß, welche Plugins verfügbar sind und startet sie


2. Control Methods (Steuermechanismen)

Module können ihre Funktionen der Außenwelt anbieten:

  • DCS (Direct Control System) = HTTP-basiert (Browser-Source in OBS)
  • ICS (Interface Control System) = GUI-Fenster (pywebview, Window Capture in OBS)

→ User können Module von außen kontrollieren


3. Daten-Sharing (Datenaustausch)

Plugins teilen Daten über:

  • HTTP-Requests (DCS-Kommunikation zwischen Plugins)
  • Dateien (data/ Verzeichnis, z.B. JSON-Dateien)
  • Webhooks (Events von Minecraft via HTTP POST)

→ Keine direkten Abhängigkeiten, nur standardisierte Protokolle!


Architektur-Prinzipien

Autonomie:       Jedes Plugin funktioniert allein
  ↓
Registrierung:   Beim Start registrieren (--register-only)
  ↓
Kommunikation:   Via HTTP (DCS) und Webhooks
  ↓
Isolation:       Crash eines Plugins beeinflusst andere nicht
  ↓
Skalierbarkeit:  Neue Plugins einfach hinzufügbar

Warum modular?

Vor (monolithisch):

  • Ein Fehler → gesamtes Programm kaputt
  • Neue Features → kompletter Rewrite
  • Skalierung unmöglich

Nach (modular):

  • Fehler isoliert ✓
  • Plugin-basiert ✓
  • Unbegrenzt erweiterbar ✓

Nächstes Kapitel: Control Methods

Control Method: DCS vs. ICS

Wie kommunizieren Plugins mit der Außenwelt?

Zwei Systeme:

DCS = Direct Control System (HTTP-basiert, Browser-Source in OBS) ICS = Interface Control System (GUI-Fenster mit pywebview, Window Capture in OBS)

# config.yaml
control_method: DCS    # Oder: ICS

DCS: Direct Control (Standard)

Plugin                    Streaming-Software (OBS)
  ↓                           ↓
Flask HTTP-Server    →    Browser-Source (http://localhost:PORT)
  → HTML/CSS wird direkt im Browser gerendert
  → Live-Updates via Server-Sent Events (SSE)

Funktioniert so:

  • Plugin startet einen Flask-Server auf localhost:PORT
  • OBS lädt die URL als Browser-Source
  • Daten werden direkt im Browser gerendert (Live-Updates via SSE)

Vorteile:

  • ✓ Schnell und direkt (keine Screencapture-Artefakte)
  • ✓ Zuverlässig
  • ✓ Transparenter Hintergrund möglich (für Overlays)

Nachteile:

  • ✗ Streaming-Software muss Browser-Source unterstützen

ICS: Interface Control (Fallback)

Plugin                    Streaming-Software (OBS)
  ↓ (pywebview Fenster)       ↓
 GUI wird angezeigt
  ↓ (Window Capture in OBS)
Overlay im Stream

Funktioniert so:

  • Plugin öffnet ein pywebview-Fenster mit der GUI
  • User nutzt Window Capture in OBS, um das Fenster aufzunehmen
  • Ergebnis: Visuelle Integration in den Stream

Vorteile:

  • ✓ Funktioniert mit jeder Streaming-Software (auch TikTok Live Studio)
  • ✓ Keine Browser-Source nötig

Nachteile:

  • ✗ Overhead durch Screencapture
  • ✗ Höhere Latenz
  • ✗ Qualitätsverlust möglich

Wann welches System?

SituationEmpfehlung
OBS Studio mit Browser-SourceDCS
TikTok Live Studio (keine Browser-Source)ICS
Custom Streaming SoftwareDCS
Lokales TestenDCS

DCS vs. ICS in der Registry

Jedes Plugin definiert bei der Registrierung, ob es ICS unterstützt:

register_plugin(AppConfig(
    name="Timer",
    path=MAIN_FILE,
    enable=True,
    level=4,
    ics=True     # Unterstützt ICS (hat pywebview-GUI)
))

register_plugin(AppConfig(
    name="App",
    path=APP_EXE_PATH,
    enable=True,
    level=2,
    ics=False    # Nur DCS (kein GUI-Fenster)
))

ics=True = Plugin hat eine pywebview-GUI und unterstützt Window Capture ics=False = Plugin läuft nur als HTTP-Server (DCS)

[!NOTE] Alle Built-in Plugins (Timer, DeathCounter, WinCounter, LikeGoal, OverlayTxt) haben ics=True.


Zur Laufzeit (start.py)

start.py prüft die control_method aus der Config und den ics-Wert jedes Plugins:

if app.ics and CONTROL_METHOD == "DCS" and app.enable:
    # Plugin hat GUI, aber DCS ist gewünscht:
    # → Starte mit --gui-hidden (nur Flask-Server, kein Fenster)
    start_exe(path=app.path, name=app.name,
              hidden=get_visibility(app.level), gui_hidden=True)
elif app.enable:
    # Normaler Start (mit GUI wenn ics=True, ohne wenn ics=False)
    start_exe(path=app.path, name=app.name,
              hidden=get_visibility(app.level))

Das bedeutet:

  • Bei control_method: DCS werden GUI-Plugins mit --gui-hidden gestartet → Flask läuft, aber kein Fenster wird geöffnet
  • Bei control_method: ICS werden GUI-Plugins normal gestartet → pywebview öffnet ein Fenster

Merksätze

  • DCS = HTTP-Server, Browser-Source in OBS
  • ICS = pywebview-Fenster, Window Capture in OBS
  • DCS ist Standard (schneller, zuverlässiger)
  • ICS ist der Fallback (wenn keine Browser-Source verfügbar)
  • Alle Built-in Plugins unterstützen ICS (ics=True)

Nächstes: PLUGIN_REGISTRY

Die PLUGIN_REGISTRY: Zentrale Plugin-Registrierung

Concept: Was ist die Registry?

Die App kann mehrere Prozesse steuern (Kern-App, GUI, Server, Plugins). Sie müssen zentral registriert und konfigurierbar sein. Dafür gibt es zwei Registries:

  1. BUILDIN_REGISTRY — fest in start.py definierte Core-Module
  2. PLUGIN_REGISTRY — dynamisch aus PLUGIN_REGISTRY.json geladene Plugins

Die AppConfig-Klasse

Jeder Registry-Eintrag ist eine AppConfig-Instanz (definiert in core/models.py):

@dataclass(slots=True)
class AppConfig:
    name: str      # Eindeutiger Name (z.B. "Timer")
    path: Path     # Absoluter Pfad zur EXE
    enable: bool   # Soll das Plugin starten?
    level: int     # Log-Level für Sichtbarkeit
    ics: bool      # Hat GUI? (Interface Control System)

Die fünf Parameter

ParameterTypBeispielFunktion
namestr"Timer"Eindeutige Identität (Logs, Status)
pathPathPath("plugins/timer/main.exe")Absoluter Pfad zur EXE
enableboolTrueStartet Plugin beim Boot?
levelint4Log-Level für Terminal-Sichtbarkeit
icsboolTrueUnterstützt GUI-Fenster (pywebview)?

[!IMPORTANT] Alle fünf Parameter sind Pflicht. Fehlt einer oder ist ein unbekannter Key vorhanden, wird ein ValueError geworfen.

Log-Level Bedeutung

Der level-Parameter steuert die Terminal-Sichtbarkeit abhängig vom log_level in der config.yaml:

LevelNameBeschreibung
0OffVersteckt alles, inklusive GUI-Fenster
1SilentVersteckt Konsolen-Fenster, GUI bleibt aktiv
2StandardZeigt nur Hauptprogramme
3AdvancedZeigt auch Hintergrund-Dienste
4DebugZeigt alle aktivierten Prozesse
5OverrideZeigt jeden Prozess, auch wenn enable=False

Logik: Ein Plugin ist sichtbar, wenn log_level >= plugin.level.

Level 0 und Level 5 sind Sonderfälle:

  • Level 0 versteckt alles und setzt gui_hidden=True
  • Level 5 überschreibt alle enable-Werte und zeigt alles

BUILDIN_REGISTRY (Core-Module)

Die Core-Module sind direkt in start.py definiert:

BUILDIN_REGISTRY: list[AppConfig] = [
    AppConfig(name="App",              path=APP_EXE_PATH,    enable=True,        level=2, ics=False),
    AppConfig(name="Minecraft Server", path=SERVER_EXE_PATH, enable=True,        level=2, ics=False),
    AppConfig(name="GUI",              path=GUI_EXE_PATH,    enable=GUI_ENABLED, level=2, ics=False),
]

Diese können nicht von außen verändert werden — sie sind fester Bestandteil des Systems.


PLUGIN_REGISTRY (Dynamische Plugins)

Plugins werden in PLUGIN_REGISTRY.json gespeichert. Diese Datei wird automatisch beim Start geladen:

[
  {
    "name": "Timer",
    "path": "C:\\...\\plugins\\timer\\main.exe",
    "enable": true,
    "level": 4,
    "ics": true
  },
  {
    "name": "Death Counter",
    "path": "C:\\...\\plugins\\deathcounter\\main.exe",
    "enable": true,
    "level": 4,
    "ics": true
  }
]

Wie Plugins sich registrieren

Plugins registrieren sich über das --register-only Flag. Der Ablauf:

1. registry.exe findet alle main.exe in plugins/
   ↓
2. Für jede main.exe: Startet mit --register-only
   ↓
3. Plugin gibt AppConfig als JSON aus (REGISTER_PLUGIN: {...})
   ↓
4. registry.exe schreibt in PLUGIN_REGISTRY.json
   ↓
5. start.py liest PLUGIN_REGISTRY.json und startet Plugins

Im Plugin (main.py):

from core import parse_args, register_plugin, AppConfig, get_base_file
from core.utils import load_config

args = parse_args()

if args.register_only:
    register_plugin(AppConfig(
        name="Timer",
        path=get_base_file(),
        enable=cfg.get("Timer", {}).get("Enable", True),
        level=4,
        ics=True
    ))
    sys.exit(0)

# ... Rest des Plugin-Codes

[!IMPORTANT] Zeitlimit: Der Registrierungsprozess hat ein hartes Limit von 5 Sekunden. Vor register_plugin() darf kein langsamer Code stehen (keine Netzwerkzugriffe, keine I/O-Operationen). Nach register_plugin() muss sofort sys.exit(0) folgen.


Wie start.py die Registry verarbeitet

start.py durchläuft beide Registries und startet die Plugins:

for registry in (BUILDIN_REGISTRY, PLUGIN_REGISTRY):
    for app in registry:
        if LOG_LEVEL == 0:
            # Level 0: Alles verstecken
            start_exe(path=app.path, name=app.name, hidden=True, gui_hidden=True)
        elif LOG_LEVEL == 5:
            # Level 5: Alles zeigen
            start_exe(path=app.path, name=app.name, hidden=False)
        else:
            if app.ics and CONTROL_METHOD == "DCS" and app.enable:
                # GUI-Plugin im DCS-Modus: GUI verstecken, nur Server
                start_exe(path=app.path, name=app.name,
                          hidden=get_visibility(app.level), gui_hidden=True)
            elif app.enable:
                # Normal starten
                start_exe(path=app.path, name=app.name,
                          hidden=get_visibility(app.level))

Scan-Cache (Performance)

Um den Registrierungsprozess zu beschleunigen, nutzt registry.py einen Scan-Cache (plugin_registry_scan_cache.json). Wenn sich eine Plugin-EXE nicht verändert hat (gleiche Dateigröße und Änderungszeit), wird das Ergebnis aus dem Cache verwendet statt das Plugin erneut zu starten.


Zusammenfassung

KomponenteDateiInhalt
AppConfigcore/models.pyDataclass mit 5 Pflichtfeldern
BUILDIN_REGISTRYstart.pyFest definierte Core-Module
PLUGIN_REGISTRYPLUGIN_REGISTRY.jsonDynamisch registrierte Plugins
Registrierungregistry.pyScannt Plugins mit --register-only
Scan-Cacheplugin_registry_scan_cache.jsonBeschleunigt wiederholte Scans

→ Weiter zu GUI-Architektur

GUI-Architektur: pywebview + Flask-Backend

Concept: GUI vs DCS

  • DCS: Reine HTTP-Server ohne GUI (Port-basiert)
  • GUI (ICS): Grafische Oberfläche in eigenem Fenster + HTTP-Backend
  • Vorteil: Visuell intuitiv, einfache Konfiguration für Benutzer
  • Technologie: pywebview (Electron-ähnlich) + Flask (Web-Framework)
┌─────────────────────────────────────┐
│   Streaming Tool GUI                │
│  ┌───────────────────────────────┐  │
│  │   pywebview Fenster           │  │
│  │  ┌─────────────────────────┐  │  │
│  │  │   HTML/CSS/JavaScript   │  │  │
│  │  │   (Benutzeroberfläche)  │  │  │
│  │  └──────────┬──────────────┘  │  │
│  └─────────────┼─────────────────┘  │
│                │ (HTTP-Requests)    │
│  ┌─────────────▼──────────────────┐ │
│  │   Flask Server (Backend)       │ │
│  │  • /api/config (GET/POST)      │ │
│  │  • /api/status (GET)           │ │
│  │  • /api/save (POST)            │ │
│  │  • /stream (SSE)               │ │
│  └────────────────────────────────┘ │
└─────────────────────────────────────┘

Aufbau einer GUI-Plugin-Datei

Dateistruktur eines GUI-Plugins:

gui_plugin.py
├── Flask-App initialisieren
├── Route @app.route("/"): HTML-UI zurückgeben
├── Route @app.route("/api/config"): Konfiguration laden/speichern
├── Route @app.route("/stream"): Server-Sent Events (SSE)
└── main(): pywebview-Fenster öffnen

Praktisches Beispiel: Einfaches GUI-Plugin

gui_example.py:

import os
import json
from flask import Flask, render_template_string, request
import webview

app = Flask(__name__)
CONFIG_FILE = "plugin_config.json"

HTML = """
<!DOCTYPE html>
<html><head>
    <title>GUI Plugin</title>
    <style>
        body { font-family: Arial; background: #222; color: #fff; padding: 20px; }
        button { padding: 10px 20px; background: #4CAF50; color: white; border: none; cursor: pointer; }
        input { padding: 8px; width: 200px; }
    </style>
</head>
<body>
    <h2>Konfiguration</h2>
    <input type="text" id="config_input" placeholder="Wert eingeben">
    <button onclick="fetchConfig()">Laden</button>
    <button onclick="saveConfig()">Speichern</button>
    <div id="output"></div>
    
    <script>
        function fetchConfig() {
            fetch('/api/config').then(r => r.json()).then(data => {
                document.getElementById('output').innerHTML = JSON.stringify(data);
                document.getElementById('config_input').value = data.setting || '';
            });
        }
        
        function saveConfig() {
            const value = document.getElementById('config_input').value;
            fetch('/api/config', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({setting: value})
            }).then(() => fetchConfig());
        }
        
        fetchConfig(); // Initial load
    </script>
</body>
</html>
"""

@app.route("/")
def index():
    return render_template_string(HTML)

@app.route("/api/config", methods=['GET', 'POST'])
def config_handler():
    if request.method == 'GET':
        try:
            with open(CONFIG_FILE) as f:
                return json.load(f)
        except:
            return {"setting": ""}
    else:  # POST
        data = request.json
        with open(CONFIG_FILE, 'w') as f:
            json.dump(data, f)
        return {"status": "saved"}

def main():
    # Flask im Hintergrund starten
    from threading import Thread
    Thread(target=lambda: app.run(port=5001, debug=False, use_reloader=False), daemon=True).start()
    
    # pywebview-Fenster öffnen (zeigt localhost:5001)
    webview.create_window('GUI Plugin', 'http://localhost:5001')
    webview.start()

if __name__ == '__main__':
    main()

Ablauf:

  1. pywebview öffnet ein Fenster
  2. Fenster lädt HTML von http://localhost:5001
  3. JavaScript sendet HTTP-Requests an Flask-Routes
  4. Backend verarbeitet, speichert Config, sendet Antwort

Kritische Aspekte

AspektBedeutungBeispiel
Port EindeutigkeitJedes GUI-Plugin braucht eindeutigen PortGUI: 5000, Timer: 7878, LikeGoal: 9797
ThreadingFlask muss in Thread laufen, damit Window nicht blockiertdaemon=True ist wichtig
SSE für Live-UpdatesServer-Sent Events für kontinuierliche Daten/stream für Like-Counter
CORSBei Browser-Quellen: Access-Control-Allow-Origin: * nötigStreaming-Software Browser-Source

→ Weiter zu Kommunikation & DCS

Kommunikation und DCS (HTTP-basierte Inter-Plugin-Kommunikation)

Concept: Warum HTTP zwischen Plugins?

Plugins arbeiten als separate Prozesse parallel. Kommunikation zwischen ihnen erfolgt über:

  • DCS (Direct Control System): HTTP-Requests zwischen Plugins (Port-basiert)
  • Webhooks: HTTP-POST-Requests von externen Programmen (z.B. Minecraft)

DCS ist die universelle Kommunikationsmethode – alle Plugins unterstützen sie.

Kommunikations-Pattern

┌──────────────┐       HTTP Request         ┌──────────────┐
│  Timer       ├───────────────────────────>│  WinCounter  │
│  Port 7878   │  POST /add?amount=1        │  Port 8080   │
│              │                            │              │
│              │       HTTP Response        │              │
│              │<───────────────────────────┤              │
│              │       "OK"                 │              │
└──────────────┘                            └──────────────┘

DCS Request-Response Workflow

Schritt-für-Schritt:

  1. Source-Plugin sendet HTTP-Request an http://localhost:PORT/endpoint
  2. Target-Plugin empfängt Request, verarbeitet Aktion
  3. Target antwortet mit JSON oder Status
  4. Source-Plugin verarbeitet Response (ggf. Fehlerbehandlung)

Wichtig: Requests sollten in Threads erfolgen, sonst blockiert das aufrufende Plugin!

Praktisches Beispiel: Timer ruft WinCounter auf

Im echten Code passiert genau das: Wenn der Timer bei 0 ankommt, sendet er einen HTTP-POST an den WinCounter, um einen Win hinzuzufügen.

WinCounter (Server auf Port 8080):

@app.route("/add", methods=["POST"])
def add():
    win_manager_instance.add_win(int(request.args.get('amount', 1)))
    return "OK"

Timer (Client):

WIN_PORT = cfg.get("WinCounter", {}).get("WebServerPort", 8080)
ADD_URL = f"http://localhost:{WIN_PORT}/add?amount=1"

class API:
    def on_timer_end(self):
        print(f"[ACTION] Timer 0 erreicht. Sende POST an {ADD_URL}")
        try:
            requests.post(ADD_URL, timeout=2)
        except Exception as e:
            print(f"[ERROR] Konnte Counter nicht erreichen: {e}")

Port-Zuordnung im Projekt

Jedes Plugin hat seinen eigenen Port, definiert in config.yaml:

PluginPortConfig-Key
GUI5000GUI.Port
OverlayTxt5005Overlaytxt.Port
MinecraftServerAPI7777MinecraftServerAPI.WebServerPort
Timer7878MinecraftServerAPI.WebServerPortTimer
DeathCounter7979MinecraftServerAPI.DEATHCOUNTER_PORT
WinCounter8080WinCounter.WebServerPort
LikeGoal9797Gifts.LIKE_GOAL_PORT

[!IMPORTANT] Jeder Port muss eindeutig sein. Wenn zwei Plugins den gleichen Port nutzen, schlägt der Start fehl.

Kritische Fehler vermeiden

FehlerProblemLösung
Synchrone Requests im Main-ThreadGUI/Server blockiertIn Thread oder mit timeout arbeiten
Nicht erreichbare Plugins"Connection refused"Port prüfen, Plugin läuft noch nicht?
Timeout zu kurzRequest bricht abMin. timeout=2 setzen
Request ohne Error-HandlingAbsturz bei FehlerImmer try/except nutzen

Server-Sent Events (SSE): Live-Updates an den Browser

Viele Plugins nutzen Server-Sent Events um ihre Daten in Echtzeit an OBS (Browser-Source) oder das pywebview-Fenster zu senden. Das Grundprinzip:

  1. Browser öffnet eine persistente Verbindung zu /stream
  2. Server sendet Daten über yield (kein return)
  3. Browser aktualisiert sich automatisch bei neuen Daten
@app.route("/stream")
def stream():
    q = Queue()
    manager.listeners.append(q)
    def event_stream():
        yield f"data: {json.dumps(manager.get_data())}\n\n"
        while True:
            data = q.get()
            yield f"data: {json.dumps(data)}\n\n"
    return Response(event_stream(), mimetype="text/event-stream")

Im Browser (JavaScript):

const es = new EventSource("/stream");
es.onmessage = (e) => {
    const data = JSON.parse(e.data);
    document.getElementById('counter').innerText = data.count;
};

Dieses Muster wird von DeathCounter, WinCounter, LikeGoal und OverlayTxt verwendet.


Webhooks: Events von Minecraft empfangen

Plugins können über einen /webhook-Endpoint Events von Minecraft empfangen. Das MinecraftServerAPI-Plugin im Minecraft-Server sendet HTTP-POST-Requests an alle konfigurierten URLs.

@app.route('/webhook', methods=['POST'])
def webhook():
    data = request.json
    event = data.get("event")
    
    if event == "player_death":
        death_manager.add_death()
    elif event == "player_respawn":
        # Reaktion auf Respawn
        pass
    
    return {"status": "ok"}, 200

Die Webhook-URLs werden in configServerAPI.yml konfiguriert:

webhooks:
  urls:
    - "http://localhost:7777/webhook"    # Main App
    - "http://localhost:7878/webhook"    # Timer
    - "http://localhost:7979/webhook"    # DeathCounter
    - "http://localhost:8080/webhook"    # WinCounter

[!TIP] Für eine ausführliche Anleitung zur Webhook-Implementierung in eigenen Plugins siehe Kapitel: Webhook-Events und Minecraft-Integration.


Zusammenfassung

KommunikationswegRichtungBeispiel
DCS (HTTP-Requests)Plugin → PluginTimer ruft WinCounter /add auf
SSE (Server-Sent Events)Plugin → Browser/OBSDeathCounter aktualisiert Overlay
WebhooksMinecraft → Pluginplayer_death Event an DeathCounter
  • DCS = HTTP-basierte Inter-Plugin-Kommunikation
  • SSE = Live-Updates an Browser-Source oder pywebview
  • Webhooks = Events von externen Programmen empfangen
  • Ports müssen eindeutig und in config.yaml konfiguriert sein
  • Fehlerbehandlung mit try/except und timeout ist Pflicht

→ Weiter zu Plugins in Streaming-Software einbinden

Plugin in Streaming-Software integrieren

Concept: Zwei Wege der Integration

Die Streaming-Tools (OBS, Streamlabs, etc.) brauchen Zugriff auf deine Plugins:

  1. ICS (GUI-Plugins): Fensteraufnahme → Das GUI-Fenster wird als Video-Layer gezeigt
  2. DCS (HTTP-Plugins): Browser-Quelle → Browser rendert HTML vom HTTP-Server

Vergleich: ICS vs DCS

AspektICSDCS
IntegrationWindow CaptureBrowser Source
TechnologieFensteraufnahmeHTTP + HTML/CSS
SkalierungNative FenstergrößeFlexibel konfigurierbar
LatenzHöher (Screenshot-zu-Screenshot)Niedriger (direkte Renderung)
FehlerbehandlungFenster muss sichtbar seinPort muss online sein
Best forDesktop GUI ToolsLive-Daten (Like-Counter, Timer)

ICS-Integration: Fensteraufnahme

In OBS:

  1. Quelle+Fensteraufnahme hinzufügen
  2. Dropdown: Wähle GUI-Anwendung (z.B. "GUI Plugin [timer.exe]")
  3. Größe/Position anpassen
  4. Fertig – GUI wird live ins Stream übernommen

Voraussetzung: Plugin muss mit ics=True in Registry registriert sein. Das Plugin selbst öffnet das pywebview-Fenster — start.py startet nur den Prozess.

DCS-Integration: Browser-Quelle

In OBS:

  1. Quelle+Browserquelle hinzufügen
  2. URL eingeben: http://localhost:PORT (z.B. http://localhost:9797)
  3. Breite/Höhe einstellen (z.B. 1280×720)
  4. Aktualisierungsrate: 60 FPS
  5. Fertig – Browser rendert deine HTML-UI live

Praktisches Beispiel:

Der Like-Counter läuft auf Port 9797:

# likegoal.py
@app.route("/")
def index():
    return f"""
    <html>
    <style>
        body {{ background: transparent; color: #fff; font-size: 48px; text-align: center; padding: 20px; }}
    </style>
    <body>
        <h1 id="count">Loading...</h1>
        <script>
            setInterval(() => {{
                fetch('/api/like_count')
                    .then(r => r.json())
                    .then(d => document.getElementById('count').innerText = d.count);
            }}, 500);
        </script>
    </body>
    </html>
    """

@app.route("/api/like_count")
def get_like_count():
    return {"count": current_likes}

# Flask im Thread starten
Thread(target=lambda: app.run(port=9797, debug=False, use_reloader=False), daemon=True).start()

In OBS: Browser-Quelle mit URL http://localhost:9797 → Live Like-Counter overlay!

Häufige Probleme & Lösungen

ProblemUrsacheLösung
URL not reachablePort blockiert/falschnetstat -ano check, Firewall öffnen
Browser-Quelle zeigt blankCORS-Fehler / HTML lädt nichtBrowser-Konsole inspizieren (F12)
Fenster-Capture funktioniert nichtModul nicht mit ics=TrueRegistry überprüfen, level checken
Latenz, VerzögerungServer zu langsamServer-Rendering optimieren, Bilder komprimieren
Es gibt nur Browser-Quelle, aber mein Modul hat ics=TrueDas ist OKics=True bedeutet "unterstützt auch ICS", nicht "muss ICS nutzen"

Integration-Checkliste

Für DCS (HTTP):

  • ☑ Flask/Server läuft im Hintergrund
  • ☑ Port in Registry korrekt
  • ☑ HTML mit transparent background (falls Overlay)
  • ☑ Browser-Quelle in OBS mit korrekter URL
  • ☑ Aktualisierungsrate 30-60 FPS

Für ICS (Window Capture):

  • ☑ pywebview öffnet Fenster
  • ☑ Registry: ics=True
  • log_level >= Modul-level
  • ☑ Fenster-Titel eindeutig
  • ☑ OBS wählt über Dropdown

Nächster Schritt: Eigenes Plugin erstellen

Python in diesem Projekt

Python ist die zentrale Sprache dieses Projekts. In diesem Kapitel lernst du, wie das Projekt organisiert ist und welche Teile zusammenspielen.


Warum Python?

Python wurde für dieses Projekt gewählt, weil es:

  • Schnell zu entwickeln ist (wenig Boilerplate-Code)
  • Gutes Ökosystem für Web (Flask), Async (asyncio), und APIs (TikTokLive) hat
  • Lesbar und wartbar bleibt, es auch selbst wenn es komplex wird
  • Cross-Platform funktioniert (Windows, macOS, Linux)

Die Hauptkomponenten

1. main.py – Das Herzstück

# Vereinfachtes Schema
def main():
    1. Config laden
    2. TikTok Client einrichten
    3. Event-Handler registrieren
    4. Client verbinden (parallel)
    5. Startup-Services starten (Server, GUI, Plugins)
    6. Event-Queue verarbeiten (Hauptloop)

Diese Datei verbindet TikTok mit dem Rest des Systems.

2. core/ – Wiederverwendbare Module

Was darin ist:

ModulZweck
models.pyDatenstrukturen (AppConfig, PluginInfo, etc.)
cli.pyCommand-Line Argumente parsen
paths.pyPfad-Funktionen (ROOT_DIR, BASE_DIR, etc.)
utils.pyHelferfunktionen (Strings bereinigen, etc.)
validator.pyConfig validieren

Diese Module kannst du überall im Projekt importieren:

from core import load_config, register_plugin, get_root_dir

3. server.py – Minecraft-Server-Starter

Startet den Minecraft-Server als Subprocess:

config.yaml
    ↓ (Xms, Xmx, Port, RCON)
server.py
    ↓ java -jar server.jar
Minecraft Server läuft

[!NOTE] Der Webhook-Endpunkt (/webhook) befindet sich in main.py, nicht in server.py.

4. registry.py – Plugin-Verwaltung

Lädt und verwaltet alle Plugins:

# Vereinfacht
PLUGIN_REGISTRY = [
    {"name": "App", "path": ..., "enable": True, ...},
    {"name": "Timer", "path": ..., "enable": True, ...},
    # Mehr Plugins...
]

5. plugins/ – Frei erweiterbar

Hier schreibst du deine eigenen Plugins:

src/plugins/
├── timer/
│   ├── main.py        # Timer-Logik
│   ├── README.md
│   └── version.txt
│
├── my_custom_plugin/  # Dein Plugin!
│   ├── main.py
│   ├── README.md
│   └── version.txt

Der Datenfluss (Vereinfacht)

TikTok Live Stream
    ↓
TikTokLive API (WebSocket)
    ↓
main.py (empfängt Events)
    ↓
Event-Handler registriert (z.B. on_gift, on_follow)
    ↓
Trigger finden + in Queue legen
    ↓
Main-Loop verarbeitet Queue
    ↓
RCON → Minecraft Server
    ↓
Minecraft Server führt Command aus

Wichtig: Das ist NICHT synchron. Events warten in einer Queue, bis sie verarbeitet werden können.


Importe verstehen

Wenn du Python-Dateien im Projekt öffnest, siehst du Importe wie:

from TikTokLive import TikTokLiveClient, TikTokLiveConnection
from TikTokLive.events import GiftEvent, FollowEvent, LikeEvent

Das sind externe Bibliotheken (nicht eingebaut in Python):

BibliothekZweck
TikTokLiveVerbindung zu TikTok Live
FlaskWeb-Framework für Webhooks
pywebviewDesktop-GUI Fenster
pyyamlConfig-Dateien lesen
asyncioAsynchrone Programmierung

Alle sind in requirements.txt aufgelistet.


Threading & Asynchronität (kurz angerissen)

Das Projekt nutzt Threading an mehreren Stellen:

TikTok Event empfangen (Thread 1)
    ↓
Queue füllen
    ↓
Main-Loop verarbeiten (Thread 2)
    ↓
Minecraft-Command senden

Warum? Weil TikTok Events nicht warten können. Wenn der Main-Loop gerade Minecraft bedient, müssen neue Events dennoch ankommen können.

Threading können kompliziert sein, deshalb behandeln wir das später genauer in Threading-and-Queues.


Welche Dateien brauchst du zum Verstehen?

Wir konzentrieren uns auf diese Dateien:

  1. main.py – Wie Daten hereinkommen
  2. server.py – Wie der Minecraft-Server gestartet wird
  3. registry.py – Wie Plugins geladen werden
  4. core/ – Hilfsfunktionen

Für Plugin-Entwicklung:

  • src/plugins/timer/main.py – Gutes Beispiel
  • config.yaml – Plugin-Konfiguration

Nicht relevant fürs erste Verständnis:

  • Build-Skripte (build.ps1, upload.ps1)
  • Migrations-Code für config
  • templatefiles

Nächster Schritt

Jetzt verstehst du die grobe Struktur. Der nächste Teil taucht tiefer ein.

Die main.py Datei

Dort sehen wir, wie die Hauptdatei aufgebaut ist und welche Aufgaben sie erfüllt.

Die Datei main.py

main.py ist das Herzstück deines Projekts. Sie ist dafür verantwortlich, mit TikTok verbunden zu bleiben und alle Events zu empfangen.


Was macht main.py?

Wenn du das Programm startest, macht main.py diese Schritte (vereinfacht):

1. Konfiguration laden (config.yaml)
     ↓
2. TikTok-Client einrichten
     ↓
3. Event-Handler registrieren ("Höre auf Gifts, Follows, Likes")
     ↓
4. Mit TikTok verbinden (bleibt verbunden)
     ↓
5. Events empfangen (kontinuierlich)
     ↓
6. Events verarbeiten & in Queue legen
     ↓
7. [Während Schritt 6 läuft: Main-Loop verarbeitet Queue]

Das ist nicht linear. Schritte 5, 6 und 7 laufen gleichzeitig (parallel) ab.


Aufbau von main.py (auf hoher Ebene)

# 1. IMPORTE
from TikTokLive import TikTokLiveClient
from TikTokLive.events import GiftEvent, FollowEvent, LikeEvent
from core.validator import validate_file, print_diagnostics
from core.paths import get_base_dir

# 2. GLOBALE VARIABLEN
MAIN_LOOP = ... # Referenz zur Hauptschleife
trigger_queue = Queue()  # Warteschlange der Trigger
like_queue = Queue()     # Warteschlange für Likes

# 3. FUNKTIONEN ZUM ERSTELLEN DES CLIENTS
def create_client(user):
    client = TikTokLiveClient(unique_id=user)
    
    @client.on(GiftEvent)
    def on_gift(event):
        # Reagiere auf Gift
        pass
    
    # Ähnlich: on_follow, on_like, etc.
    return client

# 4. HAUPTFUNKTION
def main():
    # Lade Config
    cfg = load_config(...)
    
    # Starte Client
    client = create_client(cfg["tiktok_user"])
    
    # Starte andere Services (Server, GUI, Plugins)
    # ...
    
    # Hauptloop (verarbeitet Queue)
    while True:
        event = trigger_queue.get()  # Nächsten Event holen
        process_trigger(event)       # Verarbeiten

Die Rolle von Importe

Am Anfang von main.py siehst du:

from TikTokLive import TikTokLiveClient
from TikTokLive.events import GiftEvent, FollowEvent, ConnectEvent, LikeEvent

Das heißt:

  • TikTokLiveClient: Ein Objekt, das die Verbindung zu TikTok herstellt
  • GiftEvent: Wird ausgelöst, wenn ein Gift empfangen wird
  • FollowEvent: Wird ausgelöst, wenn jemand folgt
  • LikeEvent: Wird ausgelöst, wenn Likes eintreffen

Diese werden später verwendet, um Event-Handler zu registrieren.

Weitere Importe:

from core.validator import validate_file, print_diagnostics
from core.paths import get_base_dir

Das sind Kern-Module (vom Projekt selbst), nicht externe Bibliotheken.


Schritt 1: Konfiguration laden

CONFIG_FILE = get_root_dir() / "config" / "config.yaml"

try:
    with open(CONFIG_FILE, "r", encoding="utf-8") as f:
        cfg = yaml.safe_load(f)
except Exception as e:
    print(f"FEHLER: Config konnte nicht geladen werden: {e}")
    sys.exit(1)  # Programm beenden

Das liest die config.yaml:

tiktok_user: "eintiktoker"
Timer:
  Enable: true
  StartTime: 10

Wenn das fehlschlägt, bricht das Programm ab (weil es ohne Config nicht funktioniert).


Schritt 2 & 3: Client erstellen & Handler registrieren

def create_client(user):
    """Erstelle einen TikTok-Live-Client für den angegebenen User"""
    client = TikTokLiveClient(unique_id=user)
    
    # Jetzt registrieren wir Event-Handler
    # Handler = "Funktionen, die ausgeführt werden, wenn ein Event kommt"
    
    @client.on(GiftEvent)
    def on_gift(event: GiftEvent):
        # Diese Funktion wird JEDES MAL aufgerufen, wenn ein Gift kommt
        pass  # Logik kommt später
    
    @client.on(FollowEvent)
    def on_follow(event: FollowEvent):
        # Diese Funktion wird aufgerufen, wenn jemand folgt
        pass
    
    @client.on(LikeEvent)
    def on_like(event: LikeEvent):
        # Diese Funktion wird aufgerufen, wenn Likes eintreffen
        pass
    
    return client  # Gib den konfigurierten Client zurück

Das @client.on(...) ist ein Dekorator – eine Python-Funktion, die sagt: "Rufe diese Funktion auf, wenn dieses Event kommt".


Schritt 4: Mit TikTok verbinden

client = create_client(cfg["tiktok_user"])

# Verbindung starten (asynchron)
asyncio.run(client.connect())

Das verbindet sich mit dem TikTok-Stream und bleibt verbunden. Wenn ein Event kommt, ruft der Client automatisch den entsprechenden Handler auf.


Warum ist main.py komplex?

Wenn du die echte main.py öffnest, siehst du viel mehr Code als hier erklärt:

# Echte main.py hat auch:
- Error-Handling (was wenn Fehler?)
- Combo-Gifts (wiederholte Gifts)
- Race Conditions (Multi-Threading)
- Streams (VideoEvents)
- und vieles mehr...

Das macht den Code kompliziert. Aber die Kernidee bleibt gleich:

  1. Client erstellen
  2. Handler registrieren
  3. Verbinden
  4. Events verarbeiten

Was kommt in den Event-Handlern?

Die eigentliche Magie passiert in den Event-Handlern:

@client.on(GiftEvent)
def on_gift(event: GiftEvent):
    # 1. Gift-Details auslesen
    gift_name = event.gift.name
    user = event.user.nickname
    
    # 2. Prüfen, ob diese Gift konfiguriert ist
    if gift_name in valid_functions:
        # 3. Trigger in Warteschlange legen
        MAIN_LOOP.call_soon_threadsafe(
            trigger_queue.put_nowait,
            (gift_name, user)
        )

Aber das wird später ausführlich besprochen.


Die Rolle des Hauptprogramms

main.py ist nicht die einzige Datei, die läuft. Daneben gibt es auch:

  • server.py: Startet den Minecraft-Server (Java-Subprocess, RCON-Konfiguration, server.properties)
  • registry.py: Lädt und startet alle Plugins
  • gui.py: Zeigt ein Admin-Interface

Zusammenfassung

main.py macht:

✓ Konfiguration laden
✓ TikTok-Client erstellen
✓ Event-Handler registrieren
✓ Mit TikTok verbinden
✓ Events empfangen & verarbeiten
✓ In Warteschlange legen

Alles läuft parallel ab – nicht nacheinander.


Nächster Schritt

Jetzt verstehst du die Struktur. Der nächste Schritt ist es, die Importe genauer zu verstehen.

Importe

Dort wirst du sehen, was mit den importieren Modulen angestellt wird.

Importe

Bevor wir mit der TikTok-Verarbeitung starten, müssen wir verstehen, welche Teile des Projekts wir wo verwenden.

from TikTokLive import TikTokLiveClient
from TikTokLive.events import GiftEvent, FollowEvent, ConnectEvent, LikeEvent

Was bedeutet das?

In Python bedeutet:

  • from ... import ... → „Hole bestimmte Dinge aus einer Bibliothek“

Eine Bibliothek ist einfach eine Sammlung von vorgefertigtem Code, den wir nutzen können, anstatt alles selbst zu schreiben.

Mehr dazu hier: https://en.wikipedia.org/wiki/Library_(computing)


Warum importieren wir nur bestimmte Teile?

Man importiert in der Regel nicht die komplette Bibliothek, sondern nur die Teile, die man wirklich braucht. Das hat mehrere Vorteile:

  • der Code bleibt übersichtlicher
  • es wird weniger unnötiger Code geladen
  • es ist klarer, was im Projekt verwendet wird

In unserem Fall importieren wir:

  • TikTokLiveClient → stellt die Verbindung zu TikTok her
  • GiftEvent → wird ausgelöst, wenn ein Gift gesendet wird
  • FollowEvent → wird ausgelöst, wenn jemand folgt
  • ConnectEvent → wird ausgelöst, wenn die Verbindung hergestellt wird
  • LikeEvent → wird ausgelöst, wenn Likes eingehen

Die Namen sind relativ selbsterklärend und helfen dabei, schnell zu verstehen, wofür sie zuständig sind.


[!IMPORTANT] Bevor du eine externe Bibliothek verwenden kannst (also eine, die nicht standardmäßig zu Python gehört), musst du sie installieren.

Für dieses Projekt verwendest du:

pip install TikTokLive

Falls das nicht funktioniert, kannst du alternativ:

python -m pip install TikTokLive

verwenden.

TikTok-Client & Event-Handler

Das ist das Herzstück der Event-Verarbeitung. Hier erstellen wir die Verbindung zu TikTok und registrieren Funktionen, die auf Events reagieren.


Funktion

1. Erstelle TikTok-Client
   ↓
2. Registriere Handler für bestimmte Events
   ↓
3. Handler wird AUTOMATISCH aufgerufen, wenn Event kommt

Das ist nicht "Polling" (ständig fragen "ist was los?"), sondern Event-Driven (das System sagt dir, wenn was los ist).

Visualisiert:

TikTok Live Stream läuft...
    ↓
    ↓ [Event kommt: jemand sendet Gift]
    ↓
TikTokLive API ruft automatisch: on_gift(event)
    ↓
on_gift() wird ausgeführt
    ↓
Wir verarbeiten das Gift

Schritt 1: Client erstellen

from TikTokLive import TikTokLiveClient
from TikTokLive.events import GiftEvent, FollowEvent, LikeEvent

def create_client(user):
    """Erstelle einen TikTok-Live-Client"""
    client = TikTokLiveClient(unique_id=user)
    return client

Was passiert:

  • TikTokLiveClient(unique_id=user) verbindet sich mit einem bestimmten TikTok-Account
  • Der Client lauscht auf alle Events aus diesem Stream
  • Noch keine Handler registriert – das kommt als Nächstes

Schritt 2: Handler registrieren

Ein Handler ist einfach eine Funktion, die auf ein Event reagiert. Wir nutzen den Dekorator-Ansatz:

@client.on(GiftEvent)
def on_gift(event: GiftEvent):
    print(f"Gift empfangen: {event.gift.name}")

Das @client.on(...) Dekorator sagt: "Rufe diese Funktion auf, wenn ein GiftEvent kommt."

Ähnlich für andere Events:

@client.on(FollowEvent)
def on_follow(event: FollowEvent):
    print(f"Neue Follow: {event.user.nickname}")

@client.on(LikeEvent)
def on_like(event: LikeEvent):
    print(f"Likes insgesamt: {event.total}")

Schritt 3: Handler mit Logik füllen

Das ist wo's interessant wird. Der Handler muss:

  1. Event-Daten auslesen – Was ist im Event?
  2. Validieren – Ist es ein gültiges Event?
  3. Trigger finden – Welche Aktion soll ausgelöst werden?
  4. In Queue legen – Nicht sofort ausführen, sondern queuen!

Grund für Queue: Events kommen sehr schnell. Wenn wir sie sofort verarbeiten, könnte die nächste Event-Verarbeitung blockieren oder die RCON verbindung bricht ab.

Vereinfachtes Beispiel:

@client.on(GiftEvent)
def on_gift(event: GiftEvent):
    # 1. Event-Daten auslesen
    gift_name = event.gift.name           # z.B. "Rose"
    gift_id = event.gift.id               # z.B. 1001
    repeat_count = event.repeat_count     # z.B. 3 (Combo)
    user = event.user.nickname            # z.B. "Streamer123"
    
    # 2. Validieren (ist alles OK?)
    if not gift_name or not user:
        logger.warning("Invalid gift data")
        return
    
    # 3. Trigger finden (gibt's eine Action für dieses Gift?)
    trigger = None
    if gift_name in VALID_ACTIONS:
        trigger = gift_name
    elif str(gift_id) in VALID_ACTIONS:
        trigger = str(gift_id)
    
    if not trigger:
        logger.debug(f"No action configured for gift: {gift_name}")
        return
    
    # 4. In Queue legen (nicht sofort ausführen!)
    for _ in range(repeat_count):
        MAIN_LOOP.call_soon_threadsafe(
            trigger_queue.put_nowait,
            (trigger, user)
        )
        logger.info(f"Gift queued: {gift_name} from {user}")

Schritt 4: Fehlerbehandlung

Events können fehlschlagen – wir müssen es abfangen:

@client.on(GiftEvent)
def on_gift(event: GiftEvent):
    try:
        # Dein Code hier
        gift_name = event.gift.name
        # ... Rest der Logik
        
    except AttributeError as e:
        logger.error(f"Gift data is incomplete: {e}")
    except Exception as e:
        logger.error(f"Unexpected error in on_gift: {e}", exc_info=True)
        # Wichtig: exc_info=True zeigt den kompletten Error-Stack

Warum wichtig:

  • Event-Handler darf nicht das ganze Programm crashen
  • Andere Events sollten weiterhin verarbeitet werden
  • Fehler sollten geloggt werden (für Debugging)

Die echte Implementierung (Production Code)

Der echte Code ist komplexer, weil er mit Edge Cases umgehen muss:

@client.on(GiftEvent)
def on_gift(event: GiftEvent):
    try:
        # Combo-Gifts können mehrfach gesendet werden
        if event.gift.combo:
            # Prüfe auf "Streak" (wiederholte Combo)
            if getattr(event, 'streaking', False):
                try:
                    if event.streaking:
                        return  # Ignoriere Streak-Intermediate Events
                except AttributeError:
                    pass
            
            repeat_count = event.repeat_count
        else:
            repeat_count = 1  # Non-Combo = einmal
        
        # Gift-Daten sicher machen
        gift_name = sanitize_filename(event.gift.name)
        gift_id = str(event.gift.id)
        
        # Extra-Aktion ausführen (z.B. Sound)
        execute_gift_action(gift_id)
        
        # Trigger finden
        target = None
        if gift_name in valid_functions:
            target = gift_name
        elif gift_id in valid_functions:
            target = gift_id
        
        if not target:
            return
        
        username = get_safe_username(event.user)
        
        # Mehrfach in Queue legen (bei Combos)
        for _ in range(repeat_count):
            MAIN_LOOP.call_soon_threadsafe(
                trigger_queue.put_nowait,
                (target, username)
            )
    
    except Exception:
        logger.error("ERROR in on_gift handler:", exc_info=True)

Die Komplexität verstehen

Der echte Code ist komplexer wegen:

KomplexitätGrund
if event.gift.comboManche Gifts können mehrfach wiederholt werden
getattr(event, 'streaking', False)Attribute existieren vielleicht nicht – sicher prüfen
sanitize_filename()Benutzernamen könnten spezielle Zeichen haben
call_soon_threadsafe()Multiple Threads – müssen thread-safe kommunizieren
Try-ExceptEvents dürfen nicht das ganze System crashen

Zusammenfassung

Ein TikTok-Client-Handler:

  1. ✓ Lauscht auf Events
  2. ✓ Empfängt Event-Daten
  3. ✓ Validiert die Daten
  4. ✓ Findet passende Aktion
  5. ✓ Legt in Queue (nicht sofort ausführen!)
  6. ✓ Fehler abfangen (crasht nicht)

Nächster Schritt

Jetzt verstehst du, wie Handler arbeiten. Der nächste Schritt ist verstehen, was im Event selbst steckt.

Event-System verstehen

Dort schauen wir, welche Daten jedes Event hat und wie wir sie nutzen.

Event-System verstehen

Bevor wir spezifische Event-Typen (Gifts, Follows, Likes) analysieren, müssen wir verstehen, wie Events strukturiert sind und wie sie fließen.


Event-Struktur

Ein Event ist nicht einfach "etwas ist passiert" – es enthält Daten über das, was passiert ist.

GiftEvent Beispiel

# Ein echtes GiftEvent hat diese Struktur:
{
    "user": {
        "id": "123456789",
        "nickname": "Streamer123",
        "signature": "Ich liebe Minecraft",
        # ... mehr User-Daten
    },
    "gift": {
        "id": 1001,
        "name": "Rose",
        "repeat_count": 3,              # Wie oft wurde dieser Gift gesendet?
        "combo": True,                  # Kann dieser Gift kombiniert werden?
        "description": "A beautiful rose"
    },
    "total_count": 5,                   # Gesamt-Gifts von diesem User
}

Du greifst darauf zu mit:

def on_gift(event: GiftEvent):
    event.user.nickname              # "Streamer123"
    event.gift.name                  # "Rose"
    event.gift.repeat_count          # 3
    event.gift.combo                 # True/False
    event.total_count                # 5

Event-Objekte vs. Dictionaries

Die Ereignisse sind nicht einfach Dictionaries (wie {"name": "Rose"}), sondern Objekte:

# Objekt (was wir nutzen)
event.gift.name      # ✓ Funktioniert, IDE gibt Autocomplete

# Dictionary (würde nicht funktionieren)
event["gift"]["name"]  # ✗ Komplizierter, keine IDE-Hilfe

Warum Objekte besser sind:

  • IDE kann Autocomplete geben (z.B. event.gift.<Vorschlag>)
  • Typ-Sicherheit (Python weiß, dass event.gift.name ein String ist)
  • Weniger Fehler (falsche Schlüssel → sofort Error statt Silent-Fail)

Event-Kategorien

Events werden in mehrere Kategorien eingeteilt:

KategorieBeispieleZweck
User EventsFollow, Gift, LikeAktion eines Zuschauers
System EventsConnect, DisconnectSystem-Status
Stream EventsStreamStart, StreamEndStream-Lebenszyklus

Für diese Dokumentation konzentrieren wir uns auf:

  • ✓ GiftEvent (Zuschauer sendet Gift)
  • ✓ FollowEvent (Zuschauer folgt)
  • ✓ LikeEvent (Zuschauer gibt Like)

Event-Handler Workflow

Wenn ein Event kommt, passiert folgendes:

1. TikTok Live Stream (etwas passiert)
   ↓
2. TikTokLive API empfängt Event (über WebSocket)
   ↓
3. Client sucht passenden Handler (@client.on(...))
   ↓
4. Handler-Funktion wird aufgerufen mit Event-Daten
   ↓
5. Handler verarbeitet Event
   ↓
6. Next Event kann empfangen werden

Timing: Das alles passiert in Millisekunden!


Event-Daten Validieren

Nicht alle Event-Daten sind garantiert vorhanden. Wir müssen defensiv programmieren:

@client.on(GiftEvent)
def on_gift(event: GiftEvent):
    # ✓ Sicher: mit Fallback
    gift_name = getattr(event.gift, "name", "Unknown")
    
    # ✓ Sicher: try-except
    try:
        user_id = event.user.id
    except AttributeError:
        user_id = None
    
    # ✗ Unsicher: könnte None sein
    # repeat_count = event.repeat_count  # Was wenn Attribut fehlt?

Regel: Immer davon ausgehen, dass Daten fehlen oder None sein können.


Event-Modifikationen & Flags

Manche Events haben zusätzliche Flags oder Modifikatoren:

Combo-Flag

if event.gift.combo:  # Kann dieses Gift kombiniert werden?
    # Ja: Der Zuschauer kann dieselbe Gift mehrfach senden
    # → event.repeat_count sagt wie oft
    count = event.repeat_count  # z.B. 5
else:
    # Nein: Immer nur einmal
    count = 1

Streaking-Flag (Advanced)

# Combo-Gifts können über mehrere Sekunden "streaken"
if getattr(event, "streaking", False):
    # Das ist ein Intermediate-Event (nicht das letzte)
    # Können wir überspringen wenn wir nur finale Events wollen
    return

Fehler in Events (Exception Handling)

Events können problematisch sein. Wir müssen abfangen:

@client.on(GiftEvent)
def on_gift(event: GiftEvent):
    try:
        # Event-Daten auslesen
        gift_name = event.gift.name
        user = event.user.nickname
        
        # Logik ausführen
        # ...
    
    except AttributeError:
        # Daten fehlen
        logger.error(f"Gift event missing data: {event}")
        return
    
    except Exception as e:
        # Unexpekted
        logger.error(f"Error processing gift: {e}", exc_info=True)
        return

Wichtig: Ein fehlerhaftes Event darf nicht das ganze Programm crashen. Andere Events müssen weiterlaufen.


Event-Daten speichern & verarbeiten

Manchmal wollen wir Event-Daten später verarbeiten (z.B. in der Queue):

@client.on(GiftEvent)
def on_gift(event: GiftEvent):
    # Nicht sofort verarbeiten – speichern!
    event_data = {
        "type": "gift",
        "gift_name": event.gift.name,
        "user": event.user.nickname,
        "count": event.repeat_count,
        "timestamp": event.created_at
    }
    
    # In Queue legen (wird später verarbeitet)
    trigger_queue.put_nowait(event_data)

Warum? Weil verschiedene Threads gleichzeitig arbeiten:

Thread 1: TikTok Events empfangen (schnell!)
Thread 2: Events verarbeiten (langsamer!)

Wenn Thread 2 langsam ist, staut sich Events in der Queue auf.
Das ist OK – das ist der Sinn der Queue.

Zusammenfassung: Event-System

✓ Events sind Objekte mit strukturierten Daten
✓ Unterschiedliche Event-Typen (Gift, Follow, Like, etc.)
✓ Handler werden automatisch aufgerufen ✓ Daten müssen validiert werden
✓ Fehler müssen abgefangen werden
✓ Events werden nicht sofort verarbeitet, sondern gepuffert (Queue)


Nächster Schritt

Jetzt verstehst du das System. Der nächste Schritt ist, spezifische Event-Typen anzuschauen.

Gift-Events

Dort sehen wir, wie Gift-Events speziell funktionieren (mit Combos, Repeats, etc.).

Gift-Events

Das Besondere an Gifts: Combos und Streaks

Gifts sind nicht einfach wie Follows. Ein Geschenk kann auf drei unterschiedliche Weisen ankommen:

SituationWas passiertWie oft wird der Handler aufgerufen?
Einfaches GiftZuschauer sendet Geschenk 1x1x (sofort)
Combo-GiftZuschauer sendet gleiches Geschenk mehrfach schnell hintereinanderMehrere Male (mit repeat_count)
StreakingTikTok sendet Notifications zum aktuellen Stand der ComboMehrfach (aber: wir wollen das SKIPPEN)

Das ist wichtig zu verstehen, bevor wir Code schreiben:

Zuschauer sendet 5x Rose hintereinander:
  
  00:00 - Event: Gift='Rose', repeat_count=1, streaking=False
  00:01 - Event: Gift='Rose', repeat_count=2, streaking=False  
  00:02 - Event: Gift='Rose', repeat_count=3, streaking=False
  00:03 - Event: Gift='Rose', repeat_count=4, streaking=False
  00:04 - Event: Gift='Rose', repeat_count=5, streaking=True  ← Streak-Ende!
  
  TikTok sendet auch noch Status-Updates:
  
  00:01 - Event: Gift='Rose', repeat_count=2, streaking=True  ← IGNORIEREN!
  00:02 - Event: Gift='Rose', repeat_count=3, streaking=True  ← IGNORIEREN!
  00:03 - Event: Gift='Rose', repeat_count=4, streaking=True  ← IGNORIEREN!

Warum ist das wichtig? Wenn wir jeden Status-Update verarbeiten würden, würden wir die gleiche Action 3-5x zu viel queuen.


Gift-Event Struktur: Was können wir alles aus einem Gift auslesen?

Ein GiftEvent enthält diese wichtigsten Informationen:

event.gift.name          # Giftname: "Rose", "Diamond", etc.
event.gift.id            # Gift-ID: 1, 2, 3 (numerisch)
event.gift.combo         # Kann dieses Gift gecomboet werden? True/False

event.repeat_count       # Wie oft wurde das Gift insgesamt gesendet? 1, 2, 3, 4, 5...
event.streaking          # Ist das ein Status-Update einer laufenden Combo? True/False

event.user.nickname      # Zuschauer-Name: "anna_123"
event.user.user_id       # Zuschauer-ID (numerisch)

event.gift_type          # Art des Gifts (normalerweise: "gift")
event.description        # Detailbeschreibung (z.B. "Sent Rose x5")

Die praktische Bedeutung:

  • Wir brauchen repeat_count, um zu wissen, wie oft die Action ausgeführt werden soll
  • Wir brauchen streaking, um zu wissen, ob wir dieses Event ignorieren sollen
  • Wir brauchen gift.name ODER gift.id, um zu finden, welche Aktion passt
  • Wir brauchen user.nickname, um zu speichern, wer das Geschenk sendete

Gift-Event Processing: Der 5-Schritt-Ablauf

Wenn ein Gift-Event ankommt, passiert folgendes:

1. ANKOMMEN
   Event kommt an → ist es streaking? JA → STOPP, ignorieren
   
2. ZÄHLEN
   Ist es ein Combo-Gift? JA → count = event.repeat_count
                           NEIN → count = 1
   
3. IDENTIFIZIEREN
   Giftname auslesen: "Rose"
   Sanitieren (sicher machen): "Rose" → OK
   Benutzername auslesen: "anna_123"
   
4. MATCHEN
   Passt "Rose" zu einer Aktion? Nachschauen in valid_functions
   Wenn ja → das ist unser `target`
   Wenn nein → Event ignorieren
   
5. QUEUEN
   for i in range(count):  # 5x, weil repeat_count=5
       Queue: (target, username)
       
   jetzt wird die Action asynchron verarbeitet

Visuell:

TikTok sendet: Gift Event (Rose, repeat_count=5, streaking=False)
    ↓
[STEP 1] streaking==False? ✓ Weitermachen
    ↓
[STEP 2] combo==True? repeat_count=5 → count=5
    ↓
[STEP 3] name="Rose", user="anna_123" (sanitized)
    ↓
[STEP 4] "Rose" in valid_functions? ✓ target="GIFT_ROSE"
    ↓
[STEP 5] 5x in Queue: ("GIFT_ROSE", "anna_123")
    ↓
Worker-Thread verarbeitet alle 5 nacheinander

Spezial: Streaking-Flag

Das streaking-Flag ist wichtig, weil TikTok bei langen Combo-Sequenzen Status-Updates sendet:

# Was TikTok sendet bei 5er-Combo:

Event 1: {gift: "Rose", repeat_count: 1, streaking: False}  ✓ Verarbeiten
Event 2: {gift: "Rose", repeat_count: 2, streaking: False}  ✓ Verarbeiten
Event 3: {gift: "Rose", repeat_count: 3, streaking: False}  ✓ Verarbeiten
Event 4: {gift: "Rose", repeat_count: 4, streaking: False}  ✓ Verarbeiten
Event 5: {gift: "Rose", repeat_count: 5, streaking: True}   SKIPPEN!

Warum das streaking=True Event ignorieren?

Wenn wir es verarbeiten würden, würden wir die Aktion 5x queuen = 5x falsch! Das streaking=True Event ist nur eine Nachricht von TikTok: "Die Combo ist jetzt komplett".

Wie stellen wir sicher?

if hasattr(event, 'streaking') and event.streaking:
    return  # Ignorieren, nicht verarbeiten!

Fehlerbehandlung bei Gift-Events

Gift-Handler müssen robust sein, weil mehrere Dinge schiefgehen können:

ProblemWas kann passieren?Wie schützen wir uns?
Event hat keine gift-EigenschaftAttributeErrorgetattr() mit Default-Wert
Event hat keine user-EigenschaftAttributeErrorget_safe_username() prüft
Gift-Name/ID passt zu keiner AktionGift wird ignoriertif not target: return
Benutzername enthält ungültige ZeichenFehler beim Queuensanitize_filename() bereinigt
Queue ist voll (sehr selten)put_nowait() ExceptionTry-except um Queue-Operation

Lösung: Alles in try-except wrappen:

try:
    # Gift-Handler-Code hier
except AttributeError as e:
    logger.error(f"Gift-Event hat ungültige Struktur: {e}")
except Exception as e:
    logger.error(f"Fehler im Gift-Handler: {e}", exc_info=True)

Praktisches Beispiel: Ein vollständiger Gift-Handler

Hier ist ein realer, funktionierender Gift-Handler mit allen Sicherheits-Features:

@client.on(GiftEvent)
def on_gift(event: GiftEvent):
    """
    Verarbeitet Gift-Events von TikTok.
    - Handhabt Combos (mehrfach hintereinander)
    - Ignoriert Streaking-Events (Status-Updates)
    - Queued Aktionen für asynchrone Verarbeitung
    """
    try:
        # SCHRITT 1: Streaking-Event ignorieren?
        if hasattr(event, 'streaking') and event.streaking:
            logger.debug(f"Ignoriere Streaking-Event: {event.gift.name}")
            return
        
        # SCHRITT 2: Wie oft sollen wir die Aktion ausführen?
        if event.gift.combo:
            count = event.repeat_count  # z.B. 5 bei 5er-Combo
        else:
            count = 1  # Einzelnes Gift = 1x
        
        # SCHRITT 3: Gift-Daten sicher auslesen
        gift_name = event.gift.name  # "Rose", "Diamond", etc.
        gift_id = str(event.gift.id)  # "1", "2", etc.
        username = get_safe_username(event.user)  # "anna_123" (sanitized)
        
        logger.info(
            f"Gift empfangen: {gift_name} (ID: {gift_id}) "
            f"von {username} (x{count})"
        )
        
        # SCHRITT 4: Passendes Trigger finden
        # Zuerst nach Name suchen, dann nach ID
        target = None
        if gift_name in TRIGGERS:
            target = gift_name
        elif gift_id in TRIGGERS:
            target = gift_id
        
        if not target:
            logger.warning(f"Kein Trigger für Gift '{gift_name}' definiert")
            return
        
        # SCHRITT 5: Action in Queue legen (count-mal)
        for _ in range(count):
            try:
                MAIN_LOOP.call_soon_threadsafe(
                    trigger_queue.put_nowait,
                    (target, username)
                )
            except Exception as e:
                logger.error(
                    f"Fehler beim Queuen von Gift-Aktion: {e}",
                    exc_info=True
                )
        
        logger.debug(f"✓ {count}x Action '{target}' gequeuet")
        
    except AttributeError as e:
        logger.error(
            f"Gift-Event ist unvollständig (fehlende Property): {e}",
            exc_info=True
        )
    except Exception as e:
        logger.error(
            f"Unerwarteter Fehler im Gift-Handler: {e}",
            exc_info=True
        )

Was macht dieser Code?

  1. Streaking ignorieren – Nur echte Gift-Events verarbeiten
  2. Count bestimmen – 1x oder mehrfach?
  3. Daten auslesen – Gift-Name, ID, Benutzername
  4. Logger-Info – Sichtbarer Feedback für Debugging
  5. Trigger finden – Nach Name, dann nach ID
  6. Queue-Operation – Thread-sicher mit call_soon_threadsafe
  7. Fehlerbehandlung – Alles ist geschützt mit try-except

Noch einfacher: Minimales Beispiel

Wenn dir der obige Handler zu lang ist, hier ein minimales Beispiel, das auch funktioniert:

@client.on(GiftEvent)
def on_gift(event: GiftEvent):
    # Streaking-Events ignorieren
    if getattr(event, 'streaking', False):
        return
    
    # Wie oft?
    count = event.repeat_count if event.gift.combo else 1
    
    # Welcher Trigger?
    target = event.gift.name  # oder: event.gift.id
    
    # Queuen (count-mal)
    for _ in range(count):
        MAIN_LOOP.call_soon_threadsafe(
            trigger_queue.put_nowait,
            (target, event.user.nickname)
        )

Das ist deutlich kürzer und macht das Gleiche – aber ohne explizite Fehlerbehandlung.


Zusammenfassung & Nächster Schritt

Was du jetzt weißt:

KonzeptErklärung
Combo-GiftsGleiche Gifts mehrfach = repeat_count erhöht sich
StreakingTikTok sendet Status-Updates = wir ignorieren die
Trigger-MatchingGift-Name oder ID → zu Aktion (TRIGGERS dictionary)
Asynchrone Queuecall_soon_threadsafe macht es thread-sicher
FehlerbehandlungTry-except schützt vor unerwarteten Strukturen

Was passiert NACH dem Gift-Handler?

Die gequeuete Action wird später vom Worker-Thread verarbeitet.

Nächstes Kapitel: Follow-Events


[!TIP] Wenn du deinen eigenen Gift-Handler schreiben willst, verwende das Minimale Beispiel oben und bau dann je nach Bedarf Fehlerbehandlung ein.

Follow-Events

Das Besondere an Follows: Einfach und direkt

Follows sind deutlich einfacher als Gifts, weil:

MerkmalGiftsFollows
Mehrfach-VerarbeitungCombo möglich (5x, 10x, etc.)Immer nur 1x
Status-UpdatesStreaking: Mehrfach NotificationsKeine Notifications
Trigger-VerwaltungName UND ID möglichEin einziger Trigger: "follow"
Fehler-KomplexitätHoch (Combos, Streaking, Race Conditions)Niedrig (linearer Ablauf)

Das Gute: Follow-Handler sind perfekt zum Lernen, weil sie die Grundstruktur zeigen ohne viel Komplexität.


Follow-Event Struktur: Was können wir auslesen?

Ein FollowEvent enthält diese Informationen:

event.user.nickname      # Zuschauer-Name: "anna_xyz"
event.user.user_id       # Zuschauer-ID (numerisch)

event.follow_user.nickname    # Der Account, der gefolgt wurde
event.follow_user.user_id     # ID des gefolgten Accounts

event.timestamp          # Zeitpunkt des Events
event.event_type         # Art des Events (normalerweise: "follow")

In der Praxis: Wir brauchen vor allem event.user.nickname, um zu wissen, wer gefolgt hat.


Follow-Event Processing: Der 3-Schritt-Ablauf

Wenn ein Follow-Event ankommt, ist der Ablauf sehr einfach:

1. EVENT EMPFANGEN
   FollowEvent kommt an
   
2. BENUTZERNAMEN AUSLESEN & SANITIEREN
   username = get_safe_username(event.user)
   z.B.: "anna_xyz"
   
3. TRIGGER IN QUEUE LEGEN
   Es gibt nur einen Trigger: "follow"
   → Aktion ausführen oder ignorieren

Visuell:

Zuschauer folgt Stream
        ↓
TikTok sendet: FollowEvent
        ↓
[STEP 1] Event empfangen ✓
        ↓
[STEP 2] Username = "anna_xyz" ✓
        ↓
[STEP 3] Trigger "follow" definiert? 
         JA  → In Queue: ("follow", "anna_xyz")
         NEIN → Ignorieren
        ↓
Worker-Thread verarbeitet

Follow-Daten Struktur im Code

Wenn du im Follow-Handler debuggen möchtest, kannst du diese Properties nutzen:

@client.on(FollowEvent)
def on_follow(event: FollowEvent):
    # Wer hat gefolgt?
    follower_name = event.user.nickname
    follower_id = event.user.user_id
    
    # Wem wurde gefolgt?
    followed_user = event.follow_user.nickname
    followed_id = event.follow_user.user_id
    
    # Wann?
    timestamp = event.timestamp
    
    # Debuggen:
    print(f"{follower_name} folgt {followed_user} um {timestamp}")

Hinweis: In den meisten Fällen interessiert uns nur event.user.nickname, weil wir wissen, wer gefolgt hat.


Einfache Fehlerbehandlung

Da Follow-Events einfach sind, brauchen wir weniger Fehlerbehandlung:

try:
    username = get_safe_username(event.user)
    # ... Rest des Codes
except AttributeError:
    logger.error("Follow-Event ist unvollständig", exc_info=True)
except Exception as e:
    logger.error(f"Fehler im Follow-Handler: {e}", exc_info=True)

Hauptrisiken:

  • event.user existiert nicht? → get_safe_username() schützt
  • get_safe_username() gibt einen leeren String zurück? → OK, wird trotzdem gequeuet
  • Queue ist voll? → Sehr selten, try-except reicht

Praktisches Beispiel: Ein vollständiger Follow-Handler

Hier ist der Standard-Follow-Handler mit Best-Practices:

@client.on(FollowEvent)
def on_follow(event: FollowEvent):
    """
    Verarbeitet Follow-Events von TikTok.
    - Einfache Struktur (keine Combos)
    - Arbeitet direkt mit Trigger 'follow'
    """
    try:
        # SCHRITT 1: Benutzernamen auslesen
        username = get_safe_username(event.user)
        
        # SCHRITT 2: Logging
        logger.info(f"Follow empfangen von: {username}")
        
        # SCHRITT 3: Prüfen, ob Follow-Trigger definiert ist
        if "follow" not in TRIGGERS:
            logger.warning("Kein 'follow' Trigger definiert, ignoriere Event")
            return
        
        # SCHRITT 4: Follow-Aktion in Queue legen
        try:
            MAIN_LOOP.call_soon_threadsafe(
                trigger_queue.put_nowait,
                ("follow", username)
            )
            logger.debug(f"✓ Follow-Action gequeuet für: {username}")
        except Exception as e:
            logger.error(
                f"Fehler beim Queuen von Follow-Aktion: {e}",
                exc_info=True
            )
    
    except AttributeError as e:
        logger.error(
            f"Follow-Event ist unvollständig: {e}",
            exc_info=True
        )
    except Exception as e:
        logger.error(
            f"Unerwarteter Fehler im Follow-Handler: {e}",
            exc_info=True
        )

Was macht dieser Code?

  1. Username auslesen & sanitieren – Mit get_safe_username()
  2. Logging – Wir sehen im Log, wer folgt
  3. Trigger prüfen – Existiert der "follow" Trigger?
  4. Queuen – Mit call_soon_threadsafe()
  5. Fehlerbehandlung – Alles geschützt

Noch einfacher: Minimales Beispiel

Der absolute Minimal-Handler (funktioniert auch!):

@client.on(FollowEvent)
def on_follow(event: FollowEvent):
    username = get_safe_username(event.user)
    MAIN_LOOP.call_soon_threadsafe(
        trigger_queue.put_nowait,
        ("follow", username)
    )

Das ist es! Drei Zeilen, macht genau das Gleiche.


Unterschied zu Gifts (Wiederholung zum Vergleich)

Um zu verstehen, warum Follow-Handler so einfach sind:

Gift-Handler:

if streaking: return           # ← Streaking prüfen
count = repeat_count or 1      # ← Mehrfach-Verarbeitung
for _ in range(count):         # ← Loop!
    queue.put(...)

Follow-Handler:

queue.put(...)                 # ← Direkt, kein Loop nötig!

Das ist der Hauptunterschied!


Edge Cases (wenn was Schiefläuft)

Was kann bei Follows schiefgehen?

SzenarioFolgeLösung
event.user ist NoneAttributeErrorget_safe_username() wirft Exception
Username ist leer string ""Wird so gequeuetNormal, kein Problem
"follow" Trigger existiert nichtEvent ignoriertreturn früh
Queue voll (extrem selten)put_nowait() ExceptionTry-except fängt es
TikTok sendet Follow 2x schnellZwei Events hintereinanderBeide werden verarbeitet (Gewollt!)

Fazit: Follow-Handler sind sehr robust – es gibt wenig, was schiefgehen kann.


Zusammenfassung & Nächster Schritt

Was du jetzt weißt:

KonzeptErklärung
Follow = EinfachKeine Combos, kein Streaking, nur 1 Trigger
3-Schritt-AblaufUsername → Prüfen → Queuen
Trigger "follow"Der einzige Trigger für Follow-Events
FehlerbehandlungMinimal nötig, get_safe_username() schützt viel
Best Practice PatternGleich wie bei Gifts, nur viel kürzer

Was passiert NACH dem Follow-Handler?

Die gequeuete "follow"-Action wird später vom Worker-Thread verarbeitet (z.B. Minecraft-Befehl ausführen).

Nächstes Kapitel: Like-Events


[!TIP] Follow-Events sind perfekt zum Experimentieren. Versuch, den Minimal-Handler zu erweitern (z.B. spezielle Behandlung für bestimmte Usernames).

Like-Events

Das Besondere an Likes: Kontinuierliche Zählung

Like-Events sind völlig anders als Gifts und Follows:

MerkmalGiftsFollowsLikes
Event-TypDiskrete Events ("Gift gesendet")Diskrete Events ("Folgt")Kontinuierliche Zählung
HäufigkeitSelten (User sendet Geschenk)Selten (User folgt)SEHR OFT
BenutzernamenJa, sichtbarJa, sichtbarEher selten sichtbar
Trigger-Logik"Wenn Gift""Wenn Follow""Wenn Zähler erreicht z.b. 100er-Marke"
Threading-ProblemNein, einfachNein, einfachJA, Race Conditions!

Das Kernproblem: Like-Events kommen so schnell an, dass mehrere Threads gleichzeitig auf die gleichen Daten zugreifen können. Das führt zu Race Conditions wenn wir nicht aufpassen.


Das Problem der Race Conditions erklärt

Stell dir vor, zwei Like-Events kommen gleichzeitig an:

Thread 1:  Liest Like-Zähler:  100
Thread 2:  Liest Like-Zähler:  100
           ↓
Thread 1:  Berechnet: 100 > last_blocks? JA → Trigger!
Thread 2:  Berechnet: 100 > last_blocks? JA → Trigger!
           ↓
Thread 1:  Schreibt: last_blocks = 100
Thread 2:  Schreibt: last_blocks = 100
           ↓
ERGEBNIS: Trigger ausgelöst 2x statt 1x! 

Lösung: Lock (Mutex)

Ein Lock sorgt dafür, dass nur ein Thread gleichzeitig den kritischen Code ausführt:

Thread 1:  Wartet auf Lock... ⏳
Thread 2:  BEKOMMT LOCK ✓
           Liest, berechnet, schreibt
           GIBT LOCK FREI
           ↓
Thread 1:  BEKOMMT LOCK ✓
           Liest, berechnet, schreibt (mit aktualisierten Daten!)
           GIBT LOCK FREI
           ↓
ERGEBNIS: Trigger ausgelöst 1x (+ 1x, richtig sequenziell) ✓

Like-Zählung visualisiert: Der Unterschied zu anderen Events

GIFTS/FOLLOWS (Diskret):
  
  00:00 - Event "Gift Rose"        → Trigger: "GIFT_ROSE"
  00:05 - Event "Follow"           → Trigger: "FOLLOW"
  00:10 - (nichts)
  00:15 - Event "Gift Diamond"     → Trigger: "GIFT_DIAMOND"

LIKES (Kontinuierlich):

  00:00 - LikeEvent: total=1000
  00:01 - LikeEvent: total=1000
  00:02 - LikeEvent: total=1000
  00:03 - LikeEvent: total=1005  ← +5 Likes!
  00:04 - LikeEvent: total=1012  ← +7 Likes!
  
  Wenn wir jede 10er-Marke triggern wollen:
  
  1000-1009: keine Trigger
  1010+    : 1 Trigger
  1020+    : 1 Trigger
  1030+    : 1 Trigger
  etc.

  Mit unserem Code:
  
  current_blocks = 1012 // 10 = 101
  last_blocks = 100
  diff = 101 - 100 = 1
  → Trigger 1x ausgelöst ✓

LikeEvent Struktur

Ein LikeEvent enthält diese Informationen:

event.total              # Gesamte Like-Anzahl bis jetzt: 1005, 1010, 1025 etc.
event.likeCount          # Likes in dieser Session/Streak: 5, 7, 15 etc.

event.user.nickname      # Benutzername manchmal nicht verfügbar
event.timestamp          # Zeitpunkt des Events

Like-Event Processing: Der 6-Schritt-Ablauf

Wenn Like-Events ankommen, passiert folgendes:

1. ERSTES EVENT?
   Ist start_likes noch None? JA → Initialisieren, return
   
2. DELTA BERECHNEN  
   Likes seit Start: current_total - start_likes
   z.B.: 1025 - 1000 = 25
   
3. LOCK HOLEN
   Warte bis kein anderer Thread aktiv ist
   
4. REGELN DURCHGEHEN
   Für jede Like-Regel:
     - Intervall auslesen ("every": 100)
     - Berechnen, wie viele Intervalle erreicht wurden
     - Prüfen, ob neue Intervalle seit letztem Check
     
5. TRIGGERS QUEUEN
   Für jedes neue Intervall:
     - Aktion in die Queue legen
     
6. LOCK FREIGEBEN
   Nächster Thread kann jetzt arbeiten

Intervall-Berechnung erklärt

Das ist die Kern-Logik für Like-Zählung:

every = 100  # Alle 100 Likes einen Trigger

# Szenario 1: 1010 Likes gesamt
current_blocks = 1010 // 100  # = 10 (zehnte 100er-Marke)
last_blocks = 9               # (wir waren bei 900)
diff = 10 - 9 = 1             # → 1 Trigger

# Szenario 2: 1025 Likes gesamt  
current_blocks = 1025 // 100  # = 10 (immer noch zehnte Marke!)
last_blocks = 10              # (wir wissen schon von Marke 10)
diff = 10 - 10 = 0            # → Kein neuer Trigger

# Szenario 3: 1200 Likes gesamt
current_blocks = 1200 // 100  # = 12 (zwölfte Marke)
last_blocks = 10              # (alte Marke)
diff = 12 - 10 = 2            # → 2 Triggers hintereinander!

Das // ist wichtig! Das ist Integer-Division (ganzzahlig). Sie ist der Schlüssel für die Block-Berechnung.


Fehlerbehandlung bei Like-Events

Like-Handler brauchen besondere Fehlerbehandlung wegen des Locks:

like_lock = threading.Lock()

try:
    with like_lock:  # ← Python: Automatisch lock/unlock
        # Kritischer Code hier
except Exception as e:
    logger.error(f"Fehler im Like-Handler: {e}", exc_info=True)
    # Lock wird AUTOMATISCH freigegeben, auch wenn Error!

Warum with like_lock verwenden?

Weil Python automatisch den Lock immer freigibt, selbst wenn ein Error passiert. Das ist wichtig - sonst würde der Lock "hängenbleiben" und alle anderen Threads warten ewig!


Praktisches Beispiel: Ein vollständiger Like-Handler

Hier ist ein realer, funktionierender Like-Handler:

import threading

# Global initialisieren
like_lock = threading.Lock()
start_likes = None
last_overlay_sent = 0
last_overlay_time = 0

LIKE_TRIGGERS = [
    {"id": "goal_100", "every": 100, "last_blocks": 0, "function": "LIKE_GOAL_100"},
    {"id": "goal_500", "every": 500, "last_blocks": 0, "function": "LIKE_GOAL_500"},
]

def initialize_likes(total):
    """Beim ersten Event: Startwert setzen"""
    global start_likes
    start_likes = total
    logger.info(f"Like-Tracking initialisiert mit: {total} Likes")

@client.on(LikeEvent)
def on_like(event: LikeEvent):
    """
    Verarbeitet Like-Events von TikTok.
    - Kontinuierliche Zählung statt einzelner Events
    - Thread-sicher mit Locks
    - Triggert beim Erreichen von Like-Marken (100, 500, 1000, etc.)
    """
    global start_likes, last_overlay_sent, last_overlay_time
    
    try:
        # SCHRITT 1: Erste Initialisierung?
        if start_likes is None:
            initialize_likes(event.total)
            return
        
        # SCHRITT 2: Berechne Likes seit Start
        total_since_start = event.total - start_likes
        
        logger.debug(f"Like-Event: {event.total} total, "
                    f"{total_since_start} seit Start")
        
        # SCHRITT 3: Lock holen (Thread-sicherheit!)
        with like_lock:
            
            # SCHRITT 4: Jede Like-Regel durchgehen
            for rule in LIKE_TRIGGERS:
                every = rule["every"]
                
                # Invalide Regeln überspringen
                if every <= 0:
                    continue
                
                # SCHRITT 5: Berechne aktuelle und letzte Block-Nummer
                current_blocks = total_since_start // every
                last_blocks = rule["last_blocks"]
                
                # Neue Blocks erreicht?
                if current_blocks > last_blocks:
                    diff = current_blocks - last_blocks
                    rule["last_blocks"] = current_blocks
                    
                    logger.info(
                        f"Like-Trigger '{rule['id']}': "
                        f"{current_blocks} Marken erreicht (+{diff})"
                    )
                    
                    # SCHRITT 6: Für jeden neuen Block: Action queuen
                    for _ in range(diff):
                        try:
                            MAIN_LOOP.call_soon_threadsafe(
                                trigger_queue.put_nowait,
                                (rule["function"], {})
                            )
                        except Exception as e:
                            logger.error(
                                f"Fehler beim Queuen von Like-Action: {e}",
                                exc_info=True
                            )
        
        # (Lock wird hier automatisch freigegeben)
        
    except Exception as e:
        logger.error(
            f"Unerwarteter Fehler im Like-Handler: {e}",
            exc_info=True
        )

Was macht dieser Code?

  1. Initialisieren – Beim ersten Event den Startwert setzen
  2. Delta berechnen – Wie viele Likes sind neu?
  3. Lock holen – Thread-sicherheit aktivieren
  4. Regeln durchgehen – Für jede Like-Marke (100, 500, etc.)
  5. Blocks berechnen – Mit Integer-Division //
  6. Queuen – Auf jede neue Marke eine Aktion

Noch einfacher: Minimales Beispiel

Das absolute Minimum (funktioniert auch, braucht aber manuelle Lock-Verwaltung):

like_lock = threading.Lock()
start_likes = None

@client.on(LikeEvent)
def on_like(event: LikeEvent):
    global start_likes
    
    if start_likes is None:
        start_likes = event.total
        return
    
    delta = event.total - start_likes
    
    with like_lock:
        # Wenn delta 100 erreicht: Trigger
        if delta >= 100 and delta - 100 < 1:  # Erste 100er-Marke
            MAIN_LOOP.call_soon_threadsafe(
                trigger_queue.put_nowait,
                ("LIKE_GOAL_100", {})
            )

Das ist viel kürzer, aber auch weniger flexibel. Der komplette Handler oben ist besser!


Unterschied zwischen Gifts/Follows und Likes

Um zu verstehen, warum Like-Handler komplexer sind:

Gifts/Follows:

# Event kommt → Sofort verarbeiten → Fertig
@client.on(GiftEvent)
def on_gift(event):
    queue.put(...)

Likes:

# Events kommen SEHR OFT → Track wie viele → Trigger pro Intervall
@client.on(LikeEvent)
def on_like(event):
    # Zähle: Wie viele Likes seit Start?
    delta = event.total - start_likes
    
    # Berechne: Wie viele 100er-Marken?
    blocks = delta // 100
    
    # Vergleiche: Neue Marken seit letztmal?
    if blocks > last_blocks:
        # DANN: Triggern!
        queue.put(...)

Der Unterschied: Aggregation statt direkte Weitergabe!


Edge Cases bei Like-Events

Was kann schiefgehen?

SzenarioProblemLösung
Like-Event vor Initialisierungstart_likes ist Noneif start_likes is None: initialize()
Zwei Events gleichzeitigRace Conditionwith like_lock schützt
Intervall ist 0Division by Zeroif every <= 0: continue
Sehr schnelle Like-FlutViele Events/SecBlocks werden korrekt aggregiert
Lock hängtThread blockiert ewigwith like_lock auto-freigibt

Fazit: Like-Handler versprechen die meiste Fehlerbehandlung, besonders wegen des Locks.


Zusammenfassung & Nächster Schritt

Was du jetzt weißt:

KonzeptErklärung
Likes ≠ GiftsKontinuierliche Zählung statt einzelner Events
Race ConditionsMehrere Threads greifen gleichzeitig zu → Lock notwendig
Block-Berechnungblocks = total_likes // intervall
InitialisierungBeim ersten Event: Startwert setzen
Lock-Patternwith threading.Lock() für Thread-Sicherheit
FehlerbehandlungLock wird auch bei Errors freigegeben (with macht das)

Was passiert NACH dem Like-Handler?

Die gequeuteten Like-Aktionen werden später vom Worker-Thread verarbeitet (z.B. "Gratuliere zu 100 Likes!").

Nächstes Kapitel: Threading & Queues


[!NOTE] Like-Handler zeigen dir die echte Komplexität von Multi-Threading. Das ist nicht einfach, aber super wichtig für performante Systeme!

Threading & Queues: Asynchrone Verarbeitung

Warum können wir Events nicht direkt ausführen?

Stell dir vor, ein Event-Handler würde Dinge direkt ausführen:

# FALSCH - direkt ausführen:
@client.on(GiftEvent)
def on_gift(event):
    execute_minecraft_command(...)   # ← Blockt!
    wait_for_response()              # ← Dauert lange!
    update_overlay(...)              # ← Noch länger!
    # In der Zeit: Neue Events stauen sich auf

Das Problem: Während wir auf die Minecraft-Response warten, können keine neuen TikTok-Events verarbeitet werden. Die TikTok-Verbindung "hängt" und wir verlieren Events!

Die Lösung: Events in eine Queue (Warteschlange) legen und asynchron verarbeiten!

# ✓ RICHTIG - in Queue legen:
@client.on(GiftEvent)
def on_gift(event):
    trigger_queue.put_nowait((target, username))  # ← Sehr schnell!
    # Fertig! Event-Handler kehrt sofort zurück
    
# Ein anderer Thread verarbeitet die Queue:
while True:
    target, username = trigger_queue.get()  # ← Warte auf nächste Aktion
    execute_minecraft_command(...)          # ← Kein Problem, wenn es dauert

Die Queue-Architektur visualisiert

TIKTOK-VERBINDUNG
  (sehr schnell, darf nicht blocken)
        ↓
  Event-Handler
  (auch schnell!)
        ↓
  Trigger-Queue
  (Warteschlange)
   [GIFT_ROSE]
   [FOLLOW]
   [LIKE_GOAL_100]
   [GIFT_DIAMOND]
        ↓
  Worker-Thread
  (kann auch langsam sein)
        ↓
  Minecraft-Befehle
  (können lange dauern)

Der Vorteil: Die TikTok-Verbindung wird nie blockiert, egal wie überlastet der Worker-Thread ist!


Queue Operations: put, get, put_nowait

import queue
from threading import Thread

trigger_queue = queue.Queue(maxsize=1000)

# Operation 1: PUT (mit Warten)
trigger_queue.put((target, username))  
# Wenn Queue voll: Warte bis Platz frei wird

# Operation 2: PUT_NOWAIT (ohne Warten)
trigger_queue.put_nowait((target, username))
# Wenn Queue voll: Exception (QueueFull)
# → Das ist gut! Es wir ntuns, wenn was schiefläuft

# Operation 3: GET (mit Warten)
item = trigger_queue.get()
# Wenn Queue leer: Warte bis Item kommt
# → BLOCKT den Worker-Thread, bis etwas zu tun ist

# Operation 4: GET_NOWAIT (ohne Warten)
try:
    item = trigger_queue.get_nowait()
except queue.Empty:
    # Queue war leer, mach was anderes

call_soon_threadsafe: Thread-sichere Aufrufe

In unserem Streaming-Tool verwenden wir call_soon_threadsafe statt normalem put:

# Normaler put() - unsicher wenn MainLoop aktiv:
trigger_queue.put_nowait((target, username))  # Könnte Race Condition sein

# Besser: call_soon_threadsafe
MAIN_LOOP.call_soon_threadsafe(
    trigger_queue.put_nowait,
    (target, username)
)  # ✓ Thread-sicher!

Warum? call_soon_threadsafe sorgt dafür, dass die Operation im MainLoop-Thread ausgeführt wird, nicht im Event-Handler-Thread. Das vermeidet Race Conditions!


Race Conditions und Locks (nochmal wiederholt)

Eine Race Condition tritt auf, wenn zwei Threads gleichzeitig auf die gleiche Daten zugreifen:

# Race Condition:
counter = 0

Thread 1: counter = counter + 1  # Liest 0, schreibt 1
         ↓ (interrupt!)
Thread 2: counter = counter + 1  # Liest 0, schreibt 1
         
RESULT: counter = 1 (sollte aber 2 sein!)

# ✓ Mit Lock:
counter = 0
lock = threading.Lock()

Thread 1: with lock:            # Sperrt Lock
           counter = counter + 1  # Liest 0, schreibt 1
           # Lock freigegeben
         ↓
Thread 2: with lock:            # Wartet auf Lock
           counter = counter + 1  # Liest 1, schreibt 2
           # Lock freigegeben
           
RESULT: counter = 2 ✓

Pattern: Immer with threading.Lock() für kritische Daten verwenden!


Praktisches Beispiel: Worker-Thread Implementation

Der Worker-Thread liest Events aus der Queue und verarbeitet sie:

import threading
import queue

trigger_queue = queue.Queue()

def worker_thread():
    """Dieser Thread verarbeitet Trigger aus der Queue"""
    while True:
        try:
            # Warte auf nächste Aktion
            target, username = trigger_queue.get(timeout=1)
            
            # Verarbeite Aktion
            logger.info(f"Verarbeite: {target} für {username}")
            
            try:
                execute_trigger(target, username)
            except Exception as e:
                logger.error(f"Fehler bei Trigger {target}: {e}")
            
            # Markiere als "done"
            trigger_queue.task_done()
            
        except queue.Empty:
            # Timeout: Nichts in der Queue, weitermachen
            continue
        except Exception as e:
            logger.error(f"Worker-Thread Fehler: {e}")

# Starte Worker-Thread (als Daemon, läuft im Hintergrund)
worker = threading.Thread(target=worker_thread, daemon=True)
worker.start()

Overlay-Updates: Ein praktisches Anwendungsbeispiel

Overlay-Updates für Like-Counter verwenden auch die Queue:

# Separate Queue für Overlay-Updates
like_queue = queue.Queue()

@client.on(LikeEvent)
def on_like(event):
    global start_likes, last_overlay_sent, last_overlay_time
    
    if start_likes is None:
        start_likes = event.total
        return
    
    # Berechne neue Likes
    delta = event.total - start_likes
    
    # Sende Update an Overlay (aber nicht zu oft!)
    now = time.time()
    if delta > 0 and (now - last_overlay_time) >= 0.5:  # Max. 2x pro Sekunde
        try:
            MAIN_LOOP.call_soon_threadsafe(
                like_queue.put_nowait,
                delta  # Nur die Differenz senden
            )
            last_overlay_sent = delta
            last_overlay_time = now
        except queue.Full:
            logger.warning("Like-Queue ist voll, Update übersprungen")

Das ist wichtig: Nicht jedes Overlay-Update senden! Mit OVERLAY_INTERVAL (z.B. 0.5 Sekunden) begrenzen wir die Updates. Das spart Bandbreite!


Timing & Throttling: Events nicht zu schnell kommen lassen

Manchmal kommen Events SO schnell an, dass wir sie drosseln (throttle) müssen:

import time

last_event_time = 0
THROTTLE_INTERVAL = 0.1  # Mindestens 100ms zwischen Events

@client.on(LikeEvent)
def on_like(event):
    global last_event_time
    
    # Ignoriere Events die zu dicht beieinander liegen
    now = time.time()
    if now - last_event_time < THROTTLE_INTERVAL:
        return  # Zu schnell! Skippen.
    
    last_event_time = now
    
    # ... rest des Event-Handlers ...

Warum? Wenn Like-Events alle 50ms ankommen , können wir sie nicht alle verarbeiten. Mit Throttling verlanagsamen wir gezielt die ausführung.


Finale Anmerkung

Das Wichtigste zu verstehen:

Events sind nicht direkt = Aktion.

Stattdessen:

TikTok-Event → Handler → Queue → Worker-Thread → Aktion
               (schnell)   (Puffer)    (kann langsam sein)

Das macht das System:

  • ✓ Stabil (Events gehen nicht verloren)
  • ✓ Skalierbar (viele Events gleichzeitig)
  • ✓ Wartbar (Aktion-Logik ist getrennt)

Debugging & Troubleshooting

Du hast dein Plugin geschrieben, aber es funktioniert nicht wie erwartet? Hier lernst du, Fehler zu finden, zu verstehen und zu beheben.


Arten von Fehlern

Bevor wir debuggen, sollten wir wissen, welche Fehlerklassen es gibt:

FehlertypSymptomBeispiel
Syntax-FehlerProgramm startet gar nichtdef foo( - fehlende Klammer
Import-FehlerModuleNotFoundErrorAbhängigkeit nicht installiert
Runtime-FehlerProgramm crasht während AusführungDivision durch 0
Logic-FehlerProgramm läuft, macht aber Falschesif x = 5: statt if x == 5:
Configuration-FehlerSettings sind falschconfig.yaml hat ungültige YAML

Werkzeug 1: Logs (Das wichtigste!)

Wo sind die Logs?

build/release/logs/
├── debug.log          # Allgemeine Debug-Logs
├── error.log          # Nur Fehler
├── plugin_timer.log   # Plugin-spezifische Logs
└── ...

Log-Levels verstehen

In der config.yaml:

log_level: 2

[!TIP] Für Entwicklung Level auf 4 setzen:

log_level: 4

Log-Output in deinem Plugin

import logging

logger = logging.getLogger(__name__)

logger.debug("Debug-Info für Entwickler")
logger.info("Allgemeine Information")
logger.warning("Warnung – könnte Probleme verursachen")
logger.error("Ein Fehler ist aufgetreten")
logger.critical("KRITISCHER Fehler – Programm might crashed")

Beispiel:

@app.route("/webhook", methods=["POST"])
def webhook():
    logger.info(f"Webhook empfangen: {request.json}")
    
    try:
        process_event(request.json)
        logger.info("Event erfolgreich verarbeitet")
    except Exception as e:
        logger.error(f"Fehler im Event-Processing: {e}", exc_info=True)
        return {"error": str(e)}, 500

Werkzeug 2: Print-Debugging

Für schnelle Tests kannst du auch print() nutzen:

def on_gift(event):
    print(f"[DEBUG] Gift empfangen: {event.gift.name}")
    print(f"[DEBUG] User: {event.user.nickname}")
    print(f"[DEBUG] Count: {event.repeat_count}")

Aber Achtung: print() ist nicht produktionsreif. Nutze logging für echte Anwendungen.


Werkzeug 3: Try-Except Blöcke

Fehler abfangen und verstehen:

try:
    result = 10 / number  # Könnte Division-by-Zero sein
except ZeroDivisionError:
    print("FEHLER: Division durch 0!")
    return None
except Exception as e:
    print(f"Unerwarteter Fehler: {e}")
    return None

Mit traceback() für Details:

import traceback

try:
    process_data(data)
except Exception as e:
    print(f"FEHLER: {e}")
    traceback.print_exc()  # Zeigt den kompletten Error-Stack
    logger.error(f"Fehler: {e}", exc_info=True)

Werkzeug 4: Der Debugger (VS Code)

Visual Studio Code hat einen eingebauten Debugger:

Breakpoints setzen

  1. Öffne deine Python-Datei
  2. Klick links neben die Zeile → roter Punkt (Breakpoint)
  3. Starte das Programm mit F5 (Debug-Modus)
  4. Wenn die Zeile erreicht wird → Programm pausiert
  5. Inspiziere Variablen, steps durch den Code

Debug-Controls

  • F10 – Nächste Zeile (Step Over)
  • F11 – In Funktion gehen (Step Into)
  • Shift+F11 – Aus Funktion gehen
  • F5 – Weiter bis nächster Breakpoint
  • Shift+F5 – Debuggen beenden

Watch-Variablen

Rechts im Debug-Panel:

VARIABLES
├─ request
│   ├─ method: "POST"
│   ├─ json: {...}
│   └─ ...
├─ event
│   ├─ gift: {...}
│   └─ ...

Du kannst hier Variablen inspizieren, ohne zu tippen!


Häufige Fehler & Lösungen

1. "ModuleNotFoundError: No module named 'TikTokLive'"

Ursache: Abhängigkeit nicht installiert.

Lösung:

pip install -r requirements.txt

oder

pip install TikTokLive Flask pywebview pyyaml

2. "Config-Fehler bei Laden"

Ursache: configuration.yaml ist keine gültige YAML.

Test:

python -c "import yaml; yaml.safe_load(open('config/config.yaml'))"

Wenn Fehler → YAML-Syntax überprüfen (Einrückung, Doppelpunkte, etc.)


3. "Port schon in Verwendung"

Fehler: Address already in use :8080

Ursache: Ein anderes Programm nutzt den Port.

Lösung:

Windows:

netstat -ano | findstr :8080
taskkill /PID <pid_nummer> /F

macOS/Linux:

lsof -i :8080
kill -9 <pid>

Oder: Port in config.yaml ändern:

Timer:
  WebServerPort: 8081  # Statt 8080

4. "TikTok-Verbindung schlägt fehl"

Fehler: Client kann sich nicht mit TikTok verbinden.

Diagnostik:

# In main.py testen
client = TikTokLiveClient(unique_id="mein_username")
try:
    asyncio.run(client.connect())
    print("✓ Verbindung erfolgreich!")
except Exception as e:
    print(f"✗ Verbindung fehlgeschlagen: {e}")

Häufige Ursachen:

  • TikTok-User existiert nicht (falsch geschrieben)
  • Internet down
  • TikTok API hat sich geändert

5. "Plugin wird nicht geladen"

Fehler: Plugin ist in src/plugins/ aber wird nicht verwendet.

Debugging:

  1. Check: Plugin in PLUGIN_REGISTRY registriert?

    # In start.py / registry.py
    {"name": "MyPlugin", "path": ..., "enable": True, ...}
    
  2. Check: Plugin hat main.py?

    src/plugins/my_plugin/
    ├── main.py       # Muss existieren!
    ├── README.md
    └── version.txt
    
  3. Check: Plugin kann importieren?

    python src/plugins/my_plugin/main.py --register-only
    

    Wenn Fehler → Importe überprüfen


6. "Webhook wird nicht empfangen"

Fehler: Minecraft sendet Event, aber dein Plugin empfängt es nicht.

Debugging:

@app.route("/webhook", methods=["POST"])
def webhook():
    logger.info(f"Webhook empfangen: {request.json}")
    print(f"[WEBHOOK] {request.json}")  # Zusätzlich printen
    return {"success": True}, 200

Checks:

  1. Flask läuft?

    curl http://localhost:7878/webhook -X POST -d "{}"
    
  2. Firewall erlaubt Port? Port muss offen sein.

  3. Config stimmt? Port in config.yaml muss mit Flask-Port übereinstimmen.


7. "Queue läuft über" oder "Performance-Probleme"

Fehler: Viele Events → System wird langsam.

Debugging:

import asyncio

# In Haupt-Loop
while True:
    size = trigger_queue.qsize()
    if size > 100:
        logger.warning(f"Queue größe: {size} – könnte eng werden!")
    
    event = trigger_queue.get()
    process(event)

Optimierung:

  • Batch-Processing verwenden (mehrere Events auf einmal verarbeiten)
  • Threading nutzen (mehrere Worker pro Queue)
  • Events filtern (nicht alle verarbeiten)

8. "Thread-Safety Fehler" / "Race Condition"

Fehler: Sporadische, nicht-reproduzierbare Fehler (manchmal funktioniert's, manchmal nicht).

Ursache: Zwei Threads ändern gleichzeitig die Daten.

Lösung – Lock verwenden:

from threading import Lock

counter_lock = Lock()
counter = 0

def increment():
    global counter
    with counter_lock:  # Nur ein Thread auf einmal!
        counter += 1
        logger.debug(f"Counter: {counter}")

Performance-Profiling

Falls das Programm langsam ist:

1. Wo ist der Bottleneck?

import time

start = time.time()
result = process_large_data()
elapsed = time.time() - start

logger.info(f"process_large_data() brauchte {elapsed:.2f}s")

2. Profiler nutzen

python -m cProfile -s cumtime main.py

Das zeigt, welche Funktionen am meisten Zeit verbrauchen.


Debugging-Checkliste

Wenn etwas nicht funktioniert:

  • Sind die Logs lesbar?
  • Try-Except Blöcke um kritische Teile?
  • Imports korrekt? (pip install alle Abhängigkeiten?)
  • Config valide? (YAML, Ports, etc.)
  • Breakpoints gesetzt und durch den Code gelaufen?
  • Umgebungs-Variablen korrekt?
  • Andere Prozesse blocken Ressourcen? (Ports, Dateien)
  • Datentyp-Fehler? (String statt Integer, etc.)
  • Off-by-one Fehler? (Index-Fehler)
  • Race Conditions? (Threading-Probleme)

Hilfe holen

1. Beschreibe dein Problem präzise

Schlecht:

"Mein Plugin funktioniert nicht!"

Gut:

"Mein Plugin Timer startet nicht. Fehler: ModuleNotFoundError: No module named 'requests'. Ich habe pip install -r requirements.txt ausgeführt, aber es funktioniert nicht."

2. Code-Snippet teilen

# Mein on_gift Handler
@client.on(GiftEvent)
def on_gift(event):
    trigger_queue.put_nowait((event.gift.name, event.user.nickname))
    # Fehler hier?

3. Logs teilen

[ERROR] Fehler im Event-Handler:
Traceback (most recent call last):
  File "main.py", line 123, in on_gift
    ...
KeyError: 'gift_id'

4. Dein Umgebung beschreiben

  • OS: Windows / macOS / Linux
  • Python: 3.8 / 3.9 / 3.10 / 3.11 / 3.12
  • Streaming Tool Version: v1.0 / dev / etc.

Zusammenfassung

Gutes Debugging folgt diesem Flow:

Fehler bemerken
    ↓
Logs anschauen (Werkzeug 1)
    ↓
Mit Print debuggen (Werkzeug 2)
    ↓
Try-Except nutzen (Werkzeug 3)
    ↓
VS Code Debugger (Werkzeug 4)
    ↓
Fehler gefunden!
    ↓
Fix implementieren

Mit etwas Übung wirst du Fehler schnell finden können.

Anhang

Der Anhang ist ein Nachschlagewerk für Themen, die in den Hauptkapiteln zu tief gehen würden oder zusätzliche Kontexte bieten.

Hier findest du:

  • Projektstruktur – Dateien & Ordner verstehen
  • Konfiguration – Config-Details & Migration
  • Update-Prozess – Wie Updates funktionieren
  • Glossar – Alle Fachbegriffe erklärt (sehr wichtig!)

Was ist im Anhang?

Plugins ohne Python (main.exe-Pflicht)

Wie du Plugins in anderen Sprachen als Python schreibst, was du bei der Registrierung beachten musst und warum main.exe Pflicht ist. Auch wie du von main.exe aus andere Dateien/Skripte aufrufen kannst.

Kapitel öffnen


Glossar START HIER bei Unklarheiten

Das ist dein Nachschlagewerk. Wenn du einen Begriff nicht verstehst:

  • Event – Was ist das?
  • Queue – Wie funktioniert eine Warteschlange?
  • DCS/ICS – Was ist der Unterschied?
  • Threading – Warum ist das wichtig?
  • Und 50+ weitere Begriffe!

Glossar öffnen


Core-Module der Infrastruktur

Für Fortgeschrittene Entwickler: Verstehe die technische Infrastruktur:

  • paths.py – Pfad-Management
  • utils.py – Konfiguration laden
  • models.py – Datenstrukturen (AppConfig)
  • validator.py – Syntax-Validierung
  • cli.py – Command-Line Arguments

Hier erfährst du, wie die Core-Module zusammenarbeiten und wie Plugins sie nutzen.

Core-Module öffnen


Projektstruktur

Verstehe, wie das Projekt organisiert ist:

  • src/ – Quellcode
  • defaults/ – Template-Konfigurationen
  • config/ – Nutzer-Einstellungen
  • data/ – Persistent gespeicherte Daten
  • build/release/ – Fertige Distribution

Wichtig: Unterschied zwischen Entwicklungsstruktur und Release-Struktur.

Projektstruktur öffnen


Konfiguration

Details zur config.yaml:

  • Wie lädt man Konfiguration im Code?
  • Was ist config_version?
  • Wie funktioniert Config-Migration?
  • Wo kommen die Werte hin?

Config-Details öffnen


Update-Prozess

Für Maintainer & Advanced Developers:

  • Wie werden Updates heruntergeladen?
  • Was wird überschrieben, was nicht?
  • Wie aktualisiert sich der Updater selbst?
  • Welche Dateien sind sicher?

Update-Prozess öffnen


Wann nutze ich den Anhang?

SituationAnhang-Kapitel
"Ich verstehe diesen Begriff nicht"Glossar
"Wie funktioniert die Infrastruktur?"Core-Module
"Wo ist das config.yaml File?"Projektstruktur
"Welche Config-Keys gibt es?"Konfiguration
"Wie testen wir Updates?"Update-Prozess
"Ich schreibe einen Maintainer-Guide"→ Alle oben
"Wie schreibe ich ein Plugin ohne Python?"→ Plugins ohne Python

Der Anhang wird ständig erweitert

Falls du noch Themen vermisst, die hier sein sollten:

  • Datenbank-Schema
  • Performance-Optimization-Guide
  • API-Dokumentation
  • Migration-Guides

...dann gib Feedback oder schreib es selbst!


Siehe auch

Glossar

Ein komplettes Nachschlagewerk aller Fachbegriffe und Konzepte. Wenn du auf einen unbekannten Begriff stößt, findest du hier eine schnelle Erklärung.


A

Aktion / Action

Eine Operation, die das System ausführt (z.B. "sende Command an Minecraft"). Actions werden durch Events ausgelöst und sind in der actions.mca konfiguriert.

Beispiel: Das Gift-Event "Rose" triggert die Action "play_sound".

Async / Asynchrone Programmierung

Programmierung, bei der mehrere Aufgaben nicht nacheinander, sondern parallel laufen. Ein Prozess muss nicht warten, bis ein anderer fertig ist.

In diesem Projekt: TikTok-Events ankommen (Async 1) während der Main-Loop Minecraft-Commands verarbeitet (Async 2).

API (Application Programming Interface)

Eine Schnittstelle, über die zwei Programme miteinander kommunizieren.

In diesem Projekt: Wir nutzen die TikTokLive API (um Events zu empfangen) und die RCON API (um Commands an Minecraft zu senden).


C

Combo-Gift

Ein Gift, das mehrfach hintereinander gesendet werden kann. Der Nutzer sieht eine Animation mit den Gesamtzahl der Gifts.

Beispiel: Jemand sendet dieselbe Rose 5-mal → der Nutzer sieht "Rose ×5".

Im Code: event.gift.combo == True und event.repeat_count == 5

CORS (Cross-Origin Resource Sharing)

Ein Sicherheits-Mechanismus in Webserver, der bestimmt, welche externe Websites Anfragen senden dürfen.

In diesem Projekt: Unser Flask-Server nutzt CORS, um Plugin-GUIs Zugriff auf die APIs zu geben.

Command-Line Argument / Flag

Ein Wert, den man beim Starten eines Programms übergibt.

Beispiel:

python main.py --gui-hidden --register-only

Hier sind --gui-hidden und --register-only Flags.


D

DCS (Direct Control System)

Ein Kommunikations-Protokoll, bei dem Daten direkt via HTTP übertragen werden.

Vorteil: Schnell & zuverlässig.
Nachteil: Erfordert offene HTTP-Ports.

Alternative: ICS (Interface Control System).

Dekorator

Ein Python-Feature (@...), das eine Funktion mit zusätzlichem Verhalten "verziert".

Beispiel:

@client.on(GiftEvent)
def handle_gift(event):
    pass

Das @client.on(...) Dekorator registriert diese Funktion als Event-Handler.

Dependency (Abhängigkeit)

Ein externes Paket, das dein Projekt braucht.

In diesem Projekt:

  • TikTokLive - Abhängigkeit
  • Flask - Abhängigkeit
  • All sind in requirements.txt aufgelistet.

E

Event

Ein Ereignis, das im System passiert.

Beispiele:

  • Jemand sendet ein Gift
  • Jemand folgt
  • Ein Spieler stirbt in Minecraft
  • Der Server startet

Alle Events haben Eigenschaften (Daten), z.B. event.user, event.gift.name.

Event-Handler

Eine Funktion, die auf ein Event reagiert.

Beispiel:

@client.on(GiftEvent)
def on_gift(event):
    print(f"Gift empfangen: {event.gift.name}")

on_gift ist der Event-Handler für GiftEvents.


F

Flask

Ein Python-Webframework zum Erstellen von Webservern.

Verwendung in unserem Projekt:

  • Webhooks (Empfang von Minecraft-Events)
  • GUIs (pywebview nutzt Flask für die Backend-API)

Nicht zu verwechseln mit: Django (anderes Framework), FastAPI (neuer Standard).

Function

Siehe Handler/Funktion.


G

Glossar

Dieses Dokument! Ein Nachschlagewerk von Fachbegriffen.


H

Handler

Siehe Event-Handler.

HTTP / HTTPS

Netzwerk-Protokolle zum Übertragen von Daten über das Internet / Netzwerk.

HTTP = unsicher (aber schneller)
HTTPS = verschlüsselt (aber komplexer)

In diesem Projekt: Wir nutzen HTTP lokal (nicht über Internet).


I

ICS (Interface Control System)

Ein Kommunikations-Protokoll, bei dem Daten via GUI/Screen-Capture übertragen werden.

Vorteil: Funktioniert überall (auch mit TikTok Live Studio).
Nachteil: Langsamer & komplexer.

Alternative: DCS (Direct Control System).

Import

Das Laden von externem Code in dein Programm.

Beispiel:

from core import load_config

Das lädt die load_config-Funktion aus dem core-Modul.


J

JSON

Ein Datenformat zum speichern & übertragen von strukturierten Daten.

Format:

{
    "name": "John",
    "age": 30,
    "gifts": ["Rose", "Diamond"]
}

In diesem Projekt: Wird für Konfigurationen, Window-States und Daten verwendet.


L

Logging / Logs

Das Speichern von Programmierungsvorgängen in eine Datei oder als Output.

Zweck: Debuggen & Monitoring.

Beispiel:

logging.info("Gift received")
logging.error("Connection failed")

Logs landen dann in logs/debug.log.


M

main.py

Die Haupt-Programmdatei des Projekts. Sie verbindet TikTok mit dem Rest des Systems.

Middleware

Software, die zwischen zwei anderen Systemen vermittelt.

In unserem Projekt: main.py enthält den Webhook-Endpunkt und ist Middleware zwischen Minecraft und den Plugins. server.py hingegen startet den Minecraft-Server selbst.

Migration

Der Prozess, Daten von alt zu neu zu übertragen.

In diesem Projekt:

  • Config-Migration: Alte config.yaml wird auf neue Struktur aktualisiert
  • Siehe Config für Details.

Module

Wiederverwendbare Code-Bausteine, die andere Dateien/Projekte nutzen können.

In diesem Projekt:

  • core/models.py - Datenstrukturen-Module
  • core/paths.py - Pfad-Module
  • Immer im src/core/ Ordner.

O

Overlay

Eine visuelle Element, die über den Stream / Screen gelegt wird.

Beispiel: Ein Counter zeigt auf dem Screen "Deaths: 5", "Likes: 200".

In diesem Projekt: Plugins nutzen Overlays um Daten visuell darzustellen.


P

Parameter

Ein Wert, der an eine Funktion übergeben wird.

Beispiel:

def create_client(user):  # 'user' ist der Parameter
    ...

create_client("my_streamer")  # 'my_streamer' wird übergeben

Path / Pfad

Ein Dateipfad im Dateisystem.

Windows: C:\Users\...\config\config.yaml
Linux: /home/.../config/config.yaml

Plugin

Ein eigenständiges Program, das sich ins Streaming Tool einfügt.

Beispiele:

  • Timer (Countdown-Timer)
  • DeathCounter (Zählt Tode)
  • Dein Custom-Plugin

Port

Ein virtueller "Hafen", über den ein Server Verbindungen akzeptiert.

Beispiel: http://localhost:8080 - Port ist 8080.

Config: Alle Ports sind in der config.yaml konfigurierbar.

Pseudo-Code

Vereinfachter Code, der nicht syntax-korrekt ist, aber die Logik zeigt.

Beispiel:

1. Wenn Gift kommt
2. Finde Konfiguration
3. Führe Action aus

Q

Queue / Warteschlange

Eine Datenstruktur, die Elemente in der Reihenfolge speichert, in der sie hinzugefügt wurden (FIFO: First-In-First-Out).

In unserem Projekt:

  • trigger_queue: Speichert die zu verarbeitenden Trigger
  • like_queue: Speichert Like-Updates für das Overlay

R

Race Condition

Ein Bug, der auftritt, wenn zwei Threads gleichzeitig auf die gleiche Ressource zugreifen.

Beispiel: Thread 1 liest event.total, Thread 2 ändert es → Inkonsistenz!

Lösung: Lock (Mutex) - nur ein Thread darf gleichzeitig zugreifen.

Registry / Registrierung

Eine Zentrale Verwaltungs-Datei oder -System.

In diesem Projekt: PLUGIN_REGISTRY registriert alle Module & Plugins mit ihren Einstellungen.

RCON (Remote Console)

Ein Protokoll & Server, über den man Minecraft-Commands remote senden kann.

Beispiel: Statt direkt in der Minecraft Console zu tippen, sendet das Tool via RCON den Command /say "Hey!".

Reverse Engineering

Das Nachbauen eines Systems, indem man sein Verhalten von außen beobachtet.

In diesem Projekt: Die TikTokLive API basiert auf Reverse Engineering (nicht offiziell von TikTok).


S

Streak / Steigerung

Ein Combo, das mehrfach hintereinander aktiviert wird.

Beispiel: Dieselbe Rose wird 5-mal hintereinander gesendet → "Streak: 5".

SQL / Datenbank

Ein System zur strukturierten Datenspeicherung (in diesem Projekt nicht zentral relevant).


T

Thread / Threading

Ein eigenständiger Ausführungs-Fluss innerhalb eines Programms.

Analoge: Ein Programm mit 2 Threads is wie ein Streamer mit 2 Mikrophonen – beide können gleichzeitig sprechen.

Gefahr: Racing Conditions (zwei Threads ändern Daten parallel).

Trigger

Eine Bedingung, die eine Action ausführt.

Beispiel:

  • gift_1001 ist ein Trigger (wenn dieses Gift kommt)
  • follow ist ein Trigger (wenn jemand folgt)
  • Wenn Trigger ausgelöst → Action wird ausgeführt

TikTokLive API

Die externe Bibliothek, über die wir TikTok Live-Streams erreichen.

Basiert auf: Reverse Engineering (nicht offiziell).

Nutzen: from TikTokLive import TikTokLiveClient


U

Update

Ein neuer Release / eine neue Version des Projekts.

Prozess:

  1. Neue Version wird auf GitHub hochgeladen
  2. Nutzer startet das Programm
  3. Update-Script laden neue Version
  4. Alte Daten bleiben erhalten (config/, data/ überschrieben nicht)

Details: Siehe Update.


V

Validator

Ein Modul, das überprüft, ob Daten korrekt sind.

Beispiel: validator.py prüft, ob config.yaml valides YAML ist.

Variable

Ein benanntes Behältnis für Daten.

Beispiel: gift_name = event.gift.name - gift_name ist die Variable.


W

Webhook

Ein Mechanismus, bei dem ein System automatisch Daten an ein anderes sendet, wenn etwas passiert.

Im Projekt:

  • Minecraft Server sendet Webhook an unser Tool (wenn Player stirbt, spawnt, etc.)
  • Unser Tool verarbeitet dann den Webhook

Format: Normalerweise HTTP POST mit JSON-Daten.


X

(keine gängigen Begriffe)


Y

YAML

Ein Datenformat zum Speichern von strukturierten Daten (ähnlich JSON, aber lesbar-freundlicher).

Format:

timer:
  enable: true
  start_time: 10
  max_time: 60

In diesem Projekt: config.yaml ist in YAML geschrieben.


Z

Zentralisierung

Das Konzept, alles an einem Ort zu verwalten.

Im Projekt: PLUGIN_REGISTRY ist zentrale Verwaltung aller Plugins.


Schnell-Index nach Kategorie

Event-System

  • Event, Event-Handler, Trigger, Action
  • Webhook, RCON

Technik

  • API, HTTP/HTTPS, Port
  • Thread/Threading, Race Condition
  • Async, Middleware

Python & Code

  • Import, Module, Decorator
  • Parameter, Variable, Handler
  • Function, Logging

Datenformate

  • JSON, YAML, Datenbank

Struktur & Organisation

  • Registry, Migration
  • Queue, Warteschlange
  • Path / Pfad

Control & Kommunikation

  • DCS, ICS, Flask
  • CORS

Debugging & Entwicklung

  • Logging, Validator
  • Pseudo-Code, Reverse Engineering

"Ich verstehe den Begriff XYZ immer noch nicht!"

Das ist OK. Hier sind Optionen:

  1. Re-read Lesse es einfach nochmal
  2. Context: Suche nach dem Begriff in der Dokumentation – der Kontext hilft
  3. Code lesen: Schau dir an, wie der Begriff im echten Code verwendet wird
  4. Frag: Andere Entwickler oder eine KI um Hilfe

Core-Module und Infrastruktur

[!NOTE] Diese Übersicht richtet sich an Fortgeschrittene Entwickler, die verstehen wollen, wie die Infrastruktur des Systems aufgebaut ist. Erweiterte Grundkenntnisse in Python werden vorausgesetzt.


Überblick

Die Core-Module liegen in src/core/ und bilden die Infrastruktur des Systems. Sie sind nicht direkt sichtbar im Stream – aber jedes Plugin nutzt sie im Hintergrund.

ModulAufgabe
paths.pyVerzeichnis-Verwaltung & Pfade
utils.pyKonfiguration laden, Hilfsfunktionen
models.pyDatenstrukturen (AppConfig)
validator.pySyntax-Validierung (actions.mca)
cli.pyCommand-Line Arguments

paths.py – Pfad-Management

Was ist es?

paths.py kümmert sich um wo alles ist im Dateisystem.

Kurze Beispiele

from core.paths import get_root_dir, get_config_file

# Wo ist das Projekt?
root = get_root_dir()  
# → "C:\Users\User\Streaming_Tool" oder ".\build\release"

# Wo ist die config.yaml?
config = get_config_file()  
# → "C:\Users\User\Streaming_Tool\config\config.yaml"

Wichtige Funktionen

  • get_root_dir() – Projekt-Wurzel
  • get_config_file() – Pfad zu config.yaml
  • get_base_dir() – Basis-Verzeichnis (unterscheidet frozen vs. development)

Wofür braucht man das? Damit Plugins nicht hart-codieren müssen C:\...\config.yaml, sondern einfach get_config_file() aufrufen.


utils.py – Konfiguration & Hilfsfunktionen

Was ist es?

utils.py lädt und parst die config.yaml Datei. Das ist die zentrale Konfigurationsdatei des Systems.

Kurze Beispiele

from core.utils import load_config
from core.paths import get_config_file

# Config laden
config = load_config(get_config_file())

# Zugriff auf Werte
plugins = config["plugins"]
log_level = config["settings"]["log_level"]

Was macht load_config()?

  1. Prüft, ob die Datei existiert
  2. Parst YAML
  3. Gibt Dictionary zurück
  4. Bei Fehler: Beendet das Programm mit aussagekräftiger Fehlermeldung

Wofür braucht man das? Jedes Plugin muss die config.yaml lesen. load_config() macht das zuverlässig – mit Error-Handling.


models.py – Datenstrukturen

Was ist es?

models.py definiert die AppConfig – die Datenstruktur, die ein Plugin beschreibt (wie es in der Registry registriert wird).

Die AppConfig Struktur

@dataclass
class AppConfig:
    name: str          # Name des Plugins
    path: Path         # Wo liegt das Plugin?
    enable: bool       # Ist es aktiviert?
    level: int         # Priorität/Log-Level
    ics: bool          # Hat es ein GUI-Fenster?

Kurze Beispiele

from core.models import AppConfig
from pathlib import Path

# Ein Plugin definieren
timer = AppConfig(
    name="Timer",
    path=Path("src/plugins/timer"),
    enable=True,
    level=1,
    ics=True  # Hat GUI
)

# Als Dictionary für config.yaml
plugin_dict = timer.to_dict()
# → {"name": "Timer", "path": "src/plugins/timer", ...}

# Aus Dictionary zurück
timer2 = AppConfig.from_dict(plugin_dict)

Wofür braucht man das? Die PLUGIN_REGISTRY verwaltet alle Plugins als AppConfig-Objekte. Das macht die Verwaltung strukturiert und validiert.


validator.py – Syntax-Validierung

Was ist es?

validator.py prüft die actions.mca Datei auf Fehler. Es findet:

  • Fehlende Doppelpunkte
  • Ungültige Syntax
  • Doppelte Trigger
  • Formatierungsfehler

Kurze Beispiele

from core.validator import validate_text

text = """
5655:!tnt 2 0.1 2 Notch
follow:/give @a minecraft:golden_apple 7
invalid_line_without_colon
"""

diags = validate_text(text)  # Liste von Fehlern

for diag in diags:
    print(f"[{diag.severity}] Zeile {diag.line}: {diag.message}")
    # → [ERROR] Zeile 3: Fehlender Doppelpunkt

Was wird validiert?

✓ Jede Zeile muss TRIGGER:... Format haben
✓ Keine Leerzeichen direkt nach :
✓ Keine Doppel-Trigger
✓ Korrektes Command-Format

Wofür braucht man das? Damit Nutzer schnell Fehler in ihrer actions.mca sehen – mit exakten Zeilennummern und Fehlercodes.


cli.py – Command-Line Arguments

Was ist es?

cli.py parst Command-line Argumente beim Start:

python main.py --gui-hidden --register-only

Verfügbare Argumente

ArgumentEffekt
--gui-hiddenStarte ohne GUI-Fenster (Headless)
--register-onlyRegistriere nur Plugins, beende dann

Kurze Beispiele

from core.cli import parse_args

args = parse_args()

if args.gui_hidden:
    print("Starte im Headless-Modus")

if args.register_only:
    print("Nur Registry aktualisieren, dann exit")
    # ... plugin registry update ...
    sys.exit(0)

Wofür braucht man das? Ermöglicht verschiedene Start-Modi (für Testing, Automation, Wartung).


Zusammenfassung

Die Core-Module sind die Infrastruktur-Schicht:

ModulNutzen
paths.pyRichtige Pfade finden (dev vs. release)
utils.pyConfig zuverlässig laden
models.pyPlugin-Metadaten verwalten
validator.pyFehler finden & berichten
cli.pyVerschiedene Start-Modi

Praktisch für Entwickler:

  • Plugin-Entwickler: Nutzen hauptsächlich paths.py und utils.py
  • System-Entwickler (Core): Nutzen alle Module
  • Das System selbst: Nutzt alle zusammen für Registrierung & Verwaltung

Projektstruktur

Konfiguration: config.yaml im Detail

[!WARNING] Dieser Teil der Dokumentation wird nur wenig gepflegt. Es kann daher vorkommen, dass Inhalte veraltet sind oder teilweise automatisch von KI erstellt wurden und deswegen Fehlerhaft sind.

Zwei-Datei-System

Das Streaming Tool trennt strikt zwischen Template und Benutzerkonfiguration:

  • config.default.yaml (Template) → wird bei Updates überschrieben
  • config.yaml (Benutzer) → persistent, niemals überschrieben

Warum? Updates können neue Config-Keys einführen, ohne User-Daten zu löschen.

Das System

Update startet
    ↓
Prüft config_version
    ↓
       default > user?
    ↙              ↘
  Ja              Nein
  ↓                 ↓
Migration       Keine Änderung
(Merge Keys)       (User-Values bleiben)

Migration: Schritt-für-Schritt

1. Version prüfen

# config.default.yaml
config_version: 2

# config.yaml (Benutzer)
config_version: 1  ← älter!

2. System erkennt: Migration nötig

3. Merge durchführen:

  • Neue Keys aus default → in user übernehmen
  • User-Werte erhalten (nicht überschreiben!)
  • Alte Keys aus user → löschen
  • Kommentare erhalten (wenn vor Keys)

4. config.yaml neu geschrieben mit version: 2

Code: Config laden mit Fallbacks

import yaml
import sys

CONFIG_FILE = "config/config.yaml"
CONFIG_DEFAULT = "config/config.default.yaml"

def load_config():
    """Lade Config mit Error-Handling."""
    try:
        with open(CONFIG_FILE, "r", encoding="utf-8") as f:
            cfg = yaml.safe_load(f)
        if cfg is None:
            cfg = {}
        return cfg
    except FileNotFoundError:
        print("config.yaml nicht gefunden! Nutze Defaults.")
        return load_default_config()
    except Exception as e:
        print(f"Config-Fehler: {e}")
        return {}

def load_default_config():
    """Fallback auf config.default.yaml."""
    try:
        with open(CONFIG_DEFAULT) as f:
            return yaml.safe_load(f) or {}
    except:
        return {}

# Werte mit Defaults auslesen
cfg = load_config()
port = cfg.get("WebServer", {}).get("Port", 5000)
enabled = cfg.get("MyPlugin", {}).get("Enable", True)

Config-Wert-Zugriff (Best Practices)

# RICHTIG: Mit .get() + Defaults
log_level = cfg.get("Log", {}).get("Level", "INFO")

# FALSCH: Direkter Zugriff
log_level = cfg["Log"]["Level"]  # KeyError risk!

# Deep Get
db_host = cfg.get("Database", {}).get("Host", "localhost")
db_port = cfg.get("Database", {}).get("Port", 5432)

# Ganze Section mit Default
timer_cfg = cfg.get("Timer", {})

Checkliste für Config-Änderungen

  • ☑ Neue Keys → in config.default.yaml hinzufügen
  • config_version erhöht?
  • ☑ Kommentare VOR erstem Key bleiben erhalten
  • ☑ Code nutzt .get() mit Defaults
  • ☑ Test: Migration funktioniert?

Zurück zu Anhang

Ob eine Migration der Konfiguration notwendig ist, wird über die config_version gesteuert. Diese befindet sich am Anfang der Dateien:

config_version: 1
  • Datentyp: Nur Ganzzahlen (Integers).
  • Logik: Eine Migration wird nur ausgelöst, wenn die config_version in der config.default.yaml größer ist als die in der aktuellen config.yaml.

Ablauf der Migration

Der Migrationsprozess verläuft rekursiv durch alle Ebenen der Konfigurationsdatei. Dabei gelten folgende Regeln:

  • Erhalt von Nutzerwerten: Werte, die der Nutzer in seiner config.yaml angepasst hat, werden nicht überschrieben.
  • Bereinigung: Keys, die in der neuen config.default.yaml nicht mehr existieren, werden aus der config.yaml gelöscht.
  • Vollständigkeit: Neue Keys aus der Vorlage werden in die Nutzer-Config übernommen.

Alles was vor dem ersten Key in config.default.yaml steht wird nicht in die config.yaml Kopiert. Das heißt in diesem Fall alle Kommentare über config_version werden nicht Kopiert. Beachte dies beim Modifizieren.

# -------------------------------------------------------------------------
# STREAMING TOOL CONFIGURATION TEMPLATE
# -------------------------------------------------------------------------
# This file is a template.
# Personal settings should be changed in 'config.yaml' only.
# -------------------------------------------------------------------------
config_version: 1

Migration deaktivieren

In der config.yaml kann die automatische Aktualisierung der Konfiguration deaktiviert werden:

auto_update_config: true

Wird dieser Wert auf false gesetzt, unterbleibt der Abgleich mit der Default-Datei.

[!WARNING] Das Deaktivieren dieser Option erfordert eine vollständig manuelle Pflege der Konfiguration. Es gibt keinen CLI-Befehl, um die Migration nachträglich anzustoßen. Eine veraltete Struktur kann zu Fehlern oder Programmabstürzen führen.

Werte aus der Config auslesen

Um die Konfigurationswerte im Code zu nutzen, wird die config.yaml geladen und in ein Dictionary (hier cfg) überführt. Der Zugriff erfolgt anschließend über die entsprechenden Keys.

Laden der Konfiguration

Der folgende Block zeigt das standardmäßige Einlesen der Datei. Hierbei wird sichergestellt, dass das Programm bei einem Lesefehler kontrolliert abbricht:

import yaml
import sys

try:
    with open(CONFIG_FILE, "r", encoding="utf-8") as f:
        cfg = yaml.safe_load(f)
except Exception as e:
    print(f"Fehler beim Laden der Config: {e}")
    input("Drücke Enter zum Beenden...")
    sys.exit(1)

Verwendung im Code

Sobald die Variable cfg befüllt ist, kann auf die Werte zugegriffen werden. Da die Migration auch verschachtelte Strukturen unterstützt, erfolgt der Zugriff bei tieferen Ebenen über mehrere Keys:

# Zugriff auf einen Top-Level-Key
auto_update = cfg.get("auto_update_config", True)

# Zugriff auf verschachtelte Werte (Beispiel)
# Angenommen, die Config hat eine Struktur wie:
# database:
#   host: "localhost"
db_host = cfg.get("database", {}).get("host", "127.0.0.1")

# Verwendung der config_version für Logik-Prüfungen
if cfg.get("config_version", 0) < 2:
    # Spezifische Logik für ältere Config-Stände
    pass

[!TIP] Arbeiten mit Dictionaries

Da die Konfiguration nach dem Laden als Standard-Python-Dictionary vorliegt, solltest du dich mit den fortgeschrittenen Methoden zur Datenmanipulation vertraut machen. Das spart Code-Zeilen und verhindert Laufzeitfehler.

Besonders relevant sind:

  • Sicherer Zugriff (.get()): Vermeide KeyError-Abstürze, indem du Standardwerte (Defaults) direkt beim Auslesen definierst.
  • Verschachtelte Strukturen: Lerne, wie man effizient auf tiefer liegende Ebenen zugreift (z. B. über cfg['database']['host'] oder sicherere Ketten).
  • Type Hinting: Schau dir an, wie du Typ-Hinweise nutzt, damit deine IDE dich beim Programmieren unterstützt und du genau weißt, ob ein Wert ein int, bool oder str sein muss.
  • Exceptions: Verstehe, wie man spezifische Fehler beim Parsen von YAML-Dateien abfängt, um dem Endnutzer hilfreiche Fehlermeldungen statt kryptischer Tracebacks zu zeigen.

Update-Prozess

Plugin erstellen ohne Python

[!WARNING] Keine Garantie für die Codebeispiele.
Die Code-Beispiele in diesem Kapitel (Rust, C++) wurden von einer KI generiert. Ich habe selbst wenig Kenntnisse in diesen Sprachen und kann nicht garantieren, dass sie korrekt, vollständig oder fehlerfrei sind. Nutze sie als Ausgangspunkt und prüfe sie sorgfältig, bevor du sie produktiv einsetzt.

Überblick

Du kannst ein Plugin in jeder Sprache schreiben, die eine native Windows-.exe erzeugt. Das System startet ausschließlich die Datei main.exe in deinem Plugin-Ordner – wie diese erzeugt wird (MSVC-Compiler, Rust/Cargo, MinGW, etc.) ist irrelevant.

[!IMPORTANT] main.exe ist die einzige Pflichtdatei des Plugin-Systems.
Python-Plugins funktionieren übrigens genauso: main.py wird über PyInstaller zu main.exe kompiliert. Aus Sicht des Systems gibt es keinen Unterschied.

Was du selbst implementieren musst (Python erledigt das per core-Modul automatisch):

  • Registrierungsprotokoll (--register-only)
  • Argument-Parsing (--gui-hidden)
  • HTTP-Server für /webhook
  • Konfiguration lesen (YAML / JSON)
  • Datenspeicherung

Ordnerstruktur

src/plugins/
└── myplugin/
    ├── main.exe        ← vom System gestartet (kompiliert aus deinem Code)
    ├── README.md
    └── version.txt

Beim Build wird der gesamte src/plugins/myplugin/-Ordner nach build/release/plugins/myplugin/ kopiert.


Wie der Registry-Scanner funktioniert

Bevor das Hauptprogramm Plugins startet, läuft registry.exe. Sie sucht alle main.exe-Dateien im plugins/-Ordner und führt jede mit --register-only aus:

registry.exe
  ├── findet: plugins/myplugin/main.exe
  ├── ruft auf: main.exe --register-only   (cwd = plugins/myplugin/)
  ├── liest stdout, parst erstes gültiges JSON-Objekt
  └── speichert Metadaten in PLUGIN_REGISTRY.json

Danach liest start.py die PLUGIN_REGISTRY.json und startet jede aktivierte main.exe (diesmal ohne --register-only).

[!NOTE] Der Scanner cached Registrierungsergebnisse anhand von Dateigröße und Änderungszeit. Wenn du die main.exe neu kompilierst, wird sie beim nächsten Start automatisch neu gescannt.


Das Registrierungsprotokoll

Pflichtformat

Wenn dein Plugin mit --register-only gestartet wird, muss es auf stdout eine Zeile im folgenden Format ausgeben und dann mit Exit-Code 0 beenden:

REGISTER_PLUGIN: {"name":"MeinPlugin","path":"C:\\absoluter\\pfad\\zu\\main.exe","enable":true,"level":4,"ics":false}

Das Präfix REGISTER_PLUGIN: ist empfohlen (genau wie es Python ausgibt), aber der Scanner akzeptiert auch eine Zeile, die direkt als JSON-Objekt parsebar ist.

Pflichtfelder

FeldTypBeschreibung
namestringEindeutiger Name des Plugins
pathstringAbsoluter Pfad zur main.exe
enableboolOb das Plugin beim Start gestartet wird
levelintSichtbarkeitslevel (siehe unten)
icsboolHat das Plugin ein GUI-Fenster?

Sichtbarkeitslevel (level)

Steuert, ob das Konsolenfenster des Plugins angezeigt wird, abhängig vom log_level in der config.yaml:

LevelBedeutung
0Verboten – überschreibt alle Sichtbarkeitsregeln, nie verwenden!
1Für sehr sehr wichtige Ausgaben
2Hauptprogramme
3Hintergrunddienste
4Debug/Entwicklung ← für eigene Plugins empfohlen
5Verboten – überschreibt alle Sichtbarkeitsregeln, nie verwenden!

[!NOTE] Level 0 und 5 dürfen nicht verwendet werden. Wenn in der config.yaml log_level = 0 oder log_level = 5 gesetzt ist, überschreiben diese Werte sämtliche Sichtbarkeitsregeln für alle Programme und Plugins. Kein Plugin oder Programm darf Level 0 oder 5 als Wert setzen.

ICS vs. DCS

  • ics: false → Plugin ohne GUI (nur HTTP-Server im Hintergrund). Das ist der Standardfall.
  • ics: true → Plugin öffnet ein Fenster. Wenn control_method in der config.yaml auf DCS steht, wird dein Plugin mit --gui-hidden gestartet (kein Fenster öffnen).

Der path-Wert

Der path-Wert muss der absolute Pfad zur main.exe sein. Da der Scanner dein Plugin mit dem vollen absoluten Pfad aufruft, kannst du argv[0] nutzen:

  • Rust: std::env::current_exe() – die zuverlässigste Methode
  • C++: Win32-API GetModuleFileNameA(NULL, buf, MAX_PATH) oder std::filesystem::absolute(argv[0])

Argument-Handling

Dein Plugin muss mindestens zwei Argumente kennen:

ArgumentVerhalten
--register-onlyJSON ausgeben, sofort beenden
--gui-hiddenFenster nicht öffnen (nur relevant wenn ics: true)

Den Webhook-Server implementieren

Das Minecraft-Plugin sendet bei Ereignissen HTTP-POST-Requests an alle konfigurierten URLs. Dein Plugin kann einen HTTP-Server starten und den Endpunkt /webhook bereitstellen.

Event-Payload

{
    "load_type": "INGAME_GAMEPLAY",
    "event": "player_death",
    "message": "Player died from fall damage"
}

load_type kann u.a. INGAME_GAMEPLAY oder STARTUP sein. event entspricht dem Ereignisnamen aus der configServerAPI.yml.

Häufige Events

EventStandardmäßig aktiv
player_death
player_respawn
player_join
player_quit
block_break
entity_death

Die vollständige Liste findest du in configServerAPI.yml.

Port konfigurieren

Lege in der config.yaml einen Port für dein Plugin an:

MeinPlugin:
  Enable: true
  WebServerPort: 8888

Füge die Webhook-URL dann in configServerAPI.yml (Minecraft-Plugin-Config) ein:

webhooks:
  urls:
    - "http://localhost:7777/webhook"
    - "http://localhost:7878/webhook"
    - "http://localhost:7979/webhook"
    - "http://localhost:8080/webhook"
    - "http://localhost:8888/webhook"   # dein Plugin

[!IMPORTANT] Jede Portnummer im System muss eindeutig sein. Nutze niemals einen bereits belegten Port.


Pfade zur Laufzeit

Wenn main.exe läuft, lassen sich alle wichtigen Verzeichnisse aus dem eigenen Pfad ableiten:

build/release/
├── config/
│   └── config.yaml       ← Konfiguration
├── data/                 ← persistente Daten
├── logs/                 ← Log-Dateien
└── plugins/
    └── myplugin/
        └── main.exe      ← dein Plugin
VariableBerechnungBeispiel
BASE_DIRVerzeichnis von main.exe…/plugins/myplugin/
ROOT_DIRBASE_DIR/../..…/build/release/
CONFIG_FILEROOT_DIR/config/config.yaml
DATA_DIRROOT_DIR/data/
LOGS_DIRROOT_DIR/logs/

Rust:

#![allow(unused)]
fn main() {
let exe_path = std::env::current_exe().unwrap();
let base_dir = exe_path.parent().unwrap();        // plugins/myplugin/
let root_dir = base_dir.parent().unwrap()
                        .parent().unwrap();        // build/release/
let config_file = root_dir.join("config").join("config.yaml");
let data_dir    = root_dir.join("data");
let logs_dir    = root_dir.join("logs");
}

C++:

#include <filesystem>
namespace fs = std::filesystem;

char buf[MAX_PATH];
GetModuleFileNameA(NULL, buf, MAX_PATH);
fs::path base_dir   = fs::path(buf).parent_path();       // plugins/myplugin/
fs::path root_dir   = base_dir.parent_path().parent_path(); // build/release/
fs::path config_file = root_dir / "config" / "config.yaml";
fs::path data_dir    = root_dir / "data";
fs::path logs_dir    = root_dir / "logs";

Konfiguration lesen

Die config.yaml ist eine YAML-Datei. Lies sie beim Start deines Plugins:

Rust (mit serde_yaml):

#![allow(unused)]
fn main() {
let content = std::fs::read_to_string(&config_file).unwrap_or_default();
let cfg: serde_yaml::Value = serde_yaml::from_str(&content).unwrap_or(serde_yaml::Value::Null);
let port = cfg["MeinPlugin"]["WebServerPort"].as_u64().unwrap_or(8888) as u16;
let enabled = cfg["MeinPlugin"]["Enable"].as_bool().unwrap_or(true);
}

C++ (mit yaml-cpp):

YAML::Node cfg = YAML::LoadFile(config_file.string());
int port    = cfg["MeinPlugin"]["WebServerPort"].as<int>(8888);
bool enabled = cfg["MeinPlugin"]["Enable"].as<bool>(true);

Wenn die Datei fehlt oder ein Schlüssel nicht vorhanden ist, verwende immer einen Default-Wert – das Plugin soll nie wegen einer fehlenden Config-Zeile abstürzen.


Datenspeicherung

Persistente Daten (Zähler, Zustände, Fenstergröße) speicherst du als JSON-Datei im DATA_DIR:

build/release/data/myplugin_state.json

Schreibe atomar (erst in .tmp, dann umbenennen), um Datenverlust bei unerwartetem Beenden zu vermeiden.


Kommunikation mit anderen Plugins

Plugins kommunizieren per HTTP auf localhost. Die Ports stehen in config.yaml:

WinCounter:
  WebServerPort: 8080

Rust (mit ureq):

#![allow(unused)]
fn main() {
// Fire-and-forget (kein Warten auf Antwort)
std::thread::spawn(|| {
    let _ = ureq::post("http://localhost:8080/add?amount=1").call();
});
}

C++ (mit cpp-httplib):

httplib::Client cli("localhost", 8080);
cli.set_connection_timeout(2);
auto res = cli.Post("/add?amount=1");
if (!res || res->status != 200) {
    // Plugin nicht erreichbar – Fehler loggen, nicht abstürzen
}

[!NOTE] Das andere Plugin kann offline oder noch nicht gestartet sein. Immer Timeout setzen und Fehler abfangen.


Vollständiges Beispiel – Rust

Demonstriert alle Pflichtbestandteile: Registrierung, Webhook-Server, Config-Lesen, Datenspeicherung.

Abhängigkeiten (Cargo.toml):

[dependencies]
tiny_http   = "0.12"
serde       = { version = "1", features = ["derive"] }
serde_json  = "1"
serde_yaml  = "0.9"

src/main.rs:

use std::env;
use std::fs;
use std::io::Read;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use tiny_http::{Response, Server};

// ---------------------------------------------------------------------------
// Pfade
// ---------------------------------------------------------------------------
fn exe_path() -> PathBuf {
    env::current_exe().expect("Kann Exe-Pfad nicht bestimmen")
}

fn base_dir() -> PathBuf {
    exe_path().parent().unwrap().to_path_buf()
}

fn root_dir() -> PathBuf {
    base_dir().parent().unwrap().parent().unwrap().to_path_buf()
}

// ---------------------------------------------------------------------------
// State
// ---------------------------------------------------------------------------
#[derive(serde::Serialize, serde::Deserialize, Default)]
struct State {
    count: u64,
}

fn load_state(path: &PathBuf) -> State {
    if path.exists() {
        let s = fs::read_to_string(path).unwrap_or_default();
        serde_json::from_str(&s).unwrap_or_default()
    } else {
        State::default()
    }
}

fn save_state(path: &PathBuf, state: &State) {
    let tmp = path.with_extension("tmp");
    fs::write(&tmp, serde_json::to_string_pretty(state).unwrap()).ok();
    fs::rename(&tmp, path).ok();
}

// ---------------------------------------------------------------------------
// Hauptprogramm
// ---------------------------------------------------------------------------
fn main() {
    let args: Vec<String> = env::args().collect();

    // --- Registrierung ---
    if args.iter().any(|a| a == "--register-only") {
        // Absoluten Pfad zur eigenen main.exe bestimmen
        let exe = exe_path().to_string_lossy().replace('\\', "\\\\");

        // Config lesen um enable-Flag dynamisch zu setzen
        let root = root_dir();
        let config_file = root.join("config").join("config.yaml");
        let content = fs::read_to_string(&config_file).unwrap_or_default();
        let cfg: serde_yaml::Value =
            serde_yaml::from_str(&content).unwrap_or(serde_yaml::Value::Null);
        let enabled = cfg["MeinPlugin"]["Enable"].as_bool().unwrap_or(true);

        println!(
            r#"REGISTER_PLUGIN: {{"name":"MeinPlugin","path":"{exe}","enable":{enabled},"level":4,"ics":false}}"#
        );
        std::process::exit(0);
    }

    let gui_hidden = args.iter().any(|a| a == "--gui-hidden");
    // gui_hidden wird hier nicht benötigt, da ics=false (kein Fenster)
    let _ = gui_hidden;

    // --- Konfiguration laden ---
    let root = root_dir();
    let config_file = root.join("config").join("config.yaml");
    let content = fs::read_to_string(&config_file).unwrap_or_default();
    let cfg: serde_yaml::Value =
        serde_yaml::from_str(&content).unwrap_or(serde_yaml::Value::Null);

    let port: u16 = cfg["MeinPlugin"]["WebServerPort"]
        .as_u64()
        .unwrap_or(8888) as u16;

    // --- State laden ---
    let data_dir = root.join("data");
    fs::create_dir_all(&data_dir).ok();
    let state_file = data_dir.join("meinplugin_state.json");
    let state = Arc::new(Mutex::new(load_state(&state_file)));

    // --- HTTP-Server starten ---
    let server = Server::http(format!("127.0.0.1:{port}"))
        .expect("HTTP-Server konnte nicht gestartet werden");

    println!("[MeinPlugin] läuft auf Port {port}");

    for mut request in server.incoming_requests() {
        let url = request.url().to_string();
        let method = request.method().as_str().to_string();

        if url == "/webhook" && method == "POST" {
            let mut body = String::new();
            request.as_reader().read_to_string(&mut body).ok();

            if let Ok(json) = serde_json::from_str::<serde_json::Value>(&body) {
                let event = json["event"].as_str().unwrap_or("");

                if event == "player_death" {
                    let mut s = state.lock().unwrap();
                    s.count += 1;
                    println!("[MeinPlugin] Tode: {}", s.count);
                    save_state(&state_file, &s);
                }
            }

            let response = Response::from_string(r#"{"status":"ok"}"#)
                .with_header("Content-Type: application/json".parse().unwrap());
            request.respond(response).ok();

        } else if url == "/" && method == "GET" {
            let s = state.lock().unwrap();
            let body = format!(r#"{{"count":{}}}"#, s.count);
            let response = Response::from_string(body)
                .with_header("Content-Type: application/json".parse().unwrap());
            request.respond(response).ok();

        } else {
            request.respond(Response::from_string("Not Found").with_status_code(404)).ok();
        }
    }
}

Vollständiges Beispiel – C++

Verwendet cpp-httplib (single-header) und nlohmann/json (single-header).

// Kompilierung (MSVC):
//   cl /std:c++17 /EHsc main.cpp /Fe:main.exe
// Kompilierung (MinGW):
//   g++ -std=c++17 -O2 main.cpp -o main.exe -lws2_32

#define CPPHTTPLIB_OPENSSL_SUPPORT 0
#include "httplib.h"       // https://github.com/yhirose/cpp-httplib
#include "json.hpp"        // https://github.com/nlohmann/json

#include <windows.h>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <mutex>
#include <string>

namespace fs = std::filesystem;
using json   = nlohmann::json;

// ---------------------------------------------------------------------------
// Pfade
// ---------------------------------------------------------------------------
fs::path get_exe_path() {
    char buf[MAX_PATH];
    GetModuleFileNameA(NULL, buf, MAX_PATH);
    return fs::path(buf);
}

fs::path base_dir() { return get_exe_path().parent_path(); }
fs::path root_dir() { return base_dir().parent_path().parent_path(); }

// ---------------------------------------------------------------------------
// State
// ---------------------------------------------------------------------------
struct State { uint64_t count = 0; };
std::mutex state_mutex;
State g_state;

void save_state(const fs::path& path) {
    fs::path tmp = path;
    tmp.replace_extension(".tmp");
    std::ofstream f(tmp);
    f << json{{"count", g_state.count}}.dump(2);
    f.close();
    fs::rename(tmp, path);
}

void load_state(const fs::path& path) {
    if (!fs::exists(path)) return;
    std::ifstream f(path);
    try {
        json j; f >> j;
        g_state.count = j.value("count", 0ULL);
    } catch (...) {}
}

// ---------------------------------------------------------------------------
// Config lesen (vereinfacht – keine yaml-cpp-Abhängigkeit benötigt)
// Für YAML empfiehlt sich yaml-cpp: https://github.com/jbeder/yaml-cpp
// Hier wird der Port aus der config.yaml per einfachem Scan extrahiert.
// ---------------------------------------------------------------------------
uint16_t read_port(const fs::path& config_file, uint16_t default_port) {
    if (!fs::exists(config_file)) return default_port;
    std::ifstream f(config_file);
    std::string line, section;
    bool in_section = false;
    while (std::getline(f, line)) {
        if (!line.empty() && line[0] != ' ' && line[0] != '#') {
            in_section = (line.find("MeinPlugin:") != std::string::npos);
        }
        if (in_section) {
            auto pos = line.find("WebServerPort:");
            if (pos != std::string::npos) {
                try { return static_cast<uint16_t>(std::stoi(line.substr(pos + 14))); }
                catch (...) {}
            }
        }
    }
    return default_port;
}

// ---------------------------------------------------------------------------
// Hauptprogramm
// ---------------------------------------------------------------------------
int main(int argc, char* argv[]) {
    // --- Registrierung ---
    for (int i = 1; i < argc; ++i) {
        if (std::string(argv[i]) == "--register-only") {
            std::string exe = get_exe_path().string();
            // Backslashes für JSON escapen
            std::string escaped;
            for (char c : exe) {
                if (c == '\\') escaped += "\\\\";
                else escaped += c;
            }

            // enable-Flag aus Config lesen (vereinfacht: immer true)
            std::cout << "REGISTER_PLUGIN: "
                      << "{\"name\":\"MeinPlugin\","
                      << "\"path\":\"" << escaped << "\","
                      << "\"enable\":true,"
                      << "\"level\":4,"
                      << "\"ics\":false}"
                      << std::endl;
            return 0;
        }
    }

    bool gui_hidden = false;
    for (int i = 1; i < argc; ++i)
        if (std::string(argv[i]) == "--gui-hidden") gui_hidden = true;
    (void)gui_hidden; // ics=false, daher nicht relevant

    // --- Pfade & Konfiguration ---
    fs::path root       = root_dir();
    fs::path config_file = root / "config" / "config.yaml";
    fs::path data_dir   = root / "data";
    fs::create_directories(data_dir);
    fs::path state_file = data_dir / "meinplugin_state.json";

    uint16_t port = read_port(config_file, 8888);

    // --- State laden ---
    load_state(state_file);

    // --- HTTP-Server ---
    httplib::Server svr;
    svr.set_error_handler([](const auto&, auto& res) {
        res.set_content(R"({"status":"error"})", "application/json");
        res.status = 500;
    });

    // GET / – Status abfragen
    svr.Get("/", [&](const httplib::Request&, httplib::Response& res) {
        std::lock_guard<std::mutex> lock(state_mutex);
        res.set_content("{\"count\":" + std::to_string(g_state.count) + "}", "application/json");
    });

    // POST /webhook – Events empfangen
    svr.Post("/webhook", [&](const httplib::Request& req, httplib::Response& res) {
        try {
            auto j = json::parse(req.body);
            std::string event = j.value("event", "");

            if (event == "player_death") {
                std::lock_guard<std::mutex> lock(state_mutex);
                g_state.count++;
                std::cout << "[MeinPlugin] Tode: " << g_state.count << "\n";
                save_state(state_file);
            }
        } catch (const std::exception& e) {
            std::cerr << "[MeinPlugin] Webhook-Fehler: " << e.what() << "\n";
        }
        res.set_content(R"({"status":"ok"})", "application/json");
    });

    std::cout << "[MeinPlugin] läuft auf Port " << port << "\n";
    svr.listen("127.0.0.1", port);   // blockierend
    return 0;
}

Fehlerbehandlung & Best Practices

[!WARNING] Das System startet ein abgestürztes Plugin nicht automatisch neu.

SituationWas tun
Config-Datei fehltDefault-Wert nutzen, nicht abstürzen
Port bereits belegtFehlermeldung ausgeben, cleanly exit
HTTP-Request kehrt nicht zurückImmer Timeout setzen
Unbehandelter AbsturzTop-level Exception-Handler mit Log-Ausgabe
JSON-Parsefehler im Webhooktry/catch, 200 OK trotzdem zurückgeben

Logging

Schreibe Logs nach ROOT_DIR/logs/meinplugin.log. Nutze atomares Schreiben (anhängen) und logge mindestens:

  • Plugin-Start mit Port
  • Jeden empfangenen Event-Typ
  • Jeden Fehler mit Zeitstempel