fix(stage-ui-live2d): support CJK paths in Live2D archives (#2219)

This commit is contained in:
Neko
2026-08-04 22:05:40 +08:00
committed by GitHub
parent 2abb1ac9ff
commit 5a56770f1c
3 changed files with 153 additions and 4 deletions
@@ -47,6 +47,17 @@ function createShisihangshiSettingsText(): string {
})
}
function createCjkPathSettingsText(): string {
return JSON.stringify({
Version: 3,
FileReferences: {
Moc: '测试角色.moc3',
Textures: ['中文纹理/texture_00.png'],
},
Groups: [],
})
}
const appleDoubleHeader = new Uint8Array([0, 5, 22, 7, 0, 2, 0, 0, 77, 97, 99, 32, 79, 83, 32, 88])
describe('live2d zip loader settings sanitization', () => {
@@ -83,6 +94,46 @@ describe('live2d zip loader settings sanitization', () => {
])
})
it('loads a zip model whose settings and resources use CJK paths', async () => {
await import('./live2d-zip-loader')
const { FileLoader, Live2DModel, ZipLoader } = await import('pixi-live2d-display/cubism4')
const zip = new JSZip()
zip.file('中文路径模型/测试角色.model3.json', createCjkPathSettingsText())
zip.file('中文路径模型/测试角色.moc3', new Uint8Array([77, 79, 67, 51]))
zip.file('中文路径模型/中文纹理/texture_00.png', new Uint8Array([1, 2, 3]))
const zipBytes = await zip.generateAsync({ type: 'uint8array' })
const reader = await JSZip.loadAsync(await blobFromBytes(zipBytes).arrayBuffer())
const settings = await ZipLoader.createSettings(reader)
const files = Object.assign(await ZipLoader.unzip(reader, settings), { settings })
const objectUrl = `zip://test/${settings.url}`
Object.assign(settings, { _objectURL: objectUrl })
// ROOT CAUSE:
//
// FileLoader encoded each webkitRelativePath before comparing it with settings.resolveURL().
// CJK names therefore became percent-encoded on only one side of the comparison.
//
// settings.resolveURL('测试角色.moc3') !== encodeURI(file.webkitRelativePath)
//
// The loader now keeps both sides as decoded archive paths, matching its unzip and upload stages.
const context = {
source: files,
options: {},
live2dModel: new Live2DModel(),
}
try {
await expect(FileLoader.factory(context, async () => {})).resolves.toBeUndefined()
}
finally {
for (const resourceUrl of Object.values(FileLoader.filesMap[objectUrl] ?? {}))
URL.revokeObjectURL(resourceUrl)
delete FileLoader.filesMap[objectUrl]
}
})
it('loads a zip model when a macOS AppleDouble settings sidecar is present before the real settings file', async () => {
await import('./live2d-zip-loader')
const { ZipLoader } = await import('pixi-live2d-display/cubism4')
@@ -134,7 +185,51 @@ describe('live2d zip loader settings sanitization', () => {
const settings = await FileLoader.createSettings(files)
expect(settings.physics).toBeUndefined()
expect(() => settings.validateFiles(files.map(file => encodeURI(file.webkitRelativePath)))).not.toThrow()
expect(() => settings.validateFiles(files.map(file => file.webkitRelativePath))).not.toThrow()
})
it('loads an OPFS-restored file directory whose settings and resources use CJK paths', async () => {
await import('./live2d-zip-loader')
const { FileLoader, Live2DModel } = await import('pixi-live2d-display/cubism4')
const files = [
fileWithRelativePath(
createCjkPathSettingsText(),
'测试角色.model3.json',
'中文路径模型/测试角色.model3.json',
),
fileWithRelativePath(
new Uint8Array([77, 79, 67, 51]),
'测试角色.moc3',
'中文路径模型/测试角色.moc3',
),
fileWithRelativePath(
new Uint8Array([1, 2, 3]),
'texture_00.png',
'中文路径模型/中文纹理/texture_00.png',
),
]
const existingObjectUrls = new Set(Object.keys(FileLoader.filesMap))
const context = {
source: files,
options: {},
live2dModel: new Live2DModel(),
}
try {
await expect(FileLoader.factory(context, async () => {})).resolves.toBeUndefined()
}
finally {
for (const objectUrl of Object.keys(FileLoader.filesMap)) {
if (existingObjectUrls.has(objectUrl))
continue
for (const resourceUrl of Object.values(FileLoader.filesMap[objectUrl] ?? {}))
URL.revokeObjectURL(resourceUrl)
URL.revokeObjectURL(objectUrl)
delete FileLoader.filesMap[objectUrl]
}
}
})
it('loads an OPFS-restored file directory when a macOS AppleDouble settings sidecar is present before the real settings file', async () => {
+54
View File
@@ -23,6 +23,15 @@ index 0d40d5d40533881d154e3c2cdda9b360c57eaaa6..8dbdb1b36e2da42da264dc3533523e61
if (!settingsFile) {
throw new TypeError("Settings file not found");
}
@@ -1411,7 +1411,7 @@ FileLoader.factory = (context, next) => __async(void 0, null, function* () {
} else if (!settings._objectURL) {
throw new Error('"_objectURL" must be specified in ModelSettings');
}
- settings.validateFiles(files.map((file) => encodeURI(file.webkitRelativePath)));
+ settings.validateFiles(files.map((file) => file.webkitRelativePath));
yield _FileLoader.upload(files, settings);
settings.resolveURL = function(url2) {
return _FileLoader.resolveURL(this._objectURL, url2);
@@ -1458,7 +1458,7 @@ const _ZipLoader = class {
static createSettings(reader) {
return __async(this, null, function* () {
@@ -45,6 +54,15 @@ index c3db490f8dbfdd63ad40648fe0e0325604f22e95..4b452db0b109ba7deaddf397ef351ae7
if (!settingsFile) {
throw new TypeError("Settings file not found");
}
@@ -1411,7 +1411,7 @@ var __async = (__this, __arguments, generator) => {
} else if (!settings._objectURL) {
throw new Error('"_objectURL" must be specified in ModelSettings');
}
- settings.validateFiles(files.map((file) => encodeURI(file.webkitRelativePath)));
+ settings.validateFiles(files.map((file) => file.webkitRelativePath));
yield _FileLoader.upload(files, settings);
settings.resolveURL = function(url) {
return _FileLoader.resolveURL(this._objectURL, url);
@@ -1458,7 +1458,7 @@ var __async = (__this, __arguments, generator) => {
static createSettings(reader) {
return __async(this, null, function* () {
@@ -67,6 +85,15 @@ index f21619f5794c00542bb2a9df340e50d4453a0367..38bd069a5d1ec37380c376c2ff2d7fed
if (!settingsFile) {
throw new TypeError("Settings file not found");
}
@@ -5050,7 +5050,7 @@ FileLoader.factory = (context, next) => __async(void 0, null, function* () {
} else if (!settings._objectURL) {
throw new Error('"_objectURL" must be specified in ModelSettings');
}
- settings.validateFiles(files.map((file) => encodeURI(file.webkitRelativePath)));
+ settings.validateFiles(files.map((file) => file.webkitRelativePath));
yield _FileLoader.upload(files, settings);
settings.resolveURL = function(url2) {
return _FileLoader.resolveURL(this._objectURL, url2);
@@ -5097,7 +5097,7 @@ const _ZipLoader = class {
static createSettings(reader) {
return __async(this, null, function* () {
@@ -89,6 +116,15 @@ index 03c1fb2b32b3b908a8b0e604aa3ee50aa9130169..99fc587e3ca7e6f433099e13f33e0e48
if (!settingsFile) {
throw new TypeError("Settings file not found");
}
@@ -5050,7 +5050,7 @@ var __async = (__this, __arguments, generator) => {
} else if (!settings._objectURL) {
throw new Error('"_objectURL" must be specified in ModelSettings');
}
- settings.validateFiles(files.map((file) => encodeURI(file.webkitRelativePath)));
+ settings.validateFiles(files.map((file) => file.webkitRelativePath));
yield _FileLoader.upload(files, settings);
settings.resolveURL = function(url) {
return _FileLoader.resolveURL(this._objectURL, url);
@@ -5097,7 +5097,7 @@ var __async = (__this, __arguments, generator) => {
static createSettings(reader) {
return __async(this, null, function* () {
@@ -111,6 +147,15 @@ index f969d304f346c4e420b6798dbd4dec81b902a5e4..7e6b4d3ef53f1ea0648030899de9debd
if (!settingsFile) {
throw new TypeError("Settings file not found");
}
@@ -1411,7 +1411,7 @@ FileLoader.factory = (context, next) => __async(void 0, null, function* () {
} else if (!settings._objectURL) {
throw new Error('"_objectURL" must be specified in ModelSettings');
}
- settings.validateFiles(files.map((file) => encodeURI(file.webkitRelativePath)));
+ settings.validateFiles(files.map((file) => file.webkitRelativePath));
yield _FileLoader.upload(files, settings);
settings.resolveURL = function(url2) {
return _FileLoader.resolveURL(this._objectURL, url2);
@@ -1458,7 +1458,7 @@ const _ZipLoader = class {
static createSettings(reader) {
return __async(this, null, function* () {
@@ -133,6 +178,15 @@ index bc3d800c45889979190175cc88cdd8251feb9b5e..d1a660708ff81e1379f173709a32c6f4
if (!settingsFile) {
throw new TypeError("Settings file not found");
}
@@ -1411,7 +1411,7 @@ var __async = (__this, __arguments, generator) => {
} else if (!settings._objectURL) {
throw new Error('"_objectURL" must be specified in ModelSettings');
}
- settings.validateFiles(files.map((file) => encodeURI(file.webkitRelativePath)));
+ settings.validateFiles(files.map((file) => file.webkitRelativePath));
yield _FileLoader.upload(files, settings);
settings.resolveURL = function(url) {
return _FileLoader.resolveURL(this._objectURL, url);
@@ -1458,7 +1458,7 @@ var __async = (__this, __arguments, generator) => {
static createSettings(reader) {
return __async(this, null, function* () {
+3 -3
View File
@@ -1207,7 +1207,7 @@ patchedDependencies:
hash: 4bbfdca823ab48b74086e6e7d4b2f9baf5ee7c0ba9aee0c279b3c91c50bfd797
path: patches/mineflayer-pathfinder.patch
pixi-live2d-display:
hash: 122ac09349321d5bfe9d9817aa095cd1c9c4132af86345aa9e27ba8b63dadf2c
hash: 7881eb52793f46a10d6c04d62b1799a3d0ef79f83edc0283777baa6db339ed6d
path: patches/pixi-live2d-display.patch
sponsorkit@17.1.0:
hash: f5887da52a29ae85732e01644d05c0c274231fe76b95aebeafbfab6dc4444f88
@@ -4745,7 +4745,7 @@ importers:
version: 4.2.0(5af797a0b5bd919a06578c24a7479046)
pixi-live2d-display:
specifier: 'catalog:'
version: 0.4.0(patch_hash=122ac09349321d5bfe9d9817aa095cd1c9c4132af86345aa9e27ba8b63dadf2c)(0736f5bd67d2e4de7e9fff36ad63fa2e)
version: 0.4.0(patch_hash=7881eb52793f46a10d6c04d62b1799a3d0ef79f83edc0283777baa6db339ed6d)(0736f5bd67d2e4de7e9fff36ad63fa2e)
three:
specifier: 'catalog:'
version: 0.184.0
@@ -33525,7 +33525,7 @@ snapshots:
- '@pixi/settings'
- '@pixi/utils'
pixi-live2d-display@0.4.0(patch_hash=122ac09349321d5bfe9d9817aa095cd1c9c4132af86345aa9e27ba8b63dadf2c)(0736f5bd67d2e4de7e9fff36ad63fa2e):
pixi-live2d-display@0.4.0(patch_hash=7881eb52793f46a10d6c04d62b1799a3d0ef79f83edc0283777baa6db339ed6d)(0736f5bd67d2e4de7e9fff36ad63fa2e):
dependencies:
'@pixi/core': 6.5.10(@pixi/constants@6.5.10)(@pixi/extensions@6.5.10)(@pixi/math@6.5.10)(@pixi/runner@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))(@pixi/ticker@6.5.10(@pixi/extensions@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))(@pixi/utils@6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))
'@pixi/display': 6.5.10(@pixi/constants@6.5.10)(@pixi/math@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10))(@pixi/utils@6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)))