feat(mod): backend integration, module cleanup, and SpatialGui port
This commit is contained in:
parent
ff2edc8eb6
commit
10d90dce93
47 changed files with 2059 additions and 718 deletions
|
|
@ -0,0 +1,40 @@
|
|||
package dev.loki.lovisual.features.gui.hud.draggable.impl.combat;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link MediaPlayerConstants#scrollOffset}, the pure marquee-scroll offset
|
||||
* calculator shared by the title and artist rows of the MediaPlayer HUD widget.
|
||||
*/
|
||||
final class MediaPlayerConstantsScrollTest {
|
||||
|
||||
@Test
|
||||
void staysAtZeroDuringThePause() {
|
||||
assertEquals(0f, MediaPlayerConstants.scrollOffset(0f, 100f, 20f, 0.8f));
|
||||
assertEquals(0f, MediaPlayerConstants.scrollOffset(0.5f, 100f, 20f, 0.8f));
|
||||
assertEquals(0f, MediaPlayerConstants.scrollOffset(0.8f, 100f, 20f, 0.8f));
|
||||
}
|
||||
|
||||
@Test
|
||||
void slidesLeftAtTheGivenSpeedAfterThePause() {
|
||||
float offset = MediaPlayerConstants.scrollOffset(1.8f, 100f, 20f, 0.8f);
|
||||
// 1.8s - 0.8s pause = 1.0s of travel at 20px/s.
|
||||
assertEquals(-20f, offset, 1e-4f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void wrapsBackToZeroAtTheStartOfTheNextCycle() {
|
||||
// cycleTime = 0.8 + 100/20 = 5.8s; just past that should read like t≈0 again.
|
||||
float wrapped = MediaPlayerConstants.scrollOffset(5.9f, 100f, 20f, 0.8f);
|
||||
float freshStart = MediaPlayerConstants.scrollOffset(0.1f, 100f, 20f, 0.8f);
|
||||
assertEquals(freshStart, wrapped, 1e-4f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsZeroForNonPositiveSpeed() {
|
||||
assertEquals(0f, MediaPlayerConstants.scrollOffset(5f, 100f, 0f, 0.8f));
|
||||
assertEquals(0f, MediaPlayerConstants.scrollOffset(5f, 100f, -1f, 0.8f));
|
||||
}
|
||||
}
|
||||
|
|
@ -124,6 +124,28 @@ public final class LrcParserTest {
|
|||
assertEquals("Trim me", lines.get(0).text());
|
||||
}
|
||||
|
||||
@Test
|
||||
void activeLineIndexReturnsMinusOneBeforeFirstLine() {
|
||||
List<LyricLine> lines = LrcParser.parse("[00:10.00]First\n[00:20.00]Second");
|
||||
assertEquals(-1, LrcParser.activeLineIndex(lines, 5_000L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void activeLineIndexReturnsMinusOneForEmptyOrNullList() {
|
||||
assertEquals(-1, LrcParser.activeLineIndex(List.of(), 1_000L));
|
||||
assertEquals(-1, LrcParser.activeLineIndex(null, 1_000L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void activeLineIndexTracksCurrentLineAsTimeAdvances() {
|
||||
List<LyricLine> lines = LrcParser.parse("[00:10.00]First\n[00:20.00]Second\n[00:30.00]Third");
|
||||
|
||||
assertEquals(0, LrcParser.activeLineIndex(lines, 10_000L));
|
||||
assertEquals(0, LrcParser.activeLineIndex(lines, 19_999L));
|
||||
assertEquals(1, LrcParser.activeLineIndex(lines, 20_000L));
|
||||
assertEquals(2, LrcParser.activeLineIndex(lines, 999_999L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void handlesMultipleLinesWithMixedFormats() {
|
||||
String lrc = "[0:15.5]Short\n[01:30]No fraction\n[00:05.12]Two digits\n[00:05.123]Three digits";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.wings;
|
||||
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector3f;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* Regression test for the matrix bug that made {@link WingsRenderer} render nothing:
|
||||
* {@code transformPosition} must agree with JOML's own column-major position
|
||||
* transform (Vector3f.mulPosition), including the translation component - a
|
||||
* row/column mix-up here previously dropped the player's world-space
|
||||
* translation entirely, collapsing every wing vertex toward the world origin.
|
||||
*/
|
||||
final class WingsRendererTransformTest {
|
||||
|
||||
@Test
|
||||
void matchesJomlPositionTransformIncludingTranslation() {
|
||||
Matrix4f mat = new Matrix4f()
|
||||
.translate(100.5f, 64.0f, -200.25f)
|
||||
.rotateY((float) Math.toRadians(37f))
|
||||
.rotateX((float) Math.toRadians(12f))
|
||||
.scale(1.4f);
|
||||
|
||||
float lx = 0.86f, ly = -0.46f, lz = 0f;
|
||||
|
||||
Vec3 actual = WingsRenderer.transformPosition(mat, lx, ly, lz);
|
||||
|
||||
Vector3f expected = new Vector3f(lx, ly, lz).mulPosition(mat);
|
||||
|
||||
assertEquals(expected.x, (float) actual.x, 1e-4f);
|
||||
assertEquals(expected.y, (float) actual.y, 1e-4f);
|
||||
assertEquals(expected.z, (float) actual.z, 1e-4f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void translationIsActuallyApplied() {
|
||||
Matrix4f mat = new Matrix4f().translate(10f, 20f, 30f);
|
||||
|
||||
Vec3 origin = WingsRenderer.transformPosition(mat, 0f, 0f, 0f);
|
||||
|
||||
// Before the fix this returned (0, 0, 0): the translation column was
|
||||
// never read, so a vertex at the local origin never moved to the
|
||||
// player's world position.
|
||||
assertEquals(10f, (float) origin.x, 1e-4f);
|
||||
assertEquals(20f, (float) origin.y, 1e-4f);
|
||||
assertEquals(30f, (float) origin.z, 1e-4f);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package dev.loki.lovisual.features.platform;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* Pure normalization tests for {@link BackendEndpoint}: no config file I/O
|
||||
* and no singleton involved, matching the value-object test style used by
|
||||
* dev.loki.lovisual.config.values.PrimitiveValuesTest.
|
||||
*/
|
||||
public final class BackendEndpointTest {
|
||||
|
||||
private static final String FALLBACK = "https://api.lovisual.example";
|
||||
|
||||
@Test
|
||||
void keepsAValidHttpsUrlUnchanged() {
|
||||
assertEquals("https://api.lovisual.com", BackendEndpoint.normalize("https://api.lovisual.com", FALLBACK));
|
||||
}
|
||||
|
||||
@Test
|
||||
void trimsWhitespaceAndTrailingSlash() {
|
||||
assertEquals("https://api.lovisual.com", BackendEndpoint.normalize(" https://api.lovisual.com/ ", FALLBACK));
|
||||
}
|
||||
|
||||
@Test
|
||||
void stripsMultipleTrailingSlashes() {
|
||||
assertEquals("https://api.lovisual.com", BackendEndpoint.normalize("https://api.lovisual.com///", FALLBACK));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fallsBackWhenValueIsBlank() {
|
||||
assertEquals(FALLBACK, BackendEndpoint.normalize("", FALLBACK));
|
||||
assertEquals(FALLBACK, BackendEndpoint.normalize(" ", FALLBACK));
|
||||
assertEquals(FALLBACK, BackendEndpoint.normalize(null, FALLBACK));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fallsBackWhenValueIsMalformed() {
|
||||
assertEquals(FALLBACK, BackendEndpoint.normalize("not a url", FALLBACK));
|
||||
assertEquals(FALLBACK, BackendEndpoint.normalize("just-a-hostname", FALLBACK));
|
||||
assertEquals(FALLBACK, BackendEndpoint.normalize("://missing-scheme", FALLBACK));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fallbackItselfIsAlsoNormalized() {
|
||||
assertEquals("https://api.lovisual.example", BackendEndpoint.normalize(null, "https://api.lovisual.example/"));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package dev.loki.lovisual.features.platform;
|
||||
|
||||
import dev.loki.lovisual.config.values.primitive.StringValue;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* Covers the {@code backendBaseUrl} setting the way {@link PlatformConfig}
|
||||
* declares it (same name/default), without touching the singleton or its
|
||||
* on-disk config file. {@link dev.loki.lovisual.config.io.ConfigSerializer}
|
||||
* persists a config object purely through each {@code ConfigValue}'s
|
||||
* {@code toJson}/{@code fromJson}, so exercising that pair here is exactly
|
||||
* what "round-trips through the config serializer" means, matching the style
|
||||
* of dev.loki.lovisual.config.values.PrimitiveValuesTest.
|
||||
*/
|
||||
public final class PlatformConfigValueTest {
|
||||
|
||||
@Test
|
||||
void defaultsToProductionBackendUrlWhenNothingIsConfigured() {
|
||||
StringValue value = new StringValue("backendBaseUrl", PlatformConfig.DEFAULT_BACKEND_BASE_URL);
|
||||
assertEquals(PlatformConfig.DEFAULT_BACKEND_BASE_URL, value.get());
|
||||
assertEquals(PlatformConfig.DEFAULT_BACKEND_BASE_URL, BackendEndpoint.normalize(value.get(), PlatformConfig.DEFAULT_BACKEND_BASE_URL));
|
||||
}
|
||||
|
||||
@Test
|
||||
void overriddenValueRoundTripsThroughToJsonFromJson() {
|
||||
StringValue value = new StringValue("backendBaseUrl", PlatformConfig.DEFAULT_BACKEND_BASE_URL);
|
||||
value.fromJson("https://staging.lovisual.example");
|
||||
|
||||
Object serialized = value.toJson();
|
||||
assertEquals("https://staging.lovisual.example", serialized);
|
||||
|
||||
StringValue reloaded = new StringValue("backendBaseUrl", PlatformConfig.DEFAULT_BACKEND_BASE_URL);
|
||||
reloaded.fromJson(serialized);
|
||||
assertEquals("https://staging.lovisual.example", reloaded.get());
|
||||
assertEquals("https://staging.lovisual.example", BackendEndpoint.normalize(reloaded.get(), PlatformConfig.DEFAULT_BACKEND_BASE_URL));
|
||||
}
|
||||
|
||||
@Test
|
||||
void malformedStoredValueFallsBackAfterRoundTrip() {
|
||||
StringValue value = new StringValue("backendBaseUrl", PlatformConfig.DEFAULT_BACKEND_BASE_URL);
|
||||
value.fromJson("not-a-url");
|
||||
|
||||
Object serialized = value.toJson();
|
||||
StringValue reloaded = new StringValue("backendBaseUrl", PlatformConfig.DEFAULT_BACKEND_BASE_URL);
|
||||
reloaded.fromJson(serialized);
|
||||
|
||||
assertEquals(PlatformConfig.DEFAULT_BACKEND_BASE_URL,
|
||||
BackendEndpoint.normalize(reloaded.get(), PlatformConfig.DEFAULT_BACKEND_BASE_URL));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package dev.loki.lovisual.features.platform.http;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Pure JSON/status mapping logic of {@link ApiResult}, covering exactly the
|
||||
* response shapes the backend plans document: 2xx bodies, {@code {"error":
|
||||
* "..."}} error bodies, 404/202 markers used by the device-token poll and
|
||||
* shared-config lookup, and unparseable/empty bodies.
|
||||
*/
|
||||
public final class ApiResultTest {
|
||||
|
||||
@Test
|
||||
void twoHundredWithBodyIsSuccess() {
|
||||
ApiResult result = ApiResult.of(200, "{\"device_token\":\"lvd_abc\"}");
|
||||
assertTrue(result.success());
|
||||
assertEquals("lvd_abc", result.asObject().get("device_token").getAsString());
|
||||
assertNull(result.error());
|
||||
}
|
||||
|
||||
@Test
|
||||
void nonTwoHundredWithErrorBodyExposesMessage() {
|
||||
ApiResult result = ApiResult.of(400, "{\"error\":\"bad request\"}");
|
||||
assertFalse(result.success());
|
||||
assertEquals("bad request", result.error());
|
||||
}
|
||||
|
||||
@Test
|
||||
void nonTwoHundredWithoutParsableBodyFallsBackToStatusCode() {
|
||||
ApiResult result = ApiResult.of(500, "not json");
|
||||
assertFalse(result.success());
|
||||
assertEquals("HTTP 500", result.error());
|
||||
assertNull(result.body());
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyBodyOnSuccessIsHandled() {
|
||||
ApiResult result = ApiResult.of(204, "");
|
||||
assertTrue(result.success());
|
||||
assertNull(result.body());
|
||||
assertNull(result.asObject());
|
||||
}
|
||||
|
||||
@Test
|
||||
void fourOhFourIsNotFoundAndTwoOhTwoIsPending() {
|
||||
assertTrue(ApiResult.of(404, "{\"error\":\"unknown\"}").isNotFound());
|
||||
assertTrue(ApiResult.of(202, "{}").isPending());
|
||||
assertFalse(ApiResult.of(200, "{}").isNotFound());
|
||||
assertFalse(ApiResult.of(200, "{}").isPending());
|
||||
}
|
||||
|
||||
@Test
|
||||
void networkErrorHasNoStatusCodeAndCarriesTheMessage() {
|
||||
ApiResult result = ApiResult.networkError("timed out");
|
||||
assertFalse(result.success());
|
||||
assertEquals(-1, result.statusCode());
|
||||
assertEquals("timed out", result.error());
|
||||
}
|
||||
|
||||
@Test
|
||||
void networkErrorFallsBackToGenericMessageWhenNoneGiven() {
|
||||
ApiResult result = ApiResult.networkError(null);
|
||||
assertEquals("Network error", result.error());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
package dev.loki.lovisual.features.platform.json;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import dev.loki.lovisual.config.profile.codec.ConfigProfileEntry;
|
||||
import dev.loki.lovisual.config.profile.codec.ConfigProfileType;
|
||||
import dev.loki.lovisual.config.profile.snapshot.ConfigProfileMeta;
|
||||
import dev.loki.lovisual.config.profile.snapshot.ConfigProfileSnapshot;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Round-trips {@link ConfigProfileSnapshot} through {@link ConfigProfileJsonCodec},
|
||||
* covering every value type {@code ConfigValue#fromJson} accepts (booleans,
|
||||
* numbers, strings, nested lists/maps) plus meta fields, since this codec is
|
||||
* the adapter between the on-disk binary profile format and the backend's
|
||||
* JSON {@code data} field.
|
||||
*/
|
||||
public final class ConfigProfileJsonCodecTest {
|
||||
|
||||
@Test
|
||||
void roundTripsMetaAndSimpleValues() {
|
||||
Map<String, Object> values = new LinkedHashMap<>();
|
||||
values.put("enabled", true);
|
||||
values.put("scale", 1.5);
|
||||
values.put("label", "Hello");
|
||||
values.put("count", 42);
|
||||
|
||||
ConfigProfileMeta meta = new ConfigProfileMeta("id1", "My Profile", ConfigProfileType.MODULES, "Tester", 1000L, 2000L, 1);
|
||||
ConfigProfileSnapshot snapshot = new ConfigProfileSnapshot(meta,
|
||||
List.of(new ConfigProfileEntry("module:Test", "Test", values)));
|
||||
|
||||
JsonObject json = ConfigProfileJsonCodec.toJson(snapshot);
|
||||
ConfigProfileSnapshot restored = ConfigProfileJsonCodec.fromJson(json);
|
||||
|
||||
assertEquals("id1", restored.meta().id());
|
||||
assertEquals("My Profile", restored.meta().name());
|
||||
assertEquals(ConfigProfileType.MODULES, restored.meta().type());
|
||||
assertEquals("Tester", restored.meta().author());
|
||||
assertEquals(1000L, restored.meta().createdAt());
|
||||
assertEquals(2000L, restored.meta().updatedAt());
|
||||
assertEquals(1, restored.entries().size());
|
||||
|
||||
ConfigProfileEntry entry = restored.entries().get(0);
|
||||
assertEquals("module:Test", entry.ownerId());
|
||||
assertEquals(Boolean.TRUE, entry.values().get("enabled"));
|
||||
assertEquals(1.5, (Double) entry.values().get("scale"));
|
||||
assertEquals("Hello", entry.values().get("label"));
|
||||
assertEquals(42.0, (Double) entry.values().get("count"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void roundTripsNestedListsAndMaps() {
|
||||
Map<String, Object> nested = new LinkedHashMap<>();
|
||||
nested.put("x", 1);
|
||||
nested.put("y", 2);
|
||||
Map<String, Object> values = new LinkedHashMap<>();
|
||||
values.put("tags", List.of("a", "b", "c"));
|
||||
values.put("point", nested);
|
||||
|
||||
ConfigProfileMeta meta = new ConfigProfileMeta("id2", "Nested", ConfigProfileType.HUD, "Tester", 1L, 2L, 1);
|
||||
ConfigProfileSnapshot snapshot = new ConfigProfileSnapshot(meta,
|
||||
List.of(new ConfigProfileEntry("hud.draggable:widget", "Widget", values)));
|
||||
|
||||
ConfigProfileSnapshot restored = ConfigProfileJsonCodec.fromJson(ConfigProfileJsonCodec.toJson(snapshot));
|
||||
ConfigProfileEntry entry = restored.entries().get(0);
|
||||
|
||||
Object tags = entry.values().get("tags");
|
||||
assertTrue(tags instanceof List<?>);
|
||||
assertEquals(List.of("a", "b", "c"), tags);
|
||||
|
||||
Object point = entry.values().get("point");
|
||||
assertTrue(point instanceof Map<?, ?>);
|
||||
assertEquals(1.0, (Double) ((Map<?, ?>) point).get("x"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingMetaAndEntriesDecodeToEmptyDefaults() {
|
||||
ConfigProfileSnapshot restored = ConfigProfileJsonCodec.fromJson(new JsonObject());
|
||||
assertTrue(restored.entries().isEmpty());
|
||||
assertEquals(ConfigProfileType.MODULES, restored.meta().type());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue