Refactor: reorganize scripts with domain-driven design structure
Move all C# scripts from flat structure to organized folders: - Core/ for game logic (Game, GameState, GameSettings) - UI/Menus/ and UI/Dialogs/ for user interface - Systems/ for reusable systems (Save, Localization) - Data/ for data models and configuration - Added framework for future: Gameplay/, Story/, Modding/ Update all .tscn scene files to reference new script paths. Fix timing issue in AdvancedSaveDialog focus handling.
This commit is contained in:
9
scripts/Systems/Events/EventSystem.cs
Normal file
9
scripts/Systems/Events/EventSystem.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
// Placeholder för event system
|
||||
// Detta kommer att innehålla en global event bus för löst kopplad kommunikation mellan system
|
||||
|
||||
namespace TheGame.Systems.Events
|
||||
{
|
||||
// TODO: Implementera EventBus/MessageBus system
|
||||
// TODO: Implementera EventData base class
|
||||
// TODO: Implementera event subscription system
|
||||
}
|
||||
1
scripts/Systems/Events/EventSystem.cs.uid
Normal file
1
scripts/Systems/Events/EventSystem.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b3slxjam3wsfl
|
||||
255
scripts/Systems/Localization/LocalizationManager.cs
Normal file
255
scripts/Systems/Localization/LocalizationManager.cs
Normal file
@@ -0,0 +1,255 @@
|
||||
using Godot;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace TheGame
|
||||
{
|
||||
public class LocalizationManager
|
||||
{
|
||||
private static LocalizationManager _instance;
|
||||
private Dictionary<string, string> _currentTranslations;
|
||||
private Dictionary<string, string> _fallbackTranslations;
|
||||
private string _currentLanguage = "eng";
|
||||
private readonly string _languagesPath;
|
||||
|
||||
public static LocalizationManager Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_instance == null)
|
||||
{
|
||||
_instance = new LocalizationManager();
|
||||
}
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
private LocalizationManager()
|
||||
{
|
||||
// För development: använd fil-systemet
|
||||
// För export: använd Godot's resource system
|
||||
_languagesPath = "res://resources/languages";
|
||||
|
||||
// Skapa engelska som fallback om den inte finns
|
||||
CreateDefaultEnglishFile();
|
||||
|
||||
// Ladda engelska som fallback
|
||||
_fallbackTranslations = LoadLanguageFile("eng");
|
||||
|
||||
// Ladda nuvarande språk
|
||||
_currentTranslations = _fallbackTranslations;
|
||||
}
|
||||
|
||||
private void CreateDefaultEnglishFile()
|
||||
{
|
||||
// Kontrollera om engelsk fil finns i resources
|
||||
if (!Godot.FileAccess.FileExists("res://resources/languages/eng.json"))
|
||||
{
|
||||
var defaultTranslations = new Dictionary<string, string>
|
||||
{
|
||||
// Main Menu
|
||||
{"main_menu_title", "THE GAME"},
|
||||
{"main_menu_start", "Start Game"},
|
||||
{"main_menu_load", "Load Game"},
|
||||
{"main_menu_settings", "Settings"},
|
||||
{"main_menu_exit", "Exit"},
|
||||
|
||||
// Pause Menu
|
||||
{"pause_menu_title", "PAUSED"},
|
||||
{"pause_menu_resume", "Resume"},
|
||||
{"pause_menu_save", "Save Game"},
|
||||
{"pause_menu_settings", "Settings"},
|
||||
{"pause_menu_main_menu", "Exit to Main Menu"},
|
||||
{"pause_menu_desktop", "Exit to Desktop"},
|
||||
|
||||
// Load Game Menu
|
||||
{"load_game_title", "LOAD GAME"},
|
||||
{"load_game_back", "Back to Main Menu"},
|
||||
{"load_game_no_saves", "No saved games found"},
|
||||
{"load_game_playtime", "Playtime"},
|
||||
{"load_game_saved", "Saved"},
|
||||
|
||||
// Save Dialog
|
||||
{"save_dialog_title", "Save Game"},
|
||||
{"save_dialog_enter_name", "Enter save name:"},
|
||||
{"save_dialog_placeholder", "My Save Game"},
|
||||
|
||||
// Settings Menu
|
||||
{"settings_title", "SETTINGS"},
|
||||
{"settings_general", "General"},
|
||||
{"settings_video", "Video"},
|
||||
{"settings_audio", "Audio"},
|
||||
{"settings_apply", "Apply"},
|
||||
{"settings_cancel", "Cancel"},
|
||||
{"settings_back", "Back"},
|
||||
|
||||
// General Settings
|
||||
{"settings_language", "Language"},
|
||||
|
||||
// Video Settings
|
||||
{"settings_fullscreen", "Fullscreen"},
|
||||
|
||||
// Audio Settings
|
||||
{"settings_master_volume", "Master Volume"},
|
||||
{"settings_background_volume", "Background Volume"},
|
||||
{"settings_effects_volume", "Effects Volume"},
|
||||
{"settings_radio_volume", "Radio Volume"},
|
||||
|
||||
// Game
|
||||
{"game_content", "GAME CONTENT HERE\\nPress ESC or click pause button to pause"},
|
||||
|
||||
// Common
|
||||
{"ok", "OK"},
|
||||
{"cancel", "Cancel"},
|
||||
{"yes", "Yes"},
|
||||
{"no", "No"}
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
string jsonString = JsonSerializer.Serialize(defaultTranslations, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
});
|
||||
|
||||
// Försök spara endast i development mode
|
||||
if (OS.IsDebugBuild())
|
||||
{
|
||||
var file = Godot.FileAccess.Open("res://resources/languages/eng.json", Godot.FileAccess.ModeFlags.Write);
|
||||
if (file != null)
|
||||
{
|
||||
file.StoreString(jsonString);
|
||||
file.Close();
|
||||
GD.Print("Created default English language file in resources");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
GD.PrintErr($"Failed to create English language file: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<string> GetAvailableLanguages()
|
||||
{
|
||||
var languages = new List<string>();
|
||||
|
||||
try
|
||||
{
|
||||
// Använd DirAccess för att läsa från resources
|
||||
var dir = DirAccess.Open("res://resources/languages");
|
||||
if (dir != null)
|
||||
{
|
||||
dir.ListDirBegin();
|
||||
string fileName = dir.GetNext();
|
||||
|
||||
while (fileName != "")
|
||||
{
|
||||
if (!dir.CurrentIsDir() && fileName.EndsWith(".json"))
|
||||
{
|
||||
string languageCode = fileName.GetBaseName();
|
||||
languages.Add(languageCode);
|
||||
}
|
||||
fileName = dir.GetNext();
|
||||
}
|
||||
dir.ListDirEnd();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
GD.PrintErr($"Failed to get available languages: {ex.Message}");
|
||||
}
|
||||
|
||||
// Se till att engelska alltid finns
|
||||
if (!languages.Contains("eng"))
|
||||
{
|
||||
languages.Add("eng");
|
||||
}
|
||||
|
||||
GD.Print($"Available languages: {string.Join(", ", languages)}");
|
||||
return languages;
|
||||
}
|
||||
|
||||
private Dictionary<string, string> LoadLanguageFile(string languageCode)
|
||||
{
|
||||
var translations = new Dictionary<string, string>();
|
||||
string filePath = $"res://resources/languages/{languageCode}.json";
|
||||
|
||||
try
|
||||
{
|
||||
if (Godot.FileAccess.FileExists(filePath))
|
||||
{
|
||||
var file = Godot.FileAccess.Open(filePath, Godot.FileAccess.ModeFlags.Read);
|
||||
if (file != null)
|
||||
{
|
||||
string jsonString = file.GetAsText();
|
||||
file.Close();
|
||||
|
||||
var loadedTranslations = JsonSerializer.Deserialize<Dictionary<string, string>>(jsonString);
|
||||
if (loadedTranslations != null)
|
||||
{
|
||||
translations = loadedTranslations;
|
||||
GD.Print($"Loaded {translations.Count} translations for language: {languageCode}");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
GD.Print($"Language file not found: {filePath}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
GD.PrintErr($"Failed to load language file {filePath}: {ex.Message}");
|
||||
}
|
||||
|
||||
return translations;
|
||||
}
|
||||
|
||||
public void SetLanguage(string languageCode)
|
||||
{
|
||||
if (_currentLanguage == languageCode)
|
||||
return;
|
||||
|
||||
_currentLanguage = languageCode;
|
||||
_currentTranslations = LoadLanguageFile(languageCode);
|
||||
|
||||
GD.Print($"Language changed to: {languageCode}");
|
||||
|
||||
// Skicka signal för att uppdatera alla UI-element
|
||||
GetTree().CallGroup("localized_ui", "UpdateLocalization");
|
||||
}
|
||||
|
||||
public string GetText(string key)
|
||||
{
|
||||
// Försök hämta från nuvarande språk
|
||||
if (_currentTranslations != null && _currentTranslations.ContainsKey(key))
|
||||
{
|
||||
return _currentTranslations[key];
|
||||
}
|
||||
|
||||
// Fallback till engelska
|
||||
if (_fallbackTranslations != null && _fallbackTranslations.ContainsKey(key))
|
||||
{
|
||||
return _fallbackTranslations[key];
|
||||
}
|
||||
|
||||
// Om inte ens engelska finns, returnera nyckeln
|
||||
GD.PrintErr($"Missing translation for key: {key}");
|
||||
return key;
|
||||
}
|
||||
|
||||
public string GetCurrentLanguage()
|
||||
{
|
||||
return _currentLanguage;
|
||||
}
|
||||
|
||||
private SceneTree GetTree()
|
||||
{
|
||||
return Engine.GetMainLoop() as SceneTree;
|
||||
}
|
||||
}
|
||||
}
|
||||
1
scripts/Systems/Localization/LocalizationManager.cs.uid
Normal file
1
scripts/Systems/Localization/LocalizationManager.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dqkp0ni5te700
|
||||
57
scripts/Systems/Save/SaveInstance.cs
Normal file
57
scripts/Systems/Save/SaveInstance.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
|
||||
namespace TheGame
|
||||
{
|
||||
public class SaveInstance
|
||||
{
|
||||
public string InstanceId { get; set; }
|
||||
public float GameTime { get; set; }
|
||||
public DateTime SaveDate { get; set; }
|
||||
public string DisplayName { get; set; }
|
||||
public bool IsAutoSave { get; set; } = false;
|
||||
|
||||
public SaveInstance()
|
||||
{
|
||||
InstanceId = Guid.NewGuid().ToString("N")[..8]; // 8-tecken ID
|
||||
SaveDate = DateTime.Now;
|
||||
DisplayName = $"Save {SaveDate:HH:mm:ss}";
|
||||
}
|
||||
|
||||
public SaveInstance(float gameTime, string displayName = "", bool isAutoSave = false)
|
||||
{
|
||||
InstanceId = Guid.NewGuid().ToString("N")[..8];
|
||||
GameTime = gameTime;
|
||||
SaveDate = DateTime.Now;
|
||||
IsAutoSave = isAutoSave;
|
||||
|
||||
if (string.IsNullOrEmpty(displayName))
|
||||
{
|
||||
if (isAutoSave)
|
||||
{
|
||||
DisplayName = $"Auto-save {SaveDate:HH:mm:ss}";
|
||||
}
|
||||
else
|
||||
{
|
||||
DisplayName = $"Save {SaveDate:HH:mm:ss}";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DisplayName = displayName;
|
||||
}
|
||||
}
|
||||
|
||||
public string GetFormattedGameTime()
|
||||
{
|
||||
int hours = (int)(GameTime / 3600);
|
||||
int minutes = (int)((GameTime % 3600) / 60);
|
||||
int seconds = (int)(GameTime % 60);
|
||||
return $"{hours:D2}:{minutes:D2}:{seconds:D2}";
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{DisplayName} - {GetFormattedGameTime()} ({SaveDate:yyyy-MM-dd HH:mm:ss})";
|
||||
}
|
||||
}
|
||||
}
|
||||
1
scripts/Systems/Save/SaveInstance.cs.uid
Normal file
1
scripts/Systems/Save/SaveInstance.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cxycpmfalsahg
|
||||
290
scripts/Systems/Save/SaveManager.cs
Normal file
290
scripts/Systems/Save/SaveManager.cs
Normal file
@@ -0,0 +1,290 @@
|
||||
using Godot;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace TheGame
|
||||
{
|
||||
public class SaveData
|
||||
{
|
||||
public GameSeed GameSeed { get; set; }
|
||||
public List<SaveInstance> Instances { get; set; } = new List<SaveInstance>();
|
||||
|
||||
// Backward compatibility
|
||||
public float GameTime { get; set; }
|
||||
public DateTime SaveDate { get; set; }
|
||||
public string SaveName { get; set; }
|
||||
public string DisplayName { get; set; } = "";
|
||||
|
||||
public SaveData()
|
||||
{
|
||||
GameSeed = new GameSeed();
|
||||
}
|
||||
|
||||
public SaveData(GameSeed gameSeed)
|
||||
{
|
||||
GameSeed = gameSeed;
|
||||
}
|
||||
|
||||
public SaveInstance GetLatestInstance()
|
||||
{
|
||||
if (Instances.Count == 0) return null;
|
||||
return Instances.OrderByDescending(i => i.SaveDate).First();
|
||||
}
|
||||
|
||||
public SaveInstance GetInstanceWithHighestGameTime()
|
||||
{
|
||||
if (Instances.Count == 0) return null;
|
||||
return Instances.OrderByDescending(i => i.GameTime).First();
|
||||
}
|
||||
}
|
||||
|
||||
public class SaveManager
|
||||
{
|
||||
private readonly string _saveDirectory;
|
||||
|
||||
public SaveManager()
|
||||
{
|
||||
// Använd user data directory istället för executable directory
|
||||
string userDataDir = OS.GetUserDataDir();
|
||||
_saveDirectory = Path.Combine(userDataDir, "saves");
|
||||
|
||||
// Skapa saves mapp om den inte finns
|
||||
if (!Directory.Exists(_saveDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(_saveDirectory);
|
||||
GD.Print($"Created saves directory: {_saveDirectory}");
|
||||
}
|
||||
else
|
||||
{
|
||||
GD.Print($"Using saves directory: {_saveDirectory}");
|
||||
}
|
||||
}
|
||||
|
||||
public bool SaveGame(float gameTime, string displayName = "", GameSeed gameSeed = null, bool overwriteLatest = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Om ingen seed angiven, använd nuvarande eller skapa ny
|
||||
if (gameSeed == null)
|
||||
{
|
||||
gameSeed = GameState.CurrentGameSeed ?? new GameSeed();
|
||||
GameState.CurrentGameSeed = gameSeed;
|
||||
}
|
||||
|
||||
// Sök efter befintlig SaveData för denna seed
|
||||
var existingSaveData = FindSaveDataBySeed(gameSeed);
|
||||
|
||||
if (existingSaveData == null)
|
||||
{
|
||||
// Skapa ny SaveData för denna seed
|
||||
existingSaveData = new SaveData(gameSeed);
|
||||
}
|
||||
|
||||
// Skapa ny instans
|
||||
var newInstance = new SaveInstance(gameTime, displayName);
|
||||
|
||||
if (overwriteLatest && existingSaveData.Instances.Count > 0)
|
||||
{
|
||||
// Skriv över den senaste instansen
|
||||
var latestInstance = existingSaveData.GetLatestInstance();
|
||||
existingSaveData.Instances.Remove(latestInstance);
|
||||
}
|
||||
|
||||
existingSaveData.Instances.Add(newInstance);
|
||||
|
||||
// Spara till fil
|
||||
bool saveSuccess = SaveSeedToFile(existingSaveData);
|
||||
|
||||
if (saveSuccess)
|
||||
{
|
||||
GD.Print($"Game saved - Seed: {gameSeed.SeedValue}, Instance: {newInstance.DisplayName}");
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
GD.PrintErr($"Failed to save game - Seed: {gameSeed.SeedValue}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
GD.PrintErr($"Error saving game: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public SaveData FindSaveDataBySeed(GameSeed gameSeed)
|
||||
{
|
||||
string fileName = $"seed_{gameSeed.SeedValue}.json";
|
||||
string filePath = Path.Combine(_saveDirectory, fileName);
|
||||
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
string jsonString = File.ReadAllText(filePath);
|
||||
var saveData = JsonSerializer.Deserialize<SaveData>(jsonString);
|
||||
return saveData;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
GD.PrintErr($"Failed to load seed file {filePath}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool SaveSeedToFile(SaveData saveData)
|
||||
{
|
||||
string fileName = $"seed_{saveData.GameSeed.SeedValue}.json";
|
||||
string filePath = Path.Combine(_saveDirectory, fileName);
|
||||
|
||||
try
|
||||
{
|
||||
string jsonString = JsonSerializer.Serialize(saveData, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
});
|
||||
File.WriteAllText(filePath, jsonString);
|
||||
GD.Print($"Seed saved to: {filePath}");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
GD.PrintErr($"Failed to save seed file: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public List<SaveData> GetSavedGames()
|
||||
{
|
||||
var savedGames = new List<SaveData>();
|
||||
|
||||
GD.Print($"Looking for saves in: {_saveDirectory}");
|
||||
|
||||
if (!Directory.Exists(_saveDirectory))
|
||||
{
|
||||
GD.Print("Save directory does not exist");
|
||||
return savedGames;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Ladda både nya seed-filer och gamla format för backward compatibility
|
||||
var allFiles = Directory.GetFiles(_saveDirectory, "*.json");
|
||||
GD.Print($"Found {allFiles.Length} save files");
|
||||
|
||||
foreach (string filePath in allFiles)
|
||||
{
|
||||
GD.Print($"Processing save file: {filePath}");
|
||||
try
|
||||
{
|
||||
string jsonString = File.ReadAllText(filePath);
|
||||
|
||||
if (Path.GetFileName(filePath).StartsWith("seed_"))
|
||||
{
|
||||
// Ny seed-baserad fil
|
||||
var saveData = JsonSerializer.Deserialize<SaveData>(jsonString);
|
||||
if (saveData != null && saveData.Instances.Count > 0)
|
||||
{
|
||||
savedGames.Add(saveData);
|
||||
GD.Print($"Successfully loaded seed: {saveData.GameSeed.SeedValue} with {saveData.Instances.Count} instances");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Gammal fil - konvertera till nytt format
|
||||
var oldSaveData = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonString);
|
||||
if (oldSaveData != null)
|
||||
{
|
||||
var convertedSave = ConvertOldSaveFormat(oldSaveData);
|
||||
if (convertedSave != null)
|
||||
{
|
||||
savedGames.Add(convertedSave);
|
||||
GD.Print($"Converted old save format");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
GD.PrintErr($"Failed to load save file {filePath}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// Sortera efter senaste aktivitet
|
||||
savedGames = savedGames.OrderByDescending(s => s.GetLatestInstance()?.SaveDate ?? DateTime.MinValue).ToList();
|
||||
GD.Print($"Total loaded saves: {savedGames.Count}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
GD.PrintErr($"Failed to read saves directory: {ex.Message}");
|
||||
}
|
||||
|
||||
return savedGames;
|
||||
}
|
||||
|
||||
private SaveData ConvertOldSaveFormat(Dictionary<string, object> oldData)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Extrahera värden från gammal format
|
||||
var gameTime = Convert.ToSingle(oldData.GetValueOrDefault("GameTime", 0f));
|
||||
var saveDateStr = oldData.GetValueOrDefault("SaveDate", "")?.ToString();
|
||||
var displayName = oldData.GetValueOrDefault("DisplayName", "")?.ToString();
|
||||
|
||||
if (DateTime.TryParse(saveDateStr, out DateTime saveDate))
|
||||
{
|
||||
var gameSeed = new GameSeed($"OLD_{DateTime.Now.Ticks % 100000000:X}", "Converted Save");
|
||||
var saveData = new SaveData(gameSeed);
|
||||
|
||||
var instance = new SaveInstance(gameTime, displayName ?? "Converted Save");
|
||||
instance.SaveDate = saveDate;
|
||||
saveData.Instances.Add(instance);
|
||||
|
||||
return saveData;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
GD.PrintErr($"Failed to convert old save format: {ex.Message}");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public bool LoadGame(SaveData saveData, SaveInstance instance = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Om ingen specifik instans angiven, ta den med högst speltid
|
||||
if (instance == null)
|
||||
{
|
||||
instance = saveData.GetInstanceWithHighestGameTime();
|
||||
}
|
||||
|
||||
if (instance == null)
|
||||
{
|
||||
GD.PrintErr("No valid instance found to load");
|
||||
return false;
|
||||
}
|
||||
|
||||
GameState.LoadedGameTime = instance.GameTime;
|
||||
GameState.CurrentGameSeed = saveData.GameSeed;
|
||||
GameState.LoadedInstance = instance;
|
||||
|
||||
GD.Print($"Loaded game - Seed: {saveData.GameSeed.SeedValue}, Instance: {instance.DisplayName}, Time: {instance.GetFormattedGameTime()}");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
GD.PrintErr($"Failed to load game: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
scripts/Systems/Save/SaveManager.cs.uid
Normal file
1
scripts/Systems/Save/SaveManager.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ci407hx25v6nb
|
||||
Reference in New Issue
Block a user