feat: Upgrade server with HyNB v2.4 modular architecture, minigames, admin GM web panel, companion summons, and car radio
This commit is contained in:
1 parent
74e2963a12
commit
3e5adf1dc5
1571 files changed
+336468
-6348
No files matched your search
@@ -1,5 +1,5 @@
|
||||
using Ananta.SDK.Network;
|
||||
using Ananta.SDK.Rpc;
|
||||
using Ananta.Server.Configuration;
|
||||
using Ananta.Server.Protocol.Client4229938;
|
||||
using Ananta.Server.RpcTypes.Client4229938;
|
||||
using SceneMethods = Ananta.Server.RpcTypes.Client4229938.Methods.GameScene;
|
||||
@@ -7,9 +7,9 @@ using SceneMethods = Ananta.Server.RpcTypes.Client4229938.Methods.GameScene;
|
||||
namespace Ananta.Server.Handlers.Game;
|
||||
|
||||
/// <summary>
|
||||
/// Private-server GM surface (build 4229938): the in-game GM console (ALT+F1) sends these
|
||||
/// Private-server GM surface (build 4229938): the in-game GM console sends these
|
||||
/// C2S invokes. Strategy is return-real-ids-first: the client spawns GM entities locally
|
||||
/// (GM invoke call sites ignore the return), the server records the ids so the debug
|
||||
/// (GM invoke call sites ignore the return), the server records the ids so the admin
|
||||
/// panel can manage them. If a command proves to wait for a server broadcast, the push
|
||||
/// (SyncLogicVehicleEnter etc.) gets added for that command.
|
||||
/// </summary>
|
||||
@@ -19,105 +19,141 @@ internal sealed partial class GameRouter
|
||||
private static long _gmEnemySeq = 330000000000L;
|
||||
|
||||
[Handler(MethodId.GmSpawnVehicle, HandlerPacketKind.Invoke)]
|
||||
private Task GmSpawnVehicle(Connection conn, UxRpcMessage msg)
|
||||
private async Task GmSpawnVehicle(Connection conn, UxRpcMessage msg)
|
||||
{
|
||||
var args = msg.GetArgs<SceneMethods.GmSpawnVehicle>();
|
||||
var entityId = (ulong)Interlocked.Increment(ref _gmEntitySeq);
|
||||
RegisterSummoned(entityId, args.TemplateId, args.Position.X, args.Position.Y, args.Position.Z, args.Facing);
|
||||
conn.Log.Info($"[GM] spawn vehicle cfg={args.TemplateId} suit={args.SuitId} own={args.LoadOwnVehicle} spoon='{args.SpoonName}' at=({args.Position.X:F1},{args.Position.Y:F1},{args.Position.Z:F1}) -> entity={entityId}");
|
||||
return conn.ReturnAsync(msg, entityId);
|
||||
|
||||
// Use the 9-version direct spawn path so the vehicle actually materializes
|
||||
// beside the player and is drivable. Falls back to a plain id return if the
|
||||
// spawn path rejects the config (unknown / model-less vehicles).
|
||||
var nearPos = new Vec3(args.Position.X, args.Position.Y, args.Position.Z);
|
||||
var result = await SpawnDirectAsync(conn.Session, args.TemplateId, nearPos, args.Facing, "gm-spawn");
|
||||
if (result.Ok)
|
||||
{
|
||||
entityId = result.EntityId;
|
||||
conn.Log.Info($"[GM] spawn vehicle cfg={args.TemplateId} suit={args.SuitId} own={args.LoadOwnVehicle} spoon='{args.SpoonName}' at=({args.Position.X:F1},{args.Position.Y:F1},{args.Position.Z:F1}) -> entity={entityId}");
|
||||
}
|
||||
else
|
||||
{
|
||||
conn.Log.Warn($"[GM] spawn vehicle cfg={args.TemplateId} rejected ({result.Message}); returning placeholder entity={entityId}");
|
||||
}
|
||||
await conn.ReturnAsync(msg, entityId);
|
||||
}
|
||||
|
||||
[Handler(MethodId.GmAddEnemyWithPosition, HandlerPacketKind.Invoke)]
|
||||
private Task GmAddEnemyWithPosition(Connection conn, UxRpcMessage msg)
|
||||
private async Task GmAddEnemyWithPosition(Connection conn, UxRpcMessage msg)
|
||||
{
|
||||
var args = msg.GetArgs<SceneMethods.GmAddEnemyWithPosition>();
|
||||
var entityId = (ulong)Interlocked.Increment(ref _gmEnemySeq);
|
||||
conn.Log.Info($"[GM] add enemy id={args.EnemyId} camp={args.Camp} nav={args.NavTagType} at=({args.Position.X:F1},{args.Position.Y:F1},{args.Position.Z:F1}) -> entity={entityId}");
|
||||
return conn.ReturnAsync(msg, entityId);
|
||||
var at = new Vec3(args.Position.X, args.Position.Y, args.Position.Z);
|
||||
var entityId = await SpawnGmEnemyAsync(conn, args.EnemyId, at, nameof(GmAddEnemyWithPosition));
|
||||
await conn.ReturnAsync(msg, entityId);
|
||||
}
|
||||
|
||||
[Handler(MethodId.GmAddEnemy, HandlerPacketKind.Invoke)]
|
||||
private Task GmAddEnemy(Connection conn, UxRpcMessage msg)
|
||||
private async Task GmAddEnemy(Connection conn, UxRpcMessage msg)
|
||||
{
|
||||
var args = msg.GetArgs<SceneMethods.GmAddEnemy>();
|
||||
var entityId = (ulong)Interlocked.Increment(ref _gmEnemySeq);
|
||||
conn.Log.Info($"[GM] add enemy id={args.EnemyId} camp={args.Camp} tree='{args.TreeName}' -> entity={entityId}");
|
||||
return conn.ReturnAsync(msg, entityId);
|
||||
var entityId = await SpawnGmEnemyAsync(conn, args.EnemyId, null, nameof(GmAddEnemy));
|
||||
await conn.ReturnAsync(msg, entityId);
|
||||
}
|
||||
|
||||
[Handler(MethodId.GmAddEnemyByPlayer, HandlerPacketKind.Invoke)]
|
||||
private Task GmAddEnemyByPlayer(Connection conn, UxRpcMessage msg)
|
||||
private async Task GmAddEnemyByPlayer(Connection conn, UxRpcMessage msg)
|
||||
{
|
||||
var args = msg.GetArgs<SceneMethods.GmAddEnemyByPlayer>();
|
||||
var entityId = (ulong)Interlocked.Increment(ref _gmEnemySeq);
|
||||
conn.Log.Info($"[GM] add enemy by player id={args.EnemyId} camp={args.Camp} -> entity={entityId}");
|
||||
return conn.ReturnAsync(msg, entityId);
|
||||
var entityId = await SpawnGmEnemyAsync(conn, args.EnemyId, null, nameof(GmAddEnemyByPlayer));
|
||||
await conn.ReturnAsync(msg, entityId);
|
||||
}
|
||||
|
||||
[Handler(MethodId.GmAddNpc, HandlerPacketKind.Invoke)]
|
||||
private async Task GmAddNpc(Connection conn, UxRpcMessage msg)
|
||||
{
|
||||
var args = msg.GetArgs<SceneMethods.GmAddNpc>();
|
||||
var entityId = await SpawnGmNpcAsync(conn, args.Npcid, nameof(GmAddNpc));
|
||||
await conn.ReturnAsync(msg, entityId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Materialises a civilian / friendly NPC for a GM add command.
|
||||
///
|
||||
/// GmAddNpc carries only two uint32s, so unlike the enemy commands it survives the client
|
||||
/// console bridge intact and is currently the only usable C2S GM spawn command typed in
|
||||
/// ALT+F1 (e.g. "GmAddNpc 4065053 0").
|
||||
/// </summary>
|
||||
private async Task<ulong> SpawnGmNpcAsync(Connection conn, uint npcId, string tag)
|
||||
{
|
||||
if (npcId == 0)
|
||||
{
|
||||
conn.Log.Warn($"[GM] {tag} got npcId=0 — nothing spawned");
|
||||
return (ulong)Interlocked.Increment(ref _gmEnemySeq);
|
||||
}
|
||||
|
||||
var state = GetStateIfExists(conn.Session);
|
||||
var hasTransform = state is not null && state.HasLastReportedPlayerTransform;
|
||||
var basePos = hasTransform ? state!.LastReportedPlayerPosition : new Vec3(0, 0, 0);
|
||||
var facing = hasTransform ? state!.LastReportedPlayerRotation.Y : 0f;
|
||||
|
||||
// Same placement rule as SpawnGmEnemyAsync: a few units in front of the player.
|
||||
// Y must stay at the player's own height: the client takes the server-supplied
|
||||
// position verbatim (no ground snapping), so any +1 offset leaves the unit hovering.
|
||||
// ⛑ facing 是度数,必须先转弧度(2026-09-26 修,详见 SpawnAgentAsync 里的同一处注释)。
|
||||
var yaw = facing * (MathF.PI / 180f);
|
||||
var x = basePos.X + (float)Math.Sin(yaw) * 3.5f;
|
||||
var z = basePos.Z + (float)Math.Cos(yaw) * 3.5f;
|
||||
var y = basePos.Y;
|
||||
|
||||
var result = await GameAdminBridge.SpawnAgentAsync(npcId, x, y, z, facing, hostile: false);
|
||||
conn.Log.Info($"[GM] {tag} npc={npcId} ok={result.Ok} uid={result.Uid} at=({x:F1},{y:F1},{z:F1}) {result.Message}");
|
||||
return result.Ok && result.Uid != 0
|
||||
? result.Uid
|
||||
: (ulong)Interlocked.Increment(ref _gmEnemySeq);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Materialises an enemy for a GM add command.
|
||||
///
|
||||
/// The client forwards AddEnemy* to the server as a C2S invoke, so nothing appears unless
|
||||
/// the server actually spawns. It also mangles the id on the way back (the panel asks for
|
||||
/// 40100010 and the C2S arrives with 40900579), so the id the operator typed is preferred
|
||||
/// over the forwarded one whenever it is still fresh.
|
||||
/// </summary>
|
||||
private async Task<ulong> SpawnGmEnemyAsync(Connection conn, uint forwardedId, Vec3? explicitPos, string tag)
|
||||
{
|
||||
var state = GetStateIfExists(conn.Session);
|
||||
var hasTransform = state is not null && state.HasLastReportedPlayerTransform;
|
||||
var basePos = explicitPos ?? (hasTransform ? state!.LastReportedPlayerPosition : new Vec3(0, 0, 0));
|
||||
var facing = hasTransform ? state!.LastReportedPlayerRotation.Y : 0f;
|
||||
|
||||
// The operator's intent wins over the mangled forwarded id.
|
||||
var id = GameAdminBridge.TakePendingGmEnemyId();
|
||||
if (id == 0) id = forwardedId;
|
||||
if (id == 0)
|
||||
{
|
||||
conn.Log.Warn($"[GM] {tag} has no enemy id (forwarded=0, no pending intent) — nothing spawned");
|
||||
return (ulong)Interlocked.Increment(ref _gmEnemySeq);
|
||||
}
|
||||
|
||||
// Drop it a few units in front of the player instead of inside them.
|
||||
// ⛑ facing 是度数,必须先转弧度(2026-09-26 修,详见 SpawnAgentAsync 里的同一处注释)。
|
||||
var yaw = facing * (MathF.PI / 180f);
|
||||
var x = explicitPos is null ? basePos.X + (float)Math.Sin(yaw) * 3.5f : basePos.X;
|
||||
var z = explicitPos is null ? basePos.Z + (float)Math.Cos(yaw) * 3.5f : basePos.Z;
|
||||
var y = basePos.Y;
|
||||
|
||||
var result = await GameAdminBridge.SpawnAgentAsync(id, x, y, z, facing, hostile: true);
|
||||
conn.Log.Info($"[GM] {tag} forwarded={forwardedId} effective={id} ok={result.Ok} uid={result.Uid} at=({x:F1},{y:F1},{z:F1}) {result.Message}");
|
||||
return result.Ok && result.Uid != 0
|
||||
? result.Uid
|
||||
: (ulong)Interlocked.Increment(ref _gmEnemySeq);
|
||||
}
|
||||
|
||||
[Handler(MethodId.GmTeleportXYZ, HandlerPacketKind.Invoke)]
|
||||
private async Task GmTeleportXYZ(Connection conn, UxRpcMessage msg)
|
||||
{
|
||||
var args = msg.GetArgs<SceneMethods.GmTeleportXYZ>();
|
||||
if (MathF.Abs(args.X) < 0.1f && MathF.Abs(args.Z) < 0.1f)
|
||||
{
|
||||
conn.Session.Log.Warn($"[GM] reject teleport to origin (0, 0, 0)");
|
||||
await conn.ReturnAsync(msg, 0);
|
||||
return;
|
||||
}
|
||||
var state = GetWorldState(msg.Context);
|
||||
var targetY = args.Y;
|
||||
if (targetY <= 10f)
|
||||
{
|
||||
targetY = (state != null && state.LastReportedPlayerPosition.Y > 10f)
|
||||
? state.LastReportedPlayerPosition.Y
|
||||
: 274.6f;
|
||||
}
|
||||
var targetPos = new Vec3(args.X, targetY, args.Z);
|
||||
var facing = args.Facing;
|
||||
var unitId = state?.ActiveSpiritUnitId ?? Profile.InitialUnitId;
|
||||
|
||||
if (state != null)
|
||||
{
|
||||
state.LastReportedPlayerPosition = targetPos;
|
||||
state.PendingTeleportPosition = targetPos;
|
||||
state.PendingTeleportFacing = facing;
|
||||
state.PendingTeleportId = 1;
|
||||
}
|
||||
|
||||
var sync = new SceneMethods.SyncTeleport
|
||||
{
|
||||
option = new SceneMethods.TeleportOption
|
||||
{
|
||||
teleportId = 1,
|
||||
Position = new SceneMethods.UxVector3(targetPos.X, targetPos.Y, targetPos.Z),
|
||||
Facing = facing,
|
||||
IsSwitchScene = false,
|
||||
WaitTaskResource = false,
|
||||
MapEntranceId = 0
|
||||
}
|
||||
};
|
||||
await conn.NotifyAsync(MethodId.SyncTeleport, sync);
|
||||
if (unitId != 0)
|
||||
{
|
||||
await conn.NotifyAsync(MethodId.SyncUnitPositionAndFacing,
|
||||
WorldCodec.PositionAndFacing(unitId, targetPos, facing));
|
||||
}
|
||||
|
||||
conn.Log.Info($"[GM] teleport to=({targetPos.X:F1},{targetPos.Y:F1},{targetPos.Z:F1}) facing={facing:F1}");
|
||||
conn.Log.Info($"[GM] teleport to=({args.X:F1},{args.Y:F1},{args.Z:F1}) facing={args.Facing:F1}");
|
||||
await TeleportPlayerInstantAsync(msg.Context, args.X, args.Y, args.Z, args.Facing, "gm-teleport-xyz");
|
||||
await conn.ReturnEmptyOkAsync(msg);
|
||||
}
|
||||
|
||||
[Handler(MethodId.DavinciCode, HandlerPacketKind.Notify)]
|
||||
private static void DavinciCode(Connection conn, UxRpcMessage msg)
|
||||
{
|
||||
conn.Log.Info("[SECURITY] DavinciCode notify absorbed");
|
||||
}
|
||||
|
||||
[Handler(MethodId.GmDaVinciCode, HandlerPacketKind.Invoke)]
|
||||
private static Task GmDaVinciCode(Connection conn, UxRpcMessage msg)
|
||||
{
|
||||
conn.Log.Info("[SECURITY] GmDaVinciCode invoke absorbed");
|
||||
return conn.ReturnEmptyOkAsync(msg);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user