using System; using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Hosting; using Npgsql; using PhoenixLib.Logging; namespace Plugin.ResourceLoader.Services { public class DbFirstResourceHydratorService : IHostedService { private readonly ResourceLoadingConfiguration _configuration; public DbFirstResourceHydratorService(ResourceLoadingConfiguration configuration) { _configuration = configuration; } public Task StartAsync(CancellationToken cancellationToken) { bool dbFirst = string.Equals(Environment.GetEnvironmentVariable("DB_FIRST"), "true", StringComparison.OrdinalIgnoreCase) || string.Equals(Environment.GetEnvironmentVariable("RESOURCE_DB_FIRST"), "true", StringComparison.OrdinalIgnoreCase); if (!dbFirst) { return Task.CompletedTask; } bool strictDbOnly = string.Equals(Environment.GetEnvironmentVariable("STRICT_DB_ONLY"), "true", StringComparison.OrdinalIgnoreCase); try { string host = Environment.GetEnvironmentVariable("DATABASE_IP") ?? Environment.GetEnvironmentVariable("POSTGRES_DATABASE_IP") ?? "127.0.0.1"; string port = Environment.GetEnvironmentVariable("DATABASE_PORT") ?? Environment.GetEnvironmentVariable("POSTGRES_DATABASE_PORT") ?? "5432"; string db = Environment.GetEnvironmentVariable("DATABASE_NAME") ?? Environment.GetEnvironmentVariable("POSTGRES_DATABASE_NAME") ?? "game"; string user = Environment.GetEnvironmentVariable("DATABASE_USER") ?? Environment.GetEnvironmentVariable("POSTGRES_DATABASE_USER") ?? "postgres"; string pass = Environment.GetEnvironmentVariable("DATABASE_PASSWORD") ?? Environment.GetEnvironmentVariable("POSTGRES_DATABASE_PASSWORD") ?? "postgres"; using var conn = new NpgsqlConnection($"Host={host};Port={port};Database={db};Username={user};Password={pass}"); conn.Open(); var counters = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["dat"] = 0, ["lang"] = 0, ["maps"] = 0, ["config_scripts"] = 0 }; using var cmd = new NpgsqlCommand("SELECT category, relative_path, content FROM resource_files;", conn); using var reader = cmd.ExecuteReader(); while (reader.Read()) { string category = reader.GetString(0); string relativePath = reader.GetString(1).Replace('/', Path.DirectorySeparatorChar); byte[] content = (byte[])reader[2]; string? destination = category switch { "dat" => Path.Combine(_configuration.ResourcePaths, relativePath), "lang" => Path.Combine(_configuration.ResourcePaths, relativePath), "maps" => Path.Combine(_configuration.ResourcePaths, relativePath), "config_scripts" => Path.Combine(Directory.GetCurrentDirectory(), relativePath), _ => null }; if (destination == null) { continue; } string? dir = Path.GetDirectoryName(destination); if (!string.IsNullOrWhiteSpace(dir)) { Directory.CreateDirectory(dir); } File.WriteAllBytes(destination, content); if (counters.ContainsKey(category)) { counters[category]++; } } if (strictDbOnly && (counters["dat"] == 0 || counters["lang"] == 0 || counters["maps"] == 0)) { throw new InvalidOperationException("STRICT_DB_ONLY enabled but one or more required resource categories are missing in resource_files."); } Log.Info($"[DB_FIRST] Hydrated files from DB: dat={counters["dat"]} lang={counters["lang"]} maps={counters["maps"]} scripts={counters["config_scripts"]}"); } catch (Exception ex) { Log.Error("[DB_FIRST] Failed to hydrate resources from database", ex); if (strictDbOnly) { throw; } } return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; } }