mirror of
https://github.com/Heretek-AI/GDevelop-Heretek.git
synced 2026-10-11 21:09:52 +00:00
Migrate Spine extension to @esotericsoftware/spine-pixi-v7 (#8595)
- Replace the third-party `[email protected]` with the official Esoteric Software runtime for PixiJS v7 (`@esotericsoftware/[email protected]`). - The new runtime only fully supports skeletons exported from **Spine 4.2**. Older exports (e.g. 4.0) may partially render but mesh deformations won't work due to a Spine JSON schema change (`deform` → `attachments`).
This commit is contained in:
@@ -54,8 +54,9 @@ module.exports = {
|
||||
.addDefaultBehavior('OpacityCapability::OpacityBehavior')
|
||||
.addDefaultBehavior('AnimatableCapability::AnimatableBehavior')
|
||||
.setIncludeFile('Extensions/Spine/spineruntimeobject.js')
|
||||
.addIncludeFile('Extensions/Spine/spine-pixi-v7/spine-pixi-v7-pre.js')
|
||||
.addIncludeFile('Extensions/Spine/spine-pixi-v7/spine-pixi-v7.js')
|
||||
.addIncludeFile('Extensions/Spine/spineruntimeobject-pixi-renderer.js')
|
||||
.addIncludeFile('Extensions/Spine/pixi-spine/pixi-spine.js')
|
||||
.addIncludeFile('Extensions/Spine/managers/pixi-spine-atlas-manager.js')
|
||||
.addIncludeFile('Extensions/Spine/managers/pixi-spine-manager.js')
|
||||
.setCategory('Advanced')
|
||||
@@ -280,7 +281,7 @@ module.exports = {
|
||||
const { PIXI, RenderedInstance, gd } = objectsRenderingService;
|
||||
|
||||
class RenderedSpineInstance extends RenderedInstance {
|
||||
/** @type {pixi_spine.Spine | null} */
|
||||
/** @type {spine.Spine | null} */
|
||||
_spine = null;
|
||||
/** @type {PIXI.Sprite} */
|
||||
_placeholder;
|
||||
@@ -467,7 +468,8 @@ module.exports = {
|
||||
// if custom size is set it will be reinitialized in update method
|
||||
spine.scale.set(1, 1);
|
||||
spine.state.setAnimation(0, source, shouldLoop);
|
||||
spine.state.tracks[0].trackTime = 0;
|
||||
const newTrack = spine.state.tracks[0];
|
||||
if (newTrack) newTrack.trackTime = 0;
|
||||
spine.update(0);
|
||||
spine.autoUpdate = false;
|
||||
}
|
||||
@@ -497,49 +499,34 @@ module.exports = {
|
||||
gd.SpineObjectConfiguration
|
||||
);
|
||||
this._pixiResourcesLoader
|
||||
.getSpineData(this._project, object.getSpineResourceName())
|
||||
.then((spineDataOrLoadingError) => {
|
||||
.createSpine(this._project, object.getSpineResourceName())
|
||||
.then((spineInstance) => {
|
||||
if (this._wasDestroyed) return;
|
||||
if (this._spine) this._pixiObject.removeChild(this._spine);
|
||||
|
||||
if (!spineDataOrLoadingError.skeleton) {
|
||||
console.error(
|
||||
'Unable to load Spine (' +
|
||||
(spineDataOrLoadingError.loadingErrorReason ||
|
||||
'Unknown reason') +
|
||||
')',
|
||||
spineDataOrLoadingError.loadingError
|
||||
);
|
||||
if (!spineInstance) {
|
||||
this._spine = null;
|
||||
this._placeholder.alpha = 255;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this._spine = new PIXI.Spine(spineDataOrLoadingError.skeleton);
|
||||
this._spine = spineInstance;
|
||||
|
||||
// Apply the default skin if configured.
|
||||
const skinName = object.getSkinName();
|
||||
if (skinName && this._spine) {
|
||||
try {
|
||||
this._spine.skeleton.setSkinByName(skinName);
|
||||
this._spine.skeleton.setSlotsToSetupPose();
|
||||
} catch (skinError) {
|
||||
console.warn(
|
||||
'Unable to set skin "' + skinName + '":',
|
||||
skinError
|
||||
);
|
||||
}
|
||||
const skinName = object.getSkinName();
|
||||
if (skinName) {
|
||||
try {
|
||||
spineInstance.skeleton.setSkinByName(skinName);
|
||||
spineInstance.skeleton.setSlotsToSetupPose();
|
||||
} catch (skinError) {
|
||||
console.warn(
|
||||
'Unable to set skin "' + skinName + '":',
|
||||
skinError
|
||||
);
|
||||
}
|
||||
|
||||
this._pixiObject.addChild(this._spine);
|
||||
this._placeholder.alpha = 0;
|
||||
} catch (error) {
|
||||
console.error('Exception while loading Spine.', error);
|
||||
this._spine = null;
|
||||
this._placeholder.alpha = 255;
|
||||
return;
|
||||
}
|
||||
|
||||
this._pixiObject.addChild(spineInstance);
|
||||
this._placeholder.alpha = 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,33 +4,24 @@
|
||||
* This project is released under the MIT License.
|
||||
*/
|
||||
namespace gdjs {
|
||||
/**
|
||||
* The callback called when a text that was requested is loaded (or an error occurred).
|
||||
* @category Resources > Spine
|
||||
*/
|
||||
export type SpineAtlasManagerRequestCallback = (
|
||||
error: Error | null,
|
||||
content?: pixi_spine.TextureAtlas
|
||||
) => void;
|
||||
|
||||
const atlasKinds: ResourceKind[] = ['atlas'];
|
||||
|
||||
/**
|
||||
* AtlasManager loads atlas files with pixi loader, using the "atlas" resources
|
||||
* registered in the game resources and process them to Pixi TextureAtlas.
|
||||
* SpineAtlasManager loads `.atlas` files via the official `@esotericsoftware/spine-pixi-v7`
|
||||
* Pixi atlas loader, sharing texture pages with the engine's ImageManager.
|
||||
*
|
||||
* The loader is auto-registered by the `spine-pixi-v7` IIFE bundle. We simply prepare
|
||||
* the asset metadata (`data.images`) so that PIXI.Assets binds atlas pages to the
|
||||
* already-loaded base textures instead of fetching them again.
|
||||
*
|
||||
* Contrary to audio/fonts, text files are loaded asynchronously, when requested.
|
||||
* You should properly handle errors, and give the developer/player a way to know
|
||||
* that loading failed.
|
||||
* @category Resources > Spine
|
||||
*/
|
||||
export class SpineAtlasManager implements gdjs.ResourceManager {
|
||||
private _imageManager: ImageManager;
|
||||
private _resourceLoader: ResourceLoader;
|
||||
private _loadedSpineAtlases =
|
||||
new gdjs.ResourceCache<pixi_spine.TextureAtlas>();
|
||||
private _loadedSpineAtlases = new gdjs.ResourceCache<spine.TextureAtlas>();
|
||||
private _loadingSpineAtlases = new gdjs.ResourceCache<
|
||||
Promise<pixi_spine.TextureAtlas>
|
||||
Promise<spine.TextureAtlas>
|
||||
>();
|
||||
|
||||
/**
|
||||
@@ -49,8 +40,8 @@ namespace gdjs {
|
||||
return atlasKinds;
|
||||
}
|
||||
|
||||
async processResource(resourceName: string): Promise<void> {
|
||||
// Do nothing because pixi-spine parses resources by itself.
|
||||
async processResource(_resourceName: string): Promise<void> {
|
||||
// The spine-pixi-v7 atlas loader parses the resource itself.
|
||||
}
|
||||
|
||||
async loadResource(resourceName: string): Promise<void> {
|
||||
@@ -58,88 +49,60 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns promisified loaded atlas resource if it is available, loads it otherwise.
|
||||
*
|
||||
* @param resourceName The name of resource to load.
|
||||
* Returns a cached promise resolving to the loaded atlas, loading it if needed.
|
||||
*/
|
||||
getOrLoad(resourceName: string): Promise<pixi_spine.TextureAtlas> {
|
||||
getOrLoad(resourceName: string): Promise<spine.TextureAtlas> {
|
||||
const resource = this._getAtlasResource(resourceName);
|
||||
|
||||
if (!resource) {
|
||||
return Promise.reject(
|
||||
`Unable to find atlas for resource '${resourceName}'.`
|
||||
new Error(`Unable to find atlas for resource '${resourceName}'.`)
|
||||
);
|
||||
}
|
||||
|
||||
let loadingPromise = this._loadingSpineAtlases.get(resource);
|
||||
|
||||
if (!loadingPromise) {
|
||||
loadingPromise = new Promise<pixi_spine.TextureAtlas>(
|
||||
(resolve, reject) => {
|
||||
const onLoad: SpineAtlasManagerRequestCallback = (
|
||||
error,
|
||||
content
|
||||
) => {
|
||||
if (error) {
|
||||
return reject(
|
||||
`Error while preloading a spine atlas resource: ${error}`
|
||||
);
|
||||
}
|
||||
if (!content) {
|
||||
return reject(
|
||||
`Cannot reach texture atlas for resource '${resourceName}'.`
|
||||
);
|
||||
}
|
||||
|
||||
resolve(content);
|
||||
};
|
||||
|
||||
this.load(resource, onLoad);
|
||||
}
|
||||
);
|
||||
|
||||
this._loadingSpineAtlases.set(resource, loadingPromise);
|
||||
const cachedAtlas = this._loadedSpineAtlases.get(resource);
|
||||
if (cachedAtlas) {
|
||||
return Promise.resolve(cachedAtlas);
|
||||
}
|
||||
|
||||
const inflight = this._loadingSpineAtlases.get(resource);
|
||||
if (inflight) {
|
||||
return inflight;
|
||||
}
|
||||
|
||||
const loadingPromise = this._load(resource).then((atlas) => {
|
||||
this._loadedSpineAtlases.set(resource, atlas);
|
||||
return atlas;
|
||||
});
|
||||
this._loadingSpineAtlases.set(resource, loadingPromise);
|
||||
return loadingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load specified atlas resource and pass it to callback once it is loaded.
|
||||
*
|
||||
* @param resource The data of resource to load.
|
||||
* @param callback The callback to pass atlas to it once it is loaded.
|
||||
*/
|
||||
load(
|
||||
resource: ResourceData,
|
||||
callback: SpineAtlasManagerRequestCallback
|
||||
): void {
|
||||
private async _load(resource: ResourceData): Promise<spine.TextureAtlas> {
|
||||
const game = this._resourceLoader.getRuntimeGame();
|
||||
const embeddedResourcesNames = game.getEmbeddedResourcesNames(
|
||||
resource.name
|
||||
);
|
||||
|
||||
if (!embeddedResourcesNames.length)
|
||||
return callback(
|
||||
new Error(`${resource.name} do not have image metadata!`)
|
||||
);
|
||||
if (!embeddedResourcesNames.length) {
|
||||
throw new Error(`${resource.name} does not have image metadata!`);
|
||||
}
|
||||
|
||||
const images = embeddedResourcesNames.reduce<{
|
||||
[key: string]: PIXI.Texture;
|
||||
[key: string]: PIXI.BaseTexture;
|
||||
}>((imagesMap, embeddedResourceName) => {
|
||||
const mappedResourceName = game.resolveEmbeddedResource(
|
||||
resource.name,
|
||||
embeddedResourceName
|
||||
);
|
||||
// The v7 atlas loader expects BaseTexture instances when sharing pages
|
||||
// with already-loaded textures.
|
||||
imagesMap[embeddedResourceName] =
|
||||
this._imageManager.getOrLoadPIXITexture(mappedResourceName);
|
||||
|
||||
this._imageManager.getOrLoadPIXITexture(
|
||||
mappedResourceName
|
||||
).baseTexture;
|
||||
return imagesMap;
|
||||
}, {});
|
||||
const onLoad = (atlas: pixi_spine.TextureAtlas) => {
|
||||
this._loadedSpineAtlases.set(resource, atlas);
|
||||
callback(null, atlas);
|
||||
};
|
||||
|
||||
const url = this._resourceLoader.getFullUrl(resource.file);
|
||||
const alias = url;
|
||||
|
||||
@@ -150,45 +113,33 @@ namespace gdjs {
|
||||
: 'anonymous',
|
||||
});
|
||||
PIXI.Assets.add({ alias, src: url, data: { images } });
|
||||
PIXI.Assets.load<pixi_spine.TextureAtlas | string>(alias).then(
|
||||
(atlas) => {
|
||||
/**
|
||||
* Ideally atlas of TextureAtlas should be passed here
|
||||
* but there is known issue in case of preloaded images (see https://github.com/pixijs/spine/issues/537)
|
||||
*
|
||||
* Here covered all possible ways to make it work fine if issue is fixed in pixi-spine or after migration to spine-pixi
|
||||
*/
|
||||
if (typeof atlas === 'string') {
|
||||
new pixi_spine.TextureAtlas(
|
||||
atlas,
|
||||
(textureName, textureCb) =>
|
||||
//@ts-ignore
|
||||
textureCb(images[textureName].baseTexture),
|
||||
onLoad
|
||||
);
|
||||
} else {
|
||||
onLoad(atlas);
|
||||
}
|
||||
}
|
||||
);
|
||||
return PIXI.Assets.load<spine.TextureAtlas>(alias);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given atlas resource was loaded (preloaded or loaded with `load`).
|
||||
* Check if the given atlas resource was loaded.
|
||||
* @param resourceName The name of the atlas resource.
|
||||
* @returns true if the content of the atlas resource is loaded, false otherwise.
|
||||
*/
|
||||
isLoaded(resourceName: string): boolean {
|
||||
return !!this._loadedSpineAtlases.getFromName(resourceName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Pixi TextureAtlas for the given resource that is already loaded (preloaded or loaded with `load`).
|
||||
* If the resource is not loaded, `null` will be returned.
|
||||
* @param resourceName The name of the atlas resource.
|
||||
* @returns the TextureAtlas of the atlas if loaded, `null` otherwise.
|
||||
* Returns the alias used to register the atlas in PIXI.Assets,
|
||||
* or null if the resource is not loaded.
|
||||
*/
|
||||
getAtlasTexture(resourceName: string): pixi_spine.TextureAtlas | null {
|
||||
getAtlasAlias(resourceName: string): string | null {
|
||||
const resource = this._getAtlasResource(resourceName);
|
||||
if (!resource) return null;
|
||||
return this._loadedSpineAtlases.get(resource)
|
||||
? this._resourceLoader.getFullUrl(resource.file)
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the loaded TextureAtlas for the given resource, if available.
|
||||
*/
|
||||
getAtlasTexture(resourceName: string): spine.TextureAtlas | null {
|
||||
return this._loadedSpineAtlases.getFromName(resourceName);
|
||||
}
|
||||
|
||||
@@ -198,9 +149,9 @@ namespace gdjs {
|
||||
? resource
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* To be called when the game is disposed.
|
||||
* Clear the Spine atlases loaded in this manager.
|
||||
*/
|
||||
dispose(): void {
|
||||
this._loadedSpineAtlases.clear();
|
||||
@@ -208,21 +159,16 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
unloadResource(resourceData: ResourceData): void {
|
||||
const loadedSpineAtlas = this._loadedSpineAtlases.getFromName(
|
||||
resourceData.name
|
||||
);
|
||||
if (loadedSpineAtlas) {
|
||||
loadedSpineAtlas.dispose();
|
||||
this._loadedSpineAtlases.delete(resourceData);
|
||||
const resource = this._getAtlasResource(resourceData.name);
|
||||
// PIXI.Assets.unload disposes the TextureAtlas and clears the cache entry,
|
||||
// preventing a stale atlas from being reused on the next load.
|
||||
if (resource) {
|
||||
const alias = this._resourceLoader.getFullUrl(resource.file);
|
||||
PIXI.Assets.unload(alias).catch(() => {});
|
||||
}
|
||||
|
||||
const loadingSpineAtlas = this._loadingSpineAtlases.getFromName(
|
||||
resourceData.name
|
||||
);
|
||||
if (loadingSpineAtlas) {
|
||||
loadingSpineAtlas.then((atl) => atl.dispose());
|
||||
this._loadingSpineAtlases.delete(resourceData);
|
||||
}
|
||||
this._loadedSpineAtlases.delete(resourceData);
|
||||
this._loadingSpineAtlases.delete(resourceData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,17 +9,32 @@ namespace gdjs {
|
||||
const resourceKinds: ResourceKind[] = ['spine'];
|
||||
|
||||
/**
|
||||
* SpineManager manages pixi spine skeleton data.
|
||||
* Aliases used to instantiate a Spine container via `Spine.from`.
|
||||
*/
|
||||
export type SpineAssetAliases = {
|
||||
skeletonAlias: string;
|
||||
atlasAlias: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* SpineManager loads Spine skeleton (`.json` / `.skel`) resources via the
|
||||
* official `@esotericsoftware/spine-pixi-v7` asset loader. The skeleton is
|
||||
* stored in the global `PIXI.Assets` cache under a stable alias, ready to be
|
||||
* consumed by `spine.Spine.from(...)`.
|
||||
* @category Resources > Spine
|
||||
*/
|
||||
export class SpineManager implements gdjs.ResourceManager {
|
||||
private _spineAtlasManager: SpineAtlasManager;
|
||||
private _resourceLoader: ResourceLoader;
|
||||
private _loadedSpines = new gdjs.ResourceCache<pixi_spine.ISkeletonData>();
|
||||
/**
|
||||
* Stores the asset aliases needed to construct a Spine container.
|
||||
* The atlas page textures are tracked separately by the SpineAtlasManager.
|
||||
*/
|
||||
private _loadedSpines = new gdjs.ResourceCache<SpineAssetAliases>();
|
||||
|
||||
/**
|
||||
* @param resourceLoader The resources loader of the game.
|
||||
* @param spineAtlasManager The resources data of the game.
|
||||
* @param spineAtlasManager The Spine atlas manager of the game.
|
||||
*/
|
||||
constructor(
|
||||
resourceLoader: gdjs.ResourceLoader,
|
||||
@@ -33,8 +48,8 @@ namespace gdjs {
|
||||
return resourceKinds;
|
||||
}
|
||||
|
||||
async processResource(resourceName: string): Promise<void> {
|
||||
// Do nothing because pixi-spine parses resources by itself.
|
||||
async processResource(_resourceName: string): Promise<void> {
|
||||
// The spine-pixi-v7 skeleton loader parses the resource itself.
|
||||
}
|
||||
|
||||
async loadResource(resourceName: string): Promise<void> {
|
||||
@@ -42,12 +57,12 @@ namespace gdjs {
|
||||
|
||||
if (!resource) {
|
||||
return logger.error(
|
||||
`Unable to find spine json for resource ${resourceName}.`
|
||||
`Unable to find spine skeleton for resource ${resourceName}.`
|
||||
);
|
||||
}
|
||||
|
||||
const url = this._resourceLoader.getFullUrl(resource.file);
|
||||
const alias = url;
|
||||
const skeletonUrl = this._resourceLoader.getFullUrl(resource.file);
|
||||
const skeletonAlias = skeletonUrl;
|
||||
|
||||
try {
|
||||
const game = this._resourceLoader.getRuntimeGame();
|
||||
@@ -55,10 +70,10 @@ namespace gdjs {
|
||||
resource.name
|
||||
);
|
||||
|
||||
// there should be exactly one file which is pointing to atlas
|
||||
// there should be exactly one embedded resource (the atlas)
|
||||
if (embeddedResourcesNames.length !== 1) {
|
||||
return logger.error(
|
||||
`Unable to find atlas metadata for resource spine json ${resourceName}.`
|
||||
`Unable to find atlas metadata for resource spine skeleton ${resourceName}.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,53 +81,47 @@ namespace gdjs {
|
||||
resource.name,
|
||||
embeddedResourcesNames[0]
|
||||
);
|
||||
const spineAtlas =
|
||||
await this._spineAtlasManager.getOrLoad(atlasResourceName);
|
||||
await this._spineAtlasManager.getOrLoad(atlasResourceName);
|
||||
const atlasAlias =
|
||||
this._spineAtlasManager.getAtlasAlias(atlasResourceName);
|
||||
|
||||
if (!atlasAlias) {
|
||||
return logger.error(
|
||||
`Atlas '${atlasResourceName}' was loaded but no alias is registered.`
|
||||
);
|
||||
}
|
||||
|
||||
PIXI.Assets.setPreferences({
|
||||
preferWorkers: false,
|
||||
crossOrigin: this._resourceLoader.checkIfCredentialsRequired(url)
|
||||
crossOrigin: this._resourceLoader.checkIfCredentialsRequired(
|
||||
skeletonUrl
|
||||
)
|
||||
? 'use-credentials'
|
||||
: 'anonymous',
|
||||
});
|
||||
PIXI.Assets.add({
|
||||
alias,
|
||||
src: url,
|
||||
data: { spineAtlas },
|
||||
});
|
||||
const loadedJson = await PIXI.Assets.load(alias);
|
||||
PIXI.Assets.add({ alias: skeletonAlias, src: skeletonUrl });
|
||||
await PIXI.Assets.load(skeletonAlias);
|
||||
|
||||
if (loadedJson.spineData) {
|
||||
this._loadedSpines.set(resource, loadedJson.spineData);
|
||||
} else {
|
||||
logger.error(
|
||||
`Loader cannot process spine resource ${resource.name} correctly.`
|
||||
);
|
||||
}
|
||||
this._loadedSpines.set(resource, { skeletonAlias, atlasAlias });
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error while preloading spine resource ${resource.name}: ${error}`
|
||||
);
|
||||
PIXI.Assets.unload(alias);
|
||||
await PIXI.Assets.unload(skeletonAlias).catch(() => {});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the object for the given resource that is already loaded (preloaded or loaded with `loadJson`).
|
||||
* If the resource is not loaded, `null` will be returned.
|
||||
*
|
||||
* @param resourceName The name of the spine skeleton.
|
||||
* @returns the spine skeleton if loaded, `null` otherwise.
|
||||
* Returns the asset aliases required to instantiate a Spine container,
|
||||
* or `null` if the resource is not loaded yet.
|
||||
*/
|
||||
getSpine(resourceName: string): pixi_spine.ISkeletonData | null {
|
||||
getSpineAliases(resourceName: string): SpineAssetAliases | null {
|
||||
return this._loadedSpines.getFromName(resourceName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given spine skeleton was loaded.
|
||||
* @param resourceName The name of the spine skeleton.
|
||||
* @returns true if the content of the spine skeleton is loaded, false otherwise.
|
||||
*/
|
||||
isSpineLoaded(resourceName: string): boolean {
|
||||
return !!this._loadedSpines.getFromName(resourceName);
|
||||
@@ -127,15 +136,23 @@ namespace gdjs {
|
||||
|
||||
/**
|
||||
* To be called when the game is disposed.
|
||||
* Clear the Spine skeleton data loaded in this manager.
|
||||
*/
|
||||
dispose(): void {
|
||||
this._loadedSpines.clear();
|
||||
}
|
||||
|
||||
unloadResource(resourceData: ResourceData): void {
|
||||
const loadedSpine = this._loadedSpines.get(resourceData);
|
||||
if (loadedSpine) {
|
||||
const aliases = this._loadedSpines.get(resourceData);
|
||||
if (aliases) {
|
||||
// Drop cached SkeletonData entries created by Spine.from for this skeleton.
|
||||
const skeletonCache = spine.Spine.skeletonCache;
|
||||
const cachePrefix = `${aliases.skeletonAlias}-${aliases.atlasAlias}-`;
|
||||
for (const cacheKey of Object.keys(skeletonCache)) {
|
||||
if (cacheKey.startsWith(cachePrefix)) {
|
||||
delete skeletonCache[cacheKey];
|
||||
}
|
||||
}
|
||||
PIXI.Assets.unload(aliases.skeletonAlias).catch(() => {});
|
||||
this._loadedSpines.delete(resourceData);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
+11
-11
@@ -1,11 +1,11 @@
|
||||
Spine Runtimes License Agreement
|
||||
Last updated February 20, 2024. Replaces all prior versions.
|
||||
|
||||
Copyright (c) 2013-2024, Esoteric Software LLC
|
||||
|
||||
Integration of the Spine Runtimes into software or otherwise creating derivative works of the Spine Runtimes is permitted under the terms and conditions of Section 2 of the Spine Editor License Agreement:
|
||||
http://esotericsoftware.com/spine-editor-license
|
||||
|
||||
Otherwise, it is permitted to integrate the Spine Runtimes into software or otherwise create derivative works of the Spine Runtimes (collectively, "Products"), provided that each user of the Products must obtain their own Spine Editor license and redistribution of the Products in any form must include this license and copyright notice.
|
||||
|
||||
THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
Spine Runtimes License Agreement
|
||||
Last updated February 20, 2024. Replaces all prior versions.
|
||||
|
||||
Copyright (c) 2013-2024, Esoteric Software LLC
|
||||
|
||||
Integration of the Spine Runtimes into software or otherwise creating derivative works of the Spine Runtimes is permitted under the terms and conditions of Section 2 of the Spine Editor License Agreement:
|
||||
http://esotericsoftware.com/spine-editor-license
|
||||
|
||||
Otherwise, it is permitted to integrate the Spine Runtimes into software or otherwise create derivative works of the Spine Runtimes (collectively, "Products"), provided that each user of the Products must obtain their own Spine Editor license and redistribution of the Products in any form must include this license and copyright notice.
|
||||
|
||||
THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -0,0 +1,10 @@
|
||||
"use strict";
|
||||
// Shim: map @pixi/* package requires to the global PIXI object so the esbuild
|
||||
// IIFE bundle can resolve its PixiJS dependencies in a browser <script> context.
|
||||
var require = (function (originalRequire) {
|
||||
return function shimmedRequire(id) {
|
||||
if (typeof id === "string" && id.indexOf("@pixi/") === 0) return PIXI;
|
||||
if (typeof originalRequire === "function") return originalRequire(id);
|
||||
throw Error('Dynamic require of "' + id + '" is not supported');
|
||||
};
|
||||
})(typeof require !== "undefined" ? require : undefined);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,35 +1,20 @@
|
||||
namespace gdjs {
|
||||
const isSpine = (obj: any): obj is pixi_spine.Spine =>
|
||||
obj instanceof pixi_spine.Spine;
|
||||
|
||||
// See https://github.com/pixijs/spine/issues/562
|
||||
// IPointAttachment is not declared and exported but its implementation does exist and it is used in runtime
|
||||
interface IPointAttachment extends pixi_spine.IVertexAttachment {
|
||||
computeWorldPosition(
|
||||
bone: pixi_spine.IBone,
|
||||
point: pixi_spine.Vector2
|
||||
): pixi_spine.Vector2;
|
||||
computeWorldRotation(bone: pixi_spine.IBone): number;
|
||||
}
|
||||
|
||||
interface IExtendedBone extends pixi_spine.IBone {
|
||||
scaleX: number;
|
||||
scaleY: number;
|
||||
rotation: number;
|
||||
parent: IExtendedBone | null;
|
||||
}
|
||||
const isSpine = (obj: any): obj is spine.Spine =>
|
||||
typeof spine !== 'undefined' && obj instanceof spine.Spine;
|
||||
|
||||
const isPointAttachment = (
|
||||
attachment: pixi_spine.IAttachment
|
||||
): attachment is IPointAttachment =>
|
||||
!!attachment && attachment.type === pixi_spine.AttachmentType.Point;
|
||||
attachment: spine.Attachment | null
|
||||
): attachment is spine.PointAttachment =>
|
||||
typeof spine !== 'undefined' &&
|
||||
!!attachment &&
|
||||
attachment instanceof spine.PointAttachment;
|
||||
|
||||
/**
|
||||
* @category Renderers > Spine
|
||||
*/
|
||||
export class SpineRuntimeObjectPixiRenderer {
|
||||
private _object: gdjs.SpineRuntimeObject;
|
||||
private _rendererObject: pixi_spine.Spine | PIXI.Container;
|
||||
private _rendererObject: spine.Spine | PIXI.Container;
|
||||
private _isAnimationComplete = true;
|
||||
|
||||
/**
|
||||
@@ -64,7 +49,7 @@ namespace gdjs {
|
||||
this._rendererObject.update(timeDelta);
|
||||
}
|
||||
|
||||
getRendererObject(): pixi_spine.Spine | PIXI.Container {
|
||||
getRendererObject(): spine.Spine | PIXI.Container {
|
||||
return this._rendererObject;
|
||||
}
|
||||
|
||||
@@ -146,33 +131,33 @@ namespace gdjs {
|
||||
setMixing(from: string, to: string, duration: number): void {
|
||||
if (!isSpine(this._rendererObject)) return;
|
||||
|
||||
this._rendererObject.stateData.setMix(from, to, duration);
|
||||
this._rendererObject.state.data.setMix(from, to, duration);
|
||||
}
|
||||
|
||||
setAnimation(animation: string, loop: boolean): void {
|
||||
if (isSpine(this._rendererObject)) {
|
||||
const onCompleteListener: pixi_spine.IAnimationStateListener = {
|
||||
complete: () => {
|
||||
this._isAnimationComplete = true;
|
||||
(this._rendererObject as pixi_spine.Spine).state.removeListener(
|
||||
onCompleteListener
|
||||
);
|
||||
},
|
||||
};
|
||||
if (!isSpine(this._rendererObject)) return;
|
||||
|
||||
this._isAnimationComplete = false;
|
||||
this._rendererObject.state.addListener(onCompleteListener);
|
||||
this._rendererObject.state.setAnimation(0, animation, loop);
|
||||
this._rendererObject.update(0);
|
||||
}
|
||||
const onCompleteListener: spine.AnimationStateListener = {
|
||||
complete: () => {
|
||||
this._isAnimationComplete = true;
|
||||
(this._rendererObject as spine.Spine).state.removeListener(
|
||||
onCompleteListener
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
this._isAnimationComplete = false;
|
||||
this._rendererObject.state.addListener(onCompleteListener);
|
||||
this._rendererObject.state.setAnimation(0, animation, loop);
|
||||
this._rendererObject.update(0);
|
||||
}
|
||||
|
||||
getAnimationDuration(sourceAnimationName: string) {
|
||||
getAnimationDuration(sourceAnimationName: string): number {
|
||||
if (!isSpine(this._rendererObject)) {
|
||||
return 0;
|
||||
}
|
||||
const animation =
|
||||
this._rendererObject.spineData.findAnimation(sourceAnimationName);
|
||||
this._rendererObject.skeleton.data.findAnimation(sourceAnimationName);
|
||||
return animation ? animation.duration : 0;
|
||||
}
|
||||
|
||||
@@ -184,9 +169,11 @@ namespace gdjs {
|
||||
if (tracks.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
// This should be fine because only 1 track is used.
|
||||
// Only one track is used.
|
||||
const track = tracks[0];
|
||||
// @ts-ignore TrackEntry.getAnimationTime is not exposed.
|
||||
if (!track) {
|
||||
return 0;
|
||||
}
|
||||
return track.getAnimationTime();
|
||||
}
|
||||
|
||||
@@ -199,6 +186,7 @@ namespace gdjs {
|
||||
return;
|
||||
}
|
||||
const track = tracks[0];
|
||||
if (!track) return;
|
||||
track.trackTime = time;
|
||||
}
|
||||
|
||||
@@ -216,12 +204,12 @@ namespace gdjs {
|
||||
getPointAttachmentPosition(
|
||||
attachmentName: string,
|
||||
slotName?: string
|
||||
): pixi_spine.Vector2 {
|
||||
): spine.Vector2 {
|
||||
if (!slotName) {
|
||||
slotName = attachmentName;
|
||||
}
|
||||
if (!isSpine(this._rendererObject)) {
|
||||
return new pixi_spine.Vector2(
|
||||
return new spine.Vector2(
|
||||
this._rendererObject.x,
|
||||
this._rendererObject.y
|
||||
);
|
||||
@@ -234,13 +222,17 @@ namespace gdjs {
|
||||
this._rendererObject
|
||||
);
|
||||
|
||||
return new PIXI.Matrix()
|
||||
const worldPoint = attachment.computeWorldPosition(
|
||||
slot.bone,
|
||||
new spine.Vector2()
|
||||
);
|
||||
const transformed = new PIXI.Matrix()
|
||||
.rotate(this._rendererObject.rotation)
|
||||
.scale(this._rendererObject.scale.x, this._rendererObject.scale.y)
|
||||
.translate(this._rendererObject.x, this._rendererObject.y)
|
||||
.apply(
|
||||
attachment.computeWorldPosition(slot.bone, new pixi_spine.Vector2())
|
||||
);
|
||||
.apply({ x: worldPoint.x, y: worldPoint.y });
|
||||
|
||||
return new spine.Vector2(transformed.x, transformed.y);
|
||||
}
|
||||
|
||||
getPointAttachmentRotation(
|
||||
@@ -262,8 +254,6 @@ namespace gdjs {
|
||||
this._rendererObject
|
||||
);
|
||||
|
||||
const bone = slot.bone as IExtendedBone;
|
||||
|
||||
if (isWorld) {
|
||||
return (
|
||||
gdjs.toDegrees(this._rendererObject.rotation) +
|
||||
@@ -271,19 +261,19 @@ namespace gdjs {
|
||||
);
|
||||
}
|
||||
|
||||
return bone.rotation;
|
||||
return slot.bone.rotation;
|
||||
}
|
||||
|
||||
getPointAttachmentScale(
|
||||
attachmentName: string,
|
||||
slotName?: string,
|
||||
isWorld?: boolean
|
||||
): pixi_spine.Vector2 {
|
||||
): spine.Vector2 {
|
||||
if (!slotName) {
|
||||
slotName = attachmentName;
|
||||
}
|
||||
if (!isSpine(this._rendererObject)) {
|
||||
return new pixi_spine.Vector2(
|
||||
return new spine.Vector2(
|
||||
this._rendererObject.scale.x,
|
||||
this._rendererObject.scale.y
|
||||
);
|
||||
@@ -296,29 +286,22 @@ namespace gdjs {
|
||||
this._rendererObject
|
||||
);
|
||||
|
||||
let scaleX = 1;
|
||||
let scaleY = 1;
|
||||
|
||||
const bone = slot.bone as IExtendedBone;
|
||||
|
||||
scaleX = bone.scaleX;
|
||||
scaleY = bone.scaleY;
|
||||
let scaleX = slot.bone.scaleX;
|
||||
let scaleY = slot.bone.scaleY;
|
||||
|
||||
if (isWorld) {
|
||||
let parent = bone.parent;
|
||||
let parent = slot.bone.parent;
|
||||
while (parent) {
|
||||
if (parent) {
|
||||
scaleX *= parent.scaleX;
|
||||
scaleY *= parent.scaleY;
|
||||
}
|
||||
scaleX *= parent.scaleX;
|
||||
scaleY *= parent.scaleY;
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
||||
scaleX = scaleX * this._rendererObject.scale.x;
|
||||
scaleY = scaleY * this._rendererObject.scale.y;
|
||||
scaleX *= this._rendererObject.scale.x;
|
||||
scaleY *= this._rendererObject.scale.y;
|
||||
}
|
||||
|
||||
return new pixi_spine.Vector2(scaleX, scaleY);
|
||||
return new spine.Vector2(scaleX, scaleY);
|
||||
}
|
||||
|
||||
setSkin(skinName: string): void {
|
||||
@@ -340,15 +323,17 @@ namespace gdjs {
|
||||
|
||||
getSkin(): string {
|
||||
if (!isSpine(this._rendererObject)) return '';
|
||||
return this._rendererObject.skeleton.skin.name;
|
||||
return this._rendererObject.skeleton.skin
|
||||
? this._rendererObject.skeleton.skin.name
|
||||
: '';
|
||||
}
|
||||
|
||||
getAvailableSkins(): string[] {
|
||||
if (!isSpine(this._rendererObject)) return [];
|
||||
return this._rendererObject.skeleton.data.skins.map((skin) => skin.name);
|
||||
return this._rendererObject.skeleton.data.skins.map((s) => s.name);
|
||||
}
|
||||
|
||||
private constructRendererObject(): pixi_spine.Spine | PIXI.Container {
|
||||
private constructRendererObject(): spine.Spine | PIXI.Container {
|
||||
const game = this.instanceContainer.getGame();
|
||||
const spineManager = game.getSpineManager();
|
||||
|
||||
@@ -359,16 +344,43 @@ namespace gdjs {
|
||||
return new PIXI.Container();
|
||||
}
|
||||
|
||||
return new pixi_spine.Spine(
|
||||
spineManager.getSpine(this._object.spineResourceName)!
|
||||
const aliases = spineManager.getSpineAliases(
|
||||
this._object.spineResourceName
|
||||
);
|
||||
if (!aliases) {
|
||||
return new PIXI.Container();
|
||||
}
|
||||
|
||||
try {
|
||||
const spineObject = spine.Spine.from({
|
||||
skeleton: aliases.skeletonAlias,
|
||||
atlas: aliases.atlasAlias,
|
||||
autoUpdate: false,
|
||||
});
|
||||
|
||||
const version = spineObject.skeleton.data.version;
|
||||
if (version && !version.startsWith('4.2')) {
|
||||
console.warn(
|
||||
`[Spine] Resource '${this._object.spineResourceName}' was exported with Spine ${version}. ` +
|
||||
`The runtime requires Spine 4.2. Animations may not work correctly.`
|
||||
);
|
||||
}
|
||||
|
||||
return spineObject;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Unable to instantiate Spine container for resource '${this._object.spineResourceName}':`,
|
||||
error
|
||||
);
|
||||
return new PIXI.Container();
|
||||
}
|
||||
}
|
||||
|
||||
private static getSlotAndAttachmentFromRenderObject(
|
||||
attachmentName: string,
|
||||
slotName: string,
|
||||
renderObject: pixi_spine.Spine
|
||||
): { slot: pixi_spine.ISlot; attachment: IPointAttachment } {
|
||||
renderObject: spine.Spine
|
||||
): { slot: spine.Slot; attachment: spine.PointAttachment } {
|
||||
const slot = renderObject.skeleton.findSlot(slotName);
|
||||
if (!slot) {
|
||||
throw new Error(
|
||||
|
||||
@@ -114,7 +114,7 @@ namespace gdjs {
|
||||
this.invalidateHitboxes();
|
||||
}
|
||||
|
||||
getRendererObject(): pixi_spine.Spine | PIXI.Container {
|
||||
getRendererObject(): spine.Spine | PIXI.Container {
|
||||
return this._renderer.getRendererObject();
|
||||
}
|
||||
|
||||
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
import * as pixi_spine from 'pixi-spine';
|
||||
|
||||
export = pixi_spine;
|
||||
export as namespace pixi_spine;
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import * as spine from '@esotericsoftware/spine-pixi-v7';
|
||||
|
||||
export = spine;
|
||||
export as namespace spine;
|
||||
Generated
+1506
-2537
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -15,7 +15,7 @@
|
||||
"lebab": "^3.1.0",
|
||||
"minimist": "^1.2.5",
|
||||
"patch-package": "^6.4.7",
|
||||
"pixi-spine": "4.0.4",
|
||||
"@esotericsoftware/spine-pixi-v7": "~4.2.116",
|
||||
"pixi.js": "7.4.2",
|
||||
"prettier": "3.4.2",
|
||||
"recursive-readdir": "^2.2.2",
|
||||
|
||||
@@ -61,7 +61,8 @@ const untransformedPaths = [
|
||||
'Extensions/TileMap/pako/dist/pako.min.js',
|
||||
'Extensions/TileMap/pixi-tilemap/dist/pixi-tilemap.umd.js',
|
||||
'Extensions/TileMap/helper/TileMapHelper.js',
|
||||
'Extensions/Spine/pixi-spine/pixi-spine.js',
|
||||
'Extensions/Spine/spine-pixi-v7/spine-pixi-v7-pre.js',
|
||||
'Extensions/Spine/spine-pixi-v7/spine-pixi-v7.js',
|
||||
].map((untransformedPath) => path.resolve(gdevelopRootPath, untransformedPath));
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,99 +3,92 @@ spineboy.png
|
||||
filter: Linear, Linear
|
||||
scale: 0.5
|
||||
crosshair
|
||||
bounds: 813, 160, 45, 45
|
||||
bounds: 352, 7, 45, 45
|
||||
eye-indifferent
|
||||
bounds: 569, 2, 47, 45
|
||||
bounds: 862, 105, 47, 45
|
||||
eye-surprised
|
||||
bounds: 643, 7, 47, 45
|
||||
rotate: 90
|
||||
bounds: 505, 79, 47, 45
|
||||
front-bracer
|
||||
bounds: 811, 51, 29, 40
|
||||
bounds: 826, 66, 29, 40
|
||||
front-fist-closed
|
||||
bounds: 807, 93, 38, 41
|
||||
bounds: 786, 65, 38, 41
|
||||
front-fist-open
|
||||
bounds: 815, 210, 43, 44
|
||||
bounds: 710, 51, 43, 44
|
||||
rotate: 90
|
||||
front-foot
|
||||
bounds: 706, 64, 63, 35
|
||||
rotate: 90
|
||||
bounds: 210, 6, 63, 35
|
||||
front-shin
|
||||
bounds: 80, 11, 41, 92
|
||||
bounds: 665, 128, 41, 92
|
||||
rotate: 90
|
||||
front-thigh
|
||||
bounds: 754, 12, 23, 56
|
||||
bounds: 2, 2, 23, 56
|
||||
rotate: 90
|
||||
front-upper-arm
|
||||
bounds: 618, 5, 23, 49
|
||||
bounds: 250, 205, 23, 49
|
||||
goggles
|
||||
bounds: 214, 20, 131, 83
|
||||
bounds: 665, 171, 131, 83
|
||||
gun
|
||||
bounds: 347, 14, 105, 102
|
||||
rotate: 90
|
||||
bounds: 798, 152, 105, 102
|
||||
head
|
||||
bounds: 80, 105, 136, 149
|
||||
bounds: 2, 27, 136, 149
|
||||
hoverboard-board
|
||||
bounds: 2, 8, 246, 76
|
||||
rotate: 90
|
||||
bounds: 2, 178, 246, 76
|
||||
hoverboard-thruster
|
||||
bounds: 478, 2, 30, 32
|
||||
bounds: 722, 96, 30, 32
|
||||
rotate: 90
|
||||
hoverglow-small
|
||||
bounds: 218, 117, 137, 38
|
||||
rotate: 90
|
||||
bounds: 275, 81, 137, 38
|
||||
mouth-grind
|
||||
bounds: 775, 80, 47, 30
|
||||
rotate: 90
|
||||
bounds: 614, 97, 47, 30
|
||||
mouth-oooo
|
||||
bounds: 779, 31, 47, 30
|
||||
rotate: 90
|
||||
bounds: 612, 65, 47, 30
|
||||
mouth-smile
|
||||
bounds: 783, 207, 47, 30
|
||||
rotate: 90
|
||||
bounds: 661, 64, 47, 30
|
||||
muzzle-glow
|
||||
bounds: 779, 4, 25, 25
|
||||
bounds: 382, 54, 25, 25
|
||||
muzzle-ring
|
||||
bounds: 451, 14, 25, 105
|
||||
bounds: 275, 54, 25, 105
|
||||
rotate: 90
|
||||
muzzle01
|
||||
bounds: 664, 60, 67, 40
|
||||
bounds: 911, 95, 67, 40
|
||||
rotate: 90
|
||||
muzzle02
|
||||
bounds: 580, 56, 68, 42
|
||||
rotate: 90
|
||||
bounds: 792, 108, 68, 42
|
||||
muzzle03
|
||||
bounds: 478, 36, 83, 53
|
||||
bounds: 956, 171, 83, 53
|
||||
rotate: 90
|
||||
muzzle04
|
||||
bounds: 533, 49, 75, 45
|
||||
rotate: 90
|
||||
bounds: 275, 7, 75, 45
|
||||
muzzle05
|
||||
bounds: 624, 56, 68, 38
|
||||
rotate: 90
|
||||
bounds: 140, 3, 68, 38
|
||||
neck
|
||||
bounds: 806, 8, 18, 21
|
||||
bounds: 250, 182, 18, 21
|
||||
portal-bg
|
||||
bounds: 258, 121, 133, 133
|
||||
bounds: 140, 43, 133, 133
|
||||
portal-flare1
|
||||
bounds: 690, 2, 56, 30
|
||||
rotate: 90
|
||||
bounds: 554, 65, 56, 30
|
||||
portal-flare2
|
||||
bounds: 510, 3, 57, 31
|
||||
bounds: 759, 112, 57, 31
|
||||
rotate: 90
|
||||
portal-flare3
|
||||
bounds: 722, 4, 58, 30
|
||||
rotate: 90
|
||||
bounds: 554, 97, 58, 30
|
||||
portal-shade
|
||||
bounds: 393, 121, 133, 133
|
||||
bounds: 275, 121, 133, 133
|
||||
portal-streaks1
|
||||
bounds: 528, 126, 126, 128
|
||||
bounds: 410, 126, 126, 128
|
||||
portal-streaks2
|
||||
bounds: 656, 129, 125, 125
|
||||
bounds: 538, 129, 125, 125
|
||||
rear-bracer
|
||||
bounds: 826, 13, 28, 36
|
||||
bounds: 857, 67, 28, 36
|
||||
rear-foot
|
||||
bounds: 743, 70, 57, 30
|
||||
rotate: 90
|
||||
bounds: 663, 96, 57, 30
|
||||
rear-shin
|
||||
bounds: 174, 14, 38, 89
|
||||
bounds: 414, 86, 38, 89
|
||||
rotate: 90
|
||||
rear-thigh
|
||||
bounds: 783, 158, 28, 47
|
||||
bounds: 756, 63, 28, 47
|
||||
rear-upper-arm
|
||||
bounds: 783, 136, 20, 44
|
||||
bounds: 60, 5, 20, 44
|
||||
rotate: 90
|
||||
torso
|
||||
bounds: 123, 13, 49, 90
|
||||
bounds: 905, 164, 49, 90
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 239 KiB After Width: | Height: | Size: 240 KiB |
@@ -5201,11 +5201,6 @@
|
||||
"name": "death",
|
||||
"source": "death"
|
||||
},
|
||||
{
|
||||
"loop": false,
|
||||
"name": "hit",
|
||||
"source": "hit"
|
||||
},
|
||||
{
|
||||
"loop": false,
|
||||
"name": "shoot",
|
||||
|
||||
@@ -172,7 +172,8 @@ module.exports = function (config) {
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/TileMap/collision/TransformedTileMap.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/TileMap/helper/TileMapHelper.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/TileMap/pako/dist/pako.min.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Spine/pixi-spine/pixi-spine.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Spine/spine-pixi-v7/spine-pixi-v7-pre.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Spine/spine-pixi-v7/spine-pixi-v7.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Spine/spineruntimeobject.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Spine/spineruntimeobject-pixi-renderer.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Spine/managers/*.js',
|
||||
|
||||
Generated
+25
-132
@@ -12,6 +12,7 @@
|
||||
"dependencies": {
|
||||
"@blueprintjs/core": "file:src/Utils/BlueprintJsPlaceholder",
|
||||
"@blueprintjs/icons": "file:src/Utils/BlueprintJsPlaceholder",
|
||||
"@esotericsoftware/spine-pixi-v7": "~4.2.116",
|
||||
"@lingui/react": "github:4ian/lingui-react#master",
|
||||
"@material-ui/core": "4.11.0",
|
||||
"@material-ui/icons": "4.9.1",
|
||||
@@ -31,7 +32,6 @@
|
||||
"lodash": "4.17.4",
|
||||
"node-require-function": "^1.2.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"pixi-spine": "4.0.4",
|
||||
"pixi.js-legacy": "7.4.2",
|
||||
"posthog-js": "1.275.2",
|
||||
"prop-types": "^15.5.10",
|
||||
@@ -3416,6 +3416,30 @@
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esotericsoftware/spine-core": {
|
||||
"version": "4.2.116",
|
||||
"resolved": "https://registry.npmjs.org/@esotericsoftware/spine-core/-/spine-core-4.2.116.tgz",
|
||||
"integrity": "sha512-MLaGxr933E5lcH23dFQtCkGNrwl0MRietVCuuanOlO50yCNALrYqH2VUTB12Wt1Zg999Okz+7UBVjpExJYL7hQ==",
|
||||
"license": "LicenseRef-LICENSE"
|
||||
},
|
||||
"node_modules/@esotericsoftware/spine-pixi-v7": {
|
||||
"version": "4.2.116",
|
||||
"resolved": "https://registry.npmjs.org/@esotericsoftware/spine-pixi-v7/-/spine-pixi-v7-4.2.116.tgz",
|
||||
"integrity": "sha512-sNs7++WcwXkmLiL86JzMP/KqGx304O8mbiHFXYLMCWwMUjyzovSwpVdpJnzLnmeX9SpQTxoyRS4QY1cHtsSW4Q==",
|
||||
"license": "LicenseRef-LICENSE",
|
||||
"dependencies": {
|
||||
"@esotericsoftware/spine-core": "4.2.116"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@pixi/assets": "^7.2.4",
|
||||
"@pixi/core": "^7.2.4",
|
||||
"@pixi/display": "^7.2.4",
|
||||
"@pixi/events": "^7.2.4",
|
||||
"@pixi/graphics": "^7.2.4",
|
||||
"@pixi/mesh": "^7.2.4",
|
||||
"@pixi/text": "^7.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@fal-works/esbuild-plugin-global-externals": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@fal-works/esbuild-plugin-global-externals/-/esbuild-plugin-global-externals-2.1.2.tgz",
|
||||
@@ -6576,88 +6600,6 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixi-spine/base": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@pixi-spine/base/-/base-4.0.6.tgz",
|
||||
"integrity": "sha512-m7tbwGF9LTWoDP3/p1lj7tPYUiZroFGJa3hqCpDT24oP0+qPwm81zhvOddcvKpm9WvyG7EahQfncayXMNm0n7A==",
|
||||
"peerDependencies": {
|
||||
"@pixi/core": "^7.0.0",
|
||||
"@pixi/display": "^7.0.0",
|
||||
"@pixi/graphics": "^7.0.0",
|
||||
"@pixi/mesh": "^7.0.0",
|
||||
"@pixi/mesh-extras": "^7.0.0",
|
||||
"@pixi/sprite": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixi-spine/loader-base": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@pixi-spine/loader-base/-/loader-base-4.0.6.tgz",
|
||||
"integrity": "sha512-sdbT1fbjHJFxESn8R2vulIyPu6xVBocJhrfs/kdpEWmsGUHTFdh7ANKaCqvXPvQh6wO5tgJNGhuLxtSu8VNzYg==",
|
||||
"peerDependencies": {
|
||||
"@pixi-spine/base": "^4.0.0",
|
||||
"@pixi/assets": " ^7.0.0",
|
||||
"@pixi/core": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixi-spine/loader-uni": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@pixi-spine/loader-uni/-/loader-uni-4.0.6.tgz",
|
||||
"integrity": "sha512-DCu4YRVXoC/BGyxVVlWiaFs/4H62PbM6vx6OiH/Xt5PYSvYSAFlM/GnYRabgvFD0W1iOHQxfY4F7jdz8SjNs4Q==",
|
||||
"peerDependencies": {
|
||||
"@pixi-spine/base": "^4.0.0",
|
||||
"@pixi-spine/loader-base": "^4.0.0",
|
||||
"@pixi-spine/runtime-3.7": "^4.0.0",
|
||||
"@pixi-spine/runtime-3.8": "^4.0.0",
|
||||
"@pixi-spine/runtime-4.1": "^4.0.0",
|
||||
"@pixi/assets": " ^7.0.0",
|
||||
"@pixi/core": "^7.0.0",
|
||||
"@pixi/display": "^7.0.0",
|
||||
"@pixi/graphics": "^7.0.0",
|
||||
"@pixi/mesh": "^7.0.0",
|
||||
"@pixi/mesh-extras": "^7.0.0",
|
||||
"@pixi/sprite": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixi-spine/runtime-3.7": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@pixi-spine/runtime-3.7/-/runtime-3.7-4.0.6.tgz",
|
||||
"integrity": "sha512-bl2I0PsECKKncfLjH2xUggNcnzRTVCBQGGLHLxhUJsiOqQ56VY5eFHPpkf91Pk2fZN6P9X+RNvZntW7Z5GKQRQ==",
|
||||
"license": "SEE SPINE-LICENSE",
|
||||
"peerDependencies": {
|
||||
"@pixi-spine/base": "^4.0.0",
|
||||
"@pixi/core": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixi-spine/runtime-3.8": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@pixi-spine/runtime-3.8/-/runtime-3.8-4.0.6.tgz",
|
||||
"integrity": "sha512-caHEFt7Iwzercec5cOI+Sf2tzOT4ejNAn6ZjBurWAEeXZ8YCaj41PTnlnJAh6ex80AP3WWFJcbnqcXV/6G0sMA==",
|
||||
"license": "SEE SPINE-LICENSE",
|
||||
"peerDependencies": {
|
||||
"@pixi-spine/base": "^4.0.0",
|
||||
"@pixi/core": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixi-spine/runtime-4.0": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@pixi-spine/runtime-4.0/-/runtime-4.0-4.0.6.tgz",
|
||||
"integrity": "sha512-C4aHtbpg0UNmCd/0zS2SQ96fdcrwpx0AkHBi3TO/JXbh6fM6DQOObV+EXtxTNIY8Y/ntJtqM3Rz5d1pmgY6d3g==",
|
||||
"license": "SEE SPINE-LICENSE",
|
||||
"peerDependencies": {
|
||||
"@pixi-spine/base": "^4.0.0",
|
||||
"@pixi/core": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixi-spine/runtime-4.1": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@pixi-spine/runtime-4.1/-/runtime-4.1-4.0.6.tgz",
|
||||
"integrity": "sha512-hiX6of8Usll/zIubGB5ufPY0JaBGt0Yvpc0zEXCtxyWCx3habp8XtPNta3MmEQUVChD0nX5yks8vtzxAahFJew==",
|
||||
"license": "SEE SPINE-LICENSE",
|
||||
"peerDependencies": {
|
||||
"@pixi-spine/base": "^4.0.0",
|
||||
"@pixi/core": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixi/accessibility": {
|
||||
"version": "7.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@pixi/accessibility/-/accessibility-7.4.2.tgz",
|
||||
@@ -11969,31 +11911,6 @@
|
||||
"@types/unist": "^2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/hoist-non-react-statics": {
|
||||
"version": "3.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz",
|
||||
"integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"hoist-non-react-statics": "^3.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/hoist-non-react-statics/node_modules/hoist-non-react-statics": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
||||
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"react-is": "^16.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/html-minifier-terser": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
|
||||
@@ -31228,30 +31145,6 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/pixi-spine": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/pixi-spine/-/pixi-spine-4.0.4.tgz",
|
||||
"integrity": "sha512-XRq1yARVoi4av7RXnd9+P37SWI9+e4/f5yTScZPJGB+sY5VcRYN6BYkBQ+y8nUKI1aJIjlms9z+pGxqikm+eFQ==",
|
||||
"license": "SEE SPINE-LICENSE",
|
||||
"dependencies": {
|
||||
"@pixi-spine/base": "^4.0.3",
|
||||
"@pixi-spine/loader-base": "^4.0.4",
|
||||
"@pixi-spine/loader-uni": "^4.0.3",
|
||||
"@pixi-spine/runtime-3.7": "^4.0.3",
|
||||
"@pixi-spine/runtime-3.8": "^4.0.3",
|
||||
"@pixi-spine/runtime-4.0": "^4.0.3",
|
||||
"@pixi-spine/runtime-4.1": "^4.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@pixi/assets": "^7.0.0",
|
||||
"@pixi/core": "^7.0.0",
|
||||
"@pixi/display": "^7.0.0",
|
||||
"@pixi/graphics": "^7.0.0",
|
||||
"@pixi/mesh": "^7.0.0",
|
||||
"@pixi/mesh-extras": "^7.0.0",
|
||||
"@pixi/sprite": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pixi.js": {
|
||||
"version": "7.4.2",
|
||||
"resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-7.4.2.tgz",
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
"lodash": "4.17.4",
|
||||
"node-require-function": "^1.2.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"pixi-spine": "4.0.4",
|
||||
"@esotericsoftware/spine-pixi-v7": "~4.2.116",
|
||||
"pixi.js-legacy": "7.4.2",
|
||||
"posthog-js": "1.275.2",
|
||||
"prop-types": "^15.5.10",
|
||||
@@ -103,14 +103,14 @@
|
||||
"@material-ui/styles": "4.11.5"
|
||||
},
|
||||
"overrides": {
|
||||
"pixi-spine": {
|
||||
"@esotericsoftware/spine-pixi-v7": {
|
||||
"@pixi/assets": "7.4.2",
|
||||
"@pixi/core": "7.4.2",
|
||||
"@pixi/display": "7.4.2",
|
||||
"@pixi/events": "7.4.2",
|
||||
"@pixi/graphics": "7.4.2",
|
||||
"@pixi/mesh": "7.4.2",
|
||||
"@pixi/mesh-extras": "7.4.2",
|
||||
"@pixi/sprite": "7.4.2"
|
||||
"@pixi/text": "7.4.2"
|
||||
},
|
||||
"react": "$react",
|
||||
"react-dom": "$react-dom",
|
||||
|
||||
@@ -96,14 +96,25 @@ const loadProjectEventsFunctionsExtension = (
|
||||
eventsFunctionsExtension: gdEventsFunctionsExtension,
|
||||
options: OptionsForGeneration
|
||||
): Promise<void> => {
|
||||
const extensionName = eventsFunctionsExtension.getName();
|
||||
const phase = options.skipCodeGeneration ? 'metadata' : 'codegen';
|
||||
return generateEventsFunctionExtension(
|
||||
project,
|
||||
eventsFunctionsExtension,
|
||||
options
|
||||
).then(extension => {
|
||||
gd.JsPlatform.get().addNewExtension(extension);
|
||||
extension.delete();
|
||||
});
|
||||
).then(
|
||||
extension => {
|
||||
gd.JsPlatform.get().addNewExtension(extension);
|
||||
extension.delete();
|
||||
},
|
||||
error => {
|
||||
console.error(
|
||||
`[EventsFunctionsExtensionsLoader] Failed to load extension "${extensionName}" (phase=${phase}):`,
|
||||
error
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -313,16 +324,26 @@ const generateFreeFunction = (
|
||||
codeGenerationContext: CodeGenerationContext
|
||||
): Promise<void> => {
|
||||
const metadataDeclarationHelper = new gd.MetadataDeclarationHelper();
|
||||
const { functionMetadata } = generateFreeFunctionMetadata(
|
||||
project,
|
||||
extension,
|
||||
eventsFunctionsExtension,
|
||||
eventsFunction,
|
||||
// $FlowFixMe[incompatible-type]
|
||||
options,
|
||||
codeGenerationContext,
|
||||
metadataDeclarationHelper
|
||||
);
|
||||
let functionMetadata;
|
||||
try {
|
||||
({ functionMetadata } = generateFreeFunctionMetadata(
|
||||
project,
|
||||
extension,
|
||||
eventsFunctionsExtension,
|
||||
eventsFunction,
|
||||
// $FlowFixMe[incompatible-type]
|
||||
options,
|
||||
codeGenerationContext,
|
||||
metadataDeclarationHelper
|
||||
));
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[EventsFunctionsExtensionsLoader] Failed to generate metadata for free function "${eventsFunctionsExtension.getName()}::${eventsFunction.getName()}":`,
|
||||
error
|
||||
);
|
||||
metadataDeclarationHelper.delete();
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!options.skipCodeGeneration) {
|
||||
const includeFiles = new gd.SetString();
|
||||
@@ -333,16 +354,28 @@ const generateFreeFunction = (
|
||||
eventsFunction,
|
||||
codeGenerationContext.codeNamespacePrefix
|
||||
);
|
||||
const code = eventsFunctionsExtensionCodeGenerator.generateFreeEventsFunctionCompleteCode(
|
||||
eventsFunctionsExtension,
|
||||
eventsFunction,
|
||||
codeNamespace,
|
||||
includeFiles,
|
||||
// For now, always generate functions for runtime (this disables
|
||||
// generation of profiling for groups (see EventsCodeGenerator))
|
||||
// as extensions generated can be used either for preview or export.
|
||||
true
|
||||
);
|
||||
let code;
|
||||
try {
|
||||
code = eventsFunctionsExtensionCodeGenerator.generateFreeEventsFunctionCompleteCode(
|
||||
eventsFunctionsExtension,
|
||||
eventsFunction,
|
||||
codeNamespace,
|
||||
includeFiles,
|
||||
// For now, always generate functions for runtime (this disables
|
||||
// generation of profiling for groups (see EventsCodeGenerator))
|
||||
// as extensions generated can be used either for preview or export.
|
||||
true
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[EventsFunctionsExtensionsLoader] Failed to generate code for free function "${eventsFunctionsExtension.getName()}::${eventsFunction.getName()}":`,
|
||||
error
|
||||
);
|
||||
includeFiles.delete();
|
||||
eventsFunctionsExtensionCodeGenerator.delete();
|
||||
metadataDeclarationHelper.delete();
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Add any include file required by the function to the list
|
||||
// of include files for this function (so that when used, the "dependencies"
|
||||
|
||||
@@ -85,14 +85,18 @@ export default (React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
const spineData = await PixiResourcesLoader.getSpineData(
|
||||
const spine = await PixiResourcesLoader.createSpine(
|
||||
project,
|
||||
spineResourceName
|
||||
);
|
||||
if (cancelled) return;
|
||||
if (cancelled) {
|
||||
if (spine) spine.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
if (spineData.skeleton && spineData.skeleton.skins) {
|
||||
setSkinNames(spineData.skeleton.skins.map(skin => skin.name));
|
||||
if (spine && spine.skeleton.data.skins) {
|
||||
setSkinNames(spine.skeleton.data.skins.map(skin => skin.name));
|
||||
spine.destroy();
|
||||
} else {
|
||||
setSkinNames([]);
|
||||
}
|
||||
|
||||
@@ -24,9 +24,7 @@ import { makeDragSourceAndDropTarget } from '../../UI/DragAndDrop/DragSourceAndD
|
||||
import { DragHandleIcon } from '../../UI/DragHandle';
|
||||
import DropIndicator from '../../UI/SortableVirtualizedItemList/DropIndicator';
|
||||
import GDevelopThemeContext from '../../UI/Theme/GDevelopThemeContext';
|
||||
import PixiResourcesLoader, {
|
||||
type SpineDataOrLoadingError,
|
||||
} from '../../ObjectsRendering/PixiResourcesLoader';
|
||||
import PixiResourcesLoader from '../../ObjectsRendering/PixiResourcesLoader';
|
||||
import useAlertDialog from '../../UI/Alert/useAlertDialog';
|
||||
import { PropertyResourceSelector, PropertyField } from './PropertyFields';
|
||||
import AlertMessage from '../../UI/AlertMessage';
|
||||
@@ -102,8 +100,24 @@ const SpineEditor = ({
|
||||
{}
|
||||
);
|
||||
|
||||
const [spineData, setSpineData] = React.useState<SpineDataOrLoadingError>({
|
||||
skeleton: null,
|
||||
type SpineLoadingState = {|
|
||||
isLoaded: boolean,
|
||||
loadingError: ?Error,
|
||||
loadingErrorReason:
|
||||
| null
|
||||
| 'invalid-spine-resource'
|
||||
| 'missing-texture-atlas-name'
|
||||
| 'spine-resource-loading-error'
|
||||
| 'invalid-atlas-resource'
|
||||
| 'missing-texture-resources'
|
||||
| 'atlas-resource-loading-error',
|
||||
|};
|
||||
|
||||
const [spineVersion, setSpineVersion] = React.useState<?string>(null);
|
||||
const [skinNames, setSkinNames] = React.useState<Array<string>>([]);
|
||||
const [animationNames, setAnimationNames] = React.useState<Array<string>>([]);
|
||||
const [spineData, setSpineData] = React.useState<SpineLoadingState>({
|
||||
isLoaded: false,
|
||||
loadingError: null,
|
||||
loadingErrorReason: null,
|
||||
});
|
||||
@@ -115,41 +129,78 @@ const SpineEditor = ({
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
const spineData = await PixiResourcesLoader.getSpineData(
|
||||
const result = await PixiResourcesLoader.getSpineData(
|
||||
project,
|
||||
spineResourceName
|
||||
);
|
||||
if (cancelled) return;
|
||||
|
||||
setSpineData(spineData);
|
||||
if (!result.aliases) {
|
||||
setSpineData({
|
||||
isLoaded: false,
|
||||
loadingError: result.loadingError,
|
||||
loadingErrorReason: result.loadingErrorReason,
|
||||
});
|
||||
setSourceSelectOptions([]);
|
||||
setSkinNames([]);
|
||||
setSpineVersion(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (spineData.skeleton) {
|
||||
const spine = await PixiResourcesLoader.createSpine(
|
||||
project,
|
||||
spineResourceName
|
||||
);
|
||||
if (cancelled) {
|
||||
if (spine) spine.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
if (spine) {
|
||||
const skeletonData = spine.skeleton.data;
|
||||
setSpineVersion(skeletonData.version || null);
|
||||
const animations = skeletonData.animations.map(
|
||||
animation => animation.name
|
||||
);
|
||||
setAnimationNames(animations);
|
||||
setSourceSelectOptions(
|
||||
spineData.skeleton.animations.map(animation => (
|
||||
animations.map(animationName => (
|
||||
<SelectOption
|
||||
key={animation.name}
|
||||
value={animation.name}
|
||||
label={animation.name}
|
||||
key={animationName}
|
||||
value={animationName}
|
||||
label={animationName}
|
||||
shouldNotTranslate
|
||||
/>
|
||||
))
|
||||
);
|
||||
setSkinNames(skeletonData.skins.map(skin => skin.name));
|
||||
setSpineData({
|
||||
isLoaded: true,
|
||||
loadingError: null,
|
||||
loadingErrorReason: null,
|
||||
});
|
||||
spine.destroy();
|
||||
} else {
|
||||
setSourceSelectOptions([]);
|
||||
setSkinNames([]);
|
||||
setAnimationNames([]);
|
||||
setSpineData({
|
||||
isLoaded: false,
|
||||
loadingError: null,
|
||||
loadingErrorReason: 'spine-resource-loading-error',
|
||||
});
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
},
|
||||
[project, spineResourceName, setSourceSelectOptions]
|
||||
);
|
||||
|
||||
const skinsSelectOptionsList: Array<string> = React.useMemo(
|
||||
() => {
|
||||
if (spineData.skeleton && spineData.skeleton.skins) {
|
||||
return spineData.skeleton.skins.map(skin => skin.name);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
[spineData.skeleton]
|
||||
);
|
||||
const skinsSelectOptionsList: Array<string> = skinNames;
|
||||
|
||||
const skinName = spineConfiguration.getSkinName();
|
||||
|
||||
@@ -186,8 +237,7 @@ const SpineEditor = ({
|
||||
|
||||
const scanNewAnimations = React.useCallback(
|
||||
() => {
|
||||
const { skeleton } = spineData;
|
||||
if (!skeleton) return;
|
||||
if (!spineData.isLoaded || animationNames.length === 0) return;
|
||||
|
||||
setNameErrors({});
|
||||
|
||||
@@ -199,19 +249,19 @@ const SpineEditor = ({
|
||||
);
|
||||
|
||||
let hasAddedAnimation = false;
|
||||
for (const resourceAnimation of skeleton.animations) {
|
||||
if (animationSources.includes(resourceAnimation.name)) {
|
||||
for (const resourceAnimationName of animationNames) {
|
||||
if (animationSources.includes(resourceAnimationName)) {
|
||||
continue;
|
||||
}
|
||||
const newAnimationName = spineConfiguration.hasAnimationNamed(
|
||||
resourceAnimation.name
|
||||
resourceAnimationName
|
||||
)
|
||||
? ''
|
||||
: resourceAnimation.name;
|
||||
: resourceAnimationName;
|
||||
|
||||
const newAnimation = new gd.SpineAnimation();
|
||||
newAnimation.setName(newAnimationName);
|
||||
newAnimation.setSource(resourceAnimation.name);
|
||||
newAnimation.setSource(resourceAnimationName);
|
||||
spineConfiguration.addAnimation(newAnimation);
|
||||
newAnimation.delete();
|
||||
hasAddedAnimation = true;
|
||||
@@ -239,7 +289,8 @@ const SpineEditor = ({
|
||||
},
|
||||
[
|
||||
forceUpdate,
|
||||
spineData,
|
||||
spineData.isLoaded,
|
||||
animationNames,
|
||||
spineConfiguration,
|
||||
onObjectUpdated,
|
||||
onSizeUpdated,
|
||||
@@ -391,7 +442,7 @@ const SpineEditor = ({
|
||||
projectScopedContainersAccessor={projectScopedContainersAccessor}
|
||||
onChange={onChangeSpineResourceName}
|
||||
/>
|
||||
{!spineData.skeleton && spineData.loadingErrorReason ? (
|
||||
{!spineData.isLoaded && spineData.loadingErrorReason ? (
|
||||
<AlertMessage kind="error">
|
||||
{spineData.loadingErrorReason === 'invalid-spine-resource' ? (
|
||||
<Trans>
|
||||
@@ -427,6 +478,15 @@ const SpineEditor = ({
|
||||
) : null}
|
||||
</AlertMessage>
|
||||
) : null}
|
||||
{spineVersion && !spineVersion.startsWith('4.2') ? (
|
||||
<AlertMessage kind="warning">
|
||||
<Trans>
|
||||
This Spine resource was exported with Spine {spineVersion}. The
|
||||
runtime requires data exported from Spine 4.2. Animations may not
|
||||
work correctly — please re-export from Spine 4.2.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
) : null}
|
||||
<Text size="block-title" noMargin>
|
||||
<Trans>Default size</Trans>
|
||||
</Text>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// @flow
|
||||
import 'pixi-spine';
|
||||
import RenderedUnknownInstance from './Renderers/RenderedUnknownInstance';
|
||||
import RenderedSpriteInstance from './Renderers/RenderedSpriteInstance';
|
||||
import RenderedTiledSpriteInstance from './Renderers/RenderedTiledSpriteInstance';
|
||||
@@ -16,7 +15,7 @@ import ResourcesLoader from '../ResourcesLoader';
|
||||
import RenderedInstance from './Renderers/RenderedInstance';
|
||||
import Rendered3DInstance from './Renderers/Rendered3DInstance';
|
||||
import * as PIXI_LEGACY from 'pixi.js-legacy';
|
||||
import * as PIXI_SPINE from 'pixi-spine';
|
||||
import * as PIXI_SPINE from '@esotericsoftware/spine-pixi-v7';
|
||||
import * as THREE from 'three';
|
||||
import * as SkeletonUtils from 'three/examples/jsm/utils/SkeletonUtils';
|
||||
import optionalRequire from '../Utils/OptionalRequire';
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
// @flow
|
||||
import 'pixi-spine';
|
||||
import slugs from 'slugs';
|
||||
import axios from 'axios';
|
||||
import * as PIXI from 'pixi.js-legacy';
|
||||
import * as PIXI_SPINE from 'pixi-spine';
|
||||
import { ISkeleton, TextureAtlas } from 'pixi-spine';
|
||||
import { Spine, TextureAtlas } from '@esotericsoftware/spine-pixi-v7';
|
||||
import * as THREE from 'three';
|
||||
import { GLTFLoader, GLTF } from 'three/examples/jsm/loaders/GLTFLoader';
|
||||
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
|
||||
@@ -17,6 +15,7 @@ const gd: libGDevelop = global.gd;
|
||||
type SpineTextureAtlasOrLoadingError = {|
|
||||
// $FlowFixMe[value-as-type]
|
||||
textureAtlas: ?TextureAtlas,
|
||||
atlasAlias: ?string,
|
||||
loadingError: ?Error,
|
||||
loadingErrorReason:
|
||||
| null
|
||||
@@ -25,9 +24,17 @@ type SpineTextureAtlasOrLoadingError = {|
|
||||
| 'atlas-resource-loading-error',
|
||||
|};
|
||||
|
||||
/**
|
||||
* Aliases registered into `PIXI.Assets`. Pass them to `Spine.from(...)` to
|
||||
* instantiate a Spine container.
|
||||
*/
|
||||
export type SpineAliases = {|
|
||||
skeletonAlias: string,
|
||||
atlasAlias: string,
|
||||
|};
|
||||
|
||||
export type SpineDataOrLoadingError = {|
|
||||
// $FlowFixMe[value-as-type]
|
||||
skeleton: ?ISkeleton,
|
||||
aliases: ?SpineAliases,
|
||||
loadingError: ?Error,
|
||||
loadingErrorReason:
|
||||
| null
|
||||
@@ -492,25 +499,21 @@ export default class PixiResourcesLoader {
|
||||
}
|
||||
if (spineAtlasPromises[resourceName]) {
|
||||
// TODO: only unload if no other resources pointing to the same Spine Atlas?
|
||||
|
||||
await PIXI.Assets.unload(resourceName).catch(async () => {
|
||||
// Workaround:
|
||||
// This is an expected error due to https://github.com/pixijs/spine/issues/537 issue (read comments
|
||||
// and search the other mentions to this issue in the codebase):
|
||||
// A string, instead of a TextureAtlas, is stored as the loaded atlas resource (which is the root cause of this exception).
|
||||
// pixi-spine considers it acts on a TextureAtlas and tries to call dispose on it that causes a TypeError.
|
||||
const { textureAtlas } = await spineAtlasPromises[resourceName];
|
||||
if (textureAtlas) {
|
||||
textureAtlas.dispose(); // Workaround by doing `dispose` ourselves.
|
||||
}
|
||||
});
|
||||
await PIXI.Assets.unload(resourceName).catch(() => {});
|
||||
delete spineAtlasPromises[resourceName];
|
||||
|
||||
// Also reload any resource embedding this resource:
|
||||
await this._reloadEmbedderResources(project, resourceName, 'spine');
|
||||
}
|
||||
if (spineDataPromises[resourceName]) {
|
||||
// TODO: only unload if no other resources pointing to the same Spine Data?
|
||||
// Drop SkeletonData entries cached by Spine.from for this skeleton alias,
|
||||
// so that next instantiation re-parses with the (possibly updated) atlas.
|
||||
const cachePrefix = `${resourceName}-`;
|
||||
for (const cacheKey of Object.keys(Spine.skeletonCache)) {
|
||||
if (cacheKey.startsWith(cachePrefix)) {
|
||||
delete Spine.skeletonCache[cacheKey];
|
||||
}
|
||||
}
|
||||
|
||||
await PIXI.Assets.unload(resourceName);
|
||||
delete spineDataPromises[resourceName];
|
||||
@@ -918,10 +921,14 @@ export default class PixiResourcesLoader {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Pixi spine texture atlas of the specified resource names.
|
||||
* Pre-load the texture atlas for a Spine asset and register it in PIXI.Assets
|
||||
* under the resource name as alias.
|
||||
*
|
||||
* The texture pages already loaded by ImageManager are forwarded via the
|
||||
* `data.images` metadata so the v7 atlas loader does not re-fetch them.
|
||||
*
|
||||
* @param project The project
|
||||
* @param spineTextureAtlasName The name of the atlas texture resource.
|
||||
* @returns The requested texture atlas, or null if it could not be loaded.
|
||||
*/
|
||||
static async _getSpineTextureAtlas(
|
||||
project: gdProject,
|
||||
@@ -934,6 +941,7 @@ export default class PixiResourcesLoader {
|
||||
if (!spineTextureAtlasName) {
|
||||
return {
|
||||
textureAtlas: null,
|
||||
atlasAlias: null,
|
||||
loadingError: null,
|
||||
loadingErrorReason: 'invalid-atlas-resource',
|
||||
};
|
||||
@@ -946,6 +954,7 @@ export default class PixiResourcesLoader {
|
||||
) {
|
||||
return {
|
||||
textureAtlas: null,
|
||||
atlasAlias: null,
|
||||
loadingError: null,
|
||||
loadingErrorReason: 'invalid-atlas-resource',
|
||||
};
|
||||
@@ -955,6 +964,7 @@ export default class PixiResourcesLoader {
|
||||
if (resource.getKind() !== 'atlas') {
|
||||
return {
|
||||
textureAtlas: null,
|
||||
atlasAlias: null,
|
||||
loadingError: null,
|
||||
loadingErrorReason: 'invalid-atlas-resource',
|
||||
};
|
||||
@@ -967,21 +977,22 @@ export default class PixiResourcesLoader {
|
||||
if (!textureAtlasMappingEntries.length) {
|
||||
return {
|
||||
textureAtlas: null,
|
||||
atlasAlias: null,
|
||||
loadingError: null,
|
||||
loadingErrorReason: 'missing-texture-resources',
|
||||
};
|
||||
}
|
||||
|
||||
// The v7 atlas loader expects BaseTexture instances when sharing pages
|
||||
// with already-loaded textures.
|
||||
const images = textureAtlasMappingEntries.reduce(
|
||||
(imagesMapping, [relatedPath, resourceName]) => {
|
||||
// flow check
|
||||
if (typeof resourceName === 'string') {
|
||||
imagesMapping[relatedPath] = this.getPIXITexture(
|
||||
project,
|
||||
resourceName
|
||||
);
|
||||
).baseTexture;
|
||||
}
|
||||
|
||||
return imagesMapping;
|
||||
},
|
||||
{}
|
||||
@@ -1007,32 +1018,13 @@ export default class PixiResourcesLoader {
|
||||
data: { images },
|
||||
});
|
||||
PIXI.Assets.load(spineTextureAtlasName).then(
|
||||
atlas => {
|
||||
// Ideally atlas of type `TextureAtlas` should be passed here.
|
||||
// But there is a known issue in case of preloaded images (see https://github.com/pixijs/spine/issues/537
|
||||
// and search the other mentions to this issue in the codebase).
|
||||
//
|
||||
// This branching covers all possible ways to make it work fine,
|
||||
// if issue is fixed in pixi-spine or after migration to spine-pixi.
|
||||
if (typeof atlas === 'string') {
|
||||
new PIXI_SPINE.TextureAtlas(
|
||||
atlas,
|
||||
(textureName, textureCb) =>
|
||||
textureCb(images[textureName].baseTexture),
|
||||
textureAtlas =>
|
||||
resolve({
|
||||
textureAtlas,
|
||||
loadingError: null,
|
||||
loadingErrorReason: null,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
resolve({
|
||||
textureAtlas: atlas,
|
||||
loadingError: null,
|
||||
loadingErrorReason: null,
|
||||
});
|
||||
}
|
||||
textureAtlas => {
|
||||
resolve({
|
||||
textureAtlas,
|
||||
atlasAlias: spineTextureAtlasName,
|
||||
loadingError: null,
|
||||
loadingErrorReason: null,
|
||||
});
|
||||
},
|
||||
err => {
|
||||
console.error(
|
||||
@@ -1040,6 +1032,7 @@ export default class PixiResourcesLoader {
|
||||
);
|
||||
resolve({
|
||||
textureAtlas: null,
|
||||
atlasAlias: null,
|
||||
loadingError: err,
|
||||
loadingErrorReason: 'atlas-resource-loading-error',
|
||||
});
|
||||
@@ -1049,10 +1042,12 @@ export default class PixiResourcesLoader {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Pixi spine data for the specified resource name.
|
||||
* Pre-load a Spine skeleton (`.json`/`.skel`) and its atlas, registering both
|
||||
* in `PIXI.Assets`. The returned aliases can be passed directly to
|
||||
* `Spine.from(...)` to instantiate a Spine container.
|
||||
*
|
||||
* @param project The project
|
||||
* @param spineName The name of the spine json resource
|
||||
* @returns The requested spine skeleton.
|
||||
* @param spineName The name of the spine resource
|
||||
*/
|
||||
static async getSpineData(
|
||||
project: gdProject,
|
||||
@@ -1065,7 +1060,7 @@ export default class PixiResourcesLoader {
|
||||
const resourceManager = project.getResourcesManager();
|
||||
if (!spineName || !resourceManager.hasResource(spineName)) {
|
||||
return {
|
||||
skeleton: null,
|
||||
aliases: null,
|
||||
loadingError: null,
|
||||
loadingErrorReason: 'invalid-spine-resource',
|
||||
};
|
||||
@@ -1074,7 +1069,7 @@ export default class PixiResourcesLoader {
|
||||
const resource = resourceManager.getResource(spineName);
|
||||
if (resource.getKind() !== 'spine') {
|
||||
return {
|
||||
skeleton: null,
|
||||
aliases: null,
|
||||
loadingError: null,
|
||||
loadingErrorReason: 'invalid-spine-resource',
|
||||
};
|
||||
@@ -1086,7 +1081,7 @@ export default class PixiResourcesLoader {
|
||||
: null;
|
||||
if (typeof spineTextureAtlasName !== 'string') {
|
||||
return {
|
||||
skeleton: null,
|
||||
aliases: null,
|
||||
loadingError: null,
|
||||
loadingErrorReason: 'missing-texture-atlas-name',
|
||||
};
|
||||
@@ -1095,14 +1090,15 @@ export default class PixiResourcesLoader {
|
||||
return (spineDataPromises[spineName] = new Promise(resolve => {
|
||||
this._getSpineTextureAtlas(project, spineTextureAtlasName).then(
|
||||
textureAtlasOrLoadingError => {
|
||||
if (!textureAtlasOrLoadingError.textureAtlas) {
|
||||
if (!textureAtlasOrLoadingError.atlasAlias) {
|
||||
return resolve({
|
||||
skeleton: null,
|
||||
aliases: null,
|
||||
loadingError: textureAtlasOrLoadingError.loadingError,
|
||||
loadingErrorReason: textureAtlasOrLoadingError.loadingErrorReason,
|
||||
});
|
||||
}
|
||||
|
||||
const atlasAlias = textureAtlasOrLoadingError.atlasAlias;
|
||||
const spineUrl = ResourcesLoader.getResourceFullUrl(
|
||||
project,
|
||||
spineName,
|
||||
@@ -1119,24 +1115,21 @@ export default class PixiResourcesLoader {
|
||||
PIXI.Assets.add({
|
||||
alias: spineName,
|
||||
src: spineUrl,
|
||||
data: {
|
||||
spineAtlas: textureAtlasOrLoadingError.textureAtlas,
|
||||
},
|
||||
});
|
||||
PIXI.Assets.load(spineName).then(
|
||||
jsonData => {
|
||||
() => {
|
||||
resolve({
|
||||
skeleton: jsonData.spineData,
|
||||
aliases: { skeletonAlias: spineName, atlasAlias },
|
||||
loadingError: null,
|
||||
loadingErrorReason: null,
|
||||
});
|
||||
},
|
||||
err => {
|
||||
console.error(
|
||||
`Error while loading Spine data "${spineName}": ${err}.\nCheck if you selected correct files.`
|
||||
`Error while loading Spine data "${spineName}": ${err}.\nCheck if you selected correct files. Note that spine-pixi-v7 only supports skeletons exported from Spine 4.2.`
|
||||
);
|
||||
resolve({
|
||||
skeleton: null,
|
||||
aliases: null,
|
||||
loadingError: err,
|
||||
loadingErrorReason: 'spine-resource-loading-error',
|
||||
});
|
||||
@@ -1147,6 +1140,41 @@ export default class PixiResourcesLoader {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a Spine container from the loaded skeleton + atlas resources.
|
||||
* Returns `null` if the resource cannot be loaded.
|
||||
*
|
||||
* @param project The project
|
||||
* @param spineName The name of the spine resource
|
||||
*/
|
||||
static async createSpine(
|
||||
project: gdProject,
|
||||
spineName: string
|
||||
// $FlowFixMe[value-as-type]
|
||||
): Promise<Spine | null> {
|
||||
const result = await this.getSpineData(project, spineName);
|
||||
if (!result.aliases) {
|
||||
console.error(
|
||||
`Unable to load Spine "${spineName}" (${result.loadingErrorReason ||
|
||||
'unknown reason'})`,
|
||||
result.loadingError
|
||||
);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Spine.from({
|
||||
skeleton: result.aliases.skeletonAlias,
|
||||
atlas: result.aliases.atlasAlias,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Exception while instantiating Spine "${spineName}":`,
|
||||
error
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the PIXI video texture represented by the given resource.
|
||||
* If not loaded, it will load it.
|
||||
|
||||
@@ -4,6 +4,20 @@ import { type I18n } from '@lingui/core';
|
||||
type TranslationFunction = (string => string) | null;
|
||||
type NotNullTranslationFunction = string => string;
|
||||
|
||||
// Lingui interprets translation keys as ICU MessageFormat templates.
|
||||
// Descriptions/sentences coming from third-party events extensions may legitimately
|
||||
// contain `{x}` / `{x, y}` substrings that look like placeholders but are not real
|
||||
// ones (e.g. `Output format: {key, value}`). Such strings throw inside `i18n._`
|
||||
// (`formatters[type] is not a function`), which is something we never want.
|
||||
// Fall back to the raw string in that case.
|
||||
const safeTranslate = (i18nModule: I18n, str: string): string => {
|
||||
try {
|
||||
return i18nModule._(str);
|
||||
} catch (error) {
|
||||
return str;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Given the i18n object, return the function that can be used
|
||||
* to translate strings. Useful for wiring i18n to extensions
|
||||
@@ -13,7 +27,7 @@ type NotNullTranslationFunction = string => string;
|
||||
export const getTranslationFunction = (i18n: ?I18n): TranslationFunction => {
|
||||
const i18nModule = i18n; // Make flow happy, ensure i18nModule is const.
|
||||
if (i18nModule) {
|
||||
return (str: string) => i18nModule._(str);
|
||||
return (str: string) => safeTranslate(i18nModule, str);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -30,7 +44,7 @@ export const getNotNullTranslationFunction = (
|
||||
): NotNullTranslationFunction => {
|
||||
const i18nModule = i18n; // Make flow happy, ensure i18nModule is const.
|
||||
if (i18nModule) {
|
||||
return (str: string) => i18nModule._(str);
|
||||
return (str: string) => safeTranslate(i18nModule, str);
|
||||
}
|
||||
|
||||
return (str: string) => str;
|
||||
|
||||
+2
-1
@@ -49,7 +49,8 @@
|
||||
"GDJS/Runtime/libs/CocoonJS",
|
||||
"Extensions/Effects/pixi-filters/filter-alpha.js",
|
||||
"SharedLibs/TileMapHelper/**/*",
|
||||
"Extensions/Spine/pixi-spine/pixi-spine.js",
|
||||
"Extensions/Spine/spine-pixi-v7/spine-pixi-v7-pre.js",
|
||||
"Extensions/Spine/spine-pixi-v7/spine-pixi-v7.js",
|
||||
"Extensions/P2P/A_peer.js",
|
||||
"Extensions/Multiplayer/peer.js"
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user