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 @@
*
* 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 |
@@ -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));
+ }
+
}