Persist minigame, gameevent and resource files (dat/lang) to PostgreSQL

This commit is contained in:
nizar 2026-02-24 10:47:01 +01:00
parent 59c2c9796e
commit aafa4585f9
8 changed files with 286 additions and 0 deletions

View file

@ -5,6 +5,7 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using Plugin.ResourceLoader.Loaders;
using Plugin.ResourceLoader.Services;
using WingsAPI.Data.ActDesc;
using WingsAPI.Data.GameData;
using WingsAPI.Plugins;
@ -40,6 +41,7 @@ namespace Plugin.ResourceLoader
services.AddSingleton<IGameDataLanguageService, InMemoryGameDataLanguageService>();
services.AddSingleton<IBattleEntityAlgorithmService, BattleEntityAlgorithmService>();
services.AddHostedService<ResourceFilesPostgresSyncService>();
}
}
@ -51,6 +53,7 @@ namespace Plugin.ResourceLoader
{
services.AddSingleton<IResourceLoader<GenericTranslationDto>, GenericTranslationGrpcLoader>();
services.AddSingleton<IGameLanguageService, InMemoryMultilanguageService>();
services.AddHostedService<ResourceFilesPostgresSyncService>();
}
}
}

View file

@ -5,6 +5,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
<PackageReference Include="Npgsql" Version="5.0.18" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\PhoenixLib.Caching\PhoenixLib.Caching.csproj" />
<ProjectReference Include="..\..\PhoenixLib.Multilanguage\PhoenixLib.Multilanguage.csproj" />

View file

@ -0,0 +1,118 @@
using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Npgsql;
using PhoenixLib.Logging;
namespace Plugin.ResourceLoader.Services
{
public class ResourceFilesPostgresSyncService : IHostedService
{
private readonly ResourceLoadingConfiguration _configuration;
public ResourceFilesPostgresSyncService(ResourceLoadingConfiguration configuration)
{
_configuration = configuration;
}
public Task StartAsync(CancellationToken cancellationToken)
{
if (string.Equals(Environment.GetEnvironmentVariable("PARSER_DB_SYNC"), "false", StringComparison.OrdinalIgnoreCase))
{
return Task.CompletedTask;
}
try
{
string host = Environment.GetEnvironmentVariable("DATABASE_IP")
?? Environment.GetEnvironmentVariable("POSTGRES_DATABASE_IP")
?? "127.0.0.1";
string port = Environment.GetEnvironmentVariable("DATABASE_PORT")
?? Environment.GetEnvironmentVariable("POSTGRES_DATABASE_PORT")
?? "5432";
string db = Environment.GetEnvironmentVariable("DATABASE_NAME")
?? Environment.GetEnvironmentVariable("POSTGRES_DATABASE_NAME")
?? "game";
string user = Environment.GetEnvironmentVariable("DATABASE_USER")
?? Environment.GetEnvironmentVariable("POSTGRES_DATABASE_USER")
?? "postgres";
string pass = Environment.GetEnvironmentVariable("DATABASE_PASSWORD")
?? Environment.GetEnvironmentVariable("POSTGRES_DATABASE_PASSWORD")
?? "postgres";
string root = _configuration.ResourcePaths;
string datPath = _configuration.GameDataPath;
string langPath = _configuration.GameLanguagePath;
string[] datFiles = Directory.Exists(datPath)
? Directory.GetFiles(datPath, "*", SearchOption.AllDirectories)
: Array.Empty<string>();
string[] langFiles = Directory.Exists(langPath)
? Directory.GetFiles(langPath, "*", SearchOption.AllDirectories)
: Array.Empty<string>();
using var conn = new NpgsqlConnection($"Host={host};Port={port};Database={db};Username={user};Password={pass}");
conn.Open();
using var tx = conn.BeginTransaction();
using (var cmd = new NpgsqlCommand(@"CREATE TABLE IF NOT EXISTS resource_files (
id BIGSERIAL PRIMARY KEY,
category TEXT NOT NULL,
relative_path TEXT NOT NULL,
sha256 TEXT NOT NULL,
content BYTEA NOT NULL,
size_bytes INT NOT NULL,
synced_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE(category, relative_path)
);", conn, tx))
{
cmd.ExecuteNonQuery();
}
UpsertFiles(conn, tx, root, "dat", datFiles);
UpsertFiles(conn, tx, root, "lang", langFiles);
tx.Commit();
Log.Info($"[PARSER_DB_SYNC] Synced resource_files dat={datFiles.Length} lang={langFiles.Length}");
}
catch (Exception ex)
{
Log.Error("[PARSER_DB_SYNC] Failed to sync resource files", ex);
}
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
private static void UpsertFiles(NpgsqlConnection conn, NpgsqlTransaction tx, string root, string category, string[] files)
{
foreach (string file in files.Where(File.Exists))
{
byte[] content = File.ReadAllBytes(file);
string hash;
using (SHA256 sha = SHA256.Create())
{
hash = Convert.ToHexString(sha.ComputeHash(content));
}
string relative = Path.GetRelativePath(root, file).Replace('\\', '/');
using var cmd = new NpgsqlCommand(@"INSERT INTO resource_files(category,relative_path,sha256,content,size_bytes,synced_at)
VALUES (@category,@path,@sha,@content,@size,NOW())
ON CONFLICT (category, relative_path)
DO UPDATE SET sha256=EXCLUDED.sha256, content=EXCLUDED.content, size_bytes=EXCLUDED.size_bytes, synced_at=NOW();", conn, tx);
cmd.Parameters.AddWithValue("category", category);
cmd.Parameters.AddWithValue("path", relative);
cmd.Parameters.AddWithValue("sha", hash);
cmd.Parameters.AddWithValue("content", content);
cmd.Parameters.AddWithValue("size", content.Length);
cmd.ExecuteNonQuery();
}
}
}
}

View file

@ -7,6 +7,7 @@ using WingsEmu.Game.Characters.Events;
using WingsEmu.Game.Configurations.Miniland;
using WingsEmu.Game.Miniland;
using WingsEmu.Game.Networking;
using WingsEmu.Plugins.BasicImplementations.ServerConfigs.Persistence;
namespace WingsEmu.Plugins.BasicImplementations.Managers;
@ -20,11 +21,14 @@ public class MinigameManager : IMinigameManager
{
_minigameConfiguration = minigameConfiguration;
_lockService = lockService;
InitializePersistence();
}
public async Task<bool> CanRefreshMinigamesFreeProductionPoints(long characterId) =>
await _lockService.TryAddTemporaryLockAsync($"game:locks:minigame-refresh:{characterId}", DateTime.UtcNow.Date.AddDays(1));
public void InitializePersistence() => ParserDataPostgresSync.SyncMinigames(_minigameConfiguration);
public MinigameScoresHolder GetScores(int minigameVnum)
{

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using Npgsql;
using PhoenixLib.Logging;
using WingsAPI.Data.Drops;
@ -8,6 +9,7 @@ using WingsEmu.DTOs.Maps;
using WingsEmu.DTOs.Recipes;
using WingsEmu.DTOs.ServerDatas;
using WingsEmu.DTOs.Shops;
using WingsEmu.Game.Configurations.Miniland;
namespace WingsEmu.Plugins.BasicImplementations.ServerConfigs.Persistence;
@ -492,6 +494,77 @@ VALUES (@id,@amount,@chance,@item,@map,@mon,@race,@subrace);", conn, tx);
}
}
public static void SyncMinigames(MinigameConfiguration minigameConfiguration)
{
if (!Enabled || minigameConfiguration == null) return;
try
{
using var conn = new NpgsqlConnection(BuildConnectionString());
conn.Open();
using var tx = conn.BeginTransaction();
using (var cmd = new NpgsqlCommand(@"CREATE TABLE IF NOT EXISTS minigame_config (
id BIGSERIAL PRIMARY KEY,
minigame_vnum INT,
minigame_type INT,
minimum_level INT,
minimum_reputation INT,
rewards_json JSONB
);
CREATE TABLE IF NOT EXISTS minigame_scores_holders (
id BIGSERIAL PRIMARY KEY,
minigame_type INT,
scores_json JSONB
);
CREATE TABLE IF NOT EXISTS global_minigame_config (
id BIGSERIAL PRIMARY KEY,
config_json JSONB
);", conn, tx)) cmd.ExecuteNonQuery();
using (var cmd = new NpgsqlCommand("TRUNCATE TABLE minigame_config, minigame_scores_holders, global_minigame_config RESTART IDENTITY;", conn, tx)) cmd.ExecuteNonQuery();
if (minigameConfiguration.Minigames != null)
{
foreach (Minigame m in minigameConfiguration.Minigames)
{
using var cmd = new NpgsqlCommand(@"INSERT INTO minigame_config(minigame_vnum,minigame_type,minimum_level,minimum_reputation,rewards_json)
VALUES (@vnum,@type,@minLvl,@minRep,@rewards::jsonb);", conn, tx);
cmd.Parameters.AddWithValue("vnum", m.Vnum);
cmd.Parameters.AddWithValue("type", (int)m.Type);
cmd.Parameters.AddWithValue("minLvl", m.MinimumLevel);
cmd.Parameters.AddWithValue("minRep", m.MinimumReputation);
cmd.Parameters.AddWithValue("rewards", JsonSerializer.Serialize(m.Rewards));
cmd.ExecuteNonQuery();
}
}
if (minigameConfiguration.ScoresHolders != null)
{
foreach (MinigameScoresHolder s in minigameConfiguration.ScoresHolders)
{
using var cmd = new NpgsqlCommand(@"INSERT INTO minigame_scores_holders(minigame_type,scores_json)
VALUES (@type,@scores::jsonb);", conn, tx);
cmd.Parameters.AddWithValue("type", (int)s.Type);
cmd.Parameters.AddWithValue("scores", JsonSerializer.Serialize(s.Scores));
cmd.ExecuteNonQuery();
}
}
using (var cmd = new NpgsqlCommand("INSERT INTO global_minigame_config(config_json) VALUES (@cfg::jsonb);", conn, tx))
{
cmd.Parameters.AddWithValue("cfg", JsonSerializer.Serialize(minigameConfiguration.Configuration));
cmd.ExecuteNonQuery();
}
tx.Commit();
Log.Info($"[PARSER_DB_SYNC] Synced minigames={minigameConfiguration.Minigames?.Count ?? 0} holders={minigameConfiguration.ScoresHolders?.Count ?? 0}");
}
catch (Exception ex)
{
Log.Error("[PARSER_DB_SYNC] Failed to sync minigames", ex);
}
}
public static void SyncRecipes(IReadOnlyList<RecipeDTO> recipes)
{
if (!Enabled) return;

View file

@ -20,6 +20,7 @@ using WingsEmu.Plugins.GameEvents.Configuration.InstantBattle;
using WingsEmu.Plugins.GameEvents.Consumers;
using WingsEmu.Plugins.GameEvents.Matchmaking.Matchmaker;
using WingsEmu.Plugins.GameEvents.RecurrentJob;
using WingsEmu.Plugins.GameEvents.Services;
namespace WingsEmu.Plugins.GameEvents
{
@ -59,6 +60,8 @@ namespace WingsEmu.Plugins.GameEvents
{
[GameEventType.InstantBattle] = new InstantBattleMatchmaker(s.GetService<IGlobalInstantBattleConfiguration>())
}));
services.AddHostedService<GameEventConfigPostgresSyncService>();
}
}
}

View file

@ -0,0 +1,79 @@
using System;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Npgsql;
using PhoenixLib.Logging;
using WingsEmu.Plugins.GameEvents.Configuration.InstantBattle;
namespace WingsEmu.Plugins.GameEvents.Services
{
public class GameEventConfigPostgresSyncService : IHostedService
{
private readonly IGlobalInstantBattleConfiguration _configuration;
public GameEventConfigPostgresSyncService(IGlobalInstantBattleConfiguration configuration)
{
_configuration = configuration;
}
public Task StartAsync(CancellationToken cancellationToken)
{
try
{
string host = Environment.GetEnvironmentVariable("DATABASE_IP")
?? Environment.GetEnvironmentVariable("POSTGRES_DATABASE_IP")
?? "127.0.0.1";
string port = Environment.GetEnvironmentVariable("DATABASE_PORT")
?? Environment.GetEnvironmentVariable("POSTGRES_DATABASE_PORT")
?? "5432";
string db = Environment.GetEnvironmentVariable("DATABASE_NAME")
?? Environment.GetEnvironmentVariable("POSTGRES_DATABASE_NAME")
?? "game";
string user = Environment.GetEnvironmentVariable("DATABASE_USER")
?? Environment.GetEnvironmentVariable("POSTGRES_DATABASE_USER")
?? "postgres";
string pass = Environment.GetEnvironmentVariable("DATABASE_PASSWORD")
?? Environment.GetEnvironmentVariable("POSTGRES_DATABASE_PASSWORD")
?? "postgres";
using var conn = new NpgsqlConnection($"Host={host};Port={port};Database={db};Username={user};Password={pass}");
conn.Open();
using var tx = conn.BeginTransaction();
using (var cmd = new NpgsqlCommand(@"CREATE TABLE IF NOT EXISTS gameevent_instant_battle_configs (
id BIGSERIAL PRIMARY KEY,
map_id INT,
game_event_type INT,
config_json JSONB
);", conn, tx)) cmd.ExecuteNonQuery();
using (var cmd = new NpgsqlCommand("TRUNCATE TABLE gameevent_instant_battle_configs RESTART IDENTITY;", conn, tx)) cmd.ExecuteNonQuery();
var configs = (_configuration as GlobalInstantBattleConfiguration)?.Configurations ?? Enumerable.Empty<InstantBattleConfiguration>();
foreach (InstantBattleConfiguration c in configs)
{
using var cmd = new NpgsqlCommand("INSERT INTO gameevent_instant_battle_configs(map_id,game_event_type,config_json) VALUES (@map,@type,@cfg::jsonb);", conn, tx);
cmd.Parameters.AddWithValue("map", c.MapId);
cmd.Parameters.AddWithValue("type", (int)c.GameEventType);
cmd.Parameters.AddWithValue("cfg", JsonSerializer.Serialize(c));
cmd.ExecuteNonQuery();
}
tx.Commit();
Log.Info($"[PARSER_DB_SYNC] Synced instant_battle_configs={configs.Count()}");
}
catch (Exception ex)
{
Log.Error("[PARSER_DB_SYNC] Failed to sync gameevents", ex);
}
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
}

View file

@ -8,6 +8,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
<PackageReference Include="Npgsql" Version="5.0.18" />
<ProjectReference Include="..\..\PhoenixLib.Configuration\PhoenixLib.Configuration.csproj" />
<ProjectReference Include="..\..\Plugin.RainbowBattle\Plugin.RainbowBattle.csproj" />