@block_factory/lib 0.0.1 → 0.0.3
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/index.d.ts +32 -1
- package/index.js +486 -19
- package/index.ts +33 -1
- package/package.json +3 -2
- package/sys/Threads.d.ts +16 -0
- package/sys/Threads.ts +43 -0
- package/tsconfig.types.json +13 -0
- package/util/Forms/Form.d.ts +16 -0
- package/util/Forms/Form.ts +31 -0
- package/util/Forms/FormAction.d.ts +41 -0
- package/util/Forms/FormAction.ts +196 -0
- package/util/Forms/FormMessage.d.ts +30 -0
- package/util/Forms/FormMessage.ts +87 -0
- package/util/Forms/FormModal.d.ts +57 -0
- package/util/Forms/FormModal.ts +183 -0
- package/util/Forms/FormRegistry.d.ts +26 -0
- package/util/Forms/FormRegistry.ts +43 -0
- package/util/Signal.d.ts +8 -4
- package/util/Signal.ts +16 -58
- package/util/System.d.ts +4 -0
- package/util/System.ts +21 -0
- package/util/Wrapper/Container.d.ts +9 -0
- package/util/Wrapper/Container.ts +34 -0
- package/util/Wrapper/IEntity.d.ts +12 -0
- package/util/Wrapper/IEntity.ts +34 -0
- package/util/Wrapper/IPlayer.d.ts +12 -0
- package/util/Wrapper/IPlayer.ts +34 -0
- package/util/Form.d.ts +0 -75
- package/util/Form.ts +0 -246
package/sys/Threads.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { system, TicksPerSecond } from "@minecraft/server";
|
|
2
|
+
import { Signal } from "../util/Signal";
|
|
3
|
+
|
|
4
|
+
class Threads {
|
|
5
|
+
public static create(): Threads { return new Threads() }
|
|
6
|
+
public readonly MAIN = new Signal<number>();
|
|
7
|
+
public readonly LATE = new Signal();
|
|
8
|
+
public MAIN_INTERVAL_RATE: number = 0;
|
|
9
|
+
public LATE_INTERVAL_RATE: number = TicksPerSecond;
|
|
10
|
+
|
|
11
|
+
private _MAIN_ID: number;
|
|
12
|
+
private _LATE_ID: number;
|
|
13
|
+
private _delta = this.createDeltaTimer();
|
|
14
|
+
|
|
15
|
+
private constructor() {
|
|
16
|
+
this._MAIN_ID = system.runInterval(() => {
|
|
17
|
+
if (this.MAIN.count <= 0) return;
|
|
18
|
+
try { const delta = this._delta(); this.MAIN.emit(delta) }
|
|
19
|
+
catch (error) {
|
|
20
|
+
throw Error(`ERROR: _THREAD_.MAIN:${this._MAIN_ID} | ${error}`)
|
|
21
|
+
}
|
|
22
|
+
}, this.MAIN_INTERVAL_RATE);
|
|
23
|
+
|
|
24
|
+
this._LATE_ID = system.runInterval(() => {
|
|
25
|
+
if (this.LATE.count <= 0) return;
|
|
26
|
+
try { this.LATE.emit() }
|
|
27
|
+
catch (error) { throw Error(`ERROR: _THREAD_.LATE:${this._LATE_ID} | ${error}`) }
|
|
28
|
+
}, this.LATE_INTERVAL_RATE);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private createDeltaTimer() {
|
|
32
|
+
let last = Date.now();
|
|
33
|
+
|
|
34
|
+
return function nextDelta(): number {
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
const delta = (now - last) / 1000; // seconds
|
|
37
|
+
last = now;
|
|
38
|
+
return delta;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const _THREAD_ = Threads.create();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"emitDeclarationOnly": true,
|
|
5
|
+
"noEmit": false,
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": ".",
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"declarationMap": true
|
|
10
|
+
},
|
|
11
|
+
"include": ["**/*.ts"],
|
|
12
|
+
"exclude": ["node_modules", "dist"]
|
|
13
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Player, RawMessage } from "@minecraft/server";
|
|
2
|
+
|
|
3
|
+
export declare type IFormValue =
|
|
4
|
+
| string
|
|
5
|
+
| boolean
|
|
6
|
+
| number
|
|
7
|
+
| undefined;
|
|
8
|
+
|
|
9
|
+
export declare abstract class IForm {
|
|
10
|
+
static returnText: RawMessage | string;
|
|
11
|
+
static isOccupied(player: Player): boolean;
|
|
12
|
+
static closeForms(player: Player): void;
|
|
13
|
+
static closeOccupiedForms(): void;
|
|
14
|
+
protected setOccupied(player: Player, occupied: boolean): void;
|
|
15
|
+
protected abstract build(player: Player): void;
|
|
16
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Player, RawMessage, world } from "@minecraft/server";
|
|
2
|
+
import { uiManager } from "@minecraft/server-ui";
|
|
3
|
+
|
|
4
|
+
export declare type IFormValue = string | boolean | number | undefined;
|
|
5
|
+
|
|
6
|
+
export abstract class IForm {
|
|
7
|
+
private static readonly occupiedPlayers: Set<string> = new Set();
|
|
8
|
+
public static returnText: RawMessage | string = "Back";
|
|
9
|
+
|
|
10
|
+
public static isOccupied(player: Player): boolean {
|
|
11
|
+
return IForm.occupiedPlayers.has(player.id);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public static closeForms(player: Player): void {
|
|
15
|
+
uiManager.closeAllForms(player);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public static closeOccupiedForms(): void {
|
|
19
|
+
for (const playerId of IForm.occupiedPlayers) {
|
|
20
|
+
const player = world.getEntity(playerId) as Player | undefined;
|
|
21
|
+
if (player) uiManager.closeAllForms(player);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
protected setOccupied(player: Player, occupied: boolean): void {
|
|
26
|
+
if (occupied) IForm.occupiedPlayers.add(player.id);
|
|
27
|
+
else IForm.occupiedPlayers.delete(player.id);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
protected abstract build(player: Player): void;
|
|
31
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { IForm } from "./Form";
|
|
2
|
+
import { Player, RawMessage } from "@minecraft/server";
|
|
3
|
+
import { ActionFormData, FormCancelationReason } from "@minecraft/server-ui";
|
|
4
|
+
|
|
5
|
+
export declare type ActionFormCtor<T extends IActionFormData = IActionFormData> = new () => T;
|
|
6
|
+
|
|
7
|
+
export declare interface IButtonOptions {
|
|
8
|
+
id?: string | number;
|
|
9
|
+
iconPath?: string;
|
|
10
|
+
subForm?: ActionFormCtor;
|
|
11
|
+
allowReturn?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export declare interface IActionFormResponse {
|
|
15
|
+
readonly cancelationReason?: FormCancelationReason;
|
|
16
|
+
readonly canceled: boolean;
|
|
17
|
+
readonly selection?: number;
|
|
18
|
+
readonly id?: string | number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export declare abstract class IActionFormData extends IForm {
|
|
22
|
+
protected form: ActionFormData;
|
|
23
|
+
|
|
24
|
+
protected abstract build(player: Player): void;
|
|
25
|
+
|
|
26
|
+
protected abstract onSubmit(player: Player, response: IActionFormResponse): void;
|
|
27
|
+
|
|
28
|
+
protected onCancel?(player: Player, response: IActionFormResponse): void;
|
|
29
|
+
|
|
30
|
+
title(titleText: RawMessage | string): this;
|
|
31
|
+
body(bodyText: RawMessage | string): this;
|
|
32
|
+
divider(): this;
|
|
33
|
+
header(text: RawMessage | string): this;
|
|
34
|
+
label(text: RawMessage | string): this;
|
|
35
|
+
|
|
36
|
+
button(text: RawMessage | string, options?: IButtonOptions): this;
|
|
37
|
+
|
|
38
|
+
show(player: Player): Promise<IActionFormResponse>;
|
|
39
|
+
|
|
40
|
+
protected _showChained(player: Player, returnStack: ActionFormCtor[]): Promise<IActionFormResponse>;
|
|
41
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { IForm } from "./Form";
|
|
2
|
+
import { Player, RawMessage } from "@minecraft/server";
|
|
3
|
+
import { ActionFormData, ActionFormResponse, FormCancelationReason } from "@minecraft/server-ui";
|
|
4
|
+
|
|
5
|
+
const RETURN_ID = "d7213196-4cb6-4199-a40b-22fbf2d944ef";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Constructor type for ActionForm classes.
|
|
9
|
+
* Used for sub-form navigation and chaining.
|
|
10
|
+
*/
|
|
11
|
+
export type ActionFormCtor<T extends IActionFormData = IActionFormData> = new () => T;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Options used when adding a button to an ActionForm.
|
|
15
|
+
*/
|
|
16
|
+
export interface IButtonOptions {
|
|
17
|
+
/**
|
|
18
|
+
* Logical identifier returned in the form response.
|
|
19
|
+
* If omitted, the numeric button index is used.
|
|
20
|
+
*/
|
|
21
|
+
id?: string | number;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Optional icon path from a resource pack.
|
|
25
|
+
*/
|
|
26
|
+
iconPath?: string;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Form class to open when this button is selected.
|
|
30
|
+
*/
|
|
31
|
+
subForm?: ActionFormCtor;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Whether the sub-form is allowed to return to this form.
|
|
35
|
+
* Defaults to true.
|
|
36
|
+
*/
|
|
37
|
+
allowReturn?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type ButtonMeta = {
|
|
41
|
+
id: string | number;
|
|
42
|
+
subForm?: ActionFormCtor;
|
|
43
|
+
allowReturn: boolean;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Normalized response object returned by IActionFormData.
|
|
48
|
+
*/
|
|
49
|
+
export interface IActionFormResponse {
|
|
50
|
+
/**Reason the form was canceled, if applicable.*/
|
|
51
|
+
readonly cancelationReason?: FormCancelationReason;
|
|
52
|
+
/** Whether the form was canceled by the player.*/
|
|
53
|
+
readonly canceled: boolean;
|
|
54
|
+
/**Raw button index selected by the player.*/
|
|
55
|
+
readonly selection?: number;
|
|
56
|
+
/**Logical button id associated with the selection.*/
|
|
57
|
+
readonly id?: string | number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Abstract base class for ActionForm-based menus.
|
|
62
|
+
*
|
|
63
|
+
* Provides:
|
|
64
|
+
* - Button id mapping
|
|
65
|
+
* - Occupied player handling
|
|
66
|
+
* - Sub-form chaining with automatic Back navigation
|
|
67
|
+
* - Normalized response objects
|
|
68
|
+
*/
|
|
69
|
+
export abstract class IActionFormData extends IForm {
|
|
70
|
+
protected form: ActionFormData = new ActionFormData();
|
|
71
|
+
|
|
72
|
+
private _buttons = new Map<number, ButtonMeta>();
|
|
73
|
+
private _nextButtonIndex = 0;
|
|
74
|
+
private _id: string = "untitled";
|
|
75
|
+
|
|
76
|
+
protected abstract build(player: Player): void;
|
|
77
|
+
|
|
78
|
+
protected abstract onSubmit(player: Player, response: IActionFormResponse): void;
|
|
79
|
+
|
|
80
|
+
protected onCancel?(player: Player, response: IActionFormResponse): void;
|
|
81
|
+
|
|
82
|
+
public title(titleText: RawMessage | string): this {
|
|
83
|
+
this.form.title(titleText);
|
|
84
|
+
this._id = typeof titleText === "string" ? titleText : JSON.stringify(titleText);
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public body(bodyText: RawMessage | string): this { this.form.body(bodyText); return this; }
|
|
89
|
+
|
|
90
|
+
public divider(): this { this.form.divider(); return this; }
|
|
91
|
+
|
|
92
|
+
public header(text: RawMessage | string): this { this.form.header(text); return this; }
|
|
93
|
+
|
|
94
|
+
public label(text: RawMessage | string): this { this.form.label(text); return this; }
|
|
95
|
+
|
|
96
|
+
public button(text: RawMessage | string, options: IButtonOptions = {}): this {
|
|
97
|
+
if (options.iconPath) this.form.button(text, options.iconPath);
|
|
98
|
+
else this.form.button(text);
|
|
99
|
+
|
|
100
|
+
const id: string | number = options.id ?? this._nextButtonIndex;
|
|
101
|
+
const allowReturn = options.allowReturn ?? true;
|
|
102
|
+
|
|
103
|
+
this._buttons.set(this._nextButtonIndex, {
|
|
104
|
+
id,
|
|
105
|
+
subForm: options.subForm,
|
|
106
|
+
allowReturn,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
this._nextButtonIndex++;
|
|
110
|
+
return this;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public async show(player: Player): Promise<IActionFormResponse> {
|
|
114
|
+
if (IForm.isOccupied(player)) {
|
|
115
|
+
throw new Error(`Player ${player.id} is already occupied by form: ${this._id}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this.setOccupied(player, true);
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
return await this._showInternal(player, []);
|
|
122
|
+
} finally {
|
|
123
|
+
this.setOccupied(player, false);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
protected async _showChained(player: Player, returnStack: ActionFormCtor[]): Promise<IActionFormResponse> {
|
|
128
|
+
return await this._showInternal(player, returnStack);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private _wrapResponse(response: ActionFormResponse, id?: string | number): IActionFormResponse {
|
|
132
|
+
return {
|
|
133
|
+
canceled: response.canceled,
|
|
134
|
+
cancelationReason: response.cancelationReason,
|
|
135
|
+
selection: response.selection,
|
|
136
|
+
id,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private async _showInternal(player: Player, returnStack: ActionFormCtor[]): Promise<IActionFormResponse> {
|
|
141
|
+
|
|
142
|
+
this.form = new ActionFormData();
|
|
143
|
+
|
|
144
|
+
this._buttons.clear();
|
|
145
|
+
this._nextButtonIndex = 0;
|
|
146
|
+
|
|
147
|
+
this.build(player);
|
|
148
|
+
|
|
149
|
+
if (returnStack.length > 0) {
|
|
150
|
+
this.form.button(IForm.returnText);
|
|
151
|
+
this._buttons.set(this._nextButtonIndex, {
|
|
152
|
+
id: RETURN_ID,
|
|
153
|
+
subForm: returnStack[returnStack.length - 1],
|
|
154
|
+
allowReturn: true,
|
|
155
|
+
});
|
|
156
|
+
this._nextButtonIndex++;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const response = await this.form.show(player);
|
|
160
|
+
|
|
161
|
+
if (response.canceled) {
|
|
162
|
+
const wrapped = this._wrapResponse(response, undefined);
|
|
163
|
+
this.onCancel?.(player, wrapped);
|
|
164
|
+
return wrapped;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const meta =
|
|
168
|
+
response.selection !== undefined
|
|
169
|
+
? this._buttons.get(response.selection)
|
|
170
|
+
: undefined;
|
|
171
|
+
|
|
172
|
+
if (meta?.id === RETURN_ID && meta.subForm) {
|
|
173
|
+
const prevCtor = returnStack[returnStack.length - 1];
|
|
174
|
+
const newStack = returnStack.slice(0, -1);
|
|
175
|
+
const prev = new prevCtor();
|
|
176
|
+
return await prev._showChained(player, newStack);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const wrapped = this._wrapResponse(response, meta?.id);
|
|
180
|
+
|
|
181
|
+
this.onSubmit(player, wrapped);
|
|
182
|
+
|
|
183
|
+
if (meta?.subForm) {
|
|
184
|
+
const next = new meta.subForm();
|
|
185
|
+
const allowReturn = meta.allowReturn ?? true;
|
|
186
|
+
|
|
187
|
+
const nextStack = allowReturn
|
|
188
|
+
? [...returnStack, this.constructor as ActionFormCtor]
|
|
189
|
+
: [...returnStack];
|
|
190
|
+
|
|
191
|
+
return await next._showChained(player, nextStack);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return wrapped;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { IForm } from "./Form";
|
|
2
|
+
import { Player, RawMessage } from "@minecraft/server";
|
|
3
|
+
import { MessageFormData, MessageFormResponse } from "@minecraft/server-ui";
|
|
4
|
+
|
|
5
|
+
export declare abstract class IMessageFormData extends IForm {
|
|
6
|
+
protected form: MessageFormData;
|
|
7
|
+
|
|
8
|
+
private _id: string;
|
|
9
|
+
|
|
10
|
+
protected abstract build(player: Player): void;
|
|
11
|
+
|
|
12
|
+
protected abstract onSubmit(
|
|
13
|
+
player: Player,
|
|
14
|
+
response: MessageFormResponse
|
|
15
|
+
): void;
|
|
16
|
+
|
|
17
|
+
protected onCancel?(player: Player, response: MessageFormResponse): void;
|
|
18
|
+
|
|
19
|
+
title(titleText: RawMessage | string): this;
|
|
20
|
+
|
|
21
|
+
body(bodyText: RawMessage | string): this;
|
|
22
|
+
|
|
23
|
+
button0(text: RawMessage | string, id?: string | number): this;
|
|
24
|
+
|
|
25
|
+
button1(text: RawMessage | string, id?: string | number): this;
|
|
26
|
+
|
|
27
|
+
show(player: Player): Promise<MessageFormResponse>;
|
|
28
|
+
|
|
29
|
+
private _showInternal(player: Player): Promise<MessageFormResponse>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { IForm } from "./Form";
|
|
2
|
+
import { Player, RawMessage } from "@minecraft/server";
|
|
3
|
+
import {
|
|
4
|
+
MessageFormData,
|
|
5
|
+
MessageFormResponse,
|
|
6
|
+
} from "@minecraft/server-ui";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Abstract base class for MessageFormData.
|
|
10
|
+
*
|
|
11
|
+
* Provides:
|
|
12
|
+
* - occupied player handling
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export type IMessageFormResponse = MessageFormResponse; // re-export wrapper for consistency
|
|
16
|
+
|
|
17
|
+
export abstract class IMessageFormData extends IForm {
|
|
18
|
+
protected form: MessageFormData = new MessageFormData();
|
|
19
|
+
|
|
20
|
+
private _id: string = "untitled";
|
|
21
|
+
|
|
22
|
+
protected abstract build(player: Player): void;
|
|
23
|
+
|
|
24
|
+
protected abstract onSubmit(
|
|
25
|
+
player: Player,
|
|
26
|
+
response: IMessageFormResponse
|
|
27
|
+
): void;
|
|
28
|
+
|
|
29
|
+
protected onCancel?(player: Player, response: IMessageFormResponse): void;
|
|
30
|
+
|
|
31
|
+
public title(titleText: RawMessage | string): this {
|
|
32
|
+
this.form.title(titleText);
|
|
33
|
+
this._id =
|
|
34
|
+
typeof titleText === "string"
|
|
35
|
+
? titleText
|
|
36
|
+
: JSON.stringify(titleText);
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public body(bodyText: RawMessage | string): this {
|
|
41
|
+
this.form.body(bodyText);
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
public button0(text: RawMessage | string,id: string | number = 0): this {
|
|
45
|
+
this.form.button1(text);
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public button1(text: RawMessage | string,id: string | number = 1): this {
|
|
50
|
+
this.form.button2(text);
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public async show(player: Player): Promise<IMessageFormResponse> {
|
|
55
|
+
if (IForm.isOccupied(player)) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Player ${player.id} is already occupied by form: ${this._id}`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.setOccupied(player, true);
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
return await this._showInternal(player);
|
|
65
|
+
} finally {
|
|
66
|
+
this.setOccupied(player, false);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private async _showInternal(
|
|
71
|
+
player: Player
|
|
72
|
+
): Promise<IMessageFormResponse> {
|
|
73
|
+
this.form = new MessageFormData();
|
|
74
|
+
|
|
75
|
+
this.build(player);
|
|
76
|
+
|
|
77
|
+
const res = await this.form.show(player);
|
|
78
|
+
|
|
79
|
+
if (res.canceled || res.selection === undefined) {
|
|
80
|
+
this.onCancel?.(player, res);
|
|
81
|
+
return res;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.onSubmit(player, res);
|
|
85
|
+
return res;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { IForm, IFormValue } from "./Form";
|
|
2
|
+
import { Player, RawMessage } from "@minecraft/server";
|
|
3
|
+
import { ModalFormData, FormCancelationReason } from "@minecraft/server-ui";
|
|
4
|
+
|
|
5
|
+
export declare interface IModalFormResponse {
|
|
6
|
+
readonly cancelationReason?: FormCancelationReason;
|
|
7
|
+
readonly canceled: boolean;
|
|
8
|
+
readonly formValues?: IFormValue[];
|
|
9
|
+
readonly widget?: Map<string, IFormValue>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export declare interface ITextFieldOptions {
|
|
13
|
+
id: string;
|
|
14
|
+
placeholder: string | RawMessage;
|
|
15
|
+
defaultValue?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export declare interface IDropdownOptions {
|
|
19
|
+
id: string;
|
|
20
|
+
choices: string[];
|
|
21
|
+
defaultValueIndex?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export declare interface ISliderOptions {
|
|
25
|
+
id: string;
|
|
26
|
+
min: number;
|
|
27
|
+
max: number;
|
|
28
|
+
step: number;
|
|
29
|
+
defaultValue?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export declare interface IToggleOptions {
|
|
33
|
+
id: string;
|
|
34
|
+
defaultValue?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export declare abstract class IModalFormData extends IForm {
|
|
38
|
+
protected form: ModalFormData;
|
|
39
|
+
|
|
40
|
+
protected abstract build(player: Player): void;
|
|
41
|
+
|
|
42
|
+
protected abstract onSubmit(player: Player, response: IModalFormResponse): void;
|
|
43
|
+
|
|
44
|
+
protected onCancel?(player: Player, response: IModalFormResponse): void;
|
|
45
|
+
|
|
46
|
+
title(titleText: RawMessage | string): this;
|
|
47
|
+
divider(): this;
|
|
48
|
+
header(text: RawMessage | string): this;
|
|
49
|
+
label(text: RawMessage | string): this;
|
|
50
|
+
|
|
51
|
+
textField(label: string | RawMessage, options: ITextFieldOptions): this;
|
|
52
|
+
dropdown(label: string | RawMessage, options: IDropdownOptions): this;
|
|
53
|
+
slider(label: string | RawMessage, options: ISliderOptions): this;
|
|
54
|
+
toggle(label: string | RawMessage, options: IToggleOptions): this;
|
|
55
|
+
|
|
56
|
+
show(player: Player): Promise<IModalFormResponse>;
|
|
57
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { IForm } from "./Form";
|
|
2
|
+
import { Player, RawMessage } from "@minecraft/server";
|
|
3
|
+
import {
|
|
4
|
+
ModalFormData,
|
|
5
|
+
ModalFormResponse,
|
|
6
|
+
FormCancelationReason,
|
|
7
|
+
ModalFormDataDropdownOptions,
|
|
8
|
+
ModalFormDataSliderOptions,
|
|
9
|
+
ModalFormDataTextFieldOptions,
|
|
10
|
+
ModalFormDataToggleOptions,
|
|
11
|
+
} from "@minecraft/server-ui";
|
|
12
|
+
|
|
13
|
+
export type FormValue = string | boolean | number | undefined;
|
|
14
|
+
|
|
15
|
+
/** Normalized response for Modal forms */
|
|
16
|
+
export interface IModalFormResponse {
|
|
17
|
+
/**Reason the form was canceled, if applicable.*/
|
|
18
|
+
readonly cancelationReason?: FormCancelationReason;
|
|
19
|
+
/** Whether the form was canceled by the player.*/
|
|
20
|
+
readonly canceled: boolean;
|
|
21
|
+
/** Raw values array from the form */
|
|
22
|
+
readonly formValues?: FormValue[];
|
|
23
|
+
/** Mapped values by widget id */
|
|
24
|
+
readonly widget?: Map<string, FormValue>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Options for each widget type */
|
|
28
|
+
export interface ITextFieldOptions {
|
|
29
|
+
id: string;
|
|
30
|
+
placeholder: string | RawMessage;
|
|
31
|
+
defaultValue?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface IDropdownOptions {
|
|
34
|
+
id: string;
|
|
35
|
+
choices: string[];
|
|
36
|
+
defaultValueIndex?: number;
|
|
37
|
+
}
|
|
38
|
+
export interface ISliderOptions {
|
|
39
|
+
id: string;
|
|
40
|
+
min: number;
|
|
41
|
+
max: number;
|
|
42
|
+
step: number;
|
|
43
|
+
defaultValue?: number;
|
|
44
|
+
}
|
|
45
|
+
export interface IToggleOptions {
|
|
46
|
+
id: string;
|
|
47
|
+
defaultValue?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type WidgetMeta =
|
|
51
|
+
| { type: "textField"; id: string }
|
|
52
|
+
| { type: "dropdown"; id: string }
|
|
53
|
+
| { type: "slider"; id: string }
|
|
54
|
+
| { type: "toggle"; id: string };
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Abstract base class for ModalFormData-based menus.
|
|
58
|
+
*
|
|
59
|
+
* Provides:
|
|
60
|
+
* - Occupied player handling
|
|
61
|
+
* - Widget id mapping into a Map<string, FormValue>
|
|
62
|
+
* - Normalized response object
|
|
63
|
+
*/
|
|
64
|
+
export abstract class IModalFormData extends IForm {
|
|
65
|
+
protected form: ModalFormData = new ModalFormData();
|
|
66
|
+
|
|
67
|
+
private _id: string = "untitled";
|
|
68
|
+
private _widgetMeta: WidgetMeta[] = [];
|
|
69
|
+
|
|
70
|
+
protected abstract build(player: Player): void;
|
|
71
|
+
|
|
72
|
+
protected abstract onSubmit(player: Player, response: IModalFormResponse): void;
|
|
73
|
+
|
|
74
|
+
protected onCancel?(player: Player, response: IModalFormResponse): void;
|
|
75
|
+
|
|
76
|
+
public title(titleText: RawMessage | string): this {
|
|
77
|
+
this.form.title(titleText);
|
|
78
|
+
this._id = typeof titleText === "string" ? titleText : JSON.stringify(titleText);
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public divider(): this {
|
|
83
|
+
this.form.divider();
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public header(text: RawMessage | string): this {
|
|
88
|
+
this.form.header(text);
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
public label(text: RawMessage | string): this {
|
|
93
|
+
this.form.label(text);
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public textField(label: string | RawMessage, options: ITextFieldOptions): this {
|
|
98
|
+
const opts: ModalFormDataTextFieldOptions = { defaultValue: options.defaultValue };
|
|
99
|
+
this.form.textField(label, options.placeholder, opts);
|
|
100
|
+
this._widgetMeta.push({ type: "textField", id: options.id });
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public dropdown(label: string | RawMessage, options: IDropdownOptions): this {
|
|
105
|
+
const opts: ModalFormDataDropdownOptions = { defaultValueIndex: options.defaultValueIndex };
|
|
106
|
+
this.form.dropdown(label, options.choices, opts);
|
|
107
|
+
this._widgetMeta.push({ type: "dropdown", id: options.id });
|
|
108
|
+
return this;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
public slider(label: string | RawMessage, options: ISliderOptions): this {
|
|
112
|
+
const opts: ModalFormDataSliderOptions = {
|
|
113
|
+
defaultValue: options.defaultValue,
|
|
114
|
+
valueStep: options.step,
|
|
115
|
+
};
|
|
116
|
+
this.form.slider(label, options.min, options.max, opts);
|
|
117
|
+
this._widgetMeta.push({ type: "slider", id: options.id });
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public toggle(label: string | RawMessage, options: IToggleOptions): this {
|
|
122
|
+
const opts: ModalFormDataToggleOptions = { defaultValue: options.defaultValue };
|
|
123
|
+
this.form.toggle(label, opts);
|
|
124
|
+
this._widgetMeta.push({ type: "toggle", id: options.id });
|
|
125
|
+
return this;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
public async show(player: Player): Promise<IModalFormResponse> {
|
|
129
|
+
if (IForm.isOccupied(player)) {
|
|
130
|
+
throw new Error(`Player ${player.id} is already occupied by form: ${this._id}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
this.setOccupied(player, true);
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
return await this._showInternal(player);
|
|
137
|
+
} finally {
|
|
138
|
+
this.setOccupied(player, false);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private _wrapResponse(res: ModalFormResponse, values?: Map<string, FormValue>): IModalFormResponse {
|
|
143
|
+
return {
|
|
144
|
+
canceled: res.canceled,
|
|
145
|
+
cancelationReason: res.cancelationReason,
|
|
146
|
+
formValues: res.formValues as FormValue[] | undefined,
|
|
147
|
+
widget: values,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private _mapValues(formValues: FormValue[] | undefined): Map<string, FormValue> | undefined {
|
|
152
|
+
if (!formValues) return undefined;
|
|
153
|
+
|
|
154
|
+
const out = new Map<string, FormValue>();
|
|
155
|
+
for (let i = 0; i < formValues.length; i++) {
|
|
156
|
+
const meta = this._widgetMeta[i];
|
|
157
|
+
if (!meta) continue;
|
|
158
|
+
out.set(meta.id, formValues[i]);
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private async _showInternal(player: Player): Promise<IModalFormResponse> {
|
|
164
|
+
this.form = new ModalFormData();
|
|
165
|
+
this._widgetMeta = [];
|
|
166
|
+
|
|
167
|
+
this.build(player);
|
|
168
|
+
|
|
169
|
+
const res = await this.form.show(player);
|
|
170
|
+
|
|
171
|
+
if (res.canceled) {
|
|
172
|
+
const wrapped = this._wrapResponse(res);
|
|
173
|
+
this.onCancel?.(player, wrapped);
|
|
174
|
+
return wrapped;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const values = this._mapValues(res.formValues as FormValue[] | undefined);
|
|
178
|
+
const wrapped = this._wrapResponse(res, values);
|
|
179
|
+
|
|
180
|
+
this.onSubmit(player, wrapped);
|
|
181
|
+
return wrapped;
|
|
182
|
+
}
|
|
183
|
+
}
|