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_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"

View file

@ -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<string> 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);
}
}
}

View file

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

View file

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

View file

@ -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<string> 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);
}
}
}

View file

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

View file

@ -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);
}
}
}
}

View file

@ -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<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');
if (lineSave.Length <= 1)
@ -153,5 +174,38 @@ namespace Plugin.ResourceLoader.Loaders
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.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]))

View file

@ -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;
}
}
}

View file

@ -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,

View file

@ -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);
}
}
}
}

View file

@ -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<long, QuestPrizeDto> dictionaryRewards)
{
using var questRewardStream = new StreamReader(fileRewardsPath, Encoding.GetEncoding(1252));

View file

@ -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<string> currentLine, IReadOnlyCollection<SkillDTO> skills)
{
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.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);
}
}
}
}

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.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<SkillDTO> _skillLoader;
public ResourceFilesPostgresSyncService(ResourceLoadingConfiguration configuration)
public ResourceFilesPostgresSyncService(ResourceLoadingConfiguration configuration, IResourceLoader<SkillDTO> 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>();
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}");
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<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.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<string> 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);
}
}
}

View file

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

View file

@ -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<DropDTO>();
List<DropDTO> drops = null;
bool dbFirst = ParserDataPostgresReader.DbFirstEnabled;
bool strictDbOnly = ParserDataPostgresReader.StrictDbOnlyEnabled;
if (dbFirst)
{
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.");
}
}
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)
{

View file

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using PhoenixLib.Caching;
using PhoenixLib.Logging;
@ -26,8 +27,36 @@ public class ItemBoxManager : IItemBoxManager
public void Initialize()
{
int boxesCount = 0;
var allBoxes = new List<ItemBoxDto>();
List<ItemBoxDto> allBoxes = null;
bool dbFirst = ParserDataPostgresReader.DbFirstEnabled;
bool strictDbOnly = ParserDataPostgresReader.StrictDbOnlyEnabled;
if (dbFirst)
{
try
{
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);
}
if (strictDbOnly && (allBoxes == null || allBoxes.Count == 0))
{
throw new InvalidOperationException("DB_FIRST/STRICT_DB_ONLY enabled but no item_boxes were loaded from database.");
}
}
if (allBoxes == null || allBoxes.Count == 0)
{
allBoxes = new List<ItemBoxDto>();
foreach (ItemBoxImportFile file in _itemBoxConfigurations)
{
ItemBoxDto box = file.ToDto();
@ -36,10 +65,7 @@ public class ItemBoxManager : IItemBoxManager
continue;
}
// just the item box itself
allBoxes.Add(box);
_itemBoxesCache.Set(box.Id.ToString(), box);
boxesCount++;
}
foreach (RandomBoxImportFile file in _randomBoxConfigurations)
@ -53,12 +79,17 @@ public class ItemBoxManager : IItemBoxManager
}
allBoxes.Add(box);
_itemBoxesCache.Set(box.Id.ToString(), box);
boxesCount++;
}
}
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");
}
}

View file

@ -68,6 +68,8 @@ public class MapManager : IMapManager
public async Task Initialize()
{
bool dbFirst = ParserDataPostgresReader.DbFirstEnabled;
bool strictDbOnly = ParserDataPostgresReader.StrictDbOnlyEnabled;
int count = 0;
IEnumerable<MapDataDTO> 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<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)
{
if (!_portalDataByMapId.TryGetValue(portal.SourceMapId, out List<PortalDTO> 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)
{

View file

@ -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<MapMonsterDTO> monsters = null;
bool dbFirst = ParserDataPostgresReader.DbFirstEnabled;
bool strictDbOnly = ParserDataPostgresReader.StrictDbOnlyEnabled;
if (dbFirst)
{
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)

View file

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@ -25,6 +26,35 @@ public class MapNpcManager : IMapNpcManager
}
public async Task InitializeAsync()
{
List<MapNpcDTO> npcs = null;
bool dbFirst = ParserDataPostgresReader.DbFirstEnabled;
bool strictDbOnly = ParserDataPostgresReader.StrictDbOnlyEnabled;
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<MapNpcObject> importedNpcs = _mapNpcConfigurations.SelectMany(x => x.Npcs.Select(s =>
{
@ -32,8 +62,9 @@ public class MapNpcManager : IMapNpcManager
return s;
}));
var npcs = importedNpcs.Select(s => s.ToDto()).ToList();
npcs = importedNpcs.Select(s => s.ToDto()).ToList();
ParserDataPostgresSync.SyncMapNpcs(npcs);
}
int count = 0;
foreach (MapNpcDTO npcDto in npcs)

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();
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);

View file

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@ -32,7 +33,36 @@ public class RecipeManager : IRecipeManager
public async Task InitializeAsync()
{
var recipes = new List<RecipeDTO>();
List<RecipeDTO> recipes = null;
bool dbFirst = ParserDataPostgresReader.DbFirstEnabled;
bool strictDbOnly = ParserDataPostgresReader.StrictDbOnlyEnabled;
if (dbFirst)
{
try
{
recipes = ParserDataPostgresReader.LoadRecipes();
Log.Info($"[DB_FIRST] Loaded {recipes.Count} recipes from database");
}
catch (Exception e)
{
if (strictDbOnly)
{
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);
}
if (strictDbOnly && (recipes == null || recipes.Count == 0))
{
throw new InvalidOperationException("DB_FIRST/STRICT_DB_ONLY enabled but no recipes were loaded from database.");
}
}
if (recipes == null || recipes.Count == 0)
{
recipes = new List<RecipeDTO>();
foreach (RecipeObject recipeObject in _files.SelectMany(x => x.Recipes))
{
if (recipeObject == null)
@ -78,6 +108,7 @@ public class RecipeManager : IRecipeManager
}
ParserDataPostgresSync.SyncRecipes(recipes);
}
int count = 0;
foreach (RecipeDTO recipe in recipes)

View file

@ -34,6 +34,35 @@ public class ShopManager : IShopManager
}
public async Task InitializeAsync()
{
List<ShopDTO> allShops = null;
bool dbFirst = ParserDataPostgresReader.DbFirstEnabled;
bool strictDbOnly = ParserDataPostgresReader.StrictDbOnlyEnabled;
if (dbFirst)
{
try
{
allShops = ParserDataPostgresReader.LoadShops();
Log.Info($"[DB_FIRST] Loaded {allShops.Count} shops from database");
}
catch (Exception 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.");
}
}
if (allShops == null || allShops.Count == 0)
{
IEnumerable<MapNpcObject> importedNpcs = _importFile.SelectMany(x => x.Npcs.Select(s =>
{
@ -41,10 +70,7 @@ public class ShopManager : IShopManager
return s;
})).ToList();
int shopItemsCount = 0;
int shopSkillsCount = 0;
int count = 0;
var allShops = new List<ShopDTO>();
allShops = new List<ShopDTO>();
foreach (MapNpcObject npc in importedNpcs)
{
@ -89,10 +115,6 @@ public class ShopManager : IShopManager
}
allShops.Add(shop);
_shopsByNpcId.Set(shop.MapNpcId, _shopFactory.CreateShop(shop));
shopItemsCount += shop.Items?.Count ?? 0;
shopSkillsCount += shop.Skills?.Count ?? 0;
count++;
}
catch (Exception e)
{
@ -101,7 +123,18 @@ public class ShopManager : IShopManager
}
ParserDataPostgresSync.SyncShops(allShops);
Log.Info($"[SHOP_MANAGER] Loaded {count.ToString()} shops.");
}
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.");
}

View file

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