diff --git a/dependencies.gradle b/dependencies.gradle index 9238e601..a2bfb039 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,7 +1,7 @@ dependencies { - api("com.github.GTNewHorizons:GTNHLib:0.11.48:dev") - implementation("com.github.GTNewHorizons:GTNHExtLib:1.0.4:dev") + api("com.github.GTNewHorizons:GTNHLib:0.11.52:dev") + implementation("com.github.GTNewHorizons:GTNHExtLib:1.0.5:dev") implementation("io.github.legacymoddingmc:unimixins:0.3.1:dev") shadowImplementation("org.yaml:snakeyaml:1.33") { transitive = false } @@ -18,41 +18,41 @@ dependencies { shadowImplementation("org.eclipse.elk:org.eclipse.elk.alg.layered:0.11.0") shadowImplementation("org.eclipse.xtext:org.eclipse.xtext.xbase.lib:2.36.0") - devOnlyNonPublishable("com.github.GTNewHorizons:StructureLib:1.4.43:dev") - devOnlyNonPublishable("com.github.GTNewHorizons:GT5-Unofficial:5.09.54.194:dev") - devOnlyNonPublishable("com.github.GTNewHorizons:NotEnoughItems:2.8.140-GTNH:dev") - devOnlyNonPublishable("com.github.GTNewHorizons:Applied-Energistics-2-Unofficial:rv3-beta-1061-GTNH:dev") + devOnlyNonPublishable("com.github.GTNewHorizons:StructureLib:1.4.45:dev") + devOnlyNonPublishable("com.github.GTNewHorizons:GT5-Unofficial:5.09.54.205:dev") + devOnlyNonPublishable("com.github.GTNewHorizons:NotEnoughItems:2.8.155-GTNH:dev") + devOnlyNonPublishable("com.github.GTNewHorizons:Applied-Energistics-2-Unofficial:rv3-beta-1080-GTNH:dev") devOnlyNonPublishable("com.github.GTNewHorizons:CarpentersBlocks:3.7.3-GTNH:dev") - devOnlyNonPublishable("com.github.GTNewHorizons:ForgeMultipart:1.7.14:dev") + devOnlyNonPublishable("com.github.GTNewHorizons:ForgeMultipart:1.7.16:dev") devOnlyNonPublishable("com.github.GTNewHorizons:WirelessRedstone-CBE:1.7.12:dev") devOnlyNonPublishable("com.github.GTNewHorizons:Railcraft:9.17.31:dev") - devOnlyNonPublishable("com.github.GTNewHorizons:EnhancedLootBags:1.3.4:dev") - devOnlyNonPublishable("com.github.GTNewHorizons:BetterQuesting:3.8.88-GTNH:dev") - devOnlyNonPublishable("com.github.GTNewHorizons:Angelica:2.2.13:dev") - devOnlyNonPublishable("com.github.GTNewHorizons:TX-Loader:1.9:dev") + devOnlyNonPublishable("com.github.GTNewHorizons:EnhancedLootBags:1.3.5:dev") + devOnlyNonPublishable("com.github.GTNewHorizons:BetterQuesting:3.8.89-GTNH:dev") + devOnlyNonPublishable("com.github.GTNewHorizons:Angelica:2.2.28:dev") + devOnlyNonPublishable("com.github.GTNewHorizons:TX-Loader:1.9.1:dev") devOnlyNonPublishable("curse.maven:cofh-core-69162:2388751") - runtimeOnlyNonPublishable("com.github.GTNewHorizons:Baubles-Expanded:2.2.23-GTNH:dev") - runtimeOnlyNonPublishable("com.github.GTNewHorizons:Botania:1.13.36-GTNH:dev") + runtimeOnlyNonPublishable("com.github.GTNewHorizons:Baubles-Expanded:2.2.25-GTNH:dev") + runtimeOnlyNonPublishable("com.github.GTNewHorizons:Botania:1.13.37-GTNH:dev") runtimeOnlyNonPublishable("com.github.GTNewHorizons:Botanic-horizons:1.12.10-GTNH:dev") - compileOnlyApi("com.github.GTNewHorizons:BlockRenderer6343:1.4.23:dev") { transitive = false } - runtimeOnlyNonPublishable("com.github.GTNewHorizons:BlockRenderer6343:1.4.23:dev") { transitive = false } + compileOnlyApi("com.github.GTNewHorizons:BlockRenderer6343:1.4.24:dev") { transitive = false } + runtimeOnlyNonPublishable("com.github.GTNewHorizons:BlockRenderer6343:1.4.24:dev") { transitive = false } runtimeOnlyNonPublishable("com.github.GTNewHorizons:EnderCore:0.5.15:dev") { transitive = false } - runtimeOnlyNonPublishable("com.github.GTNewHorizons:ServerUtilities:2.4.10:dev") + runtimeOnlyNonPublishable("com.github.GTNewHorizons:ServerUtilities:2.4.14:dev") runtimeOnlyNonPublishable("it.unimi.dsi:fastutil:8.5.18") runtimeOnlyNonPublishable(rfg.deobf("curse.maven:spark-361579:4271867")) compileOnlyApi("com.github.GTNewHorizons:nei-custom-diagram:1.8.36:dev") { transitive = false } - compileOnlyApi("com.github.GTNewHorizons:TinkersConstruct:1.14.112-GTNH:dev") { transitive = false } + compileOnlyApi("com.github.GTNewHorizons:TinkersConstruct:1.14.118-GTNH:dev") { transitive = false } compileOnlyApi("com.github.GTNewHorizons:Mantle:0.5.4:dev") { transitive = false } compileOnlyApi("com.github.GTNewHorizons:Mobs-Info:0.6.0-GTNH:dev") { transitive = false } compileOnlyApi("com.github.GTNewHorizons:LogisticsPipes:1.5.36-GTNH:dev") { transitive = false } compileOnlyApi("com.github.GTNewHorizons:BuildCraft:7.1.64:dev") { transitive = false } compileOnlyApi("com.github.GTNewHorizons:Translocators:1.4.5:dev") { transitive = false } compileOnlyApi("com.github.GTNewHorizons:SimpleSkinBackport:1.0.3-GTNH:dev") { transitive = false } - compileOnlyApi("com.github.GTNewHorizons:OpenComputers:1.12.61-GTNH:dev") { transitive = false } + compileOnlyApi("com.github.GTNewHorizons:OpenComputers:1.12.64-GTNH:dev") { transitive = false } compileOnlyApi("com.github.slprime:ChromaticTooltips:1.0.28-GTNH:dev") { transitive = false } - compileOnlyApi("ganymedes01.etfuturum:Et-Futurum-Requiem:2.6.59-GTNH") { transitive = false } + compileOnlyApi("ganymedes01.etfuturum:Et-Futurum-Requiem:2.6.60-GTNH") { transitive = false } // jvmDowngrader compileOnly("javax.xml.bind:jaxb-api:2.3.1") @@ -64,7 +64,7 @@ dependencies { annotationProcessor("com.google.auto.value:auto-value:1.10.4") testImplementation("org.junit.jupiter:junit-jupiter-api:5.12.2") - testImplementation("com.github.GTNewHorizons:StructureLib:1.4.43:dev") + testImplementation("com.github.GTNewHorizons:StructureLib:1.4.45:dev") testImplementation("org.joml:joml:1.10.8") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.12.2") testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.12.2") @@ -75,4 +75,4 @@ configurations.configureEach { exclude group: "com.github.GTNewHorizons", module: "CodeChickenLib" exclude group: "com.github.GTNewHorizons", module: "Hodgepodge" exclude group: "com.github.GTNewHorizons", module: "Baubles" -} +} \ No newline at end of file diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/RecipeCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/RecipeCompiler.java index 7dce43ae..bb9857b7 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/RecipeCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/RecipeCompiler.java @@ -12,6 +12,7 @@ import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTBase; +import net.minecraftforge.oredict.OreDictionary; import org.jetbrains.annotations.Nullable; @@ -347,6 +348,14 @@ public interface HandlerRecipeAccess { List readIngredientSlots(Object handler, int recipeIndex); + /** + * Reads catalyst or auxiliary slots exposed by a handler. Older integrations may not + * provide these slots, so the default keeps the access contract source-compatible. + */ + default List readOtherSlots(Object handler, int recipeIndex) { + return List.of(); + } + @Nullable NeiRecipeLookup.Slot readResultSlot(Object handler, int recipeIndex); } @@ -404,7 +413,8 @@ public static boolean stackMatches(@Nullable ItemStack stack, IdUtils.ParsedItem if (stack == null) return false; Item refItem = (Item) Item.itemRegistry.getObject(ref.rawKey()); if (refItem == null || stack.getItem() != refItem) return false; - if (!ref.isWildcardMeta() && stack.getItemDamage() != ref.meta()) return false; + if (!ref.isWildcardMeta() && stack.getItemDamage() != ref.meta() + && stack.getItemDamage() != OreDictionary.WILDCARD_VALUE) return false; if (ref.nbt() != null) { if (stack.stackTagCompound == null) return false; if (!ref.nbt() @@ -433,6 +443,57 @@ public static boolean resultSlotContains(@Nullable NeiRecipeLookup.Slot result, return false; } + /** + * Matches a tag target against one handler recipe. Normal recipe tags are output queries. An + * explicitly named handler may instead expose the requested stack as an ingredient or + * catalyst, as Fuel and Brewing do in NEI, so those slots are checked when requested. + */ + public static boolean handlerTargetMatches(Object handler, int recipeIndex, IdUtils.ParsedItemRef ref, + HandlerRecipeAccess recipeAccess, boolean includeInputSlots) { + if (resultSlotContains(recipeAccess.readResultSlot(handler, recipeIndex), ref)) { + return true; + } + if (!includeInputSlots) { + return false; + } + if (slotsContain(recipeAccess.readIngredientSlots(handler, recipeIndex), ref)) { + return true; + } + if (slotsContain(recipeAccess.readOtherSlots(handler, recipeIndex), ref)) { + return true; + } + return ref.nbt() == null + && damageableItemVariantPresent(recipeAccess.readIngredientSlots(handler, recipeIndex), ref); + } + + public static boolean handlerTargetMatches(NeiRecipeLookup.Entry entry, IdUtils.ParsedItemRef ref, + boolean includeInputSlots) { + if (entry == null) return false; + if (resultSlotContains(entry.result, ref)) return true; + if (!includeInputSlots) return false; + if (slotsContain(entry.ingredients, ref)) return true; + if (slotsContain(entry.others, ref)) return true; + return ref.nbt() == null && damageableItemVariantPresent(entry.ingredients, ref); + } + + private static boolean damageableItemVariantPresent(@Nullable List slots, + IdUtils.ParsedItemRef ref) { + if (slots == null) return false; + Item refItem = (Item) Item.itemRegistry.getObject(ref.rawKey()); + if (refItem == null) return false; + for (NeiRecipeLookup.Slot slot : slots) { + if (slot == null || slot.stacks == null) continue; + for (ItemStack stack : slot.stacks) { + if (stack != null && stack.getItem() == refItem + && stack.isItemStackDamageable() + && stack.getItemDamage() > 0) { + return true; + } + } + } + return false; + } + public static boolean recipeSlotsContain(@Nullable List slots, IdUtils.ParsedItemRef ref) { if (slots == null) return false; for (RecipeSlot slot : slots) { @@ -581,6 +642,10 @@ public static boolean recipeMatches(Object handler, int recipeIndex, FilterExpr return true; } + public static boolean recipeMatches(NeiRecipeLookup.Entry entry, FilterExpr inputExpr, FilterExpr outputExpr) { + return entry != null && entryMatches(entry, inputExpr, outputExpr); + } + public static boolean entryMatches(NeiRecipeLookup.Entry e, FilterExpr inputExpr, FilterExpr outputExpr) { if (!outputExpr.isEmpty() && !evalResultSlot(e.result, outputExpr)) return false; if (!inputExpr.isEmpty() && !evalSlots(e.ingredients, inputExpr)) return false; diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/DefaultStyles.java b/src/main/java/com/hfstudio/guidenh/guide/document/DefaultStyles.java index 4b9288dc..2607c983 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/DefaultStyles.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/DefaultStyles.java @@ -58,7 +58,7 @@ private DefaultStyles() {} .font(null) .build(); public static final TextStyle HEADING3 = TextStyle.builder() - .fontScale(1f) + .fontScale(1.17f) .font(null) .build(); public static final TextStyle HEADING4 = TextStyle.builder() diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytBalancedColumns.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytBalancedColumns.java index edc1c567..51cd27a5 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytBalancedColumns.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytBalancedColumns.java @@ -8,11 +8,11 @@ import lombok.Setter; /** - * Places children into columns as wide as the widest of them, filling the shortest column first. + * Places children into the shortest available column while preserving each child's measured width. * *

- * Column count is based on the widest child and limited by {@link #getMaxColumns()}. - * Children wider than a column are laid out at their own width, falling back to a vertical stack. + * The highest column count whose measured column widths fit the available width is selected, up to + * {@link #getMaxColumns()}. If even two columns cannot fit, children are laid out as a vertical stack. */ @Getter @Setter @@ -34,56 +34,62 @@ protected LytRect computeBoxLayout(LayoutContext context, int x, int y, int avai return verticalStack(context, x, y, availableWidth); } - int widest = widestChildWidth(context, x, y, availableWidth); - int columns = columnCountFor(availableWidth, widest); - if (columns <= 1) { - return verticalStack(context, x, y, availableWidth); + int[] itemWidths = new int[count]; + int[] itemHeights = new int[count]; + for (int i = 0; i < count; i++) { + LytBlock child = children.get(i); + LytRect measured = child.layout(context, x, y, availableWidth); + itemWidths[i] = Math.max(1, measured.width() + child.getMarginLeft() + child.getMarginRight()); + itemHeights[i] = Math.max(1, measured.height() + child.getMarginTop() + child.getMarginBottom()); } - int columnWidth = widest; + int columns = Math.min(count, maxColumns); + while (columns > 1) { + int[] assignments = new int[count]; + int[] columnWidths = new int[columns]; + int[] columnBottoms = new int[columns]; + for (int i = 0; i < count; i++) { + int column = shortestColumn(columnBottoms); + assignments[i] = column; + columnWidths[column] = Math.max(columnWidths[column], itemWidths[i]); + columnBottoms[column] += itemHeights[i] + gap; + } + int requiredWidth = Math.max(0, columns - 1) * gap; + for (int width : columnWidths) { + requiredWidth += width; + } + if (requiredWidth <= availableWidth) { + return layoutColumns(context, x, y, availableWidth, assignments, columnWidths, columns); + } + columns--; + } + return verticalStack(context, x, y, availableWidth); + } + + private LytRect layoutColumns(LayoutContext context, int x, int y, int availableWidth, int[] assignments, + int[] columnWidths, int columns) { int[] columnBottoms = new int[columns]; LytBlock[] previousBlocks = new LytBlock[columns]; int contentWidth = 0; int contentHeight = 0; - - for (LytBlock child : children) { - int blockWidth = Math.max(1, columnWidth - child.getMarginLeft() - child.getMarginRight()); - int columnIndex = shortestColumn(columnBottoms); - - int columnX = x + columnIndex * (columnWidth + gap) + child.getMarginLeft(); - int columnY = Layouts.offsetIntoContentArea( - LytAxis.VERTICAL, - y + columnBottoms[columnIndex], - previousBlocks[columnIndex], - child); - LytRect childBounds = child.layout(context, columnX, columnY, blockWidth); - - columnBottoms[columnIndex] = childBounds.bottom() - y + child.getMarginBottom() + gap; - previousBlocks[columnIndex] = child; + int[] columnX = new int[columns]; + for (int i = 1; i < columns; i++) { + columnX[i] = columnX[i - 1] + columnWidths[i - 1] + gap; + } + for (int i = 0; i < children.size(); i++) { + LytBlock child = children.get(i); + int column = assignments[i]; + int blockWidth = Math.max(1, columnWidths[column] - child.getMarginLeft() - child.getMarginRight()); + int childX = x + columnX[column] + child.getMarginLeft(); + int childY = Layouts + .offsetIntoContentArea(LytAxis.VERTICAL, y + columnBottoms[column], previousBlocks[column], child); + LytRect childBounds = child.layout(context, childX, childY, blockWidth); + columnBottoms[column] = childBounds.bottom() - y + child.getMarginBottom() + gap; + previousBlocks[column] = child; contentWidth = Math.max(contentWidth, childBounds.right() - x); contentHeight = Math.max(contentHeight, childBounds.bottom() - y); } - - return new LytRect(x, y, contentWidth, contentHeight); - } - - private int widestChildWidth(LayoutContext context, int x, int y, int availableWidth) { - int widest = 0; - for (LytBlock child : children) { - LytRect bounds = child.layout(context, x, y, availableWidth); - widest = Math.max(widest, bounds.width() + child.getMarginLeft() + child.getMarginRight()); - } - return widest; - } - - private int columnCountFor(int availableWidth, int widest) { - int limit = Math.min(children.size(), maxColumns); - if (widest <= 0) { - return Math.max(1, limit); - } - // Columns are as wide as the widest child, so n of them need n widths and n-1 gaps. - int byWidth = (availableWidth + gap) / (widest + gap); - return Math.max(1, Math.min(limit, byWidth)); + return new LytRect(x, y, Math.min(availableWidth, contentWidth), contentHeight); } private static int shortestColumn(int[] columnBottoms) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index 475e11cf..750f7d8d 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -33,18 +33,21 @@ import net.minecraft.client.gui.GuiTextField; import net.minecraft.client.gui.GuiYesNoCallback; import net.minecraft.client.gui.inventory.GuiContainer; +import net.minecraft.client.renderer.RenderHelper; import net.minecraft.client.renderer.Tessellator; import net.minecraft.client.renderer.texture.DynamicTexture; import net.minecraft.client.resources.IResourcePack; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.inventory.Container; import net.minecraft.item.ItemStack; +import net.minecraft.util.EnumChatFormatting; import net.minecraft.util.ResourceLocation; import org.jetbrains.annotations.Nullable; import org.lwjgl.input.Keyboard; import org.lwjgl.input.Mouse; import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; import com.hfstudio.guidenh.ClientProxy; import com.hfstudio.guidenh.client.command.GuideNhClientBridgeController; @@ -116,6 +119,7 @@ import com.hfstudio.guidenh.guide.internal.input.GuideKeyBindingSupport; import com.hfstudio.guidenh.guide.internal.item.RegionWandExporter; import com.hfstudio.guidenh.guide.internal.markdown.CodeBlockClipboardService; +import com.hfstudio.guidenh.guide.internal.recipe.NeiItemTooltip; import com.hfstudio.guidenh.guide.internal.screen.GuideIconButton; import com.hfstudio.guidenh.guide.internal.screen.GuideNavBar; import com.hfstudio.guidenh.guide.internal.screen.GuideNavBar.ContextTarget; @@ -4777,11 +4781,107 @@ private void renderItemTooltip(ItemTooltip tooltip, int mouseX, int mouseY, return; } + if (tooltip instanceof NeiItemTooltip neiTooltip && neiTooltip.getPermutations() + .size() > 1) { + renderNeiItemTooltip(neiTooltip, mouseX, mouseY, interaction); + return; + } + List lines = GuideItemTooltipLines.build(tooltip, mc); FontRenderer font = GuideItemTooltipRenderSupport.resolveFont(stack, mc.fontRenderer); drawHoveringTextAtAdjustedPosition(lines, mouseX, mouseY, font, resolveTooltipBounds(interaction)); } + private void renderNeiItemTooltip(NeiItemTooltip tooltip, int mouseX, int mouseY, + @Nullable DocumentInteractionState interaction) { + ItemStack stack = tooltip.getStack(); + FontRenderer font = GuideItemTooltipRenderSupport.resolveFont(stack, mc.fontRenderer); + List lines = new ArrayList<>(GuideItemTooltipLines.build(tooltip, mc)); + List permutations = tooltip.getPermutations(); + int maxColumns = 11; + int maxRows = 4; + int visibleCount = Math.min(permutations.size(), maxColumns * maxRows); + int columns = Math.min(maxColumns, visibleCount); + int rows = Math.max(1, (visibleCount + columns - 1) / columns); + String spacer = tooltipSpacer(font, columns * 18); + int labelLine = Math.max(0, lines.size() - 1); + for (int row = 0; row < rows; row++) { + lines.add(spacer); + lines.add(spacer); + } + + LytRect bounds = resolveTooltipBounds(interaction); + TooltipLayout layout = computeHoveringTextLayout(lines, mouseX, mouseY, font, bounds); + drawHoveringText(lines, layout.mouseAnchorX(), layout.mouseAnchorY(), font); + + int firstItemY = layout.top() + (labelLine + 1) * 10; + GL11.glPushAttrib(GL11.GL_ENABLE_BIT | GL11.GL_COLOR_BUFFER_BIT | GL11.GL_LIGHTING_BIT); + GL11.glEnable(GL12.GL_RESCALE_NORMAL); + this.zLevel = 300.0F; + itemRender.zLevel = 300.0F; + try { + int activeIndex = tooltip.getActivePermutationIndex(); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_DEPTH_TEST); + GL11.glEnable(GL11.GL_ALPHA_TEST); + GL11.glColor4f(1F, 1F, 1F, 1F); + for (int index = 0; index < visibleCount; index++) { + if (index == activeIndex) { + int x = layout.textX() + (index % columns) * 18; + int y = firstItemY + (index / columns) * 20; + drawRect(x, y, x + 18, y + 18, 0x66555555); + } + } + GL11.glColor4f(1F, 1F, 1F, 1F); + GL11.glEnable(GL11.GL_DEPTH_TEST); + RenderHelper.enableGUIStandardItemLighting(); + this.zLevel = 400.0F; + itemRender.zLevel = 400.0F; + for (int index = 0; index < visibleCount; index++) { + ItemStack candidate = permutations.get(index); + if (candidate == null) continue; + int x = layout.textX() + (index % columns) * 18 + 1; + int y = firstItemY + (index / columns) * 20 + 1; + itemRender.renderItemAndEffectIntoGUI(mc.fontRenderer, mc.getTextureManager(), candidate, x, y); + } + if (visibleCount < permutations.size()) { + String hidden = "+" + (permutations.size() - visibleCount); + int x = layout.textX() + columns * 18 - font.getStringWidth(hidden) - 2; + int y = firstItemY + (rows - 1) * 20 + 3; + font.drawStringWithShadow(EnumChatFormatting.GRAY + hidden, x, y, 0xFFFFFF); + } + } finally { + itemRender.zLevel = 0.0F; + this.zLevel = 0.0F; + RenderHelper.disableStandardItemLighting(); + GL11.glPopAttrib(); + } + + for (int index = 0; index < visibleCount; index++) { + int x = layout.textX() + (index % columns) * 18 + 1; + int y = firstItemY + (index / columns) * 20 + 1; + if (isOver(x, y, 16, 16, mouseX, mouseY)) { + List candidateLines = GuideItemTooltipLines.build( + new NeiItemTooltip(permutations.get(index), tooltip.getHandler(), tooltip.getRecipeIndex()), + mc); + drawHoveringTextAtAdjustedPosition(candidateLines, mouseX, mouseY, font, bounds); + break; + } + } + } + + private static String tooltipSpacer(FontRenderer font, int width) { + int spaceWidth = Math.max(1, font.getCharWidth(' ')); + int count = Math.max(1, (width + spaceWidth - 1) / spaceWidth); + StringBuilder value = new StringBuilder(count); + value.append(" ".repeat(count)); + return value.toString(); + } + + private static boolean isOver(int x, int y, int width, int height, int mouseX, int mouseY) { + return mouseX >= x && mouseX < x + width && mouseY >= y && mouseY < y + height; + } + @Nullable private GuideTooltip resolveSceneBlockTooltip(LytGuidebookScene scene, int x, int y, int z) { String name = blockDisplayName(scene, x, y, z); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/SceneEditorScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/SceneEditorScreen.java index 29cec952..ed202d47 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/SceneEditorScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/SceneEditorScreen.java @@ -23,6 +23,7 @@ import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.gui.GuiTextField; +import net.minecraft.client.renderer.RenderHelper; import net.minecraft.client.renderer.Tessellator; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; @@ -36,6 +37,7 @@ import org.lwjgl.input.Keyboard; import org.lwjgl.input.Mouse; import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; import com.hfstudio.guidenh.client.command.GuideNhClientBridgeController; import com.hfstudio.guidenh.config.ModConfig; @@ -102,6 +104,7 @@ import com.hfstudio.guidenh.guide.internal.editor.preview.SceneEditorSnapModes; import com.hfstudio.guidenh.guide.internal.editor.preview.SceneEditorSnapService; import com.hfstudio.guidenh.guide.internal.item.RegionWandExporter; +import com.hfstudio.guidenh.guide.internal.recipe.NeiItemTooltip; import com.hfstudio.guidenh.guide.internal.screen.GuideIconButton; import com.hfstudio.guidenh.guide.internal.structure.GuideTextNbtCodec; import com.hfstudio.guidenh.guide.internal.tooltip.GuideItemTooltipLines; @@ -1761,11 +1764,102 @@ private void renderPreviewItemTooltip(ItemTooltip tooltip, int mouseX, int mouse return; } + if (tooltip instanceof NeiItemTooltip neiTooltip && neiTooltip.getPermutations() + .size() > 1) { + renderNeiPreviewItemTooltip(neiTooltip, mouseX, mouseY); + return; + } + List lines = GuideItemTooltipLines.build(tooltip, mc); FontRenderer font = GuideItemTooltipRenderSupport.resolveFont(stack, mc.fontRenderer); drawHoveringText(lines, mouseX, mouseY, font); } + private void renderNeiPreviewItemTooltip(NeiItemTooltip tooltip, int mouseX, int mouseY) { + FontRenderer font = GuideItemTooltipRenderSupport.resolveFont(tooltip.getStack(), mc.fontRenderer); + List lines = new ArrayList<>(GuideItemTooltipLines.build(tooltip, mc)); + List permutations = tooltip.getPermutations(); + int visibleCount = Math.min(permutations.size(), 44); + int columns = Math.min(11, visibleCount); + int rows = Math.max(1, (visibleCount + columns - 1) / columns); + String spacer = tooltipSpacer(font, columns * 18); + int labelLine = Math.max(0, lines.size() - 1); + for (int row = 0; row < rows; row++) { + lines.add(spacer); + lines.add(spacer); + } + int tooltipWidth = 0; + for (String line : lines) tooltipWidth = Math.max(tooltipWidth, font.getStringWidth(line)); + int tooltipHeight = lines.size() == 1 ? 8 : 8 + (lines.size() - 1) * 10; + int textX = mouseX + 12; + if (textX + tooltipWidth + 4 > width) textX = mouseX - tooltipWidth - 24; + int textY = mouseY - 12; + if (textY + tooltipHeight + 8 > height) textY = height - tooltipHeight - 8; + if (textY - 4 < 0) textY = 4; + drawHoveringText(lines, textX - 12, textY + 12, font); + int firstItemY = textY + (labelLine + 1) * 10; + GL11.glPushAttrib(GL11.GL_ENABLE_BIT | GL11.GL_COLOR_BUFFER_BIT | GL11.GL_LIGHTING_BIT); + GL11.glEnable(GL12.GL_RESCALE_NORMAL); + this.zLevel = 300.0F; + itemRender.zLevel = 300.0F; + try { + int activeIndex = tooltip.getActivePermutationIndex(); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_DEPTH_TEST); + GL11.glEnable(GL11.GL_ALPHA_TEST); + GL11.glColor4f(1F, 1F, 1F, 1F); + for (int index = 0; index < visibleCount; index++) { + if (index == activeIndex) { + int x = textX + (index % columns) * 18; + int y = firstItemY + (index / columns) * 20; + drawRect(x, y, x + 18, y + 18, 0x66555555); + } + } + GL11.glColor4f(1F, 1F, 1F, 1F); + GL11.glEnable(GL11.GL_DEPTH_TEST); + RenderHelper.enableGUIStandardItemLighting(); + this.zLevel = 400.0F; + itemRender.zLevel = 400.0F; + for (int index = 0; index < visibleCount; index++) { + ItemStack candidate = permutations.get(index); + if (candidate == null) continue; + itemRender.renderItemAndEffectIntoGUI( + mc.fontRenderer, + mc.getTextureManager(), + candidate, + textX + (index % columns) * 18 + 1, + firstItemY + (index / columns) * 20 + 1); + } + } finally { + itemRender.zLevel = 0.0F; + this.zLevel = 0.0F; + RenderHelper.disableStandardItemLighting(); + GL11.glPopAttrib(); + } + for (int index = 0; index < visibleCount; index++) { + int iconX = textX + (index % columns) * 18 + 1; + int iconY = firstItemY + (index / columns) * 20 + 1; + if (mouseX >= iconX && mouseX < iconX + 16 && mouseY >= iconY && mouseY < iconY + 16) { + drawHoveringText( + GuideItemTooltipLines.build( + new NeiItemTooltip(permutations.get(index), tooltip.getHandler(), tooltip.getRecipeIndex()), + mc), + mouseX, + mouseY, + font); + break; + } + } + } + + private static String tooltipSpacer(FontRenderer font, int width) { + int spaceWidth = Math.max(1, font.getCharWidth(' ')); + int count = Math.max(1, (width + spaceWidth - 1) / spaceWidth); + StringBuilder value = new StringBuilder(count); + value.append(" ".repeat(count)); + return value.toString(); + } + private void drawPreviewContentTooltip(ContentTooltip tooltip, int mouseX, int mouseY) { int pad = 4; int maxWidth = Math.max(80, (this.width * 4) / 5); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java index b92a6a0f..ffa3fc94 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java @@ -2,8 +2,9 @@ import java.util.ArrayList; import java.util.Collections; -import java.util.IdentityHashMap; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; @@ -69,7 +70,9 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { .copy(); } - boolean hasHandlerFilter = ph.handlerName != null || ph.handlerId != null || ph.handlerOrder >= 0; + boolean hasHandlerFilter = ph.handlerName != null || ph.handlerId != null + || ph.handlerOrder >= 0 + || !ph.handlerWhitelist.isEmpty(); boolean hasRecipeFilter = !ph.inputExpr.isEmpty() || !ph.outputExpr.isEmpty(); int limit = ph.limit; boolean usageQuery = ph.usageQuery; @@ -77,7 +80,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { // NEI handler path List rawHandlers = usageQuery ? RecipeCache.getUsageHandlers(targetStack) : RecipeCache.getCraftingHandlers(targetStack); - if (!usageQuery && hasHandlerFilter) { + if (!usageQuery && (ph.handlerId != null || ph.handlerName != null || !ph.handlerWhitelist.isEmpty())) { List usage = RecipeCache.getUsageHandlers(targetStack); if (!usage.isEmpty()) { if (rawHandlers.isEmpty()) { @@ -85,9 +88,11 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } else { List merged = new ArrayList<>(rawHandlers.size() + usage.size()); merged.addAll(rawHandlers); - IdentityHashMap seen = new IdentityHashMap<>(merged.size()); - for (Object h : rawHandlers) seen.put(h, Boolean.TRUE); - for (Object h : usage) if (seen.put(h, Boolean.TRUE) == null) merged.add(h); + Set seen = new LinkedHashSet<>(); + for (Object h : rawHandlers) seen.add(NeiRecipeLookup.handlerFingerprint(h)); + for (Object h : usage) { + if (seen.add(NeiRecipeLookup.handlerFingerprint(h))) merged.add(h); + } rawHandlers = merged; } } @@ -116,6 +121,11 @@ public List readIngredientSlots(Object h, int ri) { return NeiRecipeLookup.readIngredientSlots(h, ri); } + @Override + public List readOtherSlots(Object h, int ri) { + return NeiRecipeLookup.readOtherSlots(h, ri); + } + @Override public @Nullable NeiRecipeLookup.Slot readResultSlot(Object h, int ri) { return NeiRecipeLookup.readResultSlot(h, ri); @@ -131,6 +141,7 @@ public List readIngredientSlots(Object h, int ri) { RecipeCompiler.effectiveHandlerBlacklist(ph.handlerBlacklist)); if (!handlers.isEmpty()) { List boxes = new ArrayList<>(); + Set recipeFingerprints = new LinkedHashSet<>(); for (int hi = 0; hi < handlers.size() && boxes.size() < limit; hi++) { Object handler = handlers.get(hi); int num = GuideNhIntegrationRegistry.global() @@ -138,9 +149,20 @@ public List readIngredientSlots(Object h, int ri) { int recipeStart = Math.max(ph.recipeIndex, 0); int recipeEnd = ph.recipeIndex >= 0 ? Math.min(num, ph.recipeIndex + 1) : num; for (int ri = recipeStart; ri < recipeEnd && boxes.size() < limit; ri++) { - if (hasRecipeFilter - && !RecipeCompiler.recipeMatches(handler, ri, ph.inputExpr, ph.outputExpr, recipeAccess)) + NeiRecipeLookup.Entry entry = new NeiRecipeLookup.Entry( + "", + "", + recipeAccess.readIngredientSlots(handler, ri), + recipeAccess.readOtherSlots(handler, ri), + recipeAccess.readResultSlot(handler, ri)); + if (!usageQuery && !RecipeCompiler.handlerTargetMatches( + entry, + ph.ref, + ph.handlerId != null || ph.handlerName != null || !ph.handlerWhitelist.isEmpty())) { continue; + } + if (hasRecipeFilter && !RecipeCompiler.recipeMatches(entry, ph.inputExpr, ph.outputExpr)) continue; + if (!recipeFingerprints.add(NeiRecipeLookup.recipeFingerprint(handler, entry))) continue; boxes.add(new LytNeiRecipeBox(handler, ri, !usageQuery)); } } @@ -152,6 +174,14 @@ public List readIngredientSlots(Object h, int ri) { showFallback(ctx, ph, "Recipe index " + ph.recipeIndex + " not found for " + ph.idStr); return; } + if (hasHandlerFilter) { + if (ph.fallbackText != null && !ph.fallbackText.isEmpty()) { + showFallback(ctx, ph, "No recipe found for " + ph.idStr + " with the requested handler"); + } else { + GuideDebugLog.debug("Recipe handler matched but exposed no target recipe for {}", ph.idStr); + } + return; + } } else if (hasHandlerFilter) { if (ph.fallbackText != null && !ph.fallbackText.isEmpty()) { String handlerPart = ""; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/recipe/LytNeiRecipeBox.java b/src/main/java/com/hfstudio/guidenh/guide/internal/recipe/LytNeiRecipeBox.java index fba806a7..40b19cb6 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/recipe/LytNeiRecipeBox.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/recipe/LytNeiRecipeBox.java @@ -16,6 +16,7 @@ import com.hfstudio.guidenh.guide.color.ColorUtils; import com.hfstudio.guidenh.guide.document.LytRect; import com.hfstudio.guidenh.guide.document.block.LytBlock; +import com.hfstudio.guidenh.guide.document.interaction.DocumentDragTarget; import com.hfstudio.guidenh.guide.document.interaction.GuideTooltip; import com.hfstudio.guidenh.guide.document.interaction.InteractiveElement; import com.hfstudio.guidenh.guide.document.interaction.TextTooltip; @@ -31,6 +32,8 @@ import com.hfstudio.guidenh.integration.api.RecipeSlot; import com.hfstudio.guidenh.integration.nei.GuideScreenNeiBridge.EditorAccess; import com.hfstudio.guidenh.integration.nei.NeiGuideNavigation; +import com.hfstudio.guidenh.integration.nei.NeiRecipeLookup; +import com.hfstudio.guidenh.integration.nei.NeiRecipePermutationController; import com.hfstudio.guidenh.integration.neicustomdiagram.NeiCustomDiagramBridge; import lombok.Getter; @@ -53,7 +56,7 @@ * "recipe pool" category; hovering a slot yields an {@link NeiItemTooltip} carrying the extra * tooltip lines that NEI's tab normally contributes. The icon itself has no tooltip. */ -public class LytNeiRecipeBox extends LytBlock implements InteractiveElement { +public class LytNeiRecipeBox extends LytBlock implements InteractiveElement, DocumentDragTarget { public static final int FRAME_BORDER = 4; public static final int ICON_SIZE = 8; @@ -96,6 +99,12 @@ public class LytNeiRecipeBox extends LytBlock implements InteractiveElement { */ private final boolean otherStacksBroken; private boolean actionButtonHovered; + private long slotCacheRevision = Long.MIN_VALUE; + private boolean slotCacheSkippedOthers; + private List cachedIngredients = List.of(); + private List cachedOthers = List.of(); + private @Nullable RecipeSlot cachedResult; + private boolean slotCacheResultAvailable; public LytNeiRecipeBox(Object handler, int recipeIndex) { this(handler, recipeIndex, true); @@ -210,6 +219,7 @@ private void renderRecipeBody(int bodyX, int bodyY, RenderContext context) { bodyAbs.width(), bodyAbs.height()); } else { + RecipeSlots slots = recipeSlots(GuideNhIntegrationRegistry.global()); NeiHandlerRenderer.render( handler, recipeIndex, @@ -221,10 +231,36 @@ private void renderRecipeBody(int bodyX, int bodyY, RenderContext context) { bodyHeight, -1, -1, - otherStacksBroken); + otherStacksBroken, + slots.ingredients, + slots.others, + slots.result, + slotCacheResultAvailable); } } + private RecipeSlots recipeSlots(GuideNhIntegrationRegistry registry) { + long revision = NeiRecipePermutationController.revision(handler, recipeIndex); + if (revision >= 0 && revision == slotCacheRevision && slotCacheSkippedOthers == otherStacksBroken) { + return new RecipeSlots(cachedIngredients, cachedOthers, cachedResult); + } + List ingredients = registry.readRecipeIngredientSlots(handler, recipeIndex); + List others = otherStacksBroken ? List.of() : registry.readRecipeOtherSlots(handler, recipeIndex); + RecipeSlot result = registry.readRecipeResultSlot(handler, recipeIndex); + long updatedRevision = NeiRecipePermutationController.revision(handler, recipeIndex); + if (updatedRevision >= 0) { + cachedIngredients = ingredients; + cachedOthers = others; + cachedResult = result; + slotCacheResultAvailable = true; + slotCacheRevision = updatedRevision; + slotCacheSkippedOthers = otherStacksBroken; + } + return new RecipeSlots(ingredients, others, result); + } + + private record RecipeSlots(List ingredients, List others, @Nullable RecipeSlot result) {} + private void warnRecipeRenderFailure(Throwable t) { String key = handler.getClass() .getName() + ":" @@ -389,12 +425,13 @@ public Optional getTooltip(float mx, float my) { return Optional.of(customDiagramTooltip); } - ItemStack hit = findSlotHit(registry.readRecipeIngredientSlots(handler, recipeIndex), bodyX, bodyY, px, py); + RecipeSlots slots = recipeSlots(registry); + ItemStack hit = findSlotHit(slots.ingredients, bodyX, bodyY, px, py); if (hit == null) { - hit = findSlotHit(registry.readRecipeOtherSlots(handler, recipeIndex), bodyX, bodyY, px, py); + hit = findSlotHit(slots.others, bodyX, bodyY, px, py); } if (hit == null) { - RecipeSlot result = registry.readRecipeResultSlot(handler, recipeIndex); + RecipeSlot result = slots.result; if (result != null) { ItemStack shown = pickVisibleStack(result); if (shown != null && isOver(bodyX + result.x(), bodyY + result.y(), SLOT_SIZE, SLOT_SIZE, px, py)) { @@ -403,7 +440,32 @@ public Optional getTooltip(float mx, float my) { } } if (hit == null) return Optional.empty(); - return Optional.of(new NeiItemTooltip(hit, handler, recipeIndex)); + NeiRecipeLookup.PositionedStackHit positionedStackHit = NeiRecipeLookup + .findPositionedStackHit(handler, recipeIndex, px - bodyX, py - bodyY); + Object positionedStack = positionedStackHit == null ? null : positionedStackHit.positionedStack; + boolean input = positionedStackHit != null && positionedStackHit.ingredient; + List permutations = input && NeiRecipeLookup.showCycledIngredientsTooltip() + ? NeiRecipeLookup.filteredPermutations(positionedStack) + : List.of(); + if (permutations.size() <= 1) permutations = List.of(); + return Optional.of( + new NeiItemTooltip( + hit, + handler, + recipeIndex, + NeiRecipeLookup.positionedTooltip(handler, positionedStack, input), + permutations, + NeiRecipeLookup.acceptsLabel(positionedStack))); + } + + @Override + public boolean scroll(int documentX, int documentY, int wheelDelta) { + int bodyX = bounds.x() + FRAME_BORDER + BODY_HORIZONTAL_BLEED; + int bodyTop = bounds.y() + FRAME_BORDER + titleHeight + BODY_MARGIN + bodyTopInset; + int bodyY = bodyTop + bodyYShift; + if (!isOver(bodyX, bodyY, bodyWidth, bodyHeight, documentX, documentY)) return false; + return NeiRecipeLookup + .scrollPermutation(handler, recipeIndex, documentX - bodyX, documentY - bodyY, wheelDelta); } @Override @@ -431,6 +493,9 @@ public boolean mouseClicked(GuideUiHost screen, int x, int y, int button, boolea } public static @Nullable ItemStack pickVisibleStack(RecipeSlot s) { + if (s != null && s.currentStack() != null && s.currentStack().stackSize > 0) { + return s.currentStack(); + } if (s == null || s.stacks() .isEmpty()) return null; for (int i = 0, n = s.stacks() diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/recipe/NeiAnimationTicker.java b/src/main/java/com/hfstudio/guidenh/guide/internal/recipe/NeiAnimationTicker.java index c1713bae..b7ef5104 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/recipe/NeiAnimationTicker.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/recipe/NeiAnimationTicker.java @@ -3,6 +3,7 @@ import java.util.WeakHashMap; import com.hfstudio.guidenh.integration.api.GuideNhIntegrationRegistry; +import com.hfstudio.guidenh.integration.nei.NeiRecipePermutationController; import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.eventhandler.SubscribeEvent; @@ -21,37 +22,50 @@ public class NeiAnimationTicker { private NeiAnimationTicker() {} + private static final WeakHashMap UPDATE_CAPABILITY = new WeakHashMap<>(); + public static void ensureUpdating(Object handler) { - if (!GuideNhIntegrationRegistry.global() - .canUpdateRecipeAnimation(handler)) return; + if (handler == null) return; synchronized (TRACKED) { - TRACKED.put(handler, Boolean.TRUE); + Boolean cachedCapability = UPDATE_CAPABILITY.get(handler); + if (cachedCapability == null) { + cachedCapability = GuideNhIntegrationRegistry.global() + .canUpdateRecipeAnimation(handler); + UPDATE_CAPABILITY.put(handler, cachedCapability); + } if (!registered) { registered = true; FMLCommonHandler.instance() .bus() .register(new NeiAnimationTicker()); } + if (cachedCapability) { + TRACKED.put(handler, Boolean.TRUE); + } } } public static void clear() { synchronized (TRACKED) { TRACKED.clear(); + UPDATE_CAPABILITY.clear(); } + NeiRecipePermutationController.clear(); } @SubscribeEvent public void onClientTick(TickEvent.ClientTickEvent event) { if (event.phase != TickEvent.Phase.END) return; + NeiRecipePermutationController.tick(); Object[] snapshot; synchronized (TRACKED) { + if (TRACKED.isEmpty()) return; snapshot = TRACKED.keySet() .toArray(); } + GuideNhIntegrationRegistry registry = GuideNhIntegrationRegistry.global(); for (Object o : snapshot) { - GuideNhIntegrationRegistry.global() - .updateRecipeAnimation(o); + registry.updateRecipeAnimation(o); } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/recipe/NeiHandlerRenderer.java b/src/main/java/com/hfstudio/guidenh/guide/internal/recipe/NeiHandlerRenderer.java index 2d9cffb0..f59af381 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/recipe/NeiHandlerRenderer.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/recipe/NeiHandlerRenderer.java @@ -56,6 +56,28 @@ private NeiHandlerRenderer() {} */ public static @Nullable ItemStack render(Object handler, int recipeIndex, int screenX, int screenY, int clipX, int clipY, int clipWidth, int clipHeight, int mouseX, int mouseY, boolean skipForeground) { + return render( + handler, + recipeIndex, + screenX, + screenY, + clipX, + clipY, + clipWidth, + clipHeight, + mouseX, + mouseY, + skipForeground, + null, + null, + null, + false); + } + + static @Nullable ItemStack render(Object handler, int recipeIndex, int screenX, int screenY, int clipX, int clipY, + int clipWidth, int clipHeight, int mouseX, int mouseY, boolean skipForeground, + @Nullable List cachedIngredients, @Nullable List cachedOthers, + @Nullable RecipeSlot cachedResult, boolean cachedResultAvailable) { GuideNhIntegrationRegistry registry = GuideNhIntegrationRegistry.global(); if (handler == null || !registry.canRenderRecipeHandler(handler)) return null; @@ -89,56 +111,94 @@ private NeiHandlerRenderer() {} } // Phase 2: draw every positioned stack on top. - ItemStack hovered = null; - hovered = drawSlots( - registry.readRecipeIngredientSlots(handler, recipeIndex), - screenX, - screenY, - mouseX, - mouseY, - hovered); - if (!skipForeground) { - hovered = drawSlots( - registry.readRecipeOtherSlots(handler, recipeIndex), - screenX, - screenY, - mouseX, - mouseY, - hovered); - } + List ingredients = cachedIngredients != null ? cachedIngredients + : registry.readRecipeIngredientSlots(handler, recipeIndex); + List others = skipForeground ? List.of() + : cachedOthers != null ? cachedOthers : registry.readRecipeOtherSlots(handler, recipeIndex); + RecipeSlot result = cachedResultAvailable ? cachedResult : registry.readRecipeResultSlot(handler, recipeIndex); + return drawSlotBatch(ingredients, others, result, screenX, screenY, mouseX, mouseY); + } - RecipeSlot result = registry.readRecipeResultSlot(handler, recipeIndex); - if (result != null) { - ItemStack shown = pickVisibleStack(result); - if (shown != null) { - drawStackWithCount(shown, screenX + result.x(), screenY + result.y()); - if (isOver(screenX + result.x(), screenY + result.y(), mouseX, mouseY)) { - hovered = shown; - } + private static @Nullable ItemStack drawSlotBatch(List ingredients, List others, + @Nullable RecipeSlot result, int screenX, int screenY, int mouseX, int mouseY) { + GL11.glPushAttrib(GL11.GL_ENABLE_BIT | GL11.GL_CURRENT_BIT | GL11.GL_COLOR_BUFFER_BIT | GL11.GL_LIGHTING_BIT); + ItemStack hovered = null; + Minecraft mc = Minecraft.getMinecraft(); + try { + GL11.glDisable(GL11.GL_BLEND); + ColorUtils.applyGlColor(ColorUtils.WHITE.getColor()); + RenderHelper.enableGUIStandardItemLighting(); + OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240f, 240f); + OpenGlHelper.setActiveTexture(OpenGlHelper.defaultTexUnit); + GL11.glEnable(GL11.GL_LIGHTING); + GL11.glEnable(GL11.GL_NORMALIZE); + GL11.glEnable(GL11.GL_DEPTH_TEST); + GL11.glEnable(GL11.GL_ALPHA_TEST); + ITEM_RENDERER.zLevel = 100f; + hovered = drawSlotBatchGroup(ingredients, screenX, screenY, mouseX, mouseY, mc, hovered); + hovered = drawSlotBatchGroup(others, screenX, screenY, mouseX, mouseY, mc, hovered); + if (result != null) { + hovered = drawSlotBatchSlot(result, screenX, screenY, mouseX, mouseY, mc, hovered); } + } finally { + ITEM_RENDERER.zLevel = 0f; + RenderHelper.disableStandardItemLighting(); + GL11.glPopAttrib(); + OpenGlHelper.setActiveTexture(OpenGlHelper.defaultTexUnit); + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_DEPTH_TEST); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + ColorUtils.applyGlColor(ColorUtils.WHITE.getColor()); } return hovered; } - private static @Nullable ItemStack drawSlots(List slots, int screenX, int screenY, int mouseX, - int mouseY, @Nullable ItemStack currentHovered) { + private static @Nullable ItemStack drawSlotBatchGroup(List slots, int screenX, int screenY, int mouseX, + int mouseY, Minecraft mc, @Nullable ItemStack currentHovered) { ItemStack hovered = currentHovered; - for (RecipeSlot s : slots) { - ItemStack shown = pickVisibleStack(s); - if (shown == null) continue; - drawStackWithCount(shown, screenX + s.x(), screenY + s.y()); - if (isOver(screenX + s.x(), screenY + s.y(), mouseX, mouseY)) { - hovered = shown; - } + for (RecipeSlot slot : slots) { + hovered = drawSlotBatchSlot(slot, screenX, screenY, mouseX, mouseY, mc, hovered); } return hovered; } + private static @Nullable ItemStack drawSlotBatchSlot(RecipeSlot slot, int screenX, int screenY, int mouseX, + int mouseY, Minecraft mc, @Nullable ItemStack currentHovered) { + ItemStack shown = pickVisibleStack(slot); + if (shown == null) return currentHovered; + try { + ITEM_RENDERER.renderItemAndEffectIntoGUI( + mc.fontRenderer, + mc.getTextureManager(), + shown, + screenX + slot.x(), + screenY + slot.y()); + if (shown.stackSize > 0) { + ITEM_RENDERER.renderItemOverlayIntoGUI( + mc.fontRenderer, + mc.getTextureManager(), + shown, + screenX + slot.x(), + screenY + slot.y()); + } + } catch (Throwable t) { + GuideDisplayItemStacks.warnRenderFailure("NeiHandlerRenderer", shown, t); + } + return isOver(screenX + slot.x(), screenY + slot.y(), mouseX, mouseY) ? shown : currentHovered; + } + public static @Nullable ItemStack pickVisibleStack(RecipeSlot s) { - if (s == null || s.stacks() == null - || s.stacks() - .isEmpty()) + if (s == null) { return null; + } + ItemStack current = s.currentStack(); + if (current != null && current.stackSize > 0) { + return current; + } + if (s.stacks() == null || s.stacks() + .isEmpty()) return null; ItemStack zeroCountFallback = null; for (int i = 0, n = s.stacks() .size(); i < n; i++) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/recipe/NeiItemTooltip.java b/src/main/java/com/hfstudio/guidenh/guide/internal/recipe/NeiItemTooltip.java index 314be376..602afdce 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/recipe/NeiItemTooltip.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/recipe/NeiItemTooltip.java @@ -4,11 +4,14 @@ import java.util.List; import net.minecraft.item.ItemStack; +import net.minecraft.util.EnumChatFormatting; import com.hfstudio.guidenh.guide.document.interaction.ItemTooltip; import com.hfstudio.guidenh.guide.document.interaction.ItemTooltipAppender; import com.hfstudio.guidenh.integration.api.GuideNhIntegrationRegistry; +import codechicken.nei.NEIClientUtils; + /** * An {@link ItemTooltip} that lets the NEI handler contribute extra lines via * {@code IRecipeHandler.handleItemTooltip}. The GuideScreen renderer checks for this subtype and @@ -18,26 +21,97 @@ public class NeiItemTooltip extends ItemTooltip implements ItemTooltipAppender { private final Object handler; private final int recipeIndex; + private final List positionedTooltip; + private final List permutations; + private final int activePermutationIndex; + private final String acceptsLabel; public NeiItemTooltip(ItemStack stack, Object handler, int recipeIndex) { + this(stack, handler, recipeIndex, null); + } + + public NeiItemTooltip(ItemStack stack, Object handler, int recipeIndex, List positionedTooltip) { + this(stack, handler, recipeIndex, positionedTooltip, List.of(), null); + } + + public NeiItemTooltip(ItemStack stack, Object handler, int recipeIndex, List positionedTooltip, + List permutations, String acceptsLabel) { super(stack); this.handler = handler; this.recipeIndex = recipeIndex; + this.positionedTooltip = copyStrings(positionedTooltip); + this.permutations = copyStacks(permutations); + this.activePermutationIndex = findActivePermutationIndex(stack, this.permutations); + this.acceptsLabel = acceptsLabel; } - /** Append handler-specific lines to {@code base} using a fresh temp list as NEI expects. */ + /** + * Passes the already-built vanilla tooltip to the handler. NEI handlers can both append lines + * and update existing indexed lines, so using an empty temporary list loses handler data. + */ public void appendExtraLines(List base) { if (base == null) return; - List temp = new ArrayList<>(); GuideNhIntegrationRegistry.global() - .appendRecipeItemTooltip(handler, getStack(), temp, recipeIndex); - for (String line : temp) { - if (line != null && !line.isEmpty() && !base.contains(line)) base.add(line); + .appendRecipeItemTooltip(handler, getStack(), base, recipeIndex); + for (String line : positionedTooltip) { + if (line != null && !line.isEmpty()) base.add(line); + } + if (permutations.size() > 1) { + String label = acceptsLabel == null || acceptsLabel.isEmpty() ? NEIClientUtils.translate("recipe.accepts") + : acceptsLabel; + base.add(EnumChatFormatting.GRAY + label + ":"); + } + } + + public List getPermutations() { + return permutations; + } + + public int getActivePermutationIndex() { + return activePermutationIndex; + } + + private static int findActivePermutationIndex(ItemStack active, List candidates) { + if (active == null) return -1; + for (int index = 0; index < candidates.size(); index++) { + ItemStack candidate = candidates.get(index); + if (candidate != null && NEIClientUtils.areStacksSameType(candidate, active)) return index; } + return -1; + } + + public Object getHandler() { + return handler; + } + + public int getRecipeIndex() { + return recipeIndex; + } + + public String getAcceptsLabel() { + return acceptsLabel; } @Override public void appendTooltipLines(List lines) { appendExtraLines(lines); } + + private static List copyStrings(List values) { + if (values == null || values.isEmpty()) return List.of(); + ArrayList copy = new ArrayList<>(values.size()); + for (String value : values) { + if (value != null && !value.isEmpty()) copy.add(value); + } + return copy.isEmpty() ? List.of() : List.copyOf(copy); + } + + private static List copyStacks(List values) { + if (values == null || values.isEmpty()) return List.of(); + ArrayList copy = new ArrayList<>(values.size()); + for (ItemStack value : values) { + if (value != null) copy.add(value); + } + return copy.isEmpty() ? List.of() : List.copyOf(copy); + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/screen/GuideNavBar.java b/src/main/java/com/hfstudio/guidenh/guide/internal/screen/GuideNavBar.java index efbb71e5..ba3a422a 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/screen/GuideNavBar.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/screen/GuideNavBar.java @@ -550,7 +550,17 @@ private boolean renderRow(Minecraft mc, FontRenderer fr, Row row, int rowY, int GuidePageIcon icon = displayRow.icon(); if (icon != null) { int iconY = rowY + (ROW_H - ICON_SIZE) / 2; - drawMiniIcon(mc, icon, textX, iconY); + if (sticky) { + GL11.glPushAttrib(GL11.GL_DEPTH_BUFFER_BIT); + try { + GL11.glDepthFunc(GL11.GL_ALWAYS); + drawMiniIcon(mc, icon, textX, iconY); + } finally { + GL11.glPopAttrib(); + } + } else { + drawMiniIcon(mc, icon, textX, iconY); + } textX += ICON_SIZE + 2; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/syntax/BuiltinSyntaxContributor.java b/src/main/java/com/hfstudio/guidenh/guide/internal/syntax/BuiltinSyntaxContributor.java index 85083a1d..bda6e677 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/syntax/BuiltinSyntaxContributor.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/syntax/BuiltinSyntaxContributor.java @@ -590,17 +590,20 @@ public void contribute(SyntaxSink sink) { AttributeSyntax.of("flip", SyntaxValueKind.STRING), AttributeSyntax.of("offsetX", SyntaxValueKind.INT), AttributeSyntax.of("offsetY", SyntaxValueKind.INT), - AttributeSyntax.of("offsetZ", SyntaxValueKind.INT)); + AttributeSyntax.of("offsetZ", SyntaxValueKind.INT), + AttributeSyntax.of("locked", SyntaxValueKind.BOOLEAN)); sink.attributes( "Tier", AttributeSyntax.of("value", SyntaxValueKind.INT), - AttributeSyntax.of("expr", SyntaxValueKind.INT)); + AttributeSyntax.of("expr", SyntaxValueKind.INT), + AttributeSyntax.of("locked", SyntaxValueKind.BOOLEAN)); sink.attributes( "Channel", AttributeSyntax.of("name", SyntaxValueKind.STRING), AttributeSyntax.of("id", SyntaxValueKind.STRING), AttributeSyntax.of("value", SyntaxValueKind.INT), - AttributeSyntax.of("expr", SyntaxValueKind.INT)); + AttributeSyntax.of("expr", SyntaxValueKind.INT), + AttributeSyntax.of("locked", SyntaxValueKind.BOOLEAN)); sink.attributes("Facing", AttributeSyntax.of("value", SyntaxValueKind.STRING)); sink.attributes("Rotation", AttributeSyntax.of("value", SyntaxValueKind.STRING)); sink.attributes("Flip", AttributeSyntax.of("value", SyntaxValueKind.STRING)); @@ -1027,6 +1030,7 @@ private static void registerStructureLibAttributes(SyntaxSink sink) { AttributeSyntax.of("facing", SyntaxValueKind.STRING), AttributeSyntax.of("rotation", SyntaxValueKind.STRING), AttributeSyntax.of("flip", SyntaxValueKind.STRING), + AttributeSyntax.of("locked", SyntaxValueKind.BOOLEAN), AttributeSyntax.of("gtActiveController", SyntaxValueKind.BOOLEAN), AttributeSyntax.of("gtPlaceHatches", SyntaxValueKind.BOOLEAN), AttributeSyntax.of("channelName", SyntaxValueKind.STRING), @@ -1041,12 +1045,14 @@ private static void registerStructureLibAttributes(SyntaxSink sink) { "Tier", AttributeSyntax.of("value", SyntaxValueKind.STRING), AttributeSyntax.of("expr", SyntaxValueKind.STRING), - AttributeSyntax.of("tier", SyntaxValueKind.INT)); + AttributeSyntax.of("tier", SyntaxValueKind.INT), + AttributeSyntax.of("locked", SyntaxValueKind.BOOLEAN)); sink.attributes( "Channel", AttributeSyntax.of("value", SyntaxValueKind.STRING), AttributeSyntax.of("expr", SyntaxValueKind.STRING), - AttributeSyntax.of("name", SyntaxValueKind.STRING)); + AttributeSyntax.of("name", SyntaxValueKind.STRING), + AttributeSyntax.of("locked", SyntaxValueKind.BOOLEAN)); } private static void registerAttributeCorrections(SyntaxSink sink) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/welcome/GuideWelcomeScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/welcome/GuideWelcomeScreen.java index 54635902..bcd856b0 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/welcome/GuideWelcomeScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/welcome/GuideWelcomeScreen.java @@ -2,6 +2,7 @@ import java.awt.Desktop; import java.net.URI; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Optional; @@ -11,6 +12,7 @@ import net.minecraft.client.gui.GuiConfirmOpenLink; import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.gui.GuiYesNoCallback; +import net.minecraft.client.renderer.RenderHelper; import net.minecraft.client.resources.I18n; import net.minecraft.item.ItemStack; import net.minecraft.util.ResourceLocation; @@ -20,6 +22,7 @@ import org.lwjgl.input.Keyboard; import org.lwjgl.input.Mouse; import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; import com.hfstudio.guidenh.ClientProxy; import com.hfstudio.guidenh.GuideNH; @@ -44,6 +47,7 @@ import com.hfstudio.guidenh.guide.internal.GuideScreen; import com.hfstudio.guidenh.guide.internal.MutableGuide; import com.hfstudio.guidenh.guide.internal.host.LytHost; +import com.hfstudio.guidenh.guide.internal.recipe.NeiItemTooltip; import com.hfstudio.guidenh.guide.internal.screen.GuideIconButton; import com.hfstudio.guidenh.guide.internal.tooltip.GuideItemTooltipLines; import com.hfstudio.guidenh.guide.internal.tooltip.GuideItemTooltipRenderSupport; @@ -675,11 +679,101 @@ private void drawItemTooltip(ItemTooltip tooltip, int mouseX, int mouseY) { if (stack == null) { return; } + if (tooltip instanceof NeiItemTooltip neiTooltip && neiTooltip.getPermutations() + .size() > 1) { + drawNeiItemTooltip(neiTooltip, mouseX, mouseY); + return; + } List lines = GuideItemTooltipLines.build(tooltip, mc); FontRenderer font = GuideItemTooltipRenderSupport.resolveFont(stack, fontRendererObj); drawHoveringText(lines, mouseX, mouseY, font); } + private void drawNeiItemTooltip(NeiItemTooltip tooltip, int mouseX, int mouseY) { + FontRenderer font = GuideItemTooltipRenderSupport.resolveFont(tooltip.getStack(), fontRendererObj); + List lines = new ArrayList<>(GuideItemTooltipLines.build(tooltip, mc)); + List permutations = tooltip.getPermutations(); + int visibleCount = Math.min(permutations.size(), 44); + int columns = Math.min(11, visibleCount); + int rows = Math.max(1, (visibleCount + columns - 1) / columns); + String spacer = tooltipSpacer(font, columns * 18); + int labelLine = Math.max(0, lines.size() - 1); + for (int row = 0; row < rows; row++) { + lines.add(spacer); + lines.add(spacer); + } + int tooltipWidth = 0; + for (String line : lines) tooltipWidth = Math.max(tooltipWidth, font.getStringWidth(line)); + int tooltipHeight = lines.size() == 1 ? 8 : 8 + (lines.size() - 1) * 10; + int textX = mouseX + 12; + if (textX + tooltipWidth + 4 > width) textX = mouseX - tooltipWidth - 24; + int textY = mouseY - 12; + if (textY + tooltipHeight + 8 > height) textY = height - tooltipHeight - 8; + if (textY - 4 < 0) textY = 4; + drawHoveringText(lines, textX - 12, textY + 12, font); + int firstItemY = textY + (labelLine + 1) * 10; + GL11.glPushAttrib(GL11.GL_ENABLE_BIT | GL11.GL_COLOR_BUFFER_BIT | GL11.GL_LIGHTING_BIT); + GL11.glEnable(GL12.GL_RESCALE_NORMAL); + zLevel = 300F; + itemRender.zLevel = 300F; + try { + int activeIndex = tooltip.getActivePermutationIndex(); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_DEPTH_TEST); + GL11.glEnable(GL11.GL_ALPHA_TEST); + GL11.glColor4f(1F, 1F, 1F, 1F); + for (int index = 0; index < visibleCount; index++) { + if (index == activeIndex) { + int x = textX + (index % columns) * 18; + int y = firstItemY + (index / columns) * 20; + drawRect(x, y, x + 18, y + 18, 0x66555555); + } + } + GL11.glColor4f(1F, 1F, 1F, 1F); + GL11.glEnable(GL11.GL_DEPTH_TEST); + RenderHelper.enableGUIStandardItemLighting(); + zLevel = 400F; + itemRender.zLevel = 400F; + for (int index = 0; index < visibleCount; index++) { + ItemStack candidate = permutations.get(index); + if (candidate == null) continue; + itemRender.renderItemAndEffectIntoGUI( + mc.fontRenderer, + mc.getTextureManager(), + candidate, + textX + (index % columns) * 18 + 1, + firstItemY + (index / columns) * 20 + 1); + } + } finally { + itemRender.zLevel = 0F; + zLevel = 0F; + RenderHelper.disableStandardItemLighting(); + GL11.glPopAttrib(); + } + for (int index = 0; index < visibleCount; index++) { + int iconX = textX + (index % columns) * 18 + 1; + int iconY = firstItemY + (index / columns) * 20 + 1; + if (mouseX >= iconX && mouseX < iconX + 16 && mouseY >= iconY && mouseY < iconY + 16) { + drawHoveringText( + GuideItemTooltipLines.build( + new NeiItemTooltip(permutations.get(index), tooltip.getHandler(), tooltip.getRecipeIndex()), + mc), + mouseX, + mouseY, + font); + break; + } + } + } + + private static String tooltipSpacer(FontRenderer font, int width) { + int spaceWidth = Math.max(1, font.getCharWidth(' ')); + int count = Math.max(1, (width + spaceWidth - 1) / spaceWidth); + StringBuilder value = new StringBuilder(count); + value.append(" ".repeat(count)); + return value.toString(); + } + private void drawContentTooltip(ContentTooltip tooltip, int mouseX, int mouseY) { int padding = 4; int left = padding; diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java b/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java index e96da659..ef374e64 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java @@ -1572,6 +1572,9 @@ public void setStructureLibSelectionChangeListener( private void setStructureLibCurrentTierInternal(int structureLibCurrentTier, boolean notifyListener) { StructureLibSceneMetadata.TierData tierData = getStructureLibTierData(); + if (tierData != null && tierData.isLocked()) { + return; + } int previousValue = this.structureLibCurrentTier; if (tierData == null) { this.structureLibCurrentTier = Math.max(1, structureLibCurrentTier); @@ -1641,6 +1644,9 @@ private void setStructureLibChannelValueInternal(String channelId, int value, bo if (normalized == null) { return; } + if (channelData != null && channelData.isLocked()) { + return; + } int previousValue = getStructureLibChannelValue(normalized); int nextValue; if (channelData == null) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/StructureLibSceneBinding.java b/src/main/java/com/hfstudio/guidenh/guide/scene/StructureLibSceneBinding.java index be472c43..9011821e 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/StructureLibSceneBinding.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/StructureLibSceneBinding.java @@ -1,6 +1,7 @@ package com.hfstudio.guidenh.guide.scene; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.Map; import java.util.Objects; import java.util.function.Consumer; @@ -11,6 +12,7 @@ import com.hfstudio.guidenh.integration.structurelib.StructureLibImportResult; import com.hfstudio.guidenh.integration.structurelib.StructureLibPreviewSelection; import com.hfstudio.guidenh.integration.structurelib.StructureLibSceneMetadata; +import com.hfstudio.guidenh.integration.structurelib.StructureLibSceneOptions; import lombok.Getter; @@ -29,6 +31,8 @@ public class StructureLibSceneBinding { @Getter private int currentTier = StructureLibPreviewSelection.DEFAULT_MASTER_TIER; private final LinkedHashMap channelOverrides = new LinkedHashMap<>(); + private boolean tierLocked; + private final LinkedHashSet lockedChannels = new LinkedHashSet<>(); @Nullable private Consumer selectionChangeListener; @@ -65,7 +69,7 @@ public void setMetadata(@Nullable StructureLibSceneMetadata metadata) { int previousTier = currentTier; LinkedHashMap previousChannels = new LinkedHashMap<>(channelOverrides); boolean hadMetadata = this.metadata != null; - this.metadata = metadata; + this.metadata = metadata != null ? metadata.withControlLocks(tierLocked, lockedChannels) : null; if (metadata == null) { currentTier = StructureLibPreviewSelection.DEFAULT_MASTER_TIER; channelOverrides.clear(); @@ -87,8 +91,20 @@ public void setMetadata(@Nullable StructureLibSceneMetadata metadata) { } } + public void setControlLocks(@Nullable StructureLibSceneOptions options) { + tierLocked = options != null && options.isTierLocked(); + lockedChannels.clear(); + if (options != null) { + lockedChannels.addAll(options.getLockedChannels()); + } + if (metadata != null) { + setMetadata(metadata); + } + } + public void setCurrentTier(int tier) { StructureLibSceneMetadata.TierData td = metadata != null ? metadata.getTierData() : null; + if (td != null && td.isLocked()) return; this.currentTier = td != null ? StructureLibSceneMetadata.clamp(tier, td.getMinValue(), td.getMaxValue()) : Math.max(1, tier); } @@ -102,6 +118,7 @@ public void setChannelValue(String channelId, int value) { String normalized = StructureLibPreviewSelection.normalizeChannelId(channelId); if (normalized == null) return; StructureLibSceneMetadata.ChannelData cd = metadata != null ? metadata.getChannelData(normalized) : null; + if (cd != null && cd.isLocked()) return; int next = cd != null ? StructureLibSceneMetadata.clamp(value, cd.getMinValue(), cd.getMaxValue()) : Math.max(0, value); if (next > 0) channelOverrides.put(normalized, next); @@ -131,7 +148,19 @@ public void setPendingSelection(@Nullable StructureLibPreviewSelection pendingSe public void applyPreviewSelection(StructureLibPreviewSelection selection) { if (selection == null) return; setCurrentTier(selection.getMasterTier()); + LinkedHashMap lockedValues = new LinkedHashMap<>(); + if (metadata != null) { + for (StructureLibSceneMetadata.ChannelData channel : metadata.getChannelDataList()) { + if (channel != null && channel.isLocked()) { + Integer value = channelOverrides.get(channel.getChannelId()); + if (value != null) { + lockedValues.put(channel.getChannelId(), value); + } + } + } + } channelOverrides.clear(); + channelOverrides.putAll(lockedValues); for (Map.Entry entry : selection.getChannelOverrides() .entrySet()) { setChannelValue(entry.getKey(), entry.getValue()); diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/element/ImportStructureLibElementCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/scene/element/ImportStructureLibElementCompiler.java index 7124aad6..8b774f2c 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/element/ImportStructureLibElementCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/element/ImportStructureLibElementCompiler.java @@ -91,6 +91,7 @@ public void compile(GuidebookLevel level, CameraSettings camera, PageCompiler co selection.getIntegrationOptions()); StructureLibSceneBinding binding = scene.registerStructureLibBinding(structureName); + binding.setControlLocks(mergedOptions); binding.setRebuildRecipe(request, offsetX, offsetY, offsetZ, formed); // Build initial metadata from ConstructableData (not a world operation). diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/element/StructureLibSceneOptionParser.java b/src/main/java/com/hfstudio/guidenh/guide/scene/element/StructureLibSceneOptionParser.java index ac66b4ec..87fc0a60 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/element/StructureLibSceneOptionParser.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/element/StructureLibSceneOptionParser.java @@ -38,6 +38,9 @@ public static StructureLibSceneOptions parseAttributes(PageCompiler compiler, Ly MdxJsxElementFields element) { StructureLibSceneOptions.Builder builder = StructureLibSceneOptions.builder(); applyIntAttribute(compiler, errorSink, element, builder, "tier"); + if (element.getAttribute("tier") != null && readLockFlag(compiler, errorSink, element)) { + builder.tierLocked(true); + } applyStringAttribute(compiler, errorSink, element, builder, "facing"); applyStringAttribute(compiler, errorSink, element, builder, "rotation"); applyStringAttribute(compiler, errorSink, element, builder, "flip"); @@ -58,6 +61,9 @@ public static StructureLibSceneOptions parseAttributes(PageCompiler compiler, Ly } if (value > 0) { builder.channel(channelName, value); + if (readLockFlag(compiler, errorSink, element)) { + builder.channelLocked(channelName, true); + } } } return builder.build(); @@ -68,7 +74,13 @@ private static void applyChild(PageCompiler compiler, LytErrorSink errorSink, String name = childElement.name(); switch (name) { case "Tier": - builder.tier(resolveIntValue(compiler, errorSink, childElement)); + Integer tier = resolveIntValue(compiler, errorSink, childElement); + if (tier != null) { + builder.tier(tier); + if (readLockFlag(compiler, errorSink, childElement)) { + builder.tierLocked(true); + } + } break; case "Channel": applyChannel(compiler, errorSink, builder, childElement); @@ -107,6 +119,9 @@ private static void applyChannel(PageCompiler compiler, LytErrorSink errorSink, Integer value = resolveIntValue(compiler, errorSink, childElement); if (name != null && value != null) { builder.channel(name, value); + if (readLockFlag(compiler, errorSink, childElement)) { + builder.channelLocked(name, true); + } } } @@ -212,4 +227,8 @@ private static int readInt(PageCompiler compiler, LytErrorSink errorSink, MdxJsx String attribute, int defaultValue) { return MdxAttrs.getInt(compiler, errorSink, element, attribute, defaultValue); } + + private static boolean readLockFlag(PageCompiler compiler, LytErrorSink errorSink, MdxJsxElementFields element) { + return MdxAttrs.getBoolean(compiler, errorSink, element, "locked", false); + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteAnimatedPng.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteAnimatedPng.java index 8c86c0bc..a8f7af66 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteAnimatedPng.java +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteAnimatedPng.java @@ -1,6 +1,7 @@ package com.hfstudio.guidenh.guide.siteexport.site; import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; @@ -10,7 +11,11 @@ import java.util.List; import java.util.zip.CRC32; +import javax.imageio.IIOImage; import javax.imageio.ImageIO; +import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; +import javax.imageio.stream.MemoryCacheImageOutputStream; public class GuideSiteAnimatedPng { @@ -51,58 +56,63 @@ public static byte[] encode(List frames) throws IOException { chunk(out, "acTL", controlBytes.toByteArray()); int sequence = 0; BufferedImage previous = null; - for (Frame frame : frames) { - BufferedImage image = frame.image(); - if (image.getWidth() != width || image.getHeight() != height || frame.ticks() > 65535) { - throw new IllegalArgumentException("Animated PNG frame dimensions or duration are invalid"); - } - int[] rect = changedRectangle(previous, image); - ByteArrayOutputStream frameBytes = new ByteArrayOutputStream(); - DataOutputStream frameControl = new DataOutputStream(frameBytes); - frameControl.writeInt(sequence++); - frameControl.writeInt(rect[2]); - frameControl.writeInt(rect[3]); - frameControl.writeInt(rect[0]); - frameControl.writeInt(rect[1]); - frameControl.writeShort(frame.ticks()); - frameControl.writeShort(20); - frameControl.writeByte(0); - frameControl.writeByte(0); - chunk(out, "fcTL", frameBytes.toByteArray()); + var writers = ImageIO.getImageWritersByFormatName("png"); + if (!writers.hasNext()) { + throw new IOException("No PNG writer is available"); + } + ImageWriter writer = writers.next(); + try { + for (Frame frame : frames) { + BufferedImage image = frame.image(); + if (image.getWidth() != width || image.getHeight() != height || frame.ticks() > 65535) { + throw new IllegalArgumentException("Animated PNG frame dimensions or duration are invalid"); + } + int[] rect = changedRectangle(previous, image); + ByteArrayOutputStream frameBytes = new ByteArrayOutputStream(); + DataOutputStream frameControl = new DataOutputStream(frameBytes); + frameControl.writeInt(sequence++); + frameControl.writeInt(rect[2]); + frameControl.writeInt(rect[3]); + frameControl.writeInt(rect[0]); + frameControl.writeInt(rect[1]); + frameControl.writeShort(frame.ticks()); + frameControl.writeShort(20); + frameControl.writeByte(0); + frameControl.writeByte(0); + chunk(out, "fcTL", frameBytes.toByteArray()); - BufferedImage crop = new BufferedImage(rect[2], rect[3], BufferedImage.TYPE_INT_ARGB); - crop.setRGB( - 0, - 0, - rect[2], - rect[3], - image.getRGB(rect[0], rect[1], rect[2], rect[3], null, 0, rect[2]), - 0, - rect[2]); - ByteArrayOutputStream pngBytes = new ByteArrayOutputStream(); - if (!ImageIO.write(crop, "png", pngBytes)) { - throw new IOException("No PNG writer is available"); - } - // ImageIO owns PNG compression; APNG adds frame control and sequence numbers. - DataInputStream png = new DataInputStream(new ByteArrayInputStream(pngBytes.toByteArray())); - png.readLong(); - while (png.available() > 0) { - int length = png.readInt(); - String type = new String(png.readNBytes(4), StandardCharsets.US_ASCII); - byte[] data = png.readNBytes(length); - png.readInt(); - if (!"IDAT".equals(type)) continue; - if (previous == null) { - chunk(out, "IDAT", data); - } else { - ByteArrayOutputStream partBytes = new ByteArrayOutputStream(); - DataOutputStream part = new DataOutputStream(partBytes); - part.writeInt(sequence++); - part.write(data); - chunk(out, "fdAT", partBytes.toByteArray()); + BufferedImage crop = new BufferedImage(rect[2], rect[3], BufferedImage.TYPE_INT_ARGB); + copyRectangle(image, crop, rect); + ByteArrayOutputStream pngBytes = new ByteArrayOutputStream(); + try (MemoryCacheImageOutputStream imageOutput = new MemoryCacheImageOutputStream(pngBytes)) { + writer.setOutput(imageOutput); + ImageWriteParam parameters = writer.getDefaultWriteParam(); + writer.write(null, new IIOImage(crop, null, null), parameters); + } finally { + writer.reset(); + } + DataInputStream png = new DataInputStream(new ByteArrayInputStream(pngBytes.toByteArray())); + png.readLong(); + while (png.available() > 0) { + int length = png.readInt(); + String type = new String(png.readNBytes(4), StandardCharsets.US_ASCII); + byte[] data = png.readNBytes(length); + png.readInt(); + if (!"IDAT".equals(type)) continue; + if (previous == null) { + chunk(out, "IDAT", data); + } else { + ByteArrayOutputStream partBytes = new ByteArrayOutputStream(); + DataOutputStream part = new DataOutputStream(partBytes); + part.writeInt(sequence++); + part.write(data); + chunk(out, "fdAT", partBytes.toByteArray()); + } } + previous = image; } - previous = image; + } finally { + writer.dispose(); } chunk(out, "IEND", new byte[0]); return bytes.toByteArray(); @@ -112,10 +122,15 @@ private static int[] changedRectangle(BufferedImage previous, BufferedImage imag int width = image.getWidth(); int height = image.getHeight(); if (previous == null) return new int[] { 0, 0, width, height }; + int[] previousPixels = packedPixels(previous); + int[] currentPixels = packedPixels(image); int left = width, right = -1, top = height, bottom = -1; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { - if (previous.getRGB(x, y) != image.getRGB(x, y)) { + int index = y * width + x; + int previousPixel = previousPixels != null ? previousPixels[index] : previous.getRGB(x, y); + int currentPixel = currentPixels != null ? currentPixels[index] : image.getRGB(x, y); + if (previousPixel != currentPixel) { left = Math.min(left, x); right = Math.max(right, x); top = Math.min(top, y); @@ -126,6 +141,48 @@ private static int[] changedRectangle(BufferedImage previous, BufferedImage imag return right < 0 ? new int[] { 0, 0, 1, 1 } : new int[] { left, top, right - left + 1, bottom - top + 1 }; } + private static void copyRectangle(BufferedImage source, BufferedImage target, int[] rectangle) { + int[] sourcePixels = packedPixels(source); + int[] targetPixels = packedPixels(target); + int width = rectangle[2]; + int height = rectangle[3]; + if (sourcePixels != null && targetPixels != null) { + int sourceWidth = source.getWidth(); + for (int row = 0; row < height; row++) { + System.arraycopy( + sourcePixels, + (rectangle[1] + row) * sourceWidth + rectangle[0], + targetPixels, + row * width, + width); + } + return; + } + target.setRGB( + 0, + 0, + width, + height, + source.getRGB(rectangle[0], rectangle[1], width, height, null, 0, width), + 0, + width); + } + + private static int[] packedPixels(BufferedImage image) { + if (image.getType() != BufferedImage.TYPE_INT_ARGB || image.getRaster() + .getMinX() != 0 + || image.getRaster() + .getMinY() != 0 + || !(image.getRaster() + .getDataBuffer() instanceof DataBufferInt data) + || data.getOffset() != 0) { + return null; + } + int expectedLength = image.getWidth() * image.getHeight(); + int[] pixels = data.getData(); + return pixels.length == expectedLength ? pixels : null; + } + private static void chunk(DataOutputStream out, String type, byte[] data) throws IOException { byte[] name = type.getBytes(StandardCharsets.US_ASCII); out.writeInt(data.length); diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteAssetRegistry.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteAssetRegistry.java index 87ddde4a..07b73f28 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteAssetRegistry.java +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteAssetRegistry.java @@ -73,18 +73,7 @@ public GuideSiteAssetRegistry(Path outDir) { public String writeShared(String bucket, String extension, byte[] content) throws Exception { checkWriteFailure(); Path relative = sharedPath(bucket, extension, sha256(content)); - if (completedPaths.contains(relative)) { - return relative.toString() - .replace('\\', '/'); - } - Path absolute = outDir.resolve(relative); - ensureDirectory(absolute.getParent()); - synchronized (pathLock(relative)) { - if (!completedPaths.contains(relative) && !Files.exists(absolute)) { - Files.write(absolute, content); - } - completedPaths.add(relative); - } + scheduleWrite(relative, () -> content); return relative.toString() .replace('\\', '/'); } @@ -97,7 +86,7 @@ public String writeSharedCompressed(String bucket, String extension, byte[] cont } public String writePngAsync(String bucket, BufferedImage image) throws Exception { - Path relative = sharedPath(bucket, ".png", sha256Image(image)); + Path relative = sharedPath(bucket, ".png", imageHash(image)); scheduleWrite(relative, () -> encodePng(image)); return relative.toString() .replace('\\', '/'); @@ -111,7 +100,7 @@ public String writeAnimatedPngAsync(String bucket, List producer) throws Exce if (completedPaths.contains(relative) || pendingPaths.contains(relative)) { return; } - if (Files.exists(absolute)) { - completedPaths.add(relative); - return; - } pendingWrites.acquire(); try { checkWriteFailure(); @@ -232,7 +217,7 @@ private byte[] encodePng(BufferedImage image) throws IOException { return output.toByteArray(); } - private String sha256Image(BufferedImage image) throws Exception { + String imageHash(BufferedImage image) throws Exception { if (image.getType() != BufferedImage.TYPE_INT_ARGB || !(image.getRaster() .getDataBuffer() instanceof DataBufferInt data)) { throw new IllegalArgumentException("Site image must use TYPE_INT_ARGB"); diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteRecipeExporter.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteRecipeExporter.java index 12490b41..f70b81fe 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteRecipeExporter.java +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteRecipeExporter.java @@ -96,20 +96,19 @@ private String renderGrid(List> ingredients, GuideSi html.append("
"); appendSlotBoxes(html, ingredients); html.append("
"); + html.append(""); if (supportingSlots != null && !supportingSlots.isEmpty()) { html.append("
"); appendSlotBoxes(html, supportingSlots); html.append("
"); } - html.append(""); - // Wrap the result icon in an `ingredient-box` so the output slot also receives the - // slot.png border-image background (previously the result was rendered as a bare - // `recipe-result` div with no slot frame, which made the output look unboxed). + // Wrap the result icon in an ingredient box so the output slot keeps the same frame as inputs. html.append("
"); appendTooltipIcon(html, resultItem); html.append("
"); + html.append(""); html.append(""); return html.toString(); } @@ -227,7 +226,8 @@ public String renderNeiPositionedSlots(List slots, GuideSi } if (usePhase1Canvas) { - html.append("
siteRecipeSlots(Object handler, int re private static NeiRecipeLookup.Slot siteRecipeResultSlot(Object handler, int recipeIndex) { RecipeSlot slot = GuideNhIntegrationRegistry.global() .readRecipeResultSlot(handler, recipeIndex); - return slot != null ? new NeiRecipeLookup.Slot(slot.x(), slot.y(), slot.stacks()) : null; + return slot != null ? new NeiRecipeLookup.Slot(slot.x(), slot.y(), slot.stacks(), slot.currentStack()) : null; } private static List toNeiSlots(List slots) { @@ -45,7 +47,7 @@ private static List toNeiSlots(List slots) { } List converted = new ArrayList<>(slots.size()); for (RecipeSlot slot : slots) { - converted.add(new NeiRecipeLookup.Slot(slot.x(), slot.y(), slot.stacks())); + converted.add(new NeiRecipeLookup.Slot(slot.x(), slot.y(), slot.stacks(), slot.currentStack())); } return converted; } @@ -453,13 +455,14 @@ private String renderInternal(RenderRequest request) { boolean hasRecipeFilter = !request.inputExpr.isEmpty() || !request.outputExpr.isEmpty(); boolean hasHandlerFilter = request.handlerNameFilter != null || request.handlerIdFilter != null - || request.handlerOrder >= 0; + || request.handlerOrder >= 0 + || !request.handlerWhitelist.isEmpty(); RawHandlerRenderResult rawHandlerResult = renderFromRawHandlers(request, targetStack, hasRecipeFilter); if (!rawHandlerResult.renderedRecipes.isEmpty()) { return exporter.renderRecipeCollection(rawHandlerResult.renderedRecipes, request.multi); } - if (hasHandlerFilter && !rawHandlerResult.hadHandlersAfterFilter) { + if (hasHandlerFilter) { return fallbackParagraph(request.fallbackText); } @@ -481,9 +484,8 @@ private RawHandlerRenderResult renderFromRawHandlers(RenderRequest request, Item boolean hasRecipeFilter) { List rawHandlers = request.usageQuery ? safeHandlers(rawHandlerFinder.findUsageHandlers(targetStack)) : safeHandlers(rawHandlerFinder.findCraftingHandlers(targetStack)); - boolean hasHandlerFilter = request.handlerNameFilter != null || request.handlerIdFilter != null - || request.handlerOrder >= 0; - if (!request.usageQuery && hasHandlerFilter) { + if (!request.usageQuery && (request.handlerIdFilter != null || request.handlerNameFilter != null + || !request.handlerWhitelist.isEmpty())) { rawHandlers = mergeHandlers(rawHandlers, safeHandlers(rawHandlerFinder.findUsageHandlers(targetStack))); } @@ -496,10 +498,12 @@ private RawHandlerRenderResult renderFromRawHandlers(RenderRequest request, Item request.handlerWhitelist, RecipeCompiler.effectiveHandlerBlacklist(request.handlerBlacklist)); if (handlers.isEmpty()) { - return new RawHandlerRenderResult(List.of(), false); + return new RawHandlerRenderResult(List.of()); } + IdUtils.ParsedItemRef targetRef = parseTargetRef(request.recipeId, request.defaultNamespace); List renderedRecipes = new ArrayList<>(); + Set recipeFingerprints = new LinkedHashSet<>(); for (int hi = 0; hi < handlers.size() && renderedRecipes.size() < request.limit; hi++) { Object handler = handlers.get(hi); int recipeCount = handlerRuntime.recipeCount(handler); @@ -507,18 +511,26 @@ private RawHandlerRenderResult renderFromRawHandlers(RenderRequest request, Item int recipeEnd = request.recipeIndex >= 0 ? Math.min(recipeCount, request.recipeIndex + 1) : recipeCount; for (int recipeIndex = recipeStart; recipeIndex < recipeEnd && renderedRecipes.size() < request.limit; recipeIndex++) { - if (hasRecipeFilter && !RecipeCompiler - .recipeMatches(handler, recipeIndex, request.inputExpr, request.outputExpr, handlerRuntime)) { + NeiRecipeLookup.Entry entry = readRawHandlerEntry(handler, recipeIndex); + if (!request.usageQuery && (targetRef == null || !RecipeCompiler.handlerTargetMatches( + entry, + targetRef, + request.handlerIdFilter != null || request.handlerNameFilter != null + || !request.handlerWhitelist.isEmpty()))) { + continue; + } + if (hasRecipeFilter && !RecipeCompiler.recipeMatches(entry, request.inputExpr, request.outputExpr)) { continue; } + if (!recipeFingerprints.add(NeiRecipeLookup.recipeFingerprint(handler, entry))) continue; - String rendered = renderHandlerRecipe(handler, recipeIndex, targetStack); + String rendered = renderHandlerRecipe(handler, recipeIndex, targetStack, entry); if (!rendered.isEmpty()) { renderedRecipes.add(rendered); } } } - return new RawHandlerRenderResult(renderedRecipes, true); + return new RawHandlerRenderResult(renderedRecipes); } private List renderFromNeiEntries(RenderRequest request, ItemStack targetStack, boolean hasRecipeFilter) { @@ -531,6 +543,7 @@ private List renderFromNeiEntries(RenderRequest request, ItemStack targe } List renderedRecipes = new ArrayList<>(); + Set recipeFingerprints = new LinkedHashSet<>(); for (int i = 0; i < refs.size() && renderedRecipes.size() < request.limit; i++) { NeiRecipeLookup.CraftingRecipeRef ref = refs.get(i); if (request.recipeIndex >= 0 && (ref == null || ref.recipeIndex != request.recipeIndex)) { @@ -540,9 +553,13 @@ private List renderFromNeiEntries(RenderRequest request, ItemStack targe if (entry == null || !neiEntryHasAnySlots(entry)) { continue; } + if (!matchesRequestedTarget(request, ref, entry)) { + continue; + } if (hasRecipeFilter && !RecipeCompiler.entryMatches(entry, request.inputExpr, request.outputExpr)) { continue; } + if (!recipeFingerprints.add(NeiRecipeLookup.recipeFingerprint(ref.handler, entry))) continue; @Nullable GuideSiteNeiPhase1BackgroundExporter.Result phase1 = neiPhase1Capture(ref.handler, ref.recipeIndex); @@ -571,6 +588,7 @@ private List renderFromUsageEntries(RenderRequest request, ItemStack tar } List renderedRecipes = new ArrayList<>(); + Set recipeFingerprints = new LinkedHashSet<>(); int entryStart = Math.max(request.recipeIndex, 0); int entryEnd = request.recipeIndex >= 0 ? Math.min(entries.size(), request.recipeIndex + 1) : entries.size(); for (int i = entryStart; i < entryEnd && renderedRecipes.size() < request.limit; i++) { @@ -581,6 +599,7 @@ private List renderFromUsageEntries(RenderRequest request, ItemStack tar if (hasRecipeFilter && !RecipeCompiler.entryMatches(entry, request.inputExpr, request.outputExpr)) { continue; } + if (!recipeFingerprints.add(NeiRecipeLookup.recipeFingerprint(null, entry))) continue; String rendered = layoutRegistry.render( SiteRecipeLayoutContext @@ -600,6 +619,7 @@ private List renderFromVanillaEntries(RenderRequest request, ItemStack t } List renderedRecipes = new ArrayList<>(); + Set recipeFingerprints = new LinkedHashSet<>(); int entryStart = Math.max(request.recipeIndex, 0); int entryEnd = request.recipeIndex >= 0 ? Math.min(vanillaEntries.size(), request.recipeIndex + 1) : vanillaEntries.size(); @@ -611,6 +631,7 @@ private List renderFromVanillaEntries(RenderRequest request, ItemStack t if (hasRecipeFilter && !RecipeCompiler.vanillaEntryMatches(entry, request.inputExpr, request.outputExpr)) { continue; } + if (!recipeFingerprints.add(vanillaFingerprint(entry))) continue; String rendered = layoutRegistry .render(SiteRecipeLayoutContext.vanilla(entry, targetStack, exporter, itemIconResolver)); @@ -621,26 +642,74 @@ private List renderFromVanillaEntries(RenderRequest request, ItemStack t return renderedRecipes; } - private String renderHandlerRecipe(Object handler, int recipeIndex, ItemStack targetStack) { + private static String vanillaFingerprint(RecipeLookup.Entry entry) { + StringBuilder value = new StringBuilder(); + if (entry == null) return value.toString(); + if (entry.input3x3 != null) { + for (ItemStack stack : entry.input3x3) appendStackFingerprint(value, stack); + } + appendStackFingerprint(value, entry.result); + value.append(entry.shapeless); + return value.toString(); + } + + private static void appendStackFingerprint(StringBuilder value, @Nullable ItemStack stack) { + if (stack == null || stack.getItem() == null) { + value.append("null;"); + return; + } + Object registryName = Item.itemRegistry.getNameForObject(stack.getItem()); + value.append( + registryName != null ? registryName + : stack.getItem() + .getClass() + .getName()) + .append('#') + .append(stack.getItemDamage()) + .append('x') + .append(stack.stackSize); + if (stack.stackTagCompound != null) value.append('@') + .append(stack.stackTagCompound); + value.append(';'); + } + + private NeiRecipeLookup.Entry readRawHandlerEntry(Object handler, int recipeIndex) { + List ingredients = handlerRuntime.readIngredientSlots(handler, recipeIndex); + List supporting = handlerRuntime.readOtherSlots(handler, recipeIndex); + NeiRecipeLookup.Slot result = handlerRuntime.readResultSlot(handler, recipeIndex); + String name = handlerRuntime.handlerName(handler); + return new NeiRecipeLookup.Entry( + name == null ? "" : name, + name == null ? "" : name, + ingredients, + supporting, + result); + } + + private String renderHandlerRecipe(Object handler, int recipeIndex, ItemStack targetStack, + NeiRecipeLookup.Entry entry) { + List ingredientSlots = entry.ingredients; + List supportingSlotData = entry.others; + NeiRecipeLookup.Slot resultSlot = entry.result; List> ingredients = exporter - .ingredientItemsFromNeiSlots(handlerRuntime.readIngredientSlots(handler, recipeIndex), itemIconResolver); + .ingredientItemsFromNeiSlots(ingredientSlots, itemIconResolver); List> supportingSlots = exporter - .supportingSlotItemsFromNeiSlots(handlerRuntime.readOtherSlots(handler, recipeIndex), itemIconResolver); - GuideSiteExportedItem resultItem = exporter - .resultItem(handlerRuntime.readResultSlot(handler, recipeIndex), targetStack, itemIconResolver); + .supportingSlotItemsFromNeiSlots(supportingSlotData, itemIconResolver); + GuideSiteExportedItem resultItem = exporter.resultItem(resultSlot, targetStack, itemIconResolver); if (ingredients.isEmpty() && supportingSlots.isEmpty() && resultItem.isEmpty()) { return ""; } @Nullable GuideSiteNeiPhase1BackgroundExporter.Result phase1 = neiPhase1Capture(handler, recipeIndex); return layoutRegistry.render( - SiteRecipeLayoutContext.rawHandler( + SiteRecipeLayoutContext.rawHandlerWithEntry( handler, recipeIndex, targetStack, exporter, itemIconResolver, rawHandlerSlotAccess, + entry, phase1 != null ? phase1.relativeUrl : null, phase1 != null ? phase1.pixelWidth : null, phase1 != null ? phase1.pixelHeight : null, @@ -665,6 +734,30 @@ private static boolean neiEntryHasAnySlots(NeiRecipeLookup.Entry entry) { return entry.result != null; } + private boolean matchesRequestedTarget(RenderRequest request, @Nullable NeiRecipeLookup.CraftingRecipeRef ref, + NeiRecipeLookup.Entry entry) { + if (ref == null || entry == null) { + return false; + } + IdUtils.ParsedItemRef target = parseTargetRef(request.recipeId, request.defaultNamespace); + if (target == null) { + return false; + } + if (request.handlerIdFilter != null || request.handlerNameFilter != null + || !request.handlerWhitelist.isEmpty()) { + return RecipeCompiler.handlerTargetMatches(ref.handler, ref.recipeIndex, target, handlerRuntime, true); + } + return RecipeCompiler.resultSlotContains(entry.result, target); + } + + private static @Nullable IdUtils.ParsedItemRef parseTargetRef(String recipeId, String defaultNamespace) { + try { + return IdUtils.parseItemRef(recipeId, defaultNamespace); + } catch (IllegalArgumentException ignored) { + return null; + } + } + private static SiteRecipeRawHandlerAccess rawHandlerSlots(final HandlerRuntime hr) { return new SiteRecipeRawHandlerAccess() { @@ -695,12 +788,12 @@ private List mergeHandlers(List craftingHandlers, List u List merged = new ArrayList<>(craftingHandlers.size() + usageHandlers.size()); merged.addAll(craftingHandlers); - IdentityHashMap seen = new IdentityHashMap<>(merged.size()); + Set seen = new LinkedHashSet<>(); for (Object handler : craftingHandlers) { - seen.put(handler, Boolean.TRUE); + seen.add(NeiRecipeLookup.handlerFingerprint(handler)); } for (Object handler : usageHandlers) { - if (seen.put(handler, Boolean.TRUE) == null) { + if (seen.add(NeiRecipeLookup.handlerFingerprint(handler))) { merged.add(handler); } } @@ -783,11 +876,9 @@ private RenderRequest(String tagName, String recipeId, String fallbackText, Stri private static class RawHandlerRenderResult { private final List renderedRecipes; - private final boolean hadHandlersAfterFilter; - private RawHandlerRenderResult(List renderedRecipes, boolean hadHandlersAfterFilter) { + private RawHandlerRenderResult(List renderedRecipes) { this.renderedRecipes = renderedRecipes; - this.hadHandlersAfterFilter = hadHandlersAfterFilter; } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteSceneTessellatorCapture.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteSceneTessellatorCapture.java index 150536ca..55ef88cb 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteSceneTessellatorCapture.java +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteSceneTessellatorCapture.java @@ -48,6 +48,7 @@ public class GuideSiteSceneTessellatorCapture { private ByteBuffer texturePixelBuffer; private final List meshes = new ArrayList<>(); private final Map textures = new LinkedHashMap<>(); + private final Map indexDataCache = new HashMap<>(); private static final int RAW_VERTEX_STRIDE = 8; private static final byte[] EMPTY_VERTEX_BYTES = new byte[0]; @@ -407,7 +408,14 @@ private int mapDepthTest(boolean depthEnabled) { } private EncodedIndexData buildIndexData(int vertexCount) { - return encodeIndexData(drawMode, vertexCount); + IndexCacheKey key = new IndexCacheKey(drawMode, vertexCount); + EncodedIndexData cached = indexDataCache.get(key); + if (cached != null) { + return cached; + } + EncodedIndexData created = encodeIndexData(drawMode, vertexCount); + indexDataCache.put(key, created); + return created; } private static int mapPrimitiveType(int drawMode) { @@ -662,6 +670,8 @@ static final class VertexDecodeScratch { record EncodedIndexData(byte[] indexBuffer, long indexCount, int indexType, int primitiveType) {} + private record IndexCacheKey(int drawMode, int vertexCount) {} + public static class CapturedMesh { final byte[] vertexBuffer; diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteTextureAnimations.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteTextureAnimations.java index 2c568fa3..a75f7883 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteTextureAnimations.java +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteTextureAnimations.java @@ -47,6 +47,9 @@ public class GuideSiteTextureAnimations implements AutoCloseable { private final AtomicLong captureCallCount = new AtomicLong(); private final AtomicLong capturedVertexCount = new AtomicLong(); private final AtomicLong matchedSpriteCount = new AtomicLong(); + private final AtomicLong animationCacheHitCount = new AtomicLong(); + private final Map renderedAnimationCache = new HashMap<>(); + private boolean hasAnimatedSprites; private boolean indexed; public GuideSiteTextureAnimations(GuideSiteAssetRegistry assets) { @@ -87,6 +90,7 @@ public int ticksUntilChange(long tick) { public class Capture implements AutoCloseable { + private final GuideSiteTextureAnimations owner = GuideSiteTextureAnimations.this; private final Capture previous; private final Map seen = new IdentityHashMap<>(); private final List used = new ArrayList<>(); @@ -155,7 +159,7 @@ public Capture beginCapture() { } public static void captureVertices(int[] vertices, int vertexCount, boolean textured) { - if (active != null && textured && vertices != null && vertexCount > 0) { + if (active != null && active.owner.hasAnimatedSprites && textured && vertices != null && vertexCount > 0) { active.collect(vertices, vertexCount); } } @@ -219,6 +223,9 @@ private void index(ITextureObject object) { } } atlasCells.put(atlas.getGlTextureId(), cells); + if (!cells.isEmpty()) { + hasAnimatedSprites = true; + } } finally { GL11.glBindTexture(GL11.GL_TEXTURE_2D, binding); OpenGlHelper.setActiveTexture(unit); @@ -234,6 +241,12 @@ public String exportRendered(String bucket, Supplier renderer) th sprites = capture.sprites(); } if (sprites.isEmpty()) return assets.writePngAsync(bucket, first); + AnimationCacheKey cacheKey = new AnimationCacheKey(bucket, assets.imageHash(first), List.copyOf(sprites)); + String cachedPath = renderedAnimationCache.get(cacheKey); + if (cachedPath != null) { + animationCacheHitCount.incrementAndGet(); + return cachedPath; + } animatedExportCount.incrementAndGet(); long period = 1; for (Sprite sprite : sprites) { @@ -314,7 +327,9 @@ public String exportRendered(String bucket, Supplier renderer) th .tickCounter()); } } - return assets.writeAnimatedPngAsync(bucket, frames); + String exportedPath = assets.writeAnimatedPngAsync(bucket, frames); + renderedAnimationCache.put(cacheKey, exportedPath); + return exportedPath; } private int estimateFrameCount(long period, List sprites) { @@ -333,7 +348,7 @@ private int estimateFrameCount(long period, List sprites) { @Override public void close() { GuideDebugLog.infoAlways( - "[GuideNH] [GuideSiteTextureAnimations] exports={}, animated={}, renderedFrames={}, fallbacks={}, renderTimeMs={}, captureCalls={}, capturedVertices={}, matchedSprites={}", + "[GuideNH] [GuideSiteTextureAnimations] exports={}, animated={}, renderedFrames={}, fallbacks={}, renderTimeMs={}, captureCalls={}, capturedVertices={}, matchedSprites={}, cacheHits={}", exportCount.get(), animatedExportCount.get(), renderedFrameCount.get(), @@ -341,11 +356,14 @@ public void close() { TimeUnit.NANOSECONDS.toMillis(renderNanos.get()), captureCallCount.get(), capturedVertexCount.get(), - matchedSpriteCount.get()); + matchedSpriteCount.get(), + animationCacheHitCount.get()); } private record FrameState(int textureFrame, int frameCounter, int tickCounter) {} + private record AnimationCacheKey(String bucket, String firstImageHash, List sprites) {} + private static long gcd(long a, long b) { while (b != 0) { long remainder = a % b; diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/layout/DefaultSiteRecipeLayoutStrategy.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/layout/DefaultSiteRecipeLayoutStrategy.java index 497499c7..1d9f8d93 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/layout/DefaultSiteRecipeLayoutStrategy.java +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/layout/DefaultSiteRecipeLayoutStrategy.java @@ -58,6 +58,13 @@ public String render(SiteRecipeLayoutContext ctx) { return ""; } int idx = ctx.rawRecipeIndex(); + NeiRecipeLookup.Entry snapshot = ctx.rawHandlerEntry(); + if (snapshot != null) { + return exporter.renderNeiOverlayGridItems( + exporter.ingredientItemsFromNeiEntry(snapshot, resolver), + exporter.resultItem(snapshot.result, ctx.targetStack(), resolver), + exporter.supportingSlotItemsFromNeiEntry(snapshot, resolver)); + } List> ingredients = exporter .ingredientItemsFromNeiSlots(access.readIngredientSlots(handler, idx), resolver); List> supporting = exporter diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/layout/PositionedNeiSiteRecipeLayoutStrategy.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/layout/PositionedNeiSiteRecipeLayoutStrategy.java index 6439e5a4..ad362fbd 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/layout/PositionedNeiSiteRecipeLayoutStrategy.java +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/layout/PositionedNeiSiteRecipeLayoutStrategy.java @@ -48,6 +48,15 @@ private static List collectSlots(SiteRecipeLayoutContext c return out; } if (ctx.kind() == SiteRecipeSourceKind.RAW_HANDLER) { + NeiRecipeLookup.Entry snapshot = ctx.rawHandlerEntry(); + if (snapshot != null) { + addAll(out, snapshot.ingredients); + addAll(out, snapshot.others); + if (snapshot.result != null) { + out.add(snapshot.result); + } + return out; + } Object handler = ctx.rawHandler(); SiteRecipeRawHandlerAccess access = ctx.rawHandlerAccess(); if (handler == null || access == null) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/layout/SiteRecipeLayoutContext.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/layout/SiteRecipeLayoutContext.java index f17911b1..992a771b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/layout/SiteRecipeLayoutContext.java +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/layout/SiteRecipeLayoutContext.java @@ -28,6 +28,7 @@ public class SiteRecipeLayoutContext { private final @Nullable Object rawHandler; private final int rawRecipeIndex; private final @Nullable SiteRecipeRawHandlerAccess rawHandlerAccess; + private final @Nullable NeiRecipeLookup.Entry rawHandlerEntry; /** * Site-relative URL (e.g. prefixed with * {@link GuideSitePageAssetExporter#ROOT_PREFIX}). @@ -41,8 +42,9 @@ private SiteRecipeLayoutContext(SiteRecipeSourceKind kind, ItemStack targetStack GuideSiteItemIconResolver itemIconResolver, @Nullable RecipeLookup.Entry vanillaEntry, @Nullable RecipeEntry recipeEntry, @Nullable NeiRecipeLookup.Entry neiEntry, @Nullable Object rawHandler, int rawRecipeIndex, @Nullable SiteRecipeRawHandlerAccess rawHandlerAccess, - @Nullable String neiPhase1BackgroundUrl, @Nullable Integer neiPhase1CanvasWidthPx, - @Nullable Integer neiPhase1CanvasHeightPx, @Nullable Integer neiPhase1BodyYShiftPx) { + @Nullable NeiRecipeLookup.Entry rawHandlerEntry, @Nullable String neiPhase1BackgroundUrl, + @Nullable Integer neiPhase1CanvasWidthPx, @Nullable Integer neiPhase1CanvasHeightPx, + @Nullable Integer neiPhase1BodyYShiftPx) { this.kind = kind; this.targetStack = targetStack; this.exporter = exporter; @@ -53,6 +55,7 @@ private SiteRecipeLayoutContext(SiteRecipeSourceKind kind, ItemStack targetStack this.rawHandler = rawHandler; this.rawRecipeIndex = rawRecipeIndex; this.rawHandlerAccess = rawHandlerAccess; + this.rawHandlerEntry = rawHandlerEntry; this.neiPhase1BackgroundUrl = neiPhase1BackgroundUrl; this.neiPhase1CanvasWidthPx = neiPhase1CanvasWidthPx; this.neiPhase1CanvasHeightPx = neiPhase1CanvasHeightPx; @@ -75,6 +78,7 @@ public static SiteRecipeLayoutContext vanilla(RecipeLookup.Entry entry, ItemStac null, null, null, + null, null); } @@ -94,6 +98,7 @@ public static SiteRecipeLayoutContext recipeEntry(RecipeEntry entry, ItemStack t null, null, null, + null, null); } @@ -132,6 +137,7 @@ public static SiteRecipeLayoutContext neiEntry(NeiRecipeLookup.Entry entry, Item null, -1, null, + null, neiPhase1BackgroundUrl, neiPhase1CanvasWidthPx, neiPhase1CanvasHeightPx, @@ -187,6 +193,30 @@ public static SiteRecipeLayoutContext rawHandler(Object handler, int recipeIndex handler, recipeIndex, rawHandlerAccess, + null, + neiPhase1BackgroundUrl, + neiPhase1CanvasWidthPx, + neiPhase1CanvasHeightPx, + neiPhase1BodyYShiftPx); + } + + public static SiteRecipeLayoutContext rawHandlerWithEntry(Object handler, int recipeIndex, ItemStack targetStack, + GuideSiteRecipeExporter exporter, GuideSiteItemIconResolver itemIconResolver, + SiteRecipeRawHandlerAccess rawHandlerAccess, NeiRecipeLookup.Entry entry, + @Nullable String neiPhase1BackgroundUrl, @Nullable Integer neiPhase1CanvasWidthPx, + @Nullable Integer neiPhase1CanvasHeightPx, @Nullable Integer neiPhase1BodyYShiftPx) { + return new SiteRecipeLayoutContext( + SiteRecipeSourceKind.RAW_HANDLER, + targetStack, + exporter, + itemIconResolver, + null, + null, + null, + handler, + recipeIndex, + rawHandlerAccess, + entry, neiPhase1BackgroundUrl, neiPhase1CanvasWidthPx, neiPhase1CanvasHeightPx, @@ -233,6 +263,10 @@ public int rawRecipeIndex() { return rawHandlerAccess; } + public @Nullable NeiRecipeLookup.Entry rawHandlerEntry() { + return rawHandlerEntry; + } + public @Nullable String neiPhase1BackgroundUrl() { return neiPhase1BackgroundUrl; } diff --git a/src/main/java/com/hfstudio/guidenh/integration/GuideNhClientIntegrationBootstrap.java b/src/main/java/com/hfstudio/guidenh/integration/GuideNhClientIntegrationBootstrap.java index 7b4f42f0..cb2c7529 100644 --- a/src/main/java/com/hfstudio/guidenh/integration/GuideNhClientIntegrationBootstrap.java +++ b/src/main/java/com/hfstudio/guidenh/integration/GuideNhClientIntegrationBootstrap.java @@ -54,8 +54,6 @@ public class GuideNhClientIntegrationBootstrap { - private GuideNhClientIntegrationBootstrap() {} - public static void preInitClient() { registerBlockDisplayProviders(); registerPreviewTileEntityProviders(); diff --git a/src/main/java/com/hfstudio/guidenh/integration/GuideNhIntegrationBootstrap.java b/src/main/java/com/hfstudio/guidenh/integration/GuideNhIntegrationBootstrap.java index 1b80b7c9..30709cc4 100644 --- a/src/main/java/com/hfstudio/guidenh/integration/GuideNhIntegrationBootstrap.java +++ b/src/main/java/com/hfstudio/guidenh/integration/GuideNhIntegrationBootstrap.java @@ -9,8 +9,6 @@ public class GuideNhIntegrationBootstrap { - private GuideNhIntegrationBootstrap() {} - public static void preInitCommon() { registerKnownModDescriptors(); GuideNhIntegrationRegistry.global() diff --git a/src/main/java/com/hfstudio/guidenh/integration/api/GuideNhIntegrationRegistry.java b/src/main/java/com/hfstudio/guidenh/integration/api/GuideNhIntegrationRegistry.java index be09b18b..40cd6f60 100644 --- a/src/main/java/com/hfstudio/guidenh/integration/api/GuideNhIntegrationRegistry.java +++ b/src/main/java/com/hfstudio/guidenh/integration/api/GuideNhIntegrationRegistry.java @@ -52,6 +52,14 @@ public class GuideNhIntegrationRegistry { private final List recipeHandlerSlotProviders = new ArrayList<>(); private final List recipeDrawableRenderProviders = new ArrayList<>(); private final List recipeHandlerRenderProviders = new ArrayList<>(); + private volatile List rawRecipeHandlerProviderSnapshot = List.of(); + private volatile List recipeEntryProviderSnapshot = List.of(); + private volatile List recipeItemTooltipProviderSnapshot = List.of(); + private volatile List recipeAnimationUpdateProviderSnapshot = List.of(); + private volatile List recipeHandlerMetadataProviderSnapshot = List.of(); + private volatile List recipeHandlerSlotProviderSnapshot = List.of(); + private volatile List recipeDrawableRenderProviderSnapshot = List.of(); + private volatile List recipeHandlerRenderProviderSnapshot = List.of(); private final List blockStatsProviders = new ArrayList<>(); /** * Published for the compile worker, which reads it while this thread may still be registering a provider. @@ -321,11 +329,12 @@ public synchronized void registerRawRecipeHandlerProvider(RawRecipeHandlerProvid } if (!rawRecipeHandlerProviders.contains(provider)) { rawRecipeHandlerProviders.add(provider); + rawRecipeHandlerProviderSnapshot = List.copyOf(rawRecipeHandlerProviders); } } - public synchronized List rawRecipeHandlerProviders() { - return List.copyOf(rawRecipeHandlerProviders); + public List rawRecipeHandlerProviders() { + return rawRecipeHandlerProviderSnapshot; } public synchronized void registerRecipeEntryProvider(RecipeEntryProvider provider) { @@ -334,11 +343,12 @@ public synchronized void registerRecipeEntryProvider(RecipeEntryProvider provide } if (!recipeEntryProviders.contains(provider)) { recipeEntryProviders.add(provider); + recipeEntryProviderSnapshot = List.copyOf(recipeEntryProviders); } } - public synchronized List recipeEntryProviders() { - return List.copyOf(recipeEntryProviders); + public List recipeEntryProviders() { + return recipeEntryProviderSnapshot; } public synchronized void registerRecipeItemTooltipProvider(RecipeItemTooltipProvider provider) { @@ -347,11 +357,12 @@ public synchronized void registerRecipeItemTooltipProvider(RecipeItemTooltipProv } if (!recipeItemTooltipProviders.contains(provider)) { recipeItemTooltipProviders.add(provider); + recipeItemTooltipProviderSnapshot = List.copyOf(recipeItemTooltipProviders); } } - public synchronized List recipeItemTooltipProviders() { - return List.copyOf(recipeItemTooltipProviders); + public List recipeItemTooltipProviders() { + return recipeItemTooltipProviderSnapshot; } public synchronized void registerRecipeAnimationUpdateProvider(RecipeAnimationUpdateProvider provider) { @@ -360,11 +371,12 @@ public synchronized void registerRecipeAnimationUpdateProvider(RecipeAnimationUp } if (!recipeAnimationUpdateProviders.contains(provider)) { recipeAnimationUpdateProviders.add(provider); + recipeAnimationUpdateProviderSnapshot = List.copyOf(recipeAnimationUpdateProviders); } } - public synchronized List recipeAnimationUpdateProviders() { - return List.copyOf(recipeAnimationUpdateProviders); + public List recipeAnimationUpdateProviders() { + return recipeAnimationUpdateProviderSnapshot; } public synchronized void registerRecipeHandlerMetadataProvider(RecipeHandlerMetadataProvider provider) { @@ -373,11 +385,12 @@ public synchronized void registerRecipeHandlerMetadataProvider(RecipeHandlerMeta } if (!recipeHandlerMetadataProviders.contains(provider)) { recipeHandlerMetadataProviders.add(provider); + recipeHandlerMetadataProviderSnapshot = List.copyOf(recipeHandlerMetadataProviders); } } - public synchronized List recipeHandlerMetadataProviders() { - return List.copyOf(recipeHandlerMetadataProviders); + public List recipeHandlerMetadataProviders() { + return recipeHandlerMetadataProviderSnapshot; } public synchronized void registerRecipeHandlerSlotProvider(RecipeHandlerSlotProvider provider) { @@ -386,11 +399,12 @@ public synchronized void registerRecipeHandlerSlotProvider(RecipeHandlerSlotProv } if (!recipeHandlerSlotProviders.contains(provider)) { recipeHandlerSlotProviders.add(provider); + recipeHandlerSlotProviderSnapshot = List.copyOf(recipeHandlerSlotProviders); } } - public synchronized List recipeHandlerSlotProviders() { - return List.copyOf(recipeHandlerSlotProviders); + public List recipeHandlerSlotProviders() { + return recipeHandlerSlotProviderSnapshot; } public synchronized void registerRecipeDrawableRenderProvider(RecipeDrawableRenderProvider provider) { @@ -399,11 +413,12 @@ public synchronized void registerRecipeDrawableRenderProvider(RecipeDrawableRend } if (!recipeDrawableRenderProviders.contains(provider)) { recipeDrawableRenderProviders.add(provider); + recipeDrawableRenderProviderSnapshot = List.copyOf(recipeDrawableRenderProviders); } } - public synchronized List recipeDrawableRenderProviders() { - return List.copyOf(recipeDrawableRenderProviders); + public List recipeDrawableRenderProviders() { + return recipeDrawableRenderProviderSnapshot; } public synchronized void registerRecipeHandlerRenderProvider(RecipeHandlerRenderProvider provider) { @@ -412,11 +427,12 @@ public synchronized void registerRecipeHandlerRenderProvider(RecipeHandlerRender } if (!recipeHandlerRenderProviders.contains(provider)) { recipeHandlerRenderProviders.add(provider); + recipeHandlerRenderProviderSnapshot = List.copyOf(recipeHandlerRenderProviders); } } - public synchronized List recipeHandlerRenderProviders() { - return List.copyOf(recipeHandlerRenderProviders); + public List recipeHandlerRenderProviders() { + return recipeHandlerRenderProviderSnapshot; } public List queryRawCraftingHandlers(@Nullable ItemStack target) { diff --git a/src/main/java/com/hfstudio/guidenh/integration/api/RecipeSlot.java b/src/main/java/com/hfstudio/guidenh/integration/api/RecipeSlot.java index b2b947b8..38429835 100644 --- a/src/main/java/com/hfstudio/guidenh/integration/api/RecipeSlot.java +++ b/src/main/java/com/hfstudio/guidenh/integration/api/RecipeSlot.java @@ -4,16 +4,24 @@ import net.minecraft.item.ItemStack; +import org.jetbrains.annotations.Nullable; + public class RecipeSlot { private final int x; private final int y; private final List stacks; + private final @Nullable ItemStack currentStack; public RecipeSlot(int x, int y, List stacks) { + this(x, y, stacks, null); + } + + public RecipeSlot(int x, int y, List stacks, @Nullable ItemStack currentStack) { this.x = x; this.y = y; this.stacks = stacks == null ? List.of() : List.copyOf(stacks); + this.currentStack = currentStack; } public int x() { @@ -27,4 +35,9 @@ public int y() { public List stacks() { return stacks; } + + @Nullable + public ItemStack currentStack() { + return currentStack; + } } diff --git a/src/main/java/com/hfstudio/guidenh/integration/nei/NeiDirectCalls.java b/src/main/java/com/hfstudio/guidenh/integration/nei/NeiDirectCalls.java index 1b083ffe..607a68e6 100644 --- a/src/main/java/com/hfstudio/guidenh/integration/nei/NeiDirectCalls.java +++ b/src/main/java/com/hfstudio/guidenh/integration/nei/NeiDirectCalls.java @@ -7,15 +7,21 @@ import org.jetbrains.annotations.Nullable; +import codechicken.nei.NEIClientConfig; +import codechicken.nei.NEIClientUtils; +import codechicken.nei.NEIServerUtils; import codechicken.nei.PositionedStack; import codechicken.nei.drawable.DrawableResource; +import codechicken.nei.recipe.Badge; import codechicken.nei.recipe.GuiCraftingRecipe; +import codechicken.nei.recipe.GuiRecipe; import codechicken.nei.recipe.GuiRecipeTab; import codechicken.nei.recipe.GuiUsageRecipe; import codechicken.nei.recipe.HandlerInfo; import codechicken.nei.recipe.ICraftingHandler; import codechicken.nei.recipe.IRecipeHandler; import codechicken.nei.recipe.IUsageHandler; +import codechicken.nei.recipe.StackInfo; import codechicken.nei.recipe.TemplateRecipeHandler; public class NeiDirectCalls { @@ -54,10 +60,8 @@ public static List getUsageHandlers(ItemStack target) { GuiUsageRecipe.usagehandlers = new ArrayList<>(original); try { List result = GuiUsageRecipe.getUsageHandlers("item", target); - List out = new ArrayList<>(result.size()); - for (IUsageHandler h : result) { - if (h != null) out.add(h); - } + List out = copyHandlers(result); + addDamageableUsageHandlers(out, target); return out; } finally { GuiUsageRecipe.usagehandlers = original; @@ -143,14 +147,24 @@ public static boolean otherStacksThrows(Object handler, int idx) { } public static void handleItemTooltip(Object handler, ItemStack stack, List tooltip, int idx) { - // Passing null for GuiRecipe is intentional 鈥?we are not inside a live recipe GUI. - List result = h(handler).handleItemTooltip(null, stack, tooltip, idx); + GuiRecipe gui = currentGuiRecipe(); + List result = h(handler).handleItemTooltip(gui, stack, tooltip, idx); if (result != null && result != tooltip) { tooltip.clear(); tooltip.addAll(result); } } + @Nullable + private static GuiRecipe currentGuiRecipe() { + try { + if (NEIClientUtils.getGuiContainer() instanceof GuiReciperecipe) { + return recipe; + } + } catch (Throwable ignored) {} + return null; + } + public static int relX(Object ps) { return ((PositionedStack) ps).relx; } @@ -167,6 +181,133 @@ public static int relY(Object ps) { return ((PositionedStack) ps).item; } + public static List filteredPermutations(Object ps) { + return ((PositionedStack) ps).getFilteredPermutations(); + } + + /** + * Returns all candidate stacks for a positioned slot, keeping NEI's filtered order first and + * adding candidates hidden by the current search or preset filters afterwards. The embedded + * guide has no NEI recipe-group filter state, so dropping those candidates would make its + * accepts tooltip incomplete. + */ + public static List allPermutations(Object ps) { + PositionedStack positionedStack = (PositionedStack) ps; + List result = new ArrayList<>(); + try { + addUniqueCopies(result, positionedStack.getFilteredPermutations()); + } catch (Throwable ignored) { + // Raw PositionedStack.items remains available when an optional NEI filter is not initialized. + } + ItemStack[] rawItems = positionedStack.items; + if (rawItems != null) { + for (ItemStack item : rawItems) { + addUniqueCopy(result, item); + } + } + return result; + } + + public static void setPermutationToRender(Object ps, ItemStack stack) { + ((PositionedStack) ps).setPermutationToRender(stack); + } + + public static @Nullable List positionedTooltip(Object ps) { + return ((PositionedStack) ps).getTooltip(); + } + + public static @Nullable List positionedTooltip(Object handler, Object ps, boolean input) { + PositionedStack positionedStack = (PositionedStack) ps; + HandlerInfo handlerInfo = info(handler); + if (handlerInfo == null || !handlerInfo.getShowBadge()) { + return positionedStack.getTooltip(); + } + ArrayList tooltip = new ArrayList<>(); + List badges = positionedStack.getBadges(); + if (badges == null) { + badges = defaultBadges(positionedStack, input); + } + if (badges != null) { + for (Badge badge : badges) { + if (badge == null || badge.getTooltip() == null) continue; + tooltip.addAll(badge.getTooltip()); + } + } + List customTooltip = positionedStack.getTooltip(); + if (customTooltip != null) { + tooltip.addAll(customTooltip); + } + return tooltip.isEmpty() ? null : tooltip; + } + + public static @Nullable String acceptsLabel(Object ps) { + return ((PositionedStack) ps).getAcceptsLabel(); + } + + public static boolean showCycledIngredientsTooltip() { + return NEIClientConfig.showCycledIngredientsTooltip(); + } + + private static List defaultBadges(PositionedStack positionedStack, boolean input) { + ItemStack item = positionedStack.item; + if (item == null) return List.of(); + if (input) { + if (StackInfo.getAmount(item) == 0) return List.of(Badge.notConsumed()); + if (positionedStack.getChance() == 0) return List.of(Badge.notConsumedParallel()); + if (positionedStack.getChance() != PositionedStack.CHANCE_FULL) { + return List.of(Badge.consumeChance(positionedStack.getChance() / (float) PositionedStack.CHANCE_FULL)); + } + return List.of(); + } + if (positionedStack.getChance() != PositionedStack.CHANCE_FULL) { + return List.of(Badge.outputChance(positionedStack.getChance() / (float) PositionedStack.CHANCE_FULL)); + } + return List.of(); + } + + public static boolean contains(Object ps, int x, int y) { + return ((PositionedStack) ps).contains(x, y); + } + + public static boolean sameTypeWithNbt(ItemStack first, ItemStack second) { + return NEIServerUtils.areStacksSameTypeWithNBT(first, second); + } + + private static List copyHandlers(List handlers) { + List out = new ArrayList<>(handlers == null ? 0 : handlers.size()); + if (handlers != null) { + for (IRecipeHandler handler : handlers) { + if (handler != null && !out.contains(handler)) out.add(handler); + } + } + return out; + } + + private static void addDamageableUsageHandlers(List out, @Nullable ItemStack target) { + if (target == null || !target.isItemStackDamageable() || target.getItemDamage() > 0) return; + int maxDamage = target.getMaxDamage(); + if (maxDamage <= 0) return; + ItemStack damaged = target.copy(); + damaged.setItemDamage(Math.max(1, maxDamage - 1)); + List damagedHandlers = GuiUsageRecipe.getUsageHandlers("item", damaged); + for (IUsageHandler handler : damagedHandlers) { + if (handler != null && !out.contains(handler)) out.add(handler); + } + } + + private static void addUniqueCopies(List target, List values) { + if (values == null) return; + for (ItemStack value : values) addUniqueCopy(target, value); + } + + private static void addUniqueCopy(List target, @Nullable ItemStack value) { + if (value == null) return; + for (ItemStack existing : target) { + if (sameTypeWithNbt(existing, value)) return; + } + target.add(value.copy()); + } + // HandlerInfo via GuiRecipeTab.getHandlerInfo (public static method) @Nullable diff --git a/src/main/java/com/hfstudio/guidenh/integration/nei/NeiRecipeHandlerSlotProvider.java b/src/main/java/com/hfstudio/guidenh/integration/nei/NeiRecipeHandlerSlotProvider.java index 2e3ec628..8ef2ce28 100644 --- a/src/main/java/com/hfstudio/guidenh/integration/nei/NeiRecipeHandlerSlotProvider.java +++ b/src/main/java/com/hfstudio/guidenh/integration/nei/NeiRecipeHandlerSlotProvider.java @@ -44,6 +44,6 @@ public static RecipeSlot convertSlot(@Nullable NeiRecipeLookup.Slot slot) { if (slot == null) { return null; } - return new RecipeSlot(slot.relx, slot.rely, slot.stacks); + return new RecipeSlot(slot.relx, slot.rely, slot.stacks, slot.current); } } diff --git a/src/main/java/com/hfstudio/guidenh/integration/nei/NeiRecipeLookup.java b/src/main/java/com/hfstudio/guidenh/integration/nei/NeiRecipeLookup.java index 6c494b35..daf0e300 100644 --- a/src/main/java/com/hfstudio/guidenh/integration/nei/NeiRecipeLookup.java +++ b/src/main/java/com/hfstudio/guidenh/integration/nei/NeiRecipeLookup.java @@ -1,8 +1,14 @@ package com.hfstudio.guidenh.integration.nei; import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; +import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import org.jetbrains.annotations.Nullable; @@ -12,16 +18,27 @@ public class NeiRecipeLookup { + private static final Map PERMUTATION_CACHE = Collections + .synchronizedMap(new WeakHashMap<>()); + private static final Map HANDLER_FINGERPRINT_CACHE = Collections + .synchronizedMap(new WeakHashMap<>()); + public static class Slot { public final int relx; public final int rely; public final List stacks; + public final @Nullable ItemStack current; public Slot(int relx, int rely, List stacks) { + this(relx, rely, stacks, null); + } + + public Slot(int relx, int rely, List stacks, @Nullable ItemStack current) { this.relx = relx; this.rely = rely; this.stacks = stacks; + this.current = current; } } @@ -62,11 +79,16 @@ public static List findCraftingRecipeRefs(ItemStack target) { try { List handlers = NeiDirectCalls.getCraftingHandlers(target); List out = new ArrayList<>(); + Set fingerprints = new LinkedHashSet<>(); for (Object handler : handlers) { if (handler == null) continue; CraftingRecipeRef[] refs = readHandlerCraftingRecipeRefs(handler); if (refs != null && refs.length > 0) { - out.addAll(List.of(refs)); + for (CraftingRecipeRef ref : refs) { + if (ref != null && fingerprints.add(recipeFingerprint(ref.handler, ref.entry))) { + out.add(ref); + } + } } } return out; @@ -197,6 +219,273 @@ public static void appendItemTooltip(Object handler, ItemStack stack, List slots) { + out.append("["); + if (slots != null) { + for (Slot slot : slots) appendSlot(out, slot); + } + out.append("]"); + } + + private static void appendSlot(StringBuilder out, @Nullable Slot slot) { + if (slot == null) { + out.append("null;"); + return; + } + out.append(slot.relx) + .append(',') + .append(slot.rely) + .append('{'); + if (slot.stacks != null) { + for (ItemStack stack : slot.stacks) appendStack(out, stack); + } + out.append("};"); + } + + private static void appendStack(StringBuilder out, @Nullable ItemStack stack) { + if (stack == null || stack.getItem() == null) { + out.append("null,"); + return; + } + Object registryName = Item.itemRegistry.getNameForObject(stack.getItem()); + out.append( + registryName != null ? registryName + : stack.getItem() + .getClass() + .getName()) + .append('#') + .append(stack.getItemDamage()) + .append('x') + .append(stack.stackSize); + if (stack.stackTagCompound != null) out.append('@') + .append(stack.stackTagCompound); + out.append(','); + } + + private static String nullToEmpty(@Nullable String value) { + return value == null ? "" : value; + } + + public static class PositionedStackHit { + + public final Object positionedStack; + public final boolean ingredient; + + public PositionedStackHit(Object positionedStack, boolean ingredient) { + this.positionedStack = positionedStack; + this.ingredient = ingredient; + } + } + + @Nullable + public static List positionedTooltip(@Nullable Object positionedStack) { + if (positionedStack == null) return null; + try { + return NeiDirectCalls.positionedTooltip(positionedStack); + } catch (Throwable ignored) { + return null; + } + } + + @Nullable + public static List positionedTooltip(Object handler, Object positionedStack, boolean input) { + if (handler == null || positionedStack == null) return null; + try { + return NeiDirectCalls.positionedTooltip(handler, positionedStack, input); + } catch (Throwable ignored) { + return null; + } + } + + @Nullable + public static String acceptsLabel(@Nullable Object positionedStack) { + if (positionedStack == null) return null; + try { + return NeiDirectCalls.acceptsLabel(positionedStack); + } catch (Throwable ignored) { + return null; + } + } + + public static boolean showCycledIngredientsTooltip() { + try { + return NeiDirectCalls.showCycledIngredientsTooltip(); + } catch (Throwable ignored) { + return true; + } + } + + public static List filteredPermutations(@Nullable Object positionedStack) { + if (positionedStack == null) return List.of(); + ItemStack[] sourceItems; + try { + sourceItems = NeiDirectCalls.items(positionedStack); + } catch (Throwable ignored) { + sourceItems = null; + } + try { + List filtered = NeiDirectCalls.filteredPermutations(positionedStack); + PermutationCache cached = PERMUTATION_CACHE.get(positionedStack); + if (cached != null && cached.matches(sourceItems, filtered)) return cached.values; + List result = new ArrayList<>(); + addUniquePermutationCopies(result, filtered); + if (sourceItems != null) { + for (ItemStack source : sourceItems) { + addUniquePermutationCopy(result, source); + } + } + result = result.isEmpty() ? List.of() : List.copyOf(result); + PERMUTATION_CACHE.put(positionedStack, new PermutationCache(sourceItems, result, filtered)); + return result; + } catch (Throwable ignored) { + return List.of(); + } + } + + private static void addUniquePermutationCopies(List target, List values) { + if (values == null) return; + for (ItemStack value : values) addUniquePermutationCopy(target, value); + } + + private static void addUniquePermutationCopy(List target, @Nullable ItemStack value) { + if (value == null) return; + for (ItemStack existing : target) { + if (NeiDirectCalls.sameTypeWithNbt(existing, value)) return; + } + target.add(value.copy()); + } + + private static class PermutationCache { + + private final ItemStack[] sourceItems; + private final List values; + private final List filtered; + + private PermutationCache(ItemStack[] sourceItems, List values, List filtered) { + this.sourceItems = sourceItems; + this.values = values; + this.filtered = filtered; + } + + private boolean matches(ItemStack[] currentItems, List currentFiltered) { + if (sourceItems != currentItems && !sameItemArray(sourceItems, currentItems)) return false; + return sameItemList(filtered, currentFiltered); + } + + private static boolean sameItemArray(ItemStack[] first, ItemStack[] second) { + if (first == second) return true; + if (first == null || second == null || first.length != second.length) return false; + for (int index = 0; index < first.length; index++) { + if (first[index] != second[index]) return false; + } + return true; + } + + private static boolean sameItemList(List first, List second) { + if (first == second) return true; + if (first == null || second == null || first.size() != second.size()) return false; + for (int index = 0; index < first.size(); index++) { + ItemStack left = first.get(index); + ItemStack right = second.get(index); + if (left == right) continue; + if (left == null || right == null || !NeiDirectCalls.sameTypeWithNbt(left, right)) return false; + } + return true; + } + } + + @Nullable + public static PositionedStackHit findPositionedStackHit(Object handler, int recipeIndex, int localX, int localY) { + if (!Mods.NotEnoughItems.isModLoaded() || handler == null) return null; + try { + Object positionedStack = findPositionedStack( + safeList(NeiDirectCalls.ingredientStacks(handler, recipeIndex)), + localX, + localY); + if (positionedStack != null) return new PositionedStackHit(positionedStack, true); + positionedStack = findPositionedStack( + safeList(NeiDirectCalls.otherStacks(handler, recipeIndex)), + localX, + localY); + if (positionedStack != null) return new PositionedStackHit(positionedStack, false); + Object result = NeiDirectCalls.resultStack(handler, recipeIndex); + return result != null && NeiDirectCalls.contains(result, localX, localY) + ? new PositionedStackHit(result, false) + : null; + } catch (Throwable ignored) { + return null; + } + } + + @Nullable + public static Object findPositionedStack(Object handler, int recipeIndex, int localX, int localY) { + PositionedStackHit hit = findPositionedStackHit(handler, recipeIndex, localX, localY); + return hit == null ? null : hit.positionedStack; + } + + @Nullable + private static Object findPositionedStack(List stacks, int localX, int localY) { + for (Object positionedStack : stacks) { + if (positionedStack != null && NeiDirectCalls.contains(positionedStack, localX, localY)) { + return positionedStack; + } + } + return null; + } + + private static List safeList(List values) { + return values == null ? List.of() : values; + } + public static int lookupRecipeHeight(Object handler, int recipeIndex) { if (!Mods.NotEnoughItems.isModLoaded() || handler == null) return 0; try { @@ -209,7 +498,11 @@ public static int lookupRecipeHeight(Object handler, int recipeIndex) { public static List readIngredientSlots(Object handler, int recipeIndex) { if (!Mods.NotEnoughItems.isModLoaded() || handler == null) return List.of(); try { - return readSlotList(NeiDirectCalls.ingredientStacks(handler, recipeIndex)); + return readSlotList( + handler, + recipeIndex, + "ingredient", + NeiDirectCalls.ingredientStacks(handler, recipeIndex)); } catch (Throwable t) { return List.of(); } @@ -218,7 +511,7 @@ public static List readIngredientSlots(Object handler, int recipeIndex) { public static List readOtherSlots(Object handler, int recipeIndex) { if (!Mods.NotEnoughItems.isModLoaded() || handler == null) return List.of(); try { - return readSlotList(NeiDirectCalls.otherStacks(handler, recipeIndex)); + return readSlotList(handler, recipeIndex, "other", NeiDirectCalls.otherStacks(handler, recipeIndex)); } catch (Throwable t) { return List.of(); } @@ -241,7 +534,7 @@ public static boolean otherStacksThrows(Object handler, int recipeIndex) { public static @Nullable Slot readResultSlot(Object handler, int recipeIndex) { if (!Mods.NotEnoughItems.isModLoaded() || handler == null) return null; try { - return readSlot(NeiDirectCalls.resultStack(handler, recipeIndex)); + return readSlot(handler, recipeIndex, "result", 0, NeiDirectCalls.resultStack(handler, recipeIndex)); } catch (Throwable t) { return null; } @@ -339,9 +632,9 @@ public static void drawHandlerImage(Object drawable, int x, int y) { .getSimpleName(); CraftingRecipeRef[] out = new CraftingRecipeRef[n]; for (int i = 0; i < n; i++) { - List ing = readSlotList(NeiDirectCalls.ingredientStacks(handler, i)); - List oth = readSlotList(NeiDirectCalls.otherStacks(handler, i)); - Slot res = readSlot(NeiDirectCalls.resultStack(handler, i)); + List ing = readSlotList(handler, i, "ingredient", NeiDirectCalls.ingredientStacks(handler, i)); + List oth = readSlotList(handler, i, "other", NeiDirectCalls.otherStacks(handler, i)); + Slot res = readSlot(handler, i, "result", 0, NeiDirectCalls.resultStack(handler, i)); out[i] = new CraftingRecipeRef(handler, i, new Entry(handlerName, recipeName, ing, oth, res)); } return out; @@ -364,8 +657,9 @@ public static void drawHandlerImage(Object drawable, int x, int y) { public static List readSlotList(Object obj) { if (!(obj instanceof List)) return List.of(); - List out = new ArrayList<>(); - for (Object ps : (List) obj) { + List values = (List) obj; + List out = new ArrayList<>(values.size()); + for (Object ps : values) { Slot s = readSlot(ps); if (s != null) out.add(s); } @@ -373,12 +667,49 @@ public static List readSlotList(Object obj) { } public static @Nullable Slot readSlot(@Nullable Object ps) { + return readSlot(null, 0, "", 0, ps); + } + + public static void advancePermutations(Object handler, int recipeIndex) { + if (!Mods.NotEnoughItems.isModLoaded() || handler == null) return; + try { + NeiRecipePermutationController.advance(handler, recipeIndex); + } catch (Throwable ignored) {} + } + + public static boolean scrollPermutation(Object handler, int recipeIndex, int localX, int localY, int wheelDelta) { + if (!Mods.NotEnoughItems.isModLoaded() || handler == null) return false; + try { + return NeiRecipePermutationController.scroll(handler, recipeIndex, localX, localY, wheelDelta); + } catch (Throwable ignored) { + return false; + } + } + + private static List readSlotList(Object handler, int recipeIndex, String group, List values) { + if (values == null || values.isEmpty()) return List.of(); + if (handler != null) { + NeiRecipePermutationController.applyAll(handler, recipeIndex, group, values); + } + List out = new ArrayList<>(values.size()); + for (int index = 0; index < values.size(); index++) { + Slot slot = readSlot(null, recipeIndex, group, index, values.get(index)); + if (slot != null) out.add(slot); + } + return out; + } + + private static @Nullable Slot readSlot(@Nullable Object handler, int recipeIndex, String group, int ordinal, + @Nullable Object ps) { if (ps == null) return null; try { + if (handler != null) { + NeiRecipePermutationController.apply(handler, recipeIndex, group, ordinal, ps); + } int relx = NeiDirectCalls.relX(ps); int rely = NeiDirectCalls.relY(ps); - List stacks = new ArrayList<>(); ItemStack[] itemsArr = NeiDirectCalls.items(ps); + List stacks = itemsArr == null ? new ArrayList<>(1) : new ArrayList<>(itemsArr.length); if (itemsArr != null) { for (ItemStack s : itemsArr) { if (s != null) stacks.add(s); @@ -389,7 +720,7 @@ public static List readSlotList(Object obj) { if (single != null) stacks.add(single); } if (stacks.isEmpty()) return null; - return new Slot(relx, rely, stacks); + return new Slot(relx, rely, stacks, NeiDirectCalls.item(ps)); } catch (Throwable t) { return null; } diff --git a/src/main/java/com/hfstudio/guidenh/integration/nei/NeiRecipePermutationController.java b/src/main/java/com/hfstudio/guidenh/integration/nei/NeiRecipePermutationController.java new file mode 100644 index 00000000..e9fa5782 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/integration/nei/NeiRecipePermutationController.java @@ -0,0 +1,276 @@ +package com.hfstudio.guidenh.integration.nei; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; + +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.item.ItemStack; + +/** Keeps the currently rendered NEI permutation in sync with the embedded recipe view. */ +public class NeiRecipePermutationController { + + private static final Object LOCK = new Object(); + private static final WeakHashMap> STATES = new WeakHashMap<>(); + + private NeiRecipePermutationController() {} + + /** + * Advances every embedded recipe once per client tick. NEI advances its own permutation state + * from the widget tick counter, so this method must not be called from a render pass. + */ + public static void tick() { + synchronized (LOCK) { + if (GuiScreen.isShiftKeyDown()) return; + for (Map handlerStates : STATES.values()) { + for (RecipeState state : handlerStates.values()) { + state.renderRevision++; + state.clientTicks++; + int cycle = state.clientTicks / 20; + if (cycle == state.lastCycle) continue; + state.lastCycle = cycle; + state.candidateRevision++; + for (SlotState slot : state.slots.values()) { + if (slot.candidates.size() > 1) { + slot.index = (slot.index + 1) % slot.candidates.size(); + } + } + } + } + } + } + + public static void advance(Object handler, int recipeIndex) { + if (handler == null) return; + synchronized (LOCK) { + state(handler, recipeIndex); + } + } + + public static void apply(Object handler, int recipeIndex, String group, int ordinal, Object positionedStack) { + if (handler == null || positionedStack == null) return; + synchronized (LOCK) { + applyLocked(handler, recipeIndex, group, ordinal, positionedStack, state(handler, recipeIndex)); + } + } + + static void applyAll(Object handler, int recipeIndex, String group, List positionedStacks) { + if (handler == null || positionedStacks == null || positionedStacks.isEmpty()) return; + synchronized (LOCK) { + RecipeState state = state(handler, recipeIndex); + for (int ordinal = 0; ordinal < positionedStacks.size(); ordinal++) { + Object positionedStack = positionedStacks.get(ordinal); + if (positionedStack != null) { + applyLocked(handler, recipeIndex, group, ordinal, positionedStack, state); + } + } + } + } + + public static boolean scroll(Object handler, int recipeIndex, int localX, int localY, int wheelDelta) { + if (handler == null || wheelDelta == 0 || !GuiScreen.isShiftKeyDown()) return false; + synchronized (LOCK) { + RecipeState state = state(handler, recipeIndex); + List ingredients = safeList(NeiDirectCalls.ingredientStacks(handler, recipeIndex)); + List catalysts = safeList(NeiDirectCalls.otherStacks(handler, recipeIndex)); + ItemStack target = findAndAdvance( + handler, + recipeIndex, + "ingredient", + ingredients, + state, + localX, + localY, + wheelDelta); + if (target == null) { + target = findAndAdvance(handler, recipeIndex, "other", catalysts, state, localX, localY, wheelDelta); + } + if (target == null) return false; + setMatchingSlots(handler, recipeIndex, "ingredient", ingredients, state, target); + setMatchingSlots(handler, recipeIndex, "other", catalysts, state, target); + state.renderRevision++; + return true; + } + } + + private static ItemStack findAndAdvance(Object handler, int recipeIndex, String group, List stacks, + RecipeState state, int localX, int localY, int wheelDelta) { + for (int i = 0; i < stacks.size(); i++) { + Object positionedStack = stacks.get(i); + if (!NeiDirectCalls.contains(positionedStack, localX, localY)) continue; + applyLocked(handler, recipeIndex, group, i, positionedStack, state); + SlotState slot = state.slots + .get(new SlotKey(group, i, NeiDirectCalls.relX(positionedStack), NeiDirectCalls.relY(positionedStack))); + ItemStack target = chooseAdjacent(slot, wheelDelta); + if (target != null) return target; + } + return null; + } + + public static void clear() { + synchronized (LOCK) { + STATES.clear(); + } + } + + public static long revision(Object handler, int recipeIndex) { + if (handler == null) return -1L; + synchronized (LOCK) { + Map handlerStates = STATES.get(handler); + if (handlerStates == null) return -1L; + RecipeState state = handlerStates.get(recipeIndex); + return state == null ? -1L : state.renderRevision; + } + } + + private static void setMatchingSlots(Object handler, int recipeIndex, String group, List stacks, + RecipeState state, ItemStack target) { + for (int i = 0; i < stacks.size(); i++) { + Object positionedStack = stacks.get(i); + applyLocked(handler, recipeIndex, group, i, positionedStack, state); + SlotKey key = new SlotKey( + group, + i, + NeiDirectCalls.relX(positionedStack), + NeiDirectCalls.relY(positionedStack)); + SlotState slot = state.slots.get(key); + int index = findCandidate(slot == null ? List.of() : slot.candidates, target); + if (index >= 0) { + slot.index = index; + setPermutationIfChanged(positionedStack, target); + } + } + } + + private static ItemStack chooseAdjacent(SlotState slot, int wheelDelta) { + if (slot == null || slot.candidates.size() <= 1) return null; + slot.index = Math.floorMod(slot.index - Integer.signum(wheelDelta), slot.candidates.size()); + return slot.candidates.get(slot.index); + } + + private static void applyLocked(Object handler, int recipeIndex, String group, int ordinal, Object positionedStack, + RecipeState state) { + SlotKey key = new SlotKey( + group, + ordinal, + NeiDirectCalls.relX(positionedStack), + NeiDirectCalls.relY(positionedStack)); + SlotState slot = state.slots.computeIfAbsent(key, ignored -> new SlotState()); + applySlot(state, slot, positionedStack); + } + + private static void applySlot(RecipeState state, SlotState slot, Object positionedStack) { + ItemStack[] sourceItems = safeItems(positionedStack); + if (slot.candidateRevision != state.candidateRevision || !slot.matchesSource(positionedStack, sourceItems)) { + List candidates = resolveCandidates(positionedStack, sourceItems); + if (candidates.isEmpty()) { + ItemStack current = NeiDirectCalls.item(positionedStack); + if (current != null) candidates = List.of(current.copy()); + } + if (!sameCandidates(slot.candidates, candidates)) { + ItemStack current = NeiDirectCalls.item(positionedStack); + slot.candidates = candidates; + slot.index = findCandidate(candidates, current); + } + slot.sourcePositionedStack = positionedStack; + slot.sourceItems = sourceItems; + slot.candidateRevision = state.candidateRevision; + } + if (!slot.candidates.isEmpty()) { + slot.index = Math.floorMod(slot.index, slot.candidates.size()); + setPermutationIfChanged(positionedStack, slot.candidates.get(slot.index)); + } + } + + private static void setPermutationIfChanged(Object positionedStack, ItemStack target) { + if (NeiDirectCalls.item(positionedStack) != target) { + NeiDirectCalls.setPermutationToRender(positionedStack, target); + } + } + + private static RecipeState state(Object handler, int recipeIndex) { + return STATES.computeIfAbsent(handler, ignored -> new HashMap<>()) + .computeIfAbsent(recipeIndex, ignored -> new RecipeState()); + } + + private static List safeList(List values) { + return values == null ? List.of() : values; + } + + private static List resolveCandidates(Object positionedStack, ItemStack[] rawItems) { + List candidates; + try { + candidates = NeiRecipeLookup.filteredPermutations(positionedStack); + } catch (Throwable ignored) { + candidates = List.of(); + } + if (!candidates.isEmpty()) return candidates; + try { + if (rawItems == null || rawItems.length == 0) return List.of(); + List rawCandidates = new ArrayList<>(rawItems.length); + for (ItemStack rawItem : rawItems) { + if (rawItem != null) rawCandidates.add(rawItem.copy()); + } + return rawCandidates.isEmpty() ? List.of() : List.copyOf(rawCandidates); + } catch (Throwable ignored) { + return List.of(); + } + } + + private static ItemStack[] safeItems(Object positionedStack) { + try { + return NeiDirectCalls.items(positionedStack); + } catch (Throwable ignored) { + return null; + } + } + + private static boolean sameCandidates(List first, List second) { + if (first.size() != second.size()) return false; + for (int i = 0; i < first.size(); i++) { + if (!NeiDirectCalls.sameTypeWithNbt(first.get(i), second.get(i))) return false; + } + return true; + } + + private static int findCandidate(List candidates, ItemStack current) { + if (current == null) return 0; + for (int i = 0; i < candidates.size(); i++) { + if (NeiDirectCalls.sameTypeWithNbt(candidates.get(i), current)) return i; + } + return 0; + } + + private static class RecipeState { + + private int clientTicks; + private int lastCycle; + private long candidateRevision; + private long renderRevision; + private final Map slots = new HashMap<>(); + } + + private static class SlotState { + + private List candidates = List.of(); + private int index; + private long candidateRevision = -1; + private Object sourcePositionedStack; + private ItemStack[] sourceItems; + + private boolean matchesSource(Object positionedStack, ItemStack[] currentItems) { + if (currentItems == sourceItems && currentItems != null) return true; + if (currentItems != null && sourceItems != null && currentItems.length == sourceItems.length) { + for (int i = 0; i < currentItems.length; i++) { + if (currentItems[i] != sourceItems[i]) return false; + } + return true; + } + return currentItems == null && sourceItems == null && sourcePositionedStack == positionedStack; + } + } + + private record SlotKey(String group, int ordinal, int x, int y) {} +} diff --git a/src/main/java/com/hfstudio/guidenh/integration/structurelib/StructureLibSceneMetadata.java b/src/main/java/com/hfstudio/guidenh/integration/structurelib/StructureLibSceneMetadata.java index dbae54cf..44140f9f 100644 --- a/src/main/java/com/hfstudio/guidenh/integration/structurelib/StructureLibSceneMetadata.java +++ b/src/main/java/com/hfstudio/guidenh/integration/structurelib/StructureLibSceneMetadata.java @@ -94,13 +94,18 @@ public StructureLibSceneMetadata offsetBlockTooltips(int offsetX, int offsetY, i } public StructureLibSceneMetadata withTierData(int minValue, int maxValue, int defaultValue, int currentValue) { + return withTierData(minValue, maxValue, defaultValue, currentValue, false); + } + + public StructureLibSceneMetadata withTierData(int minValue, int maxValue, int defaultValue, int currentValue, + boolean locked) { return new StructureLibSceneMetadata( controller, piece, facing, rotation, flip, - new TierData(minValue, maxValue, defaultValue, currentValue), + new TierData(minValue, maxValue, defaultValue, currentValue, locked), channelDataList, blockTooltipDataByPos); } @@ -123,8 +128,13 @@ public StructureLibSceneMetadata withTierAndChannelData(int minValue, int maxVal } public StructureLibSceneMetadata withChannelData(String channelId, String label, int maxValue, int currentValue) { + return withChannelData(channelId, label, maxValue, currentValue, false); + } + + public StructureLibSceneMetadata withChannelData(String channelId, String label, int maxValue, int currentValue, + boolean locked) { LinkedHashMap updated = new LinkedHashMap<>(channelDataById); - ChannelData next = new ChannelData(channelId, label, maxValue, 0, currentValue); + ChannelData next = new ChannelData(channelId, label, maxValue, 0, currentValue, locked); updated.put(next.getChannelId(), next); return new StructureLibSceneMetadata( controller, @@ -137,6 +147,49 @@ public StructureLibSceneMetadata withChannelData(String channelId, String label, blockTooltipDataByPos); } + public StructureLibSceneMetadata withControlLocks(boolean tierLocked, @Nullable Set lockedChannels) { + TierData lockedTier = tierData; + if (tierData != null) { + lockedTier = new TierData( + tierData.minValue, + tierData.maxValue, + tierData.defaultValue, + tierData.currentValue, + tierData.locked || tierLocked); + } + Set normalizedChannels = new LinkedHashSet<>(); + if (lockedChannels != null) { + for (String channel : lockedChannels) { + String normalized = StructureLibPreviewSelection.normalizeChannelId(channel); + if (normalized != null) { + normalizedChannels.add(normalized); + } + } + } + List lockedChannelData = new ArrayList<>(channelDataList.size()); + for (ChannelData channel : channelDataList) { + if (channel != null) { + lockedChannelData.add( + new ChannelData( + channel.channelId, + channel.label, + channel.maxValue, + channel.defaultValue, + channel.currentValue, + channel.locked || normalizedChannels.contains(channel.channelId))); + } + } + return new StructureLibSceneMetadata( + controller, + piece, + facing, + rotation, + flip, + lockedTier, + lockedChannelData, + blockTooltipDataByPos); + } + @Nullable public BlockTooltipData getBlockTooltipData(int x, int y, int z) { return blockTooltipDataByPos.get(packBlockPos(x, y, z)); @@ -379,18 +432,24 @@ public static class TierData { private final int maxValue; private final int defaultValue; private final int currentValue; + private final boolean locked; public TierData(int minValue, int maxValue, int defaultValue, int currentValue) { + this(minValue, maxValue, defaultValue, currentValue, false); + } + + public TierData(int minValue, int maxValue, int defaultValue, int currentValue, boolean locked) { int normalizedMin = Math.max(1, minValue); int normalizedMax = Math.max(normalizedMin, maxValue); this.minValue = normalizedMin; this.maxValue = normalizedMax; this.defaultValue = clamp(defaultValue, normalizedMin, normalizedMax); this.currentValue = clamp(currentValue, normalizedMin, normalizedMax); + this.locked = locked; } public boolean isSelectable() { - return maxValue > minValue; + return !locked && maxValue > minValue; } } @@ -402,8 +461,14 @@ public static class ChannelData { private final int maxValue; private final int defaultValue; private final int currentValue; + private final boolean locked; public ChannelData(String channelId, String label, int maxValue, int defaultValue, int currentValue) { + this(channelId, label, maxValue, defaultValue, currentValue, false); + } + + public ChannelData(String channelId, String label, int maxValue, int defaultValue, int currentValue, + boolean locked) { String normalizedChannelId = StructureLibPreviewSelection.normalizeChannelId(channelId); String normalizedLabel = normalizeOptional(label); int normalizedMax = Math.max(0, maxValue); @@ -412,6 +477,7 @@ public ChannelData(String channelId, String label, int maxValue, int defaultValu this.maxValue = normalizedMax; this.defaultValue = clamp(defaultValue, 0, normalizedMax); this.currentValue = clamp(currentValue, 0, normalizedMax); + this.locked = locked; } public int getMinValue() { @@ -419,7 +485,7 @@ public int getMinValue() { } public boolean isSelectable() { - return maxValue > 0; + return !locked && maxValue > 0; } } } diff --git a/src/main/java/com/hfstudio/guidenh/integration/structurelib/StructureLibSceneOptions.java b/src/main/java/com/hfstudio/guidenh/integration/structurelib/StructureLibSceneOptions.java index 8304b341..0f618b36 100644 --- a/src/main/java/com/hfstudio/guidenh/integration/structurelib/StructureLibSceneOptions.java +++ b/src/main/java/com/hfstudio/guidenh/integration/structurelib/StructureLibSceneOptions.java @@ -2,7 +2,9 @@ import java.util.Collections; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.Map; +import java.util.Set; import org.jetbrains.annotations.Nullable; @@ -24,6 +26,10 @@ public class StructureLibSceneOptions { @Getter private final Map channelOverrides; @Getter + private final boolean tierLocked; + @Getter + private final Set lockedChannels; + @Getter private final boolean gregTechActiveController; @Getter private final boolean gregTechPlaceHatches; @@ -31,11 +37,28 @@ public class StructureLibSceneOptions { public StructureLibSceneOptions(@Nullable String facing, @Nullable String rotation, @Nullable String flip, @Nullable Integer tier, @Nullable Map channelOverrides, boolean gregTechActiveController, boolean gregTechPlaceHatches) { + this( + facing, + rotation, + flip, + tier, + channelOverrides, + false, + Collections.emptySet(), + gregTechActiveController, + gregTechPlaceHatches); + } + + public StructureLibSceneOptions(@Nullable String facing, @Nullable String rotation, @Nullable String flip, + @Nullable Integer tier, @Nullable Map channelOverrides, boolean tierLocked, + @Nullable Set lockedChannels, boolean gregTechActiveController, boolean gregTechPlaceHatches) { this.facing = normalizeOptional(facing); this.rotation = normalizeOptional(rotation); this.flip = normalizeOptional(flip); this.tier = tier != null && tier > 0 ? tier : null; this.channelOverrides = StructureLibPreviewSelection.immutableChannelOverrides(channelOverrides); + this.tierLocked = tier != null && tier > 0 && tierLocked; + this.lockedChannels = immutableChannelIds(lockedChannels); this.gregTechActiveController = gregTechActiveController; this.gregTechPlaceHatches = gregTechPlaceHatches; } @@ -73,6 +96,8 @@ public boolean hasOverrides() { || flip != null || tier != null || !channelOverrides.isEmpty() + || tierLocked + || !lockedChannels.isEmpty() || gregTechActiveController || gregTechPlaceHatches; } @@ -99,6 +124,8 @@ public StructureLibSceneOptions merge(StructureLibSceneOptions overrides) { overrides.flip != null ? overrides.flip : flip, overrides.tier != null ? overrides.tier : tier, channels, + overrides.tier != null ? overrides.tierLocked : tierLocked, + mergeLockedChannels(overrides.lockedChannels), overrides.gregTechActiveController || gregTechActiveController, overrides.gregTechPlaceHatches || gregTechPlaceHatches); } @@ -127,6 +154,29 @@ public static String normalizeOptional(@Nullable String value) { return trimmed.isEmpty() ? null : trimmed; } + private static Set immutableChannelIds(@Nullable Set source) { + if (source == null || source.isEmpty()) { + return Set.of(); + } + LinkedHashSet normalized = new LinkedHashSet<>(); + for (String value : source) { + String channel = StructureLibPreviewSelection.normalizeChannelId(value); + if (channel != null) { + normalized.add(channel); + } + } + return normalized.isEmpty() ? Set.of() : Set.copyOf(normalized); + } + + private Set mergeLockedChannels(Set overrides) { + if (overrides == null || overrides.isEmpty()) { + return lockedChannels; + } + LinkedHashSet merged = new LinkedHashSet<>(lockedChannels); + merged.addAll(overrides); + return merged; + } + public static class Builder { @Nullable @@ -138,6 +188,8 @@ public static class Builder { @Nullable private Integer tier; private final Map channels = new LinkedHashMap<>(); + private boolean tierLocked; + private final Set lockedChannels = new LinkedHashSet<>(); private boolean gregTechActiveController; private boolean gregTechPlaceHatches; @@ -161,6 +213,11 @@ public Builder tier(@Nullable Integer tier) { return this; } + public Builder tierLocked(boolean tierLocked) { + this.tierLocked = tierLocked; + return this; + } + public Builder channel(String name, int value) { String normalized = StructureLibPreviewSelection.normalizeChannelId(name); if (normalized != null && value > 0) { @@ -169,6 +226,18 @@ public Builder channel(String name, int value) { return this; } + public Builder channelLocked(String name, boolean locked) { + String normalized = StructureLibPreviewSelection.normalizeChannelId(name); + if (normalized != null) { + if (locked) { + lockedChannels.add(normalized); + } else { + lockedChannels.remove(normalized); + } + } + return this; + } + public Builder channels(@Nullable Map channels) { if (channels != null) { for (Map.Entry entry : channels.entrySet()) { @@ -198,6 +267,8 @@ public StructureLibSceneOptions build() { flip, tier, channels.isEmpty() ? Collections.emptyMap() : channels, + tierLocked, + lockedChannels, gregTechActiveController, gregTechPlaceHatches); } diff --git a/src/main/resources/assets/guidenh/siteexport/app.css b/src/main/resources/assets/guidenh/siteexport/app.css index 68d3f059..71f53d65 100644 --- a/src/main/resources/assets/guidenh/siteexport/app.css +++ b/src/main/resources/assets/guidenh/siteexport/app.css @@ -105,7 +105,7 @@ h2 { } h3 { - font-size: 1rem; + font-size: 1.17em; font-family: var(--minecraft-font); } @@ -1584,9 +1584,17 @@ p { } .guide-recipe-gallery { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + gap: 0.8em; margin-bottom: 0.8em; } +.guide-recipe-gallery > .recipe-grid { + margin-bottom: 0; +} + .guide-quest-link { color: var(--color-link); } @@ -1789,11 +1797,7 @@ p { } .recipe-grid { - display: inline-grid; - grid-template-rows: auto auto; - grid-template-columns: max-content 1fr max-content; - row-gap: 10px; - column-gap: 10px; + display: inline-block; align-items: center; padding: 2px; margin-bottom: 0.8em; @@ -1841,8 +1845,10 @@ p { .recipe-grid--nei-positioned .recipe-positioned-slot--phase1-overlay { box-sizing: border-box; - border: none; - background: transparent; + border: calc(1px * var(--gui-scale)) solid; + border-image-source: url('textures/slot.png'); + border-image-slice: 1 fill; + background-color: rgba(139, 139, 139, 0.35); pointer-events: auto; } @@ -1881,7 +1887,11 @@ p { } .recipe-main { - display: contents; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 10px; + min-width: 0; } .recipe-ingredients { @@ -1914,9 +1924,11 @@ p { .recipe-arrow { align-self: center; justify-self: center; - width: 85px; - height: 50px; + width: calc(24px * var(--gui-scale)); + height: calc(17px * var(--gui-scale)); + background: url('textures/recipe_arrow_light.png') center / 100% 100% no-repeat; image-rendering: pixelated; + flex: 0 0 auto; } .ingredient-box, diff --git a/web-editor/public/reference/de/_de/GameScene.md b/web-editor/public/reference/de/_de/GameScene.md index b8194f7b..08e5b6f9 100644 --- a/web-editor/public/reference/de/_de/GameScene.md +++ b/web-editor/public/reference/de/_de/GameScene.md @@ -411,6 +411,10 @@ Imports Eine StructureLib multiblock Vorschau by controller id. | `rotation` | nein | Drehung Überschreibung passed zu Die importer | | `flip` | nein | flip/mirror Überschreibung passed zu Die importer | | `channel` | nein | Ganzzahl channel Überschreibung für channel-aware structures | +| `tier` | nein | initialer tier-Wert für tier-aware structures | +| `channelName` | nein | Name des Kanals für den root `channel`- oder `value`-Override | +| `value` | nein | Kanalwert zusammen mit `channelName` | +| `locked` | nein | sperrt den ausdrücklich gesetzten tier- oder Kanalwert | | `offsetX` | nein | Ganzzahl X offset applied zu alle placed Blöcke (Standard `0`) | | `offsetY` | nein | Ganzzahl Y offset applied zu alle placed Blöcke, clamped zu `[0, worldHeight-1]` (Standard `0`) | | `offsetZ` | nein | Ganzzahl Z offset applied zu alle placed Blöcke (Standard `0`) | @@ -431,6 +435,8 @@ Beispiel: ````md + + ```` @@ -468,7 +474,9 @@ channel sliders. | untergeordnetes Element Tag | Bedeutung | | --- | --- | | `` | Master tier Wert. | +| `` | sperrt den ausdrücklich gesetzten tier- oder Kanalwert. | | `` | Named StructureLib channel Überschreibung. Repeat für mehrere Kanäle. | +| `` | sperrt den ausdrücklich gesetzten tier- oder Kanalwert. | | `` | Standard facing. | | `` | Standard Drehung. | | `` | Standard flip/mirror. | @@ -483,8 +491,8 @@ keeping Fallback casings by Standard. ````md - - + + diff --git a/web-editor/public/reference/de/_de/Tags-Reference.md b/web-editor/public/reference/de/_de/Tags-Reference.md index 0adfcb03..375c1312 100644 --- a/web-editor/public/reference/de/_de/Tags-Reference.md +++ b/web-editor/public/reference/de/_de/Tags-Reference.md @@ -77,7 +77,7 @@ Inline markdown auch unterstützt action Links für sound playback: | `` | Block-level BetterQuesting quest summary card (compat Tag, nur registriert wenn BetterQuesting ist geladen) | `id`, `show_desc`, `show_tooltip` | `` ist ein `` untergeordnetes Element Tag. It accepts `controller`, `piece`, `facing`, -`rotation`, `flip`, und `channel` Attribut, und auch unterstützt StructureLib Standard untergeordnetes Element Tags: +`rotation`, `flip`, und `tier`, `channel`, `channelName`, `value`, `locked` Attribut, und auch unterstützt StructureLib Standard untergeordnetes Element Tags: ``, ``, ``, ``, ``, ``, ``, und ``. @@ -679,7 +679,7 @@ diese Tags nur funktionieren innerhalb `` / ``: | Tag | Zweck | Schlüssel Attribut | | --- | --- | --- | | `` | import Eine extern SNBT/NBT structure Ressource | `src`, `x`, `y`, `z`, `offsetX`, `offsetY`, `offsetZ`, `formed` | -| `` | import Eine StructureLib multiblock by controller id | `controller`, `name`, `piece`, `facing`, `rotation`, `flip`, `channel`, `offsetX`, `offsetY`, `offsetZ`, `formed` | +| `` | import Eine StructureLib multiblock by controller id | `controller`, `name`, `piece`, `facing`, `rotation`, `flip`, `tier`, `channel`, `channelName`, `value`, `locked`, `offsetX`, `offsetY`, `offsetZ`, `formed` | | `` | entfernen bereits-placed Blöcke dass übereinstimmen Eine Block matcher | `id` | | `` | stamp Die gleich untergeordnetes Element Annotationen onto jede passend placed Block | `id` | diff --git a/web-editor/public/reference/en/_us/GameScene.md b/web-editor/public/reference/en/_us/GameScene.md index 53663eb6..04328256 100644 --- a/web-editor/public/reference/en/_us/GameScene.md +++ b/web-editor/public/reference/en/_us/GameScene.md @@ -407,7 +407,11 @@ Imports a StructureLib multiblock preview by controller id. | `facing` | no | facing override passed to the importer | | `rotation` | no | rotation override passed to the importer | | `flip` | no | flip/mirror override passed to the importer | +| `tier` | no | initial tier value for tier-aware structures | | `channel` | no | integer channel override for channel-aware structures | +| `channelName` | no | name of the channel used with the root `channel` or `value` override | +| `value` | no | channel value used with `channelName` | +| `locked` | no | when `true`, locks the explicitly supplied root tier or channel value during interaction | | `offsetX` | no | integer X offset applied to all placed blocks (default `0`) | | `offsetY` | no | integer Y offset applied to all placed blocks, clamped to `[0, worldHeight-1]` (default `0`) | | `offsetZ` | no | integer Z offset applied to all placed blocks (default `0`) | @@ -428,6 +432,8 @@ Example: ````md + + ```` @@ -465,7 +471,9 @@ channel sliders. | Child tag | Meaning | | --- | --- | | `` | Master tier value. | +| `` | Master tier value that cannot be changed by the user. | | `` | Named StructureLib channel override. Repeat for multiple channels. | +| `` | Named channel override that cannot be changed by the user. | | `` | Default facing. | | `` | Default rotation. | | `` | Default flip/mirror. | @@ -480,8 +488,8 @@ keeping fallback casings by default. ````md - - + + diff --git a/web-editor/public/reference/en/_us/Tags-Reference.md b/web-editor/public/reference/en/_us/Tags-Reference.md index e3abdf30..5b52ee60 100644 --- a/web-editor/public/reference/en/_us/Tags-Reference.md +++ b/web-editor/public/reference/en/_us/Tags-Reference.md @@ -74,7 +74,7 @@ Inline markdown also supports action links for sound playback: | `` | block-level BetterQuesting quest summary card (compat tag, only registered when BetterQuesting is loaded) | `id`, `show_desc`, `show_tooltip` | `` is a `` child tag. It accepts `controller`, `piece`, `facing`, -`rotation`, `flip`, and `channel` attributes, and also supports StructureLib default child tags: +`rotation`, `flip`, `tier`, `channel`, `channelName`, `value`, and `locked` attributes, and also supports StructureLib default child tags: ``, ``, ``, ``, ``, ``, ``, and ``. @@ -676,7 +676,7 @@ These tags only work inside `` / ``: | Tag | Purpose | Key attributes | | --- | --- | --- | | `` | import an external SNBT/NBT structure asset | `src`, `x`, `y`, `z`, `offsetX`, `offsetY`, `offsetZ`, `formed` | -| `` | import a StructureLib multiblock by controller id | `controller`, `name`, `piece`, `facing`, `rotation`, `flip`, `channel`, `offsetX`, `offsetY`, `offsetZ`, `formed` | +| `` | import a StructureLib multiblock by controller id | `controller`, `name`, `piece`, `facing`, `rotation`, `flip`, `tier`, `channel`, `channelName`, `value`, `locked`, `offsetX`, `offsetY`, `offsetZ`, `formed` | | `` | remove already-placed blocks that match a block matcher | `id` | | `` | stamp the same child annotations onto every matching placed block | `id` | diff --git a/web-editor/public/reference/es/_es/GameScene.md b/web-editor/public/reference/es/_es/GameScene.md index d851a879..c86222c5 100644 --- a/web-editor/public/reference/es/_es/GameScene.md +++ b/web-editor/public/reference/es/_es/GameScene.md @@ -411,6 +411,10 @@ importaciones Un StructureLib multiblock vista previa by controller id. | `rotation` | no | rotación anular passed Un El importer | | `flip` | no | flip/mirror anular passed Un El importer | | `channel` | no | entero channel anular para channel-aware estructuras | +| `tier` | no | valor inicial de tier para estructuras compatibles | +| `channelName` | no | nombre del canal usado con la anulación raíz `channel` o `value` | +| `value` | no | valor del canal usado con `channelName` | +| `locked` | no | bloquea el valor de tier o canal indicado explícitamente | | `offsetX` | no | entero X offset applied Un todos colocado bloques (predeterminado `0`) | | `offsetY` | no | entero Y offset applied Un todos colocado bloques, clamped a `[0, worldHeight-1]` (predeterminado `0`) | | `offsetZ` | no | entero Z offset applied Un todos colocado bloques (predeterminado `0`) | @@ -431,6 +435,8 @@ Ejemplo: ````md + + ```` @@ -468,7 +474,9 @@ channel sliders. | hijo etiqueta | significado | | --- | --- | | `` | Master tier valor. | +| `` | bloquea el valor de tier o canal indicado explícitamente. | | `` | Named StructureLib channel anular. Repeat para varios canales. | +| `` | bloquea el valor de tier o canal indicado explícitamente. | | `` | predeterminado facing. | | `` | predeterminado rotación. | | `` | predeterminado flip/mirror. | @@ -483,8 +491,8 @@ keeping reserva casings by predeterminado. ````md - - + + diff --git a/web-editor/public/reference/es/_es/Tags-Reference.md b/web-editor/public/reference/es/_es/Tags-Reference.md index c74cd4f5..fe544cb3 100644 --- a/web-editor/public/reference/es/_es/Tags-Reference.md +++ b/web-editor/public/reference/es/_es/Tags-Reference.md @@ -77,7 +77,7 @@ Inline markdown también admite action enlaces para sound playback: | `` | bloque-level BetterQuesting quest summary card (compat etiqueta, solo registrado cuando BetterQuesting es cargado) | `id`, `show_desc`, `show_tooltip` | `` es un `` hijo etiqueta. It accepts `controller`, `piece`, `facing`, -`rotation`, `flip`, y `channel` atributos, y también admite StructureLib predeterminado hijo etiquetas: +`rotation`, `flip`, y `tier`, `channel`, `channelName`, `value`, `locked` atributos, y también admite StructureLib predeterminado hijo etiquetas: ``, ``, ``, ``, ``, ``, ``, y ``. @@ -679,7 +679,7 @@ These etiquetas solo funcionan dentro de `` / ``: | etiqueta | Purpose | clave atributos | | --- | --- | --- | | `` | import Un externo SNBT/NBT estructura recurso | `src`, `x`, `y`, `z`, `offsetX`, `offsetY`, `offsetZ`, `formed` | -| `` | import Un StructureLib multiblock by controller id | `controller`, `name`, `piece`, `facing`, `rotation`, `flip`, `channel`, `offsetX`, `offsetY`, `offsetZ`, `formed` | +| `` | import Un StructureLib multiblock by controller id | `controller`, `name`, `piece`, `facing`, `rotation`, `flip`, `tier`, `channel`, `channelName`, `value`, `locked`, `offsetX`, `offsetY`, `offsetZ`, `formed` | | `` | eliminar ya-colocado bloques que coincidir Un bloque matcher | `id` | | `` | stamp El mismo hijo anotaciones onto cada coincidente colocado bloque | `id` | diff --git a/web-editor/public/reference/fr/_fr/GameScene.md b/web-editor/public/reference/fr/_fr/GameScene.md index bea15e0f..bbabee6e 100644 --- a/web-editor/public/reference/fr/_fr/GameScene.md +++ b/web-editor/public/reference/fr/_fr/GameScene.md @@ -411,6 +411,10 @@ Imports Un StructureLib multiblock aperçu by controller id. | `rotation` | non | rotation remplacer passed vers Le importer | | `flip` | non | flip/mirror remplacer passed vers Le importer | | `channel` | non | entier channel remplacer pour channel-aware structures | +| `tier` | non | valeur tier initiale pour les structures compatibles | +| `channelName` | non | nom du canal utilisé avec la surcharge racine `channel` ou `value` | +| `value` | non | valeur du canal utilisée avec `channelName` | +| `locked` | non | verrouille la valeur de tier ou de canal fournie explicitement | | `offsetX` | non | entier X offset applied vers tous placed blocs (par défaut `0`) | | `offsetY` | non | entier Y offset applied vers tous placed blocs, clamped vers `[0, worldHeight-1]` (par défaut `0`) | | `offsetZ` | non | entier Z offset applied vers tous placed blocs (par défaut `0`) | @@ -431,6 +435,8 @@ Exemple: ````md + + ```` @@ -468,7 +474,9 @@ channel sliders. | enfant balise | Meaning | | --- | --- | | `` | Master tier valeur. | +| `` | verrouille la valeur de tier ou de canal fournie explicitement. | | `` | Named StructureLib channel remplacer. Repeat pour plusieurs channels. | +| `` | verrouille la valeur de tier ou de canal fournie explicitement. | | `` | par défaut facing. | | `` | par défaut rotation. | | `` | par défaut flip/mirror. | @@ -483,8 +491,8 @@ keeping repli casings by par défaut. ````md - - + + diff --git a/web-editor/public/reference/fr/_fr/Tags-Reference.md b/web-editor/public/reference/fr/_fr/Tags-Reference.md index 162e5717..14f158ee 100644 --- a/web-editor/public/reference/fr/_fr/Tags-Reference.md +++ b/web-editor/public/reference/fr/_fr/Tags-Reference.md @@ -77,7 +77,7 @@ Inline markdown aussi prend en charge action liens pour sound playback: | `` | bloc-level BetterQuesting quest summary card (compat balise, seulement enregistré lorsque BetterQuesting est chargé) | `id`, `show_desc`, `show_tooltip` | `` est un `` enfant balise. It accepts `controller`, `piece`, `facing`, -`rotation`, `flip`, et `channel` attributs, et aussi prend en charge StructureLib par défaut enfant balises: +`rotation`, `flip`, et `tier`, `channel`, `channelName`, `value`, `locked` attributs, et aussi prend en charge StructureLib par défaut enfant balises: ``, ``, ``, ``, ``, ``, ``, et ``. @@ -679,7 +679,7 @@ These balises seulement fonctionnent dans `` / ``: | balise | Purpose | clé attributs | | --- | --- | --- | | `` | import Un externe SNBT/NBT structure ressource | `src`, `x`, `y`, `z`, `offsetX`, `offsetY`, `offsetZ`, `formed` | -| `` | import Un StructureLib multiblock by controller id | `controller`, `name`, `piece`, `facing`, `rotation`, `flip`, `channel`, `offsetX`, `offsetY`, `offsetZ`, `formed` | +| `` | import Un StructureLib multiblock by controller id | `controller`, `name`, `piece`, `facing`, `rotation`, `flip`, `tier`, `channel`, `channelName`, `value`, `locked`, `offsetX`, `offsetY`, `offsetZ`, `formed` | | `` | supprimer déjà-placed blocs que correspondre Un bloc matcher | `id` | | `` | stamp Le même enfant annotations onto chaque correspondant placed bloc | `id` | diff --git a/web-editor/public/reference/ja/_jp/GameScene.md b/web-editor/public/reference/ja/_jp/GameScene.md index 5a67a359..fc8699ae 100644 --- a/web-editor/public/reference/ja/_jp/GameScene.md +++ b/web-editor/public/reference/ja/_jp/GameScene.md @@ -411,6 +411,10 @@ Imports StructureLib multiblock プレビュー by controller id. | `rotation` | いいえ | 回転 上書き passed へ importer | | `flip` | いいえ | flip/mirror 上書き passed へ importer | | `channel` | いいえ | 整数 channel 上書き 用 channel-aware structures | +| `tier` | いいえ | tier 対応構造の初期 tier 値 | +| `channelName` | いいえ | ルートの `channel` または `value` 上書きに使う channel 名 | +| `value` | いいえ | `channelName` と組み合わせる channel 値 | +| `locked` | いいえ | 明示した tier または channel の値を固定 | | `offsetX` | いいえ | 整数 X offset applied へ すべての placed ブロック (既定値 `0`) | | `offsetY` | いいえ | 整数 Y offset applied へ すべての placed ブロック, clamped へ `[0, worldHeight-1]` (既定値 `0`) | | `offsetZ` | いいえ | 整数 Z offset applied へ すべての placed ブロック (既定値 `0`) | @@ -431,6 +435,8 @@ Imports StructureLib multiblock プレビュー by controller id. ````md + + ```` @@ -468,7 +474,9 @@ channel sliders. | 子 タグ | 意味 | | --- | --- | | `` | Master tier 値. | +| `` | 明示した tier または channel の値を固定. | | `` | Named StructureLib channel 上書き. Repeat 用 複数の channels. | +| `` | 明示した tier または channel の値を固定. | | `` | 既定値 facing. | | `` | 既定値 回転. | | `` | 既定値 flip/mirror. | @@ -483,8 +491,8 @@ keeping フォールバック casings by 既定値. ````md - - + + diff --git a/web-editor/public/reference/ja/_jp/Tags-Reference.md b/web-editor/public/reference/ja/_jp/Tags-Reference.md index e4849d94..639e01c4 100644 --- a/web-editor/public/reference/ja/_jp/Tags-Reference.md +++ b/web-editor/public/reference/ja/_jp/Tags-Reference.md @@ -77,7 +77,7 @@ Inline Markdown も 対応します action リンク 用 sound playback: | `` | ブロック-level BetterQuesting quest summary card (compat タグ, のみ 登録済み 場合 BetterQuesting です 読み込み済み) | `id`, `show_desc`, `show_tooltip` | `` は `` 子 タグ. It accepts `controller`, `piece`, `facing`, -`rotation`, `flip`, と `channel` 属性, と も 対応します StructureLib 既定値 子 タグ: +`rotation`, `flip`, と `tier`, `channel`, `channelName`, `value`, `locked` 属性, と も 対応します StructureLib 既定値 子 タグ: ``, ``, ``, ``, ``, ``, ``, と ``. @@ -679,7 +679,7 @@ $$\begin{pmatrix} a & b \\ c & d \end{pmatrix}$$ | タグ | Purpose | キー 属性 | | --- | --- | --- | | `` | import 外部 SNBT/NBT structure アセット | `src`, `x`, `y`, `z`, `offsetX`, `offsetY`, `offsetZ`, `formed` | -| `` | import StructureLib multiblock by controller id | `controller`, `name`, `piece`, `facing`, `rotation`, `flip`, `channel`, `offsetX`, `offsetY`, `offsetZ`, `formed` | +| `` | import StructureLib multiblock by controller id | `controller`, `name`, `piece`, `facing`, `rotation`, `flip`, `tier`, `channel`, `channelName`, `value`, `locked`, `offsetX`, `offsetY`, `offsetZ`, `formed` | | `` | 削除 すでに-placed ブロック that 一致 a ブロック matcher | `id` | | `` | stamp 同じ 子 注釈 onto すべての 一致する placed ブロック | `id` | diff --git a/web-editor/public/reference/nl/_nl/GameScene.md b/web-editor/public/reference/nl/_nl/GameScene.md index 20d709e5..c3f30138 100644 --- a/web-editor/public/reference/nl/_nl/GameScene.md +++ b/web-editor/public/reference/nl/_nl/GameScene.md @@ -411,6 +411,10 @@ Imports Een StructureLib multiblock voorbeeldweergave by controller id. | `rotation` | no | rotatie override passed naar De importer | | `flip` | no | flip/mirror override passed naar De importer | | `channel` | no | geheel getal channel override voor channel-aware structures | +| `tier` | nee | initiële tier-waarde voor tier-bewuste structuren | +| `channelName` | nee | naam van het kanaal voor de root `channel`- of `value`-override | +| `value` | nee | kanaalwaarde samen met `channelName` | +| `locked` | nee | vergrendelt de expliciet opgegeven tier- of kanaalwaarde | | `offsetX` | no | geheel getal X offset applied naar alle placed blokken (Standaard `0`) | | `offsetY` | no | geheel getal Y offset applied naar alle placed blokken, clamped naar `[0, worldHeight-1]` (Standaard `0`) | | `offsetZ` | no | geheel getal Z offset applied naar alle placed blokken (Standaard `0`) | @@ -431,6 +435,8 @@ Example: ````md + + ```` @@ -468,7 +474,9 @@ channel sliders. | kind tag | Betekenis | | --- | --- | | `` | Master tier waarde. | +| `` | vergrendelt de expliciet opgegeven tier- of kanaalwaarde. | | `` | Named StructureLib channel override. Repeat voor meerdere channels. | +| `` | vergrendelt de expliciet opgegeven tier- of kanaalwaarde. | | `` | Standaard facing. | | `` | Standaard rotatie. | | `` | Standaard flip/mirror. | @@ -483,8 +491,8 @@ keeping fallback casings by Standaard. ````md - - + + diff --git a/web-editor/public/reference/nl/_nl/Tags-Reference.md b/web-editor/public/reference/nl/_nl/Tags-Reference.md index 477c7c3f..61eb2bbc 100644 --- a/web-editor/public/reference/nl/_nl/Tags-Reference.md +++ b/web-editor/public/reference/nl/_nl/Tags-Reference.md @@ -77,7 +77,7 @@ Inline Markdown ook ondersteunt action links voor sound playback: | `` | blok-level BetterQuesting quest summary card (compat tag, alleen registered wanneer BetterQuesting is loaded) | `id`, `show_desc`, `show_tooltip` | `` is a `` kind tag. It accepts `controller`, `piece`, `facing`, -`rotation`, `flip`, en `channel` attributen, en ook ondersteunt StructureLib Standaard kind tags: +`rotation`, `flip`, en `tier`, `channel`, `channelName`, `value`, `locked` attributen, en ook ondersteunt StructureLib Standaard kind tags: ``, ``, ``, ``, ``, ``, ``, en ``. @@ -679,7 +679,7 @@ Deze tags alleen work binnen `` / ``: | tag | Purpose | Key attributen | | --- | --- | --- | | `` | import Een external SNBT/NBT structure asset | `src`, `x`, `y`, `z`, `offsetX`, `offsetY`, `offsetZ`, `formed` | -| `` | import Een StructureLib multiblock by controller id | `controller`, `name`, `piece`, `facing`, `rotation`, `flip`, `channel`, `offsetX`, `offsetY`, `offsetZ`, `formed` | +| `` | import Een StructureLib multiblock by controller id | `controller`, `name`, `piece`, `facing`, `rotation`, `flip`, `tier`, `channel`, `channelName`, `value`, `locked`, `offsetX`, `offsetY`, `offsetZ`, `formed` | | `` | verwijderen al-placed blokken that match Een blok matcher | `id` | | `` | stamp De zelfde kind Annotaties onto iedere matching placed blok | `id` | diff --git a/web-editor/public/reference/pl/_pl/GameScene.md b/web-editor/public/reference/pl/_pl/GameScene.md index e5e203e4..44995a21 100644 --- a/web-editor/public/reference/pl/_pl/GameScene.md +++ b/web-editor/public/reference/pl/_pl/GameScene.md @@ -411,6 +411,10 @@ importy StructureLib multiblock podgląd by controller id. | `rotation` | no | obrót nadpisywać passed do importer | | `flip` | no | flip/mirror nadpisywać passed do importer | | `channel` | no | liczba całkowita channel nadpisywać dla channel-aware struktury | +| `tier` | nie | początkowa wartość tier dla struktur obsługujących tier | +| `channelName` | nie | nazwa kanału używana z głównym nadpisaniem `channel` lub `value` | +| `value` | nie | wartość kanału używana z `channelName` | +| `locked` | nie | blokuje jawnie podaną wartość tier lub channel | | `offsetX` | no | liczba całkowita X offset applied do wszystkie umieszczony bloki (Domyślne `0`) | | `offsetY` | no | liczba całkowita Y offset applied do wszystkie umieszczony bloki, clamped do `[0, worldHeight-1]` (Domyślne `0`) | | `offsetZ` | no | liczba całkowita Z offset applied do wszystkie umieszczony bloki (Domyślne `0`) | @@ -431,6 +435,8 @@ Przykład: ````md + + ```` @@ -468,7 +474,9 @@ channel sliders. | element podrzędny tag | Znaczenie | | --- | --- | | `` | Master tier wartość. | +| `` | blokuje jawnie podaną wartość tier lub channel. | | `` | Named StructureLib channel nadpisywać. Repeat dla wiele kanały. | +| `` | blokuje jawnie podaną wartość tier lub channel. | | `` | Domyślne facing. | | `` | Domyślne obrót. | | `` | Domyślne flip/mirror. | @@ -483,8 +491,8 @@ keeping awaryjny casings by Domyślne. ````md - - + + diff --git a/web-editor/public/reference/pl/_pl/Tags-Reference.md b/web-editor/public/reference/pl/_pl/Tags-Reference.md index ec84143d..fb8f5c9b 100644 --- a/web-editor/public/reference/pl/_pl/Tags-Reference.md +++ b/web-editor/public/reference/pl/_pl/Tags-Reference.md @@ -77,7 +77,7 @@ Inline Markdown także obsługuje action links dla sound playback: | `` | blok-level BetterQuesting quest summary card (compat tag, tylko zarejestrowany gdy BetterQuesting is załadowany) | `id`, `show_desc`, `show_tooltip` | `` is a `` element podrzędny tag. It accepts `controller`, `piece`, `facing`, -`rotation`, `flip`, i `channel` atrybuty, i także obsługuje StructureLib Domyślne element podrzędny tagi: +`rotation`, `flip`, i `tier`, `channel`, `channelName`, `value`, `locked` atrybuty, i także obsługuje StructureLib Domyślne element podrzędny tagi: ``, ``, ``, ``, ``, ``, ``, i ``. @@ -679,7 +679,7 @@ Te tagi tylko działa wewnątrz `` / ``: | tag | Purpose | klucz atrybuty | | --- | --- | --- | | `` | import zewnętrzny SNBT/NBT struktura zasób | `src`, `x`, `y`, `z`, `offsetX`, `offsetY`, `offsetZ`, `formed` | -| `` | import StructureLib multiblock by controller id | `controller`, `name`, `piece`, `facing`, `rotation`, `flip`, `channel`, `offsetX`, `offsetY`, `offsetZ`, `formed` | +| `` | import StructureLib multiblock by controller id | `controller`, `name`, `piece`, `facing`, `rotation`, `flip`, `tier`, `channel`, `channelName`, `value`, `locked`, `offsetX`, `offsetY`, `offsetZ`, `formed` | | `` | usuwać już-umieszczony bloki który pasować blok matcher | `id` | | `` | stamp ten sam element podrzędny Adnotacje onto każdy pasujący umieszczony blok | `id` | diff --git a/web-editor/public/reference/pt/_br/GameScene.md b/web-editor/public/reference/pt/_br/GameScene.md index 7528d814..cfd7e746 100644 --- a/web-editor/public/reference/pt/_br/GameScene.md +++ b/web-editor/public/reference/pt/_br/GameScene.md @@ -411,6 +411,10 @@ Imports Uma StructureLib multiblock visualização by controller id. | `rotation` | não | rotação substituir passed para O importer | | `flip` | não | flip/mirror substituir passed para O importer | | `channel` | não | inteiro channel substituir para channel-aware structures | +| `tier` | não | valor inicial de tier para estruturas compatíveis | +| `channelName` | não | nome do canal usado com a substituição raiz `channel` ou `value` | +| `value` | não | valor do canal usado com `channelName` | +| `locked` | não | bloqueia o valor de tier ou canal fornecido explicitamente | | `offsetX` | não | inteiro X offset applied para todos placed blocos (Padrão `0`) | | `offsetY` | não | inteiro Y offset applied para todos placed blocos, clamped para `[0, worldHeight-1]` (Padrão `0`) | | `offsetZ` | não | inteiro Z offset applied para todos placed blocos (Padrão `0`) | @@ -431,6 +435,8 @@ Exemplo: ````md + + ```` @@ -468,7 +474,9 @@ channel sliders. | filho tag | Significado | | --- | --- | | `` | Master tier valor. | +| `` | bloqueia o valor de tier ou canal fornecido explicitamente. | | `` | Named StructureLib channel substituir. Repeat para vários channels. | +| `` | bloqueia o valor de tier ou canal fornecido explicitamente. | | `` | Padrão facing. | | `` | Padrão rotação. | | `` | Padrão flip/mirror. | @@ -483,8 +491,8 @@ keeping reserva casings by Padrão. ````md - - + + diff --git a/web-editor/public/reference/pt/_br/Tags-Reference.md b/web-editor/public/reference/pt/_br/Tags-Reference.md index b8d4673e..3d866190 100644 --- a/web-editor/public/reference/pt/_br/Tags-Reference.md +++ b/web-editor/public/reference/pt/_br/Tags-Reference.md @@ -77,7 +77,7 @@ Inline Markdown também suporta action links para sound playback: | `` | bloco-level BetterQuesting quest summary card (compat tag, somente registrado quando BetterQuesting é carregado) | `id`, `show_desc`, `show_tooltip` | `` é a `` filho tag. It accepts `controller`, `piece`, `facing`, -`rotation`, `flip`, e `channel` atributos, e também suporta StructureLib Padrão filho tags: +`rotation`, `flip`, e `tier`, `channel`, `channelName`, `value`, `locked` atributos, e também suporta StructureLib Padrão filho tags: ``, ``, ``, ``, ``, ``, ``, e ``. @@ -679,7 +679,7 @@ Estes tags somente funcionam dentro de `` / ``: | tag | Purpose | chave atributos | | --- | --- | --- | | `` | import Um externo SNBT/NBT structure recurso | `src`, `x`, `y`, `z`, `offsetX`, `offsetY`, `offsetZ`, `formed` | -| `` | import Uma StructureLib multiblock by controller id | `controller`, `name`, `piece`, `facing`, `rotation`, `flip`, `channel`, `offsetX`, `offsetY`, `offsetZ`, `formed` | +| `` | import Uma StructureLib multiblock by controller id | `controller`, `name`, `piece`, `facing`, `rotation`, `flip`, `tier`, `channel`, `channelName`, `value`, `locked`, `offsetX`, `offsetY`, `offsetZ`, `formed` | | `` | remover já-placed blocos que corresponder Uma bloco matcher | `id` | | `` | stamp O mesmo filho Anotações onto cada correspondente placed bloco | `id` | diff --git a/web-editor/public/reference/ru/_ru/GameScene.md b/web-editor/public/reference/ru/_ru/GameScene.md index 5a09f1b6..c81b48a0 100644 --- a/web-editor/public/reference/ru/_ru/GameScene.md +++ b/web-editor/public/reference/ru/_ru/GameScene.md @@ -405,6 +405,10 @@ GuideNH в настоящее время регистрирует следующ | `rotation` | нет | переопределение поворота передано импортеру | | `flip` | нет | переопределение переворота/зеркала передано импортеру | | `channel` | нет | переопределение целочисленного канала для структур, поддерживающих канал. | +| `tier` | нет | начальное значение tier для структур с уровнями | +| `channelName` | нет | имя канала для корневого переопределения `channel` или `value` | +| `value` | нет | значение канала вместе с `channelName` | +| `locked` | нет | блокирует явно заданное значение tier или канала | | `offsetX` | нет | целочисленное смещение по оси X, применяемое ко всем размещенным блокам (по умолчанию `0`). | | `offsetY` | нет | целочисленное смещение по оси Y, применяемое ко всем размещенным блокам, ограниченное значением `[0, worldHeight-1]` (по умолчанию `0`). | | `offsetZ` | нет | целочисленное смещение по оси Z, применяемое ко всем размещенным блокам (по умолчанию `0`). | @@ -425,6 +429,8 @@ GuideNH в настоящее время регистрирует следующ ````md + + ```` @@ -462,7 +468,9 @@ GuideNH в настоящее время регистрирует следующ | Дочерний тег | Значение | | --- | --- | | `` | Значение уровня «Мастер». | +| `` | блокирует явно заданное значение tier или канала. | | `` | переопределение именованного канала StructureLib. Повторите для нескольких каналов. | +| `` | блокирует явно заданное значение tier или канала. | | `` | Облицовка по умолчанию. | | `` | вращение по умолчанию. | | `` | Переворот/зеркало по умолчанию. | @@ -477,8 +485,8 @@ GuideNH в настоящее время регистрирует следующ ````md - - + + diff --git a/web-editor/public/reference/ru/_ru/Tags-Reference.md b/web-editor/public/reference/ru/_ru/Tags-Reference.md index 240c124d..c939b892 100644 --- a/web-editor/public/reference/ru/_ru/Tags-Reference.md +++ b/web-editor/public/reference/ru/_ru/Tags-Reference.md @@ -74,7 +74,7 @@ | `` | сводная карточка квеста BetterQuesting на уровне блока (тег совместимости, регистрируется только при загрузке BetterQuesting) | `id`, `show_desc`, `show_tooltip` | `` — это дочерний тег ``. Он принимает `controller`, `piece`, `facing`, -атрибуты `rotation`, `flip` и `channel`, а также поддерживают дочерние теги StructureLib по умолчанию: +атрибуты `rotation`, `flip` и `tier`, `channel`, `channelName`, `value`, `locked`, а также поддерживают дочерние теги StructureLib по умолчанию: ``, ``, ``, ``, ``, ``, `` и ``. @@ -676,7 +676,7 @@ $$\begin{pmatrix} a & b \\ c & d \end{pmatrix}$$ | Тег | Цель | Ключевые атрибуты | | --- | --- | --- | | `` | импорт внешнего актива структуры SNBT/NBT. | `src`, `x`, `y`, `z`, `offsetX`, `offsetY`, `offsetZ`, `formed` | -| `` | импортируйте мультиблок StructureLib по идентификатору контроллера. | `controller`, `name`, `piece`, `facing`, `rotation`, `flip`, `channel`, `offsetX`, `offsetY`, `offsetZ`, `formed` | +| `` | импортируйте мультиблок StructureLib по идентификатору контроллера. | `controller`, `name`, `piece`, `facing`, `rotation`, `flip`, `tier`, `channel`, `channelName`, `value`, `locked`, `offsetX`, `offsetY`, `offsetZ`, `formed` | | `` | удалить уже размещенные блоки, соответствующие сопоставителю блоков. | `id` | | `` | наносить одни и те же дочерние аннотации на каждый соответствующий размещенный блок. | `id` | diff --git a/web-editor/public/reference/uk/_ua/GameScene.md b/web-editor/public/reference/uk/_ua/GameScene.md index 2dcd5360..a6d80110 100644 --- a/web-editor/public/reference/uk/_ua/GameScene.md +++ b/web-editor/public/reference/uk/_ua/GameScene.md @@ -411,6 +411,10 @@ Places a блок до перегляд світ. | `rotation` | no | обертання перевизначати passed до importer | | `flip` | no | flip/mirror перевизначати passed до importer | | `channel` | no | ціле число channel перевизначати для channel-aware структури | +| `tier` | ні | початкове значення tier для структур із рівнями | +| `channelName` | ні | назва каналу для кореневого перевизначення `channel` або `value` | +| `value` | ні | значення каналу разом із `channelName` | +| `locked` | ні | блокує явно задане значення tier або каналу | | `offsetX` | no | ціле число X offset applied до усі розміщений блоки (Типове `0`) | | `offsetY` | no | ціле число Y offset applied до усі розміщений блоки, clamped до `[0, worldHeight-1]` (Типове `0`) | | `offsetZ` | no | ціле число Z offset applied до усі розміщений блоки (Типове `0`) | @@ -431,6 +435,8 @@ Places a блок до перегляд світ. ````md + + ```` @@ -468,7 +474,9 @@ channel sliders. | дочірній елемент тег | Значення | | --- | --- | | `` | Master tier значення. | +| `` | блокує явно задане значення tier або каналу. | | `` | Named StructureLib channel перевизначати. Repeat для кілька канали. | +| `` | блокує явно задане значення tier або каналу. | | `` | Типове facing. | | `` | Типове обертання. | | `` | Типове flip/mirror. | @@ -483,8 +491,8 @@ keeping резервний варіант casings by Типове. ````md - - + + diff --git a/web-editor/public/reference/uk/_ua/Tags-Reference.md b/web-editor/public/reference/uk/_ua/Tags-Reference.md index cf3ce2e3..d0e1cf62 100644 --- a/web-editor/public/reference/uk/_ua/Tags-Reference.md +++ b/web-editor/public/reference/uk/_ua/Tags-Reference.md @@ -77,7 +77,7 @@ Inline Markdown також підтримує action links для sound playback | `` | блок-level BetterQuesting quest summary card (compat тег, лише зареєстрований коли BetterQuesting is завантажений) | `id`, `show_desc`, `show_tooltip` | `` is a `` дочірній елемент тег. It accepts `controller`, `piece`, `facing`, -`rotation`, `flip`, і `channel` атрибути, і також підтримує StructureLib Типове дочірній елемент теги: +`rotation`, `flip`, і `tier`, `channel`, `channelName`, `value`, `locked` атрибути, і також підтримує StructureLib Типове дочірній елемент теги: ``, ``, ``, ``, ``, ``, ``, і ``. @@ -679,7 +679,7 @@ $$\begin{pmatrix} a & b \\ c & d \end{pmatrix}$$ | тег | Purpose | ключ атрибути | | --- | --- | --- | | `` | import зовнішній SNBT/NBT структура ресурс | `src`, `x`, `y`, `z`, `offsetX`, `offsetY`, `offsetZ`, `formed` | -| `` | import StructureLib multiblock by controller id | `controller`, `name`, `piece`, `facing`, `rotation`, `flip`, `channel`, `offsetX`, `offsetY`, `offsetZ`, `formed` | +| `` | import StructureLib multiblock by controller id | `controller`, `name`, `piece`, `facing`, `rotation`, `flip`, `tier`, `channel`, `channelName`, `value`, `locked`, `offsetX`, `offsetY`, `offsetZ`, `formed` | | `` | видалити вже-розміщений блоки який відповідати a блок matcher | `id` | | `` | stamp той самий дочірній елемент Анотації onto кожен відповідний розміщений блок | `id` | diff --git a/web-editor/public/reference/zh/_cn/GameScene.md b/web-editor/public/reference/zh/_cn/GameScene.md index 2d52c618..c4d5328f 100644 --- a/web-editor/public/reference/zh/_cn/GameScene.md +++ b/web-editor/public/reference/zh/_cn/GameScene.md @@ -382,7 +382,11 @@ GuideNH 当前注册了以下场景子标签: | `facing` | 否 | 传给导入器的朝向覆盖值 | | `rotation` | 否 | 传给导入器的旋转覆盖值 | | `flip` | 否 | 传给导入器的镜像覆盖值 | +| `tier` | 否 | 支持 tier 的结构的初始 tier 值 | | `channel` | 否 | 支持频道结构的频道整数覆盖值 | +| `channelName` | 否 | 与根标签 `channel` 或 `value` 覆盖值配合使用的频道名称 | +| `value` | 否 | 与 `channelName` 配合使用的频道值 | +| `locked` | 否 | 为 `true` 时锁定根标签显式提供的 tier 或频道值,交互时不能修改 | | `offsetX` | 否 | 所有放置方块的 X 偏移,整数,默认 `0` | | `offsetY` | 否 | 所有放置方块的 Y 偏移,整数,会被限制在 `[0, 世界高度-1]`,默认 `0` | | `offsetZ` | 否 | 所有放置方块的 Z 偏移,整数,默认 `0` | @@ -403,6 +407,8 @@ GuideNH 当前注册了以下场景子标签: ````md + + ```` @@ -439,7 +445,9 @@ StructureLib 默认值也可以写成子标签。这些默认值会成为场景 | 子标签 | 含义 | | --- | --- | | `` | 主 tier 值。 | +| `` | 锁定为 4 的主 tier 值,用户不能修改。 | | `` | 具名 StructureLib channel 覆盖值;可以重复。 | +| `` | 锁定的具名 channel 覆盖值,用户不能修改。 | | `` | 默认朝向。 | | `` | 默认旋转。 | | `` | 默认翻转 / 镜像。 | @@ -453,8 +461,8 @@ StructureLib 默认值也可以写成子标签。这些默认值会成为场景 ````md - - + + diff --git a/web-editor/public/reference/zh/_cn/Tags-Reference.md b/web-editor/public/reference/zh/_cn/Tags-Reference.md index 1163bbbf..4020e252 100644 --- a/web-editor/public/reference/zh/_cn/Tags-Reference.md +++ b/web-editor/public/reference/zh/_cn/Tags-Reference.md @@ -76,7 +76,7 @@ | `` | 块级 BetterQuesting 任务摘要卡片(兼容标签,仅当 BetterQuesting 已加载时注册) | `id`, `show_desc`, `show_tooltip` | `` 是 `` 的子标签。它支持 `controller`、`piece`、`facing`、 -`rotation`、`flip`、`channel` 属性,也支持用于设置 StructureLib 默认值的子标签: +`rotation`、`flip`、`tier`、`channel`、`channelName`、`value`、`locked` 属性,也支持用于设置 StructureLib 默认值的子标签: ``、``、``、``、``、``、 ``、``。 @@ -673,7 +673,7 @@ $$\begin{pmatrix} a & b \\ c & d \end{pmatrix}$$ | 标签 | 用途 | 关键属性 | | --- | --- | --- | | `` | 导入外部 SNBT/NBT 结构资源 | `src`, `x`, `y`, `z`, `offsetX`, `offsetY`, `offsetZ`, `formed` | -| `` | 通过控制器 id 导入 StructureLib 多方块 | `controller`, `name`, `piece`, `facing`, `rotation`, `flip`, `channel`, `offsetX`, `offsetY`, `offsetZ`, `formed` | +| `` | 通过控制器 id 导入 StructureLib 多方块 | `controller`, `name`, `piece`, `facing`, `rotation`, `flip`, `tier`, `channel`, `channelName`, `value`, `locked`, `offsetX`, `offsetY`, `offsetZ`, `formed` | | `` | 移除已放置且匹配指定方块匹配器的方块 | `id` | | `` | 把同一组子注解扩展到场景中所有匹配方块上 | `id` | diff --git a/web-editor/public/reference/zh/_tw/GameScene.md b/web-editor/public/reference/zh/_tw/GameScene.md index 556aec20..3a5b225e 100644 --- a/web-editor/public/reference/zh/_tw/GameScene.md +++ b/web-editor/public/reference/zh/_tw/GameScene.md @@ -383,6 +383,10 @@ GuideNH 目前註冊了以下場景子標籤: | `rotation` | 否 | 傳給導入器的旋轉覆蓋值 | | `flip` | 否 | 傳給導入器的鏡像覆蓋值 | | `channel` | 否 | 支援頻道結構的頻道整數覆蓋值 | +| `tier` | 否 | 支援 tier 的結構初始 tier 值 | +| `channelName` | 否 | 與根標籤 `channel` 或 `value` 覆蓋值搭配的頻道名稱 | +| `value` | 否 | 與 `channelName` 搭配使用的頻道值 | +| `locked` | 否 | 鎖定明確提供的 tier 或頻道值 | | `offsetX` | 否 | 所有放置方塊的 X 偏移,整數,預設 `0` | | `offsetY` | 否 | 所有放置方塊的 Y 偏移,整數,會被限制在 `[0, 世界高度-1]`,預設 `0` | | `offsetZ` | 否 | 所有放置方塊的 Z 偏移,整數,預設 `0` | @@ -403,6 +407,8 @@ GuideNH 目前註冊了以下場景子標籤: ````md + + ```` @@ -439,7 +445,9 @@ StructureLib 預設值也可以寫成子標籤。這些預設值會成為場景 | 子標籤 | 意義 | | --- | --- | | `` | 主 tier 值。 | +| `` | 鎖定明確提供的 tier 或頻道值. | | `` | 具名 StructureLib channel 覆蓋值;可重複。 | +| `` | 鎖定明確提供的 tier 或頻道值. | | `` | 預設朝向。 | | `` | 默认旋转。 | | `` | 默认翻转 / 镜像。 | @@ -453,8 +461,8 @@ StructureLib 預設值也可以寫成子標籤。這些預設值會成為場景 ````md - - + + diff --git a/web-editor/public/reference/zh/_tw/Tags-Reference.md b/web-editor/public/reference/zh/_tw/Tags-Reference.md index f5ad56af..11299931 100644 --- a/web-editor/public/reference/zh/_tw/Tags-Reference.md +++ b/web-editor/public/reference/zh/_tw/Tags-Reference.md @@ -76,7 +76,7 @@ | `` | 區塊級 BetterQuesting 任務摘要卡片(相容標籤,僅當 BetterQuesting 已載入時註冊) | `id`, `show_desc`, `show_tooltip` | `` 是 `` 的子標籤。它支援 `controller`、`piece`、`facing`、 -`rotation`、`flip`、`channel` 屬性,也支援用於設定 StructureLib 預設值的子標籤: +`rotation`、`flip`、`tier`, `channel`, `channelName`, `value`, `locked` 屬性,也支援用於設定 StructureLib 預設值的子標籤: ``、``、``、``、``、``、 ``、``。 @@ -673,7 +673,7 @@ $$\begin{pmatrix} a & b \\ c & d \end{pmatrix}$$ | 标签 | 用途 | 关键属性 | | --- | --- | --- | | `` | 导入外部 SNBT/NBT 结构资源 | `src`, `x`, `y`, `z`, `offsetX`, `offsetY`, `offsetZ`, `formed` | -| `` | 透過控制器 id 導入 StructureLib 多方塊 | `controller`, `name`, `piece`, `facing`, `rotation`, `flip`, `channel`, `offsetX`, `offsetY`, `offsetZ`, `formed` | +| `` | 透過控制器 id 導入 StructureLib 多方塊 | `controller`, `name`, `piece`, `facing`, `rotation`, `flip`, `tier`, `channel`, `channelName`, `value`, `locked`, `offsetX`, `offsetY`, `offsetZ`, `formed` | | `` | 移除已放置且符合指定方塊匹配器的方塊 | `id` | | `` | 把同一組子註解擴展到場景中所有符合方塊上 | `id` | diff --git a/web-editor/public/styles.css b/web-editor/public/styles.css index 0b0f5e17..f07e2d34 100644 --- a/web-editor/public/styles.css +++ b/web-editor/public/styles.css @@ -201,7 +201,7 @@ input:focus, textarea:focus { border-color: var(--accent); box-shadow: 0 0 0 2px .guide-page h4, .guide-page h5, .guide-page h6 { margin: 1em 0 .4em; line-height: 1.25; } .guide-page h1 { font-size: 28px; color: var(--accent); } .guide-page h2 { font-size: 21px; } -.guide-page h3 { font-size: 16px; } +.guide-page h3 { font-size: 1.17em; } .guide-page p { line-height: 1.7; margin: .75em 0; } .guide-page pre { max-width: 100%; white-space: pre-wrap; } .guide-page hr { margin: 1.3em 0; border: 0; border-top: 1px solid var(--border); } diff --git a/wiki/GameScene-zh-CN.md b/wiki/GameScene-zh-CN.md index c97c6b0f..e88abcea 100644 --- a/wiki/GameScene-zh-CN.md +++ b/wiki/GameScene-zh-CN.md @@ -382,7 +382,11 @@ GuideNH 当前注册了以下场景子标签: | `facing` | 否 | 传给导入器的朝向覆盖值 | | `rotation` | 否 | 传给导入器的旋转覆盖值 | | `flip` | 否 | 传给导入器的镜像覆盖值 | +| `tier` | 否 | 支持 tier 的结构的初始 tier 值 | | `channel` | 否 | 支持频道结构的频道整数覆盖值 | +| `channelName` | 否 | 与根标签 `channel` 或 `value` 覆盖值配合使用的频道名称 | +| `value` | 否 | 与 `channelName` 配合使用的频道值 | +| `locked` | 否 | 为 `true` 时锁定根标签显式提供的 tier 或频道值,交互时不能修改 | | `offsetX` | 否 | 所有放置方块的 X 偏移,整数,默认 `0` | | `offsetY` | 否 | 所有放置方块的 Y 偏移,整数,会被限制在 `[0, 世界高度-1]`,默认 `0` | | `offsetZ` | 否 | 所有放置方块的 Z 偏移,整数,默认 `0` | @@ -403,6 +407,8 @@ GuideNH 当前注册了以下场景子标签: ````md + + ```` @@ -439,7 +445,9 @@ StructureLib 默认值也可以写成子标签。这些默认值会成为场景 | 子标签 | 含义 | | --- | --- | | `` | 主 tier 值。 | +| `` | 锁定为 4 的主 tier 值,用户不能修改。 | | `` | 具名 StructureLib channel 覆盖值;可以重复。 | +| `` | 锁定的具名 channel 覆盖值,用户不能修改。 | | `` | 默认朝向。 | | `` | 默认旋转。 | | `` | 默认翻转 / 镜像。 | @@ -453,8 +461,8 @@ StructureLib 默认值也可以写成子标签。这些默认值会成为场景 ````md - - + + diff --git a/wiki/GameScene.md b/wiki/GameScene.md index 53663eb6..04328256 100644 --- a/wiki/GameScene.md +++ b/wiki/GameScene.md @@ -407,7 +407,11 @@ Imports a StructureLib multiblock preview by controller id. | `facing` | no | facing override passed to the importer | | `rotation` | no | rotation override passed to the importer | | `flip` | no | flip/mirror override passed to the importer | +| `tier` | no | initial tier value for tier-aware structures | | `channel` | no | integer channel override for channel-aware structures | +| `channelName` | no | name of the channel used with the root `channel` or `value` override | +| `value` | no | channel value used with `channelName` | +| `locked` | no | when `true`, locks the explicitly supplied root tier or channel value during interaction | | `offsetX` | no | integer X offset applied to all placed blocks (default `0`) | | `offsetY` | no | integer Y offset applied to all placed blocks, clamped to `[0, worldHeight-1]` (default `0`) | | `offsetZ` | no | integer Z offset applied to all placed blocks (default `0`) | @@ -428,6 +432,8 @@ Example: ````md + + ```` @@ -465,7 +471,9 @@ channel sliders. | Child tag | Meaning | | --- | --- | | `` | Master tier value. | +| `` | Master tier value that cannot be changed by the user. | | `` | Named StructureLib channel override. Repeat for multiple channels. | +| `` | Named channel override that cannot be changed by the user. | | `` | Default facing. | | `` | Default rotation. | | `` | Default flip/mirror. | @@ -480,8 +488,8 @@ keeping fallback casings by default. ````md - - + + diff --git a/wiki/Tags-Reference-zh-CN.md b/wiki/Tags-Reference-zh-CN.md index 1e161589..a07962fa 100644 --- a/wiki/Tags-Reference-zh-CN.md +++ b/wiki/Tags-Reference-zh-CN.md @@ -76,7 +76,7 @@ | `` | 块级 BetterQuesting 任务摘要卡片(兼容标签,仅当 BetterQuesting 已加载时注册) | `id`, `show_desc`, `show_tooltip` | `` 是 `` 的子标签。它支持 `controller`、`piece`、`facing`、 -`rotation`、`flip`、`channel` 属性,也支持用于设置 StructureLib 默认值的子标签: +`rotation`、`flip`、`tier`、`channel`、`channelName`、`value`、`locked` 属性,也支持用于设置 StructureLib 默认值的子标签: ``、``、``、``、``、``、 ``、``。 @@ -622,7 +622,7 @@ $$\begin{pmatrix} a & b \\ c & d \end{pmatrix}$$ | 标签 | 用途 | 关键属性 | | --- | --- | --- | | `` | 导入外部 SNBT/NBT 结构资源 | `src`, `x`, `y`, `z`, `offsetX`, `offsetY`, `offsetZ`, `formed` | -| `` | 通过控制器 id 导入 StructureLib 多方块 | `controller`, `name`, `piece`, `facing`, `rotation`, `flip`, `channel`, `offsetX`, `offsetY`, `offsetZ`, `formed` | +| `` | 通过控制器 id 导入 StructureLib 多方块 | `controller`, `name`, `piece`, `facing`, `rotation`, `flip`, `tier`, `channel`, `channelName`, `value`, `locked`, `offsetX`, `offsetY`, `offsetZ`, `formed` | | `` | 移除已放置且匹配指定方块匹配器的方块 | `id` | | `` | 把同一组子注解扩展到场景中所有匹配方块上 | `id` | diff --git a/wiki/Tags-Reference.md b/wiki/Tags-Reference.md index e3abdf30..5b52ee60 100644 --- a/wiki/Tags-Reference.md +++ b/wiki/Tags-Reference.md @@ -74,7 +74,7 @@ Inline markdown also supports action links for sound playback: | `` | block-level BetterQuesting quest summary card (compat tag, only registered when BetterQuesting is loaded) | `id`, `show_desc`, `show_tooltip` | `` is a `` child tag. It accepts `controller`, `piece`, `facing`, -`rotation`, `flip`, and `channel` attributes, and also supports StructureLib default child tags: +`rotation`, `flip`, `tier`, `channel`, `channelName`, `value`, and `locked` attributes, and also supports StructureLib default child tags: ``, ``, ``, ``, ``, ``, ``, and ``. @@ -676,7 +676,7 @@ These tags only work inside `` / ``: | Tag | Purpose | Key attributes | | --- | --- | --- | | `` | import an external SNBT/NBT structure asset | `src`, `x`, `y`, `z`, `offsetX`, `offsetY`, `offsetZ`, `formed` | -| `` | import a StructureLib multiblock by controller id | `controller`, `name`, `piece`, `facing`, `rotation`, `flip`, `channel`, `offsetX`, `offsetY`, `offsetZ`, `formed` | +| `` | import a StructureLib multiblock by controller id | `controller`, `name`, `piece`, `facing`, `rotation`, `flip`, `tier`, `channel`, `channelName`, `value`, `locked`, `offsetX`, `offsetY`, `offsetZ`, `formed` | | `` | remove already-placed blocks that match a block matcher | `id` | | `` | stamp the same child annotations onto every matching placed block | `id` | diff --git a/wiki/resourcepack/assets/guidenh/guidenh/_en_us/scene-import.md b/wiki/resourcepack/assets/guidenh/guidenh/_en_us/scene-import.md index ea890bcc..24b84820 100644 --- a/wiki/resourcepack/assets/guidenh/guidenh/_en_us/scene-import.md +++ b/wiki/resourcepack/assets/guidenh/guidenh/_en_us/scene-import.md @@ -27,8 +27,8 @@ correctly: - - + + @@ -51,6 +51,10 @@ Default unformed controller preview: ``` +Use `locked={true}` on a `` or `` child to lock that default value. The same +attribute can be placed on `` with `tier`, or with `channelName` and +`channel`/`value`, to lock a root override. The public syntax uses `locked` consistently. + Explicit formed controller preview: ```mdx diff --git a/wiki/resourcepack/assets/guidenh/guidenh/_zh_cn/scene-import.md b/wiki/resourcepack/assets/guidenh/guidenh/_zh_cn/scene-import.md index c310082f..e4f11d25 100644 --- a/wiki/resourcepack/assets/guidenh/guidenh/_zh_cn/scene-import.md +++ b/wiki/resourcepack/assets/guidenh/guidenh/_zh_cn/scene-import.md @@ -23,8 +23,8 @@ StructureLib 默认值可以写成子标签,点击 reset view 时会恢复这 - - + + @@ -48,6 +48,10 @@ StructureLib 默认值可以写成子标签,点击 reset view 时会恢复这 ``` +在 `` 或 `` 子标签上使用 `locked={true}` 可以锁定这个默认值。根标签也可以 +使用同一个属性:配合 `tier`,或配合 `channelName` 与 `channel`/`value`,即可锁定根覆盖值。 +对外语法统一使用 `locked`。 + 显式展示成型状态的示例: ```mdx