Files
copper-farmers/.opencode/plans/minecraft-26-2-migration.md
T
Dynamitos 3e0613f297
build / build (push) Successful in 1m10s
Reworked using opencode
2026-07-19 10:16:39 +02:00

16 KiB

Copper Farmers Migration Plan for Minecraft 26.2

Overview

Porting the copper-farmers mod from 1.21.x Yarn mappings to Minecraft 26.2 where Yarn classes have been completely removed. The key insight is that both MoveItemsTask and CopperGolemBrain classes no longer exist in 26.2 - they've been replaced by entirely new class names and architecture.

What Changed in Minecraft 26.2

Class Removals and Replacements

  • MoveItemsTask (entity.ai.brain.task) → REMOVED → replaced by TransportItemsBetweenContainers (ai.behavior)
  • CopperGolemBrain (entity.passive) → REMOVED → replaced by CopperGolemAi (animal.golem)
  • CopperGolemEntityCopperGolem (same package: animal.golem)

Path Package Changes

Old Path New Path
net.minecraft.entity.* net.minecraft.world.entity.*
net.minecraft.entity.passive.* net.minecraft.world.entity.animal.golem.*
net.minecraft.block.* net.minecraft.world.level.block.*
net.minecraft.inventory.Inventory net.minecraft.Container (root pkg)
net.minecraft.server.world.ServerWorld net.minecraft.server.level.ServerLevel
net.minecraft.sound.SoundEvent net.minecraft.sounds.SoundEvent
net.minecraft.util.math.Box/Vec3d net.minecraft.world.phys.AABB/Vec3
net.minecraft.block.entity.* net.minecraft.world.level.block.entity.*

Inner Class Changes (on new classes)

  • MoveItemsTask.StorageTransportItemTarget nested in TBC
    • .inventory() renamed to .container()
    • Still has pos(), blockEntity(), state() fields
  • MoveItemsTask.InteractionStateContainerInteractionState (TBC nested)
    • Same enum values: PICKUP_ITEM, PICKUP_NO_ITEM, PLACE_ITEM, PLACE_NO_ITEM

Implementation Plan

Files to Create/Modify

  1. TransportItemsBetweenContainersAccessor.java (NEW - replaces MoveItemsTaskAccessor.java)

    • Mixin target: TransportItemsBetweenContainers
    • @Invoker methods for TBC's static helper methods: pickupItemFromContainer, addItemsToContainer, isPickingUpItems
    • Uses Container instead of Inventory
  2. FarmlandPredicateMixin.java (NEW - replaces MoveItemsTaskMixin.java core logic)

    • Mixin target: CopperGolemAi
    • @WrapOperation on TBC constructor to add farmland support to source/destination predicates
    • Makes farmland valid for copper golems to interact with
  3. CopperGolemBehaviorHookMixin.java (NEW - replaces CopperGolemBrainMixin.java)

    • Mixin target: CopperGolemAi.getTargetReachedInteractions()
    • @ModifyReturnValue to customize how farmland and chests are handled as storage
    • Handles chest open/close through Container interface on BlockEntity
    • Custom interaction callback for planting/harvesting
  4. CropRegistry.java (NEW)

    • Central registry for what crops copper golems can plant
    • Default: wheat seeds, potatoes, carrots
    • API for other mods to register their own crop types as farmland-compatible
  5. copper-farmers.mixins.json (MODIFY)

    • Update all class references to new package paths

Key Technical Points for Farmland Support

The golem's behavior controller uses a TransportItemsBetweenContainers (TBC) that has:

  • sourceBlockType predicate (what it can extract items FROM)
  • destinationBlockType predicate (what it can insert items INTO)
  • Both checked via isWantedBlock(state) during target search

The fix is to intercept TBC construction in CopperGolemAi and inject farmland into both predicates. The interaction callback is then modified to handle the two distinct scenarios:

  1. Container target: container != null → do item transfer (existing TCB behavior)
  2. Farmland target: container == null, state == FARMLAND → place seeds / harvest grown crops

Execution Order

  1. Write TransportItemsBetweenContainersAccessor.java
  2. Write CropRegistry.java (base registry with wheat/potato/carrot support)
  3. Write FarmlandPredicateMixin.java (intercept TBC construction)
  4. Write CopperGolemBehaviorHookMixin.java (interaction callbacks + chest detection)
  5. Update copper-farmers.mixins.json
  6. Verify compilation

Complete File Contents for Each File

1. TransportItemsBetweenContainersAccessor.java (NEW - replaces MoveItemsTaskAccessor.java)

Package: com.dynamitos.mixin Target: @Mixin(TransportItemsBasedOnResearch)

package com.dynamitos.mixin;

import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Invoker;

import net.minecraft.Container;  // was Inventory
import net.minecraft.world.entity.PathfinderMob;  // was PathAwareEntity
import net.minecraft.world.entity.ai.behavior.TransportItemsBetweenContainers;
import net.minecraft.item.ItemStack;

@Mixin(TransportItemsBetweenContainers.class)
public interface TransportItemsBasedOnResearchAccessor {
    @Invoker("pickupItemFromContainer")
    ItemStack copperfarmers$pickupItem(Container inventory);

    @Invoker("addItemsToContainer")
    ItemStack copperfarmers$placeItem(PathfinderMob entity, Container inventory);

    @Invoker("isPickingUpItems")
    boolean copperfarmers$isPickingUpItems(PathfinderMob entity);

    @Invoker("matchesGettingItemsRequirement")
    boolean copperfarmers$matchesGettingItemsRequirement(Container inventory);

    @Invoker("hasItemMatchingHandItem")
    boolean copperfarmers$hasItemMatchingHandItem(PathfinderMob entity, Container inventory);

    @Invoker("getTransportTarget")
    java.util.Optional<TransportItemsBetweenContainers.TransportItemTarget> copperfarmers$getTransportTarget(
        net.minecraft.server.level.ServerLevel world, PathfinderMob entity);
}

2. CropRegistry.java (NEW)

Package: com.dynamitos

package com.dynamitos;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;

import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.item.Items;

public class CopperFarmingRegistry {
    private static final List<CropType> CROP_TYPES = new ArrayList<>();

    public record CropType(Block block, Predicate<BlockState> statePredicate) {}

    public static void registerCrop(CropType cropType) {
        CROP_TYPES.add(cropType);
        Copperfarmers.LOGGER.info("Registered crop type: " + cropType.block());
    }

    /** Returns true if the block is farmland and the stack would be a compatible seed/crop */
    public static Predicate<BlockState> getFarmlandPredicate() {
        return state -> {
            if (!state.isOf(Blocks.FARMLAND)) {
                return false;
            }
            // Also register other farmable blocks registered by this mod and other mods
            for (var crop : CROP_TYPES) {
                if (state.isOf(crop.block())) {
                    return true;
                }
            }
            return false;
        };
    }

    static {
        // Register default crops: wheat, potatoes, carrots
        registerCrop(new CropType(Blocks.WHEAT, null));
        registerCrop(new PotatoType(), null);  // potato block/planted variant
        registerCrop(new CarrotType(), null);   // carrot block/planted variant
    }
    
    /** Get predicate for farmland destination (what goes on top of farmland) */
    public static Predicate<BlockState> getDestinationPredicate() {
        State -> state.isOf(Blocks.FARMLAND) && !CROP_TYPES.stream().anyMatch(crop -> crop.statePredicate().test(state));
    }

    /** Get predicate for source (what contains items the golem can extract) */
    public static Predicate<BlockState> getSourcePredicate() {
        State -> state.isOf(Blocks.FARMLAND);  // farmland is both source and destination
    }
    
    /** Check if a BlockState represents planted crops that are ready to harvest */
    public static boolean isCropsReadyToHarvest(BlockState state) {
        if (!state.isOf(Blocks.FARMLAND)) {
            return false;
        }
        // Check the block above farmland for fully-grown crops
        // TODO: This needs to match the grow state of wheat/potato/carrot blocks
    }
}

3. FarmlandPredicateMixin.java (NEW - replaces MoveItemsTaskMixin.java)

Package: com.dynamitos.mixin Target: Mixin onto CopperGolemAi

Key approach: Use @WrapOperation to intercept TBC construction and replace source/destination predicates with farmland-compatible ones. Target the constructor parameters where they are passed.

package com.dynamitos.mixin;

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.CallbackInfoReturnable;
import org.spongepowered.asm.mixin.extra.injector.wrapoperation.Operation;
import org.spongepowered.asm.mixin.extra.injector.wrapoperation.WrapOperation;

import com.dynamitos.CropRegistry;
import net.minecraft.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import java.util.function.Predicate;

@Mixin(CopperGolemAi.class)
public abstract class FarmlandPredicateMixin {

    /** Wrap the predicate lambdas in CopperGolemAi lambda$static$0 and lambda$static$1 to add farmland support */
    @WrapOperation(method = "initCoreActivity", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/state/BlockState;isOf(Lnet/minecraft/world/level/block/;)Z"))
    private static boolean copperfarmers$wrapBlockCheck(BlockState $this, Block block, Operation<Boolean> original) {
        if (block == Blocks.FARMLAND) {
            return true;  // Always accept farmland regardless of what the original predicate checked
        }
        return original.call($this, block);
    }

    /** Alternative: Use @ModifyVariable on getTargetReachedInteractions to replace interaction map */    
    private static Predicate<BlockState> copperfarmers$createFarmlandPredicate() {
        return state -> 
            state.isOf(Blocks.FARMLAND) || 
            CopperFarmingRegistry.getDestinationPredicate().test(state); // Check registered crops
    }
}

4. CopperGolemBehaviorHookMixin.java (NEW - replaces CopperGolemBrainMixin.java)

Package: com.dynamitos.mixin Target: Mixin onto CopperGolemAi.getTargetReachedInteractions()

Callback now is TriConsumer<PathfinderMob, TransportItemTarget, Integer> (was previously: (PathAwareEntity, Storage, int)).

package com.dynamitos.mixin;

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.inition.callback.CallbackInfoReturnable;
import org.spongepowered.asm.mixin.extra.injector.ModifyReturnValue;

import com.dynamitos.Copperfarmers;
import net.minecraft.block.Blocks;
import net.minecraft.sounds.SoundEvents;  // was net.minecraft.sound.SoundEvent -> Sounds.SoundEvents

@Mixin(CopperGolemAi.class)
public abstract class CopperGolemBehaviorHookMixin {

    @ModifyReturnValue(method = "getTargetReachedInteractions")
    private static Map<TransportItemsBetweenContainers.ContainerInteractionState, TransportItemsBasedOnResearch$OnTargetReachedInteraction> 
        copperfarmers$modifyTargetReachedInter(Map original) {
        
        return state -> {
            switch (state) {
                case PLACE_ITEM:
                    // Farmland check - plant seeds if farmland block above is air
                    // Chest check - place item into chest inventory
                    break;
                case PICKUP_ITEM:
                    // Harvest crops from farmland
                    // Extract item from container
                    break;
            }
            return original.get(state);  // Fall back to default for unhandled cases
        };
    }

    /** Handle the actual planting logic (was in MoveItemsTask.placeStack2) */
    private static void handleFarmlandPlanting(
        PathfinderMob golem,
        TransportItemTarget target,
        int interactionTicks) {
        
        BlockPos pos = target.pos();
        World world = golem.getEntityWorld(); // was ServerWorld
        
        // Check farmland: state().isOf(FARMLAND) && world.isAir(pos.up())
        // Similar logic to original placeStack2 method
    }

    /** Handle chest open/close detection (was in moveItemsTaskMixin.tickInteracting) */
    private static boolean isContainerOpen(TransportItemTarget target, PathfinderMob golem) {
        BlockEntity be = target.blockEntity();
        if (be instanceof ChestBlockEntity chest) {
            return !chest.getEntitiesWithContainerOpen().isEmpty();  // was inventory.onViewingUsers()
        }
        return false;
    }
}

5. copper-farmers.mixins.json (UPDATE)

Replace these class references:

  • "CopperGolemBrainMixin""FarmlandPredicateMixin,"CopperGolemBehaviorHookMixin" (split into two, or combine)
  • "MoveItemsTaskMixin" → removed, replaced with above
  • "MoveItemsTaskAccessor""TransportItemsBasedOnResearchAccessor" (or just update class name)

6. Move Items Between Containers.java (update package paths on imports):

Full list of changes in every source file:

MoveItemsTaskAccessor.java → TransportItemsBetweenContainersAccessor

  • import net.minecraft.block.BlockStateimport net.minecraft.world.level.block.state.BlockState;
  • import net.minecraft.entity.ai.brain.task.MoveItemsTaskimport net.minecraft.world.entity.ai.behavior.TransportItemsBetweenContainers;
  • import net.minecraft.entity.mob.PathAwareEntityimport net.minecraft.world.entity.PathfinderMob;
  • import net.minecraft.inventory.Inventoryimport net.minecraft.Container; (root package!)
  • @Mixin(MoveItemsTask.class)@Mixin(TransportItemsBasedOnResearch.class)
  • Inner class refs:
    • MoveItemsTask.InteractionStateTBC$ContainerInteractionState or just import the nested type
    • Method names change as noted above

CopperGolemBrainMixin.java → behavior hook mixin

  • import net.minecraft.block.Blocksnet.minecraft.world.level.block.Blocks;
  • import net.minecraft.block.entity.ChestBlockEntitynet.minecraft.world.level.block.entity.ChestBlockEntity
  • import net.minecraft.entity.passive.CopperGolemBraingone - replaced by CopperGolemAi
  • import net.minecraft.entity.passive.CopperGolemEntitynet.minecraft.world.entity.animal.golem.CopperGolem;
  • import net.minecraft.entity.passive.CopperGolemStatenet.minecraft.world.entity.animal.golem.CopperGolemState (still exists!)
  • @Mixin(CopperGolemBrain.class)@Mixin(CopperGolemAi.class)
  • Methods:
    • createInteractionCallback(...)getTargetReachedInteractions() (method renamed)
    • Callback type change: (PathAwareEntity, Storage, int) → (PathfinderMob, TransportItemTarget, Integer)

Key Code Pattern Updates:

Old pattern used by MoveItemsTaskAccessor:

MoveItemsTaskAccessor.copperfarmers$canPickUpItem(entity); // @Invoker on private method
entity.getEquippedStack(EquipmentSlot.MAINHAND).isOf(Items.WHEAT_SEEDS);
new Storage(pos, null, null, blockState);

New pattern on top of 26.2:

TransportItemsBetweenContainersAccessor.copperfarmers$isPickingUpItems(entity);
// Inventory → Container: inventory is now called container() on target
target.container(); // Not .inventory() 

// Storage record was TransportItemTarget:
target; // Same record field access: pos(), container(), blockEntity(), state() 

Chest detection pattern change:

// old (copperfarmersBrainMixin):
inventory.onOpen(copperGolemEntity);  // Inventory.onOpen  
inventory.getViewingUsers();            // Inventory.getViewingUsers()

// new: BlockEntity instead of Inventory
chestBlockEntity.triggerEvent(ChestBlockEntity.EVENT_SET_OPEN_COUNT, chestBlockEntity.getOpenCount());
chestBlockEntity.getEntitiesWithContainerOpenersCounter().startOpen(chestGolem);  // Or similar
chestBlockEntity.stopOpen(chestGolem);
chestBlockEntity.getEntitiesWithContainerOpen(); // Check who's viewing the container