From 8ebdccc8307f32d062f0a908b123faf09c8be895 Mon Sep 17 00:00:00 2001 From: 3944Realms Date: Sun, 1 Mar 2026 22:34:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84CommandNode=EF=BC=8C?= =?UTF-8?q?=E4=BD=BF=E5=85=B6=E6=94=AF=E6=8C=81=E6=9D=83=E9=99=90=E6=8E=A7?= =?UTF-8?q?=E5=88=B6=E8=BE=93=E5=87=BA=E4=BB=A5=E5=8F=8A=E5=85=BC=E5=AE=B9?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=99=A8=E5=88=9D=E5=A7=8B=E5=8C=96=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E6=8E=A7=E5=88=B6=E8=A7=A6=E5=8F=91=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 194 +++++++++++- README.md | 18 ++ gradle.properties | 2 +- .../core/command/ICommandHelpManager.java | 74 ++--- .../lib39/core/command/IHelpCommand.java | 4 +- .../lib39/core/command/model/CommandNode.java | 281 +++++++++++++++++- .../lib39/core/compat/CompatManager.java | 2 +- .../lib39/example/compat/Lib39Compat.java | 1 + .../r3944realms/lib39/util/IUniPosOwner.java | 200 +++++++++++++ 9 files changed, 724 insertions(+), 52 deletions(-) create mode 100644 src/main/java/top/r3944realms/lib39/util/IUniPosOwner.java diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 63c3cde..c05a066 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,12 @@ -name: Build +name: Build and Release -on: [push, pull_request] +on: + push: + tags: + - 'v*' + +permissions: + contents: write jobs: build: @@ -10,7 +16,6 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 - fetch-tags: true - name: Setup JDK 21 uses: actions/setup-java@v4 @@ -18,8 +23,185 @@ jobs: java-version: '21' distribution: 'temurin' - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 + - name: Make gradlew executable + run: chmod +x ./gradlew - name: Build with Gradle - run: ./gradlew build \ No newline at end of file + run: ./gradlew build + + - name: Prepare release files + run: | + mkdir -p release-files + cp build/libs/*.jar release-files/ 2>/dev/null || true + echo "准备发布的文件:" + ls -la release-files/ + + - name: Upload release artifacts + uses: actions/upload-artifact@v4 + with: + name: release-files + path: release-files/ + retention-days: 7 + + release: + runs-on: ubuntu-latest + needs: build + if: startsWith(github.ref, 'refs/tags/v') + + steps: + - name: Checkout with full history + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: release-files + path: ./dist + + - name: Generate CZ-compliant changelog + id: generate_changelog + run: | + CURRENT_TAG="${{ github.ref_name }}" + PREV_TAG=$(git describe --tags --abbrev=0 $(git rev-list --tags --skip=1 --max-count=1) 2>/dev/null || echo "") + + # 创建临时文件 + TEMP_FILE=$(mktemp) + + echo "# 🚀 版本 $CURRENT_TAG 发布" > $TEMP_FILE + echo "" >> $TEMP_FILE + echo "## 📋 变更摘要" >> $TEMP_FILE + echo "" >> $TEMP_FILE + + if [ -z "$PREV_TAG" ]; then + echo "### 初始版本发布" >> $TEMP_FILE + echo "" >> $TEMP_FILE + echo "这是项目的第一个正式版本。" >> $TEMP_FILE + echo "" >> $TEMP_FILE + # 获取所有提交并按类型分组 + git log --pretty=format:"%s" --reverse | while read -r line; do + echo "- $line" >> $TEMP_FILE + done + else + echo "### 从 $PREV_TAG 到 $CURRENT_TAG 的变更" >> $TEMP_FILE + echo "" >> $TEMP_FILE + + # 定义符合CZ规范的提交类型映射 + declare -A commit_types + commit_types=( + ["✨ 新功能"]="^(feat|feature)(\(.*\))?:" + ["🐛 修复"]="^(fix|bugfix)(\(.*\))?:" + ["📝 文档"]="^(docs|documentation)(\(.*\))?:" + ["🎨 样式"]="^(style)(\(.*\))?:" + ["🔨 重构"]="^(refactor)(\(.*\))?:" + ["⚡️ 性能"]="^(perf|performance)(\(.*\))?:" + ["✅ 测试"]="^(test)(\(.*\))?:" + ["🔧 构建"]="^(build)(\(.*\))?:" + ["👷 CI"]="^(ci)(\(.*\))?:" + ["📦 依赖"]="^(chore|deps)(\(.*\))?:" + ["⏪ 回退"]="^(revert)(\(.*\))?:" + ) + + # 获取所有提交 + COMMITS=$(git log --pretty=format:"%s" $PREV_TAG..HEAD) + + # 处理每种类型的提交 + for type_name in "${!commit_types[@]}"; do + pattern="${commit_types[$type_name]}" + + # 提取匹配的提交 + matched_commits=$(echo "$COMMITS" | grep -E "$pattern" || true) + + if [ -n "$matched_commits" ]; then + echo "#### $type_name" >> $TEMP_FILE + echo "" >> $TEMP_FILE + + # 处理每条提交,提取scope和subject + echo "$matched_commits" | while read -r commit; do + # 解析scope和subject + if [[ $commit =~ ^[a-z]+\((.*)\):\ (.*) ]]; then + scope="${BASH_REMATCH[1]}" + subject="${BASH_REMATCH[2]}" + echo "- **$scope**: $subject" >> $TEMP_FILE + elif [[ $commit =~ ^[a-z]+:\ (.*) ]]; then + subject="${BASH_REMATCH[1]}" + echo "- $subject" >> $TEMP_FILE + else + echo "- $commit" >> $TEMP_FILE + fi + done + echo "" >> $TEMP_FILE + fi + done + + # 处理破坏性变更(BREAKING CHANGE) + breaking_changes=$(git log --pretty=format:"%b" $PREV_TAG..HEAD | grep -i "BREAKING CHANGE" || true) + if [ -n "$breaking_changes" ]; then + echo "#### ⚠️ 破坏性变更" >> $TEMP_FILE + echo "" >> $TEMP_FILE + echo "$breaking_changes" | while read -r line; do + echo "- $line" >> $TEMP_FILE + done + echo "" >> $TEMP_FILE + fi + + # 处理未分类的提交 + uncategorized="$COMMITS" + for pattern in "${commit_types[@]}"; do + uncategorized=$(echo "$uncategorized" | grep -v -E "$pattern" || true) + done + + if [ -n "$uncategorized" ]; then + echo "#### 📝 其他更改" >> $TEMP_FILE + echo "" >> $TEMP_FILE + echo "$uncategorized" | while read -r commit; do + echo "- $commit" >> $TEMP_FILE + done + echo "" >> $TEMP_FILE + fi + fi + + echo "## 📊 统计信息" >> $TEMP_FILE + echo "" >> $TEMP_FILE + + if [ -z "$PREV_TAG" ]; then + TOTAL_COMMITS=$(git rev-list --count HEAD) + echo "- 总提交数: $TOTAL_COMMITS" >> $TEMP_FILE + echo "- 首次发布" >> $TEMP_FILE + else + COMMITS=$(git rev-list --count $PREV_TAG..HEAD) + echo "- 本次发布提交数: $COMMITS" >> $TEMP_FILE + echo "- 上一个版本: $PREV_TAG" >> $TEMP_FILE + fi + + echo "- 发布日期: $(date '+%Y年%m月%d日')" >> $TEMP_FILE + echo "- 当前版本: $CURRENT_TAG" >> $TEMP_FILE + echo "" >> $TEMP_FILE + echo "---" >> $TEMP_FILE + echo "" >> $TEMP_FILE + echo "### 📜 详细提交历史" >> $TEMP_FILE + echo "" >> $TEMP_FILE + + # 显示所有提交的详细列表,包含scope信息 + if [ -z "$PREV_TAG" ]; then + git log --pretty=format:"- **%h** %s - %an (%ad)" --date=short --reverse | head -100 >> $TEMP_FILE + else + git log --pretty=format:"- **%h** %s - %an (%ad)" --date=short $PREV_TAG..HEAD | head -100 >> $TEMP_FILE + fi + + # 将文件内容输出到变量 + CHANGELOG_CONTENT=$(cat $TEMP_FILE) + echo "changelog<> $GITHUB_OUTPUT + echo "$CHANGELOG_CONTENT" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Create Release + uses: ncipollo/release-action@v1 + with: + artifacts: "dist/*.jar" + tag: ${{ github.ref_name }} + name: "版本 ${{ github.ref_name }}" + body: ${{ steps.generate_changelog.outputs.changelog }} + draft: false + prerelease: false \ No newline at end of file diff --git a/README.md b/README.md index 535f3ac..d896c44 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,21 @@ **Lib39** is a general-purpose dependency library for Minecraft mods. It provides utility methods and core functionality that other mods can build upon. +### How to implementation? + +​ **In repositories:** + +```groovy + maven { + name = "LTD Maven" + url = "https://nexus.bot.leisuretimedock.top/repository/maven-public/" +} +``` + +​ **In dependencies:** + +```groovy +dependencies { + implementation("top.r3944realms.lib39:lib39:1.20.1-0.4.1") +} +``` \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index d21b4ee..73200c0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -33,7 +33,7 @@ mod_name=3944Realms 's Lib Mod # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. mod_license=MIT # The mod version. See https://semver.org/ -mod_version=0.3.6 +mod_version=0.4.1 # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. # This should match the base package used for the mod sources. # See https://maven.apache.org/guides/mini/guide-naming-conventions.html diff --git a/src/main/java/top/r3944realms/lib39/core/command/ICommandHelpManager.java b/src/main/java/top/r3944realms/lib39/core/command/ICommandHelpManager.java index bc479c1..11a267f 100644 --- a/src/main/java/top/r3944realms/lib39/core/command/ICommandHelpManager.java +++ b/src/main/java/top/r3944realms/lib39/core/command/ICommandHelpManager.java @@ -1,6 +1,7 @@ package top.r3944realms.lib39.core.command; import net.minecraft.ChatFormatting; +import net.minecraft.commands.CommandSourceStack; import net.minecraft.network.chat.*; import net.minecraft.resources.ResourceLocation; import org.jetbrains.annotations.NotNull; @@ -435,43 +436,46 @@ public interface ICommandHelpManager { private void buildCommandTreeString(@NotNull CommandNode node, @NotNull String indent, @Nullable String currentFullPath, - @NotNull List result) { + @NotNull List result, + CommandSourceStack commandSourceStack) { boolean isRoot = indent.isEmpty(); - MutableComponent commandLine = buildCommandLine(node, indent, isRoot, currentFullPath); - result.add(commandLine.append(Component.literal(NEWLINE))); + if (node.testPermission(commandSourceStack)) { + MutableComponent commandLine = buildCommandLine(node, indent, isRoot, currentFullPath); + result.add(commandLine.append(Component.literal(NEWLINE))); - // 遞歸處理子節點 - String childIndent = indent + "| "; - if (node.isExpanded()) { - String newFullPath = (currentFullPath != null && !currentFullPath.isEmpty()) - ? currentFullPath + " " + node.getName() - : "/" + node.getName(); + // 遞歸處理子節點 + String childIndent = indent + "| "; + if (node.isExpanded()) { + String newFullPath = (currentFullPath != null && !currentFullPath.isEmpty()) + ? currentFullPath + " " + node.getName() + : "/" + node.getName(); - for (CommandNode child : node.getChildren()) { - // 只顯示有描述的子命令 - if (!child.getDescription().getString().isEmpty()) { - buildCommandTreeString(child, childIndent, newFullPath, result); + for (CommandNode child : node.getChildren()) { + // 只顯示有描述的子命令 + if (!child.getDescription().getString().isEmpty() && node.testPermission(commandSourceStack)) { + buildCommandTreeString(child, childIndent, newFullPath, result, commandSourceStack); + } } - } - } else if (shouldShowCollapsedInfo(node)) { - long childCount = getValidChildCount(node); - if (childCount > 0) { - MutableComponent collapsedInfo = Component.literal(indent + "| " + "└─ ") - .withStyle(ChatFormatting.GRAY) - .append(Component.translatable( - Lib39LangKey.Message.HELP_NODE_EXPAND.getKey(), - childCount - ).withStyle(ChatFormatting.GRAY)); + } else if (shouldShowCollapsedInfo(node)) { + long childCount = getValidChildCount(node); + if (childCount > 0) { + MutableComponent collapsedInfo = Component.literal(indent + "| " + "└─ ") + .withStyle(ChatFormatting.GRAY) + .append(Component.translatable( + Lib39LangKey.Message.HELP_NODE_EXPAND.getKey(), + childCount + ).withStyle(ChatFormatting.GRAY)); - collapsedInfo.withStyle(Style.EMPTY - .withClickEvent(new ClickEvent( - ClickEvent.Action.RUN_COMMAND, - "/" + getCommandHead() + " help toggle " + node.hashCode() - )) - .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.translatable(Lib39LangKey.Message.HELP_CLICK_EXPAND.getKey()))) - ); + collapsedInfo.withStyle(Style.EMPTY + .withClickEvent(new ClickEvent( + ClickEvent.Action.RUN_COMMAND, + "/" + getCommandHead() + " help toggle " + node.hashCode() + )) + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.translatable(Lib39LangKey.Message.HELP_CLICK_EXPAND.getKey()))) + ); - result.add(collapsedInfo.append(Component.literal(NEWLINE))); + result.add(collapsedInfo.append(Component.literal(NEWLINE))); + } } } } @@ -481,9 +485,9 @@ public interface ICommandHelpManager { * * @return 命令树列表 command tree */ - default List getCommandTree() { + default List getCommandTree(CommandSourceStack commandSourceStack) { List result = new ArrayList<>(); - buildCommandTreeString(getRootNode(), "", "", result); + buildCommandTreeString(getRootNode(), "", "", result, commandSourceStack); return result; } @@ -535,8 +539,8 @@ public interface ICommandHelpManager { * * @return the mutable component */ - default MutableComponent buildCommandTreeHelp() { - List commandTree = getCommandTree(); + default MutableComponent buildCommandTreeHelp(CommandSourceStack commandSourceStack) { + List commandTree = getCommandTree(commandSourceStack); return buildHelpMessage(Component.translatable(Lib39LangKey.Message.HELP_HEADER.getKey(), Component.translatable(getHeadKey())), commandTree); } diff --git a/src/main/java/top/r3944realms/lib39/core/command/IHelpCommand.java b/src/main/java/top/r3944realms/lib39/core/command/IHelpCommand.java index d1e2ab3..9878475 100644 --- a/src/main/java/top/r3944realms/lib39/core/command/IHelpCommand.java +++ b/src/main/java/top/r3944realms/lib39/core/command/IHelpCommand.java @@ -87,7 +87,7 @@ public interface IHelpCommand { */ default int handleHelp(@NotNull CommandContext context) { ICommandHelpManager commandHelpManager = getCommandHelpManager(); - MutableComponent helpMessage = commandHelpManager.buildCommandTreeHelp(); + MutableComponent helpMessage = commandHelpManager.buildCommandTreeHelp(context.getSource()); sendSuccess(context.getSource(), helpMessage); return 1; } @@ -103,7 +103,7 @@ public interface IHelpCommand { ICommandHelpManager commandHelpManager = getCommandHelpManager(); boolean success = commandHelpManager.toggleNodeExpanded(hash); if (success) { - MutableComponent helpMessage = Component.literal("\n".repeat(2)).append(commandHelpManager.buildCommandTreeHelp()); + MutableComponent helpMessage = Component.literal("\n".repeat(2)).append(commandHelpManager.buildCommandTreeHelp(context.getSource())); sendSuccess(context.getSource(), helpMessage); } else if (shouldShowToggleFailed()) { sendFailure(context.getSource(), Component.translatable(Lib39LangKey.Message.HELP_TOGGLE_FAILED.getKey())); diff --git a/src/main/java/top/r3944realms/lib39/core/command/model/CommandNode.java b/src/main/java/top/r3944realms/lib39/core/command/model/CommandNode.java index 85d4ec0..1caf181 100644 --- a/src/main/java/top/r3944realms/lib39/core/command/model/CommandNode.java +++ b/src/main/java/top/r3944realms/lib39/core/command/model/CommandNode.java @@ -1,5 +1,6 @@ package top.r3944realms.lib39.core.command.model; +import net.minecraft.commands.CommandSourceStack; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.MutableComponent; import org.jetbrains.annotations.Contract; @@ -8,6 +9,7 @@ import org.jetbrains.annotations.Nullable; import top.r3944realms.lib39.core.command.ICommandHelpManager; import java.util.*; +import java.util.function.Predicate; /** * The type Command node. @@ -18,6 +20,8 @@ public class CommandNode { private boolean hashValid = false; private final ICommandHelpManager helpManager; private final String name; + private final Predicate testPermission; + private static final Predicate TRUE = i -> true; private final MutableComponent description; private final Map children; @@ -31,6 +35,9 @@ public class CommandNode { // 展开/闭合状态 private boolean expanded = true; + public boolean testPermission(CommandSourceStack source) { + return testPermission.test(source); + } /** * Instantiates a new Command node. * @@ -40,12 +47,7 @@ public class CommandNode { * @param isRoot the is root */ public CommandNode(ICommandHelpManager helpManager, String name, MutableComponent description, boolean isRoot) { - this.name = name; - this.helpManager = helpManager; - this.description = description; - this.children = new TreeMap<>(); - this.isRoot = isRoot; - invalidateHash(); + this(helpManager, name, description, isRoot, TRUE); } /** @@ -56,10 +58,42 @@ public class CommandNode { * @param description the description */ public CommandNode(ICommandHelpManager helpManager, String name, MutableComponent description) { + this(helpManager, name, description, TRUE); + } + + /** + * Instantiates a new Command node. + * + * @param helpManager the help manager + * @param name the name + * @param description the description + * @param isRoot the is root + * @param testPermission test has permission + */ + public CommandNode(ICommandHelpManager helpManager, String name, MutableComponent description, boolean isRoot, Predicate testPermission) { this.name = name; this.helpManager = helpManager; this.description = description; this.children = new TreeMap<>(); + this.isRoot = isRoot; + this.testPermission = testPermission; + invalidateHash(); + } + + /** + * Instantiates a new Command node. + * + * @param helpManager the help manager + * @param name the name + * @param description the description + * @param testPermission test has permission + */ + public CommandNode(ICommandHelpManager helpManager, String name, MutableComponent description, Predicate testPermission) { + this.name = name; + this.helpManager = helpManager; + this.description = description; + this.children = new TreeMap<>(); + this.testPermission = testPermission; invalidateHash(); } @@ -467,6 +501,36 @@ public class CommandNode { return root(name, Component.translatable(translationKey)); } + /** + * 添加根節點 + * + * @param name the name + * @param description the description + * @param testPermission the test permission + * @return the builder + */ + public Builder root(@NotNull String name, @NotNull MutableComponent description, Predicate testPermission) { + if (root != null) { + throw new IllegalStateException("Root node already set"); + } + root = new CommandNode(helpManager, name, description, true, testPermission); + nodeStack.push(root); + return this; + } + + /** + * 添加根節點(使用翻譯鍵) + * + * @param name the name + * @param translationKey the translation key + * @param testPermission the test permission + * @return the builder + */ + public Builder root(@NotNull String name, @NotNull String translationKey, Predicate testPermission) { + return root(name, Component.translatable(translationKey), testPermission); + } + + /** * 進入子節點(向下移動) * @@ -552,6 +616,96 @@ public class CommandNode { return this; } + /** + * 進入子節點(向下移動) + * + * @param name the name + * @param description the description + * @param testPermission the test permission + * @return the builder + */ + public Builder push(@NotNull String name, @NotNull MutableComponent description, Predicate testPermission) { + checkBuilt(); + if (nodeStack.isEmpty()) { + throw new IllegalStateException("No parent node available. Call root() first."); + } + + CommandNode parent = nodeStack.peek(); + CommandNode child = new CommandNode(helpManager, name, description, testPermission); + + if (!child.isLeaf()) { + child.setExpanded(false); + } + + parent.addChild(child); + nodeStack.push(child); + return this; + } + + /** + * 進入子節點(使用翻譯鍵) + * + * @param name the name + * @param translationKey the translation key + * @param testPermission the test permission + * @return the builder + */ + public Builder push(@NotNull String name, @NotNull String translationKey, Predicate testPermission) { + return push(name, Component.translatable(translationKey), testPermission); + } + + /** + * 進入子節點並添加參數 + * + * @param name the name + * @param description the description + * @param testPermission the test permission + * @param parameters the parameters + * @return the builder + */ + public Builder pushWithParams(@NotNull String name, @NotNull MutableComponent description, + Predicate testPermission, @NotNull Parameter... parameters) { + push(name, description, testPermission); + current().addParameters(parameters); + return this; + } + + /** + * 進入子節點並添加必填參數 + * + * @param name the name + * @param description the description + * @param testPermission the test permission + * @param paramNames the param names + * @return the builder + */ + public Builder pushWithRequiredParams(@NotNull String name, @NotNull MutableComponent description, + Predicate testPermission, @NotNull String @NotNull ... paramNames) { + push(name, description, testPermission); + for (String paramName : paramNames) { + current().addParameter(paramName, true); + } + return this; + } + + /** + * 進入子節點並添加可選參數 + * + * @param name the name + * @param description the description + * @param testPermission the test permission + * @param paramNames the param names + * @return the builder + */ + public Builder pushWithOptionalParams(@NotNull String name, @NotNull MutableComponent description, + Predicate testPermission, @NotNull String @NotNull ... paramNames) { + push(name, description, testPermission); + for (String paramName : paramNames) { + current().addParameter(paramName, false); + } + return this; + } + /** * 返回上一級節點(向上移動) * @@ -711,6 +865,119 @@ public class CommandNode { return branch(name, Component.translatable(translationKey), configurator); } + /** + * 添加一個完整的命令分支(鏈式調用) + * + * @param name the name + * @param description the description + * @param testPermission the test permission + * @param configurator the configurator + * @return the builder + */ + public Builder branch(@NotNull String name, @NotNull MutableComponent description, + Predicate testPermission, @NotNull BranchConfigurator configurator) { + push(name, description, testPermission); + configurator.configure(this); + pop(); + return this; + } + + /** + * 添加一個完整的命令分支(使用翻譯鍵) + * + * @param name the name + * @param translationKey the translation key + * @param testPermission the test permission + * @param configurator the configurator + * @return the builder + */ + public Builder branch(@NotNull String name, @NotNull String translationKey, + Predicate testPermission, @NotNull BranchConfigurator configurator) { + return branch(name, Component.translatable(translationKey), testPermission, configurator); + } + + /** + * 快速添加葉子節點(沒有子節點的節點) + * + * @param name the name + * @param description the description + * @param testPermission the test permission + * @param parameters the parameters + * @return the builder + */ + public Builder leaf(@NotNull String name, @NotNull MutableComponent description, + Predicate testPermission, @NotNull Parameter @NotNull ... parameters) { + push(name, description, testPermission); + if (parameters.length > 0) { + params(parameters); + } + pop(); + return this; + } + + /** + * 快速添加葉子節點(使用翻譯鍵) + * + * @param name the name + * @param translationKey the translation key + * @param testPermission the test permission + * @param parameters the parameters + * @return the builder + */ + public Builder leaf(@NotNull String name, @NotNull String translationKey, + Predicate testPermission, @NotNull Parameter... parameters) { + return leaf(name, Component.translatable(translationKey), testPermission,parameters); + } + + /** + * 批量添加多個葉子節點 + * + * @param commands the commands + * @param testPermission the test permission + * @return the builder + */ + public Builder leaves(@NotNull Map commands, Predicate testPermission) { + for (Map.Entry entry : commands.entrySet()) { + leaf(entry.getKey(), entry.getValue(), testPermission); + } + return this; + } + + /** + * 批量添加多個葉子節點 + * + * @param commands the commands + * @param testPermission the test permission + * @return the builder + */ + public Builder leavesT(@NotNull Map commands, Predicate testPermission) { + for (Map.Entry entry : commands.entrySet()) { + leaf(entry.getKey(), entry.getValue(), testPermission); + } + return this; + } + + /** + * 批量添加多個帶參數的葉子節點 + * + * @param commands the commands + * @param testPermission the test permission + * @return the builder + */ + public Builder leavesWithParams(@NotNull Map commands, Predicate testPermission) { + for (Map.Entry entry : commands.entrySet()) { + LeafConfig config = entry.getValue(); + push(entry.getKey(), config.description(), testPermission); + if (config.parameters() != null && !config.parameters().isEmpty()) { + for (Parameter param : config.parameters()) { + param(param.name(), param.required()); + } + } + pop(); + } + return this; + } + /** * 快速添加葉子節點(沒有子節點的節點) * @@ -739,7 +1006,7 @@ public class CommandNode { */ public Builder leaf(@NotNull String name, @NotNull String translationKey, @NotNull Parameter... parameters) { - return leaf(name, Component.translatable(translationKey), parameters); + return leaf(name, Component.translatable(translationKey) ,parameters); } /** diff --git a/src/main/java/top/r3944realms/lib39/core/compat/CompatManager.java b/src/main/java/top/r3944realms/lib39/core/compat/CompatManager.java index 53548e3..ae26ebd 100644 --- a/src/main/java/top/r3944realms/lib39/core/compat/CompatManager.java +++ b/src/main/java/top/r3944realms/lib39/core/compat/CompatManager.java @@ -151,7 +151,7 @@ public abstract class CompatManager { // 初始化所有兼容模块 for (Map.Entry entry : compats.entrySet()) { - if (!entry.getValue().isInitialized()) { + if (!entry.getValue().isInitialized() && entry.getValue().isModLoaded()) { try { entry.getValue().initialize(); entry.getValue().setInitialize(true); diff --git a/src/main/java/top/r3944realms/lib39/example/compat/Lib39Compat.java b/src/main/java/top/r3944realms/lib39/example/compat/Lib39Compat.java index 05fb3e9..e212b6c 100644 --- a/src/main/java/top/r3944realms/lib39/example/compat/Lib39Compat.java +++ b/src/main/java/top/r3944realms/lib39/example/compat/Lib39Compat.java @@ -51,6 +51,7 @@ public class Lib39Compat implements ICompat { public void addCommonModListener(@NotNull IEventBus modBus) { modBus.addListener(this::onSetUp); } + private void onSetUp (@NotNull FMLCommonSetupEvent event) { event.enqueueWork(() -> Lib39.LOGGER.info("Loading Lib39 Compat")); } diff --git a/src/main/java/top/r3944realms/lib39/util/IUniPosOwner.java b/src/main/java/top/r3944realms/lib39/util/IUniPosOwner.java new file mode 100644 index 0000000..fef500f --- /dev/null +++ b/src/main/java/top/r3944realms/lib39/util/IUniPosOwner.java @@ -0,0 +1,200 @@ +package top.r3944realms.lib39.util; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; + +/** + * 一个对象接口,该对象可声称对特定维度中的 BlockPos 拥有唯一所有权。 + * 这个系统被称为 "uniPos",可确保(Level、BlockPos)对在同一时间只能被一个对象 "拥有"。 + * 一次只能被一个对象 "拥有"。 + * + *

重要:由于使用了 weakValues(),实现类必须被其他对象强引用, + * 否则会被 GC 清理导致锁自动释放。通常这意味着将实现类实例存储在 + * 适当的管理器或容器中。 + * @author sch246 + */ +public interface IUniPosOwner { + + /** + * 检查特定位置当前是否被 *任意* 对象拥有。 + * + * @param level 维度 The level (dimension) of the position. + * @param pos 坐标 The position to lock. + * @return true if the position has an owner, false otherwise. + */ + default boolean isLocked(Level level, BlockPos pos) { + return UniPosManager.INSTANCE.getOwner(level, pos) != null; + } + + /** + * 获取位置的当前所有者(如果存在的话)。 + * + * @param level 维度 The level (dimension) of the position. + * @param pos 坐标 The position to lock. + * @return An Optional containing the owner, or an empty Optional if the position is not owned. + */ + default Optional getOwner(Level level, BlockPos pos) { + return Optional.ofNullable(UniPosManager.INSTANCE.getOwner(level, pos)); + } + + /** + * 检查该对象是否可以锁定指定位置。 + * 如果该位置当前未锁定,或者该对象已经是所有者,则该值为 true。 + * 注意:此方法不是原子操作,仅用于快速检查。 + * 实际锁定时应使用 tryLock() 并检查返回值。 + * + * @param level 维度 The level (dimension) of the position. + * @param pos 坐标 The position to lock. + * @return true if this object can claim ownership. + */ + default boolean canLock(Level level, BlockPos pos) { + IUniPosOwner owner = UniPosManager.INSTANCE.getOwner(level, pos); + return owner == null || owner == this; + } + + /** + * 尝试对该对象的指定位置声明所有权。 + * 此操作是原子操作。 + * + * @param level 维度 The level (dimension) of the position. + * @param pos 坐标 The position to lock. + * @return true if ownership was successfully claimed or was already held by this object, + * false if the position is owned by another object. + */ + default boolean tryLock(Level level, BlockPos pos) { + return UniPosManager.INSTANCE.tryLock(level, pos, this); + } + + /** + * 释放指定位置的所有权。 + * 只有当该对象是当前所有者时,此操作才会成功。 + * + * @param level 位置的级别(维度) The level (dimension) of the position. + * @param pos 要解锁的位置 The position to unlock. + * @return true if the lock was successfully removed, false otherwise. + */ + default boolean unLock(Level level, BlockPos pos) { + return UniPosManager.INSTANCE.unLock(level, pos, this); + } + + /** + * 续租,继续持有锁 + * @param level 维度 + * @param pos 位置 + */ + default void refreshLock(Level level, BlockPos pos) { + UniPosManager.INSTANCE.refreshLock(level, pos, this); + } +} + +/** + 管理 IUniPosOwner 系统的单例存储。 + 该类是后台实现,大多数类不应直接使用。 + 在同一维度键(ResourceKey) -> 每维度的坐标缓存。 + // 缓存键为 BlockPos 的 long 值,值为 IUniPosOwner 的弱值(weak values)。 + // 使用维度键确保:同一维度的不同 Level 实例在服务端共享同一套锁; + // 使用弱值确保:当所有者被 GC 时,锁能自动释放。 + private final ConcurrentMap, Cache> dimensionLocks; + + private UniPosManager() { + this.dimensionLocks = new ConcurrentHashMap<>(); + } + + /** + 获取或创建特定维度键对应的 Cache。 + Cache 使用 BlockPos 的 long 值作为键,IUniPosOwner 的弱值作为值。 + 按 ResourceKey 分隔缓存,因此同一维度的不同 Level 实例在服务端共享同一套锁。 + computeIfAbsent 是原子操作,保证线程安全地获取或创建 Cache。 + */ + private Cache getDimensionCache(Level level) { + // computeIfAbsent 是原子操作,保证线程安全地获取或创建 Cache + return dimensionLocks.computeIfAbsent(level.dimension(), k -> + CacheBuilder.newBuilder() + .expireAfterWrite(5, TimeUnit.SECONDS) + .build() + ); + } + + /** + * 获取位置的当前所有者。 + * + * @param level 维度 + * @param pos 坐标 + * @return 如果存在所有者,则返回所有者对象;否则返回 null。 + */ + @Nullable + public IUniPosOwner getOwner(Level level, BlockPos pos) { + return getDimensionCache(level).getIfPresent(pos.asLong()); + } + + /** + * 尝试为指定位置设置所有者。 + * 如果锁已被同一所有者获得或持有。 + * 此操作是原子操作,避免竞态条件。 + * + * @param level 维度 + * @param pos 坐标 + * @param owner 要声明所有权的对象 + * @return true if the lock was acquired or already held by the same owner, false otherwise. + */ + public boolean tryLock(Level level, BlockPos pos, IUniPosOwner owner) { + IUniPosOwner existing = getDimensionCache(level).asMap().putIfAbsent(pos.asLong(), owner); + return existing == null || existing == owner; + } + + /** + * 解锁一个位置,但前提是所提供的所有者是当前所有者。 + * 此操作是原子操作。 + * + * @param level 维度 + * @param pos 要解锁的位置 + * @param owner 尝试解锁的对象 + * @return true if the lock was successfully released by this owner. + */ + public boolean unLock(Level level, BlockPos pos, IUniPosOwner owner) { + Cache cache = dimensionLocks.get(level.dimension()); + if (cache == null) { + return false; // 该维度没有锁 + } + + // 使用 asMap() 提供的原子操作 + return cache.asMap().remove(pos.asLong(), owner); + } + + /** + * 续租,继续持有锁 + * @param level 维度 + * @param pos 位置 + * @param owner 对象 + */ + public void refreshLock(Level level, BlockPos pos, IUniPosOwner owner) { + Cache cache = dimensionLocks.get(level.dimension()); + if (cache != null) { + // 只有当锁的主人是当前 owner 时,才更新时间 + // put 操作会重置 expireAfterWrite 的计时器 + cache.asMap().computeIfPresent(pos.asLong(), (k, v) -> { + if (v == owner) return owner; + return v; + }); + } + } +} \ No newline at end of file