diff --git a/src/main/java/net/xevianlight/aphelion/block/custom/LaunchPad.java b/src/main/java/net/xevianlight/aphelion/block/custom/LaunchPad.java index c6355ef..ca3439b 100644 --- a/src/main/java/net/xevianlight/aphelion/block/custom/LaunchPad.java +++ b/src/main/java/net/xevianlight/aphelion/block/custom/LaunchPad.java @@ -2,8 +2,12 @@ package net.xevianlight.aphelion.block.custom; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; import net.minecraft.world.item.Item; import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.Level; import net.minecraft.world.level.LevelAccessor; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.SoundType; @@ -11,7 +15,12 @@ import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.StateDefinition; import net.minecraft.world.level.block.state.properties.BlockStateProperties; import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.xevianlight.aphelion.Aphelion; +import net.xevianlight.aphelion.core.init.ModDimensions; +import net.xevianlight.aphelion.core.saveddata.SpacePartitionSavedData; +import net.xevianlight.aphelion.core.saveddata.types.PartitionData; import net.xevianlight.aphelion.util.ModTags; +import net.xevianlight.aphelion.util.SpacePartition; import org.jetbrains.annotations.NotNull; public class LaunchPad extends Block { @@ -73,5 +82,4 @@ public class LaunchPad extends Block { } return state; } - } diff --git a/src/main/java/net/xevianlight/aphelion/block/custom/RocketAssembler.java b/src/main/java/net/xevianlight/aphelion/block/custom/RocketAssembler.java index 64fb6b9..f958238 100644 --- a/src/main/java/net/xevianlight/aphelion/block/custom/RocketAssembler.java +++ b/src/main/java/net/xevianlight/aphelion/block/custom/RocketAssembler.java @@ -2,6 +2,7 @@ package net.xevianlight.aphelion.block.custom; import com.mojang.serialization.MapCodec; import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.InteractionResult; import net.minecraft.world.entity.player.Player; @@ -15,6 +16,9 @@ import net.minecraft.world.level.block.state.properties.BooleanProperty; import net.minecraft.world.phys.BlockHitResult; import net.xevianlight.aphelion.block.custom.base.BasicHorizontalEntityBlock; import net.xevianlight.aphelion.block.entity.custom.RocketAssemblerBlockEntity; +import net.xevianlight.aphelion.core.init.ModDimensions; +import net.xevianlight.aphelion.core.saveddata.SpacePartitionSavedData; +import net.xevianlight.aphelion.core.saveddata.types.PartitionData; import net.xevianlight.aphelion.entites.vehicles.RocketEntity; import net.xevianlight.aphelion.util.AphelionBlockStateProperties; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/net/xevianlight/aphelion/block/custom/base/BasicEntityBlock.java b/src/main/java/net/xevianlight/aphelion/block/custom/base/BasicEntityBlock.java index 4d6823d..402cf68 100644 --- a/src/main/java/net/xevianlight/aphelion/block/custom/base/BasicEntityBlock.java +++ b/src/main/java/net/xevianlight/aphelion/block/custom/base/BasicEntityBlock.java @@ -37,14 +37,16 @@ public abstract class BasicEntityBlock extends BaseEntityBlock { public @Nullable BlockEntityTicker getTicker(@NotNull Level level, @NotNull BlockState state, @NotNull BlockEntityType blockEntityType) { return !shouldTick ? null : (entityLevel, pos, blockState, blockEntity) -> { if (blockEntity instanceof TickableBlockEntity tickable) { - long time = level.getGameTime() - pos.hashCode(); + if (!tickable.isInitialized()) tickable.firstTick(entityLevel, blockState, pos); + + long time = entityLevel.getGameTime() - pos.hashCode(); tickable.tick(entityLevel, time, blockState, pos); - if (level.isClientSide()) { - tickable.clientTick((ClientLevel) level, time, state, pos); + + if (entityLevel.isClientSide()) { + tickable.clientTick((ClientLevel) entityLevel, time, blockState, pos); } else { - tickable.serverTick((ServerLevel) level, time, state, pos); + tickable.serverTick((ServerLevel) entityLevel, time, blockState, pos); } - if (!tickable.isInitialized()) tickable.firstTick(level, state, pos); } }; } diff --git a/src/main/java/net/xevianlight/aphelion/block/custom/base/StationEngineBlockEntity.java b/src/main/java/net/xevianlight/aphelion/block/custom/base/StationEngineBlockEntity.java new file mode 100644 index 0000000..646865e --- /dev/null +++ b/src/main/java/net/xevianlight/aphelion/block/custom/base/StationEngineBlockEntity.java @@ -0,0 +1,77 @@ +package net.xevianlight.aphelion.block.custom.base; + +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.xevianlight.aphelion.core.init.ModDimensions; +import net.xevianlight.aphelion.core.saveddata.SpacePartitionSavedData; +import net.xevianlight.aphelion.core.saveddata.types.PartitionData; + +import javax.annotation.Nullable; + +public abstract class StationEngineBlockEntity extends BlockEntity implements TickableBlockEntity { + + private boolean isInitialized = false; + private @Nullable PartitionData data; + + /** + * The travel speed in AU/tick. + */ + public abstract double getTravelSpeed(); + + protected StationEngineBlockEntity(BlockEntityType type, BlockPos pos, BlockState blockState) { + super(type, pos, blockState); + } + + @Override + public void clientTick(ClientLevel level, long time, BlockState state, BlockPos pos) { + + } + + /** + * Handles station travel logic for this engine. + * + *

If the associated station is currently traveling, this method advances + * its movement using the value returned by {@link #getTravelSpeed()}.

+ * + *

Subclasses may override this method to add additional server-side behavior. + * When doing so, {@code super.serverTick(...)} should be called to preserve + * the default travel logic.

+ * + *

This method is invoked once per server tick.

+ * + * @param level the server level the block entity exists in + * @param time the current tick time offset used for scheduling + * @param state the current block state + * @param pos the world position of the block entity + */ + @Override + public void serverTick(ServerLevel level, long time, BlockState state, BlockPos pos) { + double speed = getTravelSpeed(); + if (data != null) { + if (data.isTraveling()) { + data.travel(speed); + } + } + } + + @Override + public boolean isInitialized() { + return isInitialized; + } + + @Override + public void firstTick(Level level, BlockState state, BlockPos pos) { + if (level.isClientSide()) return; + if (level instanceof ServerLevel serverLevel) { + if (serverLevel.dimension() == ModDimensions.SPACE) { + data = SpacePartitionSavedData.get(serverLevel).getDataForBlockPos(pos); + } + } + isInitialized = true; + } +} diff --git a/src/main/java/net/xevianlight/aphelion/block/custom/base/StationRocketEngineBlockEntity.java b/src/main/java/net/xevianlight/aphelion/block/custom/base/StationRocketEngineBlockEntity.java new file mode 100644 index 0000000..da83d25 --- /dev/null +++ b/src/main/java/net/xevianlight/aphelion/block/custom/base/StationRocketEngineBlockEntity.java @@ -0,0 +1,32 @@ +package net.xevianlight.aphelion.block.custom.base; + +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.xevianlight.aphelion.core.saveddata.types.PartitionData; +import net.xevianlight.aphelion.util.Constants; + +public class StationRocketEngineBlockEntity extends StationEngineBlockEntity { + + /// Seconds to travel 1 AU + private final double SECONDS_PER_AU = 60; + /// AU per tick + private final double SPEED = 1/(SECONDS_PER_AU*20); + + @Override + public double getTravelSpeed() { + return SPEED; + } + + protected StationRocketEngineBlockEntity(BlockEntityType type, BlockPos pos, BlockState blockState) { + // TODO change type to ModBlockEntities.STATION_ROCKET_ENGINE_BLOCK_ENTITY.get() + super(type, pos, blockState); + } + + @Override + public void serverTick(ServerLevel level, long time, BlockState state, BlockPos pos) { + super.serverTick(level, time, state, pos); // IMPORTANT!!! + } +} diff --git a/src/main/java/net/xevianlight/aphelion/block/custom/base/TickableBlockEntity.java b/src/main/java/net/xevianlight/aphelion/block/custom/base/TickableBlockEntity.java index ae86af3..131f9f1 100644 --- a/src/main/java/net/xevianlight/aphelion/block/custom/base/TickableBlockEntity.java +++ b/src/main/java/net/xevianlight/aphelion/block/custom/base/TickableBlockEntity.java @@ -9,41 +9,74 @@ import net.minecraft.world.level.block.state.BlockState; public interface TickableBlockEntity { /** - * Runs on both the client AND server. - * @param level - * @param time - * @param state - * @param pos + * Runs on both the client and server once per tick. + * + *

This is intended for logic that is common to both sides. Side-specific logic + * should go in {@link #clientTick(ClientLevel, long, BlockState, BlockPos)} or + * {@link #serverTick(ServerLevel, long, BlockState, BlockPos)}.

+ * + * @param level the current level + * @param time a deterministic per-position tick time (see ticker implementation) + * @param state the current block state + * @param pos the world position of the block entity */ default void tick (Level level, long time, BlockState state, BlockPos pos) {}; /** - * Runs on the client only - * @param level - * @param time - * @param state - * @param pos + * Runs on the client only once per tick. + * + *

Use this for client-side visual updates, particles, sounds, animation state, + * or other logic that must not run on the logical server.

+ * + * @param level the client level + * @param time a deterministic per-position tick time (see ticker implementation) + * @param state the current block state + * @param pos the world position of the block entity */ void clientTick(ClientLevel level, long time, BlockState state, BlockPos pos); /** - * Runs on the server only - * @param level - * @param time - * @param state - * @param pos + * Runs on the server only once per tick. + * + *

Use this for authoritative game logic such as inventory processing, energy + * generation/consumption, entity spawning, saving state, and network sync triggers.

+ * + * @param level the server level + * @param time a deterministic per-position tick time (see ticker implementation) + * @param state the current block state + * @param pos the world position of the block entity */ void serverTick(ServerLevel level, long time, BlockState state, BlockPos pos); + /** + * Returns whether this object has completed its initialization logic. + * + *

If this method returns {@code false}, {@link #firstTick(Level, BlockState, BlockPos)} + * will be invoked at the start of each tick on both the client and server until + * initialization is complete.

+ * + *

Implementations should return {@code true} once initialization has finished + * to prevent {@code firstTick} from running again.

+ * + * @return {@code true} if initialization has completed, {@code false} otherwise + */ default boolean isInitialized() { return true; - }; + } /** - * Runs on client AND server, once only. - * @param level - * @param state - * @param pos + * Performs initialization logic for this object. + * + *

This method is called at the start of each tick on both the client and server + * whenever {@link #isInitialized()} returns {@code false}. It will continue to be + * invoked every tick until initialization is complete.

+ * + *

Implementations should perform any required setup and ensure that + * {@code isInitialized()} returns {@code true} afterward.

+ * + * @param level the level the block entity exists in + * @param state the current block state + * @param pos the world position of the block entity */ void firstTick(Level level, BlockState state, BlockPos pos); diff --git a/src/main/java/net/xevianlight/aphelion/block/entity/custom/RocketAssemblerBlockEntity.java b/src/main/java/net/xevianlight/aphelion/block/entity/custom/RocketAssemblerBlockEntity.java index cb90668..44d4b15 100644 --- a/src/main/java/net/xevianlight/aphelion/block/entity/custom/RocketAssemblerBlockEntity.java +++ b/src/main/java/net/xevianlight/aphelion/block/entity/custom/RocketAssemblerBlockEntity.java @@ -21,6 +21,9 @@ import net.xevianlight.aphelion.Aphelion; import net.xevianlight.aphelion.block.custom.base.TickableBlockEntity; import net.xevianlight.aphelion.core.init.ModBlockEntities; import net.xevianlight.aphelion.core.init.ModBlocks; +import net.xevianlight.aphelion.core.init.ModDimensions; +import net.xevianlight.aphelion.core.saveddata.SpacePartitionSavedData; +import net.xevianlight.aphelion.core.saveddata.types.PartitionData; import net.xevianlight.aphelion.entites.vehicles.RocketEntity; import net.xevianlight.aphelion.util.AphelionBlockStateProperties; import net.xevianlight.aphelion.util.ModTags; @@ -38,6 +41,8 @@ public class RocketAssemblerBlockEntity extends BlockEntity implements TickableB private PadInfo padBounds; RocketEntity lastRocket; + private @Nullable PartitionData data; + public @Nullable PadInfo getPadBounds() { return padBounds; } @@ -60,9 +65,19 @@ public class RocketAssemblerBlockEntity extends BlockEntity implements TickableB return dx * dy * dz; } + + public BlockPos getCenter() { + int centerX = (min.getX() + max.getX()) / 2; + int centerZ = (min.getZ() + max.getZ()) / 2; + + // bottom Y level + int y = min.getY(); + + return new BlockPos(centerX, y, centerZ); + } } - private final Block TOWER_BLOCK = ModBlocks.BLOCK_STEEL.get(); + private static final Block TOWER_BLOCK = ModBlocks.BLOCK_STEEL.get(); public BlockPos towerBasePos; private boolean connected(BlockState state, Direction dir) { @@ -318,23 +333,44 @@ public class RocketAssemblerBlockEntity extends BlockEntity implements TickableB boolean formed = newBounds != null; if (state.getValue(AphelionBlockStateProperties.FORMED) != formed) { level.setBlockAndUpdate(pos, state.setValue(AphelionBlockStateProperties.FORMED, formed)); + if (data != null) { + if (formed) { + data.addLandingPadController(pos); + } else { + data.removeLandingPadController(pos); + } + } } } @Override public void firstTick(Level level, BlockState state, BlockPos pos) { + if (level.isClientSide()) return; + facing = getBlockState().getValue(BlockStateProperties.HORIZONTAL_FACING); padScanStart = getBlockPos().mutable().below().relative(facing.getOpposite()); + + if (level instanceof ServerLevel serverLevel) { + if (serverLevel.dimension() == ModDimensions.SPACE) { + data = SpacePartitionSavedData.get(serverLevel).getDataForBlockPos(pos); + } + } + this.isInitialized = true; } + @Override + public void onRemoved() { + if (data == null) return; + data.removeLandingPadController(worldPosition); + } private static boolean isPad(BlockState s) { return s.is(ModTags.Blocks.LAUNCH_PAD); // or s.getBlock() == ModBlocks.PAD.get() } private static boolean isTower(BlockState s) { - return s.is(ModBlocks.BLOCK_STEEL); + return s.is(TOWER_BLOCK); } @Override diff --git a/src/main/java/net/xevianlight/aphelion/block/entity/custom/renderer/RocketAssemblerBlockEntityRenderer.java b/src/main/java/net/xevianlight/aphelion/block/entity/custom/renderer/RocketAssemblerBlockEntityRenderer.java index 3d93480..08b9825 100644 --- a/src/main/java/net/xevianlight/aphelion/block/entity/custom/renderer/RocketAssemblerBlockEntityRenderer.java +++ b/src/main/java/net/xevianlight/aphelion/block/entity/custom/renderer/RocketAssemblerBlockEntityRenderer.java @@ -1,14 +1,18 @@ package net.xevianlight.aphelion.block.entity.custom.renderer; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.blaze3d.vertex.VertexFormat; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.LevelRenderer; import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderStateShard; import net.minecraft.client.renderer.RenderType; import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; import net.minecraft.world.phys.AABB; import net.xevianlight.aphelion.block.entity.custom.RocketAssemblerBlockEntity; import org.jetbrains.annotations.NotNull; @@ -37,6 +41,21 @@ public class RocketAssemblerBlockEntityRenderer implements BlockEntityRenderer { - int x = SpacePartitionHelper.get(ColumnPosArgument.getColumnPos(context, "pos").x()); - int z = SpacePartitionHelper.get(ColumnPosArgument.getColumnPos(context, "pos").z()); + int x = SpacePartition.get(ColumnPosArgument.getColumnPos(context, "pos").x()); + int z = SpacePartition.get(ColumnPosArgument.getColumnPos(context, "pos").z()); ResourceLocation orbit = ResourceLocationArgument.getId(context, "orbit"); ServerLevel level = context.getSource().getLevel(); @@ -85,8 +82,8 @@ public class AphelionCommand { .then(Commands.literal("get") .then(Commands.argument("pos", ColumnPosArgument.columnPos()) .executes(context -> { - int x = SpacePartitionHelper.get(ColumnPosArgument.getColumnPos(context, "pos").x()); - int z = SpacePartitionHelper.get(ColumnPosArgument.getColumnPos(context, "pos").z()); + int x = SpacePartition.get(ColumnPosArgument.getColumnPos(context, "pos").x()); + int z = SpacePartition.get(ColumnPosArgument.getColumnPos(context, "pos").z()); ServerLevel level = context.getSource().getLevel(); ResourceLocation orbit = SpacePartitionSavedData.get(level).getOrbitForPartition(x, z); @@ -110,8 +107,8 @@ public class AphelionCommand { .then(Commands.literal("clear") .then(Commands.argument("pos", ColumnPosArgument.columnPos()) .executes(context -> { - int x = SpacePartitionHelper.get(ColumnPosArgument.getColumnPos(context, "pos").x()); - int z = SpacePartitionHelper.get(ColumnPosArgument.getColumnPos(context, "pos").z()); + int x = SpacePartition.get(ColumnPosArgument.getColumnPos(context, "pos").x()); + int z = SpacePartition.get(ColumnPosArgument.getColumnPos(context, "pos").z()); ServerLevel level = context.getSource().getLevel(); @@ -148,8 +145,8 @@ public class AphelionCommand { .executes(context -> { ServerLevel level = context.getSource().getLevel(); - int x = SpacePartitionHelper.get(ColumnPosArgument.getColumnPos(context,"pos").x()); - int z = SpacePartitionHelper.get(ColumnPosArgument.getColumnPos(context,"pos").z()); + int x = SpacePartition.get(ColumnPosArgument.getColumnPos(context,"pos").x()); + int z = SpacePartition.get(ColumnPosArgument.getColumnPos(context,"pos").z()); long key = SpacePartitionSavedData.pack(x,z); @@ -195,7 +192,7 @@ public class AphelionCommand { int x = ColumnPosArgument.getColumnPos(context, "pos").x(); int z = ColumnPosArgument.getColumnPos(context, "pos").z(); - String stationCoord = SpacePartitionHelper.get(x) + " " + SpacePartitionHelper.get(z); + String stationCoord = SpacePartition.get(x) + " " + SpacePartition.get(z); Component clickableOutput = getClickablePos(stationCoord, ChatFormatting.GREEN); @@ -217,8 +214,8 @@ public class AphelionCommand { double x = (double) IntegerArgumentType.getInteger(context, "x"); double z = (double) IntegerArgumentType.getInteger(context, "z"); - int destX = (int) Math.floor(x * SpacePartitionHelper.SIZE) + (SpacePartitionHelper.SIZE / 2); - int destZ = (int) Math.floor(z * SpacePartitionHelper.SIZE) + (SpacePartitionHelper.SIZE / 2); + int destX = (int) Math.floor(x * SpacePartition.SIZE) + (SpacePartition.SIZE / 2); + int destZ = (int) Math.floor(z * SpacePartition.SIZE) + (SpacePartition.SIZE / 2); String stationCoord = x + ", " + z; @@ -255,8 +252,8 @@ public class AphelionCommand { .then(Commands.argument("pos", ColumnPosArgument.columnPos()) .then(Commands.argument("id", ResourceLocationArgument.id()) .executes(context -> { - int px = SpacePartitionHelper.get(ColumnPosArgument.getColumnPos(context, "pos").x()); - int pz = SpacePartitionHelper.get(ColumnPosArgument.getColumnPos(context, "pos").z()); + int px = SpacePartition.get(ColumnPosArgument.getColumnPos(context, "pos").x()); + int pz = SpacePartition.get(ColumnPosArgument.getColumnPos(context, "pos").z()); ResourceLocation orbit = ResourceLocationArgument.getId(context, "id"); ServerLevel level = context.getSource().getLevel(); @@ -277,8 +274,8 @@ public class AphelionCommand { .then(Commands.literal("get") .then(Commands.argument("pos", ColumnPosArgument.columnPos()) .executes(context -> { - int px = SpacePartitionHelper.get(ColumnPosArgument.getColumnPos(context, "pos").x()); - int pz = SpacePartitionHelper.get(ColumnPosArgument.getColumnPos(context, "pos").z()); + int px = SpacePartition.get(ColumnPosArgument.getColumnPos(context, "pos").x()); + int pz = SpacePartition.get(ColumnPosArgument.getColumnPos(context, "pos").z()); ServerLevel level = context.getSource().getLevel(); PartitionData data = SpacePartitionSavedData.get(level).getData(px, pz); @@ -306,8 +303,8 @@ public class AphelionCommand { .then(Commands.argument("pos", ColumnPosArgument.columnPos()) .then(Commands.argument("player", GameProfileArgument.gameProfile()) .executes(context -> { - int px = SpacePartitionHelper.get(ColumnPosArgument.getColumnPos(context, "pos").x()); - int pz = SpacePartitionHelper.get(ColumnPosArgument.getColumnPos(context, "pos").z()); + int px = SpacePartition.get(ColumnPosArgument.getColumnPos(context, "pos").x()); + int pz = SpacePartition.get(ColumnPosArgument.getColumnPos(context, "pos").z()); ServerLevel level = context.getSource().getLevel(); PartitionData data = SpacePartitionSavedData.get(level).getData(px, pz); diff --git a/src/main/java/net/xevianlight/aphelion/core/init/ModDimensions.java b/src/main/java/net/xevianlight/aphelion/core/init/ModDimensions.java new file mode 100644 index 0000000..f999ee6 --- /dev/null +++ b/src/main/java/net/xevianlight/aphelion/core/init/ModDimensions.java @@ -0,0 +1,10 @@ +package net.xevianlight.aphelion.core.init; + +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.Level; +import net.xevianlight.aphelion.Aphelion; + +public final class ModDimensions { + public static final ResourceKey SPACE = ResourceKey.create(Registries.DIMENSION, Aphelion.id("space")); +} diff --git a/src/main/java/net/xevianlight/aphelion/core/saveddata/SpacePartitionSavedData.java b/src/main/java/net/xevianlight/aphelion/core/saveddata/SpacePartitionSavedData.java index b0300dc..209f25c 100644 --- a/src/main/java/net/xevianlight/aphelion/core/saveddata/SpacePartitionSavedData.java +++ b/src/main/java/net/xevianlight/aphelion/core/saveddata/SpacePartitionSavedData.java @@ -2,6 +2,7 @@ package net.xevianlight.aphelion.core.saveddata; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.minecraft.core.BlockPos; import net.minecraft.core.HolderLookup; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; @@ -10,6 +11,7 @@ import net.minecraft.server.level.ServerLevel; import net.minecraft.world.level.saveddata.SavedData; import net.xevianlight.aphelion.Aphelion; import net.xevianlight.aphelion.core.saveddata.types.PartitionData; +import net.xevianlight.aphelion.util.SpacePartition; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -54,10 +56,10 @@ public class SpacePartitionSavedData extends SavedData { // Distances (optional; default 0.0) if (e.contains("DistanceTraveled", CompoundTag.TAG_DOUBLE)) { - pd.setDistanceTraveled(e.getDouble("DistanceTraveled")); + pd.setDistanceTraveledAU(e.getDouble("DistanceTraveled")); } if (e.contains("DistanceToDest", CompoundTag.TAG_DOUBLE)) { - pd.setDistanceToDest(e.getDouble("DistanceToDest")); + pd.setTripDistanceAU(e.getDouble("DistanceToDest")); } if (e.hasUUID("Owner")) { @@ -102,8 +104,8 @@ public class SpacePartitionSavedData extends SavedData { // Traveling + distances e.putBoolean("Traveling", pd.isTraveling()); - e.putDouble("DistanceTraveled", pd.getDistanceTraveled()); - e.putDouble("DistanceToDest", pd.getDistanceToDest()); + e.putDouble("DistanceTraveled", pd.getDistanceTraveledAU()); + e.putDouble("DistanceToDest", pd.getTripDistanceAU()); if (pd.getOwner() != null) { e.putUUID("Owner", pd.getOwner()); @@ -158,15 +160,23 @@ public class SpacePartitionSavedData extends SavedData { } /** - * Gets the mutable PartitionData object stored at px, pz - * @param px - * @param pz - * @return + * Returns the {@link PartitionData} stored at the given partition indices. + * + *

The returned {@code PartitionData} is mutable. Any modifications to the returned + * object will be persisted to the server.

+ * + *

If no {@code PartitionData} exists at the specified indices, a new instance is + * created using {@code aphelion:orbit/default}, stored in the server cache, and returned.

+ * + * @param px the partition X index + * @param pz the partition Z index + * @return the {@code PartitionData} associated with the specified partition indices */ - public @Nullable PartitionData getData(int px, int pz) { + public @NotNull PartitionData getData(int px, int pz) { long key = pack(px, pz); PartitionData data = map.get(key); if (data == null) { + // pick a sensible default orbit, or null if you truly allow it data = new PartitionData(Aphelion.id("orbit/default")); map.put(key, data); @@ -175,6 +185,61 @@ public class SpacePartitionSavedData extends SavedData { return data; } + /** + * Returns the {@link PartitionData} for the partition containing the given world position. + * + *

The returned {@code PartitionData} is mutable. Any modifications to the returned + * object will be persisted to the server.

+ * + *

If no {@code PartitionData} exists for the partition containing the specified + * position, a new instance is created using {@code aphelion:orbit/default}, + * stored in the server cache, and returned.

+ * + * @param x the world X coordinate + * @param z the world Z coordinate + * @return the {@code PartitionData} associated with the partition containing the position + */ + public @NotNull PartitionData getDataForPos(int x, int z) { + int px = SpacePartition.get(x); + int pz = SpacePartition.get(z); + long key = pack(px, pz); + PartitionData data = map.get(key); + if (data == null) { + + data = new PartitionData(Aphelion.id("orbit/default")); + map.put(key, data); + setDirty(); + } + return data; + } + + /** + * Returns the {@link PartitionData} for the partition containing the given block position. + * + *

The returned {@code PartitionData} is mutable. Any modifications to the returned + * object will be persisted to the server.

+ * + *

If no {@code PartitionData} exists for the partition containing the specified + * position, a new instance is created using {@code aphelion:orbit/default}, + * stored in the server cache, and returned.

+ * + * @param pos the world block position + * @return the {@code PartitionData} associated with the partition containing the position + */ + public @NotNull PartitionData getDataForBlockPos(BlockPos pos) { + int px = SpacePartition.get(pos.getX()); + int pz = SpacePartition.get(pos.getZ()); + long key = pack(px, pz); + PartitionData data = map.get(key); + if (data == null) { + + data = new PartitionData(Aphelion.id("orbit/default")); + map.put(key, data); + setDirty(); + } + return data; + } + public void overwriteAllExistingOrbits(ResourceLocation orbit) { if (map.isEmpty()) return; diff --git a/src/main/java/net/xevianlight/aphelion/core/saveddata/types/PartitionData.java b/src/main/java/net/xevianlight/aphelion/core/saveddata/types/PartitionData.java index ee3ea8b..25a80bf 100644 --- a/src/main/java/net/xevianlight/aphelion/core/saveddata/types/PartitionData.java +++ b/src/main/java/net/xevianlight/aphelion/core/saveddata/types/PartitionData.java @@ -3,17 +3,13 @@ package net.xevianlight.aphelion.core.saveddata.types; import io.netty.buffer.ByteBuf; import net.minecraft.core.BlockPos; import net.minecraft.core.UUIDUtil; -import net.minecraft.nbt.CompoundTag; import net.minecraft.network.codec.ByteBufCodecs; import net.minecraft.network.codec.StreamCodec; import net.minecraft.resources.ResourceLocation; import net.xevianlight.aphelion.util.BigCodec; import org.jetbrains.annotations.Nullable; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.UUID; +import java.util.*; public class PartitionData { public static final int MAX_PADS = 64; @@ -22,32 +18,37 @@ public class PartitionData { @Nullable private ResourceLocation orbit; @Nullable private ResourceLocation destination; private boolean traveling; - private double distanceTraveled; - private double distanceToDest; + /// How far we've already gone + private double distanceTraveledAU; + /// Total trip distance, from start to finish + private double tripDistanceAU; private boolean generated; private UUID owner; private List landingPadControllers; + private List engines; public PartitionData(@Nullable ResourceLocation orbit) { this.orbit = orbit; this.destination = null; this.traveling = false; - this.distanceTraveled = 0; - this.distanceToDest = 0; + this.distanceTraveledAU = 0; + this.tripDistanceAU = 0; this.generated = false; this.owner = null; this.landingPadControllers = List.of(); + this.engines = List.of(); } public PartitionData(PartitionData other) { this.orbit = other.orbit; this.destination = other.destination; this.traveling = other.traveling; - this.distanceTraveled = other.distanceTraveled; - this.distanceToDest = other.distanceToDest; + this.distanceTraveledAU = other.distanceTraveledAU; + this.tripDistanceAU = other.tripDistanceAU; this.generated = other.generated; this.owner = other.owner; this.landingPadControllers = other.landingPadControllers; + this.engines = other.engines; } public static final StreamCodec STREAM_CODEC = @@ -64,10 +65,10 @@ public class PartitionData { // doubles -> DOUBLE codec ByteBufCodecs.DOUBLE, - PartitionData::getDistanceTraveled, + PartitionData::getDistanceTraveledAU, ByteBufCodecs.DOUBLE, - PartitionData::getDistanceToDest, + PartitionData::getTripDistanceAU, ByteBufCodecs.optional(UUIDUtil.STREAM_CODEC), d -> Optional.ofNullable(d.getOwner()), @@ -78,15 +79,19 @@ public class PartitionData { BLOCKPOS_LIST_CODEC, PartitionData::getLandingPadControllers, - (orbitOpt, destOpt, traveling, distTraveled, distToDest, ownerOpt, generated, controllers) -> { + BLOCKPOS_LIST_CODEC, + PartitionData::getEngines, + + (orbitOpt, destOpt, traveling, distTraveled, distToDest, ownerOpt, generated, controllers, engines) -> { PartitionData data = new PartitionData(orbitOpt.orElse(null)); data.destination = destOpt.orElse(null); data.traveling = traveling; - data.distanceTraveled = distTraveled; - data.distanceToDest = distToDest; + data.distanceTraveledAU = distTraveled; + data.tripDistanceAU = distToDest; data.owner = ownerOpt.orElse(null); data.generated = generated; data.landingPadControllers = controllers; + data.engines = engines; return data; } ); @@ -115,24 +120,35 @@ public class PartitionData { this.traveling = traveling; } - public double getDistanceTraveled() { - return distanceTraveled; + public double getDistanceTraveledAU() { + return distanceTraveledAU; } - public void setDistanceTraveled(double distanceTraveled) { - this.distanceTraveled = distanceTraveled; + public void setDistanceTraveledAU(double distanceTraveledAU) { + this.distanceTraveledAU = distanceTraveledAU; } - public double getDistanceToDest() { - return distanceToDest; + public double getTripDistanceAU() { + return tripDistanceAU; } - public void setDistanceToDest(double distanceToDest) { - this.distanceToDest = distanceToDest; + public void setTripDistanceAU(double tripDistanceAU) { + this.tripDistanceAU = tripDistanceAU; } + /** + * Advances travel progress by the specified distance in AU. + * + *

This increases {@code distanceTraveledAU} by the given amount and clamps + * the result so it never exceeds {@code tripDistanceAU}.

+ * + *

If the requested distance would overshoot the destination, the traveled + * distance is set to exactly {@code tripDistanceAU}.

+ * + * @param distance the distance to advance in astronomical units (AU) + */ public void travel(double distance) { - distanceTraveled = Math.min( distanceTraveled + distance, distanceToDest); + distanceTraveledAU = Math.min(distanceTraveledAU + distance, tripDistanceAU); } public boolean isGenerated() { @@ -151,14 +167,40 @@ public class PartitionData { this.owner = owner; } + /** + * Returns a copy of the world positions of all landing pad controllers tracked + * by this partition. + * + *

This method returns only the stored {@link BlockPos} locations of known + * landing pad controllers, not the controller instances or block entities + * themselves. To interact with a controller, retrieve its block entity from + * the world using the returned positions.

+ * + *

The returned list is a defensive copy and may be modified without affecting + * the underlying partition data. To persist changes, use + * {@code setLandingPadControllers(...)}.

+ * + * @return a mutable copy of the landing pad controller positions known to this partition + */ public List getLandingPadControllers() { - return landingPadControllers; + return new ArrayList<>(landingPadControllers); } public void setLandingPadControllers(List landingPadControllers) { this.landingPadControllers = landingPadControllers; } + + /** + * Adds a landing pad controller at the specified world position. + * + *

If a controller does not already exist at the given position, it is added + * to the internal collection and the method returns {@code true}. If a controller + * is already present at that position, no changes are made.

+ * + * @param pos the world position of the landing pad controller to add + * @return {@code true} if the controller was added, {@code false} if it already existed + */ public boolean addLandingPadController(BlockPos pos) { if (!landingPadControllers.contains(pos)) { landingPadControllers.add(pos); @@ -167,10 +209,40 @@ public class PartitionData { return false; } + /** + * Removes the landing pad controller at the specified world position. + * + *

If a controller exists at the given position, it is removed from the + * internal collection and the method returns {@code true}. If no controller + * is present at that position, no changes are made.

+ * + * @param pos the world position of the landing pad controller to remove + * @return {@code true} if a controller was removed, {@code false} otherwise + */ public boolean removeLandingPadController(BlockPos pos) { return landingPadControllers.remove(pos); } + public List getEngines() { + return engines; + } + + public void setEngines(List engines) { + this.engines = engines; + } + + public boolean addEngine(BlockPos pos) { + if (!engines.contains(pos)) { + engines.add(pos); + return true; + } + return false; + } + + public boolean removeEngine(BlockPos pos) { + return engines.remove(pos); + } + @Override public boolean equals(Object obj) { if (this == obj) return true; @@ -181,8 +253,8 @@ public class PartitionData { return Objects.equals(this.orbit, that.orbit) && Objects.equals(this.destination, that.destination) && this.traveling == that.traveling - && Double.compare(this.distanceTraveled, that.distanceTraveled) == 0 - && Double.compare(this.distanceToDest, that.distanceToDest) == 0 + && Double.compare(this.distanceTraveledAU, that.distanceTraveledAU) == 0 + && Double.compare(this.tripDistanceAU, that.tripDistanceAU) == 0 && this.generated == that.generated && Objects.equals(this.owner, that.owner); } diff --git a/src/main/java/net/xevianlight/aphelion/network/PartitionSync.java b/src/main/java/net/xevianlight/aphelion/network/PartitionSync.java index 186807c..f801208 100644 --- a/src/main/java/net/xevianlight/aphelion/network/PartitionSync.java +++ b/src/main/java/net/xevianlight/aphelion/network/PartitionSync.java @@ -10,7 +10,7 @@ import net.xevianlight.aphelion.Aphelion; import net.xevianlight.aphelion.core.saveddata.SpacePartitionSavedData; import net.xevianlight.aphelion.core.saveddata.types.PartitionData; import net.xevianlight.aphelion.network.packet.PartitionPayload; -import net.xevianlight.aphelion.util.SpacePartitionHelper; +import net.xevianlight.aphelion.util.SpacePartition; import java.util.UUID; @@ -42,8 +42,8 @@ public final class PartitionSync { } private static PartitionPayload computePartitionFor(ServerPlayer sp) { - int px = (int)Math.floor(sp.getX() / SpacePartitionHelper.SIZE); - int pz = (int)Math.floor(sp.getZ() / SpacePartitionHelper.SIZE); + int px = (int)Math.floor(sp.getX() / SpacePartition.SIZE); + int pz = (int)Math.floor(sp.getZ() / SpacePartition.SIZE); PartitionData live = SpacePartitionSavedData.get(sp.serverLevel()).getData(px, pz); diff --git a/src/main/java/net/xevianlight/aphelion/util/SpacePartitionHelper.java b/src/main/java/net/xevianlight/aphelion/util/SpacePartition.java similarity index 87% rename from src/main/java/net/xevianlight/aphelion/util/SpacePartitionHelper.java rename to src/main/java/net/xevianlight/aphelion/util/SpacePartition.java index 5d46e3c..edceede 100644 --- a/src/main/java/net/xevianlight/aphelion/util/SpacePartitionHelper.java +++ b/src/main/java/net/xevianlight/aphelion/util/SpacePartition.java @@ -1,6 +1,6 @@ package net.xevianlight.aphelion.util; -public class SpacePartitionHelper { +public class SpacePartition { public static final int SIZE = 16;