diff --git a/common/build.gradle.kts b/common/build.gradle.kts index c918e1820..07481fb66 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { // Add dependencies on the required Kotlin modules. implementation("org.reflections:reflections:0.10.2") implementation("com.github.Edouard127:KDiscordIPC:$discordIPCVersion") + implementation("com.pngencoder:pngencoder:0.15.0") // Add Kotlin implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion") diff --git a/common/src/main/java/com/lambda/mixin/MinecraftClientMixin.java b/common/src/main/java/com/lambda/mixin/MinecraftClientMixin.java index 9ffcd5c65..809a077c4 100644 --- a/common/src/main/java/com/lambda/mixin/MinecraftClientMixin.java +++ b/common/src/main/java/com/lambda/mixin/MinecraftClientMixin.java @@ -35,6 +35,16 @@ void onTickPost(CallbackInfo ci) { EventFlow.post(new TickEvent.Post()); } + @Inject(method = "render", at = @At("HEAD")) + void onLoopTickPre(CallbackInfo ci) { + EventFlow.post(new TickEvent.Render.Pre()); + } + + @Inject(method = "render", at = @At("RETURN")) + void onLoopTickPost(CallbackInfo ci) { + EventFlow.post(new TickEvent.Render.Post()); + } + @Inject(at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;info(Ljava/lang/String;)V", shift = At.Shift.AFTER, remap = false), method = "stop") private void onShutdown(CallbackInfo ci) { EventFlow.post(new ClientEvent.Shutdown()); diff --git a/common/src/main/kotlin/com/lambda/core/Loader.kt b/common/src/main/kotlin/com/lambda/core/Loader.kt index 271143d2c..f2bb399f4 100644 --- a/common/src/main/kotlin/com/lambda/core/Loader.kt +++ b/common/src/main/kotlin/com/lambda/core/Loader.kt @@ -45,14 +45,7 @@ object Loader { LOG.info("Initializing ${Lambda.MOD_NAME} ${Lambda.VERSION}") val initTime = measureTimeMillis { - loadables.forEach { loadable -> - val info: String - val phaseTime = measureTimeMillis { - info = loadable.load() - } - - LOG.info("$info in ${phaseTime}ms") - } + loadables.forEach { LOG.info(it.load()) } } LOG.info("${Lambda.MOD_NAME} ${Lambda.VERSION} was successfully initialized (${initTime}ms)") diff --git a/common/src/main/kotlin/com/lambda/event/events/TickEvent.kt b/common/src/main/kotlin/com/lambda/event/events/TickEvent.kt index 85f9b1a0a..003f956d9 100644 --- a/common/src/main/kotlin/com/lambda/event/events/TickEvent.kt +++ b/common/src/main/kotlin/com/lambda/event/events/TickEvent.kt @@ -1,43 +1,98 @@ package com.lambda.event.events import com.lambda.event.Event -import com.lambda.event.EventFlow -import com.lambda.event.events.TickEvent.Post -import com.lambda.event.events.TickEvent.Pre -/** - * An abstract class representing a [TickEvent] in the [EventFlow]. - * - * A [TickEvent] is a type of [Event] that is triggered at each tick of the game loop. - * It has two subclasses: [Pre] and [Post], which are triggered before and after the tick, respectively. - * - * The [TickEvent] class is designed to be extended by any class that needs to react to ticks. - * - * @see Pre - * @see Post - */ abstract class TickEvent : Event { /** - * A class representing a [TickEvent] that is triggered before each tick of the game loop. + * Triggered before each iteration of the game loop. + * + * Phases: + * + * 1. **Pre-Tick**: Increments uptime, steps world tick manager, decrement item use cooldown. + * 2. **GUI Update**: Processes delayed messages, updates HUD. + * 3. **Game Mode Update**: Updates targeted entity, ticks tutorial, and interaction managers. + * 4. **Texture Update**: Ticks texture manager. + * 5. **Screen Handling**: Manages screen logic, ticks current screen. + * 6. **Debug HUD Update**: Resets debug HUD chunk. + * 7. **Input Handling**: Handles input events, decrements attack cooldown. + * 8. **World Update**: Ticks game and world renderers, world entities. + * 9. **Music and Sound Update**: Ticks music tracker and sound manager. + * 10. **Tutorial and Social Interactions**: Handles tutorial and social interactions, ticks world. + * 11. **Pending Connection**: Ticks integrated server connection. + * 12. **Keyboard Handling**: Polls for debug crash key presses. + * + * @see net.minecraft.client.MinecraftClient.tick */ class Pre : TickEvent() /** - * A class representing a [TickEvent] that is triggered after each tick of the game loop. + * Triggered after each iteration of the game loop. + * Targeted at 20 ticks per second. + * + * Phases: + * + * 1. **Pre-Tick**: Increments uptime, steps world tick manager, decrement item use cooldown. + * 2. **GUI Update**: Processes delayed messages, updates HUD. + * 3. **Game Mode Update**: Updates targeted entity, ticks tutorial, and interaction managers. + * 4. **Texture Update**: Ticks texture manager. + * 5. **Screen Handling**: Manages screen logic, ticks current screen. + * 6. **Debug HUD Update**: Resets debug HUD chunk. + * 7. **Input Handling**: Handles input events, decrements attack cooldown. + * 8. **World Update**: Ticks game and world renderers, world entities (such as [TickEvent.Player]). + * 9. **Music and Sound Update**: Ticks music tracker and sound manager. + * 10. **Tutorial and Social Interactions**: Handles tutorial and social interactions, ticks world. + * 11. **Pending Connection**: Ticks integrated server connection. + * 12. **Keyboard Handling**: Polls for debug crash key presses. + * + * @see net.minecraft.client.MinecraftClient.tick */ class Post : TickEvent() /** - * A class representing a [TickEvent] that is triggered when the player gets ticked. + * Triggered before ([Pre]) and after ([Post]) each render tick. + * + * Phases: + * + * 1. **Pre-Render**: Prepares the window for rendering, checks for window close, handles resource reloads. + * 2. **Task Execution**: Executes pending render tasks. + * 3. **Client Tick**: Ticks the client ([TickEvent.Pre] and [TickEvent.Post]) until tick target was met. + * 4. **Render**: Performs the actual rendering of the game. + * 5. **Post-Render**: Finalizes the rendering process, updates the window. + * + * @see net.minecraft.client.MinecraftClient.render + */ + abstract class Render : TickEvent() { + /** + * Triggered before each render tick ([TickEvent.Render]) of the game loop. + */ + class Pre : TickEvent() + + /** + * Triggered after each render tick ([TickEvent.Render]) of the game loop. + */ + class Post : TickEvent() + } + + /** + * Triggered before ([Pre]) and after ([Post]) each player tick that is run during the game loop [TickEvent.Pre]. + * + * Phases: + * + * 1. **Pre-Tick**: Prepares player state before the tick. + * 2. **Movement**: Handles player movement and input. + * 3. **Action**: Processes player actions like swinging hand. + * 4. **Post-Tick**: Finalizes player state after the tick. + * + * @see net.minecraft.client.network.ClientPlayerEntity.tick */ abstract class Player : TickEvent() { /** - * A class representing a [TickEvent.Player] that is triggered before each player tick. + * Triggered before each player tick ([TickEvent.Player]). */ class Pre : Player() /** - * A class representing a [TickEvent.Player] that is triggered after each player tick. + * Triggered after each player tick ([TickEvent.Player]). */ class Post : Player() } diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/vao/vertex/BufferUsage.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/BufferUsage.kt similarity index 60% rename from common/src/main/kotlin/com/lambda/graphics/buffer/vao/vertex/BufferUsage.kt rename to common/src/main/kotlin/com/lambda/graphics/buffer/BufferUsage.kt index 1cd56273d..7300d8e3f 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/vao/vertex/BufferUsage.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/BufferUsage.kt @@ -1,10 +1,12 @@ -package com.lambda.graphics.buffer.vao.vertex +package com.lambda.graphics.buffer import com.lambda.graphics.gl.GLObject import org.lwjgl.opengl.GL30C.GL_DYNAMIC_DRAW import org.lwjgl.opengl.GL30C.GL_STATIC_DRAW +import org.lwjgl.opengl.GL30C.GL_STREAM_DRAW enum class BufferUsage(override val gl: Int) : GLObject { STATIC(GL_STATIC_DRAW), - DYNAMIC(GL_DYNAMIC_DRAW) -} \ No newline at end of file + DYNAMIC(GL_DYNAMIC_DRAW), + STREAM(GL_STREAM_DRAW); +} diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/FrameBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/FrameBuffer.kt index e16e32891..db164955a 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/FrameBuffer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/FrameBuffer.kt @@ -1,4 +1,5 @@ package com.lambda.graphics.buffer + import com.lambda.Lambda.mc import com.lambda.graphics.RenderMain import com.lambda.graphics.buffer.vao.VAO @@ -124,4 +125,4 @@ class FrameBuffer(private val depth: Boolean = false) { private val vao = VAO(VertexMode.TRIANGLES, VertexAttrib.Group.POS_UV) private var lastFrameBuffer: Int? = null } -} \ No newline at end of file +} diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/pbo/PixelBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/pbo/PixelBuffer.kt new file mode 100644 index 000000000..17e815600 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/pbo/PixelBuffer.kt @@ -0,0 +1,169 @@ +package com.lambda.graphics.buffer.pbo + +import com.lambda.Lambda.LOG +import com.lambda.graphics.buffer.BufferUsage +import org.lwjgl.opengl.GL +import org.lwjgl.opengl.GL45C.* +import java.nio.ByteBuffer + +/** + * Represents a Pixel Buffer Object (PBO) that facilitates asynchronous data transfer to the GPU. + * This class manages the creation, usage, and cleanup of PBOs and provides methods to map textures and upload data efficiently. + * + * @property width The width of the texture in pixels. + * @property height The height of the texture in pixels. + * @property buffers The number of PBOs to be used. Default is 2, which allows double buffering. + * @property bufferUsage The usage pattern of the buffer, indicating how the buffer will be used (static, dynamic, etc.). + */ +class PixelBuffer( + private val width: Int, + private val height: Int, + private val buffers: Int = 2, + private val bufferUsage: BufferUsage = BufferUsage.DYNAMIC, +) { + private val pboIds = IntArray(buffers) + private var writeIdx = 0 // Used to copy pixels from the PBO to the texture + private var uploadIdx = 0 // Used to upload data to the PBO + + private val queryId = glGenQueries() // Used to measure the time taken to upload data to the PBO + private val uploadTime get() = + IntArray(1).also { glGetQueryObjectiv(queryId, GL_QUERY_RESULT, it) }.first() + private var transferRate = 0L // The transfer rate in bytes per second + + private val pboSupported = GL.getCapabilities().OpenGL30 || GL.getCapabilities().GL_ARB_pixel_buffer_object + + private var initialDataSent: Boolean = false + + /** + * Maps the given texture ID to the buffer and performs the necessary operations to upload the texture data. + * + * @param id The texture ID to which the buffer will be mapped. + * @param buffer The [ByteBuffer] containing the pixel data to be uploaded to the texture. + */ + fun mapTexture(id: Int, buffer: ByteBuffer) { + if (!initialDataSent) { + glBindTexture(GL_TEXTURE_2D, id) + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0) + glBindTexture(GL_TEXTURE_2D, 0) + + initialDataSent = true + + return + } + + upload(buffer) { + // Bind the texture + glBindTexture(GL_TEXTURE_2D, id) + + if (buffers > 0 && pboSupported) { + // Bind the next PBO to update pixel values + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[writeIdx]) + + // Perform the actual data transfer to the GPU + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0) + } else { + // Perform the actual data transfer to the GPU + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, buffer) + } + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) + + // Unbind the texture + glBindTexture(GL_TEXTURE_2D, 0) + } + } + + /** + * Uploads the given pixel data to the PBO and executes the provided processing function to manage the PBO's data transfer. + * + * @param data The [ByteBuffer] containing the pixel data to be uploaded. + * @param process A lambda function to execute after uploading the data to manage the PBO's data transfer. + */ + fun upload(data: ByteBuffer, process: () -> Unit) = + recordTransfer { + if (buffers >= 2) + uploadIdx = (writeIdx + 1) % buffers + + // Copy the pixel values from the PBO to the texture + process() + + // Bind the current PBO for writing + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[uploadIdx]) + + // Note that glMapBuffer() causes sync issue. + // If GPU is working with this buffer, glMapBuffer() will wait(stall) + // until GPU to finish its job. To avoid waiting (idle), you can call + // first glBufferData() with NULL pointer before glMapBuffer(). + // If you do that, the previous data in PBO will be discarded and + // glMapBuffer() returns a new allocated pointer immediately + // even if GPU is still working with the previous data. + glBufferData(GL_PIXEL_UNPACK_BUFFER, width * height * 4L, bufferUsage.gl) + + // Map the buffer into the memory + val bufferData = glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY) + if (bufferData != null) { + bufferData.put(data) + + // Release the buffer + glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER) + } else throw IllegalStateException("Failed to map the buffer") + + // Unbind the PBO + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0) + + // Swap the indices + writeIdx = uploadIdx + } + + /** + * Measures and records the time taken to transfer data to the PBO, calculating the transfer rate in bytes per second. + * + * @param block A lambda function representing the block of code where the transfer occurs. + */ + private fun recordTransfer(block: () -> Unit) { + // Start the timer + glBeginQuery(GL_TIME_ELAPSED, queryId) + + // Perform the transfer + block() + + // Stop the timer + glEndQuery(GL_TIME_ELAPSED) + + // Calculate the transfer rate + val time = uploadTime + if (time > 0) transferRate = (width * height * 4L * 1_000_000_000) / time + } + + /** + * Cleans up resources by deleting the PBOs when the object is no longer in use. + */ + fun finalize() { + // Delete the PBOs + glDeleteBuffers(pboIds) + } + + /** + * Initializes the PBOs, allocates memory for them, and handles unsupported PBO scenarios. + * + * @throws IllegalArgumentException If the number of buffers is less than 0. + */ + init { + if (buffers < 0) throw IllegalArgumentException("Buffers must be greater than or equal to 0") + + if (!pboSupported && buffers > 0) + LOG.warn("Client tried to utilize PBOs, but they are not supported on the machine, falling back to direct buffer upload") + + // Generate the PBOs + glGenBuffers(pboIds) + + // Fill the buffers with null data to allocate the memory spaces + repeat(buffers) { + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[it]) + glBufferData(GL_PIXEL_UNPACK_BUFFER, width * height * 4L, bufferUsage.gl) + } + + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0) // Unbind the buffer + } +} diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/vao/VAO.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/vao/VAO.kt index b6cb584cc..ccec90409 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/vao/VAO.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/vao/VAO.kt @@ -1,6 +1,6 @@ package com.lambda.graphics.buffer.vao -import com.lambda.graphics.buffer.vao.vertex.BufferUsage +import com.lambda.graphics.buffer.BufferUsage import com.lambda.graphics.buffer.vao.vertex.VertexAttrib import com.lambda.graphics.buffer.vao.vertex.VertexMode import com.lambda.graphics.gl.Matrices diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/ChunkedESP.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/ChunkedESP.kt index 966fe139d..1d1f8ef2c 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/ChunkedESP.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/ChunkedESP.kt @@ -5,7 +5,7 @@ import com.lambda.event.events.TickEvent import com.lambda.event.events.WorldEvent import com.lambda.event.listener.SafeListener.Companion.concurrentListener import com.lambda.event.listener.SafeListener.Companion.listener -import com.lambda.graphics.buffer.vao.vertex.BufferUsage +import com.lambda.graphics.buffer.BufferUsage import com.lambda.graphics.renderer.esp.impl.ESPRenderer import com.lambda.graphics.renderer.esp.impl.StaticESPRenderer import com.lambda.module.modules.client.RenderSettings @@ -126,4 +126,4 @@ class ChunkedESP private constructor( } } } -} \ No newline at end of file +} diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/global/StaticESP.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/global/StaticESP.kt index 454d624b9..fa40ed0ca 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/global/StaticESP.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/global/StaticESP.kt @@ -4,7 +4,7 @@ import com.lambda.event.EventFlow.post import com.lambda.event.events.RenderEvent import com.lambda.event.events.TickEvent import com.lambda.event.listener.SafeListener.Companion.listener -import com.lambda.graphics.buffer.vao.vertex.BufferUsage +import com.lambda.graphics.buffer.BufferUsage import com.lambda.graphics.renderer.esp.impl.StaticESPRenderer object StaticESP : StaticESPRenderer(BufferUsage.DYNAMIC, false) { @@ -15,4 +15,4 @@ object StaticESP : StaticESPRenderer(BufferUsage.DYNAMIC, false) { upload() } } -} \ No newline at end of file +} diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/DynamicESPRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/DynamicESPRenderer.kt index bf8f9a878..e9383f035 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/DynamicESPRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/DynamicESPRenderer.kt @@ -1,5 +1,5 @@ package com.lambda.graphics.renderer.esp.impl -import com.lambda.graphics.buffer.vao.vertex.BufferUsage +import com.lambda.graphics.buffer.BufferUsage -open class DynamicESPRenderer : ESPRenderer(BufferUsage.DYNAMIC, true) \ No newline at end of file +open class DynamicESPRenderer : ESPRenderer(BufferUsage.DYNAMIC, true) diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/ESPRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/ESPRenderer.kt index c35a4a2e5..15d0977da 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/ESPRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/ESPRenderer.kt @@ -2,7 +2,7 @@ package com.lambda.graphics.renderer.esp.impl import com.lambda.Lambda.mc import com.lambda.graphics.buffer.vao.VAO -import com.lambda.graphics.buffer.vao.vertex.BufferUsage +import com.lambda.graphics.buffer.BufferUsage import com.lambda.graphics.buffer.vao.vertex.VertexAttrib import com.lambda.graphics.buffer.vao.vertex.VertexMode import com.lambda.graphics.gl.GlStateUtils.withFaceCulling diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/StaticESPRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/StaticESPRenderer.kt index 26c7c9140..592412dde 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/StaticESPRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/StaticESPRenderer.kt @@ -1,7 +1,7 @@ package com.lambda.graphics.renderer.esp.impl import com.lambda.graphics.buffer.vao.IRenderContext -import com.lambda.graphics.buffer.vao.vertex.BufferUsage +import com.lambda.graphics.buffer.BufferUsage import java.awt.Color import java.util.concurrent.ConcurrentHashMap @@ -45,4 +45,4 @@ open class StaticESPRenderer( } data class Vertex(val x: Double, val y: Double, val z: Double, val color: Color) -} \ No newline at end of file +} diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaEmoji.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaEmoji.kt index 87cef41fd..cfe21c3d5 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaEmoji.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaEmoji.kt @@ -17,7 +17,7 @@ enum class LambdaEmoji(private val zipUrl: String) { object Loader : Loadable { override fun load(): String { entries.forEach(LambdaEmoji::loadGlyphs) - return "Loaded ${entries.size} emoji pools" + return "Loaded ${entries.size} emoji sets with a total of ${entries.sumOf { it.glyphs.count }} emojis" } } } diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/EmojiGlyphs.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/EmojiGlyphs.kt index d8f92e6e6..6e2dd4e05 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/EmojiGlyphs.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/EmojiGlyphs.kt @@ -3,23 +3,21 @@ package com.lambda.graphics.renderer.gui.font.glyph import com.google.common.math.IntMath.pow import com.lambda.Lambda.LOG import com.lambda.graphics.texture.MipmapTexture +import com.lambda.http.Method +import com.lambda.http.request import com.lambda.module.modules.client.RenderSettings -import com.lambda.threading.runGameScheduled -import com.lambda.threading.runIO import com.lambda.util.math.Vec2d import java.awt.Color import java.awt.Graphics2D import java.awt.image.BufferedImage import java.io.File -import java.net.URL import java.util.zip.ZipFile import javax.imageio.ImageIO import kotlin.math.ceil import kotlin.math.log2 import kotlin.math.sqrt -import kotlin.system.measureTimeMillis +import kotlin.time.Duration.Companion.days -// TODO: AbstractGlyphs to use for both Font & Emoji glyphs? class EmojiGlyphs(zipUrl: String) { private val emojiMap = mutableMapOf() private lateinit var fontTexture: MipmapTexture @@ -27,12 +25,11 @@ class EmojiGlyphs(zipUrl: String) { private lateinit var image: BufferedImage private lateinit var graphics: Graphics2D + val count get() = emojiMap.size + init { runCatching { - val time = measureTimeMillis { - downloadAndProcessZip(zipUrl) - } - LOG.info("Loaded ${emojiMap.size} emojis in $time ms") + downloadAndProcessZip(zipUrl) }.onFailure { LOG.error("Failed to load emojis: ${it.message}", it) fontTexture = MipmapTexture(BufferedImage(1024, 1024, BufferedImage.TYPE_INT_ARGB)) @@ -40,14 +37,20 @@ class EmojiGlyphs(zipUrl: String) { } private fun downloadAndProcessZip(zipUrl: String) { - val file = File.createTempFile("emoji", ".zip").apply { deleteOnExit() } + val file = request(zipUrl) { + method(Method.GET) + }.maybeDownload("emojis.zip", maxAge = 30.days) - URL(zipUrl).openStream().use { input -> - file.outputStream().use { output -> - input.copyTo(output) - } - } + fontTexture = MipmapTexture(processZip(file)) + } + /** + * Processes the given zip file and loads the emojis into the texture. + * + * @param file The zip file containing the emojis. + * @return The texture containing the emojis. + */ + private fun processZip(file: File): BufferedImage { ZipFile(file).use { zip -> val firstImage = ImageIO.read(zip.getInputStream(zip.entries().nextElement())) val length = zip.size().toDouble() @@ -90,7 +93,7 @@ class EmojiGlyphs(zipUrl: String) { } } - fontTexture = MipmapTexture(image) + return image } fun bind() { diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/FontGlyphs.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/FontGlyphs.kt index e00cbb284..3461ec2af 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/FontGlyphs.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/FontGlyphs.kt @@ -1,6 +1,6 @@ package com.lambda.graphics.renderer.gui.font.glyph -import com.lambda.Lambda +import com.lambda.Lambda.LOG import com.lambda.graphics.texture.MipmapTexture import com.lambda.graphics.texture.TextureUtils.getCharImage import com.lambda.module.modules.client.RenderSettings @@ -11,54 +11,61 @@ import java.awt.Font import java.awt.Graphics2D import java.awt.image.BufferedImage import kotlin.math.max -import kotlin.system.measureTimeMillis -class FontGlyphs(font: Font) { +class FontGlyphs( + private val font: Font +) { private val charMap = Int2ObjectOpenHashMap() - private val fontTexture: MipmapTexture + private lateinit var fontTexture: MipmapTexture var fontHeight = 0.0; private set init { - val time = measureTimeMillis { - val image = BufferedImage(TEXTURE_SIZE, TEXTURE_SIZE, BufferedImage.TYPE_INT_ARGB) + runCatching { + processGlyphs() + LOG.info("Font ${font.fontName} loaded with ${charMap.size} characters") + }.onFailure { + LOG.error("Failed to load font glyphs: ${it.message}", it) + fontTexture = MipmapTexture(BufferedImage(1024, 1024, BufferedImage.TYPE_INT_ARGB)) + } + } - val graphics = image.graphics as Graphics2D - graphics.background = Color(0, 0, 0, 0) + private fun processGlyphs() { + val image = BufferedImage(TEXTURE_SIZE, TEXTURE_SIZE, BufferedImage.TYPE_INT_ARGB) - var x = 0 - var y = 0 - var rowHeight = 0 + val graphics = image.graphics as Graphics2D + graphics.background = Color(0, 0, 0, 0) - (Char.MIN_VALUE.. - val charImage = getCharImage(font, char) ?: return@forEach + var x = 0 + var y = 0 + var rowHeight = 0 - rowHeight = max(rowHeight, charImage.height + STEP) + (Char.MIN_VALUE.. + val charImage = getCharImage(font, char) ?: return@forEach - if (x + charImage.width >= TEXTURE_SIZE) { - y += rowHeight - x = 0 - rowHeight = 0 - } + rowHeight = max(rowHeight, charImage.height + STEP) - check(y + charImage.height <= TEXTURE_SIZE) { "Can't load font glyphs. Texture size is too small" } + if (x + charImage.width >= TEXTURE_SIZE) { + y += rowHeight + x = 0 + rowHeight = 0 + } - graphics.drawImage(charImage, x, y, null) + check(y + charImage.height <= TEXTURE_SIZE) { "Can't load font glyphs. Texture size is too small" } - val size = Vec2d(charImage.width, charImage.height) - val uv1 = Vec2d(x, y) * ONE_TEXEL_SIZE - val uv2 = Vec2d(x, y).plus(size) * ONE_TEXEL_SIZE + graphics.drawImage(charImage, x, y, null) - charMap[char.code] = GlyphInfo(size, uv1, uv2) - fontHeight = max(fontHeight, size.y) + val size = Vec2d(charImage.width, charImage.height) + val uv1 = Vec2d(x, y) * ONE_TEXEL_SIZE + val uv2 = Vec2d(x, y).plus(size) * ONE_TEXEL_SIZE - x += charImage.width + STEP - } + charMap[char.code] = GlyphInfo(size, uv1, uv2) + fontHeight = max(fontHeight, size.y) - fontTexture = MipmapTexture(image) + x += charImage.width + STEP } - Lambda.LOG.info("Font ${font.fontName} loaded with ${charMap.size} characters in $time ms") + fontTexture = MipmapTexture(image) } fun bind() { diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/MipmapTexture.kt b/common/src/main/kotlin/com/lambda/graphics/texture/MipmapTexture.kt index f52906d64..ea918e92a 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/MipmapTexture.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/MipmapTexture.kt @@ -36,7 +36,13 @@ class MipmapTexture(image: BufferedImage, levels: Int = 4) : Texture() { } companion object { + /** + * Retrieves an image from the resources folder and generates a mipmap texture. + * + * @param path The path to the image. + * @param levels The number of mipmap levels. + */ fun fromResource(path: String, levels: Int = 4): MipmapTexture = MipmapTexture(ImageIO.read(LambdaResource(path).stream), levels) } -} \ No newline at end of file +} diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt index 7512b523b..ff5990c87 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt @@ -1,12 +1,10 @@ package com.lambda.graphics.texture import com.lambda.graphics.texture.TextureUtils.bindTexture -import com.lambda.threading.mainThread -import com.lambda.threading.runGameScheduled import org.lwjgl.opengl.GL13.glGenTextures -abstract class Texture { - private val id = glGenTextures() +open class Texture { + val id = glGenTextures() fun bind(slot: Int = 0) = bindTexture(id, slot) } diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt b/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt index 5bb814180..d3a6f94e8 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt @@ -1,19 +1,23 @@ package com.lambda.graphics.texture import com.mojang.blaze3d.systems.RenderSystem +import com.pngencoder.PngEncoder import net.minecraft.client.texture.NativeImage import org.lwjgl.BufferUtils -import org.lwjgl.opengl.GL13C.* +import org.lwjgl.opengl.GL45C.* import java.awt.* import java.awt.image.BufferedImage -import java.io.ByteArrayOutputStream -import javax.imageio.ImageIO import kotlin.math.roundToInt import kotlin.math.sqrt object TextureUtils { - private val metricCache = mutableMapOf() + private const val COMPRESSION_LEVEL = 1 + private const val THREADED_COMPRESSION = false + private val metricCache = mutableMapOf() + private val encoderPreset = PngEncoder() + .withCompressionLevel(COMPRESSION_LEVEL) + .withMultiThreadedCompressionEnabled(THREADED_COMPRESSION) fun bindTexture(id: Int, slot: Int = 0) { RenderSystem.activeTexture(GL_TEXTURE0 + slot) @@ -35,21 +39,6 @@ object TextureUtils { // Array of floats normalized to [0.0, 1.0] -> [R, G, B, A] glTexImage2D(GL_TEXTURE_2D, lod, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, readImage(bufferedImage)) - // I'd also like to use glTexSubImage2D, but we have an issue where the function - // would return an error about an invalid texture format. - // - // It would allow us to upload texture data asynchronously and is more efficient - // from testing we gain approximately 20% runtime performance. - // If someone with advanced OpenGL knowledge could help us out, that would be great. - // (Very unlikely to happen, but I can hope) - // - // I've also read online that glTexStorage2D can be used for the same purpose as - // glTexImage2D with NULL data. - // However, some users may have ancient hardware that does not support this function. - // as it was implemented in OpenGL 4.2 and ES 3.0. - // - // glTexSubImage2D(GL_TEXTURE_2D, lod, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, readImage(bufferedImage)) - setupTexture(GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR) } @@ -73,10 +62,10 @@ object TextureUtils { } private fun readImage(bufferedImage: BufferedImage): Long { - val stream = ByteArrayOutputStream() - ImageIO.write(bufferedImage, "png", stream) + val bytes = encoderPreset + .withBufferedImage(bufferedImage) + .toBytes() - val bytes = stream.toByteArray() val buffer = BufferUtils .createByteBuffer(bytes.size) .put(bytes) @@ -89,7 +78,7 @@ object TextureUtils { if (!font.canDisplay(codePoint)) return null val fontMetrics = metricCache.getOrPut(font) { - val image = BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB) + val image = BufferedImage(COMPRESSION_LEVEL, COMPRESSION_LEVEL, BufferedImage.TYPE_INT_ARGB) val graphics2D = image.createGraphics() graphics2D.font = font diff --git a/common/src/main/kotlin/com/lambda/gui/impl/AbstractClickGui.kt b/common/src/main/kotlin/com/lambda/gui/impl/AbstractClickGui.kt index aca17d474..d2643785f 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/AbstractClickGui.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/AbstractClickGui.kt @@ -10,17 +10,13 @@ import com.lambda.gui.api.GuiEvent import com.lambda.gui.api.LambdaGui import com.lambda.gui.api.component.WindowComponent import com.lambda.gui.api.component.core.list.ChildLayer -import com.lambda.gui.impl.clickgui.LambdaClickGui import com.lambda.gui.impl.clickgui.buttons.SettingButton import com.lambda.gui.impl.clickgui.windows.ModuleWindow import com.lambda.gui.impl.clickgui.windows.tag.CustomModuleWindow import com.lambda.gui.impl.clickgui.windows.tag.TagWindow -import com.lambda.gui.impl.hudgui.LambdaHudGui import com.lambda.module.Module import com.lambda.module.modules.client.ClickGui import com.lambda.util.Mouse -import com.mojang.blaze3d.systems.RenderSystem.recordRenderCall -import kotlin.reflect.KMutableProperty import kotlin.reflect.KMutableProperty0 abstract class AbstractClickGui(name: String, owner: Module? = null) : LambdaGui(name, owner) { @@ -128,4 +124,4 @@ abstract class AbstractClickGui(name: String, owner: Module? = null) : LambdaGui if (!isOpen) return closing = true } -} \ No newline at end of file +} diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/ModuleButton.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/ModuleButton.kt index 86ec9933c..df80cf068 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/ModuleButton.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/ModuleButton.kt @@ -174,12 +174,11 @@ class ModuleButton( } override fun performClickAction(e: GuiEvent.MouseClick) { - val sound = when (e.button) { + when (e.button) { Mouse.Button.Left -> { module.toggle() - if (module.isEnabled) LambdaSound.MODULE_ON else LambdaSound.MODULE_OFF - } + } Mouse.Button.Right -> { // Don't let user spam val targetHeight = if (isOpen) settingsHeight else 0.0 @@ -189,13 +188,10 @@ class ModuleButton( if (isOpen) settingsLayer.onEvent(GuiEvent.Show()) updateHeight() - if (isOpen) LambdaSound.SETTINGS_OPEN else LambdaSound.SETTINGS_CLOSE + val sound = if (isOpen) LambdaSound.SETTINGS_OPEN else LambdaSound.SETTINGS_CLOSE + playSoundRandomly(sound.event) } - - else -> return } - - playSoundRandomly(sound.event) } override fun equals(other: Any?) = diff --git a/common/src/main/kotlin/com/lambda/http/Request.kt b/common/src/main/kotlin/com/lambda/http/Request.kt index f975fa0ff..c3002601c 100644 --- a/common/src/main/kotlin/com/lambda/http/Request.kt +++ b/common/src/main/kotlin/com/lambda/http/Request.kt @@ -2,10 +2,12 @@ package com.lambda.http import com.lambda.Lambda import com.lambda.util.FolderRegister.cache +import com.lambda.util.FolderRegister.createFileIfNotExists +import com.lambda.util.FolderRegister.createIfNotExists import java.io.File +import java.io.OutputStream import java.net.HttpURLConnection import java.net.URL -import java.time.Instant import kotlin.time.Duration import kotlin.time.Duration.Companion.days @@ -33,14 +35,21 @@ data class Request( /** * Downloads the resource at the specified path and caches it for future use. * - * @param path The path to the resource. + * @param name The full name of the file to be cached. * @param maxAge The maximum age of the cached resource. Default is 4 days. + * + * @return A pair containing the cached file and a boolean indicating whether the file was downloaded. */ - fun maybeDownload(path: String, maxAge: Duration = 4.days): ByteArray { - val file = File("${cache}/${path.substringAfterLast("/").hashCode()}") + fun maybeDownload( + name: String, + maxAge: Duration = 7.days, + ): File { + val (file, wasCreated) = createFileIfNotExists(name, cache, true) - if (file.exists() && Instant.now().toEpochMilli() - file.lastModified() < maxAge.inWholeMilliseconds) - return file.readBytes() + if (System.currentTimeMillis() - file.lastModified() < maxAge.inWholeMilliseconds + && file.length() > 0 + && !wasCreated) + return file file.writeText("") // Clear the file before writing to it. @@ -64,7 +73,7 @@ data class Request( } } - return file.readBytes() + return file } /** diff --git a/common/src/main/kotlin/com/lambda/interaction/PlayerPacketManager.kt b/common/src/main/kotlin/com/lambda/interaction/PlayerPacketManager.kt index 491ca07bb..1a6e95e6d 100644 --- a/common/src/main/kotlin/com/lambda/interaction/PlayerPacketManager.kt +++ b/common/src/main/kotlin/com/lambda/interaction/PlayerPacketManager.kt @@ -5,7 +5,6 @@ import com.lambda.core.Loadable import com.lambda.event.EventFlow.post import com.lambda.event.EventFlow.postChecked import com.lambda.event.events.PlayerPacketEvent -import com.lambda.interaction.rotation.Rotation.Companion.fixSensitivity import com.lambda.threading.runSafe import com.lambda.util.collections.LimitedOrderedSet import com.lambda.util.math.VecUtils.approximate diff --git a/common/src/main/kotlin/com/lambda/module/Module.kt b/common/src/main/kotlin/com/lambda/module/Module.kt index 4cca48a40..61545b980 100644 --- a/common/src/main/kotlin/com/lambda/module/Module.kt +++ b/common/src/main/kotlin/com/lambda/module/Module.kt @@ -18,6 +18,8 @@ import com.lambda.gui.impl.clickgui.LambdaClickGui import com.lambda.gui.impl.clickgui.buttons.ModuleButton import com.lambda.module.modules.client.ClickGui import com.lambda.module.tag.ModuleTag +import com.lambda.sound.LambdaSound +import com.lambda.sound.SoundManager.playSoundRandomly import com.lambda.util.KeyCode import com.lambda.util.Nameable @@ -119,6 +121,14 @@ abstract class Module( || screen is LambdaClickGui) ) toggle() } + + onEnable { + playSoundRandomly(LambdaSound.MODULE_ON.event) + } + + onDisable { + playSoundRandomly(LambdaSound.MODULE_OFF.event) + } } fun enable() { diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt b/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt index c2726bd6f..099dbdafb 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt @@ -28,6 +28,6 @@ object RenderSettings : Module( private enum class Page { Font, - ESP + ESP, } } diff --git a/common/src/main/kotlin/com/lambda/module/modules/movement/Blink.kt b/common/src/main/kotlin/com/lambda/module/modules/movement/Blink.kt index ddae9757b..702931923 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/movement/Blink.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/movement/Blink.kt @@ -14,10 +14,12 @@ import com.lambda.util.ClientPacket import com.lambda.util.PacketUtils.handlePacketSilently import com.lambda.util.PacketUtils.sendPacketSilently import com.lambda.util.math.ColorUtils.setAlpha +import com.lambda.util.math.VecUtils.minus import net.minecraft.network.packet.c2s.play.PlayerMoveC2SPacket import net.minecraft.network.packet.s2c.play.EntityVelocityUpdateS2CPacket import net.minecraft.util.math.BlockPos import net.minecraft.util.math.Box +import net.minecraft.util.math.Vec3d import java.util.concurrent.ConcurrentLinkedDeque object Blink : Module( @@ -61,6 +63,16 @@ object Blink : Module( return@listener } + listener { event -> + val packet = event.packet + if (packet !is PlayerMoveC2SPacket) return@listener + + val vec = Vec3d(packet.getX(0.0), packet.getY(0.0), packet.getZ(0.0)) + if (vec == Vec3d.ZERO) return@listener + + lastBox = player.boundingBox.offset(vec - player.pos) + } + listener { event -> if (!isActive || !shiftVelocity) return@listener @@ -81,12 +93,6 @@ object Blink : Module( while (packetPool.isNotEmpty()) { packetPool.poll().let { packet -> connection.sendPacketSilently(packet) - - if (packet is PlayerMoveC2SPacket && packet.changesPosition()) { - lastBox = player.boundingBox - .offset(player.pos.negate()) - .offset(packet.getX(0.0), packet.getY(0.0), packet.getZ(0.0)) - } } } diff --git a/common/src/main/kotlin/com/lambda/module/modules/movement/TickShift.kt b/common/src/main/kotlin/com/lambda/module/modules/movement/TickShift.kt index 1f9d92f1c..893674986 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/movement/TickShift.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/movement/TickShift.kt @@ -26,10 +26,13 @@ object TickShift : Module( private val boostAmount by setting("Boost", 3.0, 1.1..20.0, 0.01) private val slowdown by setting("Slowdown", 0.35, 0.01..0.9, 0.01) private val delaySetting by setting("Delay", 0, 0..2000, 10) - private val strict by setting("Strict", true) - private val shiftVelocity by setting("Shift velocity", true) + private val grim by setting("Grim", true) + private val strictSetting by setting("Strict", true) { !grim } + private val shiftVelocity by setting("Shift velocity", true) { grim } private val requiresAura by setting("Requires Aura", false) + private val strict get() = grim || strictSetting + val isActive: Boolean get() { if (requiresAura && (!KillAura.isEnabled || KillAura.target == null)) return false return System.currentTimeMillis() - lastBoost > delaySetting @@ -90,7 +93,7 @@ object TickShift : Module( } listener { event -> - if (!isActive) return@listener + if (!isActive || !grim || event.isCanceled()) return@listener if (event.packet !is CommonPongC2SPacket) return@listener pingPool.add(event.packet) @@ -99,7 +102,7 @@ object TickShift : Module( } listener { event -> - if (!isActive || !shiftVelocity) return@listener + if (!isActive || !grim || !shiftVelocity || event.isCanceled()) return@listener if (event.packet !is EntityVelocityUpdateS2CPacket) return@listener if (event.packet.id != player.id) return@listener diff --git a/common/src/main/kotlin/com/lambda/util/FolderRegister.kt b/common/src/main/kotlin/com/lambda/util/FolderRegister.kt index 26af87db9..86e698d57 100644 --- a/common/src/main/kotlin/com/lambda/util/FolderRegister.kt +++ b/common/src/main/kotlin/com/lambda/util/FolderRegister.kt @@ -8,8 +8,11 @@ import com.lambda.util.FolderRegister.mods import com.lambda.util.FolderRegister.packetLogs import com.lambda.util.FolderRegister.replay import com.lambda.util.StringUtils.sanitizeForFilename +import org.apache.commons.codec.digest.DigestUtils import java.io.File +import java.io.InputStream import java.net.InetSocketAddress +import java.security.MessageDigest /** * The [FolderRegister] object is responsible for managing the directory structure of the application. @@ -31,9 +34,7 @@ object FolderRegister { val cache: File = File(lambda, "cache") fun File.createIfNotExists() { - if (!exists()) { - mkdirs() - } + createFileIfNotExists(this.name, this.parentFile) } fun File.listRecursive(predicate: (File) -> Boolean = { true }) = walk().filter(predicate) @@ -48,4 +49,72 @@ object FolderRegister { path.createIfNotExists() return path } + + /** + * Returns a file with the given name in the specified directory, creating it if it does not exist. + * If the directory is not specified, it will try to parse it from the name. + * Otherwise, it will default to the Lambda directory. + * + * @param name The name of the file. + * @param directory The directory in which the file is located. Default is the Lambda directory. + * @param hash Whether to hash the name of the file. + * @return A pair containing the file and a boolean indicating whether the file was created. + */ + fun createFileIfNotExists(name: String, directory: File? = null, hash: Boolean = false): Pair { + var parsedDir: File = directory ?: lambda + + if (directory == null) { + parsedDir = name.substringAfterLast('/').substringBeforeLast('.') + .let { if (it.isEmpty()) lambda else File(it) } + } + + val compiledName = + if (hash) DigestUtils.sha256Hex(name) + else name + + val file = File(parsedDir, compiledName) + val created = !file.exists() + + if (created) { + file.parentFile.mkdirs() + file.createNewFile() + } + + return file to created + } + + + /** + * Returns a file with the given name in the specified directory, creating it if it does not exist. + * If the directory is not specified, it will try to parse it from the name. + * Otherwise, it will default to the Lambda directory. + * + * @param name The name of the file. + * @param directory The directory in which the file is located. Default is the Lambda directory. + * @param compute A lambda function to compute the file contents if it was created. + */ + @JvmName("getFileOrComputeByteArray") + inline fun getFileOrCompute(name: String, directory: File? = null, compute: () -> ByteArray): File { + val (file, wasCreated) = createFileIfNotExists(name, directory) + if (wasCreated) file.outputStream().use { it.write(compute()) } + + return file + } + + /** + * Returns a file with the given name in the specified directory, creating it if it does not exist. + * If the directory is not specified, it will try to parse it from the name. + * Otherwise, it will default to the Lambda directory. + * + * @param name The name of the file. + * @param directory The directory in which the file is located. Default is the Lambda directory. + * @param compute A lambda function to compute the file contents if it was created. + */ + @JvmName("getFileOrComputeInputStream") + inline fun getFileOrCompute(name: String, directory: File? = null, compute: () -> InputStream): File { + val (file, wasCreated) = createFileIfNotExists(name, directory) + if (wasCreated) file.outputStream().use { compute().copyTo(it) } + + return file + } } diff --git a/fabric/build.gradle.kts b/fabric/build.gradle.kts index 2cbb32749..8844cc10f 100644 --- a/fabric/build.gradle.kts +++ b/fabric/build.gradle.kts @@ -67,6 +67,7 @@ dependencies { includeLib("org.javassist:javassist:3.28.0-GA") includeLib("dev.babbaj:nether-pathfinder:1.5") includeLib("com.github.Edouard127:KDiscordIPC:$discordIPCVersion") + includeLib("com.pngencoder:pngencoder:0.15.0") // Add mods to the mod jar includeMod("net.fabricmc.fabric-api:fabric-api:$fabricApiVersion+$minecraftVersion") diff --git a/forge/build.gradle.kts b/forge/build.gradle.kts index 906913a81..935fa55ed 100644 --- a/forge/build.gradle.kts +++ b/forge/build.gradle.kts @@ -81,6 +81,7 @@ dependencies { includeLib("org.reflections:reflections:0.10.2") includeLib("org.javassist:javassist:3.28.0-GA") includeLib("com.github.Edouard127:KDiscordIPC:$discordIPCVersion") + includeLib("com.pngencoder:pngencoder:0.15.0") // Add mods to the mod jar includeMod("thedarkcolour:kotlinforforge:$kotlinForgeVersion") diff --git a/neoforge/build.gradle.kts b/neoforge/build.gradle.kts index 12c4af40c..9978da288 100644 --- a/neoforge/build.gradle.kts +++ b/neoforge/build.gradle.kts @@ -68,6 +68,7 @@ dependencies { includeLib("org.javassist:javassist:3.28.0-GA") includeLib("dev.babbaj:nether-pathfinder:1.5") includeLib("com.github.Edouard127:KDiscordIPC:$discordIPCVersion") + includeLib("com.pngencoder:pngencoder:0.15.0") // Add mods to the mod jar includeMod("thedarkcolour:kotlinforforge-neoforge:$kotlinForgeVersion")