using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using Ananta.SDK.Network;
using Ananta.SDK.Rpc;
using Ananta.SDK.Serialization;
using Ananta.Server.Configuration;
using Ananta.Server.Protocol.Client4229938;
using Mj = Ananta.Server.Protocol.Client4229938.MahjongRules4229938;
namespace Ananta.Server.Handlers.Game;
///
/// 单机麻将(1 真人 + 3 NPC)—— 移植自捐赠分支 DRMK-PS_09_HyNB-v1.9-R64 的
/// GameRouter.MahjongOnline.cs(391 行)+ MahjongRules4229938.cs(329 行,零改动直拷)。
///
/// 为什么现在能做(2026-10-04 重估,推翻 10-03 的"重活、建议单独排期"判断):
/// 1. 主线 MethodIds.json 与该分支字节级相同,40+ 个 SyncMj* 常量本来就全齐,不需要搬协议表;
/// 2. 该分支带 .runtime/r62-mahjong-wire-samples.json(41KB 实机抓包),
/// SyncMjLoginResult 里三个 NPC 座位就是 Player101/102/103,
/// 说明实机跑的正是"1 真人 + 3 NPC"这个形态;
/// 3. 原实现的 Coop 依赖只集中在 5 个辅助函数里(MahjongPeers / NotifyCoopPeerAsync /
/// CoopRoster.TryGetBySession / CoopChanges / ActivityLobbies),
/// 单人场景下全部退化为 MahjongGate + 直连推送。
///
/// wire 形状逐字段照抄捐赠端,未重新推导。本机复核过的样本片段(SyncMjLoginResult):
/// FF + U64(pid) + U64(agentId) + I32(templateId) + U32(level) + UxString(name) +
/// FF 00 + U32(templateId) + U32(0) + I32(score=10000) + I32(seat) + U32(0) + U32(0),
/// 四座位之后接 FF 05 + 4 个 bool + I32(owner)。
///
/// 已知局限:Mj.Automatic 的对手策略是"能胡就胡 + 弃定缺花色/摸切 + 别人出牌一律 Pass",
/// 所以 NPC 不会碰/杠/吃。规则层能打完整局,但对手很笨。
/// 客户端侧确认无本地 AI(Gameplay/Majiang/ 22 个 lua 里 AI/Robot 零命中)。
///
/// 没登记的号:Exit(63320834) 与 Chi(63693559) 故意不进白名单 ——
/// 前者名字太通用(别的玩法也在用),后者 XLCH 规则本来就不吃。
///
internal sealed partial class GameRouter
{
// ---------------------------------------------------------------- 常量
/// 座位 duty 基值(SyncMahjongReadyStart 的第二字段 = 12114040 + seat)。
private const uint MahjongSeatDutyBase = 12114040;
///
/// NPC 座位的展示模板 —— 实机样本(r62-mahjong-wire-samples.json 的 SyncMjLoginResult)
/// 四个座位都是 15020967,NPC 与真人同模板(用 Python 按本文件字段序解析后核对:见 _mj_wire_check.py)。
///
private const int MahjongNpcTemplateId = 15020967;
/// NPC agent 实例 id 前缀(实机样本里是 0x3000000002 / 05 / 07)。
private const ulong MahjongNpcAgentBase = 0x0000300000000000UL;
/// 合成房间 id 的自增起点(客户端只当它是桌子 uid)。
private static ulong _mahjongRoomSeq = 0x0000300000000000UL;
// ---------------------------------------------------------------- 状态
/// 一个座位的展示身份(对应 SyncMjLoginResult 每座位那 16 段)。
private sealed class MjSeatInfo
{
internal ulong Pid;
internal ulong AgentId; // 自己视角看到的 agent
internal ulong RemoteAgentId; // 别人视角看到的 agent
internal int TemplateId;
internal uint Level;
internal string Name = string.Empty;
internal bool IsHuman;
}
private sealed class MahjongGame
{
internal ulong Room;
internal TcpSession Session = null!;
internal readonly ulong[] Players = new ulong[4];
internal MjSeatInfo[] Seats = new MjSeatInfo[4];
internal readonly HashSet Ready = new();
internal readonly HashSet Loaded = new();
internal readonly HashSet Again = new();
internal readonly byte[] Auto = new byte[4];
internal readonly int[] Misses = new int[4];
internal Mj Rules = new(RandomNumberGenerator.GetInt32(4));
internal int[] WireIds = NewMahjongIds();
internal byte Phase; // 0 未开局 / 1 loading / 2 playing / 3 finished
internal int SeenVersion = -1;
internal long Clock = Stopwatch.GetTimestamp();
}
private static readonly Dictionary MahjongGames = new();
/// 替代捐赠端的 CoopChanges:单人场景下就是一把进程内互斥锁。
private static readonly SemaphoreSlim MahjongGate = new(1, 1);
private static ulong NextMahjongRoom() => Interlocked.Increment(ref _mahjongRoomSeq);
private static int[] NewMahjongIds()
{
int[] a = Enumerable.Range(1, 108).ToArray();
for (int i = a.Length - 1; i > 0; i--)
{
int j = RandomNumberGenerator.GetInt32(i + 1);
(a[i], a[j]) = (a[j], a[i]);
}
return a;
}
private static MahjongGame? FindMahjong(TcpSession session)
{
foreach (var g in MahjongGames.Values)
if (ReferenceEquals(g.Session, session)) return g;
return null;
}
private static int HumanSeat(MahjongGame g)
{
for (int i = 0; i < 4; i++)
if (g.Seats[i] != null && g.Seats[i].IsHuman) return i;
return 0;
}
private static void FillMahjongSeats(MahjongGame g, ulong playerPid, uint templateId, string playerName, int human)
{
for (int i = 0; i < 4; i++)
{
bool isHuman = i == human;
ulong npcAgent = MahjongNpcAgentBase | (uint)(i * 2 + 1);
g.Seats[i] = new MjSeatInfo
{
Pid = isHuman ? playerPid : (ulong)(101 + i),
AgentId = isHuman ? playerPid : npcAgent,
RemoteAgentId = isHuman ? playerPid : npcAgent,
TemplateId = isHuman ? (int)templateId : MahjongNpcTemplateId,
Level = 1,
// 实机样本里 NPC 座位名就是 Player101/102/103;先用同款保证 wire 同构。
Name = isHuman ? playerName : "Player" + (100 + i).ToString(),
IsHuman = isHuman,
};
g.Players[i] = g.Seats[i].Pid;
}
}
private static Task MjSend(MahjongGame g, uint method, UxWriter body)
{
byte[] bytes = body.ToArray();
g.Session.Log.Info($"[MJ] -> mid={method} {bytes.Length}b {Convert.ToHexString(bytes)}");
return g.Session.NotifyAsync(method, bytes, CancellationToken.None);
}
// ---------------------------------------------------------------- wire(照抄捐赠端)
private static UxWriter MjInts(UxWriter w, IReadOnlyList values)
{
w.U8(255).Int7(values.Count);
foreach (int n in values) w.I32(n);
return w;
}
private static UxWriter MjTile(UxWriter w, MahjongGame g, int tile, bool reveal = true)
{
// 牌面 id 是一次随机置换:客户端动画需要稳定 id,但不该从 id 反推暗牌。
if (tile == 0) return w.I32(-1).I32(0).U8(1).Bool(false).I32(27);
return w.I32(g.WireIds[tile - 1]).I32(reveal ? Mj.Index(tile) % 9 : 0)
.U8(reveal ? Mj.Suit(tile) : (byte)1).Bool(false).I32(reveal ? Mj.Index(tile) : 27);
}
private static UxWriter MjTiles(UxWriter w, MahjongGame g, IEnumerable tiles, bool reveal = true)
{
int[] a = tiles.ToArray();
w.U8(255).Int7(a.Length);
foreach (int t in a) MjTile(w, g, t, reveal);
return w;
}
private static bool MjVisible(MahjongGame g, int seat, int viewer)
=> seat == viewer || g.Rules.Seats[seat].HasWon || g.Phase == 3;
private static UxWriter MjMeld(UxWriter w, MahjongGame g, Mj.Meld m, bool reveal = true)
{
MjTile(w.U8(255).I32(m.Kind == 2 ? -1 : m.Source), g, m.Tile, reveal);
return MjTiles(w, g, m.Kind == 2 ? m.Selected.Where(t => t != m.Tile) : m.Selected, reveal).U8(m.Kind);
}
private static UxWriter MjMelds(UxWriter w, MahjongGame g, IEnumerable melds, int seat, int viewer)
{
var a = melds.ToArray();
w.U8(255).Int7(a.Length);
foreach (var m in a) MjMeld(w, g, m, m.Kind != 2 || MjVisible(g, seat, viewer));
return w;
}
private static UxWriter MjInfo(MahjongGame g, int viewer)
{
var r = g.Rules;
var w = new UxWriter().U8(255).U8(r.State).I32(r.Round).I32(r.Remaining).I32(r.Banker)
.I32(r.Turn).I32(r.LastTurn).I32(r.LastDrawSeat).U8(255).Int7(4);
for (int seat = 0; seat < 4; seat++)
{
var s = r.Seats[seat];
w.U8(255).I32(s.Score).I32(s.Holds.Count);
MjTiles(w, g, s.Holds, MjVisible(g, seat, viewer));
MjTiles(w, g, s.Folds);
MjMelds(w, g, s.Melds, seat, viewer);
w.I32(0).U8(r.State >= 4 || seat == viewer ? s.Que : (byte)0);
MjTiles(w, g, s.Received, seat == viewer);
MjTiles(w, g, s.HuTiles);
}
MjTiles(w, g, r.WonTiles);
for (int i = 0; i < 4; i++) MjTiles(w, g, Array.Empty());
return w.F32((float)Math.Min(30, Stopwatch.GetElapsedTime(g.Clock).TotalSeconds));
}
private static UxWriter MjRoom(MahjongGame g, int viewer)
{
var w = new UxWriter().U8(255).I32(1).U64(g.Room).U8(2)
.U8(g.Phase == 3 ? (byte)3 : g.Phase >= 1 ? (byte)2 : (byte)0)
.U8(255).Int7(4);
for (int seat = 0; seat < 4; seat++)
{
var s = g.Seats[seat];
bool self = g.Seats[viewer].Pid == s.Pid;
w.U8(255).U64(s.Pid).U64(self ? s.AgentId : s.RemoteAgentId)
.I32(s.TemplateId).U32(s.Level).UxString(s.Name)
.U8(255).U8(0).U32((uint)s.TemplateId).U32(0)
.I32(g.Rules.Seats[seat].Score).I32(seat).U32(0).U32(0);
}
w.U8(255).Int7(4);
for (int i = 0; i < 4; i++) w.Bool(g.Loaded.Contains(g.Players[i]));
return w.I32(HumanSeat(g));
}
private static UxWriter MjWorldRoom(MahjongGame g)
{
var w = new UxWriter().U8(255).U64(g.Room).U64(g.Seats[HumanSeat(g)].Pid)
.U8(255).Int7(g.Players.Count(p => p != 0));
for (int i = 0; i < 4; i++)
if (g.Players[i] != 0)
w.U8(255).U64(g.Players[i]).U32(MahjongSeatDutyBase + (uint)i)
.Bool(g.Ready.Contains(g.Players[i])).Bool(g.Seats[i].IsHuman);
return w;
}
private static UxWriter MjResult(MahjongGame g)
{
var w = new UxWriter().U8(255).U8(255).Int7(g.Rules.Actions.Count);
foreach (var a in g.Rules.Actions)
{
w.U8(255).U8(a.Kind).I32(a.Owner);
MjInts(w, a.Targets).I32(a.Score).Bool(a.Transferred)
.I32(a.Pattern == 0 ? 2 : a.Pattern == 1 ? 1 : 0).U8(a.Pattern).U8(255).Int7(a.Kind == 0 ? 1 : 0);
if (a.Kind == 0) w.U8(a.Pattern);
MjTile(w, g, a.Tile).I32(a.Roots).I32(a.Flags).I32(a.Fan);
}
w.U8(255).Int7(4);
foreach (var s in g.Rules.Seats) MjTiles(w.U8(255), g, s.Holds);
return w;
}
// ---------------------------------------------------------------- 流程
private static async Task LoadMahjongCoreAsync(MahjongGame g)
{
g.Phase = 1;
g.Loaded.Clear();
g.Clock = Stopwatch.GetTimestamp();
await MjSend(g, MethodId.IGameSceneToClient_SyncMahjongWorldBattleRoomInfo, MjWorldRoom(g));
await MjSend(g, MethodId.IGameSceneToClient_SyncMahjongReadyStart,
new UxWriter().U64(g.Room).U32(MahjongSeatDutyBase + (uint)HumanSeat(g)));
}
private static async Task BeginMahjongCoreAsync(MahjongGame g)
{
if (g.Phase >= 2) return;
g.Phase = 2;
g.Rules.Start();
g.Clock = Stopwatch.GetTimestamp();
g.SeenVersion = g.Rules.Version;
for (int i = 0; i < 4; i++) g.Loaded.Add(g.Players[i]);
await MjSend(g, MethodId.SyncInMjGame, new UxWriter().Bool(true).Bool(false).U8(2));
await MjSend(g, MethodId.SyncMjLoginResult, MjRoom(g, HumanSeat(g)));
await MjSend(g, MethodId.SyncMjGameInfo, MjInfo(g, HumanSeat(g)));
await FlushMahjongEvents(g);
await RunMahjongNpcTurnsAsync(g);
}
private static async Task FlushMahjongEvents(MahjongGame g)
{
int viewer = HumanSeat(g);
if (g.SeenVersion != g.Rules.Version)
{
g.SeenVersion = g.Rules.Version;
g.Clock = Stopwatch.GetTimestamp();
}
foreach (var e in g.Rules.Events)
{
switch (e.Kind)
{
case "ExchangeBegin":
case "DingBegin":
await MjResumePromptAsync(g, viewer);
break;
case "ExchangeChosen":
if (e.Seat == viewer)
await MjSend(g, MethodId.SyncMjHuanPai, MjTiles(new(), g, e.Tiles!).I32(e.Seat));
break;
case "ExchangeDone":
for (int i = 0; i < 4; i++)
await MjSend(g, MethodId.SyncMjHuanPaiResult,
MjTiles(MjTiles(new UxWriter().I32(i), g, e.Hands![i], i == viewer),
g, g.Rules.Seats[i].Received, i == viewer).I32(e.Value));
break;
case "DingDone":
{
var q = new UxWriter().U8(255).Int7(4);
foreach (var s in g.Rules.Seats) q.U8(s.Que);
await MjSend(g, MethodId.SyncMjDingQue, q);
break;
}
case "Turn":
await MjSend(g, MethodId.SyncMjTurn, new UxWriter().I32(e.Seat).F32(0));
break;
case "Score":
await MjSend(g, MethodId.SyncMjScoreChange, new UxWriter().I32(e.Seat).I32(e.Value).I32(e.Source));
break;
case "Draw":
case "Discard":
case "Peng":
case "Gang":
{
bool reveal = MjVisible(g, e.Seat, viewer);
var w = MjTile(new UxWriter().I32(e.Seat), g, e.Tile,
e.Kind == "Draw" ? reveal : e.Kind != "Gang" || e.Value != 2 || reveal);
uint mid;
if (e.Kind == "Draw") { w.I32(e.Value); mid = MethodId.SyncMjMoPai; }
else if (e.Kind == "Discard") { w.Bool(false); mid = MethodId.SyncMjChuPai; }
else
{
MjTiles(w, g, e.Kind == "Gang" && e.Value == 2 ? e.Tiles!.Where(t => t != e.Tile) : e.Tiles!,
e.Kind != "Gang" || e.Value != 2 || reveal);
// 这条 RPC 用 MjActionType;已经落袋的副露用 MjPCGType。
if (e.Kind == "Gang") w.U8(e.Value == 2 ? (byte)1 : e.Value == 4 ? (byte)2 : (byte)3);
mid = e.Kind == "Gang" ? MethodId.SyncMjGang : MethodId.SyncMjPeng;
}
await MjSend(g, mid, MjTiles(w, g, e.Hands![e.Seat], reveal));
break;
}
case "HanGang":
await MjSend(g, MethodId.SyncMjHanGang, MjTile(new UxWriter().I32(e.Seat), g, e.Tile).U8((byte)e.Value));
break;
case "Hu":
{
var hu = MjTile(MjInts(new(), e.Seats!), g, e.Tile).I32(e.Source);
MjInts(hu, e.Flags!);
hu.U8(255).Int7(e.Seats!.Length);
foreach (int seat in e.Seats) MjTiles(hu.U8(255), g, e.Hands![seat]);
await MjSend(g, MethodId.SyncMjHu, hu);
if (e.Value == 1)
await MjSend(g, MethodId.SyncMjHolds,
MjTiles(new(), g, e.Hands![e.Source], MjVisible(g, e.Source, viewer))
.I32(e.Hands[e.Source].Length).I32(e.Source));
break;
}
case "Pass":
if (e.Seat == viewer)
await MjSend(g, MethodId.SyncMjGuo, MjTile(new(), g, e.Tile));
break;
case "Pigs":
case "Refund":
case "Unready":
await MjSend(g, e.Kind == "Pigs" ? MethodId.SyncMjChaHuaZhu
: e.Kind == "Refund" ? MethodId.SyncMjTuiShui : MethodId.SyncMjChaDaJiao,
MjInts(new(), e.Seats!));
break;
case "Transfer":
await MjSend(g, MethodId.SyncMjMaoZhuanYu, new UxWriter().I32(e.Seat));
break;
case "End":
g.Phase = 3;
g.Again.Clear();
await MjSend(g, MethodId.SyncMjGameOver, MjResult(g));
await MjSend(g, MethodId.SyncMjRoomState, new UxWriter().U8(3));
break;
}
}
g.Rules.Events.Clear();
if (g.Phase == 2 && g.Rules.State == 4) await MjSendOptionsAsync(g, viewer);
}
private static async Task MjResumePromptAsync(MahjongGame g, int seat)
{
var r = g.Rules;
float elapsed = (float)Stopwatch.GetElapsedTime(g.Clock).TotalSeconds;
if (r.State == 1 && r.Seats[seat].Exchange == null)
await MjSend(g, MethodId.SyncMjHuanPaiBegin, MjTiles(new(), g, r.DefaultExchange(seat)).F32(elapsed));
if (r.State == 3 && r.Seats[seat].Que == 0)
await MjSend(g, MethodId.SyncMjDingQueBegin, new UxWriter().U8(r.DefaultQue(seat)).F32(elapsed));
if (r.State == 4) await MjSendOptionsAsync(g, seat);
}
private static Task MjSendOptionsAsync(MahjongGame g, int seat)
{
var o = g.Rules.GetOptions(seat);
var w = MjTile(new UxWriter().U8(255), g, o.Tile).Bool(o.Hu).Bool(false);
MjMelds(w, g, o.Peng, seat, seat);
MjMelds(w, g, Array.Empty(), seat, seat);
MjMelds(w, g, o.Gang, seat, seat);
return MjSend(g, MethodId.SyncMjOperations,
w.Bool(o.Discard).F32((float)Stopwatch.GetElapsedTime(g.Clock).TotalSeconds));
}
///
/// 让三个 NPC 座位把该做的动作做掉(换牌 / 定缺 / 摸切 / 胡 / 过)。
/// Mj.Automatic 自带守卫(State、轮次、是否已响应),所以无脑轮询是安全的。
///
private static async Task RunMahjongNpcTurnsAsync(MahjongGame g)
{
int human = HumanSeat(g);
for (int round = 0; round < 24; round++)
{
bool moved = false;
for (int seat = 0; seat < 4; seat++)
{
if (seat == human) continue;
byte flags = (byte)(g.Auto[seat] == 0 ? 7 : g.Auto[seat]);
if (g.Rules.Automatic(seat, flags)) moved = true;
}
if (!moved) break;
await FlushMahjongEvents(g);
await Task.Delay(350); // 给客户端留出牌动画的时间
}
await FlushMahjongEvents(g);
}
private static async Task RemoveMahjongGameCoreAsync(MahjongGame g)
{
if (!MahjongGames.Remove(g.Room)) return;
await MjSend(g, MethodId.SyncInMjGame, new UxWriter().Bool(false).Bool(false).U8(2));
await MjSend(g, MethodId.IGameSceneToClient_SyncMahjongTimeOut, new UxWriter().U64(g.Room));
await MjSend(g, MethodId.IGameSceneToClient_SyncMahjongWorldBattleRoomInfo, new UxWriter().U8(0));
}
private static async Task WatchMahjongAsync(MahjongGame g)
{
while (true)
{
await Task.Delay(500);
await MahjongGate.WaitAsync();
try
{
if (!MahjongGames.TryGetValue(g.Room, out var now) || !ReferenceEquals(now, g)) return;
double elapsed = Stopwatch.GetElapsedTime(g.Clock).TotalSeconds;
var r = g.Rules;
if (g.Phase != 2)
{
if (elapsed < (g.Phase == 3 ? 300 : 600)) continue;
await RemoveMahjongGameCoreAsync(g);
return;
}
double limit = r.State == 1 ? 20 : r.State == 3 ? 10 : 30;
bool expired = elapsed >= limit;
int human = HumanSeat(g);
if (expired)
{
// 真人超时:替他自动走一步;连续 3 次转托管(与捐赠端同策略)。
if (r.Automatic(human, 7))
{
if (++g.Misses[human] >= 3 && g.Auto[human] != 7)
{
g.Auto[human] = 7;
await MjSend(g, MethodId.SyncMjAutoEnterTuoGuan, new UxWriter());
}
g.Clock = Stopwatch.GetTimestamp();
}
}
if (expired || r.Waiting || r.State == 4) await RunMahjongNpcTurnsAsync(g);
}
finally { MahjongGate.Release(); }
}
}
// ---------------------------------------------------------------- 入口 / 打牌
private static string MahjongPlayerName()
{
var p = PrivateServerConfigStore.Current.Player;
if (!string.IsNullOrWhiteSpace(p.DisplayName)) return p.DisplayName;
if (!string.IsNullOrWhiteSpace(p.UserName)) return p.UserName;
return "Player";
}
///
/// AskStartMahjongGame(63184671) —— 单机麻将入口。
/// C2S = roomType(u8) + npcIdLs(List7Bit u32) + seatIndex(i32) + addFavor(bool) + gameType(u8)。
/// RET = void(空 body + retcode 0,与捐赠端同形)。
///
[Handler(MethodId.AskStartMahjongGame)]
private async Task MahjongStart4229938(Connection conn, UxRpcMessage msg)
{
var b = msg.Body ?? Array.Empty();
byte roomType = b.Length > 0 ? b[0] : (byte)0;
int seatIndex = b.Length >= 6 ? BinaryPrimitives.ReadInt32LittleEndian(b.AsSpan(b.Length - 6, 4)) : -1;
bool addFavor = b.Length >= 2 && b[^2] != 0;
byte gameType = b.Length >= 1 ? b[^1] : (byte)0;
var cfg = PrivateServerConfigStore.Current.Player;
int human = seatIndex >= 0 && seatIndex < 4 ? seatIndex : 0;
await MahjongGate.WaitAsync();
try
{
if (FindMahjong(conn.Session) is { } old) await RemoveMahjongGameCoreAsync(old);
var g = new MahjongGame { Room = NextMahjongRoom(), Session = conn.Session };
FillMahjongSeats(g, cfg.Pid, cfg.InitialSpiritTemplateId, MahjongPlayerName(), human);
MahjongGames[g.Room] = g;
conn.Log.Info($"[MJ] start room={g.Room} roomType={roomType} seat={human} " +
$"addFavor={addFavor} gameType={gameType} pid={cfg.Pid}");
await conn.ReturnRawAsync(msg, Array.Empty(), 0);
await LoadMahjongCoreAsync(g);
// 客户端若发 MahjongPlayerReady 会提前开局;否则 1.2s 后直接开,不等它。
_ = Task.Run(async () =>
{
await Task.Delay(1200);
await MahjongGate.WaitAsync();
try
{
if (MahjongGames.TryGetValue(g.Room, out var cur) && ReferenceEquals(cur, g))
await BeginMahjongCoreAsync(g);
}
finally { MahjongGate.Release(); }
});
_ = WatchMahjongAsync(g);
}
finally { MahjongGate.Release(); }
}
[Handler(MethodId.MahjongPlayerReady)]
[Handler(MethodId.HuanPai)]
[Handler(MethodId.DingQue)]
[Handler(MethodId.ChuPai)]
[Handler(MethodId.Peng)]
[Handler(MethodId.Gang)]
[Handler(MethodId.Hu)]
[Handler(MethodId.Guo)]
[Handler(MethodId.TuoGuan)]
[Handler(MethodId.ArrangeMjHolds)]
[Handler(MethodId.BackToMahjong)]
private async Task MahjongRequest4229938(Connection conn, UxRpcMessage msg)
{
await MahjongGate.WaitAsync();
try
{
if (FindMahjong(conn.Session) is not { } g)
{
if (msg.IsInvoke) await conn.ReturnRawAsync(msg, Array.Empty(), 0);
return;
}
int seat = HumanSeat(g);
var b = msg.Body ?? Array.Empty();
bool ok = false;
var r = g.Rules;
if (msg.MethodId == MethodId.MahjongPlayerReady)
{
if (b.Length == 8 && BitConverter.ToUInt64(b) == g.Room && g.Phase >= 1)
{
ok = true;
g.Loaded.Add(g.Players[seat]);
if (g.Phase == 1) await BeginMahjongCoreAsync(g);
}
}
else if (msg.MethodId == MethodId.BackToMahjong)
{
if (b.Length == 0 && g.Phase >= 2)
{
ok = true;
await MjSend(g, MethodId.SyncMjLoginResult, MjRoom(g, seat));
await MjSend(g, MethodId.SyncMjGameInfo, MjInfo(g, seat));
await MjResumePromptAsync(g, seat);
}
}
else if (g.Phase == 2)
{
if (msg.MethodId == MethodId.HuanPai)
ok = TryMjTiles(b, g, 3, out var exchange) && r.Exchange(seat, exchange);
else if (msg.MethodId == MethodId.DingQue)
ok = b.Length == 1 && r.DingQue(seat, b[0]);
else if (msg.MethodId == MethodId.Peng)
ok = TryMjTiles(b, g, 2, out var peng) && r.Peng(seat, peng);
else if (msg.MethodId == MethodId.Gang)
ok = b.Length == 14 && TryMjTile(new(b), g, out int kong) && r.Gang(seat, kong);
else if (msg.MethodId == MethodId.ChuPai)
ok = b.Length == 15 && b[14] == 0 && TryMjTile(new(b), g, out int discard) && r.Discard(seat, discard);
else if (msg.MethodId == MethodId.Hu)
ok = b.Length == 0 && r.Hu(seat);
else if (msg.MethodId == MethodId.Guo)
ok = b.Length == 0 && r.Pass(seat);
else if (msg.MethodId == MethodId.TuoGuan)
{
if (b.Length == 1 && b[0] <= 7) { g.Auto[seat] = b[0]; ok = true; }
}
else if (msg.MethodId == MethodId.ArrangeMjHolds)
ok = true; // 手牌排序纯客户端表现,服务端不记账。
if (ok)
{
g.Misses[seat] = 0;
g.Clock = Stopwatch.GetTimestamp();
}
}
conn.Log.Info($"[MJ] req mid={msg.MethodId} {b.Length}b ok={ok} phase={g.Phase} state={r.State}");
if (msg.IsInvoke)
await conn.ReturnRawAsync(msg,
ok && msg.MethodId == MethodId.Gang
? new UxWriter().Bool(r.Waiting && r.RobKong).ToArray()
: Array.Empty(), 0);
if (MahjongGames.ContainsKey(g.Room))
{
await FlushMahjongEvents(g);
await RunMahjongNpcTurnsAsync(g);
}
}
finally { MahjongGate.Release(); }
}
private static bool TryMjTile(UxReader reader, MahjongGame g, out int tile)
{
int id = reader.I32(), pai = reader.I32();
byte suit = reader.U8(), red = reader.U8();
int index = reader.I32();
tile = Array.IndexOf(g.WireIds, id) + 1;
return tile > 0 && index == Mj.Index(tile) && pai == index % 9 && suit == Mj.Suit(tile) && red == 0;
}
private static bool TryMjTiles(byte[] body, MahjongGame g, int count, out int[] tiles)
{
tiles = Array.Empty();
if (body.Length != 2 + 14 * count || body[0] != 255 || body[1] != count + 1) return false;
var r = new UxReader(body);
r.U8();
r.Int7();
tiles = new int[count];
for (int i = 0; i < count; i++)
if (!TryMjTile(r, g, out tiles[i])) return false;
return true;
}
}