diff --git a/pom.xml b/pom.xml
index 30ccd623f7..02d7aea8a2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -82,6 +82,11 @@
codemc-repo
https://repo.codemc.io/repository/maven-public/
+
+
+ opencollab-snapshot
+ https://repo.opencollab.dev/main/
+
@@ -238,7 +243,7 @@
com.github.mcchampions.dough
dough-api
- 0facd94827
+ 7136585ffd
io.papermc
@@ -414,5 +419,13 @@
b7a2bd8
compile
+
+
+
+ org.geysermc.geyser
+ api
+ 2.4.2-SNAPSHOT
+ provided
+
diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/integrations/IntegrationsManager.java b/src/main/java/io/github/thebusybiscuit/slimefun4/integrations/IntegrationsManager.java
index afb588854b..2c875681f1 100644
--- a/src/main/java/io/github/thebusybiscuit/slimefun4/integrations/IntegrationsManager.java
+++ b/src/main/java/io/github/thebusybiscuit/slimefun4/integrations/IntegrationsManager.java
@@ -12,6 +12,7 @@
import java.util.logging.Level;
import lombok.Getter;
+import me.qscbm.slimefun4.integrations.GeyserIntegration;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Server;
@@ -138,6 +139,10 @@ private void onServerLoad() {
* This method is called when the {@link Server} has finished loading all its {@link Plugin Plugins}.
*/
private void onServerStart() {
+ // Geyser Integration (custom skulls)
+ load("Geyser-Spigot", integration -> {
+ new GeyserIntegration().register();
+ });
try {
// Load Protection plugin integrations
protectionManager = new ProtectionManager(plugin);
diff --git a/src/main/java/me/qscbm/slimefun4/integrations/GeyserIntegration.java b/src/main/java/me/qscbm/slimefun4/integrations/GeyserIntegration.java
new file mode 100644
index 0000000000..2dc8b438b3
--- /dev/null
+++ b/src/main/java/me/qscbm/slimefun4/integrations/GeyserIntegration.java
@@ -0,0 +1,58 @@
+package me.qscbm.slimefun4.integrations;
+
+import io.github.bakedlibs.dough.config.Config;
+import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItem;
+import io.github.thebusybiscuit.slimefun4.implementation.Slimefun;
+import me.qscbm.slimefun4.utils.NBTUtils;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.inventory.meta.SkullMeta;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+public class GeyserIntegration {
+ public void register() {
+ Slimefun.logger().info("开始加载自定义粘液科技Geyser支持");
+
+ Config config = new Config(new File("plugins/Geyser-Spigot/custom-skulls.yml"));
+ List l = config.getStringList("skin-hashes");
+ int successCount = 0;
+ int errorCount = 0;
+ for (SlimefunItem slimefunItem : Slimefun.getRegistry().getAllSlimefunItems()) {
+ ItemStack itemStack = slimefunItem.getItem();
+ if (itemStack.getType() != Material.PLAYER_HEAD) {
+ continue;
+ }
+ ItemMeta m = itemStack.getItemMeta();
+ if (!(m instanceof SkullMeta meta)) {
+ errorCount++;
+ continue;
+ }
+ String textureCode = NBTUtils.getTexture(meta);
+ if (textureCode == null) {
+ errorCount++;
+ continue;
+ }
+ String[] ts = textureCode.split("/");
+ String hash = ts[ts.length - 1];
+ if (!l.contains(hash)) {
+ l.add(hash);
+ }
+ successCount++;
+ }
+ String[] i = {"player-names", "player-uuids", "player-profiles"};
+ for (String t : i) {
+ if (config.getStringList(t).isEmpty()) {
+ config.setValue(t, new ArrayList<>());
+ }
+ }
+ config.setValue("skin-hashes", l);
+ config.save();
+ Slimefun.logger().info("成功加载" + successCount + "个自定义头颅");
+ Slimefun.logger().info("加载失败" + errorCount + "个自定义头颅");
+ Slimefun.logger().warning("完成加载自定义粘液科技Geyser支持,如果不生效请重启服务器");
+ }
+}
diff --git a/src/main/java/me/qscbm/slimefun4/utils/NBTUtils.java b/src/main/java/me/qscbm/slimefun4/utils/NBTUtils.java
new file mode 100644
index 0000000000..3ebde93b1c
--- /dev/null
+++ b/src/main/java/me/qscbm/slimefun4/utils/NBTUtils.java
@@ -0,0 +1,105 @@
+package me.qscbm.slimefun4.utils;
+
+import com.destroystokyo.paper.profile.ProfileProperty;
+import com.mojang.authlib.GameProfile;
+import com.mojang.authlib.properties.Property;
+import io.github.bakedlibs.dough.reflection.ReflectionUtils;
+import io.github.thebusybiscuit.slimefun4.implementation.Slimefun;
+import org.bukkit.Bukkit;
+import org.bukkit.inventory.meta.SkullMeta;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Base64;
+import java.util.Collection;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class NBTUtils {
+ public static final String CRAFTBUKKIT_PACKAGE_NAME = Bukkit.getServer().getClass().getPackage().getName();
+
+ public static final String VERSION;
+
+ private static final Class> CRAFT_SKULL_META_CLAZZ;
+
+ private static final Field SKULL_PROFILE;
+
+ private static final Class> RESOLVABLE_PROFILE;
+
+ private static Method RESOLVABLE_PROFILE_GAME_PROFILE_GETTER = null;
+
+ private static final Method PROPERTY_NAME_GETTER;
+ private static final Method PROPERTY_VALUE_GETTER;
+
+ static {
+ String detectedVersion = CRAFTBUKKIT_PACKAGE_NAME.substring(CRAFTBUKKIT_PACKAGE_NAME.lastIndexOf('.') + 1);
+ if (!detectedVersion.startsWith("v")) {
+ // Paper or something...
+ detectedVersion = VersionUtils.getBukkitVersion();
+ }
+ VERSION = detectedVersion;
+ try {
+ CRAFT_SKULL_META_CLAZZ = Class.forName(CRAFTBUKKIT_PACKAGE_NAME + ".inventory." + "CraftMetaSkull");
+ SKULL_PROFILE = CRAFT_SKULL_META_CLAZZ.getDeclaredField("profile");
+ SKULL_PROFILE.setAccessible(true);
+ } catch (ClassNotFoundException | NoSuchFieldException e) {
+ throw new RuntimeException(e);
+ }
+ Class> resolvableProfile;
+ try {
+ resolvableProfile = Class.forName("net.minecraft.world.item.component.ResolvableProfile");
+ RESOLVABLE_PROFILE_GAME_PROFILE_GETTER = ReflectionUtils.getMethod(resolvableProfile, "f");
+ } catch (ClassNotFoundException e) {
+ resolvableProfile = null;
+ }
+ RESOLVABLE_PROFILE = resolvableProfile;
+ PROPERTY_NAME_GETTER = ReflectionUtils.getMethod(Property.class, "getName");
+ PROPERTY_VALUE_GETTER = ReflectionUtils.getMethod(Property.class, "getValue");
+ }
+
+ public static String getTexture(SkullMeta skullMeta) {
+ try {
+ Object pro = SKULL_PROFILE.get(skullMeta);
+ if (RESOLVABLE_PROFILE != null && RESOLVABLE_PROFILE.isInstance(pro)) {
+ if (RESOLVABLE_PROFILE_GAME_PROFILE_GETTER != null) {
+ pro = RESOLVABLE_PROFILE_GAME_PROFILE_GETTER.invoke(pro);
+ }
+ }
+
+ if (pro == null) {
+ return null;
+ }
+ GameProfile profile = (GameProfile) pro;
+ Collection properties = profile.getProperties().values();
+ for (Property prop : properties) {
+ String texture = null;
+ if (PROPERTY_NAME_GETTER != null) {
+ if ("textures".equals(PROPERTY_NAME_GETTER.invoke(prop))) {
+ texture = new String(Base64.getDecoder().decode((String) PROPERTY_VALUE_GETTER.invoke(prop)));
+ }
+ } else {
+ if ("textures".equals(prop.name())) {
+ texture = new String(Base64.getDecoder().decode(prop.value()));
+ }
+ }
+ return getMatch(texture, "\\{\"url\":\"(.*?)\"\\}");
+ }
+ return null;
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ Slimefun.logger().warning(e.getMessage());
+ return null;
+ }
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private static String getMatch(String string, String regex) {
+ Pattern pattern = Pattern.compile(regex);
+ Matcher matcher = pattern.matcher(string);
+ if (matcher.find()) {
+ return matcher.group(1);
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml
index 098a30a4b8..3cf20c0369 100644
--- a/src/main/resources/plugin.yml
+++ b/src/main/resources/plugin.yml
@@ -20,6 +20,7 @@ softdepend:
- ItemsAdder
- Vault
- Orebfuscator
+ - Geyser-Spigot
# We hook into these plugins too, but they depend on Slimefun.
loadBefore: