Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package net.tfminecraft.magic.command;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import net.tfminecraft.magic.artifact.config.ArtifactRarityRegistry;
import net.tfminecraft.magic.artifact.config.ArtifactTypeDef;
import net.tfminecraft.magic.artifact.config.ArtifactTypeRegistry;

/** Completes the finite portions of the diagnostic's single-token path grammar. */
final class ArtifactPathCompletion {
private ArtifactPathCompletion() {}

static List<String> complete(String token) {
String lower = token.toLowerCase(Locale.ROOT);
if (!lower.startsWith("magic.(")) {
return extend("", token, List.of("magic.artifact", "magic.("));
}
if (token.indexOf(')') >= 0 || token.chars().anyMatch(Character::isWhitespace)) return List.of();
int start = token.lastIndexOf(';') + 1;
if (start == 0) start = "magic.(".length();
String head = token.substring(0, start);
String current = token.substring(start);
Set<String> used = new HashSet<>();
String primary = null;
for (String previous : token.substring("magic.(".length(), start).split(";")) {
if (previous.isEmpty()) continue;
String[] pair = previous.split("=", 2);
String key = pair[0].toLowerCase(Locale.ROOT);
if (key.equals("primary") || key.equals("element")) {
if (pair.length != 2 || type(pair[1]) == null) return List.of();
primary = pair[1];
used.add("primary"); used.add("element");
} else if (key.equals("rarity")) {
if (pair.length != 2 || ArtifactRarityRegistry.getAll().stream()
.noneMatch(rarity -> rarity.getId().equalsIgnoreCase(pair[1]))) return List.of();
used.add(key);
} else {
if (type(key) == null) return List.of();
if (pair.length == 2) {
try { Double.parseDouble(pair[1]); }
catch (NumberFormatException ex) { return List.of(); }
}
used.add(key);
}
}
int equals = current.indexOf('=');
if (equals >= 0) {
String key = current.substring(0, equals).toLowerCase(Locale.ROOT);
if (used.contains(key)) return List.of();
List<String> values = new ArrayList<>();
if (key.equals("primary") || key.equals("element")) {
ArtifactTypeRegistry.getAll().forEach(type -> values.add(type.getElementId()));
} else if (key.equals("rarity")) {
ArtifactTypeDef selected = type(primary);
ArtifactRarityRegistry.getAll().stream()
.filter(rarity -> selected == null || selected.hasRarity(rarity.getId()))
.forEach(rarity -> values.add(rarity.getId()));
} else {
return List.of(); // Numeric caps stay manual.
}
List<String> endings = new ArrayList<>();
values.forEach(value -> { endings.add(value + ")"); endings.add(value + ";"); });
return extend(head + current.substring(0, equals + 1), current.substring(equals + 1), endings);
}
List<String> keys = new ArrayList<>();
for (String key : List.of("primary", "element", "rarity")) {
if (!used.contains(key)) keys.add(key + "=");
}
for (String id : ArtifactTypeRegistry.getAllIds()) {
if (!used.contains(id.toLowerCase(Locale.ROOT))) {
keys.add(id + ")"); keys.add(id + ";"); keys.add(id + "=");
}
}
return extend(head, current, keys);
}

private static ArtifactTypeDef type(String id) {
if (id == null) return null;
return ArtifactTypeRegistry.getAll().stream()
.filter(type -> type.getElementId().equalsIgnoreCase(id)).findFirst().orElse(null);
}

private static List<String> extend(String head, String typed, List<String> candidates) {
return candidates.stream().filter(candidate -> candidate.regionMatches(true, 0, typed, 0, typed.length()))
.map(candidate -> head + typed + candidate.substring(typed.length())).distinct().toList();
}
}
27 changes: 23 additions & 4 deletions src/main/java/net/tfminecraft/magic/command/MagicCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -669,7 +669,8 @@ public List<String> onTabComplete(CommandSender sender, Command command, String
}
boolean give = WeaponGiveCommand.allowed(sender);
boolean admin = hasAdmin(sender);
boolean rune = hasRuneKeybind(sender);
boolean isPlayer = sender instanceof Player;
boolean rune = isPlayer && hasRuneKeybind(sender);
if (!admin && !rune && !give) {
return Collections.emptyList();
}
Expand All @@ -682,7 +683,7 @@ public List<String> onTabComplete(CommandSender sender, Command command, String
options.add("rune");
}
if (admin) {
options.addAll(SUBCOMMANDS);
options.addAll(isPlayer ? SUBCOMMANDS : List.of("reload", "artifact", "resonance"));
}
return filterPrefix(options, args[0]);
}
Expand All @@ -707,6 +708,9 @@ public List<String> onTabComplete(CommandSender sender, Command command, String
if (!admin) {
return Collections.emptyList();
}
if (!isPlayer && List.of("open", "refresh", "rune", "fillchest", "shrine").contains(args[0].toLowerCase(Locale.ROOT))) {
return Collections.emptyList();
}
if ("fillchest".equalsIgnoreCase(args[0])) {
if (args.length == 2) {
List<String> options = new ArrayList<>();
Expand Down Expand Up @@ -753,9 +757,16 @@ public List<String> onTabComplete(CommandSender sender, Command command, String
}
if (args.length == 2) {
return filterPrefix(
List.of("roll", "give", "create", "path", "setfill", "setmuffle", "unmuffle"),
isPlayer ? List.of("roll", "give", "create", "path", "setfill", "setmuffle", "unmuffle")
: List.of("roll", "give", "path"),
args[1]);
}
if (!isPlayer && !List.of("roll", "give", "path").contains(args[1].toLowerCase(Locale.ROOT))) {
return Collections.emptyList();
}
if ("path".equalsIgnoreCase(args[1])) {
return args.length == 3 ? ArtifactPathCompletion.complete(args[2]) : Collections.emptyList();
}
if ("setfill".equalsIgnoreCase(args[1])) {
if (args.length == 3) {
return filterPrefix(ElementRegistry.getAllIds(), args[2]);
Expand Down Expand Up @@ -791,9 +802,17 @@ private static List<String> completeRollArgs(String[] args, int elementIndex) {
}
if (args.length == elementIndex + 1) {
List<String> options = new ArrayList<>();
String primary = args[elementIndex - 1];
boolean random = "random".equalsIgnoreCase(primary);
List<ArtifactTypeDef> types = ArtifactTypeRegistry.getAll().stream()
.filter(ArtifactTypeDef::isEnabled)
.filter(type -> random ? type.getWeight() > 0 && Double.isFinite(type.getWeight())
&& net.tfminecraft.magic.artifact.config.ArtifactGeneratorCache.inRandomPool(type.getElementId())
: type.getElementId().equalsIgnoreCase(primary)).toList();
if (types.isEmpty()) return Collections.emptyList();
options.add("roll");
for (ArtifactRarityDef rarity : ArtifactRarityRegistry.getAll()) {
options.add(rarity.getId());
if (types.stream().anyMatch(type -> type.hasRarity(rarity.getId()))) options.add(rarity.getId());
}
return filterPrefix(options, args[elementIndex]);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
package net.tfminecraft.magic.command;

import java.util.List;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;

import net.tfminecraft.magic.Magic;
import net.tfminecraft.magic.Messages;

public final class ResonanceCommand implements CommandExecutor {
public final class ResonanceCommand implements CommandExecutor, TabCompleter {

@Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
return List.of();
}

@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
Expand Down
18 changes: 16 additions & 2 deletions src/main/java/net/tfminecraft/magic/command/WeaponGiveCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,24 @@ static List<String> complete(CommandSender sender, String[] args) {
else if (!"give".equalsIgnoreCase(args[1])) return List.of();
else if (args.length == 3) Bukkit.getOnlinePlayers().forEach(p -> options.add(p.getName()));
else if (args.length == 4) ArchetypeRegistry.getAll().keySet().forEach(t -> options.add(t.name().toLowerCase(Locale.ROOT)));
else if (args.length == 5) options.addAll(ElementRegistry.getAllIds());
else if (args.length == 5) ElementRegistry.getAllIds().stream().filter(ElementVisibility::shownOnCharge).forEach(options::add);
else if (args.length >= 7) {
GearType type = GearType.fromId(args[3]);
PartRegistry.matching(null, type).forEach(p -> options.add(p.getId()));
ArchetypeDef archetype = ArchetypeRegistry.get(type);
if (archetype == null) return List.of();
Map<String, PartDef> selected = new LinkedHashMap<>();
for (int i = 6; i < args.length - 1; i++) {
PartDef part = PartRegistry.get(args[i]);
if (part == null || part.isDisabled() || !part.supports(type)
|| selected.putIfAbsent(part.getPartType(), part) != null) return List.of();
}
List<String> open = PartSlots.open(archetype, selected.get(PartSlots.CORE));
if (!open.containsAll(selected.keySet())) return List.of();
PartRegistry.matching(null, type).stream()
.filter(part -> open.contains(part.getPartType()) && !selected.containsKey(part.getPartType()))
.filter(part -> !PartSlots.CORE.equals(part.getPartType())
|| PartSlots.open(archetype, part).containsAll(selected.keySet()))
.forEach(part -> options.add(part.getId()));
}
String prefix = args[args.length - 1].toLowerCase(Locale.ROOT);
return options.stream().filter(s -> s.toLowerCase(Locale.ROOT).startsWith(prefix)).sorted().toList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ void reloadPermissionOffersAdminTabsWithoutRuneAccess() {
assertTrue(f.tab("resonance", "set", "Mage", "fire", "").isEmpty());
assertTrue(f.tab("shrine", "fill", "fire", "").isEmpty());
ArtifactRarityRegistry.register(ArtifactGenerationTest.rarity("rare", 1, 1, 1));
assertTrue(f.tab("artifact", "roll", "fire", "ra").contains("rare"));
assertTrue(f.tab("artifact", "roll", "fire", "ra").isEmpty());
}

@Test
Expand Down
2 changes: 1 addition & 1 deletion src/test/java/net/tfminecraft/magic/CommandTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ void chestModesAndTabSuggestionsRespectPermissionsEnabledTypesAndPositions() {
assertEquals(List.of("Mage"), tab("artifact", "give", ""));
assertEquals(List.of("random", "fire"), tab("artifact", "give", "Mage", ""));
assertEquals(List.of("roll"), tab("artifact", "give", "Mage", "fire", ""));
assertTrue(tab("artifact", "path", "").isEmpty());
assertEquals(List.of("magic.artifact", "magic.("), tab("artifact", "path", ""));
assertTrue(tab("unknown", "").isEmpty());
}

Expand Down
96 changes: 96 additions & 0 deletions src/test/java/net/tfminecraft/magic/CompletionRegressionTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package net.tfminecraft.magic;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

import java.util.List;
import java.util.Map;
import net.tfminecraft.magic.artifact.config.*;
import net.tfminecraft.magic.artifact.path.ArtifactPathParser;
import net.tfminecraft.magic.command.ResonanceCommand;
import org.bukkit.command.CommandSender;
import org.bukkit.command.PluginCommand;
import org.junit.jupiter.api.*;

class CompletionRegressionTest {
CommandTest fixture = new CommandTest();
@BeforeEach void setup() { fixture.setup(); ArtifactGeneratorCache.reset(); }
@AfterEach void cleanup() { fixture.cleanup(); ArtifactGeneratorCache.reset(); }

List<String> console(String... args) {
var sender = mock(CommandSender.class);
when(sender.hasPermission(anyString())).thenReturn(true);
return fixture.command.onTabComplete(sender, null, "magic", args);
}

@Test void consoleOnlySeesExecutableRoutesAtEveryDepth() {
assertEquals(List.of("weapon", "reload", "artifact", "resonance"), console(""));
assertEquals(List.of("reload", "resonance"), console("RE"));
assertEquals(List.of("roll", "give", "path"), console("artifact", ""));
for (String root : List.of("open", "refresh", "rune", "fillchest", "shrine")) {
assertTrue(console(root, "").isEmpty());
assertTrue(console(root, "fill", "").isEmpty());
}
for (String action : List.of("create", "setfill", "setmuffle", "unmuffle")) {
assertTrue(console("artifact", action, "").isEmpty());
}
assertEquals(List.of("random", "fire"), console("artifact", "roll", ""));
assertEquals(List.of("Mage"), console("artifact", "give", ""));
assertEquals(List.of("magic.artifact"), console("artifact", "path", "magic.a"));
assertEquals(List.of("give"), console("weapon", ""));
assertEquals(List.of("get", "set", "add", "reset"), console("resonance", ""));
assertTrue(fixture.tab("artifact", "").contains("create"));
}

@Test void raritiesFollowSelectedBracketsAndRandomPool() {
ArtifactRarityRegistry.register(ArtifactGenerationTest.rarity("common", 1, 1, 1));
ArtifactRarityRegistry.register(ArtifactGenerationTest.rarity("rare", 1, 1, 1));
ArtifactTypeRegistry.register(new ArtifactTypeDef("ice", "names", "models", true, 1,
Map.of("rare", new CapRange(5, 10)), Map.of()));
ArtifactTypeRegistry.register(new ArtifactTypeDef("zero", "names", "models", true, 0,
Map.of("rare", new CapRange(5, 10)), Map.of()));
ArtifactTypeRegistry.register(new ArtifactTypeDef("infinite", "names", "models", true, Double.POSITIVE_INFINITY,
Map.of("rare", new CapRange(5, 10)), Map.of()));
assertEquals(List.of("roll", "common"), fixture.tab("artifact", "roll", "FiRe", ""));
assertEquals(List.of("common"), fixture.tab("artifact", "give", "Mage", "fire", "CO"));
assertTrue(fixture.tab("artifact", "roll", "water", "").isEmpty());
assertTrue(fixture.tab("artifact", "roll", "missing", "").isEmpty());
ArtifactGeneratorCache.setRandomPool(List.of("fire", "zero", "infinite"));
assertEquals(List.of("roll", "common"), fixture.tab("artifact", "roll", "random", ""));
ArtifactGeneratorCache.reset();
assertEquals(List.of("roll", "common", "rare"), fixture.tab("artifact", "roll", "random", ""));
}

@Test void pathCompletesFiniteGrammarAndPreservesEarlierTokenText() {
ArtifactRarityRegistry.register(ArtifactGenerationTest.rarity("common", 1, 1, 1));
ArtifactRarityRegistry.register(ArtifactGenerationTest.rarity("rare", 1, 1, 1));
assertEquals(List.of("magic.artifact", "magic.("), fixture.tab("artifact", "path", ""));
assertEquals(List.of("magic.artifact"), fixture.tab("artifact", "path", "magic.a"));
assertTrue(fixture.tab("artifact", "path", "Magic.(Pri").contains("Magic.(Primary="));
String typed = "Magic.(primary=Fi";
assertEquals(List.of(typed + "re)", typed + "re;"), fixture.tab("artifact", "path", typed));
String withCap = "magic.(primary=fire;fire=12.5;rarity=CO";
assertEquals(List.of(withCap + "mmon)", withCap + "mmon;"), fixture.tab("artifact", "path", withCap));
for (String candidate : fixture.tab("artifact", "path", withCap)) {
if (candidate.endsWith(")")) assertNotNull(ArtifactPathParser.parse(candidate));
}
assertTrue(fixture.tab("artifact", "path", "magic.(primary=fire;rarity=ra").isEmpty());
assertTrue(fixture.tab("artifact", "path", "magic.(fire=").isEmpty());
assertTrue(fixture.tab("artifact", "path", "magic.(fire=12").isEmpty());
assertTrue(fixture.tab("artifact", "path", "magic.(primary=fire)").isEmpty());
assertFalse(fixture.tab("artifact", "path", "magic.(primary=fire;").contains("magic.(primary=fire;element="));
assertTrue(fixture.tab("artifact", "path", "magic.artifact", "").isEmpty());
assertTrue(fixture.tab("artifact", "unknown", "").isEmpty());
}

@Test void resonanceExecutorSuppressesDefaultNamesIncludingAlias() throws Exception {
var ctor = PluginCommand.class.getDeclaredConstructor(String.class, org.bukkit.plugin.Plugin.class);
ctor.setAccessible(true);
var registered = ctor.newInstance("resonance", Magic.plugin);
registered.setExecutor(new ResonanceCommand());
for (String alias : List.of("resonance", "res")) {
assertTrue(registered.tabComplete(fixture.player, alias, new String[]{""}).isEmpty());
assertTrue(registered.tabComplete(fixture.player, alias, new String[]{"Ma", ""}).isEmpty());
}
}
}
29 changes: 29 additions & 0 deletions src/test/java/net/tfminecraft/magic/WeaponGiveTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,33 @@ void reject(int index, String value, String message) {
when(sender.hasPermission("custom.staff")).thenReturn(false);
assertTrue(tab("weapon","give","").isEmpty()); assertTrue(tab("").isEmpty());
}
@Test void elementsAndPartSlotsMatchAssemblyRules() {
ElementRegistry.register(DomainTest.element("shadow"));
try (var visibility = mockStatic(ElementVisibility.class)) {
visibility.when(() -> ElementVisibility.shownOnCharge("fire")).thenReturn(true);
assertEquals(List.of("fire"), tab("weapon", "give", "Mage", "staff", ""));
assertTrue(tab("weapon", "give", "Mage", "staff", "Sh").isEmpty());
}
ArchetypeRegistry.register(new ArchetypeDef(GearType.STAFF, "Staff", "v.STICK", "v.STICK", false,
List.of("core", "handle", "tome"), Map.of()));
PartRegistry.register(new PartDef("limited", "Core", "core", 1, Set.of(GearType.STAFF), "v.STICK",
Map.of(), List.of("handle"), Map.of(), Map.of(), List.of(), "", 1, false));
PartRegistry.register(part("handle", "handle", false, GearType.STAFF));
PartRegistry.register(part("tome", "tome", false, GearType.STAFF));
PartRegistry.register(part("extra", "blade", false, GearType.STAFF));
PartRegistry.register(part("disabled", "handle", true, GearType.STAFF));
PartRegistry.register(part("wand", "handle", false, GearType.WAND));
assertEquals(List.of("core", "handle", "limited", "tome"), tab("weapon", "give", "Mage", "staff", "fire", "10", ""));
assertEquals(List.of("limited"), tab("weapon", "give", "Mage", "STAFF", "fire", "10", "LIM"));
assertEquals(List.of("handle"), tab("weapon", "give", "Mage", "staff", "fire", "10", "limited", "H"));
assertTrue(tab("weapon", "give", "Mage", "staff", "fire", "10", "limited", "handle", "").isEmpty());
assertEquals(List.of("core"), tab("weapon", "give", "Mage", "staff", "fire", "10", "tome", "c"));
assertTrue(tab("weapon", "give", "Mage", "staff", "fire", "10", "tome", "l").isEmpty());
for (String previous : List.of("missing", "disabled", "wand", "extra")) {
assertTrue(tab("weapon", "give", "Mage", "staff", "fire", "10", previous, "").isEmpty());
}
assertTrue(tab("weapon", "give", "Mage", "staff", "fire", "10", "handle", "handle", "").isEmpty());
assertTrue(tab("weapon", "give", "Mage", "unknown", "fire", "10", "").isEmpty());
}

}
Loading
Loading