diff --git a/CHANGELOG.md b/CHANGELOG.md index 50e1f3dcd..78ec2d2bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # ChangeLog +## 3.14.0 +* @akashic/pdi-types@1.10.0 に追従 + * `"binary"` アセットに対応 + * `ScriptAsset#exports` に対応 + ## 3.13.0 * どのボタンでマウスクリックが行われたか認識できるように * 内部モジュールの更新 diff --git a/package-lock.json b/package-lock.json index fbee5b51e..c7552cc40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,22 @@ { "name": "@akashic/akashic-engine", - "version": "3.13.0", + "version": "3.14.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@akashic/akashic-engine", - "version": "3.13.0", + "version": "3.14.0", "license": "MIT", "dependencies": { - "@akashic/game-configuration": "~1.10.0", + "@akashic/game-configuration": "~1.12.0", "@akashic/pdi-types": "~1.11.1", "@akashic/playlog": "~3.2.0", "@akashic/trigger": "~2.0.0" }, "devDependencies": { "@akashic/eslint-config": "1.1.1", - "@akashic/pdi-common-impl": "^1.3.0", + "@akashic/pdi-common-impl": "^1.4.0", "@types/jest": "^29.0.0", "@typescript-eslint/eslint-plugin": "^5.29.0", "eslint": "^8.18.0", @@ -67,23 +67,23 @@ } }, "node_modules/@akashic/game-configuration": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@akashic/game-configuration/-/game-configuration-1.10.0.tgz", - "integrity": "sha512-LCSIuk4noqspJ8Ilhe8QeS4mb3qd3LZHz9GkXIORBuRnFf6wbtLzab0YlbmxlOWOIXF4ApRBO3pD3YpSQonAJg==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@akashic/game-configuration/-/game-configuration-1.12.0.tgz", + "integrity": "sha512-mL7yo0o4HD2KwUHH7/Z4AFMTZl7bs4zBFRLxeg9D8kpwUfS8y1BPDTMlWdfDmataLjHDZAsYq4B6aC/XL7+Haw==", "dependencies": { - "@akashic/pdi-types": "^1.7.0" + "@akashic/pdi-types": "^1.11.1" }, "optionalDependencies": { "es6-promise": "^4.2.8" } }, "node_modules/@akashic/pdi-common-impl": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@akashic/pdi-common-impl/-/pdi-common-impl-1.3.0.tgz", - "integrity": "sha512-6Tvr1Hor8GTlc7VbqDtbGmuEdegNt55l65YD2Yrg8GT4mG5p7d+lLOGritauHmVPJ1QJWssc7wAfyWzWRfFmpQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@akashic/pdi-common-impl/-/pdi-common-impl-1.4.0.tgz", + "integrity": "sha512-MhiJ2WcqaKBT24+QF7x72dtz6GqG6V7KI82lW5i9nF8lhOD3Tv49L8/ScZezLOi8GY1TW4v9mxX1GhL8yc1YwQ==", "dev": true, "dependencies": { - "@akashic/pdi-types": "^1.8.0", + "@akashic/pdi-types": "^1.10.0", "@akashic/trigger": "^2.0.0" } }, @@ -8292,21 +8292,21 @@ } }, "@akashic/game-configuration": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@akashic/game-configuration/-/game-configuration-1.10.0.tgz", - "integrity": "sha512-LCSIuk4noqspJ8Ilhe8QeS4mb3qd3LZHz9GkXIORBuRnFf6wbtLzab0YlbmxlOWOIXF4ApRBO3pD3YpSQonAJg==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@akashic/game-configuration/-/game-configuration-1.12.0.tgz", + "integrity": "sha512-mL7yo0o4HD2KwUHH7/Z4AFMTZl7bs4zBFRLxeg9D8kpwUfS8y1BPDTMlWdfDmataLjHDZAsYq4B6aC/XL7+Haw==", "requires": { - "@akashic/pdi-types": "^1.7.0", + "@akashic/pdi-types": "^1.11.1", "es6-promise": "^4.2.8" } }, "@akashic/pdi-common-impl": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@akashic/pdi-common-impl/-/pdi-common-impl-1.3.0.tgz", - "integrity": "sha512-6Tvr1Hor8GTlc7VbqDtbGmuEdegNt55l65YD2Yrg8GT4mG5p7d+lLOGritauHmVPJ1QJWssc7wAfyWzWRfFmpQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@akashic/pdi-common-impl/-/pdi-common-impl-1.4.0.tgz", + "integrity": "sha512-MhiJ2WcqaKBT24+QF7x72dtz6GqG6V7KI82lW5i9nF8lhOD3Tv49L8/ScZezLOi8GY1TW4v9mxX1GhL8yc1YwQ==", "dev": true, "requires": { - "@akashic/pdi-types": "^1.8.0", + "@akashic/pdi-types": "^1.10.0", "@akashic/trigger": "^2.0.0" } }, diff --git a/package.json b/package.json index 32401ac1e..c104414cd 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,17 @@ { "name": "@akashic/akashic-engine", - "version": "3.13.0", + "version": "3.14.0", "description": "The core library of Akashic Engine", "main": "index.js", "dependencies": { - "@akashic/game-configuration": "~1.10.0", + "@akashic/game-configuration": "~1.12.0", "@akashic/pdi-types": "~1.11.1", "@akashic/playlog": "~3.2.0", "@akashic/trigger": "~2.0.0" }, "devDependencies": { "@akashic/eslint-config": "1.1.1", - "@akashic/pdi-common-impl": "^1.3.0", + "@akashic/pdi-common-impl": "^1.4.0", "@types/jest": "^29.0.0", "@typescript-eslint/eslint-plugin": "^5.29.0", "eslint": "^8.18.0", diff --git a/src/AssetAccessor.ts b/src/AssetAccessor.ts index 8a46e2bc1..831b94b81 100644 --- a/src/AssetAccessor.ts +++ b/src/AssetAccessor.ts @@ -1,4 +1,4 @@ -import type { AudioAsset, ImageAsset, ScriptAsset, TextAsset, VectorImageAsset } from "@akashic/pdi-types"; +import type { AudioAsset, BinaryAsset, ImageAsset, ScriptAsset, TextAsset, VectorImageAsset } from "@akashic/pdi-types"; import type { AssetManager } from "./AssetManager"; /** @@ -108,6 +108,30 @@ export class AssetAccessor { return this._assetManager.peekLiveAssetByAccessorPath(path, "vector-image"); } + /** + * パスから読み込み済みのバイナリアセット(現在のシーンで読み込んだ、またはグローバルなアセット)を取得する。 + * + * パスはgame.jsonのあるディレクトリをルート (`/`) とする、 `/` 区切りの絶対パスでなければならない。 + * 当該のバイナリアセットが読み込まれていない場合、エラー。 + * + * @param path 取得するバイナリアセットのパス + */ + getBinary(path: string): BinaryAsset { + return this._assetManager.peekLiveAssetByAccessorPath(path, "binary"); + } + + /** + * パスから読み込み済みのバイナリアセット(現在のシーンで読み込んだ、またはグローバルなアセット)を取得し、その内容のバイト配列を返す。 + * + * パスはgame.jsonのあるディレクトリをルート (`/`) とする、 `/` 区切りの絶対パスでなければならない。 + * 当該のバイナリアセットが読み込まれていない場合、エラー。 + * + * @param path 内容のバイト配列を取得するバイナリアセットのパス + */ + getBinaryData(path: string): ArrayBuffer { + return this.getBinary(path).data; + } + /** * 与えられたパターンまたはフィルタにマッチするパスを持つ、読み込み済みの全画像アセット(現在のシーンで読み込んだ、またはグローバルなアセット)を取得する。 * @@ -167,6 +191,16 @@ export class AssetAccessor { return this._assetManager.peekAllLiveAssetsByPattern(patternOrFilter ?? "**/*", "vector-image"); } + /** + * 与えられたパターンまたはフィルタにマッチするパスを持つ、読み込み済みのバイナリアセット(現在のシーンで読み込んだ、またはグローバルなアセット)を取得する。 + * 引数の仕様については `AssetAccessor#getAllImages()` の仕様を参照のこと。 + * + * @param patternOrFilter 取得するベクタ画像アセットのパスパターンまたはフィルタ。省略した場合、読み込み済みの全て + */ + getAllBinaries(patternOrFilter?: string | ((path: string) => boolean)): BinaryAsset[] { + return this._assetManager.peekAllLiveAssetsByPattern(patternOrFilter ?? "**/*", "binary"); + } + /** * アセットIDから読み込み済みの画像アセット(現在のシーンで読み込んだ、またはグローバルなアセット)を取得する。 * 当該の画像アセットが読み込まれていない場合、エラー。 @@ -236,4 +270,24 @@ export class AssetAccessor { getVectorImageById(assetId: string): VectorImageAsset { return this._assetManager.peekLiveAssetById(assetId, "vector-image"); } + + /** + * アセットIDから読み込み済みのバイナリアセット(現在のシーンで読み込んだ、またはグローバルなアセット)を取得する。 + * 当該のバイナリアセットが読み込まれていない場合、エラー。 + * + * @param assetId 取得するバイナリアセットのID + */ + getBinaryById(assetId: string): BinaryAsset { + return this._assetManager.peekLiveAssetById(assetId, "binary"); + } + + /** + * アセットIDから読み込み済みのバイナリアセット(現在のシーンで読み込んだ、またはグローバルなアセット)を取得し、その内容のバイト配列を返す。 + * 当該のバイナリアセットが読み込まれていない場合、エラー。 + * + * @param assetId 取得するバイナリアセットのID + */ + getBinaryDataById(assetId: string): ArrayBuffer { + return this.getBinaryById(assetId).data; + } } diff --git a/src/AssetManager.ts b/src/AssetManager.ts index 0bac065fd..bde4308ec 100644 --- a/src/AssetManager.ts +++ b/src/AssetManager.ts @@ -10,7 +10,8 @@ import type { TextAssetConfigurationBase, AudioAssetConfigurationBase, VideoAssetConfigurationBase, - VectorImageAssetConfigurationBase + VectorImageAssetConfigurationBase, + BinaryAssetConfigurationBase } from "@akashic/game-configuration"; import type { Asset, @@ -23,12 +24,14 @@ import type { ScriptAsset, TextAsset, VideoAsset, - VectorImageAsset + VectorImageAsset, + BinaryAsset } from "@akashic/pdi-types"; import type { AssetGenerationConfiguration } from "./AssetGenerationConfiguration"; import type { AssetManagerLoadHandler } from "./AssetManagerLoadHandler"; import type { AudioSystem } from "./AudioSystem"; import type { AudioSystemManager } from "./AudioSystemManager"; +import { EmptyBinaryAsset } from "./auxiliary/EmptyBinaryAsset"; import { EmptyGeneratedVectorImageAsset } from "./auxiliary/EmptyGeneratedVectorImageAsset"; import { EmptyVectorImageAsset } from "./auxiliary/EmptyVectorImageAsset"; import { PartialImageAsset } from "./auxiliary/PartialImageAsset"; @@ -36,7 +39,7 @@ import type { DynamicAssetConfiguration } from "./DynamicAssetConfiguration"; import { ExceptionFactory } from "./ExceptionFactory"; import { VideoSystem } from "./VideoSystem"; -export type OneOfAsset = AudioAsset | ImageAsset | ScriptAsset | TextAsset | VideoAsset | VectorImageAsset; +export type OneOfAsset = AudioAsset | ImageAsset | ScriptAsset | TextAsset | VideoAsset | VectorImageAsset | BinaryAsset; // TODO: 以下の internal types を game-configuration に切り出す type AssetConfigurationCore = @@ -45,7 +48,8 @@ type AssetConfigurationCore = | VideoAssetConfiguration | AudioAssetConfiguration | TextAssetConfiguration - | ScriptAssetConfiguration; + | ScriptAssetConfiguration + | BinaryAssetConfiguration; type UnneededKeysForAsset = "path" | "virtualPath" | "global"; @@ -67,6 +71,9 @@ interface TextAssetConfiguration interface ScriptAssetConfiguration extends Omit, Omit {} +interface BinaryAssetConfiguration + extends Omit, + Omit {} type AssetIdOrConf = string | DynamicAssetConfiguration | AssetGenerationConfiguration; @@ -649,7 +656,7 @@ export class AssetManager implements AssetLoadHandler { case "text": return resourceFactory.createTextAsset(id, uri); case "script": - return resourceFactory.createScriptAsset(id, uri); + return resourceFactory.createScriptAsset(id, uri, conf.exports); case "video": // VideoSystemはまだ中身が定義されていなが、将来のためにVideoAssetにVideoSystemを渡すという体裁だけが整えられている。 // 以上を踏まえ、ここでは簡単のために都度新たなVideoSystemインスタンスを生成している。 @@ -660,6 +667,11 @@ export class AssetManager implements AssetLoadHandler { return new EmptyVectorImageAsset(id, uri, conf.width, conf.height, conf.hint); } return resourceFactory.createVectorImageAsset(id, uri, conf.width, conf.height, conf.hint); + case "binary": + if (!resourceFactory.createBinaryAsset) { + return new EmptyBinaryAsset(id, uri); + } + return resourceFactory.createBinaryAsset(id, uri); default: throw ExceptionFactory.createAssertionError( "AssertionError#_createAssetFor: unknown asset type " + type + " for asset ID: " + id diff --git a/src/DynamicAssetConfiguration.ts b/src/DynamicAssetConfiguration.ts index 8e249b487..f1c0508ec 100644 --- a/src/DynamicAssetConfiguration.ts +++ b/src/DynamicAssetConfiguration.ts @@ -1,6 +1,7 @@ import type { AssetConfigurationCommonBase, AudioAssetConfigurationBase, + BinaryAssetConfigurationBase, ImageAssetConfigurationBase, ScriptAssetConfigurationBase, TextAssetConfigurationBase, @@ -14,7 +15,8 @@ export type DynamicAssetConfiguration = | DynamicVectorImageAssetConfigurationBase | DynamicTextAssetConfigurationBase | DynamicScriptAssetConfigurationBase - | DynamicVideoAssetConfigurationBase; + | DynamicVideoAssetConfigurationBase + | DynamicBinaryAssetConfigurationBase; /** * (実行時に定義される)Assetの設定を表すインターフェース。 @@ -75,5 +77,12 @@ export interface DynamicScriptAssetConfigurationBase extends Omit, Omit {} +/** + * BinaryAssetの設定。 + */ +export interface DynamicBinaryAssetConfigurationBase + extends Omit, + Omit {} + // interface メンバは多重継承できないため、 DynamicAssetConfigurationBase の type メンバ を Omit する type UnneededKeysForDynamicAsset = "path" | "virtualPath" | "global"; diff --git a/src/__tests__/AssetAccessorSpec.ts b/src/__tests__/AssetAccessorSpec.ts index 016893f2f..7a660cee3 100644 --- a/src/__tests__/AssetAccessorSpec.ts +++ b/src/__tests__/AssetAccessorSpec.ts @@ -57,6 +57,11 @@ describe("test AssetAccessor", () => { width: 64, height: 64 }, + "id-assets/bin/lib01.wasm": { + type: "binary", + path: "assets/bin/lib01.wasm", + virtualPath: "assets/bin/lib01.wasm" + }, "node_modules/@akashic-extension/some-library/lib/index.js": { type: "script", path: "node_modules/@akashic-extension/some-library/lib/index.js", @@ -94,6 +99,8 @@ describe("test AssetAccessor", () => { testValue: true }); + const sampleArrayBufferContent = new ArrayBuffer(8); + const assetIds = [ "id-script/main.js", "id-assets/stage01/bgm01", @@ -102,6 +109,7 @@ describe("test AssetAccessor", () => { "id-assets/stage01/map.json", "id-assets/chara01/image.png", "id-assets/icon/icon01.svg", + "id-assets/bin/lib01.wasm", "node_modules/@akashic-extension/some-library/lib/index.js", "node_modules/@akashic-extension/some-library/assets/image.png", "node_modules/@akashic-extension/some-library/assets/boss.png", @@ -111,6 +119,7 @@ describe("test AssetAccessor", () => { function setupAssetAccessor(assetIds: string[], fail: (arg: any) => void, callback: (accessor: AssetAccessor) => void): void { const game = new Game(gameConfiguration); game.resourceFactory.scriptContents["assets/stage01/map.json"] = sampleJSONFileContent; + game.resourceFactory.binaryContents["assets/bin/lib01.wasm"] = sampleArrayBufferContent; const manager = game._assetManager; const accessor = new AssetAccessor(manager); @@ -165,8 +174,16 @@ describe("test AssetAccessor", () => { path: "assets/icon/icon01.svg" }); + expect(extractAssetProps(accessor.getBinary("/assets/bin/lib01.wasm"))).toEqual({ + id: "id-assets/bin/lib01.wasm", + type: "binary", + path: "assets/bin/lib01.wasm" + }); + expect(accessor.getTextContent("/assets/stage01/map.json")).toBe(sampleJSONFileContent); expect(accessor.getJSONContent("/assets/stage01/map.json")).toEqual(JSON.parse(sampleJSONFileContent)); + expect(accessor.getBinaryData("/assets/bin/lib01.wasm")).toBe(sampleArrayBufferContent); + done(); } ); @@ -268,6 +285,14 @@ describe("test AssetAccessor", () => { } ]); + expect(accessor.getAllBinaries().map(extractAssetProps)).toEqual([ + { + id: "id-assets/bin/lib01.wasm", + type: "binary", + path: "assets/bin/lib01.wasm" + } + ]); + done(); } ); @@ -308,8 +333,15 @@ describe("test AssetAccessor", () => { path: "assets/icon/icon01.svg" }); + expect(extractAssetProps(accessor.getBinaryById("id-assets/bin/lib01.wasm"))).toEqual({ + id: "id-assets/bin/lib01.wasm", + type: "binary", + path: "assets/bin/lib01.wasm" + }); + expect(accessor.getTextContentById("id-assets/stage01/map.json")).toBe(sampleJSONFileContent); expect(accessor.getJSONContentById("id-assets/stage01/map.json")).toEqual(JSON.parse(sampleJSONFileContent)); + expect(accessor.getBinaryDataById("id-assets/bin/lib01.wasm")).toBe(sampleArrayBufferContent); done(); } ); diff --git a/src/__tests__/helpers/mock.ts b/src/__tests__/helpers/mock.ts index b3950066a..085589e60 100644 --- a/src/__tests__/helpers/mock.ts +++ b/src/__tests__/helpers/mock.ts @@ -383,8 +383,8 @@ export class ScriptAsset extends pci.ScriptAsset { game: g.Game; _failureController: LoadFailureController; - constructor(game: g.Game, necessaryRetryCount: number, id: string, assetPath: string) { - super(id, assetPath); + constructor(game: g.Game, necessaryRetryCount: number, id: string, assetPath: string, exports?: string[]) { + super(id, assetPath, exports); this.game = game; this._failureController = new LoadFailureController(necessaryRetryCount); } @@ -461,6 +461,26 @@ export class GeneratedVectorImageAsset extends pci.VectorImageAsset { } } +export class BinaryAsset extends pci.BinaryAsset { + game: g.Game; + + constructor(game: g.Game, id: string, assetPath: string) { + super(id, assetPath); + this.game = game; + } + + _load(loader: AssetLoadHandler): void { + setTimeout(() => { + if ((this.game.resourceFactory as ResourceFactory).binaryContents.hasOwnProperty(this.path)) { + this.data = (this.game.resourceFactory as ResourceFactory).binaryContents[this.path]; + } else { + this.data = new ArrayBuffer(0); + } + if (!this.destroyed()) loader._onAssetLoad(this); + }, 0); + } +} + export class AudioPlayer extends pci.AudioPlayer { canHandleStoppedValue: boolean; @@ -495,6 +515,7 @@ export class GlyphFactory extends pci.GlyphFactory { export class ResourceFactory extends pci.ResourceFactory { _game: g.Game; scriptContents: { [path: string]: string }; + binaryContents: { [path: string]: ArrayBuffer }; // 真である限り createXXAsset() が DelayedAsset を生成する(現在は createImageAsset() のみ)。 // DelayedAsset は、flushDelayedAssets() 呼び出しまで読み込み完了(またはエラー)通知を遅延するアセットである。 @@ -507,6 +528,7 @@ export class ResourceFactory extends pci.ResourceFactory { constructor() { super(); this.scriptContents = {}; + this.binaryContents = {}; this._game = undefined!; this.createsDelayedAsset = false; this._necessaryRetryCount = 0; @@ -569,8 +591,12 @@ export class ResourceFactory extends pci.ResourceFactory { return new TextAsset(this._game, this._necessaryRetryCount, id, assetPath); } - createScriptAsset(id: string, assetPath: string): ScriptAsset { - return new ScriptAsset(this._game, this._necessaryRetryCount, id, assetPath); + createScriptAsset(id: string, assetPath: string, exports?: string[]): ScriptAsset { + return new ScriptAsset(this._game, this._necessaryRetryCount, id, assetPath, exports); + } + + createBinaryAsset(id: string, assetPath: string): BinaryAsset { + return new BinaryAsset(this._game, id, assetPath); } createSurface(width: number, height: number): pci.Surface { diff --git a/src/auxiliary/EmptyBinaryAsset.ts b/src/auxiliary/EmptyBinaryAsset.ts new file mode 100644 index 000000000..b89fc8724 --- /dev/null +++ b/src/auxiliary/EmptyBinaryAsset.ts @@ -0,0 +1,43 @@ +import type { Asset, AssetLoadHandler, BinaryAsset } from "@akashic/pdi-types"; +import { Trigger } from "@akashic/trigger"; + +export class EmptyBinaryAsset implements BinaryAsset { + id: string; + path: string; + originalPath: string; + type: "binary" = "binary"; + data: ArrayBuffer; + + onDestroyed: Trigger = new Trigger(); + + constructor(id: string, path: string) { + this.id = id; + this.path = path; + this.originalPath = path; + this.data = new ArrayBuffer(0); + } + + inUse(): boolean { + return false; + } + + destroy(): void { + if (this.destroyed()) { + return; + } + this.onDestroyed.destroy(); + this.onDestroyed = undefined!; + } + + destroyed(): boolean { + return !this.onDestroyed; + } + + _load(loader: AssetLoadHandler): void { + loader._onAssetLoad(this); + } + + _assetPathFilter(path: string): string { + return path; + } +}