mirror of
https://github.com/FabricMC/fabric.git
synced 2025-04-01 01:30:00 -04:00
Update the Dimension API (#1172)
* Reimplement Dimension API
* Forgot to update dimension-api mixins.json
* Did suggested changes.
* Move Nullable import.
* Remove unneeded whitespace
* Added automatic testing testmod feature.
* ...Removed unused imports...
* And other stuff the auto style system complains about...
* ...
* Drop Networking
* Completed suggested changes.
* Revert "Completed suggested changes."
This reverts commit 97740e6134
.
* Did requested changes.
* Forgot colon
* Turn plural to singular
Co-authored-by: i509VCB <git@i509.me>
This commit is contained in:
parent
fddcc0d88f
commit
bbf4c01986
15 changed files with 216 additions and 383 deletions
fabric-dimensions-v1
build.gradle
settings.gradlesrc
main
java/net/fabricmc/fabric
api/dimension/v1
impl/dimension
mixin/dimension
resources
testmod
java/net/fabricmc/fabric/test/dimension
resources/data/fabric_dimension
|
@ -3,10 +3,10 @@ version = getSubprojectVersion(project, "1.0.0")
|
|||
|
||||
dependencies {
|
||||
testmodCompile project(path: ':fabric-command-api-v1', configuration: 'dev')
|
||||
testmodCompile project(path: ':fabric-resource-loader-v0', configuration: 'dev')
|
||||
testmodCompile project(path: ':fabric-lifecycle-events-v1', configuration: 'dev')
|
||||
}
|
||||
|
||||
moduleDependencies(project, [
|
||||
'fabric-api-base',
|
||||
'fabric-networking-v0',
|
||||
'fabric-registry-sync-v0'
|
||||
'fabric-api-base'
|
||||
])
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.fabricmc.fabric.api.dimension.v1;
|
||||
|
||||
import net.minecraft.block.pattern.BlockPattern;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.server.world.ServerWorld;
|
||||
import net.minecraft.util.math.Direction;
|
||||
|
||||
/**
|
||||
* Responsible for placing an Entity once they have entered a dimension.
|
||||
* used in Entity::changeDimension.
|
||||
*
|
||||
* @deprecated Experimental feature, may be removed or changed without further notice due to potential changes to Dimensions in subsequent versions.
|
||||
*
|
||||
* @see FabricDimensions
|
||||
*/
|
||||
@Deprecated
|
||||
@FunctionalInterface
|
||||
public interface EntityPlacer {
|
||||
/**
|
||||
* Handles the placement of an entity going to a dimension.
|
||||
* Utilized by {@link FabricDimensions#teleport(Entity, ServerWorld, EntityPlacer)} to specify placement logic when needed.
|
||||
*
|
||||
* <p>This method may have side effects such as the creation of a portal in the target dimension,
|
||||
* or the creation of a chunk loading ticket.
|
||||
*
|
||||
* @param portalDir the direction the portal is facing, meaningless if no portal was used
|
||||
* @param horizontalOffset the horizontal offset of the entity relative to the front top left corner of the portal, meaningless if no portal was used
|
||||
* @param verticalOffset the vertical offset of the entity relative to the front top left corner of the portal, meaningless if no portal was used
|
||||
* @return a teleportation target, or {@code null} to fall back to further handling
|
||||
* @apiNote When this method is called, the entity's world is its source dimension.
|
||||
*/
|
||||
/* @Nullable */
|
||||
BlockPattern.TeleportTarget placeEntity(Entity teleported, ServerWorld destination, Direction portalDir, double horizontalOffset, double verticalOffset);
|
||||
}
|
|
@ -17,11 +17,11 @@
|
|||
package net.fabricmc.fabric.api.dimension.v1;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.server.world.ServerWorld;
|
||||
import net.minecraft.util.registry.RegistryKey;
|
||||
import net.minecraft.world.World;
|
||||
import net.minecraft.world.TeleportTarget;
|
||||
|
||||
import net.fabricmc.fabric.impl.dimension.FabricDimensionInternals;
|
||||
|
||||
|
@ -37,74 +37,29 @@ public final class FabricDimensions {
|
|||
}
|
||||
|
||||
/**
|
||||
* Teleports an entity to a different dimension, using custom placement logic.
|
||||
* Teleports an entity to a different dimension, placing it at the specified destination.
|
||||
*
|
||||
* <p>This method behaves as if:
|
||||
* <pre>{@code teleported.changeDimension(destination)}</pre>
|
||||
* <p>Using this method will circumvent Vanilla's portal placement code.
|
||||
*
|
||||
* <p>If {@code destination} has a default placer, that placer will be used. If {@code destination} is
|
||||
* the nether or the overworld, the default logic is the vanilla path.
|
||||
* For any other dimension, the default placement behaviour is undefined.
|
||||
* When delegating to a placement logic that uses portals, the entity's {@code lastPortalPosition},
|
||||
* {@code lastPortalDirectionVector}, and {@code lastPortalDirection} fields should be updated
|
||||
* before calling this method.
|
||||
*
|
||||
* <p>After calling this method, {@code teleported} may be invalidated. Callers should use
|
||||
* the returned entity for any further manipulation.
|
||||
* <p>When teleporting to another dimension, the entity may be replaced with a new entity in the target
|
||||
* dimension. This is not the case for players, but needs to be accounted for by the caller.
|
||||
*
|
||||
* @param teleported the entity to teleport
|
||||
* @param destination the dimension the entity will be teleported to
|
||||
* @return the teleported entity, or a clone of it
|
||||
* @see #teleport(Entity, ServerWorld)
|
||||
*/
|
||||
public static <E extends Entity> E teleport(E teleported, ServerWorld destination) {
|
||||
return teleport(teleported, destination, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Teleports an entity to a different dimension, using custom placement logic.
|
||||
*
|
||||
* <p>If {@code customPlacement} is {@code null}, this method behaves as if:
|
||||
* <pre>{@code teleported.changeDimension(destination)}</pre>
|
||||
* The {@code customPlacement} may itself return {@code null}, in which case
|
||||
* the default placement logic for that dimension will be run.
|
||||
*
|
||||
* <p>If {@code destination} has a default placer, that placer will be used. If {@code destination} is
|
||||
* the nether or the overworld, the default logic is the vanilla path.
|
||||
* For any other dimension, the default placement behaviour is undefined.
|
||||
* When delegating to a placement logic that uses portals, the entity's {@code lastPortalPosition},
|
||||
* {@code lastPortalDirectionVector}, and {@code lastPortalDirection} fields should be updated
|
||||
* before calling this method.
|
||||
*
|
||||
* <p>After calling this method, {@code teleported} may be invalidated. Callers should use
|
||||
* the returned entity for any further manipulation.
|
||||
*
|
||||
* @param teleported the entity to teleport
|
||||
* @param destination the dimension the entity will be teleported to
|
||||
* @param customPlacer custom placement logic that will run before the default one,
|
||||
* or {@code null} to use the dimension's default behavior.
|
||||
* @param <E> the type of the teleported entity
|
||||
* @return the teleported entity, or a clone of it
|
||||
* @param target where the entity will be placed in the target world.
|
||||
* As in Vanilla, the target's velocity is not applied to players.
|
||||
* If target is null, the entity will not be teleported.
|
||||
* @param <E> the type of the teleported entity
|
||||
* @return Returns the teleported entity in the target dimension, which may be a new entity or <code>teleported</code>,
|
||||
* depending on the entity type.
|
||||
* @throws IllegalStateException if this method is called on a client entity
|
||||
* @apiNote this method must be called from the main server thread
|
||||
*/
|
||||
public static <E extends Entity> E teleport(E teleported, ServerWorld destination, /*Nullable*/ EntityPlacer customPlacer) {
|
||||
@Nullable
|
||||
public static <E extends Entity> E teleport(E teleported, ServerWorld destination, TeleportTarget target) {
|
||||
Preconditions.checkNotNull(target, "A target must be provided");
|
||||
Preconditions.checkState(!teleported.world.isClient, "Entities can only be teleported on the server side");
|
||||
|
||||
return FabricDimensionInternals.changeDimension(teleported, destination, customPlacer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a default placer for a dimension, this is used when an entity is teleported to a dimension without
|
||||
* a specified {@link EntityPlacer}.
|
||||
*
|
||||
* @param registryKey The dimension {@link RegistryKey}
|
||||
* @param entityPlacer The {@link EntityPlacer}
|
||||
*/
|
||||
public static void registerDefaultPlacer(RegistryKey<World> registryKey, EntityPlacer entityPlacer) {
|
||||
Preconditions.checkState(!FabricDimensionInternals.DEFAULT_PLACERS.containsKey(registryKey), "Only 1 EntityPlacer can be registered per dimension");
|
||||
Preconditions.checkState(!registryKey.getValue().getNamespace().equals("minecraft"), "Minecraft dimensions cannot have a default placer");
|
||||
|
||||
FabricDimensionInternals.DEFAULT_PLACERS.put(registryKey, entityPlacer);
|
||||
return FabricDimensionInternals.changeDimension(teleported, destination, target);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,116 +16,41 @@
|
|||
|
||||
package net.fabricmc.fabric.impl.dimension;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
import net.minecraft.block.pattern.BlockPattern;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.server.world.ServerWorld;
|
||||
import net.minecraft.util.math.Direction;
|
||||
import net.minecraft.util.registry.RegistryKey;
|
||||
import net.minecraft.world.World;
|
||||
import net.minecraft.world.TeleportTarget;
|
||||
|
||||
import net.fabricmc.fabric.api.dimension.v1.EntityPlacer;
|
||||
import net.fabricmc.fabric.api.dimension.v1.FabricDimensions;
|
||||
import net.fabricmc.fabric.mixin.dimension.EntityHooks;
|
||||
|
||||
public final class FabricDimensionInternals {
|
||||
/**
|
||||
* The target passed to the last call to {@link FabricDimensions#teleport(Entity, ServerWorld, TeleportTarget)}.
|
||||
*/
|
||||
private static TeleportTarget currentTarget;
|
||||
|
||||
private FabricDimensionInternals() {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
public static final Map<RegistryKey<World>, EntityPlacer> DEFAULT_PLACERS = new HashMap<>();
|
||||
|
||||
/**
|
||||
* The entity currently being transported to another dimension.
|
||||
* Returns the last target set when a user of the API requested teleportation, or null.
|
||||
*/
|
||||
private static final ThreadLocal<Entity> PORTAL_ENTITY = new ThreadLocal<>();
|
||||
/**
|
||||
* The custom placement logic passed from {@link FabricDimensions#teleport(Entity, ServerWorld, EntityPlacer)}.
|
||||
*/
|
||||
private static EntityPlacer customPlacement;
|
||||
|
||||
/*
|
||||
* The dimension change hooks consist of two steps:
|
||||
* - First, we memorize the currently teleported entity, and set required fields
|
||||
* - Then, we retrieve the teleported entity in the placement logic in PortalForcer#getPortal
|
||||
* and use it to call the entity placers
|
||||
* This lets us use the exact same logic for any entity and prevent the vanilla getPortal (which has unwanted
|
||||
* side effects) from running, while keeping the patches minimally invasive.
|
||||
*
|
||||
* Shortcomings: bugs may arise if another patch cancels the teleportation method between
|
||||
* #prepareDimensionalTeleportation and #tryFindPlacement, AND a mod calls PortalForcer#getPortal directly
|
||||
* right after.
|
||||
*/
|
||||
|
||||
public static void prepareDimensionalTeleportation(Entity entity) {
|
||||
Preconditions.checkNotNull(entity);
|
||||
PORTAL_ENTITY.set(entity);
|
||||
|
||||
// Set values used by `PortalForcer#changeDimension` to prevent a NPE crash.
|
||||
EntityHooks access = ((EntityHooks) entity);
|
||||
|
||||
if (entity.getLastNetherPortalDirectionVector() == null) {
|
||||
access.setLastNetherPortalDirectionVector(entity.getRotationVector());
|
||||
}
|
||||
|
||||
if (entity.getLastNetherPortalDirection() == null) {
|
||||
access.setLastNetherPortalDirection(entity.getHorizontalFacing());
|
||||
}
|
||||
}
|
||||
|
||||
/* Nullable */
|
||||
public static BlockPattern.TeleportTarget tryFindPlacement(ServerWorld destination, Direction portalDir, double portalX, double portalY) {
|
||||
Preconditions.checkNotNull(destination);
|
||||
Entity teleported = PORTAL_ENTITY.get();
|
||||
PORTAL_ENTITY.set(null);
|
||||
|
||||
// If the entity is null, the call does not come from a vanilla context
|
||||
if (teleported == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Custom placement logic, falls back to default dimension placement if no placement or target found
|
||||
EntityPlacer customPlacement = FabricDimensionInternals.customPlacement;
|
||||
|
||||
if (customPlacement != null) {
|
||||
BlockPattern.TeleportTarget customTarget = customPlacement.placeEntity(teleported, destination, portalDir, portalX, portalY);
|
||||
|
||||
if (customTarget != null) {
|
||||
return customTarget;
|
||||
}
|
||||
}
|
||||
|
||||
// Default placement logic, falls back to vanilla if not a fabric dimension
|
||||
RegistryKey<World> registryKey = destination.getRegistryKey();
|
||||
|
||||
if (DEFAULT_PLACERS.containsKey(registryKey)) {
|
||||
BlockPattern.TeleportTarget defaultTarget = DEFAULT_PLACERS.get(registryKey).placeEntity(teleported, destination, portalDir, portalX, portalY);
|
||||
|
||||
if (defaultTarget == null) {
|
||||
throw new IllegalStateException("Mod dimension " + destination.getRegistryKey().getValue().toString() + " returned an invalid teleport target");
|
||||
}
|
||||
|
||||
return defaultTarget;
|
||||
}
|
||||
|
||||
// Vanilla / other implementations logic, undefined behaviour on custom dimensions
|
||||
return null;
|
||||
public static TeleportTarget getCustomTarget() {
|
||||
return currentTarget;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <E extends Entity> E changeDimension(E teleported, ServerWorld dimension, EntityPlacer placement) {
|
||||
assert !teleported.world.isClient : "Entities can only be teleported on the server side";
|
||||
assert Thread.currentThread() == ((ServerWorld) teleported.world).getServer().getThread() : "Entities must be teleported from the main server thread";
|
||||
public static <E extends Entity> E changeDimension(E teleported, ServerWorld dimension, TeleportTarget target) {
|
||||
Preconditions.checkArgument(!teleported.world.isClient, "Entities can only be teleported on the server side");
|
||||
Preconditions.checkArgument(Thread.currentThread() == ((ServerWorld) teleported.world).getServer().getThread(), "Entities must be teleported from the main server thread");
|
||||
|
||||
try {
|
||||
customPlacement = placement;
|
||||
return (E) teleported.changeDimension(dimension);
|
||||
currentTarget = target;
|
||||
return (E) teleported.moveToWorld(dimension);
|
||||
} finally {
|
||||
customPlacement = null;
|
||||
currentTarget = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.fabricmc.fabric.mixin.dimension;
|
||||
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.util.math.Direction;
|
||||
import net.minecraft.util.math.Vec3d;
|
||||
|
||||
@Mixin(Entity.class)
|
||||
public interface EntityHooks {
|
||||
@Accessor
|
||||
void setLastNetherPortalDirectionVector(Vec3d vec);
|
||||
|
||||
@Accessor
|
||||
void setLastNetherPortalDirection(Direction dir);
|
||||
}
|
|
@ -23,14 +23,25 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
|||
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.server.world.ServerWorld;
|
||||
import net.minecraft.world.TeleportTarget;
|
||||
|
||||
import net.fabricmc.fabric.impl.dimension.FabricDimensionInternals;
|
||||
|
||||
/**
|
||||
* This mixin implements {@link Entity#getTeleportTarget(ServerWorld)} for modded dimensions, as Vanilla will
|
||||
* not return a teleport target for anything but Vanilla dimensions.
|
||||
*/
|
||||
@Mixin(Entity.class)
|
||||
public abstract class MixinEntity {
|
||||
// Inject right before the direction vector is retrieved by the game
|
||||
@Inject(method = "changeDimension", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;getLastNetherPortalDirectionVector()Lnet/minecraft/util/math/Vec3d;"))
|
||||
private void onGetPortal(ServerWorld targetWorld, CallbackInfoReturnable<Entity> cir) {
|
||||
FabricDimensionInternals.prepareDimensionalTeleportation((Entity) (Object) this);
|
||||
public class EntityMixin {
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Inject(method = "getTeleportTarget", at = @At("HEAD"), cancellable = true, allow = 1)
|
||||
public void getTeleportTarget(ServerWorld destination, CallbackInfoReturnable<TeleportTarget> cri) {
|
||||
Entity self = (Entity) (Object) this;
|
||||
// Check if a destination has been set for the entity currently being teleported
|
||||
TeleportTarget customTarget = FabricDimensionInternals.getCustomTarget();
|
||||
|
||||
if (customTarget != null) {
|
||||
cri.setReturnValue(customTarget);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.fabricmc.fabric.mixin.dimension;
|
||||
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
import net.minecraft.block.pattern.BlockPattern;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.server.world.ServerWorld;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.util.math.Direction;
|
||||
import net.minecraft.util.math.Vec3d;
|
||||
import net.minecraft.world.PortalForcer;
|
||||
|
||||
import net.fabricmc.fabric.impl.dimension.FabricDimensionInternals;
|
||||
|
||||
@Mixin(PortalForcer.class)
|
||||
public abstract class MixinPortalForcer {
|
||||
@Shadow
|
||||
@Final
|
||||
private ServerWorld world;
|
||||
|
||||
@Inject(method = "usePortal", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;getLastNetherPortalDirectionVector()Lnet/minecraft/util/math/Vec3d;"))
|
||||
private void onUsePortal(Entity teleported, float yaw, CallbackInfoReturnable<Boolean> cir) {
|
||||
FabricDimensionInternals.prepareDimensionalTeleportation(teleported);
|
||||
}
|
||||
|
||||
@Inject(method = "getPortal", at = @At("HEAD"), cancellable = true)
|
||||
private void findEntityPlacement(BlockPos pos, Vec3d velocity, Direction portalDir, double portalX, double portalY, boolean player, CallbackInfoReturnable<BlockPattern.TeleportTarget> cir) {
|
||||
BlockPattern.TeleportTarget ret = FabricDimensionInternals.tryFindPlacement(this.world, portalDir, portalX, portalY);
|
||||
|
||||
if (ret != null) {
|
||||
cir.setReturnValue(ret);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.fabricmc.fabric.mixin.dimension;
|
||||
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
import net.minecraft.world.World;
|
||||
import net.minecraft.world.dimension.DimensionType;
|
||||
|
||||
@Mixin(World.class)
|
||||
public abstract class MixinWorld {
|
||||
@Shadow
|
||||
public abstract DimensionType getDimension();
|
||||
|
||||
@Shadow
|
||||
private int ambientDarkness;
|
||||
|
||||
/* World.isDay() and World.isNight() enable the day-night cycle as well as some entity behavior
|
||||
* (such as bees). In vanilla, these methods are hardcoded to only work in the overworld. This
|
||||
* redirector pretends that all dimensions with a visible sky are DimensionType.OVERWORLD, which
|
||||
* makes the time checks for modded dimensions work.
|
||||
*
|
||||
* Dimension.hasVisibleSky() is true for the overworld, false for the nether and the end, and
|
||||
* customizable for modded dimensions. It is already used for time checking in other places
|
||||
* such as clocks.
|
||||
*/
|
||||
@Inject(method = "isDay", at = @At("HEAD"), cancellable = true)
|
||||
private void isDay(CallbackInfoReturnable<Boolean> infoReturnable) {
|
||||
if (getDimension().hasSkyLight()) {
|
||||
infoReturnable.setReturnValue(ambientDarkness < 4);
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(method = "isNight", at = @At("HEAD"), cancellable = true)
|
||||
private void isNight(CallbackInfoReturnable<Boolean> infoReturnable) {
|
||||
if (getDimension().hasSkyLight()) {
|
||||
infoReturnable.setReturnValue(!(ambientDarkness < 4));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.fabricmc.fabric.mixin.dimension;
|
||||
|
||||
import com.mojang.serialization.Lifecycle;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.ModifyVariable;
|
||||
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||
|
||||
import net.minecraft.nbt.Tag;
|
||||
import net.minecraft.resource.DataPackSettings;
|
||||
import net.minecraft.server.Main;
|
||||
import net.minecraft.util.dynamic.RegistryOps;
|
||||
import net.minecraft.util.registry.DynamicRegistryManager;
|
||||
import net.minecraft.world.gen.GeneratorOptions;
|
||||
import net.minecraft.world.level.LevelInfo;
|
||||
import net.minecraft.world.level.LevelProperties;
|
||||
import net.minecraft.world.level.storage.LevelStorage;
|
||||
|
||||
/**
|
||||
* This Mixin aims to solve a Minecraft Vanilla bug where datapacks are ignored during creation of the
|
||||
* initial LevelProperties when a dedicated server creates a completely new level.
|
||||
*
|
||||
* <p>This also includes the datapacks of loaded Fabric mods, and results in modded dimensions only
|
||||
* being available after restarting the server, once the world has been created.
|
||||
*
|
||||
* <p>This Mixin aims to solve this problem by saving and loading the level.dat file once, after
|
||||
* a new set of level properties is created. This will apply the same logic as reloading the
|
||||
* level.dat after a restart, now including all datapack dimensions.
|
||||
*
|
||||
* <p>See https://bugs.mojang.com/browse/MC-195468 for a related bug report.
|
||||
*
|
||||
* <p>In 1.17: Retest if this bug still occurs without this Mixin by launching a dedicated server with the
|
||||
* dimension testmod, and no world directory. If the dimension is available (i.e. in /execute in, or via
|
||||
* the testmod's commands), then the bug is fixed and this Mixin can be removed.
|
||||
*/
|
||||
@Mixin(value = Main.class, remap = false)
|
||||
public class ServerBugfixMixin {
|
||||
@Unique
|
||||
private static LevelStorage.Session fabric_session;
|
||||
|
||||
@Unique
|
||||
private static DynamicRegistryManager.Impl fabric_dynamicRegistry;
|
||||
|
||||
@Unique
|
||||
private static RegistryOps<Tag> fabric_registryOps;
|
||||
|
||||
@ModifyVariable(at = @At(value = "INVOKE_ASSIGN", target = "Lnet/minecraft/util/registry/DynamicRegistryManager;create()Lnet/minecraft/util/registry/DynamicRegistryManager$Impl;"), method = "main", remap = false, allow = 1)
|
||||
private static DynamicRegistryManager.Impl captureDynamicRegistry(DynamicRegistryManager.Impl value) {
|
||||
fabric_dynamicRegistry = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
@ModifyVariable(at = @At(value = "INVOKE_ASSIGN", target = "Lnet/minecraft/world/level/storage/LevelStorage;createSession(Ljava/lang/String;)Lnet/minecraft/world/level/storage/LevelStorage$Session;"), method = "main", remap = false, allow = 1)
|
||||
private static LevelStorage.Session captureSession(LevelStorage.Session value) {
|
||||
fabric_session = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
@ModifyVariable(at = @At(value = "INVOKE_ASSIGN", target = "Lnet/minecraft/util/dynamic/RegistryOps;of(Lcom/mojang/serialization/DynamicOps;Lnet/minecraft/resource/ResourceManager;Lnet/minecraft/util/registry/DynamicRegistryManager$Impl;)Lnet/minecraft/util/dynamic/RegistryOps;"), method = "main", remap = false, allow = 1)
|
||||
private static RegistryOps<Tag> captureRegistryOps(RegistryOps<Tag> value) {
|
||||
fabric_registryOps = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
@Redirect(method = "main", at = @At(value = "NEW", target = "net/minecraft/world/level/LevelProperties"), remap = false, allow = 1)
|
||||
private static LevelProperties onCreateNewLevelProperties(LevelInfo levelInfo, GeneratorOptions generatorOptions, Lifecycle lifecycle) {
|
||||
DataPackSettings dataPackSettings = levelInfo.getDataPackSettings();
|
||||
|
||||
// Save the level.dat file
|
||||
fabric_session.backupLevelDataFile(fabric_dynamicRegistry, new LevelProperties(levelInfo, generatorOptions, lifecycle));
|
||||
|
||||
// And reload it again, and replace the actual level properties with it
|
||||
return (LevelProperties) fabric_session.readLevelProperties(fabric_registryOps, dataPackSettings);
|
||||
}
|
||||
}
|
|
@ -3,10 +3,8 @@
|
|||
"package": "net.fabricmc.fabric.mixin.dimension",
|
||||
"compatibilityLevel": "JAVA_8",
|
||||
"mixins": [
|
||||
"EntityHooks",
|
||||
"MixinEntity",
|
||||
"MixinPortalForcer",
|
||||
"MixinWorld"
|
||||
"EntityMixin",
|
||||
"ServerBugfixMixin"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
|
|
|
@ -16,65 +16,113 @@
|
|||
|
||||
package net.fabricmc.fabric.test.dimension;
|
||||
|
||||
import static net.minecraft.entity.EntityType.COW;
|
||||
import static net.minecraft.server.command.CommandManager.literal;
|
||||
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
|
||||
import net.minecraft.block.Blocks;
|
||||
import net.minecraft.block.pattern.BlockPattern;
|
||||
import net.minecraft.command.CommandException;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.server.command.ServerCommandSource;
|
||||
import net.minecraft.server.network.ServerPlayerEntity;
|
||||
import net.minecraft.server.world.ServerWorld;
|
||||
import net.minecraft.text.LiteralText;
|
||||
import net.minecraft.util.Identifier;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.util.math.Direction;
|
||||
import net.minecraft.util.math.Vec3d;
|
||||
import net.minecraft.util.registry.Registry;
|
||||
import net.minecraft.util.registry.RegistryKey;
|
||||
import net.minecraft.world.TeleportTarget;
|
||||
import net.minecraft.world.World;
|
||||
import net.minecraft.util.Identifier;
|
||||
import net.minecraft.util.math.Vec3d;
|
||||
import net.minecraft.world.dimension.DimensionOptions;
|
||||
import net.minecraft.world.dimension.DimensionType;
|
||||
|
||||
import net.fabricmc.api.ModInitializer;
|
||||
import net.fabricmc.fabric.api.command.v1.CommandRegistrationCallback;
|
||||
import net.fabricmc.fabric.api.dimension.v1.FabricDimensions;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
|
||||
|
||||
public class FabricDimensionTest implements ModInitializer {
|
||||
private static RegistryKey<World> dimensionRegistryKey;
|
||||
// The dimension options refer to the JSON-file in the dimension subfolder of the datapack,
|
||||
// which will always share it's ID with the world that is created from it
|
||||
private static final RegistryKey<DimensionOptions> DIMENSION_KEY = RegistryKey.of(
|
||||
Registry.DIMENSION_OPTIONS,
|
||||
new Identifier("fabric_dimension", "void")
|
||||
);
|
||||
|
||||
private static RegistryKey<World> WORLD_KEY = RegistryKey.of(
|
||||
Registry.DIMENSION,
|
||||
DIMENSION_KEY.getValue()
|
||||
);
|
||||
|
||||
private static final RegistryKey<DimensionType> DIMENSION_TYPE_KEY = RegistryKey.of(
|
||||
Registry.DIMENSION_TYPE_KEY,
|
||||
new Identifier("fabric_dimension", "void_type")
|
||||
);
|
||||
|
||||
@Override
|
||||
public void onInitialize() {
|
||||
Registry.register(Registry.CHUNK_GENERATOR, new Identifier("fabric_dimension", "void"), VoidChunkGenerator.CODEC);
|
||||
|
||||
dimensionRegistryKey = RegistryKey.of(Registry.DIMENSION, new Identifier("fabric_dimension", "void"));
|
||||
WORLD_KEY = RegistryKey.of(Registry.DIMENSION, new Identifier("fabric_dimension", "void"));
|
||||
|
||||
FabricDimensions.registerDefaultPlacer(dimensionRegistryKey, FabricDimensionTest::placeEntityInVoid);
|
||||
ServerLifecycleEvents.SERVER_STARTED.register(server -> {
|
||||
ServerWorld overworld = server.getWorld(World.OVERWORLD);
|
||||
ServerWorld world = server.getWorld(WORLD_KEY);
|
||||
|
||||
if (world == null) throw new AssertionError("Test world doesn't exist.");
|
||||
|
||||
Entity entity = COW.create(overworld);
|
||||
|
||||
if (!entity.world.getRegistryKey().equals(World.OVERWORLD)) throw new AssertionError("Entity starting world isn't the overworld");
|
||||
|
||||
TeleportTarget target = new TeleportTarget(Vec3d.ZERO, new Vec3d(1, 1, 1), 45f, 60f);
|
||||
|
||||
Entity teleported = FabricDimensions.teleport(entity, world, target);
|
||||
|
||||
if (teleported == null) throw new AssertionError("Entity didn't teleport");
|
||||
|
||||
if (!teleported.world.getRegistryKey().equals(WORLD_KEY)) throw new AssertionError("Target world not reached.");
|
||||
|
||||
if (!teleported.getPos().equals(target.position)) throw new AssertionError("Target Position not reached.");
|
||||
});
|
||||
|
||||
CommandRegistrationCallback.EVENT.register((dispatcher, dedicated) ->
|
||||
dispatcher.register(literal("fabric_dimension_test").executes(FabricDimensionTest.this::executeTestCommand))
|
||||
dispatcher.register(literal("fabric_dimension_test").executes(FabricDimensionTest.this::swapTargeted))
|
||||
);
|
||||
}
|
||||
|
||||
private int executeTestCommand(CommandContext<ServerCommandSource> context) throws CommandSyntaxException {
|
||||
ServerPlayerEntity serverPlayerEntity = context.getSource().getPlayer();
|
||||
ServerWorld serverWorld = serverPlayerEntity.getServerWorld();
|
||||
private int swapTargeted(CommandContext<ServerCommandSource> context) throws CommandSyntaxException {
|
||||
ServerPlayerEntity player = context.getSource().getPlayer();
|
||||
ServerWorld serverWorld = player.getServerWorld();
|
||||
ServerWorld modWorld = getModWorld(context);
|
||||
|
||||
if (!serverWorld.getRegistryKey().equals(dimensionRegistryKey)) {
|
||||
serverPlayerEntity.changeDimension(context.getSource().getMinecraftServer().getWorld(dimensionRegistryKey));
|
||||
if (serverWorld != modWorld) {
|
||||
TeleportTarget target = new TeleportTarget(new Vec3d(0.5, 101, 0.5), Vec3d.ZERO, 0, 0);
|
||||
FabricDimensions.teleport(player, modWorld, target);
|
||||
|
||||
if (player.world != modWorld) {
|
||||
throw new CommandException(new LiteralText("Teleportation failed!"));
|
||||
}
|
||||
|
||||
modWorld.setBlockState(new BlockPos(0, 100, 0), Blocks.DIAMOND_BLOCK.getDefaultState());
|
||||
modWorld.setBlockState(new BlockPos(0, 101, 0), Blocks.TORCH.getDefaultState());
|
||||
} else {
|
||||
FabricDimensions.teleport(serverPlayerEntity, context.getSource().getMinecraftServer().getWorld(World.OVERWORLD), FabricDimensionTest::placeEntity);
|
||||
TeleportTarget target = new TeleportTarget(new Vec3d(0, 100, 0), Vec3d.ZERO,
|
||||
(float) Math.random() * 360 - 180, (float) Math.random() * 360 - 180);
|
||||
FabricDimensions.teleport(player, getWorld(context, World.OVERWORLD), target);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static BlockPattern.TeleportTarget placeEntity(Entity teleported, ServerWorld destination, Direction portalDir, double horizontalOffset, double verticalOffset) {
|
||||
return new BlockPattern.TeleportTarget(new Vec3d(0, 100, 0), Vec3d.ZERO, 0);
|
||||
private ServerWorld getModWorld(CommandContext<ServerCommandSource> context) {
|
||||
return getWorld(context, WORLD_KEY);
|
||||
}
|
||||
|
||||
private static BlockPattern.TeleportTarget placeEntityInVoid(Entity teleported, ServerWorld destination, Direction portalDir, double horizontalOffset, double verticalOffset) {
|
||||
destination.setBlockState(new BlockPos(0, 100, 0), Blocks.DIAMOND_BLOCK.getDefaultState());
|
||||
destination.setBlockState(new BlockPos(0, 101, 0), Blocks.TORCH.getDefaultState());
|
||||
return new BlockPattern.TeleportTarget(new Vec3d(0.5, 101, 0.5), Vec3d.ZERO, 0);
|
||||
private ServerWorld getWorld(CommandContext<ServerCommandSource> context, RegistryKey<World> dimensionRegistryKey) {
|
||||
return context.getSource().getMinecraftServer().getWorld(dimensionRegistryKey);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ public class VoidChunkGenerator extends ChunkGenerator {
|
|||
|
||||
public static final Codec<VoidChunkGenerator> CODEC = RecordCodecBuilder.create((instance) ->
|
||||
instance.group(
|
||||
BiomeSource.field_24713.fieldOf("biome_source")
|
||||
BiomeSource.CODEC.fieldOf("biome_source")
|
||||
.forGetter((generator) -> generator.biomeSource),
|
||||
Codec.BOOL.fieldOf("custom_bool")
|
||||
.forGetter((generator) -> generator.customBool)
|
||||
|
@ -51,7 +51,7 @@ public class VoidChunkGenerator extends ChunkGenerator {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected Codec<? extends ChunkGenerator> method_28506() {
|
||||
protected Codec<? extends ChunkGenerator> getCodec() {
|
||||
return CODEC;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"ultrawarm": false,
|
||||
"natural": false,
|
||||
"shrunk": false,
|
||||
"coordinate_scale": 1,
|
||||
"ambient_light": 0.1,
|
||||
"has_skylight": true,
|
||||
"has_ceiling": false,
|
|
@ -21,7 +21,7 @@ include 'fabric-command-api-v1'
|
|||
include 'fabric-containers-v0'
|
||||
include 'fabric-content-registries-v0'
|
||||
include 'fabric-crash-report-info-v1'
|
||||
//include 'fabric-dimensions-v1'
|
||||
include 'fabric-dimensions-v1'
|
||||
include 'fabric-events-interaction-v0'
|
||||
include 'fabric-events-lifecycle-v0'
|
||||
include 'fabric-game-rule-api-v1'
|
||||
|
|
Loading…
Add table
Reference in a new issue