using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Hosting; using Npgsql; using PhoenixLib.Logging; using WingsAPI.Data.GameData; using WingsEmu.DTOs.BCards; using WingsEmu.DTOs.Skills; namespace Plugin.ResourceLoader.Services { public class ResourceFilesPostgresSyncService : IHostedService { private readonly ResourceLoadingConfiguration _configuration; private readonly IResourceLoader _skillLoader; public ResourceFilesPostgresSyncService(ResourceLoadingConfiguration configuration, IResourceLoader skillLoader) { _configuration = configuration; _skillLoader = skillLoader; } public Task StartAsync(CancellationToken cancellationToken) { if (string.Equals(Environment.GetEnvironmentVariable("PARSER_DB_SYNC"), "false", StringComparison.OrdinalIgnoreCase)) { return Task.CompletedTask; } 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"; string root = _configuration.ResourcePaths; string datPath = _configuration.GameDataPath; string langPath = _configuration.GameLanguagePath; string mapsPath = _configuration.GameMapsPath; string configScriptsPath = Path.Combine(Directory.GetCurrentDirectory(), "config", "scripts"); string[] datFiles = Directory.Exists(datPath) ? Directory.GetFiles(datPath, "*", SearchOption.AllDirectories) : Array.Empty(); string[] langFiles = Directory.Exists(langPath) ? Directory.GetFiles(langPath, "*", SearchOption.AllDirectories) : Array.Empty(); string[] mapFiles = Directory.Exists(mapsPath) ? Directory.GetFiles(mapsPath, "*", SearchOption.AllDirectories) : Array.Empty(); string[] scriptFiles = Directory.Exists(configScriptsPath) ? Directory.GetFiles(configScriptsPath, "*", SearchOption.AllDirectories) : Array.Empty(); using var conn = new NpgsqlConnection($"Host={host};Port={port};Database={db};Username={user};Password={pass}"); conn.Open(); using var tx = conn.BeginTransaction(); using (var cmd = new NpgsqlCommand(@"CREATE TABLE IF NOT EXISTS resource_files ( id BIGSERIAL PRIMARY KEY, category TEXT NOT NULL, relative_path TEXT NOT NULL, sha256 TEXT NOT NULL, content BYTEA NOT NULL, size_bytes INT NOT NULL, synced_at TIMESTAMP NOT NULL DEFAULT NOW(), UNIQUE(category, relative_path) ); CREATE TABLE IF NOT EXISTS skills ( id INT PRIMARY KEY, name TEXT, class INT, cast_id INT, cast_time INT, cooldown INT, mp_cost INT, cp_cost INT, skill_type INT, attack_type INT, hit_type INT, target_type INT, range INT, aoe_range INT, level_minimum INT, element INT, data_json JSONB, synced_at TIMESTAMP NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS skill_bcards ( id BIGSERIAL PRIMARY KEY, skill_id INT NOT NULL, bcard_type INT, bcard_subtype INT, first_data INT, second_data INT, cast_type INT ); CREATE TABLE IF NOT EXISTS skill_combos ( id BIGSERIAL PRIMARY KEY, skill_id INT NOT NULL, hit INT, animation INT, effect INT );", conn, tx)) { cmd.ExecuteNonQuery(); } UpsertFiles(conn, tx, root, "dat", datFiles); UpsertFiles(conn, tx, root, "lang", langFiles); UpsertFiles(conn, tx, root, "maps", mapFiles); UpsertFiles(conn, tx, Directory.GetCurrentDirectory(), "config_scripts", scriptFiles); SyncSkills(conn, tx, _skillLoader.LoadAsync().GetAwaiter().GetResult()); tx.Commit(); Log.Info($"[PARSER_DB_SYNC] Synced resource_files dat={datFiles.Length} lang={langFiles.Length} maps={mapFiles.Length} scripts={scriptFiles.Length} skills=ok"); } catch (Exception ex) { Log.Error("[PARSER_DB_SYNC] Failed to sync resource files", ex); } return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; private static void UpsertFiles(NpgsqlConnection conn, NpgsqlTransaction tx, string root, string category, string[] files) { foreach (string file in files.Where(File.Exists)) { byte[] content = File.ReadAllBytes(file); string hash; using (SHA256 sha = SHA256.Create()) { hash = Convert.ToHexString(sha.ComputeHash(content)); } string relative = Path.GetRelativePath(root, file).Replace('\\', '/'); using var cmd = new NpgsqlCommand(@"INSERT INTO resource_files(category,relative_path,sha256,content,size_bytes,synced_at) VALUES (@category,@path,@sha,@content,@size,NOW()) ON CONFLICT (category, relative_path) DO UPDATE SET sha256=EXCLUDED.sha256, content=EXCLUDED.content, size_bytes=EXCLUDED.size_bytes, synced_at=NOW();", conn, tx); cmd.Parameters.AddWithValue("category", category); cmd.Parameters.AddWithValue("path", relative); cmd.Parameters.AddWithValue("sha", hash); cmd.Parameters.AddWithValue("content", content); cmd.Parameters.AddWithValue("size", content.Length); cmd.ExecuteNonQuery(); } } private static void SyncSkills(NpgsqlConnection conn, NpgsqlTransaction tx, IReadOnlyList skills) { using (var cmd = new NpgsqlCommand("TRUNCATE TABLE skills, skill_bcards, skill_combos RESTART IDENTITY;", conn, tx)) { cmd.ExecuteNonQuery(); } foreach (SkillDTO s in skills) { using (var cmd = new NpgsqlCommand(@"INSERT INTO skills(id,name,class,cast_id,cast_time,cooldown,mp_cost,cp_cost,skill_type,attack_type,hit_type,target_type,range,aoe_range,level_minimum,element,data_json,synced_at) VALUES (@id,@name,@class,@castId,@castTime,@cooldown,@mp,@cp,@stype,@atype,@htype,@ttype,@range,@aoe,@lvl,@element,@json::jsonb,NOW());", conn, tx)) { cmd.Parameters.AddWithValue("id", s.Id); cmd.Parameters.AddWithValue("name", (object?)s.Name ?? DBNull.Value); cmd.Parameters.AddWithValue("class", s.Class); cmd.Parameters.AddWithValue("castId", s.CastId); cmd.Parameters.AddWithValue("castTime", s.CastTime); cmd.Parameters.AddWithValue("cooldown", s.Cooldown); cmd.Parameters.AddWithValue("mp", s.MpCost); cmd.Parameters.AddWithValue("cp", s.CPCost); cmd.Parameters.AddWithValue("stype", (int)s.SkillType); cmd.Parameters.AddWithValue("atype", (int)s.AttackType); cmd.Parameters.AddWithValue("htype", (int)s.HitType); cmd.Parameters.AddWithValue("ttype", (int)s.TargetType); cmd.Parameters.AddWithValue("range", s.Range); cmd.Parameters.AddWithValue("aoe", s.AoERange); cmd.Parameters.AddWithValue("lvl", s.LevelMinimum); cmd.Parameters.AddWithValue("element", s.Element); cmd.Parameters.AddWithValue("json", System.Text.Json.JsonSerializer.Serialize(s)); cmd.ExecuteNonQuery(); } foreach (BCardDTO b in s.BCards ?? Enumerable.Empty()) { using var bcmd = new NpgsqlCommand("INSERT INTO skill_bcards(skill_id,bcard_type,bcard_subtype,first_data,second_data,cast_type) VALUES (@sid,@t,@st,@f,@sec,@c);", conn, tx); bcmd.Parameters.AddWithValue("sid", s.Id); bcmd.Parameters.AddWithValue("t", b.Type); bcmd.Parameters.AddWithValue("st", b.SubType); bcmd.Parameters.AddWithValue("f", b.FirstData); bcmd.Parameters.AddWithValue("sec", b.SecondData); bcmd.Parameters.AddWithValue("c", b.CastType); bcmd.ExecuteNonQuery(); } foreach (ComboDTO c in s.Combos ?? Enumerable.Empty()) { using var ccmd = new NpgsqlCommand("INSERT INTO skill_combos(skill_id,hit,animation,effect) VALUES (@sid,@h,@a,@e);", conn, tx); ccmd.Parameters.AddWithValue("sid", s.Id); ccmd.Parameters.AddWithValue("h", c.Hit); ccmd.Parameters.AddWithValue("a", c.Animation); ccmd.Parameters.AddWithValue("e", c.Effect); ccmd.ExecuteNonQuery(); } } } } }