Restore Legacy link modifier travel behavior
Build / build (push) Canceled after 0s

This commit is contained in:
dnorberg committed 2026-10-21 13:36:30 -04:00
1 parent b9faa28dd5
commit 6337b5aa10
16 files changed
+355 -57

No files matched your search

@@ -11,7 +11,6 @@ import net.dnorberg.rivencraft.registry.ModItems;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.util.RandomSource;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.item.ItemStack;
@@ -54,16 +53,16 @@ public final class CrystalPortalBlock extends BaseEntityBlock {
@Override protected boolean canBeReplaced(BlockState state, Fluid fluid) { return false; }
@Override protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity) {
if (!(level instanceof ServerLevel server) || !(entity instanceof ServerPlayer player)) return;
if (!(level instanceof ServerLevel server)) return;
long now = server.getServer().getTickCount();
if (PortalTravelCooldown.blocked(player, now)) return;
if (PortalTravelCooldown.blocked(entity, now)) return;
if (!(level.getBlockEntity(pos) instanceof CrystalPortalBlockEntity portal)
|| portal.receptacle() == null || !level.hasChunkAt(portal.receptacle())
|| !(level.getBlockEntity(portal.receptacle()) instanceof BookReceptacleBlockEntity holder)) return;
ItemStack book = holder.getBook();
if (book.isEmpty()) return;
PortalTravelCooldown.mark(player, now);
PortalTravelQueue.enqueue(player, server, pos, portal.receptacle(), book, now);
PortalTravelCooldown.mark(entity, now);
PortalTravelQueue.enqueue(entity, server, pos, portal.receptacle(), book, now);
}
@Override protected void neighborChanged(BlockState state, Level level, BlockPos pos, Block neighbor,
@@ -101,9 +101,14 @@ public final class AgeDimensionService {
public static Optional<LinkTarget> resolve(ServerPlayer player, AgeReference reference,
OrderedWriting writing, boolean generatePlatform) {
AgeDefinition definition = AgeRegistrySavedData.resolveOrCreate(player.serverLevel(), reference, writing);
if (AgeRegistrySavedData.get(player.serverLevel()).isDead(definition.id())) return Optional.empty();
ServerLevel level = ensureLoaded(player.getServer(), definition);
return resolve(player.serverLevel(), reference, writing, generatePlatform);
}
public static Optional<LinkTarget> resolve(ServerLevel source, AgeReference reference,
OrderedWriting writing, boolean generatePlatform) {
AgeDefinition definition = AgeRegistrySavedData.resolveOrCreate(source, reference, writing);
if (AgeRegistrySavedData.get(source).isDead(definition.id())) return Optional.empty();
ServerLevel level = ensureLoaded(source.getServer(), definition);
if (level == null) {
return Optional.empty();
}
@@ -0,0 +1,58 @@
package net.dnorberg.rivencraft.book;
import net.dnorberg.rivencraft.mixin.MobDropChanceAccessor;
import net.minecraft.world.Container;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.animal.horse.AbstractHorse;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.ItemStack;
/** Legacy-style Disarm: leave carried items and eligible equipment at the departure point. */
public final class LinkDisarmService {
private LinkDisarmService() {}
public static boolean mayTravel(Entity entity) {
// Legacy refuses loose items and dropped Link Books on Disarm links.
return !(entity instanceof ItemEntity);
}
public static void disarm(Entity entity) {
if (entity instanceof ServerPlayer player) {
eject(player, player.getInventory());
player.containerMenu.broadcastChanges();
}
if (entity instanceof Container container) eject(entity, container);
if (entity instanceof AbstractHorse horse) eject(entity, horse.getInventory());
if (entity instanceof Mob mob) dropEquipment(mob);
}
private static void eject(Entity carrier, Container inventory) {
for (int slot = 0; slot < inventory.getContainerSize(); slot++) {
ItemStack stack = inventory.getItem(slot);
if (stack.isEmpty()) continue;
inventory.setItem(slot, ItemStack.EMPTY);
carrier.spawnAtLocation(stack);
}
inventory.setChanged();
}
private static void dropEquipment(Mob mob) {
for (EquipmentSlot slot : EquipmentSlot.values()) {
ItemStack stack = mob.getItemBySlot(slot);
if (stack.isEmpty()) continue;
float chance = ((MobDropChanceAccessor) mob).rivencraft$getEquipmentDropChance(slot);
mob.setItemSlot(slot, ItemStack.EMPTY);
if (chance <= 0 || mob.getRandom().nextFloat() >= chance) continue;
if (chance <= 1 && stack.isDamageableItem()) {
int minimum = Math.max(stack.getMaxDamage() - 25, 1);
int damage = stack.getMaxDamage() - mob.getRandom().nextInt(
1 + mob.getRandom().nextInt(minimum));
stack.setDamageValue(Math.clamp(damage, 1, minimum));
}
mob.spawnAtLocation(stack);
}
}
}
@@ -0,0 +1,125 @@
package net.dnorberg.rivencraft.book;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.core.BlockPos;
import net.minecraft.core.GlobalPos;
import net.minecraft.gametest.framework.GameTest;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.animal.horse.Donkey;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.entity.monster.Zombie;
import net.minecraft.world.entity.vehicle.MinecartChest;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import net.neoforged.neoforge.gametest.GameTestHolder;
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
@GameTestHolder(Rivencraft.MOD_ID)
@PrefixGameTestTemplate(false)
public final class LinkModifierBehaviorGameTests {
private LinkModifierBehaviorGameTests() {}
@GameTest(template = "empty")
public static void maintainMomentumRotatesToDestinationYaw(GameTestHelper helper) {
Vec3 result = LinkingService.rotatedMomentum(new Vec3(3, -0.5, 4), 90);
helper.assertTrue(Math.abs(result.x) < 0.0001 && Math.abs(result.y + 0.3) < 0.0001
&& Math.abs(result.z - 5) < 0.0001,
"Legacy momentum preserves horizontal speed, rotates to destination yaw, and adds a small lift");
helper.succeed();
}
@GameTest(template = "empty")
public static void disarmRefusesLooseItems(GameTestHelper helper) {
var level = helper.getLevel();
ItemEntity item = new ItemEntity(level, 1, 1, 1, new ItemStack(Items.DIAMOND));
level.addFreshEntity(item);
LinkTarget target = new LinkTarget(GlobalPos.of(level.dimension(), helper.absolutePos(new BlockPos(2, 1, 2))),
0, 0);
helper.assertValueEqual(LinkingService.travelEntityPortal(item, target,
new LinkProperties(LinkProperties.DISARM | LinkProperties.INTRA_LINKING), false),
LinkingService.LinkResult.DISARM_REJECTED,
"Legacy Disarm must refuse a loose item before teleportation");
helper.assertTrue(item.isAlive() && item.getItem().is(Items.DIAMOND),
"Rejected item must remain at the source");
helper.succeed();
}
@GameTest(template = "empty")
public static void ordinaryLinkMovesLooseItemWithoutMakingPlatform(GameTestHelper helper) {
var level = helper.getLevel();
BlockPos landing = helper.absolutePos(new BlockPos(2, 2, 2));
level.setBlock(landing.below(), Blocks.AIR.defaultBlockState(), 2);
level.setBlock(landing, Blocks.AIR.defaultBlockState(), 2);
level.setBlock(landing.above(), Blocks.AIR.defaultBlockState(), 2);
ItemEntity item = new ItemEntity(level, 1, 1, 1, new ItemStack(Items.DIAMOND));
level.addFreshEntity(item);
LinkTarget target = new LinkTarget(GlobalPos.of(level.dimension(), landing), 0, 0);
helper.assertValueEqual(LinkingService.travelEntityPortal(item, target,
new LinkProperties(LinkProperties.INTRA_LINKING), false),
LinkingService.LinkResult.SUCCESS, "Ordinary portal links may carry loose items");
helper.assertTrue(item.blockPosition().distManhattan(landing) <= 1,
"Item must arrive at the linked location");
helper.assertTrue(level.getBlockState(landing.below()).isAir(),
"A Linking Book must not generate a landing platform by default");
helper.succeed();
}
@GameTest(template = "empty")
public static void explicitGeneratePlatformAddsOnlyArrivalFloor(GameTestHelper helper) {
var level = helper.getLevel();
BlockPos landing = helper.absolutePos(new BlockPos(2, 2, 2));
level.setBlock(landing.below(), Blocks.AIR.defaultBlockState(), 2);
level.setBlock(landing, Blocks.AIR.defaultBlockState(), 2);
level.setBlock(landing.above(), Blocks.AIR.defaultBlockState(), 2);
level.setBlock(landing.above(2), Blocks.AIR.defaultBlockState(), 2);
ItemEntity item = new ItemEntity(level, 1, 1, 1, new ItemStack(Items.DIAMOND));
level.addFreshEntity(item);
LinkTarget target = new LinkTarget(GlobalPos.of(level.dimension(), landing), 0, 0);
helper.assertValueEqual(LinkingService.travelEntityPortal(item, target,
new LinkProperties(LinkProperties.INTRA_LINKING | LinkProperties.GENERATE_PLATFORM), false),
LinkingService.LinkResult.SUCCESS, "An explicit platform link must travel");
helper.assertTrue(level.getBlockState(landing.below()).is(
net.dnorberg.rivencraft.registry.ModBlocks.CLOUD_PLATFORM.get()),
"An enabled Linking Book platform must add one cloud block below the arrival");
helper.succeed();
}
@GameTest(template = "empty")
public static void disarmEmptiesCarrierAndMobEquipment(GameTestHelper helper) {
var level = helper.getLevel();
Vec3 source = Vec3.atCenterOf(helper.absolutePos(new BlockPos(1, 1, 1)));
MinecartChest cart = EntityType.CHEST_MINECART.create(level);
Donkey donkey = EntityType.DONKEY.create(level);
Zombie zombie = EntityType.ZOMBIE.create(level);
helper.assertTrue(cart != null && donkey != null && zombie != null, "Carriers must instantiate");
cart.setPos(source);
donkey.setPos(source);
zombie.setPos(source);
level.addFreshEntity(cart);
level.addFreshEntity(donkey);
level.addFreshEntity(zombie);
cart.setItem(0, new ItemStack(Items.EMERALD));
donkey.getInventory().setItem(0, new ItemStack(Items.SADDLE));
zombie.setItemSlot(EquipmentSlot.MAINHAND, new ItemStack(Items.IRON_SWORD));
zombie.setDropChance(EquipmentSlot.MAINHAND, 2.0F);
LinkDisarmService.disarm(cart);
LinkDisarmService.disarm(donkey);
LinkDisarmService.disarm(zombie);
helper.assertTrue(cart.getItem(0).isEmpty() && donkey.getInventory().getItem(0).isEmpty()
&& zombie.getItemBySlot(EquipmentSlot.MAINHAND).isEmpty(),
"Disarm must remove inventory contents and worn equipment");
var drops = level.getEntitiesOfClass(ItemEntity.class, new AABB(source, source).inflate(3));
helper.assertTrue(drops.stream().anyMatch(item -> item.getItem().is(Items.EMERALD))
&& drops.stream().anyMatch(item -> item.getItem().is(Items.SADDLE))
&& drops.stream().anyMatch(item -> item.getItem().is(Items.IRON_SWORD)),
"Carrier items and guaranteed-drop mob equipment must remain at departure");
helper.succeed();
}
}
@@ -11,7 +11,7 @@ import net.minecraft.network.chat.Component;
/** Flags written into a Link Panel and carried by books assembled from it. */
public record LinkProperties(int flags) {
public static final int DISARM = 1;
/** Retained to read older test books; a newly edited book uses DISABLE_PLATFORM instead. */
/** Linking Books do not create a platform unless this Legacy flag is enabled. */
public static final int GENERATE_PLATFORM = 2;
public static final int MAINTAIN_MOMENTUM = 4;
public static final int INTRA_LINKING = 8;
@@ -48,6 +48,7 @@ public record LinkProperties(int flags) {
if (has(INTRA_LINKING)) lines.add(propertyName("intra_linking"));
if (has(INTRA_LINKING_ONLY)) lines.add(propertyName("intra_linking_only"));
if (has(RELATIVE_LINK)) lines.add(propertyName("relative_link"));
if (has(GENERATE_PLATFORM)) lines.add(propertyName("generate_platform"));
if (has(DISABLE_PLATFORM)) lines.add(propertyName("disable_platform"));
}
@@ -63,6 +64,7 @@ public record LinkProperties(int flags) {
case INTRA_LINKING -> propertyName("intra_linking");
case INTRA_LINKING_ONLY -> propertyName("intra_linking_only");
case RELATIVE_LINK -> propertyName("relative_link");
case GENERATE_PLATFORM -> propertyName("generate_platform");
case DISABLE_PLATFORM -> propertyName("disable_platform");
default -> throw new IllegalArgumentException("Unknown link property flag: " + flag);
};
@@ -6,16 +6,20 @@ import java.util.stream.IntStream;
import java.util.stream.Stream;
import net.dnorberg.rivencraft.compat.ImmersivePortalsChunkLoaders;
import net.dnorberg.rivencraft.entity.DroppedBookEntity;
import net.dnorberg.rivencraft.portal.PortalTravelCooldown;
import net.dnorberg.rivencraft.registry.ModBlocks;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.item.ItemStack;
import net.minecraft.tags.BlockTags;
import net.minecraft.world.level.border.WorldBorder;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.phys.Vec3;
import net.minecraft.world.level.portal.DimensionTransition;
import net.neoforged.fml.ModList;
/** Server-side validation and execution for book travel. */
@@ -96,9 +100,9 @@ public final class LinkingService {
Vec3 position = destination.get();
Vec3 momentum = player.getDeltaMovement();
if (properties.has(LinkProperties.DISARM)) dropInventoryAtSource(player);
if (properties.has(LinkProperties.DISARM)) LinkDisarmService.disarm(player);
teleport(player, destinationLevel, position, target, true);
if (properties.has(LinkProperties.MAINTAIN_MOMENTUM)) player.setDeltaMovement(momentum);
applyMomentum(player, momentum, target, properties);
player.resetFallDistance();
return LinkResult.SUCCESS;
}
@@ -114,9 +118,9 @@ public final class LinkingService {
landingOrigin(player, destinationLevel, target, properties), true);
if (destination.isEmpty()) return LinkResult.NO_SAFE_DESTINATION;
Vec3 momentum = player.getDeltaMovement();
if (properties.has(LinkProperties.DISARM)) dropInventoryAtSource(player);
if (properties.has(LinkProperties.DISARM)) LinkDisarmService.disarm(player);
teleport(player, destinationLevel, destination.get(), target, instant);
if (properties.has(LinkProperties.MAINTAIN_MOMENTUM)) player.setDeltaMovement(momentum);
applyMomentum(player, momentum, target, properties);
player.resetFallDistance();
return LinkResult.SUCCESS;
}
@@ -142,36 +146,80 @@ public final class LinkingService {
Vec3 position = destination.get();
Vec3 momentum = player.getDeltaMovement();
if (properties.has(LinkProperties.DISARM)) dropInventoryAtSource(player);
if (properties.has(LinkProperties.DISARM)) LinkDisarmService.disarm(player);
teleport(player, destinationLevel, position, target, instant);
if (properties.has(LinkProperties.MAINTAIN_MOMENTUM)) player.setDeltaMovement(momentum);
if (properties.has(LinkProperties.GENERATE_PLATFORM))
generateArrivalPlatform(destinationLevel, position);
applyMomentum(player, momentum, target, properties);
player.resetFallDistance();
return LinkResult.SUCCESS;
}
/** Non-player portal travel follows the same link rules; Disarm may remove carried cargo. */
public static LinkResult travelEntityPortal(Entity entity, LinkTarget target,
LinkProperties properties, boolean requiresFooting) {
if (!(entity.level() instanceof ServerLevel source)) return LinkResult.MISSING_DIMENSION;
ServerLevel destinationLevel = source.getServer().getLevel(target.location().dimension());
if (destinationLevel == null) return LinkResult.MISSING_DIMENSION;
if (properties.has(LinkProperties.INTRA_LINKING_ONLY) && destinationLevel != source)
return LinkResult.INTRA_ONLY;
if (destinationLevel == source && !properties.has(LinkProperties.INTRA_LINKING)
&& !properties.has(LinkProperties.INTRA_LINKING_ONLY)) return LinkResult.INTRA_LINKING_REQUIRED;
if (properties.has(LinkProperties.DISARM) && !LinkDisarmService.mayTravel(entity))
return LinkResult.DISARM_REJECTED;
if (!entity.canChangeDimensions(source, destinationLevel)) return LinkResult.NO_SAFE_DESTINATION;
BlockPos origin = landingOrigin(entity, destinationLevel, target, properties);
Optional<Vec3> destination = resolveDestination(destinationLevel, origin, requiresFooting);
if (destination.isEmpty()) return LinkResult.NO_SAFE_DESTINATION;
Vec3 momentum = entity.getDeltaMovement();
if (properties.has(LinkProperties.DISARM)) LinkDisarmService.disarm(entity);
Entity arrived = entity.changeDimension(new DimensionTransition(destinationLevel, destination.get(),
properties.has(LinkProperties.MAINTAIN_MOMENTUM)
? rotatedMomentum(momentum, target.yaw()) : Vec3.ZERO,
target.yaw(), target.pitch(), DimensionTransition.DO_NOTHING));
if (arrived == null) return LinkResult.NO_SAFE_DESTINATION;
if (!requiresFooting && properties.has(LinkProperties.GENERATE_PLATFORM))
generateArrivalPlatform(destinationLevel, destination.get());
arrived.setXRot(target.pitch());
arrived.resetFallDistance();
PortalTravelCooldown.mark(arrived, source.getServer().getTickCount());
return LinkResult.SUCCESS;
}
static Vec3 rotatedMomentum(Vec3 incoming, float destinationYaw) {
// Legacy rotates the horizontal speed to the destination yaw and adds a small upward lift.
double horizontalSpeed = Math.hypot(incoming.x, incoming.z);
double radians = Math.toRadians(destinationYaw);
return new Vec3(horizontalSpeed * Math.cos(radians), incoming.y + 0.2,
horizontalSpeed * Math.sin(radians));
}
private static void applyMomentum(ServerPlayer player, Vec3 incoming, LinkTarget target,
LinkProperties properties) {
player.setDeltaMovement(properties.has(LinkProperties.MAINTAIN_MOMENTUM)
? rotatedMomentum(incoming, target.yaw()) : Vec3.ZERO);
}
private static void generateArrivalPlatform(ServerLevel level, Vec3 arrival) {
BlockPos feet = BlockPos.containing(arrival);
BlockPos floor = feet.below();
// Legacy only fills a missing floor when there is clear space above the arrival.
if (level.isEmptyBlock(floor) && level.isEmptyBlock(feet.above(2)))
level.setBlock(floor, ModBlocks.CLOUD_PLATFORM.get().defaultBlockState(), 3);
}
/** Legacy relative links retain the traveller's offset from the source spawn. */
private static BlockPos landingOrigin(ServerPlayer player, ServerLevel destinationLevel,
private static BlockPos landingOrigin(Entity player, ServerLevel destinationLevel,
LinkTarget target, LinkProperties properties) {
if (!properties.has(LinkProperties.RELATIVE_LINK)) return target.location().pos();
BlockPos sourceSpawn = player.serverLevel().getSharedSpawnPos();
BlockPos sourceSpawn = ((ServerLevel) player.level()).getSharedSpawnPos();
BlockPos destinationSpawn = destinationLevel.getSharedSpawnPos();
BlockPos departure = player.blockPosition();
return destinationSpawn.offset(departure.getX() - sourceSpawn.getX(),
departure.getY() - sourceSpawn.getY(), departure.getZ() - sourceSpawn.getZ());
}
/** Drop each inventory stack as an item entity at the departure point. */
private static void dropInventoryAtSource(ServerPlayer player) {
var inventory = player.getInventory();
for (int slot = 0; slot < inventory.getContainerSize(); slot++) {
var stack = inventory.getItem(slot);
if (stack.isEmpty()) continue;
inventory.setItem(slot, net.minecraft.world.item.ItemStack.EMPTY);
player.drop(stack, false);
}
player.containerMenu.broadcastChanges();
}
private static void teleport(
ServerPlayer player,
ServerLevel destinationLevel,
@@ -285,6 +333,7 @@ public final class LinkingService {
MISSING_DIMENSION,
INTRA_LINKING_REQUIRED,
INTRA_ONLY,
DISARM_REJECTED,
NO_SAFE_DESTINATION
}
}
@@ -6,6 +6,7 @@ import net.dnorberg.rivencraft.menu.LinkModifierMenu;
import net.dnorberg.rivencraft.network.SetLinkModifierTitlePayload;
import net.dnorberg.rivencraft.network.SetLinkModifierSeedPayload;
import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.registry.ModItems;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.EditBox;
import net.minecraft.client.gui.components.Tooltip;
@@ -137,9 +138,12 @@ public final class LinkModifierScreen extends AbstractContainerScreen<LinkModifi
@Override protected void renderBg(GuiGraphics graphics, float partialTick, int mouseX, int mouseY) {
graphics.blit(BACKGROUND, leftPos, topPos, 0, 0, imageWidth, imageHeight, 256, 256);
LinkProperties properties = menu.book().getOrDefault(ModDataComponents.LINK_PROPERTIES.get(), LinkProperties.NONE);
for (int index = 0; index < FLAGS.length; index++)
for (int index = 0; index < FLAGS.length; index++) {
boolean selected = FLAGS[index] == LinkProperties.DISABLE_PLATFORM
? platformEnabled(properties) : properties.has(FLAGS[index]);
square(graphics, 5 + (index / 3) * 20, 10 + (index % 3) * 20,
properties.has(FLAGS[index]), false);
selected, false);
}
if (menu.canDelete(minecraft.player)) {
square(graphics, 120, 32, menu.awaitingDeletionConfirmation(), false);
if (menu.awaitingDeletionConfirmation()) square(graphics, 140, 32, false, true);
@@ -166,8 +170,8 @@ public final class LinkModifierScreen extends AbstractContainerScreen<LinkModifi
if (inside(mouseX, mouseY, 5 + (index / 3) * 20, 10 + (index % 3) * 20, 18, 18)) {
Component label = FLAGS[index] == LinkProperties.DISABLE_PLATFORM
? Component.literal("Generate Platform: "
+ (menu.book().getOrDefault(ModDataComponents.LINK_PROPERTIES.get(), LinkProperties.NONE)
.has(LinkProperties.DISABLE_PLATFORM) ? "Off" : "On"))
+ (platformEnabled(menu.book().getOrDefault(ModDataComponents.LINK_PROPERTIES.get(),
LinkProperties.NONE)) ? "On" : "Off"))
: LinkProperties.name(FLAGS[index]);
graphics.renderTooltip(font, label, mouseX, mouseY);
return;
@@ -180,4 +184,10 @@ public final class LinkModifierScreen extends AbstractContainerScreen<LinkModifi
? "screen.rivencraft.link_modifier.confirm" : "screen.rivencraft.link_modifier.retire"),
mouseX, mouseY);
}
private boolean platformEnabled(LinkProperties properties) {
return menu.book().is(ModItems.LINKING_BOOK.get())
? properties.has(LinkProperties.GENERATE_PLATFORM)
: !properties.has(LinkProperties.DISABLE_PLATFORM);
}
}
@@ -142,7 +142,8 @@ public final class LinkModifierMenu extends AbstractContainerMenu {
case TOGGLE_INTRA -> LinkProperties.INTRA_LINKING;
case TOGGLE_INTRA_ONLY -> LinkProperties.INTRA_LINKING_ONLY;
case TOGGLE_RELATIVE -> LinkProperties.RELATIVE_LINK;
case TOGGLE_PLATFORM -> LinkProperties.DISABLE_PLATFORM;
case TOGGLE_PLATFORM -> book().is(ModItems.LINKING_BOOK.get())
? LinkProperties.GENERATE_PLATFORM : LinkProperties.DISABLE_PLATFORM;
default -> 0;
};
if (flag == 0 || ((flag == LinkProperties.INTRA_LINKING || flag == LinkProperties.INTRA_LINKING_ONLY)
@@ -0,0 +1,12 @@
package net.dnorberg.rivencraft.mixin;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.entity.Mob;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Invoker;
@Mixin(Mob.class)
public interface MobDropChanceAccessor {
@Invoker("getEquipmentDropChance")
float rivencraft$getEquipmentDropChance(EquipmentSlot slot);
}
@@ -2,12 +2,13 @@ package net.dnorberg.rivencraft.portal;
import java.util.Map;
import java.util.WeakHashMap;
import net.minecraft.world.entity.Entity;
import net.minecraft.server.level.ServerPlayer;
/** Session-only portal cooldowns: tick counters must never be saved with players. */
public final class PortalTravelCooldown {
public static final long TICKS = 40;
private static final Map<ServerPlayer, Long> LAST_USE = new WeakHashMap<>();
private static final Map<Entity, Long> LAST_USE = new WeakHashMap<>();
private PortalTravelCooldown() {}
@@ -15,16 +16,16 @@ public final class PortalTravelCooldown {
return previous >= 0 && now >= previous && now - previous < TICKS;
}
public static boolean blocked(ServerPlayer player, long now) {
return blocked(now, LAST_USE.getOrDefault(player, -1L));
public static boolean blocked(Entity entity, long now) {
return blocked(now, LAST_USE.getOrDefault(entity, -1L));
}
public static void mark(ServerPlayer player, long now) {
LAST_USE.put(player, now);
public static void mark(Entity entity, long now) {
LAST_USE.put(entity, now);
}
public static void forget(ServerPlayer player) {
LAST_USE.remove(player);
public static void forget(Entity entity) {
LAST_USE.remove(entity);
}
public static void clear() {
@@ -7,34 +7,43 @@ import java.util.List;
import java.util.Map;
import net.dnorberg.rivencraft.block.entity.BookReceptacleBlockEntity;
import net.dnorberg.rivencraft.block.entity.CrystalPortalBlockEntity;
import net.dnorberg.rivencraft.book.AgeDimensionService;
import net.dnorberg.rivencraft.book.AgeReference;
import net.dnorberg.rivencraft.book.LinkDisarmService;
import net.dnorberg.rivencraft.book.LinkProperties;
import net.dnorberg.rivencraft.book.LinkTarget;
import net.dnorberg.rivencraft.book.LinkingService;
import net.dnorberg.rivencraft.item.DescriptiveBookItem;
import net.dnorberg.rivencraft.item.LinkingBookItem;
import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.registry.ModItems;
import net.dnorberg.rivencraft.writing.OrderedWriting;
import net.minecraft.core.BlockPos;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.item.ItemStack;
/** Defers block-collision travel until the movement packet that entered the portal has finished. */
public final class PortalTravelQueue {
private static final Map<ServerPlayer, Pending> PENDING = new IdentityHashMap<>();
private static final Map<Entity, Pending> PENDING = new IdentityHashMap<>();
private record Pending(ServerLevel source, BlockPos portal, BlockPos receptacle,
ItemStack book, long queuedTick) {}
private record Travel(ServerPlayer player, Pending pending) {}
private record Travel(Entity entity, Pending pending) {}
private PortalTravelQueue() {}
public static void enqueue(ServerPlayer player, ServerLevel source, BlockPos portal,
public static void enqueue(Entity entity, ServerLevel source, BlockPos portal,
BlockPos receptacle, ItemStack book, long now) {
PENDING.putIfAbsent(player, new Pending(source, portal.immutable(), receptacle.immutable(),
PENDING.putIfAbsent(entity, new Pending(source, portal.immutable(), receptacle.immutable(),
book.copyWithCount(1), now));
}
public static void process(MinecraftServer server) {
List<Travel> ready = new ArrayList<>();
Iterator<Map.Entry<ServerPlayer, Pending>> iterator = PENDING.entrySet().iterator();
Iterator<Map.Entry<Entity, Pending>> iterator = PENDING.entrySet().iterator();
while (iterator.hasNext()) {
var entry = iterator.next();
if (server.getTickCount() <= entry.getValue().queuedTick()) continue;
@@ -44,21 +53,45 @@ public final class PortalTravelQueue {
// Travel can itself cause block-collision callbacks, so do not keep an
// iterator over the pending map while changing the player's dimension.
for (Travel travel : ready) {
ServerPlayer player = travel.player();
Entity entity = travel.entity();
Pending pending = travel.pending();
if (server.getPlayerList().getPlayer(player.getUUID()) != player
|| !player.isAlive() || player.serverLevel() != pending.source()
if ((entity instanceof ServerPlayer player
&& server.getPlayerList().getPlayer(player.getUUID()) != player)
|| entity.isRemoved() || !entity.isAlive() || entity.level() != pending.source()
|| !(pending.source().getBlockEntity(pending.portal()) instanceof CrystalPortalBlockEntity portal)
|| !pending.receptacle().equals(portal.receptacle())
|| !(pending.source().getBlockEntity(pending.receptacle()) instanceof BookReceptacleBlockEntity holder)
|| !ItemStack.isSameItemSameComponents(holder.getBook(), pending.book())) continue;
if (pending.book().is(ModItems.LINKING_BOOK.get()))
LinkingBookItem.usePortalBook(player, pending.book());
else if (pending.book().is(ModItems.DESCRIPTIVE_BOOK.get()))
DescriptiveBookItem.usePortalBook(player, pending.book());
if (entity instanceof ServerPlayer player) {
if (pending.book().is(ModItems.LINKING_BOOK.get()))
LinkingBookItem.usePortalBook(player, pending.book());
else if (pending.book().is(ModItems.DESCRIPTIVE_BOOK.get()))
DescriptiveBookItem.usePortalBook(player, pending.book());
} else {
travelEntity(entity, pending);
}
}
}
private static void travelEntity(Entity entity, Pending pending) {
ItemStack book = pending.book();
LinkProperties properties = book.getOrDefault(ModDataComponents.LINK_PROPERTIES.get(), LinkProperties.NONE);
if (properties.has(LinkProperties.DISARM) && !LinkDisarmService.mayTravel(entity)) return;
LinkTarget target;
boolean descriptive = book.is(ModItems.DESCRIPTIVE_BOOK.get());
if (descriptive) {
AgeReference reference = book.get(ModDataComponents.AGE_REFERENCE.get());
if (reference == null) return;
properties = properties.forDescriptiveBook();
target = AgeDimensionService.resolve(pending.source(), reference, OrderedWriting.EMPTY,
!properties.has(LinkProperties.DISABLE_PLATFORM)).orElse(null);
} else if (book.is(ModItems.LINKING_BOOK.get())) {
target = book.get(ModDataComponents.LINK_TARGET.get());
} else return;
if (target == null) return;
LinkingService.travelEntityPortal(entity, target, properties, descriptive);
}
public static void forget(ServerPlayer player) { PENDING.remove(player); }
public static void clear() { PENDING.clear(); }
}
@@ -18,6 +18,7 @@
"item.rivencraft.link_property.intra_linking_only": "Intra-Linking Only",
"item.rivencraft.link_property.relative_link": "Relative Link",
"item.rivencraft.link_property.disable_platform": "Generate Platform: Off",
"item.rivencraft.link_property.generate_platform": "Generate Platform: On",
"item.rivencraft.page.unresolved": "Unresolved Symbol Page",
"item.rivencraft.notebook": "Notebook",
"item.rivencraft.collation_folder": "Collation Folder",
@@ -7,6 +7,7 @@
"AgeDimensionSeedMixin",
"AgeDungeonPlacementMixin",
"AgeStructureBiomeMixin",
"MobDropChanceAccessor",
"StructurePlacementAccessor"
],
"client": [