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
1 change: 1 addition & 0 deletions src/main/java/net/tfminecraft/cooking/Cooking.java
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ public void onEnable() {
getCommand("cooking").setTabCompleter(commands);
if (husbandryEnabled) {
HusbandryAnimalsCommand animals = new HusbandryAnimalsCommand();
getServer().getPluginManager().registerEvents(animals, this);
getCommand("animals").setExecutor(animals);
getCommand("animals").setTabCompleter(animals);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeSet;
import java.util.UUID;

import org.bukkit.Bukkit;
Expand All @@ -14,12 +17,33 @@
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;

import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.tfminecraft.cooking.Cooking;

public final class HusbandryAnimalsCommand implements CommandExecutor, TabCompleter {
public final class HusbandryAnimalsCommand implements CommandExecutor, TabCompleter, Listener {

private final Map<UUID, String> knownNames = new HashMap<>();

public HusbandryAnimalsCommand() {
// Populate outside completion; getOfflinePlayers may read player data.
for (OfflinePlayer player : Bukkit.getOfflinePlayers()) {
String name = player.getName();
if (name != null) {
knownNames.put(player.getUniqueId(), name);
}
}
}

@EventHandler
public void onJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
knownNames.put(player.getUniqueId(), player.getName());
}

@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
Expand Down Expand Up @@ -85,13 +109,17 @@ public List<String> onTabComplete(CommandSender sender, Command command, String
return Collections.emptyList();
}
String prefix = args[0].toLowerCase(Locale.ROOT);
List<String> names = new ArrayList<>();
TreeSet<String> names = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
for (String name : knownNames.values()) {
if (name.toLowerCase(Locale.ROOT).startsWith(prefix) && Bukkit.getOfflinePlayerIfCached(name) != null) {
names.add(name);
}
}
for (Player player : Bukkit.getOnlinePlayers()) {
if (player.getName().toLowerCase(Locale.ROOT).startsWith(prefix)) {
names.add(player.getName());
}
}
names.sort(String.CASE_INSENSITIVE_ORDER);
return names;
return new ArrayList<>(names);
}
}
38 changes: 36 additions & 2 deletions src/main/java/net/tfminecraft/cooking/manager/CommandManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,35 @@ public List<String> onTabComplete(CommandSender sender, Command command, String
.sorted(String.CASE_INSENSITIVE_ORDER)
.collect(Collectors.toList());
}
if (sender instanceof Player && args.length >= 2) {
if (args[0].equalsIgnoreCase("preview")) {
if (args.length == 2) {
return matching(List.of("mixing_bowl"), args[1]);
}
if (args.length == 3 && args[1].equalsIgnoreCase("mixing_bowl")) {
return matching(List.of("flour", "water", "yeast", "dough", "all", "clear"), args[2]);
}
}
if (args[0].equalsIgnoreCase("qualitytest")) {
if (args.length == 2) {
return matching(List.of("pickup", "compose", "compose2"), args[1]);
}
if (args.length >= 4 && (args[1].equalsIgnoreCase("compose")
|| args[1].equalsIgnoreCase("compose2"))) {
String expression = String.join(" ", Arrays.copyOfRange(args, 2, args.length - 1));
String[] foods = args[1].equalsIgnoreCase("compose2") ? expression.split("\\|", -1)
: new String[] { expression };
for (String food : foods) {
FoodParser.Result parsed = FoodParser.parse(food.trim());
if (parsed == null || parsed.template == null) {
return Collections.emptyList();
}
}
return matching(Arrays.stream(CompositionContext.values()).map(CompositionContext::name).toList(),
args[args.length - 1]);
}
}
}
if (args.length == 2 && args[0].equalsIgnoreCase("husbandry")) {
String prefix = args[1].toLowerCase(Locale.ROOT);
List<String> out = new ArrayList<>();
Expand All @@ -170,15 +199,20 @@ public List<String> onTabComplete(CommandSender sender, Command command, String
}
if (args.length == 4 && args[0].equalsIgnoreCase("husbandry")
&& args[1].equalsIgnoreCase("spawn")) {
return List.of("0", "20", "1000");
return matching(List.of("0", "20", "1000"), args[3]);
}
if (args.length == 5 && args[0].equalsIgnoreCase("husbandry")
&& args[1].equalsIgnoreCase("spawn")) {
return List.of("0", "100", "200");
return matching(List.of("0", "100", "200"), args[4]);
}
return Collections.emptyList();
}

private static List<String> matching(List<String> candidates, String token) {
String prefix = token.toLowerCase(Locale.ROOT);
return candidates.stream().filter(value -> value.toLowerCase(Locale.ROOT).startsWith(prefix)).toList();
}

private void sendUsage(Player player) {
player.sendMessage("§e/cooking reload");
player.sendMessage("§e/cooking food <player> <value>");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,21 +97,21 @@

@Test void damageRulesDistinguishOwnersStaffStrangersMobsProjectilesAndEnvironment() throws Exception {
var listener = new HusbandryDamageListener(); Cow cow = f.spawn(Cow.class);
var unmanaged = new EntityDamageEvent(cow, EntityDamageEvent.DamageCause.FALL, 2); listener.onDamage(unmanaged); assertFalse(unmanaged.isCancelled());

Check warning on line 100 in src/test/java/net/tfminecraft/cooking/husbandry/HusbandryRemainingCoverageTest.java

View workflow job for this annotation

GitHub Actions / build

EntityDamageEvent(org.bukkit.entity.Entity,org.bukkit.event.entity.EntityDamageEvent.DamageCause,double) in org.bukkit.event.entity.EntityDamageEvent has been deprecated and marked for removal
HusbandryAnimal animal = f.record(cow); f.own(animal, f.player);
for (String key : List.of("owner", "other-players", "mobs", "environment")) f.config.set("damage." + key, false); f.reload();
assertTrue(attack(listener, cow, f.player)); assertTrue(attack(listener, cow, f.neighbour)); assertTrue(attack(listener, cow, f.spawn(Cow.class)));
f.neighbour.setOp(true); assertFalse(attack(listener, cow, f.neighbour));
Arrow arrow = mock(Arrow.class); when(arrow.getShooter()).thenReturn(f.player); assertTrue(attack(listener, cow, arrow)); when(arrow.getShooter()).thenReturn(null); assertFalse(attack(listener, cow, arrow));
assertFalse(attack(listener, cow, f.spawn(ItemDisplay.class)));
var fall = new EntityDamageEvent(cow, EntityDamageEvent.DamageCause.FALL, 2); listener.onDamage(fall); assertTrue(fall.isCancelled());

Check warning on line 107 in src/test/java/net/tfminecraft/cooking/husbandry/HusbandryRemainingCoverageTest.java

View workflow job for this annotation

GitHub Actions / build

EntityDamageEvent(org.bukkit.entity.Entity,org.bukkit.event.entity.EntityDamageEvent.DamageCause,double) in org.bukkit.event.entity.EntityDamageEvent has been deprecated and marked for removal
var custom = new EntityDamageEvent(cow, EntityDamageEvent.DamageCause.CUSTOM, 2); listener.onDamage(custom); assertFalse(custom.isCancelled());

Check warning on line 108 in src/test/java/net/tfminecraft/cooking/husbandry/HusbandryRemainingCoverageTest.java

View workflow job for this annotation

GitHub Actions / build

EntityDamageEvent(org.bukkit.entity.Entity,org.bukkit.event.entity.EntityDamageEvent.DamageCause,double) in org.bukkit.event.entity.EntityDamageEvent has been deprecated and marked for removal
for (String key : List.of("owner", "other-players", "mobs", "environment")) f.config.set("damage." + key, true); f.reload();
f.neighbour.setOp(false); assertFalse(attack(listener, cow, f.player)); assertFalse(attack(listener, cow, f.neighbour)); assertFalse(attack(listener, cow, f.spawn(Cow.class)));
when(f.plugin.getHusbandryRepository()).thenReturn(null); assertFalse(attack(listener, cow, f.player));
}
boolean attack(HusbandryDamageListener listener, Entity victim, Entity attacker) {
var event = new EntityDamageByEntityEvent(attacker, victim, EntityDamageEvent.DamageCause.ENTITY_ATTACK, 2); listener.onDamage(event); return event.isCancelled();

Check warning on line 114 in src/test/java/net/tfminecraft/cooking/husbandry/HusbandryRemainingCoverageTest.java

View workflow job for this annotation

GitHub Actions / build

EntityDamageByEntityEvent(org.bukkit.entity.Entity,org.bukkit.entity.Entity,org.bukkit.event.entity.EntityDamageEvent.DamageCause,double) in org.bukkit.event.entity.EntityDamageByEntityEvent has been deprecated and marked for removal
}

@Test void deathCleansRecordsReplacesMatureRoastsAndPreservesVanillaWool() throws Exception {
Expand Down Expand Up @@ -148,6 +148,43 @@
assertTrue(f.repository.exists(offline.uuid()));
}

@Test void animalTargetsUseKnownCachedNamesAndDropExpiredCacheEntriesWithoutRescanning() {
OfflinePlayer offline = mock(OfflinePlayer.class);
when(offline.getUniqueId()).thenReturn(UUID.randomUUID());
when(offline.getName()).thenReturn("OfflineOwner");
OfflinePlayer unknown = mock(OfflinePlayer.class);
OfflinePlayer uncached = mock(OfflinePlayer.class);
when(uncached.getName()).thenReturn("Uncached");
f.player.setOp(true);
try (var bukkit = mockStatic(Bukkit.class, CALLS_REAL_METHODS)) {
bukkit.when(Bukkit::getOfflinePlayers).thenReturn(new OfflinePlayer[]{offline, unknown, uncached, f.player});
bukkit.when(() -> Bukkit.getOfflinePlayerIfCached("OfflineOwner")).thenReturn(offline);
bukkit.when(() -> Bukkit.getOfflinePlayerIfCached("Uncached")).thenReturn(null);
var command = new HusbandryAnimalsCommand();
assertEquals(List.of("Farmer", "Neighbour", "OfflineOwner"), command.onTabComplete(f.player, null, "animals", new String[]{""}));
assertEquals(List.of("OfflineOwner"), command.onTabComplete(f.player, null, "livestock", new String[]{"oFf"}));
assertTrue(command.onTabComplete(f.player, null, "animals", new String[]{"missing"}).isEmpty());
bukkit.when(() -> Bukkit.getOfflinePlayerIfCached("Uncached")).thenReturn(uncached);
assertEquals(List.of("Uncached"), command.onTabComplete(f.player, null, "animals", new String[]{"un"}));
command.onJoin(new org.bukkit.event.player.PlayerJoinEvent(offlinePlayer(offline.getUniqueId(), "RenamedOwner"), net.kyori.adventure.text.Component.empty()));
bukkit.when(() -> Bukkit.getOfflinePlayerIfCached("RenamedOwner")).thenReturn(offline);
assertEquals(List.of("RenamedOwner"), command.onTabComplete(f.player, null, "livestock", new String[]{"re"}));
assertTrue(command.onTabComplete(f.player, null, "animals", new String[]{"off"}).isEmpty());
bukkit.when(() -> Bukkit.getOfflinePlayerIfCached("RenamedOwner")).thenReturn(null);
assertTrue(command.onTabComplete(f.player, null, "livestock", new String[]{"re"}).isEmpty());
bukkit.verify(Bukkit::getOfflinePlayers, times(1));
assertTrue(command.onTabComplete(f.neighbour, null, "animals", new String[]{""}).isEmpty());
assertTrue(command.onTabComplete(f.player, null, "animals", new String[]{"Farmer", ""}).isEmpty());
}
}

private Player offlinePlayer(UUID id, String name) {
Player player = mock(Player.class);
when(player.getUniqueId()).thenReturn(id);
when(player.getName()).thenReturn(name);
return player;
}

@Test void itemUsesHandleCreativeDurabilityBreakingStacksAndStoredLabels() {
assertFalse(HusbandryItems.matches(null, "feed")); assertFalse(HusbandryItems.matches(new ItemStack(Material.WHEAT), " ")); assertFalse(HusbandryItems.matches(new ItemStack(Material.WHEAT), null));
HusbandryItems.useFromMainHand(null); HusbandryItems.consumeOne(null); ItemStack empty = new ItemStack(Material.AIR); HusbandryItems.consumeOne(empty); assertSame(empty, HusbandryItems.applyUse(empty)); assertNull(HusbandryItems.applyUse(null));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,58 @@ class CommandManagerCoverageTest {
assertTrue(tabs("husbandry", "spawn", "cow", "1", "2", "").isEmpty());
}

@Test void previewAndQualityModesCompleteOnlyUsableFiniteArguments() {
assertEquals(List.of("mixing_bowl"), tabs("preview", ""));
assertEquals(List.of("mixing_bowl"), tabs("PREVIEW", "MiX"));
assertTrue(tabs("preview", "unknown").isEmpty());
assertEquals(List.of("flour", "water", "yeast", "dough", "all", "clear"), tabs("preview", "mixing_bowl", ""));
assertEquals(List.of("water"), tabs("preview", "MIXING_BOWL", "Wa"));
assertTrue(tabs("preview", "other", "").isEmpty());
assertTrue(tabs("preview", "mixing_bowl", "all", "").isEmpty());
assertEquals(List.of("pickup", "compose", "compose2"), tabs("qualitytest", ""));
assertEquals(List.of("compose", "compose2"), tabs("QUALITYTEST", "Co"));
assertTrue(tabs("qualitytest", "unknown").isEmpty());
assertTrue(tabs("qualitytest", "compose", "").isEmpty());
assertTrue(tabs("qualitytest", "pickup", "food(type=roast)", "").isEmpty());
assertTrue(commands.onTabComplete(server.getConsoleSender(), command, "cooking", new String[]{"preview", ""}).isEmpty());
player.setOp(false);
assertTrue(tabs("qualitytest", "").isEmpty());
}

@Test void compositionContextsFollowCompleteFoodExpressionsIncludingSpacesAndMultipleFoods() {
org.bukkit.configuration.MemoryConfiguration config = new org.bukkit.configuration.MemoryConfiguration();
config.set("name", "Roast");
config.set("update", false);
FoodItem roast = new FoodItem("roast", config);
staticMock(FoodLoader.class).when(() -> FoodLoader.getByString("roast")).thenReturn(roast);
List<String> contexts = List.of("CUTTING_BOARD", "MIXING_BOWL", "SAUCE_SCOOP", "SOUP_SCOOP", "CARVE",
"CHURN", "FRYING_PAN", "SAUSAGE_MAKER", "MILLING", "BAKING");
assertEquals(contexts, tabs("qualitytest", "compose", "food(type=roast)", ""));
assertEquals(List.of("BAKING"), tabs("qualitytest", "COMPOSE", "food(type=roast)", "bA"));
assertEquals(List.of("SAUCE_SCOOP", "SAUSAGE_MAKER"), tabs("qualitytest", "compose2", "food(type=roast)|food(type=roast)", "sA"));
assertEquals(contexts, tabs("qualitytest", "compose", "food(type=roast;origin=Sunday", "roast)", ""));
assertEquals(contexts, tabs("qualitytest", "compose2", "food(type=roast)", "|", "food(type=roast)", ""));
assertTrue(tabs("qualitytest", "compose", "food(type=roast)").isEmpty());
assertTrue(tabs("qualitytest", "compose", "food(type=roast", "").isEmpty());
assertTrue(tabs("qualitytest", "compose", "food(type=missing)", "").isEmpty());
assertTrue(tabs("qualitytest", "compose2", "food(type=roast)|", "").isEmpty());
assertTrue(tabs("qualitytest", "compose2", "food(type=roast)|incomplete", "").isEmpty());
assertTrue(tabs("qualitytest", "compose", "food(type=roast)", "unknown").isEmpty());
assertTrue(tabs("qualitytest", "compose", "food(type=roast)", "BAKING", "").isEmpty());
var parser = staticMock(FoodParser.class);
parser.when(() -> FoodParser.parse("empty")).thenReturn(new FoodParser.Result());
assertTrue(tabs("qualitytest", "compose", "empty", "").isEmpty());
}

@Test void numericExamplesMatchTheCurrentToken() {
assertEquals(List.of("1000"), tabs("husbandry", "spawn", "COW", "10"));
assertEquals(List.of("20"), tabs("husbandry", "spawn", "COW", "2"));
assertTrue(tabs("husbandry", "spawn", "COW", "9").isEmpty());
assertEquals(List.of("200"), tabs("HUSBANDRY", "SPAWN", "COW", "20", "2"));
assertEquals(List.of("100"), tabs("husbandry", "spawn", "COW", "20", "10"));
assertTrue(tabs("husbandry", "spawn", "COW", "20", "9").isEmpty());
}

@Test void foodCommandValidatesArityTargetCharacterAndFoodRange() {
MockedStatic<RPCharacters> characters = staticMock(RPCharacters.class);
run("food");
Expand Down
Loading