diff --git a/Refresh.GameServer/RefreshGameServer.cs b/Refresh.GameServer/RefreshGameServer.cs index ebb5abb0a..47ae11995 100644 --- a/Refresh.GameServer/RefreshGameServer.cs +++ b/Refresh.GameServer/RefreshGameServer.cs @@ -19,6 +19,7 @@ using Refresh.GameServer.Importing; using Refresh.GameServer.Middlewares; using Refresh.GameServer.Services; +using Refresh.GameServer.Storage; using Refresh.GameServer.Time; using Refresh.GameServer.Types.Levels.Categories; using Refresh.GameServer.Types.Roles; @@ -52,16 +53,20 @@ public RefreshGameServer( this._databaseProvider.Initialize(); this._dataStore = dataStore; + DryArchiveConfig dryConfig = Config.LoadFromJsonFile("dry.json", this.Logger); + if (dryConfig.Enabled) + this._dataStore = new AggregateDataStore(dataStore, new DryDataStore(dryConfig)); + this.SetupInitializer(() => { GameDatabaseProvider provider = databaseProvider.Invoke(); this.WorkerManager?.Stop(); - this.WorkerManager = new WorkerManager(this.Logger, this._dataStore!, provider); + this.WorkerManager = new WorkerManager(this.Logger, this._dataStore, provider); authProvider ??= new GameAuthenticationProvider(this._config!); - this.InjectBaseServices(provider, authProvider, dataStore); + this.InjectBaseServices(provider, authProvider, this._dataStore); }); } diff --git a/Refresh.GameServer/Storage/AggregateDataStore.cs b/Refresh.GameServer/Storage/AggregateDataStore.cs new file mode 100644 index 000000000..b3dc645af --- /dev/null +++ b/Refresh.GameServer/Storage/AggregateDataStore.cs @@ -0,0 +1,42 @@ +using System.Collections.Frozen; +using Bunkum.Core.Storage; + +namespace Refresh.GameServer.Storage; + +public class AggregateDataStore : IDataStore +{ + private IDataStore PrimaryStore { get; } + private FrozenSet AllStores { get; } + + public AggregateDataStore(IDataStore primary, params IDataStore[] others) + { + this.PrimaryStore = primary; + + List stores = new(others.Length + 1) { primary }; + stores.AddRange(others); + + this.AllStores = stores.ToFrozenSet(); + } + + private IDataStore GetStoreContainingKey(string key) + { + IDataStore? store = this.AllStores.FirstOrDefault(s => s.ExistsInStore(key)); + + if (store == null) + throw new InvalidOperationException($"No data stores contained the key '{key}'."); + + return store; + } + + public bool ExistsInStore(string key) => this.AllStores.Any(s => s.ExistsInStore(key)); + public byte[] GetDataFromStore(string key) => this.GetStoreContainingKey(key).GetDataFromStore(key); + public Stream GetStreamFromStore(string key) => this.GetStoreContainingKey(key).GetStreamFromStore(key); + + public string[] GetKeysFromStore() => this.AllStores.SelectMany(r => r.GetKeysFromStore()).ToArray(); + + public bool WriteToStore(string key, byte[] data) => this.PrimaryStore.WriteToStore(key, data); + public bool RemoveFromStore(string key) => this.PrimaryStore.RemoveFromStore(key); + + public Stream OpenWriteStream(string key) => this.PrimaryStore.OpenWriteStream(key); + public bool WriteToStoreFromStream(string key, Stream data) => this.PrimaryStore.WriteToStoreFromStream(key, data); +} \ No newline at end of file diff --git a/Refresh.GameServer/Storage/DryArchiveConfig.cs b/Refresh.GameServer/Storage/DryArchiveConfig.cs new file mode 100644 index 000000000..690d2c760 --- /dev/null +++ b/Refresh.GameServer/Storage/DryArchiveConfig.cs @@ -0,0 +1,18 @@ +using Bunkum.Core.Configuration; + +namespace Refresh.GameServer.Storage; + +public class DryArchiveConfig : Config +{ + public override int CurrentConfigVersion => 1; + public override int Version { get; set; } + + protected override void Migrate(int oldVer, dynamic oldConfig) + { + + } + + public bool Enabled { get; set; } + public string Location { get; set; } = "/var/dry/"; + public bool UseFolderNames { get; set; } = true; +} \ No newline at end of file diff --git a/Refresh.GameServer/Storage/DryDataStore.cs b/Refresh.GameServer/Storage/DryDataStore.cs new file mode 100644 index 000000000..885c29b4e --- /dev/null +++ b/Refresh.GameServer/Storage/DryDataStore.cs @@ -0,0 +1,99 @@ +using System.Text; +using Bunkum.Core.Storage; +using JetBrains.Annotations; +using Refresh.GameServer.Verification; + +namespace Refresh.GameServer.Storage; + +public class DryDataStore : IDataStore +{ + private readonly DryArchiveConfig _config; + + private const string WriteError = $"{nameof(DryDataStore)} is an archive, and cannot be written to."; + + public DryDataStore(DryArchiveConfig config) + { + this._config = config; + } + + [Pure] + private string? GetPath(ReadOnlySpan hash) + { + if (!CommonPatterns.Sha1Regex().IsMatch(hash)) + return null; + + StringBuilder builder = new(); + + // /var/dry + builder.Append(this._config.Location); + + // /var/dry/ + if (!this._config.Location.EndsWith('/')) + builder.Append('/'); + + // /var/dry/dry23r0/ + if (this._config.UseFolderNames) + { + builder.Append("dry23r"); + builder.Append(hash[0]); + builder.Append('/'); + } + + // /var/dry/dry23r0/01/ + builder.Append(hash.Slice(0, 2)); + builder.Append('/'); + + // /var/dry/dry23r0/01/02/ + builder.Append(hash.Slice(2, 2)); + builder.Append('/'); + + // /var/dry/dry2er0/01/02/010220123644c8e53d4054bf0d30d0e2bd0786ff8 + builder.Append(hash); + + return builder.ToString(); + } + + public bool ExistsInStore(string key) => File.Exists(this.GetPath(key)); + + public bool WriteToStore(string key, byte[] data) + { + throw new InvalidOperationException(WriteError); + } + + public byte[] GetDataFromStore(string key) + { + string? path = this.GetPath(key); + + if (path == null) + throw new FormatException("The key was invalid."); + + return File.ReadAllBytes(path); + } + + public bool RemoveFromStore(string key) + { + throw new InvalidOperationException(WriteError); + } + + public string[] GetKeysFromStore() => []; // TODO: Implement when we want to store these as GameAssets + + public bool WriteToStoreFromStream(string key, Stream data) + { + throw new InvalidOperationException(WriteError); + } + + public Stream GetStreamFromStore(string key) + { + string? path = this.GetPath(key); + + if (path == null) + throw new FormatException("The key was invalid."); + + return File.OpenRead(path); + } + + public Stream OpenWriteStream(string key) + { + throw new InvalidOperationException(WriteError); + } +} \ No newline at end of file