feat: Add metro transit, gadgets, shop, social apps, and traffic models checkpoint
This commit is contained in:
1 parent
caa42b84ed
commit
74e2963a12
79 files changed
+527962
-496
No files matched your search
@@ -0,0 +1,122 @@
|
||||
using System.Text.Json;
|
||||
using Ananta.Server.Configuration;
|
||||
|
||||
namespace Ananta.Server.ClientData.Client4229938;
|
||||
|
||||
internal static class BehaviorTaskCatalog
|
||||
{
|
||||
internal sealed record BehaviorTask(
|
||||
uint Id,
|
||||
int AnimState,
|
||||
int BehaviorLayer,
|
||||
int ActionSelector,
|
||||
int PostureTag,
|
||||
IReadOnlyList<int> MainActionIds);
|
||||
|
||||
private static readonly Lazy<IReadOnlyList<BehaviorTask>> Cache = new(Load);
|
||||
|
||||
internal static IReadOnlyList<BehaviorTask> All => Cache.Value;
|
||||
|
||||
internal static int Count => Cache.Value.Count;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
internal static BehaviorTask? FirstByAnimState(int animState)
|
||||
=> Cache.Value.FirstOrDefault(t => t.AnimState == animState && t.MainActionIds.Count > 0);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
internal static uint CrowdTaskId => FirstByAnimState(1)?.Id ?? 0;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
internal static uint VehicleNpcTaskId => FirstByAnimState(3)?.Id ?? 0;
|
||||
|
||||
private static IReadOnlyList<BehaviorTask> Load()
|
||||
{
|
||||
var result = new List<BehaviorTask>(512);
|
||||
try
|
||||
{
|
||||
var root = PrivateServerConfigStore.ResolveProjectPath(
|
||||
PrivateServerConfigStore.Current.Paths.ClientConfigs);
|
||||
var path = Path.Combine(root, "AetherNpcBehaviorTaskDefineConfig.json");
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
Console.WriteLine($"[BEHAVIOR] 找不到 {path}(行为任务自动挑选将不可用)");
|
||||
return result;
|
||||
}
|
||||
|
||||
using var doc = JsonDocument.Parse(File.ReadAllText(path));
|
||||
if (!doc.RootElement.TryGetProperty("records", out var recs)
|
||||
|| recs.ValueKind != JsonValueKind.Array)
|
||||
return result;
|
||||
|
||||
foreach (var r in recs.EnumerateArray())
|
||||
{
|
||||
if (!r.TryGetProperty("Id", out var idNode) || !idNode.TryGetUInt32(out var id) || id == 0)
|
||||
continue;
|
||||
|
||||
var mains = new List<int>(8);
|
||||
if (r.TryGetProperty("MainActionIds", out var m)
|
||||
&& m.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var x in m.EnumerateArray())
|
||||
if (x.ValueKind == JsonValueKind.Number && x.TryGetInt32(out var v))
|
||||
mains.Add(v);
|
||||
}
|
||||
|
||||
result.Add(new BehaviorTask(
|
||||
id,
|
||||
r.TryGetProperty("AnimState", out var a) && a.TryGetInt32(out var av) ? av : -1,
|
||||
r.TryGetProperty("BehaviorLayer", out var l) && l.TryGetInt32(out var lv) ? lv : -1,
|
||||
r.TryGetProperty("ActionSelector", out var s) && s.TryGetInt32(out var sv) ? sv : -1,
|
||||
r.TryGetProperty("BehaviorPostureGameplayTag", out var p) && p.TryGetInt32(out var pv) ? pv : -1,
|
||||
mains));
|
||||
}
|
||||
|
||||
result.Sort(static (a, b) => a.Id.CompareTo(b.Id));
|
||||
|
||||
|
||||
|
||||
Console.WriteLine($"[BEHAVIOR] AetherNpcBehaviorTaskDefineConfig 已载入 {result.Count} 条"
|
||||
+ $"(带动作 {result.Count(static t => t.MainActionIds.Count > 0)} 条)");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[BEHAVIOR] 载入失败: {ex.Message}");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
internal static (int Count, uint Crowd, uint Vehicle, string Detail) Probe()
|
||||
{
|
||||
var all = All;
|
||||
var byState = all.GroupBy(static t => t.AnimState)
|
||||
.OrderBy(static g => g.Key)
|
||||
.Select(static g => $"{g.Key}:{g.Count()}");
|
||||
var lines = new List<string>
|
||||
{
|
||||
" AnimState 分布(None0/Default1/Sitting2/Driving3/Riding4/Impacted5/Interacting6/Sick7) = "
|
||||
+ string.Join(" ", byState),
|
||||
};
|
||||
foreach (var st in new[] { 1, 3, 5, 6 })
|
||||
{
|
||||
var t = FirstByAnimState(st);
|
||||
if (t is null) { lines.Add($" AnimState={st} → (无)"); continue; }
|
||||
lines.Add($" AnimState={st} → Id={t.Id} Layer={t.BehaviorLayer} "
|
||||
+ $"Selector={t.ActionSelector} Posture={t.PostureTag} "
|
||||
+ $"MainActionIds=[{string.Join(",", t.MainActionIds.Take(4))}]");
|
||||
}
|
||||
return (all.Count, CrowdTaskId, VehicleNpcTaskId, string.Join('\n', lines));
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,15 @@ internal static class EconomyConfigRepository
|
||||
internal static bool TryPool(uint id, out GachaPool pool)
|
||||
=> Cache.Value.Pools.TryGetValue(id, out pool!);
|
||||
|
||||
/// <summary>Distinct non-zero gacha ticket item ids (pool MoneyId) across every pool.</summary>
|
||||
internal static IReadOnlyCollection<uint> GachaTicketItemIds()
|
||||
=> Cache.Value.Pools.Values
|
||||
.Select(p => p.MoneyId)
|
||||
.Where(id => id != 0)
|
||||
.Distinct()
|
||||
.OrderBy(id => id)
|
||||
.ToArray();
|
||||
|
||||
internal static bool TryTierRule(uint id, out GachaTierRule rule)
|
||||
=> Cache.Value.TierRules.TryGetValue(id, out rule!);
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
using System.Text.Json;
|
||||
using Ananta.Server.Configuration;
|
||||
|
||||
namespace Ananta.Server.ClientData.Client4229938;
|
||||
|
||||
/// <summary>
|
||||
/// Chinese → English name mapping for the debug panel spawn lists. The tables are generated
|
||||
/// by VaultTool/build_translation.py (word segmentation + single-char fallback) over the
|
||||
/// AgentConfig / UrbanDiversityPOIActionConfig name columns; unknown names fall back to the
|
||||
/// original string.
|
||||
/// </summary>
|
||||
internal static class NameTranslator
|
||||
{
|
||||
private static readonly Lazy<Dictionary<string, string>> Agents = new(() => Load("agent_names_en.json"));
|
||||
private static readonly Lazy<Dictionary<string, string>> PoiActions = new(() => Load("poi_actions_en.json"));
|
||||
|
||||
public static string Agent(string? name)
|
||||
=> Translate(Agents.Value, name);
|
||||
|
||||
public static string PoiAction(string? name)
|
||||
=> Translate(PoiActions.Value, name);
|
||||
|
||||
private static string Translate(Dictionary<string, string> table, string? name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return name ?? string.Empty;
|
||||
return table.TryGetValue(name, out var en) && !string.IsNullOrWhiteSpace(en) ? en : name;
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> Load(string file)
|
||||
{
|
||||
try
|
||||
{
|
||||
var root = PrivateServerConfigStore.ResolveProjectPath(PrivateServerConfigStore.Current.Paths.ClientConfigs);
|
||||
var path = Path.Combine(root, file);
|
||||
if (!File.Exists(path))
|
||||
return [];
|
||||
using var doc = JsonDocument.Parse(File.ReadAllText(path));
|
||||
var dict = new Dictionary<string, string>();
|
||||
foreach (var p in doc.RootElement.EnumerateObject())
|
||||
dict[p.Name] = p.Value.GetString() ?? string.Empty;
|
||||
return dict;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,10 @@ internal static class NpcCatalog4229938
|
||||
if (list.Count >= limit) break;
|
||||
continue;
|
||||
}
|
||||
if (!entry.Name.Contains(q!, StringComparison.OrdinalIgnoreCase) &&
|
||||
// Match the translated English name, the original (Chinese) name and the id.
|
||||
var translated = NameTranslator.Agent(entry.Name);
|
||||
if (!translated.Contains(q!, StringComparison.OrdinalIgnoreCase) &&
|
||||
!entry.Name.Contains(q!, StringComparison.OrdinalIgnoreCase) &&
|
||||
!entry.Id.ToString().Contains(q!))
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace Ananta.Server.ClientData.Client4229938;
|
||||
|
||||
internal sealed record PoiActionCatalogEntry4229938(uint Id, uint MainActionGroup, uint[] MainActions)
|
||||
{
|
||||
internal string Name { get; init; } = "";
|
||||
internal uint UsageId { get; init; }
|
||||
internal uint[] BelongingSceneItems { get; init; } = [];
|
||||
internal uint SelectMainAction(ulong entityId)
|
||||
@@ -18,6 +19,9 @@ internal static class PoiActionCatalog4229938
|
||||
internal static bool TryGet(uint id, out PoiActionCatalogEntry4229938 entry)
|
||||
=> Cache.Value.TryGetValue(id, out entry!);
|
||||
|
||||
/// <summary>Every row of UrbanDiversityPOIActionConfig, for the panel's POI picker.</summary>
|
||||
internal static IReadOnlyCollection<PoiActionCatalogEntry4229938> All() => Cache.Value.Values;
|
||||
|
||||
private static Dictionary<uint, PoiActionCatalogEntry4229938> Load()
|
||||
{
|
||||
var root = PrivateServerConfigStore.ResolveProjectPath(PrivateServerConfigStore.Current.Paths.ClientConfigs);
|
||||
@@ -59,7 +63,12 @@ internal static class PoiActionCatalog4229938
|
||||
if (slotItem != 0) belongingItems = [slotItem];
|
||||
}
|
||||
}
|
||||
result[id] = new(id, actionGroup, actions) { UsageId = usageId, BelongingSceneItems = belongingItems };
|
||||
result[id] = new(id, actionGroup, actions)
|
||||
{
|
||||
Name = row.TryGetProperty("Name", out var nameNode) && nameNode.ValueKind == JsonValueKind.String ? nameNode.GetString() ?? "" : "",
|
||||
UsageId = usageId,
|
||||
BelongingSceneItems = belongingItems,
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
using System.Text.Json;
|
||||
using Ananta.Server.Configuration;
|
||||
|
||||
namespace Ananta.Server.ClientData.Client4229938;
|
||||
|
||||
/// <summary>One RailLineConfig row (a physical light-rail / cruise line, and its direction).</summary>
|
||||
internal sealed record RailLine4229938(
|
||||
uint Id,
|
||||
string LineName,
|
||||
uint TrainTemplateId,
|
||||
string PrefabPath,
|
||||
string BlobName,
|
||||
bool RingLine,
|
||||
bool IsTestPath,
|
||||
int DepartureInterval,
|
||||
int MaxTrainAmount,
|
||||
double TotalTravelDuration,
|
||||
double DestinationArrivalTime,
|
||||
int AvgVelocity,
|
||||
bool FirstStopDumb,
|
||||
bool LastStopDumb,
|
||||
int FirstTrainHour,
|
||||
int FirstTrainMinute,
|
||||
int LastTrainHour,
|
||||
int LastTrainMinute)
|
||||
{
|
||||
/// <summary>
|
||||
/// How many carriages the client should keep alive on this line. The timetable repeats every
|
||||
/// TotalTravelDuration seconds and a train leaves every DepartureInterval, so the steady-state
|
||||
/// fleet size is the ratio of the two - capped by the config's own MaxTrainAmount.
|
||||
/// </summary>
|
||||
internal int EffectiveTrainCount
|
||||
{
|
||||
get
|
||||
{
|
||||
if (TotalTravelDuration <= 0 || DepartureInterval <= 0)
|
||||
return Math.Clamp(MaxTrainAmount, 1, 32);
|
||||
var byInterval = (int)Math.Floor(TotalTravelDuration / DepartureInterval);
|
||||
var cap = MaxTrainAmount > 0 ? MaxTrainAmount : 32;
|
||||
return Math.Clamp(byInterval, 1, Math.Clamp(cap, 1, 32));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One RailStopConfig row (a scheduled call of one line). Note that the JSON field literally named
|
||||
/// "LineName" holds the <em>stop</em> name (e.g. 白帆湾车库 for the depot stop), not the line name.
|
||||
/// </summary>
|
||||
internal sealed record RailStop4229938(
|
||||
uint Id,
|
||||
uint LineId,
|
||||
string StopName,
|
||||
uint NextStopId,
|
||||
double ArrivalTime,
|
||||
double InsertTime,
|
||||
uint GateOpenSide,
|
||||
bool DefaultExitStop,
|
||||
uint StationId);
|
||||
|
||||
/// <summary>One RailStationConfig row (a platform shared by one or more lines).</summary>
|
||||
internal sealed record RailStation4229938(
|
||||
uint Id,
|
||||
string Name,
|
||||
string ChineseName,
|
||||
string EnglishName,
|
||||
double X,
|
||||
double Y,
|
||||
double Z,
|
||||
uint[] Lines)
|
||||
{
|
||||
/// <summary>Localised display name, falling back to the English/raw name.</summary>
|
||||
internal string DisplayName(bool chinese)
|
||||
{
|
||||
if (chinese)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(ChineseName)) return ChineseName;
|
||||
if (!string.IsNullOrWhiteSpace(Name)) return Name;
|
||||
return EnglishName ?? string.Empty;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(EnglishName)) return EnglishName;
|
||||
if (!string.IsNullOrWhiteSpace(Name)) return Name;
|
||||
return ChineseName ?? string.Empty;
|
||||
}
|
||||
|
||||
internal double DistanceTo(RailStation4229938 other)
|
||||
{
|
||||
var dx = X - other.X;
|
||||
var dy = Y - other.Y;
|
||||
var dz = Z - other.Z;
|
||||
return Math.Sqrt(dx * dx + dy * dy + dz * dz);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A line with its stops already ordered along the travel direction, so the nth entry of
|
||||
/// <see cref="Stops"/> is the nth station of <see cref="RailLine4229938"/>'s own Stations array.
|
||||
/// </summary>
|
||||
internal sealed record RailRoute4229938(RailLine4229938 Line, IReadOnlyList<RailStop4229938> Stops);
|
||||
|
||||
/// <summary>Where a train is right now, derived from ElapsedTime + the baked timetable.</summary>
|
||||
internal sealed record RailTrainProgress(
|
||||
int StopIndex,
|
||||
int NextStopIndex,
|
||||
RailStop4229938 CurrentStop,
|
||||
RailStop4229938? NextStop,
|
||||
RailStation4229938? CurrentStation,
|
||||
RailStation4229938? NextStation,
|
||||
double SecondsToNext,
|
||||
double MetresToNext,
|
||||
bool AtStation);
|
||||
|
||||
/// <summary>
|
||||
/// Build-4229938 light-rail / cruise catalog, read from the Rail*Config.json files in the configured
|
||||
/// client-data directory. Field names verified against ConfigDump_v3 and the Lua
|
||||
/// Auto.WriteMetroClientInfo writer in RPCSerializeAuto.lua.
|
||||
/// </summary>
|
||||
internal static class RailCatalog4229938
|
||||
{
|
||||
private static readonly Lazy<Dictionary<uint, RailLine4229938>> LineCache = new(LoadLines);
|
||||
private static readonly Lazy<Dictionary<uint, RailStation4229938>> StationCache = new(LoadStations);
|
||||
private static readonly Lazy<Dictionary<uint, RailRoute4229938>> RouteCache = new(LoadRoutes);
|
||||
|
||||
internal static IReadOnlyList<RailLine4229938> AllLines => LineCache.Value.Values
|
||||
.OrderBy(l => l.Id)
|
||||
.ToArray();
|
||||
|
||||
internal static bool TryGetLine(uint id, out RailLine4229938 line)
|
||||
=> LineCache.Value.TryGetValue(id, out line!);
|
||||
|
||||
internal static bool TryGetStation(uint id, out RailStation4229938 station)
|
||||
=> StationCache.Value.TryGetValue(id, out station!);
|
||||
|
||||
internal static bool TryGetRoute(uint lineId, out RailRoute4229938 route)
|
||||
=> RouteCache.Value.TryGetValue(lineId, out route!);
|
||||
|
||||
internal static IReadOnlyList<RailStation4229938> AllStations => StationCache.Value.Values
|
||||
.OrderBy(s => s.Id)
|
||||
.ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// Position of train <paramref name="trainIndex"/> of <paramref name="line"/> at
|
||||
/// <paramref name="elapsedSeconds"/> seconds into the day's service. Trains are evenly spaced by
|
||||
/// DepartureInterval and wrap around TotalTravelDuration, which is what the stops' ArrivalTime
|
||||
/// values are measured against (the last stop's arrival equals the total travel duration).
|
||||
/// </summary>
|
||||
internal static double ElapsedForTrain(RailLine4229938 line, int trainIndex, double elapsedSeconds)
|
||||
{
|
||||
var total = line.TotalTravelDuration;
|
||||
if (total <= 0) return 0;
|
||||
var phase = elapsedSeconds + (trainIndex * (double)line.DepartureInterval);
|
||||
var wrapped = phase % total;
|
||||
return wrapped < 0 ? wrapped + total : wrapped;
|
||||
}
|
||||
|
||||
/// <summary>Resolves "where is the train" from its ElapsedTime and the baked stop timetable.</summary>
|
||||
internal static RailTrainProgress? ProgressAt(RailLine4229938 line, double elapsed)
|
||||
{
|
||||
if (!TryGetRoute(line.Id, out var route) || route.Stops.Count == 0)
|
||||
return null;
|
||||
|
||||
var stops = route.Stops;
|
||||
var index = 0;
|
||||
for (var i = 0; i < stops.Count; i++)
|
||||
{
|
||||
if (stops[i].ArrivalTime <= elapsed + 0.001)
|
||||
index = i;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
var current = stops[index];
|
||||
var nextIndex = index + 1 < stops.Count ? index + 1 : (line.RingLine ? 0 : -1);
|
||||
var next = nextIndex >= 0 ? stops[nextIndex] : null;
|
||||
|
||||
TryGetStation(current.StationId, out var currentStation);
|
||||
RailStation4229938? nextStation = null;
|
||||
if (next is not null) TryGetStation(next.StationId, out nextStation);
|
||||
|
||||
var secondsToNext = next is null ? 0 : Math.Max(0, next.ArrivalTime - elapsed);
|
||||
var metresToNext = 0d;
|
||||
if (next is not null)
|
||||
{
|
||||
var leg = next.ArrivalTime - current.ArrivalTime;
|
||||
if (currentStation is not null && nextStation is not null && leg > 0.001)
|
||||
{
|
||||
// Straight-line interpolation of the leg: the stop positions are the platform anchors,
|
||||
// so this is an estimate, not the true track distance.
|
||||
var legLength = currentStation.DistanceTo(nextStation);
|
||||
metresToNext = legLength * (secondsToNext / leg);
|
||||
}
|
||||
else if (line.AvgVelocity > 0)
|
||||
{
|
||||
metresToNext = secondsToNext * line.AvgVelocity;
|
||||
}
|
||||
}
|
||||
|
||||
return new RailTrainProgress(
|
||||
index,
|
||||
nextIndex,
|
||||
current,
|
||||
next,
|
||||
currentStation,
|
||||
nextStation,
|
||||
secondsToNext,
|
||||
metresToNext,
|
||||
elapsed - current.ArrivalTime <= current.InsertTime + 0.5);
|
||||
}
|
||||
|
||||
private static Dictionary<uint, RailLine4229938> LoadLines()
|
||||
{
|
||||
var result = new Dictionary<uint, RailLine4229938>();
|
||||
foreach (var row in ReadRecords("RailLineConfig.json"))
|
||||
{
|
||||
var id = UInt(row, "Id");
|
||||
var total = Double(row, "TotalTravelDuration");
|
||||
if (id == 0 || total <= 0)
|
||||
continue;
|
||||
var (firstHour, firstMinute) = Clock(row, "FirstTrainTime", 6, 0);
|
||||
var (lastHour, lastMinute) = Clock(row, "LastTrainTime", 23, 30);
|
||||
result[id] = new RailLine4229938(
|
||||
id,
|
||||
Str(row, "LineName"),
|
||||
UInt(row, "TrainTemplateId"),
|
||||
Str(row, "PrefabPath"),
|
||||
Str(row, "BlobName"),
|
||||
Bool(row, "RingLine"),
|
||||
Bool(row, "IsTestPath"),
|
||||
Int(row, "DepartureInterval"),
|
||||
Int(row, "MaxTrainAmount"),
|
||||
total,
|
||||
Double(row, "DestinationArrivalTime"),
|
||||
Int(row, "AvgVelocity"),
|
||||
Bool(row, "FirstStopDumb"),
|
||||
Bool(row, "LastStopDumb"),
|
||||
firstHour,
|
||||
firstMinute,
|
||||
lastHour,
|
||||
lastMinute);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Dictionary<uint, RailStation4229938> LoadStations()
|
||||
{
|
||||
var result = new Dictionary<uint, RailStation4229938>();
|
||||
foreach (var row in ReadRecords("RailStationConfig.json"))
|
||||
{
|
||||
var id = UInt(row, "Id");
|
||||
if (id == 0)
|
||||
continue;
|
||||
var position = row.TryGetProperty("Position", out var pos) && pos.ValueKind == JsonValueKind.Array
|
||||
? pos.EnumerateArray().Select(v => v.ValueKind == JsonValueKind.Number ? v.GetDouble() : 0d).ToArray()
|
||||
: [];
|
||||
result[id] = new RailStation4229938(
|
||||
id,
|
||||
Str(row, "StationName"),
|
||||
Str(row, "StationChineseName"),
|
||||
Str(row, "StationEnglishName"),
|
||||
position.Length > 0 ? position[0] : 0,
|
||||
position.Length > 1 ? position[1] : 0,
|
||||
position.Length > 2 ? position[2] : 0,
|
||||
UIntArray(row, "Lines"));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Dictionary<uint, RailRoute4229938> LoadRoutes()
|
||||
{
|
||||
var lines = LineCache.Value;
|
||||
var stations = StationCache.Value;
|
||||
|
||||
// The line's Stations array is index-aligned with the stop rows, so the nth stop of a line
|
||||
// belongs to the nth station id (id 0 is the depot placeholder, which has no RailStationConfig row).
|
||||
var stationIdsByLine = new Dictionary<uint, uint[]>();
|
||||
foreach (var row in ReadRecords("RailLineConfig.json"))
|
||||
{
|
||||
var id = UInt(row, "Id");
|
||||
if (id != 0)
|
||||
stationIdsByLine[id] = UIntArray(row, "Stations");
|
||||
}
|
||||
|
||||
// RailStopConfig only carries LineId, so grouping by it already gives one ordered run per line
|
||||
// (Line 2 is the reverse direction and therefore has its own stop rows).
|
||||
var byLine = new Dictionary<uint, List<RailStop4229938>>();
|
||||
foreach (var row in ReadRecords("RailStopConfig.json"))
|
||||
{
|
||||
var lineId = UInt(row, "LineId");
|
||||
if (lineId == 0)
|
||||
continue;
|
||||
var stop = new RailStop4229938(
|
||||
UInt(row, "Id"),
|
||||
lineId,
|
||||
Str(row, "LineName"),
|
||||
UInt(row, "NextStop"),
|
||||
Double(row, "ArrivalTime"),
|
||||
Double(row, "InsertTime"),
|
||||
UInt(row, "GateOpenSide"),
|
||||
Bool(row, "DefaultExitStop"),
|
||||
0);
|
||||
if (!byLine.TryGetValue(lineId, out var list))
|
||||
byLine[lineId] = list = [];
|
||||
list.Add(stop);
|
||||
}
|
||||
|
||||
var routes = new Dictionary<uint, RailRoute4229938>();
|
||||
foreach (var (lineId, raw) in byLine)
|
||||
{
|
||||
if (!lines.TryGetValue(lineId, out var line))
|
||||
continue;
|
||||
|
||||
var ordered = raw
|
||||
.OrderBy(s => s.ArrivalTime)
|
||||
.ThenBy(s => s.Id)
|
||||
.ToList();
|
||||
|
||||
var stationIds = stationIdsByLine.TryGetValue(lineId, out var ids) ? ids : [];
|
||||
for (var i = 0; i < ordered.Count; i++)
|
||||
{
|
||||
var stationId = i < stationIds.Length ? stationIds[i] : 0;
|
||||
if (stationId != 0 && !stations.ContainsKey(stationId))
|
||||
stationId = 0;
|
||||
ordered[i] = ordered[i] with { StationId = stationId };
|
||||
}
|
||||
|
||||
routes[lineId] = new RailRoute4229938(line, ordered);
|
||||
}
|
||||
return routes;
|
||||
}
|
||||
|
||||
private static uint[] UIntArray(JsonElement row, string name)
|
||||
{
|
||||
if (!row.TryGetProperty(name, out var node) || node.ValueKind != JsonValueKind.Array)
|
||||
return [];
|
||||
var values = new List<uint>();
|
||||
foreach (var item in node.EnumerateArray())
|
||||
{
|
||||
if (item.ValueKind == JsonValueKind.Number && item.TryGetUInt32(out var value))
|
||||
values.Add(value);
|
||||
}
|
||||
return values.ToArray();
|
||||
}
|
||||
|
||||
private static IEnumerable<JsonElement> ReadRecords(string fileName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var root = PrivateServerConfigStore.ResolveProjectPath(PrivateServerConfigStore.Current.Paths.ClientConfigs);
|
||||
var path = Path.Combine(root, fileName);
|
||||
if (!File.Exists(path))
|
||||
return [];
|
||||
using var doc = JsonDocument.Parse(File.ReadAllText(path));
|
||||
if (!doc.RootElement.TryGetProperty("records", out var records) || records.ValueKind != JsonValueKind.Array)
|
||||
return [];
|
||||
// Clone so the elements survive the disposed JsonDocument.
|
||||
return records.EnumerateArray().Select(row => row.Clone()).ToArray();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private static string Str(JsonElement row, string name)
|
||||
=> row.TryGetProperty(name, out var node) && node.ValueKind == JsonValueKind.String ? node.GetString() ?? string.Empty : string.Empty;
|
||||
|
||||
private static uint UInt(JsonElement row, string name)
|
||||
=> row.TryGetProperty(name, out var node) && node.ValueKind == JsonValueKind.Number && node.TryGetUInt32(out var value) ? value : 0u;
|
||||
|
||||
private static int Int(JsonElement row, string name)
|
||||
=> row.TryGetProperty(name, out var node) && node.ValueKind == JsonValueKind.Number && node.TryGetInt32(out var value) ? value : 0;
|
||||
|
||||
private static double Double(JsonElement row, string name)
|
||||
=> row.TryGetProperty(name, out var node) && node.ValueKind == JsonValueKind.Number ? node.GetDouble() : 0d;
|
||||
|
||||
private static bool Bool(JsonElement row, string name)
|
||||
=> row.TryGetProperty(name, out var node) && node.ValueKind == JsonValueKind.True;
|
||||
|
||||
private static (int Hour, int Minute) Clock(JsonElement row, string name, int fallbackHour, int fallbackMinute)
|
||||
{
|
||||
if (!row.TryGetProperty(name, out var node) || node.ValueKind != JsonValueKind.Object)
|
||||
return (fallbackHour, fallbackMinute);
|
||||
var hour = node.TryGetProperty("hour", out var h) && h.TryGetInt32(out var hourValue) ? hourValue : fallbackHour;
|
||||
var minute = node.TryGetProperty("minute", out var m) && m.TryGetInt32(out var minuteValue) ? minuteValue : fallbackMinute;
|
||||
return (hour, minute);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
using System.Text.Json;
|
||||
using Ananta.Server.Configuration;
|
||||
|
||||
namespace Ananta.Server.ClientData.Client4229938;
|
||||
|
||||
internal static class SocialCatalogRepository4229938
|
||||
{
|
||||
|
||||
internal sealed record PostRow(
|
||||
uint Id,
|
||||
uint Publisher,
|
||||
string Txt,
|
||||
uint Likes,
|
||||
uint LikeDrop,
|
||||
bool IfShowTips,
|
||||
bool IfStory,
|
||||
bool IfPinStory,
|
||||
bool WithMe,
|
||||
IReadOnlyList<uint> CommentIds);
|
||||
|
||||
|
||||
internal sealed record PublisherRow(
|
||||
uint Id,
|
||||
string Name,
|
||||
uint Simage,
|
||||
uint NpcCultivation,
|
||||
IReadOnlyList<uint> SamePublishers);
|
||||
|
||||
|
||||
internal sealed record CommentRow(uint Id, uint Publisher, uint NextId, string Txt);
|
||||
|
||||
private static readonly object Sync = new();
|
||||
private static List<PostRow>? _posts;
|
||||
private static Dictionary<uint, PublisherRow>? _publishers;
|
||||
private static Dictionary<uint, CommentRow>? _comments;
|
||||
|
||||
private static string Root => PrivateServerConfigStore.ResolveProjectPath(
|
||||
PrivateServerConfigStore.Current.Paths.ClientConfigs);
|
||||
|
||||
|
||||
internal static IReadOnlyList<PostRow> AllPosts
|
||||
{
|
||||
get { EnsureLoaded(); lock (Sync) return _posts!; }
|
||||
}
|
||||
|
||||
|
||||
internal static PostRow? Post(uint id)
|
||||
{
|
||||
EnsureLoaded();
|
||||
lock (Sync)
|
||||
return _posts!.FirstOrDefault(x => x.Id == id);
|
||||
}
|
||||
|
||||
|
||||
internal static PublisherRow? Publisher(uint id)
|
||||
{
|
||||
EnsureLoaded();
|
||||
lock (Sync)
|
||||
return _publishers!.TryGetValue(id, out var v) ? v : null;
|
||||
}
|
||||
|
||||
|
||||
internal static CommentRow? Comment(uint id)
|
||||
{
|
||||
EnsureLoaded();
|
||||
lock (Sync)
|
||||
return _comments!.TryGetValue(id, out var v) ? v : null;
|
||||
}
|
||||
|
||||
internal static string Summary()
|
||||
{
|
||||
EnsureLoaded();
|
||||
lock (Sync)
|
||||
return $"SocialMediaConfig={_posts!.Count} 条动态"
|
||||
+ $" / SocialMediaNPCConfig={_publishers!.Count} 个发布者"
|
||||
+ $" / SocialMediaCommentConfig={_comments!.Count} 条评论"
|
||||
+ $"(WithMe={_posts.Count(x => x.WithMe)})";
|
||||
}
|
||||
|
||||
private static void EnsureLoaded()
|
||||
{
|
||||
if (_posts is not null)
|
||||
return;
|
||||
|
||||
lock (Sync)
|
||||
{
|
||||
if (_posts is not null)
|
||||
return;
|
||||
|
||||
var posts = new List<PostRow>();
|
||||
var publishers = new Dictionary<uint, PublisherRow>();
|
||||
var comments = new Dictionary<uint, CommentRow>();
|
||||
var root = Root;
|
||||
|
||||
ForEachRecord(root, "SocialMediaConfig.json", row =>
|
||||
{
|
||||
var id = U32(row, "Id");
|
||||
if (id == 0)
|
||||
return;
|
||||
posts.Add(new PostRow(
|
||||
id,
|
||||
U32(row, "Publisher"),
|
||||
Str(row, "Txt") ?? string.Empty,
|
||||
U32(row, "Likes"),
|
||||
U32(row, "LikeDrop"),
|
||||
Bool(row, "IfShowTips"),
|
||||
Bool(row, "IfStory"),
|
||||
Bool(row, "IfPinStory"),
|
||||
Bool(row, "WithMe"),
|
||||
U32List(row, "Comment")));
|
||||
});
|
||||
|
||||
ForEachRecord(root, "SocialMediaNPCConfig.json", row =>
|
||||
{
|
||||
var id = U32(row, "Id");
|
||||
if (id == 0)
|
||||
return;
|
||||
publishers[id] = new PublisherRow(
|
||||
id,
|
||||
Str(row, "Name") ?? string.Empty,
|
||||
U32(row, "Simage"),
|
||||
U32(row, "NpcCultivation"),
|
||||
U32List(row, "SamePublishers"));
|
||||
});
|
||||
|
||||
ForEachRecord(root, "SocialMediaCommentConfig.json", row =>
|
||||
{
|
||||
var id = U32(row, "Id");
|
||||
if (id == 0)
|
||||
return;
|
||||
comments[id] = new CommentRow(
|
||||
id,
|
||||
U32(row, "Publisher"),
|
||||
U32(row, "NextId"),
|
||||
Str(row, "Txt") ?? string.Empty);
|
||||
});
|
||||
|
||||
_posts = [.. posts.OrderBy(x => x.Id)];
|
||||
_publishers = publishers;
|
||||
_comments = comments;
|
||||
|
||||
Console.WriteLine($"[SOCIAL] {Summary()}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private static void ForEachRecord(string root, string fileName, Action<JsonElement> visit)
|
||||
{
|
||||
var path = Path.Combine(root, fileName);
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
Console.WriteLine($"[SOCIAL] 缺少配置 {fileName}(叭叭 App 内容会不完整)");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(File.ReadAllText(path));
|
||||
if (!document.RootElement.TryGetProperty("records", out var records)
|
||||
|| records.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
Console.WriteLine($"[SOCIAL] {fileName} 没有 records 数组");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var row in records.EnumerateArray())
|
||||
{
|
||||
if (row.ValueKind == JsonValueKind.Object)
|
||||
visit(row);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[SOCIAL] 读取 {fileName} 失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static uint U32(JsonElement row, string name)
|
||||
=> row.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.Number
|
||||
? v.GetUInt32()
|
||||
: 0u;
|
||||
|
||||
private static string? Str(JsonElement row, string name)
|
||||
=> row.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String
|
||||
? v.GetString()
|
||||
: null;
|
||||
|
||||
private static bool Bool(JsonElement row, string name)
|
||||
=> row.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.True;
|
||||
|
||||
private static IReadOnlyList<uint> U32List(JsonElement row, string name)
|
||||
{
|
||||
if (!row.TryGetProperty(name, out var v) || v.ValueKind != JsonValueKind.Array)
|
||||
return [];
|
||||
var list = new List<uint>();
|
||||
foreach (var item in v.EnumerateArray())
|
||||
{
|
||||
if (item.ValueKind == JsonValueKind.Number)
|
||||
list.Add(item.GetUInt32());
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using System.Text.Json;
|
||||
using Ananta.Server.Configuration;
|
||||
|
||||
namespace Ananta.Server.ClientData.Client4229938;
|
||||
|
||||
/// <summary>One TaskConfig row: everything the quest state machine needs from the server side.</summary>
|
||||
internal sealed record TaskEntry4229938(
|
||||
uint Id,
|
||||
string Name,
|
||||
uint RelatedRaid,
|
||||
int CounterCount,
|
||||
uint[] NextTasks);
|
||||
|
||||
/// <summary>
|
||||
/// Build-4229938 quest table (TaskConfig.json, 4.3k tasks from the client dump). The client
|
||||
/// bakes the same config, so the server only tracks state per task id and chains NextTasks on
|
||||
/// submit; names/objectives render from the client's own copy.
|
||||
/// </summary>
|
||||
internal static class TaskCatalog4229938
|
||||
{
|
||||
private static readonly Lazy<Dictionary<uint, TaskEntry4229938>> Cache = new(Load);
|
||||
|
||||
internal static IReadOnlyDictionary<uint, TaskEntry4229938> All => Cache.Value;
|
||||
|
||||
internal static bool TryGet(uint taskId, out TaskEntry4229938 entry)
|
||||
=> Cache.Value.TryGetValue(taskId, out entry!);
|
||||
|
||||
internal static IEnumerable<TaskEntry4229938> ByRaid(uint raidId)
|
||||
=> Cache.Value.Values.Where(t => t.RelatedRaid == raidId);
|
||||
|
||||
private static Dictionary<uint, TaskEntry4229938> Load()
|
||||
{
|
||||
var result = new Dictionary<uint, TaskEntry4229938>();
|
||||
try
|
||||
{
|
||||
var root = PrivateServerConfigStore.ResolveProjectPath(PrivateServerConfigStore.Current.Paths.ClientConfigs);
|
||||
var path = Path.Combine(root, "TaskConfig.json");
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
Console.WriteLine($"[TASK] TaskConfig.json not found at {path}; quest catalog disabled.");
|
||||
return result;
|
||||
}
|
||||
using var doc = JsonDocument.Parse(File.ReadAllText(path));
|
||||
// Dump table shape: {"_class":..., "records":[...]}.
|
||||
if (!doc.RootElement.TryGetProperty("records", out var rows) || rows.ValueKind != JsonValueKind.Array)
|
||||
return result;
|
||||
|
||||
foreach (var row in rows.EnumerateArray())
|
||||
{
|
||||
var id = U32(row, "Id");
|
||||
if (id == 0)
|
||||
continue;
|
||||
var next = new List<uint>();
|
||||
if (row.TryGetProperty("NextTasks", out var nt) && nt.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var n in nt.EnumerateArray())
|
||||
next.Add(n.GetUInt32());
|
||||
}
|
||||
var counterCount = 0;
|
||||
if (row.TryGetProperty("Counter", out var counters) && counters.ValueKind == JsonValueKind.Array)
|
||||
counterCount = counters.GetArrayLength();
|
||||
|
||||
result[id] = new TaskEntry4229938(
|
||||
id,
|
||||
row.TryGetProperty("Name", out var name) ? name.GetString() ?? string.Empty : string.Empty,
|
||||
U32(row, "RelatedRaid"),
|
||||
counterCount,
|
||||
next.ToArray());
|
||||
}
|
||||
Console.WriteLine($"[TASK] Quest catalog ready: {result.Count} tasks.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[TASK-ERROR] Failed to load TaskConfig.json: {ex.Message}");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static uint U32(JsonElement row, string field)
|
||||
=> row.TryGetProperty(field, out var v) && v.ValueKind == JsonValueKind.Number ? v.GetUInt32() : 0u;
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
using System.Text.Json;
|
||||
using Ananta.Server.Configuration;
|
||||
|
||||
namespace Ananta.Server.ClientData.Client4229938;
|
||||
|
||||
internal static class TaskCatalogRepository
|
||||
{
|
||||
private static readonly object Sync = new();
|
||||
private static Dictionary<uint, string>? _names;
|
||||
private static Dictionary<uint, string>? _titles;
|
||||
private static Dictionary<uint, string>? _descriptions;
|
||||
private static Dictionary<uint, int[]>? _counters;
|
||||
private static Dictionary<uint, string[]>? _objectives;
|
||||
private static Dictionary<uint, uint[]>? _nextTasks;
|
||||
private static Dictionary<uint, int>? _titleIds;
|
||||
private static Dictionary<uint, uint>? _relatedRaids;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
internal static uint[] NextTasks(uint taskId)
|
||||
{
|
||||
EnsureLoaded();
|
||||
lock (Sync)
|
||||
return _nextTasks!.TryGetValue(taskId, out var value) ? value : [];
|
||||
}
|
||||
|
||||
|
||||
internal static int TitleId(uint taskId)
|
||||
{
|
||||
EnsureLoaded();
|
||||
lock (Sync)
|
||||
return _titleIds!.TryGetValue(taskId, out var value) ? value : 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
internal static uint RelatedRaid(uint taskId)
|
||||
{
|
||||
EnsureLoaded();
|
||||
lock (Sync)
|
||||
return _relatedRaids!.TryGetValue(taskId, out var value) ? value : 0u;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
internal static IReadOnlyList<uint> ChainFrom(uint rootId, int maxLength = 200)
|
||||
{
|
||||
EnsureLoaded();
|
||||
var chain = new List<uint>();
|
||||
var seen = new HashSet<uint>();
|
||||
var current = rootId;
|
||||
while (current != 0 && seen.Add(current) && chain.Count < maxLength)
|
||||
{
|
||||
chain.Add(current);
|
||||
var next = NextTasks(current);
|
||||
current = next.Length > 0 ? next[0] : 0u;
|
||||
}
|
||||
return chain;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
internal static int[] Counters(uint taskId)
|
||||
{
|
||||
EnsureLoaded();
|
||||
lock (Sync)
|
||||
return _counters!.TryGetValue(taskId, out var value) ? value : [];
|
||||
}
|
||||
|
||||
|
||||
internal static string[] Objectives(uint taskId)
|
||||
{
|
||||
EnsureLoaded();
|
||||
lock (Sync)
|
||||
return _objectives!.TryGetValue(taskId, out var value) ? value : [];
|
||||
}
|
||||
|
||||
internal static IReadOnlyDictionary<uint, string> Names
|
||||
{
|
||||
get { EnsureLoaded(); lock (Sync) return new Dictionary<uint, string>(_names!); }
|
||||
}
|
||||
|
||||
internal static int Count
|
||||
{
|
||||
get { EnsureLoaded(); lock (Sync) return _names!.Count; }
|
||||
}
|
||||
|
||||
internal static string? Name(uint taskId)
|
||||
{
|
||||
EnsureLoaded();
|
||||
lock (Sync)
|
||||
return _names!.TryGetValue(taskId, out var value) ? value : null;
|
||||
}
|
||||
|
||||
internal static string? Title(uint taskId)
|
||||
{
|
||||
EnsureLoaded();
|
||||
lock (Sync)
|
||||
return _titles!.TryGetValue(taskId, out var value) ? value : null;
|
||||
}
|
||||
|
||||
internal static string? Description(uint taskId)
|
||||
{
|
||||
EnsureLoaded();
|
||||
lock (Sync)
|
||||
return _descriptions!.TryGetValue(taskId, out var value) ? value : null;
|
||||
}
|
||||
|
||||
|
||||
internal static IEnumerable<(uint Id, string Name)> Search(string? term, int limit = 200)
|
||||
{
|
||||
EnsureLoaded();
|
||||
lock (Sync)
|
||||
{
|
||||
var query = _names!.AsEnumerable();
|
||||
if (!string.IsNullOrWhiteSpace(term))
|
||||
{
|
||||
var needle = term.Trim();
|
||||
query = uint.TryParse(needle, out var exact)
|
||||
? query.Where(x => x.Key == exact || x.Value.Contains(needle, StringComparison.OrdinalIgnoreCase))
|
||||
: query.Where(x => x.Value.Contains(needle, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
return query.OrderBy(x => x.Key).Take(limit).Select(x => (x.Key, x.Value)).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureLoaded()
|
||||
{
|
||||
if (_names is not null)
|
||||
return;
|
||||
lock (Sync)
|
||||
{
|
||||
if (_names is not null)
|
||||
return;
|
||||
|
||||
_names = [];
|
||||
_titles = [];
|
||||
_descriptions = [];
|
||||
_counters = [];
|
||||
_objectives = [];
|
||||
_nextTasks = [];
|
||||
_titleIds = [];
|
||||
_relatedRaids = [];
|
||||
try
|
||||
{
|
||||
var config = PrivateServerConfigStore.Current;
|
||||
var root = PrivateServerConfigStore.ResolveProjectPath(config.Paths.ClientConfigs);
|
||||
var path = Path.Combine(root, "TaskConfig.json");
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
Console.WriteLine($"[TASKS] 找不到 {path};任务名不可用");
|
||||
return;
|
||||
}
|
||||
|
||||
using var document = JsonDocument.Parse(File.ReadAllText(path));
|
||||
if (!document.RootElement.TryGetProperty("records", out var records) ||
|
||||
records.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
Console.WriteLine($"[TASKS] {path} 没有 records 数组");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var row in records.EnumerateArray())
|
||||
{
|
||||
if (!row.TryGetProperty("Id", out var idNode) || !idNode.TryGetUInt32(out var id) || id == 0)
|
||||
continue;
|
||||
var name = ReadString(row, "Name");
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
_names[id] = name!;
|
||||
var title = ReadString(row, "Title");
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
_titles[id] = title!;
|
||||
var description = ReadString(row, "WorkDescription");
|
||||
if (!string.IsNullOrWhiteSpace(description))
|
||||
_descriptions[id] = description!;
|
||||
|
||||
var counters = ReadIntArray(row, "Counter");
|
||||
if (counters.Length > 0)
|
||||
_counters[id] = counters;
|
||||
var objectives = ReadStringArray(row, "EventObjective");
|
||||
if (objectives.Length > 0)
|
||||
_objectives[id] = objectives;
|
||||
|
||||
var next = ReadUIntArray(row, "NextTasks");
|
||||
if (next.Length > 0)
|
||||
_nextTasks[id] = next;
|
||||
_titleIds[id] = ReadInt32(row, "Title");
|
||||
_relatedRaids[id] = ReadUInt32(row, "RelatedRaid");
|
||||
}
|
||||
|
||||
Console.WriteLine($"[TASKS] TaskConfig 已载入: {_names.Count} 个具名任务, {_counters.Count} 个带计数, 来自 {path}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[TASKS] 载入 TaskConfig 失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ReadString(JsonElement row, string name)
|
||||
{
|
||||
if (!row.TryGetProperty(name, out var node))
|
||||
return null;
|
||||
return node.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String => node.GetString(),
|
||||
JsonValueKind.Number => node.ToString(),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
private static int[] ReadIntArray(JsonElement row, string name)
|
||||
{
|
||||
if (!row.TryGetProperty(name, out var node) || node.ValueKind != JsonValueKind.Array)
|
||||
return [];
|
||||
return node.EnumerateArray()
|
||||
.Where(x => x.TryGetInt32(out _))
|
||||
.Select(x => x.GetInt32())
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static int ReadInt32(JsonElement row, string name)
|
||||
=> row.TryGetProperty(name, out var node) && node.TryGetInt32(out var value) ? value : 0;
|
||||
|
||||
private static uint ReadUInt32(JsonElement row, string name)
|
||||
=> row.TryGetProperty(name, out var node) && node.TryGetUInt32(out var value) ? value : 0u;
|
||||
|
||||
private static uint[] ReadUIntArray(JsonElement row, string name)
|
||||
{
|
||||
if (!row.TryGetProperty(name, out var node) || node.ValueKind != JsonValueKind.Array)
|
||||
return [];
|
||||
return node.EnumerateArray()
|
||||
.Where(x => x.TryGetUInt32(out _))
|
||||
.Select(x => x.GetUInt32())
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static string[] ReadStringArray(JsonElement row, string name)
|
||||
{
|
||||
if (!row.TryGetProperty(name, out var node) || node.ValueKind != JsonValueKind.Array)
|
||||
return [];
|
||||
return node.EnumerateArray()
|
||||
.Where(x => x.ValueKind == JsonValueKind.String)
|
||||
.Select(x => x.GetString() ?? string.Empty)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using System.Text.Json;
|
||||
using Ananta.Server.Configuration;
|
||||
|
||||
namespace Ananta.Server.ClientData.Client4229938;
|
||||
|
||||
/// <summary>One VehicleSpawnConfig row: what NPC drives a vehicle config and its paint color pool.</summary>
|
||||
internal sealed record VehicleSpawnEntry4229938(
|
||||
uint VehicleId,
|
||||
uint DriverNpcType,
|
||||
uint PassengerNpcType,
|
||||
int PassengerCount,
|
||||
uint[] ColorMods);
|
||||
|
||||
/// <summary>
|
||||
/// Build-4229938 ambient traffic spawn table (VehicleSpawnConfig.json): pairs each traffic
|
||||
/// vehicle config with its driver NPC formwork, passengers and paint color pool — the same
|
||||
/// data the real game uses (driver 45200091, passenger 45200093, color parts 9994xxxx).
|
||||
/// </summary>
|
||||
internal static class VehicleSpawnCatalog4229938
|
||||
{
|
||||
private static readonly VehicleSpawnEntry4229938 Fallback = new(0, 45200091, 45200093, 0, []);
|
||||
private static readonly Lazy<Dictionary<uint, VehicleSpawnEntry4229938>> Cache = new(Load);
|
||||
|
||||
internal static VehicleSpawnEntry4229938 Get(uint vehicleId)
|
||||
=> Cache.Value.TryGetValue(vehicleId, out var entry) ? entry : Fallback;
|
||||
|
||||
private static Dictionary<uint, VehicleSpawnEntry4229938> Load()
|
||||
{
|
||||
var result = new Dictionary<uint, VehicleSpawnEntry4229938>();
|
||||
try
|
||||
{
|
||||
var root = PrivateServerConfigStore.ResolveProjectPath(PrivateServerConfigStore.Current.Paths.ClientConfigs);
|
||||
var path = Path.Combine(root, "VehicleSpawnConfig.json");
|
||||
if (!File.Exists(path))
|
||||
return result;
|
||||
using var doc = JsonDocument.Parse(File.ReadAllText(path));
|
||||
if (!doc.RootElement.TryGetProperty("records", out var records) || records.ValueKind != JsonValueKind.Array)
|
||||
return result;
|
||||
foreach (var row in records.EnumerateArray())
|
||||
{
|
||||
var vehicleId = U32(row, "VehicleId");
|
||||
if (vehicleId == 0)
|
||||
continue;
|
||||
var driver = FirstU32(row, "NewNpcType");
|
||||
if (driver == 0)
|
||||
continue;
|
||||
var passengerCount = I32(row, "HasPassenger");
|
||||
var passenger = FirstU32(row, "NewPassengerType");
|
||||
result[vehicleId] = new VehicleSpawnEntry4229938(
|
||||
vehicleId,
|
||||
driver,
|
||||
passenger == 0 ? 45200093u : passenger,
|
||||
Math.Clamp(passengerCount, 0, 3),
|
||||
U32Array(row, "ColorMod"));
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Missing/corrupt table: the fallback entry keeps traffic spawning with defaults.
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static uint FirstU32(JsonElement row, string name)
|
||||
{
|
||||
if (!row.TryGetProperty(name, out var node) || node.ValueKind != JsonValueKind.Array)
|
||||
return 0u;
|
||||
foreach (var item in node.EnumerateArray())
|
||||
if (item.ValueKind == JsonValueKind.Number && item.TryGetUInt32(out var value))
|
||||
return value;
|
||||
return 0u;
|
||||
}
|
||||
|
||||
private static uint U32(JsonElement row, string name)
|
||||
=> row.TryGetProperty(name, out var node) && node.ValueKind == JsonValueKind.Number && node.TryGetUInt32(out var value) ? value : 0u;
|
||||
|
||||
private static uint[] U32Array(JsonElement row, string name)
|
||||
{
|
||||
if (!row.TryGetProperty(name, out var node) || node.ValueKind != JsonValueKind.Array)
|
||||
return [];
|
||||
var values = new List<uint>();
|
||||
foreach (var item in node.EnumerateArray())
|
||||
if (item.ValueKind == JsonValueKind.Number && item.TryGetUInt32(out var value))
|
||||
values.Add(value);
|
||||
return values.ToArray();
|
||||
}
|
||||
|
||||
private static int I32(JsonElement row, string name)
|
||||
=> row.TryGetProperty(name, out var node) && node.ValueKind == JsonValueKind.Number && node.TryGetInt32(out var value) ? value : 0;
|
||||
}
|
||||
Reference in new issue
Block a user