Skip to content

Commit 4f75e6d

Browse files
committed
feat: add Lootr mod integration for Treasure Chests
Adds compatibility with Lootr mod to provide per-player loot generation for Treasure Chests. When Lootr is installed, each player gets their own unique inventory when opening a Treasure Chest.
1 parent c93e0bc commit 4f75e6d

10 files changed

Lines changed: 402 additions & 5 deletions

File tree

build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,8 @@ dependencies {
118118
compileOnly "mezz.jei:jei-${project.minecraft_version}-neoforge:${project.jei_version}"
119119

120120
compileOnly "curse.maven:jade-324717:${project.jade_version}"
121-
compileOnly "curse.maven:lootr-361276:${project.lootr_version}"
121+
// Lootr via Maven for full API access
122+
compileOnly "noobanidus.mods.lootr:lootr-neoforge:${project.lootr_maven_version}"
122123

123124
// implementation fileTree(dir: 'libs', include: '*.jar')
124125

gradle.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ debugutils_version=1.0.6
3838
jei_version=19.21.0.246
3939
jade_version=5813144
4040
lootr_version=5832064
41+
lootr_maven_version=1.21.1-1.11.36.109
4142
rei_version=16.0.783
4243
cloth_config_version=15.0.140
4344
architectury_version=13.0.6

src/main/java/com/aetherteam/aether/block/dungeon/TreasureChestBlock.java

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import net.minecraft.world.phys.BlockHitResult;
3737
import net.minecraft.world.phys.shapes.CollisionContext;
3838
import net.minecraft.world.phys.shapes.VoxelShape;
39+
import net.neoforged.fml.ModList;
3940
import net.neoforged.neoforge.event.EventHooks;
4041

4142
import java.util.function.Supplier;
@@ -84,7 +85,13 @@ public BlockEntityType<? extends TreasureChestBlockEntity> blockEntityType() {
8485

8586
@Override
8687
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, BlockState state, BlockEntityType<T> blockEntityType) {
87-
return level.isClientSide() ? createTickerHelper(blockEntityType, this.blockEntityType(), TreasureChestBlockEntity::lidAnimateTick) : null;
88+
if (level.isClientSide()) {
89+
return createTickerHelper(blockEntityType, this.blockEntityType(), TreasureChestBlockEntity::lidAnimateTick);
90+
} else if (ModList.get().isLoaded("lootr")) {
91+
// Lootr integration: use Lootr's ticker for decay/refresh mechanics
92+
return com.aetherteam.aether.integration.lootr.LootrCompat.getTicker();
93+
}
94+
return null;
8895
}
8996

9097
@Override
@@ -98,6 +105,7 @@ public void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSource
98105
/**
99106
* [CODE COPY] - {@link ChestBlock#useWithoutItem(BlockState, Level, BlockPos, Player, BlockHitResult)}.<br><br>
100107
* Handles behavior for checking if a chest is locked and being able to unlock the chest.
108+
* When Lootr is installed, uses Lootr's per-player inventory system.
101109
*
102110
* @param state The {@link BlockState} of the block.
103111
* @param level The {@link Level} the block is in.
@@ -114,11 +122,23 @@ public InteractionResult useWithoutItem(BlockState state, Level level, BlockPos
114122
if (level.isClientSide()) {
115123
return InteractionResult.SUCCESS;
116124
} else {
117-
MenuProvider menuprovider = this.getMenuProvider(state, level, pos);
118-
if (menuprovider != null) {
119-
player.openMenu(menuprovider);
125+
// Lootr integration: use per-player inventory when Lootr is installed
126+
if (ModList.get().isLoaded("lootr")) {
127+
if (player.isShiftKeyDown()) {
128+
com.aetherteam.aether.integration.lootr.LootrCompat.handleTreasureChestSneak(player, pos, level);
129+
} else {
130+
com.aetherteam.aether.integration.lootr.LootrCompat.openTreasureChest(player, pos, level);
131+
}
120132
player.awardStat(Stats.CUSTOM.get(Stats.OPEN_CHEST));
121133
PiglinAi.angerNearbyPiglins(player, true);
134+
} else {
135+
// Default behavior without Lootr
136+
MenuProvider menuprovider = this.getMenuProvider(state, level, pos);
137+
if (menuprovider != null) {
138+
player.openMenu(menuprovider);
139+
player.awardStat(Stats.CUSTOM.get(Stats.OPEN_CHEST));
140+
PiglinAi.angerNearbyPiglins(player, true);
141+
}
122142
}
123143

124144
return InteractionResult.CONSUME;
@@ -185,6 +205,15 @@ public void onRemove(BlockState state, Level level, BlockPos pos, BlockState sta
185205
}
186206
}
187207

208+
@Override
209+
public void playerDestroy(Level level, Player player, BlockPos pos, BlockState state, BlockEntity blockEntity, ItemStack itemStack) {
210+
super.playerDestroy(level, player, pos, state, blockEntity, itemStack);
211+
// Lootr integration: notify Lootr when chest is destroyed
212+
if (ModList.get().isLoaded("lootr") && blockEntity instanceof TreasureChestBlockEntity treasureChest) {
213+
com.aetherteam.aether.integration.lootr.LootrCompat.onTreasureChestDestroyed(level, player, pos, treasureChest);
214+
}
215+
}
216+
188217
/**
189218
* Prevents Treasure Chests from being broken when they're locked, but allows it when they're unlocked.
190219
*

src/main/java/com/aetherteam/aether/blockentity/TreasureChestBlockEntity.java

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@
44
import com.aetherteam.aether.block.AetherBlocks;
55
import com.aetherteam.aether.item.components.AetherDataComponents;
66
import com.aetherteam.aether.item.components.DungeonKind;
7+
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
78
import net.minecraft.core.BlockPos;
89
import net.minecraft.core.Direction;
910
import net.minecraft.core.HolderLookup;
1011
import net.minecraft.core.NonNullList;
1112
import net.minecraft.core.component.DataComponentMap;
1213
import net.minecraft.nbt.CompoundTag;
14+
import net.minecraft.nbt.ListTag;
15+
import net.minecraft.nbt.NbtUtils;
16+
import net.minecraft.nbt.Tag;
1317
import net.minecraft.network.Connection;
1418
import net.minecraft.network.chat.Component;
1519
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
@@ -31,6 +35,8 @@
3135
import net.minecraft.world.level.block.entity.*;
3236
import net.minecraft.world.level.block.state.BlockState;
3337

38+
import java.util.Set;
39+
import java.util.UUID;
3440
import java.util.stream.IntStream;
3541

3642
/**
@@ -39,6 +45,13 @@
3945
*/
4046
public class TreasureChestBlockEntity extends RandomizableContainerBlockEntity implements LidBlockEntity, WorldlyContainer {
4147
private NonNullList<ItemStack> items = NonNullList.withSize(27, ItemStack.EMPTY);
48+
49+
// Lootr integration fields
50+
private UUID lootrInfoId = null;
51+
private boolean lootrHasBeenOpened = false;
52+
private final Set<UUID> lootrClientOpeners = new ObjectOpenHashSet<>();
53+
private boolean lootrClientOpened = false;
54+
4255
private final ContainerOpenersCounter openersCounter = new ContainerOpenersCounter() {
4356
protected void onOpen(Level level, BlockPos pos, BlockState state) {
4457
TreasureChestBlockEntity.playSound(level, pos, state, SoundEvents.CHEST_OPEN);
@@ -181,6 +194,41 @@ public boolean getLocked() {
181194
return this.locked;
182195
}
183196

197+
// Lootr integration methods
198+
public UUID getLootrInfoUUID() {
199+
if (this.lootrInfoId == null) {
200+
this.lootrInfoId = UUID.randomUUID();
201+
}
202+
return this.lootrInfoId;
203+
}
204+
205+
public boolean getLootrHasBeenOpened() {
206+
return this.lootrHasBeenOpened;
207+
}
208+
209+
public void setLootrHasBeenOpened(boolean opened) {
210+
this.lootrHasBeenOpened = opened;
211+
}
212+
213+
public Set<UUID> getLootrClientOpeners() {
214+
return this.lootrClientOpeners;
215+
}
216+
217+
public boolean isLootrClientOpened() {
218+
return this.lootrClientOpened;
219+
}
220+
221+
public void setLootrClientOpened(boolean opened) {
222+
this.lootrClientOpened = opened;
223+
}
224+
225+
public boolean hasClientOpened(UUID uuid) {
226+
if (this.lootrClientOpened) {
227+
return true;
228+
}
229+
return !this.lootrClientOpeners.isEmpty() && this.lootrClientOpeners.contains(uuid);
230+
}
231+
184232
@Override
185233
public void startOpen(Player player) {
186234
if (!this.remove && !player.isSpectator()) {
@@ -275,6 +323,18 @@ public void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) {
275323
if (!this.trySaveLootTable(tag)) {
276324
ContainerHelper.saveAllItems(tag, this.items, registries);
277325
}
326+
// Lootr integration
327+
if (this.lootrInfoId != null) {
328+
tag.putUUID("LootrInfoId", this.lootrInfoId);
329+
}
330+
tag.putBoolean("LootrHasBeenOpened", this.lootrHasBeenOpened);
331+
if (this.level != null && this.level.isClientSide() && !this.lootrClientOpeners.isEmpty()) {
332+
ListTag openersList = new ListTag();
333+
for (UUID opener : this.lootrClientOpeners) {
334+
openersList.add(NbtUtils.createUUID(opener));
335+
}
336+
tag.put("LootrOpeners", openersList);
337+
}
278338
}
279339

280340
@Override
@@ -286,6 +346,20 @@ public void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) {
286346
if (!this.tryLoadLootTable(tag)) {
287347
ContainerHelper.loadAllItems(tag, this.items, registries);
288348
}
349+
// Lootr integration
350+
if (tag.hasUUID("LootrInfoId")) {
351+
this.lootrInfoId = tag.getUUID("LootrInfoId");
352+
}
353+
if (tag.contains("LootrHasBeenOpened", Tag.TAG_BYTE)) {
354+
this.lootrHasBeenOpened = tag.getBoolean("LootrHasBeenOpened");
355+
}
356+
this.lootrClientOpeners.clear();
357+
if (tag.contains("LootrOpeners", Tag.TAG_LIST)) {
358+
ListTag openersList = tag.getList("LootrOpeners", Tag.TAG_INT_ARRAY);
359+
for (Tag openerTag : openersList) {
360+
this.lootrClientOpeners.add(NbtUtils.loadUUID(openerTag));
361+
}
362+
}
289363
}
290364

291365
@Override

src/main/java/com/aetherteam/aether/client/AetherAtlases.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ public class AetherAtlases {
1212
public static Material TREASURE_CHEST_LEFT_MATERIAL;
1313
public static Material TREASURE_CHEST_RIGHT_MATERIAL;
1414

15+
// Lootr integration textures
16+
public static Material LOOTR_TREASURE_CHEST_OPENED_MATERIAL;
17+
public static Material LOOTR_TREASURE_CHEST_UNOPENED_MATERIAL;
18+
1519
/**
1620
* Need to register these static values here from {@link AetherClient#clientSetup(FMLClientSetupEvent)},
1721
* otherwise they'll be loaded too early from static initialization in the field.
@@ -20,6 +24,10 @@ public static void registerTreasureChestAtlases() {
2024
TREASURE_CHEST_MATERIAL = getChestMaterial("treasure_chest");
2125
TREASURE_CHEST_LEFT_MATERIAL = getChestMaterial("treasure_chest_left");
2226
TREASURE_CHEST_RIGHT_MATERIAL = getChestMaterial("treasure_chest_right");
27+
28+
// Lootr textures (used when Lootr is installed)
29+
LOOTR_TREASURE_CHEST_OPENED_MATERIAL = getChestMaterial("lootr/treasure_chest_opened");
30+
LOOTR_TREASURE_CHEST_UNOPENED_MATERIAL = getChestMaterial("lootr/treasure_chest_unopened");
2331
}
2432

2533
public static void registerWoodTypeAtlases() {

src/main/java/com/aetherteam/aether/client/renderer/blockentity/TreasureChestRenderer.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
import com.aetherteam.aether.blockentity.TreasureChestBlockEntity;
44
import com.aetherteam.aether.client.AetherAtlases;
5+
import net.minecraft.client.Minecraft;
56
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
67
import net.minecraft.client.renderer.blockentity.ChestRenderer;
78
import net.minecraft.client.resources.model.Material;
89
import net.minecraft.world.level.block.state.properties.ChestType;
10+
import net.neoforged.fml.ModList;
911

1012
public class TreasureChestRenderer extends ChestRenderer<TreasureChestBlockEntity> {
1113
public TreasureChestRenderer(BlockEntityRendererProvider.Context context) {
@@ -14,6 +16,17 @@ public TreasureChestRenderer(BlockEntityRendererProvider.Context context) {
1416

1517
@Override
1618
protected Material getMaterial(TreasureChestBlockEntity blockEntity, ChestType chestType) {
19+
// Lootr integration: use opened/unopened textures based on player's state
20+
if (ModList.get().isLoaded("lootr") && chestType == ChestType.SINGLE) {
21+
if (Minecraft.getInstance().player != null
22+
&& !blockEntity.getLocked()
23+
&& blockEntity.hasClientOpened(Minecraft.getInstance().player.getUUID())) {
24+
return AetherAtlases.LOOTR_TREASURE_CHEST_OPENED_MATERIAL;
25+
} else if (!blockEntity.getLocked()) {
26+
return AetherAtlases.LOOTR_TREASURE_CHEST_UNOPENED_MATERIAL;
27+
}
28+
}
29+
1730
return switch (chestType) {
1831
case LEFT -> AetherAtlases.TREASURE_CHEST_LEFT_MATERIAL;
1932
case RIGHT -> AetherAtlases.TREASURE_CHEST_RIGHT_MATERIAL;
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package com.aetherteam.aether.integration.lootr;
2+
3+
import com.aetherteam.aether.blockentity.TreasureChestBlockEntity;
4+
import net.minecraft.core.BlockPos;
5+
import net.minecraft.server.level.ServerPlayer;
6+
import net.minecraft.world.entity.player.Player;
7+
import net.minecraft.world.level.Level;
8+
import net.minecraft.world.level.block.entity.BlockEntity;
9+
import net.minecraft.world.level.block.entity.BlockEntityTicker;
10+
import noobanidus.mods.lootr.common.api.LootrAPI;
11+
import noobanidus.mods.lootr.common.api.data.ILootrInfoProvider;
12+
import noobanidus.mods.lootr.common.api.data.blockentity.ILootrBlockEntity;
13+
14+
/**
15+
* Wrapper class for Lootr API calls.
16+
* This class should only be loaded when Lootr is present to avoid ClassNotFoundException.
17+
*/
18+
public class LootrCompat {
19+
20+
/**
21+
* Opens a treasure chest using Lootr's per-player inventory system.
22+
*
23+
* @param player The player opening the chest
24+
* @param pos The position of the chest
25+
* @param level The level containing the chest
26+
*/
27+
public static void openTreasureChest(Player player, BlockPos pos, Level level) {
28+
if (player instanceof ServerPlayer serverPlayer) {
29+
ILootrInfoProvider provider = ILootrInfoProvider.of(pos, level);
30+
if (provider != null) {
31+
LootrAPI.handleProviderOpen(provider, serverPlayer);
32+
}
33+
}
34+
}
35+
36+
/**
37+
* Handles shift-click on a treasure chest to mark it as unopened.
38+
*
39+
* @param player The player interacting with the chest
40+
* @param pos The position of the chest
41+
* @param level The level containing the chest
42+
*/
43+
public static void handleTreasureChestSneak(Player player, BlockPos pos, Level level) {
44+
if (player instanceof ServerPlayer serverPlayer) {
45+
ILootrInfoProvider provider = ILootrInfoProvider.of(pos, level);
46+
if (provider != null) {
47+
LootrAPI.handleProviderSneak(provider, serverPlayer);
48+
}
49+
}
50+
}
51+
52+
/**
53+
* Called when a treasure chest is destroyed to handle Lootr cleanup.
54+
*
55+
* @param level The level
56+
* @param player The player who destroyed the chest
57+
* @param pos The position of the chest
58+
* @param blockEntity The block entity
59+
*/
60+
public static void onTreasureChestDestroyed(Level level, Player player, BlockPos pos, TreasureChestBlockEntity blockEntity) {
61+
LootrAPI.playerDestroyed(level, player, pos, blockEntity);
62+
}
63+
64+
/**
65+
* Checks if a player has already opened a specific treasure chest (client-side).
66+
*
67+
* @param blockEntity The treasure chest block entity
68+
* @param player The player to check
69+
* @return true if the player has opened this chest
70+
*/
71+
public static boolean hasClientOpened(TreasureChestBlockEntity blockEntity, Player player) {
72+
return blockEntity.hasClientOpened(player.getUUID());
73+
}
74+
75+
/**
76+
* @return true if Lootr is configured to use vanilla textures
77+
*/
78+
public static boolean isVanillaTextures() {
79+
return LootrAPI.isVanillaTextures();
80+
}
81+
82+
/**
83+
* Gets the Lootr block entity ticker for decay/refresh mechanics.
84+
*
85+
* @return The Lootr ticker
86+
*/
87+
@SuppressWarnings("unchecked")
88+
public static <T extends BlockEntity> BlockEntityTicker<T> getTicker() {
89+
return (BlockEntityTicker<T>) (BlockEntityTicker<TreasureChestBlockEntity>) ILootrBlockEntity::ticker;
90+
}
91+
}

0 commit comments

Comments
 (0)