2021年12月3日 星期五

Java 讀取寫入文字檔案的幾種方法

在這裡我紀錄了一些 Java 讀寫檔案文字內容的一些方法:
package test;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class FileReadWriteTest {

	public static void main(String[] args) throws IOException {
		String filePath = "D:\\MyTextFileTest.txt";
		new File(filePath).createNewFile();

		writeFileContent_1(filePath, true, "中文字測試");
		writeFileContent_1(filePath, true, "你好哈囉");
		
		System.out.println(readFileContent_1(filePath));
		System.out.println(readFileContent_2(filePath));
	}
	
	/*************** Write File ***************/
	public static void writeFileContent_1(String filePath, boolean isAppend, String contentToWrite) {		
		try (
				FileOutputStream fileOutputStream = new FileOutputStream(new File(filePath), isAppend);
				OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream, "UTF-8");
				BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter);
			){
			
			bufferedWriter.write(contentToWrite);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	public static void writeFileContent_2(String filePath, boolean isAppend, String contentToWrite) {
		try {
			if (isAppend) {			
				Files.writeString(Paths.get(filePath), contentToWrite, StandardCharsets.UTF_8, StandardOpenOption.APPEND);			
			}else {
				Files.writeString(Paths.get(filePath), contentToWrite, StandardCharsets.UTF_8);
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		
	}
	
	/*************** Read File ***************/
	public static String readFileContent_1(String filePath) {
		StringBuffer fileContent = new StringBuffer();

		try (
				FileInputStream fileInputStream = new FileInputStream(new File(filePath));
				InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, "UTF-8");
				BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
			) {
			
			String readContent = "";
			while ((readContent = bufferedReader.readLine()) != null) {
				fileContent.append(readContent + "\n");
			}
			if (fileContent.length() > 0) {
				//remove the latest added "\n"
				fileContent.deleteCharAt(fileContent.length() - 1);
			}
		} catch (IOException e) {
			e.printStackTrace();
		}

		return fileContent.toString();
	}
	
	public static String readFileContent_2(String filePath) {
		String fileContent = "";
		
		try {
			fileContent = Files.readString(Paths.get(filePath), StandardCharsets.UTF_8);
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		return fileContent;
	}
}

參考資料:

  1. How to read UTF-8 encoded data from a file – Java
  2. How to append text to an existing file in Java?

2021年11月26日 星期五

得到瀏覽器 scrollbar 寬度

Get the Scrollbar Width of an Element

const scrollbarWidth = document.body.offsetWidth - document.body.clientWidth;

參考:

https://www.javascripttutorial.net/dom/css/get-the-scrollbar-width-of-an-element/


Note:

2021/11/26

不知道為什麼,現在發現

document.body.offsetWidth 會等於  document.body.clientWidth,
不過研究了一下 Bootstrap Modal 的程式源碼,

發現可以用

window.innerWidth - document.documentElement.clientWidth

來得到正確的 scrollbarWidth 

原因不明,或許跟 web 標準改變有關??

2021年7月1日 星期四

[MS sql server] 使用 Sql 語法在不同 database 中間倒資料的方法

 在登入一個 sql server下,

可以用以下語法存取另一個 sql  server,

例如以下語法可以登入名為 xxxServer, port 為 1300 的 sql server 

exec sp_addlinkedserver 'myXxxServer', '', 'SQLOLEDB', 'xxxServer,1300'  -- create linked server to xxxServer
exec sp_addlinkedsrvlogin 'myXxxServer', 'false',null, '{帳號}', '{密碼}' -- login xxxServer

然後就可以用 myXxxServer 這個名字 (可自取) 存取 xxxServer,

例如讀取其中的一個名為 xxxTable 的 table,其 table 在名為 xxxDatabase 的 database 中:

SELECT *
FROM myXxxServer.xxxDatabase.dbo.xxxTable 

還可以做到跨 sql server 的 JOIN 等操作,非常方便,例如:

SELECT *
FROM myXxxServer.xxxDatabase.dbo.xxxTable A INNER JOIN
someTable B ON A.a = B.b

或是倒資料,例如:

INSERT INTO A(a1, a2)
SELECT b1, b2
FROM myXxxServer.xxxDatabase.dbo.B

最後再用以下語法登出 xxxServer

exec sp_dropserver 'myXxxServer', 'droplogins' -- free myXxxServer linked server


**補充:

如果要倒的資料欄位裡有主鍵 (Primary Key),

需要把禁止修改主鍵的功能關掉,Update 資料完後再開回來。

再來此時不能用星號 "*" 來代表所有欄位,要把欄位名稱一個個寫出來才能成功寫入。

例如:

SET IDENTITY_INSERT A ON;

INSERT INTO A(a1, a2)
SELECT b1, b2
FROM myXxxServer.xxxDatabase.dbo.B


SET IDENTITY_INSERT A OFF;


如果想要快速的得到欄位名稱的字串,可以用以下語法來得到
(以下為得到名為 A 這個 table 的所有欄位名稱,會用逗號分隔欄位名,最後用成一串字來輸出):

SELECT SUBSTRING(
    (
	SELECT ', ' + QUOTENAME(COLUMN_NAME)
        FROM INFORMATION_SCHEMA.COLUMNS
        WHERE TABLE_NAME = 'A'
        ORDER BY ORDINAL_POSITION
        FOR XML path('')
    )
    , 3, 200000
)

執行結果就像是: [a1], [a2], [a3]

2021年3月30日 星期二

Docker 練習 - 安裝 Tomcat - 設定 virtualBox 的 Port Forwarding

此例使用 Windows 10 + virtualBox + Ubuntu 64bits

# 安裝想要的版本的 tomcat
docker pull tomcat:9.0-jdk11-openjdk

# 啟動 container
docker run -d -p 8080:8080 --name myTomcat tomcat:9.0-jdk11-openjdk

# 因為某些原因 (目前還不清楚),tomcat 的預設歡迎頁面被放到 /usr/local/tomcat/webapps.dist 下而不是 /usr/local/tomcat/webapps 下,
# 所以想要看觀迎頁面可以自己手動把頁面放到 /usr/local/tomcat/webapps 下

# 進入 container 終端 (terminal)
docker exec -it myTomcat bash

# 把觀迎頁面放到 /usr/local/tomcat/webapps 下
cp -r ./webapps.dist/* ./webapps/

# 離開終端
exit

# 在宿主機 (host) 測試是否能得到歡迎頁面的訊息 (應該會是非 404 的頁面)
curl http://localhost:8080


# 如果在 Windows 中想要看到 virtualBox 中的 Linux 的 container 中的歡迎頁面,
# 須要設定 Port Forwarding。
# 我們先要查出"主體 IP" (virtualBox 在 Windows 上使用的網路介面卡上的 IP) 和 "客體 IP" (virtualBox 內部的 Linux 所使用的 內部 IP),然後將它們對應起來。

# 先在 Windows 中使用 ipconfig 指令查出 IP,即"主體 IP",如下圖紅框處


# 再到 virtualBox 中的 Linux 使用 ip address 指今 (hostname -I 也可以)查出"客體 IP",如下圖紅框處

得到"主體 IP"和"客體 IP"後,到 virtualBox 中,
開啟 "設定" --> "網路" --> "附加到" 選 "NAT",按下"連接埠轉送",開始進行設定。
填上剛剛查得的"主體 IP"和"客體 IP"即各自想要對應的 port,"協定" 選擇自己想要的協定,
例如只是要在瀏覽器看到畫面的話,選 TCP 即可,"名稱" 可自訂。

這樣 virtualBox 的 Port Forwarding 就設定好了,可以開始來測試觀迎畫面。
在 Windows 中打開瀏覽器,網址輸入
http://{"主體 IP"}:{port}
例如此例為:
http://192.168.56.1:8080
就可以看到 Tomcat 的歡迎畫面了,如下圖:


參考資料:

Linux - Docker 和 docker-compose 的好用指令記錄 - 學習紀錄

使用 Linux Ubuntu 為例子。

#可在 Windows 上用 PowerShell 安裝 WSL (預設安裝 WSL2),參考 安裝 WSL | Microsoft Learn
wsl --install

# 安裝 docker ,參考 Install Docker Engine on Ubuntu
# 先移除例如可能 Linux 機器上已存在的非官方 package,
例如:
  • docker.io
  • docker-compose
  • docker-compose-v2
  • docker-doc
  • docker-buildx
  • podman-docker
#移除的指令:
sudo apt remove $(dpkg --get-selections docker.io docker-compose docker-compose-v2 docker-doc docker-buildx podman-docker containerd runc | cut -f1)

#設定 Dokcer apt repository
# Add Docker's official GPG key:
sudo apt update
sudo apt install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF

sudo apt update
#安裝最新版本的 Docker (包括 Docker Compose)
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
--------------------------------------------------------------

Docker 好用指令 :

# 觀看 Docker 啟動狀態 (兩個指令都可以)
service docker status
sudo systemctl status docker

# 啟動 Docker (兩個指令都可以)
service docker start
sudo systemctl start docker

# 關閉 Docker
stop docker

# 重啟 Docker
restart docker

----------------------- Image 相關 --------------------
#搜尋 image (映像檔)
docker search {關鍵字}
Ex: docker search ubuntu

# 例出所有 image
docker images

# 下載 XXX image (如要下載最新版,version (在 docker 中又稱 tag) 可省略)
docker pull {image 名}:{version}
Ex: docker pull tomcat:9.0

# 刪除 Image
docker rmi {某個 images}

----------------------- Volume 相關 --------------------
# 建立一個具名 Volume (不給名字會自動幫你建一個亂數名字的 Volume)
docker volume create {自己取的 Volume 名字}

# 列出所有**具名**的 Volume 的資訊,非具名的 Volume 不會在這顯示
(例如在 run container 時直接用 -v {宿主機路徑}:{container 路徑} 指定的 Volume)
docker volume list

# 列出特定 Volume 的詳細資訊
docker volume inspect {Volume 名字}

# 移除 Volume
docker volume rm {Volume 名字}

----------------------- Container 相關 --------------------
# ps [-a] :(process stauts ??) 例出所有運行中的 container (容器),如果要列出全部 (包含非運行的)的 container,可以加上 -a
docker ps
docker ps -a

# rm :(remove) 刪除某 container
docker rm {某 container 的 id 或 name}

# 查看某個 container 的 log
docker logs XXX

# 開啟一個或多個已停止的 container
docker start {container} {container} ......

# 停止關閉某個 container
docker stop XXX

# 重啟某個 container (等同 docker stop + docker start)
docker restart XXX

# cp : 從宿主機複制檔案或資料夾到 container,或從 container 複制檔案或資料夾到 宿主機,container 的路徑前面要加 container 名,並用冒號 (:) 分隔
docker cp {來源路徑} {目標路徑}
Ex: docker cp /home/xxx xxContainer:/user/...

# exec : 在已運行的 container 上執行命令
docker exec {參數} {某個container} {命令}
Ex: docker exec -it mysql bash

#其他可選參數:
# -w : work directory,可設定一開始所在的 container 內部 path
docker exec -w {container 中的路徑} {某個 contenr} {命令}
Ex: docker exec -i -w /some/path/inside/container tomcat pwd
像述指令也可用 bash -c "cd ... && ..." 如以下方法達成:
docker exec -i tomcat bash -c "cd /some/path/inside/container && pwd"

----------------------- 運行 Container 相關 --------------------
# 以 image 建立 container
docker create {image}

# 以 XXX images 運行 container
docker run XXX

# docker run 的各種參數 :

# --name :為要運行的 container 取名
docker run --name {自己取的名字} {某個 image}

# -p : (expose 的縮寫),暴露 container 的 port 並對應到指定的宿主機的 port,
docker run -p {宿主機的port}:{container 的 port} XXX
Ex: docker run -p 8081:8080 tomcat

# -i -t 或 --interactive --tty 或 -it :(stdin和 pueudo-tty 功能的組合) 運行 container 並進入 command line 互動介面
-t:attach時Container的螢幕會接到原來的螢幕上。
-i:attach時鍵盤輸入會被Container接手
docker run -i -t XXX
#或
docker run -it XXX

#離開互動介面
exit

# -d :Detached 的縮寫,以分離模式運行 container,進行背景常駐執行,即不進入 Container 的 命令提示界面,例如像 Daemon 這種常駐程式
docker run -d ubuntu /bin/sh -c "while true; do echo hello world; sleep 1; done"

# -v 或 --volume :分享資料夾,可掛載宿主機 (指運行 Docker 的 Linux) 的某資料夾路徑到 container 的某資料夾路徑,用於持久化檔案,避免檔案因 container 關閉而消失,有幾種寫法

# (掛載特定宿主機資料夾路徑 或 某具名 Volume) 到 container 資料夾路徑
docker -v {宿主機資料夾路徑 或 某具名 Volume}:{container 資料夾路徑}
Ex: docker -v /home/tomcat:/usr/local/tomcat/webapps

--rm : (clean up),可在 container exit 後自動刪除 container 
docker run -it --rm -p 8888:8080 tomcat:9.0

----------------------- Image 的輸出與導入 save/load 和 Container 的輸出與導入 export/import 相關 --------------------

#輸出 Docker Image 成檔案
docker save --output {要輸出的 Docker Image 檔案位置} {imageName:tag}
範例:
docker save --output /usr/local/my-images/postgres-16-image.tar postgres:16

#將 Dcoker Image tar 檔導入成 Docker Image
docker load --input {被輸出的 Docker Image 檔案位置}
範例:
docker load --input /usr/local/my-images/postgres-16-image.tar

#輸出 Docker Container 成檔案
docker export --output="{要輸出的 Docker Container 檔案位置}" {Container Name}
範例:
docker export --output="latest.tar" some_container
#將 Dcoker Container 檔導入成 Docker Container,可以重新指定 Container name, tag
docker import {被輸出的 Docker Container 檔案位置}
範例:
docker import /path/to/exampleimage.tgz
cat exampleimage.tgz | docker import - exampleimagelocal:new

參考:

----------------------------------------------
在 Container 中,可用
host.docker.internal
做為 domain  來連至本地端,
此法僅適於 Docker Desktop for Windows,並且通常為開發測試時使用

好用連結:

---------------------- docker-compose 好用指令 ---------------------------------------------------------------
#docker-compose 的指令在 Linux 上的指令是 docker compose。

#docker-compose 可以用 --project-directory 設定要啟動 docker compose project 的資料夾位置,這樣就不用一定要先用 cd 指令移到 docker compose project 位置。

#docker-compose 可以用 -f 設定要啟動 docker compose project 要用的 docker-compose.yml 設定檔位置,這樣就不用一定要先用 cd 指令移到 docker compose project 位置,docker-compose.yml 也不一定要放在 docker compose project 的資料夾內。


範例:
docker-compose --project-directory ./xxx/xxx/xxx -f  ./yyy/yyy/yyy/docker-compose.yml up -d --build

# 依據 docker-compose.yml 的設定 重新 build image 並 start service (如果已經 start 就重新 start) 某個 service
docker compose up -d --build --force-recreate --no-deps <service 名稱>
參考:

2021年3月29日 星期一

Linux - virtual box share folder 的設定

 在 Windows 中,使用 virtual box 安裝好 Ubuntu 64 server 後,

如果想方便的分享 Windows 的資料夾給 Virtual box 開啟的 VM 使用,

可以使用 VirtualBox 的 "共用資料夾" 功能。

下面示範步驟

--------------------------------------------------------------------------

安裝 VirtualBox Guest Additions :

在 Virutalbox 開啟的 VM 視窗上選擇 "裝置" --> "插入 Guest Additions CD 像"


在 VM 命令列視窗中找指令

建立資料夾 (資料夾可自訂) : mkdir -p /mnt/cdrom

掛載光碟機上去 : mount /dev/cdrom /mnt/cdrom

進入光碟機:cd /mnt/cdrom

安裝 VirtualBox Guest Additions: sh ./VBoxLinuxAdditions.run --nox11

--------------------------------------------------------------------------

接著先把 VM 關機,在 Virtualbox 上對此VM做共用資料夾的設定:"設定" --> "共用資料夾" 


其中,"資料夾路徑" 為 Windows 中要分享給 VM 的路徑,
"資料夾名稱" 為VM 要掛載的名稱 (可自訂),
"掛載點"為VM中對應的分享資料夾路徑 (可自訂)。
接著開啟 VM 並輸入指令:
掛載分享資料夾:sudo mount -t vboxsf {資料夾名稱} {掛載點}
Ex: sudo mount -t vboxsf virtualBoxShareFolder /home/virtualBoxShareFolder

2021年2月7日 星期日

Minecraft Java 版 (1.16.4, jdk 1.8) 自製 Forge Mod - 實作物品、合成表、物品特殊效果

繼上次
後,這次我們要練習自制物品,其中會包括物品的建立、名字的翻譯、相應的合成表和物品特殊效果之類的實作。

這次要建立的是一個叫做 "超級劍 (Super Sword)" 的物品,其有以下特性:
  1. 其物品類別為 "戰鬥 (Combat)" 物品。 
  2. 其有自己的名字 (英文叫 Super Sword,中文叫超級劍)。
  3. 其為"劍 (Sword)"這個物品的客制版本,有自己的攻擊傷害等數值。
  4. 其有自己的2D圖示,並拿此圖示做為 3D 時的樣子 (例如玩家拿在手上的樣子)。
  5. 其被玩家拿在手上時,有自己效果,此例為玩家不會受到攻擊及效果傷害,並且攻擊者會受到最大生命值一半的傷害。
實作版本:
Minecraft Java 版 - 1.16.4
JDK1.8
forge-1.16.4-35.1.4-mdk

先來看一下最後成品的資料結構,在這裡不會重頭無中生有的撰寫程式碼,
我會直接拿這篇文章
的程式碼拿來修改:



首先是建立 SuperSword 的物件,
main.java.com.my.mode.item.SuperSword.java :
package com.my.mode.item;

import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.IItemTier;
import net.minecraft.item.Item;
import net.minecraft.item.ItemGroup;
import net.minecraft.item.Rarity;
import net.minecraft.item.SwordItem;
import net.minecraft.item.crafting.Ingredient;
import net.minecraft.util.DamageSource;
import net.minecraft.world.World;
import net.minecraftforge.event.entity.living.LivingAttackEvent;
import net.minecraftforge.event.entity.living.LivingHurtEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;

public class SuperSword extends SwordItem{

	public final static String ITEM_ID = "super_sword"; 
	
	public SuperSword() {
		super(new IItemTier() {

			@Override
			public int getMaxUses() {
				// TODO Auto-generated method stub
				return 100;
			}

			@Override
			public float getEfficiency() {
				// TODO Auto-generated method stub
				return 0;
			}

			@Override
			public float getAttackDamage() {
				// TODO Auto-generated method stub
				return 100;
			}

			@Override
			public int getHarvestLevel() {
				// TODO Auto-generated method stub
				return 0;
			}

			@Override
			public int getEnchantability() {
				// TODO Auto-generated method stub
				return 0;
			}

			@Override
			public Ingredient getRepairMaterial() {
				// TODO Auto-generated method stub
				return null;
			}
			
		}, 100, 1, (new Item.Properties()).group(ItemGroup.COMBAT));
	}

	@SubscribeEvent
	public void onLivingAttackEvent(LivingAttackEvent event) {
		LivingEntity livingEntity = event.getEntityLiving();
		World world = livingEntity.getEntityWorld();
		if(!world.isRemote && livingEntity instanceof PlayerEntity) {
			PlayerEntity playerEntity = (PlayerEntity) livingEntity;
			if (playerEntity.getHeldItemMainhand().getItem() instanceof SuperSword
			 || playerEntity.getHeldItemOffhand().getItem() instanceof SuperSword) {
				//把事件取消,效果為攻擊無效,不會愛傷
				event.setCanceled(true);
				
				//增加效果,效果為對攻擊玩家 (player) 的生物造成傷害
				if (event.getSource().getTrueSource() instanceof LivingEntity) {
					LivingEntity attacker = (LivingEntity) event.getSource().getTrueSource();			
					attacker.attackEntityFrom(DamageSource.GENERIC, attacker.getMaxHealth() / 2);
				}
			}
		}
	}
	
	@SubscribeEvent
	public void onLivingAttackEvent(LivingHurtEvent event) {
		LivingEntity livingEntity = event.getEntityLiving();
		World world = livingEntity.getEntityWorld();

		if(!world.isRemote && livingEntity instanceof PlayerEntity) {
			PlayerEntity playerEntity = (PlayerEntity) livingEntity;
			if (playerEntity.getHeldItemMainhand().getItem() instanceof SuperSword
			 || playerEntity.getHeldItemOffhand().getItem() instanceof SuperSword) {
				//把事件取消,效果為傷害無效,不會愛傷
				event.setCanceled(true);
			}
		}
	}
}


在 SuperSword.java 的建構子 (constructor) 中,SuperSword 繼承了 SwordItem,
所以其實本質就是一個 "劍 (Sword)" 物品,
在建構子中,我們用
net.minecraft.item.SwordItem.SwordItem(IItemTier tier, int attackDamageIn, float attackSpeedIn, Properties builderIn)
重新定義了 SuperSword 的攻擊傷害 (attackDamageIn)、攻擊速度 (attackSpeedIn) 和物品類別 (ItemGroup.COMBAT)。

再來我們實作兩個監聽事件的 method,分別為 onLivingAttackEvent()、onLivingAttackEvent() 來監聽 LivingAttackEvent (當生物被攻擊時觸發)、onLivingAttackEvent (當生物被傷害時觸發)。
在 method 中,先取得事件的對象,即被攻擊、被傷害的對象,
LivingEntity livingEntity = event.getEntityLiving();
如果對象為玩家 (PlayerEntity) 的話,再來判斷玩家手上是否拿著 SuperSword 物品
if (playerEntity.getHeldItemMainhand().getItem() instanceof SuperSword
			 || playerEntity.getHeldItemOffhand().getItem() instanceof SuperSword)
如果玩家拿著 SuperSword,則將事件取消,而
LivingAttackEvent 各 LivingHurtEvent 被取消的效果為:事件的對象不會受到攻擊、受傷的傷害。

在 onLivingAttackEvent() 裡我們使用
if (event.getSource().getTrueSource() instanceof LivingEntity) {
     LivingEntity attacker = (LivingEntity) event.getSource().getTrueSource();			
     attacker.attackEntityFrom(DamageSource.GENERIC, attacker.getMaxHealth() / 2);
}
                
再多取得攻擊者物件,判斷攻擊者是否為 LiveingEntity 後,
對其產生了一個普通傷害,傷害為攻擊者最大生命值的一半。

建立設計好 SuperSword.java 後,再來是要把 SuperSword 物品註冊到遊戲中 (先不管名字和外觀),首先先建立一個工具類, Items.java
  
main.java.com.my.mode.item.Items.java :
package com.my.mode.item;

import com.my.mode.MyMod;

import net.minecraft.item.Item;
import net.minecraftforge.fml.RegistryObject;
import net.minecraftforge.registries.DeferredRegister;
import net.minecraftforge.registries.ForgeRegistries;

public class Items {
	private static DeferredRegister<Item> REGISTER = null;
	
	public static DeferredRegister<Item> getRegister(){
		if (REGISTER == null) {
			REGISTER = DeferredRegister.create(ForgeRegistries.ITEMS, MyMod.MOD_ID);
		}		
		//注冊自製物品
		REGISTER.register(SuperSword.ITEM_ID, () -> new SuperSword());
		
        return REGISTER;
    }
}

在 Items.java 裡, 我們會使用 DeferredRegister 來將 SuperSword 註冊進去。
接著我們修改一下 ModEventBusHandler.java

main/java/com/my/mode/ModEventBusHandler.java :
package com.my.mode;

import com.my.mode.item.Items;

import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;

public class ModEventBusHandler {
	public static void register() {
		IEventBus modEventBus = FMLJavaModLoadingContext.get().getModEventBus();
		modEventBus.register(ModEventBusHandler.class);
		modEventBus.register(new ModEventBusHandler());
		//註冊自製物品,並將其註冊到 ModEventBus 中
		Items.getRegister().register(modEventBus);
	}
	
	@SubscribeEvent
	public void onCommonSetupEvent(FMLCommonSetupEvent event) {
		ForgeEventBusHandler.register();
	}
}

在這裡,我們使用了
Items.getRegister().register(modEventBus);
來將 SuperSword 主冊到遊戲中。
此時 SuperSword 這個物品已經存在在遊戲中了,但它還沒有效果,即是說 SuperSword 裡的
onLivingAttackEvent() 和 onLivingAttackEvent() 不會有任何的效果,
因為我們只有註冊物品,但還沒有把監聽事 method 給註冊到遊戲中。
所以我們要來修改
com.my.mode.ForgeEventBusHandler.java :
package com.my.mode;

import com.my.mode.item.SuperSword;

import net.minecraft.client.Minecraft;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityType;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.item.ItemEntity;
import net.minecraft.entity.monster.MonsterEntity;
import net.minecraft.entity.passive.FoxEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.Item;
import net.minecraft.util.DamageSource;
import net.minecraft.world.World;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.entity.item.ItemTossEvent;
import net.minecraftforge.event.entity.living.LivingAttackEvent;
import net.minecraftforge.event.entity.living.LivingEvent;
import net.minecraftforge.event.entity.living.LivingHurtEvent;
import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.eventbus.api.SubscribeEvent;

public class ForgeEventBusHandler {
	public static void register() {
		IEventBus forgeEventBus = MinecraftForge.EVENT_BUS;
		forgeEventBus.register(ForgeEventBusHandler.class);
		forgeEventBus.register(new ForgeEventBusHandler());
		
		//註冊超級劍 (super sword) 的事件效果
		forgeEventBus.register(new SuperSword());
		forgeEventBus.register(SuperSword.class);
	}
	
	// Event 有分成在 ModEventUs 上的,或在 ForgeEventBus 上的,
	// 此例的 ItemTossEvent 為在 ForgeEventBus 上的 Event
	@SubscribeEvent
	public void onItemTossEvent(ItemTossEvent event) {
		//ItemTossEvent: 物品丟棄時觸發的 Event, 
		//在遊戲中可用按鍵 q 來丟棄物品
		ItemEntity item = event.getEntityItem();
		World world = item.getEntityWorld();
		
		if (!world.isRemote) { // 判斷是否為 logical server 端,即處理邏輯的那端
							   // 如果此例的 isRemote 為 true,即為 logical client 端
							   // 可參考
							   // https://hackmd.io/@immortalmice/Hkj9s-CvU/https%3A%2F%2Fhackmd.io%2F%40immortalmice%2FrJKayrf9U
			
			//建立一個狐狸物件
			FoxEntity fox = new FoxEntity(EntityType.FOX, world);
			fox.setPosition(item.getPosX(), item.getPosY(), item.getPosZ());
			//將物件放到世界中
			world.addEntity(fox);
		}
	}
}
在 ForgeEventBusHandler.java 中,只有多加了兩行程式碼,就是
forgeEventBus.register(new SuperSword());
forgeEventBus.register(SuperSword.class);
用來把 SuperSword 裡的事件監聽 method 給註冊到遊戲中 (或說 ForgeEventBus 中)。

Note:

forgeEventBus.register(new SuperSword()); 用來註冊 SuperSword 裡的 non-static method,
forgeEventBus.register(SuperSword.class); 用來註冊 SuperSwrod 裡的 static method。

到這裡,SuperSword 已經正常的被加到遊戲中了,並且只要玩家拿著 SuperSword (左手或是右手),就不會受到任何攻擊及效果傷害,攻擊者如果對玩家發動攻擊就會受到攻擊者一半生命值的傷害。

但是 SuperSword 這時還沒有自己的名字和外觀,此時 SuperSword 在遊戲中的名字會是:
item.mymod.super_sword。

所以我們要來設定 SuperSword 的名字和外觀。
先建立兩個 json 格式的翻譯檔,en_us.json 和 zh_tw.json,其中內容為:
/main/resources/assets/mymod/lang/en_us.json :
{
    "item.mymod.super_sword": "Super Sword"
}
/main/resources/assets/mymod/lang/zh_tw.json :
{
    "item.mymod.super_sword": "超級劍"
}

可以看到其定義了英文 (en_us) 和繁體中文 (zh_tw) 的名字,
要注意的是 json 檔所放的路徑是規定的,其中 "mymod" 可改成自己 mod 的 id。

再來是設定外觀,在這邊我己經先畫了一張 16x16 pxiel 背景透明的圖,super_sword.png,
擺在以下位置,
/main/resources/assets/mymod/textures/item/super_sword.png :
一樣的,路徑是規定的,"mymod" 可換成自己 mod 的 id。

有了圖了以後,建立一個 super_sword.json 檔來設定外觀的資料:
/main/resources/assets/mymod/models/item/super_sword.json:
{
    "parent": "item/generated",
    "textures": {
        "layer0": "mymod:item/super_sword"
    },
    "display": {
	    "thirdperson_righthand": {
	      "rotation": [0, 90, 0],
	      "translation" : [0, 3, 0],
	      "scale" : [1, 1, 1]
	    }
  	}
}

在這邊我們多設定了 thirdpersion_righthand,也就是當物品拿在右手上時,
第三人稱看到時,物品的旋轉(rotation), 位移 (translation) 和縮放 (scale) 狀態,
三個數字分別代表 x, y, z 軸。
其中我調整了 ratation 和 translation,把 SuperSword 調成看起來像是被玩家握在劍柄的狀態,
如果沒調整的話,玩家會握在物件圖片 y 軸 (也就是高度沒軸) 的 2/4 處,對 16 x 16 pixel 的圖就是高度第 5 格到第 8 格處。

Note:

此時如果把 SuperSword 拿在左手,會發現一樣如拿在右手上一樣有吃到調整的設定,
應該是因為當有設定右手時,如果左手沒設定的話會自動以右手設定為準。

此時 SuperSword 的設定大致已完成了,但這時 SuperSword 還沒有自己的合成表配方,
也就是除非使用了遊戲指令,例如:
/give @p mymod:super_sword
不然無法正常得到此物品。

所以我們現在要來建立 SuperSword 的配方。
合成設定除了用程式來實以外,
minecraft forge 推出了方便使用的 json 格式設定,這邊會以 json 格式來做設定。
建立一個 super_sword_from_crafting.json,內容如下:

/main/resources/data/mymod/recipes/super_sword_from_crafting.json :
{
    "type": "minecraft:crafting_shaped",
    "pattern":
    [
        " x ",
        "xxx",
        " x "
    ],
    "key":
    {
        "x":
        {
            "item": "minecraft:dirt"
        }
    },
    "result":
    {
        "item": "mymod:super_sword",
        "count": 1
    }
}

"type" 代表了合成表的類型
minecraft:crafting_shaped 是有序配方,
代表配方為特定物品用特定方式排列,類如 minecraft 中的 "門 (door)" 的合成。
或是可以選 minecraft:crafting_shapeless,為無序配方,
代表此配方不在乎原料如何排列,只有由特定的原料即可合成,
類如 minecraft 中的 "火焰彈 (fire_charge)" 的合成。
可參考

"pattern" 即為合成表,給 type = minecraft:crafting_shaped 使用 (minecraft:crafting_shapeless 則是使用 ingredients),x 為物品的 key 值 (不一定要用x,可以用任何想要的英文字)。
而"key" 代物你在 "pattern" 中使用的 key 值各代表什麼物品。
"Result" 代表產生的物品和數目。

在此例中,我們設定了,只要把5個泥土排成十字就可以合成出 SuperSword。

至此已大功告成了! 我們可以開始來實際看看在遊戲中 SuperSword 的效果了,
以下為最後的成果實機測試影片:



源碼下載:

參考資料: