using Ananta.SDK.Network; using Ananta.SDK.Rpc; using Ananta.Server.Protocol.Client4229938; using GameMethods = Ananta.Server.RpcTypes.Client4229938.Methods.Game; namespace Ananta.Server.Handlers.Game; /// /// EonBug / hacker phone app — the interactive half of the BBS. /// /// The data half lives in GameRouter.Profiles.cs (SyncHackerJobInfo 64189208: handle, post /// list, rank, daily counters). This file is what happens when the player actually types in the /// app, and the whole reason it exists is one symptom: open 千年虫 (EonBug), type a name, and the /// UI freezes in the dialog with no way out. /// /// Root cause (matches the reference branch's own logs): the client keeps asking /// AskChangeHackerName(63975191) and never gets an answer, so its local "this account has no handle /// yet" state never clears and the dialog re-arms every couple of seconds. Our tree used to have no /// handler registered for it at all, which is the same failure with extra steps — the request /// landed, nothing answered it, and the client stayed stuck. /// internal sealed partial class GameRouter { /// Per-session EonBug handle. Survives character switches within one login. const string HackerHandleKey = "hacker.handle"; /// The handle used when the player never picked one. const string DefaultHackerHandle = "Ananta"; [Handler(MethodId.AskChangeHackerName, HandlerPacketKind.Invoke)] private async Task AskChangeHackerName(Connection conn, UxRpcMessage msg) { var name = string.Empty; try { name = msg.GetArgs()?.name ?? string.Empty; } catch (Exception ex) { // Still answer: an unanswered INVOKE leaves the awaited dialog armed forever, which is // the exact symptom this handler exists to remove. A parse failure only costs us the // new handle, so fall back to whatever we already published. msg.Context.Session.Log.Warn($"[HACK-JOB] AskChangeHackerName 解析失败(沿用旧代号): {ex.Message}"); } name = name.Trim(); if (name.Length > 32) name = name[..32]; // ⛑️ Always ack, even for an empty name — see the catch above. Without the empty OK the // client's await never lands and the dialog stays up. await conn.ReturnEmptyOkAsync(msg); if (name.Length == 0) { msg.Context.Session.Log.Warn("[HACK-JOB] 收到空的黑客代号(忽略)"); return; } msg.Context.Session.Items[HackerHandleKey] = name; msg.Context.Session.Log.Info($"[HACK-JOB] 黑客代号改为 '{name}' — 立即回推 SyncHackerJobInfo"); await RepublishHackerHandleAsync(msg.Context); } /// /// Re-push the BBS backbone with whatever handle is stored. Without this the change only exists /// server-side: nothing tells the client, so HackerSpiritInfo[cardId] stays empty and the /// app reopens as a new user — back to the same dialog. /// async Task RepublishHackerHandleAsync(RpcContext ctx) { var templateId = GetWorldState(ctx).ActiveSpiritTemplateId; await PublishHackerJobRuntime4229938(ctx, templateId, "hacker-rename", ctx.Session.Items.TryGetValue(HackerHandleKey, out var stored) ? stored as string : null); } /// The EonBug handle to publish: the one the player picked, else the default. static string HackerHandle4229938(TcpSession session) => session.Items.TryGetValue(HackerHandleKey, out var stored) && stored is string s && s.Length > 0 ? s : DefaultHackerHandle; /// /// The two remaining EonBug forum actions: read a new post (63150736) and accept a job from one /// (63036377). Both return void per DefaultReturnCatalog4229938, so an empty OK is the /// whole contract. /// /// ⛑ The point of routing them is not content — the forum's post list already rides /// SyncHackerJobInfo (64189208) from GameRouter.Profiles.cs. Both are INVOKEs, so with no /// handler the client awaits a reply that never comes: tapping a post or 接差事 leaves the UI /// armed forever, the same failure mode AskChangeHackerName used to have. /// /// Nothing is persisted: the client commits the read/accept locally, and there is no server-side /// job simulation to advance. /// [Handler(MethodId.AskReadHackerNewPost, HandlerPacketKind.Invoke)] [Handler(MethodId.AskAcceptHackerPostTask, HandlerPacketKind.Invoke)] private async Task EonBugForumAction(Connection conn, UxRpcMessage msg) { var action = msg.MethodId == MethodId.AskAcceptHackerPostTask ? "accept-post-task" : "read-new-post"; conn.Log.Info($"[HACK-JOB] {action} bytes={msg.Body.Length}"); await conn.ReturnEmptyOkAsync(msg); } }