diff --git a/odin/src/main/java/me/odin/mixin/mixins/MixinNetworkPlayerInfo.java b/odin/src/main/java/me/odin/mixin/mixins/MixinNetworkPlayerInfo.java new file mode 100644 index 000000000..331486efa --- /dev/null +++ b/odin/src/main/java/me/odin/mixin/mixins/MixinNetworkPlayerInfo.java @@ -0,0 +1,29 @@ +package me.odin.mixin.mixins; + +import com.mojang.authlib.GameProfile; +import me.odinmain.features.impl.render.DevPlayers; +import net.minecraft.client.network.NetworkPlayerInfo; +import net.minecraft.util.ResourceLocation; +import org.spongepowered.asm.mixin.Debug; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Debug(export = true) +@Mixin(NetworkPlayerInfo.class) +public abstract class MixinNetworkPlayerInfo { + + @Shadow private ResourceLocation locationCape; + + @Shadow @Final private GameProfile gameProfile; + + @Inject(method = "getLocationCape", at = @At("HEAD"), cancellable = true) + private void getDevCape(CallbackInfoReturnable cir) { + this.locationCape = DevPlayers.INSTANCE.hookGetLocationCape(this.gameProfile); + if (this.locationCape != null) cir.setReturnValue(this.locationCape); + } + +} diff --git a/odin/src/main/resources/mixins.odin.json b/odin/src/main/resources/mixins.odin.json index 879031dd2..e4195a5bf 100644 --- a/odin/src/main/resources/mixins.odin.json +++ b/odin/src/main/resources/mixins.odin.json @@ -30,6 +30,7 @@ "mixins.MixinModList", "mixins.MixinMouseHelper", "mixins.MixinNetworkManager", + "mixins.MixinNetworkPlayerInfo", "mixins.MixinPlayerControllerMP", "mixins.MixinRenderDragon", "mixins.MixinRenderEntityItem", diff --git a/odinclient/src/main/java/me/odinclient/mixin/mixins/MixinNetworkPlayerInfo.java b/odinclient/src/main/java/me/odinclient/mixin/mixins/MixinNetworkPlayerInfo.java new file mode 100644 index 000000000..226792a7b --- /dev/null +++ b/odinclient/src/main/java/me/odinclient/mixin/mixins/MixinNetworkPlayerInfo.java @@ -0,0 +1,29 @@ +package me.odinclient.mixin.mixins; + +import com.mojang.authlib.GameProfile; +import me.odinmain.features.impl.render.DevPlayers; +import net.minecraft.client.network.NetworkPlayerInfo; +import net.minecraft.util.ResourceLocation; +import org.spongepowered.asm.mixin.Debug; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Debug(export = true) +@Mixin(NetworkPlayerInfo.class) +public abstract class MixinNetworkPlayerInfo { + + @Shadow private ResourceLocation locationCape; + + @Shadow @Final private GameProfile gameProfile; + + @Inject(method = "getLocationCape", at = @At("HEAD"), cancellable = true) + private void getDevCape(CallbackInfoReturnable cir) { + this.locationCape = DevPlayers.INSTANCE.hookGetLocationCape(this.gameProfile); + if (this.locationCape != null) cir.setReturnValue(this.locationCape); + } + +} diff --git a/odinclient/src/main/resources/mixins.odinclient.json b/odinclient/src/main/resources/mixins.odinclient.json index cec73d31b..b30af1807 100644 --- a/odinclient/src/main/resources/mixins.odinclient.json +++ b/odinclient/src/main/resources/mixins.odinclient.json @@ -37,6 +37,7 @@ "mixins.MixinMouseHelper", "mixins.MixinNetHandlerPlayClient", "mixins.MixinNetworkManager", + "mixins.MixinNetworkPlayerInfo", "mixins.MixinPlayerControllerMP", "mixins.MixinRenderDragon", "mixins.MixinRenderEntityItem", diff --git a/src/main/kotlin/me/odinmain/OdinMain.kt b/src/main/kotlin/me/odinmain/OdinMain.kt index 2cc765646..eb2371516 100644 --- a/src/main/kotlin/me/odinmain/OdinMain.kt +++ b/src/main/kotlin/me/odinmain/OdinMain.kt @@ -71,6 +71,7 @@ object OdinMain { petCommand, visualWordsCommand, PosMsgCommand ) OdinFont.init() + scope.launch(Dispatchers.IO) { DevPlayers.preloadCapes() } } fun postInit() { diff --git a/src/main/kotlin/me/odinmain/features/impl/render/DevPlayers.kt b/src/main/kotlin/me/odinmain/features/impl/render/DevPlayers.kt index d35920cc4..41147381e 100644 --- a/src/main/kotlin/me/odinmain/features/impl/render/DevPlayers.kt +++ b/src/main/kotlin/me/odinmain/features/impl/render/DevPlayers.kt @@ -1,15 +1,18 @@ package me.odinmain.features.impl.render +import com.google.gson.Gson import com.google.gson.GsonBuilder import com.google.gson.JsonDeserializationContext import com.google.gson.JsonDeserializer import com.google.gson.JsonElement -import kotlinx.coroutines.DelicateCoroutinesApi +import com.google.gson.annotations.SerializedName +import com.mojang.authlib.GameProfile import kotlinx.coroutines.runBlocking import me.odinmain.OdinMain.mc import me.odinmain.OdinMain.scope import me.odinmain.features.impl.render.ClickGUIModule.devSize +import me.odinmain.utils.downloadFile import me.odinmain.utils.getDataFromServer import me.odinmain.utils.render.Color import me.odinmain.utils.render.translate @@ -17,11 +20,19 @@ import net.minecraft.client.entity.AbstractClientPlayer import net.minecraft.client.model.ModelBase import net.minecraft.client.model.ModelRenderer import net.minecraft.client.renderer.GlStateManager +import net.minecraft.client.renderer.texture.DynamicTexture import net.minecraft.entity.player.EntityPlayer import net.minecraft.util.ResourceLocation import net.minecraftforge.client.event.RenderPlayerEvent import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import java.awt.image.BufferedImage +import java.io.File +import java.io.IOException import java.lang.reflect.Type +import java.net.URL +import javax.imageio.ImageIO +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.math.cos import kotlin.math.sin @@ -30,7 +41,7 @@ object DevPlayers { val isDev get() = devs.containsKey(mc.session?.username) data class DevPlayer(val xScale: Float = 1f, val yScale: Float = 1f, val zScale: Float = 1f, - val wings: Boolean = false, val wingsColor: Color = Color(255, 255, 255)) + val wings: Boolean = false, val wingsColor: Color = Color(255, 255, 255), var capeLocation: ResourceLocation? = null) data class DevData(val devName: String, val wingsColor: Triple, val size: Triple, val wings: Boolean) @Suppress("UNCHECKED_CAST") @@ -98,10 +109,9 @@ object DevPlayers { DragonWings.renderWings(event.entityPlayer, event.partialRenderTick, dev) } - private val dragonWingTextureLocation = ResourceLocation("textures/entity/enderdragon/dragon.png") - object DragonWings : ModelBase() { + private val dragonWingTextureLocation = ResourceLocation("textures/entity/enderdragon/dragon.png") private val wing: ModelRenderer private val wingTip: ModelRenderer @@ -174,4 +184,85 @@ object DevPlayers { return f } } + + val capeFolder = File(mc.mcDataDir, "config/odin/capes") + private val capeUpdateCache = mutableMapOf() + data class Capes( + @SerializedName("capes") + val capes: List + ) + + @OptIn(ExperimentalEncodingApi::class) + fun preloadCapes() { + if (!capeFolder.exists()) capeFolder.mkdirs() + + val capeList = fetchCapeList("https://odtheking.github.io/Odin/capes/capes.json") + + capeList.forEach { fileName -> + val capeFile = File(capeFolder, fileName) + val capeUrl = "https://odtheking.github.io/Odin/capes/$fileName" + + synchronized(capeUpdateCache) { + if (capeUpdateCache[fileName] != true) { + if (!capeFile.exists() || !isFileUpToDate(capeUrl, capeFile)) { + println("Downloading cape: $fileName") + downloadFile(capeUrl, capeFile.path) + } + capeUpdateCache[fileName] = true + } + } + } + } + + private fun fetchCapeList(manifestUrl: String): List { + return try { + val json = URL(manifestUrl).readText() + val manifest = Gson().fromJson(json, Capes::class.java) + manifest.capes + } catch (e: IOException) { + e.printStackTrace() + emptyList() + } + } + + @OptIn(ExperimentalEncodingApi::class) + fun hookGetLocationCape(gameProfile: GameProfile): ResourceLocation? { + val name = gameProfile.name + if (!devs.containsKey(name)) return null + val dev = devs[name] + + val nameEncoded = Base64.encode(name.toByteArray()) + val capeFile = File(capeFolder, "$nameEncoded.png") + + return getCapeLocation(dev, capeFile) + } + + private fun isFileUpToDate(url: String, file: File): Boolean { + try { + val connection = URL(url).openConnection() + connection.connect() + val remoteLastModified = connection.getHeaderFieldDate("Last-Modified", 0L) + val localLastModified = file.lastModified() + return localLastModified >= remoteLastModified + } catch (e: IOException) { + e.printStackTrace() + } + return false + } + + private fun getCapeLocation(dev: DevPlayer?, file: File): ResourceLocation? { + if (dev?.capeLocation == null && file.exists()) { + var image: BufferedImage? = null + try { + image = ImageIO.read(file) + } catch (e: IOException) { + e.printStackTrace() + } + + val capeLocation = mc.textureManager.getDynamicTextureLocation("odincapes", DynamicTexture(image)) + dev?.capeLocation = capeLocation + } + return dev?.capeLocation + } + } \ No newline at end of file diff --git a/src/main/kotlin/me/odinmain/utils/WebUtils.kt b/src/main/kotlin/me/odinmain/utils/WebUtils.kt index 7f256d2af..fbfc5712e 100644 --- a/src/main/kotlin/me/odinmain/utils/WebUtils.kt +++ b/src/main/kotlin/me/odinmain/utils/WebUtils.kt @@ -7,7 +7,9 @@ import kotlinx.coroutines.withTimeoutOrNull import me.odinmain.OdinMain.logger import me.odinmain.features.impl.render.DevPlayers import java.io.BufferedReader +import java.io.File import java.io.FileOutputStream +import java.io.InputStream import java.io.InputStreamReader import java.io.OutputStreamWriter import java.net.HttpURLConnection @@ -116,22 +118,21 @@ fun fetchURLData(url: String): String { } fun downloadFile(url: String, outputPath: String) { - val wrappedURL = URL(url) - val connection = wrappedURL.openConnection() + val url = URL(url) + val connection = url.openConnection() connection.connect() - val inputStream = connection.getInputStream() - val outputStream = FileOutputStream(outputPath) + val inputStream: InputStream = connection.getInputStream() + val outputFile = File(outputPath) - val buffer = ByteArray(1024) - var bytesRead: Int + outputFile.parentFile?.mkdirs() - while (inputStream.read(buffer).also { bytesRead = it } != -1) { - outputStream.write(buffer, 0, bytesRead) + val outputStream = FileOutputStream(outputFile) + inputStream.use { input -> + outputStream.use { output -> + input.copyTo(output) + } } - - outputStream.close() - inputStream.close() } suspend fun hasBonusPaulScore(): Boolean = withTimeoutOrNull(5000) {