feat(gameplay): integrate DRMK NPC spawner, dynamic scene presets, pure combat combo router, and clean character switching

This commit is contained in:
CAtlas0784 committed 2026-09-17 22:38:58 +07:00
1 parent 3fd7922ac8
commit b5fc15e4ec
20 files changed
+2329029 -53

No files matched your search

@@ -0,0 +1,68 @@
using System.Text.Json;
using Ananta.Server.Configuration;
namespace Ananta.Server.ClientData.Client4229938;
internal sealed record NpcCatalogEntry4229938(
uint Id,
string Name,
uint GeneralModelId,
uint AgentPersonaId,
uint AnimSetTag,
uint BehaviorActionConfigId,
uint ActionGroupId,
uint AiSettingId,
uint InteractSettingId);
internal static class NpcCatalog4229938
{
private static readonly Lazy<Dictionary<uint, NpcCatalogEntry4229938>> Cache = new(Load);
internal static bool TryGet(uint id, out NpcCatalogEntry4229938 entry)
=> Cache.Value.TryGetValue(id, out entry!);
private static Dictionary<uint, NpcCatalogEntry4229938> Load()
{
var root = PrivateServerConfigStore.ResolveProjectPath(PrivateServerConfigStore.Current.Paths.ClientConfigs);
var path = Path.Combine(root, "AgentConfig.json");
using var doc = JsonDocument.Parse(File.ReadAllText(path));
if (!doc.RootElement.TryGetProperty("records", out var records) || records.ValueKind != JsonValueKind.Array)
throw new InvalidDataException($"AgentConfig does not contain records: {path}");
var result = new Dictionary<uint, NpcCatalogEntry4229938>();
foreach (var row in records.EnumerateArray())
{
if (!row.TryGetProperty("Id", out var idNode) || !idNode.TryGetUInt32(out var id) || id == 0)
continue;
var name = row.TryGetProperty("Name", out var n) && n.ValueKind == JsonValueKind.String ? n.GetString() ?? "" : "";
var model = row.TryGetProperty("GeneralModelId", out var m) && m.TryGetUInt32(out var modelId) ? modelId : 0;
static uint UInt32(JsonElement source, string propertyName)
=> source.TryGetProperty(propertyName, out var node) && node.TryGetUInt32(out var value) ? value : 0;
var sex = UInt32(row, "SexType");
var age = UInt32(row, "Age");
var economicLevel = UInt32(row, "EconomicLevel");
var refined = economicLevel >= 3;
var persona = (sex, age >= 45, refined) switch
{
(1, false, false) when age < 18 => 45200003u,
(2, false, false) when age < 18 => 45200004u,
(1, false, false) => 45200007u,
(2, false, false) => 45200008u,
(1, false, true) => 45200009u,
(2, false, true) => 45200010u,
(1, true, false) => 45200011u,
(2, true, false) => 45200012u,
(1, true, true) => 45200013u,
(2, true, true) => 45200014u,
_ => 0u,
};
var animSetTag = UInt32(row, "AnimStereotype");
if (animSetTag == 0)
animSetTag = UInt32(row, "BattleAnimType");
result[id] = new(id, name, model, persona, animSetTag,
UInt32(row, "Action"), UInt32(row, "ActionGroupId"),
UInt32(row, "AI"), UInt32(row, "InteractSetting"));
}
return result;
}
}
@@ -0,0 +1,66 @@
using System.Text.Json;
using Ananta.Server.Configuration;
namespace Ananta.Server.ClientData.Client4229938;
internal sealed record PoiActionCatalogEntry4229938(uint Id, uint MainActionGroup, uint[] MainActions)
{
internal uint UsageId { get; init; }
internal uint[] BelongingSceneItems { get; init; } = [];
internal uint SelectMainAction(ulong entityId)
=> MainActions.Length == 0 ? 0 : MainActions[(int)(entityId % (ulong)MainActions.Length)];
}
internal static class PoiActionCatalog4229938
{
private static readonly Lazy<Dictionary<uint, PoiActionCatalogEntry4229938>> Cache = new(Load);
internal static bool TryGet(uint id, out PoiActionCatalogEntry4229938 entry)
=> Cache.Value.TryGetValue(id, out entry!);
private static Dictionary<uint, PoiActionCatalogEntry4229938> Load()
{
var root = PrivateServerConfigStore.ResolveProjectPath(PrivateServerConfigStore.Current.Paths.ClientConfigs);
var path = Path.Combine(root, "UrbanDiversityPOIActionConfig.json");
using var doc = JsonDocument.Parse(File.ReadAllText(path));
if (!doc.RootElement.TryGetProperty("records", out var records) || records.ValueKind != JsonValueKind.Array)
throw new InvalidDataException($"UrbanDiversityPOIActionConfig does not contain records: {path}");
var result = new Dictionary<uint, PoiActionCatalogEntry4229938>();
using var usagesDoc = JsonDocument.Parse(File.ReadAllText(Path.Combine(root, "BelongingBelongingUsageConfig.json")));
var usages = usagesDoc.RootElement.GetProperty("records").EnumerateArray()
.ToDictionary(row => row.GetProperty("Id").GetUInt32());
foreach (var row in records.EnumerateArray())
{
if (!row.TryGetProperty("Id", out var idNode) || !idNode.TryGetUInt32(out var id) || id == 0)
continue;
var actionGroup = row.TryGetProperty("MainActionGroup", out var groupNode) && groupNode.TryGetUInt32(out var group)
? group
: 0;
var actions = row.TryGetProperty("MainActions", out var actionsNode) && actionsNode.ValueKind == JsonValueKind.Array
? actionsNode.EnumerateArray()
.Select(node => node.TryGetUInt32(out var action) ? action : 0)
.Where(action => action != 0)
.ToArray()
: [];
var usageId = row.TryGetProperty("UsageId", out var usageNode) ? usageNode.GetUInt32() : 0;
uint[] belongingItems = [];
if (usageId != 0)
{
if (!usages.TryGetValue(usageId, out var usage))
throw new InvalidDataException($"POI {id} refers to missing belonging usage {usageId}");
belongingItems = usage.GetProperty("BelongingSceneItem").EnumerateArray()
.Select(node => node.GetUInt32()).Where(item => item != 0).Distinct().ToArray();
// Type-based usages choose from the NPC's inventory. For standalone
// admin NPCs, seed the usage's configured representative item.
if (belongingItems.Length == 0 && usage.GetProperty("BelongingType").GetArrayLength() != 0)
{
var slotItem = usage.GetProperty("SlotSceneItem").GetUInt32();
if (slotItem != 0) belongingItems = [slotItem];
}
}
result[id] = new(id, actionGroup, actions) { UsageId = usageId, BelongingSceneItems = belongingItems };
}
return result;
}
}
@@ -0,0 +1,107 @@
using System.Text.Json;
using Ananta.Server.Configuration;
namespace Ananta.Server.ClientData.Client4229938;
internal sealed record ScenePresetEntry4229938(
string Key, string Label, uint RaidId, ulong InstanceId, uint UniverseId,
float X, float Y, float Z, float Facing);
/// <summary>
/// Scene ("raid") preset catalog built from the game dumps:
/// RaidConfig.json (raid → SceneId = instance id, Multiverse = universe id) and
/// MapentranceConfig.json (entrance → RaidId + Coordinate/MapTeleportPos + facing).
/// Verified: Nova raid 23300888 → scene/instance 20001222, Lingyun 23300999 → 20001223.
/// </summary>
internal static class SceneCatalog4229938
{
private static readonly Lazy<IReadOnlyList<ScenePresetEntry4229938>> Cache = new(Load);
internal static IReadOnlyList<ScenePresetEntry4229938> Presets => Cache.Value;
internal static bool TryGetByRaid(uint raidId, out ScenePresetEntry4229938 preset)
{
foreach (var p in Cache.Value)
{
if (p.RaidId != raidId)
continue;
preset = p;
return true;
}
preset = null!;
return false;
}
private static IReadOnlyList<ScenePresetEntry4229938> Load()
{
var root = PrivateServerConfigStore.ResolveProjectPath(PrivateServerConfigStore.Current.Paths.ClientConfigs);
var raids = LoadRecords(Path.Combine(root, "RaidConfig.json"));
var entrances = LoadRecords(Path.Combine(root, "MapentranceConfig.json"));
var raidById = new Dictionary<uint, JsonElement>();
foreach (var row in raids)
{
if (row.TryGetProperty("Id", out var id) && id.TryGetUInt32(out var raidId) && raidId != 0)
raidById[raidId] = row;
}
uint defaultUniverse;
try { defaultUniverse = PrivateServerConfigStore.Current.World.UniverseId; }
catch { defaultUniverse = 76000888; }
var result = new List<ScenePresetEntry4229938>();
foreach (var row in entrances)
{
if (!row.TryGetProperty("Id", out var idNode) || !idNode.TryGetUInt32(out var entranceId))
continue;
if (!row.TryGetProperty("RaidId", out var raidNode) || !raidNode.TryGetUInt32(out var raidId) || raidId == 0)
continue;
var coords = row.TryGetProperty("MapTeleportPos", out var tp) && tp.ValueKind == JsonValueKind.Array && tp.GetArrayLength() >= 3
? tp
: row.TryGetProperty("Coordinate", out var c) && c.ValueKind == JsonValueKind.Array && c.GetArrayLength() >= 3 ? c : default;
if (coords.ValueKind != JsonValueKind.Array)
continue;
var xyz = coords.EnumerateArray().Take(3).ToArray();
if (xyz.Any(e => e.ValueKind != JsonValueKind.Number))
continue;
float x = (float)xyz[0].GetDouble(), y = (float)xyz[1].GetDouble(), z = (float)xyz[2].GetDouble();
if (!float.IsFinite(x) || !float.IsFinite(y) || !float.IsFinite(z))
continue;
float facing = row.TryGetProperty("EntranceFacing", out var f) && f.ValueKind == JsonValueKind.Number
? (float)f.GetDouble() : 0f;
ulong instanceId = 0;
uint universeId = defaultUniverse;
if (raidById.TryGetValue(raidId, out var raid))
{
if (raid.TryGetProperty("SceneId", out var s) && s.TryGetUInt64(out var sceneId) && sceneId != 0)
instanceId = sceneId;
if (raid.TryGetProperty("Multiverse", out var m) && m.TryGetUInt32(out var multi) && multi != 0)
universeId = multi;
}
if (instanceId == 0)
continue;
var name = row.TryGetProperty("Name", out var n) && n.ValueKind == JsonValueKind.String
? n.GetString() ?? string.Empty : string.Empty;
if (string.IsNullOrWhiteSpace(name))
name = $"Entrance {entranceId}";
result.Add(new ScenePresetEntry4229938(
$"entrance-{entranceId}", $"{name} (raid {raidId})",
raidId, instanceId, universeId, x, y, z, facing));
}
result.Sort((a, b)
=> a.RaidId != b.RaidId ? a.RaidId.CompareTo(b.RaidId) : string.Compare(a.Label, b.Label, StringComparison.Ordinal));
return result;
}
private static List<JsonElement> LoadRecords(string path)
{
using var doc = JsonDocument.Parse(File.ReadAllText(path));
if (!doc.RootElement.TryGetProperty("records", out var records) || records.ValueKind != JsonValueKind.Array)
throw new InvalidDataException($"Config does not contain records: {path}");
// Clone: the document is disposed on return.
return records.EnumerateArray().Select(e => e.Clone()).ToList();
}
}
@@ -0,0 +1,35 @@
using System.Text.Json;
using Ananta.Server.Configuration;
namespace Ananta.Server.ClientData.Client4229938;
internal sealed record WeatherEntry4229938(uint Id, string Name);
/// <summary>Weather catalog from WeatherConfig.json (5 entries in this build).</summary>
internal static class WeatherCatalog4229938
{
private static readonly Lazy<IReadOnlyList<WeatherEntry4229938>> Cache = new(Load);
internal static IReadOnlyList<WeatherEntry4229938> All => Cache.Value;
private static IReadOnlyList<WeatherEntry4229938> Load()
{
var root = PrivateServerConfigStore.ResolveProjectPath(PrivateServerConfigStore.Current.Paths.ClientConfigs);
var path = Path.Combine(root, "WeatherConfig.json");
if (!File.Exists(path))
return [];
using var doc = JsonDocument.Parse(File.ReadAllText(path));
var result = new List<WeatherEntry4229938>();
if (!doc.RootElement.TryGetProperty("records", out var records) || records.ValueKind != JsonValueKind.Array)
return result;
foreach (var row in records.EnumerateArray())
{
if (!row.TryGetProperty("Id", out var id) || !id.TryGetUInt32(out var weatherId) || weatherId == 0)
continue;
var name = row.TryGetProperty("Name", out var n) && n.ValueKind == JsonValueKind.String
? n.GetString() ?? string.Empty : string.Empty;
result.Add(new WeatherEntry4229938(weatherId, string.IsNullOrWhiteSpace(name) ? $"Weather {weatherId}" : name));
}
return result.OrderBy(w => w.Id).ToList();
}
}