Skip to content

Commit 72e660c

Browse files
authored
Merge pull request #3 from CaveNightingale/sand-duper-fix
Fix the bug that handed item disappeared when teleporting via nether portal.
2 parents d9218ec + ff3c9a9 commit 72e660c

6 files changed

Lines changed: 236 additions & 25 deletions

File tree

Lines changed: 161 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,92 @@
11
package wearblackallday.dimthread.mixin;
22

3+
import net.minecraft.block.BlockState;
34
import net.minecraft.entity.Entity;
4-
import net.minecraft.entity.FallingBlockEntity;
5+
import net.minecraft.entity.EntityDimensions;
6+
import net.minecraft.entity.EntityPose;
7+
import net.minecraft.nbt.NbtCompound;
8+
import net.minecraft.server.MinecraftServer;
59
import net.minecraft.server.world.ServerWorld;
10+
import net.minecraft.state.property.Properties;
11+
import net.minecraft.util.math.BlockPos;
12+
import net.minecraft.util.math.Direction;
13+
import net.minecraft.util.math.Vec3d;
14+
import net.minecraft.world.BlockLocating;
15+
import net.minecraft.world.Heightmap;
16+
import net.minecraft.world.TeleportTarget;
17+
import net.minecraft.world.World;
18+
import net.minecraft.world.border.WorldBorder;
19+
import net.minecraft.world.dimension.AreaHelper;
20+
import net.minecraft.world.dimension.DimensionType;
21+
import org.jetbrains.annotations.NotNull;
22+
import org.jetbrains.annotations.Nullable;
23+
import org.slf4j.Logger;
624
import org.spongepowered.asm.mixin.Final;
725
import org.spongepowered.asm.mixin.Mixin;
826
import org.spongepowered.asm.mixin.Shadow;
927
import org.spongepowered.asm.mixin.injection.At;
1028
import org.spongepowered.asm.mixin.injection.Inject;
29+
import org.spongepowered.asm.mixin.injection.Redirect;
1130
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
1231
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
1332
import wearblackallday.dimthread.DimThread;
33+
import wearblackallday.dimthread.util.UncompletedTeleportTarget;
1434

15-
import org.slf4j.LoggerFactory;
35+
import java.util.Optional;
1636

1737
@Mixin(Entity.class)
18-
public abstract class EntityMixin implements Cloneable {
38+
public abstract class EntityMixin {
1939

20-
public boolean isCloned = false;
21-
22-
@Shadow @Final
23-
abstract void setRemoved(Entity.RemovalReason reason);
40+
private NbtCompound nbtCachedForMoveToWorld;
2441

42+
private UncompletedTeleportTarget uncompletedTeleportTargetForMoveToWorld;
2543
@Shadow
26-
abstract void removeFromDimension();
44+
protected abstract void removeFromDimension();
45+
46+
47+
@Shadow public abstract World getWorld();
48+
49+
@Shadow public abstract NbtCompound writeNbt(NbtCompound nbt);
50+
51+
@Shadow public abstract @Nullable Entity moveToWorld(ServerWorld destination);
52+
53+
@Shadow private int netherPortalCooldown;
54+
55+
@Shadow protected BlockPos lastNetherPortalPosition;
56+
57+
@Shadow public abstract boolean isRemoved();
58+
59+
@Shadow @Nullable public abstract MinecraftServer getServer();
60+
61+
@Shadow public World world;
62+
63+
@Shadow public abstract double getX();
64+
65+
@Shadow public abstract double getY();
66+
67+
@Shadow public abstract double getZ();
68+
69+
@Shadow protected abstract Optional<BlockLocating.Rectangle> getPortalRect(ServerWorld destWorld, BlockPos destPos, boolean destIsNether, WorldBorder worldBorder);
70+
71+
@Shadow protected abstract Vec3d positionInPortal(Direction.Axis portalAxis, BlockLocating.Rectangle portalRect);
72+
73+
@Shadow public abstract EntityDimensions getDimensions(EntityPose pose);
74+
75+
@Shadow public abstract EntityPose getPose();
76+
77+
@Shadow public abstract Vec3d getVelocity();
78+
79+
@Shadow public abstract float getYaw();
80+
81+
@Shadow public abstract float getPitch();
82+
83+
@Shadow protected abstract @Nullable TeleportTarget getTeleportTarget(ServerWorld destination);
84+
85+
@Shadow protected abstract void unsetRemoved();
86+
87+
@Shadow public abstract void readNbt(NbtCompound nbt);
88+
89+
@Shadow @Final private static Logger LOGGER;
2790

2891
/**
2992
* Schedules moving entities between dimensions to the server thread. Once all
@@ -35,40 +98,113 @@ public abstract class EntityMixin implements Cloneable {
3598
* another thread will cause a deadlock in the server chunk manager.
3699
*/
37100
@Inject(method = "moveToWorld", at = @At("HEAD"), cancellable = true)
38-
public void moveToWorld(ServerWorld destination, CallbackInfoReturnable<Entity> ci) {
101+
public void onMoveToWorld(ServerWorld destination, CallbackInfoReturnable<Entity> ci) {
39102
if (!DimThread.MANAGER.isActive(destination.getServer()))
40103
return;
41104

42105
if (DimThread.owns(Thread.currentThread())) {
43-
Entity snapshot = null;
44-
try {
45-
snapshot = (Entity) (this.clone());
46-
} catch (CloneNotSupportedException e) {
47-
throw new RuntimeException(e);
48-
}
49-
final Entity finalSnapshot = snapshot;
106+
nbtCachedForMoveToWorld = writeNbt(new NbtCompound());
107+
uncompletedTeleportTargetForMoveToWorld = createTeleportTargetUncompleted(destination);
50108
destination.getServer().execute(
51-
() -> finalSnapshot.moveToWorld(destination)
109+
() -> {
110+
Entity entity = this.moveToWorld(destination);
111+
if(entity == null) {
112+
this.unsetRemoved();
113+
nbtCachedForMoveToWorld.putInt("PortalCooldown", this.netherPortalCooldown);
114+
this.readNbt(nbtCachedForMoveToWorld);
115+
this.uncompletedTeleportTargetForMoveToWorld = null;
116+
this.nbtCachedForMoveToWorld = null;
117+
this.world.spawnEntity((Entity) (Object) this);// if the teleporting failed, we need to add it back to the world
118+
LOGGER.debug("Failed to teleport {}, return it to its previous world", this);
119+
}
120+
}
52121
);
53122
this.removeFromDimension();
54123
ci.setReturnValue(null);
55124
}
56125
}
57126

58127
/**
59-
* @author xiaoyu2006
60-
* @reason If this is a cloned entity, it should not execute removeFromDimension.
128+
* Perform deep copy instead of clone() to fix the bug that handed item disappear while teleporting
129+
*/
130+
@Redirect(method = "moveToWorld", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;copyFrom(Lnet/minecraft/entity/Entity;)V"))
131+
private void onMoveToWorldCopyFrom(Entity instance, Entity original) {
132+
if(DimThread.MANAGER.isActive(getServer())) {
133+
NbtCompound nbtCompound = ((EntityMixin) (Object) original).nbtCachedForMoveToWorld;
134+
nbtCompound.remove("Dimension");
135+
instance.readNbt(nbtCompound);
136+
((EntityMixin) (Object) instance).netherPortalCooldown = ((EntityMixin) (Object) original).netherPortalCooldown;
137+
((EntityMixin) (Object) instance).lastNetherPortalPosition = ((EntityMixin) (Object) original).lastNetherPortalPosition;
138+
} else {
139+
instance.copyFrom(original);
140+
}
141+
}
142+
143+
/**
144+
* We have to use the data we cached when moveToWorld is called
145+
* It's getting modified later
61146
*/
147+
@Redirect(method = "moveToWorld", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;getTeleportTarget(Lnet/minecraft/server/world/ServerWorld;)Lnet/minecraft/world/TeleportTarget;"))
148+
private TeleportTarget onMoveToWorldGetTeleportTarget(@NotNull Entity instance, ServerWorld destination) {
149+
EntityMixin ins = ((EntityMixin) (Object) instance);
150+
if(DimThread.MANAGER.isActive(getServer())) {
151+
return ins.uncompletedTeleportTargetForMoveToWorld == null ? null : ins.uncompletedTeleportTargetForMoveToWorld.complete(destination);
152+
} else {
153+
return ins.getTeleportTarget(destination);
154+
}
155+
}
156+
62157
@Inject(method = "removeFromDimension", at = @At("HEAD"), cancellable = true)
63-
public void clonedDoNotRemove(CallbackInfo ci) {
64-
if (this.isCloned) {
158+
private void onRemoveFromDimension(CallbackInfo ci) {
159+
if(isRemoved()) {
65160
ci.cancel();
66161
}
67162
}
68163

69-
protected Object clone() throws CloneNotSupportedException {
70-
EntityMixin cloned = (EntityMixin) super.clone();
71-
cloned.isCloned = true;
72-
return cloned;
164+
/**
165+
* We check this because when we call this method in getServer().execute(), the entity has already been removed
166+
*/
167+
@Redirect(method = "moveToWorld", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;isRemoved()Z"))
168+
private boolean onMoveToWorldIsRemoved(Entity instance) {
169+
return instance.isRemoved() && ((EntityMixin) (Object) instance).nbtCachedForMoveToWorld == null;
170+
}
171+
172+
/**
173+
* take a snapshot of some values so that codes modified it later doesn't affect teleporting
174+
*/
175+
private UncompletedTeleportTarget createTeleportTargetUncompleted(ServerWorld dest) {
176+
boolean isEndReturnPortal = world.getRegistryKey() == World.END && dest.getRegistryKey() == World.OVERWORLD;
177+
boolean isEndPortal = dest.getRegistryKey() == World.END;
178+
Vec3d velocity = getVelocity();
179+
float yaw = getYaw(), pitch = getPitch();
180+
if(!isEndPortal && !isEndReturnPortal) {
181+
boolean isNetherPortal = dest.getRegistryKey() == World.NETHER;
182+
boolean isNetherReturnPortal = world.getRegistryKey() == World.NETHER;
183+
if(!isNetherPortal && !isNetherReturnPortal) {
184+
return dest1 -> null;
185+
} else {
186+
WorldBorder border = dest.getWorldBorder();
187+
double scale = DimensionType.getCoordinateScaleFactor(world.getDimension(), dest.getDimension());
188+
BlockPos target = border.clamp(getX() * scale, getY(), getZ() * scale);
189+
BlockState portalState = world.getBlockState(lastNetherPortalPosition);
190+
Direction.Axis axis;
191+
Vec3d vec3d;
192+
EntityDimensions dimensions = getDimensions(getPose());
193+
if (portalState.contains(Properties.HORIZONTAL_AXIS)) {
194+
axis = portalState.get(Properties.HORIZONTAL_AXIS);
195+
BlockLocating.Rectangle rectangle = BlockLocating.getLargestRectangle(this.lastNetherPortalPosition, axis, 21, Direction.Axis.Y, 21, (blockPos) -> this.world.getBlockState(blockPos) == portalState);
196+
vec3d = this.positionInPortal(axis, rectangle);
197+
} else {
198+
axis = Direction.Axis.X;
199+
vec3d = new Vec3d(0.5, 0.0, 0.0);
200+
}
201+
return dest1 -> getPortalRect(dest1, target, isNetherPortal, border).map((rect) -> AreaHelper.getNetherTeleportTarget(dest1, rect, axis, vec3d, dimensions, velocity, yaw, pitch)).orElse(null);
202+
}
203+
} else {
204+
return dest1 -> {
205+
BlockPos target = isEndPortal ? ServerWorld.END_SPAWN_POS : dest1.getTopPosition(Heightmap.Type.MOTION_BLOCKING_NO_LEAVES, dest1.getSpawnPos());
206+
return new TeleportTarget(new Vec3d(target.getX() + 0.5, target.getY(), target.getZ() + 0.5), velocity, yaw, pitch);
207+
};
208+
}
73209
}
74210
}

src/main/java/wearblackallday/dimthread/mixin/MinecraftServerMixin.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
1414
import wearblackallday.dimthread.DimThread;
1515
import wearblackallday.dimthread.util.CrashInfo;
16+
import wearblackallday.dimthread.util.ServerWorldAccessor;
1617
import wearblackallday.dimthread.util.ThreadPool;
1718

1819
import java.util.Collections;
@@ -72,6 +73,7 @@ public void tickWorlds(BooleanSupplier shouldKeepTicking, CallbackInfo ci) {
7273
});
7374

7475
pool.awaitCompletion();
76+
getWorlds().forEach(world -> ((ServerWorldAccessor) world).dimthread_tickTime()); // Time ticking is not thread-safe, fix https://github.com/WearBlackAllDay/DimensionalThreading/issues/72
7577

7678
if(crash.get() != null) {
7779
crash.get().crash("Exception ticking world");
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package wearblackallday.dimthread.mixin;
2+
3+
import net.minecraft.server.world.ServerWorld;
4+
import net.minecraft.util.profiler.Profiler;
5+
import net.minecraft.util.registry.RegistryEntry;
6+
import net.minecraft.util.registry.RegistryKey;
7+
import net.minecraft.world.MutableWorldProperties;
8+
import net.minecraft.world.World;
9+
import net.minecraft.world.dimension.DimensionType;
10+
import org.spongepowered.asm.mixin.Mixin;
11+
import org.spongepowered.asm.mixin.Shadow;
12+
import org.spongepowered.asm.mixin.injection.At;
13+
import org.spongepowered.asm.mixin.injection.Inject;
14+
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
15+
import wearblackallday.dimthread.DimThread;
16+
import wearblackallday.dimthread.util.ServerWorldAccessor;
17+
18+
import java.util.function.Supplier;
19+
20+
@Mixin(ServerWorld.class)
21+
public abstract class ServerWorldMixin extends World implements ServerWorldAccessor {
22+
protected ServerWorldMixin(MutableWorldProperties properties, RegistryKey<World> registryRef, RegistryEntry<DimensionType> registryEntry, Supplier<Profiler> profiler, boolean isClient, boolean debugWorld, long seed) {
23+
super(properties, registryRef, registryEntry, profiler, isClient, debugWorld, seed);
24+
}
25+
26+
@Shadow protected abstract void tickTime();
27+
28+
boolean onMainThread = false;
29+
boolean timeTickedOnWorldThread = false;
30+
31+
/**
32+
* Time ticking is not thread-safe. We cancel time ticking from the world thread. However, DimThread will tick time on the main thread
33+
*/
34+
@Inject(method = "tickTime", at = @At("HEAD"), cancellable = true)
35+
private void preventTimeTicking(CallbackInfo ci) {
36+
if (DimThread.MANAGER.isActive(getServer()) && !onMainThread) {
37+
timeTickedOnWorldThread = true;
38+
ci.cancel();
39+
}
40+
}
41+
42+
@Override
43+
public void dimthread_tickTime() {
44+
if (timeTickedOnWorldThread) {
45+
onMainThread = true;
46+
tickTime();
47+
onMainThread = false;
48+
timeTickedOnWorldThread = false;
49+
}
50+
}
51+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package wearblackallday.dimthread.util;
2+
3+
/**
4+
* Create this calass wo that we can call tickTime from ServerWorldMixin
5+
*/
6+
public interface ServerWorldAccessor {
7+
void dimthread_tickTime();
8+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package wearblackallday.dimthread.util;
2+
3+
import net.minecraft.server.world.ServerWorld;
4+
import net.minecraft.world.TeleportTarget;
5+
import org.jetbrains.annotations.Nullable;
6+
7+
/**
8+
* This is used for teleport target which is not completed right now, we are going to complete it in another thread
9+
* For thread-unsafe operations
10+
*/
11+
public interface UncompletedTeleportTarget {
12+
@Nullable TeleportTarget complete(ServerWorld dest);
13+
}

src/main/resources/dimthread.mixins.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"MinecraftServerMixin",
99
"RedstoneWireBlockMixin",
1010
"ServerChunkManagerMixin",
11+
"ServerWorldMixin",
1112
"WorldMixin"
1213
],
1314
"client": [

0 commit comments

Comments
 (0)