nms-block-entity

star 1

實作自定義 NMS BlockEntity(含 NBT 序列化、Tick 邏輯、客戶端同步),比 Bukkit BlockState 更靈活(Paper NMS + Mojang-mapped)/ Implement custom NMS BlockEntity with NBT serialization, tick logic, and client sync

MrPippi By MrPippi schedule Updated 4/30/2026

name: nms-block-entity description: "實作自定義 NMS BlockEntity(含 NBT 序列化、Tick 邏輯、客戶端同步),比 Bukkit BlockState 更靈活(Paper NMS + Mojang-mapped)/ Implement custom NMS BlockEntity with NBT serialization, tick logic, and client sync"

NMS Block Entity / NMS 自定義方塊實體

技能名稱 / Skill Name

nms-block-entity

目的 / Purpose

繼承 NMS BlockEntity 實作自定義方塊實體,實現 NBT 讀寫、伺服器端 Tick 邏輯(BlockEntityTicker)、以及透過封包同步狀態至客戶端。

NMS 版本需求 / NMS Version Requirements

  • Paper 1.21 – 1.21.3
  • Paperweight userdev 1.7.2+
  • Mojang mappings(已由 Paper 1.20.5+ 原生支援)

觸發條件 / Triggers

  • 「BlockEntity」「TileEntity」「自定義方塊實體」「block entity」「tile entity」
  • 「方塊 tick」「block tick」「BlockEntityTicker」「方塊 NBT」「block nbt」
  • 「自定義方塊 NMS」「custom block nms」

輸入參數 / Inputs

參數 範例 說明
package_name com.example.block 產出類別所在 package
entity_class_name GeneratorBlockEntity BlockEntity 子類名
has_ticker true 是否需要 tick 邏輯
base_block CHEST 用哪個 NMS 方塊作為載體

輸出產物 / Outputs

  • CustomBlockEntity.java — BlockEntity 實作(含 NBT load/save)
  • CustomBlockEntityTicker.java(選)— ServerLevel tick 邏輯
  • BlockEntityHelper.java — 在世界中取得/設定 BlockEntity 工具

Paperweight 建置設定 / Build Setup

參見 Skills/paper-nms/PLATFORM.md。關鍵依賴:

dependencies {
    paperweight.paperDevBundle('1.21.1-R0.1-SNAPSHOT')
}

代碼範本 / Code Template

CustomBlockEntity.java

package com.example.block;

import net.minecraft.core.BlockPos;
import net.minecraft.core.HolderLookup;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ClientGamePacketListener;
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;

import javax.annotation.Nullable;

@SuppressWarnings("UnstableApiUsage")
public class CustomBlockEntity extends BlockEntity {

    // 自定義欄位
    private int storedEnergy = 0;
    private String ownerName = "";

    public CustomBlockEntity(BlockEntityType<?> type, BlockPos pos, BlockState state) {
        super(type, pos, state);
    }

    // ─── NBT 序列化 ──────────────────────────────────────────────────

    /** 儲存自定義資料到 NBT(世界儲存 + 封包同步)。 */
    @Override
    protected void saveAdditional(CompoundTag tag, HolderLookup.Provider provider) {
        super.saveAdditional(tag, provider);
        tag.putInt("storedEnergy", storedEnergy);
        tag.putString("ownerName", ownerName);
    }

    /** 從 NBT 讀取自定義資料(世界載入 + 封包接收)。 */
    @Override
    public void loadAdditional(CompoundTag tag, HolderLookup.Provider provider) {
        super.loadAdditional(tag, provider);
        storedEnergy = tag.getInt("storedEnergy");
        ownerName = tag.getString("ownerName");
    }

    // ─── 客戶端同步 ──────────────────────────────────────────────────

    /** 產生傳送給客戶端的更新封包(BlockEntityDataPacket)。 */
    @Override
    @Nullable
    public Packet<ClientGamePacketListener> getUpdatePacket() {
        return ClientboundBlockEntityDataPacket.create(this);
    }

    /** 取得用於封包同步的 NBT(可只傳必要欄位)。 */
    @Override
    public CompoundTag getUpdateTag(HolderLookup.Provider provider) {
        return saveWithoutMetadata(provider);
    }

    /** 通知客戶端狀態變更(呼叫後自動發送更新封包)。 */
    public void markDirtyAndSync() {
        setChanged();
        if (level != null && !level.isClientSide) {
            level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3);
        }
    }

    // ─── Getter/Setter ───────────────────────────────────────────────

    public int getStoredEnergy() { return storedEnergy; }

    public void setStoredEnergy(int energy) {
        this.storedEnergy = energy;
        markDirtyAndSync();
    }

    public String getOwnerName() { return ownerName; }

    public void setOwnerName(String name) {
        this.ownerName = name;
        markDirtyAndSync();
    }
}

CustomBlockEntityTicker.java(Tick 邏輯)

package com.example.block;

import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntityTicker;
import net.minecraft.world.level.block.state.BlockState;

@SuppressWarnings("UnstableApiUsage")
public class CustomBlockEntityTicker implements BlockEntityTicker<CustomBlockEntity> {

    private static final int TICK_INTERVAL = 20; // 每秒執行一次
    private int tickCount = 0;

    @Override
    public void tick(Level level, BlockPos pos, BlockState state, CustomBlockEntity entity) {
        if (level.isClientSide) return; // 只在伺服器端執行

        tickCount++;
        if (tickCount % TICK_INTERVAL != 0) return;

        // 每 20 tick(1 秒)執行一次邏輯
        if (entity.getStoredEnergy() < 1000) {
            entity.setStoredEnergy(entity.getStoredEnergy() + 10);
        }
    }
}

BlockEntityHelper.java(世界操作工具)

package com.example.block;

import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.entity.BlockEntity;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.craftbukkit.v1_21_R1.CraftWorld;

import java.util.Optional;

@SuppressWarnings("UnstableApiUsage")
public final class BlockEntityHelper {

    private BlockEntityHelper() {}

    /** 取得指定位置的 BlockEntity(若類型不匹配回傳 empty)。 */
    @SuppressWarnings("unchecked")
    public static <T extends BlockEntity> Optional<T> get(
            Location loc, Class<T> type) {
        net.minecraft.world.level.Level level = ((CraftWorld) loc.getWorld()).getHandle();
        BlockPos pos = new BlockPos(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ());
        BlockEntity be = level.getBlockEntity(pos);
        if (type.isInstance(be)) return Optional.of(type.cast(be));
        return Optional.empty();
    }

    /** 取得指定位置的自定義 BlockEntity。 */
    public static Optional<CustomBlockEntity> getCustom(Location loc) {
        return get(loc, CustomBlockEntity.class);
    }
}

推薦目錄結構 / Recommended Directory Structure

src/main/java/com/example/
├── MyNmsPlugin.java
└── block/
    ├── CustomBlockEntity.java
    ├── CustomBlockEntityTicker.java
    └── BlockEntityHelper.java

執行緒安全注意事項 / Thread Safety

  • ⚠️ 所有 BlockEntity 操作(讀取、修改、markDirtyAndSync()必須在主執行緒呼叫
  • ⚠️ tick() 由 NMS 在主執行緒呼叫,內部不可進行阻塞 IO
  • ⚠️ level.isClientSide 必須在 tick 內檢查,防止在客戶端 Tick 執行伺服器邏輯
  • 詳見 Skills/_shared/nms-threading.md

失敗回退 / Fallback

錯誤 原因 解法
NBT 資料丟失 saveAdditional 未呼叫 super 確保呼叫 super.saveAdditional(tag, provider)
客戶端不同步 未呼叫 markDirtyAndSync() 每次修改欄位後呼叫
getBlockEntity() 回傳 null 方塊位置無 BlockEntity 確認方塊類型有 BlockEntity 支援
tick 未執行 BlockEntityType 未正確注冊 Paper plugin 環境需透過 RegistryAccess 注冊類型
Install via CLI
npx skills add https://github.com/MrPippi/MJP-Claude-Skills --skill nms-block-entity
Repository Details
star Stars 1
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator