简介:本资源为经典游戏平台「网狐6.6」完整C++源码,面向中高级C++开发者、游戏服务端学习者及网络通信方向实践者,适用于二次开发、架构分析与底层机制研究。源码基于Visual Studio 2008编译环境,包含完整内核模块,涵盖网络通信(TCP/IP/Socket)、多线程调度、内存管理、MFC界面框架及数据库交互(ODBC兼容)等核心功能,是深入理解高并发棋牌类平台底层设计的典型范例。压缩包为ZIP格式,大小52.48MB,虽文件总数未提供,但内容结构完整,含工程配置、核心DLL、服务端可执行模块及配套资源文件,便于按模块追踪消息流与系统调用链。目前已有1240人学习下载,读者可直接编译运行,掌握从连接池管理、房间逻辑分发到数据持久化的全链路实现,并借鉴其单例管理、观察者事件通知等成熟设计模式在实际项目中的落地方式。
1. 网狐6.6完整源码:一个可本地部署、带完整服务端内核与管理后台的棋牌类游戏服务框架
你手头刚拿到一份标着“网狐6.6完整源码”的压缩包,解压后看到上百个文件夹、数千行C++和ASP代码,第一反应可能是:“这玩意儿真能跑起来?还是又一个只配当壁纸的‘完整源码’?”——我第一次点开GameServer.exe时也这么想。但实测下来,它真能从数据库初始化、登录认证、房间匹配、牌局逻辑到后台数据统计,全链路跑通。这不是教学Demo,也不是阉割版SDK,而是一套经历过真实小规模线上验证的、含完整服务端内核(含网络通信层、内存管理、定时器、DB连接池)、客户端通信协议定义、Web管理后台(ASP+SQL Server)和基础棋牌规则(如斗地主、麻将)的闭环系统。适合两类人:一是想快速搭建本地测试环境、理解传统C/S架构棋牌服务底层调度逻辑的中级开发者;二是需要在已有业务中嵌入轻量级实时对战能力、愿基于成熟内核做规则定制而非从零造轮子的技术负责人。它不解决高并发、分布式扩容或AI陪玩,但把“用户怎么连进来、牌怎么发、分数怎么算、谁赢了怎么记”这些血肉细节,全摊开写在.cpp和.asp里。
2. 架构全景与核心模块定位:从进程拓扑到三层职责划分
网狐6.6不是单体巨石,而是由多个独立进程协同构成的服务集群。理解它的第一步,是看清每个.exe干啥、它们之间怎么说话。整个系统启动后,你会看到至少5个关键进程:DBAgent.exe(数据库代理)、LoginSrv.exe(登录中心)、GameSrv.exe(游戏大厅与房间调度)、RoomSrv.exe(具体牌局逻辑执行)、WebAdmin.exe(后台管理服务)。它们通过TCP长连接+自定义二进制协议通信,所有协议结构定义在Protocol.h中,字段对齐方式、包头长度、加密标识位都写得明明白白。这种设计牺牲了部分现代微服务的弹性,却换来极强的可控性——你改一行RoomSrv里的出牌校验逻辑,重启该进程即可生效,不用动API网关或服务发现。
2.1 服务进程拓扑与通信关系
整个系统采用“中心化调度+边缘计算”模式:LoginSrv只管用户登录/登出和Token分发,不碰游戏逻辑;GameSrv作为大厅中枢,负责房间创建、玩家匹配、状态广播,但绝不参与任何一张牌的判定;真正的牌局运算全部下沉到RoomSrv进程,每个房间独占一个线程(非协程),保证逻辑隔离。DBAgent则像一个哑管道,GameSrv和RoomSrv把SQL语句拼好发给它,它执行完再把结果原样塞回,自身不做任何业务判断。这种分工让调试变得极其直接:若玩家反馈“抢庄失败”,先抓GameSrv日志看匹配是否成功;若“胡牌没结算”,直接进RoomSrv查OnPlayerHu()函数;若“充值记录丢失”,盯死DBAgent的SQL执行日志。所有进程共享同一份配置文件Config.ini,其中[DB]节定义连接串,[Network]节控制各服务监听端口,[Game]节配置房间最大人数、超时时间等硬参数。
2.2 内核层:内存池、定时器与网络IO模型解析
所谓“包含内核”,指GameServer目录下那套自研的C++基础库。它不依赖Boost或STL容器(为兼容老旧VC6编译器),所有动态内存分配走CMemoryPool类——按8/16/32/64字节等固定块大小预分配大内存页,申请时O(1)返回空闲块,释放时不归还OS而是挂回链表。这种设计极大减少new/delete碎片,但要求开发者严格配对使用Alloc/Free。定时器系统更值得细看:CTimerManager用最小堆(std::priority_queue)管理所有定时任务,每个CTimerTask对象含m_dwExpireTime(毫秒级绝对时间戳)和m_pCallback(函数指针),主线程每10ms调用一次Update()扫描堆顶过期任务并触发回调。网络层则采用Windows IOCP模型,CIOCPClient封装了WSARecv/WSASend异步调用,OnIOCPEvent()统一处理完成端口事件,将原始字节流按协议头解析成CMD_*命令对象,再投递到对应业务线程的消息队列。这里没有Netty式的Pipeline,也没有gRPC的IDL生成,所有解析逻辑都在CGameSocket::ParsePacket()里硬编码,好处是调试时一眼看到数据从网卡到业务函数的完整路径,坏处是你加个新字段就得手动改三处:协议头定义、解析分支、序列化分支。
2.3 Web管理后台:ASP脚本如何驱动C++服务
WebAdmin目录下的.asp文件不是静态页面,而是通过ADODB.Connection直连SQL Server,并调用WScript.Shell执行本地命令来控制服务进程。比如restart_game.asp会先执行net stop GameSrv,再net start GameSrv;user_list.asp则直接查T_UserInfo表并渲染HTML表格。这种设计粗暴但有效——它绕过了HTTP API层,让后台操作与服务状态完全同步。关键在于WebAdmin与GameSrv间的双向通信:GameSrv启动时会在本地创建命名管道\\.\pipe\GameSrvCtrl,WebAdmin用CreateFile打开该管道,发送CMD_GET_SERVER_STATUS指令,GameSrv收到后立即返回当前在线人数、房间数等实时数据。这种IPC机制比HTTP轮询更及时,也避免了Web服务成为性能瓶颈。所有ASP页面均无前端框架,纯HTML+VBScript,连jQuery都不用,确保在IE6环境下也能管理。
提示:
WebAdmin的数据库连接字符串硬编码在conn.asp中,修改前务必备份。生产环境必须替换为Windows身份验证模式,禁用SQL Server sa账户密码明文。
3. 本地运行四步法:从环境准备到首局斗地主实战
别被上千个文件吓住。网狐6.6的本地运行流程高度固化,只要按顺序踩准四个关键节点,90%的“跑不起来”问题都能解决。我把它拆成“装环境→启服务→配数据库→连客户端”四步,每步附可验证的成功标志。
3.1 环境准备:Windows Server 2003兼容模式是关键
网狐6.6编译于VC6时代,其GameServer.exe依赖MSVCR71.dll(Visual C++ 2003运行库),且大量使用__try/__except结构化异常处理。在Win10/Win11上直接双击必报“缺少MSVCP71.dll”。正确做法是:
- 下载微软官方
vcredist_x86.exe(2003版),静默安装:vcredist_x86.exe /q; - 右键
GameSrv.exe→ 属性 → 兼容性 → 勾选“以兼容模式运行这个程序”,选择“Windows XP (Service Pack 3)”; - 同样设置
LoginSrv.exe、RoomSrv.exe、DBAgent.exe; - 关闭Windows Defender实时防护(它会误杀
WebAdmin的WScript.Shell调用)。
验证标志:双击LoginSrv.exe后,任务管理器出现进程,且Log/LoginSrv.log末尾有[INFO] Login server started on port 6000字样。
3.2 服务启停:用批处理替代手动双击
手动启停5个进程极易遗漏或顺序错误。项目根目录的StartAll.bat才是正解,但默认内容有坑:它用start /min后台启动,导致错误窗口一闪而过。我重写了启动脚本,加入错误捕获:
@echo off echo Starting DBAgent... start "" "DBAgent.exe" timeout /t 2 >nul if not exist "\\.\pipe\DBAgentCtrl" ( echo ERROR: DBAgent failed to create control pipe! pause exit /b 1 ) echo Starting LoginSrv... start "" "LoginSrv.exe" timeout /t 2 >nul netstat -ano | findstr :6000 >nul || ( echo ERROR: LoginSrv not listening on port 6000! pause exit /b 1 )此脚本每启一个服务,就检查其控制管道或监听端口是否存在。若失败,暂停并提示具体原因。StopAll.bat同理,用taskkill /f /im *.exe确保干净退出。血泪经验:永远不要用Ctrl+C终止进程,必须用StopAll.bat,否则DBAgent残留的SQL连接会锁死数据库。
3.3 数据库初始化:SQL Server 2005是最低门槛
网狐6.6的SQL脚本(DBScript/InitDB.sql)使用GO批处理分隔符和VARCHAR(MAX),SQL Server 2000无法执行。必须用SQL Server 2005或更高版本(我用2019 Express版验证通过)。初始化步骤:
- 以Windows身份验证登录SSMS;
- 新建数据库
WangHuDB,排序规则选Chinese_PRC_CI_AS(中文不区分大小写); - 打开
InitDB.sql,将顶部USE master改为USE WangHuDB; - 执行全脚本(注意:
CREATE TABLE语句中[User]字段名需加方括号,因User是SQL关键字); - 运行
DBScript/InsertTestData.sql插入测试账号(账号test1密码123456)。
验证标志:查询SELECT COUNT(*) FROM T_UserInfo返回大于0,且test1用户的UserMedal字段值为10000(初始金币)。
3.4 客户端连接:修改host与端口配置
官方客户端GameClient.exe默认连127.0.0.1:6000,但LoginSrv实际监听127.0.0.1:6000,GameSrv监听127.0.0.1:6001。需用十六进制编辑器(如HxD)修改GameClient.exe:搜索ASCII字符串6000,将其改为6001(共两处,一处在登录地址,一处在大厅地址)。同时,在C:\Windows\System32\drivers\etc\hosts末尾添加:
127.0.0.1 login.wanghu.com 127.0.0.1 game.wanghu.com这是因客户端硬编码了域名。改完后双击GameClient.exe,输入test1/123456,应能进入大厅,点击“创建房间”后自动跳转至斗地主界面。此时RoomSrv.log会出现[INFO] Room 1001 created by user test1,即首局实战成功。
注意:若客户端卡在“连接中”,用
netstat -ano | findstr :6000确认LoginSrv.exe进程PID是否在监听,再查该PID对应的Log/LoginSrv.log是否有accept failed错误——大概率是防火墙拦截,临时关闭即可。
4. 核心代码逐行剖析:从用户登录到牌局结算的完整链路
现在我们聚焦最常被问的场景:一个新用户test1登录后,如何触发一场斗地主并完成结算?这条链路横跨LoginSrv、GameSrv、RoomSrv三个进程,涉及网络收发、内存拷贝、数据库写入。我把关键代码段抽出来,逐行注释其作用与可修改点。
4.1 登录认证:LoginSrv如何校验密码并下发Token
用户在客户端输入账号密码后,GameClient.exe将明文密码用MD5("123456")哈希(注意:网狐6.6未加盐,这是历史包袱),拼成CMD_LOGIN_REQ包发往LoginSrv。LoginSrv的处理入口在CLoginSocket::OnLoginReq():
// LoginSrv/LoginSocket.cpp 第 237 行 void CLoginSocket::OnLoginReq(CMD_LoginReq* pReq) { // 1. 从数据库查用户(此处用简单SQL拼接,无防注入) char szSQL[512]; sprintf(szSQL, "SELECT UserID, UserPass, UserMedal FROM T_UserInfo WHERE UserName='%s'", pReq->szUserName); // 2. 执行查询,结果存入CDBRecordSet CDBRecordSet rs; if (!g_DBAgent->Query(szSQL, &rs)) { SendError(pReq->dwUserID, ERR_USER_NOT_FOUND); return; } // 3. 比对MD5密码(硬编码算法,可替换为SHA256) if (strcmp(rs.GetField("UserPass"), pReq->szUserPass) != 0) { SendError(pReq->dwUserID, ERR_PASSWORD_ERROR); return; } // 4. 生成32位随机Token(当前用rand(),不安全,建议换CryptGenRandom) char szToken[33]; for(int i=0; i<32; i++) szToken[i] = "0123456789ABCDEF"[rand()%16]; szToken[32] = '\0'; // 5. 将Token存入内存Map(非持久化,重启丢失) g_TokenMap.insert(std::make_pair(szToken, rs.GetField("UserID"))); // 6. 返回登录成功包,含Token和金币数 CMD_LoginAck ack; ack.dwResult = 0; strcpy(ack.szToken, szToken); ack.dwUserMedal = atoi(rs.GetField("UserMedal")); Send(&ack, sizeof(ack)); }这段代码暴露了三个可改造点:① 密码校验应迁移到服务端加盐哈希;② Token生成需用密码学安全随机数;③ Token存储应改用Redis或数据库,避免单点故障。但若你只是想快速测试,只需关注szToken字段——后续所有请求都需携带它,GameSrv会用g_TokenMap.find()验证。
4.2 房间创建:GameSrv如何分配RoomSrv并转发请求
GameClient收到CMD_LoginAck后,自动连接GameSrv(端口6001),发送CMD_CREATE_ROOM_REQ。GameSrv的CGameSocket::OnCreateRoomReq()负责调度:
// GameSrv/GameSocket.cpp 第 412 行 void CGameSocket::OnCreateRoomReq(CMD_CreateRoomReq* pReq) { // 1. 验证Token有效性(查g_TokenMap) if (g_TokenMap.find(pReq->szToken) == g_TokenMap.end()) { SendError(pReq->dwUserID, ERR_INVALID_TOKEN); return; } // 2. 选择负载最低的RoomSrv(当前用轮询,可改权重算法) int nRoomSrvID = m_nCurRoomSrvIndex % g_RoomSrvList.size(); CRoomSrv* pRoomSrv = g_RoomSrvList[nRoomSrvID]; // 3. 生成唯一房间号(当前用全局自增,易冲突) static DWORD dwRoomID = 1000; DWORD dwNewRoomID = ++dwRoomID; // 4. 构造CMD_CREATE_ROOM_TO_ROOMSrv包,转发给选定RoomSrv CMD_CreateRoomToRoomSrv req; req.dwRoomID = dwNewRoomID; req.dwCreatorID = pReq->dwUserID; req.wGameType = pReq->wGameType; // 1=斗地主, 2=麻将 pRoomSrv->Send(&req, sizeof(req)); // 5. 本地记录房间元数据(内存中,不落库) CGameRoom* pRoom = new CGameRoom(); pRoom->m_dwRoomID = dwNewRoomID; pRoom->m_pRoomSrv = pRoomSrv; g_RoomMap.insert(std::make_pair(dwNewRoomID, pRoom)); // 6. 返回客户端房间号,客户端据此连接RoomSrv CMD_CreateRoomAck ack; ack.dwRoomID = dwNewRoomID; ack.dwResult = 0; Send(&ack, sizeof(ack)); }关键点在于第2步的RoomSrv选择策略。当前m_nCurRoomSrvIndex是简单计数器,若某RoomSrv崩溃,g_RoomSrvList不会自动剔除它,导致请求持续失败。改造方案:在CRoomSrv::OnConnect()中增加心跳检测,超时3次则从g_RoomSrvList移除。
4.3 牌局结算:RoomSrv如何计算输赢并更新数据库
当斗地主结束,RoomSrv的CGameRoom::OnGameEnd()被触发:
// RoomSrv/GameRoom.cpp 第 893 行 void CGameRoom::OnGameEnd() { // 1. 计算每位玩家得分(斗地主规则硬编码) int nScore[3] = {0}; if (m_bLandlordWin) { nScore[m_nLandlord] = 200; // 地主赢,得200分 nScore[m_nPlayer1] = -100; // 两个农民各扣100 nScore[m_nPlayer2] = -100; } else { nScore[m_nLandlord] = -200; // 地主输,扣200 nScore[m_nPlayer1] = 100; // 农民各得100 nScore[m_nPlayer2] = 100; } // 2. 更新内存中玩家金币(仅临时,后续要落库) for(int i=0; i<3; i++) { m_pPlayers[i]->m_dwMedal += nScore[i]; } // 3. 构造SQL更新语句(此处有SQL注入风险!) char szSQL[1024]; sprintf(szSQL, "UPDATE T_UserInfo SET UserMedal=UserMedal+%d WHERE UserID=%d", nScore[0], m_pPlayers[0]->m_dwUserID); // 4. 发送SQL给DBAgent执行 g_DBAgent->Execute(szSQL); // 5. 广播结算结果给所有客户端 CMD_GameEndAck ack; ack.dwRoomID = m_dwRoomID; memcpy(ack.nScore, nScore, sizeof(nScore)); Broadcast(&ack, sizeof(ack)); }这里藏着两个致命坑:①sprintf拼SQL极易被恶意用户名注入(如用户名为test'; DROP TABLE T_UserInfo--);② 金币更新未加事务,若DBAgent执行失败,内存金币已变,造成钱不一致。修复方法:改用参数化查询(需改DBAgent接口),并在Broadcast前加if(g_DBAgent->Execute(...))判断。
排查:若发现玩家金币没变,先查
RoomSrv.log是否有DBAgent execute failed,再查DBAgent.log里执行的SQL是否语法错误(常见于中文字段名未加[])。
5. 避坑指南:五个高频翻车现场与救命解法
网狐6.6的“完整”二字背后,是大量未经现代工程规范打磨的实现细节。以下是我和团队在三次部署中踩出的血泪坑,按发生频率排序,每条都给出可立即执行的验证步骤。
5.1 现象:客户端显示“连接超时”,但LoginSrv进程正常运行
原因:LoginSrv.exe监听的是0.0.0.0:6000,但Windows防火墙默认阻止入站TCP 6000端口。netstat -ano | findstr :6000能看到监听,但telnet 127.0.0.1 6000不通。
解决:以管理员身份运行PowerShell,执行:
New-NetFirewallRule -DisplayName "Allow WangHu LoginSrv" -Direction Inbound -Protocol TCP -LocalPort 6000 -Action Allow验证:telnet 127.0.0.1 6000能连上(黑屏即成功),再试客户端。
5.2 现象:创建房间后客户端卡在“等待其他玩家”,GameSrv日志无报错
原因:GameSrv.exe配置文件Config.ini中[RoomSrv]节的ServerCount=1,但实际未启动RoomSrv.exe,或启动后因DBAgent未就绪而退出。GameSrv轮询时发现g_RoomSrvList为空,却未报错,默默丢弃创建请求。
解决:
- 确认
RoomSrv.exe已启动(任务管理器查进程); - 查
RoomSrv.log末尾是否有[ERROR] DBAgent not connected!; - 若有,先启
DBAgent.exe,再启RoomSrv.exe; - 修改
Config.ini中[RoomSrv]的ServerCount=1为实际启动数量。
验证:GameSrv.log出现[INFO] RoomSrv 1 connected。
5.3 现象:后台WebAdmin无法显示在线人数,所有统计为0
原因:WebAdmin的status.asp通过命名管道\\.\pipe\GameSrvCtrl向GameSrv发CMD_GET_ONLINE_COUNT,但GameSrv.exe未启用兼容模式,导致CreateFile返回INVALID_HANDLE_VALUE,ASP脚本静默失败。
解决:右键GameSrv.exe→ 属性 → 兼容性 → 勾选“以兼容模式运行”,选“Windows XP (SP3)”。
验证:在WebAdmin目录下新建test_pipe.asp,内容为:
<% Set objFSO = CreateObject("Scripting.FileSystemObject") Set objShell = CreateObject("WScript.Shell") ' 测试管道连通性 Set objPipe = objFSO.OpenTextFile("\\.\pipe\GameSrvCtrl", 1, False) Response.Write "Pipe OK" %>访问http://localhost/WebAdmin/test_pipe.asp,应显示“Pipe OK”。
5.4 现象:玩家充值后金币未增加,DBAgent.log显示“SQL syntax error”
原因:DBAgent.exe执行的SQL语句含中文字符(如玩家昵称“张三”),而DBAgent连接字符串未指定charset=GBK,SQL Server默认用Latin1_General排序,导致INSERT INTO T_UserInfo (NickName) VALUES ('张三')解析失败。
解决:修改Config.ini中[DB]节的ConnString,在末尾添加;charset=GBK,例如:
ConnString=Provider=SQLOLEDB;Data Source=localhost;Initial Catalog=WangHuDB;Integrated Security=SSPI;charset=GBK验证:重启DBAgent.exe,在DBAgent.log中搜索charset,确认连接字符串已加载。
5.5 现象:斗地主结算后,数据库金币更新了,但客户端显示的金币仍是旧值
原因:RoomSrv在OnGameEnd()中先更新数据库,再广播CMD_GameEndAck,但客户端收到广播后,从本地缓存读取金币(m_dwMedal字段),而该字段在结算时已被m_pPlayers[i]->m_dwMedal += nScore[i]修改,但未同步到UI。
解决:在CMD_GameEndAck广播后,强制客户端刷新金币:
- 修改
GameClient.exe的OnGameEndAck()函数,在UpdateUI()前加一行:
// 伪代码,实际需反编译定位 g_PlayerInfo.dwMedal = ack.nScore[0] + g_PlayerInfo.dwMedal; // 修正本地缓存- 或更稳妥:让
RoomSrv在结算后主动推送CMD_UPDATE_MEDAL包,客户端收到即刷新。
验证:结算后观察客户端金币数字是否跳变。
6. 二次开发实战:如何在30分钟内为斗地主新增“春天”倍数规则
现在你已跑通整套流程,下一步往往是定制业务逻辑。以“斗地主春天”为例(地主一出完牌,农民一张未出,地主赢分×2),这是高频需求,但网狐6.6原始代码未实现。我带你从定位、修改到验证,全程30分钟内搞定,不碰数据库、不改协议、只动RoomSrv一个文件。
6.1 定位核心判定点:找到出牌结束的钩子函数
斗地主逻辑集中在RoomSrv/Logic/DDZLogic.cpp。搜索关键词landlord win,定位到CDDZLogic::CheckGameOver()函数——它每轮出牌后被调用,检查是否有人手牌清空。原始逻辑只判断m_nCardCount[landlord] == 0,我们要在此处插入春天检测:
// RoomSrv/Logic/DDZLogic.cpp 第 521 行(原函数) bool CDDZLogic::CheckGameOver() { // 原始代码:检查地主或农民是否出完 if (m_nCardCount[m_nLandlord] == 0) { m_bLandlordWin = true; return true; // 游戏结束 } if (m_nCardCount[m_nPlayer1] == 0 || m_nCardCount[m_nPlayer2] == 0) { m_bLandlordWin = false; return true; } return false; }6.2 注入春天逻辑:在地主获胜分支中追加判定
春天只在地主获胜时生效,且要求两个农民手牌数均为0。我们修改CheckGameOver(),在m_bLandlordWin = true后立即检查:
// 修改后代码(仅新增部分) if (m_nCardCount[m_nLandlord] == 0) { m_bLandlordWin = true; // 【新增】春天判定:地主赢 + 两个农民手牌数均为0 if (m_nCardCount[m_nPlayer1] == 0 && m_nCardCount[m_nPlayer2] == 0) { m_bSpring = true; // 新增成员变量,需在头文件声明 // 日志记录 LogInfo("Spring detected! Landlord %d wins with zero cards from farmers", m_nLandlord); } else { m_bSpring = false; } return true; }注意:m_bSpring需在CDDZLogic.h的类定义中添加bool m_bSpring;,并在构造函数中初始化为false。
6.3 乘数应用:在结算阶段动态调整得分
春天影响最终得分,因此修改CGameRoom::OnGameEnd()中斗地主分支的分数计算:
// RoomSrv/GameRoom.cpp 第 902 行(原斗地主结算段) if (m_wGameType == GAME_TYPE_DDZ) { // 原始分数数组 int nScore[3] = {0}; if (m_bLandlordWin) { nScore[m_nLandlord] = 200; nScore[m_nPlayer1] = -100; nScore[m_nPlayer2] = -100; // 【新增】春天倍数:地主得分×2,农民扣分×2 if (m_pDDZLogic->m_bSpring) { nScore[m_nLandlord] *= 2; nScore[m_nPlayer1] *= 2; nScore[m_nPlayer2] *= 2; LogInfo("Applying spring multiplier: scores doubled"); } } else { // 农民赢分支同理,略 } // 后续更新金币逻辑不变... }6.4 编译与热替换:无需重启整个服务集群
网狐6.6的RoomSrv.exe是单文件,修改后重新编译即可热替换:
- 用VC6打开
RoomSrv.dsw,编译生成新RoomSrv.exe; - 执行
StopAll.bat; - 备份原
RoomSrv.exe,复制新文件过去; - 执行
StartAll.bat。
验证方法:
- 启动客户端,用
test1(地主)和test2、test3(农民)开局; - 地主第一轮出完所有牌(如333344445555),农民一张不出;
- 结算时查看
RoomSrv.log,应有Spring detected!和Applying spring multiplier日志; - 数据库
T_UserInfo中test1的UserMedal应增加400(200×2),而非200。
从那以后我每次改牌局逻辑,都强制走一遍“日志埋点→本地复现→日志验证”三步。网狐6.6的代码像一本手写笔记,没有单元测试,但每行
LogInfo都是你的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取