跳至主要內容

FreeMinecraftModels API 與開發者指南

產生的 Java 參考文件提供可搜尋的類別、方法和簽章,並記錄各模組的原始碼版本與提交。

FreeMinecraftModels 是獨立外掛,也為其他外掛提供 API。

共用依賴與執行環境設定請參閱 Java API 索引。依賴範例使用已發布的 2.12.9,目前原始碼版本為 2.12.9。請使用與伺服器已安裝外掛相符的依賴版本。

Maven 儲存庫​

<repository>
<id>magmaguy-repo-releases</id>
<name>MagmaGuy's Repository</name>
<url>https://repo.magmaguy.com/releases</url>
</repository>
<repository>
<id>magmaguy-repo-snapshots</id>
<name>MagmaGuy's Snapshot Repository</name>
<url>https://repo.magmaguy.com/snapshots</url>
</repository>

依賴​

<dependency>
<groupId>com.magmaguy</groupId>
<artifactId>FreeMinecraftModels</artifactId>
<version>2.12.9</version>
<scope>provided</scope>
</dependency>

使用 compileOnly/provided。不要將外掛 shade 進你自己的 jar 中。

核心入口點​

  • ModeledEntityManager.modelExists(String)
  • ModeledEntityManager.reload()
  • ModeledEntityManager.getAllEntities()
  • ModeledEntityManager.getDynamicEntities()
  • ModeledEntityManager.propEntities()
  • DisguiseAPI — 將玩家偽裝/取消偽裝為已載入的模型
  • LocationAPI — 註冊地城偵測器與保護提供者(提供給 Lua em.location.* 判斷使用)
  • ScriptedItemAPI — 為第三方 ItemStack 設定 FMM 物品定義的識別與顯示資料
  • ItemConstructionSnapshot - 在伺服器執行緒上擷取物品與顯示模型資料,以便在其他執行緒上建構物品

ModeledEntityManager.getAllEntities()、getDynamicEntities() 和 propEntities() 回傳的是當下時間點的副本。變更回傳的集合或映射並不會修改 FMM 的即時註冊表。請使用 modelExists(String) 做存在性檢查,而不要保留並反覆複製整份註冊表。

核心執行階段類型​

  • ModeledEntity
  • StaticEntity
  • DynamicEntity
  • PropEntity

建立實體​

StaticEntity preview = StaticEntity.create("example_model", location);
DynamicEntity mobModel = DynamicEntity.create("example_model", livingEntity);
DynamicEntity mount = DynamicEntity.createWithInvisibility("example_model", livingEntity);
PropEntity prop = PropEntity.spawnPropEntity("example_model", location);

如果請求的模型 ID 未載入,所有建立路徑都會回傳 null。

此外,當目標方塊上已經載入了同一個模型的道具時,PropEntity.spawnPropEntity 也會回傳 null —— 重複的道具會被拒絕而不是疊加上去,並且會記錄一則 [FMM Props] Prevented duplicate prop spawn ... 警告。請務必檢查它的回傳值是否為 null;只有非 null 的回傳值才能確認道具真的被建立了。

createWithInvisibility 是一種變體,會套用隱形藥水效果,而不是對客戶端隱藏實體。這會保持實體在客戶端被追蹤,這對於載具操控(由 /fmm mount 在內部使用)是必要的,也讓模型的展示實體骨骼能在 Java 客戶端上騎在該實體上;請參閱 displayBonesRideTheirEntity。

實用的執行階段方法​

  • ModeledEntity#setDisplayName(String) -- 設定模型的名牌
  • ModeledEntity#setDisplayNameVisible(boolean) -- 控制名牌是否可見
  • ModeledEntity#setLeftClickCallback(...)
  • ModeledEntity#setRightClickCallback(...)
  • ModeledEntity#setHitboxContactCallback(...)
  • ModeledEntity#setModeledEntityHitByProjectileCallback(...)
  • ModeledEntity#playAnimation(String, boolean blend, boolean loop) -- 當名稱既不符合任何內建狀態、也不符合模型中的動畫,或在請求 death 之後,回傳 false。對自訂動畫而言,blend = true 會在動作層上疊在 idle 或 walk 之上播放它,false 則讓它接管整個姿勢;loop 只適用於自訂動畫。請參閱動畫混合
  • ModeledEntity#stopCurrentAnimations() -- 結束自訂動畫與攻擊,並讓模型瞬間切換到 idle;死亡開始後不會有任何作用
  • ModeledEntity#hasAnimation(String)
  • ModeledEntity#damage(double) / damage(Entity damager, double) / damage(Entity damager) / damage(Projectile) -- 都經由該實體的 DamageableComponent 處理
  • ModeledEntity#attack(LivingEntity) / attack(LivingEntity, double damage)
  • ModeledEntity#teleport(Location, boolean teleportUnderlyingEntity)
  • ModeledEntity#setUnderlyingEntity(Entity) / ModeledEntity.getModeledEntity(Entity) -- 從 Bukkit 實體反查附加其上的模型的靜態方法,找不到則為 null
  • ModeledEntity#showUnderlyingEntity(Player) / hideUnderlyingEntity(Player) -- 針對每位玩家控制底層原版實體的可見性
  • ModeledEntity#getEntityID() -- 回傳模型 ID 字串
  • ModeledEntity#getModelInstanceId() -- 每個實例的 UUID,在該模型的生命週期內保持不變
  • ModeledEntity#isRemoved() / isDying() -- 生命週期旗標
  • ModeledEntity#getLocation() -- 回傳當前的 Location
  • ModeledEntity#getSpawnLocation() -- 回傳建立該模型時所在的 Location
  • ModeledEntity#getWorld() -- 回傳 World
  • ModeledEntity#getSkeleton() / getSkeletonBlueprint() / getMountPointManager() -- 執行階段結構存取
  • ModeledEntity#getInteractionComponent() / getHitboxComponent() / getDamageableComponent() / getAnimationComponent() -- 上述便利方法背後的元件物件
  • ModeledEntity.getLoadedModeledEntities() -- 所有已載入模型化實體的靜態即時集合
  • ModeledEntity#getViewers() -- 回傳可以看到該實體的玩家 HashSet<UUID>
  • ModeledEntity#getNametagBones() -- 回傳名牌骨骼的 List<Bone>(用於放置額外文字很有用)
  • ModeledEntity#getScaleModifier() / setScaleModifier(double)
  • ModeledEntity#removeWithDeathAnimation() -- 以死亡動畫移除(如果存在的話)
  • ModeledEntity#removeWithMinimizedAnimation() -- 以縮小動畫移除
  • ModeledEntity#remove() -- 立即移除實體與所有骨骼
  • ModeledEntity#setTintColor(Color) / getTintColor() -- 透過皮革護甲染料通道套用持續的顏色色調。受傷閃爍會短暫覆寫色調,然後淡回原色。傳入 null 來清除。
  • ModeledEntity#setViewDistanceOverride(int) / getEffectiveViewDistance() -- 針對單一實體覆寫 DefaultConfig.maxModelViewDistance。傳入 -1 來還原為外掛範圍的預設值。
  • DynamicEntity#setSyncMovement(boolean)
  • DynamicEntity#isDamagesOnContact() / setDamagesOnContact(boolean) -- 控制實體是否會透過碰撞箱接觸對玩家造成傷害
  • DynamicEntity.isDynamicEntity(Entity) / DynamicEntity.getDynamicEntity(Entity) -- 從 Bukkit 實體查詢的靜態方法
  • DynamicEntity#getBodyLocation() -- 朝向模型 yaw 的實體位置。當 weightedTurning 開啟時,該 yaw 會以轉向慣性跟隨生物的身體 yaw,因此在模型轉向期間可能落後於生物。移動同步開啟時,getLocation() 會回傳相同的位置
  • Bone#getBoneLocation()

光線、傾斜、置中與點擊​

  • ModeledEntity#setBrightnessOverride(int blockLight, int skyLight) -- 以固定亮度照亮每個顯示骨骼,兩者各為 0 到 15,而不使用骨骼所在位置的光線。15, 15 為完全明亮。觀看者會在下一次骨骼更新時收到變化。僅適用於 Java 顯示骨骼:Bedrock 和盔甲架骨骼維持世界光線
  • ModeledEntity#clearBrightnessOverride() / hasBrightnessOverride() / getBrightnessOverrideBlockLight() / getBrightnessOverrideSkyLight() -- 解除或讀取固定光線。沒有覆寫時,getter 回傳 -1
  • DynamicEntity#setTravelOrientation(TravelOrientation) / getTravelOrientation() -- 讓模型隨移動而傾斜。TravelOrientation 是一個 record,包含 Style(UPRIGHT、TOP_FIRST、FRONT_FIRST)、startSpeed、fullSpeed、maxTilt 和 turnSpeed;TravelOrientation.UPRIGHT 為預設值,傳入 null 即可恢復。請參閱移動朝向
  • ModeledEntity#setBodyTilt(Quaterniondc) / getBodyTilt() -- 以繞模型方塊中央的世界空間旋轉讓整個模型傾斜,發生在面向與動畫之後。傳入 null 讓它直立。不是 UPRIGHT 的移動朝向會每個 tick 設定此值;在直立的模型上,你設定的傾斜會保留
  • ModeledEntity#setCenteredOnBody(boolean) / isCenteredOnBody() / getBodyOffset() -- 把模型方塊的中央畫在實體的中央。請參閱身體置中
  • DynamicEntity#setClickSurface(boolean) / isClickSurface() -- 為 true 時,客戶端會在模型上得到一個不可見的 Interaction 實體,讓玩家點擊他們看到的模型,而不是本體自己的方框。它的大小取自模型的 hitbox 骨骼,沒有該骨骼時取自模型自身的邊界,並在兩個 tick 內生效。預設為關閉,因為覆蓋長形或旋轉過的模型的方框,也會接到空白角落的點擊
  • DynamicEntity#isSteerableMount() / hasSteeringRider() / seatSteeringRider(Player) -- 由裝上馬鞍的馬所穿戴的模型是可操控的坐騎。沒有人騎乘時,seatSteeringRider 會讓玩家作為操控騎乘者坐在馬本身上,並回傳他們現在是否正在騎乘。預設情況下,對這類模型按右鍵就會這麼做
  • DynamicEntity#liftSaddleTo(Bone) -- 縮放生物,讓馬鞍在模型的靜止姿勢下落在指定的座位骨骼上,同時模型保持其大小。/fmm mount 以模型的第一根 m_ 骨骼使用它。當伺服器無法回報馬鞍高度時,它不會有任何作用

名牌與注視目標​

ModeledEntity 提供 setDisplayNameLines(List<String>)、setDisplayNameScale(float)、setDisplayNameLineGap(double)、setDisplayNameVisible(boolean) 和 getNameplateLocation()。名牌骨骼為選用;沒有時,名牌位於模型靜止時最高立方體上方四分之一格(四像素)處,並隨實體縮放。

多行名稱:

  • setDisplayName("Top line\nBottom line") 和 setDisplayNameLines(List<String>) 依閱讀順序由上到下接受各行。最下面一行固定在錨點,其餘行向上延伸。由明確的換行字元定義各行;文字不會自動換行。
  • 空名稱會隱藏名牌。setDisplayNameVisible(false) 會隱藏每一行,且在文字變更後仍然有效。
  • setDisplayNameScale(float) 獨立於模型大小縮放文字。setDisplayNameLineGap(double) 設定行與行之間的淨間距,單位為縮放 1 時的方塊數,預設為 0.1。縮放 1 時一行高 0.225 個方塊,因此預設情況下連續兩行相距 0.325 個方塊。
  • 當模型有多個 tag_ 骨骼時,依字母順序第一個會作為名牌的錨點,包括它的移動與動畫。
  • Java 玩家看到的是文字顯示,字形和間距都會縮放。Bedrock 玩家看到的是各自獨立的標記盔甲架標籤,無法縮放,因此間距維持在縮放 1。選用 Bedrock 標籤需要後端伺服器上有 Floodgate 或 Geyser 來辨識 Bedrock 玩家;沒有身分轉發、只有代理的設置無法可靠地選用它們。
  • 觀看範圍、隱形、移除和重新載入都跟隨模型。沒有變化的文字和位置不會傳送封包。

關於 EliteMobs 如何排列使用 FreeMinecraftModels 模型的 NPC 的名稱與角色,請參閱建立 NPC。

DynamicEntity.setLookTarget(Location) 為骨架設定視覺上的注視目標。傳入的位置會被複製,不會旋轉或移動底層實體。傳入 null 即可解除。

PropEntity 專屬方法​

  • PropEntity.isPropEntity(ArmorStand) / PropEntity.getPropEntityID(ArmorStand) -- 從支撐用的盔甲座辨識道具
  • PropEntity.hasLoadedPropOnSameBlock(String entityID, Location) -- spawnPropEntity 內部執行的重複檢查;如果你想先分支處理而不是檢查 null,可以先呼叫它
  • PropEntity.respawnPropEntityFromArmorStand(String entityID, ArmorStand) -- 圍繞在區塊重新載入後倖存的盔甲座重建道具
  • PropEntity.getPropEntities() -- 以盔甲座 UUID 為鍵的已載入道具快照;修改它不會變更執行中的登錄表
  • PropEntity#setPersistent(boolean) -- 切換支撐用盔甲座的持久化
  • PropEntity#setCustomDataString(NamespacedKey, String) / getCustomDataString(NamespacedKey) -- 在道具上讀寫你自己的 PDC 值。這與 Lua 的 set_persistent_data / get_persistent_data 輔助函式使用同一個儲存區,位於 fmm_lua_<key> 命名空間下
  • PropEntity#remove() / remove(boolean showRealBlocks) -- 移除模型但保留持久化項目
  • PropEntity#permanentlyRemove() -- 移除模型以及其持久化項目
  • PropEntity#setVoxelizeConfig(boolean voxelize, boolean solidify) / applySolidify() -- voxelize: / solidify: YML 欄位的執行階段等價方法
  • PropEntity#showFakePropBlocksToPlayer(Player) / showRealBlocksToPlayer(Player) 以及對應的 ...ToAllPlayers() 變體 -- 控制那些只以封包送出、用來讓實體化道具在客戶端具有碰撞的屏障方塊

魔法武器整合​

使用魔法服務前先檢查 MagicWeaponAPI.isOperational()。isWeapon(String) 檢查武器定義 ID;applyWeaponData(ItemStack, String) 套用武器識別與顯示資料,保留原物品的名稱、說明、等級與附魔。服務已註冊不代表已能正常運作。

選用的 MagicAttackResolver 可拒絕施法、篩選目標並排序、保存發射時的戰鬥資訊,以及計算傷害。必須使用傳入的一次性 MagicDamageApplication,不能直接傷害目標。輸入、飛行、碰撞及傷害套用由 FMM 負責。

MagicProjectileTravelEvent 是同步且可取消的碰撞查詢,涵蓋整段移動軌跡,也包含命中時的檢查。取消後會吸收飛行物,不造成傷害或爆炸。同一線段可能被檢查多次,因此監聽器不能將它視為傷害通知。

事件介面​

右鍵輸入會先針對真實的底層實體觸發 ModeledEntityInteractEvent。它繼承 PlayerInteractEntityEvent 的處理器,因此一般的實體保護監聽器可以取消它。取消後便不會觸發模型右鍵事件及回呼。這項檢查與方塊放置、破壞權限不同。

右鍵檢查如何配合:

  • 方塊與模型的互動是分開決定的。取消 PlayerInteractEvent 會讓方塊或物品動作維持被取消,但右鍵仍然可以透過 FMM 的觸及範圍與方塊遮擋檢查選中模型。左鍵的取消則與以往一樣。
  • ModeledEntityInteractEvent 使用主手。FMM 只會在該分派結束後才觸發 ModeledEntityRightClickEvent,而被取消的 ModeledEntityRightClickEvent 會停止模型回呼、道具行為和預設的騎乘。
  • 橋接事件是權限檢查,不是另一次點擊。FMM 自己的輸入、魔法武器和道具路由器會忽略它。會在原生實體互動事件上執行動作的外掛,可以略過 ModeledEntityInteractEvent 以避免重複動作;保護監聽器則應繼續處理它。
  • 沒有底層 Bukkit 實體的模型無法觸發 Bukkit 實體事件,但仍會觸發可取消的 ModeledEntityRightClickEvent。持久道具和 EliteMobs NPC 有底層實體,兩種檢查都會進行。

通用互動事件:

  • ModeledEntityLeftClickEvent
  • ModeledEntityRightClickEvent
  • ModeledEntityHitboxContactEvent
  • ModeledEntityHitByProjectileEvent

這四個都是可取消的 Bukkit 事件;FMM 的內建偵測路徑會在主伺服器執行緒上派發它們。取消其中一個會阻止 FMM 對應的回呼/預設行為。碰撞箱接觸掃描每兩個伺服器 tick 執行一次;點擊事件仍保有短暫的每位玩家去重複視窗,因此封包互動路徑與 OBB 射線路徑不會讓同一個動作觸發兩次。

生命週期事件:

  • FmmReloadedEvent -- 完整初始化成功後,排定於 40 ticks 後在主執行緒觸發,包含啟動及成功的完整重新載入。僅重新載入匯入內容的流程也會在成功時觸發。準備階段遭拒的請求會保留目前的執行狀態,不會發出重新載入完成事件

公開的互動介面刻意不區分模型類型。較舊的 StaticEntity*Event、DynamicEntity*Event 與 PropEntity*Event 變體,以及 ResourcePackGenerationEvent,都已不再屬於目前的 API。請改用上述四個通用的模型化實體事件與 FmmReloadedEvent。

持有長期存在的 DynamicEntity 或 PropEntity 參考的消費者外掛(EliteMobs、BetterStructures 等)必須處理此事件,在仍存在的底層實體上重新建立其模型附件。如果不這樣做,那些實體在重新載入後會變成不可見,因為 FMM 在 onDisable 期間拆除了 display entity,而消費者的參考現在已失效。

@EventHandler
public void onFmmReloaded(FmmReloadedEvent event) {
for (MyTrackedEntity tracked : myEntities) {
if (tracked.bukkitEntity != null && tracked.bukkitEntity.isValid()) {
tracked.fmmModel = DynamicEntity.create("my_model", tracked.bukkitEntity);
}
}
}

ModeledEntityHitByProjectileEvent 與 OBB 拋射物偵測​

FMM 使用 OBB(定向碰撞箱)命中偵測來處理對模型化實體的拋射物。當拋射物與模型化實體的 OBB 碰撞箱相交時,FMM 會觸發 ModeledEntityHitByProjectileEvent。這是一個標準的可取消 Bukkit 事件。

關鍵細節:

  • 偵測會檢查已追蹤位置之間的線段,以找出穿越薄型模型的情況;沒有前一個位置時,改用重疊檢查
  • 該線段上的候選目標會由近而遠依序解析,與模型註冊表的走訪順序無關
  • 非穿透型拋射物只會導向一個模型化目標。穿透(Piercing)箭矢可以依飛行順序命中 穿透等級 + 1 個不同的模型化目標,而且當 OBB 與原版底層碰撞箱同時觀察到它時也不會重複觸發
  • 預設的動態實體處理器會使用拋射物在撞擊當下的速度,把這次撞擊當成真正的拋射物傷害事件轉發給底層的生物實體。這保留了拋射物的傷害來源、戰鬥外掛的處理以及射擊者歸屬
  • 取消事件會阻止 FMM 的回呼/預設傷害行為,但該次碰撞仍會消耗掉這個拋射物的模型化目標額度。因此被取消的非穿透命中依然會消耗該拋射物
@EventHandler
public void onProjectileHitModel(ModeledEntityHitByProjectileEvent event) {
ModeledEntity target = event.getEntity();
Projectile projectile = event.getProjectile();
// Cancel to prevent damage
event.setCancelled(true);
}

物品與模型公用程式​

ModelItemFactory​

用於以程式化方式建立模型相關 ItemStack 的工廠類別。

// Create a prop placement item (uses "model_id" PDC key)
ItemStack placementItem = ModelItemFactory.createModelItem("lamp_post", Material.STICK);

// Create a custom item from config (uses "fmm_item_id" PDC key)
PropScriptConfigFields config = ItemScriptManager.getItemDefinitions().get("magic_sword");
ItemStack customItem = ModelItemFactory.createCustomItem("magic_sword", config);
  • createModelItem(String modelId, Material material) -- 為道具建立放置物品。在 1.21.4+ 上,如果存在顯示 JSON 會自動套用 display model 渲染。
  • createCustomItem(String itemId, PropScriptConfigFields config) -- 從統一設定建立帶有名稱、物品說明、附魔與 display model 的自訂物品。
  • formatModelName(String modelId) -- 公用程式,將像 01_em_flame_sword 這樣的模型 ID 轉換為 Flame Sword。

DisplayModelRegistry​

追蹤哪些模型擁有可用的顯示 JSON 的簡單註冊表。

// Check if a model has a display model JSON registered
boolean has3D = DisplayModelRegistry.hasDisplayModel("magic_sword");
  • register(String modelId) -- 註冊模型 ID(在重新載入期間內部呼叫)
  • hasDisplayModel(String modelId) -- 檢查顯示模型是否已註冊;格式錯誤的 .json 不會因檔案存在就註冊
  • getRegisteredModels() -- 回傳所有已註冊顯示模型的模型 ID 的不可變 Set<String>
  • shutdown() -- 清除所有註冊

ItemScriptManager​

雖然保留了原類別名稱,它目前是物品與魔法武器定義的目錄,不再執行裝備物品的腳本。getItemDefinitions() 提供已註冊的物品定義,getWeaponCatalog() 提供已驗證的武器定義。舊的執行中腳本、裝備更新及玩家腳本移除 API 已刪除。

ScriptedItemAPI​

  • isValidItemId(String):檢查完全相符的物品定義 ID,即去掉 .yml 的設定檔名稱。
  • getItemConfig(String):回傳定義,不存在則回傳 null。
  • applyScriptedItemData(ItemStack, String):設定 fmm_item_id,並在有已註冊顯示模型時套用該模型。ID 無效或缺少物品中繼資料時回傳 false。

此方法不會變更原物品的名稱、說明或附魔,也不會附加 Lua 行為。請透過 MagmaCore 另外套用附魔。弓與弩的拉弓狀態屬於基底顯示模型定義;不要在物品 ID 後面加上 _idle。

ModelItemAPI​

只需要 FMM 顯示模型時,請使用 ModelItemAPI.applyDisplayModel(ItemStack, String)。它檢查顯示模型註冊表,不設定物品定義的識別資料。這與選擇 FMM 武器或物品定義是不同的操作。

ItemConstructionSnapshot​

適用於在伺服器執行緒之外建構物品的外掛。請在伺服器執行緒上呼叫 ItemConstructionSnapshot.capture();它會複製目前的物品定義、顯示模型與魔法武器種類。之後工作執行緒即可呼叫 item(id) 或 weapon(id),並以 apply(ItemStack) 將傳回的 ItemData 套用到 ItemStack;這會為物品定義設定 fmm_item_id,並在有已註冊顯示模型時套用該模型。除非該 ID 是魔法武器,且擷取時魔法服務處於可運作狀態,否則 weapon(id) 會回傳 null。發布建構好的物品前,請在伺服器執行緒上呼叫 requireCurrent():若 FMM 的定義自擷取後已變更(例如重新載入之後),它會拋出 ConcurrentModificationException。

DisguiseAPI​

玩家偽裝功能的公開入口點。第三方外掛應呼叫此類別,而非內部的 DisguiseManager,以使內部重構保持安全。

import com.magmaguy.freeminecraftmodels.api.DisguiseAPI;

// Disguise a player as a loaded model. Replaces any existing disguise cleanly.
boolean ok = DisguiseAPI.disguise(player, "dragon");

// Undisguise (returns true if a disguise was removed).
DisguiseAPI.undisguise(player);

// Query state.
boolean disguised = DisguiseAPI.isDisguised(player);
String modelID = DisguiseAPI.getDisguiseModelID(player); // null if not disguised

// Snapshot of all currently disguised players.
Collection<Player> all = DisguiseAPI.getDisguisedPlayers();
  • disguise(Player, String modelID) -- 嘗試替換前會先移除舊偽裝;未知 ID 會在移除後回傳 false。請傳入線上玩家和確切的已載入 ID;API 不會正規化 ID 或檢查指令權限
  • undisguise(Player) -- 如果移除了偽裝則回傳 true
  • isDisguised(Player) -- 快速布林檢查
  • getDisguiseModelID(Player) -- 回傳活躍的模型 ID 或 null
  • getDisguisedPlayers() -- 偽裝中玩家的不可修改快照

FMM 結合玩家預設可見性、隱形藥水效果與裝備顯示覆寫。只有本次偽裝原先加入的效果遭移除時才會補回;偽裝前已有的效果不受其管理或恢復。指令會先驗證模型再替換,與 API 不同。生命週期與客戶端顯示限制請見玩家偽裝。

LocationAPI​

供外掛貢獻地城偵測與區域保護檢查的公開 API。註冊的判斷會提供給 FMM 的 Lua em.location.is_in_dungeon 與 em.location.is_protected 檢查使用(由 pickupable.lua 與 storage_double.lua 等預製腳本使用)。

外掛傳入一般的 Predicate<Location>,所以沒有 shade 過的 FMM 類型會跨外掛 classloader。

import com.magmaguy.freeminecraftmodels.api.LocationAPI;

// On your plugin's enable, after WorldGuard/EliteMobs/etc. are available.
LocationAPI.registerDungeonLocator("EliteMobs",
location -> EliteMobs.isInsideDungeon(location));

LocationAPI.registerProtectionProvider("WorldGuard",
location -> WorldGuardBridge.isProtected(location));
  • registerDungeonLocator(String providerName, Predicate<Location> predicate) — 任何回傳 true 的已註冊判斷會將位置標記為「在地城中」
  • registerProtectionProvider(String providerName, Predicate<Location> predicate) — 任何回傳 true 的已註冊判斷會將位置標記為受保護
  • unregisterDungeonLocator(String providerName) 與 unregisterProtectionProvider(String providerName) 會移除以該名稱註冊的判斷。請在外掛的 onDisable 中呼叫,讓 FMM 不再呼叫已停用的實例

以相同的提供者名稱再次註冊會取代先前的判斷,因此每次啟用都重新註冊的外掛不會累積重複的副本。以 null 作為提供者名稱註冊的判斷既無法被取代,也無法取消註冊。

操作員可以使用 /fmm location 驗證註冊情況,該指令會回報即時的提供者數量並對當前位置測試這兩種判斷。

保護提供者與道具放置​

同一批保護提供者也驅動了 preventPropPlacementInProtectedRegions 這項道具放置檢查,但該檢查呼叫的是 canBuild(player, location),而不是只看位置的 isProtected(location)。這個差別很重要:

提供者canBuild 行為
內建的 WorldGuard 轉接器會尊重 WorldGuard 自身的 bypass,然後交由 WorldGuard 的 testBuild 判斷 —— 因此區域成員與擁有者可以正常建造
內建的 GriefPrevention 轉接器該位置沒有領地即視為允許;在領地內則交由 GriefPrevention 針對該玩家的建造權限判斷
透過 LocationAPI.registerProtectionProvider 註冊的提供者退回使用 !isProtected(location) —— 只看位置,會阻擋所有人,只要你的判斷認為該位置受保護

Predicate<Location> 無法表達特定玩家的權限。若你的區域系統需要依玩家檢查權限,請直接實作 MagmaCore 的 RegionProtectionProvider、覆寫 canBuild,再以 LocationQueryRegistry.registerProtectionProvider 註冊。

還有另外兩個值得知道的行為:

  • 轉接器失敗時採取封閉式失敗。 如果某個提供者在建造查詢期間拋出例外,該次放置會被拒絕,並記錄一則指名該提供者的警告。它絕不會默默地退回「允許」。
  • 每一個已註冊的提供者都必須同意。 第一個說不的提供者說了算;透過 LocationOwnership 註冊的所有權提供者仍然只看位置,並會阻擋所有人。

物品放置會先檢查相鄰格。體素化道具還會依四捨五入且至少一格的碰撞箱尺寸,檢查旋轉後的占用範圍;封包屏障體積使用另一種計算。直接呼叫 PropEntity.spawnPropEntity 不會執行玩家放置物品的檢查。透過其他外掛提供放置功能前,請先閱讀配置。

PropScriptConfigFields​

讀取模型旁邊的 YAML,供道具與物品定義使用。道具的 scripts: 清單會繫結獨立的 Lua 腳本。指定 material: 則註冊手持物品,不能再使用非空的 scripts: 清單。

各自適用的範例請參閱模型設定,武器設定格式請參閱魔法武器。

Lua 腳本​

FMM 透過 MagmaCore 引擎支援道具 Lua 腳本。檔案放在 plugins/FreeMinecraftModels/scripts/,並透過模型旁邊的 YML 繫結。磁碟上的檔名必須以 .lua 結尾;設定中可包含或省略副檔名。

道具會將 scripts: 列出的所有腳本繫結為獨立實例。手持物品的效果由另一個附魔提供者執行環境處理,不能使用非空的物品 scripts: 清單。

道具腳本鉤子​

鉤子觸發時機
on_spawn道具被生成到世界中
on_game_tick道具存在的每個 tick
on_zone_enter玩家進入由腳本建立的受監看區域
on_zone_leave玩家離開由腳本建立的受監看區域
on_destroy道具被永久移除(區塊卸載或重新載入時不會觸發)
on_left_click玩家左鍵點擊道具
on_right_click玩家右鍵點擊道具
on_projectile_hit投射物擊中道具;context.projectile 是該投射物,context.event.player 是射擊者

物品腳本鉤子​

裝備物品的 Lua 鉤子與 context.item 已移除。物品效果請使用附魔定義。道具鉤子仍受支援。

道具腳本 Context 表格​

道具腳本會接收一個 context 表格。以下是主要 API 的摘要 -- 完整細節請參閱 Lua 道具 API。

context.prop:

  • model_id -- 藍圖模型名稱
  • current_location -- 道具的當前位置
  • play_animation(name, blend, loop) -- 請求指定動畫(blend 與 loop 預設為 true,也就是把循環動畫疊在道具的 idle 之上);請求會在模型下一個 tick 套用
  • stop_animation() -- 結束自訂動畫,若有 idle 就回到該動畫;不能中斷處於終止狀態的死亡動畫
  • hurt_visual() -- 在道具上播放受傷(紅色閃爍)視覺效果
  • pickup() -- 將「移除道具並掉落其放置物品」排入佇列
  • mount(player) -- 將一次騎乘嘗試排入佇列;回傳 true 只表示該玩家與騎乘管理員是有效的,並不表示最終有分配到座位
  • dismount(player) -- 將一次下乘檢查排入佇列;回傳 true 只表示該玩家與騎乘管理員是有效的
  • get_passengers() -- 回傳當前騎在道具上的玩家清單
  • spawn_elitemobs_boss(filename, x, y, z) -- 在道具當前世界中的絕對座標處生成 EliteMobs Boss

context.event:

  • 可用於道具的 on_left_click、on_right_click、on_zone_enter、on_zone_leave 與 on_projectile_hit
  • 當底層鉤子可被取消時,可使用 cancel()、uncancel()、is_cancelled
  • player -- 觸發事件的玩家;在 on_projectile_hit 中則是射出該投射物的生物實體

context.world:

  • spawn_entity(entity_type, x, y, z) -- 生成原版實體,若實體類型無效則回傳 nil
  • set_block_at(x, y, z, material) -- 若材料有效則將方塊變更排入佇列;未載入的區塊會被略過
  • 加上粒子、音效、方塊查詢、閃電以及附近實體查找

context.cooldowns:

  • check_local(key, ticks) -- 檢查並啟動一個腳本層級的冷卻
  • global_ready() / set_global(ticks) -- 道具的共用冷卻

玩家物件(來自 context.player 或 context.event.player):

  • get_held_item() -- 回傳玩家手持的物品;type 為大寫的 Bukkit 材料名稱
  • is_holding_elitemobs_item(filename):檢查主手物品的 EliteMobs 檔名標籤,例如 shroomwhispers_shears.yml。顯示名稱和翻譯不影響此檢查。空手、物品不符或 EliteMobs 整合無法使用時傳回 false。
  • consume_held_item() -- 排定移除一個主手物品,以任務執行時的主手內容為準;不回傳付款結果
  • has_item(material) -- 檢查玩家是否擁有物品
  • send_message(text) -- 向玩家發送聊天訊息
  • game_mode -- 玩家當前的遊戲模式

使用檔名識別 EliteMobs 任務物品:

if not player:is_holding_elitemobs_item("shroomwhispers_shears.yml") then
return
end

此方法由 FMM 的 EliteMobs 整合提供。重新命名普通物品不會替它加入所需的檔名標籤。

道具腳本範例​

return {
api_version = 1,

on_spawn = function(context)
context.prop:play_animation("idle", true, true)
end,

on_right_click = function(context)
if context.cooldowns:check_local("activate", 40) then
context.prop:play_animation("activate", false, false)
end
end
}

物品腳本範例​

舊的裝備物品範例已不受支援。請參閱魔法武器和附魔定義,使用目前的系統建立物品。

從原始碼建置​

原始碼位於 GitHub。FreeMinecraftModels 是一個 Java 21 的 Maven 專案:

mvn -DskipTests package
  • 外掛 jar 會輸出到 target/FreeMinecraftModels.jar。
  • MagmaCore、bStats 和其他內附的函式庫會在 package 階段被 shade 進來。MagmaCore 會被重新定位到 com.magmaguy.freeminecraftmodels.magmacore,因此伺服器不需要另外安裝 MagmaCore。
  • Maven 會從 MagmaGuy 的儲存庫解析 MagmaCore 2.2.0-SNAPSHOT。當原始碼需要尚未在那裡發布的 MagmaCore 變更時,請先建置 MagmaCore 並發布到你的本機 Maven 儲存庫。-Dmagmacore.version=<version> 可針對其他 MagmaCore 版本建置。
  • 設定了 MC_DIST_DIR 環境變數時,建置也會把 jar 複製到該資料夾。

匯入的模型、材質、字型與內容包仍保有其作者的授權。從它們產生資源包並不會改變這些權利。

說明​

  • FreeMinecraftModels 是一個已安裝外掛的依賴,並非可嵌入的函式庫。
  • 如果你的外掛需要新匯入的模型,請呼叫 ModeledEntityManager.reload(),而不要試著自行重建 FreeMinecraftModels 的狀態。
  • 目前 FMM 原始碼依賴 MagmaCore 2.2.0-SNAPSHOT,由其提供共用 Lua 引擎、LocationQueryRegistry 與 WorldFolderResolver。
  • FreeMinecraftModels 將 WorldGuard、WorldEdit、GriefPrevention、Vault、floodgate 與 Geyser-Spigot 宣告為 softdepend。WorldGuard 和 GriefPrevention 提供內建的玩家別保護介面;WorldEdit 是 WorldGuard 的相依項目,不是獨立的 FMM 保護介面。商店需要 Vault、經濟提供者及已啟用的商店設定。Bedrock 顯示也需要相容資源包與整合路徑。
  • ModeledEntityManager.reload() 請求與 /fmm reload 相同的非同步準備及完整生命週期,不會等待完成。請從主執行緒呼叫,並以成功的生命週期通知確認結果,而非 void 回傳值。無效的候選內容可讓重新載入遭拒,同時保留運作中的狀態。