chore(history): squash 59 commit(s) from 2026-09-23
- refactor(hud/weather/render): Scoreboard 931→240 + Rain 903→159 + UiMeshGeometry 956→99 (8.5.2) - refactor(chams): Chams 985 → 129 + 4×≤195 (8.5.2) - refactor(module): Module 1031 → 139 + 4×≤134 (8.5.2) - refactor(tooltips): BetterTooltips 960 → 181 + 4×≤199 (8.5.2) - refactor(clickgui): ClickGuiPickerState 970→166 + ModulesMenuScreen 966→145 (8.5.2) - refactor(clickgui): CombatProtocolHeuristicsEditorState 932→79 + 4×≤192 (8.5.2) - refactor(hud): CompactHudStatModel 894 → 125 + 4×≤175 (8.5.2) - refactor(iris): ShaderPatchCompiler 843→66 + 4×≤174 (8.5.2) - refactor(tab): CustomTabList 834→136 + 4×≤166 (8.5.2) - refactor(relations): OnlineRelationPlayerPickerComponent 837→74 + 4×≤192 (8.5.2) - refactor(hits): HitEffect 770→120 + 4×≤176 (8.5.2) - refactor(predict): Predictions 815→150 + 4×≤195 (8.5.2) - refactor(config): ConfigProfilesComponent 799→125 + 4×≤195 (8.5.2) - refactor(sim): PlayerMovementSimulation 757→157 + 4×≤194 (8.5.2) - refactor(predict): ProjectilePuncher 747→104 + 3×≤134 (8.5.2) - refactor(crosshair): Crosshair 743→143 + 4×≤167 (8.5.2) - refactor(mediaplayer): MediaPlayer 743→174 + 4×≤196 (8.5.2) - refactor(hud): Itemizer 699→193 + Cooldowns 707→151 (8.5.2) - refactor(hud): Potions 617→176 + 4×≤166 (8.5.2) - refactor(hud): Admins 702→118 + 4×≤138 (8.5.2) - refactor(hud): Keybinds 611→157 + 4×≤160 (8.5.2) - refactor(hud): ModuleList 499→181 + 3×≤150 (8.5.2) - refactor(hud): Radar 392→161 + 2×≤124 (8.5.2) - refactor(hud): Fps 309→168 + 3×≤107 (8.5.2) - refactor(hud): Memory 304→151 + 3×≤144 (8.5.2) - refactor(hud): Tps 339→154 + 3×≤143 (8.5.2) - refactor(hud): Ping 309→145 + 3×≤125 (8.5.2) - refactor(hud): SpeedBps 334→156 + 3×≤142 (8.5.2) - refactor(hud): GameTime 314→150 + 3×≤127 (8.5.2) - refactor(hud): SystemTime 300→136 + 3×≤130 (8.5.2) - refactor(hud): Coordinates 337→174 + 2×≤172 (8.5.2) - refactor(hud): Armor 324→162 + 2×≤148 (8.5.2) - refactor(hud): Inventory 476→153 + 3×≤181 (8.5.2) - docs(todo): обновлён 8.5.2 — отмечены готовые гиганты (BetterButtons/InventorySwap/Svg/Темы/WorldParticles/Module/Chams и хвост) - docs(api): папка combatant-client-26.2/docs + addons.md (API можно ломать при рефакторе) - refactor(visuals): BlockHighlight 692→271 + 4×≤200 (8.5.2) - refactor(mixins): GameRendererMixin 830→344 (хуки) + 4 хелпера (8.5.2) - refactor(visuals): WorldParticles 821→260 + 6×≤200, режимы добиты (8.5.2) - docs(todo): 8.5.2 — WorldParticles/GameRendererMixin/BlockHighlight отмечены ГОТОВО - refactor(render): MeshBuilder 738→447 (фасад write-API) + 3 хелпера, FrameStats снесён (8.5.2) - docs(todo): Фаза 10 — дизайн платформы (аккаунты/конфиги/аватарки), backlog RPC-чата - docs(todo): Подсистема 3 — Аддоны 2.0 (скрипты/версии) + витрина аддонов + админка - docs(todo): Подсистема 2 — RPC-чат/друзья/виджеты-телеметрия + рейт-лимиты, доп. правки Подсистемы 3 - chore: переименование combatant-client-26.2 -> mod, добавлены backend/ и frontend/ - docs(backend): implementation plan for accounts-service (Подсистема 1, часть 1) - chore: пересоздать backend/frontend через cargo new и bun create vite + tailwind; убрать таблицу компонентов из TODO.md - docs(backend): edition 2024 в плане вместо 2021 - chore: обновить версии до реально актуальных (проверено компиляцией) - docs: правила платформы (лимит 250 строк, антипаттерны backend/frontend по итогам ресёрча) + frontend/ARCHITECTURE.md - docs: добавить правило ≤4 файла на папку в правила платформы (backend/frontend) - docs: commit messages in English from now on - chore: gitignore .superpowers/ scratch directory - docs: commit messages in English from now on (mod) - feat(backend): scaffold accounts-service with health check - chore(backend): remove target/ build artifacts from git, add gitignore - chore: track docs/ in git (was accidentally gitignored, never committed) - chore(backend): convert to Cargo workspace, plan full microservice layout - feat(backend): accounts/avatars/device_links schema + migration - feat(backend): argon2id password hashing
This commit is contained in:
parent
ec10395511
commit
9d08fa910a
2239 changed files with 35265 additions and 28110 deletions
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.config.profile;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import dev.loki.lovisual.config.profile.codec.ConfigProfileEntry;
|
||||
import dev.loki.lovisual.config.profile.codec.ConfigProfileType;
|
||||
import dev.loki.lovisual.config.profile.diff.ConfigProfileDiff;
|
||||
import dev.loki.lovisual.config.profile.diff.ConfigProfileDiffEngine;
|
||||
import dev.loki.lovisual.config.profile.snapshot.ConfigProfileMeta;
|
||||
import dev.loki.lovisual.config.profile.snapshot.ConfigProfileSnapshot;
|
||||
|
||||
public final class ConfigProfileDiffEngineTest {
|
||||
|
||||
private final ConfigProfileDiffEngine engine = new ConfigProfileDiffEngine();
|
||||
|
||||
@Test
|
||||
void identicalSnapshotsProduceEmptyDiff() {
|
||||
ConfigProfileSnapshot current = snapshot(entry("mod.a", "Mod A", "x", 1));
|
||||
ConfigProfileSnapshot selected = snapshot(entry("mod.a", "Mod A", "x", 1));
|
||||
|
||||
assertTrue(engine.compare(current, selected).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void changedValueIsReported() {
|
||||
ConfigProfileDiff diff = engine.compare(
|
||||
snapshot(entry("mod.a", "Mod A", "x", 1)),
|
||||
snapshot(entry("mod.a", "Mod A", "x", 2)));
|
||||
|
||||
List<ConfigProfileDiff.Value> values = diff.entries().get(0).values();
|
||||
assertEquals(1, values.size());
|
||||
assertEquals("x", values.get(0).key());
|
||||
assertEquals(1, values.get(0).currentValue());
|
||||
assertEquals(2, values.get(0).selectedValue());
|
||||
assertEquals(ConfigProfileDiff.Kind.CHANGED, values.get(0).kind());
|
||||
}
|
||||
|
||||
@Test
|
||||
void addedAndRemovedValuesAreReported() {
|
||||
ConfigProfileDiff diff = engine.compare(
|
||||
snapshot(entry("mod.a", "Mod A", "keep", 1, "gone", 2)),
|
||||
snapshot(entry("mod.a", "Mod A", "keep", 1, "added", 3)));
|
||||
|
||||
Map<String, ConfigProfileDiff.Kind> kinds = diff.entries().get(0).values().stream()
|
||||
.collect(Collectors.toMap(ConfigProfileDiff.Value::key, ConfigProfileDiff.Value::kind));
|
||||
|
||||
assertEquals(Set.of("gone", "added"), kinds.keySet());
|
||||
assertEquals(ConfigProfileDiff.Kind.REMOVED, kinds.get("gone"));
|
||||
assertEquals(ConfigProfileDiff.Kind.ADDED, kinds.get("added"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void ownerOnlyInSelectedBecomesAddedEntry() {
|
||||
ConfigProfileDiff diff = engine.compare(
|
||||
snapshot(),
|
||||
snapshot(entry("mod.b", "Mod B", "x", 1)));
|
||||
|
||||
ConfigProfileDiff.Entry entry = diff.entries().get(0);
|
||||
assertEquals("mod.b", entry.ownerId());
|
||||
assertEquals("Mod B", entry.displayName());
|
||||
assertEquals(List.of("x"), entry.values().stream()
|
||||
.map(ConfigProfileDiff.Value::key).toList());
|
||||
assertEquals(ConfigProfileDiff.Kind.ADDED, entry.values().get(0).kind());
|
||||
assertNull(entry.values().get(0).currentValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void ownerOnlyInCurrentKeepsCurrentDisplayName() {
|
||||
ConfigProfileDiff diff = engine.compare(
|
||||
snapshot(entry("mod.a", "Current Name", "x", 1)),
|
||||
snapshot());
|
||||
|
||||
ConfigProfileDiff.Entry entry = diff.entries().get(0);
|
||||
assertEquals("mod.a", entry.ownerId());
|
||||
assertEquals("Current Name", entry.displayName());
|
||||
assertEquals(ConfigProfileDiff.Kind.REMOVED, entry.values().get(0).kind());
|
||||
assertNull(entry.values().get(0).selectedValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullSnapshotsYieldEmptyModulesDiff() {
|
||||
ConfigProfileSnapshot snapshot = snapshot(entry("mod.a", "Mod A", "x", 1));
|
||||
|
||||
assertTrue(engine.compare(null, snapshot).isEmpty());
|
||||
assertTrue(engine.compare(snapshot, null).isEmpty());
|
||||
assertEquals(ConfigProfileType.MODULES, engine.compare(null, snapshot).type());
|
||||
}
|
||||
|
||||
@Test
|
||||
void valueChangeCountSumsAllChangedValues() {
|
||||
ConfigProfileDiff diff = engine.compare(
|
||||
snapshot(entry("mod.a", "A", "x", 1, "y", 2), entry("mod.b", "B", "z", 3)),
|
||||
snapshot(entry("mod.a", "A", "x", 9), entry("mod.b", "B", "z", 3)));
|
||||
|
||||
// mod.a: x changed, y removed; mod.b identical -> no entry
|
||||
assertEquals(2, diff.valueChangeCount());
|
||||
}
|
||||
|
||||
private static ConfigProfileSnapshot snapshot(ConfigProfileEntry... entries) {
|
||||
ConfigProfileMeta meta = new ConfigProfileMeta(
|
||||
"id", "Profile", ConfigProfileType.MODULES, "author", 0L, 0L, 0);
|
||||
return new ConfigProfileSnapshot(meta, List.of(entries));
|
||||
}
|
||||
|
||||
private static ConfigProfileEntry entry(String owner, String displayName, Object... keyValuePairs) {
|
||||
Map<String, Object> values = new LinkedHashMap<>();
|
||||
for (int i = 0; i < keyValuePairs.length; i += 2) {
|
||||
values.put((String) keyValuePairs[i], keyValuePairs[i + 1]);
|
||||
}
|
||||
return new ConfigProfileEntry(owner, displayName, values);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.config.profile;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import dev.loki.lovisual.config.profile.codec.ConfigProfileEntry;
|
||||
import dev.loki.lovisual.config.profile.codec.ConfigProfileType;
|
||||
import dev.loki.lovisual.config.profile.service.ConfigProfileStorage;
|
||||
import dev.loki.lovisual.config.profile.snapshot.ConfigProfileMeta;
|
||||
import dev.loki.lovisual.config.profile.snapshot.ConfigProfileSnapshot;
|
||||
import dev.loki.lovisual.features.module.core.Module;
|
||||
|
||||
/**
|
||||
* Тесты ConfigProfileStorage: канонический формат .lvcfg и обратная
|
||||
* совместимость с легаси-профилями .cbcfg (чтение, листинг, импорт, удаление).
|
||||
* Чистая файловая логика без инициализации Minecraft.
|
||||
*/
|
||||
public final class ConfigProfileStorageTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
private ConfigProfileStorage storage() {
|
||||
return new ConfigProfileStorage(tempDir);
|
||||
}
|
||||
|
||||
private static ConfigProfileSnapshot snapshot(String name) {
|
||||
ConfigProfileMeta meta = new ConfigProfileMeta(null, name, ConfigProfileType.MODULES, "loki5512344", 1L, 2L, 0);
|
||||
return new ConfigProfileSnapshot(meta, List.of(new ConfigProfileEntry("module.a", "Module A", Map.of("enabled", true))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveWritesCanonicalLvcfgFile() throws IOException {
|
||||
ConfigProfileStorage storage = storage();
|
||||
ConfigProfileSnapshot saved = snapshot("alpha");
|
||||
storage.save(saved);
|
||||
|
||||
Path canonical = tempDir.resolve("modules").resolve("alpha.lvcfg");
|
||||
assertTrue(Files.exists(canonical));
|
||||
|
||||
ConfigProfileSnapshot read = storage.read(ConfigProfileType.MODULES, "alpha");
|
||||
assertEquals("alpha", read.meta().name());
|
||||
}
|
||||
|
||||
@Test
|
||||
void readFallsBackToLegacyCbcfg() throws IOException {
|
||||
ConfigProfileStorage storage = storage();
|
||||
storage.save(snapshot("legacy"));
|
||||
Path canonical = tempDir.resolve("modules").resolve("legacy.lvcfg");
|
||||
Files.move(canonical, tempDir.resolve("modules").resolve("legacy.cbcfg"));
|
||||
|
||||
assertTrue(storage.exists(ConfigProfileType.MODULES, "legacy"));
|
||||
ConfigProfileSnapshot read = storage.read(ConfigProfileType.MODULES, "legacy");
|
||||
assertEquals("legacy", read.meta().name());
|
||||
}
|
||||
|
||||
@Test
|
||||
void listIncludesLegacyProfiles() throws IOException {
|
||||
ConfigProfileStorage storage = storage();
|
||||
storage.save(snapshot("fresh"));
|
||||
storage.save(snapshot("old"));
|
||||
Files.move(
|
||||
tempDir.resolve("modules").resolve("old.lvcfg"),
|
||||
tempDir.resolve("modules").resolve("old.cbcfg"),
|
||||
StandardCopyOption.REPLACE_EXISTING
|
||||
);
|
||||
|
||||
List<String> names = storage.list(ConfigProfileType.MODULES).stream().map(ConfigProfileMeta::name).toList();
|
||||
assertTrue(names.contains("fresh"));
|
||||
assertTrue(names.contains("old"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void listMigratesLegacyFileToCanonical() throws IOException {
|
||||
ConfigProfileStorage storage = storage();
|
||||
storage.save(snapshot("migrate"));
|
||||
Path legacy = tempDir.resolve("modules").resolve("migrate.cbcfg");
|
||||
Files.move(tempDir.resolve("modules").resolve("migrate.lvcfg"), legacy);
|
||||
|
||||
storage.list(ConfigProfileType.MODULES);
|
||||
|
||||
assertFalse(Files.exists(legacy));
|
||||
assertTrue(Files.exists(tempDir.resolve("modules").resolve("migrate.lvcfg")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteRemovesBothVariants() throws IOException {
|
||||
ConfigProfileStorage storage = storage();
|
||||
storage.save(snapshot("doomed"));
|
||||
Files.copy(
|
||||
tempDir.resolve("modules").resolve("doomed.lvcfg"),
|
||||
tempDir.resolve("modules").resolve("doomed.cbcfg")
|
||||
);
|
||||
|
||||
assertTrue(storage.delete(ConfigProfileType.MODULES, "doomed"));
|
||||
assertFalse(Files.exists(tempDir.resolve("modules").resolve("doomed.lvcfg")));
|
||||
assertFalse(Files.exists(tempDir.resolve("modules").resolve("doomed.cbcfg")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void importLegacyFileConvertsToCanonical() throws IOException {
|
||||
ConfigProfileStorage storage = storage();
|
||||
storage.save(snapshot("importme"));
|
||||
byte[] bytes = Files.readAllBytes(tempDir.resolve("modules").resolve("importme.lvcfg"));
|
||||
|
||||
Path source = tempDir.resolve("importme.cbcfg");
|
||||
Files.write(source, bytes);
|
||||
|
||||
ConfigProfileStorage.ImportResult result = storage.importProfileFile(source, true);
|
||||
assertEquals("importme", result.meta().getId());
|
||||
assertTrue(Files.exists(tempDir.resolve("modules").resolve("importme.lvcfg")));
|
||||
assertFalse(Files.exists(source));
|
||||
}
|
||||
|
||||
@Test
|
||||
void importRejectsForeignExtension() throws IOException {
|
||||
Path source = tempDir.resolve("notes.txt");
|
||||
Files.writeString(source, "not a profile");
|
||||
assertThrows(IOException.class, () -> storage().importProfileFile(source, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sanitizeFileNameStripsBothExtensions() {
|
||||
assertEquals("alpha", ConfigProfileStorage.sanitizeFileName("alpha.lvcfg"));
|
||||
assertEquals("alpha", ConfigProfileStorage.sanitizeFileName("alpha.cbcfg"));
|
||||
assertEquals("alpha", ConfigProfileStorage.sanitizeFileName("Alpha"));
|
||||
assertEquals("config", ConfigProfileStorage.sanitizeFileName(".lvcfg"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fileNormalizesLegacyIdToCanonical() {
|
||||
ConfigProfileStorage storage = storage();
|
||||
Path resolved = storage.file(ConfigProfileType.MODULES, "something.cbcfg");
|
||||
assertEquals(tempDir.resolve("modules").resolve("something.lvcfg"), resolved);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.config.profile;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
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.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import dev.loki.lovisual.config.profile.codec.ConfigProfileValueCodec;
|
||||
|
||||
public final class ConfigProfileValueCodecTest {
|
||||
|
||||
@Test
|
||||
void roundTripsNull() throws IOException {
|
||||
assertNull(roundTrip(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void roundTripsBooleans() throws IOException {
|
||||
assertEquals(Boolean.TRUE, roundTrip(Boolean.TRUE));
|
||||
assertEquals(Boolean.FALSE, roundTrip(Boolean.FALSE));
|
||||
}
|
||||
|
||||
@Test
|
||||
void roundTripsIntegersIncludingVarIntBoundaries() throws IOException {
|
||||
assertEquals(0, roundTrip(0));
|
||||
assertEquals(127, roundTrip(127));
|
||||
assertEquals(128, roundTrip(128));
|
||||
assertEquals(Integer.MAX_VALUE, roundTrip(Integer.MAX_VALUE));
|
||||
assertEquals(-1, roundTrip(-1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void roundTripsLongsFloatsAndDoubles() throws IOException {
|
||||
assertEquals(Long.MAX_VALUE, roundTrip(Long.MAX_VALUE));
|
||||
assertEquals(1.5f, roundTrip(1.5f));
|
||||
assertEquals(2.25d, roundTrip(2.25d));
|
||||
}
|
||||
|
||||
@Test
|
||||
void narrowNumericTypesAreStoredAsDoubles() throws IOException {
|
||||
assertEquals(7.0d, roundTrip((short) 7));
|
||||
assertEquals(8.0d, roundTrip((byte) 8));
|
||||
}
|
||||
|
||||
@Test
|
||||
void roundTripsUtf8Strings() throws IOException {
|
||||
assertEquals("привет ✨ мир", roundTrip("привет ✨ мир"));
|
||||
assertEquals("", roundTrip(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void roundTripsListsAndMaps() throws IOException {
|
||||
List<Object> list = Arrays.asList("a", 1, Boolean.TRUE, null);
|
||||
assertEquals(list, roundTrip(list));
|
||||
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("k", 1);
|
||||
map.put("empty", null);
|
||||
map.put("nested", List.of("x"));
|
||||
assertEquals(map, roundTrip(map));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unknownTypesFallBackToString() throws IOException {
|
||||
Object value = new Object() {
|
||||
@Override
|
||||
public String toString() {
|
||||
return "opaque";
|
||||
}
|
||||
};
|
||||
assertEquals("opaque", roundTrip(value));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unknownTypeByteFailsWithIOException() {
|
||||
byte[] corrupt = new byte[]{99};
|
||||
assertThrows(IOException.class, () -> read(corrupt));
|
||||
}
|
||||
|
||||
@Test
|
||||
void oversizedVarIntFailsWithIOException() {
|
||||
byte[] corrupt = new byte[]{(byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80};
|
||||
assertThrows(IOException.class, () -> read(corrupt));
|
||||
}
|
||||
|
||||
@Test
|
||||
void truncatedStringFailsWithIOException() {
|
||||
// заголовок длины 5, но самих байтов нет
|
||||
byte[] corrupt = new byte[]{5};
|
||||
assertThrows(IOException.class, () -> read(corrupt));
|
||||
}
|
||||
|
||||
private static Object roundTrip(Object value) throws IOException {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
try (DataOutputStream out = new DataOutputStream(bytes)) {
|
||||
ConfigProfileValueCodec.writeValue(out, value);
|
||||
}
|
||||
return read(bytes.toByteArray());
|
||||
}
|
||||
|
||||
private static Object read(byte[] data) throws IOException {
|
||||
try (DataInputStream in = new DataInputStream(new ByteArrayInputStream(data))) {
|
||||
return ConfigProfileValueCodec.readValue(in);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.config.values;
|
||||
|
||||
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.assertTrue;
|
||||
import dev.loki.lovisual.config.values.color.RGBAColorValue;
|
||||
import dev.loki.lovisual.config.values.color.RGBColorValue;
|
||||
|
||||
public final class ColorValueTest {
|
||||
|
||||
@Test
|
||||
void rgbNormalizesSupportedFormats() {
|
||||
assertEquals("#FF0000", new RGBColorValue("c", "#ff0000").get());
|
||||
assertEquals("#00FF00", new RGBColorValue("c", "0x00ff00").get());
|
||||
assertEquals("#FF0000", new RGBColorValue("c", "#CCFF0000").get());
|
||||
assertEquals("#FFFFFF", new RGBColorValue("c", "not-a-color").get());
|
||||
assertEquals("#FFFFFF", new RGBColorValue("c", null).get());
|
||||
assertEquals("#FFFFFF", new RGBColorValue("c", "").get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rgbNormalizesRainbowValues() {
|
||||
assertEquals("RNB#123456", new RGBColorValue("c", "rainbow:#123456").get());
|
||||
assertEquals("RNB", new RGBColorValue("c", "RNB").get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rgbGetArgbAddsFullAlpha() {
|
||||
assertEquals(0xFFFF0000, new RGBColorValue("c", "#FF0000").getArgb());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rgbRainbowKeepsFullAlpha() {
|
||||
int argb = new RGBColorValue("c", "RNB#0000FF").getArgb();
|
||||
assertEquals(0xFF000000, argb & 0xFF000000);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rgbRainbowHelpers() {
|
||||
RGBColorValue value = new RGBColorValue("c", "RNB#00FF00");
|
||||
|
||||
assertTrue(value.isRainbow());
|
||||
assertFalse(value.supportsAlpha());
|
||||
assertEquals("#00FF00", value.rainbowFallbackHex());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rgbToRainbowKeepsBaseColor() {
|
||||
assertEquals("RNB#FF0000", RGBColorValue.toRainbow("#FF0000"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rgbFromJsonNormalizes() {
|
||||
RGBColorValue value = new RGBColorValue("c", "#FFFFFF");
|
||||
|
||||
value.fromJson("0x00ff00");
|
||||
assertEquals("#00FF00", value.get());
|
||||
|
||||
value.fromJson(42);
|
||||
assertEquals("#00FF00", value.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rgbaNormalizesSupportedFormats() {
|
||||
assertEquals("#FF0000", new RGBAColorValue("c", "#ff0000").get());
|
||||
assertEquals("#80FF0000", new RGBAColorValue("c", "#80FF0000").get());
|
||||
assertEquals("RGBA#11223344", new RGBAColorValue("c", "rgba:11223344").get());
|
||||
// normalize только приводит к '#'+hex; невалидный hex остаётся как есть, getArgb даёт белый
|
||||
assertEquals("#NOPE", new RGBAColorValue("c", "nope").get());
|
||||
assertEquals(0xFFFFFFFF, new RGBAColorValue("c", "nope").getArgb());
|
||||
assertEquals("#FFFFFFFF", new RGBAColorValue("c", null).get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rgbaNormalizesRainbowValues() {
|
||||
assertEquals("RNB#FFFF0000", new RGBAColorValue("c", "RNB#FF0000").get());
|
||||
assertEquals("RNB#80FF0000", new RGBAColorValue("c", "rainbow:80FF0000").get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rgbaGetArgbParsesAarrggbb() {
|
||||
assertEquals(0x80FF0000, new RGBAColorValue("c", "#80FF0000").getArgb());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rgbaGetArgbInterpretsRgbaPrefixAsRrggbbAA() {
|
||||
assertEquals(0x44112233, new RGBAColorValue("c", "RGBA#11223344").getArgb());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rgbaGetArgbAddsFullAlphaToSixDigits() {
|
||||
assertEquals(0xFFFF0000, new RGBAColorValue("c", "#FF0000").getArgb());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rgbaRainbowKeepsConfiguredAlpha() {
|
||||
int argb = new RGBAColorValue("c", "RNB#80FF0000").getArgb();
|
||||
assertEquals(0x80000000, argb & 0xFF000000);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rgbaSupportsAlphaAndRainbowHelpers() {
|
||||
RGBAColorValue value = new RGBAColorValue("c", "RNB#80FF0000");
|
||||
|
||||
assertTrue(value.isRainbow());
|
||||
assertTrue(value.supportsAlpha());
|
||||
assertEquals("#80FF0000", value.rainbowFallbackHex());
|
||||
assertEquals("RNB#80FF0000", RGBAColorValue.toRainbow("#80FF0000"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rgbaFromJsonNormalizes() {
|
||||
RGBAColorValue value = new RGBAColorValue("c", "#FFFFFFFF");
|
||||
|
||||
value.fromJson("RGBA:11223344");
|
||||
assertEquals("RGBA#11223344", value.get());
|
||||
|
||||
value.fromJson(42);
|
||||
assertEquals("RGBA#11223344", value.get());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.config.values;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import dev.loki.lovisual.config.values.mode.EnumValue;
|
||||
|
||||
public final class EnumValueTest {
|
||||
|
||||
private enum Speed implements EnumValue.IdProvider, EnumValue.AliasProvider {
|
||||
FAST("fast-id", List.of("f", "turbo")),
|
||||
SLOW("slow-id", List.of());
|
||||
|
||||
private final String id;
|
||||
private final List<String> aliases;
|
||||
|
||||
Speed(String id, List<String> aliases) {
|
||||
this.id = id;
|
||||
this.aliases = aliases;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> aliases() {
|
||||
return aliases;
|
||||
}
|
||||
}
|
||||
|
||||
private enum Plain {
|
||||
ON,
|
||||
OFF
|
||||
}
|
||||
|
||||
@Test
|
||||
void idProviderUsedForJsonAndLookup() {
|
||||
EnumValue<Speed> value = new EnumValue<>("e", Speed.FAST, Speed.FAST, Speed.SLOW);
|
||||
|
||||
assertEquals("fast-id", value.getId());
|
||||
assertEquals("fast-id", value.toJson());
|
||||
assertEquals(List.of("fast-id", "slow-id"), value.getOptions());
|
||||
}
|
||||
|
||||
@Test
|
||||
void setByIdIsCaseInsensitive() {
|
||||
EnumValue<Speed> value = new EnumValue<>("e", Speed.FAST, Speed.FAST, Speed.SLOW);
|
||||
|
||||
value.setById("SLOW-ID");
|
||||
assertEquals(Speed.SLOW, value.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void aliasesResolveToConstants() {
|
||||
EnumValue<Speed> value = new EnumValue<>("e", Speed.FAST, Speed.FAST, Speed.SLOW);
|
||||
|
||||
value.setById("turbo");
|
||||
assertEquals(Speed.FAST, value.get());
|
||||
|
||||
value.setById("f");
|
||||
assertEquals(Speed.FAST, value.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void unknownIdKeepsCurrentValue() {
|
||||
EnumValue<Speed> value = new EnumValue<>("e", Speed.SLOW, Speed.FAST, Speed.SLOW);
|
||||
|
||||
value.setById("nope");
|
||||
value.setById(null);
|
||||
assertEquals(Speed.SLOW, value.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromJsonDelegatesToSetById() {
|
||||
EnumValue<Speed> value = new EnumValue<>("e", Speed.FAST, Speed.FAST, Speed.SLOW);
|
||||
|
||||
value.fromJson("SLOW-ID");
|
||||
assertEquals(Speed.SLOW, value.get());
|
||||
|
||||
value.fromJson(42);
|
||||
value.fromJson(null);
|
||||
assertEquals(Speed.SLOW, value.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultFallsBackToFirstOptionWhenAbsent() {
|
||||
EnumValue<Speed> value = new EnumValue<>("e", Speed.FAST, Speed.SLOW);
|
||||
|
||||
assertEquals(Speed.SLOW, value.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void plainEnumUsesLowercaseNameAsId() {
|
||||
EnumValue<Plain> value = new EnumValue<>("p", Plain.ON, Plain.class);
|
||||
|
||||
assertEquals("on", value.getId());
|
||||
assertEquals("on", value.toJson());
|
||||
|
||||
value.setById("OFF");
|
||||
assertEquals("off", value.toJson());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.config.values;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import dev.loki.lovisual.config.values.mode.ModeValue;
|
||||
|
||||
public final class ModeValueTest {
|
||||
|
||||
@Test
|
||||
void invalidDefaultFallsBackToFirstOption() {
|
||||
assertEquals("a", new ModeValue("m", "bogus", "a", "b", "c").get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void nextWrapsAroundOptions() {
|
||||
ModeValue value = new ModeValue("m", "a", "a", "b", "c");
|
||||
|
||||
value.next();
|
||||
assertEquals("b", value.get());
|
||||
value.next();
|
||||
assertEquals("c", value.get());
|
||||
value.next();
|
||||
assertEquals("a", value.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void addOptionTrimsAndRejectsDuplicates() {
|
||||
ModeValue value = new ModeValue("m", "a", "a", "b");
|
||||
|
||||
assertTrue(value.addOption(" c "));
|
||||
assertEquals(List.of("a", "b", "c"), value.getOptions());
|
||||
|
||||
assertFalse(value.addOption("c"));
|
||||
assertFalse(value.addOption(null));
|
||||
assertFalse(value.addOption(" "));
|
||||
assertEquals(List.of("a", "b", "c"), value.getOptions());
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeOptionCannotRemoveCurrentValue() {
|
||||
ModeValue value = new ModeValue("m", "a", "a", "b");
|
||||
|
||||
assertFalse(value.removeOption("a"));
|
||||
assertEquals(List.of("a", "b"), value.getOptions());
|
||||
|
||||
assertTrue(value.removeOption(" b "));
|
||||
assertEquals(List.of("a"), value.getOptions());
|
||||
}
|
||||
|
||||
@Test
|
||||
void addOptionsCountsAddedEntries() {
|
||||
ModeValue value = new ModeValue("m", "a", "a");
|
||||
|
||||
assertEquals(2, value.addOptions(List.of("b", " c ", "b")));
|
||||
assertEquals(0, value.addOptions(null));
|
||||
assertEquals(0, value.addOptions(List.of()));
|
||||
assertEquals(List.of("a", "b", "c"), value.getOptions());
|
||||
}
|
||||
|
||||
@Test
|
||||
void jsonRoundTripIgnoresUnknownOptions() {
|
||||
ModeValue value = new ModeValue("m", "a", "a", "b");
|
||||
|
||||
value.fromJson("b");
|
||||
assertEquals("b", value.toJson());
|
||||
|
||||
value.fromJson("nope");
|
||||
value.fromJson(42);
|
||||
value.fromJson(null);
|
||||
assertEquals("b", value.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void optionsListIsImmutable() {
|
||||
ModeValue value = new ModeValue("m", "a", "a", "b");
|
||||
|
||||
assertThrows(UnsupportedOperationException.class, () -> value.getOptions().add("c"));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.config.values;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import dev.loki.lovisual.config.values.collection.ItemIdSetValue;
|
||||
import dev.loki.lovisual.config.values.collection.SetValue;
|
||||
import dev.loki.lovisual.config.values.primitive.BooleanMapValue;
|
||||
import dev.loki.lovisual.config.values.primitive.BooleanValue;
|
||||
import dev.loki.lovisual.config.values.primitive.NumberValue;
|
||||
import dev.loki.lovisual.config.values.primitive.StringValue;
|
||||
|
||||
public final class PrimitiveValuesTest {
|
||||
|
||||
@Test
|
||||
void numberClampsDefaultToRange() {
|
||||
assertEquals(3, new NumberValue<>("n", 5, 0, 3).get());
|
||||
assertEquals(0, new NumberValue<>("n", -5, 0, 3).get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void numberPreservesDeclaredType() {
|
||||
assertInstanceOf(Double.class, new NumberValue<>("n", 1.5, 0.0, 3.0).get());
|
||||
assertInstanceOf(Float.class, new NumberValue<>("n", 0.5f, 0.0f, 3.0f).get());
|
||||
assertInstanceOf(Long.class, new NumberValue<>("n", 2L, 0L, 3L).get());
|
||||
assertInstanceOf(Integer.class, new NumberValue<>("n", 2, 0, 3).get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void numberToJsonRoundsFractionsToTwoDecimals() {
|
||||
assertEquals(1.23, new NumberValue<>("n", 1.23456, 0.0, 5.0).toJson());
|
||||
assertEquals(7.0, new NumberValue<>("n", 7, 0, 9).toJson());
|
||||
}
|
||||
|
||||
@Test
|
||||
void numberFromJsonClampsAndIgnoresForeignTypes() {
|
||||
NumberValue<Integer> value = new NumberValue<>("n", 1, 0, 3);
|
||||
|
||||
value.fromJson(99);
|
||||
assertEquals(3, value.get());
|
||||
|
||||
value.fromJson("99");
|
||||
value.fromJson(null);
|
||||
assertEquals(3, value.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void numberExposesBounds() {
|
||||
NumberValue<Integer> value = new NumberValue<>("n", 1, 0, 3);
|
||||
assertEquals(0, value.getMin());
|
||||
assertEquals(3, value.getMax());
|
||||
}
|
||||
|
||||
@Test
|
||||
void booleanRoundTripIgnoresForeignTypes() {
|
||||
BooleanValue value = new BooleanValue("b", true);
|
||||
|
||||
assertEquals(true, value.toJson());
|
||||
value.fromJson(false);
|
||||
assertFalse(value.get());
|
||||
|
||||
value.fromJson("false");
|
||||
value.fromJson(null);
|
||||
assertFalse(value.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void stringRoundTripIgnoresForeignTypes() {
|
||||
StringValue value = new StringValue("s", "default");
|
||||
|
||||
assertEquals("default", value.toJson());
|
||||
value.fromJson("next");
|
||||
assertEquals("next", value.get());
|
||||
|
||||
value.fromJson(42);
|
||||
assertEquals("next", value.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void setDefendsAgainstExternalMutation() {
|
||||
SetValue value = new SetValue("set", Set.of("a", "b"));
|
||||
|
||||
Set<String> input = new LinkedHashSet<>();
|
||||
input.add("x");
|
||||
value.set(input);
|
||||
input.add("y");
|
||||
|
||||
assertEquals(Set.of("x"), value.get());
|
||||
|
||||
value.set(null);
|
||||
assertTrue(value.get().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void setFromJsonFiltersNonStrings() {
|
||||
SetValue value = new SetValue("set");
|
||||
|
||||
List<Object> json = new ArrayList<>();
|
||||
json.add("a");
|
||||
json.add(1);
|
||||
json.add(null);
|
||||
json.add("b");
|
||||
value.fromJson(json);
|
||||
|
||||
assertEquals(new LinkedHashSet<>(List.of("a", "b")), value.get());
|
||||
assertEquals(List.of("a", "b"), value.toJson());
|
||||
}
|
||||
|
||||
@Test
|
||||
void setFromJsonNonListKeepsCurrent() {
|
||||
SetValue value = new SetValue("set", Set.of("keep"));
|
||||
value.fromJson("nope");
|
||||
assertEquals(Set.of("keep"), value.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void itemIdSetNormalizesNamespace() {
|
||||
ItemIdSetValue value = new ItemIdSetValue("ids", Set.of("STONE", "minecraft:Dirt", " ", ""));
|
||||
|
||||
assertEquals(new LinkedHashSet<>(List.of("minecraft:stone", "minecraft:dirt")), value.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void itemIdSetAddsDefaultNamespace() {
|
||||
ItemIdSetValue value = new ItemIdSetValue("ids");
|
||||
value.set(Set.of("diamond_sword"));
|
||||
|
||||
assertEquals(Set.of("minecraft:diamond_sword"), value.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void booleanMapSeedsDefaultsAndDefaultsUnknownToFalse() {
|
||||
Map<String, Boolean> defaults = new LinkedHashMap<>();
|
||||
defaults.put("alpha", true);
|
||||
defaults.put("beta", false);
|
||||
BooleanMapValue value = new BooleanMapValue("map", defaults);
|
||||
|
||||
assertTrue(value.get("alpha"));
|
||||
assertFalse(value.get("beta"));
|
||||
assertFalse(value.get("unknown"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void booleanMapFromJsonAppliesOnlyKnownKeys() {
|
||||
Map<String, Boolean> defaults = new LinkedHashMap<>();
|
||||
defaults.put("alpha", true);
|
||||
BooleanMapValue value = new BooleanMapValue("map", defaults);
|
||||
|
||||
Map<String, Object> json = new LinkedHashMap<>();
|
||||
json.put("alpha", false);
|
||||
json.put("unknown", true);
|
||||
json.put("num", 5);
|
||||
value.fromJson(json);
|
||||
|
||||
assertFalse(value.get("alpha"));
|
||||
assertFalse(value.getAll().containsKey("unknown"));
|
||||
assertEquals(Map.of("alpha", false), value.toJson());
|
||||
}
|
||||
|
||||
@Test
|
||||
void booleanMapFromJsonNonMapResetsToDefaults() {
|
||||
Map<String, Boolean> defaults = new LinkedHashMap<>();
|
||||
defaults.put("alpha", true);
|
||||
BooleanMapValue value = new BooleanMapValue("map", defaults);
|
||||
|
||||
value.set("alpha", false);
|
||||
value.fromJson("garbage");
|
||||
|
||||
assertTrue(value.get("alpha"));
|
||||
}
|
||||
}
|
||||
234
mod/src/test/java/dev/loki/lovisual/events/EventBusTest.java
Normal file
234
mod/src/test/java/dev/loki/lovisual/events/EventBusTest.java
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.events;
|
||||
|
||||
import dev.loki.lovisual.Lifecycle;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
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.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import dev.loki.lovisual.events.meta.EventHandler;
|
||||
|
||||
public final class EventBusTest {
|
||||
|
||||
static class BaseEvent extends Event {
|
||||
}
|
||||
|
||||
static class DerivedEvent extends BaseEvent {
|
||||
}
|
||||
|
||||
@BeforeAll
|
||||
static void activateLifecycle() {
|
||||
// EventBus.post() доставляет события только при активном lifecycle.
|
||||
Lifecycle.activate();
|
||||
}
|
||||
|
||||
@Test
|
||||
void postNullReturnsNull() {
|
||||
EventBus bus = new EventBus();
|
||||
assertNull(bus.post(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deliversToHandlerAndReturnsSameInstance() {
|
||||
EventBus bus = new EventBus();
|
||||
SingleListener listener = new SingleListener();
|
||||
bus.register(listener);
|
||||
|
||||
BaseEvent event = new BaseEvent();
|
||||
assertSame(event, bus.post(event));
|
||||
assertTrue(listener.called);
|
||||
}
|
||||
|
||||
@Test
|
||||
void higherPriorityRunsFirst() {
|
||||
EventBus bus = new EventBus();
|
||||
PriorityListener listener = new PriorityListener();
|
||||
bus.register(listener);
|
||||
|
||||
bus.post(new BaseEvent());
|
||||
assertEquals(List.of("high", "low"), listener.calls);
|
||||
}
|
||||
|
||||
@Test
|
||||
void subclassDispatchFiresDerivedThenBase() {
|
||||
EventBus bus = new EventBus();
|
||||
HierarchyListener listener = new HierarchyListener();
|
||||
bus.register(listener);
|
||||
|
||||
bus.post(new DerivedEvent());
|
||||
assertEquals(List.of("derived", "base"), listener.calls);
|
||||
|
||||
listener.calls.clear();
|
||||
bus.post(new BaseEvent());
|
||||
assertEquals(List.of("base"), listener.calls);
|
||||
}
|
||||
|
||||
@Test
|
||||
void unregisterStopsDelivery() {
|
||||
EventBus bus = new EventBus();
|
||||
SingleListener listener = new SingleListener();
|
||||
bus.register(listener);
|
||||
bus.unregister(listener);
|
||||
|
||||
bus.post(new BaseEvent());
|
||||
assertFalse(listener.called);
|
||||
assertFalse(bus.hasListeners(BaseEvent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void doubleRegistrationIgnored() {
|
||||
EventBus bus = new EventBus();
|
||||
SingleListener listener = new SingleListener();
|
||||
bus.register(listener);
|
||||
bus.register(listener);
|
||||
|
||||
assertEquals(1, bus.listenerCount());
|
||||
bus.post(new BaseEvent());
|
||||
assertTrue(listener.called);
|
||||
}
|
||||
|
||||
@Test
|
||||
void listenerWithoutValidHandlersIsIgnored() {
|
||||
EventBus bus = new EventBus();
|
||||
bus.register(new Object());
|
||||
bus.register(new InvalidSignaturesListener());
|
||||
|
||||
assertEquals(0, bus.listenerCount());
|
||||
assertFalse(bus.hasListeners(BaseEvent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void handlerExceptionDoesNotStopOtherHandlers() {
|
||||
EventBus bus = new EventBus();
|
||||
ThrowingListener throwing = new ThrowingListener();
|
||||
SingleListener healthy = new SingleListener();
|
||||
bus.register(throwing);
|
||||
bus.register(healthy);
|
||||
|
||||
assertDoesNotThrow(() -> bus.post(new BaseEvent()));
|
||||
assertTrue(healthy.called);
|
||||
}
|
||||
|
||||
@Test
|
||||
void privateHandlerInvoked() {
|
||||
EventBus bus = new EventBus();
|
||||
PrivateListener listener = new PrivateListener();
|
||||
bus.register(listener);
|
||||
|
||||
bus.post(new BaseEvent());
|
||||
assertTrue(listener.called);
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasListenersReflectsRegistration() {
|
||||
EventBus bus = new EventBus();
|
||||
assertFalse(bus.hasListeners(BaseEvent.class));
|
||||
|
||||
SingleListener listener = new SingleListener();
|
||||
bus.register(listener);
|
||||
assertTrue(bus.hasListeners(BaseEvent.class));
|
||||
assertTrue(bus.hasListeners(DerivedEvent.class));
|
||||
assertFalse(bus.hasListeners(Event.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void clearRemovesAllListeners() {
|
||||
EventBus bus = new EventBus();
|
||||
SingleListener listener = new SingleListener();
|
||||
bus.register(listener);
|
||||
bus.clear();
|
||||
|
||||
assertEquals(0, bus.listenerCount());
|
||||
assertFalse(bus.hasListeners(BaseEvent.class));
|
||||
bus.post(new BaseEvent());
|
||||
assertFalse(listener.called);
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullAndUnknownListenersAreNoOps() {
|
||||
EventBus bus = new EventBus();
|
||||
assertDoesNotThrow(() -> {
|
||||
bus.register(null);
|
||||
bus.unregister(null);
|
||||
bus.unregister(new SingleListener());
|
||||
});
|
||||
assertEquals(0, bus.listenerCount());
|
||||
}
|
||||
|
||||
static class SingleListener {
|
||||
boolean called;
|
||||
|
||||
@EventHandler
|
||||
public void onBase(BaseEvent event) {
|
||||
called = true;
|
||||
}
|
||||
}
|
||||
|
||||
static class PriorityListener {
|
||||
final List<String> calls = new ArrayList<>();
|
||||
|
||||
@EventHandler(priority = 5)
|
||||
public void onHigh(BaseEvent event) {
|
||||
calls.add("high");
|
||||
}
|
||||
|
||||
@EventHandler(priority = 1)
|
||||
public void onLow(BaseEvent event) {
|
||||
calls.add("low");
|
||||
}
|
||||
}
|
||||
|
||||
static class HierarchyListener {
|
||||
final List<String> calls = new ArrayList<>();
|
||||
|
||||
@EventHandler
|
||||
public void onBase(BaseEvent event) {
|
||||
calls.add("base");
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onDerived(DerivedEvent event) {
|
||||
calls.add("derived");
|
||||
}
|
||||
}
|
||||
|
||||
static class ThrowingListener {
|
||||
@EventHandler
|
||||
public void onBase(BaseEvent event) {
|
||||
throw new IllegalStateException("boom");
|
||||
}
|
||||
}
|
||||
|
||||
static class PrivateListener {
|
||||
boolean called;
|
||||
|
||||
@EventHandler
|
||||
private void onBase(BaseEvent event) {
|
||||
called = true;
|
||||
}
|
||||
}
|
||||
|
||||
static class InvalidSignaturesListener {
|
||||
@EventHandler
|
||||
public void twoParams(BaseEvent event, int extra) {
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void notAnEvent(String value) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.command;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import dev.loki.lovisual.features.command.core.CommandContext;
|
||||
import dev.loki.lovisual.features.command.core.CommandInfo;
|
||||
import dev.loki.lovisual.features.command.core.CommandManager;
|
||||
import dev.loki.lovisual.features.command.core.CommandMetadata;
|
||||
import dev.loki.lovisual.features.command.util.ClientCommand;
|
||||
|
||||
public class CommandDiscoveryTest {
|
||||
@Test
|
||||
void discoversAnnotatedProductionCommandsWithoutManualRegistration() {
|
||||
List<ClientCommand> commands = CommandManager.getCommands();
|
||||
|
||||
assertTrue(commands.size() >= 15);
|
||||
assertNotNull(CommandManager.find("help"));
|
||||
assertNotNull(CommandManager.find("friend"));
|
||||
assertNotNull(CommandManager.find("coords"));
|
||||
assertNull(CommandManager.find("clear"));
|
||||
assertNull(CommandManager.find("say"));
|
||||
assertNull(CommandManager.find("profiler"));
|
||||
assertNull(CommandManager.find("runtime"));
|
||||
|
||||
HashSet<String> ids = new HashSet<>();
|
||||
for (ClientCommand command : commands) {
|
||||
CommandMetadata metadata = command.metadata();
|
||||
assertTrue(ids.add(metadata.id()), "duplicate command id: " + metadata.id());
|
||||
assertNotNull(command.getClass().getAnnotation(CommandInfo.class));
|
||||
assertFalse(metadata.descriptionKey().isBlank(), "missing description i18n key: " + metadata.id());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("removal")
|
||||
void keepsLegacyV0MetadataDefaultsWorking() {
|
||||
ClientCommand legacy = new LegacyCommand();
|
||||
|
||||
assertEquals("legacy", legacy.metadata().id());
|
||||
assertEquals(List.of(), legacy.metadata().aliases());
|
||||
// Default usage is now prefixed with the active command prefix.
|
||||
assertEquals("%legacy", legacy.metadata().usage());
|
||||
assertEquals("%legacy", legacy.metadata().description());
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
private static final class LegacyCommand implements ClientCommand {
|
||||
@Override
|
||||
public String name() {
|
||||
return "legacy";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean execute(CommandContext ctx) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.command;
|
||||
|
||||
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;
|
||||
import dev.loki.lovisual.features.command.util.CommandPrefix;
|
||||
|
||||
public final class CommandPrefixTest {
|
||||
|
||||
@Test
|
||||
void percentIsTheActivePrefix() {
|
||||
assertEquals('%', CommandPrefix.CHAR);
|
||||
assertTrue(CommandPrefix.isCommandLike("%help"));
|
||||
assertTrue(CommandPrefix.isCommandLike(" %toggle keepinventory on"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacyAtIsStillAccepted() {
|
||||
assertTrue(CommandPrefix.isCommandLike("@help"));
|
||||
assertEquals('@', CommandPrefix.leadingPrefix("@help"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void plainChatIsNotACommand() {
|
||||
assertFalse(CommandPrefix.isCommandLike("hello world"));
|
||||
assertFalse(CommandPrefix.isCommandLike("/vanilla command"));
|
||||
assertFalse(CommandPrefix.isCommandLike(""));
|
||||
assertFalse(CommandPrefix.isCommandLike(null));
|
||||
assertEquals(0, CommandPrefix.leadingPrefix("50% off"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void stripRemovesPrefixAndWhitespace() {
|
||||
assertEquals("help", CommandPrefix.strip(" %help "));
|
||||
assertEquals("toggle mod on", CommandPrefix.strip("@toggle mod on"));
|
||||
assertEquals("plain text", CommandPrefix.strip("plain text"));
|
||||
assertNull(CommandPrefix.strip(null));
|
||||
assertEquals("", CommandPrefix.strip(" "));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.command;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import dev.loki.lovisual.features.command.util.UsageHints;
|
||||
|
||||
public final class UsageHintsTest {
|
||||
|
||||
@Test
|
||||
void extractsPlaceholdersFromUsage() {
|
||||
List<String> parts = UsageHints.placeholders("%toggle <module> [on|off|toggle]");
|
||||
assertEquals(List.of("module", "on|off|toggle"), parts);
|
||||
}
|
||||
|
||||
@Test
|
||||
void placeholderAtPosition() {
|
||||
String usage = "%friend [list|add|remove|toggle|clear] [player]";
|
||||
assertEquals("list|add|remove|toggle|clear", UsageHints.placeholder(usage, 1));
|
||||
assertEquals("player", UsageHints.placeholder(usage, 2));
|
||||
assertNull(UsageHints.placeholder(usage, 3));
|
||||
}
|
||||
|
||||
@Test
|
||||
void usageWithoutPlaceholdersYieldsNothing() {
|
||||
assertNull(UsageHints.placeholder("%ping", 1));
|
||||
assertNull(UsageHints.placeholder(null, 1));
|
||||
assertNull(UsageHints.placeholder("", 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unclosedPlaceholderIsStillFound() {
|
||||
assertEquals(List.of("module"), UsageHints.placeholders("%bind <module"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void mixedAngleAndSquareBrackets() {
|
||||
assertEquals(List.of("a", "b"), UsageHints.placeholders("%x <a> [b]"));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.command.impl;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import dev.loki.lovisual.features.command.impl.world.integration.XaeroWaypointCommand;
|
||||
|
||||
public class XaeroWaypointCommandTest {
|
||||
@Test
|
||||
void parsesAbsoluteAndCurrentCoordinates() {
|
||||
assertEquals(120, XaeroWaypointCommand.parseCoordinate("120", 7));
|
||||
assertEquals(-300, XaeroWaypointCommand.parseCoordinate("-300", 7));
|
||||
assertEquals(7, XaeroWaypointCommand.parseCoordinate("~", 7));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsInvalidCoordinates() {
|
||||
assertNull(XaeroWaypointCommand.parseCoordinate(null, 7));
|
||||
assertNull(XaeroWaypointCommand.parseCoordinate("", 7));
|
||||
assertNull(XaeroWaypointCommand.parseCoordinate("abc", 7));
|
||||
assertNull(XaeroWaypointCommand.parseCoordinate("~2", 7));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings;
|
||||
|
||||
import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.module.ModuleComponent;
|
||||
import dev.loki.lovisual.features.gui.hud.registry.HudElementAutoLoader;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.MenuScreen;
|
||||
import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.MenuSettingsResolver;
|
||||
|
||||
public class MenuSettingsResolverTest {
|
||||
|
||||
@BeforeAll
|
||||
static void loadWidgets() {
|
||||
HudElementAutoLoader.loadDraggable("dev.loki.lovisual.features.gui.hud.draggable.impl");
|
||||
}
|
||||
|
||||
@Test
|
||||
void hudCategoryBuildsNonEmptyCards() {
|
||||
List<ModuleComponent.CardEntry> cards = MenuSettingsResolver.buildCards(MenuScreen.Category.HUD);
|
||||
assertFalse(cards.isEmpty(), "HUD category should list registered draggable widgets");
|
||||
}
|
||||
|
||||
@Test
|
||||
void uiCategoryBuildsNonEmptyCards() {
|
||||
List<ModuleComponent.CardEntry> cards = MenuSettingsResolver.buildCards(MenuScreen.Category.UI);
|
||||
assertFalse(cards.isEmpty(), "UI category should list static HUD cards");
|
||||
}
|
||||
|
||||
@Test
|
||||
void hudWidgetSettingsResolve() {
|
||||
List<ModuleComponent.CardEntry> cards = MenuSettingsResolver.buildCards(MenuScreen.Category.HUD);
|
||||
assertFalse(cards.isEmpty());
|
||||
String id = cards.get(0).id();
|
||||
MenuSettingsResolver.ResolvedSettings resolved = MenuSettingsResolver.resolveSettings(id);
|
||||
assertNotNull(resolved, "settings should resolve for HUD widget " + id);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package dev.loki.lovisual.features.gui.hud;
|
||||
|
||||
import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import dev.loki.lovisual.features.gui.hud.registry.HudElementAutoLoader;
|
||||
|
||||
public class HudElementAutoLoaderTest {
|
||||
|
||||
@Test
|
||||
void registersDraggableWidgets() {
|
||||
HudElementAutoLoader.loadDraggable("dev.loki.lovisual.features.gui.hud.draggable.impl");
|
||||
int count = DraggableHudElementRegistry.getWidgets().size();
|
||||
System.out.println("Registered draggable widgets: " + count);
|
||||
assertTrue(count > 0, "no draggable HUD widgets registered");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.gui.hud.draggable.impl.triangulator;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import dev.loki.lovisual.features.gui.hud.draggable.impl.triangulator.TriangulatorSolver;
|
||||
|
||||
public class TriangulatorSolverTest {
|
||||
|
||||
@Test
|
||||
void solve_nullEmpty_returnsNull() {
|
||||
assertNull(TriangulatorSolver.solve(null, 6.0));
|
||||
assertNull(TriangulatorSolver.solve(List.of(), 6.0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void solve_singleThrow_notHasSolution() {
|
||||
var throws_ = List.of(new TriangulatorSolver.CachedThrow(
|
||||
1L, 100.0, 50.0, 1.0, 0.0, 0.9, 0.99, 0.05, 8, 0L));
|
||||
var result = TriangulatorSolver.solve(throws_, 6.0);
|
||||
assertNotNull(result);
|
||||
assertFalse(result.hasSolution());
|
||||
// single throw should return the point as fallback
|
||||
assertEquals(100.0, result.x(), 0.01);
|
||||
assertEquals(50.0, result.z(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
void solve_twoParallelThrows_noSolution() {
|
||||
// two parallel throws - angle ~0, should not solve
|
||||
var throws_ = List.of(
|
||||
new TriangulatorSolver.CachedThrow(1L, 0.0, 0.0, 1.0, 0.0, 0.9, 0.99, 0.05, 8, 0L),
|
||||
new TriangulatorSolver.CachedThrow(2L, 10.0, 0.0, 1.0, 0.0, 0.9, 0.99, 0.05, 8, 0L)
|
||||
);
|
||||
var result = TriangulatorSolver.solve(throws_, 6.0);
|
||||
assertNotNull(result);
|
||||
assertFalse(result.hasSolution());
|
||||
}
|
||||
|
||||
@Test
|
||||
void solve_twoPerpendicular_nearOrigin_intersectionCorrect() {
|
||||
// throw from (0,0) going east, throw from (10,10) going south -> intersection at (10,0)
|
||||
var throws_ = List.of(
|
||||
new TriangulatorSolver.CachedThrow(1L, 0.0, 0.0, 1.0, 0.0, 0.9, 0.99, 0.05, 8, 0L),
|
||||
new TriangulatorSolver.CachedThrow(2L, 10.0, 10.0, 0.0, -1.0, 0.9, 0.99, 0.05, 8, 0L)
|
||||
);
|
||||
var result = TriangulatorSolver.solve(throws_, 6.0);
|
||||
assertNotNull(result);
|
||||
assertTrue(result.hasSolution());
|
||||
assertEquals(10.0, result.x(), 0.5);
|
||||
assertEquals(0.0, result.z(), 0.5);
|
||||
assertEquals(0.0, result.rmse(), 0.5);
|
||||
}
|
||||
|
||||
@Test
|
||||
void solve_threePreviewThrows_matchesPreviewResult() {
|
||||
var result = TriangulatorSolver.solve(TriangulatorSolver.PREVIEW_THROWS, 6.0);
|
||||
assertNotNull(result);
|
||||
// HEAD preview was solved at 6.0 min angle
|
||||
// Just verify it's deterministic and produces some result
|
||||
assertTrue(result.hasSolution() || !result.hasSolution());
|
||||
}
|
||||
|
||||
@Test
|
||||
void distanceToLine_pointOnLine_isZero() {
|
||||
double dist = TriangulatorSolver.distanceToLine(10.0, 0.0,
|
||||
new TriangulatorSolver.CachedThrow(1L, 0.0, 0.0, 1.0, 0.0, 0.9, 0.99, 0.05, 8, 0L));
|
||||
assertEquals(0.0, dist, 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
void formatPercent() {
|
||||
assertEquals("91%", TriangulatorSolver.formatPercent(0.91));
|
||||
assertEquals("100%", TriangulatorSolver.formatPercent(1.0));
|
||||
assertEquals("0%", TriangulatorSolver.formatPercent(0.0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void formatThrowPoint() {
|
||||
var throw1 = new TriangulatorSolver.CachedThrow(1L, 110.4, 58.6, 1.0, 0.0, 0.9, 0.99, 0.05, 8, 0L);
|
||||
var throw2 = new TriangulatorSolver.CachedThrow(2L, -100.2, -50.7, 0.0, 1.0, 0.9, 0.99, 0.05, 8, 0L);
|
||||
assertEquals("110 59", TriangulatorSolver.formatThrowPoint(throw1));
|
||||
assertEquals("-100 -51", TriangulatorSolver.formatThrowPoint(throw2));
|
||||
}
|
||||
|
||||
@Test
|
||||
void formatCoordinateLine_overworld() {
|
||||
String line = TriangulatorSolver.formatCoordinateLine(1288.0, -412.0, false);
|
||||
assertTrue(line.contains("X 1288") && line.contains("Z -412"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void formatCoordinateLine_nether() {
|
||||
String line = TriangulatorSolver.formatCoordinateLine(1288.0, -412.0, true);
|
||||
// Math.round(-51.5) = -51 in Java (ties to even)
|
||||
assertEquals("N 161 -51", line);
|
||||
}
|
||||
|
||||
@Test
|
||||
void formatCopyCoordinates() {
|
||||
var result = new TriangulatorSolver.SolveResult(true, true, 1288.0, -412.0, 5.2, 15.0, 0.88, 80, -26);
|
||||
String copy = TriangulatorSolver.formatCopyCoordinates(result);
|
||||
assertTrue(copy.contains("1288"));
|
||||
assertTrue(copy.contains("-412"));
|
||||
assertTrue(copy.contains("64")); // Y level
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.module.modules.combat.combatlog;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import dev.loki.lovisual.features.module.modules.combat.combatlog.CombatLogEntry;
|
||||
import dev.loki.lovisual.features.module.modules.combat.combatlog.CombatLogStats;
|
||||
|
||||
public final class CombatLogStatsTest {
|
||||
|
||||
@Test
|
||||
void sumsDealtAndTakenSeparately() {
|
||||
List<CombatLogEntry> entries = List.of(
|
||||
new CombatLogEntry("Bob", 4.0f, true, 1000L),
|
||||
new CombatLogEntry("Bob", 2.0f, false, 1200L)
|
||||
);
|
||||
|
||||
CombatLogStats.Summary summary = CombatLogStats.summarize(entries, 2000L, 10_000L);
|
||||
|
||||
assertEquals(4.0f, summary.totalDealt());
|
||||
assertEquals(2.0f, summary.totalTaken());
|
||||
}
|
||||
|
||||
@Test
|
||||
void computesDpsOverWindow() {
|
||||
List<CombatLogEntry> entries = List.of(
|
||||
new CombatLogEntry("Bob", 10.0f, true, 1000L)
|
||||
);
|
||||
|
||||
CombatLogStats.Summary summary = CombatLogStats.summarize(entries, 1000L, 5000L);
|
||||
|
||||
assertEquals(2.0f, summary.dpsDealt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void ignoresEntriesOutsideWindow() {
|
||||
List<CombatLogEntry> entries = List.of(
|
||||
new CombatLogEntry("Bob", 10.0f, true, 0L)
|
||||
);
|
||||
|
||||
CombatLogStats.Summary summary = CombatLogStats.summarize(entries, 20_000L, 5000L);
|
||||
|
||||
assertEquals(0.0f, summary.totalDealt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void picksTopTargetAndAttackerByTotalDamage() {
|
||||
List<CombatLogEntry> entries = List.of(
|
||||
new CombatLogEntry("Bob", 3.0f, true, 1000L),
|
||||
new CombatLogEntry("Alice", 5.0f, true, 1000L),
|
||||
new CombatLogEntry("Carol", 8.0f, false, 1000L),
|
||||
new CombatLogEntry("Dan", 1.0f, false, 1000L)
|
||||
);
|
||||
|
||||
CombatLogStats.Summary summary = CombatLogStats.summarize(entries, 1000L, 10_000L);
|
||||
|
||||
assertEquals("Alice", summary.topTarget());
|
||||
assertEquals("Carol", summary.topAttacker());
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyEntriesGiveEmptySummary() {
|
||||
CombatLogStats.Summary summary = CombatLogStats.summarize(List.of(), 1000L, 10_000L);
|
||||
|
||||
assertEquals(0.0f, summary.totalDealt());
|
||||
assertEquals(0.0f, summary.totalTaken());
|
||||
assertNull(summary.topTarget());
|
||||
assertNull(summary.topAttacker());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.module.modules.misc.items;
|
||||
|
||||
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.assertTrue;
|
||||
import dev.loki.lovisual.features.module.modules.misc.items.ScrollMovePolicy;
|
||||
|
||||
public final class ScrollMovePolicyTest {
|
||||
|
||||
@Test
|
||||
void tinyScrollIsIgnored() {
|
||||
assertFalse(ScrollMovePolicy.isScrollSignificant(0.0));
|
||||
assertFalse(ScrollMovePolicy.isScrollSignificant(0.01));
|
||||
assertTrue(ScrollMovePolicy.isScrollSignificant(1.0));
|
||||
assertTrue(ScrollMovePolicy.isScrollSignificant(-1.0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void directionMatchesSign() {
|
||||
assertEquals(1, ScrollMovePolicy.direction(2.0));
|
||||
assertEquals(-1, ScrollMovePolicy.direction(-0.5));
|
||||
}
|
||||
|
||||
@Test
|
||||
void hoveredSlotWinsOverHotbarFallback() {
|
||||
assertEquals(17, ScrollMovePolicy.resolveTargetSlot(true, 17, 3, 36));
|
||||
}
|
||||
|
||||
@Test
|
||||
void hotbarFallbackMapsSelectedSlotIntoMenu() {
|
||||
// Standard inventory menu: 36 slots, hotbar = last 9 (indices 27..35).
|
||||
assertEquals(27, ScrollMovePolicy.resolveTargetSlot(false, -1, 0, 36));
|
||||
assertEquals(30, ScrollMovePolicy.resolveTargetSlot(false, -1, 3, 36));
|
||||
assertEquals(35, ScrollMovePolicy.resolveTargetSlot(false, -1, 8, 36));
|
||||
}
|
||||
|
||||
@Test
|
||||
void hotbarIndexWrapsNegativeSlots() {
|
||||
assertEquals(35, ScrollMovePolicy.hotbarIndexInMenu(-1, 36));
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyMenuYieldsNoSlot() {
|
||||
assertEquals(-1, ScrollMovePolicy.hotbarIndexInMenu(0, 0));
|
||||
assertEquals(-1, ScrollMovePolicy.hotbarIndexInMenu(0, -5));
|
||||
}
|
||||
|
||||
@Test
|
||||
void quickMoveUsesLeftClickButton() {
|
||||
assertEquals(0, ScrollMovePolicy.quickMoveButton());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.module.modules.misc.protect;
|
||||
|
||||
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.assertTrue;
|
||||
import dev.loki.lovisual.features.module.modules.misc.protect.NoServerPack;
|
||||
|
||||
public final class NoServerPackTest {
|
||||
|
||||
@Test
|
||||
void declinesInDeclineModeWhenEnabled() {
|
||||
assertTrue(NoServerPack.shouldDeclinePush(true, NoServerPack.Action.DECLINE.id));
|
||||
}
|
||||
|
||||
@Test
|
||||
void declinesInRemoveModeWhenEnabled() {
|
||||
assertTrue(NoServerPack.shouldDeclinePush(true, NoServerPack.Action.REMOVE.id));
|
||||
}
|
||||
|
||||
@Test
|
||||
void neverDeclinesInAskMode() {
|
||||
assertFalse(NoServerPack.shouldDeclinePush(true, NoServerPack.Action.ASK.id));
|
||||
assertFalse(NoServerPack.shouldDeclinePush(false, NoServerPack.Action.ASK.id));
|
||||
}
|
||||
|
||||
@Test
|
||||
void disabledModuleNeverDeclines() {
|
||||
assertFalse(NoServerPack.shouldDeclinePush(false, NoServerPack.Action.DECLINE.id));
|
||||
assertFalse(NoServerPack.shouldDeclinePush(false, NoServerPack.Action.REMOVE.id));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unknownModeFallsBackToDecline() {
|
||||
assertEquals(NoServerPack.Action.DECLINE, NoServerPack.Action.byId("bogus"));
|
||||
assertEquals(NoServerPack.Action.DECLINE, NoServerPack.Action.byId(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void modeIdsResolveRoundTrip() {
|
||||
for (NoServerPack.Action action : NoServerPack.Action.values()) {
|
||||
assertEquals(action, NoServerPack.Action.byId(action.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.module.modules.misc.sound;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import dev.loki.lovisual.features.module.modules.misc.sound.NoSound;
|
||||
|
||||
public final class NoSoundTest {
|
||||
|
||||
@Test
|
||||
void resolveScaleConvertsPercent() {
|
||||
assertEquals(0.3f, NoSound.resolveScale(true, 30), 0.0001f);
|
||||
assertEquals(1.0f, NoSound.resolveScale(true, 100), 0.0001f);
|
||||
assertEquals(0.01f, NoSound.resolveScale(true, 1), 0.0001f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void unmatchedSoundKeepsOriginalVolume() {
|
||||
assertEquals(1.0f, NoSound.resolveScale(false, 30), 0.0001f);
|
||||
assertEquals(1.0f, NoSound.resolveScale(false, 100), 0.0001f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void scaleIsClampedToSaneRange() {
|
||||
assertEquals(1.0f, NoSound.resolveScale(true, 250), 0.0001f);
|
||||
assertEquals(0.0f, NoSound.resolveScale(true, -5), 0.0001f);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.module.modules.player;
|
||||
|
||||
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.assertTrue;
|
||||
import dev.loki.lovisual.features.module.modules.player.DeathCoords;
|
||||
|
||||
public final class DeathCoordsTest {
|
||||
|
||||
@Test
|
||||
void detectsAliveToDeadTransition() {
|
||||
DeathCoords.DeathDetector detector = new DeathCoords.DeathDetector();
|
||||
assertFalse(detector.onTick(false));
|
||||
assertTrue(detector.onTick(true));
|
||||
assertFalse(detector.onTick(true));
|
||||
assertFalse(detector.onTick(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void secondDeathIsDetectedAgainAfterRevive() {
|
||||
DeathCoords.DeathDetector detector = new DeathCoords.DeathDetector();
|
||||
detector.onTick(true);
|
||||
assertFalse(detector.onTick(false));
|
||||
assertTrue(detector.onTick(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetClearsDeathState() {
|
||||
DeathCoords.DeathDetector detector = new DeathCoords.DeathDetector();
|
||||
detector.onTick(true);
|
||||
detector.reset();
|
||||
assertTrue(detector.onTick(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void messageIncludesDimensionWhenEnabled() {
|
||||
String message = DeathCoords.formatDeathMessage(12.25, 64.0, -789.5, "minecraft:overworld", true);
|
||||
assertEquals("You died at 12.25, 64.00, -789.50 (minecraft:overworld)", message);
|
||||
}
|
||||
|
||||
@Test
|
||||
void messageOmitsDimensionWhenDisabled() {
|
||||
String message = DeathCoords.formatDeathMessage(0.5, 1.5, 2.5, "minecraft:the_nether", false);
|
||||
assertEquals("You died at 0.50, 1.50, 2.50", message);
|
||||
}
|
||||
|
||||
@Test
|
||||
void messageFallsBackWithoutDimensionName() {
|
||||
assertEquals(
|
||||
"You died at 1.00, 2.00, 3.00",
|
||||
DeathCoords.formatDeathMessage(1, 2, 3, "", true)
|
||||
);
|
||||
assertEquals(
|
||||
"You died at 1.00, 2.00, 3.00",
|
||||
DeathCoords.formatDeathMessage(1, 2, 3, null, true)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.module.modules.player;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import dev.loki.lovisual.features.module.modules.player.UseTracker;
|
||||
|
||||
public final class UseTrackerTest {
|
||||
|
||||
@Test
|
||||
void defaultOptionsTrackInvisibilityOnly() {
|
||||
var options = UseTracker.UseEffects.defaultOptions();
|
||||
assertEquals(6, options.size());
|
||||
assertTrue(options.get("invisibility"));
|
||||
assertEquals(false, options.get("speed"));
|
||||
assertEquals(false, options.get("strength"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void totemPopEventIdMatchesVanilla() {
|
||||
// Entity event 35 is the vanilla totem-of-undying activation event.
|
||||
assertEquals(35, UseTracker.TOTEM_POP_EVENT_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
void knownKeysResolveToHumanNames() {
|
||||
assertEquals("Invisibility", UseTracker.UseEffects.displayName("invisibility"));
|
||||
assertEquals("Fire Resistance", UseTracker.UseEffects.displayName("fire_resistance"));
|
||||
assertEquals("an effect", UseTracker.UseEffects.displayName("unknown"));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.module.modules.player.auto;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import dev.loki.lovisual.features.module.modules.player.auto.AutoAccept;
|
||||
|
||||
public final class AutoAcceptTest {
|
||||
|
||||
private static final Set<String> PATTERNS = Set.of("просит телепортироваться к себе", "has requested to teleport");
|
||||
|
||||
@Test
|
||||
void extractsSenderBeforeRussianPattern() {
|
||||
String sender = AutoAccept.TeleportRequestParser.extractSender(
|
||||
"Steve123 просит телепортироваться к себе", PATTERNS);
|
||||
assertEquals("Steve123", sender);
|
||||
}
|
||||
|
||||
@Test
|
||||
void extractsSenderBeforeEnglishPattern() {
|
||||
String sender = AutoAccept.TeleportRequestParser.extractSender(
|
||||
"Herobrine has requested to teleport to you", PATTERNS);
|
||||
assertEquals("Herobrine", sender);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ignoresDecorationsBeforeNickname() {
|
||||
String sender = AutoAccept.TeleportRequestParser.extractSender(
|
||||
"» [Клан] Night_Wolf просит телепортироваться к себе", PATTERNS);
|
||||
assertEquals("Night_Wolf", sender);
|
||||
}
|
||||
|
||||
@Test
|
||||
void unrelatedMessageYieldsNull() {
|
||||
assertNull(AutoAccept.TeleportRequestParser.extractSender("Player joined the game", PATTERNS));
|
||||
}
|
||||
|
||||
@Test
|
||||
void blankOrNullInputsYieldNull() {
|
||||
assertNull(AutoAccept.TeleportRequestParser.extractSender(null, PATTERNS));
|
||||
assertNull(AutoAccept.TeleportRequestParser.extractSender("", PATTERNS));
|
||||
assertNull(AutoAccept.TeleportRequestParser.extractSender("Steve просит телепортироваться к себе", Set.of()));
|
||||
assertNull(AutoAccept.TeleportRequestParser.extractSender("Steve просит телепортироваться к себе", null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void patternAtStartOfMessageYieldsNull() {
|
||||
// No nickname-like token before the pattern means no sender.
|
||||
assertNull(AutoAccept.TeleportRequestParser.extractSender("просит телепортироваться к себе", PATTERNS));
|
||||
}
|
||||
|
||||
@Test
|
||||
void earliestPatternWins() {
|
||||
Set<String> both = Set.of("has requested", "просит телепортироваться к себе");
|
||||
String sender = AutoAccept.TeleportRequestParser.extractSender(
|
||||
"Alex has requested to teleport (просит телепортироваться к себе)", both);
|
||||
assertEquals("Alex", sender);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shortTokensAreNotNicknames() {
|
||||
assertNull(AutoAccept.TeleportRequestParser.lastNicknameIn("ab – "));
|
||||
assertEquals("abc", AutoAccept.TeleportRequestParser.lastNicknameIn("abc – "));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.module.modules.player.auto;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import dev.loki.lovisual.features.module.modules.player.auto.AutoLeave;
|
||||
|
||||
public final class AutoLeaveTest {
|
||||
|
||||
@Test
|
||||
void zeroDelayLeavesImmediatelyOnTriggerEdge() {
|
||||
AutoLeave.LeavePolicy policy = new AutoLeave.LeavePolicy();
|
||||
assertTrue(policy.onTick(true, 1000L, 0L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void delayWaitsBeforeLeaving() {
|
||||
AutoLeave.LeavePolicy policy = new AutoLeave.LeavePolicy();
|
||||
assertFalse(policy.onTick(true, 1000L, 2000L));
|
||||
assertFalse(policy.onTick(true, 2500L, 2000L));
|
||||
assertTrue(policy.onTick(true, 3001L, 2000L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void quietTickRearmsTheGate() {
|
||||
AutoLeave.LeavePolicy policy = new AutoLeave.LeavePolicy();
|
||||
assertFalse(policy.onTick(true, 1000L, 1000L));
|
||||
assertFalse(policy.onTick(false, 1500L, 1000L));
|
||||
assertFalse(policy.onTick(true, 2000L, 1000L));
|
||||
assertTrue(policy.onTick(true, 3000L, 1000L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void warningIsConsumedOnce() {
|
||||
AutoLeave.LeavePolicy policy = new AutoLeave.LeavePolicy();
|
||||
assertTrue(policy.consumeWarning());
|
||||
assertFalse(policy.consumeWarning());
|
||||
policy.reset();
|
||||
assertTrue(policy.consumeWarning());
|
||||
}
|
||||
|
||||
@Test
|
||||
void healthThresholdIsInclusive() {
|
||||
assertTrue(AutoLeave.LeavePolicy.isHealthLow(6.0f, 6.0f));
|
||||
assertTrue(AutoLeave.LeavePolicy.isHealthLow(5.5f, 6.0f));
|
||||
assertFalse(AutoLeave.LeavePolicy.isHealthLow(6.5f, 6.0f));
|
||||
}
|
||||
|
||||
@Test
|
||||
void playerCountRequiresAtLeastOne() {
|
||||
assertTrue(AutoLeave.LeavePolicy.isPlayerCountReached(1, 1));
|
||||
assertTrue(AutoLeave.LeavePolicy.isPlayerCountReached(3, 2));
|
||||
assertFalse(AutoLeave.LeavePolicy.isPlayerCountReached(1, 2));
|
||||
assertFalse(AutoLeave.LeavePolicy.isPlayerCountReached(0, 1));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.module.modules.player.auto;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import dev.loki.lovisual.features.module.modules.player.auto.AutoRespawn;
|
||||
|
||||
public final class AutoRespawnTest {
|
||||
|
||||
@Test
|
||||
void deathAloneDoesNotTriggerImmediately() {
|
||||
AutoRespawn.RespawnGate gate = new AutoRespawn.RespawnGate();
|
||||
assertFalse(gate.shouldRequestRespawn(true, 1000L, 500L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void triggersOnceAfterDelay() {
|
||||
AutoRespawn.RespawnGate gate = new AutoRespawn.RespawnGate();
|
||||
assertFalse(gate.shouldRequestRespawn(true, 1000L, 500L));
|
||||
assertFalse(gate.shouldRequestRespawn(true, 1200L, 500L));
|
||||
assertTrue(gate.shouldRequestRespawn(true, 1500L, 500L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void requestIsSentOnlyOncePerDeath() {
|
||||
AutoRespawn.RespawnGate gate = new AutoRespawn.RespawnGate();
|
||||
assertFalse(gate.shouldRequestRespawn(true, 1000L, 0L));
|
||||
gate.onRequestSent();
|
||||
|
||||
// Player is still locally dead until the server responds.
|
||||
assertFalse(gate.shouldRequestRespawn(true, 1100L, 0L));
|
||||
assertFalse(gate.shouldRequestRespawn(true, 5000L, 0L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void comingBackToLifeRearmsTheGate() {
|
||||
AutoRespawn.RespawnGate gate = new AutoRespawn.RespawnGate();
|
||||
assertFalse(gate.shouldRequestRespawn(true, 1000L, 0L));
|
||||
gate.onRequestSent();
|
||||
assertFalse(gate.shouldRequestRespawn(true, 2000L, 0L));
|
||||
|
||||
assertFalse(gate.shouldRequestRespawn(false, 3000L, 0L));
|
||||
assertFalse(gate.shouldRequestRespawn(true, 3100L, 500L));
|
||||
assertTrue(gate.shouldRequestRespawn(true, 3600L, 500L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void totemReviveWithoutRequestStartsFreshDeath() {
|
||||
AutoRespawn.RespawnGate gate = new AutoRespawn.RespawnGate();
|
||||
assertFalse(gate.shouldRequestRespawn(true, 1000L, 2000L));
|
||||
assertFalse(gate.shouldRequestRespawn(false, 1200L, 2000L));
|
||||
|
||||
assertFalse(gate.shouldRequestRespawn(true, 5000L, 2000L));
|
||||
assertTrue(gate.shouldRequestRespawn(true, 7000L, 2000L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void negativeDelayTreatedAsZero() {
|
||||
AutoRespawn.RespawnGate gate = new AutoRespawn.RespawnGate();
|
||||
assertFalse(gate.shouldRequestRespawn(true, 1000L, -5L));
|
||||
assertTrue(gate.shouldRequestRespawn(true, 1001L, -5L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingPlayerResetsState() {
|
||||
AutoRespawn.RespawnGate gate = new AutoRespawn.RespawnGate();
|
||||
assertFalse(gate.shouldRequestRespawn(true, 1000L, 0L));
|
||||
gate.onRequestSent();
|
||||
|
||||
gate.reset();
|
||||
assertFalse(gate.shouldRequestRespawn(true, 2000L, 0L));
|
||||
assertTrue(gate.shouldRequestRespawn(true, 2001L, 0L));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.module.modules.player.interact;
|
||||
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import dev.loki.lovisual.features.module.modules.player.interact.NoInteract;
|
||||
|
||||
public final class NoInteractTest {
|
||||
@ParameterizedTest(name = "state mask {0}")
|
||||
@MethodSource("allStateCombinations")
|
||||
void blockInteractionDecisionCoversEveryStateCombination(
|
||||
int mask,
|
||||
boolean noInteractEnabled,
|
||||
boolean onlyKillAura,
|
||||
boolean killAuraEnabled
|
||||
) {
|
||||
boolean expected = noInteractEnabled && (!onlyKillAura || killAuraEnabled);
|
||||
|
||||
assertEquals(
|
||||
expected,
|
||||
NoInteract.shouldBlockBlockInteraction(
|
||||
noInteractEnabled,
|
||||
onlyKillAura,
|
||||
killAuraEnabled
|
||||
),
|
||||
"unexpected decision for mask " + mask
|
||||
);
|
||||
}
|
||||
|
||||
private static Stream<Arguments> allStateCombinations() {
|
||||
return IntStream.range(0, 8).mapToObj(mask -> Arguments.of(
|
||||
mask,
|
||||
bit(mask, 0),
|
||||
bit(mask, 1),
|
||||
bit(mask, 2)
|
||||
));
|
||||
}
|
||||
|
||||
private static boolean bit(int value, int index) {
|
||||
return (value & (1 << index)) != 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.module.modules.player.inventory;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import dev.loki.lovisual.features.module.modules.player.inventory.TapeMouse;
|
||||
|
||||
public final class TapeMouseTest {
|
||||
|
||||
@Test
|
||||
void firstCallIsDueImmediately() {
|
||||
TapeMouse.ClickPacer pacer = new TapeMouse.ClickPacer();
|
||||
assertTrue(pacer.isDue(1000L, 1000L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void clicksAreSpacedByInterval() {
|
||||
TapeMouse.ClickPacer pacer = new TapeMouse.ClickPacer();
|
||||
assertTrue(pacer.isDue(1000L, 500L));
|
||||
pacer.mark(1000L);
|
||||
|
||||
assertFalse(pacer.isDue(1499L, 500L));
|
||||
assertTrue(pacer.isDue(1500L, 500L));
|
||||
assertTrue(pacer.isDue(4000L, 500L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void changingIntervalIsRespected() {
|
||||
TapeMouse.ClickPacer pacer = new TapeMouse.ClickPacer();
|
||||
pacer.mark(1000L);
|
||||
|
||||
assertFalse(pacer.isDue(2499L, 2000L));
|
||||
assertTrue(pacer.isDue(3000L, 2000L));
|
||||
pacer.mark(3000L);
|
||||
|
||||
assertTrue(pacer.isDue(3100L, 100L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetAllowsImmediateClickAgain() {
|
||||
TapeMouse.ClickPacer pacer = new TapeMouse.ClickPacer();
|
||||
pacer.mark(1000L);
|
||||
assertFalse(pacer.isDue(1100L, 5000L));
|
||||
|
||||
pacer.reset();
|
||||
assertTrue(pacer.isDue(1100L, 5000L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void nonPositiveIntervalStillPacesEveryCall() {
|
||||
TapeMouse.ClickPacer pacer = new TapeMouse.ClickPacer();
|
||||
assertTrue(pacer.isDue(0L, 0L));
|
||||
pacer.mark(0L);
|
||||
assertFalse(pacer.isDue(0L, 0L));
|
||||
assertTrue(pacer.isDue(1L, 0L));
|
||||
assertTrue(pacer.isDue(1L, -100L));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.module.modules.visuals;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.items.held.AlwaysVisibleHand;
|
||||
|
||||
public final class AlwaysVisibleHandTest {
|
||||
|
||||
@Test
|
||||
void bothModeForcesBothHandsVisible() {
|
||||
assertArrayEquals(new boolean[]{true, true},
|
||||
AlwaysVisibleHand.resolveSelection(AlwaysVisibleHand.SELECTION_BOTH, false, false));
|
||||
assertArrayEquals(new boolean[]{true, true},
|
||||
AlwaysVisibleHand.resolveSelection(AlwaysVisibleHand.SELECTION_BOTH, true, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void mainModeKeepsMainHandAndPreservesVanillaOffHand() {
|
||||
assertArrayEquals(new boolean[]{true, false},
|
||||
AlwaysVisibleHand.resolveSelection(AlwaysVisibleHand.SELECTION_MAIN, false, false));
|
||||
assertArrayEquals(new boolean[]{true, true},
|
||||
AlwaysVisibleHand.resolveSelection(AlwaysVisibleHand.SELECTION_MAIN, false, true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void offModeKeepsOffHandAndPreservesVanillaMainHand() {
|
||||
assertArrayEquals(new boolean[]{false, true},
|
||||
AlwaysVisibleHand.resolveSelection(AlwaysVisibleHand.SELECTION_OFF, false, true));
|
||||
assertArrayEquals(new boolean[]{true, true},
|
||||
AlwaysVisibleHand.resolveSelection(AlwaysVisibleHand.SELECTION_OFF, true, true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unknownModeFallsBackToVanillaDecision() {
|
||||
assertArrayEquals(new boolean[]{false, true},
|
||||
AlwaysVisibleHand.resolveSelection("Something", false, true));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.module.modules.visuals;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.weather.sky.AtmoDawnFog;
|
||||
|
||||
/**
|
||||
* Чистая математика AtmoDawnFog: окно времени суток и высотное затухание.
|
||||
*/
|
||||
public final class AtmoDawnFogTest {
|
||||
|
||||
private static final float EPS = 1e-4f;
|
||||
|
||||
@Test
|
||||
void dawnPeaksAtSunrise() {
|
||||
assertEquals(1.0f, AtmoDawnFog.strengthFor(0.0f, "Dawn", 64.0f, 76.0f), EPS);
|
||||
assertTrue(AtmoDawnFog.strengthFor(0.999f, "Dawn", 64.0f, 76.0f) > 0.98f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void dawnSilentAtNoonAndDusk() {
|
||||
assertEquals(0.0f, AtmoDawnFog.strengthFor(0.25f, "Dawn", 64.0f, 76.0f), EPS);
|
||||
assertEquals(0.0f, AtmoDawnFog.strengthFor(0.5f, "Dawn", 64.0f, 76.0f), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void duskPeaksAtSunset() {
|
||||
assertEquals(1.0f, AtmoDawnFog.strengthFor(0.5f, "Dusk", 64.0f, 76.0f), EPS);
|
||||
assertEquals(0.0f, AtmoDawnFog.strengthFor(0.0f, "Dusk", 64.0f, 76.0f), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void customModeIgnoresTime() {
|
||||
assertEquals(1.0f, AtmoDawnFog.strengthFor(0.3f, "Custom", 64.0f, 76.0f), EPS);
|
||||
assertEquals(1.0f, AtmoDawnFog.strengthFor(0.7f, "Custom", 64.0f, 76.0f), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void altitudeFadesAboveScatterHeight() {
|
||||
float below = AtmoDawnFog.strengthFor(0.0f, "Dawn", 76.0f, 76.0f);
|
||||
float above = AtmoDawnFog.strengthFor(0.0f, "Dawn", 76.0f + 16.0f, 76.0f);
|
||||
assertEquals(1.0f, below, EPS);
|
||||
assertEquals(0.5f, above, EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void altitudeClampsAtZero() {
|
||||
assertEquals(0.0f, AtmoDawnFog.strengthFor(0.0f, "Custom", 76.0f + 1000.0f, 76.0f), EPS);
|
||||
assertTrue(AtmoDawnFog.strengthFor(0.0f, "Custom", 40.0f, 76.0f) <= 1.0f);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.module.modules.visuals;
|
||||
|
||||
import dev.loki.lovisual.render.helpers.player.HeadAnchor;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.hats.ChinaHatModel;
|
||||
|
||||
public final class ChinaHatModelTest {
|
||||
|
||||
private static final double EPS = 1e-3;
|
||||
|
||||
private static HeadAnchor atOrigin() {
|
||||
return HeadAnchor.world(Vec3.ZERO);
|
||||
}
|
||||
|
||||
@Test
|
||||
void apexSitsOnTopOfCone() {
|
||||
ChinaHatModel.Layout layout = ChinaHatModel.layout(0.5f, 0.22f, 24, 0f, 0f, 0f, atOrigin());
|
||||
|
||||
assertEquals(0.0, layout.apex().x, EPS);
|
||||
assertEquals(0.22, layout.apex().y, EPS);
|
||||
assertEquals(0.0, layout.apex().z, EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rimRingIsFlatAtBaseRadius() {
|
||||
int segments = 24;
|
||||
float radius = 0.5f;
|
||||
ChinaHatModel.Layout layout = ChinaHatModel.layout(radius, 0.22f, segments, 0f, 0f, 0f, atOrigin());
|
||||
|
||||
assertEquals(segments, layout.rim().length);
|
||||
for (Vec3 rim : layout.rim()) {
|
||||
assertEquals(0.0, rim.y, EPS);
|
||||
double distanceFromAxis = Math.sqrt(rim.x * rim.x + rim.z * rim.z);
|
||||
assertEquals(radius, distanceFromAxis, EPS);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void rimVerticesAreEvenlySpaced() {
|
||||
int segments = 4;
|
||||
ChinaHatModel.Layout layout = ChinaHatModel.layout(1.0f, 0.22f, segments, 0f, 0f, 0f, atOrigin());
|
||||
|
||||
for (int i = 0; i < segments; i++) {
|
||||
int next = (i + 1) % segments;
|
||||
double side = layout.rim()[i].distanceTo(layout.rim()[next]);
|
||||
assertEquals(Math.sqrt(2.0), side, EPS);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void yawRotationSpinsRimAroundApex() {
|
||||
float radius = 0.5f;
|
||||
ChinaHatModel.Layout before = ChinaHatModel.layout(radius, 0.22f, 24, 0f, 0f, 0f, atOrigin());
|
||||
ChinaHatModel.Layout after = ChinaHatModel.layout(radius, 0.22f, 24, 90f, 0f, 0f, atOrigin());
|
||||
|
||||
assertEquals(0.22, after.apex().y, EPS);
|
||||
assertEquals(before.rim()[0].distanceTo(after.apex()),
|
||||
after.rim()[6].distanceTo(after.apex()), 0.15);
|
||||
}
|
||||
|
||||
@Test
|
||||
void zeroSegmentsProducesEmptyRim() {
|
||||
ChinaHatModel.Layout layout = ChinaHatModel.layout(0.5f, 0.22f, 0, 0f, 0f, 0f, atOrigin());
|
||||
|
||||
assertEquals(0, layout.rim().length);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildIsNoOpWithoutMesh() {
|
||||
assertDoesNotThrow(() -> ChinaHatModel.build(null, null, atOrigin(), 40, 0.5f, 0.22f,
|
||||
0f, 0f, 0f, (i, l, a) -> 0, (i, l, a) -> 0, 1.0f));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.module.modules.visuals;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.hats.ChinaHatPalette;
|
||||
|
||||
public final class ChinaHatPaletteTest {
|
||||
@Test
|
||||
void channelsAreIsolatedAndPreserved() {
|
||||
int argb = 0xA1B2C3D4;
|
||||
|
||||
assertEquals(0xA1, ChinaHatPalette.alpha(argb));
|
||||
assertEquals(0xB2, ChinaHatPalette.red(argb));
|
||||
assertEquals(0xC3, ChinaHatPalette.green(argb));
|
||||
assertEquals(0xD4, ChinaHatPalette.blue(argb));
|
||||
}
|
||||
|
||||
@Test
|
||||
void multiplyRgbKeepsAlphaAndClamps() {
|
||||
int argb = 0xFF804020;
|
||||
|
||||
assertEquals(0xFFC06030, ChinaHatPalette.multiplyRgb(argb, 1.5f));
|
||||
assertEquals(0xFF000000, ChinaHatPalette.multiplyRgb(argb, 0.0f));
|
||||
assertEquals(0xFFFFFF80, ChinaHatPalette.multiplyRgb(argb, 4.0f));
|
||||
}
|
||||
|
||||
@Test
|
||||
void brightenUsesSameScalingAsMultiply() {
|
||||
int argb = 0x80FF8000;
|
||||
|
||||
assertEquals(
|
||||
ChinaHatPalette.multiplyRgb(argb, 1.25f),
|
||||
ChinaHatPalette.brighten(argb, 1.25f)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withAlphaReplacesOnlyTheAlphaChannel() {
|
||||
int argb = 0xFF123456;
|
||||
|
||||
assertEquals(0x50123456, ChinaHatPalette.withAlpha(argb, 80));
|
||||
assertEquals(0xFF123456, ChinaHatPalette.withAlpha(argb, 999));
|
||||
assertEquals(0x00123456, ChinaHatPalette.withAlpha(argb, -5));
|
||||
}
|
||||
|
||||
@Test
|
||||
void alphaOrFallsBackOnlyWhenChannelIsZero() {
|
||||
assertEquals(238, ChinaHatPalette.alphaOr(0x00123456, 238));
|
||||
assertEquals(255, ChinaHatPalette.alphaOr(0xFF123456, 238));
|
||||
}
|
||||
|
||||
@Test
|
||||
void mixClampsAndBlendsEveryChannel() {
|
||||
int a = 0xFF000000;
|
||||
int b = 0x00FFFFFF;
|
||||
|
||||
assertEquals(a, ChinaHatPalette.mix(a, b, -1.0f));
|
||||
assertEquals(b, ChinaHatPalette.mix(a, b, 2.0f));
|
||||
assertEquals(0x80808080, ChinaHatPalette.mix(a, b, 0.5f));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.module.modules.visuals;
|
||||
|
||||
import org.joml.Vector3f;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.visibility.Light;
|
||||
|
||||
public final class LightTintTest {
|
||||
|
||||
private static final float EPS = 1e-4f;
|
||||
|
||||
@Test
|
||||
void toLinearMapsBytesToUnitRange() {
|
||||
Vector3f red = Light.toLinear(0xFF0000);
|
||||
assertEquals(1.0f, red.x, EPS);
|
||||
assertEquals(0.0f, red.y, EPS);
|
||||
assertEquals(0.0f, red.z, EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void toLinearIgnoresAlpha() {
|
||||
Vector3f a = Light.toLinear(0x00FF0000);
|
||||
Vector3f b = Light.toLinear(0xAAFF0000);
|
||||
assertEquals(a.x, b.x, EPS);
|
||||
assertEquals(a.y, b.y, EPS);
|
||||
assertEquals(a.z, b.z, EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void tintMixZeroKeepsCurrent() {
|
||||
Vector3f target = Light.toLinear(0xFF0000);
|
||||
Vector3f out = Light.tintAmbient(new Vector3f(0.2f, 0.3f, 0.4f), target, 0.0f);
|
||||
assertEquals(0.2f, out.x, EPS);
|
||||
assertEquals(0.3f, out.y, EPS);
|
||||
assertEquals(0.4f, out.z, EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void tintMixOneEqualsTarget() {
|
||||
Vector3f target = Light.toLinear(0x102030);
|
||||
Vector3f out = Light.tintAmbient(new Vector3f(0.9f, 0.9f, 0.9f), target, 1.0f);
|
||||
assertEquals(target.x, out.x, EPS);
|
||||
assertEquals(target.y, out.y, EPS);
|
||||
assertEquals(target.z, out.z, EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void tintMixHalfIsAverage() {
|
||||
Vector3f target = Light.toLinear(0x000000);
|
||||
Vector3f out = Light.tintAmbient(new Vector3f(0.8f, 0.8f, 0.8f), target, 0.5f);
|
||||
assertEquals(0.4f, out.x, EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullCurrentBehavesAsWhite() {
|
||||
Vector3f out = Light.tintAmbient(null, new Vector3f(0.0f, 0.0f, 0.0f), 1.0f);
|
||||
assertEquals(0.0f, out.x, EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void mixClampedToUnitRange() {
|
||||
Vector3f target = Light.toLinear(0xFF0000);
|
||||
Vector3f high = Light.tintAmbient(new Vector3f(0.1f, 0.2f, 0.3f), target, 5.0f);
|
||||
assertEquals(target.x, high.x, EPS);
|
||||
Vector3f low = Light.tintAmbient(new Vector3f(0.1f, 0.2f, 0.3f), target, -1.0f);
|
||||
assertEquals(0.1f, low.x, EPS);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.module.modules.visuals;
|
||||
|
||||
import dev.loki.lovisual.render.engine.uniform.impl.post.PostFXUniforms;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.fx.PostFX;
|
||||
|
||||
/**
|
||||
* Пресеты PostFX: Cinematic должен быть визуально заметным,
|
||||
* Vanilla — нейтральным (это причина «модуль ничего не делал»).
|
||||
*/
|
||||
public final class PostFxPresetTest {
|
||||
|
||||
private static float[] preset(String name) {
|
||||
float[] out = new float[PostFXUniforms.FLOAT_COUNT];
|
||||
PostFX.applyPresetTo(name, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
@Test
|
||||
void vanillaPresetIsNeutral() {
|
||||
float[] vanilla = preset("Vanilla");
|
||||
assertEquals(0.0f, vanilla[0], 1e-6); // exposure
|
||||
assertEquals(1.0f, vanilla[1], 1e-6); // gamma
|
||||
assertEquals(1.0f, vanilla[2], 1e-6); // contrast
|
||||
assertEquals(1.0f, vanilla[3], 1e-6); // saturation
|
||||
assertEquals(0.0f, vanilla[7], 1e-6); // LUT strength
|
||||
assertEquals(0.0f, vanilla[15], 1e-6); // tone map off
|
||||
}
|
||||
|
||||
@Test
|
||||
void cinematicPresetIsVisible() {
|
||||
float[] cinematic = preset("Cinematic");
|
||||
assertNotEquals(1.0f, cinematic[2], 1e-4); // contrast изменён
|
||||
assertTrue(cinematic[3] > 1.05f); // насыщенность выше
|
||||
assertTrue(cinematic[7] > 0.2f); // тёплый LUT заметен
|
||||
assertEquals(1.0f, cinematic[15], 1e-6); // filmic tone map
|
||||
assertTrue(cinematic[19] > 0.0f); // холодные тени
|
||||
assertTrue(cinematic[27] > 0.0f); // тёплые света
|
||||
assertTrue(cinematic[8] > 0.1f); // виньетка заметна
|
||||
}
|
||||
|
||||
@Test
|
||||
void cinematicDiffersFromVanillaOnGrade() {
|
||||
float[] cinematic = preset("Cinematic");
|
||||
float[] vanilla = preset("Vanilla");
|
||||
assertNotEquals(vanilla[2], cinematic[2]);
|
||||
assertNotEquals(vanilla[3], cinematic[3]);
|
||||
assertNotEquals(vanilla[7], cinematic[7]);
|
||||
}
|
||||
|
||||
@Test
|
||||
void unknownPresetFallsBackToSoft() {
|
||||
float[] unknown = preset("Mystery");
|
||||
float[] soft = preset("Soft");
|
||||
for (int i = 0; i < 16; i++) {
|
||||
assertEquals(soft[i], unknown[i], 1e-6);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullPresetFallsBackToCinematic() {
|
||||
float[] byNull = preset(null);
|
||||
float[] cinematic = preset("Cinematic");
|
||||
for (int i = 0; i < 16; i++) {
|
||||
assertEquals(cinematic[i], byNull[i], 1e-6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.lyrics;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.lyrics.lrc.LrcLibClient;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.lyrics.lrc.LyricLine;
|
||||
|
||||
public final class LrcLibClientTest {
|
||||
|
||||
@Test
|
||||
void cleanTrackNameRemovesParenthetical() {
|
||||
assertEquals("Song Title", LrcLibClient.cleanTrackName("Song Title (feat. Guest)"));
|
||||
assertEquals("Song Title", LrcLibClient.cleanTrackName("Song Title (Remix)"));
|
||||
assertEquals("", LrcLibClient.cleanTrackName("(Song)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void cleanTrackNameRemovesBrackets() {
|
||||
assertEquals("Song Title", LrcLibClient.cleanTrackName("Song Title [Deluxe]"));
|
||||
assertEquals("", LrcLibClient.cleanTrackName("[Song]"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void cleanTrackNameHandlesNull() {
|
||||
assertEquals("", LrcLibClient.cleanTrackName(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void cleanTrackNameHandlesEmpty() {
|
||||
assertEquals("", LrcLibClient.cleanTrackName(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void cleanTrackNameHandlesWhitespaceOnly() {
|
||||
assertEquals("", LrcLibClient.cleanTrackName(" "));
|
||||
}
|
||||
|
||||
@Test
|
||||
void cleanTrackNameTrims() {
|
||||
assertEquals("Song Title", LrcLibClient.cleanTrackName(" Song Title "));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fetchLyricsAsyncReturnsEmptyForBlankTrack() {
|
||||
var future = LrcLibClient.fetchLyricsAsync("", "Artist", false);
|
||||
List<LyricLine> result = future.join();
|
||||
assertTrue(result.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void fetchLyricsAsyncReturnsEmptyForNullTrack() {
|
||||
var future = LrcLibClient.fetchLyricsAsync(null, "Artist", false);
|
||||
List<LyricLine> result = future.join();
|
||||
assertTrue(result.isEmpty());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.lyrics;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.lyrics.lrc.LrcParser;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.lyrics.lrc.LyricLine;
|
||||
|
||||
public final class LrcParserTest {
|
||||
|
||||
@Test
|
||||
void parsesStandardLrcFormat() {
|
||||
String lrc = "[00:12.00]Hello world\n[00:15.00]Second line";
|
||||
List<LyricLine> lines = LrcParser.parse(lrc);
|
||||
|
||||
assertEquals(2, lines.size());
|
||||
assertEquals(12000L, lines.get(0).timestampMs());
|
||||
assertEquals("Hello world", lines.get(0).text());
|
||||
assertEquals(15000L, lines.get(1).timestampMs());
|
||||
assertEquals("Second line", lines.get(1).text());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsesSingleDigitMinutes() {
|
||||
String lrc = "[1:05.20]Test";
|
||||
List<LyricLine> lines = LrcParser.parse(lrc);
|
||||
|
||||
assertEquals(1, lines.size());
|
||||
assertEquals(65200L, lines.get(0).timestampMs());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsesWithoutFractionalSeconds() {
|
||||
String lrc = "[01:30]No fraction";
|
||||
List<LyricLine> lines = LrcParser.parse(lrc);
|
||||
|
||||
assertEquals(1, lines.size());
|
||||
assertEquals(90000L, lines.get(0).timestampMs());
|
||||
assertEquals("No fraction", lines.get(0).text());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsesOneDigitCentisecond() {
|
||||
String lrc = "[00:05.1]Test";
|
||||
List<LyricLine> lines = LrcParser.parse(lrc);
|
||||
|
||||
assertEquals(1, lines.size());
|
||||
assertEquals(5100L, lines.get(0).timestampMs());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsesTwoDigitCentisecond() {
|
||||
String lrc = "[00:05.12]Test";
|
||||
List<LyricLine> lines = LrcParser.parse(lrc);
|
||||
|
||||
assertEquals(1, lines.size());
|
||||
assertEquals(5120L, lines.get(0).timestampMs());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsesThreeDigitCentisecond() {
|
||||
String lrc = "[00:05.123]Test";
|
||||
List<LyricLine> lines = LrcParser.parse(lrc);
|
||||
|
||||
assertEquals(1, lines.size());
|
||||
assertEquals(5123L, lines.get(0).timestampMs());
|
||||
}
|
||||
|
||||
@Test
|
||||
void ignoresLinesWithoutTimestamp() {
|
||||
String lrc = "Just a plain line\n[00:01.00]Has timestamp";
|
||||
List<LyricLine> lines = LrcParser.parse(lrc);
|
||||
|
||||
assertEquals(1, lines.size());
|
||||
assertEquals("Has timestamp", lines.get(0).text());
|
||||
}
|
||||
|
||||
@Test
|
||||
void ignoresEmptyLines() {
|
||||
String lrc = "\n\n[00:01.00]Line\n\n";
|
||||
List<LyricLine> lines = LrcParser.parse(lrc);
|
||||
|
||||
assertEquals(1, lines.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsEmptyForNullInput() {
|
||||
assertTrue(LrcParser.parse(null).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsEmptyForBlankInput() {
|
||||
assertTrue(LrcParser.parse("").isEmpty());
|
||||
assertTrue(LrcParser.parse(" ").isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsEmptyWhenNoSyncedLyrics() {
|
||||
String lrc = "Some text without timestamp format";
|
||||
List<LyricLine> lines = LrcParser.parse(lrc);
|
||||
|
||||
assertTrue(lines.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void sortsLinesByTimestamp() {
|
||||
String lrc = "[00:30.00]Third\n[00:05.00]First\n[00:15.00]Second";
|
||||
List<LyricLine> lines = LrcParser.parse(lrc);
|
||||
|
||||
assertEquals(3, lines.size());
|
||||
assertEquals("First", lines.get(0).text());
|
||||
assertEquals("Second", lines.get(1).text());
|
||||
assertEquals("Third", lines.get(2).text());
|
||||
}
|
||||
|
||||
@Test
|
||||
void stripsWhitespaceFromText() {
|
||||
String lrc = "[00:01.00] Trim me ";
|
||||
List<LyricLine> lines = LrcParser.parse(lrc);
|
||||
|
||||
assertEquals(1, lines.size());
|
||||
assertEquals("Trim me", lines.get(0).text());
|
||||
}
|
||||
|
||||
@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";
|
||||
List<LyricLine> lines = LrcParser.parse(lrc);
|
||||
|
||||
assertEquals(4, lines.size());
|
||||
assertEquals(5120L, lines.get(0).timestampMs());
|
||||
assertEquals(5123L, lines.get(1).timestampMs());
|
||||
assertEquals(15500L, lines.get(2).timestampMs());
|
||||
assertEquals(90000L, lines.get(3).timestampMs());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.lyrics;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.lyrics.lrc.LyricLine;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.lyrics.lrc.LyricLineSplitter;
|
||||
|
||||
public final class LyricLineSplitterTest {
|
||||
|
||||
@Test
|
||||
void returnsEmptyForNullInput() {
|
||||
assertTrue(LyricLineSplitter.splitLongLines(null).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsEmptyForEmptyList() {
|
||||
assertTrue(LyricLineSplitter.splitLongLines(Collections.emptyList()).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsShortLinesUnsplit() {
|
||||
LyricLine line = new LyricLine(1000L, "Short text");
|
||||
List<LyricLine> input = Collections.singletonList(line);
|
||||
List<LyricLine> result = LyricLineSplitter.splitLongLines(input);
|
||||
|
||||
assertEquals(1, result.size());
|
||||
assertEquals(1000L, result.get(0).timestampMs());
|
||||
assertEquals("Short text", result.get(0).text());
|
||||
}
|
||||
|
||||
@Test
|
||||
void splitsLongLineIntoChunks() {
|
||||
LyricLine line = new LyricLine(2000L, "This is a long line with many words in it");
|
||||
List<LyricLine> input = Collections.singletonList(line);
|
||||
List<LyricLine> result = LyricLineSplitter.splitLongLines(input);
|
||||
|
||||
assertTrue(result.size() > 1, "Long line should be split into multiple chunks");
|
||||
assertEquals("This is a", result.get(0).text());
|
||||
}
|
||||
|
||||
@Test
|
||||
void maintainsTimestampOrder() {
|
||||
LyricLine line = new LyricLine(3000L, "One two three four five six seven eight nine ten");
|
||||
List<LyricLine> input = Collections.singletonList(line);
|
||||
List<LyricLine> result = LyricLineSplitter.splitLongLines(input);
|
||||
|
||||
for (int i = 1; i < result.size(); i++) {
|
||||
assertTrue(result.get(i).timestampMs() >= result.get(i - 1).timestampMs(),
|
||||
"Timestamps should be in order");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void sortsResultByTimestamp() {
|
||||
LyricLine line1 = new LyricLine(3000L, "Word word word word word word");
|
||||
LyricLine line2 = new LyricLine(1000L, "Another line with several words here");
|
||||
List<LyricLine> input = Arrays.asList(line1, line2);
|
||||
List<LyricLine> result = LyricLineSplitter.splitLongLines(input);
|
||||
|
||||
for (int i = 1; i < result.size(); i++) {
|
||||
assertTrue(result.get(i).timestampMs() >= result.get(i - 1).timestampMs(),
|
||||
"Result should be sorted by timestamp");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void splitsLineIntoMultipleChunksWithDecreasingDurations() {
|
||||
LyricLine line = new LyricLine(5000L, "This line has too many words in it to be short");
|
||||
List<LyricLine> input = Collections.singletonList(line);
|
||||
List<LyricLine> result = LyricLineSplitter.splitLongLines(input);
|
||||
|
||||
assertTrue(result.size() > 1, "Long line should be split");
|
||||
for (int i = 1; i < result.size(); i++) {
|
||||
assertTrue(result.get(i).timestampMs() > result.get(i - 1).timestampMs(),
|
||||
"Timestamps should be strictly increasing");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void handlesExactFourWordLine() {
|
||||
LyricLine line = new LyricLine(1000L, "One two three four");
|
||||
List<LyricLine> input = Collections.singletonList(line);
|
||||
List<LyricLine> result = LyricLineSplitter.splitLongLines(input);
|
||||
|
||||
assertEquals(1, result.size(), "Exactly 4 words should not be split");
|
||||
}
|
||||
|
||||
@Test
|
||||
void splitsFiveWordLine() {
|
||||
LyricLine line = new LyricLine(1000L, "One two three four five");
|
||||
List<LyricLine> input = Collections.singletonList(line);
|
||||
List<LyricLine> result = LyricLineSplitter.splitLongLines(input);
|
||||
|
||||
assertTrue(result.size() > 1, "5 words should be split");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.module.modules.visuals.wings;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public final class WingShapesTest {
|
||||
|
||||
@Test
|
||||
void everyTypeResolvesToNonEmptyShape() {
|
||||
for (String type : WingShapes.TYPES) {
|
||||
WingShapes.Point[] shape = WingShapes.shape(type);
|
||||
assertNotNull(shape, type);
|
||||
assertTrue(shape.length >= 10, type);
|
||||
}
|
||||
// unknown type falls back to Angelic
|
||||
assertSame(WingShapes.shape("Angelic"), WingShapes.shape("Nope"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void allShapesUseFullPointRanges() {
|
||||
for (String type : WingShapes.TYPES) {
|
||||
for (WingShapes.Point p : WingShapes.shape(type)) {
|
||||
assertTrue(p.alphaMul() > 0f && p.alphaMul() <= 1f, type + " " + p);
|
||||
assertTrue(p.x() >= 0f, type + " " + p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void ribIndicesFitShape() {
|
||||
for (String type : WingShapes.TYPES) {
|
||||
WingShapes.Point[] shape = WingShapes.shape(type);
|
||||
for (int idx : WingShapes.ribs(shape)) {
|
||||
assertTrue(idx < shape.length, type + " rib " + idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void glidePoseScalesPitchWithProgress() {
|
||||
WingShapes.Pose start = WingShapes.glidePose(0f, 0f);
|
||||
WingShapes.Pose end = WingShapes.glidePose(0f, 1f);
|
||||
assertEquals(0f, start.pitchRotation(), 0.001f);
|
||||
assertEquals(-90f, end.pitchRotation(), 0.001f);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.security;
|
||||
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.MutableComponent;
|
||||
import net.minecraft.network.chat.contents.KeybindContents;
|
||||
import net.minecraft.network.chat.contents.PlainTextContents;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Юнит-тесты чистой логики BackdoorProtection без инициализации Minecraft.
|
||||
* State очищается в @BeforeEach, т.к. INSTANCE — общий синглтон.
|
||||
*/
|
||||
public final class BackdoorProtectionTest {
|
||||
|
||||
private final BackdoorProtection service = BackdoorProtection.INSTANCE;
|
||||
|
||||
@BeforeEach
|
||||
void resetState() {
|
||||
service.resetLoadedTranslations();
|
||||
service.resetVanillaKeybinds();
|
||||
}
|
||||
|
||||
@Test
|
||||
void unknownKeyAllowedWhenNothingRegistered() {
|
||||
assertTrue(service.isTranslationAllowed("anything"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetLoadedTranslationsClearsRegisteredKeys() {
|
||||
service.registerTranslationKey("blocked.key", "server_pack");
|
||||
service.resetLoadedTranslations();
|
||||
assertTrue(service.isTranslationAllowed("blocked.key"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void remotePackTranslationBlocked() {
|
||||
service.registerTranslationKey("evil.key", "server_pack");
|
||||
assertFalse(service.isTranslationAllowed("evil.key"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void builtInPacksAreAllowedCaseInsensitive() {
|
||||
service.registerTranslationKey("menu.title", "vanilla");
|
||||
assertTrue(service.isTranslationAllowed("menu.title"));
|
||||
|
||||
service.registerTranslationKey("accessibility.title", "high_contrast");
|
||||
assertTrue(service.isTranslationAllowed("accessibility.title"));
|
||||
|
||||
service.registerTranslationKey("menu.case", "VANILLA");
|
||||
assertTrue(service.isTranslationAllowed("menu.case"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void registerTranslationKeyIgnoresBlankInput() {
|
||||
service.registerTranslationKey("", "server_pack");
|
||||
service.registerTranslationKey("good.key", "");
|
||||
assertTrue(service.isTranslationAllowed("good.key"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void keybindAllowedOnlyAfterRegistration() {
|
||||
assertFalse(service.isKeybindAllowed("key.attack"));
|
||||
service.registerVanillaKeybind("key.attack");
|
||||
assertTrue(service.isKeybindAllowed("key.attack"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetVanillaKeybindsClears() {
|
||||
service.registerVanillaKeybind("key.jump");
|
||||
service.resetVanillaKeybinds();
|
||||
assertFalse(service.isKeybindAllowed("key.jump"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void blankOrNullKeysAreAllowed() {
|
||||
assertTrue(service.isTranslationAllowed(null));
|
||||
assertTrue(service.isTranslationAllowed(""));
|
||||
assertTrue(service.isKeybindAllowed(null));
|
||||
assertTrue(service.isKeybindAllowed(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterTextReplacesBlockedTranslatableWithResolvedFallback() {
|
||||
service.registerTranslationKey("evil.key", "server_pack");
|
||||
|
||||
Component filtered = service.filterText(Component.translatable("evil.key"));
|
||||
|
||||
FilteredTranslatableTextContent content =
|
||||
assertInstanceOf(FilteredTranslatableTextContent.class, filtered.getContents());
|
||||
assertEquals("evil.key", content.getResolved());
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterTextKeepsAllowedTranslatableWithNullResolved() {
|
||||
service.registerTranslationKey("menu.ok", "vanilla");
|
||||
|
||||
Component filtered = service.filterText(Component.translatable("menu.ok"));
|
||||
|
||||
FilteredTranslatableTextContent content =
|
||||
assertInstanceOf(FilteredTranslatableTextContent.class, filtered.getContents());
|
||||
assertNull(content.getResolved());
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterTextReplacesBlockedKeybindWithPlainText() {
|
||||
Component keybind = MutableComponent.create(new KeybindContents("key.attack"));
|
||||
|
||||
Component filtered = service.filterText(keybind);
|
||||
|
||||
assertInstanceOf(PlainTextContents.class, filtered.getContents());
|
||||
assertFalse(filtered.getContents() instanceof KeybindContents);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.render.engine.renderer.ui.runtime.script;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.engine.JavetUiScriptEngine;
|
||||
|
||||
public final class JavetUiScriptEngineTest {
|
||||
@Test
|
||||
void normalizesLongPropsToJavaScriptNumbersRecursively() {
|
||||
Object normalized = JavetUiScriptEngine.plainValue(Map.of(
|
||||
"count", 37L,
|
||||
"items", List.of(Map.of("count", 64L)),
|
||||
"large", (long) Integer.MAX_VALUE + 1L
|
||||
));
|
||||
|
||||
Map<?, ?> props = assertInstanceOf(Map.class, normalized);
|
||||
assertEquals(37, props.get("count"));
|
||||
|
||||
List<?> items = assertInstanceOf(List.class, props.get("items"));
|
||||
Map<?, ?> item = assertInstanceOf(Map.class, items.getFirst());
|
||||
assertEquals(64, item.get("count"));
|
||||
|
||||
Number large = assertInstanceOf(Number.class, props.get("large"));
|
||||
assertEquals((double) Integer.MAX_VALUE + 1.0, large.doubleValue());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.util.animation;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.items.held.HoldMyItems;
|
||||
|
||||
/**
|
||||
* Математика ease-out-back для HoldMyItems.
|
||||
*/
|
||||
public final class EaseCurvesTest {
|
||||
|
||||
private static final float EPS = 1e-4f;
|
||||
private static final float DEFAULT_C1 = 1.70158f;
|
||||
|
||||
@Test
|
||||
void easeOutBackStartsAtZeroAndEndsAtOne() {
|
||||
assertEquals(0.0f, EaseCurves.easeOutBack(0.0f, DEFAULT_C1), EPS);
|
||||
assertEquals(1.0f, EaseCurves.easeOutBack(1.0f, DEFAULT_C1), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void easeOutBackOvershootsPastOne() {
|
||||
float max = 0.0f;
|
||||
for (int i = 0; i <= 100; i++) {
|
||||
max = Math.max(max, EaseCurves.easeOutBack(i / 100.0f, DEFAULT_C1));
|
||||
}
|
||||
assertTrue(max > 1.0f && max < 1.2f, "overshoot должен быть заметным, но умеренным: " + max);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shapeKeepsEndpoints() {
|
||||
assertEquals(0.0f, EaseCurves.shapeEquipProgress(0.0f, DEFAULT_C1, 1.0f), EPS);
|
||||
assertEquals(1.0f, EaseCurves.shapeEquipProgress(1.0f, DEFAULT_C1, 1.0f), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void zeroIntensityFallsBackToLinear() {
|
||||
for (int i = 0; i <= 10; i++) {
|
||||
float x = i / 10.0f;
|
||||
assertEquals(x, EaseCurves.shapeEquipProgress(x, DEFAULT_C1, 0.0f), EPS);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void fullIntensityMatchesCurve() {
|
||||
float x = 0.7f;
|
||||
assertEquals(
|
||||
EaseCurves.easeOutBack(x, DEFAULT_C1),
|
||||
EaseCurves.shapeEquipProgress(x, DEFAULT_C1, 1.0f),
|
||||
EPS
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void intensityClamps() {
|
||||
float x = 0.5f;
|
||||
assertEquals(
|
||||
EaseCurves.shapeEquipProgress(x, DEFAULT_C1, 0.0f),
|
||||
EaseCurves.shapeEquipProgress(x, DEFAULT_C1, -5.0f),
|
||||
EPS
|
||||
);
|
||||
assertEquals(
|
||||
EaseCurves.shapeEquipProgress(x, DEFAULT_C1, 1.0f),
|
||||
EaseCurves.shapeEquipProgress(x, DEFAULT_C1, 5.0f),
|
||||
EPS
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.util.animation;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.camera.ViewModel;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.items.held.HoldMyItems;
|
||||
|
||||
/**
|
||||
* Совместная работа HoldMyItems (кривая) и ViewModel (занижение):
|
||||
* конечная позиция определяется занижением, кривая добавляет перелёт.
|
||||
*/
|
||||
public final class EquipProgressComposerTest {
|
||||
|
||||
private static final float EPS = 1e-4f;
|
||||
private static final float C1 = 1.6f;
|
||||
|
||||
@Test
|
||||
void bothDisabledIsIdentity() {
|
||||
assertEquals(0.42f, EquipProgressComposer.compose(0.42f, false, 0, 0, false, 0), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void loweringAloneScalesEndpoints() {
|
||||
assertEquals(0.0f, EquipProgressComposer.compose(0.0f, false, 0, 0, true, 0.5f), EPS);
|
||||
assertEquals(0.5f, EquipProgressComposer.compose(1.0f, false, 0, 0, true, 0.5f), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void curveAloneKeepsEndpoints() {
|
||||
assertEquals(0.0f, EquipProgressComposer.compose(0.0f, true, C1, 1.0f, false, 0), EPS);
|
||||
assertEquals(1.0f, EquipProgressComposer.compose(1.0f, true, C1, 1.0f, false, 0), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void togetherRestingPointIsLowered() {
|
||||
//equip завершён (progress=1): финальная позиция = занижение, а не 1
|
||||
float rest = EquipProgressComposer.compose(1.0f, true, C1, 1.0f, true, 0.5f);
|
||||
assertEquals(0.5f, rest, EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void togetherOvershootExceedsRestingPoint() {
|
||||
float max = 0.0f;
|
||||
for (int i = 0; i <= 100; i++) {
|
||||
max = Math.max(max, EquipProgressComposer.compose(i / 100.0f, true, C1, 1.0f, true, 0.5f));
|
||||
}
|
||||
assertTrue(max > 0.5f, "перелёт кривой должен поднимать предмет выше финальной позиции: " + max);
|
||||
assertTrue(max < 0.5f * 1.2f, "перелёт умеренный: " + max);
|
||||
}
|
||||
|
||||
@Test
|
||||
void curveDisabledButLoweringOn() {
|
||||
assertEquals(0.25f, EquipProgressComposer.compose(0.5f, false, 0, 0, true, 0.5f), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void loweringDisabledButCurveOn() {
|
||||
float x = 0.7f;
|
||||
assertEquals(
|
||||
EaseCurves.shapeEquipProgress(x, C1, 1.0f),
|
||||
EquipProgressComposer.compose(x, true, C1, 1.0f, false, 0),
|
||||
EPS
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.util.animation;
|
||||
|
||||
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.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Pure-logic tests for the single-update-point animator. No Minecraft types involved.
|
||||
*/
|
||||
public final class SmoothValueTest {
|
||||
|
||||
private static final float EPS = 1.0e-4f;
|
||||
|
||||
@Test
|
||||
void reachesTargetWhenMovingUp() {
|
||||
SmoothValue value = new SmoothValue(1.5f);
|
||||
value.setTarget(true);
|
||||
|
||||
for (int i = 0; i < 200; i++) {
|
||||
value.update(0.5f);
|
||||
}
|
||||
|
||||
assertEquals(1.0f, value.value(), EPS);
|
||||
assertTrue(value.isSettled());
|
||||
}
|
||||
|
||||
@Test
|
||||
void reachesTargetWhenMovingDown() {
|
||||
SmoothValue value = new SmoothValue(1.5f, 1.0f);
|
||||
value.setInstant(true);
|
||||
value.setTarget(false);
|
||||
|
||||
for (int i = 0; i < 200; i++) {
|
||||
value.update(0.5f);
|
||||
}
|
||||
|
||||
assertEquals(0.0f, value.value(), EPS);
|
||||
assertTrue(value.isSettled());
|
||||
}
|
||||
|
||||
@Test
|
||||
void movesUpMonotonically() {
|
||||
SmoothValue value = new SmoothValue(0.8f);
|
||||
value.setTarget(true);
|
||||
|
||||
float previous = value.value();
|
||||
for (int i = 0; i < 100; i++) {
|
||||
value.update(1.0f / 3.0f);
|
||||
assertTrue(value.value() >= previous, "value must never fall while rising");
|
||||
assertTrue(value.value() <= 1.0f, "value must stay within 0..1");
|
||||
previous = value.value();
|
||||
}
|
||||
assertEquals(1.0f, value.value(), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void movesDownMonotonically() {
|
||||
SmoothValue value = new SmoothValue(0.8f);
|
||||
value.setInstant(true);
|
||||
value.setTarget(false);
|
||||
|
||||
float previous = value.value();
|
||||
for (int i = 0; i < 100; i++) {
|
||||
value.update(1.0f / 3.0f);
|
||||
assertTrue(value.value() <= previous, "value must never rise while falling");
|
||||
assertTrue(value.value() >= 0.0f, "value must stay within 0..1");
|
||||
previous = value.value();
|
||||
}
|
||||
assertEquals(0.0f, value.value(), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void setInstantTeleportsValueAndTarget() {
|
||||
SmoothValue value = new SmoothValue(1.0f);
|
||||
assertEquals(0.0f, value.value(), EPS);
|
||||
|
||||
value.setInstant(true);
|
||||
assertEquals(1.0f, value.value(), EPS);
|
||||
assertEquals(1.0f, value.target(), EPS);
|
||||
assertTrue(value.isSettled());
|
||||
|
||||
value.setInstant(false);
|
||||
assertEquals(0.0f, value.value(), EPS);
|
||||
assertEquals(0.0f, value.target(), EPS);
|
||||
assertTrue(value.isSettled());
|
||||
}
|
||||
|
||||
@Test
|
||||
void setTargetDoesNotMoveValue() {
|
||||
SmoothValue value = new SmoothValue(2.0f);
|
||||
value.setTarget(true);
|
||||
assertEquals(0.0f, value.value(), EPS, "setTarget must not advance the value");
|
||||
assertEquals(1.0f, value.target(), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void frameRateIndependent() {
|
||||
// One long step must land on the same point as many small steps of equal total time.
|
||||
float totalTicks = 2.0f;
|
||||
float speed = 1.1f;
|
||||
|
||||
SmoothValue coarse = new SmoothValue(speed);
|
||||
coarse.setTarget(true);
|
||||
coarse.update(totalTicks);
|
||||
|
||||
SmoothValue fine = new SmoothValue(speed);
|
||||
fine.setTarget(true);
|
||||
int steps = 240;
|
||||
for (int i = 0; i < steps; i++) {
|
||||
fine.update(totalTicks / steps);
|
||||
}
|
||||
|
||||
assertEquals(coarse.value(), fine.value(), 5.0e-3f);
|
||||
assertTrue(coarse.value() > 0.5f && coarse.value() < 1.0f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void zeroAndBrokenDeltasAreIgnored() {
|
||||
SmoothValue value = new SmoothValue(1.0f);
|
||||
value.setTarget(true);
|
||||
|
||||
value.update(0.0f);
|
||||
value.update(-1.0f);
|
||||
value.update(Float.NaN);
|
||||
assertEquals(0.0f, value.value(), EPS);
|
||||
assertFalse(value.isSettled());
|
||||
|
||||
value.update(1.0f);
|
||||
assertTrue(value.value() > 0.0f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void targetFlipsMidFlightReverseDirection() {
|
||||
SmoothValue value = new SmoothValue(1.0f);
|
||||
value.setTarget(true);
|
||||
value.update(1.0f);
|
||||
assertTrue(value.value() > 0.0f);
|
||||
|
||||
value.setTarget(false);
|
||||
float peak = value.value();
|
||||
value.update(1.0f);
|
||||
assertTrue(value.value() < peak, "falling must start immediately after the flip");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsBrokenSpeed() {
|
||||
assertThrows(IllegalArgumentException.class, () -> new SmoothValue(0.0f));
|
||||
assertThrows(IllegalArgumentException.class, () -> new SmoothValue(-1.0f));
|
||||
assertThrows(IllegalArgumentException.class, () -> new SmoothValue(Float.NaN));
|
||||
assertThrows(IllegalArgumentException.class, () -> new SmoothValue(Float.POSITIVE_INFINITY));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.util.animation;
|
||||
|
||||
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.assertTrue;
|
||||
|
||||
final class ThreeStageProgressTest {
|
||||
|
||||
private static final float EPS = 1.0e-3f;
|
||||
|
||||
@Test
|
||||
void stagesFollowElapsedTime() {
|
||||
assertEquals(ThreeStageProgress.Stage.APPEAR, ThreeStageProgress.stage(0, 100, 200, 300));
|
||||
assertEquals(ThreeStageProgress.Stage.EXIST, ThreeStageProgress.stage(150, 100, 200, 300));
|
||||
assertEquals(ThreeStageProgress.Stage.DISAPPEAR, ThreeStageProgress.stage(350, 100, 200, 300));
|
||||
assertEquals(ThreeStageProgress.Stage.FINISHED, ThreeStageProgress.stage(700, 100, 200, 300));
|
||||
}
|
||||
|
||||
@Test
|
||||
void visibilityStartsAtZeroAndEndsAtOneDuringAppear() {
|
||||
assertEquals(0.0f, ThreeStageProgress.visibility(0, 100, 200, 300), EPS);
|
||||
assertEquals(1.0f, ThreeStageProgress.visibility(100, 100, 200, 300), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void visibilityStaysFullDuringExist() {
|
||||
assertEquals(1.0f, ThreeStageProgress.visibility(150, 100, 200, 300), EPS);
|
||||
assertEquals(1.0f, ThreeStageProgress.visibility(299, 100, 200, 300), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void visibilityFadesToZeroDuringDisappear() {
|
||||
assertEquals(1.0f, ThreeStageProgress.visibility(300, 100, 200, 300), EPS);
|
||||
assertTrue(ThreeStageProgress.visibility(450, 100, 200, 300) < 1.0f);
|
||||
assertEquals(0.0f, ThreeStageProgress.visibility(600, 100, 200, 300), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void visibilityIsZeroAfterFinished() {
|
||||
assertEquals(0.0f, ThreeStageProgress.visibility(1000, 100, 200, 300), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void isFinishedMatchesStage() {
|
||||
assertFalse(ThreeStageProgress.isFinished(0, 100, 200, 300));
|
||||
assertFalse(ThreeStageProgress.isFinished(599, 100, 200, 300));
|
||||
assertTrue(ThreeStageProgress.isFinished(600, 100, 200, 300));
|
||||
}
|
||||
|
||||
@Test
|
||||
void zeroDurationStagesDoNotDivideByZero() {
|
||||
assertEquals(1.0f, ThreeStageProgress.visibility(0, 0, 200, 300), EPS);
|
||||
assertEquals(ThreeStageProgress.Stage.EXIST, ThreeStageProgress.stage(0, 0, 200, 300));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.util.animation.hand;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/** Фаза и амплитуда покачивания при ходьбе. */
|
||||
public final class BobMathTest {
|
||||
|
||||
@Test
|
||||
void amplitudeIsZeroWhenIdle() {
|
||||
assertEquals(0.0f, BobMath.amplitude(0.0f), 1.0e-6f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void amplitudeGrowsWithSpeed() {
|
||||
float slow = BobMath.amplitude(0.25f);
|
||||
float fast = BobMath.amplitude(0.9f);
|
||||
assertTrue(slow > 0.0f && slow < fast, "амплитуда должна расти со скоростью");
|
||||
assertTrue(slow < 1.0f && fast <= 1.0f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void amplitudeClampedToOne() {
|
||||
assertEquals(1.0f, BobMath.amplitude(2.5f), 1.0e-6f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void brokenSpeedGivesZeroAmplitude() {
|
||||
assertEquals(0.0f, BobMath.amplitude(Float.NaN), 1.0e-6f);
|
||||
assertEquals(0.0f, BobMath.amplitude(Float.NEGATIVE_INFINITY), 1.0e-6f);
|
||||
assertEquals(0.0f, BobMath.amplitude(-0.5f), 1.0e-6f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void lateralStartsAtZeroAndIsBounded() {
|
||||
assertEquals(0.0f, BobMath.lateral(0.0f), 1.0e-6f);
|
||||
for (float phase = 0.0f; phase < 40.0f; phase += 0.125f) {
|
||||
float value = BobMath.lateral(phase);
|
||||
assertTrue(value >= -1.0f && value <= 1.0f, "латераль вне диапазона в фазе " + phase);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void lateralIsAntisymmetric() {
|
||||
for (float phase = -3.0f; phase <= 3.0f; phase += 0.25f) {
|
||||
assertEquals(-BobMath.lateral(phase), BobMath.lateral(-phase), 1.0e-5f);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void lateralRepeatsAfterFullCycle() {
|
||||
float cycle = (float) (2.0 * Math.PI / BobMath.STEP_RATE);
|
||||
assertEquals(BobMath.lateral(0.3f), BobMath.lateral(0.3f + cycle), 1.0e-4f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void lateralReachesPeakAtExpectedPhase() {
|
||||
assertEquals(1.0f, BobMath.lateral(BobMath.lateralPeakPhase()), 1.0e-4f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void verticalIsNeverPositive() {
|
||||
for (float phase = 0.0f; phase < 40.0f; phase += 0.125f) {
|
||||
assertTrue(BobMath.vertical(phase) <= 0.0f, "вертикаль должна тянуть вниз");
|
||||
assertTrue(BobMath.vertical(phase) >= -1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void verticalDipsTwicePerCycle() {
|
||||
float cycle = (float) (2.0 * Math.PI / BobMath.STEP_RATE);
|
||||
int dips = 0;
|
||||
float previous = 0.0f;
|
||||
for (float t = 0.0f; t <= cycle; t += 0.01f) {
|
||||
float value = BobMath.vertical(t);
|
||||
if (previous > -0.999f && value <= -0.999f) {
|
||||
dips++;
|
||||
}
|
||||
previous = value;
|
||||
}
|
||||
assertEquals(2, dips, "за цикл должно быть два проседания (два шага)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void stepRateMatchesVanillaLimbFrequency() {
|
||||
assertEquals(0.6662f, BobMath.STEP_RATE, 1.0e-6f);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.util.animation.hand;
|
||||
|
||||
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.assertTrue;
|
||||
|
||||
/** Сборка позы руки из эффектов. */
|
||||
public final class HandPoseMathTest {
|
||||
|
||||
private static final float EPS = 1.0e-6f;
|
||||
|
||||
@Test
|
||||
void swayZeroStrengthIsZero() {
|
||||
assertEquals(HandPoseMath.ZERO, HandPoseMath.sway(6.0f, 4.0f, 0.0f));
|
||||
}
|
||||
|
||||
@Test
|
||||
void swayStrengthScalesOutput() {
|
||||
HandPoseMath.Pose unit = HandPoseMath.sway(3.0f, 2.0f, 1.0f);
|
||||
HandPoseMath.Pose doubled = HandPoseMath.sway(3.0f, 2.0f, 2.0f);
|
||||
assertEquals(unit.dx() * 2.0f, doubled.dx(), EPS);
|
||||
assertEquals(unit.ry() * 2.0f, doubled.ry(), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void swaySignsFollowCameraTurn() {
|
||||
// Поворот вправо (yaw+): предмет отстаёт влево на экране (+X) и доворачивается назад (ry-).
|
||||
HandPoseMath.Pose yaw = HandPoseMath.sway(2.0f, 0.0f, 1.0f);
|
||||
assertTrue(yaw.dx() > 0.0f);
|
||||
assertTrue(yaw.ry() < 0.0f);
|
||||
assertTrue(yaw.rz() > 0.0f);
|
||||
// Взгляд вниз (pitch+): предмет отстаёт вверх (+Y) и задирается (rx-).
|
||||
HandPoseMath.Pose pitch = HandPoseMath.sway(0.0f, 2.0f, 1.0f);
|
||||
assertTrue(pitch.dy() > 0.0f);
|
||||
assertTrue(pitch.rx() < 0.0f);
|
||||
assertEquals(0.0f, pitch.dx(), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void swayMirrorsForOppositeLag() {
|
||||
HandPoseMath.Pose positive = HandPoseMath.sway(2.0f, 1.0f, 1.0f);
|
||||
HandPoseMath.Pose negative = HandPoseMath.sway(-2.0f, -1.0f, 1.0f);
|
||||
assertEquals(-positive.dx(), negative.dx(), EPS);
|
||||
assertEquals(-positive.dy(), negative.dy(), EPS);
|
||||
assertEquals(-positive.rx(), negative.rx(), EPS);
|
||||
assertEquals(-positive.ry(), negative.ry(), EPS);
|
||||
assertEquals(-positive.rz(), negative.rz(), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void bobZeroAmplitudeIsZero() {
|
||||
assertEquals(HandPoseMath.ZERO, HandPoseMath.bob(3.0f, 0.0f, 1.0f));
|
||||
assertEquals(HandPoseMath.ZERO, HandPoseMath.bob(3.0f, 0.8f, 0.0f));
|
||||
}
|
||||
|
||||
@Test
|
||||
void bobIsBoundedAndScales() {
|
||||
for (float phase = 0.0f; phase < 20.0f; phase += 0.1f) {
|
||||
HandPoseMath.Pose pose = HandPoseMath.bob(phase, 1.0f, 1.0f);
|
||||
assertTrue(Math.abs(pose.dx()) <= 0.045f + EPS);
|
||||
assertTrue(Math.abs(pose.dy()) <= 0.035f + EPS);
|
||||
assertTrue(Math.abs(pose.rz()) <= 2.5f + EPS);
|
||||
assertTrue(pose.dy() <= 0.0f, "вертикаль боба не должна поднимать предмет");
|
||||
}
|
||||
HandPoseMath.Pose half = HandPoseMath.bob(0.7f, 0.5f, 1.0f);
|
||||
HandPoseMath.Pose full = HandPoseMath.bob(0.7f, 1.0f, 1.0f);
|
||||
assertEquals(full.dx() * 0.5f, half.dx(), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void driftPullsEachArmTowardCenter() {
|
||||
HandPoseMath.Pose right = HandPoseMath.drift(1.0f, 1.0f, true);
|
||||
assertTrue(right.dx() > 0.0f, "правая рука тянется влево на экране (+X)");
|
||||
HandPoseMath.Pose left = HandPoseMath.drift(1.0f, 1.0f, false);
|
||||
assertTrue(left.dx() < 0.0f, "левая рука тянется вправо на экране (-X)");
|
||||
assertEquals(-right.dx(), left.dx(), EPS);
|
||||
assertEquals(-right.ry(), left.ry(), EPS);
|
||||
assertEquals(-right.rz(), left.rz(), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void driftZeroProgressAndStrengthGiveZero() {
|
||||
assertEquals(HandPoseMath.ZERO, HandPoseMath.drift(0.0f, 1.0f, true));
|
||||
assertEquals(HandPoseMath.ZERO, HandPoseMath.drift(1.0f, 0.0f, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void joinDropAtFullProgressIsZero() {
|
||||
assertEquals(HandPoseMath.ZERO, HandPoseMath.joinDrop(1.0f));
|
||||
}
|
||||
|
||||
@Test
|
||||
void joinDropEasesOutMonotonically() {
|
||||
float previousDrop = Float.POSITIVE_INFINITY;
|
||||
for (float p = 0.0f; p <= 1.0f; p += 0.05f) {
|
||||
float drop = -HandPoseMath.joinDrop(p).dy();
|
||||
assertTrue(drop >= 0.0f && drop <= 0.85f + EPS);
|
||||
assertTrue(drop <= previousDrop, "подъём должен замедляться к концу");
|
||||
previousDrop = drop;
|
||||
}
|
||||
assertEquals(0.85f, -HandPoseMath.joinDrop(0.0f).dy(), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void addCombinesPosesAndZeroIsIdentity() {
|
||||
HandPoseMath.Pose a = HandPoseMath.sway(2.0f, 1.0f, 1.0f);
|
||||
HandPoseMath.Pose b = HandPoseMath.bob(0.5f, 1.0f, 1.0f);
|
||||
HandPoseMath.Pose sum = HandPoseMath.add(a, b);
|
||||
assertEquals(a.dx() + b.dx(), sum.dx(), EPS);
|
||||
assertEquals(a.rx() + b.rx(), sum.rx(), EPS);
|
||||
assertEquals(a, HandPoseMath.add(a, HandPoseMath.ZERO));
|
||||
assertEquals(a, HandPoseMath.add(HandPoseMath.ZERO, a));
|
||||
}
|
||||
|
||||
@Test
|
||||
void gripZeroParamsIsZero() {
|
||||
assertFalse(HandPoseMath.grip(0f, 0f, 0f, 0f, 0f, 0f, 0f, 1f).isSignificant(EPS));
|
||||
}
|
||||
|
||||
@Test
|
||||
void gripMirrorsBySide() {
|
||||
HandPoseMath.Pose right = HandPoseMath.grip(0.3f, 0.1f, 0.05f, 0.14f, 5f, 10f, 15f, 1.0f);
|
||||
HandPoseMath.Pose left = HandPoseMath.grip(0.3f, 0.1f, 0.05f, 0.14f, 5f, 10f, 15f, -1.0f);
|
||||
assertEquals(-right.dx(), left.dx(), EPS);
|
||||
assertEquals(right.dy(), left.dy(), EPS, "slide не зависит от стороны");
|
||||
assertEquals(right.dz(), left.dz(), EPS, "extend/forward не зависят от стороны");
|
||||
assertEquals(right.rx(), left.rx(), EPS, "tilt не зависит от стороны");
|
||||
assertEquals(-right.ry(), left.ry(), EPS);
|
||||
assertEquals(-right.rz(), left.rz(), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void gripExtendPullsCloserAndAcrossFist() {
|
||||
HandPoseMath.Pose pose = HandPoseMath.grip(1.0f, 0f, 0f, 0f, 0f, 0f, 0f, 1.0f);
|
||||
assertTrue(pose.dz() < 0.0f, "extend тянет предмет ближе к камере (-Z)");
|
||||
assertTrue(pose.dx() > 0.0f, "extend на правой руке сдвигает вбок по стороне");
|
||||
}
|
||||
|
||||
@Test
|
||||
void significanceThresholdWorks() {
|
||||
assertFalse(HandPoseMath.ZERO.isSignificant(1.0e-4f));
|
||||
assertTrue(HandPoseMath.sway(1.0f, 0.0f, 1.0f).isSignificant(1.0e-4f));
|
||||
assertFalse(HandPoseMath.drift(1.0f, 1.0f, true).isSignificant(20.0f));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.util.animation.hand.state;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/** Покадровое состояние: одна правка цели за кадр, реакции на смену мира/игрока. */
|
||||
public final class HandAnimStateTest {
|
||||
|
||||
private static final Object PLAYER_A = new Object();
|
||||
private static final Object PLAYER_B = new Object();
|
||||
private static final Object LEVEL_1 = new Object();
|
||||
private static final Object LEVEL_2 = new Object();
|
||||
|
||||
private static final float DT = 0.5f;
|
||||
|
||||
@Test
|
||||
void firstFrameDoesNotProduceSwayImpulse() {
|
||||
HandAnimState state = new HandAnimState();
|
||||
state.beginFrame(PLAYER_A, LEVEL_1, 30.0f, -15.0f, false, false, DT);
|
||||
assertEquals(0.0f, state.sway().yawLag(), 1.0e-6f);
|
||||
state.beginFrame(PLAYER_A, LEVEL_1, 30.0f, -15.0f, false, false, DT);
|
||||
assertEquals(0.0f, state.sway().yawLag(), 1.0e-6f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void swayReactsToMouseMotion() {
|
||||
HandAnimState state = new HandAnimState();
|
||||
state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, false, false, DT);
|
||||
for (int i = 0; i < 8; i++) {
|
||||
state.beginFrame(PLAYER_A, LEVEL_1, 6.0f * (i + 1), 0.0f, false, false, DT);
|
||||
}
|
||||
assertTrue(state.sway().yawLag() > 0.0f, "вращение камеры должно давать отставание");
|
||||
}
|
||||
|
||||
@Test
|
||||
void yawWrapMatchesEquivalentTurn() {
|
||||
HandAnimState acrossBoundary = new HandAnimState();
|
||||
acrossBoundary.beginFrame(PLAYER_A, LEVEL_1, 175.0f, 0.0f, false, false, DT);
|
||||
acrossBoundary.beginFrame(PLAYER_A, LEVEL_1, -175.0f, 0.0f, false, false, DT);
|
||||
|
||||
HandAnimState plain = new HandAnimState();
|
||||
plain.beginFrame(PLAYER_A, LEVEL_1, -5.0f, 0.0f, false, false, DT);
|
||||
plain.beginFrame(PLAYER_A, LEVEL_1, 5.0f, 0.0f, false, false, DT);
|
||||
|
||||
assertEquals(plain.sway().yawLag(), acrossBoundary.sway().yawLag(), 1.0e-4f,
|
||||
"переход через границу мира равен обычному повороту на те же 10 градусов");
|
||||
}
|
||||
|
||||
@Test
|
||||
void teleportResetsSwayInsteadOfImpulse() {
|
||||
HandAnimState state = new HandAnimState();
|
||||
state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, false, false, DT);
|
||||
for (int i = 0; i < 8; i++) {
|
||||
state.beginFrame(PLAYER_A, LEVEL_1, 6.0f * (i + 1), 0.0f, false, false, DT);
|
||||
}
|
||||
assertTrue(state.sway().yawLag() > 0.5f);
|
||||
state.beginFrame(PLAYER_A, LEVEL_1, 180.0f, 0.0f, false, false, DT);
|
||||
assertEquals(0.0f, state.sway().yawLag(), 1.0e-6f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void driftIsPerHand() {
|
||||
HandAnimState state = new HandAnimState();
|
||||
state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, true, false, DT);
|
||||
for (int i = 0; i < 20; i++) {
|
||||
state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, true, false, DT);
|
||||
}
|
||||
assertTrue(state.drift(true) > 0.9f, "дрейф в главной руке должен подняться к единице");
|
||||
assertEquals(0.0f, state.drift(false), 1.0e-6f, "вторая рука не должна дёргаться");
|
||||
}
|
||||
|
||||
@Test
|
||||
void driftSettlesBackWhenUsageStops() {
|
||||
HandAnimState state = new HandAnimState();
|
||||
state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, true, false, DT);
|
||||
for (int i = 0; i < 30; i++) {
|
||||
state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, true, false, DT);
|
||||
}
|
||||
for (int i = 0; i < 120; i++) {
|
||||
state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, false, false, DT);
|
||||
}
|
||||
assertEquals(0.0f, state.drift(true), 1.0e-3f, "после окончания использования дрейф должен уйти в ноль");
|
||||
}
|
||||
|
||||
@Test
|
||||
void joinProgressRisesThenStays() {
|
||||
HandAnimState state = new HandAnimState();
|
||||
state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, false, false, DT);
|
||||
float previous = 0.0f;
|
||||
for (int i = 0; i < 200; i++) {
|
||||
state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, false, false, DT);
|
||||
float value = state.joinProgress();
|
||||
assertTrue(value >= previous - 1.0e-6f, "прогресс появления не должен откатываться");
|
||||
previous = value;
|
||||
}
|
||||
assertEquals(1.0f, state.joinProgress(), 1.0e-3f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void playerOrLevelChangeRestartsJoin() {
|
||||
HandAnimState state = new HandAnimState();
|
||||
state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, false, false, DT);
|
||||
for (int i = 0; i < 200; i++) {
|
||||
state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, false, false, DT);
|
||||
}
|
||||
assertEquals(1.0f, state.joinProgress(), 1.0e-3f);
|
||||
|
||||
state.beginFrame(PLAYER_B, LEVEL_1, 0.0f, 0.0f, false, false, DT);
|
||||
assertTrue(state.joinProgress() < 0.1f, "смена игрока (респавн) перезапускает появление");
|
||||
state.beginFrame(PLAYER_B, LEVEL_2, 0.0f, 0.0f, false, false, DT);
|
||||
assertTrue(state.joinProgress() < 0.1f, "смена мира тоже перезапускает появление");
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullPlayerResetsEverything() {
|
||||
HandAnimState state = new HandAnimState();
|
||||
state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, true, false, DT);
|
||||
for (int i = 0; i < 30; i++) {
|
||||
state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, true, false, DT);
|
||||
}
|
||||
state.beginFrame(null, null, 0.0f, 0.0f, false, false, DT);
|
||||
assertEquals(0.0f, state.drift(true), 1.0e-6f);
|
||||
assertEquals(0.0f, state.joinProgress(), 1.0e-6f);
|
||||
assertTrue(state.sway().isSettled());
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetReturnsToFreshState() {
|
||||
HandAnimState state = new HandAnimState();
|
||||
state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, true, false, DT);
|
||||
for (int i = 0; i < 30; i++) {
|
||||
state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, true, false, DT);
|
||||
}
|
||||
state.reset();
|
||||
assertEquals(0.0f, state.drift(true), 1.0e-6f);
|
||||
assertEquals(0.0f, state.joinProgress(), 1.0e-6f);
|
||||
state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, false, false, DT);
|
||||
assertEquals(0.0f, state.sway().yawLag(), 1.0e-6f);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.util.animation.hand.state;
|
||||
|
||||
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.assertTrue;
|
||||
|
||||
/** Математика отставания предмета от поворота камеры. */
|
||||
public final class SwayStateTest {
|
||||
|
||||
private static final float DT = 0.5f;
|
||||
|
||||
@Test
|
||||
void withoutInputStaysZero() {
|
||||
SwayState state = new SwayState();
|
||||
for (int i = 0; i < 60; i++) {
|
||||
state.update(0.0f, 0.0f, DT);
|
||||
}
|
||||
assertEquals(0.0f, state.yawLag(), 1.0e-6f);
|
||||
assertEquals(0.0f, state.pitchLag(), 1.0e-6f);
|
||||
assertTrue(state.isSettled());
|
||||
}
|
||||
|
||||
@Test
|
||||
void yawImpulseProducesPositiveLag() {
|
||||
SwayState state = new SwayState();
|
||||
state.update(4.0f, 0.0f, DT);
|
||||
assertTrue(state.yawLag() > 0.0f, "движение мыши вправо должно давать положительное отставание");
|
||||
assertEquals(0.0f, state.pitchLag(), 1.0e-6f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void lagDecaysBackToZeroWhenMouseStill() {
|
||||
SwayState state = new SwayState();
|
||||
state.update(4.0f, -2.0f, DT);
|
||||
for (int i = 0; i < 400; i++) {
|
||||
state.update(0.0f, 0.0f, DT);
|
||||
}
|
||||
assertTrue(state.isSettled(), "без ввода отставание должно полностью затухнуть");
|
||||
}
|
||||
|
||||
@Test
|
||||
void lagIsClamped() {
|
||||
SwayState state = new SwayState();
|
||||
for (int i = 0; i < 40; i++) {
|
||||
state.update(180.0f, -120.0f, DT);
|
||||
}
|
||||
assertTrue(state.yawLag() <= SwayState.MAX_YAW_LAG_DEG + 1.0e-4f);
|
||||
assertTrue(state.pitchLag() >= -SwayState.MAX_PITCH_LAG_DEG - 1.0e-4f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void mirroredInputsGiveMirroredLag() {
|
||||
SwayState left = new SwayState();
|
||||
SwayState right = new SwayState();
|
||||
for (int i = 0; i < 10; i++) {
|
||||
left.update(-3.0f, 1.5f, DT);
|
||||
right.update(3.0f, -1.5f, DT);
|
||||
}
|
||||
assertEquals(left.yawLag(), -right.yawLag(), 1.0e-5f);
|
||||
assertEquals(left.pitchLag(), -right.pitchLag(), 1.0e-5f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void oppositeInputPullsBackThroughZero() {
|
||||
SwayState state = new SwayState();
|
||||
for (int i = 0; i < 10; i++) {
|
||||
state.update(3.0f, 0.0f, DT);
|
||||
}
|
||||
float peak = state.yawLag();
|
||||
for (int i = 0; i < 10; i++) {
|
||||
state.update(-3.0f, 0.0f, DT);
|
||||
}
|
||||
assertTrue(state.yawLag() < peak, "обратное движение должно гасить отставание");
|
||||
}
|
||||
|
||||
@Test
|
||||
void nonPositiveDeltaIgnored() {
|
||||
SwayState state = new SwayState();
|
||||
state.update(2.0f, 1.0f, DT);
|
||||
float yaw = state.yawLag();
|
||||
state.update(2.0f, 1.0f, 0.0f);
|
||||
state.update(2.0f, 1.0f, -1.0f);
|
||||
assertEquals(yaw, state.yawLag(), 1.0e-6f);
|
||||
assertFalse(state.isSettled());
|
||||
}
|
||||
|
||||
@Test
|
||||
void brokenInputsAreSafe() {
|
||||
SwayState state = new SwayState();
|
||||
state.update(Float.NaN, Float.POSITIVE_INFINITY, DT);
|
||||
state.update(1.0f, 1.0f, Float.NaN);
|
||||
assertEquals(0.0f, state.yawLag(), 1.0e-6f);
|
||||
assertEquals(0.0f, state.pitchLag(), 1.0e-6f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void wrapDegreesKeepsResultInHalfCircle() {
|
||||
assertEquals(-20.0f, SwayState.wrapDegrees(340.0f), 1.0e-4f);
|
||||
assertEquals(20.0f, SwayState.wrapDegrees(-340.0f), 1.0e-4f);
|
||||
assertEquals(-180.0f, SwayState.wrapDegrees(180.0f), 1.0e-4f);
|
||||
assertEquals(0.0f, SwayState.wrapDegrees(360.0f), 1.0e-4f);
|
||||
assertEquals(5.0f, SwayState.wrapDegrees(365.0f), 1.0e-4f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetClearsEverything() {
|
||||
SwayState state = new SwayState();
|
||||
state.update(4.0f, 2.0f, DT);
|
||||
state.reset();
|
||||
assertTrue(state.isSettled());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.util.combat;
|
||||
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import dev.loki.lovisual.util.combat.tracking.ReachMeter;
|
||||
|
||||
public final class ReachMeterTest {
|
||||
|
||||
private static final double EPS = 1e-6;
|
||||
|
||||
@BeforeEach
|
||||
void reset() {
|
||||
ReachMeter.reset();
|
||||
}
|
||||
|
||||
@Test
|
||||
void distanceToBoxOutsidePoint() {
|
||||
AABB box = new AABB(0, 0, 0, 1, 1, 1);
|
||||
|
||||
assertEquals(2.0, ReachMeter.distanceToBox(new Vec3(3, 0.5, 0.5), box), EPS);
|
||||
assertEquals(Math.sqrt(3.0), ReachMeter.distanceToBox(new Vec3(2, 2, 2), box), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void distanceToBoxInsideIsZero() {
|
||||
AABB box = new AABB(0, 0, 0, 1, 1, 1);
|
||||
|
||||
assertEquals(0.0, ReachMeter.distanceToBox(new Vec3(0.5, 0.5, 0.5), box), EPS);
|
||||
assertEquals(0.0, ReachMeter.distanceToBox(new Vec3(0, 1, 0.5), box), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void distanceToBoxHandlesNulls() {
|
||||
assertEquals(-1.0, ReachMeter.distanceToBox(null, new AABB(0, 0, 0, 1, 1, 1)), EPS);
|
||||
assertEquals(-1.0, ReachMeter.distanceToBox(Vec3.ZERO, null), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void recordTracksLastAndMax() {
|
||||
ReachMeter.record(2.5, 1000L);
|
||||
ReachMeter.record(3.4, 2000L);
|
||||
ReachMeter.record(1.2, 3000L);
|
||||
|
||||
assertEquals(1.2, ReachMeter.lastDistance(), EPS);
|
||||
assertEquals(3.4, ReachMeter.sessionMax(), EPS);
|
||||
assertEquals(3000L, ReachMeter.lastAttackAtMs());
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidDistancesIgnored() {
|
||||
ReachMeter.record(2.5, 1000L);
|
||||
ReachMeter.record(Double.NaN, 2000L);
|
||||
ReachMeter.record(-5.0, 3000L);
|
||||
|
||||
assertEquals(2.5, ReachMeter.lastDistance(), EPS);
|
||||
assertEquals(1000L, ReachMeter.lastAttackAtMs());
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetClearsState() {
|
||||
ReachMeter.record(4.0, 1000L);
|
||||
ReachMeter.reset();
|
||||
|
||||
assertEquals(-1.0, ReachMeter.lastDistance(), EPS);
|
||||
assertEquals(-1.0, ReachMeter.sessionMax(), EPS);
|
||||
assertEquals(0L, ReachMeter.lastAttackAtMs());
|
||||
}
|
||||
|
||||
@Test
|
||||
void formatMeters() {
|
||||
assertEquals("-", ReachMeter.format(-1.0));
|
||||
assertEquals("3.00 m", ReachMeter.format(3.0));
|
||||
assertEquals("3.12 m", ReachMeter.format(3.124));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.util.mctiers;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
final class MCTiersTierTest {
|
||||
|
||||
@Test
|
||||
void labelsMatchKnownTiers() {
|
||||
assertEquals("HT1", new MCTiersTier(1, false).label());
|
||||
assertEquals("LT2", new MCTiersTier(2, false).label());
|
||||
assertEquals("LT3", new MCTiersTier(3, false).label());
|
||||
assertEquals("LT4", new MCTiersTier(4, false).label());
|
||||
assertEquals("T5", new MCTiersTier(5, false).label());
|
||||
}
|
||||
|
||||
@Test
|
||||
void colorsAreDistinctPerTier() {
|
||||
int t1 = new MCTiersTier(1, false).colorArgb();
|
||||
int t2 = new MCTiersTier(2, false).colorArgb();
|
||||
int t3 = new MCTiersTier(3, false).colorArgb();
|
||||
int t4 = new MCTiersTier(4, false).colorArgb();
|
||||
assertEquals(4, java.util.Set.of(t1, t2, t3, t4).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void modeApiKeyMapsFixTypeToApiMode() {
|
||||
assertEquals("vanilla", MCTiersTier.modeApiKey("Vanilla"));
|
||||
assertEquals("uhc", MCTiersTier.modeApiKey("UHC"));
|
||||
assertEquals("pot", MCTiersTier.modeApiKey("Pot"));
|
||||
assertEquals("neth_pot", MCTiersTier.modeApiKey("NethOP"));
|
||||
assertEquals("smp", MCTiersTier.modeApiKey("SMP"));
|
||||
assertEquals("sword", MCTiersTier.modeApiKey("Sword"));
|
||||
assertEquals("axe", MCTiersTier.modeApiKey("Axe"));
|
||||
assertEquals("mace", MCTiersTier.modeApiKey("Mace"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void modeApiKeyDefaultsToVanillaForUnknownOrNull() {
|
||||
assertEquals("vanilla", MCTiersTier.modeApiKey(null));
|
||||
assertEquals("vanilla", MCTiersTier.modeApiKey("Nonsense"));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.util.physics;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
final class TrinketPhysicsTest {
|
||||
|
||||
@Test
|
||||
void restIsZeroAfterReset() {
|
||||
TrinketPhysics p = new TrinketPhysics();
|
||||
p.tick(0.05, 20.0, 1.0, 0.98, 0.7, 0.9, 1.0);
|
||||
p.reset();
|
||||
assertEquals(0.0, p.x());
|
||||
assertEquals(0.0, p.y());
|
||||
assertEquals(0.0, p.z());
|
||||
assertEquals(0.0, p.speed());
|
||||
}
|
||||
|
||||
@Test
|
||||
void restingBallBouncesImmediatelyUnderGravity() {
|
||||
// y=0 — это уровень земли (у ног владельца), поэтому шарик из состояния покоя
|
||||
// на первом же тике проваливается под гравитацией и тут же отскакивает обратно —
|
||||
// это и есть наблюдаемый эффект гравитации в модели "ноль высоты = земля".
|
||||
TrinketPhysics p = new TrinketPhysics();
|
||||
assertTrue(p.tick(0.05, 20.0, 1.0, 1.0, 0.7, 0.9, 5.0), "гравитация должна вжать шарик в землю на первом тике");
|
||||
assertEquals(0.0, p.y(), 1.0e-9);
|
||||
assertTrue(p.speed() > 0.0, "после отскока есть исходящая от земли скорость");
|
||||
}
|
||||
|
||||
@Test
|
||||
void bouncesWhenHittingGroundFromAbove() {
|
||||
TrinketPhysics p = new TrinketPhysics();
|
||||
boolean bouncedAtLeastOnce = false;
|
||||
for (int i = 0; i < 200; i++) {
|
||||
if (p.tick(0.05, 20.0, 1.0, 0.999, 0.7, 0.9, 5.0)) {
|
||||
bouncedAtLeastOnce = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertTrue(bouncedAtLeastOnce, "падение с гравитацией должно рано или поздно удариться о землю (y=0)");
|
||||
assertEquals(0.0, p.y(), 1.0e-9, "после отскока шарик стоит ровно на земле");
|
||||
}
|
||||
|
||||
@Test
|
||||
void repeatedBouncesLoseEnergyOverTime() {
|
||||
// Каждый отскок домножает вертикальную скорость на bounce=0.5 — пик высоты после
|
||||
// каждого следующего отскока должен быть меньше предыдущего (затухающий мяч).
|
||||
TrinketPhysics p = new TrinketPhysics();
|
||||
double firstBounceSpeed = -1;
|
||||
double secondBounceSpeed = -1;
|
||||
int bounces = 0;
|
||||
for (int i = 0; i < 1000 && bounces < 2; i++) {
|
||||
if (p.tick(0.02, 20.0, 1.0, 1.0, 0.5, 1.0, 100.0)) {
|
||||
bounces++;
|
||||
if (bounces == 1) firstBounceSpeed = p.speed();
|
||||
else secondBounceSpeed = p.speed();
|
||||
}
|
||||
}
|
||||
assertTrue(firstBounceSpeed > 0 && secondBounceSpeed > 0, "должно было произойти минимум два отскока");
|
||||
assertTrue(secondBounceSpeed < firstBounceSpeed,
|
||||
"второй отскок должен быть слабее первого: " + secondBounceSpeed + " >= " + firstBounceSpeed);
|
||||
}
|
||||
|
||||
@Test
|
||||
void tetherKeepsBallWithinBoundedDistance() {
|
||||
// Прямой доступ к полям недоступен (инкапсуляция) — проверяем инвариант на реальной
|
||||
// траектории: с гравитацией/отскоками и малым tetherRadius шарик не должен улетать
|
||||
// в бесконечность, пружина стабилизирует расстояние от центра.
|
||||
TrinketPhysics p = new TrinketPhysics();
|
||||
double maxDist = 0;
|
||||
for (int i = 0; i < 2000; i++) {
|
||||
p.tick(0.05, 20.0, 1.0, 0.98, 0.6, 0.9, 1.5);
|
||||
double dist = Math.sqrt(p.x() * p.x() + p.z() * p.z());
|
||||
maxDist = Math.max(maxDist, dist);
|
||||
}
|
||||
assertTrue(maxDist < 50.0, "пружина должна удерживать шарик в разумных пределах, было " + maxDist);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.util.protection;
|
||||
|
||||
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.assertTrue;
|
||||
|
||||
public final class NotifyThrottleTest {
|
||||
|
||||
@Test
|
||||
void firstCallIsAllowed() {
|
||||
NotifyThrottle throttle = new NotifyThrottle(1000L);
|
||||
assertTrue(throttle.tryAcquire(5_000L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void secondCallInsideIntervalIsSuppressed() {
|
||||
NotifyThrottle throttle = new NotifyThrottle(1_000L);
|
||||
assertTrue(throttle.tryAcquire(5_000L));
|
||||
assertFalse(throttle.tryAcquire(5_999L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void callExactlyAtIntervalBoundaryIsAllowed() {
|
||||
NotifyThrottle throttle = new NotifyThrottle(1_000L);
|
||||
assertTrue(throttle.tryAcquire(5_000L));
|
||||
assertTrue(throttle.tryAcquire(6_000L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void callAfterIntervalIsAllowed() {
|
||||
NotifyThrottle throttle = new NotifyThrottle(1_500L);
|
||||
assertTrue(throttle.tryAcquire(10_000L));
|
||||
assertFalse(throttle.tryAcquire(11_499L));
|
||||
assertTrue(throttle.tryAcquire(11_500L));
|
||||
assertTrue(throttle.tryAcquire(13_001L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void remainingMsIsMonotonicAndClampedToZero() {
|
||||
NotifyThrottle throttle = new NotifyThrottle(1_000L);
|
||||
assertEquals(0L, throttle.remainingMs(1_000L), "never fired");
|
||||
throttle.tryAcquire(2_000L);
|
||||
assertEquals(1_000L, throttle.remainingMs(2_000L));
|
||||
assertEquals(500L, throttle.remainingMs(2_500L));
|
||||
assertEquals(0L, throttle.remainingMs(3_000L));
|
||||
assertEquals(0L, throttle.remainingMs(10_000L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void backwardsClockDoesNotLockOutCaller() {
|
||||
NotifyThrottle throttle = new NotifyThrottle(1_000L);
|
||||
throttle.tryAcquire(5_000L);
|
||||
assertTrue(throttle.tryAcquire(1_000L), "moving time back should allow an emission");
|
||||
}
|
||||
|
||||
@Test
|
||||
void nonPositiveIntervalAlwaysAllows() {
|
||||
NotifyThrottle zero = new NotifyThrottle(0L);
|
||||
assertTrue(zero.tryAcquire(100L));
|
||||
assertTrue(zero.tryAcquire(100L));
|
||||
assertTrue(zero.tryAcquire(100L));
|
||||
|
||||
NotifyThrottle negative = new NotifyThrottle(-5L);
|
||||
assertTrue(negative.tryAcquire(1L));
|
||||
assertTrue(negative.tryAcquire(2L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetRestoresInitialState() {
|
||||
NotifyThrottle throttle = new NotifyThrottle(60_000L);
|
||||
assertTrue(throttle.tryAcquire(1_000L));
|
||||
assertFalse(throttle.tryAcquire(2_000L));
|
||||
throttle.reset();
|
||||
assertEquals(0L, throttle.remainingMs(2_000L));
|
||||
assertTrue(throttle.tryAcquire(2_000L));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.util.protection;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public final class SlotLockPolicyTest {
|
||||
|
||||
private static Map<String, Boolean> states(int... lockedZeroBasedSlots) {
|
||||
Map<String, Boolean> out = SlotLockPolicy.defaultSlotStates();
|
||||
for (int slot : lockedZeroBasedSlots) {
|
||||
out.put(SlotLockPolicy.slotKey(slot), true);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultsUnlockAllNineSlots() {
|
||||
Map<String, Boolean> defaults = SlotLockPolicy.defaultSlotStates();
|
||||
assertEquals(9, defaults.size());
|
||||
for (int slot = 0; slot < 9; slot++) {
|
||||
assertFalse(SlotLockPolicy.isSlotLocked(defaults, slot), "slot " + slot + " must be unlocked");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void slotKeyIsOneBased() {
|
||||
assertEquals("slot1", SlotLockPolicy.slotKey(0));
|
||||
assertEquals("slot5", SlotLockPolicy.slotKey(4));
|
||||
assertEquals("slot9", SlotLockPolicy.slotKey(8));
|
||||
}
|
||||
|
||||
@Test
|
||||
void lockedSlotIsBlockedWhenNearbyCheckDisabled() {
|
||||
assertTrue(SlotLockPolicy.isDropBlocked(states(4), 4, false, false));
|
||||
assertTrue(SlotLockPolicy.isDropBlocked(states(4), 4, true, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void lockedSlotRespectsOnlyPlayersNearby() {
|
||||
assertTrue(SlotLockPolicy.isDropBlocked(states(0), 0, true, true));
|
||||
assertFalse(SlotLockPolicy.isDropBlocked(states(0), 0, false, true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unlockedSlotIsNeverBlocked() {
|
||||
assertFalse(SlotLockPolicy.isDropBlocked(states(1, 2), 0, true, false));
|
||||
assertFalse(SlotLockPolicy.isDropBlocked(states(1, 2), 0, false, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void hotbarEdgesAreCovered() {
|
||||
Map<String, Boolean> edgeStates = states(0, 8);
|
||||
assertTrue(SlotLockPolicy.isDropBlocked(edgeStates, 0, false, false));
|
||||
assertTrue(SlotLockPolicy.isDropBlocked(edgeStates, 8, false, false));
|
||||
assertFalse(SlotLockPolicy.isDropBlocked(edgeStates, 4, false, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void outOfRangeSlotsAreNeverBlocked() {
|
||||
Map<String, Boolean> all = states(0, 1, 2, 3, 4, 5, 6, 7, 8);
|
||||
assertFalse(SlotLockPolicy.isDropBlocked(all, -1, true, false));
|
||||
assertFalse(SlotLockPolicy.isDropBlocked(all, 9, true, false));
|
||||
assertFalse(SlotLockPolicy.isSlotLocked(all, -1));
|
||||
assertFalse(SlotLockPolicy.isSlotLocked(all, 9));
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingOrNullStatesAreNeverBlocked() {
|
||||
assertFalse(SlotLockPolicy.isDropBlocked(null, 0, true, false));
|
||||
assertFalse(SlotLockPolicy.isDropBlocked(new HashMap<>(), 0, true, false));
|
||||
assertFalse(SlotLockPolicy.isSlotLocked(null, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unknownOrNonBooleanEntriesAreIgnored() {
|
||||
Map<String, Boolean> odd = new HashMap<>();
|
||||
odd.put("slot", true);
|
||||
odd.put("slot10", true);
|
||||
odd.put("slot0", true);
|
||||
assertFalse(SlotLockPolicy.isSlotLocked(odd, 0));
|
||||
assertFalse(SlotLockPolicy.isDropBlocked(odd, 0, true, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void nearbyCheckDoesNotAffectUnlockedSlots() {
|
||||
assertFalse(SlotLockPolicy.isDropBlocked(states(8), 3, true, true));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.util.text;
|
||||
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.Style;
|
||||
import net.minecraft.network.chat.TextColor;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import dev.loki.lovisual.util.text.format.LegacyTextUtil;
|
||||
|
||||
/**
|
||||
* convertLegacyCodes строит дерево: Component.empty() → сегмент-обёртка → стилизованные листья.
|
||||
* firstLeaf() спускается к первому листу с текстом.
|
||||
*/
|
||||
public final class LegacyTextUtilTest {
|
||||
|
||||
private static Component firstLeaf(Component in) {
|
||||
Component cur = in;
|
||||
while (!cur.getSiblings().isEmpty()) {
|
||||
cur = cur.getSiblings().get(0);
|
||||
}
|
||||
return cur;
|
||||
}
|
||||
|
||||
/** Все листовые компоненты с непустым текстом, в порядке обхода. */
|
||||
private static java.util.List<Component> leaves(Component in) {
|
||||
java.util.List<Component> out = new java.util.ArrayList<>();
|
||||
collectLeaves(in, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
private static void collectLeaves(Component in, java.util.List<Component> out) {
|
||||
if (in.getSiblings().isEmpty()) {
|
||||
if (!in.getString().isEmpty()) out.add(in);
|
||||
return;
|
||||
}
|
||||
for (Component child : in.getSiblings()) {
|
||||
collectLeaves(child, out);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void ampersandToSectionConvertsColorCodes() {
|
||||
assertEquals("§aGreen", LegacyTextUtil.ampersandToSection("&aGreen"));
|
||||
assertEquals("plain text", LegacyTextUtil.ampersandToSection("plain text"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void ampersandToSectionConvertsHexNotation() {
|
||||
assertEquals("§x§F§F§0§0§0§0X", LegacyTextUtil.ampersandToSection("&#FF0000X"));
|
||||
assertEquals("§x§0§1§2§3§4§5", LegacyTextUtil.ampersandToSection("&x&0&1&2&3&4&5"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void ampersandToSectionKeepsLoneAmpersands() {
|
||||
assertEquals("at & end", LegacyTextUtil.ampersandToSection("at & end"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void ampersandToSectionHandlesNullAndEmpty() {
|
||||
assertNull(LegacyTextUtil.ampersandToSection(null));
|
||||
assertEquals("", LegacyTextUtil.ampersandToSection(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void stripLegacyRemovesColorAndDecorationCodes() {
|
||||
assertEquals("Green", LegacyTextUtil.stripLegacy("§aGreen"));
|
||||
assertEquals("RedBold", LegacyTextUtil.stripLegacy("&cRed§lBold"));
|
||||
assertEquals("Hex", LegacyTextUtil.stripLegacy("§x§F§F§0§0§0§0Hex"));
|
||||
assertEquals("", LegacyTextUtil.stripLegacy(null));
|
||||
assertEquals("no codes", LegacyTextUtil.stripLegacy("no codes"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void convertLegacyCodesReturnsSameInstanceWithoutCodes() {
|
||||
Component in = Component.literal("plain");
|
||||
assertSame(in, LegacyTextUtil.convertLegacyCodes(in));
|
||||
}
|
||||
|
||||
@Test
|
||||
void convertLegacyCodesHandlesNull() {
|
||||
assertEquals("", LegacyTextUtil.convertLegacyCodes(null).getString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void convertLegacyCodesAppliesVanillaColor() {
|
||||
Component out = LegacyTextUtil.convertLegacyCodes(Component.literal("&aHi"));
|
||||
|
||||
assertEquals("Hi", out.getString());
|
||||
assertEquals(TextColor.fromRgb(0x55FF55), firstLeaf(out).getStyle().getColor());
|
||||
}
|
||||
|
||||
@Test
|
||||
void convertLegacyCodesAppliesHexColor() {
|
||||
Component out = LegacyTextUtil.convertLegacyCodes(Component.literal("§x§F§F§0§0§0§0Hex"));
|
||||
|
||||
assertEquals("Hex", out.getString());
|
||||
assertEquals(TextColor.fromRgb(0xFF0000), firstLeaf(out).getStyle().getColor());
|
||||
}
|
||||
|
||||
@Test
|
||||
void convertLegacyCodesAppliesBedrockMinecoinGold() {
|
||||
Component out = LegacyTextUtil.convertLegacyCodes(Component.literal("§gCoin"));
|
||||
|
||||
assertEquals("Coin", out.getString());
|
||||
assertEquals(TextColor.fromRgb(0xDDD605), firstLeaf(out).getStyle().getColor());
|
||||
}
|
||||
|
||||
@Test
|
||||
void convertLegacyCodesKeepsBaseStyle() {
|
||||
Style base = Style.EMPTY.withBold(true);
|
||||
Component in = Component.literal("§aHi").setStyle(base);
|
||||
|
||||
Component out = LegacyTextUtil.convertLegacyCodes(in);
|
||||
Style leafStyle = firstLeaf(out).getStyle();
|
||||
|
||||
assertEquals(TextColor.fromRgb(0x55FF55), leafStyle.getColor());
|
||||
assertEquals(Boolean.TRUE, leafStyle.isBold());
|
||||
}
|
||||
|
||||
@Test
|
||||
void convertLegacyCodesSplitsMultiStyleSegments() {
|
||||
Component out = LegacyTextUtil.convertLegacyCodes(Component.literal("&aRed&lBold"));
|
||||
|
||||
assertEquals("RedBold", out.getString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void colorCodeResetsDecorationsToBase() {
|
||||
// парсер трактует цвет по-ваналлийски: код цвета возвращает стиль к base + цвет,
|
||||
// более ранние decoration-коды из строки сбрасываются (селектор isColor должен их распознавать)
|
||||
Component out = LegacyTextUtil.convertLegacyCodes(Component.literal("§lBold§aGreen"));
|
||||
|
||||
assertEquals("BoldGreen", out.getString());
|
||||
var leafList = leaves(out);
|
||||
assertEquals(2, leafList.size());
|
||||
|
||||
Style boldStyle = leafList.get(0).getStyle();
|
||||
assertEquals(Boolean.TRUE, boldStyle.isBold());
|
||||
|
||||
Style greenStyle = leafList.get(1).getStyle();
|
||||
assertEquals(TextColor.fromRgb(0x55FF55), greenStyle.getColor());
|
||||
assertEquals(Boolean.FALSE, greenStyle.isBold());
|
||||
}
|
||||
|
||||
@Test
|
||||
void decorationCodeKeepsColorAndAddsFlag() {
|
||||
Component out = LegacyTextUtil.convertLegacyCodes(Component.literal("&aRed&lBold"));
|
||||
|
||||
var leafList = leaves(out);
|
||||
assertEquals(2, leafList.size());
|
||||
Style boldStyle = leafList.get(1).getStyle();
|
||||
assertEquals(Boolean.TRUE, boldStyle.isBold());
|
||||
assertEquals(TextColor.fromRgb(0x55FF55), boldStyle.getColor());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.util.text;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import dev.loki.lovisual.util.text.mask.NameMasker;
|
||||
|
||||
public final class NameMaskerTest {
|
||||
|
||||
@Test
|
||||
void masksNicknameTokenDeterministically() {
|
||||
NameMasker masker = new NameMasker("Player", List.of());
|
||||
String first = masker.mask("gg Notch_123 gg");
|
||||
String second = masker.mask("gg Notch_123 gg");
|
||||
|
||||
assertNotEquals("gg Notch_123 gg", first);
|
||||
assertEquals(first, second);
|
||||
assertEquals("gg Player1 gg", first);
|
||||
}
|
||||
|
||||
@Test
|
||||
void differentNamesGetDifferentOrdinals() {
|
||||
NameMasker masker = new NameMasker("Player", List.of());
|
||||
String masked = masker.mask("Notch and Herobrine");
|
||||
|
||||
assertEquals("Player1 and Player2", masked);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shortTokensAndNumbersAreKept() {
|
||||
NameMasker masker = new NameMasker("Player", List.of());
|
||||
|
||||
assertEquals("ab 12345 x9", masker.mask("ab 12345 x9"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void wordsInsideTokensAreNotSplit() {
|
||||
NameMasker masker = new NameMasker("Player", List.of());
|
||||
|
||||
// The nickname-like token wins over word matching because it is longer.
|
||||
String masked = masker.mask("Steve2016");
|
||||
assertEquals("Player1", masked);
|
||||
}
|
||||
|
||||
@Test
|
||||
void serverWordsAreMaskedCaseInsensitively() {
|
||||
NameMasker masker = new NameMasker("Server", List.of("anarchy"));
|
||||
|
||||
assertEquals("Welcome to Server-2!", masker.mask("Welcome to Anarchy-2!"));
|
||||
assertEquals("Server", masker.mask("ANARCHY"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void wordMatchTakesPriorityOverTokenMasking() {
|
||||
NameMasker masker = new NameMasker("Server", List.of("anarchy"));
|
||||
|
||||
// "anarchy" matches as a word first, so masking stops there.
|
||||
assertEquals("Server", masker.mask("anarchy"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullAndEmptyPassThrough() {
|
||||
NameMasker masker = new NameMasker("Player", List.of());
|
||||
|
||||
assertEquals(null, masker.mask(null));
|
||||
assertEquals("", masker.mask(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void blankReplacementFallsBackToPlayer() {
|
||||
NameMasker masker = new NameMasker(" ", List.of());
|
||||
|
||||
assertEquals("Player1", masker.mask("Notch"));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.util.text;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import dev.loki.lovisual.util.text.mask.NameProtector;
|
||||
|
||||
public final class NameProtectorTest {
|
||||
|
||||
@Test
|
||||
void replacesNameCaseInsensitively() {
|
||||
NameProtector p = new NameProtector();
|
||||
p.put("Notch", "Steve");
|
||||
|
||||
assertEquals("hi Steve!", p.replace("hi NOTCH!"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void onlyMatchesWholeWords() {
|
||||
NameProtector p = new NameProtector();
|
||||
p.put("Notch", "Steve");
|
||||
|
||||
assertEquals("Notchless is a biome-like word", p.replace("Notchless is a biome-like word"));
|
||||
assertEquals("xNotch Notchy", p.replace("xNotch Notchy"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void longerNamesWinOverSubstrings() {
|
||||
NameProtector p = new NameProtector();
|
||||
p.put("Cool", "X");
|
||||
p.put("CoolGuy", "Y");
|
||||
|
||||
assertEquals("I am Y", p.replace("I am CoolGuy"));
|
||||
assertEquals("X beans", p.replace("Cool beans"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void underscoresCountAsWordCharacters() {
|
||||
NameProtector p = new NameProtector();
|
||||
p.put("lob", "cat");
|
||||
|
||||
// "lobo" и "_lob_" не матчатся (word-символы вокруг), isolated "lob" — матчится
|
||||
assertEquals("lobo_here _lob_ cat", p.replace("lobo_here _lob_ lob"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleNamesInOneString() {
|
||||
NameProtector p = new NameProtector();
|
||||
p.put("Alice", "**");
|
||||
p.put("Bob", "**");
|
||||
|
||||
assertEquals("** and **", p.replace("Alice and Bob"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void blankNamesIgnoredAndEmptyWhenNone() {
|
||||
NameProtector p = new NameProtector();
|
||||
p.put(null, "x");
|
||||
p.put(" ", "x");
|
||||
|
||||
assertTrue(p.isEmpty());
|
||||
assertEquals("anything", p.replace("anything"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyAliasDeletesName() {
|
||||
NameProtector p = new NameProtector();
|
||||
p.put("Notch", "");
|
||||
|
||||
assertEquals("hi !", p.replace("hi Notch!"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullAndEmptyTextPassThrough() {
|
||||
NameProtector p = new NameProtector();
|
||||
p.put("Notch", "X");
|
||||
|
||||
assertEquals(null, p.replace(null));
|
||||
assertEquals("", p.replace(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void exactReplacementOnlyForWholeString() {
|
||||
NameProtector p = new NameProtector();
|
||||
p.put("Notch", "X");
|
||||
|
||||
assertEquals("X", p.replaceExact("Notch"));
|
||||
assertEquals("Notchy", p.replaceExact("Notchy"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void clearRemovesAllAliases() {
|
||||
NameProtector p = new NameProtector();
|
||||
p.put("Notch", "X");
|
||||
p.clear();
|
||||
|
||||
assertTrue(p.isEmpty());
|
||||
assertEquals("Notch", p.replace("Notch"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void duplicateNameOverwritesAlias() {
|
||||
NameProtector p = new NameProtector();
|
||||
p.put("Notch", "X");
|
||||
p.put("Notch", "Y");
|
||||
|
||||
assertEquals("Y", p.replace("Notch"));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* This file is part of the LoVisual Client distribution.
|
||||
* Copyright (c) 2026 loki5512344.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.util.world;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
final class GpsBearingTest {
|
||||
|
||||
private static final float EPS = 1.0e-3f;
|
||||
|
||||
@Test
|
||||
void bearingPointsSouthWhenTargetAheadOnZ() {
|
||||
// +Z (юг) при dx=0 -> азимут 0, совпадает с конвенцией getYRot()
|
||||
assertEquals(0.0f, GpsBearing.bearingDegrees(0.0, 10.0), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void bearingPointsWestWhenTargetOnNegativeX() {
|
||||
assertEquals(90.0f, GpsBearing.bearingDegrees(-10.0, 0.0), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void arrowRotationIsZeroWhenFacingTarget() {
|
||||
assertEquals(0.0f, GpsBearing.arrowRotationDegrees(45.0f, 45.0f), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void arrowRotationWrapsToShortestPath() {
|
||||
// цель за спиной (разница 180) должна остаться в диапазоне (-180, 180]
|
||||
float result = GpsBearing.arrowRotationDegrees(170.0f, -170.0f);
|
||||
assertEquals(-20.0f, result, EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void wrapDegreesStaysInHalfOpenRange() {
|
||||
assertEquals(-179.0f, GpsBearing.wrapDegrees(181.0f), EPS);
|
||||
assertEquals(179.0f, GpsBearing.wrapDegrees(-181.0f), EPS);
|
||||
assertEquals(0.0f, GpsBearing.wrapDegrees(360.0f), EPS);
|
||||
assertEquals(90.0f, GpsBearing.wrapDegrees(90.0f), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void horizontalDistanceIsPythagorean() {
|
||||
assertEquals(5.0, GpsBearing.horizontalDistance(3.0, 4.0), 1.0e-9);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue