This commit is contained in:
2026-05-13 20:38:59 +03:00
parent 9055c9c93c
commit 5620324b2e
4904 changed files with 391162 additions and 0 deletions
@@ -0,0 +1,66 @@
package dev.ryanhcode.sable.neoforge;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.SableCommonEvents;
import dev.ryanhcode.sable.SableConfig;
import dev.ryanhcode.sable.command.SableCommand;
import dev.ryanhcode.sable.command.argument.SubLevelSelectorModifiers;
import dev.ryanhcode.sable.index.SableAttributes;
import dev.ryanhcode.sable.physics.config.FloatingBlockMaterialDataHandler;
import dev.ryanhcode.sable.physics.config.block_properties.PhysicsBlockPropertiesDefinitionLoader;
import dev.ryanhcode.sable.physics.config.dimension_physics.DimensionPhysicsData;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.world.entity.ai.attributes.Attribute;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.fml.CrashReportCallables;
import net.neoforged.fml.ModContainer;
import net.neoforged.fml.common.Mod;
import net.neoforged.fml.config.ModConfig;
import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent;
import net.neoforged.neoforge.common.NeoForge;
import net.neoforged.neoforge.event.AddReloadListenerEvent;
import net.neoforged.neoforge.event.OnDatapackSyncEvent;
import net.neoforged.neoforge.event.RegisterCommandsEvent;
import net.neoforged.neoforge.registries.DeferredRegister;
@Mod(Sable.MOD_ID)
public final class SableNeoForge {
public SableNeoForge(final ModContainer modContainer, final IEventBus modBus) {
Sable.init();
final IEventBus neoBus = NeoForge.EVENT_BUS;
neoBus.addListener(this::registerCommand);
neoBus.addListener(this::registerReloadListeners);
modBus.addListener(this::serverSetup);
neoBus.addListener(this::syncDataPack);
SubLevelSelectorModifiers.registerModifiers();
final DeferredRegister<Attribute> attributes = DeferredRegister.create(BuiltInRegistries.ATTRIBUTE, Sable.MOD_ID);
SableAttributes.PUNCH_STRENGTH = attributes.register(SableAttributes.PUNCH_STRENGTH_NAME, () -> SableAttributes.PUNCH_STRENGTH_ATTRIBUTE);
SableAttributes.PUNCH_COOLDOWN = attributes.register(SableAttributes.PUNCH_COOLDOWN_NAME, () -> SableAttributes.PUNCH_COOLDOWN_ATTRIBUTE);
attributes.register(modBus);
modContainer.registerConfig(ModConfig.Type.COMMON, SableConfig.SPEC);
CrashReportCallables.registerHeader(Sable::getCrashHeader);
}
public void registerReloadListeners(final AddReloadListenerEvent event) {
event.addListener(PhysicsBlockPropertiesDefinitionLoader.INSTANCE);
event.addListener(DimensionPhysicsData.ReloadListener.INSTANCE);
event.addListener(FloatingBlockMaterialDataHandler.ReloadListener.INSTANCE);
}
private void serverSetup(final FMLCommonSetupEvent event) {
SableAttributes.register();
}
private void registerCommand(final RegisterCommandsEvent event) {
SableCommand.register(event.getDispatcher(), event.getBuildContext());
}
private void syncDataPack(final OnDatapackSyncEvent event) {
SableCommonEvents.syncDataPacket(packet -> event.getRelevantPlayers().forEach(player -> player.connection.send(packet)));
}
}
@@ -0,0 +1,41 @@
package dev.ryanhcode.sable.neoforge;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.SableClient;
import dev.ryanhcode.sable.SableClientConfig;
import dev.ryanhcode.sable.neoforge.compatibility.flywheel.FlywheelCompatNeoForge;
import dev.ryanhcode.sable.physics.config.FloatingBlockMaterialDataHandler;
import dev.ryanhcode.sable.sublevel.render.dispatcher.SubLevelRenderDispatcher;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.fml.ModContainer;
import net.neoforged.fml.common.Mod;
import net.neoforged.fml.config.ModConfig;
import net.neoforged.fml.event.config.ModConfigEvent;
import net.neoforged.neoforge.client.event.ClientPlayerNetworkEvent;
import net.neoforged.neoforge.client.event.RegisterClientReloadListenersEvent;
import net.neoforged.neoforge.common.NeoForge;
@Mod(value = Sable.MOD_ID, dist = Dist.CLIENT)
public final class SableNeoForgeClient {
public SableNeoForgeClient(final ModContainer modContainer, final IEventBus modBus) {
final IEventBus neoBus = NeoForge.EVENT_BUS;
SableClient.init();
modContainer.registerConfig(ModConfig.Type.CLIENT, SableClientConfig.SPEC);
modBus.<ModConfigEvent.Loading>addListener(event -> SableClientConfig.onUpdate(false));
modBus.<ModConfigEvent.Reloading>addListener(event -> SableClientConfig.onUpdate(true));
neoBus.<ClientPlayerNetworkEvent.LoggingOut>addListener(event -> {
if (event.getPlayer() != null) { // LoggingOut may fire when logging in
FloatingBlockMaterialDataHandler.clearMaterials();
}
});
modBus.<RegisterClientReloadListenersEvent>addListener(event -> event.registerReloadListener((arg, arg2, arg3, arg4, executor, executor2) -> SubLevelRenderDispatcher.get().reload(arg, arg2, arg3, arg4, executor, executor2)));
if (FlywheelCompatNeoForge.FLYWHEEL_LOADED) {
Sable.LOGGER.warn("NOTE: Sable is loaded with Flywheel. Sable contains extensive shader overrides and a full light-storage replacement. Expect this to cause compatibility issues. If issues arise, please report them to the Sable issue tracker ({}) instead of the Flywheel issue tracker.", Sable.ISSUE_TRACKER_URL);
}
}
}
@@ -0,0 +1,98 @@
package dev.ryanhcode.sable.neoforge.compatibility.flywheel;
import dev.engine_room.flywheel.lib.visualization.VisualizationHelper;
import dev.ryanhcode.sable.api.sublevel.ClientSubLevelContainer;
import dev.ryanhcode.sable.api.sublevel.SubLevelContainer;
import dev.ryanhcode.sable.companion.math.Pose3d;
import dev.ryanhcode.sable.sublevel.ClientSubLevel;
import dev.ryanhcode.sable.sublevel.SubLevel;
import foundry.veil.Veil;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectMaps;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectIterator;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
import java.util.Objects;
import java.util.UUID;
public class FlywheelCompatNeoForge {
public static boolean FLYWHEEL_LOADED = Veil.platform().isModLoaded("flywheel");
private static final Long2ObjectMap<SubLevelFlwRenderState> RENDER_POSES = Long2ObjectMaps.synchronize(new Long2ObjectOpenHashMap<>());
/**
* Tries to add a flywheel visual for a block-entity.
* Done for single block sub-level renderers, because we're not compiling their sections.
*/
public static void tryAddVisual(final BlockEntity blockEntity) {
VisualizationHelper.tryAddBlockEntity(blockEntity);
}
public static void preVisualizationFrame(final Level level, final float partialTicks) {
final ClientSubLevelContainer container = (ClientSubLevelContainer) SubLevelContainer.getContainer(level);
if (container == null) {
RENDER_POSES.clear();
return;
}
final ObjectIterator<Long2ObjectMap.Entry<SubLevelFlwRenderState>> iter = RENDER_POSES.long2ObjectEntrySet().iterator();
while (iter.hasNext()) {
final Long2ObjectMap.Entry<SubLevelFlwRenderState> entry = iter.next();
final long pos = entry.getLongKey();
final SubLevelFlwRenderState poseEntry = entry.getValue();
final int plotX = ChunkPos.getX(pos);
final int plotZ = ChunkPos.getZ(pos);
final SubLevel subLevel = container.getSubLevel(plotX, plotZ);
if (subLevel == null || !Objects.equals(subLevel.getUniqueId(), poseEntry.subLevelID)) {
iter.remove();
continue;
}
// TODO: some other form of removal when we don't have any more flywheel contraption visuals to worry about?
updateEntry(container, (ClientSubLevel) subLevel, poseEntry, partialTicks);
}
}
public static SubLevelFlwRenderState getInfo(final long plotCoord) {
return RENDER_POSES.get(plotCoord);
}
private static void updateEntry(final ClientSubLevelContainer container, final ClientSubLevel clientSubLevel, final SubLevelFlwRenderState poseEntry, final float partialTicks) {
poseEntry.sceneID = container.getLightingSceneId(clientSubLevel);
poseEntry.subLevelID = clientSubLevel.getUniqueId();
poseEntry.renderPose.set(clientSubLevel.renderPose(partialTicks));
poseEntry.latestSkyLightScale = clientSubLevel.getLatestSkyLightScale();
poseEntry.centerChunk = clientSubLevel.getPlot().getCenterChunk();
}
public static void createRenderInfo(final Level level, final SubLevel subLevel) {
final ClientSubLevelContainer container = (ClientSubLevelContainer) SubLevelContainer.getContainer(level);
if (container == null) return;
final ChunkPos plotPos = subLevel.getPlot().plotPos;
final long plotCoord = ChunkPos.asLong(plotPos.x - container.getOrigin().x, plotPos.z - container.getOrigin().y);
RENDER_POSES.computeIfAbsent(plotCoord, x -> {
final SubLevelFlwRenderState renderState = new SubLevelFlwRenderState();
updateEntry(container, (ClientSubLevel) subLevel, renderState, 1.0f);
return renderState;
});
}
public static class SubLevelFlwRenderState {
public int sceneID;
public final Pose3d renderPose = new Pose3d();
public UUID subLevelID;
public float latestSkyLightScale;
public ChunkPos centerChunk;
}
}
@@ -0,0 +1,7 @@
package dev.ryanhcode.sable.neoforge.compatibility.flywheel;
public class SableFlywheelEmbeddingUniforms {
public static final String SCENE_MATRIX = "_flw_lightingSceneMatrixUniform";
public static final String SCENE = "_flw_lightingSceneUniform";
public static final String SKY_LIGHT_SCALE = "_flw_lightingSkyLightScaleUniform";
}
@@ -0,0 +1,462 @@
package dev.ryanhcode.sable.neoforge.compatibility.flywheel;
import dev.engine_room.flywheel.api.task.Plan;
import dev.engine_room.flywheel.api.visual.EffectVisual;
import dev.engine_room.flywheel.api.visualization.VisualizationContext;
import dev.engine_room.flywheel.api.visualization.VisualizationManager;
import dev.engine_room.flywheel.backend.BackendDebugFlags;
import dev.engine_room.flywheel.backend.engine.LightStorage;
import dev.engine_room.flywheel.backend.engine.indirect.StagingBuffer;
import dev.engine_room.flywheel.backend.gl.buffer.GlBuffer;
import dev.engine_room.flywheel.lib.instance.InstanceTypes;
import dev.engine_room.flywheel.lib.instance.TransformedInstance;
import dev.engine_room.flywheel.lib.task.SimplePlan;
import dev.engine_room.flywheel.lib.visual.SimpleDynamicVisual;
import dev.engine_room.flywheel.lib.visual.component.HitboxComponent;
import dev.engine_room.flywheel.lib.visual.util.InstanceRecycler;
import dev.ryanhcode.sable.ActiveSableCompanion;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.api.sublevel.ClientSubLevelContainer;
import dev.ryanhcode.sable.api.sublevel.SubLevelContainer;
import dev.ryanhcode.sable.neoforge.mixin.compatibility.flywheel.LightStorageAccessor;
import dev.ryanhcode.sable.sublevel.ClientSubLevel;
import dev.ryanhcode.sable.sublevel.SubLevel;
import dev.ryanhcode.sable.sublevel.plot.LevelPlot;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.longs.Long2IntMap;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import it.unimi.dsi.fastutil.longs.LongSet;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.LightTexture;
import net.minecraft.core.SectionPos;
import net.minecraft.core.Vec3i;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelAccessor;
import org.jetbrains.annotations.Nullable;
import org.lwjgl.system.MemoryUtil;
import java.util.BitSet;
public class SableFlywheelLightStorage extends LightStorage {
private static final int INVALID_SECTION = -1;
public static final int STATIC_SCENE_ID = 0;
private final SableLightLut sableLut;
private final Int2ObjectMap<Long2IntMap> scene2SectionArenaIndexMap;
private final BitSet changed = new BitSet();
private final LongSet updatedSections = new LongOpenHashSet();
private boolean isDebugOn = false;
@Nullable
private LongSet requestedSections;
public SableFlywheelLightStorage(final LevelAccessor level) {
super(level);
this.sableLut = new SableLightLut();
this.scene2SectionArenaIndexMap = new Int2ObjectOpenHashMap<>();
}
@Override
public EffectVisual<?> visualize(final VisualizationContext ctx, final float partialTick) {
return new SableFlywheelLightStorage.DebugVisual(ctx, partialTick);
}
@Override
public <C> Plan<C> createFramePlan() {
return SimplePlan.of(() -> {
if (BackendDebugFlags.LIGHT_STORAGE_VIEW != this.isDebugOn) {
final var visualizationManager = VisualizationManager.get(this.level());
// Really should be non-null, but just in case.
if (visualizationManager != null) {
if (BackendDebugFlags.LIGHT_STORAGE_VIEW) {
visualizationManager.effects()
.queueAdd(this);
} else {
visualizationManager.effects()
.queueRemove(this);
}
}
this.isDebugOn = BackendDebugFlags.LIGHT_STORAGE_VIEW;
}
if (this.updatedSections.isEmpty() && this.requestedSections == null) {
return;
}
this.updateLightSections();
});
}
/**
* Set the set of requested sections.
* <p> When set, this will be processed in the next frame plan. It may not be set every frame.
*
* @param sections The set of sections requested by the impl.
*/
@Override
public void sections(final LongSet sections) {
this.requestedSections = sections;
}
private void updateLightSections() {
this.removeUnusedSections();
final ActiveSableCompanion helper = Sable.HELPER;
final ClientLevel level = Minecraft.getInstance().level;
final ClientSubLevelContainer container = SubLevelContainer.getContainer(level);
final Int2ObjectMap<LongSet> sectionsToCollect;
if (this.requestedSections == null) {
sectionsToCollect = new Int2ObjectOpenHashMap<>();
} else {
sectionsToCollect = new Int2ObjectOpenHashMap<>();
for (final long section : this.requestedSections) {
final SectionPos sectionPos = SectionPos.of(section);
final SubLevel subLevel = helper.getContaining(level, sectionPos);
int lightingSceneId = 0;
if (subLevel instanceof final ClientSubLevel clientSubLevel) {
final int subLevelLightingSceneId = container.getLightingSceneId(clientSubLevel);
if (subLevelLightingSceneId != -1) {
lightingSceneId = subLevelLightingSceneId;
}
}
sectionsToCollect.computeIfAbsent(lightingSceneId, x -> new LongOpenHashSet())
.add(section);
}
}
for (final int scene : this.scene2SectionArenaIndexMap.keySet()) {
final Long2IntMap section2ArenaIndex = this.scene2SectionArenaIndexMap.get(scene);
final LongSet longs = sectionsToCollect.get(scene);
if (longs != null) {
longs.removeAll(section2ArenaIndex.keySet());
}
}
for (final long updatedSection : this.updatedSections) {
for (int x = -1; x <= 1; ++x) {
for (int y = -1; y <= 1; ++y) {
for (int z = -1; z <= 1; ++z) {
final long section = SectionPos.offset(updatedSection, x, y, z);
final SectionPos sectionPos = SectionPos.of(section);
final SubLevel subLevel = helper.getContaining(level, sectionPos);
if (subLevel instanceof final ClientSubLevel clientSubLevel) {
final int lightingSceneId = container.getLightingSceneId(clientSubLevel);
if (lightingSceneId != -1) {
final Long2IntMap map = this.scene2SectionArenaIndexMap.get(lightingSceneId);
if (map != null && map.containsKey(section)) {
sectionsToCollect.computeIfAbsent(lightingSceneId, ignored -> new LongOpenHashSet()).add(section);
}
continue;
}
}
if (this.scene2SectionArenaIndexMap.values().stream().anyMatch(map -> map.containsKey(section))) {
sectionsToCollect.computeIfAbsent(0, ignored -> new LongOpenHashSet()).add(section);
}
}
}
}
}
for (final Int2ObjectMap.Entry<LongSet> entry : sectionsToCollect.int2ObjectEntrySet()) {
final int scene = entry.getIntKey();
final LongSet sections = entry.getValue();
for (final long section : sections) {
this.collectSection(scene, section);
}
}
this.updatedSections.clear();
this.requestedSections = null;
}
public void onLightUpdate(final long section) {
this.updatedSections.add(section);
}
public void collectSection(final int scene, final long section) {
final int index = this.indexForSection(scene, section);
this.changed.set(index);
final long ptr = this.arena.indexToPointer(index);
// Zero it out first. This is basically free and makes it easier to handle missing sections later.
MemoryUtil.memSet(ptr, 0, LightStorage.SECTION_SIZE_BYTES);
((LightStorageAccessor) this).getCollector().collectSection(ptr, section);
}
private int indexForSection(final int scene, long section) {
final Long2IntMap map = this.scene2SectionArenaIndexMap.get(scene);
int out = map != null ? map.get(section) : INVALID_SECTION;
// Need to allocate.
if (out == INVALID_SECTION) {
out = this.arena.alloc();
this.scene2SectionArenaIndexMap.computeIfAbsent(scene, (ignored) -> {
final Long2IntOpenHashMap newMap = new Long2IntOpenHashMap();
newMap.defaultReturnValue(INVALID_SECTION);
return newMap;
}).put(section, out);
final SectionPos sectionPos = SectionPos.of(section);
final SubLevel subLevel = Sable.HELPER.getContainingClient(sectionPos);
if (subLevel != null) {
final LevelPlot plot = subLevel.getPlot();
final ChunkPos centerChunk = plot.getCenterChunk();
section = SectionPos.asLong(
sectionPos.x() - centerChunk.x,
sectionPos.y(),
sectionPos.z() - centerChunk.z
);
}
this.beginTrackingSection(scene, section, out);
}
return out;
}
private void removeUnusedSections() {
if (this.requestedSections == null) {
return;
}
final ClientLevel world = Minecraft.getInstance().level;
boolean anyRemoved = false;
for (final Int2ObjectMap.Entry<Long2IntMap> sceneEntry : this.scene2SectionArenaIndexMap.int2ObjectEntrySet()) {
final int sceneId = sceneEntry.getIntKey();
final Long2IntMap section2ArenaIndex = sceneEntry.getValue();
final var entries = section2ArenaIndex.long2IntEntrySet();
final var it = entries.iterator();
while (it.hasNext()) {
final var entry = it.next();
final var section = entry.getLongKey();
if (!this.requestedSections.contains(section)) {
this.arena.free(entry.getIntValue());
var localSection = section;
final SectionPos sectionPos = SectionPos.of(section);
final SubLevelContainer container = SubLevelContainer.getContainer(world);
if (container != null && container.inBounds(sectionPos.x(), sectionPos.z())) {
final int logPlotSize = container.getLogPlotSize();
final int plotX = (sectionPos.x() >> logPlotSize);
final int plotZ = (sectionPos.z() >> logPlotSize);
localSection = SectionPos.asLong(sectionPos.x() - ((plotX << logPlotSize) + (1 << (logPlotSize - 1))),
sectionPos.y(),
sectionPos.z() - ((plotZ << logPlotSize) + (1 << (logPlotSize - 1))));
}
this.endTrackingSection(sceneId, localSection);
it.remove();
anyRemoved = true;
}
}
}
if (anyRemoved) {
this.sableLut.prune();
((LightStorageAccessor)this).setNeedsLutRebuild(true);
}
}
private void beginTrackingSection(final int scene, final long section, final int index) {
this.sableLut.add(scene, section, index);
((LightStorageAccessor)this).setNeedsLutRebuild(true);
}
private void endTrackingSection(final int scene, final long section) {
this.sableLut.remove(scene, section);
((LightStorageAccessor)this).setNeedsLutRebuild(true);
}
@Override
public void uploadChangedSections(final StagingBuffer staging, final int dstVbo) {
for (int i = this.changed.nextSetBit(0); i >= 0; i = this.changed.nextSetBit(i + 1)) {
staging.enqueueCopy(this.arena.indexToPointer(i), SECTION_SIZE_BYTES, dstVbo, (long) i * SECTION_SIZE_BYTES);
}
this.changed.clear();
}
@Override
public void upload(final GlBuffer buffer) {
if (this.changed.isEmpty()) {
return;
}
buffer.upload(this.arena.indexToPointer(0), (long) this.arena.capacity() * SECTION_SIZE_BYTES);
this.changed.clear();
}
@Override
public IntArrayList createLut() {
return this.sableLut.flatten();
}
public class DebugVisual implements EffectVisual<LightStorage>, SimpleDynamicVisual {
private final InstanceRecycler<TransformedInstance> boxes;
private final Vec3i renderOrigin;
public DebugVisual(final VisualizationContext ctx, final float partialTick) {
this.renderOrigin = ctx.renderOrigin();
this.boxes = new InstanceRecycler<>(() -> ctx.instancerProvider()
.instancer(InstanceTypes.TRANSFORMED, HitboxComponent.BOX_MODEL)
.createInstance());
}
@Override
public void beginFrame(final Context ctx) {
this.boxes.resetCount();
this.setupSectionBoxes();
this.setupLutRangeBoxes();
this.boxes.discardExtra();
}
private void setupSectionBoxes() {
for (final Int2ObjectMap.Entry<Long2IntMap> entry : SableFlywheelLightStorage.this.scene2SectionArenaIndexMap.int2ObjectEntrySet()) {
final int sceneId = entry.getIntKey();
final Long2IntMap section2ArenaIndex = entry.getValue();
section2ArenaIndex.keySet()
.forEach(l -> {
final var x = SectionPos.x(l) * 16 - this.renderOrigin.getX();
final var y = SectionPos.y(l) * 16 - this.renderOrigin.getY();
final var z = SectionPos.z(l) * 16 - this.renderOrigin.getZ();
final var instance = this.boxes.get();
// Slightly smaller than a full 16x16x16 section to make it obvious which sections
// are actually represented when many are tiled next to each other.
instance.setIdentityTransform()
.translate(x + 1, y + 1, z + 1)
.scale(14)
.color(255, 255, sceneId * 64)
.light(LightTexture.FULL_BRIGHT)
.setChanged();
});
}
}
private void setupLutRangeBoxes() {
final var first = SableFlywheelLightStorage.this.sableLut.indices;
final var base1 = first.base();
final var size1 = first.size();
final float debug1 = base1 * 16 - this.renderOrigin.getY();
float min2 = Float.POSITIVE_INFINITY;
float max2 = Float.NEGATIVE_INFINITY;
float min3 = Float.POSITIVE_INFINITY;
float max3 = Float.NEGATIVE_INFINITY;
for (int y = 0; y < size1; y++) {
final var second = first.getRaw(y);
if (second == null) {
continue;
}
final var base2 = second.base();
final var size2 = second.size();
final float y2 = (base1 + y) * 16 - this.renderOrigin.getY() + 7.5f;
min2 = Math.min(min2, base2);
max2 = Math.max(max2, base2 + size2);
float minLocal3 = Float.POSITIVE_INFINITY;
float maxLocal3 = Float.NEGATIVE_INFINITY;
final float debug2 = base2 * 16 - this.renderOrigin.getX();
for (int x = 0; x < size2; x++) {
final var third = second.getRaw(x);
if (third == null) {
continue;
}
final var base3 = third.base();
final var size3 = third.size();
final float x2 = (base2 + x) * 16 - this.renderOrigin.getX() + 7.5f;
min3 = Math.min(min3, base3);
max3 = Math.max(max3, base3 + size3);
minLocal3 = Math.min(minLocal3, base3);
maxLocal3 = Math.max(maxLocal3, base3 + size3);
final float debug3 = base3 * 16 - this.renderOrigin.getZ();
for (int z = 0; z < size3; z++) {
this.boxes.get()
.setIdentityTransform()
.translate(x2, y2, debug3)
.scale(1, 1, size3 * 16)
.color(0, 0, 255)
.light(LightTexture.FULL_BRIGHT)
.setChanged();
}
}
this.boxes.get()
.setIdentityTransform()
.translate(debug2, y2, minLocal3 * 16 - this.renderOrigin.getZ())
.scale(size2 * 16, 1, (maxLocal3 - minLocal3) * 16)
.color(255, 0, 0)
.light(LightTexture.FULL_BRIGHT)
.setChanged();
}
this.boxes.get()
.setIdentityTransform()
.translate(min2 * 16 - this.renderOrigin.getX(), debug1, min3 * 16 - this.renderOrigin.getZ())
.scale((max2 - min2) * 16, size1 * 16, (max3 - min3) * 16)
.color(0, 255, 0)
.light(LightTexture.FULL_BRIGHT)
.setChanged();
}
@Override
public void update(final float partialTick) {
}
@Override
public void delete() {
this.boxes.delete();
}
}
}
@@ -0,0 +1,9 @@
package dev.ryanhcode.sable.neoforge.compatibility.flywheel;
public class SableFlywheelMatrixBuffer {
public static final int INFO_SIZE_BYTES = (16 + 12) * Float.BYTES +
Float.BYTES + // sky light scale
Integer.BYTES + // scene ID
2 * Float.BYTES + // padding
16 * Float.BYTES; // scene matrix
}
@@ -0,0 +1,389 @@
package dev.ryanhcode.sable.neoforge.compatibility.flywheel;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import net.minecraft.core.SectionPos;
import org.jetbrains.annotations.Nullable;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
/**
* A light look-up table based on the Flywheel / RogueLogix LUT scheme, with an extra layer for the lighting scene ID
* First layer is studio, Y, then X, then Z.
*/
public final class SableLightLut {
public final Layer<Layer<Layer<IntLayer>>> indices = new Layer<>();
public void add(final int scene, final long position, final int index) {
final var x = SectionPos.x(position);
final var y = SectionPos.y(position);
final var z = SectionPos.z(position);
this.indices.computeIfAbsent(scene, Layer::new)
.computeIfAbsent(y, Layer::new)
.computeIfAbsent(x, IntLayer::new)
.set(z, index + 1);
}
public void prune() {
// Maybe this could be done better incrementally?
this.indices.prune((scene) -> scene.prune((middle) -> middle.prune(IntLayer::prune)));
}
public void remove(final int scene, final long section) {
final var x = SectionPos.x(section);
final var y = SectionPos.y(section);
final var z = SectionPos.z(section);
final var first = this.indices.get(scene);
if (first == null) {
return;
}
final var second = first.get(y);
if (second == null) {
return;
}
final var third = second.get(x);
if (third == null) {
return;
}
third.clear(z);
}
public IntArrayList flatten() {
final var out = new IntArrayList();
this.indices.fillLut(out, (sceneIndices, lut1) -> sceneIndices.fillLut(lut1,
(yIndices, lut2) -> yIndices.fillLut(lut2, IntLayer::fillLut)
));
return out;
}
@FunctionalInterface
public interface Prune<T> {
boolean prune(T t);
}
public static final class Layer<T> {
private boolean hasBase = false;
private int base = 0;
private Object[] nextLayer = new Object[0];
public void fillLut(final IntArrayList lut, final BiConsumer<T, IntArrayList> inner) {
lut.add(this.base);
lut.add(this.nextLayer.length);
final int innerIndexBase = lut.size();
// Reserve space for the inner indices...
lut.size(innerIndexBase + this.nextLayer.length);
for (int i = 0; i < this.nextLayer.length; i++) {
final var innerIndices = (T) this.nextLayer[i];
if (innerIndices == null) {
continue;
}
final int layerPosition = lut.size();
// ...so we can write in their actual positions later.
lut.set(innerIndexBase + i, layerPosition);
// Append the next layer to the lut.
inner.accept(innerIndices, lut);
}
}
public int base() {
return this.base;
}
public int size() {
return this.nextLayer.length;
}
@Nullable
public T getRaw(final int i) {
if (i < 0) {
return null;
}
if (i >= this.nextLayer.length) {
return null;
}
return (T) this.nextLayer[i];
}
@Nullable
public T get(final int i) {
if (!this.hasBase) {
return null;
}
return this.getRaw(i - this.base);
}
public T computeIfAbsent(final int i, final Supplier<T> ifAbsent) {
if (!this.hasBase) {
// We don't want to default to base 0, so we'll use the first value we get.
this.base = i;
this.hasBase = true;
}
if (i < this.base) {
this.rebase(i);
}
final var offset = i - this.base;
if (offset >= this.nextLayer.length) {
this.resize(offset + 1);
}
var out = this.nextLayer[offset];
if (out == null) {
out = ifAbsent.get();
this.nextLayer[offset] = out;
}
return (T) out;
}
/**
* @return {@code true} if the layer is now empty.
*/
public boolean prune(final Prune<T> inner) {
if (!this.hasBase) {
return true;
}
// Prune the next layer before checking for leading/trailing zeros.
for (var i = 0; i < this.nextLayer.length; i++) {
final var o = this.nextLayer[i];
if (o != null && inner.prune((T) o)) {
this.nextLayer[i] = null;
}
}
final var leadingZeros = this.getLeadingZeros();
if (leadingZeros == this.nextLayer.length) {
return true;
}
final var trailingZeros = this.getTrailingZeros();
if (leadingZeros == 0 && trailingZeros == 0) {
return false;
}
final var newIndices = new Object[this.nextLayer.length - leadingZeros - trailingZeros];
System.arraycopy(this.nextLayer, leadingZeros, newIndices, 0, newIndices.length);
this.nextLayer = newIndices;
this.base += leadingZeros;
return false;
}
private int getLeadingZeros() {
int out = 0;
for (final Object index : this.nextLayer) {
if (index == null) {
out++;
} else {
break;
}
}
return out;
}
private int getTrailingZeros() {
int out = 0;
for (int i = this.nextLayer.length - 1; i >= 0; i--) {
if (this.nextLayer[i] == null) {
out++;
} else {
break;
}
}
return out;
}
private void resize(final int length) {
final var newIndices = new Object[length];
System.arraycopy(this.nextLayer, 0, newIndices, 0, this.nextLayer.length);
this.nextLayer = newIndices;
}
private void rebase(final int newBase) {
final var growth = this.base - newBase;
final var newIndices = new Object[this.nextLayer.length + growth];
// Shift the existing elements to the end of the new array to maintain their offset with the new base.
System.arraycopy(this.nextLayer, 0, newIndices, growth, this.nextLayer.length);
this.nextLayer = newIndices;
this.base = newBase;
}
}
public static final class IntLayer {
private boolean hasBase = false;
private int base = 0;
private int[] indices = new int[0];
public void fillLut(final IntArrayList lut) {
lut.add(this.base);
lut.add(this.indices.length);
for (final int index : this.indices) {
lut.add(index);
}
}
public int base() {
return this.base;
}
public int size() {
return this.indices.length;
}
public int getRaw(final int i) {
if (i < 0) {
return 0;
}
if (i >= this.indices.length) {
return 0;
}
return this.indices[i];
}
public int get(final int i) {
if (!this.hasBase) {
return 0;
}
return this.getRaw(i - this.base);
}
public void set(final int i, final int index) {
if (!this.hasBase) {
this.base = i;
this.hasBase = true;
}
if (i < this.base) {
this.rebase(i);
}
final var offset = i - this.base;
if (offset >= this.indices.length) {
this.resize(offset + 1);
}
this.indices[offset] = index;
}
/**
* @return {@code true} if the layer is now empty.
*/
public boolean prune() {
if (!this.hasBase) {
return true;
}
final var leadingZeros = this.getLeadingZeros();
if (leadingZeros == this.indices.length) {
return true;
}
final var trailingZeros = this.getTrailingZeros();
if (leadingZeros == 0 && trailingZeros == 0) {
return false;
}
final var newIndices = new int[this.indices.length - leadingZeros - trailingZeros];
System.arraycopy(this.indices, leadingZeros, newIndices, 0, newIndices.length);
this.indices = newIndices;
this.base += leadingZeros;
return false;
}
private int getTrailingZeros() {
int out = 0;
for (int i = this.indices.length - 1; i >= 0; i--) {
if (this.indices[i] == 0) {
out++;
} else {
break;
}
}
return out;
}
private int getLeadingZeros() {
int out = 0;
for (final int index : this.indices) {
if (index == 0) {
out++;
} else {
break;
}
}
return out;
}
public void clear(final int i) {
if (!this.hasBase) {
return;
}
if (i < this.base) {
return;
}
final var offset = i - this.base;
if (offset >= this.indices.length) {
return;
}
this.indices[offset] = 0;
}
private void resize(final int length) {
final var newIndices = new int[length];
System.arraycopy(this.indices, 0, newIndices, 0, this.indices.length);
this.indices = newIndices;
}
private void rebase(final int newBase) {
final var growth = this.base - newBase;
final var newIndices = new int[this.indices.length + growth];
System.arraycopy(this.indices, 0, newIndices, growth, this.indices.length);
this.indices = newIndices;
this.base = newBase;
}
}
}
@@ -0,0 +1,29 @@
package dev.ryanhcode.sable.neoforge.event;
import dev.ryanhcode.sable.sublevel.system.SubLevelPhysicsSystem;
import net.neoforged.bus.api.Event;
/**
* 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.
*/
public class ForgeSablePostPhysicsTickEvent extends Event {
private final SubLevelPhysicsSystem physicsSystem;
private final double timeStep;
public ForgeSablePostPhysicsTickEvent(final SubLevelPhysicsSystem physicsSystem, final double timeStep) {
this.physicsSystem = physicsSystem;
this.timeStep = timeStep;
}
public SubLevelPhysicsSystem getPhysicsSystem() {
return this.physicsSystem;
}
public double getTimeStep() {
return this.timeStep;
}
}
@@ -0,0 +1,29 @@
package dev.ryanhcode.sable.neoforge.event;
import dev.ryanhcode.sable.sublevel.system.SubLevelPhysicsSystem;
import net.neoforged.bus.api.Event;
/**
* 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.
*/
public class ForgeSablePrePhysicsTickEvent extends Event {
private final SubLevelPhysicsSystem physicsSystem;
private final double timeStep;
public ForgeSablePrePhysicsTickEvent(final SubLevelPhysicsSystem physicsSystem, final double timeStep) {
this.physicsSystem = physicsSystem;
this.timeStep = timeStep;
}
public SubLevelPhysicsSystem getPhysicsSystem() {
return this.physicsSystem;
}
public double getTimeStep() {
return this.timeStep;
}
}
@@ -0,0 +1,26 @@
package dev.ryanhcode.sable.neoforge.event;
import dev.ryanhcode.sable.api.sublevel.SubLevelContainer;
import net.minecraft.world.level.Level;
import net.neoforged.bus.api.Event;
/**
* Fired when Sable has finished initialization for a level and its sub-level container is ready to use.
*/
public class ForgeSableSubLevelContainerReadyEvent extends Event {
private final Level level;
private final SubLevelContainer container;
public ForgeSableSubLevelContainerReadyEvent(final Level level, final SubLevelContainer container) {
this.level = level;
this.container = container;
}
public Level getLevel() {
return this.level;
}
public SubLevelContainer getContainer() {
return this.container;
}
}
@@ -0,0 +1,92 @@
package dev.ryanhcode.sable.neoforge.gametest;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.api.SubLevelAssemblyHelper;
import dev.ryanhcode.sable.companion.math.BoundingBox3i;
import dev.ryanhcode.sable.companion.math.BoundingBox3ic;
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 net.minecraft.core.BlockPos;
import net.minecraft.gametest.framework.GameTest;
import net.minecraft.gametest.framework.GameTestAssertPosException;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.state.BlockState;
import net.neoforged.neoforge.gametest.GameTestHolder;
import org.joml.Quaterniond;
import org.joml.Vector3d;
import org.joml.Vector3i;
import org.joml.Vector3ic;
import java.util.ArrayList;
import java.util.List;
@GameTestHolder(Sable.MOD_ID)
public final class AssemblyTest {
@GameTest(template = "brittlebreak")
public static void testBrittleBreaking(final GameTestHelper helper) {
final ServerLevel level = helper.getLevel();
final ServerSubLevelContainer plotContainer = SubLevelContainer.getContainer(level);
if (plotContainer == null) {
throw new IllegalStateException("Plot container not found in level");
}
final SubLevelPhysicsSystem physicsSystem = plotContainer.physicsSystem();
if (physicsSystem == null) {
throw new IllegalStateException("Plot container does not have physics");
}
final BlockPos min = helper.absolutePos(new BlockPos(0, 1, 0));
final BlockPos max = helper.absolutePos(new BlockPos(2, 3, 2));
final BoundingBox3i bounds = new BoundingBox3i(
min.getX(), min.getY(), min.getZ(),
max.getX(), max.getY(), max.getZ()
);
final List<BlockState> expectedStates = new ArrayList<>(bounds.volume());
for (final BlockPos pos : BlockPos.betweenClosed(min, max)) {
expectedStates.add(level.getBlockState(pos));
}
final ServerSubLevel subLevel = SubLevelAssemblyHelper.assembleBlocks(level, min, BlockPos.betweenClosed(min, max), bounds);
physicsSystem.getPipeline().teleport(subLevel,
new Vector3d(min.getX() + (1 + max.getX() - min.getX()) / 2.0,
min.getY() + (1 + max.getY() - min.getY()) / 2.0,
min.getZ() + (1 + max.getZ() - min.getZ()) / 2.0),
helper.getTestRotation().rotation().transformation().getNormalizedRotation(new Quaterniond()));
helper.runAtTickTime(10, () -> {
final Level plot = subLevel.getLevel();
final BoundingBox3ic sublevelBounds = subLevel.getPlot().getBoundingBox();
final Vector3ic actualSize = sublevelBounds.size(new Vector3i());
final Vector3ic expectedSize = bounds.size(new Vector3i());
if (actualSize.equals(expectedSize)) {
int i = 0;
for (final BlockPos pos : BlockPos.betweenClosed(sublevelBounds.minX(),
sublevelBounds.minY(),
sublevelBounds.minZ(),
sublevelBounds.maxX(),
sublevelBounds.maxY(),
sublevelBounds.maxZ())) {
final BlockState expected = expectedStates.get(i);
if (!plot.getBlockState(pos).equals(expected)) {
throw new GameTestAssertPosException("Expected %s".formatted(expected.getBlock().getName().getString()), pos, pos, helper.getTick());
}
i++;
}
helper.succeed();
} else {
helper.fail("Expected %dx%dx%d region, got %dx%dx%d".formatted(
expectedSize.x(),
expectedSize.y(),
expectedSize.z(),
actualSize.x(),
actualSize.y(),
actualSize.z()));
}
});
}
}
@@ -0,0 +1,133 @@
package dev.ryanhcode.sable.neoforge.gametest;
import dev.ryanhcode.sable.Sable;
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.physics.config.dimension_physics.DimensionPhysicsData;
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
import dev.ryanhcode.sable.sublevel.system.SubLevelPhysicsSystem;
import net.minecraft.core.BlockPos;
import net.minecraft.gametest.framework.GameTest;
import net.minecraft.gametest.framework.GameTestAssertPosException;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.Blocks;
import net.neoforged.neoforge.gametest.GameTestHolder;
import org.joml.Vector3d;
import org.joml.Vector3dc;
import static dev.ryanhcode.sable.neoforge.gametest.SableTestHelper.*;
@GameTestHolder(Sable.MOD_ID)
public final class PhysicsTest {
@GameTest(template = "continuouscollision")
public static void testContinuousCollision(final GameTestHelper helper) {
final ServerLevel level = helper.getLevel();
final ServerSubLevelContainer plotContainer = SubLevelContainer.getContainer(level);
if (plotContainer == null) {
throw new IllegalStateException("Plot container not found in level");
}
final SubLevelPhysicsSystem physicsSystem = plotContainer.physicsSystem();
if (physicsSystem == null) {
throw new IllegalStateException("Plot container does not have physics");
}
final ServerSubLevel subLevel = spawnSingleBlockSubLevel(plotContainer, absolutePosition(helper, new Vector3d(2.5, 4, 1.5)), Blocks.GLASS.defaultBlockState());
final RigidBodyHandle handle = physicsSystem.getPhysicsHandle(subLevel);
final Vector3d impulse = absoluteDirection(helper, new Vector3d(0, 10, 20));
helper.startSequence()
.thenExecuteAfter(10, () -> handle.applyLinearImpulse(impulse))
.thenExecuteFor(40, () -> {
final Vector3d globalPos = subLevel.logicalPose().position();
final Vector3d localPos = localPosition(helper, globalPos);
if (localPos.z >= 9 || !isInBounds(helper, globalPos)) {
helper.fail("Sublevel passed through wall", BlockPos.containing(localPos.x, localPos.y, localPos.z));
}
}).thenSucceed();
}
// FIXME allow manual tests to run automatically when rapier is set to 64-bit mode
@GameTest(template = "gravity", required = false)
public static void testGravity(final GameTestHelper helper) {
final ServerLevel level = helper.getLevel();
final ServerSubLevelContainer plotContainer = SubLevelContainer.getContainer(level);
if (plotContainer == null) {
throw new IllegalStateException("Plot container not found in level");
}
final SubLevelPhysicsSystem physicsSystem = plotContainer.physicsSystem();
if (physicsSystem == null) {
throw new IllegalStateException("Plot container does not have physics");
}
final Vector3dc spawnPos = absolutePosition(helper, new Vector3d(2.5, 12, 2.5));
final ServerSubLevel subLevel = spawnSingleBlockSubLevel(plotContainer, spawnPos, Blocks.DIAMOND_BLOCK.defaultBlockState());
helper.runAfterDelay(20, () -> {
if (subLevel.isRemoved()) {
helper.fail("Sublevel was removed");
return;
}
final Vector3dc gravity = DimensionPhysicsData.getGravity(helper.getLevel(), spawnPos);
final RigidBodyHandle handle = physicsSystem.getPhysicsHandle(subLevel);
final Vector3dc linearVelocity = handle.getLinearVelocity(new Vector3d());
if (!gravity.equals(linearVelocity, 1e-2)) {
final Vector3d localPos = localPosition(helper, spawnPos);
helper.fail("Sublevel velocity didn't follow gravity: Delta: " + gravity.distance(linearVelocity), BlockPos.containing(localPos.x, localPos.y, localPos.z));
return;
}
// 1/2 * a * t * t
// t = 1
final Vector3d expectedDelta = gravity.mul(0.5, new Vector3d());
final Vector3d delta = subLevel.logicalPose().position().sub(spawnPos, new Vector3d());
// FIXME for some reason the sublevels don't have a consistent distance travelled. It seems like they aren't spawned on the first tick?
if (!expectedDelta.equals(delta, 1e-2)) {
final Vector3d localPos = localPosition(helper, spawnPos);
helper.fail("Sublevel position didn't follow gravity. Delta: " + expectedDelta.distance(delta), BlockPos.containing(localPos.x, localPos.y, localPos.z));
}
helper.succeed();
});
}
@GameTest(template = "snag", attempts = 10, requiredSuccesses = 10, required = false)
public static void testSnag(final GameTestHelper helper) {
final ServerLevel level = helper.getLevel();
final ServerSubLevelContainer plotContainer = SubLevelContainer.getContainer(level);
if (plotContainer == null) {
throw new IllegalStateException("Plot container not found in level");
}
final SubLevelPhysicsSystem physicsSystem = plotContainer.physicsSystem();
if (physicsSystem == null) {
throw new IllegalStateException("Plot container does not have physics");
}
final Vector3dc spawnPos = absolutePosition(helper, new Vector3d(13, 3.5, 3.5));
final ServerSubLevel subLevel = spawnSingleBlockSubLevel(plotContainer, spawnPos, Blocks.DIAMOND_BLOCK.defaultBlockState());
final RigidBodyHandle handle = physicsSystem.getPhysicsHandle(subLevel);
final Vector3d impulse = absoluteDirection(helper, new Vector3d(-60, 0, 0));
helper.startSequence()
.thenExecuteAfter(10, () -> handle.applyLinearImpulse(impulse))
.thenExecuteFor(40, () -> {
final Vector3d globalPos = subLevel.logicalPose().position();
final Vector3d localPos = localPosition(helper, globalPos);
if (localPos.x <= 9 && isInBounds(helper, globalPos)) {
helper.succeed();
}
}).thenFail(() -> {
final Vector3dc position = subLevel.logicalPose().position();
final BlockPos globalPos = BlockPos.containing(position.x(), position.y(), position.z());
return new GameTestAssertPosException("Sub-level got stuck", globalPos, helper.relativePos(globalPos), helper.getTick());
});
}
}
@@ -0,0 +1,81 @@
package dev.ryanhcode.sable.neoforge.gametest;
import dev.ryanhcode.sable.companion.math.Pose3d;
import dev.ryanhcode.sable.api.sublevel.SubLevelContainer;
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
import dev.ryanhcode.sable.sublevel.SubLevel;
import dev.ryanhcode.sable.sublevel.plot.LevelPlot;
import net.minecraft.core.BlockPos;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.CommonLevelAccessor;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.AABB;
import org.joml.Vector3d;
import org.joml.Vector3dc;
import java.util.function.Consumer;
public final class SableTestHelper {
public static ServerSubLevel spawnSubLevel(final SubLevelContainer plotContainer, final Vector3dc pos, final Consumer<CommonLevelAccessor> setter) {
final Pose3d pose = new Pose3d();
pose.position().set(pos);
final SubLevel subLevel = plotContainer.allocateNewSubLevel(pose);
final LevelPlot plot = subLevel.getPlot();
final ChunkPos center = plot.getCenterChunk();
plot.newEmptyChunk(center);
setter.accept(plot.getEmbeddedLevelAccessor());
subLevel.updateLastPose();
return (ServerSubLevel) subLevel;
}
public static ServerSubLevel spawnSingleBlockSubLevel(final SubLevelContainer plotContainer, final Vector3dc pos, final BlockState state) {
return spawnSubLevel(plotContainer, pos, accessor -> accessor.setBlock(BlockPos.ZERO, state, 3));
}
public static Vector3d absoluteDirection(final GameTestHelper helper, final Vector3dc localDirection) {
return new Vector3d(localDirection).rotateY(-getAngle(helper.getTestRotation()));
}
public static Vector3d localDirection(final GameTestHelper helper, final Vector3dc globalDirection) {
return new Vector3d(globalDirection).rotateY(getAngle(helper.getTestRotation()));
}
public static Vector3d absolutePosition(final GameTestHelper helper, final Vector3dc localPosition) {
final BlockPos origin = helper.testInfo.getStructureBlockPos();
final Vector3d pos = localPosition.sub(0.5, 0.5, 0.5, new Vector3d()).rotateY(-getAngle(helper.getTestRotation()));
return pos.add(origin.getX() + 0.5, origin.getY() + 0.5, origin.getZ() + 0.5);
}
public static Vector3d localPosition(final GameTestHelper helper, final Vector3dc globalPosition) {
final BlockPos origin = helper.testInfo.getStructureBlockPos();
final Vector3d pos = globalPosition.sub(origin.getX(), origin.getY(), origin.getZ(), new Vector3d());
return pos.rotateY(getAngle(helper.getTestRotation()));
}
public static double getAngle(final Rotation rotation) {
return switch (rotation) {
case NONE -> 0;
case CLOCKWISE_90 -> Math.PI / 2.0;
case CLOCKWISE_180 -> Math.PI;
case COUNTERCLOCKWISE_90 -> -Math.PI / 2.0;
};
}
/**
* Checks if the specified global position is within the bounds of the test.
*
* @param helper The game test helper instance
* @param globalPosition The position in global space to check
* @return Whether that position is inside the bounds of the test
*/
public static boolean isInBounds(final GameTestHelper helper, final Vector3dc globalPosition) {
final AABB box = helper.getBounds();
return box.contains(globalPosition.x(), globalPosition.y(), globalPosition.z());
}
}
@@ -0,0 +1,41 @@
package dev.ryanhcode.sable.neoforge.mixin.block_entity_visible;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.companion.math.BoundingBox3d;
import dev.ryanhcode.sable.sublevel.SubLevel;
import net.minecraft.client.renderer.LevelRenderer;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderDispatcher;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
import net.minecraft.client.renderer.culling.Frustum;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.phys.AABB;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(value = LevelRenderer.class, priority = 2000)
public class LevelRendererMixin {
/**
* @author RyanH
* @reason Take sub-levels into account for visibility check
*/
@SuppressWarnings("unchecked")
@Redirect(method = "renderLevel", at = @At(value = "INVOKE", target = "Lnet/neoforged/neoforge/client/ClientHooks;isBlockEntityRendererVisible(Lnet/minecraft/client/renderer/blockentity/BlockEntityRenderDispatcher;Lnet/minecraft/world/level/block/entity/BlockEntity;Lnet/minecraft/client/renderer/culling/Frustum;)Z"), require = 0)
private static <T extends BlockEntity> boolean isBlockEntityRendererVisible(final BlockEntityRenderDispatcher dispatcher, final BlockEntity blockEntity, final Frustum frustum) {
final BlockEntityRenderer<T> renderer = (BlockEntityRenderer<T>) dispatcher.getRenderer(blockEntity);
if (renderer == null) return false;
AABB renderBounds = renderer.getRenderBoundingBox((T) blockEntity);
final SubLevel subLevel = Sable.HELPER.getContainingClient(renderBounds.getCenter());
if (subLevel != null) {
final BoundingBox3d bb = new BoundingBox3d(renderBounds);
renderBounds = bb.transform(subLevel.logicalPose(), bb).toMojang();
}
return frustum.isVisible(renderBounds);
}
}
@@ -0,0 +1,114 @@
package dev.ryanhcode.sable.neoforge.mixin.block_outline_render;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import com.llamalad7.mixinextras.sugar.Share;
import com.llamalad7.mixinextras.sugar.ref.LocalBooleanRef;
import com.mojang.blaze3d.vertex.PoseStack;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.companion.math.Pose3dc;
import dev.ryanhcode.sable.mixinhelpers.block_outline_render.SubLevelCamera;
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.LevelRenderer;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.core.BlockPos;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
import net.minecraft.world.phys.Vec3;
import org.jetbrains.annotations.Nullable;
import org.joml.Quaterniondc;
import org.joml.Quaternionf;
import org.joml.Vector3dc;
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.ModifyArg;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/**
* Transforms block hover outlines for sublevels.
*/
@Mixin(value = LevelRenderer.class, priority = 2000) // This makes sure it applies after normal mixins
public abstract class LevelRendererMixin {
// Storage vectors to avoid repeated allocation
private final @Unique Quaternionf sable$orientationStorage = new Quaternionf();
private final @Unique SubLevelCamera sable$sublevelCamera = new SubLevelCamera();
@Shadow
@Nullable
private ClientLevel level;
@WrapOperation(method = "renderLevel", at = @At(value = "INVOKE", target = "Lnet/neoforged/neoforge/client/ClientHooks;onDrawHighlight(Lnet/minecraft/client/renderer/LevelRenderer;Lnet/minecraft/client/Camera;Lnet/minecraft/world/phys/HitResult;Lnet/minecraft/client/DeltaTracker;Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;)Z"))
private boolean sable$preRenderHitOutline(final LevelRenderer context, final Camera camera, final HitResult target, final DeltaTracker deltaTracker, final PoseStack poseStack, final MultiBufferSource bufferSource, final Operation<Boolean> original, @Share("drawn") final LocalBooleanRef drawnRef) {
if (!(target instanceof final BlockHitResult blockTarget)) {
return original.call(context, camera, target, deltaTracker, poseStack, bufferSource);
}
final BlockPos blockPos = blockTarget.getBlockPos();
final ClientSubLevel subLevel = (ClientSubLevel) Sable.HELPER.getContaining(this.level, blockPos);
if (subLevel == null) {
return original.call(context, camera, target, deltaTracker, poseStack, bufferSource);
}
poseStack.pushPose();
final Pose3dc pose = subLevel.renderPose();
this.sable$sublevelCamera.setCamera(camera);
this.sable$sublevelCamera.setPose(pose);
final Vec3 cameraPosition = this.sable$sublevelCamera.getPosition();
final Vec3 realCameraPosition = camera.getPosition();
final Vector3dc position = pose.position();
final Vector3dc rotationPoint = pose.rotationPoint();
final Quaterniondc orientation = pose.orientation();
final Vector3dc scale = pose.scale();
poseStack.translate(
(float) (position.x() - realCameraPosition.x),
(float) (position.y() - realCameraPosition.y),
(float) (position.z() - realCameraPosition.z)
);
poseStack.mulPose(this.sable$orientationStorage.set(orientation));
poseStack.translate(
(float) -(rotationPoint.x() - cameraPosition.x),
(float) -(rotationPoint.y() - cameraPosition.y),
(float) -(rotationPoint.z() - cameraPosition.z)
);
poseStack.scale((float) scale.x(), (float) scale.y(), (float) scale.z());
drawnRef.set(true);
return original.call(context, this.sable$sublevelCamera, target, deltaTracker, poseStack, bufferSource);
}
@Inject(method = "renderLevel", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/debug/DebugRenderer;render(Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource$BufferSource;DDD)V"))
public void sable$poseRenderHitOutline(final CallbackInfo ci, @Local final PoseStack poseStack, @Share("drawn") final LocalBooleanRef drawnRef) {
if (drawnRef.get()) {
poseStack.popPose();
this.sable$sublevelCamera.clear();
}
}
@ModifyArg(method = "renderLevel", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/LevelRenderer;renderHitOutline(Lcom/mojang/blaze3d/vertex/PoseStack;Lcom/mojang/blaze3d/vertex/VertexConsumer;Lnet/minecraft/world/entity/Entity;DDDLnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;)V"), index = 3)
public double modifyX(final double original, @Share("drawn") final LocalBooleanRef drawnRef) {
return drawnRef.get() ? this.sable$sublevelCamera.getPosition().x : original;
}
@ModifyArg(method = "renderLevel", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/LevelRenderer;renderHitOutline(Lcom/mojang/blaze3d/vertex/PoseStack;Lcom/mojang/blaze3d/vertex/VertexConsumer;Lnet/minecraft/world/entity/Entity;DDDLnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;)V"), index = 4)
public double modifyY(final double original, @Share("drawn") final LocalBooleanRef drawnRef) {
return drawnRef.get() ? this.sable$sublevelCamera.getPosition().y : original;
}
@ModifyArg(method = "renderLevel", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/LevelRenderer;renderHitOutline(Lcom/mojang/blaze3d/vertex/PoseStack;Lcom/mojang/blaze3d/vertex/VertexConsumer;Lnet/minecraft/world/entity/Entity;DDDLnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;)V"), index = 5)
public double modifyZ(final double original, @Share("drawn") final LocalBooleanRef drawnRef) {
return drawnRef.get() ? this.sable$sublevelCamera.getPosition().z : original;
}
}
@@ -0,0 +1,105 @@
package dev.ryanhcode.sable.neoforge.mixin.camera_rotation;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.sugar.Local;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.companion.math.Pose3dc;
import dev.ryanhcode.sable.mixinhelpers.camera.camera_rotation.EntitySubLevelRotationHelper;
import dev.ryanhcode.sable.sublevel.ClientSubLevel;
import net.minecraft.client.Camera;
import net.minecraft.client.Minecraft;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec3;
import net.neoforged.neoforge.client.event.ViewportEvent;
import org.joml.Quaterniond;
import org.joml.Quaternionf;
import org.joml.Vector3f;
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.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(Camera.class)
public abstract class CameraMixin {
@Shadow
@Final
private static Vector3f FORWARDS;
@Shadow
@Final
private static Vector3f UP;
@Shadow
@Final
private static Vector3f LEFT;
@Shadow
@Final
private Quaternionf rotation;
@Shadow
@Final
private Vector3f left;
@Shadow
@Final
private Vector3f up;
@Shadow
@Final
private Vector3f forwards;
@Shadow
private float yRot;
@Shadow
private float xRot;
@Shadow
private Entity entity;
@Shadow protected abstract void setRotation(float f, float g, float roll);
@Redirect(method = "setup", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;setRotation(FFF)V", ordinal = 1))
private void sable$redirectSetRotation(final Camera camera, final float f, final float g, final float roll, @Local final ViewportEvent.ComputeCameraAngles event) {
this.setRotation(event.getYaw() + 180.0f, -event.getPitch(), roll);
}
@WrapMethod(method = "setPosition(Lnet/minecraft/world/phys/Vec3;)V")
private void sable$setPosition(final Vec3 arg, final Operation<Void> original) {
if (this.entity == null) {
original.call(arg);
return;
}
final Level level = this.entity.level();
final ClientSubLevel subLevel = (ClientSubLevel) Sable.HELPER.getContaining(level, arg);
if(subLevel == null) {
original.call(arg);
return;
}
final Pose3dc pose = subLevel.renderPose();
final Vec3 pos = pose.transformPosition(arg);
original.call(pos);
}
@Inject(method = "setRotation(FFF)V", at = @At(value = "INVOKE", target = "Lorg/joml/Quaternionf;rotationYXZ(FFF)Lorg/joml/Quaternionf;", shift = At.Shift.AFTER))
public void sable$rotateView(final float f, final float g, final float roll, final CallbackInfo ci) {
final float pt = Minecraft.getInstance().getTimer().getGameTimeDeltaPartialTick(true);
final Quaterniond ridingOrientation = EntitySubLevelRotationHelper.getEntityOrientation(this.entity, (x) -> ((ClientSubLevel) x).renderPose(), pt, EntitySubLevelRotationHelper.Type.CAMERA);
if (ridingOrientation != null) {
this.rotation.premul(new Quaternionf(ridingOrientation));
FORWARDS.rotate(this.rotation, this.forwards);
UP.rotate(this.rotation, this.up);
LEFT.rotate(this.rotation, this.left);
final Vector3f euler = this.rotation.getEulerAnglesYXZ(new Vector3f());
this.yRot = -180.0f - (float) Math.toDegrees(euler.y);
this.xRot = (float) -Math.toDegrees(euler.x);
}
}
}
@@ -0,0 +1,32 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.backpacks;
import com.llamalad7.mixinextras.sugar.Local;
import com.llamalad7.mixinextras.sugar.ref.LocalBooleanRef;
import com.spydnel.backpacks.events.BackpackPickupEvents;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.sublevel.SubLevel;
import net.minecraft.core.BlockPos;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.phys.Vec3;
import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
@Mixin(BackpackPickupEvents.class)
public class BackpackPickupEventsMixin {
@Inject(method = "onRightClickBlock", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/Level;isUnobstructed(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/phys/shapes/CollisionContext;)Z"), locals = LocalCapture.CAPTURE_FAILEXCEPTION)
private static void sable$onRightClickBlock(final PlayerInteractEvent.RightClickBlock event, final CallbackInfo ci, @Local(name = "isAbove") final LocalBooleanRef isAbove) {
final Player player = event.getEntity();
final BlockPos pos = event.getPos();
final SubLevel containing = Sable.HELPER.getContaining(player.level(), pos);
if (containing != null) {
final Vec3 world = containing.logicalPose().transformPosition(pos.above().getBottomCenter());
isAbove.set(world.y - 0.1 > player.getEyeY());
}
}
}
@@ -0,0 +1,84 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.airflow;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import com.simibubi.create.content.kinetics.fan.AirCurrent;
import com.simibubi.create.content.kinetics.fan.IAirCurrentSource;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.companion.math.BoundingBox3d;
import dev.ryanhcode.sable.companion.math.JOMLConversion;
import dev.ryanhcode.sable.sublevel.SubLevel;
import net.minecraft.core.Vec3i;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import org.joml.Vector3d;
import org.spongepowered.asm.mixin.Final;
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.CallbackInfo;
import java.lang.ref.WeakReference;
@Mixin(AirCurrent.class)
public abstract class AirCurrentMixin {
@Shadow
@Final
public IAirCurrentSource source;
@Unique
private WeakReference<SubLevel> sable$subLevelReference;
@Inject(method = "tick", at = @At("HEAD"))
public void sable$updateSubLevel(final CallbackInfo ci) {
if (this.sable$subLevelReference == null) {
this.sable$subLevelReference = new WeakReference<>(Sable.HELPER.getContaining(this.source.getAirCurrentWorld(), this.source.getAirCurrentPos()));
}
}
@Redirect(method = "tickAffectedEntities", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;getBoundingBox()Lnet/minecraft/world/phys/AABB;"))
public AABB sable$reverseProjectEntityBB(final Entity instance) {
final SubLevel subLevel = this.sable$subLevelReference.get();
if (subLevel != null) {
return new BoundingBox3d(instance.getBoundingBox()).transformInverse(subLevel.logicalPose(), new BoundingBox3d()).toMojang();
}
return instance.getBoundingBox();
}
@WrapOperation(method = "tickAffectedEntities", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;setDeltaMovement(Lnet/minecraft/world/phys/Vec3;)V"))
public void sable$transformFlowVector(final Entity instance, final Vec3 vec3, final Operation<Void> original, @Local final Vec3i flow, @Local(ordinal = 2) final float acceleration, @Local final Vec3 previousMotion, @Local(ordinal = 3) final float maxAcceleration) {
final SubLevel subLevel = this.sable$subLevelReference.get();
if (subLevel != null) {
final Vector3d nonIntFlow = JOMLConversion.atLowerCornerOf(flow);
subLevel.logicalPose().transformNormal(nonIntFlow);
final double xIn = Mth.clamp((double) ((float) nonIntFlow.get(0) * acceleration) - previousMotion.x, -maxAcceleration, maxAcceleration);
final double yIn = Mth.clamp((double) ((float) nonIntFlow.get(1) * acceleration) - previousMotion.y, -maxAcceleration, maxAcceleration);
final double zIn = Mth.clamp((double) ((float) nonIntFlow.get(2) * acceleration) - previousMotion.z, -maxAcceleration, maxAcceleration);
original.call(instance, previousMotion.add(new Vec3(xIn, yIn, zIn).scale(0.125f)));
return;
}
original.call(instance, vec3);
}
@Redirect(method = "tickAffectedEntities", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;position()Lnet/minecraft/world/phys/Vec3;"))
public Vec3 sable$reverseProjectAllPositions(final Entity instance) {
final SubLevel subLevel = this.sable$subLevelReference.get();
if (subLevel != null) {
return subLevel.logicalPose().transformPositionInverse(instance.position());
}
return instance.position();
}
}
@@ -0,0 +1,20 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.airflow;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.simibubi.create.content.kinetics.fan.processing.FanProcessingType;
import dev.ryanhcode.sable.ActiveSableCompanion;
import dev.ryanhcode.sable.Sable;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level;
import org.spongepowered.asm.mixin.Mixin;
@Mixin(FanProcessingType.class)
public interface FanProcessingTypeMixin {
@WrapMethod(method = "getAt")
private static FanProcessingType getAt(Level level, BlockPos pos, Operation<FanProcessingType> original) {
ActiveSableCompanion helper = Sable.HELPER;
return helper.runIncludingSubLevels(level, pos.getCenter(), true, helper.getContaining(level, pos),
(subLevel, relativePos) -> original.call(level, relativePos));
}
}
@@ -0,0 +1,60 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.basin_interactions;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.simibubi.create.content.processing.basin.BasinBlockEntity;
import com.simibubi.create.content.processing.burner.BlazeBurnerBlock;
import dev.ryanhcode.sable.ActiveSableCompanion;
import dev.ryanhcode.sable.Sable;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import org.jetbrains.annotations.Nullable;
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;
@Mixin(BasinBlockEntity.class)
public class BasinBlockEntityMixin extends BlockEntity {
@Shadow @Nullable private BlazeBurnerBlock.@Nullable HeatLevel cachedHeatLevel;
public BasinBlockEntityMixin(final BlockEntityType<?> arg, final BlockPos arg2, final BlockState arg3) {
super(arg, arg2, arg3);
}
@Inject(method = "getHeatLevel", at = @At(value = "INVOKE", target = "Lcom/simibubi/create/content/processing/basin/BasinBlockEntity;getHeatLevelOf(Lnet/minecraft/world/level/block/state/BlockState;)Lcom/simibubi/create/content/processing/burner/BlazeBurnerBlock$HeatLevel;", shift = At.Shift.AFTER), cancellable = true)
private void sable$accountForSubLevels(final CallbackInfoReturnable<BlazeBurnerBlock.HeatLevel> cir) {
if (this.cachedHeatLevel != null && this.cachedHeatLevel != BlazeBurnerBlock.HeatLevel.NONE) {
return;
}
final Level level = this.getLevel();
final BlockPos originalPos = this.getBlockPos().below();
final ActiveSableCompanion helper = Sable.HELPER;
final BlazeBurnerBlock.HeatLevel heatLevel = helper.runIncludingSubLevels(level, originalPos.getCenter(), false, helper.getContaining(level, originalPos), (subLevel, pos) -> {
final BlazeBurnerBlock.HeatLevel internalHeat = BasinBlockEntity.getHeatLevelOf(level.getBlockState(pos));
if (internalHeat != BlazeBurnerBlock.HeatLevel.NONE) {
return internalHeat;
}
return null;
});
if (heatLevel != null) {
cir.setReturnValue(heatLevel);
}
}
@WrapOperation(method = "*", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/Level;getBlockEntity(Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/level/block/entity/BlockEntity;"))
public BlockEntity sable$accountForSubLevels(final Level level, final BlockPos pos, final Operation<BlockEntity> original) {
final ActiveSableCompanion helper = Sable.HELPER;
return helper.runIncludingSubLevels(level, pos.getCenter(), true, helper.getContaining(level, pos), (subLevel, internalPos) -> original.call(level, internalPos));
}
}
@@ -0,0 +1,42 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.basin_interactions;
import com.simibubi.create.content.processing.basin.BasinOperatingBlockEntity;
import com.simibubi.create.foundation.blockEntity.behaviour.simple.DeferralBehaviour;
import dev.ryanhcode.sable.ActiveSableCompanion;
import dev.ryanhcode.sable.Sable;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
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.CallbackInfo;
@Mixin(BasinOperatingBlockEntity.class)
public abstract class BasinOperatingBlockEntityMixin {
@Shadow
public DeferralBehaviour basinChecker;
@Unique
private int sable$forceUpdateTicks = 0;
@Inject(method = "tick", at = @At("HEAD"), remap = false)
private void sable$forceUpdate(final CallbackInfo ci) {
if (this.sable$forceUpdateTicks == 5) { //only update every 5 ticks
this.basinChecker.scheduleUpdate();
this.sable$forceUpdateTicks = 0;
}
this.sable$forceUpdateTicks++;
}
@Redirect(method = "getBasin", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/Level;getBlockEntity(Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/level/block/entity/BlockEntity;"))
private BlockEntity sable$accountForSubLevels(final Level level, final BlockPos pos) {
final ActiveSableCompanion helper = Sable.HELPER;
return helper.runIncludingSubLevels(level, pos.getCenter(), true, helper.getContaining(level, pos), (subLevel, internalPos) -> level.getBlockEntity(internalPos));
}
}
@@ -0,0 +1,50 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.behaviour_compatibility;
import com.simibubi.create.content.kinetics.belt.behaviour.BeltProcessingBehaviour;
import com.simibubi.create.content.kinetics.belt.behaviour.DirectBeltInputBehaviour;
import com.simibubi.create.content.kinetics.belt.behaviour.TransportedItemStackHandlerBehaviour;
import com.simibubi.create.foundation.blockEntity.behaviour.BehaviourType;
import com.simibubi.create.foundation.blockEntity.behaviour.BlockEntityBehaviour;
import com.simibubi.create.foundation.blockEntity.behaviour.edgeInteraction.EdgeInteractionBehaviour;
import com.simibubi.create.foundation.blockEntity.behaviour.inventory.InvManipulationBehaviour;
import dev.ryanhcode.sable.ActiveSableCompanion;
import dev.ryanhcode.sable.Sable;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
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.callback.CallbackInfoReturnable;
@Mixin(BlockEntityBehaviour.class)
public abstract class BlockEntityBehaviourMixin {
@Shadow
public static <T extends BlockEntityBehaviour> T get(final BlockEntity be, final BehaviourType<T> type) {
return null;
}
@Inject(method = "get(Lnet/minecraft/world/level/BlockGetter;Lnet/minecraft/core/BlockPos;Lcom/simibubi/create/foundation/blockEntity/behaviour/BehaviourType;)Lcom/simibubi/create/foundation/blockEntity/behaviour/BlockEntityBehaviour;",
at = @At(value = "HEAD"), remap = false, cancellable = true)
private static <T extends BlockEntityBehaviour> void sable$accountForSubLevels(final BlockGetter reader, final BlockPos pos, final BehaviourType<T> type, final CallbackInfoReturnable<T> cir) {
if (reader instanceof final Level level && BlockEntityBehaviourMixin.sable$checkType(type)) {
final ActiveSableCompanion helper = Sable.HELPER;
final BlockEntity caughtBE = helper.runIncludingSubLevels(level, pos.getCenter(), true, helper.getContaining(level, pos), (subLevel, internalPos) -> level.getBlockEntity(internalPos));
if (caughtBE != null) {
cir.setReturnValue(get(caughtBE, type));
}
}
}
@Unique
private static boolean sable$checkType(final BehaviourType<?> type) {
return type == BeltProcessingBehaviour.TYPE || type == DirectBeltInputBehaviour.TYPE ||
type == TransportedItemStackHandlerBehaviour.TYPE || type == InvManipulationBehaviour.TYPE ||
type == EdgeInteractionBehaviour.TYPE;
}
}
@@ -0,0 +1,94 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.behaviour_compatibility.block_breaking_behaviour;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.simibubi.create.api.behaviour.movement.MovementBehaviour;
import com.simibubi.create.content.contraptions.behaviour.MovementContext;
import com.simibubi.create.content.kinetics.base.BlockBreakingMovementBehaviour;
import dev.ryanhcode.sable.ActiveSableCompanion;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.neoforge.mixinhelper.compatibility.create.block_breakers.SubLevelBlockBreakingUtility;
import dev.ryanhcode.sable.sublevel.SubLevel;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.Vec3;
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.CallbackInfo;
@Mixin(BlockBreakingMovementBehaviour.class)
public abstract class BlockBreakingMovementBehaviourMixin implements MovementBehaviour {
@Shadow
public abstract boolean canBreak(Level world, BlockPos breakingPos, BlockState state);
@WrapMethod(method = "visitNewPosition")
public void sable$checkPosition(final MovementContext context, final BlockPos pos, final Operation<Void> original) {
if (!context.stall) {
original.call(context, pos);
if (!context.stall) {
final Vec3 localCenter = context.localPos.getCenter();
final Vec3 sublevelLocalCenter = context.contraption.entity.toGlobalVector(localCenter, 1);
final Vec3 subLevelLocalDir = context.rotation.apply(this.getActiveAreaOffset(context));
final BlockPos breakingPosWSublevel = SubLevelBlockBreakingUtility.findBreakingPos(
(blockPos, state) -> this.canBreak(context.world, blockPos, state),
Sable.HELPER.getContaining(context.world, context.contraption.anchor),
context.world,
subLevelLocalDir,
sublevelLocalCenter,
pos
);
original.call(context, breakingPosWSublevel);
}
}
}
@Inject(method = "tick", at = @At("HEAD"), cancellable = true)
public void sable$testBreakingPosDist(final MovementContext context, final CallbackInfo ci) {
final CompoundTag data = context.data;
if (data.contains("BreakingPos") || data.contains("LastPos")) {
final BlockPos blockPos = NbtUtils.readBlockPos(data, "BreakingPos").orElseGet(() -> NbtUtils.readBlockPos(data, "LastPos").orElse(null));
if (blockPos != null) {
final Vec3 localCenter = context.localPos.getCenter();
Vec3 sublevelLocalCenter = context.contraption.entity.toGlobalVector(localCenter, 1);
Vec3 targetCenter = blockPos.getCenter();
final ActiveSableCompanion helper = Sable.HELPER;
final SubLevel parentSublevel = helper.getContaining(context.world, context.contraption.anchor);
final SubLevel targetSubLevel = helper.getContaining(context.world, blockPos);
if (parentSublevel != null) {
sublevelLocalCenter = parentSublevel.logicalPose().transformPosition(sublevelLocalCenter);
}
if (targetSubLevel != null) {
targetCenter = targetSubLevel.logicalPose().transformPosition(targetCenter);
}
if (sublevelLocalCenter.distanceToSqr(targetCenter) > 2 * 2) {
data.remove("Progress");
data.remove("TicksUntilNextProgress");
data.remove("BreakingPos");
data.remove("LastPos");
data.remove("WaitingTicks");
context.stall = false;
context.world.destroyBlockProgress(data.getInt("BreakerId"), blockPos, -1);
ci.cancel();
}
}
}
}
}
@@ -0,0 +1,38 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.behaviour_compatibility.block_breaking_behaviour;
import com.llamalad7.mixinextras.sugar.Local;
import com.simibubi.create.content.contraptions.behaviour.MovementContext;
import com.simibubi.create.content.kinetics.saw.SawMovementBehaviour;
import dev.ryanhcode.sable.ActiveSableCompanion;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.sublevel.SubLevel;
import net.minecraft.world.phys.Vec3;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(SawMovementBehaviour.class)
public class SawMovementBehaviourMixin {
@Redirect(method = "dropItemFromCutTree", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/phys/Vec3;distanceTo(Lnet/minecraft/world/phys/Vec3;)D"))
public double sable$fixSpeed(final Vec3 instance, final Vec3 vec3, @Local(argsOnly = true) final MovementContext context) {
return Math.sqrt(Sable.HELPER.distanceSquaredWithSubLevels(context.world, instance, vec3));
}
@Redirect(method = "dropItemFromCutTree", at = @At(value = "FIELD", target = "Lcom/simibubi/create/content/contraptions/behaviour/MovementContext;relativeMotion:Lnet/minecraft/world/phys/Vec3;"))
public Vec3 sable$fixRelativeMotion(final MovementContext instance, @Local(argsOnly = true) final MovementContext context, @Local(ordinal = 0) final Vec3 dropPos) {
final ActiveSableCompanion helper = Sable.HELPER;
final SubLevel parentSublevel = helper.getContaining(context.world, context.contraption.anchor);
final SubLevel targetSublevel = helper.getContaining(context.world, dropPos);
Vec3 orignalMotion = context.relativeMotion;
if (parentSublevel != null) {
orignalMotion = parentSublevel.logicalPose().transformNormal(orignalMotion);
}
if (targetSublevel != null) {
orignalMotion = targetSublevel.logicalPose().transformNormalInverse(orignalMotion);
}
return orignalMotion;
}
}
@@ -0,0 +1,30 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.behaviour_compatibility.harvester_behaviour;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.simibubi.create.content.contraptions.actors.harvester.HarvesterMovementBehaviour;
import com.simibubi.create.content.contraptions.behaviour.MovementContext;
import dev.ryanhcode.sable.ActiveSableCompanion;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.neoforge.mixinhelper.compatibility.create.behavior_compatibility.harvester_block_entity.DummyMovementContext;
import net.minecraft.core.BlockPos;
import org.spongepowered.asm.mixin.Mixin;
@Mixin(HarvesterMovementBehaviour.class)
public class HarvesterMovementBehaviourMixin {
@SuppressWarnings("ResultOfMethodCallIgnored")
@WrapMethod(method = "visitNewPosition")
public void sable$checkAllPositions(final MovementContext context, final BlockPos pos, final Operation<Void> original) {
if (context instanceof DummyMovementContext) {
original.call(context, pos);
return;
}
final ActiveSableCompanion helper = Sable.HELPER;
helper.runIncludingSubLevels(context.world, pos.getCenter(), true, helper.getContaining(context.contraption.entity), (sublevel, blockPos) -> {
original.call(context, blockPos);
return null;
});
}
}
@@ -0,0 +1,71 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.behaviour_compatibility.harvester_block_entity;
import com.simibubi.create.content.contraptions.actors.harvester.HarvesterBlockEntity;
import com.simibubi.create.foundation.blockEntity.CachedRenderBBBlockEntity;
import dev.ryanhcode.sable.ActiveSableCompanion;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.api.block.BlockEntitySubLevelActor;
import dev.ryanhcode.sable.companion.math.JOMLConversion;
import dev.ryanhcode.sable.neoforge.mixinhelper.compatibility.create.harvester.HarvesterLerpedSpeed;
import dev.ryanhcode.sable.neoforge.mixinhelper.compatibility.create.harvester.HarvesterTicker;
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
import net.createmod.catnip.animation.LerpedFloat;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Position;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
@Mixin(HarvesterBlockEntity.class)
public abstract class HarvesterBlockEntityMixin extends CachedRenderBBBlockEntity implements HarvesterLerpedSpeed, BlockEntitySubLevelActor {
@Unique
private final LerpedFloat sable$lerpedSpeed = LerpedFloat.angular();
@Unique
private BlockPos sable$previousPos = BlockPos.ZERO;
public HarvesterBlockEntityMixin(final BlockEntityType<?> type, final BlockPos pos, final BlockState state) {
super(type, pos, state);
}
@Override
public void sable$clientTick() {
final double velocity = Sable.HELPER.getVelocity(this.getLevel(), JOMLConversion.atCenterOf(this.getBlockPos())).length();
this.sable$lerpedSpeed.chase(this.sable$lerpedSpeed.getValue() + (velocity * 5), 20f, LerpedFloat.Chaser.LINEAR);
this.sable$lerpedSpeed.tickChaser();
}
/**
* Server tick
*/
@Override
public void sable$tick(final ServerSubLevel subLevel) {
final ActiveSableCompanion helper = Sable.HELPER;
final Position center = this.getBlockPos().getCenter();
BlockPos gatheredPos = helper.runIncludingSubLevels(this.level, center, false, helper.getContaining(this), (sublevel, pos) -> {
if (HarvesterTicker.blockEntityBehaviour.isValidCrop(this.level, pos, this.level.getBlockState(pos))) {
return pos;
}
return null;
});
if (gatheredPos == null) {
gatheredPos = BlockPos.containing(helper.projectOutOfSubLevel(this.level, center));
}
if (!this.sable$previousPos.equals(gatheredPos)) {
this.sable$previousPos = gatheredPos;
HarvesterTicker.dummyMovementContext.update(this.level, this.getBlockPos(), this.getBlockState(), null);
HarvesterTicker.blockEntityBehaviour.visitNewPosition(HarvesterTicker.dummyMovementContext, this.sable$previousPos);
}
}
@Override
public LerpedFloat sable$getLerpedFloat() {
return this.sable$lerpedSpeed;
}
}
@@ -0,0 +1,24 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.behaviour_compatibility.harvester_block_entity;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.simibubi.create.content.contraptions.actors.harvester.HarvesterMovementBehaviour;
import com.simibubi.create.content.contraptions.behaviour.MovementContext;
import dev.ryanhcode.sable.neoforge.mixinhelper.compatibility.create.behavior_compatibility.harvester_block_entity.DummyMovementContext;
import dev.ryanhcode.sable.neoforge.mixinhelper.compatibility.create.harvester.HarvesterTicker;
import net.minecraft.world.item.ItemStack;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@Mixin(HarvesterMovementBehaviour.class)
public class HarvesterBlockEntityUsageMixin {
@WrapOperation(method = "lambda$visitNewPosition$0", at = @At(value = "INVOKE", target = "Lcom/simibubi/create/content/contraptions/actors/harvester/HarvesterMovementBehaviour;collectOrDropItem(Lcom/simibubi/create/content/contraptions/behaviour/MovementContext;Lnet/minecraft/world/item/ItemStack;)V"))
public void sable$replaceDropItem(final HarvesterMovementBehaviour instance, final MovementContext movementContext, final ItemStack itemStack, final Operation<Void> original) {
if (movementContext instanceof DummyMovementContext) {
HarvesterTicker.dropItem(movementContext.world, itemStack, movementContext.localPos);
} else {
original.call(instance, movementContext, itemStack);
}
}
}
@@ -0,0 +1,29 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.behaviour_compatibility.harvester_block_entity;
import com.simibubi.create.content.contraptions.actors.AttachedActorBlock;
import com.simibubi.create.content.contraptions.actors.harvester.HarvesterBlock;
import com.simibubi.create.content.contraptions.actors.harvester.HarvesterBlockEntity;
import com.simibubi.create.foundation.block.IBE;
import dev.ryanhcode.sable.neoforge.mixinhelper.compatibility.create.harvester.HarvesterTicker;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityTicker;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import org.spongepowered.asm.mixin.Mixin;
@Mixin(HarvesterBlock.class)
public abstract class HarvesterBlockMixin extends AttachedActorBlock implements IBE<HarvesterBlockEntity> {
protected HarvesterBlockMixin(final Properties properties) {
super(properties);
}
@Override
public <S extends BlockEntity> BlockEntityTicker<S> getTicker(final Level level, final BlockState state, final BlockEntityType<S> blockEntityType) {
if (level.isClientSide) {
return new HarvesterTicker();
} else {
return null;
}
}
}
@@ -0,0 +1,34 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.behaviour_compatibility.harvester_block_entity;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import com.simibubi.create.content.contraptions.actors.harvester.HarvesterBlockEntity;
import com.simibubi.create.content.contraptions.actors.harvester.HarvesterRenderer;
import dev.ryanhcode.sable.neoforge.mixinhelper.compatibility.create.harvester.HarvesterLerpedSpeed;
import net.createmod.catnip.math.AngleHelper;
import net.createmod.catnip.render.SuperByteBuffer;
import net.minecraft.core.Direction;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec3;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@Mixin(HarvesterRenderer.class)
public class HarvesterRendererMixin {
@WrapOperation(method = "renderSafe(Lcom/simibubi/create/content/contraptions/actors/harvester/HarvesterBlockEntity;FLcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;II)V", at = @At(value = "INVOKE", target = "Lcom/simibubi/create/content/contraptions/actors/harvester/HarvesterRenderer;transform(Lnet/minecraft/world/level/Level;Lnet/minecraft/core/Direction;Lnet/createmod/catnip/render/SuperByteBuffer;FLnet/minecraft/world/phys/Vec3;)V"))
public void sable$smoothSpeed(final Level world, final Direction facing, final SuperByteBuffer superBuffer, final float speed, final Vec3 pivot, final Operation<Void> original, @Local final HarvesterBlockEntity be, @Local final float pt) {
if (be.getAnimatedSpeed() != 0) {
original.call(world, facing, superBuffer, speed, pivot);
} else { // use our own transformation
final float originOffset = 1.0f / 16.0f;
final Vec3 rotOffset = new Vec3(0, pivot.y * originOffset, pivot.z * originOffset);
superBuffer.rotateCentered(AngleHelper.rad(AngleHelper.horizontalAngle(facing)), Direction.UP)
.translate(rotOffset.x, rotOffset.y, rotOffset.z)
.rotate(AngleHelper.rad(-((HarvesterLerpedSpeed) be).sable$getLerpedFloat().getValue(pt)), Direction.WEST)
.translate(-rotOffset.x, -rotOffset.y, -rotOffset.z);
}
}
}
@@ -0,0 +1,40 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.belt;
import com.simibubi.create.content.kinetics.base.KineticBlockEntity;
import com.simibubi.create.content.kinetics.belt.BeltBlockEntity;
import dev.ryanhcode.sable.Sable;
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.core.BlockPos;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import org.spongepowered.asm.mixin.Mixin;
@Mixin(BeltBlockEntity.class)
public abstract class BeltBlockEntityMixin extends KineticBlockEntity {
public BeltBlockEntityMixin(final BlockEntityType<?> typeIn, final BlockPos pos, final BlockState state) {
super(typeIn, pos, state);
}
@Override
public void onSpeedChanged(final float previousSpeed) {
super.onSpeedChanged(previousSpeed);
final SubLevelContainer container = SubLevelContainer.getContainer(this.level);
if (container instanceof final ServerSubLevelContainer serverSubLevelContainer) {
final SubLevelPhysicsSystem physicsSystem = serverSubLevelContainer.physicsSystem();
final BlockPos blockPos = this.getBlockPos();
physicsSystem.wakeUpObjectsAt(blockPos.getX(), blockPos.getY(), blockPos.getZ());
final SubLevel subLevel = Sable.HELPER.getContaining(this);
if (subLevel instanceof final ServerSubLevel serverSubLevel) {
physicsSystem.getPipeline().wakeUp(serverSubLevel);
}
}
}
}
@@ -0,0 +1,19 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.belt;
import com.simibubi.create.content.kinetics.belt.BeltBlock;
import dev.ryanhcode.sable.api.block.BlockWithSubLevelCollisionCallback;
import dev.ryanhcode.sable.api.physics.callback.BlockSubLevelCollisionCallback;
import dev.ryanhcode.sable.neoforge.physics.callback.BeltBlockCallback;
import org.spongepowered.asm.mixin.Mixin;
/**
* Allows belts on Sub-Levels to obtain entities as passengers
*/
@Mixin(BeltBlock.class)
public class BeltBlockMixin implements BlockWithSubLevelCollisionCallback {
@Override
public BlockSubLevelCollisionCallback sable$getCallback() {
return BeltBlockCallback.INSTANCE;
}
}
@@ -0,0 +1,66 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.belt;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import com.llamalad7.mixinextras.sugar.ref.LocalRef;
import com.simibubi.create.content.kinetics.belt.BeltBlockEntity;
import com.simibubi.create.content.kinetics.belt.transport.BeltMovementHandler;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.sublevel.SubLevel;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.phys.Vec3;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(BeltMovementHandler.class)
public class BeltMovementHandlerMixin {
@WrapOperation(method = "transportEntity", at = {
@At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;getY()D", ordinal = 0),
@At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;getY()D", ordinal = 1),
@At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;getY()D", ordinal = 2)
})
private static double sable$getLocalEntityY(final Entity instance, final Operation<Double> original, @Local(argsOnly = true) final BeltBlockEntity be) {
final SubLevel subLevel = Sable.HELPER.getContaining(be);
if (subLevel != null) {
return subLevel.logicalPose().transformPositionInverse(instance.position()).y;
}
return original.call(instance);
}
@ModifyVariable(method = "transportEntity", at = @At("STORE"), ordinal = 0)
private static double sable$diffCenter(final double originalValue,
@Local(argsOnly = true) final BeltBlockEntity be,
@Local(argsOnly = true) final Entity entity,
@Local(ordinal = 0) final BlockPos pos,
@Local final Direction.Axis axis) {
final SubLevel subLevel = Sable.HELPER.getContaining(be);
if (subLevel == null) {
return originalValue;
}
final Vec3 entityPos = subLevel.logicalPose().transformPositionInverse(entity.position());
return axis == Direction.Axis.Z ? (pos.getX() + 0.5 - entityPos.x()) : (pos.getZ() + 0.5 - entityPos.z());
}
@Inject(method = "transportEntity", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;maxUpStep()F"))
private static void sable$maxUpStep(final BeltBlockEntity beltBE, final Entity entityIn, final BeltMovementHandler.TransportedEntityInfo info, final CallbackInfo ci, @Local(ordinal = 0) final LocalRef<Vec3> movement) {
final SubLevel subLevel = Sable.HELPER.getContaining(beltBE);
if (subLevel != null) {
movement.set(subLevel.logicalPose().transformNormal(movement.get()));
}
}
}
@@ -0,0 +1,31 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.belt;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import com.llamalad7.mixinextras.sugar.Local;
import com.simibubi.create.content.kinetics.belt.BeltBlockEntity;
import com.simibubi.create.content.kinetics.belt.BeltRenderer;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.sublevel.ClientSubLevel;
import net.minecraft.client.Camera;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.phys.Vec3;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
/**
* Make upright items face the the camera properly on belts
*/
@Mixin(BeltRenderer.class)
public class BeltRendererMixin {
@ModifyExpressionValue(method = "renderItem", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;getPosition()Lnet/minecraft/world/phys/Vec3;"))
private Vec3 sable$renderViewEntityPosition(final Vec3 original, @Local(argsOnly = true) final BeltBlockEntity be) {
final ClientSubLevel subLevel = Sable.HELPER.getContainingClient(be);
if (subLevel != null) {
return subLevel.renderPose().transformPositionInverse(original);
} else {
return original;
}
}
}
@@ -0,0 +1,107 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.big_outlines_interaction;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import com.simibubi.create.foundation.block.BigOutlines;
import com.simibubi.create.foundation.utility.RaycastHelper;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.neoforge.mixinhelper.compatibility.create.raycasts.SableRaycastHelper;
import dev.ryanhcode.sable.sublevel.ClientSubLevel;
import net.createmod.catnip.animation.AnimationTickHolder;
import net.minecraft.client.Minecraft;
import net.minecraft.core.BlockPos;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.Vec3;
import net.minecraft.world.phys.shapes.VoxelShape;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import java.util.function.Predicate;
/**
* Makes it so big outline tracing (which affects mc.hitResult) is able to target sublevels
*/
@Mixin(BigOutlines.class)
public class BigOutlinesMixin {
/**
* Needed to pass the sublevel from the function to its lambda,
* This code is client side only so this should be thread safe
*
*/
@Unique
private static ClientSubLevel sable$predicateSubLevel = null;
@WrapOperation(method = "pick", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/phys/Vec3;distanceToSqr(Lnet/minecraft/world/phys/Vec3;)D"))
private static double sable$modifyMaxRange(Vec3 worldHitPos,
final Vec3 origin,
final Operation<Double> original,
@Local final Minecraft minecraft) {
final ClientSubLevel containing = Sable.HELPER.getContainingClient(worldHitPos);
final float pt = AnimationTickHolder.getPartialTicks(minecraft.level);
if (containing != null) {
worldHitPos = containing.renderPose(pt).transformPosition(worldHitPos);
}
return original.call(worldHitPos, origin);
}
@Redirect(method = "pick", at = @At(value = "INVOKE", target = "Lcom/simibubi/create/foundation/utility/RaycastHelper;rayTraceUntil(Lnet/minecraft/world/phys/Vec3;Lnet/minecraft/world/phys/Vec3;Ljava/util/function/Predicate;)Lcom/simibubi/create/foundation/utility/RaycastHelper$PredicateTraceResult;"))
private static RaycastHelper.PredicateTraceResult sable$useSubLevelInclusiveCast(final Vec3 worldOrigin,
final Vec3 worldTarget,
final Predicate<BlockPos> predicate,
@Local final Minecraft minecraft) {
return SableRaycastHelper.rayCastUntilWithSublevels(minecraft.level, worldOrigin, worldTarget, (subLevel, pos) -> {
sable$predicateSubLevel = (ClientSubLevel) subLevel;
return predicate.test(pos);
});
}
@WrapOperation(
method = "lambda$pick$0",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/phys/shapes/VoxelShape;clip(Lnet/minecraft/world/phys/Vec3;Lnet/minecraft/world/phys/Vec3;Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/phys/BlockHitResult;"
)
)
private static BlockHitResult sable$clipUsingLocalSubLevel(final VoxelShape instance,
final Vec3 origin,
final Vec3 target,
final BlockPos blockPos,
final Operation<BlockHitResult> original) {
final float pt = AnimationTickHolder.getPartialTicks(Minecraft.getInstance().level);
if (sable$predicateSubLevel == null) {
return original.call(instance, origin, target, blockPos);
} else {
final Vec3 localOrigin = sable$predicateSubLevel.renderPose(pt).transformPositionInverse(origin);
final Vec3 localTarget = sable$predicateSubLevel.renderPose(pt).transformPositionInverse(target);
return original.call(instance, localOrigin, localTarget, blockPos);
}
}
@WrapOperation(
method = "lambda$pick$0",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/phys/Vec3;distanceToSqr(Lnet/minecraft/world/phys/Vec3;)D"
)
)
private static double sable$distanceToWithSubLevel(final Vec3 instance,
final Vec3 origin,
final Operation<Double> original) {
final float pt = AnimationTickHolder.getPartialTicks(Minecraft.getInstance().level);
if (sable$predicateSubLevel == null) {
return original.call(instance, origin);
} else {
final Vec3 localOrigin = sable$predicateSubLevel.renderPose(pt).transformPositionInverse(origin);
return original.call(instance, localOrigin);
}
}
}
@@ -0,0 +1,40 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.blaze_burner;
import com.llamalad7.mixinextras.sugar.Local;
import com.llamalad7.mixinextras.sugar.ref.LocalDoubleRef;
import com.simibubi.create.content.processing.burner.BlazeBurnerBlockEntity;
import com.simibubi.create.foundation.blockEntity.SmartBlockEntity;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.sublevel.SubLevel;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import org.joml.Vector3d;
import org.spongepowered.asm.mixin.Mixin;
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.callback.CallbackInfo;
@Mixin(BlazeBurnerBlockEntity.class)
public abstract class BlazeBurnerBlockEntityMixin extends SmartBlockEntity {
@Unique
private static Vector3d sable$playerPos = new Vector3d();
public BlazeBurnerBlockEntityMixin(final BlockEntityType<?> type, final BlockPos pos, final BlockState state) {
super(type, pos, state);
}
@Inject(method = "tickAnimation", at = @At(value = "INVOKE_ASSIGN", target = "Lnet/minecraft/client/player/LocalPlayer;getZ()D"))
private void sable$projectPlayerPosition(final CallbackInfo ci, @Local(name = "x") final LocalDoubleRef x, @Local(name = "z") final LocalDoubleRef z, @Local(name = "player") final LocalPlayer player) {
final SubLevel subLevel = Sable.HELPER.getContaining(this);
if (subLevel != null) {
sable$playerPos.set(x.get(), player.getEyeY(), z.get());
subLevel.logicalPose().transformPositionInverse(sable$playerPos);
x.set(sable$playerPos.x);
z.set(sable$playerPos.z);
}
}
}
@@ -0,0 +1,51 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.block_breakers;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import com.simibubi.create.content.kinetics.drill.DrillBlock;
import com.simibubi.create.content.kinetics.saw.SawBlock;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.api.math.LevelReusedVectors;
import dev.ryanhcode.sable.api.math.OrientedBoundingBox3d;
import dev.ryanhcode.sable.companion.math.JOMLConversion;
import dev.ryanhcode.sable.mixinterface.entity.entity_sublevel_collision.LevelExtension;
import dev.ryanhcode.sable.sublevel.SubLevel;
import dev.ryanhcode.sable.sublevel.entity_collision.SubLevelEntityCollision;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.AABB;
import org.joml.Vector3d;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@Mixin({SawBlock.class, DrillBlock.class})
public class BlockBreakingKineticBlockEntityDamageMixin {
@WrapOperation(method = "entityInside", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/phys/AABB;intersects(Lnet/minecraft/world/phys/AABB;)Z"))
public boolean sable$fixBlockBreakerDamage(final AABB blockAABB, final AABB entityAABB, final Operation<Boolean> original, @Local(argsOnly = true) final Level level) {
final Vector3d jomlCenterPos = JOMLConversion.toJOML(blockAABB.getCenter());
final SubLevel parentSublevel = Sable.HELPER.getContaining(level, jomlCenterPos);
if (parentSublevel != null) {
final LevelReusedVectors jomlSink = ((LevelExtension) level).sable$getJOMLSink();
final Vector3d entityCenter = JOMLConversion.toJOML(entityAABB.getCenter());
final Vector3d sideLengths = new Vector3d(entityAABB.getXsize(), entityAABB.getYsize(), entityAABB.getZsize());
final OrientedBoundingBox3d burnerBounds = new OrientedBoundingBox3d(parentSublevel.logicalPose().transformPosition(jomlCenterPos), new Vector3d(blockAABB.getXsize()), parentSublevel.logicalPose().orientation(), jomlSink);
final OrientedBoundingBox3d entityBounds = new OrientedBoundingBox3d(entityCenter, sideLengths, JOMLConversion.QUAT_IDENTITY, jomlSink);
// use the rotated player hit-box for consistency with sub-level collision
jomlSink.entityBoxOrientation.identity();
final double yaw = SubLevelEntityCollision.getHitBoxYaw(parentSublevel.logicalPose());
jomlSink.entityBoxOrientation.rotateY(yaw);
entityBounds.setOrientation(jomlSink.entityBoxOrientation);
if (OrientedBoundingBox3d.sat(burnerBounds, entityBounds).lengthSquared() > 0.0) {
return true;
}
}
return original.call(blockAABB, entityAABB);
}
}
@@ -0,0 +1,49 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.block_breakers;
import com.simibubi.create.content.kinetics.base.BlockBreakingKineticBlockEntity;
import com.simibubi.create.content.kinetics.saw.SawBlock;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.neoforge.mixinhelper.compatibility.create.block_breakers.SubLevelBlockBreakingUtility;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.phys.Vec3;
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.Redirect;
/**
* Makes blocks such as drills and saws work between sublevels and the main level
*/
@Mixin(BlockBreakingKineticBlockEntity.class)
public abstract class BlockBreakingKineticBlockEntityMixin extends BlockEntity {
public BlockBreakingKineticBlockEntityMixin(final BlockEntityType<?> pType, final BlockPos pPos, final BlockState pBlockState) {
super(pType, pPos, pBlockState);
}
@Shadow
public abstract boolean canBreak(BlockState stateToBreak, float blockHardness);
@Redirect(remap = false, method = "tick", at = @At(value = "INVOKE", target = "Lcom/simibubi/create/content/kinetics/base/BlockBreakingKineticBlockEntity;getBreakingPos()Lnet/minecraft/core/BlockPos;"))
private BlockPos sable$preGetBlockToBreak(final BlockBreakingKineticBlockEntity be) {
assert this.level != null;
final BlockPos breakingPos = this.getBlockPos().relative(this.getBlockState().getValue(BlockStateProperties.FACING));
final BlockState originalStateToBreak = this.level.getBlockState(breakingPos);
if (!this.canBreak(originalStateToBreak, originalStateToBreak.getDestroySpeed(this.level, breakingPos))) {
return SubLevelBlockBreakingUtility.findBreakingPos((pos, state) -> this.canBreak(state, state.getDestroySpeed(this.level, pos)),
Sable.HELPER.getContaining(this),
this.getLevel(),
Vec3.atLowerCornerOf(this.getBlockState().getValue(SawBlock.FACING).getNormal()),
this.getBlockPos().getCenter(),
breakingPos);
} else {
return breakingPos;
}
}
}
@@ -0,0 +1,27 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.blueprint;
import com.simibubi.create.content.equipment.blueprint.BlueprintEntity;
import dev.ryanhcode.sable.annotation.MixinModVersionConstraint;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
@MixinModVersionConstraint("(,6.0.11)")
@Mixin(BlueprintEntity.class)
public abstract class BlueprintEntityMixin extends Entity {
public BlueprintEntityMixin(final EntityType<?> entityType, final Level level) {
super(entityType, level);
}
/**
* @author IThundxr
* @reason Switch to Player#canInteractWithEntity, which is patched by sable.
*/
@Overwrite
public boolean canPlayerUse(final Player player) {
return player.canInteractWithEntity(this, 8);
}
}
@@ -0,0 +1,39 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.chain_conveyor;
import com.llamalad7.mixinextras.sugar.Local;
import com.simibubi.create.content.kinetics.chainConveyor.ChainConveyorBlockEntity;
import com.simibubi.create.content.kinetics.chainConveyor.ChainConveyorPackage;
import net.minecraft.core.BlockPos;
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.CallbackInfo;
import java.util.List;
import java.util.Map;
@Mixin(ChainConveyorBlockEntity.class)
public abstract class ChainConveyorBlockEntityMixin {
@Shadow
public Map<BlockPos, ChainConveyorBlockEntity.ConnectionStats> connectionStats;
@Shadow
Map<BlockPos, List<ChainConveyorPackage>> travellingPackages;
@Shadow
protected abstract void drop(ChainConveyorPackage box);
@Inject(method = "removeInvalidConnections", at = @At(value = "INVOKE", target = "Ljava/util/Iterator;remove()V"))
public void dropInvalidPackages(final CallbackInfo ci, @Local(name = "next") final BlockPos next) {
this.connectionStats.remove(next);
final List<ChainConveyorPackage> packages = this.travellingPackages.remove(next);
if (packages != null && !packages.isEmpty()) {
for (final ChainConveyorPackage box : packages) {
this.drop(box);
}
}
}
}
@@ -0,0 +1,28 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.chain_conveyor;
import com.simibubi.create.content.kinetics.chainConveyor.ChainConveyorBlock;
import com.simibubi.create.content.kinetics.chainConveyor.ChainConveyorBlockEntity;
import dev.ryanhcode.sable.api.block.BlockSubLevelAssemblyListener;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.state.BlockState;
import org.spongepowered.asm.mixin.Mixin;
@Mixin(ChainConveyorBlock.class)
public class ChainConveyorBlockMixin implements BlockSubLevelAssemblyListener {
@Override
public void beforeMove(final ServerLevel originLevel, final ServerLevel resultingLevel, final BlockState newState, final BlockPos oldPos, final BlockPos newPos) {
if (originLevel.getBlockEntity(oldPos) instanceof final ChainConveyorBlockEntity be) {
// This tells all connected conveyors to re-check and detach if necessary next tick
be.notifyConnectedToValidate();
}
}
@Override
public void afterMove(final ServerLevel originLevel, final ServerLevel resultingLevel, final BlockState newState, final BlockPos oldPos, final BlockPos newPos) {
if (resultingLevel.getBlockEntity(newPos) instanceof final ChainConveyorBlockEntity be) {
be.checkInvalid = true;
}
}
}
@@ -0,0 +1,274 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.contraptions;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.simibubi.create.content.contraptions.AbstractContraptionEntity;
import com.simibubi.create.content.contraptions.Contraption;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.api.block.BlockSubLevelLiftProvider;
import dev.ryanhcode.sable.api.physics.mass.MassTracker;
import dev.ryanhcode.sable.api.sublevel.KinematicContraption;
import dev.ryanhcode.sable.companion.math.BoundingBox3i;
import dev.ryanhcode.sable.companion.math.JOMLConversion;
import dev.ryanhcode.sable.physics.config.block_properties.PhysicsBlockPropertyHelper;
import dev.ryanhcode.sable.physics.floating_block.FloatingBlockCluster;
import dev.ryanhcode.sable.physics.floating_block.FloatingClusterContainer;
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
import dev.ryanhcode.sable.sublevel.SubLevel;
import dev.ryanhcode.sable.sublevel.system.SubLevelPhysicsSystem;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.createmod.catnip.math.VecHelper;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.world.phys.Vec3;
import org.joml.Matrix3d;
import org.joml.Quaterniond;
import org.joml.Vector3d;
import org.joml.Vector3dc;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
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.CallbackInfo;
import java.util.List;
import java.util.Map;
@Mixin(AbstractContraptionEntity.class)
public abstract class AbstractContraptionEntityMixin extends Entity implements KinematicContraption {
@Unique
private final Vector3d sable$cachedGlobalPosition = new Vector3d();
@Unique
private final Object2ObjectMap<BlockPos, BlockSubLevelLiftProvider.LiftProviderContext> sable$liftProviderContexts = new Object2ObjectOpenHashMap<>();
@Unique
private final FloatingClusterContainer sable$floatingClusterContainer = new FloatingClusterContainer();
@Shadow
protected Contraption contraption;
@Unique
private BoundingBox3i sable$localBounds;
@Unique
private MassTracker sable$massTracker;
@Unique
private boolean sable$added = false;
public AbstractContraptionEntityMixin(final EntityType<?> arg, final Level arg2) {
super(arg, arg2);
}
@Shadow
public abstract Vec3 applyRotation(Vec3 localPos, float partialTicks);
@Shadow
public abstract Vec3 getPrevAnchorVec();
@Shadow
public abstract Vec3 getAnchorVec();
@Redirect(method = "moveCollidedEntitiesOnDisassembly", at = @At(value = "INVOKE", target = "Lcom/simibubi/create/content/contraptions/AbstractContraptionEntity;toLocalVector(Lnet/minecraft/world/phys/Vec3;F)Lnet/minecraft/world/phys/Vec3;"))
private Vec3 sable$applyTransform(final AbstractContraptionEntity instance, final Vec3 localVec, final float partialTicks) {
final SubLevel subLevel = Sable.HELPER.getContaining(instance);
return instance.toLocalVector(subLevel != null ? subLevel.logicalPose().transformPositionInverse(localVec) : localVec, partialTicks);
}
@WrapOperation(method = "moveCollidedEntitiesOnDisassembly", at = {
@At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;setPos(DDD)V"),
@At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;teleportTo(DDD)V")
})
private void sable$applyTransform(final Entity instance, final double x, final double y, final double z, final Operation<Void> original) {
final Vector3d pos = Sable.HELPER.projectOutOfSubLevel(instance.level(), new Vector3d(x, y, z));
original.call(instance, pos.x, pos.y, pos.z);
}
@Inject(method = "tick", at = @At(value = "INVOKE", target = "Ljava/util/Map;entrySet()Ljava/util/Set;"), remap = false)
private void sable$contraptionInitialize(final CallbackInfo ci) {
if (!this.sable$added && this.level() instanceof final ServerLevel serverLevel) {
this.sable$buildProperties();
this.sable$addToPlot();
this.sable$addToPipeline(serverLevel);
this.sable$added = true;
}
}
@Override
public Map<BlockPos, BlockSubLevelLiftProvider.LiftProviderContext> sable$liftProviders() {
return this.sable$liftProviderContexts;
}
/**
* @author RyanH
* @reason Players shouldn't be saved to the same chunks as contraptions in sub-levels
*/
@Overwrite
@Override
public CompoundTag saveWithoutId(final CompoundTag nbt) {
final Vec3 vec = this.position();
final List<Entity> passengers = this.getPassengers();
for (final Entity entity : passengers) {
// Only part added by sable \/
if (entity instanceof Player) continue;
// setPos has world accessing side-effects when removed == null
entity.removalReason = RemovalReason.UNLOADED_TO_CHUNK;
// Gather passengers into same chunk when saving
final Vec3 prevVec = entity.position();
entity.setPosRaw(vec.x, prevVec.y, vec.z);
// Super requires all passengers to not be removed in order to write them to the
// tag
entity.removalReason = null;
}
final CompoundTag tag = super.saveWithoutId(nbt);
return tag;
}
@Unique
private void sable$buildProperties() {
for (final Map.Entry<BlockPos, StructureTemplate.StructureBlockInfo> entry : this.contraption.getBlocks().entrySet()) {
final BlockPos blockPos = entry.getKey();
final StructureTemplate.StructureBlockInfo info = entry.getValue();
final BlockState state = info.state();
if (state.isAir()) continue;
if (this.sable$localBounds == null) {
this.sable$localBounds = new BoundingBox3i(blockPos.getX(), blockPos.getY(), blockPos.getZ(), blockPos.getX(), blockPos.getY(), blockPos.getZ());
}
this.sable$localBounds.expandTo(blockPos.getX(), blockPos.getY(), blockPos.getZ());
if (state.getBlock() instanceof final BlockSubLevelLiftProvider prov) {
final BlockSubLevelLiftProvider.LiftProviderContext context = new BlockSubLevelLiftProvider.LiftProviderContext(blockPos, state, Vec3.atLowerCornerOf(prov.sable$getNormal(state).getNormal()));
this.sable$liftProviderContexts.put(blockPos, context);
}
if (PhysicsBlockPropertyHelper.getFloatingMaterial(state) != null)
this.sable$floatingClusterContainer.addFloatingBlock(state, new Vector3d(blockPos.getX(), blockPos.getY(), blockPos.getZ()));
}
assert this.sable$localBounds != null;
this.sable$massTracker = MassTracker.build(this.sable$blockGetter(), this.sable$localBounds);
final Vector3d temp = this.sable$massTracker.getCenterOfMass().negate(new Vector3d()).add(0.5, 0.5, 0.5);
for (final FloatingBlockCluster cluster : this.sable$floatingClusterContainer.clusters) {
cluster.getBlockData().translateOrigin(temp);
}
}
@Unique
private void sable$addToPlot() {
final SubLevel subLevel = Sable.HELPER.getContaining(this);
if (subLevel != null) {
final ServerSubLevel serverSubLevel = (ServerSubLevel) subLevel;
serverSubLevel.getPlot().addContraption(this);
}
}
@Unique
private void sable$removeFromPlot() {
final SubLevel subLevel = Sable.HELPER.getContaining(this);
if (subLevel != null) {
final ServerSubLevel serverSubLevel = (ServerSubLevel) subLevel;
serverSubLevel.getPlot().removeContraption(this);
}
}
@Override
public void setRemoved(final RemovalReason removalReason) {
if (this.level() instanceof final ServerLevel serverLevel) {
this.sable$removeFromPlot();
this.sable$removeFromPipeline(serverLevel);
}
super.setRemoved(removalReason);
}
@Unique
private void sable$addToPipeline(final ServerLevel serverLevel) {
final SubLevelPhysicsSystem physics = SubLevelPhysicsSystem.require(serverLevel);
physics.getPipeline().add(this);
}
@Unique
private void sable$removeFromPipeline(final ServerLevel serverLevel) {
final SubLevelPhysicsSystem physics = SubLevelPhysicsSystem.require(serverLevel);
physics.getPipeline().remove(this);
}
@Override
public void sable$getLocalBounds(final BoundingBox3i bounds) {
bounds.set(this.sable$localBounds);
}
@Override
public BlockGetter sable$blockGetter() {
return this.contraption.getContraptionWorld();
}
@Override
public MassTracker sable$getMassTracker() {
return this.sable$massTracker;
}
@Override
public Vector3dc sable$getPosition(final double partialTick) {
Vec3 localVec = JOMLConversion.toMojang(this.sable$massTracker.getCenterOfMass());
final Vec3 anchor = this.getPrevAnchorVec().lerp(this.getAnchorVec(), partialTick);
final Vec3 rotationOffset = VecHelper.getCenterOf(BlockPos.ZERO);
localVec = localVec.subtract(rotationOffset);
localVec = this.applyRotation(localVec, (float) partialTick);
localVec = localVec.add(rotationOffset).add(anchor);
return JOMLConversion.toJOML(localVec, this.sable$cachedGlobalPosition);
}
@Override
public Quaterniond sable$getOrientation(final double partialTick) {
final Matrix3d matrix = new Matrix3d();
final Vector3d tempColumn = new Vector3d();
for (int i = 0; i < 3; i++) {
matrix.getColumn(i, tempColumn);
final Vec3 transformed = this.applyRotation(JOMLConversion.toMojang(tempColumn), (float) partialTick);
matrix.setColumn(i, transformed.x, transformed.y, transformed.z);
}
return matrix.getNormalizedRotation(new Quaterniond());
}
@Override
public boolean sable$isValid() {
return !this.isRemoved();
}
@Override
public boolean sable$shouldCollide() {
return true;
}
@Override
public FloatingClusterContainer sable$getFloatingClusterContainer() {
return this.sable$floatingClusterContainer;
}
}
@@ -0,0 +1,156 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.contraptions;
import com.llamalad7.mixinextras.sugar.Local;
import com.llamalad7.mixinextras.sugar.Share;
import com.llamalad7.mixinextras.sugar.ref.LocalRef;
import com.simibubi.create.content.contraptions.AbstractContraptionEntity;
import com.simibubi.create.content.contraptions.ContraptionCollider;
import com.simibubi.create.foundation.collision.Matrix3d;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.companion.math.BoundingBox3d;
import dev.ryanhcode.sable.companion.math.Pose3d;
import dev.ryanhcode.sable.sublevel.SubLevel;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(value = ContraptionCollider.class, remap = false)
public class ContraptionColliderMixin {
@Unique
private static org.joml.Matrix3d sable$toJOML(final Matrix3d createMatrix) {
final org.joml.Matrix3d jomlMatrix = new org.joml.Matrix3d();
final Matrix3dAccessor accessor = ((Matrix3dAccessor) createMatrix);
jomlMatrix.set(
accessor.getM00(), accessor.getM01(), accessor.getM02(),
accessor.getM10(), accessor.getM11(), accessor.getM12(),
accessor.getM20(), accessor.getM21(), accessor.getM22()
);
return jomlMatrix;
}
@Unique
private static Matrix3d sable$toCreate(final org.joml.Matrix3d jomlMatrix) {
final Matrix3d createMatrix = new Matrix3d();
final Matrix3dAccessor accessor = ((Matrix3dAccessor) createMatrix);
accessor.setM00(jomlMatrix.m00);
accessor.setM01(jomlMatrix.m01);
accessor.setM02(jomlMatrix.m02);
accessor.setM10(jomlMatrix.m10);
accessor.setM11(jomlMatrix.m11);
accessor.setM12(jomlMatrix.m12);
accessor.setM20(jomlMatrix.m20);
accessor.setM21(jomlMatrix.m21);
accessor.setM22(jomlMatrix.m22);
return createMatrix;
}
@Redirect(method = "collideEntities", at = @At(value = "INVOKE", target = "Lcom/simibubi/create/content/contraptions/AbstractContraptionEntity;getBoundingBox()Lnet/minecraft/world/phys/AABB;"))
private static AABB sable$contraptionBounds(final AbstractContraptionEntity instance, @Share("subLevel") final LocalRef<SubLevel> contraptionSubLevel) {
final SubLevel subLevel = Sable.HELPER.getContaining(instance);
contraptionSubLevel.set(subLevel);
if (subLevel != null) {
final BoundingBox3d globalBB = new BoundingBox3d(instance.getBoundingBox());
globalBB.transform(subLevel.logicalPose(), globalBB);
return globalBB.toMojang();
}
return instance.getBoundingBox();
}
@Redirect(method = "collideEntities", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/phys/AABB;expandTowards(DDD)Lnet/minecraft/world/phys/AABB;"))
private static AABB sable$entityQueryBounds(final AABB instance, final double d, final double e, final double f, @Local(argsOnly = true) final AbstractContraptionEntity contraption, @Share("subLevel") final LocalRef<SubLevel> contraptionSubLevel) {
final SubLevel subLevel = contraptionSubLevel.get();
if (subLevel != null) {
final BoundingBox3d globalBB = new BoundingBox3d(contraption.getBoundingBox().inflate(2.0).expandTowards(d, e, f));
globalBB.transform(subLevel.logicalPose(), globalBB);
return globalBB.toMojang();
}
return instance.expandTowards(d, e, f);
}
@Redirect(method = "collideEntities", at = @At(value = "INVOKE", target = "Lcom/simibubi/create/content/contraptions/AbstractContraptionEntity;position()Lnet/minecraft/world/phys/Vec3;"))
private static Vec3 sable$contraptionPosition(final AbstractContraptionEntity instance, @Share("subLevel") final LocalRef<SubLevel> contraptionSubLevel) {
final SubLevel subLevel = contraptionSubLevel.get();
if (subLevel != null) {
return subLevel.logicalPose().transformPosition(instance.position());
}
return instance.position();
}
@Redirect(method = "collideEntities", at = @At(value = "INVOKE", target = "Lcom/simibubi/create/content/contraptions/AbstractContraptionEntity;getPrevPositionVec()Lnet/minecraft/world/phys/Vec3;"))
private static Vec3 sable$getPrevPositionVec(final AbstractContraptionEntity instance, @Share("subLevel") final LocalRef<SubLevel> contraptionSubLevel) {
final SubLevel subLevel = contraptionSubLevel.get();
if (subLevel != null) {
return subLevel.logicalPose().transformPosition(instance.getPrevPositionVec());
}
return instance.getPrevPositionVec();
}
@Redirect(method = "collideEntities", at = @At(value = "INVOKE", target = "Lcom/simibubi/create/content/contraptions/AbstractContraptionEntity;getAnchorVec()Lnet/minecraft/world/phys/Vec3;"))
private static Vec3 sable$getAnchorVec(final AbstractContraptionEntity instance, @Share("subLevel") final LocalRef<SubLevel> contraptionSubLevel) {
final SubLevel subLevel = contraptionSubLevel.get();
if (subLevel != null) {
return subLevel.logicalPose().transformPosition(instance.getAnchorVec().add(0.5, 0.5, 0.5)).subtract(0.5, 0.5, 0.5);
}
return instance.getAnchorVec();
}
@Redirect(method = "collideEntities", at = @At(value = "INVOKE", target = "Lcom/simibubi/create/content/contraptions/AbstractContraptionEntity$ContraptionRotationState;asMatrix()Lcom/simibubi/create/foundation/collision/Matrix3d;"))
private static Matrix3d sable$rotationMatrix(final AbstractContraptionEntity.ContraptionRotationState rotationState, @Local(argsOnly = true) final AbstractContraptionEntity contraption, @Share("subLevel") final LocalRef<SubLevel> contraptionSubLevel) {
final SubLevel subLevel = contraptionSubLevel.get();
if (subLevel != null) {
final Pose3d pose = subLevel.logicalPose();
final org.joml.Matrix3d jomlMatrix = sable$toJOML(rotationState.asMatrix());
jomlMatrix.rotateLocal(pose.orientation());
return sable$toCreate(jomlMatrix);
}
return rotationState.asMatrix();
}
@Redirect(method = "collideEntities", at = @At(value = "INVOKE", target = "Lcom/simibubi/create/content/contraptions/AbstractContraptionEntity;toLocalVector(Lnet/minecraft/world/phys/Vec3;F)Lnet/minecraft/world/phys/Vec3;"))
private static Vec3 sable$toLocalVector(final AbstractContraptionEntity instance, final Vec3 localVec, final float partialTicks, @Share("subLevel") final LocalRef<SubLevel> contraptionSubLevel) {
final SubLevel subLevel = contraptionSubLevel.get();
if (subLevel != null) {
final Pose3d pose = subLevel.logicalPose();
return instance.toLocalVector(pose.transformPositionInverse(localVec), partialTicks);
}
return instance.toLocalVector(localVec, partialTicks);
}
@Redirect(method = "collideEntities", at = @At(value = "INVOKE", target = "Lcom/simibubi/create/content/contraptions/AbstractContraptionEntity;getContactPointMotion(Lnet/minecraft/world/phys/Vec3;)Lnet/minecraft/world/phys/Vec3;"))
private static Vec3 sable$getContactPointMotion(final AbstractContraptionEntity instance, final Vec3 globalContactPoint, @Share("subLevel") final LocalRef<SubLevel> contraptionSubLevel) {
final SubLevel subLevel = contraptionSubLevel.get();
if (subLevel != null) {
final Pose3d pose = subLevel.logicalPose();
final Vec3 localContactPoint = pose.transformPositionInverse(globalContactPoint);
final Vec3 motion = pose.transformNormal(instance.getContactPointMotion(localContactPoint))
.add(globalContactPoint.subtract(subLevel.lastPose().transformPosition(localContactPoint)));
return motion;
}
return instance.getContactPointMotion(globalContactPoint);
}
}
@@ -0,0 +1,28 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.contraptions;
import com.mojang.blaze3d.vertex.PoseStack;
import com.simibubi.create.content.contraptions.actors.contraptionControls.ContraptionControlsRenderer;
import com.simibubi.create.content.contraptions.behaviour.MovementContext;
import com.simibubi.create.content.contraptions.render.ContraptionMatrices;
import dev.ryanhcode.sable.Sable;
import net.minecraft.world.phys.Vec3;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
/**
* Fixes the rendering for the Contraption Controls to take sublevels into account
*/
@Mixin(value = ContraptionControlsRenderer.class, remap = false)
public class ContraptionControlsRendererMixin {
@Redirect(method = "renderInContraption",
at = @At(value = "FIELD", target = "Lcom/simibubi/create/content/contraptions/behaviour/MovementContext;position:Lnet/minecraft/world/phys/Vec3;", ordinal = 1))
private static Vec3 sable$distanceRemix(final MovementContext instance) {
return Sable.HELPER.projectOutOfSubLevel(instance.world, instance.position);
}
@Redirect(method = "renderInContraption", at = @At(value = "INVOKE", target = "Lcom/simibubi/create/content/contraptions/render/ContraptionMatrices;getViewProjection()Lcom/mojang/blaze3d/vertex/PoseStack;"))
private static PoseStack sable$getViewProjection(final ContraptionMatrices instance) {
return instance.getModelViewProjection();
}
}
@@ -0,0 +1,58 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.contraptions;
import com.simibubi.create.content.contraptions.AbstractContraptionEntity;
import com.simibubi.create.content.contraptions.ContraptionHandlerClient;
import dev.ryanhcode.sable.ActiveSableCompanion;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.sublevel.SubLevel;
import net.minecraft.client.Minecraft;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
/**
* Fixes the right click interaction and contraption raytracing in Create to take into account sublevels
*/
@Mixin(ContraptionHandlerClient.class)
public abstract class ContraptionHandlerClientMixin {
@Redirect(method = "getRayInputs", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/phys/Vec3;distanceTo(Lnet/minecraft/world/phys/Vec3;)D"))
private static double sable$projectDistanceTo1(final Vec3 eyePos, final Vec3 itemPos) {
return Math.sqrt(Sable.HELPER.distanceSquaredWithSubLevels(Minecraft.getInstance().level, eyePos, itemPos));
}
@Redirect(method = "rightClickingOnContraptionsGetsHandledLocally", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/phys/Vec3;distanceTo(Lnet/minecraft/world/phys/Vec3;)D"))
private static double sable$projectDistanceTo2(final Vec3 eyePos, final Vec3 itemPos) {
return Math.sqrt(Sable.HELPER.distanceSquaredWithSubLevels(Minecraft.getInstance().level, eyePos, itemPos));
}
@Redirect(method = "rightClickingOnContraptionsGetsHandledLocally",
at = @At(value = "INVOKE", target = "Lcom/simibubi/create/content/contraptions/AbstractContraptionEntity;getBoundingBox()Lnet/minecraft/world/phys/AABB;"))
private static AABB sable$moveBoundingBoxToProjectedPos(final AbstractContraptionEntity instance){
final Vec3 projectedPos = Sable.HELPER.projectOutOfSubLevel(instance.level(), instance.getAnchorVec());
final AABB boundingBox = instance.getBoundingBox();
return boundingBox.move(Vec3.ZERO.subtract(boundingBox.getCenter())).move(projectedPos);
}
@Redirect(method = "rayTraceContraption",
at = @At(value = "INVOKE", target = "Lcom/simibubi/create/content/contraptions/AbstractContraptionEntity;toLocalVector(Lnet/minecraft/world/phys/Vec3;F)Lnet/minecraft/world/phys/Vec3;"),
remap = false)
private static Vec3 sable$projectedContraptionClip(final AbstractContraptionEntity abce, Vec3 localVec, final float partialTicks){
final ActiveSableCompanion helper = Sable.HELPER;
final SubLevel sublevel1 = helper.getContaining(abce.level(), localVec);
final SubLevel contraptionSublevel = helper.getContaining(abce);
if (contraptionSublevel != sublevel1) {
if (sublevel1 != null)
localVec = sublevel1.logicalPose().transformPosition(localVec);
if (contraptionSublevel != null)
localVec = contraptionSublevel.logicalPose().transformPositionInverse(localVec);
}
return abce.toLocalVector(localVec, 1);
}
}
@@ -0,0 +1,98 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.contraptions;
import com.mojang.blaze3d.vertex.PoseStack;
import com.simibubi.create.content.contraptions.AbstractContraptionEntity;
import com.simibubi.create.content.contraptions.render.ContraptionVisual;
import dev.engine_room.flywheel.api.visualization.VisualEmbedding;
import dev.engine_room.flywheel.api.visualization.VisualizationContext;
import dev.engine_room.flywheel.lib.visual.AbstractEntityVisual;
import dev.ryanhcode.sable.api.sublevel.SubLevelContainer;
import dev.ryanhcode.sable.companion.math.Pose3dc;
import dev.ryanhcode.sable.neoforge.compatibility.flywheel.FlywheelCompatNeoForge;
import dev.ryanhcode.sable.neoforge.mixinterface.compatibility.flywheel.EmbeddedEnvironmentExtension;
import net.minecraft.core.Vec3i;
import net.minecraft.util.Mth;
import net.minecraft.world.level.ChunkPos;
import org.joml.Quaternionf;
import org.joml.Vector3d;
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.CallbackInfo;
/**
* Fixes the rendering of contraption visuals on sub-levels
*/
@Mixin(ContraptionVisual.class)
public abstract class ContraptionVisualMixin extends AbstractEntityVisual<AbstractContraptionEntity> {
@Shadow
@Final
protected VisualEmbedding embedding;
@Shadow
@Final
private PoseStack contraptionMatrix;
public ContraptionVisualMixin(final VisualizationContext ctx, final AbstractContraptionEntity entity, final float partialTick) {
super(ctx, entity, partialTick);
}
@Inject(method = "setEmbeddingMatrices", at = @At(value = "HEAD"), cancellable = true)
private void sable$setEmbeddingMatrices(final float partialTick, final CallbackInfo ci) {
final SubLevelContainer container = SubLevelContainer.getContainer(this.entity.level());
if (container == null)
return;
final ChunkPos chunkPos = this.entity.chunkPosition();
final boolean inBounds = container.inBounds(chunkPos);
if (!inBounds)
return;
final int plotX = (chunkPos.x >> container.getLogPlotSize()) - container.getOrigin().x;
final int plotZ = (chunkPos.z >> container.getLogPlotSize()) - container.getOrigin().y;
final FlywheelCompatNeoForge.SubLevelFlwRenderState state = FlywheelCompatNeoForge.getInfo(ChunkPos.asLong(plotX, plotZ));
if (state == null) return;
final Vec3i origin = this.renderOrigin();
final Vector3d pos = new Vector3d();
if (this.entity.isPrevPosInvalid()) {
pos.x = this.entity.getX();
pos.y = this.entity.getY();
pos.z = this.entity.getZ();
} else {
pos.x = Mth.lerp(partialTick, this.entity.xo, this.entity.getX());
pos.y = Mth.lerp(partialTick, this.entity.yo, this.entity.getY());
pos.z = Mth.lerp(partialTick, this.entity.zo, this.entity.getZ());
}
final ChunkPos centerChunk = state.centerChunk;
final PoseStack sceneMatrix = new PoseStack();
sceneMatrix.translate(
(float) (pos.x - centerChunk.getMinBlockX()),
(float) pos.y,
(float) (pos.z - centerChunk.getMinBlockZ())
);
this.entity.applyLocalTransforms(sceneMatrix, partialTick);
final Pose3dc renderPose = state.renderPose;
renderPose.transformPosition(pos).sub(origin.getX(), origin.getY(), origin.getZ());
this.contraptionMatrix.setIdentity();
this.contraptionMatrix.translate(pos.x, pos.y, pos.z);
this.contraptionMatrix.mulPose(new Quaternionf(renderPose.orientation()));
this.entity.applyLocalTransforms(this.contraptionMatrix, partialTick);
this.embedding.transforms(this.contraptionMatrix.last().pose(), this.contraptionMatrix.last().normal());
if (this.embedding instanceof final EmbeddedEnvironmentExtension extension) {
extension.sable$setLightingInfo(sceneMatrix.last().pose(), state.sceneID, state.latestSkyLightScale / 15.0f);
}
ci.cancel();
}
}
@@ -0,0 +1,63 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.contraptions;
import com.simibubi.create.foundation.collision.Matrix3d;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(Matrix3d.class)
public interface Matrix3dAccessor {
@Accessor("m00")
double getM00();
@Accessor("m00")
void setM00(double value);
@Accessor("m01")
double getM01();
@Accessor("m01")
void setM01(double value);
@Accessor("m02")
double getM02();
@Accessor("m02")
void setM02(double value);
@Accessor("m10")
double getM10();
@Accessor("m10")
void setM10(double value);
@Accessor("m11")
double getM11();
@Accessor("m11")
void setM11(double value);
@Accessor("m12")
double getM12();
@Accessor("m12")
void setM12(double value);
@Accessor("m20")
double getM20();
@Accessor("m20")
void setM20(double value);
@Accessor("m21")
double getM21();
@Accessor("m21")
void setM21(double value);
@Accessor("m22")
double getM22();
@Accessor("m22")
void setM22(double value);
}
@@ -0,0 +1,25 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.contraptions;
import dev.engine_room.flywheel.impl.visualization.VisualizationEventHandler;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.neoforge.compatibility.flywheel.FlywheelCompatNeoForge;
import dev.ryanhcode.sable.sublevel.SubLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.Level;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(VisualizationEventHandler.class)
public class VisualizationEventHandlerMixin {
@Inject(method = "onEntityJoinLevel", at = @At("TAIL"))
private static void sable$onEntityJoinLevel(final Level level, final Entity entity, final CallbackInfo ci) {
final SubLevel subLevel = Sable.HELPER.getContaining(entity);
if (subLevel != null) {
FlywheelCompatNeoForge.createRenderInfo(level, subLevel);
}
}
}
@@ -0,0 +1,67 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.crushing_wheel;
import com.simibubi.create.content.kinetics.base.RotatedPillarKineticBlock;
import com.simibubi.create.content.kinetics.crusher.CrushingWheelBlock;
import com.simibubi.create.content.kinetics.crusher.CrushingWheelBlockEntity;
import com.simibubi.create.foundation.block.IBE;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.sublevel.SubLevel;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.Vec3;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
@Mixin(CrushingWheelBlock.class)
public abstract class CrushingWheelBlockMixin extends RotatedPillarKineticBlock implements IBE<CrushingWheelBlockEntity> {
public CrushingWheelBlockMixin(final Properties arg) {
super(arg);
}
/**
* @author RyanH
* @reason Take into account sub-levels existing
*/
@Overwrite
public void entityInside(final BlockState state, final Level level, final BlockPos pos, final Entity entityIn) {
final SubLevel subLevel = Sable.HELPER.getContaining(level, pos);
Vec3 entityPos = entityIn.position();
if (subLevel != null) {
entityPos = subLevel.logicalPose().transformPositionInverse(entityPos);
}
if (entityPos.y() < pos.getY() + 1.25f || !entityIn.onGround())
return;
final float speed = this.getBlockEntityOptional(level, pos).map(CrushingWheelBlockEntity::getSpeed)
.orElse(0f);
double x = 0;
double z = 0;
final double entityX = entityPos.x();
final double entityZ = entityPos.z();
if (state.getValue(AXIS) == Direction.Axis.X) {
z = speed / 20f;
x += (pos.getX() + .5f - entityX) * .1f;
}
if (state.getValue(AXIS) == Direction.Axis.Z) {
x = speed / -20f;
z += (pos.getZ() + .5f - entityZ) * .1f;
}
Vec3 impulse = new Vec3(x, 0, z);
if (subLevel != null) {
impulse = subLevel.logicalPose().transformNormal(impulse);
}
entityIn.setDeltaMovement(entityIn.getDeltaMovement().add(impulse));
}
}
@@ -0,0 +1,80 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.crushing_wheel_entity_processing;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.simibubi.create.content.kinetics.crusher.CrushingWheelControllerBlockEntity;
import com.simibubi.create.foundation.blockEntity.SmartBlockEntity;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.companion.math.BoundingBox3d;
import dev.ryanhcode.sable.sublevel.SubLevel;
import net.minecraft.core.BlockPos;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.AABB;
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.callback.CallbackInfo;
@Mixin(CrushingWheelControllerBlockEntity.class)
public abstract class CrushingWheelControllerBlockEntityMixin extends SmartBlockEntity {
@Shadow
public Entity processingEntity;
@Unique
private SubLevel sable$parentSublevel = null;
public CrushingWheelControllerBlockEntityMixin(final BlockEntityType<?> typeIn, final BlockPos pos, final BlockState state) {
super(typeIn, pos, state);
}
@Inject(method = "tick", at = @At("HEAD"))
public void sable$initSublevel(final CallbackInfo ci) {
this.sable$parentSublevel = Sable.HELPER.getContaining(this);
}
@WrapOperation(method = "tick", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;getBoundingBox()Lnet/minecraft/world/phys/AABB;"))
public AABB sable$pushEntityLocalAABB(final Entity instance, final Operation<AABB> original) {
final AABB boundingBox = original.call(instance);
if (this.sable$parentSublevel != null) {
final BoundingBox3d bb3d = new BoundingBox3d(boundingBox);
bb3d.transformInverse(this.sable$parentSublevel.logicalPose());
return bb3d.toMojang();
}
return boundingBox;
}
@WrapOperation(method = "tick", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;getX()D"))
public double sable$pushEntityLocalX(final Entity instance, final Operation<Double> original) {
Double x = original.call(instance);
if (this.sable$parentSublevel != null) {
x = this.sable$parentSublevel.logicalPose().transformPositionInverse(instance.position()).x;
}
return x;
}
@WrapOperation(method = "tick", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;getY()D"))
public double sable$pushEntityLocalY(final Entity instance, final Operation<Double> original) {
Double y = original.call(instance);
if (this.sable$parentSublevel != null) {
y = this.sable$parentSublevel.logicalPose().transformPositionInverse(instance.position()).y;
}
return y;
}
@WrapOperation(method = "tick", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;getZ()D"))
public double sable$pushEntityLocalZ(final Entity instance, final Operation<Double> original) {
Double z = original.call(instance);
if (this.sable$parentSublevel != null) {
z = this.sable$parentSublevel.logicalPose().transformPositionInverse(instance.position()).z;
}
return z;
}
}
@@ -0,0 +1,70 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.deployer;
import com.llamalad7.mixinextras.sugar.Local;
import com.simibubi.create.content.kinetics.deployer.DeployerBlockEntity;
import com.simibubi.create.foundation.blockEntity.SmartBlockEntity;
import dev.ryanhcode.sable.ActiveSableCompanion;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.sublevel.SubLevel;
import net.createmod.catnip.data.Iterate;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.Vec3;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import org.spongepowered.asm.mixin.injection.Redirect;
import javax.annotation.Nullable;
@Mixin(DeployerBlockEntity.class)
public abstract class DeployerBlockEntityMixin extends SmartBlockEntity {
public DeployerBlockEntityMixin(final BlockEntityType<?> type, final BlockPos pos, final BlockState state) {
super(type, pos, state);
}
@Redirect(method = "start", at = @At(value = "INVOKE", target = "Ljava/lang/Math;min(DD)D"))
private double sable$deployerMin(final double a, final double b, @Local(ordinal = 1) final Vec3 rayOrigin, @Local(ordinal = 0) final BlockHitResult result) {
return Math.min(Math.sqrt(Sable.HELPER.distanceSquaredWithSubLevels(this.level, result.getLocation(), rayOrigin)), b);
}
@ModifyArg(method = "activate", at = @At(value = "INVOKE", target = "Lcom/simibubi/create/content/kinetics/deployer/DeployerHandler;activate(Lcom/simibubi/create/content/kinetics/deployer/DeployerFakePlayer;Lnet/minecraft/world/phys/Vec3;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/phys/Vec3;Lcom/simibubi/create/content/kinetics/deployer/DeployerBlockEntity$Mode;)V"), index = 2, remap = false)
private BlockPos sable$checkPositions(final BlockPos pos) {
final Vec3 centerPos = Vec3.atCenterOf(pos);
ActiveSableCompanion helper = Sable.HELPER;
final BlockPos gatheredPos = helper.runIncludingSubLevels(this.getLevel(), centerPos, true, helper.getContaining(this), this::sable$getState);
if (gatheredPos != null)
return gatheredPos;
return pos;
}
@Unique
@Nullable
private BlockPos sable$getState(final SubLevel subLevel, final BlockPos pos) {
final Level level = this.getLevel();
assert level != null;
final BlockState state = level.getBlockState(pos);
if (!state.isAir()) {
return pos;
} else {
for (final Direction direction : Iterate.directions) {
if (!level.getBlockState(pos.relative(direction)).isAir())
return pos;
}
}
return null;
}
}
@@ -0,0 +1,27 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.depot;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import com.llamalad7.mixinextras.sugar.Local;
import com.simibubi.create.content.logistics.depot.DepotRenderer;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.sublevel.ClientSubLevel;
import dev.ryanhcode.sable.sublevel.SubLevel;
import net.minecraft.client.Minecraft;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.phys.Vec3;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(DepotRenderer.class)
public class DepotRendererMixin {
@ModifyExpressionValue(method = "renderItem", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;getPosition()Lnet/minecraft/world/phys/Vec3;"))
private static Vec3 sable$renderViewEntityPosition(final Vec3 original, @Local(argsOnly = true) final Vec3 position) {
final ClientSubLevel subLevel = Sable.HELPER.getContainingClient(position);
if (subLevel != null) {
return subLevel.renderPose().transformPositionInverse(original);
} else {
return original;
}
}
}
@@ -0,0 +1,21 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.display_link;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import com.simibubi.create.content.redstone.displayLink.ClickToLinkBlockItem;
import dev.ryanhcode.sable.Sable;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Vec3i;
import net.minecraft.world.level.Level;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@Mixin(ClickToLinkBlockItem.class)
public class ClickToLinkBlockItemMixin {
@WrapOperation(method = "useOn", at = @At(value = "INVOKE", target = "Lnet/minecraft/core/BlockPos;closerThan(Lnet/minecraft/core/Vec3i;D)Z"))
public boolean sable$accountForSubLevels(final BlockPos instance, final Vec3i pos, final double v, final Operation<Boolean> original, @Local final Level level) {
return Sable.HELPER.distanceSquaredWithSubLevels(level, instance.getX() + 0.5, instance.getY() + 0.5, instance.getZ() + 0.5, pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5) < v * v;
}
}
@@ -0,0 +1,31 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.display_link;
import com.simibubi.create.content.redstone.displayLink.DisplayLinkBlockEntity;
import com.simibubi.create.content.redstone.displayLink.LinkWithBulbBlockEntity;
import com.simibubi.create.infrastructure.config.AllConfigs;
import dev.ryanhcode.sable.Sable;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(DisplayLinkBlockEntity.class)
public abstract class DisplayLinkBlockEntityMixin extends LinkWithBulbBlockEntity {
private DisplayLinkBlockEntityMixin(final BlockEntityType<?> type, final BlockPos pos, final BlockState state) {
super(type, pos, state);
}
@Inject(method = "getTargetPosition", at = @At("TAIL"), cancellable = true)
public void sable$accountForSubLevels(final CallbackInfoReturnable<BlockPos> cir) {
final BlockPos target = cir.getReturnValue();
final int range = AllConfigs.server().logistics.displayLinkRange.get();
final BlockPos pos = this.getBlockPos();
if (Sable.HELPER.distanceSquaredWithSubLevels(this.level, pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5, target.getX() + 0.5, target.getY() + 0.5, target.getZ() + 0.5) >= range * range) {
cir.setReturnValue(BlockPos.ZERO);
}
}
}
@@ -0,0 +1,21 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.display_link;
import com.simibubi.create.content.redstone.displayLink.DisplayLinkBlock;
import com.simibubi.create.content.redstone.displayLink.DisplayLinkBlockEntity;
import dev.ryanhcode.sable.api.block.BlockSubLevelAssemblyListener;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.state.BlockState;
import org.spongepowered.asm.mixin.Mixin;
@Mixin(DisplayLinkBlock.class)
public class DisplayLinkBlockMixin implements BlockSubLevelAssemblyListener {
@SuppressWarnings("AddedMixinMembersNamePattern")
@Override
public void afterMove(final ServerLevel originLevel, final ServerLevel resultingLevel, final BlockState newState, final BlockPos oldPos, final BlockPos newPos) {
if (originLevel.getBlockEntity(oldPos) instanceof final DisplayLinkBlockEntity be && resultingLevel.getBlockEntity(newPos) instanceof final DisplayLinkBlockEntity newBe) {
newBe.target(be.getTargetPosition());
}
}
}
@@ -0,0 +1,164 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.ejector;
import com.simibubi.create.content.logistics.depot.EjectorBlockEntity;
import com.simibubi.create.content.logistics.depot.EntityLauncher;
import com.simibubi.create.foundation.blockEntity.SmartBlockEntity;
import dev.ryanhcode.sable.ActiveSableCompanion;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.api.physics.handle.RigidBodyHandle;
import dev.ryanhcode.sable.api.sublevel.SubLevelContainer;
import dev.ryanhcode.sable.mixinterface.clip_overwrite.ClipContextExtension;
import dev.ryanhcode.sable.neoforge.mixinhelper.compatibility.create.ejector.SubLevelScanResult;
import dev.ryanhcode.sable.physics.config.dimension_physics.DimensionPhysicsData;
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
import dev.ryanhcode.sable.sublevel.SubLevel;
import dev.ryanhcode.sable.sublevel.system.SubLevelPhysicsSystem;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.ClipContext;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
import net.minecraft.world.phys.Vec3;
import net.minecraft.world.phys.shapes.CollisionContext;
import org.jetbrains.annotations.Nullable;
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.callback.CallbackInfo;
/**
* Makes weighted ejectors apply an impulse to sub-levels above them
*/
@Mixin(EjectorBlockEntity.class)
public abstract class EjectorBlockEntityMixin extends SmartBlockEntity {
@Unique
private static final int SUB_LEVEL_SCAN_TIME = 2;
@Shadow
private boolean launch;
@Shadow
private EjectorBlockEntity.State state;
@Shadow
private boolean powered;
@Shadow
private EntityLauncher launcher;
@Unique
private int sable$scanTimer = SUB_LEVEL_SCAN_TIME;
@Unique
private int sable$readyTimer = 0;
public EjectorBlockEntityMixin(final BlockEntityType<?> type, final BlockPos pos, final BlockState state) {
super(type, pos, state);
}
@Shadow
public abstract void activate();
@Shadow
protected abstract Direction getFacing();
@Inject(method = "activateDeferred", at = @At(value = "INVOKE", target = "Lcom/simibubi/create/content/logistics/depot/EjectorBlockEntity;launchItems()V"))
public void sable$launchSubLevels(final CallbackInfo ci) {
final SubLevelScanResult scanResult = this.sable$lookForLaunchableSubLevels();
if (scanResult == null) return;
final ServerSubLevel otherSubLevel = scanResult.serverSubLevel();
final SubLevelPhysicsSystem physicsSystem = SubLevelContainer.getContainer((ServerLevel) this.level).physicsSystem();
final BlockPos blockPos = this.getBlockPos();
final SubLevel containingSubLevel = Sable.HELPER.getContaining(this.level, blockPos);
// math to compute the impulse to launch a 1 kpg sub-level to the target
// authored by Eriksonn
final double c = 3.0 * Math.max(1.0, this.launcher.getHorizontalDistance() / 10.0); // velocity constraint [m/s]
final double px = this.launcher.getHorizontalDistance();
final double py = this.launcher.getVerticalDistance();
// TODO: Make this use gravity at the ejector position
final double g = -DimensionPhysicsData.getGravity(this.level).y;
double vx = c;
if (py > 0) {
vx = Math.min(c, px * Math.sqrt(0.5 * g / py));
}
final double vy = vx * py / px + 0.5 * g * px / vx;
final Vec3 verticalImpulse = new Vec3(0.0, vy, 0.0);
final Vec3 localHit = Vec3.atLowerCornerOf(this.getFacing().getNormal()).scale(vx)
.add(verticalImpulse);
final Vec3 globalHitDirection = containingSubLevel != null ?
containingSubLevel.logicalPose().transformNormal(localHit) :
localHit;
final RigidBodyHandle otherHandle = physicsSystem.getPhysicsHandle(otherSubLevel);
otherHandle.applyImpulseAtPoint(scanResult.result().getBlockPos().getCenter(), otherSubLevel.logicalPose().transformNormalInverse(globalHitDirection));
if (containingSubLevel != null) {
final RigidBodyHandle handle = physicsSystem.getPhysicsHandle((ServerSubLevel) containingSubLevel);
handle.applyImpulseAtPoint(blockPos.getCenter(), containingSubLevel.logicalPose().transformNormalInverse(globalHitDirection).scale(-1.0));
}
}
private @Nullable SubLevelScanResult sable$lookForLaunchableSubLevels() {
final ActiveSableCompanion helper = Sable.HELPER;
final SubLevel containingSubLevel = helper.getContaining(this);
final BlockPos blockPos = this.getBlockPos();
final ClipContext clipContext = new ClipContext(blockPos.getCenter(), Vec3.upFromBottomCenterOf(blockPos, 1.0), ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, CollisionContext.empty());
final ClipContextExtension extension = (ClipContextExtension) clipContext;
// Ignore the main world. We only care about sub-levels.
extension.sable$setIgnoreMainLevel(true);
extension.sable$setIgnoredSubLevel(containingSubLevel);
final BlockHitResult result = this.level.clip(clipContext);
if (result.getType() == HitResult.Type.MISS) return null;
final SubLevel subLevel = helper.getContaining(this.level, result.getLocation());
if (!(subLevel instanceof final ServerSubLevel serverSubLevel)) {
return null;
}
return new SubLevelScanResult(result, serverSubLevel);
}
@Inject(method = "tick", at = @At("HEAD"))
public void sable$tick(final CallbackInfo ci) {
if (this.level.isClientSide && !this.isVirtual()) return;
this.sable$scanTimer--;
if (this.sable$scanTimer <= 0) {
this.sable$scanTimer = SUB_LEVEL_SCAN_TIME;
if (this.state == EjectorBlockEntity.State.RETRACTING ||
this.powered ||
this.launcher.getHorizontalDistance() == 0) {
this.sable$readyTimer = 0;
return;
}
final SubLevelScanResult result = this.sable$lookForLaunchableSubLevels();
if (result != null) {
this.sable$readyTimer++;
} else {
this.sable$readyTimer = 0;
}
if (this.sable$readyTimer > 3) {
this.activate();
this.notifyUpdate();
this.sable$readyTimer = 0;
}
}
}
}
@@ -0,0 +1,32 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.elevator_controls;
import com.simibubi.create.content.contraptions.AbstractContraptionEntity;
import com.simibubi.create.content.contraptions.elevator.ElevatorControlsHandler;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.companion.math.BoundingBox3d;
import dev.ryanhcode.sable.sublevel.SubLevel;
import net.minecraft.world.phys.AABB;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
/**
* Fixes the range check on Elevator Controls in Create to take into account sub-levels
*/
@Mixin(ElevatorControlsHandler.class)
public class ElevatorControlsHandlerMixin {
@Redirect(method = "onScroll",
at = @At(value = "INVOKE", target = "Lcom/simibubi/create/content/contraptions/AbstractContraptionEntity;getBoundingBox()Lnet/minecraft/world/phys/AABB;"))
private static AABB sable$projectAABB(final AbstractContraptionEntity instance) {
final SubLevel subLevel = Sable.HELPER.getContaining(instance.level(), instance.getBoundingBox().getCenter());
final AABB projectedBB = instance.getBoundingBox();
if (subLevel != null) {
final BoundingBox3d bb = new BoundingBox3d(projectedBB);
return bb.transform(subLevel.logicalPose(), bb).toMojang();
}
return projectedBB;
}
}
@@ -0,0 +1,22 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.entity_falls_on_block;
import com.simibubi.create.content.processing.basin.BasinBlock;
import net.minecraft.core.BlockPos;
import net.minecraft.world.entity.Entity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
/**
* Makes basins use the standing on position of items instead of their block position for picking them up, as the
* on position of entities will be overwritten by Sable to be inside of the plot of a sub-level an item is resting on
*/
@Mixin(BasinBlock.class)
public class BasinBlockMixin {
@Redirect(method = "updateEntityAfterFallOn", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;blockPosition()Lnet/minecraft/core/BlockPos;"))
private BlockPos sable$updateEntityAfterFallOn(final Entity instance) {
return instance.getOnPos();
}
}
@@ -0,0 +1,53 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.entity_falls_on_block;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.simibubi.create.AllBlocks;
import com.simibubi.create.content.kinetics.belt.BeltBlock;
import com.simibubi.create.content.kinetics.millstone.MillstoneBlock;
import com.tterrag.registrate.util.entry.BlockEntry;
import dev.ryanhcode.sable.ActiveSableCompanion;
import dev.ryanhcode.sable.Sable;
import net.minecraft.core.BlockPos;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@Mixin({ BeltBlock.class, MillstoneBlock.class })
public class BeltMillstoneBlocksMixin extends Block {
public BeltMillstoneBlocksMixin(final Properties pProperties) {
super(pProperties);
}
@WrapOperation(method = "updateEntityAfterFallOn", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;blockPosition()Lnet/minecraft/core/BlockPos;"))
public BlockPos sable$checkForSubLevels(final Entity instance, final Operation<BlockPos> original) {
final Level level = instance.level();
BlockEntry<?> entry;
if ((Object) this instanceof BeltBlock) {
entry = AllBlocks.BELT;
} else {
entry = AllBlocks.MILLSTONE;
}
final ActiveSableCompanion helper = Sable.HELPER;
final BlockPos gatheredBeltPos = helper.runIncludingSubLevels(level, instance.position(), true, null, (subLevel, internalPos) -> {
if (entry.has(level.getBlockState(internalPos))) {
return internalPos;
} else if (entry.has(level.getBlockState(internalPos.below()))) {
return internalPos.below();
}
return null;
});
if (gatheredBeltPos != null) {
return gatheredBeltPos;
}
return original.call(instance);
}
}
@@ -0,0 +1,18 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.entity_falls_on_block;
import com.simibubi.create.content.kinetics.saw.SawBlock;
import net.minecraft.core.BlockPos;
import net.minecraft.world.entity.Entity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(SawBlock.class)
public class SawBlockMixin {
@Redirect(method = "updateEntityAfterFallOn", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;blockPosition()Lnet/minecraft/core/BlockPos;"))
private BlockPos sable$updateEntityAfterFallOn(final Entity instance) {
return instance.getOnPos();
}
}
@@ -0,0 +1,23 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.entity_falls_on_block;
import com.simibubi.create.content.contraptions.actors.seat.SeatBlock;
import com.simibubi.create.content.processing.basin.BasinBlock;
import net.minecraft.core.BlockPos;
import net.minecraft.world.entity.Entity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
/**
* Makes seats use the standing on position of entities instead of their block position for sitting them down, as the
* on position of entities will be overwritten by Sable to be inside of the plot of a sub-level an entity is resting on
*/
@Mixin(SeatBlock.class)
public class SeatBlockMixin {
@Redirect(method = "updateEntityAfterFallOn", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;blockPosition()Lnet/minecraft/core/BlockPos;"))
private BlockPos sable$updateEntityAfterFallOn(final Entity instance) {
return instance.getOnPos();
}
}
@@ -0,0 +1,20 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.factory_panel;
import com.llamalad7.mixinextras.sugar.Local;
import com.simibubi.create.content.logistics.factoryBoard.FactoryPanelConnectionHandler;
import dev.ryanhcode.sable.Sable;
import net.minecraft.client.Minecraft;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Vec3i;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(FactoryPanelConnectionHandler.class)
public class FactoryPanelConnectionHandlerMixin {
@Redirect(method = "clientTick", at = @At(value = "INVOKE", target = "Lnet/minecraft/core/BlockPos;closerThan(Lnet/minecraft/core/Vec3i;D)Z"))
private static boolean closerThan(final BlockPos instance, final Vec3i pos, final double maxDistance, @Local final Minecraft mc) {
return Sable.HELPER.distanceSquaredWithSubLevels(mc.level, instance.getX(), instance.getY(), instance.getZ(), pos.getX(), pos.getY(), pos.getZ()) < maxDistance * maxDistance;
}
}
@@ -0,0 +1,61 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.fans_provide_force;
import com.simibubi.create.content.kinetics.base.KineticBlockEntity;
import com.simibubi.create.content.kinetics.fan.EncasedFanBlock;
import com.simibubi.create.content.kinetics.fan.EncasedFanBlockEntity;
import dev.ryanhcode.sable.api.block.propeller.BlockEntityPropeller;
import dev.ryanhcode.sable.api.block.propeller.BlockEntitySubLevelPropellerActor;
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
@Mixin(EncasedFanBlockEntity.class)
public class EncasedFanBlockEntityMixin extends KineticBlockEntity implements BlockEntitySubLevelPropellerActor, BlockEntityPropeller {
@Unique
private boolean sable$blocked;
public EncasedFanBlockEntityMixin(final BlockEntityType<?> type, final BlockPos pos, final BlockState state) {
super(type, pos, state);
}
@Override
public void sable$tick(final ServerSubLevel subLevel) {
final BlockPos frontPos = this.getBlockPos().relative(this.getBlockState().getValue(EncasedFanBlock.FACING));
this.sable$blocked = !this.level.getBlockState(frontPos).isAir();
}
@Override
public BlockEntityPropeller getPropeller() {
return this;
}
@Override
public Direction getBlockDirection() {
return this.getBlockState().getValue(EncasedFanBlock.FACING);
}
protected float sable$getPropSpeed() {
final float rotationSpeed = convertToAngular(this.getSpeed());
return this.getBlockDirection().getAxisDirection().getStep() * rotationSpeed * (10 / 3);
}
@Override
public double getAirflow() {
return 0.1f * this.sable$getPropSpeed();
}
@Override
public double getThrust() {
return 0.3f * this.sable$getPropSpeed();
}
@Override
public boolean isActive() {
return !this.sable$blocked && Math.abs(this.sable$getPropSpeed()) > 0.01f;
}
}
@@ -0,0 +1,71 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.fluid_handling;
import com.simibubi.create.content.fluids.OpenEndedPipe;
import dev.ryanhcode.sable.ActiveSableCompanion;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.sublevel.SubLevel;
import net.createmod.catnip.math.BlockFace;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.Vec3;
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.CallbackInfo;
@Mixin(OpenEndedPipe.class)
public abstract class OpenEndedPipeMixin {
@Shadow
private BlockPos outputPos;
@Shadow
private Level world;
@Shadow
public abstract BlockPos getPos();
@Unique
private BlockPos sable$plotOutputPos;
@Inject(method = "<init>", at = @At("TAIL"), remap = false)
private void sable$saveCurrentPos(final BlockFace face, final CallbackInfo ci) {
this.sable$plotOutputPos = this.outputPos;
}
@Redirect(method = "*", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/Level;getBlockState(Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/level/block/state/BlockState;"))
private BlockState sable$getBlockstateInclSublevels(final Level level, final BlockPos pos) {
this.outputPos = this.sable$plotOutputPos;
final ActiveSableCompanion helper = Sable.HELPER;
final Vec3 checkPos = Vec3.atCenterOf(this.sable$plotOutputPos);
BlockState gatheredState = helper.runIncludingSubLevels(level, checkPos, true, helper.getContaining(level, checkPos), this::sable$gatherState);
if (gatheredState == null) {
this.outputPos = this.sable$plotOutputPos;
gatheredState = level.getBlockState(this.sable$plotOutputPos);
}
return gatheredState;
}
@Unique
private BlockState sable$gatherState(final SubLevel level, final BlockPos b) {
final BlockState checkedState = this.world.getBlockState(b);
if (!checkedState.isAir()) {
this.outputPos = b;
return checkedState;
}
return null;
}
@Redirect(method = "provideFluidToSpace",
at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/Level;setBlock(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;I)Z", ordinal = 1))
private boolean sable$preventInWorldPlace(final Level instance, final BlockPos pPos, final BlockState pNesubleveltate, final int pFlags) {
return instance.setBlock(this.sable$plotOutputPos, pNesubleveltate, 3);
}
}
@@ -0,0 +1,17 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.fluid_handling;
import com.simibubi.create.content.fluids.PipeConnection;
import dev.ryanhcode.sable.Sable;
import net.minecraft.client.Minecraft;
import net.minecraft.world.phys.Vec3;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(PipeConnection.class)
public class PipeConnectionMixin {
@Redirect(method = "isRenderEntityWithinDistance", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/phys/Vec3;distanceTo(Lnet/minecraft/world/phys/Vec3;)D"))
private static double sable$distanceIncludingSubLevels(final Vec3 instance, final Vec3 vec3) {
return Math.sqrt(Sable.HELPER.distanceSquaredWithSubLevels(Minecraft.getInstance().level, instance, vec3));
}
}
@@ -0,0 +1,62 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.fluid_tank_heating;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.simibubi.create.content.fluids.tank.BoilerData;
import com.simibubi.create.content.fluids.tank.FluidTankBlockEntity;
import dev.ryanhcode.sable.ActiveSableCompanion;
import dev.ryanhcode.sable.Sable;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.state.BlockState;
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.callback.CallbackInfo;
@Mixin(BoilerData.class)
public class BoilerDataMixin {
@Shadow
public boolean needsHeatLevelUpdate;
@Unique
private int sable$ticksUntilUpdate = 20;
@Inject(method = "tick", at = @At(value = "FIELD", target = "Lcom/simibubi/create/content/fluids/tank/BoilerData;ticksUntilNextSample:I", ordinal = 0))
public void sable$forceUpdateHeatIfDisconnected(final FluidTankBlockEntity controller, final CallbackInfo ci) {
if (this.sable$ticksUntilUpdate-- <= 0) {
this.sable$ticksUntilUpdate = 20;
this.needsHeatLevelUpdate = true;
}
}
@WrapOperation(method = "updateTemperature", at = @At(value = "INVOKE", target = "Lcom/simibubi/create/api/boiler/BoilerHeater;findHeat(Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;)F"))
public float sable$subLevelHeating(final Level level, final BlockPos pos, final BlockState state, final Operation<Float> original) {
final Float originalHeat = original.call(level, pos, state);
if (originalHeat != -1) {
return originalHeat;
}
final ActiveSableCompanion helper = Sable.HELPER;
final Float gatheredHeat = helper.runIncludingSubLevels(level, pos.getCenter(), false, helper.getContaining(level, pos), (subLevel, internalPos) -> {
final Float internalHeat = original.call(level, internalPos, level.getBlockState(internalPos));
if (internalHeat != -1) {
return internalHeat;
}
return null;
});
if (gatheredHeat != null) {
return gatheredHeat;
}
return -1;
}
}
@@ -0,0 +1,53 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.flywheel;
import com.simibubi.create.content.kinetics.base.IRotate;
import com.simibubi.create.content.kinetics.base.KineticBlockEntity;
import com.simibubi.create.content.kinetics.flywheel.FlywheelBlockEntity;
import dev.ryanhcode.sable.api.block.BlockEntitySubLevelReactionWheel;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.HolderLookup;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import org.joml.Vector3d;
import org.spongepowered.asm.mixin.Mixin;
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.callback.CallbackInfo;
@Mixin(FlywheelBlockEntity.class)
public abstract class FlywheelBlockEntityMixin extends KineticBlockEntity implements BlockEntitySubLevelReactionWheel {
@Unique float sable$smoothedSpeed = 0;
public FlywheelBlockEntityMixin(BlockEntityType<?> arg, BlockPos arg2, BlockState arg3) {
super(arg, arg2, arg3);
}
@Inject(method = "tick",at = @At("HEAD"))
public void sable$tick(CallbackInfo ci)
{
sable$smoothedSpeed += (speed - sable$smoothedSpeed) / 32f;
}
@Inject(method = "write",at = @At("TAIL"))
public void sable$write(CompoundTag compound, HolderLookup.Provider registries, boolean clientPacket, CallbackInfo ci)
{
compound.putFloat("SmoothedSpeed",sable$smoothedSpeed);
}
@Inject(method = "read",at = @At("TAIL"))
public void sable$read(CompoundTag compound, HolderLookup.Provider registries, boolean clientPacket, CallbackInfo ci)
{
sable$smoothedSpeed = compound.getFloat("SmoothedSpeed");
}
@Override
public void sable$getAngularVelocity(Vector3d v) {
Direction.Axis axis = ((IRotate) getBlockState()
.getBlock()).getRotationAxis(getBlockState());
Direction dir = Direction.get(Direction.AxisDirection.NEGATIVE,axis);
float angularSpeed = sable$smoothedSpeed * (float)Math.TAU / 60f;
v.set(dir.getStepX(),dir.getStepY(),dir.getStepZ()).mul(angularSpeed);
}
}
@@ -0,0 +1,57 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.frogports;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import com.simibubi.create.content.kinetics.chainConveyor.ChainConveyorBlockEntity;
import com.simibubi.create.content.kinetics.chainConveyor.ChainConveyorPackage;
import com.simibubi.create.content.logistics.packagePort.frogport.FrogportBlockEntity;
import com.simibubi.create.foundation.blockEntity.SmartBlockEntity;
import com.simibubi.create.infrastructure.config.AllConfigs;
import dev.ryanhcode.sable.Sable;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.Vec3;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import java.util.Map;
@Mixin(ChainConveyorBlockEntity.class)
public abstract class ChainConveyorBlockEntityMixin extends SmartBlockEntity {
public ChainConveyorBlockEntityMixin(final BlockEntityType<?> type, final BlockPos pos, final BlockState state) {
super(type, pos, state);
}
@WrapOperation(method = "exportToPort", at = @At(value = "INVOKE", target = "Lcom/simibubi/create/content/logistics/packagePort/frogport/FrogportBlockEntity;isBackedUp()Z"))
public boolean sable$testSublevelDistance(final FrogportBlockEntity instance, final Operation<Boolean> original, @Local(argsOnly = true) final ChainConveyorPackage chainPackage) {
final Vec3 packagePos = chainPackage.worldPosition;
if (packagePos == null) {
return original.call(instance);
}
final Vec3 frogPos = instance.getBlockPos().getCenter();
final int maxRange = AllConfigs.server().logistics.packagePortRange.get() + 2;
return original.call(instance) || Sable.HELPER.distanceSquaredWithSubLevels(instance.getLevel(), packagePos, frogPos) > maxRange * maxRange;
}
@WrapOperation(method = "tick", at = @At(value = "INVOKE", target = "Lcom/simibubi/create/content/kinetics/chainConveyor/ChainConveyorBlockEntity;notifyPortToAnticipate(Lnet/minecraft/core/BlockPos;)V"))
public void sable$testSublevelDistance1(final ChainConveyorBlockEntity instance, final BlockPos blockPos, final Operation<Void> original, @Local(name = "portEntry") final Map.Entry<BlockPos, ChainConveyorBlockEntity.ConnectedPort> entry, @Local final ChainConveyorPackage chainPackage) {
final Vec3 packagePos = chainPackage.worldPosition;
if (packagePos == null) {
original.call(instance, blockPos);
return;
}
final Vec3 frogPos = this.worldPosition.offset(entry.getKey()).getCenter();
final int maxRange = AllConfigs.server().logistics.packagePortRange.get() + 2;
if (Sable.HELPER.distanceSquaredWithSubLevels(this.getLevel(), packagePos, frogPos) < maxRange * maxRange) {
original.call(instance, blockPos);
}
}
}
@@ -0,0 +1,90 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.frogports;
import com.llamalad7.mixinextras.sugar.Local;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import com.simibubi.create.content.kinetics.chainConveyor.ChainConveyorInteractionHandler;
import com.simibubi.create.content.kinetics.chainConveyor.ChainConveyorShape;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.companion.math.Pose3dc;
import dev.ryanhcode.sable.sublevel.ClientSubLevel;
import dev.ryanhcode.sable.sublevel.SubLevel;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.core.BlockPos;
import net.minecraft.world.phys.Vec3;
import org.joml.Quaternionf;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(ChainConveyorInteractionHandler.class)
public class ChainConveyorInteractionHandlerMixin {
@Shadow
public static BlockPos selectedLift;
@Shadow
public static ChainConveyorShape selectedShape;
@Redirect(method = "clientTick", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/phys/Vec3;distanceToSqr(Lnet/minecraft/world/phys/Vec3;)D"))
private static double sable$addParticleInternal(final Vec3 instance, final Vec3 vec3) {
return Sable.HELPER.distanceSquaredWithSubLevels(Minecraft.getInstance().level, instance, vec3);
}
@Redirect(method = "clientTick", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/phys/Vec3;subtract(Lnet/minecraft/world/phys/Vec3;)Lnet/minecraft/world/phys/Vec3;", ordinal = 0))
private static Vec3 sable$fromSubLiftVec(final Vec3 from, final Vec3 liftVec, @Local(ordinal = 0) final ChainConveyorShape shape) {
final SubLevel subLevel = Sable.HELPER.getContainingClient(liftVec);
if (subLevel != null) {
return subLevel.logicalPose().transformPositionInverse(from).subtract(liftVec);
}
return from.subtract(liftVec);
}
@Redirect(method = "clientTick", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/phys/Vec3;subtract(Lnet/minecraft/world/phys/Vec3;)Lnet/minecraft/world/phys/Vec3;", ordinal = 1))
private static Vec3 sable$toSubLiftVec(final Vec3 to, final Vec3 liftVec, @Local(ordinal = 0) final ChainConveyorShape shape) {
final SubLevel subLevel = Sable.HELPER.getContainingClient(liftVec);
if (subLevel != null) {
return subLevel.logicalPose().transformPositionInverse(to).subtract(liftVec);
}
return to.subtract(liftVec);
}
/**
* @author RyanH
* @reason Take sub-levels into account
*/
@Overwrite
public static void drawCustomBlockSelection(final PoseStack ms, final MultiBufferSource buffer, final Vec3 camera) {
if (selectedLift == null || selectedShape == null)
return;
final VertexConsumer vb = buffer.getBuffer(RenderType.lines());
ms.pushPose();
Vec3 pos = Vec3.atLowerCornerOf(selectedLift);
final SubLevel subLevel = Sable.HELPER.getContainingClient(pos);
if (subLevel instanceof final ClientSubLevel clientSubLevel) {
final Pose3dc renderPose = clientSubLevel.renderPose();
pos = renderPose.transformPosition(pos);
ms.translate(pos.x() - camera.x, pos.y() - camera.y, pos.z() - camera.z);
ms.mulPose(new Quaternionf(renderPose.orientation()));
} else {
ms.translate(pos.x() - camera.x, pos.y() - camera.y, pos.z() - camera.z);
}
((ChainConveyorShapeAccessor) selectedShape).invokeDrawOutline(selectedLift, ms, vb);
ms.popPose();
}
}
@@ -0,0 +1,40 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.frogports;
import com.llamalad7.mixinextras.sugar.Local;
import com.simibubi.create.content.kinetics.chainConveyor.ChainConveyorBlockEntity;
import com.simibubi.create.content.kinetics.chainConveyor.ChainConveyorRidingHandler;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.companion.math.JOMLConversion;
import dev.ryanhcode.sable.companion.math.Pose3dc;
import dev.ryanhcode.sable.sublevel.SubLevel;
import net.minecraft.client.Minecraft;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.core.BlockPos;
import net.minecraft.world.phys.Vec3;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(ChainConveyorRidingHandler.class)
public class ChainConveyorRidingHandlerMixin {
@Redirect(method = "clientTick", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/phys/Vec3;subtract(Lnet/minecraft/world/phys/Vec3;)Lnet/minecraft/world/phys/Vec3;", ordinal = 1))
private static Vec3 sable$fixDiff(final Vec3 targetPosition, final Vec3 playerPosition) {
return JOMLConversion.toMojang(Sable.HELPER.projectOutOfSubLevel(Minecraft.getInstance().level, JOMLConversion.toJOML(targetPosition))
.sub(playerPosition.x, playerPosition.y, playerPosition.z));
}
@Redirect(method = "updateTargetPosition", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/player/LocalPlayer;getLookAngle()Lnet/minecraft/world/phys/Vec3;"))
private static Vec3 sable$fixLookAngle(final LocalPlayer instance, @Local(ordinal = 1) final BlockPos connection, @Local final ChainConveyorBlockEntity clbe) {
final SubLevel subLevel = Sable.HELPER.getContaining(clbe);
if (subLevel != null) {
final Pose3dc pose = subLevel.logicalPose();
final Vec3 lookAngle = instance.getLookAngle();
return pose.transformNormalInverse(lookAngle);
}
return instance.getLookAngle();
}
}
@@ -0,0 +1,16 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.frogports;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import com.simibubi.create.content.kinetics.chainConveyor.ChainConveyorShape;
import net.minecraft.core.BlockPos;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Invoker;
@Mixin(ChainConveyorShape.class)
public interface ChainConveyorShapeAccessor {
@Invoker
void invokeDrawOutline(BlockPos anchor, PoseStack ms, VertexConsumer vb);
}
@@ -0,0 +1,43 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.frogports;
import com.llamalad7.mixinextras.sugar.Local;
import com.simibubi.create.content.kinetics.chainConveyor.ChainConveyorPackage;
import com.simibubi.create.content.kinetics.chainConveyor.ChainPackageInteractionHandler;
import com.simibubi.create.foundation.utility.RaycastHelper;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.sublevel.SubLevel;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.phys.Vec3;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(ChainPackageInteractionHandler.class)
public class ChainPackageInteractionHandlerMixin {
@Redirect(method = "lambda$onUse$0", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/player/LocalPlayer;getEyePosition()Lnet/minecraft/world/phys/Vec3;"))
private static Vec3 sable$getTraceOrigin(final LocalPlayer instance, @Local(argsOnly = true) final ChainConveyorPackage.ChainConveyorPackagePhysicsData data) {
Vec3 origin = instance.getEyePosition();
final SubLevel subLevel = Sable.HELPER.getContainingClient(data.targetPos);
if (subLevel != null) {
origin = subLevel.logicalPose().transformPositionInverse(origin);
}
return origin;
}
@Redirect(method = "lambda$onUse$0", at = @At(value = "INVOKE", target = "Lcom/simibubi/create/foundation/utility/RaycastHelper;getTraceTarget(Lnet/minecraft/world/entity/player/Player;DLnet/minecraft/world/phys/Vec3;)Lnet/minecraft/world/phys/Vec3;"))
private static Vec3 sable$getTraceTarget(final Player playerIn, final double range, final Vec3 from, @Local(argsOnly = true) final ChainConveyorPackage.ChainConveyorPackagePhysicsData data) {
Vec3 target = RaycastHelper.getTraceTarget(playerIn, range, playerIn.getEyePosition());
final SubLevel subLevel = Sable.HELPER.getContainingClient(data.targetPos);
if (subLevel != null) {
target = subLevel.logicalPose().transformPositionInverse(target);
}
return target;
}
}
@@ -0,0 +1,31 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.frogports;
import com.llamalad7.mixinextras.sugar.Local;
import com.simibubi.create.content.kinetics.chainConveyor.ChainConveyorBlockEntity;
import com.simibubi.create.content.kinetics.chainConveyor.ChainConveyorPackage;
import com.simibubi.create.content.kinetics.chainConveyor.ChainPackageInteractionPacket;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerPlayer;
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.CallbackInfo;
@Mixin(ChainPackageInteractionPacket.class)
public class ChainPackageInteractionPacketMixin {
@Shadow @Final private BlockPos selectedConnection;
@Shadow @Final private float chainPosition;
@Inject(method = "applySettings(Lnet/minecraft/server/level/ServerPlayer;Lcom/simibubi/create/content/kinetics/chainConveyor/ChainConveyorBlockEntity;)V", at = @At(value = "INVOKE", target = "Lcom/simibubi/create/content/kinetics/chainConveyor/ChainConveyorBlockEntity;addLoopingPackage(Lcom/simibubi/create/content/kinetics/chainConveyor/ChainConveyorPackage;)Z"))
private void sable$initialiseLoopingWorldPosition(final ServerPlayer player, final ChainConveyorBlockEntity be, final CallbackInfo ci, @Local(name = "chainConveyorPackage") final ChainConveyorPackage chainConveyorPackage) {
chainConveyorPackage.worldPosition = be.getPackagePosition(this.chainPosition, null);
}
@Inject(method = "applySettings(Lnet/minecraft/server/level/ServerPlayer;Lcom/simibubi/create/content/kinetics/chainConveyor/ChainConveyorBlockEntity;)V", at = @At(value = "INVOKE", target = "Lcom/simibubi/create/content/kinetics/chainConveyor/ChainConveyorBlockEntity;addTravellingPackage(Lcom/simibubi/create/content/kinetics/chainConveyor/ChainConveyorPackage;Lnet/minecraft/core/BlockPos;)Z"))
private void sable$initialiseTravellingWorldPosition(final ServerPlayer player, final ChainConveyorBlockEntity be, final CallbackInfo ci, @Local(name = "chainConveyorPackage") final ChainConveyorPackage chainConveyorPackage) {
chainConveyorPackage.worldPosition = be.getPackagePosition(this.chainPosition, this.selectedConnection);
}
}
@@ -0,0 +1,30 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.frogports;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.simibubi.create.content.logistics.packagePort.PackagePortBlockEntity;
import com.simibubi.create.content.logistics.packagePort.PackagePortTarget;
import com.simibubi.create.content.logistics.packagePort.frogport.FrogportBlockEntity;
import dev.ryanhcode.sable.neoforge.mixinhelper.compatibility.create.frogports.FrogportMixinHelper;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.phys.Vec3;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
/**
* Makes frogports face the proper way when picking up and depositing cross-sub-level
*/
@Mixin(FrogportBlockEntity.class)
public class FrogportBlockEntityMixin {
@WrapOperation(method = "getYaw", at = @At(value = "INVOKE", target = "Lcom/simibubi/create/content/logistics/packagePort/PackagePortTarget;getExactTargetLocation(Lcom/simibubi/create/content/logistics/packagePort/PackagePortBlockEntity;Lnet/minecraft/world/level/LevelAccessor;Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/phys/Vec3;"))
public Vec3 sable$getExactTargetLocation(final PackagePortTarget instance,
final PackagePortBlockEntity packagePortBlockEntity,
final LevelAccessor levelAccessor,
final BlockPos blockPos,
final Operation<Vec3> original) {
return FrogportMixinHelper.getExactTargetLocation(instance, packagePortBlockEntity, levelAccessor, blockPos, original);
}
}
@@ -0,0 +1,25 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.frogports;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import com.simibubi.create.content.logistics.packagePort.frogport.FrogportBlock;
import com.simibubi.create.content.logistics.packagePort.frogport.FrogportBlockEntity;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.sublevel.SubLevel;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.phys.Vec3;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@Mixin(FrogportBlock.class)
public class FrogportBlockMixin {
@WrapOperation(method = "lambda$setPlacedBy$0", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/LivingEntity;position()Lnet/minecraft/world/phys/Vec3;"))
private static Vec3 sable$projectPlayerPosition(final LivingEntity instance, final Operation<Vec3> original, @Local(argsOnly = true, name = "arg2") final FrogportBlockEntity be) {
final SubLevel subLevel = Sable.HELPER.getContaining(be);
if (subLevel == null) {
return original.call(instance);
}
return subLevel.logicalPose().transformPositionInverse(original.call(instance));
}
}
@@ -0,0 +1,30 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.frogports;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.simibubi.create.content.logistics.packagePort.PackagePortBlockEntity;
import com.simibubi.create.content.logistics.packagePort.PackagePortTarget;
import com.simibubi.create.content.logistics.packagePort.frogport.FrogportRenderer;
import dev.ryanhcode.sable.neoforge.mixinhelper.compatibility.create.frogports.FrogportMixinHelper;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.phys.Vec3;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
/**
* Makes frogports face & extend the proper amount when picking up and depositing cross-sub-level
*/
@Mixin(FrogportRenderer.class)
public class FrogportRendererMixin {
@WrapOperation(method = "renderSafe(Lcom/simibubi/create/content/logistics/packagePort/frogport/FrogportBlockEntity;FLcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;II)V", at = @At(value = "INVOKE", target = "Lcom/simibubi/create/content/logistics/packagePort/PackagePortTarget;getExactTargetLocation(Lcom/simibubi/create/content/logistics/packagePort/PackagePortBlockEntity;Lnet/minecraft/world/level/LevelAccessor;Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/phys/Vec3;"))
public Vec3 sable$getExactTargetLocation(final PackagePortTarget instance,
final PackagePortBlockEntity packagePortBlockEntity,
final LevelAccessor levelAccessor,
final BlockPos blockPos,
final Operation<Vec3> original) {
return FrogportMixinHelper.getExactTargetLocation(instance, packagePortBlockEntity, levelAccessor, blockPos, original);
}
}
@@ -0,0 +1,30 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.frogports;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.simibubi.create.content.logistics.packagePort.PackagePortBlockEntity;
import com.simibubi.create.content.logistics.packagePort.PackagePortTarget;
import com.simibubi.create.content.logistics.packagePort.frogport.FrogportVisual;
import dev.ryanhcode.sable.neoforge.mixinhelper.compatibility.create.frogports.FrogportMixinHelper;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.phys.Vec3;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
/**
* Makes frogports face & extend the proper amount when picking up and depositing cross-sub-level
*/
@Mixin(FrogportVisual.class)
public class FrogportVisualMixin {
@WrapOperation(method = "animate", at = @At(value = "INVOKE", target = "Lcom/simibubi/create/content/logistics/packagePort/PackagePortTarget;getExactTargetLocation(Lcom/simibubi/create/content/logistics/packagePort/PackagePortBlockEntity;Lnet/minecraft/world/level/LevelAccessor;Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/phys/Vec3;"))
public Vec3 sable$getExactTargetLocation(final PackagePortTarget instance,
final PackagePortBlockEntity packagePortBlockEntity,
final LevelAccessor levelAccessor,
final BlockPos blockPos,
final Operation<Vec3> original) {
return FrogportMixinHelper.getExactTargetLocation(instance, packagePortBlockEntity, levelAccessor, blockPos, original);
}
}
@@ -0,0 +1,24 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.frogports;
import com.llamalad7.mixinextras.sugar.Local;
import com.simibubi.create.content.logistics.packagePort.PackagePortPlacementPacket;
import dev.ryanhcode.sable.Sable;
import net.minecraft.core.Position;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.phys.Vec3;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
/**
* Makes the distance check done by Create's {@link PackagePortPlacementPacket} handling take sub-levels into account.
*/
@Mixin(PackagePortPlacementPacket.class)
public class PackagePortPlacementPacketMixin {
@Redirect(method = "handle", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/phys/Vec3;closerThan(Lnet/minecraft/core/Position;D)Z"))
private boolean sable$handle(final Vec3 instance, final Position position, final double d, @Local(argsOnly = true) final ServerPlayer player) {
return Sable.HELPER.distanceSquaredWithSubLevels(player.level(), instance, position.x(), position.y(), position.z()) < d * d;
}
}
@@ -0,0 +1,39 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.frogports;
import com.llamalad7.mixinextras.sugar.Local;
import com.simibubi.create.content.kinetics.chainConveyor.ChainConveyorBlockEntity;
import com.simibubi.create.content.logistics.packagePort.PackagePortTarget;
import com.simibubi.create.infrastructure.config.AllConfigs;
import dev.ryanhcode.sable.Sable;
import net.minecraft.core.BlockPos;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.phys.Vec3;
import org.jetbrains.annotations.Nullable;
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;
@Mixin(PackagePortTarget.ChainConveyorFrogportTarget.class)
public class PackagePortTargetMixin {
@Shadow
public float chainPos;
@Shadow
@Nullable
public BlockPos connection;
@Inject(method = "export", at = @At(value = "INVOKE", target = "Ljava/util/Set;contains(Ljava/lang/Object;)Z"), cancellable = true)
public void sable$testSublevelDistance(final LevelAccessor level, final BlockPos portPos, final ItemStack box, final boolean simulate, final CallbackInfoReturnable<Boolean> cir, @Local final ChainConveyorBlockEntity cbe) {
final Vec3 targetPos = cbe.getPackagePosition(this.chainPos, this.connection);
final int maxRange = AllConfigs.server().logistics.packagePortRange.get() + 2;
if (Sable.HELPER.distanceSquaredWithSubLevels((Level) level, targetPos, portPos.getX() + 0.5, portPos.getY() + 0.5, portPos.getZ() + 0.5) > maxRange * maxRange) {
cir.setReturnValue(false);
}
}
}
@@ -0,0 +1,54 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.frogports;
import com.simibubi.create.content.logistics.packagePort.PackagePortTargetSelectionHandler;
import com.simibubi.create.infrastructure.config.AllConfigs;
import dev.ryanhcode.sable.ActiveSableCompanion;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.companion.math.JOMLConversion;
import dev.ryanhcode.sable.sublevel.SubLevel;
import net.minecraft.client.Minecraft;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec3;
import org.joml.Vector3d;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow;
@Mixin(PackagePortTargetSelectionHandler.class)
public class PackagePortTargetSelectionHandlerMixin {
@Shadow
public static boolean isPostbox;
/**
* @author RyanH
* @reason Take into account sub-level
*/
@Overwrite
public static String validateDiff(final Vec3 nonProjectedTarget, final BlockPos placedPos) {
final ActiveSableCompanion helper = Sable.HELPER;
final Minecraft mc = Minecraft.getInstance();
final LocalPlayer player = mc.player;
final Level level = player.level();
final Vector3d target = helper.projectOutOfSubLevel(level, JOMLConversion.toJOML(nonProjectedTarget));
final SubLevel frogSubLevel = helper.getContaining(level, placedPos);
if (frogSubLevel != null) {
frogSubLevel.logicalPose().transformPositionInverse(target);
}
final Vector3d localDiff = target.sub(placedPos.getX() + 0.5, placedPos.getY(), placedPos.getZ() + 0.5);
if (localDiff.y < 0.0 && !isPostbox) {
return "package_port.cannot_reach_down";
}
final double packagePortRange = AllConfigs.server().logistics.packagePortRange.get();
if (localDiff.lengthSquared() > packagePortRange * packagePortRange) {
return "package_port.too_far";
}
return null;
}
}
@@ -0,0 +1,31 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.frogports;
import com.llamalad7.mixinextras.sugar.Local;
import com.simibubi.create.foundation.blockEntity.SmartBlockEntity;
import com.simibubi.create.foundation.blockEntity.renderer.SmartBlockEntityRenderer;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.sublevel.ClientSubLevel;
import dev.ryanhcode.sable.sublevel.SubLevel;
import net.minecraft.client.renderer.entity.EntityRenderDispatcher;
import org.joml.Quaterniondc;
import org.joml.Quaternionf;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(SmartBlockEntityRenderer.class)
public class SmartBlockEntityRendererMixin<T extends SmartBlockEntity> {
@Redirect(method = "renderNameplateOnHover", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/entity/EntityRenderDispatcher;cameraOrientation()Lorg/joml/Quaternionf;"))
private Quaternionf sable$renderNameTag(final EntityRenderDispatcher instance, @Local(argsOnly = true) final T be) {
final SubLevel subLevel = Sable.HELPER.getContaining(be);
if (subLevel == null) {
return instance.cameraOrientation();
}
final Quaterniondc subLevelOrientation = ((ClientSubLevel) subLevel).renderPose().orientation();
return instance.cameraOrientation().premul(new Quaternionf(subLevelOrientation).conjugate());
}
}
@@ -0,0 +1,28 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.funnels;
import com.llamalad7.mixinextras.sugar.Local;
import com.simibubi.create.content.logistics.funnel.FunnelBlock;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.companion.math.JOMLConversion;
import net.minecraft.core.Vec3i;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec3;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
/**
* Fixes a Funnel check in Create to take into account sub-levels
*/
@Mixin(FunnelBlock.class)
public class FunnelBlockMixin {
@Redirect(method = "entityInside",
at = @At(value = "INVOKE",
target = "Lnet/createmod/catnip/math/VecHelper;getCenterOf(Lnet/minecraft/core/Vec3i;)Lnet/minecraft/world/phys/Vec3;"),
remap = false)
private Vec3 sable$projectFunnelPos(final Vec3i pos, @Local(argsOnly = true) final Level level) {
return JOMLConversion.toMojang(Sable.HELPER.projectOutOfSubLevel(level, JOMLConversion.atCenterOf(pos)));
}
}
@@ -0,0 +1,57 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.hose_pulley;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.simibubi.create.content.fluids.hosePulley.HosePulleyBlockEntity;
import com.simibubi.create.foundation.blockEntity.SmartBlockEntity;
import dev.ryanhcode.sable.ActiveSableCompanion;
import dev.ryanhcode.sable.Sable;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
@Mixin(HosePulleyBlockEntity.class)
public abstract class HosePulleyBlockEntityMixin extends SmartBlockEntity {
public HosePulleyBlockEntityMixin(final BlockEntityType<?> type, final BlockPos pos, final BlockState state) {
super(type, pos, state);
}
@WrapOperation(method = "lazyTick", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/Level;getBlockState(Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/level/block/state/BlockState;"))
public BlockState sable$checkForCollisions1(final Level instance, final BlockPos blockPos, final Operation<BlockState> original) {
return this.sable$getBlockState(instance, blockPos, original, true);
}
@WrapOperation(method = "tick", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/Level;getBlockState(Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/level/block/state/BlockState;"))
public BlockState sable$checkForCollisions2(final Level instance, final BlockPos blockPos, final Operation<BlockState> original) {
return this.sable$getBlockState(instance, blockPos, original, false);
}
@WrapOperation(method = "onSpeedChanged", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/Level;getBlockState(Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/level/block/state/BlockState;"))
public BlockState sable$checkForCollisions3(final Level instance, final BlockPos blockPos, final Operation<BlockState> original) {
return this.sable$getBlockState(instance, blockPos, original, false);
}
@Unique
private BlockState sable$getBlockState(final Level level, final BlockPos blockPos, final Operation<BlockState> original, boolean inverseReplaceCheck) {
final ActiveSableCompanion helper = Sable.HELPER;
final BlockState gatheredState = helper.runIncludingSubLevels(level, blockPos.getCenter(), true, helper.getContaining(level, this.getBlockPos()), (sublevel, pos) -> {
final BlockState innerState = original.call(level, pos);
if (inverseReplaceCheck ^ innerState.canBeReplaced()) {
return innerState;
}
return null;
});
if (gatheredState != null) {
return gatheredState;
}
return original.call(level, blockPos);
}
}
@@ -0,0 +1,85 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.hose_pulley;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.simibubi.create.content.fluids.hosePulley.HosePulleyFluidHandler;
import com.simibubi.create.content.fluids.transfer.FluidDrainingBehaviour;
import dev.ryanhcode.sable.ActiveSableCompanion;
import dev.ryanhcode.sable.Sable;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level;
import net.neoforged.neoforge.fluids.FluidStack;
import net.neoforged.neoforge.fluids.capability.IFluidHandler;
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.callback.CallbackInfoReturnable;
import java.util.function.Supplier;
@Mixin(HosePulleyFluidHandler.class)
public abstract class HosePulleyFluidHandlerMixin {
@Shadow
private FluidDrainingBehaviour drainer;
@Shadow
private Supplier<BlockPos> rootPosGetter;
@Unique
private BlockPos sable$lastValidPos = null;
@Inject(method = "drainInternal", at = @At("HEAD"))
public void sable$updateLastValidPos(final int maxDrain, final FluidStack resource, final IFluidHandler.FluidAction action, final CallbackInfoReturnable<FluidStack> cir) {
final ActiveSableCompanion helper = Sable.HELPER;
final Level level = this.drainer.getWorld();
final float distance = 1.5f;
this.sable$lastValidPos = helper.runIncludingSubLevels(level, this.rootPosGetter.get().getCenter(), true, helper.getContaining(level, this.drainer.getPos()), (sublevel, pos) -> {
if (sable$hasFluid(level, pos)) {
//add some leniency to the fluid gathering, while keeping large jumps (local -> other sublevel etc) possible
if (this.sable$lastValidPos == null || this.sable$lastValidPos.distSqr(pos) > distance * distance) {
return pos;
}
//no changes needed
return this.sable$lastValidPos;
}
return null;
});
}
@WrapOperation(method = "drainInternal", at = @At(value = "INVOKE", target = "Lcom/simibubi/create/content/fluids/transfer/FluidDrainingBehaviour;getDrainableFluid(Lnet/minecraft/core/BlockPos;)Lnet/neoforged/neoforge/fluids/FluidStack;"))
public FluidStack sable$modifyGetDrainableFluid(final FluidDrainingBehaviour instance, final BlockPos rootPos, final Operation<FluidStack> original) {
if (this.sable$lastValidPos != null) {
return original.call(instance, this.sable$lastValidPos);
}
return original.call(instance, rootPos);
}
@WrapOperation(method = "drainInternal", at = @At(value = "INVOKE", target = "Lcom/simibubi/create/content/fluids/transfer/FluidDrainingBehaviour;pullNext(Lnet/minecraft/core/BlockPos;Z)Z"))
public boolean sable$modifyPullNext(final FluidDrainingBehaviour instance, final BlockPos root, final boolean simulate, final Operation<Boolean> original) {
if (this.sable$lastValidPos != null) {
return original.call(instance, this.sable$lastValidPos, simulate);
}
return original.call(instance, root, simulate);
}
@WrapOperation(method = "getFluidInTank", at = @At(value = "INVOKE", target = "Lcom/simibubi/create/content/fluids/transfer/FluidDrainingBehaviour;getDrainableFluid(Lnet/minecraft/core/BlockPos;)Lnet/neoforged/neoforge/fluids/FluidStack;"))
public FluidStack sable$modifyGetFluidInTank(final FluidDrainingBehaviour instance, final BlockPos rootPos, final Operation<FluidStack> original) {
if (this.sable$lastValidPos != null) {
return original.call(instance, this.sable$lastValidPos);
}
return original.call(instance, rootPos);
}
@Unique
private static boolean sable$hasFluid(final Level level, final BlockPos pos) {
return !level.getFluidState(pos).isEmpty();
}
}
@@ -0,0 +1,17 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.impact;
import com.simibubi.create.content.equipment.bell.AbstractBellBlock;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Invoker;
@Mixin(AbstractBellBlock.class)
public interface AbstractBellBlockAccessor {
@Invoker
boolean invokeRing(Level world, BlockPos pos, Direction direction, Player player);
}
@@ -0,0 +1,17 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.impact;
import com.simibubi.create.content.equipment.bell.AbstractBellBlock;
import dev.ryanhcode.sable.api.block.BlockWithSubLevelCollisionCallback;
import dev.ryanhcode.sable.api.physics.callback.BlockSubLevelCollisionCallback;
import dev.ryanhcode.sable.neoforge.physics.callback.AbstractBellBlockCallback;
import org.spongepowered.asm.mixin.Mixin;
@Mixin(AbstractBellBlock.class)
public class AbstractBellBlockMixin implements BlockWithSubLevelCollisionCallback {
@Override
public BlockSubLevelCollisionCallback sable$getCallback() {
return AbstractBellBlockCallback.INSTANCE;
}
}
@@ -0,0 +1,48 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.inventory_manipulation;
import com.google.common.base.Predicate;
import com.llamalad7.mixinextras.sugar.Local;
import com.simibubi.create.foundation.blockEntity.behaviour.inventory.CapManipulationBehaviourBase;
import dev.ryanhcode.sable.ActiveSableCompanion;
import dev.ryanhcode.sable.Sable;
import net.createmod.catnip.math.BlockFace;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.neoforged.neoforge.capabilities.BlockCapability;
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.Redirect;
@Mixin(CapManipulationBehaviourBase.class)
public class CapManipulationBehaviourBaseMixin {
@Shadow protected Predicate<BlockEntity> filter;
@Shadow protected boolean bypassSided;
@Unique
private BlockPos sable$caughtPos;
@Redirect(method = "findNewCapability", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/Level;getBlockEntity(Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/level/block/entity/BlockEntity;"))
public BlockEntity sable$findNewCapOnSubLevel(final Level level, final BlockPos blockPos) {
final ActiveSableCompanion helper = Sable.HELPER;
return helper.runIncludingSubLevels(level, blockPos.getCenter(), true, helper.getContaining(level, blockPos), (subLevel, internalPos) -> {
final BlockEntity caughtBE = level.getBlockEntity(internalPos);
if (this.filter.apply(caughtBE)) {
this.sable$caughtPos = internalPos;
return caughtBE;
}
return null;
});
}
@Redirect(method = "findNewCapability", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/Level;getCapability(Lnet/neoforged/neoforge/capabilities/BlockCapability;Lnet/minecraft/core/BlockPos;Ljava/lang/Object;)Ljava/lang/Object;"))
public <T> T sable$redirectPos(final Level instance, final BlockCapability<T, Direction> blockCapability, final BlockPos pos, final Object dir, @Local final BlockFace targetBlockFace) {
return instance.getCapability(blockCapability, this.sable$caughtPos, this.bypassSided ? null : targetBlockFace.getFace());
}
}
@@ -0,0 +1,64 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.inventory_manipulation;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.simibubi.create.content.logistics.chute.ChuteBlockEntity;
import com.simibubi.create.foundation.blockEntity.SmartBlockEntity;
import dev.ryanhcode.sable.ActiveSableCompanion;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.sublevel.SubLevel;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.neoforged.neoforge.capabilities.Capabilities;
import net.neoforged.neoforge.items.IItemHandler;
import org.joml.Vector3d;
import org.spongepowered.asm.mixin.Mixin;
@Mixin(ChuteBlockEntity.class)
public abstract class ChuteBlockEntityMixin extends SmartBlockEntity {
public ChuteBlockEntityMixin(final BlockEntityType<?> type, final BlockPos pos, final BlockState state) {
super(type, pos, state);
}
@WrapMethod(method = "grabCapability")
public IItemHandler sable$grabCap(final Direction side, final Operation<IItemHandler> original) {
final IItemHandler handler = original.call(side);
if (handler != null) {
return handler;
}
// anything past this, we don't really need a cache... It has the potential to constantly move as it's not local
final Level level = this.getLevel();
assert level != null;
final BlockPos checkPos = this.worldPosition.relative(side);
final Direction opposite = side.getOpposite();
final Vector3d mut = new Vector3d(opposite.getStepX(), opposite.getStepY(), opposite.getStepZ());
final ActiveSableCompanion helper = Sable.HELPER;
final SubLevel parentSublevel = helper.getContaining(level, checkPos);
if (parentSublevel != null) {
parentSublevel.logicalPose().transformNormalInverse(mut);
}
final Vector3d includSublevelDir = new Vector3d(mut);
return helper.runIncludingSubLevels(
level,
checkPos.getCenter(),
false,
parentSublevel,
(sublevel, pos) -> {
includSublevelDir.set(mut);
if (sublevel != null) {
sublevel.logicalPose().transformNormal(includSublevelDir);
}
return level.getCapability(Capabilities.ItemHandler.BLOCK, pos, Direction.getNearest(includSublevelDir.x, includSublevelDir.y, includSublevelDir.z));
}
);
}
}
@@ -0,0 +1,63 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.lectern_controller;
import com.llamalad7.mixinextras.sugar.Local;
import com.simibubi.create.content.redstone.link.controller.LecternControllerBlockEntity;
import com.simibubi.create.foundation.blockEntity.SmartBlockEntity;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.neoforge.mixinterface.compatibility.create.LecternControllerBlockEntityExtension;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.Vec3;
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.CallbackInfo;
import java.util.UUID;
@Mixin(LecternControllerBlockEntity.class)
public abstract class LecternControllerBlockEntityMixin extends SmartBlockEntity implements LecternControllerBlockEntityExtension {
public LecternControllerBlockEntityMixin(final BlockEntityType<?> type, final BlockPos pos, final BlockState state) {
super(type, pos, state);
}
@Shadow
protected abstract void stopUsing(Player player);
@Shadow
private UUID user;
@Unique
private boolean sable$noDrop;
@Inject(method = "dropController", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/Level;addFreshEntity(Lnet/minecraft/world/entity/Entity;)Z"), cancellable = true)
public void sable$dropController(final BlockState state, final CallbackInfo ci) {
if (!this.sable$noDrop) {
return;
}
ci.cancel();
final Entity entity = ((ServerLevel) this.level).getEntity(this.user);
if (entity instanceof final Player player) {
this.stopUsing(player);
}
}
@Redirect(method = "playerInRange", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/phys/Vec3;distanceToSqr(Lnet/minecraft/world/phys/Vec3;)D"))
private static double sable$fixDistanceCheck(final Vec3 a, final Vec3 b, @Local(argsOnly = true) final Level level) {
return Sable.HELPER.distanceSquaredWithSubLevels(level, a, b);
}
@Override
public void sable$setNoDrop() {
this.sable$noDrop = true;
}
}
@@ -0,0 +1,21 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.lectern_controller;
import com.simibubi.create.content.redstone.link.controller.LecternControllerBlock;
import com.simibubi.create.content.redstone.link.controller.LecternControllerBlockEntity;
import dev.ryanhcode.sable.api.block.BlockSubLevelAssemblyListener;
import dev.ryanhcode.sable.neoforge.mixinterface.compatibility.create.LecternControllerBlockEntityExtension;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.state.BlockState;
import org.spongepowered.asm.mixin.Mixin;
@Mixin(LecternControllerBlock.class)
public class LecternControllerBlockMixin implements BlockSubLevelAssemblyListener {
@Override
public void afterMove(final ServerLevel originLevel, final ServerLevel resultingLevel, final BlockState newState, final BlockPos oldPos, final BlockPos newPos) {
if (originLevel.getBlockEntity(oldPos) instanceof final LecternControllerBlockEntity be) {
((LecternControllerBlockEntityExtension) be).sable$setNoDrop();
}
}
}
@@ -0,0 +1,28 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.mechnical_arm;
import com.simibubi.create.content.kinetics.mechanicalArm.ArmBlockEntity;
import com.simibubi.create.foundation.blockEntity.SmartBlockEntity;
import dev.ryanhcode.sable.Sable;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(ArmBlockEntity.class)
public abstract class MechanicalArmBlockEntity extends SmartBlockEntity {
public MechanicalArmBlockEntity(final BlockEntityType<?> type, final BlockPos pos, final BlockState state) {
super(type, pos, state);
}
@Inject(method = "isAreaActuallyLoaded", at = @At("HEAD"), cancellable = true)
private void sable$forceMechArmsLoad(final BlockPos center, final int range, final CallbackInfoReturnable<Boolean> cir) {
if (Sable.HELPER.getContaining(this.getLevel(), center) != null) {
cir.setReturnValue(true);
}
}
}
@@ -0,0 +1,55 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.mechnical_arm;
import com.llamalad7.mixinextras.sugar.Local;
import com.llamalad7.mixinextras.sugar.Share;
import com.llamalad7.mixinextras.sugar.ref.LocalRef;
import com.simibubi.create.content.kinetics.mechanicalArm.ArmInteractionPoint;
import com.simibubi.create.content.kinetics.mechanicalArm.ArmInteractionPointHandler;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.sublevel.SubLevel;
import net.createmod.catnip.lang.LangBuilder;
import net.minecraft.ChatFormatting;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import org.spongepowered.asm.mixin.Mixin;
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.CallbackInfo;
@Mixin(ArmInteractionPointHandler.class)
public class MechanicalArmSublevelFailure {
@Inject(method = "flushSettings", at = @At("HEAD"))
private static void sable$gatherSublevelInformation(final BlockPos pos, final CallbackInfo ci, @Share("parentSublevel") final LocalRef<SubLevel> parentSublevel, @Share("pointsRemovedSublevel") final LocalRef<Integer> pointsRemovedSublevel) {
parentSublevel.set(Sable.HELPER.getContainingClient(pos));
pointsRemovedSublevel.set(0);
}
@Inject(method = "flushSettings", at = @At(value = "INVOKE", target = "Lnet/minecraft/core/BlockPos;closerThan(Lnet/minecraft/core/Vec3i;D)Z"))
private static void sable$removeDifferentSublevelPoints(final BlockPos pos, final CallbackInfo ci, @Local(name = "point") final ArmInteractionPoint point, @Share("pointsRemovedSublevel") final LocalRef<Integer> pointsRemovedSublevel, @Share("parentSublevel") final LocalRef<SubLevel> parentSublevel) {
final SubLevel pointsublevel = Sable.HELPER.getContainingClient(point.getPos());
if (parentSublevel.get() != pointsublevel) {
pointsRemovedSublevel.set(pointsRemovedSublevel.get() + 1);
}
}
@Redirect(method = "flushSettings", at = @At(value = "INVOKE", target = "Lnet/createmod/catnip/lang/LangBuilder;translate(Ljava/lang/String;[Ljava/lang/Object;)Lnet/createmod/catnip/lang/LangBuilder;"))
private static LangBuilder sable$relayRemovedPoints(final LangBuilder instance, final String langKey, final Object[] args, @Local(name = "removed") final int removed, @Share("pointsRemovedSublevel") final LocalRef<Integer> pointsRemovedSublevel) {
final Integer arg = (Integer) args[0];
Component errorComponent = Component.empty();
if (pointsRemovedSublevel.get() == 0) {
instance.translate(langKey, args);
} else if (arg - pointsRemovedSublevel.get() == 0) {
errorComponent = Component.translatable("sable.create.remove.points_removed_sublevel", removed)
.withStyle(ChatFormatting.RED);
} else {
errorComponent = Component.translatable("sable.create.mechanical_arm.points_removed_sublevel_and_range", removed)
.withStyle(ChatFormatting.RED);
}
instance.add(errorComponent);
return instance;
}
}
@@ -0,0 +1,13 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.nozzle;
import com.simibubi.create.content.kinetics.fan.NozzleBlockEntity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(NozzleBlockEntity.class)
public interface NozzleBlockEntityAccessor {
@Accessor
float getRange();
}
@@ -0,0 +1,61 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.nozzle.block_entity;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import com.simibubi.create.content.kinetics.fan.NozzleBlockEntity;
import com.simibubi.create.foundation.blockEntity.SmartBlockEntity;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.companion.math.JOMLConversion;
import dev.ryanhcode.sable.neoforge.mixinterface.compatibility.create.NozzleBlockEntityExtension;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Vec3i;
import net.minecraft.core.particles.ParticleOptions;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.Vec3;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(NozzleBlockEntity.class)
public abstract class NozzleBEFixesMixin extends SmartBlockEntity {
public NozzleBEFixesMixin(final BlockEntityType<?> type, final BlockPos pos, final BlockState state) {
super(type, pos, state);
}
@Redirect(remap = false, method = "tick", at = @At(value = "INVOKE", target = "Lnet/createmod/catnip/math/VecHelper;getCenterOf(Lnet/minecraft/core/Vec3i;)Lnet/minecraft/world/phys/Vec3;"))
public Vec3 sable$nozzlePosition(final Vec3i pos) {
return JOMLConversion.toMojang(Sable.HELPER.projectOutOfSubLevel(this.getLevel(), JOMLConversion.atCenterOf(pos)));
}
@Redirect(remap = false, method = "lazyTick", at = @At(value = "INVOKE", target = "Lnet/createmod/catnip/math/VecHelper;getCenterOf(Lnet/minecraft/core/Vec3i;)Lnet/minecraft/world/phys/Vec3;"))
public Vec3 sable$nozzlePositionLazy(final Vec3i pos) {
return JOMLConversion.toMojang(Sable.HELPER.projectOutOfSubLevel(this.getLevel(), JOMLConversion.atCenterOf(pos)));
}
@Redirect(method = "canSee", at = @At(value = "INVOKE", target = "Lnet/createmod/catnip/math/VecHelper;getCenterOf(Lnet/minecraft/core/Vec3i;)Lnet/minecraft/world/phys/Vec3;"), remap = false)
private Vec3 sable$projectCenter(final Vec3i pos) {
return JOMLConversion.toMojang(Sable.HELPER.projectOutOfSubLevel(this.getLevel(), JOMLConversion.atCenterOf(pos)));
}
@WrapOperation(method = "tick", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/Mth;clamp(III)I"))
public int sable$clampParticlesMore(final int value, final int min, final int max, final Operation<Integer> original) {
return original.call(value, 3, max);
}
@Redirect(method = "tick", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/Level;addParticle(Lnet/minecraft/core/particles/ParticleOptions;DDDDDD)V"))
public void sable$checkDirection(final Level instance, final ParticleOptions particleOptions, final double x, final double y, final double z, final double mx, final double my, final double mz, @Local(ordinal = 0) final Vec3 origin, @Local(ordinal = 1) final Vec3 start) {
final Vec3 direction = start.subtract(origin).normalize();
final Direction nearest = Direction.getNearest(direction.x, direction.y, direction.z);
if (!((NozzleBlockEntityExtension) this).sable$getValidDirections().contains(nearest)) {
return;
}
instance.addParticle(particleOptions, x, y, z, mx, my, mz);
}
}
@@ -0,0 +1,81 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.nozzle.block_entity;
import com.simibubi.create.content.kinetics.fan.NozzleBlockEntity;
import com.simibubi.create.foundation.blockEntity.SmartBlockEntity;
import dev.ryanhcode.sable.ActiveSableCompanion;
import dev.ryanhcode.sable.Sable;
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.neoforge.mixinhelper.compatibility.create.nozzles.NozzleHoveringHelper;
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
import net.createmod.catnip.data.Couple;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.ClipContext;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.Vec3;
import net.minecraft.world.phys.shapes.CollisionContext;
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.callback.CallbackInfo;
import java.util.List;
@Mixin(NozzleBlockEntity.class)
public abstract class NozzleHoveringMixin extends SmartBlockEntity implements BlockEntitySubLevelActor {
@Shadow private boolean pushing;
@Shadow private float range;
@Unique
private List<Couple<Vec3>> sable$rayPoints = null;
public NozzleHoveringMixin(final BlockEntityType<?> type, final BlockPos pos, final BlockState state) {
super(type, pos, state);
}
@Inject(method = "<init>", at = @At("TAIL"))
public void sable$generateRays(final BlockEntityType type, final BlockPos pos, final BlockState state, final CallbackInfo ci) {
this.sable$rayPoints = NozzleHoveringHelper.gatherRaycastPoints(state);
}
@Inject(method = "tick", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/RandomSource;nextInt(I)I"))
private void addPhysicsParticles(final CallbackInfo ci) {
final ActiveSableCompanion helper = Sable.HELPER;
if (helper.getContaining(this) != null && this.pushing) {
final Vec3 blockCorner = Vec3.atLowerCornerOf(this.getBlockPos());
final Couple<Vec3> ray = this.sable$rayPoints.get(this.level.random.nextInt(this.sable$rayPoints.size()));
final Vec3 start = ray.getFirst().add(blockCorner);
final Vec3 end = ray.getSecond().add(blockCorner);
final ClipContext context = new ClipContext(
start,
end,
ClipContext.Block.OUTLINE,
ClipContext.Fluid.ANY,
CollisionContext.empty()
);
final BlockHitResult clip = this.level.clip(context);
NozzleHoveringHelper.spawnWindHitParticle(
this.level, helper.getContaining(this), clip,
JOMLConversion.toJOML(start), this.range / 40
);
}
}
@Override
public void sable$physicsTick(final ServerSubLevel subLevel, final RigidBodyHandle handle, final double timeStep) {
final Vector3d force = NozzleHoveringHelper.gatherForceFromRays(subLevel, timeStep, this.getLevel(), this.getBlockPos(), (NozzleBlockEntity) (Object) this, this.sable$rayPoints);
if (force != null) {
final QueuedForceGroup forceGroup = subLevel.getOrCreateQueuedForceGroup(ForceGroups.PROPULSION.get());
forceGroup.applyAndRecordPointForce(JOMLConversion.toJOML(Vec3.atCenterOf(this.getBlockPos())), force);
}
}
}
@@ -0,0 +1,47 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.nozzle.block_entity;
import com.simibubi.create.content.kinetics.fan.NozzleBlockEntity;
import com.simibubi.create.foundation.blockEntity.SmartBlockEntity;
import dev.ryanhcode.sable.neoforge.mixinterface.compatibility.create.NozzleBlockEntityExtension;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import org.spongepowered.asm.mixin.Mixin;
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.callback.CallbackInfo;
import java.util.EnumSet;
@Mixin(NozzleBlockEntity.class)
public abstract class ValidNozzledirectionMixin extends SmartBlockEntity implements NozzleBlockEntityExtension {
@Unique
private final EnumSet<Direction> sable$validDirections = EnumSet.noneOf(Direction.class);
public ValidNozzledirectionMixin(final BlockEntityType<?> type, final BlockPos pos, final BlockState state) {
super(type, pos, state);
}
@Override
public EnumSet<Direction> sable$getValidDirections() {
return this.sable$validDirections;
}
@Inject(method = "tick", at = @At("HEAD"))
public void sable$updateValidDirections(final CallbackInfo ci) {
this.sable$validDirections.clear();
if (this.getLevel() != null) {
for (final Direction value : Direction.values()) {
final BlockState state = this.getLevel().getBlockState(this.getBlockPos().relative(value));
if (state.canBeReplaced()) {
this.sable$validDirections.add(value);
}
}
}
}
}
@@ -0,0 +1,78 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.particles;
import com.llamalad7.mixinextras.sugar.Local;
import com.llamalad7.mixinextras.sugar.ref.LocalRef;
import com.simibubi.create.content.kinetics.fan.AirFlowParticle;
import com.simibubi.create.content.kinetics.fan.IAirCurrentSource;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.sublevel.SubLevel;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.particle.SimpleAnimatedParticle;
import net.minecraft.client.particle.SpriteSet;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import org.spongepowered.asm.mixin.Final;
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.CallbackInfo;
@Mixin(AirFlowParticle.class)
public abstract class AirFlowParticleMixin extends SimpleAnimatedParticle {
@Unique
Vec3 sable$subLevelOrientation;
@Shadow
@Final
private IAirCurrentSource source;
protected AirFlowParticleMixin(final ClientLevel arg, final double d, final double e, final double f, final SpriteSet arg2, final float g) {
super(arg, d, e, f, arg2, g);
}
@Inject(method = "tick", at = @At("HEAD"), cancellable = true)
public void sable$fixAirflowParticle(final CallbackInfo ci) {
if (this.source == null || this.source.getAirCurrent() == null || this.source.getAirCurrent().direction == null) {
this.remove();
ci.cancel();
}
}
@Redirect(method = "tick", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/phys/AABB;contains(DDD)Z", ordinal = 0))
public boolean sable$reverseProjectPos(final AABB instance, final double x, final double y, final double z) {
final SubLevel subLevel = Sable.HELPER.getContainingClient(this.source.getAirCurrentPos());
if (subLevel != null) {
return true;
}
return instance.contains(x, y, z);
}
@Redirect(method = "tick", at = @At(value = "NEW", target = "(DDD)Lnet/minecraft/world/phys/Vec3;", ordinal = 0))
public Vec3 sable$reverseProjectPos2(final double x, final double y, final double z) {
final SubLevel subLevel = Sable.HELPER.getContainingClient(this.source.getAirCurrentPos());
if (subLevel != null) {
return subLevel.logicalPose().transformPositionInverse(new Vec3(x, y, z));
}
return new Vec3(x, y, z);
}
@Inject(method = "tick", at = @At(value = "INVOKE", target = "Lcom/simibubi/create/content/kinetics/fan/IAirCurrentSource;getAirCurrent()Lcom/simibubi/create/content/kinetics/fan/AirCurrent;", ordinal = 1))
public void sable$transformNormal(final CallbackInfo ci, @Local(ordinal = 1) final LocalRef<Vec3> motion) {
final SubLevel subLevel = Sable.HELPER.getContainingClient(this.source.getAirCurrentPos());
if (subLevel != null) {
if (this.sable$subLevelOrientation == null /*|| !this.source.getAirCurrent().pushing*/) {
this.sable$subLevelOrientation = subLevel.logicalPose().transformNormal(motion.get());
}
} else {
this.sable$subLevelOrientation = motion.get();
}
motion.set(this.sable$subLevelOrientation);
}
}
@@ -0,0 +1,105 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.particles;
import com.simibubi.create.foundation.particle.AirParticle;
import com.simibubi.create.foundation.particle.AirParticleData;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.api.particle.ParticleSubLevelKickable;
import net.createmod.catnip.math.VecHelper;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.particle.SimpleAnimatedParticle;
import net.minecraft.client.particle.SpriteSet;
import net.minecraft.core.Direction;
import net.minecraft.util.Mth;
import net.minecraft.world.phys.Vec3;
import org.joml.Vector3d;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
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.callback.CallbackInfo;
@Mixin(AirParticle.class)
public abstract class AirParticleMixin extends SimpleAnimatedParticle implements ParticleSubLevelKickable {
@Shadow
private float twirlAngleOffset;
@Shadow
private float twirlRadius;
@Shadow
private float drag;
@Unique
private double sable$originX;
@Unique
private double sable$originZ;
@Unique
private double sable$originY;
@Unique
private double sable$targetY;
@Unique
private double sable$targetX;
@Unique
private double sable$targetZ;
@Shadow
private Direction.Axis twirlAxis;
protected AirParticleMixin(final ClientLevel arg, final double d, final double e, final double f, final SpriteSet arg2, final float g) {
super(arg, d, e, f, arg2, g);
}
@Inject(method = "<init>", at = @At("TAIL"))
private void sable$postInit(final ClientLevel world, final AirParticleData data, final double x, final double y, final double z, final double dx, final double dy, final double dz, final SpriteSet sprite, final CallbackInfo ci) {
this.sable$originX = x;
this.sable$originY = y;
this.sable$originZ = z;
this.sable$targetX = x + dx;
this.sable$targetY = y + dy;
this.sable$targetZ = z + dz;
}
/**
* @author RyanH
* @reason Fix target / origin handling with sub-levels
*/
@Overwrite
public void tick() {
this.xo = this.x;
this.yo = this.y;
this.zo = this.z;
if (this.age++ >= this.lifetime) {
this.remove();
return;
}
final float progress = (float) Math.pow(((float) this.age) / this.lifetime, this.drag);
final float angle = (progress * 2 * 360 + this.twirlAngleOffset) % 360;
final Vec3 twirl = VecHelper.rotate(new Vec3(0, this.twirlRadius, 0), angle, this.twirlAxis);
final double desiredX = (Mth.lerp(progress, this.sable$originX, this.sable$targetX) + twirl.x);
final double desiredY = (Mth.lerp(progress, this.sable$originY, this.sable$targetY) + twirl.y);
final double desiredZ = (Mth.lerp(progress, this.sable$originZ, this.sable$targetZ) + twirl.z);
final Vector3d desiredVec = Sable.HELPER.projectOutOfSubLevel(this.level, new Vector3d(desiredX, desiredY, desiredZ));
this.xd = desiredVec.x - this.x;
this.yd = desiredVec.y - this.y;
this.zd = desiredVec.z - this.z;
this.setSpriteFromAge(this.sprites);
this.move(this.xd, this.yd, this.zd);
}
@Override
public boolean sable$shouldKickFromTracking() {
return false;
}
@Override
public boolean sable$shouldCollideWithTrackingSubLevel() {
return false;
}
}
@@ -0,0 +1,59 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.raycast;
import com.llamalad7.mixinextras.sugar.Local;
import com.simibubi.create.foundation.utility.RaycastHelper;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.companion.math.JOMLConversion;
import dev.ryanhcode.sable.sublevel.SubLevel;
import net.minecraft.core.BlockPos;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.phys.Vec3;
import org.joml.Vector3d;
import org.spongepowered.asm.mixin.Mixin;
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;
import java.util.function.Predicate;
import static dev.ryanhcode.sable.neoforge.mixinhelper.compatibility.create.raycasts.SableRaycastHelper.rayCastUntilWithSublevels;
/**
* Fixes Create's {@link RaycastHelper} to take into account sub-levels in raycasts
*/
@Mixin(RaycastHelper.class)
public class RaycastHelperMixin {
@Redirect(method = "getTraceTarget", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/phys/Vec3;add(DDD)Lnet/minecraft/world/phys/Vec3;"))
private static Vec3 sable$rotateWithSublevels(final Vec3 instance, final double pX, final double pY, final double pZ, @Local(argsOnly = true) final Player player) {
Vec3 resultTarget = new Vec3(pX, pY, pZ);
final Entity vehicle = player.getVehicle();
if (vehicle != null) {
final SubLevel vehicleSubLevel = Sable.HELPER.getContaining(player.level(), vehicle.position());
// Rotate the target if the player is in a vehicle
if (vehicleSubLevel != null) {
final Vector3d vec = JOMLConversion.toJOML(resultTarget);
vehicleSubLevel.logicalPose().orientation().transform(vec);
resultTarget = JOMLConversion.toMojang(vec);
}
}
return instance.add(resultTarget);
}
@Inject(method = "rayTraceUntil(Lnet/minecraft/world/entity/player/Player;DLjava/util/function/Predicate;)Lcom/simibubi/create/foundation/utility/RaycastHelper$PredicateTraceResult;",
at = @At(value = "HEAD"),
remap = false, cancellable = true)
private static void sable$rayTraceSublevels(final Player playerIn, final double range, final Predicate<BlockPos> predicate, final CallbackInfoReturnable<RaycastHelper.PredicateTraceResult> cir) {
final Vec3 start = playerIn.getEyePosition();
final Vec3 end = RaycastHelper.getTraceTarget(playerIn, range, start);
cir.setReturnValue(rayCastUntilWithSublevels(playerIn.level(), start, end, predicate));
}
}
@@ -0,0 +1,29 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.redstone_contacts;
import com.simibubi.create.AllBlockEntityTypes;
import com.simibubi.create.AllBlocks;
import com.simibubi.create.foundation.data.CreateRegistrate;
import com.tterrag.registrate.util.entry.BlockEntityEntry;
import dev.ryanhcode.sable.neoforge.mixinhelper.compatibility.create.redstone_contact.RedstoneContactBlockEntity;
import dev.ryanhcode.sable.neoforge.mixinhelper.compatibility.create.redstone_contact.RedstoneContactBlockEntityTypeGetter;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
@Mixin(AllBlockEntityTypes.class)
public class AllBlockEntityTypesMixin implements RedstoneContactBlockEntityTypeGetter {
@Shadow @Final private static CreateRegistrate REGISTRATE;
@Unique
private static final BlockEntityEntry<RedstoneContactBlockEntity> REDSTONE_CONTACT = REGISTRATE
.blockEntity("redstone_contact", RedstoneContactBlockEntity::new)
.validBlock(AllBlocks.REDSTONE_CONTACT)
.register();
@Override
public BlockEntityEntry<RedstoneContactBlockEntity> sable$getRedstoneContactType() {
return REDSTONE_CONTACT;
}
}
@@ -0,0 +1,45 @@
package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.redstone_contacts;
import com.simibubi.create.AllBlockEntityTypes;
import com.simibubi.create.content.redstone.contact.RedstoneContactBlock;
import com.simibubi.create.foundation.block.IBE;
import com.simibubi.create.foundation.block.WrenchableDirectionalBlock;
import dev.ryanhcode.sable.neoforge.mixinhelper.compatibility.create.redstone_contact.RedstoneContactBlockEntity;
import dev.ryanhcode.sable.neoforge.mixinhelper.compatibility.create.redstone_contact.RedstoneContactBlockEntityTypeGetter;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityTicker;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
@Mixin(RedstoneContactBlock.class)
public class RedstoneContactBlockMixin extends WrenchableDirectionalBlock implements IBE<RedstoneContactBlockEntity> {
@Unique
private static final AllBlockEntityTypes sable$cursed = new AllBlockEntityTypes();
public RedstoneContactBlockMixin(final Properties properties) {
super(properties);
}
@Override
public Class<RedstoneContactBlockEntity> getBlockEntityClass() {
return RedstoneContactBlockEntity.class;
}
@Override
public BlockEntityType<? extends RedstoneContactBlockEntity> getBlockEntityType() {
return ((RedstoneContactBlockEntityTypeGetter) sable$cursed).sable$getRedstoneContactType().get();
}
@Override
public <S extends BlockEntity> BlockEntityTicker<S> getTicker(final Level level, final BlockState p_153213_, final BlockEntityType<S> p_153214_) {
if (!level.isClientSide) {
return IBE.super.getTicker(level, p_153213_, p_153214_);
} else {
return null;
}
}
}

Some files were not shown because too many files have changed in this diff Show More