/** * 小游戏 / 真机包体可能裁剪 sp.SkeletonData 上的部分实例方法,但引擎在设置 skeletonData、 * _updateSkeletonData、部分 Inspector 枚举更新时仍会调用它们。 * * 这里**一次性**在原型与资源实例上补齐「赋值 skeletonData 相关链路」可能用到的 API, * 避免每出现一种 `xxx is not a function` 再补一处。(实现需与 engine `cocos/spine/skeleton-data.ts` 行为一致。) * * 已覆盖:resetEnums、isEmpty、getRuntimeData、getSkinsEnum、getAnimsEnum * sp.Skeleton 原型:resetEnums(少数版本会从组件侧调用) */ import { Enum, error, murmurhash2_32_gc, sp } from 'cc'; function noop() { // 运行态无编辑器枚举 UI,空实现即可 } function mergedUUIDForSkeletonData(this: any): string { const textures = this.textures || []; const hashContent = [this._atlasText ?? '', ...textures.map((t: { getId: () => string }) => t.getId())].join(''); const id = this._uuid ?? this.uuid ?? ''; return `${id}${murmurhash2_32_gc(hashContent, 668)}`; } function skeletonDataGetRuntimeData(this: any, quiet?: boolean): any { if (this._skeletonCache) { return this._skeletonCache; } if (!(this.textures && this.textures.length > 0) && this.textureNames && this.textureNames.length > 0) { if (!quiet) { error(`${this.name} no textures found!`); } return null; } const wasm = sp.spine?.wasmUtil as typeof sp.spine.wasmUtil | undefined; if (!wasm || typeof wasm.querySpineSkeletonDataByUUID !== 'function') { return null; } const heap = wasm.wasm?.HEAPU8; if (!heap) { return null; } const uuid = mergedUUIDForSkeletonData.call(this); let spData: any; try { spData = wasm.querySpineSkeletonDataByUUID(uuid); } catch { spData = null; } if (spData) { this._skeletonCache = spData; } else { const texturesArr = this.textures || []; const size = texturesArr.length; const textureUUIDs: string[] = []; for (let i = 0; i < size; ++i) { const tex = texturesArr[i]; textureUUIDs.push(tex.uuid || tex.getId()); } const names = this.textureNames || []; if (this._skeletonJson) { try { const jsonStr = typeof this.skeletonJsonStr === 'string' ? this.skeletonJsonStr : JSON.stringify(this._skeletonJson); this._skeletonCache = wasm.createSpineSkeletonDataWithJson(jsonStr, this._atlasText ?? '', names, textureUUIDs); wasm.registerSpineSkeletonDataWithUUID(this._skeletonCache, uuid); } catch (e) { if (!quiet) { console.warn('[SpineRuntimePatch] createSpineSkeletonDataWithJson failed', this.name, e); } return null; } } else { const native = this._nativeAsset; if (!native) { return null; } const rawData = new Uint8Array(native); const byteSize = rawData.length; if (byteSize === 0) { return null; } // 与 engine 一致:先拷到 wasm 堆再解码;任一步失败都要 freeStoreMemory,否则堆状态异常会触发后续 memory access out of bounds const ptr = wasm.createStoreMemory(byteSize); try { const wasmMem = heap.subarray(ptr, ptr + byteSize); wasmMem.set(rawData); this._skeletonCache = wasm.createSpineSkeletonDataWithBinary( byteSize, this._atlasText ?? '', names, textureUUIDs ); wasm.registerSpineSkeletonDataWithUUID(this._skeletonCache, uuid); } catch (e) { if (!quiet) { console.warn( '[SpineRuntimePatch] createSpineSkeletonDataWithBinary failed (资源与 Spine 运行时版本不匹配或二进制损坏时常见)', this.name, e ); } this._skeletonCache = null; return null; } finally { try { wasm.freeStoreMemory(); } catch { /* ignore */ } } } } return this._skeletonCache; } function skeletonDataIsEmpty(this: any): boolean { const atlas = this._atlasText; const atlasLen = typeof atlas === 'string' ? atlas.length : 0; return atlasLen === 0 && !this._skeletonJson && !this._nativeAsset; } function skinNameToKey(name: unknown): string { if (typeof name === 'string') return name; if (name && typeof (name as { str?: string }).str === 'string') return (name as { str: string }).str; return String(name ?? ''); } /** 与 engine skeleton-data.ts getSkinsEnum() 一致 */ function skeletonDataGetSkinsEnum(this: any): Record | null { if (this._skinsEnum) { return this._skinsEnum; } const sd = this.getRuntimeData(true); if (!sd) return null; const skins = sd.skins; if (!skins || skins.length === 0) { this._skinsEnum = Enum({}); return this._skinsEnum; } const enumDef: Record = {}; for (let i = 0; i < skins.length; i++) { const key = skinNameToKey(skins[i]?.name); enumDef[key] = i; } this._skinsEnum = Enum(enumDef); return this._skinsEnum; } /** 与 engine skeleton-data.ts getAnimsEnum() 一致 */ function skeletonDataGetAnimsEnum(this: any): Record | null { if (this._animsEnum && Object.keys(this._animsEnum).length > 1) { return this._animsEnum; } const sd = this.getRuntimeData(true); if (!sd) return null; const anims = sd.animations; if (!anims || anims.length === 0) { const empty = Enum({ ' ': 0 }); this._animsEnum = empty; return this._animsEnum; } const enumDef: Record = { ' ': 0 }; for (let i = 0; i < anims.length; i++) { const key = skinNameToKey(anims[i]?.name); enumDef[key] = i + 1; } this._animsEnum = Enum(enumDef); return this._animsEnum; } /** 与 sp.SkeletonData 上可能被裁掉的方法一一对应 */ const SKELETON_DATA_FALLBACKS: ReadonlyArray<{ name: string; impl: (...args: any[]) => any }> = [ { name: 'resetEnums', impl: noop }, { name: 'isEmpty', impl: skeletonDataIsEmpty }, { name: 'getRuntimeData', impl: skeletonDataGetRuntimeData }, { name: 'getSkinsEnum', impl: skeletonDataGetSkinsEnum }, { name: 'getAnimsEnum', impl: skeletonDataGetAnimsEnum }, ]; /** * 给 sp.SkeletonData / sp.Skeleton 打齐运行时可能被裁掉的方法(可重复调用)。 */ export function patchSpineSkeletonDataRuntime(): void { try { const sdp = sp.SkeletonData?.prototype as unknown as Record | undefined; if (sdp) { for (const { name, impl } of SKELETON_DATA_FALLBACKS) { if (typeof sdp[name] !== 'function') { sdp[name] = impl; } } } const skp = sp.Skeleton?.prototype as { resetEnums?: unknown } | undefined; if (skp && typeof skp.resetEnums !== 'function') { skp.resetEnums = noop; } } catch { /* ignore */ } } /** * 赋值 skeletonData 前调用:先打原型,再处理实例。 * * 第二次进局等资源从缓存取出时,实例上常见 `resetEnums: null` / 非函数占位,照样会遮住原型链, * 而 sp.Skeleton 的 setter 首句是 `value.resetEnums()`,表现为「第一次正常、第二次 t.resetEnums is not a function」。 * * 策略: * 1. 删掉可配置、且值为 undefined / null / 非函数的自有「方法名」占位(不动 accessor 描述符)。 * 2. 按顺序对 SKELETON_DATA_FALLBACKS 五项做实例兜底:若仍拿不到可调用方法则 defineProperty(优先继承的原型方法,不解构覆盖)。 */ export function ensureSpineSkeletonDataRuntime(asset: unknown): void { if (asset == null) return; patchSpineSkeletonDataRuntime(); const a = asset as any; for (const { name } of SKELETON_DATA_FALLBACKS) { const own = Object.getOwnPropertyDescriptor(a, name); if (!own || own.configurable === false || !('value' in own)) continue; const v = own.value; if (v === undefined || v === null || typeof v !== 'function') { try { delete a[name]; } catch { /* ignore */ } } } for (const { name, impl } of SKELETON_DATA_FALLBACKS) { if (typeof a[name] === 'function') continue; try { Object.defineProperty(a, name, { value: impl, writable: true, configurable: true, enumerable: false, }); } catch { try { a[name] = impl; } catch { /* ignore */ } } } } /** 兼容旧名:历史代码与 GameController 等仍引用此名 */ export const patchSpineSkeletonResetEnums = patchSpineSkeletonDataRuntime; /** 兼容旧名:LangSpine / SpineLoader / SetSpine 等仍引用此名 */ export const ensureSpineAssetResetEnums = ensureSpineSkeletonDataRuntime; patchSpineSkeletonDataRuntime();