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?
| Profil | Empfohlener Einstieg |
|---|---|
| Python-Grundlagen vorhanden | Start mit Grundkonzepte, dann Setup |
| Erweiterte-Python-Kenntnisse vorhanden | System-Überblick → Python in diesem Projekt |
| System erweitern & anpassen mit Python | Direkt zu Plugin-Entwicklung oder Eigenen $ Befehle |
| Debuggen / Troubleshooting | Debugging & 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)
- Grundkonzepte
- Setup
- System-Überblick
- Event-Verarbeitung
- Minecraft-Integration
- System-Architektur
- Plugin-Entwicklung
Option 2: Schnelleinstieg für Erfahrene
- Grundkonzepte (10 Minuten)
- Plugin-Entwicklung
- 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
- Finde dein Level: Anfänger? Dann start mit Grundkonzepte & Begriffe.
- Lies progressiv: Kapitel bauen aufeinander auf.
- Überspringe nichts leicht: Wenn etwas unklar ist, geh zurück zu vorherigen Kapiteln.
- Nutze das Glossar: Unbekannte Begriffe? Glossar.
- 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:
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):
| Phase | Was passiert | Ergebnis |
|---|---|---|
| 1. Empfangen | TikTok-Events werden empfangen | Strukturierte Event-Daten |
| 2. Verarbeiten | Events werden klassifiziert | Klare Kategorien (Gift/Follow/Like/...) |
| 3. Ausführen | Befehl wird an Minecraft gesendet | Minecraft-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:
- Nächstes Kapitel: Lokale Entwicklung einrichten → Setup auf deinem Rechner
- Dann: Wie das System zusammenarbeitet → Architekturfür mittel Detail
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.jarwird benötigt (Minecraft-Server-JAR-Datei).
[!IMPORTANT] Stelle sicher, dass sich sowohl der Ordner
tools/Java/als auch die Dateitools/server.jarim 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
- Besuche https://www.python.org/downloads/
- Lade die aktuelle Python 3.X herunter (Windows x86-64)
- Wichtig: Beim Installer aktiviere die Option "Add Python to PATH"
- 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
- Besuche https://git-scm.com/download/win oder https://desktop.github.com/download/
- Lade den Installer herunter
- 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 Dateitools/server.jarvorhanden 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:
-
Besuche das Repository: https://github.com/TechnikLey/Streaming_Tool
-
Klicke auf den grünen Button "Code" (oben rechts)
-
Wähle "Download ZIP"
-
Entpacke die ZIP-Datei an einem geeigneten Ort (z.B.
C:\Users\dein_name\Streaming_Tool) -
Ö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 Dateitools/server.jarbenö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:
tiktok_user: Dein TikTok-Kanal-Name- 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
| Problem | Lösung |
|---|---|
python: command not found | Python 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 use | Andere 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:
-
Lade VS Code herunter: https://code.visualstudio.com/
-
Öffne den Streaming_Tool-Ordner:
File → Open Folder -
Installiere diese Erweiterungen (Extensions):
- Python (Microsoft)
- Pylance (Microsoft)
-
Wende den Python-Interpreter auf deine
venvan:Ctrl+Shift+P→ "Python: Select Interpreter"- Wähle
./venv/bin/python(oder.\venv\Scripts\python.exeauf Windows) - Allternativ direkt Python auswählen wenn du kein
venvnutzt
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:
| Phase | Aufgabe | Wer macht es? |
|---|---|---|
| 1. Empfangen | Daten von TikTok-Servern abholen | TikTokLive API (in unserem Programm) |
| 2. Verarbeiten | Events verstehen & dokumentieren | Python-Skripte analyzieren die Rohdaten |
| 3. Ausführen | Befehl an Minecraft schicken | RCON ü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:
- Bestimmt, welcher Befehl nötig ist (basierend auf dem Event)
- Sendet ihn über RCON an Minecraft
- 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ällt | Folge |
|---|---|
| 1 bricht | Keine Events von TikTok → Nichts passiert |
| 2 bricht | Events werden nicht verstanden → Falsche Aktion oder gar keine |
| 3 bricht | Befehl erreicht Minecraft nicht → Game hat keine Reaktion |
Nächste Schritte
Jede dieser 3 Phasen wird in diesem Kapitel im Detail erklärt:
- Daten von TikTok empfangen – Wie die API funktioniert
- Events verarbeiten – Wie das Programm Daten analysiert
- Daten an Minecraft senden – Wie RCON funktioniert
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), wieactions.mcageschrieben 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:
- Verbindet sich mit TikTok-Servern (wie die Mobile App)
- Hört zu auf dem WebSocket-Stream
- Empfängt Events (Gifts, Follows, Likes) in Echtzeit
- Ü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:
| Typ | Beispiel |
|---|---|
| Gift | User sendet 5x Gifts |
| Follow | User folgt dem Kanal |
| Like | User liked einen Stream |
| Share | User teilt den Stream |
| Comment | User 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?
| Problem | Folge | Lösung |
|---|---|---|
| Event-Struktur unbekannt | Klassifizierung fehlgeschlagen | Error-Log, Event wird verworfen |
| Queue läuft über | Speicher-Problem (sehr selten) | Ältere Events löschen |
| Zu viele Events pro Sekunde | Backlog aufgebaut | Minecraft braucht länger zu reagieren |
| Event kommt beschädigt an | Daten-Parse-Fehler | Validierung 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
-
Verbindung aufbauen
Programm → "Ich bin Admin, hier ist das Passwort" Server → Authentifizierung OK, Verbindung offen -
Befehl schicken
Programm → "/say User XY hat gefolgt!" Server → Befehl wird ausgeführt (im Spiel sichtbar) -
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?
| Problem | Folge | Lösung |
|---|---|---|
| RCON-Server nicht erreichbar | Befehle können nicht gesendet werden | Minecraft-Server prüfen, Firewall-Einstellungen |
| Falsches Passwort | Authentifizierung fehlgeschlagen | Passwort in config.yaml überprüfen |
| Falscher Port | Verbindung schlägt fehl | Standard: 25575, in config.yaml überprüfen |
| Befehl syntaktisch falsch | Minecraft lehnt ab | Befehl in actions.mca überprüfen |
| Zu viele Befehle pro Sekunde | Minecraft kann nicht alle abarbeiten | Backlog aufgebaut, Spieler sieht zeitverzögerte Reaktion |
| Minecraft-Server crasht | RCON-Verbindung bricht ab | Auto-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.yamlim Plugin-Ordner erstellen und diese für Einstellungen nutzen. So sparst du dir später Anpassungen am Code, wenn die globaleconfig.yamlnicht 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.yamlkonfiguriert - 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
- Plugin-Struktur verstehen (Ordner, Dateien, Config)
- HTTP-Server mit Flask erstellen (Events empfangen)
- Minecraft-Befehle senden (RCON-Kommunikation)
- Datenspeicherung & Konfiguration (Benutzerdaten)
- GUI mit pywebview (Visuelles Interface optional)
- Zwischen Plugins kommunizieren (HTTP + Fehlerbehandlung)
- 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.yamlDatei 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 Konfigurationsdateiparse_args: Liest Command-Line-Argumenteget_root_dir,get_base_dir,get_base_file: Ermitteln wichtige Verzeichnisse und Dateipfaderegister_plugin: Registriert dein PluginAppConfig: 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: StattTrue/Falsezu hardcodieren, kannst du auch Config-Werte nutzen:enable=cfg.get("custom_name", {}).get("enable", True)So können Nutzer dein Plugin in der
config.yamlein- und ausschalten! -
level: Bestimmt ab wann das Terminal sichtbar ist (abhängig vomlog_levelin derconfig.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 wirdTrue= GUI wird unterstütztFalse= 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
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 mitsys.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.ymlim Projekt. Hier ein paar Beispiele:
player_deathplayer_respawnplayer_joinplayer_quitblock_breakentity_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:
- Flask starten und auf Port X lauschen
- Den
/webhookEndpoint bereitstellen - 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 SpielSTARTUP: 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:
-
Webhook funktioniert nicht?
- Prüf, dass dein Port in
config.yamleingestellt ist - Prüf, dass die URL in
configServerAPI.ymlkorrekt ist - Schau in deine Log-Datei
- Prüf, dass dein Port in
-
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
/webhookEndpoint verarbeitet eingehende Events - Der Port muss in
config.yamlundconfigServerAPI.ymlsynchronisiert 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()ausconfig.yaml - Daten speichern: JSON in
DATA_DIRfü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:
- Flask-Backend (Python) → HTTP-Server, Verarbeitung, Daten
- 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:
- HTTP-Requests (Clean, async-ready) EMPFOHLEN
- Datei-Austausch (Einfach, aber Race Conditions)
- 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
| Phase | Fehler | Handling |
|---|---|---|
| Startup | Config fehlt | Defaults nutzen + Log |
| Flask Server | Port bereits in Benutzung | Alternative Port + Error-Message |
| HTTP-Requests | Timeout/Connection | Retry-Logic + Fallback |
| Datei-I/O | Permission denied | Try-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
| Fehler | Ursache | Fix |
|---|---|---|
| Port already in use | Port 8001 belegt | Alternative Port in config.yaml |
| Connection refused | Anderes Plugin offline | try-except + Fallback |
| Timeout | Request zu langsam | timeout=5 erhöhen |
| JSON decode error | Malformed response | json.JSONDecodeError fangen |
| FileNotFoundError | Config-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.ps1script und es dann imreleaseOrdner 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
/healthEndpoint- 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:
- actions.mca – Die Datei mit allen Mappings (statisch)
- Code in main.py – Liest die Datei beim Start
- RCON-Protokoll – Sendet Commands an Minecraft (Netzwerk)
Warum diese Aufteilung?
- ✓ User können die
actions.mcabearbeiten 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
.mcaDatei 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?
| Pfad | Zweck |
|---|---|
defaults/actions.mca | Vorlage mit Beispiel-Mappings |
data/actions.mca | Wird 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 Spielerlike_2→ Inventar leeren + alle Spieler tötenlikes→ 2 Creeper spawnen (Vanilla mitx2)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
| Konzept | Erklärung |
|---|---|
| Format | TRIGGER:<TYPE>COMMAND xANZAHL |
| Dateien | defaults/actions.mca (Vorlage) → data/actions.mca (aktiv) |
| Validierung | generate_datapack() prüft Syntax beim Start |
| Ablauf | Event → 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:
| Teil | Bedeutung | Beispiel |
|---|---|---|
| TRIGGER | Eindeutiger Name oder ID | 8913, follow, likes |
| : | Trennzeichen | : (immer erforderlich) |
| <TYPE> | Art des Commands | /, !, $, >> |
| COMMAND | Was soll passieren? | give @a diamond, tnt 2 0.1 2 |
| xANZAHL | Wie 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 inconfig.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 1× 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
| Concept | Erklärung |
|---|---|
| Trigger | Gift-ID, follow, likes, like_2 |
| Command-Typen | / (Vanilla → mcfunction), ! (Plugin → RCON), $ (Spezial) |
| xANZAHL | Command N-mal wiederholen |
| Semikolon | Mehrere 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:
- Eine Zeile pro Aktion – Einfach zu verstehen
- Trennung mit
:undx– Klare Struktur ohne Klammern - Minimal, prägnant – Anfänger verstehen es schnell
- Nicht zu streng (Optional:
xkann fehlen) - Kommentar-Support (#) – Lines einfach deaktivieren
- 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.
/summondauert länger als/say - Z.B.
!tnt 1000dauert länger als!tnt 1
Type (/, !, $) ist aus Performence sicht egal!
Es kommt auf denn Command an.
Zusammenfassung
- Parser zerlegt actions.mca einmal beim Start
- In-Memory-Dictionary wird aufgebaut
- Command-Typen werden klassifiziert (/, !, $)
- Runtime = schnelle Dictionary-Lookups
- 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 allefollow-,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?
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:
randomtime- (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 eigenenimport-Anweisungen für externe Pakete wierequests,flask,aiohttpusw. — 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 nachevent_hooks/kopiert. Der Bot lädt die Hooks immer aus dem Release-Pfad (event_hooks/), nicht aussrc/event_hooks.
Die Ladereihenfolge im Detail:
- Parsing:
generate_datapack()liestactions.mcaund sammelt alle$-Command-Namen (z. B.$begruessung→begruessung) - Import: Alle
.py-Dateien inevent_hooks/werden viaimportlibdynamisch importiert - Registrierung: Für jedes geladene Modul wird
register(api)aufgerufen — dort werden die Handler registriert - 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 aufapi. api.register_action("begruessung", begruessung)meldet den Handler unter dem Namenbegruessungan. Dieser Name muss exakt mit dem$-Command in deractions.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:
| Argument | Typ | Beschreibung |
|---|---|---|
user | str | Der TikTok-Benutzername, der das Event ausgelöst hat (z. B. "max_mustermann") |
trigger | str | Der Name des $-Commands, der gerade ausgeführt wird (z. B. "begruessung") |
context | dict | Reserviert 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 deractions.mcasteht. 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 wiebegruessungodersuperjump— 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 —followsteht links in deractions.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?
- TikTok meldet Gift
5655→ Trigger5655wird abgearbeitet execute_global_command("5655", …)findet$grosses_geschenk→ ruft deinen Handler auf- Handler sendet die RCON-Nachricht und schiebt
"follow"in die Queue - Kurz darauf:
execute_global_command("follow", …)läuft — führt$begruessungund/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: $begruessungdef begruessung(user, trigger, context): api.enqueue_trigger("follow", user) # ← Loop!Sobald jemand auf TikTok folgt, löst das den
follow-Trigger aus. Dessen Handler schiebtfollowwieder in die Queue, der Handler feuert erneut, schiebt wiederfollowhinein — 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_triggerwirft keine Exception — es gibt nur ein stillesreturnzurück zur aufrufenden Stelle. Das bedeutet:
Der Rest des Handlers läuft normal weiter. Hat
begruessungnach demenqueue_trigger-Aufruf noch weiteren Code (z. B. weiterercon_enqueue-Aufrufe, Logging, etc.), wird der vollständig ausgeführt. Nur der Eineenqueue_trigger-Aufruf ist blockiert.Die restlichen Aktionen aus der
actions.mca-Zeile laufen ebenfalls normal. Angenommen,followist so eingetragen:follow: $begruessung; /give @a minecraft:golden_apple 7Der Handler
begruessungwird aufgerufen (inklusive allem Code dahinter), und der/give-Command wird danach normal ausgeführt — der Ban betrifft nur denenqueue_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?
execute_global_command("5655", …)→ findet$geschenk_klein→ ruft deinen Handler auf- Handler gibt Speed-Effekt und schiebt
"dankeschoen"in die Queue execute_global_command("dankeschoen", …)→ findet$dankeschoen→ ruftdankeschoen-Handler auf- 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.mcaCommands, 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.pystarten und so Trigger bequem aus der Konsole testen – ganz ohne die .exe zu verwenden. Beachte aber: Auch beim Testen mitsend_trigger.pymuss 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:
randomWenn 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
| Problem | Folge | Lösung |
|---|---|---|
| Queue voll | Events gehen verloren | put_nowait() mit Exception-Handling |
| Verbindung bricht ab | Es kommen keine Commands an | Auto-reconnect |
| Command zu groß | RCON-Error | Command splitten |
| Zu schnell senden | Minecraft-Crash | Throttling 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
| Aspekt | Ohne .mcfunction | Mit .mcfunction |
|---|---|---|
| RCON-Last | 500 Pakete | 1 Paket |
| Geschwindigkeit | 5+ Sekunden | ~1 Tick |
| Queue-Belastung | Hoch | Minimal |
| Datendurchsatz | Riesig | Winzig |
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(
/) mitxN→ werden in .mcfunction-Dateien ausgelagert - Plugin Commands(
!) & Built-in($) → RCON direct sent - .mcfunction-Dateien werden beim Start generiert, nicht live updatet
- Performance:
xNsollte ≤ 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
| Kategorie | Speicherort | Zweck | Beispiele |
|---|---|---|---|
| Module (Core) | src/core/ | Infrastruktur & Kernlogik | validator, models, utils, paths, cli |
| Built-in Plugins | src/plugins/ | Standard-Funktionen | Timer, DeathCounter, WinCounter, LikeGoal, OverlayTxt |
| Custom Plugins | plugins/ (Benutzer) | Benutzerdefinierte Erweiterungen | Deine 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?
| Situation | Empfehlung |
|---|---|
| OBS Studio mit Browser-Source | DCS |
| TikTok Live Studio (keine Browser-Source) | ICS |
| Custom Streaming Software | DCS |
| Lokales Testen | DCS |
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: DCSwerden GUI-Plugins mit--gui-hiddengestartet → Flask läuft, aber kein Fenster wird geöffnet - Bei
control_method: ICSwerden 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)
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:
- BUILDIN_REGISTRY — fest in
start.pydefinierte Core-Module - PLUGIN_REGISTRY — dynamisch aus
PLUGIN_REGISTRY.jsongeladene 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
| Parameter | Typ | Beispiel | Funktion |
|---|---|---|---|
name | str | "Timer" | Eindeutige Identität (Logs, Status) |
path | Path | Path("plugins/timer/main.exe") | Absoluter Pfad zur EXE |
enable | bool | True | Startet Plugin beim Boot? |
level | int | 4 | Log-Level für Terminal-Sichtbarkeit |
ics | bool | True | Unterstützt GUI-Fenster (pywebview)? |
[!IMPORTANT] Alle fünf Parameter sind Pflicht. Fehlt einer oder ist ein unbekannter Key vorhanden, wird ein
ValueErrorgeworfen.
Log-Level Bedeutung
Der level-Parameter steuert die Terminal-Sichtbarkeit abhängig vom log_level in der config.yaml:
| Level | Name | Beschreibung |
|---|---|---|
| 0 | Off | Versteckt alles, inklusive GUI-Fenster |
| 1 | Silent | Versteckt Konsolen-Fenster, GUI bleibt aktiv |
| 2 | Standard | Zeigt nur Hauptprogramme |
| 3 | Advanced | Zeigt auch Hintergrund-Dienste |
| 4 | Debug | Zeigt alle aktivierten Prozesse |
| 5 | Override | Zeigt 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). Nachregister_plugin()muss sofortsys.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
| Komponente | Datei | Inhalt |
|---|---|---|
| AppConfig | core/models.py | Dataclass mit 5 Pflichtfeldern |
| BUILDIN_REGISTRY | start.py | Fest definierte Core-Module |
| PLUGIN_REGISTRY | PLUGIN_REGISTRY.json | Dynamisch registrierte Plugins |
| Registrierung | registry.py | Scannt Plugins mit --register-only |
| Scan-Cache | plugin_registry_scan_cache.json | Beschleunigt 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:
- pywebview öffnet ein Fenster
- Fenster lädt HTML von
http://localhost:5001 - JavaScript sendet HTTP-Requests an Flask-Routes
- Backend verarbeitet, speichert Config, sendet Antwort
Kritische Aspekte
| Aspekt | Bedeutung | Beispiel |
|---|---|---|
| Port Eindeutigkeit | Jedes GUI-Plugin braucht eindeutigen Port | GUI: 5000, Timer: 7878, LikeGoal: 9797 |
| Threading | Flask muss in Thread laufen, damit Window nicht blockiert | daemon=True ist wichtig |
| SSE für Live-Updates | Server-Sent Events für kontinuierliche Daten | /stream für Like-Counter |
| CORS | Bei Browser-Quellen: Access-Control-Allow-Origin: * nötig | Streaming-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:
- Source-Plugin sendet HTTP-Request an
http://localhost:PORT/endpoint - Target-Plugin empfängt Request, verarbeitet Aktion
- Target antwortet mit JSON oder Status
- 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:
| Plugin | Port | Config-Key |
|---|---|---|
| GUI | 5000 | GUI.Port |
| OverlayTxt | 5005 | Overlaytxt.Port |
| MinecraftServerAPI | 7777 | MinecraftServerAPI.WebServerPort |
| Timer | 7878 | MinecraftServerAPI.WebServerPortTimer |
| DeathCounter | 7979 | MinecraftServerAPI.DEATHCOUNTER_PORT |
| WinCounter | 8080 | WinCounter.WebServerPort |
| LikeGoal | 9797 | Gifts.LIKE_GOAL_PORT |
[!IMPORTANT] Jeder Port muss eindeutig sein. Wenn zwei Plugins den gleichen Port nutzen, schlägt der Start fehl.
Kritische Fehler vermeiden
| Fehler | Problem | Lösung |
|---|---|---|
| Synchrone Requests im Main-Thread | GUI/Server blockiert | In Thread oder mit timeout arbeiten |
| Nicht erreichbare Plugins | "Connection refused" | Port prüfen, Plugin läuft noch nicht? |
| Timeout zu kurz | Request bricht ab | Min. timeout=2 setzen |
| Request ohne Error-Handling | Absturz bei Fehler | Immer 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:
- Browser öffnet eine persistente Verbindung zu
/stream - Server sendet Daten über
yield(keinreturn) - 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
| Kommunikationsweg | Richtung | Beispiel |
|---|---|---|
| DCS (HTTP-Requests) | Plugin → Plugin | Timer ruft WinCounter /add auf |
| SSE (Server-Sent Events) | Plugin → Browser/OBS | DeathCounter aktualisiert Overlay |
| Webhooks | Minecraft → Plugin | player_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.yamlkonfiguriert sein - Fehlerbehandlung mit
try/exceptundtimeoutist 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:
- ICS (GUI-Plugins): Fensteraufnahme → Das GUI-Fenster wird als Video-Layer gezeigt
- DCS (HTTP-Plugins): Browser-Quelle → Browser rendert HTML vom HTTP-Server
Vergleich: ICS vs DCS
| Aspekt | ICS | DCS |
|---|---|---|
| Integration | Window Capture | Browser Source |
| Technologie | Fensteraufnahme | HTTP + HTML/CSS |
| Skalierung | Native Fenstergröße | Flexibel konfigurierbar |
| Latenz | Höher (Screenshot-zu-Screenshot) | Niedriger (direkte Renderung) |
| Fehlerbehandlung | Fenster muss sichtbar sein | Port muss online sein |
| Best for | Desktop GUI Tools | Live-Daten (Like-Counter, Timer) |
ICS-Integration: Fensteraufnahme
In OBS:
Quelle→+→Fensteraufnahmehinzufügen- Dropdown: Wähle GUI-Anwendung (z.B. "GUI Plugin [timer.exe]")
- Größe/Position anpassen
- 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:
Quelle→+→Browserquellehinzufügen- URL eingeben:
http://localhost:PORT(z.B.http://localhost:9797) - Breite/Höhe einstellen (z.B. 1280×720)
- Aktualisierungsrate: 60 FPS
- 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
| Problem | Ursache | Lösung |
|---|---|---|
| URL not reachable | Port blockiert/falsch | netstat -ano check, Firewall öffnen |
| Browser-Quelle zeigt blank | CORS-Fehler / HTML lädt nicht | Browser-Konsole inspizieren (F12) |
| Fenster-Capture funktioniert nicht | Modul nicht mit ics=True | Registry überprüfen, level checken |
| Latenz, Verzögerung | Server zu langsam | Server-Rendering optimieren, Bilder komprimieren |
| Es gibt nur Browser-Quelle, aber mein Modul hat ics=True | Das ist OK | ics=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
transparentbackground (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:
| Modul | Zweck |
|---|---|
models.py | Datenstrukturen (AppConfig, PluginInfo, etc.) |
cli.py | Command-Line Argumente parsen |
paths.py | Pfad-Funktionen (ROOT_DIR, BASE_DIR, etc.) |
utils.py | Helferfunktionen (Strings bereinigen, etc.) |
validator.py | Config 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):
| Bibliothek | Zweck |
|---|---|
TikTokLive | Verbindung zu TikTok Live |
Flask | Web-Framework für Webhooks |
pywebview | Desktop-GUI Fenster |
pyyaml | Config-Dateien lesen |
asyncio | Asynchrone 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:
- main.py – Wie Daten hereinkommen
- server.py – Wie der Minecraft-Server gestartet wird
- registry.py – Wie Plugins geladen werden
- 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.
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:
- Client erstellen
- Handler registrieren
- Verbinden
- 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 herGiftEvent→ wird ausgelöst, wenn ein Gift gesendet wirdFollowEvent→ wird ausgelöst, wenn jemand folgtConnectEvent→ wird ausgelöst, wenn die Verbindung hergestellt wirdLikeEvent→ 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:
- Event-Daten auslesen – Was ist im Event?
- Validieren – Ist es ein gültiges Event?
- Trigger finden – Welche Aktion soll ausgelöst werden?
- 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ät | Grund |
|---|---|
if event.gift.combo | Manche 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-Except | Events dürfen nicht das ganze System crashen |
Zusammenfassung
Ein TikTok-Client-Handler:
- ✓ Lauscht auf Events
- ✓ Empfängt Event-Daten
- ✓ Validiert die Daten
- ✓ Findet passende Aktion
- ✓ Legt in Queue (nicht sofort ausführen!)
- ✓ Fehler abfangen (crasht nicht)
Nächster Schritt
Jetzt verstehst du, wie Handler arbeiten. Der nächste Schritt ist verstehen, was im Event selbst steckt.
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.nameein String ist) - Weniger Fehler (falsche Schlüssel → sofort Error statt Silent-Fail)
Event-Kategorien
Events werden in mehrere Kategorien eingeteilt:
| Kategorie | Beispiele | Zweck |
|---|---|---|
| User Events | Follow, Gift, Like | Aktion eines Zuschauers |
| System Events | Connect, Disconnect | System-Status |
| Stream Events | StreamStart, StreamEnd | Stream-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.
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:
| Situation | Was passiert | Wie oft wird der Handler aufgerufen? |
|---|---|---|
| Einfaches Gift | Zuschauer sendet Geschenk 1x | 1x (sofort) |
| Combo-Gift | Zuschauer sendet gleiches Geschenk mehrfach schnell hintereinander | Mehrere Male (mit repeat_count) |
| Streaking | TikTok sendet Notifications zum aktuellen Stand der Combo | Mehrfach (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.nameODERgift.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:
| Problem | Was kann passieren? | Wie schützen wir uns? |
|---|---|---|
Event hat keine gift-Eigenschaft | AttributeError | getattr() mit Default-Wert |
Event hat keine user-Eigenschaft | AttributeError | get_safe_username() prüft |
| Gift-Name/ID passt zu keiner Aktion | Gift wird ignoriert | if not target: return |
| Benutzername enthält ungültige Zeichen | Fehler beim Queuen | sanitize_filename() bereinigt |
| Queue ist voll (sehr selten) | put_nowait() Exception | Try-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?
- Streaking ignorieren – Nur echte Gift-Events verarbeiten
- Count bestimmen – 1x oder mehrfach?
- Daten auslesen – Gift-Name, ID, Benutzername
- Logger-Info – Sichtbarer Feedback für Debugging
- Trigger finden – Nach Name, dann nach ID
- Queue-Operation – Thread-sicher mit
call_soon_threadsafe - 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:
| Konzept | Erklärung |
|---|---|
| Combo-Gifts | Gleiche Gifts mehrfach = repeat_count erhöht sich |
| Streaking | TikTok sendet Status-Updates = wir ignorieren die |
| Trigger-Matching | Gift-Name oder ID → zu Aktion (TRIGGERS dictionary) |
| Asynchrone Queue | call_soon_threadsafe macht es thread-sicher |
| Fehlerbehandlung | Try-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:
| Merkmal | Gifts | Follows |
|---|---|---|
| Mehrfach-Verarbeitung | Combo möglich (5x, 10x, etc.) | Immer nur 1x |
| Status-Updates | Streaking: Mehrfach Notifications | Keine Notifications |
| Trigger-Verwaltung | Name UND ID möglich | Ein einziger Trigger: "follow" |
| Fehler-Komplexität | Hoch (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.userexistiert nicht? → get_safe_username() schütztget_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?
- Username auslesen & sanitieren – Mit get_safe_username()
- Logging – Wir sehen im Log, wer folgt
- Trigger prüfen – Existiert der "follow" Trigger?
- Queuen – Mit call_soon_threadsafe()
- 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?
| Szenario | Folge | Lösung |
|---|---|---|
event.user ist None | AttributeError | get_safe_username() wirft Exception |
Username ist leer string "" | Wird so gequeuet | Normal, kein Problem |
| "follow" Trigger existiert nicht | Event ignoriert | return früh |
| Queue voll (extrem selten) | put_nowait() Exception | Try-except fängt es |
| TikTok sendet Follow 2x schnell | Zwei Events hintereinander | Beide werden verarbeitet (Gewollt!) |
Fazit: Follow-Handler sind sehr robust – es gibt wenig, was schiefgehen kann.
Zusammenfassung & Nächster Schritt
Was du jetzt weißt:
| Konzept | Erklärung |
|---|---|
| Follow = Einfach | Keine Combos, kein Streaking, nur 1 Trigger |
| 3-Schritt-Ablauf | Username → Prüfen → Queuen |
| Trigger "follow" | Der einzige Trigger für Follow-Events |
| Fehlerbehandlung | Minimal nötig, get_safe_username() schützt viel |
| Best Practice Pattern | Gleich 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:
| Merkmal | Gifts | Follows | Likes |
|---|---|---|---|
| Event-Typ | Diskrete Events ("Gift gesendet") | Diskrete Events ("Folgt") | Kontinuierliche Zählung |
| Häufigkeit | Selten (User sendet Geschenk) | Selten (User folgt) | SEHR OFT |
| Benutzernamen | Ja, sichtbar | Ja, sichtbar | Eher selten sichtbar |
| Trigger-Logik | "Wenn Gift" | "Wenn Follow" | "Wenn Zähler erreicht z.b. 100er-Marke" |
| Threading-Problem | Nein, einfach | Nein, einfach | JA, 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?
- Initialisieren – Beim ersten Event den Startwert setzen
- Delta berechnen – Wie viele Likes sind neu?
- Lock holen – Thread-sicherheit aktivieren
- Regeln durchgehen – Für jede Like-Marke (100, 500, etc.)
- Blocks berechnen – Mit Integer-Division
// - 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?
| Szenario | Problem | Lösung |
|---|---|---|
| Like-Event vor Initialisierung | start_likes ist None | if start_likes is None: initialize() |
| Zwei Events gleichzeitig | Race Condition | with like_lock schützt |
| Intervall ist 0 | Division by Zero | if every <= 0: continue |
| Sehr schnelle Like-Flut | Viele Events/Sec | Blocks werden korrekt aggregiert |
| Lock hängt | Thread blockiert ewig | with like_lock auto-freigibt |
Fazit: Like-Handler versprechen die meiste Fehlerbehandlung, besonders wegen des Locks.
Zusammenfassung & Nächster Schritt
Was du jetzt weißt:
| Konzept | Erklärung |
|---|---|
| Likes ≠ Gifts | Kontinuierliche Zählung statt einzelner Events |
| Race Conditions | Mehrere Threads greifen gleichzeitig zu → Lock notwendig |
| Block-Berechnung | blocks = total_likes // intervall |
| Initialisierung | Beim ersten Event: Startwert setzen |
| Lock-Pattern | with threading.Lock() für Thread-Sicherheit |
| Fehlerbehandlung | Lock 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:
| Fehlertyp | Symptom | Beispiel |
|---|---|---|
| Syntax-Fehler | Programm startet gar nicht | def foo( - fehlende Klammer |
| Import-Fehler | ModuleNotFoundError | Abhängigkeit nicht installiert |
| Runtime-Fehler | Programm crasht während Ausführung | Division durch 0 |
| Logic-Fehler | Programm läuft, macht aber Falsches | if x = 5: statt if x == 5: |
| Configuration-Fehler | Settings sind falsch | config.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
4setzen:
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
- Öffne deine Python-Datei
- Klick links neben die Zeile → roter Punkt (Breakpoint)
- Starte das Programm mit
F5(Debug-Modus) - Wenn die Zeile erreicht wird → Programm pausiert
- Inspiziere Variablen, steps durch den Code
Debug-Controls
F10– Nächste Zeile (Step Over)F11– In Funktion gehen (Step Into)Shift+F11– Aus Funktion gehenF5– Weiter bis nächster BreakpointShift+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:
-
Check: Plugin in
PLUGIN_REGISTRYregistriert?# In start.py / registry.py {"name": "MyPlugin", "path": ..., "enable": True, ...} -
Check: Plugin hat
main.py?src/plugins/my_plugin/ ├── main.py # Muss existieren! ├── README.md └── version.txt -
Check: Plugin kann importieren?
python src/plugins/my_plugin/main.py --register-onlyWenn 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:
-
Flask läuft?
curl http://localhost:7878/webhook -X POST -d "{}" -
Firewall erlaubt Port? Port muss offen sein.
-
Config stimmt? Port in
config.yamlmuss 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 installalle 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 habepip install -r requirements.txtausgefü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.
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!
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.
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.
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?
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?
Wann nutze ich den Anhang?
| Situation | Anhang-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 – Das wichtigste Nachschlagewerk
- Debugging & Troubleshooting – Wenn etwas nicht funktioniert
- Hauptdokumentation – Zurück zum Hauptinhaltsverzeichnis
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ängigkeitFlask- Abhängigkeit- All sind in
requirements.txtaufgelistet.
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.yamlwird 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-Modulecore/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 Triggerlike_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_1001ist ein Trigger (wenn dieses Gift kommt)followist 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:
- Neue Version wird auf GitHub hochgeladen
- Nutzer startet das Programm
- Update-Script laden neue Version
- 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:
- Re-read Lesse es einfach nochmal
- Context: Suche nach dem Begriff in der Dokumentation – der Kontext hilft
- Code lesen: Schau dir an, wie der Begriff im echten Code verwendet wird
- 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.
| Modul | Aufgabe |
|---|---|
| paths.py | Verzeichnis-Verwaltung & Pfade |
| utils.py | Konfiguration laden, Hilfsfunktionen |
| models.py | Datenstrukturen (AppConfig) |
| validator.py | Syntax-Validierung (actions.mca) |
| cli.py | Command-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-Wurzelget_config_file()– Pfad zu config.yamlget_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()?
- Prüft, ob die Datei existiert
- Parst YAML
- Gibt Dictionary zurück
- 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
| Argument | Effekt |
|---|---|
--gui-hidden | Starte ohne GUI-Fenster (Headless) |
--register-only | Registriere 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:
| Modul | Nutzen |
|---|---|
| paths.py | Richtige Pfade finden (dev vs. release) |
| utils.py | Config zuverlässig laden |
| models.py | Plugin-Metadaten verwalten |
| validator.py | Fehler finden & berichten |
| cli.py | Verschiedene Start-Modi |
Praktisch für Entwickler:
- Plugin-Entwickler: Nutzen hauptsächlich
paths.pyundutils.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.yamlhinzufügen - ☑
config_versionerhöht? - ☑ Kommentare VOR erstem Key bleiben erhalten
- ☑ Code nutzt
.get()mit Defaults - ☑ Test: Migration funktioniert?
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_versionin derconfig.default.yamlgrößer ist als die in der aktuellenconfig.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.yamlangepasst hat, werden nicht überschrieben. - Bereinigung: Keys, die in der neuen
config.default.yamlnicht mehr existieren, werden aus derconfig.yamlgelö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()): VermeideKeyError-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,booloderstrsein 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.exeist die einzige Pflichtdatei des Plugin-Systems.
Python-Plugins funktionieren übrigens genauso:main.pywird über PyInstaller zumain.exekompiliert. 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.exeneu 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
| Feld | Typ | Beschreibung |
|---|---|---|
name | string | Eindeutiger Name des Plugins |
path | string | Absoluter Pfad zur main.exe |
enable | bool | Ob das Plugin beim Start gestartet wird |
level | int | Sichtbarkeitslevel (siehe unten) |
ics | bool | Hat das Plugin ein GUI-Fenster? |
Sichtbarkeitslevel (level)
Steuert, ob das Konsolenfenster des Plugins angezeigt wird, abhängig vom log_level in der config.yaml:
| Level | Bedeutung |
|---|---|
| 0 | Verboten – überschreibt alle Sichtbarkeitsregeln, nie verwenden! |
| 1 | Für sehr sehr wichtige Ausgaben |
| 2 | Hauptprogramme |
| 3 | Hintergrunddienste |
| 4 | Debug/Entwicklung ← für eigene Plugins empfohlen |
| 5 | Verboten – überschreibt alle Sichtbarkeitsregeln, nie verwenden! |
[!NOTE] Level 0 und 5 dürfen nicht verwendet werden. Wenn in der
config.yamllog_level = 0oderlog_level = 5gesetzt 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. Wenncontrol_methodin derconfig.yamlaufDCSsteht, wird dein Plugin mit--gui-hiddengestartet (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)oderstd::filesystem::absolute(argv[0])
Argument-Handling
Dein Plugin muss mindestens zwei Argumente kennen:
| Argument | Verhalten |
|---|---|
--register-only | JSON ausgeben, sofort beenden |
--gui-hidden | Fenster 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
| Event | Standardmäß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
| Variable | Berechnung | Beispiel |
|---|---|---|
BASE_DIR | Verzeichnis von main.exe | …/plugins/myplugin/ |
ROOT_DIR | BASE_DIR/../.. | …/build/release/ |
CONFIG_FILE | ROOT_DIR/config/config.yaml | |
DATA_DIR | ROOT_DIR/data/ | |
LOGS_DIR | ROOT_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.
| Situation | Was tun |
|---|---|
| Config-Datei fehlt | Default-Wert nutzen, nicht abstürzen |
| Port bereits belegt | Fehlermeldung ausgeben, cleanly exit |
| HTTP-Request kehrt nicht zurück | Immer Timeout setzen |
| Unbehandelter Absturz | Top-level Exception-Handler mit Log-Ausgabe |
| JSON-Parsefehler im Webhook | try/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