diff --git a/src/generated/resources/.cache/f2b50d6f71e71bf61f265d7522c84b7b1ed8feaa b/src/generated/resources/.cache/f2b50d6f71e71bf61f265d7522c84b7b1ed8feaa index 9996814d..2378c3a2 100644 --- a/src/generated/resources/.cache/f2b50d6f71e71bf61f265d7522c84b7b1ed8feaa +++ b/src/generated/resources/.cache/f2b50d6f71e71bf61f265d7522c84b7b1ed8feaa @@ -1,4 +1,5 @@ -// 26.1.2 2026-07-15T21:53:24.6999995 Model Definitions - powertool +// 26.1.2 2026-08-03T14:45:54.5401026 Model Definitions - powertool +a3b63de8a87442d752fca4d7e0b19538467a2e42 assets/powertool/items/adjustable_spyglass.json 8ea8f7958a6d3eb49c2e3750c50b8c4cf1b2ea78 assets/powertool/items/auto_vanish_acacia_boat.json 05e9e4bd8b74fd0ae65ff5047d1bf36a36e5d5b3 assets/powertool/items/auto_vanish_bamboo_raft.json 49f5b6218b25736e0ea25adbe2ca71534b347c25 assets/powertool/items/auto_vanish_birch_boat.json @@ -9,6 +10,7 @@ b4d893ae13496f3bbfe1bb789fe124485eef95df assets/powertool/items/auto_vanish_mang 8acb3dcb82337a67704631a307531f93037d7b5d assets/powertool/items/auto_vanish_minecart.json 9472ac537b3f38519b308e5fc7b521a586920d6a assets/powertool/items/auto_vanish_oak_boat.json 4bdbc7743d9e08df96f3114f80e3dfc5807415d6 assets/powertool/items/auto_vanish_spruce_boat.json +93f05d0d848ec7d7731686386c15823adcec6a3b assets/powertool/items/bbc_holographic_sign.json f373c5954ab4a9d638ad70c37dbb4ff545eda294 assets/powertool/items/bezier_curve_block.json a0ece115128e69014712a99caba3e7782fa9c0da assets/powertool/items/black_distant_horizon_cheating_block.json 4963a077391c03d50b6497d5db92a3e08858ef85 assets/powertool/items/blue_distant_horizon_cheating_block.json diff --git a/src/generated/resources/assets/powertool/items/bbc_holographic_sign.json b/src/generated/resources/assets/powertool/items/bbc_holographic_sign.json new file mode 100644 index 00000000..c9f4e361 --- /dev/null +++ b/src/generated/resources/assets/powertool/items/bbc_holographic_sign.json @@ -0,0 +1,7 @@ +{ + "hand_animation_on_swap": false, + "model": { + "type": "minecraft:model", + "model": "powertool:item/bbc_holographic_sign" + } +} \ No newline at end of file diff --git a/src/main/java/org/teacon/powertool/block/PowerToolBlocks.java b/src/main/java/org/teacon/powertool/block/PowerToolBlocks.java index 30dbe13d..b37ab5a8 100644 --- a/src/main/java/org/teacon/powertool/block/PowerToolBlocks.java +++ b/src/main/java/org/teacon/powertool/block/PowerToolBlocks.java @@ -50,6 +50,7 @@ import org.teacon.powertool.block.cosmetical.CosmeticSimpleBlock; import org.teacon.powertool.block.cosmetical.CosmeticTrapdoor; import org.teacon.powertool.block.cosmetical.CosmeticWaterloggedBlock; +import org.teacon.powertool.block.entity.BBCodeHolographicSignBlockEntity; import org.teacon.powertool.block.entity.BezierCurveBlockEntity; import org.teacon.powertool.block.entity.CommonHolographicSignBlockEntity; import org.teacon.powertool.block.entity.ItemDisplayBlockEntity; @@ -143,6 +144,7 @@ public class PowerToolBlocks { public static DeferredHolder HOLOGRAPHIC_SIGN; public static DeferredHolder LINK_HOLOGRAPHIC_SIGN; public static DeferredHolder RAW_JSON_HOLOGRAPHIC_SIGN; + public static DeferredHolder BBC_HOLOGRAPHIC_SIGN; public static DeferredHolder WHITE_TRASH_CAN; public static DeferredHolder WHITE_TRASH_CAN_CAP; public static DeferredHolder GRAY_TRASH_CAN; @@ -175,6 +177,7 @@ public class PowerToolBlocks { public static DeferredHolder, BlockEntityType> HOLOGRAPHIC_SIGN_BLOCK_ENTITY; public static DeferredHolder, BlockEntityType> LINK_HOLOGRAPHIC_SIGN_BLOCK_ENTITY; public static DeferredHolder, BlockEntityType> RAW_JSON_HOLOGRAPHIC_SIGN_BLOCK_ENTITY; + public static DeferredHolder, BlockEntityType> BBC_HOLOGRAPHIC_SIGN_BLOCK_ENTITY; public static DeferredHolder, BlockEntityType> TRASH_CAN_WITH_CONTAINER_BLOCK_ENTITY; public static DeferredHolder, BlockEntityType> REGISTER_BLOCK_ENTITY; public static DeferredHolder, BlockEntityType> SAFE_BLOCK_ENTITY; @@ -224,6 +227,7 @@ public static void register(IEventBus bus) { HOLOGRAPHIC_SIGN = BLOCKS.registerBlock("holographic_sign", (p) -> new HolographicSignBlock(p.strength(1.0F).noCollision().noLootTable(), SignType.COMMON)); LINK_HOLOGRAPHIC_SIGN = BLOCKS.registerBlock("link_holographic_sign", (p) -> new HolographicSignBlock(p.strength(1.0F).noCollision().noLootTable(), SignType.URL)); RAW_JSON_HOLOGRAPHIC_SIGN = BLOCKS.registerBlock("raw_json_holographic_sign", (p) -> new HolographicSignBlock(p.strength(1.0F).noCollision().noLootTable(), SignType.RAW_JSON)); + BBC_HOLOGRAPHIC_SIGN = BLOCKS.registerBlock("bbc_holographic_sign", (p) -> new HolographicSignBlock(p.strength(1.0F).noCollision().noLootTable(), SignType.BBC)); WHITE_TRASH_CAN = BLOCKS.registerBlock("white_trash_can", (p) -> new TrashCanWithContainer(p.strength(1.0F).noOcclusion())); WHITE_TRASH_CAN_CAP = BLOCKS.registerBlock("white_trash_can_cap", (p) -> new TrapDoorBlock(BlockSetType.COPPER, p.strength(1.0F).noOcclusion())); GRAY_TRASH_CAN = BLOCKS.registerBlock("gray_trash_can", (p) -> new TrashCanWithContainer(p.strength(1.0F).noOcclusion())); @@ -277,6 +281,9 @@ public static void register(IEventBus bus) { RAW_JSON_HOLOGRAPHIC_SIGN_BLOCK_ENTITY = BLOCK_ENTITIES.register("raw_json_holographic_sign", () -> new BlockEntityType<>( RawJsonHolographicSignBlockEntity::new, RAW_JSON_HOLOGRAPHIC_SIGN.get() )); + BBC_HOLOGRAPHIC_SIGN_BLOCK_ENTITY = BLOCK_ENTITIES.register("bbc_holographic_sign", () -> new BlockEntityType<>( + BBCodeHolographicSignBlockEntity::new, BBC_HOLOGRAPHIC_SIGN.get() + )); TRASH_CAN_WITH_CONTAINER_BLOCK_ENTITY = BLOCK_ENTITIES.register("trash_can_with_container", () -> new BlockEntityType<>( TrashCanWithContainerBlockEntity::new, TRASH_CAN.get(), WHITE_TRASH_CAN.get(), GRAY_TRASH_CAN.get(), GREEN_TRASH_CAN.get() )); @@ -368,6 +375,7 @@ public static void register(IEventBus bus) { ITEMS.registerItem("holographic_sign", (p) -> new PowerToolBlockItem(HOLOGRAPHIC_SIGN.get(), p)); ITEMS.registerItem("link_holographic_sign", (p) -> new PowerToolBlockItem(LINK_HOLOGRAPHIC_SIGN.get(), p)); ITEMS.registerItem("raw_json_holographic_sign", (p) -> new PowerToolBlockItem(RAW_JSON_HOLOGRAPHIC_SIGN.get(), p)); + ITEMS.registerItem("bbc_holographic_sign", (p) -> new PowerToolBlockItem(BBC_HOLOGRAPHIC_SIGN.get(), p)); ITEMS.registerItem("trash_can", (p) -> new PowerToolBlockItem(TRASH_CAN.get(), p)); ITEMS.registerItem("white_trash_can", (p) -> new PowerToolBlockItem(WHITE_TRASH_CAN.get(), p)); ITEMS.registerItem("white_trash_can_cap", (p) -> new PowerToolBlockItem(WHITE_TRASH_CAN_CAP.get(), p)); diff --git a/src/main/java/org/teacon/powertool/block/entity/BBCodeHolographicSignBlockEntity.java b/src/main/java/org/teacon/powertool/block/entity/BBCodeHolographicSignBlockEntity.java new file mode 100644 index 00000000..556f4d96 --- /dev/null +++ b/src/main/java/org/teacon/powertool/block/entity/BBCodeHolographicSignBlockEntity.java @@ -0,0 +1,826 @@ +package org.teacon.powertool.block.entity; + +import eu.pb4.placeholders.api.parsers.NodeParser; +import eu.pb4.placeholders.api.parsers.TagParser; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.StringRepresentable; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; +import org.teacon.powertool.block.PowerToolBlocks; + +import java.util.*; +import java.util.concurrent.CompletableFuture; + + +public class BBCodeHolographicSignBlockEntity extends BaseHolographicSignBlockEntity { + + public static final NodeParser TPAPI_PARSER = TagParser.DEFAULT; + + /** + * Represents text alignment for BBCodes and global default. + */ + public enum TextAlign implements StringRepresentable { + LEFT("left"), + CENTER("center"), + RIGHT("right"); + + public static final TextAlign DEFAULT = CENTER; + + private final String serializedName; + + TextAlign(String serializedName) { + this.serializedName = serializedName; + } + + @Override + public @NonNull String getSerializedName() { + return serializedName; + } + + public static Align toAlignEnum(TextAlign align) { + return switch (align) { + case LEFT -> Align.LEFT; + case CENTER -> Align.CENTER; + case RIGHT -> Align.RIGHT; + }; + } + } + + + private enum ParserState { + TEXT, + TAG_START, + TAG_NAME, + TAG_PARAM, + CLOSE_TAG, + CLOSE_NAME + } + + /** + * 样式状态 - 用于保存和恢复嵌套标签的样式 + */ + private static class StyleState { + Integer color; + Integer backgroundColor; + Float size; + TextAlign align; + Float lineHeight; + Boolean bold; + Boolean underline; + Boolean italic; + + StyleState(Integer color, Integer backgroundColor, Float size, TextAlign align, Float lineHeight, + Boolean bold, Boolean underline, Boolean italic) { + this.color = color; + this.backgroundColor = backgroundColor; + this.size = size; + this.align = align; + this.lineHeight = lineHeight; + this.bold = bold; + this.underline = underline; + this.italic = italic; + } + } + + /** + * 解析上下文 - 状态机的运行时上下文 + * 包含当前状态、缓冲区、样式状态等信息 + */ + private static class ParseContext { + ParserState state = ParserState.TEXT; + StringBuilder currentText = new StringBuilder(); + StringBuilder tagBuffer = new StringBuilder(); // 用于暂存标签名称或参数 + + // 当前正在解析的标签信息 + String pendingTagName = null; + String pendingTagParam = null; + + // 当前样式状态(使用数组以便在闭包中修改) + Integer currentColor = null; + Integer currentBackgroundColor = null; + Float currentSize = null; + TextAlign currentAlign = null; + Float currentLineHeight = null; + Boolean currentBold = null; + Boolean currentUnderline = null; + Boolean currentItalic = null; + + // 样式栈 - 用于处理嵌套标签 + Deque styleStack = new ArrayDeque<>(); + + // 当前正在构建的行 + ParsedLine currentLine; + + // 解析结果 + List parsedLines = new ArrayList<>(); + + // 全局默认值 + final TextAlign defaultAlign; + final Float defaultLineHeight; + + ParseContext(TextAlign defaultAlign, Float defaultLineHeight) { + this.defaultAlign = defaultAlign; + this.defaultLineHeight = defaultLineHeight; + this.currentLine = new ParsedLine(defaultAlign, defaultLineHeight); + } + + /** + * 将当前文本缓冲区的内容添加到当前行(使用当前样式) + */ + void flushText() { + if (!currentText.isEmpty()) { + currentLine.addSegment(new TextSegment( + currentText.toString(), + currentColor, currentBackgroundColor, currentSize, currentAlign, + currentLineHeight, currentBold, currentUnderline, currentItalic)); + currentText = new StringBuilder(); + } + } + + /** + * 换行:结束当前行,开始新行 + */ + void newLine() { + flushText(); + // 完成当前行(不添加换行符标记,因为 ParsedLine 已经完成行分割) + currentLine.mergeSegments(); + parsedLines.add(currentLine); + // 开始新行,保持当前样式状态 + currentLine = new ParsedLine( + currentAlign != null ? currentAlign : defaultAlign, + currentLineHeight != null ? currentLineHeight : defaultLineHeight + ); + } + + /** + * 应用一个打开标签,更新当前样式状态 + * 将当前状态压入栈中,以便后续关闭标签时恢复 + */ + boolean applyOpenTag(String tagName, String tagParam) { + flushText(); // 先处理之前的文本 + + // 保存当前状态到栈(在修改之前) + styleStack.push(new StyleState(currentColor, currentBackgroundColor, currentSize, currentAlign, + currentLineHeight, currentBold, currentUnderline, currentItalic)); + + switch (tagName.toLowerCase()) { + case "color", "c" -> { + currentColor = parseColor(tagParam); + return true; + } + case "bg", "background" -> { + currentBackgroundColor = parseColor(tagParam); + return true; + } + case "size" -> { + try { + currentSize = Float.parseFloat(tagParam); + return true; + } catch (NumberFormatException e) { + currentSize = null; + } + } + case "align", "a" -> { + try { + currentAlign = TextAlign.valueOf(tagParam.toUpperCase()); + return true; + } catch (IllegalArgumentException e) { + currentAlign = null; + } + } + case "lineheight", "lh" -> { + try { + currentLineHeight = Float.parseFloat(tagParam); + return true; + } catch (NumberFormatException e) { + currentLineHeight = null; + } + } + case "bold", "b" -> { + currentBold = true; + return true; + } + case "underline", "u" -> { + currentUnderline = true; + return true; + } + case "italic", "i" -> { + currentItalic = true; + return true; + } + } + return false; + } + + /** + * 应用一个关闭标签,从栈中恢复之前的样式状态 + */ + void applyCloseTag(String tagName) { + flushText(); // 先处理之前的文本 + + // 从栈中恢复之前的样式状态 + if (!styleStack.isEmpty()) { + StyleState prevState = styleStack.pop(); + + switch (tagName.toLowerCase()) { + case "color", "c" -> currentColor = prevState.color; + case "bg", "background" -> currentBackgroundColor = prevState.backgroundColor; + case "size" -> currentSize = prevState.size; + case "align", "a" -> currentAlign = prevState.align; + case "lineheight", "lh" -> currentLineHeight = prevState.lineHeight; + case "bold", "b" -> currentBold = prevState.bold; + case "underline", "u" -> currentUnderline = prevState.underline; + case "italic", "i" -> currentItalic = prevState.italic; + } + } else { + // 如果栈为空(异常情况),重置为 null + switch (tagName.toLowerCase()) { + case "color", "c" -> currentColor = null; + case "bg", "background" -> currentBackgroundColor = null; + case "size" -> currentSize = null; + case "align", "a" -> currentAlign = null; + case "lineheight", "lh" -> currentLineHeight = null; + case "bold", "b" -> currentBold = null; + case "underline", "u" -> currentUnderline = null; + case "italic", "i" -> currentItalic = null; + } + } + } + } + + /** + * Represents a styled text segment with all formatting properties. + * Characters with the same style should be merged into one segment. + * + */ + public static final class TextSegment { + private final String text; + private final @Nullable Integer color; + private final @Nullable Integer backgroundColor; + private final @Nullable Float size; + private final @Nullable TextAlign align; + private final @Nullable Float lineHeight; + private final @Nullable Boolean bold; + private final @Nullable Boolean underline; + private final @Nullable Boolean italic; + + /** + * @param color null means use default + * @param backgroundColor null means use default (transparent) + * @param size null means use default + * @param align null means use default (affects entire line) + * @param lineHeight null means use default + * @param bold null means use default + * @param underline null means use default + * @param italic null means use default + */ + public TextSegment(String text, @Nullable Integer color, @Nullable Integer backgroundColor, + @Nullable Float size, @Nullable TextAlign align, + @Nullable Float lineHeight, @Nullable Boolean bold, @Nullable Boolean underline, + @Nullable Boolean italic) { + this.text = text; + this.color = color; + this.backgroundColor = backgroundColor; + this.size = size; + this.align = align; + this.lineHeight = lineHeight; + this.bold = bold; + this.underline = underline; + this.italic = italic; + } + + /** + * Check if this segment has the same style as another (ignoring text content). + */ + public boolean sameStyle(@Nullable TextSegment other) { + if (other == null) return false; + return Objects.equals(this.color, other.color) && + Objects.equals(this.backgroundColor, other.backgroundColor) && + Objects.equals(this.size, other.size) && + Objects.equals(this.align, other.align) && + Objects.equals(this.lineHeight, other.lineHeight) && + Objects.equals(this.bold, other.bold) && + Objects.equals(this.underline, other.underline) && + Objects.equals(this.italic, other.italic); + } + + /** + * Create a new segment with merged text from this and another segment with same style. + */ + public TextSegment merge(TextSegment other) { + if (!sameStyle(other)) return this; + return new TextSegment(this.text + other.text, color, backgroundColor, size, align, + lineHeight, bold, underline, italic); + } + + public String text() { + return text; + } + + public @Nullable Integer color() { + return color; + } + + public @Nullable Integer backgroundColor() { + return backgroundColor; + } + + public @Nullable Float size() { + return size; + } + + public @Nullable TextAlign align() { + return align; + } + + public @Nullable Float lineHeight() { + return lineHeight; + } + + public @Nullable Boolean bold() { + return bold; + } + + public @Nullable Boolean underline() { + return underline; + } + + public @Nullable Boolean italic() { + return italic; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (TextSegment) obj; + return Objects.equals(this.text, that.text) && + Objects.equals(this.color, that.color) && + Objects.equals(this.backgroundColor, that.backgroundColor) && + Objects.equals(this.size, that.size) && + Objects.equals(this.align, that.align) && + Objects.equals(this.lineHeight, that.lineHeight) && + Objects.equals(this.bold, that.bold) && + Objects.equals(this.underline, that.underline) && + Objects.equals(this.italic, that.italic); + } + + @Override + public int hashCode() { + return Objects.hash(text, color, backgroundColor, size, align, lineHeight, bold, underline, italic); + } + + @Override + public String toString() { + return "TextSegment[" + + "text=" + text + ", " + + "color=" + color + ", " + + "backgroundColor=" + backgroundColor + ", " + + "size=" + size + ", " + + "align=" + align + ", " + + "lineHeight=" + lineHeight + ", " + + "bold=" + bold + ", " + + "underline=" + underline + ", " + + "italic=" + italic + ']'; + } + + } + + /** + * Represents a parsed line with multiple text segments. + */ + public static class ParsedLine { + public final List segments = new ArrayList<>(); + public TextAlign lineAlign; // alignment for the entire line + public Float lineLineHeight; // line-height for this entire line + + public ParsedLine(TextAlign defaultAlign, Float defaultLineHeight) { + this.lineAlign = defaultAlign; + this.lineLineHeight = defaultLineHeight; + } + + public void addSegment(TextSegment segment) { + segments.add(segment); + // If segment specifies alignment, update line alignment + if (segment.align != null) { + lineAlign = segment.align; + } + // If segment specifies line-height, update line line-height + if (segment.lineHeight != null) { + lineLineHeight = segment.lineHeight; + } + } + + /** + * Merge consecutive segments with the same style. + */ + public void mergeSegments() { + List merged = new ArrayList<>(); + for (var segment : segments) { + if (!merged.isEmpty() && merged.getLast().sameStyle(segment)) { + merged.set(merged.size() - 1, merged.getLast().merge(segment)); + } else { + merged.add(segment); + } + } + segments.clear(); + segments.addAll(merged); + } + } + + // Raw BBCodes content (one entry per line for editing) + public List rawContent = new ArrayList<>(); + + // Client-side render cache (built by renderer) + public Object clientCacheData = null; + + // Parsed for rendering (lines separated by \n in the original text) + public List parsedLines = new ArrayList<>(); + + // Filtered content after text filtering + public List forRender = new ArrayList<>(); + + // Global default parameters + public int defaultColor = 0xFFFFFFFF; // ARGB format + public float defaultScale = 1.0F; + public TextAlign defaultAlign = TextAlign.DEFAULT; + public float defaultLineHeight = 1.0F; // Line height multiplier + public float minWidth = 0F; // Minimum width in blocks + public boolean defaultBold = false; + public boolean defaultUnderline = false; + public boolean defaultItalic = false; + + public BBCodeHolographicSignBlockEntity(BlockPos pPos, BlockState pBlockState) { + super(PowerToolBlocks.BBC_HOLOGRAPHIC_SIGN_BLOCK_ENTITY.get(), pPos, pBlockState); + } + + @Override + public void writeTo(@NonNull ValueOutput output) { + super.writeTo(output); + + // 只写入原始内容和全局参数,parsedLines 可以在读取后重新解析 + + // 写入原始内容(每行一个字符串) + output.putInt("contentSize", rawContent.size()); + for (int i = 0; i < rawContent.size(); i++) { + output.putString("content_" + i, rawContent.get(i)); + } + + // 写入全局默认参数 + output.putInt("defaultColor", defaultColor); + output.putFloat("defaultScale", defaultScale); + output.putInt("defaultAlign", defaultAlign.ordinal()); + output.putFloat("defaultLineHeight", defaultLineHeight); + output.putFloat("minWidth", minWidth); + output.putBoolean("defaultBold", defaultBold); + output.putBoolean("defaultUnderline", defaultUnderline); + output.putBoolean("defaultItalic", defaultItalic); + } + + @Override + public void readFrom(@NonNull ValueInput input) { + super.readFrom(input); + + // 读取原始内容 + var contentSize = input.getIntOr("contentSize", 0); + rawContent.clear(); + for (int i = 0; i < contentSize; i++) { + rawContent.add(input.getStringOr("content_" + i, "")); + } + + // 读取全局默认参数 + defaultColor = input.getIntOr("defaultColor", 0xFFFFFFFF); + defaultScale = input.getFloatOr("defaultScale", 1.0F); + int alignOrdinal = input.getIntOr("defaultAlign", TextAlign.DEFAULT.ordinal()); + defaultAlign = TextAlign.values()[alignOrdinal % TextAlign.values().length]; + defaultLineHeight = input.getFloatOr("defaultLineHeight", 1.0F); + minWidth = input.getFloatOr("minWidth", 0F); + defaultBold = input.getBooleanOr("defaultBold", false); + defaultUnderline = input.getBooleanOr("defaultUnderline", false); + defaultItalic = input.getBooleanOr("defaultItalic", false); + + // 更新基类属性 + this.colorInARGB = defaultColor; + this.scale = defaultScale; + this.align = TextAlign.toAlignEnum(defaultAlign); + + // 在客户端重新解析 BBCodes + if (getLevel() != null && getLevel().isClientSide()) { + // 重新构建 forRender 并解析 + forRender.clear(); + for (var rawLine : rawContent) { + forRender.add(Component.literal(rawLine)); + } + parseBBCode(forRender); + } + } + + @Override + public void filterMessage(@NonNull ServerPlayer player) { + forRender.clear(); + var taskList = new ArrayList>(); + + for (var rawLine : rawContent) { + var task = player.getTextFilter().processStreamMessage(rawLine); + task.thenAccept(filtered -> { + if (player.isTextFilteringEnabled()) { + forRender.add(Component.literal(filtered.filteredOrEmpty())); + } else { + forRender.add(Component.literal(filtered.raw())); + } + }); + taskList.add(task); + } + + var finalTask = CompletableFuture.allOf(taskList.toArray(new CompletableFuture[0])); + finalTask.thenAcceptAsync((_) -> { + parseBBCode(forRender); + this.setChanged(); + if (level != null) { + var state = this.getBlockState(); + level.sendBlockUpdated(this.getBlockPos(), state, state, Block.UPDATE_CLIENTS); + } + }, player.server); + } + + /** + * Parse BBCodes from the content list. + * All content is joined with \n and parsed as a single continuous text using a state machine. + */ + public void parseBBCode(List content) { + parsedLines.clear(); + + // Join all content with \n to create a single text to parse + StringBuilder fullText = new StringBuilder(); + for (int i = 0; i < content.size(); i++) { + if (i > 0) { + fullText.append("\n"); + } + fullText.append(content.get(i).getString()); + } + + // Parse using state machine + ParseContext ctx = new ParseContext(defaultAlign, defaultLineHeight); + parseWithStateMachine(fullText.toString(), ctx); + parsedLines = ctx.parsedLines; + + // Clear client cache so renderer will rebuild it + clientCacheData = null; + } + + /** + * 使用状态机解析 BBCode 文本 + *

+ * 状态机逻辑: + *

+ * TEXT 状态(默认): + * - '[' -> TAG_START(可能是标签开始) + * - '\n' -> newLine()(换行) + * - 其他 -> 累积文本 + *

+ * TAG_START 状态: + * - '/' -> CLOSE_TAG(关闭标签) + * - 字母 -> TAG_NAME(打开标签) + * - 其他 -> 回到 TEXT('[' 当作普通字符) + *

+ * TAG_NAME 状态: + * - '=' -> TAG_PARAM(有参数) + * - ']' -> 应用标签,回到 TEXT + * - 字母/数字 -> 继续读取标签名 + * - 其他 -> 回到 TEXT(无效标签) + *

+ * TAG_PARAM 状态: + * - ']' -> 应用标签(带参数),回到 TEXT + * - 其他 -> 继续读取参数值 + *

+ * CLOSE_TAG 状态: + * - 字母 -> CLOSE_NAME(读取关闭标签名) + * - 其他 -> 回到 TEXT(无效的 '[/') + *

+ * CLOSE_NAME 状态: + * - ']' -> 关闭标签,回到 TEXT + * - 字母/数字 -> 继续读取标签名 + * - 其他 -> 回到 TEXT(无效关闭标签) + */ + private void parseWithStateMachine(String text, ParseContext ctx) { + if (text.isEmpty()) { + return; + } + + ctx.state = ParserState.TEXT; + + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + + switch (ctx.state) { + case TEXT -> { + // 默认状态:读取普通文本 + if (c == '[') { + ctx.state = ParserState.TAG_START; + ctx.tagBuffer.setLength(0); + } else if (c == '\n') { + ctx.newLine(); + } else { + ctx.currentText.append(c); + } + } + case TAG_START -> { + // 刚遇到 '[',判断是什么标签 + if (c == '/') { + ctx.state = ParserState.CLOSE_TAG; + ctx.tagBuffer.setLength(0); + } else if (Character.isLetter(c)) { + ctx.state = ParserState.TAG_NAME; + ctx.tagBuffer.setLength(0); + ctx.tagBuffer.append(c); + } else { + // 不是标签,'[' 当作普通字符 + ctx.currentText.append('['); + ctx.currentText.append(c); + ctx.state = ParserState.TEXT; + } + } + case TAG_NAME -> { + // 正在读取标签名称 + if (c == '=') { + // 标签有参数 + ctx.pendingTagName = ctx.tagBuffer.toString(); + ctx.tagBuffer.setLength(0); + ctx.state = ParserState.TAG_PARAM; + } else if (c == ']') { + // 无参数标签完成 + ctx.pendingTagName = ctx.tagBuffer.toString(); + boolean result = ctx.applyOpenTag(ctx.pendingTagName, null); + if (!result) { + ctx.currentText.append('['); + ctx.currentText.append(ctx.pendingTagName); + ctx.currentText.append(']'); + } + ctx.state = ParserState.TEXT; + } else if (Character.isLetterOrDigit(c)) { + ctx.tagBuffer.append(c); + } else { + // 无效标签,丢弃 + ctx.currentText.append('['); + ctx.currentText.append(ctx.tagBuffer); + ctx.currentText.append(c); + ctx.tagBuffer.setLength(0); + ctx.state = ParserState.TEXT; + } + } + case TAG_PARAM -> { + // 正在读取标签参数 + if (c == ']') { + // 有参数标签完成 + ctx.pendingTagParam = ctx.tagBuffer.toString(); + boolean result = ctx.applyOpenTag(ctx.pendingTagName, ctx.pendingTagParam); + if (!result) { + ctx.currentText.append('['); + ctx.currentText.append(ctx.pendingTagName); + ctx.currentText.append(']'); + } + ctx.state = ParserState.TEXT; + } else { + ctx.tagBuffer.append(c); + } + } + case CLOSE_TAG -> { + // 遇到 '[/',等待标签名称 + if (Character.isLetter(c)) { + ctx.state = ParserState.CLOSE_NAME; + ctx.tagBuffer.setLength(0); + ctx.tagBuffer.append(c); + } else { + // 无效的 '[/',当作普通字符 + ctx.currentText.append('['); + ctx.currentText.append('/'); + ctx.currentText.append(c); + ctx.state = ParserState.TEXT; + } + } + case CLOSE_NAME -> { + // 正在读取关闭标签名称 + if (c == ']') { + // 关闭标签完成 + String closeTagName = ctx.tagBuffer.toString(); + // 检查是否是有效的关闭标签 + if (isValidTagName(closeTagName)) { + ctx.applyCloseTag(closeTagName); + } else { + // 无效标签,当作普通文本 + ctx.currentText.append("[/"); + ctx.currentText.append(closeTagName); + ctx.currentText.append("]"); + } + ctx.state = ParserState.TEXT; + } else if (Character.isLetterOrDigit(c)) { + ctx.tagBuffer.append(c); + } else { + // 无效关闭标签 - 将已读取的内容当作普通文本 + ctx.currentText.append("[/"); + ctx.currentText.append(ctx.tagBuffer.toString()); + ctx.currentText.append(c); + ctx.tagBuffer.setLength(0); + ctx.state = ParserState.TEXT; + } + } + } + } + + // 处理结束时的状态 + if (ctx.state == ParserState.TEXT) { + ctx.flushText(); + } + + // 添加最后一行(如果有内容) + if (!ctx.currentLine.segments.isEmpty()) { + ctx.currentLine.mergeSegments(); + ctx.parsedLines.add(ctx.currentLine); + } + } + + /** + * Check if a tag name is valid (supported by the parser). + */ + private static boolean isValidTagName(String tagName) { + return switch (tagName.toLowerCase()) { + case "color", "c", "bg", "background", + "size", "s", "align", "a", "lineheight", "lh", + "bold", "b", "underline", "u", "italic", "i" -> true; + default -> false; + }; + } + + /** + * Parse color from various formats: + * - Hex ARGB: #AARRGGBB or #ARGB + * - Hex RGB: #RRGGBB or #RGB + * - Named colors: red, green, blue, etc. + */ + private static Integer parseColor(String colorStr) { + colorStr = colorStr.trim().toLowerCase(); + + // Handle hex color + if (colorStr.startsWith("#")) { + try { + String hex = colorStr.substring(1); + + // 8位格式: #AARRGGBB + if (hex.length() == 8) { + return (Integer.parseInt(hex, 16)); + } + // 4位格式: #ARGB + else if (hex.length() == 4) { + // Expand #ARGB to #AARRGGBB + int a = Integer.parseInt(hex.substring(0, 1), 16) * 17; + int r = Integer.parseInt(hex.substring(1, 2), 16) * 17; + int g = Integer.parseInt(hex.substring(2, 3), 16) * 17; + int b = Integer.parseInt(hex.substring(3, 4), 16) * 17; + return (a << 24) | (r << 16) | (g << 8) | b; + } + // 6位格式: #RRGGBB + else if (hex.length() == 6) { + int rgb = Integer.parseInt(hex, 16); + return 0xFF000000 | rgb; // Add full alpha + } + // 3位格式: #RGB + else if (hex.length() == 3) { + // Expand #RGB to #RRGGBB + int r = Integer.parseInt(hex.substring(0, 1), 16) * 17; + int g = Integer.parseInt(hex.substring(1, 2), 16) * 17; + int b = Integer.parseInt(hex.substring(2, 3), 16) * 17; + return 0xFF000000 | (r << 16) | (g << 8) | b; + } + } catch (NumberFormatException e) { + return null; + } + } + + // Handle named colors + return switch (colorStr) { + case "black" -> 0xFF000000; + case "dark_blue" -> 0xFF0000AA; + case "dark_green" -> 0xFF00AA00; + case "dark_aqua" -> 0xFF00AAAA; + case "dark_red" -> 0xFFAA0000; + case "dark_purple" -> 0xFFAA00AA; + case "gold" -> 0xFFFFAA00; + case "gray" -> 0xFFAAAAAA; + case "dark_gray" -> 0xFF555555; + case "blue" -> 0xFF5555FF; + case "green" -> 0xFF55FF55; + case "aqua" -> 0xFF55FFFF; + case "red" -> 0xFFFF5555; + case "light_purple" -> 0xFFFF55FF; + case "yellow" -> 0xFFFFFF55; + case "white" -> 0xFFFFFFFF; + default -> null; + }; + } +} diff --git a/src/main/java/org/teacon/powertool/block/entity/CommonHolographicSignBlockEntity.java b/src/main/java/org/teacon/powertool/block/entity/CommonHolographicSignBlockEntity.java index 0c216089..b34ff0c9 100644 --- a/src/main/java/org/teacon/powertool/block/entity/CommonHolographicSignBlockEntity.java +++ b/src/main/java/org/teacon/powertool/block/entity/CommonHolographicSignBlockEntity.java @@ -2,6 +2,7 @@ import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import eu.pb4.placeholders.api.ParserContext; +import eu.pb4.placeholders.api.PlaceholderContext; import eu.pb4.placeholders.api.parsers.NodeParser; import eu.pb4.placeholders.api.parsers.TagParser; import net.minecraft.core.BlockPos; @@ -22,16 +23,16 @@ @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault public class CommonHolographicSignBlockEntity extends BaseHolographicSignBlockEntity { - + public List contents = Collections.emptyList(); public List renderedContents = Collections.emptyList(); public static final NodeParser TPAPI_PARSER = TagParser.DEFAULT; - + public CommonHolographicSignBlockEntity(BlockPos pPos, BlockState pBlockState) { super(PowerToolBlocks.HOLOGRAPHIC_SIGN_BLOCK_ENTITY.get(), pPos, pBlockState); } - + @Override public void readFrom(ValueInput input) { super.readFrom(input); @@ -39,7 +40,7 @@ public void readFrom(ValueInput input) { if (getLevel() != null && getLevel().isClientSide()) { renderedContents = contents.stream().map(storedComponent -> { String storedComponentFormatless = storedComponent.getString(); - Component parsed = TPAPI_PARSER.parseComponent(storedComponentFormatless, ParserContext.of()); + Component parsed = TPAPI_PARSER.parseComponent(storedComponentFormatless, PlaceholderContext.of(getLevel()).asParserContext()); if (parsed.getString().equals(storedComponentFormatless)) { return Component.literal(storedComponentFormatless); } else { @@ -48,7 +49,7 @@ public void readFrom(ValueInput input) { }).toList(); } } - + @Override public void writeTo(ValueOutput output) { super.writeTo(output); @@ -57,7 +58,7 @@ public void writeTo(ValueOutput output) { list.add(c); } } - + @Override public void filterMessage(ServerPlayer player) { var task = player.getTextFilter() diff --git a/src/main/java/org/teacon/powertool/block/holo_sign/SignType.java b/src/main/java/org/teacon/powertool/block/holo_sign/SignType.java index f30b1040..c51c537f 100644 --- a/src/main/java/org/teacon/powertool/block/holo_sign/SignType.java +++ b/src/main/java/org/teacon/powertool/block/holo_sign/SignType.java @@ -10,6 +10,7 @@ import net.minecraft.world.level.block.state.BlockState; import org.jspecify.annotations.NonNull; import org.teacon.powertool.block.entity.BaseHolographicSignBlockEntity; +import org.teacon.powertool.block.entity.BBCodeHolographicSignBlockEntity; import org.teacon.powertool.block.entity.CommonHolographicSignBlockEntity; import org.teacon.powertool.block.entity.LinkHolographicSignBlockEntity; import org.teacon.powertool.block.entity.RawJsonHolographicSignBlockEntity; @@ -20,6 +21,7 @@ public enum SignType implements StringRepresentable { COMMON(CommonHolographicSignBlockEntity::new), URL(LinkHolographicSignBlockEntity::new), RAW_JSON(RawJsonHolographicSignBlockEntity::new), + BBC(BBCodeHolographicSignBlockEntity::new), ; private final BiFunction blockEntitySupplier; diff --git a/src/main/java/org/teacon/powertool/client/PowerToolClientEvents.java b/src/main/java/org/teacon/powertool/client/PowerToolClientEvents.java index 0f030faf..94c1d02a 100644 --- a/src/main/java/org/teacon/powertool/client/PowerToolClientEvents.java +++ b/src/main/java/org/teacon/powertool/client/PowerToolClientEvents.java @@ -64,6 +64,7 @@ import org.teacon.powertool.client.renders.entity.FenceKnotRenderer; import org.teacon.powertool.client.renders.entity.MartingCarEntityRenderer; import org.teacon.powertool.client.renders.entity.model.MartingCarEntityModel; +import org.teacon.powertool.client.renders.holo_sign.BBCodeHolographicSignBlockEntityRenderer; import org.teacon.powertool.client.renders.holo_sign.HolographicSignBlockEntityRenderer; import org.teacon.powertool.client.renders.holo_sign.LinkHolographicSignBlockEntityRenderer; import org.teacon.powertool.client.renders.holo_sign.RawJsonHolographicSignBlockEntityRenderer; @@ -359,6 +360,10 @@ public static void renderers(EntityRenderersEvent.RegisterRenderers event) { PowerToolBlocks.RAW_JSON_HOLOGRAPHIC_SIGN_BLOCK_ENTITY.get(), RawJsonHolographicSignBlockEntityRenderer::new ); + event.registerBlockEntityRenderer( + PowerToolBlocks.BBC_HOLOGRAPHIC_SIGN_BLOCK_ENTITY.get(), + BBCodeHolographicSignBlockEntityRenderer::new + ); event.registerBlockEntityRenderer(PowerToolBlocks.TEMPLE_BLOCK_ENTITY.get(), TempleRenderer::new); event.registerBlockEntityRenderer( PowerToolBlocks.JEI_RECIPE_DISPLAY_BLOCK_ENTITY.get(), diff --git a/src/main/java/org/teacon/powertool/client/gui/holo_sign/BBCodeHolographicSignEditingScreen.java b/src/main/java/org/teacon/powertool/client/gui/holo_sign/BBCodeHolographicSignEditingScreen.java new file mode 100644 index 00000000..2d7a7687 --- /dev/null +++ b/src/main/java/org/teacon/powertool/client/gui/holo_sign/BBCodeHolographicSignEditingScreen.java @@ -0,0 +1,135 @@ +package org.teacon.powertool.client.gui.holo_sign; + +import com.xkball.xklib.ui.render.IComponent; +import com.xkball.xklib.ui.widget.Widget; +import com.xkball.xklib.ui.widget.container.ContainerWidget; +import dev.vfyjxf.taffy.style.TextAlign; +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.Nullable; +import org.teacon.powertool.annotation.NonNullByDefault; +import org.teacon.powertool.block.entity.BBCodeHolographicSignBlockEntity; +import org.teacon.powertool.client.gui.widget.AutoWidthMultiLineEditBoxWrapper; +import org.teacon.powertool.client.gui.widget.MultiLineEditBox; + +import java.util.ArrayList; +import java.util.Arrays; + +/** + * GUI screen for editing BBC Holographic Sign content. + * Uses parent class for most settings (color, scale, alignment, etc.). + * BBC-specific settings: + * - Min width (blocks) + * Fixed defaults (no GUI): + * - Line height: 1.0 + * - Bold: false + * - Underline: false + * - Italic: false + */ +@NonNullByDefault +public class BBCodeHolographicSignEditingScreen extends BaseHolographicSignEditingScreen { + + public static final int MAXIMUM_LINE_COUNT = 100; + public static final int MAXIMUM_LINE_LENGTH = 256; + private static final int MINIMUM_EDIT_BOX_WIDTH = 120; + + private String @Nullable [] messages; + private @Nullable MultiLineEditBox editBox; + + // BBC-specific parameter: min width + protected float bbcMinWidth; + + public BBCodeHolographicSignEditingScreen(BBCodeHolographicSignBlockEntity theSign) { + super(Component.translatable("sign.edit.bbc"), theSign); + } + + @Override + protected void readData(BBCodeHolographicSignBlockEntity theSign) { + super.readData(theSign); + var size = theSign.rawContent.size(); + this.messages = new String[size]; + for (int i = 0; i < size; i++) { + messages[i] = theSign.rawContent.get(i); + } + bbcMinWidth = theSign.minWidth; + } + + @Override + protected Widget createLeftPanel() { + var basePanel = (ContainerWidget) super.createLeftPanel(); + + // Add BBC-specific min width input + basePanel.addChild(createFloatInput( + 0F, 10F, 0.1F, + () -> IComponent.literal("Min Width (blocks)"), + () -> bbcMinWidth, + w -> bbcMinWidth = w + )); + + return basePanel; + } + + @Override + protected Widget createRightPanel() { + this.editBox = new MultiLineEditBox(); + this.editBox.setMaxLines(MAXIMUM_LINE_COUNT); + this.editBox.setMaxLineLength(MAXIMUM_LINE_LENGTH); + var editBoxWrapper = new AutoWidthMultiLineEditBoxWrapper(this.editBox, MINIMUM_EDIT_BOX_WIDTH); + if (this.messages != null) editBox.setValue(String.join("\n", messages)); + editBoxWrapper.setValueListener(s -> { + if (s.isEmpty()) { + this.messages = new String[0]; + } else { + this.messages = s.split("\n", -1); + } + }); + return new ContainerWidget().inlineStyle(""" + size: 100% 100%; + flex-direction: column; + align-items: flex-start; + justify-content: center; + overflow-x: scroll; + scrollbar-width: 8; + """) + .addChild(editBoxWrapper.inlineStyle("width: auto; min-width: 50%; height: 148rpx; flex-shrink: 0;")); + } + + @Override + protected void writeBackToBE() { + super.writeBackToBE(); + var toSend = this.messages == null ? new String[0] : Arrays.copyOfRange(this.messages, 0, this.messages.length); + sign.rawContent = new ArrayList<>(Arrays.asList(toSend)); + // Build forRender list + sign.forRender.clear(); + for (var line : toSend) { + sign.forRender.add(Component.literal(line)); + } + // Use parent class values as defaults + sign.defaultColor = this.color; + sign.defaultScale = this.scale; + sign.defaultAlign = switch (this.textAlign) { + case LEFT -> BBCodeHolographicSignBlockEntity.TextAlign.LEFT; + case CENTER -> BBCodeHolographicSignBlockEntity.TextAlign.CENTER; + case RIGHT -> BBCodeHolographicSignBlockEntity.TextAlign.RIGHT; + }; + // BBC-specific values + sign.minWidth = bbcMinWidth; + // Fixed defaults + sign.defaultLineHeight = 1.0F; + sign.defaultBold = false; + sign.defaultUnderline = false; + sign.defaultItalic = false; + + // Parse BBCodes after updating + sign.parseBBCode(sign.forRender); + } + + @Override + public void onTextAlignChange() { + if (this.editBox == null) return; + this.editBox.setTextAlign(switch (this.textAlign) { + case LEFT -> TextAlign.LEFT; + case CENTER -> TextAlign.CENTER; + case RIGHT -> TextAlign.RIGHT; + }); + } +} diff --git a/src/main/java/org/teacon/powertool/client/gui/holo_sign/BaseHolographicSignEditingScreen.java b/src/main/java/org/teacon/powertool/client/gui/holo_sign/BaseHolographicSignEditingScreen.java index 94dc5c2d..55b87e8f 100644 --- a/src/main/java/org/teacon/powertool/client/gui/holo_sign/BaseHolographicSignEditingScreen.java +++ b/src/main/java/org/teacon/powertool/client/gui/holo_sign/BaseHolographicSignEditingScreen.java @@ -14,6 +14,7 @@ import net.minecraft.world.level.block.entity.BlockEntity; import net.neoforged.neoforge.client.network.ClientPacketDistributor; import org.teacon.powertool.annotation.NonNullByDefault; +import org.teacon.powertool.block.entity.BBCodeHolographicSignBlockEntity; import org.teacon.powertool.block.entity.BaseHolographicSignBlockEntity; import org.teacon.powertool.block.entity.CommonHolographicSignBlockEntity; import org.teacon.powertool.block.entity.LinkHolographicSignBlockEntity; @@ -63,6 +64,8 @@ public static Screen creatHoloSignScreen(BlockEntity sign, SignType type) { sign instanceof LinkHolographicSignBlockEntity be ? new LinkHolographicSignEditingScreen(be) : null; case RAW_JSON -> sign instanceof RawJsonHolographicSignBlockEntity be ? new RawJsonHolographicSignEditingScreen(be) : null; + case BBC -> + sign instanceof BBCodeHolographicSignBlockEntity be ? new BBCodeHolographicSignEditingScreen(be) : null; }; } diff --git a/src/main/java/org/teacon/powertool/client/renders/holo_sign/BBCodeHolographicSignBlockEntityRenderer.java b/src/main/java/org/teacon/powertool/client/renders/holo_sign/BBCodeHolographicSignBlockEntityRenderer.java new file mode 100644 index 00000000..65091b11 --- /dev/null +++ b/src/main/java/org/teacon/powertool/client/renders/holo_sign/BBCodeHolographicSignBlockEntityRenderer.java @@ -0,0 +1,235 @@ +package org.teacon.powertool.client.renders.holo_sign; + +import com.mojang.blaze3d.vertex.PoseStack; +import eu.pb4.placeholders.api.PlaceholderContext; +import eu.pb4.placeholders.api.parsers.MarkdownLiteParserV1; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; +import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; +import net.minecraft.client.renderer.feature.ModelFeatureRenderer; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.Style; +import net.minecraft.network.chat.TextColor; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; +import org.teacon.powertool.annotation.NonNullByDefault; +import org.teacon.powertool.block.entity.BBCodeHolographicSignBlockEntity; + +import java.util.ArrayList; +import java.util.List; + +@NonNullByDefault +public class BBCodeHolographicSignBlockEntityRenderer implements BlockEntityRenderer { + + public BBCodeHolographicSignBlockEntityRenderer(BlockEntityRendererProvider.Context context) { + } + + /** + * Cached render data for a single text segment. + */ + public record CachedSegment(Component component, float scale, int color, int backgroundColor, float width) { + } + + /** + * Cached render data for a single line. + */ + public record CachedLine(List segments, BBCodeHolographicSignBlockEntity.TextAlign align, + float lineWidth, float lineHeight, float maxScale) { + } + + /** + * All cached render data for the sign. + */ + public record CachedRenderData(List lines, float totalMaxWidth, float totalHeight) { + } + + public static class BBCSignBEState extends HolographicSignBlockEntityRenderer.HoloSignStateBase { + public @Nullable List parsedLines; + public float defaultScale; + public int defaultColor; + public float defaultLineHeight; + public float minWidth; + public int globalBackgroundColor; + public @Nullable CachedRenderData cachedData; + } + + @Override + public BBCSignBEState createRenderState() { + return new BBCSignBEState(); + } + + @Override + public void extractRenderState(BBCodeHolographicSignBlockEntity be, BBCSignBEState state, float partialTicks, Vec3 cameraPosition, ModelFeatureRenderer.@Nullable CrumblingOverlay breakProgress) { + BlockEntityRenderer.super.extractRenderState(be, state, partialTicks, cameraPosition, breakProgress); + Font font = Minecraft.getInstance().font; + state.extractState(be); + state.parsedLines = be.parsedLines; + state.defaultScale = be.defaultScale; + state.defaultColor = be.defaultColor; + state.defaultLineHeight = be.defaultLineHeight; + state.minWidth = be.minWidth; + state.globalBackgroundColor = HolographicSignBlockEntityRenderer.getBackgroundColor(state); + + // Check if we need to build cached data + if (be.clientCacheData instanceof CachedRenderData cached) { + state.cachedData = cached; + } else { + state.cachedData = buildCachedData(be, font, state.globalBackgroundColor); + be.clientCacheData = state.cachedData; + } + } + + /** + * Build cached render data from parsed lines. + */ + private CachedRenderData buildCachedData(BBCodeHolographicSignBlockEntity be, Font font, int globalBackgroundColor) { + List lines = new ArrayList<>(); + float totalHeight = 0; + float totalMaxWidth = be.minWidth * 40.0F; + + for (var line : be.parsedLines) { + List segments = new ArrayList<>(); + float lineWidth = 0.0f; + float maxScale = be.defaultScale; + float lineHeightMult = line.lineLineHeight != null ? line.lineLineHeight : be.defaultLineHeight; + + for (var segment : line.segments) { + // Build component with TPAPI_PARSER + BBC styles + Component styledComponent = buildSegmentComponent(segment, be); + + float segmentScale = segment.size() != null ? segment.size() : be.defaultScale; + int segmentColor = segment.color() != null ? segment.color() : be.defaultColor; + int segmentBgColor = segment.backgroundColor() != null ? segment.backgroundColor() : globalBackgroundColor; + + // Calculate width + float segmentWidth = (font.width(styledComponent.getString()) * segmentScale); + + segments.add(new CachedSegment(styledComponent, segmentScale, segmentColor, segmentBgColor, segmentWidth)); + + lineWidth += segmentWidth; + if (segmentScale > maxScale) maxScale = segmentScale; + } + + + float lineHeight = (font.lineHeight + 2) * lineHeightMult * maxScale; + lines.add(new CachedLine(segments, line.lineAlign, lineWidth, lineHeight, maxScale)); + totalHeight += lineHeight; + totalMaxWidth = Math.max(lineWidth, totalMaxWidth); + } + + return new CachedRenderData(lines, totalMaxWidth, totalHeight); + } + + /** + * Build a styled component for a text segment. + */ + private Component buildSegmentComponent(BBCodeHolographicSignBlockEntity.TextSegment segment, BBCodeHolographicSignBlockEntity be) { + int segmentColor = segment.color() != null ? segment.color() : be.defaultColor; + Boolean bold = segment.bold() != null ? segment.bold() : be.dropShadow; + Boolean underline = segment.underline() != null ? segment.underline() : false; + Boolean italic = segment.italic() != null ? segment.italic() : false; + + // Parse with TPAPI first + Component parsed = MarkdownLiteParserV1.ALL + .parseComponent(segment.text(), PlaceholderContext.of(Minecraft.getInstance().player).asParserContext()); + + // Create BBC style from segment properties + Style bbcStyle = Style.EMPTY + .withColor(TextColor.fromRgb(segmentColor & 0xFFFFFF)) + .withBold(bold) + .withUnderlined(underline) + .withItalic(italic); + + // Merge: BBC as base, TPAPI parsed style on top (overrides) + return parsed.copy().setStyle(parsed.getStyle().applyTo(bbcStyle)); + } + + @Override + public void submit(BBCSignBEState state, PoseStack poseStack, SubmitNodeCollector submitNodeCollector, CameraRenderState camera) { + renderInternal(state, poseStack, submitNodeCollector, state.lightCoords, false, camera); + if (state.bidirectional) { + renderInternal(state, poseStack, submitNodeCollector, state.lightCoords, true, camera); + } + } + + public void renderInternal(BBCSignBEState theSign, PoseStack poseStack, SubmitNodeCollector nodeCollector, int packedLight, boolean backFace, CameraRenderState camera) { + poseStack.pushPose(); + HolographicSignBlockEntityRenderer.beforeRender(theSign, poseStack, camera, backFace); + + var cachedData = theSign.cachedData; + if (cachedData == null || cachedData.lines.isEmpty()) { + poseStack.popPose(); + return; + } + + // Starting Y position (centered vertically) + float currentY = -cachedData.totalHeight / 2.0F; + + for (var line : cachedData.lines) { + var lineAlign = line.align; + float lineWidth = line.lineWidth; + float lineHeight = line.lineHeight; + + // Calculate starting X based on alignment + + float currentX = switch (lineAlign) { + case LEFT -> -cachedData.totalMaxWidth / 2.0F; + case CENTER -> -lineWidth / 2.0F; + case RIGHT -> cachedData.totalMaxWidth / 2.0F - lineWidth; + }; + + for (var segment : line.segments) { + float x = currentX; + + renderSegmentWithScale(poseStack, nodeCollector, segment.component, theSign.dropShadow, + segment.color, segment.backgroundColor, packedLight, x, currentY, segment.scale, segment.width); + + // Update currentX for next segment + currentX += segment.width; + } + + // Move to next line + currentY += lineHeight; + } + + poseStack.popPose(); + } + + /** + * Render a text segment with a specific scale. + */ + private void renderSegmentWithScale(PoseStack poseStack, SubmitNodeCollector nodeCollector, + Component textComponent, boolean dropShadow, int color, int backgroundColor, int packedLight, + float x, float y, float scale, float width) { + + poseStack.pushPose(); + + // Apply scale for this segment + poseStack.translate(x, y, 0); + poseStack.scale(scale, scale, 1.0F); + poseStack.translate(-x, -y, 0); + + // Calculate render position based on alignment + + // Render background if enabled + if (backgroundColor != 0 && backgroundColor != org.teacon.powertool.utils.VanillaUtils.TRANSPARENT) { + HolographicSignBlockEntityRenderer.renderBackground(poseStack, nodeCollector, backgroundColor, packedLight, + x - 1.0F / scale, y - 1.0F / scale, (int) (width / scale)); + } + + // Render text with styling + nodeCollector.submitText(poseStack, x, y, textComponent.getVisualOrderText(), + dropShadow, Font.DisplayMode.POLYGON_OFFSET, packedLight, color, 0, 0); + + poseStack.popPose(); + } + + @Override + public AABB getRenderBoundingBox(BBCodeHolographicSignBlockEntity blockEntity) { + return new AABB(blockEntity.getBlockPos()).inflate(8, 8, 8); + } +} diff --git a/src/main/resources/assets/powertool/lang/en_us.json b/src/main/resources/assets/powertool/lang/en_us.json index 187f869e..615a7083 100644 --- a/src/main/resources/assets/powertool/lang/en_us.json +++ b/src/main/resources/assets/powertool/lang/en_us.json @@ -101,6 +101,7 @@ "block.powertool.temple": "Keeper's shrine", "block.powertool.link_holographic_sign": "Hyperlink Holographic Sign", "block.powertool.raw_json_holographic_sign": "Raw Json Holographic Sign", + "block.powertool.bbc_holographic_sign": "BBC Holographic Sign", "block.powertool.white_trash_can": "White Trash Can", "block.powertool.white_trash_can_cap": "White Trash Can Cap", "block.powertool.gray_trash_can": "Gray Trash Can", @@ -304,5 +305,7 @@ "powertool.creative_block_break_undo.success": "Restored the block at %s.", "powertool.creative_block_break_undo.failed": "Could not restore the block at %s because the position is not empty.", "powertool.creative_block_break_undo.tp": "click to teleport", - "powertool.creative_block_break_undo.empty": "There are no creative block breaks to undo." + "powertool.creative_block_break_undo.empty": "There are no creative block breaks to undo.", + + "sign.edit.bbc": "Edit BBC Sign Content" } diff --git a/src/main/resources/assets/powertool/lang/zh_cn.json b/src/main/resources/assets/powertool/lang/zh_cn.json index 606e86fb..cf55ad06 100644 --- a/src/main/resources/assets/powertool/lang/zh_cn.json +++ b/src/main/resources/assets/powertool/lang/zh_cn.json @@ -155,6 +155,7 @@ "block.powertool.temple": "土地庙", "block.powertool.link_holographic_sign": "超链接全息告示牌", "block.powertool.raw_json_holographic_sign": "原始Json格式全息告示牌", + "block.powertool.bbc_holographic_sign": "BBC全息告示牌", "block.powertool.white_trash_can": "白色垃圾桶", "block.powertool.white_trash_can_cap": "白色垃圾桶盖子", "block.powertool.gray_trash_can": "灰色垃圾桶", @@ -308,5 +309,7 @@ "powertool.creative_block_break_undo.success": "已恢复位于 %s 的方块。", "powertool.creative_block_break_undo.failed": "无法恢复位于 %s 的方块,原位置不是空气。", "powertool.creative_block_break_undo.tp": "点击传送", - "powertool.creative_block_break_undo.empty": "没有可撤销的创造模式方块破坏记录。" + "powertool.creative_block_break_undo.empty": "没有可撤销的创造模式方块破坏记录。", + + "sign.edit.bbc": "编辑 BBC 告示牌内容" } diff --git a/src/main/resources/assets/powertool/models/item/bbc_holographic_sign.json b/src/main/resources/assets/powertool/models/item/bbc_holographic_sign.json new file mode 100644 index 00000000..c7aae74d --- /dev/null +++ b/src/main/resources/assets/powertool/models/item/bbc_holographic_sign.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/handheld", + "textures": { + "layer0": "powertool:item/holographic_bbcode" + } +} diff --git a/src/main/resources/assets/powertool/textures/item/holographic_bbcode.png b/src/main/resources/assets/powertool/textures/item/holographic_bbcode.png new file mode 100644 index 00000000..bfef1d6e Binary files /dev/null and b/src/main/resources/assets/powertool/textures/item/holographic_bbcode.png differ