Skip to content

Commit 3ed16c9

Browse files
committed
LootSearch
1 parent 51ff8f2 commit 3ed16c9

File tree

9 files changed

+608
-119
lines changed

9 files changed

+608
-119
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,11 @@ I did not, nor could I copy their code directly as most are Meteor based mods. S
357357

358358
![UI](https://i.imgur.com/4b5IA0u.png)
359359

360+
### LootSearch
361+
- Using [my fork](https://github.com/cev-api/SeedMapper-CevAPI) of [SeedMapper](https://github.com/xpple/SeedMapper/) you can export loot tables for selected/all structures and then parse that information using the same UI as ChestSearch.
362+
- You can search for specific loot and set a waypoint towards it.
363+
- Will not function without a valid SeedMapper loot export JSON for the server you're in.
364+
360365
## What’s changed or improved in this fork?
361366

362367
### ItemESP (Expanded)

src/main/java/net/wurstclient/chestsearch/ChestRecorder.java

Lines changed: 3 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ public class ChestRecorder
4141
private final Map<Integer, List<ItemStack>> buffers = new HashMap<>();
4242
private final Map<Integer, TimerTask> pendingSnapshots = new HashMap<>();
4343

44+
// (notifications are handled by the UI layer)
45+
4446
public static class Bounds
4547
{
4648
public final int minX, minY, minZ;
@@ -461,45 +463,7 @@ private void recordFromStacksInternal(String serverIp, String dimension,
461463
manager.upsertChest(serverIp, dimension, minX, minY, minZ, items, maxX,
462464
maxY, maxZ, facing, Integer.valueOf(x), Integer.valueOf(y),
463465
Integer.valueOf(z));
464-
465-
// debug removed
466-
467-
// Notify container screen that chest was saved so UI text appears
468-
final int nx = minX, ny = minY, nz = minZ;
469-
try
470-
{
471-
Object screen = net.wurstclient.WurstClient.MC == null ? null
472-
: net.wurstclient.WurstClient.MC.screen;
473-
if(screen != null)
474-
{
475-
try
476-
{
477-
java.lang.reflect.Field fMsg =
478-
screen.getClass().getDeclaredField("lastRecordMessage");
479-
java.lang.reflect.Field fUntil =
480-
screen.getClass().getDeclaredField("lastRecordUntilMs");
481-
fMsg.setAccessible(true);
482-
fUntil.setAccessible(true);
483-
java.time.LocalTime t = java.time.LocalTime.now();
484-
String ts =
485-
t.truncatedTo(java.time.temporal.ChronoUnit.SECONDS)
486-
.toString();
487-
String msg = "Chest recorded, position " + nx + "," + ny
488-
+ "," + nz + " at " + ts;
489-
fMsg.set(screen, msg);
490-
fUntil.setLong(screen, System.currentTimeMillis() + 4000);
491-
}catch(NoSuchFieldException nsf)
492-
{
493-
// not the expected screen type, ignore
494-
}catch(Throwable ignored)
495-
{
496-
// reflection failed, ignore
497-
}
498-
}
499-
}catch(Throwable ignored)
500-
{
501-
ignored.printStackTrace();
502-
}
466+
// Notifications are handled on container close by the UI mixin.
503467
}
504468

505469
private static String sanitizePath(String raw)

src/main/java/net/wurstclient/clickgui/screens/ChestSearchScreen.java

Lines changed: 127 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,19 @@ public ChestSearchScreen(Screen prev, Object ignored)
201201
this.openedByKeybind = (ignored instanceof Boolean && (Boolean)ignored);
202202
}
203203

204+
/**
205+
* Construct using a custom ChestManager (for example, lootsearch data).
206+
*/
207+
public ChestSearchScreen(Screen prev,
208+
net.wurstclient.chestsearch.ChestManager manager,
209+
boolean openedByKeybind)
210+
{
211+
super(Component.literal("Chest Search"));
212+
this.prev = prev;
213+
this.chestManager = manager == null ? new ChestManager() : manager;
214+
this.openedByKeybind = openedByKeybind;
215+
}
216+
204217
@Override
205218
protected void init()
206219
{
@@ -279,7 +292,8 @@ private void onSearchChanged(String q)
279292
currentQuery = qq;
280293
lastMatchQuery = qq.toLowerCase(Locale.ROOT);
281294
matchCache.clear();
282-
this.chestManager = new ChestManager();
295+
if(this.chestManager == null)
296+
this.chestManager = new ChestManager();
283297
java.util.List<ChestEntry> raw =
284298
qq.isEmpty() ? new java.util.ArrayList<>(chestManager.all())
285299
: new java.util.ArrayList<>(chestManager.search(qq));
@@ -413,7 +427,19 @@ private java.util.List<ChestEntry> applyRadiusFilter(
413427
net.minecraft.client.Minecraft mc = WurstClient.MC;
414428
if(mc == null || mc.player == null)
415429
return entries;
416-
430+
431+
// If displaying LootChestManager data (seedmapper exports), skip
432+
// the ChestSearch display radius filter so all loot entries are shown.
433+
try
434+
{
435+
if(this.chestManager != null
436+
&& this.chestManager.getClass().getName()
437+
.equals("net.wurstclient.lootsearch.LootChestManager"))
438+
{
439+
return entries;
440+
}
441+
}catch(Throwable ignored)
442+
{}
417443
net.wurstclient.hacks.ChestSearchHack hack;
418444
try
419445
{
@@ -504,6 +530,14 @@ private void rebuildRowButtons()
504530
searchField.getValue() == null ? "" : searchField.getValue();
505531
java.util.List<ChestEntry.ItemEntry> matches =
506532
collectMatches(e, query);
533+
boolean isLootManager = false;
534+
try
535+
{
536+
isLootManager = this.chestManager != null
537+
&& this.chestManager.getClass().getName()
538+
.equals("net.wurstclient.lootsearch.LootChestManager");
539+
}catch(Throwable ignored)
540+
{}
507541
int matchLines = matches.isEmpty() ? 1 : matches.size();
508542
int boxHeight = computeBoxHeight(pinnedEntry, matchLines);
509543
if(y + boxHeight < visibleTop)
@@ -520,76 +554,106 @@ private void rebuildRowButtons()
520554
.withStyle(style -> style
521555
.withColor(net.minecraft.ChatFormatting.GOLD))
522556
: Component.literal("ESP");
523-
Button espBtn = Button.builder(espLabel, b -> {
524-
try
525-
{
526-
String dimLocal = normalizeDimension(e.dimension);
527-
// Use this entry's clicked position (the block the
528-
// player actually clicked when recording) so ESP draws
529-
// on the expected chest half.
530-
BlockPos useMin = e.getClickedPos();
531-
boolean exists = false;
532-
if(WurstClient.MC != null && WurstClient.MC.level != null)
533-
{
534-
var world = WurstClient.MC.level;
535-
var state = world.getBlockState(useMin);
536-
boolean container = state != null && (state
537-
.getBlock() instanceof net.minecraft.world.level.block.ChestBlock
538-
|| state
539-
.getBlock() instanceof net.minecraft.world.level.block.BarrelBlock
540-
|| state
541-
.getBlock() instanceof net.minecraft.world.level.block.ShulkerBoxBlock
542-
|| state
543-
.getBlock() instanceof net.minecraft.world.level.block.DecoratedPotBlock
544-
|| state
545-
.getBlock() instanceof net.minecraft.world.level.block.EnderChestBlock);
546-
boolean hasBe = world.getBlockEntity(useMin) != null;
547-
exists = container && hasBe;
548-
}
549-
if(!exists)
557+
Button espBtn = null;
558+
if(!isLootManager)
559+
{
560+
espBtn = Button.builder(espLabel, b -> {
561+
try
550562
{
551-
try
563+
String dimLocal = normalizeDimension(e.dimension);
564+
// Use this entry's clicked position (the block the
565+
// player actually clicked when recording) so ESP draws
566+
// on the expected chest half.
567+
BlockPos useMin = e.getClickedPos();
568+
boolean exists = false;
569+
if(WurstClient.MC != null
570+
&& WurstClient.MC.level != null)
552571
{
553-
new ChestManager().removeChest(e.serverIp,
554-
e.dimension, useMin.getX(), useMin.getY(),
555-
useMin.getZ());
556-
}catch(Throwable ignored)
557-
{}
558-
this.chestManager = new ChestManager();
559-
onSearchChanged(searchField.getValue());
572+
boolean isLootManagerInner = false;
573+
try
574+
{
575+
isLootManagerInner = this.chestManager != null
576+
&& this.chestManager.getClass().getName()
577+
.equals(
578+
"net.wurstclient.lootsearch.LootChestManager");
579+
}catch(Throwable ignored)
580+
{}
581+
if(!isLootManagerInner)
582+
{
583+
var world = WurstClient.MC.level;
584+
var state = world.getBlockState(useMin);
585+
boolean container = state != null && (state
586+
.getBlock() instanceof net.minecraft.world.level.block.ChestBlock
587+
|| state
588+
.getBlock() instanceof net.minecraft.world.level.block.BarrelBlock
589+
|| state
590+
.getBlock() instanceof net.minecraft.world.level.block.ShulkerBoxBlock
591+
|| state
592+
.getBlock() instanceof net.minecraft.world.level.block.DecoratedPotBlock
593+
|| state
594+
.getBlock() instanceof net.minecraft.world.level.block.EnderChestBlock);
595+
boolean hasBe =
596+
world.getBlockEntity(useMin) != null;
597+
exists = container && hasBe;
598+
}else
599+
{
600+
// For loot export entries, they may not exist
601+
// as placed blocks in
602+
// the world; allow ESP drawing without removing
603+
// the entry.
604+
exists = true;
605+
}
606+
}
607+
if(!exists)
608+
{
609+
try
610+
{
611+
new ChestManager().removeChest(e.serverIp,
612+
e.dimension, useMin.getX(), useMin.getY(),
613+
useMin.getZ());
614+
}catch(Throwable ignored)
615+
{}
616+
this.chestManager = new ChestManager();
617+
onSearchChanged(searchField.getValue());
618+
minecraft.execute(this::refreshPins);
619+
return;
620+
}
621+
net.wurstclient.hacks.ChestSearchHack hack =
622+
WurstClient.INSTANCE.getHax().chestSearchHack;
623+
net.wurstclient.chestsearch.TargetHighlighter.INSTANCE
624+
.setColors(hack.getEspFillARGB(),
625+
hack.getEspLineARGB());
626+
// render a single-block ESP at the canonical primary
627+
// position
628+
net.wurstclient.chestsearch.TargetHighlighter.INSTANCE
629+
.toggle(dimLocal, useMin, useMin,
630+
hack.getEspTimeMs());
560631
minecraft.execute(this::refreshPins);
561-
return;
562-
}
563-
net.wurstclient.hacks.ChestSearchHack hack =
564-
WurstClient.INSTANCE.getHax().chestSearchHack;
565-
net.wurstclient.chestsearch.TargetHighlighter.INSTANCE
566-
.setColors(hack.getEspFillARGB(),
567-
hack.getEspLineARGB());
568-
// render a single-block ESP at the canonical primary
569-
// position
570-
net.wurstclient.chestsearch.TargetHighlighter.INSTANCE
571-
.toggle(dimLocal, useMin, useMin, hack.getEspTimeMs());
572-
minecraft.execute(this::refreshPins);
573-
}catch(Throwable ignored)
574-
{}
575-
}).bounds(0, btnY, 40, 16).build();
576-
if(espActive)
577-
espBtn.setTooltip(net.minecraft.client.gui.components.Tooltip
578-
.create(Component.literal("ESP active")));
632+
}catch(Throwable ignored)
633+
{}
634+
}).bounds(0, btnY, 40, 16).build();
635+
if(espActive)
636+
espBtn
637+
.setTooltip(net.minecraft.client.gui.components.Tooltip
638+
.create(Component.literal("ESP active")));
639+
}
579640
// position esp and wp buttons to the right side of the result box
580641
int boxRight = x + 340;
581642
int wpWidth = 56;
582-
int espWidth = 40;
643+
int espWidth = isLootManager ? 0 : 40;
583644
int deleteWidth = 56;
584645
// Stack buttons vertically at the right edge (ESP, Waypoint,
585646
// Delete)
586647
int stackWidth = Math.max(Math.max(wpWidth, espWidth), deleteWidth);
587648
int stackRight = boxRight - 6; // 6px padding from box edge
588649
int stackX = stackRight - stackWidth;
589650
// place esp at top, wp below, delete below that
590-
espBtn.setPosition(stackX + (stackWidth - espWidth) / 2, btnY);
591-
addRenderableWidget(espBtn);
592-
rowButtons.add(espBtn);
651+
if(!isLootManager && espBtn != null)
652+
{
653+
espBtn.setPosition(stackX + (stackWidth - espWidth) / 2, btnY);
654+
addRenderableWidget(espBtn);
655+
rowButtons.add(espBtn);
656+
}
593657
boolean hasWp = waypointActive;
594658
Component wpLabel =
595659
hasWp
@@ -681,10 +745,12 @@ else if(p.contains("end"))
681745
// hide per-row buttons when their row is outside the visible
682746
// scrolling region so they don't overlap header/search UI
683747
boolean rowVisible = btnY >= visibleTop && btnY <= visibleBottom;
684-
espBtn.visible = rowVisible;
748+
if(!isLootManager && espBtn != null)
749+
espBtn.visible = rowVisible;
685750
wpBtn.visible = rowVisible;
686751
delBtn.visible = rowVisible;
687-
espBtn.active = rowVisible;
752+
if(!isLootManager && espBtn != null)
753+
espBtn.active = rowVisible;
688754
wpBtn.active = rowVisible;
689755
delBtn.active = rowVisible;
690756
delBtn.setTooltip(net.minecraft.client.gui.components.Tooltip

src/main/java/net/wurstclient/hack/HackList.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,8 @@ public final class HackList implements UpdateListener
227227
public final LogoutSpotsHack logoutSpotsHack = new LogoutSpotsHack();
228228
public final TridentEspHack tridentEspHack = new TridentEspHack();
229229
public final ChestSearchHack chestSearchHack = new ChestSearchHack();
230+
public final net.wurstclient.hacks.LootSearchHack lootSearchHack =
231+
new net.wurstclient.hacks.LootSearchHack();
230232
public final CoordLoggerHack coordLoggerHack = new CoordLoggerHack();
231233

232234
private final TreeMap<String, Hack> hax =
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright (c) 2014-2025 Wurst-Imperium and contributors.
3+
*
4+
* This source code is subject to the terms of the GNU General Public
5+
* License, version 3. If a copy of the GPL was not distributed with this
6+
* file, You can obtain one at: https://www.gnu.org/licenses/gpl-3.0.txt
7+
*/
8+
package net.wurstclient.hacks;
9+
10+
import net.wurstclient.Category;
11+
import net.wurstclient.hack.Hack;
12+
import net.wurstclient.WurstClient;
13+
import net.wurstclient.lootsearch.LootChestManager;
14+
import net.wurstclient.lootsearch.LootSearchUtil;
15+
16+
import java.io.File;
17+
18+
public final class LootSearchHack extends Hack
19+
{
20+
public LootSearchHack()
21+
{
22+
super("LootSearch");
23+
setCategory(Category.ITEMS);
24+
}
25+
26+
@Override
27+
protected void onEnable()
28+
{
29+
try
30+
{
31+
String serverIp = null;
32+
try
33+
{
34+
if(WurstClient.MC != null
35+
&& WurstClient.MC.getCurrentServer() != null)
36+
serverIp = WurstClient.MC.getCurrentServer().ip;
37+
}catch(Throwable ignored)
38+
{}
39+
40+
File dir = LootSearchUtil.getSeedmapperLootDir();
41+
if(dir == null || !dir.exists() || !dir.isDirectory())
42+
{
43+
if(WurstClient.MC != null && WurstClient.MC.player != null)
44+
WurstClient.MC.player.displayClientMessage(
45+
net.minecraft.network.chat.Component.literal(
46+
"SeedMapper loot folder not found. Run SeedMapper and export loot first."),
47+
false);
48+
setEnabled(false);
49+
return;
50+
}
51+
52+
File f = LootSearchUtil.findFileForServer(serverIp);
53+
if(f == null)
54+
{
55+
if(WurstClient.MC != null && WurstClient.MC.player != null)
56+
WurstClient.MC.player
57+
.displayClientMessage(
58+
net.minecraft.network.chat.Component.literal(
59+
"No loot export found for this server."),
60+
false);
61+
setEnabled(false);
62+
return;
63+
}
64+
65+
LootChestManager mgr = new LootChestManager(f, serverIp);
66+
WurstClient.MC.setScreen(
67+
new net.wurstclient.clickgui.screens.ChestSearchScreen(
68+
WurstClient.MC.screen, mgr, Boolean.TRUE));
69+
}catch(Throwable t)
70+
{
71+
t.printStackTrace();
72+
}
73+
setEnabled(false);
74+
}
75+
}

0 commit comments

Comments
 (0)