Environment Data saving and Floodfill. Added additional data to PartitionPayload and PartitionData for later

This commit is contained in:
XevianLight
2026-01-29 21:43:18 -07:00
parent f3bd3f891a
commit b012528247
30 changed files with 901 additions and 159 deletions

2
.gitignore vendored
View File

@@ -23,4 +23,4 @@ run
runs
run-data
repo
repo

View File

@@ -0,0 +1,64 @@
{
"variants": {
"facing=east,formed=false,lit=false": {
"model": "aphelion:block/electric_arc_furnace",
"y": 90
},
"facing=east,formed=false,lit=true": {
"model": "aphelion:block/electric_arc_furnace",
"y": 90
},
"facing=east,formed=true,lit=false": {
"model": "aphelion:block/electric_arc_furnace",
"y": 90
},
"facing=east,formed=true,lit=true": {
"model": "aphelion:block/electric_arc_furnace",
"y": 90
},
"facing=north,formed=false,lit=false": {
"model": "aphelion:block/electric_arc_furnace"
},
"facing=north,formed=false,lit=true": {
"model": "aphelion:block/electric_arc_furnace"
},
"facing=north,formed=true,lit=false": {
"model": "aphelion:block/electric_arc_furnace"
},
"facing=north,formed=true,lit=true": {
"model": "aphelion:block/electric_arc_furnace"
},
"facing=south,formed=false,lit=false": {
"model": "aphelion:block/electric_arc_furnace",
"y": 180
},
"facing=south,formed=false,lit=true": {
"model": "aphelion:block/electric_arc_furnace",
"y": 180
},
"facing=south,formed=true,lit=false": {
"model": "aphelion:block/electric_arc_furnace",
"y": 180
},
"facing=south,formed=true,lit=true": {
"model": "aphelion:block/electric_arc_furnace",
"y": 180
},
"facing=west,formed=false,lit=false": {
"model": "aphelion:block/electric_arc_furnace",
"y": 270
},
"facing=west,formed=false,lit=true": {
"model": "aphelion:block/electric_arc_furnace",
"y": 270
},
"facing=west,formed=true,lit=false": {
"model": "aphelion:block/electric_arc_furnace",
"y": 270
},
"facing=west,formed=true,lit=true": {
"model": "aphelion:block/electric_arc_furnace",
"y": 270
}
}
}

View File

@@ -0,0 +1,8 @@
{
"parent": "minecraft:block/orientable",
"textures": {
"front": "aphelion:block/electric_arc_furnace_front",
"side": "minecraft:block/blast_furnace_side",
"top": "minecraft:block/blast_furnace_top"
}
}

View File

@@ -0,0 +1,3 @@
{
"parent": "aphelion:block/arc_furnace_casing"
}

View File

@@ -0,0 +1,3 @@
{
"parent": "aphelion:block/vacuum_arc_furnace_controller"
}

View File

@@ -0,0 +1,21 @@
{
"type": "minecraft:block",
"pools": [
{
"bonus_rolls": 0.0,
"conditions": [
{
"condition": "minecraft:survives_explosion"
}
],
"entries": [
{
"type": "minecraft:item",
"name": "aphelion:arc_furnace_casing"
}
],
"rolls": 1.0
}
],
"random_sequence": "aphelion:blocks/arc_furnace_casing"
}

View File

@@ -0,0 +1,21 @@
{
"type": "minecraft:block",
"pools": [
{
"bonus_rolls": 0.0,
"conditions": [
{
"condition": "minecraft:survives_explosion"
}
],
"entries": [
{
"type": "minecraft:item",
"name": "aphelion:launch_pad"
}
],
"rolls": 1.0
}
],
"random_sequence": "aphelion:blocks/launch_pad"
}

View File

@@ -0,0 +1,21 @@
{
"type": "minecraft:block",
"pools": [
{
"bonus_rolls": 0.0,
"conditions": [
{
"condition": "minecraft:survives_explosion"
}
],
"entries": [
{
"type": "minecraft:item",
"name": "aphelion:vacuum_arc_furnace_controller"
}
],
"rolls": 1.0
}
],
"random_sequence": "aphelion:blocks/vacuum_arc_furnace_controller"
}

View File

@@ -0,0 +1,21 @@
{
"type": "minecraft:block",
"pools": [
{
"bonus_rolls": 0.0,
"conditions": [
{
"condition": "minecraft:survives_explosion"
}
],
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:air"
}
],
"rolls": 1.0
}
],
"random_sequence": "aphelion:blocks/vaf_dummy_block"
}

View File

@@ -0,0 +1,5 @@
{
"values": [
"aphelion:launch_pad"
]
}

View File

@@ -142,7 +142,7 @@ public class BaseMultiblockDummyBlockEntity extends BlockEntity implements IMult
// Force rerender on client
if (level != null) {
level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3);
requestModelDataUpdate(); // if you rely on model data
requestModelDataUpdate(); // if you rely on model partitionData
}
}
@@ -179,7 +179,7 @@ public class BaseMultiblockDummyBlockEntity extends BlockEntity implements IMult
setChanged();
if (level != null) {
level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3);
requestModelDataUpdate(); // only if you use model data
requestModelDataUpdate(); // only if you use model partitionData
}
}
}

View File

@@ -11,7 +11,7 @@ import net.xevianlight.aphelion.Aphelion;
import net.xevianlight.aphelion.client.dimension.DimensionRenderer;
import net.xevianlight.aphelion.client.dimension.DimensionRendererCache;
import net.xevianlight.aphelion.client.dimension.SpaceSkyEffects;
import net.xevianlight.aphelion.core.space.SpacePartitionSavedData;
import net.xevianlight.aphelion.core.saveddata.SpacePartitionSavedData;
import net.xevianlight.aphelion.util.SpacePartitionHelper;
@EventBusSubscriber(modid = Aphelion.MOD_ID, value = Dist.CLIENT)
@@ -50,5 +50,6 @@ public class AphelionDebugOverlay {
event.getLeft().add(" Orbit: " + orbitId);
// event.getLeft().add(" Sky: " + rendererSummary);
event.getLeft().add(" Station: " + x + " " + z + " ID: " + SpacePartitionSavedData.pack(x,z));
event.getLeft().add(" Station Destination:" + PartitionClientState.lastData().getDestination());
}
}

View File

@@ -1,5 +1,6 @@
package net.xevianlight.aphelion.client;
import net.xevianlight.aphelion.core.saveddata.types.PartitionData;
import net.xevianlight.aphelion.network.packet.PartitionPayload;
import java.util.Optional;
@@ -14,7 +15,15 @@ public final class PartitionClientState {
}
public static String idOrUnknown() {
return last != null ? last.id() : "unknown";
String orbit = String.valueOf(last.partitionData().getOrbit());
if (orbit == null) {
return "aphleion:orbit/default";
}
return last != null ? orbit : "unknown";
}
public static PartitionData lastData() {
return last.partitionData();
}
//
// public static int pxOr(int fallback) {

View File

@@ -71,7 +71,7 @@ public class DimensionSkyEffects extends DimensionSpecialEffects {
// int py = PartitionClientState.pyOr(0);
var data = ResourceLocation.parse(PartitionClientState.idOrUnknown());
// var data = SpacePartitionSavedData.get(serverLevel).getOrbitForPartition((int) x, (int) z);
// var partitionData = SpacePartitionSavedData.get(serverLevel).getOrbitForPartition((int) x, (int) z);
if (data != null) return data;
return ResourceLocation.fromNamespaceAndPath(Aphelion.MOD_ID, "orbit/default");

View File

@@ -5,12 +5,9 @@ import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.DimensionSpecialEffects;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.phys.Vec3;
import net.xevianlight.aphelion.Aphelion;
import net.xevianlight.aphelion.client.PartitionClientState;
import net.xevianlight.aphelion.core.space.SpacePartitionSavedData;
import net.xevianlight.aphelion.util.SpacePartitionHelper;
import org.jetbrains.annotations.Nullable;
import org.joml.Matrix4f;
@@ -29,7 +26,7 @@ public class SpaceSkyEffects extends DimensionSpecialEffects {
return ResourceLocation.withDefaultNamespace("overworld");
}
if (effectsId.equals(Aphelion.id("space"))) {
return SpaceSkyEffects.orbitForPos(camera.getPosition()); // or inline this logic
return orbitForPos(camera.getPosition()); // or inline this logic
}
return effectsId;
}
@@ -80,7 +77,7 @@ public class SpaceSkyEffects extends DimensionSpecialEffects {
// int py = PartitionClientState.pyOr(0);
var data = ResourceLocation.parse(PartitionClientState.idOrUnknown());
// var data = SpacePartitionSavedData.get(serverLevel).getOrbitForPartition((int) x, (int) z);
// var partitionData = SpacePartitionSavedData.get(serverLevel).getOrbitForPartition((int) x, (int) z);
if (data != null) return data;
return ResourceLocation.fromNamespaceAndPath(Aphelion.MOD_ID, "orbit/default");

View File

@@ -15,17 +15,18 @@ import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.*;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ColumnPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.RelativeMovement;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Blocks;
import net.xevianlight.aphelion.Aphelion;
import net.xevianlight.aphelion.core.space.SpacePartitionSavedData;
import net.xevianlight.aphelion.core.saveddata.SpacePartitionSavedData;
import net.xevianlight.aphelion.entites.vehicles.RocketEntity;
import net.xevianlight.aphelion.planet.Planet;
import net.xevianlight.aphelion.util.RocketStructure;
import net.xevianlight.aphelion.util.SpacePartitionHelper;
import net.xevianlight.aphelion.util.registries.ModRegistries;
import org.jetbrains.annotations.NotNull;
import java.util.EnumSet;
@@ -244,6 +245,24 @@ public class AphelionCommand {
)
)
)
.then(Commands.literal("destination")
.then(Commands.literal("set").then(
Commands.argument("pos", ColumnPosArgument.columnPos())
.then(Commands.argument("id", ResourceLocationArgument.id())
.executes(context -> {
int x = SpacePartitionHelper.get(ColumnPosArgument.getColumnPos(context, "pos").x());
int z = SpacePartitionHelper.get(ColumnPosArgument.getColumnPos(context, "pos").z());
ResourceLocation orbit = ResourceLocationArgument.getId(context, "id");
ServerLevel level = context.getSource().getLevel();
SpacePartitionSavedData.get(level).getData(x,z).setDestination(orbit);
return 1;
})
)
)
)
)
)
.then(Commands.literal("planet")
.then(Commands.literal("tp")

View File

@@ -0,0 +1,129 @@
package net.xevianlight.aphelion.core.saveddata;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import net.minecraft.core.BlockPos;
import net.minecraft.core.HolderLookup;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.Tag;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.saveddata.SavedData;
import net.xevianlight.aphelion.core.saveddata.types.EnvironmentData;
import org.jetbrains.annotations.NotNull;
import java.util.Map;
/**
* Pattern:
* - World-level SavedData
* - Outer map keyed by section (chunkX, sectionY, chunkZ) packed into a long
* - Inner map keyed by localIndex (0..4095) -> packed int env value
*
* Sparse by design: blocks not present in the inner map are implicitly "default environment".
*/
public class EnvironmentSavedData extends SavedData {
private final Long2IntOpenHashMap envData = new Long2IntOpenHashMap();
private static final String NAME = "aphelion_environment";
public static EnvironmentSavedData create() {
return new EnvironmentSavedData();
}
@Override
@NotNull
public CompoundTag save(@NotNull CompoundTag tag, @NotNull HolderLookup.Provider provider) {
int size = envData.size();
long[] positions = new long[size];
int[] data = new int[size];
int i = 0;
for (var e : envData.long2IntEntrySet()) {
positions[i] = e.getLongKey();
data[i] = e.getIntValue();
i++;
}
tag.putLongArray("Position", positions);
tag.putIntArray("Value", data);
return tag;
}
public static EnvironmentSavedData load(CompoundTag tag, HolderLookup.Provider lookupProvider) {
EnvironmentSavedData data = create();
if (!tag.contains("Position", Tag.TAG_LONG_ARRAY) || !tag.contains("Value", Tag.TAG_INT_ARRAY)) { return data; }
long[] positions = tag.getLongArray("Positions");
int[] values = tag.getIntArray("Value");
int length = Math.min(positions.length, values.length);
data.envData.ensureCapacity(length);
for (int i = 0; i < length; i++) {
data.envData.put(positions[i], values[i]);
}
return data;
}
public EnvironmentData getDataForPosition(BlockPos pos) {
int packed = envData.getOrDefault(pos.asLong(), EnvironmentData.DEFAULT_PACKED);
return EnvironmentData.unpack(packed);
}
public void setDataForPosition(BlockPos pos, EnvironmentData data) {
putOrRemove(pos.asLong(), data.pack());
}
public boolean hasOxygen(BlockPos pos) {
var data = getDataForPosition(pos);
return data.hasOxygen();
}
public void setOxygen(BlockPos pos, boolean value) {
var data = getDataForPosition(pos);
data.setOxygen(value);
putOrRemove(pos.asLong(), data.pack());
}
public float getGravity(BlockPos pos) {
var data = getDataForPosition(pos);
return data.getGravity();
}
public void setGravity(BlockPos pos, float value) {
var data = getDataForPosition(pos);
data.setGravity(value);
putOrRemove(pos.asLong(), data.pack());
}
public short getTemperature(BlockPos pos) {
var data = getDataForPosition(pos);
return data.getTemperature();
}
public void setTemperature(BlockPos pos, short value) {
var data = getDataForPosition(pos);
data.setTemperature(value);
putOrRemove(pos.asLong(), data.pack());
}
private void putOrRemove(long key, int packed) {
if (packed == EnvironmentData.DEFAULT_PACKED) {
envData.remove(key);
} else {
envData.put(key, packed);
}
setDirty();
}
public static EnvironmentSavedData get(ServerLevel level) {
return level.getDataStorage().computeIfAbsent(
new Factory<>(EnvironmentSavedData::create, EnvironmentSavedData::load),
NAME
);
}
}

View File

@@ -0,0 +1,185 @@
package net.xevianlight.aphelion.core.saveddata;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import net.minecraft.core.HolderLookup;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.resources.ResourceLocation;
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 org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class SpacePartitionSavedData extends SavedData {
private static final String NAME = "aphelion_station_partitions";
private final Long2ObjectMap<PartitionData> map = new Long2ObjectOpenHashMap<>();
public static SpacePartitionSavedData create() {
return new SpacePartitionSavedData();
}
public static SpacePartitionSavedData load(CompoundTag tag, HolderLookup.Provider lookupProvider) {
SpacePartitionSavedData data = create();
ListTag entries = tag.getList("Entries", CompoundTag.TAG_COMPOUND);
for (int i = 0; i < entries.size(); i++) {
CompoundTag e = entries.getCompound(i);
long key = e.getLong("Key");
ResourceLocation orbitRL = null;
if (e.contains("Orbit", CompoundTag.TAG_STRING)) {
orbitRL = ResourceLocation.tryParse(e.getString("Orbit"));
}
PartitionData pd = new PartitionData(orbitRL);
// Destination (optional)
if (e.contains("Destination", CompoundTag.TAG_STRING)) {
ResourceLocation destRL = ResourceLocation.tryParse(e.getString("Destination"));
pd.setDestination(destRL); // ok if null (parse fail)
} else {
pd.setDestination(null);
}
// Traveling (optional; default false)
if (e.contains("Traveling", CompoundTag.TAG_BYTE)) {
pd.setTraveling(e.getBoolean("Traveling"));
}
// Distances (optional; default 0.0)
if (e.contains("DistanceTraveled", CompoundTag.TAG_DOUBLE)) {
pd.setDistanceTraveled(e.getDouble("DistanceTraveled"));
}
if (e.contains("DistanceToDest", CompoundTag.TAG_DOUBLE)) {
pd.setDistanceToDest(e.getDouble("DistanceToDest"));
}
data.map.put(key, pd);
}
return data;
}
@Override
public @NotNull CompoundTag save(CompoundTag tag, HolderLookup.@NotNull Provider registries) {
ListTag entries = new ListTag();
map.long2ObjectEntrySet().forEach(entry -> {
long key = entry.getLongKey();
PartitionData pd = entry.getValue();
CompoundTag e = new CompoundTag();
e.putLong("Key", key);
// Orbit
if (pd.getOrbit() != null) {
e.putString("Orbit", pd.getOrbit().toString());
}
// Destination (only if present)
if (pd.getDestination() != null) {
e.putString("Destination", pd.getDestination().toString());
}
// Traveling + distances
e.putBoolean("Traveling", pd.isTraveling());
e.putDouble("DistanceTraveled", pd.getDistanceTraveled());
e.putDouble("DistanceToDest", pd.getDistanceToDest());
entries.add(e);
});
tag.put("Entries", entries);
return tag;
}
public @Nullable ResourceLocation getOrbitForPartition(int px, int pz) {
PartitionData data = map.get(pack(px, pz));
if (data == null) return null;
return map.get(pack(px, pz)).getOrbit();
}
public void setOrbitForPartition(int px, int pz, ResourceLocation orbit) {
long key = pack(px, pz);
PartitionData prev = map.get(key);
PartitionData newData = new PartitionData(prev);
newData.setOrbit(orbit);
if (!newData.equals(prev)) {
map.put(key, newData);
setDirty();
}
}
public boolean clearOrbitForPartition(int px, int pz) {
long key = pack(px, pz);
PartitionData removed = map.remove(key);
if (removed != null) {
setDirty();
return true;
}
return false;
}
public void clearAllOrbits() {
if (!map.isEmpty()) {
map.clear();
setDirty();
}
}
public @Nullable 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);
setDirty();
}
return data;
}
public void overwriteAllExistingOrbits(ResourceLocation orbit) {
if (map.isEmpty()) return;
boolean changed = false;
for (var entry : map.long2ObjectEntrySet()) {
if(!orbit.equals(entry.getValue())) {
entry.getValue().setOrbit(orbit);
changed = true;
}
}
if (changed) setDirty();
}
public static SpacePartitionSavedData get(ServerLevel level) {
return level.getDataStorage().computeIfAbsent(
new Factory<>(SpacePartitionSavedData::create, SpacePartitionSavedData::load),
NAME
);
}
public static long pack(int px, int pz) {
return (((long) px) << 32) | (pz & 0xffffffffL);
}
public static int unpackX(long key) {
return (int)(key >> 32);
}
public static int unpackZ(long key) {
return (int)key;
}
}

View File

@@ -0,0 +1,98 @@
package net.xevianlight.aphelion.core.saveddata.types;
public final class EnvironmentData {
public static final boolean DEFAULT_OXYGEN = true;
public static final short DEFAULT_TEMPERATURE = (short) 294.2611; // 70F
public static final float DEFAULT_GRAVITY = 9.80665f; // 1G
public static final int DEFAULT_PACKED = new EnvironmentData(DEFAULT_OXYGEN, DEFAULT_TEMPERATURE, DEFAULT_GRAVITY).pack();
/* We can pack all of this into an int value per block position.
* If we have to store partitionData for an entire chunk section (16^3), this amounts to 16kB per section.
* 1000 sections touched is 16MB
* This is acceptable for partitionData that will only exist where it is not equal to the default values
*/
private static final int OXYGEN_BITS = 1; // Boolean. Do we have oxygen or no?
private static final int TEMPERATURE_BITS = Short.SIZE; // 16 bits should suffice for temperature, gives 0k to 65536k range, more than enough
private static final int GRAVITY_BITS = 15; // Leftover bits can be assigned to gravity, 32768 values
private static final float GRAVITY_PRECISION = 100.0f; // 2 decimal precision
private static final int OXYGEN_BIT = 0;
private static final int TEMPERATURE_BIT = OXYGEN_BIT + OXYGEN_BITS; // next 16 bits
private static final int GRAVITY_BIT = TEMPERATURE_BIT + TEMPERATURE_BITS; // next 15 bits
private boolean oxygen;
private short temperature;
private float gravity;
public EnvironmentData(boolean oxygen, short temperature, float gravity) {
this.oxygen = oxygen;
this.temperature = temperature;
this.gravity = gravity;
}
public int pack() {
int packedData = 0;
packedData |= (this.oxygen ? 1 : 0) << OXYGEN_BIT;
packedData |= (this.temperature & ((1 << TEMPERATURE_BITS) - 1)) << TEMPERATURE_BIT;
packedData |= (int) (this.gravity * GRAVITY_PRECISION) << GRAVITY_BIT;
return packedData;
}
public static EnvironmentData unpack(int packedData) {
boolean oxygen = ((packedData >> OXYGEN_BIT) & 1) == 1;
short temperature = (short) ((packedData >> TEMPERATURE_BIT) & ((1 << TEMPERATURE_BITS) - 1));
float gravity = ((packedData >> GRAVITY_BIT) & ((1 << GRAVITY_BITS) - 1)) / GRAVITY_PRECISION;
return new EnvironmentData(oxygen, temperature, gravity);
}
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (obj == null || obj.getClass() != this.getClass()) return false;
var that = (EnvironmentData) obj;
return this.oxygen == that.oxygen &&
this.temperature == that.temperature &&
Float.floatToIntBits(this.gravity) == Float.floatToIntBits(that.gravity);
}
@Override
public String toString() {
return "EnvironmentData[" +
"oxygen=" + oxygen + ", " +
"temperature=" + temperature + ", " +
"gravity=" + gravity + ']';
}
public boolean hasOxygen() {
return oxygen;
}
public void setOxygen(boolean oxygen) {
this.oxygen = oxygen;
}
public short getTemperature() {
return temperature;
}
public void setTemperature(short temperature) {
this.temperature = temperature;
}
public float getGravity() {
return gravity;
}
public void setGravity(float gravity) {
this.gravity = gravity;
}
}

View File

@@ -0,0 +1,121 @@
package net.xevianlight.aphelion.core.saveddata.types;
import io.netty.buffer.ByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.resources.ResourceLocation;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
import java.util.Optional;
public class PartitionData {
@Nullable private ResourceLocation orbit;
@Nullable private ResourceLocation destination;
private boolean traveling;
private double distanceTraveled;
private double distanceToDest;
public PartitionData(@Nullable ResourceLocation orbit) {
this.orbit = orbit;
this.destination = null;
this.traveling = false;
this.distanceTraveled = 0;
this.distanceToDest = 0;
}
public PartitionData(PartitionData other) {
this.orbit = other.orbit;
this.destination = other.destination;
this.traveling = other.traveling;
this.distanceTraveled = other.distanceTraveled;
this.distanceToDest = other.distanceToDest;
}
public static final StreamCodec<ByteBuf, PartitionData> STREAM_CODEC =
StreamCodec.composite(
// orbit is nullable -> optional codec
ByteBufCodecs.optional(ResourceLocation.STREAM_CODEC),
d -> Optional.ofNullable(d.getOrbit()),
ByteBufCodecs.optional(ResourceLocation.STREAM_CODEC),
d -> Optional.ofNullable(d.getDestination()),
ByteBufCodecs.BOOL,
PartitionData::isTraveling,
// doubles -> DOUBLE codec
ByteBufCodecs.DOUBLE,
PartitionData::getDistanceTraveled,
ByteBufCodecs.DOUBLE,
PartitionData::getDistanceToDest,
(orbitOpt, destOpt, traveling, distTraveled, distToDest) -> {
PartitionData data = new PartitionData(orbitOpt.orElse(null));
data.destination = destOpt.orElse(null);
data.traveling = traveling;
data.distanceTraveled = distTraveled;
data.distanceToDest = distToDest;
return data;
}
);
public @Nullable ResourceLocation getOrbit() {
return this.orbit;
}
public void setOrbit(ResourceLocation orbit) {
this.orbit = orbit;
}
public @Nullable ResourceLocation getDestination() {
return destination;
}
public void setDestination(@Nullable ResourceLocation destination) {
this.destination = destination;
}
public boolean isTraveling() {
return traveling;
}
public void setTraveling(boolean traveling) {
this.traveling = traveling;
}
public double getDistanceTraveled() {
return distanceTraveled;
}
public void setDistanceTraveled(double distanceTraveled) {
this.distanceTraveled = distanceTraveled;
}
public double getDistanceToDest() {
return distanceToDest;
}
public void setDistanceToDest(double distanceToDest) {
this.distanceToDest = distanceToDest;
}
public void travel(double distance) {
distanceTraveled = Math.min( distanceTraveled + distance, distanceToDest);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || obj.getClass() != this.getClass()) return false;
PartitionData that = (PartitionData) obj;
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;
}
}

View File

@@ -1,121 +0,0 @@
package net.xevianlight.aphelion.core.space;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import net.minecraft.core.HolderLookup;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.Tag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.saveddata.SavedData;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
public class SpacePartitionSavedData extends SavedData {
private static final String NAME = "aphelion_station_partitions";
private final Long2ObjectMap<ResourceLocation> map = new Long2ObjectOpenHashMap<>();
public static SpacePartitionSavedData create() {
return new SpacePartitionSavedData();
}
public static SpacePartitionSavedData load(CompoundTag tag, HolderLookup.Provider lookupProvider) {
SpacePartitionSavedData data = create();
ListTag entires = tag.getList("Entries", CompoundTag.TAG_COMPOUND);
for (int i = 0; i < entires.size(); i++) {
CompoundTag e = entires.getCompound(i);
long key = e.getLong("Key");
String orbit = e.getString("Orbit"); // "aphelion/mars"
ResourceLocation orbitRL = ResourceLocation.tryParse(orbit);
if (orbitRL != null)
data.map.put(key, orbitRL);
}
return data;
}
@Override
public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) {
ListTag entries = new ListTag();
map.long2ObjectEntrySet().forEach(entry -> {
CompoundTag e = new CompoundTag();
e.putLong("Key", entry.getLongKey());
e.putString("Orbit", entry.getValue().toString());
entries.add(e);
});
tag.put("Entries", entries);
return tag;
}
public @Nullable ResourceLocation getOrbitForPartition(int px, int pz) {
return map.get(pack(px, pz));
}
public void setOrbitForPartition(int px, int pz, ResourceLocation orbit) {
long key = pack(px, pz);
ResourceLocation prev = map.get(key);
if (!orbit.equals(prev)) {
map.put(key, orbit);
setDirty();
}
}
public boolean clearOrbitForPartition(int px, int pz) {
long key = pack(px, pz);
ResourceLocation removed = map.remove(key);
if (removed != null) {
setDirty();;
return true;
}
return false;
}
public void clearAllOrbits() {
if (!map.isEmpty()) {
map.clear();
setDirty();
}
}
public void overwriteAllExistingOrbits(ResourceLocation orbit) {
if (map.isEmpty()) return;
boolean changed = false;
for (var entry : map.long2ObjectEntrySet()) {
if(!orbit.equals(entry.getValue())) {
entry.setValue(orbit);
changed = true;
}
}
if (changed) setDirty();
}
public static SpacePartitionSavedData get(ServerLevel level) {
return level.getDataStorage().computeIfAbsent(
new Factory<>(SpacePartitionSavedData::create, SpacePartitionSavedData::load),
NAME
);
}
public static long pack(int px, int pz) {
return (((long) px) << 32) | (pz & 0xffffffffL);
}
public static int unpackX(long key) {
return (int)(key >> 32);
}
public static int unpackZ(long key) {
return (int)key;
}
}

View File

@@ -11,6 +11,6 @@ public class PartitionPayloadHandler {
public static void handleDataOnMain(PartitionPayload data, IPayloadContext context) {
// Set our local partition state to the packet we just received.
PartitionClientState.set(data);
Aphelion.LOGGER.info("Partition packet received! id={}", data.id());
Aphelion.LOGGER.info("Partition packet received! id={}", data.partitionData());
}
}

View File

@@ -4,16 +4,14 @@ package net.xevianlight.aphelion.network;
import net.minecraft.server.level.ServerPlayer;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.neoforge.event.entity.player.PlayerEvent;
import net.neoforged.neoforge.event.tick.ServerTickEvent;
import net.neoforged.neoforge.network.PacketDistributor;
import net.xevianlight.aphelion.Aphelion;
import net.xevianlight.aphelion.core.space.SpacePartitionSavedData;
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 java.util.HashSet;
import java.util.Set;
import java.util.UUID;
@EventBusSubscriber(modid = Aphelion.MOD_ID)
@@ -44,14 +42,14 @@ public final class PartitionSync {
}
private static PartitionPayload computePartitionFor(ServerPlayer sp) {
// convert player position to partition coords
int px = (int)Math.floor(sp.getX() / SpacePartitionHelper.SIZE);
int pz = (int)Math.floor(sp.getZ() / SpacePartitionHelper.SIZE);
// Get the orbit for the partition the player is in and create a packet for it
var orbit = SpacePartitionSavedData.get(sp.serverLevel()).getOrbitForPartition(px, pz);
String orbitId = (orbit != null) ? orbit.toString() : "aphelion:orbit/default";
PartitionData live = SpacePartitionSavedData.get(sp.serverLevel()).getData(px, pz);
return new PartitionPayload(orbitId);
// snapshot so mutations later dont affect cached payloads
PartitionData snapshot = (live == null) ? null : new PartitionData(live);
return new PartitionPayload(snapshot);
}
}

View File

@@ -1,24 +1,39 @@
package net.xevianlight.aphelion.network.packet;
import io.netty.buffer.ByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceLocation;
import net.xevianlight.aphelion.Aphelion;
import net.xevianlight.aphelion.core.saveddata.types.PartitionData;
public record PartitionPayload(String id) implements CustomPacketPayload {
public static final Type<PartitionPayload> TYPE = new CustomPacketPayload.Type<>(ResourceLocation.fromNamespaceAndPath(Aphelion.MOD_ID, "partition_data"));
import java.util.Objects;
public static final StreamCodec<ByteBuf, PartitionPayload> STREAM_CODEC = StreamCodec.composite(
ByteBufCodecs.STRING_UTF8,
PartitionPayload::id,
public record PartitionPayload(PartitionData partitionData) implements CustomPacketPayload {
public static final Type<PartitionPayload> TYPE =
new Type<>(ResourceLocation.fromNamespaceAndPath(Aphelion.MOD_ID, "partition_data"));
PartitionPayload::new
);
public static final StreamCodec<ByteBuf, PartitionPayload> STREAM_CODEC =
StreamCodec.composite(
PartitionData.STREAM_CODEC,
PartitionPayload::partitionData,
PartitionPayload::new
);
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
PartitionPayload that = (PartitionPayload) o;
return partitionData.equals(that.partitionData);
}
@Override
public int hashCode() {
return Objects.hashCode(partitionData);
}
}

View File

@@ -10,12 +10,16 @@ import net.xevianlight.aphelion.util.registries.ModRegistries;
public record Planet(
ResourceKey<Level> dimension,
double orbitDistance,
ResourceKey<StarSystem> system
ResourceKey<StarSystem> system,
boolean oxygen,
double gravity
) {
public static final Codec<Planet> CODEC = RecordCodecBuilder.create(inst -> inst.group(
ResourceKey.codec(Registries.DIMENSION).fieldOf("dimension").forGetter(Planet::dimension),
Codec.DOUBLE.fieldOf("orbit_distance").forGetter(Planet::orbitDistance),
ResourceKey.codec(ModRegistries.STAR_SYSTEM).fieldOf("star_system").forGetter(Planet::system)
ResourceKey.codec(Registries.DIMENSION).fieldOf("dimension").forGetter(Planet::dimension),
Codec.DOUBLE.fieldOf("orbit_distance").forGetter(Planet::orbitDistance),
ResourceKey.codec(ModRegistries.STAR_SYSTEM).fieldOf("star_system").forGetter(Planet::system),
Codec.BOOL.fieldOf("oxygen").forGetter(Planet::oxygen),
Codec.DOUBLE.fieldOf("gravity").forGetter(Planet::gravity)
).apply(inst, Planet::new));
}

View File

@@ -18,7 +18,9 @@ public final class PlanetCache {
public static final Planet DEFAULT = new Planet(
ResourceKey.create(Registries.DIMENSION, ResourceLocation.withDefaultNamespace("overworld")),
1,
ResourceKey.create(ModRegistries.STAR_SYSTEM, Aphelion.id("sol"))
ResourceKey.create(ModRegistries.STAR_SYSTEM, Aphelion.id("sol")),
true,
1
);
public static void registerPlanets(Map<ResourceLocation, Planet> planets) {

View File

@@ -0,0 +1,114 @@
package net.xevianlight.aphelion.util;
import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue;
import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import it.unimi.dsi.fastutil.longs.LongSet;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.phys.shapes.VoxelShape;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
public final class FloodFill3D {
private static final Direction[] DIRECTIONS = Direction.values();
public static final SolidBlockPredicate TEST_FULL_SEAL = (level, pos, state, positions, queue, direction) -> {
if (state.isAir()) return true;
if (state.is(ModTags.Blocks.PASSES_FLOOD_FILL)) return true;
if (state.is(ModTags.Blocks.BLOCKS_FLOOD_FILL)) return false;
if (state.isCollisionShapeFullBlock(level, pos)) return false;
VoxelShape collisionShape = state.getCollisionShape(level, pos);
if (collisionShape.isEmpty()) return true;
if (!isSideSolid(collisionShape, direction)) return true;
if (!isFaceSturdy(collisionShape, direction) && !isFaceSturdy(collisionShape, direction.getOpposite())) {
return true;
}
// Check the other directions to find a potential path for the partial block.
for (Direction dir : DIRECTIONS) {
if (dir.getAxis() == direction.getAxis()) continue;
var adjacentPos = pos.relative(dir);
var adjacentState = level.getBlockState(adjacentPos);
if (adjacentState.isAir()) return true;
}
positions.add(pos.asLong());
return false;
};
public static Set<BlockPos> run(Level level, BlockPos start, int limit, SolidBlockPredicate predicate, boolean retainOrder) {
level.getProfiler().push("adastra-floodfill");
LongSet positions = retainOrder ? new LongLinkedOpenHashSet(limit) : new LongOpenHashSet(limit);
LongArrayFIFOQueue queue = new LongArrayFIFOQueue(limit);
queue.enqueue(start.asLong());
while (!queue.isEmpty() && positions.size() < limit) {
long packedPos = queue.dequeueLong();
if (positions.contains(packedPos)) continue;
positions.add(packedPos);
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(BlockPos.getX(packedPos), BlockPos.getY(packedPos), BlockPos.getZ(packedPos));
for (Direction direction : DIRECTIONS) {
pos.set(packedPos);
pos.move(direction);
BlockState state = level.getBlockState(pos);
if (!predicate.test(level, pos, state, positions, queue, direction)) continue;
queue.enqueue(pos.asLong());
}
}
Set<BlockPos> result = retainOrder ? new LinkedHashSet<>(positions.size()) : new HashSet<>(positions.size());
for (long pos : positions) {
result.add(BlockPos.of(pos));
}
level.getProfiler().pop();
return result;
}
private static boolean isSideSolid(VoxelShape collisionShape, Direction dir) {
return switch (dir.getAxis()) {
case X -> isAxisCovered(collisionShape, Direction.Axis.Y, Direction.Axis.Z);
case Y -> isAxisCovered(collisionShape, Direction.Axis.X, Direction.Axis.Z);
case Z -> isAxisCovered(collisionShape, Direction.Axis.X, Direction.Axis.Y);
};
}
private static boolean isAxisCovered(VoxelShape shape, Direction.Axis axis1, Direction.Axis axis2) {
return shape.min(axis1) <= 0 && shape.max(axis1) >= 1 && shape.min(axis2) <= 0 && shape.max(axis2) >= 1;
}
private static boolean isFaceSturdy(VoxelShape collisionShape, Direction dir) {
VoxelShape faceShape = collisionShape.getFaceShape(dir);
if (faceShape.isEmpty()) return true;
var aabbs = faceShape.toAabbs();
if (aabbs.isEmpty()) return true;
return checkBounds(aabbs.get(0), dir.getAxis());
}
private static boolean checkBounds(AABB bounds, Direction.Axis axis) {
return switch (axis) {
case X -> bounds.minY <= 0 && bounds.maxY >= 1 && bounds.minZ <= 0 && bounds.maxZ >= 1;
case Y -> bounds.minX <= 0 && bounds.maxX >= 1 && bounds.minZ <= 0 && bounds.maxZ >= 1;
case Z -> bounds.minX <= 0 && bounds.maxX >= 1 && bounds.minY <= 0 && bounds.maxY >= 1;
};
}
@FunctionalInterface
public interface SolidBlockPredicate {
boolean test(Level level, BlockPos pos, BlockState state, LongSet positions, LongArrayFIFOQueue queue, Direction direction);
}
}

View File

@@ -18,6 +18,8 @@ public class ModTags {
public static final TagKey<Block> STORAGE_BLOCKS_STEEL = commonTag("storage_blocks/steel");
public static final TagKey<Block> LAUNCH_PAD = createTag("launch_pad");
public static final TagKey<Block> PASSES_FLOOD_FILL = createTag("passes_flood_fill");
public static final TagKey<Block> BLOCKS_FLOOD_FILL = createTag("blocks_flood_fill");
private static TagKey<Block> commonTag(String name) {
return BlockTags.create(ResourceLocation.fromNamespaceAndPath("c", name));

View File

@@ -1,4 +1,4 @@
# This is an example neoforge.mods.toml file. It contains the data relating to the loading mods.
# This is an example neoforge.mods.toml file. It contains the partitionData relating to the loading mods.
# There are several mandatory fields (#mandatory), and many more that are optional (#optional).
# The overall format is standard TOML format, v0.5.0.
# Note that there are a couple of TOML lists in this file.

View File

@@ -1,5 +1,7 @@
{
"dimension": "minecraft:overworld",
"orbit_distance": 1,
"star_system": "aphelon:sol"
"star_system": "aphelon:sol",
"gravity": 1,
"oxygen": true
}