using Ananta.SDK.Network; using Ananta.SDK.Rpc; using Ananta.SDK.Serialization; using Ananta.Server.Protocol.Client4229938; using Ananta.Server.RpcTypes.Client4229938; using SceneMethods = Ananta.Server.RpcTypes.Client4229938.Methods.GameScene; namespace Ananta.Server.Handlers.Game; /// /// Gadget / SceneDevice interaction handlers (build 4229938). /// /// Grid-streamed gadgets (see GadgetStreamingEngine) render from the client's baked data, /// but actually USING them is a server round-trip: pressing F runs the gadget's local graph /// which fires AskInteractCmd and AskSceneDevice* asks. This router answers each ask the way /// a single-player authoritative server would — ack the check-index, then echo the change /// back through the matching Sync* notify so the gadget graph proceeds (vending machine UI, /// claw machine, doors, arcades...). /// internal sealed partial class GameRouter { [Handler(MethodId.AskInteractCmds, HandlerPacketKind.Notify)] private async Task AskInteractCmds(Connection conn, UxRpcMessage msg) { var data = msg.GetArgs(); conn.Log.Info($"[GADGET] interact cmds x{data.Data.Count}"); await conn.Session.NotifyAsync(MethodId.IGameSceneToClient_SyncInteractCmds, UxSerializer.Serialize( new SceneMethods.SyncInteractCmds4229938 { Data = data.Data }), CancellationToken.None); } // Handled in GameRouter.SceneDevices.cs (with loop protection and config toggles) // ------------------------------------------------------------------ // NPC dialog (F on an NPC): the client resolves the dialog id from its own // config and asks us to run it; we echo the dialog back so the UI opens. // ------------------------------------------------------------------ [Handler(MethodId.AskAgentStartNpcDialog, HandlerPacketKind.Notify)] private async Task AskAgentStartNpcDialog(Connection conn, UxRpcMessage msg) { var args = msg.GetArgs(); conn.Log.Info($"[NPC] start dialog agent={args.AgentEntityId} dialog={args.NpcDialogId} duration={args.DialogDuration}"); await conn.Session.NotifyAsync(MethodId.SyncAgentPrepareStartNpcDialog, UxSerializer.Serialize( new SceneMethods.SyncAgentPrepareStartNpcDialog4229938 { AgentId = args.AgentEntityId, NpcDialogId = args.NpcDialogId, NpcSoundId = 0, VirtualNpc = false, EntireDialogId = 0, }), CancellationToken.None); } [Handler(MethodId.AskAgentFinishNpcDialog, HandlerPacketKind.Notify)] private async Task AskAgentFinishNpcDialog(Connection conn, UxRpcMessage msg) { var args = msg.GetArgs(); conn.Log.Info($"[NPC] finish dialog agent={args.AgentEntityId} dialog={args.NpcDialogId}"); await conn.Session.NotifyAsync(MethodId.SyncAgentFinishNpcDialog, UxSerializer.Serialize( new SceneMethods.SyncAgentFinishNpcDialog4229938 { AgentId = args.AgentEntityId, }), CancellationToken.None); } // ------------------------------------------------------------------ // Gadget doors / scene rooms: interior transitions and door teleports. // The client drives the movement itself; the server only acknowledges so // the gadget graph doesn't stall on the ask. // ------------------------------------------------------------------ // ------------------------------------------------------------------ // Gadget doors / scene rooms: the client asks on door enter/leave and the // geometry of the interior only loads once the server answers with // SyncSpoonRoomState — without it the player falls through the world. // ------------------------------------------------------------------ [Handler(MethodId.AskGadgetSceneRoomTrigger, HandlerPacketKind.Invoke)] private async Task AskGadgetSceneRoomTrigger(Connection conn, UxRpcMessage msg) { var args = msg.GetArgs(); conn.Log.Info($"[ROOM] scene room trigger room={args.SceneRoomId} gadget={args.GadgetUniqueId} enter={args.IsEnter}"); await conn.ReturnEmptyOkAsync(msg); await PushRoomStateAsync(conn.Session, args.SceneRoomId, args.IsEnter); } [Handler(MethodId.AskEnterSceneRoom, HandlerPacketKind.Invoke)] private async Task AskEnterSceneRoom(Connection conn, UxRpcMessage msg) { var args = msg.GetArgs(); conn.Log.Info($"[ROOM] enter room={args.SceneRoomId} vehicle={args.IsVehicle}"); await conn.ReturnEmptyOkAsync(msg); await PushRoomStateAsync(conn.Session, args.SceneRoomId, enabled: true); } [Handler(MethodId.AskExistSceneRoom, HandlerPacketKind.Invoke)] private async Task AskExistSceneRoom(Connection conn, UxRpcMessage msg) { var args = msg.GetArgs(); conn.Log.Info($"[ROOM] leave room={args.SceneRoomId} vehicle={args.IsVehicle}"); await conn.ReturnEmptyOkAsync(msg); await PushRoomStateAsync(conn.Session, args.SceneRoomId, enabled: false); } [Handler(MethodId.AskAgentSceneRoomTrigger, HandlerPacketKind.Invoke)] private async Task AskAgentSceneRoomTrigger(Connection conn, UxRpcMessage msg) { var args = msg.GetArgs(); conn.Log.Info($"[ROOM] agent room trigger room={args.SceneRoomId} agent={args.SpoonAgentId} enter={args.IsEnter}"); await conn.ReturnEmptyOkAsync(msg); await PushRoomStateAsync(conn.Session, args.SceneRoomId, args.IsEnter); } private static async Task PushRoomStateAsync(TcpSession session, int sceneRoomId, bool enabled) { await session.NotifyAsync(MethodId.SyncSpoonRoomState, UxSerializer.Serialize( new SceneMethods.SyncSpoonRoomState4229938 { SceneRoomId = sceneRoomId, Enabled = enabled, TaskId = 0, }), CancellationToken.None); } // ------------------------------------------------------------------ // NPC dating ("เดต") — the InviteRide family. The client's GM console can // request a date with an NPC (GmStartInviteRideNpc); the server activates // that NPC as the ride companion and pushes its interaction menu property. // ------------------------------------------------------------------ [Handler(MethodId.GmStartInviteRideNpc, HandlerPacketKind.Invoke)] private async Task GmStartInviteRideNpc(Connection conn, UxRpcMessage msg) { var args = msg.GetArgs(); conn.Log.Info($"[DATE] start invite-ride npc={args.NpcId}"); await conn.ReturnEmptyOkAsync(msg); // Activate the companion... await conn.Session.NotifyAsync(MethodId.SyncInviteRideNpcInfo, UxSerializer.Serialize( new SceneMethods.SyncInviteRideNpcInfo4229938 { InviteRideNpcInstanceId = args.NpcId, IsInviteRideNpcActive = true, }), CancellationToken.None); // ...and give it the dating interaction menu (Type=21; menu entries render // from the client's own configs — empty list keeps the default menu). await conn.Session.NotifyAsync(MethodId.IGameSceneToClient_SyncAgentPropertyChange, UxSerializer.Serialize( new SceneMethods.SyncAgentPropertyInviteRideChange4229938 { AgentId = args.NpcId, Type = 21, }), CancellationToken.None); } /// Gift giving to an NPC — accepted, inventory is sandbox-unlimited. [Handler(MethodId.AskInteractNpcWithGift, HandlerPacketKind.Invoke)] private Task AskInteractNpcWithGift(Connection conn, UxRpcMessage msg) { var args = msg.GetArgs(); conn.Log.Info($"[NPC] gift activity={args.ActivityCfgId} item={args.ItemId} x{args.Count}"); return conn.ReturnEmptyOkAsync(msg); } }