feat: custom capes

This commit is contained in:
Timo Ley 2023-11-20 20:48:07 +01:00
parent 5dee30ea9e
commit c5de54cca7
18 changed files with 195 additions and 13 deletions

View file

@ -12,8 +12,11 @@ public class AnvilLib {
public static final Logger LOGGER = LogManager.getLogger(); public static final Logger LOGGER = LogManager.getLogger();
public static void initialize() { public static void initialize() {
Bus.MAIN.register(new ClientEventHandler());
GeckoLib.initialize(); GeckoLib.initialize();
} }
public static void initializeClient() {
Bus.MAIN.register(new ClientEventHandler());
}
} }

View file

@ -2,6 +2,7 @@ package net.anvilcraft.anvillib.cosmetics;
import java.io.File; import java.io.File;
import java.net.URI; import java.net.URI;
import java.util.Objects;
import java.util.Map.Entry; import java.util.Map.Entry;
import net.anvilcraft.anvillib.AnvilLib; import net.anvilcraft.anvillib.AnvilLib;
@ -9,25 +10,25 @@ import net.anvilcraft.anvillib.cosmetics.remote.RemoteCosmeticProvider;
import net.anvilcraft.anvillib.event.AddEntityRenderLayersEvent; import net.anvilcraft.anvillib.event.AddEntityRenderLayersEvent;
import net.anvilcraft.anvillib.event.Bus; import net.anvilcraft.anvillib.event.Bus;
import net.anvilcraft.anvillib.event.IEventBusRegisterable; import net.anvilcraft.anvillib.event.IEventBusRegisterable;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.render.entity.EntityRenderer; import net.minecraft.client.render.entity.EntityRenderer;
import net.minecraft.client.render.entity.PlayerEntityRenderer; import net.minecraft.client.render.entity.PlayerEntityRenderer;
import net.minecraft.entity.player.PlayerEntity; import net.minecraft.entity.player.PlayerEntity;
public class ClientEventHandler implements IEventBusRegisterable { public class ClientEventHandler implements IEventBusRegisterable {
private void onAddLayers(AddEntityRenderLayersEvent ev) { private void onAddLayers(AddEntityRenderLayersEvent ev) {
for (Entry<String, EntityRenderer<? extends PlayerEntity>> skin : ev.skinMap().entrySet()) for (Entry<String, EntityRenderer<? extends PlayerEntity>> skin : ev.skinMap().entrySet())
if (skin.getValue() instanceof PlayerEntityRenderer render) if (skin.getValue() instanceof PlayerEntityRenderer render)
render.addFeature(new CosmeticFeatureRenderer(render, skin.getKey())); render.addFeature(new CosmeticFeatureRenderer(render, skin.getKey()));
} }
private void registerRemoteCosmetics() { public static void registerRemoteCosmetics(File assetsCache) {
File gameDir = MinecraftClient.getInstance().runDirectory; File cacheDir = new File(Objects.requireNonNull(assetsCache), "anvillib");
File cacheDir = new File(gameDir, "anvillibCache"); //TODO: use assets cache dir
try { try {
URI playerBase = new URI("https://api.tilera.xyz/anvillib/data/players/"); URI playerBase = new URI("https://api.tilera.xyz/anvillib/data/players/");
URI cosmeticBase = new URI("https://api.tilera.xyz/anvillib/data/cosmetics/"); URI cosmeticBase = new URI("https://api.tilera.xyz/anvillib/data/cosmetics/");
CosmeticsManager.registerProvider(new RemoteCosmeticProvider(playerBase, cosmeticBase, cacheDir)); URI capeBase = new URI("https://api.tilera.xyz/anvillib/data/capes/");
CosmeticsManager.registerProvider(new RemoteCosmeticProvider(playerBase, cosmeticBase, capeBase, cacheDir));
} catch (Exception e) { } catch (Exception e) {
AnvilLib.LOGGER.error(e); AnvilLib.LOGGER.error(e);
} }
@ -36,6 +37,5 @@ public class ClientEventHandler implements IEventBusRegisterable {
@Override @Override
public void registerEventHandlers(Bus bus) { public void registerEventHandlers(Bus bus) {
bus.register(AddEntityRenderLayersEvent.class, this::onAddLayers); bus.register(AddEntityRenderLayersEvent.class, this::onAddLayers);
this.registerRemoteCosmetics();
} }
} }

View file

@ -16,6 +16,7 @@ import software.bernie.geckolib3.geo.render.built.GeoModel;
public class CosmeticsManager { public class CosmeticsManager {
private static List<ICosmeticProvider> providers = new ArrayList<>(); private static List<ICosmeticProvider> providers = new ArrayList<>();
private static Map<UUID, List<ICosmetic>> cosmeticCache = new HashMap<>(); private static Map<UUID, List<ICosmetic>> cosmeticCache = new HashMap<>();
private static Map<UUID, Identifier> capeCache = new HashMap<>();
private static Set<UUID> activePlayers = new HashSet<>(); private static Set<UUID> activePlayers = new HashSet<>();
private static Map<Identifier, GeoModel> cachedModels = new ConcurrentHashMap<>(); private static Map<Identifier, GeoModel> cachedModels = new ConcurrentHashMap<>();
private static Map<Identifier, AnimationFile> cachedAnimations = new ConcurrentHashMap<>(); private static Map<Identifier, AnimationFile> cachedAnimations = new ConcurrentHashMap<>();
@ -40,6 +41,10 @@ public class CosmeticsManager {
List<ICosmetic> cosmetics = cosmeticCache.get(player); List<ICosmetic> cosmetics = cosmeticCache.get(player);
for (ICosmeticProvider provider : providers) { for (ICosmeticProvider provider : providers) {
provider.addCosmetics(player, (cosmetic) -> cosmetics.add(cosmetic)); provider.addCosmetics(player, (cosmetic) -> cosmetics.add(cosmetic));
if (!capeCache.containsKey(player)) {
Identifier cape = provider.getCape(player);
if (cape != null) capeCache.put(player, cape);
}
} }
} }
@ -56,6 +61,11 @@ public class CosmeticsManager {
return cosmeticCache.get(uuid); return cosmeticCache.get(uuid);
} }
public static Identifier getCape(UUID player) {
loadPlayer(player);
return capeCache.getOrDefault(player, null);
}
protected static GeoModel getModel(Identifier id) { protected static GeoModel getModel(Identifier id) {
return cachedModels.get(id); return cachedModels.get(id);
} }

View file

@ -3,8 +3,14 @@ package net.anvilcraft.anvillib.cosmetics;
import java.util.UUID; import java.util.UUID;
import java.util.function.Consumer; import java.util.function.Consumer;
import net.minecraft.util.Identifier;
public interface ICosmeticProvider { public interface ICosmeticProvider {
boolean requestsRefresh(); boolean requestsRefresh();
void addCosmetics(UUID player, Consumer<ICosmetic> cosmeticAdder); void addCosmetics(UUID player, Consumer<ICosmetic> cosmeticAdder);
default Identifier getCape(UUID player) {
return null;
}
} }

View file

@ -14,9 +14,11 @@ import net.anvilcraft.anvillib.AnvilLib;
import net.anvilcraft.anvillib.cosmetics.ICosmetic; import net.anvilcraft.anvillib.cosmetics.ICosmetic;
import net.anvilcraft.anvillib.cosmetics.ICosmeticProvider; import net.anvilcraft.anvillib.cosmetics.ICosmeticProvider;
import net.anvilcraft.anvillib.cosmetics.remote.model.CosmeticData; import net.anvilcraft.anvillib.cosmetics.remote.model.CosmeticData;
import net.anvilcraft.anvillib.cosmetics.remote.thread.CapeLoaderThread;
import net.anvilcraft.anvillib.cosmetics.remote.thread.CosmeticAssetsLoaderThread; import net.anvilcraft.anvillib.cosmetics.remote.thread.CosmeticAssetsLoaderThread;
import net.anvilcraft.anvillib.cosmetics.remote.thread.CosmeticLoaderThread; import net.anvilcraft.anvillib.cosmetics.remote.thread.CosmeticLoaderThread;
import net.anvilcraft.anvillib.cosmetics.remote.thread.PlayerCosmeticLoaderThread; import net.anvilcraft.anvillib.cosmetics.remote.thread.PlayerCosmeticLoaderThread;
import net.minecraft.util.Identifier;
import net.minecraft.util.Util; import net.minecraft.util.Util;
public class RemoteCosmeticProvider implements ICosmeticProvider { public class RemoteCosmeticProvider implements ICosmeticProvider {
@ -25,16 +27,21 @@ public class RemoteCosmeticProvider implements ICosmeticProvider {
public final Map<String, RemoteCosmetic> cosmetics = new ConcurrentHashMap<>(); public final Map<String, RemoteCosmetic> cosmetics = new ConcurrentHashMap<>();
public final Map<UUID, Set<String>> playerCosmetics = new ConcurrentHashMap<>(); public final Map<UUID, Set<String>> playerCosmetics = new ConcurrentHashMap<>();
public final Map<String, Identifier> capes = new ConcurrentHashMap<>();
public final Map<UUID, String> playerCapes = new ConcurrentHashMap<>();
private final Map<String, Boolean> knownCosmetics = new ConcurrentHashMap<>(); private final Map<String, Boolean> knownCosmetics = new ConcurrentHashMap<>();
private final Map<String, Boolean> knownCapes = new ConcurrentHashMap<>();
private boolean dirty = false; private boolean dirty = false;
public final URI playerBase; public final URI playerBase;
public final URI cosmeticBase; public final URI cosmeticBase;
public final URI capeBase;
private final File cacheDir; private final File cacheDir;
public RemoteCosmeticProvider(URI playerBase, URI cosmeticBase, File cacheDir) { public RemoteCosmeticProvider(URI playerBase, URI cosmeticBase, URI capeBase, File cacheDir) {
this.playerBase = playerBase; this.playerBase = playerBase;
this.cosmeticBase = cosmeticBase; this.cosmeticBase = cosmeticBase;
this.capeBase = capeBase;
this.cacheDir = cacheDir; this.cacheDir = cacheDir;
} }
@ -60,6 +67,13 @@ public class RemoteCosmeticProvider implements ICosmeticProvider {
} }
} }
@Override
public Identifier getCape(UUID player) {
if (!this.playerCapes.containsKey(player)) return null;
String cape = this.playerCapes.get(player);
return this.capes.getOrDefault(cape, null);
}
public void markDirty() { public void markDirty() {
synchronized(this) { synchronized(this) {
this.dirty = true; this.dirty = true;
@ -73,7 +87,7 @@ public class RemoteCosmeticProvider implements ICosmeticProvider {
} }
public void loadCosmetic(String id) throws MalformedURLException { public void loadCosmetic(String id) throws MalformedURLException {
if (this.cosmetics.containsKey(id) || knownCosmetics.containsKey(id)) return; if (this.cosmetics.containsKey(id) || this.knownCosmetics.containsKey(id)) return;
this.knownCosmetics.put(id, true); this.knownCosmetics.put(id, true);
URI url = cosmeticBase.resolve(id); URI url = cosmeticBase.resolve(id);
Util.getMainWorkerExecutor().execute(new CosmeticLoaderThread(url, this)); Util.getMainWorkerExecutor().execute(new CosmeticLoaderThread(url, this));
@ -83,6 +97,13 @@ public class RemoteCosmeticProvider implements ICosmeticProvider {
Util.getMainWorkerExecutor().execute(new CosmeticAssetsLoaderThread(cosmetic, data, this.cacheDir, this)); Util.getMainWorkerExecutor().execute(new CosmeticAssetsLoaderThread(cosmetic, data, this.cacheDir, this));
} }
public void loadCape(String id) throws MalformedURLException {
if (this.capes.containsKey(id) || this.knownCapes.containsKey(id)) return;
this.knownCapes.put(id, true);
URI url = capeBase.resolve(id);
Util.getMainWorkerExecutor().execute(new CapeLoaderThread(id, url, this.cacheDir, this));
}
public void failCosmeticLoading(String id) { public void failCosmeticLoading(String id) {
AnvilLib.LOGGER.error("Cosmetic loading failed: {}", id); AnvilLib.LOGGER.error("Cosmetic loading failed: {}", id);
this.cosmetics.remove(id); this.cosmetics.remove(id);

View file

@ -0,0 +1,10 @@
package net.anvilcraft.anvillib.cosmetics.remote.model;
import com.google.gson.annotations.Expose;
public class CapeData {
@Expose
public String id;
@Expose
public String url;
}

View file

@ -11,5 +11,7 @@ public class PlayerData {
public UUID uuid; public UUID uuid;
@Expose @Expose
public List<String> cosmetics; public List<String> cosmetics;
@Expose
public String cape;
} }

View file

@ -0,0 +1,58 @@
package net.anvilcraft.anvillib.cosmetics.remote.thread;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import com.google.common.hash.Hashing;
import net.anvilcraft.anvillib.AnvilLib;
import net.anvilcraft.anvillib.cosmetics.remote.RemoteCosmeticProvider;
import net.anvilcraft.anvillib.cosmetics.remote.model.CapeData;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.texture.AbstractTexture;
import net.minecraft.client.texture.MissingSprite;
import net.minecraft.client.texture.PlayerSkinTexture;
import net.minecraft.client.texture.TextureManager;
import net.minecraft.util.Identifier;
public class CapeLoaderThread extends FileDownloaderThread {
private String id;
private File cacheDir;
private URI url;
private RemoteCosmeticProvider provider;
private TextureManager textureManager = MinecraftClient.getInstance().getTextureManager();
public CapeLoaderThread(String id, URI url, File cacheDir, RemoteCosmeticProvider provider) {
super("0.2.0");
this.id = id;
this.url = url;
this.cacheDir = cacheDir;
this.provider = provider;
}
@SuppressWarnings("deprecation")
@Override
public void run() {
CapeData data = null;
try {
data = this.loadJson(url, CapeData.class);
} catch (IOException e) {
AnvilLib.LOGGER.error("Can't load cape: {}", id, e);
return;
}
Identifier location = new Identifier("anvillib", "textures/cape/"+data.id);
String hash = Hashing.sha1().hashUnencodedChars(data.id).toString();
AbstractTexture texture = this.textureManager.getOrDefault(location, MissingSprite.getMissingSpriteTexture());
if (texture == MissingSprite.getMissingSpriteTexture()) {
File file = new File(this.cacheDir, hash.length() > 2 ? hash.substring(0, 2) : "xx");
File file2 = new File(file, hash);
texture = new PlayerSkinTexture(file2, data.url, new Identifier("textures/block/dirt.png"), false, null);
this.textureManager.registerTexture(location, texture);
}
this.provider.capes.put(data.id, location);
this.provider.markDirty();
}
}

View file

@ -76,6 +76,7 @@ public class CosmeticAssetsLoaderThread extends FileDownloaderThread {
this.cosmetic.loadAnimations(animations, anim); this.cosmetic.loadAnimations(animations, anim);
} }
@SuppressWarnings("deprecation")
private void loadTexture(TextureData data) { private void loadTexture(TextureData data) {
String hash = Hashing.sha1().hashUnencodedChars(this.data.id).toString(); String hash = Hashing.sha1().hashUnencodedChars(this.data.id).toString();
AbstractTexture texture = this.textureManager.getOrDefault(this.cosmetic.getTextureLocation(), MissingSprite.getMissingSpriteTexture()); AbstractTexture texture = this.textureManager.getOrDefault(this.cosmetic.getTextureLocation(), MissingSprite.getMissingSpriteTexture());

View file

@ -26,6 +26,10 @@ public class PlayerCosmeticLoaderThread extends FileDownloaderThread{
this.provider.loadCosmetic(id); this.provider.loadCosmetic(id);
this.provider.playerCosmetics.get(player.uuid).add(id); this.provider.playerCosmetics.get(player.uuid).add(id);
} }
if (player.cape != null) {
this.provider.loadCape(player.cape);
this.provider.playerCapes.put(player.uuid, player.cape);
}
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
} }

View file

@ -0,0 +1,29 @@
package net.anvilcraft.anvillib.mixin.client;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
import com.mojang.authlib.GameProfile;
import net.anvilcraft.anvillib.cosmetics.CosmeticsManager;
import net.minecraft.client.network.AbstractClientPlayerEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;
@Mixin(AbstractClientPlayerEntity.class)
public abstract class AbstractClientPlayerEntityMixin extends PlayerEntity {
public AbstractClientPlayerEntityMixin(World world, BlockPos pos, float yaw, GameProfile profile) {
super(world, pos, yaw, profile);
}
/**
* @reason Custom capes & no Mojank capes
* @author tilera
*/
@Overwrite
public Identifier getCapeTexture() {
return CosmeticsManager.getCape(this.uuid);
}
}

View file

@ -0,0 +1,18 @@
package net.anvilcraft.anvillib.mixin.client;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.At;
import net.anvilcraft.anvillib.cosmetics.ClientEventHandler;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.RunArgs;
@Mixin(MinecraftClient.class)
public class MinecraftClientMixin {
@Inject(at = @At("RETURN"), method = "<init>")
public void init(RunArgs args, CallbackInfo info) {
ClientEventHandler.registerRemoteCosmetics(args.directories.assetDir);
}
}

View file

@ -3,7 +3,10 @@
"package": "net.anvilcraft.anvillib.mixin", "package": "net.anvilcraft.anvillib.mixin",
"compatibilityLevel": "JAVA_17", "compatibilityLevel": "JAVA_17",
"minVersion": "0.8", "minVersion": "0.8",
"client": [], "client": [
"client.MinecraftClientMixin",
"client.AbstractClientPlayerEntityMixin"
],
"mixins": [ "mixins": [
"accessor.AnimatedGeoModelAccessor", "accessor.AnimatedGeoModelAccessor",
"common.RecipeManagerMixin", "common.RecipeManagerMixin",

View file

@ -1,10 +1,16 @@
package net.anvilcraft.anvillib; package net.anvilcraft.anvillib;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.api.ModInitializer; import net.fabricmc.api.ModInitializer;
public class AnvilLibFabric implements ModInitializer { public class AnvilLibFabric implements ModInitializer, ClientModInitializer {
@Override @Override
public void onInitialize() { public void onInitialize() {
AnvilLib.initialize(); AnvilLib.initialize();
} }
@Override
public void onInitializeClient() {
AnvilLib.initializeClient();
}
} }

View file

@ -1,4 +1,4 @@
package net.anvilcraft.anvillib.mixin.client; package net.anvilcraft.anvillib.mixin.fabric.client;
import java.util.Map; import java.util.Map;

View file

@ -4,6 +4,7 @@
"compatibilityLevel": "JAVA_17", "compatibilityLevel": "JAVA_17",
"minVersion": "0.8", "minVersion": "0.8",
"client": [ "client": [
"client.EntityRenderDispatcherMixin"
], ],
"mixins": [ "mixins": [
], ],

View file

@ -17,6 +17,9 @@
"entrypoints": { "entrypoints": {
"main": [ "main": [
"net.anvilcraft.anvillib.AnvilLibFabric" "net.anvilcraft.anvillib.AnvilLibFabric"
],
"client": [
"net.anvilcraft.anvillib.AnvilLibFabric"
] ]
}, },
"mixins": [ "mixins": [

View file

@ -1,10 +1,17 @@
package net.anvilcraft.anvillib; package net.anvilcraft.anvillib;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent;
@Mod(AnvilLib.MODID) @Mod(AnvilLib.MODID)
public class AnvilLibForge { public class AnvilLibForge {
public AnvilLibForge() { public AnvilLibForge() {
AnvilLib.initialize(); AnvilLib.initialize();
} }
@SubscribeEvent
public void onClientInitialize(FMLClientSetupEvent event) {
AnvilLib.initializeClient();
}
} }