更新内容

1. 命令帮助菜单
2. SimpleLanguageProvider 构造器扩展 支持多ILangKeyValueCollection数组参数
This commit is contained in:
叁玖领域 2025-12-08 00:39:53 +08:00
parent 19463edd61
commit 2b0a14d0e9
31 changed files with 1628 additions and 149 deletions

View File

@ -103,8 +103,34 @@ obfuscation {
}
dependencies {
//
implementation 'org.joml:joml:1.10.5'
//
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.3'
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.9.3'
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.9.3'
//
testImplementation 'org.assertj:assertj-core:3.24.2'
// Mock库
testImplementation 'org.mockito:mockito-core:5.3.1'
testImplementation 'org.mockito:mockito-junit-jupiter:5.3.1'
//
testImplementation 'org.hamcrest:hamcrest:2.2'
}
test {
useJUnitPlatform()
//
testLogging {
events "passed", "skipped", "failed"
showStandardStreams = true
}
//
classpath = sourceSets.test.runtimeClasspath
}
// This block of code expands all declared replace properties in the specified resource targets.

View File

@ -1,2 +1,2 @@
// 1.20.1 2025-11-22T23:38:13.2517748 Languages: zh_tw
84dba66c4c768fd7754eaabe990c96f14a30c1df assets/lib39/lang/zh_tw.json
// 1.20.1 2025-12-08T00:17:05.2366865 Languages: zh_tw
2167da412113ad2aa18c3e8a9a53480f055e7661 assets/lib39/lang/zh_tw.json

View File

@ -1,2 +1,2 @@
// 1.20.1 2025-11-22T23:38:13.249775 Languages: zh_cn
48655f133966c9a854c57b560d070850af5a289f assets/lib39/lang/zh_cn.json
// 1.20.1 2025-12-08T00:17:05.2366865 Languages: zh_cn
f8dfabafd006b9552df1398d234eb8d23913b8e5 assets/lib39/lang/zh_cn.json

View File

@ -1,2 +1,2 @@
// 1.20.1 2025-11-22T23:38:13.2517748 Languages: lzh
a12c11d89d484a0e4f193ebd11b63722b8c501df assets/lib39/lang/lzh.json
// 1.20.1 2025-12-08T00:17:05.2366865 Languages: lzh
e098d6e171f79cbb5e41a51547abcad520f70edb assets/lib39/lang/lzh.json

View File

@ -1,2 +1,2 @@
// 1.20.1 2025-11-22T23:38:13.2507757 Languages: en_us
65df86271a054138e168311be826408455b3c33a assets/lib39/lang/en_us.json
// 1.20.1 2025-12-08T00:17:05.2366865 Languages: en_us
96726aafcb2b56b6610f7226299b4442132b8f22 assets/lib39/lang/en_us.json

View File

@ -1,4 +1,14 @@
{
"commands.lib39.help.basic.help": "Show Help Info",
"commands.lib39.help.command_not_found": "Command not found: %s",
"commands.lib39.help.header": "===== %s =====",
"commands.lib39.help.no_entries": "No help entries available",
"commands.lib39.help.node.expand": "%d subcommands collapsed",
"commands.lib39.help.node.toggle.collapse": "Collapse",
"commands.lib39.help.node.toggle.expand": "Expand",
"commands.lib39.help.page_info": "Page %d of %d",
"commands.lib39.help.subcommands_title": "Subcommands:",
"commands.lib39.help.toggle_failed": "Toggle Failed: No Hash Cached",
"item.lib39.fabric": "Fabric",
"item.lib39.forge": "Forge",
"item.lib39.neoforge": "NeoForge"

View File

@ -1,4 +1,14 @@
{
"commands.lib39.help.basic.help": "示助之訊",
"commands.lib39.help.command_not_found": "令未見之: %s",
"commands.lib39.help.header": " %s ",
"commands.lib39.help.no_entries": "尚無助之目錄",
"commands.lib39.help.node.expand": "%d 子令已收",
"commands.lib39.help.node.toggle.collapse": "收",
"commands.lib39.help.node.toggle.expand": "展",
"commands.lib39.help.page_info": "第 %d 卷,凡 %d 卷",
"commands.lib39.help.subcommands_title": "子令:",
"commands.lib39.help.toggle_failed": "變更未果: 無貯存之哈希",
"item.lib39.fabric": "織",
"item.lib39.forge": "砧",
"item.lib39.neoforge": "狸"

View File

@ -1,4 +1,14 @@
{
"commands.lib39.help.basic.help": "显示帮助信息",
"commands.lib39.help.command_not_found": "命令不存在: %s",
"commands.lib39.help.header": "===== %s 命令帮助 =====",
"commands.lib39.help.no_entries": "暂无帮助条目",
"commands.lib39.help.node.expand": "%d 个子命令已折叠",
"commands.lib39.help.node.toggle.collapse": "折叠",
"commands.lib39.help.node.toggle.expand": "展开",
"commands.lib39.help.page_info": "第 %d 页,共 %d 页",
"commands.lib39.help.subcommands_title": "子命令:",
"commands.lib39.help.toggle_failed": "切换失败: 无缓存Hash",
"item.lib39.fabric": "织布",
"item.lib39.forge": "铁砧",
"item.lib39.neoforge": "小狐狸"

View File

@ -1,4 +1,14 @@
{
"commands.lib39.help.basic.help": "顯示幫助信息",
"commands.lib39.help.command_not_found": "指令不存在: %s",
"commands.lib39.help.header": "===== %s 命令幫助 =====",
"commands.lib39.help.no_entries": "暫無幫助條目",
"commands.lib39.help.node.expand": "%d 個子指令已折疊",
"commands.lib39.help.node.toggle.collapse": "折疊",
"commands.lib39.help.node.toggle.expand": "展開",
"commands.lib39.help.page_info": "第 %d 頁,共 %d 頁",
"commands.lib39.help.subcommands_title": "子指令:",
"commands.lib39.help.toggle_failed": "切換失敗: 無緩存Hash",
"item.lib39.fabric": "織布",
"item.lib39.forge": "铁砧",
"item.lib39.neoforge": "狐狸"

View File

@ -84,7 +84,7 @@ public class Lib39 {
*
* @return the boolean
*/
static boolean shouldRegisterExamples() {
public static boolean shouldRegisterExamples() {
return !FMLEnvironment.production || Boolean.getBoolean(ENABLE_EXAMPLES_PROPERTY_KEY);
}

View File

@ -0,0 +1,66 @@
package top.r3944realms.lib39.api.event;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import net.minecraft.commands.CommandBuildContext;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.eventbus.api.Event;
import top.r3944realms.lib39.core.command.CommandNode;
import top.r3944realms.lib39.core.command.CommandPath;
import top.r3944realms.lib39.core.command.ICommandHelpManager;
import top.r3944realms.lib39.core.command.ParameterBuilder;
public class RegisterCommandHelpEvent extends Event {
private final LiteralArgumentBuilder<CommandSourceStack> builder;
private final ICommandHelpManager helpManager;
private final CommandBuildContext context;
public RegisterCommandHelpEvent(LiteralArgumentBuilder<CommandSourceStack> builder, ICommandHelpManager helpManager, CommandBuildContext source) {
this.builder = builder;
this.helpManager = helpManager;
this.context = source;
}
public ResourceLocation getID() {
return helpManager.getID();
}
public void addChild(LiteralArgumentBuilder<CommandSourceStack> child) {
this.builder.then(child);
}
public CommandBuildContext getContext(){
return this.context;
}
public LiteralArgumentBuilder<CommandSourceStack> getTree(){
return this.builder;
}
/**
* 注册命令帮助信息
* @param CommandNode 命令节点
* @param description 命令描述
*/
public void registerHelp(CommandNode CommandNode, MutableComponent description) {
this.helpManager.registerCommandHelp(CommandNode, description);
}
/**
* 注册命令帮助信息
* @param CommandNode 命令节点
* @param descriptionKey 命令描述的语言键
*/
public void registerHelp(CommandNode CommandNode, String descriptionKey) {
this.helpManager.registerCommandHelp(CommandNode, descriptionKey);
}
/**
* 注册命令参数
* @param commandPath 命令节点
* @param parametersBuilder 参数列表构造器
*/
public void registerParameters(CommandPath commandPath, ParameterBuilder parametersBuilder) {
this.helpManager.registerCommandParameters(commandPath, parametersBuilder);
}
}

View File

@ -0,0 +1,22 @@
package top.r3944realms.lib39.base.command;
import net.minecraft.resources.ResourceLocation;
import top.r3944realms.lib39.Lib39;
import top.r3944realms.lib39.core.command.SimpleCommandHelpManager;
public class Lib39CommandHelpManager extends SimpleCommandHelpManager {
public static volatile Lib39CommandHelpManager INSTANCE = new Lib39CommandHelpManager();
ResourceLocation ID = Lib39.rl("command_helper");
public Lib39CommandHelpManager() {
initialize();
}
@Override
public ResourceLocation getID() {
return ID;
}
@Override
public String getHeadKey() {
return "lib39";
}
}

View File

@ -0,0 +1,17 @@
package top.r3944realms.lib39.base.command;
import net.minecraftforge.event.RegisterCommandsEvent;
import org.jetbrains.annotations.NotNull;
import top.r3944realms.lib39.core.command.ICommandHelpManager;
import top.r3944realms.lib39.core.command.SimpleHelpCommand;
public class Lib39HelpCommand extends SimpleHelpCommand {
public Lib39HelpCommand(@NotNull RegisterCommandsEvent event) {
super(event);
}
@Override
public ICommandHelpManager getCommandHelpManager() {
return Lib39CommandHelpManager.INSTANCE;
}
}

View File

@ -1,23 +1,21 @@
package top.r3944realms.lib39.example.datagen;
package top.r3944realms.lib39.base.datagen;
import net.minecraft.data.DataProvider;
import net.minecraftforge.data.event.GatherDataEvent;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import top.r3944realms.lib39.Lib39;
import top.r3944realms.lib39.base.datagen.provider.ExItemModelProvider;
import top.r3944realms.lib39.base.datagen.value.Lib39LangKey;
import top.r3944realms.lib39.datagen.provider.SimpleLanguageProvider;
import top.r3944realms.lib39.datagen.value.McLocale;
import top.r3944realms.lib39.example.datagen.data.ExLib39LangKeys;
import top.r3944realms.lib39.example.datagen.provider.ExItemModelProvider;
/**
* The type Ex lib 39 data gen event.
*/
public class EXLib39DataGenEvent {
public class Lib39BaseDataGenEvent {
/**
* The Logger.
*/
static Logger logger = LoggerFactory.getLogger(EXLib39DataGenEvent.class);
static Logger logger = LoggerFactory.getLogger(Lib39BaseDataGenEvent.class);
/**
* Gather data.
@ -30,16 +28,17 @@ public class EXLib39DataGenEvent {
LanguageGenerator(event, McLocale.ZH_CN);
LanguageGenerator(event, McLocale.ZH_TW);
LanguageGenerator(event, McLocale.LZH);
ModelDataGenerate(event);
if (Lib39.shouldRegisterExamples()) {
ModelDataGenerate(event);
}
}
private static void LanguageGenerator(GatherDataEvent event, McLocale language) {
private static void LanguageGenerator(@NotNull GatherDataEvent event, McLocale language) {
event.getGenerator().addProvider(
event.includeClient(),
(DataProvider.Factory<SimpleLanguageProvider>) pOutput -> new SimpleLanguageProvider(pOutput, Lib39.MOD_ID ,language ,ExLib39LangKeys.INSTANCE)
(DataProvider.Factory<SimpleLanguageProvider>) pOutput -> new SimpleLanguageProvider(pOutput, Lib39.MOD_ID ,language , Lib39LangKey.INSTANCE)
);
}
private static void ModelDataGenerate(GatherDataEvent event) {
private static void ModelDataGenerate(@NotNull GatherDataEvent event) {
event.getGenerator().addProvider(
event.includeClient(),
(DataProvider.Factory<ExItemModelProvider>) pOutput -> new ExItemModelProvider(pOutput, event.getExistingFileHelper())

View File

@ -13,7 +13,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.lib39.example.datagen.provider;
package top.r3944realms.lib39.base.datagen.provider;
import net.minecraft.data.PackOutput;
import net.minecraft.resources.ResourceLocation;
@ -22,9 +22,10 @@ import net.minecraftforge.client.model.generators.ItemModelProvider;
import net.minecraftforge.common.data.ExistingFileHelper;
import net.minecraftforge.registries.ForgeRegistries;
import top.r3944realms.lib39.Lib39;
import top.r3944realms.lib39.base.datagen.Lib39BaseDataGenEvent;
import top.r3944realms.lib39.base.datagen.value.Lib39LangKey;
import top.r3944realms.lib39.datagen.value.LangKeyValue;
import top.r3944realms.lib39.datagen.value.ModPartEnum;
import top.r3944realms.lib39.example.datagen.data.ExLib39LangKeys;
import java.util.ArrayList;
import java.util.List;
@ -61,7 +62,7 @@ public class ExItemModelProvider extends ItemModelProvider {
DefaultModItemModelRegister();
}
private void init() {
for(LangKeyValue obj : ExLib39LangKeys.INSTANCE.getValues()) {
for(LangKeyValue obj : Lib39LangKey.INSTANCE.getValues()) {
if(!(obj.isDefault() && obj.getMPE().equals(ModPartEnum.ITEM))) continue;
objectList.add(obj.getItem());
}

View File

@ -0,0 +1,191 @@
package top.r3944realms.lib39.base.datagen.value;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.Unmodifiable;
import top.r3944realms.lib39.Lib39;
import top.r3944realms.lib39.datagen.value.ILangKeyValueCollection;
import top.r3944realms.lib39.datagen.value.LangKeyValue;
import top.r3944realms.lib39.datagen.value.ModPartEnum;
import top.r3944realms.lib39.example.core.register.ExLib39Items;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public enum Lib39LangKey implements ILangKeyValueCollection {
INSTANCE;
Lib39LangKey() {
initLangKeyValues();
}
public static final class Message {
private static final Set<LangKeyValue> items = new HashSet<>();
public static final LangKeyValue HELP_HEADER = addAndRet(
LangKeyValue.ofKey(
"commands.lib39.help.header", ModPartEnum.MESSAGE,
"===== %s =====",
"===== %s 命令帮助 =====",
"===== %s 命令幫助 =====",
" %s ", // 文言文表示分隔線
true
)
);
public static final LangKeyValue HELP_PAGE_INFO = addAndRet(
LangKeyValue.ofKey(
"commands.lib39.help.page_info", ModPartEnum.MESSAGE,
"Page %d of %d",
"第 %d 页,共 %d 页",
"第 %d 頁,共 %d 頁",
"第 %d 卷,凡 %d 卷", // 文言文表示頁
true
)
);
public static final LangKeyValue HELP_NO_ENTRIES = addAndRet(
LangKeyValue.ofKey(
"commands.lib39.help.no_entries", ModPartEnum.MESSAGE,
"No help entries available",
"暂无帮助条目",
"暫無幫助條目",
"尚無助之目錄", // 文言文尚無幫助的目錄
true
)
);
public static final LangKeyValue HELP_TOGGLE_FAILED = addAndRet(
LangKeyValue.ofKey(
"commands.lib39.help.toggle_failed", ModPartEnum.MESSAGE,
"Toggle Failed: No Hash Cached",
"切换失败: 无缓存Hash",
"切換失敗: 無緩存Hash",
"變更未果: 無貯存之哈希", // 文言文變更沒有成功沒有貯存的哈希
true
)
);
public static final LangKeyValue HELP_COMMAND_NOT_FOUND = addAndRet(
LangKeyValue.ofKey(
"commands.lib39.help.command_not_found", ModPartEnum.MESSAGE,
"Command not found: %s",
"命令不存在: %s",
"指令不存在: %s",
"令未見之: %s", // 文言文命令沒有見到
true
)
);
public static final LangKeyValue HELP_SUBCOMMANDS_TITLE = addAndRet(
LangKeyValue.ofKey(
"commands.lib39.help.subcommands_title", ModPartEnum.MESSAGE,
"Subcommands:",
"子命令:",
"子指令:",
"子令:", // 文言文子命令
true
)
);
public static final LangKeyValue HELP_NODE_EXPAND = addAndRet(
LangKeyValue.ofKey(
"commands.lib39.help.node.expand", ModPartEnum.MESSAGE,
"%d subcommands collapsed",
"%d 个子命令已折叠",
"%d 個子指令已折疊",
"%d 子令已收", // 文言文子命令已經收起
true
)
);
public static final LangKeyValue HELP_NODE_TOGGLE_EXPAND = addAndRet(
LangKeyValue.ofKey(
"commands.lib39.help.node.toggle.expand", ModPartEnum.MESSAGE,
"Expand",
"展开",
"展開",
"", // 文言文展開
true
)
);
public static final LangKeyValue HELP_NODE_TOGGLE_COLLAPSE = addAndRet(
LangKeyValue.ofKey(
"commands.lib39.help.node.toggle.collapse", ModPartEnum.MESSAGE,
"Collapse",
"折叠",
"折疊",
"", // 文言文收起
true
)
);
public static final LangKeyValue BASIC_HELP = addAndRet(
LangKeyValue.ofKey(
"commands.lib39.help.basic.help", ModPartEnum.MESSAGE,
"Show Help Info",
"显示帮助信息",
"顯示幫助信息",
"示助之訊", // 文言文顯示幫助的訊息
true
)
);
private static LangKeyValue addAndRet(LangKeyValue item) {
items.add(item);
return item;
}
public static Set<LangKeyValue> getItems() {
return items;
}
}
/**
* The Lang key values.
*/
final List<LangKeyValue> langKeyValues = new ArrayList<>();
/**
* Init lang key values.
*/
public void initLangKeyValues() {
Message.getItems().forEach(this::addLang);
if (Lib39.shouldRegisterExamples()) {
addLang(LangKeyValue.ofSupplier(
ExLib39Items.FABRIC, ModPartEnum.ITEM,
"Fabric", "织布", "織布", "", true
));
addLang(LangKeyValue.ofSupplier(
ExLib39Items.NEOFORGE, ModPartEnum.ITEM,
"NeoForge", "小狐狸", "狐狸", "", true
));
addLang(LangKeyValue.ofSupplier(
ExLib39Items.FORGE, ModPartEnum.ITEM,
"Forge", "铁砧", "铁砧", "", true
));
}
}
/**
* Add lang.
*
* @param keyValue the key value
*/
public void addLang(LangKeyValue keyValue) {
langKeyValues.add(keyValue);
}
/**
* Clear.
*/
public void clear() {
langKeyValues.clear();
}
@Contract(pure = true)
@Override
public @Unmodifiable List<LangKeyValue> getValues() {
return List.copyOf(langKeyValues);
}
}

View File

@ -0,0 +1,140 @@
package top.r3944realms.lib39.core.command;
import net.minecraft.network.chat.MutableComponent;
import java.util.*;
public class CommandNode {
private boolean isRoot = false;
private int hashCache = 0;
private boolean hashValid = false;
private final ICommandHelpManager helpManager;
private final String name;
private final MutableComponent description;
private final Map<String, CommandNode> children;
// 参数列表用于存储命令参数
private final List<Parameter> parameters = new ArrayList<>();
// 展开/闭合状态
private boolean expanded = true;
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();
}
public CommandNode(ICommandHelpManager helpManager, String name, MutableComponent description) {
this.name = name;
this.helpManager = helpManager;
this.description = description;
this.children = new TreeMap<>();
invalidateHash();
}
public void addChild(CommandNode child) {
children.put(child.name, child);
invalidateHash();
}
public CommandNode getChild(String name) {
return children.get(name);
}
public String getName() {
return name;
}
public MutableComponent getDescription() {
return description;
}
public Collection<CommandNode> getChildren() {
return children.values();
}
// 添加参数
public void addParameter(String parameter, boolean required) {
parameters.add(new Parameter(parameter, required));
invalidateHash();
}
// 获取参数列表
public List<Parameter> getParameters() {
return parameters;
}
// 获取展开状态
public boolean isExpanded() {
return expanded;
}
// 设置展开状态
public void setExpanded(boolean expanded) {
this.expanded = expanded;
}
// 切换展开状态
public void toggleExpanded() {
this.expanded = !this.expanded;
}
@Override
public int hashCode() {
return computeHash();
}
// 计算节点的Hash值
public int computeHash() {
if (!hashValid) {
if (hashCache != 0) {
helpManager.getCache().remove(hashCache);
}
int result = 31;
result = 31 * result + name.hashCode();
result = 31 * result + description.getString().hashCode();
// 计算参数的Hash
for (Parameter param : parameters) {
result = 31 * result + param.name().hashCode();
result = 31 * result + (param.required() ? 1 : 0);
}
// 计算子节点的Hash
for (CommandNode child : children.values()) {
result = 31 * result + child.computeHash();
}
if (!isRoot && children.size() > 1) {
expanded = false;
}
hashCache = result;
helpManager.getCache().put(result, this);
hashValid = true;
}
return hashCache;
}
// 使Hash缓存失效
private void invalidateHash() {
hashValid = false;
computeHash();
}
//递归构建完整路径
public String getFullPath() {
StringBuilder sb = new StringBuilder();
if (sb.isEmpty()) {
sb.append(name);
} else {
sb.insert(0, name + " ");
}
return sb.toString();
}
}

View File

@ -0,0 +1,86 @@
package top.r3944realms.lib39.core.command;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* 命令路径构建器 - 提供编译时类型安全
*/
public final class CommandPath {
private final List<String> segments;
private final String fullPath;
private CommandPath(List<String> segments) {
this.segments = List.copyOf(segments);
this.fullPath = String.join(" ", segments);
}
@Contract("_ -> new")
public static @NotNull CommandPath of(String... segments) {
validateSegments(segments);
return new CommandPath(List.of(segments));
}
@Contract("_ -> new")
public static @NotNull CommandPath fromString(@NotNull String path) {
return of(path.split(" "));
}
@Contract("_ -> new")
public @NotNull CommandPath then(String... subSegments) {
validateSegments(subSegments);
List<String> newSegments = new ArrayList<>(this.segments);
newSegments.addAll(List.of(subSegments));
return new CommandPath(newSegments);
}
public Optional<CommandPath> parent() {
if (segments.size() <= 1) {
return Optional.empty();
}
return Optional.of(new CommandPath(segments.subList(0, segments.size() - 1)));
}
public String lastSegment() {
return segments.isEmpty() ? "" : segments.get(segments.size() - 1);
}
public String @NotNull [] segments() {
return segments.toArray(new String[0]);
}
public String fullPath() {
return fullPath;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CommandPath that = (CommandPath) o;
return Objects.equals(fullPath, that.fullPath);
}
@Override
public int hashCode() {
return fullPath.hashCode();
}
@Override
public String toString() {
return fullPath;
}
private static void validateSegments(String @NotNull [] segments) {
for (String segment : segments) {
if (segment == null || segment.isEmpty() || segment.contains(" ")) {
throw new IllegalArgumentException("Invalid command segment: " + segment);
}
}
}
}

View File

@ -0,0 +1,296 @@
package top.r3944realms.lib39.core.command;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.ClickEvent;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.network.chat.Style;
import net.minecraft.resources.ResourceLocation;
import org.jetbrains.annotations.NotNull;
import top.r3944realms.lib39.base.datagen.value.Lib39LangKey;
import java.util.*;
public interface ICommandHelpManager {
String NEWLINE = "\n";
ResourceLocation getID();
String getHeadKey();
default String getCommandHead() {
return getID().getNamespace();
}
default CommandNode init() {
CommandNode root;
root = new CommandNode(this, getCommandHead(), Component.translatable(Lib39LangKey.Message.HELP_HEADER.getKey(), Component.translatable(getHeadKey())), true);
root.addChild(new CommandNode(this, "help", Component.translatable(Lib39LangKey.Message.BASIC_HELP.getKey())));
return root;
}
Map<Integer, CommandNode> getCache();
CommandNode getRootNode();
default void registerCommandHelp(@NotNull CommandNode commandNode, MutableComponent description) {
registerCommandHelp(commandNode.getFullPath(), description);
}
default void registerCommandHelp(@NotNull CommandNode commandNode, String descriptionKey) {
registerCommandHelp(commandNode.getFullPath(), Component.translatable(descriptionKey));
}
default void registerCommandHelp(@NotNull CommandPath commandPath) {
registerCommandHelp(commandPath.fullPath(), Component.literal(""));
}
default void registerCommandParameters(@NotNull CommandPath commandPath, @NotNull ParameterBuilder parameters) {
registerCommandParameters(commandPath.fullPath(), parameters.build());
}
private void registerCommandHelp(@NotNull String commandPath, MutableComponent description) {
String[] pathParts = commandPath.split(" ");
CommandNode currentNode = getRootNode();
int startIndex = pathParts[0].equals(getRootNode().getName()) ? 1 : 0;
for (int i = startIndex; i < pathParts.length; i++) {
String part = pathParts[i];
CommandNode child = currentNode.getChild(part);
if (child == null) {
MutableComponent nodeDescription = (i == pathParts.length - 1) ? description : Component.literal("");
child = new CommandNode(this, part, nodeDescription);
currentNode.addChild(child);
}
currentNode = child;
}
}
/**
* 根据路径查找节点
*/
default Optional<CommandNode> findNode(CommandPath path) {
CommandNode currentNode = getRootNode();
String[] segments = path.segments();
// 跳过根节点如果路径包含
int startIndex = segments[0].equals(getRootNode().getName()) ? 1 : 0;
for (int i = startIndex; i < segments.length; i++) {
currentNode = currentNode.getChild(segments[i]);
if (currentNode == null) {
return Optional.empty();
}
}
return Optional.of(currentNode);
}
/**
* 检查命令是否存在
*/
default boolean hasCommand(CommandPath path) {
return findNode(path).isPresent();
}
private void registerCommandHelp(String commandPath, String descriptionKey) {
registerCommandHelp(commandPath, Component.translatable(descriptionKey));
}
private void registerCommandHelp(String commandPath) {
registerCommandHelp(commandPath, Component.literal(""));
}
/**
* 注册命令参数支持单个参数可选标记使用*前缀表示必选参数
* @param commandPath 命令路径 "fpsm tacz dummy"
* @param parameters 参数列表 "*requiredParam", "optionalParam"
*/
private void registerCommandParameters(@NotNull String commandPath, String... parameters) {
String[] pathParts = commandPath.split(" ");
CommandNode currentNode = getRootNode();
// 遍历命令路径找到目标节点
int startIndex = pathParts[0].equals(getRootNode().getName()) ? 1 : 0;
for (int i = startIndex; i < pathParts.length; i++) {
String part = pathParts[i];
CommandNode child = currentNode.getChild(part);
if (child == null) {
// 如果节点不存在创建空描述节点
child = new CommandNode(this, part, Component.literal(""));
currentNode.addChild(child);
}
currentNode = child;
}
// 添加参数处理可选标记
for (String param : parameters) {
boolean required = param.startsWith("*");
String paramName = required ? param.substring(1) : param;
currentNode.addParameter(paramName, required);
}
}
/**
* 动态添加子指令到指定命令路径
* @param commandPath 命令路径 "fpsm map modify"
* @param childName 子指令名称
* @param description 子指令描述
* @return 是否添加成功
*/
default boolean addChildCommand(@NotNull String commandPath, String childName, MutableComponent description) {
String[] pathParts = commandPath.split(" ");
CommandNode currentNode = getRootNode();
// 遍历命令路径找到目标父节点
for (String part : pathParts) {
if (!part.equals(currentNode.getName())) {
CommandNode child = currentNode.getChild(part);
if (child == null) {
// 路径不存在创建中间节点
child = new CommandNode(this, part, Component.literal(""));
currentNode.addChild(child);
}
currentNode = child;
}
}
// 添加子指令
CommandNode childNode = new CommandNode(this, childName, description);
currentNode.addChild(childNode);
return true;
}
/**
* 构建单个命令节点的显示格式
* @param node 当前命令节点
* @param indent 当前缩进
* @param isRoot 是否为根节点
* @return 格式化后的命令节点组件
*/
private @NotNull MutableComponent buildCommandLine(CommandNode node, String indent, boolean isRoot) {
if (isRoot) {
// 根节点特殊处理
return Component.literal("/" + node.getName()).withStyle(ChatFormatting.AQUA);
} else {
// 非根节点显示命令和描述
MutableComponent prefix = Component.literal(indent + "└─ ").withStyle(ChatFormatting.GRAY);
MutableComponent commandName = Component.literal(node.getName()).withStyle(ChatFormatting.DARK_AQUA);
MutableComponent displayLine = prefix.append(commandName);
// 添加参数显示
if (!node.getParameters().isEmpty()) {
for (Parameter param : node.getParameters()) {
if (param.required()) {
displayLine.append(Component.literal(" <").withStyle(ChatFormatting.GRAY))
.append(Component.literal(param.name()).withStyle(ChatFormatting.WHITE))
.append(Component.literal("> ").withStyle(ChatFormatting.GRAY));
} else {
displayLine.append(Component.literal(" [").withStyle(ChatFormatting.GRAY))
.append(Component.literal(param.name()).withStyle(ChatFormatting.WHITE))
.append(Component.literal("] ").withStyle(ChatFormatting.GRAY));
}
}
}
// 添加分隔符和描述
displayLine.append(Component.literal(" - ").withStyle(ChatFormatting.DARK_GRAY))
.append(node.getDescription().withStyle(ChatFormatting.GRAY));
if (node.getChildren().size() > 1) {
String toggleKey = node.isExpanded() ? Lib39LangKey.Message.HELP_NODE_TOGGLE_COLLAPSE.getKey() : Lib39LangKey.Message.HELP_NODE_TOGGLE_EXPAND.getKey();
displayLine.append(Component.literal(" [").withStyle(ChatFormatting.GRAY))
.append(Component.translatable(toggleKey).withStyle(ChatFormatting.YELLOW))
.append(Component.literal("]").withStyle(ChatFormatting.GRAY));
}
return displayLine.withStyle(Style.EMPTY.withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/"+ getCommandHead() +" help toggle " + node.hashCode())));
}
}
/**
* 递归构建命令树的字符串表示
* @param node 当前命令节点
* @param indent 当前缩进
* @param commandPath 当前命令路径
* @param result 结果列表
*/
private void buildCommandTreeString(CommandNode node, @NotNull String indent, String commandPath, @NotNull List<MutableComponent> result) {
boolean isRoot = indent.isEmpty();
MutableComponent commandLine = buildCommandLine(node, indent, isRoot);
result.add(commandLine.append(NEWLINE));
// 递归处理子节点
String childIndent = indent + "| ";
if (node.isExpanded()) {
String fullCommandPath = commandPath.isEmpty() ? "/" + node.getName() : commandPath + " " + node.getName();
for (CommandNode child : node.getChildren()) {
buildCommandTreeString(child, childIndent, fullCommandPath, result);
}
} else {
result.add(Component.literal(indent + "| " + "└─ ").withStyle(ChatFormatting.GRAY)
.append(Component.translatable(Lib39LangKey.Message.HELP_NODE_EXPAND.getKey(), node.getChildren().size())
.withStyle(ChatFormatting.GRAY))
.append(NEWLINE));
}
}
/**
* 获取命令树的字符串表示
* @return 命令树列表
*/
default List<MutableComponent> getCommandTree() {
List<MutableComponent> result = new ArrayList<>();
buildCommandTreeString(getRootNode(), "", "", result);
return result;
}
/**
* 切换指定节点的展开/闭合状态
* @param hashCode 节点哈希值
* @return 是否成功切换
*/
default boolean toggleNodeExpanded(int hashCode) {
CommandNode currentNode = getCache().getOrDefault(hashCode, null);
if (currentNode == null) {
return false;
}
if (currentNode.getChildren().size() <= 1) {
return false;
}
currentNode.toggleExpanded();
return true;
}
/**
* 构建帮助消息
* @param header 帮助头部
* @param entries 帮助条目列表
*/
default MutableComponent buildHelpMessage(@NotNull Component header, @NotNull List<MutableComponent> entries) {
MutableComponent helpMessage = Component.empty();
// 添加头部
helpMessage.append(header.copy()).append(NEWLINE);
// 添加分隔线
helpMessage.append(Component.literal("\n"));
// 添加当前页的帮助内容
if (entries.isEmpty()) {
helpMessage.append(Component.translatable(Lib39LangKey.Message.HELP_NO_ENTRIES.getKey())).append(NEWLINE);
} else {
for (MutableComponent entry : entries) {
helpMessage.append(entry);
}
}
return helpMessage;
}
default MutableComponent buildCommandTreeHelp() {
List<MutableComponent> commandTree = getCommandTree();
return buildHelpMessage(Component.translatable(Lib39LangKey.Message.HELP_HEADER.getKey(), Component.translatable(getHeadKey())), commandTree);
}
}

View File

@ -0,0 +1,77 @@
package top.r3944realms.lib39.core.command;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import net.minecraft.commands.CommandBuildContext;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent;
import net.minecraftforge.common.MinecraftForge;
import org.jetbrains.annotations.NotNull;
import top.r3944realms.lib39.api.event.RegisterCommandHelpEvent;
import top.r3944realms.lib39.base.datagen.value.Lib39LangKey;
import javax.annotation.Nullable;
public interface IHelpCommand {
default boolean shouldShowToggleFailed() {
return false;
}
@Nullable
default LiteralArgumentBuilder<CommandSourceStack> getHelpHead() {
return null;
}
ICommandHelpManager getCommandHelpManager();
default LiteralArgumentBuilder<CommandSourceStack> buildCommand(CommandDispatcher<CommandSourceStack> dispatcher, CommandBuildContext context) {
LiteralArgumentBuilder<CommandSourceStack> head = getHelpHead();
if (head == null) {
head = LiteralArgumentBuilder.literal(getCommandHelpManager().getID().getNamespace());
}
LiteralArgumentBuilder<CommandSourceStack> tree = head.requires(this::requestPermission)
.then(Commands.literal("help").executes(this::handleHelp)
.then(Commands.literal("toggle")
.then(Commands.argument("hash", IntegerArgumentType.integer()).executes(this::handleHelpToggle))
));
RegisterCommandHelpEvent registerHelpCommandEvent = new RegisterCommandHelpEvent(tree, getCommandHelpManager(), context);
MinecraftForge.EVENT_BUS.post(registerHelpCommandEvent);
dispatcher.register(head);
return head;
}
default boolean requestPermission(CommandSourceStack context) {
return true;
}
default int handleHelp(@NotNull CommandContext<CommandSourceStack> context) {
ICommandHelpManager commandHelpManager = getCommandHelpManager();
MutableComponent helpMessage = commandHelpManager.buildCommandTreeHelp();
sendSuccess(context.getSource(), helpMessage);
return 1;
}
default int handleHelpToggle(@NotNull CommandContext<CommandSourceStack> context) {
int hash = IntegerArgumentType.getInteger(context, "hash");
ICommandHelpManager commandHelpManager = getCommandHelpManager();
boolean success = commandHelpManager.toggleNodeExpanded(hash);
if (success) {
MutableComponent helpMessage = Component.literal("\n".repeat(2)).append(commandHelpManager.buildCommandTreeHelp());
sendSuccess(context.getSource(), helpMessage);
} else if (shouldShowToggleFailed()) {
sendFailure(context.getSource(), Component.translatable(Lib39LangKey.Message.HELP_TOGGLE_FAILED.getKey()));
}
return 1;
}
static void sendSuccess(@NotNull CommandSourceStack source, Component key) {
source.sendSuccess(() -> key, true);
}
static void sendFailure(@NotNull CommandSourceStack source, Component key) {
source.sendFailure(key);
}
}

View File

@ -0,0 +1,3 @@
package top.r3944realms.lib39.core.command;
public record Parameter(String name, boolean required) {}

View File

@ -0,0 +1,31 @@
package top.r3944realms.lib39.core.command;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
public class ParameterBuilder {
private final List<String> parameters = new ArrayList<>();
public ParameterBuilder required(String name) {
parameters.add("*" + name);
return this;
}
public ParameterBuilder optional(String name) {
parameters.add(name);
return this;
}
public String[] build() {
return parameters.toArray(new String[0]);
}
// 链式调用的便利方法
@Contract(" -> new")
public static @NotNull ParameterBuilder builder() {
return new ParameterBuilder();
}
}

View File

@ -0,0 +1,58 @@
package top.r3944realms.lib39.core.command;
import net.minecraft.resources.ResourceLocation;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
public abstract class SimpleCommandHelpManager implements ICommandHelpManager {
private CommandNode root;
private final Map<Integer, CommandNode> nodeCache = new HashMap<>();
public SimpleCommandHelpManager() {
//
}
/**
* 延遲初始化根節點
*/
public void initialize() {
if (root == null) {
// 現在子類的字段已經初始化完成
ResourceLocation id = getID();
if (id == null) {
throw new IllegalStateException("getID() must return non-null");
}
this.root = init();
}
}
@Override
public final @NotNull String getCommandHead() {
return getID().getNamespace();
}
@Override
public final Map<Integer, CommandNode> getCache() {
return nodeCache;
}
/**
* 獲取根節點如果未初始化則初始化
*/
@Override
public final CommandNode getRootNode() {
if (root == null) {
initialize();
}
return root;
}
/**
* 檢查是否已初始化
*/
public boolean isInitialized() {
return root != null;
}
}

View File

@ -0,0 +1,19 @@
package top.r3944realms.lib39.core.command;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import net.minecraft.commands.CommandBuildContext;
import net.minecraft.commands.CommandSourceStack;
import net.minecraftforge.event.RegisterCommandsEvent;
import org.jetbrains.annotations.NotNull;
public abstract class SimpleHelpCommand implements IHelpCommand {
protected final LiteralArgumentBuilder<CommandSourceStack> root;
public SimpleHelpCommand(@NotNull RegisterCommandsEvent event) {
root = buildCommand(event.getDispatcher(), event.getBuildContext());
}
public SimpleHelpCommand(CommandDispatcher<CommandSourceStack> dispatcher,
CommandBuildContext context) {
root = buildCommand(dispatcher, context);
}
}

View File

@ -8,7 +8,9 @@ import net.minecraft.world.item.CreativeModeTab;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.data.event.GatherDataEvent;
import net.minecraftforge.event.BuildCreativeModeTabContentsEvent;
import net.minecraftforge.event.RegisterCommandsEvent;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.event.entity.EntityJoinLevelEvent;
import net.minecraftforge.event.entity.EntityLeaveLevelEvent;
@ -21,6 +23,8 @@ import net.minecraftforge.registries.RegistryObject;
import top.r3944realms.lib39.Lib39;
import top.r3944realms.lib39.api.event.RegisterCompatEvent;
import top.r3944realms.lib39.api.event.SyncManagerRegisterEvent;
import top.r3944realms.lib39.base.datagen.Lib39BaseDataGenEvent;
import top.r3944realms.lib39.base.command.Lib39HelpCommand;
import top.r3944realms.lib39.core.compat.CompatManager;
import top.r3944realms.lib39.core.sync.ISyncData;
import top.r3944realms.lib39.core.sync.SyncData2Manager;
@ -149,6 +153,10 @@ public class CommonEventHandler {
}
}
}
@SubscribeEvent
public static void onRegisterCommand (RegisterCommandsEvent event) {
Lib39HelpCommand lib39HelpCommand = new Lib39HelpCommand(event);
}
}
/**
@ -184,6 +192,11 @@ public class CommonEventHandler {
event.enqueueWork(() -> {
IEventBus modBus = FMLJavaModLoadingContext.get().getModEventBus();
IEventBus gameBus = MinecraftForge.EVENT_BUS;
try {
Class.forName("top.r3944realms.lib39.base.command.Lib39CommandHelpManager");
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
compatManager = new CompatManager(modBus, gameBus);
MinecraftForge.EVENT_BUS.post(new RegisterCompatEvent(compatManager));
compatManager.initializeAll();
@ -219,6 +232,16 @@ public class CommonEventHandler {
}
}
/**
* Gather data.
*
* @param event the event
*/
@SubscribeEvent
public static void gatherData(GatherDataEvent event) {
Lib39BaseDataGenEvent.gatherData(event);
}
/**
* Gets item add map.
*

View File

@ -3,6 +3,7 @@ package top.r3944realms.lib39.datagen.provider;
import net.minecraft.data.PackOutput;
import net.minecraftforge.common.data.LanguageProvider;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import top.r3944realms.lib39.datagen.value.ILangKeyValue;
import top.r3944realms.lib39.datagen.value.ILangKeyValueCollection;
import top.r3944realms.lib39.datagen.value.McLocale;
@ -18,6 +19,8 @@ import java.util.Map;
public class SimpleLanguageProvider extends LanguageProvider {
private final McLocale language;
private final ILangKeyValueCollection langKeyValueCollection;
@Nullable
private ILangKeyValueCollection[] langKeyValueCollections;
private final Map<String, String> translationMap; // Better naming
private final List<String> orderedKeys; // Better naming than "objects"
@ -39,9 +42,40 @@ public class SimpleLanguageProvider extends LanguageProvider {
this.orderedKeys = new ArrayList<>();
initializeTranslations();
}
/**
* Instantiates a new Simple language provider.
*
* @param output the output
* @param modId the mod id
* @param language the language
* @param langKeyValueCollection the lang key value collection
*/
public SimpleLanguageProvider(PackOutput output, String modId,
@NotNull McLocale language,
ILangKeyValueCollection... langKeyValueCollection) {
super(output, modId, language.mcCode());
this.language = language;
this.langKeyValueCollection = null;
this.langKeyValueCollections = langKeyValueCollection;
this.translationMap = new HashMap<>();
this.orderedKeys = new ArrayList<>();
initializeTranslations();
}
private void initializeTranslations() {
for (ILangKeyValue langKeyValue : langKeyValueCollection.getValues()) {
if (langKeyValueCollection != null) {
addToTranslationMap(langKeyValueCollection);
} else if (langKeyValueCollections != null) {
for (ILangKeyValueCollection keyValueCollection : langKeyValueCollections) {
if (keyValueCollection != null) {
addToTranslationMap(keyValueCollection);
}
}
}
}
private void addToTranslationMap(ILangKeyValueCollection keyValueCollection) {
for (ILangKeyValue langKeyValue : keyValueCollection.getValues()) {
String key = langKeyValue.getKey();
String value = langKeyValue.getLang(language);

View File

@ -258,6 +258,30 @@ public class LangKeyValue implements ILangKeyValue {
.build();
}
/**
* Of supplier lang key value.
*
* @param supplier the supplier
* @param MPE the mpe
* @param US_EN the us en
* @param SIM_CN the sim cn
* @param TRA_CN the tra cn
* @param isDefault the is default
* @return the lang key value
*/
@Contract(value = "_, _, _, _, _, _ -> new", pure = true)
public static @NotNull LangKeyValue ofSupplier(Supplier<?> supplier, ModPartEnum MPE,
String US_EN, String SIM_CN, String TRA_CN, boolean isDefault) {
return builder()
.supplier(supplier)
.MPE(MPE)
.US_EN(US_EN)
.SIM_CN(SIM_CN)
.TRA_CN(TRA_CN)
.isDefault(isDefault)
.build();
}
/**
* Of supplier lang key value.
*
@ -308,30 +332,6 @@ public class LangKeyValue implements ILangKeyValue {
.build();
}
/**
* Of supplier lang key value.
*
* @param supplier the supplier
* @param MPE the mpe
* @param US_EN the us en
* @param SIM_CN the sim cn
* @param TRA_CN the tra cn
* @param isDefault the is default
* @return the lang key value
*/
@Contract(value = "_, _, _, _, _, _ -> new", pure = true)
public static @NotNull LangKeyValue ofSupplier(Supplier<?> supplier, ModPartEnum MPE,
String US_EN, String SIM_CN, String TRA_CN, Boolean isDefault) {
return builder()
.supplier(supplier)
.MPE(MPE)
.US_EN(US_EN)
.SIM_CN(SIM_CN)
.TRA_CN(TRA_CN)
.isDefault(isDefault)
.build();
}
/**
* Of key lang key value.
*
@ -354,6 +354,30 @@ public class LangKeyValue implements ILangKeyValue {
.build();
}
/**
* Of key lang key value.
*
* @param key the key
* @param MPE the mpe
* @param US_EN the us en
* @param SIM_CN the sim cn
* @param TRA_CN the tra cn
* @param isDefault the is default
* @return the lang key value
*/
@Contract(value = "_, _, _, _, _, _ -> new", pure = true)
public static @NotNull LangKeyValue ofKey(String key, ModPartEnum MPE,
String US_EN, String SIM_CN, String TRA_CN, boolean isDefault) {
return builder()
.key(key)
.MPE(MPE)
.US_EN(US_EN)
.SIM_CN(SIM_CN)
.TRA_CN(TRA_CN)
.isDefault(isDefault)
.build();
}
/**
* Of key lang key value.
*
@ -381,27 +405,30 @@ public class LangKeyValue implements ILangKeyValue {
/**
* Of key lang key value.
*
* @param key the key
* @param MPE the mpe
* @param US_EN the us en
* @param SIM_CN the sim cn
* @param TRA_CN the tra cn
* @param key the key
* @param MPE the mpe
* @param US_EN the us en
* @param SIM_CN the sim cn
* @param TRA_CN the tra cn
* @param LZH the lzh
* @param isDefault the is default
* @return the lang key value
*/
@Contract(value = "_, _, _, _, _, _ -> new", pure = true)
@Contract(value = "_, _, _, _, _, _, _ -> new", pure = true)
public static @NotNull LangKeyValue ofKey(String key, ModPartEnum MPE,
String US_EN, String SIM_CN, String TRA_CN, Boolean isDefault) {
String US_EN, String SIM_CN, String TRA_CN, String LZH, boolean isDefault) {
return builder()
.key(key)
.MPE(MPE)
.US_EN(US_EN)
.SIM_CN(SIM_CN)
.TRA_CN(TRA_CN)
.LZH(LZH)
.isDefault(isDefault)
.build();
}
@Override
public String getKey() {
return Objects.requireNonNullElseGet(key, () -> switch (MPE) {

View File

@ -2,7 +2,6 @@ package top.r3944realms.lib39.example.core.event;
import net.minecraft.world.entity.Entity;
import net.minecraftforge.common.capabilities.RegisterCapabilitiesEvent;
import net.minecraftforge.data.event.GatherDataEvent;
import net.minecraftforge.event.AttachCapabilitiesEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
@ -11,7 +10,6 @@ import top.r3944realms.lib39.core.sync.CachedSyncManager;
import top.r3944realms.lib39.example.content.capability.AbstractedTestSyncData;
import top.r3944realms.lib39.example.content.capability.ExCapabilityHandler;
import top.r3944realms.lib39.example.content.capability.TestSyncData;
import top.r3944realms.lib39.example.datagen.EXLib39DataGenEvent;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@ -87,14 +85,5 @@ public class ExCommonEventHandler {
ExCapabilityHandler.registerCapability(event);
}
/**
* Gather data.
*
* @param event the event
*/
@SubscribeEvent
public static void gatherData(GatherDataEvent event) {
EXLib39DataGenEvent.gatherData(event);
}
}
}

View File

@ -1,73 +0,0 @@
package top.r3944realms.lib39.example.datagen.data;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.Unmodifiable;
import top.r3944realms.lib39.datagen.value.ILangKeyValueCollection;
import top.r3944realms.lib39.datagen.value.LangKeyValue;
import top.r3944realms.lib39.datagen.value.ModPartEnum;
import top.r3944realms.lib39.example.core.register.ExLib39Items;
import java.util.ArrayList;
import java.util.List;
/**
* The enum Ex lib 39 lang keys.
*/
public enum ExLib39LangKeys implements ILangKeyValueCollection {
/**
* Instance ex lib 39 lang keys.
*/
INSTANCE;
ExLib39LangKeys() {
initLangKeyValues();
}
/**
* The Lang key values.
*/
final List<LangKeyValue> langKeyValues = new ArrayList<>();
/**
* Init lang key values.
*/
public void initLangKeyValues() {
addLang(LangKeyValue.ofSupplier(
ExLib39Items.FABRIC, ModPartEnum.ITEM,
"Fabric", "织布" , "織布" ,"" , true
));
addLang(LangKeyValue.ofSupplier(
ExLib39Items.NEOFORGE, ModPartEnum.ITEM,
"NeoForge", "小狐狸", "狐狸", "", true
));
addLang(LangKeyValue.ofSupplier(
ExLib39Items.FORGE, ModPartEnum.ITEM,
"Forge", "铁砧", "铁砧", "", true
));
}
/**
* Add lang.
*
* @param keyValue the key value
*/
public void addLang(LangKeyValue keyValue) {
langKeyValues.add(keyValue);
}
/**
* Clear.
*/
public void clear() {
langKeyValues.clear();
}
@Contract(pure = true)
@Override
public @Unmodifiable List<LangKeyValue> getValues() {
return List.copyOf(langKeyValues);
}
}

View File

@ -1,7 +0,0 @@
package top.r3944realms.lib39.util.command;
/**
* The type Command help helper.
*/
public class CommandHelpHelper {
}

View File

@ -0,0 +1,414 @@
package top.r3944realms.lib39;
import org.joml.*;
import org.joml.Math;
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.*;
import org.junit.jupiter.params.provider.*;
import static org.assertj.core.api.Assertions.*;
public class CoordinateTransformTest {
private static final float EPSILON = 0.0001f;
// ==================== 基础变换测试 ====================
@Test
void testWorldToLocalTransform() {
// 创建一个变换先缩放再旋转最后平移
Matrix4f transform = new Matrix4f()
.translate(10, 5, 0) // 平移
.rotateY((float) Math.PI / 2) // 绕Y轴旋转90度
.scale(2, 1, 1); // X轴缩放2倍
// 世界坐标 (0,0,0) 应该变换到 (10,5,0)
Vector4f worldPoint = new Vector4f(0, 0, 0, 1);
Vector4f localPoint = transform.transform(worldPoint);
assertThat(localPoint.x()).isCloseTo(10.0f, within(EPSILON));
assertThat(localPoint.y()).isCloseTo(5.0f, within(EPSILON));
assertThat(localPoint.z()).isCloseTo(0.0f, within(EPSILON));
}
@Test
void testLocalToWorldTransform() {
// 创建一个变换
Matrix4f localToWorld = new Matrix4f()
.translate(5, 3, 2)
.rotateX((float)Math.PI / 4);
// 逆变换世界到局部
Matrix4f worldToLocal = new Matrix4f(localToWorld).invert();
// 测试点
Vector4f localPoint = new Vector4f(1, 0, 0, 1);
// 局部 -> 世界
Vector4f worldPoint = localToWorld.transform(localPoint);
// 世界 -> 局部
Vector4f backToLocal = worldToLocal.transform(worldPoint);
// 应该能回到原点
assertThat(backToLocal.x()).isCloseTo(localPoint.x(), within(EPSILON));
assertThat(backToLocal.y()).isCloseTo(localPoint.y(), within(EPSILON));
assertThat(backToLocal.z()).isCloseTo(localPoint.z(), within(EPSILON));
}
// ==================== 旋转测试 ====================
@ParameterizedTest
@CsvSource({
"0, 1, 0, 0", // 0度旋转
"90, 0, 0, -1", // 90度旋转
"180, -1, 0, 0", // 180度旋转
"270, 0, 0, 1" // 270度旋转
})
void testRotationAroundYAxis(float degrees, float expectedX, float expectedY, float expectedZ) {
float radians = (float)Math.toRadians(degrees);
Matrix4f rotation = new Matrix4f().rotateY(radians);
// 原始点 (1, 0, 0)
Vector4f point = new Vector4f(1, 0, 0, 1);
Vector4f rotated = rotation.transform(point);
assertThat(rotated.x()).isCloseTo(expectedX, within(EPSILON));
assertThat(rotated.y()).isCloseTo(expectedY, within(EPSILON));
assertThat(rotated.z()).isCloseTo(expectedZ, within(EPSILON));
}
@Test
void testRotationComposition() {
// 绕X轴旋转90度
Matrix4f rotX = new Matrix4f().rotateX((float)Math.PI / 2);
// 绕Z轴旋转90度
Matrix4f rotZ = new Matrix4f().rotateZ((float)Math.PI / 2);
// 组合旋转先X后Z
Matrix4f combined = rotZ.mul(rotX);
// (1, 0, 0)
Vector4f point = new Vector4f(1, 0, 0, 1);
Vector4f transformed = combined.transform(point);
// 手动计算验证
// (1,0,0) 绕X转90度 -> (1,0,0) 实际上X轴旋转不影响X轴上的点
// 再绕Z转90度 -> (0,1,0)
assertThat(transformed.x()).isCloseTo(0.0f, within(EPSILON));
assertThat(transformed.y()).isCloseTo(1.0f, within(EPSILON));
assertThat(transformed.z()).isCloseTo(0.0f, within(EPSILON));
}
// ==================== 缩放测试 ====================
@Test
void testNonUniformScaling() {
Matrix4f scale = new Matrix4f().scale(2, 0.5f, 3);
Vector4f point = new Vector4f(1, 2, 3, 1);
Vector4f scaled = scale.transform(point);
assertThat(scaled.x()).isCloseTo(2.0f, within(EPSILON)); // 1 * 2
assertThat(scaled.y()).isCloseTo(1.0f, within(EPSILON)); // 2 * 0.5
assertThat(scaled.z()).isCloseTo(9.0f, within(EPSILON)); // 3 * 3
}
// ==================== 平移测试 ====================
@Test
void testTranslation() {
Matrix4f translation = new Matrix4f().translate(5, -3, 2.5f);
Vector4f point = new Vector4f(1, 2, 3, 1);
Vector4f translated = translation.transform(point);
assertThat(translated.x()).isCloseTo(6.0f, within(EPSILON)); // 1 + 5
assertThat(translated.y()).isCloseTo(-1.0f, within(EPSILON)); // 2 - 3
assertThat(translated.z()).isCloseTo(5.5f, within(EPSILON)); // 3 + 2.5
}
// ==================== 变换顺序测试 ====================
@Test
void testTransformOrderMatters() {
Vector4f point = new Vector4f(1, 0, 0, 1);
// 顺序1先平移后旋转
Matrix4f translateThenRotate = new Matrix4f()
.rotateY((float)Math.PI / 2) // 旋转90度
.translate(5, 0, 0); // 平移
// 顺序2先旋转后平移
Matrix4f rotateThenTranslate = new Matrix4f()
.translate(5, 0, 0)
.rotateY((float)Math.PI / 2);
Vector4f result1 = translateThenRotate.transform(point);
Vector4f result2 = rotateThenTranslate.transform(point);
// 两个结果应该不同
assertThat(result1.x()).isNotCloseTo(result2.x(), within(EPSILON));
assertThat(result1.z()).isNotCloseTo(result2.z(), within(EPSILON));
// 验证具体值
// 顺序1(1,0,0) 先平移->(6,0,0) 再旋转90度->(0,0,-6)
assertThat(result1.x()).isCloseTo(0.0f, within(EPSILON));
assertThat(result1.z()).isCloseTo(-6.0f, within(EPSILON));
// 顺序2(1,0,0) 先旋转90度->(0,0,-1) 再平移->(5,0,-1)
assertThat(result2.x()).isCloseTo(5.0f, within(EPSILON));
assertThat(result2.z()).isCloseTo(-1.0f, within(EPSILON));
}
// ==================== 相机视图变换测试 ====================
@Test
void testViewTransform() {
// 相机在 (0,0,5)看向原点 (0,0,0)上方向为Y轴
Matrix4f viewMatrix = new Matrix4f()
.lookAt(0, 0, 5, // 相机位置
0, 0, 0, // 看向的点
0, 1, 0); // 上方向
// 世界坐标的原点 (0,0,0) 在相机空间中应该在 (0,0,-5)
Vector4f worldPoint = new Vector4f(0, 0, 0, 1);
Vector4f viewPoint = viewMatrix.transform(worldPoint);
assertThat(viewPoint.x()).isCloseTo(0.0f, within(EPSILON));
assertThat(viewPoint.y()).isCloseTo(0.0f, within(EPSILON));
assertThat(viewPoint.z()).isCloseTo(-5.0f, within(EPSILON)); // 相机看向-Z方向
}
@Test
void testLookAtMatrixProperties() {
Vector3f eye = new Vector3f(10, 5, 10);
Vector3f center = new Vector3f(0, 0, 0);
Vector3f up = new Vector3f(0, 1, 0);
Matrix4f view = new Matrix4f().lookAt(eye, center, up);
// 1. 视图矩阵应该是正交矩阵逆等于转置
Matrix4f invView = new Matrix4f(view).invert();
Matrix4f transposeView = new Matrix4f(view).transpose();
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
assertThat(invView.get(i, j)).isCloseTo(transposeView.get(i, j), within(EPSILON));
}
}
// 2. 相机位置在视图空间应该是原点
Vector4f eyeHomogeneous = new Vector4f(eye, 1);
Vector4f eyeInView = view.transform(eyeHomogeneous);
assertThat(eyeInView.x()).isCloseTo(0.0f, within(EPSILON));
assertThat(eyeInView.y()).isCloseTo(0.0f, within(EPSILON));
assertThat(eyeInView.z()).isCloseTo(0.0f, within(EPSILON));
}
// ==================== 投影变换测试 ====================
@Test
void testPerspectiveProjection() {
float fov = (float)Math.toRadians(60);
float aspect = 16.0f / 9.0f;
float near = 0.1f;
float far = 100.0f;
Matrix4f projection = new Matrix4f().perspective(fov, aspect, near, far);
// 测试近平面上的点 (0,0,-near) 应该映射到 NDC z = -1
Vector4f nearPoint = new Vector4f(0, 0, -near, 1);
Vector4f projected = projection.transform(nearPoint);
projected.div(projected.w); // 透视除法
assertThat(projected.z()).isCloseTo(-1.0f, within(EPSILON));
// 测试远平面上的点 (0,0,-far) 应该映射到 NDC z = 1
Vector4f farPoint = new Vector4f(0, 0, -far, 1);
projected = projection.transform(farPoint);
projected.div(projected.w);
assertThat(projected.z()).isCloseTo(1.0f, within(EPSILON));
}
@Test
void testOrthographicProjection() {
float left = -10, right = 10;
float bottom = -5, top = 5;
float near = 0.1f, far = 100.0f;
Matrix4f ortho = new Matrix4f().ortho(left, right, bottom, top, near, far);
// 测试边界映射
Vector4f leftBottomNear = new Vector4f(left, bottom, -near, 1);
Vector4f projected = ortho.transform(leftBottomNear);
// 正交投影没有透视除法直接映射到NDC [-1,1]
assertThat(projected.x()).isCloseTo(-1.0f, within(EPSILON));
assertThat(projected.y()).isCloseTo(-1.0f, within(EPSILON));
assertThat(projected.z()).isCloseTo(-1.0f, within(EPSILON));
}
// ==================== MVP变换链测试 ====================
@Test
void testMVPChain() {
// 模型变换
Matrix4f model = new Matrix4f()
.translate(5, 0, 0)
.rotateY((float)Math.PI / 4);
// 视图变换相机
Matrix4f view = new Matrix4f()
.lookAt(0, 2, 10, 0, 0, 0, 0, 1, 0);
// 投影变换
Matrix4f projection = new Matrix4f()
.perspective((float)Math.toRadians(60), 16.0f/9.0f, 0.1f, 100.0f);
// 完整的MVP矩阵
Matrix4f mvp = projection.mul(view.mul(model));
// 测试局部坐标 (0,0,0)
Vector4f localPoint = new Vector4f(0, 0, 0, 1);
Vector4f clipPoint = mvp.transform(localPoint);
// 执行透视除法
clipPoint.div(clipPoint.w());
// 结果应该在NDC范围内 [-1, 1]
assertThat(clipPoint.x()).isBetween(-1.0f - EPSILON, 1.0f + EPSILON);
assertThat(clipPoint.y()).isBetween(-1.0f - EPSILON, 1.0f + EPSILON);
assertThat(clipPoint.z()).isBetween(-1.0f - EPSILON, 1.0f + EPSILON);
}
// ==================== 法线变换测试 ====================
@Test
void testNormalTransformation() {
// 创建一个包含非均匀缩放的变换
Matrix4f model = new Matrix4f()
.scale(2, 1, 1) // 只在X轴缩放
.rotateY((float)Math.PI / 6);
// 原始法线 (1,0,0)
Vector3f normal = new Vector3f(1, 0, 0);
// 错误的做法直接使用模型矩阵的3x3部分
Matrix3f wrongTransform = new Matrix3f(model);
Vector3f wrongResult = wrongTransform.transform(normal);
// 正确的做法使用逆转置矩阵
Matrix3f normalMatrix = new Matrix3f(model).invert().transpose();
Vector3f correctResult = normalMatrix.transform(normal).normalize();
// 两个结果应该不同因为缩放不是均匀的
assertThat(wrongResult.x()).isNotCloseTo(correctResult.x(), within(EPSILON));
assertThat(wrongResult.y()).isNotCloseTo(correctResult.y(), within(EPSILON));
assertThat(wrongResult.z()).isNotCloseTo(correctResult.z(), within(EPSILON));
// 验证法线变换后仍保持垂直性简化测试
Vector3f vertex = new Vector3f(1, 0, 0);
Vector3f transformedVertex = new Vector3f(vertex).mul(normalMatrix);
// 法线和顶点变换后的点积应该接近0垂直
float dot = correctResult.dot(transformedVertex);
assertThat(Math.abs(dot)).isCloseTo(0.0f, within(0.1f));
}
// ==================== 四元数旋转测试 ====================
@Test
void testQuaternionRotation() {
// 创建绕Y轴旋转90度的四元数
Quaternionf quat = new Quaternionf()
.rotateY((float)Math.PI / 2);
// 转换为矩阵
Matrix4f matrixFromQuat = new Matrix4f().rotation(quat);
// 直接创建矩阵
Matrix4f matrixDirect = new Matrix4f().rotateY((float)Math.PI / 2);
// 两个矩阵应该相同
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
assertThat(matrixFromQuat.get(i, j))
.isCloseTo(matrixDirect.get(i, j), within(EPSILON));
}
}
// 测试四元数旋转点
Vector3f point = new Vector3f(1, 0, 0);
Vector3f rotatedByQuat = quat.transform(point);
Vector3f rotatedByMatrix = matrixDirect.transformPosition(point, new Vector3f());
assertThat(rotatedByQuat.x()).isCloseTo(rotatedByMatrix.x(), within(EPSILON));
assertThat(rotatedByQuat.y()).isCloseTo(rotatedByMatrix.y(), within(EPSILON));
assertThat(rotatedByQuat.z()).isCloseTo(rotatedByMatrix.z(), within(EPSILON));
}
// ==================== 性能测试 ====================
@RepeatedTest(3)
@DisplayName("变换矩阵性能测试")
void testTransformPerformance() {
int iterations = 100000;
Matrix4f transform = new Matrix4f()
.translate(1, 2, 3)
.rotateXYZ(0.1f, 0.2f, 0.3f)
.scale(1.5f);
Vector4f point = new Vector4f(1, 2, 3, 1);
long startTime = System.nanoTime();
for (int i = 0; i < iterations; i++) {
Vector4f result = transform.transform(point);
// 确保结果被使用防止被优化掉
if (Float.isNaN(result.x())) {
Assertions.fail("不应该发生");
}
}
long endTime = System.nanoTime();
long durationMs = (endTime - startTime) / 1_000_000;
System.out.printf("Transform %d points in %d ms%n", iterations, durationMs);
assertThat(durationMs).isLessThan(1000); // 应该在1秒内完成
}
// ==================== 边缘情况测试 ====================
@Test
void testZeroScaling() {
Matrix4f scale = new Matrix4f().scale(0, 1, 1);
Vector4f point = new Vector4f(1, 2, 3, 1);
Vector4f result = scale.transform(point);
assertThat(result.x()).isCloseTo(0.0f, within(EPSILON));
assertThat(result.y()).isCloseTo(2.0f, within(EPSILON));
assertThat(result.z()).isCloseTo(3.0f, within(EPSILON));
}
@Test
void testNegativeScaling() {
Matrix4f scale = new Matrix4f().scale(-1, 1, 1);
Vector4f point = new Vector4f(1, 2, 3, 1);
Vector4f result = scale.transform(point);
assertThat(result.x()).isCloseTo(-1.0f, within(EPSILON));
assertThat(result.y()).isCloseTo(2.0f, within(EPSILON));
assertThat(result.z()).isCloseTo(3.0f, within(EPSILON));
}
@Test
void testIdentityTransform() {
Matrix4f identity = new Matrix4f().identity();
Vector4f point = new Vector4f(1, 2, 3, 1);
Vector4f result = identity.transform(point);
assertThat(result.x()).isCloseTo(point.x(), within(EPSILON));
assertThat(result.y()).isCloseTo(point.y(), within(EPSILON));
assertThat(result.z()).isCloseTo(point.z(), within(EPSILON));
}
}