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 void initialize() {
Bus.MAIN.register(new ClientEventHandler());
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.net.URI;
import java.util.Objects;
import java.util.Map.Entry;
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.Bus;
import net.anvilcraft.anvillib.event.IEventBusRegisterable;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.render.entity.EntityRenderer;
import net.minecraft.client.render.entity.PlayerEntityRenderer;
import net.minecraft.entity.player.PlayerEntity;
public class ClientEventHandler implements IEventBusRegisterable {
private void onAddLayers(AddEntityRenderLayersEvent ev) {
for (Entry<String, EntityRenderer<? extends PlayerEntity>> skin : ev.skinMap().entrySet())
if (skin.getValue() instanceof PlayerEntityRenderer render)
render.addFeature(new CosmeticFeatureRenderer(render, skin.getKey()));
}
private void registerRemoteCosmetics() {
File gameDir = MinecraftClient.getInstance().runDirectory;
File cacheDir = new File(gameDir, "anvillibCache"); //TODO: use assets cache dir
public static void registerRemoteCosmetics(File assetsCache) {
File cacheDir = new File(Objects.requireNonNull(assetsCache), "anvillib");
try {
URI playerBase = new URI("https://api.tilera.xyz/anvillib/data/players/");
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) {
AnvilLib.LOGGER.error(e);
}
@ -36,6 +37,5 @@ public class ClientEventHandler implements IEventBusRegisterable {
@Override
public void registerEventHandlers(Bus bus) {
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 {
private static List<ICosmeticProvider> providers = new ArrayList<>();
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 Map<Identifier, GeoModel> cachedModels = new ConcurrentHashMap<>();
private static Map<Identifier, AnimationFile> cachedAnimations = new ConcurrentHashMap<>();
@ -40,6 +41,10 @@ public class CosmeticsManager {
List<ICosmetic> cosmetics = cosmeticCache.get(player);
for (ICosmeticProvider provider : providers) {
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);
}
public static Identifier getCape(UUID player) {
loadPlayer(player);
return capeCache.getOrDefault(player, null);
}
protected static GeoModel getModel(Identifier id) {
return cachedModels.get(id);
}

View file

@ -3,8 +3,14 @@ package net.anvilcraft.anvillib.cosmetics;
import java.util.UUID;
import java.util.function.Consumer;
import net.minecraft.util.Identifier;
public interface ICosmeticProvider {
boolean requestsRefresh();
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.ICosmeticProvider;
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.CosmeticLoaderThread;
import net.anvilcraft.anvillib.cosmetics.remote.thread.PlayerCosmeticLoaderThread;
import net.minecraft.util.Identifier;
import net.minecraft.util.Util;
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<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> knownCapes = new ConcurrentHashMap<>();
private boolean dirty = false;
public final URI playerBase;
public final URI cosmeticBase;
public final URI capeBase;
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.cosmeticBase = cosmeticBase;
this.capeBase = capeBase;
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() {
synchronized(this) {
this.dirty = true;
@ -73,7 +87,7 @@ public class RemoteCosmeticProvider implements ICosmeticProvider {
}
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);
URI url = cosmeticBase.resolve(id);
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));
}
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) {
AnvilLib.LOGGER.error("Cosmetic loading failed: {}", 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;
@Expose
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);
}
@SuppressWarnings("deprecation")
private void loadTexture(TextureData data) {
String hash = Hashing.sha1().hashUnencodedChars(this.data.id).toString();
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.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) {
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",
"compatibilityLevel": "JAVA_17",
"minVersion": "0.8",
"client": [],
"client": [
"client.MinecraftClientMixin",
"client.AbstractClientPlayerEntityMixin"
],
"mixins": [
"accessor.AnimatedGeoModelAccessor",
"common.RecipeManagerMixin",

View file

@ -1,10 +1,16 @@
package net.anvilcraft.anvillib;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.api.ModInitializer;
public class AnvilLibFabric implements ModInitializer {
public class AnvilLibFabric implements ModInitializer, ClientModInitializer {
@Override
public void onInitialize() {
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;

View file

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

View file

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

View file

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