Files
Ananta-ps/Ananta.Server/Ananta.Handlers/Game/GameRouter.MahjongSolo.cs
T

658 lines
28 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
/// <summary>
/// 单机麻将(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 规则本来就不吃。
/// </summary>
internal sealed partial class GameRouter
{
// ---------------------------------------------------------------- 常量
/// <summary>座位 duty 基值(SyncMahjongReadyStart 的第二字段 = 12114040 + seat)。</summary>
private const uint MahjongSeatDutyBase = 12114040;
/// <summary>
/// NPC 座位的展示模板 —— 实机样本(r62-mahjong-wire-samples.json 的 SyncMjLoginResult)
/// 四个座位都是 15020967,NPC 与真人同模板(用 Python 按本文件字段序解析后核对:见 _mj_wire_check.py)。
/// </summary>
private const int MahjongNpcTemplateId = 15020967;
/// <summary>NPC agent 实例 id 前缀(实机样本里是 0x3000000002 / 05 / 07)。</summary>
private const ulong MahjongNpcAgentBase = 0x0000300000000000UL;
/// <summary>合成房间 id 的自增起点(客户端只当它是桌子 uid)。</summary>
private static ulong _mahjongRoomSeq = 0x0000300000000000UL;
// ---------------------------------------------------------------- 状态
/// <summary>一个座位的展示身份(对应 SyncMjLoginResult 每座位那 16 段)。</summary>
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<ulong> Ready = new();
internal readonly HashSet<ulong> Loaded = new();
internal readonly HashSet<ulong> 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<ulong, MahjongGame> MahjongGames = new();
/// <summary>替代捐赠端的 CoopChanges:单人场景下就是一把进程内互斥锁。</summary>
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<int> 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<int> 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<Mj.Meld> 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<int>());
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<Mj.Meld>(), 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));
}
/// <summary>
/// 让三个 NPC 座位把该做的动作做掉(换牌 / 定缺 / 摸切 / 胡 / 过)。
/// Mj.Automatic 自带守卫(State、轮次、是否已响应),所以无脑轮询是安全的。
/// </summary>
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";
}
/// <summary>
/// AskStartMahjongGame(63184671) —— 单机麻将入口。
/// C2S = roomType(u8) + npcIdLs(List7Bit u32) + seatIndex(i32) + addFavor(bool) + gameType(u8)。
/// RET = void(空 body + retcode 0,与捐赠端同形)。
/// </summary>
[Handler(MethodId.AskStartMahjongGame)]
private async Task MahjongStart4229938(Connection conn, UxRpcMessage msg)
{
var b = msg.Body ?? Array.Empty<byte>();
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<byte>(), 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<byte>(), 0);
return;
}
int seat = HumanSeat(g);
var b = msg.Body ?? Array.Empty<byte>();
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<byte>(), 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<int>();
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;
}
}