diff --git a/common/src/main/java/com/lambda/mixin/render/GlStateManagerMixin.java b/common/src/main/java/com/lambda/mixin/render/GlStateManagerMixin.java new file mode 100644 index 000000000..82e2b29ad --- /dev/null +++ b/common/src/main/java/com/lambda/mixin/render/GlStateManagerMixin.java @@ -0,0 +1,49 @@ +package com.lambda.mixin.render; + +import com.lambda.graphics.gl.GlStateUtils; +import com.mojang.blaze3d.platform.GlStateManager; +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.CallbackInfo; + +import static org.lwjgl.opengl.GL11.*; +import static org.lwjgl.opengl.GL11.GL_CULL_FACE; + +@Mixin(GlStateManager.class) +public class GlStateManagerMixin { + @Inject(method = "_enableDepthTest", at = @At("TAIL"), remap = false) + private static void depthTestEnable(CallbackInfo ci) { + GlStateUtils.capSet(GL_DEPTH_TEST, true); + } + + @Inject(method = "_disableDepthTest", at = @At("TAIL"), remap = false) + private static void depthTestDisable(CallbackInfo ci) { + GlStateUtils.capSet(GL_DEPTH_TEST, false); + } + + @Inject(method = "_depthMask", at = @At("TAIL"), remap = false) + private static void depthMask(boolean mask, CallbackInfo ci) { + GlStateUtils.capSet(GL_DEPTH, mask); + } + + @Inject(method = "_enableBlend", at = @At("TAIL"), remap = false) + private static void blendEnable(CallbackInfo ci) { + GlStateUtils.capSet(GL_BLEND, true); + } + + @Inject(method = "_disableBlend", at = @At("TAIL"), remap = false) + private static void blendDisable(CallbackInfo ci) { + GlStateUtils.capSet(GL_BLEND, false); + } + + @Inject(method = "_enableCull", at = @At("TAIL"), remap = false) + private static void cullEnable(CallbackInfo ci) { + GlStateUtils.capSet(GL_CULL_FACE, true); + } + + @Inject(method = "_disableCull", at = @At("TAIL"), remap = false) + private static void cullDisable(CallbackInfo ci) { + GlStateUtils.capSet(GL_CULL_FACE, false); + } +} diff --git a/common/src/main/java/com/lambda/mixin/render/InGameHudMixin.java b/common/src/main/java/com/lambda/mixin/render/InGameHudMixin.java new file mode 100644 index 000000000..f98c97947 --- /dev/null +++ b/common/src/main/java/com/lambda/mixin/render/InGameHudMixin.java @@ -0,0 +1,17 @@ +package com.lambda.mixin.render; + +import com.lambda.graphics.RenderMain; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.hud.InGameHud; +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.CallbackInfo; + +@Mixin(InGameHud.class) +public class InGameHudMixin { + @Inject(method = "render", at = @At("TAIL")) + private void onRender(DrawContext context, float tickDelta, CallbackInfo ci) { + RenderMain.render2D(); + } +} diff --git a/common/src/main/java/com/lambda/mixin/render/VertexBufferMixin.java b/common/src/main/java/com/lambda/mixin/render/VertexBufferMixin.java new file mode 100644 index 000000000..6c3584660 --- /dev/null +++ b/common/src/main/java/com/lambda/mixin/render/VertexBufferMixin.java @@ -0,0 +1,25 @@ +package com.lambda.mixin.render; + +import com.lambda.graphics.gl.VaoUtils; +import com.mojang.blaze3d.systems.RenderSystem; +import net.minecraft.client.gl.VertexBuffer; +import net.minecraft.client.render.BufferBuilder; +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; + +import java.nio.ByteBuffer; + +@Mixin(VertexBuffer.class) +public class VertexBufferMixin { + @Shadow + private int indexBufferId; + + @Inject(method = "uploadIndexBuffer", at = @At("RETURN")) + private void onConfigureIndexBuffer(BufferBuilder.DrawParameters parameters, ByteBuffer vertexBuffer, CallbackInfoReturnable cir) { + RenderSystem.ShapeIndexBuffer value = cir.getReturnValue(); + VaoUtils.lastIbo = value == null ? this.indexBufferId : value.id; + } +} diff --git a/common/src/main/kotlin/com/lambda/Lambda.kt b/common/src/main/kotlin/com/lambda/Lambda.kt index f2acfcdd2..3a861bd35 100644 --- a/common/src/main/kotlin/com/lambda/Lambda.kt +++ b/common/src/main/kotlin/com/lambda/Lambda.kt @@ -3,7 +3,10 @@ package com.lambda import com.google.gson.Gson import com.google.gson.GsonBuilder import com.lambda.config.serializer.* +import com.lambda.config.serializer.gui.ModuleTagSerializer +import com.lambda.config.serializer.gui.TagWindowSerializer import com.lambda.core.Loader +import com.lambda.gui.impl.clickgui.windows.TagWindow import com.lambda.module.tag.ModuleTag import com.lambda.util.KeyCode import net.minecraft.block.Block @@ -24,6 +27,7 @@ object Lambda { val gson: Gson = GsonBuilder() .setPrettyPrinting() .registerTypeAdapter(ModuleTag::class.java, ModuleTagSerializer) + .registerTypeAdapter(TagWindow::class.java, TagWindowSerializer) .registerTypeAdapter(KeyCode::class.java, KeyCodeSerializer) .registerTypeAdapter(Color::class.java, ColorSerializer) .registerTypeAdapter(BlockPos::class.java, BlockPosSerializer) diff --git a/common/src/main/kotlin/com/lambda/command/CommandManager.kt b/common/src/main/kotlin/com/lambda/command/CommandManager.kt index 4c3f1f7a4..c55d75982 100644 --- a/common/src/main/kotlin/com/lambda/command/CommandManager.kt +++ b/common/src/main/kotlin/com/lambda/command/CommandManager.kt @@ -20,6 +20,7 @@ import org.reflections.Reflections import org.reflections.scanners.Scanners import org.reflections.util.ClasspathHelper import org.reflections.util.ConfigurationBuilder +import java.awt.Color import kotlin.math.max import kotlin.math.min @@ -95,7 +96,7 @@ object CommandManager : Configurable(LambdaConfig), Loadable { val position = min(syntax.input.length, syntax.cursor) player.sendMessage(buildText { clickEvent(suggestCommand("$prefix${reader.string}")) { - color(Color.GREY) { + color(Color.GRAY) { if (position > ERROR_PADDING) { literal("...") } diff --git a/common/src/main/kotlin/com/lambda/command/commands/ModuleCommand.kt b/common/src/main/kotlin/com/lambda/command/commands/ModuleCommand.kt index 5d8e73ee4..6c3165f86 100644 --- a/common/src/main/kotlin/com/lambda/command/commands/ModuleCommand.kt +++ b/common/src/main/kotlin/com/lambda/command/commands/ModuleCommand.kt @@ -19,6 +19,7 @@ import com.lambda.util.Communication.warn import com.lambda.util.StringUtils import com.lambda.util.text.* import com.lambda.util.text.ClickEvents.suggestCommand +import java.awt.Color object ModuleCommand : LambdaCommand { override val name = "module" @@ -36,7 +37,7 @@ object ModuleCommand : LambdaCommand { } this@ModuleCommand.info(buildText { - styled(Color.GREY) { + styled(Color.GRAY) { literal("Enabled Modules: ") } enabled.forEachIndexed { index, module -> @@ -70,7 +71,7 @@ object ModuleCommand : LambdaCommand { } ?: return@executeWithResult failure(buildText { styled(Color.RED) { literal("Module ") - styled(Color.GREY) { + styled(Color.GRAY) { literal("$name ") } literal("not found!") @@ -88,7 +89,7 @@ object ModuleCommand : LambdaCommand { literal(", ") } clickEvent(suggestCommand("$prefix${input.replace(name, s)}")) { - styled(Color.GREY) { + styled(Color.GRAY) { literal(s) } } @@ -102,7 +103,7 @@ object ModuleCommand : LambdaCommand { } else { if (enable().value() == module.isEnabled) { this@ModuleCommand.warn(buildText { - styled(Color.GREY) { + styled(Color.GRAY) { literal("$name already ") literal(if (module.isEnabled) "enabled" else "disabled") } @@ -117,7 +118,7 @@ object ModuleCommand : LambdaCommand { } } this@ModuleCommand.info(buildText { - styled(Color.GREY) { + styled(Color.GRAY) { literal("$name ") } styled(if (module.isEnabled) Color.GREEN else Color.RED) { diff --git a/common/src/main/kotlin/com/lambda/config/Configurable.kt b/common/src/main/kotlin/com/lambda/config/Configurable.kt index be81458f8..00e091f41 100644 --- a/common/src/main/kotlin/com/lambda/config/Configurable.kt +++ b/common/src/main/kotlin/com/lambda/config/Configurable.kt @@ -2,6 +2,7 @@ package com.lambda.config import com.google.gson.JsonElement import com.google.gson.JsonObject +import com.google.gson.reflect.TypeToken import com.lambda.Lambda import com.lambda.Lambda.LOG import com.lambda.config.settings.CharSetting @@ -21,6 +22,7 @@ import com.lambda.util.Nameable import net.minecraft.block.Block import net.minecraft.util.math.BlockPos import java.awt.Color +import java.lang.reflect.Type /** * Represents a set of [AbstractSetting]s that are associated with the [name] of the [Configurable]. @@ -165,7 +167,7 @@ abstract class Configurable(configuration: Configuration) : Jsonable, Nameable { defaultValue: List, description: String = "", noinline visibility: () -> Boolean = { true }, - ) = ListSetting(name, defaultValue, description, visibility).also { + ) = ListSetting(name, defaultValue, object : TypeToken>() {}.type, description, visibility).also { settings.add(it) } @@ -191,7 +193,7 @@ abstract class Configurable(configuration: Configuration) : Jsonable, Nameable { defaultValue: Map, description: String = "", noinline visibility: () -> Boolean = { true }, - ) = MapSetting(name, defaultValue, description, visibility).also { + ) = MapSetting(name, defaultValue, object : TypeToken>() {}.type, description, visibility).also { settings.add(it) } @@ -217,7 +219,7 @@ abstract class Configurable(configuration: Configuration) : Jsonable, Nameable { defaultValue: Set, description: String = "", noinline visibility: () -> Boolean = { true }, - ) = SetSetting(name, defaultValue, description, visibility).also { + ) = SetSetting(name, defaultValue, object : TypeToken>() {}.type, description, visibility).also { settings.add(it) } diff --git a/common/src/main/kotlin/com/lambda/config/Configuration.kt b/common/src/main/kotlin/com/lambda/config/Configuration.kt index 61a4e4c2f..3dc6768a7 100644 --- a/common/src/main/kotlin/com/lambda/config/Configuration.kt +++ b/common/src/main/kotlin/com/lambda/config/Configuration.kt @@ -41,7 +41,7 @@ abstract class Configuration : Jsonable { init { unsafeListener { tryLoad() } - unsafeListener { trySave() } + unsafeListener(Int.MIN_VALUE) { trySave() } configurations.add(this) } @@ -90,7 +90,7 @@ abstract class Configuration : Jsonable { } .onFailure { val message = "Failed to load ${configName.capitalize()} config, loading backup" - LOG.error(message) + LOG.error(message, it) this@Configuration.logError(message) runCatching { load(backup) } .onSuccess { diff --git a/common/src/main/kotlin/com/lambda/config/configurations/GuiConfig.kt b/common/src/main/kotlin/com/lambda/config/configurations/GuiConfig.kt new file mode 100644 index 000000000..8b6a2fc59 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/config/configurations/GuiConfig.kt @@ -0,0 +1,9 @@ +package com.lambda.config.configurations + +import com.lambda.config.Configuration +import com.lambda.util.FolderRegister + +object GuiConfig : Configuration() { + override val configName = "gui" + override val primary = FolderRegister.config.resolve("$configName.json") +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/config/serializer/ModuleTagSerializer.kt b/common/src/main/kotlin/com/lambda/config/serializer/gui/ModuleTagSerializer.kt similarity index 94% rename from common/src/main/kotlin/com/lambda/config/serializer/ModuleTagSerializer.kt rename to common/src/main/kotlin/com/lambda/config/serializer/gui/ModuleTagSerializer.kt index c6c40cad5..c9665de04 100644 --- a/common/src/main/kotlin/com/lambda/config/serializer/ModuleTagSerializer.kt +++ b/common/src/main/kotlin/com/lambda/config/serializer/gui/ModuleTagSerializer.kt @@ -1,4 +1,4 @@ -package com.lambda.config.serializer +package com.lambda.config.serializer.gui import com.google.gson.* import com.lambda.module.tag.ModuleTag diff --git a/common/src/main/kotlin/com/lambda/config/serializer/gui/TagWindowSerializer.kt b/common/src/main/kotlin/com/lambda/config/serializer/gui/TagWindowSerializer.kt new file mode 100644 index 000000000..337e34474 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/config/serializer/gui/TagWindowSerializer.kt @@ -0,0 +1,52 @@ +package com.lambda.config.serializer.gui + +import com.google.gson.* +import com.lambda.gui.impl.clickgui.windows.TagWindow +import com.lambda.module.tag.ModuleTag +import com.lambda.util.math.Vec2d +import java.lang.reflect.Type + +object TagWindowSerializer : JsonSerializer, JsonDeserializer { + override fun serialize( + src: TagWindow?, + typeOfSrc: Type?, + context: JsonSerializationContext?, + ): JsonElement = src?.let { + JsonObject().apply { + addProperty("title", it.title) + add("tags", JsonArray().apply { + it.tags.forEach { tag -> + add(tag.name) + } + }) + addProperty("width", it.width) + addProperty("height", it.height) + addProperty("isOpen", it.isOpen) + add("position", JsonArray().apply { + add(it.position.x) + add(it.position.y) + }) + } + } ?: JsonNull.INSTANCE + + override fun deserialize( + json: JsonElement?, + typeOfT: Type?, + context: JsonDeserializationContext?, + ) = json?.asJsonObject?.let { + TagWindow( + tags = it["tags"].asJsonArray.map { tag -> + ModuleTag(tag.asString) + }.toSet(), + title = it["title"].asString, + width = it["width"].asDouble, + height = it["height"].asDouble + ).apply { + isOpen = it["isOpen"].asBoolean + position = Vec2d( + it["position"].asJsonArray[0].asDouble, + it["position"].asJsonArray[1].asDouble + ) + } + } ?: throw JsonParseException("Invalid window data") +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/config/settings/collections/ListSetting.kt b/common/src/main/kotlin/com/lambda/config/settings/collections/ListSetting.kt index e2c687ffe..cb6452996 100644 --- a/common/src/main/kotlin/com/lambda/config/settings/collections/ListSetting.kt +++ b/common/src/main/kotlin/com/lambda/config/settings/collections/ListSetting.kt @@ -1,13 +1,14 @@ package com.lambda.config.settings.collections import com.google.gson.JsonElement -import com.google.gson.reflect.TypeToken import com.lambda.Lambda.gson import com.lambda.config.AbstractSetting +import java.lang.reflect.Type -class ListSetting( +class ListSetting( override val name: String, defaultValue: List, + private val type: Type, description: String, visibility: () -> Boolean, ) : AbstractSetting>( @@ -16,7 +17,6 @@ class ListSetting( visibility ) { override fun loadFromJson(serialized: JsonElement) { - val listType = object : TypeToken>() {}.type - value = gson.fromJson(serialized, listType) + value = gson.fromJson(serialized, type) } } \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/config/settings/collections/MapSetting.kt b/common/src/main/kotlin/com/lambda/config/settings/collections/MapSetting.kt index 0c1b5e46f..a9190f4d5 100644 --- a/common/src/main/kotlin/com/lambda/config/settings/collections/MapSetting.kt +++ b/common/src/main/kotlin/com/lambda/config/settings/collections/MapSetting.kt @@ -1,13 +1,14 @@ package com.lambda.config.settings.collections -import com.google.common.reflect.TypeToken import com.google.gson.JsonElement import com.lambda.Lambda.gson import com.lambda.config.AbstractSetting +import java.lang.reflect.Type class MapSetting( override val name: String, defaultValue: Map, + private val type: Type, description: String, visibility: () -> Boolean, ) : AbstractSetting>( @@ -16,7 +17,6 @@ class MapSetting( visibility ) { override fun loadFromJson(serialized: JsonElement) { - val mapType = object : TypeToken>() {}.type - value = gson.fromJson(serialized, mapType) + value = gson.fromJson(serialized, type) } } \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/config/settings/collections/SetSetting.kt b/common/src/main/kotlin/com/lambda/config/settings/collections/SetSetting.kt index b0379cce4..5bc7b8264 100644 --- a/common/src/main/kotlin/com/lambda/config/settings/collections/SetSetting.kt +++ b/common/src/main/kotlin/com/lambda/config/settings/collections/SetSetting.kt @@ -1,13 +1,14 @@ package com.lambda.config.settings.collections import com.google.gson.JsonElement -import com.google.gson.reflect.TypeToken import com.lambda.Lambda.gson import com.lambda.config.AbstractSetting +import java.lang.reflect.Type class SetSetting( override val name: String, defaultValue: Set, + private val type: Type, description: String, visibility: () -> Boolean, ) : AbstractSetting>( @@ -16,7 +17,6 @@ class SetSetting( visibility ) { override fun loadFromJson(serialized: JsonElement) { - val setType = object : TypeToken>() {}.type - value = gson.fromJson(serialized, setType) + value = gson.fromJson(serialized, type) } } \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/core/Loader.kt b/common/src/main/kotlin/com/lambda/core/Loader.kt index a30ecaef5..ff7ab72ef 100644 --- a/common/src/main/kotlin/com/lambda/core/Loader.kt +++ b/common/src/main/kotlin/com/lambda/core/Loader.kt @@ -3,6 +3,10 @@ package com.lambda.core import com.lambda.Lambda import com.lambda.Lambda.LOG import com.lambda.command.CommandManager +import com.lambda.config.configurations.GuiConfig +import com.lambda.graphics.renderer.gui.font.LambdaFont +import com.lambda.gui.impl.clickgui.GuiConfigurable +import com.lambda.gui.impl.clickgui.LambdaClickGui import com.lambda.interaction.PlayerPacketManager import com.lambda.interaction.RotationManager import com.lambda.module.ModuleRegistry @@ -14,7 +18,8 @@ object Loader { ModuleRegistry, CommandManager, RotationManager, - PlayerPacketManager + PlayerPacketManager, + LambdaFont.Loader ) fun initialize() { @@ -33,5 +38,7 @@ object Loader { } LOG.info("${Lambda.MOD_NAME} ${Lambda.VERSION} was successfully initialized (${initTime}ms)") + + GuiConfigurable // ToDo: Find more elegant solution } } diff --git a/common/src/main/kotlin/com/lambda/event/events/RenderEvent.kt b/common/src/main/kotlin/com/lambda/event/events/RenderEvent.kt index cbe671710..178163df5 100644 --- a/common/src/main/kotlin/com/lambda/event/events/RenderEvent.kt +++ b/common/src/main/kotlin/com/lambda/event/events/RenderEvent.kt @@ -1,9 +1,19 @@ package com.lambda.event.events +import com.lambda.Lambda.mc import com.lambda.event.Event import com.lambda.event.callback.Cancellable import com.lambda.event.callback.ICancellable +import com.lambda.util.math.Vec2d abstract class RenderEvent : Event { + class World : RenderEvent() + + abstract class GUI(val scaleFactor: Double) : RenderEvent() { + class Scaled(scaleFactor: Double) : GUI(scaleFactor) + class Fixed : GUI(1.0) + + val screenSize = Vec2d(mc.window.framebufferWidth, mc.window.framebufferHeight) / scaleFactor + } class UpdateTarget : RenderEvent(), ICancellable by Cancellable() } \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/RenderMain.kt b/common/src/main/kotlin/com/lambda/graphics/RenderMain.kt new file mode 100644 index 000000000..c3a935af3 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/RenderMain.kt @@ -0,0 +1,40 @@ +package com.lambda.graphics + +import com.lambda.Lambda.mc +import com.lambda.event.EventFlow.post +import com.lambda.event.events.RenderEvent +import com.lambda.graphics.gl.GlStateUtils.setupGL +import com.lambda.graphics.gl.Matrices +import com.lambda.graphics.gl.Matrices.resetMatrix +import com.lambda.graphics.gl.Matrices.translate +import com.lambda.module.modules.client.GuiSettings +import org.joml.Matrix4f + +object RenderMain { + val projectionMatrix = Matrix4f() + val modelViewMatrix: Matrix4f get() = Matrices.stack.peek().positionMatrix + + @JvmStatic + fun render2D() { + resetMatrix() + translate(0.0, 0.0, -3000.0) + + setupGL { + rescale(GuiSettings.scale) + RenderEvent.GUI.Scaled(GuiSettings.scale).post() + + rescale(1.0) + RenderEvent.GUI.Fixed().post() + } + } + + private fun rescale(factor: Double) { + val width = mc.window.framebufferWidth.toFloat() + val height = mc.window.framebufferHeight.toFloat() + + val scaledWidth = width / factor + val scaledHeight = height / factor + + projectionMatrix.setOrtho(0f, scaledWidth.toFloat(), scaledHeight.toFloat(), 0f, 1000f, 21000f) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/animation/Animation.kt b/common/src/main/kotlin/com/lambda/graphics/animation/Animation.kt new file mode 100644 index 000000000..6afd4908f --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/animation/Animation.kt @@ -0,0 +1,50 @@ +package com.lambda.graphics.animation + +import com.lambda.Lambda.mc +import com.lambda.util.math.MathUtils.lerp +import com.lambda.util.primitives.extension.partialTicks +import kotlin.math.abs +import kotlin.reflect.KProperty + +class Animation(initialValue: Double, val update: (Double) -> Double) { + private var prevValue = initialValue + private var currValue = initialValue + + operator fun getValue(thisRef: Any?, property: KProperty<*>) = + lerp(prevValue, currValue, mc.partialTicks) + + operator fun setValue(thisRef: Any?, property: KProperty<*>, valueIn: Double) { + prevValue = valueIn + currValue = valueIn + } + + fun tick() { + prevValue = currValue + currValue = update(currValue) + } + + companion object { + fun AnimationTicker.exp(min: () -> Double, max: () -> Double, speed: Double, flag: () -> Boolean) = + exp(min, max, { speed }, flag) + + fun AnimationTicker.exp(min: Double, max: Double, speed: () -> Double, flag: () -> Boolean) = + exp({ min }, { max }, speed, flag) + + fun AnimationTicker.exp(min: Double, max: Double, speed: Double, flag: () -> Boolean) = + exp({ min }, { max }, { speed }, flag) + + @Suppress("NAME_SHADOWING") + fun AnimationTicker.exp(min: () -> Double, max: () -> Double, speed: () -> Double, flag: () -> Boolean) = + Animation(min()) { + val min = min(); val max = max() + val target = if (flag()) max else min + + if (abs(target - it) < CLAMP * abs(max - min)) target + else lerp(it, target, speed()) + }.apply(::register) + + // Exponent animation will never reach target value + private const val CLAMP = 0.001 + } +} + diff --git a/common/src/main/kotlin/com/lambda/graphics/animation/AnimationTicker.kt b/common/src/main/kotlin/com/lambda/graphics/animation/AnimationTicker.kt new file mode 100644 index 000000000..1d11d281a --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/animation/AnimationTicker.kt @@ -0,0 +1,11 @@ +package com.lambda.graphics.animation + +class AnimationTicker { + private val animations = mutableListOf() + + fun register(animation: Animation) = + animations.add(animation) + + fun tick() = + animations.forEach(Animation::tick) +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/vao/IRenderContext.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/vao/IRenderContext.kt new file mode 100644 index 000000000..d19a7ec69 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/vao/IRenderContext.kt @@ -0,0 +1,24 @@ +package com.lambda.graphics.buffer.vao + +import java.awt.Color + +interface IRenderContext { + fun vec3(x: Double, y: Double, z: Double): IRenderContext + fun vec2(x: Double, y: Double): IRenderContext + fun color(color: Color): IRenderContext + fun end(): Int + + fun putLine(vertex1: Int, vertex2: Int) + fun putTriangle(vertex1: Int, vertex2: Int, vertex3: Int) + fun putQuad(vertex1: Int, vertex2: Int, vertex3: Int, vertex4: Int) + + fun render() + fun upload() + fun clear() + + fun grow(amount: Int) + + fun use(block: IRenderContext.() -> Unit) { + block() + } +} \ No newline at end of file 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 new file mode 100644 index 000000000..dfec78dc7 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/vao/VAO.kt @@ -0,0 +1,198 @@ +package com.lambda.graphics.buffer.vao + +import com.lambda.graphics.buffer.vao.vertex.VertexAttrib +import com.lambda.graphics.buffer.vao.vertex.VertexMode +import com.lambda.graphics.gl.Memory.address +import com.lambda.graphics.gl.Memory.byteBuffer +import com.lambda.graphics.gl.Memory.capacity +import com.lambda.graphics.gl.Memory.color +import com.lambda.graphics.gl.Memory.copy +import com.lambda.graphics.gl.Memory.int +import com.lambda.graphics.gl.Memory.vec2 +import com.lambda.graphics.gl.Memory.vec3 +import com.lambda.graphics.gl.VaoUtils +import com.lambda.graphics.gl.VaoUtils.bindIndexBuffer +import com.lambda.graphics.gl.VaoUtils.bindVertexArray +import com.lambda.graphics.gl.VaoUtils.bindVertexBuffer +import com.lambda.graphics.gl.VaoUtils.bufferData +import com.lambda.graphics.gl.VaoUtils.unbindIndexBuffer +import com.lambda.graphics.gl.VaoUtils.unbindVertexArray +import com.lambda.graphics.gl.VaoUtils.unbindVertexBuffer +import com.lambda.threading.runOnGameThread +import com.mojang.blaze3d.systems.RenderSystem.drawElements +import org.lwjgl.opengl.GL30C.* +import java.awt.Color +import java.nio.ByteBuffer + +class VAO( + private val drawMode: VertexMode, + attribGroup: VertexAttrib.Group +) : IRenderContext { + private var vao = 0 + private var vbo = 0 + private var ibo = 0 + + private val objectSize: Int + + private lateinit var vertices: ByteBuffer + private var verticesPointer = 0L + private var verticesPosition = 0L + + private lateinit var indices: ByteBuffer + private var indicesPointer = 0L + private var indicesCount = 0 + + private var vertexIndex = 0 + + init { + val stride = attribGroup.stride + objectSize = stride * drawMode.indicesCount + + runOnGameThread { + vertices = byteBuffer(objectSize * 256 * 4) + verticesPointer = address(vertices) + verticesPosition = verticesPointer + + indices = byteBuffer(drawMode.indicesCount * 512 * 4) + indicesPointer = address(indices) + + vao = glGenVertexArrays() + bindVertexArray(vao) + + vbo = glGenBuffers() + bindVertexBuffer(vbo) + + ibo = glGenBuffers() + bindIndexBuffer(ibo) + + var pointer = 0L + attribGroup.attributes.forEachIndexed { index, attrib -> + VaoUtils.enableVertexAttribute(index) + VaoUtils.vertexAttribute(index, attrib.componentCount, attrib.gl, attrib.normalized, stride, pointer) + pointer += attrib.size + } + + unbindVertexArray() + unbindVertexBuffer() + unbindIndexBuffer() + } + } + + override fun vec3(x: Double, y: Double, z: Double): VAO { + verticesPosition += vec3(verticesPosition, x, y, z) + return this + } + + override fun vec2(x: Double, y: Double): VAO { + verticesPosition += vec2(verticesPosition, x, y) + return this + } + + override fun color(color: Color): VAO { + verticesPosition += color(verticesPosition, color) + return this + } + + override fun end() = vertexIndex++ + + override fun putLine(vertex1: Int, vertex2: Int) { + growIndices(2) + val p = indicesPointer + indicesCount * 4L + + int(p + 0, vertex1) + int(p + 4, vertex2) + indicesCount += 2 + } + + override fun putTriangle(vertex1: Int, vertex2: Int, vertex3: Int) { + growIndices(3) + val p = indicesPointer + indicesCount * 4L + + int(p + 0, vertex1) + int(p + 4, vertex2) + int(p + 8, vertex3) + indicesCount += 3 + } + + override fun putQuad(vertex1: Int, vertex2: Int, vertex3: Int, vertex4: Int) { + growIndices(6) + val p = indicesPointer + indicesCount * 4L + + int(p + 0, vertex1) + int(p + 4, vertex2) + int(p + 8, vertex3) + int(p + 12, vertex3) + int(p + 16, vertex4) + int(p + 20, vertex1) + indicesCount += 6 + } + + override fun grow(amount: Int) { + val cap = vertices.capacity + if ((vertexIndex + amount + 1) * objectSize < cap) return + + val offset = verticesPosition - verticesPointer + var newSize = cap * 2 + if (newSize % objectSize != 0) newSize += newSize % objectSize + val newVertices = byteBuffer(newSize) + + val from = address(vertices) + val to = address(newVertices) + copy(from, to, offset) + + vertices = newVertices + verticesPointer = address(vertices) + verticesPosition = verticesPointer + offset + } + + private fun growIndices(amount: Int) { + val cap = indices.capacity + if ((indicesCount + amount) * 4 < cap) return + + var newSize = cap * 2 + if (newSize % drawMode.indicesCount != 0) newSize += newSize % (drawMode.indicesCount * 4) + val newIndices = byteBuffer(newSize) + + val from = address(indices) + val to = address(newIndices) + copy(from, to, indicesCount * 4L) + + indices = newIndices + indicesPointer = address(indices) + } + + override fun render() { + if (indicesCount <= 0) return + + bindVertexArray(vao) + drawElements(drawMode.gl, indicesCount, GL_UNSIGNED_INT) + unbindVertexArray() + } + + override fun upload() { + if (indicesCount <= 0) return + + val vboData = vertices.limit((verticesPosition - verticesPointer).toInt()) + val iboData = indices.limit(indicesCount * 4) + + bindVertexBuffer(vbo) + bufferData(GL_ARRAY_BUFFER, vboData, GL_DYNAMIC_DRAW) + unbindVertexBuffer() + + bindIndexBuffer(ibo) + bufferData(GL_ELEMENT_ARRAY_BUFFER, iboData, GL_DYNAMIC_DRAW) + unbindIndexBuffer() + } + + override fun clear() { + verticesPosition = verticesPointer + vertexIndex = 0 + indicesCount = 0 + } + + fun destroy() { + glDeleteBuffers(ibo) + glDeleteBuffers(vbo) + glDeleteVertexArrays(vao) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/vao/vertex/VertexAttrib.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/vao/vertex/VertexAttrib.kt new file mode 100644 index 000000000..8085a20c4 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/vao/vertex/VertexAttrib.kt @@ -0,0 +1,19 @@ +package com.lambda.graphics.buffer.vao.vertex + +import com.lambda.graphics.gl.GLObject +import org.lwjgl.opengl.GL11C.* + +enum class VertexAttrib(val componentCount: Int, componentSize: Int, val normalized: Boolean, override val gl: Int) : GLObject { + Vec2(2, 4, false, GL_FLOAT), + Vec3(3, 4, false, GL_FLOAT), + Color(4, 1, true, GL_UNSIGNED_BYTE); + + val size = componentCount * componentSize + + enum class Group(vararg val attributes: VertexAttrib) { + FONT(Vec2, Vec2, Color), + RECT(Vec2, Vec2, Vec3, Color); + + val stride = attributes.sumOf { attribute -> attribute.size } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/vao/vertex/VertexMode.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/vao/vertex/VertexMode.kt new file mode 100644 index 000000000..8ced5baa3 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/vao/vertex/VertexMode.kt @@ -0,0 +1,9 @@ +package com.lambda.graphics.buffer.vao.vertex + +import com.lambda.graphics.gl.GLObject +import org.lwjgl.opengl.GL11C.* + +enum class VertexMode(val indicesCount: Int, override val gl: Int) : GLObject { + LINES(2, GL_LINES), + TRIANGLES(3, GL_TRIANGLES) +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/gl/GLObject.kt b/common/src/main/kotlin/com/lambda/graphics/gl/GLObject.kt new file mode 100644 index 000000000..3046a03ff --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/gl/GLObject.kt @@ -0,0 +1,5 @@ +package com.lambda.graphics.gl + +interface GLObject { + val gl: Int +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/gl/GlStateUtils.kt b/common/src/main/kotlin/com/lambda/graphics/gl/GlStateUtils.kt new file mode 100644 index 000000000..b0e474eed --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/gl/GlStateUtils.kt @@ -0,0 +1,75 @@ +package com.lambda.graphics.gl + +import com.mojang.blaze3d.systems.RenderSystem +import com.mojang.blaze3d.systems.RenderSystem.depthMask +import org.lwjgl.opengl.GL30C + +@Suppress("NOTHING_TO_INLINE") +object GlStateUtils { + private var depthTestState = true + private var depthMaskState = true + private var blendState = false + private var cullState = true + + fun setupGL(block: () -> Unit) { + val savedDepthTest = depthTestState + val savedDepthMask = depthMaskState + val savedBlend = blendState + val savedCull = cullState + + depthTest(false) + depthMask(false) + blend(true) + cull(false) + lineSmooth(true) + + block() + + depthTest(savedDepthTest) + depthMask(savedDepthMask) + blend(savedBlend) + cull(savedCull) + lineSmooth(false) + } + + fun withDepth(block: () -> Unit) { + depthTest(true) + block() + depthTest(false) + } + + @JvmStatic + fun capSet(id: Int, flag: Boolean) { + val field = when (id) { + GL30C.GL_DEPTH_TEST -> ::depthTestState + GL30C.GL_DEPTH -> ::depthMaskState + GL30C.GL_BLEND -> ::blendState + GL30C.GL_CULL_FACE -> ::cullState + else -> return + } + + field.set(flag) + } + + private inline fun blend(flag: Boolean) { + if (flag) { + RenderSystem.enableBlend() + RenderSystem.defaultBlendFunc() + } else RenderSystem.disableBlend() + } + + private inline fun cull(flag: Boolean) { + if (flag) RenderSystem.enableCull() + else RenderSystem.disableCull() + } + + private inline fun depthTest(flag: Boolean) { + if (flag) RenderSystem.enableDepthTest() + else RenderSystem.disableDepthTest() + } + + private inline fun lineSmooth(flag: Boolean) { + if (flag) GL30C.glEnable(GL30C.GL_LINE_SMOOTH) + else GL30C.glDisable(GL30C.GL_LINE_SMOOTH) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/gl/Matrices.kt b/common/src/main/kotlin/com/lambda/graphics/gl/Matrices.kt new file mode 100644 index 000000000..1932ec531 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/gl/Matrices.kt @@ -0,0 +1,33 @@ +package com.lambda.graphics.gl + +import net.minecraft.client.util.math.MatrixStack + +object Matrices { + var stack = MatrixStack() + private val matrix get() = stack.peek().positionMatrix + + fun pushMatrix() { + stack.push() + } + + fun popMatrix() { + stack.push() + } + + fun resetMatrix() { + stack = MatrixStack() + } + + fun translate(x: Double, y: Double, z: Double = 0.0) { + matrix.translate(x.toFloat(), y.toFloat(), z.toFloat()) + } + + fun scale(x: Double, y: Double, z: Double = 1.0) { + matrix.scale(x.toFloat(), y.toFloat(), z.toFloat()) + } + + fun scale(value: Double) { + val valueFloat = value.toFloat() + matrix.scale(valueFloat, valueFloat, valueFloat) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/gl/Memory.kt b/common/src/main/kotlin/com/lambda/graphics/gl/Memory.kt new file mode 100644 index 000000000..95374e8b8 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/gl/Memory.kt @@ -0,0 +1,61 @@ +package com.lambda.graphics.gl + +import com.lambda.graphics.buffer.vao.vertex.VertexAttrib +import org.lwjgl.BufferUtils +import org.lwjgl.system.MemoryUtil +import java.awt.Color +import java.nio.Buffer +import java.nio.ByteBuffer + +object Memory { + private val vec2Size = VertexAttrib.Vec2.size + private val vec3Size = VertexAttrib.Vec3.size + private val colorSize = VertexAttrib.Color.size + + fun vec2(address: Long, x: Double, y: Double): Int { + float(address + 0, x) + float(address + 4, y) + return vec2Size + } + + fun vec3(address: Long, x: Double, y: Double, z: Double): Int { + float(address + 0, x) + float(address + 4, y) + float(address + 8, z) + return vec3Size + } + + fun color(address: Long, color: Color): Int { + byte(address + 0, color.red .toByte()) + byte(address + 1, color.green.toByte()) + byte(address + 2, color.blue .toByte()) + byte(address + 3, color.alpha.toByte()) + return colorSize + } + + private fun byte(address: Long, value: Byte) { + MemoryUtil.memPutByte(address, value) + } + + fun int(address: Long, value: Int) { + MemoryUtil.memPutInt(address, value) + } + + fun float(address: Long, value: Double) { + MemoryUtil.memPutFloat(address, value.toFloat()) + } + + fun address(buffer: Buffer): Long { + return MemoryUtil.memAddress0(buffer) + } + + fun copy(from: Long, to: Long, bytes: Long) { + MemoryUtil.memCopy(from, to, bytes) + } + + fun byteBuffer(cap: Int): ByteBuffer { + return BufferUtils.createByteBuffer(cap) + } + + val Buffer.capacity get() = capacity() +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/gl/Scissor.kt b/common/src/main/kotlin/com/lambda/graphics/gl/Scissor.kt new file mode 100644 index 000000000..248975925 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/gl/Scissor.kt @@ -0,0 +1,53 @@ +package com.lambda.graphics.gl + +import com.lambda.Lambda.mc +import com.lambda.module.modules.client.GuiSettings +import com.lambda.util.math.MathUtils.ceilToInt +import com.lambda.util.math.MathUtils.floorToInt +import com.lambda.util.math.Rect +import com.mojang.blaze3d.systems.RenderSystem.disableScissor +import com.mojang.blaze3d.systems.RenderSystem.enableScissor +import kotlin.math.max + +object Scissor { + private var stack = ArrayDeque() + + fun scissor(rect: Rect, block: () -> Unit) { + // clamp corners so children scissor box can't overlap parent + val processed = stack.lastOrNull()?.let { rect.clamp(it) } ?: rect + registerScissor(processed, block) + } + + private fun registerScissor(rect: Rect, block: () -> Unit) { + scissor(rect) + + block() + + stack.removeLast() + stack.lastOrNull().apply(::scissor) + } + + private fun scissor(entry: Rect?) { + if (entry == null) { + disableScissor() + return + } + + stack.add(entry) + + val pos1 = entry.leftTop * GuiSettings.scale + val pos2 = entry.rightBottom * GuiSettings.scale + + val width = max(pos2.x - pos1.x, 0.0) + val height = max(pos2.y - pos1.y, 0.0) + + val y = mc.window.framebufferHeight - pos1.y - height + + enableScissor( + pos1.x.floorToInt(), + y.floorToInt(), + width.ceilToInt(), + height.ceilToInt() + ) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/gl/VaoUtils.kt b/common/src/main/kotlin/com/lambda/graphics/gl/VaoUtils.kt new file mode 100644 index 000000000..4aa7d91ce --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/gl/VaoUtils.kt @@ -0,0 +1,44 @@ +package com.lambda.graphics.gl + +import com.mojang.blaze3d.platform.GlStateManager +import net.minecraft.client.render.BufferRenderer +import org.lwjgl.opengl.GL30C.* +import java.nio.ByteBuffer + +@Suppress("NOTHING_TO_INLINE") +object VaoUtils { + @JvmField var lastIbo = 0 + private var prevIbo = 0 + + fun bindVertexArray(vao: Int) { + glBindVertexArray(vao) + BufferRenderer.currentVertexBuffer = null + } + + fun bindVertexBuffer(vbo: Int) = + glBindBuffer(GL_ARRAY_BUFFER, vbo) + + fun bindIndexBuffer(ibo: Int) { + if (ibo != 0) prevIbo = lastIbo + val targetIbo = if (ibo != 0) ibo else prevIbo + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, targetIbo) + } + + fun unbindVertexArray() = + bindVertexArray(0) + + fun unbindVertexBuffer() = + bindVertexBuffer(0) + + fun unbindIndexBuffer() = + bindIndexBuffer(0) + + inline fun enableVertexAttribute(i: Int) = + glEnableVertexAttribArray(i) + + inline fun vertexAttribute(index: Int, size: Int, type: Int, normalized: Boolean, stride: Int, pointer: Long) = + glVertexAttribPointer(index, size, type, normalized, stride, pointer) + + inline fun bufferData(target: Int, data: ByteBuffer, usage: Int) = + GlStateManager._glBufferData(target, data, usage) +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/IRenderEntry.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/IRenderEntry.kt new file mode 100644 index 000000000..3c0297986 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/IRenderEntry.kt @@ -0,0 +1,31 @@ +package com.lambda.graphics.renderer + +import com.lambda.graphics.buffer.vao.IRenderContext + +interface IRenderEntry > { + val owner: IRenderer + val updateBlock: T.() -> Unit + + /** + * Builds data for rendering + */ + fun build(ctx: IRenderContext) + + /** + * Updates this render entry + */ + fun update() { + @Suppress("UNCHECKED_CAST") + updateBlock(this as T) + } + + /** + * Destroys this render entry + * + * Calling this function has the same result as calling renderer.remove(myEntry) + */ + fun destroy() { + @Suppress("UNCHECKED_CAST") + owner.remove(this as T) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/IRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/IRenderer.kt new file mode 100644 index 000000000..31cb9f6f1 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/IRenderer.kt @@ -0,0 +1,33 @@ +package com.lambda.graphics.renderer + +interface IRenderer > { + /** + * Registers new render entry + */ + fun build(block: T.() -> Unit): T + + /** + * Removes given entry from the render set + */ + fun remove(entry: T): T + + /** + * Performs a draw call and rebuilds VAO before (if needed) + */ + fun render() + + /** + * Updates all render entries + */ + fun update() + + /** + * Clears the render set + */ + fun clear() + + /** + * Destroys this renderer and frees v-ram used by VAO + */ + fun destroy() +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/Renderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/Renderer.kt new file mode 100644 index 000000000..a806576ae --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/Renderer.kt @@ -0,0 +1,77 @@ +package com.lambda.graphics.renderer + +import com.lambda.graphics.buffer.vao.VAO +import kotlinx.coroutines.* +import kotlin.properties.Delegates + +abstract class Renderer > : IRenderer { + private val entrySet = mutableSetOf() + private var rebuild = false + private var destroyed = false + + val asRenderer get() = this as IRenderer + + abstract val vao: VAO + protected abstract fun newEntry(block: T.() -> Unit): T + + override fun build(block: T.() -> Unit): T { + checkDestroyed() + return newEntry(block).process(entrySet::add) + } + + override fun remove(entry: T): T { + checkDestroyed() + return entry.process(entrySet::remove) + } + + override fun render() { + checkDestroyed() + + if (rebuild) { + rebuild = false + + vao.clear() + entrySet.forEach { it.build(vao) } + vao.upload() + } + + vao.render() + } + + override fun update() { + checkDestroyed() + entrySet.forEach(IRenderEntry::update) + } + + override fun clear() { + checkDestroyed() + + entrySet.clear() + vao.clear() + } + + override fun destroy() { + checkDestroyed() + + entrySet.clear() + vao.destroy() + destroyed = true + } + + private fun T.process(action: T.() -> Unit): T { + this.apply(action) + rebuild = true + + return this + } + + fun field(initValue: T) = + Delegates.observable(initValue) { _, prev: T, curr: T -> + if (prev == curr) return@observable + rebuild = true + } + + private fun checkDestroyed() { + check(!destroyed) { "Using the renderer after it is destroyed" } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/AbstractGuiRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/AbstractGuiRenderer.kt new file mode 100644 index 000000000..a074c5801 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/AbstractGuiRenderer.kt @@ -0,0 +1,13 @@ +package com.lambda.graphics.renderer.gui + +import com.lambda.graphics.buffer.vao.VAO +import com.lambda.graphics.buffer.vao.vertex.VertexAttrib +import com.lambda.graphics.buffer.vao.vertex.VertexMode +import com.lambda.graphics.renderer.Renderer +import com.lambda.graphics.renderer.IRenderEntry + +abstract class AbstractGuiRenderer > ( + vertexType: VertexAttrib.Group +) : Renderer() { + override val vao = VAO(VertexMode.TRIANGLES, vertexType) +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontEntry.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontEntry.kt new file mode 100644 index 000000000..ade50fe8e --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontEntry.kt @@ -0,0 +1,117 @@ +package com.lambda.graphics.renderer.gui.font + +import com.lambda.graphics.buffer.vao.IRenderContext +import com.lambda.graphics.renderer.IRenderEntry +import com.lambda.graphics.renderer.gui.font.glyph.CharInfo +import com.lambda.module.modules.client.FontSettings +import com.lambda.util.math.Vec2d +import java.awt.Color + +class FontEntry( + override val owner: FontRenderer, + override val updateBlock: IFontEntry.() -> Unit, + private val font: LambdaFont +) : IFontEntry { + override var text by owner.field("") + override var position by owner.field(Vec2d.ZERO) + + override var color by owner.field(Color.WHITE) + override var scale by owner.field(1.0) + override var shadow by owner.field(true) + + private var shadowSetting by owner.field(true) + private var shadowBrightness by owner.field(0.0) + private var shadowShift by owner.field(0.0) + private var baselineOffset by owner.field(0.0) + private var gap by owner.field(0.0) + private val actualScale get() = scale * 0.12 + + override fun getWidth(text: String): Double { + var width = 0.0 + + text.forEach { char -> + val glyph = font[char] ?: return@forEach + width += glyph.size.x + gap + } + + return width * actualScale + } + + override val height: Double + get() = font.glyphs.fontHeight * actualScale * 0.7 + + override fun build(ctx: IRenderContext) = ctx.use { + val scaledShadowShift = shadowShift * actualScale + val scaledGap = gap * actualScale + val shadowColor = getShadowColor(color) + + var posX = 0.0 + val posY = height * -0.5 + baselineOffset * actualScale + + text.toCharArray().forEach { char -> + val charInfo = font[char] ?: return@forEach + val scaledSize = charInfo.size * actualScale + + val pos1 = Vec2d(posX, posY) + val pos2 = pos1 + scaledSize + + if (shadow && FontSettings.shadow) { + val shadowPos1 = pos1 + scaledShadowShift + val shadowPos2 = shadowPos1 + scaledSize + putCharQuad(shadowPos1, shadowPos2, shadowColor, charInfo) + } + + putCharQuad(pos1, pos2, color, charInfo) + + posX += scaledSize.x + scaledGap + } + } + + override fun update() { + shadowSetting = FontSettings.shadow + shadowBrightness = FontSettings.shadowBrightness + shadowShift = FontSettings.shadowShift * 4.0 + baselineOffset = FontSettings.baselineOffset * 2.0f - 10f + gap = FontSettings.gapSetting * 0.5f - 0.8f + + super.update() + } + + private fun IRenderContext.putCharQuad(pos1: Vec2d, pos2: Vec2d, color: Color, ci: CharInfo) { + val x = position.x + val y = position.y + + grow(4) + + putQuad( + vec2(pos1.x + x, pos1.y + y).vec2(ci.uv1.x, ci.uv1.y).color(color).end(), + vec2(pos1.x + x, pos2.y + y).vec2(ci.uv1.x, ci.uv2.y).color(color).end(), + vec2(pos2.x + x, pos2.y + y).vec2(ci.uv2.x, ci.uv2.y).color(color).end(), + vec2(pos2.x + x, pos1.y + y).vec2(ci.uv2.x, ci.uv1.y).color(color).end() + ) + } + + private fun getShadowColor(color: Color) = Color( + (color.red * shadowBrightness).toInt(), + (color.green * shadowBrightness).toInt(), + (color.blue * shadowBrightness).toInt(), + color.alpha + ) +} + +interface IFontEntry : IRenderEntry { + var text: String + var position: Vec2d + + var color: Color + var scale: Double + var shadow: Boolean + + fun getWidth(text: String): Double + + val width get() = getWidth(text) + val height: Double + + val widthVec get() = Vec2d(width, 0.0) + val heightVec get() = Vec2d(0.0, height) +} diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt new file mode 100644 index 000000000..a75513daf --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt @@ -0,0 +1,22 @@ +package com.lambda.graphics.renderer.gui.font + +import com.lambda.graphics.buffer.vao.vertex.VertexAttrib +import com.lambda.graphics.renderer.gui.AbstractGuiRenderer +import com.lambda.graphics.shader.Shader + +class FontRenderer(private val font: LambdaFont = LambdaFont.FiraSansRegular) : AbstractGuiRenderer( + VertexAttrib.Group.FONT +) { + override fun render() { + shader.use() + font.glyphs.bind() + super.render() + } + + override fun newEntry(block: IFontEntry.() -> Unit) = + FontEntry(this, block, font) + + companion object { + private val shader = Shader("renderer/font") + } +} diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaFont.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaFont.kt new file mode 100644 index 000000000..7651d7704 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaFont.kt @@ -0,0 +1,29 @@ +package com.lambda.graphics.renderer.gui.font + +import com.lambda.core.Loadable +import com.lambda.graphics.renderer.gui.font.glyph.FontGlyphs +import com.lambda.util.LambdaResource +import java.awt.Font + +enum class LambdaFont(private val fontName: String) { + FiraSansRegular("FiraSans-Regular"), + FiraSansBold("FiraSans-Bold"); + + lateinit var glyphs: FontGlyphs + + operator fun get(char: Char) = glyphs.getChar(char) + + fun loadGlyphs() { + val resource = LambdaResource("fonts/$fontName.ttf") + val stream = resource.stream ?: throw IllegalStateException("Failed to locate font $fontName") + val font = Font.createFont(Font.TRUETYPE_FONT, stream).deriveFont(64.0f) + glyphs = FontGlyphs(font) + } + + object Loader : Loadable { + override fun load(): String { + entries.forEach(LambdaFont::loadGlyphs) + return "Loaded ${entries.size} fonts" + } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/CharInfo.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/CharInfo.kt new file mode 100644 index 000000000..ab5791219 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/CharInfo.kt @@ -0,0 +1,9 @@ +package com.lambda.graphics.renderer.gui.font.glyph + +import com.lambda.util.math.Vec2d + +class CharInfo( + val size: Vec2d, + val uv1: Vec2d, + val uv2: Vec2d +) \ No newline at end of file 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 new file mode 100644 index 000000000..0e4a98466 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/FontGlyphs.kt @@ -0,0 +1,85 @@ +package com.lambda.graphics.renderer.gui.font.glyph + +import com.lambda.Lambda +import com.lambda.graphics.texture.MipmapTexture +import com.lambda.graphics.texture.TextureUtils.getCharImage +import com.lambda.module.modules.client.FontSettings +import com.lambda.util.math.Vec2d +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap +import java.awt.Color +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) { + private val charMap = Int2ObjectOpenHashMap() + private val fontTexture: MipmapTexture + + var fontHeight = 0.0; private set + + init { + val time = measureTimeMillis { + val image = BufferedImage(TEXTURE_SIZE, TEXTURE_SIZE, BufferedImage.TYPE_INT_ARGB) + + val graphics = image.graphics as Graphics2D + graphics.background = Color(0, 0, 0, 0) + + var x = 0 + var y = 0 + var rowHeight = 0 + + // Because UTF16 takes 2 bytes per character, we can't use the full range of characters + (Char.MIN_VALUE.. + val charImage = getCharImage(font, char) ?: return@forEach + + rowHeight = max(rowHeight, charImage.height) + + if (x + charImage.width >= TEXTURE_SIZE) { + y += rowHeight + x = 0 + rowHeight = 0 + } + + check(y + charImage.height < TEXTURE_SIZE) { "Can't load font glyphs. Texture size is too small" } + + graphics.drawImage(charImage, x, y, null) + + 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 + + charMap[char.code] = CharInfo(size, uv1, uv2) + fontHeight = max(fontHeight, size.y) + + x += charImage.width + } + + fontTexture = MipmapTexture(image) + } + + Lambda.LOG.info("Font ${font.fontName} loaded with ${charMap.size} characters (${time}ms)") + } + + fun bind() { + with(fontTexture) { + bind() + setLOD(FontSettings.lodBias.toFloat()) + } + } + + fun getChar(char: Char): CharInfo? = + charMap[char.code] + + companion object { + // The size cannot be bigger than 2^15 because the rasterizer needs to be fed with dimensions that when multiplied together are less than 2^31 + // This can be bypassed by using a custom rasterizer, but it's not worth the effort + // The size is also limited by the java heap size, as the image is stored in memory + // and then uploaded to the GPU + // Since most Lambda users probably have bad pc, the default size is 2048, which includes latin, cyrillic, greek and arabic + // and in the future we could grow the textures when needed + private val TEXTURE_SIZE = FontSettings.amountOfGlyphs * 2 + private val ONE_TEXEL_SIZE = 1.0 / TEXTURE_SIZE + } +} diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/RectEntry.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/RectEntry.kt new file mode 100644 index 000000000..085aaf4eb --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/RectEntry.kt @@ -0,0 +1,76 @@ +package com.lambda.graphics.renderer.gui.rect + +import com.lambda.graphics.buffer.vao.IRenderContext +import com.lambda.graphics.renderer.IRenderEntry +import com.lambda.util.math.Rect +import java.awt.Color +import kotlin.math.min + +class RectEntry( + override val owner: RectRenderer, + override val updateBlock: IRectEntry.() -> Unit +) : IRectEntry { + override var position by owner.field(Rect.ZERO) + override var roundRadius by owner.field(0.0) + + private var leftTop by owner.field(Color.WHITE!!) + private var rightTop by owner.field(Color.WHITE!!) + private var rightBottom by owner.field(Color.WHITE!!) + private var leftBottom by owner.field(Color.WHITE!!) + + override fun color(leftTop: Color, rightTop: Color, rightBottom: Color, leftBottom: Color) { + this.leftTop = leftTop + this.rightTop = rightTop + this.rightBottom = rightBottom + this.leftBottom = leftBottom + } + + override fun build(ctx: IRenderContext) = ctx.use { + if (leftTop .alpha < MIN_ALPHA && + rightTop .alpha < MIN_ALPHA && + rightBottom.alpha < MIN_ALPHA && + leftBottom .alpha < MIN_ALPHA + ) return@use + + val pos1 = position.leftTop + val pos2 = position.rightBottom + + val size = pos2 - pos1 + if (size.x < MIN_SIZE || size.y < MIN_SIZE) return@use + + val halfSize = size * 0.5 + val maxRadius = min(halfSize.x, halfSize.y) + + val round = min(roundRadius, maxRadius) + + val p1 = pos1 - 0.75 + val p2 = pos2 + 0.75 + + grow(4) + + putQuad( + vec2(p1.x, p1.y).vec2(0.0, 0.0).vec3(size.x, size.y, round).color(leftTop).end(), + vec2(p1.x, p2.y).vec2(0.0, 1.0).vec3(size.x, size.y, round).color(leftBottom).end(), + vec2(p2.x, p2.y).vec2(1.0, 1.0).vec3(size.x, size.y, round).color(rightBottom).end(), + vec2(p2.x, p1.y).vec2(1.0, 0.0).vec3(size.x, size.y, round).color(rightTop).end() + ) + } + + companion object { + private const val MIN_ALPHA = 5 + private const val MIN_SIZE = 0.5 + } +} + +interface IRectEntry : IRenderEntry { + var position: Rect + var roundRadius: Double + + fun color(leftTop: Color, rightTop: Color, rightBottom: Color, leftBottom: Color) + + fun color(color: Color) = color(color, color, color, color) + + fun colorH(left: Color, right: Color) = color(left, right, right, left) + + fun colorV(top: Color, bottom: Color) = color(top, top, bottom, bottom) +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/RectRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/RectRenderer.kt new file mode 100644 index 000000000..5ba178377 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/RectRenderer.kt @@ -0,0 +1,48 @@ +package com.lambda.graphics.renderer.gui.rect + +import com.lambda.graphics.buffer.vao.vertex.VertexAttrib +import com.lambda.graphics.renderer.gui.AbstractGuiRenderer +import com.lambda.graphics.shader.Shader +import com.lambda.module.modules.client.GuiSettings +import com.lambda.util.math.Vec2d +import com.mojang.blaze3d.systems.RenderSystem.blendFunc +import com.mojang.blaze3d.systems.RenderSystem.defaultBlendFunc +import org.lwjgl.glfw.GLFW.glfwGetTime +import org.lwjgl.opengl.GL11.GL_ONE +import org.lwjgl.opengl.GL11.GL_SRC_ALPHA + +class RectRenderer : AbstractGuiRenderer( + VertexAttrib.Group.RECT +) { + var shadeColor = false + var fancyBlending = false + + override fun render() { + shader.use() + + shader["u_Shade"] = shadeColor + + if (shadeColor) { + shader["u_Time"] = glfwGetTime() * GuiSettings.colorSpeed * 5.0 + shader["u_Color1"] = GuiSettings.shadeColor1 + shader["u_Color2"] = GuiSettings.shadeColor2 + shader["u_Size"] = Vec2d.ONE / Vec2d(GuiSettings.colorWidth, GuiSettings.colorHeight) + } + + if (fancyBlending) blendFunc(GL_SRC_ALPHA, GL_ONE) + super.render() + defaultBlendFunc() + } + + override fun build(block: IRectEntry.() -> Unit): IRectEntry { + shadeColor = false + return super.build(block) + } + + override fun newEntry(block: IRectEntry.() -> Unit) = + RectEntry(this, block) + + companion object { + private val shader = Shader("renderer/rect") + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/shader/Shader.kt b/common/src/main/kotlin/com/lambda/graphics/shader/Shader.kt new file mode 100644 index 000000000..b0ec5c513 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/shader/Shader.kt @@ -0,0 +1,66 @@ +package com.lambda.graphics.shader + +import com.lambda.graphics.RenderMain +import com.lambda.graphics.shader.ShaderUtils.createShaderProgram +import com.lambda.graphics.shader.ShaderUtils.loadShader +import com.lambda.graphics.shader.ShaderUtils.uniformMatrix +import com.lambda.threading.mainThread +import com.lambda.util.LambdaResource +import com.lambda.util.math.Vec2d +import it.unimi.dsi.fastutil.objects.Object2IntMap +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap +import org.joml.Matrix4f +import org.lwjgl.opengl.GL20C.* +import java.awt.Color + +class Shader(fragmentPath: String, vertexPath: String) { + private val uniformCache: Object2IntMap = Object2IntOpenHashMap() + + private val id by mainThread { + createShaderProgram( + loadShader(ShaderType.VERTEX_SHADER, LambdaResource("shaders/vertex/$vertexPath.vert")), + loadShader(ShaderType.FRAGMENT_SHADER, LambdaResource("shaders/fragment/$fragmentPath.frag")) + ) + } + + constructor(path: String) : this(path, path) + + fun use() { + glUseProgram(id) + set("u_Projection", RenderMain.projectionMatrix) + set("u_ModelView", RenderMain.modelViewMatrix) + } + + private fun loc(name: String) = + if (uniformCache.containsKey(name)) + uniformCache.getInt(name) + else + glGetUniformLocation(id, name).let { location -> + uniformCache.put(name, location) + location + } + + operator fun set(name: String, v: Boolean) = + glUniform1i(loc(name), if (v) 1 else 0) + + operator fun set(name: String, v: Int) = + glUniform1i(loc(name), v) + + operator fun set(name: String, v: Double) = + glUniform1f(loc(name), v.toFloat()) + + operator fun set(name: String, vec: Vec2d) = + glUniform2f(loc(name), vec.x.toFloat(), vec.y.toFloat()) + + operator fun set(name: String, color: Color) = + glUniform4f( + loc(name), + color.red / 255f, + color.green / 255f, + color.blue / 255f, + color.alpha / 255f + ) + + operator fun set(name: String, mat: Matrix4f) = + uniformMatrix(loc(name), mat) +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/shader/ShaderType.kt b/common/src/main/kotlin/com/lambda/graphics/shader/ShaderType.kt new file mode 100644 index 000000000..afa0cfe2b --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/shader/ShaderType.kt @@ -0,0 +1,10 @@ +package com.lambda.graphics.shader + +import com.lambda.graphics.gl.GLObject +import org.lwjgl.opengl.GL20C.GL_FRAGMENT_SHADER +import org.lwjgl.opengl.GL20C.GL_VERTEX_SHADER + +enum class ShaderType(override val gl: Int) : GLObject { + FRAGMENT_SHADER(GL_FRAGMENT_SHADER), + VERTEX_SHADER(GL_VERTEX_SHADER) +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/shader/ShaderUtils.kt b/common/src/main/kotlin/com/lambda/graphics/shader/ShaderUtils.kt new file mode 100644 index 000000000..61166045f --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/shader/ShaderUtils.kt @@ -0,0 +1,82 @@ +package com.lambda.graphics.shader + +import com.google.common.collect.ImmutableList +import com.lambda.util.LambdaResource +import com.mojang.blaze3d.platform.GlStateManager +import org.apache.commons.io.IOUtils +import org.joml.Matrix4f +import org.lwjgl.BufferUtils +import org.lwjgl.opengl.GL30C.* + +object ShaderUtils { + private val matrixBuffer = BufferUtils.createFloatBuffer(4 * 4) + private const val shaderInfoLogLength = 512 + + fun loadShader(type: ShaderType, resource: LambdaResource): Int { + // Create new shader object + val shader = glCreateShader(type.gl) + + // Attach source code and compile it + val text = IOUtils.toString(resource.stream, Charsets.UTF_8) + GlStateManager.glShaderSource(shader, ImmutableList.of(text)) + val error = compileShader(shader) + + // Handle error + error?.let { err -> + val builder = StringBuilder() + .append("Failed to compile ${type.name} shader").appendLine() + .append("Path: ${resource.path}").appendLine() + .append("Compiler output:").appendLine() + .append(err) + + throw RuntimeException(builder.toString()) + } + + return shader + } + + fun createShaderProgram(vert: Int, frag: Int): Int { + // Create new shader program + val program = glCreateProgram() + val error = linkProgram(program, vert, frag) + + // Handle error + error?.let { err -> + val builder = StringBuilder() + .append("Failed to link shader program").appendLine() + .append("Output:").appendLine() + .append(err) + + throw RuntimeException(builder.toString()) + } + + glDeleteShader(vert) + glDeleteShader(frag) + + return program + } + + private fun compileShader(shader: Int): String? { + glCompileShader(shader) + val status = glGetShaderi(shader, GL_COMPILE_STATUS) + + return if (status != GL_FALSE) null + else glGetShaderInfoLog(shader, shaderInfoLogLength) + } + + private fun linkProgram(program: Int, vertShader: Int, fragShader: Int): String? { + glAttachShader(program, vertShader) + glAttachShader(program, fragShader) + glLinkProgram(program) + + val status = glGetProgrami(program, GL_LINK_STATUS) + + return if (status != GL_FALSE) null + else glGetProgramInfoLog(program, shaderInfoLogLength) + } + + fun uniformMatrix(location: Int, v: Matrix4f) { + v.get(matrixBuffer) + glUniformMatrix4fv(location, false, matrixBuffer) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/MipmapTexture.kt b/common/src/main/kotlin/com/lambda/graphics/texture/MipmapTexture.kt new file mode 100644 index 000000000..aaff7f168 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/texture/MipmapTexture.kt @@ -0,0 +1,34 @@ +package com.lambda.graphics.texture + +import com.lambda.graphics.texture.TextureUtils.rescale +import com.lambda.graphics.texture.TextureUtils.setupLOD +import com.lambda.graphics.texture.TextureUtils.upload +import org.lwjgl.opengl.GL14.* +import java.awt.image.BufferedImage + +class MipmapTexture(private val image: BufferedImage, private val levels: Int = 4) : Texture() { + private var lastLod: Float? = null + + override fun init() { + setupLOD(levels) + + // Upload base image + upload(image, 0) + + // Upload downscaled ones + for (level in 1..levels) { + val newWidth = image.width shr level + val newHeight = image.height shr level + val scaled = image.rescale(newWidth, newHeight) + + upload(scaled, level) + } + } + + fun setLOD(targetLod: Float) { + if (lastLod == targetLod) return + lastLod = targetLod + + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, targetLod) + } +} \ 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 new file mode 100644 index 000000000..0ff1b10f2 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt @@ -0,0 +1,29 @@ +package com.lambda.graphics.texture + +import com.lambda.graphics.texture.TextureUtils.bindTexture +import com.lambda.threading.mainThread +import org.lwjgl.opengl.GL13.glGenTextures + +abstract class Texture { + private val id by mainThread { + glGenTextures().apply { + bindTexture(this) + init() + } + } + + protected abstract fun init() + + open fun bind() = bindTexture(id) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Texture + + return id == other.id + } + + override fun hashCode() = id +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt b/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt new file mode 100644 index 000000000..5361b4cc3 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt @@ -0,0 +1,120 @@ +package com.lambda.graphics.texture + +import com.mojang.blaze3d.systems.RenderSystem +import net.minecraft.client.texture.NativeImage +import org.lwjgl.BufferUtils +import org.lwjgl.opengl.GL13C.* +import java.awt.Color +import java.awt.Font +import java.awt.RenderingHints +import java.awt.Transparency +import java.awt.image.BufferedImage +import java.io.ByteArrayOutputStream +import javax.imageio.ImageIO +import kotlin.math.roundToInt +import kotlin.math.sqrt + +object TextureUtils { + fun bindTexture(id: Int, slot: Int = 0) { + RenderSystem.activeTexture(GL_TEXTURE0 + slot) + RenderSystem.bindTexture(id) + } + + fun upload(bufferedImage: BufferedImage, lod: Int) { + val width = bufferedImage.width + val height = bufferedImage.height + + glTexImage2D(GL_TEXTURE_2D, lod, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, readImage(bufferedImage)) + setupTexture(GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR) + } + + fun setupLOD(levels: Int) { + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_LOD, 0) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LOD, levels) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, levels) + } + + fun setupTexture(minFilter: Int, magFilter: Int) { + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, minFilter) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, magFilter) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) + + glPixelStorei(GL_UNPACK_ROW_LENGTH, 0) + glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0) + glPixelStorei(GL_UNPACK_SKIP_ROWS, 0) + glPixelStorei(GL_UNPACK_ALIGNMENT, 4) + } + + private fun readImage(bufferedImage: BufferedImage): Long { + val stream = ByteArrayOutputStream() + ImageIO.write(bufferedImage, "png", stream) + + val bytes = stream.toByteArray() + val buffer = BufferUtils + .createByteBuffer(bytes.size) + .put(bytes) + .flip() + + return NativeImage.read(buffer).pointer + } + + fun getCharImage(font: Font, char: Char): BufferedImage? { + if (!font.canDisplay(char)) return null + + val tempGraphics2D = BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB).createGraphics() + tempGraphics2D.font = font + val fontMetrics = tempGraphics2D.fontMetrics + tempGraphics2D.dispose() + + val charWidth = if (fontMetrics.charWidth(char) > 0) fontMetrics.charWidth(char) else 8 + val charHeight = if (fontMetrics.height > 0) fontMetrics.height else font.size + + val charImage = BufferedImage(charWidth, charHeight, BufferedImage.TYPE_INT_ARGB) + val graphics2D = charImage.createGraphics() + + graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) + graphics2D.font = font + graphics2D.color = Color.WHITE + graphics2D.drawString(char.toString(), 0, fontMetrics.ascent) + graphics2D.dispose() + + return charImage + } + + fun BufferedImage.rescale(targetWidth: Int, targetHeight: Int): BufferedImage { + val type = if (this.transparency == Transparency.OPAQUE) + BufferedImage.TYPE_INT_RGB + else BufferedImage.TYPE_INT_ARGB + + var image = this + + var width = image.width + var height = image.height + + val divisorX = sqrt((width / targetWidth).toDouble()) + val divisorY = sqrt((height / targetHeight).toDouble()) + + do { + if (width > targetWidth) { + width = (width / divisorX).roundToInt().coerceAtLeast(targetWidth) + } + + if (height > targetHeight) { + height = (height / divisorY).roundToInt().coerceAtLeast(targetHeight) + } + + val tempImage = BufferedImage(width, height, type) + val graphics2D = tempImage.createGraphics() + + graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR) + graphics2D.drawImage(image, 0, 0, width, height, null) + graphics2D.dispose() + + image = tempImage + } while (width != targetWidth || height != targetHeight) + + return image + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/gui/api/LambdaGui.kt b/common/src/main/kotlin/com/lambda/gui/api/LambdaGui.kt new file mode 100644 index 000000000..1131da747 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/gui/api/LambdaGui.kt @@ -0,0 +1,114 @@ +package com.lambda.gui.api + +import com.lambda.Lambda.mc +import com.lambda.event.EventFlow.syncListeners +import com.lambda.event.events.RenderEvent +import com.lambda.event.events.TickEvent +import com.lambda.event.listener.UnsafeListener +import com.lambda.gui.api.component.core.IComponent +import com.lambda.module.Module +import com.lambda.util.KeyCode +import com.lambda.util.Mouse +import com.lambda.util.Nameable +import com.lambda.util.math.Vec2d +import com.mojang.blaze3d.systems.RenderSystem.recordRenderCall +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.gui.screen.Screen +import net.minecraft.text.Text + +@Suppress("LeakingThis") +abstract class LambdaGui( + override val name: String, + private val owner: Module? = null +) : Screen(Text.of(name)), IComponent, Nameable { + private var screenSize = Vec2d.ZERO + + private val renderListener = UnsafeListener(0, this, false) { event -> + event as RenderEvent.GUI.Scaled + screenSize = event.screenSize + onRender() + } + + private val tickListener = UnsafeListener(0, this, false) { + onTick() + } + + /** + * Shows this gui screen + * + * No safe context required (TODO: let user open clickgui via main menu) + */ + fun show() { + mc.currentScreen?.close() + + recordRenderCall { // wait for the previous screen to be closed + mc.setScreen(this) + } + } + + final override fun onDisplayed() { + onShow() + + with(syncListeners) { + subscribe(renderListener) + subscribe(tickListener) + } + } + + final override fun removed() { + onHide() + + // quick crashfix (is there any other way to prevent gui being closed twice?) + mc.currentScreen = null + owner?.disable() + mc.currentScreen = this + + with(syncListeners) { + unsubscribe(renderListener) + unsubscribe(tickListener) + } + } + + final override fun render(context: DrawContext?, mouseX: Int, mouseY: Int, delta: Float) { + // Let's remove background tint + } + + final override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + onKey(KeyCode(keyCode)) + + if (keyCode == KeyCode.Escape.key) { + close() + } + + return true + } + + final override fun charTyped(chr: Char, modifiers: Int): Boolean { + onChar(chr) + return true + } + + final override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { + onMouseClick(Mouse.Button(button), Mouse.Action.Click, rescaleMouse(mouseX, mouseY)) + return true + } + + final override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean { + onMouseClick(Mouse.Button(button), Mouse.Action.Release, rescaleMouse(mouseX, mouseY)) + return true + } + + final override fun mouseMoved(mouseX: Double, mouseY: Double) { + onMouseMove(rescaleMouse(mouseX, mouseY)) + } + + final override fun shouldPause() = false + + private fun rescaleMouse(mouseX: Double, mouseY: Double): Vec2d { + val mcMouse = Vec2d(mouseX, mouseY) + val mcWindow = Vec2d(mc.window.scaledWidth, mc.window.scaledHeight) + + val uv = mcMouse / mcWindow + return uv * screenSize + } +} diff --git a/common/src/main/kotlin/com/lambda/gui/api/component/InteractiveComponent.kt b/common/src/main/kotlin/com/lambda/gui/api/component/InteractiveComponent.kt new file mode 100644 index 000000000..0bd6e7555 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/gui/api/component/InteractiveComponent.kt @@ -0,0 +1,40 @@ +package com.lambda.gui.api.component + +import com.lambda.gui.api.component.core.IComponent +import com.lambda.gui.api.component.core.IRectComponent +import com.lambda.util.Mouse +import com.lambda.util.math.Vec2d + +abstract class InteractiveComponent : IComponent, IRectComponent { + protected var hovered = false + protected var pressed = false; set(value) { + if (field == value) return + field = value + + if (value) onPress() + else onRelease() + } + + protected var activeMouseButton: Mouse.Button? = null + + protected open fun onPress() {} + protected open fun onRelease() {} + + override fun onShow() { + hovered = false + pressed = false + } + + override fun onMouseMove(mouse: Vec2d) { + hovered = rect.contains(mouse) + } + + override fun onMouseClick( + button: Mouse.Button, action: Mouse.Action, mouse: Vec2d + ) { + activeMouseButton = button.takeUnless { + it.isMainButton && action == Mouse.Action.Click + } + pressed = hovered && button.isMainButton && action == Mouse.Action.Click + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/gui/api/component/WindowComponent.kt b/common/src/main/kotlin/com/lambda/gui/api/component/WindowComponent.kt new file mode 100644 index 000000000..f6e13ced9 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/gui/api/component/WindowComponent.kt @@ -0,0 +1,139 @@ +package com.lambda.gui.api.component + +import com.lambda.graphics.animation.Animation.Companion.exp +import com.lambda.graphics.animation.AnimationTicker +import com.lambda.graphics.gl.Scissor.scissor +import com.lambda.graphics.renderer.gui.font.IFontEntry +import com.lambda.gui.api.component.core.IComponent +import com.lambda.gui.api.component.core.list.IListComponent +import com.lambda.gui.api.component.core.list.ChildComponent +import com.lambda.gui.api.layer.RenderLayer +import com.lambda.module.modules.client.ClickGui +import com.lambda.module.modules.client.GuiSettings +import com.lambda.util.KeyCode +import com.lambda.util.Mouse +import com.lambda.util.math.MathUtils.toInt +import com.lambda.util.math.Rect +import com.lambda.util.math.Vec2d +import kotlin.math.abs + +abstract class WindowComponent : InteractiveComponent(), IListComponent { + abstract val title: String + + abstract var width: Double + abstract var height: Double + + var position = Vec2d.ZERO + + var isOpen = true + private var dragOffset: Vec2d? = null + private val padding get() = ClickGui.windowPadding + + final override val rect get() = Rect.basedOn(position, width, renderHeight + titleBarHeight) + val contentRect get() = rect.shrink(padding).moveFirst(Vec2d(0.0, titleBarHeight - padding)) + + private val titleBar get() = Rect.basedOn(rect.leftTop, rect.size.x, titleBarHeight) + private val titleBarHeight get() = titleFont.height + 2 + padding * 2 + private val titleFont: IFontEntry + + private val layer = RenderLayer() + private val animation = AnimationTicker() + + override val children = mutableListOf() + val subLayer = RenderLayer(true) + + private val actualHeight get() = height + padding * 2 * isOpen.toInt() + private var renderHeight by animation.exp({ 0.0 }, ::actualHeight, 0.5, ::isOpen) + + init { + // Background + layer.rect.build { + position = rect + roundRadius = ClickGui.windowRadius + color(GuiSettings.backgroundColor) + } + + // Title + titleFont = layer.font.build { + text = title + position = titleBar.center - widthVec * 0.5 + } + } + + override fun onShow() { + super.onShow() + super.onShow() + + dragOffset = null + renderHeight = 0.0 + } + + override fun onHide() { + super.onHide() + } + + override fun onTick() { + animation.tick() + + setChildrenAccessibility { child -> + child.rect in contentRect + } + + children + .filter(ChildComponent::accessible) + .forEach(IComponent::onTick) + } + + override fun onRender() { + layer.render() + + scissor(contentRect) { + subLayer.render() + super.onRender() + } + } + + override fun onMouseMove(mouse: Vec2d) { + super.onMouseMove(mouse) + super.onMouseMove(mouse) + + dragOffset?.let { + position = mouse - it + } + } + + override fun onKey(key: KeyCode) { + super.onKey(key) + } + + override fun onChar(char: Char) { + super.onChar(char) + } + + override fun onMouseClick(button: Mouse.Button, action: Mouse.Action, mouse: Vec2d) { + super.onMouseClick(button, action, mouse) + + dragOffset = null + + if (mouse in titleBar && action == Mouse.Action.Click) { + when(button) { + Mouse.Button.Left -> dragOffset = mouse - position + Mouse.Button.Right -> { + // Don't let user spam + val targetHeight = if (isOpen) actualHeight else 0.0 + if (abs(targetHeight - renderHeight) > 1) return + + isOpen = !isOpen + } + } + } + + super.onMouseClick(button, action, mouse) + } + + private fun setChildrenAccessibility(flag: (T) -> Boolean) { + children.forEach { child -> + child.accessible = flag(child) + } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/gui/api/component/core/IComponent.kt b/common/src/main/kotlin/com/lambda/gui/api/component/core/IComponent.kt new file mode 100644 index 000000000..7dc3504ae --- /dev/null +++ b/common/src/main/kotlin/com/lambda/gui/api/component/core/IComponent.kt @@ -0,0 +1,23 @@ +package com.lambda.gui.api.component.core + +import com.lambda.util.KeyCode +import com.lambda.util.Mouse +import com.lambda.util.math.Vec2d + +interface IComponent { + fun onShow() {} + + fun onHide() {} + + fun onTick() {} + + fun onRender() {} + + fun onKey(key: KeyCode) {} + + fun onChar(char: Char) {} + + fun onMouseClick(button: Mouse.Button, action: Mouse.Action, mouse: Vec2d) {} + + fun onMouseMove(mouse: Vec2d) {} +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/gui/api/component/core/IRectComponent.kt b/common/src/main/kotlin/com/lambda/gui/api/component/core/IRectComponent.kt new file mode 100644 index 000000000..0843a1ccb --- /dev/null +++ b/common/src/main/kotlin/com/lambda/gui/api/component/core/IRectComponent.kt @@ -0,0 +1,7 @@ +package com.lambda.gui.api.component.core + +import com.lambda.util.math.Rect + +interface IRectComponent { + val rect: Rect +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/gui/api/component/core/list/ChildComponent.kt b/common/src/main/kotlin/com/lambda/gui/api/component/core/list/ChildComponent.kt new file mode 100644 index 000000000..c8209122f --- /dev/null +++ b/common/src/main/kotlin/com/lambda/gui/api/component/core/list/ChildComponent.kt @@ -0,0 +1,14 @@ +package com.lambda.gui.api.component.core.list + +import com.lambda.gui.api.component.InteractiveComponent + +abstract class ChildComponent : InteractiveComponent(), IChildComponent { + // mostly used to create an animation when an element appears + var accessible = false; set(value) { + if (field == value) return + field = value + + if (value) onShow() + else onHide() + } +} diff --git a/common/src/main/kotlin/com/lambda/gui/api/component/core/list/IChildComponent.kt b/common/src/main/kotlin/com/lambda/gui/api/component/core/list/IChildComponent.kt new file mode 100644 index 000000000..353ab501f --- /dev/null +++ b/common/src/main/kotlin/com/lambda/gui/api/component/core/list/IChildComponent.kt @@ -0,0 +1,7 @@ +package com.lambda.gui.api.component.core.list + +import com.lambda.gui.api.component.core.IComponent + +interface IChildComponent { + val owner: IComponent +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/gui/api/component/core/list/IListComponent.kt b/common/src/main/kotlin/com/lambda/gui/api/component/core/list/IListComponent.kt new file mode 100644 index 000000000..303ea9ff5 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/gui/api/component/core/list/IListComponent.kt @@ -0,0 +1,55 @@ +package com.lambda.gui.api.component.core.list + +import com.lambda.gui.api.component.core.IComponent +import com.lambda.util.KeyCode +import com.lambda.util.Mouse +import com.lambda.util.math.Vec2d + +interface IListComponent : IComponent { + val children: List + + fun isChildAccessible(child: T): Boolean = (child as? ChildComponent)?.accessible ?: true + + override fun onShow() { + children.forEach(IComponent::onShow) + } + + override fun onHide() { + children.forEach(IComponent::onHide) + } + + override fun onTick() { + children.forEach(IComponent::onTick) + } + + override fun onRender() { + children.forEach(IComponent::onRender) + } + + override fun onKey(key: KeyCode) { + children.filter(::isChildAccessible).forEach { child -> + child.onKey(key) + } + } + + override fun onChar(char: Char) { + children.filter(::isChildAccessible).forEach { child -> + child.onChar(char) + } + } + + override fun onMouseClick(button: Mouse.Button, action: Mouse.Action, mouse: Vec2d) { + children.filter(::isChildAccessible).forEach { child -> + child.onMouseClick(button, action, mouse) + } + } + + override fun onMouseMove(mouse: Vec2d) { + children.forEach { child -> + child.onMouseMove( + if (isChildAccessible(child)) mouse + else Vec2d(-1000.0, -1000.0) // junky but worky way to unfocus + ) + } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/gui/api/component/sub/ButtonComponent.kt b/common/src/main/kotlin/com/lambda/gui/api/component/sub/ButtonComponent.kt new file mode 100644 index 000000000..4e1265ead --- /dev/null +++ b/common/src/main/kotlin/com/lambda/gui/api/component/sub/ButtonComponent.kt @@ -0,0 +1,126 @@ +package com.lambda.gui.api.component.sub + +import com.lambda.graphics.animation.Animation.Companion.exp +import com.lambda.graphics.animation.AnimationTicker +import com.lambda.gui.api.component.WindowComponent +import com.lambda.gui.api.component.core.list.ChildComponent +import com.lambda.module.modules.client.ClickGui +import com.lambda.module.modules.client.GuiSettings +import com.lambda.util.Mouse +import com.lambda.util.math.ColorUtils.multAlpha +import com.lambda.util.math.MathUtils.lerp +import com.lambda.util.math.MathUtils.toInt +import com.lambda.util.math.Rect +import com.lambda.util.math.Vec2d +import java.awt.Color +import kotlin.math.abs + +abstract class ButtonComponent(final override val owner: WindowComponent<*>) : ChildComponent() { + abstract val position: Vec2d + abstract val size: Vec2d + + abstract val text: String + open val active get() = pressed + + private val actualSize get() = Vec2d(if (size.x == FILL_PARENT) owner.contentRect.size.x else size.x, size.y) + final override val rect get() = Rect.basedOn(position, actualSize) + owner.contentRect.leftTop + + private val layer = owner.subLayer + private val animation = AnimationTicker() + + private var activeAnimation by animation.exp(0.0, 1.0, 0.1, ::active) + private var hoverRectAnimation by animation.exp({ 0.0 }, { 1.0 }, { if (renderHovered) 0.5 else 0.1 }, ::renderHovered) + private var hoverFontAnimation by animation.exp(0.0, 1.0, 0.5, ::renderHovered) + private var pressAnimation by animation.exp(0.0, 1.0, 0.5, ::pressed) + private val interactAnimation get() = lerp(hoverRectAnimation, 1.5, pressAnimation) * 0.4 + + private var lastHoveredTime = 0L + private val renderHovered get() = hovered || + System.currentTimeMillis() - lastHoveredTime < 110 // a bit more than 2 ticks + + init { + // Active color + layer.rect.build { + position = rect.shrink(interactAnimation) + color(GuiSettings.mainColor.multAlpha(activeAnimation * 0.2)) + } + + // Hover glint + layer.rect.build { + val hoverRect = Rect.basedOn(rect.leftTop, rect.size.x * hoverRectAnimation, rect.size.y) + position = hoverRect.shrink(interactAnimation) + + val alpha = interactAnimation * 0.3 + color(GuiSettings.mainColor.multAlpha(alpha)) + } + + // Toggle fx + layer.rect.build { + val left = rect - Vec2d(rect.size.x, 0.0) + val right = rect + Vec2d(rect.size.x, 0.0) + + position = lerp(left, right, activeAnimation) + .clamp(rect) + .shrink(interactAnimation) + + // 0.0 .. 1.0 .. 0.0 animation + val alpha = 1.0 - (abs(activeAnimation - 0.5) * 2.0) + val color = GuiSettings.mainColor.multAlpha(alpha * 0.8) + + // "Tail" effect + val leftColor = color.multAlpha(1.0 - active.toInt()) + val rightColor = color.multAlpha(active.toInt().toDouble()) + + colorH(leftColor, rightColor) + } + + // Text + layer.font.build { + text = this@ButtonComponent.text + scale = 1.0 - pressAnimation * 0.08 + + color = lerp(Color.WHITE, GuiSettings.mainColor, activeAnimation) + + val x = rect.left + ClickGui.windowPadding + interactAnimation + hoverFontAnimation * 2.0 + position = Vec2d(x, rect.center.y) + } + } + + abstract fun performClickAction(mouse: Mouse.Button) + + override fun onShow() { + super.onShow() + reset() + } + + override fun onHide() { + super.onHide() + reset() + } + + override fun onTick() { + animation.tick() + } + + override fun onRelease() { + if (hovered) activeMouseButton?.let(::performClickAction) + } + + override fun onMouseMove(mouse: Vec2d) { + super.onMouseMove(mouse) + + val time = System.currentTimeMillis() + if (hovered) lastHoveredTime = time + } + + private fun reset() { + activeAnimation = 0.0 + hoverRectAnimation = 0.0 + pressAnimation = 0.0 + lastHoveredTime = 0L + } + + companion object { + const val FILL_PARENT = -1.0 + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/gui/api/layer/RenderLayer.kt b/common/src/main/kotlin/com/lambda/gui/api/layer/RenderLayer.kt new file mode 100644 index 000000000..fe871fc28 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/gui/api/layer/RenderLayer.kt @@ -0,0 +1,30 @@ +package com.lambda.gui.api.layer + +import com.lambda.graphics.renderer.gui.font.FontRenderer +import com.lambda.graphics.renderer.gui.rect.RectRenderer +import com.lambda.module.modules.client.GuiSettings + +class RenderLayer(private val allowEffects: Boolean = false) { + private val rectRenderer = RectRenderer() + + val rect = rectRenderer.asRenderer + val font = FontRenderer().asRenderer + + fun render() { + rect.update() + font.update() + + rectRenderer.apply { + shadeColor = GuiSettings.shade && allowEffects + fancyBlending = GuiSettings.glow && allowEffects + render() + } + + font.render() + } + + fun destroy() { + rect.destroy() + font.destroy() + } +} diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/GuiConfigurable.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/GuiConfigurable.kt new file mode 100644 index 000000000..79158c61d --- /dev/null +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/GuiConfigurable.kt @@ -0,0 +1,10 @@ +package com.lambda.gui.impl.clickgui + +import com.lambda.config.Configurable +import com.lambda.config.configurations.GuiConfig +import com.lambda.gui.impl.clickgui.windows.TagWindow + +object GuiConfigurable : Configurable(GuiConfig) { + override val name = "gui" + val windows = setting("windows", listOf(TagWindow())) +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/LambdaClickGui.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/LambdaClickGui.kt new file mode 100644 index 000000000..b24ccf7a0 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/LambdaClickGui.kt @@ -0,0 +1,10 @@ +package com.lambda.gui.impl.clickgui + +import com.lambda.gui.api.LambdaGui +import com.lambda.gui.api.component.WindowComponent +import com.lambda.gui.api.component.core.list.IListComponent +import com.lambda.module.modules.client.ClickGui + +object LambdaClickGui : LambdaGui("ClickGui", ClickGui), IListComponent> { + override val children: List> get() = GuiConfigurable.windows.value +} \ 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 new file mode 100644 index 000000000..3649b06ec --- /dev/null +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/ModuleButton.kt @@ -0,0 +1,27 @@ +package com.lambda.gui.impl.clickgui.buttons + +import com.lambda.gui.api.component.WindowComponent +import com.lambda.gui.api.component.sub.ButtonComponent +import com.lambda.module.Module +import com.lambda.module.modules.client.ClickGui +import com.lambda.util.Mouse +import com.lambda.util.math.Vec2d + +class ModuleButton(val module: Module, owner: WindowComponent<*>) : ButtonComponent(owner) { + override val position get() = Vec2d(0.0, heightOffset) + override val size get() = Vec2d(FILL_PARENT, ClickGui.buttonHeight) + + override val text: String get() = module.name + override val active: Boolean get() = module.isEnabled + + var heightOffset = 0.0 + + override fun performClickAction(mouse: Mouse.Button) { + when (mouse) { + Mouse.Button.Left -> module.toggle() + Mouse.Button.Right -> { + // open settings window + } + } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/windows/TagWindow.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/windows/TagWindow.kt new file mode 100644 index 000000000..7249a0e11 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/windows/TagWindow.kt @@ -0,0 +1,41 @@ +package com.lambda.gui.impl.clickgui.windows + +import com.lambda.gui.api.LambdaGui +import com.lambda.gui.api.component.WindowComponent +import com.lambda.gui.api.component.core.list.IChildComponent +import com.lambda.gui.impl.clickgui.LambdaClickGui +import com.lambda.gui.impl.clickgui.buttons.ModuleButton +import com.lambda.module.ModuleRegistry +import com.lambda.module.modules.client.ClickGui +import com.lambda.module.tag.ModuleTag + +class TagWindow( + val tags: Set = setOf(), + override var title: String = "Untitled", + override var width: Double = 110.0, + override var height: Double = 300.0, + override val owner: LambdaGui = LambdaClickGui +) : WindowComponent(), IChildComponent { + init { + ModuleRegistry.modules.filter { module -> + module.customTags.value.any(tags::contains) || tags.isEmpty() + }.forEach { + children.add(ModuleButton(it, this)) + } + } + + override fun onRender() { + updateModules() + super.onRender() + } + + private fun updateModules() { + children.sortBy { it.module.name } + + // ToDo: Update tag filter + + children.forEachIndexed { i, button -> + button.heightOffset = i * (ClickGui.buttonHeight + ClickGui.buttonStep) + } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/PlayerPacketManager.kt b/common/src/main/kotlin/com/lambda/interaction/PlayerPacketManager.kt index f9e7b5936..da2f04057 100644 --- a/common/src/main/kotlin/com/lambda/interaction/PlayerPacketManager.kt +++ b/common/src/main/kotlin/com/lambda/interaction/PlayerPacketManager.kt @@ -16,6 +16,8 @@ import com.lambda.util.primitives.extension.component2 import com.lambda.util.primitives.extension.component3 import net.minecraft.network.packet.c2s.play.ClientCommandC2SPacket import net.minecraft.network.packet.c2s.play.PlayerMoveC2SPacket.* +import net.minecraft.util.math.MathHelper.square +import net.minecraft.util.math.Vec3d object PlayerPacketManager : Loadable { val configurations = LimitedOrderedSet(100) diff --git a/common/src/main/kotlin/com/lambda/module/Module.kt b/common/src/main/kotlin/com/lambda/module/Module.kt index 7adf79675..545bc6195 100644 --- a/common/src/main/kotlin/com/lambda/module/Module.kt +++ b/common/src/main/kotlin/com/lambda/module/Module.kt @@ -1,5 +1,6 @@ package com.lambda.module +import com.google.gson.reflect.TypeToken import com.lambda.config.AbstractSetting import com.lambda.config.Configurable import com.lambda.config.Configuration @@ -13,6 +14,7 @@ import com.lambda.event.listener.Listener import com.lambda.event.listener.SafeListener import com.lambda.event.listener.SafeListener.Companion.listener import com.lambda.event.listener.UnsafeListener +import com.lambda.gui.impl.clickgui.LambdaClickGui import com.lambda.module.tag.ModuleTag import com.lambda.util.KeyCode import com.lambda.util.Nameable @@ -97,7 +99,7 @@ abstract class Module( private val isEnabledSetting = setting("Enabled", enabledByDefault, visibility = { false }) private val keybindSetting = setting("Keybind", defaultKeybind) private val isVisible = setting("Visible", true) - private val customTags = setting("Tags", defaultTags, visibility = { false }) + val customTags = setting("Tags", defaultTags, visibility = { false }) var isEnabled by isEnabledSetting override val isMuted: Boolean @@ -106,9 +108,11 @@ abstract class Module( init { listener(alwaysListen = true) { event -> - if (mc.currentScreen == null && event.key == keybind.key) { - toggle() - } + val screen = mc.currentScreen + if (event.key == keybind.key + && (screen == null + || screen is LambdaClickGui) + ) toggle() } } diff --git a/common/src/main/kotlin/com/lambda/module/modules/Packetlogger.kt b/common/src/main/kotlin/com/lambda/module/modules/Packetlogger.kt index eecc91ea7..f2a44e6e4 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/Packetlogger.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/Packetlogger.kt @@ -1,5 +1,6 @@ package com.lambda.module.modules +import com.google.gson.reflect.TypeToken import com.lambda.Lambda import com.lambda.Lambda.mc import com.lambda.event.EventFlow.lambdaScope @@ -20,6 +21,7 @@ import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch import net.minecraft.network.packet.Packet +import java.awt.Color import java.io.File import java.nio.file.Path import java.time.format.DateTimeFormatter @@ -36,8 +38,8 @@ object Packetlogger : Module( private val networkSide by setting("Network Side", NetworkSide.ANY, "Side of the network to log packets from") private val logTicks by setting("Log Ticks", true, "Show game ticks in the log") private val scope by setting("Scope", Scope.ANY, "Scope of packets to log") - private val whitelist by setting("Whitelist Packets", emptyList(), "Packets to whitelist") { scope == Scope.WHITELIST } - private val blacklist by setting("Blacklist Packets", emptyList(), "Packets to blacklist") { scope == Scope.BLACKLIST } + private val whitelist by setting("Whitelist Packets", listOf(), "Packets to whitelist") { scope == Scope.WHITELIST } + private val blacklist by setting("Blacklist Packets", listOf(), "Packets to blacklist") { scope == Scope.BLACKLIST } private val maxRecursionDepth by setting("Max Recursion Depth", 6, 1..10, 1, "Maximum recursion depth for packet serialization") private val logConcurrent by setting("Build Data Concurrent", false, "Whether to serialize packets concurrently. Will not save packets in chronological order but wont lag the game.") @@ -88,7 +90,7 @@ object Packetlogger : Module( val info = buildText { clickEvent(ClickEvents.openFile(relativePath.pathString)) { literal("Packet logger started: ") - color(Color.GOLD) { literal(fileName) } + color(Color.YELLOW) { literal(fileName) } literal(" (click to open)") } } @@ -124,7 +126,7 @@ object Packetlogger : Module( val info = buildText { literal("Stopped logging packets to ") clickEvent(ClickEvents.openFile(it.relativePath.pathString)) { - color(Color.GOLD) { literal(it.relativePath.pathString) } + color(Color.YELLOW) { literal(it.relativePath.pathString) } literal(" (click to open)") } } diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt b/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt new file mode 100644 index 000000000..4b18b7b80 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt @@ -0,0 +1,39 @@ +package com.lambda.module.modules.client + +import com.lambda.event.events.ClientEvent +import com.lambda.event.listener.UnsafeListener.Companion.unsafeListener +import com.lambda.gui.impl.clickgui.LambdaClickGui +import com.lambda.module.Module +import com.lambda.module.tag.ModuleTag +import com.lambda.threading.mainThread +import java.awt.Color + +object ClickGui : Module( + name = "ClickGui", + description = "Sexy", + defaultTags = setOf(ModuleTag.CLIENT) +) { + // General + val windowRadius by setting("Window Radius", 2.0, 0.0..10.0, 0.1) + val windowPadding by setting("Window Padding", 2.0, 0.0..10.0, 0.1) + val buttonHeight by setting("Button Height", 11.0, 8.0..20.0, 0.1) + val buttonStep by setting("Button Step", 1.0, 0.0..5.0, 0.1) + + init { + onEnable { + if (mc.currentScreen != LambdaClickGui) { + LambdaClickGui.show() + } + } + + onDisable { + if (mc.currentScreen == LambdaClickGui) { + LambdaClickGui.close() + } + } + + unsafeListener { + disable() + } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/FontSettings.kt b/common/src/main/kotlin/com/lambda/module/modules/client/FontSettings.kt new file mode 100644 index 000000000..ed0c42c82 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/module/modules/client/FontSettings.kt @@ -0,0 +1,20 @@ +package com.lambda.module.modules.client + +import com.lambda.module.Module +import com.lambda.module.tag.ModuleTag + +object FontSettings : Module( + name = "FontSettings", + description = "Font renderer configuration", + defaultTags = setOf(ModuleTag.CLIENT) +) { + val shadow by setting("Shadow", true) + val shadowBrightness by setting("Shadow Brightness", 0.35, 0.0..0.5, 0.01, visibility = { shadow }) + val shadowShift by setting("Shadow Shift", 1.0, 0.0..2.0, 0.05, visibility = { shadow }) + val gapSetting by setting("Gap", 1.5, -10.0..10.0, 0.5) + val baselineOffset by setting("Vertical Offset", 0.0, -10.0..10.0, 0.5) + val amountOfGlyphs by setting("Glyph Count", 2048, 128..65536, 1, description = "Restart required") + private val lodBiasSetting by setting("Smoothing", 0.0, -10.0..10.0, 0.5) + + val lodBias get() = lodBiasSetting * 0.25f - 0.75f +} diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/GuiSettings.kt b/common/src/main/kotlin/com/lambda/module/modules/client/GuiSettings.kt new file mode 100644 index 000000000..60c98773f --- /dev/null +++ b/common/src/main/kotlin/com/lambda/module/modules/client/GuiSettings.kt @@ -0,0 +1,38 @@ +package com.lambda.module.modules.client + +import com.lambda.module.Module +import com.lambda.module.tag.ModuleTag +import java.awt.Color + +object GuiSettings : Module( + name = "GuiSettings", + description = "Visual behaviour configuration", + defaultTags = setOf(ModuleTag.CLIENT) +) { + private val page by setting("Page", Page.General) + + // General + private val scaleSetting by setting("Scale", 1.0, 0.5..3.0, 0.01, visibility = { page == Page.General }) + + // Colors + private val primaryColor by setting("Primary Color", Color(130, 200, 255), visibility = { page == Page.Colors }) + private val secondaryColor by setting("Secondary Color", Color(225, 130, 225), visibility = { page == Page.Colors && shade }) + val backgroundColor by setting("Background Color", Color(0, 0, 0, 80), visibility = { page == Page.Colors }) + val glow by setting("Glow", true, visibility = { page == Page.Colors }) + val shade by setting("Shade Color", true, visibility = { page == Page.Colors }) + val colorWidth by setting("Color Width", 40.0, 1.0..100.0, 1.0, visibility = { page == Page.Colors && shade }) + val colorHeight by setting("Color Height", 40.0, 1.0..100.0, 1.0, visibility = { page == Page.Colors && shade }) + val colorSpeed by setting("Color Speed", 1.0, 0.1..10.0, 0.1, visibility = { page == Page.Colors && shade }) + + val mainColor: Color get() = if (shade) Color.WHITE else primaryColor + + val shadeColor1 get() = primaryColor + val shadeColor2 get() = secondaryColor + + enum class Page { + General, + Colors + } + + val scale get() = scaleSetting * 2 +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/Rubberband.kt b/common/src/main/kotlin/com/lambda/module/modules/client/Rubberband.kt index 9f79a8118..8b9ff9d03 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/Rubberband.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/Rubberband.kt @@ -8,12 +8,12 @@ import com.lambda.module.tag.ModuleTag import com.lambda.util.Communication.warn import com.lambda.util.math.VecUtils.dist import com.lambda.util.math.VecUtils.distSq -import com.lambda.util.text.Color import com.lambda.util.text.buildText import com.lambda.util.text.color import com.lambda.util.text.literal import net.minecraft.network.packet.s2c.play.PlayerPositionLookS2CPacket import net.minecraft.util.math.Vec3d +import java.awt.Color // ToDo: Should also include last packet info as HUD element and connection state. // We should find a better name. diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/ServerSpoof.kt b/common/src/main/kotlin/com/lambda/module/modules/client/ServerSpoof.kt index 3afb0451e..aaf045395 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/ServerSpoof.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/ServerSpoof.kt @@ -12,6 +12,7 @@ import net.minecraft.network.packet.BrandCustomPayload import net.minecraft.network.packet.c2s.common.CustomPayloadC2SPacket import net.minecraft.network.packet.s2c.common.ResourcePackSendS2CPacket import net.minecraft.text.ClickEvent +import java.awt.Color object ServerSpoof : Module( name = "ServerSpoof", diff --git a/common/src/main/kotlin/com/lambda/threading/MainThreadInit.kt b/common/src/main/kotlin/com/lambda/threading/MainThreadInit.kt new file mode 100644 index 000000000..074f873ec --- /dev/null +++ b/common/src/main/kotlin/com/lambda/threading/MainThreadInit.kt @@ -0,0 +1,17 @@ +package com.lambda.threading + +import kotlin.reflect.KProperty + +class MainThreadInit (private val initializer: () -> T) { + private lateinit var value: T + + operator fun getValue(thisRef: Any?, property: KProperty<*>) = value + + init { + runOnGameThread { + value = initializer() + } + } +} + +fun mainThread(initializer: () -> T) = MainThreadInit(initializer) \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/util/Communication.kt b/common/src/main/kotlin/com/lambda/util/Communication.kt index 5c637baad..e849d26ae 100644 --- a/common/src/main/kotlin/com/lambda/util/Communication.kt +++ b/common/src/main/kotlin/com/lambda/util/Communication.kt @@ -2,11 +2,14 @@ package com.lambda.util import com.lambda.Lambda import com.lambda.Lambda.mc +import com.lambda.threading.runOnGameThread import com.lambda.threading.runSafe +import com.lambda.threading.runSafeOnGameThread import com.lambda.util.StringUtils.capitalize import com.lambda.util.text.* import net.minecraft.client.toast.SystemToast import net.minecraft.text.Text +import java.awt.Color object Communication { val ascii = """ @@ -33,10 +36,10 @@ object Communication { } fun Any.toast(message: Text, logLevel: LogLevel = LogLevel.INFO) { - runSafe { - buildText { - text(this@toast.source(logLevel, color = Color.YELLOW)) - }.let { title -> + buildText { + text(this@toast.source(logLevel, color = Color.YELLOW)) + }.let { title -> + runSafeOnGameThread { mc.toastManager.add(logLevel.toast(title, message)) } } @@ -52,11 +55,11 @@ object Communication { } fun Any.log(message: Text, logLevel: LogLevel = LogLevel.INFO, source: String = "", textSource: Text = Text.empty()) { - runSafe { - buildText { - text(this@log.source(logLevel, source, textSource)) - text(message) - }.let { log -> + buildText { + text(this@log.source(logLevel, source, textSource)) + text(message) + }.let { log -> + runSafeOnGameThread { player.sendMessage(log) } } @@ -66,7 +69,7 @@ object Communication { logLevel: LogLevel, source: String = "", textSource: Text = Text.empty(), - color: Color = Color.GREY, + color: Color = Color.GRAY ) = buildText { text(logLevel.prefix()) diff --git a/common/src/main/kotlin/com/lambda/util/LambdaResource.kt b/common/src/main/kotlin/com/lambda/util/LambdaResource.kt new file mode 100644 index 000000000..f462bd3ad --- /dev/null +++ b/common/src/main/kotlin/com/lambda/util/LambdaResource.kt @@ -0,0 +1,6 @@ +package com.lambda.util + +class LambdaResource(val path: String) { + val stream get() = + javaClass.getResourceAsStream("/assets/lambda/$path") +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/util/Mouse.kt b/common/src/main/kotlin/com/lambda/util/Mouse.kt new file mode 100644 index 000000000..01b5c940e --- /dev/null +++ b/common/src/main/kotlin/com/lambda/util/Mouse.kt @@ -0,0 +1,21 @@ +package com.lambda.util + +import org.lwjgl.glfw.GLFW + +class Mouse { + @JvmInline + value class Button(val key: Int) { + companion object { + val Left = Button(GLFW.GLFW_MOUSE_BUTTON_LEFT) + val Right = Button(GLFW.GLFW_MOUSE_BUTTON_RIGHT) + val Middle = Button(GLFW.GLFW_MOUSE_BUTTON_MIDDLE) + } + + val isMainButton get() = key == GLFW.GLFW_MOUSE_BUTTON_LEFT || key == GLFW.GLFW_MOUSE_BUTTON_RIGHT + } + + enum class Action { + Click, + Release + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/util/math/Color.kt b/common/src/main/kotlin/com/lambda/util/math/Color.kt new file mode 100644 index 000000000..15cdcffeb --- /dev/null +++ b/common/src/main/kotlin/com/lambda/util/math/Color.kt @@ -0,0 +1,19 @@ +package com.lambda.util.math + +import java.awt.Color + +val Color.hsb get() = Color.RGBtoHSB(red, green, blue, null) + .map(Float::toDouble) + .toDoubleArray() + +fun DoubleArray.readHSB() = + Color.getHSBColor(this[0].toFloat(), this[1].toFloat(), this[2].toFloat()) + +val Color.hue get() = hsb[0] +val Color.saturation get() = hsb[1] +val Color.brightness get() = hsb[2] + +val Color.r get() = red / 255f +val Color.g get() = green / 255f +val Color.b get() = blue / 255f +val Color.a get() = alpha / 255f \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/util/math/MathUtils.kt b/common/src/main/kotlin/com/lambda/util/math/MathUtils.kt index 3927758d2..94abd79f5 100644 --- a/common/src/main/kotlin/com/lambda/util/math/MathUtils.kt +++ b/common/src/main/kotlin/com/lambda/util/math/MathUtils.kt @@ -125,6 +125,11 @@ object MathUtils { lerp(start.y, end.y, factor) ) + fun lerp(start: Rect, end: Rect, factor: Double) = + Rect( + lerp(start.leftTop, end.leftTop, factor), + lerp(start.rightBottom, end.rightBottom, factor) + ) fun lerp(start: Rotation, end: Rotation, factor: Double) = Rotation( diff --git a/common/src/main/kotlin/com/lambda/util/math/Rect.kt b/common/src/main/kotlin/com/lambda/util/math/Rect.kt new file mode 100644 index 000000000..8fcfe756c --- /dev/null +++ b/common/src/main/kotlin/com/lambda/util/math/Rect.kt @@ -0,0 +1,53 @@ +package com.lambda.util.math + +import com.lambda.util.math.MathUtils.lerp + +data class Rect(private val pos1: Vec2d, private val pos2: Vec2d) { + val left = pos1.x + val top = pos1.y + val right = pos2.x + val bottom = pos2.y + + val leftTop get() = Vec2d(left, top) + val rightTop get() = Vec2d(right, top) + val rightBottom get() = Vec2d(right, bottom) + val leftBottom get() = Vec2d(left, bottom) + + val size get() = Vec2d(right - left, bottom - top) + val center get() = lerp(pos1, pos2, 0.5) + + operator fun plus(vec2d: Vec2d) = Rect(pos1 + vec2d, pos2 + vec2d) + operator fun minus(vec2d: Vec2d) = Rect(pos1 - vec2d, pos2 - vec2d) + + fun moveFirst(vec2d: Vec2d) = Rect(pos1 + vec2d, pos2) + fun moveSecond(vec2d: Vec2d) = Rect(pos1, pos2 + vec2d) + + fun expand(amount: Double) = Rect(pos1 - amount, pos2 + amount) + fun shrink(amount: Double) = expand(-amount) + + fun clamp(rect: Rect) = + Rect( + Vec2d(max(left, rect.left), max(top, rect.top)), + Vec2d(min(right, rect.right), min(bottom, rect.bottom)) + ) + + operator fun contains(point: Vec2d): Boolean { + if (size.x <= 0.0 || size.y <= 0.0) return false + return point.x in left..right && point.y in top..bottom + } + + operator fun contains(other: Rect): Boolean { + if (size.x <= 0.0 || size.y <= 0.0) return false + return other.leftTop in this || other.rightBottom in this || leftTop in other || rightBottom in other + } + + companion object { + val ZERO = Rect(Vec2d.ZERO, Vec2d.ZERO) + + fun basedOn(base: Vec2d, width: Double, height: Double) = + Rect(base, base + Vec2d(width, height)) + + fun basedOn(base: Vec2d, size: Vec2d) = + Rect(base, base + size) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/util/math/Vec2d.kt b/common/src/main/kotlin/com/lambda/util/math/Vec2d.kt index a88e90ea0..31f85bf9b 100644 --- a/common/src/main/kotlin/com/lambda/util/math/Vec2d.kt +++ b/common/src/main/kotlin/com/lambda/util/math/Vec2d.kt @@ -1,47 +1,32 @@ package com.lambda.util.math -import net.minecraft.util.math.Vec2f -import kotlin.math.roundToInt - data class Vec2d(val x: Double, val y: Double) { constructor(x: Int, y: Int) : this(x.toDouble(), y.toDouble()) constructor(x: Float, y: Float) : this(x.toDouble(), y.toDouble()) - constructor(x: Short, y: Short) : this(x.toDouble(), y.toDouble()) operator fun plus(vec2d: Vec2d) = plus(vec2d.x, vec2d.y) - operator fun plus(add: Double) = plus(add, add) - fun plus(x: Double, y: Double) = Vec2d(this.x + x, this.y + y) operator fun minus(vec2d: Vec2d) = minus(vec2d.x, vec2d.y) - operator fun minus(sub: Double) = minus(sub, sub) - fun minus(x: Double, y: Double) = plus(-x, -y) operator fun times(vec2d: Vec2d) = times(vec2d.x, vec2d.y) - operator fun times(multiplier: Double) = times(multiplier, multiplier) - fun times(x: Double, y: Double) = Vec2d(this.x * x, this.y * y) operator fun div(vec2d: Vec2d) = div(vec2d.x, vec2d.y) - operator fun div(divider: Double) = div(divider, divider) - fun div(x: Double, y: Double) = Vec2d(this.x / x, this.y / y) - val vec2f = Vec2f(x.toFloat(), y.toFloat()) - val rounded get() = Vec2d(x.roundToInt(), y.roundToInt()) - companion object { - val ZERO: Vec2d = Vec2d(0.0, 0.0) - val ONE: Vec2d = Vec2d(1.0, 1.0) + val ZERO = Vec2d(0.0, 0.0) + val ONE = Vec2d(1.0, 1.0) - val RIGHT: Vec2d = Vec2d(1.0, 0.0) - val LEFT: Vec2d = Vec2d(-1.0, 0.0) - val TOP: Vec2d = Vec2d(0.0, 1.0) - val BOTTOM: Vec2d = Vec2d(0.0, -1.0) + val LEFT = Vec2d(-1.0, 0.0) + val RIGHT = Vec2d(1.0, 0.0) + val TOP = Vec2d(0.0, 1.0) + val BOTTOM = Vec2d(0.0, -1.0) } } \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/util/text/Color.kt b/common/src/main/kotlin/com/lambda/util/text/Color.kt deleted file mode 100644 index 30b47b06c..000000000 --- a/common/src/main/kotlin/com/lambda/util/text/Color.kt +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright 2023 The Quilt Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.lambda.util.text - -import net.minecraft.block.MapColor -import net.minecraft.text.TextColor -import net.minecraft.util.DyeColor -import net.minecraft.util.Formatting -import kotlin.math.roundToInt - - -/** - * Converts RGB values from floats to a [Color] for use in text. - * Each argument should be within 0 and 1. If a number is outside the range, the function will automatically default it - * to the maximum or minimum value, depending on which is nearer to the provided value. - * - * @param red The red channel of the color - * @param green The green channel of the color - * @param blue The blue channel of the color - * - * @return A [Color] created from the provided RGB Channels - */ -@Suppress("MagicNumber") -fun Color(red: Float, green: Float, blue: Float): Color = Color( - (red.coerceIn(0F, 1F) * 255).roundToInt(), - (green.coerceIn(0F, 1F) * 255).roundToInt(), - (blue.coerceIn(0F, 1F) * 255).roundToInt() -) - -/** - * Converts RGB values from doubles to a [Color] for use in text. - * Each argument should be within 0 and 1. If a number is outside the range, the function will automatically default it - * to the maximum or minimum value, depending on which is nearer to the provided value. - * - * @param red The red channel of the color - * @param green The green channel of the color - * @param blue The blue channel of the color - * - * @return A [Color] created from the provided RGB Channels - */ -@Suppress("MagicNumber") -fun Color(red: Double, green: Double, blue: Double): Color = Color( - (red.coerceIn(0.0, 1.0) * 255).roundToInt(), - (green.coerceIn(0.0, 1.0) * 255).roundToInt(), - (blue.coerceIn(0.0, 1.0) * 255).roundToInt(), -) - -/** - * A color class that can transform RGB values into color codes. - * - * @property value The RGB value to convert to a color code. - */ -@JvmInline -value class Color(val value: Int) { - @Suppress("MagicNumber") - constructor(red: Int, green: Int, blue: Int) : this( - (red.coerceIn(0, 255) shl 16) + - (green.coerceIn(0, 255) shl 8) + - blue.coerceIn(0, 255) - ) - - /** A color of red influenced by [value]. */ - val red: Int get() = value shr 16 and 0xFF - - /** A color of green influenced by [value]. */ - val green: Int get() = value shr 8 and 0xFF - - /** A color of blue influenced by [value]. */ - val blue: Int get() = value and 0xFF - - companion object { - /** Minecraft's native black color. */ - val BLACK: Color = Color(0x000000) - - /** Minecraft's native dark blue color. */ - val DARK_BLUE: Color = Color(0x0000AA) - - /** Minecraft's native dark green color. */ - val DARK_GREEN: Color = Color(0x00AA00) - - /** Minecraft's native dark aqua color. */ - val DARK_AQUA: Color = Color(0x00AAAA) - - /** Minecraft's native dark red color. */ - val DARK_RED: Color = Color(0xAA0000) - - /** Minecraft's native dark purple color. */ - val DARK_PURPLE: Color = Color(0xAA00AA) - - /** Minecraft's native gold color. */ - val GOLD: Color = Color(0xFFAA00) - - /** Minecraft's native grey color. */ - val GREY: Color = Color(0xAAAAAA) - - /** Minecraft's native dark grey color. */ - val DARK_GREY: Color = Color(0x555555) - - /** Minecraft's native blue color. */ - val BLUE: Color = Color(0x5555FF) - - /** Minecraft's native green color. */ - val GREEN: Color = Color(0x55FF55) - - /** Minecraft's native aqua color. */ - val AQUA: Color = Color(0x55FFFF) - - /** Minecraft's native red color. */ - val RED: Color = Color(0xFF5555) - - /** Minecraft's native light purple color. */ - val LIGHT_PURPLE: Color = Color(0xFF55FF) - - /** Minecraft's native yellow color. */ - val YELLOW: Color = Color(0xFFFF55) - - /** Minecraft's native white color. */ - val WHITE: Color = Color(0xFFFFFF) - - /** - * Gets a color from [TextColor] and converts it to [Color]. - * - * @param color The [TextColor] to convert - * @return A [Color] from [TextColor] - */ - fun from(color: TextColor): Color { - return Color(color.rgb) - } - - /** - * Gets a color from [Formatting] and converts it to [Color]. - * If the color value is null, it defaults to black. - * - * @param color The [Formatting] to convert - * @return A [Color] from [Formatting] or black if invalid/null - */ - fun from(color: Formatting): Color { - return Color(color.colorValue?.let(::Color)?.value ?: BLACK.value) - } - - /** - * Gets a color from [MapColor] and converts it to [Color]. - * - * @param color The [MapColor] to convert - * @return A [Color] from [MapColor] - */ - fun from(color: MapColor): Color { - return Color(color.color) - } - - /** - * Gets a color from [DyeColor] and converts it to [Color]. - * - * @param color The [DyeColor] to convert - * @return A [Color] from [DyeColor] - */ - fun from(color: DyeColor): Color { - return Color(color.signColor) - } - } - - /** - * Converts [value] to a [TextColor]. - * - * @return A [TextColor] created from the [value] value - */ - fun toTextColor(): TextColor { - return TextColor.fromRgb(value) - } -} diff --git a/common/src/main/kotlin/com/lambda/util/text/StyleDsl.kt b/common/src/main/kotlin/com/lambda/util/text/StyleDsl.kt index 4d2e811a8..53e227a18 100644 --- a/common/src/main/kotlin/com/lambda/util/text/StyleDsl.kt +++ b/common/src/main/kotlin/com/lambda/util/text/StyleDsl.kt @@ -18,11 +18,9 @@ package com.lambda.util.text -import net.minecraft.text.ClickEvent -import net.minecraft.text.HoverEvent -import net.minecraft.text.MutableText -import net.minecraft.text.Style +import net.minecraft.text.* import net.minecraft.util.Identifier +import java.awt.Color /** * Marks objects as being part the Style Builder DSL. @@ -177,7 +175,7 @@ class StyleBuilder { * @param blue The blue channel of the color */ fun color(red: Double, green: Double, blue: Double) { - this.color = Color(red, green, blue) + color(red.toFloat(), green.toFloat(), blue.toFloat()) } /** @@ -225,7 +223,7 @@ class StyleBuilder { * into this builder. */ fun copyFrom(base: Style) { - color = base.color?.let(Color::from) ?: color + color = base.color?.let { Color(it.rgb) } ?: color bold = base.isBold italic = base.isItalic strikethrough = base.strikethrough @@ -250,7 +248,7 @@ class StyleBuilder { } return Style( - color?.toTextColor(), + color?.let { TextColor.fromRgb(it.rgb) }, bold, italic, underlined, diff --git a/common/src/main/kotlin/com/lambda/util/text/TextDsl.kt b/common/src/main/kotlin/com/lambda/util/text/TextDsl.kt index ba2a59120..f2f4bbcf6 100644 --- a/common/src/main/kotlin/com/lambda/util/text/TextDsl.kt +++ b/common/src/main/kotlin/com/lambda/util/text/TextDsl.kt @@ -18,6 +18,7 @@ package com.lambda.util.text import net.minecraft.text.* import net.minecraft.util.Identifier +import java.awt.Color import java.util.* /** @@ -320,7 +321,7 @@ fun TextBuilder.styled( @TextDsl fun TextBuilder.styled(style: Style, action: TextBuilder.() -> Unit) { styled( - style.color?.let(Color::from) ?: this.style.color, + style.color?.let { Color(it.rgb) } ?: this.style.color, style.isBold, style.isItalic, style.isUnderlined, diff --git a/common/src/main/resources/assets/lambda/fonts/FiraSans-Bold.ttf b/common/src/main/resources/assets/lambda/fonts/FiraSans-Bold.ttf new file mode 100644 index 000000000..95e166024 Binary files /dev/null and b/common/src/main/resources/assets/lambda/fonts/FiraSans-Bold.ttf differ diff --git a/common/src/main/resources/assets/lambda/fonts/FiraSans-Regular.ttf b/common/src/main/resources/assets/lambda/fonts/FiraSans-Regular.ttf new file mode 100644 index 000000000..d9fdc0e92 Binary files /dev/null and b/common/src/main/resources/assets/lambda/fonts/FiraSans-Regular.ttf differ diff --git a/common/src/main/resources/assets/lambda/shaders/fragment/renderer/font.frag b/common/src/main/resources/assets/lambda/shaders/fragment/renderer/font.frag new file mode 100644 index 000000000..3522a98c5 --- /dev/null +++ b/common/src/main/resources/assets/lambda/shaders/fragment/renderer/font.frag @@ -0,0 +1,13 @@ +#version 330 core + +uniform sampler2D u_Texture; + +in vec2 v_TexCoord; +in vec4 v_Color; + +out vec4 color; + +void main() { + float alpha = texture(u_Texture, v_TexCoord).a; + color = vec4(v_Color.rgb, v_Color.a * alpha); +} \ No newline at end of file diff --git a/common/src/main/resources/assets/lambda/shaders/fragment/renderer/rect.frag b/common/src/main/resources/assets/lambda/shaders/fragment/renderer/rect.frag new file mode 100644 index 000000000..08a07c8be --- /dev/null +++ b/common/src/main/resources/assets/lambda/shaders/fragment/renderer/rect.frag @@ -0,0 +1,45 @@ +#version 330 core + +uniform bool u_Shade; +uniform float u_Time; +uniform vec4 u_Color1; +uniform vec4 u_Color2; +uniform vec2 u_Size; + +in vec2 v_Position; +in vec2 v_TexCoord; +in vec4 v_Color; +in vec2 v_Size; +in float v_RoundRadius; + +out vec4 color; + +#define SMOOTHING 0.5 + +vec4 shade() { + if (!u_Shade) return v_Color; + + vec2 pos = v_Position * u_Size; + float p = sin(pos.x + pos.y - u_Time) * 0.5 + 0.5; + + return mix(u_Color1, u_Color2, p) * vec4(1.0, 1.0, 1.0, v_Color.a); +} + +vec4 round() { + vec2 halfSize = v_Size * 0.5; + + float radius = max(v_RoundRadius, SMOOTHING); + + vec2 smoothVec = vec2(SMOOTHING); + vec2 coord = mix(-smoothVec, v_Size + smoothVec, v_TexCoord); + + vec2 center = halfSize - coord; + float distance = length(max(abs(center) - halfSize + radius, 0.0)) - radius; + + float alpha = 1.0 - smoothstep(-SMOOTHING, SMOOTHING, distance); + return vec4(1.0, 1.0, 1.0, clamp(alpha, 0.0, 1.0)); +} + +void main() { + color = shade() * round(); +} \ No newline at end of file diff --git a/common/src/main/resources/assets/lambda/shaders/vertex/renderer/font.vert b/common/src/main/resources/assets/lambda/shaders/vertex/renderer/font.vert new file mode 100644 index 000000000..96ad268b9 --- /dev/null +++ b/common/src/main/resources/assets/lambda/shaders/vertex/renderer/font.vert @@ -0,0 +1,18 @@ +#version 330 core + +layout (location = 0) in vec4 pos; +layout (location = 1) in vec2 uv; +layout (location = 2) in vec4 color; + +uniform mat4 u_Projection; +uniform mat4 u_ModelView; + +out vec2 v_TexCoord; +out vec4 v_Color; + +void main() { + gl_Position = u_Projection * u_ModelView * pos; + + v_TexCoord = uv; + v_Color = color; +} \ No newline at end of file diff --git a/common/src/main/resources/assets/lambda/shaders/vertex/renderer/rect.vert b/common/src/main/resources/assets/lambda/shaders/vertex/renderer/rect.vert new file mode 100644 index 000000000..39940d4b9 --- /dev/null +++ b/common/src/main/resources/assets/lambda/shaders/vertex/renderer/rect.vert @@ -0,0 +1,26 @@ +#version 330 core + +layout (location = 0) in vec4 pos; +layout (location = 1) in vec2 uv; +layout (location = 2) in vec3 data; +layout (location = 3) in vec4 color; + +uniform mat4 u_Projection; +uniform mat4 u_ModelView; + +out vec2 v_Position; +out vec2 v_TexCoord; +out vec4 v_Color; +out vec2 v_Size; +out float v_RoundRadius; + +void main() { + gl_Position = u_Projection * u_ModelView * pos; + + v_Position = pos.xy; + v_TexCoord = uv; + v_Color = color; + + v_Size = data.xy; + v_RoundRadius = data.z; +} \ No newline at end of file diff --git a/common/src/main/resources/lambda.accesswidener b/common/src/main/resources/lambda.accesswidener index eba7ff6b3..f0e1dbcf9 100644 --- a/common/src/main/resources/lambda.accesswidener +++ b/common/src/main/resources/lambda.accesswidener @@ -16,6 +16,11 @@ accessible method net/minecraft/entity/Entity movementInputToVelocity (Lnet/mine accessible method net/minecraft/client/render/Camera setPos (DDD)V accessible method net/minecraft/client/render/Camera setRotation (FF)V +# Renderer +accessible field com/mojang/blaze3d/systems/RenderSystem$ShapeIndexBuffer id I +accessible field net/minecraft/client/render/BufferRenderer currentVertexBuffer Lnet/minecraft/client/gl/VertexBuffer; +accessible field net/minecraft/client/texture/NativeImage pointer J + # Text accessible field net/minecraft/text/Style color Lnet/minecraft/text/TextColor; accessible field net/minecraft/text/Style bold Ljava/lang/Boolean; diff --git a/common/src/main/resources/lambda.mixins.common.json b/common/src/main/resources/lambda.mixins.common.json index e7dd75816..e73266017 100644 --- a/common/src/main/resources/lambda.mixins.common.json +++ b/common/src/main/resources/lambda.mixins.common.json @@ -20,6 +20,9 @@ "render.ChatScreenMixin", "render.DebugHudMixin", "render.GameRendererMixin", + "render.GlStateManagerMixin", + "render.InGameHudMixin", + "render.VertexBufferMixin", "render.LightmapTextureManagerMixin", "render.LivingEntityRendererMixin", "render.RenderTickCounterMixin",