hytale-ui-windows
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseHytale UI Windows
Hytale UI 窗口开发指南
Complete guide for creating custom UI windows, container interfaces, and interactive menus in Hytale server plugins.
本指南详细介绍如何为Hytale服务器插件创建自定义UI窗口、容器界面和交互式菜单。
When to use this skill
何时使用本技能
Use this skill when:
- Creating custom inventory windows
- Building container interfaces (chests, benches)
- Implementing crafting UI systems
- Making interactive menus
- Handling window actions and clicks
- Syncing window state between server and client
- Creating .ui layout files for custom pages
- Designing HUD elements and overlays
在以下场景中使用本技能:
- 创建自定义背包窗口
- 构建容器界面(箱子、工作台)
- 实现合成UI系统
- 制作交互式菜单
- 处理窗口操作与点击事件
- 同步服务器与客户端的窗口状态
- 为自定义页面创建.ui布局文件
- 设计HUD元素与覆盖层
UI System Overview
UI系统概述
Hytale's UI system consists of two main approaches:
-
Window System (Java) - For inventory containers, crafting benches, and block-tied UIs
- Uses classes with
WindowWindowManager - Sends JSON data via
getData() - Handles predefined types
WindowAction
- Uses
-
Custom UI Pages (Java) - For dynamic forms, lists, dialogs, and interactive pages
- Uses classes with
CustomUIPagePageManager - Loads files dynamically via
.uiUICommandBuilder - Binds events with typed data via
UIEventBuilder
- Uses
Both systems use client-side .ui files to define visual layout and styling.
Hytale的UI系统主要包含两种实现方式:
-
窗口系统(Java) - 适用于背包容器、合成台以及与方块绑定的UI
- 使用类配合
WindowWindowManager - 通过发送JSON数据
getData() - 处理预定义的类型
WindowAction
- 使用
-
自定义UI页面(Java) - 适用于动态表单、列表、对话框和交互式页面
- 使用类配合
CustomUIPagePageManager - 通过动态加载.ui文件
UICommandBuilder - 通过绑定带类型数据的事件
UIEventBuilder
- 使用
两种系统都使用客户端.ui文件来定义视觉布局和样式。
.ui Files
.ui文件
UI files () are client-side layout files that define the visual structure of windows and pages. They use a declarative syntax with:
.ui- Variables () - Reusable values and styles
@Name = value; - Imports () - Reference other UI files
$C = "path/to/file.ui"; - Elements () - UI widgets with nested children
WidgetType { properties } - Templates () - Instantiate reusable components
$C.@TemplateName { overrides }
UI文件(.ui)是客户端布局文件,用于定义窗口和页面的视觉结构。它使用声明式语法,包含:
- 变量()- 可复用的值和样式
@Name = value; - 导入()- 引用其他UI文件
$C = "path/to/file.ui"; - 元素()- 包含子元素的UI组件
WidgetType { properties } - 模板()- 实例化可复用组件
$C.@TemplateName { overrides }
IMPORTANT: File Location
重要提示:文件位置
All files MUST be placed in in your plugin JAR.
.uiresources/Common/UI/Custom/your-plugin/
src/main/resources/
manifest.json # Must have "IncludesAssetPack": true
Common/
UI/
Custom/
MyPage.ui # Your custom UI files go here
MyHud.ui
ListItem.uiRequirements:
- Your MUST contain
manifest.json"IncludesAssetPack": true - UI files go in (NOT
resources/Common/UI/Custom/)assets/Server/Content/UI/Custom/ - In Java code, reference files by filename only:
commandBuilder.append("MyPage.ui")
Common Error:
Could not find document XXXXX for Custom UI Append command- This means your file is not in
.uior the path is wrongCommon/UI/Custom/ - Double-check the file location and that is set to
IncludesAssetPacktrue
所有.ui文件必须放置在插件JAR包的目录下。
resources/Common/UI/Custom/your-plugin/
src/main/resources/
manifest.json # 必须包含"IncludesAssetPack": true
Common/
UI/
Custom/
MyPage.ui # 自定义UI文件存放位置
MyHud.ui
ListItem.ui要求:
- 必须包含
manifest.json"IncludesAssetPack": true - UI文件需放在(而非
resources/Common/UI/Custom/)assets/Server/Content/UI/Custom/ - 在Java代码中,仅通过文件名引用文件:
commandBuilder.append("MyPage.ui")
常见错误:
Could not find document XXXXX for Custom UI Append command- 该错误表示.ui文件未在目录下,或路径错误
Common/UI/Custom/ - 请仔细检查文件位置,并确认已设置为
IncludesAssetPacktrue
Basic .ui File Structure
基础.ui文件结构
$C = "../Common.ui";
$C.@PageOverlay {} // Dark background overlay
$C.@Container {
Anchor: (Width: 600, Height: 400);
#Title {
$C.@Title { @Text = %page.title; }
}
#Content {
LayoutMode: Top;
Label #ValueLabel { Text: ""; } // ID for code access
$C.@TextButton #ActionBtn {
@Text = %page.action;
}
}
}
$C.@BackButton {}$C = "../Common.ui";
$C.@PageOverlay {} // 深色背景覆盖层
$C.@Container {
Anchor: (Width: 600, Height: 400);
#Title {
$C.@Title { @Text = %page.title; }
}
#Content {
LayoutMode: Top;
Label #ValueLabel { Text: ""; } // 供代码访问的ID
$C.@TextButton #ActionBtn {
@Text = %page.action;
}
}
}
$C.@BackButton {}Key Concepts
核心语法概念
| Syntax | Purpose | Example |
|---|---|---|
| Variable definition | |
| Import file | |
| Use template | |
| Element ID for code | |
| Translation key | |
| Spread/extend | |
See for complete .ui file documentation.
references/ui-file-syntax.md| 语法 | 用途 | 示例 |
|---|---|---|
| 变量定义 | |
| 导入文件 | |
| 使用模板 | |
| 供代码访问的元素ID | |
| 翻译键 | |
| 扩展样式 | |
查看获取完整的.ui文件文档。
references/ui-file-syntax.mdWindow Architecture Overview
窗口架构概述
Hytale uses a window system for server-controlled UI. Windows are opened server-side and rendered client-side, with actions sent back to the server for processing. Window data is transmitted as JSON and inventory contents are synced separately.
Hytale使用窗口系统实现服务器控制的UI。窗口在服务器端打开,在客户端渲染,用户操作会发送回服务器处理。窗口数据以JSON格式传输,背包内容则单独同步。
Window Class Hierarchy
窗口类层级
Window (abstract)
├── ContainerWindow # Simple item container (implements ItemContainerWindow)
├── ItemStackContainerWindow # Container tied to an ItemStack (implements ItemContainerWindow)
├── FieldCraftingWindow # Pocket/inventory crafting (WindowType.PocketCrafting)
├── MemoriesWindow # Memories/achievements display (WindowType.Memories)
└── BlockWindow (abstract) # Tied to a block in the world (implements ValidatedWindow)
├── ContainerBlockWindow # Container tied to a block (implements ItemContainerWindow)
└── BenchWindow (abstract) # Crafting bench base (implements MaterialContainerWindow)
├── ProcessingBenchWindow # Furnace-like processing (implements ItemContainerWindow)
└── CraftingWindow (abstract)
├── SimpleCraftingWindow # Basic workbench crafting (implements MaterialContainerWindow)
├── DiagramCraftingWindow # Blueprint/anvil crafting (implements ItemContainerWindow)
└── StructuralCraftingWindow # Block transformation crafting (implements ItemContainerWindow)Window (抽象类)
├── ContainerWindow // 简单物品容器(实现ItemContainerWindow)
├── ItemStackContainerWindow // 与物品栈绑定的容器(实现ItemContainerWindow)
├── FieldCraftingWindow // 随身/背包合成(WindowType.PocketCrafting)
├── MemoriesWindow // 回忆/成就展示(WindowType.Memories)
└── BlockWindow (抽象类) // 与世界方块绑定的UI(实现ValidatedWindow)
├── ContainerBlockWindow // 与方块绑定的容器(实现ItemContainerWindow)
└── BenchWindow (抽象类) // 合成台基类(实现MaterialContainerWindow)
├── ProcessingBenchWindow // 熔炉类加工界面(实现ItemContainerWindow)
└── CraftingWindow (抽象类)
├── SimpleCraftingWindow // 基础工作台合成(实现MaterialContainerWindow)
├── DiagramCraftingWindow // 蓝图/铁砧合成(实现ItemContainerWindow)
└── StructuralCraftingWindow // 方块转换合成(实现ItemContainerWindow)Key Interfaces
核心接口
| Interface | Purpose |
|---|---|
| Windows with item inventory slots |
| Windows with extra resource materials |
| Windows that validate state (e.g., player distance) |
| 接口 | 用途 |
|---|---|
| 包含物品背包槽位的窗口 |
| 包含额外资源材料的窗口 |
| 需要验证状态的窗口(如玩家距离) |
Window Types (WindowType Enum)
窗口类型(WindowType枚举)
| WindowType | Value | Description | Use Case |
|---|---|---|---|
| 0 | Item storage | Chests, backpacks |
| 1 | Field crafting | Player inventory crafting |
| 2 | Standard crafting | Crafting tables |
| 3 | Blueprint-based | Advanced workbenches, anvils |
| 4 | Block transformation | Stonecutters, construction benches |
| 5 | Time-based conversion | Furnaces, smelters |
| 6 | Special display | Memory/achievement UI |
| 窗口类型 | 数值 | 描述 | 使用场景 |
|---|---|---|---|
| 0 | 物品存储 | 箱子、背包 |
| 1 | 随身合成 | 玩家背包内的合成界面 |
| 2 | 标准合成 | 工作台 |
| 3 | 蓝图合成 | 高级工作台、铁砧 |
| 4 | 方块转换 | 切石机、建筑工作台 |
| 5 | 计时转换 | 熔炉、冶炼炉 |
| 6 | 特殊展示 | 回忆/成就UI |
Window Flow
窗口工作流程
Server: openWindow(window) -> OpenWindow packet (ID 200) -> Client: Render UI
Client: User Action -> SendWindowAction packet (ID 203) -> Server: handleAction()
Server: invalidate() -> updateWindows() -> UpdateWindow packet (ID 201) -> Client: Refresh UI
Server: closeWindow() -> CloseWindow packet (ID 202) -> Client: Close UI服务器: openWindow(window) -> 发送OpenWindow数据包(ID 200)-> 客户端: 渲染UI
客户端: 用户操作 -> 发送SendWindowAction数据包(ID 203)-> 服务器: handleAction()
服务器: invalidate() -> updateWindows() -> 发送UpdateWindow数据包(ID 201)-> 客户端: 刷新UI
服务器: closeWindow() -> 发送CloseWindow数据包(ID 202)-> 客户端: 关闭UIWindow Data Pattern
窗口数据模式
Windows use to return a that is serialized and sent to the client. This data controls client-side rendering:
getData()JsonObjectjava
@Override
public JsonObject getData() {
JsonObject data = new JsonObject();
data.addProperty("type", windowType.ordinal());
data.addProperty("title", "My Window");
data.addProperty("customProperty", someValue);
return data;
}窗口通过返回,该对象会被序列化并发送到客户端,用于控制客户端渲染:
getData()JsonObjectjava
@Override
public JsonObject getData() {
JsonObject data = new JsonObject();
data.addProperty("type", windowType.ordinal());
data.addProperty("title", "My Window");
data.addProperty("customProperty", someValue);
return data;
}Basic Window Implementation
基础窗口实现
Abstract Window Base
抽象窗口基类
All windows extend from and must implement these abstract methods:
Windowjava
package com.example.myplugin.windows;
import com.google.gson.JsonObject;
import com.hypixel.hytale.server.core.entity.entities.player.windows.Window;
import com.hypixel.hytale.protocol.packets.window.WindowType;
import com.hypixel.hytale.protocol.packets.window.WindowAction;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
public class CustomWindow extends Window {
private final JsonObject windowData = new JsonObject();
public CustomWindow() {
super(WindowType.Container);
// Initialize window data
windowData.addProperty("title", "Custom Window");
}
@Override
public JsonObject getData() {
// Return data to send to client (serialized as JSON)
return windowData;
}
@Override
protected boolean onOpen0() {
// Called when window opens
// Return false to cancel opening
return true;
}
@Override
protected void onClose0() {
// Called when window closes - cleanup here
}
@Override
public void handleAction(Ref<EntityStore> ref, Store<EntityStore> store, WindowAction action) {
// Handle window actions from client
// Default implementation is no-op
}
}所有窗口都继承自,并必须实现以下抽象方法:
Windowjava
package com.example.myplugin.windows;
import com.google.gson.JsonObject;
import com.hypixel.hytale.server.core.entity.entities.player.windows.Window;
import com.hypixel.hytale.protocol.packets.window.WindowType;
import com.hypixel.hytale.protocol.packets.window.WindowAction;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
public class CustomWindow extends Window {
private final JsonObject windowData = new JsonObject();
public CustomWindow() {
super(WindowType.Container);
// 初始化窗口数据
windowData.addProperty("title", "Custom Window");
}
@Override
public JsonObject getData() {
// 返回要发送到客户端的数据(序列化为JSON)
return windowData;
}
@Override
protected boolean onOpen0() {
// 窗口打开时调用
// 返回false可取消窗口打开
return true;
}
@Override
protected void onClose0() {
// 窗口关闭时调用 - 在此处执行清理操作
}
@Override
public void handleAction(Ref<EntityStore> ref, Store<EntityStore> store, WindowAction action) {
// 处理来自客户端的窗口操作
// 默认实现为空操作
}
}Opening Windows
打开窗口
Windows are opened through the :
WindowManagerjava
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.server.core.command.system.CommandContext;
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand;
import com.hypixel.hytale.server.core.entity.entities.Player;
import com.hypixel.hytale.server.core.entity.entities.player.windows.WindowManager;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import com.hypixel.hytale.protocol.packets.window.OpenWindow;
import javax.annotation.Nonnull;
public class StorageCommand extends AbstractPlayerCommand {
public StorageCommand() {
super("storage", "Open storage window");
}
@Override
protected void execute(
@Nonnull CommandContext context,
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref,
@Nonnull PlayerRef playerRef,
@Nonnull World world
) {
world.execute(() -> {
Player player = store.getComponent(ref, Player.getComponentType());
StorageWindow window = new StorageWindow();
// Open via WindowManager
WindowManager windowManager = player.getWindowManager();
OpenWindow packet = windowManager.openWindow(window);
if (packet != null) {
// Window opened successfully - packet is sent automatically
context.sendSuccess("Window opened!");
} else {
// Opening was cancelled (onOpen0() returned false)
context.sendError("Failed to open window");
}
});
}
}通过打开窗口:
WindowManagerjava
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.server.core.command.system.CommandContext;
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand;
import com.hypixel.hytale.server.core.entity.entities.Player;
import com.hypixel.hytale.server.core.entity.entities.player.windows.WindowManager;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import com.hypixel.hytale.protocol.packets.window.OpenWindow;
import javax.annotation.Nonnull;
public class StorageCommand extends AbstractPlayerCommand {
public StorageCommand() {
super("storage", "打开存储窗口");
}
@Override
protected void execute(
@Nonnull CommandContext context,
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref,
@Nonnull PlayerRef playerRef,
@Nonnull World world
) {
world.execute(() -> {
Player player = store.getComponent(ref, Player.getComponentType());
StorageWindow window = new StorageWindow();
// 通过WindowManager打开窗口
WindowManager windowManager = player.getWindowManager();
OpenWindow packet = windowManager.openWindow(window);
if (packet != null) {
// 窗口成功打开 - 数据包会自动发送
context.sendSuccess("窗口已打开!");
} else {
// 窗口打开被取消(onOpen0()返回false)
context.sendError("打开窗口失败");
}
});
}
}Updating Windows
更新窗口
Mark a window as needing update with :
invalidate()java
public void updateData(String newValue) {
windowData.addProperty("value", newValue);
invalidate(); // Mark for update
}
// For full rebuild (client re-renders entire window)
public void requireRebuild() {
setNeedRebuild();
invalidate();
}Updates are batched and sent via which checks flag.
WindowManager.updateWindows()isDirty调用标记窗口需要更新:
invalidate()java
public void updateData(String newValue) {
windowData.addProperty("value", newValue);
invalidate(); // 标记窗口需要更新
}
// 完全重建窗口(客户端重新渲染整个窗口)
public void requireRebuild() {
setNeedRebuild();
invalidate();
}更新操作会被批量处理,并通过发送,该方法会检查标记。
WindowManager.updateWindows()isDirtyWindow Manager
窗口管理器
The handles window lifecycle for each player:
WindowManagerjava
// Get player's window manager
WindowManager windowManager = player.getWindowManager();
// Open a window (returns OpenWindow packet or null if cancelled)
OpenWindow packet = windowManager.openWindow(new MyWindow());
// Open multiple windows atomically (all or none)
List<OpenWindow> packets = windowManager.openWindows(window1, window2);
// Get window by ID
Window window = windowManager.getWindow(windowId);
// Get all open windows
List<Window> windows = windowManager.getWindows();
// Update a specific window (sends UpdateWindow packet)
windowManager.updateWindow(window);
// Update all dirty windows
windowManager.updateWindows();
// Validate all ValidatedWindow instances (closes invalid ones)
windowManager.validateWindows();
// Close a specific window
windowManager.closeWindow(windowId);
// Close all windows
windowManager.closeAllWindows();
// Mark a window as changed
windowManager.markWindowChanged(windowId);WindowManagerjava
// 获取玩家的窗口管理器
WindowManager windowManager = player.getWindowManager();
// 打开窗口(返回OpenWindow数据包,若被取消则返回null)
OpenWindow packet = windowManager.openWindow(new MyWindow());
// 原子化打开多个窗口(要么全部打开,要么都不打开)
List<OpenWindow> packets = windowManager.openWindows(window1, window2);
// 通过ID获取窗口
Window window = windowManager.getWindow(windowId);
// 获取所有已打开的窗口
List<Window> windows = windowManager.getWindows();
// 更新指定窗口(发送UpdateWindow数据包)
windowManager.updateWindow(window);
// 更新所有需要更新的窗口
windowManager.updateWindows();
// 验证所有ValidatedWindow实例(关闭无效窗口)
windowManager.validateWindows();
// 关闭指定窗口
windowManager.closeWindow(windowId);
// 关闭所有窗口
windowManager.closeAllWindows();
// 标记窗口已更改
windowManager.markWindowChanged(windowId);Window IDs
窗口ID
- ID is reserved for client-requested windows
0 - ID is invalid
-1 - Server-assigned IDs start at 1 and increment
- ID 预留给客户端请求的窗口
0 - ID 为无效ID
-1 - 服务器分配的ID从1开始递增
Block Windows
方块绑定窗口
Windows tied to blocks in the world (chests, crafting tables). Extends which implements :
BlockWindowValidatedWindowjava
public class CustomChestWindow extends BlockWindow implements ItemContainerWindow {
private final SimpleItemContainer itemContainer;
private final JsonObject windowData = new JsonObject();
public CustomChestWindow(int x, int y, int z, int rotationIndex, BlockType blockType) {
super(WindowType.Container, x, y, z, rotationIndex, blockType);
this.itemContainer = new SimpleItemContainer(27); // 3 rows
// Set max interaction distance (default: 7.0)
setMaxDistance(7.0);
// Initialize window data
Item item = blockType.getItem();
windowData.addProperty("blockItemId", item != null ? item.getId() : "");
}
@Override
public JsonObject getData() {
return windowData;
}
@Override
public ItemContainer getItemContainer() {
return itemContainer;
}
@Override
protected boolean onOpen0() {
// Load chest contents from block entity
PlayerRef playerRef = getPlayerRef();
Ref<EntityStore> ref = playerRef.getReference();
Store<EntityStore> store = ref.getStore();
World world = store.getExternalData().getWorld();
// Load items from persistent storage
loadItemsFromWorld(world);
return true;
}
@Override
protected void onClose0() {
// Save chest contents
saveItemsToWorld();
}
}与世界中方块绑定的窗口(如箱子、工作台),继承自,该类实现了:
BlockWindowValidatedWindowjava
public class CustomChestWindow extends BlockWindow implements ItemContainerWindow {
private final SimpleItemContainer itemContainer;
private final JsonObject windowData = new JsonObject();
public CustomChestWindow(int x, int y, int z, int rotationIndex, BlockType blockType) {
super(WindowType.Container, x, y, z, rotationIndex, blockType);
this.itemContainer = new SimpleItemContainer(27); // 3行槽位
// 设置最大交互距离(默认:7.0)
setMaxDistance(7.0);
// 初始化窗口数据
Item item = blockType.getItem();
windowData.addProperty("blockItemId", item != null ? item.getId() : "");
}
@Override
public JsonObject getData() {
return windowData;
}
@Override
public ItemContainer getItemContainer() {
return itemContainer;
}
@Override
protected boolean onOpen0() {
// 从方块实体加载箱子内容
PlayerRef playerRef = getPlayerRef();
Ref<EntityStore> ref = playerRef.getReference();
Store<EntityStore> store = ref.getStore();
World world = store.getExternalData().getWorld();
// 从持久化存储加载物品
loadItemsFromWorld(world);
return true;
}
@Override
protected void onClose0() {
// 保存箱子内容
saveItemsToWorld();
}
}Block Validation
方块验证
BlockWindow- Player is within of the block (default 7.0 blocks)
maxDistance - The block still exists in the world
- The block type matches (via item comparison)
When validation fails, the window is automatically closed.
BlockWindow- 玩家与方块的距离在范围内(默认7.0格)
maxDistance - 方块仍存在于世界中
- 方块类型匹配(通过物品比较)
当验证失败时,窗口会自动关闭。
Block Interaction Handler
方块交互处理器
java
@EventHandler
public void onBlockInteract(BlockInteractEvent event) {
Player player = event.getPlayer();
BlockPos pos = event.getBlockPos();
Block block = event.getBlock();
if (block.getType().getId().equals("my_mod:custom_chest")) {
CustomChestWindow window = new CustomChestWindow(
pos.x(), pos.y(), pos.z(),
block.getRotationIndex(),
block.getType()
);
player.getWindowManager().openWindow(window);
event.setCancelled(true);
}
}java
@EventHandler
public void onBlockInteract(BlockInteractEvent event) {
Player player = event.getPlayer();
BlockPos pos = event.getBlockPos();
Block block = event.getBlock();
if (block.getType().getId().equals("my_mod:custom_chest")) {
CustomChestWindow window = new CustomChestWindow(
pos.x(), pos.y(), pos.z(),
block.getRotationIndex(),
block.getType()
);
player.getWindowManager().openWindow(window);
event.setCancelled(true);
}
}Crafting Windows
合成窗口
BenchWindow Base
合成台基类
All crafting bench windows extend :
BenchWindowjava
public abstract class BenchWindow extends BlockWindow implements MaterialContainerWindow {
protected final Bench bench;
protected final BenchState benchState;
protected final JsonObject windowData = new JsonObject();
private MaterialExtraResourcesSection extraResourcesSection;
// Window data includes:
// - type: bench type ordinal
// - id: bench ID string
// - name: translation key
// - blockItemId: item ID
// - tierLevel: current tier level
// - worldMemoriesLevel: world memories level
// - progress: crafting progress (0.0 - 1.0)
// - tierUpgradeProgress: tier upgrade progress
}所有合成台窗口都继承自:
BenchWindowjava
public abstract class BenchWindow extends BlockWindow implements MaterialContainerWindow {
protected final Bench bench;
protected final BenchState benchState;
protected final JsonObject windowData = new JsonObject();
private MaterialExtraResourcesSection extraResourcesSection;
// 窗口数据包含:
// - type: 合成台类型序号
// - id: 合成台ID字符串
// - name: 翻译键
// - blockItemId: 物品ID
// - tierLevel: 当前等级
// - worldMemoriesLevel: 世界回忆等级
// - progress: 合成进度(0.0 - 1.0)
// - tierUpgradeProgress: 等级升级进度
}SimpleCraftingWindow (Basic Workbench)
基础合成窗口(SimpleCraftingWindow)
java
public class WorkbenchWindow extends SimpleCraftingWindow {
public WorkbenchWindow(BenchState benchState) {
super(benchState);
}
@Override
public void handleAction(Ref<EntityStore> ref, Store<EntityStore> store, WindowAction action) {
if (action instanceof CraftRecipeAction craftAction) {
String recipeId = craftAction.recipeId;
int quantity = craftAction.quantity;
// Handle crafting
CraftingManager craftingManager = store.getComponent(ref, CraftingManager.getComponentType());
craftSimpleItem(store, ref, craftingManager, craftAction);
} else if (action instanceof TierUpgradeAction) {
// Handle bench tier upgrade
handleTierUpgrade(ref, store);
}
}
}java
public class WorkbenchWindow extends SimpleCraftingWindow {
public WorkbenchWindow(BenchState benchState) {
super(benchState);
}
@Override
public void handleAction(Ref<EntityStore> ref, Store<EntityStore> store, WindowAction action) {
if (action instanceof CraftRecipeAction craftAction) {
String recipeId = craftAction.recipeId;
int quantity = craftAction.quantity;
// 处理合成操作
CraftingManager craftingManager = store.getComponent(ref, CraftingManager.getComponentType());
craftSimpleItem(store, ref, craftingManager, craftAction);
} else if (action instanceof TierUpgradeAction) {
// 处理合成台等级升级
handleTierUpgrade(ref, store);
}
}
}ProcessingBenchWindow (Furnace-like)
加工台窗口(ProcessingBenchWindow,类似熔炉)
java
public class SmelterWindow extends ProcessingBenchWindow {
public SmelterWindow(BenchState benchState) {
super(benchState);
}
// ProcessingBenchWindow provides:
// - setActive(boolean): toggle processing
// - setProgress(float): update progress (0.0 - 1.0)
// - setFuelTime(float): current fuel remaining
// - setMaxFuel(int): maximum fuel capacity
// - setProcessingSlots(Set<Short>): slots currently processing
// - setProcessingFuelSlots(Set<Short>): fuel slots in use
@Override
public void handleAction(Ref<EntityStore> ref, Store<EntityStore> store, WindowAction action) {
if (action instanceof SetActiveAction activeAction) {
setActive(activeAction.state);
invalidate();
} else if (action instanceof TierUpgradeAction) {
handleTierUpgrade(ref, store);
}
}
}java
public class SmelterWindow extends ProcessingBenchWindow {
public SmelterWindow(BenchState benchState) {
super(benchState);
}
// ProcessingBenchWindow提供以下方法:
// - setActive(boolean): 切换加工状态
// - setProgress(float): 更新进度(0.0 - 1.0)
// - setFuelTime(float): 当前剩余燃料时间
// - setMaxFuel(int): 最大燃料容量
// - setProcessingSlots(Set<Short>): 当前正在加工的槽位
// - setProcessingFuelSlots(Set<Short>): 正在使用的燃料槽位
@Override
public void handleAction(Ref<EntityStore> ref, Store<EntityStore> store, WindowAction action) {
if (action instanceof SetActiveAction activeAction) {
setActive(activeAction.state);
invalidate();
} else if (action instanceof TierUpgradeAction) {
handleTierUpgrade(ref, store);
}
}
}Updating Crafting Progress
更新合成进度
java
// Update progress with throttling (min 5% change or 500ms interval)
public void updateCraftingJob(float percent) {
windowData.addProperty("progress", percent);
checkProgressInvalidate(percent);
}
public void updateBenchUpgradeJob(float percent) {
windowData.addProperty("tierUpgradeProgress", percent);
checkProgressInvalidate(percent);
}
// On tier level change (requires full rebuild)
public void updateBenchTierLevel(int newValue) {
windowData.addProperty("tierLevel", newValue);
updateBenchUpgradeJob(0.0f);
setNeedRebuild();
invalidate();
}java
// 带节流的进度更新(最小5%变化或500ms间隔)
public void updateCraftingJob(float percent) {
windowData.addProperty("progress", percent);
checkProgressInvalidate(percent);
}
public void updateBenchUpgradeJob(float percent) {
windowData.addProperty("tierUpgradeProgress", percent);
checkProgressInvalidate(percent);
}
// 等级变化时(需要完全重建窗口)
public void updateBenchTierLevel(int newValue) {
windowData.addProperty("tierLevel", newValue);
updateBenchUpgradeJob(0.0f);
setNeedRebuild();
invalidate();
}Item Container Windows
物品容器窗口
Windows with inventory slots implement :
ItemContainerWindowjava
public interface ItemContainerWindow {
@Nonnull ItemContainer getItemContainer();
}包含物品背包槽位的窗口需实现接口:
ItemContainerWindowjava
public interface ItemContainerWindow {
@Nonnull ItemContainer getItemContainer();
}ItemContainer Integration
物品容器集成
java
public class InventoryWindow extends Window implements ItemContainerWindow {
private final SimpleItemContainer itemContainer;
private final JsonObject windowData = new JsonObject();
public InventoryWindow(int size) {
super(WindowType.Container);
this.itemContainer = new SimpleItemContainer(size);
// Register change listener for automatic updates
itemContainer.registerChangeEvent(EventPriority.NORMAL, event -> {
invalidate();
});
}
@Override
public ItemContainer getItemContainer() {
return itemContainer;
}
@Override
public JsonObject getData() {
return windowData;
}
@Override
protected boolean onOpen0() {
return true;
}
@Override
protected void onClose0() {
// Cleanup
}
}Note: When a window implements , the automatically:
ItemContainerWindowWindowManager- Registers a change listener to mark the window dirty when inventory changes
- Includes in
InventorySectionandOpenWindowpacketsUpdateWindow - Unregisters the listener when the window closes
java
public class InventoryWindow extends Window implements ItemContainerWindow {
private final SimpleItemContainer itemContainer;
private final JsonObject windowData = new JsonObject();
public InventoryWindow(int size) {
super(WindowType.Container);
this.itemContainer = new SimpleItemContainer(size);
// 注册变更监听器,实现自动更新
itemContainer.registerChangeEvent(EventPriority.NORMAL, event -> {
invalidate();
});
}
@Override
public ItemContainer getItemContainer() {
return itemContainer;
}
@Override
public JsonObject getData() {
return windowData;
}
@Override
protected boolean onOpen0() {
return true;
}
@Override
protected void onClose0() {
// 清理操作
}
}注意: 当窗口实现时,会自动:
ItemContainerWindowWindowManager- 注册变更监听器,当背包内容变化时标记窗口为脏状态
- 在和
OpenWindow数据包中包含UpdateWindowInventorySection - 窗口关闭时注销监听器
Window Actions
窗口操作处理
Handle user interactions with :
handleAction()java
@Override
public void handleAction(Ref<EntityStore> ref, Store<EntityStore> store, WindowAction action) {
if (action instanceof CraftRecipeAction craft) {
handleCraft(craft.recipeId, craft.quantity);
} else if (action instanceof SelectSlotAction select) {
handleSlotSelect(select.slot);
} else if (action instanceof SetActiveAction active) {
handleActiveToggle(active.state);
} else if (action instanceof SortItemsAction sort) {
handleSort(sort.sortType);
}
}通过处理用户交互:
handleAction()java
@Override
public void handleAction(Ref<EntityStore> ref, Store<EntityStore> store, WindowAction action) {
if (action instanceof CraftRecipeAction craft) {
handleCraft(craft.recipeId, craft.quantity);
} else if (action instanceof SelectSlotAction select) {
handleSlotSelect(select.slot);
} else if (action instanceof SetActiveAction active) {
handleActiveToggle(active.state);
} else if (action instanceof SortItemsAction sort) {
handleSort(sort.sortType);
}
}WindowAction Types
窗口操作类型
| Type ID | Class | Fields | Description |
|---|---|---|---|
| 0 | | | Craft a recipe |
| 1 | | (none) | Upgrade bench tier |
| 2 | | | Select a slot |
| 3 | | | Cycle block type direction |
| 4 | | | Toggle processing on/off |
| 5 | | (none) | Confirm diagram crafting |
| 6 | | | Change recipe category |
| 7 | | (none) | Cancel current crafting |
| 8 | | | Sort inventory items |
| 类型ID | 类 | 字段 | 描述 |
|---|---|---|---|
| 0 | | | 合成指定配方 |
| 1 | | 无 | 升级合成台等级 |
| 2 | | | 选择槽位 |
| 3 | | | 循环切换方块类型方向 |
| 4 | | | 切换加工状态 |
| 5 | | 无 | 确认蓝图合成 |
| 6 | | | 切换配方分类 |
| 7 | | 无 | 取消当前合成 |
| 8 | | | 排序背包物品 |
SortType Enum
排序类型枚举
java
public enum SortType {
Name(0), // Sort by item translation key
Type(1), // Sort by item type (Weapon, Armor, Tool, Item, Special)
Rarity(2); // Sort by quality value (reversed)
}java
public enum SortType {
Name(0), // 按物品翻译键排序
Type(1), // 按物品类型排序(武器、护甲、工具、物品、特殊)
Rarity(2); // 按品质值排序(倒序)
}Window Packets
窗口数据包
Network communication for windows:
窗口的网络通信使用以下数据包:
Server to Client
服务器到客户端
| Packet | ID | Fields | Purpose |
|---|---|---|---|
| 200 | | Open window on client |
| 201 | | Update window contents |
| 202 | | Close window on client |
| 数据包 | ID | 字段 | 用途 |
|---|---|---|---|
| 200 | | 在客户端打开窗口 |
| 201 | | 更新窗口内容 |
| 202 | | 在客户端关闭窗口 |
Client to Server
客户端到服务器
| Packet | ID | Fields | Purpose |
|---|---|---|---|
| 203 | | User interaction |
| 204 | | Request client-initiated window |
| 数据包 | ID | 字段 | 用途 |
|---|---|---|---|
| 203 | | 发送用户交互操作 |
| 204 | | 请求打开客户端触发的窗口 |
Packet Structure
数据包结构
The packet includes:
OpenWindow- : JSON string with window-specific data
windowData - :
inventory(nullable) - only forInventorySectionItemContainerWindow - :
extraResources(nullable) - only forExtraResourcesMaterialContainerWindow
java
// Creating OpenWindow packet (done automatically by WindowManager)
OpenWindow packet = new OpenWindow(
windowId,
window.getType(),
window.getData().toString(), // JSON string
itemContainerWindow != null ? itemContainerWindow.getItemContainer().toPacket() : null,
materialContainerWindow != null ? materialContainerWindow.getExtraResourcesSection().toPacket() : null
);OpenWindow- : JSON字符串,包含窗口特定数据
windowData - :
inventory(可选)- 仅InventorySection包含ItemContainerWindow - :
extraResources(可选)- 仅ExtraResources包含MaterialContainerWindow
java
// 创建OpenWindow数据包(由WindowManager自动完成)
OpenWindow packet = new OpenWindow(
windowId,
window.getType(),
window.getData().toString(), // JSON字符串
itemContainerWindow != null ? itemContainerWindow.getItemContainer().toPacket() : null,
materialContainerWindow != null ? materialContainerWindow.getExtraResourcesSection().toPacket() : null
);Client-Requestable Windows
客户端可请求的窗口
Some windows can be opened by client request (e.g., pressing a key). Register these in :
Window.CLIENT_REQUESTABLE_WINDOW_TYPESjava
public class MyPlugin extends JavaPlugin {
@Override
protected void setup() {
// Register client-requestable window
Window.CLIENT_REQUESTABLE_WINDOW_TYPES.put(
WindowType.Memories,
MemoriesWindow::new
);
}
}When client sends packet, the server:
ClientOpenWindow- Looks up the in
WindowTypeCLIENT_REQUESTABLE_WINDOW_TYPES - Creates a new window instance using the supplier
- Opens it with ID 0 via
windowManager.clientOpenWindow(window)
java
// Handle client-requested window
@PacketHandler
public void onClientOpenWindow(ClientOpenWindow packet) {
Supplier<? extends Window> supplier = Window.CLIENT_REQUESTABLE_WINDOW_TYPES.get(packet.type);
if (supplier != null) {
Window window = supplier.get();
UpdateWindow updatePacket = windowManager.clientOpenWindow(window);
if (updatePacket != null) {
player.sendPacket(updatePacket);
}
}
}部分窗口可由客户端请求打开(如按下按键),需在中注册:
Window.CLIENT_REQUESTABLE_WINDOW_TYPESjava
public class MyPlugin extends JavaPlugin {
@Override
protected void setup() {
// 注册客户端可请求的窗口
Window.CLIENT_REQUESTABLE_WINDOW_TYPES.put(
WindowType.Memories,
MemoriesWindow::new
);
}
}当客户端发送数据包时,服务器会:
ClientOpenWindow- 在中查找
CLIENT_REQUESTABLE_WINDOW_TYPESWindowType - 使用提供的供应商创建新窗口实例
- 通过以ID 0打开窗口
windowManager.clientOpenWindow(window)
java
// 处理客户端请求的窗口
@PacketHandler
public void onClientOpenWindow(ClientOpenWindow packet) {
Supplier<? extends Window> supplier = Window.CLIENT_REQUESTABLE_WINDOW_TYPES.get(packet.type);
if (supplier != null) {
Window window = supplier.get();
UpdateWindow updatePacket = windowManager.clientOpenWindow(window);
if (updatePacket != null) {
player.sendPacket(updatePacket);
}
}
}Custom Window Rendering
自定义窗口渲染
Define window appearance through :
getData()java
public class CustomMenuWindow extends Window {
private final JsonObject windowData = new JsonObject();
public CustomMenuWindow() {
super(WindowType.Container);
setupLayout();
}
@Override
public JsonObject getData() {
return windowData;
}
private void setupLayout() {
windowData.addProperty("title", "Main Menu");
windowData.addProperty("rows", 6);
// Add custom properties for client rendering
JsonArray menuItems = new JsonArray();
menuItems.add(createMenuItem("pvp", "PvP Arena", "diamond_sword", 20));
menuItems.add(createMenuItem("survival", "Survival", "grass_block", 22));
menuItems.add(createMenuItem("lobby", "Lobby", "ender_pearl", 24));
windowData.add("menuItems", menuItems);
}
private JsonObject createMenuItem(String id, String name, String icon, int slot) {
JsonObject item = new JsonObject();
item.addProperty("id", id);
item.addProperty("name", name);
item.addProperty("icon", icon);
item.addProperty("slot", slot);
return item;
}
@Override
public void handleAction(Ref<EntityStore> ref, Store<EntityStore> store, WindowAction action) {
if (action instanceof SelectSlotAction select) {
switch (select.slot) {
case 20 -> joinPvP(ref, store);
case 22 -> joinSurvival(ref, store);
case 24 -> teleportToLobby(ref, store);
}
}
}
@Override
protected boolean onOpen0() { return true; }
@Override
protected void onClose0() { }
}通过定义窗口外观:
getData()java
public class CustomMenuWindow extends Window {
private final JsonObject windowData = new JsonObject();
public CustomMenuWindow() {
super(WindowType.Container);
setupLayout();
}
@Override
public JsonObject getData() {
return windowData;
}
private void setupLayout() {
windowData.addProperty("title", "主菜单");
windowData.addProperty("rows", 6);
// 添加自定义属性供客户端渲染
JsonArray menuItems = new JsonArray();
menuItems.add(createMenuItem("pvp", "PvP竞技场", "diamond_sword", 20));
menuItems.add(createMenuItem("survival", "生存模式", "grass_block", 22));
menuItems.add(createMenuItem("lobby", "大厅", "ender_pearl", 24));
windowData.add("menuItems", menuItems);
}
private JsonObject createMenuItem(String id, String name, String icon, int slot) {
JsonObject item = new JsonObject();
item.addProperty("id", id);
item.addProperty("name", name);
item.addProperty("icon", icon);
item.addProperty("slot", slot);
return item;
}
@Override
public void handleAction(Ref<EntityStore> ref, Store<EntityStore> store, WindowAction action) {
if (action instanceof SelectSlotAction select) {
switch (select.slot) {
case 20 -> joinPvP(ref, store);
case 22 -> joinSurvival(ref, store);
case 24 -> teleportToLobby(ref, store);
}
}
}
@Override
protected boolean onOpen0() { return true; }
@Override
protected void onClose0() { }
}Material Container Windows
材料容器窗口
Windows with extra resource materials implement :
MaterialContainerWindowjava
public interface MaterialContainerWindow {
@Nonnull MaterialExtraResourcesSection getExtraResourcesSection();
void invalidateExtraResources();
boolean isValid();
}包含额外资源材料的窗口需实现接口:
MaterialContainerWindowjava
public interface MaterialContainerWindow {
@Nonnull MaterialExtraResourcesSection getExtraResourcesSection();
void invalidateExtraResources();
boolean isValid();
}MaterialExtraResourcesSection
额外资源区域类
java
public class MaterialExtraResourcesSection {
private boolean valid;
private ItemContainer itemContainer;
private ItemQuantity[] extraMaterials;
// Methods
public void setExtraMaterials(ItemQuantity[] materials);
public ExtraResources toPacket();
public boolean isValid();
public void setValid(boolean valid);
}Usage in crafting windows:
java
@Override
public MaterialExtraResourcesSection getExtraResourcesSection() {
if (!extraResourcesSection.isValid()) {
// Recompute extra materials from bench state
CraftingManager.feedExtraResourcesSection(benchState, extraResourcesSection);
}
return extraResourcesSection;
}
@Override
public void invalidateExtraResources() {
extraResourcesSection.setValid(false);
invalidate();
}java
public class MaterialExtraResourcesSection {
private boolean valid;
private ItemContainer itemContainer;
private ItemQuantity[] extraMaterials;
// 方法
public void setExtraMaterials(ItemQuantity[] materials);
public ExtraResources toPacket();
public boolean isValid();
public void setValid(boolean valid);
}在合成窗口中的使用:
java
@Override
public MaterialExtraResourcesSection getExtraResourcesSection() {
if (!extraResourcesSection.isValid()) {
// 从合成台状态重新计算额外材料
CraftingManager.feedExtraResourcesSection(benchState, extraResourcesSection);
}
return extraResourcesSection;
}
@Override
public void invalidateExtraResources() {
extraResourcesSection.setValid(false);
invalidate();
}Close Event Registration
关闭事件注册
Register handlers for when a window closes:
java
public class MyWindow extends Window {
@Override
protected boolean onOpen0() {
// Register close event handler
registerCloseEvent(event -> {
// Called when window closes
saveData();
cleanupResources();
});
// With priority
registerCloseEvent(EventPriority.FIRST, event -> {
// Called first
});
return true;
}
}注册窗口关闭时的处理程序:
java
public class MyWindow extends Window {
@Override
protected boolean onOpen0() {
// 注册关闭事件处理程序
registerCloseEvent(event -> {
// 窗口关闭时调用
saveData();
cleanupResources();
});
// 带优先级的注册
registerCloseEvent(EventPriority.FIRST, event -> {
// 最先被调用
});
return true;
}
}Complete Example: Container Block Window
完整示例:方块容器窗口
java
package com.example.storage;
import com.google.gson.JsonObject;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.protocol.packets.window.WindowAction;
import com.hypixel.hytale.protocol.packets.window.WindowType;
import com.hypixel.hytale.protocol.packets.window.SortItemsAction;
import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType;
import com.hypixel.hytale.server.core.entity.entities.player.windows.BlockWindow;
import com.hypixel.hytale.server.core.entity.entities.player.windows.ItemContainerWindow;
import com.hypixel.hytale.server.core.inventory.container.ItemContainer;
import com.hypixel.hytale.server.core.inventory.container.SimpleItemContainer;
import com.hypixel.hytale.server.core.inventory.container.SortType;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
public class StorageBlockWindow extends BlockWindow implements ItemContainerWindow {
private final SimpleItemContainer itemContainer;
private final JsonObject windowData = new JsonObject();
public StorageBlockWindow(int x, int y, int z, int rotationIndex, BlockType blockType, int rows) {
super(WindowType.Container, x, y, z, rotationIndex, blockType);
this.itemContainer = new SimpleItemContainer(rows * 9);
// Initialize window data
windowData.addProperty("title", "Storage");
windowData.addProperty("rows", rows);
windowData.addProperty("blockItemId", blockType.getItem().getId());
}
@Override
public JsonObject getData() {
return windowData;
}
@Override
public ItemContainer getItemContainer() {
return itemContainer;
}
@Override
protected boolean onOpen0() {
// Load items from persistent storage
loadFromStorage();
return true;
}
@Override
protected void onClose0() {
// Save items to persistent storage
saveToStorage();
}
@Override
public void handleAction(Ref<EntityStore> ref, Store<EntityStore> store, WindowAction action) {
if (action instanceof SortItemsAction sort) {
SortType serverSortType = SortType.fromPacket(sort.sortType);
itemContainer.sort(serverSortType);
invalidate();
}
}
private void loadFromStorage() {
// Load from block entity or database
}
private void saveToStorage() {
// Save to block entity or database
}
}java
package com.example.storage;
import com.google.gson.JsonObject;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.protocol.packets.window.WindowAction;
import com.hypixel.hytale.protocol.packets.window.WindowType;
import com.hypixel.hytale.protocol.packets.window.SortItemsAction;
import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType;
import com.hypixel.hytale.server.core.entity.entities.player.windows.BlockWindow;
import com.hypixel.hytale.server.core.entity.entities.player.windows.ItemContainerWindow;
import com.hypixel.hytale.server.core.inventory.container.ItemContainer;
import com.hypixel.hytale.server.core.inventory.container.SimpleItemContainer;
import com.hypixel.hytale.server.core.inventory.container.SortType;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
public class StorageBlockWindow extends BlockWindow implements ItemContainerWindow {
private final SimpleItemContainer itemContainer;
private final JsonObject windowData = new JsonObject();
public StorageBlockWindow(int x, int y, int z, int rotationIndex, BlockType blockType, int rows) {
super(WindowType.Container, x, y, z, rotationIndex, blockType);
this.itemContainer = new SimpleItemContainer(rows * 9);
// 初始化窗口数据
windowData.addProperty("title", "存储箱");
windowData.addProperty("rows", rows);
windowData.addProperty("blockItemId", blockType.getItem().getId());
}
@Override
public JsonObject getData() {
return windowData;
}
@Override
public ItemContainer getItemContainer() {
return itemContainer;
}
@Override
protected boolean onOpen0() {
// 从持久化存储加载物品
loadFromStorage();
return true;
}
@Override
protected void onClose0() {
// 将物品保存到持久化存储
saveToStorage();
}
@Override
public void handleAction(Ref<EntityStore> ref, Store<EntityStore> store, WindowAction action) {
if (action instanceof SortItemsAction sort) {
SortType serverSortType = SortType.fromPacket(sort.sortType);
itemContainer.sort(serverSortType);
invalidate();
}
}
private void loadFromStorage() {
// 从方块实体或数据库加载
}
private void saveToStorage() {
// 保存到方块实体或数据库
}
}Usage
使用示例
java
@EventHandler
public void onBlockInteract(BlockInteractEvent event) {
Block block = event.getBlock();
if (block.getType().getId().equals("my_mod:storage_block")) {
StorageBlockWindow window = new StorageBlockWindow(
event.getX(), event.getY(), event.getZ(),
block.getRotationIndex(),
block.getType(),
3 // 3 rows
);
Player player = event.getPlayer();
OpenWindow packet = player.getWindowManager().openWindow(window);
if (packet != null) {
event.setCancelled(true);
}
}
}java
@EventHandler
public void onBlockInteract(BlockInteractEvent event) {
Block block = event.getBlock();
if (block.getType().getId().equals("my_mod:storage_block")) {
StorageBlockWindow window = new StorageBlockWindow(
event.getX(), event.getY(), event.getZ(),
block.getRotationIndex(),
block.getType(),
3 // 3行槽位
);
Player player = event.getPlayer();
OpenWindow packet = player.getWindowManager().openWindow(window);
if (packet != null) {
event.setCancelled(true);
}
}
}Creating .ui Files for Windows
为窗口创建.ui文件
Basic Page Template
基础页面模板
Create a new page UI file in :
resources/Common/UI/Custom/// MyCustomPage.ui
$C = "../Common.ui";
$C.@PageOverlay {}
$C.@Container {
Anchor: (Width: 500, Height: 400);
#Title {
$C.@Title {
@Text = %server.customUI.myPage.title;
}
}
#Content {
LayoutMode: Top;
Padding: (Full: 16);
// Page content here
Label #InfoLabel {
Style: $C.@DefaultLabelStyle;
Text: "";
}
Group {
Anchor: (Height: 16); // Spacer
}
$C.@TextButton #ConfirmButton {
@Text = %server.customUI.general.confirm;
}
}
}
$C.@BackButton {}在目录下创建新的页面UI文件:
resources/Common/UI/Custom/// MyCustomPage.ui
$C = "../Common.ui";
$C.@PageOverlay {}
$C.@Container {
Anchor: (Width: 500, Height: 400);
#Title {
$C.@Title {
@Text = %server.customUI.myPage.title;
}
}
#Content {
LayoutMode: Top;
Padding: (Full: 16);
// 页面内容
Label #InfoLabel {
Style: $C.@DefaultLabelStyle;
Text: "";
}
Group {
Anchor: (Height: 16); // 间隔符
}
$C.@TextButton #ConfirmButton {
@Text = %server.customUI.general.confirm;
}
}
}
$C.@BackButton {}Container with Header and Scrollable Content
带头部和滚动内容的容器
$C = "../Common.ui";
$C.@PageOverlay {}
$C.@Container {
Anchor: (Width: 800, Height: 600);
#Title {
Group {
$C.@Title {
@Text = %server.customUI.listPage.title;
}
$C.@HeaderSearch {} // Search input on right
}
}
#Content {
LayoutMode: Left; // Side-by-side panels
// Left panel - list
Group #ListView {
Anchor: (Width: 250);
LayoutMode: TopScrolling;
ScrollbarStyle: $C.@DefaultScrollbarStyle;
}
// Right panel - details
Group #DetailView {
FlexWeight: 1;
LayoutMode: Top;
Padding: (Left: 10);
Label #ItemName {
Style: (FontSize: 20, RenderBold: true);
Anchor: (Bottom: 10);
}
Label #ItemDescription {
Style: (FontSize: 14, TextColor: #96a9be, Wrap: true);
}
}
}
}
$C.@BackButton {}$C = "../Common.ui";
$C.@PageOverlay {}
$C.@Container {
Anchor: (Width: 800, Height: 600);
#Title {
Group {
$C.@Title {
@Text = %server.customUI.listPage.title;
}
$C.@HeaderSearch {} // 右侧搜索输入框
}
}
#Content {
LayoutMode: Left; // 并排面板
// 左侧面板 - 列表
Group #ListView {
Anchor: (Width: 250);
LayoutMode: TopScrolling;
ScrollbarStyle: $C.@DefaultScrollbarStyle;
}
// 右侧面板 - 详情
Group #DetailView {
FlexWeight: 1;
LayoutMode: Top;
Padding: (Left: 10);
Label #ItemName {
Style: (FontSize: 20, RenderBold: true);
Anchor: (Bottom: 10);
}
Label #ItemDescription {
Style: (FontSize: 14, TextColor: #96a9be, Wrap: true);
}
}
}
}
$C.@BackButton {}Reusable List Item Component
可复用列表项组件
Create in :
resources/Common/UI/Custom/MyListItem.ui$C = "../Common.ui";
$Sounds = "../Sounds.ui";
TextButton {
Anchor: (Bottom: 4, Height: 36);
Padding: (Horizontal: 12);
Style: (
Sounds: $Sounds.@ButtonsLight,
Default: (
LabelStyle: (FontSize: 14, VerticalAlignment: Center),
Background: (Color: #00000000)
),
Hovered: (
LabelStyle: (FontSize: 14, VerticalAlignment: Center),
Background: #ffffff(0.1)
),
Pressed: (
LabelStyle: (FontSize: 14, VerticalAlignment: Center),
Background: #ffffff(0.15)
)
);
Text: ""; // Set dynamically
}在中创建:
resources/Common/UI/Custom/MyListItem.ui$C = "../Common.ui";
$Sounds = "../Sounds.ui";
TextButton {
Anchor: (Bottom: 4, Height: 36);
Padding: (Horizontal: 12);
Style: (
Sounds: $Sounds.@ButtonsLight,
Default: (
LabelStyle: (FontSize: 14, VerticalAlignment: Center),
Background: (Color: #00000000)
),
Hovered: (
LabelStyle: (FontSize: 14, VerticalAlignment: Center),
Background: #ffffff(0.1)
),
Pressed: (
LabelStyle: (FontSize: 14, VerticalAlignment: Center),
Background: #ffffff(0.15)
)
);
Text: ""; // 动态设置
}Grid Layout with Cards
卡片网格布局
$C = "../Common.ui";
$C.@PageOverlay {}
$C.@DecoratedContainer {
Anchor: (Width: 900, Height: 650);
#Title {
Label {
Style: $C.@TitleStyle;
Text: %server.customUI.gridPage.title;
}
}
#Content {
LayoutMode: Top;
// Scrollable grid container
Group #GridContainer {
FlexWeight: 1;
LayoutMode: TopScrolling;
ScrollbarStyle: $C.@DefaultScrollbarStyle;
Padding: (Full: 8);
// Cards wrap automatically
Group #CardGrid {
LayoutMode: LeftCenterWrap;
}
}
// Footer with actions
Group #Footer {
Anchor: (Height: 50);
LayoutMode: Left;
Padding: (Top: 10);
Group { FlexWeight: 1; } // Spacer
$C.@SecondaryTextButton #CancelBtn {
@Anchor = (Width: 120, Right: 10);
@Text = %client.general.button.cancel;
}
$C.@TextButton #ConfirmBtn {
@Anchor = (Width: 120);
@Text = %client.general.button.confirm;
}
}
}
}$C = "../Common.ui";
$C.@PageOverlay {}
$C.@DecoratedContainer {
Anchor: (Width: 900, Height: 650);
#Title {
Label {
Style: $C.@TitleStyle;
Text: %server.customUI.gridPage.title;
}
}
#Content {
LayoutMode: Top;
// 可滚动网格容器
Group #GridContainer {
FlexWeight: 1;
LayoutMode: TopScrolling;
ScrollbarStyle: $C.@DefaultScrollbarStyle;
Padding: (Full: 8);
// 卡片自动换行
Group #CardGrid {
LayoutMode: LeftCenterWrap;
}
}
// 底部操作栏
Group #Footer {
Anchor: (Height: 50);
LayoutMode: Left;
Padding: (Top: 10);
Group { FlexWeight: 1; } // 间隔符
$C.@SecondaryTextButton #CancelBtn {
@Anchor = (Width: 120, Right: 10);
@Text = %client.general.button.cancel;
}
$C.@TextButton #ConfirmBtn {
@Anchor = (Width: 120);
@Text = %client.general.button.confirm;
}
}
}
}Card Component
卡片组件
$C = "../Common.ui";
$Sounds = "../Sounds.ui";
Button {
Anchor: (Width: 140, Height: 160, Right: 8, Bottom: 8);
Style: (
Sounds: $Sounds.@ButtonsLight,
Default: (Background: (TexturePath: "CardBackground.png", Border: 8)),
Hovered: (Background: (TexturePath: "CardBackgroundHovered.png", Border: 8)),
Pressed: (Background: (TexturePath: "CardBackgroundPressed.png", Border: 8))
);
Group {
LayoutMode: Top;
Anchor: (Full: 8);
// Icon
Group {
LayoutMode: Middle;
Anchor: (Height: 80);
AssetImage #CardIcon {
Anchor: (Width: 64, Height: 64);
}
}
// Title
Label #CardTitle {
Style: (
FontSize: 13,
HorizontalAlignment: Center,
TextColor: #ffffff,
Wrap: true
);
}
// Subtitle
Label #CardSubtitle {
Style: (
FontSize: 11,
HorizontalAlignment: Center,
TextColor: #7a9cc6
);
}
}
}$C = "../Common.ui";
$Sounds = "../Sounds.ui";
Button {
Anchor: (Width: 140, Height: 160, Right: 8, Bottom: 8);
Style: (
Sounds: $Sounds.@ButtonsLight,
Default: (Background: (TexturePath: "CardBackground.png", Border: 8)),
Hovered: (Background: (TexturePath: "CardBackgroundHovered.png", Border: 8)),
Pressed: (Background: (TexturePath: "CardBackgroundPressed.png", Border: 8))
);
Group {
LayoutMode: Top;
Anchor: (Full: 8);
// 图标
Group {
LayoutMode: Middle;
Anchor: (Height: 80);
AssetImage #CardIcon {
Anchor: (Width: 64, Height: 64);
}
}
// 标题
Label #CardTitle {
Style: (
FontSize: 13,
HorizontalAlignment: Center,
TextColor: #ffffff,
Wrap: true
);
}
// 副标题
Label #CardSubtitle {
Style: (
FontSize: 11,
HorizontalAlignment: Center,
TextColor: #7a9cc6
);
}
}
}HUD Element
HUD元素
Create in :
resources/Common/UI/Custom/MyHudElement.uiGroup {
Anchor: (Top: 20, Left: 20, Width: 200, Height: 40);
LayoutMode: Left;
// Background with transparency
Group #Container {
Background: #000000(0.4);
Padding: (Horizontal: 12, Vertical: 8);
LayoutMode: Left;
// Icon
Group {
Background: "StatusIcon.png";
Anchor: (Width: 24, Height: 24, Right: 8);
}
// Value display
Label #ValueLabel {
Style: (
FontSize: 18,
VerticalAlignment: Center,
TextColor: #ffffff
);
Text: "0";
}
}
}在中创建:
resources/Common/UI/Custom/MyHudElement.uiGroup {
Anchor: (Top: 20, Left: 20, Width: 200, Height: 40);
LayoutMode: Left;
// 半透明背景
Group #Container {
Background: #000000(0.4);
Padding: (Horizontal: 12, Vertical: 8);
LayoutMode: Left;
// 图标
Group {
Background: "StatusIcon.png";
Anchor: (Width: 24, Height: 24, Right: 8);
}
// 值显示
Label #ValueLabel {
Style: (
FontSize: 18,
VerticalAlignment: Center,
TextColor: #ffffff
);
Text: "0";
}
}
}Input Form
输入表单
$C = "../Common.ui";
$C.@PageOverlay {}
$C.@Container {
Anchor: (Width: 400, Height: 350);
#Title {
$C.@Title {
@Text = %server.customUI.formPage.title;
}
}
#Content {
LayoutMode: Top;
Padding: (Full: 16);
// Name field
Label {
Text: %server.customUI.formPage.nameLabel;
Style: $C.@DefaultLabelStyle;
Anchor: (Bottom: 4);
}
$C.@TextField #NameInput {
PlaceholderText: %server.customUI.formPage.namePlaceholder;
Anchor: (Bottom: 12);
}
// Amount field
Label {
Text: %server.customUI.formPage.amountLabel;
Style: $C.@DefaultLabelStyle;
Anchor: (Bottom: 4);
}
$C.@NumberField #AmountInput {
@Anchor = (Width: 100);
Value: 1;
Format: (MinValue: 1, MaxValue: 64);
Anchor: (Bottom: 12);
}
// Checkbox option
$C.@CheckBoxWithLabel #EnableOption {
@Text = %server.customUI.formPage.enableOption;
@Checked = false;
Anchor: (Bottom: 20);
}
// Submit button
$C.@TextButton #SubmitButton {
@Text = %server.customUI.general.submit;
}
}
}
$C.@BackButton {}$C = "../Common.ui";
$C.@PageOverlay {}
$C.@Container {
Anchor: (Width: 400, Height: 350);
#Title {
$C.@Title {
@Text = %server.customUI.formPage.title;
}
}
#Content {
LayoutMode: Top;
Padding: (Full: 16);
// 名称字段
Label {
Text: %server.customUI.formPage.nameLabel;
Style: $C.@DefaultLabelStyle;
Anchor: (Bottom: 4);
}
$C.@TextField #NameInput {
PlaceholderText: %server.customUI.formPage.namePlaceholder;
Anchor: (Bottom: 12);
}
// 数量字段
Label {
Text: %server.customUI.formPage.amountLabel;
Style: $C.@DefaultLabelStyle;
Anchor: (Bottom: 4);
}
$C.@NumberField #AmountInput {
@Anchor = (Width: 100);
Value: 1;
Format: (MinValue: 1, MaxValue: 64);
Anchor: (Bottom: 12);
}
// 复选框选项
$C.@CheckBoxWithLabel #EnableOption {
@Text = %server.customUI.formPage.enableOption;
@Checked = false;
Anchor: (Bottom: 20);
}
// 提交按钮
$C.@TextButton #SubmitButton {
@Text = %server.customUI.general.submit;
}
}
}
$C.@BackButton {}Custom UI Pages
自定义UI页面
Custom UI Pages are an alternative to the Window system for displaying server-controlled UI. They provide more flexibility for dynamic content and typed event handling.
自定义UI页面是窗口系统的替代方案,用于展示服务器控制的UI,它为动态内容和类型化事件处理提供了更大的灵活性。
When to Use Custom Pages vs Windows
自定义页面 vs 窗口的适用场景
| Use Custom Pages When | Use Windows When |
|---|---|
| Dynamic list content | Inventory/item containers |
| Forms with text inputs | Crafting benches |
| Search/filter interfaces | Storage containers |
| Dialog/choice screens | Block-tied interactions |
| Complex multi-step wizards | Processing/smelting UI |
| 适用自定义页面的场景 | 适用窗口的场景 |
|---|---|
| 动态列表内容 | 背包/物品容器 |
| 带文本输入的表单 | 合成台 |
| 搜索/过滤界面 | 存储容器 |
| 对话框/选择界面 | 方块绑定交互 |
| 复杂多步骤向导 | 加工/冶炼UI |
Page Class Hierarchy
页面类层级
CustomUIPage (abstract)
├── BasicCustomUIPage # Simple static pages
└── InteractiveCustomUIPage<T> # Typed event handling (most common)CustomUIPage (抽象类)
├── BasicCustomUIPage // 简单静态页面
└── InteractiveCustomUIPage<T> // 类型化事件处理(最常用)Quick Start Example
快速入门示例
java
// 1. Create page class with typed event data
public class MyPage extends InteractiveCustomUIPage<MyPage.EventData> {
public MyPage(PlayerRef playerRef) {
super(playerRef, CustomPageLifetime.CanDismiss, EventData.CODEC);
}
@Override
public void build(Ref<EntityStore> ref, UICommandBuilder cmd, UIEventBuilder evt, Store<EntityStore> store) {
// Load UI file (from resources/Common/UI/Custom/)
cmd.append("MyPage.ui");
// Set values
cmd.set("#TitleLabel.Text", "Welcome!");
// Bind button click
evt.addEventBinding(
CustomUIEventBindingType.Activating,
"#ConfirmButton",
EventData.of("Action", "Confirm")
);
}
@Override
public void handleDataEvent(Ref<EntityStore> ref, Store<EntityStore> store, EventData data) {
if ("Confirm".equals(data.getAction())) {
this.close();
}
}
// Event data with codec
public static class EventData {
public static final BuilderCodec<EventData> CODEC = BuilderCodec.builder(EventData.class, EventData::new)
.append(new KeyedCodec<>("Action", Codec.STRING), (e, s) -> e.action = s, e -> e.action)
.add()
.build();
private String action;
public String getAction() { return action; }
}
}
// 2. Open from a command (AbstractPlayerCommand has 5 parameters)
@Override
protected void execute(
@Nonnull CommandContext context,
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref,
@Nonnull PlayerRef playerRef,
@Nonnull World world
) {
world.execute(() -> {
Player player = store.getComponent(ref, Player.getComponentType());
player.getPageManager().openCustomPage(ref, store, new MyPage(playerRef));
});
}java
// 1. 创建带类型化事件数据的页面类
public class MyPage extends InteractiveCustomUIPage<MyPage.EventData> {
public MyPage(PlayerRef playerRef) {
super(playerRef, CustomPageLifetime.CanDismiss, EventData.CODEC);
}
@Override
public void build(Ref<EntityStore> ref, UICommandBuilder cmd, UIEventBuilder evt, Store<EntityStore> store) {
// 加载UI文件(来自resources/Common/UI/Custom/)
cmd.append("MyPage.ui");
// 设置值
cmd.set("#TitleLabel.Text", "欢迎!");
// 绑定按钮点击事件
evt.addEventBinding(
CustomUIEventBindingType.Activating,
"#ConfirmButton",
EventData.of("Action", "Confirm")
);
}
@Override
public void handleDataEvent(Ref<EntityStore> ref, Store<EntityStore> store, EventData data) {
if ("Confirm".equals(data.getAction())) {
this.close();
}
}
// 带编解码器的事件数据
public static class EventData {
public static final BuilderCodec<EventData> CODEC = BuilderCodec.builder(EventData.class, EventData::new)
.append(new KeyedCodec<>("Action", Codec.STRING), (e, s) -> e.action = s, e -> e.action)
.add()
.build();
private String action;
public String getAction() { return action; }
}
}
// 2. 通过命令打开页面(AbstractPlayerCommand有5个参数)
@Override
protected void execute(
@Nonnull CommandContext context,
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref,
@Nonnull PlayerRef playerRef,
@Nonnull World world
) {
world.execute(() -> {
Player player = store.getComponent(ref, Player.getComponentType());
player.getPageManager().openCustomPage(ref, store, new MyPage(playerRef));
});
}Key Components
核心组件
UICommandBuilder
UICommandBuilder
Loads UI files and sets property values. All files are in :
.uiresources/Common/UI/Custom/java
UICommandBuilder cmd = new UICommandBuilder();
cmd.append("MyPage.ui"); // Load UI file (just filename)
cmd.set("#Label.Text", "Hello"); // Set text
cmd.set("#Checkbox.Value", true); // Set boolean
cmd.clear("#List"); // Clear children
cmd.append("#List", "ListItem.ui"); // Add child (just filename)用于加载UI文件并设置属性值,所有.ui文件都位于:
resources/Common/UI/Custom/java
UICommandBuilder cmd = new UICommandBuilder();
cmd.append("MyPage.ui"); // 加载UI文件(仅需文件名)
cmd.set("#Label.Text", "Hello"); // 设置文本
cmd.set("#Checkbox.Value", true); // 设置布尔值
cmd.clear("#List"); // 清空子元素
cmd.append("#List", "ListItem.ui"); // 添加子元素(仅需文件名)UIEventBuilder
UIEventBuilder
Binds UI events to server callbacks:
java
UIEventBuilder evt = new UIEventBuilder();
// Button click with static data
evt.addEventBinding(
CustomUIEventBindingType.Activating,
"#Button",
EventData.of("Action", "Click")
);
// Input change capturing value (@ prefix = codec key)
evt.addEventBinding(
CustomUIEventBindingType.ValueChanged,
"#SearchInput",
EventData.of("@Query", "#SearchInput.Value")
);用于绑定UI事件到服务器回调:
java
UIEventBuilder evt = new UIEventBuilder();
// 带静态数据的按钮点击事件
evt.addEventBinding(
CustomUIEventBindingType.Activating,
"#Button",
EventData.of("Action", "Click")
);
// 捕获输入值的变更事件(@前缀表示编解码器键)
evt.addEventBinding(
CustomUIEventBindingType.ValueChanged,
"#SearchInput",
EventData.of("@Query", "#SearchInput.Value")
);CustomPageLifetime
页面生命周期选项
| Value | Description |
|---|---|
| Only server can close |
| Player can close with ESC |
| ESC or world interaction |
| 值 | 描述 |
|---|---|
| 仅服务器可关闭页面 |
| 玩家可按ESC关闭页面 |
| 玩家可按ESC或与世界交互关闭页面 |
Dynamic List Pattern
动态列表模式
java
private void buildList(UICommandBuilder cmd, UIEventBuilder evt) {
cmd.clear("#ItemList");
for (int i = 0; i < items.size(); i++) {
String selector = "#ItemList[" + i + "]";
cmd.append("#ItemList", "ListItem.ui"); // Just filename, not path
cmd.set(selector + " #Name.Text", items.get(i).getName());
evt.addEventBinding(
CustomUIEventBindingType.Activating,
selector,
EventData.of("ItemId", items.get(i).getId()),
false // Don't lock interface
);
}
}
// Update list without full rebuild
public void refreshList() {
UICommandBuilder cmd = new UICommandBuilder();
UIEventBuilder evt = new UIEventBuilder();
buildList(cmd, evt);
this.sendUpdate(cmd, evt, false);
}java
private void buildList(UICommandBuilder cmd, UIEventBuilder evt) {
cmd.clear("#ItemList");
for (int i = 0; i < items.size(); i++) {
String selector = "#ItemList[" + i + "]";
cmd.append("#ItemList", "ListItem.ui"); // 仅需文件名,无需路径
cmd.set(selector + " #Name.Text", items.get(i).getName());
evt.addEventBinding(
CustomUIEventBindingType.Activating,
selector,
EventData.of("ItemId", items.get(i).getId()),
false // 不锁定界面
);
}
}
// 无需完全重建即可更新列表
public void refreshList() {
UICommandBuilder cmd = new UICommandBuilder();
UIEventBuilder evt = new UIEventBuilder();
buildList(cmd, evt);
this.sendUpdate(cmd, evt, false);
}Closing Pages
关闭页面
java
// From within page
this.close();
// From outside
player.getPageManager().setPage(ref, store, Page.None);See for complete documentation including:
references/custom-ui-pages.md- Full class reference for CustomUIPage, InteractiveCustomUIPage, BasicCustomUIPage
- All UICommandBuilder and UIEventBuilder methods
- CustomUIEventBindingType enum values
- BuilderCodec pattern for typed event data
- Complete working examples
java
// 在页面内部关闭
this.close();
// 在页面外部关闭
player.getPageManager().setPage(ref, store, Page.None);查看获取完整文档,包括:
references/custom-ui-pages.md- CustomUIPage、InteractiveCustomUIPage、BasicCustomUIPage的完整类参考
- UICommandBuilder和UIEventBuilder的所有方法
- CustomUIEventBindingType枚举值
- 类型化事件数据的BuilderCodec模式
- 完整的工作示例
Best Practices
最佳实践
State Management
状态管理
java
// Always invalidate after modifications
public void updateValue(String key, Object value) {
windowData.addProperty(key, value.toString());
invalidate(); // Mark for next update cycle
}
// For structural changes, use setNeedRebuild
public void rebuildCategories() {
recalculateCategories();
setNeedRebuild(); // Client will re-render entire window
invalidate();
}java
// 修改后务必调用invalidate()
public void updateValue(String key, Object value) {
windowData.addProperty(key, value.toString());
invalidate(); // 标记为需要在下一更新周期更新
}
// 结构变更时使用setNeedRebuild
public void rebuildCategories() {
recalculateCategories();
setNeedRebuild(); // 客户端将重新渲染整个窗口
invalidate();
}Resource Cleanup
资源清理
java
@Override
protected void onClose0() {
// Cancel scheduled tasks
if (updateTask != null) {
updateTask.cancel(false);
}
// Save state
saveToDatabase();
// Return items to player if needed
returnItemsToPlayer();
// Unregister event listeners (if manually registered)
}java
@Override
protected void onClose0() {
// 取消定时任务
if (updateTask != null) {
updateTask.cancel(false);
}
// 保存状态
saveToDatabase();
// 如有需要,将物品返还给玩家
returnItemsToPlayer();
// 注销手动注册的事件监听器
}Thread Safety
线程安全
Window operations should be on the main server thread:
java
public void updateFromAsync(Data data) {
server.getScheduler().runTask(() -> {
applyData(data);
invalidate();
});
}窗口操作应在服务器主线程执行:
java
public void updateFromAsync(Data data) {
server.getScheduler().runTask(() -> {
applyData(data);
invalidate();
});
}Progress Update Throttling
进度更新节流
For windows with progress bars (like crafting), throttle updates:
java
private static final float MIN_PROGRESS_CHANGE = 0.05f;
private static final long MIN_UPDATE_INTERVAL_MS = 500L;
private float lastUpdatePercent;
private long lastUpdateTimeMs;
private void checkProgressInvalidate(float percent) {
if (lastUpdatePercent != percent) {
long time = System.currentTimeMillis();
if (percent >= 1.0f ||
percent < lastUpdatePercent ||
percent - lastUpdatePercent > MIN_PROGRESS_CHANGE ||
time - lastUpdateTimeMs > MIN_UPDATE_INTERVAL_MS ||
lastUpdateTimeMs == 0L) {
lastUpdatePercent = percent;
lastUpdateTimeMs = time;
invalidate();
}
}
}对于带进度条的窗口(如合成台),请对更新操作进行节流:
java
private static final float MIN_PROGRESS_CHANGE = 0.05f;
private static final long MIN_UPDATE_INTERVAL_MS = 500L;
private float lastUpdatePercent;
private long lastUpdateTimeMs;
private void checkProgressInvalidate(float percent) {
if (lastUpdatePercent != percent) {
long time = System.currentTimeMillis();
if (percent >= 1.0f ||
percent < lastUpdatePercent ||
percent - lastUpdatePercent > MIN_PROGRESS_CHANGE ||
time - lastUpdateTimeMs > MIN_UPDATE_INTERVAL_MS ||
lastUpdateTimeMs == 0L) {
lastUpdatePercent = percent;
lastUpdateTimeMs = time;
invalidate();
}
}
}Troubleshooting
故障排除
Window Not Opening
窗口无法打开
- Check returns
onOpen0()true - Verify WindowType is valid
- Check for exceptions in initialization
- Ensure WindowManager.openWindow() is called on correct thread
- 检查是否返回
onOpen0()true - 验证WindowType是否有效
- 检查初始化过程中是否有异常
- 确保WindowManager.openWindow()在正确的线程调用
Items Not Updating
物品未更新
- Call after modifications
invalidate() - Verify window implements correctly
ItemContainerWindow - Check is being called (usually automatic)
WindowManager.updateWindows() - Verify returns the correct container
getItemContainer()
- 修改后调用
invalidate() - 验证窗口是否正确实现
ItemContainerWindow - 检查是否被调用(通常自动执行)
WindowManager.updateWindows() - 验证是否返回正确的容器
getItemContainer()
Actions Not Received
操作未被接收
- Ensure is implemented
handleAction() - Check action type casting (use pattern matching)
instanceof - Verify window ID matches in client packets
- 确保已实现
handleAction() - 检查操作类型的转换(使用模式匹配)
instanceof - 验证客户端数据包中的窗口ID是否匹配
Window Closing Unexpectedly
窗口意外关闭
For subclasses:
BlockWindow- Check player is within (default 7.0)
maxDistance - Verify block still exists at position
- Ensure block type hasn't changed
对于子类:
BlockWindow- 检查玩家是否在范围内(默认7.0格)
maxDistance - 验证方块是否仍在指定位置
- 确保方块类型未发生变化
Detailed References
详细参考文档
For comprehensive documentation:
- - Complete .ui file syntax and widget reference
references/ui-file-syntax.md - - CustomUIPage system, event binding, and typed event handling
references/custom-ui-pages.md - - All window types with configuration options
references/window-types.md - - Item containers, sorting, and inventory handling
references/slot-handling.md
如需更全面的文档,请查看:
- - 完整的.ui文件语法和组件参考
references/ui-file-syntax.md - - 自定义UI页面系统、事件绑定和类型化事件处理
references/custom-ui-pages.md - - 所有窗口类型及配置选项
references/window-types.md - - 物品容器、排序和背包处理
references/slot-handling.md