
All checks were successful
Build / Build (push) Successful in 29s
This commit introduces binary serialization for the RAM database to improve performance and adds a mission end event handler to ensure data is saved with a backup when the mission ends. - Implemented binary serialization and deserialization for key-values, hash tables, and lists in `Utils.cs`. This replaces the previous text-based serialization, resulting in faster save and load times. - Moved data writing and reading logic from `RAMDb.cs` to `Utils.cs` for better code organization and reusability. - Modified `RAMDb.cs` to use the new binary serialization methods (`ExportToBinary` and `ImportFromBinary`). - Added a mission end event handler in `XEH_preInit_server.sqf` to trigger a forced save with backup when the mission ends. This ensures that data is not lost in case of unexpected server shutdowns. - Updated the extension binaries (`ArmaRAMDb_x64.dll` and `ArmaRAMDb_x64.so`) with the new serialization logic. - Added a static `db` property to the `Main` class to allow access to the `RAMDb` instance from anywhere in the extension. - Modified the `Save` and `Load` methods in `Main.cs` to use the new binary serialization methods. - Updated the `Dispose` method in `RAMDb.cs` to ensure a final save and backup is performed when the object is disposed.
256 lines
9.3 KiB
C#
256 lines
9.3 KiB
C#
using System.Collections.Concurrent;
|
|
|
|
#pragma warning disable IDE0130 // Namespace does not match folder structure
|
|
namespace ArmaRAMDb
|
|
#pragma warning restore IDE0130 // Namespace does not match folder structure
|
|
{
|
|
public class RAMDb(string ardbPath = null) : IDisposable
|
|
{
|
|
private readonly string _ardbPath = Path.Combine(Environment.CurrentDirectory, ardbPath ?? Main.DEFAULT_ARDB_PATH);
|
|
public static readonly ConcurrentDictionary<string, string> _keyValues = new();
|
|
public static readonly ConcurrentDictionary<string, ConcurrentDictionary<string, string>> _hashTables = new();
|
|
public static readonly ConcurrentDictionary<string, List<string>> _lists = new();
|
|
|
|
public static bool AutoBackupEnabled { get; set; } = false;
|
|
public static int BackupFrequencyMinutes { get; set; } = 60;
|
|
public static int MaxBackupsToKeep { get; set; } = 10;
|
|
private static Timer _backupTimer;
|
|
|
|
public void ImportFromBinary()
|
|
{
|
|
try
|
|
{
|
|
if (File.Exists(_ardbPath))
|
|
{
|
|
using var stream = new FileStream(_ardbPath, FileMode.Open);
|
|
using var reader = new BinaryReader(stream);
|
|
|
|
int version = reader.ReadInt32();
|
|
if (version != 1)
|
|
{
|
|
Main.Log($"Unsupported ARDB format version: {version}", "warning");
|
|
return;
|
|
}
|
|
|
|
_keyValues.Clear();
|
|
_hashTables.Clear();
|
|
_lists.Clear();
|
|
|
|
// Read KeyValues
|
|
int keyValueCount = reader.ReadInt32();
|
|
for (int i = 0; i < keyValueCount; i++)
|
|
{
|
|
string key = reader.ReadString();
|
|
string value = reader.ReadString();
|
|
_keyValues.TryAdd(key, value);
|
|
Main.Log($"Loaded key-value: {key} = {value[..Math.Min(50, value.Length)]}...", "debug");
|
|
}
|
|
|
|
// Read HashTables
|
|
int tableCount = reader.ReadInt32();
|
|
for (int i = 0; i < tableCount; i++)
|
|
{
|
|
string tableName = reader.ReadString();
|
|
Main.Log($"Loading table: {tableName}", "debug");
|
|
|
|
var concurrentDict = new ConcurrentDictionary<string, string>();
|
|
int entryCount = reader.ReadInt32();
|
|
|
|
for (int j = 0; j < entryCount; j++)
|
|
{
|
|
string key = reader.ReadString();
|
|
string value = reader.ReadString();
|
|
concurrentDict.TryAdd(key, value);
|
|
Main.Log($"Loaded entry: {key} = {value[..Math.Min(50, value.Length)]}...", "debug");
|
|
}
|
|
|
|
_hashTables.TryAdd(tableName, concurrentDict);
|
|
}
|
|
|
|
// Read Lists
|
|
int listCount = reader.ReadInt32();
|
|
for (int i = 0; i < listCount; i++)
|
|
{
|
|
string listName = reader.ReadString();
|
|
Main.Log($"Loading list: {listName}", "debug");
|
|
|
|
var items = new List<string>();
|
|
int itemCount = reader.ReadInt32();
|
|
|
|
for (int j = 0; j < itemCount; j++)
|
|
{
|
|
string value = reader.ReadString();
|
|
items.Add(value);
|
|
Main.Log($"Loaded item: {value[..Math.Min(50, value.Length)]}...", "debug");
|
|
}
|
|
|
|
_lists.TryAdd(listName, items);
|
|
}
|
|
|
|
Main.Log("ARDB import complete", "debug");
|
|
}
|
|
else
|
|
{
|
|
Main.Log($"ARDB file not found at path: {_ardbPath}. A new database will be created when data is saved.", "info");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Main.Log($"Error during ARDB import: {ex.Message}", "error");
|
|
}
|
|
}
|
|
|
|
public void ExportToBinary(bool createBackup = false)
|
|
{
|
|
try
|
|
{
|
|
Directory.CreateDirectory(Path.GetDirectoryName(_ardbPath));
|
|
|
|
using (var stream = new FileStream(_ardbPath, FileMode.Create))
|
|
using (var writer = new BinaryWriter(stream))
|
|
{
|
|
writer.Write(1);
|
|
|
|
Utils.WriteDataToBinaryWriter(writer);
|
|
}
|
|
|
|
if (createBackup)
|
|
{
|
|
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
|
string backupDirectory = Path.Combine(Path.GetDirectoryName(_ardbPath), "backups");
|
|
string backupFileName = Path.GetFileNameWithoutExtension(_ardbPath) + "_" + timestamp + Path.GetExtension(_ardbPath);
|
|
string backupPath = Path.Combine(backupDirectory, backupFileName);
|
|
|
|
Directory.CreateDirectory(backupDirectory);
|
|
|
|
using (var stream = new FileStream(backupPath, FileMode.Create))
|
|
using (var writer = new BinaryWriter(stream))
|
|
{
|
|
Utils.WriteDataToBinaryWriter(writer);
|
|
}
|
|
|
|
Main.Log($"Created backup at: {backupPath}", "debug");
|
|
}
|
|
|
|
Main.Log("ARDB export complete", "debug");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Main.Log($"Error during ARDB export: {ex.Message}", "error");
|
|
}
|
|
}
|
|
|
|
public List<string> ListBackups()
|
|
{
|
|
string backupDirectory = Path.Combine(Path.GetDirectoryName(_ardbPath), "backups");
|
|
List<string> backups = [];
|
|
|
|
if (Directory.Exists(backupDirectory))
|
|
{
|
|
backups = [.. Directory.GetFiles(backupDirectory, "*.ardb").OrderByDescending(file => file)];
|
|
}
|
|
|
|
return backups;
|
|
}
|
|
|
|
public static bool RestoreFromBackup(string backupPath)
|
|
{
|
|
if (File.Exists(backupPath))
|
|
{
|
|
try
|
|
{
|
|
using var stream = new FileStream(backupPath, FileMode.Open);
|
|
using var reader = new BinaryReader(stream);
|
|
|
|
int version = reader.ReadInt32();
|
|
if (version != 1)
|
|
{
|
|
Main.Log($"Unsupported ARDB format version in backup: {version}", "warning");
|
|
return false;
|
|
}
|
|
|
|
_keyValues.Clear();
|
|
_hashTables.Clear();
|
|
_lists.Clear();
|
|
|
|
Utils.ReadDataFromBinaryReader(reader);
|
|
|
|
Main.Log($"Restored from backup: {backupPath}", "info");
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Main.Log($"Failed to restore from backup: {ex.Message}", "error");
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public static void InitializeAutoBackup()
|
|
{
|
|
if (AutoBackupEnabled)
|
|
{
|
|
_backupTimer?.Dispose();
|
|
|
|
_backupTimer = new Timer(BackupTimerCallback, null,
|
|
TimeSpan.FromMinutes(BackupFrequencyMinutes),
|
|
TimeSpan.FromMinutes(BackupFrequencyMinutes));
|
|
|
|
Main.Log($"Automatic backup initialized (every {BackupFrequencyMinutes} minutes)", "info");
|
|
}
|
|
}
|
|
|
|
private static void BackupTimerCallback(object state)
|
|
{
|
|
try
|
|
{
|
|
Main.db.ExportToBinary(true);
|
|
|
|
ManageBackupRotation();
|
|
|
|
Main.Log($"Automatic backup created at {DateTime.Now}", "info");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Main.Log($"Automatic backup failed: {ex.Message}", "error");
|
|
}
|
|
}
|
|
|
|
private static void ManageBackupRotation()
|
|
{
|
|
try
|
|
{
|
|
var backups = Main.db.ListBackups();
|
|
|
|
if (backups.Count > MaxBackupsToKeep)
|
|
{
|
|
for (int i = MaxBackupsToKeep; i < backups.Count; i++)
|
|
{
|
|
File.Delete(backups[i]);
|
|
Main.Log($"Deleted old backup: {backups[i]}", "info");
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Main.Log($"Backup rotation failed: {ex.Message}", "error");
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
try
|
|
{
|
|
ExportToBinary(true);
|
|
_backupTimer?.Dispose();
|
|
Main.Log("RAMDb disposed with final save and backup", "action");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Main.Log($"Error during final save on disposal: {ex.Message}", "error");
|
|
}
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
}
|
|
} |