diff --git a/build.gradle b/build.gradle
index db51ea6..672cf3c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -6,6 +6,7 @@ plugins {
id 'maven-publish'
id 'com.github.johnrengelman.shadow' version '8.1.1'
id 'net.neoforged.moddev.legacyforge' version '2.0.103'
+ id 'com.dorongold.task-tree' version '2.1.1'
}
java {
@@ -382,3 +383,122 @@ idea {
tasks.withType(GenerateModuleMetadata) {
enabled = false
}
+tasks.register('showTaskTree') {
+ doLast {
+ def showTaskDeps
+ showTaskDeps = { task, prefix = '' ->
+ println "${prefix}${task.name}"
+ task.getTaskDependencies().getDependencies(task).each { dep ->
+ showTaskDeps(dep, prefix + ' ')
+ }
+ }
+
+ def targetTask = tasks.findByName('build')
+ if (targetTask) {
+ println "构建任务依赖树:"
+ showTaskDeps(targetTask)
+ } else {
+ println "未找到 build 任务"
+ }
+ }
+}
+/**
+ build
+ ├── check
+ │ └── test
+ │ ├── compileTestJava
+ │ │ ├── classes
+ │ │ │ ├── compileJava
+ │ │ │ │ └── createMcpToSrg
+ │ │ │ │ └── extractSrg
+ │ │ │ │ └── downloadMcpConfig
+ │ │ │ └── processResources
+ │ │ └── compileJava
+ │ │ └── createMcpToSrg
+ │ │ └── extractSrg
+ │ │ └── downloadMcpConfig
+ │ ├── testClasses
+ │ │ ├── processTestResources
+ │ │ └── compileTestJava
+ │ │ ├── classes
+ │ │ │ ├── compileJava
+ │ │ │ │ └── createMcpToSrg
+ │ │ │ │ └── extractSrg
+ │ │ │ │ └── downloadMcpConfig
+ │ │ │ └── processResources
+ │ │ └── compileJava
+ │ │ └── createMcpToSrg
+ │ │ └── extractSrg
+ │ │ └── downloadMcpConfig
+ │ ├── classes
+ │ │ ├── compileJava
+ │ │ │ └── createMcpToSrg
+ │ │ │ └── extractSrg
+ │ │ │ └── downloadMcpConfig
+ │ │ └── processResources
+ │ └── compileJava
+ │ └── createMcpToSrg
+ │ └── extractSrg
+ │ └── downloadMcpConfig
+ └── assemble
+ ├── reobfJarJar
+ │ ├── createMcpToSrg
+ │ │ └── extractSrg
+ │ │ └── downloadMcpConfig
+ │ ├── configureReobfTaskForReobfJarJar
+ │ └── proguard
+ │ └── jarJar
+ │ ├── classes
+ │ │ ├── compileJava
+ │ │ │ └── createMcpToSrg
+ │ │ │ └── extractSrg
+ │ │ │ └── downloadMcpConfig
+ │ │ └── processResources
+ │ ├── compileJava
+ │ │ └── createMcpToSrg
+ │ │ └── extractSrg
+ │ │ └── downloadMcpConfig
+ │ └── addMixinsToJarJar
+ │ └── compileJava
+ │ └── createMcpToSrg
+ │ └── extractSrg
+ │ └── downloadMcpConfig
+ ├── reobfJar
+ │ ├── jar
+ │ │ ├── classes
+ │ │ │ ├── compileJava
+ │ │ │ │ └── createMcpToSrg
+ │ │ │ │ └── extractSrg
+ │ │ │ │ └── downloadMcpConfig
+ │ │ │ └── processResources
+ │ │ ├── compileJava
+ │ │ │ └── createMcpToSrg
+ │ │ │ └── extractSrg
+ │ │ │ └── downloadMcpConfig
+ │ │ └── addMixinsToJar
+ │ │ └── compileJava
+ │ │ └── createMcpToSrg
+ │ │ └── extractSrg
+ │ │ └── downloadMcpConfig
+ │ ├── createMcpToSrg
+ │ │ └── extractSrg
+ │ │ └── downloadMcpConfig
+ │ └── configureReobfTaskForReobfJar
+ └── jar
+ ├── classes
+ │ ├── compileJava
+ │ │ └── createMcpToSrg
+ │ │ └── extractSrg
+ │ │ └── downloadMcpConfig
+ │ └── processResources
+ ├── compileJava
+ │ └── createMcpToSrg
+ │ └── extractSrg
+ │ └── downloadMcpConfig
+ └── addMixinsToJar
+ └── compileJava
+ └── createMcpToSrg
+ └── extractSrg
+ └── downloadMcpConfig
+
+ */
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 7df8c52..9e0d342 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -33,7 +33,7 @@ mod_name=3944Realms 's Lib Mod
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
mod_license=MIT
# The mod version. See https://semver.org/
-mod_version=0.0.17
+mod_version=0.0.18
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
# This should match the base package used for the mod sources.
# See https://maven.apache.org/guides/mini/guide-naming-conventions.html
diff --git a/src/generated/resources/.cache/1de3d2ee724999f84a11b20b51c37030049be277 b/src/generated/resources/.cache/1de3d2ee724999f84a11b20b51c37030049be277
index 6acc8d5..acf9f54 100644
--- a/src/generated/resources/.cache/1de3d2ee724999f84a11b20b51c37030049be277
+++ b/src/generated/resources/.cache/1de3d2ee724999f84a11b20b51c37030049be277
@@ -1,2 +1,2 @@
-// 1.20.1 2025-10-25T19:14:21.1829335 Languages: zh_tw
-4cb94c651f6aa74538a2ab25cb183cffd75be688 assets/lib39/lang/zh_tw.json
+// 1.20.1 2025-11-22T23:38:13.2517748 Languages: zh_tw
+84dba66c4c768fd7754eaabe990c96f14a30c1df assets/lib39/lang/zh_tw.json
diff --git a/src/generated/resources/.cache/2a65ee2815744be1ef1ffdae1c9a37f2a9cbe2ac b/src/generated/resources/.cache/2a65ee2815744be1ef1ffdae1c9a37f2a9cbe2ac
index d291622..eb816d6 100644
--- a/src/generated/resources/.cache/2a65ee2815744be1ef1ffdae1c9a37f2a9cbe2ac
+++ b/src/generated/resources/.cache/2a65ee2815744be1ef1ffdae1c9a37f2a9cbe2ac
@@ -1,2 +1,2 @@
-// 1.20.1 2025-10-25T19:14:21.1764262 Languages: zh_cn
-a1db601a0fca923c5434b8b0843773a3115d0b59 assets/lib39/lang/zh_cn.json
+// 1.20.1 2025-11-22T23:38:13.249775 Languages: zh_cn
+48655f133966c9a854c57b560d070850af5a289f assets/lib39/lang/zh_cn.json
diff --git a/src/generated/resources/.cache/2dbf84d84cf6f7b7a95fea9038e192dbf226e5f5 b/src/generated/resources/.cache/2dbf84d84cf6f7b7a95fea9038e192dbf226e5f5
index c17c4ef..5a2f35d 100644
--- a/src/generated/resources/.cache/2dbf84d84cf6f7b7a95fea9038e192dbf226e5f5
+++ b/src/generated/resources/.cache/2dbf84d84cf6f7b7a95fea9038e192dbf226e5f5
@@ -1,3 +1,4 @@
-// 1.20.1 2025-10-25T18:42:29.3405259 Item Models: lib39
+// 1.20.1 2025-11-22T23:38:13.2527751 Item Models: lib39
14f581c8f8e7f0de004c57a180f371e60e7b12ae assets/lib39/models/item/fabric.json
+70583055336790fc837836ea6b49d16cfc8b64b8 assets/lib39/models/item/forge.json
447b36747d0aa8748dcd86715f4cce2cff19aca7 assets/lib39/models/item/neoforge.json
diff --git a/src/generated/resources/.cache/82018c5420b46ddbb7071e62df09fdecd98133e6 b/src/generated/resources/.cache/82018c5420b46ddbb7071e62df09fdecd98133e6
index b518cbb..9003b0a 100644
--- a/src/generated/resources/.cache/82018c5420b46ddbb7071e62df09fdecd98133e6
+++ b/src/generated/resources/.cache/82018c5420b46ddbb7071e62df09fdecd98133e6
@@ -1,2 +1,2 @@
-// 1.20.1 2025-10-25T19:14:21.1804258 Languages: lzh
-098df025475d3f4d9d2abefa91a7d38c44644ba4 assets/lib39/lang/lzh.json
+// 1.20.1 2025-11-22T23:38:13.2517748 Languages: lzh
+a12c11d89d484a0e4f193ebd11b63722b8c501df assets/lib39/lang/lzh.json
diff --git a/src/generated/resources/.cache/c622617f6fabf890a00b9275cd5f643584a8a2c8 b/src/generated/resources/.cache/c622617f6fabf890a00b9275cd5f643584a8a2c8
index 99da09b..d94a19a 100644
--- a/src/generated/resources/.cache/c622617f6fabf890a00b9275cd5f643584a8a2c8
+++ b/src/generated/resources/.cache/c622617f6fabf890a00b9275cd5f643584a8a2c8
@@ -1,2 +1,2 @@
-// 1.20.1 2025-10-25T19:14:21.1784269 Languages: en_us
-c6b6aadca0a922823a8c949ebde93f8f999737f9 assets/lib39/lang/en_us.json
+// 1.20.1 2025-11-22T23:38:13.2507757 Languages: en_us
+65df86271a054138e168311be826408455b3c33a assets/lib39/lang/en_us.json
diff --git a/src/generated/resources/assets/lib39/lang/en_us.json b/src/generated/resources/assets/lib39/lang/en_us.json
index 1989dd6..98e9097 100644
--- a/src/generated/resources/assets/lib39/lang/en_us.json
+++ b/src/generated/resources/assets/lib39/lang/en_us.json
@@ -1,4 +1,5 @@
{
"item.lib39.fabric": "Fabric",
+ "item.lib39.forge": "Forge",
"item.lib39.neoforge": "NeoForge"
}
\ No newline at end of file
diff --git a/src/generated/resources/assets/lib39/lang/lzh.json b/src/generated/resources/assets/lib39/lang/lzh.json
index 865d7b3..99cd302 100644
--- a/src/generated/resources/assets/lib39/lang/lzh.json
+++ b/src/generated/resources/assets/lib39/lang/lzh.json
@@ -1,4 +1,5 @@
{
"item.lib39.fabric": "織",
+ "item.lib39.forge": "砧",
"item.lib39.neoforge": "狸"
}
\ No newline at end of file
diff --git a/src/generated/resources/assets/lib39/lang/zh_cn.json b/src/generated/resources/assets/lib39/lang/zh_cn.json
index ad3ca87..19f82cb 100644
--- a/src/generated/resources/assets/lib39/lang/zh_cn.json
+++ b/src/generated/resources/assets/lib39/lang/zh_cn.json
@@ -1,4 +1,5 @@
{
"item.lib39.fabric": "织布",
+ "item.lib39.forge": "铁砧",
"item.lib39.neoforge": "小狐狸"
}
\ No newline at end of file
diff --git a/src/generated/resources/assets/lib39/lang/zh_tw.json b/src/generated/resources/assets/lib39/lang/zh_tw.json
index e14a166..ad24e76 100644
--- a/src/generated/resources/assets/lib39/lang/zh_tw.json
+++ b/src/generated/resources/assets/lib39/lang/zh_tw.json
@@ -1,4 +1,5 @@
{
"item.lib39.fabric": "織布",
+ "item.lib39.forge": "铁砧",
"item.lib39.neoforge": "狐狸"
}
\ No newline at end of file
diff --git a/src/generated/resources/assets/lib39/models/item/forge.json b/src/generated/resources/assets/lib39/models/item/forge.json
new file mode 100644
index 0000000..8989f8c
--- /dev/null
+++ b/src/generated/resources/assets/lib39/models/item/forge.json
@@ -0,0 +1,6 @@
+{
+ "parent": "minecraft:item/generated",
+ "textures": {
+ "layer0": "lib39:item/forge"
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/top/r3944realms/lib39/Lib39.java b/src/main/java/top/r3944realms/lib39/Lib39.java
index e294953..5718f1d 100644
--- a/src/main/java/top/r3944realms/lib39/Lib39.java
+++ b/src/main/java/top/r3944realms/lib39/Lib39.java
@@ -1,8 +1,11 @@
package top.r3944realms.lib39;
+import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.fml.ModList;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.loading.FMLEnvironment;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import top.r3944realms.lib39.core.network.NetworkHandler;
@@ -29,6 +32,11 @@ public class Lib39 {
initialize();
}
+ /**
+ * The constant ENABLE_EXAMPLES_PROPERTY_KEY.
+ */
+ public static final String ENABLE_EXAMPLES_PROPERTY_KEY = "lib39.enable_examples";
+
/**
* Initialize.
*/
@@ -43,6 +51,17 @@ public class Lib39 {
}
+ /**
+ * Rl resource location.
+ *
+ * @param path the path
+ * @return the resource location
+ */
+ @Contract("_ -> new")
+ public static @NotNull ResourceLocation rl(String path) {
+ return new ResourceLocation(MOD_ID, path);
+ }
+
/**
* The type Mod info.
*/
@@ -66,7 +85,7 @@ public class Lib39 {
* @return the boolean
*/
static boolean shouldRegisterExamples() {
- return !FMLEnvironment.production;
+ return !FMLEnvironment.production || Boolean.getBoolean(ENABLE_EXAMPLES_PROPERTY_KEY);
}
/**
diff --git a/src/main/java/top/r3944realms/lib39/api/event/SyncManagerRegisterEvent.java b/src/main/java/top/r3944realms/lib39/api/event/SyncManagerRegisterEvent.java
index ebb61a4..0f106a6 100644
--- a/src/main/java/top/r3944realms/lib39/api/event/SyncManagerRegisterEvent.java
+++ b/src/main/java/top/r3944realms/lib39/api/event/SyncManagerRegisterEvent.java
@@ -4,6 +4,7 @@ import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.Entity;
import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.eventbus.api.Event;
+import org.jetbrains.annotations.NotNull;
import top.r3944realms.lib39.core.sync.ISyncData;
import top.r3944realms.lib39.core.sync.ISyncManager;
import top.r3944realms.lib39.core.sync.SyncData2Manager;
@@ -159,7 +160,7 @@ public class SyncManagerRegisterEvent extends Event {
ResourceLocation id,
ISyncManager syncManager,
Function> dataProvider,
- Class>... allowedEntityClasses
+ Class> @NotNull ... allowedEntityClasses
) {
registerSyncManager(id, syncManager, dataProvider);
if (allowedEntityClasses.length > 0) {
diff --git a/src/main/java/top/r3944realms/lib39/client/gui/component/WheelWidget.java b/src/main/java/top/r3944realms/lib39/client/gui/component/WheelWidget.java
new file mode 100644
index 0000000..a4ecbfe
--- /dev/null
+++ b/src/main/java/top/r3944realms/lib39/client/gui/component/WheelWidget.java
@@ -0,0 +1,769 @@
+package top.r3944realms.lib39.client.gui.component;
+
+import com.mojang.blaze3d.platform.Window;
+import com.mojang.blaze3d.systems.RenderSystem;
+import com.mojang.blaze3d.vertex.*;
+import net.minecraft.MethodsReturnNonnullByDefault;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.GuiGraphics;
+import net.minecraft.client.gui.components.AbstractWidget;
+import net.minecraft.client.gui.narration.NarrationElementOutput;
+import net.minecraft.client.renderer.ShaderInstance;
+import net.minecraft.network.chat.Component;
+import org.joml.Matrix4f;
+import org.joml.Vector2f;
+import top.r3944realms.lib39.client.shader.Lib39Shaders;
+import top.r3944realms.lib39.util.MathUtil;
+import top.r3944realms.lib39.util.lang.FourConsumer;
+import top.r3944realms.lib39.util.lang.Pair;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * The type Wheel widget.
+ *
+ * @author QiuShui1012
+ */
+@MethodsReturnNonnullByDefault
+@ParametersAreNonnullByDefault
+public class WheelWidget extends AbstractWidget {
+ /**
+ * The constant IGNORE_CURSOR_MOVE_LENGTH.
+ */
+ public static final int IGNORE_CURSOR_MOVE_LENGTH = 15;
+ private static final Vector2f ROTATION_START = new Vector2f(0, 1);
+ private static final int SELECTION_EFFECT_COLOR = 0xddFFFF00;
+ private static final int SELECTION_EFFECT_RADIUS = 20;
+
+ private final Minecraft minecraft = Minecraft.getInstance();
+ private final Vector2f centerPos;
+ private final float ringInnerRadius;
+ private final float ringOuterRadius;
+ private final int delay;
+ private final int animationMs;
+ private final int closingAnimationMs; //ms
+ private final int ringColor;
+ private final int selectionEffectColor;
+ private final int selectionEffectRadius;
+ private final float selectionAnimationSpeedFactor;
+ private final int textColor;
+ private final float textScale;
+ private final List sections = new ArrayList<>();
+
+ private long displayTime = System.currentTimeMillis();
+ private float currentAngle = 0;
+
+ /**
+ * Gets current section index.
+ *
+ * @return the current section index
+ */
+ public int getCurrentSectionIndex() {
+ return currentSectionIndex;
+ }
+
+ /**
+ * Is closing animation started boolean.
+ *
+ * @return the boolean
+ */
+ public boolean isClosingAnimationStarted() {
+ return closingAnimationStarted;
+ }
+
+ private int currentSectionIndex = -1;
+ private Vector2f selectionEffectPos;
+ private boolean animationStarted = false;
+
+ /**
+ * Sets closing animation started.
+ *
+ * @param closingAnimationStarted the closing animation started
+ */
+ public void setClosingAnimationStarted(boolean closingAnimationStarted) {
+ this.closingAnimationStarted = closingAnimationStarted;
+ }
+
+ private boolean closingAnimationStarted = false;
+
+ /**
+ * Instantiates a new Wheel widget.
+ *
+ * @param x the x
+ * @param y the y
+ * @param width the width
+ * @param height the height
+ * @param ringInnerRadius the ring inner radius
+ * @param ringOuterRadius the ring outer radius
+ * @param textScale the text scale
+ * @param sections the sections
+ */
+ public WheelWidget(
+ int x, int y, int width, int height,
+ float ringInnerRadius, float ringOuterRadius, float textScale,
+ List>> sections
+ ) {
+ this(x, y, width, height, Component.empty(), ringInnerRadius, ringOuterRadius, textScale, sections);
+ }
+
+ /**
+ * Instantiates a new Wheel widget.
+ *
+ * @param x the x
+ * @param y the y
+ * @param width the width
+ * @param height the height
+ * @param ringInnerRadius the ring inner radius
+ * @param ringOuterRadius the ring outer radius
+ * @param textScale the text scale
+ * @param degreeOffsetAngle the degree offset angle
+ * @param sections the sections
+ */
+ public WheelWidget(
+ int x, int y, int width, int height,
+ float ringInnerRadius, float ringOuterRadius, float textScale, float degreeOffsetAngle,
+ List>> sections
+ ) {
+ this(x, y, width, height, Component.empty(), ringInnerRadius, ringOuterRadius, textScale, degreeOffsetAngle, sections);
+ }
+
+ /**
+ * Instantiates a new Wheel widget.
+ *
+ * @param x the x
+ * @param y the y
+ * @param width the width
+ * @param height the height
+ * @param ringInnerRadius the ring inner radius
+ * @param ringOuterRadius the ring outer radius
+ * @param sections the sections
+ */
+ public WheelWidget(
+ int x, int y, int width, int height,
+ float ringInnerRadius, float ringOuterRadius,
+ List>> sections
+ ) {
+ this(x, y, width, height, Component.empty(), ringInnerRadius, ringOuterRadius, sections);
+ }
+
+ /**
+ * Instantiates a new Wheel widget.
+ *
+ * @param x the x
+ * @param y the y
+ * @param width the width
+ * @param height the height
+ * @param message the message
+ * @param ringInnerRadius the ring inner radius
+ * @param ringOuterRadius the ring outer radius
+ * @param textScale the text scale
+ * @param degreeOffsetAngle the degree offset angle
+ * @param sections the sections
+ */
+ public WheelWidget(
+ int x, int y, int width, int height, Component message,
+ float ringInnerRadius, float ringOuterRadius, float textScale, float degreeOffsetAngle,
+ List>> sections
+ ) {
+ this(
+ x, y, width, height, message,
+ ringInnerRadius, ringOuterRadius,
+ 150, 300, 150,
+ 0x00000000,
+ 0xddffff00, 20, 5f,
+ 0xfdfdfd, textScale, degreeOffsetAngle,
+ sections
+ );
+ }
+
+ /**
+ * Instantiates a new Wheel widget.
+ *
+ * @param x the x
+ * @param y the y
+ * @param width the width
+ * @param height the height
+ * @param message the message
+ * @param ringInnerRadius the ring inner radius
+ * @param ringOuterRadius the ring outer radius
+ * @param sections the sections
+ */
+ public WheelWidget(
+ int x, int y, int width, int height, Component message,
+ float ringInnerRadius, float ringOuterRadius,
+ List>> sections
+ ) {
+ this(
+ x, y, width, height, message,
+ ringInnerRadius, ringOuterRadius,
+ 150, 300, 150,
+ 0x00000000,
+ 0xddffff00, 20, 5f,
+ 0xfdfdfd, 1f, 0f,
+ sections
+ );
+ }
+
+ /**
+ * Instantiates a new Wheel widget.
+ *
+ * @param x the x
+ * @param y the y
+ * @param width the width
+ * @param height the height
+ * @param message the message
+ * @param ringInnerRadius the ring inner radius
+ * @param ringOuterRadius the ring outer radius
+ * @param degreeOffsetAngle the degree offset angle
+ * @param sections the sections
+ */
+ public WheelWidget(
+ int x, int y, int width, int height, Component message,
+ float ringInnerRadius, float ringOuterRadius, float degreeOffsetAngle,
+ List>> sections
+ ) {
+ this(
+ x, y, width, height, message,
+ ringInnerRadius, ringOuterRadius,
+ 150, 300, 150,
+ 0x00000000,
+ 0xddffff00, 20, 5f,
+ 0xfdfdfd, 1f, degreeOffsetAngle,
+ sections
+ );
+ }
+
+ /**
+ * Instantiates a new Wheel widget.
+ *
+ * @param x the x
+ * @param y the y
+ * @param width the width
+ * @param height the height
+ * @param ringInnerRadius the ring inner radius
+ * @param ringOuterRadius the ring outer radius
+ * @param delay the delay
+ * @param animationMs the animation ms
+ * @param closingAnimationMs the closing animation ms
+ * @param ringColor the ring color
+ * @param selectionEffectColor the selection effect color
+ * @param selectionEffectRadius the selection effect radius
+ * @param selectionAnimationSpeedFactor the selection animation speed factor
+ * @param textColor the text color
+ * @param textScale the text scale
+ * @param sections the sections
+ */
+ public WheelWidget(
+ int x, int y, int width, int height,
+ float ringInnerRadius, float ringOuterRadius,
+ int delay, int animationMs, int closingAnimationMs,
+ int ringColor,
+ int selectionEffectColor, int selectionEffectRadius, float selectionAnimationSpeedFactor,
+ int textColor, float textScale,
+ List>> sections
+ ) {
+ this(
+ x, y, width, height, Component.empty(),
+ ringInnerRadius, ringOuterRadius,
+ delay, animationMs, closingAnimationMs,
+ ringColor,
+ selectionEffectColor, selectionEffectRadius, selectionAnimationSpeedFactor,
+ textColor, textScale, 0f,
+ sections
+ );
+ }
+
+ /**
+ * Instantiates a new Wheel widget.
+ *
+ * @param x the x
+ * @param y the y
+ * @param width the width
+ * @param height the height
+ * @param message the message
+ * @param ringInnerRadius the ring inner radius
+ * @param ringOuterRadius the ring outer radius
+ * @param delay the delay
+ * @param animationMs the animation ms
+ * @param closingAnimationMs the closing animation ms
+ * @param ringColor the ring color
+ * @param selectionEffectColor the selection effect color
+ * @param selectionEffectRadius the selection effect radius
+ * @param selectionAnimationSpeedFactor the selection animation speed factor
+ * @param textColor the text color
+ * @param textScale the text scale
+ * @param degreeOffsetAngle the degree offset angle
+ * @param sections the sections
+ */
+ public WheelWidget(
+ int x, int y, int width, int height, Component message,
+ float ringInnerRadius, float ringOuterRadius,
+ int delay, int animationMs, int closingAnimationMs,
+ int ringColor,
+ int selectionEffectColor, int selectionEffectRadius, float selectionAnimationSpeedFactor,
+ int textColor, float textScale, float degreeOffsetAngle,
+ List>> sections
+ ) {
+ super(x, y, width, height, message);
+ this.centerPos = new Vector2f(this.getX() + this.getWidth() / 2f, this.getY() + this.getHeight() / 2f);
+ this.ringInnerRadius = Math.max(ringInnerRadius, IGNORE_CURSOR_MOVE_LENGTH);
+ this.ringOuterRadius = ringOuterRadius;
+ this.delay = delay;
+ this.animationMs = animationMs;
+ this.closingAnimationMs = closingAnimationMs;
+ this.ringColor = ringColor;
+ this.selectionEffectColor = selectionEffectColor;
+ this.selectionEffectRadius = selectionEffectRadius;
+ this.selectionAnimationSpeedFactor = selectionAnimationSpeedFactor;
+ this.textColor = textColor;
+ this.textScale = textScale;
+ float degreeEachRotation = 360f / sections.size();
+ for (int i = 0; i < sections.size(); i++) {
+ Pair> section = sections.get(i);
+ float rotation = MathUtil.clampWithProportion((degreeEachRotation * i + degreeOffsetAngle) % 360, 0, 360);
+ Vector2f rotated = MathUtil.rotationDegrees(ROTATION_START, rotation)
+ .mul(1, -1)
+ .mul(this.getSectionCircleDiameter())
+ .add(this.centerPos);
+ float detectionStart = (float) (Math.toRadians(rotation - degreeEachRotation / 2f) + Math.PI * 2);
+ float detectionEnd = (float) (Math.toRadians(rotation + degreeEachRotation / 2f) + Math.PI * 2);
+ detectionStart = detectionStart % (float) (Math.PI * 2);
+ detectionEnd = detectionEnd % (float) (Math.PI * 2);
+ this.sections.add(new WheelSection(
+ rotated,
+ (float) (Math.toRadians(rotation) % (Math.PI * 2)),
+ detectionStart,
+ detectionEnd,
+ section.first,
+ section.second
+ ));
+ }
+ this.selectionEffectPos = MathUtil.rotate(
+ MathUtil.copy(ROTATION_START)
+ .mul(this.getSectionCircleDiameter()),
+ this.currentAngle
+ );
+ }
+
+ /**
+ * Gets section circle diameter.
+ *
+ * @return the section circle diameter
+ */
+// 滚轮选择器中每个扇形的圆形直径
+ public float getSectionCircleDiameter() {
+ return this.ringOuterRadius + this.ringInnerRadius;
+ }
+
+ /**
+ * Sets current index.
+ *
+ * @param index the index
+ * @return the current index
+ */
+ public WheelWidget setCurrentIndex(int index) {
+ this.currentSectionIndex = index;
+ this.currentAngle = this.sections.get(index).angle;
+ this.selectionEffectPos = MathUtil.rotate(
+ MathUtil.copy(ROTATION_START)
+ .mul(this.getSectionCircleDiameter()),
+ this.currentAngle
+ );
+ return this;
+ }
+
+ /**
+ * Gets section size.
+ *
+ * @return the section size
+ */
+ public int getSectionSize() {
+ return this.sections.size();
+ }
+
+
+ @Override
+ public boolean mouseScrolled(double mouseX, double mouseY, double delta) {
+ if (delta > 0) {
+ if (this.currentSectionIndex == this.getSectionSize() - 1) {
+ this.currentSectionIndex = 0;
+ } else {
+ this.currentSectionIndex++;
+ }
+ } else if (delta < 0) {
+ if (this.currentSectionIndex == 0) {
+ this.currentSectionIndex = this.getSectionSize() - 1;
+ } else {
+ this.currentSectionIndex--;
+ }
+ }
+ for (WheelSection section : this.sections) {
+ if (this.sections.indexOf(section) == this.currentSectionIndex) {
+ this.currentAngle = section.angle;
+ return true;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Check mouse pos.
+ *
+ * @param mouseX the mouse x
+ * @param mouseY the mouse y
+ */
+ public void checkMousePos(double mouseX, double mouseY) {
+ if (this.closingAnimationStarted) return;
+ float centerX = this.centerPos.x;
+ float centerY = this.centerPos.y;
+ // 鼠标距离屏幕中心的位置向量
+ Vector2f cursorPos = new Vector2f((float) mouseX - centerX, (float) mouseY - centerY);
+
+ if (cursorPos.length() < IGNORE_CURSOR_MOVE_LENGTH) return;
+
+ Vector2f rotationStart = new Vector2f(0, 1);
+ cursorPos.normalize();
+ // 计算夹角弧度
+ double rot = Math.acos(rotationStart.dot(cursorPos) / (rotationStart.length() * cursorPos.length()));
+ double rotation = cursorPos.x < 0 ? Math.PI - rot : Math.PI + rot;
+ for (WheelSection section : this.sections) {
+ if (section.angleStart > section.angleEnd && rotation >= section.angleStart
+ || rotation >= section.angleStart && rotation <= section.angleEnd
+ ) {
+ this.currentAngle = section.angle;
+ this.currentSectionIndex = this.sections.indexOf(section);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Should render boolean.
+ *
+ * @return the boolean
+ */
+ public boolean shouldRender() {
+ if (this.animationStarted) return true;
+ return (this.displayTime + this.delay) <= System.currentTimeMillis();
+ }
+
+ @Override
+ public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) {
+ this.checkMousePos(mouseX, mouseY);
+ this.renderWidget(guiGraphics, mouseX, mouseY, partialTick);
+ }
+
+ @Override
+ protected void renderWidget(GuiGraphics guiGraphics, int i, int i1, float v) {
+ RenderSystem.enableDepthTest();
+ RenderSystem.enableBlend();
+ this.renderClosingAnimation(guiGraphics);
+ if (!this.shouldRender()) {
+ return;
+ }
+ if (this.closingAnimationStarted) return;
+ if (!this.animationStarted) {
+ this.animationStarted = true;
+ this.displayTime = System.currentTimeMillis();
+ }
+ PoseStack poseStack = guiGraphics.pose();
+ float delta = this.displayTime + this.animationMs - System.currentTimeMillis();
+ if (delta > 0) {
+ float progress = 1 - (delta / this.animationMs);
+ progress = (float) (-Math.pow(progress, 2) + 2 * progress);
+ if (progress == 0) return;
+ this.renderProgressAnimation(guiGraphics, progress);
+ return;
+ }
+ renderRing(
+ guiGraphics,
+ this.centerPos.x,
+ this.centerPos.y,
+ this.ringColor,
+ this.ringInnerRadius * 2,
+ this.ringOuterRadius * 2
+ );
+ this.renderSelection(guiGraphics);
+ for (WheelSection value : this.sections) {
+ float x = value.center.x;
+ float y = value.center.y;
+ poseStack.pushPose();
+ poseStack.translate(x - 10, y - 10, 100);
+ value.renderer.accept(guiGraphics, poseStack, 20, 20);
+ poseStack.popPose();
+ poseStack.pushPose();
+ float coordinateScale = 0.7f;
+ float offsetX = 0.1f * this.width;
+ float offsetY = 0.1f * this.height;
+ float adjustedX = (x - offsetX) / coordinateScale;
+ float adjustedY = (y - offsetY - 20 * this.textScale) / coordinateScale;
+
+ poseStack.translate(offsetX, offsetY, 0);
+ poseStack.scale(coordinateScale, coordinateScale, coordinateScale);
+ poseStack.translate(adjustedX, adjustedY, 0);
+ poseStack.scale(this.textScale / coordinateScale, this.textScale / coordinateScale, this.textScale / coordinateScale);
+ guiGraphics.drawCenteredString(
+ minecraft.font,
+ value.subTitle,
+ 0,
+ 0,
+ (0xff << 24) | this.textColor
+ );
+ poseStack.popPose();
+ }
+ RenderSystem.disableDepthTest();
+ RenderSystem.disableBlend();
+ }
+
+ /**
+ * Render closing animation.
+ *
+ * @param guiGraphics the gui graphics
+ */
+ public void renderClosingAnimation(GuiGraphics guiGraphics) {
+ if (!this.closingAnimationStarted) return;
+ float delta = this.displayTime + this.closingAnimationMs - System.currentTimeMillis();
+ float progress = delta / this.closingAnimationMs;
+ if(progress >= 1 || progress <= 0) {
+ this.minecraft.setScreen(null);
+ }
+ this.renderProgressAnimation(guiGraphics, progress);
+ }
+ private void renderProgressAnimation(GuiGraphics guiGraphics, float progress) {
+ progress = (float) (-Math.pow(progress, 2) + 2 * progress);
+ if (progress == 0) return;
+ PoseStack poseStack = guiGraphics.pose();
+ poseStack.pushPose();
+ renderRing(
+ guiGraphics,
+ this.centerPos.x,
+ this.centerPos.y,
+ this.ringColor,
+ this.ringInnerRadius * 2 * progress,
+ this.ringOuterRadius * 2 * progress
+ );
+ poseStack.popPose();
+ if(this.currentSectionIndex != -1) {
+ WheelSection section = this.sections.get(this.currentSectionIndex);
+ Vector2f center = new Vector2f(
+ (section.center.x - this.centerPos.x) / this.getSectionCircleDiameter(),
+ (section.center.y - this.centerPos.y) / this.getSectionCircleDiameter()
+ ).mul(this.getSectionCircleDiameter() * progress).add(this.centerPos.x, this.centerPos.y);
+ renderSelectionEffect(
+ guiGraphics,
+ center.x,
+ center.y,
+ this.selectionEffectColor,
+ this.selectionEffectRadius
+ );
+ }
+ for (WheelSection value : this.sections) {
+ if (sections.get(0) != value) continue;
+ Vector2f center = new Vector2f(
+ (value.center.x - this.centerPos.x) / this.getSectionCircleDiameter(),
+ (value.center.y - this.centerPos.y) / this.getSectionCircleDiameter()
+ ).mul(this.getSectionCircleDiameter() * progress).add(this.centerPos.x, this.centerPos.y);
+ float x = center.x;
+ float y = center.y;
+ poseStack.pushPose();
+ poseStack.translate(x - 10, y - 10, 100);
+ value.renderer.accept(guiGraphics, poseStack, 20, 20);
+ poseStack.pushPose();
+ }
+ }
+
+ /**
+ * Render ring.
+ *
+ * @param guiGraphics the gui graphics
+ * @param centerX the center x
+ * @param centerY the center y
+ * @param color the color
+ * @param innerRadius the inner radius
+ * @param outerRadius the outer radius
+ */
+ public static void renderRing(
+ GuiGraphics guiGraphics,
+ float centerX,
+ float centerY,
+ int color,
+ float innerRadius, // 改为半径
+ float outerRadius // 改为半径
+ ) {
+ PoseStack poseStack = guiGraphics.pose();
+ poseStack.pushPose();
+
+ Tesselator tesselator = Tesselator.getInstance();
+ BufferBuilder buffer = tesselator.getBuilder();
+
+ // 计算足够大的绘制区域来覆盖整个环形(基于外半径)
+ float margin = outerRadius + 100f; // 使用半径计算边距
+ float x1 = centerX - margin;
+ float y1 = centerY - margin;
+ float x2 = centerX + margin;
+ float y2 = centerY + margin;
+
+ buffer.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR);
+
+ Matrix4f matrix = poseStack.last().pose();
+ buffer.vertex(matrix, x1, y1, -300).color(color).endVertex();
+ buffer.vertex(matrix, x1, y2, -300).color(color).endVertex();
+ buffer.vertex(matrix, x2, y2, -300).color(color).endVertex();
+ buffer.vertex(matrix, x2, y1, -300).color(color).endVertex();
+
+ setupRingShader(centerX, centerY, innerRadius, outerRadius);
+
+ BufferUploader.drawWithShader(buffer.end());
+ poseStack.popPose();
+ }
+
+ private static void setupRingShader(float centerX, float centerY, float innerRadius, float outerRadius) {
+ Window window = Minecraft.getInstance().getWindow();
+ float guiScale = (float) window.getGuiScale();
+
+ RenderSystem.setShader(Lib39Shaders::getRingShader);
+
+ // 转换到像素坐标(考虑GUI缩放)
+ float pixelCenterX = centerX * guiScale;
+ float pixelCenterY = window.getHeight() - (centerY * guiScale); // 翻转Y坐标
+
+ // 半径考虑GUI缩放
+ float pixelInnerRadius = innerRadius * guiScale;
+ float pixelOuterRadius = outerRadius * guiScale;
+ float pixelAntiAliasing = 2.0f * guiScale; // 抗锯齿范围
+
+ System.out.println("Shader Params - Center: (" + pixelCenterX + ", " + pixelCenterY +
+ "), InnerRadius: " + pixelInnerRadius + ", OuterRadius: " + pixelOuterRadius);
+
+ ShaderInstance shader = Lib39Shaders.getRingShader();
+ shader.safeGetUniform("Center").set(pixelCenterX, pixelCenterY);
+ shader.safeGetUniform("InnerRadius").set(pixelInnerRadius);
+ shader.safeGetUniform("OuterRadius").set(pixelOuterRadius);
+ shader.safeGetUniform("AntiAliasing").set(pixelAntiAliasing);
+ shader.safeGetUniform("ColorModulator").set(1.0f, 1.0f, 1.0f, .5f);
+ }
+
+ private void renderSelection(GuiGraphics guiGraphics) {
+ float selectionEffectAngle = MathUtil.angle(
+ MathUtil.copy(ROTATION_START),
+ this.selectionEffectPos
+ );
+
+ float diffAngle = this.currentAngle - selectionEffectAngle;
+
+ if (diffAngle > Math.PI) {
+ diffAngle -= (float) (Math.PI * 2);
+ } else if (diffAngle < -Math.PI) {
+ diffAngle += (float) (Math.PI * 2);
+ }
+
+ this.selectionEffectPos = MathUtil.rotate(
+ this.selectionEffectPos,
+ diffAngle / this.selectionAnimationSpeedFactor
+ );
+
+ Vector2f pos = MathUtil.copy(this.selectionEffectPos)
+ .mul(1, -1)
+ .add(this.centerPos);
+
+ // 调用时使用半径
+ renderSelectionEffect(
+ guiGraphics,
+ pos.x,
+ pos.y,
+ SELECTION_EFFECT_COLOR,
+ SELECTION_EFFECT_RADIUS // 确保这是半径值
+ );
+ }
+
+ /**
+ * Render selection effect.
+ *
+ * @param guiGraphics the gui graphics
+ * @param centerX the center x
+ * @param centerY the center y
+ * @param color the color
+ * @param radius the radius
+ */
+ public static void renderSelectionEffect(
+ GuiGraphics guiGraphics,
+ float centerX,
+ float centerY,
+ int color,
+ float radius
+ ) {
+ RenderSystem.enableBlend();
+ RenderSystem.defaultBlendFunc();
+ RenderSystem.disableDepthTest();
+ PoseStack poseStack = guiGraphics.pose();
+ Matrix4f matrix4f = poseStack.last().pose();
+ Tesselator tesselator = Tesselator.getInstance();
+ BufferBuilder buffer = tesselator.getBuilder();
+ buffer.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR);
+
+ float x1 = centerX - radius - 5;
+ float y1 = centerY - radius - 5;
+ float x2 = centerX + radius + 5;
+ float y2 = centerY + radius + 5;
+ buffer.vertex(matrix4f, x1, y1, -200).color(color).endVertex();
+ buffer.vertex(matrix4f, x1, y2, -200).color(color).endVertex();
+ buffer.vertex(matrix4f, x2, y2, -200).color(color).endVertex();
+ buffer.vertex(matrix4f, x2, y1, -200).color(color).endVertex();
+
+ Window window = Minecraft.getInstance().getWindow();
+ float guiScale = (float) window.getGuiScale();
+ RenderSystem.setShader(Lib39Shaders::getSelectionShader);
+ System.out.println("Selection Effect Params:");
+ System.out.println(" Center: " + centerX + ", " + centerY);
+ System.out.println(" Radius: " + radius);
+ System.out.println(" GUI Scale: " + guiScale);
+ System.out.println(" Framebuffer: " + window.getWidth() + "x" + window.getHeight());
+ Lib39Shaders.getSelectionShader()
+ .safeGetUniform("Center")
+ .set(centerX * guiScale, centerY * guiScale);
+ Lib39Shaders.getSelectionShader()
+ .safeGetUniform("FramebufferSize")
+ .set((float) window.getWidth(), (float) window.getHeight());
+ Lib39Shaders.getSelectionShader()
+ .safeGetUniform("Radius")
+ .set(radius * guiScale);
+ Lib39Shaders.getSelectionShader()
+ .safeGetUniform("AntiAliasingRadius")
+ .set(guiScale); // 根据需要调整
+ RenderSystem.setShaderColor(1, 1, 1, 1);
+ BufferUploader.drawWithShader(Objects.requireNonNull(buffer.end()));
+ RenderSystem.enableDepthTest();
+ }
+
+ /**
+ * On closing.
+ */
+ public void onClosing() {
+ if (this.shouldRender() && !this.closingAnimationStarted) {
+ this.displayTime = System.currentTimeMillis();
+ this.closingAnimationStarted = true;
+ } else {
+ this.minecraft.setScreen(null);
+ }
+ }
+
+ @Override
+ protected void updateWidgetNarration(NarrationElementOutput narrationElementOutput) {
+ }
+
+ /**
+ * The type Wheel section.
+ */
+ public record WheelSection(
+ Vector2f center,
+ float angle,
+ float angleStart,
+ float angleEnd,
+ Component subTitle,
+ FourConsumer renderer
+ ) {
+ }
+}
diff --git a/src/main/java/top/r3944realms/lib39/client/renderer/RadialMenuRenderer.java b/src/main/java/top/r3944realms/lib39/client/renderer/RadialMenuRenderer.java
new file mode 100644
index 0000000..a20d515
--- /dev/null
+++ b/src/main/java/top/r3944realms/lib39/client/renderer/RadialMenuRenderer.java
@@ -0,0 +1,381 @@
+package top.r3944realms.lib39.client.renderer;
+
+import com.mojang.blaze3d.systems.RenderSystem;
+import com.mojang.blaze3d.vertex.*;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.GuiGraphics;
+import net.minecraft.client.renderer.GameRenderer;
+import net.minecraft.network.chat.Component;
+import net.minecraft.util.Mth;
+import net.minecraft.world.item.ItemStack;
+import org.joml.Matrix4f;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Function;
+
+/**
+ * 圆形径向菜单渲染器
+ * 用于创建美观的圆形选择菜单
+ *
+ * @param the type parameter
+ */
+public class RadialMenuRenderer {
+ /**
+ * The constant DEFAULT_INNER_RADIUS.
+ */
+// 默认配置常量
+ public static final float DEFAULT_INNER_RADIUS = 30f;
+ /**
+ * The constant DEFAULT_OUTER_RADIUS.
+ */
+ public static final float DEFAULT_OUTER_RADIUS = 80f;
+ /**
+ * The constant DEFAULT_MIDDLE_RADIUS.
+ */
+ public static final float DEFAULT_MIDDLE_RADIUS = 55f;
+ /**
+ * The constant DEFAULT_SEGMENTS.
+ */
+ public static final int DEFAULT_SEGMENTS = 64;
+
+ // 配置选项
+ private final float innerRadius;
+ private final float outerRadius;
+ private final float middleRadius;
+ private final int segments;
+ private final boolean enableHoverAnimation;
+ private final ColorScheme colorScheme;
+
+ // 状态
+ private int hoveredIndex = -1;
+ private final float[] hoverAnimations;
+ private long lastAnimationTime = 0;
+
+ /**
+ * The type Color scheme.
+ */
+ public static class ColorScheme {
+ /**
+ * The Normal color.
+ */
+ public final float[] normalColor;
+ /**
+ * The Hovered color.
+ */
+ public final float[] hoveredColor;
+ /**
+ * The Selected color.
+ */
+ public final float[] selectedColor;
+ /**
+ * The Background color.
+ */
+ public final float[] backgroundColor;
+
+ /**
+ * Instantiates a new Color scheme.
+ *
+ * @param normalColor the normal color
+ * @param hoveredColor the hovered color
+ * @param selectedColor the selected color
+ * @param backgroundColor the background color
+ */
+ public ColorScheme(float[] normalColor, float[] hoveredColor, float[] selectedColor, float[] backgroundColor) {
+ this.normalColor = normalColor;
+ this.hoveredColor = hoveredColor;
+ this.selectedColor = selectedColor;
+ this.backgroundColor = backgroundColor;
+ }
+
+ /**
+ * The constant DEFAULT.
+ */
+// 预定义颜色方案
+ public static final ColorScheme DEFAULT = new ColorScheme(
+ new float[]{0.3f, 0.3f, 0.8f, 0.6f}, // 正常 - 蓝色
+ new float[]{0.9f, 0.7f, 0.1f, 0.8f}, // 悬停 - 金色
+ new float[]{0.2f, 0.8f, 0.2f, 0.9f}, // 选中 - 绿色
+ new float[]{0.1f, 0.1f, 0.1f, 0.7f} // 背景
+ );
+
+ /**
+ * The constant FIRE.
+ */
+ public static final ColorScheme FIRE = new ColorScheme(
+ new float[]{0.8f, 0.3f, 0.1f, 0.6f}, // 正常 - 红色
+ new float[]{1.0f, 0.5f, 0.0f, 0.8f}, // 悬停 - 橙色
+ new float[]{1.0f, 0.9f, 0.0f, 0.9f}, // 选中 - 黄色
+ new float[]{0.2f, 0.1f, 0.0f, 0.7f} // 背景
+ );
+
+ /**
+ * The constant NATURE.
+ */
+ public static final ColorScheme NATURE = new ColorScheme(
+ new float[]{0.2f, 0.6f, 0.3f, 0.6f}, // 正常 - 绿色
+ new float[]{0.4f, 0.8f, 0.4f, 0.8f}, // 悬停 - 亮绿
+ new float[]{0.1f, 0.9f, 0.7f, 0.9f}, // 选中 - 青绿
+ new float[]{0.1f, 0.2f, 0.1f, 0.7f} // 背景
+ );
+ }
+
+
+ /**
+ * Instantiates a new Radial menu renderer.
+ */
+ public RadialMenuRenderer() {
+ this(DEFAULT_INNER_RADIUS, DEFAULT_OUTER_RADIUS, ColorScheme.DEFAULT);
+ }
+
+ /**
+ * Instantiates a new Radial menu renderer.
+ *
+ * @param innerRadius the inner radius
+ * @param outerRadius the outer radius
+ */
+ public RadialMenuRenderer(float innerRadius, float outerRadius) {
+ this(innerRadius, outerRadius, ColorScheme.DEFAULT);
+ }
+
+ /**
+ * Instantiates a new Radial menu renderer.
+ *
+ * @param innerRadius the inner radius
+ * @param outerRadius the outer radius
+ * @param colorScheme the color scheme
+ */
+ public RadialMenuRenderer(float innerRadius, float outerRadius, ColorScheme colorScheme) {
+ this(innerRadius, outerRadius, (innerRadius + outerRadius) / 2f, DEFAULT_SEGMENTS, true, colorScheme);
+ }
+
+ /**
+ * Instantiates a new Radial menu renderer.
+ *
+ * @param innerRadius the inner radius
+ * @param outerRadius the outer radius
+ * @param middleRadius the middle radius
+ * @param segments the segments
+ * @param enableHoverAnimation the enable hover animation
+ * @param colorScheme the color scheme
+ */
+ public RadialMenuRenderer(float innerRadius, float outerRadius, float middleRadius,
+ int segments, boolean enableHoverAnimation, ColorScheme colorScheme) {
+ this.innerRadius = innerRadius;
+ this.outerRadius = outerRadius;
+ this.middleRadius = middleRadius;
+ this.segments = segments;
+ this.enableHoverAnimation = enableHoverAnimation;
+ this.colorScheme = colorScheme;
+ this.hoverAnimations = new float[0];
+ }
+
+ /**
+ * 渲染圆形菜单
+ *
+ * @param guiGraphics the gui graphics
+ * @param entries the entries
+ * @param titleProvider the title provider
+ * @param iconProvider the icon provider
+ * @param selectedIndex the selected index
+ * @param trackMouse the track mouse
+ */
+ public void render(GuiGraphics guiGraphics, List entries,
+ Function titleProvider,
+ Function iconProvider,
+ int selectedIndex, boolean trackMouse) {
+ if (entries.isEmpty()) return;
+
+ // 更新动画状态
+ updateHoverAnimations(entries.size());
+
+ // 设置渲染状态
+ RenderSystem.enableBlend();
+ RenderSystem.defaultBlendFunc();
+ RenderSystem.setShader(GameRenderer::getPositionColorShader);
+
+ float centerX = guiGraphics.guiWidth() / 2f;
+ float centerY = guiGraphics.guiHeight() / 2f;
+
+ guiGraphics.pose().pushPose();
+ guiGraphics.pose().translate(centerX, centerY, 0f);
+
+ // 渲染所有扇形区域
+ renderSectors(guiGraphics, entries, selectedIndex);
+
+ // 渲染图标和文本
+ renderIconsAndText(guiGraphics, entries, titleProvider, iconProvider);
+
+ guiGraphics.pose().popPose();
+
+ RenderSystem.disableBlend();
+ }
+
+ /**
+ * 渲染扇形区域
+ */
+ private void renderSectors(GuiGraphics guiGraphics, List entries, int selectedIndex) {
+ int count = entries.size();
+ float angleSize = 360f / count;
+
+ for (int i = 0; i < count; i++) {
+ float startAngle = -90f + i * angleSize;
+ float currentOuterRadius = outerRadius;
+
+ // 悬停动画效果
+ if (enableHoverAnimation && i < hoverAnimations.length) {
+ currentOuterRadius += hoverAnimations[i] * 5f;
+ }
+
+ // 颜色设置
+ float[] color = getSectorColor(i, selectedIndex, entries.get(i));
+
+ // 绘制扇形
+ drawSector(guiGraphics, startAngle, angleSize, innerRadius, currentOuterRadius, color);
+ }
+ }
+
+
+ /**
+ * 获取扇形颜色
+ */
+ private float[] getSectorColor(int index, int selectedIndex, T entry) {
+ if (index == selectedIndex) {
+ return colorScheme.selectedColor; // 选中状态
+ } else if (index == hoveredIndex) {
+ return colorScheme.hoveredColor; // 悬停状态
+ } else {
+ return colorScheme.normalColor; // 普通状态
+ }
+ }
+
+ /**
+ * 绘制单个扇形
+ */
+ private void drawSector(GuiGraphics guiGraphics, float startAngle, float angleSize,
+ float innerRadius, float outerRadius, float[] color) {
+ BufferBuilder buffer = Tesselator.getInstance().getBuilder();
+ buffer.begin(VertexFormat.Mode.TRIANGLE_STRIP, DefaultVertexFormat.POSITION_COLOR);
+
+ Matrix4f matrix = guiGraphics.pose().last().pose();
+ float segments = Math.max(8, this.segments * (angleSize / 360f));
+
+ for (int i = 0; i <= segments; i++) {
+ float progress = i / segments;
+ float angle = startAngle + progress * angleSize;
+ float rad = angle * Mth.DEG_TO_RAD;
+
+ float cos = Mth.cos(rad);
+ float sin = Mth.sin(rad);
+
+ // 外圈顶点
+ buffer.vertex(matrix, outerRadius * cos, outerRadius * sin, 0)
+ .color(color[0], color[1], color[2], color[3]).endVertex();
+ // 内圈顶点
+ buffer.vertex(matrix, innerRadius * cos, innerRadius * sin, 0)
+ .color(color[0], color[1], color[2], color[3] * 0.6f).endVertex();
+ }
+
+ BufferUploader.drawWithShader(buffer.end());
+ }
+
+ /**
+ * 渲染图标和文本
+ */
+ private void renderIconsAndText(GuiGraphics guiGraphics, List entries,
+ Function titleProvider,
+ Function iconProvider) {
+ int count = entries.size();
+ var font = Minecraft.getInstance().font;
+
+ for (int i = 0; i < count; i++) {
+ T entry = entries.get(i);
+ float angle = (-90f + 360f * (i + 0.5f) / count) * Mth.DEG_TO_RAD;
+
+ // 计算位置
+ float x = Mth.cos(angle) * middleRadius;
+ float y = Mth.sin(angle) * middleRadius;
+
+ // 渲染图标
+ ItemStack icon = iconProvider.apply(entry);
+ if (!icon.isEmpty()) {
+ guiGraphics.renderItem(icon, (int)(x - 8), (int)(y - 8));
+ }
+
+ // 渲染文本
+ Component title = titleProvider.apply(entry);
+ guiGraphics.pose().pushPose();
+ guiGraphics.pose().translate(x, y + 12, 0);
+ guiGraphics.pose().scale(0.7f, 0.7f, 0.7f);
+ guiGraphics.drawString(font, title, -font.width(title) / 2, 0, 0xFFFFFF, true);
+ guiGraphics.pose().popPose();
+ }
+ }
+
+ /**
+ * 更新悬停动画
+ */
+ private void updateHoverAnimations(int entryCount) {
+ if (!enableHoverAnimation) return;
+
+ long currentTime = System.currentTimeMillis();
+ float deltaTime = Math.min((currentTime - lastAnimationTime) / 1000f, 0.1f);
+ lastAnimationTime = currentTime;
+
+ // 确保数组大小正确
+ if (hoverAnimations.length != entryCount) {
+ // 这里需要重新初始化数组,实际使用时应该处理数组大小变化
+ }
+
+ // 更新动画值
+ for (int i = 0; i < hoverAnimations.length && i < entryCount; i++) {
+ if (i == hoveredIndex) {
+ hoverAnimations[i] = Mth.clamp(hoverAnimations[i] + deltaTime * 2f, 0f, 1f);
+ } else {
+ hoverAnimations[i] = Mth.clamp(hoverAnimations[i] - deltaTime * 3f, 0f, 1f);
+ }
+ }
+ }
+
+ /**
+ * 获取鼠标下的条目索引
+ *
+ * @param entries the entries
+ * @param mouseX the mouse x
+ * @param mouseY the mouse y
+ * @return the hovered entry
+ */
+ public int getHoveredEntry(List entries, double mouseX, double mouseY) {
+ float centerX = Minecraft.getInstance().getWindow().getGuiScaledWidth() / 2f;
+ float centerY = Minecraft.getInstance().getWindow().getGuiScaledHeight() / 2f;
+
+ double relX = mouseX - centerX;
+ double relY = mouseY - centerY;
+ double distance = Math.sqrt(relX * relX + relY * relY);
+
+ // 检查是否在有效范围内
+ if (distance < innerRadius || distance > outerRadius) {
+ hoveredIndex = -1;
+ return -1;
+ }
+
+ // 计算角度
+ double angle = Math.atan2(relY, relX) * Mth.RAD_TO_DEG;
+ angle = (angle + 450) % 360; // 标准化到 0-360
+
+ int count = entries.size();
+ int index = (int) (angle / (360f / count)) % count;
+
+ hoveredIndex = index;
+ return index;
+ }
+
+ /**
+ * 清除状态
+ */
+ public void clearState() {
+ hoveredIndex = -1;
+ // 重置动画数组
+ Arrays.fill(hoverAnimations, 0f);
+ }
+}
diff --git a/src/main/java/top/r3944realms/lib39/client/shader/Lib39Shaders.java b/src/main/java/top/r3944realms/lib39/client/shader/Lib39Shaders.java
new file mode 100644
index 0000000..03b7f9b
--- /dev/null
+++ b/src/main/java/top/r3944realms/lib39/client/shader/Lib39Shaders.java
@@ -0,0 +1,73 @@
+package top.r3944realms.lib39.client.shader;
+
+import com.mojang.blaze3d.vertex.DefaultVertexFormat;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.ShaderInstance;
+import net.minecraftforge.client.event.RegisterShadersEvent;
+import top.r3944realms.lib39.Lib39;
+
+/**
+ * The type Lib 39 shaders.
+ */
+public class Lib39Shaders {
+ /**
+ * The Minecraft.
+ */
+ static final Minecraft MINECRAFT = Minecraft.getInstance();
+
+ /**
+ * Gets ring shader.
+ *
+ * @return the ring shader
+ */
+ public static ShaderInstance getRingShader() {
+ return ringShader;
+ }
+
+ /**
+ * The Ring shader.
+ */
+ static ShaderInstance ringShader;
+
+ /**
+ * Gets selection shader.
+ *
+ * @return the selection shader
+ */
+ public static ShaderInstance getSelectionShader() {
+ return selectionShader;
+ }
+
+ /**
+ * The Selection shader.
+ */
+ static ShaderInstance selectionShader;
+
+ /**
+ * Register.
+ *
+ * @param event the event
+ */
+ public static void register(RegisterShadersEvent event) {
+ try {
+ event.registerShader(
+ new ShaderInstance(
+ event.getResourceProvider(),
+ Lib39.rl("ring"),
+ DefaultVertexFormat.POSITION_COLOR
+ ),
+ it -> ringShader = it
+ );
+ event.registerShader(
+ new ShaderInstance(
+ event.getResourceProvider(),
+ Lib39.rl("selection"),
+ DefaultVertexFormat.POSITION_COLOR
+ ),
+ it -> selectionShader = it
+ );
+ } catch (Exception e) {
+ Lib39.LOGGER.error("Failed to register shader", e);
+ }
+ }
+}
diff --git a/src/main/java/top/r3944realms/lib39/core/event/ClientEventHandler.java b/src/main/java/top/r3944realms/lib39/core/event/ClientEventHandler.java
index f148493..bcb43b5 100644
--- a/src/main/java/top/r3944realms/lib39/core/event/ClientEventHandler.java
+++ b/src/main/java/top/r3944realms/lib39/core/event/ClientEventHandler.java
@@ -1,5 +1,11 @@
package top.r3944realms.lib39.core.event;
+import net.minecraftforge.api.distmarker.Dist;
+import net.minecraftforge.client.event.RegisterShadersEvent;
+import net.minecraftforge.eventbus.api.SubscribeEvent;
+import top.r3944realms.lib39.Lib39;
+import top.r3944realms.lib39.client.shader.Lib39Shaders;
+
/**
* The type Client handler.
*/
@@ -7,7 +13,18 @@ public class ClientEventHandler {
/**
* The type Mod.
*/
- public static class Mod extends ClientEventHandler {}
+ @net.minecraftforge.fml.common.Mod.EventBusSubscriber(value = Dist.CLIENT, bus = net.minecraftforge.fml.common.Mod.EventBusSubscriber.Bus.MOD, modid = Lib39.MOD_ID)
+ public static class Mod extends ClientEventHandler {
+ /**
+ * On register shaders.
+ *
+ * @param event the event
+ */
+ @SubscribeEvent
+ public static void onRegisterShaders(RegisterShadersEvent event) {
+ Lib39Shaders.register(event);
+ }
+ }
/**
* The type Game.
diff --git a/src/main/java/top/r3944realms/lib39/core/event/CommonEventHandler.java b/src/main/java/top/r3944realms/lib39/core/event/CommonEventHandler.java
index a4951fa..f383129 100644
--- a/src/main/java/top/r3944realms/lib39/core/event/CommonEventHandler.java
+++ b/src/main/java/top/r3944realms/lib39/core/event/CommonEventHandler.java
@@ -186,6 +186,7 @@ public class CommonEventHandler {
IEventBus gameBus = MinecraftForge.EVENT_BUS;
compatManager = new CompatManager(modBus, gameBus);
MinecraftForge.EVENT_BUS.post(new RegisterCompatEvent(compatManager));
+ compatManager.initializeAll();
});
}
diff --git a/src/main/java/top/r3944realms/lib39/datagen/provider/SimpleLootTableProvider.java b/src/main/java/top/r3944realms/lib39/datagen/provider/SimpleLootTableProvider.java
new file mode 100644
index 0000000..4bc1dcf
--- /dev/null
+++ b/src/main/java/top/r3944realms/lib39/datagen/provider/SimpleLootTableProvider.java
@@ -0,0 +1,42 @@
+package top.r3944realms.lib39.datagen.provider;
+
+import net.minecraft.data.PackOutput;
+import net.minecraft.data.loot.LootTableProvider;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.world.level.storage.loot.LootTable;
+import net.minecraft.world.level.storage.loot.ValidationContext;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * The type Simple loot table provider.
+ */
+public class SimpleLootTableProvider extends LootTableProvider {
+ /**
+ * Instantiates a new Simple loot table provider.
+ *
+ * @param output the output
+ * @param subProvidersWrapper the sub providers wrapper
+ */
+ public SimpleLootTableProvider(PackOutput output, @NotNull SubProvidersWrapper subProvidersWrapper) {
+ super(output, Set.of(), subProvidersWrapper.entries);
+ }
+
+ /**
+ * Instantiates a new Simple loot table provider.
+ *
+ * @param output the output
+ * @param requiredTables the required tables
+ * @param subProvidersWrapper the sub providers wrapper
+ */
+ public SimpleLootTableProvider(PackOutput output, Set requiredTables, @NotNull SubProvidersWrapper subProvidersWrapper) {
+ super(output, requiredTables, subProvidersWrapper.entries);
+ }
+
+ @Override
+ protected void validate(@NotNull Map map, @NotNull ValidationContext validationcontext) {
+ map.forEach((id, table) -> table.validate(validationcontext));
+ }
+}
diff --git a/src/main/java/top/r3944realms/lib39/datagen/provider/SubProvidersWrapper.java b/src/main/java/top/r3944realms/lib39/datagen/provider/SubProvidersWrapper.java
new file mode 100644
index 0000000..d32fe75
--- /dev/null
+++ b/src/main/java/top/r3944realms/lib39/datagen/provider/SubProvidersWrapper.java
@@ -0,0 +1,46 @@
+package top.r3944realms.lib39.datagen.provider;
+
+import net.minecraft.data.loot.LootTableProvider;
+import net.minecraft.world.level.storage.loot.parameters.LootContextParamSets;
+import top.r3944realms.lib39.datagen.provider.subprovider.BlockLootTables;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The type Sub providers wrapper.
+ */
+public class SubProvidersWrapper {
+ /**
+ * The Entries.
+ */
+ public List entries = new ArrayList<>();
+
+ /**
+ * Instantiates a new Sub providers wrapper.
+ */
+ public SubProvidersWrapper() {}
+
+ /**
+ * Add entry sub providers wrapper.
+ *
+ * @param entry the entry
+ * @return the sub providers wrapper
+ */
+ public SubProvidersWrapper addEntry(LootTableProvider.SubProviderEntry entry) {
+ entries.add(entry);
+ return this;
+ }
+
+ /**
+ * Add block entry sub providers wrapper.
+ *
+ * @param blockLootTables the block loot tables
+ * @return the sub providers wrapper
+ */
+ public SubProvidersWrapper addBlockEntry(BlockLootTables blockLootTables) {
+ entries.add(new LootTableProvider.SubProviderEntry(() -> blockLootTables, LootContextParamSets.BLOCK));
+ return this;
+ }
+
+}
diff --git a/src/main/java/top/r3944realms/lib39/datagen/provider/subprovider/BlockLootTables.java b/src/main/java/top/r3944realms/lib39/datagen/provider/subprovider/BlockLootTables.java
new file mode 100644
index 0000000..0329a67
--- /dev/null
+++ b/src/main/java/top/r3944realms/lib39/datagen/provider/subprovider/BlockLootTables.java
@@ -0,0 +1,373 @@
+package top.r3944realms.lib39.datagen.provider.subprovider;
+
+import net.minecraft.advancements.critereon.ItemPredicate;
+import net.minecraft.data.loot.BlockLootSubProvider;
+import net.minecraft.world.flag.FeatureFlags;
+import net.minecraft.world.item.Item;
+import net.minecraft.world.item.enchantment.Enchantments;
+import net.minecraft.world.level.ItemLike;
+import net.minecraft.world.level.block.Block;
+import net.minecraft.world.level.block.FlowerPotBlock;
+import net.minecraft.world.level.block.state.properties.Property;
+import net.minecraft.world.level.storage.loot.LootPool;
+import net.minecraft.world.level.storage.loot.LootTable;
+import net.minecraft.world.level.storage.loot.entries.LootItem;
+import net.minecraft.world.level.storage.loot.functions.ApplyBonusCount;
+import net.minecraft.world.level.storage.loot.functions.SetItemCountFunction;
+import net.minecraft.world.level.storage.loot.predicates.LootItemBlockStatePropertyCondition;
+import net.minecraft.world.level.storage.loot.predicates.MatchTool;
+import net.minecraft.world.level.storage.loot.providers.number.ConstantValue;
+import net.minecraft.world.level.storage.loot.providers.number.UniformGenerator;
+import net.minecraftforge.registries.RegistryObject;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Function;
+
+/**
+ * The type Block loot tables.
+ */
+@SuppressWarnings("unused")
+public class BlockLootTables extends BlockLootSubProvider {
+
+ private final List blockEntries = new ArrayList<>();
+
+ /**
+ * Instantiates a new Block loot tables.
+ */
+ public BlockLootTables() {
+ super(Set.of(), FeatureFlags.REGISTRY.allFlags());
+ }
+
+ // ==================== 流畅 API 构建方法 ====================
+
+ /**
+ * 添加自掉落的方块
+ *
+ * @param block the block
+ */
+ public void dropSelf(RegistryObject block) {
+ addEntry(block, this::createSingleItemTable);
+ }
+
+ /**
+ * 批量添加自掉落的方块
+ *
+ * @param blocks the blocks
+ * @return the block loot tables
+ */
+ @Contract("_ -> this")
+ @SafeVarargs
+ public final BlockLootTables dropSelf(RegistryObject @NotNull ... blocks) {
+ for (RegistryObject block : blocks) {
+ dropSelf(block);
+ }
+ return this;
+ }
+
+ /**
+ * 添加需要丝绸之触才掉落的方块
+ *
+ * @param block the block
+ * @return the block loot tables
+ */
+ public BlockLootTables dropWhenSilkTouch(RegistryObject block) {
+ return addEntry(block, BlockLootSubProvider::createSilkTouchOnlyTable);
+ }
+
+ /**
+ * 添加掉落其他物品的方块
+ *
+ * @param block the block
+ * @param item the item
+ * @return the block loot tables
+ */
+ public BlockLootTables dropOther(RegistryObject block, RegistryObject extends ItemLike> item) {
+ return addEntry(block, pBlock -> this.createSingleItemTable(item.get()));
+ }
+
+ /**
+ * 添加只能被剪子剪下的方块
+ *
+ * @param block the block
+ * @return the block loot tables
+ */
+ public BlockLootTables dropWhenShears(RegistryObject block) {
+ return addEntry(block, BlockLootSubProvider::createShearsOnlyDrop);
+ }
+
+ /**
+ * 添加矿物的掉落表(支持时运附魔)
+ *
+ * @param block the block
+ * @param oreItem the ore item
+ * @return the block loot tables
+ */
+ public BlockLootTables dropOre(RegistryObject block, RegistryObject- oreItem) {
+ return addEntry(block, b -> this.createOreDrop(b, oreItem.get()));
+ }
+
+ /**
+ * 添加红石矿石掉落表
+ *
+ * @param block the block
+ * @return the block loot tables
+ */
+ public BlockLootTables dropRedstoneOre(RegistryObject block) {
+ return addEntry(block, this::createRedstoneOreDrops);
+ }
+
+ /**
+ * 添加青金石矿石掉落表
+ *
+ * @param block the block
+ * @return the block loot tables
+ */
+ public BlockLootTables dropLapisOre(RegistryObject block) {
+ return addEntry(block, this::createLapisOreDrops);
+ }
+
+ /**
+ * 添加铜矿石掉落表
+ *
+ * @param block the block
+ * @return the block loot tables
+ */
+ public BlockLootTables dropCopperOre(RegistryObject block) {
+ return addEntry(block, this::createCopperOreDrops);
+ }
+
+ /**
+ * 添加地毯类方块的掉落(一次掉落2个)
+ *
+ * @param block the block
+ * @return the block loot tables
+ */
+ public BlockLootTables dropCarpet(RegistryObject block) {
+ return addEntry(block, b -> LootTable.lootTable()
+ .withPool(LootPool.lootPool()
+ .setRolls(ConstantValue.exactly(1))
+ .add(LootItem.lootTableItem(b)
+ .apply(SetItemCountFunction.setCount(ConstantValue.exactly(2.0F))))));
+ }
+
+ /**
+ * 添加台阶方块的掉落
+ *
+ * @param block the block
+ * @return the block loot tables
+ */
+ public BlockLootTables dropSlab(RegistryObject block) {
+ return addEntry(block, this::createSlabItemTable);
+ }
+
+ /**
+ * 添加门方块的掉落(只掉落下半部分)
+ *
+ * @param block the block
+ * @return the block loot tables
+ */
+ public BlockLootTables dropDoor(RegistryObject block) {
+ return addEntry(block, this::createDoorTable);
+ }
+
+ /**
+ * 添加花盆的掉落
+ *
+ * @param block the block
+ * @return the block loot tables
+ */
+ public BlockLootTables dropFlowerPot(RegistryObject block) {
+ return addEntry(block, (pBlock) -> this.createPotFlowerItemTable(((FlowerPotBlock)pBlock).getContent()));
+ }
+
+ /**
+ * 添加树叶的掉落
+ *
+ * @param leavesBlock the leaves block
+ * @param saplingBlock the sapling block
+ * @param chances the chances
+ * @return the block loot tables
+ */
+ public BlockLootTables dropLeaves(RegistryObject leavesBlock,
+ RegistryObject saplingBlock,
+ float... chances) {
+ return addEntry(leavesBlock, b -> this.createLeavesDrops(b, saplingBlock.get(), chances));
+ }
+
+ /**
+ * 添加橡树叶的掉落(包含苹果)
+ *
+ * @param leavesBlock the leaves block
+ * @param saplingBlock the sapling block
+ * @param chances the chances
+ * @return the block loot tables
+ */
+ public BlockLootTables dropOakLeaves(RegistryObject leavesBlock,
+ RegistryObject saplingBlock,
+ float... chances) {
+ return addEntry(leavesBlock, b -> this.createOakLeavesDrops(b, saplingBlock.get(), chances));
+ }
+
+ /**
+ * 添加农作物的掉落
+ *
+ * @param cropBlock the crop block
+ * @param cropItem the crop item
+ * @param seedsItem the seeds item
+ * @param ageProperty the age property
+ * @param maxAge the max age
+ * @return the block loot tables
+ */
+ public BlockLootTables dropCrop(RegistryObject cropBlock,
+ RegistryObject
- cropItem,
+ RegistryObject
- seedsItem,
+ Property ageProperty,
+ int maxAge) {
+ return addEntry(cropBlock, b -> this.createCropDrops(
+ b,
+ cropItem.get(),
+ seedsItem.get(),
+ LootItemBlockStatePropertyCondition.hasBlockStateProperties(b)
+ .setProperties(net.minecraft.advancements.critereon.StatePropertiesPredicate.Builder.properties()
+ .hasProperty(ageProperty, maxAge))
+ ));
+ }
+
+ /**
+ * 自定义掉落表
+ *
+ * @param block the block
+ * @param factory the factory
+ * @return the block loot tables
+ */
+ public BlockLootTables custom(RegistryObject block, Function factory) {
+ return addEntry(block, factory);
+ }
+
+ /**
+ * 没有掉落
+ *
+ * @param block the block
+ * @return the block loot tables
+ */
+ public BlockLootTables noDrop(RegistryObject block) {
+ return addEntry(block, b -> noDrop());
+ }
+
+ // ==================== 批量操作方法 ====================
+
+ /**
+ * 批量操作一系列方块
+ *
+ * @param blocks the blocks
+ * @param operation the operation
+ * @return the block loot tables
+ */
+ public BlockLootTables batch(@NotNull Iterable> blocks,
+ Function, BlockLootTables> operation) {
+ for (RegistryObject block : blocks) {
+ operation.apply(block);
+ }
+ return this;
+ }
+
+ /**
+ * 对一组方块应用相同的操作
+ *
+ * @param operation the operation
+ * @param blocks the blocks
+ * @return the block loot tables
+ */
+ @Contract("_, _ -> this")
+ @SafeVarargs
+ public final BlockLootTables applyToAll(Function, BlockLootTables> operation,
+ RegistryObject @NotNull ... blocks) {
+ for (RegistryObject block : blocks) {
+ operation.apply(block);
+ }
+ return this;
+ }
+
+ // ==================== 构建方法 ====================
+
+ /**
+ * 构建并返回自身(用于流畅API链式调用)
+ *
+ * @return the block loot tables
+ */
+ public BlockLootTables build() {
+ return this;
+ }
+
+ @Override
+ protected void generate() {
+ for (BlockEntry entry : blockEntries) {
+ this.add(entry.block.get(), entry.factory);
+ }
+ }
+
+ // ==================== 内部类和方法 ====================
+
+ private BlockLootTables addEntry(RegistryObject block, Function factory) {
+ blockEntries.add(new BlockEntry(block, factory));
+ return this;
+ }
+
+ private record BlockEntry(RegistryObject block, Function factory) {}
+
+ // ==================== 静态工厂方法 ====================
+
+ /**
+ * 创建新的战利品表生成器
+ *
+ * @return the block loot tables
+ */
+ @Contract(" -> new")
+ public static @NotNull BlockLootTables create() {
+ return new BlockLootTables();
+ }
+
+ /**
+ * 快速创建基本矿物的掉落表
+ *
+ * @param oreBlock the ore block
+ * @param oreItem the ore item
+ * @return the loot table .@ not null builder
+ */
+ public static LootTable.@NotNull Builder simpleOreDrop(Block oreBlock, Item oreItem) {
+ return LootTable.lootTable()
+ .withPool(LootPool.lootPool()
+ .setRolls(ConstantValue.exactly(1))
+ .add(LootItem.lootTableItem(oreItem)
+ .when(MatchTool.toolMatches(ItemPredicate.Builder.item()
+ .hasEnchantment(new net.minecraft.advancements.critereon.EnchantmentPredicate(
+ Enchantments.SILK_TOUCH,
+ net.minecraft.advancements.critereon.MinMaxBounds.Ints.atLeast(1)
+ ))).invert())
+ .apply(SetItemCountFunction.setCount(UniformGenerator.between(1, 1)))
+ .apply(ApplyBonusCount.addOreBonusCount(Enchantments.BLOCK_FORTUNE))));
+ }
+
+ /**
+ * 快速创建石质方块的掉落表(需要镐子)
+ *
+ * @param stoneBlock the stone block
+ * @param dropItem the drop item
+ * @return the loot table .@ not null builder
+ */
+ public static LootTable.@NotNull Builder stoneDrop(Block stoneBlock, Item dropItem) {
+ return LootTable.lootTable()
+ .withPool(LootPool.lootPool()
+ .setRolls(ConstantValue.exactly(1))
+ .add(LootItem.lootTableItem(dropItem)
+ .when(MatchTool.toolMatches(ItemPredicate.Builder.item()
+ .hasEnchantment(new net.minecraft.advancements.critereon.EnchantmentPredicate(
+ Enchantments.SILK_TOUCH,
+ net.minecraft.advancements.critereon.MinMaxBounds.Ints.atLeast(1)
+ ))))));
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/top/r3944realms/lib39/example/client/screen/ForgeScreen.java b/src/main/java/top/r3944realms/lib39/example/client/screen/ForgeScreen.java
new file mode 100644
index 0000000..3d4c634
--- /dev/null
+++ b/src/main/java/top/r3944realms/lib39/example/client/screen/ForgeScreen.java
@@ -0,0 +1,127 @@
+package top.r3944realms.lib39.example.client.screen;
+
+import com.mojang.blaze3d.vertex.PoseStack;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.GuiGraphics;
+import net.minecraft.client.gui.components.Renderable;
+import net.minecraft.client.gui.screens.Screen;
+import net.minecraft.client.player.LocalPlayer;
+import net.minecraft.network.chat.Component;
+import net.minecraft.world.InteractionHand;
+import net.minecraft.world.item.ItemStack;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+import org.joml.Vector2f;
+import top.r3944realms.lib39.client.gui.component.WheelWidget;
+import top.r3944realms.lib39.util.lang.FourConsumer;
+import top.r3944realms.lib39.util.lang.Pair;
+
+import java.util.List;
+import java.util.Objects;
+
+import static top.r3944realms.lib39.client.gui.component.WheelWidget.IGNORE_CURSOR_MOVE_LENGTH;
+
+/**
+ * The type Forge screen.
+ */
+public class ForgeScreen extends Screen {
+ private final LocalPlayer player = Objects.requireNonNull(Minecraft.getInstance().player);
+ private final InteractionHand hand;
+ private final int mode;
+
+ /**
+ * The Wheel.
+ */
+ public WheelWidget wheel;
+
+ /**
+ * Instantiates a new Forge screen.
+ *
+ * @param hand the hand
+ * @param mode the mode
+ */
+ public ForgeScreen(InteractionHand hand, int mode) {
+ super(Component.literal("Test"));
+ this.hand = hand;
+ this.mode = mode;
+ }
+
+ @Override
+ protected void init() {
+ int leftPos = (this.width - 75) / 2;
+ int topPos = (this.height - 75) / 2;
+ ItemStack holding = player.getItemInHand(this.hand);
+ WheelWidget wheel = new WheelWidget(
+ leftPos, topPos, 75, 75,
+ 12.5f, 32.5f, 0.75f,
+ List.of(
+ Pair.of(
+ Component.literal("auto"),
+ renderItem(holding)),
+ Pair.of(
+ Component.literal("axe"),
+ renderItem(holding)),
+ Pair.of(
+ Component.literal("shovel"),
+ renderItem(holding)),
+ Pair.of(
+ Component.literal("hoe"),
+ renderItem(holding)),
+ Pair.of(
+ Component.literal("pickaxe"),
+ renderItem(holding))
+ )
+ ).setCurrentIndex(this.wheel != null ? this.wheel.getCurrentSectionIndex() : this.mode);
+ this.clearWidgets();
+ this.wheel = this.addRenderableWidget(wheel);
+ }
+
+ @Contract(pure = true)
+ private static @NotNull FourConsumer renderItem(ItemStack holding) {
+ return (graphics, pose, width, height) -> {
+ ItemStack stack = holding.copy();
+ graphics.renderItem(stack, 2, 2, 9910597);
+ };
+ }
+ @Override
+ public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) {
+ if (wheel != null && wheel.isClosingAnimationStarted()) return true;
+ float screenCenterX = this.width / 2f;
+ float screenCenterY = this.height / 2f;
+ Vector2f cursorVec2 = new Vector2f(
+ (float) mouseX - screenCenterX,
+ (float) mouseY - screenCenterY
+ );
+ if (cursorVec2.length() < IGNORE_CURSOR_MOVE_LENGTH) {
+ return true;
+ }
+ return super.mouseDragged(mouseX, mouseY, button, dragX, dragY);
+ }
+
+ @Override
+ public boolean mouseReleased(double mouseX, double mouseY, int button) {
+ if (wheel != null ) {
+ wheel.onClosing();
+ }
+ return super.mouseReleased(mouseX, mouseY, button);
+ }
+
+ @Override
+ public void removed() {
+ super.removed();
+ }
+
+
+ @Override
+ public void render(@NotNull GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) {
+ for (Renderable renderable : this.renderables) {
+ renderable.render(guiGraphics, mouseX, mouseY, partialTick);
+ }
+ }
+
+ @Override
+ public boolean isPauseScreen() {
+ return false;
+ }
+
+}
diff --git a/src/main/java/top/r3944realms/lib39/example/content/item/ForgeItem.java b/src/main/java/top/r3944realms/lib39/example/content/item/ForgeItem.java
new file mode 100644
index 0000000..8c47461
--- /dev/null
+++ b/src/main/java/top/r3944realms/lib39/example/content/item/ForgeItem.java
@@ -0,0 +1,33 @@
+package top.r3944realms.lib39.example.content.item;
+
+import net.minecraft.client.Minecraft;
+import net.minecraft.world.InteractionHand;
+import net.minecraft.world.InteractionResultHolder;
+import net.minecraft.world.entity.player.Player;
+import net.minecraft.world.item.Item;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.level.Level;
+import org.jetbrains.annotations.NotNull;
+import top.r3944realms.lib39.example.client.screen.ForgeScreen;
+
+/**
+ * The type Forge item.
+ */
+public class ForgeItem extends Item {
+ /**
+ * Instantiates a new Forge item.
+ *
+ * @param properties the properties
+ */
+ public ForgeItem(Properties properties) {
+ super(properties);
+ }
+
+ @Override
+ public @NotNull InteractionResultHolder use(@NotNull Level level, @NotNull Player player, @NotNull InteractionHand usedHand) {
+ if (level.isClientSide() && usedHand == InteractionHand.MAIN_HAND) {
+ Minecraft.getInstance().setScreen(new ForgeScreen(usedHand, 0));
+ }
+ return super.use(level, player, usedHand);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/top/r3944realms/lib39/example/core/register/ExLib39Items.java b/src/main/java/top/r3944realms/lib39/example/core/register/ExLib39Items.java
index de21c86..78e9561 100644
--- a/src/main/java/top/r3944realms/lib39/example/core/register/ExLib39Items.java
+++ b/src/main/java/top/r3944realms/lib39/example/core/register/ExLib39Items.java
@@ -7,6 +7,7 @@ import net.minecraftforge.registries.ForgeRegistries;
import net.minecraftforge.registries.RegistryObject;
import top.r3944realms.lib39.Lib39;
import top.r3944realms.lib39.example.content.item.FabricItem;
+import top.r3944realms.lib39.example.content.item.ForgeItem;
import top.r3944realms.lib39.example.content.item.NeoForgeItem;
/**
@@ -38,6 +39,16 @@ public class ExLib39Items {
.stacksTo(1)
.fireResistant()
));
+ /**
+ * The constant FORGE.
+ */
+ public static final RegistryObject
- FORGE =
+ ITEMS.register("forge",
+ () -> new ForgeItem(
+ new Item.Properties()
+ .stacksTo(1)
+ .fireResistant()
+ ));
/**
* Register.
diff --git a/src/main/java/top/r3944realms/lib39/example/datagen/data/ExLib39LangKeys.java b/src/main/java/top/r3944realms/lib39/example/datagen/data/ExLib39LangKeys.java
index 48c65a0..392b35a 100644
--- a/src/main/java/top/r3944realms/lib39/example/datagen/data/ExLib39LangKeys.java
+++ b/src/main/java/top/r3944realms/lib39/example/datagen/data/ExLib39LangKeys.java
@@ -40,6 +40,10 @@ public enum ExLib39LangKeys implements ILangKeyValueCollection {
ExLib39Items.NEOFORGE, ModPartEnum.ITEM,
"NeoForge", "小狐狸", "狐狸", "狸", true
));
+ addLang(LangKeyValue.ofSupplier(
+ ExLib39Items.FORGE, ModPartEnum.ITEM,
+ "Forge", "铁砧", "铁砧", "砧", true
+ ));
}
diff --git a/src/main/java/top/r3944realms/lib39/util/MathUtil.java b/src/main/java/top/r3944realms/lib39/util/MathUtil.java
new file mode 100644
index 0000000..35a5580
--- /dev/null
+++ b/src/main/java/top/r3944realms/lib39/util/MathUtil.java
@@ -0,0 +1,195 @@
+package top.r3944realms.lib39.util;
+
+import it.unimi.dsi.fastutil.ints.Int2DoubleMap;
+import it.unimi.dsi.fastutil.ints.Int2DoubleOpenHashMap;
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.Direction;
+import net.minecraft.core.Vec3i;
+import org.joml.Vector2f;
+
+import static java.lang.Math.*;
+
+/**
+ * 直接拿铁砧工艺的
+ */
+public class MathUtil {
+ /**
+ * Calc a vector2 that equals to a vector2 rotated an angle
+ *
+ * @param v origin vector, wont be changed
+ * @param deg angle rotated, in degrees
+ * @return rotated vector2
+ */
+ public static Vector2f rotationDegrees(Vector2f v, float deg) {
+ return rotate(v, (float) toRadians(deg));
+ }
+
+ /**
+ * Calc a vector2 that equals to a vector2 rotated an angle
+ *
+ * @param v origin vector, wont be changed
+ * @param d angle rotated, in radians
+ * @return rotated vector2
+ */
+ public static Vector2f rotate(Vector2f v, float d) {
+ return new Vector2f(
+ (float) (v.x * cos(d) - v.y * sin(d)),
+ (float) (v.x * sin(d) + v.y * cos(d))
+ );
+ }
+
+ /**
+ * Copy vector 2 f.
+ *
+ * @param v the v
+ * @return the vector 2 f
+ */
+ public static Vector2f copy(Vector2f v) {
+ return new Vector2f(v.x, v.y);
+ }
+
+ /**
+ * Angle float.
+ *
+ * @param from the from
+ * @param to the to
+ * @return Angle in radians
+ */
+ public static float angle(Vector2f from, Vector2f to) {
+ return (float) ((atan2(to.y, to.x) - atan2(from.y, from.x)) % (Math.PI * 2));
+ }
+
+ /**
+ * Angle degrees float.
+ *
+ * @param from the from
+ * @param to the to
+ * @return Angle in degrees
+ */
+ public static float angleDegrees(Vector2f from, Vector2f to) {
+ return (float) toDegrees(angle(from, to));
+ }
+
+ /**
+ * Safe divide float.
+ *
+ * @param a the a
+ * @param b the b
+ * @return the float
+ */
+ public static float safeDivide(float a, float b) {
+ if (a == b) return 1;
+ return a / b;
+ }
+
+ /**
+ * Is in range boolean.
+ *
+ * @param value the value
+ * @param min the min
+ * @param max the max
+ * @return the boolean
+ */
+ public static boolean isInRange(double value, double min, double max) {
+ if (min > max) {
+ double min1 = min;
+ min = max;
+ max = min1;
+ }
+
+ return value > min && value < max;
+ }
+
+ /**
+ * Is in range boolean.
+ *
+ * @param valueX the value x
+ * @param valueY the value y
+ * @param minX the min x
+ * @param minY the min y
+ * @param maxX the max x
+ * @param maxY the max y
+ * @return the boolean
+ */
+ public static boolean isInRange(double valueX, double valueY, double minX, double minY, double maxX, double maxY) {
+ if (minX > maxX) {
+ double minX1 = minX;
+ minX = maxX;
+ maxX = minX1;
+ }
+ if (minY > maxY) {
+ double minY1 = minY;
+ minY = maxY;
+ maxY = minY1;
+ }
+
+ return valueX > minX && valueX < maxX && valueY > minY && valueY < maxY;
+ }
+
+ /**
+ * Dist vec 3 i.
+ *
+ * @param a the a
+ * @param b the b
+ * @return the vec 3 i
+ */
+ public static Vec3i dist(BlockPos a, BlockPos b) {
+ return new Vec3i(a.getX() - b.getX(), a.getY() - b.getY(), a.getZ() - b.getZ());
+ }
+
+ /**
+ * Gets direction.
+ *
+ * @param from the from
+ * @param to the to
+ * @return the direction
+ */
+ public static Direction getDirection(BlockPos from, BlockPos to) {
+ return Direction.fromDelta(from.getX() - to.getX(), from.getY() - to.getY(), from.getZ() - to.getZ());
+ }
+
+ private static final Int2DoubleMap FACTORIAL_CACHE = new Int2DoubleOpenHashMap();
+
+ /**
+ * Factorial double.
+ *
+ * @param value the value
+ * @return the double
+ */
+ public static double factorial(int value) {
+ if (value < 1) return 1;
+ if (FACTORIAL_CACHE.containsKey(value)) return FACTORIAL_CACHE.get(value);
+ double result = 1;
+ for (int i = 2; i <= value; i++) {
+ result *= i;
+ }
+ FACTORIAL_CACHE.put(value, result);
+ return result;
+ }
+
+ /**
+ * Clamp with proportion float.
+ *
+ * @param value the value
+ * @param min the min
+ * @param max the max
+ * @return the float
+ */
+ public static float clampWithProportion(float value, float min, float max) {
+ float length = Math.abs(max - min);
+ if (length == 0) throw new IllegalArgumentException("The min value " + min + " cannot be equal to the max value" + max + "!");
+
+ if (value > max) {
+ while (value > max + length) {
+ value -= length;
+ }
+ return max - (max - value);
+ } else if (value < min) {
+ while (value < min + length) {
+ value += length;
+ }
+ return min + (value - min);
+ }
+ return value;
+ }
+}
diff --git a/src/main/java/top/r3944realms/lib39/util/command/CommandHelpHelper.java b/src/main/java/top/r3944realms/lib39/util/command/CommandHelpHelper.java
new file mode 100644
index 0000000..dc80535
--- /dev/null
+++ b/src/main/java/top/r3944realms/lib39/util/command/CommandHelpHelper.java
@@ -0,0 +1,7 @@
+package top.r3944realms.lib39.util.command;
+
+/**
+ * The type Command help helper.
+ */
+public class CommandHelpHelper {
+}
diff --git a/src/main/java/top/r3944realms/lib39/util/lang/FourConsumer.java b/src/main/java/top/r3944realms/lib39/util/lang/FourConsumer.java
new file mode 100644
index 0000000..fbe2512
--- /dev/null
+++ b/src/main/java/top/r3944realms/lib39/util/lang/FourConsumer.java
@@ -0,0 +1,36 @@
+package top.r3944realms.lib39.util.lang;
+
+/**
+ * The interface Four consumer.
+ *
+ * @param the type parameter
+ * @param the type parameter
+ * @param the type parameter
+ * @param the type parameter
+ */
+public interface FourConsumer {
+ /**
+ * Accept.
+ *
+ * @param x the x
+ * @param y the y
+ * @param z the z
+ * @param w the w
+ */
+ void accept(X x, Y y, Z z, W w);
+
+ /**
+ * Noop.
+ *
+ * @param the type parameter
+ * @param the type parameter
+ * @param the type parameter
+ * @param the type parameter
+ * @param x the x
+ * @param y the y
+ * @param z the z
+ * @param w the w
+ */
+ static void noop(X x, Y y, Z z, W w) {
+ }
+}
diff --git a/src/main/java/top/r3944realms/lib39/util/nbt/NBTWriter.java b/src/main/java/top/r3944realms/lib39/util/nbt/NBTWriter.java
index 7235330..d70cdc8 100644
--- a/src/main/java/top/r3944realms/lib39/util/nbt/NBTWriter.java
+++ b/src/main/java/top/r3944realms/lib39/util/nbt/NBTWriter.java
@@ -585,6 +585,21 @@ public class NBTWriter {
return this;
}
+ /**
+ * Uuid value if nbt writer.
+ *
+ * @param key the key
+ * @param value the value
+ * @param condition the condition
+ * @return the nbt writer
+ */
+ public NBTWriter uuidValueIf(String key, UUID value, boolean condition) {
+ if (condition && value != null) {
+ root.putUUID(key, value);
+ }
+ return this;
+ }
+
/**
* Boolean value if nbt writer.
*
diff --git a/src/main/java/top/r3944realms/lib39/util/resolve/EntityListResolve.java b/src/main/java/top/r3944realms/lib39/util/resolve/EntityListResolve.java
new file mode 100644
index 0000000..6d786ed
--- /dev/null
+++ b/src/main/java/top/r3944realms/lib39/util/resolve/EntityListResolve.java
@@ -0,0 +1,161 @@
+package top.r3944realms.lib39.util.resolve;
+
+import net.minecraft.core.registries.Registries;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.tags.TagKey;
+import net.minecraft.world.entity.EntityType;
+import org.jetbrains.annotations.NotNull;
+import top.r3944realms.lib39.Lib39;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The type Entity list resolve.
+ */
+public abstract class EntityListResolve {
+ /**
+ * The Result.
+ */
+ protected EntityListResolve.EntityResolveResult result;
+
+ /**
+ * The type Entity resolve result.
+ */
+ public static class EntityResolveResult {
+ /**
+ * The Entity list.
+ */
+ protected final List entityList = new ArrayList<>();
+ /**
+ * The Tag list.
+ */
+ protected final List tagList = new ArrayList<>();
+ /**
+ * The Mod list.
+ */
+ protected final List modList = new ArrayList<>();
+
+ /**
+ * The enum Type.
+ */
+ public enum Type {
+ /**
+ * Entity type.
+ */
+ ENTITY,
+ /**
+ * Tag type.
+ */
+ TAG,
+ /**
+ * Mod type.
+ */
+ MOD
+ }
+
+ /**
+ * Gets map.
+ *
+ * @param type the type
+ * @return the map
+ */
+ public List getMap(@NotNull Type type) {
+ return switch (type) {
+ case ENTITY -> entityList;
+ case TAG -> tagList;
+ case MOD -> modList;
+ };
+ }
+
+ /**
+ * Update.
+ *
+ * @param entity the entity
+ * @param tag the tag
+ * @param mod the mod
+ */
+ public void update(List entity, List tag, List mod) {
+ entityList.clear();
+ entityList.addAll(entity);
+ tagList.clear();
+ tagList.addAll(tag);
+ modList.clear();
+ modList.addAll(mod);
+ }
+ }
+
+ /**
+ * Resolve entity resolve result.
+ *
+ * @param configs the configs
+ * @return the entity resolve result
+ */
+ public EntityResolveResult resolve(@NotNull List configs) {
+ List entityList = new ArrayList<>();
+ List tagList = new ArrayList<>();
+ List modList = new ArrayList<>();
+ for (String config : configs) {
+ if (!isMatch(config)) continue;
+
+ try {
+ String[] entities = resolveEntities(config);
+ for (String e : entities) {
+ String trimmed = e.trim();
+ if (trimmed.equals("*")) modList.add("*");
+ else if (trimmed.startsWith("#")) {
+ String body = trimmed.substring(1).trim();
+ if (body.contains(":")) tagList.add(body);
+ else modList.add(body);
+ } else entityList.add(trimmed);
+ }
+ result.update(entityList, tagList, modList);
+ } catch (NumberFormatException ex) {
+ Lib39.LOGGER.error("Invalid offset config: {}", config);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Is match boolean.
+ *
+ * @param input the input
+ * @return the boolean
+ */
+ protected abstract boolean isMatch(String input);
+
+ /**
+ * Resolve entities string [ ].
+ *
+ * @param input the input
+ * @return the string [ ]
+ */
+ protected abstract String[] resolveEntities(String input);
+
+ /**
+ * Is entity in list boolean.
+ *
+ * @param type the type
+ * @return the boolean
+ */
+ @SuppressWarnings("deprecation")
+ public boolean isEntityInList(EntityType> type) {
+ String entityId = type.builtInRegistryHolder().key().location().toString();
+ String modId = entityId.split(":")[0];
+ for (String rs : result.entityList) {
+ if (rs.equals(entityId)) return true;
+ }
+ for(String rs : result.tagList) {
+ String body = rs.substring(1);
+ ResourceLocation tagId = new ResourceLocation(body);
+ TagKey> tag = TagKey.create(Registries.ENTITY_TYPE, tagId);
+ if (type.builtInRegistryHolder().is(tag)) return true;
+ }
+ for(String rs : result.modList) {
+ String body = rs.substring(1);
+ if (!body.contains(":") && body.equals(modId)) return true;
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/top/r3944realms/lib39/util/resolve/EntityMapResolve.java b/src/main/java/top/r3944realms/lib39/util/resolve/EntityMapResolve.java
new file mode 100644
index 0000000..059eb2a
--- /dev/null
+++ b/src/main/java/top/r3944realms/lib39/util/resolve/EntityMapResolve.java
@@ -0,0 +1,214 @@
+package top.r3944realms.lib39.util.resolve;
+
+import net.minecraft.core.registries.Registries;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.tags.TagKey;
+import net.minecraft.world.entity.EntityType;
+import org.jetbrains.annotations.NotNull;
+import top.r3944realms.lib39.Lib39;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * The type Entity map resolve.
+ *
+ * @param the type parameter
+ */
+public abstract class EntityMapResolve {
+ /**
+ * The Result.
+ */
+ protected EntityResolveResult result;
+
+ /**
+ * The type Entity resolve result.
+ *
+ * @param the type parameter
+ */
+ public static class EntityResolveResult {
+ /**
+ * The Entity map.
+ */
+ protected final Map entityMap = new HashMap<>();
+ /**
+ * The Tag map.
+ */
+ protected final Map tagMap = new HashMap<>();
+ /**
+ * The Mod map.
+ */
+ protected final Map modMap = new HashMap<>();
+
+ /**
+ * The enum Type.
+ */
+ public enum Type {
+ /**
+ * Entity type.
+ */
+ ENTITY,
+ /**
+ * Tag type.
+ */
+ TAG,
+ /**
+ * Mod type.
+ */
+ MOD
+ }
+
+ /**
+ * Gets map.
+ *
+ * @param type the type
+ * @return the map
+ */
+ public Map getMap(@NotNull Type type) {
+ return switch (type) {
+ case ENTITY -> entityMap;
+ case TAG -> tagMap;
+ case MOD -> modMap;
+ };
+ }
+
+ /**
+ * Update.
+ *
+ * @param entity the entity
+ * @param tag the tag
+ * @param mod the mod
+ */
+ public void update(Map entity, Map tag, Map mod) {
+ entityMap.clear();
+ entityMap.putAll(entity);
+ tagMap.clear();
+ tagMap.putAll(tag);
+ modMap.clear();
+ modMap.putAll(mod);
+ }
+ }
+
+ /**
+ * Resolve entity resolve result.
+ *
+ * @param configs the configs
+ * @return the entity resolve result
+ */
+ public EntityResolveResult resolve(@NotNull List configs) {
+ Map entityMap = new HashMap<>();
+ Map tagMap = new HashMap<>();
+ Map modMap = new HashMap<>();
+
+ for (String config : configs) {
+ if (!isMatch(config)) continue;
+
+ try {
+ T t = resolveT(config);
+
+ String[] entities = resolveEntities(config);
+ for (String e : entities) {
+ String trimmed = e.trim();
+ if (trimmed.equals("*")) modMap.put("*", t);
+ else if (trimmed.startsWith("#")) {
+ String body = trimmed.substring(1).trim();
+ if (body.contains(":")) tagMap.put(body, t);
+ else modMap.put(body, t);
+ } else entityMap.put(trimmed, t);
+ }
+ result.update(entityMap, tagMap, modMap);
+ } catch (NumberFormatException ex) {
+ Lib39.LOGGER.error("Invalid offset config: {}", config);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Is match boolean.
+ *
+ * @param input the input
+ * @return the boolean
+ */
+ protected abstract boolean isMatch(String input);
+
+ /**
+ * Resolve t t.
+ *
+ * @param input the input
+ * @return the t
+ */
+ protected abstract T resolveT(String input);
+
+ /**
+ * Resolve entities string [ ].
+ *
+ * @param input the input
+ * @return the string [ ]
+ */
+ protected abstract String[] resolveEntities(String input);
+ /**
+ * 查找实体对应的值,如果找到返回匹配结果,否则返回null
+ */
+ @SuppressWarnings("deprecation")
+ private EntityMatchResult findEntityMatch(EntityType> type) {
+ String entityId = type.builtInRegistryHolder().key().location().toString();
+ String modId = entityId.split(":")[0];
+
+ // 检查实体ID匹配
+ for (String rs : result.entityMap.keySet()) {
+ if (rs.equals(entityId)) {
+ return new EntityMatchResult<>(EntityResolveResult.Type.ENTITY, rs, result.entityMap.get(rs));
+ }
+ }
+
+ // 检查标签匹配
+ for (String rs : result.tagMap.keySet()) {
+ String body = rs.startsWith("#") ? rs.substring(1) : rs;
+ ResourceLocation tagId = new ResourceLocation(body);
+ TagKey> tag = TagKey.create(Registries.ENTITY_TYPE, tagId);
+ if (type.builtInRegistryHolder().is(tag)) {
+ return new EntityMatchResult<>(EntityResolveResult.Type.TAG, rs, result.tagMap.get(rs));
+ }
+ }
+
+ // 检查模组匹配
+ for (String rs : result.modMap.keySet()) {
+ String body = rs.startsWith("#") ? rs.substring(1) : rs;
+ if (!body.contains(":") && body.equals(modId)) {
+ return new EntityMatchResult<>(EntityResolveResult.Type.MOD, rs, result.modMap.get(rs));
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Is entity t in map boolean.
+ *
+ * @param type the type
+ * @return the boolean
+ */
+ public boolean isEntityTInMap(EntityType> type) {
+ return findEntityMatch(type) != null;
+ }
+
+ /**
+ * Gets entity t in map.
+ *
+ * @param type the type
+ * @return the entity t in map
+ */
+ public T getEntityTInMap(EntityType> type) {
+ EntityMatchResult match = findEntityMatch(type);
+ return match != null ? match.value : null;
+ }
+
+ /**
+ * 内部类,用于存储实体匹配结果
+ */
+ private record EntityMatchResult(EntityResolveResult.Type type, String key, T value) {
+ }
+
+}
diff --git a/src/main/resources/assets/lib39/shaders/core/ring.fsh b/src/main/resources/assets/lib39/shaders/core/ring.fsh
new file mode 100644
index 0000000..39aa703
--- /dev/null
+++ b/src/main/resources/assets/lib39/shaders/core/ring.fsh
@@ -0,0 +1,49 @@
+#version 150
+
+in vec4 vertexColor;
+
+uniform vec4 ColorModulator;
+uniform vec2 Center;
+uniform float InnerRadius;
+uniform float OuterRadius;
+uniform float AntiAliasing;
+
+out vec4 fragColor;
+
+void main() {
+ float dist = distance(gl_FragCoord.xy, Center);
+ float alpha = 0.0;
+
+ // 确保内外半径合理
+ if (OuterRadius <= InnerRadius) {
+ vec4 color = vertexColor;
+ color.a = 0;
+ fragColor = color;
+ }
+
+ // 计算环形 alpha
+ if (dist >= InnerRadius && dist <= OuterRadius) {
+ alpha = 1.0;
+
+ // 内边缘抗锯齿
+ if (dist < InnerRadius + AntiAliasing) {
+ float fade = (dist - InnerRadius) / AntiAliasing;
+ alpha *= fade;
+ }
+
+ // 外边缘抗锯齿
+ if (dist > OuterRadius - AntiAliasing) {
+ float fade = 1.0 - (dist - (OuterRadius - AntiAliasing)) / AntiAliasing;
+ alpha *= fade;
+ }
+ }
+
+ vec4 color = vertexColor;
+ color.a *= alpha;
+
+ if (alpha > 0.0) {
+ fragColor = color * ColorModulator;
+ } else {
+ discard;
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/assets/lib39/shaders/core/ring.json b/src/main/resources/assets/lib39/shaders/core/ring.json
new file mode 100644
index 0000000..756ea0c
--- /dev/null
+++ b/src/main/resources/assets/lib39/shaders/core/ring.json
@@ -0,0 +1,44 @@
+{
+ "vertex": "position_color",
+ "fragment": "lib39:ring",
+ "samplers": [
+ ],
+ "uniforms": [
+ { "name": "ModelViewMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] },
+ { "name": "ProjMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] },
+ { "name": "ColorModulator", "type": "float", "count": 4, "values": [ 1.0, 1.0, 1.0, 1.0 ] },
+ {
+ "name": "Center",
+ "type": "float",
+ "count": 2,
+ "values": [
+ 0.0,
+ 0.0
+ ]
+ },
+ {
+ "name": "InnerRadius",
+ "type": "float",
+ "count": 1,
+ "values": [
+ 0.0
+ ]
+ },
+ {
+ "name": "OuterRadius",
+ "type": "float",
+ "count": 1,
+ "values": [
+ 0.0
+ ]
+ },
+ {
+ "name": "AntiAliasing",
+ "type": "float",
+ "count": 1,
+ "values": [
+ 1.25
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/main/resources/assets/lib39/shaders/core/selection.fsh b/src/main/resources/assets/lib39/shaders/core/selection.fsh
new file mode 100644
index 0000000..27e9904
--- /dev/null
+++ b/src/main/resources/assets/lib39/shaders/core/selection.fsh
@@ -0,0 +1,23 @@
+#version 150
+
+in vec4 vertexColor;
+
+uniform vec4 ColorModulator;
+uniform vec2 FramebufferSize;
+uniform vec2 Center;
+uniform float Radius;
+uniform float AntiAliasingRadius;
+
+out vec4 fragColor;
+
+void main() {
+ vec2 fragPos = vec2(gl_FragCoord.x, FramebufferSize.y - gl_FragCoord.y);
+ float distance = distance(fragPos, Center);
+ vec4 color = vec4(0, 0, 0, 0);
+ if (distance <= Radius) {
+ color = vertexColor;
+ color.a = smoothstep(Radius, 0.0, distance) * vertexColor.a;
+ }
+
+ fragColor = color * ColorModulator;
+}
\ No newline at end of file
diff --git a/src/main/resources/assets/lib39/shaders/core/selection.json b/src/main/resources/assets/lib39/shaders/core/selection.json
new file mode 100644
index 0000000..b9a2e52
--- /dev/null
+++ b/src/main/resources/assets/lib39/shaders/core/selection.json
@@ -0,0 +1,45 @@
+{
+ "vertex": "position_color",
+ "fragment": "lib39:selection",
+ "samplers": [
+ ],
+ "uniforms": [
+ { "name": "ModelViewMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] },
+ { "name": "ProjMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] },
+ { "name": "ColorModulator", "type": "float", "count": 4, "values": [ 1.0, 1.0, 1.0, 1.0 ] },
+ {
+ "name": "Center",
+ "type": "float",
+ "count": 2,
+ "values": [
+ 0.0,
+ 0.0
+ ]
+ },
+ {
+ "name": "FramebufferSize",
+ "type": "float",
+ "count": 2,
+ "values": [
+ 0.0,
+ 0.0
+ ]
+ },
+ {
+ "name": "Radius",
+ "type": "float",
+ "count": 1,
+ "values": [
+ 0.0
+ ]
+ },
+ {
+ "name": "AntiAliasingRadius",
+ "type": "float",
+ "count": 1,
+ "values": [
+ 1.5
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/main/resources/assets/lib39/textures/item/forge.png b/src/main/resources/assets/lib39/textures/item/forge.png
new file mode 100644
index 0000000..22a6c8f
Binary files /dev/null and b/src/main/resources/assets/lib39/textures/item/forge.png differ