たくさんの自由帳
Androidのお話
たくさんの自由帳
投稿日 : | 0 日前
文字数(だいたい) : 3328
どうもこんばんわ。
一応言っておくと定住先でよく使われる1.12.2ではなく1.21.2です。
(最近はどこに定住しているのか知らない)
自作 MOD のMinecraft 1.21.2
への移行メモです。
移行メモ振り返ってみたけど、1.20.6
から全部今年なの・・・
今回も地味に大変だった。前回のエンチャント作り直しよりはマシかな?
レシピ周り(ServerWorld#getRecipeManager
とか)触ってなければ多分今回のアップデートは怖くない。はず。
今回も今回とてFabric
の方々がまとめてくれているので、どこが変わっているかはそれを見ればいいです。
https://fabricmc.net/2024/10/14/1212.html
はバグ修正バージョンらしいので、1.21.2
からの対応は無いと思います。
https://takusan.negitoro.dev/posts/minecraft_mod_1_21_migration/
Fabric
チームがまとめてくれてないですが、JSON
の構造が変更されています。
https://github.com/takusan23/ClickManaita2/blob/1.21.3-fabric/src/main/resources/data/clickmanaita/recipe/clickmanaita_wood.json
オブジェクト{ "item": "アイテムID" }
じゃなくて、アイテムのID
を渡すようになりました。
{
"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"
}
}
1.21.1
時代のコードを貼り付けるとこんなエラーが出る。
NullPointerException: Item id not set
これは、Minecraft
にアイテムを追加する前までに、アイテムの設定(食べ物かどうか、スタック数)へアイテムID
を設定しておく必要があるということです。
いままでは、Minecraft
に登録する際に、アイテムID
とアイテムのクラスのインスタンスを渡せば登録できたのですが、もう一箇所、アイテム設定の時点でも渡す必要があります。
Fabric + Kotlin
の場合は多分こんな感じで、Item.Settings#registryKey
を呼べば良いはず。
// アイテム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
を呼べば良いはず。
// 各 アイテム 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;
}
型推論に頼っている場合は明示的に型を渡さないといけなくなりそうです。
// SUCCESS にすると腕を振るう
var clickResult:ActionResult = ActionResult.PASS
if (/* 何かあれば */) {
clickResult = ActionResult.SUCCESS
}
BlockEntityType.Builder
は削除された?FabricBlockEntityTypeBuilder
になったそうです。
- 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
の説明で十分な気がするけど一応。
https://fabricmc.net/wiki/tutorial:networking
今回はレシピが戻せない理由をクライアント側へ送ろうと思います。
戻せない理由はenum(初見で読めない)
で定義済みで、enum
のシリアライズにはordinal()
で順番を使おうと思います、、、
まずはネットワークで使う一意のID
を作ります。
/** ネットワークの 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
していけば良いです。
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
で、サーバー側から送られてきたデータを受け取ることができます。
// サーバー側
// コンストラクタなど、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
}
}
}
あとはサーバー側でクライアント側に送る処理を書けば良いはず。
サーバー側である必要があります。多分
// サーバー側であること
if (player !is ServerPlayerEntity) return
ServerPlayNetworking.send(player, ResetTableErrorPayload(blockPos, verifyResult))
差分をおいておきます。
以上です。88888888。ノシ