Zum Inhalt springen
  • Dunkel
  • Hell
  • System

Daten-Persistenz: JSON & CSV in Dateien

Globals sind perfekt für einzelne Werte: ein Zähler, der letzte Gewinner, ein Flag. Sobald du aber eine wachsende Liste speichern willst, etwa ein Log aller gespielten Matches, oder die Daten ausserhalb von Streamer.bot auswerten möchtest, etwa in Excel oder einem Python-Skript, wird eine Datei zur besseren Wahl. Eine Datei kannst du öffnen, sichern, versionieren und mit anderen Programmen lesen. Dieser Abschnitt zeigt, wie du aus C# heraus mit System.IO Dateien schreibst, eigene Objekte als JSON ablegst und beim Start wieder einliest, plus den einfachen CSV-Weg ganz ohne Library.

Core C# Code Execute C# Code

Doku:

Beide speichern Zustand über die Action hinaus. Die Frage ist, wie die Daten aussehen und wer sie liest.

KriteriumGlobalDatei
Einzelwert (Zähler, Flag)IdealOverkill
Wachsende Liste, viele EinträgeUnhandlichIdeal
Lesen ausserhalb von SB (Excel, Editor)NeinJa
Strukturierte ObjekteNur mit JSON-String drumherumNativ als JSON-Datei
GeschwindigkeitSchneller (RAM)Langsamer (Disk)
Backup, VersionierungNur über ExportDatei einfach kopieren

Faustregel: Einzelwerte bleiben im Global. Listen, Tabellen und alles was ein Mensch oder ein anderes Programm lesen soll, kommt in eine Datei. Das No-Code-Pendant dazu ist das File I/O Pattern.

Die File-Klasse aus System.IO deckt fast alles ab, was du brauchst. Drei Methoden reichen für den Anfang:

using System.IO;
public class CPHInline {
public bool Execute() {
string path = @"D:\streamerbot-data\notiz.txt";
// komplett überschreiben (legt die Datei an falls nicht vorhanden)
File.WriteAllText(path, "Hallo Welt");
// eine Zeile anhängen (legt die Datei ebenfalls an falls nötig)
File.AppendAllText(path, "\nNoch eine Zeile");
// den ganzen Inhalt als String einlesen
string inhalt = File.ReadAllText(path);
CPH.LogInfo($"Datei-Inhalt: {inhalt}");
return true;
}
}
  • File.WriteAllText schreibt einen String und überschreibt eine bestehende Datei komplett.
  • File.AppendAllText hängt an, ohne den alten Inhalt zu löschen. Genau richtig für Logs.
  • File.ReadAllText liest den gesamten Inhalt als einen String.

Das using System.IO; gehört nach oben in deinen Code. DLL-Referenzen und Using-Direktiven verwaltest du sonst im References-Tab der C#-Action, beschrieben in der offiziellen Doku zu Custom-DLLs.

In C#-Strings ist der Backslash ein Escape-Zeichen. Ein Windows-Pfad mit einfachen Backslashes wird deshalb falsch interpretiert. Zwei saubere Wege:

// 1) Backslashes verdoppeln
string a = "D:\\streamerbot-data\\matches.json";
// 2) verbatim-String mit @ davor, dann zählt der Backslash normal
string b = @"D:\streamerbot-data\matches.json";

Der @-Weg ist lesbarer und der empfohlene Standard. Nutze immer einen absoluten Pfad. Ein relativer Pfad wie "matches.json" landet sonst irgendwo im Streamer.bot-Programmverzeichnis, was du selten willst und schwer wiederfindest.

Der eigentliche Mehrwert: du speicherst nicht nur Text, sondern echte Objekte. Definiere eine kleine Klasse, sammle sie in einer List<T> und serialisiere die ganze Liste mit Newtonsoft in einen JSON-String. Newtonsoft.Json ist in C#-Actions standardmässig referenziert, die Methoden findest du in der C#-Methoden-Referenz.

using System;
using System.IO;
using System.Collections.Generic;
using Newtonsoft.Json;
public class MatchResult {
public string Map { get; set; }
public bool Won { get; set; }
public int Kills { get; set; }
public long Timestamp { get; set; }
}
public class CPHInline {
private const string Path = @"D:\streamerbot-data\matches.json";
public bool Execute() {
// 1) bestehende Liste laden (oder leere Liste beim ersten Lauf)
List<MatchResult> matches;
if (File.Exists(Path)) {
string json = File.ReadAllText(Path);
matches = JsonConvert.DeserializeObject<List<MatchResult>>(json) ?? new List<MatchResult>();
} else {
matches = new List<MatchResult>();
}
// 2) neuen Eintrag aus Argumenten bauen (TryGetArg statt args[...])
CPH.TryGetArg("map", out string map);
long now = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds;
matches.Add(new MatchResult {
Map = map ?? "unknown",
Won = true,
Kills = 17,
Timestamp = now
});
// 3) komplette Liste als JSON zurückschreiben (formatiert = lesbar)
string output = JsonConvert.SerializeObject(matches, Formatting.Indented);
File.WriteAllText(Path, output);
CPH.SendMessage($"📊 Match gespeichert, insgesamt {matches.Count} Matches im Log.");
return true;
}
}

Das Muster ist immer dasselbe: laden, ändern, ganz zurückschreiben. Beim ersten Lauf existiert die Datei nicht, File.Exists ist false und du startest mit einer leeren Liste. JsonConvert.DeserializeObject<List<MatchResult>> macht aus dem JSON-Text wieder echte C#-Objekte, JsonConvert.SerializeObject mit Formatting.Indented schreibt sie menschenlesbar zurück. Den ?? new List<MatchResult>()-Teil brauchst du, weil DeserializeObject bei leerem oder kaputtem Inhalt null liefern kann. Den map-Wert holst du mit CPH.TryGetArg, weil ein direkter Zugriff über args["map"] bei fehlendem Key eine Exception wirft und die Instanz blockieren kann.

Wenn die Daten flach sind und in Excel landen sollen, ist CSV oft praktischer als JSON. Für einfache Fälle reichen string.Join zum Bauen und Split zum Lesen, ganz ohne Library.

using System;
using System.IO;
using System.Collections.Generic;
public class CPHInline {
private const string Path = @"D:\streamerbot-data\matches.csv";
public bool Execute() {
// Bauen: eine Zeile zusammensetzen und anhängen
CPH.TryGetArg("map", out string map);
CPH.TryGetArg("kills", out string kills);
string zeile = string.Join(";", map ?? "", kills ?? "0", "true");
File.AppendAllText(Path, zeile + "\n");
// Parsen: alle Zeilen lesen und in Felder zerlegen
if (File.Exists(Path)) {
foreach (string line in File.ReadAllLines(Path)) {
if (string.IsNullOrWhiteSpace(line)) continue;
string[] felder = line.Split(';');
string m = felder[0];
string k = felder[1];
CPH.LogInfo($"Map: {m}, Kills: {k}");
}
}
return true;
}
}

Im deutschsprachigen Excel ist das Semikolon ; als Trenner oft praktischer als das Komma, weil Excel-DE Komma als Dezimaltrennzeichen liest. File.ReadAllLines gibt dir ein string[] mit allen Zeilen, Split(';') zerlegt jede Zeile in ihre Felder.

Datei-Zugriffe können fehlschlagen, weil die Datei fehlt, gesperrt ist oder der Pfad nicht stimmt. Ohne Absicherung bricht die ganze Action mit einer Exception ab. Pack den Zugriff deshalb in einen try-catch und logge den Fehler, statt die Action stillschweigend sterben zu lassen.

using System.IO;
public class CPHInline {
public bool Execute() {
string path = @"D:\streamerbot-data\matches.json";
try {
string json = File.ReadAllText(path);
CPH.LogInfo($"Geladen, {json.Length} Zeichen.");
return true;
} catch (FileNotFoundException) {
CPH.LogWarn("Datei noch nicht vorhanden, das ist beim ersten Lauf normal.");
return false;
} catch (IOException ex) {
// z.B. Datei von einem anderen Programm gesperrt
CPH.LogError($"Datei-Fehler: {ex.Message}");
return false;
}
}
}

So weisst du im Log genau, was schiefging, und der Rest deiner Automation läuft kontrolliert weiter, statt mit einer rohen Exception abzubrechen.

  • Datei existiert beim ersten Lauf nicht: Jeder Lese-Zugriff muss den allerersten Start aushalten, an dem noch nichts geschrieben wurde. Immer mit File.Exists(path) prüfen oder den try-catch auf FileNotFoundException nutzen und mit einer leeren Liste starten.
  • Relativer Pfad zeigt ins SB-Verzeichnis: Schreibst du nur "matches.json" statt eines absoluten Pfads, landet die Datei im aktuellen Arbeitsverzeichnis von Streamer.bot, nicht dort wo du sie vermutest. Immer absolut, etwa @"D:\streamerbot-data\matches.json".
  • Backslash nicht escaped: "D:\daten\x.json" wird in C# falsch interpretiert, weil \d und \x Escape-Sequenzen sind. Entweder @"D:\daten\x.json" mit @ oder "D:\\daten\\x.json" mit doppelten Backslashes.
  • Ordner fehlt: File.WriteAllText legt die Datei an, aber nicht den Ordner. Verzeichnis vorher anlegen oder Directory.CreateDirectory(...) aufrufen, sonst DirectoryNotFoundException.
  • Argumente direkt indizieren: args["map"] wirft bei fehlendem Key eine KeyNotFoundException und kann die Instanz blockieren. Immer CPH.TryGetArg("map", out string map) nutzen und einen Default setzen.
  • Gleichzeitiger Zugriff: Wenn zwei Actions oder ein externes Programm dieselbe Datei zur selben Zeit anfassen, kann eine IOException fliegen oder die Datei kurz leer wirken. Bei häufigen Schreibvorgängen try-catch setzen und im Zweifel über die Action-Queue serialisieren, statt parallel zu schreiben.
  • DeserializeObject liefert null: Bei leerem oder beschädigtem JSON gibt JsonConvert.DeserializeObject<List<T>> ein null zurück. Immer mit ?? new List<T>() absichern, sonst kommt beim nächsten .Add eine NullReferenceException.