83 lines
4.2 KiB
C#
83 lines
4.2 KiB
C#
using System.Buffers.Binary;
|
||
using Ananta.SDK.Rpc;
|
||
using Ananta.SDK.Serialization;
|
||
using Ananta.Server.Handlers;
|
||
using Ananta.Server.Protocol.Client4229938;
|
||
|
||
namespace Ananta.Server.Handlers.Game;
|
||
|
||
/// <summary>
|
||
/// 牛奶车(gadget 版)交互的服务端那一半。
|
||
///
|
||
/// 背景(2026-10-01 实机):用 <c>debugPlace</c> 探针把 <c>T_MilkCar</c>(pathId 331844513) /
|
||
/// <c>T_Milk_Drive_mcTemp</c>(530867541) 摆到玩家面前 6 m 后,客户端给出四个交互点:
|
||
/// 车侧面=调查日报 / 联系联络员 / 道具制作,车尾=回血。
|
||
///
|
||
/// 抓包结论:
|
||
/// - 车尾「回血」客户端会发 <c>AskDrinkMilk</c>(67579042, 0x04072CA2),body 8 字节
|
||
/// = gadget uniqueId(实测 <c>0x5000000000000001</c>,与 debugPlace 的 Seq 逐位吻合)。
|
||
/// - 侧面三个里「调查日报 / 道具制作」客户端<b>一个 RPC 都不发</b> ⇒ 纯客户端本地演出,
|
||
/// 服务端无从介入(调查日报点下去还会卡在读取演出界面,属客户端 bug,重连即可解除)。
|
||
/// - 「联系联络员」走的是通用对话线 <c>AskStartDialog</c>(63754798),已在
|
||
/// <c>GameRouter.Dialog.cs</c> 里回 0u,不需要在这里重复。
|
||
///
|
||
/// 所以本文件只做一件事:接住 AskDrinkMilk 并把血回满。
|
||
///
|
||
/// 回法沿用 <c>GameAdminBridge.RestorePlayerHealthAsync</c> 的既定顺序(硬规则 52):
|
||
/// <b>先属性表(SyncUnitAttrs,带 MaxHp 分母)再绝对值(SyncUnitHp)</b>;同时把
|
||
/// <c>state.PlayerHp</c> 这本账一起改掉,否则下一次受击会从旧值结算、立刻又把人打死。
|
||
///
|
||
/// ⛑ 回滚:把 <c>GameRouter.cs</c> 白名单里的 <c>MethodId.AskDrinkMilk</c> 那一行删掉即可
|
||
/// (handler 不注册 ⇒ 回落到 typed-default 兜底,点回血只剩演出、不回血)。
|
||
/// </summary>
|
||
internal sealed partial class GameRouter
|
||
{
|
||
/// <summary>
|
||
/// 官方解锁链:徽章 <c>19000150「牛奶车解锁回血」→ Buffs:[52606182]</c>,
|
||
/// 52606182 = BuffConfig「解锁牛奶车回血功能」,Duration -1(永久)。
|
||
/// 目前<b>只记录不下发</b>:客户端并没有因为缺这个 buff 就拦住 AskDrinkMilk(实机照发),
|
||
/// 所以这里不发也不会影响可用性;将来要还原「先解锁才能回血」的门槛时,
|
||
/// 就在这里读 <c>state</c> 判定并走失败分支。
|
||
/// </summary>
|
||
private const uint MilkCarUnlockBuffId = 52606182u;
|
||
|
||
/// <summary>
|
||
/// `IGameSceneToClient.AskDrinkMilk(ulong instanceId)` — 67579042 (0x04072CA2, dump.cs:1730509)。
|
||
/// 返回 `UXRPCTask`(void)⇒ 回空 OK。请求体实测 8 字节 = 牛奶车 gadget 的 uniqueId。
|
||
/// ⛑⛑ 必须<b>先回包再推血</b>:客户端 await 的是这个 void,先推血会让回包晚一拍。
|
||
/// </summary>
|
||
[Handler(MethodId.AskDrinkMilk, HandlerPacketKind.Invoke)]
|
||
private async Task AskDrinkMilk(Connection conn, UxRpcMessage msg)
|
||
{
|
||
var body = msg.Body.ToArray();
|
||
ulong instanceId = body.Length >= 8 ? BinaryPrimitives.ReadUInt64LittleEndian(body) : 0UL;
|
||
|
||
conn.Log.Info($"[MILKCAR] drink uid=0x{instanceId:X} bytes={body.Length} hex={Convert.ToHexString(body)}");
|
||
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
|
||
var state = GetStateIfExists(conn.Session);
|
||
if (state is null)
|
||
{
|
||
conn.Log.Warn($"[MILKCAR] heal-skipped reason=no-world-state uid=0x{instanceId:X}");
|
||
return;
|
||
}
|
||
|
||
ulong unit;
|
||
lock (state.SyncRoot)
|
||
{
|
||
state.PlayerHp = CombatCodec.MaxHp;
|
||
state.PlayerDowned = false;
|
||
unit = state.ActiveSpiritUnitId != 0 ? state.ActiveSpiritUnitId : Profile.InitialUnitId;
|
||
}
|
||
|
||
// 硬规则 52:属性表(MaxHp 分母)在前,绝对值在后。
|
||
await conn.Session.NotifyAsync(MethodId.SyncUnitAttrs,
|
||
UxSerializer.Serialize(CombatCodec.UnitAttrs(unit, CombatCodec.MaxHp)), CancellationToken.None);
|
||
await conn.Session.NotifyAsync(MethodId.SyncUnitHp,
|
||
UxSerializer.Serialize(CombatCodec.UnitHp(unit, CombatCodec.MaxHp)), CancellationToken.None);
|
||
|
||
conn.Log.Info($"[MILKCAR] heal unit={unit} hp={CombatCodec.MaxHp:F0} from=uid0x{instanceId:X}");
|
||
}
|
||
}
|