Public Access
This commit is contained in:
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": [
|
||||
|
||||
Reference in new issue
Block a user