diff --git a/docs/zh-TW/contributing/contributing-to-docs.md b/docs/zh-TW/contributing/contributing-to-docs.md new file mode 100644 index 0000000..9554c35 --- /dev/null +++ b/docs/zh-TW/contributing/contributing-to-docs.md @@ -0,0 +1,28 @@ +# 為文件作出貢獻 + +我們使用 [MkDocs](https://www.mkdocs.org/) 和 [Github Pages](https://pages.github.com/) 來建立並維護你現在正在閱讀的文件網站。 + +## 先決條件 + +在開始之前,請確保你的系統已安裝以下內容: + +- **Python 3** - MkDocs 所需 +- **pip** - Python 套件安裝工具(通常隨 Python 一起提供) + +請參考 [MkDocs 的安裝指南](https://www.mkdocs.org/user-guide/installation/) 了解如何安裝這些相依套件。 + +## 如何開始 + +1. 複製 `pylon-docs` 儲存庫:`git clone https://github.com/pylonmc/pylon-docs` +2. 安裝所有必要的相依套件:`pip install -r requirements.txt` +3. 使用下列指令在本機執行文件網站:`mkdocs serve` + +## 部署 + +只有核心成員可以部署網站。 + +1. 複製 `pylonmc.github.io` 儲存庫:`git clone https://github.com/pylonmc/pylonmc.github.io` +2. 在 **pylonmc.github.io 儲存庫** 中執行以下指令以部署網站: + `mkdocs gh-deploy --config-file ../pylon-docs/mkdocs.yml --remote-branch master` + +[comment]: <> (TODO: 添加更詳細的說明,例如 Slimefun 的 (https://github.com/Slimefun/Slimefun4/wiki/Expanding-the-Wiki)) diff --git a/docs/zh-TW/contributing/getting-started.md b/docs/zh-TW/contributing/getting-started.md new file mode 100644 index 0000000..c1a7156 --- /dev/null +++ b/docs/zh-TW/contributing/getting-started.md @@ -0,0 +1,59 @@ +# 開始使用 + +Pylon Core 是以 [Kotlin](https://kotlinlang.org/) 編寫的,一種與 Java 相似但具有更多現代化特性與簡潔語法的語言。 +如果你熟悉 Java,將能很快上手 Kotlin。 + +Pylon Base 則是以 Java 編寫。 + +## 如何開始 + +1. 複製 `pylon` 儲存庫:`git clone https://github.com/pylonmc/pylon`(或使用像 GitHub Desktop 這樣的圖形介面) +2. 若你使用 IntelliJ,它會自動完成所有設定。 + 若否,請執行 `./gradlew`。這會自動複製 `pylon-core` 與 `pylon-base` 儲存庫。 +3. 如果你想將修改提交到 Pylon 專案, + **請刪除 `pylon-core` 或 `pylon-base` 目錄(視你要貢獻的部分而定),接著 fork 該儲存庫並將你的 fork 複製回同一個目錄中。** + 否則,你將無法提交 Pull Request(除非你是 Pylon 開發者並擁有該專案的存取權)。 + +更多資訊請參考 [Pylon Master Project](./master-project) 頁面。 + +## 提交貢獻 + +我們歡迎對 Core 與 Base 的貢獻,但若你計畫進行重大變更,建議先與 Pylon 團隊確認,因為我們可能已有既定規劃,未必與你的修改相容。 +如果你有興趣參與較大型的開發或有任何疑問,請加入我們的 Discord 伺服器與我們討論 :) + +完成修改後,請開啟 Pull Request,並簡要說明你做了哪些變更以及原因。 + +## 測試 + +Pylon Core 擁有一組整合測試。 +測試應僅針對關鍵功能(例如方塊儲存與配方)新增。 + +## 自訂 Dokka + +Pylon 使用自訂版本的 Dokka 來生成 Javadoc。 +這是因為預設的 Dokka 輸出存在許多錯誤,且外觀不佳。 +Seggan 已經向 Dokka 專案提交修正的 Pull Request,但尚未被合併,因此暫時採用自訂版本。 + +若你希望查看「修正版」的文件輸出,可依以下步驟操作: + +1. 複製 `pylonmc/dokka` 儲存庫:`git clone https://github.com/pylonmc/dokka` +2. 切換至 `pylon` 分支:`git checkout pylon` +3. 在根目錄中執行: + `./gradlew publishToMavenLocal -Pversion=2.1.0-pylon-SNAPSHOT` + 注意:Dokka 專案相當龐大,首次建置將花費較長時間。 + 以 Seggan 的筆電為例,第一次執行約需 10 分鐘。 + 後續建置將會更快,只要不刪除快取即可。 +4. 現在,在 Pylon 主專案中執行: + `./gradlew :pylon-core:pylon-core:dokkaGenerate -PusePylonDokka=true` + 生成的輸出會位於: + `pylon/pylon-core/pylon-core/build/dokka` + +## 卡關了,該怎麼辦? + +1. 若是與 Pylon 相關的問題,先查看文件中是否已有說明。 + 若非 Pylon 特有問題,請善用 Google 搜尋。 +2. 在相關儲存庫的 Issues 中搜尋是否已有類似問題。 +3. 在我們的 Discord 伺服器中搜尋相關關鍵字,看看是否已有討論。 +4. 若仍無法解決,請在 Discord 伺服器中發問。 + +[comment]: <> (TODO: 讓這份說明更易理解,也許可以加入截圖等輔助內容,因為經驗較少的使用者可能完全看不懂這些指令在做什麼) diff --git a/docs/zh-TW/contributing/master-project.md b/docs/zh-TW/contributing/master-project.md new file mode 100644 index 0000000..a99d7d3 --- /dev/null +++ b/docs/zh-TW/contributing/master-project.md @@ -0,0 +1,41 @@ +# Pylon 主專案 + +Pylon 有一個主儲存庫(master repository),其中包含 `pylon-core` 與 `pylon-base`。 +這樣的架構讓你能夠在自己的環境中運行自製版本的 Core, +使測試新功能變得更加容易。 +這正是「如何開始」章節中所使用的方式。 +我們建議你透過主儲存庫同時對 Base 與 Core 進行修改, +本指南後續的內容也會假設你是以這種方式進行開發。 + +## 專案結構 +主專案的結構如下: +``` +pylon/ + pylon-base/ + pylon-core/ + dokka-plugin/ + nms/ + pylon-core/ + test/ +``` +`pylon-base` 包含了 Pylon 的 Base 外掛。 +`pylon-core` 則包含四個子專案:`dokka-plugin`、`nms`、`pylon-core` 與 `test`。 +`dokka-plugin` 儲存我們自訂的 Dokka 插件,用於協助格式化 Javadocs。 +`nms` 包含所有與伺服器內部機制(server internals)互動的 Pylon Core 程式碼。 +它被分離為獨立的子專案,目的是將潛在不穩定的程式碼與其他部分隔離開來。 +`pylon-core` 包含主要的 Pylon Core 程式碼,`test` 則包含 Pylon Core 的整合測試。 + +## 任務(Tasks) +Pylon 主專案中包含了一些在開發時非常有用的任務: + +| 任務名稱 | 別名(Alias) | 說明 | +|------------------------------|---------------------|----------------------------------------------------------------------------------------------------------| +| `runServer` | `runSnapshotServer` | 使用目前版本的 Pylon Base 與 Pylon Core 啟動 Minecraft 伺服器 | +| `:pylon-base:runServer` | `runStableServer` | 使用最新穩定版本的 Pylon Core 及目前版本的 Pylon Base 啟動 Minecraft 伺服器 | +| `:pylon-core:test:runServer` | `runLiveTests` | 執行 Pylon Core 的整合測試 | + +!!! danger + 若你在 IntelliJ 中使用這些任務的別名啟動並附加除錯器,你會發現它無法運作。 + 我也不知道為什麼會發生這種情況。 + 若要成功附加除錯器,請執行實際的任務名稱而非別名。 + 例如:不要執行 `runSnapshotServer`,請執行 `runServer`。 diff --git a/docs/zh-TW/creating-addons/custom-items/adding-a-custom-item.md b/docs/zh-TW/creating-addons/custom-items/adding-a-custom-item.md new file mode 100644 index 0000000..499687e --- /dev/null +++ b/docs/zh-TW/creating-addons/custom-items/adding-a-custom-item.md @@ -0,0 +1,123 @@ +# 新增自訂物品(Adding a custom item) + +## 概述(Overview) + +到目前為止,我們所建立的「自訂」物品,只是將法國麵包恢復飢餓值從 5 提升到 6。 +但如果我們希望能夠右鍵點擊生物讓它著火該怎麼辦? +這不像食物那樣有現成的方法可用,因此我們需要撰寫一些程式碼來實現這個功能。 + +--- + +## 法國麵包火焰噴射器(The baguette flamethrower) + +為了示範這一點,讓我們建立一個新的物品:「法國麵包火焰噴射器」。 + +### 建立「一般物品」(Creating a 'normal item') + +首先,我們先建立一個「一般」物品,就像前一章所做的那樣。 + +=== "Java" +```java title="MyAddon.java" +NamespacedKey baguetteFlamethrowerKey = new NamespacedKey(this, "baguette_flamethrower"); +ItemStack baguetteFlamethrower = ItemStackBuilder.pylonItem(Material.BREAD, baguetteFlamethrowerKey) + .build(); +PylonItem.register(PylonItem.class, baguetteFlamethrower); +BasePages.FOOD.addItem(baguetteFlamethrowerKey); +``` +=== "Kotlin" +```kotlin title="MyAddon.kt" +val baguetteFlamethrowerKey = NamespacedKey(this, "baguette_flamethrower") +val baguetteFlamethrower = ItemStackBuilder.pylonItem(Material.BREAD, baguetteFlamethrowerKey) + .build() +PylonItem.register(baguetteFlamethrower) +BasePages.FOOD.addItem(baguetteFlamethrowerKey) +``` + +```yaml title="en.yml" +item: + baguette_flamethrower: + name: "Baguette Flamethrower" + lore: |- + 使用法國麵包的力量點燃你所注視的方塊 +``` + +--- + +### 建立自訂物品類別(Creating a custom item class) + +接下來,我們要新增讓實體著火的程式碼。 + +為了做到這點,我們可以建立一個自訂類別 `BaguetteFlamethrower`。 +所有 Pylon 物品類別都必須繼承 [PylonItem]。 + +=== "Java" +建立一個新檔案 `BaguetteFlamethrower.java` 並加入以下內容: + +```java title="BaguetteFlamethrower.java" +public class BaguetteFlamethrower extends PylonItem { + public BaguetteFlamethrower(@NotNull ItemStack stack) { + super(stack); + } +} +``` +=== "Kotlin" +建立一個新檔案 `BaguetteFlamethrower.kt` 並加入以下內容: + +```kotlin title="BaguetteFlamethrower.kt" +class BaguetteFlamethrower(stack: ItemStack) : PylonItem(stack) +``` + +--- + +現在我們希望當玩家手持「法國麵包火焰噴射器」右鍵點擊實體時能發生事件。 +為了實現這點,我們可以實作 [PylonItemEntityInteractor] 介面。 +這是 Pylon 內建的一個介面,包含一個方法:`onUsedToRightClickEntity`。 + +=== "Java" +```java title="BaguetteFlamethrower" hl_lines="6-9" +public class BaguetteFlamethrower extends PylonItem implements PylonItemEntityInteractor { + public BaguetteFlamethrower(@NotNull ItemStack stack) { + super(stack); + } + + @Override + public void onUsedToRightClickEntity(@NotNull PlayerInteractEntityEvent event) { + event.getRightClicked().setFireTicks(40); + } +} +``` +=== "Kotlin" +```kotlin title="BaguetteFlamethrower" hl_lines="2-4" +class BaguetteFlamethrower(stack: ItemStack) : PylonItem(stack), PylonItemEntityInteractor { + override fun onUsedToRightClickEntity(event: PlayerInteractEntityEvent) { + event.rightClicked.fireTicks = 40 + } +} +``` + +--- + +### 使用自訂物品類別(Using the custom item class) + +要讓這個類別生效,我們必須告訴 Pylon 使用此類別,而非預設的 `PylonItem`。 + +請將你的 `PylonItem.register(...)` 改為以下內容: + +=== "Java" +```java +PylonItem.register(BaguetteFlamethrower.class, baguette); +``` +=== "Kotlin" +```kotlin +PylonItem.register(baguette) +``` + +--- + +就這樣!現在啟動伺服器, +當你用法國麵包火焰噴射器右鍵點擊一個實體時,它將會著火 40 tick! + +--- + +[PylonItem]: https://pylonmc.github.io/pylon-core/docs/javadoc/io/github/pylonmc/pylon/core/item/PylonItem.html +[PylonItemEntityInteractor]: https://pylonmc.github.io/pylon-core/docs/javadoc/io/github/pylonmc/pylon/core/item/base/PylonItemEntityInteractor.html diff --git a/docs/zh-TW/creating-addons/custom-items/advanced-lore.md b/docs/zh-TW/creating-addons/custom-items/advanced-lore.md new file mode 100644 index 0000000..8c8876b --- /dev/null +++ b/docs/zh-TW/creating-addons/custom-items/advanced-lore.md @@ -0,0 +1,247 @@ +# 進階敘述(Advanced lore) + +## 概述(Overview) + +讓我們看看物品敘述還能做些什麼。現在的版本相當無聊: + +![Boring baguette flamethrower lore](img/boring-baguette-flamethrower.png) + +--- + +## Minimessage + +敘述不只是純文字——它可以使用 **標籤(tags)**,例如 ``、``、``。 +這些標籤由 [MiniMessage](https://docs.advntr.dev/minimessage/index.html) 提供。 + +!!! info + 你可以在 [這裡](https://docs.advntr.dev/minimessage/format.html) 查看完整的 MiniMessage 標籤清單。 + +以下是一些使用 minimessage 的範例: + +### 基本顏色(Simple color) + +```yaml title="en.yml" +baguette_flamethrower: + name: "Baguette Flamethrower" + lore: |- + Use the power of baguettes to ignite the entities you look at +``` + +![simple-color](img/simple-color.png) + +### 十六進位顏色(Hex color) + +```yaml title="en.yml" +baguette_flamethrower: + name: "Baguette Flamethrower" + lore: |- + Use the power of <#ff6200>baguettes to ignite the entities you look at +``` + +![hex-color](img/hex-color.png) + +### 漸層(Gradient) + +```yaml title="en.yml" +baguette_flamethrower: + name: "Baguette Flamethrower" + lore: |- + Use the power of baguettes to ignite the entities you look at +``` + +![gradient](img/gradient.png) + +### 格式化(Formatting) + +```yaml title="en.yml" +baguette_flamethrower: + name: "Baguette Flamethrower" + lore: |- + Use the power of baguettes to ignite the entities you look at +``` + +![formatting](img/formatting.png) + +!!! success "最佳實踐(Best practice)" + **避免在物品名稱與敘述中使用過多顏色。** + 理想情況下,你不應像上面範例那樣強調「baguette」,因為這沒有實際必要。 + + 雖然很容易想用顏色強調,但在 Pylon 中物品種類繁多,這樣會造成混亂! + 建議只在**特別情況**下使用顏色或格式(粗體、斜體等)。 + 例如:放大鏡(Loupe)會用紅色標示「使用後該物品會被消耗」——這樣的警示才有意義。 + +--- + +## Pylon 的自訂標籤(Pylon's custom tags) + +樂趣還沒結束——Pylon 也加入了自己的 MiniMessage 標籤! +你已經見過 ``,它在整個系統中非常常見。 +另外還有兩個非常重要的標籤:``(指令)與 ``(屬性)。 + +!!! info + 你可以在 [這裡](TODO) 查看 Pylon 的自訂標籤完整列表。 + +### 指令(Instructions ``) + +這個標籤只是快速標示特定顏色文字的捷徑,我們通常用它來表示**操作指令**: + +```yaml title="en.yml" hl_lines="5" +baguette_flamethrower: + name: "Baguette Flamethrower" + lore: |- + Use the power of baguettes to ignite the entities you look at + Right click to ignite the entity you're looking at +``` + +![formatting](img/instruction.png) + +### 屬性(Attributes ``) + +這個標籤則用於標示物品屬性: + +```yaml title="en.yml" hl_lines="6" +baguette_flamethrower: + name: "Baguette Flamethrower" + lore: |- + Use the power of baguettes to ignite the entities you look at + Right click to ignite the entity you're looking at + Burn time: 2 seconds +``` + +!!! success "最佳實踐(Best practice)" + 請依序排列:**描述 → 指令 → 屬性**,這樣可讓所有物品風格一致。 + +![formatting](img/attribute.png) + +「但等一下!」你可能會說,「我們剛才不是設定了燃燒時間嗎?它不一定總是 2 秒啊!」 + +沒錯。這正是**占位符(placeholders)**登場的時候。 + +--- + +## 占位符(Placeholders) + +有時候,我們需要從程式中傳遞變數到敘述中,例如燃燒時間。 +這可以透過占位符完成。占位符是像 `%burn-time%` 這樣的標記,代表「這裡會被替換成實際值」。 + +`PylonItem` 類別中有個方法 `getPlaceholders`, +當你覆寫它時,可以回傳一個占位符清單,用於在敘述中替換內容。 +讓我們在 `BaguetteFlamethrower` 中實作這個方法: + +=== "Java" +```java title="BaguetteFlamethrower.java" hl_lines="13-18" +public class BaguetteFlamethrower extends PylonItem implements PylonItemEntityInteractor { + private final int burnTimeTicks = getSettings().getOrThrow("burn-time-ticks", Integer.class); + + public BaguetteFlamethrower(@NotNull ItemStack stack) { + super(stack); + } + + @Override + public void onUsedToRightClickEntity(@NotNull PlayerInteractEntityEvent event) { + event.getRightClicked().setFireTicks(burnTimeTicks); + } + + @Override + public @NotNull List getPlaceholders() { + return List.of( + PylonArgument.of("burn-time", burnTimeTicks / 20.0) + ); + } +} +``` +=== "Kotlin" +```kotlin title="BaguetteFlamethrower.kt" hl_lines="8-9" +class BaguetteFlamethrower(stack: ItemStack) : PylonItem(stack), PylonItemEntityInteractor { + private val burnTimeTicks = settings.getOrThrow("burn-time-ticks", Int::class.java) + + override fun onUsedToRightClickEntity(event: PlayerInteractEntityEvent) { + event.rightClicked.fireTicks = burnTimeTicks + } + + override fun getPlaceholders() = + listOf(PylonArgument.of("burn-time", burnTimeTicks / 20.0)) +} +``` + +現在我們就能在物品敘述中使用該占位符。 +占位符的語法是用 `%` 包住你設定的名稱,例如 `%burn-time%`: + +```yaml title="en.yml" hl_lines="6" +baguette_flamethrower: + name: "Baguette Flamethrower" + lore: |- + Use the power of baguettes to ignite the entities you look at + Right click to ignite the entity you're looking at + Burn time: %burn-time% seconds +``` + +![formatting](img/placeholder.png) + +!!! success "最佳實踐(Best practice)" + **請永遠使用占位符而非硬編值(hardcoded values)**, + 以確保敘述中顯示的數值永遠與實際一致。 + +--- + +### 單位(Units) + +最後一件事。 +我們目前手動加上了「seconds」,但其實 Pylon 有一個「單位 API」可以自動處理。 +它會根據數值自動格式化單位,非常簡單好用: + +=== "Java" +```java title="BaguetteFlamethrower.java" hl_lines="16" +public class BaguetteFlamethrower extends PylonItem implements PylonItemEntityInteractor { + private final int burnTimeTicks = getSettings().getOrThrow("burn-time-ticks", Integer.class); + + public BaguetteFlamethrower(@NotNull ItemStack stack) { + super(stack); + } + + @Override + public void onUsedToRightClickEntity(@NotNull PlayerInteractEntityEvent event) { + event.getRightClicked().setFireTicks(burnTimeTicks); + } + + @Override + public @NotNull List getPlaceholders() { + return List.of( + PylonArgument.of("burn-time", UnitFormat.SECONDS.format(burnTimeTicks / 20.0)) + ); + } +} +``` +=== "Kotlin" +```kotlin title="BaguetteFlamethrower.kt" hl_lines="9" +class BaguetteFlamethrower(stack: ItemStack) : PylonItem(stack), PylonItemEntityInteractor { + private val burnTimeTicks = settings.getOrThrow("burn-time-ticks", Int::class.java) + + override fun onUsedToRightClickEntity(event: PlayerInteractEntityEvent) { + event.rightClicked.fireTicks = burnTimeTicks + } + + override fun getPlaceholders() = + listOf(PylonArgument.of("burn-time", UnitFormat.SECONDS.format(burnTimeTicks / 20.0))) +} +``` + +```yaml title="en.yml" hl_lines="6" +baguette_flamethrower: + name: "Baguette Flamethrower" + lore: |- + Use the power of baguettes to ignite the entities you look at + Right click to ignite the entity you're looking at + Burn time: %burn-time% +``` + +![formatting](img/unit.png) + +!!! info + 你可以在 [這裡](https://pylonmc.github.io/pylon-core/docs/javadoc/io/github/pylonmc/pylon/core/util/gui/unit/UnitFormat.html) 查看 Pylon 的預設單位清單。 + +!!! success "最佳實踐(Best practice)" + **請使用單位 API,而非手動加上單位文字。** + 這樣所有物品的單位都能保持一致,數值格式也更統一。 + 否則有時候可能會出現「2.0」或「2」這種差異。 + 使用單位 API 可以確保所有顯示格式都相同。 diff --git a/docs/zh-TW/creating-addons/custom-items/full-code.md b/docs/zh-TW/creating-addons/custom-items/full-code.md new file mode 100644 index 0000000..baae91a --- /dev/null +++ b/docs/zh-TW/creating-addons/custom-items/full-code.md @@ -0,0 +1,234 @@ +# Full code + +## 法棍火焰噴射器 + +=== "Java" + ```java title="MyAddon.java" + NamespacedKey baguetteFlamethrowerKey = new NamespacedKey(this, "baguette_flamethrower"); + ItemStack baguetteFlamethrower = ItemStackBuilder.pylonItem(Material.BREAD, baguetteFlamethrowerKey) + .build(); + PylonItem.register(BaguetteFlamethrower.class, baguetteFlamethrower); + BasePages.FOOD.addItem(baguetteFlamethrowerKey); + ``` +=== "Kotlin" + ```kotlin title="MyAddon.kt" + val baguetteFlamethrowerKey = NamespacedKey(this, "baguette_flamethrower") + val baguetteFlamethrower = ItemStackBuilder.pylonItem(Material.BREAD, baguetteFlamethrowerKey) + .build() + PylonItem.register(baguetteFlamethrower) + BasePages.FOOD.addItem(baguetteFlamethrowerKey) + ``` + +[](tab break) + +=== "Java" + ```java title="BaguetteFlamethrower.java" + public class BaguetteFlamethrower extends PylonItem implements PylonItemEntityInteractor { + private final int burnTimeTicks = getSettings().getOrThrow("burn-time-ticks", Integer.class); + + public BaguetteFlamethrower(@NotNull ItemStack stack) { + super(stack); + } + + @Override + public void onUsedToRightClickEntity(@NotNull PlayerInteractEntityEvent event) { + event.getRightClicked().setFireTicks(burnTimeTicks); + } + + @Override + public @NotNull List getPlaceholders() { + return List.of( + PylonArgument.of("burn-time", UnitFormat.SECONDS.format(burnTimeTicks / 20.0)) + ); + } + } + ``` +=== "Kotlin" + ```kotlin title="BaguetteFlamethrower.kt" + class BaguetteFlamethrower(stack: ItemStack) : PylonItem(stack), PylonItemEntityInteractor { + private val burnTimeTicks = settings.getOrThrow("burn-time-ticks", Int::class.java) + + override fun onUsedToRightClickEntity(event: PlayerInteractEntityEvent) { + event.rightClicked.fireTicks = burnTimeTicks + } + + override fun getPlaceholders() = + listOf(PylonArgument.of("burn-time", UnitFormat.SECONDS.format(burnTimeTicks / 20.0))) + } + ``` + +```yaml title="en.yml" +baguette_flamethrower: + name: "法棍火焰噴射器" + lore: |- + 使用法棍的力量點燃你所注視的實體 + 右鍵點擊 點燃你所注視的實體 + 燃燒時間: %burn-time% +``` + +--- + +## 智慧法棍 + +=== "Java" + ```java title="MyAddon.java" + NamespacedKey baguetteOfWisdomKey = new NamespacedKey(this, "baguette_of_wisdom"); + ItemStack baguetteOfWisdom = ItemStackBuilder.pylonItem(Material.BREAD, baguetteOfWisdomKey) + .editPdc(pdc -> pdc.set( + BaguetteOfWisdom.STORED_XP_KEY, + PylonSerializers.INTEGER, + 0 + )) + .build(); + PylonItem.register(BaguetteOfWisdom.class, baguetteOfWisdom); + BasePages.FOOD.addItem(baguetteOfWisdomKey); + ``` +=== "Kotlin" + ```kotlin title="MyAddon.kt" + val baguetteOfWisdomKey = NamespacedKey(this, "baguette_of_wisdom") + val baguetteOfWisdom = ItemStackBuilder.pylonItem(Material.BREAD, baguetteOfWisdomKey) + .editPdc { pdc -> + pdc.set( + BaguetteOfWisdom.STORED_XP_KEY, + PylonSerializers.INTEGER, + 0 + ) + } + .build() + PylonItem.register(baguetteOfWisdom) + BasePages.FOOD.addItem(baguetteOfWisdomKey) + ``` + +[](tab break) + +=== "Java" + ```java title="BaguetteOfWisdom.java" + public class BaguetteOfWisdom extends PylonItem implements PylonInteractor { + public static final NamespacedKey STORED_XP_KEY = new NamespacedKey(MyAddon.getInstance(), "stored_xp"); + + private final int xpCapacity = getSettings().getOrThrow("xp-capacity", Integer.class); + + public BaguetteOfWisdom(@NotNull ItemStack stack) { + super(stack); + } + + @Override + public @NotNull List getPlaceholders() { + return List.of( + PylonArgument.of("xp_capacity", UnitFormat.EXPERIENCE.format(xpCapacity)), + PylonArgument.of("stored_xp", UnitFormat.EXPERIENCE.format(getStoredXp())) + ); + } + + @Override + public void onUsedToRightClick(@NotNull PlayerInteractEvent event) { + if (event.getPlayer().isSneaking()) { + // 1. Read how much XP we already have stored + int xp = getStoredXp(); + + // 2. Give all the XP to the player + event.getPlayer().giveExp(xp); + + // 3. Set the stored XP to 0 + setStoredXp(0); + } else { + // 1. Read how much XP we already have stored + int xp = getStoredXp(); + + // 2. Figure out how much XP we need to take to get to `xpCapacity` + int extraXpNeeded = xpCapacity - xp; + + // 3. Take as much XP from the player as we can to get there + int xpToTake = Math.min(event.getPlayer().calculateTotalExperiencePoints(), extraXpNeeded); + event.getPlayer().giveExp(-xpToTake); + + // 4. Set the new stored XP amount + setStoredXp(xp + xpToTake); + } + } + + public void setStoredXp(int xp) { + getStack().editPersistentDataContainer(pdc -> pdc.set( + STORED_XP_KEY, + PylonSerializers.INTEGER, + xp + )); + } + + public int getStoredXp() { + return getStack().getPersistentDataContainer().get( + STORED_XP_KEY, + PylonSerializers.INTEGER + ); + } + } + ``` +=== "Kotlin" + ```kotlin title="BaguetteOfWisdom.kt" + class BaguetteOfWisdom(stack: ItemStack) : PylonItem(stack), PylonInteractor { + companion object { + val STORED_XP_KEY = NamespacedKey(MyAddon.getInstance(), "stored_xp") + } + + private val xpCapacity = settings.getOrThrow("xp-capacity", Int::class.java) + + override fun getPlaceholders() = + listOf( + PylonArgument.of("xp_capacity", UnitFormat.EXPERIENCE.format(xpCapacity)), + PylonArgument.of("stored_xp", UnitFormat.EXPERIENCE.format(getStoredXp())) + ) + + override fun onUsedToRightClick(event: PlayerInteractEvent) { + if (event.player.isSneaking) { + // 1. Read how much XP we already have stored + val xp = storedXp + + // 2. Give all the XP to the player + event.player.giveExp(xp) + + // 3. Set the stored XP to 0 + storedXp = 0 + } else { + // 1. Read how much XP we already have stored + val xp = storedXp + + // 2. Figure out how much XP we need to take to get to `xpCapacity` + val extraXpNeeded = xpCapacity - xp + + // 3. Take as much XP from the player as we can to get there + val xpToTake = min(event.player.calculateTotalExperiencePoints(), extraXpNeeded) + event.player.giveExp(-xpToTake) + + // 4. Set the new stored XP amount + storedXp = xp + xpToTake + } + } + + var storedXp: Int + get() = stack.persistentDataContainer.get( + NamespacedKey(MyAddon.instance, "stored_xp"), + PylonSerializers.INTEGER + )!! + set(value) { + stack.editPersistentDataContainer { pdc -> + pdc.set( + NamespacedKey(MyAddon.instance, "stored_xp"), + PylonSerializers.INTEGER, + value + ) + } + } + } + ``` + +```yaml title="en.yml" +item: + baguette_of_wisdom: + name: "智慧法棍" + lore: |- + 使用法棍的力量來轉移經驗值 + 右鍵點擊 以經驗值充能 + 按住 Shift + 右鍵點擊 釋放儲存的經驗值 + 經驗值容量: %xp_capacity% + 已儲存的經驗值: %stored_xp% +``` diff --git a/docs/zh-TW/creating-addons/custom-items/img/attribute.png b/docs/zh-TW/creating-addons/custom-items/img/attribute.png new file mode 100644 index 0000000..3991c35 Binary files /dev/null and b/docs/zh-TW/creating-addons/custom-items/img/attribute.png differ diff --git a/docs/zh-TW/creating-addons/custom-items/img/baguette_of_wisdom_error.png b/docs/zh-TW/creating-addons/custom-items/img/baguette_of_wisdom_error.png new file mode 100644 index 0000000..b252a25 Binary files /dev/null and b/docs/zh-TW/creating-addons/custom-items/img/baguette_of_wisdom_error.png differ diff --git a/docs/zh-TW/creating-addons/custom-items/img/baguette_of_wisdom_success.png b/docs/zh-TW/creating-addons/custom-items/img/baguette_of_wisdom_success.png new file mode 100644 index 0000000..cbd3238 Binary files /dev/null and b/docs/zh-TW/creating-addons/custom-items/img/baguette_of_wisdom_success.png differ diff --git a/docs/zh-TW/creating-addons/custom-items/img/boring-baguette-flamethrower.png b/docs/zh-TW/creating-addons/custom-items/img/boring-baguette-flamethrower.png new file mode 100644 index 0000000..50fd7be Binary files /dev/null and b/docs/zh-TW/creating-addons/custom-items/img/boring-baguette-flamethrower.png differ diff --git a/docs/zh-TW/creating-addons/custom-items/img/formatting.png b/docs/zh-TW/creating-addons/custom-items/img/formatting.png new file mode 100644 index 0000000..37b9e69 Binary files /dev/null and b/docs/zh-TW/creating-addons/custom-items/img/formatting.png differ diff --git a/docs/zh-TW/creating-addons/custom-items/img/gradient.png b/docs/zh-TW/creating-addons/custom-items/img/gradient.png new file mode 100644 index 0000000..162916a Binary files /dev/null and b/docs/zh-TW/creating-addons/custom-items/img/gradient.png differ diff --git a/docs/zh-TW/creating-addons/custom-items/img/hex-color.png b/docs/zh-TW/creating-addons/custom-items/img/hex-color.png new file mode 100644 index 0000000..45145f4 Binary files /dev/null and b/docs/zh-TW/creating-addons/custom-items/img/hex-color.png differ diff --git a/docs/zh-TW/creating-addons/custom-items/img/instruction.png b/docs/zh-TW/creating-addons/custom-items/img/instruction.png new file mode 100644 index 0000000..c55d3a0 Binary files /dev/null and b/docs/zh-TW/creating-addons/custom-items/img/instruction.png differ diff --git a/docs/zh-TW/creating-addons/custom-items/img/placeholder.png b/docs/zh-TW/creating-addons/custom-items/img/placeholder.png new file mode 100644 index 0000000..cd83106 Binary files /dev/null and b/docs/zh-TW/creating-addons/custom-items/img/placeholder.png differ diff --git a/docs/zh-TW/creating-addons/custom-items/img/simple-color.png b/docs/zh-TW/creating-addons/custom-items/img/simple-color.png new file mode 100644 index 0000000..b434c26 Binary files /dev/null and b/docs/zh-TW/creating-addons/custom-items/img/simple-color.png differ diff --git a/docs/zh-TW/creating-addons/custom-items/img/unit.png b/docs/zh-TW/creating-addons/custom-items/img/unit.png new file mode 100644 index 0000000..78fe0e3 Binary files /dev/null and b/docs/zh-TW/creating-addons/custom-items/img/unit.png differ diff --git a/docs/zh-TW/creating-addons/custom-items/making-items-configurable.md b/docs/zh-TW/creating-addons/custom-items/making-items-configurable.md new file mode 100644 index 0000000..d0f63b7 --- /dev/null +++ b/docs/zh-TW/creating-addons/custom-items/making-items-configurable.md @@ -0,0 +1,103 @@ +# 讓物品可設定 + +## 概述 + +讓我們新增一個設定檔,讓伺服器擁有者能夠調整法棍火焰噴射器點燃實體的時間長度。 + +--- + +## 建立設定檔 + +如果你查看伺服器的 Pylon 設定目錄 (`run/plugins/PylonCore/settings/pylonbase`) 你會發現每個物品與方塊都有各自的設定檔。 + +例如, `bandage.yml` contains: + +```yaml title="bandage.yml" +consume-seconds: 1.25 +heal-amount: 4.0 +``` + +我們想要新增自己的設定檔,讓你可以選擇法棍火焰噴射器點燃實體的時間長度。 + +建立一個新檔案 `resources/settings/baguette_flamethrower.yml` 並加入以下內容: + +```yaml title="baguette_flamethrower.yml" +burn-time-ticks: 40 +``` + +!!! success "最佳實踐" + 當你的設定項目代表一個數值時,請在名稱中包含單位。 如果我們只將其命名為 `burn-time`, 使用者可能不知道該值的單位是 tick,並誤以為是秒或其他單位。 + +Pylon 會自動將你的所有設定檔複製到 `run/plugins/PylonCore/settings/` 當設定檔首次被使用時。 + +--- + +## 使用設定檔 + +要讀取 `burn-time-ticks` 值,我們可以使用 `getSettings().getOrThrow(...)`: + +=== "Java" + ```java title="BaguetteFlamethrower.java" hl_lines="2" + public class BaguetteFlamethrower extends PylonItem implements PylonItemEntityInteractor { + private final int burnTimeTicks = getSettings().getOrThrow("burn-time-ticks", Integer.class); + + public BaguetteFlamethrower(@NotNull ItemStack stack) { + super(stack); + } + + @Override + public void onUsedToRightClickEntity(@NotNull PlayerInteractEntityEvent event) { + event.getRightClicked().setFireTicks(40); + } + } + ``` + !!! question "Pylon 如何知道要使用哪個設定檔?" + 設定檔實際上是依照 **key** 分別設定的,而不是依照物品本身。 當呼叫 `getSettings()`, 它只是根據物品的 key 尋找對應的設定檔! 這只是 `Settings.get(getKey())`. 如果你使用 `Settings.get(new NamespacedKey(MyAddon.getInstance(), "buffoon"))` 那麼 `buffoon.yml` 設定檔就會被讀取。 +=== "Kotlin" + ```kotlin title="BaguetteFlamethrower.kt" hl_lines="2" + class BaguetteFlamethrower(stack: ItemStack) : PylonItem(stack), PylonItemEntityInteractor { + private val burnTimeTicks = settings.getOrThrow("burn-time-ticks", Int::class.java) + + override fun onUsedToRightClickEntity(event: PlayerInteractEntityEvent) { + event.rightClicked.fireTicks = 40 + } + } + ``` + !!! question "Pylon 如何知道要使用哪個設定檔?" + 設定檔實際上是依照 **key** 分別設定的,而不是依照物品本身。 When accessing `settings`, 它只是根據物品的 key 尋找對應的設定檔! 這只是 `Settings.get(key)`. 如果你使用 `Settings.get(NamespacedKey(MyAddon.instance, "buffoon"))` 那麼 `buffoon.yml` 設定檔就會被讀取。 + +!!! question "為什麼我們使用 `getOrThrow` 而不是 `get`?" + Settings 也有一個 `get` 方法。 這個方法在找不到 key 時只會回傳 null, 因此僅應在不一定需要該 key 的情況下使用。 如果使用 `getOrThrow`,當缺少 key 時會丟出清晰的例外,並包含缺少的 key 與位置資訊。 + +現在我們就可以使用該設定值來點燃實體了! + +=== "Java" + ```java title="BaguetteFlamethrower.java" hl_lines="10" + public class BaguetteFlamethrower extends PylonItem implements PylonItemEntityInteractor { + private final int burnTimeTicks = getSettings().getOrThrow("burn-time-ticks", Integer.class); + + public BaguetteFlamethrower(@NotNull ItemStack stack) { + super(stack); + } + + @Override + public void onUsedToRightClickEntity(@NotNull PlayerInteractEntityEvent event) { + event.getRightClicked().setFireTicks(burnTimeTicks); + } + } + ``` +=== "Kotlin" + ```kotlin title="BaguetteFlamethrower.kt" hl_lines="5" + class BaguetteFlamethrower(stack: ItemStack) : PylonItem(stack), PylonItemEntityInteractor { + private val burnTimeTicks = settings.getOrThrow("burn-time-ticks", Int::class.java) + + override fun onUsedToRightClickEntity(event: PlayerInteractEntityEvent) { + event.rightClicked.fireTicks = burnTimeTicks + } + } + ``` + +試著更改設定值,燃燒時間也會隨之改變。 + +!!! danger + 執行伺服器任務時,會刪除 `plugins` 資料夾內的所有內容, 意味著你在那裡修改的設定檔將會被覆蓋。 建議你直接修改預設值,或手動啟動伺服器,以避免在重啟時刪除 `plugins` 資料夾。 diff --git a/docs/zh-TW/creating-addons/custom-items/persistent-data.md b/docs/zh-TW/creating-addons/custom-items/persistent-data.md new file mode 100644 index 0000000..207f5bb --- /dev/null +++ b/docs/zh-TW/creating-addons/custom-items/persistent-data.md @@ -0,0 +1,762 @@ +# 持久化資料 + +## 物品建構子 + +你可能會以為法棍火焰噴射器的建構子在每次建立物品時都會被呼叫,例如每次我們合成一個法棍。但事實上**並不是如此**。 + +重要的是要知道,**物品的建構子可以在任意時刻被呼叫,而不僅僅是在物品被「建立」時**。 + +??? question "等等……為什麼建構子可以任意被呼叫?" + *(sigh)* 這有點複雜。簡單來說…… + + Pylon 無法隨時追蹤每一個物品的狀態。 If we could actually modify the core server code, then this would be possible. Instead, Pylon only *temporarily* track Pylon items. + + 例如,當我們使用法棍火焰噴射器右鍵點擊一個實體時, Pylon listens to the [PlayerInteractEntityEvent] and sees that an entity has been right clicked with an item. In order to figure out what item it is - and if it's even a Pylon item at all - Pylon has to look at the key stored inside the item. But even if Pylon knows the key, it still don't know much about the item. Does the item implement [PylonItemEntityInteractor]? If so, Pylon needs to call its `onUsedToRightClickEntity`. + + 這時候建構子就會派上用場。 We can look up what class the item corresponds to - in this case BaguetteFlamethrower - and create a new instance of it. Then, we can call the `onUsedToRightClickEntity` method. + +這一切的意思是:**這個類別可以在任何時間被建立或銷毀**。 任何我們儲存在欄位中的資料都是暫時的。 + +But suppose we want to store the charge level of a portable battery. If we can't store the charge level in a field, where the hell *can* we store it? + +--- + +## 持久化資料 containers (PDCs) + +!!! info "PDC 是由 Paper 新增的功能,不是 Pylon。 " + 我們在這裡介紹它,因為在 Pylon 中它幾乎被用於所有持久化資料的儲存(包括方塊與實體)。 + +[持久化資料 container]s (PDCs) 是一種在物品上持久儲存任意資料的方法。 You can think of them as a similar sort of thing to YAML. You can 'set' keys and you can 'get' keys, and the keys can have different kinds of data - like strings, ints, or even other PDCs. + +以追蹤可攜式電池的充電等級為例, We can store the charge level in the `charge_level` key in the item's PDC. It will then be saved when the item is put in a chest, or when the server restarts. + +If this is all a bit confusing - don't worry, an example should make it clearer. + +--- + +## 智慧法棍 + +### 概念 + +[研究顯示](https://www.youtube.com/watch?v=dQw4w9WgXcQ&list=RDdQw4w9WgXcQ) that those who eat baguettes have a 68% higher IQ on average. 讓我們以此作為外掛模組的靈感。 We'll create a 'Baguette of Wisdom' which allows you to transfer experience from one player to another. + +計畫如下: + +- The baguette has a maximum XP capacity +- You can right click with the baguette to 'charge' it with experience +- You can shift right click with the baguette to discharge the experience + +To do this, we're going to need to keep track of how much experience the baguette has inside of it. + +### Creating the item + +You know the drill from last time: + +=== "Java" + ```java title="MyAddon.java" + NamespacedKey baguetteOfWisdomKey = new NamespacedKey(this, "baguette_of_wisdom"); + ItemStack baguetteOfWisdom = ItemStackBuilder.pylonItem(Material.BREAD, baguetteOfWisdomKey) + .build(); + PylonItem.register(BaguetteOfWisdom.class, baguetteOfWisdom); + BasePages.FOOD.addItem(baguetteOfWisdomKey); + ``` +=== "Kotlin" + ```kotlin title="MyAddon.kt" + val baguetteOfWisdomKey = NamespacedKey(this, "baguette_of_wisdom") + val baguetteOfWisdom = ItemStackBuilder.pylonItem(Material.BREAD, baguetteOfWisdomKey) + .build() + PylonItem.register(baguetteOfWisdom) + BasePages.FOOD.addItem(baguetteOfWisdomKey) + ``` + +[](this invisible link is important to break up the two code tabs above and below, otherwise the tabs will all be on one line) + +=== "Java" + ```java title="BaguetteOfWisdom.java" + public class BaguetteOfWisdom extends PylonItem { + public BaguetteOfWisdom(@NotNull ItemStack stack) { + super(stack); + } + } + ``` +=== "Kotlin" + ```kotlin title="BaguetteOfWisdom.kt" + class BaguetteOfWisdom(stack: ItemStack) : PylonItem(stack) + ``` + +```yaml title="en.yml" +item: + baguette_of_wisdom: + name: "Baguette of Wisdom" + lore: |- + Use the power of baguettes to transfer XP +``` + +### 新增經驗值容量設定 + +Now, let's add a config value for the max XP capacity: + +=== "Java" + ```java title="BaguetteOfWisdom.java" hl_lines="2" + public class BaguetteOfWisdom extends PylonItem { + private final int xpCapacity = getSettings().getOrThrow("xp-capacity", Integer.class); + + public BaguetteOfWisdom(@NotNull ItemStack stack) { + super(stack); + } + } + ``` +=== "Kotlin" + ```kotlin title="BaguetteOfWisdom.kt" hl_lines="2" + class BaguetteOfWisdom(stack: ItemStack) : PylonItem(stack) { + private val xpCapacity: Int = settings.getOrThrow("xp-capacity", Int::class.java) + } + ``` + +```yaml title="baguette_of_wisdom.yml" +xp-capacity: 200 +``` + +We'll be using this later. + +### 改善說明文字(lore) + +Let's add some instructions to the lore, and attributes for the max charge and current charge: + +```yaml title="en.yml" hl_lines="6-9" +item: + baguette_of_wisdom: + name: "Baguette of Wisdom" + lore: |- + Use the power of baguettes to transfer XP + Right click to charge with XP + Shift right click to discharge stored XP + XP capacity: %xp_capacity% + Stored XP: %stored_xp% +``` + +=== "Java" + ```java title="BaguetteOfWisdom.java" hl_lines="8-14" + public class BaguetteOfWisdom extends PylonItem { + private final int xpCapacity = getSettings().getOrThrow("xp-capacity", Integer.class); + + public BaguetteOfWisdom(@NotNull ItemStack stack) { + super(stack); + } + + @Override + public @NotNull List getPlaceholders() { + return List.of( + PylonArgument.of("xp_capacity", UnitFormat.EXPERIENCE.format(xpCapacity)) + // TODO add stored_xp placeholder + ); + } + } + ``` +=== "Kotlin" + ```kotlin title="BaguetteOfWisdom.kt" hl_lines="4-7" + class BaguetteOfWisdom(stack: ItemStack) : PylonItem(stack) { + private val xpCapacity: Int = settings.getOrThrow("xp-capacity", Int::class.java) + + override fun getPlaceholders() = listOf( + PylonArgument.of("xp_capacity", UnitFormat.EXPERIENCE.format(xpCapacity)) + // TODO add stored_xp placeholder + ) + } + ``` + +All of this should be familiar from the [advanced lore] section. + +### 充能/放能機制 + +Next, let's allow the player to charge by right clicking, and discharge by shift right clicking. + +We can use the [PylonInteractor] class to do this: + +=== "Java" + ```java title="BaguetteOfWisdom.java" hl_lines="1 16-23" + public class BaguetteOfWisdom extends PylonItem implements PylonInteractor { + private final int xpCapacity = getSettings().getOrThrow("xp-capacity", Integer.class); + + public BaguetteOfWisdom(@NotNull ItemStack stack) { + super(stack); + } + + @Override + public @NotNull List getPlaceholders() { + return List.of( + PylonArgument.of("xp_capacity", UnitFormat.EXPERIENCE.format(xpCapacity)) + // TODO add stored_xp placeholder + ); + } + + @Override + public void onUsedToRightClick(@NotNull PlayerInteractEvent event) { + if (event.getPlayer().isSneaking()) { + // TODO discharge logic + } else { + // TODO charge logic + } + } + } + ``` +=== "Kotlin" + ```kotlin title="BaguetteOfWisdom.kt" hl_lines="1 9-15" + + class BaguetteOfWisdom(stack: ItemStack) : PylonItem(stack), PylonInteractor { + private val xpCapacity: Int = settings.getOrThrow("xp-capacity", Int::class.java) + + override fun getPlaceholders() = listOf( + PylonArgument.of("xp_capacity", UnitFormat.EXPERIENCE.format(xpCapacity)) + // TODO add stored_xp placeholder + ) + + override fun onUsedToRightClick(event: PlayerInteractEvent) { + if (event.player.isSneaking) { + // TODO discharge logic + } else { + // TODO charge logic + } + } + } + ``` + +### 充能邏輯 + +Let's now do the **charge** logic. In order to charge a Baguette of Wisdom, we need to store its charge level. As mentioned beforehand, we can use the item's [persistent data container] to do this. To start with, let's just set the charge level to 50: + +=== "Java" + ```java title="BaguetteOfWisdom.java" hl_lines="6-10" + @Override + public void onUsedToRightClick(@NotNull PlayerInteractEvent event) { + if (event.getPlayer().isSneaking()) { + // TODO discharge logic + } else { + getStack().editPersistentDataContainer(pdc -> pdc.set( + new NamespacedKey(MyAddon.getInstance(), "stored_xp"), + PylonSerializers.INTEGER, + 50 + )); + } + } + ``` +=== "Kotlin" + ```kotlin title="BaguetteOfWisdom.kt" hl_lines="5-11" + override fun onUsedToRightClick(event: PlayerInteractEvent) { + if (event.player.isSneaking) { + // TODO discharge logic + } else { + stack.editPersistentDataContainer { pdc -> + pdc.set( + NamespacedKey(MyAddon.instance, "stored_xp"), + PylonSerializers.INTEGER, + 50 + ) + } + } + } + ``` + +As you can see, we need to provide three things to set a PDC value: the **key**, the **serializer**, and the **value**. + +The serializer is just a 'thing' that describes how to convert your type into a more primitive type that can be stored on disk - we won't go into details. You need a serializer for every type that you want to store - so you can't store, for example, `MyAddon` in a persistent data container as there is no serializer for it and it doesn't make sense to create one anyway. + +!!! info "You can find a full list of serializers [here](https://pylonmc.github.io/pylon-core/docs/javadoc/io/github/pylonmc/pylon/core/datatypes/PylonSerializers.html)" + +Ok. But what we really need to do is 'top up' the stored xp using the player's experience: + +1. Read how much XP we already have stored +2. Figure out how much XP we need to take to get to `xpCapacity` +3. Take as much XP from the player as we can to get there +4. Set the new XP amount + +=== "Java" + ```java title="BaguetteOfWisdom.java" hl_lines="6-24" + @Override + public void onUsedToRightClick(@NotNull PlayerInteractEvent event) { + if (event.getPlayer().isSneaking()) { + // TODO discharge logic + } else { + // 1. Read how much XP we already have stored + int xp = getStack().getPersistentDataContainer().get( + new NamespacedKey(MyAddon.getInstance(), "stored_xp"), + PylonSerializers.INTEGER + ); + + // 2. Figure out how much XP we need to take to get to `xpCapacity` + int extraXpNeeded = xpCapacity - xp; + + // 3. Take as much XP from the player as we can to get there + int xpToTake = Math.min(event.getPlayer().calculateTotalExperiencePoints(), extraXpNeeded); + event.getPlayer().giveExp(-xpToTake); + + // 4. Set the new stored XP amount + getStack().editPersistentDataContainer(pdc -> pdc.set( + new NamespacedKey(MyAddon.getInstance(), "stored_xp"), + PylonSerializers.INTEGER, + xp + xpToTake + )); + } + } + ``` +=== "Kotlin" + ```kotlin title="BaguetteOfWisdom.kt" hl_lines="5-25" + override fun onUsedToRightClick(event: PlayerInteractEvent) { + if (event.player.isSneaking) { + // TODO discharge logic + } else { + // 1. Read how much XP we already have stored + val xp = stack.persistentDataContainer.get( + NamespacedKey(MyAddon.instance, "stored_xp"), + PylonSerializers.INTEGER + )!! + + // 2. Figure out how much XP we need to take to get to `xpCapacity` + val extraXpNeeded = xpCapacity - xp + + // 3. Take as much XP from the player as we can to get there + val xpToTake = min(event.player.calculateTotalExperiencePoints(), extraXpNeeded) + event.player.giveExp(-xpToTake) + + // 4. Set the new stored XP amount + stack.editPersistentDataContainer { pdc -> + pdc.set( + NamespacedKey(MyAddon.instance, "stored_xp"), + PylonSerializers.INTEGER, + xp + xpToTake + ) + } + } + } + ``` + +### 放能邏輯 + +And now for the discharge logic, which is quite similar: + +=== "Java" + ```java title="BaguetteOfWisdom.java" hl_lines="4-18" + @Override + public void onUsedToRightClick(@NotNull PlayerInteractEvent event) { + if (event.getPlayer().isSneaking()) { + // 1. Read how much XP we have stored + int xp = getStack().getPersistentDataContainer().get( + new NamespacedKey(MyAddon.getInstance(), "stored_xp"), + PylonSerializers.INTEGER + ); + + // 2. Give all the XP to the player + event.getPlayer().giveExp(xp); + + // 3. Set the stored XP to 0 + getStack().editPersistentDataContainer(pdc -> pdc.set( + new NamespacedKey(MyAddon.getInstance(), "stored_xp"), + PylonSerializers.INTEGER, + 0 + )); + } else { + ... + } + } + ``` +=== "Kotlin" + ```kotlin title="BaguetteOfWisdom.kt" hl_lines="3-19" + override fun onUsedToRightClick(event: PlayerInteractEvent) { + if (event.player.isSneaking) { + // 1. Read how much XP we have stored + val xp = stack.persistentDataContainer.get( + NamespacedKey(MyAddon.instance, "stored_xp"), + PylonSerializers.INTEGER + )!! + + // 2. Give all the XP to the player + event.player.giveExp(xp) + + // 3. Set the stored XP to 0 + stack.editPersistentDataContainer { pdc -> + pdc.set( + NamespacedKey(MyAddon.instance, "stored_xp"), + PylonSerializers.INTEGER, + 0 + ) + } + } else { + ... + } + } + ``` + +### 新增佔位符(placeholder) + +Finally, let's add in the placeholder for the stored charge: + +=== "Java" + ```java title="BaguetteOfWisdom.java" hl_lines="5-10" + @Override + public @NotNull List getPlaceholders() { + return List.of( + PylonArgument.of("xp_capacity", UnitFormat.EXPERIENCE.format(xpCapacity)), + PylonArgument.of("stored_xp", UnitFormat.EXPERIENCE.format( + getStack().getPersistentDataContainer().get( + new NamespacedKey(MyAddon.getInstance(), "stored_xp"), + PylonSerializers.INTEGER + )) + ) + ); + } + ``` +=== "Kotlin" + ```kotlin title="BaguetteOfWisdom.kt" hl_lines="3-8" + override fun getPlaceholders() = listOf( + PylonArgument.of("xp_capacity", UnitFormat.EXPERIENCE.format(xpCapacity)), + PylonArgument.of("stored_xp", UnitFormat.EXPERIENCE.format( + stack.persistentDataContainer.get( + NamespacedKey(MyAddon.instance, "stored_xp"), + PylonSerializers.INTEGER + )!! + )) + ) + ``` + +### 測試功能 + +Now let's start the server and test our glorious new item. Try giving yourself a Baguette of Wisdom. + +Obviously, as I am an expert programmer, it will work perfectly the first time and nothing will go wro- *huh*? + +![Baguette of wisdom error](img/baguette_of_wisdom_error.png) + +What nonsense is this? Have the brits sabotaged us again?! + +When something like this happens, your first port of call should always be the server console to see if any errors have been logged. And indeed, if you have a look in the console you should find the following error: + +```title="console" +[12:51:22 WARN]: java.lang.NullPointerException: Cannot invoke "java.lang.Float.floatValue()" because the return value of "io.papermc.paper.persistence.PersistentDataContainerView.get(org.bukkit.NamespacedKey, org.bukkit.persistence.PersistentDataType)" is null +[12:51:22 WARN]: at my-addon-MODIFIED-1757418660070.jar//io.github.pylonmc.myaddon.BaguetteOfWisdom.getPlaceholders(BaguetteOfWisdom.java:28) +[12:51:22 WARN]: at pylon-core-0.11.2.jar//io.github.pylonmc.pylon.core.guide.button.ItemButton.getItemProvider(ItemButton.kt:47) +[12:51:22 WARN]: at xyz.xenondevs.invui.gui.SlotElement$ItemSlotElement.getItemStack(SlotElement.java:44) +[12:51:22 WARN]: at xyz.xenondevs.invui.window.AbstractWindow.redrawItem(AbstractWindow.java:109) +[12:51:22 WARN]: at xyz.xenondevs.invui.window.AbstractSingleWindow.initItems(AbstractSingleWindow.java:58) +[12:51:22 WARN]: at xyz.xenondevs.invui.window.AbstractWindow.open(AbstractWindow.java:279) +[12:51:22 WARN]: at xyz.xenondevs.invui.window.AbstractWindow$AbstractBuilder.open(AbstractWindow.java:679) +[12:51:22 WARN]: at pylon-core-0.11.2.jar//io.github.pylonmc.pylon.core.guide.pages.base.GuidePage.open(GuidePage.kt:28) +[12:51:22 WARN]: at pylon-core-0.11.2.jar//io.github.pylonmc.pylon.core.guide.button.PageButton.handleClick(PageButton.kt:38) +[12:51:22 WARN]: at xyz.xenondevs.invui.gui.AbstractGui.handleClick(AbstractGui.java:95) +[12:51:22 WARN]: at xyz.xenondevs.invui.window.AbstractSingleWindow.handleClick(AbstractSingleWindow.java:84) +[12:51:22 WARN]: at xyz.xenondevs.invui.window.AbstractWindow.handleClickEvent(AbstractWindow.java:199) +[12:51:22 WARN]: at xyz.xenondevs.invui.window.WindowManager.handleInventoryClick(WindowManager.java:117) +[12:51:22 WARN]: at co.aikar.timings.TimedEventExecutor.execute(TimedEventExecutor.java:80) +[12:51:22 WARN]: at org.bukkit.plugin.RegisteredListener.callEvent(RegisteredListener.java:71) +[12:51:22 WARN]: at io.papermc.paper.plugin.manager.PaperEventManager.callEvent(PaperEventManager.java:54) +[12:51:22 WARN]: at io.papermc.paper.plugin.manager.PaperPluginManagerImpl.callEvent(PaperPluginManagerImpl.java:131) +[12:51:22 WARN]: at org.bukkit.plugin.SimplePluginManager.callEvent(SimplePluginManager.java:628) +[12:51:22 WARN]: at net.minecraft.server.network.ServerGamePacketListenerImpl.handleContainerClick(ServerGamePacketListenerImpl.java:3208) +[12:51:22 WARN]: at net.minecraft.network.protocol.game.ServerboundContainerClickPacket.handle(ServerboundContainerClickPacket.java:59) +[12:51:22 WARN]: at net.minecraft.network.protocol.game.ServerboundContainerClickPacket.handle(ServerboundContainerClickPacket.java:14) +[12:51:22 WARN]: at net.minecraft.network.protocol.PacketUtils.lambda$ensureRunningOnSameThread$0(PacketUtils.java:29) +[12:51:22 WARN]: at net.minecraft.server.TickTask.run(TickTask.java:18) +[12:51:22 WARN]: at net.minecraft.util.thread.BlockableEventLoop.doRunTask(BlockableEventLoop.java:155) +[12:51:22 WARN]: at net.minecraft.util.thread.ReentrantBlockableEventLoop.doRunTask(ReentrantBlockableEventLoop.java:24) +[12:51:22 WARN]: at net.minecraft.server.MinecraftServer.doRunTask(MinecraftServer.java:1450) +[12:51:22 WARN]: at net.minecraft.server.MinecraftServer.doRunTask(MinecraftServer.java:176) +[12:51:22 WARN]: at net.minecraft.util.thread.BlockableEventLoop.pollTask(BlockableEventLoop.java:129) +[12:51:22 WARN]: at net.minecraft.server.MinecraftServer.pollTaskInternal(MinecraftServer.java:1430) +[12:51:22 WARN]: at net.minecraft.server.MinecraftServer.pollTask(MinecraftServer.java:1424) +[12:51:22 WARN]: at net.minecraft.util.thread.BlockableEventLoop.managedBlock(BlockableEventLoop.java:139) +[12:51:22 WARN]: at net.minecraft.server.MinecraftServer.managedBlock(MinecraftServer.java:1381) +[12:51:22 WARN]: at net.minecraft.server.MinecraftServer.waitUntilNextTick(MinecraftServer.java:1389) +[12:51:22 WARN]: at net.minecraft.server.MinecraftServer.runServer(MinecraftServer.java:1266) +[12:51:22 WARN]: at net.minecraft.server.MinecraftServer.lambda$spin$2(MinecraftServer.java:310) +``` + +Wow. That's a fat error. But the lines we are most interested in are right at the top: + +```title="console" +[12:51:22 WARN]: java.lang.NullPointerException: Cannot invoke "java.lang.Float.floatValue()" because the return value of "io.papermc.paper.persistence.PersistentDataContainerView.get(org.bukkit.NamespacedKey, org.bukkit.persistence.PersistentDataType)" is null +[12:51:22 WARN]: at my-addon-MODIFIED-1757418660070.jar//io.github.pylonmc.myaddon.BaguetteOfWisdom.getPlaceholders(BaguetteOfWisdom.java:28) +``` + +So it looks like the error occurred on line 28, in the `getPlaceholders` function, where we try to read from the persistent data container. Apparently, the 'stored_xp' key couldn't be found in the PDC, because the call to `getStack().getPersistentDataContainer().get(...)` returned null. + +!!! question "Wait, why did `getPlaceholders` get called and error *now*? We haven't given ourselves the item yet." + Simply put, the guide also needs to call `getPlaceholders` to display the item to you. The error only appears once you open the guide - or once you give yourself the item with `/py give`. + +!!! Note "Null safety" + If you've been following along in Kotlin, you may have noticed that your error is different from the one above. Additionally, if you played around with the code a bit, you may have noticed that the Kotlin code refuses to compile unless you add a `!!` after the call to `get(...)` in the `getPlaceholders` function. This is because Kotlin actually tracks nulls in the type system, and will error during compile time instead of run time if you try to use a potentially null value without checking for null first. The `!!` tells the compiler, "hey, I know what I'm doing, this value will never be null." This is one of the advantages of using Kotlin over Java, as it can help catch potential null pointer exceptions before they even happen, and is one of the reasons why Pylon Core is written in Kotlin. In this situation, an Elvis operator (`?:`) or a call to `getOrDefault` would have been more appropriate, but for the purposes of this tutorial, we will leave it as is. + +This actually makes perfect sense if you think about it. At no point do we set a default value for the stored XP, so of course any call to get it will return null. + +### 新增預設值 + +To add a default value for stored XP to the PDC, we can modify the itemstack itself when we create it: + +=== "Java" + ```java title="MyAddon.java" hl_lines="3-7" + NamespacedKey baguetteOfWisdomKey = new NamespacedKey(this, "baguette_of_wisdom"); + ItemStack baguetteOfWisdom = ItemStackBuilder.pylonItem(Material.BREAD, baguetteOfWisdomKey) + .editPdc(pdc -> pdc.set( + new NamespacedKey(this, "stored_xp"), + PylonSerializers.INTEGER, + 0 + )) + .build(); + PylonItem.register(BaguetteOfWisdom.class, baguetteOfWisdom); + BasePages.FOOD.addItem(baguetteOfWisdomKey); + ``` +=== "Kotlin" + ```kotlin title="MyAddon.kt" hl_lines="3-9" + val baguetteOfWisdomKey = NamespacedKey(this, "baguette_of_wisdom") + val baguetteOfWisdom = ItemStackBuilder.pylonItem(Material.BREAD, baguetteOfWisdomKey) + .editPdc { pdc -> + pdc.set( + NamespacedKey(this, "stored_xp"), + PylonSerializers.INTEGER, + 0 + ) + } + .build() + PylonItem.register(baguetteOfWisdom) + BasePages.FOOD.addItem(baguetteOfWisdomKey) + ``` + +Now let's try again. + +![Baguette of wisdom success](img/baguette_of_wisdom_success.png) + +Ah, perfect! + +One last thing left to do... + +### 程式整理與優化 + +智慧法棍 works, but there are some improvements we can make. + +First, we could pull out the get/set code into their own functions: + +=== "Java" + ```java title="BaguetteOfWisdom.java" hl_lines="4-10 12-17" + public class BaguetteOfWisdom extends PylonItem implements PylonInteractor { + ... + + public void setStoredXp(int xp) { + getStack().editPersistentDataContainer(pdc -> pdc.set( + new NamespacedKey(MyAddon.getInstance(), "stored_xp"), + PylonSerializers.INTEGER, + xp + )); + } + + public int getStoredXp() { + return getStack().getPersistentDataContainer().get( + new NamespacedKey(MyAddon.getInstance(), "stored_xp"), + PylonSerializers.INTEGER + ); + } + } + ``` +=== "Kotlin" + ```kotlin title="BaguetteOfWisdom.kt" hl_lines="4-17" + class BaguetteOfWisdom(stack: ItemStack) : PylonItem(stack), PylonInteractor { + ... + + var storedXp: Int + get() = stack.persistentDataContainer.get( + NamespacedKey(MyAddon.instance, "stored_xp"), + PylonSerializers.INTEGER + )!! + set(value) { + stack.editPersistentDataContainer { pdc -> + pdc.set( + NamespacedKey(MyAddon.instance, "stored_xp"), + PylonSerializers.INTEGER, + value + ) + } + } + } + ``` + + !!! Note "Delegate" + Alternatively, Pylon provides a property delegate for persistent data values that can be used to simplify this even further: + + ```kotlin title="BaguetteOfWisdom.kt" hl_lines="4-8" + class BaguetteOfWisdom(stack: ItemStack) : PylonItem(stack), PylonInteractor { + ... + + var storedXp: Int by persistentData( + NamespacedKey(MyAddon.instance, "stored_xp"), + PylonSerializers.INTEGER, + 0 + ) + } + ``` + +And now, we can use these functions in the rest of the code, which is much cleaner: + +=== "Java" + ```java title="BaguetteOfWisdom.java" hl_lines="9 17 23 26 36" + public class BaguetteOfWisdom extends PylonItem implements PylonInteractor { + + ... + + @Override + public @NotNull List getPlaceholders() { + return List.of( + PylonArgument.of("xp_capacity", UnitFormat.EXPERIENCE.format(xpCapacity)), + PylonArgument.of("stored_xp", UnitFormat.EXPERIENCE.format(getStoredXp())) + ); + } + + @Override + public void onUsedToRightClick(@NotNull PlayerInteractEvent event) { + if (event.getPlayer().isSneaking()) { + // 1. Read how much XP we already have stored + int xp = getStoredXp(); + + // 2. Give all the XP to the player + event.getPlayer().giveExp(xp); + + // 3. Set the stored XP to 0 + setStoredXp(0); + } else { + // 1. Read how much XP we already have stored + int xp = getStoredXp(); + + // 2. Figure out how much XP we need to take to get to `xpCapacity` + int extraXpNeeded = xpCapacity - xp; + + // 3. Take as much XP from the player as we can to get there + int xpToTake = Math.min(event.getPlayer().calculateTotalExperiencePoints(), extraXpNeeded); + event.getPlayer().giveExp(-xpToTake); + + // 4. Set the new stored XP amount + setStoredXp(xp + xpToTake); + } + } + + ... + } + ``` +=== "Kotlin" + + ```kotlin title="BaguetteOfWisdom.kt" hl_lines="7 13 19 22 32" + class BaguetteOfWisdom(stack: ItemStack) : PylonItem(stack), PylonInteractor { + + ... + + override fun getPlaceholders() = listOf( + PylonArgument.of("xp_capacity", UnitFormat.EXPERIENCE.format(xpCapacity)), + PylonArgument.of("stored_xp", UnitFormat.EXPERIENCE.format(storedXp)) + ) + + override fun onUsedToRightClick(event: PlayerInteractEvent) { + if (event.player.isSneaking) { + // 1. Read how much XP we already have stored + val xp = storedXp + + // 2. Give all the XP to the player + event.player.giveExp(xp) + + // 3. Set the stored XP to 0 + storedXp = 0 + } else { + // 1. Read how much XP we already have stored + val xp = storedXp + + // 2. Figure out how much XP we need to take to get to `xpCapacity` + val extraXpNeeded = xpCapacity - xp + + // 3. Take as much XP from the player as we can to get there + val xpToTake = min(event.player.calculateTotalExperiencePoints(), extraXpNeeded) + event.player.giveExp(-xpToTake) + + // 4. Set the new stored XP amount + storedXp = xp + xpToTake + } + } + + ... + } + ``` + +The second thing we should do is reuse NamespacedKeys. This is more of a 'best practice' thing - it's generally recommend to reuse keys. It'll become more apparent why later on. + +=== "Java" + ```java title="BaguetteOfWisdom.java" hl_lines="2 8 16" + public class BaguetteOfWisdom extends PylonItem implements PylonInteractor { + public static final NamespacedKey STORED_XP_KEY = new NamespacedKey(MyAddon.getInstance(), "stored_xp"); + + ... + + public void setStoredXp(int xp) { + getStack().editPersistentDataContainer(pdc -> pdc.set( + STORED_XP_KEY, + PylonSerializers.INTEGER, + xp + )); + } + + public int getStoredXp() { + return getStack().getPersistentDataContainer().get( + STORED_XP_KEY, + PylonSerializers.INTEGER + ); + } + ``` +=== "Kotlin" + ```kotlin title="BaguetteOfWisdom.kt" hl_lines="2-4 10 16" + class BaguetteOfWisdom(stack: ItemStack) : PylonItem(stack), PylonInteractor { + companion object { + val STORED_XP_KEY = NamespacedKey(MyAddon.instance, "stored_xp") + } + + ... + + var storedXp: Int + get() = stack.persistentDataContainer.get( + STORED_XP_KEY, + PylonSerializers.INTEGER + )!! + set(value) { + stack.editPersistentDataContainer { pdc -> + pdc.set( + STORED_XP_KEY, + PylonSerializers.INTEGER, + value + ) + } + } + ``` + +[](another tab break) + +=== "Java" + ```java title="MyAddon.java" hl_lines="3" + ItemStack baguetteOfWisdom = ItemStackBuilder.pylonItem(Material.BREAD, baguetteOfWisdomKey) + .editPdc(pdc -> pdc.set( + BaguetteOfWisdom.STORED_XP_KEY, + PylonSerializers.INTEGER, + 0 + )) + .build(); + ``` +=== "Kotlin" + ```kotlin title="MyAddon.kt" hl_lines="4" + val baguetteOfWisdom = ItemStackBuilder.pylonItem(Material.BREAD, baguetteOfWisdomKey) + .editPdc { pdc -> + pdc.set( + BaguetteOfWisdom.STORED_XP_KEY, + PylonSerializers.INTEGER, + 0 + ) + } + .build() + ``` + +完成! + +[PlayerInteractEntityEvent]: https://jd.papermc.io/paper/1.21.8/org/bukkit/event/player/PlayerInteractEntityEvent.html +[PylonItemEntityInteractor]: https://pylonmc.github.io/pylon-core/docs/javadoc/io/github/pylonmc/pylon/core/item/base/PylonItemEntityInteractor.html +[持久化資料 container]: https://docs.papermc.io/paper/dev/pdc/ +[advanced lore]: advanced-lore.md +[PylonInteractor]: https://pylonmc.github.io/pylon-core/docs/javadoc/io/github/pylonmc/pylon/core/item/base/PylonInteractor.html diff --git a/docs/zh-TW/creating-addons/custom-items/practice-tasks.md b/docs/zh-TW/creating-addons/custom-items/practice-tasks.md new file mode 100644 index 0000000..afca957 --- /dev/null +++ b/docs/zh-TW/creating-addons/custom-items/practice-tasks.md @@ -0,0 +1,39 @@ +# 實作練習(Practice tasks) + +### 任務 1(Task 1) + +修改法棍火焰噴射器,使其同時對目標實體召喚閃電。新增一個設定值 `strike-with-lightning`,可為 `true` 或 `false`,用以控制是否對目標實體召喚閃電。 + +??? tip "提示" + [https://jd.papermc.io/paper/1.21.8/org/bukkit/World.html#strikeLightning(org.bukkit.Location)](https://jd.papermc.io/paper/1.21.8/org/bukkit/World.html#strikeLightning(org.bukkit.Location)) + +### 任務 2(Task 2) + +建立兩個版本的法棍火焰噴射器:一個會召喚閃電(稱為「宙斯法棍火焰噴射器」),另一個不會(一般的「法棍火焰噴射器」)。 + +### 任務 3(Task 3) + +建立「至高智慧法棍」(Baguette of Supreme Wisdom),可儲存最多 1000 經驗值,同時保留原本的「智慧法棍」。 + +### 任務 4(Task 4) + +為「智慧法棍」新增一個 `efficiency`(效率)設定值。例如設定為 0.8 時,在釋放經驗值時僅能取回 80% 的經驗值。記得在說明文字(lore)中加入顯示,並建立對應的佔位符(placeholder)! + +### 任務 5(Task 5) + +建立一個新物品:「神聖審判法棍」(Baguette of Divine Judgement)。 + +此物品應與智慧法棍一樣可儲存經驗值,但當玩家按住 Shift + 右鍵點擊時,不是釋放經驗值,而是消耗儲存的經驗值在玩家注視的方塊上召喚閃電。所需的經驗值應可透過設定調整,並在說明文字中顯示。 + +### 任務 6(Task 6) + +建立一個新物品:「忠誠法棍」(Baguette of Loyalty)。當玩家按住 Shift + 右鍵點擊時,此物品會綁定到該名玩家,並記住被綁定者。若其他人嘗試再次綁定,將會被閃電擊中。 + +當右鍵點擊時,此法棍僅會治癒與其綁定的玩家。 + +治癒量應可透過設定調整,並在說明文字中顯示治癒量與綁定玩家的名稱。 + +(這項任務比其他任務稍難,且有多種實作方式。) + +??? tip "提示" + 你不能直接將玩家物件儲存在 PDC 中,但可以儲存玩家的 UUID。 diff --git a/docs/zh-TW/creating-addons/getting-started.md b/docs/zh-TW/creating-addons/getting-started.md new file mode 100644 index 0000000..2cb88d5 --- /dev/null +++ b/docs/zh-TW/creating-addons/getting-started.md @@ -0,0 +1,99 @@ +# 入門指南(Getting started) + +!!! danger + 目前 **Pylon 插件(addon)開發尚未受到正式支援**。 + Pylon 正在快速變動中,因此你的外掛在下一次版本更新時可能會以悲慘的方式壞掉。 + 你仍然可以開發外掛,但請注意,為了維持相容性,你可能需要做出重大修改。 + +--- + +## 前言(Foreword) + +想撰寫一個 Pylon 插件嗎?太棒了! +我們撰寫了這份完整的指南,盡可能讓整個過程變得簡單。 +不要被長篇文字嚇到——我們會一步步解釋所有內容,你可以慢慢來。 + +不過,這仍然需要基本的技術與程式知識。 +如果你從未使用過 IDE、編譯器,或從沒寫過 `for` 迴圈,那可能會有些吃力。 +若你有插件開發經驗,將會大大受益。 + +在開始之前,先來點前置準備…… + +--- + +### 先決條件(Prerequisites) + +我們假設你已經: + +- 了解 Java 程式設計的基礎。 +- 擁有一個 **GitHub 帳號**,並能在電腦上使用 git。 + 若你是新手,建議使用 [GitHub Desktop](https://github.com/apps/desktop)。 +- 已安裝並設定好 [IntelliJ](https://www.jetbrains.com/idea/),並對其基本操作有一定概念。 + +雖然沒有外掛開發經驗也可以跟上,但有經驗會更順利! + +--- + +### Core vs Base(核心與基礎的區別) + +在開始之前,你需要了解 **Core** 與 **Base** 的差異。 + +- **Pylon Core** 是一個函式庫(library),提供建立新方塊、實體等功能。 +- **Pylon Base** 則是一個外掛(addon),提供了「基礎內容」。 + +在開發外掛時,你很可能會同時用到這兩者。 + +--- + +### 關於 Kotlin 的說明(A note on Kotlin) + +雖然你可以使用 Java 撰寫外掛,但若你是有經驗的 Java 開發者,可能會對 [Kotlin](https://kotlinlang.org/) 感興趣。 + +Kotlin 是 Java 的替代語言,語法更簡潔、特性更現代(例如 null 安全與函式擴展),而且少了許多 Java 的繁瑣之處。 +值得一提的是,**Pylon Core 本身就是用 Kotlin 撰寫的**。 + +那麼,讓我們開始吧! + +--- + +## 開發環境設定(Setting up) + +### Fork 模板(Forking the template) + +Pylon 官方提供了一個 [外掛模板(addon template)](https://github.com/pylonmc/pylon-addon-template), +它包含了撰寫 Pylon 外掛所需的所有基本設定。 + +1. [建立該模板的 Fork](https://www.geeksforgeeks.org/git/how-to-fork-a-github-repository/)。 +2. [Clone 你自己的 Fork 到本機](https://www.geeksforgeeks.org/git/how-to-git-clone-a-remote-repository/)。 + +接著,用 IntelliJ 開啟這個專案。 +第一次開啟時,IntelliJ 可能需要幾分鐘來匯入項目。 + +![IntelliJ 匯入外掛模板畫面](img/importing-addon-template.png) + +--- + +### 模板內容介紹(What's in the template?) + +此模板設計極為精簡,不包含任何多餘內容。 +它是使用 [Gradle](https://gradle.org/) 建構的。 + +在專案根目錄中,你會看到兩個檔案: +`gradlew` 與 `gradlew.bat` —— 這是 Gradle 的啟動包裝器。 +若你使用 IntelliJ,通常無需手動操作它們。 + +還有一個重要檔案是 `build.gradle.kts`, 這是 Kotlin 語法格式的建構設定檔。 +若要新增依賴或修改專案建構方式,就要修改這裡。 + +此外,你需要特別注意兩個檔案: +- `build.gradle.kts` +- `gradle.properties` + +這兩個檔案記錄了專案的主要資訊:名稱、版本、Pylon Core 版本、主類別(main class)與群組(group)。 +請務必依你的外掛內容調整這些資訊。 + +若你不確定「主類別」與「群組」是什麼, +可參考這篇文章:[Java Packages Explained](https://www.baeldung.com/java-packages)。 + +最後,我們會看到 `MyAddon.java` —— 這是關鍵檔案! +打開它後,我們會在下一章繼續教學。 diff --git a/docs/zh-TW/creating-addons/img/importing-addon-template.png b/docs/zh-TW/creating-addons/img/importing-addon-template.png new file mode 100644 index 0000000..1759b7c Binary files /dev/null and b/docs/zh-TW/creating-addons/img/importing-addon-template.png differ diff --git a/docs/zh-TW/creating-addons/item_ux.md b/docs/zh-TW/creating-addons/item_ux.md new file mode 100644 index 0000000..3d33a68 --- /dev/null +++ b/docs/zh-TW/creating-addons/item_ux.md @@ -0,0 +1,15 @@ +# 撰寫物品敘述指南(Writing Item Lore) + +- 使用能代表該物品的材質 +- 說明順序應為:**描述 → 用途 → 屬性** +- 保持敘述(lore)簡潔;玩家討厭閱讀過長文字 +- 說明物品的功能,但避免過度冗長或資訊過多 +- 若日後想到改進方式,可回頭調整物品敘述 +- 不要過度使用顏色(避免不必要的強調或標示) +- 配方應該合乎邏輯 +- 對稱的配方應該反映可反轉性(例如斧頭的左右對稱設計) +- 新增熔爐/高爐/煙燻爐的配方時,熔煉時間應與原版相同,除非有合理理由調整 +- 方塊的材質應與對應的物品相同 +- 不要為了「填字」而撰寫毫無意義的敘述 +- 理想情況下,玩家應能僅憑敘述推測物品用途 +- 每行結尾不要使用句號(full stop) diff --git a/docs/zh-TW/creating-addons/your-first-item/adding-an-item.md b/docs/zh-TW/creating-addons/your-first-item/adding-an-item.md new file mode 100644 index 0000000..9ccffb0 --- /dev/null +++ b/docs/zh-TW/creating-addons/your-first-item/adding-an-item.md @@ -0,0 +1,163 @@ +# 新增物品(Adding an item) + +## 概述(Overview) + +到目前為止,我們的插件只有一個類別:`MyAddon`(或你改的名稱)。 +這個類別繼承了 [JavaPlugin] 並實作 [PylonAddon]。 +在類別中有一些註解說明各部分的功能,請先閱讀並了解它的運作方式。 +目前插件實際上還沒做任何事——所以讓我們來新增一個物品吧! + +我們先來製作一個可恢復 6 格飢餓值的法國麵包(baguette)。 + +要建立一個簡單的物品,我們只需要兩樣東西: +**物品的 key** 與 **物品堆疊(item stack)**。 + +--- + +## 新增物品(Adding the item) + +### 建立 key(Creating a key) + +[NamespacedKey] 是 Pylon 用來識別自訂物品、方塊、研究、實體等內容的方式。 + +!!! question "什麼是 NamespacedKey?為什麼要使用它?" + key 其實就是一段文字,例如 `pylonbase:copper_dust`,讓 Pylon 能唯一識別你的物品。這類似於原版 Minecraft 的物品 ID。 + 為什麼不直接用 `copper_dust` 呢?因為如果有兩個外掛都新增了同名物品,我們就無法區分它們! + 為了解決這個問題,Pylon 使用 [NamespacedKey],也就是將「外掛名稱」與「物品名稱」結合,例如:`my_addon:copper_dust`。 + +在 `onEnable` 方法中建立一個新的 [NamespacedKey]: +=== "Java" + ```java + NamespacedKey baguetteKey = new NamespacedKey(this, "baguette"); + ``` +=== "Kotlin" + ```kotlin + val baguetteKey = NamespacedKey(this, "baguette") + ``` + +--- + +### 建立物品堆疊(Creating the item stack) + +接著我們需要實際的物品。 +這裡會使用 [ItemStackBuilder]。 + +[ItemStackBuilder] 提供多種方法協助建立 [ItemStack]。 +例如 `.set(, )` 可設定物品屬性,如附魔、是否不可破壞等。 + +**當你建立 Pylon 物品時,務必使用 `ItemStackBuilder.pylonItem(, )`。** +雖然有其他方式建立 [ItemStack],但請**不要**用那些方法建立 Pylon 物品。 + +??? question "為什麼要用 `ItemStackBuilder.pylonItem`,而不是其他方式建立 [ItemStack]?" + 在底層,Pylon 會將物品的 key 儲存在 [PersistentDataContainer](PDC)中。 + 當你使用 `ItemStackBuilder.pylonItem` 並提供 key 時,Pylon 會自動將 key 寫入該物品的 PDC。 + 若你自行建立 ItemStack,PDC 中將不含此 key,Pylon 就無法辨識該物品。 + + 此外,[ItemStackBuilder] 也會自動設定物品的名稱與敘述(lore), 對應到預設的翻譯 key(後續章節會詳細說明)。 + +建立一個法國麵包的程式如下: +=== "Java" + ```java + ItemStack baguette = ItemStackBuilder.pylonItem(Material.BREAD, baguetteKey) + .set(DataComponentTypes.FOOD, FoodProperties.food().nutrition(6)) + .build(); + ``` +=== "Kotlin" + ```kotlin + val baguette = ItemStackBuilder.pylonItem(Material.BREAD, baguetteKey) + .set(DataComponentTypes.FOOD, FoodProperties.food().nutrition(6)) + .build() + ``` + +!!! info "Data components" + Data component 是用來描述物品的資料結構,例如: + - 「這是食物,能恢復 6 飢餓值」 + - 「這是鎬子,能以特定速度破壞方塊」 + 你可以在 [這裡](https://jd.papermc.io/paper/1.21.8/io/papermc/paper/datacomponent/DataComponentTypes.html) 查看完整列表。 + +--- + +### 註冊物品(Registering the item) + +接下來我們需要將物品註冊進 Pylon。 +這需要兩個參數:**物品堆疊**與**代表該物品的類別**。 +稍後我們會介紹如何建立自訂物品類別, 目前可先使用預設的 [PylonItem] 類別: + +=== "Java" + ```java + PylonItem.register(PylonItem.class, baguette); + ``` +=== "Kotlin" + ```kotlin + PylonItem.register(baguette) + ``` + +--- + +### 新增到 Pylon 指南(Adding the item to the guide) + +最後,我們希望這個物品能出現在 Pylon 指南中。 +讓我們把它加到「食物(food)」分類中: + +=== "Java" + ```java + BasePages.FOOD.addItem(baguetteKey); + ``` +=== "Kotlin" + ```kotlin + BasePages.FOOD.addItem(baguetteKey) + ``` + +--- + +## 整合所有內容(Putting it all together) + +以下是完整的程式碼: + +=== "Java" + ```java title="MyAddon.java" hl_lines="9-14" + // Called when our plugin is enabled + @Override + public void onEnable() { + instance = this; + + // Every Pylon addon must call this BEFORE doing anything Pylon-related + registerWithPylon(); + + NamespacedKey baguetteKey = new NamespacedKey(this, "baguette"); + ItemStack baguette = ItemStackBuilder.pylonItem(Material.BREAD, baguetteKey) + .set(DataComponentTypes.FOOD, FoodProperties.food().nutrition(6)) + .build(); + PylonItem.register(PylonItem.class, baguette); + BasePages.FOOD.addItem(baguetteKey); + } + ``` +=== "Kotlin" + ```kotlin title="MyAddon.kt" hl_lines="8-13" + // Called when our plugin is enabled + override fun onEnable() { + instance = this + + // Every Pylon addon must call this BEFORE doing anything Pylon-related + registerWithPylon() + + val baguetteKey = NamespacedKey(this, "baguette") + val baguette = ItemStackBuilder.pylonItem(Material.BREAD, baguetteKey) + .set(DataComponentTypes.FOOD, FoodProperties.food().nutrition(6)) + .build() + PylonItem.register(baguette) + BasePages.FOOD.addItem(baguetteKey) + } + ``` + +現在就可以進遊戲測試看看了! + +--- + +[JavaPlugin]: https://jd.papermc.io/paper/1.21.8/org/bukkit/plugin/java/JavaPlugin.html +[PylonAddon]: https://pylonmc.github.io/pylon-core/docs/javadoc/io/github/pylonmc/pylon/core/addon/PylonAddon.html +[NamespacedKey]: https://jd.papermc.io/paper/1.21.8/org/bukkit/NamespacedKey.html +[ItemStackBuilder]: https://pylonmc.github.io/pylon-core/docs/javadoc/io/github/pylonmc/pylon/core/item/builder/ItemStackBuilder.html +[ItemStack]: https://jd.papermc.io/paper/1.21.8/org/bukkit/inventory/ItemStack.html +[PylonItem]: https://pylonmc.github.io/pylon-core/docs/javadoc/io/github/pylonmc/pylon/core/item/PylonItem.html +[PersistentDataContainer]: https://jd.papermc.io/paper/1.21.8/org/bukkit/persistence/PersistentDataContainer.html diff --git a/docs/zh-TW/creating-addons/your-first-item/adding-name-and-lore.md b/docs/zh-TW/creating-addons/your-first-item/adding-name-and-lore.md new file mode 100644 index 0000000..d45c641 --- /dev/null +++ b/docs/zh-TW/creating-addons/your-first-item/adding-name-and-lore.md @@ -0,0 +1,95 @@ +# 使用語言系統(Using the language system) + +## 什麼是語言系統?(What is the language system?) + +「語言系統」這個詞聽起來可能有點嚇人,但其實非常簡單。 +語言系統只是讓內容**可以被翻譯**的一種方式。 + +舉例來說,假設我想創建一個讓伺服器管理員都驚嘆的神奇新物品:**核彈(Nuclear Bomb)**。 +我寫好物品的程式,然後在程式中設定物品名稱如下: + +```java +... +# (這裡是建立物品的程式碼) +... +item.setName("Nuclear Bomb") +... +``` +(這不是實際程式碼,只是示範用途) + +現在假設我們希望說西班牙語的玩家也能使用這個插件。 +在西班牙語中,「核彈」翻譯成「Bomba Nuclear」。 +但我在程式裡是直接寫死(hardcode)成 "Nuclear Bomb",那要怎麼讓西班牙玩家看到「Bomba Nuclear」呢? + +解法是使用一個通用的「翻譯鍵(translation key)」: + +```java +... +# (這裡是建立物品的程式碼) +... +item.setName("item.nuclear-bomb.name") +... +``` +(這不是實際程式碼,只是示範用途) + +接著,我們就可以為每個語言建立一個獨立的翻譯檔案,內容如下: + +```yaml title="en.yml" +item.nuclear-bomb.name: "Nuclear Bomb" +``` +(這不是實際程式碼,只是示範用途) + +```yaml title="es.yml" +item.nuclear-bomb.name: "Bomba Nuclear" +``` +(這不是實際程式碼,只是示範用途) + +當然,我們需要有一個系統來幫玩家顯示正確語言版本,而 **Pylon 會自動幫你處理這一切!** +現在,我們就來看看在 Pylon 中要怎麼做。 + +--- + +## 為法國麵包新增名稱與敘述(Adding name and lore to our baguette) + +還記得我們上面用了 `item.setName("item.nuclear-bomb.name")` 嗎? +在 **Pylon** 中,你不需要手動設定翻譯鍵,因為 Pylon **會根據你的物品 key 自動產生翻譯鍵**。 +我們只需要建立翻譯檔,並確保裡面包含正確的 key 即可。 + +打開 `src/main/resources/lang` 資料夾中的 `en.yml` 檔案(`en` 代表英文)。 + +!!! Note "新增其他語言的翻譯檔" + 若想建立西班牙語翻譯檔,可命名為 `es.yml`; + 若是捷克語則命名為 `cs.yml`,以此類推。 + 可參考 [Minecraft 語言代碼列表](https://minecraft.wiki/w/Language)。 + +然後在檔案中加入以下內容: + +```yaml title="en.yml" hl_lines="3-7" +addon: "" + +item: + baguette: + name: "Baguette" + lore: |- + The best food +``` + +請注意這裡有一個 `addon` 鍵(key),這是你的外掛名稱。 + +我們同時也為法國麵包(baguette)新增了 `name` 與 `lore`。 +這裡使用的 `baguette`,正是我們先前建立的 key,來自這一行: + +=== "Java" +```java +NamespacedKey baguetteKey = new NamespacedKey(this, "baguette"); +``` +=== "Kotlin" +```kotlin +val baguetteKey = NamespacedKey(this, "baguette") +``` + +!!! question "這些 ``、``、`` 是什麼意思?" + 這些是文字標籤,我們會在 [進階敘述(Advanced Lore)](../custom-items/advanced-lore.md) 章節詳細說明。 + +重新啟動伺服器後,你的法國麵包應該就會有名稱與敘述囉! +![Baguette with translations](img/baguette.png) diff --git a/docs/zh-TW/creating-addons/your-first-item/full-code.md b/docs/zh-TW/creating-addons/your-first-item/full-code.md new file mode 100644 index 0000000..5605496 --- /dev/null +++ b/docs/zh-TW/creating-addons/your-first-item/full-code.md @@ -0,0 +1,31 @@ +# 完整程式碼(The full code) + +我們已經學了不少內容,但實際上,整個程式碼並不多: + +=== "Java" + ```java title='MyAddon.java' + NamespacedKey baguetteKey = new NamespacedKey(this, "baguette"); + ItemStack baguette = ItemStackBuilder.pylonItem(Material.BREAD, baguetteKey) + .set(DataComponentTypes.FOOD, FoodProperties.food().nutrition(6)) + .build(); + PylonItem.register(PylonItem.class, baguette); + BasePages.FOOD.addItem(baguetteKey); + ``` +=== "Kotlin" + ```kotlin title='MyAddon.kt' + val baguetteKey = NamespacedKey(this, "baguette") + val baguette = ItemStackBuilder.pylonItem(Material.BREAD, baguetteKey) + .set(DataComponentTypes.FOOD, FoodProperties.food().nutrition(6)) + .build() + PylonItem.register(baguette) + BasePages.FOOD.addItem(baguetteKey) + ``` + +```yaml title='en.yml' +addon: "" + +item: + baguette: + name: Baguette + lore: The best food +``` diff --git a/docs/zh-TW/creating-addons/your-first-item/img/baguette-missing-translation-key.png b/docs/zh-TW/creating-addons/your-first-item/img/baguette-missing-translation-key.png new file mode 100644 index 0000000..7989678 Binary files /dev/null and b/docs/zh-TW/creating-addons/your-first-item/img/baguette-missing-translation-key.png differ diff --git a/docs/zh-TW/creating-addons/your-first-item/img/baguette.png b/docs/zh-TW/creating-addons/your-first-item/img/baguette.png new file mode 100644 index 0000000..b8705fa Binary files /dev/null and b/docs/zh-TW/creating-addons/your-first-item/img/baguette.png differ diff --git a/docs/zh-TW/creating-addons/your-first-item/img/running-test-server.png b/docs/zh-TW/creating-addons/your-first-item/img/running-test-server.png new file mode 100644 index 0000000..cea7d42 Binary files /dev/null and b/docs/zh-TW/creating-addons/your-first-item/img/running-test-server.png differ diff --git a/docs/zh-TW/creating-addons/your-first-item/practice-tasks.md b/docs/zh-TW/creating-addons/your-first-item/practice-tasks.md new file mode 100644 index 0000000..1183fc9 --- /dev/null +++ b/docs/zh-TW/creating-addons/your-first-item/practice-tasks.md @@ -0,0 +1,23 @@ +# 練習題(Practice tasks) + +### 任務 1(Task 1) +新增另一個使用相同 key 的物品。會發生什麼事? + +### 任務 2(Task 2) +將你的法國麵包(baguette)最大堆疊數設為 32。 + +??? tip "提示" + 使用 [DataComponentTypes.MAX_STACK_SIZE] + +### 任務 3(Task 3) +新增一個可頌(croissant),並給它不同的名稱與敘述(lore)。 + +### 任務 4(Task 4) +新增一把弓(bow),總耐久度只有 10,且一開始只剩下 8 點耐久。 + +??? tip "提示" + [DataComponentTypes.DAMAGE] 與 [DataComonentTypes.MAX_DAMAGE] 的命名有點誤導…… + +[DataComponentTypes.MAX_STACK_SIZE]: https://jd.papermc.io/paper/1.21.8/io/papermc/paper/datacomponent/DataComponentTypes.html#MAX_STACK_SIZE +[DataComonentTypes.MAX_DAMAGE]: https://jd.papermc.io/paper/1.21.8/io/papermc/paper/datacomponent/DataComponentTypes.html#MAX_DAMAGE +[DataComponentTypes.DAMAGE]: https://jd.papermc.io/paper/1.21.8/io/papermc/paper/datacomponent/DataComponentTypes.html#DAMAGE diff --git a/docs/zh-TW/creating-addons/your-first-item/running-your-addon.md b/docs/zh-TW/creating-addons/your-first-item/running-your-addon.md new file mode 100644 index 0000000..ab91e2a --- /dev/null +++ b/docs/zh-TW/creating-addons/your-first-item/running-your-addon.md @@ -0,0 +1,51 @@ +# 執行你的插件(Running your addon) + +## 啟動測試伺服器(Starting a test server) + +插件模板內建了一個「執行伺服器」任務,可直接在 IntelliJ 中啟動測試伺服器。 +打開 **Gradle** 面板,找到並雙擊 `runServer` 按鈕即可。 +這會在專案根目錄下建立一個新的 `run` 資料夾,其中包含測試伺服器。 +你可以自由修改這個伺服器——新增插件、調整設定檔、隨你所欲! + +![Running the test server](img/running-test-server.png) + +接著,你應該會看到伺服器輸出的控制台畫面。 +第一次執行時,系統會下載伺服器執行檔,可能需要一兩分鐘。 +由於尚未同意 EULA,第一次啟動會失敗。 +請前往剛建立的 `run` 資料夾,開啟 `eula.txt` 並接受 EULA, +然後再執行一次 `runServer` 任務。 + +要關閉伺服器時,可在控制台輸入 `stop`,或在遊戲中使用 `/stop` 指令。 + +!!! danger + **請勿使用 IntelliJ 的停止按鈕來終止任務!** + 這樣伺服器不會正常關閉,導致你無法真正停止它。 + 那樣伺服器將成為不死之身,吞噬一切。 + **N̴o̶t̵h̶i̴n̴g̸ ̵a̶n̵d̷ ̸n̷o̷ ̴o̶n̸e̸ ̸i̷s̵ ̶s̸a̴f̴e̷.̵** + **Y̶̲̏O̵̫͘Ư̸͓ ̸̪̀S̸͚͊H̷̭̓A̵̢̾L̷̘͋L̷̻̿ ̴̾͜A̸̤̿L̸͇̾L̷̟̕ ̸̫̈B̴̊ͅÈ̴̹ ̴̺̉D̸̰̓Ë̵̪S̷̪̚Ṭ̸͒R̴̹̓Ǫ̵̓Ȳ̴̥Ê̶͙D̶̰̑ ̵͉͘B̷̘̌Y̴̽ͅ ̴̙̈T̷͚͒H̷̤͂Ẽ̷̥ ̸̨͗Ą̵̾L̴̦̒M̵̗͠I̵̱͛G̸͈͝H̷̫̀T̶̰̋Y̸͎̚ ̵̦̈́J̸̣̑V̴̭̌M̸̗̋.** + +伺服器啟動後,你可以透過 `localhost:25565` 連線。 +別忘了在控制台輸入 `op <你的遊戲名稱>` 以獲取管理員權限。 + +--- + +## 取得你的物品(Getting the item) + +現在,你可以獲得自己創建的物品了。 +使用 `/py give` 指令,例如: + +``` +/py give Idra my-addon:baguette +``` + +若一切設定正確,你應該會拿到你的法國麵包(baguette)。 + +但是等等…… + +這是怎麼回事? + +![Baguette with missing translation keys](img/baguette-missing-translation-key.png) + +注意到我們在建立法國麵包時並沒有設定名稱! +若要為它加上名稱,就必須使用**語言系統(language system)**。 +別擔心,這部分非常簡單,我們會在下一章介紹。 diff --git a/docs/zh-TW/index.md b/docs/zh-TW/index.md new file mode 100644 index 0000000..b0d2439 --- /dev/null +++ b/docs/zh-TW/index.md @@ -0,0 +1,89 @@ +Pylon 是一個**即將推出**的 Minecraft Java 插件,它將大幅擴展原版遊戲的內容,包含電力機械、巨型多方塊結構、完整的流體系統、複雜的冶煉系統、廣泛的自動化選項、研究系統等更多功能。它的目標是取代 Slimefun。 + +Pylon 採用插件系統,這意味著任何人都可以撰寫外掛為 Pylon 添加內容!此外,它還具備多項實用特性,例如: + +- 一流的翻譯支援,這意味著每位玩家選擇自己的語言。 +- 完整的設定選項,包括每台機器的配置。 +- 直覺且易於使用的指南,協助玩家了解插件。 + +## :frame_photo: Pylon 圖片展示(目前為止) + +![Pylon 流體系統示意(A Pylon fluid setup)](img/fluid-setup.png) +![在 Pylon 指南中懸停於研究包上(Hovering over research pack in Pylon guide)](img/hovering-over-research-pack.png) +![液壓流體系統(Hydraulic fluid setup)](img/hydraulic-fluid-setup.png) +![銅製流體槽(Copper fluid tank)](img/looking-at-copper-fluid-tank.png) +![Pylon 指南中的研究畫面(Pylon research in the guide)](img/looking-at-research.png) +![醫療包介面(Medkit in guide)](img/medkit.png) +![放置銅管(Placing copper pipes)](img/placing-pipes.png) +![淨化塔設定介面(Purification tower config)](img/purification-tower-config.png) +![在 Pylon 指南中搜尋物品(Searching items in the Pylon guide)](img/searching-items.png) +![使用液壓磨輪轉動器(Using the hydraulic grindstone turner)](img/using-grindstone-turner.png) +![使用魔法祭壇(Using the magic altar)](img/using-magic-altar.png) +![使用冶煉爐(Using the smeltery)](img/using-smeltery.png) + +## :stopwatch: 效能與穩定性 + +我們自第一天起便以效能與穩定性為首要考量: + +- Pylon **廣泛的** 使用快取,並將大多數高負載系統以非同步方式運行,同時利用現代併行與效能特性(例如 coroutines)。 +- 此外,插件還提供多種效能調整選項——包括每台機器的更新頻率、限制玩家/區塊的機器數量、流體與能量的更新速率等。 +- Pylon 的設計最大程度地降低了資料損壞的風險,並包含多層錯誤處理機制以應對問題。 + +注意:Pylon 可能無法完全與基岩版(Bedrock)相容。 + +## :calendar: 預定時程 + +**2025 年 9~10 月** — 限邀制 Alpha 測試開始。 + +**2025 年 11~12 月** — 開放式 Alpha 測試開始(預計在 MetaMechanists 伺服器上舉行)。 +我們預計會進行多輪為期數週的 Pylon 測試,用以修正錯誤、測試效能、提升穩定性與使用者體驗,確保插件正式發布時已經完善。 + +**2026 年初~中期** — Pylon 正式發布。 + +## :keyboard: 開發 Pylon 外掛 + +我們致力於讓 Pylon 的外掛開發變得簡單、直覺、靈活且有趣。 +從數百小時的 Slimefun 外掛開發經驗中所學到的教訓,使 Pylon 的 API 更加友好、清晰且彈性十足。 +Pylon 也完全支援使用 Kotlin 撰寫外掛,這比 Java 更簡潔且易於維護。 + +目前由於 Pylon 仍處於快速變動階段,外掛開發尚未開放,且暫缺高層級開發文件。 +我們計畫不久後推出完整的外掛開發指南,詳細介紹如何建立外掛及運用 Pylon 的各種系統,敬請期待! + +## :link: 相關連結 + +Discord 邀請連結:[https://discord.gg/4tMAnBAacW](https://discord.gg/4tMAnBAacW) + +Github 專案頁面:[https://github.com/pylonmc](https://github.com/pylonmc) + +文件網站(**施工中**):[https://pylonmc.github.io/](https://pylonmc.github.io/) + +## :detective: 開發團隊 + +目前 Pylon 的核心開發團隊為: + +@ohmvir 🇨🇦 — 來自 MetaMechanists 的新血開發者,雖然剛接觸插件開發,但表現出色,快速上手並新增了許多基礎內容,如生命護符與斬首之劍,同時協助處理各類任務。 + +@overlordidra 🇬🇧 — MetaMechanists(知名 Slimefun 伺服器)經營者超過四年,亦為 Quaptics 的開發者。 +他負責了許多核心系統,包含 Pylon 指南、流體系統、液壓系統、自動測試代碼,以及自訂方塊/實體/物品追蹤系統等。 + +@seggan 🇺🇸 — 資深 Slimefun 外掛開發者,作品包括 SlimefunWarfare、SFCalc,以及知名的 Galactifun 外掛。 +他同時也對其他外掛、Paper 伺服器軟體及 Slimefun 本體有貢獻。 +Seggan 負責許多核心系統,包括完整翻譯系統、WAILA、研究系統、內部登錄系統與配方系統(以及其他眾多功能!)。 + +我們同時感謝其他協助成員: + +@ihateblueb — 曾經營 Slimefun 伺服器 Orchid,目前協助新增更多內容(例如電梯系統由她製作!)。 + +@justahuman_xd — 負責許多技術層面的功能,如方塊與盔甲材質系統,擁有豐富的 Slimefun 開發經驗,並提供了寶貴建議。 + +@.ph.enix — 曾負責我們的 CI 系統,設立自動測試與版本發布流程。雖然他已離開專案,但我們非常感謝他的貢獻。 + +@vaan1310 — 協助修復錯誤、實作資料驅動研究功能,並協助審查 Pull Request。 + +如果你有興趣參與開發,歡迎加入我們的 Discord! +不需要是專家,只要具備基本插件開發經驗即可。 +我們有各種不同難度的任務,從簡單到極具挑戰性,讓我們幫你找到合適的工作項目。 + +## :question: 有問題嗎? + +歡迎在 Discord 伺服器留言,我們將樂於回覆。 diff --git a/docs/zh-TW/installation/commands-and-permissions.md b/docs/zh-TW/installation/commands-and-permissions.md new file mode 100644 index 0000000..6d8f61f --- /dev/null +++ b/docs/zh-TW/installation/commands-and-permissions.md @@ -0,0 +1,27 @@ +# 指令與權限 + +## 預設指令 +| 指令(Command) | 權限(Permission) | +|------------------------------------|------------------------------------------| +| `/py` | `pylon.command.guide` | +| `/py guide` | `pylon.command.guide` | +| `/py research discover ` | `pylon.command.research.discover` | +| `/py research list` | `pylon.command.research.list` | +| `/py research points me` | `pylon.command.research.points.get.self` | +| `/py waila` | `pylon.command.waila` | + +## 管理員指令 +| 指令(Command) | 權限(Permission) | +|--------------------------------------------------|-------------------------------------| +| `/py debug` | `pylon.command.debug` | +| `/py give [amount]` | `pylon.command.give` | +| `/py key` | `pylon.command.key` | +| `/py research points get ` | `pylon.command.research.points.get` | +| `/py research add ` | `pylon.command.research.modify` | +| `/py research addall ` | `pylon.command.research.modify` | +| `/py research remove ` | `pylon.command.research.modify` | +| `/py research points add ` | `pylon.command.research.points.set` | +| `/py research points set ` | `pylon.command.research.points.set` | +| `/py research points subtract ` | `pylon.command.research.points.set` | +| `/py setblock ` | `pylon.command.setblock` | +| `/py confetti ` | `pylon.command.confetti` | diff --git a/docs/zh-TW/installation/installing-pylon.md b/docs/zh-TW/installation/installing-pylon.md new file mode 100644 index 0000000..1101426 --- /dev/null +++ b/docs/zh-TW/installation/installing-pylon.md @@ -0,0 +1,29 @@ +# 安裝(Installation) + +!!! danger + **⚠️ PYLON 目前仍屬於實驗性階段。** + 請僅在你「願意刪除」的測試伺服器上執行。 + 下一個版本的 Pylon **很可能與目前版本不相容**。 + 若你在正式環境安裝導致資料遺失,我們只會指著螢幕笑你 🤷‍♂️。 + +1. 確保你正在運行 **Paper** 或其分支。Pylon **不相容於 Spigot**。 +2. 從 [這裡](https://github.com/pylonmc/pylon-core/releases) 下載最新版本的 **Pylon Core**。 +3. 從 [這裡](https://github.com/pylonmc/pylon-base) 下載最新版本的 **Pylon Base**。 +4. 將 `.jar` 檔案放入伺服器的 `plugins` 資料夾,然後**重新啟動伺服器**。 + ⚠️ [請勿使用 /reload 指令](https://madelinemiller.dev/blog/problem-with-reload/)。 + 第一次啟動會比平常久,但之後的啟動速度會顯著提升。 +5. 檢查 `Pylon Core` 與 `Pylon Base` 的外掛資料夾,了解可用的所有設定選項。 +6. [安裝一些外掛吧。](list-of-addons.md) +7. 完成! + +--- + +### Pylon 的資料儲存位置在哪裡? + +你可能會注意到 Pylon 並沒有使用資料庫, 而且在插件資料夾中也找不到任何儲存檔案。 +這是因為 Pylon **將所有資料儲存在世界檔(world file)內部**, 與 Minecraft 自身儲存方塊與實體的方式相同! + +因此,你只需要備份世界檔即可: +**Pylon 的資料會永遠與世界資料保持一致。** + +這也代表 Pylon **比其他類似外掛更不容易發生資料毀損**。 diff --git a/docs/zh-TW/installation/list-of-addons.md b/docs/zh-TW/installation/list-of-addons.md new file mode 100644 index 0000000..2b8097a --- /dev/null +++ b/docs/zh-TW/installation/list-of-addons.md @@ -0,0 +1,6 @@ +# 外掛清單(List of addons) + +| 名稱(Name) | 作者(Author) | 說明(Description) | 下載(Download) | +|---------------|----------------|----------------------|------------------| +| **Base** | Pylon 團隊(Pylon team) | 新增所有 Pylon 的基礎內容。大多數外掛的運作都需要此模組。 | [GitHub](https://github.com/pylonmc/pylon-base/releases) | +| **建筑法杖(Construction-wand)** | [balugaq](https://github.com/balugaq) | 提供一些實用的法杖,幫助你更快速地建造屬於自己的城堡。 | [GitHub](https://github.com/balugaq/construction-wand) | diff --git a/docs/zh-TW/javadocs.md b/docs/zh-TW/javadocs.md new file mode 100644 index 0000000..ba28404 --- /dev/null +++ b/docs/zh-TW/javadocs.md @@ -0,0 +1,5 @@ +# Javadocs + +[Pylon Core - Javadocs](https://pylonmc.github.io/pylon-core/docs/javadoc/){ .md-button } + +[Pylon Core - KDocs](https://pylonmc.github.io/pylon-core/docs/kdoc/){ .md-button } diff --git a/docs/zh-TW/reference/fluids.md b/docs/zh-TW/reference/fluids.md new file mode 100644 index 0000000..46b4f07 --- /dev/null +++ b/docs/zh-TW/reference/fluids.md @@ -0,0 +1,71 @@ +# 流體系統(Fluids) + +!!! abstract "想建立會消耗或產生流體的方塊嗎?" + 請參考「Fluid blocks」教學。這是一份關於流體運作的技術說明。 + +## 流體點(Fluid points) + +流體點是你在放置管線時需要點擊的紅/綠/灰色立方體之一。共有三種類型的流體點:`INPUT`(綠色)、`OUTPUT`(紅色)與 `CONNECTOR`(灰色)。輸入與輸出點附加在方塊上,用於加入或移除流體;而連接點則僅用於將不同流體點相互連接。 + +若要為你的機器建立流體連接點,可以呼叫 `FluidPointInteraction.make(...)`。 + +## 流體方塊(Fluid blocks) + +### PylonFluidBlock + +任何具有流體輸入/輸出的方塊都必須實作 `PylonFluidBlock`。此介面允許你從輸入點請求流體、向輸出點提供流體,並定義如何在方塊中加入/移除流體。如有疑問,請參考「ticking」章節了解詳細運作方式。 + +更多使用說明可參閱 `PylonFluidBlock` 的 Javadoc 文件。 + +### PylonFluidBufferBlock + +通常流體機器會在內部儲存流體。例如,壓榨機(press)擁有一個內部緩衝槽(buffer)用於儲存植物油,預設容量為 1000mB。這是非常常見的情況,因此我們建立了 `PylonFluidBufferBlock` 介面來處理。此介面可讓方塊輕鬆管理流體緩衝。 + +更多使用說明可參閱 `PylonFluidBufferBlock` 的 Javadoc 文件。 + +### PylonFluidTank + +另一種常見模式是「流體槽(fluid tank)」:它一次只能儲存一種流體,但可以儲存多種類型的流體。`PylonFluidTank` 實作了此設計模式。 + +更多使用說明可參閱 `PylonFluidTank` 的 Javadoc 文件。 + +## 實體與虛擬流體點(Physical vs virtual fluid points) + +流體點分為「實體」與「虛擬」兩種。「實體流體點」是顯示在遊戲中的紅/綠/灰色方塊框,用於顯示與互動;其內部包含一個「虛擬流體點」。這樣的區分雖然看似複雜,但能讓底層邏輯更為簡潔。 + +實體流體點主要作為虛擬流體點的外層包裝,負責顯示與玩家互動;你幾乎不需要關心它。因此當我們提到「流體點」時,指的通常是虛擬流體點。 + +虛擬流體點具有以下屬性:UUID(唯一識別)、流體點類型、已連接的流體點以及所屬方塊。若該流體點是輸入或輸出類型,則其方塊必須實作 `PylonFluidBlock`。 + +## 區段(Segments) + +流體的運作依賴「區段(segment)」的概念。區段是一組相互連接的流體點集合,封裝了**所有連通的流體點**。例如(以虛線代表管線): +``` +A---B-----C D--E F +``` +此時 A、B、C 屬於同一區段;D、E 屬於另一區段;F 為第三區段。 +若移除 B 與 C 之間的管線,A、B 將形成新區段,而 C 也會成為新的區段。相反地,若將 C 與 D 連接,則原本的 A/B/C 與 D/E 區段會合併為一個新區段。 + +## 流體更新(Ticking) + +區段是流體系統運作的基礎。每個區段會依伺服器設定,每隔數個遊戲刻(tick)更新一次。當一個區段被更新時,會執行以下步驟: + +1. 針對區段內的每個方塊呼叫 `getRequestedFluids`,建立所有被請求流體及其數量的清單。 +2. 遍歷每一種被供應的流體。 +3. 若該流體不被區段允許,則跳過。 +4. 若沒有任何機器請求該流體(即 `fluidAmountRequested` 為零),則跳過。 +5. 找到至少有一台機器請求該流體時,使用輪詢(round-robin)演算法根據可通過管線的流體量與請求量,盡可能從供應方取出流體。 +6. 再次使用輪詢演算法,平均分配流體至接收方機器。 + +## 流速與允許流體(Fluid flow rates and allowed fluids) + +為了限制流體在管線中傳輸的速度,我們將流速限制設定在**區段層級**而非每條管線。這樣能在降低運算成本的同時達到效果。 +同樣地,也可以限制允許通過的流體種類(例如木製管線禁止高溫流體),這同樣以區段為單位設定。 + +這也是不同管線無法互相連接的原因之一:若兩者相連,應該採用哪個流速限制? + +當區段合併時,系統會隨機選擇其中一個區段並保留其流速與允許流體設定;當區段被分割時,新區段會繼承舊區段的設定。 + +## 管線放置邏輯(Pipe placement logic) + +這部分的邏輯極其複雜。如果你真的需要修改這段程式碼——祝你好運。 diff --git a/mkdocs.yml b/mkdocs.yml index 78e650f..e315967 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -86,3 +86,6 @@ plugins: default: true name: English build: true + - locale: zh-TW + name: 繁體中文 + build: true