更新内容
1. 命令帮助菜单 2. SimpleLanguageProvider 构造器扩展 支持多ILangKeyValueCollection数组参数
This commit is contained in:
parent
19463edd61
commit
2b0a14d0e9
28
build.gradle
28
build.gradle
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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": "狸"
|
||||
|
|
|
|||
|
|
@ -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": "小狐狸"
|
||||
|
|
|
|||
|
|
@ -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": "狐狸"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
|
|
@ -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());
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package top.r3944realms.lib39.core.command;
|
||||
|
||||
public record Parameter(String name, boolean required) {}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
package top.r3944realms.lib39.util.command;
|
||||
|
||||
/**
|
||||
* The type Command help helper.
|
||||
*/
|
||||
public class CommandHelpHelper {
|
||||
}
|
||||
414
src/test/java/top/r3944realms/lib39/CoordinateTransformTest.java
Normal file
414
src/test/java/top/r3944realms/lib39/CoordinateTransformTest.java
Normal 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));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user