@datamoc/mw_games 0.42.0 → 0.44.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +126 -0
- package/README.md +84 -1
- package/REFERENCE.md +134 -6
- package/api-surface.json +1814 -116
- package/data/microgm-essential.PROVENANCE.md +13 -0
- package/data/microgm-essential.sf2 +0 -0
- package/data/soundfont-catalog.json +8 -0
- package/dist/actors/Resource.d.ts.map +1 -1
- package/dist/actors/Resource.js +2 -3
- package/dist/actors/Resource.js.map +1 -1
- package/dist/audio/AdaptiveMusic.d.ts +85 -0
- package/dist/audio/AdaptiveMusic.d.ts.map +1 -0
- package/dist/audio/AdaptiveMusic.js +230 -0
- package/dist/audio/AdaptiveMusic.js.map +1 -0
- package/dist/audio/AudioLoader.d.ts +33 -0
- package/dist/audio/AudioLoader.d.ts.map +1 -0
- package/dist/audio/AudioLoader.js +58 -0
- package/dist/audio/AudioLoader.js.map +1 -0
- package/dist/audio/AudioViews.d.ts +70 -0
- package/dist/audio/AudioViews.d.ts.map +1 -0
- package/dist/audio/AudioViews.js +119 -0
- package/dist/audio/AudioViews.js.map +1 -0
- package/dist/audio/BusBudget.d.ts +89 -0
- package/dist/audio/BusBudget.d.ts.map +1 -0
- package/dist/audio/BusBudget.js +120 -0
- package/dist/audio/BusBudget.js.map +1 -0
- package/dist/audio/Channels.d.ts +66 -1
- package/dist/audio/Channels.d.ts.map +1 -1
- package/dist/audio/Channels.js +119 -31
- package/dist/audio/Channels.js.map +1 -1
- package/dist/audio/CoreHost.d.ts +114 -0
- package/dist/audio/CoreHost.d.ts.map +1 -0
- package/dist/audio/CoreHost.js +335 -0
- package/dist/audio/CoreHost.js.map +1 -0
- package/dist/audio/DecoderGuard.d.ts +139 -0
- package/dist/audio/DecoderGuard.d.ts.map +1 -0
- package/dist/audio/DecoderGuard.js +208 -0
- package/dist/audio/DecoderGuard.js.map +1 -0
- package/dist/audio/Fft.d.ts +3 -0
- package/dist/audio/Fft.d.ts.map +1 -0
- package/dist/audio/Fft.js +45 -0
- package/dist/audio/Fft.js.map +1 -0
- package/dist/audio/LoopTags.d.ts +18 -0
- package/dist/audio/LoopTags.d.ts.map +1 -1
- package/dist/audio/LoopTags.js +20 -0
- package/dist/audio/LoopTags.js.map +1 -1
- package/dist/audio/Midi.d.ts +35 -0
- package/dist/audio/Midi.d.ts.map +1 -1
- package/dist/audio/Midi.js +71 -1
- package/dist/audio/Midi.js.map +1 -1
- package/dist/audio/MidiRender.d.ts +47 -0
- package/dist/audio/MidiRender.d.ts.map +1 -1
- package/dist/audio/MidiRender.js +100 -2
- package/dist/audio/MidiRender.js.map +1 -1
- package/dist/audio/MidiSynth.d.ts +147 -0
- package/dist/audio/MidiSynth.d.ts.map +1 -0
- package/dist/audio/MidiSynth.js +205 -0
- package/dist/audio/MidiSynth.js.map +1 -0
- package/dist/audio/MusicPlayer.d.ts +80 -0
- package/dist/audio/MusicPlayer.d.ts.map +1 -0
- package/dist/audio/MusicPlayer.js +147 -0
- package/dist/audio/MusicPlayer.js.map +1 -0
- package/dist/audio/Playable.d.ts +7 -0
- package/dist/audio/Playable.d.ts.map +1 -1
- package/dist/audio/Playable.js.map +1 -1
- package/dist/audio/Positional.d.ts +12 -4
- package/dist/audio/Positional.d.ts.map +1 -1
- package/dist/audio/Positional.js +5 -2
- package/dist/audio/Positional.js.map +1 -1
- package/dist/audio/Resample.d.ts +61 -0
- package/dist/audio/Resample.d.ts.map +1 -0
- package/dist/audio/Resample.js +97 -0
- package/dist/audio/Resample.js.map +1 -0
- package/dist/audio/SampleDecoder.d.ts +16 -2
- package/dist/audio/SampleDecoder.d.ts.map +1 -1
- package/dist/audio/SampleDecoder.js +10 -6
- package/dist/audio/SampleDecoder.js.map +1 -1
- package/dist/audio/Settings.d.ts +99 -0
- package/dist/audio/Settings.d.ts.map +1 -0
- package/dist/audio/Settings.js +134 -0
- package/dist/audio/Settings.js.map +1 -0
- package/dist/audio/Sfx.d.ts +148 -0
- package/dist/audio/Sfx.d.ts.map +1 -0
- package/dist/audio/Sfx.js +213 -0
- package/dist/audio/Sfx.js.map +1 -0
- package/dist/audio/Sound.d.ts +2 -0
- package/dist/audio/Sound.d.ts.map +1 -1
- package/dist/audio/Sound.js +3 -0
- package/dist/audio/Sound.js.map +1 -1
- package/dist/audio/SoundFont.d.ts +5 -3
- package/dist/audio/SoundFont.d.ts.map +1 -1
- package/dist/audio/SoundFont.js +10 -8
- package/dist/audio/SoundFont.js.map +1 -1
- package/dist/audio/SpectralDistance.d.ts +34 -0
- package/dist/audio/SpectralDistance.d.ts.map +1 -0
- package/dist/audio/SpectralDistance.js +64 -0
- package/dist/audio/SpectralDistance.js.map +1 -0
- package/dist/audio/StreamChannel.d.ts +150 -0
- package/dist/audio/StreamChannel.d.ts.map +1 -0
- package/dist/audio/StreamChannel.js +390 -0
- package/dist/audio/StreamChannel.js.map +1 -0
- package/dist/audio/StreamPlayable.d.ts +73 -0
- package/dist/audio/StreamPlayable.d.ts.map +1 -0
- package/dist/audio/StreamPlayable.js +130 -0
- package/dist/audio/StreamPlayable.js.map +1 -0
- package/dist/audio/Synth.d.ts +1 -1
- package/dist/audio/Synth.d.ts.map +1 -1
- package/dist/audio/Synth.js +4 -0
- package/dist/audio/Synth.js.map +1 -1
- package/dist/audio/SyxBank.d.ts +73 -0
- package/dist/audio/SyxBank.d.ts.map +1 -0
- package/dist/audio/SyxBank.js +130 -0
- package/dist/audio/SyxBank.js.map +1 -0
- package/dist/audio/Underrun.d.ts +25 -0
- package/dist/audio/Underrun.d.ts.map +1 -0
- package/dist/audio/Underrun.js +18 -0
- package/dist/audio/Underrun.js.map +1 -0
- package/dist/audio/Wav.d.ts +56 -0
- package/dist/audio/Wav.d.ts.map +1 -0
- package/dist/audio/Wav.js +164 -0
- package/dist/audio/Wav.js.map +1 -0
- package/dist/audio/WebMidi.d.ts +58 -0
- package/dist/audio/WebMidi.d.ts.map +1 -0
- package/dist/audio/WebMidi.js +79 -0
- package/dist/audio/WebMidi.js.map +1 -0
- package/dist/audio/humanize/Onsets.d.ts.map +1 -1
- package/dist/audio/humanize/Onsets.js +1 -44
- package/dist/audio/humanize/Onsets.js.map +1 -1
- package/dist/audio/index.d.ts +43 -7
- package/dist/audio/index.d.ts.map +1 -1
- package/dist/audio/index.js +21 -3
- package/dist/audio/index.js.map +1 -1
- package/dist/battle/AttackPreview.d.ts.map +1 -1
- package/dist/battle/AttackPreview.js +8 -8
- package/dist/battle/AttackPreview.js.map +1 -1
- package/dist/battle/StatStages.d.ts.map +1 -1
- package/dist/battle/StatStages.js +2 -1
- package/dist/battle/StatStages.js.map +1 -1
- package/dist/board/Engine.d.ts +2 -0
- package/dist/board/Engine.d.ts.map +1 -1
- package/dist/board/Engine.js +1 -1
- package/dist/board/Engine.js.map +1 -1
- package/dist/board/HexSkirmish.d.ts.map +1 -1
- package/dist/board/HexSkirmish.js +11 -72
- package/dist/board/HexSkirmish.js.map +1 -1
- package/dist/board/Tactics.d.ts.map +1 -1
- package/dist/board/Tactics.js +3 -5
- package/dist/board/Tactics.js.map +1 -1
- package/dist/board/tourney.d.ts.map +1 -1
- package/dist/board/tourney.js +2 -19
- package/dist/board/tourney.js.map +1 -1
- package/dist/core/Handles.js +7 -7
- package/dist/core/Handles.js.map +1 -1
- package/dist/mw_games.global.js +25 -22
- package/dist/mwl/conditions.d.ts +12 -0
- package/dist/mwl/conditions.d.ts.map +1 -1
- package/dist/mwl/conditions.js +49 -8
- package/dist/mwl/conditions.js.map +1 -1
- package/dist/mwl/index.d.ts +13 -0
- package/dist/mwl/index.d.ts.map +1 -1
- package/dist/mwl/index.js +7 -0
- package/dist/mwl/index.js.map +1 -1
- package/dist/mwl/objects.d.ts +31 -0
- package/dist/mwl/objects.d.ts.map +1 -0
- package/dist/mwl/objects.js +83 -0
- package/dist/mwl/objects.js.map +1 -0
- package/dist/mwl/package-container.d.ts +185 -0
- package/dist/mwl/package-container.d.ts.map +1 -0
- package/dist/mwl/package-container.js +446 -0
- package/dist/mwl/package-container.js.map +1 -0
- package/dist/mwl/package-document.d.ts +217 -0
- package/dist/mwl/package-document.d.ts.map +1 -0
- package/dist/mwl/package-document.js +639 -0
- package/dist/mwl/package-document.js.map +1 -0
- package/dist/mwl/package-rom.d.ts +37 -0
- package/dist/mwl/package-rom.d.ts.map +1 -0
- package/dist/mwl/package-rom.js +96 -0
- package/dist/mwl/package-rom.js.map +1 -0
- package/dist/mwl/package-verify.d.ts +34 -0
- package/dist/mwl/package-verify.d.ts.map +1 -0
- package/dist/mwl/package-verify.js +111 -0
- package/dist/mwl/package-verify.js.map +1 -0
- package/dist/mwl/package.d.ts +184 -0
- package/dist/mwl/package.d.ts.map +1 -0
- package/dist/mwl/package.js +382 -0
- package/dist/mwl/package.js.map +1 -0
- package/dist/mwl/schema.d.ts +1 -1
- package/dist/mwl/schema.d.ts.map +1 -1
- package/dist/mwl/tables.d.ts +2 -0
- package/dist/mwl/tables.d.ts.map +1 -1
- package/dist/mwl/tables.js +13 -0
- package/dist/mwl/tables.js.map +1 -1
- package/dist/physics/PhysicsWorld.d.ts.map +1 -1
- package/dist/physics/PhysicsWorld.js +4 -3
- package/dist/physics/PhysicsWorld.js.map +1 -1
- package/dist/physics/XpbdWorld.d.ts +23 -5
- package/dist/physics/XpbdWorld.d.ts.map +1 -1
- package/dist/physics/XpbdWorld.js +91 -6
- package/dist/physics/XpbdWorld.js.map +1 -1
- package/dist/testing/FakeAudio.d.ts +149 -0
- package/dist/testing/FakeAudio.d.ts.map +1 -0
- package/dist/testing/FakeAudio.js +219 -0
- package/dist/testing/FakeAudio.js.map +1 -0
- package/dist/testing/index.d.ts +18 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +22 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/threads/index.d.ts +2 -2
- package/dist/threads/index.d.ts.map +1 -1
- package/dist/threads/index.js +1 -1
- package/dist/threads/index.js.map +1 -1
- package/dist/threads/spawn.d.ts +32 -0
- package/dist/threads/spawn.d.ts.map +1 -1
- package/dist/threads/spawn.js +35 -11
- package/dist/threads/spawn.js.map +1 -1
- package/dist/three-d/Collision3D.d.ts +46 -2
- package/dist/three-d/Collision3D.d.ts.map +1 -1
- package/dist/three-d/Collision3D.js +46 -2
- package/dist/three-d/Collision3D.js.map +1 -1
- package/dist/three-d/Grid.d.ts +12 -1
- package/dist/three-d/Grid.d.ts.map +1 -1
- package/dist/three-d/Grid.js +12 -1
- package/dist/three-d/Grid.js.map +1 -1
- package/dist/three-d/Vox.d.ts +13 -1
- package/dist/three-d/Vox.d.ts.map +1 -1
- package/dist/three-d/Vox.js +13 -1
- package/dist/three-d/Vox.js.map +1 -1
- package/dist/two-d/Game.d.ts +32 -1
- package/dist/two-d/Game.d.ts.map +1 -1
- package/dist/two-d/Game.js +105 -14
- package/dist/two-d/Game.js.map +1 -1
- package/dist/two-d/Scene2D.d.ts +24 -0
- package/dist/two-d/Scene2D.d.ts.map +1 -1
- package/dist/two-d/Scene2D.js +30 -0
- package/dist/two-d/Scene2D.js.map +1 -1
- package/dist/two-d/render/HazeLayer.d.ts.map +1 -1
- package/dist/two-d/render/HazeLayer.js +2 -1
- package/dist/two-d/render/HazeLayer.js.map +1 -1
- package/dist/two-d/render/ImageModifiers.d.ts +4 -2
- package/dist/two-d/render/ImageModifiers.d.ts.map +1 -1
- package/dist/two-d/render/ImageModifiers.js +13 -8
- package/dist/two-d/render/ImageModifiers.js.map +1 -1
- package/dist/two-d/render/NeonGlow.d.ts.map +1 -1
- package/dist/two-d/render/NeonGlow.js +2 -1
- package/dist/two-d/render/NeonGlow.js.map +1 -1
- package/dist/two-d/render/Particles.d.ts.map +1 -1
- package/dist/two-d/render/Particles.js +2 -1
- package/dist/two-d/render/Particles.js.map +1 -1
- package/dist/two-d/render/SceneTransition.d.ts +301 -0
- package/dist/two-d/render/SceneTransition.d.ts.map +1 -0
- package/dist/two-d/render/SceneTransition.js +458 -0
- package/dist/two-d/render/SceneTransition.js.map +1 -0
- package/dist/two-d/render/ScreenEffects.d.ts.map +1 -1
- package/dist/two-d/render/ScreenEffects.js +2 -1
- package/dist/two-d/render/ScreenEffects.js.map +1 -1
- package/dist/two-d/render/SkeletalSprite.d.ts.map +1 -1
- package/dist/two-d/render/SkeletalSprite.js +2 -1
- package/dist/two-d/render/SkeletalSprite.js.map +1 -1
- package/dist/two-d/render/Types2D.d.ts +2 -2
- package/dist/two-d/render/Types2D.d.ts.map +1 -1
- package/dist/two-d/render/Types2D.js +2 -2
- package/dist/two-d/render/Types2D.js.map +1 -1
- package/dist/two-d/render/index.d.ts +2 -0
- package/dist/two-d/render/index.d.ts.map +1 -1
- package/dist/two-d/render/index.js +1 -0
- package/dist/two-d/render/index.js.map +1 -1
- package/dist/two-d/ui/Bar.d.ts.map +1 -1
- package/dist/two-d/ui/Bar.js +3 -5
- package/dist/two-d/ui/Bar.js.map +1 -1
- package/dist/two-d/ui/Button.d.ts +23 -0
- package/dist/two-d/ui/Button.d.ts.map +1 -1
- package/dist/two-d/ui/Button.js +37 -0
- package/dist/two-d/ui/Button.js.map +1 -1
- package/dist/two-d/ui/Dropdown.d.ts +5 -1
- package/dist/two-d/ui/Dropdown.d.ts.map +1 -1
- package/dist/two-d/ui/Dropdown.js +8 -1
- package/dist/two-d/ui/Dropdown.js.map +1 -1
- package/dist/two-d/ui/DropdownView.d.ts +63 -3
- package/dist/two-d/ui/DropdownView.d.ts.map +1 -1
- package/dist/two-d/ui/DropdownView.js +168 -9
- package/dist/two-d/ui/DropdownView.js.map +1 -1
- package/dist/two-d/ui/FloatingText.d.ts.map +1 -1
- package/dist/two-d/ui/FloatingText.js +2 -1
- package/dist/two-d/ui/FloatingText.js.map +1 -1
- package/dist/two-d/ui/IconGrid.d.ts.map +1 -1
- package/dist/two-d/ui/IconGrid.js +2 -1
- package/dist/two-d/ui/IconGrid.js.map +1 -1
- package/dist/two-d/ui/ListView.d.ts.map +1 -1
- package/dist/two-d/ui/ListView.js +2 -1
- package/dist/two-d/ui/ListView.js.map +1 -1
- package/dist/two-d/ui/ScrollBox.d.ts +96 -0
- package/dist/two-d/ui/ScrollBox.d.ts.map +1 -1
- package/dist/two-d/ui/ScrollBox.js +218 -5
- package/dist/two-d/ui/ScrollBox.js.map +1 -1
- package/dist/two-d/ui/ScrollText.d.ts +131 -0
- package/dist/two-d/ui/ScrollText.d.ts.map +1 -0
- package/dist/two-d/ui/ScrollText.js +218 -0
- package/dist/two-d/ui/ScrollText.js.map +1 -0
- package/dist/two-d/ui/Slider.d.ts.map +1 -1
- package/dist/two-d/ui/Slider.js +4 -4
- package/dist/two-d/ui/Slider.js.map +1 -1
- package/dist/two-d/ui/Tooltip.d.ts +70 -1
- package/dist/two-d/ui/Tooltip.d.ts.map +1 -1
- package/dist/two-d/ui/Tooltip.js +115 -3
- package/dist/two-d/ui/Tooltip.js.map +1 -1
- package/dist/two-d/ui/Window.d.ts +35 -2
- package/dist/two-d/ui/Window.d.ts.map +1 -1
- package/dist/two-d/ui/Window.js +58 -3
- package/dist/two-d/ui/Window.js.map +1 -1
- package/dist/two-d/ui/index.d.ts +7 -5
- package/dist/two-d/ui/index.d.ts.map +1 -1
- package/dist/two-d/ui/index.js +3 -2
- package/dist/two-d/ui/index.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +14 -3
- package/sbom.cdx.json +19 -260
- package/tools/audio.d.mts +75 -0
- package/tools/audio.mjs +437 -0
- package/tools/browser-driver.mjs +3 -3
- package/tools/browser-smoke.mjs +19 -2
- package/tools/build-tablebases.mjs +7 -3
- package/tools/compile-openings.mjs +7 -3
- package/tools/compile-resources.d.mts +2 -0
- package/tools/compile-resources.mjs +13 -1
- package/tools/emit-page.d.mts +6 -0
- package/tools/emit-page.mjs +43 -3
- package/tools/find-chrome.mjs +4 -2
- package/tools/init.mjs +11 -3
- package/tools/mwl.mjs +144 -2
- package/tools/play-uci.mjs +9 -5
- package/tools/vite.mjs +9 -1
- package/tools.md +2 -1
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
import JSON5 from 'json5';
|
|
2
|
+
import { checkConditionSyntax } from './conditions.js';
|
|
3
|
+
import { preprocess } from './grammar.js';
|
|
4
|
+
import { checkPackagePath, PackageFormatError } from './package-container.js';
|
|
5
|
+
/**
|
|
6
|
+
* The `kind` values an `asset` tag accepts.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { PACKAGE_ASSET_KINDS } from '@datamoc/mw_games/mwl';
|
|
11
|
+
*
|
|
12
|
+
* console.log(PACKAGE_ASSET_KINDS.includes('synth-patch')); // true
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export const PACKAGE_ASSET_KINDS = [
|
|
16
|
+
'image',
|
|
17
|
+
'audio',
|
|
18
|
+
'music',
|
|
19
|
+
'font',
|
|
20
|
+
'data',
|
|
21
|
+
'synth-patch',
|
|
22
|
+
];
|
|
23
|
+
/**
|
|
24
|
+
* Kinds that are somebody's creative work: `license` and `source` are mandatory for these.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* import { PACKAGE_MEDIA_KINDS } from '@datamoc/mw_games/mwl';
|
|
29
|
+
*
|
|
30
|
+
* console.log(PACKAGE_MEDIA_KINDS.includes('data')); // false
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export const PACKAGE_MEDIA_KINDS = ['image', 'audio', 'music', 'font'];
|
|
34
|
+
/**
|
|
35
|
+
* The MWL tags a package document may hold, and the only ones. Standard game tags (`unit_type`,
|
|
36
|
+
* `event`, ...) are refused in a package: a character expands into them, and keeping the two
|
|
37
|
+
* apart means nothing in a shared file can reach the rules of the game it is dropped into.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* import { PACKAGE_TAGS } from '@datamoc/mw_games/mwl';
|
|
42
|
+
*
|
|
43
|
+
* console.log(PACKAGE_TAGS.includes('character')); // true
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export const PACKAGE_TAGS = [
|
|
47
|
+
'package',
|
|
48
|
+
'asset',
|
|
49
|
+
'character',
|
|
50
|
+
'animation',
|
|
51
|
+
'sound_effect',
|
|
52
|
+
'music',
|
|
53
|
+
'script',
|
|
54
|
+
'import',
|
|
55
|
+
];
|
|
56
|
+
/**
|
|
57
|
+
* The event vocabulary a package script may `do`. It is the data-only subset of the MWL event
|
|
58
|
+
* actions plus `play` and `give`; `hook` (game-author code), `endlevel`, `win`, `lose`,
|
|
59
|
+
* `spawn`, `kill` and anything else that decides the outcome of a scenario are not on it.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* import { PACKAGE_SCRIPT_ACTIONS } from '@datamoc/mw_games/mwl';
|
|
64
|
+
*
|
|
65
|
+
* console.log(PACKAGE_SCRIPT_ACTIONS.includes('hook')); // false
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export const PACKAGE_SCRIPT_ACTIONS = [
|
|
69
|
+
'message',
|
|
70
|
+
'say',
|
|
71
|
+
'play',
|
|
72
|
+
'give',
|
|
73
|
+
'set_variable',
|
|
74
|
+
'gold',
|
|
75
|
+
'heal_unit',
|
|
76
|
+
'fire_event',
|
|
77
|
+
];
|
|
78
|
+
/**
|
|
79
|
+
* The capabilities mwg itself knows how to grant; a package may name others, a policy decides.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```ts
|
|
83
|
+
* import { KNOWN_PACKAGE_CAPABILITIES } from '@datamoc/mw_games/mwl';
|
|
84
|
+
*
|
|
85
|
+
* console.log(KNOWN_PACKAGE_CAPABILITIES); // ['audio', 'script']
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export const KNOWN_PACKAGE_CAPABILITIES = ['audio', 'script'];
|
|
89
|
+
const ID = /^[A-Za-z_][\w.-]*$/;
|
|
90
|
+
const VERSION = /^\d+\.\d+\.\d+(?:[-+][\w.+-]*)?$/;
|
|
91
|
+
const MAX_FRAMES = 4096;
|
|
92
|
+
const MAX_CODE = 65536;
|
|
93
|
+
const MAX_CONDITION = 512;
|
|
94
|
+
const MAX_ACTIONS = 64;
|
|
95
|
+
const MAX_JSON_DEPTH = 16;
|
|
96
|
+
const MAX_IMPORT_DEPTH = 4;
|
|
97
|
+
/** every import read counts, so a fan-out of repeated imports cannot multiply within the depth bound */
|
|
98
|
+
const MAX_IMPORT_READS = 64;
|
|
99
|
+
/**
|
|
100
|
+
* Reads a package document: the MWL tags `package`, `asset`, `character`, `animation`,
|
|
101
|
+
* `sound_effect`, `music`, `script` and `import`, into plain typed data. Macros and
|
|
102
|
+
* `#include`s are expanded first, as for any MWL file. Nothing is executed: a script's `when` is
|
|
103
|
+
* only checked for syntax and its `do` only for membership of the safe vocabulary.
|
|
104
|
+
*
|
|
105
|
+
* `character` is a macro, not a compiled type of its own: it stays one record here and expands
|
|
106
|
+
* into a `unit_type` (plus the animations, effects and scripts it names) when merged. A new
|
|
107
|
+
* compiled table would need its own schema, validator and loader beside the one `unit_type`
|
|
108
|
+
* already has; the macro reuses all of them, and the fields only a renderer or a player of
|
|
109
|
+
* sound reads (portrait, sprite, theme) have no consumer in the rules core anyway.
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```ts
|
|
113
|
+
* import { parsePackageDocument } from '@datamoc/mw_games/mwl';
|
|
114
|
+
*
|
|
115
|
+
* const document = parsePackageDocument(
|
|
116
|
+
* "[{ tag: 'package', id: 'marta', version: '1.0.0' }, { tag: 'character', id: 'marta', hitpoints: 28 }]",
|
|
117
|
+
* );
|
|
118
|
+
* console.log(document.characters[0]?.stats.hitpoints); // 28
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
export function parsePackageDocument(source, options = {}) {
|
|
122
|
+
const sections = emptySections();
|
|
123
|
+
readInto(sections, source, options, 0, '', { reads: 0 });
|
|
124
|
+
if (!sections.meta)
|
|
125
|
+
throw new PackageFormatError(`${options.file ?? 'package'}: needs a [package] tag`);
|
|
126
|
+
return finish(sections, sections.meta);
|
|
127
|
+
}
|
|
128
|
+
function emptySections() {
|
|
129
|
+
return {
|
|
130
|
+
assets: [],
|
|
131
|
+
characters: [],
|
|
132
|
+
animations: [],
|
|
133
|
+
soundEffects: [],
|
|
134
|
+
music: [],
|
|
135
|
+
scripts: [],
|
|
136
|
+
imports: [],
|
|
137
|
+
messages: new Set(),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function finish(sections, meta) {
|
|
141
|
+
return {
|
|
142
|
+
meta,
|
|
143
|
+
assets: sections.assets,
|
|
144
|
+
characters: sections.characters,
|
|
145
|
+
animations: sections.animations,
|
|
146
|
+
soundEffects: sections.soundEffects,
|
|
147
|
+
music: sections.music,
|
|
148
|
+
scripts: sections.scripts,
|
|
149
|
+
imports: sections.imports,
|
|
150
|
+
messages: [...sections.messages].sort(),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/** parses one source into `sections`; a file `import` recurses with a prefix and a depth bound */
|
|
154
|
+
function readInto(sections, source, options, depth, prefix, budget) {
|
|
155
|
+
const file = options.file ?? 'package.mwl';
|
|
156
|
+
const fail = (message) => {
|
|
157
|
+
throw new PackageFormatError(`${file}: ${message}`);
|
|
158
|
+
};
|
|
159
|
+
let value;
|
|
160
|
+
try {
|
|
161
|
+
value = JSON5.parse(preprocess(source, options));
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
return fail(error instanceof Error ? error.message : String(error));
|
|
165
|
+
}
|
|
166
|
+
if (!Array.isArray(value))
|
|
167
|
+
return fail('a package document is a top-level array of nodes');
|
|
168
|
+
const text = (raw, where) => {
|
|
169
|
+
if (typeof raw === 'string')
|
|
170
|
+
return raw;
|
|
171
|
+
const gettext = raw?.$gettext;
|
|
172
|
+
if (typeof gettext === 'string' && Object.keys(raw).length === 1) {
|
|
173
|
+
sections.messages.add(gettext);
|
|
174
|
+
return gettext;
|
|
175
|
+
}
|
|
176
|
+
return fail(`${where} must be a string or _("...")`);
|
|
177
|
+
};
|
|
178
|
+
const plain = (raw, where) => {
|
|
179
|
+
if (typeof raw !== 'string')
|
|
180
|
+
return fail(`${where} must be a string`);
|
|
181
|
+
return raw;
|
|
182
|
+
};
|
|
183
|
+
const idOf = (raw, where) => {
|
|
184
|
+
if (typeof raw !== 'string' || !ID.test(raw))
|
|
185
|
+
return fail(`${where} must be an id (letters, digits, _ . -)`);
|
|
186
|
+
return raw;
|
|
187
|
+
};
|
|
188
|
+
const finite = (raw, where) => {
|
|
189
|
+
if (typeof raw !== 'number' || !Number.isFinite(raw))
|
|
190
|
+
return fail(`${where} must be a number`);
|
|
191
|
+
return raw;
|
|
192
|
+
};
|
|
193
|
+
const record = (raw, where) => {
|
|
194
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
|
|
195
|
+
return fail(`${where} must be an object`);
|
|
196
|
+
return raw;
|
|
197
|
+
};
|
|
198
|
+
const only = (body, allowed, where) => {
|
|
199
|
+
for (const key of Object.keys(body))
|
|
200
|
+
if (!allowed.includes(key))
|
|
201
|
+
fail(`${where}: unknown field "${key}"`);
|
|
202
|
+
};
|
|
203
|
+
const unique = (list, id, where) => {
|
|
204
|
+
if (list.some((item) => item.id === id))
|
|
205
|
+
fail(`${where}: duplicate id "${id}"`);
|
|
206
|
+
};
|
|
207
|
+
const local = (id) => (prefix ? `${prefix}.${id}` : id);
|
|
208
|
+
const json = (raw, where, level = 0) => {
|
|
209
|
+
if (level > MAX_JSON_DEPTH)
|
|
210
|
+
return fail(`${where} is nested too deep`);
|
|
211
|
+
if (raw === null || typeof raw === 'boolean' || typeof raw === 'string')
|
|
212
|
+
return raw;
|
|
213
|
+
if (typeof raw === 'number')
|
|
214
|
+
return Number.isFinite(raw) ? raw : fail(`${where} must be finite`);
|
|
215
|
+
if (Array.isArray(raw))
|
|
216
|
+
return raw.map((item) => json(item, where, level + 1));
|
|
217
|
+
if (typeof raw === 'object') {
|
|
218
|
+
const out = {};
|
|
219
|
+
for (const [key, item] of Object.entries(raw)) {
|
|
220
|
+
if (key === '__proto__')
|
|
221
|
+
fail(`${where} has a forbidden key`);
|
|
222
|
+
out[key] =
|
|
223
|
+
key === '$gettext' && typeof item === 'string'
|
|
224
|
+
? (sections.messages.add(item), item)
|
|
225
|
+
: json(item, where, level + 1);
|
|
226
|
+
}
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
229
|
+
return fail(`${where} must be plain data`);
|
|
230
|
+
};
|
|
231
|
+
const animationOf = (id, body, where) => {
|
|
232
|
+
only(body, ['tag', 'id', 'frames', 'fps', 'loop', 'events'], where);
|
|
233
|
+
const frames = body.frames;
|
|
234
|
+
if (!Array.isArray(frames) || frames.length === 0 || frames.length > MAX_FRAMES)
|
|
235
|
+
fail(`${where}: frames must be a list of 1 to ${MAX_FRAMES} frame numbers`);
|
|
236
|
+
frames.forEach((frame) => {
|
|
237
|
+
if (!Number.isInteger(frame) || frame < 0)
|
|
238
|
+
fail(`${where}: a frame is a non-negative integer`);
|
|
239
|
+
});
|
|
240
|
+
const events = {};
|
|
241
|
+
for (const [frame, event] of Object.entries(record(body.events ?? {}, `${where}.events`))) {
|
|
242
|
+
if (!/^\d+$/.test(frame))
|
|
243
|
+
fail(`${where}: event keys are frame numbers`);
|
|
244
|
+
const entry = record(event, `${where}.events.${frame}`);
|
|
245
|
+
only(entry, ['sound'], `${where}.events.${frame}`);
|
|
246
|
+
events[frame] = { sound: idOrRef(entry.sound, `${where}.events.${frame}.sound`) };
|
|
247
|
+
}
|
|
248
|
+
const fps = body.fps === undefined ? undefined : finite(body.fps, `${where}.fps`);
|
|
249
|
+
if (fps !== undefined && (fps <= 0 || fps > 240))
|
|
250
|
+
fail(`${where}: fps must be between 0 and 240`);
|
|
251
|
+
if (body.loop !== undefined && typeof body.loop !== 'boolean')
|
|
252
|
+
fail(`${where}: loop must be true or false`);
|
|
253
|
+
return {
|
|
254
|
+
id,
|
|
255
|
+
frames: frames,
|
|
256
|
+
...(fps === undefined ? {} : { fps }),
|
|
257
|
+
...(body.loop === undefined ? {} : { loop: body.loop }),
|
|
258
|
+
events,
|
|
259
|
+
};
|
|
260
|
+
};
|
|
261
|
+
/** a reference is an id, optionally qualified `package:id` */
|
|
262
|
+
const idOrRef = (raw, where) => {
|
|
263
|
+
if (typeof raw !== 'string' || !/^[A-Za-z_][\w.-]*(?:@[\w.+-]+)?(?::[A-Za-z_][\w.-]*)?$/.test(raw))
|
|
264
|
+
return fail(`${where} must be an id or package:id`);
|
|
265
|
+
return raw;
|
|
266
|
+
};
|
|
267
|
+
const scriptOf = (id, body, where) => {
|
|
268
|
+
only(body, ['tag', 'id', 'on', 'when', 'do', 'code'], where);
|
|
269
|
+
const on = plain(body.on, `${where}.on`);
|
|
270
|
+
if (!ID.test(on))
|
|
271
|
+
fail(`${where}: on must be an event name`);
|
|
272
|
+
let when;
|
|
273
|
+
if (body.when !== undefined) {
|
|
274
|
+
when = plain(body.when, `${where}.when`);
|
|
275
|
+
if (when.length > MAX_CONDITION)
|
|
276
|
+
fail(`${where}: when is longer than ${MAX_CONDITION} characters`);
|
|
277
|
+
try {
|
|
278
|
+
checkConditionSyntax(when);
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
fail(`${where}: when: ${error instanceof Error ? error.message : String(error)}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const rawDo = body.do ?? [];
|
|
285
|
+
if (!Array.isArray(rawDo) || rawDo.length > MAX_ACTIONS)
|
|
286
|
+
fail(`${where}: do is a list of at most ${MAX_ACTIONS} actions`);
|
|
287
|
+
const actions = rawDo.map((item, index) => {
|
|
288
|
+
const action = record(item, `${where}.do[${index}]`);
|
|
289
|
+
let tag = action.tag;
|
|
290
|
+
let fields = action;
|
|
291
|
+
if (typeof tag !== 'string') {
|
|
292
|
+
// the named form `{ message: { text: ... } }`
|
|
293
|
+
const keys = Object.keys(action);
|
|
294
|
+
tag = keys.length === 1 ? keys[0] : undefined;
|
|
295
|
+
fields = tag ? record(action[tag], `${where}.do[${index}]`) : action;
|
|
296
|
+
}
|
|
297
|
+
if (typeof tag !== 'string' || !PACKAGE_SCRIPT_ACTIONS.includes(tag))
|
|
298
|
+
return fail(`${where}.do[${index}]: "${String(tag)}" is not an allowed action (${PACKAGE_SCRIPT_ACTIONS.join(', ')})`);
|
|
299
|
+
const attributes = {};
|
|
300
|
+
for (const [key, raw] of Object.entries(fields)) {
|
|
301
|
+
if (key === 'tag')
|
|
302
|
+
continue;
|
|
303
|
+
attributes[key] =
|
|
304
|
+
typeof raw === 'number' || typeof raw === 'boolean'
|
|
305
|
+
? raw
|
|
306
|
+
: text(raw, `${where}.do[${index}].${key}`);
|
|
307
|
+
}
|
|
308
|
+
return { tag, attributes };
|
|
309
|
+
});
|
|
310
|
+
let code;
|
|
311
|
+
if (body.code !== undefined) {
|
|
312
|
+
code = plain(body.code, `${where}.code`);
|
|
313
|
+
if (code.length > MAX_CODE)
|
|
314
|
+
fail(`${where}: code is longer than ${MAX_CODE} characters`);
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
id,
|
|
318
|
+
on,
|
|
319
|
+
...(when === undefined ? {} : { when }),
|
|
320
|
+
do: actions,
|
|
321
|
+
...(code === undefined ? {} : { code }),
|
|
322
|
+
};
|
|
323
|
+
};
|
|
324
|
+
const characterOf = (id, body) => {
|
|
325
|
+
const where = `[character] ${id}`;
|
|
326
|
+
unique(sections.characters, id, where);
|
|
327
|
+
const animations = {};
|
|
328
|
+
for (const [name, item] of Object.entries(record(body.animations ?? {}, `${where}.animations`))) {
|
|
329
|
+
if (typeof item === 'string')
|
|
330
|
+
animations[name] = idOrRef(item, `${where}.animations.${name}`);
|
|
331
|
+
else {
|
|
332
|
+
// inline: becomes an animation of its own, named after the character
|
|
333
|
+
const animationId = `${id}.${name}`;
|
|
334
|
+
unique(sections.animations, animationId, where);
|
|
335
|
+
sections.animations.push(animationOf(animationId, record(item, `${where}.animations.${name}`), `${where}.animations.${name}`));
|
|
336
|
+
animations[name] = animationId;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const sounds = {};
|
|
340
|
+
for (const [name, item] of Object.entries(record(body.sounds ?? {}, `${where}.sounds`))) {
|
|
341
|
+
if (typeof item === 'string')
|
|
342
|
+
sounds[name] = idOrRef(item, `${where}.sounds.${name}`);
|
|
343
|
+
else {
|
|
344
|
+
const soundId = `${id}.${name}`;
|
|
345
|
+
unique(sections.soundEffects, soundId, where);
|
|
346
|
+
const definition = record(item, `${where}.sounds.${name}`);
|
|
347
|
+
sections.soundEffects.push({
|
|
348
|
+
id: soundId,
|
|
349
|
+
definition: JSON.parse(JSON.stringify(definition)),
|
|
350
|
+
});
|
|
351
|
+
sounds[name] = soundId;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const scripts = [];
|
|
355
|
+
const rawScripts = body.scripts ?? [];
|
|
356
|
+
if (!Array.isArray(rawScripts))
|
|
357
|
+
fail(`${where}.scripts must be a list`);
|
|
358
|
+
rawScripts.forEach((item, index) => {
|
|
359
|
+
if (typeof item === 'string')
|
|
360
|
+
scripts.push(idOrRef(item, `${where}.scripts[${index}]`));
|
|
361
|
+
else {
|
|
362
|
+
const scriptId = `${id}.script${index}`;
|
|
363
|
+
unique(sections.scripts, scriptId, where);
|
|
364
|
+
sections.scripts.push(scriptOf(scriptId, record(item, `${where}.scripts[${index}]`), `${where}.scripts[${index}]`));
|
|
365
|
+
scripts.push(scriptId);
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
let sprite;
|
|
369
|
+
if (body.sprite !== undefined) {
|
|
370
|
+
const raw = record(body.sprite, `${where}.sprite`);
|
|
371
|
+
only(raw, ['image', 'frame'], `${where}.sprite`);
|
|
372
|
+
const frame = raw.frame;
|
|
373
|
+
if (frame !== undefined &&
|
|
374
|
+
(!Array.isArray(frame) || frame.length !== 2 || frame.some((n) => !Number.isInteger(n) || n <= 0)))
|
|
375
|
+
fail(`${where}.sprite.frame is [width, height]`);
|
|
376
|
+
sprite = {
|
|
377
|
+
image: idOrRef(raw.image, `${where}.sprite.image`),
|
|
378
|
+
...(frame === undefined ? {} : { frame: frame }),
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
const stats = {};
|
|
382
|
+
for (const [key, raw] of Object.entries(body)) {
|
|
383
|
+
if (CHARACTER_FIELDS.includes(key))
|
|
384
|
+
continue;
|
|
385
|
+
if (!ID.test(key))
|
|
386
|
+
fail(`${where}: bad field name "${key}"`);
|
|
387
|
+
stats[key] = typeof raw === 'number' || typeof raw === 'boolean' ? raw : text(raw, `${where}.${key}`);
|
|
388
|
+
}
|
|
389
|
+
sections.characters.push({
|
|
390
|
+
id,
|
|
391
|
+
...(body.name === undefined ? {} : { name: text(body.name, `${where}.name`) }),
|
|
392
|
+
...(body.description === undefined ? {} : { description: text(body.description, `${where}.description`) }),
|
|
393
|
+
...(body.portrait === undefined ? {} : { portrait: idOrRef(body.portrait, `${where}.portrait`) }),
|
|
394
|
+
...(sprite === undefined ? {} : { sprite }),
|
|
395
|
+
...(body.theme === undefined ? {} : { theme: idOrRef(body.theme, `${where}.theme`) }),
|
|
396
|
+
animations,
|
|
397
|
+
sounds,
|
|
398
|
+
scripts,
|
|
399
|
+
stats,
|
|
400
|
+
});
|
|
401
|
+
};
|
|
402
|
+
const entries = [];
|
|
403
|
+
for (const entry of value) {
|
|
404
|
+
const body = record(entry, 'a node');
|
|
405
|
+
if (typeof body.tag === 'string')
|
|
406
|
+
entries.push([body.tag, body]);
|
|
407
|
+
else {
|
|
408
|
+
const keys = Object.keys(body);
|
|
409
|
+
if (keys.length !== 1)
|
|
410
|
+
fail('a node needs a tag');
|
|
411
|
+
const items = Array.isArray(body[keys[0]]) ? body[keys[0]] : [body[keys[0]]];
|
|
412
|
+
for (const item of items)
|
|
413
|
+
entries.push([keys[0], record(item, keys[0])]);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
for (const [tag, body] of entries) {
|
|
417
|
+
const where = `[${tag}]`;
|
|
418
|
+
if (!PACKAGE_TAGS.includes(tag))
|
|
419
|
+
fail(`${where} is not allowed in a package; use [character] and the other package tags`);
|
|
420
|
+
if (tag === 'package') {
|
|
421
|
+
if (depth > 0)
|
|
422
|
+
fail('an imported file cannot hold a [package] tag');
|
|
423
|
+
if (sections.meta)
|
|
424
|
+
fail('a second [package] tag');
|
|
425
|
+
only(body, ['tag', 'id', 'version', 'author', 'license', 'requires'], where);
|
|
426
|
+
const version = plain(body.version, `${where}.version`);
|
|
427
|
+
if (!VERSION.test(version))
|
|
428
|
+
fail(`${where}: version must look like 1.2.3`);
|
|
429
|
+
const requires = record(body.requires ?? {}, `${where}.requires`);
|
|
430
|
+
only(requires, ['mwg', 'retroAudio', 'capabilities'], `${where}.requires`);
|
|
431
|
+
const capabilities = requires.capabilities ?? [];
|
|
432
|
+
if (!Array.isArray(capabilities) || capabilities.some((item) => typeof item !== 'string' || !ID.test(item)))
|
|
433
|
+
fail(`${where}: requires.capabilities is a list of names`);
|
|
434
|
+
sections.meta = {
|
|
435
|
+
id: idOf(body.id, `${where}.id`),
|
|
436
|
+
version,
|
|
437
|
+
...(body.author === undefined ? {} : { author: text(body.author, `${where}.author`) }),
|
|
438
|
+
...(body.license === undefined ? {} : { license: plain(body.license, `${where}.license`) }),
|
|
439
|
+
requires: {
|
|
440
|
+
...(requires.mwg === undefined ? {} : { mwg: plain(requires.mwg, `${where}.requires.mwg`) }),
|
|
441
|
+
...(requires.retroAudio === undefined
|
|
442
|
+
? {}
|
|
443
|
+
: { retroAudio: plain(requires.retroAudio, `${where}.requires.retroAudio`) }),
|
|
444
|
+
capabilities: [...new Set(capabilities)].sort(),
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (tag === 'import') {
|
|
450
|
+
only(body, ['tag', 'src', 'package', 'version', 'as'], where);
|
|
451
|
+
if ((body.src === undefined) === (body.package === undefined))
|
|
452
|
+
fail(`${where}: needs exactly one of src and package`);
|
|
453
|
+
const imported = {
|
|
454
|
+
...(body.src === undefined ? {} : { src: checkPackagePath(plain(body.src, `${where}.src`)) }),
|
|
455
|
+
...(body.package === undefined ? {} : { package: idOf(body.package, `${where}.package`) }),
|
|
456
|
+
...(body.version === undefined ? {} : { version: plain(body.version, `${where}.version`) }),
|
|
457
|
+
...(body.as === undefined ? {} : { as: idOf(body.as, `${where}.as`) }),
|
|
458
|
+
};
|
|
459
|
+
sections.imports.push(imported);
|
|
460
|
+
if (imported.src !== undefined) {
|
|
461
|
+
if (depth + 1 > MAX_IMPORT_DEPTH)
|
|
462
|
+
fail(`${where}: imports are nested more than ${MAX_IMPORT_DEPTH} deep`);
|
|
463
|
+
if (++budget.reads > MAX_IMPORT_READS)
|
|
464
|
+
fail(`${where}: more than ${MAX_IMPORT_READS} imports in one package`);
|
|
465
|
+
const files = options.files;
|
|
466
|
+
const imported_source = files && Object.hasOwn(files, imported.src) ? files[imported.src] : undefined;
|
|
467
|
+
if (imported_source === undefined)
|
|
468
|
+
fail(`${where}: "${imported.src}" is not in the package`);
|
|
469
|
+
readInto(sections, imported_source, { ...options, file: imported.src }, depth + 1, imported.as ? local(imported.as) : prefix, budget);
|
|
470
|
+
}
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
const id = local(idOf(body.id, `${where}.id`));
|
|
474
|
+
if (tag === 'asset') {
|
|
475
|
+
only(body, ['tag', 'id', 'kind', 'path', 'mime', 'sha256', 'license', 'source'], where);
|
|
476
|
+
const kind = plain(body.kind, `${where}.kind`);
|
|
477
|
+
if (!PACKAGE_ASSET_KINDS.includes(kind))
|
|
478
|
+
fail(`${where}: kind must be one of ${PACKAGE_ASSET_KINDS.join(', ')}`);
|
|
479
|
+
unique(sections.assets, id, where);
|
|
480
|
+
const sha256 = body.sha256 === undefined ? undefined : plain(body.sha256, `${where}.sha256`);
|
|
481
|
+
if (sha256 !== undefined && !/^[0-9a-f]{64}$/.test(sha256))
|
|
482
|
+
fail(`${where}: sha256 is 64 lower-case hex digits`);
|
|
483
|
+
sections.assets.push({
|
|
484
|
+
id,
|
|
485
|
+
kind,
|
|
486
|
+
path: checkPackagePath(plain(body.path, `${where}.path`)),
|
|
487
|
+
...(body.mime === undefined ? {} : { mime: plain(body.mime, `${where}.mime`) }),
|
|
488
|
+
...(sha256 === undefined ? {} : { sha256 }),
|
|
489
|
+
...(body.license === undefined ? {} : { license: plain(body.license, `${where}.license`) }),
|
|
490
|
+
...(body.source === undefined ? {} : { source: plain(body.source, `${where}.source`) }),
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
else if (tag === 'animation') {
|
|
494
|
+
unique(sections.animations, id, where);
|
|
495
|
+
sections.animations.push(animationOf(id, body, `${where} ${id}`));
|
|
496
|
+
}
|
|
497
|
+
else if (tag === 'sound_effect') {
|
|
498
|
+
unique(sections.soundEffects, id, where);
|
|
499
|
+
const { tag: _tag, id: _id, ...rest } = body;
|
|
500
|
+
void _tag;
|
|
501
|
+
void _id;
|
|
502
|
+
sections.soundEffects.push({
|
|
503
|
+
id,
|
|
504
|
+
definition: json(rest, `${where} ${id}`),
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
else if (tag === 'music') {
|
|
508
|
+
only(body, ['tag', 'id', 'src', 'loop', 'region', 'fallback'], where);
|
|
509
|
+
unique(sections.music, id, where);
|
|
510
|
+
const region = body.region;
|
|
511
|
+
if (region !== undefined &&
|
|
512
|
+
(!Array.isArray(region) || region.length !== 2 || region.some((n) => typeof n !== 'number' || n < 0)))
|
|
513
|
+
fail(`${where}: region is [start, end]`);
|
|
514
|
+
sections.music.push({
|
|
515
|
+
id,
|
|
516
|
+
src: idOrRef(body.src, `${where}.src`),
|
|
517
|
+
...(body.loop === undefined ? {} : { loop: body.loop === true }),
|
|
518
|
+
...(region === undefined ? {} : { region: region }),
|
|
519
|
+
...(body.fallback === undefined ? {} : { fallback: idOrRef(body.fallback, `${where}.fallback`) }),
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
else if (tag === 'script') {
|
|
523
|
+
unique(sections.scripts, id, where);
|
|
524
|
+
sections.scripts.push(scriptOf(id, body, `${where} ${id}`));
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
characterOf(id, body);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
const CHARACTER_FIELDS = [
|
|
532
|
+
'tag',
|
|
533
|
+
'id',
|
|
534
|
+
'name',
|
|
535
|
+
'description',
|
|
536
|
+
'portrait',
|
|
537
|
+
'sprite',
|
|
538
|
+
'theme',
|
|
539
|
+
'animations',
|
|
540
|
+
'sounds',
|
|
541
|
+
'scripts',
|
|
542
|
+
];
|
|
543
|
+
function parseTriple(text, partial) {
|
|
544
|
+
const parts = text.split(/[-+]/)[0].split('.');
|
|
545
|
+
if (parts.length > 3 || (!partial && parts.length !== 3))
|
|
546
|
+
throw new PackageFormatError(`bad version "${text}"`);
|
|
547
|
+
return parts.map((part) => {
|
|
548
|
+
if (!/^\d{1,9}$/.test(part))
|
|
549
|
+
throw new PackageFormatError(`bad version "${text}"`);
|
|
550
|
+
return Number(part);
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
function compareTriples(a, b) {
|
|
554
|
+
for (let index = 0; index < 3; index++) {
|
|
555
|
+
const difference = (a[index] ?? 0) - (b[index] ?? 0);
|
|
556
|
+
if (difference)
|
|
557
|
+
return difference;
|
|
558
|
+
}
|
|
559
|
+
return 0;
|
|
560
|
+
}
|
|
561
|
+
/** the first version a partial one does not cover: `1.2` -> `1.3.0`, `1` -> `2.0.0` */
|
|
562
|
+
function bump(parts) {
|
|
563
|
+
if (parts.length === 1)
|
|
564
|
+
return [parts[0] + 1, 0, 0];
|
|
565
|
+
if (parts.length === 2)
|
|
566
|
+
return [parts[0], parts[1] + 1, 0];
|
|
567
|
+
return [parts[0], parts[1], parts[2] + 1];
|
|
568
|
+
}
|
|
569
|
+
function comparator(text) {
|
|
570
|
+
const match = /^(>=|<=|>|<|\^|~|=)?\s*(.*)$/.exec(text.trim());
|
|
571
|
+
const operator = match?.[1] ?? '=';
|
|
572
|
+
const body = match?.[2] ?? '';
|
|
573
|
+
if (body === '' || body === '*' || /^[xX*]$/.test(body))
|
|
574
|
+
return undefined;
|
|
575
|
+
const parts = parseTriple(body.replace(/\.[xX*]/g, ''), true);
|
|
576
|
+
const low = [parts[0], parts[1] ?? 0, parts[2] ?? 0];
|
|
577
|
+
if (operator === '>=')
|
|
578
|
+
return { low, op: 'range' };
|
|
579
|
+
if (operator === '>')
|
|
580
|
+
return parts.length === 3 ? { low, op: 'gt' } : { low: bump(parts), op: 'range' };
|
|
581
|
+
if (operator === '<')
|
|
582
|
+
return { low: [0, 0, 0], high: low, op: 'range' };
|
|
583
|
+
if (operator === '<=')
|
|
584
|
+
return parts.length === 3 ? { low, op: 'lte' } : { low: [0, 0, 0], high: bump(parts), op: 'range' };
|
|
585
|
+
if (operator === '^') {
|
|
586
|
+
// a caret allows changes right of the first non-zero part
|
|
587
|
+
const significant = parts.findIndex((part) => part > 0);
|
|
588
|
+
return { low, high: bump(parts.slice(0, significant < 0 ? parts.length : significant + 1)), op: 'range' };
|
|
589
|
+
}
|
|
590
|
+
if (operator === '~')
|
|
591
|
+
return { low, high: bump(parts.slice(0, 2)), op: 'range' };
|
|
592
|
+
return { low, high: bump(parts), op: 'range' };
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Checks a version against a small semver range: `>=`, `>`, `<=`, `<`, `^`, `~`, an exact
|
|
596
|
+
* version or a partial one (`0.42` is `0.42.x`), several separated by spaces (all must hold) and
|
|
597
|
+
* alternatives separated by `||`. A pre-release or build suffix on the version is ignored.
|
|
598
|
+
* A malformed range throws a `PackageFormatError`.
|
|
599
|
+
*
|
|
600
|
+
* @example
|
|
601
|
+
* ```ts
|
|
602
|
+
* import { satisfiesVersionRange } from '@datamoc/mw_games/mwl';
|
|
603
|
+
*
|
|
604
|
+
* console.log(satisfiesVersionRange('0.43.0', '>=0.42 <1')); // true
|
|
605
|
+
* console.log(satisfiesVersionRange('0.43.0', '^0.42.0')); // false
|
|
606
|
+
* ```
|
|
607
|
+
*/
|
|
608
|
+
export function satisfiesVersionRange(version, range) {
|
|
609
|
+
const actual = parseTriple(version, false);
|
|
610
|
+
return range.split('||').some((alternative) => alternative
|
|
611
|
+
.replace(/([<>=^~])\s+/g, '$1')
|
|
612
|
+
.trim()
|
|
613
|
+
.split(/\s+/)
|
|
614
|
+
.every((part) => {
|
|
615
|
+
const rule = comparator(part);
|
|
616
|
+
if (!rule)
|
|
617
|
+
return true;
|
|
618
|
+
if (rule.op === 'gt')
|
|
619
|
+
return compareTriples(actual, rule.low) > 0;
|
|
620
|
+
if (rule.op === 'lte')
|
|
621
|
+
return compareTriples(actual, rule.low) <= 0;
|
|
622
|
+
return compareTriples(actual, rule.low) >= 0 && (!rule.high || compareTriples(actual, rule.high) < 0);
|
|
623
|
+
}));
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Orders two `x.y.z` versions: negative when `a` is older, positive when newer. It decides which
|
|
627
|
+
* of two coexisting versions of a package owns the plain `packageId:id` name.
|
|
628
|
+
*
|
|
629
|
+
* @example
|
|
630
|
+
* ```ts
|
|
631
|
+
* import { compareVersions } from '@datamoc/mw_games/mwl';
|
|
632
|
+
*
|
|
633
|
+
* console.log(compareVersions('1.2.0', '1.10.0') < 0); // true
|
|
634
|
+
* ```
|
|
635
|
+
*/
|
|
636
|
+
export function compareVersions(a, b) {
|
|
637
|
+
return compareTriples(parseTriple(a, false), parseTriple(b, false));
|
|
638
|
+
}
|
|
639
|
+
//# sourceMappingURL=package-document.js.map
|