Add Forge 1.20.1 HD 3D Skin Layers compatibility
This commit is contained in:
@@ -3,6 +3,9 @@
|
||||
Branch `multiloader` builds the mod for **Minecraft 1.20.1** on **Fabric** and **Forge** from one
|
||||
shared `common` source set.
|
||||
|
||||
The Forge artifact includes optional compatibility for **3D Skin Layers 1.9.2**. It is a
|
||||
compile-only dependency and is not bundled into the resulting jar.
|
||||
|
||||
`main` remains the original single-loader NeoForge 1.21.1 line.
|
||||
|
||||
## Layout
|
||||
|
||||
@@ -16,6 +16,13 @@ This branch targets **Fabric and Forge 1.20.1** through an Architectury multiloa
|
||||
- HD PNG capes fall through to the vanilla renderer untouched.
|
||||
- Per-player control and persistence.
|
||||
|
||||
### 3D Skin Layers compatibility (Forge)
|
||||
|
||||
3D Skin Layers 1.9.2 is supported as an optional, non-bundled mod. Square HD skins from 128x128
|
||||
through 2048x2048 retain their original texture resolution while a bounded 64x64 alpha map is used
|
||||
to build the 3D geometry. Failures are logged with the `[SkinsG/3DSL Compat]` prefix and use safe
|
||||
fallback geometry; if fallback construction also fails, only the affected 3D layer is disabled.
|
||||
|
||||
## Backend integration (easy)
|
||||
|
||||
The mod talks to your existing site/launcher backend over plain HTTP — no server-side plugin needed.
|
||||
|
||||
@@ -32,6 +32,7 @@ subprojects {
|
||||
|
||||
repositories {
|
||||
maven { url "https://maven.architectury.dev/" }
|
||||
maven { url "https://api.modrinth.com/maven" }
|
||||
maven { url "https://maven.neoforged.net/releases/" }
|
||||
maven { url "https://maven.minecraftforge.net/" }
|
||||
maven { url "https://maven.parchmentmc.org" }
|
||||
|
||||
@@ -2,6 +2,10 @@ package xyz.nightsync.skinsg.mixin;
|
||||
|
||||
import com.mojang.blaze3d.platform.NativeImage;
|
||||
import net.minecraft.client.renderer.texture.HttpTexture;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.resources.DefaultPlayerSkin;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
@@ -18,6 +22,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
*/
|
||||
@Mixin(HttpTexture.class)
|
||||
public abstract class SkinMagicMixin {
|
||||
private static final Logger SKINSG_LOGGER = LoggerFactory.getLogger("skinsg");
|
||||
|
||||
@Shadow
|
||||
private static void setNoAlpha(NativeImage img, int x1, int y1, int x2, int y2) {}
|
||||
@@ -27,6 +32,7 @@ public abstract class SkinMagicMixin {
|
||||
|
||||
@Inject(method = "processLegacySkin", at = @At("HEAD"), cancellable = true)
|
||||
private void skinsg$processLegacySkin(NativeImage image, CallbackInfoReturnable<@Nullable NativeImage> cir) {
|
||||
try {
|
||||
final int h = image.getHeight();
|
||||
final int w = image.getWidth();
|
||||
|
||||
@@ -80,5 +86,28 @@ public abstract class SkinMagicMixin {
|
||||
|
||||
cir.setReturnValue(image);
|
||||
cir.cancel();
|
||||
} catch (Exception | LinkageError error) {
|
||||
SKINSG_LOGGER.error("[SkinsG] Failed to process skin image; falling back to the default Steve skin.", error);
|
||||
try {
|
||||
image.close();
|
||||
} catch (RuntimeException closeError) {
|
||||
SKINSG_LOGGER.debug("[SkinsG] Failed to close rejected skin image.", closeError);
|
||||
}
|
||||
cir.setReturnValue(skinsg$loadSteve());
|
||||
cir.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private static NativeImage skinsg$loadSteve() {
|
||||
try {
|
||||
var resource = Minecraft.getInstance().getResourceManager()
|
||||
.getResource(DefaultPlayerSkin.getDefaultSkin()).orElseThrow();
|
||||
try (var stream = resource.open()) {
|
||||
return NativeImage.read(stream);
|
||||
}
|
||||
} catch (Exception | LinkageError fallbackError) {
|
||||
SKINSG_LOGGER.error("[SkinsG] Default Steve skin could not be loaded; vanilla will use its fallback.", fallbackError);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ loom {
|
||||
forge {
|
||||
convertAccessWideners = true
|
||||
mixinConfig "skinsg.mixins.json"
|
||||
mixinConfig "skinsg.skinlayers3d.mixins.json"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +26,8 @@ configurations {
|
||||
dependencies {
|
||||
forge "net.minecraftforge:forge:${rootProject.forge_version}"
|
||||
modApi "dev.architectury:architectury-forge:${rootProject.architectury_api_version}"
|
||||
modCompileOnly "maven.modrinth:3dskinlayers:BBnR3V86"
|
||||
modLocalRuntime "maven.modrinth:3dskinlayers:BBnR3V86"
|
||||
|
||||
common(project(path: ":common", configuration: "namedElements")) { transitive false }
|
||||
shadowBundle project(path: ":common", configuration: "transformProductionForge")
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package xyz.nightsync.skinsg.forge.compat.skinlayers3d;
|
||||
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin;
|
||||
import org.spongepowered.asm.mixin.extensibility.IMixinInfo;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public final class SkinLayersCompatPlugin implements IMixinConfigPlugin {
|
||||
@Override public void onLoad(String mixinPackage) {}
|
||||
@Override public String getRefMapperConfig() { return null; }
|
||||
@Override public boolean shouldApplyMixin(String targetClassName, String mixinClassName) {
|
||||
return SkinLayersCompatPlugin.class.getClassLoader()
|
||||
.getResource("dev/tr7zw/skinlayers/SkinUtil.class") != null;
|
||||
}
|
||||
@Override public void acceptTargets(Set<String> myTargets, Set<String> otherTargets) {}
|
||||
@Override public List<String> getMixins() { return null; }
|
||||
@Override public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {}
|
||||
@Override public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {}
|
||||
}
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
package xyz.nightsync.skinsg.forge.compat.skinlayers3d.mixin;
|
||||
|
||||
import com.mojang.blaze3d.platform.NativeImage;
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.authlib.minecraft.MinecraftProfileTexture;
|
||||
import dev.tr7zw.skinlayers.SkinUtil;
|
||||
import dev.tr7zw.skinlayers.accessor.PlayerSettings;
|
||||
import dev.tr7zw.skinlayers.accessor.SkullSettings;
|
||||
import dev.tr7zw.skinlayers.api.Mesh;
|
||||
import dev.tr7zw.skinlayers.api.MeshHelper;
|
||||
import dev.tr7zw.skinlayers.api.SkinLayersAPI;
|
||||
import net.minecraft.client.player.AbstractClientPlayer;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Mixin(value = SkinUtil.class, remap = false)
|
||||
public abstract class SkinUtilMixin {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("skinsg");
|
||||
private static final int MAX_HD_SIZE = 2048;
|
||||
private static final long LOG_COOLDOWN_MS = 60_000L;
|
||||
private static final ConcurrentHashMap<String, Long> LAST_LOG = new ConcurrentHashMap<>();
|
||||
|
||||
@Inject(method = "setup3dLayers", at = @At("HEAD"), cancellable = true, require = 0)
|
||||
private static void skinsg$setupPlayer(AbstractClientPlayer player, PlayerSettings settings, boolean thinArms,
|
||||
CallbackInfoReturnable<Boolean> cir) {
|
||||
ResourceLocation location = player.getSkinTextureLocation();
|
||||
NativeImage skin = SkinUtil.getTexture(location, null);
|
||||
if (!isHdSkin(skin)) return;
|
||||
|
||||
if (location.equals(settings.getCurrentSkin()) && thinArms == settings.hasThinArms()) {
|
||||
cir.setReturnValue(settings.getHeadMesh() != null);
|
||||
return;
|
||||
}
|
||||
try (NativeImage meshMap = downsampleForMesh(skin)) {
|
||||
buildPlayerMeshes(meshMap, settings, thinArms);
|
||||
settings.setCurrentSkin(location);
|
||||
settings.setThinArms(thinArms);
|
||||
cir.setReturnValue(true);
|
||||
} catch (Exception | LinkageError error) {
|
||||
settings.clearMeshes();
|
||||
settings.setCurrentSkin(location);
|
||||
settings.setThinArms(thinArms);
|
||||
logFailure("PLAYER_MESH", location, skin, error);
|
||||
cir.setReturnValue(buildFallbackPlayer(settings, thinArms));
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(method = "setup3dLayers", at = @At("HEAD"), cancellable = true, require = 0)
|
||||
private static void skinsg$setupSkull(GameProfile profile, SkullSettings settings,
|
||||
CallbackInfoReturnable<Boolean> cir) {
|
||||
if (profile == null) return;
|
||||
Map<MinecraftProfileTexture.Type, MinecraftProfileTexture> textures = Minecraft.getInstance()
|
||||
.getSkinManager().getInsecureSkinInformation(profile);
|
||||
MinecraftProfileTexture texture = textures.get(MinecraftProfileTexture.Type.SKIN);
|
||||
if (texture == null) return;
|
||||
ResourceLocation location = Minecraft.getInstance().getSkinManager()
|
||||
.registerTexture(texture, MinecraftProfileTexture.Type.SKIN);
|
||||
setupSkull(location, settings, cir);
|
||||
}
|
||||
|
||||
@Inject(method = "setup3dLayers", at = @At("HEAD"), cancellable = true, require = 0)
|
||||
private static void skinsg$setupSkull(ResourceLocation location, SkullSettings settings,
|
||||
CallbackInfoReturnable<Boolean> cir) {
|
||||
setupSkull(location, settings, cir);
|
||||
}
|
||||
|
||||
private static void setupSkull(ResourceLocation location, SkullSettings settings,
|
||||
CallbackInfoReturnable<Boolean> cir) {
|
||||
NativeImage skin = SkinUtil.getTexture(location, settings);
|
||||
if (!isHdSkin(skin)) return;
|
||||
try (NativeImage meshMap = downsampleForMesh(skin)) {
|
||||
settings.setupHeadLayers(meshHelper().create3DMesh(meshMap, 8, 8, 8, 32, 0, false, 0.6f));
|
||||
settings.setInitialized(true);
|
||||
cir.setReturnValue(true);
|
||||
} catch (Exception | LinkageError error) {
|
||||
settings.setupHeadLayers(null);
|
||||
settings.setInitialized(true);
|
||||
logFailure("SKULL_MESH", location, skin, error);
|
||||
cir.setReturnValue(buildFallbackSkull(settings));
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isHdSkin(NativeImage image) {
|
||||
if (image == null) return false;
|
||||
int width = image.getWidth();
|
||||
return width > 64 && width <= MAX_HD_SIZE && image.getHeight() == width
|
||||
&& width % 64 == 0 && (width & (width - 1)) == 0;
|
||||
}
|
||||
|
||||
private static NativeImage downsampleForMesh(NativeImage source) {
|
||||
NativeImage result = new NativeImage(64, 64, true);
|
||||
int scale = source.getWidth() / 64;
|
||||
try {
|
||||
for (int y = 0; y < 64; y++) {
|
||||
for (int x = 0; x < 64; x++) {
|
||||
int selected = 0;
|
||||
int highestAlpha = -1;
|
||||
for (int sy = 0; sy < scale; sy++) {
|
||||
for (int sx = 0; sx < scale; sx++) {
|
||||
int pixel = source.getPixelRGBA(x * scale + sx, y * scale + sy);
|
||||
int alpha = pixel >>> 24;
|
||||
if (alpha > highestAlpha) {
|
||||
highestAlpha = alpha;
|
||||
selected = pixel;
|
||||
}
|
||||
}
|
||||
}
|
||||
result.setPixelRGBA(x, y, selected);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} catch (RuntimeException error) {
|
||||
result.close();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private static void buildPlayerMeshes(NativeImage skin, PlayerSettings settings, boolean thinArms) {
|
||||
MeshHelper helper = meshHelper();
|
||||
settings.clearMeshes();
|
||||
settings.setLeftLegMesh(helper.create3DMesh(skin, 4, 12, 4, 0, 48, true, 0f));
|
||||
settings.setRightLegMesh(helper.create3DMesh(skin, 4, 12, 4, 0, 32, true, 0f));
|
||||
int armWidth = thinArms ? 3 : 4;
|
||||
settings.setLeftArmMesh(helper.create3DMesh(skin, armWidth, 12, 4, 48, 48, true, -2f));
|
||||
settings.setRightArmMesh(helper.create3DMesh(skin, armWidth, 12, 4, 40, 32, true, -2f));
|
||||
settings.setTorsoMesh(helper.create3DMesh(skin, 8, 12, 4, 16, 32, true, 0f));
|
||||
settings.setHeadMesh(helper.create3DMesh(skin, 8, 8, 8, 32, 0, false, 0.6f));
|
||||
}
|
||||
|
||||
private static boolean buildFallbackPlayer(PlayerSettings settings, boolean thinArms) {
|
||||
try (NativeImage fallback = opaqueMeshMap()) {
|
||||
buildPlayerMeshes(fallback, settings, thinArms);
|
||||
return settings.getHeadMesh() != null;
|
||||
} catch (Exception | LinkageError error) {
|
||||
settings.clearMeshes();
|
||||
logFailure("PLAYER_FALLBACK", settings.getCurrentSkin(), null, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean buildFallbackSkull(SkullSettings settings) {
|
||||
try (NativeImage fallback = opaqueMeshMap()) {
|
||||
Mesh mesh = meshHelper().create3DMesh(fallback, 8, 8, 8, 32, 0, false, 0.6f);
|
||||
settings.setupHeadLayers(mesh);
|
||||
return mesh != null;
|
||||
} catch (Exception | LinkageError error) {
|
||||
settings.setupHeadLayers(null);
|
||||
logFailure("SKULL_FALLBACK", settings.getLastTexture(), null, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static NativeImage opaqueMeshMap() {
|
||||
NativeImage image = new NativeImage(64, 64, true);
|
||||
image.fillRect(0, 0, 64, 64, 0xFFFFFFFF);
|
||||
return image;
|
||||
}
|
||||
|
||||
private static MeshHelper meshHelper() {
|
||||
return SkinLayersAPI.getMeshHelper();
|
||||
}
|
||||
|
||||
private static void logFailure(String stage, ResourceLocation resource, NativeImage image, Throwable error) {
|
||||
String key = stage + '|' + resource + '|' + error.getClass().getName();
|
||||
long now = System.currentTimeMillis();
|
||||
Long previous = LAST_LOG.put(key, now);
|
||||
if (previous == null || now - previous >= LOG_COOLDOWN_MS) {
|
||||
String dimensions = image == null ? "unknown" : image.getWidth() + "x" + image.getHeight();
|
||||
LOGGER.error("[SkinsG/3DSL Compat] HD skin processing failed; using safe fallback. "
|
||||
+ "stage={}, resource={}, dimensions={}, thread={}",
|
||||
stage, resource, dimensions, Thread.currentThread().getName(), error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"required": false,
|
||||
"minVersion": "0.8",
|
||||
"compatibilityLevel": "JAVA_17",
|
||||
"package": "xyz.nightsync.skinsg.forge.compat.skinlayers3d.mixin",
|
||||
"plugin": "xyz.nightsync.skinsg.forge.compat.skinlayers3d.SkinLayersCompatPlugin",
|
||||
"client": ["SkinUtilMixin"],
|
||||
"injectors": { "defaultRequire": 0 }
|
||||
}
|
||||
+1
-1
@@ -6,7 +6,7 @@ org.gradle.parallel=true
|
||||
# --- Mod ---
|
||||
mod_id=skinsg
|
||||
mod_name=HD Skin Support (Gravit)
|
||||
mod_version=0.1.0
|
||||
mod_version=0.1.1
|
||||
mod_group_id=xyz.nightsync.skinsg
|
||||
mod_license=All Rights Reserved
|
||||
mod_author=Nghtly
|
||||
|
||||
Reference in New Issue
Block a user