From 540a4484a24d7c9c8eb80247f1ad3d655600459e Mon Sep 17 00:00:00 2001 From: XxFran10xX <318299142+XxFran10xX@users.noreply.github.com> Date: Tue, 27 Oct 2026 21:39:33 +0100 Subject: [PATCH] feat: show a per-vehicle icon in menus and on tickets Vehicles may set an optional icon section (material plus optional model_data) after model. When set, the skin menu shows every skin as the icon with a glint and "Current skin" on the active one, the seat menu shows the vehicle in slot 24, the repair menu uses it for the hull, the ownership menu shows it in slot 18, and minted tickets take its item model and custom model data. Vehicles without an icon keep the old menus. The skin click now finds the current skin by its id instead of by yellow concrete. The default vehicles use the new coal icons 7400-7421. Co-Authored-By: Claude Opus 5.5 --- .../managers/InventoryManager.java | 53 ++++++++++++---- .../managers/OwnershipGUIManager.java | 15 +++++ .../managers/VehicleManager.java | 6 +- .../vehicles/ActiveVehicle.java | 8 +++ .../vehicleframework/vehicles/Vehicle.java | 9 +++ .../vehicles/handlers/VehicleIcon.java | 58 ++++++++++++++++++ .../handlers/VehicleTicketInteract.java | 5 +- .../vehicles/handlers/VehicleTicketItems.java | 6 +- src/main/resources/vehicles/aa_turret.yml | 3 + src/main/resources/vehicles/anti_air.yml | 3 + src/main/resources/vehicles/behemoth.yml | 3 + src/main/resources/vehicles/biplane.yml | 3 + src/main/resources/vehicles/bomber.yml | 3 + src/main/resources/vehicles/cloudskimmer.yml | 3 + src/main/resources/vehicles/coal_car.yml | 3 + src/main/resources/vehicles/cruiser.yml | 3 + .../resources/vehicles/field_artillery.yml | 3 + .../resources/vehicles/fixed_artillery.yml | 3 + src/main/resources/vehicles/flat_car.yml | 3 + src/main/resources/vehicles/gunboat.yml | 3 + src/main/resources/vehicles/gyrobomber.yml | 3 + src/main/resources/vehicles/horse_cart.yml | 3 + src/main/resources/vehicles/ironclad.yml | 3 + src/main/resources/vehicles/monoplane.yml | 3 + src/main/resources/vehicles/passenger_car.yml | 3 + .../resources/vehicles/simple_locomotive.yml | 3 + src/main/resources/vehicles/sloop.yml | 3 + src/main/resources/vehicles/small_car.yml | 3 + src/main/resources/vehicles/torpedoboat.yml | 3 + src/main/resources/vehicles/wooden_cart.yml | 3 + .../CommandAndOwnershipCoverageTest.java | 11 ++++ .../InventoryManagerCoverageTest.java | 61 +++++++++++++++++++ .../managers/OwnershipManagerTest.java | 13 ++++ .../managers/VehicleManagerCoverageTest.java | 21 ++++++- .../vehicles/ActiveVehicleTest.java | 3 + .../handlers/VehicleHandlersCoverageTest.java | 57 +++++++++++++++-- 36 files changed, 368 insertions(+), 24 deletions(-) create mode 100644 src/main/java/net/tfminecraft/vehicleframework/vehicles/handlers/VehicleIcon.java 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()); }