From b9a8c341a1d99bdea6e4b47a4d7ef084053df553 Mon Sep 17 00:00:00 2001 From: iceBear67 Date: Wed, 22 Jul 2026 12:30:56 +0800 Subject: [PATCH] feat: implement ItemGroupService for creative inventory management --- gradle/wrapper/gradle-wrapper.properties | 7 ++- .../io/ib67/sfcraft/SFCraftInitializer.java | 6 +++ .../mixin/server/ServerWorldMixin.java | 3 +- .../sfcraft/module/game/ItemGroupModule.java | 27 ++++++++++ .../sfcraft/module/game/bomb/BombModule.java | 6 +++ .../module/game/cauldron/CauldronModule.java | 5 ++ .../game/crystal/GravityCrystalModule.java | 4 ++ .../module/game/lunchbox/LunchBoxModule.java | 6 +++ .../module/game/token/TokenModule.java | 5 ++ .../module/game/totem/TotemModule.java | 4 ++ .../sfcraft/registry/ItemGroupService.java | 23 ++++++++ .../itemgroup/PolymerItemGroupService.java | 53 +++++++++++++++++++ .../resources/assets/sfcraft/lang/en_us.json | 1 + .../resources/assets/sfcraft/lang/zh_cn.json | 1 + 14 files changed, 148 insertions(+), 3 deletions(-) create mode 100644 src/main/java/io/ib67/sfcraft/module/game/ItemGroupModule.java create mode 100644 src/main/java/io/ib67/sfcraft/registry/ItemGroupService.java create mode 100644 src/main/java/io/ib67/sfcraft/registry/itemgroup/PolymerItemGroupService.java diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3c0465a..a950f0b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1 +1,6 @@ -distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip +#Wed Jul 22 12:25:27 GMT+08:00 2026 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/src/main/java/io/ib67/sfcraft/SFCraftInitializer.java b/src/main/java/io/ib67/sfcraft/SFCraftInitializer.java index 1c3895e..2703d14 100644 --- a/src/main/java/io/ib67/sfcraft/SFCraftInitializer.java +++ b/src/main/java/io/ib67/sfcraft/SFCraftInitializer.java @@ -14,6 +14,7 @@ import io.ib67.sfcraft.module.room.CreativeRoomModule; import io.ib67.sfcraft.module.supervisor.WebModule; import io.ib67.sfcraft.module.supervisor.web.SchematicUploader; import io.ib67.sfcraft.module.game.GameExtensionModule; +import io.ib67.sfcraft.module.game.ItemGroupModule; import io.ib67.sfcraft.module.game.beheading.BeheadingModule; import io.ib67.sfcraft.module.game.bomb.BombModule; import io.ib67.sfcraft.module.game.cauldron.CauldronModule; @@ -24,7 +25,9 @@ import io.ib67.sfcraft.module.game.mount.MountModule; import io.ib67.sfcraft.module.game.token.TokenModule; import io.ib67.sfcraft.module.game.totem.TotemModule; import io.ib67.sfcraft.registry.CauldronRecipeRegistry; +import io.ib67.sfcraft.registry.ItemGroupService; import io.ib67.sfcraft.registry.RoomRegistry; +import io.ib67.sfcraft.registry.itemgroup.PolymerItemGroupService; import io.ib67.sfcraft.registry.cauldron.SimpleCauldronRecipeRegistry; import io.ib67.sfcraft.registry.chat.SimpleMessageDecorator; import io.ib67.sfcraft.registry.event.SFRandomEventRegistry; @@ -61,6 +64,7 @@ public class SFCraftInitializer extends GuiceModInitializer { binder().bind(RoomTeleporter.class).in(Singleton.class); binder().bind(RoomRegistry.class).to(SimpleRoomRegistry.class).in(Singleton.class); binder().bind(CauldronRecipeRegistry.class).to(SimpleCauldronRecipeRegistry.class).in(Singleton.class); + binder().bind(ItemGroupService.class).to(PolymerItemGroupService.class).in(Singleton.class); } private void registerFeatures() { @@ -99,6 +103,8 @@ public class SFCraftInitializer extends GuiceModInitializer { registerFeature(CommanderModule.class); registerFeature(MountModule.class); registerFeature(TotemModule.class); + // ItemGroupModule 必须最后注册:它冻结前面各模块被动贡献到 ItemGroupService 的物品,构建创意物品栏 + registerFeature(ItemGroupModule.class); } private void registerWebModules() { diff --git a/src/main/java/io/ib67/sfcraft/mixin/server/ServerWorldMixin.java b/src/main/java/io/ib67/sfcraft/mixin/server/ServerWorldMixin.java index 165bc9d..4f110a5 100644 --- a/src/main/java/io/ib67/sfcraft/mixin/server/ServerWorldMixin.java +++ b/src/main/java/io/ib67/sfcraft/mixin/server/ServerWorldMixin.java @@ -9,7 +9,6 @@ import org.spongepowered.asm.mixin.injection.*; @Mixin(ServerLevel.class) public abstract class ServerWorldMixin { - @ModifyArg(method = "tickTime", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/storage/ServerLevelData;setGameTime(J)V")) public long setTimeOfDay(long timeOfDay) { var _this = ((ServerLevel) (Object) this); @@ -18,7 +17,7 @@ public abstract class ServerWorldMixin { var dt = _this.dimensionTypeRegistration(); var clock = dt.value().defaultClock(); if(clock.isEmpty()) return timeOfDay; - _this.clockManager().addTicks(clock.get(), 10); + _this.clockManager().addTicks(clock.get(), 20); MixinHelper.spedUp = true; return timeOfDay + 10; } diff --git a/src/main/java/io/ib67/sfcraft/module/game/ItemGroupModule.java b/src/main/java/io/ib67/sfcraft/module/game/ItemGroupModule.java new file mode 100644 index 0000000..d944f63 --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/module/game/ItemGroupModule.java @@ -0,0 +1,27 @@ +package io.ib67.sfcraft.module.game; + +import com.google.inject.Inject; +import io.ib67.sfcraft.ServerModule; +import io.ib67.sfcraft.module.game.cauldron.CauldronModule; +import io.ib67.sfcraft.registry.ItemGroupService; + +/** + * Finalizes the mod's creative inventory tab. Must be the LAST feature registered in + * {@code registerGameFeatures()} so that, by the time its {@link #onInitialize()} runs, every + * other feature module has already contributed its items to {@link ItemGroupService}. This module + * only freezes the passively-collected set — it never registers items itself. + *

+ * The tab icon is the amethyst cauldron, pulled from {@link CauldronModule} (registered earlier, + * so its item is available here) to keep the icon type-safe rather than looked up by string id. + */ +public class ItemGroupModule extends ServerModule { + @Inject + private ItemGroupService itemGroups; + @Inject + private CauldronModule cauldron; + + @Override + public void onInitialize() { + itemGroups.freeze(cauldron.getCauldronItem()); + } +} diff --git a/src/main/java/io/ib67/sfcraft/module/game/bomb/BombModule.java b/src/main/java/io/ib67/sfcraft/module/game/bomb/BombModule.java index d0b4f38..9c11ebb 100644 --- a/src/main/java/io/ib67/sfcraft/module/game/bomb/BombModule.java +++ b/src/main/java/io/ib67/sfcraft/module/game/bomb/BombModule.java @@ -4,6 +4,7 @@ import com.google.inject.Inject; import io.ib67.sfcraft.ServerModule; import io.ib67.sfcraft.config.GameConfigService; import io.ib67.sfcraft.module.game.RegistryHelper; +import io.ib67.sfcraft.registry.ItemGroupService; import lombok.Getter; import net.fabricmc.fabric.api.loot.v3.LootTableEvents; import net.minecraft.core.component.DataComponents; @@ -28,6 +29,8 @@ import net.minecraft.world.level.storage.loot.providers.number.UniformGenerator; public class BombModule extends ServerModule { @Inject private GameConfigService config; + @Inject + private ItemGroupService itemGroups; private EntityType bombEntityType; private Item bomb; @@ -48,6 +51,9 @@ public class BombModule extends ServerModule { bomb = registerBomb("bomb", BombEntity.BombType.NORMAL); obsidianBomb = registerBomb("obsidian_bomb", BombEntity.BombType.OBSIDIAN); blazeBomb = registerBomb("blaze_bomb", BombEntity.BombType.BLAZE); + itemGroups.add(bomb); + itemGroups.add(obsidianBomb); + itemGroups.add(blazeBomb); registerLootModifiers(); } diff --git a/src/main/java/io/ib67/sfcraft/module/game/cauldron/CauldronModule.java b/src/main/java/io/ib67/sfcraft/module/game/cauldron/CauldronModule.java index d4e5a97..7fd40b0 100644 --- a/src/main/java/io/ib67/sfcraft/module/game/cauldron/CauldronModule.java +++ b/src/main/java/io/ib67/sfcraft/module/game/cauldron/CauldronModule.java @@ -7,6 +7,7 @@ import io.ib67.sfcraft.config.GameConfigService; import io.ib67.sfcraft.module.game.RegistryHelper; import io.ib67.sfcraft.module.game.item.SimplePolymerItem; import io.ib67.sfcraft.registry.CauldronRecipeRegistry; +import io.ib67.sfcraft.registry.ItemGroupService; import lombok.Getter; import net.minecraft.core.component.DataComponents; import net.minecraft.network.chat.Component; @@ -27,6 +28,8 @@ public class CauldronModule extends ServerModule { private GameConfigService config; @Inject private CauldronRecipeRegistry recipes; + @Inject + private ItemGroupService itemGroups; private AmethystCauldronBlock cauldronBlock; private BlockEntityType cauldronBlockEntity; @@ -67,5 +70,7 @@ public class CauldronModule extends ServerModule { .component(DataComponents.ENCHANTMENT_GLINT_OVERRIDE, true) .component(DataComponents.ITEM_NAME, Component.translatable("item.sfcraft.amethyst_cauldron")) ); + itemGroups.add(cauldronItem); + itemGroups.add(cauldronBlankItem); } } diff --git a/src/main/java/io/ib67/sfcraft/module/game/crystal/GravityCrystalModule.java b/src/main/java/io/ib67/sfcraft/module/game/crystal/GravityCrystalModule.java index 0b9f291..238051e 100644 --- a/src/main/java/io/ib67/sfcraft/module/game/crystal/GravityCrystalModule.java +++ b/src/main/java/io/ib67/sfcraft/module/game/crystal/GravityCrystalModule.java @@ -7,6 +7,7 @@ import io.ib67.sfcraft.callback.SFConfigReload; import io.ib67.sfcraft.config.GameConfigService; import io.ib67.sfcraft.module.game.RegistryHelper; import io.ib67.sfcraft.registry.CauldronRecipeRegistry; +import io.ib67.sfcraft.registry.ItemGroupService; import io.ib67.sfcraft.registry.cauldron.CauldronRecipe; import lombok.Getter; import net.minecraft.core.component.DataComponents; @@ -31,6 +32,8 @@ public class GravityCrystalModule extends ServerModule { private GameConfigService config; @Inject private CauldronRecipeRegistry cauldronRecipes; + @Inject + private ItemGroupService itemGroups; private GravityCrystalBlock crystalBlock; private BlockEntityType crystalBlockEntity; @@ -63,6 +66,7 @@ public class GravityCrystalModule extends ServerModule { .component(DataComponents.ENCHANTMENT_GLINT_OVERRIDE, true) .component(DataComponents.ITEM_NAME, Component.translatable("item.sfcraft.gravity_crystal")) ); + itemGroups.add(crystalItem); registerRecipes(); // /sfcraft reload 后重建配方,应用最新的 reactionTicks diff --git a/src/main/java/io/ib67/sfcraft/module/game/lunchbox/LunchBoxModule.java b/src/main/java/io/ib67/sfcraft/module/game/lunchbox/LunchBoxModule.java index 5c77c25..ae3a46d 100644 --- a/src/main/java/io/ib67/sfcraft/module/game/lunchbox/LunchBoxModule.java +++ b/src/main/java/io/ib67/sfcraft/module/game/lunchbox/LunchBoxModule.java @@ -1,7 +1,9 @@ package io.ib67.sfcraft.module.game.lunchbox; +import com.google.inject.Inject; import io.ib67.sfcraft.ServerModule; import io.ib67.sfcraft.module.game.RegistryHelper; +import io.ib67.sfcraft.registry.ItemGroupService; import lombok.Getter; import net.minecraft.core.component.DataComponents; import net.minecraft.network.chat.Component; @@ -15,6 +17,9 @@ import net.minecraft.world.item.component.Consumables; */ @Getter public class LunchBoxModule extends ServerModule { + @Inject + private ItemGroupService itemGroups; + private Item lunchBox; @Override @@ -30,5 +35,6 @@ public class LunchBoxModule extends ServerModule { .component(DataComponents.FOOD, new FoodProperties(0, 0.0F, true)) .component(DataComponents.CONSUMABLE, Consumables.DEFAULT_FOOD) ); + itemGroups.add(lunchBox); } } diff --git a/src/main/java/io/ib67/sfcraft/module/game/token/TokenModule.java b/src/main/java/io/ib67/sfcraft/module/game/token/TokenModule.java index 38c4fc8..0ea39c3 100644 --- a/src/main/java/io/ib67/sfcraft/module/game/token/TokenModule.java +++ b/src/main/java/io/ib67/sfcraft/module/game/token/TokenModule.java @@ -6,6 +6,7 @@ import io.ib67.sfcraft.callback.SFConfigReload; import io.ib67.sfcraft.config.GameConfigService; import io.ib67.sfcraft.module.game.RegistryHelper; import io.ib67.sfcraft.registry.CauldronRecipeRegistry; +import io.ib67.sfcraft.registry.ItemGroupService; import io.ib67.sfcraft.registry.cauldron.CauldronRecipe; import lombok.Getter; import net.minecraft.core.component.DataComponents; @@ -30,6 +31,8 @@ public class TokenModule extends ServerModule { private GameConfigService config; @Inject private CauldronRecipeRegistry cauldronRecipes; + @Inject + private ItemGroupService itemGroups; private Item pearlToken; private ReversePearlTokenItem reversePearlToken; @@ -55,6 +58,8 @@ public class TokenModule extends ServerModule { .durability(config.get().reverseToken.maxUses) .component(DataComponents.ITEM_NAME, Component.translatable("item.sfcraft.reverse_pearl_token")) ); + itemGroups.add(pearlToken); + itemGroups.add(reversePearlToken); registerRecipes(); SFConfigReload.EVENT.register(() -> { registeredRecipes.forEach(cauldronRecipes::unregister); diff --git a/src/main/java/io/ib67/sfcraft/module/game/totem/TotemModule.java b/src/main/java/io/ib67/sfcraft/module/game/totem/TotemModule.java index 005dfde..be1601c 100644 --- a/src/main/java/io/ib67/sfcraft/module/game/totem/TotemModule.java +++ b/src/main/java/io/ib67/sfcraft/module/game/totem/TotemModule.java @@ -7,6 +7,7 @@ import io.ib67.sfcraft.config.GameConfigService; import io.ib67.sfcraft.module.game.RegistryHelper; import io.ib67.sfcraft.module.game.item.SimplePolymerItem; import io.ib67.sfcraft.registry.CauldronRecipeRegistry; +import io.ib67.sfcraft.registry.ItemGroupService; import io.ib67.sfcraft.registry.cauldron.CauldronRecipe; import lombok.Getter; import net.minecraft.advancements.triggers.CriteriaTriggers; @@ -40,6 +41,8 @@ public class TotemModule extends ServerModule { private GameConfigService config; @Inject private CauldronRecipeRegistry cauldronRecipes; + @Inject + private ItemGroupService itemGroups; private Item standingFirmTotem; @@ -68,6 +71,7 @@ public class TotemModule extends ServerModule { // 效果改由 mixin 完全接管,复用 DeathProtection.TOTEM_OF_UNDYING 常量(见 tryProtect)。 .component(DataComponents.ITEM_NAME, Component.translatable("item.sfcraft.standing_firm_totem")) ); + itemGroups.add(standingFirmTotem); registerRecipes(); // /sfcraft reload 后重建配方,应用最新的 reactionTicks SFConfigReload.EVENT.register(() -> { diff --git a/src/main/java/io/ib67/sfcraft/registry/ItemGroupService.java b/src/main/java/io/ib67/sfcraft/registry/ItemGroupService.java new file mode 100644 index 0000000..4ff8d96 --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/registry/ItemGroupService.java @@ -0,0 +1,23 @@ +package io.ib67.sfcraft.registry; + +import net.minecraft.world.level.ItemLike; + +/** + * Collects the mod's items into a single server-side creative inventory tab (later synced to + * Polymer-compatible clients). Feature modules contribute their items via {@link #add(ItemLike)} + * in {@code onInitialize()}; a single finalizer module calls {@link #freeze(ItemLike)} last — + * after every other module has registered — to build and register the tab from what was collected. + *

+ * Collection is passive: the service only records what modules push to it and does not itself + * know about any concrete feature. Adding after {@link #freeze(ItemLike)} is an error. + */ +public interface ItemGroupService { + /** Contribute an item to the creative tab. Must be called before {@link #freeze(ItemLike)}. */ + void add(ItemLike item); + + /** + * Build and register the creative tab from everything collected so far, using {@code icon} + * as the tab icon. Must be called exactly once, after all {@link #add(ItemLike)} calls. + */ + void freeze(ItemLike icon); +} diff --git a/src/main/java/io/ib67/sfcraft/registry/itemgroup/PolymerItemGroupService.java b/src/main/java/io/ib67/sfcraft/registry/itemgroup/PolymerItemGroupService.java new file mode 100644 index 0000000..d5d05a9 --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/registry/itemgroup/PolymerItemGroupService.java @@ -0,0 +1,53 @@ +package io.ib67.sfcraft.registry.itemgroup; + +import eu.pb4.polymer.core.api.item.PolymerCreativeModeTabUtils; +import io.ib67.sfcraft.SFCraft; +import io.ib67.sfcraft.registry.ItemGroupService; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.ItemLike; + +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Default {@link ItemGroupService} backed by Polymer's server-side creative tabs + * ({@link PolymerCreativeModeTabUtils}). Items are kept in insertion order and de-duplicated; + * {@link #freeze(ItemLike)} builds the tab and registers it under {@code sfcraft:general}, + * titled by the {@code itemGroup.sfcraft.general} translation key (resolved client-side via the + * server resource pack). + */ +public class PolymerItemGroupService implements ItemGroupService { + private static final Identifier ID = Identifier.fromNamespaceAndPath(SFCraft.MOD_ID, "general"); + + // LinkedHashSet: stable tab order (module registration order) with de-duplication. + private final Set items = new LinkedHashSet<>(); + private boolean frozen; + + @Override + public void add(ItemLike item) { + if (frozen) { + throw new IllegalStateException("Item group is already frozen; cannot add " + item); + } + items.add(item); + } + + @Override + public void freeze(ItemLike icon) { + if (frozen) { + throw new IllegalStateException("Item group is already frozen"); + } + frozen = true; + var tab = PolymerCreativeModeTabUtils.builder() + .title(Component.translatable("itemGroup.sfcraft.general")) + .icon(() -> new ItemStack(icon)) + .displayItems((parameters, output) -> { + for (ItemLike item : items) { + output.accept(item); + } + }) + .build(); + PolymerCreativeModeTabUtils.registerPolymerCreativeModeTab(ID, tab); + } +} diff --git a/src/main/resources/assets/sfcraft/lang/en_us.json b/src/main/resources/assets/sfcraft/lang/en_us.json index 4410127..013fb38 100644 --- a/src/main/resources/assets/sfcraft/lang/en_us.json +++ b/src/main/resources/assets/sfcraft/lang/en_us.json @@ -1,4 +1,5 @@ { + "itemGroup.sfcraft.general": "SFCraft", "block.sfcraft.amethyst_cauldron": "Amethyst Cauldron", "block.sfcraft.gravity_crystal": "Gravity Crystal", "item.sfcraft.amethyst_cauldron_blank": "Amethyst Cauldron Shell", diff --git a/src/main/resources/assets/sfcraft/lang/zh_cn.json b/src/main/resources/assets/sfcraft/lang/zh_cn.json index c36e7fc..3dc2c98 100644 --- a/src/main/resources/assets/sfcraft/lang/zh_cn.json +++ b/src/main/resources/assets/sfcraft/lang/zh_cn.json @@ -1,4 +1,5 @@ { + "itemGroup.sfcraft.general": "SFCraft", "block.sfcraft.amethyst_cauldron": "紫水晶炼药锅", "block.sfcraft.gravity_crystal": "重力水晶", "item.sfcraft.amethyst_cauldron_blank": "紫水晶炼药锅胚体",