Skip to content

Commit ec78da5

Browse files
committed
Fix spectator camera implementation
There are several problems that were solved: 1. Spectating failed to follow players through cross-region/cross-world teleporting. 2. Inability to set camera to off-region entity 3. Various crashes involved with off-region camera entity 4. Spectator does not always follow entities through portals We fix #1 by sending ClientboundSetCameraPacket when the spectated entity becomes in tracked, as the client appears to be unable to correctly handle when the camera entity is removed, even temporarily. We fix #2 by allowing off-region entities that are not dead. However, this required the teleportation code in both setCamera and the ServerPlayer#tick method to be modified to correctly handle off-region camera targets. We fix #3 by correctly teleporting to off-region locations when needed. We fix #4 by following the Bukkit entity. This allows spectator mode to properly follow non-player entities through portals. Issues #1 (cross-world) and #4 are probably present in Paper/Vanilla, however solving #2 and #3 required a solution for these. Fixes #324
1 parent e7bb50e commit ec78da5

File tree

4 files changed

+128
-8
lines changed

4 files changed

+128
-8
lines changed

folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/TeleportUtils.java.patch

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
--- /dev/null
22
+++ b/io/papermc/paper/threadedregions/TeleportUtils.java
3-
@@ -1,0 +_,70 @@
3+
@@ -1,0 +_,82 @@
44
+package io.papermc.paper.threadedregions;
55
+
66
+import ca.spottedleaf.concurrentutil.completable.CallbackCompletable;
@@ -13,8 +13,14 @@
1313
+
1414
+public final class TeleportUtils {
1515
+
16-
+ public static void teleport(final Entity from, final boolean useFromRootVehicle, final Entity to, final Float yaw, final Float pitch,
17-
+ final long teleportFlags, final PlayerTeleportEvent.TeleportCause cause, final Consumer<Entity> onComplete) {
16+
+ public static <T extends Entity> void teleport(final T from, final boolean useFromRootVehicle, final Entity to, final Float yaw, final Float pitch,
17+
+ final long teleportFlags, final PlayerTeleportEvent.TeleportCause cause, final Consumer<Entity> onComplete) {
18+
+ teleport(from, useFromRootVehicle, to, yaw, pitch, teleportFlags, cause, onComplete, null);
19+
+ }
20+
+
21+
+ public static <T extends Entity> void teleport(final T from, final boolean useFromRootVehicle, final Entity to, final Float yaw, final Float pitch,
22+
+ final long teleportFlags, final PlayerTeleportEvent.TeleportCause cause, final Consumer<Entity> onComplete,
23+
+ final java.util.function.Predicate<T> preTeleport) {
1824
+ // retrieve coordinates
1925
+ final CallbackCompletable<Location> positionCompletable = new CallbackCompletable<>();
2026
+
@@ -27,10 +33,16 @@
2733
+ return;
2834
+ }
2935
+ final boolean scheduled = from.getBukkitEntity().taskScheduler.schedule(
30-
+ (final Entity realFrom) -> {
36+
+ (final T realFrom) -> {
3137
+ final Vec3 pos = new Vec3(
3238
+ loc.getX(), loc.getY(), loc.getZ()
3339
+ );
40+
+ if (preTeleport != null && !preTeleport.test(realFrom)) {
41+
+ if (onComplete != null) {
42+
+ onComplete.accept(null);
43+
+ }
44+
+ return;
45+
+ }
3446
+ (useFromRootVehicle ? realFrom.getRootVehicle() : realFrom).teleportAsync(
3547
+ ((CraftWorld)loc.getWorld()).getHandle(), pos, null, null, null,
3648
+ cause, teleportFlags, onComplete

folia-server/minecraft-patches/sources/net/minecraft/server/level/ServerPlayer.java.patch

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,31 @@
188188
} else {
189189
LOGGER.warn("Failed to spawn player ender pearl in level ({}), skipping", optional.get());
190190
}
191+
@@ -817,12 +_,23 @@
192+
193+
Entity camera = this.getCamera();
194+
if (camera != this) {
195+
- if (camera.isAlive()) {
196+
+ if (camera.canBeSpectated()) { // Folia - region threading - replace removed check
197+
+ if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(camera) && !camera.isRemoved()) { // Folia - region threading
198+
this.absMoveTo(camera.getX(), camera.getY(), camera.getZ(), camera.getYRot(), camera.getXRot());
199+
this.serverLevel().getChunkSource().move(this);
200+
if (this.wantsToStopRiding()) {
201+
this.setCamera(this);
202+
}
203+
+ } else { // Folia start - region threading
204+
+ Entity realCamera = camera.getBukkitEntity().getHandleRaw();
205+
+ if (realCamera != camera) {
206+
+ this.setCamera(this);
207+
+ this.setCamera(realCamera);
208+
+ } else {
209+
+ this.teleportToCameraOffRegion();
210+
+ }
211+
+ }
212+
+ // Folia end - region threading
213+
} else {
214+
this.setCamera(this);
215+
}
191216
@@ -1357,9 +_,332 @@
192217
}
193218
}
@@ -521,18 +546,66 @@
521546
if (this.isSleeping()) return null; // CraftBukkit - SPIGOT-3154
522547
if (this.isRemoved()) {
523548
return null;
524-
@@ -2398,6 +_,11 @@
549+
@@ -2397,7 +_,30 @@
550+
return (Entity)(this.camera == null ? this : this.camera);
525551
}
526552

553+
+ // Folia start - region threading
554+
+ private void teleportToCameraOffRegion() {
555+
+ Entity cameraFinal = this.camera;
556+
+ // use the task scheduler, as we don't know where the caller is invoking from
557+
+ if (this != cameraFinal) {
558+
+ this.getBukkitEntity().taskScheduler.schedule((final ServerPlayer newPlayer) -> {
559+
+ io.papermc.paper.threadedregions.TeleportUtils.teleport(
560+
+ newPlayer, false, cameraFinal, null, null, 0L,
561+
+ org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.SPECTATE, null,
562+
+ (final ServerPlayer newerPlayer) -> {
563+
+ return newerPlayer.camera == cameraFinal;
564+
+ }
565+
+ );
566+
+ }, null, 1L);
567+
+ } // else: do not bother teleporting to self
568+
+ }
569+
+ // Folia end - region threading
570+
+
527571
public void setCamera(@Nullable Entity entityToSpectate) {
528572
+ // Folia start - region threading
529-
+ if (entityToSpectate != null && !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entityToSpectate)) {
573+
+ if (entityToSpectate != null && (entityToSpectate != this && !entityToSpectate.canBeSpectated())) {
530574
+ return;
531575
+ }
532576
+ // Folia end - region threading
533577
Entity camera = this.getCamera();
534578
this.camera = (Entity)(entityToSpectate == null ? this : entityToSpectate);
535579
if (camera != this.camera) {
580+
@@ -2416,16 +_,19 @@
581+
}
582+
}
583+
// Paper end - Add PlayerStartSpectatingEntityEvent and PlayerStopSpectatingEntity
584+
- if (this.camera.level() instanceof ServerLevel serverLevel) {
585+
- this.teleportTo(serverLevel, this.camera.getX(), this.camera.getY(), this.camera.getZ(), Set.of(), this.getYRot(), this.getXRot(), false, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.SPECTATE); // CraftBukkit
586+
- }
587+
-
588+
- if (entityToSpectate != null) {
589+
- this.serverLevel().getChunkSource().move(this);
590+
- }
591+
-
592+
+ // Folia - region threading - move down
593+
+
594+
+ // Folia - region threading - not needed
595+
+
596+
+ // Folia start - region threading - handle camera setting better
597+
+ if (this.camera == this
598+
+ || (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.camera) && this.camera.moonrise$getTrackedEntity() != null
599+
+ && this.camera.moonrise$getTrackedEntity().seenBy.contains(this.connection))) {
600+
+ // Folia end - region threading - handle camera setting better
601+
this.connection.send(new ClientboundSetCameraPacket(this.camera));
602+
- this.connection.resetPosition();
603+
+ } // Folia - region threading - handle camera setting better
604+
+ //this.connection.resetPosition(); // Folia - region threading - not needed
605+
+ this.teleportToCameraOffRegion(); // Folia - region threading - moved down
606+
}
607+
}
608+
536609
@@ -2896,11 +_,11 @@
537610
}
538611

folia-server/minecraft-patches/sources/net/minecraft/world/entity/Entity.java.patch

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
private EntityDimensions dimensions;
2828
private float eyeHeight;
2929
public boolean isInPowderSnow;
30-
@@ -525,6 +_,19 @@
30+
@@ -525,6 +_,23 @@
3131
}
3232
}
3333
// Paper end - optimise entity tracker
@@ -43,6 +43,10 @@
4343
+ this.pistonDeltasGameTime += fromRedstoneTimeOffset;
4444
+ }
4545
+ }
46+
+
47+
+ public boolean canBeSpectated() {
48+
+ return !this.getBukkitEntity().taskScheduler.isRetired();
49+
+ }
4650
+ // Folia end - region ticking
4751

4852
public Entity(EntityType<?> entityType, Level level) {
@@ -992,6 +996,32 @@
992996
protected void removeAfterChangingDimensions() {
993997
this.setRemoved(Entity.RemovalReason.CHANGED_DIMENSION, null); // CraftBukkit - add Bukkit remove cause
994998
if (this instanceof Leashable leashable && leashable.isLeashed()) { // Paper - only call if it is leashed
999+
@@ -4246,6 +_,12 @@
1000+
}
1001+
1002+
public void startSeenByPlayer(ServerPlayer serverPlayer) {
1003+
+ // Folia start - region threading
1004+
+ if (serverPlayer.getCamera() == this) {
1005+
+ // set camera again
1006+
+ serverPlayer.connection.send(new net.minecraft.network.protocol.game.ClientboundSetCameraPacket(this));
1007+
+ }
1008+
+ // Folia end - region threading
1009+
}
1010+
1011+
public void stopSeenByPlayer(ServerPlayer serverPlayer) {
1012+
@@ -4255,6 +_,12 @@
1013+
new io.papermc.paper.event.player.PlayerUntrackEntityEvent(serverPlayer.getBukkitEntity(), this.getBukkitEntity()).callEvent();
1014+
}
1015+
// Paper end - entity tracking events
1016+
+ // Folia start - region threading
1017+
+ if (serverPlayer.getCamera() == this) {
1018+
+ // unset camera, the player tick method should TP us close enough again to invoke startSeenByPlayer
1019+
+ serverPlayer.connection.send(new net.minecraft.network.protocol.game.ClientboundSetCameraPacket(serverPlayer));
1020+
+ }
1021+
+ // Folia end - region threading
1022+
}
1023+
1024+
public float rotate(Rotation transformRotation) {
9951025
@@ -4790,7 +_,8 @@
9961026
}
9971027
}

folia-server/minecraft-patches/sources/net/minecraft/world/entity/LivingEntity.java.patch

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
protected int autoSpinAttackTicks;
1010
protected float autoSpinAttackDmg;
1111
@Nullable
12-
@@ -307,6 +_,21 @@
12+
@@ -307,6 +_,26 @@
1313
return this.getYHeadRot();
1414
}
1515
// CraftBukkit end
@@ -23,6 +23,11 @@
2323
+ }
2424
+
2525
+ @Override
26+
+ public boolean canBeSpectated() {
27+
+ return super.canBeSpectated() && this.getHealth() > 0.0F;
28+
+ }
29+
+
30+
+ @Override
2631
+ protected void resetStoredPositions() {
2732
+ super.resetStoredPositions();
2833
+ this.lastClimbablePos = Optional.empty();

0 commit comments

Comments
 (0)