From 62455f6ee6118b6885aa0867a6a71bea59798896 Mon Sep 17 00:00:00 2001 From: BeeIsYou Date: Wed, 22 Jul 2026 21:46:07 +0800 Subject: [PATCH 01/17] Allow UUIDs to be used to target sub levels in commands --- .../api/command/SubLevelArgumentType.java | 126 ++++++----- .../ryanhcode/sable/command/SableCommand.java | 196 ++++++++++-------- .../sable/command/SableStorageCommands.java | 17 +- .../argument/SubLevelSelectorModifiers.java | 2 +- .../argument/SubLevelSuggestionProvider.java | 10 + .../argument/selector/SubLevelTarget.java | 11 + .../argument/selector/SubLevelTargetNone.java | 17 ++ .../SubLevelTargetSelector.java} | 18 +- .../argument/selector/SubLevelTargetUUID.java | 30 +++ .../ClientSuggestionProviderMixin.java | 26 +++ .../resources/assets/sable/lang/en_us.json | 4 +- common/src/main/resources/sable.mixins.json | 1 + 12 files changed, 301 insertions(+), 157 deletions(-) create mode 100644 common/src/main/java/dev/ryanhcode/sable/command/argument/SubLevelSuggestionProvider.java create mode 100644 common/src/main/java/dev/ryanhcode/sable/command/argument/selector/SubLevelTarget.java create mode 100644 common/src/main/java/dev/ryanhcode/sable/command/argument/selector/SubLevelTargetNone.java rename common/src/main/java/dev/ryanhcode/sable/command/argument/{SubLevelSelector.java => selector/SubLevelTargetSelector.java} (92%) create mode 100644 common/src/main/java/dev/ryanhcode/sable/command/argument/selector/SubLevelTargetUUID.java create mode 100644 common/src/main/java/dev/ryanhcode/sable/mixin/command/ClientSuggestionProviderMixin.java 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/command/SableCommand.java b/common/src/main/java/dev/ryanhcode/sable/command/SableCommand.java index 90fc0f01..604ed470 100644 --- a/common/src/main/java/dev/ryanhcode/sable/command/SableCommand.java +++ b/common/src/main/java/dev/ryanhcode/sable/command/SableCommand.java @@ -53,15 +53,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 +67,29 @@ 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("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 +116,92 @@ private static int executeSetPhysicsPausedCommand(final CommandContext Component.translatable("commands.sable.physics.paused.success", Boolean.toString(pause)), true); return 1; } + + 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..f3d2a414 100644 --- a/common/src/main/java/dev/ryanhcode/sable/command/SableStorageCommands.java +++ b/common/src/main/java/dev/ryanhcode/sable/command/SableStorageCommands.java @@ -135,27 +135,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/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/resources/assets/sable/lang/en_us.json b/common/src/main/resources/assets/sable/lang/en_us.json index 1e084382..024d060c 100644 --- a/common/src/main/resources/assets/sable/lang/en_us.json +++ b/common/src/main/resources/assets/sable/lang/en_us.json @@ -108,7 +108,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..a2d77709 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", From bf9713a6430914545113572d1535bfed531a889f Mon Sep 17 00:00:00 2001 From: BeeIsYou Date: Wed, 22 Jul 2026 23:11:28 +0800 Subject: [PATCH 02/17] Network custom dimension physics to client --- .../ryanhcode/sable/SableCommonEvents.java | 2 + .../ClientboundDimensionPhysicsPacket.java | 47 +++++++++++++++++++ .../sable/network/tcp/SableTCPPackets.java | 2 + .../BezierResourceFunction.java | 15 ++++++ .../dimension_physics/DimensionPhysics.java | 26 ++++++++++ .../DimensionPhysicsData.java | 41 ++++++++++------ 6 files changed, 119 insertions(+), 14 deletions(-) create mode 100644 common/src/main/java/dev/ryanhcode/sable/network/packets/tcp/ClientboundDimensionPhysicsPacket.java 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/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()); } From ba6f8b629283f3ac7a319e8489912bcef4d19019 Mon Sep 17 00:00:00 2001 From: Tazer <85909065+tazercopter@users.noreply.github.com> Date: Fri, 31 Jul 2026 21:29:37 +0300 Subject: [PATCH 03/17] use ModifyReturnValue to optimise some mixins (#1415) Mostly inconsequential except for some heavy-use methods such as in WorldBorder or ServerLevel/ClientLevel --- .../camera/camera_rotation/EntityMixin.java | 12 ++--- .../entity_aabb_lookup/ClientLevelMixin.java | 10 ++--- .../entity_aabb_lookup/ServerLevelMixin.java | 11 ++--- .../entity/entity_kicking/BlockMixin.java | 7 ++- .../entity/entity_pathfinding/PathMixin.java | 44 ++++++++----------- .../mixin/options/OptionsScreenMixin.java | 13 +++--- .../sable/mixin/plot/ChunkMapMixin.java | 13 ++++-- .../mixin/plot/ServerChunkCacheMixin.java | 25 ++++++----- .../sable/mixin/plot/ServerLevelMixin.java | 20 +++++---- .../mixin/water_occlusion/CameraMixin.java | 15 ++++--- .../mixin/world_border/WorldBorderMixin.java | 26 +++++------ 11 files changed, 94 insertions(+), 102 deletions(-) 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/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/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) From e29b449702db8ce4d8d5d517aba51b362ff06436 Mon Sep 17 00:00:00 2001 From: RyanHCode <42245712+ryanhcode@users.noreply.github.com> Date: Fri, 31 Jul 2026 23:53:23 -0400 Subject: [PATCH 04/17] Update Veil to fix shader pre-processors getting skipped with Iris installed --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index d7f3cf59..3ece6d4f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 From 0270bc3df68e96ec9572dbec3d64b36ea116cc27 Mon Sep 17 00:00:00 2001 From: RyanHCode <42245712+ryanhcode@users.noreply.github.com> Date: Sat, 1 Aug 2026 01:13:38 -0400 Subject: [PATCH 05/17] Add primitive storage pruning on save --- .../api/sublevel/ServerSubLevelContainer.java | 5 ++ .../ticket/SubLevelLoadingTicket.java | 32 ++---------- .../ryanhcode/sable/command/SableCommand.java | 52 +++++++++++++++++-- .../sable/command/SableStorageCommands.java | 43 +++++++++++++++ .../storage/SubLevelTicketsSavedData.java | 4 +- .../storage/holding/SubLevelHoldingChunk.java | 4 ++ .../holding/SubLevelHoldingChunkMap.java | 12 ++++- .../storage/region/SubLevelRegionFile.java | 8 +++ .../storage/region/SubLevelStorageFile.java | 46 +++++++++------- .../serialization/SubLevelStorage.java | 51 +++++++++++++++++- .../resources/assets/sable/lang/en_us.json | 5 ++ 11 files changed, 207 insertions(+), 55 deletions(-) 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/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 604ed470..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 { @@ -69,6 +68,8 @@ public static void register(final CommandDispatcher dispatch .then(Commands.literal("forceload") .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))) ) @@ -117,6 +118,51 @@ private static int executeSetPhysicsPausedCommand(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); 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 f3d2a414..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(); 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..fdcdedef 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 @@ -208,6 +208,10 @@ private ByteBuffer createExternalStub() { return byteBuffer; } + public boolean isEmpty() { + return this.usedSectors.length() <= (this.beginningSectorSize / this.sectorSize); + } + /** * Writes a sub-levels data to disk * @@ -377,6 +381,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,26 +415,14 @@ public void close() throws IOException { } } - public void flush() throws IOException { - this.file.force(true); + public void delete() throws IOException { + this.file.close(); + Files.delete(this.path); + Files.deleteIfExists(this.externalFileDir); } - 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); - } - } + public void flush() throws IOException { + this.file.force(true); } class SectorSpanDataBuffer extends ByteArrayOutputStream { 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 024d060c..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", From 22b8ce976dc2877eae3a1a4d2646a57d1ea559ff Mon Sep 17 00:00:00 2001 From: RyanHCode <42245712+ryanhcode@users.noreply.github.com> Date: Sat, 1 Aug 2026 01:16:17 -0400 Subject: [PATCH 06/17] Bump to 2.0.4 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 3ece6d4f..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 From 550e644ab2eda5ab0f9c2c66ca252866256d9642 Mon Sep 17 00:00:00 2001 From: cyvack Date: Sat, 1 Aug 2026 00:15:46 -0700 Subject: [PATCH 07/17] Make BlockBreakingMovementBehaviourMixin more robust Instead of checking the current actor origin against the current block being broken, we check the current actor origin against the origin when it started breaking the block. This ensures that no matter how large of an area the actor is breaking (IE. rollers, drills from addons that break in an absurd 10x10 area, etc), breaking will still occur until the actor is moved suffeciently far away. --- .../BlockBreakingMovementBehaviourMixin.java | 79 ++++++++++++------- 1 file changed, 50 insertions(+), 29 deletions(-) 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(); } } } From 8ec2afab2253a2e2a6854cbef1ea815551cb4c88 Mon Sep 17 00:00:00 2001 From: CreativeMD Date: Fri, 7 Aug 2026 23:48:10 +0200 Subject: [PATCH 08/17] Removed LittleTiles as incompatibility (#1444) --- neoforge/src/main/resources/META-INF/neoforge.mods.toml | 4 ---- 1 file changed, 4 deletions(-) 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" From 4a26b4cf710a9144ca01b36bd0d7ed9ebcb0dcc9 Mon Sep 17 00:00:00 2001 From: RyanHCode <42245712+ryanhcode@users.noreply.github.com> Date: Wed, 12 Aug 2026 16:32:13 -0400 Subject: [PATCH 09/17] fix more storage file things --- .../api/physics/force/QueuedForceGroup.java | 1 + .../FloatingBlockController.java | 69 +++++++++---------- .../storage/region/SubLevelStorageFile.java | 59 ++++++++++++++-- 3 files changed, 88 insertions(+), 41 deletions(-) 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/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/storage/region/SubLevelStorageFile.java b/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/region/SubLevelStorageFile.java index fdcdedef..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. @@ -255,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 @@ -355,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)); } } @@ -415,10 +422,50 @@ public void close() throws IOException { } } - public void delete() throws IOException { - this.file.close(); - Files.delete(this.path); - Files.deleteIfExists(this.externalFileDir); + 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(); + } + + /** + * Attempts to delete all the external storage files + */ + private void deleteExternalFiles() { + if (!Files.isDirectory(this.externalFileDir)) { + return; + } + + 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); + } + + 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 { From ee0028700183081ced0b3cb18376a86eed4776a7 Mon Sep 17 00:00:00 2001 From: RyanHCode <42245712+ryanhcode@users.noreply.github.com> Date: Wed, 12 Aug 2026 16:43:00 -0400 Subject: [PATCH 10/17] Fix stickers incorrectly retaining their constraint data when saved to Create contraptions (sorry for all the buggy builds) --- .../create/sticker/ContraptionMixin.java | 25 +++++++++++++++++++ .../sticker/StickerBlockEntityMixin.java | 15 ++++++++++- .../create/StickerBlockEntityExtension.java | 4 +++ .../main/resources/sable-neoforge.mixins.json | 1 + 4 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 neoforge/src/main/java/dev/ryanhcode/sable/neoforge/mixin/compatibility/create/sticker/ContraptionMixin.java 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/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", From 1b5c7580930112f103f6a438f3aae230c9589457 Mon Sep 17 00:00:00 2001 From: RyanHCode <42245712+ryanhcode@users.noreply.github.com> Date: Wed, 12 Aug 2026 16:46:16 -0400 Subject: [PATCH 11/17] Small serialization fixes --- .../sable/api/sublevel/SubLevelTicketLoadingSystem.java | 7 +++++-- .../sublevel/storage/serialization/SubLevelSerializer.java | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) 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/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; } From 76b67ea46bb6b21997b012e709b607b40fd31769 Mon Sep 17 00:00:00 2001 From: Ryan H <42245712+ryanhcode@users.noreply.github.com> Date: Thu, 13 Aug 2026 10:13:26 -0400 Subject: [PATCH 12/17] Further clarify the CLA --- CONTRIBUTING.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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. From ce976ebb71fcfb7edb5afab50e54fcd9995ce2ef Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Aug 2026 23:56:07 +0000 Subject: [PATCH 13/17] Re-orient entity collision box per sub-level and substep The entity's OBB yaw was computed from subLevelPose before that variable is assigned for the current substep and sub-level, so the collision box was oriented to the previous substep's pose - or to a different sub-level entirely when several are loaded. On rotated sub-levels this makes entities catch on walls and doorframes that their (correctly oriented) box should clear. Compute the orientation inside the per-sub-level loop, right after the substep pose lerp. The custom-orientation composition matches transformEntityBoundingBox; the up-direction and bounds-center handling that also performs depends only on the custom orientation, not the yaw, so it does not need to be redone here. Co-Authored-By: Claude Fable 5 Co-Authored-By: Claude Opus 5 --- .../entity_collision/SubLevelEntityCollision.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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..ae2df0b4 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); From 96609c2a750d62602c95643b1c47a843af16ae30 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Aug 2026 23:56:21 +0000 Subject: [PATCH 14/17] Implement surface-relative entity orientation up to 45 degrees Fill in the stubbed EntitySubLevelUtil.getCustomEntityOrientation: entities standing on (tracking) a tilted sub-level adopt the tilt (swing) component of its orientation, keeping their own yaw, between 1 and 45 degrees of tilt. Below the lower bound the sub-level is treated as flat and vanilla behavior applies; above the upper bound the entity stays upright rather than being pitched onto its side. The orientation is decomposed swing-twist about world Y so the twist - the sub-level's own heading - is discarded: a deck rotating underfoot must not drag the entity's heading with it. All existing custom-orientation plumbing (rendering, shadows, camera, collision OBB, jumping, effects) engages through this stub, so filling it in is what makes surface orientation live. Co-Authored-By: Claude Fable 5 Co-Authored-By: Claude Opus 5 --- .../sable/api/entity/EntitySubLevelUtil.java | 47 ++++++++++++++++++- 1 file changed, 45 insertions(+), 2 deletions(-) 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; } } From 2c6c0a2a712dbda3a392fc96b81e3c88da8fb7a9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Aug 2026 23:56:54 +0000 Subject: [PATCH 15/17] Keep surface-oriented entities stable on tilted ground Three fixes that together make standing, walking and sneaking on a tilted sub-level behave the way flat ground does. Gravity press (LivingEntityMixin): grounded on a tilted surface, world-frame gravity leaves a tangential remainder every tick (~g*sin(tilt)) that vanilla never absorbs, because vanilla floors are always horizontal - entities creep downhill with no input. Simply not applying gravity breaks contact: the constant press into the floor is what keeps ground detection engaged, and without it motion decays to zero, contact is lost, and the entity enters a fall-catch-slide cycle. Aim the press along the surface normal instead, so contact stays engaged and the ground resolution absorbs it exactly. Rising jump ticks keep plain world gravity, detected by the velocity component along the SURFACE normal rather than world Y - walking uphill has positive world-Y and would otherwise be misread as a jump, starving the contact press for a tick and letting world gravity stutter in. An earlier iteration of this instead absorbed sub-threshold tangential motion in Block.updateEntityAfterFallOn. That only ran on landing, so it could not hold a standing entity in place, and it is superseded by the press model above. Position reconstruction (SubLevelEntityCollision): reconstruct the entity position as the exact inverse of how entityBoundsCenter was derived from it. The previous reconstruction subtracted half the body height along entityUp, which under a custom orientation displaces the entity laterally by ~eyeHeight*sin(tilt) every tick - a phantom conveyor on any tilted surface. Pose fitting (PlayerMixin): vanilla's canPlayerFitWithinBlocksAndEntities check tests the upright, world-aligned box, but a surface-oriented player's real box is tilted with the surface and clears geometry the upright box cannot, so the check spuriously forces the crawling pose in doorways. Skip the pose demotion while surface-oriented; physical obstruction is still enforced by the oriented collision. Co-Authored-By: Claude Fable 5 Co-Authored-By: Claude Opus 5 --- .../LivingEntityMixin.java | 30 +++++++++++++++++++ .../PlayerMixin.java | 14 +++++++++ .../SubLevelEntityCollision.java | 14 ++++++++- 3 files changed, 57 insertions(+), 1 deletion(-) 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/sublevel/entity_collision/SubLevelEntityCollision.java b/common/src/main/java/dev/ryanhcode/sable/sublevel/entity_collision/SubLevelEntityCollision.java index ae2df0b4..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 @@ -255,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; From a10074de8ee8ed411b743188fdb22356abe01482 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Aug 2026 23:58:18 +0000 Subject: [PATCH 16/17] Make picking and entity lighting respect surface orientation Picking (ProjectileUtilMixin): entity picking clips the look ray against the target's world-aligned AABB, but a surface-oriented entity renders tilted, so aiming at the visible model can miss the box. AABBs cannot rotate, so counter-rotate the ray into the entity's frame around its feet pivot (matching the render pivot), clip against the plain box, and transform the hit back - geometrically identical to picking an oriented box. Entities without a custom orientation take the original call unchanged. The candidate loop hands a null entity through ProjectileUtil, so the orientation lookup is null-guarded both here and at the API; without it this NPEs on the render thread. Lighting (EntityRendererMixin): probe from the oriented body center rather than the oriented eye - on tilted decks the tilted eye lever can lean the probe into adjacent geometry, sampling zero light and rendering the entity black. The body center is inside the entity's own volume and so is always in open space. Also require the probe to land in a sub-level plot's AIR before letting that sub-level darken the entity's sky light. 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 cannot really occupy - and its sky=0 would otherwise min() the entity to pitch black. Landing in air is the genuine interior-shadow case. Co-Authored-By: Claude Fable 5 Co-Authored-By: Claude Opus 5 --- .../entity_rendering/EntityRendererMixin.java | 23 +++++++- .../ProjectileUtilMixin.java | 57 +++++++++++++++++++ common/src/main/resources/sable.mixins.json | 1 + 3 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_rotations_and_riding/ProjectileUtilMixin.java 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/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/resources/sable.mixins.json b/common/src/main/resources/sable.mixins.json index a2d77709..447f2f40 100644 --- a/common/src/main/resources/sable.mixins.json +++ b/common/src/main/resources/sable.mixins.json @@ -158,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", From 4d276d6b9f7bd7b9bf17d510146a8dd93e2d5b1b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Aug 2026 23:51:08 +0000 Subject: [PATCH 17/17] Archive collide debug ledger for surface-oriented entities The per-tick [collide dbg] logging in SubLevelEntityCollision.collide is not shippable (fires every tick, client-side, per surface-oriented player), but it is the fastest way to inspect the surface-orientation force model when that work resumes. Record the snippet, its insertion point, the meaning of each field and the locals it depends on, so it can be dropped back in without digging through the feature branch. Co-Authored-By: Claude Opus 5 (cherry picked from commit afec269962f0c72e096b7805c2ed9f55f8059c6d) --- .../collide-surface-orientation.md | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 .dev/debug-snippets/collide-surface-orientation.md 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.