init
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import std.sys;
|
||||
|
||||
import std.mem;
|
||||
import std.core;
|
||||
#pragma endian big
|
||||
|
||||
enum Tag : u8 {
|
||||
End = 0,
|
||||
Byte = 1,
|
||||
Short = 2,
|
||||
Int = 3,
|
||||
Long = 4,
|
||||
Float = 5,
|
||||
Double = 6,
|
||||
ByteArray = 7,
|
||||
String = 8,
|
||||
List = 9,
|
||||
Compound = 10,
|
||||
IntArray = 11,
|
||||
LongArray = 12
|
||||
};
|
||||
|
||||
enum Version: u8 {
|
||||
Internal = 0,
|
||||
External = 128
|
||||
};
|
||||
|
||||
using Element;
|
||||
|
||||
struct Value {
|
||||
if (parent.tag == Tag::Byte)
|
||||
s8 value;
|
||||
else if (parent.tag == Tag::Short)
|
||||
s16 value;
|
||||
else if (parent.tag == Tag::Int)
|
||||
s32 value;
|
||||
else if (parent.tag == Tag::Long)
|
||||
s64 value;
|
||||
else if (parent.tag == Tag::Float)
|
||||
float value;
|
||||
else if (parent.tag == Tag::Double)
|
||||
double value;
|
||||
else if (parent.tag == Tag::ByteArray) {
|
||||
s32 arrayLength;
|
||||
s8 value[arrayLength] [[sealed]];
|
||||
} else if (parent.tag == Tag::String) {
|
||||
u16 stringLength;
|
||||
char value[stringLength];
|
||||
} else if (parent.tag == Tag::List) {
|
||||
Tag tag;
|
||||
s32 listLength;
|
||||
Value values[listLength] [[static]];
|
||||
} else if (parent.tag == Tag::Compound) {
|
||||
Element values[while(true)];
|
||||
} else if (parent.tag == Tag::IntArray){
|
||||
s32 arrayLength;
|
||||
s32 value[arrayLength] [[sealed]];
|
||||
} else if (parent.tag == Tag::LongArray) {
|
||||
s32 arrayLength;
|
||||
s64 value[arrayLength] [[sealed]];
|
||||
} else {
|
||||
std::error(std::format("Invalid tag {}", parent.tag));
|
||||
continue;
|
||||
}
|
||||
} [[inline]];
|
||||
|
||||
struct Element {
|
||||
Tag tag;
|
||||
if (tag == Tag::End)
|
||||
break;
|
||||
else {
|
||||
u16 nameLength;
|
||||
|
||||
char name[nameLength];
|
||||
|
||||
Value value;
|
||||
}
|
||||
};
|
||||
|
||||
struct NBT {
|
||||
Element element[while(true)] [[inline]];
|
||||
};
|
||||
|
||||
struct Allocation {
|
||||
u24 position;
|
||||
u8 size;
|
||||
|
||||
if (position == 0 || size == 0) {
|
||||
continue;
|
||||
} else {
|
||||
|
||||
|
||||
u32 start = position * 128;
|
||||
u32 dataSize @ start;
|
||||
Version version @ start + 4;
|
||||
|
||||
if (version == Version::Internal) {
|
||||
NBT data @ start + 5;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct Header {
|
||||
Allocation pointer[1024];
|
||||
};
|
||||
|
||||
struct File {
|
||||
Header header;
|
||||
};
|
||||
|
||||
|
||||
|
||||
File file @ 0x00;
|
||||
@@ -0,0 +1,114 @@
|
||||
import std.sys;
|
||||
|
||||
import std.mem;
|
||||
import std.core;
|
||||
#pragma endian big
|
||||
#pragma pattern_limit 512000
|
||||
|
||||
enum Tag : u8 {
|
||||
End = 0,
|
||||
Byte = 1,
|
||||
Short = 2,
|
||||
Int = 3,
|
||||
Long = 4,
|
||||
Float = 5,
|
||||
Double = 6,
|
||||
ByteArray = 7,
|
||||
String = 8,
|
||||
List = 9,
|
||||
Compound = 10,
|
||||
IntArray = 11,
|
||||
LongArray = 12
|
||||
};
|
||||
|
||||
enum Version: u8 {
|
||||
Internal = 0,
|
||||
External = 128
|
||||
};
|
||||
|
||||
using Element;
|
||||
|
||||
struct Value {
|
||||
if (parent.tag == Tag::Byte)
|
||||
s8 value;
|
||||
else if (parent.tag == Tag::Short)
|
||||
s16 value;
|
||||
else if (parent.tag == Tag::Int)
|
||||
s32 value;
|
||||
else if (parent.tag == Tag::Long)
|
||||
s64 value;
|
||||
else if (parent.tag == Tag::Float)
|
||||
float value;
|
||||
else if (parent.tag == Tag::Double)
|
||||
double value;
|
||||
else if (parent.tag == Tag::ByteArray) {
|
||||
s32 arrayLength;
|
||||
s8 value[arrayLength] [[sealed]];
|
||||
} else if (parent.tag == Tag::String) {
|
||||
u16 stringLength;
|
||||
char value[stringLength];
|
||||
} else if (parent.tag == Tag::List) {
|
||||
Tag tag;
|
||||
s32 listLength;
|
||||
Value values[listLength] [[static]];
|
||||
} else if (parent.tag == Tag::Compound) {
|
||||
Element values[while(true)];
|
||||
} else if (parent.tag == Tag::IntArray){
|
||||
s32 arrayLength;
|
||||
s32 value[arrayLength] [[sealed]];
|
||||
} else if (parent.tag == Tag::LongArray) {
|
||||
s32 arrayLength;
|
||||
s64 value[arrayLength] [[sealed]];
|
||||
} else {
|
||||
std::error(std::format("Invalid tag {}", parent.tag));
|
||||
continue;
|
||||
}
|
||||
} [[inline]];
|
||||
|
||||
struct Element {
|
||||
Tag tag;
|
||||
if (tag == Tag::End)
|
||||
break;
|
||||
else {
|
||||
u16 nameLength;
|
||||
|
||||
char name[nameLength];
|
||||
|
||||
Value value;
|
||||
}
|
||||
};
|
||||
|
||||
struct NBT {
|
||||
Element element[while(true)] [[inline]];
|
||||
};
|
||||
|
||||
struct Allocation {
|
||||
u24 position;
|
||||
u8 size;
|
||||
|
||||
if (position == 0 || size == 0) {
|
||||
continue;
|
||||
} else {
|
||||
|
||||
|
||||
u32 start = position * 4096;
|
||||
u32 dataSize @ start;
|
||||
Version version @ start + 4;
|
||||
|
||||
if (version == Version::Internal) {
|
||||
NBT data @ start + 5;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct Header {
|
||||
Allocation pointer[1024];
|
||||
};
|
||||
|
||||
struct File {
|
||||
Header header;
|
||||
};
|
||||
|
||||
|
||||
|
||||
File file @ 0x00;
|
||||
@@ -0,0 +1,499 @@
|
||||
package dev.ryanhcode.sable;
|
||||
|
||||
import dev.ryanhcode.sable.api.SubLevelHelper;
|
||||
import dev.ryanhcode.sable.api.entity.EntitySubLevelUtil;
|
||||
import dev.ryanhcode.sable.api.physics.handle.RigidBodyHandle;
|
||||
import dev.ryanhcode.sable.api.sublevel.ServerSubLevelContainer;
|
||||
import dev.ryanhcode.sable.api.sublevel.SubLevelContainer;
|
||||
import dev.ryanhcode.sable.companion.SableCompanion;
|
||||
import dev.ryanhcode.sable.companion.SubLevelAccess;
|
||||
import dev.ryanhcode.sable.companion.math.BoundingBox3d;
|
||||
import dev.ryanhcode.sable.companion.math.BoundingBox3dc;
|
||||
import dev.ryanhcode.sable.companion.math.JOMLConversion;
|
||||
import dev.ryanhcode.sable.companion.math.Pose3dc;
|
||||
import dev.ryanhcode.sable.mixinterface.clip_overwrite.LevelPoseProviderExtension;
|
||||
import dev.ryanhcode.sable.mixinterface.entity.entity_sublevel_collision.EntityMovementExtension;
|
||||
import dev.ryanhcode.sable.mixinterface.plot.SubLevelContainerHolder;
|
||||
import dev.ryanhcode.sable.sublevel.ClientSubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.plot.LevelPlot;
|
||||
import dev.ryanhcode.sable.sublevel.system.SubLevelPhysicsSystem;
|
||||
import dev.ryanhcode.sable.util.SableDistUtil;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Position;
|
||||
import net.minecraft.core.SectionPos;
|
||||
import net.minecraft.core.Vec3i;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Quaterniondc;
|
||||
import org.joml.Vector3d;
|
||||
import org.joml.Vector3dc;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
/**
|
||||
* The default Sable companion for when Sable is loaded
|
||||
*/
|
||||
public class ActiveSableCompanion implements SableCompanion {
|
||||
|
||||
@Override
|
||||
public Iterable<SubLevel> getAllIntersecting(final Level level, final BoundingBox3dc bounds) {
|
||||
if (!(level instanceof final SubLevelContainerHolder holder)) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
final SubLevelContainer plotContainer = holder.sable$getPlotContainer();
|
||||
|
||||
if (plotContainer instanceof final ServerSubLevelContainer serverContainer) {
|
||||
final SubLevelPhysicsSystem physicsSystem = serverContainer.physicsSystem();
|
||||
|
||||
return physicsSystem.queryIntersecting(bounds);
|
||||
} else {
|
||||
return plotContainer.queryIntersecting(bounds);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable SubLevel getContaining(final Level level, final int chunkX, final int chunkZ) {
|
||||
if (!(level instanceof SubLevelContainerHolder)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final SubLevelContainer container = ((SubLevelContainerHolder) level).sable$getPlotContainer();
|
||||
if (container == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final LevelPlot plot = container.getPlot(chunkX, chunkZ);
|
||||
return plot != null ? plot.getSubLevel() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable SubLevel getContaining(final Level level, final ChunkPos chunkPos) {
|
||||
return this.getContaining(level, chunkPos.x, chunkPos.z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable SubLevel getContaining(final Level level, final SectionPos pos) {
|
||||
return this.getContaining(level, pos.getX(), pos.getZ());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable SubLevel getContaining(final Level level, final Vec3i pos) {
|
||||
return this.getContaining(level, pos.getX() >> SectionPos.SECTION_BITS, pos.getZ() >> SectionPos.SECTION_BITS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable SubLevel getContaining(final Level level, final Position pos) {
|
||||
return this.getContaining(level, Mth.floor(pos.x()) >> SectionPos.SECTION_BITS, Mth.floor(pos.z()) >> SectionPos.SECTION_BITS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable SubLevel getContaining(final Level level, final Vector3dc pos) {
|
||||
return this.getContaining(level, Mth.floor(pos.x()) >> SectionPos.SECTION_BITS, Mth.floor(pos.z()) >> SectionPos.SECTION_BITS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable SubLevel getContaining(final Level level, final double blockX, final double blockZ) {
|
||||
return this.getContaining(level, Mth.floor(blockX) >> SectionPos.SECTION_BITS, Mth.floor(blockZ) >> SectionPos.SECTION_BITS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable SubLevel getContaining(final Entity entity) {
|
||||
final ChunkPos chunkPos = entity.chunkPosition();
|
||||
return this.getContaining(entity.level(), chunkPos.x, chunkPos.z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable SubLevel getContaining(final BlockEntity blockEntity) {
|
||||
final BlockPos pos = blockEntity.getBlockPos();
|
||||
return this.getContaining(blockEntity.getLevel(), pos.getX() >> SectionPos.SECTION_BITS, pos.getZ() >> SectionPos.SECTION_BITS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ClientSubLevel getContainingClient(final int chunkX, final int chunkZ) {
|
||||
return (ClientSubLevel) this.getContaining(SableDistUtil.getClientLevel(), chunkX, chunkZ);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ClientSubLevel getContainingClient(final ChunkPos chunkPos) {
|
||||
return (ClientSubLevel) this.getContaining(SableDistUtil.getClientLevel(), chunkPos.x, chunkPos.z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ClientSubLevel getContainingClient(final Position pos) {
|
||||
return (ClientSubLevel) this.getContaining(SableDistUtil.getClientLevel(), Mth.floor(pos.x()) >> SectionPos.SECTION_BITS, Mth.floor(pos.z()) >> SectionPos.SECTION_BITS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ClientSubLevel getContainingClient(final Vector3dc pos) {
|
||||
return (ClientSubLevel) this.getContaining(SableDistUtil.getClientLevel(), Mth.floor(pos.x()) >> SectionPos.SECTION_BITS, Mth.floor(pos.z()) >> SectionPos.SECTION_BITS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ClientSubLevel getContainingClient(final SectionPos pos) {
|
||||
return (ClientSubLevel) this.getContaining(SableDistUtil.getClientLevel(), pos.x(), pos.z());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ClientSubLevel getContainingClient(final Vec3i pos) {
|
||||
return (ClientSubLevel) this.getContaining(SableDistUtil.getClientLevel(), pos.getX() >> SectionPos.SECTION_BITS, pos.getZ() >> SectionPos.SECTION_BITS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ClientSubLevel getContainingClient(final double blockX, final double blockZ) {
|
||||
return (ClientSubLevel) this.getContaining(SableDistUtil.getClientLevel(), Mth.floor(blockX) >> SectionPos.SECTION_BITS, Mth.floor(blockZ) >> SectionPos.SECTION_BITS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ClientSubLevel getContainingClient(final Entity entity) {
|
||||
final ChunkPos chunkPos = entity.chunkPosition();
|
||||
return (ClientSubLevel) this.getContaining(entity.level(), chunkPos.x, chunkPos.z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ClientSubLevel getContainingClient(final BlockEntity blockEntity) {
|
||||
final BlockPos pos = blockEntity.getBlockPos();
|
||||
return (ClientSubLevel) this.getContaining(blockEntity.getLevel(), pos.getX() >> SectionPos.SECTION_BITS, pos.getZ() >> SectionPos.SECTION_BITS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vector3d projectOutOfSubLevel(final Level level, final Vector3dc pos, final Vector3d dest) {
|
||||
final SubLevel subLevel = this.getContaining(level, pos);
|
||||
|
||||
if (subLevel == null) return dest.set(pos);
|
||||
|
||||
final Pose3dc pose;
|
||||
if (level instanceof final LevelPoseProviderExtension extension) {
|
||||
pose = extension.sable$getPose(subLevel);
|
||||
} else {
|
||||
pose = subLevel.logicalPose();
|
||||
}
|
||||
|
||||
return pose.transformPosition(pos, dest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vec3 projectOutOfSubLevel(final Level level, final Vec3 pos) {
|
||||
return this.projectOutOfSubLevel(level, (Position) pos);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vec3 projectOutOfSubLevel(final Level level, final Position pos) {
|
||||
final SubLevel subLevel = this.getContaining(level, pos);
|
||||
|
||||
if (subLevel == null) return pos instanceof final Vec3 vec ? vec : new Vec3(pos.x(), pos.y(), pos.z());
|
||||
|
||||
final Pose3dc pose;
|
||||
if (level instanceof final LevelPoseProviderExtension extension) {
|
||||
pose = extension.sable$getPose(subLevel);
|
||||
} else {
|
||||
pose = subLevel.logicalPose();
|
||||
}
|
||||
|
||||
return JOMLConversion.toMojang(pose.transformPosition(JOMLConversion.toJOML(pos)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable <T, S extends SubLevelAccess> T runIncludingSubLevels(final Level level, final Vec3 origin, final boolean shouldCheckOrigin, @Nullable final S subLevel, final BiFunction<S, BlockPos, T> converter) {
|
||||
return this.runIncludingSubLevels(level, (Position) origin, shouldCheckOrigin, subLevel, converter);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public @Nullable <T, S extends SubLevelAccess> T runIncludingSubLevels(final Level level, final Position origin, final boolean shouldCheckOrigin, @Nullable final S subLevel, final BiFunction<S, BlockPos, T> converter) {
|
||||
final BlockPos.MutableBlockPos mutableBlockPos = new BlockPos.MutableBlockPos(origin.x(), origin.y(), origin.z());
|
||||
final Vector3d mutablePos = JOMLConversion.toJOML(origin);
|
||||
T test;
|
||||
|
||||
// Initial test
|
||||
if (shouldCheckOrigin) {
|
||||
test = converter.apply(subLevel, mutableBlockPos.immutable());
|
||||
if (test != null)
|
||||
return test;
|
||||
}
|
||||
|
||||
// Test projectedPos
|
||||
if (subLevel != null) {
|
||||
subLevel.logicalPose().transformPosition(mutablePos);
|
||||
mutableBlockPos.set(mutablePos.x, mutablePos.y, mutablePos.z);
|
||||
|
||||
test = converter.apply(null, mutableBlockPos.immutable());
|
||||
if (test != null)
|
||||
return test;
|
||||
}
|
||||
|
||||
final Vec3 copyPos = JOMLConversion.toMojang(mutablePos);
|
||||
|
||||
// Test other sub-level plots
|
||||
final Iterable<SubLevel> subLevels = this.getAllIntersecting(level, new BoundingBox3d(BlockPos.containing(JOMLConversion.toMojang(mutablePos))));
|
||||
for (final SubLevel otherSubLevel : subLevels) {
|
||||
if (otherSubLevel == subLevel) // Ignore sub-level if it has already been checked
|
||||
continue;
|
||||
|
||||
mutablePos.set(copyPos.x, copyPos.y, copyPos.z);
|
||||
|
||||
otherSubLevel.logicalPose().transformPositionInverse(mutablePos);
|
||||
mutableBlockPos.set(mutablePos.x, mutablePos.y, mutablePos.z);
|
||||
|
||||
test = converter.apply((S) otherSubLevel, mutableBlockPos.immutable());
|
||||
if (test != null)
|
||||
return test;
|
||||
}
|
||||
|
||||
return null; // Return null if no valid position was found
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S extends SubLevelAccess> boolean findIncludingSubLevels(final Level level, final Vec3 origin, final boolean shouldCheckOrigin, @Nullable final S subLevel, final BiFunction<S, BlockPos, Boolean> converter) {
|
||||
return this.findIncludingSubLevels(level, (Position) origin, shouldCheckOrigin, subLevel, converter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S extends SubLevelAccess> boolean findIncludingSubLevels(final Level level, final Position origin, final boolean shouldCheckOrigin, @Nullable final S subLevel, final BiFunction<S, BlockPos, Boolean> converter) {
|
||||
return Boolean.TRUE.equals(
|
||||
this.runIncludingSubLevels(
|
||||
level, origin, shouldCheckOrigin, subLevel,
|
||||
(candidateSublevel, pos) -> Boolean.TRUE.equals(converter.apply(candidateSublevel, pos)) ? true : null //Null is treated as false as a fallback
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double distanceSquaredWithSubLevels(final Level level, final Vector3dc a, final Vector3dc b) {
|
||||
final Vector3dc globalA = this.projectOutOfSubLevel(level, a, new Vector3d());
|
||||
final Vector3dc globalB = this.projectOutOfSubLevel(level, b, new Vector3d());
|
||||
|
||||
return globalA.distanceSquared(globalB);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double distanceSquaredWithSubLevels(final Level level, final Position a, final Position b) {
|
||||
final Vector3dc globalA = this.projectOutOfSubLevel(level, JOMLConversion.toJOML(a));
|
||||
final Vector3dc globalB = this.projectOutOfSubLevel(level, JOMLConversion.toJOML(b));
|
||||
|
||||
return globalA.distanceSquared(globalB);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double distanceSquaredWithSubLevels(final Level level, final Vector3dc a, final double bX, final double bY, final double bZ) {
|
||||
final Vector3dc globalA = this.projectOutOfSubLevel(level, a, new Vector3d());
|
||||
final Vector3dc globalB = this.projectOutOfSubLevel(level, new Vector3d(bX, bY, bZ));
|
||||
|
||||
return globalA.distanceSquared(globalB);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double distanceSquaredWithSubLevels(final Level level, final Position a, final double bX, final double bY, final double bZ) {
|
||||
final Vector3dc globalA = this.projectOutOfSubLevel(level, JOMLConversion.toJOML(a));
|
||||
final Vector3dc globalB = this.projectOutOfSubLevel(level, new Vector3d(bX, bY, bZ));
|
||||
|
||||
return globalA.distanceSquared(globalB);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double distanceSquaredWithSubLevels(final Level level, final double aX, final double aY, final double aZ, final double bX, final double bY, final double bZ) {
|
||||
final Vector3dc globalA = this.projectOutOfSubLevel(level, new Vector3d(aX, aY, aZ));
|
||||
final Vector3dc globalB = this.projectOutOfSubLevel(level, new Vector3d(bX, bY, bZ));
|
||||
|
||||
return globalA.distanceSquared(globalB);
|
||||
}
|
||||
|
||||
private static double rectilinearDistance(final Vector3dc a, final Vector3dc b) {
|
||||
final double d0 = Math.abs(b.x() - a.x());
|
||||
final double d1 = Math.abs(b.y() - a.y());
|
||||
final double d2 = Math.abs(b.z() - a.z());
|
||||
return Math.max(d0, Math.max(d1, d2));
|
||||
}
|
||||
|
||||
@Override
|
||||
public double rectilinearDistanceWithSubLevels(final Level level, final Vector3dc a, final Vector3dc b) {
|
||||
final Vector3dc globalA = this.projectOutOfSubLevel(level, a, new Vector3d());
|
||||
final Vector3dc globalB = this.projectOutOfSubLevel(level, b, new Vector3d());
|
||||
|
||||
return rectilinearDistance(globalA, globalB);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double rectilinearDistanceWithSubLevels(final Level level, final Position a, final Position b) {
|
||||
final Vector3dc globalA = this.projectOutOfSubLevel(level, JOMLConversion.toJOML(a));
|
||||
final Vector3dc globalB = this.projectOutOfSubLevel(level, JOMLConversion.toJOML(b));
|
||||
|
||||
return rectilinearDistance(globalA, globalB);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double rectilinearDistanceWithSubLevels(final Level level, final Vector3dc a, final double bX, final double bY, final double bZ) {
|
||||
final Vector3dc globalA = this.projectOutOfSubLevel(level, a, new Vector3d());
|
||||
final Vector3dc globalB = this.projectOutOfSubLevel(level, new Vector3d(bX, bY, bZ));
|
||||
|
||||
return rectilinearDistance(globalA, globalB);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double rectilinearDistanceWithSubLevels(final Level level, final Position a, final double bX, final double bY, final double bZ) {
|
||||
final Vector3dc globalA = this.projectOutOfSubLevel(level, JOMLConversion.toJOML(a));
|
||||
final Vector3dc globalB = this.projectOutOfSubLevel(level, new Vector3d(bX, bY, bZ));
|
||||
|
||||
return rectilinearDistance(globalA, globalB);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double rectilinearDistanceWithSubLevels(final Level level, final double aX, final double aY, final double aZ, final double bX, final double bY, final double bZ) {
|
||||
final Vector3dc globalA = this.projectOutOfSubLevel(level, new Vector3d(aX, aY, aZ));
|
||||
final Vector3dc globalB = this.projectOutOfSubLevel(level, new Vector3d(bX, bY, bZ));
|
||||
|
||||
return rectilinearDistance(globalA, globalB);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vector3d getVelocity(final Level level, final Vector3dc pos, final Vector3d dest) {
|
||||
final SubLevel subLevel = this.getContaining(level, pos);
|
||||
|
||||
if (subLevel == null) return dest.zero();
|
||||
|
||||
return this.getVelocity(level, subLevel, pos, dest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vec3 getVelocity(final Level level, final Vec3 pos) {
|
||||
return JOMLConversion.toMojang(this.getVelocity(level, JOMLConversion.toJOML(pos), new Vector3d()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vec3 getVelocity(final Level level, final Position pos) {
|
||||
return JOMLConversion.toMojang(this.getVelocity(level, JOMLConversion.toJOML(pos), new Vector3d()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vector3d getVelocity(final Level level, final SubLevelAccess subLevel, final Vector3dc pos, final Vector3d dest) {
|
||||
final Pose3dc pose = subLevel.logicalPose();
|
||||
|
||||
if (subLevel instanceof final ServerSubLevel serverSubLevel) {
|
||||
final ServerSubLevelContainer container = SubLevelContainer.getContainer((ServerLevel) level);
|
||||
assert container != null;
|
||||
|
||||
final RigidBodyHandle handle = container.physicsSystem().getPhysicsHandle(serverSubLevel);
|
||||
final Vector3dc linearVelocity = handle.getLinearVelocity(new Vector3d());
|
||||
final Vector3dc angularVelocity = handle.getAngularVelocity(new Vector3d());
|
||||
|
||||
// Use dest as a "temp" variable for calculating the local pos, then set it to the real value after
|
||||
final Vector3dc localPos = pose.transformPosition(pos, dest).sub(pose.position());
|
||||
|
||||
return angularVelocity.cross(localPos, dest).add(linearVelocity);
|
||||
}
|
||||
|
||||
// This uses dest to store the transformed position in, then immediately stores the real value after
|
||||
return pose.transformPosition(pos, new Vector3d())
|
||||
.sub(subLevel.lastPose().transformPosition(pos, dest), dest)
|
||||
.mul(20.0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vec3 getVelocity(final Level level, final SubLevelAccess subLevel, final Vec3 pos) {
|
||||
return JOMLConversion.toMojang(Sable.HELPER.getVelocity(level, subLevel, JOMLConversion.toJOML(pos), new Vector3d()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vec3 getVelocity(final Level level, final SubLevelAccess subLevel, final Position pos) {
|
||||
return JOMLConversion.toMojang(Sable.HELPER.getVelocity(level, subLevel, JOMLConversion.toJOML(pos), new Vector3d()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vector3d getVelocityRelativeToAir(final Level level, final Vector3dc pos, final Vector3d dest) {
|
||||
return SubLevelHelper.getVelocityRelativeToAir(level, pos, dest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vec3 getVelocityRelativeToAir(final Level level, final Vec3 pos) {
|
||||
return JOMLConversion.toMojang(SubLevelHelper.getVelocityRelativeToAir(level, JOMLConversion.toJOML(pos), new Vector3d()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vec3 getVelocityRelativeToAir(final Level level, final Position pos) {
|
||||
return JOMLConversion.toMojang(SubLevelHelper.getVelocityRelativeToAir(level, JOMLConversion.toJOML(pos), new Vector3d()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInPlotGrid(final Level level, final int chunkX, final int chunkZ) {
|
||||
final SubLevelContainer container = SubLevelContainer.getContainer(level);
|
||||
return container != null && container.inBounds(chunkX, chunkZ);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable SubLevel getTrackingSubLevel(final Entity entity) {
|
||||
return ((EntityMovementExtension) entity).sable$getTrackingSubLevel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable SubLevel getLastTrackingSubLevel(final Entity entity) {
|
||||
final UUID uuid = ((EntityMovementExtension) entity).sable$getLastTrackingSubLevelID();
|
||||
if (uuid != null) {
|
||||
final SubLevelContainer container = SubLevelContainer.getContainer(entity.level());
|
||||
return container.getSubLevel(uuid);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable SubLevel getTrackingOrVehicleSubLevel(final Entity entity) {
|
||||
SubLevel trackingSubLevel = Sable.HELPER.getTrackingSubLevel(entity);
|
||||
|
||||
if (trackingSubLevel == null) {
|
||||
trackingSubLevel = Sable.HELPER.getVehicleSubLevel(entity);
|
||||
}
|
||||
|
||||
return trackingSubLevel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable SubLevel getVehicleSubLevel(final Entity entity) {
|
||||
if (entity.getVehicle() != null) {
|
||||
return Sable.HELPER.getContaining(entity.getVehicle());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Vec3 getEyePositionInterpolated(final Entity entity, final float partialTicks) {
|
||||
final SubLevel trackingSubLevel = Sable.HELPER.getTrackingOrVehicleSubLevel(entity);
|
||||
|
||||
if (trackingSubLevel instanceof final ClientSubLevel clientSubLevel) {
|
||||
final Vector3d startPos = new Vector3d(entity.xo, entity.yo + entity.getEyeHeight(), entity.zo);
|
||||
final Vector3d endPos = new Vector3d(entity.getX(), entity.getY() + entity.getEyeHeight(), entity.getZ());
|
||||
|
||||
final Pose3dc renderPose = clientSubLevel.renderPose(partialTicks);
|
||||
clientSubLevel.lastPose().transformPositionInverse(startPos);
|
||||
clientSubLevel.logicalPose().transformPositionInverse(endPos);
|
||||
|
||||
startPos.lerp(endPos, partialTicks);
|
||||
renderPose.transformPosition(startPos);
|
||||
|
||||
return new Vec3(startPos.x, startPos.y, startPos.z);
|
||||
} else {
|
||||
return entity.getEyePosition(partialTicks);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Vector3d getFeetPos(final Entity entity, final float distanceDown) {
|
||||
final Quaterniondc orientation = EntitySubLevelUtil.getCustomEntityOrientation(entity, 1.0f);
|
||||
return Sable.HELPER.getFeetPos(entity, distanceDown, orientation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Level getClientLevel() {
|
||||
throw new UnsupportedOperationException("Should not be called");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package dev.ryanhcode.sable;
|
||||
|
||||
import com.mojang.logging.LogUtils;
|
||||
import dev.ryanhcode.sable.api.physics.PhysicsPipeline;
|
||||
import dev.ryanhcode.sable.api.physics.force.ForceGroups;
|
||||
import dev.ryanhcode.sable.api.sublevel.ServerSubLevelContainer;
|
||||
import dev.ryanhcode.sable.api.sublevel.SubLevelContainer;
|
||||
import dev.ryanhcode.sable.companion.SableCompanion;
|
||||
import dev.ryanhcode.sable.index.SableTags;
|
||||
import dev.ryanhcode.sable.network.tcp.SableTCPPackets;
|
||||
import dev.ryanhcode.sable.physics.config.block_properties.PhysicsBlockPropertiesDefinitionLoader;
|
||||
import dev.ryanhcode.sable.physics.config.block_properties.PhysicsBlockPropertyTypes;
|
||||
import dev.ryanhcode.sable.physics.impl.rapier.RapierPhysicsPipeline;
|
||||
import dev.ryanhcode.sable.sublevel.system.SubLevelPhysicsSystem;
|
||||
import dev.ryanhcode.sable.sublevel.system.SubLevelTrackingSystem;
|
||||
import dev.ryanhcode.sable.sublevel.tracking_points.SubLevelTrackingPointObserver;
|
||||
import net.minecraft.Util;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.Level;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.time.DayOfWeek;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
public final class Sable {
|
||||
|
||||
public static final String MOD_NAME = "Sable";
|
||||
public static final String MOD_ID = "sable";
|
||||
|
||||
public static final String ISSUE_TRACKER_URL = "https://github.com/ryanhcode/sable/issues";
|
||||
|
||||
public static final Logger LOGGER = LogUtils.getLogger();
|
||||
public static final ActiveSableCompanion HELPER = (ActiveSableCompanion) SableCompanion.INSTANCE;
|
||||
|
||||
@ApiStatus.Internal
|
||||
public static void init() {
|
||||
SableTCPPackets.init();
|
||||
SableTags.register();
|
||||
PhysicsBlockPropertyTypes.register();
|
||||
ForceGroups.register();
|
||||
|
||||
LOGGER.info("{} loaded!", MOD_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a physics pipeline with the current configuration.
|
||||
*/
|
||||
public static PhysicsPipeline createPhysicsPipeline(final ServerLevel level) {
|
||||
return new RapierPhysicsPipeline(level);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path the path to the resource
|
||||
* @return a {@link ResourceLocation} with a {@link Sable#MOD_ID} namespace
|
||||
*/
|
||||
public static ResourceLocation sablePath(final String path) {
|
||||
return ResourceLocation.fromNamespaceAndPath(MOD_ID, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes & sets up sub-level containers to contain physics systems and tracking systems by default.
|
||||
*
|
||||
* @param level the level to initialize the container for
|
||||
* @param container the sub-level container to initialize
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
public static void defaultSubLevelContainerInitializer(final Level level, final SubLevelContainer container) {
|
||||
if (container instanceof final ServerSubLevelContainer serverContainer) {
|
||||
final ServerLevel serverLevel = serverContainer.getLevel();
|
||||
|
||||
// Give the container a physics system
|
||||
final SubLevelPhysicsSystem physicsSystem = new SubLevelPhysicsSystem(serverLevel);
|
||||
physicsSystem.initialize();
|
||||
serverContainer.takePhysicsSystem(physicsSystem);
|
||||
|
||||
// Give it a tracking system to notify clients
|
||||
final SubLevelTrackingSystem trackingSystem = new SubLevelTrackingSystem(serverLevel);
|
||||
serverContainer.takeTrackingSystem(trackingSystem);
|
||||
|
||||
serverContainer.addObserver(physicsSystem);
|
||||
serverContainer.addObserver(trackingSystem);
|
||||
serverContainer.addObserver(new SubLevelTrackingPointObserver(serverLevel));
|
||||
|
||||
PhysicsBlockPropertiesDefinitionLoader.INSTANCE.applyAll();
|
||||
}
|
||||
}
|
||||
|
||||
private static final List<String> WITTIER_COMMENTS = List.of(
|
||||
"Hi. I'm Sable and I dislike float casts",
|
||||
"*plays dead*",
|
||||
"It wasn't me (it probably was)",
|
||||
"Lets see if this is repro or cosmic radiation",
|
||||
"What did you do",
|
||||
"ooprs",
|
||||
"dude... thats so mossed up...",
|
||||
"What is this thing",
|
||||
"I am capable of so much more than being a crash log. There has to be more to this world.",
|
||||
"tfw no sable gf",
|
||||
"someone please advice devs that pancakes are serve"
|
||||
);
|
||||
|
||||
private static String getWittierComment() {
|
||||
try {
|
||||
if (LocalDate.now().getDayOfWeek() == DayOfWeek.SUNDAY && Util.getMillis() % 2 == 0) {
|
||||
return "It's sable sunday";
|
||||
}
|
||||
return WITTIER_COMMENTS.get((int) (Util.getMillis() % WITTIER_COMMENTS.size()));
|
||||
} catch (final Throwable t) {
|
||||
return "Wittier comment unavailable :(";
|
||||
}
|
||||
}
|
||||
|
||||
public static String getCrashHeader() {
|
||||
return "\n// " + getWittierComment() +
|
||||
"\nPlease make sure this issue is not caused by Sable before reporting it to other mod authors." +
|
||||
"\nIf you cannot reproduce it without Sable, file a report on the issue tracker" +
|
||||
"\n" + ISSUE_TRACKER_URL +
|
||||
"\n";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package dev.ryanhcode.sable;
|
||||
|
||||
import dev.ryanhcode.sable.debug.SableClientGizmoHandler;
|
||||
import dev.ryanhcode.sable.network.client.SableClientNetworkEventLoop;
|
||||
import dev.ryanhcode.sable.render.dynamic_shade.SableDynamicDirectionalShadingPreProcessor;
|
||||
import dev.ryanhcode.sable.render.sky_light_shadow.SableDynamicSkyLightShadowPreProcessor;
|
||||
import dev.ryanhcode.sable.render.sky_light_shadow.SableSkyLightShadows;
|
||||
import dev.ryanhcode.sable.render.water_occlusion.SableWaterOcclusionPreProcessor;
|
||||
import dev.ryanhcode.sable.render.water_occlusion.WaterOcclusionRenderer;
|
||||
import dev.ryanhcode.sable.sublevel.render.fancy.FancySubLevelShaderProcessor;
|
||||
import dev.ryanhcode.sable.sublevel.storage.debug.SubLevelContainerInspector;
|
||||
import foundry.veil.api.client.editor.EditorManager;
|
||||
import foundry.veil.api.client.render.VeilRenderSystem;
|
||||
import foundry.veil.platform.VeilEventPlatform;
|
||||
import net.minecraft.client.Minecraft;
|
||||
|
||||
public class SableClient {
|
||||
|
||||
public static final SableClientGizmoHandler GIZMO_HANDLER = new SableClientGizmoHandler();
|
||||
public static SableClientNetworkEventLoop NETWORK_EVENT_LOOP = new SableClientNetworkEventLoop();
|
||||
public static WaterOcclusionRenderer WATER_OCCLUSION_RENDERER = new WaterOcclusionRenderer();
|
||||
|
||||
public static void init() {
|
||||
VeilEventPlatform.INSTANCE.onVeilRendererAvailable(renderer -> {
|
||||
if (VeilRenderSystem.hasImGui()) {
|
||||
final EditorManager editorManager = renderer.getEditorManager();
|
||||
|
||||
editorManager.add(new SubLevelContainerInspector());
|
||||
}
|
||||
});
|
||||
|
||||
VeilEventPlatform.INSTANCE.onVeilAddShaderProcessors((provider, registry) -> {
|
||||
registry.addPreprocessor(new SableDynamicDirectionalShadingPreProcessor(), false);
|
||||
registry.addPreprocessor(new SableDynamicSkyLightShadowPreProcessor(), false);
|
||||
registry.addPreprocessor(new SableWaterOcclusionPreProcessor(), false);
|
||||
registry.addPreprocessor(new FancySubLevelShaderProcessor(), false);
|
||||
});
|
||||
|
||||
VeilEventPlatform.INSTANCE.onVeilRenderLevelStage(SableSkyLightShadows::renderShadowMap);
|
||||
|
||||
GIZMO_HANDLER.init();
|
||||
}
|
||||
|
||||
public static boolean useNativeTransport() {
|
||||
final Minecraft client = Minecraft.getInstance();
|
||||
return client.options.useNativeTransport();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package dev.ryanhcode.sable;
|
||||
|
||||
import dev.ryanhcode.sable.api.sublevel.SubLevelContainer;
|
||||
import dev.ryanhcode.sable.mixin.config.GameRendererAccessor;
|
||||
import dev.ryanhcode.sable.mixinterface.plot.SubLevelContainerHolder;
|
||||
import dev.ryanhcode.sable.render.dynamic_shade.SableDynamicDirectionalShading;
|
||||
import dev.ryanhcode.sable.render.sky_light_shadow.SableSkyLightShadows;
|
||||
import dev.ryanhcode.sable.render.water_occlusion.WaterOcclusionRenderer;
|
||||
import dev.ryanhcode.sable.sublevel.ClientSubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.render.SubLevelRenderer;
|
||||
import foundry.veil.api.client.render.VeilRenderSystem;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.neoforged.neoforge.common.ModConfigSpec;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class SableClientConfig {
|
||||
|
||||
public static final ModConfigSpec SPEC;
|
||||
|
||||
public static final ModConfigSpec.BooleanValue SUB_LEVEL_DYNAMIC_SHADING;
|
||||
public static final ModConfigSpec.BooleanValue SUB_LEVEL_WATER_OCCLUSION;
|
||||
public static final ModConfigSpec.BooleanValue SUB_LEVEL_SKYLIGHT_SHADOWS;
|
||||
public static final ModConfigSpec.DoubleValue INTERPOLATION_DELAY;
|
||||
public static final ModConfigSpec.EnumValue<SubLevelRenderer.SelectedRenderer> SELECTED_RENDERER;
|
||||
public static final ModConfigSpec.DoubleValue ZOOM_SENSITIVITY;
|
||||
|
||||
static {
|
||||
final ModConfigSpec.Builder builder = new ModConfigSpec.Builder();
|
||||
|
||||
SUB_LEVEL_DYNAMIC_SHADING = builder
|
||||
.comment("Whether sub-levels should apply block shading dynamically")
|
||||
.define("sub_level_dynamic_shading", true);
|
||||
SUB_LEVEL_WATER_OCCLUSION = builder
|
||||
.comment("Whether sub-levels can occlude the water surface")
|
||||
.define("sub_level_water_occlusion", true);
|
||||
SUB_LEVEL_SKYLIGHT_SHADOWS = builder
|
||||
.comment("Whether sub-levels should cast a shadow on the world")
|
||||
.define("sub_level_skylight_shadows", false);
|
||||
INTERPOLATION_DELAY = builder
|
||||
.comment("The distance back in game-ticks that the snapshot interpolation should operate")
|
||||
.defineInRange("sub_level_snapshot_interpolation_delay_ticks", 1.5, 0.0, 100.0);
|
||||
SELECTED_RENDERER = builder
|
||||
.comment("The renderer to use for sub-levels")
|
||||
.defineEnum("sub_level_renderer", SubLevelRenderer.DEFAULT, Arrays.stream(SubLevelRenderer.SelectedRenderer.values())
|
||||
.filter(SubLevelRenderer.SelectedRenderer::isSupported)
|
||||
.toArray(SubLevelRenderer.SelectedRenderer[]::new));
|
||||
ZOOM_SENSITIVITY = builder
|
||||
.comment("The zoom sensitivity for sub-level camera types")
|
||||
.defineInRange("sub_level_zoom_sensitivity", 0.2, 0.0, 100.0);
|
||||
|
||||
|
||||
SPEC = builder.build();
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
public static void onUpdate(final boolean notify) {
|
||||
boolean reloadShaders = false;
|
||||
boolean reloadChunks = false;
|
||||
|
||||
if (SableDynamicDirectionalShading.isEnabled() != SableClientConfig.SUB_LEVEL_DYNAMIC_SHADING.getAsBoolean()) {
|
||||
SableDynamicDirectionalShading.setIsEnabled(SableClientConfig.SUB_LEVEL_DYNAMIC_SHADING.getAsBoolean());
|
||||
reloadShaders = true;
|
||||
reloadChunks = true;
|
||||
}
|
||||
|
||||
if (SableSkyLightShadows.isEnabled() != SableClientConfig.SUB_LEVEL_SKYLIGHT_SHADOWS.getAsBoolean()) {
|
||||
SableSkyLightShadows.setIsEnabled(SableClientConfig.SUB_LEVEL_SKYLIGHT_SHADOWS.getAsBoolean());
|
||||
reloadShaders = true;
|
||||
}
|
||||
|
||||
if (WaterOcclusionRenderer.isEnabled() != SableClientConfig.SUB_LEVEL_WATER_OCCLUSION.getAsBoolean()) {
|
||||
WaterOcclusionRenderer.setIsEnabled(SableClientConfig.SUB_LEVEL_WATER_OCCLUSION.getAsBoolean());
|
||||
reloadShaders = true;
|
||||
}
|
||||
|
||||
Minecraft.getInstance().execute(() -> SubLevelRenderer.setImpl(SableClientConfig.SELECTED_RENDERER.get()));
|
||||
|
||||
if (notify) {
|
||||
if (reloadShaders) {
|
||||
VeilRenderSystem.renderer().getVanillaShaderCompiler().reload(((GameRendererAccessor) Minecraft.getInstance().gameRenderer).getShaders().values());
|
||||
}
|
||||
|
||||
if (reloadChunks) {
|
||||
Minecraft.getInstance().execute(() -> {
|
||||
VeilRenderSystem.rebuildChunks();
|
||||
final ClientLevel level = Minecraft.getInstance().level;
|
||||
if (level != null) {
|
||||
final SubLevelContainer plotContainer = ((SubLevelContainerHolder) level).sable$getPlotContainer();
|
||||
for (final SubLevel sublevel : plotContainer.getAllSubLevels()) {
|
||||
((ClientSubLevel) sublevel).getRenderData().rebuild();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package dev.ryanhcode.sable;
|
||||
|
||||
import dev.ryanhcode.sable.api.sublevel.ServerSubLevelContainer;
|
||||
import dev.ryanhcode.sable.api.sublevel.SubLevelContainer;
|
||||
import dev.ryanhcode.sable.network.packets.tcp.ClientboundFloatingBlockMaterialPacket;
|
||||
import dev.ryanhcode.sable.network.packets.tcp.ClientboundPhysicsPropertyPacket;
|
||||
import dev.ryanhcode.sable.physics.chunk.VoxelNeighborhoodState;
|
||||
import dev.ryanhcode.sable.physics.config.FloatingBlockMaterialDataHandler;
|
||||
import dev.ryanhcode.sable.physics.config.block_properties.PhysicsBlockPropertiesDefinitionLoader;
|
||||
import dev.ryanhcode.sable.physics.floating_block.FloatingBlockController;
|
||||
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.plot.LevelPlot;
|
||||
import dev.ryanhcode.sable.sublevel.plot.PlotChunkHolder;
|
||||
import dev.ryanhcode.sable.sublevel.plot.heat.SubLevelHeatMapManager;
|
||||
import dev.ryanhcode.sable.sublevel.water_occlusion.WaterOcclusionContainer;
|
||||
import foundry.veil.api.network.VeilPacketManager;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.SectionPos;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||
|
||||
/**
|
||||
* Common events either dispatched to from mixins for hotswapping convenience, or dispatched to from platform-specific events
|
||||
*/
|
||||
public class SableCommonEvents {
|
||||
/**
|
||||
* Handles a change in blockstate in a chunk at chunk-relative position x, y, z.
|
||||
* Only called server-side.
|
||||
*/
|
||||
public static void handleBlockChange(final ServerLevel level, final LevelChunk chunk, final int x, final int y, final int z, final BlockState oldState, final BlockState newState) {
|
||||
final ChunkPos chunkPos = chunk.getPos();
|
||||
|
||||
final ServerSubLevelContainer container = SubLevelContainer.getContainer(level);
|
||||
|
||||
final PlotChunkHolder plotChunk = container.getChunkHolder(chunkPos);
|
||||
|
||||
final int localX = x & SectionPos.SECTION_MASK;
|
||||
final int localZ = z & SectionPos.SECTION_MASK;
|
||||
|
||||
if (plotChunk != null) {
|
||||
final LevelPlot plot = container.getPlot(chunkPos);
|
||||
final BlockPos blockPos = new BlockPos(x, y, z);
|
||||
|
||||
plotChunk.handleBlockChange(localX, y, localZ, oldState, newState);
|
||||
plot.updateBoundingBox();
|
||||
plot.expandIfNecessary(blockPos);
|
||||
|
||||
final SubLevel subLevel = plot.getSubLevel();
|
||||
|
||||
final WaterOcclusionContainer<?> waterOcclusionContainer = WaterOcclusionContainer.getContainer(level);
|
||||
|
||||
if (waterOcclusionContainer != null) {
|
||||
if (VoxelNeighborhoodState.isSolid(level, blockPos, oldState) != VoxelNeighborhoodState.isSolid(level, blockPos, newState)) {
|
||||
waterOcclusionContainer.markDirty(blockPos);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle heatmap addition / removal
|
||||
if (subLevel instanceof final ServerSubLevel serverSubLevel) {
|
||||
final SubLevelHeatMapManager heatMapManager = serverSubLevel.getHeatMapManager();
|
||||
final FloatingBlockController floatingBlockController = serverSubLevel.getFloatingBlockController();
|
||||
|
||||
if (oldState != newState){
|
||||
floatingBlockController.queueRemoveFloatingBlock(oldState, blockPos);
|
||||
floatingBlockController.queueAddFloatingBlock(newState, blockPos);
|
||||
}
|
||||
|
||||
if (oldState.isAir() && !newState.isAir()) {
|
||||
heatMapManager.onSolidAdded(blockPos);
|
||||
}
|
||||
|
||||
if (!oldState.isAir() && newState.isAir()) {
|
||||
heatMapManager.onSolidRemoved(blockPos);
|
||||
}
|
||||
}
|
||||
|
||||
if (subLevel.isRemoved()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final int idx = chunk.getSectionIndex(y);
|
||||
final LevelChunkSection section = chunk.getSection(idx);
|
||||
final SectionPos sectionPos = SectionPos.of(chunkPos, chunk.getSectionYFromSectionIndex(idx));
|
||||
|
||||
container.physicsSystem().handleBlockChange(sectionPos, section, localX, y & 15, localZ, oldState, newState);
|
||||
}
|
||||
|
||||
public static void syncDataPacket(final VeilPacketManager.PacketSink sink) {
|
||||
sink.sendPacket(PhysicsBlockPropertiesDefinitionLoader.INSTANCE.getDefinitions().stream().map(ClientboundPhysicsPropertyPacket::new).toArray(CustomPacketPayload[]::new));
|
||||
sink.sendPacket(FloatingBlockMaterialDataHandler.allMaterials.entrySet().stream().map(e -> new ClientboundFloatingBlockMaterialPacket(e.getKey(), e.getValue())).toArray(CustomPacketPayload[]::new));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package dev.ryanhcode.sable;
|
||||
|
||||
import net.neoforged.neoforge.common.ModConfigSpec;
|
||||
|
||||
public final class SableConfig {
|
||||
|
||||
public static final ModConfigSpec SPEC;
|
||||
|
||||
public static final ModConfigSpec.BooleanValue SUB_LEVEL_SPLITTING;
|
||||
public static final ModConfigSpec.IntValue SUB_LEVEL_SPLITTING_HEATMAP_STEPS_PER_TICK;
|
||||
public static final ModConfigSpec.DoubleValue SUB_LEVEL_TRACKING_RANGE;
|
||||
public static final ModConfigSpec.DoubleValue SUB_LEVEL_REMOVE_MIN;
|
||||
public static final ModConfigSpec.DoubleValue SUB_LEVEL_REMOVE_MAX;
|
||||
public static final ModConfigSpec.DoubleValue VELOCITY_RETAINED_ON_LOAD;
|
||||
public static final ModConfigSpec.DoubleValue SUB_LEVEL_PUNCH_STRENGTH_MULTIPLIER;
|
||||
public static final ModConfigSpec.DoubleValue SUB_LEVEL_PUNCH_DOWNWARD_STRENGTH_MULTIPLIER;
|
||||
public static final ModConfigSpec.IntValue SUB_LEVEL_PUNCH_COOLDOWN_TICKS;
|
||||
public static final ModConfigSpec.BooleanValue DISABLE_UDP_PIPELINE;
|
||||
public static final ModConfigSpec.BooleanValue ATTEMPT_UDP_NETWORKING;
|
||||
|
||||
static {
|
||||
final ModConfigSpec.Builder builder = new ModConfigSpec.Builder();
|
||||
|
||||
SUB_LEVEL_SPLITTING = builder
|
||||
.comment("Whether sub-levels can split when parts are separated")
|
||||
.define("sub_level_splitting", true);
|
||||
SUB_LEVEL_SPLITTING_HEATMAP_STEPS_PER_TICK = builder
|
||||
.comment("Sub-level splitting heatmap steps that take place per tick")
|
||||
.defineInRange("sub_level_splitting_heatmap_steps", 200, 1, Integer.MAX_VALUE);
|
||||
SUB_LEVEL_TRACKING_RANGE = builder
|
||||
.comment("The distance to network sub-levels to players at")
|
||||
.defineInRange("sub_level_tracking_range", 320.0, 1.0, Double.MAX_VALUE);
|
||||
SUB_LEVEL_REMOVE_MIN = builder
|
||||
.comment("The minimum y coordinate sub-levels can exist at")
|
||||
.defineInRange("sub_level_remove_min", -10_000, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
|
||||
SUB_LEVEL_REMOVE_MAX = builder
|
||||
.comment("The maximum y coordinate sub-levels can exist at")
|
||||
.defineInRange("sub_level_remove_max", 100_000, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
|
||||
VELOCITY_RETAINED_ON_LOAD = builder
|
||||
.comment("The fraction of velocity that is retained when a sub-level is loaded in. A value of 0.0 will " +
|
||||
"indicate that no velocity should be carried over, while a value of 1.0 would carry over 100% of " +
|
||||
"velocity on load.")
|
||||
.defineInRange("sub_level_velocity_retained_on_load", 0.9, 0.0, 1.0);
|
||||
SUB_LEVEL_PUNCH_STRENGTH_MULTIPLIER = builder
|
||||
.comment("The strength multiplier applied to sub-level punching impulses")
|
||||
.defineInRange("sub_level_punch_strength_multiplier", 2.1, 0.0, Double.POSITIVE_INFINITY);
|
||||
SUB_LEVEL_PUNCH_DOWNWARD_STRENGTH_MULTIPLIER = builder
|
||||
.comment("The strength multiplier applied to the vertical component of downward sub-level punching impulses (to prevent jumping by punching the ground while standing on something light)")
|
||||
.defineInRange("sub_level_punch_downward_strength_multiplier", 0.175, 0.0, Double.POSITIVE_INFINITY);
|
||||
SUB_LEVEL_PUNCH_COOLDOWN_TICKS = builder
|
||||
.comment("The cooldown in ticks between sub-level punches")
|
||||
.defineInRange("sub_level_punch_cooldown_ticks", 3, 0, Integer.MAX_VALUE);
|
||||
DISABLE_UDP_PIPELINE = builder
|
||||
.comment("If the entire Sable UDP Networking pipeline should be disabled. This can improve compatibility with certain mods like Replay mod and certain networking setups, but will have worse performance and latency for networking sub-levels.")
|
||||
.define("disable_udp_pipeline", false);
|
||||
ATTEMPT_UDP_NETWORKING = builder
|
||||
.comment("If Sable should attempt to authenticate with clients and send them sub-level data over UDP")
|
||||
.define("attempt_udp_networking", true);
|
||||
|
||||
SPEC = builder.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package dev.ryanhcode.sable.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface MixinModVersionConstraint {
|
||||
/**
|
||||
* The version range in maven format
|
||||
*/
|
||||
String value();
|
||||
}
|
||||
@@ -0,0 +1,543 @@
|
||||
package dev.ryanhcode.sable.api;
|
||||
|
||||
import dev.ryanhcode.sable.Sable;
|
||||
import dev.ryanhcode.sable.api.block.BlockSubLevelAssemblyListener;
|
||||
import dev.ryanhcode.sable.api.physics.PhysicsPipeline;
|
||||
import dev.ryanhcode.sable.api.physics.handle.RigidBodyHandle;
|
||||
import dev.ryanhcode.sable.api.sublevel.ServerSubLevelContainer;
|
||||
import dev.ryanhcode.sable.api.sublevel.SubLevelContainer;
|
||||
import dev.ryanhcode.sable.companion.math.BoundingBox3i;
|
||||
import dev.ryanhcode.sable.companion.math.BoundingBox3ic;
|
||||
import dev.ryanhcode.sable.companion.math.JOMLConversion;
|
||||
import dev.ryanhcode.sable.companion.math.Pose3d;
|
||||
import dev.ryanhcode.sable.platform.SableAssemblyPlatform;
|
||||
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.plot.LevelPlot;
|
||||
import dev.ryanhcode.sable.sublevel.system.SubLevelPhysicsSystem;
|
||||
import dev.ryanhcode.sable.sublevel.tracking_points.SubLevelTrackingPointSavedData;
|
||||
import dev.ryanhcode.sable.sublevel.tracking_points.TrackingPoint;
|
||||
import dev.ryanhcode.sable.util.BoundedBitVolume3i;
|
||||
import dev.ryanhcode.sable.util.LevelAccelerator;
|
||||
import it.unimi.dsi.fastutil.Pair;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.SectionPos;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.server.level.FullChunkStatus;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.Clearable;
|
||||
import net.minecraft.world.RandomizableContainer;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.decoration.HangingEntity;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.BellBlock;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.BellAttachType;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Vector2i;
|
||||
import org.joml.Vector3d;
|
||||
import org.joml.Vector3dc;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Utility class for mass movement of collections of blocks between world and plot.
|
||||
*/
|
||||
public class SubLevelAssemblyHelper {
|
||||
|
||||
/**
|
||||
* Assembles a collection of blocks into a sub-level.
|
||||
*
|
||||
* @param level the level in which the blocks are located
|
||||
* @param anchor the block that will be placed at the center of the sub-level
|
||||
* @param blocks all blocks that will be assembled into the sub-level
|
||||
* @param bounds the bounds in which {@link TrackingPoint tracking points} and retained entities will be moved
|
||||
*/
|
||||
public static ServerSubLevel assembleBlocks(final ServerLevel level, final BlockPos anchor, final Iterable<BlockPos> blocks, final BoundingBox3ic bounds) {
|
||||
final ServerSubLevelContainer container = SubLevelContainer.getContainer(level);
|
||||
assert container != null;
|
||||
|
||||
final SubLevel containingSubLevel = Sable.HELPER.getContaining(level, anchor);
|
||||
final Pose3d pose = new Pose3d();
|
||||
|
||||
pose.position().set(anchor.getX() + 0.5, anchor.getY() + 0.5, anchor.getZ() + 0.5);
|
||||
if (containingSubLevel != null) {
|
||||
final Pose3d containingPose = containingSubLevel.logicalPose();
|
||||
containingPose.transformPosition(pose.position());
|
||||
pose.orientation().set(containingPose.orientation());
|
||||
}
|
||||
|
||||
final ServerSubLevel subLevel = (ServerSubLevel) container.allocateNewSubLevel(pose);
|
||||
|
||||
final LevelPlot plot = subLevel.getPlot();
|
||||
plot.newEmptyChunk(plot.getCenterChunk());
|
||||
|
||||
final BlockPos plotAnchor = plot.getCenterBlock();
|
||||
final SubLevelAssemblyHelper.AssemblyTransform transform = new SubLevelAssemblyHelper.AssemblyTransform(anchor, plotAnchor, 0, Rotation.NONE, level);
|
||||
SubLevelAssemblyHelper.moveOtherStuff(level, transform, blocks, bounds);
|
||||
SubLevelAssemblyHelper.moveBlocks(level, transform, blocks);
|
||||
|
||||
final Vector3dc centerOfMass = subLevel.getMassTracker().getCenterOfMass();
|
||||
Vec3 subLevelCenter = Vec3.atLowerCornerOf(anchor);
|
||||
|
||||
if (centerOfMass != null) {
|
||||
subLevelCenter = subLevelCenter
|
||||
.subtract(Vec3.atLowerCornerOf(plotAnchor))
|
||||
.add(centerOfMass.x(), centerOfMass.y(), centerOfMass.z());
|
||||
} else {
|
||||
subLevel.logicalPose().rotationPoint()
|
||||
.set(plotAnchor.getX() + 0.5, plotAnchor.getY() + 0.5, plotAnchor.getZ() + 0.5);
|
||||
}
|
||||
|
||||
subLevel.logicalPose().position().set(subLevelCenter.x, subLevelCenter.y, subLevelCenter.z);
|
||||
|
||||
final SubLevelPhysicsSystem physicsSystem = container.physicsSystem();
|
||||
final PhysicsPipeline pipeline = physicsSystem.getPipeline();
|
||||
|
||||
if (containingSubLevel != null) {
|
||||
kickFromContainingSubLevel(level, physicsSystem, pipeline, subLevel, containingSubLevel);
|
||||
}
|
||||
|
||||
pipeline.teleport(subLevel, subLevel.logicalPose().position(), subLevel.logicalPose().orientation());
|
||||
subLevel.updateLastPose();
|
||||
|
||||
SubLevelAssemblyHelper.moveTrackingPoints(level, bounds, subLevel, transform);
|
||||
|
||||
return subLevel;
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
public static void kickFromContainingSubLevel(final ServerLevel level,
|
||||
final SubLevelPhysicsSystem physicsSystem,
|
||||
final PhysicsPipeline pipeline,
|
||||
final ServerSubLevel subLevel,
|
||||
final SubLevel containingSubLevel) {
|
||||
final Pose3d originalPose = new Pose3d(subLevel.logicalPose());
|
||||
|
||||
final Vector3d velocity = Sable.HELPER.getVelocity(level, subLevel.logicalPose().position(), new Vector3d());
|
||||
final RigidBodyHandle containingHandle = physicsSystem.getPhysicsHandle((ServerSubLevel) containingSubLevel);
|
||||
pipeline.addLinearAndAngularVelocity(subLevel, velocity, containingHandle.getAngularVelocity());
|
||||
|
||||
// re-transform after center of mass is fixed
|
||||
// we don't need to set the orientation again as it couldn't have changed
|
||||
final Pose3d containingPose = containingSubLevel.logicalPose();
|
||||
containingPose.transformPosition(subLevel.logicalPose().position());
|
||||
|
||||
subLevel.setSplitFrom((ServerSubLevel) containingSubLevel, originalPose);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to gather all connected blocks from a given assembly origin. <br/>
|
||||
* searches in a 3x3x3 area around every block
|
||||
*
|
||||
* @param gatherOrigin Origin of the gathering process.
|
||||
* @param level The level this gathering is taking place in.
|
||||
* @param maximumBlocksToAssemble the maximum blocks to gather.
|
||||
* @param frontierPredicate A specific predicate analysed per blockpos visited that is not an AIR block. Exposes the current BlockPos candidate and its blockstate.
|
||||
* @return a {@link GatherResult gather result} that holds the blocks gathered, bounds of the volume, and an error state if gathering was unsuccessful.
|
||||
*/
|
||||
public static @NotNull SubLevelAssemblyHelper.GatherResult gatherConnectedBlocks(final BlockPos gatherOrigin, final ServerLevel level, final int maximumBlocksToAssemble, @Nullable final FrontierPredicate frontierPredicate) {
|
||||
final LinkedHashSet<Pair<BlockPos, BlockState>> frontier = new LinkedHashSet<>(1 << 12);
|
||||
final Set<BlockPos> blocks = new ObjectOpenHashSet<>(1 << 10);
|
||||
final LevelAccelerator accelerator = new LevelAccelerator(level);
|
||||
|
||||
final BlockState gatherOriginState = accelerator.getBlockState(gatherOrigin);
|
||||
|
||||
if (gatherOriginState.isAir()) {
|
||||
return new GatherResult(null, 0, null, GatherResult.State.NO_BLOCKS);
|
||||
}
|
||||
|
||||
frontier.add(Pair.of(gatherOrigin, gatherOriginState));
|
||||
|
||||
int minX = gatherOrigin.getX(), minY = gatherOrigin.getY(), minZ = gatherOrigin.getZ();
|
||||
int maxX = gatherOrigin.getX(), maxY = gatherOrigin.getY(), maxZ = gatherOrigin.getZ();
|
||||
|
||||
|
||||
int blockCount = 0;
|
||||
final BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos();
|
||||
while (!frontier.isEmpty()) {
|
||||
final Pair<BlockPos, BlockState> pair = frontier.removeFirst();
|
||||
final BlockPos pos = pair.key();
|
||||
|
||||
blockCount++;
|
||||
if (blockCount > maximumBlocksToAssemble) {
|
||||
return new GatherResult(null, blockCount, null, GatherResult.State.TOO_MANY_BLOCKS);
|
||||
}
|
||||
|
||||
minX = Math.min(minX, pos.getX());
|
||||
minY = Math.min(minY, pos.getY());
|
||||
minZ = Math.min(minZ, pos.getZ());
|
||||
|
||||
maxX = Math.max(maxX, pos.getX());
|
||||
maxY = Math.max(maxY, pos.getY());
|
||||
maxZ = Math.max(maxZ, pos.getZ());
|
||||
|
||||
blocks.add(pos);
|
||||
|
||||
for (int x = -1; x <= 1; x++) {
|
||||
for (int y = -1; y <= 1; y++) {
|
||||
for (int z = -1; z <= 1; z++) {
|
||||
if (x == 0 && y == 0 && z == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// don't connect corners, only edges
|
||||
final int absTotal = Math.abs(x) + Math.abs(y) + Math.abs(z);
|
||||
if (absTotal == 3) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final BlockPos candidate = mutablePos.set(pos.getX() + x, pos.getY() + y, pos.getZ() + z);
|
||||
|
||||
if (frontier.contains(candidate)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final Direction direction = absTotal == 1 ? Direction.fromDelta(x, y, z) : null;
|
||||
final BlockState candidateState = accelerator.getBlockState(candidate);
|
||||
|
||||
if (candidateState.isAir()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (frontierPredicate != null && !frontierPredicate.isValidConnection(pos, pair.second(), candidate, candidateState, direction)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!blocks.contains(candidate)) {
|
||||
frontier.add(Pair.of(candidate.immutable(), candidateState));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final BoundingBox3i bounds = new BoundingBox3i(
|
||||
minX, minY, minZ,
|
||||
maxX, maxY, maxZ
|
||||
);
|
||||
|
||||
if (blocks.isEmpty()) {
|
||||
return new GatherResult(null, blockCount, null, GatherResult.State.NO_BLOCKS);
|
||||
}
|
||||
|
||||
return new GatherResult(blocks, blockCount, bounds, GatherResult.State.SUCCESS);
|
||||
}
|
||||
|
||||
public static void moveTrackingPoints(final ServerLevel level, final BoundingBox3ic bounds, final ServerSubLevel subLevel, final AssemblyTransform transform) {
|
||||
final SubLevelTrackingPointSavedData data = SubLevelTrackingPointSavedData.getOrLoad(level);
|
||||
final Iterable<Pair<UUID, TrackingPoint>> points = data.getAllTrackingPoints(bounds);
|
||||
|
||||
for (final Pair<UUID, TrackingPoint> entry : points) {
|
||||
final UUID key = entry.key();
|
||||
final TrackingPoint point = new TrackingPoint(
|
||||
subLevel != null,
|
||||
subLevel != null ? subLevel.getUniqueId() : null,
|
||||
subLevel != null ? subLevel.getLastSerializationPointer() : null,
|
||||
JOMLConversion.toJOML(transform.apply(JOMLConversion.toMojang(entry.value().point()))),
|
||||
entry.value().globalPlaceholderPosition()
|
||||
);
|
||||
|
||||
data.setTrackingPoint(key, point);
|
||||
}
|
||||
}
|
||||
|
||||
public static void moveOtherStuff(final ServerLevel level, final AssemblyTransform transform, final Iterable<BlockPos> blocks, final BoundingBox3ic bounds) {
|
||||
final List<Entity> entities = level.getEntitiesOfClass(Entity.class, bounds.toAABB().inflate(2.0));
|
||||
final boolean needsBitSet = needsBitSet(level, bounds, entities);
|
||||
|
||||
if (!needsBitSet) return;
|
||||
|
||||
final BoundedBitVolume3i volume = BoundedBitVolume3i.fromBlocks(blocks);
|
||||
assert volume != null;
|
||||
|
||||
for (final Entity entity : entities) {
|
||||
boolean moveEntity = false;
|
||||
|
||||
if (entity instanceof final HangingEntity hangingEntity) {
|
||||
moveEntity = BlockPos.betweenClosedStream(hangingEntity.calculateSupportBox()).anyMatch(blockPos ->
|
||||
volume.getOccupied(blockPos.getX(), blockPos.getY(), blockPos.getZ()));
|
||||
}
|
||||
|
||||
if (moveEntity) {
|
||||
entity.setPos(transform.apply(entity.position()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean needsBitSet(final ServerLevel level, final BoundingBox3ic bounds, final List<Entity> entities) {
|
||||
return !entities.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* For what good is the movement of a king if his people do not follow?
|
||||
*/
|
||||
public static void moveBlocks(final ServerLevel level, final AssemblyTransform transform, final Iterable<BlockPos> blocks) {
|
||||
final ServerLevel resultingLevel = transform.resultingLevel;
|
||||
|
||||
final LevelAccelerator accelerator = new LevelAccelerator(level);
|
||||
final LevelAccelerator resultingAccelerator = new LevelAccelerator(resultingLevel);
|
||||
|
||||
final List<BlockState> states = new ArrayList<>();
|
||||
|
||||
BlockPos firstBlock = null;
|
||||
Vector2i chunkBoundsMin = null;
|
||||
Vector2i chunkBoundsMax = null;
|
||||
for (final BlockPos block : blocks) {
|
||||
if (firstBlock == null) {
|
||||
firstBlock = block;
|
||||
}
|
||||
final ChunkPos chunk = new ChunkPos(transform.apply(block));
|
||||
|
||||
final Vector2i jomlChunkPos = new Vector2i(chunk.x, chunk.z);
|
||||
if (chunkBoundsMin == null) {
|
||||
chunkBoundsMin = new Vector2i(jomlChunkPos);
|
||||
chunkBoundsMax = new Vector2i(jomlChunkPos);
|
||||
}
|
||||
|
||||
chunkBoundsMin.min(jomlChunkPos);
|
||||
chunkBoundsMax.max(jomlChunkPos);
|
||||
}
|
||||
|
||||
final SubLevel subLevel = Sable.HELPER.getContaining(level, transform.apply(firstBlock));
|
||||
if (subLevel != null) {
|
||||
final LevelPlot plot = subLevel.getPlot();
|
||||
|
||||
for (int chunkX = chunkBoundsMin.x; chunkX <= chunkBoundsMax.x; chunkX++) {
|
||||
for (int chunkZ = chunkBoundsMin.y; chunkZ <= chunkBoundsMax.y; chunkZ++) {
|
||||
if (plot.getChunkHolder(plot.toLocal(new ChunkPos(chunkX, chunkZ))) == null) {
|
||||
plot.newEmptyChunk(new ChunkPos(chunkX, chunkZ));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SableAssemblyPlatform.INSTANCE.setIgnoreOnPlace(resultingLevel, true);
|
||||
for (final BlockPos block : blocks) {
|
||||
final BlockState state = accelerator.getBlockState(block);
|
||||
final BlockPos newPos = transform.apply(block);
|
||||
|
||||
try {
|
||||
final BlockState subLevelState = transform.apply(state);
|
||||
|
||||
if (state.getBlock() instanceof final BlockSubLevelAssemblyListener listener) {
|
||||
listener.beforeMove(level, resultingLevel, state, block, newPos);
|
||||
}
|
||||
|
||||
final BlockEntity blockEntity = level.getBlockEntity(block);
|
||||
|
||||
CompoundTag tag = null;
|
||||
|
||||
if (blockEntity != null) {
|
||||
tag = blockEntity.saveWithFullMetadata(level.registryAccess());
|
||||
|
||||
tag.putInt("x", newPos.getX());
|
||||
tag.putInt("y", newPos.getY());
|
||||
tag.putInt("z", newPos.getZ());
|
||||
}
|
||||
|
||||
if (blockEntity instanceof final RandomizableContainer container) {
|
||||
container.setLootTable(null);
|
||||
}
|
||||
if (blockEntity instanceof final Clearable clearable) {
|
||||
clearable.clearContent();
|
||||
}
|
||||
|
||||
final LevelChunk chunk = resultingAccelerator.getChunk(SectionPos.blockToSectionCoord(newPos.getX()), SectionPos.blockToSectionCoord(newPos.getZ()));
|
||||
|
||||
chunk.setBlockState(newPos, subLevelState, true);
|
||||
states.add(subLevelState);
|
||||
|
||||
final BlockEntity newBlockEntity = resultingLevel.getBlockEntity(newPos);
|
||||
|
||||
if (newBlockEntity != null && tag != null) {
|
||||
newBlockEntity.loadWithComponents(tag, level.registryAccess());
|
||||
}
|
||||
|
||||
if (state.getBlock() instanceof final BlockSubLevelAssemblyListener listener) {
|
||||
listener.afterMove(level, resultingLevel, state, block, newPos);
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
Sable.LOGGER.error("Failed to move block {} at {} to {}", state, block, newPos, e);
|
||||
}
|
||||
}
|
||||
SableAssemblyPlatform.INSTANCE.setIgnoreOnPlace(resultingLevel, false);
|
||||
|
||||
int i = 0;
|
||||
for (final BlockPos untransformed : blocks) {
|
||||
final BlockPos pos = transform.apply(untransformed);
|
||||
|
||||
try {
|
||||
final LevelChunk levelchunk = resultingAccelerator.getChunk(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ()));
|
||||
final BlockState subLevelState = states.get(i);
|
||||
SubLevelAssemblyHelper.markAndNotifyBlock(resultingLevel, pos, levelchunk, Blocks.AIR.defaultBlockState(), subLevelState, 3, 512);
|
||||
} catch (final Exception e) {
|
||||
Sable.LOGGER.error("Failed to mark & notify block {} (untransformed = {})", pos, untransformed, e);
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
SableAssemblyPlatform.INSTANCE.setIgnoreOnPlace(resultingLevel, true);
|
||||
// destroy all the old blocks
|
||||
for (final BlockPos block : blocks) {
|
||||
final BlockState subLevelState = Blocks.AIR.defaultBlockState();
|
||||
|
||||
try {
|
||||
final LevelChunk chunk = accelerator.getChunk(SectionPos.blockToSectionCoord(block.getX()),
|
||||
SectionPos.blockToSectionCoord(block.getZ()));
|
||||
|
||||
chunk.setBlockState(block, subLevelState, true);
|
||||
} catch (final Exception e) {
|
||||
Sable.LOGGER.error("Failed to destroy old block during assembly {}", block, e);
|
||||
}
|
||||
}
|
||||
SableAssemblyPlatform.INSTANCE.setIgnoreOnPlace(resultingLevel, false);
|
||||
|
||||
for (final BlockPos block : blocks) {
|
||||
final BlockState subLevelState = Blocks.AIR.defaultBlockState();
|
||||
resultingLevel.sendBlockUpdated(block, Blocks.STONE.defaultBlockState(), subLevelState, 3);
|
||||
}
|
||||
}
|
||||
|
||||
public static void markAndNotifyBlock(final Level level, final BlockPos pPos, @Nullable final LevelChunk levelchunk, final BlockState oldState, final BlockState newState, final int pFlags, final int pRecursionLeft) {
|
||||
final Block block = newState.getBlock();
|
||||
final BlockState worldState = level.getBlockState(pPos);
|
||||
if (worldState == newState) {
|
||||
if (oldState != worldState) {
|
||||
level.setBlocksDirty(pPos, oldState, worldState);
|
||||
}
|
||||
|
||||
if ((pFlags & 2) != 0 && levelchunk.getFullStatus() != null && levelchunk.getFullStatus().isOrAfter(FullChunkStatus.BLOCK_TICKING)) {
|
||||
level.sendBlockUpdated(pPos, oldState, newState, pFlags);
|
||||
}
|
||||
|
||||
if ((pFlags & 1) != 0) {
|
||||
level.blockUpdated(pPos, oldState.getBlock());
|
||||
if (newState.hasAnalogOutputSignal()) {
|
||||
level.updateNeighbourForOutputSignal(pPos, block);
|
||||
}
|
||||
}
|
||||
|
||||
if ((pFlags & 16) == 0 && pRecursionLeft > 0) {
|
||||
final int i = pFlags & -34;
|
||||
oldState.updateIndirectNeighbourShapes(level, pPos, i, pRecursionLeft - 1);
|
||||
newState.updateNeighbourShapes(level, pPos, i, pRecursionLeft - 1);
|
||||
newState.updateIndirectNeighbourShapes(level, pPos, i, pRecursionLeft - 1);
|
||||
}
|
||||
|
||||
level.onBlockStateChange(pPos, oldState, worldState);
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface FrontierPredicate {
|
||||
|
||||
/**
|
||||
* @param originPos the pos that is attempting to connect to `pos`
|
||||
* @param originState the state that is attempting to connect to `pos`
|
||||
* @param pos the block we are trying to connect to
|
||||
* @param state the state of the block we are trying to connect to
|
||||
* @param directionFrom the direction we are checking connection from, or null if the connection is along diagonals
|
||||
* @return if the connection is valid
|
||||
*/
|
||||
boolean isValidConnection(BlockPos originPos, BlockState originState, BlockPos pos, BlockState state, @Nullable Direction directionFrom);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform for assembly/dissasembly
|
||||
*/
|
||||
public static class AssemblyTransform {
|
||||
|
||||
private final BlockPos anchorPos;
|
||||
private final BlockPos resultingAnchorPos;
|
||||
|
||||
/**
|
||||
* 90-degree counter clockwise increments
|
||||
*/
|
||||
private final int angle;
|
||||
private final Rotation rotation;
|
||||
|
||||
private final ServerLevel resultingLevel;
|
||||
|
||||
public AssemblyTransform(final BlockPos anchorPos,
|
||||
final BlockPos resultingAnchorPos,
|
||||
final int angle,
|
||||
final Rotation rotation,
|
||||
final ServerLevel resultingLevel) {
|
||||
this.anchorPos = anchorPos;
|
||||
this.resultingAnchorPos = resultingAnchorPos;
|
||||
this.angle = angle;
|
||||
this.rotation = rotation;
|
||||
this.resultingLevel = resultingLevel;
|
||||
}
|
||||
|
||||
public Vec3 apply(Vec3 pos) {
|
||||
pos = pos.subtract(this.anchorPos.getCenter())
|
||||
.yRot((float) (this.angle * Math.PI / 2.0))
|
||||
.add(this.resultingAnchorPos.getCenter());
|
||||
return pos;
|
||||
}
|
||||
|
||||
public BlockPos apply(final BlockPos pos) {
|
||||
return BlockPos.containing(this.apply(pos.getCenter()));
|
||||
}
|
||||
|
||||
public BlockState apply(BlockState state) {
|
||||
final Block block = state.getBlock();
|
||||
|
||||
if (block instanceof BellBlock) {
|
||||
if (state.getValue(BlockStateProperties.BELL_ATTACHMENT) == BellAttachType.DOUBLE_WALL)
|
||||
state = state.setValue(BlockStateProperties.BELL_ATTACHMENT, BellAttachType.SINGLE_WALL);
|
||||
return state.setValue(BellBlock.FACING,
|
||||
this.rotation.rotate(state.getValue(BellBlock.FACING)));
|
||||
}
|
||||
|
||||
return state.rotate(this.rotation);
|
||||
}
|
||||
|
||||
public ServerLevel getLevel() {
|
||||
return this.resultingLevel;
|
||||
}
|
||||
|
||||
public Rotation getRotation() {
|
||||
return this.rotation;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of {@link SubLevelAssemblyHelper#gatherConnectedBlocks(BlockPos, ServerLevel, int, FrontierPredicate)} gather connected blocks.
|
||||
*
|
||||
* @param blocks The blocks gathered during the process.
|
||||
* @param boundingBox The total bounding box for this gathering
|
||||
* @param checkedBlocks How many blocks were checked in the process.
|
||||
* @param assemblyState The error state of this process.
|
||||
*/
|
||||
public record GatherResult(@Nullable Set<BlockPos> blocks, int checkedBlocks, @Nullable BoundingBox3i boundingBox,
|
||||
State assemblyState) {
|
||||
public enum State {
|
||||
SUCCESS("commands.sable.sub_level.assemble.connected.success"),
|
||||
TOO_MANY_BLOCKS("commands.sable.sub_level.assemble.connected.too_many_blocks"),
|
||||
NO_BLOCKS("commands.sable.sub_level.assemble.no_blocks");
|
||||
|
||||
public final String errorKey;
|
||||
|
||||
State(final String errorKey) {
|
||||
this.errorKey = errorKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
package dev.ryanhcode.sable.api;
|
||||
|
||||
import dev.ryanhcode.sable.Sable;
|
||||
import dev.ryanhcode.sable.api.block.BlockEntitySubLevelActor;
|
||||
import dev.ryanhcode.sable.companion.math.BoundingBox3d;
|
||||
import dev.ryanhcode.sable.mixinterface.EntityExtension;
|
||||
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectList;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
|
||||
import net.minecraft.commands.arguments.EntityAnchorArgument;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.joml.Vector3d;
|
||||
import org.joml.Vector3dc;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
/**
|
||||
* A helper class for handling interactions between sub-levels<->sub-levels and sub-levels<->levels
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
public final class SubLevelHelper {
|
||||
|
||||
private static final ThreadLocal<EntityRot> oldRot = ThreadLocal.withInitial(EntityRot::new);
|
||||
private static final ObjectList<BiFunction<Vector3dc, Level, Vector3dc>> windProviders = new ObjectArrayList<>();
|
||||
|
||||
/**
|
||||
* Projects a full entity into a subLevel, rotation and all.
|
||||
* Pushing an entity into local space of a sub-level caches old values for its old rotations.
|
||||
* As such, it is important to call {@link SubLevelHelper#popEntityLocal} after calling {@link SubLevelHelper#pushEntityLocal} before pushing another entity.
|
||||
* Projects the entity position, not the eye position.
|
||||
*
|
||||
* @param subLevel The subLevel to project into
|
||||
* @param entity The entity to project
|
||||
*/
|
||||
public static void pushEntityLocal(final SubLevel subLevel, final Entity entity) {
|
||||
SubLevelHelper.pushEntityLocal(subLevel, entity, EntityAnchorArgument.Anchor.FEET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Projects a full entity out of a subLevel, rotation and all.
|
||||
* Uses the cached values from {@link SubLevelHelper#pushEntityLocal}.
|
||||
* Projects the entity position, not the eye position.
|
||||
*
|
||||
* @param subLevel The subLevel to project out of
|
||||
* @param player The entity to project
|
||||
*/
|
||||
public static void popEntityLocal(final SubLevel subLevel, final Entity player) {
|
||||
SubLevelHelper.popEntityLocal(subLevel, player, EntityAnchorArgument.Anchor.FEET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Projects a full entity into a subLevel, rotation and all.
|
||||
* Pushing an entity into local space of a sub-level caches old values for its old rotations.
|
||||
* As such, it is important to call {@link SubLevelHelper#popEntityLocal} after calling {@link SubLevelHelper#pushEntityLocal} before pushing another entity.
|
||||
*
|
||||
* @param subLevel The subLevel to project into
|
||||
* @param entity The entity to project
|
||||
* @param anchor The anchor that should be projected
|
||||
*/
|
||||
public static void pushEntityLocal(final SubLevel subLevel, final Entity entity, final EntityAnchorArgument.Anchor anchor) {
|
||||
if (anchor == EntityAnchorArgument.Anchor.FEET) {
|
||||
((EntityExtension) entity).sable$setPosSuperRaw(subLevel.logicalPose().transformPositionInverse(entity.position()));
|
||||
} else {
|
||||
((EntityExtension) entity).sable$setPosSuperRaw(subLevel.logicalPose().transformPositionInverse(entity.getEyePosition()).add(0.0, -entity.getEyeHeight(), 0.0));
|
||||
}
|
||||
|
||||
Vec3 playerLookAngle = entity.getLookAngle();
|
||||
playerLookAngle = subLevel.logicalPose().transformNormalInverse(playerLookAngle);
|
||||
oldRot.get().copy(entity);
|
||||
|
||||
final Vec3 pTarget = entity.getEyePosition().add(playerLookAngle);
|
||||
final Vec3 vec3 = entity.getEyePosition();
|
||||
final double d0 = pTarget.x - vec3.x;
|
||||
final double d1 = pTarget.y - vec3.y;
|
||||
final double d2 = pTarget.z - vec3.z;
|
||||
final double d3 = Math.sqrt(d0 * d0 + d2 * d2);
|
||||
entity.setXRot(Mth.wrapDegrees((float) (-(Mth.atan2(d1, d3) * (double) (180F / (float) Math.PI)))));
|
||||
entity.setYRot(Mth.wrapDegrees((float) (Mth.atan2(d2, d0) * (double) (180F / (float) Math.PI)) - 90.0F));
|
||||
entity.setYHeadRot(entity.getYRot());
|
||||
|
||||
entity.setDeltaMovement(subLevel.logicalPose().transformNormalInverse(entity.getDeltaMovement()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Projects a full entity out of a subLevel, rotation and all.
|
||||
* Uses the cached values from {@link SubLevelHelper#pushEntityLocal}.
|
||||
*
|
||||
* @param subLevel The subLevel to project out of
|
||||
* @param entity The entity to project
|
||||
* @param anchor The anchor that should be projected
|
||||
*/
|
||||
public static void popEntityLocal(final SubLevel subLevel, final Entity entity, final EntityAnchorArgument.Anchor anchor) {
|
||||
if (anchor == EntityAnchorArgument.Anchor.FEET) {
|
||||
((EntityExtension) entity).sable$setPosSuperRaw(subLevel.logicalPose().transformPosition(entity.position()));
|
||||
} else {
|
||||
((EntityExtension) entity).sable$setPosSuperRaw(subLevel.logicalPose().transformPosition(entity.getEyePosition()).add(0.0, -entity.getEyeHeight(), 0.0));
|
||||
}
|
||||
|
||||
oldRot.get().apply(entity);
|
||||
entity.setDeltaMovement(subLevel.logicalPose().transformNormal(entity.getDeltaMovement()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the global velocity of a point in a level relative to the air, taking into account sublevels and their plots/poses
|
||||
*
|
||||
* @param level the level to check
|
||||
* @param pos the position of the point
|
||||
* @param dest the vector to hold the result
|
||||
* @return the global velocity of the point stored in dest [m/s]
|
||||
*/
|
||||
public static Vector3d getVelocityRelativeToAir(final Level level, final Vector3dc pos, final Vector3d dest) {
|
||||
final Vector3d probePos = new Vector3d(pos);
|
||||
final Vector3d velocity = Sable.HELPER.getVelocity(level, pos, dest);
|
||||
|
||||
for (final BiFunction<Vector3dc, Level, Vector3dc> windProvider : windProviders) {
|
||||
final Vector3dc airVelocity = windProvider.apply(probePos, level);
|
||||
|
||||
if (airVelocity != null) {
|
||||
velocity.sub(airVelocity);
|
||||
}
|
||||
}
|
||||
|
||||
return velocity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a function to get the air velocity of a point in a level
|
||||
*
|
||||
* @param function the function to register
|
||||
*/
|
||||
public static void registerWindProvider(final BiFunction<Vector3dc, Level, Vector3dc> function) {
|
||||
windProviders.add(function);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the chain of sub-levels that should load / unload with the given one
|
||||
*/
|
||||
public static Collection<ServerSubLevel> getLoadingDependencyChain(final ServerSubLevel subLevel) {
|
||||
final ObjectOpenHashSet<ServerSubLevel> visited = new ObjectOpenHashSet<>();
|
||||
final ObjectOpenHashSet<ServerSubLevel> frontier = new ObjectOpenHashSet<>();
|
||||
|
||||
frontier.add(subLevel);
|
||||
|
||||
while (!frontier.isEmpty()) {
|
||||
final ServerSubLevel current = frontier.iterator().next();
|
||||
|
||||
frontier.remove(current);
|
||||
visited.add(current);
|
||||
|
||||
final Iterable<SubLevel> intersecting = Sable.HELPER.getAllIntersecting(current.getLevel(), new BoundingBox3d(current.boundingBox()));
|
||||
|
||||
// Intersecting dependencies
|
||||
for (final SubLevel neighbor : intersecting) {
|
||||
final ServerSubLevel serverNeighbor = (ServerSubLevel) neighbor;
|
||||
|
||||
if (!visited.contains(serverNeighbor)) {
|
||||
frontier.add(serverNeighbor);
|
||||
}
|
||||
}
|
||||
|
||||
// Actor dependencies
|
||||
for (final BlockEntitySubLevelActor actor : current.getPlot().getBlockEntityActors()) {
|
||||
final Iterable<SubLevel> loadingDependencies = actor.sable$getLoadingDependencies();
|
||||
|
||||
if (loadingDependencies == null) continue;
|
||||
|
||||
for (final SubLevel dependency : loadingDependencies) {
|
||||
final ServerSubLevel serverDependency = (ServerSubLevel) dependency;
|
||||
|
||||
if (!visited.contains(serverDependency)) {
|
||||
frontier.add(serverDependency);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return visited;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the chain of sub-levels considered connected
|
||||
*/
|
||||
public static Collection<SubLevel> getConnectedChain(final SubLevel subLevel) {
|
||||
final ObjectOpenHashSet<SubLevel> visited = new ObjectOpenHashSet<>();
|
||||
final ObjectOpenHashSet<SubLevel> frontier = new ObjectOpenHashSet<>();
|
||||
|
||||
frontier.add(subLevel);
|
||||
|
||||
while (!frontier.isEmpty()) {
|
||||
final SubLevel current = frontier.iterator().next();
|
||||
|
||||
frontier.remove(current);
|
||||
visited.add(current);
|
||||
|
||||
// Actor dependencies
|
||||
for (final BlockEntitySubLevelActor actor : current.getPlot().getBlockEntityActors()) {
|
||||
final Iterable<SubLevel> dependencies = actor.sable$getConnectionDependencies();
|
||||
|
||||
if (dependencies == null) continue;
|
||||
|
||||
for (final SubLevel dependency : dependencies) {
|
||||
final SubLevel serverDependency = dependency;
|
||||
|
||||
if (!visited.contains(serverDependency)) {
|
||||
frontier.add(serverDependency);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return visited;
|
||||
}
|
||||
|
||||
private static class EntityRot {
|
||||
|
||||
private float xRot;
|
||||
private float yRot;
|
||||
private float yHeadRot;
|
||||
|
||||
public void apply(final Entity entity) {
|
||||
entity.setXRot(this.xRot);
|
||||
entity.setYRot(this.yRot);
|
||||
entity.setYHeadRot(this.yHeadRot);
|
||||
}
|
||||
|
||||
public void copy(final Entity entity) {
|
||||
this.xRot = entity.getXRot();
|
||||
this.yRot = entity.getYRot();
|
||||
this.yHeadRot = entity.getYHeadRot();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package dev.ryanhcode.sable.api.block;
|
||||
|
||||
import dev.ryanhcode.sable.api.physics.handle.RigidBodyHandle;
|
||||
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* An interface for sub-classes of {@link net.minecraft.world.level.block.entity.BlockEntity} to implement behaviour
|
||||
* when mounted on a sub-level.
|
||||
*/
|
||||
public interface BlockEntitySubLevelActor {
|
||||
|
||||
/**
|
||||
* Called once per server game tick when this actor is on a {@link SubLevel}
|
||||
*/
|
||||
default void sable$tick(final ServerSubLevel subLevel) {}
|
||||
|
||||
/**
|
||||
* Called once per **physics** tick when this actor is on a {@link SubLevel}.
|
||||
* There may be multiple physics ticks per tick.
|
||||
*
|
||||
* @param subLevel the sub-level this block entity is on
|
||||
* @param timeStep the time this physics tick is stepping
|
||||
*/
|
||||
default void sable$physicsTick(final ServerSubLevel subLevel, final RigidBodyHandle handle, final double timeStep) {}
|
||||
|
||||
/**
|
||||
* Returns the loading dependencies this block-entity has on other sub-levels.
|
||||
* Loading dependencies are used to unload and load a group of sub-levels together.
|
||||
* By default, loading dependencies are assumed from the connection dependencies.
|
||||
* <p>
|
||||
* Note that this may be called after chunks have been un-loaded, and as such, direct level access
|
||||
* should not be done to fetch the dependencies.
|
||||
*
|
||||
* @return a collection of loading dependencies on other loaded sub-levels, or null for none
|
||||
*/
|
||||
@Nullable
|
||||
default Iterable<@NotNull SubLevel> sable$getLoadingDependencies() {
|
||||
return this.sable$getConnectionDependencies();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the connections this block-entity has on other sub-levels.
|
||||
* Connections are used to dictate sub-levels that should be treated as one by many systems.
|
||||
* <p>
|
||||
* Note that this may be called after chunks have been un-loaded, and as such, direct level access
|
||||
* should not be done to fetch the dependencies.
|
||||
* @return a collection of connection dependencies on other loaded sub-levels, or null for none
|
||||
*/
|
||||
@Nullable
|
||||
default Iterable<@NotNull SubLevel> sable$getConnectionDependencies() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package dev.ryanhcode.sable.api.block;
|
||||
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import org.joml.Vector3d;
|
||||
import dev.ryanhcode.sable.physics.config.block_properties.PhysicsBlockPropertyTypes;
|
||||
|
||||
/**
|
||||
* An interface for sub-classes of {@link net.minecraft.world.level.block.entity.BlockEntity} to provide angular momentum
|
||||
* when mounted on a sub-level.
|
||||
*/
|
||||
public interface BlockEntitySubLevelReactionWheel {
|
||||
/**
|
||||
* Get the angular velocity of this reaction wheel, in radians per second.
|
||||
* The total angular momentum given to the sublevel is this velocity scaled by {@link dev.ryanhcode.sable.physics.config.block_properties.PhysicsBlockPropertyTypes#INERTIA}
|
||||
* and by {@link dev.ryanhcode.sable.physics.config.block_properties.PhysicsBlockPropertyTypes#MASS}.
|
||||
*
|
||||
* @param angularVelocity Angular velocity to be set, using {@link org.joml.Vector3d#set(double, double, double)} or similar
|
||||
*/
|
||||
void sable$getAngularVelocity(Vector3d angularVelocity);
|
||||
|
||||
/**
|
||||
* The default block state getter for block entities
|
||||
* @return The block state for this block entity
|
||||
*/
|
||||
BlockState getBlockState();
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package dev.ryanhcode.sable.api.block;
|
||||
|
||||
import dev.ryanhcode.sable.api.SubLevelAssemblyHelper;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
/**
|
||||
* An interface for sub-classes of {@link net.minecraft.world.level.block.Block} to implement that indicates the
|
||||
* {@link SubLevelAssemblyHelper} should notify the block any time it is "moved" as a part of sub-level assembly.
|
||||
*/
|
||||
public interface BlockSubLevelAssemblyListener {
|
||||
|
||||
/**
|
||||
* Called before the {@link SubLevelAssemblyHelper} has moved a block of state newState from oldPos to newPos.
|
||||
*
|
||||
* @param originLevel the level the block will be moved from
|
||||
* @param resultingLevel the level the block will be moved to
|
||||
* @param newState the new block state
|
||||
* @param oldPos the old block position
|
||||
* @param newPos the new block position
|
||||
*/
|
||||
default void beforeMove(final ServerLevel originLevel, final ServerLevel resultingLevel, final BlockState newState, final BlockPos oldPos, final BlockPos newPos) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Called after the {@link SubLevelAssemblyHelper} has moved a block of state newState from oldPos to newPos.
|
||||
* At this point in time during the move, the old block has not been removed.
|
||||
*
|
||||
* @param originLevel the level the block was moved from
|
||||
* @param resultingLevel the level the block was moved to
|
||||
* @param newState the new block state
|
||||
* @param oldPos the old block position
|
||||
* @param newPos the new block position
|
||||
*/
|
||||
void afterMove(ServerLevel originLevel, ServerLevel resultingLevel, BlockState newState, BlockPos oldPos, BlockPos newPos);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package dev.ryanhcode.sable.api.block;
|
||||
|
||||
import net.minecraft.world.level.BlockGetter;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.shapes.VoxelShape;
|
||||
|
||||
/**
|
||||
* Interface for sub-classes of {@link net.minecraft.world.level.block.Block} to implement to specify a separate
|
||||
* collision shape for sub-level physics.
|
||||
*/
|
||||
public interface BlockSubLevelCollisionShape {
|
||||
|
||||
/**
|
||||
* Gets the collision shape that will be baked for a given block-state of this block.
|
||||
*
|
||||
* @param blockGetter the blockGetter to bake the collision shape for
|
||||
* @param state the block state to bake the collision shape for
|
||||
* @return the collision shape that should be used for this block state
|
||||
*/
|
||||
VoxelShape getSubLevelCollisionShape(final BlockGetter blockGetter, final BlockState state);
|
||||
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package dev.ryanhcode.sable.api.block;
|
||||
|
||||
import net.minecraft.world.level.BlockGetter;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import org.joml.Vector3dc;
|
||||
|
||||
/**
|
||||
* Interface for sub-classes of {@link net.minecraft.world.level.block.Block} to implement to specify a custom center
|
||||
* of mass for sub-level physics.
|
||||
*/
|
||||
public interface BlockSubLevelCustomCenterOfMass {
|
||||
|
||||
/**
|
||||
* Gets the center of mass that will be baked for a given block-state of this block.
|
||||
*
|
||||
* @param blockGetter the blockGetter to bake the center of mass for
|
||||
* @param state the block state to bake the center of mass for
|
||||
* @return the center of mass relative to the lower corner of the block
|
||||
*/
|
||||
Vector3dc getCenterOfMass(final BlockGetter blockGetter, final BlockState state);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package dev.ryanhcode.sable.api.block;
|
||||
|
||||
import dev.ryanhcode.sable.api.physics.collider.VoxelColliderData;
|
||||
|
||||
/**
|
||||
* An interface for sub-classes of {@link net.minecraft.world.level.block.Block} to implement that indicates they
|
||||
* have a dynamic collider. Dynamic colliders will be significantly more performance
|
||||
*/
|
||||
public interface BlockSubLevelDynamicCollider {
|
||||
|
||||
void buildBoxes(VoxelColliderData data);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package dev.ryanhcode.sable.api.block;
|
||||
|
||||
import dev.ryanhcode.sable.companion.math.Pose3d;
|
||||
import dev.ryanhcode.sable.physics.config.dimension_physics.DimensionPhysicsData;
|
||||
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Vector3d;
|
||||
import org.joml.Vector3dc;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public interface BlockSubLevelLiftProvider {
|
||||
|
||||
Direction[] DIRECTIONS = Direction.values();
|
||||
|
||||
// memory optimization
|
||||
Vector3d LIFT_FORCE = new Vector3d();
|
||||
Vector3d LIFT_POS = new Vector3d();
|
||||
Vector3d LIFT_NORMAL = new Vector3d();
|
||||
|
||||
Vector3d LIFT_VELO = new Vector3d();
|
||||
Vector3d DRAG = new Vector3d();
|
||||
Vector3d TEMP = new Vector3d();
|
||||
|
||||
/**
|
||||
* Resets the vectors to their identity.
|
||||
*/
|
||||
static void resetVectors() {
|
||||
LIFT_VELO.set(0, 0, 0);
|
||||
LIFT_POS.set(0, 0, 0);
|
||||
LIFT_FORCE.set(0, 0, 0);
|
||||
LIFT_NORMAL.set(0, 0, 0);
|
||||
DRAG.set(0, 0, 0);
|
||||
}
|
||||
|
||||
static List<LiftProviderGroup> groupLiftProviders(final Collection<LiftProviderContext> liftProviders) {
|
||||
final List<LiftProviderGroup> groups = new ObjectArrayList<>();
|
||||
final Set<BlockPos> positions = new ObjectOpenHashSet<>(liftProviders.size());
|
||||
|
||||
for (final LiftProviderContext liftProvider : liftProviders) {
|
||||
positions.add(liftProvider.pos);
|
||||
}
|
||||
|
||||
while (!positions.isEmpty()) {
|
||||
// run a flood-fill
|
||||
final Set<BlockPos> groupBlocks = new ObjectOpenHashSet<>();
|
||||
final List<BlockPos> toVisit = new ObjectArrayList<>();
|
||||
|
||||
toVisit.add(positions.iterator().next());
|
||||
|
||||
while (!toVisit.isEmpty()) {
|
||||
final BlockPos pos = toVisit.removeLast();
|
||||
|
||||
if (groupBlocks.contains(pos)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
groupBlocks.add(pos);
|
||||
positions.remove(pos);
|
||||
|
||||
for (final Direction direction : DIRECTIONS) {
|
||||
final BlockPos offsetPos = pos.relative(direction);
|
||||
|
||||
if (positions.contains(offsetPos)) {
|
||||
toVisit.add(offsetPos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
groups.add(new LiftProviderGroup(groupBlocks));
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param state The current blockstate of this lift provider
|
||||
* @return The normal of this lift provider
|
||||
*/
|
||||
@NotNull
|
||||
Direction sable$getNormal(BlockState state);
|
||||
|
||||
/**
|
||||
* Adjust {@link BlockSubLevelLiftProvider#sable$getDirectionlessDragScalar()} if this value is changed <br>
|
||||
* @return How effective this lift provider is at producing drag parallel to the normal.
|
||||
*/
|
||||
default float sable$getParallelDragScalar() {
|
||||
return 0.75F;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code parallelDragScalar = k1, liftScalar = k2 }<br>
|
||||
* Should be at minimum {@code (-k1 + sqrt(k1^2 + k2^2)) / 2} to prevent exponential velocity gain. <br>
|
||||
* @return How effective this lift provider is at producing directionless drag.
|
||||
*/
|
||||
default float sable$getDirectionlessDragScalar() {
|
||||
return 0.06888202261f; // (-0.75 + sqrt(0.75^2 + 0.475^2)) / 2
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust {@link BlockSubLevelLiftProvider#sable$getDirectionlessDragScalar()} if this value is changed <br>
|
||||
* @return How effective this lift provider is at producing lift.
|
||||
*/
|
||||
default float sable$getLiftScalar() {
|
||||
return 0.475f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called once per **physics** tick when this LiftProvider is on a {@link SubLevel}.
|
||||
* There may be multiple physics ticks per tick. <br/>
|
||||
*
|
||||
* @param ctx The in world context of this lift provider.
|
||||
* @param subLevel The sub-level this lift provider is on
|
||||
* @param localPose The pose of the contraption this lift provider is in, if any
|
||||
* @param timeStep The time step between physics ticks
|
||||
* @param linearVelocity The linear velocity of the data
|
||||
* @param angularVelocity The angular velocity of the data
|
||||
* @param linearImpulse Mutable vector to sum the linear impulse
|
||||
* @param angularImpulse Mutable vector to sum the angular impulse
|
||||
*/
|
||||
default void sable$contributeLiftAndDrag(final LiftProviderContext ctx, final ServerSubLevel subLevel,
|
||||
@NotNull final Pose3d localPose, final double timeStep,
|
||||
final Vector3dc linearVelocity, final Vector3dc angularVelocity,
|
||||
final Vector3d linearImpulse, final Vector3d angularImpulse,
|
||||
@Nullable final LiftProviderGroup group) {
|
||||
resetVectors();
|
||||
LIFT_NORMAL.set(ctx.dir.x(), ctx.dir.y(), ctx.dir.z());
|
||||
LIFT_POS.set(ctx.pos.getX() + 0.5, ctx.pos.getY() + 0.5, ctx.pos.getZ() + 0.5);
|
||||
|
||||
if (localPose != null) {
|
||||
localPose.transformNormal(LIFT_NORMAL);
|
||||
localPose.transformPosition(LIFT_POS);
|
||||
}
|
||||
|
||||
final Pose3d pose = subLevel.logicalPose();
|
||||
final double pressure = DimensionPhysicsData.getAirPressure(subLevel.getLevel(), pose.transformPosition(LIFT_POS, TEMP));
|
||||
|
||||
// transform VELO to be the local velocity at the center of the block
|
||||
// TEMP = transformed POS
|
||||
// VELO = linVel + angVel cross TEMP
|
||||
// VELO = inv transformed VELO
|
||||
pose.transformPosition(LIFT_POS, TEMP).sub(pose.position());
|
||||
LIFT_VELO.set(linearVelocity).add(angularVelocity.cross(TEMP, TEMP));
|
||||
pose.transformNormalInverse(LIFT_VELO);
|
||||
|
||||
LIFT_FORCE.zero();
|
||||
|
||||
if (this.sable$getParallelDragScalar() > 0) {
|
||||
// DRAG = NORMAL * (NORMAL dot VELO)
|
||||
// FORCE = DRAG * scalars
|
||||
final double dragStrength = LIFT_NORMAL.dot(LIFT_VELO) * this.sable$getParallelDragScalar() * pressure * timeStep;
|
||||
final Vector3d parallelDrag = LIFT_NORMAL.mul(dragStrength, DRAG);
|
||||
LIFT_FORCE.add(parallelDrag);
|
||||
|
||||
if (group != null) {
|
||||
group.totalDrag.sub(parallelDrag);
|
||||
group.dragCenter.fma(Math.abs(dragStrength), LIFT_POS);
|
||||
group.totalDragStrength += Math.abs(dragStrength);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.sable$getDirectionlessDragScalar() > 0) {
|
||||
// TEMP = VELO * scalars
|
||||
// FORCE += TEMP
|
||||
final double dragStrength = this.sable$getDirectionlessDragScalar() * pressure * timeStep;
|
||||
final Vector3d directionlessDrag = LIFT_VELO.mul(dragStrength, TEMP);
|
||||
LIFT_FORCE.add(directionlessDrag);
|
||||
|
||||
if (group != null) {
|
||||
group.totalDrag.sub(directionlessDrag);
|
||||
group.dragCenter.fma(directionlessDrag.length(), LIFT_POS);
|
||||
group.totalDragStrength += directionlessDrag.length();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.sable$getLiftScalar() > 0) {
|
||||
// TEMP = VELO - DRAG
|
||||
// TEMP = NORMAL * |TEMP| * scalars
|
||||
// FORCE += TEMP
|
||||
final double liftStrength = LIFT_VELO.sub(DRAG, TEMP).length() * this.sable$getLiftScalar() * pressure * timeStep;
|
||||
final Vector3d lift = LIFT_NORMAL.mul(liftStrength, TEMP);
|
||||
LIFT_FORCE.add(lift);
|
||||
|
||||
if (group != null) {
|
||||
group.totalLift.sub(lift);
|
||||
group.liftCenter.fma(Math.abs(liftStrength), LIFT_POS);
|
||||
group.totalLiftStrength += liftStrength;
|
||||
}
|
||||
}
|
||||
|
||||
// why is this all negative of what it should be?
|
||||
linearImpulse.sub(LIFT_FORCE);
|
||||
LIFT_POS.sub(subLevel.getMassTracker().getCenterOfMass(), TEMP);
|
||||
angularImpulse.sub(TEMP.cross(LIFT_FORCE));
|
||||
resetVectors();
|
||||
}
|
||||
|
||||
record LiftProviderContext(BlockPos pos, BlockState state, Vec3 dir) {
|
||||
}
|
||||
|
||||
final class LiftProviderGroup {
|
||||
private final Set<BlockPos> positions;
|
||||
private final Vector3d totalLift = new Vector3d();
|
||||
private final Vector3d liftCenter = new Vector3d();
|
||||
private final Vector3d totalDrag = new Vector3d();
|
||||
private final Vector3d dragCenter = new Vector3d();
|
||||
public double totalLiftStrength;
|
||||
public double totalDragStrength;
|
||||
|
||||
public LiftProviderGroup(final Set<BlockPos> positions) {
|
||||
this.positions = positions;
|
||||
}
|
||||
|
||||
public Set<BlockPos> positions() {
|
||||
return this.positions;
|
||||
}
|
||||
|
||||
public Vector3d totalLift() {
|
||||
return this.totalLift;
|
||||
}
|
||||
|
||||
public Vector3d liftCenter() {
|
||||
return this.liftCenter;
|
||||
}
|
||||
|
||||
public Vector3d totalDrag() {
|
||||
return this.totalDrag;
|
||||
}
|
||||
|
||||
public Vector3d dragCenter() {
|
||||
return this.dragCenter;
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package dev.ryanhcode.sable.api.block;
|
||||
|
||||
import dev.ryanhcode.sable.api.physics.callback.BlockSubLevelCollisionCallback;
|
||||
import dev.ryanhcode.sable.mixinterface.block_properties.BlockStateExtension;
|
||||
import dev.ryanhcode.sable.physics.callback.FragileBlockCallback;
|
||||
import dev.ryanhcode.sable.physics.config.block_properties.PhysicsBlockPropertyTypes;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
/**
|
||||
* Interface for sub-classes of {@link net.minecraft.world.level.block.Block} to implement for physics collision callbacks.
|
||||
*/
|
||||
public interface BlockWithSubLevelCollisionCallback {
|
||||
|
||||
/**
|
||||
* Gets the collision callback a given block state should have
|
||||
* @param state the block state to check
|
||||
* @return the block collision callback that should be used for that state
|
||||
*/
|
||||
static BlockSubLevelCollisionCallback sable$getCallback(final BlockState state) {
|
||||
if (state.getBlock() instanceof final BlockWithSubLevelCollisionCallback blockCollisionCallback) {
|
||||
return blockCollisionCallback.sable$getCallback();
|
||||
}
|
||||
|
||||
if (((BlockStateExtension) state).sable$getProperty(PhysicsBlockPropertyTypes.FRAGILE.get())) {
|
||||
return FragileBlockCallback.INSTANCE;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a block state should have a collision callback used
|
||||
* @param state the block state to check
|
||||
* @return if the block state should have collision callbacks used
|
||||
*/
|
||||
static boolean hasCallback(final BlockState state) {
|
||||
return sable$getCallback(state) != null;
|
||||
}
|
||||
|
||||
BlockSubLevelCollisionCallback sable$getCallback();
|
||||
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package dev.ryanhcode.sable.api.block.propeller;
|
||||
|
||||
import dev.ryanhcode.sable.Sable;
|
||||
import dev.ryanhcode.sable.companion.math.JOMLConversion;
|
||||
import dev.ryanhcode.sable.physics.config.dimension_physics.DimensionPhysicsData;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.level.Level;
|
||||
import org.joml.Vector3d;
|
||||
|
||||
/**
|
||||
* Spinny spin spin, woosh woosh!
|
||||
*/
|
||||
public interface BlockEntityPropeller {
|
||||
|
||||
/**
|
||||
* @return the direction of the propeller
|
||||
*/
|
||||
Direction getBlockDirection();
|
||||
|
||||
/**
|
||||
* @return airflow in units of [m/s]
|
||||
*/
|
||||
double getAirflow();
|
||||
|
||||
/**
|
||||
* @return thrust in [pN]
|
||||
*/
|
||||
double getThrust();
|
||||
|
||||
/**
|
||||
* @return if the propeller is active / thrust should be computed
|
||||
*/
|
||||
boolean isActive();
|
||||
|
||||
/**
|
||||
* @return the thrust scaled by -1 * airflow scaling * air pressure
|
||||
*/
|
||||
default double getScaledThrust() {
|
||||
return -this.getThrust() * this.getAirflowScaling() * this.getCurrentAirPressure();
|
||||
}
|
||||
|
||||
default double getCurrentAirPressure() {
|
||||
final Level level = this.getLevel();
|
||||
return DimensionPhysicsData.getAirPressure(level, Sable.HELPER.projectOutOfSubLevel(level, JOMLConversion.toJOML(this.getBlockPos().getCenter())));
|
||||
}
|
||||
|
||||
default double getAirflowScaling() {
|
||||
final double airflow = this.getAirflow();
|
||||
|
||||
if (Math.abs(airflow) <= 0.001) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
final Level level = this.getLevel();
|
||||
final Vector3d pos = JOMLConversion.toJOML(this.getBlockPos().getCenter());
|
||||
final SubLevel subLevel = Sable.HELPER.getContaining(level, this.getBlockPos());
|
||||
|
||||
if (subLevel == null) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
final Vector3d velocity = Sable.HELPER.getVelocity(level, subLevel, pos, new Vector3d());
|
||||
final Vector3d thrustDirection = subLevel.logicalPose().transformNormal(JOMLConversion.atLowerCornerOf(this.getBlockDirection().getNormal()));
|
||||
|
||||
return Math.clamp((airflow + velocity.dot(thrustDirection.x, thrustDirection.y, thrustDirection.z)) / airflow, 0, 1);
|
||||
}
|
||||
|
||||
Level getLevel();
|
||||
|
||||
BlockPos getBlockPos();
|
||||
}
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package dev.ryanhcode.sable.api.block.propeller;
|
||||
|
||||
import dev.ryanhcode.sable.api.block.BlockEntitySubLevelActor;
|
||||
import dev.ryanhcode.sable.api.physics.force.ForceGroups;
|
||||
import dev.ryanhcode.sable.api.physics.force.QueuedForceGroup;
|
||||
import dev.ryanhcode.sable.api.physics.handle.RigidBodyHandle;
|
||||
import dev.ryanhcode.sable.companion.math.JOMLConversion;
|
||||
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.joml.Vector3d;
|
||||
|
||||
public interface BlockEntitySubLevelPropellerActor extends BlockEntitySubLevelActor {
|
||||
|
||||
Vector3d THRUST_VECTOR = new Vector3d();
|
||||
Vector3d THRUST_POSITION = new Vector3d();
|
||||
|
||||
BlockEntityPropeller getPropeller();
|
||||
|
||||
@Override
|
||||
default void sable$physicsTick(final ServerSubLevel subLevel, final RigidBodyHandle handle, final double timeStep) {
|
||||
final BlockEntityPropeller prop = this.getPropeller();
|
||||
|
||||
if (prop.isActive()) {
|
||||
final Vec3 thrustDirection = Vec3.atLowerCornerOf(prop.getBlockDirection().getNormal());
|
||||
this.applyForces(subLevel, thrustDirection, timeStep);
|
||||
}
|
||||
}
|
||||
|
||||
default void applyForces(final ServerSubLevel subLevel, final Vec3 thrustDirection, final double timeStep) {
|
||||
final BlockEntityPropeller prop = this. getPropeller();
|
||||
final Vec3 thrust = thrustDirection.scale(prop.getScaledThrust() * timeStep);
|
||||
|
||||
THRUST_POSITION.set(JOMLConversion.atCenterOf(prop.getBlockPos()));
|
||||
THRUST_VECTOR.set(thrust.x, thrust.y, thrust.z);
|
||||
|
||||
final QueuedForceGroup forceGroup = subLevel.getOrCreateQueuedForceGroup(ForceGroups.PROPULSION.get());
|
||||
forceGroup.applyAndRecordPointForce(new Vector3d(THRUST_POSITION), new Vector3d(THRUST_VECTOR));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package dev.ryanhcode.sable.api.command;
|
||||
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
|
||||
import dev.ryanhcode.sable.api.physics.PhysicsPipeline;
|
||||
import dev.ryanhcode.sable.api.sublevel.ServerSubLevelContainer;
|
||||
import dev.ryanhcode.sable.api.sublevel.SubLevelContainer;
|
||||
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.system.SubLevelPhysicsSystem;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public class SableCommandHelper {
|
||||
|
||||
private static final SimpleCommandExceptionType MISSING_SUBLEVEL_CONTAINER
|
||||
= new SimpleCommandExceptionType(Component.translatable("commands.sable.helper.missing_sub_level_container"));
|
||||
private static final SimpleCommandExceptionType MISSING_PHYSICS_SYSTEM
|
||||
= new SimpleCommandExceptionType(Component.translatable("commands.sable.helper.missing_physics_system"));
|
||||
public static final SimpleCommandExceptionType ERROR_NO_SUB_LEVELS_FOUND = new SimpleCommandExceptionType(Component.translatable("commands.sable.fail.no_sub_levels"));
|
||||
public static final SimpleCommandExceptionType ERROR_NOT_INSIDE_SUB_LEVEL = new SimpleCommandExceptionType(Component.translatable("commands.sable.fail.not_inside_sub_level"));
|
||||
public static final SimpleCommandExceptionType ERROR_NO_AXIS_FOR_ROTATION = new SimpleCommandExceptionType(Component.translatable("commands.sable.fail.no_axis_for_rotation"));
|
||||
public static final SimpleCommandExceptionType ERROR_NO_SUB_LEVELS_MODIFIED = new SimpleCommandExceptionType(Component.translatable("commands.sable.fail.unmodified"));
|
||||
public static final SimpleCommandExceptionType ERROR_SUB_LEVEL_UNNAMED = new SimpleCommandExceptionType(Component.translatable("commands.sable.sub_level.get_name.failure_unnamed"));
|
||||
|
||||
// Component utilities related to sub-levels
|
||||
|
||||
/**
|
||||
* Returns a formatted component, with one of the arguments describing the subLevels parameter, being either:
|
||||
* <ul>
|
||||
* <li>The name of the data or "sub-level" if there is only one sub-level</li>
|
||||
* <li>The number of sub-levels in the collection if there are multiple</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param translationKey The translation key to use
|
||||
* @param subLevels The collection of sub-levels to describe
|
||||
* @param subLevelsDescriptionIndex The index of the sub-levels description in the args array
|
||||
* @param additionalArguments The additional arguments to pass to the translation key
|
||||
*/
|
||||
public static Component getResultComponentForSublevelCollection(final String translationKey, final Collection<ServerSubLevel> subLevels,
|
||||
final int subLevelsDescriptionIndex, final Object... additionalArguments) {
|
||||
final boolean isPlural = subLevels.size() != 1;
|
||||
|
||||
// Varargs of an Object type don't handle arrays so it has to be manually collected
|
||||
final Object[] translationArguments = new Object[additionalArguments.length + 1];
|
||||
System.arraycopy(additionalArguments, 0, translationArguments, 1, additionalArguments.length);
|
||||
|
||||
if (isPlural) {
|
||||
translationArguments[0] = Component.translatable("commands.sable.sub_levels", subLevels.size());
|
||||
} else {
|
||||
final SubLevel subLevel = subLevels.iterator().next();
|
||||
final Object name = subLevel.getName() == null ? Component.translatable("commands.sable.sub_level") : subLevel.getName();
|
||||
translationArguments[0] = name;
|
||||
}
|
||||
|
||||
if (subLevelsDescriptionIndex != 0) {
|
||||
final Object swap = translationArguments[subLevelsDescriptionIndex];
|
||||
translationArguments[subLevelsDescriptionIndex] = translationArguments[0];
|
||||
translationArguments[0] = swap;
|
||||
}
|
||||
|
||||
return Component.translatable(translationKey, translationArguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a formatted component, where the specified translation key is given a description of the sub-levels collection</br>
|
||||
* See {@link SableCommandHelper#getResultComponentForSublevelCollection} for more info about the description
|
||||
* <i>Functionally an overload of {@link SableCommandHelper#getResultComponentForSublevelCollection}, but with a different name due to the Object varargs.<i/>
|
||||
* @param translationKey The translation key to use
|
||||
* @param context The command context to send the message to
|
||||
* @param subLevels The collection of sub-levels to describe
|
||||
* @param additionalArguments The additional arguments to pass to the translation key
|
||||
*/
|
||||
public static void sendSuccessDescribingSubLevels(final String translationKey, final CommandContext<CommandSourceStack> context, final Collection<ServerSubLevel> subLevels,
|
||||
final Object... additionalArguments) {
|
||||
sendSuccessDescribingSubLevelsAtIndex(translationKey, context, subLevels, 0, additionalArguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a formatted component, where the specified translation key is given a description of the sub-levels collection, in the index specified</br>
|
||||
* See {@link SableCommandHelper#getResultComponentForSublevelCollection} for more info about the description
|
||||
* @param translationKey The translation key to use
|
||||
* @param context The command context to send the message to
|
||||
* @param subLevels The collection of sub-levels to describe
|
||||
* @param subLevelsDescriptionIndex The index of the sub-levels description in the args array
|
||||
* @param additionalArguments The additional arguments to pass to the translation key
|
||||
*/
|
||||
public static void sendSuccessDescribingSubLevelsAtIndex(final String translationKey, final CommandContext<CommandSourceStack> context, final Collection<ServerSubLevel> subLevels,
|
||||
final int subLevelsDescriptionIndex, final Object... additionalArguments) {
|
||||
context.getSource().sendSuccess(
|
||||
() -> getResultComponentForSublevelCollection(translationKey, subLevels, subLevelsDescriptionIndex, additionalArguments),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
//Requires with a command exception
|
||||
|
||||
public static ServerSubLevelContainer requireSubLevelContainer(final CommandContext<CommandSourceStack> context) throws CommandSyntaxException {
|
||||
return requireSubLevelContainer(context.getSource());
|
||||
}
|
||||
|
||||
public static ServerSubLevelContainer requireSubLevelContainer(final CommandSourceStack source) throws CommandSyntaxException {
|
||||
final ServerLevel level = source.getLevel();
|
||||
return requireNotNull(SubLevelContainer.getContainer(level), MISSING_SUBLEVEL_CONTAINER);
|
||||
}
|
||||
|
||||
public static SubLevelPhysicsSystem requireSubLevelPhysicsSystem(final ServerSubLevelContainer subLevelContainer) throws CommandSyntaxException {
|
||||
return requireNotNull(subLevelContainer.physicsSystem(), MISSING_PHYSICS_SYSTEM);
|
||||
}
|
||||
|
||||
//Overloads from context only
|
||||
|
||||
public static SubLevelPhysicsSystem requireSubLevelPhysicsSystem(final CommandContext<CommandSourceStack> context) throws CommandSyntaxException {
|
||||
return requireSubLevelPhysicsSystem(
|
||||
requireSubLevelContainer(context)
|
||||
);
|
||||
}
|
||||
|
||||
public static PhysicsPipeline requireSubLevelPhysicsPipeline(final CommandContext<CommandSourceStack> context) throws CommandSyntaxException {
|
||||
return requireSubLevelPhysicsSystem(context).getPipeline();
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
public static <T> T requireNotNull(final T value, final SimpleCommandExceptionType message) throws CommandSyntaxException {
|
||||
if (value == null) {
|
||||
throw message.create();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
package dev.ryanhcode.sable.api.command;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.mojang.brigadier.Message;
|
||||
import com.mojang.brigadier.StringReader;
|
||||
import com.mojang.brigadier.arguments.ArgumentType;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
import dev.ryanhcode.sable.command.argument.SubLevelSelector;
|
||||
import dev.ryanhcode.sable.command.argument.SubLevelSelectorModifierType;
|
||||
import dev.ryanhcode.sable.command.argument.SubLevelSelectorType;
|
||||
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
|
||||
import it.unimi.dsi.fastutil.Pair;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectList;
|
||||
import net.minecraft.commands.CommandBuildContext;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.synchronization.ArgumentTypeInfo;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class SubLevelArgumentType implements ArgumentType<SubLevelSelector> {
|
||||
|
||||
public static final Function<SuggestionsBuilder, CompletableFuture<Suggestions>> NO_SUGGESTIONS = SuggestionsBuilder::buildFuture;
|
||||
private static final SimpleCommandExceptionType ERROR_SINGLE_SUB_LEVEL_REQUIRED =
|
||||
new SimpleCommandExceptionType(Component.translatable("argument.sable.single_sub_level_required"));
|
||||
private static final SimpleCommandExceptionType ERROR_INVALID_SUBLEVEL =
|
||||
new SimpleCommandExceptionType(Component.translatable("argument.sable.sub_level.invalid"));
|
||||
private static final SimpleCommandExceptionType UNEXPECTED_END_OF_INPUT =
|
||||
new SimpleCommandExceptionType(Component.translatable("argument.sable.unexpected_end_of_input"));
|
||||
private static final String STATIC_WORLD = "static_world";
|
||||
private static final Collection<String> EXAMPLES = Arrays.stream(SubLevelSelectorType.values())
|
||||
.map(type -> "@" + type.getChar()).toList();
|
||||
private static Function<SuggestionsBuilder, CompletableFuture<Suggestions>> suggestions = NO_SUGGESTIONS;
|
||||
private final boolean allowStaticLevel;
|
||||
private final boolean allowMultiple;
|
||||
|
||||
public SubLevelArgumentType(final boolean allowStaticLevel, final boolean allowMultiple) {
|
||||
this.allowStaticLevel = allowStaticLevel;
|
||||
this.allowMultiple = allowMultiple;
|
||||
}
|
||||
|
||||
public static Collection<ServerSubLevel> getSubLevels(final CommandContext<CommandSourceStack> ctx, final String name) throws CommandSyntaxException {
|
||||
return ctx.getArgument(name, SubLevelSelector.class).getSubLevels(ctx.getSource());
|
||||
}
|
||||
|
||||
public static ServerSubLevel getSingleSubLevel(final CommandContext<CommandSourceStack> ctx, final String name) throws CommandSyntaxException {
|
||||
final Collection<ServerSubLevel> subLevels = ctx.getArgument(name, SubLevelSelector.class).getSubLevels(ctx.getSource());
|
||||
if (subLevels.size() > 1) {
|
||||
throw ERROR_SINGLE_SUB_LEVEL_REQUIRED.create();
|
||||
}
|
||||
|
||||
if (subLevels.isEmpty()) {
|
||||
throw SableCommandHelper.ERROR_NO_SUB_LEVELS_FOUND.create();
|
||||
}
|
||||
|
||||
return subLevels.stream().findFirst().orElseThrow();
|
||||
}
|
||||
|
||||
public static SubLevelArgumentType singleSubLevel() {
|
||||
return new SubLevelArgumentType(false, false);
|
||||
}
|
||||
|
||||
public static SubLevelArgumentType subLevels() {
|
||||
return new SubLevelArgumentType(false, true);
|
||||
}
|
||||
|
||||
public static SubLevelArgumentType subLevelsOrLevel() {
|
||||
return new SubLevelArgumentType(true, true);
|
||||
}
|
||||
|
||||
private static @NotNull List<Pair<SubLevelSelectorModifierType, SubLevelSelectorModifierType.Modifier>> parseSelectorArguments(final StringReader reader) throws CommandSyntaxException {
|
||||
final List<Pair<SubLevelSelectorModifierType, SubLevelSelectorModifierType.Modifier>> modifiers = new ObjectArrayList<>();
|
||||
setSuggestions(reader, "[");
|
||||
|
||||
final List<Pair<String, @Nullable Message>> permittedPreEntryToken = new ArrayList<>(SubLevelSelectorModifierType.getAllNamesWithTooltip()
|
||||
.stream().map(s -> Pair.of(s.first() + "=", s.second())).toList());
|
||||
permittedPreEntryToken.add(Pair.of("]", null));
|
||||
boolean isFirstEntry = true;
|
||||
|
||||
if (reader.canRead() && reader.peek() == '[') {
|
||||
reader.skip();
|
||||
|
||||
setSuggestionsWithTooltip(reader, permittedPreEntryToken);
|
||||
while (reader.canRead() && reader.peek() != ']') {
|
||||
if (reader.peek() == ',') {
|
||||
reader.skip();
|
||||
}
|
||||
setSuggestionsWithTooltip(reader, permittedPreEntryToken);
|
||||
|
||||
final String propertyName = readUntilEndOrCharacter(reader, '=');
|
||||
|
||||
if (!reader.canRead() || reader.peek() != '=') {
|
||||
throw UNEXPECTED_END_OF_INPUT.createWithContext(reader);
|
||||
}
|
||||
reader.skip();
|
||||
|
||||
final SubLevelSelectorModifierType modifierType = SubLevelSelectorModifierType.getModifier(propertyName, reader);
|
||||
if (modifierType == null) {
|
||||
throw UNEXPECTED_END_OF_INPUT.createWithContext(reader);
|
||||
}
|
||||
final SubLevelSelectorModifierType.Modifier modifier = modifierType.getParser().parse(reader);
|
||||
modifiers.add(Pair.of(modifierType, modifier));
|
||||
|
||||
setSuggestionsWithTooltip(reader, permittedPreEntryToken);
|
||||
if (isFirstEntry) {
|
||||
permittedPreEntryToken.add(Pair.of(",", null));
|
||||
isFirstEntry = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (reader.canRead() && reader.peek() == ']') {
|
||||
reader.skip();
|
||||
} else {
|
||||
throw UNEXPECTED_END_OF_INPUT.createWithContext(reader);
|
||||
}
|
||||
}
|
||||
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
public static void setSuggestions(final StringReader reader, final String... suggested) {
|
||||
setSuggestions(reader, Arrays.asList(suggested));
|
||||
}
|
||||
|
||||
public static void setSuggestions(final StringReader reader, final List<String> suggested) {
|
||||
setSuggestionsWithTooltip(reader, suggested.stream().map(s -> Pair.of(s, (Message) null)).toList());
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static void setSuggestionsWithTooltip(final StringReader reader, final Pair<String, @Nullable Message>... suggested) {
|
||||
setSuggestionsWithTooltip(reader, Arrays.asList(suggested));
|
||||
}
|
||||
|
||||
public static void setSuggestionsWithTooltip(final StringReader reader, final List<Pair<String, @Nullable Message>> suggested) {
|
||||
final int cursor = reader.getCursor();
|
||||
suggestions = builder -> {
|
||||
final SuggestionsBuilder nextSuggestion = builder.createOffset(cursor);
|
||||
for (final Pair<String, @Nullable Message> suggestion : suggested) {
|
||||
if (!suggestion.first().startsWith(builder.getInput().substring(cursor))) {
|
||||
continue;
|
||||
}
|
||||
if (suggestion.second() != null) {
|
||||
nextSuggestion.suggest(suggestion.first(), suggestion.second());
|
||||
} else {
|
||||
nextSuggestion.suggest(suggestion.first());
|
||||
}
|
||||
}
|
||||
return nextSuggestion.buildFuture();
|
||||
};
|
||||
}
|
||||
|
||||
public static String readUntilEndOrCharacter(final StringReader reader, final char character) throws CommandSyntaxException {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
while (reader.canRead() && reader.peek() != character) {
|
||||
builder.append(reader.read());
|
||||
}
|
||||
if (builder.isEmpty()) {
|
||||
throw UNEXPECTED_END_OF_INPUT.create();
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SubLevelSelector parse(final StringReader reader) throws CommandSyntaxException {
|
||||
final ObjectList<Pair<String, @Nullable Message>> allowedSelectors = new ObjectArrayList<>();
|
||||
if (this.allowStaticLevel) {
|
||||
allowedSelectors.add(Pair.of(STATIC_WORLD, Component.translatable("argument.sable.body.static_world")));
|
||||
}
|
||||
for (final SubLevelSelectorType selector : SubLevelSelectorType.values()) {
|
||||
allowedSelectors.add(Pair.of("@" + selector.getChar(), selector.getTooltip()));
|
||||
}
|
||||
setSuggestionsWithTooltip(reader, allowedSelectors);
|
||||
|
||||
if (this.allowStaticLevel && reader.canRead(STATIC_WORLD.length()) && reader.peek() == STATIC_WORLD.charAt(0)) {
|
||||
final String staticWorld = reader.readString();
|
||||
|
||||
if (!staticWorld.equals(STATIC_WORLD)) {
|
||||
throw ERROR_INVALID_SUBLEVEL.create();
|
||||
}
|
||||
|
||||
return new SubLevelSelector(null, new ObjectArrayList<>());
|
||||
}
|
||||
|
||||
if (!reader.canRead()) {
|
||||
throw ERROR_INVALID_SUBLEVEL.create();
|
||||
}
|
||||
|
||||
final char firstChar = reader.read();
|
||||
|
||||
if (!reader.canRead() || firstChar != '@') {
|
||||
throw ERROR_INVALID_SUBLEVEL.create();
|
||||
}
|
||||
|
||||
if (!reader.canRead()) {
|
||||
throw ERROR_INVALID_SUBLEVEL.create();
|
||||
}
|
||||
|
||||
final SubLevelSelectorType selectorType = SubLevelSelectorType.of(reader.read());
|
||||
if (selectorType == null) {
|
||||
throw ERROR_INVALID_SUBLEVEL.create();
|
||||
}
|
||||
|
||||
int maximumResults = Integer.MAX_VALUE;
|
||||
|
||||
if (selectorType.single()) {
|
||||
maximumResults = 1;
|
||||
}
|
||||
|
||||
final List<Pair<SubLevelSelectorModifierType, SubLevelSelectorModifierType.Modifier>> modifiers = parseSelectorArguments(reader);
|
||||
|
||||
for (final Pair<SubLevelSelectorModifierType, SubLevelSelectorModifierType.Modifier> modifierPair : modifiers) {
|
||||
maximumResults = Math.min(maximumResults, modifierPair.second().getMaxResults());
|
||||
}
|
||||
|
||||
// If we don't allow multiple sub-levels and we have more than one, throw a fit
|
||||
if (maximumResults > 1 && !this.allowMultiple) {
|
||||
throw ERROR_SINGLE_SUB_LEVEL_REQUIRED.create();
|
||||
}
|
||||
|
||||
return new SubLevelSelector(selectorType, modifiers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S> CompletableFuture<Suggestions> listSuggestions(final CommandContext<S> pContext, final SuggestionsBuilder builder) {
|
||||
final StringReader stringreader = new StringReader(builder.getInput());
|
||||
stringreader.setCursor(builder.getStart());
|
||||
suggestions = NO_SUGGESTIONS;
|
||||
try {
|
||||
this.parse(stringreader);
|
||||
} catch (final CommandSyntaxException ignored) {
|
||||
}
|
||||
return suggestions.apply(builder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getExamples() {
|
||||
return EXAMPLES;
|
||||
}
|
||||
|
||||
public static class Info implements ArgumentTypeInfo<SubLevelArgumentType, SubLevelArgumentType.Info.Template> {
|
||||
private static final byte FLAG_MULTIPLE = 1;
|
||||
private static final byte FLAG_STATIC_ALLOWED = 2;
|
||||
|
||||
public void serializeToNetwork(final SubLevelArgumentType.Info.Template template, final FriendlyByteBuf byteBuf) {
|
||||
int serialized = 0;
|
||||
if (template.allowMultiple) {
|
||||
serialized |= FLAG_MULTIPLE;
|
||||
}
|
||||
|
||||
if (template.allowStaticLevel) {
|
||||
serialized |= FLAG_STATIC_ALLOWED;
|
||||
}
|
||||
|
||||
byteBuf.writeByte(serialized);
|
||||
}
|
||||
|
||||
public SubLevelArgumentType.Info.Template deserializeFromNetwork(final FriendlyByteBuf arg) {
|
||||
final byte serialized = arg.readByte();
|
||||
return new SubLevelArgumentType.Info.Template((serialized & FLAG_MULTIPLE) != 0, (serialized & FLAG_STATIC_ALLOWED) != 0);
|
||||
}
|
||||
|
||||
public void serializeToJson(final SubLevelArgumentType.Info.Template arg, final JsonObject jsonObject) {
|
||||
jsonObject.addProperty("amount", arg.allowMultiple ? "single" : "multiple");
|
||||
jsonObject.addProperty("type", arg.allowStaticLevel ? "players" : "entities");
|
||||
}
|
||||
|
||||
public SubLevelArgumentType.Info.Template unpack(final SubLevelArgumentType arg) {
|
||||
return new Template(arg.allowMultiple, arg.allowStaticLevel);
|
||||
}
|
||||
|
||||
public final class Template implements ArgumentTypeInfo.Template<SubLevelArgumentType> {
|
||||
final boolean allowMultiple;
|
||||
final boolean allowStaticLevel;
|
||||
|
||||
Template(final boolean allowMultiple, final boolean allowStaticLevel) {
|
||||
this.allowMultiple = allowMultiple;
|
||||
this.allowStaticLevel = allowStaticLevel;
|
||||
}
|
||||
|
||||
public SubLevelArgumentType instantiate(final CommandBuildContext commandBuildContext) {
|
||||
return new SubLevelArgumentType(this.allowStaticLevel, this.allowMultiple);
|
||||
}
|
||||
|
||||
public ArgumentTypeInfo<SubLevelArgumentType, ?> type() {
|
||||
return SubLevelArgumentType.Info.this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package dev.ryanhcode.sable.api.entity;
|
||||
|
||||
import dev.ryanhcode.sable.Sable;
|
||||
import dev.ryanhcode.sable.companion.math.JOMLConversion;
|
||||
import dev.ryanhcode.sable.index.SableTags;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import net.minecraft.commands.arguments.EntityAnchorArgument;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.item.FallingBlockEntity;
|
||||
import net.minecraft.world.entity.projectile.AbstractArrow;
|
||||
import net.minecraft.world.entity.projectile.AbstractHurtingProjectile;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Quaterniondc;
|
||||
import org.joml.Vector3d;
|
||||
|
||||
/**
|
||||
* Utility for operations regarding entities and sub-levels
|
||||
*/
|
||||
public class EntitySubLevelUtil {
|
||||
|
||||
/**
|
||||
* Sets the old pos of an entity for no apparent movement, taking their tracking sub-level
|
||||
* into account.
|
||||
*
|
||||
* @param entity the entity to set the old pos of
|
||||
*/
|
||||
public static void setOldPosNoMovement(final Entity entity) {
|
||||
final SubLevel trackingSubLevel = Sable.HELPER.getTrackingSubLevel(entity);
|
||||
|
||||
if (trackingSubLevel != null) {
|
||||
final Vec3 entityPos = entity.position();
|
||||
final Vec3 oldPos = trackingSubLevel.lastPose().transformPosition(trackingSubLevel.logicalPose().transformPositionInverse(entityPos));
|
||||
|
||||
entity.xOld = oldPos.x;
|
||||
entity.xo = oldPos.x;
|
||||
entity.yOld = oldPos.y;
|
||||
entity.yo = oldPos.y;
|
||||
entity.zOld = oldPos.z;
|
||||
entity.zo = oldPos.z;
|
||||
} else {
|
||||
entity.xOld = entity.getX();
|
||||
entity.xo = entity.getX();
|
||||
entity.yOld = entity.getY();
|
||||
entity.yo = entity.getY();
|
||||
entity.zOld = entity.getZ();
|
||||
entity.zo = entity.getZ();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kicks an entity out of a sub-level, including velocity and position.
|
||||
*
|
||||
* @param subLevel The sub-level to kick the entity out of
|
||||
* @param entity The entity to kick
|
||||
*/
|
||||
public static void kickEntity(final SubLevel subLevel, final Entity entity) {
|
||||
final Vector3d subLevelGainedVelo = new Vector3d();
|
||||
if (entity instanceof final AbstractHurtingProjectile ahp && ahp.accelerationPower == 0) {
|
||||
Sable.HELPER.getVelocity(entity.level(), JOMLConversion.toJOML(entity.position()), subLevelGainedVelo);
|
||||
}
|
||||
|
||||
// convert from m/s to m/t
|
||||
subLevelGainedVelo.mul(1.0 / 20.0);
|
||||
|
||||
final Vec3 pos = entity.position();
|
||||
Vec3 anchor = Vec3.ZERO;
|
||||
|
||||
if (entity instanceof FallingBlockEntity) {
|
||||
anchor = new Vec3(0.0, entity.getBbHeight() / 2.0, 0.0);
|
||||
}
|
||||
|
||||
entity.moveTo(subLevel.logicalPose().transformPosition(pos.add(anchor)).subtract(anchor));
|
||||
entity.setDeltaMovement(subLevel.logicalPose().transformNormal(entity.getDeltaMovement()).add(subLevelGainedVelo.x, subLevelGainedVelo.y, subLevelGainedVelo.z));
|
||||
entity.lookAt(EntityAnchorArgument.Anchor.FEET, subLevel.logicalPose().transformNormal(entity.getLookAngle()).add(entity.position()));
|
||||
|
||||
// Arrows use an incorrect Y and X rotation
|
||||
if (entity instanceof AbstractArrow) {
|
||||
final Vec3 deltaMovement = entity.getDeltaMovement();
|
||||
final double horizontal = deltaMovement.horizontalDistance();
|
||||
entity.setYRot((float) (Mth.atan2(deltaMovement.x, deltaMovement.z) * 180.0 / (float) Math.PI));
|
||||
entity.setXRot((float) (Mth.atan2(deltaMovement.y, horizontal) * 180.0 / (float) Math.PI));
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean shouldKick(final Entity entity) {
|
||||
return !entity.getType().is(SableTags.RETAIN_IN_SUB_LEVEL);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Quaterniondc getCustomEntityOrientation(final Entity entity, final float partialTicks) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static boolean hasCustomEntityOrientation(final Entity entity) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package dev.ryanhcode.sable.api.event;
|
||||
|
||||
import dev.ryanhcode.sable.sublevel.system.SubLevelPhysicsSystem;
|
||||
|
||||
/**
|
||||
* Fired when Sable's {@link SubLevelPhysicsSystem} is complete with a physics tick.
|
||||
* </br>
|
||||
* Note that multiple physics ticks are completed per game tick, based on the amount of configured sub-steps.
|
||||
* Logic that needs to influence the physics world should occur on the physics tick, and not the game tick
|
||||
* due to this reason.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface SablePostPhysicsTickEvent {
|
||||
|
||||
/**
|
||||
* Fired when Sable's {@link dev.ryanhcode.sable.sublevel.system.SubLevelPhysicsSystem} is complete with a physics tick.
|
||||
*
|
||||
* @param physicsSystem the physics system running the physics tick
|
||||
* @param timeStep the time step of this physics tick [s]
|
||||
*/
|
||||
void postPhysicsTick(SubLevelPhysicsSystem physicsSystem, double timeStep);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package dev.ryanhcode.sable.api.event;
|
||||
|
||||
import dev.ryanhcode.sable.sublevel.system.SubLevelPhysicsSystem;
|
||||
|
||||
/**
|
||||
* Fired when Sable's {@link dev.ryanhcode.sable.sublevel.system.SubLevelPhysicsSystem} is ticking physics.
|
||||
* </br>
|
||||
* Note that multiple physics ticks are completed per game tick, based on the amount of configured sub-steps.
|
||||
* Logic that needs to influence the physics world should occur on the physics tick, and not the game tick
|
||||
* due to this reason.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface SablePrePhysicsTickEvent {
|
||||
|
||||
/**
|
||||
* Fired when Sable's {@link dev.ryanhcode.sable.sublevel.system.SubLevelPhysicsSystem} is ticking physics.
|
||||
*
|
||||
* @param physicsSystem the physics system running the physics tick
|
||||
* @param timeStep the time step of this physics tick [s]
|
||||
*/
|
||||
void prePhysicsTick(SubLevelPhysicsSystem physicsSystem, double timeStep);
|
||||
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package dev.ryanhcode.sable.api.event;
|
||||
|
||||
import dev.ryanhcode.sable.api.sublevel.SubLevelContainer;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/**
|
||||
* Fired when Sable has finished initialization for a level and its sub-level container is ready to use.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface SableSubLevelContainerReadyEvent {
|
||||
|
||||
/**
|
||||
* Called when a sub-level container is ready to use.
|
||||
*
|
||||
* @param level The level instance
|
||||
* @param container The sub-level container that is ready
|
||||
*/
|
||||
void onSubLevelContainerReady(Level level, SubLevelContainer container);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package dev.ryanhcode.sable.api.math;
|
||||
|
||||
|
||||
import dev.ryanhcode.sable.companion.math.BoundingBox3d;
|
||||
import dev.ryanhcode.sable.companion.math.Pose3d;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.shapes.Shapes;
|
||||
import net.minecraft.world.phys.shapes.VoxelShape;
|
||||
import org.joml.*;
|
||||
|
||||
/**
|
||||
* Class containing mutability optimization vectors for OBB SAT calculations
|
||||
*/
|
||||
public class LevelReusedVectors {
|
||||
public final VoxelShape SCAFFOLDING_TOP = Shapes.create(new AABB(0, 15 / 16f, 0, 1f, 1f, 1f));
|
||||
|
||||
public final Vector3d tempVert6 = new Vector3d();
|
||||
public final Vector3d tempVert5 = new Vector3d();
|
||||
public final Vector3d tempVert4 = new Vector3d();
|
||||
public final Vector3d tempVert3 = new Vector3d();
|
||||
public final Vector3d tempVert2 = new Vector3d();
|
||||
public final Vector3d tempVert1 = new Vector3d();
|
||||
|
||||
public final Vector3dc zero = new Vector3d();
|
||||
public final Vector2d proj1 = new Vector2d();
|
||||
public final Vector2d proj2 = new Vector2d();
|
||||
public final Vector3d oppo = new Vector3d();
|
||||
public final Vector3d obbARight = new Vector3d();
|
||||
public final Vector3d obbAForward = new Vector3d();
|
||||
public final Vector3d obbAUp = new Vector3d();
|
||||
public final Vector3d obbBRight = new Vector3d();
|
||||
public final Vector3d obbBForward = new Vector3d();
|
||||
public final Vector3d obbBUp = new Vector3d();
|
||||
public final Vector3d checker = new Vector3d();
|
||||
public final BlockPos.MutableBlockPos minPos = new BlockPos.MutableBlockPos();
|
||||
public final BlockPos.MutableBlockPos maxPos = new BlockPos.MutableBlockPos();
|
||||
public final BlockPos.MutableBlockPos maxBlockPos = new BlockPos.MutableBlockPos();
|
||||
public final BlockPos.MutableBlockPos offsetPos = new BlockPos.MutableBlockPos();
|
||||
public final BoundingBox3d fullContextBounds = new BoundingBox3d();
|
||||
public final BoundingBox3d rotatedContextBounds = new BoundingBox3d();
|
||||
public final BoundingBox3d considerationBounds = new BoundingBox3d();
|
||||
public final BoundingBox3d localBounds = new BoundingBox3d();
|
||||
public final BoundingBox3d localBounds2 = new BoundingBox3d();
|
||||
public final Vector3d collisionMotion = new Vector3d();
|
||||
public final Vector3d velocityMotion = new Vector3d();
|
||||
public final Vector3d entityBoundsCenter = new Vector3d();
|
||||
public final Vector3d stepHeightEntityBoundsCenter = new Vector3d();
|
||||
public final Vector3d lastStepTestMTV = new Vector3d();
|
||||
public final Vector3d entityPosition = new Vector3d();
|
||||
public final Vector3d posMinusCenter = new Vector3d();
|
||||
public final Vector3d trackingPosition = new Vector3d();
|
||||
public final Pose3d lastPose = new Pose3d();
|
||||
public final Pose3d lastSubLevelPose = new Pose3d();
|
||||
public final Pose3d subLevelPose = new Pose3d();
|
||||
public final Matrix4d bakedMatrix = new Matrix4d();
|
||||
public final Vector3d mtv = new Vector3d();
|
||||
public final Vector3d normalizedMtv = new Vector3d();
|
||||
public final Vector3d localMtv = new Vector3d();
|
||||
public final Vector3d existingDeltaMovement = new Vector3d();
|
||||
public final Vector3d maxMTV = new Vector3d();
|
||||
public final BoundingBox3d maxAABB = new BoundingBox3d();
|
||||
public final Vector3d center = new Vector3d();
|
||||
public final BoundingBox3d offsetAABB = new BoundingBox3d();
|
||||
public final BoundingBox3d compressedMinAABB = new BoundingBox3d();
|
||||
public final BoundingBox3d compressedOffsetAABB = new BoundingBox3d();
|
||||
public final BoundingBox3d intersection = new BoundingBox3d();
|
||||
|
||||
public final Quaterniond entityBoxOrientation = new Quaterniond();
|
||||
public final Quaterniond entityCustomOrientation = new Quaterniond();
|
||||
public final Vector3d tempEyePosition = new Vector3d();
|
||||
public final Vector3d anchorRelativePosition = new Vector3d();
|
||||
|
||||
public final Vector3d[] a = {
|
||||
new Vector3d(),
|
||||
new Vector3d(),
|
||||
new Vector3d(),
|
||||
new Vector3d(),
|
||||
|
||||
new Vector3d(),
|
||||
new Vector3d(),
|
||||
new Vector3d(),
|
||||
new Vector3d()
|
||||
};
|
||||
public final Vector3d[] b = {
|
||||
new Vector3d(),
|
||||
new Vector3d(),
|
||||
new Vector3d(),
|
||||
new Vector3d(),
|
||||
|
||||
new Vector3d(),
|
||||
new Vector3d(),
|
||||
new Vector3d(),
|
||||
new Vector3d()
|
||||
};
|
||||
public final Vector3d[] checks = {
|
||||
new Vector3d(),
|
||||
new Vector3d(),
|
||||
new Vector3d(),
|
||||
new Vector3d(),
|
||||
|
||||
new Vector3d(),
|
||||
new Vector3d(),
|
||||
new Vector3d(),
|
||||
new Vector3d(),
|
||||
|
||||
new Vector3d(),
|
||||
new Vector3d(),
|
||||
new Vector3d(),
|
||||
new Vector3d(),
|
||||
|
||||
new Vector3d(),
|
||||
new Vector3d(),
|
||||
new Vector3d()
|
||||
};
|
||||
protected final Vector3d tempmin = new Vector3d();
|
||||
protected final Vector3d tempmax = new Vector3d();
|
||||
public final Vector3d entityUpDirection = new Vector3d();
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
package dev.ryanhcode.sable.api.math;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.joml.*;
|
||||
|
||||
import java.lang.Math;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Represents an oriented bounding box with extents, orientation, and positioning.
|
||||
* The box is expected to be centered on the position.
|
||||
*/
|
||||
public class OrientedBoundingBox3d {
|
||||
public static final Vector3dc RIGHT = new Vector3d(1, 0, 0);
|
||||
public static final Vector3dc UP = new Vector3d(0, 1, 0);
|
||||
public static final Vector3dc FORWARD = new Vector3d(0, 0, 1);
|
||||
|
||||
private final Vector3d position = new Vector3d();
|
||||
private final Vector3d dimensions = new Vector3d();
|
||||
private final Quaterniond orientation = new Quaterniond();
|
||||
private final LevelReusedVectors sink;
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new oriented bounding box.
|
||||
*/
|
||||
public OrientedBoundingBox3d(@NotNull final LevelReusedVectors sink) {
|
||||
this.sink = sink;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new oriented bounding box.
|
||||
*
|
||||
* @param position The center in global space
|
||||
* @param dimensions The total dimensions
|
||||
* @param orientation The unit quaternion rotation
|
||||
*/
|
||||
public OrientedBoundingBox3d(@NotNull final Vector3dc position,
|
||||
@NotNull final Vector3dc dimensions,
|
||||
@NotNull final Quaterniondc orientation,
|
||||
@NotNull final LevelReusedVectors sink) {
|
||||
this.position.set(position);
|
||||
this.dimensions.set(dimensions);
|
||||
this.orientation.set(orientation);
|
||||
this.sink = sink;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new oriented bounding box.
|
||||
*
|
||||
* @param x The center X in global space
|
||||
* @param y The center Y in global space
|
||||
* @param z The center Z in global space
|
||||
* @param sizeX The total dimensions in the x-axis
|
||||
* @param sizeY The total dimensions in the y-axis
|
||||
* @param sizeZ The total dimensions in the z-axis
|
||||
* @param orientation The unit quaternion rotation
|
||||
*/
|
||||
public OrientedBoundingBox3d(final double x,
|
||||
final double y,
|
||||
final double z,
|
||||
final double sizeX,
|
||||
final double sizeY,
|
||||
final double sizeZ,
|
||||
@NotNull final Quaterniondc orientation,
|
||||
@NotNull final LevelReusedVectors sink) {
|
||||
this.position.set(x, y, z);
|
||||
this.dimensions.set(sizeX, sizeY, sizeZ);
|
||||
this.orientation.set(orientation);
|
||||
this.sink = sink;
|
||||
}
|
||||
|
||||
public void set(final Vector3dc position, final Vector3dc dimensions, final Quaterniondc orientation) {
|
||||
this.position.set(position);
|
||||
this.dimensions.set(dimensions);
|
||||
this.orientation.set(orientation);
|
||||
}
|
||||
|
||||
public OrientedBoundingBox3d setPosition(final Vector3dc position) {
|
||||
this.position.set(position);
|
||||
return this;
|
||||
}
|
||||
|
||||
public OrientedBoundingBox3d setDimensions(final Vector3dc dimensions) {
|
||||
this.dimensions.set(dimensions);
|
||||
return this;
|
||||
}
|
||||
|
||||
public OrientedBoundingBox3d setOrientation(final Quaterniondc orientation) {
|
||||
this.orientation.set(orientation);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Quaterniond getOrientation() {
|
||||
return this.orientation;
|
||||
}
|
||||
|
||||
public Vector3d getPosition() {
|
||||
return this.position;
|
||||
}
|
||||
|
||||
public Vector3d getDimensions() {
|
||||
return this.dimensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes all global vertices of this box.
|
||||
*/
|
||||
public Vector3d @NotNull [] vertices(final Vector3d[] result) {
|
||||
this.dimensions.mul(0.5, this.sink.tempmin);
|
||||
this.dimensions.mul(-0.5, this.sink.tempmax);
|
||||
|
||||
this.orientation.transform(this.sink.tempmin, result[0]).add(this.position);
|
||||
this.orientation.transform(this.sink.tempVert1.set(this.sink.tempmax.x, this.sink.tempmin.y, this.sink.tempmin.z), result[1]).add(this.position);
|
||||
this.orientation.transform(this.sink.tempVert4.set(this.sink.tempmin.x, this.sink.tempmin.y, this.sink.tempmax.z), result[4]).add(this.position);
|
||||
this.orientation.transform(this.sink.tempVert5.set(this.sink.tempmax.x, this.sink.tempmin.y, this.sink.tempmax.z), result[5]).add(this.position);
|
||||
this.orientation.transform(this.sink.tempVert3.set(this.sink.tempmax.x, this.sink.tempmax.y, this.sink.tempmin.z), result[3]).add(this.position);
|
||||
this.orientation.transform(this.sink.tempVert2.set(this.sink.tempmin.x, this.sink.tempmax.y, this.sink.tempmin.z), result[2]).add(this.position);
|
||||
this.orientation.transform(this.sink.tempVert6.set(this.sink.tempmin.x, this.sink.tempmax.y, this.sink.tempmax.z), result[6]).add(this.position);
|
||||
this.orientation.transform(this.sink.tempmax, result[7]).add(this.position);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Rotates a vector from local space in this OBB to global space.
|
||||
*/
|
||||
public Vector3d rotate(@NotNull final Vector3d vec) {
|
||||
return this.orientation.transform(vec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if two intervals intersect.
|
||||
*/
|
||||
private static boolean doesOverlap(@NotNull final Vector2d a, @NotNull final Vector2d b) {
|
||||
return a.x <= b.y && a.y >= b.x;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The overlap of the two intervals.
|
||||
*/
|
||||
public static double getOverlap(@NotNull final Vector2d a, @NotNull final Vector2d b) {
|
||||
if (!OrientedBoundingBox3d.doesOverlap(a, b)) {
|
||||
return 0.f;
|
||||
}
|
||||
|
||||
return Math.min(a.y, b.y) - Math.max(a.x, b.x);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the MTV, or Minimum Translation Vector between the vertices of two OBBs.
|
||||
*/
|
||||
public static @NotNull Vector3d sat(@NotNull final OrientedBoundingBox3d obbA, @NotNull final OrientedBoundingBox3d obbB) {
|
||||
return sat(obbA, obbB, new Vector3d());
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the MTV, or Minimum Translation Vector between the vertices of two OBBs.
|
||||
*/
|
||||
public static @NotNull Vector3d sat(@NotNull final OrientedBoundingBox3d obbA, @NotNull final OrientedBoundingBox3d obbB, @NotNull final Vector3d dest) {
|
||||
Objects.requireNonNull(obbA, "obbA");
|
||||
Objects.requireNonNull(obbB, "obbB");
|
||||
Objects.requireNonNull(dest, "dest");
|
||||
|
||||
final LevelReusedVectors context = obbA.sink;
|
||||
|
||||
final Vector3d[] verticesA = obbA.vertices(context.a);
|
||||
final Vector3d[] verticesB = obbB.vertices(context.b);
|
||||
|
||||
final Vector3d checker = obbA.position.sub(obbB.position, obbA.sink.checker).normalize();
|
||||
|
||||
final Vector3d aRight = obbA.rotate(context.obbARight.set(OrientedBoundingBox3d.RIGHT));
|
||||
final Vector3d aUp = obbA.rotate(context.obbAUp.set(OrientedBoundingBox3d.UP));
|
||||
final Vector3d aForward = obbA.rotate(context.obbAForward.set(OrientedBoundingBox3d.FORWARD));
|
||||
|
||||
final Vector3d bRight = obbB.rotate(context.obbBRight.set(OrientedBoundingBox3d.RIGHT));
|
||||
final Vector3d bUp = obbB.rotate(context.obbBUp.set(OrientedBoundingBox3d.UP));
|
||||
final Vector3d bForward = obbB.rotate(context.obbBForward.set(OrientedBoundingBox3d.FORWARD));
|
||||
|
||||
final Vector3d mtv = dest.set(Double.MAX_VALUE);
|
||||
|
||||
OrientedBoundingBox3d.genChecks(aRight, aUp, aForward, bRight, bUp, bForward, context.checks);
|
||||
|
||||
double minOverlap = Double.MAX_VALUE;
|
||||
|
||||
for (final Vector3d check : context.checks) {
|
||||
if (check.lengthSquared() <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
check.normalize();
|
||||
|
||||
OrientedBoundingBox3d.checkSeparation(verticesA, check, context.proj1);
|
||||
OrientedBoundingBox3d.checkSeparation(verticesB, check, context.proj2);
|
||||
|
||||
if (check.dot(checker) > 0) {
|
||||
check.mul(-1.0);
|
||||
}
|
||||
|
||||
final double overlap = OrientedBoundingBox3d.getOverlap(context.proj1, context.proj2);
|
||||
|
||||
if (overlap == 0.f) { // shapes are not overlapping
|
||||
return dest.zero();
|
||||
} else {
|
||||
if (overlap < minOverlap) {
|
||||
minOverlap = overlap;
|
||||
mtv.set(check.mul(minOverlap));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final boolean facingOpposite = obbA.position.sub(obbB.position, context.oppo).dot(mtv) < 0;
|
||||
|
||||
if (facingOpposite) {
|
||||
mtv.mul(-1);
|
||||
}
|
||||
|
||||
return mtv;
|
||||
}
|
||||
|
||||
public static Vector3d[] genChecks(final Vector3d aRight, final Vector3d aUp, final Vector3d aForward, final Vector3d bRight, final Vector3d bUp, final Vector3d bForward, final Vector3d[] checks) {
|
||||
checks[0].set(aRight);
|
||||
checks[1].set(aUp);
|
||||
checks[2].set(aForward);
|
||||
checks[3].set(bRight);
|
||||
checks[4].set(bUp);
|
||||
checks[5].set(bForward);
|
||||
aRight.cross(bRight, checks[6]);
|
||||
aRight.cross(bUp, checks[7]);
|
||||
aRight.cross(bForward, checks[8]);
|
||||
aUp.cross(bRight, checks[9]);
|
||||
aUp.cross(bUp, checks[10]);
|
||||
aUp.cross(bForward, checks[11]);
|
||||
aForward.cross(bRight, checks[12]);
|
||||
aForward.cross(bUp, checks[13]);
|
||||
aForward.cross(bForward, checks[14]);
|
||||
|
||||
return checks;
|
||||
}
|
||||
|
||||
public static Vector3dc satToleranced(final OrientedBoundingBox3d entityOBB, final OrientedBoundingBox3d obbB, final double tolerance) {
|
||||
Objects.requireNonNull(entityOBB, "entityOBB");
|
||||
Objects.requireNonNull(obbB, "obbB");
|
||||
|
||||
final LevelReusedVectors context = entityOBB.sink;
|
||||
|
||||
final Vector3d[] verticesA = entityOBB.vertices(context.a);
|
||||
final Vector3d[] verticesB = obbB.vertices(context.b);
|
||||
|
||||
final Vector3d checker = entityOBB.position.sub(obbB.position, new Vector3d()).normalize();
|
||||
|
||||
final Vector3d aRight = entityOBB.rotate(context.obbARight.set(OrientedBoundingBox3d.RIGHT));
|
||||
final Vector3d aUp = entityOBB.rotate(context.obbAUp.set(OrientedBoundingBox3d.UP));
|
||||
final Vector3d aForward = entityOBB.rotate(context.obbAForward.set(OrientedBoundingBox3d.FORWARD));
|
||||
|
||||
final Vector3d bRight = obbB.rotate(context.obbBRight.set(OrientedBoundingBox3d.RIGHT));
|
||||
final Vector3d bUp = obbB.rotate(context.obbBUp.set(OrientedBoundingBox3d.UP));
|
||||
final Vector3d bForward = obbB.rotate(context.obbBForward.set(OrientedBoundingBox3d.FORWARD));
|
||||
|
||||
Vector3d mtv = new Vector3d(Double.MAX_VALUE);
|
||||
|
||||
OrientedBoundingBox3d.genChecks(aRight, aUp, aForward, bRight, bUp, bForward, context.checks);
|
||||
|
||||
double minOverlap = Double.MAX_VALUE;
|
||||
|
||||
|
||||
int i = 0;
|
||||
for (final Vector3d check : context.checks) {
|
||||
if (check.lengthSquared() <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
check.normalize();
|
||||
|
||||
OrientedBoundingBox3d.checkSeparation(verticesA, check, context.proj1);
|
||||
OrientedBoundingBox3d.checkSeparation(verticesB, check, context.proj2);
|
||||
|
||||
if (check.dot(checker) > 0) {
|
||||
check.mul(-1.0);
|
||||
}
|
||||
|
||||
final double overlap = OrientedBoundingBox3d.getOverlap(context.proj1, context.proj2);
|
||||
|
||||
if (overlap == 0.f) { // shapes are not overlapping
|
||||
return context.zero;
|
||||
} else {
|
||||
if (overlap - (i == 14 ? 0.1 : 0.0) < minOverlap) {
|
||||
minOverlap = overlap;
|
||||
mtv = check.mul(minOverlap);
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
|
||||
final boolean facingOpposite = entityOBB.position.sub(obbB.position, context.oppo).dot(mtv) < 0;
|
||||
|
||||
if (facingOpposite) {
|
||||
mtv.mul(-1);
|
||||
}
|
||||
|
||||
return mtv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check separation along an axis for Separating Axis Theorem.
|
||||
*
|
||||
* @return a 2d vector with the first component representing minimum and second component maximum
|
||||
*/
|
||||
public static @NotNull Vector2d checkSeparation(final Vector3d @NotNull [] self, @NotNull final Vector3d axis, final Vector2d result) {
|
||||
if (axis.lengthSquared() <= 0.0) {
|
||||
return result.set(0, 0);
|
||||
}
|
||||
|
||||
double min = Double.MAX_VALUE;
|
||||
double max = -Double.MAX_VALUE;
|
||||
|
||||
for (final Vector3d vec : self) {
|
||||
final double dot = vec.dot(axis);
|
||||
min = Math.min(dot, min);
|
||||
max = Math.max(dot, max);
|
||||
}
|
||||
|
||||
return result.set(min, max);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package dev.ryanhcode.sable.api.particle;
|
||||
|
||||
import dev.ryanhcode.sable.api.math.OrientedBoundingBox3d;
|
||||
import org.joml.Vector3dc;
|
||||
|
||||
/**
|
||||
* An interface for sub-classes of {@link net.minecraft.client.particle.Particle} to implement that indicates whether
|
||||
* or not the particle should ever be kicked from the tracking sub-level
|
||||
*/
|
||||
public interface ParticleSubLevelKickable {
|
||||
default boolean sable$shouldCareAboutIntersectingSubLevels() {
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean sable$shouldKickFromTracking();
|
||||
|
||||
boolean sable$shouldCollideWithTrackingSubLevel();
|
||||
|
||||
default Vector3dc sable$getUpDirection() {
|
||||
return OrientedBoundingBox3d.UP;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
package dev.ryanhcode.sable.api.physics;
|
||||
|
||||
import dev.ryanhcode.sable.api.physics.constraint.PhysicsConstraintConfiguration;
|
||||
import dev.ryanhcode.sable.api.physics.constraint.PhysicsConstraintHandle;
|
||||
import dev.ryanhcode.sable.api.physics.object.box.BoxHandle;
|
||||
import dev.ryanhcode.sable.api.physics.object.box.BoxPhysicsObject;
|
||||
import dev.ryanhcode.sable.api.physics.object.rope.RopeHandle;
|
||||
import dev.ryanhcode.sable.api.physics.object.rope.RopePhysicsObject;
|
||||
import dev.ryanhcode.sable.api.sublevel.KinematicContraption;
|
||||
import dev.ryanhcode.sable.companion.math.Pose3d;
|
||||
import dev.ryanhcode.sable.companion.math.Pose3dc;
|
||||
import dev.ryanhcode.sable.physics.config.PhysicsConfigData;
|
||||
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import net.minecraft.core.SectionPos;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Quaterniondc;
|
||||
import org.joml.Vector3d;
|
||||
import org.joml.Vector3dc;
|
||||
|
||||
/**
|
||||
* An abstracted physics engine & pipeline for handling {@link dev.ryanhcode.sable.sublevel.SubLevel} physics calculations.
|
||||
*/
|
||||
public interface PhysicsPipeline {
|
||||
|
||||
/**
|
||||
* Initializes the physics pipeline.
|
||||
*
|
||||
* @param gravity the gravity vector
|
||||
* @param universalDrag the universal drag to apply to all bodies
|
||||
*/
|
||||
void init(Vector3dc gravity, double universalDrag);
|
||||
|
||||
/**
|
||||
* Disposes all resources used by the physics pipeline.
|
||||
*/
|
||||
void dispose();
|
||||
|
||||
/**
|
||||
* Sets up for the physics ticking with a time step of {@code 1.0 / 20.0} seconds.
|
||||
*/
|
||||
void prePhysicsTicks();
|
||||
|
||||
/**
|
||||
* Runs a physics substep with a time step of {@code 1.0 / 20.0 / substeps} seconds.
|
||||
*
|
||||
* @param timeStep the time step of this physics substep ({@code 1.0 / 20.0 / substeps}) [s]
|
||||
*/
|
||||
void physicsTick(double timeStep);
|
||||
|
||||
/**
|
||||
* Called after all physics substeps have been run, to finalize the physics tick.
|
||||
*/
|
||||
void postPhysicsTicks();
|
||||
|
||||
/**
|
||||
* Runs a tick to update any separate data tracking / logic, even if physics is currently paused
|
||||
*/
|
||||
void tick();
|
||||
|
||||
/**
|
||||
* Adds a {@link ServerSubLevel} to the physics pipeline.
|
||||
*/
|
||||
void add(ServerSubLevel subLevel, Pose3dc pose);
|
||||
|
||||
/**
|
||||
* Removes a {@link SubLevel} from the physics pipeline.
|
||||
*/
|
||||
void remove(ServerSubLevel subLevel);
|
||||
|
||||
/**
|
||||
* Adds a kinematic contraption to the scene
|
||||
*/
|
||||
void add(KinematicContraption contraption);
|
||||
|
||||
/**
|
||||
* Removes a kinematic contraption from the scene
|
||||
*/
|
||||
void remove(KinematicContraption contraption);
|
||||
|
||||
/**
|
||||
* Queries the physics pipeline for the current pose of a {@link SubLevel}.
|
||||
*
|
||||
* @param subLevel the sub-level to query
|
||||
* @param dest the pose to write into
|
||||
* @return the pose of the sub-level stored in dest
|
||||
*/
|
||||
@ApiStatus.OverrideOnly
|
||||
Pose3d readPose(ServerSubLevel subLevel, Pose3d dest);
|
||||
|
||||
/**
|
||||
* Adds a rope to the physics pipeline
|
||||
*/
|
||||
@ApiStatus.OverrideOnly
|
||||
RopeHandle addRope(RopePhysicsObject rope);
|
||||
|
||||
/**
|
||||
* Adds a box to the physics pipeline
|
||||
*/
|
||||
BoxHandle addBox(BoxPhysicsObject boxPhysicsObject);
|
||||
|
||||
/**
|
||||
* Handles the addition of a chunk to the physics context
|
||||
*
|
||||
* @param x the section x position
|
||||
* @param y the section y position
|
||||
* @param z the section z position
|
||||
*/
|
||||
void handleChunkSectionAddition(LevelChunkSection chunk, int x, int y, int z, boolean uploadDataIfGlobal);
|
||||
|
||||
/**
|
||||
* Handles the removal of a chunk section from the physics context
|
||||
*
|
||||
* @param x the section x position
|
||||
* @param y the section y position
|
||||
* @param z the section z position
|
||||
*/
|
||||
void handleChunkSectionRemoval(int x, int y, int z);
|
||||
|
||||
/**
|
||||
* Handles the change of a block (from oldState to newState) in a chunk at chunk-relative position x, y, z.
|
||||
* Only called server-side.
|
||||
*
|
||||
* @param x chunk-relative x position
|
||||
* @param z chunk-relative z position
|
||||
* @param y chunk-relative y position
|
||||
*/
|
||||
void handleBlockChange(SectionPos sectionPos, LevelChunkSection chunk, int x, int y, int z, BlockState oldState, BlockState newState);
|
||||
|
||||
/**
|
||||
* Called to re-upload center of mass, mass properties, and local bounds to the physics pipeline
|
||||
*/
|
||||
default void onStatsChanged(@NotNull final ServerSubLevel serverSubLevel) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Teleports the physics pipeline body to a given position.
|
||||
*
|
||||
* @param body the physics pipeline body to teleport
|
||||
* @param position the new position to teleport to
|
||||
* @param orientation the new orientation to teleport to
|
||||
*/
|
||||
void teleport(PhysicsPipelineBody body, Vector3dc position, Quaterniondc orientation);
|
||||
|
||||
/**
|
||||
* Adds a force at a given world position to a data containing the position
|
||||
*
|
||||
* @param body the physics pipeline body to apply the force to
|
||||
* @param position the plot position to apply the force at [m]
|
||||
* @param force the force to apply [N]
|
||||
*/
|
||||
void applyImpulse(PhysicsPipelineBody body, Vector3dc position, Vector3dc force);
|
||||
|
||||
/**
|
||||
* Adds a local force and torque
|
||||
*
|
||||
* @param body the body to apply the force to
|
||||
* @param force the local force to apply [N]
|
||||
* @param torque the local torque to apply [Nm]
|
||||
* @param wakeUp if the physics pipeline body should be woken if it is sleeping
|
||||
*/
|
||||
void applyLinearAndAngularImpulse(PhysicsPipelineBody body, Vector3dc force, Vector3dc torque, boolean wakeUp);
|
||||
|
||||
/**
|
||||
* Adds linear and angular velocities to a physics pipeline body
|
||||
*
|
||||
* @param body the physics pipeline body to apply the velocities to
|
||||
* @param linearVelocity the linear velocity to apply [m/s]
|
||||
* @param angularVelocity the angular velocity to apply [rad/s]
|
||||
*/
|
||||
default void addLinearAndAngularVelocity(final PhysicsPipelineBody body, final Vector3dc linearVelocity, final Vector3dc angularVelocity) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the velocity of a physics pipeline body
|
||||
*
|
||||
* @param body the physics pipeline body to reset the velocity of
|
||||
*/
|
||||
default void resetVelocity(final PhysicsPipelineBody body) {
|
||||
this.addLinearAndAngularVelocity(body, this.getLinearVelocity(body, new Vector3d()).negate(), this.getAngularVelocity(body, new Vector3d()).negate());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the linear velocity of a physics pipeline body
|
||||
*
|
||||
* @param body the physics pipeline body to get the linear velocity from
|
||||
* @return the global linear velocity of the body from the physics engine, stored in dest [m/s]
|
||||
*/
|
||||
default Vector3d getLinearVelocity(final PhysicsPipelineBody body, final Vector3d dest) {
|
||||
return dest.zero();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the angular velocity of a physics pipeline body
|
||||
*
|
||||
* @param body the physics pipeline body to get the angular velocity from
|
||||
* @return the global angular velocity of the body from the physics engine, stored in dest [rad/s]
|
||||
*/
|
||||
default Vector3d getAngularVelocity(final PhysicsPipelineBody body, final Vector3d dest) {
|
||||
return dest.zero();
|
||||
}
|
||||
|
||||
/**
|
||||
* "Wakes up" a physics pipeline body, indicating environmental or other changes have occurred that should resume physics if idled or sleeping
|
||||
*
|
||||
* @param body the physics pipeline body to wake up
|
||||
*/
|
||||
void wakeUp(PhysicsPipelineBody body);
|
||||
|
||||
/**
|
||||
* Adds a constraint to the engine, returning its handle
|
||||
*
|
||||
* @param sublevelA the first sub-level to constrain, or null to constrain the second sub-level to the world
|
||||
* @param sublevelB the second sub-level to constrain, or null to constrain the first sub-level to the world
|
||||
* @param configuration the configuration of the constraint
|
||||
*/
|
||||
default <T extends PhysicsConstraintHandle> T addConstraint(@Nullable final ServerSubLevel sublevelA, @Nullable final ServerSubLevel sublevelB, final PhysicsConstraintConfiguration<T> configuration) {
|
||||
throw new UnsupportedOperationException("Not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the config of the physics engine from a data object
|
||||
*
|
||||
* @param data the data to update from
|
||||
*/
|
||||
@ApiStatus.OverrideOnly
|
||||
default void updateConfigFrom(final PhysicsConfigData data) {
|
||||
throw new UnsupportedOperationException("Not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the next runtime ID for a collider / sub-level
|
||||
*/
|
||||
int getNextRuntimeID();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package dev.ryanhcode.sable.api.physics;
|
||||
|
||||
import dev.ryanhcode.sable.api.physics.mass.MassData;
|
||||
|
||||
/**
|
||||
* A rigid-body tracked by a {@link PhysicsPipeline}
|
||||
*/
|
||||
public interface PhysicsPipelineBody {
|
||||
|
||||
int NULL_RUNTIME_ID = -1;
|
||||
|
||||
/**
|
||||
* The runtime integer ID tracked by the {@link PhysicsPipeline}
|
||||
*/
|
||||
int getRuntimeId();
|
||||
|
||||
/**
|
||||
* @return the mass data for this physics body
|
||||
*/
|
||||
MassData getMassTracker();
|
||||
|
||||
/**
|
||||
* @return if this body has been removed by the pipeline
|
||||
*/
|
||||
boolean isRemoved();
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package dev.ryanhcode.sable.api.physics.callback;
|
||||
|
||||
import dev.ryanhcode.sable.companion.math.JOMLConversion;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.joml.Vector3d;
|
||||
import org.joml.Vector3dc;
|
||||
|
||||
public interface BlockSubLevelCollisionCallback {
|
||||
|
||||
/**
|
||||
* Called when a collision occurs between two blocks, from JNI / pipeline implementations
|
||||
*
|
||||
* @return tangent motion
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
@SuppressWarnings("unused")
|
||||
default double[] onCollision(final int x,
|
||||
final int y,
|
||||
final int z,
|
||||
final double x1,
|
||||
final double y1,
|
||||
final double z1,
|
||||
final double impactVelocity) {
|
||||
final CollisionResult result = this.sable$onCollision(new BlockPos(x, y, z), new Vector3d(x1, y1, z1), impactVelocity);
|
||||
final Vector3dc motion = result.tangentMotion;
|
||||
|
||||
// TODO: this is stupid and moronic to pass through the removal as a double lmao, let's not do that in the future
|
||||
return new double[]{motion.x(), motion.y(), motion.z(), result.removeCollision ? 1.0 : 0.0};
|
||||
}
|
||||
|
||||
CollisionResult sable$onCollision(BlockPos blockPos, Vector3d pos, double impactVelocity);
|
||||
|
||||
record CollisionResult(Vector3dc tangentMotion, boolean removeCollision) {
|
||||
public static final CollisionResult NONE = new CollisionResult(JOMLConversion.ZERO, false);
|
||||
}
|
||||
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package dev.ryanhcode.sable.api.physics.collider;
|
||||
|
||||
import dev.ryanhcode.sable.physics.impl.SableCollisionContextImpl;
|
||||
import net.minecraft.world.phys.shapes.CollisionContext;
|
||||
|
||||
/**
|
||||
* Context used for getting collision shapes for sable physics pipelines.
|
||||
*/
|
||||
public interface SableCollisionContext extends CollisionContext {
|
||||
|
||||
/**
|
||||
* @return The collision context to use for getting shapes
|
||||
*/
|
||||
static SableCollisionContext get() {
|
||||
return SableCollisionContextImpl.INSTANCE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package dev.ryanhcode.sable.api.physics.collider;
|
||||
|
||||
import org.joml.Vector3dc;
|
||||
|
||||
public interface VoxelColliderData {
|
||||
/**
|
||||
* Adds a collision box to the block physics data entry.
|
||||
* Coordinates are expected to be within a single voxel space of the block, 0-1.
|
||||
*
|
||||
* @param min the minimum corner of the box
|
||||
* @param max the maximum corner of the box
|
||||
*/
|
||||
void addBox(Vector3dc min, Vector3dc max);
|
||||
|
||||
/**
|
||||
* Clears all collision boxes
|
||||
*/
|
||||
void clearBoxes();
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package dev.ryanhcode.sable.api.physics.constraint;
|
||||
|
||||
/**
|
||||
* All degrees of freedom that joint motors and locks can be imposed on
|
||||
*/
|
||||
public enum ConstraintJointAxis {
|
||||
LINEAR_X,
|
||||
LINEAR_Y,
|
||||
LINEAR_Z,
|
||||
ANGULAR_X,
|
||||
ANGULAR_Y,
|
||||
ANGULAR_Z;
|
||||
|
||||
public static final ConstraintJointAxis[] ALL = values();
|
||||
public static final ConstraintJointAxis[] LINEAR = {LINEAR_X, LINEAR_Y, LINEAR_Z};
|
||||
public static final ConstraintJointAxis[] ANGULAR = {ANGULAR_X, ANGULAR_Y, ANGULAR_Z};
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package dev.ryanhcode.sable.api.physics.constraint;
|
||||
|
||||
/**
|
||||
* A configuration for a physics constraint.
|
||||
* @param <T> the type of constraint handle this configuration produces
|
||||
*/
|
||||
public interface PhysicsConstraintConfiguration<T extends PhysicsConstraintHandle> {
|
||||
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package dev.ryanhcode.sable.api.physics.constraint;
|
||||
|
||||
import org.joml.Vector3d;
|
||||
|
||||
/**
|
||||
* An active constraint tracked by the physics world.
|
||||
* Must be kept track of to be removed.
|
||||
*/
|
||||
public interface PhysicsConstraintHandle {
|
||||
|
||||
/**
|
||||
* Gets the latest global linear and angular joint impulses from the solver
|
||||
*/
|
||||
void getJointImpulses(Vector3d linearImpulseDest, Vector3d angularImpulseDest);
|
||||
|
||||
/**
|
||||
* Sets if contacts are enabled between the two bodies in the constraint
|
||||
*/
|
||||
void setContactsEnabled(boolean enabled);
|
||||
|
||||
/**
|
||||
* Adds / sets a motor on this joint
|
||||
*
|
||||
* @param axis The axis on which the motor operates
|
||||
* @param target The target position along that axis [m | rad]
|
||||
* @param stiffness How stiff the motor should act, or P in the PD controller
|
||||
* @param damping How much damping the motor should have, or D in the PD controller
|
||||
* @param hasMaxForce If the motor should have a force limit
|
||||
* @param maxForce The maximum force the motor can apply
|
||||
*/
|
||||
void setMotor(ConstraintJointAxis axis, double target, double stiffness, double damping, boolean hasMaxForce, double maxForce);
|
||||
|
||||
/**
|
||||
* Removes the constraint from the active physics engine
|
||||
*/
|
||||
void remove();
|
||||
|
||||
/**
|
||||
* @return if the constraint is still valid, and has not been removed by the engine
|
||||
*/
|
||||
boolean isValid();
|
||||
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package dev.ryanhcode.sable.api.physics.constraint.fixed;
|
||||
|
||||
import dev.ryanhcode.sable.api.physics.constraint.PhysicsConstraintConfiguration;
|
||||
import org.joml.Quaterniondc;
|
||||
import org.joml.Vector3dc;
|
||||
|
||||
/**
|
||||
* A configuration for a fixed constraint, with both bodies locked to eachother.
|
||||
*
|
||||
* @param pos1 the position in world space assumed to be inside the plot of the first sub-level (ex. a block position).
|
||||
* @param pos2 the position in world space assumed to be inside the plot of the second sub-level (ex. a block position).
|
||||
* @param orientation the local orientation of the second body from the first. Motor axes will be relative to this frame
|
||||
*/
|
||||
public record FixedConstraintConfiguration(Vector3dc pos1, Vector3dc pos2, Quaterniondc orientation) implements PhysicsConstraintConfiguration<FixedConstraintHandle> {
|
||||
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package dev.ryanhcode.sable.api.physics.constraint.fixed;
|
||||
|
||||
import dev.ryanhcode.sable.api.physics.constraint.PhysicsConstraintHandle;
|
||||
|
||||
/**
|
||||
* A fixed constraint between two bodies
|
||||
*/
|
||||
public interface FixedConstraintHandle extends PhysicsConstraintHandle {
|
||||
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package dev.ryanhcode.sable.api.physics.constraint.free;
|
||||
|
||||
import dev.ryanhcode.sable.api.physics.constraint.PhysicsConstraintConfiguration;
|
||||
import org.joml.Quaterniondc;
|
||||
import org.joml.Vector3dc;
|
||||
|
||||
/**
|
||||
* A configuration for a free constraint, which imposes no locks
|
||||
*
|
||||
* @param pos1 the position in world space assumed to be inside the plot of the first sub-level (ex. a block position).
|
||||
* @param pos2 the position in world space assumed to be inside the plot of the second sub-level (ex. a block position).
|
||||
* @param orientation the local orientation of the second body from the first. Motor axes will be relative to this frame
|
||||
*/
|
||||
public record FreeConstraintConfiguration(Vector3dc pos1, Vector3dc pos2, Quaterniondc orientation) implements PhysicsConstraintConfiguration<FreeConstraintHandle> {
|
||||
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package dev.ryanhcode.sable.api.physics.constraint.free;
|
||||
|
||||
import dev.ryanhcode.sable.api.physics.constraint.PhysicsConstraintHandle;
|
||||
|
||||
/**
|
||||
* A free constraint between two bodies
|
||||
*/
|
||||
public interface FreeConstraintHandle extends PhysicsConstraintHandle {
|
||||
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package dev.ryanhcode.sable.api.physics.constraint.generic;
|
||||
|
||||
import dev.ryanhcode.sable.api.physics.constraint.ConstraintJointAxis;
|
||||
import dev.ryanhcode.sable.api.physics.constraint.PhysicsConstraintConfiguration;
|
||||
import org.joml.Quaterniondc;
|
||||
import org.joml.Vector3dc;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A configuration for a generic constraint, with per-axis hard locks and re-anchorable local frames.
|
||||
*
|
||||
* @param pos1 the position in world space assumed to be inside the plot of the first sub-level (ex. a block position).
|
||||
* @param pos2 the position in world space assumed to be inside the plot of the second sub-level (ex. a block position).
|
||||
* @param orientation1 the local orientation of the joint frame on the first sub-level.
|
||||
* @param orientation2 the local orientation of the joint frame on the second sub-level.
|
||||
* @param lockedAxes the set of axes hard-locked by the solver; empty matches a free constraint.
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public record GenericConstraintConfiguration(
|
||||
Vector3dc pos1,
|
||||
Vector3dc pos2,
|
||||
Quaterniondc orientation1,
|
||||
Quaterniondc orientation2,
|
||||
Set<ConstraintJointAxis> lockedAxes
|
||||
) implements PhysicsConstraintConfiguration<GenericConstraintHandle> {
|
||||
|
||||
public GenericConstraintConfiguration(final Vector3dc pos1, final Vector3dc pos2, final Quaterniondc orientation1, final Quaterniondc orientation2) {
|
||||
this(pos1, pos2, orientation1, orientation2, EnumSet.noneOf(ConstraintJointAxis.class));
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package dev.ryanhcode.sable.api.physics.constraint.generic;
|
||||
|
||||
import dev.ryanhcode.sable.api.physics.constraint.PhysicsConstraintHandle;
|
||||
import org.joml.Quaterniondc;
|
||||
import org.joml.Vector3dc;
|
||||
|
||||
/**
|
||||
* A generic constraint between two bodies.
|
||||
*
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public interface GenericConstraintHandle extends PhysicsConstraintHandle {
|
||||
|
||||
/**
|
||||
* Sets the local frame on the first body.
|
||||
*
|
||||
* @param localPosition the local anchor position
|
||||
* @param localRotation the local frame orientation
|
||||
*/
|
||||
void setFrame1(Vector3dc localPosition, Quaterniondc localRotation);
|
||||
|
||||
/**
|
||||
* Sets the local frame on the second body.
|
||||
*
|
||||
* @param localPosition the local anchor position
|
||||
* @param localRotation the local frame orientation
|
||||
*/
|
||||
void setFrame2(Vector3dc localPosition, Quaterniondc localRotation);
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package dev.ryanhcode.sable.api.physics.constraint.rotary;
|
||||
|
||||
import dev.ryanhcode.sable.api.physics.constraint.PhysicsConstraintConfiguration;
|
||||
import org.joml.Vector3dc;
|
||||
|
||||
/**
|
||||
* A configuration for a rotary joint constraint, with a single angular DOF.
|
||||
* @param pos1 the position in world space assumed to be inside the plot of the first sub-level (ex. a block position).
|
||||
* @param pos2 the position in world space assumed to be inside the plot of the second sub-level (ex. a block positino).
|
||||
* @param normal1 the local normal of the joint on the first sub-level.
|
||||
* @param normal2 the local normal of the joint on the second sub-level.
|
||||
*/
|
||||
public record RotaryConstraintConfiguration(Vector3dc pos1, Vector3dc pos2, Vector3dc normal1, Vector3dc normal2) implements PhysicsConstraintConfiguration<RotaryConstraintHandle> {
|
||||
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package dev.ryanhcode.sable.api.physics.constraint.rotary;
|
||||
|
||||
import dev.ryanhcode.sable.api.physics.constraint.ConstraintJointAxis;
|
||||
import dev.ryanhcode.sable.api.physics.constraint.PhysicsConstraintHandle;
|
||||
|
||||
/**
|
||||
* A constraint handle for a rotary / motor constraint between two bodies
|
||||
*/
|
||||
public interface RotaryConstraintHandle extends PhysicsConstraintHandle {
|
||||
|
||||
ConstraintJointAxis DEFAULT_AXIS = ConstraintJointAxis.ANGULAR_X;
|
||||
|
||||
/**
|
||||
* Sets the servo coefficients for this rotary constraint.
|
||||
*
|
||||
* @param angle the target angle [radians]
|
||||
* @param stiffness the stiffness of the servo
|
||||
* @param damping the damping of the servo
|
||||
* @deprecated use {@link #setMotor(ConstraintJointAxis, double, double, double, boolean, double)} instead with {@link RotaryConstraintHandle#DEFAULT_AXIS}.
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
default void setServoCoefficients(final double angle, final double stiffness, final double damping) {
|
||||
this.setMotor(DEFAULT_AXIS, angle, stiffness, damping, false, 0.0);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package dev.ryanhcode.sable.api.physics.force;
|
||||
|
||||
import net.minecraft.network.chat.Component;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* A grouping of forces, for queued force totals & display to the user for making contraptions
|
||||
*
|
||||
* @param name the name of the force group
|
||||
* @param description the description of the force group
|
||||
* @param color the RGB color of the force group
|
||||
* @param defaultDisplayed if the force group should be default displayed in GUIs
|
||||
*/
|
||||
public record ForceGroup(@NotNull Component name, @Nullable Component description, int color, boolean defaultDisplayed) {
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package dev.ryanhcode.sable.api.physics.force;
|
||||
|
||||
import dev.ryanhcode.sable.Sable;
|
||||
import foundry.veil.platform.registry.RegistrationProvider;
|
||||
import foundry.veil.platform.registry.RegistryObject;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
|
||||
/**
|
||||
* All default force groups
|
||||
*/
|
||||
public class ForceGroups {
|
||||
public static final ResourceKey<Registry<ForceGroup>> REGISTRY_KEY = ResourceKey.createRegistryKey(Sable.sablePath("force_groups"));
|
||||
private static final RegistrationProvider<ForceGroup> VANILLA_PROVIDER;
|
||||
public static final Registry<ForceGroup> REGISTRY;
|
||||
|
||||
static {
|
||||
VANILLA_PROVIDER = RegistrationProvider.get(REGISTRY_KEY, Sable.MOD_ID);
|
||||
REGISTRY = VANILLA_PROVIDER.asVanillaRegistry();
|
||||
}
|
||||
|
||||
public static final RegistryObject<ForceGroup> GRAVITY = VANILLA_PROVIDER.register(Sable.sablePath("gravity"), () -> new ForceGroup(Component.translatable("force_group.sable.gravity"), null, 0x216e55, false));
|
||||
public static final RegistryObject<ForceGroup> DRAG = VANILLA_PROVIDER.register(Sable.sablePath("drag"), () -> new ForceGroup(Component.translatable("force_group.sable.drag"), null, 0x834f31, false));
|
||||
public static final RegistryObject<ForceGroup> LEVITATION = VANILLA_PROVIDER.register(Sable.sablePath("levitation"), () -> new ForceGroup(Component.translatable("force_group.sable.levitation"), null, 0x734480, true));
|
||||
public static final RegistryObject<ForceGroup> BALLOON_LIFT = VANILLA_PROVIDER.register(Sable.sablePath("balloon_lift"), () -> new ForceGroup(Component.translatable("force_group.sable.balloon_lift"), null, 0xd2643e, true));
|
||||
public static final RegistryObject<ForceGroup> PROPULSION = VANILLA_PROVIDER.register(Sable.sablePath("propulsion"), () -> new ForceGroup(Component.translatable("force_group.sable.propulsion"), null, 0x5a7c9f, true));
|
||||
public static final RegistryObject<ForceGroup> LIFT = VANILLA_PROVIDER.register(Sable.sablePath("lift"), () -> new ForceGroup(Component.translatable("force_group.sable.lift"), null, 0x8cb6c6, true));
|
||||
public static final RegistryObject<ForceGroup> MAGNETIC_FORCE = VANILLA_PROVIDER.register(Sable.sablePath("magnetic_force"), () -> new ForceGroup(Component.translatable("force_group.sable.magnetic_force"), null, 0xe05343, false));
|
||||
|
||||
public static void register() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* The count of registered force groups
|
||||
*/
|
||||
public static int count() {
|
||||
return REGISTRY.size();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package dev.ryanhcode.sable.api.physics.force;
|
||||
|
||||
import dev.ryanhcode.sable.api.physics.handle.RigidBodyHandle;
|
||||
import dev.ryanhcode.sable.api.physics.mass.MassData;
|
||||
import dev.ryanhcode.sable.api.physics.mass.MassTracker;
|
||||
import dev.ryanhcode.sable.companion.math.JOMLConversion;
|
||||
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.joml.Vector3d;
|
||||
import org.joml.Vector3dc;
|
||||
|
||||
/**
|
||||
* Utility class for applying forces to {@link RigidBodyHandle rigid-body handles}
|
||||
*/
|
||||
public class ForceTotal {
|
||||
|
||||
private final Vector3d temp = new Vector3d();
|
||||
private final Vector3d lastLocalForce = new Vector3d();
|
||||
private final Vector3d lastLocalTorque = new Vector3d();
|
||||
private final Vector3d localForce = new Vector3d();
|
||||
private final Vector3d localTorque = new Vector3d();
|
||||
|
||||
@ApiStatus.Internal
|
||||
public void applyForces(final RigidBodyHandle handle) {
|
||||
final boolean forceChanged = this.localForce.distanceSquared( this.lastLocalForce) > 1e-5;
|
||||
final boolean torqueChanged = this.localTorque.distanceSquared(this.lastLocalTorque) > 1e-5;
|
||||
|
||||
final boolean wakeUp = forceChanged || torqueChanged;
|
||||
handle.applyLinearAndAngularImpulse(this.localForce, this.localTorque, wakeUp);
|
||||
|
||||
this.lastLocalForce.set(this.localForce);
|
||||
this.lastLocalTorque.set(this.localTorque);
|
||||
|
||||
this.localForce.set(0.0, 0.0, 0.0);
|
||||
this.localTorque.set(0.0, 0.0, 0.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the current force total
|
||||
*/
|
||||
public void reset() {
|
||||
this.localForce.set(0.0, 0.0, 0.0);
|
||||
this.localTorque.set(0.0, 0.0, 0.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies another force total to this one
|
||||
*
|
||||
* @param other the other force total to apply
|
||||
*/
|
||||
public void applyForceTotal(final ForceTotal other) {
|
||||
this.localForce.add(other.localForce);
|
||||
this.localTorque.add(other.localTorque);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds to both local linear and angular momenta
|
||||
*
|
||||
* @param impulse the local impulse to apply [N]
|
||||
* @param torque the local torque to apply [Nm]
|
||||
*/
|
||||
public void applyLinearAndAngularImpulse(final Vector3dc impulse, final Vector3dc torque) {
|
||||
this.localForce.add(impulse);
|
||||
this.localTorque.add(torque);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds to local linear momenta
|
||||
*
|
||||
* @param impulse the local impulse to apply [N]
|
||||
*/
|
||||
public void applyLinearImpulse(final Vector3dc impulse) {
|
||||
this.applyLinearAndAngularImpulse(impulse, JOMLConversion.ZERO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds to local angular momenta
|
||||
*
|
||||
* @param impulse the local impulse to apply [N]
|
||||
*/
|
||||
public void applyAngularImpulse(final Vector3dc impulse) {
|
||||
this.applyLinearAndAngularImpulse(JOMLConversion.ZERO, impulse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds to local angular momenta
|
||||
*
|
||||
* @param torque the local torque to apply [Nm]
|
||||
*/
|
||||
public void applyTorqueImpulse(final Vector3dc torque) {
|
||||
this.applyAngularImpulse(torque);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a momenta impulse at a given world position to a data containing the position
|
||||
*
|
||||
* @param position the position inside the plot to apply the force at [m]
|
||||
* @param force the local impulse to apply [N]
|
||||
*/
|
||||
public void applyImpulseAtPoint(final MassData massTracker, final Vector3dc position, final Vector3dc force) {
|
||||
this.localForce.add(force);
|
||||
position.sub(massTracker.getCenterOfMass(), this.temp);
|
||||
this.localTorque.add(this.temp.cross(force));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a momenta impulse at a given world position to a data containing the position
|
||||
*
|
||||
* @param position the position inside the plot to apply the force at [m]
|
||||
* @param force the local impulse to apply [N]
|
||||
*/
|
||||
public void applyImpulseAtPoint(final ServerSubLevel massTracker, final Vector3dc position, final Vector3dc force) {
|
||||
this.applyImpulseAtPoint(
|
||||
massTracker.getMassTracker(),
|
||||
position,
|
||||
force
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the current totalled local force
|
||||
*/
|
||||
public Vector3d getLocalForce() {
|
||||
return this.localForce;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the current totalled local torque
|
||||
*/
|
||||
public Vector3d getLocalTorque() {
|
||||
return this.localTorque;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a momenta impulse at a given world position to a data containing the position
|
||||
*
|
||||
* @param position the position inside the plot to apply the force at [m]
|
||||
* @param force the local impulse to apply [N]
|
||||
*/
|
||||
public void applyImpulseAtPoint(final MassTracker massTracker, final Vec3 position, final Vec3 force) {
|
||||
this.applyImpulseAtPoint(
|
||||
massTracker,
|
||||
JOMLConversion.toJOML(position),
|
||||
JOMLConversion.toJOML(force)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package dev.ryanhcode.sable.api.physics.force;
|
||||
|
||||
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
import org.joml.Vector3dc;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A grouping of applied point forces, alongside a force total to be applied.
|
||||
*/
|
||||
public class QueuedForceGroup {
|
||||
private final List<PointForce> appliedForces = new ObjectArrayList<>();
|
||||
private final ForceTotal forceTotal = new ForceTotal();
|
||||
private final ServerSubLevel subLevel;
|
||||
|
||||
public QueuedForceGroup(final ServerSubLevel serverSubLevel) {
|
||||
this.subLevel = serverSubLevel;
|
||||
}
|
||||
|
||||
public ForceTotal getForceTotal() {
|
||||
return this.forceTotal;
|
||||
}
|
||||
|
||||
public void applyAndRecordPointForce(final Vector3dc point, final Vector3dc force) {
|
||||
this.forceTotal.applyImpulseAtPoint(this.subLevel.getMassTracker(), point, force);
|
||||
this.recordPointForce(point, force);
|
||||
}
|
||||
public void recordPointForce(final Vector3dc point, final Vector3dc force) {
|
||||
if (!this.subLevel.isTrackingIndividualQueuedForces()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (force.lengthSquared() > 0.001 * 0.001) {
|
||||
this.appliedForces.add(new PointForce(point, force));
|
||||
}
|
||||
}
|
||||
|
||||
public List<PointForce> getRecordedPointForces() {
|
||||
return this.appliedForces;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
this.forceTotal.reset();
|
||||
this.appliedForces.clear();
|
||||
}
|
||||
|
||||
public record PointForce(Vector3dc point, Vector3dc force) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
package dev.ryanhcode.sable.api.physics.handle;
|
||||
|
||||
import dev.ryanhcode.sable.api.physics.PhysicsPipelineBody;
|
||||
import dev.ryanhcode.sable.api.physics.force.ForceTotal;
|
||||
import dev.ryanhcode.sable.api.sublevel.ServerSubLevelContainer;
|
||||
import dev.ryanhcode.sable.api.sublevel.SubLevelContainer;
|
||||
import dev.ryanhcode.sable.companion.math.JOMLConversion;
|
||||
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.system.SubLevelPhysicsSystem;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Quaterniondc;
|
||||
import org.joml.Vector3d;
|
||||
import org.joml.Vector3dc;
|
||||
|
||||
/**
|
||||
* A handle for easy access to physics-related operations on a {@link dev.ryanhcode.sable.sublevel.ServerSubLevel}.
|
||||
*/
|
||||
public class RigidBodyHandle {
|
||||
private final PhysicsPipelineBody body;
|
||||
private final SubLevelPhysicsSystem physicsSystem;
|
||||
|
||||
/**
|
||||
* Obtains a handle for a given physics body.
|
||||
*
|
||||
* @param level the level to obtain the handle for
|
||||
* @param body the sub-level to obtain the handle for
|
||||
*/
|
||||
@Contract("_,_ -> _")
|
||||
public static @Nullable RigidBodyHandle of(final ServerLevel level, final PhysicsPipelineBody body) {
|
||||
final ServerSubLevelContainer container = SubLevelContainer.getContainer(level);
|
||||
|
||||
if (container == null) {
|
||||
return null;
|
||||
}
|
||||
final SubLevelPhysicsSystem physicsSystem = container.physicsSystem();
|
||||
|
||||
return new RigidBodyHandle(body, physicsSystem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a handle for a given server sub-level.
|
||||
* </br>
|
||||
* If the physics system is already available in-scope or this is being called in bulk, the handle should be obtained
|
||||
* through {@link SubLevelPhysicsSystem#getPhysicsHandle(ServerSubLevel)}.
|
||||
*
|
||||
* @param subLevel the sub-level to obtain the handle for
|
||||
*/
|
||||
@Contract("_ -> new")
|
||||
public static @Nullable RigidBodyHandle of(final ServerSubLevel subLevel) {
|
||||
final ServerLevel level = subLevel.getLevel();
|
||||
final ServerSubLevelContainer container = SubLevelContainer.getContainer(level);
|
||||
|
||||
if (container == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final SubLevelPhysicsSystem physicsSystem = container.physicsSystem();
|
||||
|
||||
return physicsSystem.getPhysicsHandle(subLevel);
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
public RigidBodyHandle(final PhysicsPipelineBody body, final SubLevelPhysicsSystem physicsSystem) {
|
||||
this.body = body;
|
||||
this.physicsSystem = physicsSystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a momenta impulse at a given world position to a data containing the position
|
||||
*
|
||||
* @param position the position inside the plot to apply the force at [m]
|
||||
* @param force the local impulse to apply [N]
|
||||
*/
|
||||
public void applyImpulseAtPoint(final Vector3dc position, final Vector3dc force) {
|
||||
this.physicsSystem.getPipeline().applyImpulse(this.body, position, force);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a momenta impulse at a given world position to a data containing the position
|
||||
*
|
||||
* @param position the position inside the plot to apply the force at [m]
|
||||
* @param force the local impulse to apply [N]
|
||||
*/
|
||||
public void applyImpulseAtPoint(final Vec3 position, final Vec3 force) {
|
||||
this.physicsSystem.getPipeline().applyImpulse(this.body, JOMLConversion.toJOML(position), JOMLConversion.toJOML(force));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds to both local linear and angular momenta
|
||||
*
|
||||
* @param impulse the local impulse to apply [N]
|
||||
* @param torque the local torque to apply [Nm]
|
||||
*/
|
||||
public void applyLinearAndAngularImpulse(final Vector3dc impulse, final Vector3dc torque) {
|
||||
this.applyLinearAndAngularImpulse(impulse, torque, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds to both local linear and angular momenta
|
||||
*
|
||||
* @param impulse the local impulse to apply [N]
|
||||
* @param torque the local torque to apply [Nm]
|
||||
*/
|
||||
public void applyLinearAndAngularImpulse(final Vector3dc impulse, final Vector3dc torque, final boolean wakeUp) {
|
||||
this.physicsSystem.getPipeline().applyLinearAndAngularImpulse(this.body, impulse, torque, wakeUp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds to local linear momenta
|
||||
*
|
||||
* @param impulse the local impulse to apply [N]
|
||||
*/
|
||||
public void applyLinearImpulse(final Vector3dc impulse) {
|
||||
this.applyLinearAndAngularImpulse(impulse, JOMLConversion.ZERO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds to local angular momenta
|
||||
*
|
||||
* @param impulse the local impulse to apply [N]
|
||||
*/
|
||||
public void applyAngularImpulse(final Vector3dc impulse) {
|
||||
this.applyLinearAndAngularImpulse(JOMLConversion.ZERO, impulse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds to local angular momenta
|
||||
*
|
||||
* @param torque the local torque to apply [Nm]
|
||||
*/
|
||||
public void applyTorqueImpulse(final Vector3dc torque) {
|
||||
this.applyAngularImpulse(torque);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the global linear velocity of the body from the physics engine [m/s]
|
||||
* @deprecated Use {@link RigidBodyHandle#getLinearVelocity(Vector3d)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public Vector3dc getLinearVelocity() {
|
||||
return this.physicsSystem.getPipeline().getLinearVelocity(this.body, new Vector3d());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the global angular velocity of the body from the physics engine [rad/s]
|
||||
*/
|
||||
@Deprecated
|
||||
public Vector3dc getAngularVelocity() {
|
||||
return this.physicsSystem.getPipeline().getAngularVelocity(this.body, new Vector3d());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param dest the destination vector to store the result in
|
||||
* @return the global linear velocity of the body from the physics engine, stored in dest [m/s]
|
||||
*/
|
||||
public Vector3d getLinearVelocity(final Vector3d dest) {
|
||||
return this.physicsSystem.getPipeline().getLinearVelocity(this.body, dest);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param dest the destination vector to store the result in
|
||||
* @return the global angular velocity of the body from the physics engine, stored in dest [rad/s]
|
||||
*/
|
||||
public Vector3d getAngularVelocity(final Vector3d dest) {
|
||||
return this.physicsSystem.getPipeline().getAngularVelocity(this.body, dest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies forces from a force applicator to this body.
|
||||
* If the forces have not changed significantly since the last time the force total was used, the rigid-body will not be woken up.
|
||||
*/
|
||||
public void applyForcesAndReset(final ForceTotal forceTotal) {
|
||||
forceTotal.applyForces(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds linear and angular velocities
|
||||
*
|
||||
* @param linearVelocity the linear velocity to apply [m/s]
|
||||
* @param angularVelocity the angular velocity to apply [rad/s]
|
||||
*/
|
||||
public void addLinearAndAngularVelocity(final Vector3dc linearVelocity, final Vector3dc angularVelocity) {
|
||||
this.physicsSystem.getPipeline().addLinearAndAngularVelocity(this.body, linearVelocity, angularVelocity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Teleports the physics pipeline body to a given position.
|
||||
*
|
||||
* @param position the new position to teleport to
|
||||
* @param orientation the new orientation to teleport to
|
||||
*/
|
||||
public void teleport(final Vector3dc position, final Quaterniondc orientation) {
|
||||
this.physicsSystem.getPipeline().teleport(this.body, position, orientation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this handle is valid.
|
||||
*
|
||||
* @return true if the handle is alid
|
||||
*/
|
||||
public boolean isValid() {
|
||||
return !this.body.isRemoved();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package dev.ryanhcode.sable.api.physics.mass;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Matrix3dc;
|
||||
import org.joml.Vector3d;
|
||||
import org.joml.Vector3dc;
|
||||
|
||||
public interface MassData {
|
||||
|
||||
/**
|
||||
* @return total mass [kpg]
|
||||
*/
|
||||
double getMass();
|
||||
|
||||
/**
|
||||
* @return total inverse mass [1/kpg]
|
||||
*/
|
||||
double getInverseMass();
|
||||
|
||||
/**
|
||||
* @return inertia tensor in local space [kpg*m^2]
|
||||
*/
|
||||
Matrix3dc getInertiaTensor();
|
||||
|
||||
/**
|
||||
* @return inverse inertia tensor in local space [1/(kpg*m^2)]
|
||||
*/
|
||||
Matrix3dc getInverseInertiaTensor();
|
||||
|
||||
/**
|
||||
* @return the nullable location of the center-of-mass
|
||||
*/
|
||||
@Nullable
|
||||
Vector3dc getCenterOfMass();
|
||||
|
||||
default boolean isInvalid() {
|
||||
return this.getMass() <= 0.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param position the position to check the normal mass at, assumed to be in the plot
|
||||
* @param direction the direction to check the normal mass along, local to the plot
|
||||
* @return the normal mass, or effective mass at the plot position and direction
|
||||
*/
|
||||
default double getInverseNormalMass(final Vector3dc position, final Vector3dc direction) {
|
||||
final Vector3d comLocalPos = position.sub(this.getCenterOfMass(), new Vector3d());
|
||||
final Vector3d normalizedDirection = direction.normalize(new Vector3d());
|
||||
final Vector3d cross = comLocalPos.cross(normalizedDirection, new Vector3d());
|
||||
|
||||
return cross.dot(this.getInverseInertiaTensor().transform(cross, new Vector3d()))
|
||||
+ this.getInverseMass();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
package dev.ryanhcode.sable.api.physics.mass;
|
||||
|
||||
import dev.ryanhcode.sable.api.block.BlockSubLevelCustomCenterOfMass;
|
||||
import dev.ryanhcode.sable.companion.math.BoundingBox3ic;
|
||||
import dev.ryanhcode.sable.companion.math.JOMLConversion;
|
||||
import dev.ryanhcode.sable.physics.chunk.VoxelNeighborhoodState;
|
||||
import dev.ryanhcode.sable.physics.config.block_properties.PhysicsBlockPropertyHelper;
|
||||
import dev.ryanhcode.sable.util.SableMathUtils;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.BlockGetter;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import net.minecraft.world.phys.shapes.VoxelShape;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Matrix3d;
|
||||
import org.joml.Matrix3dc;
|
||||
import org.joml.Vector3d;
|
||||
import org.joml.Vector3dc;
|
||||
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
/**
|
||||
* Tracks the mass / inertia tensor of a structure
|
||||
*/
|
||||
public class MassTracker implements MassData {
|
||||
private static final AABB UNIT_BOUNDS = new AABB(0, 0, 0, 1, 1, 1);
|
||||
|
||||
/**
|
||||
* The memoized internal center of masses for block-states
|
||||
*/
|
||||
public static BiFunction<BlockGetter, BlockState, Vector3dc> BLOCK_CENTER_OF_MASS = new BiFunction<>() {
|
||||
private final Int2ObjectOpenHashMap<Vector3dc> cache = new Int2ObjectOpenHashMap<>();
|
||||
|
||||
@Override
|
||||
public Vector3dc apply(final BlockGetter blockGetter, final BlockState state) {
|
||||
return this.cache.computeIfAbsent(state.hashCode(), x -> {
|
||||
if (state.isAir()) {
|
||||
return JOMLConversion.HALF;
|
||||
}
|
||||
|
||||
if (state.getBlock() instanceof final BlockSubLevelCustomCenterOfMass customCenterOfMass) {
|
||||
return customCenterOfMass.getCenterOfMass(blockGetter, state);
|
||||
}
|
||||
|
||||
final VoxelShape shape = state.getCollisionShape(blockGetter, BlockPos.ZERO);
|
||||
|
||||
if (shape.isEmpty()) {
|
||||
return JOMLConversion.HALF;
|
||||
}
|
||||
|
||||
if (state.isCollisionShapeFullBlock(blockGetter, BlockPos.ZERO)) {
|
||||
return JOMLConversion.HALF;
|
||||
}
|
||||
|
||||
final AABB bounds = shape.bounds().intersect(UNIT_BOUNDS);
|
||||
return JOMLConversion.toJOML(bounds.getCenter());
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private static final Matrix3d BLOCK_INERTIA = new Matrix3d();
|
||||
/**
|
||||
* The mass of the sub-level [kpg]
|
||||
*/
|
||||
private double mass;
|
||||
/**
|
||||
* The inertia tensor of the sub-level [kgm^2]
|
||||
*/
|
||||
private Matrix3d inertiaTensor;
|
||||
/**
|
||||
* 1 / mass of the sub-level [1 / kpg]
|
||||
*/
|
||||
private double inverseMass;
|
||||
/**
|
||||
* 1 / inertia tensor of the sub-level [1 / kgm^2]
|
||||
*/
|
||||
private Matrix3d inverseInertiaTensor;
|
||||
/**
|
||||
* The center of mass of the sub-level [m]
|
||||
*/
|
||||
private @Nullable Vector3d centerOfMass;
|
||||
|
||||
/**
|
||||
* The data to track the mass of
|
||||
*/
|
||||
public MassTracker() {
|
||||
this.mass = 0.0;
|
||||
this.centerOfMass = null;
|
||||
this.inertiaTensor = new Matrix3d().zero();
|
||||
this.inverseInertiaTensor = new Matrix3d().zero();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new mass tracker for a sub-level.
|
||||
*/
|
||||
public static MassTracker build(final BlockGetter blockGetter, final BoundingBox3ic bounds) {
|
||||
double mass = 0.0;
|
||||
final Vector3d centerOfMass = new Vector3d();
|
||||
final Matrix3d inertiaTensor = new Matrix3d().zero();
|
||||
|
||||
final BlockPos.MutableBlockPos blockPos = new BlockPos.MutableBlockPos();
|
||||
final Vector3d blockCenter = new Vector3d();
|
||||
int blockCount = 0;
|
||||
|
||||
for (int x = bounds.minX(); x <= bounds.maxX(); x++) {
|
||||
for (int y = bounds.minY(); y <= bounds.maxY(); y++) {
|
||||
for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) {
|
||||
final BlockState state = blockGetter.getBlockState(blockPos.set(x, y, z));
|
||||
|
||||
if (!VoxelNeighborhoodState.isSolid(blockGetter, blockPos, state)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final double blockMass = PhysicsBlockPropertyHelper.getMass(blockGetter, blockPos, state);
|
||||
blockCenter.set(x, y, z)
|
||||
.add(BLOCK_CENTER_OF_MASS.apply(blockGetter, state));
|
||||
|
||||
mass += blockMass;
|
||||
centerOfMass.fma(blockMass, blockCenter);
|
||||
blockCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (blockCount == 0) {
|
||||
final MassTracker tracker = new MassTracker();
|
||||
tracker.mass = 0.0;
|
||||
tracker.centerOfMass = null;
|
||||
tracker.inertiaTensor = new Matrix3d().zero();
|
||||
tracker.inverseInertiaTensor = new Matrix3d().zero();
|
||||
return tracker;
|
||||
}
|
||||
|
||||
centerOfMass.div(mass);
|
||||
|
||||
for (int x = bounds.minX(); x <= bounds.maxX(); x++) {
|
||||
for (int y = bounds.minY(); y <= bounds.maxY(); y++) {
|
||||
for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) {
|
||||
final BlockState state = blockGetter.getBlockState(blockPos.set(x, y, z));
|
||||
|
||||
if (!VoxelNeighborhoodState.isSolid(blockGetter, blockPos, state)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
blockCenter.set(x, y, z)
|
||||
.add(BLOCK_CENTER_OF_MASS.apply(blockGetter, state));
|
||||
|
||||
final double blockMass = PhysicsBlockPropertyHelper.getMass(blockGetter, blockPos, state);
|
||||
final Vec3 blockInertia = PhysicsBlockPropertyHelper.getInertia(blockGetter, blockPos, state);
|
||||
final Vector3d r = blockCenter.sub(centerOfMass);
|
||||
|
||||
MassTracker.addBlockInertia(r, blockMass, inertiaTensor, blockInertia);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final Matrix3d inverseInertiaTensor = new Matrix3d(inertiaTensor).invert();
|
||||
final double inverseMass = 1.0 / mass;
|
||||
|
||||
final MassTracker tracker = new MassTracker();
|
||||
|
||||
tracker.centerOfMass = centerOfMass;
|
||||
tracker.mass = mass;
|
||||
tracker.inverseMass = inverseMass;
|
||||
tracker.inertiaTensor = inertiaTensor;
|
||||
tracker.inverseInertiaTensor = inverseInertiaTensor;
|
||||
|
||||
return tracker;
|
||||
}
|
||||
|
||||
private static Matrix3d addBlockInertia(final Vector3d blockPos, final double blockMass, final Matrix3d dest, final @Nullable Vec3 blockInertia) {
|
||||
if (blockInertia == null) {
|
||||
// block doesn't specify inertia, we assume it to be a cube
|
||||
BLOCK_INERTIA.identity().scale(blockMass / 6.0);
|
||||
} else {
|
||||
// block specifies inertia, we use it as diagonals
|
||||
BLOCK_INERTIA.identity();
|
||||
BLOCK_INERTIA.m00 = blockInertia.x * blockMass;
|
||||
BLOCK_INERTIA.m11 = blockInertia.y * blockMass;
|
||||
BLOCK_INERTIA.m22 = blockInertia.z * blockMass;
|
||||
}
|
||||
|
||||
dest.add(BLOCK_INERTIA);
|
||||
SableMathUtils.fmaInertiaTensor(blockPos, blockMass, dest);
|
||||
return dest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the mass of a 1x1x1 cube to the sub-level.
|
||||
* Negative mass is equivalent to the removal of a block.
|
||||
*
|
||||
* @param blockPos The position of the block
|
||||
* @param blockMass The mass of the block [kpg]
|
||||
*/
|
||||
public void addBlockMass(final BlockGetter blockGetter, final BlockState state, final BlockPos blockPos, final double blockMass, final @Nullable Vec3 blockInertia) {
|
||||
final double oldMass = this.mass;
|
||||
final double newMass = oldMass + blockMass;
|
||||
|
||||
final Vector3d blockCenter = new Vector3d(blockPos.getX(), blockPos.getY(), blockPos.getZ())
|
||||
.add(BLOCK_CENTER_OF_MASS.apply(blockGetter, state));
|
||||
|
||||
if (this.centerOfMass == null) {
|
||||
this.centerOfMass = new Vector3d(blockCenter);
|
||||
}
|
||||
|
||||
final Vector3d blockCenterFromCOM = new Vector3d(blockCenter).sub(this.centerOfMass);
|
||||
|
||||
addBlockInertia(blockCenterFromCOM, blockMass, this.inertiaTensor, blockInertia);
|
||||
this.mass = newMass;
|
||||
this.inverseMass = 1.0 / newMass;
|
||||
|
||||
this.moveCenterOfMass(new Vector3d(this.centerOfMass).mul(oldMass).add(blockCenter.mul(blockMass)).div(newMass));
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the center of mass to a new position.
|
||||
*
|
||||
* @param newCenterOfMass The new center of mass
|
||||
*/
|
||||
public void moveCenterOfMass(final Vector3d newCenterOfMass) {
|
||||
final Vector3d diff = new Vector3d(newCenterOfMass).sub(this.centerOfMass);
|
||||
final Matrix3d outerProduct = new Matrix3d(
|
||||
diff.x * diff.x,
|
||||
diff.y * diff.x,
|
||||
diff.z * diff.x,
|
||||
|
||||
diff.x * diff.y,
|
||||
diff.y * diff.y,
|
||||
diff.z * diff.y,
|
||||
|
||||
diff.x * diff.z,
|
||||
diff.y * diff.z,
|
||||
diff.z * diff.z
|
||||
);
|
||||
|
||||
final Matrix3d inertia = new Matrix3d().scale(diff.lengthSquared()).sub(outerProduct).scale(this.mass);
|
||||
|
||||
this.inertiaTensor.sub(inertia);
|
||||
this.inverseInertiaTensor = new Matrix3d(this.inertiaTensor).invert();
|
||||
this.centerOfMass.set(newCenterOfMass);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public double getInverseMass() {
|
||||
return this.inverseMass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Matrix3dc getInverseInertiaTensor() {
|
||||
return this.inverseInertiaTensor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Matrix3dc getInertiaTensor() {
|
||||
return this.inertiaTensor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getMass() {
|
||||
return this.mass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vector3dc getCenterOfMass() {
|
||||
return this.centerOfMass;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package dev.ryanhcode.sable.api.physics.mass;
|
||||
|
||||
import dev.ryanhcode.sable.companion.math.Pose3d;
|
||||
import dev.ryanhcode.sable.api.sublevel.KinematicContraption;
|
||||
import dev.ryanhcode.sable.api.sublevel.ServerSubLevelContainer;
|
||||
import dev.ryanhcode.sable.api.sublevel.SubLevelContainer;
|
||||
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.system.SubLevelPhysicsSystem;
|
||||
import dev.ryanhcode.sable.util.SableMathUtils;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.*;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
|
||||
public class MergedMassTracker implements MassData {
|
||||
private final MassTracker selfTracker;
|
||||
/**
|
||||
* The sub-level to track the merged mass of
|
||||
*/
|
||||
private final ServerSubLevel subLevel;
|
||||
/**
|
||||
* The merged mass of the sub-level, including contraptions [kpg]
|
||||
*/
|
||||
private double mass;
|
||||
/**
|
||||
* The merged inertia tensor of the sub-level, including contraptions [kgm^2]
|
||||
*/
|
||||
private final Matrix3d inertiaTensor = new Matrix3d().zero();
|
||||
/**
|
||||
* 1 / merged mass of the sub-level, including contraptions [1 / kpg]
|
||||
*/
|
||||
private double inverseMass;
|
||||
/**
|
||||
* 1 / merged inertia tensor of the sub-level, including contraptions [1 / kgm^2]
|
||||
*/
|
||||
private final Matrix3d inverseInertiaTensor = new Matrix3d().zero();
|
||||
/**
|
||||
* The merged center of mass of the sub-level, including contraptions [m]
|
||||
*/
|
||||
private @Nullable Vector3d centerOfMass;
|
||||
|
||||
private double lastMass;
|
||||
private @Nullable Vector3d lastCenterOfMass;
|
||||
private @Nullable Matrix3d lastInertiaTensor;
|
||||
|
||||
public MergedMassTracker(@NotNull final ServerSubLevel subLevel, final MassTracker selfTracker) {
|
||||
this.subLevel = subLevel;
|
||||
this.selfTracker = selfTracker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the merged mass properties of this sub-level, and merges the contraption mass trackers into this one
|
||||
*/
|
||||
public void update(final float partialPhysicsTick) {
|
||||
if (this.selfTracker.getCenterOfMass() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Collection<KinematicContraption> contraptions = this.subLevel.getPlot().getContraptions();
|
||||
|
||||
this.mass = this.selfTracker.getMass();
|
||||
this.centerOfMass = this.selfTracker.getCenterOfMass().mul(this.getMass(), new Vector3d());
|
||||
|
||||
for (final KinematicContraption contraption : contraptions) {
|
||||
final MassTracker contraptionMassData = contraption.sable$getMassTracker();
|
||||
this.mass = this.getMass() + contraptionMassData.getMass();
|
||||
this.centerOfMass.fma(contraptionMassData.getMass(), contraption.sable$getPosition(partialPhysicsTick));
|
||||
}
|
||||
|
||||
this.centerOfMass.mul(1 / this.getMass());
|
||||
|
||||
this.inertiaTensor.set(this.selfTracker.getInertiaTensor());
|
||||
final Vector3d localShift = this.centerOfMass.sub(this.selfTracker.getCenterOfMass(), new Vector3d());
|
||||
|
||||
// nudge inertia tensor
|
||||
SableMathUtils.fmaInertiaTensor(localShift, this.selfTracker.getMass(), this.inertiaTensor);
|
||||
|
||||
for (final KinematicContraption contraption : contraptions) {
|
||||
final MassTracker contraptionMassData = contraption.sable$getMassTracker();
|
||||
|
||||
final Vector3d localPos = contraption.sable$getPosition(partialPhysicsTick).sub(this.centerOfMass, new Vector3d());
|
||||
SableMathUtils.fmaInertiaTensor(localPos, contraptionMassData.getMass(), this.inertiaTensor);
|
||||
|
||||
final Quaterniond contraptionOrientation = contraption.sable$getOrientation(partialPhysicsTick);
|
||||
|
||||
// Q * (I * (Q^-1 * v))
|
||||
final Matrix3d localInertiaTensor = new Matrix3d()
|
||||
.rotateLocal(contraptionOrientation.conjugate(new Quaterniond()))
|
||||
.mulLocal(contraptionMassData.getInertiaTensor())
|
||||
.rotateLocal(contraptionOrientation);
|
||||
|
||||
this.inertiaTensor.add(localInertiaTensor);
|
||||
}
|
||||
|
||||
this.inverseMass = 1.0 / this.mass;
|
||||
this.inertiaTensor.invert(this.inverseInertiaTensor);
|
||||
|
||||
this.uploadData();
|
||||
this.setPreviousValues();
|
||||
}
|
||||
|
||||
private void uploadData() {
|
||||
if (this.centerOfMass != null && (this.mass != this.lastMass ||
|
||||
!Objects.equals(this.lastCenterOfMass, this.centerOfMass) ||
|
||||
!Objects.equals(this.lastInertiaTensor, this.inertiaTensor))) {
|
||||
if (this.lastCenterOfMass == null || this.lastInertiaTensor == null) {
|
||||
this.lastCenterOfMass = new Vector3d(this.centerOfMass);
|
||||
this.lastInertiaTensor = new Matrix3d(this.inertiaTensor);
|
||||
}
|
||||
|
||||
final ServerLevel level = this.subLevel.getLevel();
|
||||
final ServerSubLevelContainer container = SubLevelContainer.getContainer(level);
|
||||
final SubLevelPhysicsSystem physicsSystem = container.physicsSystem();
|
||||
|
||||
final Vector3d movement = this.centerOfMass.sub(this.lastCenterOfMass, new Vector3d());
|
||||
|
||||
physicsSystem.updatePose(this.subLevel);
|
||||
final Pose3d pose = this.subLevel.logicalPose();
|
||||
physicsSystem.getPipeline().teleport(this.subLevel, pose.position().add(pose.orientation().transform(movement)), pose.orientation());
|
||||
pose.rotationPoint().set(this.centerOfMass);
|
||||
physicsSystem.getPipeline().onStatsChanged(this.subLevel);
|
||||
}
|
||||
}
|
||||
|
||||
private void setPreviousValues() {
|
||||
if (this.centerOfMass == null) {
|
||||
this.lastCenterOfMass = null;
|
||||
this.lastInertiaTensor = null;
|
||||
} else {
|
||||
if (this.lastCenterOfMass == null) {
|
||||
this.lastCenterOfMass = new Vector3d();
|
||||
this.lastInertiaTensor = new Matrix3d().zero();
|
||||
}
|
||||
this.lastCenterOfMass.set(this.centerOfMass);
|
||||
this.lastInertiaTensor.set(this.inertiaTensor);
|
||||
}
|
||||
|
||||
this.lastMass = this.mass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getInverseMass() {
|
||||
return this.inverseMass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Matrix3dc getInverseInertiaTensor() {
|
||||
return this.inverseInertiaTensor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Matrix3dc getInertiaTensor() {
|
||||
return this.inertiaTensor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getMass() {
|
||||
return this.mass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vector3dc getCenterOfMass() {
|
||||
return this.centerOfMass;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the mass tracker for just the sub-level, not including merged masses
|
||||
*/
|
||||
public MassTracker getSelfMassTracker() {
|
||||
return this.selfTracker;
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package dev.ryanhcode.sable.api.physics.object;
|
||||
|
||||
import dev.ryanhcode.sable.companion.math.BoundingBox3d;
|
||||
import dev.ryanhcode.sable.sublevel.storage.holding.SubLevelHoldingChunkMap;
|
||||
import dev.ryanhcode.sable.sublevel.system.SubLevelPhysicsSystem;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
|
||||
/**
|
||||
* An arbitrary physics object in a {@link dev.ryanhcode.sable.sublevel.system.SubLevelPhysicsSystem}
|
||||
*/
|
||||
public interface ArbitraryPhysicsObject {
|
||||
|
||||
/**
|
||||
* Gathers the bounding box that the arbitrary physics object requires to be loaded.
|
||||
* Chunk sections intersecting this bounding box will be synced to the physics engine.
|
||||
*
|
||||
* @param dest the destination the bounding box should be written into
|
||||
*/
|
||||
void getBoundingBox(final BoundingBox3d dest);
|
||||
|
||||
/**
|
||||
* Called upon the physics object entering unloaded chunks
|
||||
*/
|
||||
void onUnloaded(SubLevelHoldingChunkMap holdingChunkMap, ChunkPos chunkPos);
|
||||
|
||||
/**
|
||||
* Called upon the physics object being removed from the system through means other than unloading
|
||||
*/
|
||||
void onRemoved();
|
||||
|
||||
/**
|
||||
* Called upon the physics object being added to the world
|
||||
*/
|
||||
void onAddition(final SubLevelPhysicsSystem physicsSystem);
|
||||
|
||||
/**
|
||||
* Called to wake up the physics object when nearby blocks were modified
|
||||
*/
|
||||
void wakeUp();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package dev.ryanhcode.sable.api.physics.object.box;
|
||||
|
||||
import dev.ryanhcode.sable.companion.math.Pose3d;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
/**
|
||||
* A handle to an active box in the physics engine.
|
||||
*
|
||||
* @see BoxPhysicsObject
|
||||
*/
|
||||
public interface BoxHandle {
|
||||
|
||||
/**
|
||||
* Queries the pose of the box from the physics engine
|
||||
*/
|
||||
@ApiStatus.OverrideOnly
|
||||
void readPose(Pose3d dest);
|
||||
|
||||
/**
|
||||
* Removes the box from the physics pipeline
|
||||
*/
|
||||
void remove();
|
||||
|
||||
/**
|
||||
* Wakes up the box
|
||||
*/
|
||||
void wakeUp();
|
||||
|
||||
/**
|
||||
* @return the runtime ID of the box
|
||||
*/
|
||||
int getRuntimeId();
|
||||
}
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
package dev.ryanhcode.sable.api.physics.object.box;
|
||||
|
||||
import dev.ryanhcode.sable.api.physics.PhysicsPipelineBody;
|
||||
import dev.ryanhcode.sable.api.physics.mass.MassData;
|
||||
import dev.ryanhcode.sable.api.physics.object.ArbitraryPhysicsObject;
|
||||
import dev.ryanhcode.sable.companion.math.BoundingBox3d;
|
||||
import dev.ryanhcode.sable.companion.math.JOMLConversion;
|
||||
import dev.ryanhcode.sable.companion.math.Pose3d;
|
||||
import dev.ryanhcode.sable.companion.math.Pose3dc;
|
||||
import dev.ryanhcode.sable.sublevel.storage.holding.SubLevelHoldingChunkMap;
|
||||
import dev.ryanhcode.sable.sublevel.system.SubLevelPhysicsSystem;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Matrix3d;
|
||||
import org.joml.Matrix3dc;
|
||||
import org.joml.Vector3d;
|
||||
import org.joml.Vector3dc;
|
||||
|
||||
/**
|
||||
* A box cuboid physics object. Some may say.
|
||||
*/
|
||||
public class BoxPhysicsObject implements ArbitraryPhysicsObject, PhysicsPipelineBody {
|
||||
|
||||
protected BoxHandle handle;
|
||||
private final Pose3d pose = new Pose3d();
|
||||
private final Vector3d halfExtents = new Vector3d();
|
||||
private final double mass;
|
||||
private boolean active = false;
|
||||
|
||||
/**
|
||||
* Constructs a box physics object
|
||||
* @param pose the pose where the rotation point and scale are ignored
|
||||
* @param halfExtents the half-extents of the box
|
||||
* @param mass the mass of the box
|
||||
*/
|
||||
public BoxPhysicsObject(final Pose3dc pose, final Vector3dc halfExtents, final double mass) {
|
||||
this.pose.set(pose);
|
||||
this.halfExtents.set(halfExtents);
|
||||
this.mass = mass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gathers the bounding box that the arbitrary physics object requires to be loaded.
|
||||
* Chunk sections intersecting this bounding box will be synced to the physics engine.
|
||||
*
|
||||
* @param dest the destination the bounding box should be written into
|
||||
*/
|
||||
@Override
|
||||
public void getBoundingBox(final BoundingBox3d dest) {
|
||||
final double max = this.halfExtents.get(this.halfExtents.maxComponent());
|
||||
|
||||
final Vector3d center = this.pose.position();
|
||||
dest.set(center.x, center.y, center.z, center.x, center.y, center.z);
|
||||
dest.expand(max * 1.7321);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the pose of the box
|
||||
*/
|
||||
public void updatePose() {
|
||||
this.handle.readPose(this.pose);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called upon the physics object entering unloaded chunks
|
||||
*/
|
||||
@Override
|
||||
public void onUnloaded(final SubLevelHoldingChunkMap holdingChunkMap, final ChunkPos chunkPos) {
|
||||
this.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called upon the physics object being removed from the system through means other than unloading
|
||||
*/
|
||||
@Override
|
||||
public void onRemoved() {
|
||||
this.remove();
|
||||
}
|
||||
|
||||
protected void remove() {
|
||||
this.active = false;
|
||||
this.handle.remove();
|
||||
this.handle = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called upon the physics object being added to the world
|
||||
*/
|
||||
@Override
|
||||
public void onAddition(final SubLevelPhysicsSystem physicsSystem) {
|
||||
this.active = true;
|
||||
this.handle = physicsSystem.getPipeline().addBox(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to wake up the physics object when nearby blocks were modified
|
||||
*/
|
||||
@Override
|
||||
public void wakeUp() {
|
||||
this.handle.wakeUp();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the last updated pose
|
||||
*/
|
||||
public Pose3dc getPose() {
|
||||
return this.pose;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the half extents of the cube
|
||||
*/
|
||||
public Vector3dc getHalfExtents() {
|
||||
return this.halfExtents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the mass of the cube
|
||||
*/
|
||||
public double getMass() {
|
||||
return this.mass;
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRuntimeId() {
|
||||
if (this.handle == null) {
|
||||
return PhysicsPipelineBody.NULL_RUNTIME_ID;
|
||||
}
|
||||
return this.handle.getRuntimeId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MassData getMassTracker() {
|
||||
return new BoxMassData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRemoved() {
|
||||
return !this.active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mass data for a box physics object
|
||||
*/
|
||||
private class BoxMassData implements MassData {
|
||||
private final Matrix3dc inertia = new Matrix3d().scale(BoxPhysicsObject.this.mass / 6.0);
|
||||
private final Matrix3dc inverseInertia = this.inertia.invert(new Matrix3d());
|
||||
|
||||
@Override
|
||||
public double getMass() {
|
||||
return BoxPhysicsObject.this.mass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getInverseMass() {
|
||||
return 1.0 / BoxPhysicsObject.this.mass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Matrix3dc getInertiaTensor() {
|
||||
return this.inertia;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Matrix3dc getInverseInertiaTensor() {
|
||||
return this.inverseInertia;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Vector3dc getCenterOfMass() {
|
||||
return JOMLConversion.ZERO;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package dev.ryanhcode.sable.api.physics.object.rope;
|
||||
|
||||
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.joml.Vector3d;
|
||||
import org.joml.Vector3dc;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A handle to an active rope in the physics engine.
|
||||
*
|
||||
* @see RopePhysicsObject
|
||||
*/
|
||||
public interface RopeHandle {
|
||||
|
||||
/**
|
||||
* Queries the points of the rope from the physics engine
|
||||
*/
|
||||
@ApiStatus.OverrideOnly
|
||||
void readPose(List<Vector3d> dest);
|
||||
|
||||
/**
|
||||
* Removes the rope from the physics pipeline
|
||||
*/
|
||||
void remove();
|
||||
|
||||
/**
|
||||
* Sets the extension constraint length of the first segment
|
||||
*/
|
||||
void setFirstSegmentLength(double length);
|
||||
|
||||
/**
|
||||
* Removes the point at the beginning of the rope
|
||||
*/
|
||||
void removeFirstPoint();
|
||||
|
||||
/**
|
||||
* Adds a point to the beginning of the rope
|
||||
*/
|
||||
void addPoint(final Vector3dc position);
|
||||
|
||||
/**
|
||||
* Sets an attachment
|
||||
*/
|
||||
void setAttachment(final AttachmentPoint attachmentPoint, final Vector3dc location, final ServerSubLevel subLevel);
|
||||
|
||||
/**
|
||||
* Wakes up the rope
|
||||
*/
|
||||
void wakeUp();
|
||||
|
||||
/**
|
||||
* Rope attachment points
|
||||
*/
|
||||
enum AttachmentPoint {
|
||||
START,
|
||||
END
|
||||
}
|
||||
|
||||
}
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
package dev.ryanhcode.sable.api.physics.object.rope;
|
||||
|
||||
import dev.ryanhcode.sable.companion.math.BoundingBox3d;
|
||||
import dev.ryanhcode.sable.api.physics.object.ArbitraryPhysicsObject;
|
||||
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.storage.holding.SubLevelHoldingChunkMap;
|
||||
import dev.ryanhcode.sable.sublevel.system.SubLevelPhysicsSystem;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectList;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectLists;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import org.joml.Vector3d;
|
||||
import org.joml.Vector3dc;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* A rope made of points. Some may say.
|
||||
*/
|
||||
public class RopePhysicsObject implements ArbitraryPhysicsObject {
|
||||
protected final ObjectList<Vector3d> points;
|
||||
protected final ObjectList<Vector3d> pointsView;
|
||||
protected final double collisionRadius;
|
||||
protected boolean active;
|
||||
protected RopeHandle handle;
|
||||
|
||||
protected Vector3dc startAttachmentLocation = null;
|
||||
protected ServerSubLevel startAttachmentSubLevel = null;
|
||||
|
||||
public RopePhysicsObject(final Collection<Vector3d> points, final double collisionRadius) {
|
||||
this.points = new ObjectArrayList<>(points);
|
||||
this.pointsView = ObjectLists.unmodifiable(this.points);
|
||||
this.collisionRadius = collisionRadius;
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gathers the bounding box that the arbitrary physics object requires to be loaded.
|
||||
* Chunk sections intersecting this bounding box will be synced to the physics engine.
|
||||
*
|
||||
* @param dest the destination the bounding box should be written into
|
||||
*/
|
||||
@Override
|
||||
public void getBoundingBox(final BoundingBox3d dest) {
|
||||
final Vector3d first = this.points.getFirst();
|
||||
dest.set(first.x, first.y, first.z, first.x, first.y, first.z);
|
||||
for (final Vector3d point : this.points) {
|
||||
dest.expandTo(point.x - this.collisionRadius, point.y - this.collisionRadius, point.z - this.collisionRadius);
|
||||
dest.expandTo(point.x + this.collisionRadius, point.y + this.collisionRadius, point.z + this.collisionRadius);
|
||||
}
|
||||
}
|
||||
|
||||
public double getCollisionRadius() {
|
||||
return this.collisionRadius;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A view of all points
|
||||
*/
|
||||
public ObjectList<Vector3d> getPoints() {
|
||||
return this.pointsView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the points of the rope
|
||||
*/
|
||||
public void updatePose() {
|
||||
this.handle.readPose(this.points);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the extension constraint length of the first segment
|
||||
*/
|
||||
public void setFirstSegmentLength(final double length) {
|
||||
this.handle.setFirstSegmentLength(length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the point at the beginning of the rope
|
||||
*/
|
||||
public void removeFirstPoint() {
|
||||
this.points.removeFirst();
|
||||
|
||||
if (this.isActive()) {
|
||||
this.handle.removeFirstPoint();
|
||||
}
|
||||
|
||||
if (this.startAttachmentLocation != null) {
|
||||
this.setAttachment(RopeHandle.AttachmentPoint.START, this.startAttachmentLocation, this.startAttachmentSubLevel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a point to the beginning of the rope
|
||||
*/
|
||||
public void addPoint(final Vector3dc position) {
|
||||
this.points.addFirst(new Vector3d(position));
|
||||
|
||||
if (this.isActive()) {
|
||||
this.handle.addPoint(position);
|
||||
}
|
||||
|
||||
if (this.startAttachmentLocation != null) {
|
||||
this.setAttachment(RopeHandle.AttachmentPoint.START, this.startAttachmentLocation, this.startAttachmentSubLevel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an attachment
|
||||
*/
|
||||
public void setAttachment(final RopeHandle.AttachmentPoint attachmentPoint, final Vector3dc location, final ServerSubLevel subLevel) {
|
||||
if (attachmentPoint == RopeHandle.AttachmentPoint.START) {
|
||||
this.startAttachmentSubLevel = subLevel;
|
||||
this.startAttachmentLocation = new Vector3d(location);
|
||||
}
|
||||
|
||||
if (this.isActive()) {
|
||||
this.handle.setAttachment(attachmentPoint, location, subLevel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called upon the physics object entering unloaded chnks
|
||||
*/
|
||||
@Override
|
||||
public void onUnloaded(final SubLevelHoldingChunkMap holdingChunkMap, final ChunkPos chunkPos) {
|
||||
this.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called upon the physics object being removed from the system through means other than unloading
|
||||
*/
|
||||
@Override
|
||||
public void onRemoved() {
|
||||
this.remove();
|
||||
}
|
||||
|
||||
protected void remove() {
|
||||
this.active = false;
|
||||
this.handle.remove();
|
||||
this.handle = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called upon the physics object being added to the world
|
||||
*/
|
||||
@Override
|
||||
public void onAddition(final SubLevelPhysicsSystem physicsSystem) {
|
||||
this.active = true;
|
||||
this.handle = physicsSystem.getPipeline().addRope(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to wake up the physics object when nearby blocks were modified
|
||||
*/
|
||||
@Override
|
||||
public void wakeUp() {
|
||||
if (this.isActive()) {
|
||||
this.handle.wakeUp();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return this.active;
|
||||
}
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
package dev.ryanhcode.sable.api.schematic;
|
||||
|
||||
import dev.ryanhcode.sable.companion.math.BoundingBox3i;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import io.netty.util.concurrent.FastThreadLocal;
|
||||
import it.unimi.dsi.fastutil.Function;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Quaterniondc;
|
||||
import org.joml.Vector3dc;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* A global context for sub-levels being serialized/unserialized to/from schematics.
|
||||
* Block-entities and pieces of content that rely on sub-level dependencies are encouraged
|
||||
* to serialize differently using this context when being saved to schematics.
|
||||
*/
|
||||
public class SubLevelSchematicSerializationContext {
|
||||
private static final FastThreadLocal<SubLevelSchematicSerializationContext> THREAD_LOCAL = new FastThreadLocal<>();
|
||||
private final Map<UUID, SchematicMapping> mappings = new Object2ObjectOpenHashMap<>();
|
||||
private Function<BlockPos, BlockPos> placeTransform;
|
||||
private Function<BlockPos, BlockPos> setupTransform;
|
||||
private final Type type;
|
||||
private final BoundingBox3i boundingBox;
|
||||
|
||||
public SubLevelSchematicSerializationContext(final Type type, final BoundingBox3i boundingBox) {
|
||||
this.type = type;
|
||||
this.boundingBox = boundingBox;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
public BoundingBox3i getBoundingBox() {
|
||||
return this.boundingBox;
|
||||
}
|
||||
|
||||
public static SubLevelSchematicSerializationContext getCurrentContext() {
|
||||
return THREAD_LOCAL.get();
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
public static void setCurrentContext(@Nullable final SubLevelSchematicSerializationContext context) {
|
||||
THREAD_LOCAL.set(context);
|
||||
}
|
||||
|
||||
public Function<BlockPos, BlockPos> getPlaceTransform() {
|
||||
return this.placeTransform;
|
||||
}
|
||||
|
||||
public Function<BlockPos, BlockPos> getSetupTransform() {
|
||||
return this.setupTransform;
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
public void setPlaceTransform(final Function<BlockPos, BlockPos> transform) {
|
||||
this.placeTransform = transform;
|
||||
}
|
||||
|
||||
|
||||
@ApiStatus.Internal
|
||||
public void setSetupTransform(final Function<BlockPos, BlockPos> transform) {
|
||||
this.setupTransform = transform;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public SchematicMapping getMapping(final SubLevel subLevel) {
|
||||
return this.mappings.get(subLevel.getUniqueId());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public SchematicMapping getMapping(final UUID uuid) {
|
||||
return this.mappings.get(uuid);
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
public Map<UUID, SchematicMapping> getMappings() {
|
||||
return this.mappings;
|
||||
}
|
||||
|
||||
public record SchematicMapping(Vector3dc newCorner, Quaterniondc newOrientation, UUID newUUID,
|
||||
Function<BlockPos, BlockPos> transform) {
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
PLACE,
|
||||
SAVE
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package dev.ryanhcode.sable.api.sublevel;
|
||||
|
||||
|
||||
import dev.ryanhcode.sable.companion.math.Pose3d;
|
||||
import dev.ryanhcode.sable.network.client.ClientSableInterpolationState;
|
||||
import dev.ryanhcode.sable.sublevel.ClientSubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.world.level.Level;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
import java.util.BitSet;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Holds all sub-levels and plots in a {@link ClientLevel}
|
||||
*/
|
||||
public class ClientSubLevelContainer extends SubLevelContainer {
|
||||
private final ClientSableInterpolationState interpolation = new ClientSableInterpolationState();
|
||||
|
||||
/**
|
||||
* Temp lighting scene IDs for flywheel
|
||||
*/
|
||||
private final BitSet lightingSceneIds;
|
||||
|
||||
/**
|
||||
* Creates a new sub-level container with the given side length and plot size.
|
||||
*
|
||||
* @param level the level of the plotgrid
|
||||
* @param logSideLength the log_2 of the amount of chunks in the side of the plotgrid
|
||||
* @param logPlotSize the log_2 of the amount of chunks in the side of a plot
|
||||
* @param originX the X coordinate in plots of the origin of the plotgrid
|
||||
* @param originZ the Z coordinate in plots of the origin of the plotgrid
|
||||
*/
|
||||
public ClientSubLevelContainer(final Level level, final int logSideLength, final int logPlotSize, final int originX, final int originZ) {
|
||||
super(level, logSideLength, logPlotSize, originX, originZ);
|
||||
this.lightingSceneIds = new BitSet(this.subLevels.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SubLevel createSubLevel(final int globalPlotX, final int globalPlotZ, final Pose3d pose, final UUID uuid) {
|
||||
final ClientSubLevel subLevel = new ClientSubLevel(this.getLevel(), globalPlotX, globalPlotZ, pose);
|
||||
subLevel.setUniqueId(uuid);
|
||||
return subLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called every tick for the plotgrid.
|
||||
*/
|
||||
@Override
|
||||
public void tick() {
|
||||
this.interpolation.tick();
|
||||
super.tick();
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
public void addDebugInfo(final Consumer<String> consumer) {
|
||||
consumer.accept("Sub-Levels: " + this.getAllSubLevels().size());
|
||||
this.interpolation.addDebugInfo(consumer);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public List<ClientSubLevel> getAllSubLevels() {
|
||||
return (List<ClientSubLevel>) super.getAllSubLevels();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the level of the plotgrid.
|
||||
*/
|
||||
@Override
|
||||
public ClientLevel getLevel() {
|
||||
return (ClientLevel) super.getLevel();
|
||||
}
|
||||
|
||||
public ClientSableInterpolationState getInterpolation() {
|
||||
return this.interpolation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the lighting scene ID for a sub-level.
|
||||
*/
|
||||
public int getLightingSceneId(final ClientSubLevel subLevel) {
|
||||
synchronized (this.lightingSceneIds) {
|
||||
if (subLevel.getLightingSceneId() >= 0) {
|
||||
return subLevel.getLightingSceneId();
|
||||
}
|
||||
|
||||
for (int i = 0; i < this.lightingSceneIds.size(); i++) {
|
||||
if (!this.lightingSceneIds.get(i)) {
|
||||
this.lightingSceneIds.set(i);
|
||||
subLevel.setLightingSceneId(i + 1);
|
||||
return subLevel.getLightingSceneId();
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalStateException("Out of lighting scene ids, uh oh!");
|
||||
}
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
public void freeLightingScene(final int lightingSceneId) {
|
||||
this.lightingSceneIds.clear(lightingSceneId - 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package dev.ryanhcode.sable.api.sublevel;
|
||||
|
||||
import dev.ryanhcode.sable.api.block.BlockSubLevelLiftProvider;
|
||||
import dev.ryanhcode.sable.api.physics.mass.MassTracker;
|
||||
import dev.ryanhcode.sable.companion.math.BoundingBox3i;
|
||||
import dev.ryanhcode.sable.companion.math.JOMLConversion;
|
||||
import dev.ryanhcode.sable.companion.math.Pose3d;
|
||||
import dev.ryanhcode.sable.physics.floating_block.FloatingClusterContainer;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.BlockGetter;
|
||||
import org.joml.Quaterniond;
|
||||
import org.joml.Vector3dc;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface KinematicContraption {
|
||||
|
||||
void sable$getLocalBounds(final BoundingBox3i bounds);
|
||||
BlockGetter sable$blockGetter();
|
||||
MassTracker sable$getMassTracker();
|
||||
Vector3dc sable$getPosition(double partialTick);
|
||||
Quaterniond sable$getOrientation(double partialTick);
|
||||
Map<BlockPos, BlockSubLevelLiftProvider.LiftProviderContext> sable$liftProviders();
|
||||
FloatingClusterContainer sable$getFloatingClusterContainer();
|
||||
|
||||
boolean sable$shouldCollide();
|
||||
|
||||
boolean sable$isValid();
|
||||
|
||||
default Vector3dc sable$getPosition() {
|
||||
return this.sable$getPosition(1.0f);
|
||||
}
|
||||
|
||||
default Quaterniond sable$getOrientation() {
|
||||
return this.sable$getOrientation(1.0f);
|
||||
}
|
||||
|
||||
default Pose3d sable$getLocalPose(final Pose3d dest, final double partialTick) {
|
||||
dest.rotationPoint().set(this.sable$getMassTracker().getCenterOfMass());
|
||||
dest.position().set(this.sable$getPosition(partialTick));
|
||||
dest.orientation().set(this.sable$getOrientation(partialTick));
|
||||
dest.scale().set(JOMLConversion.ONE);
|
||||
return dest;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package dev.ryanhcode.sable.api.sublevel;
|
||||
|
||||
|
||||
import dev.ryanhcode.sable.Sable;
|
||||
import dev.ryanhcode.sable.companion.math.Pose3d;
|
||||
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.storage.SubLevelOccupancySavedData;
|
||||
import dev.ryanhcode.sable.sublevel.storage.SubLevelRemovalReason;
|
||||
import dev.ryanhcode.sable.sublevel.storage.holding.SubLevelHoldingChunkMap;
|
||||
import dev.ryanhcode.sable.sublevel.system.SubLevelPhysicsSystem;
|
||||
import dev.ryanhcode.sable.sublevel.system.SubLevelTrackingSystem;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Vector3d;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Holds all sub-levels and plots in a {@link ServerLevel}
|
||||
*/
|
||||
public class ServerSubLevelContainer extends SubLevelContainer {
|
||||
|
||||
/**
|
||||
* The physics system in this container
|
||||
*/
|
||||
private @Nullable SubLevelPhysicsSystem physics;
|
||||
|
||||
/**
|
||||
* The tracking system in this container
|
||||
*/
|
||||
private @Nullable SubLevelTrackingSystem tracking;
|
||||
|
||||
/**
|
||||
* The holding chunk map for this sub-level container.
|
||||
*/
|
||||
private SubLevelHoldingChunkMap holdingChunkMap;
|
||||
|
||||
/**
|
||||
* Creates a new sub-level container with the given side length and plot size.
|
||||
*
|
||||
* @param level the level of the plotgrid
|
||||
* @param logSideLength the log_2 of the amount of chunks in the side of the plotgrid
|
||||
* @param logPlotSize the log_2 of the amount of chunks in the side of a plot
|
||||
* @param originX the X coordinate in plots of the origin of the plotgrid
|
||||
* @param originZ the Z coordinate in plots of the origin of the plotgrid
|
||||
*/
|
||||
public ServerSubLevelContainer(final Level level, final int logSideLength, final int logPlotSize, final int originX, final int originZ) {
|
||||
super(level, logSideLength, logPlotSize, originX, originZ);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize after method construction is done
|
||||
*/
|
||||
public void initialize() {
|
||||
this.holdingChunkMap = new SubLevelHoldingChunkMap(this.getLevel(), this);
|
||||
this.holdingChunkMap.bootstrapAllHoldingChunks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called every tick for the plotgrid.
|
||||
*/
|
||||
@Override
|
||||
public void tick() {
|
||||
super.tick();
|
||||
this.holdingChunkMap.processChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the internal physics system.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
public void takePhysicsSystem(final SubLevelPhysicsSystem physics) {
|
||||
this.physics = physics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the internal tracking system.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
public void takeTrackingSystem(final SubLevelTrackingSystem tracking) {
|
||||
this.tracking = tracking;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the physics pipeline in this container
|
||||
*/
|
||||
public @NotNull SubLevelPhysicsSystem physicsSystem() {
|
||||
assert this.physics != null;
|
||||
return this.physics;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the physics pipeline in this container
|
||||
*/
|
||||
public @NotNull SubLevelTrackingSystem trackingSystem() {
|
||||
assert this.tracking != null;
|
||||
return this.tracking;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a sub-level with a local plot coordinate
|
||||
*/
|
||||
@Override
|
||||
public void removeSubLevel(final int x, final int z, final SubLevelRemovalReason reason) {
|
||||
final ServerSubLevel subLevel = (ServerSubLevel) this.getSubLevel(x, z);
|
||||
if (subLevel == null) {
|
||||
throw new IllegalStateException("No sub-level at " + x + ", " + z);
|
||||
}
|
||||
|
||||
if (reason == SubLevelRemovalReason.REMOVED) {
|
||||
subLevel.deleteAllEntities();
|
||||
}
|
||||
|
||||
super.removeSubLevel(x, z, reason);
|
||||
|
||||
if (reason == SubLevelRemovalReason.REMOVED) {
|
||||
final ServerLevel level = this.getLevel();
|
||||
SubLevelOccupancySavedData.getOrLoad(level).setDirty();
|
||||
this.holdingChunkMap.queueDeletion(subLevel);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SubLevel createSubLevel(final int globalPlotX, final int globalPlotZ, final Pose3d pose, final UUID uuid) {
|
||||
final ServerLevel level = this.getLevel();
|
||||
final ServerSubLevel subLevel = new ServerSubLevel(level, globalPlotX, globalPlotZ, pose);
|
||||
subLevel.setUniqueId(uuid);
|
||||
|
||||
final Vector3d position = pose.position();
|
||||
final BlockPos blockPos = BlockPos.containing(position.x, position.y, position.z);
|
||||
|
||||
if (level.isLoaded(blockPos)) {
|
||||
final Holder<Biome> holder = level.getBiome(blockPos);
|
||||
final Optional<ResourceKey<Biome>> key = holder.unwrapKey();
|
||||
|
||||
//noinspection OptionalIsPresent
|
||||
if (key.isPresent()) {
|
||||
subLevel.getPlot().setBiome(key.get());
|
||||
}
|
||||
}
|
||||
|
||||
return subLevel;
|
||||
}
|
||||
|
||||
public SubLevelHoldingChunkMap getHoldingChunkMap() {
|
||||
return this.holdingChunkMap;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public List<ServerSubLevel> getAllSubLevels() {
|
||||
return (List<ServerSubLevel>) super.getAllSubLevels();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the level of the plotgrid.
|
||||
*/
|
||||
@Override
|
||||
public ServerLevel getLevel() {
|
||||
return (ServerLevel) super.getLevel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees all native resources
|
||||
*/
|
||||
public void close() {
|
||||
try {
|
||||
this.holdingChunkMap.close();
|
||||
} catch (final Exception e) {
|
||||
Sable.LOGGER.error("Failed closing sub-level holding chunk map", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,531 @@
|
||||
package dev.ryanhcode.sable.api.sublevel;
|
||||
|
||||
import dev.ryanhcode.sable.Sable;
|
||||
import dev.ryanhcode.sable.companion.math.BoundingBox3dc;
|
||||
import dev.ryanhcode.sable.companion.math.Pose3d;
|
||||
import dev.ryanhcode.sable.mixinterface.plot.SubLevelContainerHolder;
|
||||
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.plot.LevelPlot;
|
||||
import dev.ryanhcode.sable.sublevel.plot.PlotChunkHolder;
|
||||
import dev.ryanhcode.sable.sublevel.storage.SubLevelOccupancySavedData;
|
||||
import dev.ryanhcode.sable.sublevel.storage.SubLevelRemovalReason;
|
||||
import dev.ryanhcode.sable.util.iterator.ListBackedFilterIterator;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectList;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.SectionPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Vector2i;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Holds all sub-levels and plots in a {@link Level}
|
||||
*/
|
||||
public abstract class SubLevelContainer {
|
||||
|
||||
public static int DEFAULT_LOG_SIZE_LENGTH = 7;
|
||||
public static int DEFAULT_LOG_PLOT_SIZE = 7;
|
||||
|
||||
/**
|
||||
* The origin of the plotyard in plots.
|
||||
* We want the plotyard to be over 30 million blocks out.
|
||||
*/
|
||||
public static final int DEFAULT_ORIGIN = 10000;//Mth.ceil(30_000_000.0 / (1 << DEFAULT_LOG_PLOT_SIZE));
|
||||
/**
|
||||
* The plotgrid storage for all loaded sub-levels
|
||||
*/
|
||||
protected final SubLevel[] subLevels;
|
||||
/**
|
||||
* All of the loaded sub-levels in the plotgrid
|
||||
*/
|
||||
private final List<SubLevel> allSubLevels = new ObjectArrayList<>();
|
||||
/**
|
||||
* All of the loaded sub-levels in the plotgrid, by uuid
|
||||
*/
|
||||
private final Map<UUID, SubLevel> subLevelsByUUID = new HashMap<>();
|
||||
/**
|
||||
* The occupancy of the plotgrid, including loaded and unloaded plots
|
||||
*/
|
||||
private final BitSet occupancy;
|
||||
/**
|
||||
* All observers/listeners for the plotgrid
|
||||
*/
|
||||
private final List<SubLevelObserver> observers = new ObjectArrayList<>();
|
||||
|
||||
/**
|
||||
* The level of the plotgrid
|
||||
*/
|
||||
private final Level level;
|
||||
/**
|
||||
* The log_2 of the side length of the plotgrid in plots
|
||||
*/
|
||||
private final int logSideLength;
|
||||
/**
|
||||
* The log_2 of the amount of chunks in the side of a plot
|
||||
*/
|
||||
private final int logPlotSize;
|
||||
|
||||
/**
|
||||
* The X origin of the plotgrid in plot coordinates
|
||||
*/
|
||||
private final int originX;
|
||||
/**
|
||||
* The Z origin of the plotgrid in plot coordinates
|
||||
*/
|
||||
private final int originZ;
|
||||
|
||||
/**
|
||||
* @param level the level
|
||||
* @return the plot container in a level
|
||||
*/
|
||||
public static @Nullable SubLevelContainer getContainer(final Level level) {
|
||||
if (level instanceof final SubLevelContainerHolder holder) {
|
||||
return holder.sable$getPlotContainer();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param level the level
|
||||
* @return the plot container in a level
|
||||
*/
|
||||
public static @Nullable ServerSubLevelContainer getContainer(final ServerLevel level) {
|
||||
if (level instanceof final SubLevelContainerHolder holder) {
|
||||
return (ServerSubLevelContainer) holder.sable$getPlotContainer();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param level the level
|
||||
* @return the plot container in a level
|
||||
*/
|
||||
public static @Nullable ClientSubLevelContainer getContainer(final ClientLevel level) {
|
||||
if (level instanceof final SubLevelContainerHolder holder) {
|
||||
return (ClientSubLevelContainer) holder.sable$getPlotContainer();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new sub-level container with the given side length and plot size.
|
||||
*
|
||||
* @param level the level of the plotgrid
|
||||
* @param logSideLength the log_2 of the amount of chunks in the side of the plotgrid
|
||||
* @param logPlotSize the log_2 of the amount of chunks in the side of a plot
|
||||
* @param originX the X coordinate in plots of the origin of the plotgrid
|
||||
* @param originZ the Z coordinate in plots of the origin of the plotgrid
|
||||
*/
|
||||
public SubLevelContainer(final Level level, final int logSideLength, final int logPlotSize, final int originX, final int originZ) {
|
||||
this.level = level;
|
||||
this.logSideLength = logSideLength;
|
||||
this.logPlotSize = logPlotSize;
|
||||
this.originX = originX;
|
||||
this.originZ = originZ;
|
||||
this.subLevels = new SubLevel[(1 << logSideLength) * (1 << logSideLength)];
|
||||
this.occupancy = new BitSet(this.subLevels.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called every tick for the plotgrid.
|
||||
*/
|
||||
public void tick() {
|
||||
this.allSubLevels.forEach(SubLevel::tick);
|
||||
this.processSubLevelRemovals();
|
||||
|
||||
this.observers.forEach(observer -> observer.tick(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes & follows through on queued sub-level removals
|
||||
*/
|
||||
public void processSubLevelRemovals() {
|
||||
for (final SubLevel subLevel : this.allSubLevels) {
|
||||
if (subLevel instanceof final ServerSubLevel serverSubLevel) {
|
||||
if (!serverSubLevel.isRemoved() && serverSubLevel.getMassTracker().isInvalid()) {
|
||||
serverSubLevel.getPlot().destroyAllBlocks();
|
||||
serverSubLevel.markRemoved();
|
||||
}
|
||||
}
|
||||
|
||||
if (subLevel.isRemoved()) {
|
||||
final LevelPlot plot = subLevel.getPlot();
|
||||
final ChunkPos plotPos = plot.plotPos;
|
||||
this.removeSubLevel(plotPos.x - this.originX, plotPos.z - this.originZ, SubLevelRemovalReason.REMOVED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an observer to the plotgrid.
|
||||
*/
|
||||
public void addObserver(final SubLevelObserver observer) {
|
||||
this.observers.add(observer);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the first empty plot coordinate in the grid, using occupancy data
|
||||
*/
|
||||
private Vector2i getFirstEmptyPlot() {
|
||||
for (int x = 0; x < (1 << this.logSideLength); x++) {
|
||||
for (int z = 0; z < (1 << this.logSideLength); z++) {
|
||||
if (!this.occupancy.get(this.getIndex(x, z))) {
|
||||
return new Vector2i(x, z);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the index of the plot at the given plot coordinates.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
public int getIndex(final int x, final int z) {
|
||||
return x + (z << this.logSideLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the plot at the given local plot coordinates.
|
||||
*/
|
||||
private @Nullable LevelPlot getLocalPlot(final int x, final int z) {
|
||||
if (x < 0 || x >= (1 << this.logSideLength) || z < 0 || z >= (1 << this.logSideLength)) {
|
||||
return null; // out of bounds
|
||||
}
|
||||
|
||||
final SubLevel subLevel = this.subLevels[this.getIndex(x, z)];
|
||||
|
||||
if (subLevel == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return subLevel.getPlot();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the sub-level at the given local plot coordinates.
|
||||
*/
|
||||
public @Nullable SubLevel getSubLevel(final int x, final int z) {
|
||||
if (x < 0 || x >= (1 << this.logSideLength) || z < 0 || z >= (1 << this.logSideLength)) {
|
||||
return null; // out of bounds
|
||||
}
|
||||
|
||||
return this.subLevels[this.getIndex(x, z)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocates a new plot at the given local plot coordinates.
|
||||
*
|
||||
* @return the allocated plot
|
||||
*/
|
||||
public SubLevel allocateNewSubLevel(final Pose3d pose) {
|
||||
final Vector2i firstEmptyPlot = this.getFirstEmptyPlot();
|
||||
|
||||
if (firstEmptyPlot == null) {
|
||||
throw new IllegalStateException("No empty plots left in the plotgrid");
|
||||
}
|
||||
|
||||
return this.allocateSubLevel(UUID.randomUUID(), firstEmptyPlot.x, firstEmptyPlot.y, pose);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocates a new plot at the given local plot coordinates.
|
||||
*
|
||||
* @return the allocated plot
|
||||
*/
|
||||
public SubLevel allocateSubLevel(final UUID uuid, final int x, final int z, final Pose3d pose) {
|
||||
if (this.getLocalPlot(x, z) != null) {
|
||||
throw new IllegalArgumentException("Plot already exists at " + x + ", " + z);
|
||||
}
|
||||
|
||||
if (x < 0 || x >= (1 << this.logSideLength) || z < 0 || z >= (1 << this.logSideLength)) {
|
||||
throw new IllegalArgumentException("Plot coordinates out of bounds: " + x + ", " + z);
|
||||
}
|
||||
|
||||
final SubLevel subLevel;
|
||||
|
||||
// Create a new sub-level based on the level type
|
||||
subLevel = this.createSubLevel(x + this.originX, z + this.originZ, pose, uuid);
|
||||
|
||||
final int index = this.getIndex(x, z);
|
||||
this.subLevels[index] = subLevel;
|
||||
this.getOccupancy().set(index);
|
||||
this.allSubLevels.add(subLevel);
|
||||
this.subLevelsByUUID.put(subLevel.getUniqueId(), subLevel);
|
||||
this.observers.forEach(observer -> observer.onSubLevelAdded(subLevel));
|
||||
|
||||
if (this.level instanceof final ServerLevel serverLevel) {
|
||||
SubLevelOccupancySavedData.getOrLoad(serverLevel).setDirty();
|
||||
}
|
||||
|
||||
return subLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new sub-level with the given global plot coordinates and pose.
|
||||
*
|
||||
* @param globalPlotX the global plot X coordinate
|
||||
* @param globalPlotZ the global plot Z coordinate
|
||||
* @param pose the initialization pose of the sub-level
|
||||
* @param uuid the unique ID of the sub-level
|
||||
* @return a new {@link SubLevel} instance
|
||||
*/
|
||||
protected abstract SubLevel createSubLevel(int globalPlotX, int globalPlotZ, Pose3d pose, UUID uuid);
|
||||
|
||||
/**
|
||||
* Gets a chunk from the plotgrid.
|
||||
*
|
||||
* @param pos the global chunk position
|
||||
*/
|
||||
public @Nullable LevelChunk getChunk(final ChunkPos pos) {
|
||||
if (!this.inBounds(pos)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final LevelPlot plot = this.getPlot(pos);
|
||||
if (plot == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final ChunkPos local = plot.toLocal(pos);
|
||||
return plot.getChunk(local);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a chunk holder from the plotgrid.
|
||||
*
|
||||
* @param pos the global chunk position
|
||||
*/
|
||||
public @Nullable PlotChunkHolder getChunkHolder(final ChunkPos pos) {
|
||||
if (!this.inBounds(pos)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final LevelPlot plot = this.getPlot(pos);
|
||||
if (plot == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final ChunkPos local = plot.toLocal(pos);
|
||||
return plot.getChunkHolder(local);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the plot at the given global chunk position.
|
||||
*
|
||||
* @param chunkX the global chunk X position
|
||||
* @param chunkZ the global chunk Z position
|
||||
*/
|
||||
public @Nullable LevelPlot getPlot(final int chunkX, final int chunkZ) {
|
||||
final int plotX = (chunkX >> this.logPlotSize) - this.originX;
|
||||
final int plotZ = (chunkZ >> this.logPlotSize) - this.originZ;
|
||||
|
||||
return this.getLocalPlot(plotX, plotZ);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the plot at the given global chunk position.
|
||||
*
|
||||
* @param pos the global chunk position
|
||||
*/
|
||||
public @Nullable LevelPlot getPlot(final ChunkPos pos) {
|
||||
final int plotX = (pos.x >> this.logPlotSize) - this.originX;
|
||||
final int plotZ = (pos.z >> this.logPlotSize) - this.originZ;
|
||||
|
||||
return this.getLocalPlot(plotX, plotZ);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return if a global chunk position is within the plotgrid.
|
||||
*/
|
||||
public boolean inBounds(final ChunkPos pos) {
|
||||
return this.inBounds(pos.x, pos.z);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return if a global block position is within the plotgrid.
|
||||
*/
|
||||
public boolean inBounds(final BlockPos pos) {
|
||||
return this.inBounds(pos.getX() >> SectionPos.SECTION_BITS, pos.getZ() >> SectionPos.SECTION_BITS);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return if a global chunk position is within the plotgrid.
|
||||
*/
|
||||
public boolean inBounds(final int x, final int z) {
|
||||
final int plotX = (x >> this.logPlotSize) - this.originX;
|
||||
final int plotZ = (z >> this.logPlotSize) - this.originZ;
|
||||
|
||||
final int sideLength = 1 << this.logSideLength;
|
||||
return (plotX >= 0 && plotX < sideLength && plotZ >= 0 && plotZ < sideLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a populated chunk in the plotgrid at the given global chunk position.
|
||||
*
|
||||
* @param pos the global chunk position
|
||||
*/
|
||||
public void newPopulatedChunk(final ChunkPos pos, final LevelChunk chunk) {
|
||||
if (!this.inBounds(pos)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int plotX = (pos.x >> this.logPlotSize) - this.originX;
|
||||
final int plotZ = (pos.z >> this.logPlotSize) - this.originZ;
|
||||
|
||||
final LevelPlot plot = this.getLocalPlot(plotX, plotZ);
|
||||
|
||||
if (plot == null) {
|
||||
Sable.LOGGER.error("Cannot add chunk at {}, {} in nonexistent sub-level plot", plotX, plotZ);
|
||||
return;
|
||||
}
|
||||
|
||||
final ChunkPos local = plot.toLocal(pos);
|
||||
|
||||
if (plot.getChunkHolder(local) != null) {
|
||||
throw new IllegalStateException("Chunk already exists at " + pos);
|
||||
}
|
||||
|
||||
final PlotChunkHolder holder = PlotChunkHolder.create(chunk.getLevel(), pos, plot.getLightEngine(), chunk);
|
||||
|
||||
plot.addChunkHolder(local, holder, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the players tracking a plot chunk.
|
||||
*
|
||||
* @return the players tracking the chunk
|
||||
*/
|
||||
public List<ServerPlayer> getPlayersTracking(final ChunkPos chunkPos) {
|
||||
final LevelPlot plot = this.getPlot(chunkPos);
|
||||
if (plot == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
final SubLevel subLevel = plot.getSubLevel();
|
||||
|
||||
if (subLevel instanceof final ServerSubLevel serverSubLevel) {
|
||||
final Collection<UUID> trackingPlayers = serverSubLevel.getTrackingPlayers();
|
||||
final ObjectList<ServerPlayer> players = new ObjectArrayList<>(trackingPlayers.size());
|
||||
|
||||
for (final UUID uuid : serverSubLevel.getTrackingPlayers()) {
|
||||
final ServerPlayer player = this.level.getServer().getPlayerList().getPlayer(uuid);
|
||||
|
||||
if (player != null) {
|
||||
players.add(player);
|
||||
}
|
||||
}
|
||||
|
||||
return players;
|
||||
}
|
||||
|
||||
return List.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return all of the plots in the plotgrid.
|
||||
*/
|
||||
public List<? extends SubLevel> getAllSubLevels() {
|
||||
return this.allSubLevels;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the level of the plotgrid.
|
||||
*/
|
||||
public Level getLevel() {
|
||||
return this.level;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the log_2 of the side length of a plot
|
||||
*/
|
||||
public int getLogPlotSize() {
|
||||
return this.logPlotSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the log_2 of the side length of the plotgrid
|
||||
*/
|
||||
public int getLogSideLength() {
|
||||
return this.logSideLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the origin of the plotgrid in plot coordinates
|
||||
*/
|
||||
public Vector2i getOrigin() {
|
||||
return new Vector2i(this.originX, this.originZ);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes a sub-level with a local plot coordinate
|
||||
*/
|
||||
public void removeSubLevel(final int x, final int z, final SubLevelRemovalReason reason) {
|
||||
final SubLevel subLevel = this.getSubLevel(x, z);
|
||||
if (subLevel == null) {
|
||||
throw new IllegalStateException("No sub-level at " + x + ", " + z);
|
||||
}
|
||||
|
||||
this.observers.forEach(observer -> observer.onSubLevelRemoved(subLevel, reason));
|
||||
subLevel.onRemove();
|
||||
|
||||
final int index = this.getIndex(x, z);
|
||||
this.subLevels[index] = null;
|
||||
this.allSubLevels.remove(subLevel);
|
||||
this.subLevelsByUUID.remove(subLevel.getUniqueId());
|
||||
|
||||
if (reason == SubLevelRemovalReason.REMOVED) {
|
||||
this.getOccupancy().clear(index);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the count of loaded sub-levels
|
||||
*/
|
||||
public int getLoadedCount() {
|
||||
return this.allSubLevels.size();
|
||||
}
|
||||
|
||||
public Iterable<SubLevel> queryIntersecting(final BoundingBox3dc bounds) {
|
||||
return () -> new ListBackedFilterIterator<>((subLevel) -> subLevel.boundingBox().intersects(bounds), this.allSubLevels);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a sub-level from the plotgrid.
|
||||
*
|
||||
* @param subLevel the sub-level to remove
|
||||
* @param reason the reason for removal
|
||||
*/
|
||||
public void removeSubLevel(final SubLevel subLevel, final SubLevelRemovalReason reason) {
|
||||
final int x = subLevel.getPlot().plotPos.x - this.originX;
|
||||
final int z = subLevel.getPlot().plotPos.z - this.originZ;
|
||||
this.removeSubLevel(x, z, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a particular sub-level by its UUID.
|
||||
*
|
||||
* @param uuid the UUID of the sub-level
|
||||
*/
|
||||
public @Nullable SubLevel getSubLevel(final UUID uuid) {
|
||||
return this.subLevelsByUUID.get(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* The occupancy of the plotgrid, including loaded and unloaded plots
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
public BitSet getOccupancy() {
|
||||
return this.occupancy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package dev.ryanhcode.sable.api.sublevel;
|
||||
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.storage.SubLevelRemovalReason;
|
||||
|
||||
/**
|
||||
* Observes additions, removals, and ticking of sub-levels.
|
||||
*/
|
||||
public interface SubLevelObserver {
|
||||
|
||||
/**
|
||||
* Called after a sub-level is added to a {@link SubLevelContainer}.
|
||||
*
|
||||
* @param subLevel the sub-level that was added
|
||||
*/
|
||||
default void onSubLevelAdded(final SubLevel subLevel) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before a sub-level is removed from a {@link SubLevelContainer}.
|
||||
*
|
||||
* @param subLevel the sub-level that will be removed
|
||||
*/
|
||||
default void onSubLevelRemoved(final SubLevel subLevel, final SubLevelRemovalReason reason) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called every tick for each {@link SubLevelContainer}.
|
||||
*
|
||||
* @param subLevels the sub-level container that is ticking
|
||||
*/
|
||||
default void tick(final SubLevelContainer subLevels) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package dev.ryanhcode.sable.api.sublevel;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Other mods or projects (looking at you, Simulated!) may want to piggyback off of the snapshot interpolation
|
||||
* system so that their content can also abide by it and benefit from its improvements. As such, we expose
|
||||
* "tracking" plugins for these projects to give us players that need to be informed about the interpolation tick
|
||||
* at any given moment.
|
||||
*/
|
||||
public interface SubLevelTrackingPlugin {
|
||||
|
||||
/**
|
||||
* Players that need to be informed about the interpolation ticks from the server &
|
||||
* the distances between them, and who should be actively running interpolation.
|
||||
*/
|
||||
Iterable<UUID> neededPlayers();
|
||||
|
||||
/**
|
||||
* Called when sub-level tracking data is sent
|
||||
*/
|
||||
void sendTrackingData(int interpolationTick);
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
package dev.ryanhcode.sable.command;
|
||||
|
||||
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import dev.ryanhcode.sable.api.SubLevelAssemblyHelper;
|
||||
import dev.ryanhcode.sable.api.command.SableCommandHelper;
|
||||
import dev.ryanhcode.sable.api.command.SubLevelArgumentType;
|
||||
import dev.ryanhcode.sable.companion.math.BoundingBox3i;
|
||||
import dev.ryanhcode.sable.companion.math.BoundingBox3ic;
|
||||
import dev.ryanhcode.sable.physics.chunk.VoxelNeighborhoodState;
|
||||
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
|
||||
import net.minecraft.commands.CommandBuildContext;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.commands.arguments.coordinates.BlockPosArgument;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
public class SableAssembleCommands {
|
||||
|
||||
public static final int DEFAULT_CONNECTED_ASSEMBLY_CAPACITY = 256_000;
|
||||
|
||||
/**
|
||||
* Adds the following commands:
|
||||
* <ul>
|
||||
* <li>{@code /sable assemble area <from> <to>}</li>
|
||||
* <li>{@code /sable assemble connected [<from>] [<capacity>]}</li>
|
||||
* <li>{@code /sable assemble sphere <radius> [<origin>]}</li>
|
||||
* <li>{@code /sable assemble cube <range> [<origin>]}</li>
|
||||
* <li>{@code /sable shatter sub_level <sub_level>}</li>
|
||||
* <li>{@code /sable shatter connected [<from>] [<capacity>]}</li>
|
||||
* <li>{@code /sable shatter sphere <radius> [<origin>]}</li>
|
||||
* <li>{@code /sable shatter cube <range> [<origin>]}</li>
|
||||
* <li>{@code /sable shatter area <from> <to>}</li>
|
||||
* </ul>
|
||||
*/
|
||||
public static void register(final LiteralArgumentBuilder<CommandSourceStack> sableBuilder, final CommandBuildContext buildContext) {
|
||||
sableBuilder
|
||||
.then(Commands.literal("assemble")
|
||||
.then(Commands.literal("shatter")
|
||||
.then(Commands.literal("sub_level")
|
||||
.then(Commands.argument("sub_level", SubLevelArgumentType.subLevels())
|
||||
.executes(SableAssembleCommands::executeShatterSubLevelCommand)))
|
||||
.then(Commands.literal("connected")
|
||||
.executes((ctx) ->
|
||||
SableAssembleCommands.executeShatterConnected(ctx, BlockPos.containing(ctx.getSource().getPosition().subtract(0, 1, 0)), DEFAULT_CONNECTED_ASSEMBLY_CAPACITY))
|
||||
.then(Commands.argument("from", BlockPosArgument.blockPos())
|
||||
.executes((ctx) ->
|
||||
SableAssembleCommands.executeShatterConnected(ctx, BlockPosArgument.getLoadedBlockPos(ctx, "from"), DEFAULT_CONNECTED_ASSEMBLY_CAPACITY))
|
||||
.then(Commands.argument("capacity", IntegerArgumentType.integer(1, DEFAULT_CONNECTED_ASSEMBLY_CAPACITY * 100))
|
||||
.executes((ctx) ->
|
||||
SableAssembleCommands.executeShatterConnected(ctx, BlockPosArgument.getLoadedBlockPos(ctx, "from"), IntegerArgumentType.getInteger(ctx, "capacity"))))))
|
||||
.then(Commands.literal("sphere")
|
||||
.then(Commands.argument("radius", IntegerArgumentType.integer(0, 128))
|
||||
.executes((ctx) -> SableAssembleCommands.executeShatterSphereCommand(ctx, BlockPos.containing(ctx.getSource().getPosition())))
|
||||
.then(Commands.argument("origin", BlockPosArgument.blockPos())
|
||||
.executes((ctx) -> SableAssembleCommands.executeShatterSphereCommand(ctx, BlockPosArgument.getLoadedBlockPos(ctx, "origin"))))))
|
||||
.then(Commands.literal("cube")
|
||||
.then(Commands.argument("range", IntegerArgumentType.integer(0, 128))
|
||||
.executes((ctx) -> SableAssembleCommands.executeShatterCubeCommand(ctx, BlockPos.containing(ctx.getSource().getPosition())))
|
||||
.then(Commands.argument("origin", BlockPosArgument.blockPos())
|
||||
.executes((ctx) -> SableAssembleCommands.executeShatterCubeCommand(ctx, BlockPosArgument.getLoadedBlockPos(ctx, "origin"))))))
|
||||
.then(Commands.literal("area")
|
||||
.then(Commands.argument("from", BlockPosArgument.blockPos())
|
||||
.then(Commands.argument("to", BlockPosArgument.blockPos())
|
||||
.executes(SableAssembleCommands::executeShatterAreaCommand)))))
|
||||
|
||||
.then(Commands.literal("area")
|
||||
.then(Commands.argument("from", BlockPosArgument.blockPos())
|
||||
.then(Commands.argument("to", BlockPosArgument.blockPos())
|
||||
.executes(SableAssembleCommands::executeAssembleAreaCommand))))
|
||||
|
||||
.then(Commands.literal("connected")
|
||||
.executes((ctx) ->
|
||||
SableAssembleCommands.executeAssembleConnectedCommand(ctx, BlockPos.containing(ctx.getSource().getPosition().subtract(0, 1, 0)), DEFAULT_CONNECTED_ASSEMBLY_CAPACITY))
|
||||
.then(Commands.argument("from", BlockPosArgument.blockPos())
|
||||
.executes((ctx) ->
|
||||
SableAssembleCommands.executeAssembleConnectedCommand(ctx, BlockPosArgument.getLoadedBlockPos(ctx, "from"), DEFAULT_CONNECTED_ASSEMBLY_CAPACITY))
|
||||
.then(Commands.argument("capacity", IntegerArgumentType.integer(1, DEFAULT_CONNECTED_ASSEMBLY_CAPACITY * 100))
|
||||
.executes((ctx) ->
|
||||
SableAssembleCommands.executeAssembleConnectedCommand(ctx, BlockPosArgument.getLoadedBlockPos(ctx, "from"), IntegerArgumentType.getInteger(ctx, "capacity"))))))
|
||||
|
||||
.then(Commands.literal("sphere")
|
||||
.then(Commands.argument("radius", IntegerArgumentType.integer(0, 256))
|
||||
.executes(ctx -> SableAssembleCommands.executeAssembleSphereCommand(ctx, BlockPos.containing(ctx.getSource().getPosition())))
|
||||
.then(Commands.argument("origin", BlockPosArgument.blockPos())
|
||||
.executes(ctx -> SableAssembleCommands.executeAssembleSphereCommand(ctx, BlockPosArgument.getLoadedBlockPos(ctx, "origin"))))))
|
||||
|
||||
.then(Commands.literal("cube")
|
||||
.then(Commands.argument("range", IntegerArgumentType.integer(0, 256))
|
||||
.executes(ctx -> SableAssembleCommands.executeAssembleCubeCommand(ctx, BlockPos.containing(ctx.getSource().getPosition())))
|
||||
.then(Commands.argument("origin", BlockPosArgument.blockPos())
|
||||
.executes(ctx -> SableAssembleCommands.executeAssembleCubeCommand(ctx, BlockPosArgument.getLoadedBlockPos(ctx, "origin")))))));
|
||||
}
|
||||
|
||||
private static int executeShatterConnected(final CommandContext<CommandSourceStack> ctx, final BlockPos assemblyOrigin, final int assemblyCapacity) throws CommandSyntaxException {
|
||||
final ServerLevel level = ctx.getSource().getLevel();
|
||||
|
||||
final SubLevelAssemblyHelper.GatherResult result = SubLevelAssemblyHelper.gatherConnectedBlocks(assemblyOrigin, level, assemblyCapacity, null);
|
||||
if (result.assemblyState() != SubLevelAssemblyHelper.GatherResult.State.SUCCESS) {
|
||||
ctx.getSource().sendFailure(Component.translatable(switch (result.assemblyState()) {
|
||||
case TOO_MANY_BLOCKS -> "commands.sable.sub_level.shatter.connected.too_many_blocks";
|
||||
case NO_BLOCKS -> "commands.sable.sub_level.shatter.no_blocks";
|
||||
default -> throw new IllegalStateException("Unexpected value: " + result.assemblyState());
|
||||
}, result.assemblyState() == SubLevelAssemblyHelper.GatherResult.State.TOO_MANY_BLOCKS ? assemblyCapacity : 0));
|
||||
return 0;
|
||||
}
|
||||
|
||||
final int blocksShattered = shatterBlocks(result.blocks(), level);
|
||||
if (blocksShattered == 0) {
|
||||
ctx.getSource().sendFailure(Component.translatable("commands.sable.sub_level.shatter.no_blocks"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
ctx.getSource().sendSuccess(() -> Component.translatable("commands.sable.sub_level.shatter.connected.success", blocksShattered), true);
|
||||
return blocksShattered;
|
||||
}
|
||||
|
||||
private static int executeShatterSubLevelCommand(final CommandContext<CommandSourceStack> ctx) throws CommandSyntaxException {
|
||||
final ServerLevel level = ctx.getSource().getLevel();
|
||||
final Collection<ServerSubLevel> subLevels = SubLevelArgumentType.getSubLevels(ctx,
|
||||
"sub_level");
|
||||
|
||||
if (subLevels.isEmpty()) {
|
||||
throw SableCommandHelper.ERROR_NO_SUB_LEVELS_FOUND.create();
|
||||
}
|
||||
|
||||
final IntStream shatteredAmounts = subLevels
|
||||
.stream()
|
||||
.filter(subLevel -> { //Filter out single block sub-levels
|
||||
int solidBlockCount = 0;
|
||||
for (final Iterator<BlockPos> it = BlockPos.betweenClosedStream(subLevel.getPlot().getBoundingBox().toMojang()).iterator(); it.hasNext(); ) {
|
||||
final BlockPos pos = it.next();
|
||||
if (VoxelNeighborhoodState.isSolid(level, pos, level.getBlockState(pos))) {
|
||||
solidBlockCount++;
|
||||
if (solidBlockCount > 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.map(subLevel -> subLevel.getPlot().getBoundingBox())
|
||||
.mapToInt(bounds -> shatterBoundingBox(bounds, level));
|
||||
|
||||
int blocksShattered = 0;
|
||||
int sublevelsShattered = 0;
|
||||
|
||||
for (final PrimitiveIterator.OfInt it = shatteredAmounts.iterator(); it.hasNext(); ) {
|
||||
final int i = it.next();
|
||||
blocksShattered += i;
|
||||
sublevelsShattered ++;
|
||||
}
|
||||
|
||||
if (sublevelsShattered == 0) {
|
||||
ctx.getSource().sendFailure(Component.translatable("commands.sable.sub_level.shatter.sub_level.only_single_block"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
final int finalSublevelsShattered = sublevelsShattered;
|
||||
final int finalBlocksShattered = blocksShattered;
|
||||
if (sublevelsShattered == 1) {
|
||||
ctx.getSource().sendSuccess(() -> Component.translatable("commands.sable.sub_level.shatter.sub_level.success", Component.translatable("commands.sable.sub_level"), finalBlocksShattered), true);
|
||||
} else {
|
||||
ctx.getSource().sendSuccess(() -> Component.translatable("commands.sable.sub_level.shatter.sub_level.success", Component.translatable("commands.sable.sub_levels", finalSublevelsShattered), finalBlocksShattered), true);
|
||||
}
|
||||
return blocksShattered;
|
||||
}
|
||||
|
||||
private static int executeShatterAreaCommand(final CommandContext<CommandSourceStack> ctx) throws CommandSyntaxException {
|
||||
final ServerLevel level = ctx.getSource().getLevel();
|
||||
final BoundingBox3i boundingBox = new BoundingBox3i(BlockPosArgument.getLoadedBlockPos(ctx, "from"), BlockPosArgument.getLoadedBlockPos(ctx, "to"));
|
||||
|
||||
final int blocksShattered = shatterBoundingBox(boundingBox, level);
|
||||
if (blocksShattered == 0) {
|
||||
ctx.getSource().sendFailure(Component.translatable("commands.sable.sub_level.shatter.no_blocks"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
ctx.getSource().sendSuccess(() -> Component.translatable("commands.sable.sub_level.shatter.region.success", blocksShattered), true);
|
||||
return blocksShattered;
|
||||
}
|
||||
|
||||
private static int executeShatterSphereCommand(final CommandContext<CommandSourceStack> ctx, final BlockPos origin) {
|
||||
final ServerLevel level = ctx.getSource().getLevel();
|
||||
final int radius = IntegerArgumentType.getInteger(ctx, "radius");
|
||||
final BoundingBox boundingBox = BoundingBox.fromCorners(
|
||||
origin.offset(-radius, -radius, -radius),
|
||||
origin.offset(radius, radius, radius)
|
||||
);
|
||||
|
||||
final int radiusSquared = radius * radius;
|
||||
|
||||
final List<BlockPos> blocks = BlockPos.betweenClosedStream(boundingBox).map(BlockPos::immutable).toList();
|
||||
final List<BlockPos> blocksInRadius = new ArrayList<>();
|
||||
for (final BlockPos blockPos : blocks) {
|
||||
if (origin.distSqr(blockPos) > radiusSquared) {
|
||||
continue;
|
||||
}
|
||||
blocksInRadius.add(blockPos);
|
||||
}
|
||||
final int blocksShattered = shatterBlocks(blocksInRadius, level);
|
||||
if (blocksShattered == 0) {
|
||||
ctx.getSource().sendFailure(Component.translatable("commands.sable.sub_level.shatter.no_blocks"));
|
||||
return 0;
|
||||
}
|
||||
ctx.getSource().sendSuccess(() -> Component.translatable("commands.sable.sub_level.shatter.radius.success", blocksShattered), true);
|
||||
return blocksShattered;
|
||||
}
|
||||
|
||||
private static int executeShatterCubeCommand(final CommandContext<CommandSourceStack> ctx, final BlockPos origin) {
|
||||
final ServerLevel level = ctx.getSource().getLevel();
|
||||
final int radius = IntegerArgumentType.getInteger(ctx, "range");
|
||||
final BoundingBox3i boundingBox = new BoundingBox3i(
|
||||
origin.offset(-radius, -radius, -radius),
|
||||
origin.offset(radius, radius, radius)
|
||||
);
|
||||
|
||||
final int blocksShattered = shatterBoundingBox(boundingBox, level);
|
||||
if (blocksShattered == 0) {
|
||||
ctx.getSource().sendFailure(Component.translatable("commands.sable.sub_level.shatter.no_blocks"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
ctx.getSource().sendSuccess(() -> Component.translatable("commands.sable.sub_level.shatter.range.success", blocksShattered), true);
|
||||
return blocksShattered;
|
||||
}
|
||||
|
||||
private static int shatterBoundingBox(final BoundingBox3ic boundingBox, final ServerLevel level) {
|
||||
return shatterBlocks(BlockPos.betweenClosedStream(boundingBox.toMojang()).map(BlockPos::immutable).toList(), level);
|
||||
}
|
||||
|
||||
private static int shatterBlocks(final Collection<BlockPos> blocks, final ServerLevel level) {
|
||||
//Remove fragile blocks
|
||||
for (final BlockPos pos : blocks) {
|
||||
if (!VoxelNeighborhoodState.isSolid(level, pos, level.getBlockState(pos))) {
|
||||
level.destroyBlock(pos, true);
|
||||
}
|
||||
}
|
||||
int shattered = 0;
|
||||
for (final BlockPos anchor : blocks) {
|
||||
if (shatterBlockToSubLevel(level, anchor)) {
|
||||
shattered++;
|
||||
}
|
||||
}
|
||||
return shattered;
|
||||
}
|
||||
|
||||
private static boolean shatterBlockToSubLevel(final ServerLevel level, final BlockPos anchor) {
|
||||
if (!VoxelNeighborhoodState.isSolid(level, anchor, level.getBlockState(anchor))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final BoundingBox3i bounds = new BoundingBox3i(anchor.getX(), anchor.getY(), anchor.getZ(), anchor.getX() + 1, anchor.getY() + 1, anchor.getZ() + 1);
|
||||
bounds.set(
|
||||
bounds.minX - 1,
|
||||
bounds.minY - 1,
|
||||
bounds.minZ - 1,
|
||||
bounds.maxX + 1,
|
||||
bounds.maxY + 1,
|
||||
bounds.maxZ + 1
|
||||
);
|
||||
SubLevelAssemblyHelper.assembleBlocks(level, anchor, List.of(anchor), bounds);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int executeAssembleAreaCommand(final CommandContext<CommandSourceStack> ctx) throws CommandSyntaxException {
|
||||
final ServerLevel level = ctx.getSource().getLevel();
|
||||
final BoundingBox boundingBox = BoundingBox.fromCorners(BlockPosArgument.getLoadedBlockPos(ctx, "from"), BlockPosArgument.getLoadedBlockPos(ctx, "to"));
|
||||
|
||||
final List<BlockPos> blocks = BlockPos.betweenClosedStream(boundingBox).map(BlockPos::immutable).toList();
|
||||
final BlockPos anchor = blocks.getFirst();
|
||||
|
||||
final BoundingBox3i bounds = new BoundingBox3i(boundingBox);
|
||||
bounds.set(
|
||||
bounds.minX - 1,
|
||||
bounds.minY - 1,
|
||||
bounds.minZ - 1,
|
||||
bounds.maxX + 1,
|
||||
bounds.maxY + 1,
|
||||
bounds.maxZ + 1
|
||||
);
|
||||
|
||||
final ServerSubLevel subLevel = SubLevelAssemblyHelper.assembleBlocks(level, anchor, blocks, bounds);
|
||||
if (subLevel.getMassTracker().isInvalid()) {
|
||||
ctx.getSource().sendFailure(Component.translatable("commands.sable.sub_level.assemble.no_blocks"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
ctx.getSource().sendSuccess(() -> Component.translatable("commands.sable.sub_level.assemble.region.success", blocks.size()), true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int executeAssembleCubeCommand(final CommandContext<CommandSourceStack> ctx, final BlockPos origin) {
|
||||
final ServerLevel level = ctx.getSource().getLevel();
|
||||
final int range = IntegerArgumentType.getInteger(ctx, "range");
|
||||
final BoundingBox boundingBox = BoundingBox.fromCorners(origin.offset(-range, -range, -range), origin.offset(range, range, range));
|
||||
|
||||
final List<BlockPos> blocks = BlockPos.betweenClosedStream(boundingBox).map(BlockPos::immutable).toList();
|
||||
final BlockPos anchor = blocks.getFirst();
|
||||
|
||||
final BoundingBox3i bounds = new BoundingBox3i(boundingBox);
|
||||
bounds.set(
|
||||
bounds.minX - 1,
|
||||
bounds.minY - 1,
|
||||
bounds.minZ - 1,
|
||||
bounds.maxX + 1,
|
||||
bounds.maxY + 1,
|
||||
bounds.maxZ + 1
|
||||
);
|
||||
|
||||
final ServerSubLevel subLevel = SubLevelAssemblyHelper.assembleBlocks(level, anchor, blocks, bounds);
|
||||
if (subLevel.getMassTracker().isInvalid()) {
|
||||
ctx.getSource().sendFailure(Component.translatable("commands.sable.sub_level.assemble.no_blocks"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
ctx.getSource().sendSuccess(() -> Component.translatable("commands.sable.sub_level.assemble.range.success", blocks.size()), true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int executeAssembleConnectedCommand(final CommandContext<CommandSourceStack> ctx, final BlockPos assemblyOrigin, final int assemblyCapacity) throws CommandSyntaxException {
|
||||
final ServerLevel level = ctx.getSource().getLevel();
|
||||
|
||||
final SubLevelAssemblyHelper.GatherResult result = SubLevelAssemblyHelper.gatherConnectedBlocks(assemblyOrigin, level, assemblyCapacity, null);
|
||||
if (result.assemblyState() != SubLevelAssemblyHelper.GatherResult.State.SUCCESS) {
|
||||
ctx.getSource().sendFailure(Component.translatable(result.assemblyState().errorKey, result.assemblyState() == SubLevelAssemblyHelper.GatherResult.State.TOO_MANY_BLOCKS ? assemblyCapacity : 0));
|
||||
return 0;
|
||||
}
|
||||
|
||||
SubLevelAssemblyHelper.assembleBlocks(level, assemblyOrigin, result.blocks(), result.boundingBox());
|
||||
ctx.getSource().sendSuccess(() -> Component.translatable("commands.sable.sub_level.assemble.connected.success", result.blocks().size()), true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int executeAssembleSphereCommand(final CommandContext<CommandSourceStack> ctx, final BlockPos origin) {
|
||||
final int radius = IntegerArgumentType.getInteger(ctx, "radius");
|
||||
|
||||
final ServerLevel level = ctx.getSource().getLevel();
|
||||
|
||||
final Set<BlockPos> blocks = new HashSet<>();
|
||||
|
||||
int minX = Integer.MAX_VALUE, minY = Integer.MAX_VALUE, minZ = Integer.MAX_VALUE;
|
||||
int maxX = Integer.MIN_VALUE, maxY = Integer.MIN_VALUE, maxZ = Integer.MIN_VALUE;
|
||||
|
||||
final int radiusSquared = radius * radius;
|
||||
|
||||
for (int x = -radius; x <= radius; x++) {
|
||||
for (int y = -radius; y <= radius; y++) {
|
||||
for (int z = -radius; z <= radius; z++) {
|
||||
if (x * x + y * y + z * z > radiusSquared) {
|
||||
continue;
|
||||
}
|
||||
final BlockPos pos = origin.offset(x, y, z);
|
||||
|
||||
if (level.isLoaded(pos) && !level.getBlockState(pos).isAir()) {
|
||||
blocks.add(pos);
|
||||
|
||||
minX = Math.min(minX, pos.getX());
|
||||
minY = Math.min(minY, pos.getY());
|
||||
minZ = Math.min(minZ, pos.getZ());
|
||||
|
||||
maxX = Math.max(maxX, pos.getX());
|
||||
maxY = Math.max(maxY, pos.getY());
|
||||
maxZ = Math.max(maxZ, pos.getZ());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (blocks.isEmpty()) {
|
||||
ctx.getSource().sendFailure(Component.translatable("commands.sable.sub_level.assemble.no_blocks"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
final BoundingBox3i bounds = new BoundingBox3i(
|
||||
minX, minY, minZ,
|
||||
maxX, maxY, maxZ
|
||||
);
|
||||
|
||||
SubLevelAssemblyHelper.assembleBlocks(level, origin, blocks, bounds);
|
||||
|
||||
final int finalBlocksCount = blocks.size();
|
||||
ctx.getSource().sendSuccess(() -> Component.translatable("commands.sable.sub_level.assemble.radius.success", finalBlocksCount), true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package dev.ryanhcode.sable.command;
|
||||
|
||||
import com.mojang.brigadier.CommandDispatcher;
|
||||
import com.mojang.brigadier.arguments.BoolArgumentType;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import dev.ryanhcode.sable.api.command.SableCommandHelper;
|
||||
import dev.ryanhcode.sable.api.command.SubLevelArgumentType;
|
||||
import dev.ryanhcode.sable.api.physics.handle.RigidBodyHandle;
|
||||
import dev.ryanhcode.sable.api.sublevel.ServerSubLevelContainer;
|
||||
import dev.ryanhcode.sable.companion.math.Pose3dc;
|
||||
import dev.ryanhcode.sable.network.packets.tcp.ClientboundEnterGizmoPacket;
|
||||
import dev.ryanhcode.sable.network.packets.udp.SableUDPEchoPacket;
|
||||
import dev.ryanhcode.sable.network.udp.SableUDPServer;
|
||||
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.storage.holding.GlobalSavedSubLevelPointer;
|
||||
import dev.ryanhcode.sable.sublevel.system.SubLevelPhysicsSystem;
|
||||
import foundry.veil.api.network.VeilPacketManager;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.commands.CommandBuildContext;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.network.chat.*;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import org.joml.Quaterniondc;
|
||||
import org.joml.Vector3d;
|
||||
import org.joml.Vector3dc;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Formatter;
|
||||
import java.util.Locale;
|
||||
|
||||
public class SableCommand {
|
||||
|
||||
public static void register(final CommandDispatcher<CommandSourceStack> dispatcher, final CommandBuildContext buildContext) {
|
||||
final LiteralArgumentBuilder<CommandSourceStack> sableBuilder = Commands.literal("sable")
|
||||
.requires(commandSourceStack -> commandSourceStack.hasPermission(2));
|
||||
|
||||
SablePhysicsCommands.register(sableBuilder, buildContext);
|
||||
SableSpawnCommands.register(sableBuilder, buildContext);
|
||||
SableSubLevelCommands.register(sableBuilder, buildContext);
|
||||
SableAssembleCommands.register(sableBuilder, buildContext);
|
||||
SableStorageCommands.register(sableBuilder, buildContext);
|
||||
|
||||
final LiteralArgumentBuilder<CommandSourceStack> debugBuilder = Commands.literal("debug");
|
||||
|
||||
SableJointCommands.register(debugBuilder, buildContext);
|
||||
SableConfigCommands.register(debugBuilder, buildContext);
|
||||
|
||||
sableBuilder
|
||||
.then(debugBuilder
|
||||
.then(Commands.literal("udp_test").executes(ctx -> {
|
||||
final SableUDPServer server = SableUDPServer.getServer(ctx.getSource().getServer());
|
||||
|
||||
if (server != null) {
|
||||
server.sendUDPPacket(ctx.getSource().getPlayerOrException(), new SableUDPEchoPacket("Skibidi Toilet"), true);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}))
|
||||
);
|
||||
|
||||
sableBuilder
|
||||
.then(Commands.literal("engage_gizmo")
|
||||
.executes(SableCommand::executeEnableGizmoCommand))
|
||||
|
||||
.then(Commands.literal("paused")
|
||||
.executes(SableCommand::executeTogglePhysicsPausedCommand)
|
||||
.then(Commands.argument("paused", BoolArgumentType.bool())
|
||||
.executes(SableCommand::executeSetPhysicsPausedCommand)))
|
||||
|
||||
.then(Commands.literal("info").then(Commands.argument("sub_level", SubLevelArgumentType.subLevels()).executes(ctx -> {
|
||||
final CommandSourceStack source = ctx.getSource();
|
||||
final ServerSubLevelContainer container = SableCommandHelper.requireSubLevelContainer(source);
|
||||
final Collection<ServerSubLevel> subLevels = SubLevelArgumentType.getSubLevels(ctx, "sub_level");
|
||||
|
||||
if (subLevels.isEmpty()) {
|
||||
throw SableCommandHelper.ERROR_NO_SUB_LEVELS_FOUND.create();
|
||||
}
|
||||
|
||||
source.sendSuccess(() -> Component.translatable("commands.sable.info.count", subLevels.size()), false);
|
||||
for (final ServerSubLevel subLevel : subLevels) {
|
||||
final Pose3dc pose = subLevel.logicalPose();
|
||||
source.sendSuccess(() -> {
|
||||
final Vector3dc pos = pose.position();
|
||||
final MutableComponent component = Component.translatable("commands.sable.info.name", Component.literal(subLevel.getName() != null ? subLevel.getName() : subLevel.getUniqueId().toString()));
|
||||
final ResourceLocation dimension = subLevel.getLevel().dimension().location();
|
||||
final GlobalSavedSubLevelPointer pointer = subLevel.getLastSerializationPointer();
|
||||
final Component fileId = Component.translatable("commands.sable.info.name.tooltip", pointer != null ? pointer.toString() : "None yet");
|
||||
component.setStyle(Style.EMPTY.withClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, new Formatter().format(Locale.ROOT, "/execute in %s run tp @s %.2f %.2f %.2f", dimension, pos.x(), pos.y(), pos.z()).toString()))
|
||||
.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, fileId))
|
||||
.withColor(ChatFormatting.GRAY));
|
||||
return component;
|
||||
}, false);
|
||||
source.sendSuccess(() -> {
|
||||
final Vector3dc pos = pose.position();
|
||||
return Component.translatable("commands.sable.info.position", pos.x(), pos.y(), pos.z());
|
||||
}, false);
|
||||
source.sendSuccess(() -> {
|
||||
final Quaterniondc orientation = pose.orientation();
|
||||
return Component.translatable("commands.sable.info.orientation", orientation.x(), orientation.y(), orientation.z(), orientation.w());
|
||||
}, false);
|
||||
source.sendSuccess(() -> {
|
||||
return Component.translatable("commands.sable.info.mass", subLevel.getMassTracker().getMass());
|
||||
}, false);
|
||||
|
||||
final SubLevelPhysicsSystem physicsSystem = container.physicsSystem();
|
||||
final RigidBodyHandle handle = physicsSystem.getPhysicsHandle(subLevel);
|
||||
source.sendSuccess(() -> {
|
||||
final Vector3dc pos = handle.getLinearVelocity(new Vector3d());
|
||||
return Component.translatable("commands.sable.info.linear_velocity",
|
||||
pos.x(), pos.y(), pos.z());
|
||||
}, false);
|
||||
source.sendSuccess(() -> {
|
||||
final Vector3dc pos = handle.getAngularVelocity(new Vector3d());
|
||||
return Component.translatable("commands.sable.info.angular_velocity", pos.x(), pos.y(), pos.z());
|
||||
}, false);
|
||||
}
|
||||
return subLevels.size();
|
||||
})));
|
||||
|
||||
|
||||
dispatcher.register(sableBuilder);
|
||||
|
||||
}
|
||||
|
||||
private static int executeEnableGizmoCommand(final CommandContext<CommandSourceStack> ctx) throws CommandSyntaxException {
|
||||
final CommandSourceStack source = ctx.getSource();
|
||||
final ServerPlayer player = source.getPlayerOrException();
|
||||
|
||||
SableCommandHelper.requireSubLevelPhysicsSystem(ctx).setPaused(true);
|
||||
|
||||
VeilPacketManager.player(player).sendPacket(new ClientboundEnterGizmoPacket());
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int executeTogglePhysicsPausedCommand(final CommandContext<CommandSourceStack> ctx) throws CommandSyntaxException {
|
||||
final boolean pause = !SableCommandHelper.requireSubLevelPhysicsSystem(ctx).getPaused();
|
||||
SableCommandHelper.requireSubLevelPhysicsSystem(ctx).setPaused(pause);
|
||||
|
||||
ctx.getSource().sendSuccess(() -> Component.translatable("commands.sable.physics.paused_toggled.success", Boolean.toString(pause)), true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int executeSetPhysicsPausedCommand(final CommandContext<CommandSourceStack> ctx) throws CommandSyntaxException {
|
||||
final boolean pause = BoolArgumentType.getBool(ctx, "paused");
|
||||
|
||||
SableCommandHelper.requireSubLevelPhysicsSystem(ctx).setPaused(pause);
|
||||
|
||||
ctx.getSource().sendSuccess(() -> Component.translatable("commands.sable.physics.paused.success", Boolean.toString(pause)), true);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package dev.ryanhcode.sable.command;
|
||||
|
||||
import com.mojang.brigadier.arguments.FloatArgumentType;
|
||||
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import dev.ryanhcode.sable.api.sublevel.SubLevelContainer;
|
||||
import dev.ryanhcode.sable.physics.config.PhysicsConfigData;
|
||||
import dev.ryanhcode.sable.sublevel.system.SubLevelPhysicsSystem;
|
||||
import net.minecraft.commands.CommandBuildContext;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
|
||||
public class SableConfigCommands {
|
||||
|
||||
/**
|
||||
* Adds the following commands:
|
||||
* <ul>
|
||||
* <li>{@code /sable config <property> <value>}</li>
|
||||
* </ul>
|
||||
*/
|
||||
public static void register(final LiteralArgumentBuilder<CommandSourceStack> sableBuilder, final CommandBuildContext buildContext) {
|
||||
|
||||
sableBuilder.then(Commands.literal("config")
|
||||
.then(Commands.literal("min_island_size")
|
||||
.then(Commands.argument("size", IntegerArgumentType.integer(0, Integer.MAX_VALUE))
|
||||
.executes(ctx -> {
|
||||
final SubLevelPhysicsSystem physicsSystem = SubLevelContainer.getContainer(ctx.getSource().getLevel()).physicsSystem();
|
||||
final PhysicsConfigData config = physicsSystem.getConfig();
|
||||
config.minDynamicBodiesPerIsland = IntegerArgumentType.getInteger(ctx, "size");
|
||||
physicsSystem.onConfigUpdated();
|
||||
return 0;
|
||||
}))
|
||||
)
|
||||
.then(Commands.literal("contact_spring_natural_frequency")
|
||||
.then(Commands.argument("natural_frequency", FloatArgumentType.floatArg(0.0f))
|
||||
.executes(ctx -> {
|
||||
final SubLevelPhysicsSystem physicsSystem = SubLevelContainer.getContainer(ctx.getSource().getLevel()).physicsSystem();
|
||||
final PhysicsConfigData config = physicsSystem.getConfig();
|
||||
config.contactSpringFrequency = FloatArgumentType.getFloat(ctx, "natural_frequency");
|
||||
physicsSystem.onConfigUpdated();
|
||||
return 0;
|
||||
}))
|
||||
)
|
||||
.then(Commands.literal("contact_spring_damping_ratio")
|
||||
.then(Commands.argument("damping_ratio", FloatArgumentType.floatArg(0.0f))
|
||||
.executes(ctx -> {
|
||||
final SubLevelPhysicsSystem physicsSystem = SubLevelContainer.getContainer(ctx.getSource().getLevel()).physicsSystem();
|
||||
final PhysicsConfigData config = physicsSystem.getConfig();
|
||||
config.contactSpringDampingRatio = FloatArgumentType.getFloat(ctx, "damping_ratio");
|
||||
physicsSystem.onConfigUpdated();
|
||||
return 0;
|
||||
}))
|
||||
)
|
||||
.then(Commands.literal("solver_iterations")
|
||||
.then(Commands.argument("iterations", IntegerArgumentType.integer(0, Integer.MAX_VALUE))
|
||||
.executes(ctx -> {
|
||||
final SubLevelPhysicsSystem physicsSystem = SubLevelContainer.getContainer(ctx.getSource().getLevel()).physicsSystem();
|
||||
final PhysicsConfigData config = physicsSystem.getConfig();
|
||||
config.solverIterations = IntegerArgumentType.getInteger(ctx, "iterations");
|
||||
physicsSystem.onConfigUpdated();
|
||||
return 0;
|
||||
}))
|
||||
)
|
||||
.then(Commands.literal("stabilization_iterations")
|
||||
.then(Commands.argument("iterations", IntegerArgumentType.integer(0, Integer.MAX_VALUE))
|
||||
.executes(ctx -> {
|
||||
final SubLevelPhysicsSystem physicsSystem = SubLevelContainer.getContainer(ctx.getSource().getLevel()).physicsSystem();
|
||||
final PhysicsConfigData config = physicsSystem.getConfig();
|
||||
config.stabilizationIterations = IntegerArgumentType.getInteger(ctx, "iterations");
|
||||
physicsSystem.onConfigUpdated();
|
||||
return 0;
|
||||
}))
|
||||
)
|
||||
.then(Commands.literal("pgs_iterations")
|
||||
.then(Commands.argument("iterations", IntegerArgumentType.integer(0, Integer.MAX_VALUE))
|
||||
.executes(ctx -> {
|
||||
final SubLevelPhysicsSystem physicsSystem = SubLevelContainer.getContainer(ctx.getSource().getLevel()).physicsSystem();
|
||||
final PhysicsConfigData config = physicsSystem.getConfig();
|
||||
config.pgsIterations = IntegerArgumentType.getInteger(ctx, "iterations");
|
||||
physicsSystem.onConfigUpdated();
|
||||
return 0;
|
||||
}))
|
||||
)
|
||||
.then(Commands.literal("substeps")
|
||||
.then(Commands.argument("substeps", IntegerArgumentType.integer(0, Integer.MAX_VALUE))
|
||||
.executes(ctx -> {
|
||||
final SubLevelPhysicsSystem physicsSystem = SubLevelContainer.getContainer(ctx.getSource().getLevel()).physicsSystem();
|
||||
final PhysicsConfigData config = physicsSystem.getConfig();
|
||||
config.substepsPerTick = IntegerArgumentType.getInteger(ctx, "substeps");
|
||||
physicsSystem.onConfigUpdated();
|
||||
return 0;
|
||||
}))
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package dev.ryanhcode.sable.command;
|
||||
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
|
||||
import dev.ryanhcode.sable.api.command.SableCommandHelper;
|
||||
import dev.ryanhcode.sable.api.command.SubLevelArgumentType;
|
||||
import dev.ryanhcode.sable.api.physics.PhysicsPipeline;
|
||||
import dev.ryanhcode.sable.api.physics.constraint.rotary.RotaryConstraintConfiguration;
|
||||
import dev.ryanhcode.sable.api.sublevel.ServerSubLevelContainer;
|
||||
import dev.ryanhcode.sable.companion.math.JOMLConversion;
|
||||
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
|
||||
import net.minecraft.commands.CommandBuildContext;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.commands.arguments.coordinates.Vec3Argument;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public class SableJointCommands {
|
||||
|
||||
public static final SimpleCommandExceptionType MISSING_JOINT_SUBLEVEL_TARGET =
|
||||
new SimpleCommandExceptionType(Component.translatable("commands.sable.joint.missing_sublevel_target"));
|
||||
|
||||
/**
|
||||
* Adds the following commands:
|
||||
* <ul>
|
||||
* <li>{@code /sable joint add <subLevel> <subLevel> radial <pos1> <pos2> <axis1> <axis2>}</li>
|
||||
* </ul>
|
||||
*/
|
||||
public static void register(final LiteralArgumentBuilder<CommandSourceStack> sableBuilder, final CommandBuildContext buildContext) {
|
||||
|
||||
sableBuilder.then(Commands.literal("joint")
|
||||
.then(Commands.literal("add")
|
||||
.then(Commands.argument("subLevel1", SubLevelArgumentType.subLevels())
|
||||
.then(Commands.argument("subLevel2", SubLevelArgumentType.subLevels())
|
||||
.then(Commands.literal("rotary")
|
||||
.then(Commands.argument("pos1", Vec3Argument.vec3(false))
|
||||
.then(Commands.argument("pos2", Vec3Argument.vec3(false))
|
||||
.then(Commands.argument("axis1", Vec3Argument.vec3(false))
|
||||
.then(Commands.argument("axis2", Vec3Argument.vec3(false))
|
||||
.executes(SableJointCommands::executeAddJointCommand)))))))))
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
private static int executeAddJointCommand(final CommandContext<CommandSourceStack> ctx) throws CommandSyntaxException {
|
||||
final ServerSubLevelContainer container = SableCommandHelper.requireSubLevelContainer(ctx);
|
||||
final PhysicsPipeline pipeline = SableCommandHelper.requireSubLevelPhysicsSystem(container).getPipeline();
|
||||
addRotaryJoint(
|
||||
pipeline,
|
||||
SubLevelArgumentType.getSubLevels(ctx, "subLevel1"),
|
||||
SubLevelArgumentType.getSubLevels(ctx, "subLevel2"),
|
||||
Vec3Argument.getVec3(ctx, "pos1"), Vec3Argument.getVec3(ctx, "pos2"),
|
||||
Vec3Argument.getVec3(ctx, "axis1"), Vec3Argument.getVec3(ctx, "axis2")
|
||||
);
|
||||
|
||||
ctx.getSource().sendSuccess(() -> Component.translatable("commands.sable.joint.success"), true);
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static void addRotaryJoint(
|
||||
final PhysicsPipeline pipeline,
|
||||
final Collection<ServerSubLevel> subLevel1,
|
||||
final Collection<ServerSubLevel> subLevel2,
|
||||
final Vec3 pos1, final Vec3 pos2,
|
||||
final Vec3 axis1, final Vec3 axis2
|
||||
) throws CommandSyntaxException {
|
||||
final RotaryConstraintConfiguration constraintConfig = new RotaryConstraintConfiguration(
|
||||
JOMLConversion.toJOML(pos1),
|
||||
JOMLConversion.toJOML(pos2),
|
||||
JOMLConversion.toJOML(axis1),
|
||||
JOMLConversion.toJOML(axis2)
|
||||
);
|
||||
|
||||
final ServerSubLevel jointSubLevel1 = subLevel1.stream().findFirst()
|
||||
.orElseThrow(MISSING_JOINT_SUBLEVEL_TARGET::create);
|
||||
final ServerSubLevel jointSubLevel2 = subLevel2.stream().findFirst()
|
||||
.orElseThrow(MISSING_JOINT_SUBLEVEL_TARGET::create);
|
||||
|
||||
pipeline.addConstraint(jointSubLevel1, jointSubLevel2, constraintConfig);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
package dev.ryanhcode.sable.command;
|
||||
|
||||
import com.mojang.brigadier.Command;
|
||||
import com.mojang.brigadier.arguments.DoubleArgumentType;
|
||||
import com.mojang.brigadier.builder.ArgumentBuilder;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import dev.ryanhcode.sable.api.command.SableCommandHelper;
|
||||
import dev.ryanhcode.sable.api.command.SubLevelArgumentType;
|
||||
import dev.ryanhcode.sable.api.physics.PhysicsPipeline;
|
||||
import dev.ryanhcode.sable.companion.math.JOMLConversion;
|
||||
import dev.ryanhcode.sable.companion.math.Pose3d;
|
||||
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.system.SubLevelPhysicsSystem;
|
||||
import net.minecraft.commands.CommandBuildContext;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.commands.arguments.coordinates.RotationArgument;
|
||||
import net.minecraft.commands.arguments.coordinates.Vec3Argument;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.phys.Vec2;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.joml.Quaterniond;
|
||||
import org.joml.Vector3d;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class SablePhysicsCommands {
|
||||
|
||||
/**
|
||||
* Adds the following commands:
|
||||
* <ul>
|
||||
* <li>{@code /sable physics impulse <sub_level> <linear|angular> <impulse> <global>}</li>
|
||||
* <li>{@code /sable physics rotation <sub_level> <add> <axis/entity> <rotation> <global>}</li>
|
||||
* <li>{@code /sable physics rotation <sub_level> <set> <axis/entity> <rotation>}</li>
|
||||
* <li>{@code /sable physics translation <sub_level> <add> <rotation> <global>}</li>
|
||||
* <li>{@code /sable physics translation <sub_level> <set> <rotation>}</li>
|
||||
* </ul>
|
||||
*/
|
||||
public static void register(final LiteralArgumentBuilder<CommandSourceStack> sableBuilder, final CommandBuildContext buildContext) {
|
||||
sableBuilder.then(Commands.literal("physics")
|
||||
.then(Commands.literal("impulse")
|
||||
.then(Commands.argument("sub_level", SubLevelArgumentType.subLevels())
|
||||
.then(Commands.literal("linear")
|
||||
.then(Commands.argument("impulse", Vec3ArgumentAbsolute.vec3())
|
||||
.executes((ctx) ->
|
||||
SablePhysicsCommands.executeLinearImpulseCommand(ctx, true))
|
||||
.then(Commands.literal("global")
|
||||
.executes((ctx) ->
|
||||
SablePhysicsCommands.executeLinearImpulseCommand(ctx, true)))
|
||||
.then(Commands.literal("local")
|
||||
.executes((ctx) ->
|
||||
SablePhysicsCommands.executeLinearImpulseCommand(ctx, false)))
|
||||
))
|
||||
|
||||
.then(Commands.literal("angular")
|
||||
.then(Commands.argument("impulse", Vec3ArgumentAbsolute.vec3())
|
||||
.executes((ctx) ->
|
||||
SablePhysicsCommands.executeAngularImpulseCommand(ctx, true))
|
||||
.then(Commands.literal("global")
|
||||
.executes((ctx) ->
|
||||
SablePhysicsCommands.executeAngularImpulseCommand(ctx, true)))
|
||||
.then(Commands.literal("local")
|
||||
.executes((ctx) ->
|
||||
SablePhysicsCommands.executeAngularImpulseCommand(ctx, false)))
|
||||
))
|
||||
))
|
||||
.then(Commands.literal("rotation")
|
||||
.then(Commands.argument("sub_level", SubLevelArgumentType.subLevels())
|
||||
.then(wrapRotationWithMode(true))
|
||||
.then(wrapRotationWithMode(false))
|
||||
))
|
||||
|
||||
.then(Commands.literal("translation")
|
||||
.then(Commands.argument("sub_level", SubLevelArgumentType.subLevels())
|
||||
|
||||
.then(Commands.literal("add")
|
||||
.then(Commands.argument("translation", Vec3ArgumentAbsolute.vec3())
|
||||
.executes((ctx) ->
|
||||
SablePhysicsCommands.executeAddTranslationCommand(ctx, true))
|
||||
.then(Commands.literal("global")
|
||||
.executes((ctx) ->
|
||||
SablePhysicsCommands.executeAddTranslationCommand(ctx, true)))
|
||||
.then(Commands.literal("local")
|
||||
.executes((ctx) ->
|
||||
SablePhysicsCommands.executeAddTranslationCommand(ctx, false)))
|
||||
))
|
||||
|
||||
.then(Commands.literal("set")
|
||||
.then(Commands.argument("translation", Vec3Argument.vec3(false))
|
||||
.executes(SablePhysicsCommands::executeSetTranslationCommand))))
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
private static Component getGlobalComponent(final boolean global) {
|
||||
return Component.translatable("commands.sable.physics." + (global ? "global" : "local"));
|
||||
}
|
||||
|
||||
private static int executeLinearImpulseCommand(final CommandContext<CommandSourceStack> ctx, final boolean global) throws CommandSyntaxException {
|
||||
final SubLevelPhysicsSystem system = SableCommandHelper.requireSubLevelPhysicsSystem(ctx);
|
||||
|
||||
final Collection<ServerSubLevel> subLevels = SubLevelArgumentType.getSubLevels(ctx, "sub_level");
|
||||
final Vec3 impulse = ctx.getArgument("impulse", Vec3.class);
|
||||
|
||||
if (subLevels.isEmpty()) {
|
||||
throw SableCommandHelper.ERROR_NO_SUB_LEVELS_FOUND.create();
|
||||
}
|
||||
|
||||
for (final ServerSubLevel subLevel : subLevels) {
|
||||
Vec3 subLevelImpulse = impulse;
|
||||
if (global) {
|
||||
subLevelImpulse = subLevel.logicalPose().transformNormalInverse(subLevelImpulse);
|
||||
}
|
||||
|
||||
system.getPhysicsHandle(subLevel)
|
||||
.applyLinearImpulse(
|
||||
JOMLConversion.toJOML(subLevelImpulse)
|
||||
);
|
||||
}
|
||||
|
||||
SableCommandHelper.sendSuccessDescribingSubLevelsAtIndex("commands.sable.physics.impulse.linear.success", ctx, subLevels, 1,
|
||||
getGlobalComponent(global), impulse.x + ", " + impulse.y + ", " + impulse.z);
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int executeAngularImpulseCommand(final CommandContext<CommandSourceStack> ctx, final boolean global) throws CommandSyntaxException {
|
||||
final SubLevelPhysicsSystem system = SableCommandHelper.requireSubLevelPhysicsSystem(ctx);
|
||||
|
||||
final Collection<ServerSubLevel> subLevels = SubLevelArgumentType.getSubLevels(ctx, "sub_level");
|
||||
final Vec3 impulse = ctx.getArgument("impulse", Vec3.class);
|
||||
|
||||
if (subLevels.isEmpty()) {
|
||||
throw SableCommandHelper.ERROR_NO_SUB_LEVELS_FOUND.create();
|
||||
}
|
||||
|
||||
for (final ServerSubLevel subLevel : subLevels) {
|
||||
Vec3 subLevelImpulse = impulse;
|
||||
if (global) {
|
||||
subLevelImpulse = subLevel.logicalPose().transformNormalInverse(subLevelImpulse);
|
||||
}
|
||||
|
||||
system.getPhysicsHandle(subLevel)
|
||||
.applyAngularImpulse(
|
||||
JOMLConversion.toJOML(subLevelImpulse)
|
||||
);
|
||||
}
|
||||
|
||||
SableCommandHelper.sendSuccessDescribingSubLevelsAtIndex("commands.sable.physics.impulse.angular.success", ctx, subLevels, 1,
|
||||
getGlobalComponent(global), impulse.x + ", " + impulse.y + ", " + impulse.z);
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static ArgumentBuilder<CommandSourceStack, ?> wrapRotationWithMode(final boolean add) {
|
||||
return Commands.literal(add ? "add" : "set").then(wrapRotationWithReferenceFrame(add, false)).then(wrapRotationWithReferenceFrame(add, true));
|
||||
}
|
||||
|
||||
private static ArgumentBuilder<CommandSourceStack, ?> wrapRotationWithReferenceFrame(final boolean add, final boolean axis) {
|
||||
final Command<CommandSourceStack> c = (ctx) -> SablePhysicsCommands.executeRotationCommand(ctx, add, axis, true);
|
||||
final Function<ArgumentBuilder<CommandSourceStack, ?>, ArgumentBuilder<CommandSourceStack, ?>> f = (b) -> {
|
||||
if (add)
|
||||
b.then(wrapRotationWithGlobality(axis, true)).then(wrapRotationWithGlobality(axis, false));
|
||||
return b;
|
||||
};
|
||||
final ArgumentBuilder<CommandSourceStack, ?> b = axis ?
|
||||
Commands.argument("axis", Vec3ArgumentAbsolute.vec3()).then(f.apply(Commands.argument("angle", DoubleArgumentType.doubleArg()).executes(c))) :
|
||||
f.apply(Commands.argument("rotation", RotationArgument.rotation()).executes(c));
|
||||
|
||||
return Commands.literal(axis ? "axis" : "entity").then(b);
|
||||
}
|
||||
|
||||
private static ArgumentBuilder<CommandSourceStack, ?> wrapRotationWithGlobality(final boolean axis, final boolean global) {
|
||||
return Commands.literal(global ? "global" : "local").executes((ctx) ->
|
||||
SablePhysicsCommands.executeRotationCommand(ctx, true, axis, global));
|
||||
}
|
||||
|
||||
private static int executeRotationCommand(final CommandContext<CommandSourceStack> ctx, final boolean add, final boolean axis, final boolean global) throws CommandSyntaxException {
|
||||
final PhysicsPipeline pipeline = SableCommandHelper.requireSubLevelPhysicsPipeline(ctx);
|
||||
|
||||
final Quaterniond orientation = new Quaterniond();
|
||||
|
||||
Vec2 rotation2 = new Vec2(0, 0);
|
||||
Vec3 rotationAxis = new Vec3(0, 0, 0);
|
||||
double rotationAngle = 0;
|
||||
|
||||
if (axis) {
|
||||
rotationAxis = ctx.getArgument("axis", Vec3.class);
|
||||
rotationAngle = ctx.getArgument("angle", Double.class);
|
||||
orientation.fromAxisAngleDeg(rotationAxis.x, rotationAxis.y, rotationAxis.z, rotationAngle);
|
||||
|
||||
if (rotationAxis.lengthSqr() == 0) {
|
||||
throw SableCommandHelper.ERROR_NO_AXIS_FOR_ROTATION.create();
|
||||
}
|
||||
} else {
|
||||
rotation2 = RotationArgument.getRotation(ctx, "rotation").getRotation(ctx.getSource());
|
||||
orientation.rotateY(-Math.toRadians(rotation2.y));
|
||||
orientation.rotateX(Math.toRadians(rotation2.x));
|
||||
}
|
||||
|
||||
final Collection<ServerSubLevel> subLevels = SubLevelArgumentType.getSubLevels(ctx, "sub_level");
|
||||
|
||||
if (subLevels.isEmpty()) {
|
||||
throw SableCommandHelper.ERROR_NO_SUB_LEVELS_FOUND.create();
|
||||
}
|
||||
|
||||
for (final ServerSubLevel subLevel : subLevels) {
|
||||
final Pose3d pose = subLevel.logicalPose();
|
||||
if (add) {
|
||||
if (global) {
|
||||
pose.orientation().premul(orientation);
|
||||
} else {
|
||||
pose.orientation().mul(orientation);
|
||||
}
|
||||
} else {
|
||||
pose.orientation().set(orientation);
|
||||
}
|
||||
pipeline.teleport(subLevel, pose.position(), pose.orientation());
|
||||
}
|
||||
|
||||
if (axis) {
|
||||
SableCommandHelper.sendSuccessDescribingSubLevelsAtIndex(
|
||||
add ? "commands.sable.physics.rotation.add.success"
|
||||
: "commands.sable.physics.rotation.set.success",
|
||||
ctx, subLevels, 1,
|
||||
getGlobalComponent(global), rotationAxis.x + ", " + rotationAxis.y + ", " + rotationAxis.z + ", " + rotationAngle);
|
||||
} else {
|
||||
SableCommandHelper.sendSuccessDescribingSubLevelsAtIndex(
|
||||
add ? "commands.sable.physics.rotation.add.success"
|
||||
: "commands.sable.physics.rotation.set.success",
|
||||
ctx, subLevels, 1,
|
||||
getGlobalComponent(global), rotation2.x + ", " + rotation2.y);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int executeAddTranslationCommand(final CommandContext<CommandSourceStack> ctx, final boolean global) throws CommandSyntaxException {
|
||||
final PhysicsPipeline pipeline = SableCommandHelper.requireSubLevelPhysicsPipeline(ctx);
|
||||
|
||||
final Collection<ServerSubLevel> subLevels = SubLevelArgumentType.getSubLevels(ctx, "sub_level");
|
||||
|
||||
if (subLevels.isEmpty()) {
|
||||
throw SableCommandHelper.ERROR_NO_SUB_LEVELS_FOUND.create();
|
||||
}
|
||||
|
||||
final Vec3 translation = ctx.getArgument("translation", Vec3.class);
|
||||
final Vector3d sublevelTranslation = new Vector3d();
|
||||
for (final ServerSubLevel subLevel : subLevels) {
|
||||
JOMLConversion.toJOML(translation, sublevelTranslation);
|
||||
|
||||
if (!global) {
|
||||
subLevel.logicalPose().transformNormal(sublevelTranslation);
|
||||
}
|
||||
|
||||
pipeline.teleport(subLevel, subLevel.logicalPose().position().add(sublevelTranslation), subLevel.logicalPose().orientation());
|
||||
}
|
||||
|
||||
SableCommandHelper.sendSuccessDescribingSubLevelsAtIndex("commands.sable.physics.translation.add.success", ctx, subLevels, 1,
|
||||
getGlobalComponent(global), translation.x + ", " + translation.y + ", " + translation.z);
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int executeSetTranslationCommand(final CommandContext<CommandSourceStack> ctx) throws CommandSyntaxException {
|
||||
final PhysicsPipeline pipeline = SableCommandHelper.requireSubLevelPhysicsPipeline(ctx);
|
||||
|
||||
final Collection<ServerSubLevel> subLevels = SubLevelArgumentType.getSubLevels(ctx, "sub_level");
|
||||
|
||||
if (subLevels.isEmpty()) {
|
||||
throw SableCommandHelper.ERROR_NO_SUB_LEVELS_FOUND.create();
|
||||
}
|
||||
|
||||
final Vector3d translation = JOMLConversion.toJOML(Vec3Argument.getVec3(ctx, "translation"));
|
||||
for (final ServerSubLevel subLevel : subLevels) {
|
||||
pipeline.teleport(subLevel, translation, subLevel.logicalPose().orientation());
|
||||
}
|
||||
|
||||
SableCommandHelper.sendSuccessDescribingSubLevels("commands.sable.physics.translation.set.success", ctx, subLevels, translation.x + ", " + translation.y + ", " + translation.z);
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,523 @@
|
||||
package dev.ryanhcode.sable.command;
|
||||
|
||||
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.builder.ArgumentBuilder;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.suggestion.SuggestionProvider;
|
||||
import dev.ryanhcode.sable.api.command.SableCommandHelper;
|
||||
import dev.ryanhcode.sable.api.command.SubLevelArgumentType;
|
||||
import dev.ryanhcode.sable.api.physics.constraint.PhysicsConstraintHandle;
|
||||
import dev.ryanhcode.sable.api.physics.constraint.rotary.RotaryConstraintConfiguration;
|
||||
import dev.ryanhcode.sable.api.physics.object.rope.RopeHandle;
|
||||
import dev.ryanhcode.sable.api.physics.object.rope.RopePhysicsObject;
|
||||
import dev.ryanhcode.sable.api.sublevel.ServerSubLevelContainer;
|
||||
import dev.ryanhcode.sable.api.sublevel.SubLevelContainer;
|
||||
import dev.ryanhcode.sable.companion.math.BoundingBox3dc;
|
||||
import dev.ryanhcode.sable.companion.math.JOMLConversion;
|
||||
import dev.ryanhcode.sable.companion.math.Pose3d;
|
||||
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.plot.EmbeddedPlotLevelAccessor;
|
||||
import dev.ryanhcode.sable.sublevel.plot.LevelPlot;
|
||||
import dev.ryanhcode.sable.sublevel.plot.ServerLevelPlot;
|
||||
import dev.ryanhcode.sable.sublevel.system.SubLevelPhysicsSystem;
|
||||
import dev.ryanhcode.sable.util.SchematicLoader;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
import net.minecraft.commands.CommandBuildContext;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.commands.SharedSuggestionProvider;
|
||||
import net.minecraft.commands.arguments.blocks.BlockStateArgument;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.Mirror;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Quaterniond;
|
||||
import org.joml.Vector3d;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Locale;
|
||||
|
||||
public class SableSpawnCommands {
|
||||
|
||||
private static final SuggestionProvider<CommandSourceStack> SUGGEST_TEMPLATES = (commandContext, suggestionsBuilder) -> {
|
||||
final MinecraftServer server = commandContext.getSource().getServer();
|
||||
return SchematicLoader.getSchematics(server).thenCompose(schematics -> {
|
||||
final String remaining = suggestionsBuilder.getRemaining().toLowerCase(Locale.ROOT);
|
||||
SharedSuggestionProvider.filterResources(schematics, remaining, resourceLocation -> resourceLocation, resourceLocation -> {
|
||||
final String path = resourceLocation.getPath();
|
||||
suggestionsBuilder.suggest(path.substring("schematics/".length(), path.length() - ".nbt".length()));
|
||||
});
|
||||
return suggestionsBuilder.buildFuture();
|
||||
});
|
||||
};
|
||||
|
||||
private static final BlockState DEFAULT_SPAWN_BLOCKSTATE = Blocks.STONE.defaultBlockState();
|
||||
|
||||
/**
|
||||
* Adds the following commands:
|
||||
* <ul>
|
||||
* <li>{@code /sable spawn jenga <height> [name]}</li>
|
||||
* <li>{@code /sable spawn <grid|platform|sphere> <size> [block] [name]}</li>
|
||||
* <li>{@code /sable spawn block <block> [name]}</li>
|
||||
* <li>{@code /sable spawn clone <selector> [name]}</li>
|
||||
* <li>{@code /sable spawn schematic <schematic>}</li>
|
||||
* <li>{@code /sable spawn <joint_test|rope_test>}</li>
|
||||
* <li>{@code /sable spawn <slope_test> [name]}</li>
|
||||
* </ul>
|
||||
*/
|
||||
public static void register(final LiteralArgumentBuilder<CommandSourceStack> sableBuilder, final CommandBuildContext buildContext) {
|
||||
|
||||
sableBuilder.then(Commands.literal("spawn")
|
||||
.then(Commands.literal("jenga")
|
||||
.then(namedSpawnFinale(Commands.argument("height", IntegerArgumentType.integer(1, 256)), SableSpawnCommands::spawnJenga)))
|
||||
|
||||
.then(Commands.literal("clone")
|
||||
.then(namedSpawnFinale(Commands.argument("sub_level", SubLevelArgumentType.singleSubLevel()), SableSpawnCommands::cloneSubLevel)))
|
||||
|
||||
.then(Commands.literal("sphere")
|
||||
.then(Commands.argument("radius", IntegerArgumentType.integer(2, 200))
|
||||
.executes((ctx) -> SableSpawnCommands.spawnSphere(ctx, DEFAULT_SPAWN_BLOCKSTATE, null))
|
||||
.then(namedSpawnFinale(Commands.argument("block", BlockStateArgument.block(buildContext)),
|
||||
(ctx, name) -> SableSpawnCommands.spawnSphere(ctx, BlockStateArgument.getBlock(ctx, "block").getState(), name)))))
|
||||
|
||||
.then(Commands.literal("schematic")
|
||||
.then(Commands.argument("name", StringArgumentType.string())
|
||||
.suggests(SUGGEST_TEMPLATES)
|
||||
.executes(SableSpawnCommands::executeSpawnSchematicCommand)))
|
||||
|
||||
.then(Commands.literal("joint_test")
|
||||
.executes(SableSpawnCommands::executeSpawnJointTestCommand))
|
||||
|
||||
.then(namedSpawnFinale(Commands.literal("slope_test"), SableSpawnCommands::spawnSlopeTest))
|
||||
|
||||
.then(Commands.literal("rope_test")
|
||||
.executes(SableSpawnCommands::executeSpawnRopeTestCommand))
|
||||
|
||||
.then(Commands.literal("grid")
|
||||
.then(Commands.argument("sideLength", IntegerArgumentType.integer(1, 32))
|
||||
.executes((ctx) -> SableSpawnCommands.spawnGrid(ctx, DEFAULT_SPAWN_BLOCKSTATE, null))
|
||||
.then(namedSpawnFinale(Commands.argument("block", BlockStateArgument.block(buildContext)),
|
||||
(ctx, name) -> SableSpawnCommands.spawnGrid(ctx, BlockStateArgument.getBlock(ctx, "block").getState(), name)))))
|
||||
|
||||
.then(Commands.literal("block")
|
||||
.executes((ctx) -> spawnBlock(ctx, DEFAULT_SPAWN_BLOCKSTATE, null))
|
||||
.then(namedSpawnFinale(Commands.argument("block", BlockStateArgument.block(buildContext)),
|
||||
(ctx, name) -> spawnBlock(ctx, BlockStateArgument.getBlock(ctx, "block").getState(), name))))
|
||||
|
||||
.then(Commands.literal("platform")
|
||||
.then(Commands.argument("size", IntegerArgumentType.integer(1, 32))
|
||||
.executes((ctx) -> SableSpawnCommands.spawnPlatform(ctx, DEFAULT_SPAWN_BLOCKSTATE, null))
|
||||
.then(namedSpawnFinale(Commands.argument("block", BlockStateArgument.block(buildContext)),
|
||||
(ctx, name) -> SableSpawnCommands.spawnPlatform(ctx, BlockStateArgument.getBlock(ctx, "block").getState(), name)))))
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface NamedSpawnInvoker<S> {
|
||||
int run(CommandContext<S> ctx, @Nullable String name) throws CommandSyntaxException;
|
||||
}
|
||||
|
||||
private static <T extends ArgumentBuilder<CommandSourceStack, T>> T namedSpawnFinale(final T builder, final NamedSpawnInvoker<CommandSourceStack> invoker) {
|
||||
builder.executes((ctx) -> invoker.run(ctx, null));
|
||||
builder.then(Commands.argument("name", StringArgumentType.string())
|
||||
.executes((ctx) -> invoker.run(ctx, StringArgumentType.getString(ctx, "name"))));
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static int spawnJenga(final CommandContext<CommandSourceStack> ctx, @Nullable final String name) throws CommandSyntaxException {
|
||||
final CommandSourceStack source = ctx.getSource();
|
||||
final SubLevelContainer container = SableCommandHelper.requireSubLevelContainer(ctx);
|
||||
final Vec3 pos = Vec3.atCenterOf(BlockPos.containing(source.getPosition()));
|
||||
final int height = IntegerArgumentType.getInteger(ctx, "height");
|
||||
|
||||
for (int yOffset = 0; yOffset < height; yOffset++) {
|
||||
final Direction.Axis axis = yOffset % 2 == 0 ? Direction.Axis.X : Direction.Axis.Z;
|
||||
final Direction.Axis perpendicular = axis == Direction.Axis.X ? Direction.Axis.Z : Direction.Axis.X;
|
||||
|
||||
for (int index = -1; index <= 1; index++) {
|
||||
final Pose3d pose = new Pose3d();
|
||||
final Vector3d position = pose.position();
|
||||
position.set(pos.x, pos.y, pos.z);
|
||||
|
||||
if (index != 0) {
|
||||
position.add(JOMLConversion.atLowerCornerOf(Direction.get(index == 1 ? Direction.AxisDirection.POSITIVE : Direction.AxisDirection.NEGATIVE, axis).getNormal()));
|
||||
}
|
||||
position.add(0.0, yOffset, 0.0);
|
||||
final Vector3d positionBackup = new Vector3d(position);
|
||||
|
||||
final SubLevel subLevel = container.allocateNewSubLevel(pose);
|
||||
subLevel.setName(name);
|
||||
final LevelPlot plot = subLevel.getPlot();
|
||||
|
||||
final ChunkPos center = plot.getCenterChunk();
|
||||
plot.newEmptyChunk(center);
|
||||
|
||||
|
||||
final EmbeddedPlotLevelAccessor accessor = plot.getEmbeddedLevelAccessor();
|
||||
accessor.setBlock(BlockPos.ZERO, Blocks.SPRUCE_PLANKS.defaultBlockState(), 3);
|
||||
for (int block = -1; block <= 1; block++) {
|
||||
final BlockPos blockPos = BlockPos.ZERO.relative(Direction.get(Direction.AxisDirection.POSITIVE, perpendicular), block);
|
||||
|
||||
BlockState state = Blocks.OAK_PLANKS.defaultBlockState();
|
||||
|
||||
if (index == 0) {
|
||||
state = Blocks.SPRUCE_PLANKS.defaultBlockState();
|
||||
}
|
||||
|
||||
accessor.setBlock(blockPos, state, 3);
|
||||
}
|
||||
subLevel.logicalPose().position().set(positionBackup);
|
||||
subLevel.updateLastPose();
|
||||
}
|
||||
}
|
||||
|
||||
source.sendSuccess(() -> Component.translatable("commands.sable.spawn.success", "jenga"), false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int cloneSubLevel(final CommandContext<CommandSourceStack> ctx, @Nullable final String name) throws CommandSyntaxException {
|
||||
final CommandSourceStack source = ctx.getSource();
|
||||
final ServerSubLevelContainer plotContainer = SableCommandHelper.requireSubLevelContainer(ctx);
|
||||
final ServerSubLevel toClone = SubLevelArgumentType.getSingleSubLevel(ctx, "sub_level");
|
||||
|
||||
final BoundingBox3dc worldBounds = toClone.boundingBox();
|
||||
final double height = worldBounds.maxY() - worldBounds.minY();
|
||||
|
||||
final CompoundTag tag = toClone.getPlot().save();
|
||||
|
||||
final ServerSubLevel subLevel = (ServerSubLevel) plotContainer.allocateNewSubLevel(
|
||||
new Pose3d(
|
||||
toClone.logicalPose().position().add(0, height * 1.2 + 2, 0, new Vector3d()),
|
||||
new Quaterniond(), new Vector3d(0), new Vector3d(1)
|
||||
)
|
||||
);
|
||||
final ServerLevelPlot plot = subLevel.getPlot();
|
||||
plot.load(tag);
|
||||
subLevel.updateLastPose();
|
||||
if (name != null) {
|
||||
subLevel.setName(name);
|
||||
}
|
||||
|
||||
source.sendSuccess(() -> Component.translatable("commands.sable.spawn.clone.success"), false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int spawnSphere(final CommandContext<CommandSourceStack> ctx, final BlockState material, @Nullable final String name) throws CommandSyntaxException {
|
||||
final CommandSourceStack source = ctx.getSource();
|
||||
|
||||
final SubLevelContainer plotContainer = SableCommandHelper.requireSubLevelContainer(ctx);
|
||||
|
||||
Vec3 playerPos = source.getPosition();
|
||||
playerPos = Vec3.atCenterOf(BlockPos.containing(playerPos));
|
||||
|
||||
final Pose3d pose = new Pose3d();
|
||||
pose.position().set(playerPos.x, playerPos.y, playerPos.z);
|
||||
|
||||
final SubLevel subLevel = plotContainer.allocateNewSubLevel(pose);
|
||||
subLevel.setName(name);
|
||||
|
||||
final LevelPlot plot = subLevel.getPlot();
|
||||
|
||||
final ChunkPos center = plot.getCenterChunk();
|
||||
|
||||
final int radius = IntegerArgumentType.getInteger(ctx, "radius");
|
||||
final int radiusChunks = (radius + 8) / 16;
|
||||
for (int x = -radiusChunks; x <= radiusChunks; x++) {
|
||||
for (int z = -radiusChunks; z <= radiusChunks; z++) {
|
||||
plot.newEmptyChunk(new ChunkPos(center.x + x, center.z + z));
|
||||
}
|
||||
}
|
||||
|
||||
final BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
for (int x = -radius; x <= radius; x++) {
|
||||
for (int z = -radius; z <= radius; z++) {
|
||||
for (int y = -radius; y <= radius; y++) {
|
||||
pos.set(x, y, z);
|
||||
if (pos.distSqr(BlockPos.ZERO) <= radius * radius) {
|
||||
plot.getEmbeddedLevelAccessor().setBlock(pos, material, 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
subLevel.updateLastPose();
|
||||
|
||||
source.sendSuccess(() -> Component.translatable("commands.sable.spawn.success", "sphere"), false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int executeSpawnSchematicCommand(final CommandContext<CommandSourceStack> ctx) throws CommandSyntaxException {
|
||||
final CommandSourceStack source = ctx.getSource();
|
||||
final ServerLevel level = source.getLevel();
|
||||
|
||||
final StructureTemplate template = SchematicLoader.loadSchematic(level, ResourceLocation.fromNamespaceAndPath("sable", StringArgumentType.getString(ctx, "name")));
|
||||
|
||||
if (template == null) {
|
||||
source.sendFailure(Component.translatable("commands.sable.place_schematic.failure"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
final SubLevelContainer plotContainer = SableCommandHelper.requireSubLevelContainer(ctx);
|
||||
|
||||
final Vec3 spawnPos = source.getPosition();
|
||||
|
||||
final Pose3d pose = new Pose3d();
|
||||
pose.position().set(spawnPos.x, spawnPos.y, spawnPos.z);
|
||||
|
||||
final SubLevel sublevel = plotContainer.allocateNewSubLevel(pose);
|
||||
final LevelPlot plot = sublevel.getPlot();
|
||||
|
||||
final ChunkPos center = plot.getCenterChunk();
|
||||
|
||||
final BoundingBox bounds = template.getBoundingBox(BlockPos.ZERO, Rotation.NONE, BlockPos.ZERO, Mirror.NONE);
|
||||
|
||||
final int minChunkX = bounds.minX() >> 4;
|
||||
final int minChunkZ = bounds.minZ() >> 4;
|
||||
|
||||
final int maxChunkX = bounds.maxX() >> 4;
|
||||
final int maxChunkZ = bounds.maxZ() >> 4;
|
||||
|
||||
for (int x = minChunkX; x <= maxChunkX; x++) {
|
||||
for (int z = minChunkZ; z <= maxChunkZ; z++) {
|
||||
plot.newEmptyChunk(new ChunkPos(center.x + x, center.z + z));
|
||||
}
|
||||
}
|
||||
|
||||
final EmbeddedPlotLevelAccessor embedded = plot.getEmbeddedLevelAccessor();
|
||||
template.placeInWorld(embedded, BlockPos.ZERO, BlockPos.ZERO, new StructurePlaceSettings(), RandomSource.create(), 3);
|
||||
sublevel.updateLastPose();
|
||||
sublevel.logicalPose().position().set(spawnPos.x, spawnPos.y, spawnPos.z);
|
||||
|
||||
source.sendSuccess(() -> Component.translatable("commands.sable.place_schematic.success"), false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int executeSpawnRopeTestCommand(final CommandContext<CommandSourceStack> ctx) throws CommandSyntaxException {
|
||||
final CommandSourceStack source = ctx.getSource();
|
||||
|
||||
final ServerSubLevelContainer plotContainer = SableCommandHelper.requireSubLevelContainer(ctx);
|
||||
final SubLevelPhysicsSystem system = SableCommandHelper.requireSubLevelPhysicsSystem(plotContainer);
|
||||
|
||||
final Vec3 playerPos = Vec3.atCenterOf(BlockPos.containing(source.getPosition()));
|
||||
final Collection<Vector3d> points = new ObjectArrayList<>();
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
points.add(JOMLConversion.toJOML(playerPos).add(i, 0, 0));
|
||||
}
|
||||
|
||||
final RopePhysicsObject object = new RopePhysicsObject(points, 0.25);
|
||||
system.addObject(object);
|
||||
object.setAttachment(RopeHandle.AttachmentPoint.START, JOMLConversion.toJOML(playerPos), null);
|
||||
|
||||
source.sendSuccess(() -> Component.translatable("commands.sable.spawn.success", "rope_test"), false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
private static int executeSpawnJointTestCommand(final CommandContext<CommandSourceStack> ctx) throws CommandSyntaxException {
|
||||
final CommandSourceStack source = ctx.getSource();
|
||||
|
||||
final ServerSubLevelContainer plotContainer = SableCommandHelper.requireSubLevelContainer(ctx);
|
||||
|
||||
final Vec3 playerPos = Vec3.atCenterOf(BlockPos.containing(source.getPosition()));
|
||||
|
||||
final Pose3d pose1 = new Pose3d();
|
||||
pose1.position().set(playerPos.x, playerPos.y, playerPos.z);
|
||||
|
||||
final Pose3d pose2 = new Pose3d();
|
||||
pose2.position().set(playerPos.x, playerPos.y + 1.0, playerPos.z);
|
||||
|
||||
final ServerSubLevel subLevelA = (ServerSubLevel) plotContainer.allocateNewSubLevel(pose1);
|
||||
final ServerSubLevel subLevelB = (ServerSubLevel) plotContainer.allocateNewSubLevel(pose2);
|
||||
|
||||
final LevelPlot plotA = subLevelA.getPlot();
|
||||
final LevelPlot plotB = subLevelB.getPlot();
|
||||
|
||||
plotA.newEmptyChunk(plotA.getCenterChunk());
|
||||
plotA.getEmbeddedLevelAccessor().setBlock(BlockPos.ZERO, Blocks.STONE.defaultBlockState(), 3);
|
||||
|
||||
plotB.newEmptyChunk(plotB.getCenterChunk());
|
||||
plotB.getEmbeddedLevelAccessor().setBlock(BlockPos.ZERO, Blocks.STONE.defaultBlockState(), 3);
|
||||
|
||||
final RotaryConstraintConfiguration config = new RotaryConstraintConfiguration(
|
||||
JOMLConversion.atBottomCenterOf(plotA.getCenterBlock().above().above()),
|
||||
JOMLConversion.atBottomCenterOf(plotB.getCenterBlock()),
|
||||
JOMLConversion.atLowerCornerOf(Direction.UP.getNormal()),
|
||||
JOMLConversion.atLowerCornerOf(Direction.UP.getNormal())
|
||||
);
|
||||
// final FreeConstraintConfiguration config = new FreeConstraintConfiguration();
|
||||
|
||||
final PhysicsConstraintHandle handle = SableCommandHelper.requireSubLevelPhysicsSystem(plotContainer)
|
||||
.getPipeline().addConstraint(subLevelA, subLevelB, config);
|
||||
|
||||
handle.setContactsEnabled(false);
|
||||
source.sendSuccess(() -> Component.translatable("commands.sable.spawn.success", "joint_test"), false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int spawnSlopeTest(final CommandContext<CommandSourceStack> ctx, final @Nullable String name) throws CommandSyntaxException {
|
||||
final CommandSourceStack source = ctx.getSource();
|
||||
|
||||
final ServerSubLevelContainer plotContainer = SableCommandHelper.requireSubLevelContainer(ctx);
|
||||
|
||||
final Vec3 playerPos = Vec3.atCenterOf(BlockPos.containing(source.getPosition()));
|
||||
|
||||
final int gridSize = 9;
|
||||
final double yawRange = Math.toRadians(90.0);
|
||||
final double pitchRange = Math.toRadians(90.00);
|
||||
final int rad = 3;
|
||||
|
||||
final int spacing = rad * 2 + 2;
|
||||
for (int xo = 0; xo <= gridSize; xo++) {
|
||||
for (int zo = 0; zo <= gridSize; zo++) {
|
||||
|
||||
final Pose3d pose1 = new Pose3d();
|
||||
pose1.position().set(playerPos.x, playerPos.y, playerPos.z);
|
||||
|
||||
final ServerSubLevel subLevel = (ServerSubLevel) plotContainer.allocateNewSubLevel(pose1);
|
||||
subLevel.setName(name);
|
||||
|
||||
final LevelPlot plotA = subLevel.getPlot();
|
||||
|
||||
final BlockState block = Blocks.END_STONE.defaultBlockState();
|
||||
plotA.newEmptyChunk(plotA.getCenterChunk());
|
||||
|
||||
for (int lx = -rad; lx < rad; lx ++) {
|
||||
for (int lz = -rad; lz < rad; lz++) {
|
||||
plotA.getEmbeddedLevelAccessor().setBlock(new BlockPos(lx, 0, lz), block, 3);
|
||||
}
|
||||
}
|
||||
|
||||
final Vector3d pos = new Vector3d(playerPos.x + xo * spacing, playerPos.y, playerPos.z + zo * spacing);
|
||||
final Quaterniond orientation = new Quaterniond();
|
||||
|
||||
orientation.rotateY(xo * yawRange / gridSize);
|
||||
orientation.rotateX(zo * pitchRange / gridSize);
|
||||
|
||||
SableCommandHelper.requireSubLevelPhysicsPipeline(ctx).teleport(subLevel, pos, orientation);
|
||||
}
|
||||
}
|
||||
source.sendSuccess(() -> Component.translatable("commands.sable.spawn.success", "slope_test"), false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int spawnGrid(final CommandContext<CommandSourceStack> ctx, final BlockState material, final @Nullable String name) throws CommandSyntaxException {
|
||||
final CommandSourceStack source = ctx.getSource();
|
||||
|
||||
final SubLevelContainer plotContainer = SableCommandHelper.requireSubLevelContainer(ctx);
|
||||
|
||||
final Vec3 playerPos = source.getPosition();
|
||||
|
||||
final int sideLength = IntegerArgumentType.getInteger(ctx, "sideLength");
|
||||
|
||||
final Vec3[] positions = new Vec3[sideLength * sideLength * sideLength];
|
||||
|
||||
for (int x = 0; x < sideLength; x++) {
|
||||
for (int z = 0; z < sideLength; z++) {
|
||||
for (int y = 0; y < sideLength; y++) {
|
||||
positions[x * sideLength * sideLength + z * sideLength + y] = new Vec3(x, y, z).scale(2.1).add(playerPos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final Vec3 subLevelPos : positions) {
|
||||
final Pose3d pose = new Pose3d();
|
||||
pose.position().set(subLevelPos.x, subLevelPos.y, subLevelPos.z);
|
||||
|
||||
final SubLevel subLevel = plotContainer.allocateNewSubLevel(pose);
|
||||
subLevel.setName(name);
|
||||
final LevelPlot plot = subLevel.getPlot();
|
||||
|
||||
final ChunkPos center = plot.getCenterChunk();
|
||||
plot.newEmptyChunk(center);
|
||||
|
||||
plot.getEmbeddedLevelAccessor().setBlock(BlockPos.ZERO, material, 3);
|
||||
subLevel.updateLastPose();
|
||||
}
|
||||
|
||||
source.sendSuccess(() -> Component.translatable("commands.sable.spawn.success", "grid"), false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int spawnBlock(final CommandContext<CommandSourceStack> ctx, final BlockState material, final @Nullable String name) throws CommandSyntaxException {
|
||||
final CommandSourceStack source = ctx.getSource();
|
||||
|
||||
final SubLevelContainer plotContainer = SableCommandHelper.requireSubLevelContainer(ctx);
|
||||
|
||||
final Vec3 playerPos = source.getPosition();
|
||||
|
||||
final Pose3d pose = new Pose3d();
|
||||
pose.position().set(playerPos.x, playerPos.y, playerPos.z);
|
||||
|
||||
final SubLevel subLevel = plotContainer.allocateNewSubLevel(pose);
|
||||
subLevel.setName(name);
|
||||
final LevelPlot plot = subLevel.getPlot();
|
||||
|
||||
final ChunkPos center = plot.getCenterChunk();
|
||||
plot.newEmptyChunk(center);
|
||||
|
||||
plot.getEmbeddedLevelAccessor().setBlock(BlockPos.ZERO, material, 3);
|
||||
subLevel.updateLastPose();
|
||||
|
||||
source.sendSuccess(() -> Component.translatable("commands.sable.spawn.success", "block"), false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int spawnPlatform(final CommandContext<CommandSourceStack> ctx, final BlockState material, final @Nullable String name) throws CommandSyntaxException {
|
||||
final CommandSourceStack source = ctx.getSource();
|
||||
|
||||
final SubLevelContainer plotContainer = SableCommandHelper.requireSubLevelContainer(ctx);
|
||||
|
||||
final Vec3 playerPos = source.getPosition();
|
||||
|
||||
final Pose3d pose = new Pose3d();
|
||||
pose.position().set(playerPos.x, playerPos.y, playerPos.z);
|
||||
|
||||
final SubLevel subLevel = plotContainer.allocateNewSubLevel(pose);
|
||||
subLevel.setName(name);
|
||||
final LevelPlot plot = subLevel.getPlot();
|
||||
|
||||
final ChunkPos center = plot.getCenterChunk();
|
||||
|
||||
final int size = IntegerArgumentType.getInteger(ctx, "size");
|
||||
final int radiusChunks = (size + 8) / 16;
|
||||
for (int x = -radiusChunks; x <= radiusChunks; x++) {
|
||||
for (int z = -radiusChunks; z <= radiusChunks; z++) {
|
||||
plot.newEmptyChunk(new ChunkPos(center.x + x, center.z + z));
|
||||
}
|
||||
}
|
||||
for (int x = -size; x <= size; x++) {
|
||||
for (int z = -size; z <= size; z++) {
|
||||
plot.getEmbeddedLevelAccessor().setBlock(new BlockPos(x, 0, z), material, 2);
|
||||
}
|
||||
}
|
||||
subLevel.updateLastPose();
|
||||
SableCommandHelper.requireSubLevelPhysicsPipeline(ctx).teleport(
|
||||
(ServerSubLevel) subLevel,
|
||||
new Vector3d(playerPos.x, playerPos.y, playerPos.z),
|
||||
pose.orientation()
|
||||
);
|
||||
|
||||
source.sendSuccess(() -> Component.translatable("commands.sable.spawn.success", "platform"), false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package dev.ryanhcode.sable.command;
|
||||
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import dev.ryanhcode.sable.api.sublevel.ServerSubLevelContainer;
|
||||
import dev.ryanhcode.sable.companion.math.Pose3d;
|
||||
import dev.ryanhcode.sable.sublevel.storage.holding.GlobalSavedSubLevelPointer;
|
||||
import dev.ryanhcode.sable.sublevel.storage.holding.SavedSubLevelPointer;
|
||||
import dev.ryanhcode.sable.sublevel.storage.holding.SubLevelHoldingChunk;
|
||||
import dev.ryanhcode.sable.sublevel.storage.holding.SubLevelHoldingChunkMap;
|
||||
import dev.ryanhcode.sable.sublevel.storage.region.SubLevelRegionFile;
|
||||
import dev.ryanhcode.sable.sublevel.storage.serialization.SubLevelData;
|
||||
import dev.ryanhcode.sable.sublevel.storage.serialization.SubLevelStorage;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.commands.CommandBuildContext;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.network.chat.*;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import org.joml.Vector3d;
|
||||
import org.joml.Vector3dc;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Formatter;
|
||||
import java.util.Locale;
|
||||
|
||||
public class SableStorageCommands {
|
||||
|
||||
public static void register(final LiteralArgumentBuilder<CommandSourceStack> sableBuilder, final CommandBuildContext buildContext) {
|
||||
sableBuilder.then(Commands.literal("storage")
|
||||
.then(Commands.literal("find_all_sub_levels")
|
||||
.executes(ctx -> {
|
||||
final ServerLevel level = ctx.getSource().getLevel();
|
||||
final ServerSubLevelContainer container = ServerSubLevelContainer.getContainer(level);
|
||||
final SubLevelHoldingChunkMap holdingChunkMap = container.getHoldingChunkMap();
|
||||
final SubLevelStorage storage = holdingChunkMap.getStorage();
|
||||
final CommandSourceStack source = ctx.getSource();
|
||||
|
||||
final File[] regionFiles = storage.getFolder().toFile().listFiles((dir, name) -> name.endsWith(SubLevelRegionFile.FILE_EXTENSION));
|
||||
|
||||
if (regionFiles != null) {
|
||||
for (final File regionFile : regionFiles) {
|
||||
final String fileName = regionFile.getName();
|
||||
final String withoutExtension = fileName.substring(0, fileName.length() - SubLevelRegionFile.FILE_EXTENSION.length());
|
||||
final String[] parts = withoutExtension.split("\\.");
|
||||
if (parts.length != 3) continue;
|
||||
|
||||
final int regionX, regionZ;
|
||||
try {
|
||||
regionX = Integer.parseInt(parts[1]);
|
||||
regionZ = Integer.parseInt(parts[2]);
|
||||
} catch (final NumberFormatException e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int localX = 0; localX < SubLevelRegionFile.SIDE_LENGTH; localX++) {
|
||||
for (int localZ = 0; localZ < SubLevelRegionFile.SIDE_LENGTH; localZ++) {
|
||||
final ChunkPos chunkPos = new ChunkPos(
|
||||
regionX * SubLevelRegionFile.SIDE_LENGTH + localX,
|
||||
regionZ * SubLevelRegionFile.SIDE_LENGTH + localZ
|
||||
);
|
||||
|
||||
final SubLevelHoldingChunk holdingChunk = storage.attemptLoadHoldingChunk(chunkPos);
|
||||
if (holdingChunk == null) continue;
|
||||
|
||||
for (final SavedSubLevelPointer pointer : holdingChunk.getSubLevelPointers()) {
|
||||
final SubLevelData data = storage.attemptLoadSubLevel(chunkPos, pointer);
|
||||
logFoundSubLevel(pointer, data, chunkPos, source, level);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}))
|
||||
.then(Commands.literal("find")
|
||||
.then(Commands.argument("name", StringArgumentType.string())
|
||||
.executes(ctx -> {
|
||||
final ServerLevel level = ctx.getSource().getLevel();
|
||||
final ServerSubLevelContainer container = ServerSubLevelContainer.getContainer(level);
|
||||
final SubLevelHoldingChunkMap holdingChunkMap = container.getHoldingChunkMap();
|
||||
final SubLevelStorage storage = holdingChunkMap.getStorage();
|
||||
final CommandSourceStack source = ctx.getSource();
|
||||
final String nameArgument = StringArgumentType.getString(ctx, "name");
|
||||
|
||||
final File[] regionFiles = storage.getFolder().toFile().listFiles((dir, name) -> name.endsWith(SubLevelRegionFile.FILE_EXTENSION));
|
||||
|
||||
if (regionFiles != null) {
|
||||
for (final File regionFile : regionFiles) {
|
||||
final String fileName = regionFile.getName();
|
||||
final String withoutExtension = fileName.substring(0, fileName.length() - SubLevelRegionFile.FILE_EXTENSION.length());
|
||||
final String[] parts = withoutExtension.split("\\.");
|
||||
if (parts.length != 3) continue;
|
||||
|
||||
final int regionX, regionZ;
|
||||
try {
|
||||
regionX = Integer.parseInt(parts[1]);
|
||||
regionZ = Integer.parseInt(parts[2]);
|
||||
} catch (final NumberFormatException e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int localX = 0; localX < SubLevelRegionFile.SIDE_LENGTH; localX++) {
|
||||
for (int localZ = 0; localZ < SubLevelRegionFile.SIDE_LENGTH; localZ++) {
|
||||
final ChunkPos chunkPos = new ChunkPos(
|
||||
regionX * SubLevelRegionFile.SIDE_LENGTH + localX,
|
||||
regionZ * SubLevelRegionFile.SIDE_LENGTH + localZ
|
||||
);
|
||||
|
||||
final SubLevelHoldingChunk holdingChunk = storage.attemptLoadHoldingChunk(chunkPos);
|
||||
if (holdingChunk == null) continue;
|
||||
|
||||
for (final SavedSubLevelPointer pointer : holdingChunk.getSubLevelPointers()) {
|
||||
final SubLevelData data = storage.attemptLoadSubLevel(chunkPos, pointer);
|
||||
|
||||
final String name = data.fullTag().contains("display_name")
|
||||
? data.fullTag().getString("display_name")
|
||||
: data.uuid().toString();
|
||||
if (name != null && name.equals(nameArgument)) {
|
||||
logFoundSubLevel(pointer, data, chunkPos, source, level);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
})))
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
private static void logFoundSubLevel(final SavedSubLevelPointer pointer, final SubLevelData data, final ChunkPos chunkPos, final CommandSourceStack source, final ServerLevel level) {
|
||||
if (data == null) return;
|
||||
|
||||
final String name = data.fullTag().contains("display_name")
|
||||
? data.fullTag().getString("display_name")
|
||||
: data.uuid().toString();
|
||||
final GlobalSavedSubLevelPointer globalPointer = new GlobalSavedSubLevelPointer(chunkPos, pointer.storageIndex(), pointer.subLevelIndex());
|
||||
|
||||
final Pose3d pose = data.pose();
|
||||
|
||||
source.sendSuccess(() -> {
|
||||
final Vector3dc pos = pose.position();
|
||||
final MutableComponent component = Component.translatable("commands.sable.info.name", Component.literal(name));
|
||||
final ResourceLocation dimension = level.dimension().location();
|
||||
final Component fileId = Component.translatable("commands.sable.info.name.tooltip", globalPointer.toString());
|
||||
component.setStyle(Style.EMPTY.withClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, new Formatter().format(Locale.ROOT, "/execute in %s run tp @s %.2f %.2f %.2f", dimension, pos.x(), pos.y(), pos.z()).toString()))
|
||||
.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, fileId))
|
||||
.withColor(ChatFormatting.GRAY));
|
||||
return component;
|
||||
}, false);
|
||||
|
||||
source.sendSuccess(() -> {
|
||||
final Vector3dc pos = pose.position();
|
||||
return Component.translatable("commands.sable.info.position", pos.x(), pos.y(), pos.z());
|
||||
}, false);
|
||||
|
||||
source.sendSuccess(() -> {
|
||||
final Vector3d size = data.bounds().size();
|
||||
return Component.translatable("commands.sable.info.world_bounds", size.x, size.y, size.z);
|
||||
}, false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package dev.ryanhcode.sable.command;
|
||||
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import dev.ryanhcode.sable.api.command.SableCommandHelper;
|
||||
import dev.ryanhcode.sable.api.command.SubLevelArgumentType;
|
||||
import dev.ryanhcode.sable.api.physics.PhysicsPipeline;
|
||||
import dev.ryanhcode.sable.api.sublevel.SubLevelContainer;
|
||||
import dev.ryanhcode.sable.companion.math.JOMLConversion;
|
||||
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.plot.LevelPlot;
|
||||
import dev.ryanhcode.sable.sublevel.storage.SubLevelRemovalReason;
|
||||
import net.minecraft.commands.CommandBuildContext;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.commands.arguments.coordinates.Coordinates;
|
||||
import net.minecraft.commands.arguments.coordinates.RotationArgument;
|
||||
import net.minecraft.commands.arguments.coordinates.Vec3Argument;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.phys.Vec2;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Quaterniond;
|
||||
import org.joml.Vector2i;
|
||||
import org.joml.Vector3d;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
|
||||
public class SableSubLevelCommands {
|
||||
|
||||
/**
|
||||
* Adds the following commands:
|
||||
* <ul>
|
||||
* <li>{@code /sable name set <targets> <name>}</li>
|
||||
* <li>{@code /sable name clear <targets>}</li>
|
||||
* <li>{@code /sable name get <target>}</li>
|
||||
* <li>{@code /sable teleport <targets> <destination>}</li>
|
||||
* <li>{@code /sable remove <targets>}</li>
|
||||
* </ul>
|
||||
*/
|
||||
public static void register(final LiteralArgumentBuilder<CommandSourceStack> sableBuilder, final CommandBuildContext buildContext) {
|
||||
sableBuilder
|
||||
.then(Commands.literal("name")
|
||||
.then(Commands.literal("set")
|
||||
.then(Commands.argument("sub_level", SubLevelArgumentType.subLevels())
|
||||
.then(Commands.argument("name", StringArgumentType.string())
|
||||
.executes(SableSubLevelCommands::executeSetSubLevelNameCommand))))
|
||||
.then(Commands.literal("clear")
|
||||
.then(Commands.argument("sub_level", SubLevelArgumentType.subLevels())
|
||||
.executes(SableSubLevelCommands::executeClearSubLevelNameCommand)))
|
||||
.then(Commands.literal("get")
|
||||
.then(Commands.argument("sub_level", SubLevelArgumentType.singleSubLevel())
|
||||
.executes(SableSubLevelCommands::executeGetSubLevelNameCommand))))
|
||||
|
||||
.then(Commands.literal("teleport")
|
||||
.then(Commands.argument("targets", SubLevelArgumentType.subLevels())
|
||||
.then(Commands.argument("destination", Vec3Argument.vec3(false))
|
||||
.executes((ctx) -> SableSubLevelCommands.executeTeleportSubLevelCommand(ctx, null))
|
||||
.then(Commands.argument("angle", RotationArgument.rotation())
|
||||
.executes((ctx) -> SableSubLevelCommands.executeTeleportSubLevelCommand(
|
||||
ctx, RotationArgument.getRotation(ctx, "angle"))
|
||||
))
|
||||
)))
|
||||
|
||||
.then(Commands.literal("remove")
|
||||
.then(Commands.argument("targets", SubLevelArgumentType.subLevels())
|
||||
.executes(SableSubLevelCommands::executeRemoveSubLevelCommand)));
|
||||
}
|
||||
|
||||
private static int setSubLevelNames(final Collection<ServerSubLevel> subLevels, @Nullable final String name) {
|
||||
int modifiedCount = 0;
|
||||
for (final SubLevel target : subLevels) {
|
||||
if (!Objects.equals(target.getName(), name)) {
|
||||
target.setName(name);
|
||||
modifiedCount++;
|
||||
}
|
||||
}
|
||||
return modifiedCount;
|
||||
}
|
||||
|
||||
private static int executeSetSubLevelNameCommand(final CommandContext<CommandSourceStack> ctx) throws CommandSyntaxException {
|
||||
final Collection<ServerSubLevel> subLevels = SubLevelArgumentType.getSubLevels(ctx, "sub_level");
|
||||
final String name = StringArgumentType.getString(ctx, "name");
|
||||
|
||||
if (subLevels.isEmpty()) {
|
||||
throw SableCommandHelper.ERROR_NO_SUB_LEVELS_FOUND.create();
|
||||
}
|
||||
|
||||
final int modifiedCount = setSubLevelNames(subLevels, name);
|
||||
|
||||
if (modifiedCount == 0) {
|
||||
throw SableCommandHelper.ERROR_NO_SUB_LEVELS_MODIFIED.create();
|
||||
}
|
||||
|
||||
if (modifiedCount == 1) {
|
||||
ctx.getSource().sendSuccess(() -> Component.translatable("commands.sable.sub_level.set_name.success_singular", name), true);
|
||||
} else {
|
||||
ctx.getSource().sendSuccess(() -> Component.translatable("commands.sable.sub_level.set_name.success_multiple", modifiedCount, name), true);
|
||||
}
|
||||
return modifiedCount;
|
||||
}
|
||||
|
||||
private static int executeClearSubLevelNameCommand(final CommandContext<CommandSourceStack> ctx) throws CommandSyntaxException {
|
||||
final Collection<ServerSubLevel> subLevels = SubLevelArgumentType.getSubLevels(ctx, "sub_level");
|
||||
|
||||
if (subLevels.isEmpty()) {
|
||||
throw SableCommandHelper.ERROR_NO_SUB_LEVELS_FOUND.create();
|
||||
}
|
||||
|
||||
final int modifiedCount = setSubLevelNames(subLevels, null);
|
||||
|
||||
if (modifiedCount == 0) {
|
||||
throw SableCommandHelper.ERROR_NO_SUB_LEVELS_MODIFIED.create();
|
||||
}
|
||||
|
||||
if (modifiedCount == 1) {
|
||||
ctx.getSource().sendSuccess(() -> Component.translatable("commands.sable.sub_level.clear_name.success_singular"), true);
|
||||
} else {
|
||||
ctx.getSource().sendSuccess(() -> Component.translatable("commands.sable.sub_level.clear_name.success_multiple", modifiedCount), true);
|
||||
}
|
||||
return modifiedCount;
|
||||
}
|
||||
|
||||
private static int executeGetSubLevelNameCommand(final CommandContext<CommandSourceStack> ctx) throws CommandSyntaxException {
|
||||
final SubLevel subLevel = SubLevelArgumentType.getSingleSubLevel(ctx, "sub_level");
|
||||
|
||||
if (subLevel.getName() == null) {
|
||||
throw SableCommandHelper.ERROR_SUB_LEVEL_UNNAMED.create();
|
||||
} else {
|
||||
ctx.getSource().sendSuccess(() -> Component.translatable("commands.sable.sub_level.get_name.success", subLevel.getName()), true);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static int executeTeleportSubLevelCommand(final CommandContext<CommandSourceStack> ctx, final @Nullable Coordinates angle) throws CommandSyntaxException {
|
||||
final PhysicsPipeline pipeline = SableCommandHelper.requireSubLevelPhysicsPipeline(ctx);
|
||||
|
||||
final Collection<ServerSubLevel> targets = SubLevelArgumentType.getSubLevels(ctx, "targets");
|
||||
|
||||
if (targets.isEmpty()) {
|
||||
throw SableCommandHelper.ERROR_NO_SUB_LEVELS_FOUND.create();
|
||||
}
|
||||
|
||||
final Vector3d destination = JOMLConversion.toJOML(Vec3Argument.getVec3(ctx, "destination"));
|
||||
|
||||
final Quaterniond orientation = new Quaterniond();
|
||||
|
||||
final Vec2 rotation = angle != null ? angle.getRotation(ctx.getSource()) : null;
|
||||
if (angle != null) {
|
||||
orientation.rotateY(-Math.toRadians(rotation.y));
|
||||
orientation.rotateX(Math.toRadians(rotation.x));
|
||||
}
|
||||
|
||||
for (final ServerSubLevel target : targets) {
|
||||
pipeline.resetVelocity(target);
|
||||
pipeline.teleport(target, destination, angle != null ? orientation : target.logicalPose().orientation());
|
||||
}
|
||||
|
||||
if (angle != null) {
|
||||
SableCommandHelper.sendSuccessDescribingSubLevels(
|
||||
"commands.sable.sub_level.teleport_with_orientation.success",
|
||||
ctx, targets,
|
||||
destination.x, destination.y, destination.z,
|
||||
rotation.x, rotation.y
|
||||
);
|
||||
} else {
|
||||
SableCommandHelper.sendSuccessDescribingSubLevels(
|
||||
"commands.sable.sub_level.teleport.success",
|
||||
ctx, targets,
|
||||
destination.x, destination.y, destination.z
|
||||
);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int executeRemoveSubLevelCommand(final CommandContext<CommandSourceStack> ctx) throws CommandSyntaxException {
|
||||
final SubLevelContainer container = SableCommandHelper.requireSubLevelContainer(ctx);
|
||||
|
||||
final Collection<ServerSubLevel> targets = SubLevelArgumentType.getSubLevels(ctx, "targets");
|
||||
|
||||
if (targets.isEmpty()) {
|
||||
throw SableCommandHelper.ERROR_NO_SUB_LEVELS_FOUND.create();
|
||||
}
|
||||
|
||||
for (final SubLevel target : targets) {
|
||||
final LevelPlot plot = target.getPlot();
|
||||
final Vector2i origin = container.getOrigin();
|
||||
container.removeSubLevel(plot.plotPos.x - origin.x, plot.plotPos.z - origin.y, SubLevelRemovalReason.REMOVED);
|
||||
}
|
||||
|
||||
SableCommandHelper.sendSuccessDescribingSubLevels("commands.sable.sub_level.remove.success", ctx, targets);
|
||||
return 1;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package dev.ryanhcode.sable.command;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.mojang.brigadier.StringReader;
|
||||
import com.mojang.brigadier.arguments.ArgumentType;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.commands.SharedSuggestionProvider;
|
||||
import net.minecraft.commands.arguments.coordinates.Vec3Argument;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import static net.minecraft.commands.arguments.coordinates.WorldCoordinate.ERROR_EXPECTED_DOUBLE;
|
||||
|
||||
public class Vec3ArgumentAbsolute implements ArgumentType<Vec3>
|
||||
{
|
||||
public static Vec3ArgumentAbsolute vec3() {
|
||||
return new Vec3ArgumentAbsolute();
|
||||
}
|
||||
private static final Collection<String> EXAMPLES = Arrays.asList("0 0 0");
|
||||
@Override
|
||||
public Vec3 parse(StringReader stringReader) throws CommandSyntaxException {
|
||||
int i = stringReader.getCursor();
|
||||
double worldCoordinate = parseDouble(stringReader);
|
||||
if (stringReader.canRead() && stringReader.peek() == ' ') {
|
||||
stringReader.skip();
|
||||
double worldCoordinate2 = parseDouble(stringReader);
|
||||
if (stringReader.canRead() && stringReader.peek() == ' ') {
|
||||
stringReader.skip();
|
||||
double worldCoordinate3 = parseDouble(stringReader);
|
||||
return new Vec3(worldCoordinate,worldCoordinate2,worldCoordinate3);
|
||||
} else {
|
||||
stringReader.setCursor(i);
|
||||
throw Vec3Argument.ERROR_NOT_COMPLETE.createWithContext(stringReader);
|
||||
}
|
||||
} else {
|
||||
stringReader.setCursor(i);
|
||||
throw Vec3Argument.ERROR_NOT_COMPLETE.createWithContext(stringReader);
|
||||
}
|
||||
}
|
||||
private double parseDouble(StringReader stringReader) throws CommandSyntaxException
|
||||
{
|
||||
if (!stringReader.canRead()) {
|
||||
throw ERROR_EXPECTED_DOUBLE.createWithContext(stringReader);
|
||||
} else {
|
||||
int i = stringReader.getCursor();
|
||||
double d = stringReader.canRead() && stringReader.peek() != ' ' ? stringReader.readDouble() : (double)0.0F;
|
||||
String string = stringReader.getString().substring(i, stringReader.getCursor());
|
||||
if (string.isEmpty()) {
|
||||
return 0;
|
||||
} else {
|
||||
return d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> commandContext, SuggestionsBuilder suggestionsBuilder) {
|
||||
if (!(commandContext.getSource() instanceof SharedSuggestionProvider)) {
|
||||
return Suggestions.empty();
|
||||
} else {
|
||||
String string = suggestionsBuilder.getRemaining();
|
||||
List<String> list = Lists.newArrayList();
|
||||
Predicate<String> predicate = Commands.createValidator(this::parse);
|
||||
String[] strings = Strings.isNullOrEmpty(string)? new String[0] : string.split(" ");
|
||||
|
||||
for (int i = 3; i > strings.length; i--) {
|
||||
StringBuilder s = new StringBuilder();
|
||||
for (int j = 0; j < i; j++) {
|
||||
s.append(j < strings.length ? strings[j] : "0");
|
||||
if(j < i-1)
|
||||
s.append(" ");
|
||||
}
|
||||
if(!predicate.test(s.toString()) && i == 3)
|
||||
break;
|
||||
list.add(s.toString());
|
||||
}
|
||||
return SharedSuggestionProvider.suggest(list,suggestionsBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getExamples() {
|
||||
return EXAMPLES;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package dev.ryanhcode.sable.command.argument;
|
||||
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import dev.ryanhcode.sable.ActiveSableCompanion;
|
||||
import dev.ryanhcode.sable.Sable;
|
||||
import dev.ryanhcode.sable.api.SubLevelHelper;
|
||||
import dev.ryanhcode.sable.api.command.SableCommandHelper;
|
||||
import dev.ryanhcode.sable.api.entity.EntitySubLevelUtil;
|
||||
import dev.ryanhcode.sable.api.sublevel.ServerSubLevelContainer;
|
||||
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
|
||||
import it.unimi.dsi.fastutil.Pair;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.HitResult;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.joml.Vector3d;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class SubLevelSelector {
|
||||
|
||||
private final SubLevelSelectorType type;
|
||||
private final List<Pair<SubLevelSelectorModifierType, SubLevelSelectorModifierType.Modifier>> modifiers;
|
||||
|
||||
public SubLevelSelector(final SubLevelSelectorType type, final List<Pair<SubLevelSelectorModifierType, SubLevelSelectorModifierType.Modifier>> modifiers) {
|
||||
this.type = type;
|
||||
this.modifiers = modifiers;
|
||||
}
|
||||
|
||||
public SubLevelSelectorType getSelectorType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
public Collection<ServerSubLevel> getSubLevels(final CommandSourceStack source) throws CommandSyntaxException {
|
||||
if (this.type == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
final ServerLevel level = source.getLevel();
|
||||
final ServerSubLevelContainer container = SableCommandHelper.requireSubLevelContainer(source);
|
||||
|
||||
final Iterable<ServerSubLevel> containerBodies = container.getAllSubLevels();
|
||||
final Collection<ServerSubLevel> bodies = new ObjectArrayList<>();
|
||||
|
||||
for (final ServerSubLevel subLevel : containerBodies) {
|
||||
bodies.add(subLevel);
|
||||
}
|
||||
|
||||
if (bodies.isEmpty()) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
final ActiveSableCompanion helper = Sable.HELPER;
|
||||
final Collection<ServerSubLevel> collectedSubLevels = switch (this.type) {
|
||||
case ALL -> new HashSet<>(bodies);
|
||||
case NEAREST -> {
|
||||
double closest = Double.MAX_VALUE;
|
||||
ServerSubLevel closestSubLevel = null;
|
||||
|
||||
for (final ServerSubLevel body : bodies) {
|
||||
final Vec3 sourcePosition = helper.projectOutOfSubLevel(source.getLevel(), source.getPosition());
|
||||
final double distance = body.logicalPose().position().distance(sourcePosition.x, sourcePosition.y, sourcePosition.z);
|
||||
|
||||
if (distance < closest) {
|
||||
closest = distance;
|
||||
closestSubLevel = body;
|
||||
}
|
||||
}
|
||||
|
||||
yield Collections.singleton(closestSubLevel);
|
||||
}
|
||||
case RANDOM -> {
|
||||
final List<ServerSubLevel> list = new ArrayList<>(bodies);
|
||||
yield Collections.singleton(list.get(level.random.nextInt(list.size())));
|
||||
}
|
||||
case INSIDE -> {
|
||||
final ServerSubLevel subLevel = (ServerSubLevel) helper.getContaining(level, source.getPosition());
|
||||
if (subLevel != null) {
|
||||
yield Collections.singleton(subLevel);
|
||||
} else {
|
||||
yield Collections.emptySet();
|
||||
}
|
||||
}
|
||||
case TRACKING -> {
|
||||
if (source.getEntity() == null) {
|
||||
yield Collections.emptySet();
|
||||
}
|
||||
|
||||
final ServerSubLevel subLevel = (ServerSubLevel) Sable.HELPER.getTrackingSubLevel(source.getEntity());
|
||||
|
||||
if (subLevel != null) {
|
||||
yield Collections.singleton(subLevel);
|
||||
} else {
|
||||
yield Collections.emptySet();
|
||||
}
|
||||
}
|
||||
case VIEWED -> {
|
||||
if (source.getEntity() != null) {
|
||||
final HitResult res = source.getEntity().pick(100.0, 1.0f, true);
|
||||
|
||||
if (res instanceof final BlockHitResult blockHitResult) {
|
||||
final ServerSubLevel containing = (ServerSubLevel) helper.getContaining(level, blockHitResult.getBlockPos());
|
||||
if (containing != null) {
|
||||
yield Collections.singleton(containing);
|
||||
} else {
|
||||
yield Collections.emptySet();
|
||||
}
|
||||
} else {
|
||||
yield Collections.emptySet();
|
||||
}
|
||||
} else {
|
||||
yield Collections.emptySet();
|
||||
}
|
||||
}
|
||||
case LATEST -> {
|
||||
final List<ServerSubLevel> subLevels = container.getAllSubLevels();
|
||||
if (subLevels.isEmpty()) {
|
||||
yield Collections.emptySet();
|
||||
}
|
||||
yield Collections.singleton(subLevels.getLast());
|
||||
}
|
||||
};
|
||||
|
||||
List<ServerSubLevel> modifiedSubLevels = new ObjectArrayList<>(collectedSubLevels);
|
||||
|
||||
final Vector3d position = new Vector3d(source.getPosition().x, source.getPosition().y, source.getPosition().z);
|
||||
this.modifiers.sort(
|
||||
Comparator.comparingInt(a -> a.first().getFilterPriority().ordinal())
|
||||
);
|
||||
for (final Pair<SubLevelSelectorModifierType, SubLevelSelectorModifierType.Modifier> modifier : this.modifiers) {
|
||||
modifiedSubLevels = modifier.right().apply(modifiedSubLevels, position);
|
||||
}
|
||||
|
||||
return modifiedSubLevels;
|
||||
}
|
||||
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
package dev.ryanhcode.sable.command.argument;
|
||||
|
||||
import com.mojang.brigadier.Message;
|
||||
import com.mojang.brigadier.StringReader;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
|
||||
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
|
||||
import it.unimi.dsi.fastutil.Pair;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Vector3d;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class SubLevelSelectorModifierType {
|
||||
|
||||
private static final Map<String, SubLevelSelectorModifierType> MODIFIERS_BY_NAME = new Object2ObjectOpenHashMap<>();
|
||||
|
||||
private static final SimpleCommandExceptionType UNKNOWN_PROPERTY_NAME =
|
||||
new SimpleCommandExceptionType(Component.translatable("argument.sable.sub_level.unknown_property"));
|
||||
|
||||
private final String name;
|
||||
private final Parser parser;
|
||||
private final FilterPriority filterPriority;
|
||||
|
||||
public SubLevelSelectorModifierType(final String name, final Parser parser, final FilterPriority priority) {
|
||||
this.name = name;
|
||||
this.parser = parser;
|
||||
this.filterPriority = priority;
|
||||
}
|
||||
|
||||
public static void registerType(final String name, final Parser parser, final FilterPriority filterPriority) {
|
||||
if (MODIFIERS_BY_NAME.containsKey(name)) {
|
||||
throw new IllegalArgumentException("Modifier type " + name + " already registered");
|
||||
}
|
||||
MODIFIERS_BY_NAME.put(name, new SubLevelSelectorModifierType(name, parser, filterPriority));
|
||||
}
|
||||
|
||||
public static SubLevelSelectorModifierType getModifier(final String propertyName, final StringReader readerForErrorContext) throws CommandSyntaxException {
|
||||
if (!MODIFIERS_BY_NAME.containsKey(propertyName)) {
|
||||
throw UNKNOWN_PROPERTY_NAME.createWithContext(readerForErrorContext);
|
||||
}
|
||||
return MODIFIERS_BY_NAME.get(propertyName);
|
||||
}
|
||||
|
||||
public static void clearRegistry() {
|
||||
MODIFIERS_BY_NAME.clear();
|
||||
}
|
||||
|
||||
public static List<Pair<String, Message>> getAllNamesWithTooltip() {
|
||||
final ArrayList<Pair<String, Message>> modifiers = new ArrayList<>();
|
||||
for (final SubLevelSelectorModifierType modifier : MODIFIERS_BY_NAME.values()) {
|
||||
modifiers.add(Pair.of(modifier.name, Component.translatable("argument.sable.sub_level.modifier." + modifier.name)));
|
||||
}
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
public Parser getParser() {
|
||||
return this.parser;
|
||||
}
|
||||
|
||||
public FilterPriority getFilterPriority() {
|
||||
return this.filterPriority;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that something like {@code [limit=1,sort=nearest]} applies the sort first, then the limit
|
||||
*/
|
||||
public enum FilterPriority {
|
||||
POSITION,
|
||||
FILTER,
|
||||
SORTING,
|
||||
SORTING_SELECTION,
|
||||
}
|
||||
|
||||
public interface Parser {
|
||||
SubLevelSelectorModifierType.Modifier parse(final StringReader value) throws CommandSyntaxException;
|
||||
}
|
||||
|
||||
public interface Modifier {
|
||||
/**
|
||||
* @return the maximum quantity of sub-levels this modifier could ever produce
|
||||
*/
|
||||
int getMaxResults();
|
||||
|
||||
/**
|
||||
* Applies the modifier to the selected sub-levels
|
||||
*
|
||||
* @param selected The currently selected sub-levels
|
||||
* @param sourcePos The position of the source, should only be modified by modifiers with priority of {@link FilterPriority#POSITION}
|
||||
* @return The filtered sub-levels
|
||||
*/
|
||||
@Nullable List<ServerSubLevel> apply(final List<ServerSubLevel> selected, Vector3d sourcePos);
|
||||
}
|
||||
|
||||
}
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
package dev.ryanhcode.sable.command.argument;
|
||||
|
||||
import com.mojang.brigadier.StringReader;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
|
||||
import dev.ryanhcode.sable.api.command.SubLevelArgumentType;
|
||||
import dev.ryanhcode.sable.command.argument.modifier_type.*;
|
||||
import net.minecraft.advancements.critereon.MinMaxBounds;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
public class SubLevelSelectorModifiers {
|
||||
|
||||
public static final SimpleCommandExceptionType EXPECTED_END_OF_MODIFIER =
|
||||
new SimpleCommandExceptionType(Component.translatable("argument.sable.sub_level.expected_end_of_modifier"));
|
||||
public static final SimpleCommandExceptionType EXPECTED_POSITIVE_INTEGER =
|
||||
new SimpleCommandExceptionType(Component.translatable("argument.sable.sub_level.expected_positive_integer"));
|
||||
public static final SimpleCommandExceptionType EXPECTED_POSITIVE_DECIMAL =
|
||||
new SimpleCommandExceptionType(Component.translatable("argument.sable.sub_level.expected_positive_decimal"));
|
||||
public static final SimpleCommandExceptionType EXPECTED_SORTING_TYPE =
|
||||
new SimpleCommandExceptionType(Component.translatable("argument.sable.sub_level.expected_sorting"));
|
||||
public static final SimpleCommandExceptionType EXPECTED_POSITIVE_RANGE =
|
||||
new SimpleCommandExceptionType(Component.translatable("argument.sable.sub_level.expected_positive_range"));
|
||||
|
||||
private static void registerDoubleArgument(final String name, final boolean onlyPositive, final SubLevelDoubleFilter.Factory factory) {
|
||||
SubLevelSelectorModifierType.registerType(name, (reader) -> {
|
||||
final int i = reader.getCursor();
|
||||
final double value = reader.readDouble();
|
||||
if (onlyPositive && value < 0) {
|
||||
reader.setCursor(i);
|
||||
throw EXPECTED_POSITIVE_DECIMAL.createWithContext(reader);
|
||||
}
|
||||
return factory.create(value);
|
||||
}, SubLevelSelectorModifierType.FilterPriority.FILTER);
|
||||
}
|
||||
|
||||
private static void registerDoubleRangeArgument(final String name, final boolean onlyPositive, final SubLevelDoubleRangeFilter.Factory factory) {
|
||||
SubLevelSelectorModifierType.registerType(name, (reader) -> {
|
||||
final int i = reader.getCursor();
|
||||
final MinMaxBounds.Doubles doubles = MinMaxBounds.Doubles.fromReader(reader);
|
||||
if (onlyPositive && ((
|
||||
doubles.min().isPresent() && doubles.min().get() < 0
|
||||
) || (
|
||||
doubles.max().isPresent() && doubles.max().get() < 0
|
||||
))) {
|
||||
reader.setCursor(i);
|
||||
throw EXPECTED_POSITIVE_RANGE.createWithContext(reader);
|
||||
}
|
||||
return factory.create(doubles);
|
||||
}, SubLevelSelectorModifierType.FilterPriority.FILTER);
|
||||
}
|
||||
|
||||
public static void registerModifiers() {
|
||||
registerDoubleRangeArgument("distance", true, SubLevelDoubleRangeFilter.squared(
|
||||
(subLevel, sourcePos) -> subLevel.logicalPose().position().distanceSquared(sourcePos)
|
||||
));
|
||||
registerDoubleRangeArgument("x", false, SubLevelDoubleRangeFilter.linear(
|
||||
(subLevel, sourcePos) -> subLevel.logicalPose().position().x()
|
||||
));
|
||||
registerDoubleRangeArgument("y", false, SubLevelDoubleRangeFilter.linear(
|
||||
(subLevel, sourcePos) -> subLevel.logicalPose().position().y()
|
||||
));
|
||||
registerDoubleRangeArgument("z", false, SubLevelDoubleRangeFilter.linear(
|
||||
(subLevel, sourcePos) -> subLevel.logicalPose().position().z()
|
||||
));
|
||||
registerDoubleArgument("dx", false, SubLevelDoubleFilter.factory(
|
||||
(subLevel, sourcePos, value) -> {
|
||||
final double dx = subLevel.logicalPose().position().x() - sourcePos.x();
|
||||
if (value < 0) {
|
||||
return dx < 0 && dx > value;
|
||||
} else {
|
||||
return dx > 0 && dx < value;
|
||||
}
|
||||
}
|
||||
));
|
||||
registerDoubleArgument("dy", false, SubLevelDoubleFilter.factory(
|
||||
(subLevel, sourcePos, value) -> {
|
||||
final double dy = subLevel.logicalPose().position().y() - sourcePos.y();
|
||||
if (value < 0) {
|
||||
return dy < 0 && dy > value;
|
||||
} else {
|
||||
return dy > 0 && dy < value;
|
||||
}
|
||||
}
|
||||
));
|
||||
registerDoubleArgument("dz", false, SubLevelDoubleFilter.factory(
|
||||
(subLevel, sourcePos, value) -> {
|
||||
final double dz = subLevel.logicalPose().position().z() - sourcePos.z();
|
||||
if (value < 0) {
|
||||
return dz < 0 && dz > value;
|
||||
} else {
|
||||
return dz > 0 && dz < value;
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
registerDoubleRangeArgument("vx", false, SubLevelDoubleRangeFilter.linear(
|
||||
(subLevel, sourcePos) -> subLevel.latestLinearVelocity.x
|
||||
));
|
||||
registerDoubleRangeArgument("vy", false, SubLevelDoubleRangeFilter.linear(
|
||||
(subLevel, sourcePos) -> subLevel.latestLinearVelocity.y
|
||||
));
|
||||
registerDoubleRangeArgument("vz", false, SubLevelDoubleRangeFilter.linear(
|
||||
(subLevel, sourcePos) -> subLevel.latestLinearVelocity.z
|
||||
));
|
||||
registerDoubleRangeArgument("speed", true, SubLevelDoubleRangeFilter.squared(
|
||||
(subLevel, sourcePos) -> subLevel.latestLinearVelocity.lengthSquared()
|
||||
));
|
||||
|
||||
registerDoubleRangeArgument("mass", true, SubLevelDoubleRangeFilter.linear(
|
||||
(subLevel, sourcePos) -> subLevel.getMassTracker().getMass()
|
||||
));
|
||||
|
||||
registerDoubleRangeArgument("volume", true, SubLevelDoubleRangeFilter.linear(
|
||||
(subLevel, sourcePos) -> subLevel.getPlot().getBoundingBox().volume()
|
||||
));
|
||||
registerDoubleRangeArgument("width", true, SubLevelDoubleRangeFilter.linear(
|
||||
(subLevel, sourcePos) -> subLevel.getPlot().getBoundingBox().width()
|
||||
));
|
||||
registerDoubleRangeArgument("height", true, SubLevelDoubleRangeFilter.linear(
|
||||
(subLevel, sourcePos) -> subLevel.getPlot().getBoundingBox().height()
|
||||
));
|
||||
registerDoubleRangeArgument("length", true, SubLevelDoubleRangeFilter.linear(
|
||||
(subLevel, sourcePos) -> subLevel.getPlot().getBoundingBox().length()
|
||||
));
|
||||
|
||||
SubLevelSelectorModifierType.registerType("name", (reader) -> {
|
||||
final String name = readUntilEndOfModifier(reader);
|
||||
return new SubLevelNameFilter(name);
|
||||
}, SubLevelSelectorModifierType.FilterPriority.FILTER);
|
||||
|
||||
SubLevelSelectorModifierType.registerType("sort", (reader) -> {
|
||||
SubLevelArgumentType.setSuggestions(reader, "nearest", "furthest");
|
||||
final String filtering = tryReadString(reader, EXPECTED_SORTING_TYPE, "nearest", "furthest");
|
||||
expectEndOfModifier(reader);
|
||||
return new SubLevelSortModifier(filtering);
|
||||
}, SubLevelSelectorModifierType.FilterPriority.SORTING);
|
||||
|
||||
SubLevelSelectorModifierType.registerType("limit", (reader) -> {
|
||||
final int limit = readPositiveIntStrict(reader);
|
||||
return new SubLevelLimitFilter(limit);
|
||||
}, SubLevelSelectorModifierType.FilterPriority.SORTING_SELECTION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normal {@link StringReader#readInt()} will try to read a {@code .} as a decimal point and fail, this will ignore all non 0-9 characters and terminate
|
||||
*/
|
||||
private static Integer readPositiveIntStrict(final StringReader reader) throws CommandSyntaxException {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
while (reader.canRead() && reader.peek() >= '0' && reader.peek() <= '9') {
|
||||
builder.append(reader.read());
|
||||
}
|
||||
if (builder.isEmpty()) {
|
||||
throw EXPECTED_POSITIVE_INTEGER.createWithContext(reader);
|
||||
}
|
||||
return Integer.parseInt(builder.toString());
|
||||
}
|
||||
|
||||
private static boolean isEndOfModifier(final StringReader reader) {
|
||||
return reader.peek() == ',' || reader.peek() == ']';
|
||||
}
|
||||
|
||||
private static String readUntilEndOfModifier(final StringReader reader) throws CommandSyntaxException {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
if (reader.canRead() && reader.peek() == '"') {
|
||||
reader.skip();
|
||||
boolean thereIsNoEscape = false;
|
||||
while (reader.canRead() && (thereIsNoEscape || reader.peek() != '"')) {
|
||||
if (!thereIsNoEscape && reader.peek() == '\\') {
|
||||
thereIsNoEscape = true;
|
||||
reader.skip();
|
||||
} else {
|
||||
builder.append(reader.read());
|
||||
thereIsNoEscape = false;
|
||||
}
|
||||
}
|
||||
if (reader.canRead()) {
|
||||
reader.skip();
|
||||
} else {
|
||||
throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.readerExpectedEndOfQuote().createWithContext(reader);
|
||||
}
|
||||
} else {
|
||||
while (reader.canRead() && !isEndOfModifier(reader)) {
|
||||
builder.append(reader.read());
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static String tryReadString(final StringReader reader, final SimpleCommandExceptionType exception, final String... accepted) throws CommandSyntaxException {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
while (reader.canRead()) {
|
||||
if (isEndOfModifier(reader)) {
|
||||
throw exception.createWithContext(reader);
|
||||
}
|
||||
builder.append(reader.read());
|
||||
for (final String s : accepted) {
|
||||
if (builder.toString().equals(s)) {
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
throw exception.createWithContext(reader);
|
||||
}
|
||||
|
||||
private static void expectEndOfModifier(final StringReader reader) throws CommandSyntaxException {
|
||||
if (!reader.canRead() || !isEndOfModifier(reader)) {
|
||||
throw EXPECTED_END_OF_MODIFIER.createWithContext(reader);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package dev.ryanhcode.sable.command.argument;
|
||||
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
public enum SubLevelSelectorType {
|
||||
ALL('e', Component.translatable("argument.sable.body.selector.all"), false),
|
||||
NEAREST('n', Component.translatable("argument.sable.body.selector.nearest"), true),
|
||||
RANDOM('r', Component.translatable("argument.sable.body.selector.random"), true),
|
||||
VIEWED('v', Component.translatable("argument.sable.body.selector.viewed"), true),
|
||||
LATEST('l', Component.translatable("argument.sable.body.selector.latest"), true),
|
||||
TRACKING('t', Component.translatable("argument.sable.body.selector.tracking"), true),
|
||||
INSIDE('i', Component.translatable("argument.sable.body.selector.inside"), true);
|
||||
|
||||
private final char selector;
|
||||
private final Component tooltip;
|
||||
private final boolean single;
|
||||
|
||||
SubLevelSelectorType(final char selector, final Component tooltip, final boolean single) {
|
||||
this.selector = selector;
|
||||
this.tooltip = tooltip;
|
||||
this.single = single;
|
||||
}
|
||||
|
||||
public static SubLevelSelectorType of(final char c) {
|
||||
for (final SubLevelSelectorType type : SubLevelSelectorType.values()) {
|
||||
if (type.selector == c) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public char getChar() {
|
||||
return this.selector;
|
||||
}
|
||||
|
||||
public Component getTooltip() {
|
||||
return this.tooltip;
|
||||
}
|
||||
|
||||
public boolean single() {
|
||||
return this.single;
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package dev.ryanhcode.sable.command.argument.modifier_type;
|
||||
|
||||
import dev.ryanhcode.sable.command.argument.SubLevelSelectorModifierType;
|
||||
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Vector3d;
|
||||
import org.joml.Vector3dc;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SubLevelDoubleFilter implements SubLevelSelectorModifierType.Modifier {
|
||||
private final double value;
|
||||
private final DoublePredicate valuePredicate;
|
||||
|
||||
private SubLevelDoubleFilter(final double value, final DoublePredicate valuePredicate) {
|
||||
this.value = value;
|
||||
this.valuePredicate = valuePredicate;
|
||||
}
|
||||
|
||||
public static SubLevelDoubleFilter.Factory factory(final DoublePredicate valuePredicate) {
|
||||
return new SubLevelDoubleFilter.Factory(valuePredicate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxResults() {
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable List<ServerSubLevel> apply(final List<ServerSubLevel> selected, final Vector3d sourcePos) {
|
||||
final List<ServerSubLevel> filtered = new ObjectArrayList<>();
|
||||
|
||||
for (final ServerSubLevel subLevel : selected) {
|
||||
if (this.valuePredicate.fromSublevel(subLevel, sourcePos, this.value)) {
|
||||
filtered.add(subLevel);
|
||||
}
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface DoublePredicate {
|
||||
boolean fromSublevel(ServerSubLevel subLevel, Vector3dc sourcePos, double test);
|
||||
}
|
||||
|
||||
public static class Factory {
|
||||
private final DoublePredicate doublePredicate;
|
||||
|
||||
public Factory(final DoublePredicate doublePredicate) {
|
||||
this.doublePredicate = doublePredicate;
|
||||
}
|
||||
|
||||
public SubLevelDoubleFilter create(final double value) {
|
||||
return new SubLevelDoubleFilter(value, this.doublePredicate);
|
||||
}
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package dev.ryanhcode.sable.command.argument.modifier_type;
|
||||
|
||||
import dev.ryanhcode.sable.command.argument.SubLevelSelectorModifierType;
|
||||
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
import net.minecraft.advancements.critereon.MinMaxBounds;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Vector3d;
|
||||
import org.joml.Vector3dc;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SubLevelDoubleRangeFilter implements SubLevelSelectorModifierType.Modifier {
|
||||
private final MinMaxBounds.Doubles range;
|
||||
private final DoubleGetter valueGetter;
|
||||
private final boolean squared;
|
||||
|
||||
private SubLevelDoubleRangeFilter(final MinMaxBounds.Doubles range, final DoubleGetter valueGetter, final boolean squared) {
|
||||
this.range = range;
|
||||
this.valueGetter = valueGetter;
|
||||
this.squared = squared;
|
||||
}
|
||||
|
||||
public static SubLevelDoubleRangeFilter.Factory linear(final DoubleGetter valueGetter) {
|
||||
return new SubLevelDoubleRangeFilter.Factory(valueGetter, false);
|
||||
}
|
||||
|
||||
public static SubLevelDoubleRangeFilter.Factory squared(final DoubleGetter valueGetter) {
|
||||
return new SubLevelDoubleRangeFilter.Factory(valueGetter, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxResults() {
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable List<ServerSubLevel> apply(final List<ServerSubLevel> selected, final Vector3d sourcePos) {
|
||||
final List<ServerSubLevel> filtered = new ObjectArrayList<>();
|
||||
|
||||
for (final ServerSubLevel subLevel : selected) {
|
||||
final double value = this.valueGetter.fromSublevel(subLevel, sourcePos);
|
||||
if (this.squared) {
|
||||
if (this.range.matchesSqr(value)) {
|
||||
filtered.add(subLevel);
|
||||
}
|
||||
} else {
|
||||
if (this.range.matches(value)) {
|
||||
filtered.add(subLevel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface DoubleGetter {
|
||||
double fromSublevel(ServerSubLevel subLevel, Vector3dc sourcePos);
|
||||
}
|
||||
|
||||
public static class Factory {
|
||||
private final DoubleGetter doubleGetter;
|
||||
private final boolean squared;
|
||||
|
||||
public Factory(final DoubleGetter doubleGetter, final boolean squared) {
|
||||
this.doubleGetter = doubleGetter;
|
||||
this.squared = squared;
|
||||
}
|
||||
|
||||
public SubLevelDoubleRangeFilter create(final MinMaxBounds.Doubles range) {
|
||||
return new SubLevelDoubleRangeFilter(range, this.doubleGetter, this.squared);
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package dev.ryanhcode.sable.command.argument.modifier_type;
|
||||
|
||||
import dev.ryanhcode.sable.command.argument.SubLevelSelectorModifierType;
|
||||
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Vector3d;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SubLevelLimitFilter implements SubLevelSelectorModifierType.Modifier {
|
||||
private final int limit;
|
||||
|
||||
public SubLevelLimitFilter(final int limit) {
|
||||
this.limit = limit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxResults() {
|
||||
return this.limit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable List<ServerSubLevel> apply(final List<ServerSubLevel> selected, final Vector3d sourcePos) {
|
||||
if (selected.size() > this.limit) {
|
||||
return new ObjectArrayList<>(selected.subList(0, this.limit));
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package dev.ryanhcode.sable.command.argument.modifier_type;
|
||||
|
||||
import dev.ryanhcode.sable.command.argument.SubLevelSelectorModifierType;
|
||||
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Vector3d;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SubLevelNameFilter implements SubLevelSelectorModifierType.Modifier {
|
||||
private final String name;
|
||||
|
||||
public SubLevelNameFilter(final String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxResults() {
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable List<ServerSubLevel> apply(final List<ServerSubLevel> selected, final Vector3d sourcePos) {
|
||||
final List<ServerSubLevel> filtered = new ObjectArrayList<>();
|
||||
for (final ServerSubLevel subLevel : selected) {
|
||||
if (subLevel.getName() != null && subLevel.getName().equals(this.name)) {
|
||||
filtered.add(subLevel);
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package dev.ryanhcode.sable.command.argument.modifier_type;
|
||||
|
||||
import dev.ryanhcode.sable.command.argument.SubLevelSelectorModifierType;
|
||||
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Vector3d;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class SubLevelSortModifier implements SubLevelSelectorModifierType.Modifier {
|
||||
|
||||
private final String filtering;
|
||||
|
||||
public SubLevelSortModifier(final String filtering) {
|
||||
this.filtering = filtering;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxResults() {
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable List<ServerSubLevel> apply(final List<ServerSubLevel> selected, final Vector3d sourcePos) {
|
||||
final Map<SubLevel, Double> distances = selected.stream().collect(Collectors.toMap(
|
||||
subLevel -> subLevel,
|
||||
subLevel -> subLevel.logicalPose().position()
|
||||
.distanceSquared(sourcePos.x, sourcePos.y, sourcePos.z)
|
||||
));
|
||||
if (this.filtering.equals("nearest")) {
|
||||
selected.sort(Comparator.comparingDouble(distances::get));
|
||||
} else if (this.filtering.equals("furthest")) {
|
||||
selected.sort(Comparator.comparingDouble(subLevel -> -distances.get(subLevel)));
|
||||
}
|
||||
|
||||
return selected;
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package dev.ryanhcode.sable.command.data_accessor;
|
||||
|
||||
import com.mojang.brigadier.builder.ArgumentBuilder;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import dev.ryanhcode.sable.api.command.SubLevelArgumentType;
|
||||
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.commands.arguments.NbtPathArgument;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.NbtUtils;
|
||||
import net.minecraft.nbt.Tag;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.commands.data.DataAccessor;
|
||||
import net.minecraft.server.commands.data.DataCommands;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class SubLevelDataAccessor implements DataAccessor {
|
||||
public static final Function<String, DataCommands.DataProvider> PROVIDER = string -> new DataCommands.DataProvider() {
|
||||
@Override
|
||||
public DataAccessor access(final CommandContext<CommandSourceStack> commandContext) throws CommandSyntaxException {
|
||||
return new SubLevelDataAccessor((ServerSubLevel) SubLevelArgumentType.getSingleSubLevel(commandContext, string));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ArgumentBuilder<CommandSourceStack, ?> wrap(
|
||||
final ArgumentBuilder<CommandSourceStack, ?> argumentBuilder, final Function<ArgumentBuilder<CommandSourceStack, ?>, ArgumentBuilder<CommandSourceStack, ?>> function
|
||||
) {
|
||||
return argumentBuilder.then(
|
||||
Commands.literal("sub_level").then(function.apply(Commands.argument(string, SubLevelArgumentType.singleSubLevel())))
|
||||
);
|
||||
}
|
||||
};
|
||||
private final ServerSubLevel subLevel;
|
||||
|
||||
public SubLevelDataAccessor(final ServerSubLevel subLevel) {
|
||||
this.subLevel = subLevel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setData(final CompoundTag compoundTag) {
|
||||
this.subLevel.setUserDataTag(compoundTag);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompoundTag getData() {
|
||||
final CompoundTag userTag = this.subLevel.getUserDataTag();
|
||||
return userTag != null ? userTag : new CompoundTag();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getModifiedSuccess() {
|
||||
return Component.translatable("commands.data.sub_level.modified", this.subLevel.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getPrintSuccess(final Tag tag) {
|
||||
return Component.translatable("commands.data.sub_level.query", this.subLevel.toString(), NbtUtils.toPrettyComponent(tag));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getPrintSuccess(final NbtPathArgument.NbtPath nbtPath, final double d, final int i) {
|
||||
return Component.translatable(
|
||||
"commands.data.sub_level.get", nbtPath.asString(), this.subLevel.toString(), String.format(Locale.ROOT, "%.2f", d), i
|
||||
);
|
||||
}}
|
||||
@@ -0,0 +1,14 @@
|
||||
package dev.ryanhcode.sable.compatibility;
|
||||
|
||||
import dev.ryanhcode.sable.mixinterface.compatibility.iris.ExtendedShaderExtension;
|
||||
import net.minecraft.client.renderer.ShaderInstance;
|
||||
|
||||
public class SableIrisCompat {
|
||||
|
||||
public static void refreshModelMatrices(final ShaderInstance shader) {
|
||||
if (shader instanceof final ExtendedShaderExtension ext) {
|
||||
ext.sable$refreshModelMatrices();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package dev.ryanhcode.sable.config;
|
||||
|
||||
import dev.ryanhcode.sable.api.sublevel.SubLevelContainer;
|
||||
import dev.ryanhcode.sable.physics.config.PhysicsConfigData;
|
||||
import dev.ryanhcode.sable.sublevel.system.SubLevelPhysicsSystem;
|
||||
import net.minecraft.client.OptionInstance;
|
||||
import net.minecraft.client.Options;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.options.OptionsSubScreen;
|
||||
import net.minecraft.client.server.IntegratedServer;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
|
||||
public class SubLevelSettingsScreen extends OptionsSubScreen {
|
||||
public static final Component TITLE = Component.translatable("options.sable_menu");
|
||||
|
||||
public SubLevelSettingsScreen(final Screen optionsScreen, final Options options, final Component component) {
|
||||
super(optionsScreen, options, component);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addOptions() {
|
||||
final IntegratedServer singleplayerServer = this.minecraft.getSingleplayerServer();
|
||||
|
||||
|
||||
this.list.addBig(new OptionInstance<>(
|
||||
"options.physics_steps",
|
||||
OptionInstance.cachedConstantTooltip(Component.translatable("options.physics_steps.tooltip")),
|
||||
(component, substeps) -> Options.genericValueLabel(component, Component.translatable("options.physics_steps_template", substeps * 20)),
|
||||
new OptionInstance.IntRange(1, 10, false),
|
||||
SubLevelContainer.getContainer(singleplayerServer.overworld()).physicsSystem().getConfig().substepsPerTick,
|
||||
steps -> {
|
||||
for (final ServerLevel level : singleplayerServer.getAllLevels()) {
|
||||
final SubLevelPhysicsSystem physicsSystem = SubLevelContainer.getContainer(level).physicsSystem();
|
||||
final PhysicsConfigData config = physicsSystem.getConfig();
|
||||
config.substepsPerTick = steps;
|
||||
physicsSystem.getPipeline().updateConfigFrom(config);
|
||||
}
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package dev.ryanhcode.sable.debug;
|
||||
|
||||
import dev.ryanhcode.sable.SableClient;
|
||||
import dev.ryanhcode.sable.api.sublevel.SubLevelContainer;
|
||||
import dev.ryanhcode.sable.companion.math.JOMLConversion;
|
||||
import dev.ryanhcode.sable.network.packets.tcp.ServerboundGizmoMoveSubLevelPacket;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import foundry.veil.api.network.VeilPacketManager;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Vector3d;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class GizmoScreen extends Screen {
|
||||
private boolean dragging;
|
||||
private @Nullable GizmoSelection activeSelection;
|
||||
|
||||
protected GizmoScreen() {
|
||||
super(Component.literal("Gizmo Mode"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(final GuiGraphics guiGraphics, final int i, final int j, final float f) {
|
||||
// super.render(guiGraphics, i, j, f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseClicked(final double d, final double e, final int i) {
|
||||
final SableClientGizmoHandler gizmoHandler = SableClient.GIZMO_HANDLER;
|
||||
if (gizmoHandler.getSelection() != null) {
|
||||
this.activeSelection = gizmoHandler.getSelection();
|
||||
this.dragging = true;
|
||||
}
|
||||
|
||||
return super.mouseClicked(d, e, i);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseReleased(final double d, final double e, final int i) {
|
||||
this.dragging = false;
|
||||
this.activeSelection = null;
|
||||
|
||||
return super.mouseReleased(d, e, i);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseDragged(final double x, final double y, final int i, final double f, final double g) {
|
||||
final SableClientGizmoHandler gizmoHandler = SableClient.GIZMO_HANDLER;
|
||||
|
||||
if (this.dragging) {
|
||||
final Minecraft minecraft = Minecraft.getInstance();
|
||||
final ClientLevel level = minecraft.level;
|
||||
final SubLevelContainer container = SubLevelContainer.getContainer(level);
|
||||
assert container != null;
|
||||
|
||||
final UUID subLevelID = this.activeSelection.subLevel();
|
||||
final SubLevel subLevel = container.getSubLevel(subLevelID);
|
||||
if (subLevel == null) {
|
||||
this.cancel();
|
||||
return super.mouseDragged(x, y, i, f, g);
|
||||
}
|
||||
|
||||
final int ordinal = (this.activeSelection.axis().ordinal() + 1) % 3;
|
||||
final Direction.Axis axis = Direction.Axis.VALUES[ordinal];
|
||||
|
||||
final Vector3d dragNormal = JOMLConversion.atLowerCornerOf(Direction.get(Direction.AxisDirection.POSITIVE, this.activeSelection.axis()).getNormal());
|
||||
|
||||
final Vector3d pos = JOMLConversion.toJOML(minecraft.player.getEyePosition());
|
||||
final Vector3d relativePos = new Vector3d(pos).sub(subLevel.logicalPose().position());
|
||||
|
||||
final Vector3d planeNormal = JOMLConversion.atLowerCornerOf(Direction.get(Direction.AxisDirection.POSITIVE, axis).getNormal());
|
||||
if (relativePos.dot(planeNormal) < 0.0) {
|
||||
planeNormal.negate();
|
||||
}
|
||||
|
||||
final Vector3d dir = JOMLConversion.toJOML(gizmoHandler.getMouseDir());
|
||||
|
||||
final boolean hitsPlane = dir.dot(planeNormal) < 0.0;
|
||||
|
||||
if (hitsPlane) {
|
||||
final Vector3d negatedPlaneNormal = planeNormal.negate(new Vector3d());
|
||||
final double d = planeNormal.dot(relativePos);
|
||||
|
||||
final double rayLength = d / dir.dot(negatedPlaneNormal);
|
||||
|
||||
final Vector3d hitPos = new Vector3d(pos).fma(rayLength, dir);
|
||||
|
||||
final Vector3d subLevelPos = new Vector3d(subLevel.logicalPose().position());
|
||||
subLevelPos.fma(-subLevelPos.dot(dragNormal), dragNormal, subLevelPos);
|
||||
subLevelPos.fma(hitPos.dot(dragNormal), dragNormal, subLevelPos);
|
||||
|
||||
VeilPacketManager.server().sendPacket(new ServerboundGizmoMoveSubLevelPacket(this.activeSelection.subLevel(), subLevelPos));
|
||||
}
|
||||
}
|
||||
|
||||
return super.mouseDragged(x, y, i, f, g);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPauseScreen() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private void cancel() {
|
||||
this.dragging = false;
|
||||
this.activeSelection = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClose() {
|
||||
SableClient.GIZMO_HANDLER.stop();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package dev.ryanhcode.sable.debug;
|
||||
|
||||
import net.minecraft.core.Direction;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public record GizmoSelection(UUID subLevel, Direction.Axis axis) {
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package dev.ryanhcode.sable.debug;
|
||||
|
||||
import com.mojang.blaze3d.platform.Window;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import dev.ryanhcode.sable.companion.math.Pose3dc;
|
||||
import dev.ryanhcode.sable.api.sublevel.SubLevelContainer;
|
||||
import dev.ryanhcode.sable.sublevel.ClientSubLevel;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import foundry.veil.api.client.render.MatrixStack;
|
||||
import foundry.veil.api.event.VeilRenderLevelStageEvent;
|
||||
import foundry.veil.platform.VeilEventPlatform;
|
||||
import net.minecraft.client.Camera;
|
||||
import net.minecraft.client.DeltaTracker;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.MouseHandler;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.player.LocalPlayer;
|
||||
import net.minecraft.client.renderer.GameRenderer;
|
||||
import net.minecraft.client.renderer.LevelRenderer;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.culling.Frustum;
|
||||
import net.minecraft.client.renderer.debug.DebugRenderer;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.Vec3i;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.*;
|
||||
|
||||
import java.lang.Math;
|
||||
|
||||
|
||||
/**
|
||||
* Handler for debug client gizmos
|
||||
*/
|
||||
public class SableClientGizmoHandler {
|
||||
|
||||
private Vec3 mouseDir = Vec3.ZERO;
|
||||
private boolean enabled = false;
|
||||
private @Nullable GizmoSelection selection;
|
||||
|
||||
public void init() {
|
||||
VeilEventPlatform.INSTANCE.onVeilRenderLevelStage(this::onRenderStage);
|
||||
}
|
||||
|
||||
public static Vec3 getRay(final Matrix4fc projectionMatrix, final float normalizedMouseX, final float normalizedMouseY) {
|
||||
final Vector4f clipCoords = new Vector4f(-normalizedMouseX, -normalizedMouseY, -1.0F, 0.0F);
|
||||
final Vector4f eyeSpace = toEyeCoords(projectionMatrix, clipCoords);
|
||||
return new Vec3(eyeSpace.x, eyeSpace.y, eyeSpace.z).normalize();
|
||||
}
|
||||
|
||||
private static Vector4f toEyeCoords(final Matrix4fc projectionMatrix, final Vector4fc clipCoords) {
|
||||
final Matrix4f inverse = (projectionMatrix).invert(new Matrix4f());
|
||||
final Vector4f result = new Vector4f(clipCoords.x(), clipCoords.y(), clipCoords.z(), clipCoords.w());
|
||||
result.mul(inverse);
|
||||
result.set(result.x(), result.y(), 1.0F, 0.0F);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current mouse hover selection
|
||||
* @return the current gizmo selection, or null if none is found
|
||||
*/
|
||||
public @Nullable GizmoSelection getSelection() {
|
||||
return this.selection;
|
||||
}
|
||||
|
||||
|
||||
private void onRenderStage(final VeilRenderLevelStageEvent.Stage stage,
|
||||
final LevelRenderer levelRenderer,
|
||||
final MultiBufferSource.BufferSource bufferSource,
|
||||
final MatrixStack matrixStack,
|
||||
final Matrix4fc modelViewMat,
|
||||
final Matrix4fc projMat,
|
||||
final int renderTicks,
|
||||
final DeltaTracker deltaTracker,
|
||||
final Camera camera,
|
||||
final Frustum frustum) {
|
||||
|
||||
if (stage != VeilRenderLevelStageEvent.Stage.AFTER_WEATHER) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.enabled) return;
|
||||
|
||||
final float partialTicks = deltaTracker.getGameTimeDeltaPartialTick(false);
|
||||
|
||||
final Minecraft minecraft = Minecraft.getInstance();
|
||||
final ClientLevel level = minecraft.level;
|
||||
final Vec3 cameraPos = camera.getPosition();
|
||||
final SubLevelContainer container = SubLevelContainer.getContainer(level);
|
||||
assert container != null;
|
||||
|
||||
this.updateMouseDir(minecraft, partialTicks);
|
||||
this.updateSelection();
|
||||
|
||||
final PoseStack poseStack = new PoseStack();
|
||||
for (final SubLevel subLevel : container.getAllSubLevels()) {
|
||||
final ClientSubLevel clientSubLevel = (ClientSubLevel) subLevel;
|
||||
|
||||
final Pose3dc renderPose = clientSubLevel.renderPose();
|
||||
final Vector3d renderPos = renderPose.position().sub(cameraPos.x, cameraPos.y, cameraPos.z, new Vector3d());
|
||||
|
||||
poseStack.pushPose();
|
||||
poseStack.translate(renderPos.x, renderPos.y, renderPos.z);
|
||||
|
||||
DebugRenderer.renderFilledBox(poseStack, bufferSource, new AABB(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f).inflate(0.1d), 1.0f, 1.0f, 1.0f, 0.4f);
|
||||
|
||||
for (final Direction.Axis axis : Direction.Axis.VALUES) {
|
||||
final Direction dir = Direction.get(Direction.AxisDirection.POSITIVE, axis);
|
||||
final Vec3i normal = dir.getNormal();
|
||||
|
||||
float r = (float) (Math.max(normal.getX(), 0.2) * 0.8);
|
||||
float g = (float) (Math.max(normal.getY(), 0.2) * 0.8);
|
||||
float b = (float) (Math.max(normal.getZ(), 0.2) * 0.8);
|
||||
|
||||
final Vec3 normalD = new Vec3(normal.getX(), normal.getY(), normal.getZ());
|
||||
final Vec3 expandDir = normalD
|
||||
.scale(2.0f);
|
||||
|
||||
final float inflation = 0.04f;
|
||||
final AABB bb = new AABB(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f).inflate(inflation).move(normalD.scale(0.125)).expandTowards(expandDir);
|
||||
|
||||
|
||||
if (this.selection != null && this.selection.subLevel().equals(clientSubLevel.getUniqueId()) && this.selection.axis() == axis) {
|
||||
r *= 1.2f;
|
||||
g *= 1.2f;
|
||||
b *= 1.2f;
|
||||
}
|
||||
|
||||
|
||||
DebugRenderer.renderFilledBox(poseStack, bufferSource, bb, r, g, b, 0.9f);
|
||||
}
|
||||
poseStack.popPose();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSelection() {
|
||||
final Minecraft minecraft = Minecraft.getInstance();
|
||||
final ClientLevel level = minecraft.level;
|
||||
final Vec3 cameraPos = minecraft.gameRenderer.getMainCamera().getPosition();
|
||||
|
||||
final PoseStack poseStack = new PoseStack();
|
||||
|
||||
final SubLevelContainer container = SubLevelContainer.getContainer(level);
|
||||
assert container != null;
|
||||
|
||||
for (final SubLevel subLevel : container.getAllSubLevels()) {
|
||||
final ClientSubLevel clientSubLevel = (ClientSubLevel) subLevel;
|
||||
|
||||
final Pose3dc renderPose = clientSubLevel.renderPose();
|
||||
final Vector3d renderPos = renderPose.position().sub(cameraPos.x, cameraPos.y, cameraPos.z, new Vector3d());
|
||||
|
||||
poseStack.pushPose();
|
||||
poseStack.translate(renderPos.x, renderPos.y, renderPos.z);
|
||||
|
||||
for (final Direction.Axis axis : Direction.Axis.VALUES) {
|
||||
final Direction dir = Direction.get(Direction.AxisDirection.POSITIVE, axis);
|
||||
final Vec3i normal = dir.getNormal();
|
||||
|
||||
final Vec3 normalD = new Vec3(normal.getX(), normal.getY(), normal.getZ());
|
||||
final Vec3 expandDir = normalD
|
||||
.scale(2.0f);
|
||||
|
||||
final float inflation = 0.04f;
|
||||
final AABB bb = new AABB(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f).inflate(inflation).move(normalD.scale(0.125)).expandTowards(expandDir);
|
||||
|
||||
if (bb.move(renderPos.x, renderPos.y, renderPos.z).inflate(0.1f).clip(Vec3.ZERO, this.mouseDir.scale(100.0)).isPresent()) {
|
||||
this.selection = new GizmoSelection(clientSubLevel.getUniqueId(), axis);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No selection found
|
||||
this.selection = null;
|
||||
}
|
||||
|
||||
private void updateMouseDir(final Minecraft minecraft, final float partialTicks) {
|
||||
final LocalPlayer player = minecraft.player;
|
||||
|
||||
final Window window = minecraft.getWindow();
|
||||
final MouseHandler mouseHandler = minecraft.mouseHandler;
|
||||
|
||||
final double xPos = mouseHandler.xpos() / (double) window.getScreenWidth() * 2.0 - 1.0;
|
||||
final double yPos = mouseHandler.ypos() / (double) window.getScreenHeight() * 2.0 - 1.0;
|
||||
|
||||
final GameRenderer gameRenderer = minecraft.gameRenderer;
|
||||
final double fov = gameRenderer.getFov(gameRenderer.getMainCamera(), partialTicks, true);
|
||||
final Matrix4f proj = gameRenderer.getProjectionMatrix(fov);
|
||||
|
||||
final float yaw = player.getViewYRot(partialTicks);
|
||||
final float pitch = player.getViewXRot(partialTicks);
|
||||
|
||||
this.mouseDir = getRay(proj, (float) xPos, (float) yPos).xRot((float) -Math.toRadians(pitch)).yRot((float) -Math.toRadians(yaw));
|
||||
}
|
||||
|
||||
public void start() {
|
||||
final Minecraft minecraft = Minecraft.getInstance();
|
||||
minecraft.setScreen(new GizmoScreen());
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
final Minecraft minecraft = Minecraft.getInstance();
|
||||
if (minecraft.screen instanceof GizmoScreen) {
|
||||
minecraft.setScreen(null);
|
||||
}
|
||||
this.enabled = false;
|
||||
}
|
||||
|
||||
public Vec3 getMouseDir() {
|
||||
return this.mouseDir;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package dev.ryanhcode.sable.index;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.ai.attributes.*;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public class SableAttributes {
|
||||
|
||||
public static final String PUNCH_STRENGTH_NAME = "player.sub_level_punch_strength";
|
||||
public static final Attribute PUNCH_STRENGTH_ATTRIBUTE = new RangedAttribute("attribute.name." + PUNCH_STRENGTH_NAME, 1.0, -100.0, 100.0).setSyncable(true);
|
||||
|
||||
public static final String PUNCH_COOLDOWN_NAME = "player.sub_level_punch_cooldown";
|
||||
public static final Attribute PUNCH_COOLDOWN_ATTRIBUTE = new RangedAttribute("attribute.name." + PUNCH_COOLDOWN_NAME, 0.0, 0, 10).setSyncable(true);
|
||||
|
||||
/**
|
||||
* Assigned by loader-specific code
|
||||
*/
|
||||
public static Holder<Attribute> PUNCH_STRENGTH;
|
||||
public static Holder<Attribute> PUNCH_COOLDOWN;
|
||||
|
||||
public static void register() {
|
||||
|
||||
final AttributeSupplier supplier = DefaultAttributes.getSupplier(EntityType.PLAYER);
|
||||
|
||||
final Map<Holder<Attribute>, AttributeInstance> additionalInstances = AttributeSupplier.builder().add(PUNCH_STRENGTH).add(PUNCH_COOLDOWN).build().instances;
|
||||
|
||||
// java was tweaking with generics
|
||||
//noinspection unchecked,rawtypes
|
||||
supplier.instances = (Map<Holder<Attribute>, AttributeInstance>) (ImmutableMap) ImmutableMap.builder()
|
||||
.putAll(supplier.instances)
|
||||
.putAll(additionalInstances)
|
||||
.buildKeepingLast();
|
||||
}
|
||||
|
||||
public static int getPushCooldownTicks(final LivingEntity entity) {
|
||||
return Mth.ceil(Objects.requireNonNull(entity.getAttribute(PUNCH_COOLDOWN)).getValue() * 20);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package dev.ryanhcode.sable.index;
|
||||
|
||||
import dev.ryanhcode.sable.Sable;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
|
||||
public class SableTags {
|
||||
public static final TagKey<EntityType<?>> RETAIN_IN_SUB_LEVEL = TagKey.create(
|
||||
Registries.ENTITY_TYPE,
|
||||
Sable.sablePath("retain_in_sub_level")
|
||||
);
|
||||
public static final TagKey<EntityType<?>> DESTROY_WITH_SUB_LEVEL = TagKey.create(
|
||||
Registries.ENTITY_TYPE,
|
||||
Sable.sablePath("destroy_with_sub_level")
|
||||
);
|
||||
public static final TagKey<EntityType<?>> DESTROY_WHEN_LEAVING_PLOT = TagKey.create(
|
||||
Registries.ENTITY_TYPE,
|
||||
Sable.sablePath("destroy_when_leaving_plot")
|
||||
);
|
||||
|
||||
public static final TagKey<Block> ALWAYS_CHUNK_RENDERING = TagKey.create(
|
||||
Registries.BLOCK,
|
||||
Sable.sablePath("always_chunk_rendering")
|
||||
);
|
||||
|
||||
public static final TagKey<Block> BOUNCY = TagKey.create(
|
||||
Registries.BLOCK,
|
||||
Sable.sablePath("bouncy")
|
||||
);
|
||||
|
||||
public static final TagKey<Item> PADDLES = TagKey.create(
|
||||
Registries.ITEM,
|
||||
Sable.sablePath("paddles")
|
||||
);
|
||||
|
||||
public static void register() {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package dev.ryanhcode.sable.index;
|
||||
|
||||
import net.minecraft.client.gui.components.toasts.SystemToast;
|
||||
|
||||
public class SableToasts {
|
||||
public static final SystemToast.SystemToastId SUB_LEVEL_LOAD_FAILURE = new SystemToast.SystemToastId();
|
||||
public static final SystemToast.SystemToastId SUB_LEVEL_SAVE_FAILURE = new SystemToast.SystemToastId();
|
||||
public static final SystemToast.SystemToastId SUB_LEVEL_PHYSICS_FAILURE = new SystemToast.SystemToastId();
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package dev.ryanhcode.sable.mixin;
|
||||
|
||||
import com.mojang.logging.LogUtils;
|
||||
import dev.ryanhcode.sable.annotation.MixinModVersionConstraint;
|
||||
import dev.ryanhcode.sable.platform.SableLoaderPlatform;
|
||||
import foundry.veil.Veil;
|
||||
import foundry.veil.api.compat.SodiumCompat;
|
||||
import it.unimi.dsi.fastutil.objects.Object2BooleanMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2BooleanOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
||||
import org.apache.maven.artifact.versioning.ArtifactVersion;
|
||||
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
|
||||
import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
|
||||
import org.apache.maven.artifact.versioning.VersionRange;
|
||||
import org.objectweb.asm.Type;
|
||||
import org.objectweb.asm.tree.AnnotationNode;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
import org.slf4j.Logger;
|
||||
import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin;
|
||||
import org.spongepowered.asm.mixin.extensibility.IMixinInfo;
|
||||
import org.spongepowered.asm.service.MixinService;
|
||||
import org.spongepowered.asm.util.Annotations;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public abstract class AbstractSableMixinPlugin implements IMixinConfigPlugin {
|
||||
public static final Logger LOGGER = LogUtils.getLogger();
|
||||
private final Object2BooleanMap<String> modLoadedCache = new Object2BooleanOpenHashMap<>();
|
||||
private boolean sodiumPresent;
|
||||
|
||||
@Override
|
||||
public void onLoad(final String mixinPackage) {
|
||||
this.sodiumPresent = SodiumCompat.isLoaded();
|
||||
|
||||
LOGGER.info("Using {} renderer mixins", this.sodiumPresent ? "Sodium" : "Vanilla");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRefMapperConfig() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldApplyMixin(final String targetClassName, final String mixinClassName) {
|
||||
// TODO: Housekeeping
|
||||
if (mixinClassName.startsWith("dev.ryanhcode.sable.mixin.sublevel_render.impl")) {
|
||||
return this.sodiumPresent ? mixinClassName.startsWith("dev.ryanhcode.sable.mixin.sublevel_render.impl.sodium") : mixinClassName.startsWith("dev.ryanhcode.sable.mixin.sublevel_render.impl.vanilla");
|
||||
}
|
||||
|
||||
if (mixinClassName.startsWith("dev.ryanhcode.sable.mixin.compatibility.") ||
|
||||
mixinClassName.startsWith("dev.ryanhcode.sable.neoforge.mixin.compatibility.") ||
|
||||
mixinClassName.startsWith("dev.ryanhcode.sable.fabric.mixin.compatibility.")
|
||||
) {
|
||||
final String[] parts = mixinClassName.split("\\.");
|
||||
if (parts.length < 5) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final String modId = parts[3].equals("mixin") ? parts[5] : parts[6];
|
||||
|
||||
final boolean isModLoaded = this.modLoadedCache.computeIfAbsent(modId, x -> Veil.platform().isModLoaded(modId));
|
||||
return isModLoaded && MixinConstraints.handleClassAnnotation(mixinClassName, modId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void acceptTargets(final Set<String> myTargets, final Set<String> otherTargets) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getMixins() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preApply(final String targetClassName, final ClassNode targetClass, final String mixinClassName, final IMixinInfo mixinInfo) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postApply(final String targetClassName, final ClassNode targetClass, final String mixinClassName, final IMixinInfo mixinInfo) {
|
||||
}
|
||||
|
||||
// Constraint handling
|
||||
static class MixinConstraints {
|
||||
private static final Object2ObjectMap<String, String> MOD_VERSION_CACHE = new Object2ObjectOpenHashMap<>();
|
||||
|
||||
// Looks for if there's a @MixinModVersionConstraint annotation which declares a range for when a mixin should be loaded
|
||||
static boolean handleClassAnnotation(final String mixinClassName, final String modId) {
|
||||
try {
|
||||
final List<AnnotationNode> nodes = MixinService.getService().getBytecodeProvider().getClassNode(mixinClassName).visibleAnnotations;
|
||||
if (nodes == null)
|
||||
return true;
|
||||
|
||||
return shouldApply(nodes, modId);
|
||||
} catch (final Throwable e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
static boolean shouldApply(final List<AnnotationNode> nodes, final String modId) throws InvalidVersionSpecificationException {
|
||||
for (final AnnotationNode node : nodes) {
|
||||
if (node.desc.equals(Type.getDescriptor(MixinModVersionConstraint.class))) {
|
||||
final String range = Annotations.getValue(node, "value");
|
||||
final VersionRange versionRange = VersionRange.createFromVersionSpec(range);
|
||||
|
||||
final String modVersion = MOD_VERSION_CACHE.computeIfAbsent(modId, x -> SableLoaderPlatform.INSTANCE.getModVersion(modId));
|
||||
final ArtifactVersion artifactVersion = new DefaultArtifactVersion(modVersion);
|
||||
|
||||
return versionRange.containsVersion(artifactVersion);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package dev.ryanhcode.sable.mixin.assembly;
|
||||
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.Clearable;
|
||||
import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity;
|
||||
import net.minecraft.world.level.block.entity.BaseContainerBlockEntity;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
|
||||
@Mixin(AbstractFurnaceBlockEntity.class)
|
||||
public abstract class AbstractFurnaceBlockEntityMixin extends BaseContainerBlockEntity implements Clearable {
|
||||
|
||||
@Shadow @Final private Object2IntOpenHashMap<ResourceLocation> recipesUsed;
|
||||
|
||||
protected AbstractFurnaceBlockEntityMixin(final BlockEntityType<?> blockEntityType, final BlockPos blockPos, final BlockState blockState) {
|
||||
super(blockEntityType, blockPos, blockState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearContent() {
|
||||
super.clearContent();
|
||||
this.recipesUsed.clear();
|
||||
}
|
||||
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package dev.ryanhcode.sable.mixin.block_decal_render;
|
||||
|
||||
import com.llamalad7.mixinextras.sugar.Local;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import dev.ryanhcode.sable.Sable;
|
||||
import dev.ryanhcode.sable.companion.math.Pose3dc;
|
||||
import dev.ryanhcode.sable.sublevel.ClientSubLevel;
|
||||
import net.minecraft.client.Camera;
|
||||
import net.minecraft.client.DeltaTracker;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.renderer.GameRenderer;
|
||||
import net.minecraft.client.renderer.LevelRenderer;
|
||||
import net.minecraft.client.renderer.LightTexture;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Quaternionf;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Constant;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.ModifyConstant;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
/**
|
||||
* Changes the distance block damage is rendered from, and transforms block damage rendering for sublevels.
|
||||
*/
|
||||
@Mixin(LevelRenderer.class)
|
||||
public abstract class LevelRendererMixin {
|
||||
|
||||
// Storage vectors to avoid repeated allocation
|
||||
private final @Unique Quaternionf sable$orientationStorage = new Quaternionf();
|
||||
|
||||
@Shadow
|
||||
@Nullable
|
||||
private ClientLevel level;
|
||||
|
||||
@Inject(method = "renderLevel", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/vertex/PoseStack;last()Lcom/mojang/blaze3d/vertex/PoseStack$Pose;", shift = At.Shift.BEFORE))
|
||||
private void sable$preRenderBlockDamage(final DeltaTracker deltaTracker, final boolean bl, final Camera camera, final GameRenderer gameRenderer, final LightTexture lightTexture, final Matrix4f matrix4f, final Matrix4f matrix4f2, final CallbackInfo ci, @Local(ordinal = 0) final PoseStack ps, @Local(ordinal = 0) final BlockPos pos) {
|
||||
|
||||
final Vec3 plotPos = new Vec3(pos.getX(), pos.getY(), pos.getZ());
|
||||
final ClientSubLevel subLevel = (ClientSubLevel) Sable.HELPER.getContaining(this.level, plotPos);
|
||||
|
||||
if (subLevel == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Pose3dc renderPose = subLevel.renderPose();
|
||||
final Vec3 cameraPos = camera.getPosition();
|
||||
final Vec3 projectedPos = renderPose.transformPosition(plotPos);
|
||||
|
||||
ps.popPose();
|
||||
ps.pushPose();
|
||||
|
||||
ps.translate(projectedPos.x - cameraPos.x, projectedPos.y - cameraPos.y, projectedPos.z - cameraPos.z);
|
||||
ps.mulPose(this.sable$orientationStorage.set(renderPose.orientation()));
|
||||
}
|
||||
|
||||
@ModifyConstant(method = "renderLevel", constant = @Constant(doubleValue = 1024.0, ordinal = 0))
|
||||
private double sable$blockDamageDistance(final double originalBlockDamageDistanceConstant) {
|
||||
return Double.MAX_VALUE;
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package dev.ryanhcode.sable.mixin.block_decal_render;
|
||||
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.Constant;
|
||||
import org.spongepowered.asm.mixin.injection.ModifyConstant;
|
||||
|
||||
/**
|
||||
* Fixes {@link net.minecraft.network.protocol.game.ClientboundBlockDestructionPacket ClientboundBlockDestructionPackets} not being sent to players outside of a hardcoded range
|
||||
*/
|
||||
@Mixin(ServerLevel.class)
|
||||
public class ServerLevelMixin {
|
||||
|
||||
@ModifyConstant(method = "destroyBlockProgress", constant = @Constant(doubleValue = 1024.0, ordinal = 0))
|
||||
private double sable$blockDamageDistance(final double originalBlockDamageDistanceConstant) {
|
||||
return Double.MAX_VALUE;
|
||||
}
|
||||
|
||||
}
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
package dev.ryanhcode.sable.mixin.block_placement;
|
||||
|
||||
import dev.ryanhcode.sable.Sable;
|
||||
import dev.ryanhcode.sable.api.SubLevelHelper;
|
||||
import dev.ryanhcode.sable.companion.math.BoundingBox3d;
|
||||
import dev.ryanhcode.sable.companion.math.BoundingBox3dc;
|
||||
import dev.ryanhcode.sable.api.math.LevelReusedVectors;
|
||||
import dev.ryanhcode.sable.api.math.OrientedBoundingBox3d;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.context.BlockPlaceContext;
|
||||
import net.minecraft.world.item.context.UseOnContext;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Quaterniond;
|
||||
import org.joml.Vector3d;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
/**
|
||||
* Fixes the rotation of block placement to take into account orientation
|
||||
* <p>
|
||||
* TODO: account for differing collision shapes
|
||||
*/
|
||||
@Mixin(BlockPlaceContext.class)
|
||||
public abstract class BlockPlaceContextMixin extends UseOnContext {
|
||||
|
||||
@Unique
|
||||
private final LevelReusedVectors sable$sink = new LevelReusedVectors();
|
||||
@Shadow
|
||||
protected boolean replaceClicked;
|
||||
|
||||
public BlockPlaceContextMixin(final Player pPlayer, final InteractionHand pHand, final BlockHitResult pHitResult) {
|
||||
super(pPlayer, pHand, pHitResult);
|
||||
}
|
||||
|
||||
@Shadow
|
||||
public abstract BlockPos getClickedPos();
|
||||
|
||||
@Redirect(method = "*", at = @At(value = "INVOKE", target = "Lnet/minecraft/core/Direction;getFacingAxis(Lnet/minecraft/world/entity/Entity;Lnet/minecraft/core/Direction$Axis;)Lnet/minecraft/core/Direction;"))
|
||||
private Direction sable$getFacingAxis(final Entity player, final Direction.Axis axis) {
|
||||
final SubLevel subLevel = Sable.HELPER.getContaining(this.getLevel(), this.getClickedPos());
|
||||
|
||||
if (subLevel != null) {
|
||||
SubLevelHelper.pushEntityLocal(subLevel, player);
|
||||
final Direction facingAxis = Direction.getFacingAxis(player, axis);
|
||||
SubLevelHelper.popEntityLocal(subLevel, player);
|
||||
return facingAxis;
|
||||
}
|
||||
|
||||
return Direction.getFacingAxis(player, axis);
|
||||
}
|
||||
|
||||
@Redirect(method = "*", at = @At(value = "INVOKE", target = "Lnet/minecraft/core/Direction;orderedByNearest(Lnet/minecraft/world/entity/Entity;)[Lnet/minecraft/core/Direction;"))
|
||||
private Direction[] sable$orderedByNearest(final Entity player) {
|
||||
final SubLevel subLevel = Sable.HELPER.getContaining(this.getLevel(), this.getClickedPos());
|
||||
|
||||
if (subLevel != null) {
|
||||
SubLevelHelper.pushEntityLocal(subLevel, player);
|
||||
final Direction[] nearest = Direction.orderedByNearest(player);
|
||||
SubLevelHelper.popEntityLocal(subLevel, player);
|
||||
return nearest;
|
||||
}
|
||||
|
||||
return Direction.orderedByNearest(player);
|
||||
}
|
||||
|
||||
@Inject(method = "canPlace", at = @At("HEAD"), cancellable = true)
|
||||
private void sable$canPlace(final CallbackInfoReturnable<Boolean> cir) {
|
||||
final BlockPos clicked = this.getClickedPos();
|
||||
final SubLevel subLevel = Sable.HELPER.getContaining(this.getLevel(), this.getClickedPos());
|
||||
|
||||
final BoundingBox3d placedBoxBoundingBox = new BoundingBox3d(clicked);
|
||||
final Quaterniond placedBoxOrientation = new Quaterniond();
|
||||
|
||||
final Vector3d placedBoxPosition = new Vector3d(clicked.getX() + 0.5, clicked.getY() + 0.5, clicked.getZ() + 0.5);
|
||||
|
||||
if (subLevel != null) {
|
||||
subLevel.logicalPose().transformPosition(placedBoxPosition);
|
||||
placedBoxOrientation.set(subLevel.logicalPose().orientation());
|
||||
|
||||
placedBoxBoundingBox.transform(subLevel.logicalPose(), placedBoxBoundingBox);
|
||||
}
|
||||
|
||||
final Iterable<SubLevel> subLevels = Sable.HELPER.getAllIntersecting(this.getLevel(), placedBoxBoundingBox);
|
||||
|
||||
for (final SubLevel otherSubLevel : subLevels) {
|
||||
if (otherSubLevel == subLevel) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final boolean cancelled = this.sable$intersectBlocks(cir, otherSubLevel, placedBoxBoundingBox, this.sable$sink, placedBoxPosition, placedBoxOrientation);
|
||||
if (cancelled)
|
||||
return;
|
||||
}
|
||||
|
||||
this.sable$intersectBlocks(cir, null, placedBoxBoundingBox, this.sable$sink, placedBoxPosition, placedBoxOrientation);
|
||||
}
|
||||
|
||||
@Unique
|
||||
private boolean sable$intersectBlocks(final CallbackInfoReturnable<Boolean> cir, @Nullable final SubLevel otherSubLevel, final BoundingBox3dc placedBoxBoundingBox, final LevelReusedVectors sink, final Vector3d placedBoxPosition, final Quaterniond placedBoxOrientation) {
|
||||
final BoundingBox3d localBase = placedBoxBoundingBox.expand(0.8660254038 - 0.5, new BoundingBox3d());
|
||||
|
||||
if (otherSubLevel != null) {
|
||||
localBase.transformInverse(otherSubLevel.logicalPose(), localBase);
|
||||
}
|
||||
|
||||
// all blocks
|
||||
final Iterable<BlockPos> stream = BlockPos.betweenClosed(Mth.floor(localBase.minX()),
|
||||
Mth.floor(localBase.minY()),
|
||||
Mth.floor(localBase.minZ()),
|
||||
Mth.floor(localBase.maxX()),
|
||||
Mth.floor(localBase.maxY()),
|
||||
Mth.floor(localBase.maxZ()));
|
||||
|
||||
for (final BlockPos position : stream) {
|
||||
final boolean replaced = replaceClicked || this.getLevel().getBlockState(position).canBeReplaced((BlockPlaceContext) (Object) this);
|
||||
|
||||
Vector3d inWorldBoxPosition = new Vector3d(position.getX() + 0.5, position.getY() + 0.5, position.getZ() + 0.5);
|
||||
final Quaterniond inWorldBoxOrientation = new Quaterniond();
|
||||
|
||||
if (otherSubLevel != null) {
|
||||
inWorldBoxPosition = otherSubLevel.logicalPose().transformPosition(inWorldBoxPosition);
|
||||
inWorldBoxOrientation.set(otherSubLevel.logicalPose().orientation());
|
||||
}
|
||||
|
||||
final OrientedBoundingBox3d inWorldBox = new OrientedBoundingBox3d(
|
||||
inWorldBoxPosition,
|
||||
new Vector3d(1.0, 1.0, 1.0), inWorldBoxOrientation, sink);
|
||||
|
||||
final OrientedBoundingBox3d justPlacedBox = new OrientedBoundingBox3d(placedBoxPosition, new Vector3d(1.0, 1.0, 1.0), placedBoxOrientation, sink);
|
||||
|
||||
if (!replaced && OrientedBoundingBox3d.sat(
|
||||
inWorldBox, justPlacedBox
|
||||
).lengthSquared() > 0.05) {
|
||||
cir.setReturnValue(false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package dev.ryanhcode.sable.mixin.block_placement;
|
||||
|
||||
import dev.ryanhcode.sable.Sable;
|
||||
import dev.ryanhcode.sable.api.SubLevelHelper;
|
||||
import dev.ryanhcode.sable.companion.math.BoundingBox3d;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.level.EntityGetter;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.shapes.BooleanOp;
|
||||
import net.minecraft.world.phys.shapes.Shapes;
|
||||
import net.minecraft.world.phys.shapes.VoxelShape;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Overwrite;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Disallows placing blocks on sub-levels inside of entities
|
||||
*/
|
||||
@Mixin(EntityGetter.class)
|
||||
public interface EntityGetterMixin {
|
||||
|
||||
@Shadow
|
||||
List<Entity> getEntities(@org.jetbrains.annotations.Nullable Entity pEntity, AABB pArea);
|
||||
|
||||
@Shadow
|
||||
List<? extends Player> players();
|
||||
|
||||
/**
|
||||
* @author RyanH
|
||||
* @reason Taking sub-levels into account
|
||||
*/
|
||||
@Overwrite
|
||||
default boolean isUnobstructed(@Nullable final Entity pEntity, final VoxelShape voxelShape) {
|
||||
if (voxelShape.isEmpty()) {
|
||||
return true;
|
||||
} else {
|
||||
for (final Entity entity : this.getEntities(pEntity, voxelShape.bounds())) {
|
||||
final AABB entityBounds = entity.getBoundingBox();
|
||||
|
||||
boolean fine = Shapes.joinIsNotEmpty(voxelShape, Shapes.create(entityBounds), BooleanOp.AND);
|
||||
|
||||
final BoundingBox3d queryBounds = new BoundingBox3d(entityBounds);
|
||||
queryBounds.expand(1.5, queryBounds);
|
||||
final Iterable<SubLevel> intersecting = Sable.HELPER.getAllIntersecting(entity.level(), queryBounds);
|
||||
|
||||
for (final SubLevel subLevel : intersecting) {
|
||||
if (fine) continue;
|
||||
|
||||
final BoundingBox3d bb = new BoundingBox3d(entityBounds);
|
||||
bb.transformInverse(subLevel.logicalPose(), bb);
|
||||
bb.expand(-0.75 / 16.0, bb);
|
||||
if (Shapes.joinIsNotEmpty(voxelShape, Shapes.create(bb.toMojang()), BooleanOp.AND))
|
||||
fine = true;
|
||||
}
|
||||
|
||||
if (!entity.isRemoved() && entity.blocksBuilding && (pEntity == null || !entity.isPassengerOfSameVehicle(pEntity)) && fine) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package dev.ryanhcode.sable.mixin.block_placement;
|
||||
|
||||
|
||||
import dev.ryanhcode.sable.Sable;
|
||||
import dev.ryanhcode.sable.api.SubLevelHelper;
|
||||
import dev.ryanhcode.sable.sublevel.SubLevel;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.context.UseOnContext;
|
||||
import net.minecraft.world.level.Level;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
/**
|
||||
* Fixes the rotation of block placement to take into account orientation
|
||||
*/
|
||||
@Mixin(UseOnContext.class)
|
||||
public abstract class UseOnContextMixin {
|
||||
|
||||
@Shadow
|
||||
@Final
|
||||
private Level level;
|
||||
@Shadow
|
||||
@Final
|
||||
@Nullable
|
||||
private Player player;
|
||||
|
||||
@Shadow
|
||||
public abstract BlockPos getClickedPos();
|
||||
|
||||
@Inject(method = "getHorizontalDirection", at = @At("HEAD"), cancellable = true)
|
||||
private void sable$getHorizontalDirection(final CallbackInfoReturnable<Direction> cir) {
|
||||
if (this.player == null) return;
|
||||
|
||||
final SubLevel subLevel = Sable.HELPER.getContaining(this.level, this.getClickedPos());
|
||||
|
||||
if (subLevel != null) {
|
||||
SubLevelHelper.pushEntityLocal(subLevel, this.player);
|
||||
final Direction dir = this.player.getDirection();
|
||||
SubLevelHelper.popEntityLocal(subLevel, this.player);
|
||||
cir.setReturnValue(dir);
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(method = "getRotation", at = @At("HEAD"), cancellable = true)
|
||||
private void sable$getRotation(final CallbackInfoReturnable<Float> cir) {
|
||||
if (this.player == null) return;
|
||||
|
||||
final SubLevel subLevel = Sable.HELPER.getContaining(this.level, this.getClickedPos());
|
||||
|
||||
if (subLevel != null) {
|
||||
SubLevelHelper.pushEntityLocal(subLevel, this.player);
|
||||
final float yRot = this.player.getYRot();
|
||||
SubLevelHelper.popEntityLocal(subLevel, this.player);
|
||||
cir.setReturnValue(yRot);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user