Skip to content
Merged
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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,23 @@ HTM requires no additional setup apart from placing it in your mods folder. All

HTM also supports the LuckPerms API, which allows you to manage permissions. Permission nodes are listed in the relevant section. All nodes except `htm.admin` are enabled for all users by default.

## Lockable blocks

The following blocks can be locked with HTM:

- All generic container blocks, including, but not limited to:
- Furnaces, blast furnaces, smokers.
- Barrels.
- Brewing stands.
- Chests, double chests, trapped chests, and their respective copper variants.
- Crafters, dispensers, hoppers and droppers.
- Shulker boxes.
- Chiseled bookshelves.
- Decorated pots.
- Jukeboxes.
- Lecterns.
- Signs.

## Using HTM

The mod has multiple commands which you can use on your containers.
Expand Down
10 changes: 5 additions & 5 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ org.gradle.jvmargs=-Xmx1G

# Fabric Properties
# check these on https://modmuss50.me/fabric.html
minecraft_version=26.2-rc-2
loader_version=0.19.3
minecraft_version=26.3-rc-3
loader_version=0.19.5

# Mod Properties
mod_version=1.2.4
mod_version=1.2.5
maven_group=us.potatoboy
archives_base_name=htm

# Dependencies
# check this on https://modmuss50.me/fabric.html
fabric_version=0.152.1+26.2
translation_version=3.1.0+26.2
fabric_version=0.160.5+26.3
translation_version=3.2.0+26.3
protection_api_version=2.0.0
file_fix_utils_version=0.1.3-26.1
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ClientGamePacketListener;
import net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.TickTask;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand;
Expand Down Expand Up @@ -39,6 +44,12 @@ private static InteractionResult onAttackBlock(Player player, Level level, Inter
InteractionManager.execute(serverPlayer.level().getServer(), serverPlayer, pos);

level.updateNeighborsAt(pos, level.getBlockState(pos).getBlock(), null);

BlockEntity entity = level.getBlockEntity(pos);
if (entity != null) {
scheduleDelayedBlockEntityUpdate(serverPlayer, entity);
}

return InteractionResult.SUCCESS;
}
}
Expand Down Expand Up @@ -72,6 +83,7 @@ private static boolean onBeforeBreak(Level level, Player player, BlockPos pos, B
}

playerEntity.sendSystemMessage(HTMComponents.NOT_OWNER);
scheduleDelayedBlockEntityUpdate(playerEntity, blockEntity);
return false;
}

Expand Down Expand Up @@ -107,4 +119,19 @@ private static InteractionResult onPlace(Player player, BlockPlaceContext contex

return InteractionResult.PASS;
}

/// Sends the {@link ServerPlayer} a block entity update packet, if the entity has one, after a tick (approximately).
///
/// The delay is necessary when the respective {@link ClientboundBlockUpdatePacket} has just been sent, else the
/// block entity update packet may be handled on the client before the block entity has been constructed clientside,
/// leading to client side data loss.
///
/// This problem is especially visible on signs, without the delay the sign's text may just disappear.
private static void scheduleDelayedBlockEntityUpdate(ServerPlayer player, BlockEntity entity) {
Packet<ClientGamePacketListener> updatePacket = entity.getUpdatePacket();
if (updatePacket != null) {
MinecraftServer server = player.level().getServer();
server.schedule(new TickTask(server.getTickCount() + 1, () -> player.connection.send(updatePacket)));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.github.fabricservertools.htm.mixin.lockable;

import com.github.fabricservertools.htm.api.LockableObject;
import com.github.fabricservertools.htm.lock.HTMContainerLock;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.entity.SignBlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.storage.ValueInput;
import net.minecraft.world.level.storage.ValueOutput;
import org.jspecify.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;

import java.util.Optional;

@Mixin(SignBlockEntity.class)
public abstract class SignBlockEntityMixin extends BlockEntity implements LockableObject {

@Unique
private @Nullable HTMContainerLock lock = null;

public SignBlockEntityMixin(BlockEntityType<?> type, BlockPos worldPosition, BlockState blockState) {
super(type, worldPosition, blockState);
}

@Inject(method = "loadAdditional", at = @At("TAIL"))
public void readLockData(ValueInput input, CallbackInfo ci) {
readLock(input, lock -> this.lock = lock);
}

@Inject(method = "saveAdditional", at = @At("TAIL"))
public void writeLockData(ValueOutput output, CallbackInfo ci) {
writeLock(output);
}

@Override
public Optional<HTMContainerLock> getLock() {
return Optional.ofNullable(lock);
}

@Override
public void setLock(@Nullable HTMContainerLock lock) {
this.lock = lock;
setChanged();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.github.fabricservertools.htm.mixin.lockable;

import com.github.fabricservertools.htm.interactions.InteractionManager;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.BaseEntityBlock;
import net.minecraft.world.level.block.SignBlock;
import net.minecraft.world.level.block.SimpleWaterloggedBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;
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;

@Mixin(SignBlock.class)
public abstract class SignBlockMixin extends BaseEntityBlock implements SimpleWaterloggedBlock {

protected SignBlockMixin(Properties properties) {
super(properties);
}

@Inject(method = "useItemOn",
at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/SignBlock;otherPlayerIsEditingSign(Lnet/minecraft/world/entity/player/Player;Lnet/minecraft/world/level/block/entity/SignBlockEntity;)Z"),
cancellable = true)
public void checkLock(ItemStack stack, BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hitResult, CallbackInfoReturnable<InteractionResult> cir) {
if (player instanceof ServerPlayer serverPlayer && !InteractionManager.canOpen(serverPlayer, pos)) {
cir.setReturnValue(InteractionResult.FAIL);
}
}

@Inject(method = "useWithoutItem",
at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/SignBlock;otherPlayerIsEditingSign(Lnet/minecraft/world/entity/player/Player;Lnet/minecraft/world/level/block/entity/SignBlockEntity;)Z"),
cancellable = true)
public void checkLock(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hitResult, CallbackInfoReturnable<InteractionResult> cir) {
if (player instanceof ServerPlayer serverPlayer && !InteractionManager.canOpen(serverPlayer, pos)) {
cir.setReturnValue(InteractionResult.FAIL);
}
}
}
4 changes: 2 additions & 2 deletions src/main/resources/fabric.mod.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@
"htm.mixins.json"
],
"depends": {
"fabricloader": ">=0.19.3",
"fabricloader": ">=0.19.5",
"fabric-api": "*",
"minecraft": "~26.2",
"minecraft": "~26.3",
"filefixutils": "*"
}
}
8 changes: 5 additions & 3 deletions src/main/resources/htm.mixins.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
"compatibilityLevel": "JAVA_25",
"mixins": [
"HopperBlockEntityMixin",
"TransportItemsBetweenContainersMixin",
"PistonBlockMixin",
"TransportItemsBetweenContainersMixin",
"events.BlockItemMixin",
"events.EnderDragonMixin",
"events.ServerExplosionMixin",
"events.LevelMixin",
"events.ServerExplosionMixin",
"lockable.ChiseledBookShelfBlockEntityMixin",
"lockable.ChiseledBookShelfBlockMixin",
"lockable.DecoratedPotBlockEntityMixin",
Expand All @@ -19,7 +19,9 @@
"lockable.JukeboxBlockMixin",
"lockable.LecternBlockEntityMixin",
"lockable.LecternBlockMixin",
"lockable.LockableContainerMixin"
"lockable.LockableContainerMixin",
"lockable.SignBlockEntityMixin",
"lockable.SignBlockMixin"
],
"injectors": {
"defaultRequire": 1
Expand Down
Loading