Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ Before a release, `./gradlew releaseChangelog` turns *Unreleased* into the relea

## [Unreleased]

### Added

- Config sections `upgrades` and `addons` switch each upgrade and addon off: it loses its recipe and does nothing in a machine, which shows it with a red slot frame, a "no" symbol over the icon and a red tooltip line

## [4.0.0] - 2026-11-03

### Added
Expand Down
7 changes: 6 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,11 @@ The built-in turrets use the same path, so there is no special casing per turret
- **Upgrades are per machine:** `core.item.UpgradeType` is the common interface, each machine has its own enum (`TurretBaseUpgradeType`, `GeneratorUpgradeType`) and item subclass of `item.UpgradeItem` (`TurretBaseUpgradeItem`, `GeneratorUpgradeItem`).
A machine's upgrade slots accept only its own subclass.
A new machine gets its own enum and item subclass.
- **Upgrade and addon switches** (requested by the user on 2026-10-29, branch `1.20.1+-module-toggles`, for a later release): the config sections `upgrades` and `addons` have one boolean per upgrade (both machines) and addon item (`ConfigData.Upgrades` / `Addons`, `isEnabled`; `item.ModuleToggles` for stacks and item ids).
A disabled one stays in its slot but counts as not installed (`TurretBaseLogic.getUpgradeCount` / `getAddonCount` through `LogicSettings.upgradeEnabled` / `addonEnabled`, `GeneratorBlockEntity.getMultiplierUpgrades`), and its recipe and unlock advancement do not load: the resource condition `openmodularturrets:module_enabled` (`compat.ModuleEnabledCondition`, `"items"`: item id paths; registered per loader like OpenModularLighting's `block_enabled`: `FabricModuleEnabledCondition`, `NeoForgeModuleEnabledCondition`, `ForgeModuleEnabledCondition`), added by datagen to every upgrade and addon recipe.
Recipes follow the config only when data loads (restart).
The client shows it with the synced config (`client.render.DisabledModuleOverlay`): a red frame and tint behind the machine slots holding one (base and generator `renderBg`), vanilla's barrier sprite over the icon in every GUI (Fabric: client mixin `GuiGraphicsMixin` at the end of `renderItemDecorations`; NeoForge and Forge: `RegisterItemDecorationsEvent`), and a red first tooltip line (`ModuleTooltip`, `tooltip.openmodularturrets.module_disabled`).
Game tests: `ModuleToggleGameTests`.
- **Config versions:** `configVersion` in the config file; `ConfigMigration` (OMLib) runs the `ConfigMigrationSteps.STEPS` newer than the file, replacing old default values (only if unchanged) when a file of an older version is loaded.
Bump `CURRENT_VERSION` and add a step when a default changes.

Expand Down Expand Up @@ -768,7 +773,7 @@ Update this checklist as work progresses.
- [x] Item transfer: turret base ammo slots and generator fuel slots through `ItemAccess` (`getItemAccess(side)`); Fabric lookups via `FabricItemStorages`; hopper game tests (`ItemTransferGameTests`); the NeoForge capabilities come with the registration step
- [x] Use `LoaderInfo` (config folder in `OMTConfig`) and `Fuel` (generator: `isFuel(stack)`, burn time, stack-aware remainder) instead of `FabricLoader` / `FuelRegistry`; game test for the lava bucket remainder (the plugin entrypoints in `TurretRegistry` / `TurretModels` stay for "Turret API plugin discovery")
- [x] Registration: `OMTStartup.init(plugins)` + `OMTStartup.registrationSteps()` (sounds, blocks, items, block entities, entity types, creative tab, menus), NeoForge per `RegisterEvent` with the common-setup check; capabilities (items, energy); shared handlers `InteractiveBlock.handleUse`, `ConfigSync.onPlayerJoin`, `KillCounter.onDeath` (NeoForge `LivingDeathEvent` at lowest priority); `VersionCompat.creativeTabBuilder()` / `fakePlayer()`; plain `c:` tags; `RayShotCallback` as a plain listener list; Fabric render data behind `CompatBlockEntity.renderData()`; `ModSounds` registers in `init()` (the turret definitions use the sounds earlier).
OMT has no recipe conditions.
OMT had no recipe conditions until the upgrade and addon switches (see "Power & items").
NeoForge now compiles all of `main` except `datagen` (Fabric-only by design).
- [x] Mixins on NeoForge: the same `openmodularturrets.mixins.json` under `[[mixins]]` in `neoforge.mods.toml`; both `LivingEntityMixin` injection points exist in NeoForge's patched `LivingEntity` (checked once with `mixin.debug.export` on the NeoForge game test server)
- [x] Client: `OMTClient` lists the content (screens, model layers, block entity and entity renderers, item renderers via `ItemStackRenderer`, cutout blocks, tooltips, disconnect); `OMTFabricClient` / `OMTNeoForgeClient` register it and wire ticks, disconnect and the ray rendering; fence models carry `"render_type": "minecraft:cutout"`; camo models `FabricCamoModel` (Renderer API) / `NeoForgeCamoModel` (`ModelData` + forwarding colour provider), `CompatBlockEntity.remesh()` refreshes the model data.
Expand Down
2 changes: 2 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,8 @@ The full list is in [turret-api.md](turret-api.md); a game test per turret belon
Upgrades belong to one machine.
Add a constant to the machine's enum (`core.item.TurretBaseUpgradeType` or `GeneratorUpgradeType`), register the item in `ModItems` with the matching item class (`TurretBaseUpgradeItem`, `GeneratorUpgradeItem`), and implement the effect in the machine's logic.
A new machine gets its own enum and item subclass of `UpgradeItem`; its slots accept only those.
Give it a switch in `ConfigData.Upgrades` (field and `isEnabled` case): a disabled upgrade counts as not installed (`TurretBaseLogic.getUpgradeCount` asks `LogicSettings.upgradeEnabled`; another machine asks `item.ModuleToggles`), and its recipe needs the `module_enabled` condition (`OMTRecipeProvider` adds it to the upgrades and addons).
The client marks disabled ones by itself (`client.render.DisabledModuleOverlay`).

### A config value

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package omtteam.openmodularturrets.client.mixin;

import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.world.item.ItemStack;
import omtteam.openmodularturrets.client.render.DisabledModuleOverlay;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;

/**
* Fabric only (the client mixin config is listed in {@code fabric.mod.json} alone): draws the "no" symbol over disabled
* upgrades and addons after the item decorations, as NeoForge's and Forge's item decorators do there.
*/
@Mixin(GuiGraphics.class)
public abstract class GuiGraphicsMixin {
@Inject(method = "renderItemDecorations(Lnet/minecraft/client/gui/Font;Lnet/minecraft/world/item/ItemStack;IILjava/lang/String;)V",
at = @At("TAIL"))
private void openmodularturrets$disabledModule(Font font, ItemStack stack, int x, int y, @Nullable String text,
CallbackInfo ci) {
DisabledModuleOverlay.renderItemOverlay((GuiGraphics) (Object) this, stack, x, y);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import net.minecraftforge.client.event.ClientPlayerNetworkEvent;
import net.minecraftforge.client.event.EntityRenderersEvent;
import net.minecraftforge.client.event.RegisterColorHandlersEvent;
import net.minecraftforge.client.event.RegisterItemDecorationsEvent;
import net.minecraftforge.client.event.RenderLevelStageEvent;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.TickEvent;
Expand All @@ -14,6 +15,7 @@
import omtteam.openmodularturrets.client.OMTClient;
import omtteam.openmodularturrets.client.api.OMTClientPlugin;
import omtteam.openmodularturrets.client.model.TurretModels;
import omtteam.openmodularturrets.client.render.DisabledModuleOverlay;
import omtteam.openmodularturrets.client.render.effect.RayRenderer;
import omtteam.openmodularturrets.client.render.outline.TurretPlacementOutline;
import omtteam.omlib.platform.Platform;
Expand All @@ -40,6 +42,11 @@ public OMTForgeClient(IEventBus modBus) {
OMTClient.registerBlockEntityRenderers(event::registerBlockEntityRenderer);
OMTClient.registerEntityRenderers(event::registerEntityRenderer);
});
modBus.addListener((RegisterItemDecorationsEvent event) -> DisabledModuleOverlay.moduleItems().forEach(item ->
event.register(item, (graphics, font, stack, x, y) -> {
DisabledModuleOverlay.renderItemOverlay(graphics, stack, x, y);
return false;
})));
modBus.addListener((RegisterColorHandlersEvent.Block event) ->
event.register(ForgeCamoModel::color, OMTClient.camoBlocks()));
MinecraftForge.EVENT_BUS.addListener((ItemTooltipEvent event) ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import net.neoforged.neoforge.client.event.ClientTickEvent;
import net.neoforged.neoforge.client.event.EntityRenderersEvent;
import net.neoforged.neoforge.client.event.RegisterColorHandlersEvent;
import net.neoforged.neoforge.client.event.RegisterItemDecorationsEvent;
import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent;
import net.neoforged.neoforge.client.event.RenderLevelStageEvent;
//? if >=1.21.11 {
Expand All @@ -22,6 +23,7 @@
import omtteam.openmodularturrets.client.OMTClient;
import omtteam.openmodularturrets.client.api.OMTClientPlugin;
import omtteam.openmodularturrets.client.model.TurretModels;
import omtteam.openmodularturrets.client.render.DisabledModuleOverlay;
import omtteam.openmodularturrets.client.render.effect.RayRenderer;
import omtteam.openmodularturrets.client.render.outline.TurretPlacementOutline;
import omtteam.omlib.platform.Platform;
Expand Down Expand Up @@ -54,6 +56,11 @@ public OMTNeoForgeClient(IEventBus modBus) {
modBus.addListener((RegisterClientExtensionsEvent event) -> OMTClient.registerItemRenderers((item, renderer) ->
event.registerItem(NeoForgeItemRenderer.extensions(renderer), item)));
//?}
modBus.addListener((RegisterItemDecorationsEvent event) -> DisabledModuleOverlay.moduleItems().forEach(item ->
event.register(item, (graphics, font, stack, x, y) -> {
DisabledModuleOverlay.renderItemOverlay(graphics, stack, x, y);
return false;
})));
modBus.addListener((RegisterColorHandlersEvent.Block event) ->
event.register(NeoForgeCamoModel::color, OMTClient.camoBlocks()));
NeoForge.EVENT_BUS.addListener((ItemTooltipEvent event) ->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package omtteam.openmodularturrets.client.render;

import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import omtteam.omlib.compat.VersionCompat;
import omtteam.openmodularturrets.item.AddonItem;
import omtteam.openmodularturrets.item.ModuleToggles;
import omtteam.openmodularturrets.item.UpgradeItem;

import java.util.ArrayList;
import java.util.List;
//? if >=1.21.11 {
/*import net.minecraft.client.renderer.RenderPipelines;
import net.minecraft.data.AtlasIds;
*///?} else {
import net.minecraft.client.renderer.texture.TextureAtlas;
//?}

/**
* Marks upgrades and addons that are disabled in the config ({@link ModuleToggles}): a red frame around their slot in
* the machine GUIs, and vanilla's barrier sprite (a red "no" symbol) over their icon wherever an item is drawn in a GUI.
* The icon overlay runs after the item decorations: a client mixin on Fabric, an item decorator on NeoForge and Forge.
*/
public final class DisabledModuleOverlay {
private static final ResourceLocation SYMBOL = VersionCompat.id("minecraft", "item/barrier");
private static final int FRAME_COLOR = 0xFFE01010;
private static final int TINT_COLOR = 0x50FF0000;

private DisabledModuleOverlay() {
}

/** Every upgrade and addon item, for the loaders' item decorator registration. */
public static List<Item> moduleItems() {
List<Item> items = new ArrayList<>();
for (Item item : BuiltInRegistries.ITEM) {
if (item instanceof UpgradeItem || item instanceof AddonItem) items.add(item);
}
return items;
}

/**
* A red frame and tint behind every machine slot (not the player's inventory) that holds a disabled upgrade or
* addon; call it from {@code renderBg}, after the background, so the item is drawn over it.
*/
public static void renderSlotFrames(GuiGraphics graphics, AbstractContainerMenu menu, int left, int top) {
for (Slot slot : menu.slots) {
if (slot.container instanceof Inventory || !slot.isActive() || !ModuleToggles.isDisabled(slot.getItem())) {
continue;
}
int x = left + slot.x;
int y = top + slot.y;
graphics.fill(x, y, x + 16, y + 16, TINT_COLOR);
graphics.renderOutline(x - 1, y - 1, 18, 18, FRAME_COLOR);
}
}

/** The "no" symbol over the icon of a disabled upgrade or addon at {@code x, y}; nothing for other stacks. */
public static void renderItemOverlay(GuiGraphics graphics, ItemStack stack, int x, int y) {
if (!ModuleToggles.isDisabled(stack)) return;
//? if >=1.21.11 {
/*TextureAtlasSprite sprite = Minecraft.getInstance().getAtlasManager().getAtlasOrThrow(AtlasIds.ITEMS).getSprite(SYMBOL);
graphics.blitSprite(RenderPipelines.GUI_TEXTURED, sprite, x, y, 16, 16);
*///?} else {
TextureAtlasSprite sprite = Minecraft.getInstance().getTextureAtlas(TextureAtlas.LOCATION_BLOCKS).apply(SYMBOL);
// above the item (drawn around z 150), like the stack count
graphics.blit(x, y, 200, 16, 16, sprite);
//?}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Inventory;
import omtteam.openmodularturrets.OMT;
import omtteam.openmodularturrets.client.render.DisabledModuleOverlay;
import omtteam.omlib.client.compat.ClientCompat;
import omtteam.openmodularturrets.menu.GeneratorMenu;

Expand Down Expand Up @@ -59,5 +60,6 @@ protected void renderBg(GuiGraphics graphics, float partialTick, int mouseX, int
long capacity = menu.getCapacity();
int filled = (int) (menu.getEnergy() * ENERGY_HEIGHT / (capacity == 0 ? 1 : capacity));
ClientCompat.blit(graphics, TEXTURE, leftPos + 153, topPos + 17 + ENERGY_HEIGHT - filled, 196, 68 - filled, 14, filled);
DisabledModuleOverlay.renderSlotFrames(graphics, menu, leftPos, topPos);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import net.minecraft.world.entity.player.Inventory;
import omtteam.openmodularturrets.OMT;
import omtteam.openmodularturrets.blockentity.TurretBaseBlockEntity;
import omtteam.openmodularturrets.client.render.DisabledModuleOverlay;
import omtteam.omlib.client.compat.ClientCompat;
import omtteam.omlib.client.compat.ClientNetworkCompat;
import omtteam.openmodularturrets.core.base.TargetingSettings;
Expand Down Expand Up @@ -161,6 +162,7 @@ protected void renderBg(GuiGraphics graphics, float partialTick, int mouseX, int
ClientCompat.blit(graphics, texture, leftPos + 153, topPos + 17, 178, 17, 14, 51);
int u = ENERGY_FILL_U[random.nextInt(ENERGY_FILL_U.length)];
ClientCompat.blit(graphics, texture, leftPos + 153, topPos + 17 + 51 - filled, u, 68 - filled, 14, filled);
DisabledModuleOverlay.renderSlotFrames(graphics, menu, leftPos, topPos);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import omtteam.openmodularturrets.config.OMTConfig;
import omtteam.openmodularturrets.item.AddonItem;
import omtteam.openmodularturrets.item.GeneratorUpgradeItem;
import omtteam.openmodularturrets.item.ModuleToggles;
import omtteam.openmodularturrets.item.TurretBaseUpgradeItem;
import omtteam.openmodularturrets.item.UpgradeItem;

Expand All @@ -29,6 +30,10 @@ public static void append(ItemStack stack, TooltipFlag flag, List<Component> lin
boolean upgrade = stack.getItem() instanceof UpgradeItem;
boolean addon = stack.getItem() instanceof AddonItem;
if (!upgrade && !addon) return;
// switched off in the (server's) config: the item stays, but does nothing
if (ModuleToggles.isDisabled(stack)) {
lines.add(translated("module_disabled").withStyle(ChatFormatting.RED));
}
// which machine an upgrade fits into, also without shift (each machine has its own upgrades)
if (stack.getItem() instanceof TurretBaseUpgradeItem) {
lines.add(text("upgrade.machine.turret_base"));
Expand Down
Loading