Add DB-first runtime loading and strict PostgreSQL-backed config/resources paths

This commit is contained in:
nizar 2026-02-24 13:02:27 +01:00
parent aafa4585f9
commit 18d24f3cbe
29 changed files with 1753 additions and 164 deletions

View file

@ -192,9 +192,11 @@ services:
WINGSEMU_MONGO_DB: wingsemu_logs WINGSEMU_MONGO_DB: wingsemu_logs
WINGSEMU_MONGO_USERNAME: ${MONGO_ROOT_USERNAME} WINGSEMU_MONGO_USERNAME: ${MONGO_ROOT_USERNAME}
WINGSEMU_MONGO_PWD: ${MONGO_ROOT_PASSWORD} WINGSEMU_MONGO_PWD: ${MONGO_ROOT_PASSWORD}
DB_FIRST: "false"
STRICT_DB_ONLY: "false"
command: ["/app/GameChannel.dll"] command: ["/app/GameChannel.dll"]
volumes: volumes:
- ./resources:/app/resources:ro - ./resources:/app/resources
- ./config:/app/config - ./config:/app/config
ports: ports:
- "8000:8000" - "8000:8000"

View file

@ -4,6 +4,7 @@ using System.IO;
using System.Linq; using System.Linq;
using FluentValidation.Results; using FluentValidation.Results;
using MoonSharp.Interpreter; using MoonSharp.Interpreter;
using Npgsql;
using PhoenixLib.Logging; using PhoenixLib.Logging;
using Plugin.Act4.Scripting.Validator; using Plugin.Act4.Scripting.Validator;
using WingsAPI.Scripting; using WingsAPI.Scripting;
@ -32,6 +33,9 @@ public class DungeonScriptManager : IDungeonScriptManager
public void Load() public void Load()
{ {
EnsureDungeonScriptsDirectoryHydrated();
Directory.CreateDirectory(_scriptFactoryConfiguration.DungeonsDirectory);
IEnumerable<string> files = Directory.GetFiles(_scriptFactoryConfiguration.DungeonsDirectory, "*.lua"); IEnumerable<string> files = Directory.GetFiles(_scriptFactoryConfiguration.DungeonsDirectory, "*.lua");
foreach (string file in files) foreach (string file in files)
{ {
@ -69,4 +73,63 @@ public class DungeonScriptManager : IDungeonScriptManager
Log.Info($"Loaded {_cache.Count} dungeons from scripts"); 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);
}
}
} }

View file

@ -7,6 +7,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
<PackageReference Include="Npgsql" Version="5.0.18" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -8,6 +8,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
<PackageReference Include="Npgsql" Version="5.0.18" />
</ItemGroup> </ItemGroup>

View file

@ -4,6 +4,7 @@ using System.IO;
using System.Linq; using System.Linq;
using FluentValidation.Results; using FluentValidation.Results;
using MoonSharp.Interpreter; using MoonSharp.Interpreter;
using Npgsql;
using PhoenixLib.Logging; using PhoenixLib.Logging;
using Plugin.Raids.Scripting.Validator.Raid; using Plugin.Raids.Scripting.Validator.Raid;
using WingsAPI.Scripting; using WingsAPI.Scripting;
@ -32,6 +33,9 @@ public sealed class RaidScriptManager : IRaidScriptManager
public void Load() public void Load()
{ {
EnsureRaidScriptsDirectoryHydrated();
Directory.CreateDirectory(_scriptFactoryConfiguration.RaidsDirectory);
IEnumerable<string> files = Directory.GetFiles(_scriptFactoryConfiguration.RaidsDirectory, "*.lua"); IEnumerable<string> files = Directory.GetFiles(_scriptFactoryConfiguration.RaidsDirectory, "*.lua");
foreach (string file in files) foreach (string file in files)
{ {
@ -69,4 +73,63 @@ public sealed class RaidScriptManager : IRaidScriptManager
Log.Info($"Loaded {_cache.Count} raids from scripts"); 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);
}
}
} }

View file

@ -41,6 +41,7 @@ namespace Plugin.ResourceLoader
services.AddSingleton<IGameDataLanguageService, InMemoryGameDataLanguageService>(); services.AddSingleton<IGameDataLanguageService, InMemoryGameDataLanguageService>();
services.AddSingleton<IBattleEntityAlgorithmService, BattleEntityAlgorithmService>(); services.AddSingleton<IBattleEntityAlgorithmService, BattleEntityAlgorithmService>();
services.AddHostedService<DbFirstResourceHydratorService>();
services.AddHostedService<ResourceFilesPostgresSyncService>(); services.AddHostedService<ResourceFilesPostgresSyncService>();
} }
} }
@ -53,7 +54,6 @@ namespace Plugin.ResourceLoader
{ {
services.AddSingleton<IResourceLoader<GenericTranslationDto>, GenericTranslationGrpcLoader>(); services.AddSingleton<IResourceLoader<GenericTranslationDto>, GenericTranslationGrpcLoader>();
services.AddSingleton<IGameLanguageService, InMemoryMultilanguageService>(); services.AddSingleton<IGameLanguageService, InMemoryMultilanguageService>();
services.AddHostedService<ResourceFilesPostgresSyncService>();
} }
} }
} }

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Npgsql;
using PhoenixLib.Logging; using PhoenixLib.Logging;
using WingsAPI.Data.GameData; using WingsAPI.Data.GameData;
using WingsEmu.DTOs.BCards; using WingsEmu.DTOs.BCards;
@ -22,6 +23,16 @@ namespace Plugin.ResourceLoader.Loaders
{ {
string filePath = Path.Combine(_config.GameDataPath, "Card.dat"); 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)) if (!File.Exists(filePath))
{ {
throw new FileNotFoundException($"{filePath} should be present"); 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"); Log.Info($"[RESOURCE_LOADER] {cards.Count.ToString()} act desc loaded");
return cards; 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);
}
}
} }
} }

View file

@ -4,6 +4,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Npgsql;
using PhoenixLib.Logging; using PhoenixLib.Logging;
using PhoenixLib.MultiLanguage; using PhoenixLib.MultiLanguage;
using WingsAPI.Data.GameData; using WingsAPI.Data.GameData;
@ -123,10 +124,30 @@ namespace Plugin.ResourceLoader.Loaders
continue; continue;
} }
string fileLang = $"{_config.GameLanguagePath}/{string.Format(fileToParse, ToNostaleRegionKey(lang))}"; string fileName = string.Format(fileToParse, ToNostaleRegionKey(lang));
using var langFileStream = new StreamReader(fileLang, GetEncoding(lang)); string fileLang = $"{_config.GameLanguagePath}/{fileName}";
string line;
while ((line = await langFileStream.ReadLineAsync()) != null) bool dbFirst = string.Equals(Environment.GetEnvironmentVariable("DB_FIRST"), "true", StringComparison.OrdinalIgnoreCase)
|| string.Equals(Environment.GetEnvironmentVariable("RESOURCE_DB_FIRST"), "true", StringComparison.OrdinalIgnoreCase);
IEnumerable<string> 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'); string[] lineSave = line.Split('\t');
if (lineSave.Length <= 1) if (lineSave.Length <= 1)
@ -153,5 +174,38 @@ namespace Plugin.ResourceLoader.Loaders
return translations; return translations;
} }
private IEnumerable<string> 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;
}
} }
} }

View file

@ -4,6 +4,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Npgsql;
using PhoenixLib.Logging; using PhoenixLib.Logging;
using WingsAPI.Data.GameData; using WingsAPI.Data.GameData;
using WingsAPI.Packets.Enums.Shells; using WingsAPI.Packets.Enums.Shells;
@ -32,6 +33,16 @@ namespace Plugin.ResourceLoader.Loaders
string filePath = Path.Combine(_configuration.GameDataPath, FILE_NAME); 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)) if (!File.Exists(filePath))
{ {
throw new FileNotFoundException($"{filePath} should be present"); throw new FileNotFoundException($"{filePath} should be present");
@ -654,6 +665,43 @@ namespace Plugin.ResourceLoader.Loaders
item.DarkResistance = Convert.ToByte(currentLine[11]); 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) private static void FillMorphAndIndexValues(string[] currentLine, ItemDTO item)
{ {
switch (Convert.ToByte(currentLine[2])) switch (Convert.ToByte(currentLine[2]))

View file

@ -4,6 +4,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Npgsql;
using PhoenixLib.Logging; using PhoenixLib.Logging;
using WingsAPI.Data.GameData; using WingsAPI.Data.GameData;
using WingsEmu.DTOs.Maps; using WingsEmu.DTOs.Maps;
@ -21,6 +22,17 @@ namespace Plugin.ResourceLoader.Loaders
{ {
string filePath = Path.Combine(_config.GameDataPath, "MapIDData.dat"); 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)) if (!File.Exists(filePath))
{ {
throw new FileNotFoundException($"{filePath} should be present"); 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()) foreach (FileInfo file in new DirectoryInfo(_config.GameMapsPath).GetFiles())
{ {
string name = string.Empty; string name = string.Empty;
@ -81,5 +103,70 @@ namespace Plugin.ResourceLoader.Loaders
Log.Info($"[RESOURCE_LOADER] {maps.Count} Maps loaded"); Log.Info($"[RESOURCE_LOADER] {maps.Count} Maps loaded");
return maps; 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;
}
} }
} }

View file

@ -5,6 +5,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Npgsql;
using PhoenixLib.Logging; using PhoenixLib.Logging;
using WingsAPI.Data.Drops; using WingsAPI.Data.Drops;
using WingsAPI.Data.GameData; using WingsAPI.Data.GameData;
@ -40,6 +41,16 @@ namespace Plugin.ResourceLoader.Loaders
string filePath = Path.Combine(_config.GameDataPath, "monster.dat"); 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)) if (!File.Exists(filePath))
{ {
throw new FileNotFoundException($"{filePath} should be present"); 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 private enum MobFlag : long
{ {
CANT_WALK = 1, CANT_WALK = 1,

View file

@ -1,7 +1,9 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Npgsql;
using PhoenixLib.Logging; using PhoenixLib.Logging;
using WingsAPI.Data.GameData; using WingsAPI.Data.GameData;
using WingsEmu.DTOs.Quests; using WingsEmu.DTOs.Quests;
@ -18,6 +20,16 @@ namespace Plugin.ResourceLoader.Loaders
{ {
string filePath = Path.Combine(_configuration.GameDataPath, "qstnpc.dat"); 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)) if (!File.Exists(filePath))
{ {
throw new FileNotFoundException($"{filePath} should be present"); 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"); Log.Info($"[RESOURCE_LOADER] {npcQuests.Count.ToString()} NPC quests loaded");
return npcQuests; 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);
}
}
} }
} }

View file

@ -5,6 +5,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using CloneExtensions; using CloneExtensions;
using Npgsql;
using PhoenixLib.Logging; using PhoenixLib.Logging;
using WingsAPI.Data.GameData; using WingsAPI.Data.GameData;
using WingsEmu.DTOs.Quests; using WingsEmu.DTOs.Quests;
@ -29,6 +30,17 @@ namespace Plugin.ResourceLoader.Loaders
string fileQuestPath = Path.Combine(_configuration.GameDataPath, "quest.dat"); string fileQuestPath = Path.Combine(_configuration.GameDataPath, "quest.dat");
string fileRewardsPath = Path.Combine(_configuration.GameDataPath, "qstprize.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)) if (!File.Exists(fileQuestPath))
{ {
throw new FileNotFoundException($"{fileQuestPath} should be present"); throw new FileNotFoundException($"{fileQuestPath} should be present");
@ -144,6 +156,46 @@ namespace Plugin.ResourceLoader.Loaders
return _quests; 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<long, QuestPrizeDto> dictionaryRewards) private void FillRewards(string fileRewardsPath, Dictionary<long, QuestPrizeDto> dictionaryRewards)
{ {
using var questRewardStream = new StreamReader(fileRewardsPath, Encoding.GetEncoding(1252)); using var questRewardStream = new StreamReader(fileRewardsPath, Encoding.GetEncoding(1252));

View file

@ -5,6 +5,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Npgsql;
using PhoenixLib.Logging; using PhoenixLib.Logging;
using WingsAPI.Data.GameData; using WingsAPI.Data.GameData;
using WingsEmu.DTOs.BCards; using WingsEmu.DTOs.BCards;
@ -33,6 +34,25 @@ namespace Plugin.ResourceLoader.Loaders
return _skills; 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"); string filePath = Path.Combine(_config.GameDataPath, "Skill.dat");
if (!File.Exists(filePath)) if (!File.Exists(filePath))
@ -187,6 +207,66 @@ namespace Plugin.ResourceLoader.Loaders
return _skills; 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<string> currentLine, IReadOnlyCollection<SkillDTO> skills) private static void FillLevelInformation(SkillDTO skill, IReadOnlyList<string> currentLine, IReadOnlyCollection<SkillDTO> skills)
{ {
skill.LevelMinimum = currentLine[2] != "-1" ? byte.Parse(currentLine[2]) : (byte)0; skill.LevelMinimum = currentLine[2] != "-1" ? byte.Parse(currentLine[2]) : (byte)0;

View file

@ -4,6 +4,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Npgsql;
using PhoenixLib.Logging; using PhoenixLib.Logging;
using WingsAPI.Data.GameData; using WingsAPI.Data.GameData;
using WingsEmu.DTOs.Quests; using WingsEmu.DTOs.Quests;
@ -20,6 +21,16 @@ namespace Plugin.ResourceLoader.Loaders
{ {
string filePath = Path.Combine(_configuration.GameDataPath, "tutorial.dat"); 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)) if (!File.Exists(filePath))
{ {
throw new FileNotFoundException($"{filePath} should be present"); 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"); Log.Info($"[RESOURCE_LOADER] {scriptDatas.Count.ToString()} Tutorial Scripts loaded");
return scriptDatas; 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);
}
}
} }
} }

View file

@ -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<string, int>(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;
}
}

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Security.Cryptography; using System.Security.Cryptography;
@ -7,16 +8,21 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Npgsql; using Npgsql;
using PhoenixLib.Logging; using PhoenixLib.Logging;
using WingsAPI.Data.GameData;
using WingsEmu.DTOs.BCards;
using WingsEmu.DTOs.Skills;
namespace Plugin.ResourceLoader.Services namespace Plugin.ResourceLoader.Services
{ {
public class ResourceFilesPostgresSyncService : IHostedService public class ResourceFilesPostgresSyncService : IHostedService
{ {
private readonly ResourceLoadingConfiguration _configuration; private readonly ResourceLoadingConfiguration _configuration;
private readonly IResourceLoader<SkillDTO> _skillLoader;
public ResourceFilesPostgresSyncService(ResourceLoadingConfiguration configuration) public ResourceFilesPostgresSyncService(ResourceLoadingConfiguration configuration, IResourceLoader<SkillDTO> skillLoader)
{ {
_configuration = configuration; _configuration = configuration;
_skillLoader = skillLoader;
} }
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
@ -47,6 +53,8 @@ namespace Plugin.ResourceLoader.Services
string root = _configuration.ResourcePaths; string root = _configuration.ResourcePaths;
string datPath = _configuration.GameDataPath; string datPath = _configuration.GameDataPath;
string langPath = _configuration.GameLanguagePath; string langPath = _configuration.GameLanguagePath;
string mapsPath = _configuration.GameMapsPath;
string configScriptsPath = Path.Combine(Directory.GetCurrentDirectory(), "config", "scripts");
string[] datFiles = Directory.Exists(datPath) string[] datFiles = Directory.Exists(datPath)
? Directory.GetFiles(datPath, "*", SearchOption.AllDirectories) ? Directory.GetFiles(datPath, "*", SearchOption.AllDirectories)
@ -54,6 +62,12 @@ namespace Plugin.ResourceLoader.Services
string[] langFiles = Directory.Exists(langPath) string[] langFiles = Directory.Exists(langPath)
? Directory.GetFiles(langPath, "*", SearchOption.AllDirectories) ? Directory.GetFiles(langPath, "*", SearchOption.AllDirectories)
: Array.Empty<string>(); : Array.Empty<string>();
string[] mapFiles = Directory.Exists(mapsPath)
? Directory.GetFiles(mapsPath, "*", SearchOption.AllDirectories)
: Array.Empty<string>();
string[] scriptFiles = Directory.Exists(configScriptsPath)
? Directory.GetFiles(configScriptsPath, "*", SearchOption.AllDirectories)
: Array.Empty<string>();
using var conn = new NpgsqlConnection($"Host={host};Port={port};Database={db};Username={user};Password={pass}"); using var conn = new NpgsqlConnection($"Host={host};Port={port};Database={db};Username={user};Password={pass}");
conn.Open(); conn.Open();
@ -68,6 +82,42 @@ namespace Plugin.ResourceLoader.Services
size_bytes INT NOT NULL, size_bytes INT NOT NULL,
synced_at TIMESTAMP NOT NULL DEFAULT NOW(), synced_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE(category, relative_path) 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)) );", conn, tx))
{ {
cmd.ExecuteNonQuery(); cmd.ExecuteNonQuery();
@ -75,9 +125,13 @@ namespace Plugin.ResourceLoader.Services
UpsertFiles(conn, tx, root, "dat", datFiles); UpsertFiles(conn, tx, root, "dat", datFiles);
UpsertFiles(conn, tx, root, "lang", langFiles); 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(); 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) catch (Exception ex)
{ {
@ -114,5 +168,61 @@ DO UPDATE SET sha256=EXCLUDED.sha256, content=EXCLUDED.content, size_bytes=EXCLU
cmd.ExecuteNonQuery(); cmd.ExecuteNonQuery();
} }
} }
private static void SyncSkills(NpgsqlConnection conn, NpgsqlTransaction tx, IReadOnlyList<SkillDTO> 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<BCardDTO>())
{
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<ComboDTO>())
{
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();
}
}
}
} }
} }

View file

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using MoonSharp.Interpreter; using MoonSharp.Interpreter;
using Npgsql;
using PhoenixLib.Logging; using PhoenixLib.Logging;
using WingsAPI.Scripting; using WingsAPI.Scripting;
using WingsAPI.Scripting.Attribute; using WingsAPI.Scripting.Attribute;
@ -34,6 +35,9 @@ public class LuaTimeSpaceScriptManager : ITimeSpaceScriptManager
public void Load() public void Load()
{ {
EnsureTimespaceScriptsDirectoryHydrated();
Directory.CreateDirectory(_scriptFactoryConfiguration.TimeSpacesDirectory);
IEnumerable<string> files = Directory.GetFiles(_scriptFactoryConfiguration.TimeSpacesDirectory, "*.lua"); IEnumerable<string> files = Directory.GetFiles(_scriptFactoryConfiguration.TimeSpacesDirectory, "*.lua");
foreach (string file in files) foreach (string file in files)
{ {
@ -75,4 +79,63 @@ public class LuaTimeSpaceScriptManager : ITimeSpaceScriptManager
return timeSpace; 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);
}
}
} }

View file

@ -8,6 +8,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
<PackageReference Include="Npgsql" Version="5.0.18" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -2,10 +2,12 @@
// //
// Developed by NosWings Team // Developed by NosWings Team
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using PhoenixLib.Caching; using PhoenixLib.Caching;
using PhoenixLib.Logging;
using WingsAPI.Data.Drops; using WingsAPI.Data.Drops;
using WingsEmu.Game._enum; using WingsEmu.Game._enum;
using WingsEmu.Game.Managers.ServerData; using WingsEmu.Game.Managers.ServerData;
@ -31,14 +33,43 @@ public class DropManager : IDropManager
public async Task InitializeAsync() public async Task InitializeAsync()
{ {
var drops = new List<DropDTO>(); List<DropDTO> 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<DropDTO>();
foreach (DropImportFile dropImportExportFile in _dropConfigurations)
{
drops.AddRange(dropImportExportFile.Drops.SelectMany(s => s.ToDto()));
}
ParserDataPostgresSync.SyncDrops(drops);
}
foreach (DropDTO drop in drops) foreach (DropDTO drop in drops)
{ {
@ -98,4 +129,4 @@ public class DropManager : IDropManager
public IReadOnlyList<DropDTO> GetDropsByMonsterVnum(int monsterVnum) => _dropCache.Get($"monsterVnum-{monsterVnum}") ?? EmptyList; public IReadOnlyList<DropDTO> GetDropsByMonsterVnum(int monsterVnum) => _dropCache.Get($"monsterVnum-{monsterVnum}") ?? EmptyList;
public IReadOnlyList<DropDTO> GetDropsByMonsterRace(MonsterRaceType monsterRaceType, byte monsterSubRaceType) => _dropCache.Get($"race-{(byte)monsterRaceType}.{monsterSubRaceType}") ?? EmptyList; public IReadOnlyList<DropDTO> GetDropsByMonsterRace(MonsterRaceType monsterRaceType, byte monsterSubRaceType) => _dropCache.Get($"race-{(byte)monsterRaceType}.{monsterSubRaceType}") ?? EmptyList;
} }

View file

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using PhoenixLib.Caching; using PhoenixLib.Caching;
using PhoenixLib.Logging; using PhoenixLib.Logging;
@ -26,39 +27,69 @@ public class ItemBoxManager : IItemBoxManager
public void Initialize() public void Initialize()
{ {
int boxesCount = 0; List<ItemBoxDto> allBoxes = null;
var allBoxes = new List<ItemBoxDto>(); bool dbFirst = ParserDataPostgresReader.DbFirstEnabled;
foreach (ItemBoxImportFile file in _itemBoxConfigurations) bool strictDbOnly = ParserDataPostgresReader.StrictDbOnlyEnabled;
if (dbFirst)
{ {
ItemBoxDto box = file.ToDto(); try
if (box == null)
{ {
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 if (strictDbOnly && (allBoxes == null || allBoxes.Count == 0))
allBoxes.Add(box); {
_itemBoxesCache.Set(box.Id.ToString(), box); throw new InvalidOperationException("DB_FIRST/STRICT_DB_ONLY enabled but no item_boxes were loaded from database.");
boxesCount++; }
} }
foreach (RandomBoxImportFile file in _randomBoxConfigurations) if (allBoxes == null || allBoxes.Count == 0)
{ {
foreach (RandomBoxObject obj in file.Items) allBoxes = new List<ItemBoxDto>();
foreach (ItemBoxImportFile file in _itemBoxConfigurations)
{ {
ItemBoxDto box = obj.ToDtos(); ItemBoxDto box = file.ToDto();
if (box == null) if (box == null)
{ {
continue; continue;
} }
allBoxes.Add(box); 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); foreach (ItemBoxDto box in allBoxes)
Log.Info($"[ITEMBOX_MANAGER] Loaded {boxesCount} itemBoxes"); {
_itemBoxesCache.Set(box.Id.ToString(), box);
}
Log.Info($"[ITEMBOX_MANAGER] Loaded {allBoxes.Count} itemBoxes");
} }
} }

View file

@ -68,6 +68,8 @@ public class MapManager : IMapManager
public async Task Initialize() public async Task Initialize()
{ {
bool dbFirst = ParserDataPostgresReader.DbFirstEnabled;
bool strictDbOnly = ParserDataPostgresReader.StrictDbOnlyEnabled;
int count = 0; int count = 0;
IEnumerable<MapDataDTO> maps = await _mapLoader.LoadAsync(); IEnumerable<MapDataDTO> maps = await _mapLoader.LoadAsync();
foreach (MapDataDTO map in maps) foreach (MapDataDTO map in maps)
@ -79,7 +81,39 @@ public class MapManager : IMapManager
Log.Info($"[MAP_MANAGER] Loaded {count.ToString()} MapClientData"); Log.Info($"[MAP_MANAGER] Loaded {count.ToString()} MapClientData");
count = 0; count = 0;
IEnumerable<PortalDTO> portals = _portalConfigurationFiles.SelectMany(s => s.Portals.Select(p => p.ToDto())).ToList(); List<PortalDTO> portals = null;
List<ServerMapDto> 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) foreach (PortalDTO portal in portals)
{ {
if (!_portalDataByMapId.TryGetValue(portal.SourceMapId, out List<PortalDTO> portalDtos)) if (!_portalDataByMapId.TryGetValue(portal.SourceMapId, out List<PortalDTO> portalDtos))
@ -95,8 +129,6 @@ public class MapManager : IMapManager
DateTime initTime = DateTime.UtcNow; DateTime initTime = DateTime.UtcNow;
count = 0; count = 0;
int countBaseMaps = 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) foreach (ServerMapDto configuredMap in configuredMaps)
{ {

View file

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -26,12 +27,42 @@ public class MapMonsterManager : IMapMonsterManager
public async Task InitializeAsync() public async Task InitializeAsync()
{ {
var monsters = _files.SelectMany(x => x.Monsters.Select(s => List<MapMonsterDTO> monsters = null;
bool dbFirst = ParserDataPostgresReader.DbFirstEnabled;
bool strictDbOnly = ParserDataPostgresReader.StrictDbOnlyEnabled;
if (dbFirst)
{ {
s.MapId = x.MapId; try
return s.ToDto(); {
})).ToList(); monsters = ParserDataPostgresReader.LoadMapMonsters();
ParserDataPostgresSync.SyncMapMonsters(monsters); 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; int count = 0;
foreach (MapMonsterDTO npcDto in monsters) foreach (MapMonsterDTO npcDto in monsters)
@ -50,4 +81,4 @@ public class MapMonsterManager : IMapMonsterManager
public IReadOnlyList<MapMonsterDTO> GetByMapId(int mapId) => _mapMonsters.Get($"by-map-id-{mapId.ToString()}"); public IReadOnlyList<MapMonsterDTO> GetByMapId(int mapId) => _mapMonsters.Get($"by-map-id-{mapId.ToString()}");
public IReadOnlyList<MapMonsterDTO> GetMapMonstersPerVNum(int npcMonsterVnum) => _mapMonsters.Get($"by-monster-vnum-{npcMonsterVnum.ToString()}"); public IReadOnlyList<MapMonsterDTO> GetMapMonstersPerVNum(int npcMonsterVnum) => _mapMonsters.Get($"by-monster-vnum-{npcMonsterVnum.ToString()}");
} }

View file

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -26,14 +27,44 @@ public class MapNpcManager : IMapNpcManager
public async Task InitializeAsync() public async Task InitializeAsync()
{ {
IEnumerable<MapNpcObject> importedNpcs = _mapNpcConfigurations.SelectMany(x => x.Npcs.Select(s => List<MapNpcDTO> npcs = null;
{ bool dbFirst = ParserDataPostgresReader.DbFirstEnabled;
s.MapId = x.MapId; bool strictDbOnly = ParserDataPostgresReader.StrictDbOnlyEnabled;
return s;
}));
var npcs = importedNpcs.Select(s => s.ToDto()).ToList(); if (dbFirst)
ParserDataPostgresSync.SyncMapNpcs(npcs); {
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<MapNpcObject> 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; int count = 0;
foreach (MapNpcDTO npcDto in npcs) foreach (MapNpcDTO npcDto in npcs)
@ -52,4 +83,4 @@ public class MapNpcManager : IMapNpcManager
public IReadOnlyList<MapNpcDTO> GetByMapId(int mapId) => _mapNpcs.Get($"by-map-id-{mapId.ToString()}"); public IReadOnlyList<MapNpcDTO> GetByMapId(int mapId) => _mapNpcs.Get($"by-map-id-{mapId.ToString()}");
public IReadOnlyList<MapNpcDTO> GetMapNpcsPerVNum(int npcMonsterVnum) => _mapNpcs.Get($"by-npc-vnum-{npcMonsterVnum.ToString()}"); public IReadOnlyList<MapNpcDTO> GetMapNpcsPerVNum(int npcMonsterVnum) => _mapNpcs.Get($"by-npc-vnum-{npcMonsterVnum.ToString()}");
} }

View file

@ -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<DropDTO> LoadDrops()
{
var result = new List<DropDTO>();
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<MapMonsterDTO> LoadMapMonsters()
{
var result = new List<MapMonsterDTO>();
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<MapNpcDTO> LoadMapNpcs()
{
var result = new List<MapNpcDTO>();
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<ShopDTO> LoadShops()
{
var shops = new Dictionary<int, ShopDTO>();
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<ShopItemDTO>(),
Skills = new List<ShopSkillDTO>()
};
}
}
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<ShopItemDTO>();
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<ShopSkillDTO>();
shop.Skills.Add(new ShopSkillDTO
{
SkillVNum = reader.GetInt16(1),
Slot = reader.GetInt16(2),
Type = reader.GetByte(3)
});
}
}
return new List<ShopDTO>(shops.Values);
}
public static List<RecipeDTO> LoadRecipes()
{
var recipes = new Dictionary<int, RecipeDTO>();
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<RecipeItemDTO>()
};
}
}
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<RecipeItemDTO>();
recipe.Items.Add(new RecipeItemDTO
{
Slot = reader.GetInt16(1),
ItemVNum = reader.GetInt16(2),
Amount = reader.GetInt16(3)
});
}
}
return new List<RecipeDTO>(recipes.Values);
}
public static List<TeleporterDTO> LoadTeleporters()
{
var result = new List<TeleporterDTO>();
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<ItemBoxDto> LoadItemBoxes()
{
var boxes = new Dictionary<int, ItemBoxDto>();
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<ItemBoxItemDto>()
};
}
}
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<ItemBoxItemDto>();
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<ItemBoxDto>(boxes.Values);
}
public static List<ServerMapDto> LoadServerMaps()
{
var maps = new Dictionary<int, ServerMapDto>();
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<MapFlags>()
};
}
}
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<MapFlags>();
map.Flags.Add(parsed);
}
}
}
return new List<ServerMapDto>(maps.Values);
}
public static List<PortalDTO> LoadMapPortals()
{
var result = new List<PortalDTO>();
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;
}
}

View file

@ -591,12 +591,16 @@ CREATE TABLE IF NOT EXISTS recipe_items (
);", conn, tx)) cmd.ExecuteNonQuery(); );", conn, tx)) cmd.ExecuteNonQuery();
using (var cmd = new NpgsqlCommand("TRUNCATE TABLE recipes, recipe_items RESTART IDENTITY;", 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) 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) 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)) 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("amount", r.Amount);
cmd.Parameters.AddWithValue("mapNpc", (object?)r.ProducerMapNpcId ?? DBNull.Value); cmd.Parameters.AddWithValue("mapNpc", (object?)r.ProducerMapNpcId ?? DBNull.Value);
cmd.Parameters.AddWithValue("produced", r.ProducedItemVnum); 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) 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); 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("slot", it.Slot);
cmd.Parameters.AddWithValue("item", it.ItemVNum); cmd.Parameters.AddWithValue("item", it.ItemVNum);
cmd.Parameters.AddWithValue("amount", it.Amount); cmd.Parameters.AddWithValue("amount", it.Amount);

View file

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -32,52 +33,82 @@ public class RecipeManager : IRecipeManager
public async Task InitializeAsync() public async Task InitializeAsync()
{ {
var recipes = new List<RecipeDTO>(); List<RecipeDTO> recipes = null;
foreach (RecipeObject recipeObject in _files.SelectMany(x => x.Recipes)) 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");
} }
catch (Exception e)
RecipeDTO recipe = recipeObject.ToDto();
IGameItem producerItem = _itemsManager.GetItem(recipe.ProducedItemVnum);
if (producerItem is null)
{ {
Log.Warn("[RECIPE] Item not found: " + recipe.Id + if (strictDbOnly)
$" on recipe ProducerItemVnum: {recipe.ProducerItemVnum} | ProducerNpc: {recipe.ProducerNpcVnum} | Producer: {recipe.ProducerMapNpcId}");
}
List<RecipeItemDTO> items = new();
if (recipeObject.Items != null)
{
short slot = 0;
foreach (RecipeItemObject recipeItem in recipeObject.Items)
{ {
if (recipeItem == null) throw new InvalidOperationException("DB_FIRST/STRICT_DB_ONLY enabled but failed to load recipes from database.", e);
{
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++;
} }
Log.Error("[DB_FIRST] Could not load recipes from database", e);
} }
recipe.Items = items; if (strictDbOnly && (recipes == null || recipes.Count == 0))
recipes.Add(recipe); {
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<RecipeDTO>();
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<RecipeItemDTO> 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; int count = 0;
foreach (RecipeDTO recipe in recipes) foreach (RecipeDTO recipe in recipes)
@ -143,4 +174,4 @@ public class RecipeManager : IRecipeManager
public IReadOnlyList<Recipe> GetRecipesByNpcMonsterVnum(int npcVNum) => _recipes.Get($"npcVnum-{npcVNum}"); public IReadOnlyList<Recipe> GetRecipesByNpcMonsterVnum(int npcVNum) => _recipes.Get($"npcVnum-{npcVNum}");
public IReadOnlyList<Recipe> GetRecipeByProducedItemVnum(int itemVnum) => _generalRecipes.Where(x => x != null && x.ProducedItemVnum == itemVnum).ToList(); public IReadOnlyList<Recipe> GetRecipeByProducedItemVnum(int itemVnum) => _generalRecipes.Where(x => x != null && x.ProducedItemVnum == itemVnum).ToList();
} }

View file

@ -35,76 +35,109 @@ public class ShopManager : IShopManager
public async Task InitializeAsync() public async Task InitializeAsync()
{ {
IEnumerable<MapNpcObject> importedNpcs = _importFile.SelectMany(x => x.Npcs.Select(s => List<ShopDTO> allShops = null;
{ bool dbFirst = ParserDataPostgresReader.DbFirstEnabled;
s.MapId = x.MapId; bool strictDbOnly = ParserDataPostgresReader.StrictDbOnlyEnabled;
return s;
})).ToList();
int shopItemsCount = 0; if (dbFirst)
int shopSkillsCount = 0;
int count = 0;
var allShops = new List<ShopDTO>();
foreach (MapNpcObject npc in importedNpcs)
{ {
try try
{ {
if (npc.ItemShop == null && npc.SkillShop == null) allShops = ParserDataPostgresReader.LoadShops();
{ Log.Info($"[DB_FIRST] Loaded {allShops.Count} shops from database");
continue;
}
ShopDTO shop = npc.SkillShop?.ToDto() ?? npc.ItemShop.ToDto();
shop.MapNpcId = npc.MapNpcId;
if (shop.MenuType == 1)
{
shop.Skills = new List<ShopSkillDTO>();
foreach (MapNpcShopTabObject<MapNpcShopSkillObject> 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<ShopItemDTO>();
foreach (MapNpcShopTabObject<MapNpcShopItemObject> 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++;
} }
catch (Exception e) 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); if (allShops == null || allShops.Count == 0)
Log.Info($"[SHOP_MANAGER] Loaded {count.ToString()} shops."); {
IEnumerable<MapNpcObject> importedNpcs = _importFile.SelectMany(x => x.Npcs.Select(s =>
{
s.MapId = x.MapId;
return s;
})).ToList();
allShops = new List<ShopDTO>();
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<ShopSkillDTO>();
foreach (MapNpcShopTabObject<MapNpcShopSkillObject> 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<ShopItemDTO>();
foreach (MapNpcShopTabObject<MapNpcShopItemObject> 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 {shopItemsCount.ToString()} shops items.");
Log.Info($"[SHOP_MANAGER] Loaded {shopSkillsCount.ToString()} shops skills."); Log.Info($"[SHOP_MANAGER] Loaded {shopSkillsCount.ToString()} shops skills.");
} }
public ShopNpc GetShopByNpcId(int npcId) => _shopsByNpcId.Get(npcId); public ShopNpc GetShopByNpcId(int npcId) => _shopsByNpcId.Get(npcId);
} }

View file

@ -2,6 +2,7 @@
// //
// Developed by NosWings Team // Developed by NosWings Team
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -25,34 +26,67 @@ public class TeleporterManager : ITeleporterManager
public async Task InitializeAsync() public async Task InitializeAsync()
{ {
int count = 0; List<TeleporterDTO> allTeleporters = null;
var allTeleporters = new List<TeleporterDTO>(); bool dbFirst = ParserDataPostgresReader.DbFirstEnabled;
foreach (TeleporterImportFile file in _teleporterConfigurations) bool strictDbOnly = ParserDataPostgresReader.StrictDbOnlyEnabled;
if (dbFirst)
{ {
var teleporters = file.Teleporters.Select(s => try
{ {
s.MapId = file.MapId; allTeleporters = ParserDataPostgresReader.LoadTeleporters();
count++; Log.Info($"[DB_FIRST] Loaded {allTeleporters.Count} map_teleporters from database");
return s.ToDto(); }
}).ToList(); catch (Exception e)
allTeleporters.AddRange(teleporters);
_teleporters[file.MapId] = teleporters;
foreach (TeleporterDTO teleporter in teleporters)
{ {
if (!_teleportersByNpcId.TryGetValue(teleporter.MapNpcId, out List<TeleporterDTO> teleporterDtos)) if (strictDbOnly)
{ {
teleporterDtos = new List<TeleporterDTO>(); throw new InvalidOperationException("DB_FIRST/STRICT_DB_ONLY enabled but failed to load map_teleporters from database.", e);
_teleportersByNpcId[teleporter.MapNpcId] = teleporterDtos;
} }
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); if (allTeleporters == null || allTeleporters.Count == 0)
Log.Info($"[DATABASE] Loaded {count.ToString()} teleporters."); {
allTeleporters = new List<TeleporterDTO>();
foreach (TeleporterImportFile file in _teleporterConfigurations)
{
allTeleporters.AddRange(file.Teleporters.Select(s =>
{
s.MapId = file.MapId;
return s.ToDto();
}));
}
ParserDataPostgresSync.SyncTeleporters(allTeleporters);
}
foreach (IGrouping<int, TeleporterDTO> group in allTeleporters.GroupBy(s => s.MapId))
{
_teleporters[group.Key] = group.ToList();
}
foreach (TeleporterDTO teleporter in allTeleporters)
{
if (!_teleportersByNpcId.TryGetValue(teleporter.MapNpcId, out List<TeleporterDTO> teleporterDtos))
{
teleporterDtos = new List<TeleporterDTO>();
_teleportersByNpcId[teleporter.MapNpcId] = teleporterDtos;
}
teleporterDtos.Add(teleporter);
}
Log.Info($"[DATABASE] Loaded {allTeleporters.Count.ToString()} teleporters.");
} }
public IReadOnlyList<TeleporterDTO> GetTeleportByNpcId(long npcId) => _teleportersByNpcId.GetOrDefault(npcId); public IReadOnlyList<TeleporterDTO> GetTeleportByNpcId(long npcId) => _teleportersByNpcId.GetOrDefault(npcId);
public IReadOnlyList<TeleporterDTO> GetTeleportByMapId(int mapId) => _teleporters.GetOrDefault(mapId); public IReadOnlyList<TeleporterDTO> GetTeleportByMapId(int mapId) => _teleporters.GetOrDefault(mapId);
} }