@fate-app/mod-types 2.0.0
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/LICENSE +21 -0
- package/README.md +23 -0
- package/dist/bundle.d.ts +30 -0
- package/dist/bundle.js +11 -0
- package/dist/character.d.ts +35 -0
- package/dist/character.js +1 -0
- package/dist/context.d.ts +19 -0
- package/dist/context.js +1 -0
- package/dist/dice.d.ts +64 -0
- package/dist/dice.js +47 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/manifest.d.ts +103 -0
- package/dist/manifest.js +1 -0
- package/dist/resolution.d.ts +38 -0
- package/dist/resolution.js +1 -0
- package/dist/validateBundleShape.d.ts +10 -0
- package/dist/validateBundleShape.js +86 -0
- package/package.json +47 -0
- package/registry.schema.json +97 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Stanislav Sonder
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# @fate-app/mod-types
|
|
2
|
+
|
|
3
|
+
Shared TypeScript types and runtime helpers for [Assistant for Fate](https://github.com/Stanislavsonder/fate)
|
|
4
|
+
mods — the single source of truth for the mod manifest/bundle shape, consumed
|
|
5
|
+
by both the app and community mod authors.
|
|
6
|
+
|
|
7
|
+
Exports `defineFateMod`, `getModData`/`setModData`, `validateBundleShape`, and
|
|
8
|
+
the vendored `registry.schema.json` used to validate a mod's `manifest.json`.
|
|
9
|
+
|
|
10
|
+
Full author-facing documentation lives in the app repo's
|
|
11
|
+
[`docs/MOD_API.md`](https://github.com/Stanislavsonder/fate/blob/main/docs/MOD_API.md).
|
|
12
|
+
Submitting a mod to the public registry: see
|
|
13
|
+
[`fate-core-mods`](https://github.com/Stanislavsonder/fate-core-mods)'s
|
|
14
|
+
`SUBMITTING.md`.
|
|
15
|
+
|
|
16
|
+
## Version discipline
|
|
17
|
+
|
|
18
|
+
This package's version tracks `SDK_VERSION` (the `FateSDK` ABI, defined in
|
|
19
|
+
the app's `src/mods/sdk.ts`) — same major.minor, patch is free. A mod's
|
|
20
|
+
`manifest.json` `sdk` field is a semver range checked against the app's
|
|
21
|
+
`SDK_VERSION` at load time; this package (and `@fate-app/mod-build`) is how
|
|
22
|
+
you compile against a given ABI version. Don't pin a version here that
|
|
23
|
+
doesn't correspond to a real `SDK_VERSION` the app has shipped.
|
package/dist/bundle.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Character, FatePatch } from './character';
|
|
2
|
+
import type { FateContext, FateConstants, FateTemplates } from './context';
|
|
3
|
+
import type { FateModuleComponent } from './manifest';
|
|
4
|
+
import type { DiceConstructor, DiceMaterial } from './dice';
|
|
5
|
+
/** The executable half of a mod — the default export of bundle.mjs */
|
|
6
|
+
export interface FateModBundle {
|
|
7
|
+
components?: FateModuleComponent[];
|
|
8
|
+
constants?: Partial<FateConstants>;
|
|
9
|
+
templates?: Partial<FateTemplates>;
|
|
10
|
+
shared?: Record<string, unknown>;
|
|
11
|
+
onInstall?(context: FateContext, character: Character): Promise<void> | void;
|
|
12
|
+
onUninstall?(context: FateContext, character: Character): Promise<void> | void;
|
|
13
|
+
onReconfigure?(context: FateContext, character: Character): Promise<void> | void;
|
|
14
|
+
patches?: FatePatch[];
|
|
15
|
+
/** Capability sections. */
|
|
16
|
+
dice?: FateModDice;
|
|
17
|
+
theme?: FateModTheme;
|
|
18
|
+
}
|
|
19
|
+
export interface FateModDice {
|
|
20
|
+
shapes?: DiceConstructor[];
|
|
21
|
+
materials?: DiceMaterial[];
|
|
22
|
+
}
|
|
23
|
+
export interface FateModTheme {
|
|
24
|
+
css: string;
|
|
25
|
+
}
|
|
26
|
+
export type FateModCapability = 'sheetComponents' | 'dice' | 'theme' | 'translations';
|
|
27
|
+
/** Identity helper that gives mod authors typing + future validation hook */
|
|
28
|
+
export declare function defineFateMod<_TData = unknown>(bundle: FateModBundle): FateModBundle;
|
|
29
|
+
export declare function getModData<T>(character: Character, key: string): T | undefined;
|
|
30
|
+
export declare function setModData<T>(character: Character, key: string, value: T): void;
|
package/dist/bundle.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Identity helper that gives mod authors typing + future validation hook */
|
|
2
|
+
export function defineFateMod(bundle) {
|
|
3
|
+
return bundle;
|
|
4
|
+
}
|
|
5
|
+
export function getModData(character, key) {
|
|
6
|
+
return character[key];
|
|
7
|
+
}
|
|
8
|
+
export function setModData(character, key, value) {
|
|
9
|
+
;
|
|
10
|
+
character[key] = value;
|
|
11
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { FateContext } from './context';
|
|
2
|
+
export type CharacterModule = {
|
|
3
|
+
version: string;
|
|
4
|
+
config?: Record<string, unknown>;
|
|
5
|
+
};
|
|
6
|
+
export type CharacterModules = {
|
|
7
|
+
[id: string]: CharacterModule;
|
|
8
|
+
};
|
|
9
|
+
export interface Character {
|
|
10
|
+
_modules: CharacterModules;
|
|
11
|
+
_version?: string;
|
|
12
|
+
id: number;
|
|
13
|
+
name: string;
|
|
14
|
+
/**
|
|
15
|
+
* Core field, not owned by any module — a character always has one
|
|
16
|
+
* (defaults to `''`, rendered as a placeholder image) so the character
|
|
17
|
+
* list card never depends on whether sonder@core-identity is installed.
|
|
18
|
+
* Existing characters are backfilled by src/patches/v2.0.0.ts.
|
|
19
|
+
*/
|
|
20
|
+
avatar: string;
|
|
21
|
+
/**
|
|
22
|
+
* Mod-owned data. Built-ins currently attach typed fields here via
|
|
23
|
+
* `declare module '@fate-app/mod-types'` augmentation (see each module's
|
|
24
|
+
* src/types.ts); external mods (Phase 2+), which compile separately and
|
|
25
|
+
* cannot augment this package, read/write their slice through
|
|
26
|
+
* getModData/setModData instead.
|
|
27
|
+
*/
|
|
28
|
+
[modKey: string]: unknown;
|
|
29
|
+
}
|
|
30
|
+
export interface FatePatch {
|
|
31
|
+
version: string;
|
|
32
|
+
note?: string;
|
|
33
|
+
incompatible?: boolean;
|
|
34
|
+
action: (context: FateContext, character: Character) => Promise<void>;
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Character } from './character';
|
|
2
|
+
import type { FateModuleComponent, FateModuleManifest } from './manifest';
|
|
3
|
+
export interface FateTemplates {
|
|
4
|
+
character: Character;
|
|
5
|
+
[modKey: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
export interface FateConstants {
|
|
8
|
+
[modKey: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
export interface FateShared {
|
|
11
|
+
[modKey: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
export interface FateContext {
|
|
14
|
+
modules: Record<string, FateModuleManifest>;
|
|
15
|
+
constants: FateConstants;
|
|
16
|
+
components: FateModuleComponent[];
|
|
17
|
+
templates: FateTemplates;
|
|
18
|
+
shared: FateShared;
|
|
19
|
+
}
|
package/dist/context.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/dice.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type * as CANNON from 'cannon-es';
|
|
2
|
+
import type * as THREE from 'three';
|
|
3
|
+
/**
|
|
4
|
+
* Structural mirror of the app's own dice-collision event shape. Deliberately
|
|
5
|
+
* not the legacy `cannon` package's `ICollisionEvent` (an app-local quirk
|
|
6
|
+
* predating this package, kept only where the app already used it) —
|
|
7
|
+
* cannon-es's own `Body` type is all a dice author actually needs.
|
|
8
|
+
*/
|
|
9
|
+
export interface DiceCollisionEvent {
|
|
10
|
+
type: string;
|
|
11
|
+
body: CANNON.Body;
|
|
12
|
+
target: CANNON.Body;
|
|
13
|
+
contact: CANNON.ContactEquation;
|
|
14
|
+
}
|
|
15
|
+
export type DiceResult = {
|
|
16
|
+
value: number;
|
|
17
|
+
values: number[];
|
|
18
|
+
text: string;
|
|
19
|
+
color: 'success' | 'danger' | 'medium';
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* A dice material — a real (tiny) runtime class, not just a type: it has no
|
|
23
|
+
* three/cannon-es logic of its own beyond holding references, so bundling it
|
|
24
|
+
* into every dice mod costs nothing. Only the heavy libraries (three,
|
|
25
|
+
* cannon-es themselves) go through FateSDK.dice — see docs/MOD_API.md.
|
|
26
|
+
*/
|
|
27
|
+
export declare class DiceMaterial {
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly faceMaterial: THREE.MeshStandardMaterial;
|
|
30
|
+
readonly symbolMaterial: THREE.MeshStandardMaterial;
|
|
31
|
+
readonly previewColor: string;
|
|
32
|
+
constructor(name: string, faceMaterial: THREE.MeshStandardMaterial, symbolMaterial: THREE.MeshStandardMaterial, previewColor: string);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The abstract base class every custom dice shape extends. A real runtime
|
|
36
|
+
* export (not a type-only declaration) for the same reason as DiceMaterial —
|
|
37
|
+
* its own constructor logic never touches three/cannon-es directly (that's
|
|
38
|
+
* the subclass's job in createMesh/createBody), so it's safe and cheap to
|
|
39
|
+
* bundle per-mod rather than routing through FateSDK.
|
|
40
|
+
*/
|
|
41
|
+
export declare abstract class Dice {
|
|
42
|
+
material: DiceMaterial;
|
|
43
|
+
size: number;
|
|
44
|
+
quality: number;
|
|
45
|
+
mass: number;
|
|
46
|
+
world: CANNON.World;
|
|
47
|
+
onCollide: (event: DiceCollisionEvent) => void;
|
|
48
|
+
static name: string;
|
|
49
|
+
static icon: string;
|
|
50
|
+
mesh: THREE.Mesh | THREE.Group;
|
|
51
|
+
body: CANNON.Body;
|
|
52
|
+
protected constructor(material: DiceMaterial, size: number, quality: number, mass: number, world: CANNON.World, onCollide: (event: DiceCollisionEvent) => void);
|
|
53
|
+
abstract clone(): Dice;
|
|
54
|
+
abstract getResult(): number;
|
|
55
|
+
abstract formatResult(result: number | number[]): DiceResult;
|
|
56
|
+
abstract changeMaterial(material: DiceMaterial): void;
|
|
57
|
+
protected abstract createMesh(): THREE.Mesh | THREE.Group;
|
|
58
|
+
protected abstract createBody(world: CANNON.World, onCollide: (event: DiceCollisionEvent) => void): CANNON.Body;
|
|
59
|
+
}
|
|
60
|
+
export type DiceConstructor = {
|
|
61
|
+
new (material: DiceMaterial, size: number, quality: number, mass: number, world: CANNON.World, onCollide: (event: DiceCollisionEvent) => void): Dice;
|
|
62
|
+
icon: string;
|
|
63
|
+
name: string;
|
|
64
|
+
};
|
package/dist/dice.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A dice material — a real (tiny) runtime class, not just a type: it has no
|
|
3
|
+
* three/cannon-es logic of its own beyond holding references, so bundling it
|
|
4
|
+
* into every dice mod costs nothing. Only the heavy libraries (three,
|
|
5
|
+
* cannon-es themselves) go through FateSDK.dice — see docs/MOD_API.md.
|
|
6
|
+
*/
|
|
7
|
+
export class DiceMaterial {
|
|
8
|
+
name;
|
|
9
|
+
faceMaterial;
|
|
10
|
+
symbolMaterial;
|
|
11
|
+
previewColor;
|
|
12
|
+
constructor(name, faceMaterial, symbolMaterial, previewColor) {
|
|
13
|
+
this.name = name;
|
|
14
|
+
this.faceMaterial = faceMaterial;
|
|
15
|
+
this.symbolMaterial = symbolMaterial;
|
|
16
|
+
this.previewColor = previewColor;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The abstract base class every custom dice shape extends. A real runtime
|
|
21
|
+
* export (not a type-only declaration) for the same reason as DiceMaterial —
|
|
22
|
+
* its own constructor logic never touches three/cannon-es directly (that's
|
|
23
|
+
* the subclass's job in createMesh/createBody), so it's safe and cheap to
|
|
24
|
+
* bundle per-mod rather than routing through FateSDK.
|
|
25
|
+
*/
|
|
26
|
+
export class Dice {
|
|
27
|
+
material;
|
|
28
|
+
size;
|
|
29
|
+
quality;
|
|
30
|
+
mass;
|
|
31
|
+
world;
|
|
32
|
+
onCollide;
|
|
33
|
+
static name;
|
|
34
|
+
static icon;
|
|
35
|
+
mesh;
|
|
36
|
+
body;
|
|
37
|
+
constructor(material, size, quality, mass, world, onCollide) {
|
|
38
|
+
this.material = material;
|
|
39
|
+
this.size = size;
|
|
40
|
+
this.quality = quality;
|
|
41
|
+
this.mass = mass;
|
|
42
|
+
this.world = world;
|
|
43
|
+
this.onCollide = onCollide;
|
|
44
|
+
this.mesh = this.createMesh();
|
|
45
|
+
this.body = this.createBody(this.world, this.onCollide);
|
|
46
|
+
}
|
|
47
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { Component } from 'vue';
|
|
2
|
+
import type { Character, FatePatch } from './character';
|
|
3
|
+
import type { FateConstants, FateContext, FateShared, FateTemplates } from './context';
|
|
4
|
+
import type { FateModCapability, FateModDice, FateModTheme } from './bundle';
|
|
5
|
+
export interface FateModuleComponent {
|
|
6
|
+
id: string;
|
|
7
|
+
component: Component;
|
|
8
|
+
order: number;
|
|
9
|
+
}
|
|
10
|
+
export interface FateModuleManifest {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
version: string;
|
|
14
|
+
author: {
|
|
15
|
+
name: string;
|
|
16
|
+
email?: string;
|
|
17
|
+
url?: string;
|
|
18
|
+
};
|
|
19
|
+
description: {
|
|
20
|
+
short: string;
|
|
21
|
+
full?: string;
|
|
22
|
+
};
|
|
23
|
+
languages: string[];
|
|
24
|
+
tags: string[];
|
|
25
|
+
dependencies?: Record<string, string>;
|
|
26
|
+
incompatibleWith?: string[];
|
|
27
|
+
appVersion?: string;
|
|
28
|
+
loadPriority: number;
|
|
29
|
+
components?: FateModuleComponent[];
|
|
30
|
+
constants?: Partial<FateConstants>;
|
|
31
|
+
templates?: Partial<FateTemplates>;
|
|
32
|
+
shared?: Partial<FateShared>;
|
|
33
|
+
onInstall(context: FateContext, character: Character): Promise<void> | void;
|
|
34
|
+
onUninstall(context: FateContext, character: Character): Promise<void> | void;
|
|
35
|
+
onReconfigure(context: FateContext, character: Character): Promise<void> | void;
|
|
36
|
+
patches?: FatePatch[];
|
|
37
|
+
config?: FateModuleConfig;
|
|
38
|
+
/** Host mod-API (ABI) semver range this manifest was built against */
|
|
39
|
+
sdk?: string;
|
|
40
|
+
/** Entry file relative to the mod's published directory, e.g. "bundle.mjs" */
|
|
41
|
+
entry?: string;
|
|
42
|
+
capabilities?: FateModCapability[];
|
|
43
|
+
/** For 'translations'-capability mods: what the pack translates — "app" and/or other mod ids.
|
|
44
|
+
* Declarative only for now (schema stub); the runtime merge is not implemented yet. */
|
|
45
|
+
translationTargets?: string[];
|
|
46
|
+
/** Present when capabilities includes 'dice' — spread onto the manifest by assembleMod() from the bundle */
|
|
47
|
+
dice?: FateModDice;
|
|
48
|
+
/** Present when capabilities includes 'theme' — spread onto the manifest by assembleMod() from the bundle */
|
|
49
|
+
theme?: FateModTheme;
|
|
50
|
+
extra?: {
|
|
51
|
+
[key: string]: unknown;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export interface FateModuleConfig {
|
|
55
|
+
groups: FateModuleConfigGroup[];
|
|
56
|
+
options: FateModuleConfigOption[];
|
|
57
|
+
}
|
|
58
|
+
export type FateModuleConfigGroup = {
|
|
59
|
+
id: string;
|
|
60
|
+
name: string;
|
|
61
|
+
description: string;
|
|
62
|
+
};
|
|
63
|
+
export type FateModuleConfigField = {
|
|
64
|
+
id: string;
|
|
65
|
+
name: string;
|
|
66
|
+
tooltip?: string;
|
|
67
|
+
type: 'string' | 'number' | 'select' | 'custom-list' | 'boolean' | 'range';
|
|
68
|
+
multiple?: boolean;
|
|
69
|
+
default: unknown;
|
|
70
|
+
limits?: {
|
|
71
|
+
min?: number;
|
|
72
|
+
max?: number;
|
|
73
|
+
step?: number;
|
|
74
|
+
};
|
|
75
|
+
options?: {
|
|
76
|
+
value: string;
|
|
77
|
+
label: string;
|
|
78
|
+
}[];
|
|
79
|
+
itemTemplate?: {
|
|
80
|
+
fields: FateModuleConfigField[];
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
export type FateModuleConfigOption = {
|
|
84
|
+
id: string;
|
|
85
|
+
groupId?: string;
|
|
86
|
+
name: string;
|
|
87
|
+
tooltip?: string;
|
|
88
|
+
type: 'number' | 'string' | 'boolean' | 'select' | 'range' | 'custom-list';
|
|
89
|
+
multiple?: boolean;
|
|
90
|
+
default: unknown;
|
|
91
|
+
limits?: {
|
|
92
|
+
min?: number;
|
|
93
|
+
max?: number;
|
|
94
|
+
step?: number;
|
|
95
|
+
};
|
|
96
|
+
options?: {
|
|
97
|
+
value: string;
|
|
98
|
+
label: string;
|
|
99
|
+
}[];
|
|
100
|
+
itemTemplate?: {
|
|
101
|
+
fields: FateModuleConfigField[];
|
|
102
|
+
};
|
|
103
|
+
};
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { CharacterModules } from './character';
|
|
2
|
+
import type { FateModuleManifest } from './manifest';
|
|
3
|
+
export type ModulesUpdateInstruction = {
|
|
4
|
+
install: CharacterModules;
|
|
5
|
+
reconfigure: CharacterModules;
|
|
6
|
+
uninstall: CharacterModules;
|
|
7
|
+
};
|
|
8
|
+
export type ModuleResolutionIssue = {
|
|
9
|
+
type: 'missing-dependency' | 'version-mismatch' | 'app-version-mismatch' | 'incompatible-modules' | 'dependency-cycle' | 'mod-not-installed';
|
|
10
|
+
moduleId: string;
|
|
11
|
+
moduleName: string;
|
|
12
|
+
details: {
|
|
13
|
+
dependencyId?: string;
|
|
14
|
+
requiredVersion?: string;
|
|
15
|
+
actualVersion?: string;
|
|
16
|
+
dependencyName?: string;
|
|
17
|
+
appVersion?: string;
|
|
18
|
+
requiredAppVersion?: string;
|
|
19
|
+
incompatibleWith?: Array<{
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
}>;
|
|
23
|
+
cycleModules?: Array<{
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
}>;
|
|
27
|
+
};
|
|
28
|
+
suggestedActions: Array<{
|
|
29
|
+
type: 'enable' | 'disable' | 'update' | 'choose-one' | 'install';
|
|
30
|
+
description: string;
|
|
31
|
+
targetModules: string[];
|
|
32
|
+
}>;
|
|
33
|
+
};
|
|
34
|
+
export type ModuleResolutionResult = {
|
|
35
|
+
resolvedModules: FateModuleManifest[];
|
|
36
|
+
issues: ModuleResolutionIssue[];
|
|
37
|
+
disabledModules: FateModuleManifest[];
|
|
38
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { FateModBundle, FateModCapability } from './bundle';
|
|
2
|
+
/**
|
|
3
|
+
* Cheap structural checks run on a freshly `import()`ed external bundle before
|
|
4
|
+
* anything (assembleMod, translations, the character sheet) trusts its shape.
|
|
5
|
+
* Throws a descriptive error on the first violation found — the app's loader
|
|
6
|
+
* catches it and quarantines the mod rather than letting a malformed bundle
|
|
7
|
+
* crash the app; the registry's CI smoke-load check runs the exact same
|
|
8
|
+
* function so the two never drift.
|
|
9
|
+
*/
|
|
10
|
+
export declare function validateBundleShape(bundle: unknown, capabilities: FateModCapability[] | undefined): asserts bundle is FateModBundle;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const LIFECYCLE_FNS = ['onInstall', 'onUninstall', 'onReconfigure'];
|
|
2
|
+
// A legitimate skin overrides a handful of CSS custom properties/selectors —
|
|
3
|
+
// sonder@theme-pink's real CSS is well under 1KB. This is generous headroom,
|
|
4
|
+
// not a real ceiling on styling ambition: it exists only to bound the worst
|
|
5
|
+
// case (a bloated or malicious stylesheet), since raw CSS can't execute
|
|
6
|
+
// script and the existing whole-bundle size limit (manifestChecks.ts)
|
|
7
|
+
// already covers the general case indirectly.
|
|
8
|
+
const THEME_CSS_MAX_BYTES = 100 * 1024;
|
|
9
|
+
/**
|
|
10
|
+
* Cheap structural checks run on a freshly `import()`ed external bundle before
|
|
11
|
+
* anything (assembleMod, translations, the character sheet) trusts its shape.
|
|
12
|
+
* Throws a descriptive error on the first violation found — the app's loader
|
|
13
|
+
* catches it and quarantines the mod rather than letting a malformed bundle
|
|
14
|
+
* crash the app; the registry's CI smoke-load check runs the exact same
|
|
15
|
+
* function so the two never drift.
|
|
16
|
+
*/
|
|
17
|
+
export function validateBundleShape(bundle, capabilities) {
|
|
18
|
+
if (!bundle || typeof bundle !== 'object') {
|
|
19
|
+
throw new Error('bundle default export must be an object');
|
|
20
|
+
}
|
|
21
|
+
const b = bundle;
|
|
22
|
+
if (capabilities?.includes('sheetComponents') && b.components !== undefined) {
|
|
23
|
+
if (!Array.isArray(b.components)) {
|
|
24
|
+
throw new Error('bundle.components must be an array');
|
|
25
|
+
}
|
|
26
|
+
b.components.forEach((component, index) => {
|
|
27
|
+
const c = component;
|
|
28
|
+
if (!c || typeof c !== 'object' || typeof c.id !== 'string' || typeof c.order !== 'number' || !c.component) {
|
|
29
|
+
throw new Error(`bundle.components[${index}] must be { id: string, component, order: number }`);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
for (const fn of LIFECYCLE_FNS) {
|
|
34
|
+
if (b[fn] !== undefined && typeof b[fn] !== 'function') {
|
|
35
|
+
throw new Error(`bundle.${fn} must be a function`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (b.patches !== undefined) {
|
|
39
|
+
if (!Array.isArray(b.patches)) {
|
|
40
|
+
throw new Error('bundle.patches must be an array');
|
|
41
|
+
}
|
|
42
|
+
b.patches.forEach((patch, index) => {
|
|
43
|
+
const p = patch;
|
|
44
|
+
if (!p || typeof p !== 'object' || typeof p.version !== 'string' || typeof p.action !== 'function') {
|
|
45
|
+
throw new Error(`bundle.patches[${index}] must be { version: string, action: function }`);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (capabilities?.includes('theme') && b.theme !== undefined) {
|
|
50
|
+
const theme = b.theme;
|
|
51
|
+
if (!theme || typeof theme.css !== 'string') {
|
|
52
|
+
throw new Error('bundle.theme.css must be a string');
|
|
53
|
+
}
|
|
54
|
+
if (theme.css.length > THEME_CSS_MAX_BYTES) {
|
|
55
|
+
throw new Error(`bundle.theme.css is ${(theme.css.length / 1024).toFixed(0)}KB, over the ${THEME_CSS_MAX_BYTES / 1024}KB limit for a theme mod`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (capabilities?.includes('dice') && b.dice !== undefined) {
|
|
59
|
+
const dice = b.dice;
|
|
60
|
+
if (!dice || typeof dice !== 'object') {
|
|
61
|
+
throw new Error('bundle.dice must be an object');
|
|
62
|
+
}
|
|
63
|
+
if (dice.shapes !== undefined) {
|
|
64
|
+
if (!Array.isArray(dice.shapes)) {
|
|
65
|
+
throw new Error('bundle.dice.shapes must be an array');
|
|
66
|
+
}
|
|
67
|
+
dice.shapes.forEach((shape, index) => {
|
|
68
|
+
const s = shape;
|
|
69
|
+
if (typeof shape !== 'function' || typeof s?.name !== 'string' || typeof s?.icon !== 'string') {
|
|
70
|
+
throw new Error(`bundle.dice.shapes[${index}] must be a class with static name/icon strings`);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (dice.materials !== undefined) {
|
|
75
|
+
if (!Array.isArray(dice.materials)) {
|
|
76
|
+
throw new Error('bundle.dice.materials must be an array');
|
|
77
|
+
}
|
|
78
|
+
dice.materials.forEach((material, index) => {
|
|
79
|
+
const m = material;
|
|
80
|
+
if (!m || typeof m !== 'object' || typeof m.name !== 'string' || typeof m.previewColor !== 'string' || !m.faceMaterial || !m.symbolMaterial) {
|
|
81
|
+
throw new Error(`bundle.dice.materials[${index}] must be { name: string, faceMaterial, symbolMaterial, previewColor: string }`);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fate-app/mod-types",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Shared TypeScript types and runtime helpers for Assistant for Fate mods — the single source of truth for the mod manifest/bundle shape, consumed by both the app and community mod authors.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Stanislavsonder/fate.git",
|
|
9
|
+
"directory": "packages/mod-types"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./registry.schema.json": "./registry.schema.json"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"registry.schema.json"
|
|
23
|
+
],
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"cannon-es": "^0.20.0",
|
|
29
|
+
"three": "^0.185.0"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"cannon-es": "^0.20.0",
|
|
33
|
+
"three": "^0.185.0",
|
|
34
|
+
"vue": "^3.5.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependenciesMeta": {
|
|
37
|
+
"cannon-es": {
|
|
38
|
+
"optional": true
|
|
39
|
+
},
|
|
40
|
+
"three": {
|
|
41
|
+
"optional": true
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsc -p tsconfig.build.json"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://stanislavsonder.github.io/fate-core-mods/registry.schema.json",
|
|
4
|
+
"title": "FATE: Core mod manifest.json",
|
|
5
|
+
"description": "Static, non-executable metadata for a mod submitted to fate-core-mods. Anything executable belongs in bundle.mjs, never here.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["id", "version", "name", "author", "description", "languages", "tags", "loadPriority", "sdk", "entry", "capabilities"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"id": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"pattern": "^[a-z0-9-]+@[a-z0-9-]+$",
|
|
13
|
+
"description": "\"author@name\", globally unique, lowercase kebab-case on both sides of the @."
|
|
14
|
+
},
|
|
15
|
+
"version": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"pattern": "^\\d+\\.\\d+\\.\\d+(-[0-9A-Za-z.-]+)?$",
|
|
18
|
+
"description": "Semver."
|
|
19
|
+
},
|
|
20
|
+
"name": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"minLength": 1,
|
|
23
|
+
"description": "\"t.name\" i18n key (resolved via signRecord) or a plain string."
|
|
24
|
+
},
|
|
25
|
+
"author": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"required": ["name", "github"],
|
|
28
|
+
"additionalProperties": false,
|
|
29
|
+
"properties": {
|
|
30
|
+
"name": { "type": "string", "minLength": 1 },
|
|
31
|
+
"email": { "type": "string", "format": "email" },
|
|
32
|
+
"url": { "type": "string", "format": "uri" },
|
|
33
|
+
"github": { "type": "string", "pattern": "^[A-Za-z0-9-]+$", "description": "GitHub handle — CI binds this to the PR author." }
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"description": {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"required": ["short"],
|
|
39
|
+
"additionalProperties": false,
|
|
40
|
+
"properties": {
|
|
41
|
+
"short": { "type": "string", "minLength": 1, "maxLength": 160 },
|
|
42
|
+
"full": { "type": "string" }
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"languages": {
|
|
46
|
+
"type": "array",
|
|
47
|
+
"items": { "type": "string", "pattern": "^[a-z]{2}(-[A-Z]{2})?$" },
|
|
48
|
+
"minItems": 1,
|
|
49
|
+
"description": "Must match the files present under translations/."
|
|
50
|
+
},
|
|
51
|
+
"tags": {
|
|
52
|
+
"type": "array",
|
|
53
|
+
"items": { "type": "string", "minLength": 1 }
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"additionalProperties": { "type": "string" },
|
|
58
|
+
"description": "{ [modId]: semverRange }"
|
|
59
|
+
},
|
|
60
|
+
"incompatibleWith": {
|
|
61
|
+
"type": "array",
|
|
62
|
+
"items": { "type": "string" }
|
|
63
|
+
},
|
|
64
|
+
"appVersion": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"description": "Semver range gating the app version."
|
|
67
|
+
},
|
|
68
|
+
"loadPriority": {
|
|
69
|
+
"type": "number"
|
|
70
|
+
},
|
|
71
|
+
"sdk": {
|
|
72
|
+
"type": "string",
|
|
73
|
+
"description": "Semver range gating FateSDK.version (the ABI)."
|
|
74
|
+
},
|
|
75
|
+
"entry": {
|
|
76
|
+
"type": "string",
|
|
77
|
+
"const": "bundle.mjs"
|
|
78
|
+
},
|
|
79
|
+
"capabilities": {
|
|
80
|
+
"type": "array",
|
|
81
|
+
"minItems": 1,
|
|
82
|
+
"items": { "type": "string", "enum": ["sheetComponents", "dice", "theme", "translations"] },
|
|
83
|
+
"description": "\"sheetComponents\", \"dice\", and \"theme\" are fully supported for external mods (SDK >= 1.1.0). \"translations\" is declared-only for now: the manifest fields exist, but the app does not yet merge translation packs at runtime."
|
|
84
|
+
},
|
|
85
|
+
"translationTargets": {
|
|
86
|
+
"type": "array",
|
|
87
|
+
"items": { "type": "string", "pattern": "^(app|[a-z0-9-]+@[a-z0-9-]+)$" },
|
|
88
|
+
"minItems": 1,
|
|
89
|
+
"description": "For \"translations\"-capability mods: what this pack translates — \"app\" (the app's own UI strings) and/or other mod ids. Declarative only until the runtime merge ships; entry/bundle.mjs is still required for now."
|
|
90
|
+
},
|
|
91
|
+
"config": {
|
|
92
|
+
"type": "object",
|
|
93
|
+
"additionalProperties": true,
|
|
94
|
+
"description": "{ groups: [], options: [] } — declarative settings schema, structurally validated by the host app, not by this schema."
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|