From b0ef8c582a01d9540546c763427a3f2d99e71fe0 Mon Sep 17 00:00:00 2001 From: StartsMercury <89975834+StartsMercury@users.noreply.github.com> Date: Sun, 10 Nov 2024 12:19:57 +0800 Subject: [PATCH] Clean-up and Additional Bits * Add .editorconfig * Add .gitattributes * Experimental input adapter, detecting key modifier * Experimental Math util exposure: intersection calculation for mouse enter and exit * Input API, allow registering multiple processors on top of normal Gdx.input * Move single-use utils closer to calling class * Simplify GameAssetLoaderMixin, moving complexity into helper class * Use Gradle Kotlin DSL: better IDE support and less weird warnings (that adds "noise") * Work on UI Component API * Initial children components * Preparing exact mouse enter and exit detection --- .editorconfig | 20 + .gitattributes | 2 + build.gradle | 64 -- build.gradle.kts | 100 +++ settings.gradle | 15 - settings.gradle.kts | 19 + .../flux/api/input/client/InputAdapterEx.java | 172 +++++ .../input/client/InputProcessorHelper.java | 60 ++ .../api/input/client/MouseProcessorEx.java | 7 + .../crmodders/flux/api/math/Intersection.java | 365 ++++++++++ .../resource/loader/FluxFileHandle.java | 2 +- .../components/client/AbstractUIObject.java | 378 ---------- .../api/ui/components/client/Component.java | 656 ++++++++++++++++++ .../crmodders/flux/impl/base/Constants.java | 9 - .../dev/crmodders/flux/impl/base/Logging.java | 8 - .../dev/crmodders/flux/impl/base/Strings.java | 9 - .../client/FluxApiInputClientEntrypoint.java | 12 + .../flux/impl/input/client/FluxInput.java | 337 +++++++++ .../impl/input/client/FluxInputProcessor.java | 204 ++++++ .../impl/resource/loader/AssetFinder.java | 9 +- .../resource/loader/FluxAssetLoading.java | 95 +++ .../resource/loader/GameAssetLoaderMixin.java | 80 +-- src/main/resources/flux-api.mixins.json | 2 +- src/main/resources/quilt.mod.json | 1 + .../flux/api/math/IntersectionTest.java | 130 ++++ 25 files changed, 2196 insertions(+), 560 deletions(-) create mode 100644 .editorconfig create mode 100644 .gitattributes delete mode 100644 build.gradle create mode 100644 build.gradle.kts delete mode 100644 settings.gradle create mode 100644 settings.gradle.kts create mode 100644 src/main/java/dev/crmodders/flux/api/input/client/InputAdapterEx.java create mode 100644 src/main/java/dev/crmodders/flux/api/input/client/InputProcessorHelper.java create mode 100644 src/main/java/dev/crmodders/flux/api/input/client/MouseProcessorEx.java create mode 100644 src/main/java/dev/crmodders/flux/api/math/Intersection.java rename src/main/java/dev/crmodders/flux/{impl => api}/resource/loader/FluxFileHandle.java (99%) delete mode 100644 src/main/java/dev/crmodders/flux/api/ui/components/client/AbstractUIObject.java create mode 100644 src/main/java/dev/crmodders/flux/api/ui/components/client/Component.java delete mode 100644 src/main/java/dev/crmodders/flux/impl/base/Constants.java delete mode 100644 src/main/java/dev/crmodders/flux/impl/base/Logging.java delete mode 100644 src/main/java/dev/crmodders/flux/impl/base/Strings.java create mode 100644 src/main/java/dev/crmodders/flux/impl/input/client/FluxApiInputClientEntrypoint.java create mode 100644 src/main/java/dev/crmodders/flux/impl/input/client/FluxInput.java create mode 100644 src/main/java/dev/crmodders/flux/impl/input/client/FluxInputProcessor.java create mode 100644 src/main/java/dev/crmodders/flux/impl/resource/loader/FluxAssetLoading.java create mode 100644 src/test/java/dev/crmodders/flux/api/math/IntersectionTest.java diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e69374a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +max_line_length = 100 +tab_width = 4 +ij_continuation_indent_size = 4 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = false +ij_smart_tabs = false +ij_wrap_on_typing = false + +[*.java] +ij_java_imports_layout = $*,|,* +ij_java_class_count_to_use_import_on_demand = 999 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..0f09d32 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto eol=lf +*.bat text eol=crlf \ No newline at end of file diff --git a/build.gradle b/build.gradle deleted file mode 100644 index b8eb06e..0000000 --- a/build.gradle +++ /dev/null @@ -1,64 +0,0 @@ -plugins { - id "cosmicloom" - id "maven-publish" -} - -loom { - accessWidenerPath = file("src/main/resources/flux-api.accesswidener") -} - -repositories { - flatDir { - dirs "lib" - } -} - -dependencies { - cosmicReach(loom.getCosmicReach(cosmic_reach_version)) - modImplementation(loom.getCosmicQuilt(cosmic_quilt_version)) - runtimeOnly(":testmod:") -} - -processResources { - // Locations of where to inject the properties - def resourceTargets = [ - "quilt.mod.json" - ] - - // Left item is the name in the target, right is the variable name - def replaceProperties = [ - "mod_version" : project.version, - "mod_group" : project.group, - "mod_name" : project.name, - "mod_id" : id, - "mod_desc" : flux_desc, - "cosmic_reach_version": cosmic_reach_version, - ] - - inputs.properties replaceProperties - replaceProperties.put "project", project - filesMatching(resourceTargets) { - expand replaceProperties - } -} - -java { - withSourcesJar() - // withJavadocJar() - - // Sets the Java version - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 -} - - -publishing { - publications { - maven(MavenPublication) { - groupId = group - artifactId = id - - from components.java - } - } -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..1eecf70 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,100 @@ +object Constants { + const val GROUP = "dev.crmodders" + const val MODID = "flux-api" + const val VERSION = "0.8.0-alpha.1" + + const val TITLE = "Flux API" + const val DESCRIPTION = "Community focused API for Cosmic Reach Quilt" + + const val VERSION_COSMIC_REACH = "0.3.6" + const val VERSION_COSMIC_QUILT = "03cc947b041184bc656e170d164ced5bc1477b37" +} + +base { + group = Constants.GROUP + archivesName = Constants.MODID + version = Constants.VERSION +} + +plugins { + `java-library` + `maven-publish` + id("cosmicloom") +} + +java { + withSourcesJar() +// withJavadocJar() + + // Sets the Java version + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +loom { + accessWidenerPath = file("src/main/resources/flux-api.accesswidener") +} + +repositories { + flatDir { + dirs("lib") + } +} + +dependencies { + cosmicReach(loom.cosmicReachClient("pre-alpha", Constants.VERSION_COSMIC_REACH)) + modImplementation(loom.cosmicQuilt(Constants.VERSION_COSMIC_QUILT)) + runtimeOnly(":testmod:") + + compileOnly("com.badlogicgames.gdx:gdx:1.12.1") +} + +tasks.withType { + // Locations of where to inject the properties + val resourceTargets = listOf( + "quilt.mod.json" + ) + + // Left item is the name in the target, right is the variable name + val replaceProperties = mapOf( + "mod_group" to Constants.GROUP, + "mod_id" to Constants.MODID, + "mod_version" to Constants.VERSION, + + "mod_name" to Constants.TITLE, + "mod_desc" to Constants.DESCRIPTION, + + "cosmic_reach_version" to Constants.VERSION_COSMIC_REACH, + ) + + inputs.properties(replaceProperties) + + filesMatching(resourceTargets) { + expand(replaceProperties) + } +} + +testing { + suites { + val test by getting(JvmTestSuite::class) { + useJUnitJupiter() + +// sources { +// val main by sourceSets.getting +// runtimeClasspath += main.runtimeClasspath +// compileClasspath += main.compileClasspath +// } + } + } +} + +publishing { + publications { + create("maven") { + groupId = Constants.GROUP + artifactId = Constants.MODID + + from(components["java"]) + } + } +} diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 1fec258..0000000 --- a/settings.gradle +++ /dev/null @@ -1,15 +0,0 @@ -buildscript() { - repositories { - maven { - name "JitPack" - url "https://jitpack.io" - } - - mavenCentral() - } - dependencies { - classpath "org.codeberg.CRModders:cosmic-loom:PR7-SNAPSHOT" - } -} - -rootProject.name = "Flux API" diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..7a9046e --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,19 @@ +buildscript { + repositories { + maven { + name = "JitPack" + url = uri("https://jitpack.io") + } + mavenCentral() + } + + dependencies { + classpath( + group = "org.codeberg.CRModders", + name = "cosmic-loom", + version = "PR7-SNAPSHOT", + ) + } +} + +rootProject.name = "Flux API" diff --git a/src/main/java/dev/crmodders/flux/api/input/client/InputAdapterEx.java b/src/main/java/dev/crmodders/flux/api/input/client/InputAdapterEx.java new file mode 100644 index 0000000..91ab134 --- /dev/null +++ b/src/main/java/dev/crmodders/flux/api/input/client/InputAdapterEx.java @@ -0,0 +1,172 @@ +package dev.crmodders.flux.api.input.client; + +import com.badlogic.gdx.Input; +import com.badlogic.gdx.InputProcessor; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Experimental +public abstract class InputAdapterEx implements InputProcessor { + public static class Modifiers { + public static final int ALT = 1; + public static final int ALT_LEFT = 2; + public static final int ALT_RIGHT = 4; + public static final int CONTROL = 8; + public static final int CONTROL_LEFT = 16; + public static final int CONTROL_RIGHT = 32; + public static final int SHIFT = 64; + public static final int SHIFT_LEFT = 128; + public static final int SHIFT_RIGHT = 256; + + public static final int ANY_ALT = ALT | ALT_LEFT | ALT_RIGHT; + public static final int ANY_CONTROL = CONTROL | CONTROL_LEFT | CONTROL_RIGHT; + public static final int ANY_SHIFT = SHIFT | SHIFT_LEFT | SHIFT_RIGHT; + + public static boolean isAltDown(final int i) { + return (i & ANY_ALT) != 0; + } + + public static boolean isControlDown(final int i) { + return (i & ANY_CONTROL) != 0; + } + + public static boolean isShiftDown(final int i) { + return (i & ANY_SHIFT) != 0; + } + + public static boolean isAltOnly(final int i) { + return isAltDown(i) && (i & ~ANY_ALT) == 0; + } + + public static boolean isControlOnly(final int i) { + return isControlDown(i) && (i & ~ANY_CONTROL) == 0; + } + + public static boolean isShiftOnly(final int i) { + return isShiftDown(i) && (i & ~ANY_SHIFT) == 0; + } + } + + private int modifiers; + + @Override + public boolean keyDown(final int keycode) { + final var modifiers = this.modifiers |= switch (keycode) { + case Input.Keys.ALT_LEFT -> Modifiers.ALT | Modifiers.ALT_LEFT; + case Input.Keys.ALT_RIGHT -> Modifiers.ALT | Modifiers.ALT_RIGHT; + case Input.Keys.SHIFT_LEFT -> Modifiers.SHIFT | Modifiers.SHIFT_LEFT; + case Input.Keys.SHIFT_RIGHT -> Modifiers.SHIFT | Modifiers.CONTROL_RIGHT; + case Input.Keys.CONTROL_LEFT -> Modifiers.CONTROL | Modifiers.CONTROL_LEFT; + case Input.Keys.CONTROL_RIGHT -> Modifiers.CONTROL | Modifiers.CONTROL_RIGHT; + default -> 0; + }; + return this.keyDownEx(keycode, modifiers); + } + + protected abstract boolean keyDownEx(int keycode, int modifiers); + + @Override + public boolean keyUp(final int keycode) { + final var modifiers = this.modifiers; + final var result = this.keyUpEx(keycode, modifiers); + + final int mask1; + final int mask2; + switch (keycode) { + case Input.Keys.ALT_LEFT -> { + mask1 = Modifiers.ALT | Modifiers.ALT_LEFT; + mask2 = Modifiers.ALT | Modifiers.ALT_RIGHT; + } + case Input.Keys.ALT_RIGHT -> { + mask1 = Modifiers.ALT | Modifiers.ALT_RIGHT; + mask2 = Modifiers.ALT | Modifiers.ALT_LEFT; + } + case Input.Keys.CONTROL_LEFT -> { + mask1 = Modifiers.CONTROL | Modifiers.CONTROL_LEFT; + mask2 = Modifiers.CONTROL | Modifiers.CONTROL_RIGHT; + } + case Input.Keys.CONTROL_RIGHT -> { + mask1 = Modifiers.CONTROL | Modifiers.CONTROL_RIGHT; + mask2 = Modifiers.CONTROL | Modifiers.CONTROL_LEFT; + } + case Input.Keys.SHIFT_LEFT -> { + mask1 = Modifiers.SHIFT | Modifiers.SHIFT_LEFT; + mask2 = Modifiers.SHIFT | Modifiers.SHIFT_RIGHT; + } + case Input.Keys.SHIFT_RIGHT -> { + mask1 = Modifiers.SHIFT | Modifiers.SHIFT_RIGHT; + mask2 = Modifiers.SHIFT | Modifiers.SHIFT_LEFT; + } + default -> { + mask1 = 0; + mask2 = 0; + } + } + final var mask = (modifiers & mask2) != mask2 ? mask1 : mask1 & ~mask2; + this.modifiers = modifiers & ~mask; + + return result; + } + + protected abstract boolean keyUpEx(int keycode, int modifiers); + + @Override + public boolean touchDown( + final int screenX, + final int screenY, + final int pointer, + final int button + ) { + return this.touchDownEx(screenX, screenY, pointer, button, this.modifiers); + } + + protected abstract boolean touchDownEx(int screenX, int screenY, int pointer, int button, int modifiers); + + @Override + public boolean touchUp( + final int screenX, + final int screenY, + final int pointer, + final int button + ) { + return this.touchUpEx(screenX, screenY, pointer, button, this.modifiers); + } + + protected abstract boolean touchUpEx(int screenX, int screenY, int pointer, int button, int modifiers); + + @Override + public boolean touchCancelled( + final int screenX, + final int screenY, + final int pointer, + final int button + ) { + return this.touchCancelledEx(screenX, screenY, pointer, button, this.modifiers); + } + + protected abstract boolean touchCancelledEx(int screenX, int screenY, int pointer, int button, int modifiers); + + @Override + public boolean touchDragged( + final int screenX, + final int screenY, + final int pointer + ) { + return this.touchDraggedEx(screenX, screenY, pointer, this.modifiers); + } + + protected abstract boolean touchDraggedEx(int screenX, int screenY, int pointer, int modifiers); + + @Override + public boolean mouseMoved(final int screenX, final int screenY) { + return this.mouseMovedEx(screenX, screenY, this.modifiers); + } + + protected abstract boolean mouseMovedEx(int screenX, int screenY, int modifiers); + + @Override + public boolean scrolled(final float amountX, final float amountY) { + return this.scrolledEx(amountX, amountY, this.modifiers); + } + + protected abstract boolean scrolledEx(float amountX, float amountY, int modifiers); +} diff --git a/src/main/java/dev/crmodders/flux/api/input/client/InputProcessorHelper.java b/src/main/java/dev/crmodders/flux/api/input/client/InputProcessorHelper.java new file mode 100644 index 0000000..ab9ca78 --- /dev/null +++ b/src/main/java/dev/crmodders/flux/api/input/client/InputProcessorHelper.java @@ -0,0 +1,60 @@ +package dev.crmodders.flux.api.input.client; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.Input; +import com.badlogic.gdx.InputProcessor; +import dev.crmodders.flux.impl.input.client.FluxInput; + +import java.util.LinkedHashSet; + +public final class InputProcessorHelper { + /** + * Adds an input processor. + *

+ * All added input processors are ran in insertion order through an internal + * {@link LinkedHashSet}. Addition will not succeed with a {@code null} + * processor, {@code this}, or the current main processor (through + * {@link Input#setInputProcessor}). The internal set works best with input + * processors that did not override the methods {@link Object#equals} and + * {@link Object#hashCode()}, wrapper or new-type might be needed otherwise. + * + * @param processor The processor to add. + * @return {@code boolean} value representing addition success. + */ + public static boolean register(final InputProcessor processor) { + return Gdx.input instanceof final FluxInput self && self.addInputProcessor(processor); + } + + /** + * Checks input processor presence. + *

+ * Checks if a given processor is present and added through + * {@code register}. This method does not check against the main + * processor. + * + * @param processor The processor to check. + * @return {@code true} if present; {@code false} otherwise. + * @see #register + */ + public static boolean isRegistered(final InputProcessor processor) { + return Gdx.input instanceof final FluxInput self && self.containsInputProcessor(processor); + } + + /** + * Removes an input processor. + *

+ * Removes a given processor added through {@code register}. This method + * cannot remove the main processor, use + * {@code Input.setInputProcessor(null)}. + * + * @param processor The processor to remove. + * @return {@code boolean} value representing removal success. + * @see #register + * @see Input#setInputProcessor + */ + public static boolean unregister(final InputProcessor processor) { + return Gdx.input instanceof final FluxInput self && self.removeInputProcessor(processor); + } + + private InputProcessorHelper() {} +} diff --git a/src/main/java/dev/crmodders/flux/api/input/client/MouseProcessorEx.java b/src/main/java/dev/crmodders/flux/api/input/client/MouseProcessorEx.java new file mode 100644 index 0000000..badad60 --- /dev/null +++ b/src/main/java/dev/crmodders/flux/api/input/client/MouseProcessorEx.java @@ -0,0 +1,7 @@ +package dev.crmodders.flux.api.input.client; + +public interface MouseProcessorEx { + boolean mouseEntered(float screenX, float screenY); + + boolean mouseExited(float screenX, float screenY); +} diff --git a/src/main/java/dev/crmodders/flux/api/math/Intersection.java b/src/main/java/dev/crmodders/flux/api/math/Intersection.java new file mode 100644 index 0000000..43d7144 --- /dev/null +++ b/src/main/java/dev/crmodders/flux/api/math/Intersection.java @@ -0,0 +1,365 @@ +package dev.crmodders.flux.api.math; + +public final class Intersection { + public enum Axes { + NONE, X, Y, BOTH; + + public static final Axes ABSCISSA = X; + public static final Axes ORDINATE = Y; + + public static final Axes DOMAIN = X; + public static final Axes RANGE = Y; + + public static final Axes HORIZONTAL = X; + public static final Axes VERTICAL = Y; + + @Override + public String toString() { + return switch (this) { + case NONE -> "NONE"; + case X -> Axis.X.toString(); + case Y -> Axis.Y.toString(); + default -> "BOTH"; + }; + } + } + + public enum Axis { + X, Y; + + public static final Axis ABSCISSA = X; + public static final Axis ORDINATE = Y; + + public static final Axis DOMAIN = X; + public static final Axis RANGE = Y; + + public static final Axis HORIZONTAL = X; + public static final Axis VERTICAL = Y; + + @Override + public String toString() { + return switch (this) { + case X -> "X/ASBCISSA/DOMAIN/HORIZONTAL"; + case Y -> "Y/ORDINATE/RANGE/VERTICAL"; + }; + } + } + + public enum Convex { + NONE, ENTER, EXIT, BOTH; + + public int bufferStart() { + return this == EXIT ? 1 : 0; + } + + public int bufferSize() { + return switch (this) { + case NONE -> 0; + case ENTER, EXIT -> 1; + case BOTH -> 2; + }; + } + + public int componentCount() { + return 2 * this.bufferSize(); + } + + public int componentStart() { + return 2 * this.bufferStart(); + } + + public boolean entered() { + return switch (this) { + case NONE, EXIT -> false; + case ENTER, BOTH -> true; + }; + } + + public boolean exited() { + return switch (this) { + case NONE, ENTER -> false; + case EXIT, BOTH -> true; + }; + } + } + + /** + * Finds line-AABB intersection. + *

+ * This methods collects point intersections between a line segment and an AABB, + * storing into a buffer the point components and sorting it with the distance + * from the start point; there may be up to two intersections. It is assumed that + * moving left or up is negative while right or down is positive. + * + *

{@code
+     *        float[] buffer = new float[4];
+     *        int count = intersectLineToAabb(x1, y1, x2, y2, l, r, d, u, buffer);
+     *        switch (count) {
+     *            case 2:
+     *                float x1 = buffer[0];
+     *                float y1 = buffer[1];
+     *                float x2 = buffer[2];
+     *                float y2 = buffer[3];
+     *            case 1:
+     *                float x = buffer[0];
+     *                float y = buffer[1];
+     *            case 0:
+     *            default:
+     *                break;
+     *        }
+     * }
+ * + * @param x1 The line's starting point. + * @param y1 The line's starting point. + * @param x2 The line's ending point. + * @param y2 The line's ending point. + * @param l The left side of the AABB. + * @param r The right side of the AABB. + * @param d The top side of the AABB. + * @param u The bottom side of the AABB. + * @param buffer The output buffer. + * @return The number of intersections; between zero and two. + */ + public static Convex lineSegmentAndAabbUnchecked( + final float x1, + final float y1, + final float x2, + final float y2, + final float l, + final float r, + final float u, + final float d, + final float[] buffer, + final int offset + ) { + assert l <= r : "Expected left not greater than right"; + assert u <= d : "Expected up not greater than right"; + assert buffer != null : "Expected nonnull buffer"; + assert offset >= 0 : "Expected non-negative buffer offset"; + assert offset < buffer.length : "Expected offset less than buffer capacity"; + assert buffer.length >= 4 : "Expected buffer capacity no less than four"; + + // Java implementation of: https://www.desmos.com/calculator/vhtj9oiwm7 + + final float minX; + final float maxX; + final float a1; + final float a2; + + if (x1 <= x2) { + minX = Math.max(x1, a1 = l); + maxX = Math.min(x2, a2 = r); + } else { + minX = Math.max(x2, a2 = l); + maxX = Math.min(x1, a1 = r); + } + + final float minY; + final float maxY; + final float b1; + final float b2; + + if (y1 <= y2) { + minY = Math.max(y1, b1 = u); + maxY = Math.min(y2, b2 = d); + } else { + minY = Math.max(y2, b2 = u); + maxY = Math.min(y1, b1 = d); + } + + final var enter = + lineSegmentAndAxesUnchecked(x1, y1, x2, y2, minX, minY, maxX, maxY, a1, b1, buffer, offset) + != Axes.NONE; + final var exit = + lineSegmentAndAxesUnchecked(x2, y2, x1, y1, minX, minY, maxX, maxY, a2, b2, buffer, offset + 2) + != Axes.NONE; + + if (enter && exit) { + return Convex.BOTH; + } else if (exit) { + return Convex.EXIT; + } else if (enter) { + return Convex.ENTER; + } else { + return Convex.NONE; + } + } + + /** + * Line segment and axes intersection. + *

+ * Collects the point intersection of a line segment and two perpendicular + * axes, if any. When the line coincides with the + * + * @param x1 The line starting x-component. + * @param y1 The line starting y-component. + * @param x2 The line ending x-component. + * @param y2 The line ending y-component. + * @param minX The minimum returned x-component. + * @param minY The minimum returned y-component. + * @param maxX The maximum returned x-component. + * @param maxY The maximum returned y-component. + * @param a The x-position of the vertical line. + * @param b The y-position of the horizontal line. + * @param buffer The output buffer. + * @param offset The output starting offset. + * @return The intersection result; amount of intersections. + */ + public static Axes lineSegmentAndAxesUnchecked( + final float x1, + final float y1, + final float x2, + final float y2, + final float minX, + final float minY, + final float maxX, + final float maxY, + final float a, + final float b, + final float[] buffer, + final int offset + ) { + // This is unchecked after all, only present with `-ea` jvm arg + // Avoiding `&&` here might be more helpful when debugging... + assert minX <= maxX : "Expected min x no greater than max x"; + assert minY <= maxY : "Expected min y no greater than max y"; + assert buffer != null : "Expected nonnull buffer"; + assert offset >= 0 : "Expected non-negative buffer offset"; + assert offset < buffer.length : "Expected offset less than buffer capacity"; + assert buffer.length >= 2 : "Expected buffer capacity no less than two"; + + vertical: + if (minX <= a && a <= maxX) { + final float c; + + if (x1 == a && minX <= y1 && y1 <= maxX) { + c = y1; + } else if (x1 == x2) { + break vertical; + } else { + c = lineAndVerticalUnchecked(x1, y1, x2, y2, a); + if (c < minY || c > maxY) { + break vertical; + } + } + + buffer[offset] = a; + buffer[offset + 1] = c; + + return c == b ? Axes.BOTH : Axes.VERTICAL; + } + + horizontal: + if (minY <= b && b <= maxY) { + final float c; + + if (y1 == b && minY <= y1 && y1 <= maxY) { + c = x1; + } else if (y1 == y2) { + break horizontal; + } else { + c = lineAndHorizontalUnchecked(x1, y1, x2, y2, b); + if (c < minX || c > maxX) { + break horizontal; + } + } + + buffer[offset] = c; + buffer[offset + 1] = b; + + return c == a ? Axes.BOTH : Axes.HORIZONTAL; + } + + return Axes.NONE; + } + + /** + * Line and horizontal intersection. + * + * @param x1 Line first point x-component. + * @param y1 Line first point y-component. + * @param x2 Line second point x-component. + * @param y2 Line second point y-component. + * @param y The y-position of the horizontal line. + * @return The {@code y}-intercept. + * @implSpec

{@code
+     *     Intersection.lineAndAxisUnchecked(x1, x2, y1, y2, y);
+     * }
+ * @see #lineAndAxisUnchecked + */ + public static float lineAndHorizontalUnchecked( + final float x1, + final float y1, + final float x2, + final float y2, + final float y + ) { + return Intersection.lineAndAxisUnchecked(x1, x2, y1, y2, y); + } + + /** + * Line and vertical intersection. + * + * @param x1 Line first point x-component. + * @param y1 Line first point y-component. + * @param x2 Line second point x-component. + * @param y2 Line second point y-component. + * @param x The x-position of the vertical line. + * @return The {@code y}-intercept. + * @implSpec
{@code
+     *     Intersection.lineAndAxisUnchecked(y1, y2, x1, x2, x);
+     * }
+ * @see #lineAndAxisUnchecked + */ + public static float lineAndVerticalUnchecked( + final float x1, + final float y1, + final float x2, + final float y2, + final float x + ) { + return Intersection.lineAndAxisUnchecked(y1, y2, x1, x2, x); + } + + /** + * Line and axis intersection. + *

+ * This method generalizes the computation for the intersection of a line + * and a vertical or horizontal line. Input validation should be done by the + * caller with all values are expected to be finite, the output value is + * not defined otherwise. Additionally, on {@code g1 == g2}, this will + * return {@code Float.NaN}, and usually means that the given line parallels + * the axis. + *

+ * For a line segment, check the output, for example, + * {@code t1 <= t && t <= t2}, given that, {@code t1 <= t2}. + * + * @param t1 The line target component start. + * @param t2 The line target component end. + * @param g1 The line given component start. + * @param g2 The line given component end. + * @param g The given value of the axis. + * @return The value of {@code t} or the intercept to the axis. + * @implSpec

{@code
+     *     (g2 - g1) * (t - t1) == (t2 - t1) * (g - g1)
+     *     // Division Property of Equality
+     *     t - t1  == (t2 - t1) * (g - g1) / (g2 - g1)
+     *     // Subtraction Property of Equality
+     *     t == (t2 - t1) * (g - g1) / (g2 - g1) + t1
+     * }
+ * @see #lineAndHorizontalUnchecked + * @see #lineAndVerticalUnchecked + * @see Float#NaN + */ + public static float lineAndAxisUnchecked( + final float t1, + final float t2, + final float g1, + final float g2, + final float g + ) { + return (t2 - t1) * (g - g1) / (g2 - g1) + t1; + } + + private Intersection() {} +} diff --git a/src/main/java/dev/crmodders/flux/impl/resource/loader/FluxFileHandle.java b/src/main/java/dev/crmodders/flux/api/resource/loader/FluxFileHandle.java similarity index 99% rename from src/main/java/dev/crmodders/flux/impl/resource/loader/FluxFileHandle.java rename to src/main/java/dev/crmodders/flux/api/resource/loader/FluxFileHandle.java index a1ab78e..521ef0d 100644 --- a/src/main/java/dev/crmodders/flux/impl/resource/loader/FluxFileHandle.java +++ b/src/main/java/dev/crmodders/flux/api/resource/loader/FluxFileHandle.java @@ -1,4 +1,4 @@ -package dev.crmodders.flux.impl.resource.loader; +package dev.crmodders.flux.api.resource.loader; import com.badlogic.gdx.Files.FileType; import com.badlogic.gdx.files.FileHandle; diff --git a/src/main/java/dev/crmodders/flux/api/ui/components/client/AbstractUIObject.java b/src/main/java/dev/crmodders/flux/api/ui/components/client/AbstractUIObject.java deleted file mode 100644 index da45098..0000000 --- a/src/main/java/dev/crmodders/flux/api/ui/components/client/AbstractUIObject.java +++ /dev/null @@ -1,378 +0,0 @@ -package dev.crmodders.flux.api.ui.components.client; - -import com.badlogic.gdx.Gdx; -import com.badlogic.gdx.graphics.Color; -import com.badlogic.gdx.graphics.Texture; -import com.badlogic.gdx.graphics.g2d.SpriteBatch; -import com.badlogic.gdx.graphics.g2d.TextureRegion; -import com.badlogic.gdx.math.Rectangle; -import com.badlogic.gdx.math.Vector2; -import com.badlogic.gdx.utils.viewport.Viewport; -import finalforeach.cosmicreach.audio.SoundManager; -import finalforeach.cosmicreach.ui.*; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.Nullable; - -import java.util.Objects; - -import static finalforeach.cosmicreach.ui.UIElement.*; -import static finalforeach.cosmicreach.ui.UIElement.uiPanelHoverBoundsTex; - -@ApiStatus.Experimental -public class AbstractUIObject implements UIObject { - public static Color defaultTextColor() { - final var self = new Color(); - - // SAFETY: sets the value to WHITE, without clamping - self.a = 1.0f; - self.r = 1.0f; - self.g = 1.0f; - self.b = 1.0f; - - return self; - } - - /////////////////////////////// - // SIZE AND POSITIONING - /////////////////////////////// - - /** - * Stores the anchor position and size. - */ - private final Rectangle bounds; - - @Override - public void setX(final float x) { - this.bounds.x = x; - } - - @Override - public void setY(final float y) { - this.bounds.y = y; - } - - @Override - public float getWidth() { - return this.bounds.width; - } - - @Override - public float getHeight() { - return this.bounds.height; - } - - private HorizontalAnchor horizontalAnchor; - - public HorizontalAnchor getHorizontalAnchor() { - return this.horizontalAnchor; - } - - public void setHorizontalAnchor(final HorizontalAnchor horizontalAnchor) { - Objects.requireNonNull(horizontalAnchor); - this.horizontalAnchor = horizontalAnchor; - } - - public void alignToLeft() { - this.horizontalAnchor = HorizontalAnchor.LEFT_ALIGNED; - } - - public void centerHorizontally() { - this.horizontalAnchor = HorizontalAnchor.CENTERED; - } - - public void alignToRight() { - this.horizontalAnchor = HorizontalAnchor.RIGHT_ALIGNED; - } - - private VerticalAnchor verticalAnchor; - - public VerticalAnchor getVerticalAnchor() { - return this.verticalAnchor; - } - - public void setVerticalAnchor(final VerticalAnchor verticalAnchor) { - Objects.requireNonNull(verticalAnchor); - this.verticalAnchor = verticalAnchor; - } - - public void alignToTop() { - this.verticalAnchor = VerticalAnchor.TOP_ALIGNED; - } - - public void centerVertically() { - this.verticalAnchor = VerticalAnchor.CENTERED; - } - - public void alignToBottom() { - this.verticalAnchor = VerticalAnchor.BOTTOM_ALIGNED; - } - - /** - * The main color, usually used to tint text. - */ - private final Color color; - - /** - * Buffer used when dealing with the {@code ScissorStack}. - */ - private final Rectangle scissors; - - private final Vector2 tmpVec; - - private boolean active; - - private Texture buttonTexture; - - private boolean disabled; - - private boolean hovered; - - private @Nullable String text; - - private boolean visible; - - public AbstractUIObject() { - System.out.println(Gdx.input.getInputProcessor()); - this.bounds = new Rectangle(); - this.horizontalAnchor = HorizontalAnchor.CENTERED; - this.color = AbstractUIObject.defaultTextColor(); - this.scissors = new Rectangle(); - this.tmpVec = new Vector2(); - this.verticalAnchor = VerticalAnchor.CENTERED; - } - - public void onCreate() { - } - - public void onClick() { - } - - public void onMouseDown() { - } - - public void onMouseUp() { - } - - public boolean isHoveredOver(Viewport viewport, float x, float y) { - float dx = this.getDisplayX(viewport); - float dy = this.getDisplayY(viewport); - return x >= dx && y >= dy && x < dx + this.bounds.width && y < dy + this.bounds.height; - } - - protected float getDisplayX(final Viewport viewport) { - final var x = this.bounds.x; - - return switch (this.horizontalAnchor) { - case LEFT_ALIGNED -> x - viewport.getWorldWidth() / 2.0F; - case RIGHT_ALIGNED -> x + viewport.getWorldWidth() / 2.0F - this.bounds.width; - default -> x - this.bounds.width / 2.0F; - }; - } - - protected float getDisplayY(final Viewport viewport) { - final var y = this.bounds.y; - - return switch (this.verticalAnchor) { - case TOP_ALIGNED -> y - viewport.getWorldHeight() / 2.0F; - case BOTTOM_ALIGNED -> y + viewport.getWorldHeight() / 2.0F - this.bounds.height; - default -> y - this.bounds.height / 2.0F; - }; - } - - @Override - public void drawBackground( - final Viewport viewport, - final SpriteBatch batch, - final float mouseX, - final float mouseY - ) { - if (this.visible) { - this.buttonTexture = uiPanelTex; - if (Gdx.input.isButtonJustPressed(0) && Gdx.input.isButtonPressed(0)) { - this.active = true; - } - - if (this.active && !Gdx.input.isButtonPressed(0)) { - this.active = false; - this.onMouseUp(); - } - - if (this.isHoveredOver(viewport, mouseX, mouseY)) { - if (!this.hovered) { - SoundManager.INSTANCE.playSound(onHoverSound); - this.hovered = true; - } - - if (Gdx.input.isButtonJustPressed(0)) { - this.onMouseDown(); - } - - if (Gdx.input.isButtonJustPressed(0)) { - this.buttonTexture = uiPanelPressedTex; - } - } else { - this.hovered = false; - if (Gdx.input.isButtonJustPressed(0) && !Gdx.input.isButtonPressed(0)) { - this.active = false; - } - } - - this.drawElementBackground(viewport, batch); - if (Gdx.input.isButtonJustPressed(0) && !Gdx.input.isButtonPressed(0)) { - this.buttonTexture = uiPanelPressedTex; - this.onClick(); - SoundManager.INSTANCE.playSound(onClickSound); - } - - } - } - - private void drawElementBackground( - final Viewport viewport, - final SpriteBatch batch - ) { - float x = this.getDisplayX(viewport); - float y = this.getDisplayY(viewport); - if (!this.active && (!this.hovered || currentlyHeldElement != null)) { - batch.draw(uiPanelBoundsTex, x, y, 0.0F, 0.0F, this.bounds.width, this.bounds.height, 1.0F, 1.0F, 0.0F, 0, 0, this.buttonTexture.getWidth(), this.buttonTexture.getHeight(), false, true); - } else { - batch.draw(uiPanelHoverBoundsTex, x, y, 0.0F, 0.0F, this.bounds.width, this.bounds.height, 1.0F, 1.0F, 0.0F, 0, 0, this.buttonTexture.getWidth(), this.buttonTexture.getHeight(), false, true); - } - - batch.draw(this.buttonTexture, x + 1.0F, y + 1.0F, 1.0F, 1.0F, this.bounds.width - 2.0F, this.bounds.height - 2.0F, 1.0F, 1.0F, 0.0F, 0, 0, this.buttonTexture.getWidth(), this.buttonTexture.getHeight(), false, true); - } - - @Override - public void drawText(final Viewport viewport, final SpriteBatch batch) { - if (this.visible && this.text != null && !this.text.isEmpty()) { - float x = this.getDisplayX(viewport); - float y = this.getDisplayY(viewport); - FontRenderer.getTextDimensions(viewport, this.text, this.tmpVec); - if (this.tmpVec.x > this.bounds.width) { - FontRenderer.drawTextbox(batch, viewport, this.text, x, y, this.bounds.width); - } else { - float maxX = x; - float maxY = y; - - for(int i = 0; i < this.text.length(); ++i) { - char c = this.text.charAt(i); - FontTexture f = FontRenderer.getFontTexOfChar(c); - if (f == null) { - c = '?'; - f = FontRenderer.getFontTexOfChar(c); - } - - TextureRegion texReg = f.getTexRegForChar(c); - x -= f.getCharStartPos(c).x % (float)texReg.getRegionWidth(); - switch (c) { - case '\n': - y += (float)texReg.getRegionHeight(); - x = this.bounds.x; - maxX = Math.max(maxX, x); - maxY = Math.max(maxY, y); - break; - case ' ': - x += f.getCharSize(c).x / 4.0F; - maxX = Math.max(maxX, x); - break; - default: - x += f.getCharSize(c).x + f.getCharStartPos(c).x % (float)texReg.getRegionWidth() + 2.0F; - maxX = Math.max(maxX, x); - maxY = Math.max(maxY, y + (float)texReg.getRegionHeight()); - } - } - - x = this.getDisplayX(viewport); - y = this.getDisplayY(viewport); - x += this.bounds.width / 2.0F - (maxX - x) / 2.0F; - y += this.bounds.height / 2.0F - (maxY - y) / 2.0F; - - final var oldColor = new Color(batch.getColor()); - batch.setColor(this.color); - FontRenderer.drawText(batch, viewport, this.text, x, y); - batch.setColor(oldColor); - } - } - } - - public String getText() { - return this.text; - } - - public void setText(final String text) { - this.text = text; - } - - @Override - public void updateText() { - - } - - @Override - public void show() { - this.visible = true; - } - - @Override - public void hide() { - this.visible = false; - } - - @Override - public void deactivate() { - - } - - @Override - public boolean keyDown(int i) { - return false; - } - - @Override - public boolean keyUp(int i) { - return false; - } - - @Override - public boolean keyTyped(char c) { - return false; - } - - @Override - public boolean touchDown(int i, int i1, int i2, int i3) { - return false; - } - - @Override - public boolean touchUp(int i, int i1, int i2, int i3) { - return false; - } - - @Override - public boolean touchCancelled(int i, int i1, int i2, int i3) { - return false; - } - - @Override - public boolean touchDragged(int i, int i1, int i2) { - return false; - } - - @Override - public boolean mouseMoved(int screenX, int screenY) { - return false; - } - - @Override - public boolean scrolled(float v, float v1) { - return false; - } - - public void setSize(final float width, final float height) { - this.bounds.setSize(width, height); - } - - public boolean isVisible() { - return this.visible; - } -} diff --git a/src/main/java/dev/crmodders/flux/api/ui/components/client/Component.java b/src/main/java/dev/crmodders/flux/api/ui/components/client/Component.java new file mode 100644 index 0000000..9ac21bd --- /dev/null +++ b/src/main/java/dev/crmodders/flux/api/ui/components/client/Component.java @@ -0,0 +1,656 @@ +package dev.crmodders.flux.api.ui.components.client; + +import static finalforeach.cosmicreach.ui.UIElement.*; + +import com.badlogic.gdx.Input; +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.math.Rectangle; +import com.badlogic.gdx.math.Vector2; +import com.badlogic.gdx.utils.viewport.Viewport; +import dev.crmodders.flux.api.input.client.InputAdapterEx; +import dev.crmodders.flux.api.input.client.MouseProcessorEx; +import dev.crmodders.flux.api.math.Intersection; +import finalforeach.cosmicreach.audio.SoundManager; +import finalforeach.cosmicreach.ui.FontRenderer; +import finalforeach.cosmicreach.ui.HorizontalAnchor; +import finalforeach.cosmicreach.ui.UIObject; +import finalforeach.cosmicreach.ui.VerticalAnchor; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Experimental +public class Component implements MouseProcessorEx, UIObject { + public static Color defaultTextColor() { + final var self = new Color(); + + // SAFETY: sets the value to WHITE, without clamping + self.a = 1.0f; + self.r = 1.0f; + self.g = 1.0f; + self.b = 1.0f; + + return self; + } + + /////////////////////////////// + // SIZE AND POSITIONING + /////////////////////////////// + + /** + * Stores the anchor position and size. + */ + private final Rectangle bounds; + + @Override + public void setX(final float x) { + this.bounds.x = x; + } + + @Override + public void setY(final float y) { + this.bounds.y = y; + } + + @Override + public float getWidth() { + return this.bounds.width; + } + + @Override + public float getHeight() { + return this.bounds.height; + } + + private HorizontalAnchor horizontalAnchor; + + public HorizontalAnchor getHorizontalAnchor() { + return this.horizontalAnchor; + } + + public void setHorizontalAnchor(final HorizontalAnchor horizontalAnchor) { + Objects.requireNonNull(horizontalAnchor); + this.horizontalAnchor = horizontalAnchor; + } + + public void alignToLeft() { + this.horizontalAnchor = HorizontalAnchor.LEFT_ALIGNED; + } + + public void centerHorizontally() { + this.horizontalAnchor = HorizontalAnchor.CENTERED; + } + + public void alignToRight() { + this.horizontalAnchor = HorizontalAnchor.RIGHT_ALIGNED; + } + + private VerticalAnchor verticalAnchor; + + public VerticalAnchor getVerticalAnchor() { + return this.verticalAnchor; + } + + public void setVerticalAnchor(final VerticalAnchor verticalAnchor) { + Objects.requireNonNull(verticalAnchor); + this.verticalAnchor = verticalAnchor; + } + + public void alignToTop() { + this.verticalAnchor = VerticalAnchor.TOP_ALIGNED; + } + + public void centerVertically() { + this.verticalAnchor = VerticalAnchor.CENTERED; + } + + public void alignToBottom() { + this.verticalAnchor = VerticalAnchor.BOTTOM_ALIGNED; + } + + /** + * The main color, usually used to tint text. + */ + private final Color color; + + /** + * Buffer used when dealing with the {@code ScissorStack}. + */ + private final Rectangle scissors; + + private final Vector2 tmpVec; + + private boolean active; + + private final List children; + + private final List childrenView; + + private boolean disabled; + + private int focusedChildIndex; + + private boolean hovered; + + private int hoveredChildIndex; + + private final ComponentInputAdapter inputProcessor; + + private final float[] mouseEnterExit; + + private int prevMouseX; + + private int prevMouseY; + + private @Nullable String text; + + private final Viewport viewport; + + private boolean visible; + + public Component(final Viewport viewport) { + this.bounds = new Rectangle(); + this.children = new ArrayList<>(); + this.childrenView = Collections.unmodifiableList(this.children); + this.color = Component.defaultTextColor(); + this.focusedChildIndex = -1; + this.horizontalAnchor = HorizontalAnchor.CENTERED; + this.inputProcessor = new ComponentInputAdapter(); + this.mouseEnterExit = new float[Intersection.Convex.BOTH.bufferSize()]; + this.prevMouseX = -1; + this.prevMouseY = -1; + this.scissors = new Rectangle(); + this.tmpVec = new Vector2(); + this.verticalAnchor = VerticalAnchor.CENTERED; + this.viewport = viewport; + } + + public void addChild(final Component component) { + this.children.add(component); + } + + public void insertChild(final Component component, final int index) { + this.children.add(index, component); + + final var curr = this.focusedChildIndex; + if (curr >= index) { + this.focusNextChildFrom(curr); + } + } + + public Component removeChildAt(final int index) { + final var component = this.children.remove(index); + + final var curr = this.focusedChildIndex; + if (curr == index) { + this.focusedChildIndex = -1; + } else if (curr > index) { + this.focusPrevChildFrom(curr); + } + + return component; + } + + public void clearChildren() { + this.focusedChildIndex = -1; + this.children.clear(); + } + + public final List getChildrenView() { + return this.childrenView; + } + + protected void focusNextChildFrom(int index) { + if (++index >= this.children.size()) { + index = -1; + } + this.focusedChildIndex = index; + } + + protected void focusPrevChildFrom(int index) { + if (index < 0) { + index = this.children.size(); + } + this.focusedChildIndex = index - 1; + } + + public boolean isHoveredOver(Viewport viewport, float x, float y) { + float dx = this.getDisplayX(viewport); + float dy = this.getDisplayY(viewport); + return x >= dx && y >= dy && x < dx + this.bounds.width && y < dy + this.bounds.height; + } + + protected float getDisplayX(final Viewport viewport) { + final var x = this.bounds.x; + + return switch (this.horizontalAnchor) { + case LEFT_ALIGNED -> x - viewport.getWorldWidth() / 2.0F; + case RIGHT_ALIGNED -> x + viewport.getWorldWidth() / 2.0F - this.bounds.width; + default -> x - this.bounds.width / 2.0F; + }; + } + + protected float getDisplayY(final Viewport viewport) { + final var y = this.bounds.y; + + return switch (this.verticalAnchor) { + case TOP_ALIGNED -> y - viewport.getWorldHeight() / 2.0F; + case BOTTOM_ALIGNED -> y + viewport.getWorldHeight() / 2.0F - this.bounds.height; + default -> y - this.bounds.height / 2.0F; + }; + } + + protected float getDisplayX2(final Viewport viewport) { + final var x = this.bounds.x + this.bounds.width; + + return switch (this.horizontalAnchor) { + case LEFT_ALIGNED -> x - viewport.getWorldWidth() / 2.0F; + case RIGHT_ALIGNED -> x + viewport.getWorldWidth() / 2.0F - this.bounds.width; + default -> x - this.bounds.width / 2.0F; + }; + } + + protected float getDisplayY2(final Viewport viewport) { + final var y = this.bounds.y + this.bounds.height; + + return switch (this.verticalAnchor) { + case TOP_ALIGNED -> y - viewport.getWorldHeight() / 2.0F; + case BOTTOM_ALIGNED -> y + viewport.getWorldHeight() / 2.0F - this.bounds.height; + default -> y - this.bounds.height / 2.0F; + }; + } + + @Override + public void drawBackground( + final Viewport viewport, + final SpriteBatch batch, + final float mouseX, + final float mouseY + ) { + if (!this.visible) { + return; + } + + // Cosmic Reach currently only has hover, no focusing, especially tab + // focusing, and thus the lack of a dedicated texture. + final var boundsTexture = + this.active || this.hovered ? uiPanelHoverBoundsTex : uiPanelBoundsTex; + final var buttonTexture = this.active ? uiPanelPressedTex : uiPanelTex; + final var x = this.getDisplayX(viewport); + final var y = this.getDisplayY(viewport); + + batch.draw(boundsTexture, x, y, 0.0F, 0.0F, this.bounds.width, this.bounds.height, 1.0F, 1.0F, 0.0F, 0, 0, buttonTexture.getWidth(), buttonTexture.getHeight(), false, true); + batch.draw(buttonTexture, x + 1.0F, y + 1.0F, 1.0F, 1.0F, this.bounds.width - 2.0F, this.bounds.height - 2.0F, 1.0F, 1.0F, 0.0F, 0, 0, buttonTexture.getWidth(), buttonTexture.getHeight(), false, true); + } + + @Override + public void drawText(final Viewport viewport, final SpriteBatch batch) { + final var text = this.text; + + if (!this.visible || text == null || text.isEmpty()) { + return; + } + + var x = this.getDisplayX(viewport); + var y = this.getDisplayY(viewport); + + FontRenderer.getTextDimensions(viewport, text, this.tmpVec); + + if (this.tmpVec.x > this.bounds.width) { + FontRenderer.drawTextbox(batch, viewport, text, x, y, this.bounds.width); + return; + } + + var maxX = x; + var maxY = y; + + for (var i = 0; i < text.length(); ++i) { + char c = text.charAt(i); + + var f = FontRenderer.getFontTexOfChar(c); + + if (f == null) { + c = '?'; + f = FontRenderer.getFontTexOfChar(c); + } + + final var texReg = f.getTexRegForChar(c); + x -= f.getCharStartPos(c).x % (float) texReg.getRegionWidth(); + + switch (c) { + case '\n' -> { + y += (float) texReg.getRegionHeight(); + x = this.bounds.x; + maxX = Math.max(maxX, x); + maxY = Math.max(maxY, y); + } + case ' ' -> { + x += f.getCharSize(c).x / 4.0F; + maxX = Math.max(maxX, x); + } + default -> { + x += f.getCharSize(c).x + f.getCharStartPos(c).x % (float) texReg.getRegionWidth() + 2.0F; + maxX = Math.max(maxX, x); + maxY = Math.max(maxY, y + (float) texReg.getRegionHeight()); + } + } + } + + x = this.getDisplayX(viewport); + y = this.getDisplayY(viewport); + x += this.bounds.width / 2.0F - (maxX - x) / 2.0F; + y += this.bounds.height / 2.0F - (maxY - y) / 2.0F; + + final var oldColor = new Color(batch.getColor()); + batch.setColor(this.color); + FontRenderer.drawText(batch, viewport, text, x, y); + batch.setColor(oldColor); + } + + public String getText() { + return this.text; + } + + public void setText(final @Nullable String text) { + this.text = text; + } + + public @Nullable Component getChildAt(final int index) { + return 0 <= index && index < this.children.size() ? this.children.get(index) : null; + } + + public int getFocusedChildIndex() { + return this.focusedChildIndex; + } + + public @Nullable Component getFocusedChild() { + return this.getChildAt(this.focusedChildIndex); + } + + public int getHoveredChildIndex() { + return this.hoveredChildIndex; + } + + public @Nullable Component getHoveredChild() { + return this.getChildAt(this.hoveredChildIndex); + } + + @Override + public void updateText() { + + } + + @Override + public void show() { + this.visible = true; + } + + @Override + public void hide() { + this.visible = false; + } + + @Override + public void deactivate() { + + } + + @Override + public boolean keyDown(int keycode) { + return this.inputProcessor.keyDown(keycode); + } + + @Override + public boolean keyUp(int keycode) { + return this.inputProcessor.keyUp(keycode); + } + + @Override + public boolean keyTyped(char character) { + return this.inputProcessor.keyTyped(character); + } + + @Override + public boolean touchDown(int screenX, int screenY, int pointer, int button) { + var index = -1; + + for (var i = 0; i < this.children.size(); i++) { + final var child = this.children.get(i); + + if (child != null && child.isHoveredOver(this.viewport, screenX, screenY)) { + child.mouseEntered(screenX, screenY); + index = i; + } + } + + final var focused = this.getFocusedChild(); + if (focused != null) { + focused.active = false; + } + + final var child = this.children.get(index); + if (child != null) { + child.active = true; + child.hovered = true; + } + + this.hoveredChildIndex = index; + + if (this.inputProcessor.touchDown(screenX, screenY, pointer, button)) { + SoundManager.INSTANCE.playSound(onClickSound); + return true; + } else { + return false; + } + } + + @Override + public boolean touchUp(int screenX, int screenY, int pointer, int button) { + final var child = this.getHoveredChild(); + if (child != null) { + child.mouseExited(screenX, screenY); + child.hovered = false; + } + this.hoveredChildIndex = 0; + return this.inputProcessor.touchUp(screenX, screenY, pointer, button); + } + + @Override + public boolean touchCancelled(int screenX, int screenY, int pointer, int button) { + final var child = this.getHoveredChild(); + if (child != null) { + child.mouseExited(screenX, screenY); + child.hovered = false; + } + this.hoveredChildIndex = 0; + return this.inputProcessor.touchCancelled(screenX, screenY, pointer, button); + } + + @Override + public boolean touchDragged(int screenX, int screenY, int pointer) { + this.handleMouseMoved(screenX, screenY); + return this.inputProcessor.touchDragged(screenX, screenY, pointer); + } + + @Override + public boolean mouseMoved(int screenX, int screenY) { + this.handleMouseMoved(screenX, screenY); + return this.inputProcessor.mouseMoved(screenX, screenY); + } + + protected void handleMouseMoved(final int screenX, final int screenY) { + final var prevMouseX = this.prevMouseX; + final var prevMouseY = this.prevMouseY; + + var index = -1; + + for (var i = 0; i < this.children.size(); i++) { + final var child = this.children.get(i); + + final var axes = Intersection.lineSegmentAndAabbUnchecked( + prevMouseX, + prevMouseY, + screenX, + screenY, + this.getDisplayX(this.viewport), + this.getDisplayY(this.viewport), + this.getDisplayX2(this.viewport), + this.getDisplayY2(this.viewport), + this.mouseEnterExit, + 0 + ); + + // Preserves last hovered when a child is entered-exited in one event + final var prev = index; + + if (axes.entered()) { + child.mouseEntered(this.mouseEnterExit[0], this.mouseEnterExit[1]); + index = i; + } + + if (axes.exited()) { + child.mouseExited(this.mouseEnterExit[2], this.mouseEnterExit[3]); + child.hovered = false; + index = prev; + } + } + + final var child = this.children.get(index); + if (child != null) { + child.hovered = true; + } + + this.hoveredChildIndex = index; + this.prevMouseX = screenX; + this.prevMouseY = screenY; + } + + @Override + public boolean scrolled(float amountX, float amountY) { + return this.inputProcessor.scrolled(amountX, amountY); + } + + public void setSize(final float width, final float height) { + this.bounds.setSize(width, height); + } + + public boolean isVisible() { + return this.visible; + } + + @Override + public boolean mouseEntered(final float screenX, final float screenY) { + return true; + } + + @Override + public boolean mouseExited(final float screenX, final float screenY) { + return true; + } + + private final class ComponentInputAdapter extends InputAdapterEx { + @Override + protected boolean keyDownEx(final int keycode, final int modifiers) { + final var index = Component.this.focusedChildIndex; + final var child = Component.this.getChildAt(index); + if (child != null && child.keyDown(keycode)) { + return false; + } + if (keycode != Input.Keys.TAB) { + return true; + } + if (modifiers == 0) { + Component.this.focusNextChildFrom(index); + return false; + } + if (Modifiers.isShiftOnly(modifiers)) { + Component.this.focusPrevChildFrom(index); + return false; + } + return false; + } + + @Override + protected boolean keyUpEx(final int keycode, final int modifiers) { + final var child = Component.this.getFocusedChild(); + return child == null || child.keyUp(keycode); + } + + @Override + protected boolean touchDownEx( + final int screenX, + final int screenY, + final int pointer, + final int button, + final int modifiers + ) { + final var child = Component.this.getFocusedChild(); + return child == null || child.touchDown(screenX, screenY, pointer, button); + } + + @Override + protected boolean touchUpEx( + final int screenX, + final int screenY, + final int pointer, + final int button, + final int modifiers + ) { + final var child = Component.this.getFocusedChild(); + return child == null || child.touchUp(screenX, screenY, pointer, button); + } + + @Override + protected boolean touchCancelledEx( + final int screenX, + final int screenY, + final int pointer, + final int button, + final int modifiers + ) { + final var child = Component.this.getFocusedChild(); + return child == null || child.touchCancelled(screenX, screenY, pointer, button); + } + + @Override + protected boolean touchDraggedEx( + final int screenX, + final int screenY, + final int pointer, + final int modifiers + ) { + final var child = Component.this.getFocusedChild(); + return child == null || child.touchDragged(screenX, screenY, pointer); + } + + @Override + protected boolean mouseMovedEx( + final int screenX, + final int screenY, + final int modifiers + ) { + final var child = Component.this.getFocusedChild(); + return child == null || child.mouseMoved(screenX, screenY); + } + + @Override + protected boolean scrolledEx( + final float amountX, + final float amountY, + final int modifiers + ) { + final var child = Component.this.getHoveredChild(); + return child == null || child.scrolled(amountX, amountY); + } + + @Override + public boolean keyTyped(final char character) { + final var child = Component.this.getFocusedChild(); + return child == null || child.keyTyped(character); + } + } +} diff --git a/src/main/java/dev/crmodders/flux/impl/base/Constants.java b/src/main/java/dev/crmodders/flux/impl/base/Constants.java deleted file mode 100644 index 3357b70..0000000 --- a/src/main/java/dev/crmodders/flux/impl/base/Constants.java +++ /dev/null @@ -1,9 +0,0 @@ -package dev.crmodders.flux.impl.base; - -public final class Constants { - public static final String NAME = "Flux API"; - - public static final String NAMESPACE = "flux-api"; - - private Constants() {} -} diff --git a/src/main/java/dev/crmodders/flux/impl/base/Logging.java b/src/main/java/dev/crmodders/flux/impl/base/Logging.java deleted file mode 100644 index 95d43ed..0000000 --- a/src/main/java/dev/crmodders/flux/impl/base/Logging.java +++ /dev/null @@ -1,8 +0,0 @@ -package dev.crmodders.flux.impl.base; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public final class Logging { - public static final Logger LOGGER = LoggerFactory.getLogger(Constants.NAME); -} diff --git a/src/main/java/dev/crmodders/flux/impl/base/Strings.java b/src/main/java/dev/crmodders/flux/impl/base/Strings.java deleted file mode 100644 index 128220c..0000000 --- a/src/main/java/dev/crmodders/flux/impl/base/Strings.java +++ /dev/null @@ -1,9 +0,0 @@ -package dev.crmodders.flux.impl.base; - -public final class Strings { - public static boolean endsWithIgnoreCase(final String self, final String rhs) { - return self.regionMatches(true, self.length() - rhs.length(), rhs, 0, rhs.length()); - } - - private Strings() {} -} diff --git a/src/main/java/dev/crmodders/flux/impl/input/client/FluxApiInputClientEntrypoint.java b/src/main/java/dev/crmodders/flux/impl/input/client/FluxApiInputClientEntrypoint.java new file mode 100644 index 0000000..9b62c76 --- /dev/null +++ b/src/main/java/dev/crmodders/flux/impl/input/client/FluxApiInputClientEntrypoint.java @@ -0,0 +1,12 @@ +package dev.crmodders.flux.impl.input.client; + +import com.badlogic.gdx.Gdx; +import dev.crmodders.cosmicquilt.api.entrypoint.client.ClientModInitializer; +import org.quiltmc.loader.api.ModContainer; + +public final class FluxApiInputClientEntrypoint implements ClientModInitializer { + @Override + public void onInitializeClient(final ModContainer modContainer) { + Gdx.input = new FluxInput(Gdx.input); + } +} diff --git a/src/main/java/dev/crmodders/flux/impl/input/client/FluxInput.java b/src/main/java/dev/crmodders/flux/impl/input/client/FluxInput.java new file mode 100644 index 0000000..587351a --- /dev/null +++ b/src/main/java/dev/crmodders/flux/impl/input/client/FluxInput.java @@ -0,0 +1,337 @@ +package dev.crmodders.flux.impl.input.client; + +import com.badlogic.gdx.Input; +import com.badlogic.gdx.InputProcessor; + +import java.util.Objects; + +public final class FluxInput implements Input { + private final Input delegate; + + private final FluxInputProcessor inputProcessor; + + public FluxInput(final Input delegate) { + Objects.requireNonNull(delegate); + + this.delegate = delegate; + this.inputProcessor = new FluxInputProcessor(); + + this.inputProcessor.setMainProcessor(this.delegate.getInputProcessor()); + this.delegate.setInputProcessor(this.inputProcessor); + } + + /** + * Adds an input processor. + * + * @param processor The processor to add. + * @return {@code boolean} value representing addition success. + * @see FluxInputProcessor#addProcessor + */ + public boolean addInputProcessor(final InputProcessor processor) { + return this.inputProcessor.addProcessor(processor); + } + + /** + * Checks presence of input processor. + * + * @param processor The processor to check. + * @return {@code true} if present; {@code false} otherwise. + * @see FluxInputProcessor#containsProcessor + */ + public boolean containsInputProcessor(final InputProcessor processor) { + return this.inputProcessor.containsProcessor(processor); + } + + /** + * Removes an input processor. + * + * @param processor The processor to remove. + * @return {@code boolean} value representing removal success. + * @see FluxInputProcessor#removeProcessor(InputProcessor) + */ + public boolean removeInputProcessor(final InputProcessor processor) { + return this.inputProcessor.removeProcessor(processor); + } + + /** + * @param processor {@inheritDoc} + * @see FluxInputProcessor#setMainProcessor + */ + @Override + public void setInputProcessor(final InputProcessor processor) { + this.inputProcessor.setMainProcessor(processor); + } + + /** + * @return {@inheritDoc} + * @see FluxInputProcessor#getMainProcessor + */ + @Override + public InputProcessor getInputProcessor() { + return this.inputProcessor.getMainProcessor(); + } + + //////////////////////////////// + // DELEGATED IMPLEMENTATION + //////////////////////////////// + + @Override + public float getAccelerometerX() { + return delegate.getAccelerometerX(); + } + + @Override + public float getAccelerometerY() { + return delegate.getAccelerometerY(); + } + + @Override + public float getAccelerometerZ() { + return delegate.getAccelerometerZ(); + } + + @Override + public float getGyroscopeX() { + return delegate.getGyroscopeX(); + } + + @Override + public float getGyroscopeY() { + return delegate.getGyroscopeY(); + } + + @Override + public float getGyroscopeZ() { + return delegate.getGyroscopeZ(); + } + + @Override + public int getMaxPointers() { + return delegate.getMaxPointers(); + } + + @Override + public int getX() { + return delegate.getX(); + } + + @Override + public int getX(final int pointer) { + return delegate.getX(pointer); + } + + @Override + public int getDeltaX() { + return delegate.getDeltaX(); + } + + @Override + public int getDeltaX(final int pointer) { + return delegate.getDeltaX(pointer); + } + + @Override + public int getY() { + return delegate.getY(); + } + + @Override + public int getY(final int pointer) { + return delegate.getY(pointer); + } + + @Override + public int getDeltaY() { + return delegate.getDeltaY(); + } + + @Override + public int getDeltaY(final int pointer) { + return delegate.getDeltaY(pointer); + } + + @Override + public boolean isTouched() { + return delegate.isTouched(); + } + + @Override + public boolean justTouched() { + return delegate.justTouched(); + } + + @Override + public boolean isTouched(final int pointer) { + return delegate.isTouched(pointer); + } + + @Override + public float getPressure() { + return delegate.getPressure(); + } + + @Override + public float getPressure(final int pointer) { + return delegate.getPressure(pointer); + } + + @Override + public boolean isButtonPressed(final int button) { + return delegate.isButtonPressed(button); + } + + @Override + public boolean isButtonJustPressed(final int button) { + return delegate.isButtonJustPressed(button); + } + + @Override + public boolean isKeyPressed(final int key) { + return delegate.isKeyPressed(key); + } + + @Override + public boolean isKeyJustPressed(final int key) { + return delegate.isKeyJustPressed(key); + } + + @Override + public void getTextInput( + final TextInputListener listener, + final String title, + final String text, + final String hint + ) { + delegate.getTextInput(listener, title, text, hint); + } + + @Override + public void getTextInput( + final TextInputListener listener, + final String title, + final String text, + final String hint, + final OnscreenKeyboardType type + ) { + delegate.getTextInput(listener, title, text, hint, type); + } + + @Override + public void setOnscreenKeyboardVisible(final boolean visible) { + delegate.setOnscreenKeyboardVisible(visible); + } + + @Override + public void setOnscreenKeyboardVisible(final boolean visible, final OnscreenKeyboardType type) { + delegate.setOnscreenKeyboardVisible(visible, type); + } + + @Override + public void vibrate(final int milliseconds) { + delegate.vibrate(milliseconds); + } + + @Override + public void vibrate(final int milliseconds, final boolean fallback) { + delegate.vibrate(milliseconds, fallback); + } + + @Override + public void vibrate(final int milliseconds, final int amplitude, final boolean fallback) { + delegate.vibrate(milliseconds, amplitude, fallback); + } + + @Override + public void vibrate(final VibrationType vibrationType) { + delegate.vibrate(vibrationType); + } + + @Override + public float getAzimuth() { + return delegate.getAzimuth(); + } + + @Override + public float getPitch() { + return delegate.getPitch(); + } + + @Override + public float getRoll() { + return delegate.getRoll(); + } + + @Override + public void getRotationMatrix(final float[] matrix) { + delegate.getRotationMatrix(matrix); + } + + @Override + public long getCurrentEventTime() { + return delegate.getCurrentEventTime(); + } + + @Deprecated + @Override + public void setCatchBackKey(final boolean catchBack) { + delegate.setCatchBackKey(catchBack); + } + + @Deprecated + @Override + public boolean isCatchBackKey() { + return delegate.isCatchBackKey(); + } + + @Deprecated + @Override + public void setCatchMenuKey(final boolean catchMenu) { + delegate.setCatchMenuKey(catchMenu); + } + + @Deprecated + @Override + public boolean isCatchMenuKey() { + return delegate.isCatchMenuKey(); + } + + @Override + public void setCatchKey(final int keycode, final boolean catchKey) { + delegate.setCatchKey(keycode, catchKey); + } + + @Override + public boolean isCatchKey(final int keycode) { + return delegate.isCatchKey(keycode); + } + + @Override + public boolean isPeripheralAvailable(final Peripheral peripheral) { + return delegate.isPeripheralAvailable(peripheral); + } + + @Override + public int getRotation() { + return delegate.getRotation(); + } + + @Override + public Orientation getNativeOrientation() { + return delegate.getNativeOrientation(); + } + + @Override + public void setCursorCatched(final boolean catched) { + delegate.setCursorCatched(catched); + } + + @Override + public boolean isCursorCatched() { + return delegate.isCursorCatched(); + } + + @Override + public void setCursorPosition(final int x, final int y) { + delegate.setCursorPosition(x, y); + } +} diff --git a/src/main/java/dev/crmodders/flux/impl/input/client/FluxInputProcessor.java b/src/main/java/dev/crmodders/flux/impl/input/client/FluxInputProcessor.java new file mode 100644 index 0000000..fec8ea8 --- /dev/null +++ b/src/main/java/dev/crmodders/flux/impl/input/client/FluxInputProcessor.java @@ -0,0 +1,204 @@ +package dev.crmodders.flux.impl.input.client; + +import com.badlogic.gdx.InputAdapter; +import com.badlogic.gdx.InputProcessor; + +import java.util.LinkedHashSet; + +final class FluxInputProcessor implements InputProcessor { + private static final InputProcessor NOOP = new InputAdapter(); + + private final LinkedHashSet delegates; + + private InputProcessor main; + + public FluxInputProcessor() { + this.delegates = new LinkedHashSet<>(); + this.main = NOOP; + } + + /** + * Adds an input processor. + *

+ * All added input processors are ran in insertion order through an internal + * {@link LinkedHashSet}. Addition will not succeed with a {@code null} + * processor, {@code this}, or the current main processor. The internal set + * works best if the input processors did not override {@link Object#equals} + * and {@link Object#hashCode()} and in case that it did, make a wrapper. + * + * @param processor The processor to add. + * @return {@code boolean} value representing addition success. + * @see #getMainProcessor + */ + public boolean addProcessor(final InputProcessor processor) { + return processor != null + && processor != this + && processor != this.main + && this.delegates.add(processor); + } + + /** + * Checks input processor presence. + *

+ * Checks if a given processor is present and added through + * {@code addProcessor}. This method does not check against the main + * processor. + * + * @param processor The processor to check. + * @return {@code true} if present; {@code false} otherwise. + * @see #addProcessor + */ + public boolean containsProcessor(final InputProcessor processor) { + return processor != null && this.delegates.contains(processor); + } + + /** + * Removes an input processor. + *

+ * Removes a given processor added through {@code addProcessor}. This method + * cannot remove the main processor, use {@code setMainProcessor(null)}. + * + * @param processor The processor to remove. + * @return {@code boolean} value representing removal success. + * @see #addProcessor + * @see #setMainProcessor + */ + public boolean removeProcessor(final InputProcessor processor) { + return processor != null && this.delegates.remove(processor); + } + + /** + * The main front-facing input processor. + * + * @return The main processor. + * @see #setMainProcessor + */ + public InputProcessor getMainProcessor() { + final var main = this.main; + return main == NOOP ? null : main; + } + + /** + * Sets the main processor. + *

+ * The main processor is the front facing processor visible through + * {@link FluxInput#getInputProcessor} where this method is the delegate + * of {@link FluxInput#setInputProcessor}. Delegated + * {@link com.badlogic.gdx.Input}s will return a {@code FluxInputProcessor}. + *

+ * If the given processor is already present through {@link #addProcessor}, + * it's removed, as if through {@link #removeProcessor}, will it then be set + * as the new main processor. + *

+ * The previous main processor is replaced and is not re-added to the + * processor list if it was promoted from {@code addProcessor}. {@code null} + * is valid and will behave as a no-op as if a blank extension of + * {@link InputAdapter}. + * + * @param processor The new main processor. + */ + public void setMainProcessor(final InputProcessor processor) { + if (processor == null) { + this.main = NOOP; + } else { + this.delegates.remove(processor); + this.main = processor; + } + } + + @Override + public boolean keyDown(final int keycode) { + final var result = this.main.keyDown(keycode); + for (final var delegate : this.delegates) { + delegate.keyDown(keycode); + } + return result; + } + + @Override + public boolean keyUp(final int keycode) { + final var result = this.main.keyUp(keycode); + for (final var delegate : this.delegates) { + delegate.keyUp(keycode); + } + return result; + } + + @Override + public boolean keyTyped(final char character) { + final var result = this.main.keyUp(character); + for (final var delegate : this.delegates) { + delegate.keyUp(character); + } + return result; + } + + @Override + public boolean touchDown( + final int screenX, + final int screenY, + final int pointer, + final int button + ) { + final var result = this.main.touchDown(screenX, screenY, pointer, button); + for (final var delegate : this.delegates) { + delegate.touchDown(screenX, screenY, pointer, button); + } + return result; + } + + @Override + public boolean touchUp( + final int screenX, + final int screenY, + final int pointer, + final int button + ) { + final var result = this.main.touchUp(screenX, screenY, pointer, button); + for (final var delegate : this.delegates) { + delegate.touchUp(screenX, screenY, pointer, button); + } + return result; + } + + @Override + public boolean touchCancelled( + final int screenX, + final int screenY, + final int pointer, + final int button + ) { + final var result = this.main.touchCancelled(screenX, screenY, pointer, button); + for (final var delegate : this.delegates) { + delegate.touchCancelled(screenX, screenY, pointer, button); + } + return result; + } + + @Override + public boolean touchDragged(final int screenX, final int screenY, final int pointer) { + final var result = this.main.touchDragged(screenX, screenY, pointer); + for (final var delegate : this.delegates) { + delegate.touchDragged(screenX, screenY, pointer); + } + return result; + } + + @Override + public boolean mouseMoved(final int screenX, final int screenY) { + final var result = this.main.mouseMoved(screenX, screenY); + for (final var delegate : this.delegates) { + delegate.mouseMoved(screenX, screenY); + } + return result; + } + + @Override + public boolean scrolled(final float amountX, final float amountY) { + final var result = this.main.scrolled(amountX, amountY); + for (final var delegate : this.delegates) { + delegate.scrolled(amountX, amountY); + } + return result; + } +} diff --git a/src/main/java/dev/crmodders/flux/impl/resource/loader/AssetFinder.java b/src/main/java/dev/crmodders/flux/impl/resource/loader/AssetFinder.java index d1327f9..6ce2e26 100644 --- a/src/main/java/dev/crmodders/flux/impl/resource/loader/AssetFinder.java +++ b/src/main/java/dev/crmodders/flux/impl/resource/loader/AssetFinder.java @@ -1,6 +1,5 @@ package dev.crmodders.flux.impl.resource.loader; -import dev.crmodders.flux.impl.base.Strings; import finalforeach.cosmicreach.util.Identifier; import org.jetbrains.annotations.Nullable; @@ -12,7 +11,7 @@ import java.util.Objects; import java.util.function.BiConsumer; -import static dev.crmodders.flux.impl.base.Logging.LOGGER; +import static dev.crmodders.flux.impl.resource.loader.FluxAssetLoading.LOGGER; /** * Helper class to find assets. @@ -66,7 +65,7 @@ public static void findNamespaced( try (final var matches = Files.list(namespacedPath.resolve(prefixOnFs))) { matches - .filter(it -> Strings.endsWithIgnoreCase(it.getFileName().toString(), extension)) + .filter(it -> endsWithIgnoreCase(it.getFileName().toString(), extension)) .filter(Files::isRegularFile) .forEach(it -> { final var name = namespacedPath.relativize(it).toString(); @@ -80,6 +79,10 @@ public static void findNamespaced( } } + private static boolean endsWithIgnoreCase(final String self, final String rhs) { + return self.regionMatches(true, self.length() - rhs.length(), rhs, 0, rhs.length()); + } + public AssetFinder { Objects.requireNonNull(prefix, "Parameter subAssetComponent is null"); Objects.requireNonNull(extension, "Parameter extension is null"); diff --git a/src/main/java/dev/crmodders/flux/impl/resource/loader/FluxAssetLoading.java b/src/main/java/dev/crmodders/flux/impl/resource/loader/FluxAssetLoading.java new file mode 100644 index 0000000..c258701 --- /dev/null +++ b/src/main/java/dev/crmodders/flux/impl/resource/loader/FluxAssetLoading.java @@ -0,0 +1,95 @@ +package dev.crmodders.flux.impl.resource.loader; + +import com.badlogic.gdx.files.FileHandle; +import dev.crmodders.flux.api.resource.loader.FluxFileHandle; +import org.jetbrains.annotations.Nullable; +import org.quiltmc.loader.api.ModContainer; +import org.quiltmc.loader.api.QuiltLoader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.spongepowered.asm.mixin.Unique; + +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.ProviderNotFoundException; +import java.util.HashMap; +import java.util.List; +import java.util.function.BiConsumer; + +public class FluxAssetLoading { + static final Logger LOGGER = LoggerFactory.getLogger("Flux Resource Loader"); + + public static void loadJarModAssets( + final String prefixString, + final String extension, + final BiConsumer assetConsumer, + final boolean includeDirectories, + final HashMap allAssets + ) { + final var finder = createAssetFinder(prefixString, extension, assetConsumer, allAssets); + if (finder == null) { + return; + } + + QuiltLoader.getAllMods() + .stream() + .filter(mod -> mod.getSourceType() != ModContainer.BasicSourceType.BUILTIN) + .map(ModContainer::getSourcePaths) + .flatMap(List::stream) + .flatMap(List::stream) + .map(Path::normalize) + .forEach(root -> { + if (Files.isDirectory(root)) { + finder.scan(root); + return; + } + try (final var zfs = FileSystems.newFileSystem(root)) { + finder.scan(zfs.getPath("/")); + } catch (final ProviderNotFoundException cause) { + LOGGER.warn("No file system provider for {}", root, cause); + } catch (final IOException cause) { + LOGGER.warn("Unable to access file system for {}", root, cause); + } + }); + } + + @Unique + private static @Nullable AssetFinder createAssetFinder( + final String prefixNotation, + final String extension, + final BiConsumer assetConsumer, + final HashMap allAssets + ) { + final String namespace; + final Path prefix; + { + final var separator = prefixNotation.indexOf(':'); + final String prefixString; + + if (separator >= 0) { + namespace = prefixNotation.substring(0, separator); + prefixString = prefixNotation.substring(separator + 1); + } else { + namespace = null; + prefixString = prefixNotation; + } + + try { + prefix = Path.of(prefixString); + } catch (final InvalidPathException cause) { + LOGGER.warn("Invalid prefix path: {}", prefixNotation, cause); + return null; + } + } + + return new AssetFinder(namespace, prefix, extension, (identifier, path) -> { + final var handle = new FluxFileHandle(path); + final var id = identifier.toString(); + allAssets.put(id, handle); + assetConsumer.accept(id, handle); + }); + } +} diff --git a/src/main/java/dev/crmodders/flux/mixin/resource/loader/GameAssetLoaderMixin.java b/src/main/java/dev/crmodders/flux/mixin/resource/loader/GameAssetLoaderMixin.java index cfb9f13..b51b648 100644 --- a/src/main/java/dev/crmodders/flux/mixin/resource/loader/GameAssetLoaderMixin.java +++ b/src/main/java/dev/crmodders/flux/mixin/resource/loader/GameAssetLoaderMixin.java @@ -2,30 +2,20 @@ import com.badlogic.gdx.files.FileHandle; import com.llamalad7.mixinextras.sugar.Local; -import dev.crmodders.flux.impl.resource.loader.AssetFinder; -import dev.crmodders.flux.impl.resource.loader.FluxFileHandle; +import dev.crmodders.flux.impl.resource.loader.FluxAssetLoading; import finalforeach.cosmicreach.GameAssetLoader; import finalforeach.cosmicreach.util.Identifier; -import org.jetbrains.annotations.Nullable; -import org.quiltmc.loader.api.ModContainer; -import org.quiltmc.loader.api.QuiltLoader; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import java.io.IOException; -import java.nio.file.*; import java.util.HashMap; import java.util.HashSet; -import java.util.List; import java.util.function.BiConsumer; -import static dev.crmodders.flux.impl.base.Logging.LOGGER; - @Mixin(GameAssetLoader.class) public class GameAssetLoaderMixin { @Final @@ -44,66 +34,12 @@ private static void loadJarModAssets( final CallbackInfo callback, final @Local(ordinal = 0) HashSet allPaths ) { - final var finder = flux_api$createAssetFinder(prefixString, extension, assetConsumer); - if (finder == null) { - return; - } - - QuiltLoader.getAllMods() - .stream() - .filter(mod -> mod.getSourceType() != ModContainer.BasicSourceType.BUILTIN) - .map(ModContainer::getSourcePaths) - .flatMap(List::stream) - .flatMap(List::stream) - .map(Path::normalize) - .forEach(root -> { - if (Files.isDirectory(root)) { - finder.scan(root); - return; - } - try (final var zfs = FileSystems.newFileSystem(root)) { - finder.scan(zfs.getPath("/")); - } catch (final ProviderNotFoundException cause) { - LOGGER.warn("No file system provider for {}", root, cause); - } catch (final IOException cause) { - LOGGER.warn("Unable to access file system for {}", root, cause); - } - }); - } - - @Unique - private static @Nullable AssetFinder flux_api$createAssetFinder( - final String prefixNotation, - final String extension, - final BiConsumer assetConsumer - ) { - final String namespace; - final Path prefix; - { - final var separator = prefixNotation.indexOf(':'); - final String prefixString; - - if (separator >= 0) { - namespace = prefixNotation.substring(0, separator); - prefixString = prefixNotation.substring(separator + 1); - } else { - namespace = null; - prefixString = prefixNotation; - } - - try { - prefix = Path.of(prefixString); - } catch (final InvalidPathException cause) { - LOGGER.warn("Invalid prefix path: {}", prefixNotation, cause); - return null; - } - } - - return new AssetFinder(namespace, prefix, extension, (identifier, path) -> { - final var handle = new FluxFileHandle(path); - final var id = identifier.toString(); - ALL_ASSETS.put(id, handle); - assetConsumer.accept(id, handle); - }); + FluxAssetLoading.loadJarModAssets( + prefixString, + extension, + assetConsumer, + includeDirectories, + ALL_ASSETS + ); } } diff --git a/src/main/resources/flux-api.mixins.json b/src/main/resources/flux-api.mixins.json index eebe686..863ed9b 100644 --- a/src/main/resources/flux-api.mixins.json +++ b/src/main/resources/flux-api.mixins.json @@ -11,4 +11,4 @@ "injectors": { "defaultRequire": 1 } -} \ No newline at end of file +} diff --git a/src/main/resources/quilt.mod.json b/src/main/resources/quilt.mod.json index 0ce686b..3cb5703 100644 --- a/src/main/resources/quilt.mod.json +++ b/src/main/resources/quilt.mod.json @@ -29,6 +29,7 @@ }, "entrypoints": { + "client_init": "dev.crmodders.flux.impl.input.client.FluxApiInputClientEntrypoint" }, "depends": [ diff --git a/src/test/java/dev/crmodders/flux/api/math/IntersectionTest.java b/src/test/java/dev/crmodders/flux/api/math/IntersectionTest.java new file mode 100644 index 0000000..437e169 --- /dev/null +++ b/src/test/java/dev/crmodders/flux/api/math/IntersectionTest.java @@ -0,0 +1,130 @@ +package dev.crmodders.flux.api.math; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class IntersectionTest { + @Nested + class LineSegmentAndAabbUnchecked { + @ParameterizedTest + @CsvSource({ + "9.0, 4.5, 4.0, 4.0, 5.0, 10.0, 3.5, 4.0, NONE, 0.0, 0.0, 0.0, 0.0", + }) + void diagonalLineTest( + final float x1, + final float y1, + final float x2, + final float y2, + final float l, + final float r, + final float u, + final float d, + final Intersection.Convex expectedConvex, + final float expectedEnterX, + final float expectedEnterY, + final float expectedExitX, + final float expectedExitY + ) { + final var buffer = new float[4]; + + final Intersection.Convex actualConvex; + + Assertions.assertEquals( + expectedConvex, + actualConvex = Intersection.lineSegmentAndAabbUnchecked( + x1, + y1, + x2, + y2, + l, + r, + u, + d, + buffer, + 0 + ), + "Convex Intersections" + ); + + if (actualConvex.entered()) { + final float actualEnterX; + final float actualEnterY; + + Assertions.assertEquals(expectedEnterX, actualEnterX = buffer[0], "Entering Intersection Point (x)"); + Assertions.assertEquals(expectedEnterY, actualEnterY = buffer[1], "Entering Intersection Point (y)"); + } + + if (actualConvex.exited()) { + final float actualExitX; + final float actualExitY; + + Assertions.assertEquals(expectedExitX, actualExitX = buffer[2], "Exiting Intersection Point (x)"); + Assertions.assertEquals(expectedExitY, actualExitY = buffer[3], "Exiting Intersection Point (y)"); + } + } + } + + @Nested + class LineSegmentAndAxesUnchecked { + /** + * @see Test Data Visualization on Desmos + */ + @ParameterizedTest + @CsvSource({ + "9.0, 4.5, 4.0, 4.0, 5.0, 4.0, 10.0, 4.5, 10.0, 4.0, NONE, 0.0, 0.0", + "1.0, 0.0, 3.0, 1.0, 2.0, 0.0, 3.0, 1.0, 2.0, 1.0, Y, 2.0, 0.5", + "8.0, 3.0, 6.0, -1.0, 6.0, -1.0, 8.0, 2.0, 8.0, 2.0, X, 7.5, 2.0", + "0.0, 8.0, 2.0, 6.0, 1.0, 5.0, 3.0, 7.0, 1.0, 7.0, BOTH, 1.0, 7.0", + }) + void diagonalLineTest( + final float x1, + final float y1, + final float x2, + final float y2, + final float minX, + final float minY, + final float maxX, + final float maxY, + final float a, + final float b, + final Intersection.Axes expectedAxes, + final float expectedX, + final float expectedY + ) { + final var buffer = new float[2]; + + final Intersection.Axes actualAxes; + + Assertions.assertEquals( + expectedAxes, + actualAxes = Intersection.lineSegmentAndAxesUnchecked( + x1, + y1, + x2, + y2, + minX, + minY, + maxX, + maxY, + a, + b, + buffer, + 0 + ), + "Intersected Axes" + ); + + if (actualAxes == Intersection.Axes.NONE) { + return; + } + + final float actualX; + final float actualY; + + Assertions.assertEquals(expectedX, actualX = buffer[0], "Intersection Point (x)"); + Assertions.assertEquals(expectedY, actualY = buffer[1], "Intersection Point (y)"); + } + } +}