☰
Akagi 协议桥(Bridge)模块深度解析:将雀魂、天鳳、麻雀一番街统一为 mjai 事件流
2026/10/14 2:53:47 网站建设 项目流程
  • 桌面应用
  • 人工智能

【免费下载链接】Akagi

支持雀魂、天鳳、麻雀一番街、天月麻將,能夠使用自定義的AI模型實時分析對局並給出建議,內建Mortal AI作為示例。 Supports Majsoul, Tenhou, Riichi City, Amatsuki, with the ability to use custom AI models to analyze games in real time and provide suggestions. Comes with Mortal AI as a built-in example.

项目地址:https://gitcode.com/gh_mirrors/ak/Akagi
点击查看免费下载

导读

Akagi 的核心能力是「实时分析对局并给出建议」,而这一切都建立在一条统一的事件流之上:无论数据来自雀魂的 protobuf WebSocket、天鳳的 JSON WebSocket,还是麻雀一番街的二进制帧 WebSocket,最终都要转换成 AI 模型与前端 HUD 都能消费的 mjai JSONL 事件。承担这一转换职责的,就是本文要讲的src/bridge协议桥模块。读完本文,你将理解 Bridge trait 的抽象设计、桥的工厂创建流程、autoplay 层通过BridgeHooks与桥共享信息的机制,以及三个平台桥各自的线格式解析细节与重连处理方案,并掌握向 Akagi 添加新平台桥的完整步骤。

Bridge 模块的定位:wire 协议与 mjai 事件流之间的翻译层

在 Akagi 的架构中,src/bridge负责把各游戏平台私有的「线上协议」(wire protocol)翻译成 mjai JSONL 协议事件流。mjai 是日本麻将 AI 对局服务器的标准协议(JSONL:每行一个 JSON 事件),AI 机器人(bot)直接消费该协议,因此桥的存在让 Akagi 得以「一次解析、多处复用」:无论是下游的 AI bot、对局分析引擎,还是前端 HUD,看到的都是同一套 mjai 事件。

模块自述(src/bridge/README.md):Platform-specific protocol bridges between game wire protocols and the mjai JSONL protocol consumed by AI bots.

值得强调的是,mjai 事件类型并不归 bridge 模块所有,而是定义在 src/schema/mjai/mod.rs 的crate::schema::mjai中。这一设计在 src/schema/README.md 中有明确说明:所有跨越模块边界的数据——协议事件、前后端 IPC 载荷、持久化记录——都放在 schema 模块,避免被某个子系统独占。因此桥的输出、AI bot 的输入、前端 HUD 的展示共用同一套MjaiEvent类型。

核心抽象:Bridgetrait

桥的抽象在 src/bridge/mod.rs 中定义为一个只含两个方法的 trait:

pub trait Bridge: Send { fn parse(&mut self, direction: Direction, content: &[u8]) -> ParseResult; fn build(&mut self, command: &MjaiEvent) -> Option<Vec<u8>>; }
  • parse— 将一条原始的入站 WS 二进制帧翻译为零个或多个MjaiEvent。它接收一个Direction参数标明帧方向:Direction::Up表示客户端→服务器的上行(如请求),Direction::Down表示服务器→客户端的下行(如响应、通知)。
  • build— 反向转换:把一个出站 mjai 命令(同样以MjaiEvent表示)编码为可选的原始 WS 二进制帧,供 autoplay(自动打牌)使用。返回None表示该平台无法/无需编码此命令。

解析结果ParseResult

parse的返回值不是简单的事件数组,而是 ParseResult:

pub struct ParseResult { pub events: Vec<MjaiEvent>, pub parsed: Option<ParsedFrame>, }

其中events是帧翻译出的 mjai 事件(可能为零个),而parsed是桥对帧的第一层结构化视图——例如 Majsoul 解码后的 protobuf 方法名与载荷、天鳳的{tag, …}JSON 字典——它被提供给 inspector(对局检查器),让开发者能看到「桥认为这个帧是什么意思」。无法解码的帧(握手、不支持的 method、畸形载荷)返回None。

mjai 事件类型:跨项目的统一契约

MjaiEvent枚举(src/schema/mjai/mod.rs)完整镜像了 mjai 协议规范的 15 种事件类型,外加 3 人麻将特有的kita(北抜き)变体:

事件含义关键字段
start_game对局开始names、id(bot 视角的自家座位)、num_players
start_kyoku局开始bakaze、dora_marker、oya、scores、tehais
tsumo摸牌actor、pai(他家摸牌为"?")
dahai打牌actor、pai、tsumogiri(是否摸切)
chi/pon/daiminkan/kakan/ankan副露actor、target、consumed
dora宝牌指示dora_marker
reach/reach_accepted立直声明 / 成立actor
hora和牌actor、target、deltas、ura_markers
ryukyoku流局deltas
kita北抜き(仅 3 人)actor、pai
end_kyoku/end_game局 / 对局结束reason(Confirmed/Terminated)

几个值得注意的实现细节(均有源码佐证):

  • 序列化约定:枚举使用#[serde(tag = "type", rename_all = "snake_case")],因此 JSON 中"type"永远是第一个键。测试 json_consistency_and_type_first 逐条断言了每个变体 round-trip 后type置顶。
  • 玩家数量字段:start_game与start_kyoku都携带num_players(3 或 4),serde 默认值为 4,保证旧的 4 人日志行仍能原样解析。
  • Tile 用String表示:如"1m"、"5mr"(赤五)、"E"、"P"、"?"。schema 刻意不引入第三方 Tile 类型,让桥与任何牌编码库解耦。
  • end_game的语义扩展:GameEndReason::Terminated表示服务器在无最终结算载荷的情况下终止了会话(如天鳳断线重连期间,见下文),序列化时仍为{"type":"end_game"}。

桥的实例化:一个 WebSocket flow 一个桥

「一个桥实例对应一个独立游戏会话」是模块的基本原则。对于 Majsoul,这意味着每个 WebSocket flow 一个桥实例——因为每个 flow 都有自己独立的请求 id 序列与游戏状态。这一约束直接体现在 src/bridge/majsoul/mod.rs 的模块注释中,也体现在 LiqiParser 的设计上:Response 包只携带msg_id,必须从「本 flow 专属的pending请求表」中查回对应的方法名与响应类型,因此解析器不能跨 flow 共享。

桥的创建:for_platform工厂

for_platform 根据配置中的Platform枚举创建对应的Box<dyn Bridge>:

pub fn for_platform( platform: crate::config::Platform, flow_log: Option<Arc<FlowLogger>>, session: Option<Arc<Session>>, hooks: BridgeHooks, ) -> Box<dyn Bridge>

三个参数各有分工:

  • flow_log:每个 WS flow 的文本转储(每个解析过的消息一行 JSON),由 proxy 处理器在 WebSocket 升级时创建并传入(参见 src/logger/README.md 与 src/proxy/handler.rs)。
  • session:透传给需要按需打开附加日志文件的桥。例如 Majsoul 桥每个新对局会轮转出一个新的*.mjai.jsonl文件(见 rotate_mjai_log)。
  • hooks:autoplay 层的共享槽位(下节详述)。

工厂内部为每个平台组装并「builder 式」注入对应槽位:Majsoul 桥挂上time_budget与input_watch,天鳳桥挂上tenhou_state与notify,Riichi City 桥挂上riichi_inject。

调用方有两处,分别对应两种捕获路径:

  • Chromium/CDP 捕获路径(src/capture/chromium/mod.rs):由于有Page句柄,可以驱动浏览器页面点击,因此会注入time_budget、input_watch、tenhou_state等「基于页面」的槽位。
  • MITM 代理路径(src/proxy/handler.rs):没有Page句柄,只能注入 Riichi City 的帧注入门riichi_inject与notify通知通道。

BridgeHooks:autoplay 层与桥共享事实的槽位

autoplay(自动打牌)层需要一些「只有协议解析器才看得到」的事实,于是设计了BridgeHooks这个共享槽位结构(src/bridge/mod.rs)。其设计要点是:每个字段都是可选的、平台特定的;打包成一个结构体后,为某平台新增槽位就不必改动中间所有构造函数参数列表。

字段平台携带内容
time_budgetMajsoul服务器每个决策窗口的时间授权(OptionalOperationList.time_fixed/time_add,毫秒),供 autoplay 规划用(见src/autoplay/budget.rs)
input_watchMajsoul客户端自身上行输入命令的计数器,用于区分「一次真实点击」与「被 UI 吞掉的点击」(见src/autoplay/verify.rs)
tenhou_stateTenhou天鳳牌索引解析后的手牌 + 当前决策窗口——「客户端帧可编码」所需的事实(见src/autoplay/tenhou_state.rs)
riichi_injectRiichi City帧注入门,由桥在cmd_enter_room与cmd_room_end之间维护(见src/autoplay/inject.rs)
notify各平台前端 toast 通道,用于传输「mjai 事件无法承载、但必须告知用户」的状态(如天鳳局中断线重连会暂停分析),两条捕获路径都会接上

以 Majsoul 的time_budget为例:桥在解析到包含OptionalOperationList的帧时,把time_fixed/time_add写入共享槽位;autoplay 据此知道本次决策窗口还剩多少时间。再如input_watch:桥在dispatch中监听.lq.FastTest.inputOperation与.lq.FastTest.inputChiPengGang两个上行方法,只有「客户端确实接受了输入」才会发出它们,因此这些帧就是 autoplay 点击落地的证明(见 src/bridge/majsoul/mod.rs)。

三座平台桥的实现剖析

Majsoul(雀魂):protobuf + 位置相关 XOR 的五层线格式

Majsoul 桥由parser.rs(线格式解码)与mod.rs(mjai 状态机)两部分组成,线格式自述为 5 层(src/bridge/majsoul/parser.rs):

[type byte][msg_id u16 LE?][Wrapper protobuf] └─ name + data(bytes) └─ inner DynamicMessage └─ {name, data: base64(XOR(protobuf))} // action only
  • 类型字节:01=Notify(通知,无msg_id)、02=Request(请求)、03=Response(响应)。Request/Response 在偏移 1..3 携带小端 u16 的msg_id。
  • 方法名路由:依赖内嵌的 liqi.json(编译期include_str!注入)——一个以全限定方法名为键的扁平 rpc-map:
    • 两段名(lq.NotifyX)→ 直接在描述符池中查lq.NotifyX;
    • 三段名(lq.Service.method)→ 查liqi.json[".lq.Service.method"]得到{req, resp}全限定类型名,再在池中解析。测试 all_routes_resolve 断言了池中 300+ 条路由的 req/resp 类型全部可解析,防止 proto 重新生成后与 liqi.json 失配。
  • Action 数据 XOR:某些内层消息携带{name, data},其中data是对「名为name的 action」的 protobuf 字节做 base64 编码后的 XOR 密文。解码函数 wtf_decode 实现了一个位置相关的 XOR 方案:9 字节密钥表[0x84, 0x5E, 0x4E, 0x42, 0x39, 0xA2, 0x1F, 0x60, 0x1C],基数base = 23 ^ data.len(),逐字节异或base + 5*i + k。

一个重要的对称性陷阱:重连重放(GameRestore)里的 action 是不做 XOR 的。decode_restore_action(src/bridge/majsoul/parser.rs)直接 base64→protobuf 解码,若在此路径上误跑 XOR 反而会损坏字节。为此测试 gameplay_field_numbers_stable 锁定了lq.ActionPrototype等关键消息的字段编号,避免字段漂移在运行时才暴露。

状态机侧(src/bridge/majsoul/mod.rs)值得关注的状态与动作:

  • 座位解析:account_id从上行authGame请求捕获,seat在对应响应中通过seat_list索引解析;座位确定后桥发出start_game且id = seat,下游 bot 据此知道自己是哪家。
  • 立直声明处理:pending_reach_accepted记录刚通过ActionDiscardTile.is_liqi声明立直的座位,按 mjai 状态机要求,宣言牌必须先透传、随后才扣 1000 点;若下一动作是ActionHule(宣言牌被荣和),立直作废,不发出reach_accepted。
  • 杠宝牌时机(DoraTiming):mjai 区分暗杠(即乗り——摸岭上牌前翻宝牌)与加杠/大明杠(後乗り——摸岭上牌后、下次打牌前翻宝牌)。线上服务器与此吻合:暗杠的新宝牌指示随摸岭上的ActionDealTile.doras到达,而加杠/大明杠的指示被扣留到杠家下次打牌,随ActionDiscardTile增长后的doras数组一起到达。桥用PendingBeforeRinshan/PendingAfterRinshan两个时机状态调度这些指示,使dora事件在正确的节点发出。
  • 荣和目标追踪:HuleInfo本身不携带荣和目标座位,桥必须自行跟踪「最近一次翻牌揭示者的座位」(last_revealed_tile_actor),否则对杠/拔北的荣和会记错对象。该字段由ActionDiscardTile(普通荣和)、ActionAnGangAddGang(kakan)(搶槓)、ActionAnGangAddGang(ankan)(国士无双抢暗杠,雀魂特有规则)更新。
  • 对局日志:桥在每次发出start_game时于<session>/majsoul/majsoul_<ts>.mjai.jsonl轮转新文件,之后每个 mjai 事件追加为一行 JSON。

Tenhou(天鳳):纯 JSON tag 驱动的观察型桥

天鳳桥的线格式是纯 JSON:一个 WS 帧就是一个事件,由tag字段分发,心跳是字面量字节<Z/>。完整的 tag 清单与字段语义来自原 Akagi Python 天鳳桥的忠实 Rust 移植(src/bridge/tenhou/README.md):

Tag触发mjai 输出
<Z/>心跳(无)
HELO/REJOIN/SAIKAI/BYE/SHUFFLE会话控制(无)
GO规则/房间(局前)(无;暂存供MatchInfo,bit0x10= 三麻)
UN名单(无;start_game消费,空槽 = 三麻)
TAIKYOKU对局开始start_game(从oya解析自家座位)
INIT局开始start_kyoku(0 分槽检测三麻)
REINIT重连:进行中手牌的快照(无——见下文「局中重连」)
T<n>/U<n>/V<n>/W<n>摸牌(相对座位 0..3)tsumo
D<n>等(大写)手切dahai { tsumogiri: false }
d<n>等(小写)摸切dahai { tsumogiri: true }
N(带m)副露chi/pon/daiminkan/kakan/ankan/kita
REACH step=1/2立直声明 / 成立reach/reach_accepted
DORA新宝牌指示dora
AGARI和牌hora+end_kyoku(带owari时 +end_game)
RYUUKYOKU流局ryukyoku+end_kyoku(带owari时 +end_game)

几个关键的编码规则:

  • 牌编码:天鳳牌是整数索引0..=135。index / 4给出牌型(0..=33),index % 4是变体。赤五精确位于16、52、88(序列化为5mr、5pr、5sr),见 tile.rs。
  • 座位编码:天鳳消息永远使用相对座位(rel 0 是观察者),State::rel_to_abs/abs_to_rel换算到 mjai 的绝对框架。桥从<TAIKYOKU oya="N"/>解析自家绝对座位:seat = (4 - N) % 4。三麻时线上的绝对座保持 4 个位置(缺位者是线位 3 的幽灵),seed[0]跳过其发牌。
  • 副露位域:<N m="..."/>把副露种类、目标座位、牌构成打包进一个整数。位解码在 meld.rs,严格遵循十万一的mentsu136.txt规范。北抜き((m & 0x3F) == 0x20)是特例,先于结构化解码器处理。
  • t属性(决策窗口):摸牌与打牌帧可能携带t位掩码,标明服务器正在向我们提供的选项,其角色与 Majsoul 的OptionalOperationList相同,桥把它记录在State::window供autoplay::tenhou_state使用。位含义取决于携带它的帧,两集合不重叠:
bit摸牌帧(T<n>)打牌帧(D/E/F/G<n>)
1—pon
2—daiminkan
4—chi
8—ron
16tsumo agari—
32riichi—
64九種九牌—

暗杠与加杠不在掩码中——客户端从手牌自行推导,Akagi 也通过立直引擎的合法动作推导。虽然只有 autoplay 消费该窗口,解析仍然是无条件的:成本低,且「陈旧的窗口比没有窗口更糟」。

反向编码(encode.rs)是此平台Bridge::build的唯一实现,产出 mjai → 客户端帧的映射(src/bridge/tenhou/encode.rs):

mjai帧
dahai{"tag":"D","p":<index>}
reach{"tag":"REACH"}
chi{"tag":"N","type":3,"hai0":<index>,"hai1":<index>}
pon{"tag":"N","type":1,"hai0":<index>,"hai1":<index>}
daiminkan{"tag":"N","type":2}
ankan{"tag":"N","type":4,"hai":<type * 4>}
kakan{"tag":"N","type":5,"hai":<index>}
hora(自摸){"tag":"N","type":7}
hora(荣和){"tag":"N","type":6}
ryukyoku{"tag":"N","type":9}
none{"tag":"N"}

值得注意的是,autoplay 并不使用build的输出:天鳳客户端拥有自己的盘面状态,若打牌帧绕过客户端处理器直达服务器,客户端会冻结,因此autoplay::tenhou改为驱动客户端的输入路径,只从这个模块借用牌索引查询。会话控制帧(JOIN/GOK/NEXTREADY)被刻意不编码——Akagi 观察的是真实客户端,那些帧由客户端自己发送。

局中重连(REINIT)是天鳳桥最微妙的部分。当 Web 客户端重连对局时会开新 socket,桥看到的序列是:

↑ HELO ↓ GO (type, lobby) ↓ UN (full roster) ↑ GOK ↓ SAIKAI (ba, oya, sc) ↓ REINIT … then live play

——没有TAIKYOKU。<REINIT/>充当<INIT/>的替代:携带seed/ten/oya/hai,外加每个有副露座位的m<rel>(逗号分隔的<N m=…/>位域)与kawa<rel>(牌索引,255标记立直宣言——下一项即立直牌)。它是快照而非日志——没有任何信息说明副露相对打牌的先后——因此无法重建进行中手牌的 mjai 事件流(相比之下,Majsoul 的GameRestore是有序 action 日志,所以 Majsoul 桥可以重放而天鳳桥不能)。

桥对此的处理策略(src/bridge/tenhou/README.md):

  1. 从帧中解析座位与人数,保证本 flow 后续归属正确;
  2. 只重建本 flow 自身需要的东西——自家手牌(hai)、自家副露(m0)、立直(kawa0中的255)、是否手上有摸牌(牌数)——让 autoplay 与Bridge::build在本局剩余部分继续工作;
  3. 不发出任何事件,并置State::suspended:所有 handler 照常运行(手牌与窗口继续跟踪),但dispatch丢弃其事件直到下一个<INIT/>——否则会把实时对局套在「几巡之前」的快照上;
  4. 通过BridgeHooks::notify发出警告 toast,并在下一个<INIT/>补发start_game,以正确的座位与人数为 tracker、bot 与历史记录重新打开对局。

重连期间唯一仍被放行的事件是对局结束,此时end_game以terminated_game(GameEndReason::Terminated)发出:重连前的各手牌从未被本 flow 看到,历史记录会丢弃该对局,而非把它归档为数据残缺的完整对局。

Riichi City(麻雀一番街):15 字节二进制头 + JSON 的观察型桥

Riichi City 桥只通过 MITM 代理可达(原生客户端、无 Web 版,Chromium/CDP 捕获路径不适用),且默认只读(observe-only),不过其build实现了上行注入能力(见下文)。

线格式WPacket为 15 字节大端头 + 可选 JSON 体(src/bridge/riichi_city/packet.rs):

[0..4] packet_size u32 total bytes including header [4..6] header_size u16 always 15 ─┐ together the magic [6..8] version u16 always 1 ─┘ bytes 00 0f 00 01 [8..12] message_index u32 request/response correlation counter [12..14] cmd u16 binary command enum (CMDAuth = 1) [14] has_body u8 0 / 1 [15..] json_payload UTF-8 JSON (present when packet_size > 15)

命令采用两级分发:二进制cmd只在CMDAuth(1)时有用——其 JSON 体携带玩家uid(客户端→服务器);所有对局消息则在 JSON 体内携带字符串"cmd"字段(cmd_enter_room、cmd_game_start、cmd_game_action_brc…),分发以该字符串为键。两个方向都会解析(uid包是上行,对局广播是下行;客户端请求帧不匹配任何服务器cmd_*,直接忽略)。

cmd→ mjai 的映射表(src/bridge/riichi_city/README.md):

Riichi Citycmd(+ action code)mjai 事件
cmd_enter_room—(记录玩家、桌大小)
cmd_game_startstart_game(首局)→start_kyoku→ 开局tsumo
cmd_in_card_brctsumo(他家摸牌,隐藏?)
cmd_send_current_actiontsumo(自家摸牌,揭示)
cmd_game_action_brcaction 2/3/4chi
… action 5pon
… action 6daiminkan
… action 8ankan(5 的第一槽位为红)
… action 9kakan
… action 11reach? +dahai(+ 延后的reach_accepted、杠宝牌 flush)
… action 13kita(北抜き)
… action 7 / 10 / 12—(在此标记结束;由cmd_game_end结算)
cmd_gang_bao_brc—(延后一个杠宝牌指示)
cmd_game_endhora(每名和牌者)或ryukyoku,然后end_kyoku
cmd_room_endend_game

结算逻辑有几个易错的细节(src/bridge/riichi_city/mod.rs):

  • 和牌 vs 流局以all_point判定,而非win_info是否存在:win_info条目只有在all_point > 0时才是赢家;荒牌流局时win_info列出的是听牌玩家(all_point == 0,手牌揭示),必须读作流局。
  • deltas语义:每位玩家的point_profit(+extra_profit)——包含本场棒与已收的供托,但排除玩家自己的立直棒。立直的 −1000 由 mjai 消费者在reach_accepted时应用(与天鳳桥的拆分方式相同),若用user_point差分会重复计数。因此和牌 delta 之和 = +已收供托,流局 delta 之和为 0。
  • 荣和目标:win_info[].user_id是赢家,荣和目标是最新打出者(last_dahai_actor),自摸目标即赢家本人;里宝牌 =win_info[].li_bao_card。每名赢家各发一个hora(两荣/三荣会得到多个)。
  • 三麻:与 v2(用幽灵座填充到 4)不同,本桥发出原生长度 3的scores/tehais,actor 索引保持在0..=2,与天鳳桥及 V3 下游三麻管线一致。

添加一个新平台:从枚举到工厂的完整步骤

原文档给出了四步流程(src/bridge/README.md),结合源码可以展开为可操作的清单:

  1. 给config::Platform添加一个变体(src/config/platform.rs)。注意该枚举还实现了subdir()(用作日志会话下的子目录名)和From<Platform> for schema::Platform(映射到历史记录标签),新增平台时这两处也要同步补齐。
  2. 创建src/bridge/<name>/mod.rs,定义实现Bridge的结构体。结构体通常持有:一个解析器(可能带请求-响应关联表)、一个 flow 级状态机(座位、手牌、dora 等)、可选的mjai_log与shared槽位。
  3. 在 src/bridge/mod.rs 重新导出,并在for_platform添加 match 分支,用 builder 风格把需要的BridgeHooks字段注入实例。
  4. 如果平台要支持 autoplay 且其规划器需要「只有解析器看得到」的信息,就给BridgeHooks加字段而不是加参数——这正是该结构体存在的理由。

此外,参考三个既有桥的实践,还应补上:单元测试(Majsoul 侧锁定了描述符池加载、rpc-map 路由、全部路由可解析、关键字段编号;Riichi City 侧用feed(..)辅助函数配假id 喂入真实帧形状)以及平台自身的 README(天鳳桥与 Riichi City 桥各有一份独立 README 详述 tag 表、编码规则与重连语义)。

小结

Akagi 的 bridge 模块以 15+1 种MjaiEvent为统一契约,通过仅含parse/build两个方法的Bridgetrait,把三种线格式截然不同的平台(protobuf+XOR 的雀魂、纯 JSON 的天鳳、15 字节二进制头+JSON 的麻雀一番街)收敛为同一条事件流;for_platform工厂与BridgeHooks共享槽位则保证了「一个 flow 一个桥」的状态隔离与 autoplay 层的事实注入。无论是阅读对局分析逻辑、接入新平台,还是排查自动打牌的行为,都可以从 src/bridge/README.md 出发,沿 src/bridge/mod.rs → 各平台子目录(majsoul、tenhou、riichi_city)→ src/schema/mjai/mod.rs 这条链路逐层深入。

  • 桌面应用
  • 人工智能

【免费下载链接】Akagi

支持雀魂、天鳳、麻雀一番街、天月麻將,能夠使用自定義的AI模型實時分析對局並給出建議,內建Mortal AI作為示例。 Supports Majsoul, Tenhou, Riichi City, Amatsuki, with the ability to use custom AI models to analyze games in real time and provide suggestions. Comes with Mortal AI as a built-in example.

项目地址:https://gitcode.com/gh_mirrors/ak/Akagi
点击查看免费下载

相关推荐

上一篇:魔兽争霸III终极增强插件:让经典游戏在现代电脑上重生
下一篇:掌握游戏修改新境界:Wand-Enhancer免费解锁WeMod高级功能指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询