diff --git a/.dev/debug-snippets/collide-surface-orientation.md b/.dev/debug-snippets/collide-surface-orientation.md new file mode 100644 index 00000000..b2c40419 --- /dev/null +++ b/.dev/debug-snippets/collide-surface-orientation.md @@ -0,0 +1,60 @@ +# Collision debug ledger — surface-oriented entities + +Per-tick diagnostic for the surface-orientation force model (entities adopting the tilt of a +rotated sub-level). Stripped from `feature/entity-surface-orientation` before upstreaming because +it logs every tick per surface-oriented player; archived here because it is the fastest way to see +what the force model is actually doing when the tilt behaviour needs another pass. + +## Provenance + +- Added in `4798088` ("WIP: surface-orientation force model, picking, lighting (with debug ledgers)") +- Still present at `907d073`, the tip of `feature/entity-surface-orientation` +- A companion light-probe ledger lived in `EntityRendererMixin` and was already removed in `907d073` + +## What it prints + +| Field | Meaning | +| --- | --- | +| `in` | `collisionMotion` as handed to `collide` | +| `out` | `collisionInfo.motion` after resolution — the difference from `in` is what the solver absorbed | +| `inh` | `collisionInfo.inheritedMotion` — motion carried from the sub-level's own movement | +| `dm` | the entity's `getDeltaMovement()` at resolution time | +| `up` | `sink.entityUpDirection` — the surface normal the entity is oriented to | +| `vBelow` | `collisionInfo.verticalCollisionBelow` | +| `ground` | `entity.onGround()` | + +Read `up` together with `in`/`out` to tell a genuine absorption from a tangential leak: on a stable +stand the tangential (perpendicular-to-`up`) part of `out` should be ~0, and `ground` should stay +`true` every tick. A `ground` that flickers false is the fall-catch-slide cycle described in +`LivingEntityMixin.sable$noGravityOnTiltedGround`. + +## Where it goes + +`common/src/main/java/dev/ryanhcode/sable/sublevel/entity_collision/SubLevelEntityCollision.java`, +in `collide`, immediately after `collisionInfo.firstCollisions` is assigned and before +`return collisionInfo;`. + +No import changes needed — `dev.ryanhcode.sable.Sable`, `net.minecraft.world.entity.player.Player` +and `net.minecraft.world.phys.Vec3` are already imported by that file. + +```java + // TEMP diagnostics for surface-orientation force debugging — remove before merging. + if (entity.level().isClientSide && entity instanceof Player && customEntityOrientation != null) { + final Vec3 dm = entity.getDeltaMovement(); + Sable.LOGGER.info(String.format( + "[collide dbg] in=(%.5f,%.5f,%.5f) out=(%.5f,%.5f,%.5f) inh=%s dm=(%.5f,%.5f,%.5f) up=(%.3f,%.3f,%.3f) vBelow=%b ground=%b", + collisionMotionMoj.x, collisionMotionMoj.y, collisionMotionMoj.z, + collisionInfo.motion.x, collisionInfo.motion.y, collisionInfo.motion.z, + collisionInfo.inheritedMotion, + dm.x, dm.y, dm.z, + sink.entityUpDirection.x, sink.entityUpDirection.y, sink.entityUpDirection.z, + collisionInfo.verticalCollisionBelow, + entity.onGround())); + } +``` + +## Local variables it depends on + +All are in scope at that point in `collide` as of `76e9ae7`; if the method is refactored, re-check +`collisionMotionMoj`, `customEntityOrientation`, `sink.entityUpDirection` and `collisionInfo` +before assuming the block still compiles. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8999633c..fbf05399 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,7 @@ When submitting a pull request to Sable, you are giving [RyanHCode](https://github.com/ryanhcode) the right to license your contributions under the [Polyform Shield License (Version 1.0.0)](LICENSE.md), and you irrevocably permit a potential -future re-licensing of Sable, containing your contributions, under the [MIT license](https://opensource.org/license/mit). +future re-licensing of Sable by [RyanHCode](https://github.com/ryanhcode), containing your contributions, under the +[MIT license](https://opensource.org/license/mit). -The submitted code of your PR must be your own, original work, that you have the right to contribute. +The submitted code of your PR must be your own, original work, that you have the right to contribute to Sable. diff --git a/common/src/main/java/dev/ryanhcode/sable/SableCommonEvents.java b/common/src/main/java/dev/ryanhcode/sable/SableCommonEvents.java index cd57d9d9..4f6ce828 100644 --- a/common/src/main/java/dev/ryanhcode/sable/SableCommonEvents.java +++ b/common/src/main/java/dev/ryanhcode/sable/SableCommonEvents.java @@ -7,6 +7,7 @@ import dev.ryanhcode.sable.physics.chunk.VoxelNeighborhoodState; import dev.ryanhcode.sable.physics.config.FloatingBlockMaterialDataHandler; import dev.ryanhcode.sable.physics.config.block_properties.PhysicsBlockPropertiesDefinitionLoader; +import dev.ryanhcode.sable.physics.config.dimension_physics.DimensionPhysicsData; import dev.ryanhcode.sable.physics.floating_block.FloatingBlockController; import dev.ryanhcode.sable.sublevel.ServerSubLevel; import dev.ryanhcode.sable.sublevel.SubLevel; @@ -93,6 +94,7 @@ public static void handleBlockChange(final ServerLevel level, final LevelChunk c public static void syncDataPacket(final VeilPacketManager.PacketSink sink) { sink.sendPacket(PhysicsBlockPropertiesDefinitionLoader.INSTANCE.getDefinitions().stream().map(ClientboundPhysicsPropertyPacket::new).toArray(CustomPacketPayload[]::new)); + sink.sendPacket(DimensionPhysicsData.compilePacket()); sink.sendPacket(FloatingBlockMaterialDataHandler.allMaterials.entrySet().stream().map(e -> new ClientboundFloatingBlockMaterialPacket(e.getKey(), e.getValue())).toArray(CustomPacketPayload[]::new)); } } diff --git a/common/src/main/java/dev/ryanhcode/sable/api/command/SubLevelArgumentType.java b/common/src/main/java/dev/ryanhcode/sable/api/command/SubLevelArgumentType.java index cd82145f..634c51af 100644 --- a/common/src/main/java/dev/ryanhcode/sable/api/command/SubLevelArgumentType.java +++ b/common/src/main/java/dev/ryanhcode/sable/api/command/SubLevelArgumentType.java @@ -9,10 +9,15 @@ import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.SuggestionsBuilder; -import dev.ryanhcode.sable.command.argument.SubLevelSelector; import dev.ryanhcode.sable.command.argument.SubLevelSelectorModifierType; import dev.ryanhcode.sable.command.argument.SubLevelSelectorType; +import dev.ryanhcode.sable.command.argument.SubLevelSuggestionProvider; +import dev.ryanhcode.sable.command.argument.selector.SubLevelTarget; +import dev.ryanhcode.sable.command.argument.selector.SubLevelTargetNone; +import dev.ryanhcode.sable.command.argument.selector.SubLevelTargetSelector; +import dev.ryanhcode.sable.command.argument.selector.SubLevelTargetUUID; import dev.ryanhcode.sable.sublevel.ServerSubLevel; +import dev.ryanhcode.sable.sublevel.SubLevel; import it.unimi.dsi.fastutil.Pair; import it.unimi.dsi.fastutil.objects.ObjectArrayList; import it.unimi.dsi.fastutil.objects.ObjectList; @@ -24,26 +29,25 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.function.Function; -public class SubLevelArgumentType implements ArgumentType { +public class SubLevelArgumentType implements ArgumentType { - public static final Function> NO_SUGGESTIONS = SuggestionsBuilder::buildFuture; - private static final SimpleCommandExceptionType ERROR_SINGLE_SUB_LEVEL_REQUIRED = + public static final Function NO_SUGGESTIONS = b -> b; + public static final SimpleCommandExceptionType ERROR_SINGLE_SUB_LEVEL_REQUIRED = new SimpleCommandExceptionType(Component.translatable("argument.sable.single_sub_level_required")); - private static final SimpleCommandExceptionType ERROR_INVALID_SUBLEVEL = - new SimpleCommandExceptionType(Component.translatable("argument.sable.sub_level.invalid")); - private static final SimpleCommandExceptionType UNEXPECTED_END_OF_INPUT = + public static final SimpleCommandExceptionType ERROR_INVALID_SELECTOR = + new SimpleCommandExceptionType(Component.translatable("argument.sable.invalid_selector")); + public static final SimpleCommandExceptionType ERROR_UNEXPECTED_END_OF_INPUT = new SimpleCommandExceptionType(Component.translatable("argument.sable.unexpected_end_of_input")); + public static final SimpleCommandExceptionType ERROR_INVALID_UUID = new SimpleCommandExceptionType(Component.translatable("argument.sable.invalid_uuid")); + public static final SimpleCommandExceptionType ERROR_CANNOT_FIND_SUB_LEVEL = new SimpleCommandExceptionType(Component.translatable("argument.sable.cannot_find_sub_level")); private static final String STATIC_WORLD = "static_world"; private static final Collection EXAMPLES = Arrays.stream(SubLevelSelectorType.values()) .map(type -> "@" + type.getChar()).toList(); - private static Function> suggestions = NO_SUGGESTIONS; + private static Function SELECTOR_SUGGESTIONS = NO_SUGGESTIONS; private final boolean allowStaticLevel; private final boolean allowMultiple; @@ -53,11 +57,11 @@ public SubLevelArgumentType(final boolean allowStaticLevel, final boolean allowM } public static Collection getSubLevels(final CommandContext ctx, final String name) throws CommandSyntaxException { - return ctx.getArgument(name, SubLevelSelector.class).getSubLevels(ctx.getSource()); + return ctx.getArgument(name, SubLevelTarget.class).getSubLevels(ctx.getSource()); } public static ServerSubLevel getSingleSubLevel(final CommandContext ctx, final String name) throws CommandSyntaxException { - final Collection subLevels = ctx.getArgument(name, SubLevelSelector.class).getSubLevels(ctx.getSource()); + final Collection subLevels = ctx.getArgument(name, SubLevelTarget.class).getSubLevels(ctx.getSource()); if (subLevels.size() > 1) { throw ERROR_SINGLE_SUB_LEVEL_REQUIRED.create(); } @@ -83,7 +87,7 @@ public static SubLevelArgumentType subLevelsOrLevel() { private static @NotNull List> parseSelectorArguments(final StringReader reader) throws CommandSyntaxException { final List> modifiers = new ObjectArrayList<>(); - setSuggestions(reader, "["); + setSelectorSuggestions(reader, "["); final List> permittedPreEntryToken = new ArrayList<>(SubLevelSelectorModifierType.getAllNamesWithTooltip() .stream().map(s -> Pair.of(s.first() + "=", s.second())).toList()); @@ -93,28 +97,28 @@ public static SubLevelArgumentType subLevelsOrLevel() { if (reader.canRead() && reader.peek() == '[') { reader.skip(); - setSuggestionsWithTooltip(reader, permittedPreEntryToken); + setSelectorSuggestionsWithTooltip(reader, permittedPreEntryToken); while (reader.canRead() && reader.peek() != ']') { if (reader.peek() == ',') { reader.skip(); } - setSuggestionsWithTooltip(reader, permittedPreEntryToken); + setSelectorSuggestionsWithTooltip(reader, permittedPreEntryToken); final String propertyName = readUntilEndOrCharacter(reader, '='); if (!reader.canRead() || reader.peek() != '=') { - throw UNEXPECTED_END_OF_INPUT.createWithContext(reader); + throw ERROR_UNEXPECTED_END_OF_INPUT.createWithContext(reader); } reader.skip(); final SubLevelSelectorModifierType modifierType = SubLevelSelectorModifierType.getModifier(propertyName, reader); if (modifierType == null) { - throw UNEXPECTED_END_OF_INPUT.createWithContext(reader); + throw ERROR_UNEXPECTED_END_OF_INPUT.createWithContext(reader); } final SubLevelSelectorModifierType.Modifier modifier = modifierType.getParser().parse(reader); modifiers.add(Pair.of(modifierType, modifier)); - setSuggestionsWithTooltip(reader, permittedPreEntryToken); + setSelectorSuggestionsWithTooltip(reader, permittedPreEntryToken); if (isFirstEntry) { permittedPreEntryToken.add(Pair.of(",", null)); isFirstEntry = false; @@ -124,32 +128,33 @@ public static SubLevelArgumentType subLevelsOrLevel() { if (reader.canRead() && reader.peek() == ']') { reader.skip(); } else { - throw UNEXPECTED_END_OF_INPUT.createWithContext(reader); + throw ERROR_UNEXPECTED_END_OF_INPUT.createWithContext(reader); } } return modifiers; } - public static void setSuggestions(final StringReader reader, final String... suggested) { - setSuggestions(reader, Arrays.asList(suggested)); + public static void setSelectorSuggestions(final StringReader reader, final String... suggested) { + setSelectorSuggestions(reader, Arrays.asList(suggested)); } - public static void setSuggestions(final StringReader reader, final List suggested) { - setSuggestionsWithTooltip(reader, suggested.stream().map(s -> Pair.of(s, (Message) null)).toList()); + public static void setSelectorSuggestions(final StringReader reader, final List suggested) { + setSelectorSuggestionsWithTooltip(reader, suggested.stream().map(s -> Pair.of(s, (Message) null)).toList()); } @SafeVarargs - public static void setSuggestionsWithTooltip(final StringReader reader, final Pair... suggested) { - setSuggestionsWithTooltip(reader, Arrays.asList(suggested)); + public static void setSelectorSuggestionsWithTooltip(final StringReader reader, final Pair... suggested) { + setSelectorSuggestionsWithTooltip(reader, Arrays.asList(suggested)); } - public static void setSuggestionsWithTooltip(final StringReader reader, final List> suggested) { + public static void setSelectorSuggestionsWithTooltip(final StringReader reader, final List> suggested) { final int cursor = reader.getCursor(); - suggestions = builder -> { + SELECTOR_SUGGESTIONS = builder -> { final SuggestionsBuilder nextSuggestion = builder.createOffset(cursor); + final String input = builder.getInput().substring(cursor); for (final Pair suggestion : suggested) { - if (!suggestion.first().startsWith(builder.getInput().substring(cursor))) { + if (!suggestion.first().startsWith(input)) { continue; } if (suggestion.second() != null) { @@ -158,7 +163,7 @@ public static void setSuggestionsWithTooltip(final StringReader reader, final Li nextSuggestion.suggest(suggestion.first()); } } - return nextSuggestion.buildFuture(); + return nextSuggestion; }; } @@ -168,13 +173,13 @@ public static String readUntilEndOrCharacter(final StringReader reader, final ch builder.append(reader.read()); } if (builder.isEmpty()) { - throw UNEXPECTED_END_OF_INPUT.create(); + throw ERROR_UNEXPECTED_END_OF_INPUT.create(); } return builder.toString(); } @Override - public SubLevelSelector parse(final StringReader reader) throws CommandSyntaxException { + public SubLevelTarget parse(final StringReader reader) throws CommandSyntaxException { final ObjectList> allowedSelectors = new ObjectArrayList<>(); if (this.allowStaticLevel) { allowedSelectors.add(Pair.of(STATIC_WORLD, Component.translatable("argument.sable.body.static_world"))); @@ -182,35 +187,40 @@ public SubLevelSelector parse(final StringReader reader) throws CommandSyntaxExc for (final SubLevelSelectorType selector : SubLevelSelectorType.values()) { allowedSelectors.add(Pair.of("@" + selector.getChar(), selector.getTooltip())); } - setSuggestionsWithTooltip(reader, allowedSelectors); + setSelectorSuggestionsWithTooltip(reader, allowedSelectors); if (this.allowStaticLevel && reader.canRead(STATIC_WORLD.length()) && reader.peek() == STATIC_WORLD.charAt(0)) { final String staticWorld = reader.readString(); if (!staticWorld.equals(STATIC_WORLD)) { - throw ERROR_INVALID_SUBLEVEL.create(); + throw ERROR_INVALID_SELECTOR.create(); } - return new SubLevelSelector(null, new ObjectArrayList<>()); + return SubLevelTargetNone.INSTANCE; } if (!reader.canRead()) { - throw ERROR_INVALID_SUBLEVEL.create(); + throw ERROR_INVALID_SELECTOR.create(); } - final char firstChar = reader.read(); + final char firstChar = reader.peek(); - if (!reader.canRead() || firstChar != '@') { - throw ERROR_INVALID_SUBLEVEL.create(); + if (firstChar == '@') { + reader.skip(); + return this.parseSelector(reader); + } else { + return this.parseUUID(reader); } + } + private SubLevelTarget parseSelector(final StringReader reader) throws CommandSyntaxException { if (!reader.canRead()) { - throw ERROR_INVALID_SUBLEVEL.create(); + throw ERROR_INVALID_SELECTOR.create(); } final SubLevelSelectorType selectorType = SubLevelSelectorType.of(reader.read()); if (selectorType == null) { - throw ERROR_INVALID_SUBLEVEL.create(); + throw ERROR_INVALID_SELECTOR.create(); } int maximumResults = Integer.MAX_VALUE; @@ -230,19 +240,41 @@ public SubLevelSelector parse(final StringReader reader) throws CommandSyntaxExc throw ERROR_SINGLE_SUB_LEVEL_REQUIRED.create(); } - return new SubLevelSelector(selectorType, modifiers); + return new SubLevelTargetSelector(selectorType, modifiers); + } + + private SubLevelTarget parseUUID(final StringReader reader) throws CommandSyntaxException { + final String s = reader.readString(); + try { + return new SubLevelTargetUUID(UUID.fromString(s)); + } catch (final IllegalArgumentException e) { + throw ERROR_INVALID_UUID.createWithContext(reader); + } } @Override - public CompletableFuture listSuggestions(final CommandContext pContext, final SuggestionsBuilder builder) { + public CompletableFuture listSuggestions(final CommandContext pContext, SuggestionsBuilder builder) { final StringReader stringreader = new StringReader(builder.getInput()); - stringreader.setCursor(builder.getStart()); - suggestions = NO_SUGGESTIONS; + final int start = builder.getStart(); + stringreader.setCursor(start); + SELECTOR_SUGGESTIONS = NO_SUGGESTIONS; try { this.parse(stringreader); } catch (final CommandSyntaxException ignored) { } - return suggestions.apply(builder); + builder = SELECTOR_SUGGESTIONS.apply(builder); + + if (pContext.getSource() instanceof final SubLevelSuggestionProvider suggestionProvider) { + final SubLevel subLevel = suggestionProvider.getSelectedSubLevel(); + if (subLevel != null) { + final String uuid = subLevel.getUniqueId().toString(); + final String input = builder.getInput().substring(start); + if (uuid.startsWith(input)) { + builder.suggest(uuid); + } + } + } + return builder.buildFuture(); } @Override diff --git a/common/src/main/java/dev/ryanhcode/sable/api/entity/EntitySubLevelUtil.java b/common/src/main/java/dev/ryanhcode/sable/api/entity/EntitySubLevelUtil.java index a5b93131..43a00a75 100644 --- a/common/src/main/java/dev/ryanhcode/sable/api/entity/EntitySubLevelUtil.java +++ b/common/src/main/java/dev/ryanhcode/sable/api/entity/EntitySubLevelUtil.java @@ -12,6 +12,7 @@ import net.minecraft.world.entity.projectile.AbstractHurtingProjectile; import net.minecraft.world.phys.Vec3; import org.jetbrains.annotations.Nullable; +import org.joml.Quaterniond; import org.joml.Quaterniondc; import org.joml.Vector3d; @@ -88,12 +89,54 @@ public static boolean shouldKick(final Entity entity) { return !entity.getType().is(SableTags.RETAIN_IN_SUB_LEVEL); } + /** + * Entities orient to sub-levels tilted up to this angle; steeper, they stay upright. + */ + private static final double MAX_SURFACE_ORIENTATION_ANGLE = Math.toRadians(45.0); + + /** + * Below this tilt the sub-level is treated as flat and vanilla behavior applies. + */ + private static final double MIN_SURFACE_ORIENTATION_ANGLE = Math.toRadians(1.0); + + /** + * The orientation an entity standing on a tilted sub-level should adopt: the tilt (swing) + * component of the sub-level's orientation, with the yaw (twist about world Y) removed so the + * entity keeps its own heading. Returns null on flat or too-steep sub-levels, where the entity + * remains upright. + */ @Nullable public static Quaterniondc getCustomEntityOrientation(final Entity entity, final float partialTicks) { - return null; + if (entity == null) { + return null; + } + + final SubLevel subLevel = Sable.HELPER.getTrackingSubLevel(entity); + if (subLevel == null || subLevel.isRemoved()) { + return null; + } + + final Quaterniond orientation = new Quaterniond(); + subLevel.lastPose().orientation().slerp(subLevel.logicalPose().orientation(), partialTicks, orientation); + + // Swing-twist decomposition about world Y: orientation = swing * twist. The twist is the + // sub-level's heading, which the entity should NOT inherit; the swing is the deck tilt. + final Quaterniond twist = new Quaterniond(0.0, orientation.y, 0.0, orientation.w); + if (twist.lengthSquared() < 1.0e-12) { + return null; // degenerate (sub-level pitched ~180): stay upright + } + twist.normalize(); + final Quaterniond swing = orientation.mul(twist.invert(new Quaterniond()), new Quaterniond()).normalize(); + + final double tilt = 2.0 * Math.acos(Math.min(1.0, Math.abs(swing.w))); + if (tilt < MIN_SURFACE_ORIENTATION_ANGLE || tilt > MAX_SURFACE_ORIENTATION_ANGLE) { + return null; + } + + return swing; } public static boolean hasCustomEntityOrientation(final Entity entity) { - return false; + return getCustomEntityOrientation(entity, 1.0f) != null; } } diff --git a/common/src/main/java/dev/ryanhcode/sable/api/physics/force/QueuedForceGroup.java b/common/src/main/java/dev/ryanhcode/sable/api/physics/force/QueuedForceGroup.java index 3cf9e6b4..525e5b16 100644 --- a/common/src/main/java/dev/ryanhcode/sable/api/physics/force/QueuedForceGroup.java +++ b/common/src/main/java/dev/ryanhcode/sable/api/physics/force/QueuedForceGroup.java @@ -26,6 +26,7 @@ public void applyAndRecordPointForce(final Vector3dc point, final Vector3dc forc this.forceTotal.applyImpulseAtPoint(this.subLevel.getMassTracker(), point, force); this.recordPointForce(point, force); } + public void recordPointForce(final Vector3dc point, final Vector3dc force) { if (!this.subLevel.isTrackingIndividualQueuedForces()) { return; diff --git a/common/src/main/java/dev/ryanhcode/sable/api/sublevel/ServerSubLevelContainer.java b/common/src/main/java/dev/ryanhcode/sable/api/sublevel/ServerSubLevelContainer.java index 10fdc263..b1ac1437 100644 --- a/common/src/main/java/dev/ryanhcode/sable/api/sublevel/ServerSubLevelContainer.java +++ b/common/src/main/java/dev/ryanhcode/sable/api/sublevel/ServerSubLevelContainer.java @@ -273,6 +273,11 @@ public Collection collectForceLoadedSubLevels() { return subLevels; } + @ApiStatus.Internal + public Map>> collectForceLoadTickets() { + return Collections.unmodifiableMap(this.activeTickets); + } + /** * Loads sub-level tickets */ diff --git a/common/src/main/java/dev/ryanhcode/sable/api/sublevel/SubLevelTicketLoadingSystem.java b/common/src/main/java/dev/ryanhcode/sable/api/sublevel/SubLevelTicketLoadingSystem.java index 685c13ae..ffa65ce0 100644 --- a/common/src/main/java/dev/ryanhcode/sable/api/sublevel/SubLevelTicketLoadingSystem.java +++ b/common/src/main/java/dev/ryanhcode/sable/api/sublevel/SubLevelTicketLoadingSystem.java @@ -7,6 +7,8 @@ import it.unimi.dsi.fastutil.objects.ObjectArraySet; import org.jetbrains.annotations.ApiStatus; +import java.util.UUID; + /** * Manages the {@code activeTickets} map in {@link ServerSubLevelContainer} */ @@ -30,17 +32,18 @@ public void onSubLevelAdded(final SubLevel subLevel) { @Override public void onSubLevelRemoved(final SubLevel subLevel, final SubLevelRemovalReason reason) { final ServerSubLevel serverSubLevel = ((ServerSubLevel) subLevel); + final UUID uuid = subLevel.getUniqueId(); if (reason == SubLevelRemovalReason.UNLOADED) { this.container.activeTickets.remove(serverSubLevel); - final SubLevelTicketInfo info = this.container.allTickets.get(serverSubLevel); + final SubLevelTicketInfo info = this.container.allTickets.get(uuid); if (info != null) { info.setPointer(serverSubLevel.getLastSerializationPointer()); } } else if (reason == SubLevelRemovalReason.REMOVED) { - this.container.allTickets.remove(subLevel.getUniqueId()); + this.container.allTickets.remove(uuid); this.container.activeTickets.remove(serverSubLevel); } } diff --git a/common/src/main/java/dev/ryanhcode/sable/api/sublevel/ticket/SubLevelLoadingTicket.java b/common/src/main/java/dev/ryanhcode/sable/api/sublevel/ticket/SubLevelLoadingTicket.java index fc6ef0fe..e2940118 100644 --- a/common/src/main/java/dev/ryanhcode/sable/api/sublevel/ticket/SubLevelLoadingTicket.java +++ b/common/src/main/java/dev/ryanhcode/sable/api/sublevel/ticket/SubLevelLoadingTicket.java @@ -1,41 +1,15 @@ package dev.ryanhcode.sable.api.sublevel.ticket; -import java.util.Objects; import java.util.UUID; -public final class SubLevelLoadingTicket { - private final SubLevelLoadingTicketType type; - private final UUID subLevelId; - private final T key; +public record SubLevelLoadingTicket(SubLevelLoadingTicketType type, UUID subLevelId, T key) { - public SubLevelLoadingTicket(final SubLevelLoadingTicketType type, final UUID subLevelId, final T key) { - this.subLevelId = subLevelId; - this.type = type; - this.key = key; - } - - @Override - public boolean equals(final Object o) { - if (o == null || this.getClass() != o.getClass()) return false; - - final SubLevelLoadingTicket that = (SubLevelLoadingTicket) o; - return Objects.equals(this.type, that.type) && Objects.equals(this.subLevelId, that.subLevelId) && Objects.equals(this.key, that.key); + public String toCompactString() { + return "Ticket[" + this.type.name() + " (" + this.key + ")]"; } public String toString() { final String type = String.valueOf(this.type); return "SubLevelLoadingTicket[" + type + " " + this.subLevelId + " (" + this.key + ")]"; } - - public SubLevelLoadingTicketType getType() { - return this.type; - } - - public T getKey() { - return this.key; - } - - public UUID getSubLevelId() { - return this.subLevelId; - } } diff --git a/common/src/main/java/dev/ryanhcode/sable/command/SableCommand.java b/common/src/main/java/dev/ryanhcode/sable/command/SableCommand.java index 90fc0f01..200335b9 100644 --- a/common/src/main/java/dev/ryanhcode/sable/command/SableCommand.java +++ b/common/src/main/java/dev/ryanhcode/sable/command/SableCommand.java @@ -9,6 +9,7 @@ import dev.ryanhcode.sable.api.command.SubLevelArgumentType; import dev.ryanhcode.sable.api.physics.handle.RigidBodyHandle; import dev.ryanhcode.sable.api.sublevel.ServerSubLevelContainer; +import dev.ryanhcode.sable.api.sublevel.ticket.SubLevelLoadingTicket; import dev.ryanhcode.sable.api.sublevel.ticket.SubLevelLoadingTicketType; import dev.ryanhcode.sable.companion.math.Pose3dc; import dev.ryanhcode.sable.network.packets.tcp.ClientboundEnterGizmoPacket; @@ -30,9 +31,7 @@ import org.joml.Vector3d; import org.joml.Vector3dc; -import java.util.Collection; -import java.util.Formatter; -import java.util.Locale; +import java.util.*; public class SableCommand { @@ -53,15 +52,8 @@ public static void register(final CommandDispatcher dispatch sableBuilder .then(debugBuilder - .then(Commands.literal("udp_test").executes(ctx -> { - final SableUDPServer server = SableUDPServer.getServer(ctx.getSource().getServer()); - - if (server != null) { - server.sendUDPPacket(ctx.getSource().getPlayerOrException(), new SableUDPEchoPacket("Skibidi Toilet"), true); - } - - return 1; - })) + .then(Commands.literal("udp_test") + .executes(SableCommand::executeUDPTestCommand)) ); sableBuilder @@ -74,94 +66,31 @@ public static void register(final CommandDispatcher dispatch .executes(SableCommand::executeSetPhysicsPausedCommand))) .then(Commands.literal("forceload") - .then(Commands.literal("add").then(Commands.argument("sub_level", SubLevelArgumentType.subLevels()).executes(ctx -> { - final CommandSourceStack source = ctx.getSource(); - final ServerSubLevelContainer container = SableCommandHelper.requireSubLevelContainer(source); - final Collection subLevels = SubLevelArgumentType.getSubLevels(ctx, "sub_level"); - - int count = 0; - for (final ServerSubLevel subLevel : subLevels) { - if (container.addForceLoadTicket(subLevel, SubLevelLoadingTicketType.COMMAND_FORCED, Unit.INSTANCE)) { - count++; - } - } - - final int finalCount = count; - source.sendSuccess(() -> Component.translatable("commands.sable.forceload.add.count", finalCount), true); - return count; - }))) - .then(Commands.literal("remove").then(Commands.argument("sub_level", SubLevelArgumentType.subLevels()).executes(ctx -> { - final CommandSourceStack source = ctx.getSource(); - final ServerSubLevelContainer container = SableCommandHelper.requireSubLevelContainer(source); - final Collection subLevels = SubLevelArgumentType.getSubLevels(ctx, "sub_level"); - - int count = 0; - for (final ServerSubLevel subLevel : subLevels) { - if (container.removeForceLoadTicket(subLevel, SubLevelLoadingTicketType.COMMAND_FORCED, Unit.INSTANCE)) { - count++; - } - } - - final int finalCount = count; - source.sendSuccess(() -> Component.translatable("commands.sable.forceload.remove.count", finalCount), true); - return count; - }))) + .then(Commands.literal("add").then(Commands.argument("sub_level", SubLevelArgumentType.subLevels()) + .executes(SableCommand::executeForceloadAddCommand))) + .then(Commands.literal("query") + .executes(SableCommand::executeForceloadQueryCommand)) + .then(Commands.literal("remove").then(Commands.argument("sub_level", SubLevelArgumentType.subLevels()) + .executes(SableCommand::executeForceloadRemoveCommand))) ) - .then(Commands.literal("info").then(Commands.argument("sub_level", SubLevelArgumentType.subLevels()).executes(ctx -> { - final CommandSourceStack source = ctx.getSource(); - final ServerSubLevelContainer container = SableCommandHelper.requireSubLevelContainer(source); - final Collection subLevels = SubLevelArgumentType.getSubLevels(ctx, "sub_level"); - - if (subLevels.isEmpty()) { - throw SableCommandHelper.ERROR_NO_SUB_LEVELS_FOUND.create(); - } - - source.sendSuccess(() -> Component.translatable("commands.sable.info.count", subLevels.size()), false); - for (final ServerSubLevel subLevel : subLevels) { - final Pose3dc pose = subLevel.logicalPose(); - source.sendSuccess(() -> { - final Vector3dc pos = pose.position(); - final MutableComponent component = Component.translatable("commands.sable.info.name", Component.literal(subLevel.getName() != null ? subLevel.getName() : subLevel.getUniqueId().toString())); - final ResourceLocation dimension = subLevel.getLevel().dimension().location(); - final GlobalSavedSubLevelPointer pointer = subLevel.getLastSerializationPointer(); - final Component fileId = Component.translatable("commands.sable.info.name.tooltip", pointer != null ? pointer.toString() : "None yet"); - component.setStyle(Style.EMPTY.withClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, new Formatter().format(Locale.ROOT, "/execute in %s run tp @s %.2f %.2f %.2f", dimension, pos.x(), pos.y(), pos.z()).toString())) - .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, fileId)) - .withColor(ChatFormatting.GRAY)); - return component; - }, false); - source.sendSuccess(() -> { - final Vector3dc pos = pose.position(); - return Component.translatable("commands.sable.info.position", pos.x(), pos.y(), pos.z()); - }, false); - source.sendSuccess(() -> { - final Quaterniondc orientation = pose.orientation(); - return Component.translatable("commands.sable.info.orientation", orientation.x(), orientation.y(), orientation.z(), orientation.w()); - }, false); - source.sendSuccess(() -> { - return Component.translatable("commands.sable.info.mass", subLevel.getMassTracker().getMass()); - }, false); - - final SubLevelPhysicsSystem physicsSystem = container.physicsSystem(); - final RigidBodyHandle handle = physicsSystem.getPhysicsHandle(subLevel); - source.sendSuccess(() -> { - final Vector3dc pos = handle.getLinearVelocity(new Vector3d()); - return Component.translatable("commands.sable.info.linear_velocity", - pos.x(), pos.y(), pos.z()); - }, false); - source.sendSuccess(() -> { - final Vector3dc pos = handle.getAngularVelocity(new Vector3d()); - return Component.translatable("commands.sable.info.angular_velocity", pos.x(), pos.y(), pos.z()); - }, false); - } - return subLevels.size(); - }))); + .then(Commands.literal("info").then(Commands.argument("sub_level", SubLevelArgumentType.subLevels()) + .executes(SableCommand::executeInfoCommand))); dispatcher.register(sableBuilder); } + private static int executeUDPTestCommand(final CommandContext ctx) throws CommandSyntaxException { + final SableUDPServer server = SableUDPServer.getServer(ctx.getSource().getServer()); + + if (server != null) { + server.sendUDPPacket(ctx.getSource().getPlayerOrException(), new SableUDPEchoPacket("Skibidi Toilet"), true); + } + + return 1; + } + private static int executeEnableGizmoCommand(final CommandContext ctx) throws CommandSyntaxException { final CommandSourceStack source = ctx.getSource(); final ServerPlayer player = source.getPlayerOrException(); @@ -188,4 +117,137 @@ private static int executeSetPhysicsPausedCommand(final CommandContext Component.translatable("commands.sable.physics.paused.success", Boolean.toString(pause)), true); return 1; } + + private static int executeForceloadQueryCommand(final CommandContext ctx) throws CommandSyntaxException { + final CommandSourceStack source = ctx.getSource(); + final ServerSubLevelContainer container = SableCommandHelper.requireSubLevelContainer(source); + + final Map>> allTickets = container.collectForceLoadTickets(); + + int subLevelCount = 0; + int ticketCount = 0; + for (final Map.Entry>> entry : allTickets.entrySet()) { + subLevelCount ++; + ticketCount += entry.getValue().size(); + } + + final Component dimension = Component.translationArg(ctx.getSource().getLevel().dimension().location()); + + if (ticketCount == 0) { + source.sendFailure(Component.translatable("commands.sable.forceload.query.none", dimension)); + return ticketCount; + } + + final int finalTicketCount = ticketCount; + final int finalSubLevelCount = subLevelCount; + source.sendSuccess(() -> Component.translatable("commands.sable.forceload.query.count", finalTicketCount, finalSubLevelCount, dimension), true); + + for (final Map.Entry>> entry : allTickets.entrySet()) { + final ServerSubLevel subLevel = entry.getKey(); + + source.sendSuccess(() -> { + final String uuid = subLevel.getUniqueId().toString(); + final MutableComponent component = Component.translatable("commands.sable.forceload.sub_level_name", Component.literal(subLevel.getName() != null ? subLevel.getName() : uuid)); + component.setStyle(Style.EMPTY.withClickEvent(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, uuid)) + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal(uuid))) + .withColor(ChatFormatting.GRAY)); + return component; + }, true); + + final Set> tickets = entry.getValue(); + for (final SubLevelLoadingTicket ticket : tickets) { + source.sendSuccess(() -> Component.translatable("commands.sable.forceload.ticket", ticket.toCompactString()), false); + } + } + + return ticketCount; + } + + private static int executeForceloadAddCommand(final CommandContext ctx) throws CommandSyntaxException { + final CommandSourceStack source = ctx.getSource(); + final ServerSubLevelContainer container = SableCommandHelper.requireSubLevelContainer(source); + final Collection subLevels = SubLevelArgumentType.getSubLevels(ctx, "sub_level"); + + int count = 0; + for (final ServerSubLevel subLevel : subLevels) { + if (container.addForceLoadTicket(subLevel, SubLevelLoadingTicketType.COMMAND_FORCED, Unit.INSTANCE)) { + count++; + } + } + + final int finalCount = count; + source.sendSuccess(() -> Component.translatable("commands.sable.forceload.add.count", finalCount), true); + return count; + } + + private static int executeForceloadRemoveCommand(final CommandContext ctx) throws CommandSyntaxException { + final CommandSourceStack source = ctx.getSource(); + final ServerSubLevelContainer container = SableCommandHelper.requireSubLevelContainer(source); + final Collection subLevels = SubLevelArgumentType.getSubLevels(ctx, "sub_level"); + + int count = 0; + for (final ServerSubLevel subLevel : subLevels) { + if (container.removeForceLoadTicket(subLevel, SubLevelLoadingTicketType.COMMAND_FORCED, Unit.INSTANCE)) { + count++; + } + } + + final int finalCount = count; + source.sendSuccess(() -> Component.translatable("commands.sable.forceload.remove.count", finalCount), true); + return count; + } + + private static int executeInfoCommand(final CommandContext ctx) throws CommandSyntaxException { + final CommandSourceStack source = ctx.getSource(); + final ServerSubLevelContainer container = SableCommandHelper.requireSubLevelContainer(source); + final Collection subLevels = SubLevelArgumentType.getSubLevels(ctx, "sub_level"); + + if (subLevels.isEmpty()) { + throw SableCommandHelper.ERROR_NO_SUB_LEVELS_FOUND.create(); + } + + source.sendSuccess(() -> Component.translatable("commands.sable.info.count", subLevels.size()), false); + for (final ServerSubLevel subLevel : subLevels) { + final Pose3dc pose = subLevel.logicalPose(); + source.sendSuccess(() -> { + final String uuid = subLevel.getUniqueId().toString(); + final MutableComponent component = Component.translatable("commands.sable.info.name", Component.literal(subLevel.getName() != null ? subLevel.getName() : uuid)); + component.setStyle(Style.EMPTY.withClickEvent(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, uuid)) + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal(uuid))) + .withColor(ChatFormatting.GRAY)); + return component; + }, false); + source.sendSuccess(() -> { + final Vector3dc pos = pose.position(); + final GlobalSavedSubLevelPointer pointer = subLevel.getLastSerializationPointer(); + final ResourceLocation dimension = subLevel.getLevel().dimension().location(); + final Component fileId = Component.translatable("commands.sable.info.name.tooltip", pointer != null ? pointer.toString() : "None yet"); + final MutableComponent component = Component.translatable("commands.sable.info.position", pos.x(), pos.y(), pos.z()); + component.setStyle(Style.EMPTY.withClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, new Formatter().format(Locale.ROOT, "/execute in %s run tp @s %.2f %.2f %.2f", dimension, pos.x(), pos.y(), pos.z()).toString())) + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, fileId)) + .withColor(ChatFormatting.GRAY)); + return component; + }, false); + source.sendSuccess(() -> { + final Quaterniondc orientation = pose.orientation(); + return Component.translatable("commands.sable.info.orientation", orientation.x(), orientation.y(), orientation.z(), orientation.w()); + }, false); + source.sendSuccess(() -> { + return Component.translatable("commands.sable.info.mass", subLevel.getMassTracker().getMass()); + }, false); + + final SubLevelPhysicsSystem physicsSystem = container.physicsSystem(); + final RigidBodyHandle handle = physicsSystem.getPhysicsHandle(subLevel); + source.sendSuccess(() -> { + final Vector3dc pos = handle.getLinearVelocity(new Vector3d()); + return Component.translatable("commands.sable.info.linear_velocity", + pos.x(), pos.y(), pos.z()); + }, false); + source.sendSuccess(() -> { + final Vector3dc pos = handle.getAngularVelocity(new Vector3d()); + return Component.translatable("commands.sable.info.angular_velocity", pos.x(), pos.y(), pos.z()); + }, false); + } + return subLevels.size(); + } } diff --git a/common/src/main/java/dev/ryanhcode/sable/command/SableStorageCommands.java b/common/src/main/java/dev/ryanhcode/sable/command/SableStorageCommands.java index 6dcf5591..e73da649 100644 --- a/common/src/main/java/dev/ryanhcode/sable/command/SableStorageCommands.java +++ b/common/src/main/java/dev/ryanhcode/sable/command/SableStorageCommands.java @@ -30,6 +30,49 @@ public class SableStorageCommands { public static void register(final LiteralArgumentBuilder sableBuilder, final CommandBuildContext buildContext) { sableBuilder.then(Commands.literal("storage") + .then(Commands.literal("prune_regions") + .executes(ctx -> { + final ServerLevel level = ctx.getSource().getLevel(); + final ServerSubLevelContainer container = ServerSubLevelContainer.getContainer(level); + final SubLevelHoldingChunkMap holdingChunkMap = container.getHoldingChunkMap(); + final SubLevelStorage storage = holdingChunkMap.getStorage(); + + final File[] regionFiles = storage.getFolder().toFile().listFiles((dir, name) -> name.endsWith(SubLevelRegionFile.FILE_EXTENSION)); + + if (regionFiles != null) { + for (final File regionFile : regionFiles) { + final String fileName = regionFile.getName(); + final String withoutExtension = fileName.substring(0, fileName.length() - SubLevelRegionFile.FILE_EXTENSION.length()); + final String[] parts = withoutExtension.split("\\."); + if (parts.length != 3) continue; + + final int regionX, regionZ; + try { + regionX = Integer.parseInt(parts[1]); + regionZ = Integer.parseInt(parts[2]); + } catch (final NumberFormatException e) { + continue; + } + + for (int localX = 0; localX < SubLevelRegionFile.SIDE_LENGTH; localX++) { + for (int localZ = 0; localZ < SubLevelRegionFile.SIDE_LENGTH; localZ++) { + final ChunkPos chunkPos = new ChunkPos( + regionX * SubLevelRegionFile.SIDE_LENGTH + localX, + regionZ * SubLevelRegionFile.SIDE_LENGTH + localZ + ); + + final SubLevelHoldingChunk holdingChunk = storage.attemptLoadHoldingChunk(chunkPos); + if (holdingChunk == null) continue; + + if (holdingChunk.isEmpty()) { + storage.attemptRemoveHoldingChunk(chunkPos); + } + } + } + } + } + return 1; + })) .then(Commands.literal("find_all_sub_levels") .executes(ctx -> { final ServerLevel level = ctx.getSource().getLevel(); @@ -135,27 +178,30 @@ public static void register(final LiteralArgumentBuilder sab private static void logFoundSubLevel(final SavedSubLevelPointer pointer, final SubLevelData data, final ChunkPos chunkPos, final CommandSourceStack source, final ServerLevel level) { if (data == null) return; + final String uuid = data.uuid().toString(); final String name = data.fullTag().contains("display_name") ? data.fullTag().getString("display_name") - : data.uuid().toString(); + : uuid; final GlobalSavedSubLevelPointer globalPointer = new GlobalSavedSubLevelPointer(chunkPos, pointer.storageIndex(), pointer.subLevelIndex()); final Pose3d pose = data.pose(); source.sendSuccess(() -> { - final Vector3dc pos = pose.position(); final MutableComponent component = Component.translatable("commands.sable.info.name", Component.literal(name)); - final ResourceLocation dimension = level.dimension().location(); - final Component fileId = Component.translatable("commands.sable.info.name.tooltip", globalPointer.toString()); - component.setStyle(Style.EMPTY.withClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, new Formatter().format(Locale.ROOT, "/execute in %s run tp @s %.2f %.2f %.2f", dimension, pos.x(), pos.y(), pos.z()).toString())) - .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, fileId)) + component.setStyle(Style.EMPTY.withClickEvent(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, uuid)) + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal(uuid))) .withColor(ChatFormatting.GRAY)); return component; }, false); source.sendSuccess(() -> { final Vector3dc pos = pose.position(); - return Component.translatable("commands.sable.info.position", pos.x(), pos.y(), pos.z()); + final ResourceLocation dimension = level.dimension().location(); + final Component fileId = Component.translatable("commands.sable.info.name.tooltip", globalPointer.toString()); + final MutableComponent component = Component.translatable("commands.sable.info.position", pos.x(), pos.y(), pos.z()); + component.setStyle(Style.EMPTY.withClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, new Formatter().format(Locale.ROOT, "/execute in %s run tp @s %.2f %.2f %.2f", dimension, pos.x(), pos.y(), pos.z()).toString())) + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, fileId))); + return component; }, false); source.sendSuccess(() -> { diff --git a/common/src/main/java/dev/ryanhcode/sable/command/argument/SubLevelSelectorModifiers.java b/common/src/main/java/dev/ryanhcode/sable/command/argument/SubLevelSelectorModifiers.java index 37ca0fe2..127915cc 100644 --- a/common/src/main/java/dev/ryanhcode/sable/command/argument/SubLevelSelectorModifiers.java +++ b/common/src/main/java/dev/ryanhcode/sable/command/argument/SubLevelSelectorModifiers.java @@ -129,7 +129,7 @@ public static void registerModifiers() { }, SubLevelSelectorModifierType.FilterPriority.FILTER); SubLevelSelectorModifierType.registerType("sort", (reader) -> { - SubLevelArgumentType.setSuggestions(reader, "nearest", "furthest"); + SubLevelArgumentType.setSelectorSuggestions(reader, "nearest", "furthest"); final String filtering = tryReadString(reader, EXPECTED_SORTING_TYPE, "nearest", "furthest"); expectEndOfModifier(reader); return new SubLevelSortModifier(filtering); diff --git a/common/src/main/java/dev/ryanhcode/sable/command/argument/SubLevelSuggestionProvider.java b/common/src/main/java/dev/ryanhcode/sable/command/argument/SubLevelSuggestionProvider.java new file mode 100644 index 00000000..fcc077f6 --- /dev/null +++ b/common/src/main/java/dev/ryanhcode/sable/command/argument/SubLevelSuggestionProvider.java @@ -0,0 +1,10 @@ +package dev.ryanhcode.sable.command.argument; + +import dev.ryanhcode.sable.sublevel.SubLevel; +import org.jetbrains.annotations.Nullable; + +public interface SubLevelSuggestionProvider { + default @Nullable SubLevel getSelectedSubLevel() { + return null; + } +} diff --git a/common/src/main/java/dev/ryanhcode/sable/command/argument/selector/SubLevelTarget.java b/common/src/main/java/dev/ryanhcode/sable/command/argument/selector/SubLevelTarget.java new file mode 100644 index 00000000..e69a96b7 --- /dev/null +++ b/common/src/main/java/dev/ryanhcode/sable/command/argument/selector/SubLevelTarget.java @@ -0,0 +1,11 @@ +package dev.ryanhcode.sable.command.argument.selector; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import dev.ryanhcode.sable.sublevel.ServerSubLevel; +import net.minecraft.commands.CommandSourceStack; + +import java.util.Collection; + +public abstract class SubLevelTarget { + public abstract Collection getSubLevels(final CommandSourceStack source) throws CommandSyntaxException; +} \ No newline at end of file diff --git a/common/src/main/java/dev/ryanhcode/sable/command/argument/selector/SubLevelTargetNone.java b/common/src/main/java/dev/ryanhcode/sable/command/argument/selector/SubLevelTargetNone.java new file mode 100644 index 00000000..34579977 --- /dev/null +++ b/common/src/main/java/dev/ryanhcode/sable/command/argument/selector/SubLevelTargetNone.java @@ -0,0 +1,17 @@ +package dev.ryanhcode.sable.command.argument.selector; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import dev.ryanhcode.sable.sublevel.ServerSubLevel; +import net.minecraft.commands.CommandSourceStack; + +import java.util.Collection; +import java.util.List; + +public class SubLevelTargetNone extends SubLevelTarget { + public static SubLevelTargetNone INSTANCE = new SubLevelTargetNone(); + private SubLevelTargetNone() {} + @Override + public Collection getSubLevels(CommandSourceStack source) throws CommandSyntaxException { + return List.of(); + } +} diff --git a/common/src/main/java/dev/ryanhcode/sable/command/argument/SubLevelSelector.java b/common/src/main/java/dev/ryanhcode/sable/command/argument/selector/SubLevelTargetSelector.java similarity index 92% rename from common/src/main/java/dev/ryanhcode/sable/command/argument/SubLevelSelector.java rename to common/src/main/java/dev/ryanhcode/sable/command/argument/selector/SubLevelTargetSelector.java index c74921c2..837d41ec 100644 --- a/common/src/main/java/dev/ryanhcode/sable/command/argument/SubLevelSelector.java +++ b/common/src/main/java/dev/ryanhcode/sable/command/argument/selector/SubLevelTargetSelector.java @@ -1,12 +1,12 @@ -package dev.ryanhcode.sable.command.argument; +package dev.ryanhcode.sable.command.argument.selector; import com.mojang.brigadier.exceptions.CommandSyntaxException; import dev.ryanhcode.sable.ActiveSableCompanion; import dev.ryanhcode.sable.Sable; -import dev.ryanhcode.sable.api.SubLevelHelper; import dev.ryanhcode.sable.api.command.SableCommandHelper; -import dev.ryanhcode.sable.api.entity.EntitySubLevelUtil; import dev.ryanhcode.sable.api.sublevel.ServerSubLevelContainer; +import dev.ryanhcode.sable.command.argument.SubLevelSelectorModifierType; +import dev.ryanhcode.sable.command.argument.SubLevelSelectorType; import dev.ryanhcode.sable.sublevel.ServerSubLevel; import it.unimi.dsi.fastutil.Pair; import it.unimi.dsi.fastutil.objects.ObjectArrayList; @@ -19,12 +19,12 @@ import java.util.*; -public class SubLevelSelector { +public class SubLevelTargetSelector extends SubLevelTarget { private final SubLevelSelectorType type; private final List> modifiers; - public SubLevelSelector(final SubLevelSelectorType type, final List> modifiers) { + public SubLevelTargetSelector(final SubLevelSelectorType type, final List> modifiers) { this.type = type; this.modifiers = modifiers; } @@ -33,11 +33,8 @@ public SubLevelSelectorType getSelectorType() { return this.type; } + @Override public Collection getSubLevels(final CommandSourceStack source) throws CommandSyntaxException { - if (this.type == null) { - return List.of(); - } - final ServerLevel level = source.getLevel(); final ServerSubLevelContainer container = SableCommandHelper.requireSubLevelContainer(source); @@ -135,5 +132,4 @@ public Collection getSubLevels(final CommandSourceStack source) return modifiedSubLevels; } - -} \ No newline at end of file +} diff --git a/common/src/main/java/dev/ryanhcode/sable/command/argument/selector/SubLevelTargetUUID.java b/common/src/main/java/dev/ryanhcode/sable/command/argument/selector/SubLevelTargetUUID.java new file mode 100644 index 00000000..8361081a --- /dev/null +++ b/common/src/main/java/dev/ryanhcode/sable/command/argument/selector/SubLevelTargetUUID.java @@ -0,0 +1,30 @@ +package dev.ryanhcode.sable.command.argument.selector; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import dev.ryanhcode.sable.api.command.SubLevelArgumentType; +import dev.ryanhcode.sable.api.sublevel.ServerSubLevelContainer; +import dev.ryanhcode.sable.sublevel.ServerSubLevel; +import net.minecraft.commands.CommandSourceStack; + +import java.util.Collection; +import java.util.List; +import java.util.UUID; + +public class SubLevelTargetUUID extends SubLevelTarget { + private final UUID target; + + public SubLevelTargetUUID(final UUID target) { + this.target = target; + } + + @Override + public Collection getSubLevels(final CommandSourceStack source) throws CommandSyntaxException { + final ServerSubLevel subLevel = (ServerSubLevel) ServerSubLevelContainer.getContainer(source.getLevel()).getSubLevel(this.target); + + if (subLevel == null) { + throw SubLevelArgumentType.ERROR_CANNOT_FIND_SUB_LEVEL.create(); + } + + return List.of(subLevel); + } +} diff --git a/common/src/main/java/dev/ryanhcode/sable/mixin/camera/camera_rotation/EntityMixin.java b/common/src/main/java/dev/ryanhcode/sable/mixin/camera/camera_rotation/EntityMixin.java index 84ebbf49..4b976799 100644 --- a/common/src/main/java/dev/ryanhcode/sable/mixin/camera/camera_rotation/EntityMixin.java +++ b/common/src/main/java/dev/ryanhcode/sable/mixin/camera/camera_rotation/EntityMixin.java @@ -1,5 +1,6 @@ package dev.ryanhcode.sable.mixin.camera.camera_rotation; +import com.llamalad7.mixinextras.injector.ModifyReturnValue; import dev.ryanhcode.sable.companion.math.JOMLConversion; import dev.ryanhcode.sable.companion.math.Pose3dc; import dev.ryanhcode.sable.mixinhelpers.camera.camera_rotation.EntitySubLevelRotationHelper; @@ -12,8 +13,6 @@ 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; import java.util.function.Function; @@ -22,8 +21,8 @@ public abstract class EntityMixin { @Shadow private Level level; - @Inject(method = "calculateViewVector", at = @At("RETURN"), cancellable = true) - public void sable$calculateViewVector(final float f, final float g, final CallbackInfoReturnable cir) { + @ModifyReturnValue(method = "calculateViewVector", at = @At("RETURN")) + public Vec3 sable$calculateViewVector(final Vec3 original) { final Function provider; if (this.level instanceof final LevelPoseProviderExtension levelPoseProvider) { @@ -35,9 +34,10 @@ public abstract class EntityMixin { final Quaterniond orientation = EntitySubLevelRotationHelper.getEntityOrientation((Entity) (Object) this, provider, 0.0f, EntitySubLevelRotationHelper.Type.CAMERA); if (orientation != null) { - final Vec3 viewVector = cir.getReturnValue(); - cir.setReturnValue(JOMLConversion.toMojang(orientation.transform(JOMLConversion.toJOML(viewVector)))); + return JOMLConversion.toMojang(orientation.transform(JOMLConversion.toJOML(original))); } + + return original; } } diff --git a/common/src/main/java/dev/ryanhcode/sable/mixin/command/ClientSuggestionProviderMixin.java b/common/src/main/java/dev/ryanhcode/sable/mixin/command/ClientSuggestionProviderMixin.java new file mode 100644 index 00000000..4ab817eb --- /dev/null +++ b/common/src/main/java/dev/ryanhcode/sable/mixin/command/ClientSuggestionProviderMixin.java @@ -0,0 +1,26 @@ +package dev.ryanhcode.sable.mixin.command; + +import dev.ryanhcode.sable.Sable; +import dev.ryanhcode.sable.command.argument.SubLevelSuggestionProvider; +import dev.ryanhcode.sable.sublevel.SubLevel; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientSuggestionProvider; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(ClientSuggestionProvider.class) +public class ClientSuggestionProviderMixin implements SubLevelSuggestionProvider { + @Shadow + @Final + private Minecraft minecraft; + + @Override + public @Nullable SubLevel getSelectedSubLevel() { + if (this.minecraft.hitResult == null) { + return null; + } + return Sable.HELPER.getContainingClient(this.minecraft.hitResult.getLocation()); + } +} diff --git a/common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_aabb_lookup/ClientLevelMixin.java b/common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_aabb_lookup/ClientLevelMixin.java index 06629629..9799884f 100644 --- a/common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_aabb_lookup/ClientLevelMixin.java +++ b/common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_aabb_lookup/ClientLevelMixin.java @@ -1,15 +1,13 @@ package dev.ryanhcode.sable.mixin.entity.entity_aabb_lookup; +import com.llamalad7.mixinextras.injector.ModifyReturnValue; import dev.ryanhcode.sable.util.SubLevelInclusiveLevelEntityGetter; import net.minecraft.client.multiplayer.ClientLevel; -import net.minecraft.server.level.ServerLevel; import net.minecraft.world.entity.Entity; import net.minecraft.world.level.Level; import net.minecraft.world.level.entity.LevelEntityGetter; 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; /** * Wraps the client and server level {@link net.minecraft.world.level.entity.LevelEntityGetterAdapter} in a {@link SubLevelInclusiveLevelEntityGetter} @@ -17,8 +15,8 @@ @Mixin(ClientLevel.class) public class ClientLevelMixin { - @Inject(method = "getEntities()Lnet/minecraft/world/level/entity/LevelEntityGetter;", at = @At("RETURN"), cancellable = true) - private void sable$postGetEntities(final CallbackInfoReturnable> cir) { - cir.setReturnValue(new SubLevelInclusiveLevelEntityGetter<>((Level) (Object) this, cir.getReturnValue())); + @ModifyReturnValue(method = "getEntities()Lnet/minecraft/world/level/entity/LevelEntityGetter;", at = @At("RETURN")) + private LevelEntityGetter sable$postGetEntities(final LevelEntityGetter original) { + return new SubLevelInclusiveLevelEntityGetter<>((Level) (Object) this, original); } } diff --git a/common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_aabb_lookup/ServerLevelMixin.java b/common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_aabb_lookup/ServerLevelMixin.java index c33eec99..5676732a 100644 --- a/common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_aabb_lookup/ServerLevelMixin.java +++ b/common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_aabb_lookup/ServerLevelMixin.java @@ -1,16 +1,13 @@ package dev.ryanhcode.sable.mixin.entity.entity_aabb_lookup; +import com.llamalad7.mixinextras.injector.ModifyReturnValue; import dev.ryanhcode.sable.util.SubLevelInclusiveLevelEntityGetter; -import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.entity.Entity; import net.minecraft.world.level.Level; import net.minecraft.world.level.entity.LevelEntityGetter; import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Pseudo; import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; /** * Wraps the client and server level {@link net.minecraft.world.level.entity.LevelEntityGetterAdapter} in a {@link SubLevelInclusiveLevelEntityGetter} @@ -18,8 +15,8 @@ @Mixin(ServerLevel.class) public class ServerLevelMixin { - @Inject(method = "getEntities()Lnet/minecraft/world/level/entity/LevelEntityGetter;", at = @At("RETURN"), cancellable = true) - private void sable$postGetEntities(final CallbackInfoReturnable> cir) { - cir.setReturnValue(new SubLevelInclusiveLevelEntityGetter<>((Level) (Object) this, cir.getReturnValue())); + @ModifyReturnValue(method = "getEntities()Lnet/minecraft/world/level/entity/LevelEntityGetter;", at = @At("RETURN")) + private LevelEntityGetter sable$postGetEntities(final LevelEntityGetter original) { + return new SubLevelInclusiveLevelEntityGetter<>((Level) (Object) this, original); } } diff --git a/common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_kicking/BlockMixin.java b/common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_kicking/BlockMixin.java index 3e8425a3..fc2b5aff 100644 --- a/common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_kicking/BlockMixin.java +++ b/common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_kicking/BlockMixin.java @@ -1,7 +1,7 @@ package dev.ryanhcode.sable.mixin.entity.entity_kicking; +import com.llamalad7.mixinextras.sugar.Local; import dev.ryanhcode.sable.Sable; -import dev.ryanhcode.sable.api.SubLevelHelper; import dev.ryanhcode.sable.sublevel.SubLevel; import net.minecraft.core.BlockPos; import net.minecraft.world.entity.item.ItemEntity; @@ -14,7 +14,6 @@ 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; import java.util.function.Supplier; @@ -25,8 +24,8 @@ public abstract class BlockMixin { private static void popResource(final Level arg, final Supplier supplier, final ItemStack arg2) { } - @Inject(method = "popResource(Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/item/ItemStack;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/Block;popResource(Lnet/minecraft/world/level/Level;Ljava/util/function/Supplier;Lnet/minecraft/world/item/ItemStack;)V", shift = At.Shift.BEFORE), locals = LocalCapture.CAPTURE_FAILHARD, cancellable = true) - private static void sable$popResourceFromFace(final Level level, final BlockPos blockPos, final ItemStack itemStack, final CallbackInfo ci, final double yOffset, final double x, final double y, final double z) { + @Inject(method = "popResource(Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/item/ItemStack;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/Block;popResource(Lnet/minecraft/world/level/Level;Ljava/util/function/Supplier;Lnet/minecraft/world/item/ItemStack;)V", shift = At.Shift.BEFORE), cancellable = true) + private static void sable$popResourceFromFace(final Level level, final BlockPos blockPos, final ItemStack itemStack, final CallbackInfo ci, @Local(ordinal = 1) final double x, @Local(ordinal = 2) final double y, @Local(ordinal = 3) final double z) { final SubLevel subLevel = Sable.HELPER.getContaining(level, blockPos); if (subLevel != null) { diff --git a/common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_pathfinding/PathMixin.java b/common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_pathfinding/PathMixin.java index b1707bee..52baebcb 100644 --- a/common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_pathfinding/PathMixin.java +++ b/common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_pathfinding/PathMixin.java @@ -1,7 +1,7 @@ package dev.ryanhcode.sable.mixin.entity.entity_pathfinding; +import com.llamalad7.mixinextras.injector.ModifyReturnValue; import dev.ryanhcode.sable.Sable; -import dev.ryanhcode.sable.api.SubLevelHelper; import dev.ryanhcode.sable.mixinterface.entity.pathfinding.PathExtension; import dev.ryanhcode.sable.sublevel.SubLevel; import net.minecraft.core.BlockPos; @@ -12,8 +12,6 @@ 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.CallbackInfoReturnable; @Mixin(Path.class) public class PathMixin implements PathExtension { @@ -24,48 +22,42 @@ public class PathMixin implements PathExtension { @Unique private boolean sable$project; - @Inject(method = "getNextEntityPos", at = @At("RETURN"), cancellable = true) - private void sable$getNextEntityPos(final Entity entity, final CallbackInfoReturnable cir) { + @ModifyReturnValue(method = "getNextEntityPos", at = @At("RETURN")) + private Vec3 sable$getNextEntityPos(final Vec3 original, final Entity entity) { if (!this.sable$project) { - return; + return original; } - cir.setReturnValue(Sable.HELPER.projectOutOfSubLevel(entity.level(), cir.getReturnValue())); + return Sable.HELPER.projectOutOfSubLevel(entity.level(), original); } - @Inject(method = "getNextNodePos", at = @At("RETURN"), cancellable = true) - private void sable$getNextNodePos(final CallbackInfoReturnable cir) { + @ModifyReturnValue(method = "getNextNodePos", at = @At("RETURN")) + private BlockPos sable$getNextNodePos(final BlockPos original) { if (!this.sable$project) { - return; + return original; } - final BlockPos blockPos = cir.getReturnValue(); - - - final SubLevel subLevel = Sable.HELPER.getContaining(this.sable$level, blockPos); + final SubLevel subLevel = Sable.HELPER.getContaining(this.sable$level, original); if (subLevel == null) { - return; + return original; } - final BlockPos global = BlockPos.containing(subLevel.logicalPose().transformPosition(blockPos.getCenter())); - cir.setReturnValue(global); + final BlockPos global = BlockPos.containing(subLevel.logicalPose().transformPosition(original.getCenter())); + return global; } - @Inject(method = "getNodePos", at = @At("RETURN"), cancellable = true) - private void sable$getNodePos(final int i, final CallbackInfoReturnable cir) { + @ModifyReturnValue(method = "getNodePos", at = @At("RETURN")) + private BlockPos sable$getNodePos(final BlockPos original) { if (!this.sable$project) { - return; + return original; } - final BlockPos blockPos = cir.getReturnValue(); - - final SubLevel subLevel = Sable.HELPER.getContaining(this.sable$level, blockPos); + final SubLevel subLevel = Sable.HELPER.getContaining(this.sable$level, original); if (subLevel == null) { - return; + return original; } - final BlockPos global = BlockPos.containing(subLevel.logicalPose().transformPosition(blockPos.getCenter())); - cir.setReturnValue(global); + return BlockPos.containing(subLevel.logicalPose().transformPosition(original.getCenter())); } @Override diff --git a/common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_rendering/EntityRendererMixin.java b/common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_rendering/EntityRendererMixin.java index c0fa4744..e556eba1 100644 --- a/common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_rendering/EntityRendererMixin.java +++ b/common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_rendering/EntityRendererMixin.java @@ -39,6 +39,18 @@ public abstract class EntityRendererMixin { public final int getPackedLightCoords(final int original, final Entity arg, final float f) { final Vec3 lightProbeOffset = arg.getLightProbePosition(f).subtract(arg.getEyePosition(f)); final Vector3d lightProbePosition = JOMLConversion.toJOML(Sable.HELPER.getEyePositionInterpolated(arg, f)).add(lightProbeOffset.x, lightProbeOffset.y, lightProbeOffset.z); + + // For surface-oriented entities, probe from the oriented BODY CENTER instead of the + // oriented eye: on tilted decks the tilted eye lever can lean the probe inside adjacent + // geometry, sampling zero light and rendering the entity black. The body center is inside + // the entity's own volume, hence always in open space. + final org.joml.Quaterniondc surfaceOrientation = + dev.ryanhcode.sable.api.entity.EntitySubLevelUtil.getCustomEntityOrientation(arg, f); + if (surfaceOrientation != null) { + final double centerLever = arg.getEyeHeight() - arg.getBbHeight() / 2.0; + final Vector3d lever = surfaceOrientation.transform(new Vector3d(0.0, centerLever, 0.0)); + lightProbePosition.sub(lever); + } final BlockPos blockpos = BlockPos.containing(lightProbePosition.x, lightProbePosition.y, lightProbePosition.z); return LightTexture.pack(sable$getSubLevelAccountedBlockLight(original, arg.level(), LightLayer.BLOCK, blockpos, lightProbePosition), sable$getSubLevelAccountedSkyLight(original, arg.level(), LightLayer.SKY, blockpos, lightProbePosition)); @@ -81,9 +93,14 @@ public final int getPackedLightCoords(final int original, final Entity arg, fina if (isAboveGround) { if (lightLayer == LightLayer.BLOCK) { baseBrightness = Math.max(baseBrightness, level.getBrightness(lightLayer, localPosition)); - } else if (lightLayer == LightLayer.SKY) { - final int brightness = clientSubLevel.scaleSkyLight(level.getBrightness(lightLayer, localPosition)); - baseBrightness = Math.min(baseBrightness, brightness); + } else if (lightLayer == LightLayer.SKY && level.getBlockState(localPosition).isAir()) { + // Only darken from a sub-level whose plot the entity's probe actually lands in + // the AIR of (i.e. genuinely standing in that contraption's interior, under a + // roof the heightmap scan found). When a NEIGHBORING contraption's world bounds + // overlap the entity, the inverse transform drops the probe inside that + // sub-level's SOLID geometry — a position the entity can't really occupy — and + // its sky=0 would otherwise min() the entity to pitch black. + baseBrightness = Math.min(baseBrightness, clientSubLevel.scaleSkyLight(level.getBrightness(lightLayer, localPosition))); } } } diff --git a/common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_rotations_and_riding/LivingEntityMixin.java b/common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_rotations_and_riding/LivingEntityMixin.java index 0a2ef25c..359eaa09 100644 --- a/common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_rotations_and_riding/LivingEntityMixin.java +++ b/common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_rotations_and_riding/LivingEntityMixin.java @@ -33,6 +33,36 @@ public LivingEntityMixin(final EntityType entityType, final Level level) { super(entityType, level); } + @WrapOperation(method = "travel", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/LivingEntity;getGravity()D")) + private double sable$noGravityOnTiltedGround(final LivingEntity instance, final Operation original) { + final double gravity = original.call(instance); + + // Grounded on a tilted surface, world-frame gravity leaves a tangential remainder every + // tick (~g*sin(tilt)) that reads as a permanent downhill pull — but simply not applying + // gravity breaks contact: the constant press into the floor is what keeps ground detection + // engaged (without it the entity's motion decays to zero, contact is lost, and it enters a + // fall-catch-slide cycle). Instead, aim the press along the surface normal: contact stays + // engaged every tick and the ground resolution absorbs the press exactly, with no + // tangential slide. Rising jump ticks keep plain world gravity. + final Quaterniondc orientation; + if (instance.onGround() + && (orientation = EntitySubLevelUtil.getCustomEntityOrientation(instance, 1.0f)) != null) { + final Vector3d up = orientation.transform(OrientedBoundingBox3d.UP, new Vector3d()); + final Vec3 movement = instance.getDeltaMovement(); + + // Distinguish standing/walking from a rising jump by the velocity component along the + // SURFACE normal, not world Y: walking uphill has positive world-Y but stays in the + // surface plane, and treating it as a jump starves the contact press for a tick, + // letting world gravity stutter in. + if (up.dot(movement.x, movement.y, movement.z) <= 0.01) { + instance.setDeltaMovement(movement.subtract(up.x * gravity, up.y * gravity, up.z * gravity)); + return 0.0; + } + } + + return gravity; + } + @Inject(method = "jumpFromGround", at = @At("HEAD"), cancellable = true) public void sable$jumpFromGround(final CallbackInfo ci) { final Quaterniondc orientation = EntitySubLevelUtil.getCustomEntityOrientation(this, 1.0f); diff --git a/common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_rotations_and_riding/PlayerMixin.java b/common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_rotations_and_riding/PlayerMixin.java index e08fc545..259c3615 100644 --- a/common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_rotations_and_riding/PlayerMixin.java +++ b/common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_rotations_and_riding/PlayerMixin.java @@ -11,6 +11,7 @@ import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.Pose; import net.minecraft.world.entity.player.Player; import net.minecraft.world.level.Level; import net.minecraft.world.phys.AABB; @@ -22,6 +23,7 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; /** * Fixes the bounding box used for touching nearby entities when riding an entity mounted to a sub-level @@ -33,6 +35,18 @@ protected PlayerMixin(final EntityType entityType, final super(entityType, level); } + @Inject(method = "canPlayerFitWithinBlocksAndEntitiesWhen", at = @At("HEAD"), cancellable = true) + private void sable$fitWhenSurfaceOriented(final Pose pose, final CallbackInfoReturnable cir) { + // The pose system tests the upright, world-aligned vanilla box; on a tilted sub-level the + // player's real collision box is tilted with the surface and clears geometry the upright + // box cannot, so this check spuriously forces the crawling/swimming pose. Physical + // obstruction is still enforced by the oriented collision — skip the pose demotion while + // surface-oriented. + if (EntitySubLevelUtil.getCustomEntityOrientation(this, 1.0f) != null) { + cir.setReturnValue(true); + } + } + @Inject(method = "travel", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/player/Player;getDeltaMovement()Lnet/minecraft/world/phys/Vec3;", ordinal = 1)) private void sable$storeUpDeltaMovement(final Vec3 vec3, final CallbackInfo ci, diff --git a/common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_rotations_and_riding/ProjectileUtilMixin.java b/common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_rotations_and_riding/ProjectileUtilMixin.java new file mode 100644 index 00000000..7cdca5ce --- /dev/null +++ b/common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_rotations_and_riding/ProjectileUtilMixin.java @@ -0,0 +1,57 @@ +package dev.ryanhcode.sable.mixin.entity.entity_rotations_and_riding; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import dev.ryanhcode.sable.api.entity.EntitySubLevelUtil; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.projectile.ProjectileUtil; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import org.joml.Quaterniondc; +import org.joml.Vector3d; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +import java.util.Optional; + +/** + * Makes attack/projectile picking respect surface-oriented entities. + *

+ * Entity picking clips the look ray against the target's world-aligned AABB, but a + * surface-oriented entity renders tilted — aiming at the visible model can miss the box. AABBs + * cannot rotate, so instead the ray is counter-rotated into the entity's frame around its feet + * pivot (matching the render pivot), clipped against the plain box, and the hit transformed back: + * geometrically identical to picking an oriented box. + */ +@Mixin(ProjectileUtil.class) +public class ProjectileUtilMixin { + + @WrapOperation( + method = { + "getEntityHitResult(Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/phys/Vec3;Lnet/minecraft/world/phys/Vec3;Lnet/minecraft/world/phys/AABB;Ljava/util/function/Predicate;D)Lnet/minecraft/world/phys/EntityHitResult;", + "getEntityHitResult(Lnet/minecraft/world/level/Level;Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/phys/Vec3;Lnet/minecraft/world/phys/Vec3;Lnet/minecraft/world/phys/AABB;Ljava/util/function/Predicate;F)Lnet/minecraft/world/phys/EntityHitResult;" + }, + at = @At(value = "INVOKE", target = "Lnet/minecraft/world/phys/AABB;clip(Lnet/minecraft/world/phys/Vec3;Lnet/minecraft/world/phys/Vec3;)Ljava/util/Optional;")) + private static Optional sable$orientedClip(final AABB box, final Vec3 from, final Vec3 to, + final Operation> original, + @Local(ordinal = 1) final Entity target) { + final Quaterniondc orientation = target == null ? null + : EntitySubLevelUtil.getCustomEntityOrientation(target, 1.0f); + if (orientation == null) { + return original.call(box, from, to); + } + + final Vec3 pivot = target.position(); + final Vector3d localFrom = orientation.transformInverse(new Vector3d(from.x - pivot.x, from.y - pivot.y, from.z - pivot.z)); + final Vector3d localTo = orientation.transformInverse(new Vector3d(to.x - pivot.x, to.y - pivot.y, to.z - pivot.z)); + + return original.call(box, + new Vec3(localFrom.x + pivot.x, localFrom.y + pivot.y, localFrom.z + pivot.z), + new Vec3(localTo.x + pivot.x, localTo.y + pivot.y, localTo.z + pivot.z)) + .map(hit -> { + final Vector3d world = orientation.transform(new Vector3d(hit.x - pivot.x, hit.y - pivot.y, hit.z - pivot.z)); + return new Vec3(world.x + pivot.x, world.y + pivot.y, world.z + pivot.z); + }); + } +} diff --git a/common/src/main/java/dev/ryanhcode/sable/mixin/options/OptionsScreenMixin.java b/common/src/main/java/dev/ryanhcode/sable/mixin/options/OptionsScreenMixin.java index c5d5d31b..531ee48b 100644 --- a/common/src/main/java/dev/ryanhcode/sable/mixin/options/OptionsScreenMixin.java +++ b/common/src/main/java/dev/ryanhcode/sable/mixin/options/OptionsScreenMixin.java @@ -1,5 +1,6 @@ package dev.ryanhcode.sable.mixin.options; +import com.llamalad7.mixinextras.injector.ModifyReturnValue; import dev.ryanhcode.sable.config.SubLevelSettingsScreen; import net.minecraft.client.Options; import net.minecraft.client.gui.components.Button; @@ -12,8 +13,6 @@ 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; /** * Adds a button to access the sable menu on integrated servers to the {@link OptionsScreen} @@ -27,10 +26,10 @@ protected OptionsScreenMixin(final Component component) { super(component); } - @Inject(method = "createOnlineButton", at = @At("RETURN"), cancellable = true) - public void sable$createSableButton(final CallbackInfoReturnable cir) { + @ModifyReturnValue(method = "createOnlineButton", at = @At("RETURN")) + public LayoutElement sable$createSableButton(LayoutElement original) { if (this.minecraft.level == null || !this.minecraft.hasSingleplayerServer()) { - return; + return original; } final LinearLayout layout = LinearLayout.vertical(); @@ -39,10 +38,10 @@ protected OptionsScreenMixin(final Component component) { this.minecraft.setScreen(new SubLevelSettingsScreen(this, this.options, SubLevelSettingsScreen.TITLE)); }).pos(0, 30).size(150, 20).build(); - layout.addChild(cir.getReturnValue()); + layout.addChild(original); layout.spacing(5); layout.addChild(sableButton); - cir.setReturnValue(layout); + return layout; } diff --git a/common/src/main/java/dev/ryanhcode/sable/mixin/plot/ChunkMapMixin.java b/common/src/main/java/dev/ryanhcode/sable/mixin/plot/ChunkMapMixin.java index f02732b2..eba744a3 100644 --- a/common/src/main/java/dev/ryanhcode/sable/mixin/plot/ChunkMapMixin.java +++ b/common/src/main/java/dev/ryanhcode/sable/mixin/plot/ChunkMapMixin.java @@ -1,5 +1,6 @@ package dev.ryanhcode.sable.mixin.plot; +import com.llamalad7.mixinextras.injector.ModifyReturnValue; import dev.ryanhcode.sable.api.sublevel.SubLevelContainer; import dev.ryanhcode.sable.sublevel.ServerSubLevel; import dev.ryanhcode.sable.sublevel.plot.LevelPlot; @@ -33,6 +34,7 @@ public class ChunkMapMixin { @Inject(method = "getPlayers", at = @At("HEAD"), cancellable = true) private void sable$getPlayers(final ChunkPos chunkPos, final boolean bl, final CallbackInfoReturnable> cir) { final SubLevelContainer container = SubLevelContainer.getContainer(this.level); + assert container != null; if (container.inBounds(chunkPos)) { final List players = container.getPlayersTracking(chunkPos); @@ -56,15 +58,18 @@ public class ChunkMapMixin { return !updatingChunkMap.values().stream().anyMatch(chunkHolder -> !(chunkHolder instanceof PlotChunkHolder)); } - @Inject(method = "isChunkTracked", at = @At(value = "HEAD"), cancellable = true) - private void sable$isChunkTracked(final ServerPlayer serverPlayer, final int i, final int j, final CallbackInfoReturnable cir) { + @ModifyReturnValue(method = "isChunkTracked", at = @At(value = "RETURN")) + private boolean sable$isChunkTracked(boolean original, final ServerPlayer serverPlayer, final int x, final int z) { final SubLevelContainer container = SubLevelContainer.getContainer(this.level); + assert container != null; - final LevelPlot plot = container.getPlot(new ChunkPos(i, j)); + final LevelPlot plot = container.getPlot(new ChunkPos(x, z)); if (plot != null) { final ServerSubLevel subLevel = (ServerSubLevel) plot.getSubLevel(); - cir.setReturnValue(subLevel.getTrackingPlayers().contains(serverPlayer.getGameProfile().getId())); + return subLevel.getTrackingPlayers().contains(serverPlayer.getGameProfile().getId()); } + + return original; } @Inject(method = "anyPlayerCloseEnoughForSpawning", at = @At("HEAD"), cancellable = true) diff --git a/common/src/main/java/dev/ryanhcode/sable/mixin/plot/ServerChunkCacheMixin.java b/common/src/main/java/dev/ryanhcode/sable/mixin/plot/ServerChunkCacheMixin.java index 431aa273..3d731abc 100644 --- a/common/src/main/java/dev/ryanhcode/sable/mixin/plot/ServerChunkCacheMixin.java +++ b/common/src/main/java/dev/ryanhcode/sable/mixin/plot/ServerChunkCacheMixin.java @@ -1,5 +1,6 @@ package dev.ryanhcode.sable.mixin.plot; +import com.llamalad7.mixinextras.injector.ModifyReturnValue; import com.mojang.datafixers.DataFixer; import dev.ryanhcode.sable.api.sublevel.SubLevelContainer; import dev.ryanhcode.sable.sublevel.plot.PlotChunkHolder; @@ -84,14 +85,14 @@ private void getChunkFutureMainThread(final int x, final int z, final ChunkStatu } } - @Inject(method = "hasChunk", at = @At("HEAD"), cancellable = true) - private void hasChunk(final int x, final int z, final CallbackInfoReturnable cir) { + @ModifyReturnValue(method = "hasChunk", at = @At("RETURN")) + private boolean hasChunk(final boolean original, final int x, final int z) { final SubLevelContainer container = this.sable$getPlotContainer(); if (container.inBounds(x, z)) { - final ChunkAccess chunk = container.getChunk(new ChunkPos(x, z)); - - cir.setReturnValue(chunk != null); + return container.getChunk(new ChunkPos(x, z)) != null; } + + return original; } @@ -105,15 +106,15 @@ private void getChunkForLighting(final int x, final int z, final CallbackInfoRet } } - @Inject(method = "isPositionTicking", at = @At("HEAD"), cancellable = true) - private void isPositionTicking(final long pos, final CallbackInfoReturnable cir) { + @ModifyReturnValue(method = "isPositionTicking", at = @At("RETURN")) + private boolean isPositionTicking(final boolean original, final long chunkPos) { final SubLevelContainer container = this.sable$getPlotContainer(); - if (container.inBounds(ChunkPos.getX(pos), ChunkPos.getZ(pos))) { - final ChunkPos chunkPos = new ChunkPos(pos); - final LevelChunk chunk = container.getChunk(chunkPos); - - cir.setReturnValue(chunk != null); + if (container.inBounds(ChunkPos.getX(chunkPos), ChunkPos.getZ(chunkPos))) { + final LevelChunk chunk = container.getChunk(new ChunkPos(chunkPos)); + return chunk != null; } + + return original; } @Inject(method = "getFullChunk", at = @At("HEAD"), cancellable = true) diff --git a/common/src/main/java/dev/ryanhcode/sable/mixin/plot/ServerLevelMixin.java b/common/src/main/java/dev/ryanhcode/sable/mixin/plot/ServerLevelMixin.java index 66917b92..faa1b3ab 100644 --- a/common/src/main/java/dev/ryanhcode/sable/mixin/plot/ServerLevelMixin.java +++ b/common/src/main/java/dev/ryanhcode/sable/mixin/plot/ServerLevelMixin.java @@ -1,5 +1,6 @@ package dev.ryanhcode.sable.mixin.plot; +import com.llamalad7.mixinextras.injector.ModifyReturnValue; import dev.ryanhcode.sable.api.sublevel.ServerSubLevelContainer; import dev.ryanhcode.sable.api.sublevel.SubLevelContainer; import dev.ryanhcode.sable.mixinterface.plot.SubLevelContainerHolder; @@ -25,7 +26,6 @@ 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.CallbackInfoReturnable; import java.util.function.BooleanSupplier; import java.util.function.Supplier; @@ -90,24 +90,28 @@ protected ServerLevelMixin(final WritableLevelData writableLevelData, final Reso } } - @Inject(method = "shouldTickBlocksAt", at = @At("HEAD"), cancellable = true) - private void sable$shouldTickBlocksAt(final long l, final CallbackInfoReturnable cir) { + @ModifyReturnValue(method = "shouldTickBlocksAt", at = @At("RETURN")) + private boolean sable$shouldTickBlocksAt(final boolean original, final long chunkPos) { final SubLevelContainer plotContainer = SubLevelContainer.getContainer((ServerLevel) (Object) this); assert plotContainer != null; - if (plotContainer.getPlot(new ChunkPos(l)) != null) { - cir.setReturnValue(true); + if (plotContainer.getPlot(new ChunkPos(chunkPos)) != null) { + return true; } + + return original; } - @Inject(method = "isNaturalSpawningAllowed(Lnet/minecraft/world/level/ChunkPos;)Z", at = @At("HEAD"), cancellable = true) - private void sable$isNaturalSpawningAllowed(final ChunkPos chunkPos, final CallbackInfoReturnable cir) { + @ModifyReturnValue(method = "isNaturalSpawningAllowed(Lnet/minecraft/world/level/ChunkPos;)Z", at = @At("RETURN")) + private boolean sable$isNaturalSpawningAllowed(boolean original, final ChunkPos chunkPos) { final SubLevelContainer plotContainer = SubLevelContainer.getContainer((ServerLevel) (Object) this); assert plotContainer != null; if (plotContainer.getPlot(chunkPos) != null) { - cir.setReturnValue(true); + return true; } + + return original; } @Inject(method = "close", at = @At("TAIL")) diff --git a/common/src/main/java/dev/ryanhcode/sable/mixin/water_occlusion/CameraMixin.java b/common/src/main/java/dev/ryanhcode/sable/mixin/water_occlusion/CameraMixin.java index e32ff0ff..f53f2349 100644 --- a/common/src/main/java/dev/ryanhcode/sable/mixin/water_occlusion/CameraMixin.java +++ b/common/src/main/java/dev/ryanhcode/sable/mixin/water_occlusion/CameraMixin.java @@ -1,5 +1,6 @@ package dev.ryanhcode.sable.mixin.water_occlusion; +import com.llamalad7.mixinextras.injector.ModifyReturnValue; import dev.ryanhcode.sable.mixinterface.water_occlusion.CameraWaterOcclusionExtension; import dev.ryanhcode.sable.sublevel.water_occlusion.WaterOcclusionContainer; import net.minecraft.client.Camera; @@ -11,8 +12,6 @@ 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(Camera.class) public class CameraMixin implements CameraWaterOcclusionExtension { @@ -26,19 +25,21 @@ public class CameraMixin implements CameraWaterOcclusionExtension { @Unique private boolean sable$ignoreOcclusion = false; - @Inject(method = "getFluidInCamera", at = @At("RETURN"), cancellable = true) - public void sable$getFluidInCamera(final CallbackInfoReturnable cir) { + @ModifyReturnValue(method = "getFluidInCamera", at = @At("RETURN")) + public FogType sable$getFluidInCamera(final FogType original) { if (this.sable$ignoreOcclusion) { - return; + return original; } - if (cir.getReturnValue() == FogType.WATER || cir.getReturnValue() == FogType.LAVA) { + if (original == FogType.WATER || original == FogType.LAVA) { final boolean occluded = this.sable$isOccluded(); if (occluded) { - cir.setReturnValue(FogType.NONE); + return FogType.NONE; } } + + return original; } @Override diff --git a/common/src/main/java/dev/ryanhcode/sable/mixin/world_border/WorldBorderMixin.java b/common/src/main/java/dev/ryanhcode/sable/mixin/world_border/WorldBorderMixin.java index fc832ab2..1d028dea 100644 --- a/common/src/main/java/dev/ryanhcode/sable/mixin/world_border/WorldBorderMixin.java +++ b/common/src/main/java/dev/ryanhcode/sable/mixin/world_border/WorldBorderMixin.java @@ -1,7 +1,7 @@ package dev.ryanhcode.sable.mixin.world_border; +import com.llamalad7.mixinextras.injector.ModifyReturnValue; import dev.ryanhcode.sable.Sable; -import dev.ryanhcode.sable.api.SubLevelHelper; import dev.ryanhcode.sable.api.sublevel.SubLevelContainer; import dev.ryanhcode.sable.mixinterface.world_border.WorldBorderExtension; import net.minecraft.core.BlockPos; @@ -22,30 +22,26 @@ public class WorldBorderMixin implements WorldBorderExtension { @Unique private Level sable$level; - @Inject(method = "isWithinBounds(DDD)Z", at = @At("HEAD"), cancellable = true) - public void sable$isWithinBounds(final double x, final double z, final double offset, final CallbackInfoReturnable cir) { - if (this.sable$level == null) { - return; - } - + @ModifyReturnValue(method = "isWithinBounds(DDD)Z", at = @At("RETURN")) + public boolean sable$isWithinBounds(final boolean original, final double x, final double z, final double offset) { + if (original || this.sable$level == null) return original; final SubLevelContainer container = SubLevelContainer.getContainer(this.sable$level); - - if (container != null && container.inBounds(Mth.floor(x) >> 4, Mth.floor(z) >> 4)) { - cir.setReturnValue(true); - } + return container != null && container.inBounds(Mth.floor(x) >> 4, Mth.floor(z) >> 4); } - @Inject(method = "clampToBounds(DDD)Lnet/minecraft/core/BlockPos;", at = @At("HEAD"), cancellable = true) - private void sable$clampToBounds(final double x, final double y, final double z, final CallbackInfoReturnable cir) { + @ModifyReturnValue(method = "clampToBounds(DDD)Lnet/minecraft/core/BlockPos;", at = @At("RETURN")) + private BlockPos sable$clampToBounds(final BlockPos original, final double x, final double y, final double z) { if (this.sable$level == null) { - return; + return original; } final SubLevelContainer container = SubLevelContainer.getContainer(this.sable$level); if (container != null && container.inBounds(Mth.floor(x) >> 4, Mth.floor(z) >> 4)) { - cir.setReturnValue(BlockPos.containing(x, y, z)); + return BlockPos.containing(x, y, z); } + + return original; } @Inject(method = "isInsideCloseToBorder", at = @At("HEAD"), cancellable = true) diff --git a/common/src/main/java/dev/ryanhcode/sable/network/packets/tcp/ClientboundDimensionPhysicsPacket.java b/common/src/main/java/dev/ryanhcode/sable/network/packets/tcp/ClientboundDimensionPhysicsPacket.java new file mode 100644 index 00000000..eb0dc4bb --- /dev/null +++ b/common/src/main/java/dev/ryanhcode/sable/network/packets/tcp/ClientboundDimensionPhysicsPacket.java @@ -0,0 +1,47 @@ +package dev.ryanhcode.sable.network.packets.tcp; + +import dev.ryanhcode.sable.Sable; +import dev.ryanhcode.sable.network.tcp.SableTCPPacket; +import dev.ryanhcode.sable.physics.config.dimension_physics.DimensionPhysics; +import dev.ryanhcode.sable.physics.config.dimension_physics.DimensionPhysicsData; +import foundry.veil.api.network.handler.PacketContext; +import io.netty.buffer.ByteBuf; +import net.minecraft.client.Minecraft; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.Level; + +import java.util.ArrayList; +import java.util.List; + +public record ClientboundDimensionPhysicsPacket(List dimensionPhysics) implements SableTCPPacket { + public static final CustomPacketPayload.Type TYPE = new CustomPacketPayload.Type<>(Sable.sablePath("dimension_physics")); + + public static final StreamCodec CODEC = StreamCodec.composite( + ByteBufCodecs.collection( + ArrayList::new, + DimensionPhysics.STREAM_CODEC + ), ClientboundDimensionPhysicsPacket::dimensionPhysics, + ClientboundDimensionPhysicsPacket::new + ); + + + @Override + public void handle(final PacketContext context) { + Minecraft.getInstance().execute(() -> { + DimensionPhysicsData.clearPhysics(); + for (final DimensionPhysics dimensionPhysic : this.dimensionPhysics) { + final ResourceKey dimension = ResourceKey.create(Registries.DIMENSION, dimensionPhysic.dimension()); + DimensionPhysicsData.putPhysics(dimension, dimensionPhysic); + } + }); + } + + @Override + public Type type() { + return TYPE; + } +} diff --git a/common/src/main/java/dev/ryanhcode/sable/network/tcp/SableTCPPackets.java b/common/src/main/java/dev/ryanhcode/sable/network/tcp/SableTCPPackets.java index 3ba9c150..90b5d84d 100644 --- a/common/src/main/java/dev/ryanhcode/sable/network/tcp/SableTCPPackets.java +++ b/common/src/main/java/dev/ryanhcode/sable/network/tcp/SableTCPPackets.java @@ -26,6 +26,8 @@ public static void init() { PACKET_MANAGER.registerClientbound(ClientboundPhysicsPropertyPacket.TYPE, ClientboundPhysicsPropertyPacket.CODEC, ClientboundPhysicsPropertyPacket::handle); PACKET_MANAGER.registerClientbound(ClientboundFloatingBlockMaterialPacket.TYPE, ClientboundFloatingBlockMaterialPacket.CODEC, ClientboundFloatingBlockMaterialPacket::handle); + PACKET_MANAGER.registerClientbound(ClientboundDimensionPhysicsPacket.TYPE, ClientboundDimensionPhysicsPacket.CODEC, ClientboundDimensionPhysicsPacket::handle); + PACKET_MANAGER.registerClientbound(ClientboundRecentlySplitSubLevelPacket.TYPE, ClientboundRecentlySplitSubLevelPacket.CODEC, ClientboundRecentlySplitSubLevelPacket::handle); PACKET_MANAGER.registerClientbound(ClientboundSableUDPActivationPacket.TYPE, ClientboundSableUDPActivationPacket.CODEC, ClientboundSableUDPActivationPacket::handle); diff --git a/common/src/main/java/dev/ryanhcode/sable/physics/config/dimension_physics/BezierResourceFunction.java b/common/src/main/java/dev/ryanhcode/sable/physics/config/dimension_physics/BezierResourceFunction.java index b0fca4d5..6e7b7267 100644 --- a/common/src/main/java/dev/ryanhcode/sable/physics/config/dimension_physics/BezierResourceFunction.java +++ b/common/src/main/java/dev/ryanhcode/sable/physics/config/dimension_physics/BezierResourceFunction.java @@ -4,6 +4,9 @@ import com.mojang.serialization.DataResult; import com.mojang.serialization.codecs.RecordCodecBuilder; import dev.ryanhcode.sable.util.SableCodecUtil; +import io.netty.buffer.ByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; import java.util.ArrayList; import java.util.List; @@ -14,6 +17,11 @@ public class BezierResourceFunction { (bezierResourceFunction -> DataResult.success(bezierResourceFunction.getPoints())) ); + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.collection(ArrayList::new, BezierPoint.STREAM_CODEC), BezierResourceFunction::getPoints, + BezierResourceFunction::new + ); + private final List points; public BezierResourceFunction(final List points) { @@ -74,5 +82,12 @@ public record BezierPoint(double altitude, double value, double slope) { SableCodecUtil.positiveDouble(true).fieldOf("value").forGetter(BezierPoint::value), Codec.DOUBLE.fieldOf("slope").forGetter(BezierPoint::slope) ).apply(instance, BezierPoint::new)); + + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.DOUBLE, BezierPoint::altitude, + ByteBufCodecs.DOUBLE, BezierPoint::value, + ByteBufCodecs.DOUBLE, BezierPoint::slope, + BezierPoint::new + ); } } diff --git a/common/src/main/java/dev/ryanhcode/sable/physics/config/dimension_physics/DimensionPhysics.java b/common/src/main/java/dev/ryanhcode/sable/physics/config/dimension_physics/DimensionPhysics.java index cbeb1f61..810af382 100644 --- a/common/src/main/java/dev/ryanhcode/sable/physics/config/dimension_physics/DimensionPhysics.java +++ b/common/src/main/java/dev/ryanhcode/sable/physics/config/dimension_physics/DimensionPhysics.java @@ -3,6 +3,9 @@ import com.mojang.datafixers.kinds.Applicative; import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; +import io.netty.buffer.ByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; import net.minecraft.resources.ResourceLocation; import net.minecraft.util.ExtraCodecs; import net.minecraft.world.level.Level; @@ -30,6 +33,29 @@ public record DimensionPhysics(ResourceLocation dimension, int priority, Optiona Codec.BOOL.optionalFieldOf("ignore_chunks", false).forGetter(DimensionPhysics::ignoreChunks) ).apply(Applicative.unbox(instance), DimensionPhysics::new)); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember( + (dim, buf) -> { + ResourceLocation.STREAM_CODEC.encode(buf, dim.dimension); + ByteBufCodecs.INT.encode(buf, dim.priority); + ByteBufCodecs.FLOAT.apply(ByteBufCodecs::optional).encode(buf, dim.universalDrag); + ByteBufCodecs.VECTOR3F.apply(ByteBufCodecs::optional).encode(buf, dim.baseGravity); + ByteBufCodecs.DOUBLE.apply(ByteBufCodecs::optional).encode(buf, dim.basePressure); + BezierResourceFunction.STREAM_CODEC.apply(ByteBufCodecs::optional).encode(buf, dim.pressureFunction); + ByteBufCodecs.VECTOR3F.apply(ByteBufCodecs::optional).encode(buf, dim.magneticNorth); + ByteBufCodecs.BOOL.encode(buf, dim.ignoreChunks); + }, + buf -> new DimensionPhysics( + ResourceLocation.STREAM_CODEC.decode(buf), + ByteBufCodecs.INT.decode(buf), + ByteBufCodecs.FLOAT.apply(ByteBufCodecs::optional).decode(buf), + ByteBufCodecs.VECTOR3F.apply(ByteBufCodecs::optional).decode(buf), + ByteBufCodecs.DOUBLE.apply(ByteBufCodecs::optional).decode(buf), + BezierResourceFunction.STREAM_CODEC.apply(ByteBufCodecs::optional).decode(buf), + ByteBufCodecs.VECTOR3F.apply(ByteBufCodecs::optional).decode(buf), + ByteBufCodecs.BOOL.decode(buf) + ) + ); + public static DimensionPhysics createDefault(final Level level) { // constructs a bezier air pressure curve approximating an exponential decay, centered around sea level // clamped to at most 1.5 pressure underground, and with a 40-meter smooth drop-off at the build limit diff --git a/common/src/main/java/dev/ryanhcode/sable/physics/config/dimension_physics/DimensionPhysicsData.java b/common/src/main/java/dev/ryanhcode/sable/physics/config/dimension_physics/DimensionPhysicsData.java index 32866d19..eb04bbc0 100644 --- a/common/src/main/java/dev/ryanhcode/sable/physics/config/dimension_physics/DimensionPhysicsData.java +++ b/common/src/main/java/dev/ryanhcode/sable/physics/config/dimension_physics/DimensionPhysicsData.java @@ -6,6 +6,7 @@ import com.mojang.serialization.JsonOps; import dev.ryanhcode.sable.Sable; import dev.ryanhcode.sable.companion.math.JOMLConversion; +import dev.ryanhcode.sable.network.packets.tcp.ClientboundDimensionPhysicsPacket; import net.minecraft.core.registries.Registries; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; @@ -85,6 +86,30 @@ public static double getUniversalDrag(final ServerLevel level) { return physics.universalDrag().orElseGet(defaultPhysics.universalDrag()::orElseThrow); } + public static void addPhysicsWithPriority(final ResourceKey key, final DimensionPhysics newProperties) { + final DimensionPhysics existing = DIMENSION_PHYSICS_DATA.get(key); + + if (existing != null) { + if (newProperties.priority() > existing.priority()) { + DIMENSION_PHYSICS_DATA.put(key, newProperties); + } + } else { + DIMENSION_PHYSICS_DATA.put(key, newProperties); + } + } + + public static void putPhysics(final ResourceKey key, final DimensionPhysics newProperties) { + DIMENSION_PHYSICS_DATA.put(key, newProperties); + } + + public static void clearPhysics() { + DIMENSION_PHYSICS_DATA.clear(); + } + + public static ClientboundDimensionPhysicsPacket compilePacket() { + return new ClientboundDimensionPhysicsPacket(DIMENSION_PHYSICS_DATA.values().stream().toList()); + } + public static class ReloadListener extends SimpleJsonResourceReloadListener { private static final Gson GSON = new Gson(); @@ -97,21 +122,9 @@ public ReloadListener() { super(ReloadListener.GSON, NAME); } - public static void addKeyWithPriority(final Map, DimensionPhysics> data, final ResourceKey key, final DimensionPhysics newProperties) { - final DimensionPhysics existing = data.get(key); - - if (existing != null) { - if (newProperties.priority() > existing.priority()) { - data.put(key, newProperties); - } - } else { - data.put(key, newProperties); - } - } - @Override protected void apply(final Map map, final ResourceManager resourceManager, final ProfilerFiller profiler) { - DIMENSION_PHYSICS_DATA.clear(); + clearPhysics(); for (final Map.Entry entry : map.entrySet()) { try { @@ -124,7 +137,7 @@ protected void apply(final Map map, final Resourc final DimensionPhysics dimensionPhysics = dataResult.getOrThrow(); final ResourceKey dimension = ResourceKey.create(Registries.DIMENSION, dimensionPhysics.dimension()); - addKeyWithPriority(DIMENSION_PHYSICS_DATA, dimension, dimensionPhysics); + addPhysicsWithPriority(dimension, dimensionPhysics); } catch (final Exception e) { Sable.LOGGER.error("Error while loading dimension data \"{}\" : {} ", entry.getKey(), e.getMessage()); } diff --git a/common/src/main/java/dev/ryanhcode/sable/physics/floating_block/FloatingBlockController.java b/common/src/main/java/dev/ryanhcode/sable/physics/floating_block/FloatingBlockController.java index 66628f9e..da379f9d 100644 --- a/common/src/main/java/dev/ryanhcode/sable/physics/floating_block/FloatingBlockController.java +++ b/common/src/main/java/dev/ryanhcode/sable/physics/floating_block/FloatingBlockController.java @@ -39,7 +39,7 @@ public void physicsTick(final double partialPhysicsTick, final double timeStep, this.containers.clear(); this.containers.add(this.sublevelContainer); - for(final KinematicContraption contraption : this.subLevel.getPlot().getContraptions()) { + for (final KinematicContraption contraption : this.subLevel.getPlot().getContraptions()) { final FloatingClusterContainer container = contraption.sable$getFloatingClusterContainer(); final Vector3dc lastPosition = new Vector3d(contraption.sable$getPosition(partialPhysicsTick - 1.0f)); @@ -64,7 +64,7 @@ public void physicsTick(final double partialPhysicsTick, final double timeStep, localGravity.set(DimensionPhysicsData.getGravity(this.subLevel.getLevel(), this.subLevel.logicalPose().position())); this.subLevel.logicalPose().orientation().transformInverse(localGravity); - if(!this.needsTicking()) + if (!this.needsTicking()) return; this.subLevel.logicalPose().orientation().transformInverse(linearVelocity, localLinearVelocity); @@ -82,10 +82,10 @@ public void physicsTick(final double partialPhysicsTick, final double timeStep, if (cluster.getMaterial().scaleWithPressure()) cluster.getBlockData().computePressureScale(this.subLevel); - this.applyFriction(container,cluster, localGravity, localLinearVelocity, localAngularVelocity, clusterFrictionForce, clusterFrictionTorque); + this.applyFriction(container, cluster, localGravity, localLinearVelocity, localAngularVelocity, clusterFrictionForce, clusterFrictionTorque); final Vector3d recordedClusterFrictionForce = new Vector3d(clusterFrictionForce); - this.recordForce(container,cluster, dragGroup, recordedClusterFrictionForce); + this.recordForce(container, cluster, dragGroup, recordedClusterFrictionForce); recordedFrictionForces.add(recordedClusterFrictionForce); frictionForce.add(clusterFrictionForce); @@ -113,19 +113,18 @@ public void physicsTick(final double partialPhysicsTick, final double timeStep, for (final Vector3d force : recordedFrictionForces) { force.mul(timeStep);//forceScale * } - if(localGravity.lengthSquared()>0) + if (localGravity.lengthSquared() > 0) this.applyLift(localGravity, linearImpulse, angularImpulse, timeStep); linearImpulse.fma(timeStep, frictionForce); angularImpulse.fma(timeStep, frictionTorque); } - public boolean needsTicking() - { - if(this.sublevelContainer.needsTicking()) + public boolean needsTicking() { + if (this.sublevelContainer.needsTicking()) return true; for (final FloatingClusterContainer container : this.containers) { - if(container.needsTicking()) + if (container.needsTicking()) return true; } return false; @@ -174,7 +173,7 @@ private void applyLift(final Vector3d localGravity, final Vector3d linearImpulse //unit: strength * weight final double weightedForce = clusterForce * cluster.getBlockData().totalScale; - this.getTrueWeightedClusterPosition(container,cluster,weightedPositionTemp); + this.getTrueWeightedClusterPosition(container, cluster, weightedPositionTemp); if (material.preventSelfLift()) { totalForce += weightedForce; @@ -186,7 +185,7 @@ private void applyLift(final Vector3d localGravity, final Vector3d linearImpulse if (this.subLevel.isTrackingIndividualQueuedForces()) { final QueuedForceGroup levitationGroup = this.subLevel.getOrCreateQueuedForceGroup(ForceGroups.LEVITATION.get()); - this.recordForce(container,cluster, levitationGroup, new Vector3d(localGravity).mul(-weightedForce * timeStep)); + this.recordForce(container, cluster, levitationGroup, new Vector3d(localGravity).mul(-weightedForce * timeStep)); } localGravity.cross(weightedPositionTemp, torqueTemp);//torqueTemp unit: weight * position * gravity @@ -226,7 +225,7 @@ private void applyLift(final Vector3d localGravity, final Vector3d linearImpulse final Vector3d force = new Vector3d(localGravity).mul(timeStep * -cluster.getBlockData().totalScale * material.liftStrength()); force.mul(scaleFactor); - this.recordForce(container,cluster, levitationGroup, force); + this.recordForce(container, cluster, levitationGroup, force); } } } @@ -237,13 +236,13 @@ private void applyLift(final Vector3d localGravity, final Vector3d linearImpulse angularImpulse.fma(timeStep, liftingTorque); } - private void recordForce(final FloatingClusterContainer container,final FloatingBlockCluster cluster, final QueuedForceGroup forceGroup, final Vector3d force) { - forceGroup.recordPointForce(this.getTrueWeightedClusterPosition(container,cluster,new Vector3d()).div(cluster.getBlockData().totalScale).add(this.subLevel.getMassTracker().getCenterOfMass()), force); + private void recordForce(final FloatingClusterContainer container, final FloatingBlockCluster cluster, final QueuedForceGroup forceGroup, final Vector3d force) { + forceGroup.recordPointForce(this.getTrueWeightedClusterPosition(container, cluster, new Vector3d()).div(cluster.getBlockData().totalScale).add(this.subLevel.getMassTracker().getCenterOfMass()), force); } - private Vector3d getTrueWeightedClusterPosition(final FloatingClusterContainer container,final FloatingBlockCluster cluster,final Vector3d pos) - { - container.rotationOffset.transform(cluster.getBlockData().weightedPosition,pos); - return pos.fma(cluster.getBlockData().totalScale,container.positionOffset); + + private Vector3d getTrueWeightedClusterPosition(final FloatingClusterContainer container, final FloatingBlockCluster cluster, final Vector3d pos) { + container.rotationOffset.transform(cluster.getBlockData().weightedPosition, pos); + return pos.fma(cluster.getBlockData().totalScale, container.positionOffset); } private static final Matrix3d containerRotation = new Matrix3d(); @@ -261,10 +260,10 @@ private Vector3d getTrueWeightedClusterPosition(final FloatingClusterContainer c private static final Vector3d shiftedCenter = new Vector3d(); private static final Vector3d linearSlowDrag = new Vector3d(); - private void applyFriction(final FloatingClusterContainer container,final FloatingBlockCluster cluster, final Vector3dc localGravity, final Vector3dc linearVelocity, final Vector3dc angularVelocity, final Vector3d frictionForce, final Vector3d frictionTorque) { + private void applyFriction(final FloatingClusterContainer container, final FloatingBlockCluster cluster, final Vector3dc localGravity, final Vector3dc linearVelocity, final Vector3dc angularVelocity, final Vector3d frictionForce, final Vector3d frictionTorque) { double frictionScale = 1; - if(cluster.getMaterial().scaleWithGravity()) + if (cluster.getMaterial().scaleWithGravity()) frictionScale = localGravity.length(); if (cluster.getMaterial().scaleWithPressure()) frictionScale *= cluster.getBlockData().getPressureScale(); @@ -274,7 +273,7 @@ private void applyFriction(final FloatingClusterContainer container,final Floati speedScale = 0; totalAngularVelocity.set(angularVelocity).add(container.angularVelocity); - this.getTrueWeightedClusterPosition(container,cluster,clusterCenter).div(cluster.getBlockData().totalScale); + this.getTrueWeightedClusterPosition(container, cluster, clusterCenter).div(cluster.getBlockData().totalScale); cluster.getBlockData().outerProduct.scale(1 / cluster.getBlockData().totalScale, averagePositionMatrix); @@ -291,8 +290,8 @@ private void applyFriction(final FloatingClusterContainer container,final Floati //velocity of the center of lift in local space angularVelocity.cross(clusterCenter, meanVelocity); - container.rotationOffset.transform(cluster.getBlockData().weightedPosition,rotatedPos).div(cluster.getBlockData().totalScale); - final Vector3d extraContainerVelocity = container.angularVelocity.cross(rotatedPos,rotatedPos); + container.rotationOffset.transform(cluster.getBlockData().weightedPosition, rotatedPos).div(cluster.getBlockData().totalScale); + final Vector3d extraContainerVelocity = container.angularVelocity.cross(rotatedPos, rotatedPos); meanVelocity.add(linearVelocity).add(container.velocity).add(extraContainerVelocity); //center of the shifted position distribution relative to clusterCenter, variance is shiftedPositionMatrix @@ -343,7 +342,7 @@ private void matrixThingy(final Matrix3dc X, final Matrix3dc Y, final Matrix3d o } private Matrix3d getGravityMatrix(final Vector3dc g, final double verticalDrag, final double horizontalDrag, final Matrix3d target) { - if(g.lengthSquared() > 0.00001) + if (g.lengthSquared() > 0.00001) SableMathUtils.setOuterProduct(g, g, (horizontalDrag - verticalDrag) / g.dot(g), target); else target.identity(); @@ -365,31 +364,31 @@ private double getClampingFactor(final Vector3dc currentVelocity, final Vector3d return v * (1 - Math.exp(-k / v)) / k; } - private double getKineticClampingFactor(final Vector3dc currentLinearVelocity,final Vector3dc currentAngularVelocity,final Vector3d frictionForce,final Vector3d frictionTorque,final double timestep) { + private double getKineticClampingFactor(final Vector3dc currentLinearVelocity, final Vector3dc currentAngularVelocity, final Vector3d frictionForce, final Vector3d frictionTorque, final double timestep) { final double numerator = currentLinearVelocity.dot(frictionForce) + currentAngularVelocity.dot(frictionTorque); - double denominator = frictionForce.dot(frictionForce)* this.subLevel.getMassTracker().getInverseMass() + - SableMathUtils.multiplyInnerProduct(frictionTorque, this.subLevel.getMassTracker().getInverseInertiaTensor(),frictionTorque); - denominator*=timestep; - if(denominator < 1E-10) + double denominator = frictionForce.dot(frictionForce) * this.subLevel.getMassTracker().getInverseMass() + + SableMathUtils.multiplyInnerProduct(frictionTorque, this.subLevel.getMassTracker().getInverseInertiaTensor(), frictionTorque); + denominator *= timestep; + if (denominator < 1E-10) return 1; - final double t = -numerator/denominator; - return Math.max(Math.min(t,1),0); + final double t = -numerator / denominator; + return Math.max(Math.min(t, 1), 0); } public void addFloatingBlock(final BlockState state, final Vector3d pos) { - this.sublevelContainer.addFloatingBlock(state,pos); + this.sublevelContainer.addFloatingBlock(state, pos); } public void removeFloatingBlock(final BlockState state, final Vector3d pos) { - this.sublevelContainer.removeFloatingBlock(state,pos); + this.sublevelContainer.removeFloatingBlock(state, pos); } public void queueAddFloatingBlock(final BlockState state, final BlockPos pos) { - this.sublevelContainer.queueAddFloatingBlock(state,pos); + this.sublevelContainer.queueAddFloatingBlock(state, pos); } public void queueRemoveFloatingBlock(final BlockState state, final BlockPos pos) { - this.sublevelContainer.queueRemoveFloatingBlock(state,pos); + this.sublevelContainer.queueRemoveFloatingBlock(state, pos); } } diff --git a/common/src/main/java/dev/ryanhcode/sable/sublevel/entity_collision/SubLevelEntityCollision.java b/common/src/main/java/dev/ryanhcode/sable/sublevel/entity_collision/SubLevelEntityCollision.java index fdd62eab..e4ce63f0 100644 --- a/common/src/main/java/dev/ryanhcode/sable/sublevel/entity_collision/SubLevelEntityCollision.java +++ b/common/src/main/java/dev/ryanhcode/sable/sublevel/entity_collision/SubLevelEntityCollision.java @@ -207,6 +207,20 @@ public static CollisionInfo collide(final Entity entity, final Vec3 collisionMot lastPose.lerp(logicalPose, (double) (i - 1) / substeps, lastSubLevelPose); lastPose.lerp(logicalPose, (double) (i) / substeps, subLevelPose); + // Re-orient the entity's collision box to THIS sub-level's pose at THIS substep. + // The computation before the loop reads subLevelPose before it has been set for the + // current iteration, so the box was oriented to the previous substep's pose — or to + // a different sub-level entirely — making entities catch on walls and doorframes of + // rotated sub-levels. The custom-orientation composition matches + // transformEntityBoundingBox (premul); the up-direction and bounds-center handling + // it also performs depends only on the custom orientation, not the yaw, so it does + // not need to be redone here. + sink.entityBoxOrientation.identity().rotateY(getHitBoxYaw(subLevelPose)); + if (customEntityOrientation != null) { + sink.entityBoxOrientation.premul(customEntityOrientation); + } + entityBoundsOBB.setOrientation(sink.entityBoxOrientation); + rotatedContextBounds.set(fullContextBounds); if (customEntityOrientation != null) { entityBoundsOBB.vertices(sink.a); @@ -241,7 +255,19 @@ public static CollisionInfo collide(final Entity entity, final Vec3 collisionMot sink.trackingPosition.set(entityBoundsCenter).add(feetOffset); subLevelPose.transformPosition(lastSubLevelPose.transformPositionInverse(sink.trackingPosition)).sub(feetOffset, entityBoundsCenter); entityBoundsCenter.add(collisionMotion, entityBoundsOBB.getPosition()); - entityBoundsCenter.fma(verticalAnchorPosition - entity.getBoundingBox().getYsize() / 2.0, entityUp, sink.tempEyePosition).sub(0.0, verticalAnchorPosition, 0.0); + + // Reconstruct the entity position as the exact inverse of how entityBoundsCenter + // was derived from it (getAABBCenter + transformEntityBoundsCenter). The previous + // reconstruction subtracted half the body height along entityUp instead, which for + // custom orientations displaces the entity laterally by ~eyeHeight * sin(tilt) + // EVERY tick — a phantom conveyor on any tilted surface. + sink.tempEyePosition.set(entityBoundsCenter); + if (customEntityOrientation != null) { + final double eyeLever = entity.getEyeHeight() - entity.getBoundingBox().getYsize() / 2.0; + sink.tempEyePosition.sub(0.0, eyeLever, 0.0) + .add(customEntityOrientation.transform(new Vector3d(0.0, eyeLever, 0.0))); + } + sink.tempEyePosition.sub(0.0, entity.getBoundingBox().getYsize() / 2.0, 0.0); ((EntityExtension) entity).sable$setPosSuperRaw(new Vec3(sink.tempEyePosition.x, sink.tempEyePosition.y, sink.tempEyePosition.z)); boolean anySurroundingBlocksSolid = false; diff --git a/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/SubLevelTicketsSavedData.java b/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/SubLevelTicketsSavedData.java index 03b879bb..9bae61e1 100644 --- a/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/SubLevelTicketsSavedData.java +++ b/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/SubLevelTicketsSavedData.java @@ -109,10 +109,10 @@ private static SubLevelLoadingTicket deserializeTicket(final UUID subLeve } private static CompoundTag serializeTicket(final SubLevelLoadingTicket ticket) { - final SubLevelLoadingTicketType type = ticket.getType(); + final SubLevelLoadingTicketType type = ticket.type(); final Codec codec = type.codec(); - return codec.encodeStart(NbtOps.INSTANCE, ticket.getKey()) + return codec.encodeStart(NbtOps.INSTANCE, ticket.key()) .resultOrPartial(error -> Sable.LOGGER.warn("Failed to serialize ticket key for type {}: {}", type.name(), error)) .map(keyTag -> { final CompoundTag tag = new CompoundTag(); diff --git a/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/holding/SubLevelHoldingChunk.java b/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/holding/SubLevelHoldingChunk.java index 858f2b22..55c167a5 100644 --- a/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/holding/SubLevelHoldingChunk.java +++ b/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/holding/SubLevelHoldingChunk.java @@ -188,6 +188,10 @@ public boolean shouldKeepLoaded() { return this.keepLoaded; } + public boolean isEmpty() { + return this.pointers.isEmpty() && this.loadedHoldingSubLevels.isEmpty(); + } + @Override public String toString() { return "SubLevelHoldingChunk{" + diff --git a/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/holding/SubLevelHoldingChunkMap.java b/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/holding/SubLevelHoldingChunkMap.java index 1b91eecd..64a70042 100644 --- a/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/holding/SubLevelHoldingChunkMap.java +++ b/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/holding/SubLevelHoldingChunkMap.java @@ -270,6 +270,10 @@ public void saveAll() { this.queuedUnloads.add(holdingChunkPos); } } + + if (holdingChunk.isEmpty()) { + this.queuedUnloads.add(holdingChunkPos); + } } for (final ChunkPos unload : this.queuedUnloads) { @@ -283,6 +287,7 @@ public void saveAll() { for (final HoldingSubLevel holdingSubLevel : holdingChunk.getLoadedHoldingSubLevels()) { this.allHoldingSubLevels.remove(holdingSubLevel.data().uuid()); } + this.setDirty(unload); } } @@ -297,7 +302,11 @@ public void saveAll() { } if (holdingChunk != null) { - this.storage.attemptSaveHoldingChunk(chunkPos, holdingChunk); + if (holdingChunk.isEmpty()) { + this.storage.attemptRemoveHoldingChunk(chunkPos); + } else { + this.storage.attemptSaveHoldingChunk(chunkPos, holdingChunk); + } } } @@ -312,6 +321,7 @@ public void saveAll() { } this.storage.flush(); + this.storage.pruneCache(); } catch (final IOException e) { Sable.LOGGER.error("Failed to flush sub-level storage to disk", e); } diff --git a/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/region/SubLevelRegionFile.java b/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/region/SubLevelRegionFile.java index d138887e..2223f223 100644 --- a/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/region/SubLevelRegionFile.java +++ b/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/region/SubLevelRegionFile.java @@ -42,6 +42,14 @@ public void trySave(final int localX, final int localZ, final SubLevelHoldingChu } } + public void tryRemove(final int localX, final int localZ) { + try { + this.write(this.getIndex(localX, localZ), (CompoundTag) null); + } catch (final IOException e) { + Sable.LOGGER.error("Failed to remove sub-level holding chunk at ({}, {})", localX, localZ, e); + } + } + @Nullable public SubLevelHoldingChunk read(final ChunkPos chunkPos) { final int localX = chunkPos.getRegionLocalX(); diff --git a/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/region/SubLevelStorageFile.java b/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/region/SubLevelStorageFile.java index bd76a2f9..ba9d665a 100644 --- a/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/region/SubLevelStorageFile.java +++ b/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/region/SubLevelStorageFile.java @@ -15,6 +15,7 @@ import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.util.BitSet; +import java.util.stream.Stream; /** * A storage file for sub-levels. @@ -208,6 +209,10 @@ private ByteBuffer createExternalStub() { return byteBuffer; } + public boolean isEmpty() { + return this.usedSectors.length() <= (this.beginningSectorSize / this.sectorSize); + } + /** * Writes a sub-levels data to disk * @@ -251,10 +256,14 @@ protected void write(final int index, final ByteBuffer byteBuffer) throws IOExce this.usedIndices.set(index, true); this.writeHeader(); + final Path externalPath = this.getExternalFilePath(index); if (savingToExternalFile) { - Files.move(temporaryExternalFile, this.getExternalFilePath(index), StandardCopyOption.REPLACE_EXISTING); + Files.move(temporaryExternalFile, externalPath, StandardCopyOption.REPLACE_EXISTING); } else { - Files.deleteIfExists(this.getExternalFilePath(index)); + // we're not saving to an external file, but the previous time we were writing to this index there + // could've been an external file. so we remove it just in case, to prevent the detached files sticking + // around forever + Files.deleteIfExists(externalPath); } // clear the previous span of sectors if we used to store data there for this sub-level index @@ -351,6 +360,8 @@ private void clear(final int index) throws IOException { this.usedSectors.clear(spanStart, spanStart + this.getSpanLength(span)); this.writeHeader(); + + Files.deleteIfExists(this.getExternalFilePath(index)); } } @@ -377,6 +388,24 @@ public int packSpan(final int start, final int length) { return (start << 8) | length; // Pack the offset and length into a single integer } + private void padOrTruncateToFullSector() throws IOException { + // how many sectors of data are we using? + final int bytesNeededForFile = this.usedSectors.length() * this.sectorSize; + final int currentFileSize = (int) this.file.size(); + + if (currentFileSize > bytesNeededForFile) { + this.file.truncate(bytesNeededForFile); + } else { + final int desiredSize = bytesNeededForFile; + + if (currentFileSize < desiredSize) { + final ByteBuffer byteBuffer = PADDING_BUFFER.duplicate(); + byteBuffer.position(0); + this.file.write(byteBuffer, desiredSize - 1); + } + } + } + /** * Frees any native resources held by this object. */ @@ -393,28 +422,56 @@ public void close() throws IOException { } } - public void flush() throws IOException { - this.file.force(true); + public void delete() { + try { + this.file.close(); + } catch (final IOException e) { + Sable.LOGGER.error("Failed to close sub-level storage file {} before deletion", this.path, e); + } + + try { + Files.deleteIfExists(this.path); + } catch (final IOException e) { + Sable.LOGGER.error("Failed to delete sub-level storage file {}", this.path, e); + } + + // Attempt to delete all the external files + // Technically, I don't think it's possible for this to matter? Because by the time the storage file is deleted, + // it's assumed to be empty. So all the external files should be gone anyway. But let's do it anyway to be safe + this.deleteExternalFiles(); } - private void padOrTruncateToFullSector() throws IOException { - // how many sectors of data are we using? - final int bytesNeededForFile = this.usedSectors.length() * this.sectorSize; - final int currentFileSize = (int) this.file.size(); + /** + * Attempts to delete all the external storage files + */ + private void deleteExternalFiles() { + if (!Files.isDirectory(this.externalFileDir)) { + return; + } - if (currentFileSize > bytesNeededForFile) { - this.file.truncate(bytesNeededForFile); - } else { - final int desiredSize = bytesNeededForFile; + try (final Stream list = Files.list(this.externalFileDir)) { + list.forEach(path -> { + try { + Files.deleteIfExists(path); + } catch (final IOException e) { + Sable.LOGGER.error("Failed to delete external sub-level storage file {}", path, e); + } + }); + } catch (final IOException e) { + Sable.LOGGER.error("Failed to list external sub-level storage directory {}", this.externalFileDir, e); + } - if (currentFileSize < desiredSize) { - final ByteBuffer byteBuffer = PADDING_BUFFER.duplicate(); - byteBuffer.position(0); - this.file.write(byteBuffer, desiredSize - 1); - } + try { + Files.deleteIfExists(this.externalFileDir); + } catch (final IOException e) { + Sable.LOGGER.error("Failed to delete external sub-level storage directory {}", this.externalFileDir, e); } } + public void flush() throws IOException { + this.file.force(true); + } + class SectorSpanDataBuffer extends ByteArrayOutputStream { private final int subLevelIndex; diff --git a/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/serialization/SubLevelSerializer.java b/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/serialization/SubLevelSerializer.java index 67f9989b..0944212d 100644 --- a/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/serialization/SubLevelSerializer.java +++ b/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/serialization/SubLevelSerializer.java @@ -156,7 +156,7 @@ public static ServerSubLevel fullyLoad(final ServerLevel level, final SubLevelDa try { subLevel = (ServerSubLevel) plotContainer.allocateSubLevel(halfLoadedSubLevel.uuid(), plotX, plotZ, pose); } catch (final IllegalArgumentException e) { - Sable.LOGGER.error("Failed to load sub-level, skipping", halfLoadedSubLevel, e); + Sable.LOGGER.error("Failed to load sub-level {}, skipping", halfLoadedSubLevel, e); return null; } diff --git a/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/serialization/SubLevelStorage.java b/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/serialization/SubLevelStorage.java index d5d296df..9750255a 100644 --- a/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/serialization/SubLevelStorage.java +++ b/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/serialization/SubLevelStorage.java @@ -8,6 +8,7 @@ import dev.ryanhcode.sable.sublevel.storage.region.SubLevelRegionFile; import dev.ryanhcode.sable.sublevel.storage.region.SubLevelStorageFile; import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectIterator; import net.minecraft.FileUtil; import net.minecraft.core.SectionPos; import net.minecraft.nbt.CompoundTag; @@ -51,7 +52,12 @@ private SubLevelRegionFile getRegionFile(final ChunkPos chunkPos) throws IOExcep } if (this.regionCache.size() >= MAX_CACHE_SIZE) { - this.regionCache.removeLast().close(); + final SubLevelRegionFile last = this.regionCache.removeLast(); + if (last.isEmpty()) { + last.delete(); + } else { + last.close(); + } } final Path path = this.getPath(chunkPos); @@ -70,7 +76,13 @@ private SubLevelStorageFile getRegionStorageFile(final ChunkPos chunkPos, final } if (this.storageCache.size() >= MAX_CACHE_SIZE) { - this.storageCache.removeLast().close(); + final SubLevelStorageFile last = this.storageCache.removeLast(); + + if (last.isEmpty()) { + last.delete(); + } else { + last.close(); + } } FileUtil.createDirectoriesSafe(this.folder); @@ -100,7 +112,15 @@ public void attemptSaveHoldingChunk(final ChunkPos chunkPos, final SubLevelHoldi regionFile.trySave(chunkPos.getRegionLocalX(), chunkPos.getRegionLocalZ(), holdingChunk); } catch (final IOException e) { Sable.LOGGER.error("Failed to save holding chunk for {}", chunkPos, e); + } + } + public void attemptRemoveHoldingChunk(final ChunkPos chunkPos) { + try { + final SubLevelRegionFile regionFile = this.getRegionFile(chunkPos); + regionFile.tryRemove(chunkPos.getRegionLocalX(), chunkPos.getRegionLocalZ()); + } catch (final IOException e) { + Sable.LOGGER.error("Failed to remove holding chunk for {}", chunkPos, e); } } @@ -264,6 +284,33 @@ public Path getFolder() { return this.folder; } + /** + * Prunes all empty files in the cache + */ + public void pruneCache() throws IOException { + final ObjectIterator storageFiles = this.storageCache.values().iterator(); + + while (storageFiles.hasNext()) { + final SubLevelStorageFile storageFile = storageFiles.next(); + + if (storageFile.isEmpty()) { + storageFile.delete(); + storageFiles.remove(); + } + } + + final ObjectIterator regionFiles = this.regionCache.values().iterator(); + + while (regionFiles.hasNext()) { + final SubLevelRegionFile regionFile = regionFiles.next(); + + if (regionFile.isEmpty()) { + regionFile.delete(); + regionFiles.remove(); + } + } + } + /** * Flushes all cached region and storage files to disk. */ diff --git a/common/src/main/resources/assets/sable/lang/en_us.json b/common/src/main/resources/assets/sable/lang/en_us.json index 1e084382..26c8f6bb 100644 --- a/common/src/main/resources/assets/sable/lang/en_us.json +++ b/common/src/main/resources/assets/sable/lang/en_us.json @@ -78,6 +78,11 @@ "commands.sable.forceload.add.count": "Added force-loading tickets for %s sub-level(s)", "commands.sable.forceload.remove.count": "Removed force-loading tickets for %s sub-level(s)", + "commands.sable.forceload.query.none": "No force-loading tickets were found in %s", + "commands.sable.forceload.query.count": "%s force-loading ticket(s) were found for %s sub-level(s) in %s:", + "commands.sable.forceload.sub_level_name": "%s:", + "commands.sable.forceload.ticket": " %s", + "argument.sable.body.selector.all": "All sub-levels", "argument.sable.body.selector.nearest": "Nearest sub-level", "argument.sable.body.selector.random": "Random sub-level", @@ -108,7 +113,9 @@ "argument.sable.sub_level.modifier.sort": "Sort the sub-levels by distance", "argument.sable.unexpected_end_of_input": "Unexpected end of input", "argument.sable.single_sub_level_required": "Only one sub-level is allowed, but the provided selector allows more than one", - "argument.sable.sub_level.invalid": "Invalid sub-level selector", + "argument.sable.invalid_selector": "Invalid sub-level selector", + "argument.sable.invalid_uuid": "Invalid UUID", + "argument.sable.cannot_find_sub_level": "Cannot find sub-level", "argument.sable.sub_level.expected_end_of_modifier": "Expected end of modifier", "argument.sable.sub_level.expected_positive_integer": "Expected a positive integer", "argument.sable.sub_level.expected_positive_decimal": "Expected a positive decimal", diff --git a/common/src/main/resources/sable.mixins.json b/common/src/main/resources/sable.mixins.json index ca9f4fea..447f2f40 100644 --- a/common/src/main/resources/sable.mixins.json +++ b/common/src/main/resources/sable.mixins.json @@ -16,6 +16,7 @@ "camera.new_camera_types.MinecraftMixin", "clip_overwrite.ClientLevelMixin", "clip_overwrite.GameRendererMixin", + "command.ClientSuggestionProviderMixin", "compatibility.iris.ExtendedShaderMixin", "compatibility.shouldersurfing.ObjectPickerMixin", "conduit.ConduitRendererMixin", @@ -157,6 +158,7 @@ "entity.entity_rotations_and_riding.EntityTypeMixin", "entity.entity_rotations_and_riding.LivingEntityMixin", "entity.entity_rotations_and_riding.PlayerMixin", + "entity.entity_rotations_and_riding.ProjectileUtilMixin", "entity.entity_rotations_and_riding.ServerEntityMixin", "entity.entity_rotations_and_riding.ServerPlayerMixin", "entity.entity_sublevel_collision.AbstractMinecartMixin", diff --git a/gradle.properties b/gradle.properties index d7f3cf59..d888b135 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=2.0.3 +version=2.0.4 group=dev.ryanhcode.sable java_version=21 @@ -32,8 +32,8 @@ neoforge_loader_version_range=[4,) # Dependencies sable_companion_version=1.6.0 forgeconfigapiport_version=21.1.3 -veil_version=4.1.4 -imguimc_version=1.1.0 +veil_version=4.3.2 +imguimc_version=2.0.0 ## Create create_version=6.0.10-280 diff --git a/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/mixin/compatibility/create/behaviour_compatibility/block_breaking_behaviour/BlockBreakingMovementBehaviourMixin.java b/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/mixin/compatibility/create/behaviour_compatibility/block_breaking_behaviour/BlockBreakingMovementBehaviourMixin.java index 321f9bc7..97570110 100644 --- a/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/mixin/compatibility/create/behaviour_compatibility/block_breaking_behaviour/BlockBreakingMovementBehaviourMixin.java +++ b/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/mixin/compatibility/create/behaviour_compatibility/block_breaking_behaviour/BlockBreakingMovementBehaviourMixin.java @@ -7,14 +7,20 @@ import com.simibubi.create.content.kinetics.base.BlockBreakingMovementBehaviour; import dev.ryanhcode.sable.ActiveSableCompanion; import dev.ryanhcode.sable.Sable; +import dev.ryanhcode.sable.companion.math.JOMLConversion; import dev.ryanhcode.sable.neoforge.mixinhelper.compatibility.create.block_breakers.SubLevelBlockBreakingUtility; import dev.ryanhcode.sable.sublevel.SubLevel; +import net.createmod.catnip.math.VecHelper; +import net.createmod.catnip.nbt.NBTHelper; import net.minecraft.core.BlockPos; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.NbtUtils; +import net.minecraft.nbt.Tag; +import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.Vec3; +import org.joml.Vector3d; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; @@ -50,45 +56,60 @@ public abstract class BlockBreakingMovementBehaviourMixin implements MovementBeh 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)); + //make sure we're actually starting to break something + if (context.stall && (context.data.contains("BreakingPos"))) { + final Vector3d checkPos = JOMLConversion.toJOML(context.position); - if (blockPos != null) { - final Vec3 localCenter = context.localPos.getCenter(); + //project our position into the real world + final SubLevel parentSublevel = Sable.HELPER.getContaining(context.world, context.position); + if (parentSublevel != null) { + parentSublevel.logicalPose().transformPosition(checkPos); + } + + //project our position into the target's plot + final SubLevel targetSublevel = Sable.HELPER.getContaining(context.world, NbtUtils.readBlockPos(context.data, "BreakingPos").orElseThrow()); + if (targetSublevel != null) { + targetSublevel.logicalPose().transformPositionInverse(checkPos); + } - Vec3 sublevelLocalCenter = context.contraption.entity.toGlobalVector(localCenter, 1); - Vec3 targetCenter = blockPos.getCenter(); + //save the current projected position to check movement distance against + context.data.put("ProjectedPos", VecHelper.writeNBT(JOMLConversion.toMojang(checkPos))); + } + } - final ActiveSableCompanion helper = Sable.HELPER; - final SubLevel parentSublevel = helper.getContaining(context.world, context.contraption.anchor); - final SubLevel targetSubLevel = helper.getContaining(context.world, blockPos); + @Inject(method = "cancelStall", at = @At("TAIL")) + public void sable$removeProjected(final MovementContext context, final CallbackInfo ci) { + context.data.remove("ProjectedPos"); + } - if (parentSublevel != null) { - sublevelLocalCenter = parentSublevel.logicalPose().transformPosition(sublevelLocalCenter); - } + @Inject(method = "tick", at = @At("HEAD"), cancellable = true) + public void sable$testProjectedPosDist(final MovementContext context, final CallbackInfo ci) { + final CompoundTag data = context.data; - if (targetSubLevel != null) { - targetCenter = targetSubLevel.logicalPose().transformPosition(targetCenter); - } + //kind of a work-around to not require injecting into every place where BreakingPos is removed. + if (!context.data.contains("BreakingPos")) { + data.remove("ProjectedPos"); + return; + } - if (sublevelLocalCenter.distanceToSqr(targetCenter) > 2 * 2) { - data.remove("Progress"); - data.remove("TicksUntilNextProgress"); - data.remove("BreakingPos"); - data.remove("LastPos"); - data.remove("WaitingTicks"); + final Vec3 sublevelLocalCenter = context.contraption.entity.toGlobalVector(context.localPos.getCenter(), 1); + if (data.contains("ProjectedPos") && Sable.HELPER.distanceSquaredWithSubLevels(context.world, VecHelper.readNBT(data.getList("ProjectedPos", Tag.TAG_DOUBLE)), sublevelLocalCenter) > 2*2) { + final BlockPos blockPos = NbtUtils.readBlockPos(data, "BreakingPos").orElse(null); - context.stall = false; - context.world.destroyBlockProgress(data.getInt("BreakerId"), blockPos, -1); + data.remove("Progress"); + data.remove("TicksUntilNextProgress"); + data.remove("BreakingPos"); + data.remove("LastPos"); + data.remove("WaitingTicks"); + data.remove("ProjectedPos"); - ci.cancel(); - } + context.stall = false; + if (blockPos != null) { + context.world.destroyBlockProgress(data.getInt("BreakerId"), blockPos, -1); } + + ci.cancel(); } } } diff --git a/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/mixin/compatibility/create/sticker/ContraptionMixin.java b/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/mixin/compatibility/create/sticker/ContraptionMixin.java new file mode 100644 index 00000000..1280f692 --- /dev/null +++ b/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/mixin/compatibility/create/sticker/ContraptionMixin.java @@ -0,0 +1,25 @@ +package dev.ryanhcode.sable.neoforge.mixin.compatibility.create.sticker; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.simibubi.create.content.contraptions.Contraption; +import dev.ryanhcode.sable.neoforge.mixinterface.compatibility.create.StickerBlockEntityExtension; +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.block.entity.BlockEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(Contraption.class) +public class ContraptionMixin { + + @WrapOperation(method = "getBlockEntityNBT", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/entity/BlockEntity;saveWithFullMetadata(Lnet/minecraft/core/HolderLookup$Provider;)Lnet/minecraft/nbt/CompoundTag;")) + private CompoundTag sable$saveStickerNBT(final BlockEntity instance, final HolderLookup.Provider registries, final Operation original) { + if (instance instanceof final StickerBlockEntityExtension extension) { + extension.sable$saveToContraption(registries); + } + return original.call(instance, registries); + } + + +} diff --git a/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/mixin/compatibility/create/sticker/StickerBlockEntityMixin.java b/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/mixin/compatibility/create/sticker/StickerBlockEntityMixin.java index 76094ee1..7623a105 100644 --- a/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/mixin/compatibility/create/sticker/StickerBlockEntityMixin.java +++ b/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/mixin/compatibility/create/sticker/StickerBlockEntityMixin.java @@ -64,6 +64,12 @@ public abstract class StickerBlockEntityMixin extends SmartBlockEntity implement @Unique private boolean sable$hasConstraint; + /** + * kinda hacky but we ball + */ + @Unique + private boolean sable$doNotSaveConstraint; + private StickerBlockEntityMixin(final BlockEntityType type, final BlockPos pos, final BlockState state) { super(type, pos, state); } @@ -250,11 +256,18 @@ public void tick(final CallbackInfo ci) { this.sable$tickConstraint(); } + @Override + public void sable$saveToContraption(final HolderLookup.Provider registries) { + this.sable$doNotSaveConstraint = true; + this.saveWithFullMetadata(registries); + this.sable$doNotSaveConstraint = false; + } + @Inject(method = "write", at = @At("TAIL")) public void write(final CompoundTag compound, final HolderLookup.Provider registries, final boolean clientPacket, final CallbackInfo ci) { if (clientPacket) { compound.putBoolean("SableHasConstraint", this.sable$handle != null); - } else if (this.sable$handle != null) { + } else if (this.sable$handle != null && !this.sable$doNotSaveConstraint) { final CompoundTag constraint = new CompoundTag(); final BlockPos blockPos = this.getBlockPos(); constraint.putInt("ThisX", blockPos.getX()); diff --git a/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/mixinterface/compatibility/create/StickerBlockEntityExtension.java b/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/mixinterface/compatibility/create/StickerBlockEntityExtension.java index 44d73b30..70412cf8 100644 --- a/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/mixinterface/compatibility/create/StickerBlockEntityExtension.java +++ b/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/mixinterface/compatibility/create/StickerBlockEntityExtension.java @@ -1,9 +1,13 @@ package dev.ryanhcode.sable.neoforge.mixinterface.compatibility.create; +import net.minecraft.core.HolderLookup; + public interface StickerBlockEntityExtension { void sable$removeConstraint(); void sable$tickConstraint(); + + void sable$saveToContraption(HolderLookup.Provider registries); } diff --git a/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/neoforge/src/main/resources/META-INF/neoforge.mods.toml index eb12e467..8e7ba524 100644 --- a/neoforge/src/main/resources/META-INF/neoforge.mods.toml +++ b/neoforge/src/main/resources/META-INF/neoforge.mods.toml @@ -68,7 +68,3 @@ reason = "${mod_name} supports Sodium ${sodium_version} and above" [[dependencies.sable]] modId = "scalablelux" type = "incompatible" - -[[dependencies.sable]] -modId = "littletiles" -type = "incompatible" diff --git a/neoforge/src/main/resources/sable-neoforge.mixins.json b/neoforge/src/main/resources/sable-neoforge.mixins.json index b48d6e2e..2c905b21 100644 --- a/neoforge/src/main/resources/sable-neoforge.mixins.json +++ b/neoforge/src/main/resources/sable-neoforge.mixins.json @@ -146,6 +146,7 @@ "compatibility.create.schematics.SchematicPrinterMixin", "compatibility.create.schematics.SchematicToolBaseMixin", "compatibility.create.schematics.StructureTemplateMixin", + "compatibility.create.sticker.ContraptionMixin", "compatibility.create.sticker.StickerBlockEntityMixin", "compatibility.create.sticker.StickerBlockMixin", "compatibility.create.stock_ticker.StockTickerInteractionHandlerMixin",