From 18d24f3cbe98498e5b37e0ee9633233bbaa4c030 Mon Sep 17 00:00:00 2001 From: nizar Date: Tue, 24 Feb 2026 13:02:27 +0100 Subject: [PATCH] Add DB-first runtime loading and strict PostgreSQL-backed config/resources paths --- docker-compose.yml | 4 +- .../Plugin.Act4/DungeonScriptManager.cs | 63 +++ srcs/_plugins/Plugin.Act4/Plugin.Act4.csproj | 1 + .../_plugins/Plugin.Raids/Plugin.Raids.csproj | 1 + .../Scripting/RaidScriptManager.cs | 63 +++ .../FileResourceLoaderPlugin.cs | 2 +- .../Loaders/CardResourceFileLoader.cs | 51 +++ .../Loaders/GameDataLanguageFileLoader.cs | 62 ++- .../Loaders/ItemResourceFileLoader.cs | 48 +++ .../Loaders/MapResourceFileLoader.cs | 87 ++++ .../Loaders/NpcMonsterFileLoader.cs | 51 +++ .../Loaders/NpcQuestResourceFileLoader.cs | 54 ++- .../Loaders/QuestResourceFileLoader.cs | 52 +++ .../Loaders/SkillResourceFileLoader.cs | 80 ++++ .../Loaders/TutorialResourceFileLoader.cs | 51 +++ .../DbFirstResourceHydratorService.cs | 117 ++++++ .../ResourceFilesPostgresSyncService.cs | 114 +++++- .../LuaTimeSpaceScriptManager.cs | 63 +++ .../Plugin.TimeSpaces.csproj | 1 + .../ServerConfigs/DropManager.cs | 41 +- .../ServerConfigs/ItemBoxManager.cs | 67 ++- .../ServerConfigs/MapManager.cs | 38 +- .../ServerConfigs/MapMonsterManager.cs | 43 +- .../ServerConfigs/MapNpcManager.cs | 47 ++- .../Persistence/ParserDataPostgresReader.cs | 384 ++++++++++++++++++ .../Persistence/ParserDataPostgresSync.cs | 8 +- .../ServerConfigs/RecipeManager.cs | 105 +++-- .../ServerConfigs/ShopManager.cs | 149 ++++--- .../ServerConfigs/TeleporterManager.cs | 70 +++- 29 files changed, 1753 insertions(+), 164 deletions(-) create mode 100644 srcs/_plugins/Plugin.ResourceLoader/Services/DbFirstResourceHydratorService.cs create mode 100644 srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/Persistence/ParserDataPostgresReader.cs diff --git a/docker-compose.yml b/docker-compose.yml index b1c75d6..e3cf0e6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -192,9 +192,11 @@ services: WINGSEMU_MONGO_DB: wingsemu_logs WINGSEMU_MONGO_USERNAME: ${MONGO_ROOT_USERNAME} WINGSEMU_MONGO_PWD: ${MONGO_ROOT_PASSWORD} + DB_FIRST: "false" + STRICT_DB_ONLY: "false" command: ["/app/GameChannel.dll"] volumes: - - ./resources:/app/resources:ro + - ./resources:/app/resources - ./config:/app/config ports: - "8000:8000" diff --git a/srcs/_plugins/Plugin.Act4/DungeonScriptManager.cs b/srcs/_plugins/Plugin.Act4/DungeonScriptManager.cs index 3feb211..d9c50b8 100644 --- a/srcs/_plugins/Plugin.Act4/DungeonScriptManager.cs +++ b/srcs/_plugins/Plugin.Act4/DungeonScriptManager.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using FluentValidation.Results; using MoonSharp.Interpreter; +using Npgsql; using PhoenixLib.Logging; using Plugin.Act4.Scripting.Validator; using WingsAPI.Scripting; @@ -32,6 +33,9 @@ public class DungeonScriptManager : IDungeonScriptManager public void Load() { + EnsureDungeonScriptsDirectoryHydrated(); + Directory.CreateDirectory(_scriptFactoryConfiguration.DungeonsDirectory); + IEnumerable files = Directory.GetFiles(_scriptFactoryConfiguration.DungeonsDirectory, "*.lua"); foreach (string file in files) { @@ -69,4 +73,63 @@ public class DungeonScriptManager : IDungeonScriptManager Log.Info($"Loaded {_cache.Count} dungeons from scripts"); } + + private void EnsureDungeonScriptsDirectoryHydrated() + { + bool dbFirst = string.Equals(Environment.GetEnvironmentVariable("DB_FIRST"), "true", StringComparison.OrdinalIgnoreCase) + || string.Equals(Environment.GetEnvironmentVariable("RESOURCE_DB_FIRST"), "true", StringComparison.OrdinalIgnoreCase); + if (!dbFirst) + { + return; + } + + if (Directory.Exists(_scriptFactoryConfiguration.DungeonsDirectory)) + { + return; + } + + 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 cmd = new NpgsqlCommand("SELECT relative_path, content FROM resource_files WHERE category='config_scripts' AND relative_path LIKE 'config/scripts/dungeons/%';", conn); + using var reader = cmd.ExecuteReader(); + int count = 0; + while (reader.Read()) + { + string relative = reader.GetString(0).Replace('/', Path.DirectorySeparatorChar); + byte[] bytes = (byte[])reader[1]; + string fullPath = Path.Combine(Directory.GetCurrentDirectory(), relative); + Directory.CreateDirectory(Path.GetDirectoryName(fullPath) ?? _scriptFactoryConfiguration.DungeonsDirectory); + File.WriteAllBytes(fullPath, bytes); + count++; + } + + if (count > 0) + { + Log.Info($"[DB_FIRST] Hydrated {count} dungeon scripts from resource_files"); + } + } + catch (Exception ex) + { + Log.Error("[DB_FIRST] Could not hydrate dungeon scripts from database", ex); + } + } } \ No newline at end of file diff --git a/srcs/_plugins/Plugin.Act4/Plugin.Act4.csproj b/srcs/_plugins/Plugin.Act4/Plugin.Act4.csproj index f8831c6..0d00791 100644 --- a/srcs/_plugins/Plugin.Act4/Plugin.Act4.csproj +++ b/srcs/_plugins/Plugin.Act4/Plugin.Act4.csproj @@ -7,6 +7,7 @@ + diff --git a/srcs/_plugins/Plugin.Raids/Plugin.Raids.csproj b/srcs/_plugins/Plugin.Raids/Plugin.Raids.csproj index ca1337e..036f26e 100644 --- a/srcs/_plugins/Plugin.Raids/Plugin.Raids.csproj +++ b/srcs/_plugins/Plugin.Raids/Plugin.Raids.csproj @@ -8,6 +8,7 @@ + diff --git a/srcs/_plugins/Plugin.Raids/Scripting/RaidScriptManager.cs b/srcs/_plugins/Plugin.Raids/Scripting/RaidScriptManager.cs index 02d0677..b6ec0f0 100644 --- a/srcs/_plugins/Plugin.Raids/Scripting/RaidScriptManager.cs +++ b/srcs/_plugins/Plugin.Raids/Scripting/RaidScriptManager.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using FluentValidation.Results; using MoonSharp.Interpreter; +using Npgsql; using PhoenixLib.Logging; using Plugin.Raids.Scripting.Validator.Raid; using WingsAPI.Scripting; @@ -32,6 +33,9 @@ public sealed class RaidScriptManager : IRaidScriptManager public void Load() { + EnsureRaidScriptsDirectoryHydrated(); + Directory.CreateDirectory(_scriptFactoryConfiguration.RaidsDirectory); + IEnumerable files = Directory.GetFiles(_scriptFactoryConfiguration.RaidsDirectory, "*.lua"); foreach (string file in files) { @@ -69,4 +73,63 @@ public sealed class RaidScriptManager : IRaidScriptManager Log.Info($"Loaded {_cache.Count} raids from scripts"); } + + private void EnsureRaidScriptsDirectoryHydrated() + { + bool dbFirst = string.Equals(Environment.GetEnvironmentVariable("DB_FIRST"), "true", StringComparison.OrdinalIgnoreCase) + || string.Equals(Environment.GetEnvironmentVariable("RESOURCE_DB_FIRST"), "true", StringComparison.OrdinalIgnoreCase); + if (!dbFirst) + { + return; + } + + if (Directory.Exists(_scriptFactoryConfiguration.RaidsDirectory)) + { + return; + } + + 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 cmd = new NpgsqlCommand("SELECT relative_path, content FROM resource_files WHERE category='config_scripts' AND relative_path LIKE 'config/scripts/raids/%';", conn); + using var reader = cmd.ExecuteReader(); + int count = 0; + while (reader.Read()) + { + string relative = reader.GetString(0).Replace('/', Path.DirectorySeparatorChar); + byte[] bytes = (byte[])reader[1]; + string fullPath = Path.Combine(Directory.GetCurrentDirectory(), relative); + Directory.CreateDirectory(Path.GetDirectoryName(fullPath) ?? _scriptFactoryConfiguration.RaidsDirectory); + File.WriteAllBytes(fullPath, bytes); + count++; + } + + if (count > 0) + { + Log.Info($"[DB_FIRST] Hydrated {count} raid scripts from resource_files"); + } + } + catch (Exception ex) + { + Log.Error("[DB_FIRST] Could not hydrate raid scripts from database", ex); + } + } } \ No newline at end of file diff --git a/srcs/_plugins/Plugin.ResourceLoader/FileResourceLoaderPlugin.cs b/srcs/_plugins/Plugin.ResourceLoader/FileResourceLoaderPlugin.cs index 4f00f55..246e750 100644 --- a/srcs/_plugins/Plugin.ResourceLoader/FileResourceLoaderPlugin.cs +++ b/srcs/_plugins/Plugin.ResourceLoader/FileResourceLoaderPlugin.cs @@ -41,6 +41,7 @@ namespace Plugin.ResourceLoader services.AddSingleton(); services.AddSingleton(); + services.AddHostedService(); services.AddHostedService(); } } @@ -53,7 +54,6 @@ namespace Plugin.ResourceLoader { services.AddSingleton, GenericTranslationGrpcLoader>(); services.AddSingleton(); - services.AddHostedService(); } } } \ No newline at end of file diff --git a/srcs/_plugins/Plugin.ResourceLoader/Loaders/CardResourceFileLoader.cs b/srcs/_plugins/Plugin.ResourceLoader/Loaders/CardResourceFileLoader.cs index 19ff232..85afbe4 100644 --- a/srcs/_plugins/Plugin.ResourceLoader/Loaders/CardResourceFileLoader.cs +++ b/srcs/_plugins/Plugin.ResourceLoader/Loaders/CardResourceFileLoader.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Text; using System.Threading.Tasks; +using Npgsql; using PhoenixLib.Logging; using WingsAPI.Data.GameData; using WingsEmu.DTOs.BCards; @@ -22,6 +23,16 @@ namespace Plugin.ResourceLoader.Loaders { string filePath = Path.Combine(_config.GameDataPath, "Card.dat"); + if (!File.Exists(filePath)) + { + bool dbFirst = string.Equals(Environment.GetEnvironmentVariable("DB_FIRST"), "true", StringComparison.OrdinalIgnoreCase) + || string.Equals(Environment.GetEnvironmentVariable("RESOURCE_DB_FIRST"), "true", StringComparison.OrdinalIgnoreCase); + if (dbFirst) + { + TryHydrateDatFileFromDatabase("Card.dat", filePath); + } + } + if (!File.Exists(filePath)) { throw new FileNotFoundException($"{filePath} should be present"); @@ -199,5 +210,45 @@ namespace Plugin.ResourceLoader.Loaders Log.Info($"[RESOURCE_LOADER] {cards.Count.ToString()} act desc loaded"); return cards; } + + private void TryHydrateDatFileFromDatabase(string datFileName, string targetPath) + { + 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 cmd = new NpgsqlCommand("SELECT content FROM resource_files WHERE category='dat' AND relative_path=@path LIMIT 1;", conn); + cmd.Parameters.AddWithValue("path", $"dat/{datFileName}"); + object result = cmd.ExecuteScalar(); + + if (result is byte[] bytes && bytes.Length > 0) + { + Directory.CreateDirectory(Path.GetDirectoryName(targetPath) ?? _config.GameDataPath); + File.WriteAllBytes(targetPath, bytes); + Log.Info($"[DB_FIRST] Hydrated {datFileName} from resource_files"); + } + } + catch (Exception ex) + { + Log.Error($"[DB_FIRST] Could not hydrate {datFileName} from database", ex); + } + } } } \ No newline at end of file diff --git a/srcs/_plugins/Plugin.ResourceLoader/Loaders/GameDataLanguageFileLoader.cs b/srcs/_plugins/Plugin.ResourceLoader/Loaders/GameDataLanguageFileLoader.cs index a63d65e..7ff6d9e 100644 --- a/srcs/_plugins/Plugin.ResourceLoader/Loaders/GameDataLanguageFileLoader.cs +++ b/srcs/_plugins/Plugin.ResourceLoader/Loaders/GameDataLanguageFileLoader.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; +using Npgsql; using PhoenixLib.Logging; using PhoenixLib.MultiLanguage; using WingsAPI.Data.GameData; @@ -123,10 +124,30 @@ namespace Plugin.ResourceLoader.Loaders continue; } - string fileLang = $"{_config.GameLanguagePath}/{string.Format(fileToParse, ToNostaleRegionKey(lang))}"; - using var langFileStream = new StreamReader(fileLang, GetEncoding(lang)); - string line; - while ((line = await langFileStream.ReadLineAsync()) != null) + string fileName = string.Format(fileToParse, ToNostaleRegionKey(lang)); + string fileLang = $"{_config.GameLanguagePath}/{fileName}"; + + bool dbFirst = string.Equals(Environment.GetEnvironmentVariable("DB_FIRST"), "true", StringComparison.OrdinalIgnoreCase) + || string.Equals(Environment.GetEnvironmentVariable("RESOURCE_DB_FIRST"), "true", StringComparison.OrdinalIgnoreCase); + + IEnumerable lines = null; + + if (File.Exists(fileLang)) + { + lines = await File.ReadAllLinesAsync(fileLang, GetEncoding(lang)); + } + else if (dbFirst) + { + lines = TryLoadLangLinesFromDatabase(fileName, GetEncoding(lang)); + } + + if (lines == null) + { + Log.Warn($"[DB_FIRST] Missing language source '{fileLang}', skipping."); + continue; + } + + foreach (string line in lines) { string[] lineSave = line.Split('\t'); if (lineSave.Length <= 1) @@ -153,5 +174,38 @@ namespace Plugin.ResourceLoader.Loaders return translations; } + + private IEnumerable TryLoadLangLinesFromDatabase(string langFileName, Encoding encoding) + { + 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 cmd = new NpgsqlCommand("SELECT content FROM resource_files WHERE category='lang' AND (relative_path=@path OR lower(relative_path)=lower(@path) OR lower(relative_path) LIKE lower('%/' || @name)) LIMIT 1;", conn); + cmd.Parameters.AddWithValue("path", $"lang/{langFileName}"); + cmd.Parameters.AddWithValue("name", langFileName); + object result = cmd.ExecuteScalar(); + + if (result is byte[] bytes && bytes.Length > 0) + { + string text = encoding.GetString(bytes); + Log.Info($"[DB_FIRST] Loaded lang file {langFileName} from database"); + return text.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); + } + } + catch (Exception ex) + { + Log.Error($"[DB_FIRST] Could not load lang file {langFileName} from database", ex); + } + + return null; + } } } \ No newline at end of file diff --git a/srcs/_plugins/Plugin.ResourceLoader/Loaders/ItemResourceFileLoader.cs b/srcs/_plugins/Plugin.ResourceLoader/Loaders/ItemResourceFileLoader.cs index 37c4e01..4aa3e4e 100644 --- a/srcs/_plugins/Plugin.ResourceLoader/Loaders/ItemResourceFileLoader.cs +++ b/srcs/_plugins/Plugin.ResourceLoader/Loaders/ItemResourceFileLoader.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; +using Npgsql; using PhoenixLib.Logging; using WingsAPI.Data.GameData; using WingsAPI.Packets.Enums.Shells; @@ -32,6 +33,16 @@ namespace Plugin.ResourceLoader.Loaders string filePath = Path.Combine(_configuration.GameDataPath, FILE_NAME); + if (!File.Exists(filePath)) + { + bool dbFirst = string.Equals(Environment.GetEnvironmentVariable("DB_FIRST"), "true", StringComparison.OrdinalIgnoreCase) + || string.Equals(Environment.GetEnvironmentVariable("RESOURCE_DB_FIRST"), "true", StringComparison.OrdinalIgnoreCase); + if (dbFirst) + { + TryHydrateItemDatFromDatabase(filePath); + } + } + if (!File.Exists(filePath)) { throw new FileNotFoundException($"{filePath} should be present"); @@ -654,6 +665,43 @@ namespace Plugin.ResourceLoader.Loaders item.DarkResistance = Convert.ToByte(currentLine[11]); } + private void TryHydrateItemDatFromDatabase(string filePath) + { + 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 cmd = new NpgsqlCommand("SELECT content FROM resource_files WHERE category='dat' AND (relative_path='dat/Item.dat' OR relative_path='Item.dat') LIMIT 1;", conn); + object result = cmd.ExecuteScalar(); + if (result is byte[] bytes && bytes.Length > 0) + { + Directory.CreateDirectory(Path.GetDirectoryName(filePath) ?? _configuration.GameDataPath); + File.WriteAllBytes(filePath, bytes); + Log.Info("[DB_FIRST] Hydrated Item.dat from resource_files"); + } + } + catch (Exception ex) + { + Log.Error("[DB_FIRST] Could not hydrate Item.dat from database", ex); + } + } + private static void FillMorphAndIndexValues(string[] currentLine, ItemDTO item) { switch (Convert.ToByte(currentLine[2])) diff --git a/srcs/_plugins/Plugin.ResourceLoader/Loaders/MapResourceFileLoader.cs b/srcs/_plugins/Plugin.ResourceLoader/Loaders/MapResourceFileLoader.cs index ad4b79b..ddba917 100644 --- a/srcs/_plugins/Plugin.ResourceLoader/Loaders/MapResourceFileLoader.cs +++ b/srcs/_plugins/Plugin.ResourceLoader/Loaders/MapResourceFileLoader.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; +using Npgsql; using PhoenixLib.Logging; using WingsAPI.Data.GameData; using WingsEmu.DTOs.Maps; @@ -21,6 +22,17 @@ namespace Plugin.ResourceLoader.Loaders { string filePath = Path.Combine(_config.GameDataPath, "MapIDData.dat"); + if (!File.Exists(filePath)) + { + bool dbFirst = string.Equals(Environment.GetEnvironmentVariable("DB_FIRST"), "true", StringComparison.OrdinalIgnoreCase) + || string.Equals(Environment.GetEnvironmentVariable("RESOURCE_DB_FIRST"), "true", StringComparison.OrdinalIgnoreCase); + if (dbFirst) + { + TryHydrateDatFileFromDatabase("MapIDData.dat", filePath); + TryHydrateMapFilesFromDatabase(); + } + } + if (!File.Exists(filePath)) { throw new FileNotFoundException($"{filePath} should be present"); @@ -53,6 +65,16 @@ namespace Plugin.ResourceLoader.Loaders } } + if (!Directory.Exists(_config.GameMapsPath) || !new DirectoryInfo(_config.GameMapsPath).GetFiles().Any()) + { + bool dbFirst = string.Equals(Environment.GetEnvironmentVariable("DB_FIRST"), "true", StringComparison.OrdinalIgnoreCase) + || string.Equals(Environment.GetEnvironmentVariable("RESOURCE_DB_FIRST"), "true", StringComparison.OrdinalIgnoreCase); + if (dbFirst) + { + TryHydrateMapFilesFromDatabase(); + } + } + foreach (FileInfo file in new DirectoryInfo(_config.GameMapsPath).GetFiles()) { string name = string.Empty; @@ -81,5 +103,70 @@ namespace Plugin.ResourceLoader.Loaders Log.Info($"[RESOURCE_LOADER] {maps.Count} Maps loaded"); return maps; } + + private void TryHydrateDatFileFromDatabase(string datFileName, string targetPath) + { + try + { + using var conn = OpenConnection(); + using var cmd = new NpgsqlCommand("SELECT content FROM resource_files WHERE category='dat' AND relative_path=@path LIMIT 1;", conn); + cmd.Parameters.AddWithValue("path", $"dat/{datFileName}"); + object result = cmd.ExecuteScalar(); + + if (result is byte[] bytes && bytes.Length > 0) + { + Directory.CreateDirectory(Path.GetDirectoryName(targetPath) ?? _config.GameDataPath); + File.WriteAllBytes(targetPath, bytes); + Log.Info($"[DB_FIRST] Hydrated {datFileName} from resource_files"); + } + } + catch (Exception ex) + { + Log.Error($"[DB_FIRST] Could not hydrate {datFileName} from database", ex); + } + } + + private void TryHydrateMapFilesFromDatabase() + { + try + { + Directory.CreateDirectory(_config.GameMapsPath); + using var conn = OpenConnection(); + using var cmd = new NpgsqlCommand("SELECT relative_path, content FROM resource_files WHERE category='maps';", conn); + using var reader = cmd.ExecuteReader(); + int count = 0; + while (reader.Read()) + { + string relative = reader.GetString(0).Replace('/', Path.DirectorySeparatorChar); + byte[] content = (byte[])reader[1]; + string path = Path.Combine(_config.ResourcePaths, relative); + Directory.CreateDirectory(Path.GetDirectoryName(path) ?? _config.GameMapsPath); + File.WriteAllBytes(path, content); + count++; + } + + if (count > 0) + { + Log.Info($"[DB_FIRST] Hydrated {count} map files from resource_files"); + } + } + catch (Exception ex) + { + Log.Error("[DB_FIRST] Could not hydrate map files from database", ex); + } + } + + private static NpgsqlConnection OpenConnection() + { + 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"; + + var conn = new NpgsqlConnection($"Host={host};Port={port};Database={db};Username={user};Password={pass}"); + conn.Open(); + return conn; + } } } \ No newline at end of file diff --git a/srcs/_plugins/Plugin.ResourceLoader/Loaders/NpcMonsterFileLoader.cs b/srcs/_plugins/Plugin.ResourceLoader/Loaders/NpcMonsterFileLoader.cs index a99686f..04f2206 100644 --- a/srcs/_plugins/Plugin.ResourceLoader/Loaders/NpcMonsterFileLoader.cs +++ b/srcs/_plugins/Plugin.ResourceLoader/Loaders/NpcMonsterFileLoader.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Npgsql; using PhoenixLib.Logging; using WingsAPI.Data.Drops; using WingsAPI.Data.GameData; @@ -40,6 +41,16 @@ namespace Plugin.ResourceLoader.Loaders string filePath = Path.Combine(_config.GameDataPath, "monster.dat"); + if (!File.Exists(filePath)) + { + bool dbFirst = string.Equals(Environment.GetEnvironmentVariable("DB_FIRST"), "true", StringComparison.OrdinalIgnoreCase) + || string.Equals(Environment.GetEnvironmentVariable("RESOURCE_DB_FIRST"), "true", StringComparison.OrdinalIgnoreCase); + if (dbFirst) + { + TryHydrateDatFileFromDatabase("monster.dat", filePath); + } + } + if (!File.Exists(filePath)) { throw new FileNotFoundException($"{filePath} should be present"); @@ -468,6 +479,46 @@ namespace Plugin.ResourceLoader.Loaders }; } + private void TryHydrateDatFileFromDatabase(string datFileName, string targetPath) + { + 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 cmd = new NpgsqlCommand("SELECT content FROM resource_files WHERE category='dat' AND relative_path=@path LIMIT 1;", conn); + cmd.Parameters.AddWithValue("path", $"dat/{datFileName}"); + object result = cmd.ExecuteScalar(); + + if (result is byte[] bytes && bytes.Length > 0) + { + Directory.CreateDirectory(Path.GetDirectoryName(targetPath) ?? _config.GameDataPath); + File.WriteAllBytes(targetPath, bytes); + Log.Info($"[DB_FIRST] Hydrated {datFileName} from resource_files"); + } + } + catch (Exception ex) + { + Log.Error($"[DB_FIRST] Could not hydrate {datFileName} from database", ex); + } + } + private enum MobFlag : long { CANT_WALK = 1, diff --git a/srcs/_plugins/Plugin.ResourceLoader/Loaders/NpcQuestResourceFileLoader.cs b/srcs/_plugins/Plugin.ResourceLoader/Loaders/NpcQuestResourceFileLoader.cs index 253dda4..5bbaf46 100644 --- a/srcs/_plugins/Plugin.ResourceLoader/Loaders/NpcQuestResourceFileLoader.cs +++ b/srcs/_plugins/Plugin.ResourceLoader/Loaders/NpcQuestResourceFileLoader.cs @@ -1,7 +1,9 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Text; using System.Threading.Tasks; +using Npgsql; using PhoenixLib.Logging; using WingsAPI.Data.GameData; using WingsEmu.DTOs.Quests; @@ -18,6 +20,16 @@ namespace Plugin.ResourceLoader.Loaders { string filePath = Path.Combine(_configuration.GameDataPath, "qstnpc.dat"); + if (!File.Exists(filePath)) + { + bool dbFirst = string.Equals(Environment.GetEnvironmentVariable("DB_FIRST"), "true", StringComparison.OrdinalIgnoreCase) + || string.Equals(Environment.GetEnvironmentVariable("RESOURCE_DB_FIRST"), "true", StringComparison.OrdinalIgnoreCase); + if (dbFirst) + { + TryHydrateDatFileFromDatabase("qstnpc.dat", filePath); + } + } + if (!File.Exists(filePath)) { throw new FileNotFoundException($"{filePath} should be present"); @@ -66,5 +78,45 @@ namespace Plugin.ResourceLoader.Loaders Log.Info($"[RESOURCE_LOADER] {npcQuests.Count.ToString()} NPC quests loaded"); return npcQuests; } + + private void TryHydrateDatFileFromDatabase(string datFileName, string targetPath) + { + 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 cmd = new NpgsqlCommand("SELECT content FROM resource_files WHERE category='dat' AND relative_path=@path LIMIT 1;", conn); + cmd.Parameters.AddWithValue("path", $"dat/{datFileName}"); + object result = cmd.ExecuteScalar(); + + if (result is byte[] bytes && bytes.Length > 0) + { + Directory.CreateDirectory(Path.GetDirectoryName(targetPath) ?? _configuration.GameDataPath); + File.WriteAllBytes(targetPath, bytes); + Log.Info($"[DB_FIRST] Hydrated {datFileName} from resource_files"); + } + } + catch (Exception ex) + { + Log.Error($"[DB_FIRST] Could not hydrate {datFileName} from database", ex); + } + } } } \ No newline at end of file diff --git a/srcs/_plugins/Plugin.ResourceLoader/Loaders/QuestResourceFileLoader.cs b/srcs/_plugins/Plugin.ResourceLoader/Loaders/QuestResourceFileLoader.cs index 8528cb5..859d48f 100644 --- a/srcs/_plugins/Plugin.ResourceLoader/Loaders/QuestResourceFileLoader.cs +++ b/srcs/_plugins/Plugin.ResourceLoader/Loaders/QuestResourceFileLoader.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using CloneExtensions; +using Npgsql; using PhoenixLib.Logging; using WingsAPI.Data.GameData; using WingsEmu.DTOs.Quests; @@ -29,6 +30,17 @@ namespace Plugin.ResourceLoader.Loaders string fileQuestPath = Path.Combine(_configuration.GameDataPath, "quest.dat"); string fileRewardsPath = Path.Combine(_configuration.GameDataPath, "qstprize.dat"); + if (!File.Exists(fileQuestPath) || !File.Exists(fileRewardsPath)) + { + bool dbFirst = string.Equals(Environment.GetEnvironmentVariable("DB_FIRST"), "true", StringComparison.OrdinalIgnoreCase) + || string.Equals(Environment.GetEnvironmentVariable("RESOURCE_DB_FIRST"), "true", StringComparison.OrdinalIgnoreCase); + if (dbFirst) + { + TryHydrateDatFileFromDatabase("quest.dat", fileQuestPath); + TryHydrateDatFileFromDatabase("qstprize.dat", fileRewardsPath); + } + } + if (!File.Exists(fileQuestPath)) { throw new FileNotFoundException($"{fileQuestPath} should be present"); @@ -144,6 +156,46 @@ namespace Plugin.ResourceLoader.Loaders return _quests; } + private void TryHydrateDatFileFromDatabase(string datFileName, string targetPath) + { + 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 cmd = new NpgsqlCommand("SELECT content FROM resource_files WHERE category='dat' AND relative_path=@path LIMIT 1;", conn); + cmd.Parameters.AddWithValue("path", $"dat/{datFileName}"); + object result = cmd.ExecuteScalar(); + + if (result is byte[] bytes && bytes.Length > 0) + { + Directory.CreateDirectory(Path.GetDirectoryName(targetPath) ?? _configuration.GameDataPath); + File.WriteAllBytes(targetPath, bytes); + Log.Info($"[DB_FIRST] Hydrated {datFileName} from resource_files"); + } + } + catch (Exception ex) + { + Log.Error($"[DB_FIRST] Could not hydrate {datFileName} from database", ex); + } + } + private void FillRewards(string fileRewardsPath, Dictionary dictionaryRewards) { using var questRewardStream = new StreamReader(fileRewardsPath, Encoding.GetEncoding(1252)); diff --git a/srcs/_plugins/Plugin.ResourceLoader/Loaders/SkillResourceFileLoader.cs b/srcs/_plugins/Plugin.ResourceLoader/Loaders/SkillResourceFileLoader.cs index 8a36a57..b1cd4c6 100644 --- a/srcs/_plugins/Plugin.ResourceLoader/Loaders/SkillResourceFileLoader.cs +++ b/srcs/_plugins/Plugin.ResourceLoader/Loaders/SkillResourceFileLoader.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Npgsql; using PhoenixLib.Logging; using WingsAPI.Data.GameData; using WingsEmu.DTOs.BCards; @@ -33,6 +34,25 @@ namespace Plugin.ResourceLoader.Loaders return _skills; } + bool dbFirst = string.Equals(Environment.GetEnvironmentVariable("DB_FIRST"), "true", StringComparison.OrdinalIgnoreCase) + || string.Equals(Environment.GetEnvironmentVariable("RESOURCE_DB_FIRST"), "true", StringComparison.OrdinalIgnoreCase); + bool strictDbOnly = string.Equals(Environment.GetEnvironmentVariable("STRICT_DB_ONLY"), "true", StringComparison.OrdinalIgnoreCase); + + if (dbFirst) + { + int loadedFromDb = LoadSkillsFromDatabase(); + if (loadedFromDb > 0) + { + Log.Info($"[RESOURCE_LOADER] {loadedFromDb} Skills loaded from database"); + return _skills; + } + + if (strictDbOnly) + { + throw new InvalidOperationException("DB_FIRST/STRICT_DB_ONLY enabled but no skills were loaded from database."); + } + } + string filePath = Path.Combine(_config.GameDataPath, "Skill.dat"); if (!File.Exists(filePath)) @@ -187,6 +207,66 @@ namespace Plugin.ResourceLoader.Loaders return _skills; } + private int LoadSkillsFromDatabase() + { + 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 cmd = new NpgsqlCommand(@"SELECT 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 +FROM skills ORDER BY id;", conn); + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + var skill = new SkillDTO + { + Id = reader.GetInt32(0), + Name = reader.IsDBNull(1) ? string.Empty : reader.GetString(1), + Class = Convert.ToByte(reader.GetInt32(2)), + CastId = Convert.ToInt16(reader.GetInt32(3)), + CastTime = Convert.ToInt16(reader.GetInt32(4)), + Cooldown = Convert.ToInt16(reader.GetInt32(5)), + MpCost = Convert.ToInt16(reader.GetInt32(6)), + CPCost = Convert.ToByte(reader.GetInt32(7)), + SkillType = (SkillType)reader.GetInt32(8), + AttackType = (AttackType)reader.GetInt32(9), + HitType = (TargetHitType)reader.GetInt32(10), + TargetType = (TargetType)reader.GetInt32(11), + Range = Convert.ToByte(reader.GetInt32(12)), + AoERange = Convert.ToInt16(reader.GetInt32(13)), + LevelMinimum = Convert.ToByte(reader.GetInt32(14)), + Element = Convert.ToByte(reader.GetInt32(15)) + }; + + _skills.Add(skill); + } + + return _skills.Count; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Could not load skills from database, falling back to file parser"); + return 0; + } + } + private static void FillLevelInformation(SkillDTO skill, IReadOnlyList currentLine, IReadOnlyCollection skills) { skill.LevelMinimum = currentLine[2] != "-1" ? byte.Parse(currentLine[2]) : (byte)0; diff --git a/srcs/_plugins/Plugin.ResourceLoader/Loaders/TutorialResourceFileLoader.cs b/srcs/_plugins/Plugin.ResourceLoader/Loaders/TutorialResourceFileLoader.cs index dfd875d..ee9c750 100644 --- a/srcs/_plugins/Plugin.ResourceLoader/Loaders/TutorialResourceFileLoader.cs +++ b/srcs/_plugins/Plugin.ResourceLoader/Loaders/TutorialResourceFileLoader.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; +using Npgsql; using PhoenixLib.Logging; using WingsAPI.Data.GameData; using WingsEmu.DTOs.Quests; @@ -20,6 +21,16 @@ namespace Plugin.ResourceLoader.Loaders { string filePath = Path.Combine(_configuration.GameDataPath, "tutorial.dat"); + if (!File.Exists(filePath)) + { + bool dbFirst = string.Equals(Environment.GetEnvironmentVariable("DB_FIRST"), "true", StringComparison.OrdinalIgnoreCase) + || string.Equals(Environment.GetEnvironmentVariable("RESOURCE_DB_FIRST"), "true", StringComparison.OrdinalIgnoreCase); + if (dbFirst) + { + TryHydrateDatFileFromDatabase("tutorial.dat", filePath); + } + } + if (!File.Exists(filePath)) { throw new FileNotFoundException($"{filePath} should be present"); @@ -99,5 +110,45 @@ namespace Plugin.ResourceLoader.Loaders Log.Info($"[RESOURCE_LOADER] {scriptDatas.Count.ToString()} Tutorial Scripts loaded"); return scriptDatas; } + + private void TryHydrateDatFileFromDatabase(string datFileName, string targetPath) + { + 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 cmd = new NpgsqlCommand("SELECT content FROM resource_files WHERE category='dat' AND relative_path=@path LIMIT 1;", conn); + cmd.Parameters.AddWithValue("path", $"dat/{datFileName}"); + object result = cmd.ExecuteScalar(); + + if (result is byte[] bytes && bytes.Length > 0) + { + Directory.CreateDirectory(Path.GetDirectoryName(targetPath) ?? _configuration.GameDataPath); + File.WriteAllBytes(targetPath, bytes); + Log.Info($"[DB_FIRST] Hydrated {datFileName} from resource_files"); + } + } + catch (Exception ex) + { + Log.Error($"[DB_FIRST] Could not hydrate {datFileName} from database", ex); + } + } } } \ No newline at end of file diff --git a/srcs/_plugins/Plugin.ResourceLoader/Services/DbFirstResourceHydratorService.cs b/srcs/_plugins/Plugin.ResourceLoader/Services/DbFirstResourceHydratorService.cs new file mode 100644 index 0000000..4caf531 --- /dev/null +++ b/srcs/_plugins/Plugin.ResourceLoader/Services/DbFirstResourceHydratorService.cs @@ -0,0 +1,117 @@ +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; + } +} diff --git a/srcs/_plugins/Plugin.ResourceLoader/Services/ResourceFilesPostgresSyncService.cs b/srcs/_plugins/Plugin.ResourceLoader/Services/ResourceFilesPostgresSyncService.cs index 3ead7f8..64b8f44 100644 --- a/srcs/_plugins/Plugin.ResourceLoader/Services/ResourceFilesPostgresSyncService.cs +++ b/srcs/_plugins/Plugin.ResourceLoader/Services/ResourceFilesPostgresSyncService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Security.Cryptography; @@ -7,16 +8,21 @@ 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) + public ResourceFilesPostgresSyncService(ResourceLoadingConfiguration configuration, IResourceLoader skillLoader) { _configuration = configuration; + _skillLoader = skillLoader; } public Task StartAsync(CancellationToken cancellationToken) @@ -47,6 +53,8 @@ namespace Plugin.ResourceLoader.Services 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) @@ -54,6 +62,12 @@ namespace Plugin.ResourceLoader.Services 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(); @@ -68,6 +82,42 @@ namespace Plugin.ResourceLoader.Services 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(); @@ -75,9 +125,13 @@ namespace Plugin.ResourceLoader.Services 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}"); + 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) { @@ -114,5 +168,61 @@ DO UPDATE SET sha256=EXCLUDED.sha256, content=EXCLUDED.content, size_bytes=EXCLU 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(); + } + } + } } } diff --git a/srcs/_plugins/Plugin.TimeSpaces/LuaTimeSpaceScriptManager.cs b/srcs/_plugins/Plugin.TimeSpaces/LuaTimeSpaceScriptManager.cs index 374c058..93c4201 100644 --- a/srcs/_plugins/Plugin.TimeSpaces/LuaTimeSpaceScriptManager.cs +++ b/srcs/_plugins/Plugin.TimeSpaces/LuaTimeSpaceScriptManager.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using MoonSharp.Interpreter; +using Npgsql; using PhoenixLib.Logging; using WingsAPI.Scripting; using WingsAPI.Scripting.Attribute; @@ -34,6 +35,9 @@ public class LuaTimeSpaceScriptManager : ITimeSpaceScriptManager public void Load() { + EnsureTimespaceScriptsDirectoryHydrated(); + Directory.CreateDirectory(_scriptFactoryConfiguration.TimeSpacesDirectory); + IEnumerable files = Directory.GetFiles(_scriptFactoryConfiguration.TimeSpacesDirectory, "*.lua"); foreach (string file in files) { @@ -75,4 +79,63 @@ public class LuaTimeSpaceScriptManager : ITimeSpaceScriptManager return timeSpace; } + + private void EnsureTimespaceScriptsDirectoryHydrated() + { + bool dbFirst = string.Equals(Environment.GetEnvironmentVariable("DB_FIRST"), "true", StringComparison.OrdinalIgnoreCase) + || string.Equals(Environment.GetEnvironmentVariable("RESOURCE_DB_FIRST"), "true", StringComparison.OrdinalIgnoreCase); + if (!dbFirst) + { + return; + } + + if (Directory.Exists(_scriptFactoryConfiguration.TimeSpacesDirectory)) + { + return; + } + + 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 cmd = new NpgsqlCommand("SELECT relative_path, content FROM resource_files WHERE category='config_scripts' AND relative_path LIKE 'config/scripts/timespaces/%';", conn); + using var reader = cmd.ExecuteReader(); + int count = 0; + while (reader.Read()) + { + string relative = reader.GetString(0).Replace('/', Path.DirectorySeparatorChar); + byte[] bytes = (byte[])reader[1]; + string fullPath = Path.Combine(Directory.GetCurrentDirectory(), relative); + Directory.CreateDirectory(Path.GetDirectoryName(fullPath) ?? _scriptFactoryConfiguration.TimeSpacesDirectory); + File.WriteAllBytes(fullPath, bytes); + count++; + } + + if (count > 0) + { + Log.Info($"[DB_FIRST] Hydrated {count} timespace scripts from resource_files"); + } + } + catch (Exception ex) + { + Log.Error("[DB_FIRST] Could not hydrate timespace scripts from database", ex); + } + } } \ No newline at end of file diff --git a/srcs/_plugins/Plugin.TimeSpaces/Plugin.TimeSpaces.csproj b/srcs/_plugins/Plugin.TimeSpaces/Plugin.TimeSpaces.csproj index a5be995..e2a640e 100644 --- a/srcs/_plugins/Plugin.TimeSpaces/Plugin.TimeSpaces.csproj +++ b/srcs/_plugins/Plugin.TimeSpaces/Plugin.TimeSpaces.csproj @@ -8,6 +8,7 @@ + diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/DropManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/DropManager.cs index b9eb293..9fdb780 100644 --- a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/DropManager.cs +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/DropManager.cs @@ -2,10 +2,12 @@ // // Developed by NosWings Team +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using PhoenixLib.Caching; +using PhoenixLib.Logging; using WingsAPI.Data.Drops; using WingsEmu.Game._enum; using WingsEmu.Game.Managers.ServerData; @@ -31,14 +33,43 @@ public class DropManager : IDropManager public async Task InitializeAsync() { - var drops = new List(); + List drops = null; + bool dbFirst = ParserDataPostgresReader.DbFirstEnabled; + bool strictDbOnly = ParserDataPostgresReader.StrictDbOnlyEnabled; - foreach (DropImportFile dropImportExportFile in _dropConfigurations) + if (dbFirst) { - drops.AddRange(dropImportExportFile.Drops.SelectMany(s => s.ToDto())); + try + { + drops = ParserDataPostgresReader.LoadDrops(); + Log.Info($"[DB_FIRST] Loaded {drops.Count} drops from database"); + } + catch (Exception e) + { + if (strictDbOnly) + { + throw new InvalidOperationException("DB_FIRST/STRICT_DB_ONLY enabled but failed to load drops from database.", e); + } + + Log.Error("[DB_FIRST] Could not load drops from database", e); + } + + if (strictDbOnly && (drops == null || drops.Count == 0)) + { + throw new InvalidOperationException("DB_FIRST/STRICT_DB_ONLY enabled but no drops were loaded from database."); + } } - ParserDataPostgresSync.SyncDrops(drops); + if (drops == null || drops.Count == 0) + { + drops = new List(); + foreach (DropImportFile dropImportExportFile in _dropConfigurations) + { + drops.AddRange(dropImportExportFile.Drops.SelectMany(s => s.ToDto())); + } + + ParserDataPostgresSync.SyncDrops(drops); + } foreach (DropDTO drop in drops) { @@ -98,4 +129,4 @@ public class DropManager : IDropManager public IReadOnlyList GetDropsByMonsterVnum(int monsterVnum) => _dropCache.Get($"monsterVnum-{monsterVnum}") ?? EmptyList; public IReadOnlyList GetDropsByMonsterRace(MonsterRaceType monsterRaceType, byte monsterSubRaceType) => _dropCache.Get($"race-{(byte)monsterRaceType}.{monsterSubRaceType}") ?? EmptyList; -} \ No newline at end of file +} diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ItemBoxManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ItemBoxManager.cs index f584f91..c1e0517 100644 --- a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ItemBoxManager.cs +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ItemBoxManager.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using PhoenixLib.Caching; using PhoenixLib.Logging; @@ -26,39 +27,69 @@ public class ItemBoxManager : IItemBoxManager public void Initialize() { - int boxesCount = 0; - var allBoxes = new List(); - foreach (ItemBoxImportFile file in _itemBoxConfigurations) + List allBoxes = null; + bool dbFirst = ParserDataPostgresReader.DbFirstEnabled; + bool strictDbOnly = ParserDataPostgresReader.StrictDbOnlyEnabled; + + if (dbFirst) { - ItemBoxDto box = file.ToDto(); - if (box == null) + try { - continue; + allBoxes = ParserDataPostgresReader.LoadItemBoxes(); + Log.Info($"[DB_FIRST] Loaded {allBoxes.Count} item_boxes from database"); + } + catch (Exception e) + { + if (strictDbOnly) + { + throw new InvalidOperationException("DB_FIRST/STRICT_DB_ONLY enabled but failed to load item_boxes from database.", e); + } + + Log.Error("[DB_FIRST] Could not load item_boxes from database", e); } - // just the item box itself - allBoxes.Add(box); - _itemBoxesCache.Set(box.Id.ToString(), box); - boxesCount++; + if (strictDbOnly && (allBoxes == null || allBoxes.Count == 0)) + { + throw new InvalidOperationException("DB_FIRST/STRICT_DB_ONLY enabled but no item_boxes were loaded from database."); + } } - foreach (RandomBoxImportFile file in _randomBoxConfigurations) + if (allBoxes == null || allBoxes.Count == 0) { - foreach (RandomBoxObject obj in file.Items) + allBoxes = new List(); + foreach (ItemBoxImportFile file in _itemBoxConfigurations) { - ItemBoxDto box = obj.ToDtos(); + ItemBoxDto box = file.ToDto(); if (box == null) { continue; } allBoxes.Add(box); - _itemBoxesCache.Set(box.Id.ToString(), box); - boxesCount++; } + + foreach (RandomBoxImportFile file in _randomBoxConfigurations) + { + foreach (RandomBoxObject obj in file.Items) + { + ItemBoxDto box = obj.ToDtos(); + if (box == null) + { + continue; + } + + allBoxes.Add(box); + } + } + + ParserDataPostgresSync.SyncItemBoxes(allBoxes); } - ParserDataPostgresSync.SyncItemBoxes(allBoxes); - Log.Info($"[ITEMBOX_MANAGER] Loaded {boxesCount} itemBoxes"); + foreach (ItemBoxDto box in allBoxes) + { + _itemBoxesCache.Set(box.Id.ToString(), box); + } + + Log.Info($"[ITEMBOX_MANAGER] Loaded {allBoxes.Count} itemBoxes"); } -} \ No newline at end of file +} diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/MapManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/MapManager.cs index 450a931..5b93d32 100644 --- a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/MapManager.cs +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/MapManager.cs @@ -68,6 +68,8 @@ public class MapManager : IMapManager public async Task Initialize() { + bool dbFirst = ParserDataPostgresReader.DbFirstEnabled; + bool strictDbOnly = ParserDataPostgresReader.StrictDbOnlyEnabled; int count = 0; IEnumerable maps = await _mapLoader.LoadAsync(); foreach (MapDataDTO map in maps) @@ -79,7 +81,39 @@ public class MapManager : IMapManager Log.Info($"[MAP_MANAGER] Loaded {count.ToString()} MapClientData"); count = 0; - IEnumerable portals = _portalConfigurationFiles.SelectMany(s => s.Portals.Select(p => p.ToDto())).ToList(); + List portals = null; + List configuredMaps = null; + if (dbFirst) + { + try + { + configuredMaps = ParserDataPostgresReader.LoadServerMaps(); + portals = ParserDataPostgresReader.LoadMapPortals(); + Log.Info($"[DB_FIRST] Loaded maps={configuredMaps.Count} portals={portals.Count} from database"); + } + catch (Exception e) + { + if (strictDbOnly) + { + throw new InvalidOperationException("DB_FIRST/STRICT_DB_ONLY enabled but failed to load server maps/portals from database.", e); + } + + Log.Error("[DB_FIRST] Could not load server maps/portals from database", e); + } + + if (strictDbOnly && (configuredMaps == null || configuredMaps.Count == 0)) + { + throw new InvalidOperationException("DB_FIRST/STRICT_DB_ONLY enabled but no server_maps were loaded from database."); + } + } + + if (portals == null || configuredMaps == null || configuredMaps.Count == 0) + { + portals = _portalConfigurationFiles.SelectMany(s => s.Portals.Select(p => p.ToDto())).ToList(); + configuredMaps = _mapConfigurations.SelectMany(s => s.Select(p => p.ToDto())).ToList(); + ParserDataPostgresSync.SyncMapsAndPortals(configuredMaps, portals.ToList()); + } + foreach (PortalDTO portal in portals) { if (!_portalDataByMapId.TryGetValue(portal.SourceMapId, out List portalDtos)) @@ -95,8 +129,6 @@ public class MapManager : IMapManager DateTime initTime = DateTime.UtcNow; count = 0; int countBaseMaps = 0; - var configuredMaps = _mapConfigurations.SelectMany(s => s.Select(p => p.ToDto())).ToList(); - ParserDataPostgresSync.SyncMapsAndPortals(configuredMaps, portals.ToList()); foreach (ServerMapDto configuredMap in configuredMaps) { diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/MapMonsterManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/MapMonsterManager.cs index ba523a3..7b4bfda 100644 --- a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/MapMonsterManager.cs +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/MapMonsterManager.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -26,12 +27,42 @@ public class MapMonsterManager : IMapMonsterManager public async Task InitializeAsync() { - var monsters = _files.SelectMany(x => x.Monsters.Select(s => + List monsters = null; + bool dbFirst = ParserDataPostgresReader.DbFirstEnabled; + bool strictDbOnly = ParserDataPostgresReader.StrictDbOnlyEnabled; + + if (dbFirst) { - s.MapId = x.MapId; - return s.ToDto(); - })).ToList(); - ParserDataPostgresSync.SyncMapMonsters(monsters); + try + { + monsters = ParserDataPostgresReader.LoadMapMonsters(); + Log.Info($"[DB_FIRST] Loaded {monsters.Count} map_monsters from database"); + } + catch (Exception e) + { + if (strictDbOnly) + { + throw new InvalidOperationException("DB_FIRST/STRICT_DB_ONLY enabled but failed to load map_monsters from database.", e); + } + + Log.Error("[DB_FIRST] Could not load map_monsters from database", e); + } + + if (strictDbOnly && (monsters == null || monsters.Count == 0)) + { + throw new InvalidOperationException("DB_FIRST/STRICT_DB_ONLY enabled but no map_monsters were loaded from database."); + } + } + + if (monsters == null || monsters.Count == 0) + { + monsters = _files.SelectMany(x => x.Monsters.Select(s => + { + s.MapId = x.MapId; + return s.ToDto(); + })).ToList(); + ParserDataPostgresSync.SyncMapMonsters(monsters); + } int count = 0; foreach (MapMonsterDTO npcDto in monsters) @@ -50,4 +81,4 @@ public class MapMonsterManager : IMapMonsterManager public IReadOnlyList GetByMapId(int mapId) => _mapMonsters.Get($"by-map-id-{mapId.ToString()}"); public IReadOnlyList GetMapMonstersPerVNum(int npcMonsterVnum) => _mapMonsters.Get($"by-monster-vnum-{npcMonsterVnum.ToString()}"); -} \ No newline at end of file +} diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/MapNpcManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/MapNpcManager.cs index 7e3c203..f480039 100644 --- a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/MapNpcManager.cs +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/MapNpcManager.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -26,14 +27,44 @@ public class MapNpcManager : IMapNpcManager public async Task InitializeAsync() { - IEnumerable importedNpcs = _mapNpcConfigurations.SelectMany(x => x.Npcs.Select(s => - { - s.MapId = x.MapId; - return s; - })); + List npcs = null; + bool dbFirst = ParserDataPostgresReader.DbFirstEnabled; + bool strictDbOnly = ParserDataPostgresReader.StrictDbOnlyEnabled; - var npcs = importedNpcs.Select(s => s.ToDto()).ToList(); - ParserDataPostgresSync.SyncMapNpcs(npcs); + if (dbFirst) + { + try + { + npcs = ParserDataPostgresReader.LoadMapNpcs(); + Log.Info($"[DB_FIRST] Loaded {npcs.Count} map_npcs from database"); + } + catch (Exception e) + { + if (strictDbOnly) + { + throw new InvalidOperationException("DB_FIRST/STRICT_DB_ONLY enabled but failed to load map_npcs from database.", e); + } + + Log.Error("[DB_FIRST] Could not load map_npcs from database", e); + } + + if (strictDbOnly && (npcs == null || npcs.Count == 0)) + { + throw new InvalidOperationException("DB_FIRST/STRICT_DB_ONLY enabled but no map_npcs were loaded from database."); + } + } + + if (npcs == null || npcs.Count == 0) + { + IEnumerable importedNpcs = _mapNpcConfigurations.SelectMany(x => x.Npcs.Select(s => + { + s.MapId = x.MapId; + return s; + })); + + npcs = importedNpcs.Select(s => s.ToDto()).ToList(); + ParserDataPostgresSync.SyncMapNpcs(npcs); + } int count = 0; foreach (MapNpcDTO npcDto in npcs) @@ -52,4 +83,4 @@ public class MapNpcManager : IMapNpcManager public IReadOnlyList GetByMapId(int mapId) => _mapNpcs.Get($"by-map-id-{mapId.ToString()}"); public IReadOnlyList GetMapNpcsPerVNum(int npcMonsterVnum) => _mapNpcs.Get($"by-npc-vnum-{npcMonsterVnum.ToString()}"); -} \ No newline at end of file +} diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/Persistence/ParserDataPostgresReader.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/Persistence/ParserDataPostgresReader.cs new file mode 100644 index 0000000..f8f1cf1 --- /dev/null +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/Persistence/ParserDataPostgresReader.cs @@ -0,0 +1,384 @@ +using System; +using System.Collections.Generic; +using Npgsql; +using WingsAPI.Data.Drops; +using WingsAPI.Data.Shops; +using WingsEmu.DTOs.Maps; +using WingsEmu.DTOs.Recipes; +using WingsEmu.DTOs.ServerDatas; +using WingsEmu.DTOs.Shops; +using WingsEmu.Packets.Enums; + +namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs.Persistence; + +public static class ParserDataPostgresReader +{ + public static bool DbFirstEnabled => string.Equals(Environment.GetEnvironmentVariable("DB_FIRST"), "true", StringComparison.OrdinalIgnoreCase); + + public static bool StrictDbOnlyEnabled => string.Equals(Environment.GetEnvironmentVariable("STRICT_DB_ONLY"), "true", StringComparison.OrdinalIgnoreCase); + + private static string BuildConnectionString() + { + 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"; + + return $"Host={host};Port={port};Database={db};Username={user};Password={pass}"; + } + + public static List LoadDrops() + { + var result = new List(); + using var conn = new NpgsqlConnection(BuildConnectionString()); + conn.Open(); + using var cmd = new NpgsqlCommand("SELECT drop_id, amount, drop_chance, item_vnum, map_id, monster_vnum, race_type, race_sub_type FROM drops;", conn); + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + result.Add(new DropDTO + { + Id = reader.GetInt32(0), + Amount = reader.GetInt32(1), + DropChance = reader.GetInt32(2), + ItemVNum = reader.GetInt32(3), + MapId = reader.IsDBNull(4) ? null : reader.GetInt32(4), + MonsterVNum = reader.IsDBNull(5) ? null : reader.GetInt32(5), + RaceType = reader.IsDBNull(6) ? null : reader.GetInt32(6), + RaceSubType = reader.IsDBNull(7) ? null : reader.GetInt32(7) + }); + } + + return result; + } + + public static List LoadMapMonsters() + { + var result = new List(); + using var conn = new NpgsqlConnection(BuildConnectionString()); + conn.Open(); + using var cmd = new NpgsqlCommand("SELECT map_monster_id, map_id, vnum, map_x, map_y, direction, can_move FROM map_monsters;", conn); + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + result.Add(new MapMonsterDTO + { + Id = reader.GetInt32(0), + MapId = reader.GetInt32(1), + MonsterVNum = reader.GetInt32(2), + MapX = reader.GetInt16(3), + MapY = reader.GetInt16(4), + Direction = reader.IsDBNull(5) ? (byte)0 : reader.GetByte(5), + IsMoving = !reader.IsDBNull(6) && reader.GetBoolean(6) + }); + } + + return result; + } + + public static List LoadMapNpcs() + { + var result = new List(); + using var conn = new NpgsqlConnection(BuildConnectionString()); + conn.Open(); + using var cmd = new NpgsqlCommand("SELECT map_npc_id, map_id, vnum, pos_x, pos_y, effect_vnum, effect_delay, dialog_id, direction_facing FROM map_npcs;", conn); + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + result.Add(new MapNpcDTO + { + Id = reader.GetInt32(0), + MapId = reader.GetInt32(1), + NpcVNum = reader.GetInt32(2), + MapX = reader.GetInt16(3), + MapY = reader.GetInt16(4), + Effect = reader.IsDBNull(5) ? (short)0 : reader.GetInt16(5), + EffectDelay = reader.IsDBNull(6) ? (short)0 : reader.GetInt16(6), + Dialog = reader.IsDBNull(7) ? (short)0 : reader.GetInt16(7), + Direction = reader.IsDBNull(8) ? (byte)0 : reader.GetByte(8) + }); + } + + return result; + } + + public static List LoadShops() + { + var shops = new Dictionary(); + using var conn = new NpgsqlConnection(BuildConnectionString()); + conn.Open(); + + using (var cmd = new NpgsqlCommand("SELECT map_npc_id, menu_type, shop_type, name FROM shops;", conn)) + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + int mapNpcId = reader.GetInt32(0); + shops[mapNpcId] = new ShopDTO + { + MapNpcId = mapNpcId, + MenuType = (byte)reader.GetInt32(1), + ShopType = (byte)reader.GetInt32(2), + Name = reader.IsDBNull(3) ? null : reader.GetString(3), + Items = new List(), + Skills = new List() + }; + } + } + + using (var cmd = new NpgsqlCommand("SELECT map_npc_id, slot, color, item_vnum, rare, type, upgrade, price FROM shop_items;", conn)) + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + int mapNpcId = reader.GetInt32(0); + if (!shops.TryGetValue(mapNpcId, out ShopDTO shop)) + { + continue; + } + + shop.Items ??= new List(); + shop.Items.Add(new ShopItemDTO + { + Slot = reader.GetInt16(1), + Color = reader.GetByte(2), + ItemVNum = reader.GetInt32(3), + Rare = reader.GetInt16(4), + Type = reader.GetByte(5), + Upgrade = reader.GetByte(6), + Price = reader.IsDBNull(7) ? null : reader.GetInt32(7) + }); + } + } + + using (var cmd = new NpgsqlCommand("SELECT map_npc_id, skill_vnum, slot, type FROM shop_skills;", conn)) + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + int mapNpcId = reader.GetInt32(0); + if (!shops.TryGetValue(mapNpcId, out ShopDTO shop)) + { + continue; + } + + shop.Skills ??= new List(); + shop.Skills.Add(new ShopSkillDTO + { + SkillVNum = reader.GetInt16(1), + Slot = reader.GetInt16(2), + Type = reader.GetByte(3) + }); + } + } + + return new List(shops.Values); + } + + public static List LoadRecipes() + { + var recipes = new Dictionary(); + using var conn = new NpgsqlConnection(BuildConnectionString()); + conn.Open(); + + using (var cmd = new NpgsqlCommand("SELECT recipe_id, amount, producer_map_npc_id, produced_item_vnum, producer_item_vnum, producer_npc_vnum FROM recipes;", conn)) + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + int recipeId = reader.GetInt32(0); + recipes[recipeId] = new RecipeDTO + { + Id = recipeId, + Amount = reader.GetInt32(1), + ProducerMapNpcId = reader.IsDBNull(2) ? null : reader.GetInt32(2), + ProducedItemVnum = reader.GetInt32(3), + ProducerItemVnum = reader.IsDBNull(4) ? null : reader.GetInt32(4), + ProducerNpcVnum = reader.IsDBNull(5) ? null : reader.GetInt32(5), + Items = new List() + }; + } + } + + using (var cmd = new NpgsqlCommand("SELECT recipe_id, slot, item_vnum, amount FROM recipe_items;", conn)) + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + int recipeId = reader.GetInt32(0); + if (!recipes.TryGetValue(recipeId, out RecipeDTO recipe)) + { + continue; + } + + recipe.Items ??= new List(); + recipe.Items.Add(new RecipeItemDTO + { + Slot = reader.GetInt16(1), + ItemVNum = reader.GetInt16(2), + Amount = reader.GetInt16(3) + }); + } + } + + return new List(recipes.Values); + } + + public static List LoadTeleporters() + { + var result = new List(); + using var conn = new NpgsqlConnection(BuildConnectionString()); + conn.Open(); + using var cmd = new NpgsqlCommand("SELECT teleporter_id, idx, type, map_id, map_npc_id, map_x, map_y FROM map_teleporters;", conn); + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + result.Add(new TeleporterDTO + { + Id = reader.GetInt32(0), + Index = reader.GetInt16(1), + Type = (TeleporterType)reader.GetInt32(2), + MapId = reader.GetInt32(3), + MapNpcId = reader.GetInt32(4), + MapX = reader.GetInt16(5), + MapY = reader.GetInt16(6) + }); + } + + return result; + } + + public static List LoadItemBoxes() + { + var boxes = new Dictionary(); + using var conn = new NpgsqlConnection(BuildConnectionString()); + conn.Open(); + + using (var cmd = new NpgsqlCommand("SELECT item_vnum, box_type, min_rewards, max_rewards, shows_raid_panel FROM item_boxes;", conn)) + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + int itemVnum = reader.GetInt32(0); + boxes[itemVnum] = new ItemBoxDto + { + Id = itemVnum, + ItemBoxType = (ItemBoxType)reader.GetInt32(1), + MinimumRewards = reader.IsDBNull(2) ? null : reader.GetInt32(2), + MaximumRewards = reader.IsDBNull(3) ? null : reader.GetInt32(3), + ShowsRaidBoxPanelOnOpen = !reader.IsDBNull(4) && reader.GetBoolean(4), + Items = new List() + }; + } + } + + using (var cmd = new NpgsqlCommand("SELECT item_vnum, probability, min_original_rare, max_original_rare, generated_amount, generated_vnum, generated_random_rarity, generated_upgrade FROM item_box_items;", conn)) + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + int itemVnum = reader.GetInt32(0); + if (!boxes.TryGetValue(itemVnum, out ItemBoxDto box)) + { + continue; + } + + box.Items ??= new List(); + box.Items.Add(new ItemBoxItemDto + { + Probability = reader.GetInt16(1), + MinimumOriginalItemRare = reader.GetInt16(2), + MaximumOriginalItemRare = reader.GetInt16(3), + ItemGeneratedAmount = reader.GetInt16(4), + ItemGeneratedVNum = reader.GetInt32(5), + ItemGeneratedRandomRarity = !reader.IsDBNull(6) && reader.GetBoolean(6), + ItemGeneratedUpgrade = reader.GetByte(7) + }); + } + } + + return new List(boxes.Values); + } + + public static List LoadServerMaps() + { + var maps = new Dictionary(); + using var conn = new NpgsqlConnection(BuildConnectionString()); + conn.Open(); + + using (var cmd = new NpgsqlCommand("SELECT map_id, map_vnum, map_name_id, map_music_id FROM server_maps;", conn)) + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + int mapId = reader.GetInt32(0); + maps[mapId] = new ServerMapDto + { + Id = mapId, + MapVnum = reader.GetInt32(1), + NameId = reader.GetInt32(2), + MusicId = reader.GetInt32(3), + Flags = new List() + }; + } + } + + using (var cmd = new NpgsqlCommand("SELECT map_id, flag FROM server_map_flags;", conn)) + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + int mapId = reader.GetInt32(0); + if (!maps.TryGetValue(mapId, out ServerMapDto map)) + { + continue; + } + + string flag = reader.GetString(1); + if (Enum.TryParse(flag, out MapFlags parsed)) + { + map.Flags ??= new List(); + map.Flags.Add(parsed); + } + } + } + + return new List(maps.Values); + } + + public static List LoadMapPortals() + { + var result = new List(); + using var conn = new NpgsqlConnection(BuildConnectionString()); + conn.Open(); + using var cmd = new NpgsqlCommand("SELECT destination_map_id, destination_map_x, destination_map_y, source_map_id, source_map_x, source_map_y, type FROM map_portals;", conn); + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + result.Add(new PortalDTO + { + DestinationMapId = reader.GetInt32(0), + DestinationX = reader.GetInt16(1), + DestinationY = reader.GetInt16(2), + SourceMapId = reader.GetInt32(3), + SourceX = reader.GetInt16(4), + SourceY = reader.GetInt16(5), + Type = reader.GetInt16(6), + IsDisabled = false + }); + } + + return result; + } +} diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/Persistence/ParserDataPostgresSync.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/Persistence/ParserDataPostgresSync.cs index 72c9469..a74eb6d 100644 --- a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/Persistence/ParserDataPostgresSync.cs +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/Persistence/ParserDataPostgresSync.cs @@ -591,12 +591,16 @@ CREATE TABLE IF NOT EXISTS recipe_items ( );", conn, tx)) cmd.ExecuteNonQuery(); using (var cmd = new NpgsqlCommand("TRUNCATE TABLE recipes, recipe_items RESTART IDENTITY;", conn, tx)) cmd.ExecuteNonQuery(); + int fallbackRecipeId = 1; foreach (var r in recipes) { + int effectiveRecipeId = r.Id > 0 ? r.Id : fallbackRecipeId; + fallbackRecipeId = Math.Max(fallbackRecipeId + 1, effectiveRecipeId + 1); + using (var cmd = new NpgsqlCommand(@"INSERT INTO recipes(recipe_id,amount,producer_map_npc_id,produced_item_vnum,producer_item_vnum,producer_npc_vnum) VALUES (@id,@amount,@mapNpc,@produced,@prodItem,@prodNpc);", conn, tx)) { - cmd.Parameters.AddWithValue("id", r.Id); + cmd.Parameters.AddWithValue("id", effectiveRecipeId); cmd.Parameters.AddWithValue("amount", r.Amount); cmd.Parameters.AddWithValue("mapNpc", (object?)r.ProducerMapNpcId ?? DBNull.Value); cmd.Parameters.AddWithValue("produced", r.ProducedItemVnum); @@ -609,7 +613,7 @@ VALUES (@id,@amount,@mapNpc,@produced,@prodItem,@prodNpc);", conn, tx)) foreach (RecipeItemDTO it in r.Items) { using var cmd = new NpgsqlCommand("INSERT INTO recipe_items(recipe_id,slot,item_vnum,amount) VALUES (@id,@slot,@item,@amount);", conn, tx); - cmd.Parameters.AddWithValue("id", r.Id); + cmd.Parameters.AddWithValue("id", effectiveRecipeId); cmd.Parameters.AddWithValue("slot", it.Slot); cmd.Parameters.AddWithValue("item", it.ItemVNum); cmd.Parameters.AddWithValue("amount", it.Amount); diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/RecipeManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/RecipeManager.cs index fbb9c57..9649855 100644 --- a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/RecipeManager.cs +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/RecipeManager.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -32,52 +33,82 @@ public class RecipeManager : IRecipeManager public async Task InitializeAsync() { - var recipes = new List(); - foreach (RecipeObject recipeObject in _files.SelectMany(x => x.Recipes)) + List recipes = null; + bool dbFirst = ParserDataPostgresReader.DbFirstEnabled; + bool strictDbOnly = ParserDataPostgresReader.StrictDbOnlyEnabled; + + if (dbFirst) { - if (recipeObject == null) + try { - continue; + recipes = ParserDataPostgresReader.LoadRecipes(); + Log.Info($"[DB_FIRST] Loaded {recipes.Count} recipes from database"); } - - RecipeDTO recipe = recipeObject.ToDto(); - - IGameItem producerItem = _itemsManager.GetItem(recipe.ProducedItemVnum); - if (producerItem is null) + catch (Exception e) { - Log.Warn("[RECIPE] Item not found: " + recipe.Id + - $" on recipe ProducerItemVnum: {recipe.ProducerItemVnum} | ProducerNpc: {recipe.ProducerNpcVnum} | Producer: {recipe.ProducerMapNpcId}"); - } - - List items = new(); - if (recipeObject.Items != null) - { - short slot = 0; - foreach (RecipeItemObject recipeItem in recipeObject.Items) + if (strictDbOnly) { - if (recipeItem == null) - { - continue; - } - - IGameItem item = _itemsManager.GetItem(recipeItem.ItemVnum); - if (item is null) - { - Log.Warn("[RECIPE] Item not found: " + recipeItem.ItemVnum + - $" on recipe ProducerItemVnum: {recipe.ProducerItemVnum} | ProducerNpc: {recipe.ProducerNpcVnum} | Producer: {recipe.ProducerMapNpcId}"); - continue; - } - - items.Add(recipeItem.ToDto(slot)); - slot++; + throw new InvalidOperationException("DB_FIRST/STRICT_DB_ONLY enabled but failed to load recipes from database.", e); } + + Log.Error("[DB_FIRST] Could not load recipes from database", e); } - recipe.Items = items; - recipes.Add(recipe); + if (strictDbOnly && (recipes == null || recipes.Count == 0)) + { + throw new InvalidOperationException("DB_FIRST/STRICT_DB_ONLY enabled but no recipes were loaded from database."); + } } - ParserDataPostgresSync.SyncRecipes(recipes); + if (recipes == null || recipes.Count == 0) + { + recipes = new List(); + foreach (RecipeObject recipeObject in _files.SelectMany(x => x.Recipes)) + { + if (recipeObject == null) + { + continue; + } + + RecipeDTO recipe = recipeObject.ToDto(); + + IGameItem producerItem = _itemsManager.GetItem(recipe.ProducedItemVnum); + if (producerItem is null) + { + Log.Warn("[RECIPE] Item not found: " + recipe.Id + + $" on recipe ProducerItemVnum: {recipe.ProducerItemVnum} | ProducerNpc: {recipe.ProducerNpcVnum} | Producer: {recipe.ProducerMapNpcId}"); + } + + List items = new(); + if (recipeObject.Items != null) + { + short slot = 0; + foreach (RecipeItemObject recipeItem in recipeObject.Items) + { + if (recipeItem == null) + { + continue; + } + + IGameItem item = _itemsManager.GetItem(recipeItem.ItemVnum); + if (item is null) + { + Log.Warn("[RECIPE] Item not found: " + recipeItem.ItemVnum + + $" on recipe ProducerItemVnum: {recipe.ProducerItemVnum} | ProducerNpc: {recipe.ProducerNpcVnum} | Producer: {recipe.ProducerMapNpcId}"); + continue; + } + + items.Add(recipeItem.ToDto(slot)); + slot++; + } + } + + recipe.Items = items; + recipes.Add(recipe); + } + + ParserDataPostgresSync.SyncRecipes(recipes); + } int count = 0; foreach (RecipeDTO recipe in recipes) @@ -143,4 +174,4 @@ public class RecipeManager : IRecipeManager public IReadOnlyList GetRecipesByNpcMonsterVnum(int npcVNum) => _recipes.Get($"npcVnum-{npcVNum}"); public IReadOnlyList GetRecipeByProducedItemVnum(int itemVnum) => _generalRecipes.Where(x => x != null && x.ProducedItemVnum == itemVnum).ToList(); -} \ No newline at end of file +} diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ShopManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ShopManager.cs index afbdc0e..b6ddc9b 100644 --- a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ShopManager.cs +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/ShopManager.cs @@ -35,76 +35,109 @@ public class ShopManager : IShopManager public async Task InitializeAsync() { - IEnumerable importedNpcs = _importFile.SelectMany(x => x.Npcs.Select(s => - { - s.MapId = x.MapId; - return s; - })).ToList(); + List allShops = null; + bool dbFirst = ParserDataPostgresReader.DbFirstEnabled; + bool strictDbOnly = ParserDataPostgresReader.StrictDbOnlyEnabled; - int shopItemsCount = 0; - int shopSkillsCount = 0; - int count = 0; - var allShops = new List(); - - foreach (MapNpcObject npc in importedNpcs) + if (dbFirst) { try { - if (npc.ItemShop == null && npc.SkillShop == null) - { - continue; - } - - ShopDTO shop = npc.SkillShop?.ToDto() ?? npc.ItemShop.ToDto(); - - shop.MapNpcId = npc.MapNpcId; - - if (shop.MenuType == 1) - { - shop.Skills = new List(); - foreach (MapNpcShopTabObject tabs in npc.SkillShop.ShopTabs.Where(x => x.Items != null)) - { - short index = 0; - shop.Skills.AddRange(tabs.Items.Select(x => - { - ShopSkillDTO tpp = x.ToDto((byte)tabs.ShopTabId, index); - index++; - return tpp; - })); - } - } - else - { - short i = 0; - shop.Items = new List(); - foreach (MapNpcShopTabObject tabs in npc.ItemShop.ShopTabs.Where(tabs => tabs.Items != null)) - { - shop.Items.AddRange(tabs.Items.Select(s => - { - ShopItemDTO tpp = s.ToDto((byte)tabs.ShopTabId, i); - i++; - return tpp; - })); - } - } - - allShops.Add(shop); - _shopsByNpcId.Set(shop.MapNpcId, _shopFactory.CreateShop(shop)); - shopItemsCount += shop.Items?.Count ?? 0; - shopSkillsCount += shop.Skills?.Count ?? 0; - count++; + allShops = ParserDataPostgresReader.LoadShops(); + Log.Info($"[DB_FIRST] Loaded {allShops.Count} shops from database"); } catch (Exception e) { - Log.Error("[MAPNPC_IMPORT] ERROR", e); + if (strictDbOnly) + { + throw new InvalidOperationException("DB_FIRST/STRICT_DB_ONLY enabled but failed to load shops from database.", e); + } + + Log.Error("[DB_FIRST] Could not load shops from database", e); + } + + if (strictDbOnly && (allShops == null || allShops.Count == 0)) + { + throw new InvalidOperationException("DB_FIRST/STRICT_DB_ONLY enabled but no shops were loaded from database."); } } - ParserDataPostgresSync.SyncShops(allShops); - Log.Info($"[SHOP_MANAGER] Loaded {count.ToString()} shops."); + if (allShops == null || allShops.Count == 0) + { + IEnumerable importedNpcs = _importFile.SelectMany(x => x.Npcs.Select(s => + { + s.MapId = x.MapId; + return s; + })).ToList(); + + allShops = new List(); + + foreach (MapNpcObject npc in importedNpcs) + { + try + { + if (npc.ItemShop == null && npc.SkillShop == null) + { + continue; + } + + ShopDTO shop = npc.SkillShop?.ToDto() ?? npc.ItemShop.ToDto(); + + shop.MapNpcId = npc.MapNpcId; + + if (shop.MenuType == 1) + { + shop.Skills = new List(); + foreach (MapNpcShopTabObject tabs in npc.SkillShop.ShopTabs.Where(x => x.Items != null)) + { + short index = 0; + shop.Skills.AddRange(tabs.Items.Select(x => + { + ShopSkillDTO tpp = x.ToDto((byte)tabs.ShopTabId, index); + index++; + return tpp; + })); + } + } + else + { + short i = 0; + shop.Items = new List(); + foreach (MapNpcShopTabObject tabs in npc.ItemShop.ShopTabs.Where(tabs => tabs.Items != null)) + { + shop.Items.AddRange(tabs.Items.Select(s => + { + ShopItemDTO tpp = s.ToDto((byte)tabs.ShopTabId, i); + i++; + return tpp; + })); + } + } + + allShops.Add(shop); + } + catch (Exception e) + { + Log.Error("[MAPNPC_IMPORT] ERROR", e); + } + } + + ParserDataPostgresSync.SyncShops(allShops); + } + + int shopItemsCount = 0; + int shopSkillsCount = 0; + foreach (ShopDTO shop in allShops) + { + _shopsByNpcId.Set(shop.MapNpcId, _shopFactory.CreateShop(shop)); + shopItemsCount += shop.Items?.Count ?? 0; + shopSkillsCount += shop.Skills?.Count ?? 0; + } + + Log.Info($"[SHOP_MANAGER] Loaded {allShops.Count.ToString()} shops."); Log.Info($"[SHOP_MANAGER] Loaded {shopItemsCount.ToString()} shops items."); Log.Info($"[SHOP_MANAGER] Loaded {shopSkillsCount.ToString()} shops skills."); } public ShopNpc GetShopByNpcId(int npcId) => _shopsByNpcId.Get(npcId); -} \ No newline at end of file +} diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/TeleporterManager.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/TeleporterManager.cs index a321ae7..59fb22d 100644 --- a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/TeleporterManager.cs +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ServerConfigs/TeleporterManager.cs @@ -2,6 +2,7 @@ // // Developed by NosWings Team +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -25,34 +26,67 @@ public class TeleporterManager : ITeleporterManager public async Task InitializeAsync() { - int count = 0; - var allTeleporters = new List(); - foreach (TeleporterImportFile file in _teleporterConfigurations) + List allTeleporters = null; + bool dbFirst = ParserDataPostgresReader.DbFirstEnabled; + bool strictDbOnly = ParserDataPostgresReader.StrictDbOnlyEnabled; + + if (dbFirst) { - var teleporters = file.Teleporters.Select(s => + try { - s.MapId = file.MapId; - count++; - return s.ToDto(); - }).ToList(); - allTeleporters.AddRange(teleporters); - _teleporters[file.MapId] = teleporters; - foreach (TeleporterDTO teleporter in teleporters) + allTeleporters = ParserDataPostgresReader.LoadTeleporters(); + Log.Info($"[DB_FIRST] Loaded {allTeleporters.Count} map_teleporters from database"); + } + catch (Exception e) { - if (!_teleportersByNpcId.TryGetValue(teleporter.MapNpcId, out List teleporterDtos)) + if (strictDbOnly) { - teleporterDtos = new List(); - _teleportersByNpcId[teleporter.MapNpcId] = teleporterDtos; + throw new InvalidOperationException("DB_FIRST/STRICT_DB_ONLY enabled but failed to load map_teleporters from database.", e); } - teleporterDtos.Add(teleporter); + Log.Error("[DB_FIRST] Could not load map_teleporters from database", e); + } + + if (strictDbOnly && (allTeleporters == null || allTeleporters.Count == 0)) + { + throw new InvalidOperationException("DB_FIRST/STRICT_DB_ONLY enabled but no map_teleporters were loaded from database."); } } - ParserDataPostgresSync.SyncTeleporters(allTeleporters); - Log.Info($"[DATABASE] Loaded {count.ToString()} teleporters."); + if (allTeleporters == null || allTeleporters.Count == 0) + { + allTeleporters = new List(); + foreach (TeleporterImportFile file in _teleporterConfigurations) + { + allTeleporters.AddRange(file.Teleporters.Select(s => + { + s.MapId = file.MapId; + return s.ToDto(); + })); + } + + ParserDataPostgresSync.SyncTeleporters(allTeleporters); + } + + foreach (IGrouping group in allTeleporters.GroupBy(s => s.MapId)) + { + _teleporters[group.Key] = group.ToList(); + } + + foreach (TeleporterDTO teleporter in allTeleporters) + { + if (!_teleportersByNpcId.TryGetValue(teleporter.MapNpcId, out List teleporterDtos)) + { + teleporterDtos = new List(); + _teleportersByNpcId[teleporter.MapNpcId] = teleporterDtos; + } + + teleporterDtos.Add(teleporter); + } + + Log.Info($"[DATABASE] Loaded {allTeleporters.Count.ToString()} teleporters."); } public IReadOnlyList GetTeleportByNpcId(long npcId) => _teleportersByNpcId.GetOrDefault(npcId); public IReadOnlyList GetTeleportByMapId(int mapId) => _teleporters.GetOrDefault(mapId); -} \ No newline at end of file +}