Skip to content

Commit cd07c08

Browse files
authored
Merge pull request #482 from akashic-games/support-request-asset-fallback
feat: `g.Scene#requestAssets()` で対象のアセットのロード失敗時に callback 経由でエラーを通知するインタフェースを追加
2 parents 7140d24 + f3ed8ce commit cd07c08

File tree

8 files changed

+187
-8
lines changed

8 files changed

+187
-8
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# ChangeLog
22

3+
## 3.16.5
4+
* `g.Scene#requestAssets()` で対象のアセットのロード失敗時に `callback` 経由でエラーを通知するインタフェースを追加
5+
36
## 3.16.4
47
不具合修正
58
* `require()` で末尾の "index" や "index.js" を省略する表記としない表記を混在させた時、スクリプトが複数回評価される問題を修正

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@akashic/akashic-engine",
3-
"version": "3.16.4",
3+
"version": "3.16.5",
44
"description": "The core library of Akashic Engine",
55
"main": "index.js",
66
"dependencies": {

src/AssetHolder.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ export interface AssetHolderParameterObject<UserData> {
6464
* このインスタンスに紐づけるユーザ定義データ。
6565
*/
6666
userData: UserData | null;
67+
68+
/**
69+
* エラーが発生したか否かに関わらず常に `handlerSet.handleFinish` を実行するか。
70+
*/
71+
alwaysNotifyFinish?: boolean;
6772
}
6873

6974
/**
@@ -108,6 +113,16 @@ export class AssetHolder<UserData> {
108113
*/
109114
_requested: boolean;
110115

116+
/**
117+
* @private
118+
*/
119+
_alwaysNotifyFinish: boolean;
120+
121+
/**
122+
* @private
123+
*/
124+
_failureAssetIds: (string | DynamicAssetConfiguration | AssetGenerationConfiguration)[];
125+
111126
constructor(param: AssetHolderParameterObject<UserData>) {
112127
const assetManager = param.assetManager;
113128
const assetIds = param.assetIds ? param.assetIds.concat() : [];
@@ -120,6 +135,8 @@ export class AssetHolder<UserData> {
120135
this._assets = [];
121136
this._handlerSet = param.handlerSet;
122137
this._requested = false;
138+
this._alwaysNotifyFinish = !!param.alwaysNotifyFinish;
139+
this._failureAssetIds = [];
123140
}
124141

125142
request(): boolean {
@@ -138,6 +155,7 @@ export class AssetHolder<UserData> {
138155
this.userData = undefined!;
139156
this._handlerSet = undefined!;
140157
this._assetIds = undefined!;
158+
this._failureAssetIds = undefined!;
141159
this._requested = false;
142160
}
143161

@@ -165,6 +183,10 @@ export class AssetHolder<UserData> {
165183
// game.json に定義されていればゲームを止める。それ以外 (DynamicAsset) では続行。
166184
if (this._assetManager.configuration[asset.id]) {
167185
hs.handleFinish.call(hs.owner, this, false);
186+
} else if (this._alwaysNotifyFinish) {
187+
const assetConf = this._peekAssetConfFromAssetId(asset.id);
188+
this._failureAssetIds.push(assetConf);
189+
this._decrementWaitingAssetCount();
168190
}
169191
}
170192
}
@@ -179,10 +201,38 @@ export class AssetHolder<UserData> {
179201
hs.handleLoad.call(hs.owner, asset);
180202
this._assets.push(asset);
181203

204+
this._decrementWaitingAssetCount();
205+
}
206+
207+
/**
208+
* @private
209+
*/
210+
_decrementWaitingAssetCount(): void {
182211
--this.waitingAssetsCount;
183212
if (this.waitingAssetsCount > 0) return;
184213
if (this.waitingAssetsCount < 0) throw ExceptionFactory.createAssertionError("AssetHolder#_onAssetLoad: broken waitingAssetsCount");
185214

215+
const hs = this._handlerSet;
186216
hs.handleFinish.call(hs.owner, this, true);
187217
}
218+
219+
/**
220+
* @private
221+
*/
222+
_getFailureAssetIds(): (string | DynamicAssetConfiguration | AssetGenerationConfiguration)[] {
223+
return this._failureAssetIds;
224+
}
225+
226+
/**
227+
* @private
228+
*/
229+
_peekAssetConfFromAssetId(id: string): string | DynamicAssetConfiguration | AssetGenerationConfiguration {
230+
for (const assetConf of this._assetIds) {
231+
const assetId = typeof assetConf === "string" ? assetConf : assetConf.id;
232+
if (id === assetId) {
233+
return assetConf;
234+
}
235+
}
236+
throw ExceptionFactory.createAssertionError(`AssetHolder#_peekAssetConfFromAssetId: could not peek the asset: ${id}`);
237+
}
188238
}

src/ExceptionFactory.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { AssertionError, AssetLoadError, TypeMismatchError } from "@akashic/pdi-types";
2+
import type { RequestAssetDetail, RequestAssetLoadError } from "./errors";
23

34
/**
45
* 例外生成ファクトリ。
@@ -49,4 +50,12 @@ export module ExceptionFactory {
4950
e.retriable = retriable;
5051
return e;
5152
}
53+
54+
export function createRequestAssetLoadError(message: string, detail: RequestAssetDetail, cause?: any): RequestAssetLoadError {
55+
const e = new Error(message) as RequestAssetLoadError;
56+
e.name = "RequestAssetLoadError";
57+
e.detail = detail;
58+
e.cause = cause;
59+
return e;
60+
}
5261
}

src/Scene.ts

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { Camera } from "./Camera";
88
import { Camera2D } from "./Camera2D";
99
import type { DynamicAssetConfiguration } from "./DynamicAssetConfiguration";
1010
import type { E, PointDownEvent, PointMoveEvent, PointSource, PointUpEvent } from "./entities/E";
11+
import type { RequestAssetLoadError } from "./errors";
1112
import type { MessageEvent, OperationEvent } from "./Event";
1213
import { ExceptionFactory } from "./ExceptionFactory";
1314
import type { Game } from "./Game";
@@ -17,7 +18,20 @@ import type { Timer } from "./Timer";
1718
import type { TimerIdentifier } from "./TimerManager";
1819
import { TimerManager } from "./TimerManager";
1920

20-
export type SceneRequestAssetHandler = () => void;
21+
export type SceneRequestAssetHandler = (error?: RequestAssetLoadError) => void;
22+
23+
/**
24+
* `Scene#requestAsset` の引数に渡すことができるパラメータ。
25+
*/
26+
export interface SceneRequestAssetsParameterObject {
27+
assetIds: (string | DynamicAssetConfiguration | AssetGenerationConfiguration)[];
28+
29+
/**
30+
* アセットの読込みに失敗した際にコールバックを実行するかどうか。
31+
* @default false
32+
*/
33+
notifyErrorOnCallback?: boolean;
34+
}
2135

2236
/**
2337
* `Scene` のコンストラクタに渡すことができるパラメータ。
@@ -792,7 +806,7 @@ export class Scene {
792806
}
793807

794808
requestAssets(
795-
assetIds: (string | DynamicAssetConfiguration | AssetGenerationConfiguration)[],
809+
assetIdsOrConf: (string | DynamicAssetConfiguration | AssetGenerationConfiguration)[] | SceneRequestAssetsParameterObject,
796810
handler: SceneRequestAssetHandler
797811
): void {
798812
if (this._loadingState !== "ready-fired" && this._loadingState !== "loaded-fired") {
@@ -801,9 +815,20 @@ export class Scene {
801815
throw ExceptionFactory.createAssertionError("Scene#requestAssets(): can be called after loaded.");
802816
}
803817

818+
let assetIds: (string | DynamicAssetConfiguration | AssetGenerationConfiguration)[];
819+
let alwaysNotifyFinish: boolean;
820+
if (Array.isArray(assetIdsOrConf)) {
821+
assetIds = assetIdsOrConf;
822+
alwaysNotifyFinish = false;
823+
} else {
824+
assetIds = assetIdsOrConf.assetIds;
825+
alwaysNotifyFinish = !!assetIdsOrConf.notifyErrorOnCallback;
826+
}
827+
804828
const holder = new AssetHolder<SceneRequestAssetHandler>({
805829
assetManager: this.game._assetManager,
806-
assetIds: assetIds,
830+
assetIds,
831+
alwaysNotifyFinish,
807832
handlerSet: {
808833
owner: this,
809834
handleLoad: this._handleSceneAssetLoad,
@@ -812,7 +837,19 @@ export class Scene {
812837
},
813838
userData: () => {
814839
// 不要なクロージャは避けたいが生存チェックのため不可避
815-
if (!this.destroyed()) handler();
840+
if (!this.destroyed()) {
841+
const failureAssetIds = holder._getFailureAssetIds();
842+
if (failureAssetIds.length) {
843+
// このパスに入るのは AssetHolder の alwaysNotifyFinish フラグを真にした時のみであることに注意
844+
const error = ExceptionFactory.createRequestAssetLoadError(
845+
`Scene#requestAssets(): failed to load the asset. refer to the 'detail' property for more information.`,
846+
{ failureAssetIds }
847+
);
848+
handler(error);
849+
} else {
850+
handler();
851+
}
852+
}
816853
}
817854
});
818855
this._assetHolders.push(holder);

src/__tests__/SceneSpec.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Trigger } from "@akashic/trigger";
2-
import type { AssetConfiguration, SceneStateString } from "..";
2+
import type { AssetConfiguration, DynamicAssetConfiguration, SceneStateString } from "..";
33
import { AssetManager, E, Scene } from "..";
44
import { customMatchers, Game, skeletonRuntime, ImageAsset, AudioAsset } from "./helpers";
55

@@ -374,6 +374,70 @@ describe("test Scene", () => {
374374
game._startLoadingGlobalAssets();
375375
});
376376

377+
it("loads assets dynamically - notifying error through the callback", done => {
378+
const game = new Game({
379+
width: 320,
380+
height: 320,
381+
main: "",
382+
assets: {}
383+
});
384+
385+
game._onLoad.add(() => {
386+
const scene = new Scene({
387+
game: game
388+
});
389+
scene.onLoad.add(() => {
390+
let loaded = false;
391+
392+
const assetIds: DynamicAssetConfiguration[] = [
393+
{
394+
id: "unregistered-asset-1",
395+
type: "audio",
396+
duration: 450,
397+
systemId: "sound",
398+
uri: "http://example.com/assets/audio/foo"
399+
},
400+
{
401+
id: "unregistered-asset-2",
402+
type: "audio",
403+
duration: 120,
404+
systemId: "sound",
405+
uri: "http://example.com/assets/audio/baz"
406+
}
407+
];
408+
409+
game.resourceFactory.withNecessaryRetryCount(-1, () => {
410+
scene.requestAssets(
411+
{
412+
assetIds,
413+
notifyErrorOnCallback: true
414+
},
415+
error => {
416+
loaded = true;
417+
expect(error!.name).toBe("RequestAssetLoadError");
418+
expect(error!.detail).toEqual({
419+
failureAssetIds: assetIds
420+
});
421+
done();
422+
}
423+
);
424+
425+
// Scene#requestAssets() のハンドラ呼び出しは Game#tick() に同期しており、実ロードの完了後に tick() が来るまで遅延される。
426+
// テスト上は tick() を呼び出さないので、 _flushPostTickTasks() を呼び続けることで模擬する。
427+
function flushUntilLoaded(): void {
428+
if (loaded) return;
429+
game._flushPostTickTasks();
430+
setTimeout(flushUntilLoaded, 10);
431+
}
432+
flushUntilLoaded();
433+
});
434+
});
435+
game.pushScene(scene);
436+
game._flushPostTickTasks();
437+
});
438+
game._startLoadingGlobalAssets();
439+
});
440+
377441
it("does not crash even if destroyed while loading assets", done => {
378442
const game = new Game({
379443
width: 320,

src/errors.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* akashic-engine 独自のエラー型定義。
3+
*/
4+
5+
import type { ErrorLike } from "@akashic/pdi-types";
6+
import type { AssetGenerationConfiguration } from "./AssetGenerationConfiguration";
7+
import type { DynamicAssetConfiguration } from "./DynamicAssetConfiguration";
8+
9+
export interface RequestAssetDetail {
10+
failureAssetIds: (string | DynamicAssetConfiguration | AssetGenerationConfiguration)[];
11+
}
12+
13+
export interface RequestAssetLoadError extends ErrorLike {
14+
name: "RequestAssetLoadError";
15+
detail: RequestAssetDetail;
16+
}

0 commit comments

Comments
 (0)