hytale-ui-windows

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Hytale 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:
  1. Window System (Java) - For inventory containers, crafting benches, and block-tied UIs
    • Uses
      Window
      classes with
      WindowManager
    • Sends JSON data via
      getData()
    • Handles predefined
      WindowAction
      types
  2. Custom UI Pages (Java) - For dynamic forms, lists, dialogs, and interactive pages
    • Uses
      CustomUIPage
      classes with
      PageManager
    • Loads
      .ui
      files dynamically via
      UICommandBuilder
    • Binds events with typed data via
      UIEventBuilder
Both systems use client-side .ui files to define visual layout and styling.
Hytale的UI系统主要包含两种实现方式:
  1. 窗口系统(Java) - 适用于背包容器、合成台以及与方块绑定的UI
    • 使用
      Window
      类配合
      WindowManager
    • 通过
      getData()
      发送JSON数据
    • 处理预定义的
      WindowAction
      类型
  2. 自定义UI页面(Java) - 适用于动态表单、列表、对话框和交互式页面
    • 使用
      CustomUIPage
      类配合
      PageManager
    • 通过
      UICommandBuilder
      动态加载.ui文件
    • 通过
      UIEventBuilder
      绑定带类型数据的事件
两种系统都使用客户端.ui文件来定义视觉布局和样式。

.ui Files

.ui文件

UI files (
.ui
) are client-side layout files that define the visual structure of windows and pages. They use a declarative syntax with:
  • Variables (
    @Name = value;
    ) - Reusable values and styles
  • Imports (
    $C = "path/to/file.ui";
    ) - Reference other UI files
  • Elements (
    WidgetType { properties }
    ) - UI widgets with nested children
  • Templates (
    $C.@TemplateName { overrides }
    ) - Instantiate reusable components
UI文件(.ui)是客户端布局文件,用于定义窗口和页面的视觉结构。它使用声明式语法,包含:
  • 变量
    @Name = value;
    )- 可复用的值和样式
  • 导入
    $C = "path/to/file.ui";
    )- 引用其他UI文件
  • 元素
    WidgetType { properties }
    )- 包含子元素的UI组件
  • 模板
    $C.@TemplateName { overrides }
    )- 实例化可复用组件

IMPORTANT: File Location

重要提示:文件位置

All
.ui
files MUST be placed in
resources/Common/UI/Custom/
in your plugin JAR.
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.ui
Requirements:
  1. Your
    manifest.json
    MUST contain
    "IncludesAssetPack": true
  2. UI files go in
    resources/Common/UI/Custom/
    (NOT
    assets/Server/Content/UI/Custom/
    )
  3. 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
    .ui
    file is not in
    Common/UI/Custom/
    or the path is wrong
  • Double-check the file location and that
    IncludesAssetPack
    is set to
    true
所有.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
要求:
  1. manifest.json
    必须包含
    "IncludesAssetPack": true
  2. UI文件需放在
    resources/Common/UI/Custom/
    (而非
    assets/Server/Content/UI/Custom/
  3. 在Java代码中,仅通过文件名引用文件:
    commandBuilder.append("MyPage.ui")
常见错误:
Could not find document XXXXX for Custom UI Append command
  • 该错误表示.ui文件未在
    Common/UI/Custom/
    目录下,或路径错误
  • 请仔细检查文件位置,并确认
    IncludesAssetPack
    已设置为
    true

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

核心语法概念

SyntaxPurposeExample
@Var = value;
Variable definition
@FontSize = 16;
$Alias = "path";
Import file
$C = "../Common.ui";
$C.@Template {}
Use template
$C.@TextButton {}
#ElementId
Element ID for code
Label #Title {}
%key.path
Translation key
Text: %ui.title;
...@Style
Spread/extend
Style: (...@Base, Bold: true);
See
references/ui-file-syntax.md
for complete .ui file documentation.
语法用途示例
@Var = value;
变量定义
@FontSize = 16;
$Alias = "path";
导入文件
$C = "../Common.ui";
$C.@Template {}
使用模板
$C.@TextButton {}
#ElementId
供代码访问的元素ID
Label #Title {}
%key.path
翻译键
Text: %ui.title;
...@Style
扩展样式
Style: (...@Base, Bold: true);
查看
references/ui-file-syntax.md
获取完整的.ui文件文档。

Window 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

核心接口

InterfacePurpose
ItemContainerWindow
Windows with item inventory slots
MaterialContainerWindow
Windows with extra resource materials
ValidatedWindow
Windows that validate state (e.g., player distance)
接口用途
ItemContainerWindow
包含物品背包槽位的窗口
MaterialContainerWindow
包含额外资源材料的窗口
ValidatedWindow
需要验证状态的窗口(如玩家距离)

Window Types (WindowType Enum)

窗口类型(WindowType枚举)

WindowTypeValueDescriptionUse Case
Container
0Item storageChests, backpacks
PocketCrafting
1Field craftingPlayer inventory crafting
BasicCrafting
2Standard craftingCrafting tables
DiagramCrafting
3Blueprint-basedAdvanced workbenches, anvils
StructuralCrafting
4Block transformationStonecutters, construction benches
Processing
5Time-based conversionFurnaces, smelters
Memories
6Special displayMemory/achievement UI
窗口类型数值描述使用场景
Container
0物品存储箱子、背包
PocketCrafting
1随身合成玩家背包内的合成界面
BasicCrafting
2标准合成工作台
DiagramCrafting
3蓝图合成高级工作台、铁砧
StructuralCrafting
4方块转换切石机、建筑工作台
Processing
5计时转换熔炉、冶炼炉
Memories
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)-> 客户端: 关闭UI

Window Data Pattern

窗口数据模式

Windows use
getData()
to return a
JsonObject
that is serialized and sent to the client. This data controls client-side rendering:
java
@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()
返回
JsonObject
,该对象会被序列化并发送到客户端,用于控制客户端渲染:
java
@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
Window
and must implement these abstract methods:
java
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
    }
}
所有窗口都继承自
Window
,并必须实现以下抽象方法:
java
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
WindowManager
:
java
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");
            }
        });
    }
}
通过
WindowManager
打开窗口:
java
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
WindowManager.updateWindows()
which checks
isDirty
flag.
调用
invalidate()
标记窗口需要更新:
java
public void updateData(String newValue) {
    windowData.addProperty("value", newValue);
    invalidate(); // 标记窗口需要更新
}

// 完全重建窗口(客户端重新渲染整个窗口)
public void requireRebuild() {
    setNeedRebuild();
    invalidate();
}
更新操作会被批量处理,并通过
WindowManager.updateWindows()
发送,该方法会检查
isDirty
标记。

Window Manager

窗口管理器

The
WindowManager
handles window lifecycle for each player:
java
// 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);
WindowManager
负责管理每个玩家的窗口生命周期:
java
// 获取玩家的窗口管理器
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
    0
    is reserved for client-requested windows
  • ID
    -1
    is invalid
  • Server-assigned IDs start at 1 and increment
  • ID
    0
    预留给客户端请求的窗口
  • ID
    -1
    为无效ID
  • 服务器分配的ID从1开始递增

Block Windows

方块绑定窗口

Windows tied to blocks in the world (chests, crafting tables). Extends
BlockWindow
which implements
ValidatedWindow
:
java
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();
    }
}
与世界中方块绑定的窗口(如箱子、工作台),继承自
BlockWindow
,该类实现了
ValidatedWindow
java
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
automatically validates that:
  1. Player is within
    maxDistance
    of the block (default 7.0 blocks)
  2. The block still exists in the world
  3. The block type matches (via item comparison)
When validation fails, the window is automatically closed.
BlockWindow
会自动验证以下条件:
  1. 玩家与方块的距离在
    maxDistance
    范围内(默认7.0格)
  2. 方块仍存在于世界中
  3. 方块类型匹配(通过物品比较)
当验证失败时,窗口会自动关闭。

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
BenchWindow
:
java
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
}
所有合成台窗口都继承自
BenchWindow
java
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
ItemContainerWindow
:
java
public interface ItemContainerWindow {
    @Nonnull ItemContainer getItemContainer();
}
包含物品背包槽位的窗口需实现
ItemContainerWindow
接口:
java
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
ItemContainerWindow
, the
WindowManager
automatically:
  1. Registers a change listener to mark the window dirty when inventory changes
  2. Includes
    InventorySection
    in
    OpenWindow
    and
    UpdateWindow
    packets
  3. 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() {
        // 清理操作
    }
}
注意: 当窗口实现
ItemContainerWindow
时,
WindowManager
会自动:
  1. 注册变更监听器,当背包内容变化时标记窗口为脏状态
  2. OpenWindow
    UpdateWindow
    数据包中包含
    InventorySection
  3. 窗口关闭时注销监听器

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 IDClassFieldsDescription
0
CraftRecipeAction
recipeId: String
,
quantity: int
Craft a recipe
1
TierUpgradeAction
(none)Upgrade bench tier
2
SelectSlotAction
slot: int
Select a slot
3
ChangeBlockAction
down: boolean
Cycle block type direction
4
SetActiveAction
state: boolean
Toggle processing on/off
5
CraftItemAction
(none)Confirm diagram crafting
6
UpdateCategoryAction
category: String
,
itemCategory: String
Change recipe category
7
CancelCraftingAction
(none)Cancel current crafting
8
SortItemsAction
sortType: SortType
Sort inventory items
类型ID字段描述
0
CraftRecipeAction
recipeId: String
,
quantity: int
合成指定配方
1
TierUpgradeAction
升级合成台等级
2
SelectSlotAction
slot: int
选择槽位
3
ChangeBlockAction
down: boolean
循环切换方块类型方向
4
SetActiveAction
state: boolean
切换加工状态
5
CraftItemAction
确认蓝图合成
6
UpdateCategoryAction
category: String
,
itemCategory: String
切换配方分类
7
CancelCraftingAction
取消当前合成
8
SortItemsAction
sortType: SortType
排序背包物品

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

服务器到客户端

PacketIDFieldsPurpose
OpenWindow
200
id
,
windowType
,
windowData
,
inventory
,
extraResources
Open window on client
UpdateWindow
201
id
,
windowData
,
inventory
,
extraResources
Update window contents
CloseWindow
202
id
Close window on client
数据包ID字段用途
OpenWindow
200
id
,
windowType
,
windowData
,
inventory
,
extraResources
在客户端打开窗口
UpdateWindow
201
id
,
windowData
,
inventory
,
extraResources
更新窗口内容
CloseWindow
202
id
在客户端关闭窗口

Client to Server

客户端到服务器

PacketIDFieldsPurpose
SendWindowAction
203
id
,
action: WindowAction
User interaction
ClientOpenWindow
204
type: WindowType
Request client-initiated window
数据包ID字段用途
SendWindowAction
203
id
,
action: WindowAction
发送用户交互操作
ClientOpenWindow
204
type: WindowType
请求打开客户端触发的窗口

Packet Structure

数据包结构

The
OpenWindow
packet includes:
  • windowData
    : JSON string with window-specific data
  • inventory
    :
    InventorySection
    (nullable) - only for
    ItemContainerWindow
  • extraResources
    :
    ExtraResources
    (nullable) - only for
    MaterialContainerWindow
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
数据包包含:
  • windowData
    : JSON字符串,包含窗口特定数据
  • 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_TYPES
:
java
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
ClientOpenWindow
packet, the server:
  1. Looks up the
    WindowType
    in
    CLIENT_REQUESTABLE_WINDOW_TYPES
  2. Creates a new window instance using the supplier
  3. 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_TYPES
中注册:
java
public class MyPlugin extends JavaPlugin {
    
    @Override
    protected void setup() {
        // 注册客户端可请求的窗口
        Window.CLIENT_REQUESTABLE_WINDOW_TYPES.put(
            WindowType.Memories,
            MemoriesWindow::new
        );
    }
}
当客户端发送
ClientOpenWindow
数据包时,服务器会:
  1. CLIENT_REQUESTABLE_WINDOW_TYPES
    中查找
    WindowType
  2. 使用提供的供应商创建新窗口实例
  3. 通过
    windowManager.clientOpenWindow(window)
    以ID 0打开窗口
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
MaterialContainerWindow
:
java
public interface MaterialContainerWindow {
    @Nonnull MaterialExtraResourcesSection getExtraResourcesSection();
    void invalidateExtraResources();
    boolean isValid();
}
包含额外资源材料的窗口需实现
MaterialContainerWindow
接口:
java
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 {}
resources/Common/UI/Custom/
目录下创建新的页面UI文件:
// 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.ui
:
Group {
  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.ui
中创建:
Group {
  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 WhenUse Windows When
Dynamic list contentInventory/item containers
Forms with text inputsCrafting benches
Search/filter interfacesStorage containers
Dialog/choice screensBlock-tied interactions
Complex multi-step wizardsProcessing/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
.ui
files are in
resources/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

页面生命周期选项

ValueDescription
CantClose
Only server can close
CanDismiss
Player can close with ESC
CanDismissOrCloseThroughInteraction
ESC or world interaction
描述
CantClose
仅服务器可关闭页面
CanDismiss
玩家可按ESC关闭页面
CanDismissOrCloseThroughInteraction
玩家可按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
references/custom-ui-pages.md
for complete documentation including:
  • 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

窗口无法打开

  1. Check
    onOpen0()
    returns
    true
  2. Verify WindowType is valid
  3. Check for exceptions in initialization
  4. Ensure WindowManager.openWindow() is called on correct thread
  1. 检查
    onOpen0()
    是否返回
    true
  2. 验证WindowType是否有效
  3. 检查初始化过程中是否有异常
  4. 确保WindowManager.openWindow()在正确的线程调用

Items Not Updating

物品未更新

  1. Call
    invalidate()
    after modifications
  2. Verify window implements
    ItemContainerWindow
    correctly
  3. Check
    WindowManager.updateWindows()
    is being called (usually automatic)
  4. Verify
    getItemContainer()
    returns the correct container
  1. 修改后调用
    invalidate()
  2. 验证窗口是否正确实现
    ItemContainerWindow
  3. 检查
    WindowManager.updateWindows()
    是否被调用(通常自动执行)
  4. 验证
    getItemContainer()
    是否返回正确的容器

Actions Not Received

操作未被接收

  1. Ensure
    handleAction()
    is implemented
  2. Check action type casting (use
    instanceof
    pattern matching)
  3. Verify window ID matches in client packets
  1. 确保已实现
    handleAction()
  2. 检查操作类型的转换(使用
    instanceof
    模式匹配)
  3. 验证客户端数据包中的窗口ID是否匹配

Window Closing Unexpectedly

窗口意外关闭

For
BlockWindow
subclasses:
  1. Check player is within
    maxDistance
    (default 7.0)
  2. Verify block still exists at position
  3. Ensure block type hasn't changed
对于
BlockWindow
子类:
  1. 检查玩家是否在
    maxDistance
    范围内(默认7.0格)
  2. 验证方块是否仍在指定位置
  3. 确保方块类型未发生变化

Detailed References

详细参考文档

For comprehensive documentation:
  • references/ui-file-syntax.md
    - Complete .ui file syntax and widget reference
  • references/custom-ui-pages.md
    - CustomUIPage system, event binding, and typed event handling
  • references/window-types.md
    - All window types with configuration options
  • references/slot-handling.md
    - Item containers, sorting, and inventory handling
如需更全面的文档,请查看:
  • references/ui-file-syntax.md
    - 完整的.ui文件语法和组件参考
  • references/custom-ui-pages.md
    - 自定义UI页面系统、事件绑定和类型化事件处理
  • references/window-types.md
    - 所有窗口类型及配置选项
  • references/slot-handling.md
    - 物品容器、排序和背包处理