Panel: German/English, redesigned sidebar, in-game admins page, Steam Web API key link, RAR/7z save upload (SharpCompress), responsive cards; upload errors return 400; tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-27 19:40:46 +02:00
co-authored by Claude Opus 5.5
parent c74c48dfc8
commit 8df6cbcafc
12 changed files with 700 additions and 58 deletions
+45 -14
View File
@@ -97,31 +97,62 @@ public static class SaveStore
}
/// <summary>
/// Spielstand aus einer ZIP lesen. Nur die bekannten Spielstand-Dateien werden genommen (egal in welchem Ordner
/// der ZIP) - Pfade aus der ZIP werden nie benutzt. Gibt einen Fehler zurück oder null.
/// Spielstand aus einem Archiv lesen (ZIP, RAR, 7z, tar ...). Nur die bekannten Spielstand-Dateien werden genommen
/// (egal in welchem Ordner im Archiv) - Pfade aus dem Archiv werden nie benutzt. Gibt einen Fehler zurück oder null.
/// </summary>
public static string ExtractZip(byte[] zip, out Dictionary<string, byte[]> found)
public static string ExtractArchive(byte[] data, out Dictionary<string, byte[]> found)
{
found = new Dictionary<string, byte[]>(StringComparer.OrdinalIgnoreCase);
try
{
using var archive = new ZipArchive(new MemoryStream(zip), ZipArchiveMode.Read);
using var archive = SharpCompress.Archives.ArchiveFactory.OpenArchive(new MemoryStream(data), null);
long total = 0;
foreach (var entry in archive.Entries)
var result = found;
// Eintrag lesen: nur bekannte Spielstand-Dateien, Größe begrenzt. Gibt einen Fehler zurück oder null.
string Take(SharpCompress.Common.IEntry entry, Func<Stream> open)
{
string file = Path.GetFileName(entry.FullName.Replace('\\', '/'));
if (entry.IsDirectory || entry.Key == null) return null;
string file = Path.GetFileName(entry.Key.Replace('\\', '/'));
string known = SaveFiles.FirstOrDefault(f => string.Equals(f, file, StringComparison.OrdinalIgnoreCase));
if (known == null) continue;
if (entry.Length > MaxZipBytes || (total += entry.Length) > MaxZipBytes) return "The save game is too large";
using var s = entry.Open();
if (known == null) return null;
if (entry.IsEncrypted) return "The archive is password protected";
using var s = open();
using var ms = new MemoryStream();
s.CopyTo(ms);
found[known] = ms.ToArray();
var chunk = new byte[81920];
int read;
while ((read = s.Read(chunk, 0, chunk.Length)) > 0)
{
total += read;
if (total > MaxZipBytes) return "The save game is too large";
ms.Write(chunk, 0, read);
}
result[known] = ms.ToArray();
return null;
}
string error = null;
if (archive.IsSolid || archive.Type == SharpCompress.Common.ArchiveType.SevenZip)
{
// "Solide" RAR/7z: nur der Reihe nach lesbar
using var reader = archive.ExtractAllEntries();
while (error == null && reader.MoveToNextEntry()) error = Take(reader.Entry, reader.OpenEntryStream);
}
else
{
foreach (var entry in archive.Entries)
{
error = Take(entry, entry.OpenEntryStream);
if (error != null) break;
}
}
if (error != null) return error;
}
catch (Exception e) when (e is not OutOfMemoryException)
{
return "This file could not be read - upload a ZIP, RAR or 7z archive (" + e.Message + ")";
}
catch (InvalidDataException) { return "This is not a valid ZIP file"; }
if (!found.ContainsKey("defaultES2File.txt"))
return "No defaultES2File.txt in the ZIP - zip the files from %USERPROFILE%\\AppData\\LocalLow\\Amistech\\My Summer Car";
return "No defaultES2File.txt in the archive - pack the files from %USERPROFILE%\\AppData\\LocalLow\\Amistech\\My Summer Car";
return null;
}
@@ -145,7 +176,7 @@ public static class SaveStore
/// <summary>Server aus: ZIP als neuen Spielstand übernehmen (Backup vorher, neuer Reset-Punkt, Weltzustand leer).</summary>
public static string ImportZipOffline(string dataDir, byte[] zip, string by)
{
string error = ExtractZip(zip, out var files);
string error = ExtractArchive(zip, out var files);
if (error != null) return error;
try
{