Клиентская библиотека
@webgui/client — JavaScript-библиотека для страниц, работающих внутри мода WebGUI. Один пакет покрывает чистый JS, React, Vue и Svelte.
npm install @webgui/clientReact, Vue и Svelte — опциональные peer-зависимости: импорт ядра не тянет ни одну из них, и в проекте на чистом JS пакет не добавляет ничего лишнего.
Заменяет @webgui/react, @webgui/vue и @webgui/svelte
Это были три параллельные реализации одной и той же логики, которые приходилось править синхронно при каждом новом событии мода. Они продолжают работать и теперь реэкспортируют этот пакет — если остаться на них, ничего не сломается, но новые возможности выходят только здесь. См. Переход.
Как это работает
Мод внедряет window.webgui в каждую открытую страницу и по мере изменений шлёт CustomEvent'ы на window. Библиотека подписывается при загрузке модуля и отдаёт результат как сторы; настраивать нечего, провайдер монтировать не нужно.
Вне мода — в обычной вкладке, на дев-сервере, при SSR — все сторы отдают null, а все действия ничего не делают. Одна и та же страница работает и там, и там без проверок на каждом вызове.
Чистый JavaScript
import { clientStore, runCommand, isInMod } from '@webgui/client'
if (isInMod()) {
clientStore.subscribe(() => {
const c = clientStore.get()
document.querySelector('#hp').textContent = `${c.health} / ${c.maxHealth}`
})
}
document.querySelector('#home').onclick = () => runCommand('spawn')У каждого стора одни и те же три члена:
| Член | Назначение |
|---|---|
subscribe(listener) | Регистрирует слушатель без аргументов. Возвращает функцию отписки. |
get() | Текущее значение или null, пока мод ничего не прислал. |
getServerSnapshot() | Всегда null — при серверном рендеринге мода нет. |
React
import { useWebGUIClient, isInMod, isReady } from '@webgui/client/react'
export function PlayerInfo() {
const client = useWebGUIClient()
if (!isInMod()) return <p>Откройте страницу внутри Minecraft.</p>
if (!isReady(client)) return <p>Подключаемся…</p>
return <p>Привет, {client!.username}</p>
}Нужен React 18+. Все хуки построены на useSyncExternalStore — безопасны в concurrent-режиме, провайдер не требуется.
Vue
<script setup lang="ts">
import { useWebGUIClient, runCommand } from '@webgui/client/vue'
const client = useWebGUIClient()
</script>
<template>
<p v-if="client">Привет, {{ client.username }}</p>
<button @click="runCommand('spawn')">На спавн</button>
</template>Нужен Vue 3. Composables убираются через onScopeDispose, поэтому безопасны в любом effect scope.
Svelte
<script lang="ts">
import { webguiClient, runCommand } from '@webgui/client/svelte'
</script>
{#if $webguiClient}
<p>Привет, {$webguiClient.username}</p>
{/if}
<button on:click={() => runCommand('spawn')}>На спавн</button>Нужен Svelte 4+. Экспортируются обычные readable-сторы, так что синтаксис $store работает.
Состояние
| Ядро | React | Vue | Svelte |
|---|---|---|---|
clientStore | useWebGUIClient() | useWebGUIClient() | webguiClient |
entityStore | useWebGUIEntity() | useWebGUIEntity() | webguiEntity |
deathStore | useWebGUIDeath() | useWebGUIDeath() | webguiDeath |
selectorStore(fn, eq?) | useWebGUISelector(fn, eq?) | useWebGUISelector(fn, eq?) | webguiSelector(fn, eq?) |
Клиент
Присылается на 20 TPS, раз в клиентский тик:
interface WebGUIClient {
playerUuid: string
username: string
webviewMode: 'GUI_SCREEN' | 'HUD_OVERLAY' | 'NONE'
dimension: string // например "minecraft:overworld"
pos: { x: number; y: number; z: number }
look: { yaw: number; pitch: number } // поворот головы, градусы
fov: number // вертикальный угол обзора, градусы
lookingAt: LookingAt // на что смотрит игрок
health: number
maxHealth: number
food: number // 0–20
xpLevel: number
gamemode?: 'survival' | 'creative' | 'adventure' | 'spectator'
server?: { address?: string; ping?: number }
}На что смотрит игрок
Что находится под перекрестием, обновляется вместе с остальным состоянием клиента. Это собственный ответ игры — тот, которым она рисует контур блока и имя над мобом, — а не второй луч со своим представлением о дальности.
type LookingAt =
| { type: 'none' }
| { type: 'block'; block: string; pos: { x: number; y: number; z: number };
face: 'up' | 'down' | 'north' | 'south' | 'east' | 'west'; distance: number }
| { type: 'entity'; uuid: string; entityType: string; name: string;
pos: { x: number; y: number; z: number }; distance: number }| Поле | Примечания |
|---|---|
type | Всегда присутствует. none, когда игрок не смотрит ни на что, — поле не исчезает. |
block | Registry-id, например minecraft:gold_block. |
pos | Для блока — целые координаты блока; для сущности — её собственная позиция. |
face | Сторона блока, в которую пришла линия взгляда. |
distance | От глаз до точки касания — для блока это его грань, поэтому значение примерно на полблока меньше расстояния до центра. |
Сущность
Заполняется, когда GUI открыт правым кликом по привязанной сущности (см. /webgui bind); null, если открыт командой.
interface WebGUIEntity {
uuid: string
type: string // например "minecraft:villager"
name: string // кастомное имя, иначе имя типа
pos: { x: number; y: number; z: number }
}Смерть
Заполняется только на странице, которую сервер назначил экраном смерти. Справочник по полям — webgui:death.
import { useWebGUIDeath, useRespawn } from '@webgui/client/react'
export function DeathScreen() {
const death = useWebGUIDeath()
const respawn = useRespawn()
if (!death) return null
return (
<div>
<h1>Вы погибли</h1>
<p>{death.deathMessage}</p>
{death.canRespawn && <button onClick={respawn}>Возродиться</button>}
</div>
)
}Селекторы
Мод присылает состояние клиента 20 раз в секунду, поэтому компонент, которому нужно только health, иначе перерисовывался бы на каждый шаг игрока. Селектор это сужает:
const health = useWebGUISelector((c) => c.health)Если селектор строит новый объект, передайте функцию сравнения:
const pos = useWebGUISelector(
(c) => ({ x: c.pos.x, z: c.pos.z }),
(a, b) => a.x === b.x && a.z === b.z,
)Действия
postToGame, closeGui, runCommand и respawn — обычные функции, экспортируются из всех точек входа.
postToGame({ channel: 'shop:buy', item: 'diamond' }) // → на сервер, как page event
closeGui() // закрывает GUI или HUD
runCommand('give @s minecraft:diamond 1') // выполняется от имени игрока
respawn() // только на кастомном экране смертиReact дополнительно экспортирует usePostToGame, useCloseGui, useRunCommand и useRespawn — те же функции со стабильной идентичностью для массивов зависимостей.
runCommand требует разрешения сервера
Команды принимаются только из главного фрейма origin'а, указанного в trustedCommandOrigins. Отовсюду ещё мод молча отбрасывает запрос. Выполняются с правами самого игрока, так что повышения привилегий нет.
Ограничение частоты
Сервер ограничивает число page-событий на игрока в секунду (pageEventsPerSecond, по умолчанию 20). postToGame, runCommand, closeGui и respawn учитываются все. Не вызывайте их в цикле рендера.
Хелперы
| Ядро | React | Vue | Svelte |
|---|---|---|---|
isInMod() | так же | так же | так же |
isReady(value) | так же | так же | так же |
getToken(param?) | useWebGUIToken(param?) | useWebGUIToken(param?) | webguiToken(param?) |
onWebGUIEvent(name, fn) | useWebGUIEvent(name, fn) | useWebGUIEvent(name, fn) | onWebGUIEvent(name, fn) |
getToken возвращает подписанный токен, который мод дописал в URL страницы, — для проверки на бэкенде. onWebGUIEvent подписывается на именованное событие, отправленное сервером через emitToPage; он возвращает функцию отписки, а версии для фреймворков убираются сами.
Чего браузер не делает
Страница работает в Chromium, но это страница внутри игры, а не вкладка браузера:
window.open | Ничего не делает — открывать окно негде. Открывайте страницы средствами мода. |
| Скачивание файлов | Работает: файл попадает в webgui-downloads/ в папке игры, игроку сообщается имя. |
window.webgui.assetsBase | Задан, когда файлы страницы раздаёт сервер — см. Страницы, которые раздаёт сервер. |
window.webgui.isHud | true, если страница открыта как прозрачный HUD-оверлей, а не полноэкранный GUI. Пригодится, чтобы убрать фон и не навешивать обработку ввода, которую HUD всё равно не получает. |
| Контекстное меню, печать, горячие клавиши DevTools | Недоступны. Про ошибки — Отладка страницы. |
События, которые случаются раньше вашего кода
Мод шлёт webgui:death ровно один раз, сразу после загрузки документа, и перед отправкой кладёт window.webgui.client, .entity и .death. Все сторы читают эти снапшоты при импорте модуля, поэтому компонент, смонтировавшийся тиком позже, всё равно увидит значение.
Именно поэтому стоит использовать сторы библиотеки, а не голый addEventListener внутри компонента: такой слушатель вешается уже после того, как событие произошло, и не услышит ничего.
Переход
Имена не изменились, поэтому обычно правится только путь импорта:
- import { useWebGUIClient } from '@webgui/react'
+ import { useWebGUIClient } from '@webgui/client/react'- import { useWebGUIClient } from '@webgui/vue'
+ import { useWebGUIClient } from '@webgui/client/vue'- import { webguiClient } from '@webgui/svelte'
+ import { webguiClient } from '@webgui/client/svelte'Смешивать оба варианта во время постепенного перехода безопасно: старые пакеты реэкспортируют этот, поэтому оба пути импорта ведут в один и тот же стор, а не в две копии с разными значениями.