diff --git a/pom.xml b/pom.xml
index 9ab1bec..3946455 100644
--- a/pom.xml
+++ b/pom.xml
@@ -13,7 +13,7 @@
UTF-8
full
25.0.0
- 2.0.0
+ 2.4.1
2.0.1
diff --git a/src/main/java/net/tfminecraft/vfbuilders/api/ConstructionFreeze.java b/src/main/java/net/tfminecraft/vfbuilders/api/ConstructionFreeze.java
index 53a7480..a7f8f48 100644
--- a/src/main/java/net/tfminecraft/vfbuilders/api/ConstructionFreeze.java
+++ b/src/main/java/net/tfminecraft/vfbuilders/api/ConstructionFreeze.java
@@ -10,8 +10,21 @@
public interface ConstructionFreeze {
/**
- * @param constructorUuid player who started the project
+ * @param constructorUuid account that started the project
* @return why the project is paused, shown under the station timer, or null to keep counting down
*/
String freezeReason(UUID constructorUuid);
+
+ /**
+ * Stations call this form. Override it to freeze by character; the default asks
+ * {@link #freezeReason(UUID)} with the account.
+ *
+ * @param constructorUuid account that started the project
+ * @param constructorCharacterId character the account was playing when the project started,
+ * or null when it had no current character session or the project predates the field
+ * @return why the project is paused, or null to keep counting down
+ */
+ default String freezeReason(UUID constructorUuid, UUID constructorCharacterId) {
+ return freezeReason(constructorUuid);
+ }
}
diff --git a/src/main/java/net/tfminecraft/vfbuilders/api/ConstructionFreezes.java b/src/main/java/net/tfminecraft/vfbuilders/api/ConstructionFreezes.java
index abd53e5..39c3d68 100644
--- a/src/main/java/net/tfminecraft/vfbuilders/api/ConstructionFreezes.java
+++ b/src/main/java/net/tfminecraft/vfbuilders/api/ConstructionFreezes.java
@@ -13,6 +13,14 @@ private ConstructionFreezes() {
/** @return the first freeze reason for this constructor, or null when nothing pauses it */
public static String reason(UUID constructorUuid) {
+ return reason(constructorUuid, null);
+ }
+
+ /**
+ * @param constructorCharacterId may be null; providers then decide by account
+ * @return the first freeze reason for this constructor, or null when nothing pauses it
+ */
+ public static String reason(UUID constructorUuid, UUID constructorCharacterId) {
if (constructorUuid == null) {
return null;
}
@@ -20,7 +28,7 @@ public static String reason(UUID constructorUuid) {
: Bukkit.getServicesManager().getRegistrations(ConstructionFreeze.class)) {
String reason;
try {
- reason = registration.getProvider().freezeReason(constructorUuid);
+ reason = registration.getProvider().freezeReason(constructorUuid, constructorCharacterId);
} catch (RuntimeException e) {
// A broken provider must not stall every station on the server.
continue;
diff --git a/src/main/java/net/tfminecraft/vfbuilders/core/ActiveStation.java b/src/main/java/net/tfminecraft/vfbuilders/core/ActiveStation.java
index 3e92897..c36ef02 100644
--- a/src/main/java/net/tfminecraft/vfbuilders/core/ActiveStation.java
+++ b/src/main/java/net/tfminecraft/vfbuilders/core/ActiveStation.java
@@ -28,6 +28,7 @@ public class ActiveStation {
private Location loc;
private Location spawnLoc;
private UUID constructorUuid;
+ private UUID constructorCharacterId;
private TextDisplay timerDisplay;
private String freezeReason;
@@ -51,11 +52,23 @@ public ActiveStation(
int timeLeft,
Location spawnLoc,
UUID constructorUuid) {
+ this(loc, station, blueprintId, timeLeft, spawnLoc, constructorUuid, null);
+ }
+
+ public ActiveStation(
+ Location loc,
+ Station station,
+ String blueprintId,
+ int timeLeft,
+ Location spawnLoc,
+ UUID constructorUuid,
+ UUID constructorCharacterId) {
this.id = UUID.randomUUID();
this.loc = loc;
this.station = station;
this.timeLeft = timeLeft;
this.constructorUuid = constructorUuid;
+ this.constructorCharacterId = constructorCharacterId;
if (blueprintId != null) {
Blueprint blueprint = BlueprintLoader.getByString(blueprintId);
@@ -72,9 +85,14 @@ public ActiveStation(
public void selectBlueprint(Blueprint b, UUID constructorUuid) {
+ selectBlueprint(b, constructorUuid, null);
+ }
+
+ public void selectBlueprint(Blueprint b, UUID constructorUuid, UUID constructorCharacterId) {
blueprint = b;
timeLeft = b.getTime();
this.constructorUuid = constructorUuid;
+ this.constructorCharacterId = constructorCharacterId;
freezeReason = null;
ensureDisplay();
}
@@ -85,7 +103,7 @@ public boolean tick() {
return true;
}
- String reason = ConstructionFreezes.reason(constructorUuid);
+ String reason = ConstructionFreezes.reason(constructorUuid, constructorCharacterId);
if (reason != null) {
freezeReason = reason;
ensureDisplay();
@@ -125,7 +143,8 @@ public void complete() {
Location completedSpawn = spawnLoc.clone();
Bukkit.getPluginManager().callEvent(
- new VehicleConstructEvent(constructorUuid, vehicle, blueprint, completedSpawn, this));
+ new VehicleConstructEvent(
+ constructorUuid, constructorCharacterId, vehicle, blueprint, completedSpawn, this));
// Visual effects
Location center = completedSpawn.clone().add(0.5, 1, 0.5);
@@ -140,18 +159,21 @@ public void complete() {
spawnLoc = null;
blueprint = null;
constructorUuid = null;
+ constructorCharacterId = null;
}
public void cancelConstruction() {
if (blueprint != null) {
blueprint.drop(loc.clone().add(0.5, 1, 0.5));
Bukkit.getPluginManager().callEvent(
- new VehicleConstructionCancelEvent(constructorUuid, blueprint, this));
+ new VehicleConstructionCancelEvent(
+ constructorUuid, constructorCharacterId, blueprint, this));
}
removeHolograms();
spawnLoc = null;
blueprint = null;
constructorUuid = null;
+ constructorCharacterId = null;
freezeReason = null;
timeLeft = 0;
}
@@ -200,6 +222,11 @@ public UUID getConstructorUuid() {
return constructorUuid;
}
+ /** Null when the constructor had no current character session, or for older saved projects. */
+ public UUID getConstructorCharacterId() {
+ return constructorCharacterId;
+ }
+
public void rebindDefinitions() {
if (station != null) {
Station updatedStation = StationLoader.getByString(station.getId());
diff --git a/src/main/java/net/tfminecraft/vfbuilders/core/ConstructorCharacter.java b/src/main/java/net/tfminecraft/vfbuilders/core/ConstructorCharacter.java
new file mode 100644
index 0000000..b0db042
--- /dev/null
+++ b/src/main/java/net/tfminecraft/vfbuilders/core/ConstructorCharacter.java
@@ -0,0 +1,54 @@
+package net.tfminecraft.vfbuilders.core;
+
+import java.util.UUID;
+
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+
+import net.tfminecraft.tlibs.character.Characters;
+
+/**
+ * The character a player builds as. TLibs' session API is probed on first use, so an older
+ * TLibs records no character instead of failing every build. Main thread only.
+ */
+public final class ConstructorCharacter {
+
+ static final String API = "net.tfminecraft.tlibs.character.Characters";
+ static final String SESSION = "net.tfminecraft.tlibs.character.CharacterSession";
+
+ static Boolean available;
+
+ private ConstructorCharacter() {
+ }
+
+ /**
+ * @return the player's current character, or null when the player has no current session
+ * or TLibs lacks the session API
+ */
+ public static UUID of(Player player) {
+ if (available == null) {
+ available = probe(API);
+ }
+ return available ? current(player) : null;
+ }
+
+ static boolean probe(String api) {
+ try {
+ Class> characters = Class.forName(api);
+ characters.getMethod("currentSession", Player.class);
+ characters.getMethod("isCurrent", Class.forName(SESSION));
+ return true;
+ } catch (ReflectiveOperationException | LinkageError e) {
+ Bukkit.getLogger().warning(
+ "[VFBuilders] TLibs has no character session API; constructions record no character");
+ return false;
+ }
+ }
+
+ private static UUID current(Player player) {
+ return Characters.currentSession(player)
+ .filter(Characters::isCurrent)
+ .map(session -> session.ref().characterId())
+ .orElse(null);
+ }
+}
diff --git a/src/main/java/net/tfminecraft/vfbuilders/data/Database.java b/src/main/java/net/tfminecraft/vfbuilders/data/Database.java
index b83beba..aa7b7ce 100644
--- a/src/main/java/net/tfminecraft/vfbuilders/data/Database.java
+++ b/src/main/java/net/tfminecraft/vfbuilders/data/Database.java
@@ -32,8 +32,9 @@ public static void saveStations(Map stations) {
data.spawnLocation = station.hasSpawnLocation() ? toSerializableLoc(station.getSpawnLocation()) : null;
data.blueprintId = station.hasBlueprint() ? station.getBlueprint().getId() : null;
data.timeLeft = station.getTimeLeft();
- if (station.hasBlueprint() && station.getConstructorUuid() != null) {
- data.constructorUuid = station.getConstructorUuid().toString();
+ if (station.hasBlueprint()) {
+ data.constructorUuid = toText(station.getConstructorUuid());
+ data.constructorCharacterId = toText(station.getConstructorCharacterId());
}
dataList.add(data);
@@ -60,22 +61,15 @@ public static HashMap loadStations() {
Station baseStation = StationLoader.getByString(data.stationId);
if (baseStation == null) continue;
- UUID constructorUuid = null;
- if (data.constructorUuid != null && !data.constructorUuid.isEmpty()) {
- try {
- constructorUuid = UUID.fromString(data.constructorUuid);
- } catch (IllegalArgumentException ignored) {
- constructorUuid = null;
- }
- }
-
ActiveStation station = new ActiveStation(
loc,
baseStation,
data.blueprintId,
data.timeLeft,
data.spawnLocation != null ? toBukkitLoc(data.spawnLocation) : null,
- constructorUuid
+ toUuid(data.constructorUuid),
+ // Stations saved before 2.2.0 have no character; SimpleFactions resolves the heir.
+ toUuid(data.constructorCharacterId)
);
loaded.put(loc, station);
@@ -88,6 +82,21 @@ public static HashMap loadStations() {
}
// === Serializable Helpers ===
+ private static String toText(UUID id) {
+ return id != null ? id.toString() : null;
+ }
+
+ private static UUID toUuid(String text) {
+ if (text == null || text.isEmpty()) {
+ return null;
+ }
+ try {
+ return UUID.fromString(text);
+ } catch (IllegalArgumentException ignored) {
+ return null;
+ }
+ }
+
private static SerializableLoc toSerializableLoc(Location loc) {
return new SerializableLoc(loc.getWorld().getName(), loc.getBlockX(), loc.getBlockY(), loc.getBlockZ());
}
@@ -115,6 +124,7 @@ private static class StationData {
String blueprintId;
int timeLeft;
String constructorUuid;
+ String constructorCharacterId;
}
}
diff --git a/src/main/java/net/tfminecraft/vfbuilders/events/BeginVehicleConstructionEvent.java b/src/main/java/net/tfminecraft/vfbuilders/events/BeginVehicleConstructionEvent.java
index 13d658a..895b32e 100644
--- a/src/main/java/net/tfminecraft/vfbuilders/events/BeginVehicleConstructionEvent.java
+++ b/src/main/java/net/tfminecraft/vfbuilders/events/BeginVehicleConstructionEvent.java
@@ -1,5 +1,7 @@
package net.tfminecraft.vfbuilders.events;
+import java.util.UUID;
+
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
@@ -13,6 +15,7 @@ public class BeginVehicleConstructionEvent extends Event implements Cancellable
private static final HandlerList HANDLERS = new HandlerList();
private final Player constructor;
+ private final UUID constructorCharacterId;
private final Blueprint blueprint;
private final ActiveStation station;
private final Location spawnLocation;
@@ -24,7 +27,17 @@ public BeginVehicleConstructionEvent(
Blueprint blueprint,
ActiveStation station,
Location spawnLocation) {
+ this(constructor, null, blueprint, station, spawnLocation);
+ }
+
+ public BeginVehicleConstructionEvent(
+ Player constructor,
+ UUID constructorCharacterId,
+ Blueprint blueprint,
+ ActiveStation station,
+ Location spawnLocation) {
this.constructor = constructor;
+ this.constructorCharacterId = constructorCharacterId;
this.blueprint = blueprint;
this.station = station;
this.spawnLocation = spawnLocation;
@@ -35,6 +48,14 @@ public Player getConstructor() {
return constructor;
}
+ /**
+ * The character the constructor is playing, recorded on the station if construction starts.
+ * Null when the player has no current character session.
+ */
+ public UUID getConstructorCharacterId() {
+ return constructorCharacterId;
+ }
+
public Blueprint getBlueprint() {
return blueprint;
}
diff --git a/src/main/java/net/tfminecraft/vfbuilders/events/VehicleConstructEvent.java b/src/main/java/net/tfminecraft/vfbuilders/events/VehicleConstructEvent.java
index b878efd..94cdba9 100644
--- a/src/main/java/net/tfminecraft/vfbuilders/events/VehicleConstructEvent.java
+++ b/src/main/java/net/tfminecraft/vfbuilders/events/VehicleConstructEvent.java
@@ -16,6 +16,7 @@ public class VehicleConstructEvent extends Event {
private static final HandlerList HANDLERS = new HandlerList();
private final UUID constructorUuid;
+ private final UUID constructorCharacterId;
private final Player constructor;
private final ActiveVehicle vehicle;
private final Blueprint blueprint;
@@ -28,7 +29,18 @@ public VehicleConstructEvent(
Blueprint blueprint,
Location spawnLocation,
ActiveStation station) {
+ this(constructorUuid, null, vehicle, blueprint, spawnLocation, station);
+ }
+
+ public VehicleConstructEvent(
+ UUID constructorUuid,
+ UUID constructorCharacterId,
+ ActiveVehicle vehicle,
+ Blueprint blueprint,
+ Location spawnLocation,
+ ActiveStation station) {
this.constructorUuid = constructorUuid;
+ this.constructorCharacterId = constructorCharacterId;
this.constructor = constructorUuid != null ? Bukkit.getPlayer(constructorUuid) : null;
this.vehicle = vehicle;
this.blueprint = blueprint;
@@ -36,10 +48,19 @@ public VehicleConstructEvent(
this.station = station;
}
+ /** The account that started the project. */
public UUID getConstructorUuid() {
return constructorUuid;
}
+ /**
+ * The character the account was playing when the project started. Null when it had no
+ * current character session, or for projects saved before the character was recorded.
+ */
+ public UUID getConstructorCharacterId() {
+ return constructorCharacterId;
+ }
+
public Player getConstructor() {
return constructor;
}
diff --git a/src/main/java/net/tfminecraft/vfbuilders/events/VehicleConstructionCancelEvent.java b/src/main/java/net/tfminecraft/vfbuilders/events/VehicleConstructionCancelEvent.java
index a0ea3e3..c4df9ff 100644
--- a/src/main/java/net/tfminecraft/vfbuilders/events/VehicleConstructionCancelEvent.java
+++ b/src/main/java/net/tfminecraft/vfbuilders/events/VehicleConstructionCancelEvent.java
@@ -16,20 +16,35 @@ public class VehicleConstructionCancelEvent extends Event {
private static final HandlerList HANDLERS = new HandlerList();
private final UUID constructorUuid;
+ private final UUID constructorCharacterId;
private final Blueprint blueprint;
private final ActiveStation station;
public VehicleConstructionCancelEvent(UUID constructorUuid, Blueprint blueprint, ActiveStation station) {
+ this(constructorUuid, null, blueprint, station);
+ }
+
+ public VehicleConstructionCancelEvent(
+ UUID constructorUuid, UUID constructorCharacterId, Blueprint blueprint, ActiveStation station) {
this.constructorUuid = constructorUuid;
+ this.constructorCharacterId = constructorCharacterId;
this.blueprint = blueprint;
this.station = station;
}
- /** Null for constructions saved before the constructor was recorded. */
+ /** The account that started the project. Null for constructions saved before it was recorded. */
public UUID getConstructorUuid() {
return constructorUuid;
}
+ /**
+ * The character the account was playing when the project started. Null when it had no
+ * current character session, or for constructions saved before the character was recorded.
+ */
+ public UUID getConstructorCharacterId() {
+ return constructorCharacterId;
+ }
+
public Blueprint getBlueprint() {
return blueprint;
}
diff --git a/src/main/java/net/tfminecraft/vfbuilders/managers/StationManager.java b/src/main/java/net/tfminecraft/vfbuilders/managers/StationManager.java
index a15633f..77a7027 100644
--- a/src/main/java/net/tfminecraft/vfbuilders/managers/StationManager.java
+++ b/src/main/java/net/tfminecraft/vfbuilders/managers/StationManager.java
@@ -40,6 +40,7 @@
import net.tfminecraft.vfbuilders.core.ActiveStation;
import net.tfminecraft.vfbuilders.core.Blueprint;
import net.tfminecraft.vfbuilders.core.BlueprintCategory;
+import net.tfminecraft.vfbuilders.core.ConstructorCharacter;
import net.tfminecraft.vfbuilders.core.Station;
import net.tfminecraft.vfbuilders.data.Database;
import net.tfminecraft.vfbuilders.display.StationTimerDisplay;
@@ -261,8 +262,10 @@ public void onPlayerLeftClick(PlayerInteractEvent e) {
}
ActiveStation station = placement.getStation();
+ // Credit the character playing now; without a current session the build still starts.
+ UUID characterId = ConstructorCharacter.of(p);
BeginVehicleConstructionEvent beginEvent = new BeginVehicleConstructionEvent(
- p, blueprint, station, clickLoc);
+ p, characterId, blueprint, station, clickLoc);
Bukkit.getPluginManager().callEvent(beginEvent);
if (beginEvent.isCancelled()) {
if (!beginEvent.isKeepPlacement()) {
@@ -275,7 +278,7 @@ public void onPlayerLeftClick(PlayerInteractEvent e) {
blueprint.takeInputs(p);
placement.getStation().setSpawnLocation(clickLoc);
placement.setFinalSpawnLocation(clickLoc);
- station.selectBlueprint(blueprint, p.getUniqueId());
+ station.selectBlueprint(blueprint, p.getUniqueId(), characterId);
p.sendMessage("§aBuilding site chosen!");
p.sendTitle("", "§eStarted Constructing "+blueprint.getVehicle().getName(), 10, 60, 10);
p.playSound(p, Sound.ENTITY_PLAYER_LEVELUP, 1f, 1f);
diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml
index bd3c2c8..4071932 100644
--- a/src/main/resources/plugin.yml
+++ b/src/main/resources/plugin.yml
@@ -3,7 +3,6 @@ api-version: '1.21.10'
name: VFBuilders
version: ${project.version}
author: Drefvelin
-folia-supported: true
depend: [TLibs, VehicleFramework]
diff --git a/src/test/java/net/tfminecraft/vfbuilders/DatabaseTest.java b/src/test/java/net/tfminecraft/vfbuilders/DatabaseTest.java
index c3a0406..3958cfa 100644
--- a/src/test/java/net/tfminecraft/vfbuilders/DatabaseTest.java
+++ b/src/test/java/net/tfminecraft/vfbuilders/DatabaseTest.java
@@ -75,4 +75,74 @@ void savesRestoresLegacyAndOwnedProjectsAndHandlesIoFailures() throws Exception
}
}
}
+
+ @Test
+ void constructorCharacterRoundTripsAndOlderSavesLoadWithoutOne() throws Exception {
+ var server = MockBukkit.mock();
+ Path temp = DatabaseFixture.folder();
+ try {
+ VFBuilders.plugin = mock(VFBuilders.class);
+ when(VFBuilders.plugin.getDataFolder()).thenReturn(temp.toFile());
+ Files.createDirectories(temp.resolve("data"));
+ var file = temp.resolve("data/stations.json");
+ var world = server.addSimpleWorld("build-world");
+ var loc = new Location(world, 3, 64, 5);
+ var definition = mock(Station.class);
+ when(definition.getId()).thenReturn("dock");
+ StationLoader.get().clear();
+ BlueprintLoader.get().clear();
+ StationLoader.get().put("dock", definition);
+ var blueprint = mock(Blueprint.class);
+ when(blueprint.getId()).thenReturn("boat");
+ when(blueprint.getTime()).thenReturn(40);
+ BlueprintLoader.get().put("boat", blueprint);
+ var account = UUID.randomUUID();
+ var character = UUID.randomUUID();
+ try (var display = mockStatic(net.tfminecraft.vfbuilders.display.StationTimerDisplay.class)) {
+ world.unloadChunk(0, 0);
+ var station = new ActiveStation(loc, definition);
+ station.selectBlueprint(blueprint, account, character);
+ Database.saveStations(Map.of(loc, station));
+ var restored = Database.loadStations().get(loc);
+ assertEquals(account, restored.getConstructorUuid());
+ assertEquals(character, restored.getConstructorCharacterId());
+
+ station.selectBlueprint(blueprint, account, null);
+ Database.saveStations(Map.of(loc, station));
+ assertFalse(Files.readString(file).contains("constructorCharacterId"));
+ restored = Database.loadStations().get(loc);
+ assertEquals(account, restored.getConstructorUuid());
+ assertNull(restored.getConstructorCharacterId());
+
+ // A save written before the character was recorded.
+ Files.writeString(
+ file,
+ """
+ [{"location": {"world": "build-world", "x": 3, "y": 64, "z": 5},
+ "stationId": "dock", "blueprintId": "boat", "timeLeft": 7,
+ "constructorUuid": "%s"}]
+ """
+ .formatted(account));
+ restored = Database.loadStations().get(loc);
+ assertTrue(restored.hasBlueprint());
+ assertEquals(7, restored.getTimeLeft());
+ assertEquals(account, restored.getConstructorUuid());
+ assertNull(restored.getConstructorCharacterId());
+
+ Files.writeString(
+ file,
+ Files.readString(file)
+ .replace("\"timeLeft\"", "\"constructorCharacterId\": \"invalid\", \"timeLeft\""));
+ restored = Database.loadStations().get(loc);
+ assertEquals(account, restored.getConstructorUuid());
+ assertNull(restored.getConstructorCharacterId());
+ }
+ } finally {
+ VFBuilders.plugin = null;
+ MockBukkit.unmock();
+ try (var paths = Files.walk(temp)) {
+ for (var p : paths.sorted(Comparator.reverseOrder()).toList()) Files.delete(p);
+ }
+ }
+ }
}
diff --git a/src/test/java/net/tfminecraft/vfbuilders/DomainTest.java b/src/test/java/net/tfminecraft/vfbuilders/DomainTest.java
index ed5cdb0..690b4fd 100644
--- a/src/test/java/net/tfminecraft/vfbuilders/DomainTest.java
+++ b/src/test/java/net/tfminecraft/vfbuilders/DomainTest.java
@@ -169,6 +169,7 @@ void eventsExposeConstructionIdentityAndCancellationPolicy() {
assertSame(b, e.getBlueprint());
assertSame(station, e.getStation());
assertSame(loc, e.getSpawnLocation());
+ assertNull(e.getConstructorCharacterId());
assertFalse(e.isCancelled());
assertFalse(e.isKeepPlacement());
e.setCancelled(true);
@@ -184,9 +185,11 @@ void eventsExposeConstructionIdentityAndCancellationPolicy() {
assertSame(station, complete.getStation());
assertSame(loc, complete.getSpawnLocation());
assertSame(VehicleConstructEvent.getHandlerList(), complete.getHandlers());
+ assertNull(complete.getConstructorCharacterId());
assertNull(new VehicleConstructEvent(null, null, b, loc, station).getConstructor());
var cancel = new VehicleConstructionCancelEvent(player.getUniqueId(), b, station);
assertEquals(player.getUniqueId(), cancel.getConstructorUuid());
+ assertNull(cancel.getConstructorCharacterId());
assertSame(b, cancel.getBlueprint());
assertSame(station, cancel.getStation());
assertSame(VehicleConstructionCancelEvent.getHandlerList(), cancel.getHandlers());
@@ -204,6 +207,83 @@ void eventsExposeConstructionIdentityAndCancellationPolicy() {
new Cache();
}
+ @Test
+ void eventsCarryTheConstructorCharacterBesideTheAccount() {
+ var player = server.addPlayer();
+ var station = new ActiveStation(player.getLocation(), null);
+ var b = mock(Blueprint.class);
+ var loc = player.getLocation();
+ var character = UUID.randomUUID();
+ var begin = new BeginVehicleConstructionEvent(player, character, b, station, loc);
+ assertSame(player, begin.getConstructor());
+ assertEquals(character, begin.getConstructorCharacterId());
+ assertSame(b, begin.getBlueprint());
+ var complete =
+ new VehicleConstructEvent(player.getUniqueId(), character, null, b, loc, station);
+ assertSame(player, complete.getConstructor());
+ assertEquals(player.getUniqueId(), complete.getConstructorUuid());
+ assertEquals(character, complete.getConstructorCharacterId());
+ assertSame(station, complete.getStation());
+ var cancel = new VehicleConstructionCancelEvent(player.getUniqueId(), character, b, station);
+ assertEquals(player.getUniqueId(), cancel.getConstructorUuid());
+ assertEquals(character, cancel.getConstructorCharacterId());
+ assertSame(b, cancel.getBlueprint());
+ }
+
+ @Test
+ void accessorsCanBeProbedReflectively() throws Exception {
+ for (var type :
+ List.of(
+ BeginVehicleConstructionEvent.class,
+ VehicleConstructEvent.class,
+ VehicleConstructionCancelEvent.class)) {
+ assertEquals(UUID.class, type.getMethod("getConstructorCharacterId").getReturnType());
+ }
+ var freeze = ConstructionFreeze.class.getMethod("freezeReason", UUID.class, UUID.class);
+ assertTrue(freeze.isDefault());
+ assertEquals(String.class, freeze.getReturnType());
+ }
+
+ @Test
+ void accountOnlyFreezesStillDecideWhenAskedWithACharacter() {
+ var account = UUID.randomUUID();
+ var character = UUID.randomUUID();
+ var asked = new ArrayList();
+ ConstructionFreeze byAccount =
+ id -> {
+ asked.add(id);
+ return account.equals(id) ? "account frozen" : null;
+ };
+ assertEquals("account frozen", byAccount.freezeReason(account, character));
+ assertNull(byAccount.freezeReason(UUID.randomUUID(), character));
+ assertEquals(account, asked.get(0));
+
+ var plugin = MockBukkit.createMockPlugin();
+ var seen = new ArrayList>();
+ ConstructionFreeze byCharacter =
+ new ConstructionFreeze() {
+ @Override
+ public String freezeReason(UUID constructorUuid) {
+ return "account only";
+ }
+
+ @Override
+ public String freezeReason(UUID constructorUuid, UUID constructorCharacterId) {
+ seen.add(Arrays.asList(constructorUuid, constructorCharacterId));
+ return character.equals(constructorCharacterId) ? "character frozen" : null;
+ }
+ };
+ server
+ .getServicesManager()
+ .register(ConstructionFreeze.class, byCharacter, plugin, ServicePriority.Normal);
+ assertEquals("character frozen", ConstructionFreezes.reason(account, character));
+ assertNull(ConstructionFreezes.reason(account));
+ assertEquals(List.of(List.of(account, character)), seen.subList(0, 1));
+ assertEquals(Arrays.asList(account, null), seen.get(1));
+ assertNull(ConstructionFreezes.reason(null, character));
+ assertEquals(2, seen.size());
+ }
+
@Test
void freezeProvidersFailIndependentlyAndFirstReasonWins() {
assertNull(ConstructionFreezes.reason(null));
diff --git a/src/test/java/net/tfminecraft/vfbuilders/ManagerTest.java b/src/test/java/net/tfminecraft/vfbuilders/ManagerTest.java
index a66828d..e9a3312 100644
--- a/src/test/java/net/tfminecraft/vfbuilders/ManagerTest.java
+++ b/src/test/java/net/tfminecraft/vfbuilders/ManagerTest.java
@@ -220,9 +220,64 @@ void placementDistanceMaterialsAndSuccessfulCommit() throws Exception {
assertTrue(placements().isEmpty());
assertSame(b, s.getBlueprint());
assertEquals(player.getUniqueId(), s.getConstructorUuid());
+ // No character provider: the build starts without a character.
+ assertNull(s.getConstructorCharacterId());
assertEquals(player.getLocation(), s.getSpawnLocation());
}
+ @Test
+ void buildStartRecordsTheCurrentCharacterOnTheEventAndStation() throws Exception {
+ var character = UUID.randomUUID();
+ var session =
+ new net.tfminecraft.tlibs.character.CharacterSession(
+ new net.tfminecraft.tlibs.character.CharacterRef(player.getUniqueId(), character), 1L);
+ var identity = mock(net.tfminecraft.tlibs.character.CharacterIdentity.class);
+ when(identity.currentSession(player)).thenReturn(Optional.of(session));
+ when(identity.isCurrent(session)).thenReturn(true);
+ var plugin = MockBukkit.createMockPlugin();
+ server
+ .getServicesManager()
+ .register(
+ net.tfminecraft.tlibs.character.CharacterIdentity.class,
+ identity,
+ plugin,
+ org.bukkit.plugin.ServicePriority.Normal);
+ var begun = new ArrayList();
+ server
+ .getPluginManager()
+ .registerEvent(
+ net.tfminecraft.vfbuilders.events.BeginVehicleConstructionEvent.class,
+ new org.bukkit.event.Listener() {},
+ org.bukkit.event.EventPriority.MONITOR,
+ (listener, event) ->
+ begun.add(
+ ((net.tfminecraft.vfbuilders.events.BeginVehicleConstructionEvent) event)
+ .getConstructorCharacterId()),
+ plugin);
+ var s = manager.getStation(block);
+ var b = mock(Blueprint.class);
+ var vehicle = mock(net.tfminecraft.vehicleframework.vehicles.Vehicle.class);
+ when(vehicle.getName()).thenReturn("Boat");
+ when(b.getVehicle()).thenReturn(vehicle);
+ when(b.hasInputs(player)).thenReturn(true);
+ player.teleport(block.getLocation());
+ placements().put(player.getUniqueId(), new ActivePlacement(player, s, b));
+ manager.onPlayerLeftClick(left());
+ assertEquals(List.of(character), begun);
+ assertEquals(player.getUniqueId(), s.getConstructorUuid());
+ assertEquals(character, s.getConstructorCharacterId());
+
+ // A session that is no longer current is not credited; the build still starts.
+ s.cancelConstruction();
+ when(identity.isCurrent(session)).thenReturn(false);
+ placements().put(player.getUniqueId(), new ActivePlacement(player, s, b));
+ manager.onPlayerLeftClick(left());
+ assertEquals(Arrays.asList(character, null), begun);
+ assertSame(b, s.getBlueprint());
+ assertEquals(player.getUniqueId(), s.getConstructorUuid());
+ assertNull(s.getConstructorCharacterId());
+ }
+
@Test
void cancelledBeginCanKeepPlacementForConfirmation() throws Exception {
var s = manager.getStation(block);
diff --git a/src/test/java/net/tfminecraft/vfbuilders/StationTest.java b/src/test/java/net/tfminecraft/vfbuilders/StationTest.java
index bcf0184..d7a24c3 100644
--- a/src/test/java/net/tfminecraft/vfbuilders/StationTest.java
+++ b/src/test/java/net/tfminecraft/vfbuilders/StationTest.java
@@ -70,10 +70,10 @@ void selectionCountdownFreezeAndCancellation() {
assertTrue(s.hasBlueprint());
assertEquals(2, s.getTimeLeft());
assertEquals(id, s.getConstructorUuid());
- freezes.when(() -> ConstructionFreezes.reason(id)).thenReturn("pause");
+ freezes.when(() -> ConstructionFreezes.reason(id, null)).thenReturn("pause");
assertFalse(s.tick());
assertEquals(2, s.getTimeLeft());
- freezes.when(() -> ConstructionFreezes.reason(id)).thenReturn(null);
+ freezes.when(() -> ConstructionFreezes.reason(id, null)).thenReturn(null);
assertFalse(s.tick());
assertTrue(s.tick());
assertEquals(0, s.getTimeLeft());
@@ -91,6 +91,56 @@ void selectionCountdownFreezeAndCancellation() {
assertEquals(id, s.getConstructorUuid());
}
+ @Test
+ void characterIsAskedForFreezesAndCarriedToCompletionAndCancellation() {
+ var account = UUID.randomUUID();
+ var character = UUID.randomUUID();
+ var s = new ActiveStation(loc, definition);
+ s.selectBlueprint(b, account, character);
+ assertEquals(account, s.getConstructorUuid());
+ assertEquals(character, s.getConstructorCharacterId());
+ freezes.when(() -> ConstructionFreezes.reason(account, character)).thenReturn("siege");
+ assertFalse(s.tick());
+ assertEquals(2, s.getTimeLeft());
+
+ var fired = new ArrayList();
+ var plugin = MockBukkit.createMockPlugin();
+ for (var type :
+ List.of(
+ net.tfminecraft.vfbuilders.events.VehicleConstructEvent.class,
+ net.tfminecraft.vfbuilders.events.VehicleConstructionCancelEvent.class)) {
+ server
+ .getPluginManager()
+ .registerEvent(
+ type,
+ new org.bukkit.event.Listener() {},
+ org.bukkit.event.EventPriority.MONITOR,
+ (listener, event) -> fired.add(event),
+ plugin);
+ }
+ s.setSpawnLocation(loc);
+ var p = mock(Player.class);
+ when(p.getLocation()).thenReturn(loc);
+ when(world.getPlayers()).thenReturn(List.of(p));
+ try (var vf = mockStatic(VehicleFramework.class)) {
+ var manager = mock(VehicleManager.class);
+ vf.when(VehicleFramework::getVehicleManager).thenReturn(manager);
+ when(manager.spawn(loc, (Vehicle) null)).thenReturn(mock(ActiveVehicle.class));
+ s.complete();
+ }
+ var built = (net.tfminecraft.vfbuilders.events.VehicleConstructEvent) fired.get(0);
+ assertEquals(account, built.getConstructorUuid());
+ assertEquals(character, built.getConstructorCharacterId());
+ assertNull(s.getConstructorCharacterId());
+
+ s.selectBlueprint(b, account, character);
+ s.cancelConstruction();
+ var cancelled = (net.tfminecraft.vfbuilders.events.VehicleConstructionCancelEvent) fired.get(1);
+ assertEquals(account, cancelled.getConstructorUuid());
+ assertEquals(character, cancelled.getConstructorCharacterId());
+ assertNull(s.getConstructorCharacterId());
+ }
+
@Test
void restorationAndReloadRebindOrRefund() {
var old = new ActiveStation(loc, definition, null, 3, null);
@@ -104,6 +154,12 @@ void restorationAndReloadRebindOrRefund() {
BlueprintLoader.get().put("boat", b);
var s = new ActiveStation(loc, definition, "boat", 5, loc, UUID.randomUUID());
assertSame(b, s.getBlueprint());
+ assertNull(s.getConstructorCharacterId());
+ var character = UUID.randomUUID();
+ assertEquals(
+ character,
+ new ActiveStation(loc, definition, "boat", 5, loc, UUID.randomUUID(), character)
+ .getConstructorCharacterId());
s.rebindDefinitions();
assertSame(definition, s.getStation());
var replacement = mock(Station.class);
diff --git a/src/test/java/net/tfminecraft/vfbuilders/core/ConstructorCharacterTest.java b/src/test/java/net/tfminecraft/vfbuilders/core/ConstructorCharacterTest.java
new file mode 100644
index 0000000..f4d4976
--- /dev/null
+++ b/src/test/java/net/tfminecraft/vfbuilders/core/ConstructorCharacterTest.java
@@ -0,0 +1,72 @@
+package net.tfminecraft.vfbuilders.core;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.util.*;
+import net.tfminecraft.tlibs.character.*;
+import org.bukkit.plugin.ServicePriority;
+import org.junit.jupiter.api.*;
+import org.mockbukkit.mockbukkit.*;
+
+class ConstructorCharacterTest {
+ ServerMock server;
+
+ @BeforeEach
+ void setup() {
+ server = MockBukkit.mock();
+ ConstructorCharacter.available = null;
+ }
+
+ @AfterEach
+ void teardown() {
+ ConstructorCharacter.available = null;
+ MockBukkit.unmock();
+ }
+
+ @Test
+ void currentSessionNamesTheCharacterAndStaleOrMissingSessionsNameNone() {
+ var player = server.addPlayer();
+ assertNull(ConstructorCharacter.of(player));
+ assertTrue(ConstructorCharacter.available);
+
+ var character = UUID.randomUUID();
+ var session = new CharacterSession(new CharacterRef(player.getUniqueId(), character), 4L);
+ var identity = mock(CharacterIdentity.class);
+ when(identity.currentSession(player)).thenReturn(Optional.of(session));
+ when(identity.isCurrent(session)).thenReturn(true);
+ server
+ .getServicesManager()
+ .register(
+ CharacterIdentity.class,
+ identity,
+ MockBukkit.createMockPlugin(),
+ ServicePriority.Normal);
+ assertEquals(character, ConstructorCharacter.of(player));
+ when(identity.isCurrent(session)).thenReturn(false);
+ assertNull(ConstructorCharacter.of(player));
+ when(identity.currentSession(player)).thenReturn(Optional.empty());
+ assertNull(ConstructorCharacter.of(player));
+ }
+
+ @Test
+ void olderTlibsWithoutTheSessionApiRecordsNoCharacter() {
+ assertTrue(ConstructorCharacter.probe(ConstructorCharacter.API));
+ assertFalse(ConstructorCharacter.probe("net.tfminecraft.tlibs.character.Missing"));
+ // An older Characters class without the session methods.
+ assertFalse(ConstructorCharacter.probe(String.class.getName()));
+
+ var player = server.addPlayer();
+ var identity = mock(CharacterIdentity.class);
+ server
+ .getServicesManager()
+ .register(
+ CharacterIdentity.class,
+ identity,
+ MockBukkit.createMockPlugin(),
+ ServicePriority.Normal);
+ ConstructorCharacter.available = false;
+ assertNull(ConstructorCharacter.of(player));
+ verifyNoInteractions(identity);
+ }
+}