diff options
| author | coasteen <coasteen@proton.me> | 2026-08-16 08:24:07 +0330 |
|---|---|---|
| committer | coasteen <coasteen@proton.me> | 2026-08-16 08:24:07 +0330 |
| commit | 028ba9d83eec959d18e2717ef33debbec59fca26 (patch) | |
| tree | 093a405db8ad95f57c479f45e5fa88db79220524 /src/main/java/com | |
| parent | dcb35b6f63b441ca93d033aae6aef4ac5ccb0e70 (diff) | |
init
Diffstat (limited to 'src/main/java/com')
6 files changed, 207 insertions, 190 deletions
diff --git a/src/main/java/com/coasteen/armcontainers/ArmContainers.java b/src/main/java/com/coasteen/armcontainers/ArmContainers.java new file mode 100644 index 0000000..aceada0 --- /dev/null +++ b/src/main/java/com/coasteen/armcontainers/ArmContainers.java @@ -0,0 +1,29 @@ +package com.coasteen.armcontainers; + +import com.coasteen.armcontainers.create.GenericContainerPointType; +import com.simibubi.create.content.kinetics.mechanicalArm.ArmInteractionPointType; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.common.Mod; +import net.neoforged.neoforge.registries.RegisterEvent; + +@Mod(ArmContainers.MOD_ID) +public class ArmContainers { + public static final String MOD_ID = "armcontainers"; + + public ArmContainers(IEventBus modEventBus) { + modEventBus.addListener(this::onRegister); + } + + private void onRegister(RegisterEvent event) { + event.register( + ResourceKey.createRegistryKey(ResourceLocation.fromNamespaceAndPath("create", "arm_interaction_point_type")), + helper -> helper.register( + ResourceLocation.fromNamespaceAndPath(MOD_ID, "generic_container"), + new GenericContainerPointType() + ) + ); + } +} diff --git a/src/main/java/com/coasteen/armcontainers/create/GenericContainerPoint.java b/src/main/java/com/coasteen/armcontainers/create/GenericContainerPoint.java new file mode 100644 index 0000000..6a8d649 --- /dev/null +++ b/src/main/java/com/coasteen/armcontainers/create/GenericContainerPoint.java @@ -0,0 +1,143 @@ +package com.coasteen.armcontainers.create; + +import com.simibubi.create.content.kinetics.mechanicalArm.ArmBlockEntity; +import com.simibubi.create.content.kinetics.mechanicalArm.ArmInteractionPoint; +import com.simibubi.create.content.kinetics.mechanicalArm.ArmInteractionPointType; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BarrelBlock; +import net.minecraft.world.level.block.ChestBlock; +import net.minecraft.world.level.block.entity.BarrelBlockEntity; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.ChestBlockEntity; +import net.minecraft.world.level.block.entity.EnderChestBlockEntity; +import net.minecraft.world.level.block.entity.ShulkerBoxBlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.ChestType; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +public class GenericContainerPoint extends ArmInteractionPoint { + + // Track active interactions per container position to allow overlapping arm access + private static final Map<BlockPos, AtomicInteger> CONTAINER_REF_COUNTS = new ConcurrentHashMap<>(); + + public GenericContainerPoint(ArmInteractionPointType type, Level level, BlockPos pos, BlockState state) { + super(type, level, pos, state); + } + + @Override + public ItemStack extract(ArmBlockEntity arm, int slot, int amount, boolean simulate) { + ItemStack result = super.extract(arm, slot, amount, simulate); + if (!simulate && !result.isEmpty()) { + animateContainer(); + } + return result; + } + + @Override + public ItemStack extract(ArmBlockEntity arm, int amount, boolean simulate) { + ItemStack result = super.extract(arm, amount, simulate); + if (!simulate && !result.isEmpty()) { + animateContainer(); + } + return result; + } + + @Override + public ItemStack insert(ArmBlockEntity arm, ItemStack stack, boolean simulate) { + int originalCount = stack.getCount(); + ItemStack remainder = super.insert(arm, stack, simulate); + if (!simulate && remainder.getCount() < originalCount) { + animateContainer(); + } + return remainder; + } + + private void animateContainer() { + if (level == null || level.isClientSide()) return; + + BlockEntity be = level.getBlockEntity(pos); + BlockState state = level.getBlockState(pos); + + AtomicInteger refCount = CONTAINER_REF_COUNTS.computeIfAbsent(pos, k -> new AtomicInteger(0)); + + // If the container isn't currently held open by an arm, trigger the open animation + if (refCount.getAndIncrement() == 0) { + openContainer(be, state); + } + + if (level instanceof ServerLevel serverLevel) { + // Extended delay (~1.2 seconds) to keep the container open longer + CompletableFuture.delayedExecutor(1200, TimeUnit.MILLISECONDS).execute(() -> { + serverLevel.getServer().execute(() -> { + // Only close if all pending arm actions are complete + if (refCount.decrementAndGet() <= 0) { + CONTAINER_REF_COUNTS.remove(pos); + closeContainer(be); + } + }); + }); + } + } + + private void openContainer(BlockEntity be, BlockState state) { + if (be instanceof ChestBlockEntity) { + triggerChestEvent(state, 1); + playSound(SoundEvents.CHEST_OPEN); + } else if (be instanceof EnderChestBlockEntity) { + level.blockEvent(pos, state.getBlock(), 1, 1); + playSound(SoundEvents.ENDER_CHEST_OPEN); + } else if (be instanceof ShulkerBoxBlockEntity) { + level.blockEvent(pos, state.getBlock(), 1, 1); + playSound(SoundEvents.SHULKER_BOX_OPEN); + } else if (be instanceof BarrelBlockEntity) { + if (state.hasProperty(BarrelBlock.OPEN) && !state.getValue(BarrelBlock.OPEN)) { + level.setBlock(pos, state.setValue(BarrelBlock.OPEN, true), 3); + playSound(SoundEvents.BARREL_OPEN); + } + } + } + + private void closeContainer(BlockEntity be) { + if (level == null || level.isClientSide()) return; + BlockState currentState = level.getBlockState(pos); + + if (be instanceof ChestBlockEntity) { + triggerChestEvent(currentState, 0); + playSound(SoundEvents.CHEST_CLOSE); + } else if (be instanceof EnderChestBlockEntity) { + level.blockEvent(pos, currentState.getBlock(), 1, 0); + playSound(SoundEvents.ENDER_CHEST_CLOSE); + } else if (be instanceof ShulkerBoxBlockEntity) { + level.blockEvent(pos, currentState.getBlock(), 1, 0); + playSound(SoundEvents.SHULKER_BOX_CLOSE); + } else if (be instanceof BarrelBlockEntity) { + if (currentState.hasProperty(BarrelBlock.OPEN) && currentState.getValue(BarrelBlock.OPEN)) { + level.setBlock(pos, currentState.setValue(BarrelBlock.OPEN, false), 3); + playSound(SoundEvents.BARREL_CLOSE); + } + } + } + + private void triggerChestEvent(BlockState state, int param) { + level.blockEvent(pos, state.getBlock(), 1, param); + if (state.hasProperty(ChestBlock.TYPE) && state.getValue(ChestBlock.TYPE) != ChestType.SINGLE) { + BlockPos neighborPos = pos.relative(ChestBlock.getConnectedDirection(state)); + level.blockEvent(neighborPos, state.getBlock(), 1, param); + } + } + + private void playSound(SoundEvent sound) { + level.playSound(null, pos, sound, SoundSource.BLOCKS, 0.5F, level.random.nextFloat() * 0.1F + 0.9F); + } +} diff --git a/src/main/java/com/coasteen/armcontainers/create/GenericContainerPointType.java b/src/main/java/com/coasteen/armcontainers/create/GenericContainerPointType.java new file mode 100644 index 0000000..9020041 --- /dev/null +++ b/src/main/java/com/coasteen/armcontainers/create/GenericContainerPointType.java @@ -0,0 +1,35 @@ +package com.coasteen.armcontainers.create; + +import com.simibubi.create.content.kinetics.mechanicalArm.ArmInteractionPoint; +import com.simibubi.create.content.kinetics.mechanicalArm.ArmInteractionPointType; +import com.simibubi.create.content.logistics.vault.ItemVaultBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BarrelBlockEntity; +import net.minecraft.world.level.block.entity.ChestBlockEntity; +import net.minecraft.world.level.block.entity.ShulkerBoxBlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.neoforged.neoforge.capabilities.Capabilities; +import org.jetbrains.annotations.Nullable; + +public class GenericContainerPointType extends ArmInteractionPointType { + + @Override + public boolean canCreatePoint(Level level, BlockPos pos, BlockState state) { + var be = level.getBlockEntity(pos); + if (be == null) return false; + + boolean isSupportedType = be instanceof ChestBlockEntity + || be instanceof ShulkerBoxBlockEntity + || be instanceof BarrelBlockEntity + || be instanceof ItemVaultBlockEntity; + + return isSupportedType && level.getCapability(Capabilities.ItemHandler.BLOCK, pos, null) != null; + } + + @Nullable + @Override + public ArmInteractionPoint createPoint(Level level, BlockPos pos, BlockState state) { + return new GenericContainerPoint(this, level, pos, state); + } +} diff --git a/src/main/java/com/example/examplemod/Config.java b/src/main/java/com/example/examplemod/Config.java deleted file mode 100644 index 358706c..0000000 --- a/src/main/java/com/example/examplemod/Config.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.example.examplemod; - -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.item.Item; -import net.neoforged.bus.api.SubscribeEvent; -import net.neoforged.fml.common.EventBusSubscriber; -import net.neoforged.fml.event.config.ModConfigEvent; -import net.neoforged.neoforge.common.ModConfigSpec; - -// An example config class. This is not required, but it's a good idea to have one to keep your config organized. -// Demonstrates how to use Neo's config APIs -public class Config { - private static final ModConfigSpec.Builder BUILDER = new ModConfigSpec.Builder(); - - public static final ModConfigSpec.BooleanValue LOG_DIRT_BLOCK = BUILDER - .comment("Whether to log the dirt block on common setup") - .define("logDirtBlock", true); - - public static final ModConfigSpec.IntValue MAGIC_NUMBER = BUILDER - .comment("A magic number") - .defineInRange("magicNumber", 42, 0, Integer.MAX_VALUE); - - public static final ModConfigSpec.ConfigValue<String> MAGIC_NUMBER_INTRODUCTION = BUILDER - .comment("What you want the introduction message to be for the magic number") - .define("magicNumberIntroduction", "The magic number is... "); - - // a list of strings that are treated as resource locations for items - public static final ModConfigSpec.ConfigValue<List<? extends String>> ITEM_STRINGS = BUILDER - .comment("A list of items to log on common setup.") - .defineListAllowEmpty("items", List.of("minecraft:iron_ingot"), () -> "", Config::validateItemName); - - static final ModConfigSpec SPEC = BUILDER.build(); - - private static boolean validateItemName(final Object obj) { - return obj instanceof String itemName && BuiltInRegistries.ITEM.containsKey(ResourceLocation.parse(itemName)); - } -} diff --git a/src/main/java/com/example/examplemod/ExampleMod.java b/src/main/java/com/example/examplemod/ExampleMod.java deleted file mode 100644 index c35a97b..0000000 --- a/src/main/java/com/example/examplemod/ExampleMod.java +++ /dev/null @@ -1,117 +0,0 @@ -package com.example.examplemod; - -import org.slf4j.Logger; - -import com.mojang.logging.LogUtils; - -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.core.registries.Registries; -import net.minecraft.network.chat.Component; -import net.minecraft.world.food.FoodProperties; -import net.minecraft.world.item.BlockItem; -import net.minecraft.world.item.CreativeModeTab; -import net.minecraft.world.item.CreativeModeTabs; -import net.minecraft.world.item.Item; -import net.minecraft.world.level.block.Block; -import net.minecraft.world.level.block.Blocks; -import net.minecraft.world.level.block.state.BlockBehaviour; -import net.minecraft.world.level.material.MapColor; -import net.neoforged.api.distmarker.Dist; -import net.neoforged.bus.api.IEventBus; -import net.neoforged.bus.api.SubscribeEvent; -import net.neoforged.fml.common.Mod; -import net.neoforged.fml.config.ModConfig; -import net.neoforged.fml.ModContainer; -import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent; -import net.neoforged.neoforge.common.NeoForge; -import net.neoforged.neoforge.event.BuildCreativeModeTabContentsEvent; -import net.neoforged.neoforge.event.server.ServerStartingEvent; -import net.neoforged.neoforge.registries.DeferredBlock; -import net.neoforged.neoforge.registries.DeferredHolder; -import net.neoforged.neoforge.registries.DeferredItem; -import net.neoforged.neoforge.registries.DeferredRegister; - -// The value here should match an entry in the META-INF/neoforge.mods.toml file -@Mod(ExampleMod.MODID) -public class ExampleMod { - // Define mod id in a common place for everything to reference - public static final String MODID = "examplemod"; - // Directly reference a slf4j logger - public static final Logger LOGGER = LogUtils.getLogger(); - // Create a Deferred Register to hold Blocks which will all be registered under the "examplemod" namespace - public static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks(MODID); - // Create a Deferred Register to hold Items which will all be registered under the "examplemod" namespace - public static final DeferredRegister.Items ITEMS = DeferredRegister.createItems(MODID); - // Create a Deferred Register to hold CreativeModeTabs which will all be registered under the "examplemod" namespace - public static final DeferredRegister<CreativeModeTab> CREATIVE_MODE_TABS = DeferredRegister.create(Registries.CREATIVE_MODE_TAB, MODID); - - // Creates a new Block with the id "examplemod:example_block", combining the namespace and path - public static final DeferredBlock<Block> EXAMPLE_BLOCK = BLOCKS.registerSimpleBlock("example_block", BlockBehaviour.Properties.of().mapColor(MapColor.STONE)); - // Creates a new BlockItem with the id "examplemod:example_block", combining the namespace and path - public static final DeferredItem<BlockItem> EXAMPLE_BLOCK_ITEM = ITEMS.registerSimpleBlockItem("example_block", EXAMPLE_BLOCK); - - // Creates a new food item with the id "examplemod:example_id", nutrition 1 and saturation 2 - public static final DeferredItem<Item> EXAMPLE_ITEM = ITEMS.registerSimpleItem("example_item", new Item.Properties().food(new FoodProperties.Builder() - .alwaysEdible().nutrition(1).saturationModifier(2f).build())); - - // Creates a creative tab with the id "examplemod:example_tab" for the example item, that is placed after the combat tab - public static final DeferredHolder<CreativeModeTab, CreativeModeTab> EXAMPLE_TAB = CREATIVE_MODE_TABS.register("example_tab", () -> CreativeModeTab.builder() - .title(Component.translatable("itemGroup.examplemod")) //The language key for the title of your CreativeModeTab - .withTabsBefore(CreativeModeTabs.COMBAT) - .icon(() -> EXAMPLE_ITEM.get().getDefaultInstance()) - .displayItems((parameters, output) -> { - output.accept(EXAMPLE_ITEM.get());// Add the example item to the tab. For your own tabs, this method is preferred over the event - }).build()); - - // The constructor for the mod class is the first code that is run when your mod is loaded. - // FML will recognize some parameter types like IEventBus or ModContainer and pass them in automatically. - public ExampleMod(IEventBus modEventBus, ModContainer modContainer) { - // Register the commonSetup method for modloading - modEventBus.addListener(this::commonSetup); - - // Register the Deferred Register to the mod event bus so blocks get registered - BLOCKS.register(modEventBus); - // Register the Deferred Register to the mod event bus so items get registered - ITEMS.register(modEventBus); - // Register the Deferred Register to the mod event bus so tabs get registered - CREATIVE_MODE_TABS.register(modEventBus); - - // Register ourselves for server and other game events we are interested in. - // Note that this is necessary if and only if we want *this* class (ExampleMod) to respond directly to events. - // Do not add this line if there are no @SubscribeEvent-annotated functions in this class, like onServerStarting() below. - NeoForge.EVENT_BUS.register(this); - - // Register the item to a creative tab - modEventBus.addListener(this::addCreative); - - // Register our mod's ModConfigSpec so that FML can create and load the config file for us - modContainer.registerConfig(ModConfig.Type.COMMON, Config.SPEC); - } - - private void commonSetup(FMLCommonSetupEvent event) { - // Some common setup code - LOGGER.info("HELLO FROM COMMON SETUP"); - - if (Config.LOG_DIRT_BLOCK.getAsBoolean()) { - LOGGER.info("DIRT BLOCK >> {}", BuiltInRegistries.BLOCK.getKey(Blocks.DIRT)); - } - - LOGGER.info("{}{}", Config.MAGIC_NUMBER_INTRODUCTION.get(), Config.MAGIC_NUMBER.getAsInt()); - - Config.ITEM_STRINGS.get().forEach((item) -> LOGGER.info("ITEM >> {}", item)); - } - - // Add the example block item to the building blocks tab - private void addCreative(BuildCreativeModeTabContentsEvent event) { - if (event.getTabKey() == CreativeModeTabs.BUILDING_BLOCKS) { - event.accept(EXAMPLE_BLOCK_ITEM); - } - } - - // You can use SubscribeEvent and let the Event Bus discover methods to call - @SubscribeEvent - public void onServerStarting(ServerStartingEvent event) { - // Do something when the server starts - LOGGER.info("HELLO from server starting"); - } -} diff --git a/src/main/java/com/example/examplemod/ExampleModClient.java b/src/main/java/com/example/examplemod/ExampleModClient.java deleted file mode 100644 index bf9b4c6..0000000 --- a/src/main/java/com/example/examplemod/ExampleModClient.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.examplemod; - -import net.minecraft.client.Minecraft; -import net.neoforged.api.distmarker.Dist; -import net.neoforged.bus.api.SubscribeEvent; -import net.neoforged.fml.ModContainer; -import net.neoforged.fml.common.EventBusSubscriber; -import net.neoforged.fml.common.Mod; -import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent; -import net.neoforged.neoforge.client.gui.ConfigurationScreen; -import net.neoforged.neoforge.client.gui.IConfigScreenFactory; - -// This class will not load on dedicated servers. Accessing client side code from here is safe. -@Mod(value = ExampleMod.MODID, dist = Dist.CLIENT) -// You can use EventBusSubscriber to automatically register all static methods in the class annotated with @SubscribeEvent -@EventBusSubscriber(modid = ExampleMod.MODID, value = Dist.CLIENT) -public class ExampleModClient { - public ExampleModClient(ModContainer container) { - // Allows NeoForge to create a config screen for this mod's configs. - // The config screen is accessed by going to the Mods screen > clicking on your mod > clicking on config. - // Do not forget to add translations for your config options to the en_us.json file. - container.registerExtensionPoint(IConfigScreenFactory.class, ConfigurationScreen::new); - } - - @SubscribeEvent - static void onClientSetup(FMLClientSetupEvent event) { - // Some client setup code - ExampleMod.LOGGER.info("HELLO FROM CLIENT SETUP"); - ExampleMod.LOGGER.info("MINECRAFT NAME >> {}", Minecraft.getInstance().getUser().getName()); - } -} |
