using System.Net; using System.Text; using System.Text.Json; using Ananta.Server.Configuration; using Ananta.Server.Handlers.Game; using Ananta.Server.Protocol.Client4229938; namespace Ananta.Server.App.Admin; /// /// Embedded localhost admin panel (no auth by design — bind 127.0.0.1 only). /// Configured via private-server.json → admin { enabled, host, port }. /// internal sealed class AdminWebServer(PrivateServerConfig config) { private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, // ulong 超过 2^53 在 JS 里会丢精度(客户端本地单位 id 就是 8.3e18 量级), // 所以 /api/police/targets 把 uid 以字符串下发,这里允许回传字符串再解析成数字。 NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString, }; internal async Task RunAsync(CancellationToken cancellationToken) { if (!config.Admin.Enabled) { Console.WriteLine("[ADMIN] disabled by config (admin.enabled=false)"); return; } var prefix = $"http://{config.Admin.Host}:{config.Admin.Port}/"; var listener = new HttpListener(); listener.Prefixes.Add(prefix); try { listener.Start(); } catch (Exception ex) { Console.WriteLine($"[ADMIN] failed to listen on {prefix}: {ex.Message}"); return; } Console.WriteLine($"[ADMIN] panel at {prefix} (localhost only, no auth)"); using var registration = cancellationToken.Register(() => listener.Stop()); while (!cancellationToken.IsCancellationRequested) { HttpListenerContext ctx; try { ctx = await listener.GetContextAsync(); } catch (HttpListenerException) { break; } catch (ObjectDisposedException) { break; } _ = Task.Run(() => HandleAsync(ctx, cancellationToken), cancellationToken); } listener.Close(); } private static async Task HandleAsync(HttpListenerContext ctx, CancellationToken ct) { var req = ctx.Request; var res = ctx.Response; try { var path = req.Url?.AbsolutePath ?? "/"; if (req.HttpMethod == "GET" && path == "/") { void_Write(res, "text/html; charset=utf-8", PageHtml); return; } if (req.HttpMethod == "GET" && path == "/api/status") { void_WriteJson(res, StatusPayload()); return; } if (req.HttpMethod == "GET" && path == "/api/vehicles") { var snap = GameAdminBridge.GetSnapshot(); void_WriteJson(res, new { fleet = GameAdminBridge.FleetIds(), flyingWater = GameAdminBridge.FlyingWaterVehicles(), lastVehicleId = snap.LastAdminVehicleId, lastUid = snap.LastSpawnedVehicleUid, lastDriveState = snap.HasVehicleDriveState ? (byte?)snap.LastVehicleDriveState : null, }); return; } // 读玩家坐标:站到「希望 NPC 面向的那个点」上打这个接口,就能反算出 facing。 if (req.HttpMethod == "GET" && path == "/api/player/pos") { var p = GameAdminBridge.PlayerPosition(); void_WriteJson(res, new { ok = p.Ok, message = p.Message, x = Math.Round(p.X, 3), y = Math.Round(p.Y, 3), z = Math.Round(p.Z, 3), raid = p.Raid, daySecond = p.DaySecond, }); return; } if (req.HttpMethod == "GET" && path == "/api/settings") { void_WriteJson(res, new { path = PrivateServerConfigStore.ConfigPath, restartGroups = RuntimeConfigEditor.RestartGroups, values = RuntimeConfigEditor.ReadRaw(), }); return; } if (req.HttpMethod == "POST" && path == "/api/settings") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new SettingsUpdateRequest(); if (cmd.Changes is null || cmd.Changes.Count == 0) { void_WriteJson(res, new { ok = false, message = "changes 为空" }, 400); return; } var (ok, applied, error) = RuntimeConfigEditor.Apply(cmd.Changes); void_WriteJson(res, new { ok, applied, message = error ?? $"已应用 {applied} 项" }, ok ? 200 : 400); return; } if (req.HttpMethod == "POST" && path == "/api/reload-config") { // Manual trigger for the same reload the file watcher does on save. Useful when an // editor writes the file in place without firing a change event. var ok = PrivateServerConfigStore.Reload(out var msg); var metro = PrivateServerConfigStore.Current.World.Content.MetroGadgets; void_WriteJson(res, new { ok, message = msg, metroGadgets = new { enabled = metro.Enabled, useTrainConfig = metro.UseTrainConfig, seatsByCarriageTemplate = metro.SeatsByCarriageTemplate, defaultCarriageSeats = metro.DefaultCarriageSeats, }, }, ok ? 200 : 400); return; } if (req.HttpMethod == "GET" && path == "/api/unknown-log") { var query = ParseQuery(req.Url?.Query); var lines = query.TryGetValue("lines", out var raw) && int.TryParse(raw, out var n) ? Math.Clamp(n, 1, 500) : 100; void_WriteJson(res, new { lines = UnknownMethodLogger.Tail(lines) }); return; } // ---- 骰子(DiceGame)试验台(2026-09-27 移植自 HyNB-v1.9)----------------------- // 服务端→客户端那六条 Sync* 的载荷布局没有离线权威来源,字段顺序来自 IL2CPP dump, // 但「枚举线宽」「complex vs struct」只能靠实机收敛。这几个端点让格式可以 // 不重启服务端地反复试:推包 → 看 Player.log / 客户端表现 → 调 hex → 再推。 if (path == "/api/dice/state") { var ds = GameAdminBridge.RequirePlayerSession(); void_WriteJson(res, new { ok = ds is not null, message = ds is null ? "没有在线的玩家会话" : "ok" }); return; } // /api/dice/start?gadget=&model=&rounds=&target=&first= if (path == "/api/dice/start") { var ds = GameAdminBridge.RequirePlayerSession(); if (ds is null) { void_WriteJson(res, new { ok = false, message = "没有玩家会话" }, 400); return; } var dq = ParseQuery(req.Url?.Query); var gadget = QueryU64(dq, "gadget", 0); var model = (uint)QueryU64(dq, "model", 0); var rounds = (int)QueryU64(dq, "rounds", 3); var target = (int)QueryU64(dq, "target", 0); var first = (int)QueryU64(dq, "first", 0); await GameRouter.PushDiceGameStartAsync(ds.Value.Session, gadget, model, rounds, target, first); void_WriteJson(res, new { ok = true, gadget, model, rounds, target, first }); return; } // /api/dice/command?uid=&cmd=&p=1,2,3&badge=&w=1|4 if (path == "/api/dice/command") { var ds = GameAdminBridge.RequirePlayerSession(); if (ds is null) { void_WriteJson(res, new { ok = false, message = "没有玩家会话" }, 400); return; } var dq = ParseQuery(req.Url?.Query); var uid = QueryU64(dq, "uid", 0); var dcmd = (int)QueryU64(dq, "cmd", 1); var badge = (uint)QueryU64(dq, "badge", 0); // 默认 1:实机验证过命令枚举在线上是 1 字节(4 字节时客户端完全不触发)。 var width = (int)QueryU64(dq, "w", 1); var dps = new List(); if (dq.TryGetValue("p", out var rawP) && !string.IsNullOrWhiteSpace(rawP)) foreach (var part in rawP.Split(',', StringSplitOptions.RemoveEmptyEntries)) if (int.TryParse(part, out var pv)) dps.Add(pv); await GameRouter.PushDiceCommandAsync(ds.Value.Session, uid, dcmd, dps, badge, width); void_WriteJson(res, new { ok = true, uid, cmd = dcmd, p = dps, badge, enumWidth = width }); return; } // /api/dice/raw?mid=&hex= —— 直接把任意字节推给客户端,最自由的试验口 if (path == "/api/dice/raw") { var ds = GameAdminBridge.RequirePlayerSession(); if (ds is null) { void_WriteJson(res, new { ok = false, message = "没有玩家会话" }, 400); return; } var dq = ParseQuery(req.Url?.Query); var mid = (uint)QueryU64(dq, "mid", 0); dq.TryGetValue("hex", out var dhex); byte[] dbytes; try { dbytes = System.Convert.FromHexString((dhex ?? "").Trim()); } catch (Exception dex) { void_WriteJson(res, new { ok = false, message = "hex 解析失败:" + dex.Message }, 400); return; } await GameRouter.PushDiceRawAsync(ds.Value.Session, mid, dbytes); void_WriteJson(res, new { ok = true, mid, bytes = dbytes.Length }); return; } if (req.HttpMethod == "POST" && path == "/api/teleport") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new TeleportRequest(); if (!float.IsFinite(cmd.X) || !float.IsFinite(cmd.Y) || !float.IsFinite(cmd.Z)) { void_WriteJson(res, new { ok = false, message = "x/y/z must be finite numbers" }, 400); return; } var mode = (cmd.Mode ?? "instant").ToLowerInvariant(); var result = mode == "loading" ? await GameAdminBridge.TeleportWithLoadingAsync(cmd.X, cmd.Y, cmd.Z, cmd.Facing) : await GameAdminBridge.TeleportInstantAsync(cmd.X, cmd.Y, cmd.Z, cmd.Facing); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } if (req.HttpMethod == "POST" && path == "/api/gadget/spawn") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new GadgetSpawnRequest(); if (cmd.PathId == 0) { void_WriteJson(res, new { ok = false, message = "pathId is required" }, 400); return; } if (!float.IsFinite(cmd.X) || !float.IsFinite(cmd.Y) || !float.IsFinite(cmd.Z)) { void_WriteJson(res, new { ok = false, message = "x/y/z must be finite numbers" }, 400); return; } var result = await GameAdminBridge.SpawnGadgetAtAsync( cmd.PathId, cmd.X, cmd.Y, cmd.Z, cmd.Yaw, cmd.IScale); void_WriteJson(res, new { ok = result.Ok, message = result.Message, count = result.Count }); return; } if (req.HttpMethod == "POST" && path == "/api/basketball/npcfix") { // 街头篮球:点亮对手(红铁桶 42071029)被关掉的 SkinnedMeshRenderer。 // ⛑ 模型其实加载成功了,只是 body/body01 的 SMR 是 enabled=false,所以看不见。 var nr = await GameAdminBridge.FixBasketballNpcModelAsync(); void_WriteJson(res, new { ok = nr.Ok, message = nr.Message }); return; } if (req.HttpMethod == "GET" && path == "/api/roster-schedule") { void_WriteJson(res, GameAdminBridge.RosterSchedule()); return; } if (req.HttpMethod == "POST" && path == "/api/vehicle/spawn") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new VehicleSpawnRequest(); if (cmd.VehicleId == 0) { void_WriteJson(res, new { ok = false, message = "vehicleId must be non-zero" }, 400); return; } var result = await GameAdminBridge.SpawnVehicleAsync(cmd.VehicleId, cmd.Force); void_WriteJson(res, new { ok = result.Ok, message = result.Message, uid = result.Uid }); return; } if (req.HttpMethod == "POST" && path == "/api/aether/isec-probe") { var result = await GameAdminBridge.AetherIsecProbeAsync(); void_WriteJson(res, new { ok = result.Ok, message = result.Message, rows = result.Rows }); return; } if (req.HttpMethod == "POST" && path == "/api/police/arrest") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new PoliceArrestRequest(); var result = await GameAdminBridge.ArrestNpcAsync(cmd.Uid, cmd.Teleport, cmd.Pull); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } if (req.HttpMethod == "GET" && path == "/api/police/prison") { void_WriteJson(res, GameAdminBridge.PolicePrisonStatus()); return; } if (req.HttpMethod == "POST" && path == "/api/police/prison") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new PolicePrisonRequest(); _ = ulong.TryParse(cmd.Uid, out var prisonUid); var action = (cmd.Action ?? "").Trim().ToLowerInvariant(); var result = action == "release" ? await GameAdminBridge.PolicePrisonReleaseAsync() : await GameAdminBridge.PolicePrisonCommitAsync(prisonUid); void_WriteJson(res, new { ok = result.Ok, message = result.Message, action }); return; } if (req.HttpMethod == "POST" && path == "/api/police/escort") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new PoliceEscortRequest(); _ = ulong.TryParse(cmd.Uid, out var escortUid); _ = ulong.TryParse(cmd.Vehicle, out var escortVehicle); var action = (cmd.Action ?? "").Trim().ToLowerInvariant(); var result = action == "alight" ? await GameAdminBridge.PoliceEscortAlightAsync() : await GameAdminBridge.PoliceEscortBoardAsync(escortUid, escortVehicle, cmd.Seat); void_WriteJson(res, new { ok = result.Ok, message = result.Message, action }); return; } if (req.HttpMethod == "GET" && path == "/api/gatelua/status") { void_WriteJson(res, GameAdminBridge.GateLuaStatus()); return; } if (req.HttpMethod == "POST" && path == "/api/gatelua") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new GateLuaRequest(); var result = await GameAdminBridge.RunClientLuaAsync(cmd.Script ?? ""); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } if (req.HttpMethod == "POST" && path == "/api/gatelua/probe") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new GateLuaRequest(); var result = await GameAdminBridge.GateLuaProbeAsync(cmd.Script ?? ""); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } if (req.HttpMethod == "GET" && path == "/api/gatelua/units") { void_WriteJson(res, GameAdminBridge.GateLuaUnits()); return; } if (req.HttpMethod == "POST" && path == "/api/gatelua/fbtn") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new GateLuaButtonRequest(); var action = (cmd.Action ?? "add").Trim().ToLowerInvariant(); var result = await GameAdminBridge.GateLuaUnitButtonAsync(cmd.Uid, cmd.Text, action != "remove"); void_WriteJson(res, new { ok = result.Ok, message = result.Message, action }); return; } if (req.HttpMethod == "POST" && path == "/api/police/examine") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new PoliceExamineRequest(); var result = await GameAdminBridge.ExamineNpcAsync(cmd.Uid, cmd.FineCount, cmd.Teleport, cmd.Close, cmd.Pull, cmd.Wire); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } if (req.HttpMethod == "POST" && path == "/api/police/react") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new PoliceReactRequest(); var result = await GameAdminBridge.PushPoliceExamReactionAsync(cmd.ReactionId); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } // ★ 捐赠端里希总入口(2026-10-02 逐字对齐捐赠端 AdminWebServer.cs:269-274)。 // 这张卡现在只剩这一条路:npc / examine / end-examine / dismiss / prison-*。 if (req.HttpMethod == "POST" && path == "/api/lixi/police") { var command = JsonSerializer.Deserialize(await ReadBodyAsync(req), JsonOptions) ?? new(); var result = await GameAdminBridge.LixiPoliceAsync(command.Action); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } if (req.HttpMethod == "POST" && path == "/api/police/fine") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new PoliceFineRequest(); var result = await GameAdminBridge.PushPoliceFineRewardAsync(cmd.Money, cmd.JobExp); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } if (req.HttpMethod == "POST" && path == "/api/police/crimes") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new PoliceCrimesRequest(); var result = await GameAdminBridge.PushAgentCrimesAsync(cmd.Uid, cmd.Fines, cmd.Wire); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } if (req.HttpMethod == "POST" && path == "/api/plate/push") { var body = await ReadBodyAsync(req); var cmd = string.IsNullOrWhiteSpace(body) ? new GameAdminBridge.PlatePushOptions() : JsonSerializer.Deserialize(body, JsonOptions) ?? new GameAdminBridge.PlatePushOptions(); var result = await GameAdminBridge.PushPlateAsync(cmd); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } if (req.HttpMethod == "POST" && path == "/api/npclife/spoon") { var body = await ReadBodyAsync(req); var cmd = string.IsNullOrWhiteSpace(body) ? new GameAdminBridge.NpcSpoonOptions() : JsonSerializer.Deserialize(body, JsonOptions) ?? new GameAdminBridge.NpcSpoonOptions(); var result = await GameAdminBridge.PushNpcSpoonAsync(cmd); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } // 时刻表 —— SyncFavorNpcSpoonAgentId 的硬前提(客户端没有时刻表会整帧丢弃 spoon)。 if (req.HttpMethod == "POST" && path == "/api/npclife/timetable") { var body = await ReadBodyAsync(req); var cmd = string.IsNullOrWhiteSpace(body) ? new GameAdminBridge.NpcTimeTableOptions() : JsonSerializer.Deserialize(body, JsonOptions) ?? new GameAdminBridge.NpcTimeTableOptions(); var result = await GameAdminBridge.PushNpcTimeTableAsync(cmd); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } // 科斯机甲:隐藏可控原型的接管 / 返回(2026-09-26,见 GameRouter.Kesi.cs)。 if (req.HttpMethod == "POST" && path == "/api/kesi/control") { var body = await ReadBodyAsync(req); var cmd = string.IsNullOrWhiteSpace(body) ? new GameAdminBridge.KesiControlOptions() : JsonSerializer.Deserialize(body, JsonOptions) ?? new GameAdminBridge.KesiControlOptions(); var result = await GameAdminBridge.KesiControlAsync(cmd); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } // 兜底:直接给单位下 POI 动作(198 = 站姿_运动_跑步机跑步)。 if (req.HttpMethod == "POST" && path == "/api/npclife/poi") { var body = await ReadBodyAsync(req); var cmd = string.IsNullOrWhiteSpace(body) ? new GameAdminBridge.NpcPoiOptions() : JsonSerializer.Deserialize(body, JsonOptions) ?? new GameAdminBridge.NpcPoiOptions(); var result = await GameAdminBridge.PushNpcPoiActionAsync(cmd); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } if (req.HttpMethod == "GET" && path == "/api/police/targets") { var rows = GameAdminBridge.PoliceTargets(); void_WriteJson(res, new { ok = true, count = rows.Count, targets = rows.Select(t => new { uid = t.Uid.ToString(), kind = t.Kind, label = t.Label, distance = t.Distance < 0 ? null : (float?)MathF.Round(t.Distance, 1), // Distance >= 0 才有服务端坐标;-1(GM 召唤)/ -2(客户端瞄准)没有。 x = t.Distance < 0 ? null : (float?)MathF.Round(t.X, 2), y = t.Distance < 0 ? null : (float?)MathF.Round(t.Y, 2), z = t.Distance < 0 ? null : (float?)MathF.Round(t.Z, 2), }).ToArray(), }); return; } if (req.HttpMethod == "GET" && path == "/api/blackout/status") { var s = GameAdminBridge.BlackoutStatus(); void_WriteJson(res, new { online = s.Online, running = s.Running, controllerId = s.ControllerId.ToString(), remainingSeconds = s.RemainingSeconds, endsAt = s.EndsAt, }); return; } if (req.HttpMethod == "POST" && path == "/api/blackout/start") { // Optional {x,y,z} landing-spot override; empty body ⇒ config decides (billboard by default). var boBody = await ReadBodyAsync(req); float? boX = null, boY = null, boZ = null; if (!string.IsNullOrWhiteSpace(boBody)) { var cmd = JsonSerializer.Deserialize(boBody, JsonOptions); boX = cmd?.X; boY = cmd?.Y; boZ = cmd?.Z; } var result = await GameAdminBridge.StartBlackoutAsync(boX, boY, boZ); void_WriteJson(res, new { ok = result.Ok, message = result.Message, id = result.Id.ToString() }); return; } if (req.HttpMethod == "POST" && path == "/api/blackout/stop") { var result = await GameAdminBridge.StopBlackoutAsync(); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } if (req.HttpMethod == "POST" && path == "/api/crowd/refresh") { var refreshBody = await ReadBodyAsync(req); float? over = null; if (!string.IsNullOrWhiteSpace(refreshBody)) { var cmd = JsonSerializer.Deserialize(refreshBody, JsonOptions); if (cmd?.Density is > 0f) over = cmd.Density; } var result = await GameAdminBridge.RefreshCrowdAsync(over); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } if (req.HttpMethod == "GET" && path == "/api/crowd/probe") { var p = GameAdminBridge.CrowdProbe(); void_WriteJson(res, new { online = p.Online, density = Math.Round(p.Density, 4), plotName = p.PlotName, landPlotType = p.LandPlotType, hourFactor = Math.Round(p.HourFactor, 4), hour = p.Hour, explicitTime = p.ExplicitTime, }); return; } if (req.HttpMethod == "POST" && path == "/api/vehicle/enter") { var result = await GameAdminBridge.EnterVehicleAsync(); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } if (req.HttpMethod == "POST" && path == "/api/vehicle/exit") { var result = await GameAdminBridge.ExitVehicleAsync(); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } // 自动驾驶巡航速度:GET 读当前值,POST 保存(有车在自驾就热应用)。 // 2026-09-30 移植自捐赠端 v0.5-2 的 ApplyAutonomousDriveSpeed。 if (req.HttpMethod == "GET" && path == "/api/vehicle/autodrive-speed") { var s = GameAdminBridge.AutonomousDriveSpeed(); void_WriteJson(res, new { savedKmh = s.SavedKmh, effectiveKmh = MathF.Round(s.EffectiveKmh, 1), activeAutoDrive = s.ActiveAutoDrive, minKmh = s.MinKmh, maxKmh = s.MaxKmh, }); return; } if (req.HttpMethod == "POST" && path == "/api/vehicle/autodrive-speed") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new AutoDriveSpeedRequest(); if (!float.IsFinite(cmd.SpeedKmh)) { void_WriteJson(res, new { ok = false, message = "speedKmh 不是有效数字。" }); return; } var result = await GameAdminBridge.SetAutonomousDriveSpeedAsync(cmd.SpeedKmh); void_WriteJson(res, new { ok = result.Ok, message = result.Message, hotApplied = result.HotApplied }); return; } if (req.HttpMethod == "GET" && path == "/api/scenes") { void_WriteJson(res, new { presets = GameAdminBridge.ScenePresets() }); return; } if (req.HttpMethod == "POST" && path == "/api/donor-npc/spawn") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new DonorNpcSpawnRequest(); if (cmd.Items is not { Count: >= 1 } || cmd.Items.Any(i => i is null || i.NpcFormworkId == 0) || !float.IsFinite(cmd.XSpacing) || cmd.XSpacing < 0f || !float.IsFinite(cmd.ZSpacing) || cmd.ZSpacing < 0f) { void_WriteJson(res, new { ok = false, message = "provide at least one item with a non-zero npcFormworkId, a uint poiActionId, finite non-negative X/Z spacing, and maxPerRow of at least 1" }, 400); return; } if (cmd.MaxPerRow < 1) { void_WriteJson(res, new { ok = false, message = "maxPerRow must be at least 1" }, 400); return; } var result = await GameAdminBridge.SpawnDonorStaticNpcsAsync(cmd.Items, cmd.XSpacing, cmd.ZSpacing, cmd.MaxPerRow); // Entity ids exceed Number.MAX_SAFE_INTEGER, so return them as strings — otherwise // every id collapses to the same double in the browser and looks like a bug. void_WriteJson(res, new { ok = result.Ok, message = result.Message, ids = result.Ids.Select(id => id.ToString()).ToArray() }); return; } if (req.HttpMethod == "GET" && path == "/api/civilians") { void_WriteJson(res, GameAdminBridge.CivilianPanelState()); return; } if (req.HttpMethod == "GET" && path == "/api/civilians/group") { var raw = req.QueryString["id"] ?? ""; int.TryParse(raw, out var gid); void_WriteJson(res, GameAdminBridge.CivilianGroupDetail(gid)); return; } if (req.HttpMethod == "POST" && path == "/api/civilians/spawn-group") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new CivilianGroupRequest(); var result = await GameAdminBridge.SpawnCivilianGroupAsync(cmd.GroupId); // Entity ids exceed Number.MAX_SAFE_INTEGER: as raw JSON numbers every id collapses // to the same double in the browser (all 30 look identical). Always stringify. void_WriteJson(res, new { ok = result.Ok, message = result.Message, ids = result.Ids.Select(id => id.ToString()).ToArray() }); return; } if (req.HttpMethod == "GET" && path == "/api/civilians/districts") { void_WriteJson(res, GameAdminBridge.CivilianDistrictState()); return; } if (req.HttpMethod == "POST" && path == "/api/civilians/spawn-district") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new CivilianDistrictRequest(); var result = await GameAdminBridge.SpawnCiviliansByDistrictAsync(cmd.Radius, cmd.MaxTotal, cmd.Hour ?? -1); void_WriteJson(res, new { ok = result.Ok, message = result.Message, detail = result.Detail, ids = result.Ids.Select(id => id.ToString()).ToArray(), }); return; } if (req.HttpMethod == "GET" && path == "/api/functional-npcs") { void_WriteJson(res, GameAdminBridge.FunctionalNpcState()); return; } if (req.HttpMethod == "GET" && path == "/api/enemy-territory") { void_WriteJson(res, GameAdminBridge.EnemyTerritoryState()); return; } if (req.HttpMethod == "POST" && path == "/api/enemy-territory/spawn") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new EnemyTerritoryRequest(); var result = await GameAdminBridge.SpawnEnemyTerritoryAsync(cmd.Radius); void_WriteJson(res, new { ok = result.Ok, message = result.Message, ids = result.Ids.Select(id => id.ToString()).ToArray(), detail = result.Detail, }); return; } // ---------------------------------------------------------------- 搜打撤(Extraction Shooter) if (req.HttpMethod == "GET" && path == "/api/extraction/status") { void_WriteJson(res, GameAdminBridge.ExtractionState()); return; } if (req.HttpMethod == "POST" && path == "/api/extraction/options") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new ExtractionOptionsRequest(); var result = GameAdminBridge.SetExtractionOptions( cmd.FakeMatch, cmd.PreferredRaid, cmd.ExitToMainCity, cmd.ExitSettlement, cmd.WheelSync, cmd.WheelWeaponData); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } if (req.HttpMethod == "POST" && path == "/api/extraction/start") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new ExtractionStartRequest(); var result = await GameAdminBridge.StartExtractionAsync(cmd.RaidId); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } if (req.HttpMethod == "POST" && path == "/api/extraction/exit") { var result = await GameAdminBridge.ExitExtractionAsync(); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } if (path == "/api/faction/invasionspots") { void_WriteJson(res, GameAdminBridge.ListFactionInvasionSpots()); return; } if (req.HttpMethod == "POST" && path == "/api/faction/invade") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new FactionInvasionRequest(); var result = await GameAdminBridge.RunFactionInvasionAsync( cmd.EventId, cmd.Teleport, cmd.Summon, cmd.Occupy); void_WriteJson(res, new { ok = result.Ok, message = result.Message, detail = result.Detail }); return; } if (req.HttpMethod == "POST" && path == "/api/extraction/wheel/republish") { var result = await GameAdminBridge.RepublishExtractionWheel(); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } if (req.HttpMethod == "POST" && path == "/api/extraction/republish") { var result = await GameAdminBridge.RepublishExtractionLoginFrame(); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } // 取证探针:把任意 methodId + 裸 hex 直接推给客户端,用来反推未知包的形状。 if (req.HttpMethod == "POST" && path == "/api/extraction/notify") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new ExtractionNotifyRequest(); var result = await GameAdminBridge.PushExtractionNotify(cmd.MethodId, cmd.Hex); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } if (req.HttpMethod == "POST" && path == "/api/functional-npcs/spawn") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new FunctionalNpcRequest(); var result = await GameAdminBridge.SpawnFunctionalNpcsAsync(cmd.Radius, cmd.IncludeSemantic); void_WriteJson(res, new { ok = result.Ok, message = result.Message, detail = result.Detail, ids = result.Ids.Select(id => id.ToString()).ToArray(), }); return; } if (req.HttpMethod == "GET" && path == "/api/weapons") { void_WriteJson(res, new { weapons = GameAdminBridge.WeaponCatalog() }); return; } if (req.HttpMethod == "POST" && path == "/api/weapons/grant") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new WeaponGrantRequest(); var result = await GameAdminBridge.GrantWeaponAsync(cmd.TemplateId); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } if (req.HttpMethod == "POST" && path == "/api/weapons/grant-all") { var result = await GameAdminBridge.GrantAllWeaponsAsync(); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } if (req.HttpMethod == "GET" && path == "/api/timeweather") { void_WriteJson(res, GameAdminBridge.TimeWeatherStatus()); return; } if (req.HttpMethod == "POST" && path == "/api/timeweather/time") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new TimeSetRequest(); var result = await GameAdminBridge.SetTimeAsync(cmd.Hour, cmd.Minute, cmd.Fix, cmd.Transition); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } if (req.HttpMethod == "POST" && path == "/api/timeweather/weather") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new WeatherSetRequest(); var result = await GameAdminBridge.SetWeatherAsync(cmd.WeatherId, cmd.Transition); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } if (req.HttpMethod == "POST" && path == "/api/scene/switch") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new SceneSwitchRequest(); if (!string.IsNullOrWhiteSpace(cmd.Preset)) { var preset = GameAdminBridge.ScenePresets().FirstOrDefault(p => string.Equals(p.Key, cmd.Preset, StringComparison.OrdinalIgnoreCase)); if (preset is null) { void_WriteJson(res, new { ok = false, message = $"unknown preset '{cmd.Preset}'" }, 400); return; } var result = await GameAdminBridge.SwitchSceneAsync( preset.RaidId, preset.InstanceId, preset.UniverseId, preset.X, preset.Y, preset.Z, preset.Facing); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } var custom = await GameAdminBridge.SwitchSceneAsync( cmd.RaidId, cmd.InstanceId, cmd.UniverseId, cmd.X, cmd.Y, cmd.Z, cmd.Facing); void_WriteJson(res, new { ok = custom.Ok, message = custom.Message }); return; } if (req.HttpMethod == "GET" && path == "/api/timelines") { void_WriteJson(res, new { timelines = GameAdminBridge.TimelineCatalog() }); return; } if (req.HttpMethod == "POST" && path == "/api/timeline/play") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new TimelinePlayRequest(); if (string.IsNullOrWhiteSpace(cmd.TimelineName)) { void_WriteJson(res, new { ok = false, message = "timelineName must be non-empty" }, 400); return; } var result = await GameAdminBridge.PlayTimelineAsync(cmd.TimelineName); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } // ---- 剧情对话(2026-10-02 从捐赠端 PrivateServerPor 移植)---- // SyncShowDialog(64967628) 只带 dialogId + beginDialog + 全零 param; // 台词文本 / 镜头 / 语音都在客户端自己的 DialogConfig 里,服务端不搬内容。 if (req.HttpMethod == "GET" && path == "/api/story/dialogs") { var q = req.QueryString.Get("q"); var cut = req.QueryString.Get("cutscene"); bool? cutscene = cut switch { "1" or "true" => true, "0" or "false" => false, _ => null }; var limitRaw = req.QueryString.Get("limit"); var limit = int.TryParse(limitRaw, out var lv) ? lv : 200; var stats = GameAdminBridge.StoryDialogStats(); void_WriteJson(res, new { dialogs = GameAdminBridge.StoryDialogCatalog(q, cutscene, limit), total = stats.Total, cutsceneTotal = stats.Cutscene, }); return; } if (req.HttpMethod == "POST" && path == "/api/story/dialog") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new StoryDialogPlayRequest(); if (cmd.DialogId == 0) { void_WriteJson(res, new { ok = false, message = "dialogId must be non-zero" }, 400); return; } var result = await GameAdminBridge.PlayStoryDialogAsync(cmd.DialogId); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } if (req.HttpMethod == "POST" && path == "/api/story/dialog-voice") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new StoryDialogPlayRequest(); if (cmd.DialogId == 0) { void_WriteJson(res, new { ok = false, message = "dialogId must be non-zero" }, 400); return; } var result = await GameAdminBridge.PlayStoryDialogVoiceAsync(cmd.DialogId); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } if (req.HttpMethod == "GET" && path == "/api/npcs") { void_WriteJson(res, new { npcs = GameAdminBridge.NpcCatalog() }); return; } if (req.HttpMethod == "POST" && path == "/api/npc/spawn") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new NpcSpawnRequest(); if (cmd.NpcCultivationId == 0) { void_WriteJson(res, new { ok = false, message = "npcCultivationId must be non-zero" }, 400); return; } var result = await GameAdminBridge.SpawnNpcAsync(cmd.NpcCultivationId, cmd.X, cmd.Y, cmd.Z, cmd.Facing); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } if (req.HttpMethod == "POST" && path == "/api/items/grant-all") { var body = await ReadBodyAsync(req); uint count = 999u; try { var cmd = JsonSerializer.Deserialize>(body, JsonOptions); if (cmd is not null && cmd.TryGetValue("count", out var cEl) && cEl.TryGetUInt32(out var c) && c > 0) count = Math.Min(c, 999999u); } catch { /* default count */ } var result = await GameAdminBridge.GrantAllItemsAsync(count); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } if (req.HttpMethod == "POST" && path == "/api/currency/grant-all") { await ReadBodyAsync(req); var result = await GameAdminBridge.GrantAllCurrencyAsync(); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } if (req.HttpMethod == "GET" && path == "/api/spirits") { void_WriteJson(res, new { roster = GameAdminBridge.SummonRoster() .Select(s => new { spiritId = s.SpiritId, name = s.Name, unitId = s.UnitId }) }); return; } if (req.HttpMethod == "POST" && path == "/api/spirit/switch") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new SpiritSwitchRequest(); var result = await GameAdminBridge.SwitchSpiritAsync(cmd.SpiritId); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } // ---- 切人演出:按**动画**分(2026-10-03 纠正,见 Bridge 里 AdminSwitchShowEntry 的注释)---- // 主清单 = SwitchSpiritConfig 的 201 行(一行一出演出),角色是第二个下拉。 if (req.HttpMethod == "GET" && path == "/api/spirit/switch-shows") { var rows = GameAdminBridge.SwitchShowCatalog(); void_WriteJson(res, new { shows = rows, total = rows.Count, playableTotal = rows.Count(x => x.Playable), // 0 = 还没进世界。前端用它在目标角色下拉里避开当前角色(选当前角色 = 静默 no-op)。 activeSpiritId = GameAdminBridge.ActiveSpiritId(), }); return; } if (req.HttpMethod == "POST" && path == "/api/spirit/switch-show") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new SwitchShowPlayRequest(); if (cmd.SwitchShowId == 0) { void_WriteJson(res, new { ok = false, message = "switchShowId must be non-zero" }, 400); return; } var result = await GameAdminBridge.PlaySwitchShowAsync(cmd.SpiritId, cmd.SwitchShowId); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } if (req.HttpMethod == "GET" && path == "/api/metro/status") { void_WriteJson(res, GameAdminBridge.MetroStatus()); return; } if (req.HttpMethod == "POST" && path == "/api/metro/toggle") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new MetroToggleRequest(); void_WriteJson(res, GameAdminBridge.SetMetroEnabled(cmd.Enabled)); return; } if (req.HttpMethod == "POST" && path == "/api/metro/lines") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new MetroLinesRequest(); void_WriteJson(res, GameAdminBridge.ConfigureMetroLines( cmd.Lines is { Count: > 0 } ? cmd.Lines : (cmd.Lines is null ? null : []), cmd.PushMs, cmd.TimeFactor, cmd.Auto)); return; } if (req.HttpMethod == "POST" && path == "/api/metro/stop") { void_WriteJson(res, GameAdminBridge.StopMetro()); return; } if (req.HttpMethod == "GET" && path == "/api/wanted/status") { void_WriteJson(res, GameAdminBridge.WantedStatus()); return; } if (req.HttpMethod == "POST" && path == "/api/wanted/action") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new GameAdminBridge.WantedActionRequest(); void_WriteJson(res, await GameAdminBridge.WantedActionAsync(cmd)); return; } if (req.HttpMethod == "GET" && path == "/api/agents/hostile") { void_WriteJson(res, new { agents = GameAdminBridge.AgentSummonCatalog(true) }); return; } if (req.HttpMethod == "GET" && path == "/api/agents/other") { void_WriteJson(res, new { agents = GameAdminBridge.AgentSummonCatalog(false) }); return; } if (req.HttpMethod == "POST" && path == "/api/agent/spawn") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new AgentSpawnRequest(); if (cmd.AgentId == 0) { void_WriteJson(res, new { ok = false, message = "agentId must be non-zero" }, 400); return; } var result = await GameAdminBridge.SpawnAgentAsync(cmd.AgentId, cmd.X, cmd.Y, cmd.Z, cmd.Facing, cmd.Hostile); void_WriteJson(res, new { ok = result.Ok, message = result.Message, uid = result.Uid }); return; } if (req.HttpMethod == "POST" && path == "/api/player/unlock") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new PlayerUnlockRequest(); var result = await GameAdminBridge.ClearPlayerControlStatesAsync(cmd.Mode ?? "curated"); void_WriteJson(res, new { ok = result.Ok, message = result.Message, count = result.Count }); return; } if (req.HttpMethod == "POST" && path == "/api/player/revive") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new PlayerReviveRequest(); var result = await GameAdminBridge.RestorePlayerHealthAsync(cmd.UnitId ?? 0UL); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } if (req.HttpMethod == "GET" && path == "/api/player/vitals-lock") { var (ok, locked, hp, message) = GameAdminBridge.GetPlayerLifeLock(); void_WriteJson(res, new { ok, locked, hp, message }); return; } if (req.HttpMethod == "POST" && path == "/api/player/vitals-lock") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new PlayerVitalsLockRequest(); var result = GameAdminBridge.SetPlayerLifeLock(cmd.Enabled); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } if (req.HttpMethod == "POST" && path == "/api/player/unlock-auto") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new PlayerUnlockAutoRequest(); var result = GameAdminBridge.SetAutoUnlock(cmd.Enabled); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } if (req.HttpMethod == "POST" && path == "/api/agent/spawn-grid") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new AgentGridRequest(); if (cmd.AgentId == 0) { void_WriteJson(res, new { ok = false, message = "agentId must be non-zero" }, 400); return; } var result = await GameAdminBridge.SpawnAgentGridAsync(cmd.AgentId, cmd.Hostile, cmd.X, cmd.Y, cmd.Z, cmd.Distance, cmd.Facing, cmd.Rows, cmd.Cols, cmd.Spacing); void_WriteJson(res, new { ok = result.Ok, message = result.Message, points = result.Points.Select(p => new { x = MathF.Round(p.X, 2), y = MathF.Round(p.Y, 2), z = MathF.Round(p.Z, 2), uid = p.Uid, ok = p.Ok, msg = p.Message }), }); return; } if (req.HttpMethod == "POST" && path == "/api/gm/runutils") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new RunGmUtilsRequest(); var result = await GameAdminBridge.RunGmUtilsAsync(cmd.MethodName, cmd.Args); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } if (req.HttpMethod == "POST" && path == "/api/gm/command") { var body = await ReadBodyAsync(req); var cmd = JsonSerializer.Deserialize(body, JsonOptions) ?? new DoGmCommandRequest(); var result = await GameAdminBridge.DoGmCommandAsync(cmd.Command, cmd.Args); void_WriteJson(res, new { ok = result.Ok, message = result.Message }); return; } if (req.HttpMethod == "GET" && path == "/api/spawn/ids") { var kind = req.QueryString["kind"] ?? "enemy"; void_WriteJson(res, new { items = GameAdminBridge.SpawnIdCatalog(kind) }); return; } void_WriteJson(res, new { ok = false, message = "not found" }, 404); } catch (JsonException jex) { void_WriteJson(ctx.Response, new { ok = false, message = "bad request: " + jex.Message }, 400); } catch (Exception ex) { void_WriteJson(ctx.Response, new { ok = false, message = ex.Message }, 500); } } private static object StatusPayload() { var s = GameAdminBridge.GetSnapshot(); return new { connected = s.Connected, session = s.SessionId, lastSeenUtc = s.LastSeenUtc, unitId = s.UnitId, templateId = s.TemplateId, x = s.X, y = s.Y, z = s.Z, facing = s.Facing, hasTransform = s.HasTransform, ready = s.Ready, pendingTeleportId = s.PendingTeleportId, raidId = s.ActiveRaidId, instanceId = s.ActiveInstanceId, universeId = s.ActiveUniverseId, currentVehicleId = s.CurrentVehicleId, currentVehicleSeat = s.CurrentVehicleSeat, }; } private static Dictionary ParseQuery(string? query) { var result = new Dictionary(StringComparer.OrdinalIgnoreCase); if (string.IsNullOrEmpty(query)) return result; foreach (var part in query.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries)) { var eq = part.IndexOf('='); if (eq < 0) { result[Uri.UnescapeDataString(part)] = ""; continue; } result[Uri.UnescapeDataString(part[..eq])] = Uri.UnescapeDataString(part[(eq + 1)..]); } return result; } /// 取一个 u64 查询参数(骰子试验台用;解析不出来就用默认值)。 private static ulong QueryU64(Dictionary query, string key, ulong fallback) => query.TryGetValue(key, out var raw) && ulong.TryParse(raw, out var v) ? v : fallback; private static async Task ReadBodyAsync(HttpListenerRequest req) { using var reader = new StreamReader(req.InputStream, req.ContentEncoding ?? Encoding.UTF8); var text = await reader.ReadToEndAsync(); return string.IsNullOrWhiteSpace(text) ? "{}" : text; } private static void void_Write(HttpListenerResponse res, string contentType, string text, int status = 200) { var bytes = Encoding.UTF8.GetBytes(text); res.StatusCode = status; res.ContentType = contentType; // 2026-10-02:面板每次改完都要重编译服务端,浏览器缓存旧 HTML/JS 会让「改了没生效」 // 变成假故障(旧按钮调的是已删的函数 ⇒ 点了毫无反应)。彻底禁用缓存。 res.AddHeader("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0"); res.AddHeader("Pragma", "no-cache"); res.AddHeader("Expires", "0"); res.ContentLength64 = bytes.Length; res.OutputStream.Write(bytes, 0, bytes.Length); res.OutputStream.Close(); } private static void void_WriteJson(HttpListenerResponse res, object payload, int status = 200) => void_Write(res, "application/json; charset=utf-8", JsonSerializer.Serialize(payload, JsonOptions), status); /// /// 面板「实时调参」的提交体:changes 是「点号路径 → JSON 字面量」, /// 例如 {"gameplay.aether.trafficTarget": 12}。用 JsonNode 是为了保留 /// 数字/布尔/字符串的类型 —— 交给 做类型守恒。 /// private sealed class SettingsUpdateRequest { public Dictionary? Changes { get; set; } } private sealed class TeleportRequest { public float X { get; set; } public float Y { get; set; } public float Z { get; set; } public float? Facing { get; set; } public string? Mode { get; set; } } /// /// 刷单个物件(2026-10-03):POST /api/gadget/spawn —— 按 pathId 在世界坐标放一个 gadget。 /// ⛑ iScale 默认 0(0 ⇒ 客户端 1:1;100 ⇒ scale=(0,0,1) 空壳,见健身房器材那次)。 /// 街头篮球补篮筐用 pathId=1086190276(Gadgets_Common_BasketballRim)。 /// private sealed class GadgetSpawnRequest { public int PathId { get; set; } public float X { get; set; } public float Y { get; set; } public float Z { get; set; } public float Yaw { get; set; } public int IScale { get; set; } } private sealed class VehicleSpawnRequest { public uint VehicleId { get; set; } public bool Force { get; set; } } /// 自动驾驶巡航速度(km/h,2026-09-30)。 private sealed class AutoDriveSpeedRequest { public float SpeedKmh { get; set; } = 43.2f; } /// /// 正向逮捕(2026-09-25)。uid 为 0 时取最近一次 GM 召唤的 NPC。 /// ⛑ 这是验证通道不是玩法入口:官方逮捕挂在盘查面板(C# 侧)上, /// 客户端何时发 AskPoliceExamInteract(63233964) 还没有实据。 /// /// 捐赠端 /api/lixi/police 的请求体(逐字对齐捐赠端 AdminWebServer.cs:551-554)。 private sealed class LixiPoliceRequest { public string Action { get; set; } = ""; } private sealed class PoliceExamineRequest { public ulong Uid { get; set; } /// /// 塞几条罪名。0 ⇒ 全部(PoliceFineCatalog 里 IsShow 的行,当前 16 条)。 /// 2026-10-03 改:此前默认 3,盘查面板只认得 3 条案底;现在默认全发。 /// 想只发几条就显式传 1..16。 /// public uint FineCount { get; set; } = 0; /// true ⇒ 先把玩家挪到目标跟前再开盘查面板。 /// ⛑ 这条路 2026-09-25 已证伪(instant 被上报覆盖 / loading 反序列化失败), /// 留着只是回滚用;日常请用 。 public bool Teleport { get; set; } /// true ⇒ 只发「收盘查面板」帧(面板是模态的,没这个就只能用重开客户端脱身)。 /// ⛑ 2026-09-25 修:之前这个字段反序列化出来了却没传给 ExamineNpcAsync, /// 等于关面板通道一直是死的。 public bool Close { get; set; } /// true ⇒ 把目标 NPC 瞬移到玩家正前方 2.2 m 再开盘查面板(推荐)。 public bool Pull { get; set; } /// 罪名帧(SyncAgentCrimes 68251759)的线格式旋钮,见 PoliceCrimesRequest.Wire。 public string Wire { get; set; } = "a"; } /// /// POST /api/police/react:推一次「盘查反应」SyncPoliceExamReaction(64480222)。 /// ⛑ 必须在玩家点过某个选项之后再推 —— OnReceiveReaction 用的是 /// self.PreOptionId,没点过它是空的。 /// private sealed class PoliceReactRequest { /// PoliceExamReactionConfig 的 id。配置表只在客户端手里, /// 服务端没有 ⇒ 先从小 id 往上试,看面板哪个有反应。 public uint ReactionId { get; set; } = 1; } /// /// POST /api/police/crimes:单独下发「案底」(SyncAgentCrimes 68251759)。 /// 单独留一个口子是因为:盘查面板开着的时候也能补发,面板刷新就会读到, /// 不用重新开一次面板 —— 调线格式时省很多轮。 /// private sealed class PoliceCrimesRequest { public ulong Uid { get; set; } /// 罪名 id(PoliceFineCatalog.Id)。留空 ⇒ 发 RenderableFines 全部(16 条)。 public List? Fines { get; set; } /// 线格式旋钮:a(默认 [FF][Int7 count+1]) / b([FF][Raw7 count]) / /// c([Int7 count+1] 无 marker) / d([FF][u32 count])。猜错了不用重启,换字母重发即可。 public string Wire { get; set; } = "a"; } /// /// POST /api/police/exam-auto:「盘查模式」开关 —— 打开后,玩家走近一个 NPC、 /// 准星对上他(客户端自己会发 AskInteractCmd cmd=33),盘查面板自动弹出; /// 再看他一次即关闭(toggle)。 /// /// ⛑ 默认关。走路时扫到谁都弹面板会很烦,而且 spawn-group 一次性放 60 人时 /// 客户端会给每个人发一轮 cmd=33 ⇒ 不关就是连弹。 /// ⛑ 卡住了随时 POST /api/police/exam-close 强制关,不用重开客户端。 /// private sealed class PoliceExamAutoRequest { public bool Enabled { get; set; } } /// POST /api/police/exam-spawn:AgentId 为 0 ⇒ 用默认市民(40954013 咖啡店店员)。 private sealed class PoliceExamSpawnRequest { public uint AgentId { get; set; } } /// /// POST /api/police/fine:手动推一次罚款结果 —— /// ShowReceiveRewardDetail(64145515) 且 Reason = ItemReason.PoliceFine(180001)。 /// /// ⛑⛑ 正常链路是玩家点「开罚单」⇒ C2S AskPoliceFineNpc(63548238) ⇒ 服务端自动回这一帧。 /// 留这个口子是为了不等客户端也能验证线格式对不对:客户端报 /// 「64145515 反序列化失败」就说明字节错了,改完重发即可,不用重启服务端。 /// ⛑ 但 OnDropPoliceJobExp 要求 needShowFineResult 为真(点过开罚单才置位), /// 所以裸推只能验"反序列化过不过",要看到结果面板还是得先走一遍开罚单。 /// private sealed class PoliceFineRequest { /// 罚款金额(RewardDetail.Money,double)。 public double Money { get; set; } = 500; /// 警察职业经验(JobExpInfo[11300003])。 /// ⛑ 必须 > 0 —— OnDropPoliceJobExp 里 Money>0 且 JobExpInfo 没命中 Police /// 时不会弹结果面板。 public uint JobExp { get; set; } = 50; } /// 里希牢房(2026-09-30):POST /api/police/prison。 /// action = commit(收押,带 uid)|release(带出,不用 uid)。 private sealed class PolicePrisonRequest { public string Action { get; set; } = "commit"; /// uid 以字符串收(面板那侧是大整数,JS Number 装不下),空 ⇒ 0。 public string Uid { get; set; } = ""; } /// 里希押送(2026-09-30):POST /api/police/escort。 /// action = board(押上车,带 uid/vehicle/seat)|alight(押下车)。 private sealed class PoliceEscortRequest { public string Action { get; set; } = "board"; public string Uid { get; set; } = ""; public string Vehicle { get; set; } = ""; public int Seat { get; set; } = 1; } /// GATE Lua(2026-09-30):POST /api/gatelua —— 把整段 Lua 下发到客户端执行。 private sealed class GateLuaRequest { public string Script { get; set; } = ""; } /// GATE Lua F 按钮(2026-09-30):POST /api/gatelua/fbtn。 /// action = add|remove;uid 由服务端拆成 low32/high32 注入脚本。 private sealed class GateLuaButtonRequest { public string Action { get; set; } = "add"; public string Uid { get; set; } = ""; public string Text { get; set; } = "里希盘查"; } /// 嘉姆入侵:三步各自独立开关,方便蹚哪一步出问题。 private sealed class FactionInvasionRequest { public int EventId { get; set; } public bool Teleport { get; set; } = true; public bool Summon { get; set; } = true; public bool Occupy { get; set; } = true; } private sealed class PoliceArrestRequest { public ulong Uid { get; set; } /// true ⇒ 先把玩家挪到目标跟前(服务端自己算坐标),再发逮捕帧。 /// 行人是走动的,客户端上报包里的坐标是几十秒前的,不能拿来算落点。 /// ⛑ 同 PoliceExamineRequest:这条路已证伪,请用 。 public bool Teleport { get; set; } /// true ⇒ 把目标 NPC 瞬移到玩家正前方 2.2 m 再发逮捕帧(推荐)。 public bool Pull { get; set; } } private sealed class DonorNpcSpawnRequest { public List? Items { get; set; } public float XSpacing { get; set; } = 1.5f; public float ZSpacing { get; set; } = 1.5f; public int MaxPerRow { get; set; } = 10; } private sealed class CrowdRefreshRequest { /// 临时覆盖密度;留空/null 则按所在地块与时辰现算。 public float? Density { get; set; } } private sealed class BlackoutStartRequest { /// 控制器落点 X;三个都给了才生效,否则按配置(默认刷在广告牌上)。 public float? X { get; set; } public float? Y { get; set; } public float? Z { get; set; } } private sealed class CivilianGroupRequest { /// Id of a group in Configs/CivilianGroups.json (1-based, G001 …). public int GroupId { get; set; } = 1; } /// Land-plot citizen seeding (T0-8). Hour null = use the hour the world clock shows. private sealed class CivilianDistrictRequest { public float Radius { get; set; } = 150f; public int MaxTotal { get; set; } = 120; public int? Hour { get; set; } } /// Functional-NPC seeding. Radius <= 0 means the whole current scene. private sealed class FunctionalNpcRequest { public float Radius { get; set; } public bool IncludeSemantic { get; set; } = true; } private sealed class EnemyTerritoryRequest { public float Radius { get; set; } } private sealed class WeaponGrantRequest { public uint TemplateId { get; set; } } private sealed class TimeSetRequest { public int Hour { get; set; } public int Minute { get; set; } public bool Fix { get; set; } = true; public int Transition { get; set; } = 5; } private sealed class WeatherSetRequest { public int WeatherId { get; set; } public int Transition { get; set; } = 15; } private sealed class SceneSwitchRequest { public string? Preset { get; set; } public uint RaidId { get; set; } public ulong InstanceId { get; set; } public uint UniverseId { get; set; } public float X { get; set; } public float Y { get; set; } public float Z { get; set; } public float Facing { get; set; } } private sealed class StoryDialogPlayRequest { public uint DialogId { get; set; } } private sealed class TimelinePlayRequest { public string? TimelineName { get; set; } } private sealed class NpcSpawnRequest { public uint NpcCultivationId { get; set; } public float X { get; set; } public float Y { get; set; } public float Z { get; set; } public float Facing { get; set; } } private sealed class SpiritSwitchRequest { public uint SpiritId { get; set; } } /// Body of POST /api/spirit/switch-show:指定「播哪条演出」+「切成谁」。 private sealed class SwitchShowPlayRequest { public uint SwitchShowId { get; set; } public uint SpiritId { get; set; } } /// Body of POST /api/metro/toggle. Defaults to true so a body-less POST starts the dispatcher. private sealed class MetroToggleRequest { public bool Enabled { get; set; } = true; } /// /// Body of POST /api/metro/lines. null keeps the current selection (that is /// how the panel retunes only the period / time factor); an empty list turns every line off - which is /// why the distinction between null and empty matters here. /// private sealed class MetroLinesRequest { public List? Lines { get; set; } public int? PushMs { get; set; } public double? TimeFactor { get; set; } /// /// true = 回到「跟随当前地区」(按 RaidConfig.LightRailLineIds 自动选线)。 /// 不带 lines、只带 auto 时使用;发了 lines 就转手动,直到再发一次 auto。 /// public bool? Auto { get; set; } } private sealed class RunGmUtilsRequest { public string MethodName { get; set; } = string.Empty; public List Args { get; set; } = new(); } private sealed class DoGmCommandRequest { public string Command { get; set; } = string.Empty; public List Args { get; set; } = new(); } private sealed class AgentSpawnRequest { public uint AgentId { get; set; } public float X { get; set; } public float Y { get; set; } public float Z { get; set; } public float Facing { get; set; } public bool Hostile { get; set; } } private sealed class PlayerUnlockRequest { /// "curated" (hand-picked control states) or "sweep" (every id in 1..500). public string? Mode { get; set; } } private sealed class PlayerReviveRequest { /// Unit to heal. Omit for the tracked player unit; pass the client-side entity id /// (packets log: AskUnitMoveActionSimple.UnitId) when that one owns the HP bar. public ulong? UnitId { get; set; } } private sealed class PlayerUnlockAutoRequest { public bool Enabled { get; set; } } private sealed class PlayerVitalsLockRequest { /// True keeps the player's HP above zero (never knocked out). public bool Enabled { get; set; } } private sealed class AgentGridRequest { public uint AgentId { get; set; } public bool Hostile { get; set; } public float? X { get; set; } public float? Y { get; set; } public float? Z { get; set; } public float Distance { get; set; } = 30f; public float Facing { get; set; } public int Rows { get; set; } = 2; public int Cols { get; set; } = 3; public float Spacing { get; set; } = 3f; } /// 搜打撤面板的统一开关提交。全部可为 null —— null 表示这一项不改。 private sealed class ExtractionOptionsRequest { public bool? FakeMatch { get; set; } public uint? PreferredRaid { get; set; } public bool? ExitToMainCity { get; set; } public bool? ExitSettlement { get; set; } public string? WheelSync { get; set; } // off / incremental / full public bool? WheelWeaponData { get; set; } } private sealed class ExtractionStartRequest { public uint RaidId { get; set; } = 23301277; } /// 「任意 notify 探针」的提交体。Hex 可空 = 推空体。 private sealed class ExtractionNotifyRequest { public uint MethodId { get; set; } public string? Hex { get; set; } } private const string PageHtmlTemplate = """ ANANTA · GM Console
Ananta GM Console
本地 127.0.0.1:17888

快捷功能

4 张卡

场景切换



                  

发放全道具



                    
说明

发放全武器:解锁全部武器,游戏内通过武器轮盘装备。
发放全道具:每个道具只占一个格子,重复点击会把已有格子补到上面填的数量。
发放全货币:金币 / 钻石 / 绑定钻三种钱包货币各自补到 999 万(已 ≥ 该数的币种不会重复发放)。
⛑️ 钱包货币不进背包——塞进背包会让 HUD 与背包显示不一致,所以这三种是被道具发放主动跳过的,只能从「发放全货币」走钱包通道。
三者都会写入玩家存档,重登后仍在。

生成敌人(敌对单位)

单位生成在你点击那一刻的站位上 —— 点完请走开两步、转身回头看,别站在原地判断成败。

                    
说明

注意同名陷阱:黑金节奏手 有 20+ 条同名记录,按名字选必错,只有精确 Id 可用。
同名同模型的重复项已折叠为最小 ID,在「或输入ID」填被折叠的 ID 可精确生成。

生成载具

车辆出现在身旁,走近按 F 上车 / 登机。
飞行 / 水上是强制生成,绕过车队白名单。


                    
说明

两条生成路径走的是同一条命令 /api/vehicle/spawn,只是飞行 / 水上多带一个 force:true 绕过车队白名单。
「进入 / 离开」与下面的调试输出框对两种生成方式共用。
飞行/水上载具需要合适地形(船要水面,飞机要开阔空域)。走近按 F 登机。

角色、时间和天气

3 张卡

时间与天气

加载中…

                  

切换主控角色


                    
说明

服务端直切,不切场景、不播换人演出,目标直接出现在你当前位置。
名单是 roster 全部角色,含 CanJoin=false 的剧情角色 —— 这些在 Z 轮盘里是灰的,只能从这里切。
资源不全的角色切过去会 T-pose 或无武器,属正常,换回即可。

角色日程

加载中…
只列官方有日程的角色(没时刻表的 OC 占位行 / 两个主角不摆也不显示);角色要等你走近(默认 260m)才会被放出来。
传送落点 / 状态判读

「传送过去」会落在角色斜后方约 7 米并转向面向他,不会贴在脸上。 状态栏写「走近才出现」= 还没进 260 m 的放出范围。


                  

职业和独立玩法

8 张卡

Seymour 大停电


                    
这条链怎么走 / 怎么看成功

服务端只做三件事:① 发任务图 SyncPlayerLoadTaskSpoonDebug(60002222, 34) ② 用 SyncGadgetAOIAddAndRemove 放下控制器 Gadgets_Hacker_Tetris (pathId 1166913245,GraphId 36003094)③ 回答 AskStart 的 deadline、 在 Report 时推那一下 Creation 56860922。
灭灯、广告牌换肤、俄罗斯方块 UI 全是客户端原生图自己的事,服务端不演。

这次特意没做 Lua 下发:donor 靠 SyncDoToLuaMaster(45437825) 帮玩家按那个按钮, 我们没实现那条 GATE 通道。所以刷完控制器后服务端就停手,等客户端自己触发。

官方玩法 = 对着广告牌骇入(2026-09-23 实测定论):走到广告牌前 → 鼠标中键骇入 AskHack(44) → 城市停电 → 在广告牌上玩俄罗斯方块。所以控制器必须刷在 广告牌坐标上,骇入交互点才落在牌子本身。
⛑️ 别刷在玩家脚下:那会得到「距离恒为 0 ⇒ 站哪都能骇入」的假象(实测过)。
⭐ 落点 = 屏幕正中心(用户定):面板给的三个框是控制器坐标, 默认 2001.48 / 70 / 2287.69 = 官方牌 mall006a 屏幕的中心。人不要传那里 —— 70 m 高空; 「人传过去」永远走 BO_SPOTS[*].l(人行道点 1992.47,-0.01,2313.44)。
⚠️ 屏幕在高空 ⇒ 骇入交互可能超出射程。按不到就点「退回牌子脚下」把控制器放回地面。
⛔ donor §6.3 的 Position=(0,0,0) 已被实测否掉 —— 交互点会在 4 km 外。

结果读法:ok waiting-for-interact = 控制器已被客户端 ack,现在轮到客户端出牌。 controller-ack-timeout = 20 秒内没 ack,去 Player.log 找 deserialize failed。

通缉系统(警车版)


                    
这条链怎么走 / 目前能干什么

涨犯罪值(服务端自己记,不吃客户端任何犯罪上报):打中 NPC +10、击杀 +60; 阈值 50/100/200/400/800 ⇒ 1~5 星,星级一变就推 SyncPlayerCrimeLevel(68841232) 刷 HUD 星数。
出警:按星级刷 1~3 台真警车(81007029 / 81007078 / 81004027), 落在玩家背后 30~54 m;追人是客户端自己的车辆 AI, 玩家在车上时额外发 SyncPoliceDispatchVehicleChase(68586100) 咬住那台车。
结算:逃脱 等级 × 5000 入账,被捕 等级 × 2500 罚款。
自动判据(服务端真信号):通缉持续满 90 s ⇒ 自动逃脱;玩家倒地 ⇒ 自动被捕。

⛔ 这一版没有步行警员:捐赠端那半靠它自己的 crowd-agent 管线,本方没有, 而且它那份日志早于它自己的最后一次改动 ⇒ 那半根本没被实机验证过,不照抄。
⛔ 不接官方犯罪通道:SyncAgentCrimeAdd(68831313) / ReportPoliceCrimeEvent(67146776) / GmAddCrimeValue(69946581) 全都没碰, 代价是「只有服务端认可的行为会涨值」,好处是不会踩官方那套没摸清的帧时序。
⛔ 也没有犯罪任务链(商店抢劫 / 偷车)—— 那半依赖本方完全没有的任务状态物件。

怎么验:面板点「设为 3 星」⇒ 游戏里背后应当冒出 3 台警车并追过来;点「判定被捕」⇒ 推 SyncPoliceBeginArrest(68839124) 并扣钱。日志搜 [WANTED]。

势力入侵(嘉姆 · 占领地盘)

加载中…

                    
说明

官方那套「操控嘉姆占敌方地盘」拆成三步,本面板把它们串起来; ⛑ 不代切角色 —— 请先自己在角色界面切到嘉姆(spiritId 15020989)。
两个随从(2026-10-01 改过)= TAB 轮盘实际会下发的那两个槽: slot 10520000 → 40651922 晴、slot 10520017 → 40155016 盖斯; 原来那两个 40651920/40651921 的解锁条件是「成立犬魔社」,玩家没解锁,轮盘里压根不出现。
⛑ 随从没有血条、不会主动出手(模板无 ShowHPBar/InitWeaponId、attacks=none), 生成成功时只会站在你旁边 —— 日志里能看到 AskInteractCmd 就说明它出现了。
占领只是推一个包:SyncFactionInfluenceAreaOccupy(64830440) = areaId + occupy 两个字段,地图上的变色由客户端自己画(MapSubSystem_Gangster.lua)。
点位表:ClientData/4229938/Configs/FactionInvasionSpots.json, 由 ConfigDump_v3 的势力地盘/势力事件/任务事件三张表 join 导出。
⛔ 边界:本面板只做到「送到地方 + 放两个随从 + 推一个占领包」, 没有敌人波次、占领进度条、结算 —— 那套是客户端 lua 的状态机 (MapSubSystem_Gangster.lua / GangsterAreaClientState.lua), 主线服务端没有对应驱动。

科斯机甲:原生控制


                    
这条链怎么走 / 怎么用 / 怎么验

它为什么进不了花名册:FightSpiritConfig 15022030 "特摄战原型-科斯" IsPublish=false / CanJoin=false / Invisible=true ⇒ GM 面板的换人下拉里选不到它, 客户端 roster 里也永远不会有它。这是隐藏可控原型,不是普通车辆。

服务端要补的三件事(缺一不可,见 GameRouter.Kesi.cs): ① SyncPlayerAddNewSpirit(64691088) 增量补 SpiritViewData —— 登录包里没有它, 客户端 SpiritManager.Id_to_Tid 查不到 tid,后面所有和它相关的包会被静默丢掉; ② 科斯专属 loadout —— Sceneitem 80003500"科斯冲锋枪"(没有 WheelIcon, 所以常规军械库过滤器会判掉它,必须单独建一份); ③ 走本服已在用的切换链路做身份移交,不另发明一套切人。
怎么用:进游戏 → 下车 → 站室外空地 → 点「控制科斯机甲」;想退出时点「返回原角色」。
⛔ 需实机验证:能确认的只有服务端协议侧齐了。模型异步加载、镜头、 机甲武器与技能面板还没用游戏画面确认过 —— 捐赠端自己也标了"尚需游戏内验证"。
怎么验:服务端日志搜 [KESI],成功应为 enter=True ok=True; 失败先看是不是 缺少科斯原生武器/技能配置,再看 [SWITCH-MIN]。

里希盘查

原生盘查测试:切换里希,下车并退出机械狗骑乘/遥控,点「生成盘查测试 NPC」。等模型加载后站到 6 米内,点击“启动原生盘查”,无需 F。此入口仅选择本场景已生成的最近 4065053;地图市民与其他模型暂不开放。
⛑ 测试模板固定为 4065053,不要改:客户端按 AgentConfig.json → ExamType 判定盘查资格,只有 ExamType=2 的模板才会挂原生盘查流程(40431099 等 ExamType=0 的模板面板能开但按键无反应)。

盘查走客户端 S002 原生流程,已接搜身/询问回复,但实际界面与双人动画仍需验证;卡住请「结束盘查」,不会删除 NPC 或召回支援。罚单判定、押送结案、直升机搜查未完整恢复,不发放虚构奖励。机械巡卫犬 / 武装支援本次未移植(依赖 HackerDevices 与友方随从子系统)。


                    
🚓 押送(押上警车 / 押下车)
🚔 牢房(收押 / 带出)

                    
牢房这条链能做到哪一步 / 怎么验

能做的:把服务端账本里的 NPC(行人 / 地盘敌人 / 功能 NPC)移进牢房内坐标、 停掉它的 AI、改写服务端账本;带出时移回门口并按原值恢复 AI。
⛔ 做不了的:① 门口的 F 交互按钮 —— 捐赠端靠 SyncDoToLuaMaster(45437825) 的 GATE Lua 通道下发 InteractBtnMgr:AddBtn —— 这条通道主线已经搭好了 (见「街头篮球」卡里的「🧪 客户端 Lua(GATE 下发)」,10-04 已从本卡搬过去), 但按钮脚本本身还没接;② 原生押送过场 —— 要客户端 AskPoliceEscortInteract 加捐赠端那整套 PoliceRuntime 状态机,主线没有。
⇒ 所以入口就是这里这两个按钮(POST /api/police/prison)。
怎么用:① 切成里希、回主城(实例 20001222);② 走到警局牢房门口 4 m 内(X 2379.89 / Z 1823.08);③ 用上面「生成一个 NPC 并盘查」刷个人, 把它的 uid 填进左边;④ 点「收押进牢房」。
怎么验:服务端日志搜 [POLICE-PRISON];本页「刷新」看 当前收押 uid 与 离门口。

这条链怎么走 / 怎么用 / 怎么验

主入口(推荐):切成里希(FightSpiritId 15021021)→ 点「生成一个 NPC 并盘查」⇒ 服务端在你正前方 3.5 m 生成一个市民 (默认 40954013 咖啡店店员,EnemyClassType=9 非敌对), 等 1.2 s 让客户端把单位建好,然后直接对它开盘查面板。再点一次会先回收上一次生成的那个。
⛑ 为什么这条路最靠谱:uid、坐标、时机全在服务端手里,不依赖客户端任何事件 ⇒ 点一下必然出结果。
⛔ 退出只能用这里:面板里长按 F / ESC 都不做退出了 —— 四版客户端按键钩子全部实机失败(12:2x 拍板放弃),游戏内钩子已停用。 ⇒ 要关面板就点「强制关闭面板」,服务端会连发两帧收盘查帧防丢包。

怎么用(备选:扫描模式):① 切成里希;② 勾上面「扫描模式」; ③ 回游戏走到人多的地方 ⇒ 客户端每扫到一个附近的人就开盘查面板。
⛔ 关键事实:cmd=33 的 s1 不是"你准星对着的那个人" —— 10:46:37 实机取证:那批 s1 是连号 13 个、每秒约 3 个, 玩家并没在瞄准谁。它是客户端批量上报附近实体。
⇒ 所以"点一下装备 → 等你聚焦"这个模型在协议层不成立。现在改成由这个开关控制开/关。
⛔ 一次性:弹一次后聚焦模式自动关闭,再想盘查必须再点一次按钮; 60 秒内没用上也自动失效(防"陈年武装")。

为什么不是按 F / 按 R / 说话: · 按 F —— AskPoliceExamInteract(63233964) 实机 0 条;街头 NPC 没有交互位。 · 按 R —— 客户端钩键两次实机都没打通,代码还在但别指望它。 · 说话(cmd=34) —— 它同时是交互心跳,会周期自动发 ⇒ "有时候触发有时候不触发"。
召唤物不当盘查对象:机械狗 / 无人机 / 蜘蛛的 uid 判成 kind=summon ⇒ 不开面板。
怎么验:服务端日志搜 [EXAM-AUTO];客户端 Player.log(grep 要 -a)搜 [DRMK-EXAM] 看 SyncAgentPoliceExamData … opening POLICE_INQUIRY_PANEL。

街头篮球

B 档 QTE 投篮小游戏(BasketballGameManager.CreateGameCs):在玩家脚下开一局, 球架与双方模型全由客户端自己生成。
⛑ 篮筐:服务端自动刷篮筐的功能已删除(10-04),现在场上只有判定点 (锚点 +(0,+2.98,+1.61)),投的是「虚空」。
⛑ 对手红铁桶(42071029):模型其实加载成功,但 body / body01 两个 SkinnedMeshRenderer 是 enabled=false ⇒ 预设里已用 coroutine.start + coroutine.wait 在开局后 1.5 s 起轮询点亮,Player.log 搜 [DRMK] basketball npc poll。


                    
街头篮球怎么开 / 怎么验 / 坑在哪

开一局:点上面的「开始游戏」即可,在玩家脚下开局。 一局约 14 秒(00:0x 实测:开局到 TriggerBasketballFinish 只有 14 s)。
⛔ 别再看下拉:GATE Lua 下拉里的「④ 街头篮球 — 在脚下开一局 / ⑤ 街头篮球 — 收摊」两项 10-04 已移除,现在只用本卡这两个按钮。
收摊:点上面的「结束游戏」,只 DestroyGame (自动刷篮筐已删,不再扫场景收 gadget)。

怎么验:客户端 Player.log(grep 要 -a)搜 [DRMK] basketball start = 开局脚本到了;[DRMK] basketball npc poll1 n=2 turned=2 off=0 = 对手已点亮。

⛔ 三个已证实的坑:
① 别信「模型没加载」 —— 模型一直在,只是网格被关;判据是 SkinnedMeshRenderer.enabled,不是节点存不存在。
② NpcNode 下 npc1..npc15 是空占位,children=0 是正常的; 模型挂在 baseUnit.ModelSlot.transform,按篮架 slotPosition 摆位。
③ 面板页面会被浏览器缓存 —— 改完面板必须 Ctrl+F5,否则跑的还是旧 JS (判据:新 hook 不触发、旧 hook 触发)。
④ Lua 延时用 coroutine.start + coroutine.wait(实测可用); 裸 wait / UpdateBeat.Add 在本版是 nil。

不要抬头!(搜打撤)

加载中…
任意 notify 探针(取证用)—— 直接把一条裸包按 methodId 推给客户端,hex 留空 = 推空体

                    
说明

整套默认关:config/private-server.json 里 world.content.extraction.enabled=false。 关着的时候所有搜打撤相关 handler 一律 ack-only 跳过。

进图两条路:① 勾「伪造匹配」后在游戏里点开始(⛑ 开着时所有开局都会进来);② 直接点「进图」。两者走同一个 SwitchSceneAsync。

武器轮盘:下标 0 = 拳头、1 = 私有武器、下标 = CellX + 2(第一格是 2)。 整表恒 9 槽(2 + 7),空槽必须是 null,下标 0 也不能空 —— 空了就切不回拳头。

验收顺序:进图 → 开一个容器(应有按品质长短不同的读条)→ 把枪拖进背包 → 右键装备 → 看这张表里多出一行 → 游戏里 Tab / 滚轮能依次切到。服务端日志应有 [EXTRACT] wheel snapshot pushed … slots=9。

还没做的:① 撤离点负载没抓到包;② 结算帧只有 32 字节的空战斗数据;③ 容器内容不落盘,重登一天重洗。

排球

C 档 零协议小游戏(VolleyballGameManager.CreateLuaGameCs):在玩家脚下开一局, 球场、球网、球、相机、4 个角色(玩家 + 我方 AI + 对方 AI×2)全部由客户端 prefab 生成, 服务端一个协议都不用发,也不需要世界里摆任何 gadget / waypoint / NPC 实体。
赛制:6 分制、需净胜 2 分;扣杀 / 传球走 QTE(完美 / 正常 / 过早 / 过晚 / 假动作五档)。
⛔ 4 个角色是「替换形象」:本版客户端排球原生的 modelId (86952591~94)建不出模型 —— Lua 版 GetDialogModel(modelId) 与原生版 GetCutSceneModel→LoadModel 两条路都 NRE。 预设已改走 GetDialogModelByAgentId,4 个角色统一用玩家自己的形象 (队伍靠 curTeam 和站位区分)。
⛑ 试过给 4 个角色分不同形象(玩家 / Garm / Rin / Bansy),但只有玩家自己那个能建成 ⇒ loadFinishCount 到不了 4 ⇒ OnAllLoaded 不触发 ⇒ 局根本开不起来,所以不再分。


                    
排球怎么开 / 怎么验 / 坑在哪

开一局:点上面的「开始游戏」,在玩家脚下开局。
收摊:点「结束游戏」。球场是 prefab 实例, DestroyLuaGame() 会连带 Destroy(rootNode), 不像篮球那样需要额外收篮筐。

怎么验:客户端 Player.log(grep 要加 -a)搜 [DRMK] volleyball start = 脚本到了; [DRMK] volleyball poll1 game ok chars=4 = 球场已生成、4 个角色加载完。
· mgr=nil ⇒ 客户端 lua 里没有 gVolleyballGameMgr(该玩法没打包进 4229938)。
· 一直 no game yet ⇒ prefab VolleyballGameRootNodeCopy.prefab 加载失败。

⛔ 两个已证实的坑:
① 参数必须传位置数组 —— 排球内部是 Vector3.New(unpack(rootPosition)),传 {x=..,y=..,z=..} 具名表会 unpack 出 nil ⇒ 直接报错。 篮球那边是 position.x 具名访问所以能过,别把篮球的写法复制过来。
② 结算发不出去 —— 正常打完会发 SetChallengeResult(63571710),主线 Ananta.Handlers 里没有这个 handler ⇒ 掉 unknown-methods.log。画面不受影响。

任务和动画播放

3 张卡

过场动画/时间轴播放


                    
说明

过场由客户端自己播(NPC、摄像机、视频都在游戏里)。播不出来多半是这条时间轴要求特定场景/位置,先传送过去再试。

切人演出(按动画)


                    
说明

数据源是 SwitchSpiritConfig.json,一行 = 一出切人演出 —— 这才是官方的粒度:主下拉是动画(地点 · 动作 · timeline),第二个下拉才是「切成哪个角色」。

源文件 201 行,但 ClientConfigRepository 装载时按 SwitchType=2 / RaidId=23300888 / 未作废 / Weight>0 / TimeLine 非空 过滤掉 131 条 ⇒ 这里能列出的是 70 条(其中 1 条带随从,服务端补不出 ⇒ 默认范围排除)。

走 SyncPreSwitchSpirit 两段式:先推这条演出的配置,等客户端播完回报 ReportPreSwitchSpiritFinish 再真正换人,并落到该行的 Position 锚点(演出本身就有镜头位移,所以会跟着走到那里)。

① 目标角色下拉由这条演出的 FightSpiritId 填充 —— 不是每个角色都能播每一条,官方是按这种搭配编写的。选不到某个角色是正常的。
② 「随从行」(AgentId 非空)服务端这条链路补不出陪衬角色,默认范围已排除; 切成后只是演出里少几个 NPC,不会崩。
③ 演出会把你传送到它的锚点,可能几百米外 —— 这是原版行为。

剧情对话 / 台词播放


                    
说明

走 SyncShowDialog(64967628):服务端只发 dialogId + beginDialog + 全零 param,文本、镜头、语音都在客户端自己的 DialogConfig 里,服务端不搬内容。索引共 — 条,其中带镜头过场 — 条。

索引里查不到的 id 也照样能下发(客户端有自己的表)。如果点了没反应, 多半是这条台词要求特定任务/场景上下文。

人流和车流

2 张卡

车流调参(即时生效 · 免重启)


                    
说明

改完点「保存改动」:写回 正在使用的那份 private-server.json(会自动同步一份到 Ananta.Server/Ananta.App/bin/Debug/net8.0/config/)并热重载,不用重启服务端, 车流下一 tick 就按新值跑。带 ⚠ 的项改坏了会让车不动或满街撞,改动前先看括号里的提示。

人流调参(即时生效 · 需手动推送)

加载中…
说明

★ 改完必须点「立即推送密度」:人群没有 tick 循环,只在「进世界」和「移动 >35 米」推包,原地改配置是没有第二个时机让它生效的。
⛔ 硬前置:trafficEnabled(车流总开关)必须是 true —— HasZoneGraph=true 是走车辆 init 那条线发的,车流关着人群根本不会初始化。
上方读数显示你脚下此刻会算出的密度,改参数后不用走到 35 米,点右边「重新载入」就能看到目标值怎么变。
crowdDensity 超过 1 会不会被客户端 clamp 未验证,别一次拉到 4 以上。

生成和铺设NPC

7 张卡

生成NPC(角色)


                    
说明

生成的是独立 NPC 实体,不接管玩家控制。没出现就查服务端日志 [NPC] spirit-spawn。

生成市民(非敌对单位)

坐标取玩家当下站位/朝向,贴地、带完整受击反馈。

                    
说明

同名同模型的重复项已折叠为最小 ID,在「或输入ID」填被折叠的 ID 可精确生成。本地生成不读坐标——直接用玩家脚下位置,不会浮空。

街头市民(预设整组)

加载中…

                    
查看该组市民清单(最多 60 个)
说明

生成位置:玩家当前站位前方 3 米起的一块方阵,槽位随机打乱,想摆在哪条街上就走到那条街再点。
★ 系统行为:排在最后十几组的是对话 / 伙伴 AI / 阵营交互之类的系统动作,刷出来大概率没反应或姿势怪,属正常现象。
不持久化:重登 / 换场景这批市民由客户端自行清掉,想再来一批重新点即可。

按地块铺市民(真实街区)

加载中…

                    
说明

按城市里真实的地块(住宅区 / 商业区 / 学院区…)把市民放到人行道上,而不是排在角色面前。
「半径」是离你多远以内的地块参与;人多时按距离由近到远分配,远的会先被砍掉。
「时段」决定每个地块放多少人——数据来自该地块自带的一天 24 小时人流曲线。选「跟随游戏时钟」就用当前游戏里的时间。
想先看效果可以把时段固定成 18。占地块一样:不持久化。

功能 NPC(店员 / 售票 · 固定点位)

加载中…

                    
说明

把带 F 交互的 NPC 放到它本来该在的位置:店员站在自己店门口,摩天轮售票员站在摩天轮下。
默认不用点:进城后跟随循环自己跑 —— 走近 130 m 自动出现、走远 230 m 自动撤走,全程无需操作。
坐标来自 ShopConfig.Position / 城市玩法点 / 室内场景锚点,并已吸附到最近的人行道站位。
服务端只负责把人放下去 —— 按 F 之后干什么是客户端 lua 的事,服务端不参与、也不扣钱。
点位文件:ClientData/4229938/Configs/FunctionalNpcSpots.json,手动改坐标即时生效。

地盘敌人(帮派势力 / 营地)

加载中…

                    
说明

把帮派分子放到官方地盘坐标上 —— 犬魔社、谜面帮、电视脑、黑金组、龙治团、焱风联合会…
默认不用点:进城后跟随循环自己跑,走近 90 m 出现、走远 160 m 撤走。
坐标来源:FactionInfluenceAreaConfig 的 24 个地盘 × FactionAreaPointConfig 的 85 个角点, Y 吸附到最近的人行道站位(dump 只给了 XZ)。
行为:走 SpawnAgentAsync,forbidAetherAI=true ⇒ 出生后由客户端行为树接管, 所以它们不像市民那样沿人行道走,而是在地盘附近待机、玩家靠近才进入战斗。
想让城市「有人晃但不追着你打」:配置 Aether.EnemyTerritoryEngage=false。
点位文件:Configs/EnemyTerritorySpots.json,改完重启服务端生效。

批量生成氛围NPC

一行一个 NPC:AgentConfig ID,后面可选跟一个 POI 动作 ID(可自行拖动调整高度)。
NPC 摆在你面前的玩家相对网格里。

                    
说明

没出现就查服务端日志:要有 [AETHER] donor init sent 和 [NPC] STATIC_PEDESTRIAN_SPAWN_SENT 两条才算通。

公共交通

1 张卡
/*metro-card*/

杂项功能

4 张卡

玩家状态(每2秒刷新)

连接中…
字段说明

位置 / 朝向 · 单位 / 模板 / 就绪 / 变换 / 待传送 · 副本 / 实例 / 宇宙 · 载具 / 座位 · 会话 / 最后在线。
由轮询 /api/status 得到,未连接时显示「无玩家会话(请先登录游戏客户端)」。

客户端 Lua(GATE 下发)

把整段 Lua 原样发到客户端进程里执行。结果只看客户端 Player.log(grep 要 -a)里的 print_debug / print_error。
⛑ 街头篮球的预设已从本下拉移除 —— 那两个按钮在「职业和独立玩法 → 街头篮球」卡里。
—

                    
GATE 通不通的唯一硬判据
⛑ 只对服务端账本里的 NPC 有效(GM 召唤 / 地盘敌人 / 功能 NPC)。 街上随机路人是客户端本地实体,服务端拿不到它们的 uid,挂不了。

无敌模式

两件事:① 解除控制状态 —— 被打成不能动 / 不能出招时解开; ② 锁定生命值 —— 照常掉血但最低保留 1 点,不会倒地不会死(默认开)。

                    
说明

根因:敌人施法是我们伪造的,命中与硬直由客户端本地结算(我们从不下发伤害),官服那一步由服务端在结算里解除,我们没人解除 → 永久卡住。
「精选」发 24 个明确的控制/施法锁状态;若无效再用「暴力清空」。
自动解除默认开启:客户端没有「玩家被击中」的上报包,只能从发过施法指令这一刻起算。



                    
说明

敌人每次施法命中由服务端结算并记账([PLAYER-HIT]),扣的就是服务端权威血量, 每次挨打 -100(满血 1000,10 下倒地)。
「锁定生命值」只控制能不能归零,不控制掉不掉血 —— 这是有意的:锁定状态下你依然能看到战斗反馈和血条压力。
取消勾选后可归零 → 倒地 → 服务端计时 8 秒后自动回满并清除倒地状态(不走官服送医流程)。
⛑ 只锁生死,不锁硬直 / 击退 —— 该被打断照样被打断,想解开用上面的解除按钮。

自动驾驶巡航速度

调的是服务端下发给 Cruise 任务的 MaxSpeed(车自己开时的目标速度),不影响玩家手动驾驶。

                    
说明

值写进 config/autonomous-driving.json(不动 private-server.json,删掉这个文件就回到主配置的 cruiseSpeed)。
正在自动驾驶时保存 = 热应用:先 Abort(4) 掉旧巡航任务,再用新速度重建,路线不变;没在开车则下次开启生效。
已保存 但写着「下次开启生效」是正常的 —— 那只说明当前没有在自驾的车。
⛑ 若车速被道路限速压住,是 DrivingFlags 里的 DFAdjustCruiseSpeedBasedOnRoadSpeed(16384) 在起作用,把它从 gameplay.vehicleNavigation.cruiseDrivingFlags 里去掉(改成 187)即可。

调试信息

1 张卡

控制台


                    
说明

列出客户端发来、服务端没有对应 handler 的方法(logs/unknown-methods.log 的尾部)。 ⛑ 日志里 0 次只能证明「客户端没发」,不等于接住了。

Server Online Better Control  ·  A Greater Ananta
"""; // 顶栏品牌位 / 侧栏底部 / 站点标签图标(favicon)共用同一张 96x96 PNG: // 模板里写作 /*brand-icon*/ 占位,组装 PageHtml 时再 Replace 进来(见文件末尾)。 private const string BrandIconB64 = "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"; /// /// The page actually served: the template above with the metro card spliced in. Keeping the fragments /// in instead of pasted into the template means the donor panel and this /// one can still be diffed line by line later. /// private static readonly string PageHtml = PageHtmlTemplate .Replace("/*metro-css*/", MetroCardParts.Css) .Replace("/*metro-card*/", MetroCardParts.Card) .Replace("/*metro-script*/", MetroCardParts.Script) .Replace("/*view-mode-css*/", AdminViewModeParts.Css) .Replace("", AdminViewModeParts.Card) .Replace("/*view-mode-script*/", AdminViewModeParts.Script) .Replace("/*brand-icon*/", BrandIconB64); }