feat: 完善CommandNode,使其支持权限控制输出以及兼容管理器初始化方法控制触发完善
Some checks failed
Build and Release / build (push) Failing after 1h59m18s
Build and Release / release (push) Has been cancelled

This commit is contained in:
叁玖领域 2026-03-01 22:34:57 +08:00
parent fc28b6846b
commit 8ebdccc830
9 changed files with 724 additions and 52 deletions

View File

@ -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

View File

@ -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")
}
```

View File

@ -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

View File

@ -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);
}

View File

@ -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()));

View File

@ -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);
}
/**

View File

@ -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);

View File

@ -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"));
}

View 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"可确保LevelBlockPos对在同一时间只能被一个对象 "拥有"
* 一次只能被一个对象 "拥有"
*
* <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;
});
}
}
}