diff --git a/gradle.properties b/gradle.properties index 704ecc6..1041a9d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ minecraft_version_dependency=>=1.21.11 <1.22 fabric_api_version=0.139.4+1.21.11 parchment_mappings=parchment-1.21.9:2025.10.05@zip fabric_loader_version=0.18.2 -fabric_loom_version=1.14-SNAPSHOT +fabric_loom_version=1.15-SNAPSHOT # Library dependencies betterconfig_version=2.5.0 diff --git a/src/main/java/dev/xpple/seedmapper/seedmap/FeatureToggleWidget.java b/src/main/java/dev/xpple/seedmapper/seedmap/FeatureToggleWidget.java index 91b0509..ced23eb 100644 --- a/src/main/java/dev/xpple/seedmapper/seedmap/FeatureToggleWidget.java +++ b/src/main/java/dev/xpple/seedmapper/seedmap/FeatureToggleWidget.java @@ -14,7 +14,7 @@ public class FeatureToggleWidget extends Button { public FeatureToggleWidget(MapFeature feature, int x, int y) { super(x, y, feature.getDefaultTexture().width(), feature.getDefaultTexture().height(), Component.literal(feature.getName()), FeatureToggleWidget::onButtonPress, DEFAULT_NARRATION); this.feature = feature; - this.setTooltip(Tooltip.create(Component.literal(this.feature.getName()))); + this.setTooltip(Tooltip.create(this.message)); } @Override diff --git a/src/main/java/dev/xpple/seedmapper/seedmap/ItemIconButton.java b/src/main/java/dev/xpple/seedmapper/seedmap/ItemIconButton.java new file mode 100644 index 0000000..5f6863a --- /dev/null +++ b/src/main/java/dev/xpple/seedmapper/seedmap/ItemIconButton.java @@ -0,0 +1,25 @@ +package dev.xpple.seedmapper.seedmap; + +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.ItemStack; + +public class ItemIconButton extends Button { + + public static final int ICON_SIZE = 16; + + private final ItemStack item; + + protected ItemIconButton(int x, int y, ItemStack item, Component message, OnPress onPress) { + super(x, y, ICON_SIZE, ICON_SIZE, message, onPress, DEFAULT_NARRATION); + this.item = item; + this.setTooltip(Tooltip.create(message)); + } + + @Override + protected void renderContents(GuiGraphics guiGraphics, int i, int j, float f) { + guiGraphics.renderItem(this.item, this.getX(), this.getY()); + } +} diff --git a/src/main/java/dev/xpple/seedmapper/seedmap/LootSearchScreen.java b/src/main/java/dev/xpple/seedmapper/seedmap/LootSearchScreen.java new file mode 100644 index 0000000..746d8ae --- /dev/null +++ b/src/main/java/dev/xpple/seedmapper/seedmap/LootSearchScreen.java @@ -0,0 +1,689 @@ +package dev.xpple.seedmapper.seedmap; + +import com.github.cubiomes.Cubiomes; +import com.github.cubiomes.Generator; +import com.github.cubiomes.ItemStack; +import com.github.cubiomes.LootTableContext; +import com.github.cubiomes.Piece; +import com.github.cubiomes.Pos; +import com.github.cubiomes.StructureConfig; +import com.github.cubiomes.StructureSaltConfig; +import com.github.cubiomes.StructureVariant; +import com.github.cubiomes.SurfaceNoise; +import dev.xpple.seedmapper.command.commands.LocateCommand; +import dev.xpple.seedmapper.feature.StructureChecks; +import dev.xpple.seedmapper.thread.SeedMapExecutor; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent; +import net.minecraft.client.gui.screens.inventory.tooltip.DefaultTooltipPositioner; +import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.util.Mth; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Stream; + +import static dev.xpple.seedmapper.util.ChatBuilder.*; + +public class LootSearchScreen extends Screen { + + private static final int FIELD_WIDTH = 120; + private static final int FIELD_HEIGHT = 20; + private static final int BUTTON_WIDTH = 80; + private static final int BUTTON_HEIGHT = 20; + private static final int ROW_SPACING = 6; + private static final int TAB_HEIGHT = 20; + private static final int TAB_WIDTH = 80; + private static final int LIST_ROW_HEIGHT = 12; + private static final int LIST_PADDING = 8; + private static final int RESULTS_TOP = 44; + private static final int COLUMN_GAP = 6; + private static final int STRUCTURE_ICON_SIZE = 16; + private static final int STRUCTURE_ICON_GAP = 4; + private static final int STRUCTURE_BUTTON_ROWS = 2; + + private final SeedMapScreen parent; + private final SeedMapExecutor executor = new SeedMapExecutor(); + + private enum Tab { + SEARCH, + RESULTS + } + + private enum SortMode { + COUNT, + DISTANCE + } + + private Tab activeTab = Tab.SEARCH; + private SortMode structureSortMode = SortMode.COUNT; + + private @Nullable EditBox radiusEditBox; + private @Nullable Button searchButton; + private @Nullable Button searchTabButton; + private @Nullable Button resultsTabButton; + private @Nullable Button sortToggleButton; + private @Nullable EditBox resultsSearchEditBox; + private final List structureToggleButtons = new ArrayList<>(); + private final IntSet enabledStructureIds = new IntOpenHashSet(); + private final List lootFeatures; + + private Component status = Component.empty(); + private boolean searching = false; + private List itemResults = new ArrayList<>(); + private int resultsScroll = 0; + private @Nullable ItemResult selectedItem = null; + private int structureScroll = 0; + + public LootSearchScreen(SeedMapScreen parent) { + super(Component.translatable("seedMap.lootSearch.title")); + this.parent = parent; + this.lootFeatures = Stream.of(MapFeature.values()) + .filter(feature -> LocateCommand.LOOT_SUPPORTED_STRUCTURES.contains(feature.getStructureId())) + .filter(feature -> feature.getDimension() == this.parent.getDimension()) + .toList(); + this.lootFeatures.forEach(feature -> this.enabledStructureIds.add(feature.getStructureId())); + } + + @Override + protected void init() { + super.init(); + + int centerX = this.width / 2; + int tabsY = 12; + this.searchTabButton = Button.builder(Component.translatable("seedMap.lootSearch.tab.search"), button -> this.setActiveTab(Tab.SEARCH)) + .bounds(centerX - TAB_WIDTH - 4, tabsY, TAB_WIDTH, TAB_HEIGHT) + .build(); + this.resultsTabButton = Button.builder(Component.translatable("seedMap.lootSearch.tab.results"), button -> this.setActiveTab(Tab.RESULTS)) + .bounds(centerX + 4, tabsY, TAB_WIDTH, TAB_HEIGHT) + .build(); + this.resultsTabButton.active = false; + this.addRenderableWidget(this.searchTabButton); + this.addRenderableWidget(this.resultsTabButton); + + int startY = this.height / 2 - FIELD_HEIGHT - ROW_SPACING; + + this.radiusEditBox = new EditBox(this.font, centerX - FIELD_WIDTH / 2, startY, FIELD_WIDTH, FIELD_HEIGHT, Component.translatable("seedMap.lootSearch.radius")); + this.radiusEditBox.setHint(Component.translatable("seedMap.lootSearch.radiusHint")); + this.radiusEditBox.setMaxLength(7); + this.radiusEditBox.setValue("2000"); + this.addRenderableWidget(this.radiusEditBox); + + this.searchButton = Button.builder(Component.translatable("seedMap.lootSearch.search"), button -> this.startSearch()) + .bounds(centerX - BUTTON_WIDTH / 2, startY + FIELD_HEIGHT + ROW_SPACING, BUTTON_WIDTH, BUTTON_HEIGHT) + .build(); + this.addRenderableWidget(this.searchButton); + + int iconsTop = startY + FIELD_HEIGHT + ROW_SPACING + BUTTON_HEIGHT + ROW_SPACING; + int columns = (int) Math.ceil(this.lootFeatures.size() / (double) STRUCTURE_BUTTON_ROWS); + int totalWidth = columns * STRUCTURE_ICON_SIZE + (columns - 1) * STRUCTURE_ICON_GAP; + int iconStartX = centerX - totalWidth / 2; + for (int i = 0; i < this.lootFeatures.size(); i++) { + int row = i / columns; + int col = i % columns; + int x = iconStartX + col * (STRUCTURE_ICON_SIZE + STRUCTURE_ICON_GAP); + int y = iconsTop + row * (STRUCTURE_ICON_SIZE + STRUCTURE_ICON_GAP); + MapFeature feature = this.lootFeatures.get(i); + StructureToggleButton button = new StructureToggleButton(x, y, feature); + this.structureToggleButtons.add(button); + this.addRenderableWidget(button); + } + + int detailsLeft = this.width / 2 + LIST_PADDING; + int sortButtonY = this.height - LIST_PADDING - TAB_HEIGHT; + this.sortToggleButton = Button.builder(this.sortButtonLabel(), button -> this.toggleSortMode()) + .bounds(detailsLeft, sortButtonY, TAB_WIDTH + 20, TAB_HEIGHT) + .build(); + this.addRenderableWidget(this.sortToggleButton); + + int listLeft = LIST_PADDING; + int listWidth = this.width / 2 - LIST_PADDING * 2; + int searchY = this.height - LIST_PADDING - FIELD_HEIGHT; + this.resultsSearchEditBox = new EditBox(this.font, listLeft, searchY, listWidth, FIELD_HEIGHT, Component.translatable("seedMap.lootSearch.filter")); + this.resultsSearchEditBox.setHint(Component.translatable("seedMap.lootSearch.filterHint")); + this.addRenderableWidget(this.resultsSearchEditBox); + + this.updateTabVisibility(); + } + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + super.render(guiGraphics, mouseX, mouseY, partialTick); + if (this.activeTab == Tab.SEARCH) { + this.renderSearchTab(guiGraphics); + } else { + this.renderResultsTab(guiGraphics, mouseX, mouseY); + } + } + + private void renderSearchTab(GuiGraphics guiGraphics) { + guiGraphics.drawCenteredString(this.font, this.getTitle(), this.width / 2, this.height / 2 - FIELD_HEIGHT - ROW_SPACING - this.font.lineHeight - 4, 0xFF_FFFFFF); + if (!this.status.getString().isEmpty()) { + guiGraphics.drawCenteredString(this.font, this.status, this.width / 2, this.height / 2 + FIELD_HEIGHT + BUTTON_HEIGHT + ROW_SPACING * 2, 0xFF_FFFFFF); + } + } + + private void renderResultsTab(GuiGraphics guiGraphics, int mouseX, int mouseY) { + int listLeft = LIST_PADDING; + int listTop = RESULTS_TOP; + int listBottom = this.height - LIST_PADDING - FIELD_HEIGHT - ROW_SPACING; + int listHeight = Math.max(0, listBottom - listTop); + + guiGraphics.drawString(this.font, Component.translatable("seedMap.lootSearch.results"), listLeft, listTop - this.font.lineHeight - 4, 0xFF_FFFFFF); + + List filteredResults = this.getFilteredResults(); + if (filteredResults.isEmpty()) { + guiGraphics.drawString(this.font, Component.translatable("seedMap.lootSearch.noResults"), listLeft, listTop, 0xFF_A0A0A0); + return; + } + + int visibleRows = Math.max(1, listHeight / LIST_ROW_HEIGHT); + int maxScroll = Math.max(0, filteredResults.size() - visibleRows); + this.resultsScroll = Mth.clamp(this.resultsScroll, 0, maxScroll); + + int startIndex = this.resultsScroll; + int endIndex = Math.min(filteredResults.size(), startIndex + visibleRows); + int y = listTop; + for (int i = startIndex; i < endIndex; i++) { + ItemResult result = filteredResults.get(i); + int color = Objects.equals(this.selectedItem, result) ? 0xFF_FFFFFF : 0xFF_C0C0C0; + Component line = Component.literal("%s x%d".formatted(result.displayName(this.parent.getVersion()), result.totalCount)); + guiGraphics.drawString(this.font, line, listLeft, y, color); + y += LIST_ROW_HEIGHT; + } + + int gutterShift = Math.min(24, this.width / 20); + int detailsLeft = this.width / 2 + LIST_PADDING - gutterShift; + int detailsTop = listTop; + if (this.selectedItem == null) { + guiGraphics.drawString(this.font, Component.translatable("seedMap.lootSearch.selectItem"), detailsLeft, detailsTop, 0xFF_A0A0A0); + return; + } + + guiGraphics.drawString(this.font, Component.literal(this.selectedItem.displayName(this.parent.getVersion())), detailsLeft, detailsTop, 0xFF_FFFFFF); + guiGraphics.drawString(this.font, Component.translatable("seedMap.lootSearch.total", this.selectedItem.totalCount), detailsLeft, detailsTop + LIST_ROW_HEIGHT, 0xFF_C0C0C0); + + int structureListTop = detailsTop + LIST_ROW_HEIGHT * 3; + int structureListHeight = this.height - structureListTop - LIST_PADDING - TAB_HEIGHT - ROW_SPACING; + + List structures = this.selectedItem.sortedStructures(this.structureSortMode, this.parent.getPlayerPos()); + if (structures.isEmpty()) { + guiGraphics.drawString(this.font, Component.translatable("seedMap.lootSearch.noStructures"), detailsLeft, structureListTop, 0xFF_A0A0A0); + return; + } + + int detailsRight = this.width - LIST_PADDING; + int detailsWidth = detailsRight - detailsLeft; + int countWidth = this.font.width("999999"); + int maxCoordWidth = this.font.width("-30000000"); + int minCoordWidth = this.font.width("0"); + int iconSize = 12; + int remainingForCoords = detailsWidth - countWidth - iconSize - COLUMN_GAP * 3; + int coordWidth = Mth.clamp(remainingForCoords / 2, minCoordWidth, maxCoordWidth); + + int xColX = detailsLeft; + int zColX = xColX + coordWidth + COLUMN_GAP; + int countColX = zColX + coordWidth + COLUMN_GAP; + int iconColX = countColX + countWidth + COLUMN_GAP; + + guiGraphics.drawString(this.font, Component.translatable("seedMap.lootSearch.column.x"), xColX, structureListTop - LIST_ROW_HEIGHT, 0xFF_C0C0C0); + guiGraphics.drawString(this.font, Component.translatable("seedMap.lootSearch.column.z"), zColX, structureListTop - LIST_ROW_HEIGHT, 0xFF_C0C0C0); + guiGraphics.drawString(this.font, Component.translatable("seedMap.lootSearch.column.count"), countColX, structureListTop - LIST_ROW_HEIGHT, 0xFF_C0C0C0); + guiGraphics.drawString(this.font, Component.translatable("seedMap.lootSearch.column.type"), iconColX, structureListTop - LIST_ROW_HEIGHT, 0xFF_C0C0C0); + + int structureVisibleRows = Math.max(1, structureListHeight / LIST_ROW_HEIGHT); + int structureMaxScroll = Math.max(0, structures.size() - structureVisibleRows); + this.structureScroll = Mth.clamp(this.structureScroll, 0, structureMaxScroll); + int structureStartIndex = this.structureScroll; + int structureEndIndex = Math.min(structures.size(), structureStartIndex + structureVisibleRows); + Component hoverTooltip = null; + int structureY = structureListTop; + for (int i = structureStartIndex; i < structureEndIndex; i++) { + StructureEntry entry = structures.get(i); + guiGraphics.drawString(this.font, Component.literal(Integer.toString(entry.pos.getX())), xColX, structureY, 0xFF_FFFFFF); + guiGraphics.drawString(this.font, Component.literal(Integer.toString(entry.pos.getZ())), zColX, structureY, 0xFF_FFFFFF); + guiGraphics.drawString(this.font, Component.literal(Integer.toString(entry.count)), countColX, structureY, 0xFF_FFFFFF); + MapFeature.Texture texture = this.getStructureTexture(entry.structureId); + int iconX = iconColX; + int iconY = structureY + (LIST_ROW_HEIGHT - iconSize) / 2; + if (texture != null) { + SeedMapScreen.drawIconStatic(guiGraphics, texture.identifier(), iconX, iconY, iconSize, iconSize, 0xFF_FFFFFF); + } else { + guiGraphics.drawString(this.font, Component.literal("?"), iconX, structureY, 0xFF_FFFFFF); + } + if (mouseX >= iconX && mouseX <= iconX + iconSize && mouseY >= iconY && mouseY <= iconY + iconSize) { + hoverTooltip = Component.literal(Cubiomes.struct2str(entry.structureId).getString(0)); + } + structureY += LIST_ROW_HEIGHT; + } + if (hoverTooltip != null) { + guiGraphics.renderTooltip(this.font, List.of(ClientTooltipComponent.create(hoverTooltip.getVisualOrderText())), mouseX, mouseY, DefaultTooltipPositioner.INSTANCE, null); + } + + } + + private void startSearch() { + if (this.searching || this.radiusEditBox == null) { + return; + } + int radius; + try { + radius = Integer.parseInt(this.radiusEditBox.getValue()); + } catch (NumberFormatException _) { + this.status = error(Component.translatable("seedMap.lootSearch.error.invalidRadius").getString()); + return; + } + if (radius <= 0 || radius > Level.MAX_LEVEL_SIZE) { + this.status = error(Component.translatable("seedMap.lootSearch.error.radiusRange").getString()); + return; + } + + this.searching = true; + this.status = base(Component.translatable("seedMap.lootSearch.searching").getString()); + if (this.searchButton != null) { + this.searchButton.active = false; + } + if (this.resultsTabButton != null) { + this.resultsTabButton.active = false; + } + + int clampedRadius = radius; + this.executor.submitCalculation(() -> this.searchLoot(clampedRadius)) + .thenAccept(result -> { + Minecraft.getInstance().schedule(() -> { + if (result == null) { + this.status = error(Component.translatable("seedMap.lootSearch.error.searchFailed").getString()); + this.itemResults = new ArrayList<>(); + this.selectedItem = null; + } else if (result.items.isEmpty()) { + this.status = warn(Component.translatable("seedMap.lootSearch.noLoot").getString()); + this.itemResults = new ArrayList<>(); + this.selectedItem = null; + } else { + this.status = base(Component.translatable("seedMap.lootSearch.foundTypes", result.items.size()).getString()); + this.itemResults = result.items.values().stream() + .sorted((a, b) -> Integer.compare(b.totalCount, a.totalCount)) + .toList(); + this.resultsScroll = 0; + this.structureScroll = 0; + this.selectedItem = this.itemResults.getFirst(); + if (this.resultsTabButton != null) { + this.resultsTabButton.active = true; + } + this.setActiveTab(Tab.RESULTS); + } + this.searching = false; + if (this.searchButton != null) { + this.searchButton.active = true; + } + if (this.resultsTabButton != null && !this.itemResults.isEmpty()) { + this.resultsTabButton.active = true; + } + }); + }); + } + + private @Nullable SearchResults searchLoot(int radiusBlocks) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment generator = Generator.allocate(arena); + Cubiomes.setupGenerator(generator, this.parent.getVersion(), this.parent.getGeneratorFlags()); + Cubiomes.applySeed(generator, this.parent.getDimension(), this.parent.getSeed()); + + MemorySegment surfaceNoise = SurfaceNoise.allocate(arena); + Cubiomes.initSurfaceNoise(surfaceNoise, this.parent.getDimension(), this.parent.getSeed()); + + SearchResults results = new SearchResults(); + MemorySegment structurePos = Pos.allocate(arena); + MemorySegment pieces = Piece.allocateArray(StructureChecks.MAX_END_CITY_AND_FORTRESS_PIECES, arena); + MemorySegment structureVariant = StructureVariant.allocate(arena); + MemorySegment structureSaltConfig = StructureSaltConfig.allocate(arena); + MemorySegment ltcPtr = arena.allocate(Cubiomes.C_POINTER); + + int centerX = this.parent.getPlayerPos().getX(); + int centerZ = this.parent.getPlayerPos().getZ(); + int radiusSq = radiusBlocks * radiusBlocks; + + for (int structure : LocateCommand.LOOT_SUPPORTED_STRUCTURES) { + if (!this.enabledStructureIds.contains(structure)) { + continue; + } + MemorySegment structureConfig = StructureConfig.allocate(arena); + if (Cubiomes.getStructureConfig(structure, this.parent.getVersion(), structureConfig) == 0) { + continue; + } + if (StructureConfig.dim(structureConfig) != this.parent.getDimension()) { + continue; + } + int regionSizeBlocks = StructureConfig.regionSize(structureConfig) << 4; + int minRegionX = Mth.floor((centerX - radiusBlocks) / (float) regionSizeBlocks); + int maxRegionX = Mth.floor((centerX + radiusBlocks) / (float) regionSizeBlocks); + int minRegionZ = Mth.floor((centerZ - radiusBlocks) / (float) regionSizeBlocks); + int maxRegionZ = Mth.floor((centerZ + radiusBlocks) / (float) regionSizeBlocks); + + StructureChecks.GenerationCheck generationCheck = StructureChecks.getGenerationCheck(structure); + for (int regionX = minRegionX; regionX <= maxRegionX; regionX++) { + for (int regionZ = minRegionZ; regionZ <= maxRegionZ; regionZ++) { + if (Cubiomes.getStructurePos(structure, this.parent.getVersion(), this.parent.getSeed(), regionX, regionZ, structurePos) == 0) { + continue; + } + int posX = Pos.x(structurePos); + int posZ = Pos.z(structurePos); + int dx = posX - centerX; + int dz = posZ - centerZ; + if (dx * dx + dz * dz > radiusSq) { + continue; + } + if (!generationCheck.check(generator, surfaceNoise, regionX, regionZ, structurePos)) { + continue; + } + int biome = Cubiomes.getBiomeAt(generator, 4, posX >> 2, 320 >> 2, posZ >> 2); + Cubiomes.getVariant(structureVariant, structure, this.parent.getVersion(), this.parent.getSeed(), posX, posZ, biome); + biome = StructureVariant.biome(structureVariant) != -1 ? StructureVariant.biome(structureVariant) : biome; + if (Cubiomes.getStructureSaltConfig(structure, this.parent.getVersion(), biome, structureSaltConfig) == 0) { + continue; + } + int numPieces = Cubiomes.getStructurePieces(pieces, StructureChecks.MAX_END_CITY_AND_FORTRESS_PIECES, structure, structureSaltConfig, structureVariant, this.parent.getVersion(), this.parent.getSeed(), posX, posZ); + if (numPieces <= 0) { + continue; + } + for (int pieceIdx = 0; pieceIdx < numPieces; pieceIdx++) { + MemorySegment piece = Piece.asSlice(pieces, pieceIdx); + int chestCount = Piece.chestCount(piece); + if (chestCount <= 0) { + continue; + } + MemorySegment lootTables = Piece.lootTables(piece); + MemorySegment lootSeeds = Piece.lootSeeds(piece); + for (int chestIdx = 0; chestIdx < chestCount; chestIdx++) { + MemorySegment lootTable = lootTables.getAtIndex(ValueLayout.ADDRESS, chestIdx).reinterpret(Long.MAX_VALUE); + if (Cubiomes.init_loot_table_name(ltcPtr, lootTable, this.parent.getVersion()) == 0) { + continue; + } + MemorySegment lootTableContext = ltcPtr.get(ValueLayout.ADDRESS, 0).reinterpret(LootTableContext.sizeof()); + Cubiomes.set_loot_seed(lootTableContext, lootSeeds.getAtIndex(Cubiomes.C_LONG_LONG, chestIdx)); + Cubiomes.generate_loot(lootTableContext); + int lootCount = LootTableContext.generated_item_count(lootTableContext); + for (int lootIdx = 0; lootIdx < lootCount; lootIdx++) { + MemorySegment itemStackInternal = ItemStack.asSlice(LootTableContext.generated_items(lootTableContext), lootIdx); + int itemId = Cubiomes.get_global_item_id(lootTableContext, ItemStack.item(itemStackInternal)); + int count = ItemStack.count(itemStackInternal); + results.addItem(itemId, count, new BlockPos(posX, 0, posZ), structure); + } + } + } + } + } + } + return results; + } catch (Throwable t) { + return null; + } + } + + @Override + public void onClose() { + super.onClose(); + this.executor.close(() -> {}); + this.minecraft.setScreen(this.parent); + } + + @Override + public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { + if (super.mouseClicked(event, isDoubleClick)) { + return true; + } + if (this.activeTab != Tab.RESULTS || isDoubleClick) { + return false; + } + int mouseX = (int)event.x(); + int mouseY = (int)event.y(); + int listLeft = LIST_PADDING; + int listTop = RESULTS_TOP; + int listWidth = this.width / 2 - LIST_PADDING * 2; + int listBottom = this.height - LIST_PADDING - FIELD_HEIGHT - ROW_SPACING; + int listRight = listLeft + listWidth; + //int listBottom = listTop + listHeight; + if (mouseX < listLeft || mouseX > listRight || mouseY < listTop || mouseY > listBottom) { + return false; + } + List filteredResults = this.getFilteredResults(); + int index = this.resultsScroll + (mouseY - listTop) / LIST_ROW_HEIGHT; + if (index >= 0 && index < filteredResults.size()) { + this.selectedItem = filteredResults.get(index); + this.structureScroll = 0; + return true; + } + return false; + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { + if (this.activeTab != Tab.RESULTS || this.itemResults.isEmpty()) { + return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY); + } + int listLeft = LIST_PADDING; + int listTop = RESULTS_TOP; + int listWidth = this.width / 2 - LIST_PADDING * 2; + int listBottom = this.height - LIST_PADDING - FIELD_HEIGHT - ROW_SPACING; + int listHeight = Math.max(0, listBottom - listTop); + int listRight = listLeft + listWidth; + //int listBottom = listTop + listHeight; + int delta = scrollY > 0 ? -1 : 1; + + if (mouseX >= listLeft && mouseX <= listRight && mouseY >= listTop && mouseY <= listBottom) { + int visibleRows = Math.max(1, listHeight / LIST_ROW_HEIGHT); + int maxScroll = Math.max(0, this.getFilteredResults().size() - visibleRows); + this.resultsScroll = Mth.clamp(this.resultsScroll + delta, 0, maxScroll); + return true; + } + + int gutterShift = Math.min(24, this.width / 20); + int detailsLeft = this.width / 2 + LIST_PADDING - gutterShift; + int structureListTop = listTop + LIST_ROW_HEIGHT * 3; + int structureListHeight = this.height - structureListTop - LIST_PADDING - TAB_HEIGHT - ROW_SPACING; + int structureListRight = this.width - LIST_PADDING; + int structureListBottom = structureListTop + structureListHeight; + if (mouseX >= detailsLeft && mouseX <= structureListRight && mouseY >= structureListTop && mouseY <= structureListBottom && this.selectedItem != null) { + List structures = this.selectedItem.sortedStructures(this.structureSortMode, this.parent.getPlayerPos()); + int visibleRows = Math.max(1, structureListHeight / LIST_ROW_HEIGHT); + int maxScroll = Math.max(0, structures.size() - visibleRows); + this.structureScroll = Mth.clamp(this.structureScroll + delta, 0, maxScroll); + return true; + } + + return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY); + } + + private void setActiveTab(Tab tab) { + this.activeTab = tab; + this.updateTabVisibility(); + } + + private void updateTabVisibility() { + boolean searchVisible = this.activeTab == Tab.SEARCH; + if (this.radiusEditBox != null) { + this.radiusEditBox.visible = searchVisible; + } + if (this.searchButton != null) { + this.searchButton.visible = searchVisible; + } + if (this.searchTabButton != null) { + this.searchTabButton.active = this.activeTab != Tab.SEARCH; + } + if (this.resultsTabButton != null) { + boolean hasResults = !this.itemResults.isEmpty(); + this.resultsTabButton.active = this.activeTab != Tab.RESULTS && hasResults; + } + if (this.sortToggleButton != null) { + this.sortToggleButton.visible = this.activeTab == Tab.RESULTS; + this.sortToggleButton.active = this.activeTab == Tab.RESULTS && !this.itemResults.isEmpty(); + this.sortToggleButton.setMessage(this.sortButtonLabel()); + } + if (this.resultsSearchEditBox != null) { + this.resultsSearchEditBox.visible = this.activeTab == Tab.RESULTS; + this.resultsSearchEditBox.setValue(this.resultsSearchEditBox.getValue()); + } + boolean showSearchControls = this.activeTab == Tab.SEARCH; + for (StructureToggleButton button : this.structureToggleButtons) { + button.visible = showSearchControls; + button.active = showSearchControls; + } + } + + private static final class SearchResults { + private final Map items = new HashMap<>(); + + void addItem(int itemId, int count, BlockPos pos, int structureId) { + ItemResult result = this.items.computeIfAbsent(itemId, ItemResult::new); + result.totalCount += count; + result.addPosition(pos); + result.addStructureEntry(structureId, pos, count); + } + } + + private static final class ItemResult { + private final int itemId; + private int totalCount = 0; + private final List positions = new ArrayList<>(); + private final Map structureEntries = new HashMap<>(); + + private ItemResult(int itemId) { + this.itemId = itemId; + } + + private void addPosition(BlockPos pos) { + if (this.positions.isEmpty() || !this.positions.getLast().equals(pos)) { + this.positions.add(pos); + } + } + + private void addStructureEntry(int structureId, BlockPos pos, int count) { + StructureKey key = new StructureKey(structureId, pos); + StructureEntry entry = this.structureEntries.computeIfAbsent(key, StructureEntry::new); + entry.count += count; + } + + private List sortedStructures(SortMode sortMode, BlockPos playerPos) { + return this.structureEntries.values().stream() + .sorted((a, b) -> { + if (sortMode == SortMode.COUNT) { + int byCount = Integer.compare(b.count, a.count); + if (byCount != 0) { + return byCount; + } + return Long.compare(a.distanceSq(playerPos), b.distanceSq(playerPos)); + } + int byDistance = Long.compare(a.distanceSq(playerPos), b.distanceSq(playerPos)); + if (byDistance != 0) { + return byDistance; + } + return Integer.compare(b.count, a.count); + }) + .toList(); + } + + private String displayName(int version) { + String name = Cubiomes.global_id2item_name(this.itemId, version).getString(0); + return name.contains(":") ? name.substring(name.indexOf(':') + 1) : name; + } + } + + private void toggleSortMode() { + this.structureSortMode = this.structureSortMode == SortMode.COUNT ? SortMode.DISTANCE : SortMode.COUNT; + this.structureScroll = 0; + if (this.sortToggleButton != null) { + this.sortToggleButton.setMessage(this.sortButtonLabel()); + } + } + + private Component sortButtonLabel() { + return Component.translatable("seedMap.lootSearch.sort", Component.translatable(this.structureSortMode == SortMode.COUNT + ? "seedMap.lootSearch.sort.count" + : "seedMap.lootSearch.sort.distance")); + } + + private @Nullable MapFeature.Texture getStructureTexture(int structureId) { + for (MapFeature feature : MapFeature.values()) { + if (feature.getStructureId() == structureId) { + return feature.getDefaultTexture(); + } + } + return null; + } + + private List getFilteredResults() { + if (this.resultsSearchEditBox == null) { + return this.itemResults; + } + String query = this.resultsSearchEditBox.getValue().trim().toLowerCase(); + if (query.isEmpty()) { + return this.itemResults; + } + return this.itemResults.stream() + .filter(result -> result.displayName(this.parent.getVersion()).toLowerCase().contains(query)) + .toList(); + } + + private record StructureKey(int structureId, BlockPos pos) {} + + private static final class StructureEntry { + private final int structureId; + private final BlockPos pos; + private int count = 0; + + private StructureEntry(StructureKey key) { + this.structureId = key.structureId(); + this.pos = key.pos(); + } + + private long distanceSq(BlockPos playerPos) { + long dx = (long) this.pos.getX() - playerPos.getX(); + long dz = (long) this.pos.getZ() - playerPos.getZ(); + return dx * dx + dz * dz; + } + } + + private final class StructureToggleButton extends Button { + private final MapFeature feature; + + private StructureToggleButton(int x, int y, MapFeature feature) { + super(x, y, STRUCTURE_ICON_SIZE, STRUCTURE_ICON_SIZE, Component.literal(feature.getName()), (Button button) -> { + if (!(button instanceof StructureToggleButton toggle)) return; + toggle.onPress(); + }, DEFAULT_NARRATION); + this.feature = feature; + this.setTooltip(Tooltip.create(Component.literal(this.feature.getName()))); + } + + private void onPress() { + int structureId = this.feature.getStructureId(); + if (!enabledStructureIds.remove(structureId)) { + enabledStructureIds.add(structureId); + } + } + + @Override + protected void renderContents(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + int color = enabledStructureIds.contains(this.feature.getStructureId()) ? 0xFF_FFFFFF : 0x80_FFFFFF; + MapFeature.Texture texture = this.feature.getDefaultTexture(); + SeedMapScreen.drawIconStatic(guiGraphics, texture.identifier(), this.getX(), this.getY(), this.getWidth(), this.getHeight(), color); + } + } +} diff --git a/src/main/java/dev/xpple/seedmapper/seedmap/MinimapScreen.java b/src/main/java/dev/xpple/seedmapper/seedmap/MinimapScreen.java index 7dc4ef3..89250d9 100644 --- a/src/main/java/dev/xpple/seedmapper/seedmap/MinimapScreen.java +++ b/src/main/java/dev/xpple/seedmapper/seedmap/MinimapScreen.java @@ -42,18 +42,18 @@ public void renderToHud(GuiGraphics guiGraphics, float partialTick) { renderContentHeight = diagonal; } // ensures super.seedMapWidth == renderContentWidth - int renderWidth = renderContentWidth + 2 * this.horizontalPadding(); + int renderWidth = renderContentWidth + 2 * this.leftPadding(); // ensures super.seedMapHeight == renderContentHeight - int renderHeight = renderContentHeight + 2 * this.verticalPadding(); + int renderHeight = renderContentHeight + 2 * this.topPadding(); this.initForOverlay(renderWidth, renderHeight); - guiGraphics.enableScissor(this.horizontalPadding(), this.verticalPadding(), this.horizontalPadding() + contentWidth, this.verticalPadding() + contentHeight); + guiGraphics.enableScissor(this.leftPadding(), this.topPadding(), this.leftPadding() + contentWidth, this.topPadding() + contentHeight); var pose = guiGraphics.pose(); pose.pushMatrix(); if (rotateMinimap) { - pose.translate(-this.centerX + (float) (this.horizontalPadding() + contentWidth / 2), -this.centerY + (float) (this.verticalPadding() + contentHeight / 2)); + pose.translate(-this.centerX + (float) (this.leftPadding() + contentWidth / 2), -this.centerY + (float) (this.topPadding() + contentHeight / 2)); pose.translate(this.centerX, this.centerY); pose.rotate((float) (-Math.toRadians(this.getPlayerRotation().y) + Math.PI)); pose.translate(-this.centerX, -this.centerY); @@ -64,7 +64,7 @@ public void renderToHud(GuiGraphics guiGraphics, float partialTick) { pose.popMatrix(); if (Configs.RotateMinimap) { - this.drawCenterCross(guiGraphics, this.horizontalPadding() + contentWidth / 2, this.verticalPadding() + contentHeight / 2); + this.drawCenterCross(guiGraphics, this.leftPadding() + contentWidth / 2, this.topPadding() + contentHeight / 2); } guiGraphics.disableScissor(); @@ -100,12 +100,12 @@ protected boolean isMinimap() { } @Override - protected int horizontalPadding() { + protected int leftPadding() { return Configs.MinimapOffsetX; } @Override - protected int verticalPadding() { + protected int topPadding() { return Configs.MinimapOffsetY; } } diff --git a/src/main/java/dev/xpple/seedmapper/seedmap/SeedMapScreen.java b/src/main/java/dev/xpple/seedmapper/seedmap/SeedMapScreen.java index 9a65ad4..1b2c512 100644 --- a/src/main/java/dev/xpple/seedmapper/seedmap/SeedMapScreen.java +++ b/src/main/java/dev/xpple/seedmapper/seedmap/SeedMapScreen.java @@ -43,9 +43,8 @@ import it.unimi.dsi.fastutil.objects.Object2ObjectMap; import it.unimi.dsi.fastutil.objects.Object2ObjectMaps; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.objects.ObjectIterator; -import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; -import it.unimi.dsi.fastutil.objects.ObjectSet; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import it.unimi.dsi.fastutil.objects.ObjectList; import net.minecraft.SharedConstants; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; @@ -76,6 +75,7 @@ import net.minecraft.util.RandomSource; import net.minecraft.world.SimpleContainer; import net.minecraft.world.item.Item; +import net.minecraft.world.item.Items; import net.minecraft.world.item.alchemy.PotionContents; import net.minecraft.world.item.component.ItemLore; import net.minecraft.world.item.component.SuspiciousStewEffects; @@ -105,7 +105,6 @@ import java.util.Objects; import java.util.Optional; import java.util.OptionalInt; -import java.util.function.IntSupplier; import java.util.function.ToIntBiFunction; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -151,20 +150,22 @@ public class SeedMapScreen extends Screen { public static final int BIOME_SCALE = 4; public static final int SCALED_CHUNK_SIZE = LevelChunkSection.SECTION_WIDTH / BIOME_SCALE; - private static final int HORIZONTAL_PADDING = 50; - private static final int VERTICAL_PADDING = 50; + private static final int TOP_PADDING = 20; + private static final int BOTTOM_PADDING = 20; public static final int MIN_PIXELS_PER_BIOME = 1; public static final int MAX_PIXELS_PER_BIOME = 100; - private static final int HORIZONTAL_FEATURE_TOGGLE_SPACING = 5; + private static final int HORIZONTAL_FEATURE_TOGGLE_SPACING = 1; private static final int VERTICAL_FEATURE_TOGGLE_SPACING = 1; - private static final int FEATURE_TOGGLE_HEIGHT = 20; - private static final int TELEPORT_FIELD_WIDTH = 70; - private static final int WAYPOINT_NAME_FIELD_WIDTH = 100; + private static final net.minecraft.world.item.ItemStack LOOT_SEARCH_ICON_ITEM = Items.CHEST.getDefaultInstance(); - private static final IntSupplier TILE_SIZE_PIXELS = () -> TilePos.TILE_SIZE_CHUNKS * SCALED_CHUNK_SIZE * Configs.PixelsPerBiome; + private static int tileSizePixels() { + return TilePos.TILE_SIZE_CHUNKS * SCALED_CHUNK_SIZE * Configs.PixelsPerBiome; + } + + private static final double FEATURE_TOGGLE_LOWER_PADDING_FACTOR = 0.95; private static final Object2ObjectMap> biomeDataCache = new Object2ObjectOpenHashMap<>(); private static final Object2ObjectMap> structureDataCache = new Object2ObjectOpenHashMap<>(); @@ -213,13 +214,17 @@ public class SeedMapScreen extends Screen { protected int centerX; protected int centerY; + private int leftPadding; private int seedMapWidth; private int seedMapHeight; + /// A list of all features that can have their appearance on the map toggled on or off. private final List toggleableFeatures; - private final int featureIconsCombinedWidth; + /// The combined height of the icons of _all_ toggleable features. Includes padding. + private final int featureIconsCombinedHeight; - private final ObjectSet featureWidgets = new ObjectOpenHashSet<>(); + /// A list of all features currently on rendered screen. Used for click detection. + private final ObjectList featureWidgets = new ObjectArrayList<>(); private QuartPos2 mouseQuart; @@ -304,9 +309,9 @@ public SeedMapScreen(long seed, int dimension, int version, int generatorFlags, }); } - this.featureIconsCombinedWidth = this.toggleableFeatures.stream() - .map(feature -> feature.getDefaultTexture().width()) - .reduce((l, r) -> l + HORIZONTAL_FEATURE_TOGGLE_SPACING + r) + this.featureIconsCombinedHeight = this.toggleableFeatures.stream() + .map(feature -> feature.getDefaultTexture().height()) + .reduce((l, r) -> l + VERTICAL_FEATURE_TOGGLE_SPACING + r) .orElseThrow(); this.playerPos = playerPos; @@ -323,42 +328,56 @@ protected void init() { this.centerX = this.width / 2; this.centerY = this.height / 2; - this.seedMapWidth = 2 * (this.centerX - this.horizontalPadding()); - this.seedMapHeight = 2 * (this.centerY - this.verticalPadding()); + this.seedMapHeight = this.height - this.topPadding() - BOTTOM_PADDING - 1; + if (!this.isMinimap()) { + this.leftPadding = this.createFeatureToggles(); + } + this.seedMapWidth = this.width - this.leftPadding(); if (!this.isMinimap()) { - this.createFeatureToggles(); this.createTeleportField(); this.createWaypointNameField(); + ItemIconButton lootSearchButton = new ItemIconButton(this.width - ItemIconButton.ICON_SIZE - 1, this.topPadding() - ItemIconButton.ICON_SIZE - 1, LOOT_SEARCH_ICON_ITEM, Component.literal("Loot Search"), button -> { + this.minecraft.setScreen(new LootSearchScreen(SeedMapScreen.this)); + }); + this.addRenderableWidget(lootSearchButton); + this.enchantmentsRegistry = this.minecraft.player.registryAccess().lookupOrThrow(Registries.ENCHANTMENT); this.mobEffectRegistry = this.minecraft.player.registryAccess().lookupOrThrow(Registries.MOB_EFFECT); } - } + } + + private boolean isOverMap(double mouseX, double mouseY) { + return mouseX >= this.leftPadding() && mouseX <= this.leftPadding() + this.seedMapWidth + && mouseY >= this.topPadding() && mouseY <= this.topPadding() + this.seedMapHeight; + } @Override public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { super.render(guiGraphics, mouseX, mouseY, partialTick); // draw title Component seedComponent = Component.translatable("seedMap.seed", accent(Long.toString(this.seed)), Cubiomes.mc2str(this.version).getString(0), ComponentUtils.formatGeneratorFlags(this.generatorFlags)); - guiGraphics.drawString(this.font, seedComponent, this.horizontalPadding(), this.verticalPadding() - this.font.lineHeight - 1, -1); + guiGraphics.drawString(this.font, seedComponent, this.leftPadding(), this.topPadding() - this.font.lineHeight - 1, -1); this.renderBiomes(guiGraphics, mouseX, mouseY, partialTick); guiGraphics.nextStratum(); this.renderFeatures(guiGraphics, mouseX, mouseY, partialTick); - // draw hovered coordinates and biome - MutableComponent coordinates = accent("x: %d, z: %d".formatted(QuartPos.toBlock(this.mouseQuart.x()), QuartPos.toBlock(this.mouseQuart.z()))); - OptionalInt optionalBiome = getBiome(this.mouseQuart); - if (optionalBiome.isPresent()) { - coordinates = coordinates.append(" [%s]".formatted(Cubiomes.biome2str(this.version, optionalBiome.getAsInt()).getString(0))); - } + + this.teleportEditBoxX.setHint(Component.literal("X: %d".formatted(QuartPos.toBlock(this.mouseQuart.x())))); + this.teleportEditBoxZ.setHint(Component.literal("Z: %d".formatted(QuartPos.toBlock(this.mouseQuart.z())))); + if (this.displayCoordinatesCopiedTicks > 0) { - coordinates = Component.translatable("seedMap.coordinatesCopied", coordinates); + guiGraphics.setTooltipForNextFrame(Component.translatable("seedMap.coordinatesCopied"), mouseX, mouseY); + } else if (this.isOverMap(mouseX, mouseY)) { + OptionalInt optionalBiome = this.getBiome(this.mouseQuart); + if (optionalBiome.isPresent()) { + guiGraphics.setTooltipForNextFrame(Component.literal(Cubiomes.biome2str(this.version, optionalBiome.getAsInt()).getString(0)), mouseX, mouseY); + } } - guiGraphics.drawString(this.font, coordinates, this.horizontalPadding(), this.verticalPadding() + this.seedMapHeight + 1, -1); } protected void renderBiomes(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { - int tileSizePixels = TILE_SIZE_PIXELS.getAsInt(); + int tileSizePixels = tileSizePixels(); int horTileRadius = Math.ceilDiv(this.seedMapWidth, tileSizePixels) + 1; int verTileRadius = Math.ceilDiv(this.seedMapHeight, tileSizePixels) + 1; @@ -387,7 +406,8 @@ protected void renderBiomes(GuiGraphics guiGraphics, int mouseX, int mouseY, flo } protected void renderFeatures(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { - int tileSizePixels = TILE_SIZE_PIXELS.getAsInt(); + this.featureWidgets.clear(); + int tileSizePixels = tileSizePixels(); int horTileRadius = Math.ceilDiv(this.seedMapWidth, tileSizePixels) + 1; int verTileRadius = Math.ceilDiv(this.seedMapHeight, tileSizePixels) + 1; @@ -534,35 +554,35 @@ protected void renderFeatures(GuiGraphics guiGraphics, int mouseX, int mouseY, f private void drawTile(GuiGraphics guiGraphics, Tile tile) { TilePos tilePos = tile.pos(); QuartPos2f relTileQuart = QuartPos2f.fromQuartPos(QuartPos2.fromTilePos(tilePos)).subtract(this.centerQuart); - int tileSizePixels = TILE_SIZE_PIXELS.getAsInt(); + int tileSizePixels = tileSizePixels(); int minX = this.centerX + Mth.floor(Configs.PixelsPerBiome * relTileQuart.x()); int minY = this.centerY + Mth.floor(Configs.PixelsPerBiome * relTileQuart.z()); int maxX = minX + tileSizePixels; int maxY = minY + tileSizePixels; - if (maxX < this.horizontalPadding() || minX > this.horizontalPadding() + this.seedMapWidth) { + if (maxX < this.leftPadding() || minX > this.leftPadding() + this.seedMapWidth) { return; } - if (maxY < this.verticalPadding() || minY > this.verticalPadding() + this.seedMapHeight) { + if (maxY < this.topPadding() || minY > this.topPadding() + this.seedMapHeight) { return; } float u0, u1, v0, v1; - if (minX < this.horizontalPadding()) { - u0 = (float) (this.horizontalPadding() - minX) / tileSizePixels; - minX = this.horizontalPadding(); + if (minX < this.leftPadding()) { + u0 = (float) (this.leftPadding() - minX) / tileSizePixels; + minX = this.leftPadding(); } else u0 = 0; - if (maxX > this.horizontalPadding() + this.seedMapWidth) { - u1 = 1 - ((float) (maxX - this.horizontalPadding() - this.seedMapWidth) / tileSizePixels); - maxX = this.horizontalPadding() + this.seedMapWidth; + if (maxX > this.leftPadding() + this.seedMapWidth) { + u1 = 1 - ((float) (maxX - this.leftPadding() - this.seedMapWidth) / tileSizePixels); + maxX = this.leftPadding() + this.seedMapWidth; } else u1 = 1; - if (minY < this.verticalPadding()) { - v0 = (float) (this.verticalPadding() - minY) / tileSizePixels; - minY = this.verticalPadding(); + if (minY < this.topPadding()) { + v0 = (float) (this.topPadding() - minY) / tileSizePixels; + minY = this.topPadding(); } else v0 = 0; - if (maxY > this.verticalPadding() + this.seedMapHeight) { - v1 = 1 - ((float) (maxY - this.verticalPadding() - this.seedMapHeight) / tileSizePixels); - maxY = this.verticalPadding() + this.seedMapHeight; + if (maxY > this.topPadding() + this.seedMapHeight) { + v1 = 1 - ((float) (maxY - this.topPadding() - this.seedMapHeight) / tileSizePixels); + maxY = this.topPadding() + this.seedMapHeight; } else v1 = 1; guiGraphics.submitBlit(RenderPipelines.GUI_TEXTURED, tile.texture().getTextureView(), tile.texture().getSampler(), minX, minY, maxX, maxY, u0, u1, v0, v1, 0xFF_FFFFFF); @@ -613,14 +633,9 @@ private Tile createSlimeChunkTile(TilePos tilePos, BitSet slimeChunkData) { } private void drawFeatureIcons(GuiGraphics guiGraphics) { - for (ObjectIterator iterator = this.featureWidgets.iterator(); iterator.hasNext();) { - FeatureWidget widget = iterator.next(); - if (Configs.ToggledFeatures.contains(widget.feature)) { - MapFeature.Texture texture = widget.texture(); - this.drawIcon(guiGraphics, texture.identifier(), widget.x, widget.y, texture.width(), texture.height(), 0xFF_FFFFFF); - } else { - iterator.remove(); - } + for (FeatureWidget widget : this.featureWidgets) { + MapFeature.Texture texture = widget.texture(); + this.drawIcon(guiGraphics, texture.identifier(), widget.x, widget.y, texture.width(), texture.height(), 0xFF_FFFFFF); } } @@ -633,7 +648,7 @@ protected void drawPlayerIndicator(GuiGraphics guiGraphics) { int playerMinY = this.centerY + Mth.floor(Configs.PixelsPerBiome * relPlayerQuart.z()) - 10; int playerMaxX = playerMinX + 20; int playerMaxY = playerMinY + 20; - if (playerMinX < this.horizontalPadding() || playerMaxX > this.horizontalPadding() + this.seedMapWidth || playerMinY < this.verticalPadding() || playerMaxY > this.verticalPadding() + this.seedMapHeight) { + if (playerMinX < this.leftPadding() || playerMaxX > this.leftPadding() + this.seedMapWidth || playerMinY < this.topPadding() || playerMaxY > this.topPadding() + this.seedMapHeight) { return; } PlayerFaceRenderer.draw(guiGraphics, this.minecraft.player.getSkin(), playerMinX, playerMinY, 20); @@ -652,34 +667,36 @@ protected void drawDirectionArrow(GuiGraphics guiGraphics, int playerMinX, int p ; boolean withinBounds = Stream.of(new Vector2f(20, 0), new Vector2f(20, 20), new Vector2f(0, 20), new Vector2f(0, 0)) .map(transform::transformPosition) - .allMatch(v -> v.x >= this.horizontalPadding() && v.x <= this.horizontalPadding() + this.seedMapWidth && - v.y >= this.verticalPadding() && v.y <= this.verticalPadding() + this.seedMapHeight); + .allMatch(v -> isOverMap(v.x, v.y)); if (withinBounds) { drawIconStatic(guiGraphics, DIRECTION_ARROW_TEXTURE, 0, 0, 20, 20, 0xFF_FFFFFF); } guiGraphics.pose().popMatrix(); } - private void createFeatureToggles() { + /// @return the amount of vertical pixels that are reserved for the feature toggles + private int createFeatureToggles() { // TODO: replace with Gatherers API? // TODO: only calculate on resize? - int rows = Math.ceilDiv(this.featureIconsCombinedWidth, this.seedMapWidth); - int togglesPerRow = Math.ceilDiv(this.toggleableFeatures.size(), rows); - int toggleMinY = 1; - for (int row = 0; row < rows - 1; row++) { - this.createFeatureTogglesInner(row, togglesPerRow, togglesPerRow, this.horizontalPadding(), toggleMinY); - toggleMinY += FEATURE_TOGGLE_HEIGHT + VERTICAL_FEATURE_TOGGLE_SPACING; - } - int togglesInLastRow = this.toggleableFeatures.size() - togglesPerRow * (rows - 1); - this.createFeatureTogglesInner(rows - 1, togglesPerRow, togglesInLastRow, this.horizontalPadding(), toggleMinY); + int columns = Math.ceilDiv(this.featureIconsCombinedHeight, this.seedMapHeight); + int togglesPerColumn = Math.ceilDiv(this.toggleableFeatures.size(), columns); + int toggleMinX = 1; + for (int column = 0; column < columns - 1; column++) { + this.createFeatureTogglesInner(column, togglesPerColumn, togglesPerColumn, toggleMinX, this.topPadding()); + toggleMinX += 20 + HORIZONTAL_FEATURE_TOGGLE_SPACING; + } + int togglesInLastColumn = this.toggleableFeatures.size() - togglesPerColumn * (columns - 1); + this.createFeatureTogglesInner(columns - 1, togglesPerColumn, togglesInLastColumn, toggleMinX, this.topPadding()); + + return toggleMinX + 20 + HORIZONTAL_FEATURE_TOGGLE_SPACING; } - private void createFeatureTogglesInner(int row, int togglesPerRow, int maxToggles, int toggleMinX, int toggleMinY) { + private void createFeatureTogglesInner(int column, int togglesPerColumn, int maxToggles, int toggleMinX, int toggleMinY) { for (int toggle = 0; toggle < maxToggles; toggle++) { - MapFeature feature = this.toggleableFeatures.get(row * togglesPerRow + toggle); + MapFeature feature = this.toggleableFeatures.get(column * togglesPerColumn + toggle); MapFeature.Texture featureIcon = feature.getDefaultTexture(); this.addRenderableWidget(new FeatureToggleWidget(feature, toggleMinX, toggleMinY)); - toggleMinX += featureIcon.width() + HORIZONTAL_FEATURE_TOGGLE_SPACING; + toggleMinY += featureIcon.height() + VERTICAL_FEATURE_TOGGLE_SPACING; } } @@ -726,7 +743,7 @@ private BitSet calculateSlimeChunkData(TilePos tilePos) { } BlockPos pos = new BlockPos(Pos.x(structurePos), 0, Pos.z(structurePos)); - OptionalInt optionalBiome = getBiome(QuartPos2.fromBlockPos(pos)); + OptionalInt optionalBiome = this.getBiome(QuartPos2.fromBlockPos(pos)); MapFeature.Texture texture; if (optionalBiome.isEmpty()) { texture = feature.getDefaultTexture(); @@ -769,7 +786,7 @@ private BitSet calculateSlimeChunkData(TilePos tilePos) { private BitSet calculateCanyonData(TilePos tilePos) { ToIntBiFunction biomeFunction; if (this.version <= Cubiomes.MC_1_17()) { - biomeFunction = (chunkX, chunkZ) -> getBiome(new QuartPos2(QuartPos.fromSection(chunkX), QuartPos.fromSection(chunkZ))).orElseGet(() -> Cubiomes.getBiomeAt(this.biomeGenerator, 4, chunkX << 2, 0, chunkZ << 2)); + biomeFunction = (chunkX, chunkZ) -> this.getBiome(new QuartPos2(QuartPos.fromSection(chunkX), QuartPos.fromSection(chunkZ))).orElseGet(() -> Cubiomes.getBiomeAt(this.biomeGenerator, 4, chunkX << 2, 0, chunkZ << 2)); } else { biomeFunction = (_, _) -> -1; } @@ -819,18 +836,18 @@ private BlockPos calculateSpawnData() { } private void createTeleportField() { - this.teleportEditBoxX = new EditBox(this.font, this.width / 2 - TELEPORT_FIELD_WIDTH, this.verticalPadding() + this.seedMapHeight + 1, TELEPORT_FIELD_WIDTH, 20, Component.translatable("seedMap.teleportEditBoxX")); + this.teleportEditBoxX = new EditBox(this.font, this.leftPadding(), this.height - BOTTOM_PADDING, this.seedMapWidth / 4, BOTTOM_PADDING, Component.translatable("seedMap.teleportEditBoxX")); this.teleportEditBoxX.setHint(Component.literal("X")); this.teleportEditBoxX.setMaxLength(9); this.addRenderableWidget(this.teleportEditBoxX); - this.teleportEditBoxZ = new EditBox(this.font, this.width / 2, this.verticalPadding() + this.seedMapHeight + 1, TELEPORT_FIELD_WIDTH, 20, Component.translatable("seedMap.teleportEditBoxZ")); + this.teleportEditBoxZ = new EditBox(this.font, this.leftPadding() + this.seedMapWidth / 4, this.height - BOTTOM_PADDING, this.seedMapWidth / 4, BOTTOM_PADDING, Component.translatable("seedMap.teleportEditBoxZ")); this.teleportEditBoxZ.setHint(Component.literal("Z")); this.teleportEditBoxZ.setMaxLength(9); this.addRenderableWidget(this.teleportEditBoxZ); } private void createWaypointNameField() { - this.waypointNameEditBox = new EditBox(this.font, this.horizontalPadding() + this.seedMapWidth - WAYPOINT_NAME_FIELD_WIDTH, this.verticalPadding() + this.seedMapHeight + 1, WAYPOINT_NAME_FIELD_WIDTH, 20, Component.translatable("seedMap.waypointNameEditBox")); + this.waypointNameEditBox = new EditBox(this.font, this.leftPadding() + this.seedMapWidth / 2, this.height - BOTTOM_PADDING, this.seedMapWidth / 2, BOTTOM_PADDING, Component.translatable("seedMap.waypointNameEditBox")); this.waypointNameEditBox.setHint(Component.literal("Waypoint name")); this.addRenderableWidget(this.waypointNameEditBox); } @@ -838,11 +855,6 @@ private void createWaypointNameField() { protected void moveCenter(QuartPos2f newCenter) { this.centerQuart = newCenter; - this.featureWidgets.removeIf(widget -> { - widget.updatePosition(); - return !widget.withinBounds(); - }); - if (this.markerWidget != null) { this.markerWidget.updatePosition(); } @@ -870,7 +882,7 @@ public void mouseMoved(double mouseX, double mouseY) { } private void handleMapMouseMoved(double mouseX, double mouseY) { - if (mouseX < this.horizontalPadding() || mouseX > this.horizontalPadding() + this.seedMapWidth || mouseY < this.verticalPadding() || mouseY > this.verticalPadding() + this.seedMapHeight) { + if (!this.isOverMap(mouseX, mouseY)) { return; } @@ -891,11 +903,6 @@ public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, doubl Configs.PixelsPerBiome = Math.max((int) (currentScroll * MAX_PIXELS_PER_BIOME + 0.5), MIN_PIXELS_PER_BIOME); - this.featureWidgets.removeIf(widget -> { - widget.updatePosition(); - return !widget.withinBounds(); - }); - if (this.markerWidget != null) { this.markerWidget.updatePosition(); } @@ -910,7 +917,7 @@ public boolean mouseDragged(MouseButtonEvent mouseButtonEvent, double dragX, dou } double mouseX = mouseButtonEvent.x(); double mouseY = mouseButtonEvent.y(); - if (mouseX < this.horizontalPadding() || mouseX > this.horizontalPadding() + this.seedMapWidth || mouseY < this.verticalPadding() || mouseY > this.verticalPadding() + this.seedMapHeight) { + if (!this.isOverMap(mouseX, mouseY)) { return false; } @@ -951,7 +958,7 @@ private boolean handleMapFeatureLeftClicked(MouseButtonEvent mouseButtonEvent, b } double mouseX = mouseButtonEvent.x(); double mouseY = mouseButtonEvent.y(); - if (mouseX < this.horizontalPadding() || mouseX > this.horizontalPadding() + this.seedMapWidth || mouseY < this.verticalPadding() || mouseY > this.verticalPadding() + this.seedMapHeight) { + if (!this.isOverMap(mouseX, mouseY)) { return false; } Optional optionalFeatureWidget = this.featureWidgets.stream() @@ -1057,7 +1064,7 @@ private boolean handleMapMiddleClicked(MouseButtonEvent mouseButtonEvent, boolea } double mouseX = mouseButtonEvent.x(); double mouseY = mouseButtonEvent.y(); - if (mouseX < this.horizontalPadding() || mouseX > this.horizontalPadding() + this.seedMapWidth || mouseY < this.verticalPadding() || mouseY > this.verticalPadding() + this.seedMapHeight) { + if (!this.isOverMap(mouseX, mouseY)) { return false; } this.minecraft.keyboardHandler.setClipboard("%d ~ %d".formatted(QuartPos.toBlock(this.mouseQuart.x()), QuartPos.toBlock(this.mouseQuart.z()))); @@ -1072,7 +1079,7 @@ private boolean handleMapRightClicked(MouseButtonEvent mouseButtonEvent, boolean } double mouseX = mouseButtonEvent.x(); double mouseY = mouseButtonEvent.y(); - if (mouseX < this.horizontalPadding() || mouseX > this.horizontalPadding() + this.seedMapWidth || mouseY < this.verticalPadding() || mouseY > this.verticalPadding() + this.seedMapHeight) { + if (!this.isOverMap(mouseX, mouseY)) { return false; } @@ -1099,7 +1106,11 @@ private boolean handleTeleportFieldEnter(KeyEvent keyEvent) { if (keyCode != InputConstants.KEY_RETURN) { return false; } - if (!this.teleportEditBoxX.isActive() && !this.teleportEditBoxZ.isActive()) { + if (this.teleportEditBoxX == null || this.teleportEditBoxZ == null) { + return false; + } + boolean hasTeleportFocus = this.teleportEditBoxX.isActive() || this.teleportEditBoxZ.isActive(); + if (!hasTeleportFocus) { return false; } int x, z; @@ -1107,17 +1118,19 @@ private boolean handleTeleportFieldEnter(KeyEvent keyEvent) { x = Integer.parseInt(this.teleportEditBoxX.getValue()); z = Integer.parseInt(this.teleportEditBoxZ.getValue()); } catch (NumberFormatException _) { + this.clearEntryBoxFocus(); return false; } if (x < -Level.MAX_LEVEL_SIZE || x > Level.MAX_LEVEL_SIZE) { + this.clearEntryBoxFocus(); return false; } if (z < -Level.MAX_LEVEL_SIZE || z > Level.MAX_LEVEL_SIZE) { + this.clearEntryBoxFocus(); return false; } this.moveCenter(new QuartPos2f(QuartPos.fromBlock(x), QuartPos.fromBlock(z))); - this.teleportEditBoxX.setValue(""); - this.teleportEditBoxZ.setValue(""); + this.clearEntryBoxFocus(); return true; } @@ -1148,9 +1161,22 @@ private boolean handleWaypointNameFieldEnter(KeyEvent keyEvent) { return false; } this.waypointNameEditBox.setValue(""); + this.clearEntryBoxFocus(); return true; } + private void clearEntryBoxFocus() { + if (this.teleportEditBoxX != null) { + this.teleportEditBoxX.setFocused(false); + } + if (this.teleportEditBoxZ != null) { + this.teleportEditBoxZ.setFocused(false); + } + if (this.waypointNameEditBox != null) { + this.waypointNameEditBox.setFocused(false); + } + } + @Override public void onClose() { super.onClose(); @@ -1202,10 +1228,10 @@ public boolean withinBounds() { int maxX = minX + this.width(); int maxY = minY + this.height(); - if (maxX >= horizontalPadding() + seedMapWidth || maxY >= verticalPadding() + seedMapHeight) { + if (maxX >= leftPadding() + seedMapWidth || maxY >= topPadding() + seedMapHeight) { return false; } - if (minX < horizontalPadding() || minY < verticalPadding()) { + if (minX < leftPadding() || minY < topPadding()) { return false; } return true; @@ -1245,7 +1271,7 @@ private void drawIcon(GuiGraphics guiGraphics, Identifier identifier, int minX, pose.popMatrix(); } - private static void drawIconStatic(GuiGraphics guiGraphics, Identifier identifier, int minX, int minY, int iconWidth, int iconHeight, int colour) { + public static void drawIconStatic(GuiGraphics guiGraphics, Identifier identifier, int minX, int minY, int iconWidth, int iconHeight, int colour) { // Skip intersection checks (GuiRenderState.hasIntersection) you would otherwise get when calling // GuiGraphics.blit as these checks incur a significant performance hit AbstractTexture texture = Minecraft.getInstance().getTextureManager().getTexture(identifier); @@ -1271,16 +1297,20 @@ protected void updatePlayerRotation(Vec2 vec2) { this.playerRotation = vec2; } + public BlockPos getPlayerPos() { + return this.playerPos; + } + public Vec2 getPlayerRotation() { return this.playerRotation; } - protected int horizontalPadding() { - return HORIZONTAL_PADDING; + protected int leftPadding() { + return this.leftPadding; } - protected int verticalPadding() { - return VERTICAL_PADDING; + protected int topPadding() { + return TOP_PADDING; } protected long getSeed() { diff --git a/src/main/resources/assets/seedmapper/lang/en_us.json b/src/main/resources/assets/seedmapper/lang/en_us.json index 055c7ad..d69a3c4 100644 --- a/src/main/resources/assets/seedmapper/lang/en_us.json +++ b/src/main/resources/assets/seedmapper/lang/en_us.json @@ -110,8 +110,36 @@ "seedMap.chestLoot.extraInfo.lootTable": "Loot table: %s", "seedMap.chestLoot.extraInfo.lootSeed": "Loot seed: %s", "seedMap.chestLoot.stewEffect": "%s (%s seconds)", - "seedMap.coordinatesCopied": "%s (coordinates copied!)", + "seedMap.coordinatesCopied": "Coordinates copied!", "seedMap.teleportEditBoxX": "Edit box X coordinate", "seedMap.teleportEditBoxZ": "Edit box Z coordinate", - "seedMap.waypointNameEditBox": "Waypoint name edit box" + "seedMap.waypointNameEditBox": "Waypoint name edit box", + "seedMap.lootSearch.title": "Loot Search", + "seedMap.lootSearch.tab.search": "Search", + "seedMap.lootSearch.tab.results": "Results", + "seedMap.lootSearch.radius": "Radius", + "seedMap.lootSearch.radiusHint": "Radius (blocks)", + "seedMap.lootSearch.search": "Search", + "seedMap.lootSearch.filter": "Search results", + "seedMap.lootSearch.filterHint": "Filter items", + "seedMap.lootSearch.results": "Results", + "seedMap.lootSearch.noResults": "No results yet.", + "seedMap.lootSearch.selectItem": "Select an item to view details.", + "seedMap.lootSearch.total": "Total: %d", + "seedMap.lootSearch.noStructures": "No structures found.", + "seedMap.lootSearch.column.x": "X", + "seedMap.lootSearch.column.z": "Z", + "seedMap.lootSearch.column.count": "Count", + "seedMap.lootSearch.column.type": "Type", + "seedMap.lootSearch.error.invalidRadius": "Invalid radius.", + "seedMap.lootSearch.error.radiusRange": "Radius out of range.", + "seedMap.lootSearch.searching": "Searching...", + "seedMap.lootSearch.error.searchFailed": "Search failed.", + "seedMap.lootSearch.noLoot": "No loot items found.", + "seedMap.lootSearch.foundTypes": "Found %d item types.", + "seedMap.lootSearch.sort": "Sort: %s", + "seedMap.lootSearch.sort.count": "Count", + "seedMap.lootSearch.sort.distance": "Distance", + + "seedMap.clickToCopy": "Click to copy" }