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.AskInteractCmd, HandlerPacketKind.Notify)] private async Task AskInteractCmd(Connection conn, UxRpcMessage msg) { var data = msg.GetArgs(); conn.Log.Info($"[GADGET] interact cmd type={data.CmdType} receiver={data.Receiver} sender={data.Sender}"); await conn.Session.NotifyAsync(MethodId.SyncInteractCmd, UxSerializer.Serialize( new SceneMethods.SyncInteractCmd4229938 { Data = data }), CancellationToken.None); } [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); } [Handler(MethodId.AskSceneDeviceChangeState, HandlerPacketKind.Invoke)] private async Task AskSceneDeviceChangeState(Connection conn, UxRpcMessage msg) { var args = msg.GetArgs(); conn.Log.Info($"[GADGET] change state type={args.Type} id={args.Info.Id} stateType={args.Info.StateType} state={args.Info.State} check={args.Info.StateCheckIndex}"); // Ack the requested check index so the gadget graph's condition passes, then mirror // the new state back — the client applies it on the Sync, not on the invoke return. await conn.ReturnAsync(msg, new SceneMethods.SceneDeviceCheckIndexCallback4229938 { CheckIndex = args.Info.StateCheckIndex, Error = 0, }); await conn.Session.NotifyAsync(MethodId.SyncSceneDeviceStateChange, UxSerializer.Serialize( new SceneMethods.SyncSceneDeviceStateChange4229938 { Type = args.Type, Id = args.Info.Id, StateType = args.Info.StateType, State = args.Info.State, StateCheckIndex = args.Info.StateCheckIndex, }), CancellationToken.None); } [Handler(MethodId.AskSceneDeviceRecordValue, HandlerPacketKind.Invoke)] private async Task AskSceneDeviceRecordValue(Connection conn, UxRpcMessage msg) { var args = msg.GetArgs(); conn.Log.Info($"[GADGET] record value type={args.Type} id={args.Info.Id} name={args.Info.ValueName} value='{args.Info.Value}' check={args.Info.ValueCheckIndex}"); await conn.ReturnAsync(msg, new SceneMethods.SceneDeviceCheckIndexCallback4229938 { CheckIndex = args.Info.ValueCheckIndex, Error = 0, }); // Mirror the recorded value so the device graph sees the new value on Sync. await conn.Session.NotifyAsync(MethodId.SyncSceneDeviceValueChange, UxSerializer.Serialize( new SceneMethods.SyncSceneDeviceValueChange4229938 { Type = args.Type, Id = args.Info.Id, ValueName = args.Info.ValueName, Value = args.Info.Value, ValueCheckIndex = args.Info.ValueCheckIndex, }), CancellationToken.None); await conn.Session.NotifyAsync(MethodId.SyncSceneDeviceValueChange, UxSerializer.Serialize( new SceneMethods.SyncSceneDeviceValueChange4229938 { Type = args.Type, Id = args.Info.Id, ValueName = args.Info.ValueName, Value = args.Info.Value, ValueCheckIndex = args.Info.ValueCheckIndex, }), CancellationToken.None); } [Handler(MethodId.AskSceneDeviceSendSignal, HandlerPacketKind.Invoke)] private async Task AskSceneDeviceSendSignal(Connection conn, UxRpcMessage msg) { var args = msg.GetArgs(); conn.Log.Info($"[GADGET] send signal type={args.Type} id={args.Id} signal='{args.SignalName}'"); await conn.ReturnEmptyOkAsync(msg); await conn.Session.NotifyAsync(MethodId.SyncSceneDeviceSignalSend, UxSerializer.Serialize( new SceneMethods.SyncSceneDeviceSignalSend4229938 { Type = args.Type, Id = args.Id, SignalName = args.SignalName, }), CancellationToken.None); } // ------------------------------------------------------------------ // 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); } }