feat: 完善CommandNode,使其支持权限控制输出以及兼容管理器初始化方法控制触发完善
This commit is contained in:
parent
fc28b6846b
commit
8ebdccc830
194
.github/workflows/build.yml
vendored
194
.github/workflows/build.yml
vendored
|
|
@ -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
|
||||
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<<EOF" >> $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
|
||||
18
README.md
18
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")
|
||||
}
|
||||
```
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<MutableComponent> result) {
|
||||
@NotNull List<MutableComponent> 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<MutableComponent> getCommandTree() {
|
||||
default List<MutableComponent> getCommandTree(CommandSourceStack commandSourceStack) {
|
||||
List<MutableComponent> 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<MutableComponent> commandTree = getCommandTree();
|
||||
default MutableComponent buildCommandTreeHelp(CommandSourceStack commandSourceStack) {
|
||||
List<MutableComponent> commandTree = getCommandTree(commandSourceStack);
|
||||
return buildHelpMessage(Component.translatable(Lib39LangKey.Message.HELP_HEADER.getKey(), Component.translatable(getHeadKey())), commandTree);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ public interface IHelpCommand {
|
|||
*/
|
||||
default int handleHelp(@NotNull CommandContext<CommandSourceStack> 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()));
|
||||
|
|
|
|||
|
|
@ -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<CommandSourceStack> testPermission;
|
||||
private static final Predicate<CommandSourceStack> TRUE = i -> true;
|
||||
private final MutableComponent description;
|
||||
private final Map<String, CommandNode> 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<CommandSourceStack> 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<CommandSourceStack> 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<CommandSourceStack> 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<CommandSourceStack> 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<CommandSourceStack> 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<CommandSourceStack> 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<CommandSourceStack> 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<CommandSourceStack> 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<CommandSourceStack> 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<CommandSourceStack> 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<CommandSourceStack> 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<CommandSourceStack> 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<CommandSourceStack> 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<String, MutableComponent> commands, Predicate<CommandSourceStack> testPermission) {
|
||||
for (Map.Entry<String, MutableComponent> 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<String, String> commands, Predicate<CommandSourceStack> testPermission) {
|
||||
for (Map.Entry<String, String> 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<String, LeafConfig> commands, Predicate<CommandSourceStack> testPermission) {
|
||||
for (Map.Entry<String, LeafConfig> 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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ public abstract class CompatManager {
|
|||
|
||||
// 初始化所有兼容模块
|
||||
for (Map.Entry<ResourceLocation, ICompat> entry : compats.entrySet()) {
|
||||
if (!entry.getValue().isInitialized()) {
|
||||
if (!entry.getValue().isInitialized() && entry.getValue().isModLoaded()) {
|
||||
try {
|
||||
entry.getValue().initialize();
|
||||
entry.getValue().setInitialize(true);
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
|
|
|
|||
200
src/main/java/top/r3944realms/lib39/util/IUniPosOwner.java
Normal file
200
src/main/java/top/r3944realms/lib39/util/IUniPosOwner.java
Normal file
|
|
@ -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)对在同一时间只能被一个对象 "拥有"。
|
||||
* 一次只能被一个对象 "拥有"。
|
||||
*
|
||||
* <p><b>重要:</b>由于使用了 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<IUniPosOwner> 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<Level)范围内,确保任意时刻一个 BlockPos 只能被一个对象“拥有”。
|
||||
注意:锁的作用域是维度键级别,因此在服务端同一维度的不同 Level 实例之间共享。
|
||||
缓存以 BlockPos 的 long 值为键,以 IUniPosOwner 的弱值(weak values)为值;
|
||||
当所有者不再被强引用时,条目会自动移除,从而释放锁。
|
||||
该实现面向服务端使用。
|
||||
*/
|
||||
final class UniPosManager {
|
||||
|
||||
public static final UniPosManager INSTANCE = new UniPosManager();
|
||||
|
||||
// 顶层映射:维度键(ResourceKey<Level>) -> 每维度的坐标缓存。
|
||||
// 缓存键为 BlockPos 的 long 值,值为 IUniPosOwner 的弱值(weak values)。
|
||||
// 使用维度键确保:同一维度的不同 Level 实例在服务端共享同一套锁;
|
||||
// 使用弱值确保:当所有者被 GC 时,锁能自动释放。
|
||||
private final ConcurrentMap<ResourceKey<Level>, Cache<Long, IUniPosOwner>> dimensionLocks;
|
||||
|
||||
private UniPosManager() {
|
||||
this.dimensionLocks = new ConcurrentHashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
获取或创建特定维度键对应的 Cache。
|
||||
Cache 使用 BlockPos 的 long 值作为键,IUniPosOwner 的弱值作为值。
|
||||
按 ResourceKey<Level> 分隔缓存,因此同一维度的不同 Level 实例在服务端共享同一套锁。
|
||||
computeIfAbsent 是原子操作,保证线程安全地获取或创建 Cache。
|
||||
*/
|
||||
private Cache<Long, IUniPosOwner> 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<Long, IUniPosOwner> 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<Long, IUniPosOwner> 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user