feat: implement ItemGroupService for creative inventory management

This commit is contained in:
iceBear67 committed 2026-07-22 12:30:56 +08:00
1 parent 1b2712c425
commit b9a8c341a1
14 files changed
+148 -3

No files matched your search

+6 -1
View File
@@ -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
@@ -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() {
@@ -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;
}
@@ -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.
* <p>
* 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());
}
}
@@ -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<BombEntity> 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();
}
@@ -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<AmethystCauldronBlockEntity> 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);
}
}
@@ -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<GravityCrystalBlockEntity> 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
@@ -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);
}
}
@@ -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);
@@ -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(() -> {
@@ -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.
* <p>
* 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);
}
@@ -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<ItemLike> 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);
}
}
@@ -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",
@@ -1,4 +1,5 @@
{
"itemGroup.sfcraft.general": "SFCraft",
"block.sfcraft.amethyst_cauldron": "紫水晶炼药锅",
"block.sfcraft.gravity_crystal": "重力水晶",
"item.sfcraft.amethyst_cauldron_blank": "紫水晶炼药锅胚体",