Skip to content

Commit

Permalink
記事を書いた、更新もした
Browse files Browse the repository at this point in the history
  • Loading branch information
takusan23 committed Nov 16, 2024
1 parent b0179e7 commit b313a34
Show file tree
Hide file tree
Showing 3 changed files with 308 additions and 1 deletion.
40 changes: 39 additions & 1 deletion content/posts/amairo_kotlin_coroutines_suspend.md
Original file line number Diff line number Diff line change
Expand Up @@ -3375,7 +3375,6 @@ class MainActivity : ComponentActivity() {
```

`Dispatchers`の章で話した通り、`Dispatchers.Main`やシングルスレッドの`Dispatchers`を使えば、他スレッドから参照されたり変更されたりしないので、ちゃんと期待通りになります。
ちなみに`repeat { }`の中で`withContext { }`よりも`withContext { }`の中で`repeat { }`のほうが良いです。

```kotlin
class MainActivity : ComponentActivity() {
Expand Down Expand Up @@ -3414,6 +3413,45 @@ class MainActivity : ComponentActivity() {

また、変数に`@Volatile`を付けても期待通りにならないそうです(これがなんなのかいまいちわからないのでスルーします)

## ループと withContext
ちなみに`repeat { }`の中で`withContext { }`よりも`withContext { }`の中で`repeat { }`のほうが良いです。
いくら軽いとはいえ、繰り返し文のたびに`withContext { }`を呼び出すのはコストがかかるそうです。

```kotlin
class MainActivity : ComponentActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()

lifecycleScope.launch {
var count = 0
val time1 = measureTimeMillis {
repeat(10_000) {
withContext(Dispatchers.Default) {
count++ // 特に意味のないことをしてます。withContext の呼び出しが地味に重いので、time2 のようにループ開始前に呼び出しておくこと
}
}
}
val time2 = measureTimeMillis {
withContext(Dispatchers.Default) {
repeat(10_000) {
count++
}
}
}
println("ループの中で withContext = $time1")
println("withContext の中でループ = $time2")
}
}
}
```

```plaintext
ループの中で withContext = 716
withContext の中でループ = 0
```

## スレッドセーフ
いくつかあります。
まずは`AtomicInteger`、これは`Int`ですが他にも`Boolean`とかもあるはず。何回実行しても`10000`になります。
Expand Down
1 change: 1 addition & 0 deletions content/posts/android_okhttp_thanks_happy_eyeballs.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ created_at: 2024-10-29
tags:
- Android
- OkHttp
- AWS
---
`Pixel Watch 3`に朝起こしてもらってますが、起こしてくれない時があった!!
どうしたんかなって見てみたら、**オーバーヒート**でシャットダウンしてたらしい。腕を怪我する前に守ってくれた模様、ありがと~~
Expand Down
268 changes: 268 additions & 0 deletions content/posts/minecraft_mod_1_21_2_migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
---
title: 自作 MOD の Minecraft 1.21.2 移行メモ
created_at: 2024-11-17
tags:
- Minecraft
- Kotlin
- Java
---
どうもこんばんわ。
一応言っておくと定住先でよく使われる**1.12.2**ではなく**1.21.2**です。
(最近はどこに定住しているのか知らない)

# 本題
自作 MOD の`Minecraft 1.21.2`への移行メモです。
移行メモ振り返ってみたけど、`1.20.6`から全部今年なの・・・

![Imgur](https://imgur.com/HxnQHhK.png)

今回も地味に大変だった。前回のエンチャント作り直しよりはマシかな?
レシピ周り(`ServerWorld#getRecipeManager`とか)触ってなければ多分今回のアップデートは怖くない。はず。

今回も今回とて`Fabric`の方々がまとめてくれているので、どこが変わっているかはそれを見ればいいです。
https://fabricmc.net/2024/10/14/1212.html

# 1.21.3
はバグ修正バージョンらしいので、`1.21.2`からの対応は無いと思います。

# 前回
https://takusan.negitoro.dev/posts/minecraft_mod_1_21_migration/

# レシピの JSON が変更されています
`Fabric`チームがまとめてくれてないですが、`JSON`の構造が変更されています。
https://github.com/takusan23/ClickManaita2/blob/1.21.3-fabric/src/main/resources/data/clickmanaita/recipe/clickmanaita_wood.json

オブジェクト`{ "item": "アイテムID" }`じゃなくて、`アイテムのID`を渡すようになりました。

```diff
{
"type": "minecraft:crafting_shaped",
"pattern": [
"DDD",
"DDD",
"DDD"
],
"key": {
- "D": {
- "item": "minecraft:crafting_table"
- }
+ "D": "minecraft:crafting_table"
},
"result": {
"count": 1,
"id": "clickmanaita:clickmanaita_wood"
}
}
```

不定形レシピも同様です。
https://github.com/takusan23/ResetTable/blob/1.21.3-fabric/src/main/resources/data/resettable/recipe/reset_table_block.json

# アイテム、ブロックの登録前に ID を渡す必要があります
`1.21.1`時代のコードを貼り付けるとこんなエラーが出る。

```plaintext
NullPointerException: Item id not set
```

これは、`Minecraft`にアイテムを追加する前までに、アイテムの設定(食べ物かどうか、スタック数)へ`アイテムID`を設定しておく必要があるということです。
いままでは、`Minecraft`に登録する際に、`アイテムID`とアイテムのクラスのインスタンスを渡せば登録できたのですが、もう一箇所、アイテム設定の時点でも渡す必要があります。

`Fabric + Kotlin`の場合は多分こんな感じで、`Item.Settings#registryKey`を呼べば良いはず。

```kotlin
// アイテムID
private val ID_CLICKMANAITA_WOOD = Identifier.of("clickmanaita", "clickmanaita_wood")

// レジストリキー
private val KEY_CLICKMANAITA_WOOD = RegistryKey.of(RegistryKeys.ITEM, ID_CLICKMANAITA_WOOD)

/** 木製。2個増える */
val CLICKMANAITA_WOOD = ClickManaitaBaseItem(settings = Item.Settings().registryKey(KEY_CLICKMANAITA_WOOD), dropSize = 2)

/** アイテムを登録する。この関数を MOD のエントリーポイントで呼び出す */
fun register() {
// アイテム追加
Registry.register(Registries.ITEM, KEY_CLICKMANAITA_WOOD, CLICKMANAITA_WOOD)
}
```

`Forge`の場合は多分こんな感じで、`Item.Properties#setId`を呼べば良いはず。

```java
// 各 アイテム ID
private static final ResourceLocation ID_CLICKMANAITA_WOOD = ResourceLocation.fromNamespaceAndPath(ClickManaita.MOD_ID, "clickmanaita_wood");

// 各 アイテム リソースキー
private static final ResourceKey<Item> KEY_CLICKMANAITA_WOOD = ResourceKey.create(Registries.ITEM, ID_CLICKMANAITA_WOOD);

public static final DeferredRegister<Item> ITEMS = DeferredRegister.create(ForgeRegistries.ITEMS, ClickManaita.MOD_ID);

/**
* 木製のクリックまな板
* 2倍化
*/
public static final RegistryObject<ClickManaitaBaseItem> CLICKMANAITA_WOOD = ITEMS.register(KEY_CLICKMANAITA_WOOD.location().getPath(), () -> createItem(KEY_CLICKMANAITA_WOOD, 2, MaterialColor.MATERIAL_WOOD_COLOR));

/**
* アイテムを登録する。このメソッドを MOD のエントリーポイントから呼び出す。
*/
public static void register(IEventBus eventBus) {
// 登録
ITEMS.register(eventBus);
}

/**
* ClickManaitaBaseItem を作成する
*
* @param itemId アイテムのID
* @param dropSize ドロップ数
* @param tooltipColor ツールチップの色
* @return ClickManaitaBaseItem
*/
private static ClickManaitaBaseItem createItem(ResourceKey<Item> itemId, int dropSize, String tooltipColor) {
ClickManaitaBaseItem item = new ClickManaitaBaseItem((new Item.Properties().setId(itemId)), dropSize);
item.setToolTipColor(tooltipColor);
return item;
}
```

# ActionResult
型推論に頼っている場合は明示的に型を渡さないといけなくなりそうです。

```kotlin
// SUCCESS にすると腕を振るう
var clickResult:ActionResult = ActionResult.PASS

if (/* 何かあれば */) {
clickResult = ActionResult.SUCCESS
}
```

# ブロックエンティティ
`BlockEntityType.Builder`は削除された?`FabricBlockEntityTypeBuilder`になったそうです。

```diff
- val RESET_TABLE_BLOCK_ENTITY: BlockEntityType<ResetTableEntity> = BlockEntityType.Builder.create(
- { pos, state -> ResetTableEntity(pos, state) },
- ResetTableBlocks.RESET_TABLE_BLOCK
- ).build(null)

+ val RESET_TABLE_BLOCK_ENTITY: BlockEntityType<ResetTableEntity> = FabricBlockEntityTypeBuilder.create(
+ { pos, state -> ResetTableEntity(pos, state) },
+ ResetTableBlocks.RESET_TABLE_BLOCK
+ ).build(null)
```

# レシピのシステムの変更
大幅に変わってそう、これのせいで`ResetTable`はかなりの書き直しが必要だった。
一番でかいのが、サーバー側のみレシピが参照できる点。クライアント側のコード(例えば`GUI`のテクスチャを描画するクラス)ではレシピにアクセスできなくなりました。

`World`クラスにレシピに関してのクラスがあったのですが、`World`の中でも`ServerWorld`にしかレシピのクラスがゲットできなくなってしまったので、まずは`if (world instanceof ServerWorld)`する必要があります。
サーバー側で処理されるクラスの場合には(`Entity`とかはサーバー側なので`ServerWorld`になると思う)、`instanceof``ServerWorld`かどうか見れば特に問題はないはず。

詳しくはこの辺:
https://fabricmc.net/2024/10/14/1212.html#serverworld-parameters

一方`GUI`のテクスチャを描画する、クライアント側にしか無い場合は**レシピにアクセスできなくなりました**
自作`MOD`ではレシピが存在するかを`GUI`側で確認して、無い場合は無いというテキストを`GUI`に描画するためクライアント側でもレシピを使っていた。

困った。

仕方ないので、サーバー側でレシピがあるか確認して、戻せない場合にはクライアント側へ`Fabric``Networking API`で通知するようにしました。
シリアライズする必要があって面倒。

## Networking API で置き換えた話
本家の`Networking API`の説明で十分な気がするけど一応。
https://fabricmc.net/wiki/tutorial:networking

今回はレシピが戻せない理由をクライアント側へ送ろうと思います。
戻せない理由は`enum(初見で読めない)`で定義済みで、`enum`のシリアライズには`ordinal()`で順番を使おうと思います、、、

まずはネットワークで使う一意の`ID`を作ります。

```kotlin
/** ネットワークの ID 一覧 */
object ResetTableNetworkIds {
/** リセットテーブルで戻せない理由をサーバーからクライアントに送るためのネットワークの ID */
val RESET_TABLE_ERROR_NETWORK_ID = Identifier.of("clickmanaita", "reset_table_error_network")
}
```

つぎに`サーバー側`←→`クライアント側`でやり取りするためのクラスを作ります。
`Kotlin`なら`data class``Java`なら`record`で作って、`CustomPayload`インターフェース(?)を実装します。

また、わかりやすいので、ここにシリアライズ、デシリアライズ用の処理も`static`で用意しておくことにします。
`PacketByteBufs`っていう書き込むクラスが渡されて、シリアライズの際はそのクラスに順番に書き込んでいく。`writeInt()`とか、あと`Minecraft`で使いがちのブロック位置を書き込む`writeBlockPos`なんてもあります。
デシリアライズの時は書き込んだ`PacketByteBufs`が渡されます。書き込んだ順番と同じ順番で`read`していけば良いです。

```kotlin
data class ResetTableErrorPayload(
val blockPos: BlockPos,
val verifyResult: ResetTableTool.VerifyResult
) : CustomPayload {
override fun getId(): CustomPayload.Id<out CustomPayload> = ID
companion object {
val ID = CustomPayload.Id<ResetTableErrorPayload>(ResetTableNetworkIds.RESET_TABLE_ERROR_NETWORK_ID)
val CODEC = PacketCodec.of<RegistryByteBuf, ResetTableErrorPayload>(
/* encoder = */ { data, buf ->
buf.writeBlockPos(data.blockPos)
buf.writeInt(data.verifyResult.ordinal)
},
/* decoder = */ { buf ->
ResetTableErrorPayload(
blockPos = buf.readBlockPos(),
verifyResult = ResetTableTool.VerifyResult.entries[buf.readInt()]
)
}
)
}
}
```

次はサーバー側とクライアント側でネットワークを登録します。
`ClientPlayNetworking.registerGlobalReceiver`で、サーバー側から送られてきたデータを受け取ることができます。

```kotlin
// サーバー側
// コンストラクタなど、MOD のエントリーポイントで呼び出す。
// ネットワークの追加(クライアント・サーバー間でやり取りする)
PayloadTypeRegistry.playS2C().register(ResetTableErrorPayload.ID, ResetTableErrorPayload.CODEC)

// クライアント側
// コンストラクタなど、MOD のエントリーポイントで呼び出す。
// クライアント側は追加でネットワーク登録が必要
ClientPlayNetworking.registerGlobalReceiver(ResetTableErrorPayload.ID) { payload, context ->
context.client().execute {
// ネットワーク経由でイベントが来た
// 今表示されている画面がリセットテーブルの GUI の場合はエラーを出す
// currentScreenHandler は表示されている GUI のやつ
val resetTableScreenHandler = (context.player().currentScreenHandler as? ResetTableScreenHandler)
if (resetTableScreenHandler != null) {
resetTableScreenHandler.recipeVerifyResult = payload.verifyResult
}
}
}
```

あとはサーバー側でクライアント側に送る処理を書けば良いはず。
サーバー側である必要があります。多分

```kotlin
// サーバー側であること
if (player !is ServerPlayerEntity) return

ServerPlayNetworking.send(player, ResetTableErrorPayload(blockPos, verifyResult))
```

# おわりに
差分をおいておきます。

- ClickManaita Fabric 1.21 → 1.21.2
- https://github.com/takusan23/ClickManaita2/compare/1.21-fabric...1.21.3-fabric
- ClickManaita Forge 1.21 → 1.21.2
- https://github.com/takusan23/ClickManaita2/compare/1.21-forge...1.21.3-forge
- ResetTable Fabric 1.21 → 1.21.2
- https://github.com/takusan23/ResetTable/compare/1.21-fabric...1.21.3-fabric

以上です。88888888。ノシ

0 comments on commit b313a34

Please sign in to comment.