C++ 接口参考¶
1.1 入口¶
namespace hologramlib {
class IHologramLib {
public:
static IHologramLib& getInstance(); // 唯一 dllexport 工厂
IShapeDrawer& shapes(); // 形状渲染
IHologramText& holograms(); // 悬浮字全息
IItemDetail& itemDetails(); // 物品详情
IItemDisplay& itemDisplays(); // FMBE 物品悬浮(1.6.0)
ICustomEntity& customEntities(); // 自定义实体(1.10.0)
IParticleShape& particleShapes(); // 通用粒子形状(1.14.0)
IPlayerNpc& playerNpcs(); // 假玩家 NPC(1.16.0)
ITradeMenu& tradeMenus(); // 村民交易菜单(1.21.0)
INpcDialogue& npcDialogs(); // NPC 对话框(1.21.0)
IContainerMenu& containerMenus(); // 虚拟容器 / 列表界面(1.21.0)
IPlayerSensing& playerSensing(); // 感知域(1.21.0)
IFakeInventory& fakeInventories(); // 背包虚容器(1.23.0)
ISulfurDisplay& sulfurDisplays(); // 硫磺立方体展示(1.23.0)
IViewOverride& viewOverrides(); // 客户端视图覆盖(1.25.0)
bool isLseAvailable(); // LSE 兼容层是否已挂载
uint32_t version(); // 0x012300 = 1.29.0
int64_t findNearestItemDisplay(float x, float y, float z, int dim, double maxDist); // 1.6.0
void setGhostInteractListener(std::function<void(GhostInteractEvent const&)> listener); // 1.12.0
void clearGhostInteractListener(); // 1.12.0
std::vector<std::string> pollGhostInteractions(); // 1.12.0 LSE 轮询版
uint64_t addGhostInteractListener(std::function<void(GhostInteractEvent const&)> listener); // 1.19.1 多播
bool removeGhostInteractListener(uint64_t token); // 1.19.1
uint64_t addActorInteractListener(std::function<void(ActorInteractEvent const&)> listener); // 1.27.0 真实实体交互(多播)
bool removeActorInteractListener(uint64_t token); // 1.27.0
};
}
通用约定:
- 所有
id为int64_t,创建失败返回< 0 - 颜色分量
0.0 ~ 1.0(RGBA);坐标为世界坐标;setDuration单位秒 - 方法内部互斥(线程安全);发包在调用线程执行,建议主线程调用
1.2 IShapeDrawer(形状渲染)¶
| 方法 | 签名 | 说明 |
|---|---|---|
| createText | (float x, float y, float z, std::string const& text) -> int64_t |
文本形状 |
| createLine | (x1,y1,z1, x2,y2,z2: float) -> int64_t |
线段 |
| createBox | (x1,y1,z1, x2,y2,z2: float) -> int64_t |
盒体(两点对角) |
| createCircle | (x, y, z, scale: float) -> int64_t |
圆 |
| createSphere | (x, y, z, scale: float) -> int64_t |
球 |
| createArrow | (x1,y1,z1, x2,y2,z2: float) -> int64_t |
箭头 |
| setColor | (int64_t id, float r, float g, float b, float a) -> bool |
RGBA |
| setScale | (int64_t id, float scale) -> bool |
缩放 |
| setDuration | (int64_t id, float seconds) -> bool |
存留时长 |
| setDimension | (int64_t id, int dimId) -> bool |
维度 |
| setLocation | (int64_t id, float x, float y, float z) -> bool |
位置 |
| setText | (int64_t id, std::string const& text) -> bool |
文本内容 |
| setRotation | (int64_t id, float pitch, float yaw, float roll) -> bool |
三轴旋转(度; 文本形状自动启用 useRotation + 双面渲染) |
| clearRotation | (int64_t id) -> bool |
清除旋转(恢复面向相机) |
| draw | (int64_t id) -> bool |
全维度可见者绘制 |
| drawToPlayer | (int64_t id, std::string const& playerName) -> bool |
指定玩家 |
| drawToDimension | (int64_t id, int dimId) -> bool |
指定维度 |
| remove | (int64_t id) -> bool |
隐藏(保留数据) |
| update | (int64_t id) -> bool |
可见时原地重发(同 networkId 覆盖,无闪烁) |
| destroy | (int64_t id) -> bool |
销毁 |
| destroyAll | () -> void |
全部销毁 |
| exists | (int64_t id) -> bool |
存在性 |
| type | (int64_t id) -> ShapeType |
形状类型 |
| setBackgroundColor | (int64_t id, float r, float g, float b, float a) -> bool |
背景框颜色(1.26.0; 仅文本形状, 不设 = 客户端默认色) |
| clearBackgroundColor | (int64_t id) -> bool |
回客户端默认背景色(1.26.0) |
| setDepthTest | (int64_t id, bool enabled) -> bool |
穿墙可见性(1.26.0; true = 被方块/实体遮挡, false = 始终渲染) |
1.3 IHologramText(悬浮字全息)¶
整块多行文本单一背景框渲染(\n 合并所有行);变更后调用 refresh 原地重绘(同 networkId 覆盖,无闪烁)。行索引 0 起。
1.26.0 收缩后口径:行只负责文本;颜色/缩放/背景框/穿墙/旋转都是整块属性 —— 行级样式差异请在文本内嵌 § 颜色代码。
| 分类 | 方法 | 签名 | 说明 |
|---|---|---|---|
| 生命周期 | create | (float x, float y, float z) -> int64_t |
创建 |
| destroy | (int64_t id) -> bool |
销毁 | |
| destroyAll | () -> void |
全部销毁 | |
| 行管理 | addLine | (int64_t id, std::string const& text) -> bool |
追加行 |
| setLineText | (int64_t id, int line, std::string const& text) -> bool |
行文本 | |
| setLinePool | (int64_t id, int line, std::vector<std::string> const& content, int intervalMs) -> bool |
动态行(1.26.0): 内容池按 intervalMs 时间取模轮播(无状态, 多个浮字同池同相); 池 ≤1 项 / 间隔≤0 = 不轮播; 空 = 清除池 |
|
| setLineParseVariables | (int64_t id, int line, bool enabled) -> bool |
该行是否解析变量(默认 true; 关 = {} 按字面量显示)(1.26.0) |
|
| removeLine | (int64_t id, int line) -> bool |
移除行 | |
| clearLines | (int64_t id) -> bool |
清空行 | |
| getLineCount | (int64_t id) -> int |
行数 | |
| 颜色 | setColor | (int64_t id, r, g, b, a: float) -> bool |
整块文字颜色 |
| setBackgroundColor | (int64_t id, r, g, b, a: float) -> bool |
背景框颜色(不设 = 客户端默认色; 1.26.0) | |
| clearBackgroundColor | (int64_t id) -> bool |
清除背景框颜色(1.26.0) | |
| 样式 | setScale | (int64_t id, float scale) -> bool |
整块缩放(1.26.0; 替代原行级 setLineScale) |
| setDepthTest | (int64_t id, bool enabled) -> bool |
穿墙可见性(1.26.0; true = 被方块/实体遮挡) | |
| setRotation | (int64_t id, pitch, yaw, roll: float) -> bool |
三轴固定朝向(度; 不再面向相机; 1.26.0) | |
| clearRotation | (int64_t id) -> bool |
恢复面向相机(1.26.0) | |
| 位置 | setLocation | (int64_t id, float x, float y, float z) -> bool |
位置 |
| setFollowPlayer | (int64_t id, std::string const& playerName, float offsetY) -> bool |
跟随玩家(位置在调用时就地解析, 无自驱) | |
| clearFollowPlayer | (int64_t id) -> bool |
取消跟随 | |
| setDimension | (int64_t id, int dimId) -> bool |
迁移维度:已绘制时同步底层形状维度并按原绘制目标原地重发(无闪烁) | |
| 显示 | draw / drawToDimension / drawToPlayer / remove | 同形状语义 | |
| refresh | (int64_t id) -> bool |
重解析变量/跟随坐标并原地重发 |
1.26.0 收缩(明记):
setLineScale/setLineColor/setLineGradient/setLineRainbow/setLineScroll/setVerticalAnimation/setLineSpacing与tick已移除 —— 这些在"整块单形状" 模型下不可实现或空转。原因与替代(§ 颜色代码 / setScale)见 更新日志 与 版本与 ABI 对照。 C++ ABI:它们保留为废弃空槽(非纯虚 + 内联空实现 +[[deprecated]],调用返回false/ 无动作), 虚表槽位与 1.25.0 一致 —— 未重编译的旧消费方二进制按旧槽位调用不会错位;LSE 侧导出照旧删除。动态行(1.26.0;重构自 Phantom,LGPL-3.0,已按其许可标注来源):
setLinePool= 内容池按intervalMs时间取模轮播 (无状态,多个浮字同池同相);setLineParseVariables= 行级变量开关(默认开)。轮播/变量刷新由库内 0.5s 节流驱动、内容变了才重发(区别于本版移除的自驱"偏移"——那时每拍都在动、但驱出来的东西 没有消费方)。变量集:{player}{online}{time}{tps}{dimension}{x}{y}{z}+ 经 MeowPAPI 的外部占位符。含{var}的文本按观看者各一份形状(逐玩家解析),其刷新会逐玩家 原地重发(同 networkId,无闪烁)。
内置变量(PAPI 翻译后兜底解析):{time} {online} {tps} {player}。
外部占位符 %name% / {name} 经 LseBridge 调 MeowPAPI::translateString(WithPlayer)(可选,缺席原样保留)。
1.4 IItemDetail(物品详情)¶
// 在 (x,y,z) 显示 "本地化物品名 xN"(自动翻译; count<=1 无数量后缀)
// customText 非空则完全替代自动文本(支持 § 颜色码与 {变量})
// 返回悬浮字 ID(可用 holograms() 继续精修)
int64_t show(int dimId, float x, float y, float z,
std::string const std::string const& itemId, int aux, int count, itemId, int aux, int count,
std::string const std::string const& customText = ""); customText = "");
bool hide(int64_t id);
物品名解析链:ItemRegistry(HashedString)→ ItemStack → I18n(getDescriptionId) → getName/getDescriptionName → 兜底原样显示 ID(不阻断)。
1.5 IItemDisplay(FMBE 物品悬浮显示,1.6.0)¶
配置驱动的物品/方块悬浮展示(FMBE 狐狸+发包)。全部变换字段为常量数字或 Molang 表达式字符串;持久化由消费者负责(库只管渲染)。
struct ItemDisplayConfig {
std::string item{"minecraft:diamond"}; // 物品标识符
int itemAux{0}; // 附加值
float x{0}, y{64}, z{0}; // 世界坐标
int dimension{0};
std::string offsetX{"0"}, offsetY{"-4"}, offsetZ{"0"}; // v.xpos/ypos/zpos 模型单位平移
std::string baseOffsetX/Y/Z{"0"}; // v.xbasepos... 渲染像素偏移
std::string rotX{"180"}, rotY{"0"}, rotZ{"180"}; // 三轴旋转(度)
std::string scale{"0.375"}; // 缩放(方块模式建议 0.5)
std::string extendScale{"1"}, extendRotX{"-90"}, extendRotY{"0"}; // 方块模式二段变换
int mode{0}; // 0=auto 1=item 2=block
double viewDistance{64.0}; // <=0 无限制
bool enabled{true};
std::string itemNbt{}; // 物品附加数据(SNBT; 自定义名称等)
bool itemGlint{false}; // 附魔光效(BDS 原生路径注入 1 级锋利)
float hitboxWidth{0.0f}; // AABB 判定体积宽(R53; 0 = 无判定体积, 1.17.0)
float hitboxHeight{0.0f}; // AABB 判定体积高(R54; 0 = 无判定体积, 1.17.0)
};
| 方法 | 签名 | 说明 |
|---|---|---|
| create | (ItemDisplayConfig const&) -> int64_t |
创建(<0 失败) |
| destroy / destroyAll | (int64_t) -> bool / () -> void |
销毁 |
| exists | (int64_t) -> bool |
存在性 |
| get | (int64_t, ItemDisplayConfig&) -> bool |
拷贝输出当前配置 |
| setItem | (int64_t, std::string const&, int) -> bool |
换物品 |
| setPosition | (int64_t, float, float, float, int) -> bool |
dim<0 仅改坐标 |
| setOffset / setBaseOffset | (int64_t, ox, oy, oz: string) -> bool |
平移(Molang) |
| setRotation | (int64_t, rx, ry, rz: string) -> bool |
三轴旋转(Molang) |
| setScale | (int64_t, scale: string) -> bool |
缩放(Molang) |
| setExtend | (int64_t, scale, rx, ry: string) -> bool |
方块二段变换 |
| setMode / setEnabled / setViewDistance | — | 行为 |
| rotateY | (int64_t, float) -> bool |
偏航叠加增量(常量/表达式自适应) |
| getAllIds | () -> std::vector<int64_t> |
全部 ID |
| createRandom | (ItemDisplayConfig const&) -> int64_t |
随机段 [0x10000000, 0x7FFFFFFF) 自动生成不重复 ID; 成功返回生成的 ID, <0 失败 |
| createWithId | (ItemDisplayConfig const&, int64_t desiredId) -> int64_t |
指定 ID 创建(持久化恢复用); desiredId<=0 或已被占用返回 -2 |
| isIdUsed | (int64_t) -> bool |
查询 ID 是否在用 |
| scaleBy | (int64_t, double factor) -> bool |
相对缩放:现有 scale × factor; 常量直接相乘, 表达式包裹 (expr)*factor; factor<=0 返回 false |
| setItemWithNbt | (int64_t, std::string const& item, int aux, std::string const& nbt) -> bool |
换物品(带附加数据):nbt 为 SNBT; 空串 = 清除; 解析失败按无 NBT 处理并告警 |
| setGlint | (int64_t, bool on) -> bool |
附魔光效开关:开 = BDS 原生 saveEnchantsToUserData 注入 1 级锋利; 幂等 |
| follow | (int64_t, std::string const& playerName, float offX, float offY, float offZ) -> bool |
1.17.0 展示跟随玩家:每 tick 同步目标实时坐标(含跨维度自动 respawn),对已见玩家发 MoveActorAbsolute(非 teleport,客户端插值)平滑位移,全程无 respawn;玩家下线自动解除(方块原地保留);setPosition 手动设位解除跟随 |
| unfollow | (int64_t) -> bool |
1.17.0 解除跟随;无跟随关系返回 true |
| setHitbox | (int64_t, float width, float height) -> bool |
1.17.0 AABB 判定体积:对已见玩家广播 SetActorData(R53/R54),即时无 respawn;数值持久入配置(respawn 自动按当前值发包);0/0 恢复不可命中 |
可见性由库内 Level tick hook 自动同步(每 20 tick:同维度 + 可见距离内玩家自动生成/移除;玩家断线自动清理);属性变更即时生效(对已见玩家原子 despawn→respawn)。setMode / setViewDistance 幂等(同值不标脏不 respawn)。
无感创建(createSeamless)、单次跳变缩放(scaleTo)与可见白名单目前仅 LSE 导出(§2.5),未上 C++ 接口。渐变动画由消费者基于 scaleTo 步进驱动(如 5 步 easeOutCubic)。
1.6 ICustomEntity(自定义实体,1.10.0)¶
AddActorPacket 直发客户端生成纯视觉实体,不占服务端实体系统。适合 NPC 壳 / 装饰生物 / 盔甲架布景等(无碰撞、无服务端逻辑;交互经 ghost 事件路由,见 1.7)。
struct CustomEntityEquipment {
std::string name; // item name(空 = 空槽位)
int aux{0};
std::string nbt{}; // SNBT 形式, 空 = 无附加 NBT
};
struct CustomEntityConfig {
std::string identifier{"minecraft:armor_stand"}; // 实体类型标识符(短名自动补 minecraft:)
float x{0}, y{64}, z{0}; // 世界坐标
int dimension{0};
float yaw{0}, pitch{0}; // 朝向(度)
std::string nametag{}; // 头顶名字(支持 § 颜色码; 空 = 无)
bool nametagAlwaysShow{false};
float scale{1.0f}; // 客户端有效域 0.0625~10, 自动钳制
int variant{0}; // 变种(皮肤/亚种)
int markVariant{0}; // 二级变种
int colorIndex{0}; // 颜色索引(羊/项圈等染色实体)
std::int64_t flags{0}; // 原始 flags 位掩码(0x01=着火 0x20=隐身 等)
bool invisible{false}; // 隐身便捷开关(发包时合成 0x20)
double viewDistance{64.0}; // <=0 无限制
bool enabled{true};
int pose{0}; // PoseIndex 0..13(0=Standing 3..13 坐姿/睡姿/跳舞等)
CustomEntityEquipment equipment[6]; // 槽位: 0=mainhand 1=offhand 2=head 3=chest 4=legs 5=feet
std::string ridePlayerName{}; // 骑到指定玩家头上(SetActorLinkPacket; 须在线)
int64_t rideEntityId{0}; // 骑到另一自定义实体上(对方库内 id 为载具)
};
| 分类 | 方法 | 签名 | 说明 |
|---|---|---|---|
| 生命周期 | create / createRandom / createWithId | (CustomEntityConfig const&[, int64_t desiredId]) -> int64_t |
同 itemDisplay 语义(随机段 [0x10000000, 0x7FFFFFFF); desiredId<=0 或占用返回 -2) |
| destroy / destroyAll / exists / isIdUsed / getAllIds / get | — | 同 itemDisplay 语义 | |
| 属性 | setIdentifier | (int64_t, std::string const&) -> bool |
换实体类型 |
| setPosition | (int64_t, float, float, float, int) -> bool |
dim<0 仅改坐标 | |
| setRotation | (int64_t, float yaw, float pitch) -> bool |
朝向(度) | |
| setNametag | (int64_t, std::string const&) -> bool |
空串清除 | |
| setScale | (int64_t, float) -> bool |
<=0 拒绝 | |
| setVariant / setMarkVariant / setColorIndex | (int64_t, int) -> bool |
变种/染色 | |
| setFlags / setInvisible / setEnabled / setViewDistance | — | 行为 | |
| setPose | (int64_t, int pose) -> bool |
PoseIndex 0..13 | |
| setEquipmentSlot | (int64_t, int slot, std::string const& name, int aux, std::string const& nbt) -> bool |
六槽装备; name 空清空槽位; nbt 为 SNBT | |
| findNearest | (float x, float y, float z, int dim, double maxDist) -> int64_t |
最近查找; 无匹配 -1 | |
| 缩放 | scaleBy | (int64_t, double factor) -> bool |
相对缩放:现有 scale × factor,结果自动钳制 0.0625~10; factor<=0 返回 false |
| 可见性 | setVisiblePlayers | (int64_t, std::vector<std::string> const&) -> bool |
白名单(按 realName 匹配); 空列表 = 清除限制 |
| setVisiblePlayer / clearVisiblePlayers | (int64_t, std::string const&) / (int64_t) -> bool |
标量版 / 清除(恢复全员可见) | |
| 逐客户端朝向(1.20.0, 仅 C++ 接口; LSE 导出暂未覆盖) | setPlayerRotation | (int64_t, std::string const& playerName, float yaw, float pitch) -> bool |
1.20.0: 覆盖指定玩家收到的该实体朝向(出生包 AddActor 与增量包都按覆盖值下发); 未覆盖的玩家仍用 config 朝向; 玩家离线/未见过该实体返回 false; 变更走轻脏增量(无闪烁), 下一 tick 生效 |
| clearPlayerRotation | (int64_t, std::string const& playerName) -> bool |
清除单个玩家的朝向覆盖 | |
| clearPlayerRotations | (int64_t) -> bool |
清除该实体全部玩家的朝向覆盖 | |
| 诊断 | getDebugInfo | (int64_t) -> std::string |
运行态摘要; 未找到返回 not_found |
| 骑乘 | setRidePlayer | (int64_t, std::string const& playerName) -> bool |
骑到玩家头上(空名清除; 须在线) |
| setRideEntity | (int64_t, int64_t vehicleEntityId) -> bool |
骑到另一自定义实体上; 0 清除 | |
| clearRide | (int64_t) -> bool |
解除骑乘链接 | |
| 动画 | playAnimation | (int64_t, std::string const& animation, std::string const& stopExpression, int durationTicks) -> bool |
播放原版动画(AnimateEntityPacket, 如 animation.humanoid.base_pose); stopExpression 空串 = 常驻; durationTicks>0 到期自动停止 |
属性变更经 tick 脏刷新合并为单次 respawn(无闪烁串台);可见性由库内 Level tick hook 自动同步(同 itemDisplay 模式)。
1.7 GhostInteractEvent(ghost 交互事件,1.12.0)¶
客户端点击/攻击"协议上存在"的实体时,事件经 InventoryTransactionPacket 内嵌的 ItemUseOnActor 事务到达(协议 944 起 InteractPacket 不再承载实体交互);库 hook 收包后将目标 runtimeId 反查回库内 id 并派发,实现可点击 NPC / 全息菜单。
struct GhostInteractEvent {
std::string playerName; // 点击者(realName)
int action{0}; // 1=右键交互 2=左键攻击(见下方说明)
std::string domain; // "entity" / "itemDisplay" / "npc"
int64_t id{-1}; // 对应域的库内 id
bool hasPos{false};
float x{0}, y{0}, z{0};
};
action:1=右键交互 2=左键攻击(ItemUseOnActor 的 Interact/ItemInteract 映射为 1,Attack 映射为 2;与旧 InteractPacket 语义对齐,消费端无需区分来源)- 消费方式二选一(可并存):C++ 推送
setGhostInteractListener;LSE 轮询ghostPollInteractions(取走并清空队列,队列上限 256 条、满时丢最旧) - 恒调 origin,不改变 BDS 对未知 runtimeId 交互包的原版行为;无监听且无人轮询时仅做 runtimeId 段判别,近零开销
1.8 IParticleShape(通用协议层粒子形状,1.14.0)¶
点/线/矩形环/填充面/长方体框/六面/多面体 + 平移/平滑移动/旋转/自旋/缩放/跟随。批量并发发送:逐玩家 vanilla SpawnParticleEffectPacket 经 NetworkSystem 入队,BDS tick flush 自动聚合压缩单 Batch 数据报(与原版粒子广播同路径);采样/视距裁剪/周期重发由库内 tick 自驱动,消费者只管创建与控制。
| 分类 | 方法 | 签名 | 说明 |
|---|---|---|---|
| 创建 | createPoint | (std::string const& owner, int dimId, float x, float y, float z, std::string const& effect, int intervalTicks, int lifetimeTicks) -> int64_t |
单点 |
| createLine | (owner, dimId, x1,y1,z1, x2,y2,z2: float, float step, effect, intervalTicks, lifetimeTicks) -> int64_t |
线段 | |
| createRect | (owner, dimId, float cx, cy, cz, w, h, int axis, float step, effect, intervalTicks, lifetimeTicks) -> int64_t |
矩形环线(axis: 0=XY 1=YZ 2=XZ) | |
| createPlane | 同 createRect | 填充平面网格 | |
| createBox | (owner, dimId, cx, cy, cz, hx, hy, hz: float, step, effect, intervalTicks, lifetimeTicks) -> int64_t |
长方体 12 边线框(h* 为半尺寸) | |
| createBoxFaces | 同 createBox | 六面填充 | |
| createPoly | (owner, dimId, std::vector<float> const& verts, std::vector<std::int32_t> const& edges, float step, effect, intervalTicks, lifetimeTicks) -> int64_t |
多面体(verts=(x,y,z)×N;edges=(i,j)×M;锚点=质心) | |
| 控制 | setPos | (int64_t id, float x, float y, float z) -> bool |
平移锚点(=粒子移动;同时解除跟随) |
| moveBy | (int64_t id, float dx, float dy, float dz) -> bool |
相对平移 | |
| moveTo | (int64_t id, float x, float y, float z, int durationTicks) -> bool |
平滑点对点移动(easeOutCubic;解除跟随;duration<=0 立即到达) | |
| setRot | (int64_t id, float rx, float ry, float rz) -> bool |
欧拉角(度,ZYX 序,绕锚点) | |
| spin | (int64_t id, float sx, float sy, float sz) -> bool |
自旋(度/tick;0,0,0 停止) | |
| setScale | (int64_t id, float scale) -> bool |
各向同性缩放 | |
| follow | (int64_t id, std::string const& playerUuid, float offX, float offY, float offZ) -> bool |
锚点跟随玩家+偏移(每 tick 更新,跨维度) | |
| unfollow | (int64_t id) -> bool |
解除跟随 | |
| 渲染 | setEffect | (int64_t id, std::string const& effect) -> bool |
粒子效果名 |
| setVisiblePlayers | (int64_t id, std::vector<std::string> const& playerUuids) -> bool |
白名单(UUID;空=维度全员) | |
| clearVisiblePlayers | (int64_t id) -> bool |
清除白名单 | |
| setInterval | (int64_t id, int ticks) -> bool |
周期整批重发间隔 | |
| setViewDistance | (int64_t id, int blocks) -> bool |
逐玩家 3D 裁剪;0=不裁剪 | |
| setLifetime | (int64_t id, int ticks) -> bool |
从现在起;0=永久 | |
| 生命周期 | destroy / destroyAll / exists / getAllIds | 同其他域 | — |
| 诊断 | getDebugInfo | (int64_t id) const -> std::string |
运行态摘要(找不到返回 "not_found") |
约定:
intervalTicks= 周期整批重发间隔(粒子瞬态,靠重发维持常驻视觉);lifetimeTicks0 = 永久- 采样点在构造时换算为局部坐标(锚点=形状几何参考点),平移/旋转/缩放作用于局部点,下次发射自动生效
- 与 LSE
particle*(§2.8)共用同一 Manager,id 空间互通
C++ 消费示例:
auto& ps = hologramlib::IHologramLib::getInstance().particleShapes();
// 玩家脚下 16×16 粒子墙(XZ 平面填充, 跟随玩家)
auto id = ps.createPlane("MeowLand", 0, 0, 64, 0, 16, 16, 2, 1.0f,
"minecraft:endrod", 4, 0);
if (id > 0) {
ps.follow(id, playerUuid, 0, -2, 0);
ps.setVisiblePlayers(id, {playerUuid}); // 仅本人可见
}
1.9 IPlayerNpc(假玩家 NPC,1.16.0)¶
NPC 皮肤已可正常渲染(26.40.2 修复)。本节全部接口可用且数据链路完整:皮肤注册(PNG / 在线采集 / 目录导入)、
getSkinBlob/registerSkinFromBlob导出恢复、NPC 创建/移动/朝向/缩放/视距/显隐、点击交互均正常。此前的症状(客户端不渲染皮肤、外观回退默认模型)根因在 PlayerList 皮肤条目的Id/FullId为空或残留原玩家缓存键,已修复。
纯协议假玩家:PlayerListPacket(Add, 携带皮肤) → AddPlayerPacket → [20 tick] PlayerListPacket(Remove)(假玩家短暂出现在 Tab 后移除,实体因皮肤已缓存持续渲染)。不占服务端实体系统;点击交互经 ghost 管线 domain="npc" 派发(§1.7)。
皮肤注册表全局共享:PNG 文件注册(GDI+ 解码 64×64/128×128,自定义 geometry/armSize)、目录批量导入(一个子文件夹 = PNG + 可选 .json 几何模型,缺省 = 标准玩家模型)或从在线玩家采集(Player::mSkin → SerializedSkinImpl 全字段拷贝:贴图/披风/动画贴图/几何/Persona 部件/染色 → 以 skinId 注册运行时快照,玩家之后换肤不影响)。解码/采集一次,多 NPC 复用零重复开销。库不落盘——持久化由消费方负责:注册/采集/导入成功后 getSkinBlob 导出全字段二进制快照存到自己的目录,重启时 registerSkinFromBlob 恢复,不依赖源 PNG/玩家在线。
| 分类 | 方法 | 签名 | 说明 |
|---|---|---|---|
| 皮肤 | registerSkin | (PlayerNpcSkin const& skin) -> bool |
PNG 注册(skinId 空 = 文件名; 重复覆盖) |
| importSkins | (std::string const& dirPath) -> int |
目录批量导入(skinId = 子文件夹名; PNG 必需 + .json 几何可选; 返回导入数量, 目录无效 -1) |
|
| captureSkin | (std::string const& skinId, std::string const& playerName) -> bool |
从在线玩家采集(不在线返回 false; 重复覆盖) | |
| getSkinBlob | (std::string const& skinId, std::string& out) const -> bool |
全字段序列化导出(消费方持久化用; 未注册 false) | |
| registerSkinFromBlob | (std::string const& blob) -> bool |
blob 反序列化注册(与 getSkinBlob 配对; 格式非法 false) | |
| hasSkin / unregisterSkin / getSkinIds | — | unregister 有 NPC 引用时拒绝 | |
| 生命周期 | create / createRandom / createWithId | (PlayerNpcConfig const&) -> int64_t |
失败: -1 常规 / -2 id 占用 / -3 皮肤未注册; 持久化由消费者负责 |
| destroy / destroyAll / exists / get / isIdUsed / getAllIds | — | 同其他域 | |
| 属性 | setPosition | (int64_t id, float x, float y, float z, int dim) -> bool |
dim<0 仅改坐标 |
| setRotation / setNametag / setSkin / setViewDistance / setEnabled | — | setSkin 未注册返回 false; 变更经 tick 脏刷新合并为单次 respawn | |
| 朝向(1.20.0, 仅 C++ 接口; LSE 导出暂未覆盖) | setRotationLight | (int64_t id, float yaw) -> bool |
轻量朝向: 只发 MoveActorAbsolute 增量(不重建实体/不重发皮肤, 无闪烁), 适合每 tick 跟踪 |
| setPlayerRotation | (int64_t id, std::string const& playerName, float yaw) -> bool |
覆盖指定玩家收到的朝向(出生包与增量包都按覆盖值); 未覆盖玩家用 config 朝向 | |
| clearPlayerRotation / clearPlayerRotations | (int64_t[, std::string const&]) -> bool |
清除单个 / 全部玩家的朝向覆盖 | |
| 可见性 | setVisiblePlayers / clearVisiblePlayers / setVisiblePlayer | — | 玩家名白名单(空 = 全员) |
| 轻量位置(26.40.8) | setPositionLight | (int64_t id, float x, float y, float z, int dim) -> bool |
只发 MoveActorAbsolute 增量(不重建实体/不重发皮肤, 无闪烁); 与 setRotationLight 同一条轻脏通道, 同一 tick 合并一条包; dim 与当前维度不同返回 false(跨维度仍走 setPosition 重建) |
| 皮肤注入(26.40.8; 仅 C++) | injectSkin | (std::string const& viewerName, std::string const& targetName, std::string const& skinId) -> bool |
用 target 自己的 UUID / uniqueId 发 PlayerList(Add) 就地更新皮肤条目(真实 xuid + trust 三态 + 皮肤对象 OverridesPlayerAppearance=true——覆盖客户端已装备皮肤的前提); viewerName 空串 = 所有在线玩家(含 target 本人); 条目按注册表原样下发; 不自动重发(丢帧由消费方再调一次) |
| injectSkinAll | (std::string const& targetName, std::string const& skinId) -> bool |
全员版(等价 injectSkin("", targetName, skinId)) |
|
| 动画(26.40.8; 仅 C++) | playAnimation | (int64_t id, std::string const& animation, std::string const& stopExpression, int durationTicks) -> bool |
对"已见过该 NPC 的玩家"发 AnimateEntityPacket(controller 名按 NPC id 内自动唯一化, 资源包无需同名控制器); stopExpression 空 = 常驻, query.any_animation = 立刻停, durationTicks>0 到期自动补停止包; 动画名需客户端可解析(原版或随资源包下发); 无观察者返回 false |
| playAnimationTo | (int64_t id, std::string const& playerName, std::string const& animation, std::string const& stopExpression, int durationTicks) -> bool |
同上, 只发给该玩家(补发用; 未见过该 NPC 返回 false) | |
| 出生回调(26.40.8; 仅 C++) | setEntitySpawnCallback | (std::function<void(int64_t id, std::string const& playerName)>) -> void |
NPC 对某玩家出生完成回调(供补发动画/状态; EntitySpawnCallback 语义); 与 customentity 域同款队列(+2 tick 发出) |
| 诊断 | getDebugInfo | (int64_t id) const -> std::string |
运行态摘要 |
struct PlayerNpcSkin {
std::string pngPath; // PNG 路径(64/128)
std::string skinId; // 空 = 用文件名
std::string geometry{"geometry.humanoid.custom"}; // 模型(resourcePatch)
std::string armSize{"wide"}; // "wide" / "slim"
std::string geometryData; // 1.18.0 完整几何 JSON(可选, 提供时启用自定义模型)
};
struct PlayerNpcConfig {
std::string name{"NPC"}; // 显示名(nametag 同步)
std::string skinId{"default"}; // 必须已注册, 否则创建失败 -3
float x{0}, y{64}, z{0};
int dimension{0};
float yaw{0};
double viewDistance{96.0}; // <=0 无限制; 滞回: 退出需超 viewDistance+4
bool enabled{true};
};
1.10 ITradeMenu(村民交易菜单,1.21.0;1.22.0 起纯展示;1.24.0 起可纯协议真结算)¶
IHologramLib::tradeMenus()。手写包打开交易界面:界面完全由本库构造的 UpdateTradePacket 驱动,载荷与 BDS 26.40 原生村民交易逐字节一致(tests/check-trade-packet.bat 整包对拍、check-trade-offers.bat 只对 Offers 段)。交易界面只靠这个包打开——原生流程里没有 ContainerType=Trade 的 ContainerOpen。
默认不读任何点击;开了
settleLocally才读交易槽上的物品动作与带自建配方 id 的CraftRecipe动作。真结算(1.24.0):
TradeMenuSpec::settleLocally = true(且usePacketOffers = true)时,界面仍是自建UpdateTrade(服务端不放交易表),但库接住客户端的付费放置 / 取回 / 成交请求并自己落地:从真实背包扣下付费暂存、成交时校验账本并按条目把产物写进背包、关界面把没花掉的付费退回;"点一下拿起"那份也由库托管(光标)。结果通过TradeSettlementEvent回传(C++ 监听器 / LSEtradePollSettlements)。判定逻辑独立在src/trade/TradeSettlementLogic.h,离线自检tests/check-trade-settlement.bat(43 项)。已知限制:客户端要求服务端在应答里回带交易槽的槽位更正,而该段在本协议版本与协议库线格式不一致(照协议库手写会打崩客户端)—— 放料被服务端受理,但客户端界面可能撤回它。要稳定成交用usePacketOffers=false(BDS 结算)或容器 UI。实测边界:触屏在纯协议层到不了成交(付费拖不进交易槽)——触屏用真实交易表路径或容器 UI;客户端买不起就不发包;账本按"类型 + 数量"扣(不细分交易槽位);
numCrafts不展开(一次成交发一份产物)。
两条路径(TradeMenuSpec::usePacketOffers):
true+false(默认,纯展示):只发自建UpdateTrade,服务端不放交易表。玩家往付费槽放东西的请求会被 BDS 拒掉、物品弹回,界面停在"不可成交"(与参考实现 GMLIBChestUI同路)。true+true(1.24.0,纯协议真结算):见上方说明。false(真实交易表):给载体实体装真实交易表并调 BDS 的openTrading,玩家是真的在交易(物品真的消耗;触屏可走完整流程)。carrierUniqueIdOverride != 0时直接以该真实实体为交易对象;这条路上settleLocally无效。
载体实体:交易界面需要 EntityUniqueId,因此打开时会在玩家身后 5 格生成一个隐身、仅该玩家可见的假村民(minecraft:villager_v2),关闭即删除。载体是异步送达客户端的,所以 UpdateTrade 与经验条元数据都在几 tick 后发送(背靠背发时客户端还不认识该实体,界面绑不上去)。经验条不在包内,由载体实体的 TradeTier / MaxTradeTier(恒 4)/ TradeExperience 元数据驱动。
| 分类 | 方法 | 签名 | 说明 |
|---|---|---|---|
| 生命周期 | open | (std::string const& playerName, TradeMenuSpec const&) -> int64_t |
同一玩家重复调用先关旧菜单;失败 -1 |
| update / close / closeAll / isOpen / getAllIds | — | update 重发交易表(真实表路径会重开界面) |
|
| 内容(1.22.0 追加) | addOffer | (int64_t menuId, TradeMenuOffer const&) -> bool |
追加一条并就地重发交易表(不重开界面、不等延迟) |
| setTier | (int64_t menuId, int tier, int experience) -> bool |
改显示栏值 / 经验条,同样就地重发 | |
| 真结算(1.24.0) | setSettleLocally / isSettleLocally | (int64_t, bool) -> bool / (int64_t) const -> bool |
打开后随时切换真结算(初值在 TradeMenuSpec::settleLocally) |
| addSettlementListener / removeSettlementListener | (std::function<void(TradeSettlementEvent const&)>) -> uint64_t / (uint64_t) -> bool |
成交 / 失败回传(C++ 侧) | |
| pollSettlements | () -> std::vector<std::string> |
LSE 轮询(取走并清空); 条目 "player=Steve menuId=3 offer=1 ok=1 reason=" |
struct TradeMenuOffer {
TradeMenuItem buyA, buyB, sell; // buyB.type 空 = 无第二付费项
int tier{kTradeTierNovice}; // 1 基: 1=新手 … 5=大师;高于 spec.tier 即显示为未解锁
int maxUses{16};
int traderExp{0};
bool locked{false}; // true = 无论 spec.tier 多高都显示为未解锁
};
struct TradeMenuSpec {
std::string tradeType{"entity.villager.butcher"}; // 自由翻译键
int tier{kTradeTierNovice}; // 1 基, 超出夹紧
int experience{0}; // 经验条当前经验
bool usePacketOffers{true}; // 见上: 纯协议层展示 / 真实交易表
std::int64_t carrierUniqueIdOverride{0}; // 非 0 = 用该真实实体(不放自建 offers)
std::vector<TradeMenuOffer> offers;
std::string carrierIdentifier{"minecraft:villager_v2"}; // 或 "minecraft:wandering_trader"
};
// 载体实体类型(TradeMenuSpec 尾部追加): 默认村民; 流浪商人 = "minecraft:wandering_trader"
// (界面外观/头图随类型变化; 两条路径都适用, 真实交易表路径装表方式相同)
spec.carrierIdentifier = "minecraft:wandering_trader";
1.11 INpcDialogue(NPC 对话框,1.21.0)¶
IHologramLib::npcDialogs()。NpcDialoguePacket(场景名 + 正文 + NPC 名 + 按钮 JSON)下发,NpcRequestPacket 回传(点了第几个按钮 / 玩家关闭)。
- 按钮表完全由服务端逐玩家生成:按权限 / tag / 计分板 / 任务状态决定按钮是否出现,就是在调用本接口前自行算好
buttons,不需要客户端配合。 - 多层级对话 = 点击回传带回
sceneName+buttonIndex/actionId,调用方据此发送下一层。 - 载体实体是纯协议合成的
minecraft:npc(不进 BDS 实体系统),位置在世界下方y=-66——客户端看不到实体,但界面里的头像照常渲染(改用隐形标志位反而会让头像一起消失)。只发给该玩家;点按钮 / 关闭 / 离线时发RemoveActor删掉。 NpcDialogSpec::rawActionJson非空时原样作为mActionJSON下发(原版按钮结构无公开文档,用于在游戏内实测字段格式);carrierIdentifier必须是 NPC 家族,换成村民等原版实体不行(实测要求)。- 聊天框内显示的头像/模型可自定义(26.40.8):
NpcDialogSpec::avatarSkinVariant(NPC 内置皮肤变体 0..59,即NpcData.skin_list的 variant 值)—— 写进载体 ActorData 的SkinId(104),界面里的头像/模型换内置皮肤;-1(默认)不加该项(旧行为逐字节一致)。NpcDialogSpec::avatarViewSpec—— 在载体上叠一层 IViewOverride 语义(spec 语法同viewEntity):type=换载体类型(实测: 僵尸/鸡这类非 NPC 家族照常弹界面)、name=换界面标题/交互文字。skin=分支已移除(未发布批次内的收缩, 明记): 它会把载体换成"玩家模型"(PlayerList+AddPlayer), 实测客户端在该载体上点进菜单会关界面, 当前不可用;"让客户端认它是玩家"的路线由消费方自建(playerNpcs()+npcUniqueIdOverride)。要"整只模型完全自定义"也可用npcUniqueIdOverride路线:视图域 / 自定义实体生成展示实体、对话挂在它 id 上(1.27.0 的EntityView::skinId同-id 换模型也可用于那只展示实体)。LSE 侧就地修改用npcDialogSetAvatar。
struct NpcDialogButton {
std::string label;
std::vector<std::string> commands; // 展示在按钮上; 合成 NPC 不由 BDS 代跑, 命令回传给调用方
std::string actionId; // 服务器侧标识, 点击回传原样带回(对接对话树/任务)
int mode{0}; // 0=普通按钮 1=关闭 2=打开
};
struct NpcDialogClickEvent {
std::string playerName;
int64_t dialogId{-1};
std::string sceneName;
int buttonIndex{-1}; // -1 = 非按钮事件
std::string actionId;
bool closed{false};
std::vector<std::string> commands; // 被点击按钮上挂的命令(按 buttonIndex 回填)
};
1.12 IContainerMenu(虚拟容器 / 列表界面,1.21.0)¶
IHologramLib::containerMenus()。方案逐条复刻参考实现 GMLIB 的 ChestUI:
UpdateBlockPacket在玩家头上摆一个客户端侧箱子方块(只发给该玩家,服务端世界里没有它)BlockActorDataPacket摆该方块的方块实体 NBT:id=Chest/CustomName=标题/x,y,z/pairx,pairz,pairlead(大箱子)/Items=[条目物品]—— 条目物品走这份 NBT,不是逐格InventorySlot- 等 10 tick 发
ContainerOpen(ContainerType::Container(0)+ 方块坐标 + 目标实体-1) 按槽动态刷新用setItem(menuId, slot, item):只发一条InventorySlotPacket(真实箱子同步内容用的同一种包), 客户端就地换掉那一格——不重发方块实体、不重发ContainerOpen,无延迟、无闪烁;item.type为空即清空该槽。 改整页内容才用update(menuId, spec)。
翻页/进子菜单/返回用 update(menuId, spec): 复用同一个载体方块, 不拆界面、不重摆方块、不等待,
只重发方块实体 NBT(新条目/标题)+ ContainerOpen 让客户端重读。走 open() 重开每次都要付一次打开延迟
(它会先关旧菜单、恢复真方块, 再重摆、再等 openDelayTicks)。
- 玩家点击 → 客户端发
ItemStackRequest(147)→ 槽位命中本容器 → 回调(槽位号 = 条目下标)。命中容器的动作只回传、不拦:服务端没有这个容器,放行后 BDS 自己就会失败、客户端把预测撤回(物品闪一下回原位,无错误提示),回调照常收到——与参考实现 GMLIB 一致。实测教训: 若库自己代答一条ItemStackNetResult3(请求不允许),客户端会弹错误提示 - 关闭 →
ContainerClosePacket→ 把真方块改回去 → 回调closed = true
服务端根本没有这个容器:物品只是"摆在那里",玩家拿走/移动都不会真的改变任何东西(天然只读)。适合任务列表、成就列表、商店预览这类"只展示 + 点击回调"的界面。
大小容器:rows = 3 → 单箱子 27 格(方块实体不带配对键);rows = 6 → 大箱子 54 格(相邻两个箱子方块 + pairx/pairz/pairlead 配对,前 27 格进 lead 半、后 27 格进副半,各半的 Slot 都是 0..26)。方块位置取玩家脚上方 5 格并优先挑空气位(免得盖掉真实方块),关闭时按该位置真方块的网络 id 改回。容器 id 用 101..199 显示区间,与 BDS 真实容器的动态 id(1..100)不冲突。
struct ContainerMenuSpec {
int rows{3}; // 3 = 小箱子 27 格, 6 = 大箱子 54 格
std::vector<ContainerMenuItem> items; // 下标即槽位号(大箱子 0..53)
bool useMinecart{false}; // false(默认) = GMLIB 方块方案; true = 旧矿车路径(客户端不认, 仅对比)
std::string title{"虚拟容器"}; // 箱子界面标题(方块实体的 CustomName)
int openDelayTicks{7}; // 摆方块+方块实体后等多少 tick 再发 ContainerOpen
// (GMLIB: 10 再等 4 补格 ≈700ms; 本库条目在方块实体 NBT 里
// 只需 7 ≈350ms。实测 7 能开、6 开不了; 只影响"打开"那次)
};
struct ContainerClickEvent {
std::string playerName;
int64_t menuId{-1};
int slot{-1}; // 被点击的槽位(= items 的下标)
bool closed{false}; // true = 界面被关闭(此时 slot = -1)
};
1.13 感知域 / 千人千面 / 逐玩家变量全息(1.21.0)¶
感知域 IHologramLib::playerSensing():挂钩 PlayerAuthInputPacket(每玩家每 tick)逐包捕获 InputMode。
inputDeviceOf(playerName) 返回 ClientInputDevice(Unknown/KeyboardMouse/Touch/Gamepad/MotionController),
isTouch(playerName) 便捷判断。玩家离线即清;未上报 = Unknown。典型用途:按设备路由 UI
(触屏走容器列表,键鼠/手柄走交易界面 —— 触屏在纯协议层交易里走不到成交是实测结论)。
千人千面 ICustomEntity 追加:setPlayerNametag(id, player, text)(空 = 清覆盖)、
setPlayerScale(id, player, scale)(<=0 = 清覆盖)、setPlayerEquipmentSlot(id, player, slot, name, aux, nbt)
(slot 0..5,name 空 = 该槽回退 config,即时单发无闪烁)、clearPlayerAppearance(id, player)。
机制与 setPlayerRotation 相同:覆盖按玩家 uuid 保存,出生包与增量包都按覆盖值下发,未覆盖玩家用 config 值。
逐玩家变量全息:全息文本含 {var} 占位符且绘制目标不是单玩家时,库自动切换为逐观看者形状
——每个在线玩家收到按自己名字解析的独立文本({player} 对每人正确);离线/离开维度的观看者形状
就地销毁。无变量时仍是共享单形状(性能不变)。文本动画(滚动/弹跳)由库内 ServerLevelTickEvent
监听自驱,无需消费方调用 tick。
曾短暂实现过逐客户端音效与短命飘字,经讨论已整体移除(见 README 更新日志)。
1.14 IFakeInventory(背包虚容器,1.23.0)¶
IHologramLib::fakeInventories()。协议层改写客户端看到的玩家背包:一条 InventoryContentPacket(49)
(ContainerId = Inventory(0)、0..35 号槽位)把整份内容换成调用方给的那一份;单格改动走 InventorySlotPacket(50)。
服务端背包一个字都不动 —— 物品是"看起来有"。
物品序列化交给 BDS:物品描述符的 User Data 用的是 u16 名字长度 / 4 字节整数的那套 NBT 方言, 与 sculk
CompoundTag的 varint 方言不同;手写会写错(实测把客户端卡死)。所以本域用 BDS 的InventoryContentPacket/InventorySlotPacket构造包体(容器名字节 0 + storage 空描述符, 与抓包一致)。
参考实现 GMLIB 的 ChestUI 填玩家物品栏用的就是同一条路(它逐格写 InventorySlot,容器 id 同样是
Inventory(0) + InventoryContainer(29));本域的整份下发用一条 InventoryContent 搞定。
功能项与虚拟容器一致:玩家点自己背包里的伪造物品 → 回调报槽位号,物品不会真的被拿走 —— 客户端按伪造内容发出请求,服务端真实槽位对不上 → BDS 判失败 → 客户端把预测撤回(与虚拟容器同一套 表现,库不需要代答);库随后把那一格重发一次,让伪造内容不被这次回滚冲掉。
与交易菜单 / 虚拟容器共存:打开交易界面或箱子界面时 BDS 会重发玩家背包内容(伪造内容被覆盖),
所以本域默认按 refreshIntervalTicks 周期重发(默认 20 tick = 1s),也可随时 refresh()。
界面上看到的背包区域因此始终是伪造内容,点击照常回调。
| 分类 | 方法 | 签名 | 说明 |
|---|---|---|---|
| 下发 | apply | (std::string const& playerName, FakeInventorySpec const&) -> bool |
整份伪造内容(覆盖该玩家此前的); 玩家不在线 false |
| setSlot | (std::string const& playerName, int slot, ContainerMenuItem const&) -> bool |
单格改动(一条 InventorySlot,无延迟); 未 apply 过返回 false |
|
| refresh | (std::string const& playerName) -> bool |
立即重发当前伪造内容 | |
| clear / clearAll | (std::string const& playerName) -> bool / () -> void |
撤销伪造 + 停周期重发,并把真实背包重发一遍 | |
| 查询 | isActive / getActivePlayers | (std::string const&) -> bool / () -> std::vector<std::string> |
|
| 回传 | addClickListener / removeClickListener | (std::function<void(FakeInventoryClickEvent const&)>) -> uint64_t |
多播;点伪造成非空的格子才回调 |
inline constexpr int kFakeInventorySlots = 36; // 0..8 快捷栏, 9..35 主背包
struct FakeInventorySpec {
std::vector<ContainerMenuItem> items; // 下标即槽位; type 空 = 该格留空
int refreshIntervalTicks{20}; // 0 = 不周期重发
};
struct FakeInventoryClickEvent {
std::string playerName;
int slot{-1}; // 被点击的伪造槽位
};
边界(实测前先写清楚,免得误用):
- 伪造只影响客户端显示。玩家"真正能用/能吃"的仍是服务端真实物品。
- 若某格真实物品与伪造物品恰好一致,那一次操作会被服务端当真执行 —— 想让某格纯展示,别把它伪造成与真实物品相同的东西。
- 该格真实物品因为别的原因改变(捡东西 / 用物品 / 别的插件改背包)时,周期重发会把伪造内容重新盖回去。
- 护甲(
ArmorContainer6)/ 副手(OffhandContainer34)不在本域范围内,它们各有独立的容器枚举。
1.15 ISulfurDisplay(硫磺立方体展示,1.23.0)¶
IHologramLib::sulfurDisplays()。第二种摆放方式(与 IItemDisplay 的"狐狸 + 物品"并列):
生成一只 minecraft:sulfur_cube,把要展示的方块/物品装进它的主手 —— 行为包里立方体就是靠
slot.weapon.mainhand + behavior.equip_item 拿着"吞下去"的方块(客户端按装备渲染)。
外观档位 minecraft:sulfur_cube_archetype 是行为包里 client_sync: true 的 enum 属性,走手写的
ChangeMobProperty(182) 下发(26.40 线格式: Actor Id(int64 压缩) → Property Name(string) → Bool →
String → Int32(压缩) → Float)。AddActor 的 PropertySyncData 只装得下按索引的 int/float,装不下 enum,
所以属性必须在实体已被客户端认识之后发 —— 库里在 spawn 后自动补发,respawn 后重放。
本域完全委托给 ICustomEntity(立方体就是一只自定义实体),缩放 / 视距 / 可见玩家白名单 / 逐客户端
朝向等能力直接继承;新增的协议内容只有 ChangeMobProperty。
| 分类 | 方法 | 签名 | 说明 |
|---|---|---|---|
| 生成 | create | (SulfurDisplaySpec const&) -> int64_t |
返回展示 id(<0 = 失败) |
| 吞方块 | setBlock | (int64_t id, ContainerMenuItem const&) -> bool |
换"吞下去"的方块(改主手装备 → 重建一次实体) |
| 外观 | setArchetype | (int64_t id, std::string const&) -> bool |
换外观档位(ChangeMobProperty;空串 = 清掉属性;不重建实体) |
| 隐身 | setInvisible | (int64_t id, bool) -> bool |
隐身开关(spec 上的 invisible 参数同义) |
| setScale | (int64_t id, float) -> bool |
缩放(0.0625~10) | |
| 生命周期 | destroy / destroyAll / exists / getAllIds / entityIdOf | — | entityIdOf 返回背后的自定义实体 id(诊断用) |
struct SulfurDisplaySpec {
float x{0}, y{64}, z{0};
int dim{0};
ContainerMenuItem block; // **吞下去的方块**(核心参数; 走主手装备)
std::string archetype{"regular"}; // none/regular/bouncy/sticky/hot/explosive/light/...
int variant{2}; // 1=小 / 2=中(含方块那一档)
bool invisible{true}; // **隐身参数**(**默认开**: 实测隐身时方块照常渲染 → 只留内容)
float scale{1.0f};
double viewDistance{0.0};
std::vector<std::string> visiblePlayers; // 空 = 全员可见
};
隐身默认开(实测):26.40 本机客户端上确认 —— 立方体隐身时,主手里"吞下去的方块"照常渲染,
所以 invisible 默认 true,默认观感就是"一个方块浮在那里"(立方体本体不可见);要看立方体本体就设 false。
(NPC 载体的头像是会被隐身一起抹掉的,两个实体行为不同,不要套用。)
"吞生物"没有做:协议层实体没有 AI,真正吞并/消化做不到;试过让另一个实体骑在立方体上做近似, 骑乘位置与碰撞都调不出"被吞进去"的观感(实测不成立),已整体移除 —— 本域只做方块与隐身。
另外: ICustomEntity::setMobProperty(id, name, value) / clearMobProperties(id) 同批开放 ——
凡是 client_sync 的实体属性都能按名下发(硫磺立方体的档位就是这么实现的)。
1.16 IViewOverride(客户端视图覆盖,1.25.0)¶
1.29.0(26.40.10,26.51 线随
26.51.3)补齐:AddItemActor也纳入拦截。 掉落物(item 实体)的出生包走的是AddItemActor(id 15) 而不是AddActor(id 13),此前只拦后者 → "把掉落物换类型/隐藏"只做一半: 设覆盖那一刻能把已经在看的玩家换掉,但引擎随后自己发的AddItemActor会把物品图标又塞回客户端 (2026-10-16 抓包实证:RemoveActor+AddActor(minecraft:cod)之后 13ms 就来一条同 uniqueId 的AddItemActor)。现在: *hidden = true→ 丢掉该掉落物的AddItemActor(客户端从头到尾不会看到它); *identifier = "<生物>"→ 丢掉原包 + 用同一个 runtimeId/uniqueId 手写一只AddActor(属性用health 20 / scale 1的已验证配方 —— 不照抄 ItemActor 的maxHealth = 0,那是另一处会渲染异常的点); * 探测能力: LSE 侧新增viewItemSpawnHooked() -> bool(老库没有这个名字,脚本ll.hasExported判 false)。对 mob 的行为一字未改;对 item 实体是修复(此前半残)。
auto& view = lib.viewOverrides();
// 实体: 换类型 / 换皮肤(我们的模型)/ 换名字牌 / 对该玩家隐藏(playerName 空串 = 所有玩家)
hologramlib::EntityView cow;
cow.identifier = "minecraft:cow"; // 短名会自动补 minecraft: 前缀
cow.hasNametag = true;
cow.nametag = "§c奶牛"; // 出现 hasNametag 才改名字牌(值给空串 = 清空名字)
cow.nametagAlwaysShow = true;
view.overrideEntity("Steve", uniqueId, cow);
hologramlib::EntityView gone;
gone.hidden = true; // 让 Steve 看不到这只实体(阻断它的出生与更新包)
view.overrideEntity("Steve", uniqueId, gone);
// 非玩家实体: 换成"我们的模型"(已注册皮肤, 含自定义几何)—— **同一个 runtimeId/uniqueId**
hologramlib::EntityView skinned;
skinned.skinId = "ms_demo"; // IPlayerNpc 注册表里的皮肤 id
skinned.scale = 0.8f; // 可选: 模型缩放(客户端碰撞箱等比; 0 = 不缩放)
view.overrideEntity("", mobUniqueId, skinned);
// 客户端看到的是我们的模型; 服务端那边还是那只生物 —— 点它/打它/瞄准都是它本人。
// 撤销: view.clearEntity("", mobUniqueId);(库会重发真实生物的出生包, 立刻恢复原样)
// 玩家实体专用的两种"千人千面"(由 Steve 一个人看到)
hologramlib::EntityView skin;
skin.asPlayer = "Alex"; // 让 Steve 看到这只玩家变成 Alex 的样子(皮肤)
view.overrideEntity("Steve", playerUniqueId, skin);
hologramlib::EntityView asMob;
asMob.identifier = "minecraft:cow"; // 玩家模型渲染不了生物 → 库走"替换": 同一 runtimeId/uniqueId 发一只牛
view.overrideEntity("Steve", playerUniqueId, asMob);
// 方块: 把 (100,64,100) 在他客户端显示成钻石块
hologramlib::BlockView block;
block.type = "minecraft:diamond_block";
view.overrideBlock("Steve", 100, 64, 100, block);
view.clearEntity("Steve", uniqueId); // 撤销
view.clearBlock("Steve", 100, 64, 100);
view.clearAll("Steve"); // 撤销这名玩家的全部覆盖(空串 = 全部玩家)
std::string s = view.describeFor("Steve"); // "entities=1 blocks=2"
| 方法 | 返回值 | 说明 |
|---|---|---|
overrideEntity(playerName, entityUniqueId, view) |
bool | false = 玩家不在线。实体还没出生也能先设(库在它出生时套用)。五项(identifier / asPlayer / skinId / hidden / hasNametag)都没设 = 当作撤销 |
overrideBlock(playerName, x, y, z, view) |
bool | false = 玩家不在线 / 类型名不在方块注册表里。type 给空串 = 撤销 |
clearEntity(playerName, entityUniqueId) |
bool | 撤销单个实体(返回是否原来有覆盖) |
clearBlock(playerName, x, y, z) |
bool | 撤销单个方块,并把真实方块推给该玩家 |
clearAll(playerName) |
void | 清掉该玩家(空串 = 全部)的覆盖表 |
describeFor(playerName) |
string | 该玩家当前生效的覆盖条数摘要(含全局那一份);带拦截钩子计数与 sendFail |
startControl(controllerName, targetName, follow) |
bool | 让 controller 的移动/转向输入驱动 target 的真实移动(服务端位移); follow=true 时 controller 被锁到 target 上一起走(附身观感) |
stopControl(controllerName) |
bool | 停止操控(任一方下线也会自动停) |
机制(只拦不发):库拦截服务端本来要发给这名玩家的包,但只决定放行 / 丢弃 ——
从不修改引擎包字段。需要"改"的内容一律:丢掉原包 + 库自己手写协议包补发
(sculk 构造 → 回读校验 → 原始字节发送)。换类型 = 丢掉原出生包、用同一 runtimeId/uniqueId
自己发一只(其余字段照抄原包); 名字牌 = 原包照发、库自己的 SetActorData 紧随其后压过去;
方块覆盖 = 丢掉原 UpdateBlock、库自己发一条带覆盖网络 id 的。服务端世界 / 存档 / 碰撞 /
其他插件看到的都是真实内容。拦截挂在 NetworkSystem::send / sendToMultiple(BDS 按收件人发结构化包的
汇合点;实体出生包不走 LoopbackPacketSender,那是原先失效的原因),外加一层心跳:服务器每 tick
自查,实体"重新进入视野"、玩家换区块时把覆盖重新推一遍 —— 拦截漏掉的场合由它兜住。
换"我们的模型"(skinId, 1.27.0; 只对非玩家实体):库吃掉这只实体的出生包, 用同一个
runtimeId/uniqueId 发 PlayerList(Add)(携带该皮肤)+ AddPlayer —— 客户端把它当玩家实体渲染
(自定义几何/贴图都走玩家皮肤通道, 不需要资源包); 因为 id 没变, 服务端仍认它是那只生物,
点它 / 打它 / 瞄准都是它本人(伤害、掉落、其他插件看到的全是真身)。配套两件事: ①该实体的
MoveActor*(18/111) 对这些观看者被吃掉, 位置/朝向由库每 tick 用 MoveActorAbsolute 推(与
IPlayerNpc::setPositionLight 同一条轻推通道, 只在动了才发); ②PlayerList 条目 ~20 tick 后摘掉
(不会一直挂在 Tab 里, 实体保留)。撤销 = clearEntity(重发真实生物的出生包)。
皮肤必须先在 IPlayerNpc 注册表里(registerSkin / captureSkin / registerSkinFromBlob)。
自己不看自己:覆盖目标是玩家时,该玩家本人的客户端不参与覆盖——不发/不吃关于他自己实体的包
(RemoveActor / 同 runtimeId 的 AddActor / 位移打回本地玩家会让客户端错乱甚至卡死,2026-10-09 实证)。
例外:换肤类(asPlayer)含本人——PlayerList(Add) 发给本人是"自己看自己被换肤"的正路
(第三人称/第一人称自视都变;同 IPlayerNpc::injectSkin 的约定)。
逐玩家隔离与幂等:覆盖表按玩家名 + 一份全局表;sendToClients / 广播这类"同一包发给多人"的路径
会展开成逐收件人判定 —— 只为让"放行 / 丢弃"逐人生效;因为从不改字段,不存在"后一个收件人看到前一个
覆盖"的串味。同一条包被多条发包路径各判一次,结果与判一次相同。库内没有任何覆盖时,钩子不做任何解析
就直接放行。
玩家实体(1.25.0 支持):
- asPlayer = "<在线玩家名>" → 给该观看者重发一条 PlayerList(Add)(用该实体自己的 UUID + 源玩家的皮肤)
—— 与原版"皮肤更新"同机制,只发给该观看者;撤销 = 用他自己的皮肤再发一次,立刻恢复。
对任何实体只有玩家实体有效(皮肤按 UUID 认)。
- identifier = "<生物>" → 玩家模型渲染不了生物,所以库走替换(不是代理):吃掉他的 AddPlayer,用同一个 runtimeId/uniqueId 发一只 AddActor;他的 MovePlayer 对这名观看者也吃掉,位置/朝向由库按他的输入包用 MoveActorAbsolute 推。id 没变 → 服务端仍认他是那个真玩家,打他就是打他本人。 撤销时把替代实体移走,并把真身补发回该观看者:RemoveActor → 用他自己身份的 PlayerList(Add)(皮肤走玩家皮肤通道)→ AddPlayer(位置/朝向取引擎当前值、游戏模式按他输入包里的 PlayMode)。所以撤销是立刻生效的,不用重进 / 换维度。皮肤数据读不到时库不动客户端(宁可先维持现状,也不做"先删后补不回来")。
26.40 做不到的:逐玩家的缩放 / 发光 / 隐身没进 API —— 元数据表里没有 scale,
也没有 glowing / invisible 旗标(基岩版隐身是效果,走 MobEffectPacket)。
自定义实体的逐客户端缩放 / 装备槽请用 ICustomEntity 的 per-viewer 覆盖(1.21.0)。
限制(写清楚免得误用):
- 换类型只对非玩家生物生效(玩家出生包 AddPlayer 里没有类型字段;玩家实体的名字牌 / 隐藏照常可覆盖)。
- 换类型 / 隐藏 / 撤销全部立刻生效:库里对这只实体做一次"移除 + 按目标状态重发一只"(同一个
runtimeId 与 uniqueId,所以服务端后续的移动/状态更新照常作用在它身上)。代价是客户端那只实体
会被重建 —— 只随出生包来过一次的细节(变体、装备初始态)要等服务端下次同步才回来。
- 撤销时离得太远(> 64 格)或已消失的实体不重建(客户端上本来也没有它)。玩家实体走"补发真身"那条路(见上);补发出来的真身手持物品是空的、能力是默认档,引擎随后自己会纠正(切换物品会推 MobEquipment,其余状态变更会推 SetActorData)。
- 方块覆盖在区块重发后由库自动补发。
- 覆盖只存在内存里(重启服务器即失效);按玩家名的覆盖在该玩家重进后仍然有效。
- 名字牌常显状态没有读口:撤销名字牌覆盖时按"不常显"推回真实名字,服务端下次因别的原因重发元数据会带回真实值。
诊断:describeFor() 的字符串里带拦截钩子的计数(hooks(calls=… spawn=… meta=… block=… chunk=…))
与 sculk 发送失败数(sendFail=)——现象是"设了没反应"时先看它:calls 一直是 0 说明汇合点没被走到
(功能此时由心跳兜底,仍然生效,只是不即时);sendFail 大于 0 说明有包构造不合法(应上报)。
判定逻辑(坐标打包 / 区块归属 / 幂等判断 / 名字牌计划 / 类型名清洗)独立在
src/view/ViewOverrideLogic.h,离线可测:tests\check-view-override.bat(59 项)。