データパックとリソースパックでプレイヤーの名前を変更する(ように見せる)

:::note warn
この記事は **JavaEdition 1.21** にて動作確認を行っています
データパック及びコマンドにある程度の知識があることを前提としています。

追記: defaultフォントであることを前提としています。
それ以外のフォント(uniformなど)については [6. 追記](#6-追記) へ
:::



# 1. はじめに
今回こんなものを作ってみます。

<blockquote class="twitter-tweet" data-media-max-width="560"><p lang="zxx" dir="ltr"><a href="https://t.co/JcaHi0k4rA">pic.twitter.com/JcaHi0k4rA</a></p>&mdash; ヒコマ (@hikoma0000) <a href="https://twitter.com/hikoma0000/status/1865234122278604955?ref_src=twsrc%5Etfw">December 7, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

サイドバーだけでなく、チャットまで名前が変わっていますね。

::: note info
ここからは説明や過程が多くなります。
出来上がったものが見たい方は [5. おわりに](#5-おわりに) まで飛んでください
:::


# 2. 簡易的な説明
String Parsing(文字列解析)について調べていたときに見つけた、これが説明であり発端です [^1]
[Minecraft-String-Utilities のリポジトリ (https://github.com/McTsts/Minecraft-String-Utilities) ](https://github.com/McTsts/Minecraft-String-Utilities?tab=readme-ov-file#getting-width)
> ## Getting Width
> Getting the length of a non-JSON string is just a matter of parsing it and then using a look-up table for all characters you want to support. You then add up the widths of all characters (while respecting that the gap between characters is 1 pixel).
>
> You can for example use this to determine the length of a player name, then using <ins>negative spaces</ins> in team prefix/suffixes to replace the player name with a different text.
> 
> Getting the length of a JSON string is a fairly convoluted process. You first need to string parse the string, then <ins>JSON parse</ins> it, then extract all characters from the JSON, while separating bold and non-bold characters. You use the same look-up table process as for normal strings, with the exception, that all bold characters are 1 pixel wider than the non-bold variant.

何言ってるのかわからないので簡単に日本語訳

> 文字列の長さ(ピクセル)を取得するには、一文字ずつに分解して、それぞれの文字のピクセル数を記録し、そこから参照すればいい。あとはそれを合計するだけ。（文字と文字の間隔が1ピクセルであることを尊重しながら）
例えば、**プレイヤー名の長さを取得して、teamコマンドのprefix/suffixで負のスペースを使えばプレイヤー名を変えられる。**


コマンドを結構やっている方なら、もうここで何をすればいいのかわかったかも...？



### 負のスペースとは？
上の文章において、「負のスペース」という単語が出てきました。
これは一体何でしょうか？

結論から言いますと、原文にある通り **`negative space`** (以降 ネガティブスペース) 
というカスタムフォントの要素です。

ネガティブスペース、及びカスタムフォントについては、[まっしゅ](https://x.com/hama_sshh) さんの [この動画 (https://www.youtube.com/watch?v=w0Zraz-o8yE)](https://www.youtube.com/watch?v=w0Zraz-o8yE) や
[ネガティブスペースフォントのリポジトリ (https://github.com/AmberWat/NegativeSpaceFont)](https://github.com/AmberWat/NegativeSpaceFont) にてわかりやすく解説されているので見ていただきたいのですが、簡単に説明すると...

<br>
スペースなし

![no_space.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925631/83157845-0b46-1e52-1061-e9beb838af70.png)


普通のスペース

![normal_space.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925631/65526b06-dcdf-8e96-da12-643d72778d26.png)


ネガティブスペース

![negative_space_-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925631/e8101753-1754-46c0-235a-2ad62a8c7471.png)

のように、普通のスペースを「正のスペース」「プラス方向に広がるスペース」と考えると、
ネガティブスペースは **「負のスペース」「マイナス方向に広がるスペース」** と捉えられます。

なのでネガティブスペースを大きくすると...

![negative_space_-15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925631/5c4eb047-2434-27e7-6ad5-073c12149f1b.png)

このように前後関係が反対になったりもします。

プレイヤー名変更データパックではこれが最重要要素になります。



# 3. 本格的な説明
[2. 簡易的な説明](#2-簡易的な説明) と [負のスペースとは？](#負のスペースとは) の内容を組み合わせて考えます。

まず`hoge`というプレイヤーが`test`とチャットをしたという過程で話します
![Slide1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925631/3ea18d7a-5169-39ac-8699-9abfa20c1480.jpeg)

プレイヤーが所属している`team`の`prefix`にネガティブスペースを入れ、画面外に飛ばします
![Slide2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925631/00301518-a80a-0603-e910-da4def41728a.jpeg)

その後`suffix`に変更後の名前を入れます。（ここでは`fuga`としています）
![Slide3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925631/454b84b2-ffab-71a9-6dda-c448067b4510.jpeg)

変更後の名前の前に普通のスペースを挿入することで画面内に戻します。
スペースの長さは、`prefix`に入れたネガティブスペースの長さと同じにします。
![Slide5.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925631/45133705-07bf-ef2f-afb5-e62130d39643.jpeg)

プレイヤー名の長さ（`合計ピクセル数`）分だけ（今回の場合は`hoge`の長さ）、スペースの後にネガティブスペースを入れ、位置を調整します。
![Slide6.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925631/f2713eec-92f3-63db-2639-5081ff928e98.jpeg)



つまり、**本来の名前を画面外に飛ばし、文字を同じ場所に出すことで名前を変更したように見せることができる** ということです。

理論はこれで終わりです。あとはコマンドを書きましょう。



# 4. 作成手順
1. プレイヤー名の取得
2. プレイヤー名の`合計ピクセル数`を計算
3. `team`の作成と加入
4. `prefix`と`suffix`を変更



## 4-1. プレイヤー名の取得

`loot_table`の`fill_player_head`でプレイヤー名を取得します。

```r:<font color="#C3CAD5">function:</font> cnn:core/main/
summon minecraft:item_frame ~ ~500 ~ {Invisible:true,Fixed:true,Tags:[cnn.head_frame]}

execute positioned ~ ~500 ~ align xyz run loot replace entity @e[dx=0,type=item_frame,tag=cnn.head_frame,limit=1] container.0 loot cnn:head

execute positioned ~ ~500 ~ align xyz as @e[dx=0,type=item_frame,tag=cnn.head_frame,limit=1] run function cnn:core/main/head
```

```json:<font color="#C3CAD5">loot_table:</font> cnn:head
{
  "type": "minecraft:entity",
  "pools": [
    {
      "rolls": 1,
      "entries": [
        {
          "type": "minecraft:item",
          "name": "minecraft:player_head",
          "functions": [
            {
              "function": "minecraft:fill_player_head",
              "entity": "this"
            }
          ]
        }
      ]
    }
  ]
}

```

```r:<font color="#C3CAD5">loot_table:</font> cnn:core/main/head
# player_headからplayer_nameを取り出し
data modify storage custom_nick_name:_ player_name set from entity @s Item.components."minecraft:profile".name

# 用済み
kill @s
```



## 4-2. プレイヤー名の`合計ピクセル数`を計算

そして一文字あたりのピクセル数を調べます。
これはゲーム内で人力でやります。

ネガティブスペースなし
![0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925631/b39c306d-a6fb-d328-eafb-be11de507947.png)

`-3`だと...
![-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925631/71b94d91-b4ab-1d78-df4f-0c7fa015d3e2.png)

`-6`で完全に重なりました。
![-6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925631/1072b900-4f1c-36eb-5ab4-99b1bca303b5.png)

さらに [2. 簡易的な説明](#2-簡易的な説明) で書いていた通り、文字と文字の間は`1`ピクセルなので、
実際の一文字のピクセル数は`5`となります。

では、一文字分のピクセル数を`5`としてテストしてみましょう。

![zure.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925631/5b58f5c8-2943-f49e-e89d-c1fcd071ee19.png)

...ズレてしまいました。
おそらくプレイヤー名の長さの計算がおかしいのでしょう。調べてみます。

（前後関係がわかりやすいように文字に色を付けています）
ネガティブスペースなし
![i_no.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925631/1a225658-bc9f-5b35-3cd2-28dd32e310a5.png)

ネガティブスペースを`-6`だけ入れたもの
![i_-6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925631/005cebcf-d1ab-62a7-4822-dd2028fae61a.png)

おかしいですね
`a`の場合`-6`でちょうど重なったのにもかかわらず、`i`では通り越してしまっています。

つまり文字の種類ごとにピクセル数が変わるということです。
なので、面倒くさいですが、プレイヤー名に使える文字すべてのピクセル数を調べました。


|1px|    2px     |3px|4px|                  5px                  |
|:-:|:----------:|:-:|:-:|:-------------------------------------:|
|i|l （小文字の`L`）|I （大文字の`i`）|f|_ (アンダーバー) を含むその他のアルファベットと数字 |
| |              |t|k|                                           |

これを元に場合分けして計算します。

```r:<font color="#C3CAD5">function:</font> cnn:core/main/
# 処理用に一時的に移動＆文字間の1ピクセルを計算にいれるために、player_nameの長さをスコアに代入
data modify storage custom_nick_name:_ __temp__.name set from storage custom_nick_name:_ player_name
execute store result score $total_pixel cnn.main run data get storage custom_nick_name:_ player_name

# プレイヤーの名前の合計ピクセル数を計算
function cnn:core/pixel/loop
```

```r:<font color="#C3CAD5">function:</font> cnn:core/pixel/loop
# 保存しておいて、後で同じかチェックする
scoreboard players operation $else cnn.main = $total_pixel cnn.main

# player_nameの一文字目をwordに入れる
data modify storage custom_nick_name:_ word set string storage custom_nick_name:_ __temp__.name 0 1

# 文字幅が特殊なものはここで処理
execute if data storage custom_nick_name:_ {word:"i"} run scoreboard players add $total_pixel cnn.main 1
execute if data storage custom_nick_name:_ {word:"l"} run scoreboard players add $total_pixel cnn.main 2
execute if data storage custom_nick_name:_ {word:"I"} run scoreboard players add $total_pixel cnn.main 3
execute if data storage custom_nick_name:_ {word:"t"} run scoreboard players add $total_pixel cnn.main 3
execute if data storage custom_nick_name:_ {word:"f"} run scoreboard players add $total_pixel cnn.main 4
execute if data storage custom_nick_name:_ {word:"k"} run scoreboard players add $total_pixel cnn.main 4

# 保存されていたものと値が同じ = 特殊処理の際に数値が追加されていない = 文字幅が特殊ではないので5を追加
execute if score $else cnn.main = $total_pixel cnn.main run scoreboard players add $total_pixel cnn.main 5

# player_nameの一文字目を消す
data modify storage custom_nick_name:_ __temp__.name set string storage custom_nick_name:_ __temp__.name 1

# player_nameがなくなるまで再帰
execute unless data storage custom_nick_name:_ {__temp__:{name: ""}} run function cnn:core/pixel/loop
```
これで大丈夫です。




## 4-3. `team`の作成と加入

マクロを使って、名前を変更するプレイヤー専用の`team`を作ります

```r:<font color="#C3CAD5">function:</font> cnn:core/main/
# 名前変更用teamを作成し加入
# マクロ用に移動
data modify storage custom_nick_name:_ macro.nick_name set from storage custom_nick_name: input
data modify storage custom_nick_name:_ macro.player_name set from storage custom_nick_name:_ player_name
execute store result storage custom_nick_name:_ macro.total_pixel int 1 run scoreboard players get $total_pixel cnn.main

function cnn:core/team/macro with storage custom_nick_name:_ macro
```

```r:<font color="#C3CAD5">function:</font> cnn:core/team/macro
$team add cnn_$(player_name)

$team join cnn_$(player_name) $(player_name)
```




## 4-4. `prefix`と`suffix`を変更
作成した`team`の`prefix`にネガティブスペースを入れ、
`suffix`にスペースと変更後の名前を入れます。

```r:<font color="#C3CAD5">function:</font> cnn:core/team/macro
# ネガティブスペースで画面外へぶっ飛ばす
$team modify cnn_$(player_name) prefix {"translate":"space.-max"}

# ただのスペースで画面内へ戻し、位置調整後、偽の名前をセット
$team modify cnn_$(player_name) suffix [{"translate":"space.max"},{"translate":"space.-$(total_pixel)"},{"text":"$(nick_name)"}]
```

今までのものを合わせてテストすると...
![result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925631/c626382d-ec30-a02b-8293-c0b4f89d55ff.png)
できました！完璧です！

<details><summary>本当にできてるかのテスト</summary>

Aは普通にチャットしたもの。Bは「hikoma0000」と名前変更してみたもの
![A.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925631/b966bc4a-9531-2e5b-8b77-2e648fff2681.png)
![B.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925631/79b170a6-3b59-8781-2f0f-94c87391866a.png)

全く同じになっていますね。
</details>


# 5. おわりに

今回作ったデータパックに、機能を少し付け加えたものを [ここ (https://github.com/hikoma0000/CustomNickName)](https://github.com/hikoma0000/CustomNickName) に公開しました。
ぜひお使いください。


初めてQiitaの記事を書いてみました。至らぬところがございましたら、ぜひコメントしていただければ幸いです。

ご清覧ありがとうございました。


# 6. 追記

完っ全にuniformフォントについて忘れていました...

![2024-12-09_11.43.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925631/6b1b3694-fd14-e280-c301-2ce19e92b190.png)

その内対応します...

[^1]: ~~つまりアイデア自体は自分じゃないってこと~~