Skip to content

Commit

Permalink
Added error messages to ui forms that need it
Browse files Browse the repository at this point in the history
  • Loading branch information
SIsilicon committed Mar 3, 2024
1 parent 0188e7a commit 6328aa3
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 64 deletions.
22 changes: 13 additions & 9 deletions src/library/@types/classes/uiFormBuilder.d.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
/* eslint-disable @typescript-eslint/ban-types */
import { Player } from "@minecraft/server";
import { Player, RawMessage } from "@minecraft/server";
import { ActionFormData, MessageFormData, ModalFormData } from "@minecraft/server-ui";

type UIFormName = `$${string}`;
type UIAction<T extends {}, S> = (ctx: MenuContext<T>, player: Player) => S;
type DynamicElem<T extends {}, S> = S | UIAction<T, S>;
type LocalizedText = string | RawMessage;

interface BaseInput<T extends {}, S> {
name: DynamicElem<T, string>;
name: DynamicElem<T, LocalizedText>;
type: string;
default?: DynamicElem<T, S>;
}
Expand All @@ -21,7 +22,7 @@ interface Slider<T extends {}> extends BaseInput<T, number> {

interface Dropdown<T extends {}> extends BaseInput<T, number> {
type: "dropdown";
options: DynamicElem<T, string[]>;
options: DynamicElem<T, LocalizedText[]>;
}

interface TextField<T extends {}> extends BaseInput<T, string> {
Expand All @@ -37,7 +38,7 @@ type Input<T extends {}> = Slider<T> | Dropdown<T> | TextField<T> | Toggle<T>;
type SubmitAction<T extends {}> = (ctx: MenuContext<T>, player: Player, input: { [key: UIFormName]: string | number | boolean }) => void;

interface Button<T extends {}> {
text: DynamicElem<T, string>;
text: DynamicElem<T, LocalizedText>;
action: UIAction<T, void>;
}

Expand All @@ -48,22 +49,22 @@ interface ActionButton<T extends {}> extends Button<T> {

interface BaseForm<T extends {}> {
/** The title of the UI form */
title: DynamicElem<T, string>;
title: DynamicElem<T, LocalizedText>;
/** Action to perform when the user exits or cancels the form */
cancel?: UIAction<T, void>;
}

/** A form with a message and two options */
interface MessageForm<T extends {}> extends BaseForm<T> {
message: DynamicElem<T, string>;
message: DynamicElem<T, LocalizedText>;
button1: Button<T>;
button2: Button<T>;
}

/** A form with an array of buttons to interact with */
interface ActionForm<T extends {}> extends BaseForm<T> {
/** Text that appears above the array of buttons */
message?: DynamicElem<T, string>;
message?: DynamicElem<T, LocalizedText>;
/** The array of buttons to interact with */
buttons: DynamicElem<T, ActionButton<T>[]>;
}
Expand All @@ -77,12 +78,15 @@ type Form<T extends {}> = MessageForm<T> | ActionForm<T> | ModalForm<T>;
type FormData = ActionFormData | MessageFormData | ModalFormData;

interface MenuContext<T extends {}> {
readonly currentMenu: UIFormName;

getData<S extends keyof T>(key: S): T[S];
setData<S extends keyof T>(key: S, value: T[S]): void;
goto(menu: UIFormName): void;
returnto(menu: UIFormName): void;
back(): void;
confirm(title: string, message: string, yes: UIAction<T, void>, no?: UIAction<T, void>): void;
confirm(title: LocalizedText, message: LocalizedText, yes: UIAction<T, void>, no?: UIAction<T, void>): void;
error(errorMessage: LocalizedText): void;
}

export { Form, FormData, UIAction, DynamicElem, MessageForm, ActionForm, SubmitAction, ModalForm, UIFormName, MenuContext };
export { Form, FormData, UIAction, DynamicElem, MessageForm, ActionForm, SubmitAction, ModalForm, UIFormName, MenuContext, LocalizedText };
49 changes: 31 additions & 18 deletions src/library/classes/uiFormBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/ban-types */
import { MessageFormData, MessageFormResponse, ActionFormData, ActionFormResponse, ModalFormData, ModalFormResponse, FormCancelationReason, FormResponse } from "@minecraft/server-ui";
import { Form, FormData, UIAction, MessageForm, ActionForm, SubmitAction, ModalForm, UIFormName, MenuContext as MenuContextType, DynamicElem } from "../@types/classes/uiFormBuilder";
import { Player } from "@minecraft/server";
import { Form, FormData, UIAction, MessageForm, ActionForm, SubmitAction, ModalForm, UIFormName, MenuContext as MenuContextType, DynamicElem, LocalizedText } from "../@types/classes/uiFormBuilder";
import { Player, RawMessage } from "@minecraft/server";
import { setTickTimeout, contentLog } from "@notbeer-api";

abstract class UIForm<T extends {}> {
Expand All @@ -13,9 +13,9 @@ abstract class UIForm<T extends {}> {
this.cancelAction = form.cancel;
}

protected abstract build(form: Form<T>, resEl: <S>(elem: DynamicElem<T, S>) => S): FormData;
protected abstract build(form: Form<T>, resEl: <S>(elem: DynamicElem<T, S>) => S, errorFmt?: RawMessage): FormData;

public abstract enter(player: Player, ctx: MenuContextType<T>): void;
public abstract enter(player: Player, ctx: MenuContextType<T>, error?: LocalizedText): void;

// eslint-disable-next-line @typescript-eslint/no-unused-vars
public exit(player: Player, ctx: MenuContextType<T>) {
Expand All @@ -33,9 +33,14 @@ abstract class UIForm<T extends {}> {
return true;
}

protected buildFormData(player: Player, ctx: MenuContext<T>) {
protected buildFormData(player: Player, ctx: MenuContext<T>, error?: LocalizedText) {
const resolve = <S>(elem: DynamicElem<T, S>): S => this.resolve(elem, player, ctx);
return this.build(this.form, resolve);
if (typeof error === "string") {
error = { rawtext: [{ text: "§c" }, { translate: error }, { text: "§r" }] };
} else if (error) {
error = { rawtext: [{ text: "§c" }, ...error.rawtext!, { text: "§r" }] };
}
return this.build(this.form, resolve, error);
}

protected resolve<S>(element: DynamicElem<T, S>, player: Player, ctx: MenuContext<T>) {
Expand Down Expand Up @@ -80,10 +85,10 @@ class MessageUIForm<T extends {}> extends UIForm<T> {
class ActionUIForm<T extends {}> extends UIForm<T> {
private actions: UIAction<T, void>[] = []; // Changes between builds

protected build(form: ActionForm<T>, resEl: <S>(elem: DynamicElem<T, S>) => S) {
protected build(form: ActionForm<T>, resEl: <S>(elem: DynamicElem<T, S>) => S, errorFmt?: RawMessage) {
this.actions = [];
const formData = new ActionFormData();
formData.title(resEl(form.title));
formData.title(errorFmt ?? resEl(form.title));

if (form.message) formData.body(resEl(form.message));
if (resEl((ctx) => (<MenuContext<T>>ctx).canGoBack())) {
Expand All @@ -97,8 +102,8 @@ class ActionUIForm<T extends {}> extends UIForm<T> {
return formData;
}

async enter(player: Player, ctx: MenuContext<T>) {
const form = this.buildFormData(player, ctx);
enter(player: Player, ctx: MenuContext<T>, error?: LocalizedText) {
const form = this.buildFormData(player, ctx, error);
const actions = this.actions;
form.show(player).then((response: ActionFormResponse) => {
if (this.handleCancel(response, player, ctx)) return;
Expand All @@ -117,10 +122,10 @@ class ModalUIForm<T extends {}> extends UIForm<T> {
this.submit = form.submit;
}

protected build(form: ModalForm<T>, resEl: <S>(elem: DynamicElem<T, S>) => S) {
protected build(form: ModalForm<T>, resEl: <S>(elem: DynamicElem<T, S>) => S, errorFmt?: RawMessage) {
this.inputNames = [];
const formData = new ModalFormData();
formData.title(resEl(form.title));
formData.title(errorFmt ?? resEl(form.title));

const formInputs = resEl(form.inputs);
for (const id in formInputs) {
Expand All @@ -140,8 +145,8 @@ class ModalUIForm<T extends {}> extends UIForm<T> {
return formData;
}

enter(player: Player, ctx: MenuContext<T>) {
const form = this.buildFormData(player, ctx);
enter(player: Player, ctx: MenuContext<T>, error: LocalizedText) {
const form = this.buildFormData(player, ctx, error);
const inputNames = this.inputNames;
form.show(player).then((response: ModalFormResponse) => {
if (this.handleCancel(response, player, ctx)) return;
Expand Down Expand Up @@ -204,14 +209,22 @@ class MenuContext<T extends {}> implements MenuContextType<T> {
form.enter(this.player, this);
}

error(errorMessage: LocalizedText) {
this._goto(this.stack[this.stack.length - 1], errorMessage);
}

canGoBack() {
return this.stack.length > 1;
}

private _goto(menu?: UIFormName) {
get currentMenu() {
return this.stack[this.stack.length - 1];
}

private _goto(menu?: UIFormName, error?: LocalizedText) {
if (menu && menu !== this.stack[this.stack.length - 1]) this.stack.push(menu);
if (this.stack.length >= 64) throw Error("UI Stack overflow!");
UIForms.goto(menu, this.player, this);
UIForms.goto(menu, this.player, this, error);
}
}

Expand Down Expand Up @@ -261,7 +274,7 @@ class UIFormBuilder {
* @param player The player to display the UI form to
* @param ctx The context to be passed to the UI form
*/
goto(name: UIFormName, player: Player, ctx: MenuContextType<{}>) {
goto(name: UIFormName, player: Player, ctx: MenuContextType<{}>, error?: LocalizedText) {
if (this.active.has(player)) {
this.active.get(player).exit(player, ctx);
this.active.delete(player);
Expand All @@ -273,7 +286,7 @@ class UIFormBuilder {
contentLog.debug("UI going to", name, "for", player.name);
const form = this.forms.get(name);
this.active.set(player, form);
form.enter(player, ctx);
form.enter(player, ctx, error);
return form;
} else {
throw new TypeError(`Menu "${name}" has not been registered!`);
Expand Down
35 changes: 24 additions & 11 deletions src/server/modules/hotbar_ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { ItemLockMode, ItemStack, ItemUseBeforeEvent, Player, system } from "@minecraft/server";
import { Server } from "@notbeer-api";
import { PlayerUtil } from "./player_util.js";
import { MenuContext, UIAction, DynamicElem, UIFormName } from "library/@types/classes/uiFormBuilder.js";
import { MenuContext, UIAction, DynamicElem, UIFormName, LocalizedText } from "library/@types/classes/uiFormBuilder.js";
import { print } from "server/util.js";

interface HotbarItem<T extends {}> {
Expand Down Expand Up @@ -139,9 +139,7 @@ class HotbarContext<T extends {}> implements MenuContext<T> {
}

goto(menu: UIFormName) {
if (menu) {
this.stack.push(menu);
}
if (menu) this.stack.push(menu);
try {
this.currentForm = HotbarUI.goto(menu, this.player, this);
} catch (e) {
Expand All @@ -155,23 +153,40 @@ class HotbarContext<T extends {}> implements MenuContext<T> {

back() {
this.stack.pop();
this.goto(this.stack.pop());
if (this.stack.length) {
this.goto(this.stack.pop()!);
} else {
this.base?.goto(this.base.currentMenu);
}
}

returnto(menu: UIFormName) {
let popped: string | undefined;
// eslint-disable-next-line no-cond-assign
while ((popped = this.stack.pop())) {
if (popped === menu) {
this.goto(menu);
return;
}
}
this.goto(undefined);
this.base?.returnto(menu);
}

confirm() {
throw "confirm() is not implemented in hotbar UI";
confirm(): void {
throw "No 'confirm' action in a hotbar UI context.";
}

error(errorMessage: LocalizedText) {
if (typeof errorMessage === "string") {
errorMessage = { rawtext: [{ text: "§c" }, { translate: errorMessage }, { text: "§r" }] };
} else if (errorMessage) {
errorMessage = { rawtext: [{ text: "§c" }, ...errorMessage.rawtext!, { text: "§r" }] };
}
print(errorMessage, this.player, true);
}

get currentMenu() {
return this.stack[this.stack.length - 1];
}
}

Expand Down Expand Up @@ -213,9 +228,7 @@ class HotbarUIBuilder {
* @param ctx The context to be passed to the UI form
*/
goto(name: UIFormName, player: Player, ctx: MenuContext<unknown>) {
if (!(ctx instanceof HotbarContext)) {
ctx = new HotbarContext(player, ctx);
}
if (!(ctx instanceof HotbarContext)) ctx = new HotbarContext(player, ctx);

if (this.active.has(player)) {
this.active.get(player).exit(player, ctx);
Expand Down
Loading

0 comments on commit 6328aa3

Please sign in to comment.