From 6a756a3274cac226fe15d4a768de55495ac3d9fd Mon Sep 17 00:00:00 2001 From: Joywayer Date: Wed, 2 Sep 2026 08:17:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(scripts):=20SpiritType=204/5=20=E9=80=86?= =?UTF-8?q?=E5=90=91=20(cc.EditBox=20/=20cc.Sprite=20SLICED)=20+=20SizeCal?= =?UTF-8?q?cMode=20=E5=85=AC=E5=BC=8F=20(Task=205)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gameabc.min.js 逆向结果固化到 fixtures/spirit-semantics.json: SpiritType 4 = cc.EditBox 逆向证据: phoneInfo Layer00029.xml 含 type-4 spirit (Spirit3246 FrameText=12, Spirit3247 FrameText=1 等); YouleNexus Group106_PhoneVerify.prefab (映射到 Layer00029) 含 cc.EditBox 节点, 与 type-4 FrameText 输入框语义一致. 独有属性: FrameText (默认/显示文本) + ImgResID (背景图) + CanClick=1. SpiritType 5 = cc.Sprite (type=SLICED) 逆向证据 1 (smoking gun): Layer00002.xml Spirit159 (370x170, 边缘 100/80/100/80) → YouleNexus Login_Layer.prefab 唯一 cc.Sprite._type=1 (SLICED), _contentSize 严格等于 370x170. 1:1 同一个节点. 逆向证据 2: Layer00609.xml '设置背景' (100/134/100/135) → YouleNexus Layer609_Setting.prefab cc.Sprite._type=1. 独有属性: LeftEdge/TopEdge/RightEdge/BottomEdge (4 边缘宽度) = 9-slice scaling. SizeCalcMode 0/2 公式 (55 Layer 全量解析: 仅观察到这两种值): 0 = 直接读 Width/Height (Task 3 已实现). 2 = parent_size (lookup by SizeParentID) * SizeRelaWidth/Height / 10000. 全部采样 WidthCalcMode=1, SizeRelaWidth=10000 (即按 100% 比例继承父 spirit 尺寸). 测试 spirit-type-reverse.test.mjs 3 个 case 全 PASS: - SpiritType 4 component != unknown - SpiritType 5 component != unknown - spirit_types 含 5 项且全部 component != unknown --- .../fixtures/spirit-semantics.json | 63 ++++++++++++++++--- .../spirit-type-reverse.test.mjs | 30 +++++++++ 2 files changed, 84 insertions(+), 9 deletions(-) create mode 100644 cocoscreator_projects/framework-tests/legacy-layer-migration/spirit-type-reverse.test.mjs diff --git a/cocoscreator_projects/framework-tests/legacy-layer-migration/fixtures/spirit-semantics.json b/cocoscreator_projects/framework-tests/legacy-layer-migration/fixtures/spirit-semantics.json index e2aa2da..56e59cc 100644 --- a/cocoscreator_projects/framework-tests/legacy-layer-migration/fixtures/spirit-semantics.json +++ b/cocoscreator_projects/framework-tests/legacy-layer-migration/fixtures/spirit-semantics.json @@ -1,18 +1,63 @@ { + "_comment": "gameabc.min.js 逆向结果(PoC 阶段 1, 2026-09-02). 数据源权威: 下游消费方不得猜测/修补/兜底, unknown → throw.", "spirit_types": { - "0": { "component": "cc.Sprite", "note": "普通精灵" }, - "1": { "component": "cc.Label", "note": "文本" }, - "3": { "component": "cc.ProgressBar", "note": "进度条" }, - "4": { "component": "unknown", "note": "PoC 阶段 1 逆向 gameabc.min.js" }, - "5": { "component": "unknown", "note": "PoC 阶段 1 逆向 gameabc.min.js" } + "0": { + "component": "cc.Sprite", + "note": "普通精灵; _type=0 (SIMPLE), _sizeMode=0", + "evidence": "Game_Surface_3 save/Layer*.xml 中 type=0 的 spirit 均含 ImgResID (sprite 图集索引) + BelongGroupID, 无独有属性; YouleNexus Login_Layer.prefab 的普通 sprite (_type=0, _sizeMode=0) 与 type=0 spirit 一一对应" + }, + "1": { + "component": "cc.Label", + "note": "文本标签; 含 Text/FontSize/AlignHorz/FontColor/BackColor 等独有属性", + "evidence": "Game_Surface_3 中 type=1 spirit 独有 Text/FontSize/FontBold/FontColor/BackColor/AlignHorz/AlignVert/AutoSize/WordWrap/LineSpace/TextStyle 这 11 个属性; 全量 XML 解析 38/55 Layer 含 type=1; 与 YouleNexus Login_Layer 中 cc.Label 节点字段对齐" + }, + "3": { + "component": "cc.ProgressBar", + "note": "进度条; 含 Frame 属性 (frame=进度 0-1 范围 或 frame id)", + "evidence": "Game_Surface_3 中 type=3 spirit 独有 Frame 属性 (例如 Layer00002 'Spirit3254' Frame=进度条); 45/55 Layer 含 type=3; 与 YouleNexus Login_Layer.prefab 中 '进度条底' cc.ProgressBar 节点对应" + }, + "4": { + "component": "cc.EditBox", + "note": "文本输入框; 独有属性 FrameText (默认/显示文本); CanClick=1; 含 ImgResID (背景图); CanDragSize=0", + "evidence": "Game_Surface_3 中 type=4 spirit 独有 FrameText 属性 (e.g. Layer00029 phoneInfo Spirit3246 FrameText='12' Spirit3247 FrameText='1' / Layer00014 Pay_Layer Spirit446 FrameText='12'); 15/55 Layer 含 type=4; YouleNexus Group106_PhoneVerify.prefab (映射到 Layer00029.xml phoneInfo) 含 1 个 cc.EditBox 节点, 与 type=4 的 FrameText 输入框语义一致 (phoneInfo 是手机号/验证码输入场景)" + }, + "5": { + "component": "cc.Sprite", + "note": "9-slice 缩放精灵; 独有属性 LeftEdge/TopEdge/RightEdge/BottomEdge (4 个边缘宽度); CanClick=1, CanDragSize=1; Cocos 映射: cc.Sprite._type = 1 (SLICED)", + "evidence": "Game_Surface_3 中 type=5 spirit 独有 LeftEdge/TopEdge/RightEdge/BottomEdge 属性 (Layer00002 Spirit159 (100/80/100/80, 隐式) / Layer00006 Spirit287 (70/1/70/1) / Layer00017 '个人信息背景' (70/70/70/70) / Layer00609 '设置背景' (100/134/100/135) / Layer00612 Tips_Layer 5 个 type=5 边缘容器); 39/55 Layer 含 type=5; YouleNexus Login_Layer.prefab 中唯一一个 cc.Sprite._type=1 (SLICED) 节点的 _contentSize 是 370×170, 严格匹配 Layer00002 Spirit159 Width=370 Height=170 — 同一个节点, 1:1 对应; YouleNexus Layer609_Setting.prefab 中 cc.Sprite._type=1 对应 Layer00609 '设置背景' (9-slice 边缘切片背景); 4 个边缘宽度正是 9-slice 缩放 slice 区域定义" + } + }, + "size_calc_mode": { + "_comment": "55 个 Layer 全量解析: 只观察到 SizeCalcMode = 0 和 2 两种值; 0 直接读 Width/Height, 2 通过 SizeParentID 引用父 Spirit 尺寸再乘以 SizeRelaWidth/SizeRelaHeight (万分比) 比率; WidthCalcMode/HeightCalcMode=1 表示启用相对宽度/高度 (SizeRelaXxx 字段), =0 表示使用 Width/Height 自身.", + "0": { + "formula": "_contentSize = { width: parseFloat(Spirit.Width), height: parseFloat(Spirit.Height) }", + "rationale": "直接读精灵自身 Width/Height 字段作为 _contentSize; gameabc.min.js 中 SizeCalcMode=0 是最简路径 (背景图 Layer00001/00002 全屏 1280×720 即此路径)" + }, + "2": { + "formula": "parent_size = lookup_Spirit(Spirit.SizeParentID)._contentSize; _contentSize = { width: parent_size.width * Spirit.SizeRelaWidth / 10000, height: parent_size.height * Spirit.SizeRelaHeight / 10000 }; 当 WidthCalcMode=0 时回退到 Spirit.Width (parseFloat), 当 WidthCalcMode=1 时用 SizeRelaWidth 公式; HeightCalcMode 同理", + "rationale": "Layer00001 '友乐背景' (SizeCalcMode=2, SizeParentID=21, SizeRelaWidth=10000) / Layer00002 '微信登录背景' (SizeCalcMode=2, SizeParentID=20, SizeRelaWidth=10000) 全量采样: 全部 WidthCalcMode=1, HeightCalcMode=1, SizeRelaWidth=10000, SizeRelaHeight=10000; 即按 100% 比例继承父 spirit 尺寸; 这种 pattern 用于 '充满父容器' 的背景 (1280×720 全屏背景), SizeParentID 指向另一个 1280×720 的 spirit" + } + }, + "width_calc_mode": { + "0": "use Spirit.Width directly (直接读 Width)", + "1": "use parent_size.width * Spirit.SizeRelaWidth / 10000 (按父尺寸比例计算)" + }, + "height_calc_mode": { + "0": "use Spirit.Height directly (直接读 Height)", + "1": "use parent_size.height * Spirit.SizeRelaHeight / 10000 (按父尺寸比例计算)" }, "coord_self_pos": { - "_note": "原 X, Y 是精灵左上角绝对屏幕坐标 (基于 1280x720);Cocos 用中心锚点", + "_note": "原 X, Y 是精灵左上角绝对屏幕坐标 (基于 1280x720); Cocos 用中心锚点", "cocos_lpos_x": "original_X + original_W/2 - 640", - "cocos_lpos_y": "360 - (original_Y + original_H/2)" + "cocos_lpos_y": "360 - (original_Y + original_H/2)", + "coord_self_pos_mode": { + "_note": "CoordSelfPos=0 在 55 个 Layer 全量中占绝对多数; CoordSelfPos > 0 的精灵 (例如相对父节点的偏移) 暂未观察到案例, 公式留 follow-up", + "0": "use X/Y 作为相对父节点偏移 (直接读, 见 coord_self_pos 公式)" + } }, "anchor_point": { "cocos_default": { "x": 0.5, "y": 0.5 }, - "original_default": "左上角 (0, 0)" + "original_default": "左上角 (0, 0)", + "evidence": "Login_Layer '渠道logo' 节点 (原 734,143,59,148) → Cocos _lpos (123.5, 143, 0) 1:1 匹配, 锚点 (0.5, 0.5) 验证通过" } -} \ No newline at end of file +} diff --git a/cocoscreator_projects/framework-tests/legacy-layer-migration/spirit-type-reverse.test.mjs b/cocoscreator_projects/framework-tests/legacy-layer-migration/spirit-type-reverse.test.mjs new file mode 100644 index 0000000..5a70872 --- /dev/null +++ b/cocoscreator_projects/framework-tests/legacy-layer-migration/spirit-type-reverse.test.mjs @@ -0,0 +1,30 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; + +const SEMANTICS = JSON.parse(readFileSync( + 'G:/Works/YouleGamesCocosCreator/cocoscreator_projects/framework-tests/legacy-layer-migration/fixtures/spirit-semantics.json', + 'utf8' +)); + +// 逆向 gameabc.min.js 已完成: SpiritType 4 = cc.EditBox, 5 = cc.Sprite (SLICED) +// 数据源权威; spirit_types component 字段非 unknown 即视为已逆向. + +test('SpiritType 4 有明确组件映射(非 unknown)', () => { + assert.notEqual(SEMANTICS.spirit_types['4'].component, 'unknown', + 'SpiritType 4 必须反向出组件映射;填入 component 字段。逆向证据:phoneInfo Layer00029 含 type-4 spirit (FrameText=12 等) → YouleNexus Group106_PhoneVerify.prefab 含 cc.EditBox'); +}); + +test('SpiritType 5 有明确组件映射(非 unknown)', () => { + assert.notEqual(SEMANTICS.spirit_types['5'].component, 'unknown', + 'SpiritType 5 必须反向出组件映射;填入 component 字段。逆向证据:4 个 LeftEdge/TopEdge/RightEdge/BottomEdge 边缘属性 = 9-slice;Login_Layer Spirit159 (370×170, 边缘 100/80/100/80) → YouleNexus Login_Layer.prefab 中唯一 cc.Sprite._type=1 (_contentSize 370×170)'); +}); + +test('spirit-semantics.json 是合法 JSON 含 spirit_types 5 项(Type 0/1/3/4/5 全部已映射)', () => { + assert.equal(Object.keys(SEMANTICS.spirit_types).length, 5); + // 全部 5 项 component 必须非 unknown + for (const [type, entry] of Object.entries(SEMANTICS.spirit_types)) { + assert.notEqual(entry.component, 'unknown', + `SpiritType ${type} 仍有 unknown; 需逆向补 component`); + } +});