Files
bworld/common/mod_api/server.ts
T
2026-09-24 23:28:01 -03:00

253 lines
7.6 KiB
TypeScript

// what server scripts get, see "Server scripts" in MODS.md
import type { EventSignal, Face, Id, ItemStack, ModInfo, Position } from "./shared.ts";
export type { EventSignal, Face, Id, ItemStack, ModInfo, Position } from "./shared.ts";
export interface ServerContext {
mod: ModInfo;
components: ComponentRegistry; // only during setup
commands: CommandRegistry; // only during setup
events: { before: ServerBeforeEvents; after: ServerAfterEvents };
system: System;
world: ServerWorld;
players: PlayerList;
containers: ContainerApi;
recipes: RecipeApi;
ui: ServerUi;
net: ServerNet;
storage: ModStorage;
log(...args: unknown[]): void;
}
// components
export interface BlockRef {
readonly id: Id;
readonly x: number;
readonly y: number;
readonly z: number;
// tile data, any json value. saved with the world, never sent to clients
// deno-lint-ignore no-explicit-any
data: any;
}
// P is the component's params from the block json
// deno-lint-ignore no-explicit-any
export interface BlockComponent<P = any> {
on_create?(block: BlockRef, params: P): void;
on_break?(block: BlockRef, params: P, player: Player | undefined): void;
on_click?(block: BlockRef, params: P, player: Player): void;
// return true if it did something, so no block gets placed
on_interact?(block: BlockRef, params: P, player: Player): boolean;
on_tick?(block: BlockRef, params: P, dt: number): void;
on_second?(block: BlockRef, params: P, dt: number): void;
}
// deno-lint-ignore no-explicit-any
export interface ItemComponent<P = any> {
on_create?(item: ItemStack, params: P): void;
get_lore?(item: ItemStack, params: P): string;
on_use?(item: ItemStack, params: P, player: Player): void;
}
export interface ComponentRegistry {
register_block<P>(id: Id, component: BlockComponent<P>): void;
register_item<P>(id: Id, component: ItemComponent<P>): void;
}
// commands
export interface Command {
description: string;
usage: string;
run(args: string[], player: Player): void;
}
export interface CommandRegistry {
register(name: string, command: Command): void;
}
// events
export interface Cancelable {
cancel: boolean;
}
export interface BlockBreakEvent {
readonly player: Player;
readonly block: BlockRef;
readonly item: ItemStack | undefined;
}
export interface BlockPlaceEvent {
readonly player: Player;
readonly block: { readonly id: Id } & Position;
readonly face: Face;
readonly item: ItemStack | undefined;
}
export interface BlockInteractEvent {
readonly player: Player;
readonly block: BlockRef;
readonly item: ItemStack | undefined;
}
export interface ChatSendEvent {
readonly player: Player;
message: string;
}
export interface ServerBeforeEvents {
block_break: EventSignal<BlockBreakEvent & Cancelable>;
block_place: EventSignal<BlockPlaceEvent & Cancelable>;
block_interact: EventSignal<BlockInteractEvent & Cancelable>;
chat_send: EventSignal<ChatSendEvent & Cancelable>;
}
export interface ServerAfterEvents {
block_break: EventSignal<BlockBreakEvent>;
block_place: EventSignal<BlockPlaceEvent>;
block_interact: EventSignal<BlockInteractEvent>;
chat_send: EventSignal<Readonly<ChatSendEvent>>;
player_join: EventSignal<{ readonly player: Player }>;
player_leave: EventSignal<{ readonly player: Player }>;
server_start: EventSignal<Record<never, never>>;
tick: EventSignal<{ readonly dt: number }>;
}
// world and players
export interface ServerWorld {
get_block(x: number, y: number, z: number): Id | undefined; // undefined when the chunk isn't loaded
set_block(x: number, y: number, z: number, id: Id): boolean; // runs on_break / on_create, synced to everyone
get_state(x: number, y: number, z: number, name: string): number | undefined;
set_state(x: number, y: number, z: number, name: string, value: number): boolean;
get_block_data<T>(x: number, y: number, z: number): T | undefined;
is_loaded(x: number, z: number): boolean;
readonly seed: string;
}
export interface Player {
readonly id: string;
readonly name: string;
readonly position: Readonly<Position>;
readonly inventory: Container; // 36 slots, hotbar is 0-8
readonly selected_slot: number;
readonly held_item: ItemStack | undefined;
give_item(id: Id, count?: number, data?: unknown): void;
send_message(text: string): void;
teleport(x: number, y: number, z: number): void;
}
export interface PlayerList {
all(): Player[];
get(id: string): Player | undefined;
by_name(name: string): Player | undefined;
}
export interface System {
run_timeout(fn: () => void, ticks: number): number;
run_interval(fn: () => void, ticks: number): number;
clear_run(handle: number): void;
readonly current_tick: number;
}
// small key value store per mod, saved with the world
export interface ModStorage {
get<T>(key: string): T | undefined;
set(key: string, value: unknown): void; // json values only
delete(key: string): void;
}
// containers and recipes
export interface Container {
readonly id: string;
readonly size: number;
get(slot: number): ItemStack | undefined;
set(slot: number, item: ItemStack | undefined): void;
add(item: ItemStack): ItemStack | undefined; // returns what didn't fit
on_change(fn: (slot: number) => void): () => void;
}
export interface ContainerApi {
create(size: number): Container; // saved with the world
get(id: string): Container | undefined;
delete(id: string): void;
}
export interface RecipeApi {
furnace_result(input: Id): { output: ItemStack; cook_time: number } | undefined;
fuel_value(item: Id): number; // burn time in ticks, 0 if it isn't fuel
is_fuel(item: Id): boolean;
is_smeltable(item: Id): boolean;
}
// guis
export interface ActionForm {
title: string;
body?: string;
buttons: { text: string; icon?: Id }[];
}
export interface MessageForm {
title: string;
body: string;
buttons: [string, string];
}
export type FormField =
| { type: "toggle"; label: string; default?: boolean }
| { type: "slider"; label: string; min: number; max: number; step?: number; default?: number }
| { type: "dropdown"; label: string; options: string[]; default?: number }
| { type: "text"; label: string; placeholder?: string; default?: string; max_length?: number };
export interface ModalForm {
title: string;
fields: FormField[];
}
export type FormResult<T> = ({ canceled: true } & Partial<T>) | ({ canceled: false } & T);
export type SlotFilter = "smeltable" | "fuel" | ((item: ItemStack) => boolean);
export interface ContainerScreenOptions {
title: string;
container: Container;
// x and y in slot units
layout: { slot: number; x: number; y: number; filter?: SlotFilter; output_only?: boolean }[];
player_inventory?: boolean;
bars?: { id: string; x: number; y: number; texture: Id }[];
labels?: { x: number; y: number; property: string }[];
}
export interface ScreenHandle<Props = unknown> {
readonly player: Player;
set_property(id: string, value: number): void;
update(props: Props): void; // custom screens only
close(): void;
on_close(fn: () => void): void;
}
export interface ServerUi {
message_form(player: Player, form: MessageForm): Promise<FormResult<{ selection: 0 | 1 }>>;
action_form(player: Player, form: ActionForm): Promise<FormResult<{ selection: number }>>;
modal_form(player: Player, form: ModalForm): Promise<FormResult<{ values: (boolean | number | string)[] }>>;
open_container(player: Player, options: ContainerScreenOptions): ScreenHandle;
open_screen<Props>(
player: Player,
id: Id,
props: Props,
options?: { containers?: Record<string, Container> },
): ScreenHandle<Props>;
}
// mod channels
export interface ServerNet {
on<T = unknown>(channel: Id, handler: (player: Player, data: T) => void): void;
send(player: Player, channel: Id, data: unknown): void;
broadcast(channel: Id, data: unknown): void;
}