Skip to content

Commit b5c2a99

Browse files
committed
feat: restore thrown ender pearls and mounted vehicules after reconnect
1 parent 4899c70 commit b5c2a99

9 files changed

Lines changed: 286 additions & 19 deletions

File tree

src/main/java/fr/xephi/authme/data/limbo/LimboPlayer.java

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22

33
import fr.xephi.authme.task.MessageTask;
44
import org.bukkit.Location;
5+
import org.bukkit.entity.EntityType;
56
import org.bukkit.scheduler.BukkitTask;
67

78
import java.util.ArrayList;
89
import java.util.Collection;
10+
import java.util.HashSet;
11+
import java.util.Set;
12+
import java.util.UUID;
913

1014
/**
1115
* Represents a player which is not logged in and keeps track of certain states (like OP status, flying)
@@ -25,6 +29,9 @@ public class LimboPlayer {
2529
private BukkitTask timeoutTask = null;
2630
private MessageTask messageTask = null;
2731
private LimboPlayerState state = LimboPlayerState.PASSWORD_REQUIRED;
32+
private Set<UUID> enderPearlUuids = new HashSet<>();
33+
private UUID vehicleUuid;
34+
private EntityType vehicleType;
2835

2936
public LimboPlayer(Location loc, boolean operator, Collection<UserGroup> groups, boolean fly, float walkSpeed,
3037
float flySpeed) {
@@ -134,4 +141,53 @@ public LimboPlayerState getState() {
134141
public void setState(LimboPlayerState state) {
135142
this.state = state;
136143
}
144+
145+
/**
146+
* Returns the entity UUIDs of ender pearls in flight for this player,
147+
* used to restore stasis chambers after a reconnect.
148+
*
149+
* @return mutable set of ender pearl entity UUIDs (never null)
150+
*/
151+
public Set<UUID> getEnderPearlUuids() {
152+
return enderPearlUuids;
153+
}
154+
155+
/**
156+
* Sets the ender pearl entity UUIDs to restore on reconnect.
157+
*
158+
* @param pearlUuids the set of pearl entity UUIDs to track
159+
*/
160+
public void setEnderPearlUuids(Set<UUID> pearlUuids) {
161+
this.enderPearlUuids = new HashSet<>(pearlUuids);
162+
}
163+
164+
/**
165+
* Returns the UUID of the vehicle entity the player was riding when they disconnected,
166+
* used to remount them on reconnect.
167+
*
168+
* @return the vehicle entity UUID, or null if not riding anything
169+
*/
170+
public UUID getVehicleUuid() {
171+
return vehicleUuid;
172+
}
173+
174+
/**
175+
* Returns the type of the vehicle the player was riding when they disconnected.
176+
*
177+
* @return the vehicle EntityType, or null if not riding anything
178+
*/
179+
public EntityType getVehicleType() {
180+
return vehicleType;
181+
}
182+
183+
/**
184+
* Sets the vehicle to restore on reconnect. Pass null for both arguments to clear.
185+
*
186+
* @param vehicleUuid the entity UUID of the vehicle, or null
187+
* @param vehicleType the entity type of the vehicle, or null
188+
*/
189+
public void setVehicle(UUID vehicleUuid, EntityType vehicleType) {
190+
this.vehicleUuid = vehicleUuid;
191+
this.vehicleType = vehicleType;
192+
}
137193
}

src/main/java/fr/xephi/authme/data/limbo/LimboService.java

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,19 @@
66
import fr.xephi.authme.settings.Settings;
77
import fr.xephi.authme.settings.SpawnLoader;
88
import org.bukkit.Location;
9+
import org.bukkit.World;
10+
import org.bukkit.entity.EnderPearl;
11+
import org.bukkit.entity.Entity;
12+
import org.bukkit.entity.EntityType;
913
import org.bukkit.entity.Player;
1014

1115
import javax.inject.Inject;
16+
import java.util.Collections;
1217
import java.util.Locale;
1318
import java.util.Map;
1419
import java.util.Optional;
20+
import java.util.Set;
21+
import java.util.UUID;
1522
import java.util.concurrent.ConcurrentHashMap;
1623

1724
import static fr.xephi.authme.settings.properties.LimboSettings.RESTORE_ALLOW_FLIGHT;
@@ -189,4 +196,102 @@ private Optional<LimboPlayer> getLimboOrLogError(Player player, String context)
189196
}
190197
return Optional.ofNullable(limbo);
191198
}
192-
}
199+
200+
/**
201+
* Saves the entity UUIDs of in-flight ender pearls to disk so they can be restored
202+
* to the player on reconnect (stasis chamber support). Called when an authenticated
203+
* player quits with pearls in flight.
204+
*
205+
* @param player the authenticated player who is quitting
206+
* @param pearlUuids entity UUIDs of the player's in-flight ender pearls
207+
*/
208+
public void saveEnderPearlsForPlayer(Player player, Set<UUID> pearlUuids) {
209+
LimboPlayer limbo = persistence.getLimboPlayer(player);
210+
if (limbo == null) {
211+
limbo = new LimboPlayer(null, player.isOp(), Collections.emptyList(), player.getAllowFlight(),
212+
player.getWalkSpeed(), player.getFlySpeed());
213+
}
214+
limbo.setEnderPearlUuids(pearlUuids);
215+
persistence.saveLimboPlayer(player, limbo);
216+
logger.debug("Saved {0} ender pearl(s) to disk for `{1}`",
217+
pearlUuids.size(), player.getName().toLowerCase(Locale.ROOT));
218+
}
219+
220+
/**
221+
* Saves the vehicle the player was riding to disk so they can be remounted on reconnect.
222+
* Called when an authenticated player quits while inside a vehicle.
223+
*
224+
* @param player the authenticated player who is quitting
225+
* @param vehicleUuid the entity UUID of the vehicle
226+
* @param vehicleType the entity type of the vehicle
227+
*/
228+
public void saveVehicleForPlayer(Player player, UUID vehicleUuid, EntityType vehicleType) {
229+
LimboPlayer limbo = persistence.getLimboPlayer(player);
230+
if (limbo == null) {
231+
limbo = new LimboPlayer(null, player.isOp(), Collections.emptyList(), player.getAllowFlight(),
232+
player.getWalkSpeed(), player.getFlySpeed());
233+
}
234+
limbo.setVehicle(vehicleUuid, vehicleType);
235+
persistence.saveLimboPlayer(player, limbo);
236+
logger.debug("Saved vehicle {0} ({1}) to disk for `{2}`",
237+
vehicleUuid, vehicleType, player.getName().toLowerCase(Locale.ROOT));
238+
}
239+
240+
/**
241+
* Restores ender pearl shooters and remounts the player's vehicle in a single world pass.
242+
* Must be called after {@link #createLimboPlayer} so the LimboPlayer is guaranteed to
243+
* exist in memory.
244+
*
245+
* @param player the player whose entities should be restored
246+
*/
247+
public void restoreEntities(Player player) {
248+
String name = player.getName().toLowerCase(Locale.ROOT);
249+
LimboPlayer limbo = entries.get(name);
250+
if (limbo == null) {
251+
return;
252+
}
253+
Set<UUID> pearlUuids = limbo.getEnderPearlUuids();
254+
UUID vehicleUuid = limbo.getVehicleUuid();
255+
EntityType vehicleType = limbo.getVehicleType();
256+
257+
if (pearlUuids.isEmpty() && vehicleUuid == null) {
258+
return;
259+
}
260+
261+
int pearlTotal = pearlUuids.size();
262+
int pearlRestored = 0;
263+
boolean vehicleRestored = false;
264+
265+
outer:
266+
for (World world : player.getServer().getWorlds()) {
267+
for (Entity entity : world.getEntities()) {
268+
if (!pearlUuids.isEmpty() && entity instanceof EnderPearl
269+
&& pearlUuids.contains(entity.getUniqueId())) {
270+
((EnderPearl) entity).setShooter(player);
271+
pearlRestored++;
272+
} else if (vehicleUuid != null && !vehicleRestored
273+
&& vehicleUuid.equals(entity.getUniqueId())) {
274+
entity.addPassenger(player);
275+
vehicleRestored = true;
276+
}
277+
if (pearlUuids.isEmpty() && vehicleRestored) {
278+
break outer;
279+
}
280+
}
281+
}
282+
283+
pearlUuids.clear();
284+
limbo.setVehicle(null, null);
285+
286+
if (pearlTotal > 0) {
287+
logger.debug("Restored {0}/{1} ender pearl(s) for `{2}`", pearlRestored, pearlTotal, name);
288+
}
289+
if (vehicleUuid != null) {
290+
if (vehicleRestored) {
291+
logger.debug("Restored vehicle ({0}) for `{1}`", vehicleType, name);
292+
} else {
293+
logger.debug("Vehicle ({0}) no longer exists for `{1}`, skipping", vehicleType, name);
294+
}
295+
}
296+
}
297+
}

src/main/java/fr/xephi/authme/data/limbo/LimboServiceHelper.java

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@
77
import fr.xephi.authme.settings.properties.LimboSettings;
88
import fr.xephi.authme.settings.properties.RestrictionSettings;
99
import org.bukkit.Location;
10+
import org.bukkit.entity.EntityType;
1011
import org.bukkit.entity.Player;
1112

1213
import javax.inject.Inject;
1314
import java.util.Collection;
1415
import java.util.Collections;
16+
import java.util.HashSet;
1517
import java.util.List;
18+
import java.util.Set;
19+
import java.util.UUID;
1620

1721
import static fr.xephi.authme.util.Utils.isCollectionEmpty;
1822
import static java.util.stream.Collectors.toList;
@@ -78,7 +82,7 @@ void revokeLimboStates(Player player) {
7882
* Merges two existing LimboPlayer instances of a player. Merging is done the following way:
7983
* <ul>
8084
* <li><code>isOperator, allowFlight</code>: true if either limbo has true</li>
81-
* <li><code>flySpeed, walkSpeed</code>: maximum value of either limbo player</li>
85+
* <li><code>flySpeed, walkSpeed</code>: value from new limbo (most recent reading of the player's actual speed)</li>
8286
* <li><code>groups, location</code>: from old limbo if not empty/null, otherwise from new limbo</li>
8387
* </ul>
8488
*
@@ -95,12 +99,21 @@ LimboPlayer merge(LimboPlayer newLimbo, LimboPlayer oldLimbo) {
9599

96100
boolean isOperator = newLimbo.isOperator() || oldLimbo.isOperator();
97101
boolean canFly = newLimbo.isCanFly() || oldLimbo.isCanFly();
98-
float flySpeed = Math.max(newLimbo.getFlySpeed(), oldLimbo.getFlySpeed());
99-
float walkSpeed = Math.max(newLimbo.getWalkSpeed(), oldLimbo.getWalkSpeed());
102+
float flySpeed = newLimbo.getFlySpeed();
103+
float walkSpeed = newLimbo.getWalkSpeed();
100104
Collection<UserGroup> groups = getLimboGroups(oldLimbo.getGroups(), newLimbo.getGroups());
101105
Location location = firstNotNull(oldLimbo.getLocation(), newLimbo.getLocation());
102106

103-
return new LimboPlayer(location, isOperator, groups, canFly, walkSpeed, flySpeed);
107+
LimboPlayer merged = new LimboPlayer(location, isOperator, groups, canFly, walkSpeed, flySpeed);
108+
Set<UUID> mergedPearls = new HashSet<>(newLimbo.getEnderPearlUuids());
109+
mergedPearls.addAll(oldLimbo.getEnderPearlUuids());
110+
merged.setEnderPearlUuids(mergedPearls);
111+
112+
UUID vehicleUuid = oldLimbo.getVehicleUuid() != null ? oldLimbo.getVehicleUuid() : newLimbo.getVehicleUuid();
113+
EntityType vehicleType = oldLimbo.getVehicleType() != null ? oldLimbo.getVehicleType() : newLimbo.getVehicleType();
114+
merged.setVehicle(vehicleUuid, vehicleType);
115+
116+
return merged;
104117
}
105118

106119
private static Location firstNotNull(Location first, Location second) {

src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPlayerDeserializer.java

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,21 @@
1212
import fr.xephi.authme.service.BukkitService;
1313
import org.bukkit.Location;
1414
import org.bukkit.World;
15+
import org.bukkit.entity.EntityType;
1516

1617
import java.lang.reflect.Type;
1718
import java.util.ArrayList;
1819
import java.util.Collection;
1920
import java.util.Collections;
21+
import java.util.HashSet;
2022
import java.util.List;
2123
import java.util.Map;
24+
import java.util.Set;
25+
import java.util.UUID;
2226
import java.util.function.Function;
2327

2428
import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.CAN_FLY;
29+
import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.ENDER_PEARLS;
2530
import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.FLY_SPEED;
2631
import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.GROUPS;
2732
import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.IS_OP;
@@ -32,6 +37,8 @@
3237
import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOC_Y;
3338
import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOC_YAW;
3439
import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOC_Z;
40+
import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.VEHICLE_TYPE;
41+
import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.VEHICLE_UUID;
3542
import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.WALK_SPEED;
3643
import static java.util.Optional.ofNullable;
3744

@@ -65,7 +72,32 @@ public LimboPlayer deserialize(JsonElement jsonElement, Type type, JsonDeseriali
6572
float walkSpeed = getFloat(jsonObject, WALK_SPEED, LimboPlayer.DEFAULT_WALK_SPEED);
6673
float flySpeed = getFloat(jsonObject, FLY_SPEED, LimboPlayer.DEFAULT_FLY_SPEED);
6774

68-
return new LimboPlayer(loc, operator, groups, canFly, walkSpeed, flySpeed);
75+
LimboPlayer limboPlayer = new LimboPlayer(loc, operator, groups, canFly, walkSpeed, flySpeed);
76+
77+
JsonElement pearlsElement = jsonObject.get(ENDER_PEARLS);
78+
if (pearlsElement != null && pearlsElement.isJsonArray()) {
79+
Set<UUID> pearlUuids = new HashSet<>();
80+
for (JsonElement pearlElement : pearlsElement.getAsJsonArray()) {
81+
try {
82+
pearlUuids.add(UUID.fromString(pearlElement.getAsString()));
83+
} catch (IllegalArgumentException ignored) {
84+
}
85+
}
86+
limboPlayer.setEnderPearlUuids(pearlUuids);
87+
}
88+
89+
JsonElement vehicleUuidElement = jsonObject.get(VEHICLE_UUID);
90+
JsonElement vehicleTypeElement = jsonObject.get(VEHICLE_TYPE);
91+
if (vehicleUuidElement != null && vehicleTypeElement != null) {
92+
try {
93+
UUID vehicleUuid = UUID.fromString(vehicleUuidElement.getAsString());
94+
EntityType vehicleType = EntityType.valueOf(vehicleTypeElement.getAsString());
95+
limboPlayer.setVehicle(vehicleUuid, vehicleType);
96+
} catch (IllegalArgumentException ignored) {
97+
}
98+
}
99+
100+
return limboPlayer;
69101
}
70102

71103
private Location deserializeLocation(JsonObject jsonObject) {

src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPlayerSerializer.java

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
import java.lang.reflect.Type;
1313
import java.util.List;
14+
import java.util.Set;
15+
import java.util.UUID;
1416
import java.util.stream.Collectors;
1517

1618
/**
@@ -31,23 +33,28 @@ class LimboPlayerSerializer implements JsonSerializer<LimboPlayer> {
3133
static final String CAN_FLY = "can-fly";
3234
static final String WALK_SPEED = "walk-speed";
3335
static final String FLY_SPEED = "fly-speed";
36+
static final String ENDER_PEARLS = "ender-pearls";
37+
static final String VEHICLE_UUID = "vehicle-uuid";
38+
static final String VEHICLE_TYPE = "vehicle-type";
3439

3540
private static final Gson GSON = new Gson();
3641

3742

3843
@Override
3944
public JsonElement serialize(LimboPlayer limboPlayer, Type type, JsonSerializationContext context) {
40-
Location loc = limboPlayer.getLocation();
41-
JsonObject locationObject = new JsonObject();
42-
locationObject.addProperty(LOC_WORLD, loc.getWorld().getName());
43-
locationObject.addProperty(LOC_X, loc.getX());
44-
locationObject.addProperty(LOC_Y, loc.getY());
45-
locationObject.addProperty(LOC_Z, loc.getZ());
46-
locationObject.addProperty(LOC_YAW, loc.getYaw());
47-
locationObject.addProperty(LOC_PITCH, loc.getPitch());
48-
4945
JsonObject obj = new JsonObject();
50-
obj.add(LOCATION, locationObject);
46+
47+
Location loc = limboPlayer.getLocation();
48+
if (loc != null && loc.getWorld() != null) {
49+
JsonObject locationObject = new JsonObject();
50+
locationObject.addProperty(LOC_WORLD, loc.getWorld().getName());
51+
locationObject.addProperty(LOC_X, loc.getX());
52+
locationObject.addProperty(LOC_Y, loc.getY());
53+
locationObject.addProperty(LOC_Z, loc.getZ());
54+
locationObject.addProperty(LOC_YAW, loc.getYaw());
55+
locationObject.addProperty(LOC_PITCH, loc.getPitch());
56+
obj.add(LOCATION, locationObject);
57+
}
5158

5259
List<JsonObject> groups = limboPlayer.getGroups().stream().map(g -> {
5360
JsonObject jsonGroup = new JsonObject();
@@ -66,6 +73,21 @@ public JsonElement serialize(LimboPlayer limboPlayer, Type type, JsonSerializati
6673
obj.addProperty(CAN_FLY, limboPlayer.isCanFly());
6774
obj.addProperty(WALK_SPEED, limboPlayer.getWalkSpeed());
6875
obj.addProperty(FLY_SPEED, limboPlayer.getFlySpeed());
76+
77+
Set<UUID> pearlUuids = limboPlayer.getEnderPearlUuids();
78+
if (!pearlUuids.isEmpty()) {
79+
JsonArray pearlArray = new JsonArray();
80+
for (UUID uuid : pearlUuids) {
81+
pearlArray.add(uuid.toString());
82+
}
83+
obj.add(ENDER_PEARLS, pearlArray);
84+
}
85+
86+
if (limboPlayer.getVehicleUuid() != null && limboPlayer.getVehicleType() != null) {
87+
obj.addProperty(VEHICLE_UUID, limboPlayer.getVehicleUuid().toString());
88+
obj.addProperty(VEHICLE_TYPE, limboPlayer.getVehicleType().name());
89+
}
90+
6991
return obj;
7092
}
7193
}

0 commit comments

Comments
 (0)