diff --git a/src/main/java/net/tfminecraft/vehicleframework/managers/InventoryManager.java b/src/main/java/net/tfminecraft/vehicleframework/managers/InventoryManager.java index d6678bb..ff0ce3e 100644 --- a/src/main/java/net/tfminecraft/vehicleframework/managers/InventoryManager.java +++ b/src/main/java/net/tfminecraft/vehicleframework/managers/InventoryManager.java @@ -30,6 +30,7 @@ import net.tfminecraft.vehicleframework.vehicles.component.Wings; import net.tfminecraft.vehicleframework.vehicles.handlers.container.Container; import net.tfminecraft.vehicleframework.vehicles.handlers.SkinHandler; +import net.tfminecraft.vehicleframework.vehicles.handlers.VehicleIcon; import net.tfminecraft.vehicleframework.vehicles.handlers.skins.VehicleSkin; import net.tfminecraft.vehicleframework.vehicles.seat.Seat; import net.tfminecraft.vehicleframework.weapons.ActiveWeapon; @@ -41,7 +42,10 @@ public class InventoryManager { // Keep the existing legacy text representation, formatting, and exact-string comparisons. @SuppressWarnings("deprecation") public void seatSelection(Inventory i, Player p, ActiveVehicle v, boolean open) { - int size = Math.max(27, ((v.getSeatHandler().getSeats().size() + 10) / 9) * 9); + VehicleIcon icon = v.getIcon(); + // Slots 25 and 26 hold the ownership and dismount controls; slot 24 shows the vehicle when it has an icon. + int firstControl = icon == null ? 25 : 24; + int size = Math.max(27, ((v.getSeatHandler().getSeats().size() + 27 - firstControl + 8) / 9) * 9); if(open || i.getSize() < size) { i = VehicleFramework.plugin.getServer().createInventory(new VFInventoryHolder(v.getUUID(), VFGUI.SEAT_SELECTION), size, "§7Select Seat"); open = true; @@ -49,10 +53,11 @@ public void seatSelection(Inventory i, Player p, ActiveVehicle v, boolean open) i.clear(); int x = 0; for(Seat seat : v.getSeatHandler().getSeats()) { - if(x == 25) x += 2; // ownership and dismount controls + if(x == firstControl) x = 27; i.setItem(x, getSeatItem(v, seat)); x++; } + if(icon != null) i.setItem(24, createVehicleInfo(v, icon)); // Slot 25 – ownership settings button (owner, or staff on an owned vehicle) if(OwnershipActions.mayOpenMenu(p, v)) { i.setItem(25, ownershipGUI.createOwnershipButton()); @@ -119,6 +124,18 @@ private ItemStack getSeatItem(ActiveVehicle v, Seat s) { } // Keep the existing legacy text representation, formatting, and exact-string comparisons. @SuppressWarnings("deprecation") + private ItemStack createVehicleInfo(ActiveVehicle v, VehicleIcon icon) { + ItemStack i = icon.createItem(); + ItemMeta m = i.getItemMeta(); + m.setDisplayName(v.getName()); + List lore = new ArrayList<>(); + lore.add("§7Type: §e"+Text.capitalize(v.getId().replace("_", " "))); + m.setLore(lore); + i.setItemMeta(m); + return i; + } + // Keep the existing legacy text representation, formatting, and exact-string comparisons. + @SuppressWarnings("deprecation") private ItemStack createDismountButton() { ItemStack i = new ItemStack(Material.BARRIER, 1); ItemMeta m = i.getItemMeta(); @@ -139,7 +156,7 @@ public void repairWindow(Inventory i, Player p, ActiveVehicle v, boolean open, S int x = 0; for(VehicleComponent c : v.getComponents()) { while(locked.contains(x)) x++; - i.setItem(x, getComponentItem(c)); + i.setItem(x, getComponentItem(v, c)); x++; } for(ActiveWeapon weapon : v.getWeaponHandler().getWeapons()) { @@ -216,11 +233,15 @@ private ItemStack getWaterItem(String tool) { return i; } @SuppressWarnings("deprecation") - private ItemStack getComponentItem(VehicleComponent c) { + private ItemStack getComponentItem(ActiveVehicle v, VehicleComponent c) { ItemStack i = new ItemStack(Material.GREEN_CONCRETE, 1); String type = "none"; if(c instanceof Hull) { - i.setType(Material.NETHERITE_BLOCK); + if(v.getIcon() != null) { + i = v.getIcon().createItem(); + } else { + i.setType(Material.NETHERITE_BLOCK); + } type = "hull"; } else if(c instanceof Engine) { i.setType(Material.BLAST_FURNACE); @@ -290,7 +311,7 @@ public void skinSelection(Inventory i, Player p, ActiveVehicle v, boolean open) if (!SkinHandler.isModelAvailable(skin) && !skin.getId().equalsIgnoreCase(currentId)) { continue; } - i.setItem(x, getSkinItem(skin, currentId)); + i.setItem(x, getSkinItem(skin, currentId, v.getIcon())); x++; } //i.setItem(26, createDismountButton()); @@ -312,17 +333,27 @@ public void skinSelection(Inventory i, Player p, ActiveVehicle v, boolean open) // Keep the existing legacy text representation, formatting, and exact-string comparisons. @SuppressWarnings("deprecation") - private ItemStack getSkinItem(VehicleSkin s, String active) { - ItemStack i = new ItemStack(Material.GREEN_CONCRETE, 1); - if(s.getId().equalsIgnoreCase(active)) { - i = new ItemStack(Material.YELLOW_CONCRETE, 1); + private ItemStack getSkinItem(VehicleSkin s, String active, VehicleIcon icon) { + boolean current = s.getId().equalsIgnoreCase(active); + ItemStack i; + if(icon != null) { + i = icon.createItem(); + } else { + i = new ItemStack(current ? Material.YELLOW_CONCRETE : Material.GREEN_CONCRETE, 1); } ItemMeta m = i.getItemMeta(); - if(s.getId().equalsIgnoreCase(active)) { + if(current) { m.setDisplayName("§e"+s.getName()); } else { m.setDisplayName("§a"+s.getName()); } + if(current && icon != null) { + m.addEnchant(Enchantment.UNBREAKING, 1, true); + m.addItemFlags(ItemFlag.HIDE_ENCHANTS); + List lore = new ArrayList<>(); + lore.add("§eCurrent skin"); + m.setLore(lore); + } NamespacedKey key = new NamespacedKey(VehicleFramework.plugin, "vf_skin_id"); m.getPersistentDataContainer().set(key, PersistentDataType.STRING, s.getId()); i.setItemMeta(m); diff --git a/src/main/java/net/tfminecraft/vehicleframework/managers/OwnershipGUIManager.java b/src/main/java/net/tfminecraft/vehicleframework/managers/OwnershipGUIManager.java index 16a6542..c6cd5b5 100644 --- a/src/main/java/net/tfminecraft/vehicleframework/managers/OwnershipGUIManager.java +++ b/src/main/java/net/tfminecraft/vehicleframework/managers/OwnershipGUIManager.java @@ -35,6 +35,8 @@ public class OwnershipGUIManager { public static final int PICKER_TYPED_SLOT = 25; public static final int BACK_SLOT = 26; public static final int OWNER_SLOT = 22; + /** Shows the vehicle when it has an icon; clicking it does nothing. */ + public static final int VEHICLE_SLOT = 18; /** The first group switch; the others follow every second slot in {@link AccessGroup} order. */ public static final int FIRST_GROUP_SLOT = 10; @@ -92,6 +94,9 @@ public void ownershipGui(Inventory i, Player p, ActiveVehicle v, boolean open) { } } } + if (v.getIcon() != null) { + i.setItem(VEHICLE_SLOT, createVehicleInfo(v)); + } if (OwnershipMode.characters()) { i.setItem(OWNER_SLOT, createOwnerInfo(v.getOwnerData().getOwner(), Permissions.isAdmin(p))); } @@ -329,6 +334,16 @@ private ItemStack createCandidate(String name) { return i; } + // Keep the existing legacy text representation, formatting, and exact-string comparisons. + @SuppressWarnings("deprecation") + private ItemStack createVehicleInfo(ActiveVehicle v) { + ItemStack i = v.getIcon().createItem(); + ItemMeta m = i.getItemMeta(); + m.setDisplayName(v.getName()); + i.setItemMeta(m); + return i; + } + private ItemStack createOwnerInfo(String owner, boolean staff) { return createInfo(Material.NAME_TAG, "§7Owner: §f" + OwnerNames.owner(owner, staff), null); } diff --git a/src/main/java/net/tfminecraft/vehicleframework/managers/VehicleManager.java b/src/main/java/net/tfminecraft/vehicleframework/managers/VehicleManager.java index 290e174..ec3117d 100644 --- a/src/main/java/net/tfminecraft/vehicleframework/managers/VehicleManager.java +++ b/src/main/java/net/tfminecraft/vehicleframework/managers/VehicleManager.java @@ -1178,12 +1178,12 @@ public void skinSelect(InventoryClickEvent e) { return; } - if(i.getType().equals(Material.YELLOW_CONCRETE)) { + NamespacedKey key = new NamespacedKey(VehicleFramework.plugin, "vf_skin_id"); + String skinId = i.getItemMeta().getPersistentDataContainer().get(key, PersistentDataType.STRING); + if (skinId != null && skinId.equalsIgnoreCase(v.getSkinHandler().getCurrentSkin().getId())) { p.sendMessage("§cAlready using this skin"); return; } - NamespacedKey key = new NamespacedKey(VehicleFramework.plugin, "vf_skin_id"); - String skinId = i.getItemMeta().getPersistentDataContainer().get(key, PersistentDataType.STRING); if (skinId == null || !SkinHandler.isModelAvailable(v.getSkinHandler().getSkins().get(skinId))) { p.sendMessage("§cThat skin is not available."); return; diff --git a/src/main/java/net/tfminecraft/vehicleframework/vehicles/ActiveVehicle.java b/src/main/java/net/tfminecraft/vehicleframework/vehicles/ActiveVehicle.java index f826aed..dfee7aa 100644 --- a/src/main/java/net/tfminecraft/vehicleframework/vehicles/ActiveVehicle.java +++ b/src/main/java/net/tfminecraft/vehicleframework/vehicles/ActiveVehicle.java @@ -67,6 +67,7 @@ import net.tfminecraft.vehicleframework.vehicles.handlers.TrainHandler; import net.tfminecraft.vehicleframework.vehicles.handlers.train.ConsistRelinker; import net.tfminecraft.vehicleframework.vehicles.handlers.UtilityHandler; +import net.tfminecraft.vehicleframework.vehicles.handlers.VehicleIcon; import net.tfminecraft.vehicleframework.vehicles.handlers.WeaponHandler; import net.tfminecraft.vehicleframework.vehicles.seat.Seat; import net.tfminecraft.vehicleframework.vehicles.state.VehicleState; @@ -83,6 +84,7 @@ public class ActiveVehicle { protected String id; protected String uuid; protected String name; + protected VehicleIcon icon; protected boolean fixed; @@ -154,6 +156,7 @@ public ActiveVehicle(Vehicle stored, Entity e, ActiveModel m, VehicleManager man towable = stored.isTowable(); id = stored.getId(); name = stored.getName(); + icon = stored.getIcon(); uuid = UUID.randomUUID().toString(); destroyed = false; initialized = false; @@ -321,6 +324,11 @@ public ContainerHandler getContainerHandler() { public List getEntitySeatWhitelist() { return entitySeatWhitelist; } + + /** The vehicle's menu and ticket icon, or null when its config has none. */ + public VehicleIcon getIcon() { + return icon; + } //Model public boolean changeSkin(String id) { diff --git a/src/main/java/net/tfminecraft/vehicleframework/vehicles/Vehicle.java b/src/main/java/net/tfminecraft/vehicleframework/vehicles/Vehicle.java index b8ebc4c..13a4609 100644 --- a/src/main/java/net/tfminecraft/vehicleframework/vehicles/Vehicle.java +++ b/src/main/java/net/tfminecraft/vehicleframework/vehicles/Vehicle.java @@ -18,6 +18,7 @@ import net.tfminecraft.vehicleframework.vehicles.handlers.StateHandler; import net.tfminecraft.vehicleframework.vehicles.handlers.TowHandler; import net.tfminecraft.vehicleframework.vehicles.handlers.UtilityHandler; +import net.tfminecraft.vehicleframework.vehicles.handlers.VehicleIcon; import net.tfminecraft.vehicleframework.vehicles.handlers.container.ContainerHandler; import net.tfminecraft.vehicleframework.loaders.DeathTemplateLoader; import net.tfminecraft.vehicleframework.loaders.WeaponTemplateLoader; @@ -32,6 +33,9 @@ public class Vehicle { protected String name; protected String model; + //Menu and ticket icon, null when the config has none + protected VehicleIcon icon; + protected boolean fixed; protected boolean towable; @@ -85,6 +89,7 @@ public Vehicle(String key, ConfigurationSection config) { towable = config.getBoolean("towable", false); if(!config.contains("model")) VFLogger.log(key+" does not contain a model field"); model = config.getString("model"); + if(config.contains("icon")) icon = VehicleIcon.load(key, config.getConfigurationSection("icon")); skinHandler = new SkinHandler(model, config.getConfigurationSection("skins")); stateHandler = new StateHandler(config.getConfigurationSection("states")); @@ -170,6 +175,10 @@ public String getModel() { return model; } + public VehicleIcon getIcon() { + return icon; + } + public boolean isFixed() { return fixed; } diff --git a/src/main/java/net/tfminecraft/vehicleframework/vehicles/handlers/VehicleIcon.java b/src/main/java/net/tfminecraft/vehicleframework/vehicles/handlers/VehicleIcon.java new file mode 100644 index 0000000..9d80f3b --- /dev/null +++ b/src/main/java/net/tfminecraft/vehicleframework/vehicles/handlers/VehicleIcon.java @@ -0,0 +1,58 @@ +package net.tfminecraft.vehicleframework.vehicles.handlers; + +import org.bukkit.Material; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import net.tfminecraft.vehicleframework.VFLogger; +import net.tfminecraft.vehicleframework.util.LegacyModelData; + +/** The item a vehicle shows in its menus and on its tickets, from the vehicle's {@code icon} section. */ +public final class VehicleIcon { + private final Material material; + private final Integer modelData; + + public VehicleIcon(Material material, Integer modelData) { + this.material = material; + this.modelData = modelData; + } + + /** The icon in a vehicle's {@code icon} section, or null when the section has no known material. */ + public static VehicleIcon load(String vehicle, ConfigurationSection config) { + String name = config == null ? null : config.getString("material"); + Material material = name == null ? null : Material.matchMaterial(name.trim()); + if (material == null) { + VFLogger.log(vehicle + " has an icon without a known material: " + name); + return null; + } + Integer modelData = config.contains("model_data") ? config.getInt("model_data") : null; + return new VehicleIcon(material, modelData); + } + + public Material getMaterial() { + return material; + } + + /** The custom model data, or null when the icon is the plain material. */ + public Integer getModelData() { + return modelData; + } + + /** A new single item that looks like this icon. */ + public ItemStack createItem() { + ItemStack item = new ItemStack(material, 1); + if (modelData != null) { + ItemMeta meta = item.getItemMeta(); + LegacyModelData.set(meta, modelData); + item.setItemMeta(meta); + } + return item; + } + + /** Makes an item of any type render as this icon. */ + public void applyModel(ItemMeta meta) { + meta.setItemModel(material.getKey()); + LegacyModelData.set(meta, modelData); + } +} diff --git a/src/main/java/net/tfminecraft/vehicleframework/vehicles/handlers/VehicleTicketInteract.java b/src/main/java/net/tfminecraft/vehicleframework/vehicles/handlers/VehicleTicketInteract.java index 883bcb1..3688c3f 100644 --- a/src/main/java/net/tfminecraft/vehicleframework/vehicles/handlers/VehicleTicketInteract.java +++ b/src/main/java/net/tfminecraft/vehicleframework/vehicles/handlers/VehicleTicketInteract.java @@ -53,14 +53,15 @@ private static boolean canMint(Player player, ActiveVehicle vehicle) { private static void mintOne(Player player, ItemStack hand, ActiveVehicle source) { String ticketId = source.getOwnerData().getTicketId(); String name = source.getName(); + VehicleIcon icon = source.getIcon(); if (hand.getAmount() <= 1) { - VehicleTicketItems.write(hand, ticketId, name); + VehicleTicketItems.write(hand, ticketId, name, icon); return; } hand.setAmount(hand.getAmount() - 1); ItemStack ticket = hand.clone(); ticket.setAmount(1); - VehicleTicketItems.write(ticket, ticketId, name); + VehicleTicketItems.write(ticket, ticketId, name, icon); PlayerInventory inv = player.getInventory(); java.util.HashMap leftover = inv.addItem(ticket); if (!leftover.isEmpty() && player.getWorld() != null) { diff --git a/src/main/java/net/tfminecraft/vehicleframework/vehicles/handlers/VehicleTicketItems.java b/src/main/java/net/tfminecraft/vehicleframework/vehicles/handlers/VehicleTicketItems.java index aa7c951..3996e7f 100644 --- a/src/main/java/net/tfminecraft/vehicleframework/vehicles/handlers/VehicleTicketItems.java +++ b/src/main/java/net/tfminecraft/vehicleframework/vehicles/handlers/VehicleTicketItems.java @@ -35,7 +35,7 @@ public static String readId(ItemStack item) { // Keep the existing legacy text representation, formatting, and exact-string comparisons. @SuppressWarnings("deprecation") - public static void write(ItemStack item, String ticketId, String vehicleName) { + public static void write(ItemStack item, String ticketId, String vehicleName, VehicleIcon icon) { if (item == null || ticketId == null || ticketId.isBlank()) { return; } @@ -49,6 +49,10 @@ public static void write(ItemStack item, String ticketId, String vehicleName) { List lore = new ArrayList<>(); lore.add("§7Valid for this vehicle"); meta.setLore(lore); + // The ticket looks like the vehicle it admits to. + if (icon != null) { + icon.applyModel(meta); + } item.setItemMeta(meta); } diff --git a/src/main/resources/vehicles/aa_turret.yml b/src/main/resources/vehicles/aa_turret.yml index 25c71f4..376b470 100644 --- a/src/main/resources/vehicles/aa_turret.yml +++ b/src/main/resources/vehicles/aa_turret.yml @@ -1,6 +1,9 @@ aa_turret: name: "§l§eAnti-Air Turret" model: aa_turret + icon: + material: coal + model_data: 7400 fixed: true skins: aa_turret: diff --git a/src/main/resources/vehicles/anti_air.yml b/src/main/resources/vehicles/anti_air.yml index a3b2162..7e2b25a 100644 --- a/src/main/resources/vehicles/anti_air.yml +++ b/src/main/resources/vehicles/anti_air.yml @@ -1,6 +1,9 @@ anti_air: name: "§l§eAnti-Air" model: anti_air + icon: + material: coal + model_data: 7401 fixed: true skins: anti_air: diff --git a/src/main/resources/vehicles/behemoth.yml b/src/main/resources/vehicles/behemoth.yml index 7802a36..3ed7aff 100644 --- a/src/main/resources/vehicles/behemoth.yml +++ b/src/main/resources/vehicles/behemoth.yml @@ -1,6 +1,9 @@ behemoth: name: "§l§eBehemoth" model: behemoth + icon: + material: coal + model_data: 7402 skins: behemoth: name: "Behemoth" diff --git a/src/main/resources/vehicles/biplane.yml b/src/main/resources/vehicles/biplane.yml index ac4e19e..aac11d0 100644 --- a/src/main/resources/vehicles/biplane.yml +++ b/src/main/resources/vehicles/biplane.yml @@ -1,6 +1,9 @@ biplane: name: "§l§eBiplane" model: biplane + icon: + material: coal + model_data: 7403 skins: biplane: name: "Biplane" diff --git a/src/main/resources/vehicles/bomber.yml b/src/main/resources/vehicles/bomber.yml index 48b654d..19c786b 100644 --- a/src/main/resources/vehicles/bomber.yml +++ b/src/main/resources/vehicles/bomber.yml @@ -1,6 +1,9 @@ bomber: name: "§l§eBomber" model: bomber + icon: + material: coal + model_data: 7404 skins: bomber: name: "Bomber" diff --git a/src/main/resources/vehicles/cloudskimmer.yml b/src/main/resources/vehicles/cloudskimmer.yml index 31bf2e2..7c4abcf 100644 --- a/src/main/resources/vehicles/cloudskimmer.yml +++ b/src/main/resources/vehicles/cloudskimmer.yml @@ -1,6 +1,9 @@ cloudskimmer: name: "§l§eCloudskimmer" model: cloudskimmer + icon: + material: coal + model_data: 7405 skins: cloudskimmer: name: "Cloudskimmer" diff --git a/src/main/resources/vehicles/coal_car.yml b/src/main/resources/vehicles/coal_car.yml index a2ccdd3..93f7161 100644 --- a/src/main/resources/vehicles/coal_car.yml +++ b/src/main/resources/vehicles/coal_car.yml @@ -1,6 +1,9 @@ coal_car: name: "§l§eCoal Car" model: coal_car + icon: + material: coal + model_data: 7406 fixed: true skins: coal_car: diff --git a/src/main/resources/vehicles/cruiser.yml b/src/main/resources/vehicles/cruiser.yml index e9ff8b9..661f015 100644 --- a/src/main/resources/vehicles/cruiser.yml +++ b/src/main/resources/vehicles/cruiser.yml @@ -1,6 +1,9 @@ cruiser: name: "§l§eCruiser" model: cruiser + icon: + material: coal + model_data: 7407 skins: cruiser: name: "Cruiser" diff --git a/src/main/resources/vehicles/field_artillery.yml b/src/main/resources/vehicles/field_artillery.yml index 299a17a..3791215 100644 --- a/src/main/resources/vehicles/field_artillery.yml +++ b/src/main/resources/vehicles/field_artillery.yml @@ -1,6 +1,9 @@ field_artillery: name: "§l§eField Artillery" model: field_artillery + icon: + material: coal + model_data: 7408 fixed: true towable: true skins: diff --git a/src/main/resources/vehicles/fixed_artillery.yml b/src/main/resources/vehicles/fixed_artillery.yml index 7105925..d6be716 100644 --- a/src/main/resources/vehicles/fixed_artillery.yml +++ b/src/main/resources/vehicles/fixed_artillery.yml @@ -1,6 +1,9 @@ fixed_artillery: name: "§l§eFixed Artillery" model: fixed_artillery + icon: + material: coal + model_data: 7409 fixed: true skins: fixed_artillery: diff --git a/src/main/resources/vehicles/flat_car.yml b/src/main/resources/vehicles/flat_car.yml index a1f7e3c..0eaef19 100644 --- a/src/main/resources/vehicles/flat_car.yml +++ b/src/main/resources/vehicles/flat_car.yml @@ -1,6 +1,9 @@ flat_car: name: "§l§eFlat Car" model: flat_car + icon: + material: coal + model_data: 7410 fixed: true skins: flat_car: diff --git a/src/main/resources/vehicles/gunboat.yml b/src/main/resources/vehicles/gunboat.yml index 64fad59..6a2eefb 100644 --- a/src/main/resources/vehicles/gunboat.yml +++ b/src/main/resources/vehicles/gunboat.yml @@ -1,6 +1,9 @@ gunboat: name: "§l§eGunboat" model: gunboat + icon: + material: coal + model_data: 7411 skins: gunboat: name: "Guboat" diff --git a/src/main/resources/vehicles/gyrobomber.yml b/src/main/resources/vehicles/gyrobomber.yml index 8334ea0..3745559 100644 --- a/src/main/resources/vehicles/gyrobomber.yml +++ b/src/main/resources/vehicles/gyrobomber.yml @@ -1,6 +1,9 @@ gyrobomber: name: "§l§eGyrobomber" model: gyrobomber + icon: + material: coal + model_data: 7412 skins: gyrobomber: name: "Gyrobomber" diff --git a/src/main/resources/vehicles/horse_cart.yml b/src/main/resources/vehicles/horse_cart.yml index c11001d..5c96990 100644 --- a/src/main/resources/vehicles/horse_cart.yml +++ b/src/main/resources/vehicles/horse_cart.yml @@ -1,6 +1,9 @@ horse_cart: name: "§l§eHorse Cart" model: horse_cart + icon: + material: coal + model_data: 7413 skins: horse_cart: name: "Horse Cart" diff --git a/src/main/resources/vehicles/ironclad.yml b/src/main/resources/vehicles/ironclad.yml index 5d86fd2..bbf740c 100644 --- a/src/main/resources/vehicles/ironclad.yml +++ b/src/main/resources/vehicles/ironclad.yml @@ -1,6 +1,9 @@ ironclad: name: "§l§eIronclad" model: ironclad + icon: + material: coal + model_data: 7414 skins: ironclad: name: "Ironclad" diff --git a/src/main/resources/vehicles/monoplane.yml b/src/main/resources/vehicles/monoplane.yml index 900c078..ecd4af4 100644 --- a/src/main/resources/vehicles/monoplane.yml +++ b/src/main/resources/vehicles/monoplane.yml @@ -1,6 +1,9 @@ monoplane: name: "§l§eMonoplane" model: monoplane + icon: + material: coal + model_data: 7415 skins: monoplane: name: "Monoplane" diff --git a/src/main/resources/vehicles/passenger_car.yml b/src/main/resources/vehicles/passenger_car.yml index 4a63c3a..d50b39e 100644 --- a/src/main/resources/vehicles/passenger_car.yml +++ b/src/main/resources/vehicles/passenger_car.yml @@ -1,6 +1,9 @@ passenger_car: name: "§l§ePassenger Car" model: passenger_car + icon: + material: coal + model_data: 7416 fixed: true skins: passenger_car: diff --git a/src/main/resources/vehicles/simple_locomotive.yml b/src/main/resources/vehicles/simple_locomotive.yml index e5a64f8..5676939 100644 --- a/src/main/resources/vehicles/simple_locomotive.yml +++ b/src/main/resources/vehicles/simple_locomotive.yml @@ -1,6 +1,9 @@ simple_locomotive: name: "§l§eSimple Locomotive" model: simple_locomotive + icon: + material: coal + model_data: 7417 skins: simple_locomotive: name: "Simple Locomotive" diff --git a/src/main/resources/vehicles/sloop.yml b/src/main/resources/vehicles/sloop.yml index f545242..633999c 100644 --- a/src/main/resources/vehicles/sloop.yml +++ b/src/main/resources/vehicles/sloop.yml @@ -1,6 +1,9 @@ sloop: name: "§l§eSloop" model: sloop + icon: + material: coal + model_data: 7418 skins: sloop: name: "Sloop" diff --git a/src/main/resources/vehicles/small_car.yml b/src/main/resources/vehicles/small_car.yml index a539297..2bdfcf3 100644 --- a/src/main/resources/vehicles/small_car.yml +++ b/src/main/resources/vehicles/small_car.yml @@ -1,6 +1,9 @@ small_car: name: "§l§eSmall Car" model: small_car + icon: + material: coal + model_data: 7419 skins: small_car: name: "Small Car" diff --git a/src/main/resources/vehicles/torpedoboat.yml b/src/main/resources/vehicles/torpedoboat.yml index 016d88c..be706bd 100644 --- a/src/main/resources/vehicles/torpedoboat.yml +++ b/src/main/resources/vehicles/torpedoboat.yml @@ -1,6 +1,9 @@ torpedoboat: name: "§l§eTorpedoboat" model: torpedoboat + icon: + material: coal + model_data: 7420 skins: torpedoboat: name: "Torpedoboat" diff --git a/src/main/resources/vehicles/wooden_cart.yml b/src/main/resources/vehicles/wooden_cart.yml index 384c6ab..fca03d2 100644 --- a/src/main/resources/vehicles/wooden_cart.yml +++ b/src/main/resources/vehicles/wooden_cart.yml @@ -1,6 +1,9 @@ wooden_cart: name: "§l§eWooden Cart" model: wooden_cart + icon: + material: coal + model_data: 7421 skins: wooden_cart: name: "Wooden Cart" diff --git a/src/test/java/net/tfminecraft/vehicleframework/managers/CommandAndOwnershipCoverageTest.java b/src/test/java/net/tfminecraft/vehicleframework/managers/CommandAndOwnershipCoverageTest.java index 3b4408a..79aabe2 100644 --- a/src/test/java/net/tfminecraft/vehicleframework/managers/CommandAndOwnershipCoverageTest.java +++ b/src/test/java/net/tfminecraft/vehicleframework/managers/CommandAndOwnershipCoverageTest.java @@ -28,6 +28,7 @@ import net.tfminecraft.vehicleframework.tracks.*; import net.tfminecraft.vehicleframework.util.TabCompletion; import net.tfminecraft.vehicleframework.vehicles.*; +import net.tfminecraft.vehicleframework.vehicles.handlers.VehicleIcon; import net.tfminecraft.vehicleframework.vehicles.state.VehicleState; import net.tfminecraft.vehicleframework.weapons.ammunition.Ammunition; import net.tfminecraft.tlibs.character.*; @@ -102,6 +103,16 @@ class CommandAndOwnershipCoverageTest { assertEquals(Material.GRAY_STAINED_GLASS_PANE, menu.getItem(4).getType()); } + @Test void ownershipMenuShowsTheVehicleIconInItsOwnSlot() { + Inventory menu = inventory(27, null); ownership.ownershipGui(menu, player, vehicle, false); + assertEquals(Material.GRAY_STAINED_GLASS_PANE, menu.getItem(OwnershipGUIManager.VEHICLE_SLOT).getType()); + when(vehicle.getIcon()).thenReturn(new VehicleIcon(Material.COAL, null)); when(vehicle.getName()).thenReturn("§l§eBiplane"); + ownership.ownershipGui(menu, player, vehicle, false); + assertEquals(Material.COAL, menu.getItem(OwnershipGUIManager.VEHICLE_SLOT).getType()); + assertEquals("§l§eBiplane", menu.getItem(OwnershipGUIManager.VEHICLE_SLOT).getItemMeta().getDisplayName()); + assertTrue(OwnershipGUIManager.groupAt(OwnershipGUIManager.VEHICLE_SLOT).isEmpty()); + } + @Test void commandsRespectNamePermissionAndUnknownArguments() { when(command.getName()).thenReturn("other"); assertFalse(run(player, "reload")); when(command.getName()).thenReturn("VF"); assertTrue(run(player, "spawn", "cart")); diff --git a/src/test/java/net/tfminecraft/vehicleframework/managers/InventoryManagerCoverageTest.java b/src/test/java/net/tfminecraft/vehicleframework/managers/InventoryManagerCoverageTest.java index d681848..132033c 100644 --- a/src/test/java/net/tfminecraft/vehicleframework/managers/InventoryManagerCoverageTest.java +++ b/src/test/java/net/tfminecraft/vehicleframework/managers/InventoryManagerCoverageTest.java @@ -13,6 +13,7 @@ import org.bukkit.persistence.*; import org.junit.jupiter.api.*; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.*; import net.tfminecraft.vehicleframework.VehicleFramework; @@ -204,9 +205,69 @@ void repairMenuDistinguishesComponentsWeaponsAndSelectedTool(String tool) { assertEquals("§aAvailable", inventory.getItem(1).getItemMeta().getDisplayName()); assertEquals("new", tag(inventory.getItem(1), "vf_skin_id")); assertEquals(Material.GRAY_STAINED_GLASS_PANE, inventory.getItem(2).getType()); + assertEquals(List.of(), inventory.getItem(0).getItemMeta().getLore()); + verify(inventory.getItem(0).getItemMeta(), never()).addEnchant(any(), anyInt(), anyBoolean()); manager.skinSelection(inventory, player, vehicle, false); verify(player, times(1)).openInventory(any(Inventory.class)); } + @Test void iconSkinMenuShowsTheVehicleAndMarksTheCurrentSkinWithAGlint() { + when(vehicle.getIcon()).thenReturn(new VehicleIcon(Material.COAL, null)); + SkinHandler skins = mock(SkinHandler.class); when(vehicle.getSkinHandler()).thenReturn(skins); + VehicleSkin active = skin("current", "Current"), available = skin("new", "Available"); + when(skins.getCurrentSkin()).thenReturn(active); + HashMap choices = new LinkedHashMap<>(); choices.put("current", active); choices.put("new", available); + when(skins.getSkins()).thenReturn(choices); + keep(mockStatic(SkinHandler.class)).when(() -> SkinHandler.isModelAvailable(any(VehicleSkin.class))).thenReturn(true); + manager.skinSelection(null, player, vehicle, true); Inventory inventory = created.get(0); + ItemStack current = inventory.getItem(0), other = inventory.getItem(1); + assertEquals(Material.COAL, current.getType()); assertEquals(Material.COAL, other.getType()); + assertEquals("§eCurrent", current.getItemMeta().getDisplayName()); assertEquals("§aAvailable", other.getItemMeta().getDisplayName()); + assertEquals(List.of("§eCurrent skin"), current.getItemMeta().getLore()); assertEquals(List.of(), other.getItemMeta().getLore()); + verify(current.getItemMeta()).addEnchant(Enchantment.UNBREAKING, 1, true); + verify(current.getItemMeta()).addItemFlags(ItemFlag.HIDE_ENCHANTS); + verify(other.getItemMeta(), never()).addEnchant(any(), anyInt(), anyBoolean()); + assertEquals("current", tag(current, "vf_skin_id")); assertEquals("new", tag(other, "vf_skin_id")); + } + + @Test void iconSeatMenuShowsTheVehicleInSlot24AndSeatsSkipIt() { + when(vehicle.getIcon()).thenReturn(new VehicleIcon(Material.COAL, null)); + when(vehicle.getName()).thenReturn("§l§eLocomotive"); when(vehicle.getId()).thenReturn("simple_locomotive"); + when(seats.getSeats()).thenReturn(List.of(new Seat(SeatType.CAPTAIN, "driver"))); when(vehicle.isPassenger(player, false)).thenReturn(true); + manager.seatSelection(null, player, vehicle, true); Inventory inventory = created.get(0); + assertEquals(27, inventory.getSize()); + ItemStack info = inventory.getItem(24); + assertEquals(Material.COAL, info.getType()); assertEquals("§l§eLocomotive", info.getItemMeta().getDisplayName()); + assertEquals(List.of("§7Type: §eSimple Locomotive"), info.getItemMeta().getLore()); assertNull(tag(info, "vf_seat_id")); + assertEquals("driver", tag(inventory.getItem(0), "vf_seat_id")); + assertEquals(Material.TRIPWIRE_HOOK, inventory.getItem(25).getType()); assertEquals(Material.BARRIER, inventory.getItem(26).getType()); + } + + @ParameterizedTest @CsvSource({"24,27", "25,36", "33,36", "34,45"}) + void iconSeatMenuGrowsSoEverySeatStaysSelectable(int count, int size) { + when(vehicle.getIcon()).thenReturn(new VehicleIcon(Material.COAL, null)); when(vehicle.getId()).thenReturn("cart"); + List many = new ArrayList<>(); for (int i = 0; i < count; i++) many.add(new Seat(SeatType.PASSENGER, "seat_" + i)); + when(seats.getSeats()).thenReturn(many); when(vehicle.isPassenger(player, false)).thenReturn(true); + manager.seatSelection(null, player, vehicle, true); Inventory inventory = created.get(0); + assertEquals(size, inventory.getSize()); + long represented = Arrays.stream(inventory.getContents()).filter(Objects::nonNull) + .filter(item -> tag(item, "vf_seat_id") != null).count(); + assertEquals(count, represented, "Every configured seat must remain selectable"); + assertEquals(Material.COAL, inventory.getItem(24).getType()); + assertEquals(Material.TRIPWIRE_HOOK, inventory.getItem(25).getType()); assertEquals(Material.BARRIER, inventory.getItem(26).getType()); + } + + @Test void iconRepairMenuShowsTheHullAsTheVehicle() { + when(vehicle.getIcon()).thenReturn(new VehicleIcon(Material.COAL, null)); + List components = new ArrayList<>(); + components.add(component(Hull.class, Component.HULL)); components.add(component(Engine.class, Component.ENGINE)); + when(vehicle.getComponents()).thenReturn(components); Inventory inventory = inventory(27, null); + manager.repairWindow(inventory, player, vehicle, false, "repair"); + ItemStack hull = inventory.getItem(0); + assertEquals(Material.COAL, hull.getType()); assertEquals("hull", tag(hull, "vf_component_type")); + assertEquals("§7Hull", hull.getItemMeta().getDisplayName()); assertFalse(hull.getItemMeta().getLore().isEmpty()); + assertEquals(Material.BLAST_FURNACE, inventory.getItem(1).getType()); + } + private VehicleComponent component(Class type, Component kind) { VehicleComponent component = mock(type); HealthData health = new HealthData(100, 20, 60); when(component.getType()).thenReturn(kind); when(component.getHealthData()).thenReturn(health); return component; diff --git a/src/test/java/net/tfminecraft/vehicleframework/managers/OwnershipManagerTest.java b/src/test/java/net/tfminecraft/vehicleframework/managers/OwnershipManagerTest.java index 84b0149..2dab694 100644 --- a/src/test/java/net/tfminecraft/vehicleframework/managers/OwnershipManagerTest.java +++ b/src/test/java/net/tfminecraft/vehicleframework/managers/OwnershipManagerTest.java @@ -329,6 +329,19 @@ void clearingOwnershipEmptiesTheCrewInCharactersModeOnly() { assertEquals(List.of("player_Bob"), named.getOwnerData().getWhiteList()); assertFalse(named.getOwnerData().isWhiteListed()); } + @Test + void clickingTheVehicleIconInTheOwnershipMenuDoesNothing() { + groupService(); + ActiveVehicle v = vehicle(owner(kira)); + InventoryClickEvent click = menu(alex, v, OwnershipGUIManager.VEHICLE_SLOT); + manager.ownershipClick(click); + verify(click).setCancelled(true); + verify(alex, never()).sendMessage(anyString()); + verify(alex, never()).closeInventory(); + verify(gui, never()).ownershipGui(any(), any(), any(), anyBoolean()); + assertTrue(events.isEmpty()); + } + private VehicleAccessGroups groupService() { VehicleAccessGroups groups = mock(VehicleAccessGroups.class); when(groups.ready()).thenReturn(true); diff --git a/src/test/java/net/tfminecraft/vehicleframework/managers/VehicleManagerCoverageTest.java b/src/test/java/net/tfminecraft/vehicleframework/managers/VehicleManagerCoverageTest.java index 0e67eb3..3fdf221 100644 --- a/src/test/java/net/tfminecraft/vehicleframework/managers/VehicleManagerCoverageTest.java +++ b/src/test/java/net/tfminecraft/vehicleframework/managers/VehicleManagerCoverageTest.java @@ -615,14 +615,31 @@ void seatSelectionMountsEjectsAndEstablishesEntitySeatSelection() throws Excepti when(event.getSlot()).thenReturn(26); manager.seatSelect(event); verify(v).dismountPassenger(player, false); } + @Test + void vehicleInfoInTheSeatMenuIsNotASeat() { + ActiveVehicle v = vehicle("v"); Player player = player("Ryan"); register(v); + v.getOwnerData().setOwner("player_Ryan"); manager.seatInteract(player, v); clearInvocations(player); + InventoryClickEvent event = click(player, v, VFGUI.SEAT_SELECTION, 24, item(Material.COAL, null)); + manager.seatSelect(event); + verify(event).setCancelled(true); verify(player, never()).sendMessage(anyString()); verify(player, never()).closeInventory(); + verify(v, never()).addPassenger(any(), any()); verify(v, never()).dismountPassenger(any(), anyBoolean()); + verify(ownership.constructed().getFirst(), never()).ownershipGui(any(), any(), any(), anyBoolean()); + } + @Test void skinSelectionValidatesModelAndRefreshesOnSuccessfulChange() { ActiveVehicle v = vehicle("v"); Player player = player("Ryan"); manager.skinInteract(player, v); - InventoryClickEvent event = click(player, v, VFGUI.SKIN_SELECTION, 0, item(Material.YELLOW_CONCRETE, "blue")); + when(v.getSkinHandler().getCurrentSkin().getId()).thenReturn("red"); + InventoryClickEvent event = click(player, v, VFGUI.SKIN_SELECTION, 0, item(Material.YELLOW_CONCRETE, "red")); manager.skinSelect(event); verify(player).sendMessage("§cAlready using this skin"); + // Icon menus show every skin as the same item, so the current skin is found by its id. + doReturn(item(Material.COAL, "RED")).when(event).getCurrentItem(); + manager.skinSelect(event); verify(player, times(2)).sendMessage("§cAlready using this skin"); + doReturn(item(Material.COAL, null)).when(event).getCurrentItem(); + manager.skinSelect(event); verify(player).sendMessage("§cThat skin is not available."); doReturn(item(Material.GREEN_CONCRETE, "blue")).when(event).getCurrentItem(); try (MockedStatic skins = mockStatic(SkinHandler.class)) { - manager.skinSelect(event); verify(player).sendMessage("§cThat skin is not available."); + manager.skinSelect(event); verify(player, times(2)).sendMessage("§cThat skin is not available."); skins.when(() -> SkinHandler.isModelAvailable(any(net.tfminecraft.vehicleframework.vehicles.handlers.skins.VehicleSkin.class))).thenReturn(true); manager.skinSelect(event); verify(player).sendMessage("§cCould not change skin."); when(v.changeSkin("blue")).thenReturn(true); manager.skinSelect(event); diff --git a/src/test/java/net/tfminecraft/vehicleframework/vehicles/ActiveVehicleTest.java b/src/test/java/net/tfminecraft/vehicleframework/vehicles/ActiveVehicleTest.java index 5b2c6b0..99b8a37 100644 --- a/src/test/java/net/tfminecraft/vehicleframework/vehicles/ActiveVehicleTest.java +++ b/src/test/java/net/tfminecraft/vehicleframework/vehicles/ActiveVehicleTest.java @@ -123,7 +123,10 @@ void constructorBindsIdentityHandlersAndInitialState() { when(stored.isFixed()).thenReturn(true); when(stored.isTowable()).thenReturn(true); when(stored.getEntitySeatWhitelist()).thenReturn(List.of("v.cow")); + VehicleIcon icon = new VehicleIcon(Material.COAL, 7403); + when(stored.getIcon()).thenReturn(icon); ActiveVehicle v = create(); + assertSame(icon, v.getIcon()); assertTrue(v.isInitialized()); assertFalse(v.isDestroyed()); assertTrue(v.isFixed()); diff --git a/src/test/java/net/tfminecraft/vehicleframework/vehicles/handlers/VehicleHandlersCoverageTest.java b/src/test/java/net/tfminecraft/vehicleframework/vehicles/handlers/VehicleHandlersCoverageTest.java index 45ba694..6adcdc8 100644 --- a/src/test/java/net/tfminecraft/vehicleframework/vehicles/handlers/VehicleHandlersCoverageTest.java +++ b/src/test/java/net/tfminecraft/vehicleframework/vehicles/handlers/VehicleHandlersCoverageTest.java @@ -61,6 +61,7 @@ class VehicleHandlersCoverageTest { private final BukkitScheduler scheduler = mock(BukkitScheduler.class); private final World world = mock(World.class); private MockedStatic modelApi; + private MockedStatic logger; private VehicleFramework previousPlugin; private String previousTicket; @@ -68,7 +69,7 @@ class VehicleHandlersCoverageTest { previousPlugin = VehicleFramework.plugin; previousTicket = Cache.ticketItem; VehicleFramework.plugin = mock(VehicleFramework.class); when(VehicleFramework.plugin.getName()).thenReturn("VehicleFramework"); when(VehicleFramework.plugin.namespace()).thenReturn("vehicleframework"); - keep(mockStatic(VFLogger.class)); keep(mockStatic(PersistenceLog.class)); + logger = keep(mockStatic(VFLogger.class)); keep(mockStatic(PersistenceLog.class)); var bukkit = keep(mockStatic(Bukkit.class)); bukkit.when(Bukkit::getScheduler).thenReturn(scheduler); when(scheduler.runTaskLater(any(), any(Runnable.class), anyLong())).thenAnswer(c -> task(c.getArgument(1), later)); when(scheduler.runTaskTimer(any(), any(Runnable.class), anyLong(), anyLong())).thenAnswer(c -> task(c.getArgument(1), timers)); @@ -119,6 +120,49 @@ class VehicleHandlersCoverageTest { assertTrue(template.getDeathData().stream().anyMatch(data -> data.getType() == VehicleDeath.DIE && data.getDuration() == 9)); } + @Test void optionalIconSectionGivesTheVehicleAnIconAndUnknownMaterialsAreReported() { + YamlConfiguration config = vehicleConfig(); + assertNull(new Vehicle("biplane", config).getIcon()); + config.set("icon.material", " COAL "); config.set("icon.model_data", 7403); + VehicleIcon icon = new Vehicle("biplane", config).getIcon(); + assertEquals(Material.COAL, icon.getMaterial()); assertEquals(7403, icon.getModelData()); + config.set("icon.model_data", null); + assertNull(new Vehicle("biplane", config).getIcon().getModelData()); + config.set("icon.material", "not_a_material"); assertNull(new Vehicle("biplane", config).getIcon()); + logger.verify(() -> VFLogger.log("biplane has an icon without a known material: not_a_material")); + config.set("icon", "coal"); assertNull(new Vehicle("biplane", config).getIcon()); + logger.verify(() -> VFLogger.log("biplane has an icon without a known material: null")); + config.set("icon", null); config.set("icon.model_data", 7); assertNull(new Vehicle("biplane", config).getIcon()); + logger.verify(() -> VFLogger.log("biplane has an icon without a known material: null"), times(2)); + } + + @Test void iconItemsCarryTheirModelDataAndTicketsRenderAsTheIcon() { + List made = new ArrayList<>(); + try (MockedConstruction items = mockConstruction(ItemStack.class, (item, context) -> { + made.add((Material) context.arguments().get(0)); ItemMeta meta = mock(ItemMeta.class); + when(meta.getCustomModelDataComponent()).thenReturn(mock(org.bukkit.inventory.meta.components.CustomModelDataComponent.class)); + when(item.getItemMeta()).thenReturn(meta); + })) { + ItemStack plain = new VehicleIcon(Material.COAL, null).createItem(); + verify(plain, never()).setItemMeta(any()); verify(plain.getItemMeta(), never()).setCustomModelDataComponent(any()); + ItemStack modelled = new VehicleIcon(Material.COAL, 7403).createItem(); + ItemMeta modelledMeta = modelled.getItemMeta(); + verify(modelledMeta.getCustomModelDataComponent()).setFloats(List.of(7403f)); + verify(modelled).setItemMeta(modelledMeta); + assertEquals(List.of(Material.COAL, Material.COAL), made); + } + ItemStack ticket = stack(1); ItemMeta meta = ticket.getItemMeta(); + var model = mock(org.bukkit.inventory.meta.components.CustomModelDataComponent.class); when(meta.getCustomModelDataComponent()).thenReturn(model); + VehicleTicketItems.write(ticket, "ABC", "Biplane", new VehicleIcon(Material.COAL, 7403)); + assertEquals("ABC", VehicleTicketItems.readId(ticket)); verify(meta).setDisplayName("§eTicket: Biplane"); + verify(meta).setLore(List.of("§7Valid for this vehicle")); verify(meta).setItemModel(NamespacedKey.minecraft("coal")); + verify(model).setFloats(List.of(7403f)); verify(meta).setCustomModelDataComponent(model); verify(ticket).setItemMeta(meta); + ItemStack plainTicket = stack(1); + VehicleTicketItems.write(plainTicket, "ABC", "Cart", new VehicleIcon(Material.PAPER, null)); + verify(plainTicket.getItemMeta()).setItemModel(NamespacedKey.minecraft("paper")); + verify(plainTicket.getItemMeta()).setCustomModelDataComponent(null); + } + @Test void occupiedSeatCannotAttachOrReparentASecondVehicle() { ActiveVehicle parent = vehicle(), first = vehicle(), second = vehicle(); Seat seat = new Seat(parent, new Seat(SeatType.TOWING, "tow")); seat.mount(first, parent); seat.mount(second, parent); @@ -428,12 +472,13 @@ void sinkingAndDyingUseConfiguredSoundsAndDurationOrRemoveImmediately(boolean si @Test void ticketMetadataRoundTripsAndSearchIncludesOffhandAndStorageOnly() { ItemStack ticket = stack(1); assertNull(VehicleTicketItems.readId(null)); assertNull(VehicleTicketItems.readId(mock(ItemStack.class))); - assertNull(VehicleTicketItems.readId(ticket)); VehicleTicketItems.write(ticket, " ", "ignored"); assertNull(VehicleTicketItems.readId(ticket)); - VehicleTicketItems.write(null, "id", "Cart"); VehicleTicketItems.write(mock(ItemStack.class), "id", "Cart"); - VehicleTicketItems.write(ticket, "ABC", null); assertEquals("ABC", VehicleTicketItems.readId(ticket)); + assertNull(VehicleTicketItems.readId(ticket)); VehicleTicketItems.write(ticket, " ", "ignored", null); assertNull(VehicleTicketItems.readId(ticket)); + VehicleTicketItems.write(null, "id", "Cart", null); VehicleTicketItems.write(mock(ItemStack.class), "id", "Cart", null); + VehicleTicketItems.write(ticket, "ABC", null, null); assertEquals("ABC", VehicleTicketItems.readId(ticket)); verify(ticket.getItemMeta()).setDisplayName("§eTicket: vehicle"); verify(ticket.getItemMeta()).setLore(List.of("§7Valid for this vehicle")); ticket.getItemMeta().getPersistentDataContainer().set(VehicleTicketItems.key(), PersistentDataType.STRING, " "); assertNull(VehicleTicketItems.readId(ticket)); - VehicleTicketItems.write(ticket, "ABC", "Cart"); + VehicleTicketItems.write(ticket, "ABC", "Cart", null); + verify(ticket.getItemMeta(), never()).setItemModel(any()); Player rider = player("Rider"); assertFalse(VehicleTicketItems.inventoryHas(null, "ABC")); assertFalse(VehicleTicketItems.inventoryHas(rider, " ")); when(rider.getInventory().getStorageContents()).thenReturn(null); assertFalse(VehicleTicketItems.inventoryHas(rider, "ABC")); when(rider.getInventory().getItemInOffHand()).thenReturn(ticket); assertTrue(VehicleTicketItems.inventoryHas(rider, "abc")); @@ -470,7 +515,9 @@ void ticketStacksMintExactlyOneAndReturnOrDropInventoryLeftovers(boolean full) { ItemStack minted = c.getArgument(0); assertEquals(1, minted.getAmount()); assertEquals("train-pass", VehicleTicketItems.readId(minted)); HashMap leftovers = new HashMap<>(); if (full) leftovers.put(0, minted); return leftovers; }); + VehicleIcon icon = mock(VehicleIcon.class); when(locomotive.getIcon()).thenReturn(icon); assertTrue(VehicleTicketInteract.handle(admin, vehicle)); assertEquals(3, blank.getAmount()); assertNull(VehicleTicketItems.readId(blank)); + verify(icon).applyModel(any(ItemMeta.class)); verify(blank.getItemMeta(), never()).setItemModel(any()); verify(admin.getInventory()).addItem(any(ItemStack.class)); if (full) verify(world).dropItemNaturally(any(Location.class), any(ItemStack.class)); else verify(world, never()).dropItemNaturally(any(), any()); }