diff --git a/packages/core/src/asset/ResourceManager.ts b/packages/core/src/asset/ResourceManager.ts index 0395b2b29b..813775ef07 100644 --- a/packages/core/src/asset/ResourceManager.ts +++ b/packages/core/src/asset/ResourceManager.ts @@ -339,32 +339,33 @@ export class ResourceManager { this._loadingPromises = null; } - private _assignDefaultOptions(assetInfo: LoadItem): LoadItem { - assetInfo.type = assetInfo.type ?? ResourceManager._getTypeByUrl(assetInfo.url); + private _assignDefaultOptions(assetInfo: LoadItem, remoteConfig?: EditorResourceItem): void { + assetInfo.type ??= remoteConfig?.type ?? ResourceManager._getTypeByUrl(assetInfo.url); if (assetInfo.type === undefined) { throw `asset type should be specified: ${assetInfo.url}`; } assetInfo.retryCount = assetInfo.retryCount ?? this.retryCount; assetInfo.timeout = assetInfo.timeout ?? this.timeout; assetInfo.retryInterval = assetInfo.retryInterval ?? this.retryInterval; - assetInfo.url = assetInfo.url ?? assetInfo.urls.join(","); - return assetInfo; } private _loadSingleItem(itemOrURL: LoadItem | string): AssetPromise { - const item = this._assignDefaultOptions(typeof itemOrURL === "string" ? { url: itemOrURL } : itemOrURL); - let { url } = item; - - // Not absolute and base url is set - if (!Utils.isAbsoluteUrl(url) && this.baseUrl) url = Utils.resolveAbsoluteUrl(this.baseUrl, url); - + const item = typeof itemOrURL === "string" ? { url: itemOrURL } : itemOrURL; + item.url = item.url ?? item.urls.join(","); // Parse url - const { assetBaseURL, queryPath } = this._parseURL(url); + const { assetBaseURL, queryPath } = this._parseURL(item.url); const paths = queryPath ? this._parseQueryPath(queryPath) : []; // Get remote asset base url const remoteConfig = this._virtualPathResourceMap[assetBaseURL]; - const remoteAssetBaseURL = remoteConfig?.path ?? assetBaseURL; + this._assignDefaultOptions(item, remoteConfig); + + // Not absolute and base url is set + item.url = + !Utils.isAbsoluteUrl(assetBaseURL) && this.baseUrl + ? Utils.resolveAbsoluteUrl(this.baseUrl, assetBaseURL) + : assetBaseURL; + const remoteAssetBaseURL = remoteConfig?.path ?? item.url; // Check cache const cacheObject = this._assetUrlPool[remoteAssetBaseURL]; @@ -413,7 +414,7 @@ export class ResourceManager { // Check whether load main asset const mainPromise = loadingPromises[remoteAssetBaseURL] || - this._loadSubpackageAndMainAsset(loader, item, remoteAssetBaseURL, assetBaseURL, subpackageName); + this._loadSubpackageAndMainAsset(loader, item, remoteAssetBaseURL, subpackageName); mainPromise.catch((e) => { this._onSubAssetFail(remoteAssetBaseURL, queryPath, e); }); @@ -421,7 +422,7 @@ export class ResourceManager { return this._createSubAssetPromiseCallback(remoteAssetBaseURL, remoteAssetURL, queryPath); } - return this._loadSubpackageAndMainAsset(loader, item, remoteAssetBaseURL, assetBaseURL, subpackageName); + return this._loadSubpackageAndMainAsset(loader, item, remoteAssetBaseURL, subpackageName); } // For adapter mini-game platform @@ -429,19 +430,12 @@ export class ResourceManager { loader: Loader, item: LoadItem, remoteAssetBaseURL: string, - assetBaseURL: string, subpackageName: string ): AssetPromise { - return this._loadMainAsset(loader, item, remoteAssetBaseURL, assetBaseURL); + return this._loadMainAsset(loader, item, remoteAssetBaseURL); } - private _loadMainAsset( - loader: Loader, - item: LoadItem, - remoteAssetBaseURL: string, - assetBaseURL: string - ): AssetPromise { - item.url = assetBaseURL; + private _loadMainAsset(loader: Loader, item: LoadItem, remoteAssetBaseURL: string): AssetPromise { const loadingPromises = this._loadingPromises; const promise = loader.load(item, this); loadingPromises[remoteAssetBaseURL] = promise; diff --git a/tests/src/core/resource/ResourceManager.test.ts b/tests/src/core/resource/ResourceManager.test.ts index 61c62d7319..8cf6f1569e 100644 --- a/tests/src/core/resource/ResourceManager.test.ts +++ b/tests/src/core/resource/ResourceManager.test.ts @@ -1,4 +1,4 @@ -import { AssetType, ResourceManager, Texture2D } from "@galacean/engine"; +import { AssetPromise, AssetType, ResourceManager, Texture2D } from "@galacean/engine"; import "@galacean/engine-loader"; import { WebGLEngine } from "@galacean/engine"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -87,4 +87,91 @@ describe("ResourceManager", () => { } }); }); + + describe("virtualPath loading", () => { + it("infers loader type from virtualPathResourceMap when type is omitted", () => { + const resourceManager = engine.resourceManager; + resourceManager.initVirtualResources([ + { virtualPath: "Assets/extensionless", path: "https://cdn.ali.com/a.json", type: AssetType.Texture } + ]); + // @ts-ignore + const loaderSpy = vi + .spyOn(ResourceManager._loaders[AssetType.Texture], "load") + .mockReturnValue(new AssetPromise(() => {})); + + resourceManager.load({ url: "Assets/extensionless" }); + + expect(loaderSpy).toHaveBeenCalled(); + expect(loaderSpy.mock.calls[0][0].type).equal(AssetType.Texture); + loaderSpy.mockRestore(); + }); + + it("shares the main asset across sub-asset queries", () => { + const resourceManager = engine.resourceManager; + // @ts-ignore + const loaderSpy = vi + .spyOn(ResourceManager._loaders[AssetType.GLTF], "load") + .mockReturnValue(new AssetPromise(() => {})); + + resourceManager.load("https://cdn.ali.com/shared.glb?q=materials[0]"); + resourceManager.load("https://cdn.ali.com/shared.glb?q=materials[1]"); + + expect(loaderSpy).toHaveBeenCalledTimes(1); + loaderSpy.mockRestore(); + }); + + it("keeps the explicit type over the virtualPath map type", () => { + const resourceManager = engine.resourceManager; + resourceManager.initVirtualResources([ + { virtualPath: "Assets/explicit", path: "https://cdn.ali.com/x.json", type: AssetType.Texture } + ]); + // @ts-ignore + const loaderSpy = vi + .spyOn(ResourceManager._loaders[AssetType.GLTF], "load") + .mockReturnValue(new AssetPromise(() => {})); + + resourceManager.load({ url: "Assets/explicit", type: AssetType.GLTF }); + + expect(loaderSpy).toHaveBeenCalled(); + expect(loaderSpy.mock.calls[0][0].type).equal(AssetType.GLTF); + loaderSpy.mockRestore(); + }); + + it("resolves virtualPath via map even when baseUrl is set", () => { + const resourceManager = engine.resourceManager; + resourceManager.initVirtualResources([ + { virtualPath: "Assets/withBaseUrl", path: "https://cdn.ali.com/real.json", type: AssetType.Texture } + ]); + // @ts-ignore + const loaderSpy = vi + .spyOn(ResourceManager._loaders[AssetType.Texture], "load") + .mockReturnValue(new AssetPromise(() => {})); + resourceManager.baseUrl = "https://base.com/app/"; + + try { + resourceManager.load({ url: "Assets/withBaseUrl" }); + expect(loaderSpy).toHaveBeenCalled(); + expect(loaderSpy.mock.calls[0][0].type).equal(AssetType.Texture); + } finally { + resourceManager.baseUrl = null; + loaderSpy.mockRestore(); + } + }); + + it("merges urls into a single url before parsing", () => { + const resourceManager = engine.resourceManager; + // @ts-ignore + const loaderSpy = vi + .spyOn(ResourceManager._loaders[AssetType.KTXCube], "load") + .mockReturnValue(new AssetPromise(() => {})); + + resourceManager.load({ + type: AssetType.KTXCube, + urls: ["px.ktx", "nx.ktx", "py.ktx", "ny.ktx", "pz.ktx", "nz.ktx"] + }); + + expect(loaderSpy).toHaveBeenCalled(); + loaderSpy.mockRestore(); + }); + }); });