You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
258 lines
9.5 KiB
258 lines
9.5 KiB
/**
|
|
* 小游戏 / 真机包体可能裁剪 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<string, number> | 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<string, number> = {};
|
|
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<string, number> | 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<string, number> = { ' ': 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<string, unknown> | 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();
|
|
|