storageを使った小技

この記事は Minecraft Command Advent Calendar 2023 14日目の記事です。

https://qiita.com/advent-calendar/2023/mccmd

:::note warn
この記事の内容はすべて **JavaEdition 1.20.2** を基準に書かれており、その他のバージョンにおける動作確認等は一切行われていません。
一応1.13以上なら動くはずだけど、検証は全くしてないよ。ごめんね。
:::

## TL;DR

| | $n<0$ | $n=0$ | $0<n$ |
|:--|:-:|:-:|:-:|
| storageインクリメント | 変化なし | 変化なし | 増加 |
| storageデクリメント[^6億] | 増加 | 変化なし | 減少 |
| storageデクリメント*[^6億] | 変化なし | 変化なし | 減少 |

[^6億]: 前者はstoreサブコマンドで定数倍するものを、後者はdataコマンドで定数倍するものを指す。

storageインクリメント/デクリメントの時は値の範囲に気を付けよう！

## 前置き

この記事は本イベントに参加するにあたって[別記事](https://qiita.com/crafter1415/items/bb309c9442a980d9fcae)に書いたものの読み返した際にこの内容は合わないと感じ消すことを決めたものを勿体なさから別記事に分けただけの記事であり、その性質上記事としてはやや短めです。

残念ながら、この記事は初心者向け記事ではありません。特に本題。

## 本題(没部分)

突然ですが、皆さんは以下の二つのコマンドの違いが分かりますか？
```
execute store result storage _: _ int 0.5 run data get storage _: _
```
```
execute store result storage _: _ int 1 run data get storage _: _ 0.5
```
どちらも `_: _` の中身を0.5倍するコマンドです。
軽量化に触れている人は「二個目のコマンドはscaleがついてるから遅い！[^|]」と思ったかもしれませんが、今回触れる違いはここではありません。

[^|]: https://twitter.com/intsuc/status/1499005259351736320

両者ともに自然数の範囲では結果が変わることはありませんが、`_: _` に負の値を保持させた状態で実行すると結果が変化してきます。

```
data modify storage _: _ set value -7
```
```
execute store result storage _: _ int 0.5 run data get storage _: _
tellraw @s {"storage":"_:","nbt":"_"}
# -> -3
```
```
execute store result storage _: _ int 1 run data get storage _: _ 0.5
tellraw @s {"storage":"_:","nbt":"_"}
# -> -4
```

後者のコマンドの結果は1小さくなりました。
これは全ての場合でこうなるわけではなく、初期値が負の奇数だった場合のみ、後者のコマンドの結果が1小さくなります。

お気づきかと思いますが、今回重要になるのは「負の値における小数点以下の取り扱い」です。
これには、二つのコマンドの「double型→int型の変換の違い」が関わっています。

前者は `data` コマンドで得た値(int型) を `execute store` サブコマンドでdouble型にキャストしたうえで`<scale>`倍し、結果をint型にキャストしてストレージに代入しています[^6]。
ここでの「int型にキャスト」という操作は「0に近づくように」丸められます[^7]。

[^6]: MCP-Reborn 1.20により生成されたコードにより検証。
https://github.com/Hexeption/MCP-Reborn
[^7]: Java17言語仕様 5章5節 「型キャスト」
https://docs.oracle.com/javase/specs/jls/se17/html/jls-5.html#jls-5.5
Java17言語仕様 5章1節3項 「狭くなる型変換」
https://docs.oracle.com/javase/specs/jls/se17/html/jls-5.html#jls-5.1.3

対する後者は `data` コマンドのscale付きの値を取得する処理の中で、ストレージ内の数値をdouble型として取得したうえで `<scale>` 倍し、「floor関数[^8]でint型に変換」した結果を実行結果として返します[^6]。
ここでの「floor関数での変換」操作は「負の無限大に近づくように」丸められるという特徴を持ちます。

[^8]: ここでのfloor関数とは `java.lang.Math#floor` ではなく `net.minecraft.util.Mth#floor` を指す。
このクラス自体はMinecraftに内包された数値計算用クラスであり関数のドキュメントもないが、ここでは噛み砕いて「`Math#floor`を呼び出して得た結果をintにキャストして返す関数」として解釈しても何ら問題はない。

そのため、丸めの方向が同じである自然数の範囲では差は生じず、
丸めの方向が異なる負の数の範囲において丸めが発生した場合にのみ誤差が生じるわけです。

## 利用

小数の丸めを利用したコマンドの代表的[^🐈]な例として、storageインクリメント及びstorageデクリメントがあります。

[^🐈]: 代表例なのに名前で調べても全然出てこないので仕方なく利用例で調べたが、それでも2023/12/06現在デクリメントは96件(うちTUSB57件)、インクリメントに関しては10件しか出てこなかった。なんで？

### storageデクリメント

```mcfunction:storage_decrement
execute store result storage mcac2023: Day14 int 0.9999999999 run data get storage mcac2023: Day14
```

デクリメントは元の値 $n\\ [0<n]$ からその $10^{-10}$ 倍を引くことで $n-1<x<n$ を満たす値を作成し、それに対して丸め操作を行うことで $n-1$ を得ています。
$n<0$ の場合は $n<x<n+1$ を満たす値が作成されますが、この実装ではstoreサブコマンドによる丸めのため、より $0$ に近い $n+1$ が使われ、ストレージ内の値はインクリメントされます。
対して、

```mcfunction:storage_decrement_prime
execute store result storage mcac2023: Day14 int 1 run data get storage mcac2023: Day14 0.9999999999
```
このような実装を行うと、丸め操作はdataコマンドの中で行われるため負の無限大に近づくよう丸められるようになり、範囲が $n<0$ の時より負の無限大に近い $n$ が用いられ、ストレージ内の値が変化しなくなります。

負の値でデクリメントを行いたい場合、

 - $n<0$ である $n$ に対して $n-1 < x < n$ を満たす値を生成する
 - 負の無限大に近づける丸め込みをする必要があるので、生成はdataコマンドの定数倍で行う必要がある

の二点から、

```mcfunction:storage_decrement_negative
execute store result storage mcac2023: Day14 int 1 run data get storage mcac2023: Day14 1.0000000001
```
を使えばいいと分かります。

### storageインクリメント

```mcfunction:storage_increment
execute store result storage mcac2023: Day14 int -1 run data get storage mcac2023: Day14 -1.0000000001
```

インクリメントでは丸め込みがdataコマンドで行われるため、初期値 $n\\ [0<n]$ に $10^{-10}$ 倍を足したうえで正負を反転することにより元の値 $n$ に対して $-n-1<x<-n$ を満たす値が作成され、負の無限大に近い $-n-1$ に丸め込まれたのちに $-1$ 倍されることにより $n+1$ が得られます。
こちらは $n<0$ の場合、作成される値が $-n<x<-n+1$ となり、負の無限大に近い $-n$ が $-1$ 倍されて $n$ となるため、ストレージの値は変化しません。負の値でインクリメントを行いたい場合は、先ほどのデクリメントのコマンドを使うのが良さそうです。

## 蛇足

コマンドは複数になってしまいますが、値の範囲を考えずにインクリメント/デクリメントをするコマンドについても考えてみます。
そのためには入力された数値の符号を判定する必要がありますが、これについては与えられた値に極端な倍率を掛けることで `-2147483648`, `0`, `2147483647` のどれかにすることができるので、この値によって符号を判別します。

### デクリメントコマンド

```mcfunction:storage_increment_all
#>
# 値に関わらずインクリメントを行う関数
# CC0 1.0 Universal

# 21億倍して符号以外の情報を飛ばす
execute store result storage _: _ int 2147483648 run data get storage mcac2023: Day14

# 補足: 値がそもそも存在しなかった時に更新を行いたくない場合、ここで一時変数の値を消すことで更新をやめさせることが出来る
# execute unless data storage mcac2023: Day14 run data remove storage _: _

# 値が負ならデクリメントコマンドの挙動がインクリメントと同じになる
execute if data storage _: {_:-2147483648} store result storage mcac2023: Day14 int 0.9999999999 run data get storage mcac2023: Day14

# 値が0なら単純に1を代入する
execute if data storage _: {_:0} run data modify storage mcac2023: Day14 set value 1

# 値が正ならインクリメントコマンドが使える
execute if data storage _: {_:2147483647} store result storage mcac2023: Day14 int -1 run data get storage mcac2023: Day14 -1.0000000001

# 最後に一時変数を消す
data remove storage _: _

```

### インクリメントコマンド

```mcfunction:storage_decrement_all
#>
# 値に関わらずインクリメントを行う関数
# CC0 1.0 Universal

# 21億倍して符号以外の情報を飛ばす
execute store result storage _: _ int 2147483648 run data get storage mcac2023: Day14

# 補足: 値がそもそも存在しなかった時に更新を行いたくない場合、ここで一時変数の値を消すことで更新をやめさせることが出来る
# execute unless data storage mcac2023: Day14 run data remove storage _: _

# 値が負なら前章に挙げたコマンドを使う
execute if data storage _: {_:-2147483648} store result storage mcac2023: Day14 int 1 run data get storage mcac2023: Day14 1.0000000001

# 値が0なら単純に-1を代入する
execute if data storage _: {_:0} run data modify storage mcac2023: Day14 set value -1

# 値が正ならデクリメントコマンドが使える
execute if data storage _: {_:2147483647} store result storage mcac2023: Day14 int 0.9999999999 run data get storage mcac2023: Day14

# 最後に一時変数を消す
data remove storage _: _
```

## EOF

皆様も良きコマンドライフを！