摘要: 熟悉的情怀界面,怎样在保留经典布局的同时拥有更细腻的视觉表现?这次我们完成了四套大厅、两套登录页的视觉设计,并对62.89GB原版资料进行了工程梳理。本文介绍新界面的配色、人物、材质与入口布局,也公开我们识别客户端版本、核对建房流程和检查服务端构建配置的方法。当前成果包括UI设计稿、源码入口清单和经过验证的检查工具;客户端集成、引擎迁移与部署属于后续接入范围。
文章目录
-
- 一、我们完成了哪些看得见的变化
- 二、我们先把原件、勘察副本和报告分开
- 三、我们统一了报告、编码与内容指纹
- 四、我们为归档建立了可重复检查的清单
- 五、我们把九份原始资料逐项归类
- 六、我们处理了归档读取与中文编码问题
- 七、我们定向提取了关键配置和界面脚本
- 八、我们实现了客户端与服务构建检查工具
- 九、旧工程基线与Cocos新版本的适配方向
- 十、我们核对了创建房间的两层入口
- 十一、我们梳理了六位房号之后的进入流程
- 十二、我们整理了亲友圈与登录的状态分支
- 十三、我们核实了服务端的构建方式
- 十四、我们把旧部署记录整理成了环境线索
- 十五、我们验证了检查工具的异常行为
- 十六、我们确认了报告可重复与样本不变
- 十七、检查过程中采用的定位方法
- 十八、这一阶段形成的设计与技术成果
- 十八、这一阶段形成的设计与技术成果
一、我们完成了哪些看得见的变化
这次情怀焕新的出发点,是保留老玩家熟悉的界面关系,同时重新处理画面的质感。我们保留了左侧人物、右侧主要功能入口以及底部常用操作的整体结构,将人物服饰、背景层次、按钮图标和文字重新统一。打开新的大厅设计,依然可以快速辨认创建房间、加入房间、亲友圈与娱乐大厅的位置。
主大厅采用江南滨水场景与城市天际线,前景是木质露台、花枝和器物,中景是水面与桥梁,远处以建筑和天空形成纵深。我们把原先较平的画面处理成有远近层次的环境,让暖色灯光、水面倒影与人物轮廓相互呼应。人物保留旗袍与披肩的典型形象,重点细化了刺绣、丝绸、发丝和首饰的表现。

按钮并没有全部改成同一种颜色。创建房间保留珊瑚暖色,加入房间使用蜜金色,亲友圈以玉石青绿为主,通过不同区域的明度和材质建立功能辨识。娱乐大厅最初采用紫色,复核整张图后我们将其调整为暖玉色与香槟金,图标也重新制作,使它与相邻亲友圈和夕照背景更协调。
我们同时清理了原图中的测试头像、昵称、账号编号和宣传文字,保留功能名称及主要布局。最终形成四套大厅设计:经典四入口大厅、游戏列表大厅、财神大厅和比赛大厅;登录页则完成了国风人物与麻将精灵两种表达。不同方案共享细腻材质、暖金光影和清晰中文排版,同时保留各自的角色与场景特色。

对于准备评估界面定制的客户,这组设计提供了具体参照:人物是否需要更换,场景采用城市还是园林,主要入口怎样排列,哪些颜色属于功能区,哪些细节属于品牌。讨论可以直接落到页面和控件,不必只靠"更高端""更精美"这样的抽象描述。
视觉方向确定后,我们继续检查原版工程,让这些入口与已有代码建立对应。原始资料包括九个顶层文件,总计62,892,293,213字节,约62.89GB。里面既有客户端与服务端源码包,也有站点文件、运行备份和旧操作笔记。下面保留具体代码和检查过程,方便技术负责人核对本次工作依据。
二、我们先把原件、勘察副本和报告分开
我们的实际检查环境是Windows PowerShell与Python 3.12.14,工具使用Python标准库,兼容要求为Python 3.11或更高版本。归档目录读取使用系统中可调用的tar;不同机器的tar编译选项和字符编码可能不同,后文会给出失败处理。这里不安装游戏依赖,也不要求现在就打开Cocos编辑器。
我们使用下面的目录结构组织工作区。originals存放原始归档,samples存放精确提取的少量配置与脚本,reports存放检查输出,tools存放本文四个Python文件。这使原始资料、检查副本与产出文件各有明确位置。
text
qh-refresh/
originals/ 原始归档,按实际存放路径使用
samples/ 定向提取副本,不是可运行工程
reports/ JSON检查报告
tools/
audit_common.py
inventory.py
inspect_project.py
test_audit.py
实际检查从明确路径开始。下面保留我们使用的目录配置方式,公开代码中的绝对位置已泛化,复现时对应自己的工作区和归档路径。后续所有Python命令都在tools目录执行;$Original可以指向现成的原始目录,不需要复制六十多GB文件。
powershell
$Work = 'D:/qh-refresh'
$Original = 'D:/源码归档/情怀700子游戏完整版新'
$Samples = Join-Path $Work 'samples'
$Reports = Join-Path $Work 'reports'
New-Item -ItemType Directory -Path $Samples,$Reports -Force | Out-Null
Set-Location -LiteralPath (Join-Path $Work 'tools')
python --version
Get-Command tar | Select-Object Name,Source
预期第一条命令打印满足要求的Python版本,第二条显示tar可执行文件路径。如果python被系统应用商店别名接管,应使用已安装解释器的绝对路径,后续调用也保持一致。不要一部分命令使用虚拟环境,一部分命令落到另一套系统解释器,否则排查编码和依赖时会增加不必要的变量。
我们把勘察副本限定为少量文本资料,用它确认版本与入口;完整预制体、图片、图集和原生插件仍属于后续客户端接入所需的工作工程。这一区分让问题更容易定位:静态检查关注配置与代码,编辑器验证关注完整资源和实际运行状态。
三、我们统一了报告、编码与内容指纹
我们把公共检查能力收敛到tools/audit_common.py,下面是完整实现。它没有第三方依赖,被后面三个文件导入,不需要单独启动。输入是本地路径与报告对象,输出是UTF-8格式的JSON文件,或者明确的异常。路径不满足要求时,调用方应停止,不能偷偷改成写入源码目录。
python
"""Read-only source inspection helpers; reports belong outside source roots."""
import hashlib
import json
from pathlib import Path
def external_output(path, *roots):
target = Path(path).resolve()
for root in roots:
if target.is_relative_to(Path(root).resolve()):
raise ValueError("Report output must be outside every source root")
return target
def write_json(path, value):
target = Path(path)
target.parent.mkdir(parents=True, exist_ok=True)
staging = target.with_name(target.name + ".tmp")
try:
staging.write_text(
json.dumps(value, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
staging.replace(target)
finally:
staging.unlink(missing_ok=True)
def sha256(path):
digest = hashlib.sha256()
with Path(path).open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def read_text(path):
raw = Path(path).read_bytes()
if len(raw) > 4 * 1024 * 1024:
raise ValueError("Text sample exceeds 4 MiB")
if b"\x00" in raw:
raise ValueError("Unexpected NUL byte in text sample")
for encoding in ("utf-8-sig", "gb18030"):
try:
return raw.decode(encoding)
except UnicodeDecodeError:
continue
raise ValueError("Text encoding is not UTF-8 or GB18030")
external_output会先解析最终路径,再判断它是否位于任何输入根目录之下。这个约束解决一个实际问题:反复运行盘点脚本时,如果把报告写回被盘点目录,下一次就会把上一份报告也算成输入,导致文件数和总容量无故增加。它同时避免检查工具在客户端副本内生成看似原版附带的资料。
write_json先完整序列化到同目录临时文件,成功后再替换目标。这样解析配置失败或序列化失败时,已有正式报告不会被半截输出覆盖。该实现按单进程串行使用设计,不用于多个进程同时写同一个报告文件;它也没有实现断电级事务承诺。临时文件仅是写入过程,不会进入正式交付清单。
sha256采用分块读取,本文只对少量抽取文本计算指纹。原始六十多GB归档没有逐个进行全量哈希,因此后面的"输入未变化"不能扩展成全包内容完整性认证。容量和修改时间适合快速检查读取期间是否发生明显变动,内容指纹适合比较指定文件的字节是否一致,两者承担不同层面的工作。
文本读取会依次尝试UTF-8与兼容GBK的编码,并拒绝空字节异常。这里不使用忽略解码错误的方式,因为悄悄丢字符可能把中文注释、文件名甚至规则文字变成另一份内容。四兆字节检查是对文本样本用途的限制,代码会先读取文件再检查长度,不是面对任意大文件的内存防护方案。
四、我们为归档建立了可重复检查的清单
归档盘点由我们编写的tools/inventory.py完成,下面给出完整文件。前置条件是原始目录存在,四个工具文件放在同一目录。默认只读取顶层文件名、字节数和修改时间;只有显式传入--list的文件才会读取内部目录。普通.tar使用Python标准库直接读取归档头,RAR和7z归档调用系统tar。工具不会执行解压命令,也不会运行归档里的任何程序。
python
"""Inventory local archives; explicitly selected archives use tar listing only."""
import argparse
from collections import Counter
from pathlib import Path
import locale
import subprocess
import tempfile
import tarfile
from audit_common import external_output, write_json
def summarize(names):
files = [name for name in names if not name.endswith("/")]
return {
"entry_count": len(names),
"file_path_count": len(files),
"java_path_count": sum(name.endswith(".java") for name in files),
"ant_path_count": sum(name.endswith("/build.xml") for name in files),
"top_roots": dict(Counter(name.split("/")[0] for name in names)),
}
def list_archive(archive, executable="tar", timeout=60, encoding=None):
if Path(archive).suffix.lower() == ".tar":
try:
with tarfile.open(archive, "r:") as stream:
names = [item.name.rstrip("/") + ("/" if item.isdir() else "")
for item in stream]
return {"status": "listed", "reader": "python_tarfile", **summarize(names)}
except (tarfile.TarError, OSError):
return {"status": "list_failed", "reader": "python_tarfile"}
# Temporary files keep a long listing out of the console and pipe buffers.
with tempfile.TemporaryFile() as output, tempfile.TemporaryFile() as error:
try:
process = subprocess.run(
[executable, "-tf", str(archive)],
stdout=output, stderr=error, timeout=timeout, check=False,
)
except subprocess.TimeoutExpired:
return {"status": "timeout"}
except OSError:
return {"status": "tool_unavailable"}
if process.returncode:
# Do not turn partial stdout into a successful archive inventory.
return {"status": "list_failed", "exit_code": process.returncode}
if output.tell() > 64 * 1024 * 1024:
return {"status": "listing_too_large"}
output.seek(0)
raw = output.read()
try:
text = raw.decode(encoding or locale.getencoding())
except UnicodeDecodeError:
return {"status": "listing_encoding_error"}
if "\x00" in text:
return {"status": "invalid_listing"}
names = [line.replace("\\", "/") for line in text.splitlines() if line]
return {"status": "listed", "reader": "external_tar", **summarize(names)}
def inventory(root, selected=(), executable="tar", timeout=60, encoding=None):
root = Path(root).resolve()
if not root.is_dir():
raise ValueError("Archive directory does not exist")
files = sorted((p for p in root.iterdir() if p.is_file()), key=lambda p: p.name)
available = {p.name for p in files}
if set(selected) - available:
raise ValueError("A selected archive is not a top-level input file")
rows = []
for path in files:
before = path.stat()
row = {"name": path.name, "bytes": before.st_size,
"mtime_ns": before.st_mtime_ns, "status": "not_inspected"}
if path.name in selected:
row.update(list_archive(path, executable, timeout, encoding))
after = path.stat()
if (before.st_size, before.st_mtime_ns) != (after.st_size, after.st_mtime_ns):
raise ValueError("Input changed during inventory")
rows.append(row)
total = sum(row["bytes"] for row in rows)
return {
"schema": 1, "scope": "top_level_files_and_selected_archive_headers",
"total_bytes": total, "gb": round(total / 10**9, 2),
"gib": round(total / 1024**3, 2), "files": rows,
}
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--root", required=True)
parser.add_argument("--out", required=True)
parser.add_argument("--list", action="append", default=[])
parser.add_argument("--tar", default="tar")
parser.add_argument("--timeout", type=int, default=60)
parser.add_argument("--encoding", help="tar listing encoding, e.g. gb18030 or utf-8")
args = parser.parse_args()
if args.timeout <= 0:
parser.error("timeout must be positive")
try:
target = external_output(args.out, args.root)
report = inventory(args.root, args.list, args.tar, args.timeout, args.encoding)
write_json(target, report)
except (ValueError, OSError) as exc:
parser.exit(1, f"Inventory failed: {exc}\n")
failed = any(row["status"] not in ("listed", "not_inspected")
for row in report["files"])
print(f"files={len(report['files'])}, GB={report['gb']}, partial={failed}")
return 2 if failed else 0
if __name__ == "__main__":
raise SystemExit(main())
执行顺序是先验证输入目录,再确认指定归档确实属于该目录的顶层文件,然后逐个读取元数据和可选的内部目录,最后写报告。not_inspected表示没有要求检查内部内容,listed表示目录读取成功,list_failed表示工具退出码非零,timeout表示超时。失败时不生成一组零计数,避免把"没读到"解释成"确实没有"。
外部tar的标准输出和错误输出进入临时文件,长目录不会涌入终端。程序等待子进程完成后检查退出码与目录文本容量,成功才解析路径。这里的六十四兆字节阈值是读取结果前的检查,不是限制tar运行时磁盘写入量;本工具用于本地已知来源的项目盘点,不是处理任意外部压缩包的沙箱。对子进程参数、返回码和超时的处理可以参照Python官方subprocess文档。
普通TAR采用独立读取分支,是因为本次站点归档的外部目录输出存在混合编码与转义路径,仅更换终端编码仍有失败项。直接读取归档头可以绕开终端文本转换,统计时也不需要提取文件内容。该分支不调用归档的提取接口;它没有外部子进程超时参数,因此--timeout只约束外部tar。标准库接口可参照Python官方tarfile文档。
我们先完成顶层盘点,确认输入范围,使用的命令如下:
powershell
python inventory.py --root $Original --out "$Reports/顶层盘点.json"
if ($LASTEXITCODE -ne 0) { throw '顶层盘点失败,请检查输入路径' }
这次终端输出了九个文件、约62.89GB,以及没有本次目录读取失败的摘要。注意,此处没有失败只代表顶层盘点完成,绝不表示归档内部已经验收。打开JSON检查每一行的状态,会看到尚未检查内部内容的明确标记。
五、我们把九份原始资料逐项归类
本次原始目录只有文件,没有已经解压完成的工程。以下容量使用十进制单位;文件名按样本保留,便于对应操作命令。
| 文件 | 约容量 | 本篇检查范围 |
|---|---|---|
| Client_28xin.com.rar | 0.20GB | 客户端目录、项目配置和指定界面脚本 |
| Server_game_2024.rar | 0.23GB | Java文件目录及一个游戏服务构建配置 |
| 新情怀服务端源码.7z | 4.28GB | 工具读取失败,内部待核对 |
| ClientGameCode.7z | 11.14GB | 工具读取失败,内部待核对 |
| webroot.tar | 5.87GB | 站点目录与文件类型分布 |
| 服务器拷贝.tgz | 26.36GB | 本篇仅统计文件元数据 |
| qinghai_system.raw.tar.gz | 14.80GB | 本篇未挂载或恢复 |
| 情怀微信登录.rar | 8.24MB | 本篇仅统计文件元数据 |
| 情怀.docx | 14KB | 人工读取旧操作笔记 |
归类以后,我们保留了不同资料之间的版本关系作为独立核对项。例如"新情怀服务端源码"是否替代"2024服务端",需要比较账号字段、游戏编号、协议定义、配置项与数据库结构。单看归档日期,甚至单看某个文件的修改时间,都不足以判断两者可以混合部署。
webroot.tar里实际可见多个站点、PHP文件、网页资源和第三方组件,因此不能把5.87GB全部当成后台业务源码。镜像名称虽然带有raw,也还需要确认归档内的实际文件、镜像格式和系统内容;本文没有恢复它,自然不能给出已经启动的操作系统版本。
计算容量时采用字节数作为唯一底数,同时输出十进制GB与二进制GiB。文件管理器、网盘和命令行可能使用不同单位,所以58.57GiB与62.89GB描述的是同一批字节,不表示少了几个GB。正式交接最好保留精确字节数,再附上便于阅读的换算值。
六、我们处理了归档读取与中文编码问题
我们在tools目录使用下列命令读取指定归档。中文Windows上的本次tar输出需要按gb18030解码;如果自己的工具输出UTF-8,应把参数改成utf-8,而不是使用替换字符强行解析。
powershell
python inventory.py --root $Original --out "$Reports/归档盘点.json" `
--encoding gb18030 `
--list 'Client_28xin.com.rar' `
--list 'Server_game_2024.rar' `
--list 'webroot.tar' `
--list '新情怀服务端源码.7z' `
--list 'ClientGameCode.7z'
$InventoryExit = $LASTEXITCODE
if ($InventoryExit -eq 1) { throw '盘点未能完成,检查路径或写入错误' }
if ($InventoryExit -eq 2) { Write-Host '报告已生成,但有归档尚未成功读取' }
本次实际结果是两份RAR通过外部工具读取成功,一份TAR通过标准库读取成功,两份7z归档读取失败。手工查看工具错误信息时,系统tar提示不支持相关LZMA编码。这里应记为工具能力限制,不能因此认定压缩包损坏,更不能猜一个解压密码填进文章。
脚本退出码为零表示所有本次指定的目录读取完成,为二表示报告已产生但存在未完成项,为一表示参数、输入或报告写入等整体流程失败。PowerShell里的$LASTEXITCODE必须紧接着Python调用读取,因为后续其他外部命令会覆盖它。自动化任务也应按这一约定判断,而不是只看终端是否出现"报告"二字。
关键统计通过下面的查询读取:
powershell
$ArchiveReport = Get-Content -LiteralPath "$Reports/归档盘点.json" -Raw | ConvertFrom-Json
$ArchiveReport.files | Select-Object name,status,entry_count,java_path_count,ant_path_count
实际读取的客户端包含41,918个归档条目,2024服务端包含134,645个条目,其中Java路径78,375个,Ant构建文件路径391个。它们是文件或目录路径计数,可能包含公共代码、重复文件和构建结果,不能直接换算成独立可运行的子游戏数量。
七、我们定向提取了关键配置和界面脚本
第一轮检查中,我们先查看客户端项目配置的路径与归档记录尺寸,再按精确文件名提取,没有展开全部美术素材。下列命令的输入是原始RAR,输出是$Samples下的副本。它们只适用于已经在目录清单中确认存在的这些路径。
powershell
$ClientArchive = Join-Path $Original 'Client_28xin.com.rar'
tar -tvf $ClientArchive 'Client_28xin.com/project.json'
if ($LASTEXITCODE -ne 0) { throw '无法查看客户端配置条目' }
$ClientFiles = @(
'Client_28xin.com/project.json',
'Client_28xin.com/assets/script/ui/UILogin.js',
'Client_28xin.com/assets/script/ui/UILogin01.js',
'Client_28xin.com/assets/script/ui/UILogin02.js',
'Client_28xin.com/assets/script/ui/UINewMain.js',
'Client_28xin.com/assets/script/ui/UIJoin.js'
)
tar -xf $ClientArchive -C $Samples @ClientFiles
if ($LASTEXITCODE -ne 0) { throw '客户端定向提取失败,停止后续检查' }
本文样本中的project.json归档记录为145字节。提取后应能直接读取正常JSON,而不是只检查文件名是否出现。前一次勘察曾尝试把RAR内文件直接输出到标准输出,系统tar出现了异常空字节流;停止这种方式、改为定向提取后,指定文件正常可读。因此,后续脚本明确拒绝空字节文本。
随后我们提取了一份服务构建文件。这里使用与客户端不同的归档,但仍保留归档内原来的根目录,避免两个包中的同名配置相互覆盖。
powershell
tar -xf "$Original/Server_game_2024.rar" -C $Samples `
'Server_game_2024/gameServer/build.xml'
if ($LASTEXITCODE -ne 0) { throw '服务端构建配置提取失败' }
$Client = Join-Path $Samples 'Client_28xin.com'
$AntFile = Join-Path $Samples 'Server_game_2024/gameServer/build.xml'
Get-Content -LiteralPath "$Client/project.json" -Raw
提取后,我们在客户端配置中读到了version为2.2.2,以及对应的engine字段。只读检查的范围是原始包不变;允许在独立勘察目录里保存提取副本。这里没有启动项目内的批处理,也没有执行构建脚本,代码中的命令字符串都按资料内容处理。
八、我们实现了客户端与服务构建检查工具
我们用tools/inspect_project.py完成项目检查,它与公共模块放在同一目录,完整实现如下。输入是内层客户端工程路径、服务构建文件路径和外部报告路径。它读取五个明确指定的客户端脚本,不递归扫描整个工作区,也不进入临时编译目录;报告列出缺失样本,而不是用空对象掩盖缺失。
python
"""Static inspection of a deliberately small, extracted project sample."""
import argparse
import json
from pathlib import Path
import re
import xml.etree.ElementTree as ET
from audit_common import external_output, read_text, sha256, write_json
SAMPLES = (
"assets/script/ui/UILogin.js",
"assets/script/ui/UILogin01.js",
"assets/script/ui/UILogin02.js",
"assets/script/ui/UINewMain.js",
"assets/script/ui/UIJoin.js",
)
FORM = re.compile(r"ShowForm\(\s*['\"]([^'\"]+)['\"]")
def inspect_script(path, relative):
text = read_text(path)
candidates = []
# This is a navigation index, not a JavaScript parser or reachability proof.
for number, line in enumerate(text.splitlines(), 1):
stripped = line.lstrip()
if stripped.startswith("//"):
continue
for match in FORM.finditer(line):
candidates.append({"form": match.group(1), "line": number})
return {"path": relative, "sha256": sha256(path),
"form_literal_candidates": candidates}
def inspect_client(root):
root = Path(root).resolve()
project = root / "project.json"
if not project.is_file():
raise ValueError("Missing project.json; choose the inner client root")
config = json.loads(read_text(project))
if not isinstance(config, dict) or not isinstance(config.get("version"), str):
raise ValueError("project.json lacks a string version")
result = {
"scope": "selected_static_files_only", "runtime_verified": False,
"engine_field": config.get("engine"), "version_field": config["version"],
"project_sha256": sha256(project), "scripts": [], "missing_samples": [],
"join_observations": {},
}
for relative in SAMPLES:
path = root / relative
if not path.is_file():
result["missing_samples"].append(relative)
continue
result["scripts"].append(inspect_script(path, relative))
if path.name == "UIJoin.js":
text = read_text(path)
result["join_observations"] = {
"six_digit_condition_found": bool(re.search(
r"labelString\.length\s*={2,3}\s*6", text)),
"lookup_protocol_found": "room.CBaseGetGameType" in text,
"subgame_gate_found": "JoinRoomCheckSubGame" in text,
}
return result
def inspect_ant(path):
path = Path(path)
text = read_text(path)
# Decode legacy GBK before parsing, rather than ignoring invalid bytes.
text = re.sub(r"^\s*<\?xml[^>]*\?>", "", text)
root = ET.fromstring(text)
properties = {node.get("name"): node.get("value")
for node in root.findall("property")}
return {
"scope": "one_build_file", "build_verified": False,
"sha256": sha256(path), "project_name": root.get("name"),
"javac": [{key: node.get(key) for key in ("source", "target", "encoding")}
for node in root.iter("javac")],
"compiler_args": [node.get("line") for node in root.iter("compilerarg")],
"selected_games": [name for name in
(properties.get("game-list") or "").split(",") if name],
}
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--client", required=True)
parser.add_argument("--ant", required=True)
parser.add_argument("--out", required=True)
args = parser.parse_args()
try:
target = external_output(args.out, args.client, Path(args.ant).parent)
report = {"schema": 1, "client": inspect_client(args.client),
"server": inspect_ant(args.ant)}
write_json(target, report)
except (ValueError, OSError, ET.ParseError) as exc:
parser.exit(1, f"Inspection failed: {exc}\n")
missing = len(report["client"]["missing_samples"])
print(f"version={report['client']['version_field']}, missing_samples={missing}")
return 2 if missing else 0
if __name__ == "__main__":
raise SystemExit(main())
检查器把配置中的原始值命名为version_field和engine_field,避免把一个字符串包装成实际启动验证。runtime_verified与build_verified固定为假,因为程序没有打开Cocos,也没有运行Ant。只有未来增加相应验证流程并记录结果,才能改变那个阶段的结论;本脚本不提供把标记强制改成真的参数。
页面入口通过ShowForm的字面量调用建立检索索引,同时保留源码行号。它过滤整行单行注释,但不是完整JavaScript解析器,块注释、条件不可达代码和动态拼接名称仍需人工判断。因此字段明确叫候选项,不能把报告当成全部可达页面图。真正用于改造的入口,下一节会回到具体函数逐段核对。
Ant部分先按旧文件编码读取,再解析XML。输出包括编译参数、主工程名称和当前选择的游戏列表。javac的encoding属性为空,并不意味着没有设置源码编码,因为这个样本把UTF-8放在compilerarg里,报告同时保留了这部分参数。阅读构建文件时只盯某一个属性,容易得出错误结论。
我们实际使用的检查命令如下:
powershell
python inspect_project.py --client $Client --ant $AntFile `
--out "$Reports/工程检查.json"
if ($LASTEXITCODE -ne 0) { throw '工程检查不完整,请查看缺失样本或错误信息' }
实际终端摘要如下。这是检查脚本输出,不是游戏服务器启动日志。
text
version=2.2.2, missing_samples=0
读取工程报告的版本、加入房间证据以及服务编译参数:
powershell
$ProjectReport = Get-Content -LiteralPath "$Reports/工程检查.json" -Raw | ConvertFrom-Json
$ProjectReport.client | Select-Object version_field,runtime_verified,missing_samples
$ProjectReport.client.join_observations
$ProjectReport.server.javac
$ProjectReport.server.selected_games
实际报告中,加入房间的三个文本检查项均为真,编译源版本与目标版本均为1.8。它们分别证明对应文本存在、构建配置如此声明,并不证明入房网络成功,也不证明完整Java工程编译成功。报告中的样本哈希用于以后核对"现在看的代码还是不是当时那一份"。
九、旧工程基线与Cocos新版本的适配方向
客户端项目配置记录的版本是2.2.2,归档里同时存在assets、settings、场景、预制体和使用cc接口的脚本,可以把它作为Creator2.2.2工程开始核对。这里的engine值是旧项目配置字段,不应单独拿它与今天某个产品名称做字符串匹配,从而推断项目不是Creator工程。
在这个版本的工程里,应主要沿着assets里的资源和脚本查找对应关系,项目设置则另看settings。library与temp承担导入或临时缓存工作,不应作为修改主源码的入口。相关目录职责可对照Cocos Creator 2.2官方项目结构说明。本次归档中实际存在临时脚本副本,所以这一点会直接影响检索结果。
例如搜索UINewMain.js,可能同时命中资源目录与temp/quick-scripts目录。如果在缓存文件里换按钮名称,编辑器重新生成缓存后,修改可能消失;如果用两个命中数判断存在两个正式大厅版本,又会高估工程复杂度。本文通过固定样本清单,只读取资源目录下的文件,先排除这一类干扰。
另一个容易误判的地方是.meta。它记录资源身份及相关导入信息,资源引用不能只靠肉眼看到的同名图片理解。后续更换人物和按钮时,需要同时考虑旧资源引用、子资源以及预制体绑定,不能批量删除元数据再期待所有界面自动恢复。Cocos 2.2的meta说明解释了UUID变化和资源引用丢失之间的关系。
在新版技术方向上,我们查阅了Cocos Creator 3.8 LTS文档与官方更新记录。此次核对的更新页列有3.8.8,涉及Android页面大小支持、渲染与平台适配等更新。它是后续迁移评估的版本参考,原版配置中的2.2.2只是我们读取到的历史工程基线。Cocos官方更新记录、3.8 LTS文档。
我们在技术范围中区分了视觉方案与引擎迁移:本次完成的是新版设计和旧工程检查,尚未将客户端转换为3.x工程。后续适配涉及脚本组件、资源引用、原生插件及构建流程;官方也提供了相应升级说明。文章里的检查工具可以读取旧资料,但不会生成一个已经迁移完成的客户端。Cocos官方升级指南。
十、我们核对了创建房间的两层入口
我们沿着assets/script/ui/UINewMain.js中的OnClick逐段阅读,再对照UICreatRoom的调用位置核对流程。本次样本中,btn_create分支先检查当前是否已经有房间。如果当前房间编号非零,代码进入返回原房间的确认流程;只有另一条分支才显示更多玩法页面。
我们据此将新版交互清单分成两种行为状态:没有房间时选择玩法,有房间时询问是否返回。若可点击演示只把"创建房间"绑定到一个固定规则面板,虽然视觉上能点通,却会遗漏原版已经实现的业务约束。后续接入还会出现同一按钮在不同账号状态下行为不同的问题。
下面是本次源码的调用关系说明,不是可以直接粘贴运行的新代码:
text
UINewMain.OnClick / btn_create
当前已有房间 → 返回原房间确认流程
当前没有房间 → ShowForm("UIMoreGame")
UINewMain.InitGameBtnList(serverPack)
→ ShowForm("UICreatRoom", serverPack, this.gameName)
InitGameBtnList存在接收数据包后展示建房界面的调用。仅凭这两处代码,还不能完整证明玩法页内部的所有选择逻辑;下一步需要继续读取玩法列表、游戏配置和具体规则子组件。因此本文把它作为建房流程的两个已定位节点,不把中间未读部分画成已经验证的完整调用链。
界面名称中的UICreatRoom保留了原项目的拼写。重构时若自行把它改成更符合英文习惯的名称,但没有同步资源路径和管理器查找规则,就可能得到找不到预制体的错误。第一轮改造应先保留原有标识,再决定是否做有范围的重命名与引用检查。
十一、我们梳理了六位房号之后的进入流程
本次UIJoin.js中的输入逻辑用数组保存已输入数字,限制最多六位,满六位后拼接房号并请求玩法类型。这里房号是用于查找房间的字符串,不应为了方便显示而提前转成整数;如果某套编号允许前导零,字符串转数字再转回去就可能丢位。
当前代码以room.CBaseGetGameType查询房间玩法,再经过玩法编号到名称的映射,调用JoinRoomCheckSubGame。另外还存在特定玩法的入座选择分支。这些细节说明,"查询房间"和"真正进入牌桌"之间有额外步骤,新UI需要承接等待、失败和资源准备状态,而不是输入第六位就无条件显示牌桌。
下面是人工核对得到的流程提要,同样属于阅读辅助,不是替代原函数的实现:
text
数字输入未满六位 → 更新输入格
数字输入达到六位 → 查询房间对应玩法
查询失败 → 原代码重置数字输入
查询成功 → 玩法映射 → 子游戏检查与后续进入流程
特定玩法 → 另有选择座位处理
错误状态也应属于设计清单。至少需要区分房号尚未输入完整、查询等待、查询失败后的输入重置,以及后续子游戏加载反馈。接口的真实错误码和提示文案应继续从通知管理器及协议定义查找,不应凭空编出一套状态码塞回旧客户端。
可点击演示阶段可以使用明确的示例房号来展示这些分支,但必须说明是模拟交互。真正接入阶段则需要验证请求失败时是否停止后续进入、重复输入是否造成并发请求、退出弹窗后回调是否仍在修改节点,以及重进房间时资源检查是否正确。这些都是后续运行验收项,本篇没有用文本匹配代替它们。
十二、我们整理了亲友圈与登录的状态分支
大厅的btn_club会请求亲友圈列表。列表为空时显示ui/club/UIClubNone,非空时先把最小列表信息交给管理器,再显示列表界面。因此亲友圈设计不能只有一张"房间很多"的展示图,还需要没有加入任何圈时的空态、查询中的反馈和查询失败后的恢复入口。
另一个加入亲友圈分支使用不同的列表协议,并在有数据时交由管理器继续展示。这种看似重复的按钮不应在设计阶段随意合并。需要先明确它们在原版里是不同入口、不同权限场景,还是历史皮肤留下的差异,然后才能决定新版导航如何组织。
登录同样不只有一份文件。本次找到基础登录以及多个登录变体,基础脚本中还根据原生平台条件选择登录表单。这说明登录背景虽然可以统一,真正接入时仍要核对平台分支、SDK回调和账号管理器,不能因为新图上绘制了微信按钮,就认为微信认证已经适配完成。
我们把源码入口整理成以下页面对应表,供后续客户确认功能范围和界面细节。大厅与登录已有视觉设计,其余弹窗、列表及交互接入仍在待制作范围内:
| 新版页面 | 当前对应入口 | 必须补齐的状态 |
|---|---|---|
| 登录主界面 | UILogin及登录变体 | 普通输入、等待、失败、协议入口 |
| 大厅 | UINewMain | 无房间、已有房间、地区选择 |
| 玩法选择 | UIMoreGame | 玩法列表、选中与进入规则面板 |
| 建房规则 | UICreatRoom | 按具体玩法读取的规则选项 |
| 加入房间 | UIJoin | 六位输入、查询等待、失败反馈 |
| 亲友圈列表 | UIClubNone、UIClubList | 空态、列表、加载与错误 |
| 设置与战绩 | UISetting01、UIRecordAll | 弹窗关闭、数据为空、列表详情 |
十三、我们核实了服务端的构建方式
服务端样本的gameServer/build.xml采用Ant构建,编译任务中的源级别和目标级别都是1.8,同时引用公共工程及可配置的游戏列表。源码目录中也确实存在对应公共目录和大量Java路径。这些证据支持"该样本采用Java 8目标配置与Ant构建"的结论。
但source与target不等于运行时所有依赖已经兼容,也不等于数据库、消息队列或网关已经安装。构建工具还要找到实际JDK、公共源码和依赖JAR,编译任务也可能执行清理与复制。相关参数的含义可对照Apache Ant官方javac任务文档。本篇只解析XML,不调用构建任务。
配置中的游戏列表当前只有一组选定模块,公共编译过程按这个列表复制游戏源码。将全部模块目录直接统计为"这次启动支持的玩法",会跳过构建选择这一层;把某个已经编译的JAR放回另一套数据库,也不能据此认为游戏编号与规则配置一定匹配。
因此,后续搭建文章应该先选择一个明确玩法做最小链路:确认它的客户端编号、服务器模块、配置项、建房规则和返回结果。等这一条路径跑通,再扩大范围。对全部地方玩法同时启动和测试,既不利于定位问题,也会让早期故障被大量无关日志淹没。
十四、我们把旧部署记录整理成了环境线索
本次人工读取的旧文档提到了MySQL 5.6、MongoDB 3.2.8、PHP 5.5、Nginx 1.16、Redis 5.0.9和RocketMQ,还列有账号、网关以及Node/Pomelo相关启动顺序。这些是旧笔记留下的环境线索,不能直接当成本机已经安装的组件清单。
旧文档还包含历史地址替换、数据库清理以及删除运行数据的命令。它们在当时可能针对特定测试环境,但离开当时的目录状态、备份条件和数据用途,直接执行就无法判断影响。本次只阅读这些文本,没有连接历史地址,没有执行启动或清库命令;公开文章也没有复制其中的口令与后台地址。
整理新教程时,应把每条旧命令改造成可验证步骤。例如"启动数据库"之前要先知道数据文件来源、配置位置、进程用户和端口;启动之后要读取版本与目标库,再用最小查询验证结构。某条命令返回零,并不意味着连接的是期望的那份数据;端口能连通,也不表示表结构与代码一致。
目前两份7z归档尚未成功读取,镜像也没有恢复,因此还不能确定旧笔记描述的是哪一套服务端。处理顺序应是先解决读取工具,再核对目录和配置关系,最后在独立环境复现。这里不列"升级到最新数据库"的安装命令,因为未经代码和数据兼容测试的版本替换,无法成为真实可复现的搭建方法。
十五、我们验证了检查工具的异常行为
我们为检查工具配套编写了tools/test_audit.py,完整实现如下。它与前三个文件同目录,只依赖Python标准库。测试会在临时目录建立小型样本,覆盖重复检查、输入缺失、异常JSON、报告位置错误、缓存干扰、编码、目录读取失败与超时等边界,结束后自动清理测试样本。
python
"""Regression checks: failures must not look successful or modify sources."""
import json
from pathlib import Path
import subprocess
import tempfile
import tarfile
import unittest
from unittest.mock import patch
from audit_common import external_output, read_text, sha256, write_json
from inspect_project import SAMPLES, inspect_ant, inspect_client
from inventory import inventory, list_archive
class AuditTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.base = Path(self.temp.name)
self.client = self.base / "client"
self.client.mkdir()
(self.client / "project.json").write_text(
'{"version":"2.2.2","engine":"cocos2d-html5"}', encoding="utf-8")
for relative in SAMPLES:
path = self.client / relative
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text('this.ShowForm("UIJoin");\n', encoding="utf-8")
def test_repeatable_and_source_unchanged(self):
paths = [path for path in self.client.rglob("*") if path.is_file()]
before = {str(path): sha256(path) for path in paths}
first = inspect_client(self.client)
self.assertEqual(first, inspect_client(self.client))
self.assertEqual(before, {str(path): sha256(path) for path in paths})
self.assertFalse(first["runtime_verified"])
def test_missing_sample_is_explicit(self):
(self.client / SAMPLES[-1]).unlink()
report = inspect_client(self.client)
self.assertEqual(report["missing_samples"], [SAMPLES[-1]])
self.assertEqual(report["join_observations"], {})
def test_malformed_json_preserves_old_report(self):
output = self.base / "report.json"
output.write_text('{"previous":true}', encoding="utf-8")
before = output.read_bytes()
(self.client / "project.json").write_text("{bad", encoding="utf-8")
with self.assertRaises(ValueError):
write_json(output, inspect_client(self.client))
self.assertEqual(before, output.read_bytes())
def test_output_inside_source_is_rejected(self):
with self.assertRaises(ValueError):
external_output(self.client / "report.json", self.client)
def test_comment_and_cache_do_not_become_main_entries(self):
(self.client / SAMPLES[0]).write_text(
'// ShowForm("Ignored");\nthis.ShowForm("UIJoin");', encoding="utf-8")
cache = self.client / "temp/quick-scripts/assets/script/ui/UILogin.js"
cache.parent.mkdir(parents=True)
cache.write_text('this.ShowForm("WrongCache");', encoding="utf-8")
forms = inspect_client(self.client)["scripts"][0]["form_literal_candidates"]
self.assertEqual(forms, [{"form": "UIJoin", "line": 2}])
def test_join_evidence_does_not_claim_runtime(self):
(self.client / SAMPLES[-1]).write_text(
'if (this.labelString.length == 6) {\n'
'send("room.CBaseGetGameType"); JoinRoomCheckSubGame(); }',
encoding="utf-8")
result = inspect_client(self.client)
self.assertTrue(all(result["join_observations"].values()))
self.assertFalse(result["runtime_verified"])
def test_gbk_ant_with_selected_games(self):
path = self.base / "build.xml"
path.write_bytes(('<?xml version="1.0" encoding="GBK"?>'
'<project name="gameServer"><!--编译-->'
'<property name="game-list" value="DDZ,PDK"/>'
'<target><javac source="1.8" target="1.8"/>'
'</target></project>').encode("gb18030"))
result = inspect_ant(path)
self.assertEqual(result["javac"][0]["target"], "1.8")
self.assertEqual(result["selected_games"], ["DDZ", "PDK"])
self.assertFalse(result["build_verified"])
def test_listing_failure_is_not_zero_games(self):
with patch("inventory.subprocess.run") as run:
run.return_value = subprocess.CompletedProcess([], 2)
result = list_archive("sample.7z")
self.assertEqual(result["status"], "list_failed")
self.assertNotIn("java_path_count", result)
def test_listing_timeout_is_explicit(self):
with patch("inventory.subprocess.run",
side_effect=subprocess.TimeoutExpired("tar", 1)):
self.assertEqual(list_archive("sample.rar")["status"], "timeout")
def test_listing_chinese_encoding_is_explicit(self):
def fake_run(command, **kwargs):
kwargs["stdout"].write("目录/测试.java\n目录/build.xml\n".encode("gb18030"))
return subprocess.CompletedProcess(command, 0)
with patch("inventory.subprocess.run", side_effect=fake_run):
result = list_archive("sample.rar", encoding="gb18030")
self.assertEqual(result["java_path_count"], 1)
self.assertEqual(result["ant_path_count"], 1)
def test_inventory_does_not_list_without_request(self):
with patch("inventory.subprocess.run") as run:
result = inventory(self.client)
run.assert_not_called()
self.assertEqual(result["files"][0]["status"], "not_inspected")
def test_native_tar_reader_preserves_chinese_path(self):
archive = self.base / "fixture.tar"
with tarfile.open(archive, "w") as stream:
stream.addfile(tarfile.TarInfo("目录/测试.java"))
result = list_archive(archive)
self.assertEqual(result["reader"], "python_tarfile")
self.assertEqual(result["java_path_count"], 1)
self.assertEqual(result["top_roots"], {"目录": 1})
def test_nul_content_is_not_accepted_as_text(self):
path = self.base / "bad.json"
path.write_bytes(b"\x00" * 100)
with self.assertRaises(ValueError):
read_text(path)
def test_written_json_can_be_read_back(self):
output = self.base / "reports/result.json"
report = inspect_client(self.client)
write_json(output, report)
self.assertEqual(json.loads(output.read_text(encoding="utf-8")), report)
self.assertFalse(output.with_name("result.json.tmp").exists())
if __name__ == "__main__":
unittest.main(verbosity=2)
这些测试没有模拟出一个"可运行的情怀服务器"。它们验证的是本文新增检查工具的行为:重复读取不会修改样本,配置损坏时不会覆盖旧报告,失败的归档读取不会变成成功的零计数,以及文本中出现入房标识时仍然保持未验证运行状态。测试范围与工具职责一致,避免用几个字符串断言包装成完整业务验收。
其中归档失败与超时测试使用替身返回值,是为了稳定触发异常分支,不依赖某台机器刚好缺少某种解码器。真实归档目录读取另用前面的命令执行,两类结果分别记录。中文编码测试通过明确写入一段中文目录字节,确认指定解码方式确实影响统计,而不只是验证参数存在。
在tools目录执行:
powershell
python -m unittest -v
if ($LASTEXITCODE -ne 0) { throw '检查工具测试失败,暂不使用其报告作结论' }
这次实测得到十四项通过的结果。不同机器耗时会变化,不必追求与本文完全相同的秒数。如果某个测试失败,应先修复工具再重跑真实样本检查,不能把失效测试删掉后继续沿用旧报告。普通TAR读取测试会实际创建一个含中文路径的小归档再读取,补充验证标准库分支。
十六、我们确认了报告可重复与样本不变
实际工程检查中,我们又完成了一次前后对照。下面的命令在前文定义的变量仍然有效时运行,对六个指定客户端文本与一份Ant文件计算内容哈希,再重复执行检查,最后比较哈希和两次报告内容。
powershell
$ProbeFiles = @("$Client/project.json", $AntFile)
$ProbeFiles += @(
'UILogin.js','UILogin01.js','UILogin02.js','UINewMain.js','UIJoin.js'
) | ForEach-Object { Join-Path "$Client/assets/script/ui" $_ }
$Before = @($ProbeFiles | ForEach-Object { (Get-FileHash -LiteralPath $_ -Algorithm SHA256).Hash })
$PreviousReport = Get-Content -LiteralPath "$Reports/工程检查.json" -Raw
python inspect_project.py --client $Client --ant $AntFile --out "$Reports/工程检查.json"
if ($LASTEXITCODE -ne 0) { throw '重复检查失败' }
$After = @($ProbeFiles | ForEach-Object { (Get-FileHash -LiteralPath $_ -Algorithm SHA256).Hash })
if (Compare-Object $Before $After) { throw '样本内容发生变化' }
$CurrentReport = Get-Content -LiteralPath "$Reports/工程检查.json" -Raw
if ($PreviousReport -cne $CurrentReport) { throw '相同输入生成了不同报告' }
Write-Host '样本内容未变化,两次工程报告一致'
比较对象必须讲清楚:这里检查的是提取副本中的七个文件,不是整个客户端,也不是六十多GB原始包。报告没有加入每次都会变化的生成时间,所以在同一版本工具、同一组输入下,可以直接比较报告文本。如果需要记录执行时间,应放在独立验证记录里,避免污染可重复输出。
另一个实际价值是判断问题发生在哪个阶段。如果改造后界面引用异常,但这些早期样本的哈希也变了,就应该先确认工作副本是否被覆盖,或者是否切换到了另一套包。没有基线时,人们很容易把版本混用误诊成引擎或网络问题。
十七、检查过程中采用的定位方法
第一类是找不到project.json。先检查是否把客户端压缩包外面的样本总目录当成项目根目录,正确输入应进入Client_28xin.com这一层。不要在外层随手新建一个空配置让脚本通过,那样只会失去版本证据。
第二类是归档目录编码错误。查看状态是否为listing_encoding_error,确认tar输出编码后显式传入参数。本次从固定UTF-8调整为中文Windows对应编码后,两份RAR正常统计;普通TAR则改由标准库读取归档头,避开混合编码输出。读取失败的7z包仍保留失败状态。编码修正与压缩算法支持是两个问题,不应混在一起排查。
第三类检查针对候选页面与实际点击的对应关系。先按报告行号打开原脚本,核对它是否位于注释、平台判断、房间状态判断或另外的皮肤分支,再检查管理器怎样加载预制体。候选检索不能证明调用可达,单独一处字符串也不能证明按钮绑定正确。后续编辑器验证时,要把节点事件和函数入口一起查。
第四类是Ant XML读取失败或中文乱码。先确认提取文件内容正常,再区分文件自身编码与编译Java源码的编码。XML声明采用GBK而编译参数采用UTF-8是可以同时存在的,因为两者描述的对象不同。不要全目录转换编码以后才开始排查,那会同时改动大量尚未确认的文件。
第五类判断涉及检查副本和编辑器工作工程的区别。当前提取目录是勘察样本,缺少大量资源本来就是预期结果;本文检查成功只代表指定文件可读。只有完整工作副本建立以后,编辑器资源导入、场景加载、预制体依赖和原生插件报错才进入相应验收范围。
第六类判断涉及归档统计与玩法范围的区别。检查统计字段是否写的是路径数、模块目录数、构建选择数或已运行玩法数。它们不能互相替代,尤其不能从Java文件数推算游戏数。若需要对外列玩法清单,应按游戏编号建立客户端配置、服务模块和验收结果的对应表,逐项维护。
十八、这一阶段形成的设计与技术成果
这次情怀焕新已经形成两类具体成果。视觉方面,我们完成了四套大厅与两套登录页设计,统一了国风场景、人物质感、金玉材质和功能配色,并根据整图协调性重新处理了娱乐大厅。技术方面,我们完成九个顶层文件盘点,读取了两份RAR和一份TAR的目录,核对客户端版本、五个界面脚本及一个Ant构建文件,保留了可重复生成的检查报告。
四个配套Python文件已完成语法检查,十四项工具测试通过。我们对七个样本文本进行了检查前后的内容哈希对比,两次工程报告保持一致。两份7z的内部读取仍受当前工具能力限制;这些测试结果对应源码检查工具,不代表游戏客户端、数据库或设备端已经通过验收。
对于关心经典情怀界面升级的客户,现在可以直接围绕六张设计稿讨论风格与布局,也可以通过本文的入口清单确认创建房间、加入房间和亲友圈分别涉及哪些流程。已有房间的返回判断、六位房号后的玩法识别、亲友圈的空列表状态,都已经进入我们整理的交互清单。
后续接入范围包括玩法选择、建房规则、房号输入与亲友圈页面,以及3.x引擎适配和独立环境联调。具体交付以选择的页面、玩法和验证结果为依据。本篇记录的设计与检查成果,构成了这一轮情怀焕新的实际起点。
认的文件。
第五类判断涉及检查副本和编辑器工作工程的区别。当前提取目录是勘察样本,缺少大量资源本来就是预期结果;本文检查成功只代表指定文件可读。只有完整工作副本建立以后,编辑器资源导入、场景加载、预制体依赖和原生插件报错才进入相应验收范围。
第六类判断涉及归档统计与玩法范围的区别。检查统计字段是否写的是路径数、模块目录数、构建选择数或已运行玩法数。它们不能互相替代,尤其不能从Java文件数推算游戏数。若需要对外列玩法清单,应按游戏编号建立客户端配置、服务模块和验收结果的对应表,逐项维护。
十八、这一阶段形成的设计与技术成果
这次情怀焕新已经形成两类具体成果。视觉方面,我们完成了四套大厅与两套登录页设计,统一了国风场景、人物质感、金玉材质和功能配色,并根据整图协调性重新处理了娱乐大厅。技术方面,我们完成九个顶层文件盘点,读取了两份RAR和一份TAR的目录,核对客户端版本、五个界面脚本及一个Ant构建文件,保留了可重复生成的检查报告。
四个配套Python文件已完成语法检查,十四项工具测试通过。我们对七个样本文本进行了检查前后的内容哈希对比,两次工程报告保持一致。两份7z的内部读取仍受当前工具能力限制;这些测试结果对应源码检查工具,不代表游戏客户端、数据库或设备端已经通过验收。
对于关心经典情怀界面升级的客户,现在可以直接围绕六张设计稿讨论风格与布局,也可以通过本文的入口清单确认创建房间、加入房间和亲友圈分别涉及哪些流程。已有房间的返回判断、六位房号后的玩法识别、亲友圈的空列表状态,都已经进入我们整理的交互清单。
后续接入范围包括玩法选择、建房规则、房号输入与亲友圈页面,以及3.x引擎适配和独立环境联调。具体交付以选择的页面、玩法和验证结果为依据。本篇记录的设计与检查成果,构成了这一轮情怀焕新的实际起点。