diff --git a/srcs/_plugins/Plugin.ResourceLoader/FileResourceLoaderPlugin.cs b/srcs/_plugins/Plugin.ResourceLoader/FileResourceLoaderPlugin.cs index 2fa577f..4f00f55 100644 --- a/srcs/_plugins/Plugin.ResourceLoader/FileResourceLoaderPlugin.cs +++ b/srcs/_plugins/Plugin.ResourceLoader/FileResourceLoaderPlugin.cs @@ -5,6 +5,7 @@ using System; using Microsoft.Extensions.DependencyInjection; using Plugin.ResourceLoader.Loaders; +using Plugin.ResourceLoader.Services; using WingsAPI.Data.ActDesc; using WingsAPI.Data.GameData; using WingsAPI.Plugins; @@ -40,6 +41,7 @@ namespace Plugin.ResourceLoader services.AddSingleton(); services.AddSingleton(); + services.AddHostedService(); } } @@ -51,6 +53,7 @@ namespace Plugin.ResourceLoader { services.AddSingleton, GenericTranslationGrpcLoader>(); services.AddSingleton(); + services.AddHostedService(); } } } \ No newline at end of file diff --git a/srcs/_plugins/Plugin.ResourceLoader/Plugin.ResourceLoader.csproj b/srcs/_plugins/Plugin.ResourceLoader/Plugin.ResourceLoader.csproj index f3fa26a..191187e 100644 --- a/srcs/_plugins/Plugin.ResourceLoader/Plugin.ResourceLoader.csproj +++ b/srcs/_plugins/Plugin.ResourceLoader/Plugin.ResourceLoader.csproj @@ -5,6 +5,11 @@ + + + + + diff --git a/srcs/_plugins/Plugin.ResourceLoader/Services/ResourceFilesPostgresSyncService.cs b/srcs/_plugins/Plugin.ResourceLoader/Services/ResourceFilesPostgresSyncService.cs new file mode 100644 index 0000000..3ead7f8 --- /dev/null +++ b/srcs/_plugins/Plugin.ResourceLoader/Services/ResourceFilesPostgresSyncService.cs @@ -0,0 +1,118 @@ +using System; +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; + +namespace Plugin.ResourceLoader.Services +{ + public class ResourceFilesPostgresSyncService : IHostedService + { + private readonly ResourceLoadingConfiguration _configuration; + + public ResourceFilesPostgresSyncService(ResourceLoadingConfiguration configuration) + { + _configuration = configuration; + } + + 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[] datFiles = Directory.Exists(datPath) + ? Directory.GetFiles(datPath, "*", SearchOption.AllDirectories) + : Array.Empty(); + string[] langFiles = Directory.Exists(langPath) + ? Directory.GetFiles(langPath, "*", 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) +);", conn, tx)) + { + cmd.ExecuteNonQuery(); + } + + UpsertFiles(conn, tx, root, "dat", datFiles); + UpsertFiles(conn, tx, root, "lang", langFiles); + + tx.Commit(); + Log.Info($"[PARSER_DB_SYNC] Synced resource_files dat={datFiles.Length} lang={langFiles.Length}"); + } + 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(); + } + } + } +} diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Miniland/MinigameManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Miniland/MinigameManager.cs index 11be13d..9513113 100644 --- a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Miniland/MinigameManager.cs +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Miniland/MinigameManager.cs @@ -7,6 +7,7 @@ using WingsEmu.Game.Characters.Events; using WingsEmu.Game.Configurations.Miniland; using WingsEmu.Game.Miniland; using WingsEmu.Game.Networking; +using WingsEmu.Plugins.BasicImplementations.ServerConfigs.Persistence; namespace WingsEmu.Plugins.BasicImplementations.Managers; @@ -20,11 +21,14 @@ public class MinigameManager : IMinigameManager { _minigameConfiguration = minigameConfiguration; _lockService = lockService; + InitializePersistence(); } public async Task CanRefreshMinigamesFreeProductionPoints(long characterId) => await _lockService.TryAddTemporaryLockAsync($"game:locks:minigame-refresh:{characterId}", DateTime.UtcNow.Date.AddDays(1)); + public void InitializePersistence() => ParserDataPostgresSync.SyncMinigames(_minigameConfiguration); + public MinigameScoresHolder GetScores(int minigameVnum) { diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/Persistence/ParserDataPostgresSync.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/Persistence/ParserDataPostgresSync.cs index 984e2ba..72c9469 100644 --- a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/Persistence/ParserDataPostgresSync.cs +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/Persistence/ParserDataPostgresSync.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Text.Json; using Npgsql; using PhoenixLib.Logging; using WingsAPI.Data.Drops; @@ -8,6 +9,7 @@ using WingsEmu.DTOs.Maps; using WingsEmu.DTOs.Recipes; using WingsEmu.DTOs.ServerDatas; using WingsEmu.DTOs.Shops; +using WingsEmu.Game.Configurations.Miniland; namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs.Persistence; @@ -492,6 +494,77 @@ VALUES (@id,@amount,@chance,@item,@map,@mon,@race,@subrace);", conn, tx); } } + public static void SyncMinigames(MinigameConfiguration minigameConfiguration) + { + if (!Enabled || minigameConfiguration == null) return; + try + { + using var conn = new NpgsqlConnection(BuildConnectionString()); + conn.Open(); + using var tx = conn.BeginTransaction(); + + using (var cmd = new NpgsqlCommand(@"CREATE TABLE IF NOT EXISTS minigame_config ( + id BIGSERIAL PRIMARY KEY, + minigame_vnum INT, + minigame_type INT, + minimum_level INT, + minimum_reputation INT, + rewards_json JSONB +); +CREATE TABLE IF NOT EXISTS minigame_scores_holders ( + id BIGSERIAL PRIMARY KEY, + minigame_type INT, + scores_json JSONB +); +CREATE TABLE IF NOT EXISTS global_minigame_config ( + id BIGSERIAL PRIMARY KEY, + config_json JSONB +);", conn, tx)) cmd.ExecuteNonQuery(); + + using (var cmd = new NpgsqlCommand("TRUNCATE TABLE minigame_config, minigame_scores_holders, global_minigame_config RESTART IDENTITY;", conn, tx)) cmd.ExecuteNonQuery(); + + if (minigameConfiguration.Minigames != null) + { + foreach (Minigame m in minigameConfiguration.Minigames) + { + using var cmd = new NpgsqlCommand(@"INSERT INTO minigame_config(minigame_vnum,minigame_type,minimum_level,minimum_reputation,rewards_json) +VALUES (@vnum,@type,@minLvl,@minRep,@rewards::jsonb);", conn, tx); + cmd.Parameters.AddWithValue("vnum", m.Vnum); + cmd.Parameters.AddWithValue("type", (int)m.Type); + cmd.Parameters.AddWithValue("minLvl", m.MinimumLevel); + cmd.Parameters.AddWithValue("minRep", m.MinimumReputation); + cmd.Parameters.AddWithValue("rewards", JsonSerializer.Serialize(m.Rewards)); + cmd.ExecuteNonQuery(); + } + } + + if (minigameConfiguration.ScoresHolders != null) + { + foreach (MinigameScoresHolder s in minigameConfiguration.ScoresHolders) + { + using var cmd = new NpgsqlCommand(@"INSERT INTO minigame_scores_holders(minigame_type,scores_json) +VALUES (@type,@scores::jsonb);", conn, tx); + cmd.Parameters.AddWithValue("type", (int)s.Type); + cmd.Parameters.AddWithValue("scores", JsonSerializer.Serialize(s.Scores)); + cmd.ExecuteNonQuery(); + } + } + + using (var cmd = new NpgsqlCommand("INSERT INTO global_minigame_config(config_json) VALUES (@cfg::jsonb);", conn, tx)) + { + cmd.Parameters.AddWithValue("cfg", JsonSerializer.Serialize(minigameConfiguration.Configuration)); + cmd.ExecuteNonQuery(); + } + + tx.Commit(); + Log.Info($"[PARSER_DB_SYNC] Synced minigames={minigameConfiguration.Minigames?.Count ?? 0} holders={minigameConfiguration.ScoresHolders?.Count ?? 0}"); + } + catch (Exception ex) + { + Log.Error("[PARSER_DB_SYNC] Failed to sync minigames", ex); + } + } + public static void SyncRecipes(IReadOnlyList recipes) { if (!Enabled) return; diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/GameEventsPluginCore.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/GameEventsPluginCore.cs index d36a440..c8367cc 100644 --- a/srcs/_plugins/WingsEmu.Plugins.GameEvents/GameEventsPluginCore.cs +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/GameEventsPluginCore.cs @@ -20,6 +20,7 @@ using WingsEmu.Plugins.GameEvents.Configuration.InstantBattle; using WingsEmu.Plugins.GameEvents.Consumers; using WingsEmu.Plugins.GameEvents.Matchmaking.Matchmaker; using WingsEmu.Plugins.GameEvents.RecurrentJob; +using WingsEmu.Plugins.GameEvents.Services; namespace WingsEmu.Plugins.GameEvents { @@ -59,6 +60,8 @@ namespace WingsEmu.Plugins.GameEvents { [GameEventType.InstantBattle] = new InstantBattleMatchmaker(s.GetService()) })); + + services.AddHostedService(); } } } \ No newline at end of file diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/Services/GameEventConfigPostgresSyncService.cs b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Services/GameEventConfigPostgresSyncService.cs new file mode 100644 index 0000000..49a62c3 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/Services/GameEventConfigPostgresSyncService.cs @@ -0,0 +1,79 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Npgsql; +using PhoenixLib.Logging; +using WingsEmu.Plugins.GameEvents.Configuration.InstantBattle; + +namespace WingsEmu.Plugins.GameEvents.Services +{ + public class GameEventConfigPostgresSyncService : IHostedService + { + private readonly IGlobalInstantBattleConfiguration _configuration; + + public GameEventConfigPostgresSyncService(IGlobalInstantBattleConfiguration configuration) + { + _configuration = configuration; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + 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(); + using var tx = conn.BeginTransaction(); + + using (var cmd = new NpgsqlCommand(@"CREATE TABLE IF NOT EXISTS gameevent_instant_battle_configs ( + id BIGSERIAL PRIMARY KEY, + map_id INT, + game_event_type INT, + config_json JSONB +);", conn, tx)) cmd.ExecuteNonQuery(); + + using (var cmd = new NpgsqlCommand("TRUNCATE TABLE gameevent_instant_battle_configs RESTART IDENTITY;", conn, tx)) cmd.ExecuteNonQuery(); + + var configs = (_configuration as GlobalInstantBattleConfiguration)?.Configurations ?? Enumerable.Empty(); + + foreach (InstantBattleConfiguration c in configs) + { + using var cmd = new NpgsqlCommand("INSERT INTO gameevent_instant_battle_configs(map_id,game_event_type,config_json) VALUES (@map,@type,@cfg::jsonb);", conn, tx); + cmd.Parameters.AddWithValue("map", c.MapId); + cmd.Parameters.AddWithValue("type", (int)c.GameEventType); + cmd.Parameters.AddWithValue("cfg", JsonSerializer.Serialize(c)); + cmd.ExecuteNonQuery(); + } + + tx.Commit(); + Log.Info($"[PARSER_DB_SYNC] Synced instant_battle_configs={configs.Count()}"); + } + catch (Exception ex) + { + Log.Error("[PARSER_DB_SYNC] Failed to sync gameevents", ex); + } + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } +} diff --git a/srcs/_plugins/WingsEmu.Plugins.GameEvents/WingsEmu.Plugins.GameEvents.csproj b/srcs/_plugins/WingsEmu.Plugins.GameEvents/WingsEmu.Plugins.GameEvents.csproj index 1784246..6893305 100644 --- a/srcs/_plugins/WingsEmu.Plugins.GameEvents/WingsEmu.Plugins.GameEvents.csproj +++ b/srcs/_plugins/WingsEmu.Plugins.GameEvents/WingsEmu.Plugins.GameEvents.csproj @@ -8,6 +8,7 @@ +