diff --git a/README.md b/README.md index dacff1d..6776f2d 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,23 @@ are reported separately. HTML/XML reports are written to `target/site/jacoco/`. The Build workflow runs verification on pull requests and pushes to `main` and uploads both test and coverage reports. +## Server-owned vehicles + +Staff can run `/vf server` and right-click a vehicle within 60 seconds to convert it. +`/vf server list` lists live and stored server vehicles (also available from console). +Conversion clears the whitelist, group switches and tickets. These vehicles cannot be +claimed. Use requires `vf.server.use` (default op) or `vf.admin`. Whitelists, group +switches and tickets cannot be enabled on them. Revert with `/vf takeover [character]` +or the staff ownership menu's release button. + +API consumers can recognize the case-insensitive owner value `server` with +`VehicleOwners.isServer`; its display name is `Server`. It is neither unowned nor a +personal/character owner. Conversion fires the usual ownership events with staff reason +`SERVER`. The synchronous `ServerVehicleUseEvent` defaults to denied; listeners may +call `setAllowed(true)` to admit a player without permission, for example for a side +assigned by staff during a battle. Coupled cars also check their locomotive's access. +`cleanup.keep-owners: [server]` protects these vehicles from configured cleanup. + ## License Copyright (c) 2026 TF-Minecraft contributors. diff --git a/src/main/java/net/tfminecraft/vehicleframework/api/OwnershipReason.java b/src/main/java/net/tfminecraft/vehicleframework/api/OwnershipReason.java index 800dcd5..209f74c 100644 --- a/src/main/java/net/tfminecraft/vehicleframework/api/OwnershipReason.java +++ b/src/main/java/net/tfminecraft/vehicleframework/api/OwnershipReason.java @@ -18,6 +18,8 @@ public enum OwnershipReason { GUILD_ACCESS_TOGGLE, ALLY_ACCESS_TOGGLE, VASSAL_ACCESS_TOGGLE, + /** Staff converted the vehicle to server ownership. */ + SERVER, MIGRATE, RESTAMP, RELEASE, diff --git a/src/main/java/net/tfminecraft/vehicleframework/api/VehicleOwners.java b/src/main/java/net/tfminecraft/vehicleframework/api/VehicleOwners.java index a3f93df..2190d49 100644 --- a/src/main/java/net/tfminecraft/vehicleframework/api/VehicleOwners.java +++ b/src/main/java/net/tfminecraft/vehicleframework/api/VehicleOwners.java @@ -13,6 +13,7 @@ * * * + * * * @@ -23,6 +24,7 @@ * is the one captured when the entry was added. */ public final class VehicleOwners { + public static final String SERVER = "server"; public static final String NONE = "none"; public static final String NO_CHARACTER = "char_none"; public static final String CHARACTER_PREFIX = "char_"; @@ -52,6 +54,11 @@ public static boolean isUnowned(String owner) { return owner == null || owner.isBlank() || owner.equalsIgnoreCase(NONE); } + /** Whether the vehicle belongs to the server. */ + public static boolean isServer(String owner) { + return SERVER.equalsIgnoreCase(owner); + } + /** Whether the value names a character. */ public static boolean isCharacterOwner(String owner) { return characterId(owner).isPresent(); @@ -71,13 +78,16 @@ public static boolean isPersonalOwner(String owner) { } /** - * The canonical form of a value that characters mode may write ({@code none}, {@code char_none} - * or {@code char_}), or empty for anything else. + * The canonical form of a value that characters mode may write ({@code none}, {@code char_none}, + * {@code server} or {@code char_}), or empty for anything else. */ public static Optional canonical(String owner) { if (owner == null) { return Optional.empty(); } + if (isServer(owner)) { + return Optional.of(SERVER); + } if (owner.equalsIgnoreCase(NONE)) { return Optional.of(NONE); } diff --git a/src/main/java/net/tfminecraft/vehicleframework/events/ServerVehicleUseEvent.java b/src/main/java/net/tfminecraft/vehicleframework/events/ServerVehicleUseEvent.java new file mode 100644 index 0000000..739a0ea --- /dev/null +++ b/src/main/java/net/tfminecraft/vehicleframework/events/ServerVehicleUseEvent.java @@ -0,0 +1,40 @@ +package net.tfminecraft.vehicleframework.events; + +import org.bukkit.entity.Player; +import org.bukkit.event.HandlerList; +import org.bukkit.event.player.PlayerEvent; + +import net.tfminecraft.vehicleframework.vehicles.ActiveVehicle; + +/** Synchronous request to admit a player without server-use permission. Denied by default. */ +public final class ServerVehicleUseEvent extends PlayerEvent { + private static final HandlerList HANDLERS = new HandlerList(); + private final ActiveVehicle vehicle; + private boolean allowed; + + public ServerVehicleUseEvent(Player player, ActiveVehicle vehicle) { + super(player); + this.vehicle = vehicle; + } + + public ActiveVehicle getVehicle() { + return vehicle; + } + + public boolean isAllowed() { + return allowed; + } + + public void setAllowed(boolean allowed) { + this.allowed = allowed; + } + + @Override + public HandlerList getHandlers() { + return HANDLERS; + } + + public static HandlerList getHandlerList() { + return HANDLERS; + } +} diff --git a/src/main/java/net/tfminecraft/vehicleframework/managers/CommandManager.java b/src/main/java/net/tfminecraft/vehicleframework/managers/CommandManager.java index 96ca606..46324c9 100644 --- a/src/main/java/net/tfminecraft/vehicleframework/managers/CommandManager.java +++ b/src/main/java/net/tfminecraft/vehicleframework/managers/CommandManager.java @@ -138,6 +138,22 @@ public boolean onCommand(CommandSender sender, Command cmd, String label, String sender.sendMessage("§cYou do not have access to this command!"); return true; } + if(args[0].equalsIgnoreCase("server")) { + if (args.length == 2 && args[1].equalsIgnoreCase("list")) { + sender.sendMessage("§bServer vehicles:"); + for (OwnedVehicleSummary vehicle : VehicleFramework.getVehicleManager().listOwnedVehicles( + net.tfminecraft.vehicleframework.api.VehicleOwners.SERVER)) { + sender.sendMessage("§e" + vehicle.getName() + " §7- " + formatVehicleLocation(vehicle.getLocation())); + } + } else if (args.length != 1) { + sender.sendMessage("§cUsage: /vf server [list]"); + } else if (sender instanceof Player p) { + VehicleFramework.getVehicleManager().getOwnershipManager().startServer(p); + } else { + sender.sendMessage("§cOnly players can use this command."); + } + return true; + } if(args[0].equalsIgnoreCase("takeover")) { if(!(sender instanceof Player)) { sender.sendMessage("§cOnly players can use this command."); diff --git a/src/main/java/net/tfminecraft/vehicleframework/managers/InventoryManager.java b/src/main/java/net/tfminecraft/vehicleframework/managers/InventoryManager.java index 4766191..3c5d9c8 100644 --- a/src/main/java/net/tfminecraft/vehicleframework/managers/InventoryManager.java +++ b/src/main/java/net/tfminecraft/vehicleframework/managers/InventoryManager.java @@ -22,6 +22,7 @@ import net.tfminecraft.vehicleframework.ownership.AccessKey; import net.tfminecraft.vehicleframework.ownership.GroupAccess; import net.tfminecraft.vehicleframework.ownership.OwnershipAccess; +import net.tfminecraft.vehicleframework.ownership.ServerVehicles; import net.tfminecraft.vehicleframework.ownership.OwnershipActions; import net.tfminecraft.vehicleframework.VehicleFramework; import net.tfminecraft.vehicleframework.vehicles.ActiveVehicle; @@ -49,11 +50,13 @@ public class InventoryManager { * The vehicle's outside hold, if it has one and the player may open it: the owner, a group * the vehicle always admits (such as its faction), and while the owner has the whitelist * switched on, those on it and its groups. A ticket is not enough. Staff get in as for - * seats: by takeover. + * seats: by takeover. Server vehicles instead use their permission/event gate. */ public static Container outsideHold(Player p, ActiveVehicle v) { Container hold = v.hasContainers() ? v.getContainerHandler().outside() : null; if(hold == null) return null; + if (!ServerVehicles.mayAccess(p, v)) return null; + if (ServerVehicles.isServer(v)) return hold; AccessKey key = OwnershipAccess.key(p); OwnerData data = v.getOwnerData(); boolean owner = key != null && data != null && key.owns(data.getOwner()); diff --git a/src/main/java/net/tfminecraft/vehicleframework/managers/OwnershipManager.java b/src/main/java/net/tfminecraft/vehicleframework/managers/OwnershipManager.java index f377fa8..395a680 100644 --- a/src/main/java/net/tfminecraft/vehicleframework/managers/OwnershipManager.java +++ b/src/main/java/net/tfminecraft/vehicleframework/managers/OwnershipManager.java @@ -124,6 +124,7 @@ public void startTakeover(Player p, String targetName) { } target = named.get(); } + pending.cancel(p, Kind.SERVER); pending.start(p, Kind.TAKEOVER, null, actor.value().session(), target); if (target.name() == null) { p.sendMessage("§aRight-click a vehicle within 60 seconds to claim ownership."); @@ -242,6 +243,54 @@ public boolean takeover(Player p, ActiveVehicle v) { return true; } + /** Arms the staff conversion under the current character session. */ + public void startServer(Player p) { + Outcome actor = OwnershipActions.actor(p); + if (actor.refusal() != null) { + p.sendMessage(actor.refusal()); + return; + } + pending.cancel(p, Kind.TAKEOVER); + pending.start(p, Kind.SERVER, null, actor.value().session(), null); + p.sendMessage("§aRight-click a vehicle within 60 seconds to make it a server vehicle."); + } + + /** Applies a pending server conversion through the ownership event and persistence pipeline. */ + public boolean makeServer(Player p, ActiveVehicle v) { + Optional taken = pending.take(p, Kind.SERVER); + if (taken.isEmpty()) return false; + if (!Permissions.isAdmin(p)) { + p.sendMessage(OwnershipActions.NOT_ALLOWED); + return true; + } + if (!PendingActions.stillCurrent(taken.get())) { + p.sendMessage(SESSION_CHANGED); + return true; + } + Outcome actor = OwnershipActions.actor(p); + if (actor.refusal() != null) { + p.sendMessage(actor.refusal()); + return true; + } + String previousOwner = v.getOwnerData().getOwner(); + if (VehicleOwners.isServer(previousOwner)) { + p.sendMessage("§cThat is already a server vehicle."); + return true; + } + if (write(actor.value(), v, OwnershipReason.SERVER, null, null, data -> { + data.setOwner(VehicleOwners.SERVER); + data.setWhiteList(new ArrayList<>()); + data.setWhiteListed(false); + data.setAccessGroups(null); + data.setTicketsEnabled(false); + }) == Result.OK) { + p.sendMessage("§e" + v.getName() + "§a is now a server vehicle."); + net.tfminecraft.vehicleframework.VFLogger.info("server-owned " + v.getUUID() + " (" + v.getId() + + ") by " + p.getName() + ": " + previousOwner + " -> server"); + } + return true; + } + /** Starts renaming with a name tag. */ @SuppressWarnings("deprecation") public void startRename(Player p, ActiveVehicle v) { @@ -270,6 +319,10 @@ public void ownershipClick(InventoryClickEvent e) { ActiveVehicle v = menuVehicle(e, VFGUI.OWNERSHIP); if (v == null) return; Player p = (Player) e.getWhoClicked(); + if (VehicleOwners.isServer(v.getOwnerData().getOwner()) && e.getSlot() != 6) { + p.sendMessage(OwnershipActions.SERVER_ACCESS); + return; + } switch (e.getSlot()) { case 0 -> { if (change(p, v, OwnershipReason.WHITELIST_TOGGLE, null, null, diff --git a/src/main/java/net/tfminecraft/vehicleframework/managers/VehicleManager.java b/src/main/java/net/tfminecraft/vehicleframework/managers/VehicleManager.java index 11fb432..331d8a9 100644 --- a/src/main/java/net/tfminecraft/vehicleframework/managers/VehicleManager.java +++ b/src/main/java/net/tfminecraft/vehicleframework/managers/VehicleManager.java @@ -78,6 +78,7 @@ import net.tfminecraft.vehicleframework.managers.spawner.VehicleSpawner; import net.tfminecraft.vehicleframework.ownership.OwnerNames; import net.tfminecraft.vehicleframework.ownership.OwnershipAccess; +import net.tfminecraft.vehicleframework.ownership.ServerVehicles; import net.tfminecraft.vehicleframework.protocol.PacketConverter; import net.tfminecraft.vehicleframework.tracks.RecorderLog; import net.tfminecraft.vehicleframework.tracks.TrainGhosts; @@ -481,10 +482,11 @@ public void seatInteract(Player p, ActiveVehicle v) { if(v.isPassenger(p, true)) { return; } + if (!ServerVehicles.check(p, v)) return; ActiveVehicle ticketSource = v.ticketSource(); boolean hasTicket = VehicleTicketItems.inventoryHas( p, ticketSource.getOwnerData().getTicketId()); - boolean admitted = OwnershipAccess.admits(v, OwnershipAccess.key(p)); + boolean admitted = ServerVehicles.isServer(v) || OwnershipAccess.admits(v, OwnershipAccess.key(p)); if (!VehicleTicketRules.mayOpenSeatMenu(v.getOwnerData(), ticketSource.getOwnerData(), admitted, hasTicket)) { p.sendMessage(ticketSource.getOwnerData().isTicketsEnabled() ? "§cYou need a ticket to board this vehicle." @@ -518,6 +520,7 @@ private void handlePendingEntityMount(Player p, Entity entity) { p.sendMessage("§cThe vehicle has been destroyed"); return; } + if (!ServerVehicles.check(p, v)) return; if(!isEntityAllowed(entity, v.getEntitySeatWhitelist())) { p.sendMessage("§cThat creature cannot ride in this seat"); return; @@ -635,7 +638,7 @@ public void vehicleInteract(PlayerInteractEntityEvent e){ if(!vehicles.containsKey(entity)) return; ActiveVehicle v = vehicles.get(entity); // Admin takeover goes through the ownership events like any other change - if(ownership.takeover(p, v)) { + if(ownership.takeover(p, v) || ownership.makeServer(p, v)) { e.setCancelled(true); return; } @@ -645,6 +648,10 @@ public void vehicleInteract(PlayerInteractEntityEvent e){ e.setCancelled(true); return; } + if (!ServerVehicles.check(p, v)) { + e.setCancelled(true); + return; + } if(cooldown.containsKey(p)) { if(cooldown.get(p) > System.currentTimeMillis()) { return; @@ -969,9 +976,10 @@ private boolean allowTicketSeat(Player p, ActiveVehicle v, Seat seat) { if (p == null || v == null || seat == null) { return true; } + if (!ServerVehicles.check(p, v)) return false; ActiveVehicle source = v.ticketSource(); boolean has = VehicleTicketItems.inventoryHas(p, source.getOwnerData().getTicketId()); - boolean admitted = OwnershipAccess.admits(v, OwnershipAccess.key(p)); + boolean admitted = ServerVehicles.isServer(v) || OwnershipAccess.admits(v, OwnershipAccess.key(p)); if (VehicleTicketRules.mayEnter(v.getOwnerData(), source.getOwnerData(), admitted, seat.getType(), has)) { return true; } @@ -1096,6 +1104,7 @@ private void retryMount(ActiveVehicle fresh, UUID riderId, String seatBone, Play } Entity rider = riderId == null ? null : Bukkit.getEntity(riderId); if (rider instanceof Player player && player.isOnline()) { + if (!ServerVehicles.check(player, fresh)) return; takeSeat(player, fresh, seat); return; } @@ -1158,6 +1167,7 @@ public void seatSelect(InventoryClickEvent e) { if(id == null) return; Seat seat = v.getSeat(id); + if (!ServerVehicles.check(p, v)) return; // Occupied entity seat: click to dismount the entity and teleport it to the player if(i.getType().equals(Material.GRAY_CONCRETE) && seat != null && seat.getType().equals(SeatType.ENTITY)) { if(seat.isOccupied()) { diff --git a/src/main/java/net/tfminecraft/vehicleframework/ownership/GroupAccess.java b/src/main/java/net/tfminecraft/vehicleframework/ownership/GroupAccess.java index 36e2b45..1e99afe 100644 --- a/src/main/java/net/tfminecraft/vehicleframework/ownership/GroupAccess.java +++ b/src/main/java/net/tfminecraft/vehicleframework/ownership/GroupAccess.java @@ -116,7 +116,7 @@ private static Optional owned(ActiveVehicle vehicle) { return Optional.empty(); } String owner = vehicle.getOwnerData().getOwner(); - if (VehicleOwners.isUnowned(owner)) { + if (VehicleOwners.isUnowned(owner) || VehicleOwners.isServer(owner)) { return Optional.empty(); } return OwnershipStore.parseUuid(vehicle.getUUID()).map(id -> new Owned(id, owner)); diff --git a/src/main/java/net/tfminecraft/vehicleframework/ownership/OwnerNames.java b/src/main/java/net/tfminecraft/vehicleframework/ownership/OwnerNames.java index f13f573..15e5638 100644 --- a/src/main/java/net/tfminecraft/vehicleframework/ownership/OwnerNames.java +++ b/src/main/java/net/tfminecraft/vehicleframework/ownership/OwnerNames.java @@ -22,6 +22,9 @@ public static String player(Player player) { /** An owner value as text, such as "Kira" or, for staff, "Kira (Steve)". */ public static String owner(String owner, boolean staff) { + if (VehicleOwners.isServer(owner)) { + return "Server"; + } if (VehicleOwners.isUnowned(owner)) { return "nobody"; } diff --git a/src/main/java/net/tfminecraft/vehicleframework/ownership/OwnershipAccess.java b/src/main/java/net/tfminecraft/vehicleframework/ownership/OwnershipAccess.java index 6f64b4b..278bae4 100644 --- a/src/main/java/net/tfminecraft/vehicleframework/ownership/OwnershipAccess.java +++ b/src/main/java/net/tfminecraft/vehicleframework/ownership/OwnershipAccess.java @@ -51,6 +51,7 @@ public static boolean isOwner(Player player, OwnerData data) { /** Whether the key owns the vehicle, is on its whitelist, or is in a group it admits. */ public static boolean admits(ActiveVehicle vehicle, AccessKey key) { - return VehicleTicketRules.ownerOrWhitelisted(vehicle.getOwnerData(), key) || GroupAccess.admits(vehicle, key); + return !ServerVehicles.isServer(vehicle) + && (VehicleTicketRules.ownerOrWhitelisted(vehicle.getOwnerData(), key) || GroupAccess.admits(vehicle, key)); } } diff --git a/src/main/java/net/tfminecraft/vehicleframework/ownership/OwnershipActions.java b/src/main/java/net/tfminecraft/vehicleframework/ownership/OwnershipActions.java index a6dc8d2..d6688f2 100644 --- a/src/main/java/net/tfminecraft/vehicleframework/ownership/OwnershipActions.java +++ b/src/main/java/net/tfminecraft/vehicleframework/ownership/OwnershipActions.java @@ -31,6 +31,7 @@ public final class OwnershipActions { public static final String UNAVAILABLE = "§cVehicle ownership changes are unavailable right now."; public static final String NOT_OWNER = "§cYou do not own this vehicle."; public static final String NOT_ALLOWED = "§cYou cannot change that on this vehicle."; + public static final String SERVER_ACCESS = "§cServer vehicles have no whitelist, tickets or group access."; public static final String SAVE_FAILED = "§cCould not save the change. Nothing was changed."; private OwnershipActions() { @@ -91,6 +92,9 @@ public static boolean controls(Actor actor, ActiveVehicle vehicle) { * source), or empty if they may. Fires {@link VehicleOwnershipEditCheckEvent}. */ public static Optional refusal(Player player, ActiveVehicle vehicle, OwnershipReason reason) { + if (ServerVehicles.isServer(vehicle) && reason != OwnershipReason.CLEAR) { + return Optional.of(SERVER_ACCESS); + } Outcome actor = actor(player); if (actor.refusal() != null) { return Optional.of(actor.refusal()); diff --git a/src/main/java/net/tfminecraft/vehicleframework/ownership/PendingActions.java b/src/main/java/net/tfminecraft/vehicleframework/ownership/PendingActions.java index f86483f..7b9932d 100644 --- a/src/main/java/net/tfminecraft/vehicleframework/ownership/PendingActions.java +++ b/src/main/java/net/tfminecraft/vehicleframework/ownership/PendingActions.java @@ -23,7 +23,8 @@ public final class PendingActions { public enum Kind { WHITELIST_ADD("§cWhitelist entry timed out."), RENAME("§cNaming timed out."), - TAKEOVER("§cTakeover expired."); + TAKEOVER("§cTakeover expired."), + SERVER("§cServer vehicle action expired."); private final String timeoutMessage; diff --git a/src/main/java/net/tfminecraft/vehicleframework/ownership/ServerVehicles.java b/src/main/java/net/tfminecraft/vehicleframework/ownership/ServerVehicles.java new file mode 100644 index 0000000..50beed4 --- /dev/null +++ b/src/main/java/net/tfminecraft/vehicleframework/ownership/ServerVehicles.java @@ -0,0 +1,41 @@ +package net.tfminecraft.vehicleframework.ownership; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import net.tfminecraft.vehicleframework.api.VehicleOwners; +import net.tfminecraft.vehicleframework.events.ServerVehicleUseEvent; +import net.tfminecraft.vehicleframework.permissions.Permissions; +import net.tfminecraft.vehicleframework.vehicles.ActiveVehicle; + +/** Server ownership is independent of player identities, whitelists, groups and tickets. */ +public final class ServerVehicles { + private ServerVehicles() { + } + + public static boolean isServer(ActiveVehicle vehicle) { + return vehicle != null && vehicle.getOwnerData() != null + && VehicleOwners.isServer(vehicle.getOwnerData().getOwner()); + } + + public static boolean mayUse(Player player, ActiveVehicle vehicle) { + if (player.hasPermission("vf.server.use") || Permissions.isAdmin(player)) return true; + ServerVehicleUseEvent event = new ServerVehicleUseEvent(player, vehicle); + Bukkit.getPluginManager().callEvent(event); + return event.isAllowed(); + } + + /** Check the car and locomotive separately; deck clicks already resolve to their car. */ + public static boolean mayAccess(Player player, ActiveVehicle vehicle) { + if (isServer(vehicle) && !mayUse(player, vehicle)) return false; + ActiveVehicle source = vehicle.ticketSource(); + return source == vehicle || !isServer(source) || mayUse(player, source); + } + + /** Shared interaction refusal, also used for stale menus and pending entity mounts. */ + public static boolean check(Player player, ActiveVehicle vehicle) { + if (mayAccess(player, vehicle)) return true; + player.sendMessage("§cThis is a server vehicle."); + return false; + } +} diff --git a/src/main/java/net/tfminecraft/vehicleframework/util/TabCompletion.java b/src/main/java/net/tfminecraft/vehicleframework/util/TabCompletion.java index ec64605..8b1e045 100644 --- a/src/main/java/net/tfminecraft/vehicleframework/util/TabCompletion.java +++ b/src/main/java/net/tfminecraft/vehicleframework/util/TabCompletion.java @@ -25,7 +25,8 @@ public List onTabComplete(CommandSender sender, Command cmd, String labe private List candidates(CommandSender sender, Command cmd, String[] args) { if (!(sender instanceof Player)) { if (!cmd.getName().equalsIgnoreCase("vf") || !Permissions.isAdmin(sender)) return List.of(); - if (args.length == 1) return List.of("reload", "admin", "track"); + if (args.length == 1) return List.of("reload", "admin", "track", "server"); + if (args.length == 2 && args[0].equalsIgnoreCase("server")) return List.of("list"); if (args.length == 2 && args[0].equalsIgnoreCase("admin")) return List.of("quarantine"); if (args.length == 2 && args[0].equalsIgnoreCase("track")) return List.of("resync"); return List.of(); @@ -43,6 +44,7 @@ private List candidates(CommandSender sender, Command cmd, String[] args } if (Permissions.isAdmin(sender)) { completions.add("takeover"); + completions.add("server"); completions.add("reload"); completions.add("track"); completions.add("admin"); @@ -50,6 +52,8 @@ private List candidates(CommandSender sender, Command cmd, String[] args return completions; } + if (args[0].equalsIgnoreCase("server") && args.length == 2 && Permissions.isAdmin(sender)) return List.of("list"); + if (args[0].equalsIgnoreCase("admin") && args.length == 2 && Permissions.isAdmin(sender)) { completions.add("quarantine"); return completions; diff --git a/src/main/java/net/tfminecraft/vehicleframework/vehicles/handlers/VehicleTicketRules.java b/src/main/java/net/tfminecraft/vehicleframework/vehicles/handlers/VehicleTicketRules.java index 6a382a0..e2f145f 100644 --- a/src/main/java/net/tfminecraft/vehicleframework/vehicles/handlers/VehicleTicketRules.java +++ b/src/main/java/net/tfminecraft/vehicleframework/vehicles/handlers/VehicleTicketRules.java @@ -1,6 +1,7 @@ package net.tfminecraft.vehicleframework.vehicles.handlers; import net.tfminecraft.vehicleframework.data.OwnerData; +import net.tfminecraft.vehicleframework.api.VehicleOwners; import net.tfminecraft.vehicleframework.enums.SeatType; import net.tfminecraft.vehicleframework.ownership.AccessKey; @@ -15,7 +16,7 @@ public static boolean ownerOrWhitelisted(OwnerData data, String playerName) { /** A {@code null} key (no identity) matches nobody. */ public static boolean ownerOrWhitelisted(OwnerData data, AccessKey key) { - if (data == null || key == null) { + if (data == null || key == null || VehicleOwners.isServer(data.getOwner())) { return false; } if (key.owns(data.getOwner())) { @@ -45,7 +46,8 @@ public static boolean mayEnter(OwnerData access, OwnerData tickets, AccessKey ke * that would refuse them, a ticket only covers passenger seats. */ public static boolean mayEnter(OwnerData access, OwnerData tickets, boolean admitted, SeatType seat, boolean hasMatchingTicket) { - boolean ticketsEnabled = tickets != null && tickets.isTicketsEnabled(); + if (access != null && VehicleOwners.isServer(access.getOwner())) return admitted; + boolean ticketsEnabled = tickets != null && !VehicleOwners.isServer(tickets.getOwner()) && tickets.isTicketsEnabled(); if (!admitted && gated(access)) { return ticketsEnabled && seat == SeatType.PASSENGER && hasMatchingTicket; } @@ -66,15 +68,17 @@ public static boolean mayOpenSeatMenu(OwnerData access, OwnerData tickets, Acces } public static boolean mayOpenSeatMenu(OwnerData access, OwnerData tickets, boolean admitted, boolean hasMatchingTicket) { + if (access != null && VehicleOwners.isServer(access.getOwner())) return admitted; if (!gated(access) || admitted) { return true; } - return tickets != null && tickets.isTicketsEnabled() && hasMatchingTicket; + return tickets != null && !VehicleOwners.isServer(tickets.getOwner()) && tickets.isTicketsEnabled() && hasMatchingTicket; } /** Whether the vehicle's whitelist is enforced: switched on, on an owned vehicle. */ public static boolean gated(OwnerData access) { return access != null + && !VehicleOwners.isServer(access.getOwner()) && access.isWhiteListed() && access.getOwner() != null && !access.getOwner().equalsIgnoreCase("none"); diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 1eb67d2..cf763e4 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -10,4 +10,12 @@ softdepend: [MythicMobs, CoreProtect] commands: vf: useage: / - description: Vehicle Framework Command \ No newline at end of file + description: Vehicle Framework Command + +permissions: + vf.server.use: + description: Use server-owned vehicles. + default: op + vf.admin: + description: Administer vehicles and ownership. + default: op diff --git a/src/test/java/net/tfminecraft/vehicleframework/api/VehicleOwnersTest.java b/src/test/java/net/tfminecraft/vehicleframework/api/VehicleOwnersTest.java index de1b26e..c0024fe 100644 --- a/src/test/java/net/tfminecraft/vehicleframework/api/VehicleOwnersTest.java +++ b/src/test/java/net/tfminecraft/vehicleframework/api/VehicleOwnersTest.java @@ -90,4 +90,15 @@ void reasonsKnowWhichAreSystemWrites() { assertTrue(reason.reason().isSystem()); } } + + @Test void serverOwnerIsCanonicalAndNeverPersonalOrClaimable() { + assertEquals("server", VehicleOwners.SERVER); + assertTrue(VehicleOwners.isServer("SeRvEr")); + assertFalse(VehicleOwners.isServer(null)); + assertFalse(VehicleOwners.isUnowned("server")); + assertFalse(VehicleOwners.isCharacterOwner("server")); + assertFalse(VehicleOwners.isPersonalOwner("server")); + assertEquals(Optional.of("server"), VehicleOwners.canonical("SERVER")); + } + } diff --git a/src/test/java/net/tfminecraft/vehicleframework/managers/CommandAndOwnershipCoverageTest.java b/src/test/java/net/tfminecraft/vehicleframework/managers/CommandAndOwnershipCoverageTest.java index 79aabe2..26ec084 100644 --- a/src/test/java/net/tfminecraft/vehicleframework/managers/CommandAndOwnershipCoverageTest.java +++ b/src/test/java/net/tfminecraft/vehicleframework/managers/CommandAndOwnershipCoverageTest.java @@ -230,7 +230,7 @@ void ownershipMenuReflectsWhitelistTicketsAndLinkedTrainOwnership(boolean enable } var console = mock(CommandSender.class); when(console.hasPermission("vf.admin")).thenReturn(true); - assertEquals(List.of("reload", "admin", "track"), tabs.onTabComplete(console, command, "vf", new String[]{""})); + assertEquals(List.of("reload", "admin", "track", "server"), tabs.onTabComplete(console, command, "vf", new String[]{""})); assertEquals(List.of("reload"), tabs.onTabComplete(console, command, "vf", new String[]{"RE"})); assertEquals(List.of("quarantine"), tabs.onTabComplete(console, command, "vf", new String[]{"admin", "Q"})); assertEquals(List.of("resync"), tabs.onTabComplete(console, command, "vf", new String[]{"track", "R"})); @@ -445,4 +445,28 @@ private void configure(ItemStack item, Material type) { doAnswer(call -> { lore.set(0, List.copyOf(call.getArgument(0))); return null; }).when(meta).setLore(anyList()); when(meta.getLore()).thenAnswer(call -> lore.get(0)); } private T keep(T scope) { scopes.add(scope); return scope; } + + @Test void serverCommandsAndCompletionAreStaffOnlyAndListStoredAndLiveVehicles() { + assertTrue(run(player, "server")); + verify(vehicles, never()).getOwnershipManager(); + when(player.hasPermission("vf.admin")).thenReturn(true); + OwnershipManager manager = mock(OwnershipManager.class); when(vehicles.getOwnershipManager()).thenReturn(manager); + assertTrue(run(player, "server")); verify(manager).startServer(player); + assertTrue(run(player, "server", "bad")); + verify(player).sendMessage("§cUsage: /vf server [list]"); + CommandSender console = mock(CommandSender.class); when(console.hasPermission("vf.admin")).thenReturn(true); + assertTrue(run(console, "server")); verify(console).sendMessage("§cOnly players can use this command."); + OwnedVehicleSummary stored = mock(OwnedVehicleSummary.class), live = mock(OwnedVehicleSummary.class); + when(stored.getName()).thenReturn("Stored"); when(stored.getLocation()).thenReturn(Optional.empty()); + when(live.getName()).thenReturn("Live"); when(live.getLocation()).thenReturn(Optional.of(new Location(world, 1, 64, 2))); + when(vehicles.listOwnedVehicles("server")).thenReturn(List.of(stored, live)); + assertTrue(run(console, "server", "list")); + verify(console).sendMessage("§eStored §7- §7location unknown (stored)"); + verify(console).sendMessage("§eLive §7- §fworld 1, 64, 2"); + TabCompletion tabs = new TabCompletion(); + assertEquals(List.of("server"), tabs.onTabComplete(player, command, "vf", new String[]{"ser"})); + assertEquals(List.of("list"), tabs.onTabComplete(player, command, "vf", new String[]{"server", "l"})); + assertEquals(List.of("list"), tabs.onTabComplete(console, command, "vf", new String[]{"server", ""})); + } + } diff --git a/src/test/java/net/tfminecraft/vehicleframework/managers/InventoryManagerCoverageTest.java b/src/test/java/net/tfminecraft/vehicleframework/managers/InventoryManagerCoverageTest.java index bbcd686..8766993 100644 --- a/src/test/java/net/tfminecraft/vehicleframework/managers/InventoryManagerCoverageTest.java +++ b/src/test/java/net/tfminecraft/vehicleframework/managers/InventoryManagerCoverageTest.java @@ -342,4 +342,15 @@ private String tag(ItemStack item, String name) { } private static final class ItemState { Material type; String name; List lore = List.of(); } private T keep(T scope) { scopes.add(scope); return scope; } + + @Test void serverHoldRequiresPermissionEvenWithStaleWhitelist() { + when(vehicle.hasContainers()).thenReturn(true); + ContainerHandler containers = mock(ContainerHandler.class); when(vehicle.getContainerHandler()).thenReturn(containers); + Container hold = mock(Container.class); when(containers.outside()).thenReturn(hold); + when(owner.getOwner()).thenReturn("server"); + assertNull(InventoryManager.outsideHold(player, vehicle)); + when(player.hasPermission("vf.server.use")).thenReturn(true); + assertSame(hold, InventoryManager.outsideHold(player, vehicle)); + } + } diff --git a/src/test/java/net/tfminecraft/vehicleframework/managers/OwnershipManagerTest.java b/src/test/java/net/tfminecraft/vehicleframework/managers/OwnershipManagerTest.java index 2dab694..ff10b21 100644 --- a/src/test/java/net/tfminecraft/vehicleframework/managers/OwnershipManagerTest.java +++ b/src/test/java/net/tfminecraft/vehicleframework/managers/OwnershipManagerTest.java @@ -792,4 +792,66 @@ void namesModeTakeoverUsesTheAccountNameAndRefusesTypedTargets() { tasks.getLast().run(); verify(staff).sendMessage("§cTakeover expired."); } + + @Test void conversionClearsAccessAndCanBeTakenOverOrReleased() { + ActiveVehicle v = vehicle(owner(kira)); + v.getOwnerData().setWhiteList(new ArrayList<>(List.of("crew"))); + v.getOwnerData().setWhiteListed(true); v.getOwnerData().setTicketsEnabled(true); + v.getOwnerData().setAccessGroups(Set.of(AccessGroup.ALLY)); + assertFalse(manager.makeServer(staff, v)); + manager.startServer(staff); + assertTrue(manager.makeServer(staff, v)); + assertEquals("server", v.getOwnerData().getOwner()); + assertTrue(v.getOwnerData().getWhiteList().isEmpty()); + assertTrue(v.getOwnerData().getAccessGroups().isEmpty()); + assertFalse(v.getOwnerData().isWhiteListed()); assertFalse(v.getOwnerData().isTicketsEnabled()); + VehicleOwnershipChangeEvent pre = fired(VehicleOwnershipChangeEvent.class).getFirst(); + assertEquals(OwnershipReason.SERVER, pre.getReason()); assertTrue(pre.isStaff()); + assertFalse(OwnershipReason.SERVER.isSystem()); assertTrue(pre.getTarget().isEmpty()); + assertTrue(fired(VehicleOwnershipChangedEvent.class).getFirst().isSuccess()); + assertFalse(manager.claim(alex, v)); + manager.startServer(staff); manager.makeServer(staff, v); + verify(staff).sendMessage("§cThat is already a server vehicle."); + manager.startTakeover(staff, null); manager.takeover(staff, v); + assertEquals(owner(warden), v.getOwnerData().getOwner()); + assertEquals(OwnershipReason.TAKEOVER, fired(VehicleOwnershipChangeEvent.class).getLast().getReason()); + v.getOwnerData().setOwner("server"); + for (int slot : new int[]{0, 2, 4, 8, OwnershipGUIManager.FIRST_GROUP_SLOT}) manager.ownershipClick(menu(staff, v, slot)); + verify(staff, times(5)).sendMessage(OwnershipActions.SERVER_ACCESS); + for (OwnershipReason reason : List.of(OwnershipReason.WHITELIST_ADD, OwnershipReason.WHITELIST_REMOVE, + OwnershipReason.WHITELIST_TOGGLE, OwnershipReason.TICKETS_TOGGLE, OwnershipReason.ALLY_ACCESS_TOGGLE)) { + assertEquals(Optional.of(OwnershipActions.SERVER_ACCESS), OwnershipActions.refusal(staff, v, reason)); + } + manager.ownershipClick(menu(staff, v, 6)); + assertEquals("none", v.getOwnerData().getOwner()); + } + + @Test void armingServerOrTakeoverDisarmsTheOther() { + ActiveVehicle v = vehicle(owner(kira)); + manager.startTakeover(staff, null); manager.startServer(staff); + assertFalse(manager.takeover(staff, v)); + assertEquals(owner(kira), v.getOwnerData().getOwner()); + manager.startTakeover(staff, null); + assertFalse(manager.makeServer(staff, v)); + assertEquals(owner(kira), v.getOwnerData().getOwner()); + } + + @Test void conversionRechecksActorSessionAndPermissionAndHonorsVeto() { + ActiveVehicle v = vehicle("none"); + playing(staff, null); manager.startServer(staff); + verify(staff).sendMessage(OwnershipActions.NEED_CHARACTER); + playing(staff, warden); manager.startServer(staff); + when(staff.hasPermission("vf.admin")).thenReturn(false); + manager.makeServer(staff, v); verify(staff).sendMessage(OwnershipActions.NOT_ALLOWED); + when(staff.hasPermission("vf.admin")).thenReturn(true); + manager.startServer(staff); characters.when(() -> Characters.isCurrent(warden)).thenReturn(false); + manager.makeServer(staff, v); verify(staff).sendMessage(OwnershipManager.SESSION_CHANGED); + characters.when(() -> Characters.isCurrent(warden)).thenReturn(true); + manager.startServer(staff); playing(staff, null); manager.makeServer(staff, v); + verify(staff, times(2)).sendMessage(OwnershipActions.NEED_CHARACTER); + playing(staff, warden); manager.startServer(staff); + doAnswer(call -> { if (call.getArgument(0) instanceof VehicleOwnershipChangeEvent pre) pre.setCancelled(true); return null; }).when(plugins).callEvent(any(Event.class)); + manager.makeServer(staff, v); assertEquals("none", v.getOwnerData().getOwner()); + } + } diff --git a/src/test/java/net/tfminecraft/vehicleframework/managers/VehicleManagerCoverageTest.java b/src/test/java/net/tfminecraft/vehicleframework/managers/VehicleManagerCoverageTest.java index ff2b28a..8f83589 100644 --- a/src/test/java/net/tfminecraft/vehicleframework/managers/VehicleManagerCoverageTest.java +++ b/src/test/java/net/tfminecraft/vehicleframework/managers/VehicleManagerCoverageTest.java @@ -799,7 +799,7 @@ void rejectedMountRecoveryDeduplicatesAndReleasesItsReservationWhenVehicleDisapp } @ParameterizedTest - @ValueSource(strings = {"unload-failed", "storage-closed", "missing-save", "unknown-type", "spawn-failed", "missing-seat", "missing-rider", "player", "animal", "rejected-animal"}) + @ValueSource(strings = {"unload-failed", "storage-closed", "missing-save", "unknown-type", "spawn-failed", "missing-seat", "missing-rider", "player", "server-denied", "animal", "rejected-animal"}) void rejectedMountReloadEitherRetriesTheSavedSeatOrReportsWhyItCannot(String outcome) { Player player = player("Ryan"); ActiveVehicle old = vehicle("v"), fresh = vehicle("v"); register(old); VehiclePersistence persistence = persistence(); @@ -819,9 +819,13 @@ void rejectedMountReloadEitherRetriesTheSavedSeatOrReportsWhyItCannot(String out UUID notifyId = player.getUniqueId(); bukkit.when(() -> Bukkit.getPlayer(notifyId)).thenReturn(player); when(fresh.addPassenger(rider, seat)).thenReturn(outcome.equals("rejected-animal") ? MountResult.REJECTED : MountResult.MOUNTED); if (outcome.equals("storage-closed")) storage.when(VehiclePersistence::current).thenReturn(persistence, (VehiclePersistence) null); + if (outcome.equals("server-denied")) fresh.getOwnerData().setOwner("server"); manager.recoverRejectedMount(old, rider, "seat", player); assertEquals(1, scheduled.size()); scheduled.removeFirst().run(); - if (Set.of("player", "animal", "rejected-animal").contains(outcome)) { + if (outcome.equals("server-denied")) { + verify(fresh, never()).addPassenger(any(), any()); + verify(player).sendMessage("§cThis is a server vehicle."); + } else if (Set.of("player", "animal", "rejected-animal").contains(outcome)) { verify(fresh).addPassenger(rider, seat); assertSame(fresh, manager.get(fresh.getEntity())); } else if (outcome.equals("unload-failed")) { @@ -1080,4 +1084,42 @@ void ownershipAndInputDefensiveHelpersRejectMissingParticipants() throws Excepti cooldown.setAccessible(true); cooldown.invoke(manager, null, vehicle("v")); assertTrue(map("ejectCooldown").isEmpty()); } + + @Test void serverAccessGatesClickSeatsMountAndEntityMenus() throws Exception { + ActiveVehicle v = vehicle("server-v"); Player player = player("Ryan"); register(v); + v.getOwnerData().setOwner("server"); + PlayerInteractEntityEvent interact = interact(player, v.getEntity()); + manager.vehicleInteract(interact); assertTrue(interact.isCancelled()); + manager.seatInteract(player, v); + Seat seat = mock(Seat.class); when(seat.getType()).thenReturn(SeatType.PASSENGER); + when(v.getSeat("seat")).thenReturn(seat); when(v.getSeatHandler().getSeat("seat")).thenReturn(seat); + manager.mount(player, "seat", v); verify(v, never()).addPassenger(any(), any()); + map("tempVehicle").put(player, v); + manager.seatSelect(click(player, v, VFGUI.SEAT_SELECTION, 0, item(Material.CYAN_CONCRETE, "seat"))); + assertTrue(map("pendingEntityVehicle").isEmpty()); + Entity animal = mock(Entity.class); + map("pendingEntityVehicle").put(player, v); map("pendingEntitySeat").put(player, "seat"); + manager.vehicleInteract(interact(player, animal)); verify(v, never()).addPassenger(any(), any()); + when(player.hasPermission("vf.server.use")).thenReturn(true); + manager.seatInteract(player, v); + verify(inventories.constructed().getFirst()).seatSelection(null, player, v, true); + when(v.addPassenger(player, seat)).thenReturn(MountResult.MOUNTED); + manager.mount(player, "seat", v); verify(v).addPassenger(player, seat); + } + + @Test void serverConversionClickRunsBeforePreInteractAndDeckChecksLocomotive() throws Exception { + ActiveVehicle v = vehicle("server-v"); Player player = player("Ryan"); register(v); + when(player.hasPermission("vf.admin")).thenReturn(true); + when(persistence().saveLive(v)).thenReturn(true); + manager.getOwnershipManager().startServer(player); + PlayerInteractEntityEvent event = interact(player, v.getEntity()); manager.vehicleInteract(event); + assertTrue(event.isCancelled()); assertEquals("server", v.getOwnerData().getOwner()); + verify(plugins, never()).callEvent(isA(VehiclePreInteractEvent.class)); + when(player.hasPermission("vf.admin")).thenReturn(false); + ActiveVehicle car = vehicle("car"); register(car); when(car.ticketSource()).thenReturn(v); + Entity deck = mock(Entity.class); decks.when(() -> DeckBody.owner(deck)).thenReturn(car); + event = interact(player, deck); manager.vehicleInteract(event); assertTrue(event.isCancelled()); + verify(player).sendMessage("§cThis is a server vehicle."); + } + } diff --git a/src/test/java/net/tfminecraft/vehicleframework/ownership/OwnerNamesTest.java b/src/test/java/net/tfminecraft/vehicleframework/ownership/OwnerNamesTest.java index 39dd241..eb8308b 100644 --- a/src/test/java/net/tfminecraft/vehicleframework/ownership/OwnerNamesTest.java +++ b/src/test/java/net/tfminecraft/vehicleframework/ownership/OwnerNamesTest.java @@ -65,4 +65,10 @@ void onlinePlayersShowTheirDisplayNameInCharactersMode() { OwnershipFixture.mode(OwnershipIdentity.CHARACTERS); assertEquals("Hooded figure", OwnerNames.player(player)); } + + @Test void serverDisplayIsTheSameForEveryone() { + assertEquals("Server", OwnerNames.owner("SERVER", true)); + assertEquals("Server", OwnerNames.owner("server", false)); + } + } diff --git a/src/test/java/net/tfminecraft/vehicleframework/ownership/ServerVehiclesTest.java b/src/test/java/net/tfminecraft/vehicleframework/ownership/ServerVehiclesTest.java new file mode 100644 index 0000000..a9c0b72 --- /dev/null +++ b/src/test/java/net/tfminecraft/vehicleframework/ownership/ServerVehiclesTest.java @@ -0,0 +1,55 @@ +package net.tfminecraft.vehicleframework.ownership; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.mockito.ArgumentMatchers.*; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.plugin.PluginManager; +import org.junit.jupiter.api.Test; + +import net.tfminecraft.vehicleframework.data.OwnerData; +import net.tfminecraft.vehicleframework.events.ServerVehicleUseEvent; +import net.tfminecraft.vehicleframework.vehicles.ActiveVehicle; + +class ServerVehiclesTest { + @Test void permissionsEventsAndTicketSources() { + Player player = mock(Player.class); + ActiveVehicle car = mock(ActiveVehicle.class), loco = mock(ActiveVehicle.class); + OwnerData data = new OwnerData(); data.setOwner("server"); + when(car.getOwnerData()).thenReturn(data); when(car.ticketSource()).thenReturn(car); + assertFalse(ServerVehicles.isServer(null)); + assertFalse(ServerVehicles.isServer(loco)); + PluginManager plugins = mock(PluginManager.class); + try (var bukkit = mockStatic(Bukkit.class)) { + bukkit.when(Bukkit::getPluginManager).thenReturn(plugins); + assertFalse(OwnershipAccess.admits(car, AccessKey.name("Alex"))); + assertFalse(ServerVehicles.check(player, car)); + verify(player).sendMessage("§cThis is a server vehicle."); + when(player.hasPermission("vf.server.use")).thenReturn(true); + assertTrue(ServerVehicles.check(player, car)); + when(player.hasPermission("vf.server.use")).thenReturn(false); + when(player.hasPermission("vf.admin")).thenReturn(true); + assertTrue(ServerVehicles.mayUse(player, car)); + when(player.hasPermission("vf.admin")).thenReturn(false); + doAnswer(call -> { + ServerVehicleUseEvent event = call.getArgument(0); + assertSame(player, event.getPlayer()); assertSame(car, event.getVehicle()); + assertFalse(event.isAllowed()); assertFalse(event.isAsynchronous()); + assertSame(ServerVehicleUseEvent.getHandlerList(), event.getHandlers()); + event.setAllowed(true); return null; + }).when(plugins).callEvent(any(ServerVehicleUseEvent.class)); + assertTrue(ServerVehicles.mayUse(player, car)); + reset(plugins); + when(car.ticketSource()).thenReturn(loco); + when(loco.getOwnerData()).thenReturn(data); + OwnerData personal = new OwnerData(); when(car.getOwnerData()).thenReturn(personal); + assertFalse(ServerVehicles.mayAccess(player, car)); + when(player.hasPermission("vf.server.use")).thenReturn(true); + assertTrue(ServerVehicles.mayAccess(player, car)); + when(loco.getOwnerData()).thenReturn(personal); + assertTrue(ServerVehicles.mayAccess(player, car)); + } + } +} diff --git a/src/test/java/net/tfminecraft/vehicleframework/vehicles/handlers/VehicleTicketRulesTest.java b/src/test/java/net/tfminecraft/vehicleframework/vehicles/handlers/VehicleTicketRulesTest.java index 39fb204..131909a 100644 --- a/src/test/java/net/tfminecraft/vehicleframework/vehicles/handlers/VehicleTicketRulesTest.java +++ b/src/test/java/net/tfminecraft/vehicleframework/vehicles/handlers/VehicleTicketRulesTest.java @@ -174,4 +174,19 @@ private static OwnerData gatedAccess() { access.setWhiteListed(true); return access; } + + @Test void serverVehiclesIgnoreStaleAccessAndTickets() { + OwnerData data = new OwnerData(); data.setOwner("server"); + data.setWhiteListed(true); data.setWhiteList(java.util.List.of("player_Alex")); data.setTicketsEnabled(true); + assertFalse(VehicleTicketRules.gated(data)); + assertFalse(VehicleTicketRules.ownerOrWhitelisted(data, "Alex")); + assertFalse(VehicleTicketRules.mayEnter(data, data, false, SeatType.PASSENGER, true)); + assertFalse(VehicleTicketRules.mayOpenSeatMenu(data, data, false, true)); + assertTrue(VehicleTicketRules.mayEnter(data, data, true, SeatType.CAPTAIN, false)); + assertTrue(VehicleTicketRules.mayOpenSeatMenu(data, data, true, false)); + OwnerData car = new OwnerData(); car.setOwner("player_Other"); car.setWhiteListed(true); + assertFalse(VehicleTicketRules.mayEnter(car, data, false, SeatType.PASSENGER, true)); + assertFalse(VehicleTicketRules.mayOpenSeatMenu(car, data, false, true)); + } + }
Owner values
{@code char_}owned by that character
{@code server}server-owned; use requires permission or an event grant
{@code none}unowned, claimable
{@code char_none}faction vehicle without a leader character; matches nobody and * can never be claimed