443 lines
21 KiB
C#
443 lines
21 KiB
C#
using System.Buffers.Binary;
|
||||
|
|
using System.Collections.Generic;
|
|||
|
|
using System.Text;
|
|||
|
|
using Ananta.SDK.Rpc;
|
|||
|
|
using Ananta.Server.Protocol.Client4229938;
|
|||
|
|
using Ananta.Server.RpcTypes.Client4229938;
|
|||
|
|
|
|||
|
|
namespace Ananta.Server.Handlers.Game;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 音乐 / 演出线三条玩法的服务端闭环 —— 2026-10-04:
|
|||
|
|
/// <list>
|
|||
|
|
/// <item><b>1. Livehouse 音游</b>(节奏打击,引擎 <c>MusicGameManager.lua</c>)</item>
|
|||
|
|
/// <item><b>2. PartyDance 舞池</b>(同一套音游引擎,<c>LivehouseConfig.Use == Party</c> 那 5 首)</item>
|
|||
|
|
/// <item><b>3. PartyLive 派对直播</b>(点赞 / 礼物 / 弹幕,<c>PartyLiveStreamPanelStore.lua</c>)</item>
|
|||
|
|
/// </list>
|
|||
|
|
///
|
|||
|
|
/// <para>
|
|||
|
|
/// ★★ 接之前这三条线<b>全套落在空包兜底</b>里(<c>DefaultReturnCatalog4229938.cs</c>):
|
|||
|
|
/// <c>63885626 → [0xFF,0x01] list7-empty</c>、<c>63848409 → [0xFF,0x00] ReadBoolean</c>、
|
|||
|
|
/// 其余 <c>shape="void"</c>。症状不是报错,而是<b>静默没内容</b>:
|
|||
|
|
/// · 曲目列表恒空 ⇒ 面板上的等级打点永远是 0,重进清零;
|
|||
|
|
/// · <c>FirstPlay</c> 恒 false ⇒ 首次引导 timeline 永远不播;
|
|||
|
|
/// · 舞会三连里 <c>AskStartPartyDance</c> 的 Callback 什么都不做,客户端在等
|
|||
|
|
/// <c>SyncPartyDanceStart</c> 推送 ⇒ 不推 = 点了没反应;
|
|||
|
|
/// · 直播间 UI 自己不发 RPC,数据全靠 <c>SyncPartyResponse</c>(68456156) ⇒ 从不推 = 空直播间。
|
|||
|
|
/// </para>
|
|||
|
|
///
|
|||
|
|
/// <para>
|
|||
|
|
/// ⛑ 硬规则 239 —— 每个契约都对照客户端<b>自己的</b>反序列化器核过,不是照 dump.cs 猜:
|
|||
|
|
/// <c>Drmk.Proxy\proxy\lua\RPCDeserializeAuto.lua</c> 的 <c>Auto.Reader[571]</c> /
|
|||
|
|
/// <c>[568]</c> / <c>[428]</c> / <c>[429]</c> / <c>[1123]</c>,
|
|||
|
|
/// 参数形状对照 <c>_lua_all\lua\LuaGen\AutoGen\ClientToGameDelegate.lua</c> 的 <c>*_Serializer</c>。
|
|||
|
|
/// </para>
|
|||
|
|
///
|
|||
|
|
/// <para>⛑ 回滚:从 <c>GameRouter.cs</c> 的 <c>Enabled4229938MethodIds</c> 里删掉对应行即可
|
|||
|
|
/// (handler 不注册 = 回到中性兜底),本文件留着无害。</para>
|
|||
|
|
/// </summary>
|
|||
|
|
internal sealed partial class GameRouter
|
|||
|
|
{
|
|||
|
|
// ---------------------------------------------------------------------------------------
|
|||
|
|
// 1. Livehouse 音游
|
|||
|
|
//
|
|||
|
|
// 客户端整条链(逐行核过,非推断):
|
|||
|
|
// 进 Sonic Boom(IndoorId 5) / 万能旋钮(274) 的室内扇区 → 选曲面板
|
|||
|
|
// LivehouseSelectPanelStore.lua:291 OnPlayBtn
|
|||
|
|
// · LivehouseConfig.DialogBeforeTimelineSingle 命中 ⇒ 先播对话,DialogEnd 再进
|
|||
|
|
// · 否则直接 PlayLiveHouseV2
|
|||
|
|
// MusicGameManager.PlayLiveHouseV2(:324) → StartGame(:371)
|
|||
|
|
// → CheckShow(LIVEHOUSE_GAME_PANEL) + PlayGameMainTimeline(:564)
|
|||
|
|
// → 未显式跳过 intro 时先 AskQueryLiveHouseInfo():FirstPlay 为真才播 intro timeline
|
|||
|
|
// → LiveHouseMusicStart(musicId, inviteNpcId, difficulty) ← NOTIFY
|
|||
|
|
// → 打完 SetLiveHouseMusicResult(musicId, {P,G,M,S,percent}, inviteNpcId) ← INVOKE
|
|||
|
|
// → SetRecordMusicInfo + PlayLivehouseEndTimeline(Good / BadEnding 对话)
|
|||
|
|
// 开局前勾了「金手指」⇒ 先 AskLiveHouseUseItem(musicId) 再 StartGame。
|
|||
|
|
// ---------------------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
/// <summary>取登录后才能拿到的 <c>WorldEntryState</c>;还没建好就是 null(按新账号处理)。</summary>
|
|||
|
|
private WorldEntryState? LivehouseState(UxRpcMessage msg)
|
|||
|
|
=> msg.Context is null ? null : GetWorldState(msg.Context);
|
|||
|
|
|
|||
|
|
// AskQueryLiveHouseInfo (63848409) — Invoke ; 无参 ; return: Complex{ bool FirstPlay }
|
|||
|
|
//
|
|||
|
|
// 「要不要播首次引导」的唯一判据。第一次答 true,之后恒 false,由
|
|||
|
|
// LivehouseProgressStore 把这个 latch 落进 config/player-progress.json。
|
|||
|
|
[Handler(MethodId.AskQueryLiveHouseInfo, HandlerPacketKind.Invoke)]
|
|||
|
|
private Task AskQueryLiveHouseInfo4229938(Connection conn, UxRpcMessage msg)
|
|||
|
|
{
|
|||
|
|
var state = LivehouseState(msg);
|
|||
|
|
var firstPlay = state is not null && LivehouseProgressStore.TakeFirstPlay(state);
|
|||
|
|
|
|||
|
|
conn.Log.Info($"[LIVEHOUSE] AskQueryLiveHouseInfo -> FirstPlay={firstPlay}");
|
|||
|
|
return conn.ReturnAsync(msg, new LiveHouseQueryInfoPush { FirstPlay = firstPlay });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// AskLiveHouseMusicList (63885626) — Invoke ; 无参 ; return: List7Bit<Complex(571)>
|
|||
|
|
//
|
|||
|
|
// 客户端把整张表灌进 gMusicGameManager.RecordMusicInfo,之后 GetScoreLevel 才显示
|
|||
|
|
// 非最低档评级。空列表客户端<b>不报错</b>,只是等级打点全 0 —— 别指望有日志线索。
|
|||
|
|
[Handler(MethodId.AskLiveHouseMusicList, HandlerPacketKind.Invoke)]
|
|||
|
|
private Task AskLiveHouseMusicList4229938(Connection conn, UxRpcMessage msg)
|
|||
|
|
{
|
|||
|
|
var state = LivehouseState(msg);
|
|||
|
|
var records = state is null
|
|||
|
|
? new Dictionary<uint, LivehouseRecord4229938>()
|
|||
|
|
: LivehouseProgressStore.Snapshot(state);
|
|||
|
|
|
|||
|
|
var list = new List<MusicClientInfoPush>();
|
|||
|
|
foreach (var (musicId, record) in records)
|
|||
|
|
{
|
|||
|
|
if (musicId == 0 || record is null)
|
|||
|
|
continue;
|
|||
|
|
|
|||
|
|
// 顺序必须严格是 Perfect / Great / Miss / Special(见契约文件的类注释)。
|
|||
|
|
list.Add(new MusicClientInfoPush
|
|||
|
|
{
|
|||
|
|
MusicId = musicId,
|
|||
|
|
RecordInfo = new List<uint>
|
|||
|
|
{
|
|||
|
|
record.Perfect,
|
|||
|
|
record.Great,
|
|||
|
|
record.Miss,
|
|||
|
|
record.Special,
|
|||
|
|
},
|
|||
|
|
AlreadyReward = record.AlreadyReward,
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
conn.Log.Info($"[LIVEHOUSE] AskLiveHouseMusicList -> {list.Count} 首有成绩");
|
|||
|
|
return conn.ReturnAsync(msg, list);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SetLiveHouseMusicResult (63723141) — Invoke ; args: u32 musicId, list7bit<u32> counts, u32 npcId
|
|||
|
|
//
|
|||
|
|
// 唯一的成绩写入路径。客户端实际给 5 项 {Perfect, Great, Miss, Special, floor(percent)}
|
|||
|
|
// (MusicGameManager.lua:264-270),我们只需要前 4 项。
|
|||
|
|
[Handler(MethodId.SetLiveHouseMusicResult, HandlerPacketKind.Invoke)]
|
|||
|
|
private Task SetLiveHouseMusicResult4229938(Connection conn, UxRpcMessage msg)
|
|||
|
|
{
|
|||
|
|
var body = msg.Body;
|
|||
|
|
uint musicId = 0;
|
|||
|
|
uint npcId = 0;
|
|||
|
|
var counts = new List<uint>();
|
|||
|
|
|
|||
|
|
var pos = 0;
|
|||
|
|
if (body.Length >= 4)
|
|||
|
|
{
|
|||
|
|
musicId = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(pos, 4));
|
|||
|
|
pos += 4;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ReadList7Bit:长度是 varint,空表写 0x00;紧跟着 n 个裸 u32。
|
|||
|
|
if (body.Length > pos)
|
|||
|
|
{
|
|||
|
|
var n = body[pos];
|
|||
|
|
pos += 1;
|
|||
|
|
for (var i = 0; i < n && pos + 4 <= body.Length; i++)
|
|||
|
|
{
|
|||
|
|
counts.Add(BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(pos, 4)));
|
|||
|
|
pos += 4;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (body.Length >= pos + 4)
|
|||
|
|
npcId = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(pos, 4));
|
|||
|
|
|
|||
|
|
var state = LivehouseState(msg);
|
|||
|
|
if (state is null)
|
|||
|
|
{
|
|||
|
|
conn.Log.Warn($"[LIVEHOUSE] SetLiveHouseMusicResult musicId={musicId} 无 world state ⇒ 本次不落盘");
|
|||
|
|
return conn.ReturnEmptyOkAsync(msg);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
LivehouseProgressStore.Record(state, musicId, counts);
|
|||
|
|
conn.Log.Info(
|
|||
|
|
$"[LIVEHOUSE] result musicId={musicId} npcId={npcId} counts={counts.Count} " +
|
|||
|
|
$"P={U(counts, 0)} G={U(counts, 1)} M={U(counts, 2)} S={U(counts, 3)}");
|
|||
|
|
|
|||
|
|
// 落盘走 PlayerProgressStore.Save 的既有 tick(Save 读的就是 WorldEntryState)。
|
|||
|
|
return conn.ReturnEmptyOkAsync(msg);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// LiveHouseMusicStart (63293678) — Notify ; args: u32 musicId, u32 npcId, u32 difficulty
|
|||
|
|
// LiveHouseMusicInterrupt (63752208)— Notify ; args: u32 musicId, u32 npcId
|
|||
|
|
//
|
|||
|
|
// 两条都只是「服务端知道一下就够了」:客户端既不 await,回调里也没有业务分支。
|
|||
|
|
// 只落日志(将来要做局数统计时这份日志就是现成素材,同硬规则 112 的做法)。
|
|||
|
|
[Handler(MethodId.LiveHouseMusicStart)]
|
|||
|
|
[Handler(MethodId.LiveHouseMusicInterrupt)]
|
|||
|
|
private Task LiveHouseMusicNotify4229938(Connection conn, UxRpcMessage msg)
|
|||
|
|
{
|
|||
|
|
var body = msg.Body;
|
|||
|
|
var musicId = body.Length >= 4 ? BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(0, 4)) : 0u;
|
|||
|
|
var npcId = body.Length >= 8 ? BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(4, 4)) : 0u;
|
|||
|
|
|
|||
|
|
// Notify 的 handler 不需要回包,ReturnEmptyOkAsync 对 Notify 无意义(第三条坑见铁律 4)。
|
|||
|
|
var extra = msg.MethodId == MethodId.LiveHouseMusicStart && body.Length >= 12
|
|||
|
|
? $" difficulty={BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8, 4))}"
|
|||
|
|
: string.Empty;
|
|||
|
|
|
|||
|
|
conn.Log.Info($"[LIVEHOUSE] {msg.MethodId} musicId={musicId} npcId={npcId}{extra}");
|
|||
|
|
return Task.CompletedTask;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// AskLiveHouseUseItem (63665380) — Invoke ; args: u32 musicId ; return: void
|
|||
|
|
//
|
|||
|
|
// 「金手指」(强制全 Perfect)的道具消耗请求 —— LivehouseSelectPanelStore.lua:320。
|
|||
|
|
// ⛑ 客户端自己的前置检查才是权威:面板在道具不足时先弹
|
|||
|
|
// MessageConfig.LivehouseGoldFinger 而<b>不会走到这里</b>(:322)。
|
|||
|
|
// ⇒ 服务端再扣一次是双重扣减;这里只放行,扣减交给客户端驱动的道具系统。
|
|||
|
|
[Handler(MethodId.AskLiveHouseUseItem, HandlerPacketKind.Invoke)]
|
|||
|
|
private Task AskLiveHouseUseItem4229938(Connection conn, UxRpcMessage msg)
|
|||
|
|
{
|
|||
|
|
var musicId = msg.Body.Length >= 4
|
|||
|
|
? BinaryPrimitives.ReadUInt32LittleEndian(msg.Body.AsSpan(0, 4))
|
|||
|
|
: 0u;
|
|||
|
|
|
|||
|
|
conn.Log.Info($"[LIVEHOUSE] AskLiveHouseUseItem musicId={musicId} -> 放行(扣减由客户端道具系统负责)");
|
|||
|
|
return conn.ReturnEmptyOkAsync(msg);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------------------------------
|
|||
|
|
// 2. PartyDance 舞池
|
|||
|
|
//
|
|||
|
|
// ★★ 关键:契约 SyncPartyDanceStartPush / SyncPartyDanceEndPush <b>早就写好了</b>
|
|||
|
|
// (PartyWireContracts.cs:306 / :356),但全仓零使用 ⇒ 舞会从来没真正开始过。
|
|||
|
|
//
|
|||
|
|
// 客户端三连(ChooseDanceMusicPanelStore.lua:343-384 RequestStartPartyDance):
|
|||
|
|
// AskSelectPartyDanceSong(musicId) → AskSelectPartyDanceMode(mode) → AskStartPartyDance()
|
|||
|
|
// ⛑ 规则 257:那三处 `if err == MessageConfig.Ok then DisplayServerMessageId; return end`
|
|||
|
|
// 要按「<b>err != Ok 才中断</b>」读(反混淆把比较符翻了),否则下一步永远不发。
|
|||
|
|
// AskStartPartyDance 的 Callback 什么都不做 ⇒ 客户端在等 SyncPartyDanceStart。
|
|||
|
|
// ---------------------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
private const string PartyDanceSongKey = "PartyDance.SelectedMusicId";
|
|||
|
|
private const string PartyDanceModeKey = "PartyDance.SelectedMode";
|
|||
|
|
|
|||
|
|
[Handler(MethodId.AskSelectPartyDanceSong, HandlerPacketKind.Invoke)]
|
|||
|
|
private Task AskSelectPartyDanceSong4229938(Connection conn, UxRpcMessage msg)
|
|||
|
|
{
|
|||
|
|
var musicId = msg.Body.Length >= 4
|
|||
|
|
? BinaryPrimitives.ReadUInt32LittleEndian(msg.Body.AsSpan(0, 4))
|
|||
|
|
: 0u;
|
|||
|
|
|
|||
|
|
var session = msg.Context?.Session;
|
|||
|
|
if (session is not null)
|
|||
|
|
session.Items[PartyDanceSongKey] = musicId;
|
|||
|
|
|
|||
|
|
conn.Log.Info($"[PARTYDANCE] select song musicId={musicId}");
|
|||
|
|
return conn.ReturnEmptyOkAsync(msg);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[Handler(MethodId.AskSelectPartyDanceMode, HandlerPacketKind.Invoke)]
|
|||
|
|
private Task AskSelectPartyDanceMode4229938(Connection conn, UxRpcMessage msg)
|
|||
|
|
{
|
|||
|
|
// 单个 byte(UX.Game.PartyDanceMode,值域 {0,1})。服务端不猜枚举名,回显即可。
|
|||
|
|
var mode = msg.Body.Length > 0 ? msg.Body[0] : (byte)0;
|
|||
|
|
|
|||
|
|
var session = msg.Context?.Session;
|
|||
|
|
if (session is not null)
|
|||
|
|
session.Items[PartyDanceModeKey] = mode;
|
|||
|
|
|
|||
|
|
conn.Log.Info($"[PARTYDANCE] select mode={mode}");
|
|||
|
|
return conn.ReturnEmptyOkAsync(msg);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// AskStartPartyDance (63292540) — Invoke ; 无参 ; return: void
|
|||
|
|
//
|
|||
|
|
// ★ 唯一能让舞会真正开始的推送点。不推 = 面板开得出来、按钮点得动、什么都不会发生。
|
|||
|
|
[Handler(MethodId.AskStartPartyDance, HandlerPacketKind.Invoke)]
|
|||
|
|
private async Task AskStartPartyDance4229938(Connection conn, UxRpcMessage msg)
|
|||
|
|
{
|
|||
|
|
uint musicId = 0;
|
|||
|
|
byte mode = 0;
|
|||
|
|
|
|||
|
|
var session = msg.Context?.Session;
|
|||
|
|
if (session is not null)
|
|||
|
|
{
|
|||
|
|
if (session.Items.TryGetValue(PartyDanceSongKey, out var songObj) && songObj is uint song)
|
|||
|
|
musicId = song;
|
|||
|
|
if (session.Items.TryGetValue(PartyDanceModeKey, out var modeObj) && modeObj is byte m)
|
|||
|
|
mode = m;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ZoneGadgetUId 必须 = 0:官方哨兵,语义是「没有舞池机关区域 ⇒ 人人都是参与者」
|
|||
|
|
// (PartyManager.lua:432)。填非 0 还得先把 Slot1/Slot2 占好,否则客户端不认参与者身份。
|
|||
|
|
await conn.NotifyAsync(MethodId.IGameSceneToClient_SyncPartyDanceStart, new SyncPartyDanceStartPush
|
|||
|
|
{
|
|||
|
|
ZoneGadgetUId = 0,
|
|||
|
|
MusicId = musicId,
|
|||
|
|
Mode = mode,
|
|||
|
|
StartTimestamp = (uint)DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
conn.Log.Info($"[PARTYDANCE] start musicId={musicId} mode={mode} -> SyncPartyDanceStart");
|
|||
|
|
await conn.ReturnEmptyOkAsync(msg);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// AskLeavePartyDance (63516846) — Invoke ; 无参 ; return: void
|
|||
|
|
[Handler(MethodId.AskLeavePartyDance, HandlerPacketKind.Invoke)]
|
|||
|
|
private async Task AskLeavePartyDance4229938(Connection conn, UxRpcMessage msg)
|
|||
|
|
{
|
|||
|
|
// 与 Start 成对,同样用 0 哨兵(PartyWireContracts.cs:356 有说明)。
|
|||
|
|
await conn.NotifyAsync(MethodId.IGameSceneToClient_SyncPartyDanceEnd, new SyncPartyDanceEndPush
|
|||
|
|
{
|
|||
|
|
ZoneGadgetUId = 0,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
conn.Log.Info("[PARTYDANCE] leave -> SyncPartyDanceEnd");
|
|||
|
|
await conn.ReturnEmptyOkAsync(msg);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------------------------------
|
|||
|
|
// 3. PartyLive 派对直播
|
|||
|
|
//
|
|||
|
|
// ★★ 数据入口只有一个:S2C 推送 SyncPartyResponse(68456156)。
|
|||
|
|
// PartyLiveStreamPanelStore.lua 自身 grep gClientToGameDelegate = <b>0 命中</b>,
|
|||
|
|
// 一切都挂在 gEventConstants.ON_SYNC_PARTY_RESPONSE 上,而它唯一的发送点是
|
|||
|
|
// PartyManager.lua:702 OnSyncResponse ⇒ 来自 SyncPartyResponse。
|
|||
|
|
//
|
|||
|
|
// ⛑⛑ likeList / giftList 的元素<b>不是计数,是观众 npcId</b>
|
|||
|
|
// (ProcessGiftList:212-240:`for _, npcId in ipairs(giftList)`,出现一次 = 该观众又送了一份,
|
|||
|
|
// 客户端自己 `data.count = data.count + 1` 累加)。写反了就永远显示没人送礼。
|
|||
|
|
// ⛑ npcId 是 u64,客户端过 `gFurnitureUtils:ConvertServerUidToNumber(npcInfo.NpcId)` 后再
|
|||
|
|
// 去 npcIdList 花名册里反查 ⇒ 不在这份花名册里的 id 会被直接丢掉(面板上没反应)。
|
|||
|
|
//
|
|||
|
|
// C2S 三条:
|
|||
|
|
// SendPartyEvent(63765017) args: int32 eventid, string comment
|
|||
|
|
// PartyLiveSwitch(63948406) args: bool start
|
|||
|
|
// GmPartyLiveAddEvent(65785804) args: int32 eventid, string message ← GM 通道
|
|||
|
|
// ---------------------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
private const string PartyLiveLedgerKey = "PartyLive.Ledger";
|
|||
|
|
private const int PartyLiveMaxRows = 64;
|
|||
|
|
|
|||
|
|
/// <summary>每个 session 一份直播间账本。没有 session 时返回一个丢弃用的空壳。</summary>
|
|||
|
|
private static PartyLiveResponsePush PartyLiveLedger(Dictionary<string, object>? bag)
|
|||
|
|
{
|
|||
|
|
if (bag is null)
|
|||
|
|
return new PartyLiveResponsePush();
|
|||
|
|
|
|||
|
|
if (bag.TryGetValue(PartyLiveLedgerKey, out var existing) && existing is PartyLiveResponsePush ledger)
|
|||
|
|
return ledger;
|
|||
|
|
|
|||
|
|
var fresh = new PartyLiveResponsePush();
|
|||
|
|
bag[PartyLiveLedgerKey] = fresh;
|
|||
|
|
return fresh;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private Task PushPartyLiveAsync(Connection conn, PartyLiveResponsePush response, string why)
|
|||
|
|
{
|
|||
|
|
conn.Log.Info(
|
|||
|
|
$"[PARTYLIVE] {why} -> SyncPartyResponse like={response.LikeList.Count} " +
|
|||
|
|
$"gift={response.GiftList.Count} popularity={response.Popularity} msg={response.NPCMessage.Count}");
|
|||
|
|
|
|||
|
|
return conn.NotifyAsync(MethodId.SyncPartyResponse, new SyncPartyResponsePush
|
|||
|
|
{
|
|||
|
|
Response = response,
|
|||
|
|
NPCIds = new List<PartyNpcIdInfoPush>(),
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[Handler(MethodId.PartyLiveSwitch, HandlerPacketKind.Invoke)]
|
|||
|
|
private async Task PartyLiveSwitch4229938(Connection conn, UxRpcMessage msg)
|
|||
|
|
{
|
|||
|
|
// 单个 bool:`SerializeBase.WritePrimitive(writer, start, writer.WriteBoolean, false)`
|
|||
|
|
var start = msg.Body.Length > 0 && msg.Body[0] != 0;
|
|||
|
|
conn.Log.Info($"[PARTYLIVE] switch start={start}");
|
|||
|
|
|
|||
|
|
var bag = msg.Context?.Session?.Items;
|
|||
|
|
if (start && bag is not null)
|
|||
|
|
{
|
|||
|
|
// 开播推一次空账本:客户端是纯追加渲染,不会自己清上一次会话残留的礼物提示。
|
|||
|
|
bag[PartyLiveLedgerKey] = new PartyLiveResponsePush();
|
|||
|
|
await PushPartyLiveAsync(conn, new PartyLiveResponsePush(), "switch on (clear)");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
await conn.ReturnEmptyOkAsync(msg);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[Handler(MethodId.SendPartyEvent, HandlerPacketKind.Invoke)]
|
|||
|
|
private async Task SendPartyEvent4229938(Connection conn, UxRpcMessage msg)
|
|||
|
|
{
|
|||
|
|
ReadInt32AndString(msg.Body, out var eventId, out var comment);
|
|||
|
|
|
|||
|
|
var bag = msg.Context?.Session?.Items;
|
|||
|
|
var ledger = PartyLiveLedger(bag);
|
|||
|
|
|
|||
|
|
// PartyLiveStreamPanelStore.lua:发送的是 LTConfig.PartyPartyEventConfig.AskForLike 等事件 id。
|
|||
|
|
// 服务端不猜客户端表里哪个 id 是点赞/送礼(我们手上没有 PartyPartyEventConfig),
|
|||
|
|
// ⇒ 按符号分流:正 id 记进 likeList、负 id 取绝对值记进 giftList。
|
|||
|
|
// GM 想指定走礼物就从 GmPartyLiveAddEvent 发负 eventId。
|
|||
|
|
lock (ledger)
|
|||
|
|
{
|
|||
|
|
if (eventId >= 0)
|
|||
|
|
AppendRow(ledger.LikeList, eventId);
|
|||
|
|
else
|
|||
|
|
AppendRow(ledger.GiftList, -eventId);
|
|||
|
|
|
|||
|
|
ledger.Popularity = (uint)(ledger.LikeList.Count + (ledger.GiftList.Count * 3));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
await PushPartyLiveAsync(conn, ledger, $"event id={eventId} comment={comment}");
|
|||
|
|
await conn.ReturnEmptyOkAsync(msg);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GmPartyLiveAddEvent (65785804) — Invoke ; args: int32 eventid, string message ; return: void
|
|||
|
|
//
|
|||
|
|
// ★ GM 通道。官方留它是给自己调试用的;我们拿它做「服务端往直播间塞一条弹幕」。
|
|||
|
|
// 这是三个玩法里最容易现场见效的一处(不用改 GM 面板,GATE Lua / GM delegate 直接发即可)。
|
|||
|
|
[Handler(MethodId.GmPartyLiveAddEvent, HandlerPacketKind.Invoke)]
|
|||
|
|
private async Task GmPartyLiveAddEvent4229938(Connection conn, UxRpcMessage msg)
|
|||
|
|
{
|
|||
|
|
ReadInt32AndString(msg.Body, out var eventId, out var message);
|
|||
|
|
|
|||
|
|
var bag = msg.Context?.Session?.Items;
|
|||
|
|
var ledger = PartyLiveLedger(bag);
|
|||
|
|
|
|||
|
|
lock (ledger)
|
|||
|
|
{
|
|||
|
|
ledger.NPCMessage.Add(new PartyNpcLiveMessagePush { Id = eventId, Message = message });
|
|||
|
|
if (ledger.NPCMessage.Count > PartyLiveMaxRows)
|
|||
|
|
ledger.NPCMessage.RemoveRange(0, ledger.NPCMessage.Count - PartyLiveMaxRows);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
await PushPartyLiveAsync(conn, ledger, $"gm event id={eventId} msg={message}");
|
|||
|
|
await conn.ReturnEmptyOkAsync(msg);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 读 <c>int32 eventid + string</c>。string 的形状是 <c>ReadString</c>:
|
|||
|
|
/// 无值写单字节 0x00,有值写 0xFF 后跟 UTF8 裸字节。
|
|||
|
|
/// </summary>
|
|||
|
|
private static void ReadInt32AndString(byte[] body, out int eventId, out string text)
|
|||
|
|
{
|
|||
|
|
eventId = 0;
|
|||
|
|
text = string.Empty;
|
|||
|
|
if (body.Length < 4)
|
|||
|
|
return;
|
|||
|
|
|
|||
|
|
eventId = BinaryPrimitives.ReadInt32LittleEndian(body.AsSpan(0, 4));
|
|||
|
|
var pos = 4;
|
|||
|
|
if (pos >= body.Length)
|
|||
|
|
return;
|
|||
|
|
|
|||
|
|
if (body[pos] == 0)
|
|||
|
|
return;
|
|||
|
|
|
|||
|
|
pos += 1;
|
|||
|
|
if (pos < body.Length)
|
|||
|
|
text = Encoding.UTF8.GetString(body, pos, body.Length - pos);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>往「累计送礼表」追加一行。<b>每个元素是一次事件</b>,不去重也不断言 id 范围。</summary>
|
|||
|
|
private static void AppendRow(List<int> list, int id)
|
|||
|
|
{
|
|||
|
|
list.Add(id);
|
|||
|
|
if (list.Count > PartyLiveMaxRows)
|
|||
|
|
list.RemoveRange(0, list.Count - PartyLiveMaxRows);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static uint U(List<uint> list, int index) => index < list.Count ? list[index] : 0u;
|
|||
|
|
}
|