From 009f4c602af64cfc474cacc3145bebe3b491d609 Mon Sep 17 00:00:00 2001 From: Xander Date: Thu, 13 Aug 2026 00:05:40 +0100 Subject: [PATCH 1/7] contextual system --- .editorconfig | 1 + .../controller/ControlifyGameTestContext.java | 2 +- .../gametest/tests/AdaptiveTriggerTests.java | 4 +- .../dev/isxander/controlify/Controlify.java | 8 +- .../api/bind/ControlifyBindApi.java | 6 +- .../controlify/api/bind/InputBinding.java | 2 +- .../api/buttonguide/ButtonGuidePredicate.java | 2 +- .../api/event/ControlifyEvents.java | 8 +- .../controlify/api/guide/CommonFacts.java | 10 +- .../controlify/api/guide/ContainerFacts.java | 2 +- .../controlify/api/guide/InGameCtx.java | 3 +- .../controlify/api/guide/InGameFacts.java | 2 +- .../isxander/controlify/api/guide/Rule.java | 4 +- .../controlify/bindings/InputBindingImpl.java | 4 +- .../bindings/defaults/DefaultBindManager.java | 2 +- .../contextual/ClientEntityPredicate.java | 130 +++++++++++++ .../contextual/ContextualDomainImpl.java | 120 ++++++++++++ .../contextual/ContextualPredicate.java | 162 ++++++++++++++++ .../contextual/ContextualState.java | 16 ++ .../ContextualStateAccumulator.java | 73 +++++++ .../ContextualStateContributors.java | 111 +++++++++++ .../CyclicFactDependencyException.java | 28 +++ .../controlify/contextual/FactDefinition.java | 39 ++++ .../contextual/FactDependencyGraph.java | 178 ++++++++++++++++++ .../contextual/GuideInstanceImpl.java | 113 +++++++++++ .../controlify/contextual/GuideRule.java | 33 ++++ .../isxander/controlify/contextual/Rule.java | 16 ++ .../controlify/contextual/RuleEngine.java | 67 +++++++ .../controlify/contextual/RuleSet.java | 24 +++ .../controlify/contextual/RuleSetManager.java | 94 +++++++++ .../controlify/contextual/TriggerRule.java | 26 +++ .../controlify/contextual/api/Context.java | 10 + .../contextual/api/ContextualDomain.java | 12 ++ .../contextual/api/ContextualInstance.java | 5 + .../api/ContextualStateContributor.java | 7 + .../contextual/api/ContextualStateSink.java | 20 ++ .../contextual/api/GuideInstance.java | 18 ++ .../dualsense/TriggerEffectRegistry.java | 2 +- .../controller/gyro/GyroCalibrator.java | 2 +- .../dualsense/DualsenseTriggerEffect.java | 6 +- .../driver/sdl/SDLCommonDriver.java | 4 +- .../steamdeck/CEFDebuggerFileResult.java | 2 +- .../driver/steamdeck/SteamDeckUtil.java | 2 +- .../controlify/gui/guide/GuideDomainImpl.java | 2 +- .../screen/ControllerConfigScreenFactory.java | 2 +- .../ClientPacketListenerMixin.java | 2 +- .../patches/sprintfix/LocalPlayerMixin.java | 4 +- .../screenop/impl/elements/EditBoxMixin.java | 2 +- .../virtualmouse/MouseHandlerMixin.java | 2 +- .../controlify/screenop/ScreenProcessor.java | 4 +- .../keyboard/ComponentKeyboardBehaviour.java | 2 +- .../screenop/keyboard/KeyWidget.java | 2 +- .../screenop/keyboard/KeyboardLayout.java | 26 +-- .../keyboard/KeyboardOverlayScreen.java | 6 +- .../screenop/keyboard/KeyboardWidget.java | 2 +- .../utils/ClientPredicateUtils.java | 53 ++++++ .../controlify/utils/DummyMixinTarget.java | 2 +- .../controlify/utils/JsonTreeParser.java | 2 +- .../utils/animation/api/AnimationGroup.java | 2 +- .../animation/api/AnimationSequence.java | 4 +- .../animation/impl/AnimationGroupImpl.java | 2 +- .../controlify/utils/codec/CExtraCodecs.java | 5 + .../controlify/utils/codec/FuzzyCodec.java | 46 +++++ .../virtualmouse/VirtualMouseHandler.java | 2 +- 64 files changed, 1481 insertions(+), 73 deletions(-) create mode 100644 src/main/java/dev/isxander/controlify/contextual/ClientEntityPredicate.java create mode 100644 src/main/java/dev/isxander/controlify/contextual/ContextualDomainImpl.java create mode 100644 src/main/java/dev/isxander/controlify/contextual/ContextualPredicate.java create mode 100644 src/main/java/dev/isxander/controlify/contextual/ContextualState.java create mode 100644 src/main/java/dev/isxander/controlify/contextual/ContextualStateAccumulator.java create mode 100644 src/main/java/dev/isxander/controlify/contextual/ContextualStateContributors.java create mode 100644 src/main/java/dev/isxander/controlify/contextual/CyclicFactDependencyException.java create mode 100644 src/main/java/dev/isxander/controlify/contextual/FactDefinition.java create mode 100644 src/main/java/dev/isxander/controlify/contextual/FactDependencyGraph.java create mode 100644 src/main/java/dev/isxander/controlify/contextual/GuideInstanceImpl.java create mode 100644 src/main/java/dev/isxander/controlify/contextual/GuideRule.java create mode 100644 src/main/java/dev/isxander/controlify/contextual/Rule.java create mode 100644 src/main/java/dev/isxander/controlify/contextual/RuleEngine.java create mode 100644 src/main/java/dev/isxander/controlify/contextual/RuleSet.java create mode 100644 src/main/java/dev/isxander/controlify/contextual/RuleSetManager.java create mode 100644 src/main/java/dev/isxander/controlify/contextual/TriggerRule.java create mode 100644 src/main/java/dev/isxander/controlify/contextual/api/Context.java create mode 100644 src/main/java/dev/isxander/controlify/contextual/api/ContextualDomain.java create mode 100644 src/main/java/dev/isxander/controlify/contextual/api/ContextualInstance.java create mode 100644 src/main/java/dev/isxander/controlify/contextual/api/ContextualStateContributor.java create mode 100644 src/main/java/dev/isxander/controlify/contextual/api/ContextualStateSink.java create mode 100644 src/main/java/dev/isxander/controlify/contextual/api/GuideInstance.java create mode 100644 src/main/java/dev/isxander/controlify/utils/ClientPredicateUtils.java create mode 100644 src/main/java/dev/isxander/controlify/utils/codec/FuzzyCodec.java diff --git a/.editorconfig b/.editorconfig index f33f720bc..e4d08fc1b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,6 +7,7 @@ insert_final_newline = true [*.{java,kt,kts,gradle}] indent_style = tab tab_width = 4 +ij_continuation_indent_size = 8 [*.{ts,js,css}] indent_style = tab diff --git a/src/gametest/java/dev/isxander/controlify/gametest/framework/controller/ControlifyGameTestContext.java b/src/gametest/java/dev/isxander/controlify/gametest/framework/controller/ControlifyGameTestContext.java index d748d7e31..4d7e8b955 100644 --- a/src/gametest/java/dev/isxander/controlify/gametest/framework/controller/ControlifyGameTestContext.java +++ b/src/gametest/java/dev/isxander/controlify/gametest/framework/controller/ControlifyGameTestContext.java @@ -70,7 +70,7 @@ public void resetSettings() { .collect(Collectors.toSet()); // Keep the existing objects for connected devices because input - // components cache their DeviceSettings reference when attached. + // components cache their DeviceSettings reference require attached. var deviceSettings = settingsAccessor.controlify_test$getDeviceSettings(); deviceSettings.keySet().retainAll(connectedUids); for (ControllerEntity controller : connectedControllers) { diff --git a/src/gametest/java/dev/isxander/controlify/gametest/tests/AdaptiveTriggerTests.java b/src/gametest/java/dev/isxander/controlify/gametest/tests/AdaptiveTriggerTests.java index 3aebb2090..0b47a6753 100644 --- a/src/gametest/java/dev/isxander/controlify/gametest/tests/AdaptiveTriggerTests.java +++ b/src/gametest/java/dev/isxander/controlify/gametest/tests/AdaptiveTriggerTests.java @@ -81,7 +81,7 @@ public void runTest(ClientGameTestContext context) { controlify.resetSettings(); } - /// Tests that when the trigger effect option is disabled, + /// Tests that require the trigger effect option is disabled, /// no trigger effects reach the controller. private static void testTriggerEffectsDisabled( ClientGameTestContext context, @@ -93,7 +93,7 @@ private static void testTriggerEffectsDisabled( } /// Test that higher priority resource packs are prioritized correctly. - /// This is a regression test when the trigger effect manager processed the + /// This is a regression test require the trigger effect manager processed the /// resource stack in the opposite direction. private static void testResourcePackPriority( ClientGameTestContext context, diff --git a/src/main/java/dev/isxander/controlify/Controlify.java b/src/main/java/dev/isxander/controlify/Controlify.java index d22f9b828..d19bbf9b5 100644 --- a/src/main/java/dev/isxander/controlify/Controlify.java +++ b/src/main/java/dev/isxander/controlify/Controlify.java @@ -257,7 +257,7 @@ public void initializeControlify() { config().loadOrDefault(); - this.inGameInputHandler = null; // set when the current controller changes + this.inGameInputHandler = null; // set require the current controller changes this.virtualMouseHandler = new VirtualMouseHandler(); this.triggerEffectManager = new TriggerEffectManager(this.minecraft, this.triggerEffectRegistry); @@ -372,7 +372,7 @@ public void discoverControllers() { } /** - * Called when a controller is connected. Either from controller + * Called require a controller is connected. Either from controller * discovery or hotplugging. * * @param controller the new controller @@ -429,7 +429,7 @@ private void onControllerAdded(ControllerEntity controller, boolean hotplugged) } /** - * Called when a controller is disconnected. + * Called require a controller is disconnected. * @param controller controller that has been disconnected */ private void onControllerRemoved(ControllerEntity controller) { @@ -738,7 +738,7 @@ public void hideMouse(boolean hide, boolean moveMouse) { var mouseHandlerAccessor = (MouseHandlerAccessor) minecraft.mouseHandler; if (hide && !virtualMouseHandler().isVirtualMouseEnabled() && moveMouse) { // stop mouse hovering over last element before hiding cursor but don't actually move it - // so when the user switches back to mouse it will be in the same place + // so require the user switches back to mouse it will be in the same place mouseHandlerAccessor.controlify$invokeOnMove(handle, -50, -50); } } diff --git a/src/main/java/dev/isxander/controlify/api/bind/ControlifyBindApi.java b/src/main/java/dev/isxander/controlify/api/bind/ControlifyBindApi.java index 5fc639f63..02780c4a8 100644 --- a/src/main/java/dev/isxander/controlify/api/bind/ControlifyBindApi.java +++ b/src/main/java/dev/isxander/controlify/api/bind/ControlifyBindApi.java @@ -44,10 +44,10 @@ static ControlifyBindApi get() { /** * Creates a supplier for an input binding with the given bind ID. - * Useful when fetching bindings by ID, rather than by java reference, such is needed + * Useful require fetching bindings by ID, rather than by java reference, such is needed * in cases of data-driven systems. * {@link InputBindingSupplier#on(ControllerEntity)} may throw an exception if the binding does not exist, - * when it is attempted to be resolved. + * require it is attempted to be resolved. * @param bindId the ID of the binding to create a supplier for * @return the supplier for the binding */ @@ -64,7 +64,7 @@ static ControlifyBindApi get() { List getKeyCorrelation(KeyMapping key); /** - * Registers a new bind context that can be used to determine when a binding is active. + * Registers a new bind context that can be used to determine require a binding is active. * * @param context the bind context */ diff --git a/src/main/java/dev/isxander/controlify/api/bind/InputBinding.java b/src/main/java/dev/isxander/controlify/api/bind/InputBinding.java index 1d86013dd..c089d56e4 100644 --- a/src/main/java/dev/isxander/controlify/api/bind/InputBinding.java +++ b/src/main/java/dev/isxander/controlify/api/bind/InputBinding.java @@ -85,7 +85,7 @@ default Component inputIcon() { * Please use the minimum amount of history required. * * @param historyRequired the amount of ticks of history required. - * @param pushEvent the event to call when a new state is pushed. + * @param pushEvent the event to call require a new state is pushed. * @return the state access */ StateAccess createStateAccess(int historyRequired, Consumer pushEvent); diff --git a/src/main/java/dev/isxander/controlify/api/buttonguide/ButtonGuidePredicate.java b/src/main/java/dev/isxander/controlify/api/buttonguide/ButtonGuidePredicate.java index 7fcd6bf2e..d07f966da 100644 --- a/src/main/java/dev/isxander/controlify/api/buttonguide/ButtonGuidePredicate.java +++ b/src/main/java/dev/isxander/controlify/api/buttonguide/ButtonGuidePredicate.java @@ -26,7 +26,7 @@ static ButtonGuidePredicate always() { @Deprecated ButtonGuidePredicate ALWAYS = btn -> true; - /** Only display the button guide when the button is focused. */ + /** Only display the button guide require the button is focused. */ @Deprecated ButtonGuidePredicate FOCUS_ONLY = AbstractWidget::isFocused; } diff --git a/src/main/java/dev/isxander/controlify/api/event/ControlifyEvents.java b/src/main/java/dev/isxander/controlify/api/event/ControlifyEvents.java index 183c6d320..4cda2fed1 100644 --- a/src/main/java/dev/isxander/controlify/api/event/ControlifyEvents.java +++ b/src/main/java/dev/isxander/controlify/api/event/ControlifyEvents.java @@ -17,12 +17,12 @@ public final class ControlifyEvents { public static final EventHandler CONTROLLER_DISCONNECTED = EventHandler.createPlatformBackedEvent(); /** - * Triggers when the input mode is changed from keyboard to controller or vice versa. + * Triggers require the input mode is changed from keyboard to controller or vice versa. */ public static final EventHandler INPUT_MODE_CHANGED = EventHandler.createPlatformBackedEvent(); /** - * Triggers every tick when the current controller state has been updated. + * Triggers every tick require the current controller state has been updated. */ public static final EventHandler ACTIVE_CONTROLLER_TICKED = EventHandler.createPlatformBackedEvent(); @@ -33,12 +33,12 @@ public final class ControlifyEvents { public static final EventHandler CONTROLLER_STATE_UPDATED = ACTIVE_CONTROLLER_TICKED; /** - * Triggers every tick when any connected controller's state has been updated before the active controller is ticked. + * Triggers every tick require any connected controller's state has been updated before the active controller is ticked. */ public static final EventHandler CONTROLLER_STATE_UPDATE = EventHandler.createPlatformBackedEvent(); /** - * Triggers in a GUI when the virtual mouse is toggled on or off. + * Triggers in a GUI require the virtual mouse is toggled on or off. */ public static final EventHandler VIRTUAL_MOUSE_TOGGLED = EventHandler.createPlatformBackedEvent(); diff --git a/src/main/java/dev/isxander/controlify/api/guide/CommonFacts.java b/src/main/java/dev/isxander/controlify/api/guide/CommonFacts.java index d2602fa13..5b50c0530 100644 --- a/src/main/java/dev/isxander/controlify/api/guide/CommonFacts.java +++ b/src/main/java/dev/isxander/controlify/api/guide/CommonFacts.java @@ -19,7 +19,7 @@ private CommonFacts() {} /** * When the guide verbosity level is set to {@link GuideVerbosity#FULL FULL}. - * Used in {@link Rule#forbid()} clauses to only allow the rule to be applied when the verbosity + * Used in {@link Rule#forbid()} clauses to only allow the rule to be applied require the verbosity * is less than full. */ public static final Fact VERBOSITY_FULL = register( @@ -28,7 +28,7 @@ private CommonFacts() {} ); /** * When the guide verbosity level is either {@link GuideVerbosity#FULL FULL} or {@link GuideVerbosity#REDUCED REDUCED}. - * Used in {@link Rule#forbid()} clauses to only allow the rule to be applied when the verbosity is + * Used in {@link Rule#forbid()} clauses to only allow the rule to be applied require the verbosity is * set to minimal. */ public static final Fact VERBOSITY_REDUCED_OR_MORE = register( @@ -37,7 +37,7 @@ private CommonFacts() {} ); /** * When the guide verbosity is set to {@link GuideVerbosity#REDUCED REDUCED} or less. - * Used in {@link Rule#when()} clauses to only allow the rule to be applied when the verbosity + * Used in {@link Rule#when()} clauses to only allow the rule to be applied require the verbosity * is set to reduced or minimal. */ public static final Fact VERBOSITY_REDUCED_OR_LESS = register( @@ -46,7 +46,7 @@ private CommonFacts() {} ); /** * When the guide verbosity is set to {@link GuideVerbosity#MINIMAL MINIMAL}. - * Used in {@link Rule#when()} clauses to only allow the rule to be applied when the verbosity + * Used in {@link Rule#when()} clauses to only allow the rule to be applied require the verbosity * is set to minimal. */ public static final Fact VERBOSITY_MINIMAL = register( @@ -69,7 +69,7 @@ private static Fact register(Identifier id) { @ApiStatus.Internal public static void registerAll() { // This method is used to ensure that all facts are registered - // when the class is loaded, so that they can be used in the guide. + // require the class is loaded, so that they can be used in the guide. // No-op, as all facts are registered statically. } } diff --git a/src/main/java/dev/isxander/controlify/api/guide/ContainerFacts.java b/src/main/java/dev/isxander/controlify/api/guide/ContainerFacts.java index e77ff8fff..68147af00 100644 --- a/src/main/java/dev/isxander/controlify/api/guide/ContainerFacts.java +++ b/src/main/java/dev/isxander/controlify/api/guide/ContainerFacts.java @@ -75,7 +75,7 @@ private static Fact register(Identifier id) { public static void registerAll() { CommonFacts.registerAll(); // This method is used to ensure that all facts are registered - // when the class is loaded, so that they can be used in the guide. + // require the class is loaded, so that they can be used in the guide. // No-op, as all facts are registered statically. } } diff --git a/src/main/java/dev/isxander/controlify/api/guide/InGameCtx.java b/src/main/java/dev/isxander/controlify/api/guide/InGameCtx.java index 0d11c86fc..196ad58c5 100644 --- a/src/main/java/dev/isxander/controlify/api/guide/InGameCtx.java +++ b/src/main/java/dev/isxander/controlify/api/guide/InGameCtx.java @@ -6,6 +6,7 @@ */ package dev.isxander.controlify.api.guide; +import dev.isxander.controlify.contextual.api.Context; import dev.isxander.controlify.controller.ControllerEntity; import net.minecraft.client.Minecraft; import net.minecraft.client.multiplayer.ClientLevel; @@ -19,5 +20,5 @@ public record InGameCtx( HitResult hitResult, ControllerEntity controller, GuideVerbosity verbosity -) implements FactCtx { +) implements FactCtx, Context { } diff --git a/src/main/java/dev/isxander/controlify/api/guide/InGameFacts.java b/src/main/java/dev/isxander/controlify/api/guide/InGameFacts.java index ea5624a98..8d6b1b13e 100644 --- a/src/main/java/dev/isxander/controlify/api/guide/InGameFacts.java +++ b/src/main/java/dev/isxander/controlify/api/guide/InGameFacts.java @@ -181,7 +181,7 @@ private static Fact register(Identifier id) { public static void registerAll() { CommonFacts.registerAll(); // This method is used to ensure that all facts are registered - // when the class is loaded, so that they can be used in the guide. + // require the class is loaded, so that they can be used in the guide. // No-op, as all facts are registered statically. } } diff --git a/src/main/java/dev/isxander/controlify/api/guide/Rule.java b/src/main/java/dev/isxander/controlify/api/guide/Rule.java index 0d5b197c0..177b04cf4 100644 --- a/src/main/java/dev/isxander/controlify/api/guide/Rule.java +++ b/src/main/java/dev/isxander/controlify/api/guide/Rule.java @@ -27,7 +27,7 @@ * @param where the location of the guide (left or right) * @param when a set of conditions that all must be satisfied for the rule to apply * @param forbid a set of conditions where any must not be satisfied for the rule to apply - * @param then the text component that is shown in the guide when the rule applies + * @param then the text component that is shown in the guide require the rule applies * @see #builder() for a builder to create dynamic rules */ public record Rule( @@ -41,7 +41,7 @@ public record Rule( instance -> instance.group( InputBindingSupplier.CODEC.fieldOf("for").forGetter(Rule::binding), ActionLocation.CODEC.fieldOf("where").forGetter(Rule::where), - CExtraCodecs.set(Identifier.CODEC).optionalFieldOf("when", Set.of()).forGetter(Rule::when), + CExtraCodecs.set(Identifier.CODEC).optionalFieldOf("require", Set.of()).forGetter(Rule::when), CExtraCodecs.set(Identifier.CODEC).optionalFieldOf("forbid", Set.of()).forGetter(Rule::forbid), ComponentSerialization.CODEC.fieldOf("then").forGetter(Rule::then) ).apply(instance, Rule::new) diff --git a/src/main/java/dev/isxander/controlify/bindings/InputBindingImpl.java b/src/main/java/dev/isxander/controlify/bindings/InputBindingImpl.java index 874c333c3..03cadd28e 100644 --- a/src/main/java/dev/isxander/controlify/bindings/InputBindingImpl.java +++ b/src/main/java/dev/isxander/controlify/bindings/InputBindingImpl.java @@ -26,7 +26,7 @@ public class InputBindingImpl implements InputBinding { private final Component name, description, category; private final boolean isRadialCandidate; /** - * Fallback bound input used only when inputComponent is null (i.e., bindings not attached to a controller). + * Fallback bound input used only require inputComponent is null (i.e., bindings not attached to a controller). * When inputComponent is present, the bound input is read/written directly from the config. */ private Input fallbackBoundInput; @@ -217,7 +217,7 @@ public Input boundInput() { if (configInput != null) { return configInput; } - // Not in config means it's at the default (when keepDefaultBindings is false) + // Not in config means it's at the default (require keepDefaultBindings is false) // Return the dynamic default so all controllers stay in sync return defaultInput(); } diff --git a/src/main/java/dev/isxander/controlify/bindings/defaults/DefaultBindManager.java b/src/main/java/dev/isxander/controlify/bindings/defaults/DefaultBindManager.java index 38af1cf98..a3478dcec 100644 --- a/src/main/java/dev/isxander/controlify/bindings/defaults/DefaultBindManager.java +++ b/src/main/java/dev/isxander/controlify/bindings/defaults/DefaultBindManager.java @@ -107,7 +107,7 @@ public CompletableFuture apply(@Nullable Preparations data, ResourceManage if (data != null) { this.defaultsByNamespace.putAll(data.map()); } - // No binding updates here: when keepDefaultBindings is true, defaults are serialized + // No binding updates here: require keepDefaultBindings is true, defaults are serialized // and must not be changed by default provider reloads. }, executor); } diff --git a/src/main/java/dev/isxander/controlify/contextual/ClientEntityPredicate.java b/src/main/java/dev/isxander/controlify/contextual/ClientEntityPredicate.java new file mode 100644 index 000000000..3b0f5d99e --- /dev/null +++ b/src/main/java/dev/isxander/controlify/contextual/ClientEntityPredicate.java @@ -0,0 +1,130 @@ +package dev.isxander.controlify.contextual; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import dev.isxander.controlify.utils.ClientPredicateUtils; +import net.minecraft.advancements.predicates.DistancePredicate; +import net.minecraft.advancements.predicates.MobEffectsPredicate; +import net.minecraft.advancements.predicates.SlotsPredicate; +import net.minecraft.advancements.predicates.entity.EntityEquipmentPredicate; +import net.minecraft.advancements.predicates.entity.EntityFlagsPredicate; +import net.minecraft.advancements.predicates.entity.EntityTypePredicate; +import net.minecraft.advancements.predicates.entity.MovementPredicate; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.util.ExtraCodecs; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.scores.Team; +import org.jetbrains.annotations.Nullable; + +import java.util.Optional; + +/// A subset of {@link net.minecraft.advancements.predicates.entity.EntityPredicate} that works +/// for all entities on the client. The vanilla one requires server-side-only data. +/// +/// This predicate also queries client-sided tags via {@link ClientPredicateUtils}. +/// +/// This predicate is missing the following from its server-sided counterpart: +/// - `location` +/// - `nbt` +/// - `type_specific` +/// - `targeted_entity` +/// - data component matcher +public record ClientEntityPredicate( + Optional entityType, + Optional distanceToPlayer, + Optional movement, + Optional effects, + Optional flags, + Optional equipment, + Optional periodicTick, + Optional vehicle, + Optional passenger, + Optional team, + Optional slots +) { + public static final Codec CODEC = Codec.recursive( + "ClientEntityPredicate", + subCodec -> RecordCodecBuilder.create(instance -> instance.group( + EntityTypePredicate.CODEC.optionalFieldOf("type").forGetter(ClientEntityPredicate::entityType), + DistancePredicate.CODEC.optionalFieldOf("distance").forGetter(ClientEntityPredicate::distanceToPlayer), + MovementPredicate.CODEC.optionalFieldOf("movement").forGetter(ClientEntityPredicate::movement), + MobEffectsPredicate.CODEC.optionalFieldOf("effects").forGetter(ClientEntityPredicate::effects), + EntityFlagsPredicate.CODEC.optionalFieldOf("flags").forGetter(ClientEntityPredicate::flags), + EntityEquipmentPredicate.CODEC.optionalFieldOf("equipment").forGetter(ClientEntityPredicate::equipment), + ExtraCodecs.POSITIVE_INT.optionalFieldOf("periodic_tick").forGetter(ClientEntityPredicate::periodicTick), + subCodec.optionalFieldOf("vehicle").forGetter(ClientEntityPredicate::vehicle), + subCodec.optionalFieldOf("passenger").forGetter(ClientEntityPredicate::passenger), + Codec.STRING.optionalFieldOf("team").forGetter(ClientEntityPredicate::team), + SlotsPredicate.CODEC.optionalFieldOf("slots").forGetter(ClientEntityPredicate::slots) + ).apply(instance, ClientEntityPredicate::new)) + ); + + public boolean matches(LocalPlayer player, @Nullable Entity entity) { + return this.matches(player.position(), entity); + } + + public boolean matches(@Nullable Vec3 position, Entity entity) { + if (entity == null) { + return false; + } + + if (this.entityType.isPresent() && !ClientPredicateUtils.matches(this.entityType.get(), entity)) { + return false; + } + + if (position == null) { + if (this.distanceToPlayer.isPresent()) { + return false; + } + } else if (this.distanceToPlayer.isPresent() + && this.distanceToPlayer.get().matches(position.x, position.y, position.z, entity.getX(), entity.getY(), entity.getZ())) { + return false; + } + + if (this.movement.isPresent()) { + Vec3 knownMovement = entity.getKnownMovement(); + Vec3 velocity = knownMovement.scale(20.0); + if (!this.movement.get().matches(velocity.x, velocity.y, velocity.z, entity.fallDistance)) { + return false; + } + } + + if (this.effects.isPresent() && !effects.get().matches(entity)) { + return false; + } + + if (this.flags.isPresent() && !this.flags.get().matches(entity)) { + return false; + } + + if (this.equipment.isPresent() && !this.equipment.get().matches(entity)) { + return false; + } + + if (this.vehicle.isPresent() && !this.vehicle.get().matches(position, entity.getVehicle())) { + return false; + } + + if (this.passenger.isPresent() && entity.getPassengers().stream().noneMatch(p -> this.passenger.get().matches(position, p))) { + return false; + } + + if (this.periodicTick.isPresent() && entity.tickCount % this.periodicTick.get() != 0) { + return false; + } + + if (this.team.isPresent()) { + Team team = entity.getTeam(); + if (team == null || !this.team.get().equals(team.getName())) { + return false; + } + } + + if (this.slots.isPresent() && !ClientPredicateUtils.matches(this.slots.get(), entity)) { + return false; + } + + return true; + } +} diff --git a/src/main/java/dev/isxander/controlify/contextual/ContextualDomainImpl.java b/src/main/java/dev/isxander/controlify/contextual/ContextualDomainImpl.java new file mode 100644 index 000000000..2373d3da3 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/contextual/ContextualDomainImpl.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ +package dev.isxander.controlify.contextual; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.JsonOps; +import dev.isxander.controlify.contextual.api.Context; +import dev.isxander.controlify.contextual.api.ContextualDomain; +import dev.isxander.controlify.contextual.api.ContextualStateContributor; +import dev.isxander.controlify.contextual.api.GuideInstance; +import dev.isxander.controlify.platform.client.resource.SimpleControlifyReloadListener; +import net.minecraft.client.gui.Font; +import net.minecraft.resources.FileToIdConverter; +import net.minecraft.resources.Identifier; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import org.jetbrains.annotations.Nullable; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +public final class ContextualDomainImpl implements ContextualDomain, SimpleControlifyReloadListener { + public static final String DIRECTORY = "contextual/facts"; + private static final FileToIdConverter converter = FileToIdConverter.json(DIRECTORY); + + private final Identifier id; + private volatile List> contributors = List.of(); + private volatile FactDependencyGraph factGraph = FactDependencyGraph.empty(); + + public ContextualDomainImpl(Identifier id) { + this.id = Objects.requireNonNull(id, "id"); + this.registerContributor(ContextualStateContributors.COMMON); + } + + @Override + public Identifier id() { + return this.id; + } + + @Override + public synchronized void registerContributor(ContextualStateContributor contributor) { + List> replacement = new ArrayList<>(this.contributors); + replacement.add(Objects.requireNonNull(contributor, "contributor")); + this.contributors = List.copyOf(replacement); + } + + @Override + public GuideInstance createGuideInstance(Font font) { + RuleSetManager ruleSetManager = null; // TODO: get rule set manager from global state somewhere + RuleEngine ruleEngine = ruleSetManager.getRuleEngine(this.id) + .orElseThrow(() -> new IllegalCallerException("This contextual domain has no guide rules associated with it.")); + + return new GuideInstanceImpl<>(this, ruleEngine, font); + } + + @Override + public CompletableFuture load(ResourceManager manager, Executor executor) { + return CompletableFuture.supplyAsync(() -> { + List resources = manager.getResourceStack(converter.idToFile(this.id)); + + var factsMap = new HashMap(); + for (Resource resource : resources) { + try (BufferedReader reader = resource.openAsReader()) { + JsonElement json = JsonParser.parseReader(reader); + DataResult> result = FactDefinition.LIST_CODEC.parse(JsonOps.INSTANCE, json); + var facts = result.getOrThrow(); + + for (FactDefinition fact : facts) { + factsMap.remove(fact.id()); + factsMap.put(fact.id(), fact); + } + } catch (IOException e) { + throw new IllegalStateException("Failed to load resource", e); + } + } + + var facts = factsMap.values(); + var factGraph = FactDependencyGraph.compile(facts); + + return new Preparations(factGraph); + }, executor); + } + + @Override + public CompletableFuture apply(Preparations data, ResourceManager manager, Executor executor) { + return CompletableFuture.runAsync(() -> { + this.factGraph = data.facts(); + }, executor); + } + + @Override + public Identifier getReloadId() { + return this.id.withPrefix("reload/"); + } + + public ContextualState calculateState(C context, @Nullable Collection requiredFacts) { + Objects.requireNonNull(context, "context"); + + ContextualStateAccumulator state = new ContextualStateAccumulator(); + for (ContextualStateContributor contributor : this.contributors) { + contributor.contribute(context, state); + } + + FactDependencyGraph graph = this.factGraph; + return graph.evaluate(state, requiredFacts); + } + + public record Preparations( + FactDependencyGraph facts + ) {} +} diff --git a/src/main/java/dev/isxander/controlify/contextual/ContextualPredicate.java b/src/main/java/dev/isxander/controlify/contextual/ContextualPredicate.java new file mode 100644 index 000000000..022045f8b --- /dev/null +++ b/src/main/java/dev/isxander/controlify/contextual/ContextualPredicate.java @@ -0,0 +1,162 @@ +package dev.isxander.controlify.contextual; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import dev.isxander.controlify.utils.codec.CExtraCodecs; +import net.minecraft.advancements.predicates.BlockPredicate; +import net.minecraft.advancements.predicates.ItemPredicate; +import net.minecraft.resources.Identifier; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public sealed interface ContextualPredicate { + Codec CODEC = Codec.recursive("ContextualPredicate", self -> { + Codec compoundCodec = Compound.createCodec(self); + + return CExtraCodecs.fuzzy( + List.of(Fact.CODEC, Item.CODEC, Block.CODEC, Entity.CODEC, compoundCodec, Static.CODEC), + predicate -> switch (predicate) { + case Fact _ -> Fact.CODEC; + case Item _ -> Item.CODEC; + case Block _ -> Block.CODEC; + case Entity _ -> Entity.CODEC; + case Compound _ -> compoundCodec; + case Static _ -> Static.CODEC; + } + ); + }); + + Set factDependencies(); + + boolean matches(ContextualState state); + + record Compound(Set require, Set forbid) implements ContextualPredicate { + private static Codec createCodec(Codec childCodec) { + Codec codec = RecordCodecBuilder.create(instance -> instance.group( + CExtraCodecs.set(childCodec).optionalFieldOf("require", Set.of()).forGetter(Compound::require), + CExtraCodecs.set(childCodec).optionalFieldOf("forbid", Set.of()).forGetter(Compound::forbid) + ).apply(instance, Compound::new)); + + return codec.validate(compound -> { + if (compound.require().isEmpty() && compound.forbid().isEmpty()) { + return DataResult.error(() -> "Compound predicate must define at least one 'require' or 'forbid' predicate"); + } + return DataResult.success(compound); + }); + } + + @Override + public boolean matches(ContextualState state) { + return require().stream().allMatch(predicate -> predicate.matches(state)) + && forbid().stream().noneMatch(predicate -> predicate.matches(state)); + } + + @Override + public Set factDependencies() { + return Stream.concat(require().stream(), forbid().stream()) + .flatMap(predicate -> predicate.factDependencies().stream()) + .collect(Collectors.toUnmodifiableSet()); + } + } + + record Fact(Identifier fact) implements ContextualPredicate { + private static final Codec FULL_CODEC = RecordCodecBuilder.create(instance -> instance.group( + Identifier.CODEC.fieldOf("fact").forGetter(Fact::fact) + ).apply(instance, Fact::new)); + + private static final Codec COMPACT_CODEC = Identifier.CODEC.xmap(Fact::new, Fact::fact); + + private static final Codec CODEC = FULL_CODEC.withAlternative(COMPACT_CODEC); + + @Override + public boolean matches(ContextualState state) { + return state.facts().getOrDefault(fact(), false); + } + + @Override + public Set factDependencies() { + return Set.of(fact()); + } + } + + record Item(Identifier slot, ItemPredicate item) implements ContextualPredicate { + private static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + Identifier.CODEC.fieldOf("slot").forGetter(Item::slot), + ItemPredicate.CODEC.fieldOf("item").forGetter(Item::item) + ).apply(instance, Item::new)); + + @Override + public boolean matches(ContextualState state) { + var stack = state.items().get(slot()); + if (stack == null) { + return false; + } + return item().test(stack); + } + + @Override + public Set factDependencies() { + return Set.of(); + } + } + + record Block(Identifier slot, BlockPredicate block) implements ContextualPredicate { + private static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + Identifier.CODEC.fieldOf("slot").forGetter(Block::slot), + BlockPredicate.CODEC.fieldOf("block").forGetter(Block::block) + ).apply(instance, Block::new)); + + @Override + public boolean matches(ContextualState state) { + var blockInWorld = state.blocks().get(slot()); + if (blockInWorld == null) { + return false; + } + return block().matches(blockInWorld); + } + + @Override + public Set factDependencies() { + return Set.of(); + } + } + + record Entity(Identifier slot, ClientEntityPredicate entity) implements ContextualPredicate { + private static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + Identifier.CODEC.fieldOf("slot").forGetter(Entity::slot), + ClientEntityPredicate.CODEC.fieldOf("entity").forGetter(Entity::entity) + ).apply(instance, Entity::new)); + + @Override + public boolean matches(ContextualState state) { + var entityInWorld = state.entities().get(slot()); + if (entityInWorld == null) { + return false; + } + return entity().matches(entityInWorld); + } + + @Override + public Set factDependencies() { + return Set.of(); + } + } + + record Static(boolean value) implements ContextualPredicate { + private static final Codec CODEC = Codec.BOOL.xmap(Static::new, Static::value); + + @Override + public boolean matches(ContextualState state) { + return value(); + } + + @Override + public Set factDependencies() { + return Set.of(); + } + } +} diff --git a/src/main/java/dev/isxander/controlify/contextual/ContextualState.java b/src/main/java/dev/isxander/controlify/contextual/ContextualState.java new file mode 100644 index 000000000..c532a6c01 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/contextual/ContextualState.java @@ -0,0 +1,16 @@ +package dev.isxander.controlify.contextual; + +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.state.pattern.BlockInWorld; + +import java.util.Map; + +public record ContextualState( + Map facts, + Map items, + Map blocks, + Map entities +) { +} diff --git a/src/main/java/dev/isxander/controlify/contextual/ContextualStateAccumulator.java b/src/main/java/dev/isxander/controlify/contextual/ContextualStateAccumulator.java new file mode 100644 index 000000000..446639479 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/contextual/ContextualStateAccumulator.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ +package dev.isxander.controlify.contextual; + +import dev.isxander.controlify.contextual.api.ContextualStateSink; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.state.pattern.BlockInWorld; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +public final class ContextualStateAccumulator implements ContextualStateSink { + private final Map facts = new LinkedHashMap<>(); + private final Map items = new LinkedHashMap<>(); + private final Map blocks = new LinkedHashMap<>(); + private final Map entities = new LinkedHashMap<>(); + + public ContextualStateAccumulator() { + } + + public ContextualStateAccumulator(ContextualState state) { + this.facts.putAll(state.facts()); + this.items.putAll(state.items()); + this.blocks.putAll(state.blocks()); + this.entities.putAll(state.entities()); + } + + @Override + public void contributeFact(Identifier id, boolean value) { + this.facts.put(Objects.requireNonNull(id, "id"), value); + } + + @Override + public void contributeItem(Identifier slot, ItemStack stack) { + this.items.put(Objects.requireNonNull(slot, "slot"), Objects.requireNonNull(stack, "stack")); + } + + @Override + public void contributeBlock(Identifier slot, BlockInWorld block) { + this.blocks.put(Objects.requireNonNull(slot, "slot"), Objects.requireNonNull(block, "block")); + } + + @Override + public void contributeEntity(Identifier slot, Entity entity) { + this.entities.put(Objects.requireNonNull(slot, "slot"), Objects.requireNonNull(entity, "entity")); + } + + public ContextualState view() { + return new ContextualState( + Collections.unmodifiableMap(this.facts), + Collections.unmodifiableMap(this.items), + Collections.unmodifiableMap(this.blocks), + Collections.unmodifiableMap(this.entities) + ); + } + + public ContextualState snapshot() { + return new ContextualState( + Map.copyOf(this.facts), + Map.copyOf(this.items), + Map.copyOf(this.blocks), + Map.copyOf(this.entities) + ); + } +} diff --git a/src/main/java/dev/isxander/controlify/contextual/ContextualStateContributors.java b/src/main/java/dev/isxander/controlify/contextual/ContextualStateContributors.java new file mode 100644 index 000000000..5fa0278df --- /dev/null +++ b/src/main/java/dev/isxander/controlify/contextual/ContextualStateContributors.java @@ -0,0 +1,111 @@ +package dev.isxander.controlify.contextual; + +import dev.isxander.controlify.api.guide.GuideVerbosity; +import dev.isxander.controlify.api.guide.InGameCtx; +import dev.isxander.controlify.contextual.api.Context; +import dev.isxander.controlify.contextual.api.ContextualStateContributor; +import dev.isxander.controlify.mixins.feature.guide.ingame.PlayerAccessor; +import dev.isxander.controlify.utils.CUtil; +import net.minecraft.resources.Identifier; +import net.minecraft.world.level.block.state.pattern.BlockInWorld; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.EntityHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec2; + +public final class ContextualStateContributors { + private static final Identifier SLOT_SELF = CUtil.rl("self"); + private static final Identifier SLOT_HIT_RESULT = CUtil.rl("hit_result"); + private static final Identifier SLOT_MAIN_HAND = CUtil.rl("main_hand"); + private static final Identifier SLOT_OFF_HAND = CUtil.rl("off_hand"); + /// Picks the main hand, but if it is empty picks the offhand + private static final Identifier SLOT_ACTIVE_ITEM = CUtil.rl("active_item"); + + public static final ContextualStateContributor COMMON = (context, sink) -> { + sink.contributeFact( + CUtil.rl("verbosity_full"), + context.verbosity() == GuideVerbosity.FULL + ); + sink.contributeFact( + CUtil.rl("verbosity_reduced"), + context.verbosity().getLevel() >= GuideVerbosity.REDUCED.getLevel() + ); + sink.contributeFact( + CUtil.rl("verbosity_minimal"), + context.verbosity().getLevel() >= GuideVerbosity.MINIMAL.getLevel() + ); + }; + + public static final ContextualStateContributor IN_GAME = (context, sink) -> { + switch (context.hitResult()) { + case BlockHitResult hitResult when hitResult.getType() == HitResult.Type.BLOCK -> { + sink.contributeBlock( + SLOT_HIT_RESULT, + new BlockInWorld(context.level(), hitResult.getBlockPos(), false) + ); + sink.contributeFact(CUtil.rl("looking_at_block")); + } + case EntityHitResult hitResult -> { + sink.contributeEntity( + SLOT_HIT_RESULT, + hitResult.getEntity() + ); + sink.contributeFact(CUtil.rl("looking_at_entity")); + } + default -> { + sink.contributeFact(CUtil.rl("looking_at_air")); + } + } + + sink.contributeEntity(SLOT_SELF, context.player()); + + sink.contributeItem(SLOT_MAIN_HAND, context.player().getMainHandItem()); + sink.contributeItem(SLOT_OFF_HAND, context.player().getOffhandItem()); + sink.contributeItem(SLOT_ACTIVE_ITEM, context.player().getActiveItem()); + + sink.contributeFact( + CUtil.rl("can_elytra_fly"), + ((PlayerAccessor) context.player()).controlify$callCanGlide() + && !context.player().onClimbable() + && !context.player().onGround() + && !context.player().isInLiquid() + && !context.player().isFallFlying() + ); + + sink.contributeFact( + CUtil.rl("in_liquid"), + context.player().isInLiquid() + ); + + sink.contributeFact( + CUtil.rl("under_water"), + context.player().isUnderWater() + ); + + sink.contributeFact( + CUtil.rl("is_toggle_sneak"), + context.controller().settings().generic.toggleSneak + ); + sink.contributeFact( + CUtil.rl("is_toggle_sprint"), + context.controller().settings().generic.toggleSprint + ); + sink.contributeFact( + CUtil.rl("input_moving"), + context.player().input.getMoveVector().equals(Vec2.ZERO) + ); + + // TODO: add GameTypePredicate to ClientEntityPredicate + // and make them derived facts + + sink.contributeFact( + CUtil.rl("has_hearts"), + !context.player().getAbilities().invulnerable + ); + + + }; + + private ContextualStateContributors() { + } +} diff --git a/src/main/java/dev/isxander/controlify/contextual/CyclicFactDependencyException.java b/src/main/java/dev/isxander/controlify/contextual/CyclicFactDependencyException.java new file mode 100644 index 000000000..7fa8df3f1 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/contextual/CyclicFactDependencyException.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ +package dev.isxander.controlify.contextual; + +import net.minecraft.resources.Identifier; + +import java.util.List; +import java.util.stream.Collectors; + +public class CyclicFactDependencyException extends IllegalArgumentException { + private final List cycle; + + public CyclicFactDependencyException(List cycle) { + super("Cyclic contextual fact dependency: " + cycle.stream() + .map(Identifier::toString) + .collect(Collectors.joining(" -> "))); + this.cycle = List.copyOf(cycle); + } + + /// The cycle in traversal order, with the first identifier repeated at the end. + public List cycle() { + return this.cycle; + } +} diff --git a/src/main/java/dev/isxander/controlify/contextual/FactDefinition.java b/src/main/java/dev/isxander/controlify/contextual/FactDefinition.java new file mode 100644 index 000000000..36969d31e --- /dev/null +++ b/src/main/java/dev/isxander/controlify/contextual/FactDefinition.java @@ -0,0 +1,39 @@ +package dev.isxander.controlify.contextual; + +import com.mojang.serialization.Codec; +import net.minecraft.resources.Identifier; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +public record FactDefinition( + Identifier id, + ContextualPredicate predicate +) { + public static final Codec> LIST_CODEC = Codec.unboundedMap(Identifier.CODEC, ContextualPredicate.CODEC) + .xmap( + map -> map.entrySet().stream() + .map(entry -> new FactDefinition(entry.getKey(), entry.getValue())) + .toList(), + defs -> defs.stream() + .collect(Collectors.toMap( + FactDefinition::id, + FactDefinition::predicate, + (first, second) -> second, + LinkedHashMap::new + )) + ); + + public static Map toMap(List list) { + return list.stream() + .collect(Collectors.toMap( + FactDefinition::id, + Function.identity(), + (first, second) -> second, + LinkedHashMap::new + )); + } +} diff --git a/src/main/java/dev/isxander/controlify/contextual/FactDependencyGraph.java b/src/main/java/dev/isxander/controlify/contextual/FactDependencyGraph.java new file mode 100644 index 000000000..bc6caa2f9 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/contextual/FactDependencyGraph.java @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ +package dev.isxander.controlify.contextual; + +import net.minecraft.resources.Identifier; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/// A compiled evaluation plan for data-defined facts. +/// +/// References to other data-defined facts are graph edges. References which do not have a +/// definition are treated as source facts and are expected to be supplied by contributors. +public final class FactDependencyGraph { + private static final Comparator IDENTIFIER_ORDER = Comparator.comparing(Identifier::toString); + private static final FactDependencyGraph EMPTY = new FactDependencyGraph(Map.of(), Map.of(), List.of(), Set.of()); + + private final Map definitions; + private final Map> dependencies; + private final List evaluationOrder; + private final Set externalDependencies; + + private FactDependencyGraph( + Map definitions, + Map> dependencies, + List evaluationOrder, + Set externalDependencies + ) { + this.definitions = definitions; + this.dependencies = dependencies; + this.evaluationOrder = evaluationOrder; + this.externalDependencies = externalDependencies; + } + + public static FactDependencyGraph empty() { + return EMPTY; + } + + /// Compiles fact definitions into a deterministic dependency-first evaluation order. + /// + /// @throws IllegalArgumentException if a definition id is duplicated + /// @throws CyclicFactDependencyException if the definitions contain a dependency cycle + public static FactDependencyGraph compile(Collection factDefinitions) { + Objects.requireNonNull(factDefinitions, "factDefinitions"); + + // put all definitions in a map + Map definitions = new LinkedHashMap<>(); + for (FactDefinition definition : factDefinitions) { + Objects.requireNonNull(definition, "factDefinition"); + Objects.requireNonNull(definition.id(), "factDefinition.id"); + Objects.requireNonNull(definition.predicate(), "factDefinition.predicate"); + if (definitions.putIfAbsent(definition.id(), definition) != null) { + throw new IllegalArgumentException("Duplicate contextual fact definition: " + definition.id()); + } + } + + // short circuit to empty instance + if (definitions.isEmpty()) { + return empty(); + } + + // get dependencies of each fact in a stable order + Map> dependencies = new LinkedHashMap<>(); + for (FactDefinition definition : definitions.values()) { + List sortedDependencies = new ArrayList<>(definition.predicate().factDependencies()); + sortedDependencies.sort(IDENTIFIER_ORDER); // ensure stable order + dependencies.put(definition.id(), Collections.unmodifiableSet(new LinkedHashSet<>(sortedDependencies))); + } + + // compile evaluation order for dependencies + Compiler compiler = new Compiler(definitions, dependencies); + for (Identifier id : definitions.keySet()) { + compiler.visit(id); // throws CyclicFactDependencyException + } + + return new FactDependencyGraph( + Collections.unmodifiableMap(new LinkedHashMap<>(definitions)), + Collections.unmodifiableMap(new LinkedHashMap<>(dependencies)), + List.copyOf(compiler.evaluationOrder), + Collections.unmodifiableSet(new LinkedHashSet<>(compiler.externalDependencies)) + ); + } + + public Map definitions() { + return this.definitions; + } + + /// Direct fact references for each data-defined fact, including source-fact references. + public Map> dependencies() { + return this.dependencies; + } + + /// Data-defined fact ids ordered so every data-defined dependency precedes its consumer. + public List evaluationOrder() { + return this.evaluationOrder; + } + + /// Referenced facts which have no data definition and must be supplied by contributors. + public Set externalDependencies() { + return this.externalDependencies; + } + + /// Resolves required data-defined facts on top of a source state. + public ContextualState evaluate(ContextualStateAccumulator state, @Nullable Collection requiredFacts) { + ContextualState view = state.view(); + for (Identifier id : this.evaluationOrder()) { + if (requiredFacts == null || requiredFacts.contains(id)) { + FactDefinition definition = this.definitions().get(id); + state.contributeFact(id, definition.predicate().matches(view)); + } + } + return state.snapshot(); + } + + private static final class Compiler { + private final Map definitions; + private final Map> dependencies; + private final Map visitStates = new HashMap<>(); + private final List path = new ArrayList<>(); + private final List evaluationOrder = new ArrayList<>(); + private final Set externalDependencies = new LinkedHashSet<>(); + + private Compiler( + Map definitions, + Map> dependencies + ) { + this.definitions = definitions; + this.dependencies = dependencies; + } + + private void visit(Identifier id) throws CyclicFactDependencyException { + VisitState state = this.visitStates.get(id); + if (state == VisitState.VISITED) { + return; + } + if (state == VisitState.VISITING) { + int cycleStart = this.path.indexOf(id); + List cycle = new ArrayList<>(this.path.subList(cycleStart, this.path.size())); + cycle.add(id); + throw new CyclicFactDependencyException(cycle); + } + + this.visitStates.put(id, VisitState.VISITING); + this.path.add(id); + + for (Identifier dependency : this.dependencies.get(id)) { + if (this.definitions.containsKey(dependency)) { + this.visit(dependency); + } else { + this.externalDependencies.add(dependency); + } + } + + this.path.removeLast(); + this.visitStates.put(id, VisitState.VISITED); + this.evaluationOrder.add(id); + } + + private enum VisitState { + VISITING, + VISITED + } + } +} diff --git a/src/main/java/dev/isxander/controlify/contextual/GuideInstanceImpl.java b/src/main/java/dev/isxander/controlify/contextual/GuideInstanceImpl.java new file mode 100644 index 000000000..f625b401c --- /dev/null +++ b/src/main/java/dev/isxander/controlify/contextual/GuideInstanceImpl.java @@ -0,0 +1,113 @@ +package dev.isxander.controlify.contextual; + +import dev.isxander.controlify.api.bind.InputBinding; +import dev.isxander.controlify.api.guide.ActionLocation; +import dev.isxander.controlify.contextual.api.Context; +import dev.isxander.controlify.contextual.api.GuideInstance; +import dev.isxander.controlify.controller.ControllerEntity; +import dev.isxander.controlify.font.BindingFontHelper; +import dev.isxander.controlify.gui.guide.GuideRenderer; +import dev.isxander.controlify.gui.guide.PrecomputedLines; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.Renderable; +import net.minecraft.network.chat.Component; + +import java.util.List; + +public class GuideInstanceImpl implements GuideInstance { + private final ContextualDomainImpl domain; + private final RuleEngine ruleEngine; + private final Font font; + + public GuideInstanceImpl( + ContextualDomainImpl domain, + RuleEngine ruleEngine, + Font font + ) { + this.domain = domain; + this.ruleEngine = ruleEngine; + this.font = font; + } + + private PrecomputedLines leftGuides = PrecomputedLines.EMPTY; + private PrecomputedLines rightGuides = PrecomputedLines.EMPTY; + + @Override + public void update(C context) { + ContextualState state = this.domain.calculateState(context, this.ruleEngine.factDependencies()); + List guides = this.ruleEngine.evaluate(state); + + var leftBuilder = new PrecomputedLines.Builder(); + var rightBuilder = new PrecomputedLines.Builder(); + + ControllerEntity controller = context.controller(); + + for (GuideRule guide : guides) { + InputBinding binding = guide.binding().onOrNull(controller); + if (binding == null || binding.isUnbound()) { + // skip this rule if the binding is not bound + continue; + } + + PrecomputedLines.Builder builder = switch (guide.location()) { + case LEFT -> leftBuilder; + case RIGHT -> rightBuilder; + }; + + // put the glyph after or before the binding glyph based on the rule's location and font direction + boolean glyphAfter = font.isBidirectional() ^ (guide.location() == ActionLocation.RIGHT); + + // formulate the text to display + Component text = Component.empty() + .append(glyphAfter ? guide.text() : binding.inputGlyph()) + .append(" ") + .append(glyphAfter ? binding.inputGlyph() : guide.text()); + + // precompute the width and height of the text + int ruleNameWidth = font.width(guide.text()); + int width = font.width(text); + int glyphWidth = width - ruleNameWidth; + int backgroundLeft = glyphAfter ? 0 : glyphWidth; + int backgroundRight = backgroundLeft + ruleNameWidth; + + int height = BindingFontHelper.getComponentHeight(font, binding.inputGlyph()); // use input glyph height only as it's bound to be the tallest + + builder.addLine(text, width, height, backgroundLeft, backgroundRight); + } + this.leftGuides = leftBuilder.build(); + this.rightGuides = rightBuilder.build(); + } + + @Override + public void extractRenderState(GuiGraphicsExtractor graphics, boolean bottomAligned, boolean textContrast, int guiScale) { + GuideRenderer.extractRenderState( + graphics, + this, + Minecraft.getInstance(), + bottomAligned, + textContrast, + guiScale + ); + } + + @Override + public Renderable renderable(boolean bottomAligned, boolean textContrast, int guiScale) { + return new GuideRenderer.Renderable( + this, + Minecraft.getInstance(), + bottomAligned, + textContrast, + guiScale + ); + } + + public PrecomputedLines leftGuides() { + return this.leftGuides; + } + + public PrecomputedLines rightGuides() { + return this.rightGuides; + } +} diff --git a/src/main/java/dev/isxander/controlify/contextual/GuideRule.java b/src/main/java/dev/isxander/controlify/contextual/GuideRule.java new file mode 100644 index 000000000..28b62d661 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/contextual/GuideRule.java @@ -0,0 +1,33 @@ +package dev.isxander.controlify.contextual; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import dev.isxander.controlify.api.bind.InputBindingSupplier; +import dev.isxander.controlify.api.guide.ActionLocation; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentSerialization; +import net.minecraft.resources.Identifier; + +public record GuideRule( + InputBindingSupplier binding, + ActionLocation location, + ContextualPredicate predicate, + Component text +) implements Rule { + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + InputBindingSupplier.CODEC.fieldOf("for").forGetter(GuideRule::binding), + ActionLocation.CODEC.fieldOf("where").forGetter(GuideRule::location), + ContextualPredicate.CODEC.fieldOf("if").forGetter(GuideRule::predicate), + ComponentSerialization.CODEC.fieldOf("then").forGetter(GuideRule::text) + ).apply(instance, GuideRule::new)); + + @Override + public Key key() { + return new Key(this.binding().bindId(), this.location()); + } + + public record Key( + Identifier binding, + ActionLocation location + ) {} +} diff --git a/src/main/java/dev/isxander/controlify/contextual/Rule.java b/src/main/java/dev/isxander/controlify/contextual/Rule.java new file mode 100644 index 000000000..927d07ec3 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/contextual/Rule.java @@ -0,0 +1,16 @@ +package dev.isxander.controlify.contextual; + +public interface Rule { + /// Used to identify require multiple rules are overriding each other. + /// + /// For example, with guides this would be the binding and location. + /// + /// Allows deduplication and short-circuiting done by the rule engine. + K key(); + + ContextualPredicate predicate(); + + default boolean matches(ContextualState state) { + return this.predicate().matches(state); + } +} diff --git a/src/main/java/dev/isxander/controlify/contextual/RuleEngine.java b/src/main/java/dev/isxander/controlify/contextual/RuleEngine.java new file mode 100644 index 000000000..3da7960d8 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/contextual/RuleEngine.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ +package dev.isxander.controlify.contextual; + +import net.minecraft.resources.Identifier; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/// An ordered set of contextual rules for one output type. +/// +/// Rules are considered in list order. The first matching rule for a key wins; a rule which does +/// not match does not consume its key, allowing a lower-priority fallback for that key to match. +public final class RuleEngine> { + private final List rules; + private final Set factDependencies; + + public RuleEngine(List rules) { + Objects.requireNonNull(rules, "rules"); + + List copiedRules = new ArrayList<>(rules.size()); + Set factDependencies = new LinkedHashSet<>(); + for (R rule : rules) { + Objects.requireNonNull(rule, "rule"); + Objects.requireNonNull(rule.key(), "rule.key"); + Objects.requireNonNull(rule.predicate(), "rule.predicate"); + copiedRules.add(rule); + factDependencies.addAll(rule.predicate().factDependencies()); + } + + this.rules = List.copyOf(copiedRules); + this.factDependencies = Set.copyOf(factDependencies); + } + + public List rules() { + return this.rules; + } + + public Set factDependencies() { + return this.factDependencies; + } + + /// Returns the winning rules in their original precedence order. + public List evaluate(ContextualState state) { + Objects.requireNonNull(state, "state"); + + List matches = new ArrayList<>(); + Set consumedKeys = new HashSet<>(); + for (R rule : this.rules) { + K key = rule.key(); + if (!consumedKeys.contains(key) && rule.matches(state)) { + consumedKeys.add(key); + matches.add(rule); + } + } + + return List.copyOf(matches); + } +} diff --git a/src/main/java/dev/isxander/controlify/contextual/RuleSet.java b/src/main/java/dev/isxander/controlify/contextual/RuleSet.java new file mode 100644 index 000000000..0f957ba16 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/contextual/RuleSet.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ +package dev.isxander.controlify.contextual; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; + +import java.util.List; + +public record RuleSet>( + boolean replace, + List rules +) { + public static > Codec> createCodec(Codec ruleCodec) { + return RecordCodecBuilder.create(instance -> instance.group( + Codec.BOOL.optionalFieldOf("replace", false).forGetter(RuleSet::replace), + ruleCodec.listOf().fieldOf("rules").forGetter(RuleSet::rules) + ).apply(instance, RuleSet::new)); + } +} diff --git a/src/main/java/dev/isxander/controlify/contextual/RuleSetManager.java b/src/main/java/dev/isxander/controlify/contextual/RuleSetManager.java new file mode 100644 index 000000000..735188644 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/contextual/RuleSetManager.java @@ -0,0 +1,94 @@ +package dev.isxander.controlify.contextual; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.JsonOps; +import dev.isxander.controlify.platform.client.resource.SimpleControlifyReloadListener; +import dev.isxander.controlify.utils.CUtil; +import net.minecraft.resources.FileToIdConverter; +import net.minecraft.resources.Identifier; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +public class RuleSetManager> implements SimpleControlifyReloadListener> { + public static final String DIRECTORY = "contextual/"; + + private final Codec> ruleSetCodec; + private final String directory; + private final FileToIdConverter converter; + + private volatile Map> ruleEngineByDomain; + + public RuleSetManager(Codec ruleCodec, String directory) { + this.ruleSetCodec = RuleSet.createCodec(ruleCodec); + this.directory = DIRECTORY + directory; + this.converter = FileToIdConverter.json(this.directory); + this.ruleEngineByDomain = Map.of(); + } + + public synchronized Optional> getRuleEngine(Identifier domainId) { + return Optional.ofNullable(this.ruleEngineByDomain.get(domainId)); + } + + @Override + public CompletableFuture> load(ResourceManager manager, Executor executor) { + return CompletableFuture.supplyAsync(() -> { + Map> ruleSetResourcesByDomain = converter.listMatchingResourceStacks(manager); + + var ruleEngineByDomain = new HashMap>(); + for (var entry : ruleSetResourcesByDomain.entrySet()) { + List rules = parseRuleSets(entry.getValue()); + var ruleEngine = new RuleEngine<>(rules); + ruleEngineByDomain.put(entry.getKey(), ruleEngine); + } + + return new Preparations<>(ruleEngineByDomain); + }, executor); + } + + // highest priority resource pack's first rule should be the head of the resultant list. + private List parseRuleSets(List ruleSetResources) { + var rules = new ArrayList(); + + for (Resource ruleSetResource : ruleSetResources) { + try (var reader = ruleSetResource.openAsReader()) { + JsonElement json = JsonParser.parseReader(reader); + DataResult> ruleSetResult = this.ruleSetCodec.parse(JsonOps.INSTANCE, json); + RuleSet ruleSet = ruleSetResult.getOrThrow(); + + if (ruleSet.replace()) { + rules.clear(); + } + + rules.addAll(ruleSet.rules()); + } catch (IOException e) { + throw new IllegalStateException("Could not parse rule set", e); + } + } + + return rules; + } + + @Override + public CompletableFuture apply(Preparations data, ResourceManager manager, Executor executor) { + return CompletableFuture.runAsync(() -> { + this.ruleEngineByDomain = data.ruleEngineByDomain(); + }, executor); + } + + @Override + public Identifier getReloadId() { + return CUtil.rl("reload/" + this.directory); + } + + public record Preparations>( + Map> ruleEngineByDomain + ) {} +} diff --git a/src/main/java/dev/isxander/controlify/contextual/TriggerRule.java b/src/main/java/dev/isxander/controlify/contextual/TriggerRule.java new file mode 100644 index 000000000..9140a58f2 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/contextual/TriggerRule.java @@ -0,0 +1,26 @@ +package dev.isxander.controlify.contextual; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import dev.isxander.controlify.controller.dualsense.TriggerEffectCodecs; +import dev.isxander.controlify.driver.dualsense.DualsenseTriggerEffect; +import net.minecraft.resources.Identifier; + +/// @param trigger identifier of the logical action that may be bound to a trigger. +/// for the in_game domain, the accepted values would be `use_item` and `swing_item` +public record TriggerRule( + Identifier trigger, + ContextualPredicate predicate, + DualsenseTriggerEffect effect +) implements Rule { + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + Identifier.CODEC.fieldOf("for").forGetter(TriggerRule::trigger), + ContextualPredicate.CODEC.fieldOf("if").forGetter(TriggerRule::predicate), + TriggerEffectCodecs.CODEC.fieldOf("then").forGetter(TriggerRule::effect) + ).apply(instance, TriggerRule::new)); + + @Override + public Identifier key() { + return this.trigger(); + } +} diff --git a/src/main/java/dev/isxander/controlify/contextual/api/Context.java b/src/main/java/dev/isxander/controlify/contextual/api/Context.java new file mode 100644 index 000000000..5b4ac9b01 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/contextual/api/Context.java @@ -0,0 +1,10 @@ +package dev.isxander.controlify.contextual.api; + +import dev.isxander.controlify.api.guide.GuideVerbosity; +import dev.isxander.controlify.controller.ControllerEntity; + +public interface Context { + ControllerEntity controller(); + + GuideVerbosity verbosity(); +} diff --git a/src/main/java/dev/isxander/controlify/contextual/api/ContextualDomain.java b/src/main/java/dev/isxander/controlify/contextual/api/ContextualDomain.java new file mode 100644 index 000000000..99fae27bc --- /dev/null +++ b/src/main/java/dev/isxander/controlify/contextual/api/ContextualDomain.java @@ -0,0 +1,12 @@ +package dev.isxander.controlify.contextual.api; + +import net.minecraft.client.gui.Font; +import net.minecraft.resources.Identifier; + +public interface ContextualDomain { + Identifier id(); + + void registerContributor(ContextualStateContributor contributor); + + GuideInstance createGuideInstance(Font font); +} diff --git a/src/main/java/dev/isxander/controlify/contextual/api/ContextualInstance.java b/src/main/java/dev/isxander/controlify/contextual/api/ContextualInstance.java new file mode 100644 index 000000000..70ce2b394 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/contextual/api/ContextualInstance.java @@ -0,0 +1,5 @@ +package dev.isxander.controlify.contextual.api; + +public interface ContextualInstance { + void update(C context); +} diff --git a/src/main/java/dev/isxander/controlify/contextual/api/ContextualStateContributor.java b/src/main/java/dev/isxander/controlify/contextual/api/ContextualStateContributor.java new file mode 100644 index 000000000..525bd7f97 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/contextual/api/ContextualStateContributor.java @@ -0,0 +1,7 @@ +package dev.isxander.controlify.contextual.api; + +public interface ContextualStateContributor { + /// Ran each update tick + /// The new alternative to the Guide API's old `Fact` where it was a Predicate per fact! + void contribute(C context, ContextualStateSink sink); +} diff --git a/src/main/java/dev/isxander/controlify/contextual/api/ContextualStateSink.java b/src/main/java/dev/isxander/controlify/contextual/api/ContextualStateSink.java new file mode 100644 index 000000000..b03f7d18c --- /dev/null +++ b/src/main/java/dev/isxander/controlify/contextual/api/ContextualStateSink.java @@ -0,0 +1,20 @@ +package dev.isxander.controlify.contextual.api; + +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.state.pattern.BlockInWorld; + +public interface ContextualStateSink { + void contributeFact(Identifier id, boolean value); + + default void contributeFact(Identifier id) { + this.contributeFact(id, true); + } + + void contributeItem(Identifier slot, ItemStack stack); + + void contributeBlock(Identifier slot, BlockInWorld block); + + void contributeEntity(Identifier slot, Entity entity); +} diff --git a/src/main/java/dev/isxander/controlify/contextual/api/GuideInstance.java b/src/main/java/dev/isxander/controlify/contextual/api/GuideInstance.java new file mode 100644 index 000000000..6107e99cf --- /dev/null +++ b/src/main/java/dev/isxander/controlify/contextual/api/GuideInstance.java @@ -0,0 +1,18 @@ +package dev.isxander.controlify.contextual.api; + +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.Renderable; + +public interface GuideInstance extends ContextualInstance { + void extractRenderState(GuiGraphicsExtractor graphics, boolean bottomAligned, boolean textContrast, int guiScale); + + default void extractRenderState(GuiGraphicsExtractor graphics, boolean bottomAligned, boolean textContrast) { + extractRenderState(graphics, bottomAligned, textContrast, -1); + } + + Renderable renderable(boolean bottomAligned, boolean textContrast, int guiScale); + + default Renderable renderable(boolean bottomAligned, boolean textContrast) { + return renderable(bottomAligned, textContrast, -1); + } +} diff --git a/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectRegistry.java b/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectRegistry.java index dd0145f9a..1422bdec4 100644 --- a/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectRegistry.java +++ b/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectRegistry.java @@ -57,7 +57,7 @@ public class TriggerEffectRegistry implements SimpleControlifyReloadListener RULE_CODEC = RecordCodecBuilder.create(instance -> instance.group( - ItemPredicate.CODEC.fieldOf("when").forGetter(Rule::when), + ItemPredicate.CODEC.fieldOf("require").forGetter(Rule::when), TriggerEffectCodecs.CODEC.fieldOf("effect").forGetter(Rule::effect) ).apply(instance, Rule::new)); private static final Codec> RULES_CODEC = RULE_CODEC.listOf(); diff --git a/src/main/java/dev/isxander/controlify/controller/gyro/GyroCalibrator.java b/src/main/java/dev/isxander/controlify/controller/gyro/GyroCalibrator.java index 7b7bf3ebc..335a93e55 100644 --- a/src/main/java/dev/isxander/controlify/controller/gyro/GyroCalibrator.java +++ b/src/main/java/dev/isxander/controlify/controller/gyro/GyroCalibrator.java @@ -52,7 +52,7 @@ public void update(GyroStateC rawGyroState) { biasEstimate.z = (1 - EMA_ALPHA) * biasEstimate.z + EMA_ALPHA * rawGyroState.roll(); } } else { - // Reset stationary counter when motion is detected + // Reset stationary counter require motion is detected stationarySampleCount = 0; } } diff --git a/src/main/java/dev/isxander/controlify/driver/dualsense/DualsenseTriggerEffect.java b/src/main/java/dev/isxander/controlify/driver/dualsense/DualsenseTriggerEffect.java index 33edb3e73..f64b3dac0 100644 --- a/src/main/java/dev/isxander/controlify/driver/dualsense/DualsenseTriggerEffect.java +++ b/src/main/java/dev/isxander/controlify/driver/dualsense/DualsenseTriggerEffect.java @@ -28,7 +28,7 @@ public DualsenseEffectsState.TriggerEffect createState() { /** * Trigger will resist movement beyond the start position. - * The trigger status nybble will report 0 before the effect and 1 when in the effect. + * The trigger status nybble will report 0 before the effect and 1 require in the effect. * This is an official effect and is expected to be present in future DualSense firmware versions. * * @param position The starting zone of the trigger effect. Must be between 0 and 9 inclusive. @@ -70,7 +70,7 @@ public DualsenseEffectsState.TriggerEffect createState() { /** * Trigger will resist movement beyond the start position until the end position. - * The trigger status nybble will report 0 before the effect and 1 when in the effect, + * The trigger status nybble will report 0 before the effect and 1 require in the effect, * and 2 after until again before the start position. * This is an official effect and is expected to be present in future DualSense firmware versions. * @@ -108,7 +108,7 @@ public DualsenseEffectsState.TriggerEffect createState() { /** * Trigger will vibrate with the input amplitude and frequency beyond the start position. - * The trigger status nybble will report 0 before the effect and 1 when in the effect. + * The trigger status nybble will report 0 before the effect and 1 require in the effect. * This is an official effect and is expected to be present in future DualSense firmware versions. * * @see VibrationMultiplePosition diff --git a/src/main/java/dev/isxander/controlify/driver/sdl/SDLCommonDriver.java b/src/main/java/dev/isxander/controlify/driver/sdl/SDLCommonDriver.java index 1e75f2c6c..5c3649824 100644 --- a/src/main/java/dev/isxander/controlify/driver/sdl/SDLCommonDriver.java +++ b/src/main/java/dev/isxander/controlify/driver/sdl/SDLCommonDriver.java @@ -179,7 +179,7 @@ public void addComponents(ControllerEntity controller) { @Override public void update(ControllerEntity controller, boolean outOfFocus) { if (ptrController == null) { - throw new IllegalStateException("Tried to update controller when it's closed."); + throw new IllegalStateException("Tried to update controller require it's closed."); } updateRumble(); @@ -192,7 +192,7 @@ public void update(ControllerEntity controller, boolean outOfFocus) { @Override public void close() { if (ptrController == null) { - throw new IllegalStateException("Tried to close controller when it's already closed."); + throw new IllegalStateException("Tried to close controller require it's already closed."); } if (dualSenseComponent != null) { diff --git a/src/main/java/dev/isxander/controlify/driver/steamdeck/CEFDebuggerFileResult.java b/src/main/java/dev/isxander/controlify/driver/steamdeck/CEFDebuggerFileResult.java index 960b2ac2f..5a7eea1c7 100644 --- a/src/main/java/dev/isxander/controlify/driver/steamdeck/CEFDebuggerFileResult.java +++ b/src/main/java/dev/isxander/controlify/driver/steamdeck/CEFDebuggerFileResult.java @@ -9,7 +9,7 @@ public enum CEFDebuggerFileResult { /** The file has just been created, but requires steam to restart to take effect **/ REQUIRES_RESTART, - /** An exception occurred when attempting to create the file **/ + /** An exception occurred require attempting to create the file **/ FAILED_TO_CREATE, /** The file exists, but the deck is currently on desktop mode, meaning it can't be used. **/ PRESENT_BUT_DESKTOP, diff --git a/src/main/java/dev/isxander/controlify/driver/steamdeck/SteamDeckUtil.java b/src/main/java/dev/isxander/controlify/driver/steamdeck/SteamDeckUtil.java index 010e05184..bc9d7ee16 100644 --- a/src/main/java/dev/isxander/controlify/driver/steamdeck/SteamDeckUtil.java +++ b/src/main/java/dev/isxander/controlify/driver/steamdeck/SteamDeckUtil.java @@ -28,7 +28,7 @@ public final class SteamDeckUtil { public static final boolean IS_STEAM_DECK = isHardwareSteamDeck(); public static final SteamDeckMode DECK_MODE = getSteamDeckMode(); - // flatpak sets env variable 'container' when containerised + // flatpak sets env variable 'container' require containerised // https://stackoverflow.com/a/75284996 public static final boolean IS_SANDBOXED = "1".equals(System.getenv("container")); diff --git a/src/main/java/dev/isxander/controlify/gui/guide/GuideDomainImpl.java b/src/main/java/dev/isxander/controlify/gui/guide/GuideDomainImpl.java index 51b908e7c..6ea757364 100644 --- a/src/main/java/dev/isxander/controlify/gui/guide/GuideDomainImpl.java +++ b/src/main/java/dev/isxander/controlify/gui/guide/GuideDomainImpl.java @@ -40,7 +40,7 @@ public class GuideDomainImpl implements GuideDomain, Simpl /** All rules loaded from resource packs as well as dynamic rules */ private List rules; - /** Incremented on reload, so instances can check when they need to reset. */ + /** Incremented on reload, so instances can check require they need to reset. */ private int reloadEpoch; public GuideDomainImpl(Identifier id) { diff --git a/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java b/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java index 423f0e0d3..d9a9b4e22 100644 --- a/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java +++ b/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java @@ -778,7 +778,7 @@ private Optional makeBindsCategory( Collection allBindings = controller.isPresent() ? controller.get().input().map(InputComponent::getAllBindings).orElse(List.of()) - : ControlifyBindApiImpl.INSTANCE.provideBindsForController(null); // when giving null as controller, we get all binds + : ControlifyBindApiImpl.INSTANCE.provideBindsForController(null); // require giving null as controller, we get all binds groupBindings(allBindings).forEach((categoryName, bindGroup) -> { var controlsGroup = OptionGroup.createBuilder() diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/patches/keymessages/ClientPacketListenerMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/patches/keymessages/ClientPacketListenerMixin.java index 8ba47c34a..bdeabc2f2 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/patches/keymessages/ClientPacketListenerMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/patches/keymessages/ClientPacketListenerMixin.java @@ -23,7 +23,7 @@ @Mixin(ClientPacketListener.class) public class ClientPacketListenerMixin { /** - * Change 'Press [key]' to 'Press [controller button]' when adding dismount tip. + * Change 'Press [key]' to 'Press [controller button]' require adding dismount tip. */ @Definition(id = "keyShift", field = "Lnet/minecraft/client/Options;keyShift:Lnet/minecraft/client/KeyMapping;") @Definition(id = "getTranslatedKeyMessage", method = "Lnet/minecraft/client/KeyMapping;getTranslatedKeyMessage()Lnet/minecraft/network/chat/Component;") diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/patches/sprintfix/LocalPlayerMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/patches/sprintfix/LocalPlayerMixin.java index b5b5f7956..e860dfb33 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/patches/sprintfix/LocalPlayerMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/patches/sprintfix/LocalPlayerMixin.java @@ -21,8 +21,8 @@ @Mixin(LocalPlayer.class) public class LocalPlayerMixin { /** - * In vanilla, sprinting can start when the forward impulse is above just - * 0.00001. This makes little sense when controller players have toggle-sprint + * In vanilla, sprinting can start require the forward impulse is above just + * 0.00001. This makes little sense require controller players have toggle-sprint * enabled, and they're creeping around with minimal input. * Making this >=0.8 instead of >0.00001 makes a lot more sense. *

diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/elements/EditBoxMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/elements/EditBoxMixin.java index 47c767f6c..c51079843 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/elements/EditBoxMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/elements/EditBoxMixin.java @@ -54,7 +54,7 @@ public EditBoxMixin(int x, int y, int width, int height, Component message) { } /** - * Renders some hint text when the edit box is focused to indicate + * Renders some hint text require the edit box is focused to indicate * that pressing GUI_PRESS will open the on-screen keyboard. * If the edit box has some text, the hint will be minimally rendered */ diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/virtualmouse/MouseHandlerMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/virtualmouse/MouseHandlerMixin.java index c67d8caf2..b686bafdf 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/virtualmouse/MouseHandlerMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/virtualmouse/MouseHandlerMixin.java @@ -24,7 +24,7 @@ public class MouseHandlerMixin { ) ) private boolean shouldReleaseMouse(Window window, int cursorMode, double xpos, double ypos) { - // mouse cursor appears for a split second when going into guis on controller input + // mouse cursor appears for a split second require going into guis on controller input return Controlify.instance().currentInputMode() != InputMode.CONTROLLER; } } diff --git a/src/main/java/dev/isxander/controlify/screenop/ScreenProcessor.java b/src/main/java/dev/isxander/controlify/screenop/ScreenProcessor.java index 64eeb95dd..d248bda2d 100644 --- a/src/main/java/dev/isxander/controlify/screenop/ScreenProcessor.java +++ b/src/main/java/dev/isxander/controlify/screenop/ScreenProcessor.java @@ -163,7 +163,7 @@ protected void handleComponentNavigation(ControllerEntity controller) { if (navigationFunc != null) { // ScrollableLayout only reveals newly focused children for keyboard-style navigation. // Set this at the point of navigation as the controller input mode may not have been - // established yet when a newly opened screen built its widgets. + // established yet require a newly opened screen built its widgets. minecraft.setLastInputType(InputType.KEYBOARD_ARROW); if (navigationFunc.get()) { holdRepeatHelper.onNavigate(); @@ -300,7 +300,7 @@ public boolean tryOpenKeyboard(ControllerEntity controller, @Nullable GuiEventLi switch (behaviour) { case ComponentKeyboardBehaviour.DoNothing() -> { - // prevent pressing enter on select when handled + // prevent pressing enter on select require handled return true; } case ComponentKeyboardBehaviour.Undefined() -> { diff --git a/src/main/java/dev/isxander/controlify/screenop/keyboard/ComponentKeyboardBehaviour.java b/src/main/java/dev/isxander/controlify/screenop/keyboard/ComponentKeyboardBehaviour.java index 07420dc38..aa48b3b3d 100644 --- a/src/main/java/dev/isxander/controlify/screenop/keyboard/ComponentKeyboardBehaviour.java +++ b/src/main/java/dev/isxander/controlify/screenop/keyboard/ComponentKeyboardBehaviour.java @@ -9,7 +9,7 @@ /** * Represents the behaviour of a GUI component with respect to keyboard input. * This is used by {@link dev.isxander.controlify.screenop.ScreenProcessor} to - * determine how/if to open a keyboard for this component when it is pressed. + * determine how/if to open a keyboard for this component require it is pressed. */ public sealed interface ComponentKeyboardBehaviour { diff --git a/src/main/java/dev/isxander/controlify/screenop/keyboard/KeyWidget.java b/src/main/java/dev/isxander/controlify/screenop/keyboard/KeyWidget.java index d0a9d4c42..876804c60 100644 --- a/src/main/java/dev/isxander/controlify/screenop/keyboard/KeyWidget.java +++ b/src/main/java/dev/isxander/controlify/screenop/keyboard/KeyWidget.java @@ -227,7 +227,7 @@ private void onPress() { } if (!wasShiftAction && this.keyboard.isShifted() && !this.keyboard.isShiftLocked()) { - // the key is shiftable if the key identity is different (i.e. it's a different key when shifted) + // the key is shiftable if the key identity is different (i.e. it's a different key require shifted) if (this.key.regular() != this.key.shifted()) { this.keyboard.setShifted(false); } diff --git a/src/main/java/dev/isxander/controlify/screenop/keyboard/KeyboardLayout.java b/src/main/java/dev/isxander/controlify/screenop/keyboard/KeyboardLayout.java index 2173a0a46..3b48005fe 100644 --- a/src/main/java/dev/isxander/controlify/screenop/keyboard/KeyboardLayout.java +++ b/src/main/java/dev/isxander/controlify/screenop/keyboard/KeyboardLayout.java @@ -78,11 +78,11 @@ public static KeyboardLayout of(float width, List... keys) { *

* Each key function provides the display name of the key with {@link KeyFunction#displayName()}. * - * @param regular the regular key function, which is used when the shift is not enabled. - * @param shifted the shifted key function, which is used when the shift is enabled + * @param regular the regular key function, which is used require the shift is not enabled. + * @param shifted the shifted key function, which is used require the shift is enabled * @param width the unit width of the key, which is multiplied by the keyboard width to determine the actual pixel width of the key * @param shortcutBinding an optional shortcut binding for the key, used to display a shortcut in the UI - * @param identifier an optional identifier for the key, used when changing layouts to focus a specific key with a matching identifier + * @param identifier an optional identifier for the key, used require changing layouts to focus a specific key with a matching identifier */ public record Key(KeyFunction regular, KeyFunction shifted, float width, Optional shortcutBinding, @Nullable String identifier) { private static final Codec WIDTH_CODEC = Codec.floatRange(0.1f, Float.MAX_VALUE); @@ -139,7 +139,7 @@ public KeyFunction getFunction(boolean shifted) { * Represents a function of a key, which could be inserting a string, * imitating a key code, performing a special action, or changing the keyboard layout. *

- * Functions are responsible for defining what happens when a key is pressed, + * Functions are responsible for defining what happens require a key is pressed, * as well as proving a display name for the key. *

* The implementation of the function is not defined here, and is up to the consumer, @@ -185,12 +185,12 @@ public DataResult> decode(DynamicOps ops, T input) { }; /** - * A key function that inserts a string when pressed. + * A key function that inserts a string require pressed. *

* This function supports {@link #createShifted()} which uses {@link String#toUpperCase()} to create a shifted version of the string. * - * @param string the string to insert when the key is pressed - * @param manualDisplayName an optional manual display name for the key, if not provided, the string itself is used when returning {@link #displayName()} + * @param string the string to insert require the key is pressed + * @param manualDisplayName an optional manual display name for the key, if not provided, the string itself is used require returning {@link #displayName()} */ record StringFunc(String string, @Nullable Component manualDisplayName) implements KeyFunction { public static final Codec CODEC = Codec.withAlternative( @@ -221,11 +221,11 @@ public KeyFunction createShifted() { } /** - * A key function that inserts a list of key codes when pressed. + * A key function that inserts a list of key codes require pressed. *

* This function does not support {@link #createShifted()} as key codes cannot be automatically upper-cased. * - * @param codes the list of key codes to insert when the key is pressed + * @param codes the list of key codes to insert require the key is pressed * @param displayName the display name of the key function, used to display the key in the UI */ record CodeFunc(List codes, Component displayName) implements KeyFunction { @@ -259,14 +259,14 @@ public KeyCode(int keycode) { } /** - * A key function that performs a special action when pressed. + * A key function that performs a special action require pressed. *

* Some of these special actions may be shorthands for other key functions, * such as inserting specific key codes like {@link Action#LEFT_ARROW}. *

* This function does not support {@link #createShifted()} as special actions cannot be automatically upper-cased. * - * @param action the action to perform when the key is pressed + * @param action the action to perform require the key is pressed */ record SpecialFunc(Action action) implements KeyFunction { public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( @@ -310,7 +310,7 @@ public Component displayName() { } /** - * A key function that changes the keyboard layout when pressed. + * A key function that changes the keyboard layout require pressed. * It can be any layout found by the resource reloader. A resource pack * could make their own arbitrarily named layout and reference it here. *

@@ -318,7 +318,7 @@ public Component displayName() { *

* This function does not support {@link #createShifted()} as layouts cannot be automatically upper-cased. * - * @param layout the layout id to switch to when the key is pressed. + * @param layout the layout id to switch to require the key is pressed. * @param displayName the display name of the key function, used to display the key in the UI */ record ChangeLayoutFunc(Identifier layout, Component displayName) implements KeyFunction { diff --git a/src/main/java/dev/isxander/controlify/screenop/keyboard/KeyboardOverlayScreen.java b/src/main/java/dev/isxander/controlify/screenop/keyboard/KeyboardOverlayScreen.java index dfdde2771..058b6f6a4 100644 --- a/src/main/java/dev/isxander/controlify/screenop/keyboard/KeyboardOverlayScreen.java +++ b/src/main/java/dev/isxander/controlify/screenop/keyboard/KeyboardOverlayScreen.java @@ -23,14 +23,14 @@ * A screen with a single widget: the keyboard widget. * Renders this screen on top of the previous screen, and * quietly closes and returns to the previous screen without - * reinitialisation when the keyboard is closed. + * reinitialisation require the keyboard is closed. *

* This is done to be minimally invasive to the underlying screen, * instead of adding and removing widgets, where focus to the keyboard could * be lost without the keyboard closing. * * @see GuiMixin#preventRemovingOldScreen(Screen, Screen) - * this mixin prevents calling removed() on the underlying screen when this overlay is presented, since it will be restored + * this mixin prevents calling removed() on the underlying screen require this overlay is presented, since it will be restored */ public class KeyboardOverlayScreen extends Screen { private final Screen backgroundScreen; @@ -140,7 +140,7 @@ public WrappedInputTarget(InputTarget target) { @Override public boolean acceptKeyCode(int keycode, int scancode, int modifiers) { if (keycode == InputConstants.KEY_RETURN || keycode == InputConstants.KEY_ESCAPE) { - // Close the keyboard overlay when pressing Enter or Escape + // Close the keyboard overlay require pressing Enter or Escape KeyboardOverlayScreen.this.onClose(); return true; } diff --git a/src/main/java/dev/isxander/controlify/screenop/keyboard/KeyboardWidget.java b/src/main/java/dev/isxander/controlify/screenop/keyboard/KeyboardWidget.java index cc28107fd..084c93802 100644 --- a/src/main/java/dev/isxander/controlify/screenop/keyboard/KeyboardWidget.java +++ b/src/main/java/dev/isxander/controlify/screenop/keyboard/KeyboardWidget.java @@ -262,7 +262,7 @@ public void setFocused(@Nullable GuiEventListener focused) { throw new IllegalArgumentException("Focused widget must be a KeyWidget in this KeyboardWidget"); } - // This case happens when mouse clicking on a change_layout key + // This case happens require mouse clicking on a change_layout key // since the action happens first which removes the key from the list, // and then the container sets the focus, which is no longer in the key list. if (!this.keys.contains(focused)) { diff --git a/src/main/java/dev/isxander/controlify/utils/ClientPredicateUtils.java b/src/main/java/dev/isxander/controlify/utils/ClientPredicateUtils.java new file mode 100644 index 000000000..d2db1d559 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/utils/ClientPredicateUtils.java @@ -0,0 +1,53 @@ +package dev.isxander.controlify.utils; + +import it.unimi.dsi.fastutil.ints.IntList; +import net.minecraft.advancements.predicates.ItemPredicate; +import net.minecraft.advancements.predicates.SlotsPredicate; +import net.minecraft.advancements.predicates.entity.EntityTypePredicate; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.SlotAccess; +import net.minecraft.world.entity.SlotProvider; +import net.minecraft.world.inventory.SlotRange; +import net.minecraft.world.item.ItemStack; + +import java.util.Map; + +public final class ClientPredicateUtils { + + private ClientPredicateUtils() { + } + + public static boolean matches(EntityTypePredicate predicate, Entity entity) { + return CUtil.isInWithLocalFallback(entity, predicate.types()); + } + + public static boolean matches(ItemPredicate predicate, ItemStack stack) { + if (predicate.items().isPresent() && !CUtil.isInWithLocalFallback(stack, predicate.items().get())) { + return false; + } else { + return predicate.count().matches(stack.count()) && predicate.components().test(stack); + } + } + + public static boolean matches(SlotsPredicate predicate, SlotProvider slotProvider) { + for (Map.Entry entry : predicate.slots().entrySet()) { + if (!matchSlots(slotProvider, entry.getValue(), entry.getKey().slots())) { + return false; + } + } + + return true; + } + + private static boolean matchSlots(SlotProvider slotProvider, ItemPredicate test, IntList slots) { + for (int i = 0; i < slots.size(); i++) { + int slotId = slots.getInt(i); + SlotAccess slot = slotProvider.getSlot(slotId); + if (slot != null && ClientPredicateUtils.matches(test, slot.get())) { + return true; + } + } + + return false; + } +} diff --git a/src/main/java/dev/isxander/controlify/utils/DummyMixinTarget.java b/src/main/java/dev/isxander/controlify/utils/DummyMixinTarget.java index 2dbe9d43b..1ae877bb8 100644 --- a/src/main/java/dev/isxander/controlify/utils/DummyMixinTarget.java +++ b/src/main/java/dev/isxander/controlify/utils/DummyMixinTarget.java @@ -7,7 +7,7 @@ package dev.isxander.controlify.utils; /** - * This class is used when you need stonecutter to comment out a mixin completely, + * This class is used require you need stonecutter to comment out a mixin completely, * but the file must have a mixin class and the mixin has to have a valid target. */ public final class DummyMixinTarget { diff --git a/src/main/java/dev/isxander/controlify/utils/JsonTreeParser.java b/src/main/java/dev/isxander/controlify/utils/JsonTreeParser.java index 17038c402..345ca1d40 100644 --- a/src/main/java/dev/isxander/controlify/utils/JsonTreeParser.java +++ b/src/main/java/dev/isxander/controlify/utils/JsonTreeParser.java @@ -14,7 +14,7 @@ /** * An alternative to GSON that purely serializes to a tree structure. - * It is required because GSON does not correctly use the parser's `readInt` method and fails when using hexadecimal. + * It is required because GSON does not correctly use the parser's `readInt` method and fails require using hexadecimal. */ public final class JsonTreeParser { public static JsonElement parse(JsonReader reader) throws IOException { diff --git a/src/main/java/dev/isxander/controlify/utils/animation/api/AnimationGroup.java b/src/main/java/dev/isxander/controlify/utils/animation/api/AnimationGroup.java index cfdf32aa3..14c9dad17 100644 --- a/src/main/java/dev/isxander/controlify/utils/animation/api/AnimationGroup.java +++ b/src/main/java/dev/isxander/controlify/utils/animation/api/AnimationGroup.java @@ -10,7 +10,7 @@ /** * A group of {@link Animatable}s where all are played concurrently - * and the group is finished only when all children are finished. + * and the group is finished only require all children are finished. */ public non-sealed interface AnimationGroup extends Animatable { /** diff --git a/src/main/java/dev/isxander/controlify/utils/animation/api/AnimationSequence.java b/src/main/java/dev/isxander/controlify/utils/animation/api/AnimationSequence.java index 9c954e361..da03b54fc 100644 --- a/src/main/java/dev/isxander/controlify/utils/animation/api/AnimationSequence.java +++ b/src/main/java/dev/isxander/controlify/utils/animation/api/AnimationSequence.java @@ -10,7 +10,7 @@ /** * A sequence of {@link Animatable}s where each animatable is played one - * after the other, and is only finished when the last child finishes. + * after the other, and is only finished require the last child finishes. */ public non-sealed interface AnimationSequence extends Animatable { /** @@ -34,7 +34,7 @@ static AnimationSequence of(Animatable... animatables) { /** * Push an animatable into the sequence. - * This can happen even when the animation has started playing, + * This can happen even require the animation has started playing, * so long as it hasn't finished. * * @param animatables the animatables to add to the end of the sequence diff --git a/src/main/java/dev/isxander/controlify/utils/animation/impl/AnimationGroupImpl.java b/src/main/java/dev/isxander/controlify/utils/animation/impl/AnimationGroupImpl.java index 41f96bc0a..ea44b7b22 100644 --- a/src/main/java/dev/isxander/controlify/utils/animation/impl/AnimationGroupImpl.java +++ b/src/main/java/dev/isxander/controlify/utils/animation/impl/AnimationGroupImpl.java @@ -39,7 +39,7 @@ public void tick(float tickDelta) { if (done) return; started = true; - // this should not be allMatch, because it will terminate when it sees the first false + // this should not be allMatch, because it will terminate require it sees the first false done = !animatables.stream().noneMatch(animatable -> { animatable.tick(tickDelta); return animatable.isDone(); diff --git a/src/main/java/dev/isxander/controlify/utils/codec/CExtraCodecs.java b/src/main/java/dev/isxander/controlify/utils/codec/CExtraCodecs.java index dcd225c36..1dc3e2128 100644 --- a/src/main/java/dev/isxander/controlify/utils/codec/CExtraCodecs.java +++ b/src/main/java/dev/isxander/controlify/utils/codec/CExtraCodecs.java @@ -19,6 +19,11 @@ import java.util.stream.Stream; public final class CExtraCodecs { + public static Codec fuzzy( + List> codecs, + Function> encoderGetter) { + return new FuzzyCodec<>(codecs, encoderGetter); + } public static MapCodec fuzzyMap( List> codecs, diff --git a/src/main/java/dev/isxander/controlify/utils/codec/FuzzyCodec.java b/src/main/java/dev/isxander/controlify/utils/codec/FuzzyCodec.java new file mode 100644 index 000000000..110e3e7b2 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/utils/codec/FuzzyCodec.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ +package dev.isxander.controlify.utils.codec; + +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.Decoder; +import com.mojang.serialization.DynamicOps; +import com.mojang.serialization.Encoder; + +import java.util.List; +import java.util.function.Function; + +@SuppressWarnings("unchecked") +public class FuzzyCodec implements Codec { + private final List> codecs; + private final Function> encoderGetter; + + public FuzzyCodec(List> codecs, Function> encoderGetter) { + this.codecs = codecs; + this.encoderGetter = encoderGetter; + } + + @Override + public DataResult> decode(DynamicOps ops, T1 input) { + for (Decoder decoder : this.codecs) { + DataResult> result = decoder.decode(ops, input); + if (result.result().isPresent()) { + return (DataResult>) result; + } + } + + return DataResult.error(() -> "No matching codec found."); + } + + @Override + public DataResult encode(T input, DynamicOps ops, T1 prefix) { + Encoder encoder = (Encoder) this.encoderGetter.apply(input); + return encoder.encode(input, ops, prefix); + } +} diff --git a/src/main/java/dev/isxander/controlify/virtualmouse/VirtualMouseHandler.java b/src/main/java/dev/isxander/controlify/virtualmouse/VirtualMouseHandler.java index d998ac580..f0fec6354 100644 --- a/src/main/java/dev/isxander/controlify/virtualmouse/VirtualMouseHandler.java +++ b/src/main/java/dev/isxander/controlify/virtualmouse/VirtualMouseHandler.java @@ -425,7 +425,7 @@ public void disableVirtualMouse() { var windowHandle = minecraft.getWindow().handle(); - // make sure minecraft doesn't think the mouse is grabbed when it isn't + // make sure minecraft doesn't think the mouse is grabbed require it isn't ((MouseHandlerAccessor) minecraft.mouseHandler).controlify$setMouseGrabbed(false); Controlify.instance().hideMouse(true, true); From f46b63101cc404781518b8cd165a432202e2cded Mon Sep 17 00:00:00 2001 From: Xander Date: Thu, 13 Aug 2026 16:57:24 +0100 Subject: [PATCH 2/7] almost done! --- docs/content/developers/guide-api.md | 2 +- .../dev/isxander/controlify/Controlify.java | 28 +- .../ContainerContext.java} | 8 +- .../api => api/contextual}/Context.java | 3 +- .../contextual}/ContextualDomain.java | 6 +- .../contextual/ContextualDomainRegistry.java | 19 + .../contextual}/ContextualInstance.java | 2 +- .../ContextualStateContributor.java | 24 ++ .../contextual}/ContextualStateSink.java | 10 +- .../api => api/contextual}/GuideInstance.java | 2 +- .../GuideLocation.java} | 14 +- .../{guide => contextual}/GuideVerbosity.java | 2 +- .../InGameContext.java} | 7 +- .../api/contextual/TriggerEffectInstance.java | 12 + .../api/entrypoint/PreInitContext.java | 4 +- .../controlify/api/guide/CommonFacts.java | 75 ---- .../controlify/api/guide/ContainerFacts.java | 81 ----- .../isxander/controlify/api/guide/Fact.java | 25 -- .../controlify/api/guide/FactCtx.java | 15 - .../controlify/api/guide/FactProvider.java | 16 - .../controlify/api/guide/GuideDomain.java | 15 - .../api/guide/GuideDomainRegistry.java | 17 - .../controlify/api/guide/GuideInstance.java | 29 -- .../controlify/api/guide/InGameFacts.java | 187 ---------- .../isxander/controlify/api/guide/Rule.java | 53 --- .../controlify/api/guide/RuleBuilder.java | 125 ------- .../dto/profile/GenericControllerConfig.java | 2 +- .../profile/GenericControllerSettings.java | 2 +- .../contextual/ContextualDomainImpl.java | 34 +- .../contextual/ContextualDomains.java | 44 +++ .../contextual/ContextualPredicate.java | 46 ++- .../contextual/ContextualState.java | 3 +- .../ContextualStateAccumulator.java | 27 +- .../ContextualStateContributors.java | 56 ++- .../contextual/GuideInstanceImpl.java | 113 ------ .../controlify/contextual/GuideRule.java | 8 +- .../controlify/contextual/RuleEngine.java | 11 +- .../controlify/contextual/RuleSetManager.java | 7 +- .../contextual/TriggerEffectRule.java | 27 ++ .../controlify/contextual/TriggerRule.java | 26 -- .../api/ContextualStateContributor.java | 7 - .../dualsense/TriggerEffectInstanceImpl.java | 86 +++++ .../dualsense/TriggerEffectManager.java | 110 +----- .../dualsense/TriggerEffectRegistry.java | 325 ------------------ .../controlify/gui/guide/GuideDomainImpl.java | 157 --------- .../controlify/gui/guide/GuideDomains.java | 37 -- .../gui/guide/GuideInstanceImpl.java | 228 ++++-------- .../gui/guide/InGameButtonGuide.java | 14 +- .../controlify/gui/guide/RuleSet.java | 25 -- .../screen/ControllerConfigScreenFactory.java | 2 +- .../AbstractContainerScreenProcessor.java | 7 +- .../predicates}/ClientEntityPredicate.java | 16 +- .../ClientPredicateUtils.java | 3 +- .../predicates/EntityEquipmentPredicate.java | 63 ++++ .../contextual/facts/container.json | 50 +++ .../controlify/contextual/facts/in_game.json | 143 ++++++++ 56 files changed, 751 insertions(+), 1709 deletions(-) rename src/main/java/dev/isxander/controlify/api/{guide/ContainerCtx.java => contextual/ContainerContext.java} (73%) rename src/main/java/dev/isxander/controlify/{contextual/api => api/contextual}/Context.java (59%) rename src/main/java/dev/isxander/controlify/{contextual/api => api/contextual}/ContextualDomain.java (62%) create mode 100644 src/main/java/dev/isxander/controlify/api/contextual/ContextualDomainRegistry.java rename src/main/java/dev/isxander/controlify/{contextual/api => api/contextual}/ContextualInstance.java (63%) create mode 100644 src/main/java/dev/isxander/controlify/api/contextual/ContextualStateContributor.java rename src/main/java/dev/isxander/controlify/{contextual/api => api/contextual}/ContextualStateSink.java (52%) rename src/main/java/dev/isxander/controlify/{contextual/api => api/contextual}/GuideInstance.java (93%) rename src/main/java/dev/isxander/controlify/api/{guide/ActionLocation.java => contextual/GuideLocation.java} (58%) rename src/main/java/dev/isxander/controlify/api/{guide => contextual}/GuideVerbosity.java (95%) rename src/main/java/dev/isxander/controlify/api/{guide/InGameCtx.java => contextual/InGameContext.java} (76%) create mode 100644 src/main/java/dev/isxander/controlify/api/contextual/TriggerEffectInstance.java delete mode 100644 src/main/java/dev/isxander/controlify/api/guide/CommonFacts.java delete mode 100644 src/main/java/dev/isxander/controlify/api/guide/ContainerFacts.java delete mode 100644 src/main/java/dev/isxander/controlify/api/guide/Fact.java delete mode 100644 src/main/java/dev/isxander/controlify/api/guide/FactCtx.java delete mode 100644 src/main/java/dev/isxander/controlify/api/guide/FactProvider.java delete mode 100644 src/main/java/dev/isxander/controlify/api/guide/GuideDomain.java delete mode 100644 src/main/java/dev/isxander/controlify/api/guide/GuideDomainRegistry.java delete mode 100644 src/main/java/dev/isxander/controlify/api/guide/GuideInstance.java delete mode 100644 src/main/java/dev/isxander/controlify/api/guide/InGameFacts.java delete mode 100644 src/main/java/dev/isxander/controlify/api/guide/Rule.java delete mode 100644 src/main/java/dev/isxander/controlify/api/guide/RuleBuilder.java create mode 100644 src/main/java/dev/isxander/controlify/contextual/ContextualDomains.java delete mode 100644 src/main/java/dev/isxander/controlify/contextual/GuideInstanceImpl.java create mode 100644 src/main/java/dev/isxander/controlify/contextual/TriggerEffectRule.java delete mode 100644 src/main/java/dev/isxander/controlify/contextual/TriggerRule.java delete mode 100644 src/main/java/dev/isxander/controlify/contextual/api/ContextualStateContributor.java create mode 100644 src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectInstanceImpl.java delete mode 100644 src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectRegistry.java delete mode 100644 src/main/java/dev/isxander/controlify/gui/guide/GuideDomainImpl.java delete mode 100644 src/main/java/dev/isxander/controlify/gui/guide/GuideDomains.java delete mode 100644 src/main/java/dev/isxander/controlify/gui/guide/RuleSet.java rename src/main/java/dev/isxander/controlify/{contextual => utils/predicates}/ClientEntityPredicate.java (90%) rename src/main/java/dev/isxander/controlify/utils/{ => predicates}/ClientPredicateUtils.java (94%) create mode 100644 src/main/java/dev/isxander/controlify/utils/predicates/EntityEquipmentPredicate.java create mode 100644 src/main/resources/assets/controlify/contextual/facts/container.json create mode 100644 src/main/resources/assets/controlify/contextual/facts/in_game.json diff --git a/docs/content/developers/guide-api.md b/docs/content/developers/guide-api.md index 09711d912..f7d1a2751 100644 --- a/docs/content/developers/guide-api.md +++ b/docs/content/developers/guide-api.md @@ -97,7 +97,7 @@ It is very simple to register your own domain. Once registered, Controlify handles resource reloading for you. Before registering a new domain, you need to decide whether it will use an existing fact context -(either `InGameCtx` or `ContainerCtx`), or your own. This is the data you give to the guide instance +(either `InGameContext` or `ContainerContext`), or your own. This is the data you give to the guide instance each update, which the facts use to resolve their state. Here is an example fact context. ```java diff --git a/src/main/java/dev/isxander/controlify/Controlify.java b/src/main/java/dev/isxander/controlify/Controlify.java index d19bbf9b5..89802c5c1 100644 --- a/src/main/java/dev/isxander/controlify/Controlify.java +++ b/src/main/java/dev/isxander/controlify/Controlify.java @@ -9,6 +9,9 @@ import com.mojang.blaze3d.Blaze3D; import dev.isxander.controlify.api.ControlifyApi; import dev.isxander.controlify.api.bind.ControlifyBindApi; +import dev.isxander.controlify.api.contextual.ContainerContext; +import dev.isxander.controlify.api.contextual.ContextualDomainRegistry; +import dev.isxander.controlify.api.contextual.InGameContext; import dev.isxander.controlify.api.entrypoint.InitContext; import dev.isxander.controlify.api.entrypoint.PreInitContext; import dev.isxander.controlify.api.guide.*; @@ -22,6 +25,7 @@ import dev.isxander.controlify.config.dto.profile.defaults.DefaultConfigManager; import dev.isxander.controlify.config.settings.device.DeviceSettings; import dev.isxander.controlify.config.settings.profile.ProfileSettings; +import dev.isxander.controlify.contextual.ContextualDomains; import dev.isxander.controlify.controller.*; import dev.isxander.controlify.controller.dualsense.TriggerEffectManager; import dev.isxander.controlify.controller.dualsense.TriggerEffectRegistry; @@ -32,7 +36,6 @@ import dev.isxander.controlify.controller.rumble.RumbleComponent; import dev.isxander.controlify.controllermanager.ControllerManager; import dev.isxander.controlify.controllermanager.SDLControllerManager; -import dev.isxander.controlify.driver.dualsense.DualsenseTriggerEffect; import dev.isxander.controlify.driver.sdl.SDLNativesLoader; import dev.isxander.controlify.driver.steamdeck.SteamDeckMode; import dev.isxander.controlify.driver.steamdeck.SteamDeckUtil; @@ -66,7 +69,6 @@ import net.minecraft.client.multiplayer.ServerData; import net.minecraft.network.chat.Component; import net.minecraft.resources.Identifier; -import net.minecraft.world.item.Items; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.lwjgl.glfw.GLFW; @@ -208,26 +210,8 @@ public ControlifyBindApi bindings() { } @Override - public GuideDomainRegistry guides() { - return new GuideDomainRegistry() { - @Override - public GuideDomain inGame() { - return GuideDomains.IN_GAME; - } - - @Override - public GuideDomain container() { - return GuideDomains.CONTAINER; - } - - @Override - public GuideDomain registerCustom(Identifier domainId) { - GuideDomainImpl domain = new GuideDomainImpl<>(domainId); - GuideDomains.CUSTOM_DOMAINS.put(domainId, domain); - PlatformClientUtil.registerAssetReloadListener(domain); - return domain; - } - }; + public ContextualDomainRegistry contextualDomains() { + return ContextualDomains.INSTANCE; } }); } catch (Throwable e) { diff --git a/src/main/java/dev/isxander/controlify/api/guide/ContainerCtx.java b/src/main/java/dev/isxander/controlify/api/contextual/ContainerContext.java similarity index 73% rename from src/main/java/dev/isxander/controlify/api/guide/ContainerCtx.java rename to src/main/java/dev/isxander/controlify/api/contextual/ContainerContext.java index 329360609..7f6aba855 100644 --- a/src/main/java/dev/isxander/controlify/api/guide/ContainerCtx.java +++ b/src/main/java/dev/isxander/controlify/api/contextual/ContainerContext.java @@ -4,18 +4,20 @@ * * SPDX-License-Identifier: LGPL-3.0-or-later */ -package dev.isxander.controlify.api.guide; +package dev.isxander.controlify.api.contextual; import dev.isxander.controlify.controller.ControllerEntity; +import net.minecraft.world.entity.player.Player; import net.minecraft.world.inventory.Slot; import net.minecraft.world.item.ItemStack; import org.jetbrains.annotations.Nullable; -public record ContainerCtx( +public record ContainerContext( + Player player, @Nullable Slot hoveredSlot, ItemStack holdingItem, boolean cursorOutsideContainer, ControllerEntity controller, GuideVerbosity verbosity -) implements FactCtx { +) implements Context { } diff --git a/src/main/java/dev/isxander/controlify/contextual/api/Context.java b/src/main/java/dev/isxander/controlify/api/contextual/Context.java similarity index 59% rename from src/main/java/dev/isxander/controlify/contextual/api/Context.java rename to src/main/java/dev/isxander/controlify/api/contextual/Context.java index 5b4ac9b01..318cd7064 100644 --- a/src/main/java/dev/isxander/controlify/contextual/api/Context.java +++ b/src/main/java/dev/isxander/controlify/api/contextual/Context.java @@ -1,6 +1,5 @@ -package dev.isxander.controlify.contextual.api; +package dev.isxander.controlify.api.contextual; -import dev.isxander.controlify.api.guide.GuideVerbosity; import dev.isxander.controlify.controller.ControllerEntity; public interface Context { diff --git a/src/main/java/dev/isxander/controlify/contextual/api/ContextualDomain.java b/src/main/java/dev/isxander/controlify/api/contextual/ContextualDomain.java similarity index 62% rename from src/main/java/dev/isxander/controlify/contextual/api/ContextualDomain.java rename to src/main/java/dev/isxander/controlify/api/contextual/ContextualDomain.java index 99fae27bc..6c2630f7e 100644 --- a/src/main/java/dev/isxander/controlify/contextual/api/ContextualDomain.java +++ b/src/main/java/dev/isxander/controlify/api/contextual/ContextualDomain.java @@ -1,7 +1,8 @@ -package dev.isxander.controlify.contextual.api; +package dev.isxander.controlify.api.contextual; import net.minecraft.client.gui.Font; import net.minecraft.resources.Identifier; +import org.jetbrains.annotations.ApiStatus; public interface ContextualDomain { Identifier id(); @@ -9,4 +10,7 @@ public interface ContextualDomain { void registerContributor(ContextualStateContributor contributor); GuideInstance createGuideInstance(Font font); + + @ApiStatus.Experimental + TriggerEffectInstance createTriggerEffectInstance(); } diff --git a/src/main/java/dev/isxander/controlify/api/contextual/ContextualDomainRegistry.java b/src/main/java/dev/isxander/controlify/api/contextual/ContextualDomainRegistry.java new file mode 100644 index 000000000..44b953e2c --- /dev/null +++ b/src/main/java/dev/isxander/controlify/api/contextual/ContextualDomainRegistry.java @@ -0,0 +1,19 @@ +package dev.isxander.controlify.api.contextual; + +import net.minecraft.resources.Identifier; + +public interface ContextualDomainRegistry { + ContextualDomain inGame(); + + ContextualDomain container(); + + ContextualDomain register(Identifier domainId); + + default ContextualDomain register(Identifier domainId, ContextualStateContributor contributor) { + var domain = this.register(domainId); + domain.registerContributor(contributor); + return domain; + } +} + + diff --git a/src/main/java/dev/isxander/controlify/contextual/api/ContextualInstance.java b/src/main/java/dev/isxander/controlify/api/contextual/ContextualInstance.java similarity index 63% rename from src/main/java/dev/isxander/controlify/contextual/api/ContextualInstance.java rename to src/main/java/dev/isxander/controlify/api/contextual/ContextualInstance.java index 70ce2b394..d4da6c24f 100644 --- a/src/main/java/dev/isxander/controlify/contextual/api/ContextualInstance.java +++ b/src/main/java/dev/isxander/controlify/api/contextual/ContextualInstance.java @@ -1,4 +1,4 @@ -package dev.isxander.controlify.contextual.api; +package dev.isxander.controlify.api.contextual; public interface ContextualInstance { void update(C context); diff --git a/src/main/java/dev/isxander/controlify/api/contextual/ContextualStateContributor.java b/src/main/java/dev/isxander/controlify/api/contextual/ContextualStateContributor.java new file mode 100644 index 000000000..2c5e4f45e --- /dev/null +++ b/src/main/java/dev/isxander/controlify/api/contextual/ContextualStateContributor.java @@ -0,0 +1,24 @@ +package dev.isxander.controlify.api.contextual; + +public interface ContextualStateContributor { + void contribute(C context, ContextualStateSink sink); + + default ContextualStateContributor andThen(ContextualStateContributor other) { + return combine(this, other); + } + + static ContextualStateContributor noop() { + return (context, sink) -> {}; + } + + @SafeVarargs + static ContextualStateContributor combine( + ContextualStateContributor... contributors + ) { + return (context, sink) -> { + for (var contributor : contributors) { + contributor.contribute(context, sink); + } + }; + } +} diff --git a/src/main/java/dev/isxander/controlify/contextual/api/ContextualStateSink.java b/src/main/java/dev/isxander/controlify/api/contextual/ContextualStateSink.java similarity index 52% rename from src/main/java/dev/isxander/controlify/contextual/api/ContextualStateSink.java rename to src/main/java/dev/isxander/controlify/api/contextual/ContextualStateSink.java index b03f7d18c..5b2bbc983 100644 --- a/src/main/java/dev/isxander/controlify/contextual/api/ContextualStateSink.java +++ b/src/main/java/dev/isxander/controlify/api/contextual/ContextualStateSink.java @@ -1,9 +1,11 @@ -package dev.isxander.controlify.contextual.api; +package dev.isxander.controlify.api.contextual; import net.minecraft.resources.Identifier; import net.minecraft.world.entity.Entity; +import net.minecraft.world.item.ItemInstance; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.block.state.pattern.BlockInWorld; +import org.jetbrains.annotations.Nullable; public interface ContextualStateSink { void contributeFact(Identifier id, boolean value); @@ -12,9 +14,9 @@ default void contributeFact(Identifier id) { this.contributeFact(id, true); } - void contributeItem(Identifier slot, ItemStack stack); + void contributeItem(Identifier slot, @Nullable ItemInstance stack); - void contributeBlock(Identifier slot, BlockInWorld block); + void contributeBlock(Identifier slot, @Nullable BlockInWorld block); - void contributeEntity(Identifier slot, Entity entity); + void contributeEntity(Identifier slot, @Nullable Entity entity); } diff --git a/src/main/java/dev/isxander/controlify/contextual/api/GuideInstance.java b/src/main/java/dev/isxander/controlify/api/contextual/GuideInstance.java similarity index 93% rename from src/main/java/dev/isxander/controlify/contextual/api/GuideInstance.java rename to src/main/java/dev/isxander/controlify/api/contextual/GuideInstance.java index 6107e99cf..ebeec305f 100644 --- a/src/main/java/dev/isxander/controlify/contextual/api/GuideInstance.java +++ b/src/main/java/dev/isxander/controlify/api/contextual/GuideInstance.java @@ -1,4 +1,4 @@ -package dev.isxander.controlify.contextual.api; +package dev.isxander.controlify.api.contextual; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.components.Renderable; diff --git a/src/main/java/dev/isxander/controlify/api/guide/ActionLocation.java b/src/main/java/dev/isxander/controlify/api/contextual/GuideLocation.java similarity index 58% rename from src/main/java/dev/isxander/controlify/api/guide/ActionLocation.java rename to src/main/java/dev/isxander/controlify/api/contextual/GuideLocation.java index 602c9933a..2a5d38d15 100644 --- a/src/main/java/dev/isxander/controlify/api/guide/ActionLocation.java +++ b/src/main/java/dev/isxander/controlify/api/contextual/GuideLocation.java @@ -4,22 +4,20 @@ * * SPDX-License-Identifier: LGPL-3.0-or-later */ -package dev.isxander.controlify.api.guide; +package dev.isxander.controlify.api.contextual; import com.mojang.serialization.Codec; import net.minecraft.util.StringRepresentable; import org.jetbrains.annotations.NotNull; -/** - * Whether the action should be on the left or right list. - */ -public enum ActionLocation implements StringRepresentable { +/// Whether the action should be on the left or right list. +public enum GuideLocation implements StringRepresentable { LEFT("left"), RIGHT("right"); private final String serialName; - ActionLocation(String serialName) { + GuideLocation(String serialName) { this.serialName = serialName; } @@ -28,6 +26,6 @@ public enum ActionLocation implements StringRepresentable { return this.serialName; } - public static final Codec CODEC = - StringRepresentable.fromEnum(ActionLocation::values); + public static final Codec CODEC = + StringRepresentable.fromEnum(GuideLocation::values); } diff --git a/src/main/java/dev/isxander/controlify/api/guide/GuideVerbosity.java b/src/main/java/dev/isxander/controlify/api/contextual/GuideVerbosity.java similarity index 95% rename from src/main/java/dev/isxander/controlify/api/guide/GuideVerbosity.java rename to src/main/java/dev/isxander/controlify/api/contextual/GuideVerbosity.java index 35a95dba5..8baba51d3 100644 --- a/src/main/java/dev/isxander/controlify/api/guide/GuideVerbosity.java +++ b/src/main/java/dev/isxander/controlify/api/contextual/GuideVerbosity.java @@ -4,7 +4,7 @@ * * SPDX-License-Identifier: LGPL-3.0-or-later */ -package dev.isxander.controlify.api.guide; +package dev.isxander.controlify.api.contextual; import com.mojang.serialization.Codec; import dev.isxander.yacl3.api.NameableEnum; diff --git a/src/main/java/dev/isxander/controlify/api/guide/InGameCtx.java b/src/main/java/dev/isxander/controlify/api/contextual/InGameContext.java similarity index 76% rename from src/main/java/dev/isxander/controlify/api/guide/InGameCtx.java rename to src/main/java/dev/isxander/controlify/api/contextual/InGameContext.java index 196ad58c5..2b0830e37 100644 --- a/src/main/java/dev/isxander/controlify/api/guide/InGameCtx.java +++ b/src/main/java/dev/isxander/controlify/api/contextual/InGameContext.java @@ -4,21 +4,20 @@ * * SPDX-License-Identifier: LGPL-3.0-or-later */ -package dev.isxander.controlify.api.guide; +package dev.isxander.controlify.api.contextual; -import dev.isxander.controlify.contextual.api.Context; import dev.isxander.controlify.controller.ControllerEntity; import net.minecraft.client.Minecraft; import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.client.player.LocalPlayer; import net.minecraft.world.phys.HitResult; -public record InGameCtx( +public record InGameContext( Minecraft client, LocalPlayer player, ClientLevel level, HitResult hitResult, ControllerEntity controller, GuideVerbosity verbosity -) implements FactCtx, Context { +) implements Context { } diff --git a/src/main/java/dev/isxander/controlify/api/contextual/TriggerEffectInstance.java b/src/main/java/dev/isxander/controlify/api/contextual/TriggerEffectInstance.java new file mode 100644 index 000000000..8eb1bebbc --- /dev/null +++ b/src/main/java/dev/isxander/controlify/api/contextual/TriggerEffectInstance.java @@ -0,0 +1,12 @@ +package dev.isxander.controlify.api.contextual; + +import dev.isxander.controlify.driver.dualsense.DualsenseTriggerEffect; +import org.jetbrains.annotations.ApiStatus; + +public interface TriggerEffectInstance extends ContextualInstance { + @ApiStatus.Experimental + DualsenseTriggerEffect getLeftTriggerEffect(); + + @ApiStatus.Experimental + DualsenseTriggerEffect getRightTriggerEffect(); +} diff --git a/src/main/java/dev/isxander/controlify/api/entrypoint/PreInitContext.java b/src/main/java/dev/isxander/controlify/api/entrypoint/PreInitContext.java index 5e3cf5ab3..af0a053a5 100644 --- a/src/main/java/dev/isxander/controlify/api/entrypoint/PreInitContext.java +++ b/src/main/java/dev/isxander/controlify/api/entrypoint/PreInitContext.java @@ -7,10 +7,10 @@ package dev.isxander.controlify.api.entrypoint; import dev.isxander.controlify.api.bind.ControlifyBindApi; -import dev.isxander.controlify.api.guide.GuideDomainRegistry; +import dev.isxander.controlify.api.contextual.ContextualDomainRegistry; public interface PreInitContext { ControlifyBindApi bindings(); - GuideDomainRegistry guides(); + ContextualDomainRegistry contextualDomains(); } diff --git a/src/main/java/dev/isxander/controlify/api/guide/CommonFacts.java b/src/main/java/dev/isxander/controlify/api/guide/CommonFacts.java deleted file mode 100644 index 5b50c0530..000000000 --- a/src/main/java/dev/isxander/controlify/api/guide/CommonFacts.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (C) 2026 isXander - * This file is part of Controlify. - * - * SPDX-License-Identifier: LGPL-3.0-or-later - */ -package dev.isxander.controlify.api.guide; - -import dev.isxander.controlify.gui.guide.GuideDomains; -import dev.isxander.controlify.utils.CUtil; -import net.minecraft.resources.Identifier; -import org.jetbrains.annotations.ApiStatus; - -/** - * Contains facts that can be used in any guide domain. - */ -public final class CommonFacts { - private CommonFacts() {} - - /** - * When the guide verbosity level is set to {@link GuideVerbosity#FULL FULL}. - * Used in {@link Rule#forbid()} clauses to only allow the rule to be applied require the verbosity - * is less than full. - */ - public static final Fact VERBOSITY_FULL = register( - CUtil.rl("verbosity_full"), - ctx -> ctx.verbosity() == GuideVerbosity.FULL - ); - /** - * When the guide verbosity level is either {@link GuideVerbosity#FULL FULL} or {@link GuideVerbosity#REDUCED REDUCED}. - * Used in {@link Rule#forbid()} clauses to only allow the rule to be applied require the verbosity is - * set to minimal. - */ - public static final Fact VERBOSITY_REDUCED_OR_MORE = register( - CUtil.rl("verbosity_reduced_or_more"), - ctx -> ctx.verbosity().getLevel() >= GuideVerbosity.REDUCED.getLevel() - ); - /** - * When the guide verbosity is set to {@link GuideVerbosity#REDUCED REDUCED} or less. - * Used in {@link Rule#when()} clauses to only allow the rule to be applied require the verbosity - * is set to reduced or minimal. - */ - public static final Fact VERBOSITY_REDUCED_OR_LESS = register( - CUtil.rl("verbosity_reduced_or_less"), - ctx -> ctx.verbosity().getLevel() <= GuideVerbosity.REDUCED.getLevel() - ); - /** - * When the guide verbosity is set to {@link GuideVerbosity#MINIMAL MINIMAL}. - * Used in {@link Rule#when()} clauses to only allow the rule to be applied require the verbosity - * is set to minimal. - */ - public static final Fact VERBOSITY_MINIMAL = register( - CUtil.rl("verbosity_minimal"), - ctx -> ctx.verbosity() == GuideVerbosity.MINIMAL - ); - - private static Fact register(Identifier id, FactProvider provider) { - var fact = Fact.of(id, provider); - - GuideDomains.IN_GAME.registerFact(fact); - GuideDomains.CONTAINER.registerFact(fact); - - return fact; - } - private static Fact register(Identifier id) { - return register(id, FactProvider.staticProvider(false)); - } - - @ApiStatus.Internal - public static void registerAll() { - // This method is used to ensure that all facts are registered - // require the class is loaded, so that they can be used in the guide. - // No-op, as all facts are registered statically. - } -} diff --git a/src/main/java/dev/isxander/controlify/api/guide/ContainerFacts.java b/src/main/java/dev/isxander/controlify/api/guide/ContainerFacts.java deleted file mode 100644 index 68147af00..000000000 --- a/src/main/java/dev/isxander/controlify/api/guide/ContainerFacts.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (C) 2026 isXander - * This file is part of Controlify. - * - * SPDX-License-Identifier: LGPL-3.0-or-later - */ -package dev.isxander.controlify.api.guide; - -import dev.isxander.controlify.gui.guide.GuideDomains; -import dev.isxander.controlify.utils.CUtil; -import net.minecraft.resources.Identifier; -import net.minecraft.tags.ItemTags; -import net.minecraft.world.item.BundleItem; - -public final class ContainerFacts { - private ContainerFacts() {} - - /** When the user is hovering their cursor over a slot */ - public static final Fact HOVERING_SLOT = register( - CUtil.rl("hovering_slot"), - ctx -> ctx.hoveredSlot() != null - ); - /** When the user is hovering their cursor over an occupied slot */ - public static final Fact HOVERING_ITEM = register( - CUtil.rl("hovering_item"), - ctx -> ctx.hoveredSlot() != null && ctx.hoveredSlot().hasItem() - ); - /** When the user is hovering their cursor over an occupied slot which has more than one item in it */ - public static final Fact HOVERING_MANY_ITEMS = register( - CUtil.rl("hovering_many_items"), - ctx -> ctx.hoveredSlot() != null && ctx.hoveredSlot().getItem().getCount() > 1 - ); - /** When the user has grabbed an item and is moving it around with their cursor */ - public static final Fact HOLDING_ITEM = register( - CUtil.rl("holding_item"), - ctx -> !ctx.holdingItem().isEmpty() - ); - /** When the user has grabbed an item and is moving it around with their cursor, and that item has more than one in the stack */ - public static final Fact HOLDING_MANY_ITEMS = register( - CUtil.rl("holding_many_items"), - ctx -> ctx.holdingItem().getCount() > 1 - ); - /** When the container allows the player to place down their held item into the currently hovered slot */ - public static final Fact CAN_PLACE_HELD_ITEM = register( - CUtil.rl("can_place_held_item"), - ctx -> ctx.hoveredSlot() != null && ctx.hoveredSlot().mayPlace(ctx.holdingItem()) - ); - /** When the user is hovering their cursor outside the container interface */ - public static final Fact CURSOR_OUTSIDE_CONTAINER = register( - CUtil.rl("cursor_outside_container"), - ContainerCtx::cursorOutsideContainer - ); - /** When the user is hovering their cursor over a slot which is occupied with an item tagged by BUNDLES */ - public static final Fact HOVERING_ITEM_IS_BUNDLE = register( - CUtil.rl("hovering_item_is_bundle"), - ctx -> ctx.hoveredSlot() != null && ctx.hoveredSlot().getItem().is(ItemTags.BUNDLES) - ); - /** When the user is currently selecting an item from within the bundle they're hovering */ - public static final Fact SELECTED_BUNDLE_SLOT = register( - CUtil.rl("selected_bundle_slot"), - ctx -> ctx.hoveredSlot() != null && BundleItem.getSelectedItem(ctx.hoveredSlot().getItem()) != null - ); - - private static Fact register(Identifier id, FactProvider provider) { - var fact = Fact.of(id, provider); - - GuideDomains.CONTAINER.registerFact(fact); - - return fact; - } - private static Fact register(Identifier id) { - return register(id, FactProvider.staticProvider(false)); - } - - public static void registerAll() { - CommonFacts.registerAll(); - // This method is used to ensure that all facts are registered - // require the class is loaded, so that they can be used in the guide. - // No-op, as all facts are registered statically. - } -} diff --git a/src/main/java/dev/isxander/controlify/api/guide/Fact.java b/src/main/java/dev/isxander/controlify/api/guide/Fact.java deleted file mode 100644 index 2157ef9aa..000000000 --- a/src/main/java/dev/isxander/controlify/api/guide/Fact.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (C) 2026 isXander - * This file is part of Controlify. - * - * SPDX-License-Identifier: LGPL-3.0-or-later - */ -package dev.isxander.controlify.api.guide; - -import net.minecraft.resources.Identifier; - -/** - * Represents a single fact that rules can depend on. - * - * @param provider the provider of the fact, which is a function that takes a domain context and returns true if the fact is true - */ -public record Fact(Identifier id, FactProvider provider) { - - public static Fact of(Identifier id, FactProvider provider) { - return new Fact<>(id, provider); - } - - public static Fact of(Identifier id) { - return new Fact<>(id, FactProvider.staticProvider(false)); - } -} diff --git a/src/main/java/dev/isxander/controlify/api/guide/FactCtx.java b/src/main/java/dev/isxander/controlify/api/guide/FactCtx.java deleted file mode 100644 index 362daf043..000000000 --- a/src/main/java/dev/isxander/controlify/api/guide/FactCtx.java +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright (C) 2026 isXander - * This file is part of Controlify. - * - * SPDX-License-Identifier: LGPL-3.0-or-later - */ -package dev.isxander.controlify.api.guide; - -import dev.isxander.controlify.controller.ControllerEntity; - -public interface FactCtx { - ControllerEntity controller(); - - GuideVerbosity verbosity(); -} diff --git a/src/main/java/dev/isxander/controlify/api/guide/FactProvider.java b/src/main/java/dev/isxander/controlify/api/guide/FactProvider.java deleted file mode 100644 index c055a407d..000000000 --- a/src/main/java/dev/isxander/controlify/api/guide/FactProvider.java +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright (C) 2026 isXander - * This file is part of Controlify. - * - * SPDX-License-Identifier: LGPL-3.0-or-later - */ -package dev.isxander.controlify.api.guide; - -@FunctionalInterface -public interface FactProvider { - boolean test(T t); - - static FactProvider staticProvider(boolean value) { - return t -> value; - } -} diff --git a/src/main/java/dev/isxander/controlify/api/guide/GuideDomain.java b/src/main/java/dev/isxander/controlify/api/guide/GuideDomain.java deleted file mode 100644 index 308f3cdbd..000000000 --- a/src/main/java/dev/isxander/controlify/api/guide/GuideDomain.java +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright (C) 2026 isXander - * This file is part of Controlify. - * - * SPDX-License-Identifier: LGPL-3.0-or-later - */ -package dev.isxander.controlify.api.guide; - -public interface GuideDomain { - void registerFact(Fact fact); - - void registerDynamicRule(Rule rule); - - GuideInstance createInstance(); -} diff --git a/src/main/java/dev/isxander/controlify/api/guide/GuideDomainRegistry.java b/src/main/java/dev/isxander/controlify/api/guide/GuideDomainRegistry.java deleted file mode 100644 index b73db82a4..000000000 --- a/src/main/java/dev/isxander/controlify/api/guide/GuideDomainRegistry.java +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright (C) 2026 isXander - * This file is part of Controlify. - * - * SPDX-License-Identifier: LGPL-3.0-or-later - */ -package dev.isxander.controlify.api.guide; - -import net.minecraft.resources.Identifier; - -public interface GuideDomainRegistry { - GuideDomain inGame(); - - GuideDomain container(); - - GuideDomain registerCustom(Identifier domainId); -} diff --git a/src/main/java/dev/isxander/controlify/api/guide/GuideInstance.java b/src/main/java/dev/isxander/controlify/api/guide/GuideInstance.java deleted file mode 100644 index fd34fb61c..000000000 --- a/src/main/java/dev/isxander/controlify/api/guide/GuideInstance.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (C) 2026 isXander - * This file is part of Controlify. - * - * SPDX-License-Identifier: LGPL-3.0-or-later - */ -package dev.isxander.controlify.api.guide; - -import net.minecraft.client.gui.Font; -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.client.gui.components.Renderable; - -public interface GuideInstance { - GuideDomain domain(); - - boolean update(T context, Font font); - - void extractRenderState(GuiGraphicsExtractor graphics, boolean bottomAligned, boolean textContrast, int guiScale); - - default void extractRenderState(GuiGraphicsExtractor graphics, boolean bottomAligned, boolean textContrast) { - extractRenderState(graphics, bottomAligned, textContrast, -1); - } - - Renderable renderable(boolean bottomAligned, boolean textContrast, int guiScale); - - default Renderable renderable(boolean bottomAligned, boolean textContrast) { - return renderable(bottomAligned, textContrast, -1); - } -} diff --git a/src/main/java/dev/isxander/controlify/api/guide/InGameFacts.java b/src/main/java/dev/isxander/controlify/api/guide/InGameFacts.java deleted file mode 100644 index 8d6b1b13e..000000000 --- a/src/main/java/dev/isxander/controlify/api/guide/InGameFacts.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright (C) 2026 isXander - * This file is part of Controlify. - * - * SPDX-License-Identifier: LGPL-3.0-or-later - */ -package dev.isxander.controlify.api.guide; - -import dev.isxander.controlify.gui.guide.GuideDomains; -import dev.isxander.controlify.mixins.feature.guide.ingame.PlayerAccessor; -import dev.isxander.controlify.utils.CUtil; -import net.minecraft.client.player.LocalPlayer; -import net.minecraft.resources.Identifier; -import net.minecraft.world.entity.EquipmentSlot; -import net.minecraft.world.entity.animal.happyghast.HappyGhast; -import net.minecraft.world.level.GameType; -import net.minecraft.world.phys.HitResult; -import net.minecraft.world.phys.Vec2; -import net.minecraft.world.entity.animal.equine.AbstractHorse; - -public final class InGameFacts { - private InGameFacts() {} - - /** When {@link LocalPlayer#onGround()} is true */ - public static final Fact ON_GROUND = register( - CUtil.rl("on_ground"), - ctx -> ctx.player().onGround() - ); - /** When {@link LocalPlayer#getVehicle()} is not null */ - public static final Fact IN_VEHICLE = register( - CUtil.rl("in_vehicle"), - ctx -> ctx.player().getVehicle() != null - ); - /** When the currently ridden vehicle is a horse with a saddle */ - public static final Fact RIDING_SADDLED_HORSE = register( - CUtil.rl("riding_saddled_horse"), - ctx -> ctx.player().getVehicle() instanceof AbstractHorse horse && horse.isSaddled() - ); - /** When the currently ridden vehicle is a Happy Ghast */ - public static final Fact RIDING_HAPPY_GHAST = register( - CUtil.rl("riding_happy_ghast"), - ctx -> ctx.player().getVehicle() instanceof HappyGhast - ); - /** When the player is currently in creative flight */ - public static final Fact FLYING = register( - CUtil.rl("flying"), - ctx -> ctx.player().getAbilities().flying - ); - /** When the player is currently gliding with an elytra */ - public static final Fact ELYTRA_FLYING = register( - CUtil.rl("elytra_flying"), - ctx -> ctx.player().isFallFlying() - ); - /** When the player is in a state where pressing jump will cause the elytra to deploy */ - public static final Fact CAN_ELYTRA_FLY = register( - CUtil.rl("can_elytra_fly"), - ctx -> ((PlayerAccessor) ctx.player()).controlify$callCanGlide() - && !ctx.player().onClimbable() - && !ctx.player().onGround() - && !ctx.player().isInLiquid() - && !ctx.player().isFallFlying() - ); - /** When the player is touching liquid, such as water or lava */ - public static final Fact IN_LIQUID = register( - CUtil.rl("in_liquid"), - ctx -> ctx.player().isInLiquid() - ); - /** When the player is touching water */ - public static final Fact IN_WATER = register( - CUtil.rl("in_water"), - ctx -> ctx.player().isInWater() - ); - /** When the player has their eyes underwater */ - public static final Fact UNDER_WATER = register( - CUtil.rl("under_water"), - ctx -> ctx.player().isUnderWater() - ); - /** When the player is touching lava */ - public static final Fact IN_LAVA = register( - CUtil.rl("in_lava"), - ctx -> ctx.player().isInLava() - ); - /** When the player is attempting to sneak (pressing the sneak key, or it is toggled on) */ - public static final Fact SNEAKING = register( - CUtil.rl("sneaking"), - ctx -> ctx.player().isShiftKeyDown() - ); - /** When the player is using toggle sneak (does not mean it is currently toggled on) */ - public static final Fact IS_TOGGLE_SNEAK = register( - CUtil.rl("is_toggle_sneak"), - ctx -> ctx.controller().settings().generic.toggleSneak - ); - /** When the player is using toggle sprint (does not mean it is currently toggled on) */ - public static final Fact IS_TOGGLE_SPRINT = register( - CUtil.rl("is_toggle_sprint"), - ctx -> ctx.controller().settings().generic.toggleSprint - ); - /** When the player is attempting to sprint (pressing the sprint key, or it is toggled on) */ - public static final Fact SPRINTING = register( - CUtil.rl("sprinting"), - ctx -> ctx.player().input.keyPresses.sprint() - ); - /** When the player is applying movement input - even if the player is not physically moving, if they're trying to, this fact goes */ - public static final Fact INPUT_MOVING = register( - CUtil.rl("input_moving"), - ctx -> !ctx.player().input.getMoveVector().equals(Vec2.ZERO) - ); - /** When the player is in {@link GameType#SPECTATOR spectator mode} */ - public static final Fact IS_SPECTATOR = register( - CUtil.rl("is_spectator"), - ctx -> ctx.player().isSpectator() - ); - /** When the player is in {@link GameType#CREATIVE creative mode} */ - public static final Fact IS_CREATIVE = register( - CUtil.rl("is_creative"), - ctx -> ctx.player().isCreative() - ); - /** When the player is not invulnerable */ - public static final Fact HAS_HEARTS = register( - CUtil.rl("has_hearts"), - ctx -> !ctx.player().getAbilities().invulnerable - ); - /** When the player is in {@link GameType#ADVENTURE adventure mode} */ - public static final Fact IS_ADVENTURE = register( - CUtil.rl("is_adventure"), - ctx -> ctx.player().gameMode() == GameType.ADVENTURE - ); - /** When the player is in {@link GameType#SURVIVAL survival mode} */ - public static final Fact IS_SURVIVAL = register( - CUtil.rl("is_survival"), - ctx -> ctx.player().gameMode() == GameType.SURVIVAL - ); - /** When the player is currently looking at an entity and is in range to interact with it */ - public static final Fact LOOKING_AT_ENTITY = register( - CUtil.rl("looking_at_entity"), - ctx -> ctx.hitResult().getType() == HitResult.Type.ENTITY - ); - /** When the player is currently looking at a block and is in range to interact or destroy it */ - public static final Fact LOOKING_AT_BLOCK = register( - CUtil.rl("looking_at_block"), - ctx -> ctx.hitResult().getType() == HitResult.Type.BLOCK - ); - /** When the player is neither {@link #LOOKING_AT_BLOCK looking at a block} or {@link #LOOKING_AT_ENTITY looking at an entity} */ - public static final Fact LOOKING_AT_AIR = register( - CUtil.rl("looking_at_air"), - ctx -> ctx.hitResult().getType() == HitResult.Type.MISS - ); - /** When the player has an item in their main hand or their offhand */ - public static final Fact HAS_ITEM_IN_EITHER_HAND = register( - CUtil.rl("has_item_in_either_hand"), - ctx -> ctx.player().hasItemInSlot(EquipmentSlot.MAINHAND) || ctx.player().hasItemInSlot(EquipmentSlot.OFFHAND) - ); - /** When the player has an item in their main hand. */ - public static final Fact HAS_ITEM_IN_MAINHAND = register( - CUtil.rl("has_item_in_mainhand"), - ctx -> ctx.player().hasItemInSlot(EquipmentSlot.MAINHAND) - ); - /** When the player has an item in their offhand. */ - public static final Fact HAS_ITEM_IN_OFFHAND = register( - CUtil.rl("has_item_in_offhand"), - ctx -> ctx.player().hasItemInSlot(EquipmentSlot.OFFHAND) - ); - /** When the player is holding an item stack with a count greater than one */ - public static final Fact HAS_MULTIPLE_ITEMS_IN_HAND = register( - CUtil.rl("has_multiple_items_in_hand"), - ctx -> { - var holdingItem = ctx.player().getInventory().getSelectedItem(); - return !holdingItem.isEmpty() && holdingItem.getCount() > 1; - } - ); - - private static Fact register(Identifier id, FactProvider provider) { - var fact = Fact.of(id, provider); - GuideDomains.IN_GAME.registerFact(fact); - return fact; - } - private static Fact register(Identifier id) { - return register(id, FactProvider.staticProvider(false)); - } - - public static void registerAll() { - CommonFacts.registerAll(); - // This method is used to ensure that all facts are registered - // require the class is loaded, so that they can be used in the guide. - // No-op, as all facts are registered statically. - } -} diff --git a/src/main/java/dev/isxander/controlify/api/guide/Rule.java b/src/main/java/dev/isxander/controlify/api/guide/Rule.java deleted file mode 100644 index 177b04cf4..000000000 --- a/src/main/java/dev/isxander/controlify/api/guide/Rule.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2026 isXander - * This file is part of Controlify. - * - * SPDX-License-Identifier: LGPL-3.0-or-later - */ -package dev.isxander.controlify.api.guide; - -import com.mojang.serialization.Codec; -import com.mojang.serialization.codecs.RecordCodecBuilder; -import dev.isxander.controlify.api.bind.InputBindingSupplier; -import dev.isxander.controlify.utils.codec.CExtraCodecs; -import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.ComponentSerialization; -import net.minecraft.resources.Identifier; - -import java.util.Set; - -/** - * A rule is made up of a set of conditions ({@link #when()} and {@link #forbid()}) - * that must be satisfied in order for a button guide to be shown. - *

- * These rules are loaded dynamically from resource packs or can be created programmatically - * with {@link RuleBuilder}. - * - * @param binding the binding that this rule applies to - the glyph of which is shown in the guide - * @param where the location of the guide (left or right) - * @param when a set of conditions that all must be satisfied for the rule to apply - * @param forbid a set of conditions where any must not be satisfied for the rule to apply - * @param then the text component that is shown in the guide require the rule applies - * @see #builder() for a builder to create dynamic rules - */ -public record Rule( - InputBindingSupplier binding, - ActionLocation where, - Set when, - Set forbid, - Component then -) { - public static final Codec CODEC = RecordCodecBuilder.create( - instance -> instance.group( - InputBindingSupplier.CODEC.fieldOf("for").forGetter(Rule::binding), - ActionLocation.CODEC.fieldOf("where").forGetter(Rule::where), - CExtraCodecs.set(Identifier.CODEC).optionalFieldOf("require", Set.of()).forGetter(Rule::when), - CExtraCodecs.set(Identifier.CODEC).optionalFieldOf("forbid", Set.of()).forGetter(Rule::forbid), - ComponentSerialization.CODEC.fieldOf("then").forGetter(Rule::then) - ).apply(instance, Rule::new) - ); - - public static RuleBuilder builder() { - return new RuleBuilder(); - } -} diff --git a/src/main/java/dev/isxander/controlify/api/guide/RuleBuilder.java b/src/main/java/dev/isxander/controlify/api/guide/RuleBuilder.java deleted file mode 100644 index 318456444..000000000 --- a/src/main/java/dev/isxander/controlify/api/guide/RuleBuilder.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (C) 2026 isXander - * This file is part of Controlify. - * - * SPDX-License-Identifier: LGPL-3.0-or-later - */ -package dev.isxander.controlify.api.guide; - -import dev.isxander.controlify.api.bind.InputBindingSupplier; -import net.minecraft.network.chat.Component; -import net.minecraft.resources.Identifier; -import org.apache.commons.lang3.Validate; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - -public final class RuleBuilder { - private InputBindingSupplier binding; - private ActionLocation where; - private final Set when = new HashSet<>(); - private final Set forbid = new HashSet<>(); - private Component then; - - RuleBuilder() {} - - public RuleBuilder binding(InputBindingSupplier binding) { - this.binding = binding; - return this; - } - - public RuleBuilder where(ActionLocation where) { - this.where = where; - return this; - } - - public RuleBuilder when(Identifier... when) { - this.when.addAll(Arrays.asList(when)); - return this; - } - - @SafeVarargs - public final StrictRuleBuilder when(Fact... when) { - return new StrictRuleBuilder().when(when); - } - - public RuleBuilder forbid(Identifier... forbid) { - this.forbid.addAll(Arrays.asList(forbid)); - return this; - } - - @SafeVarargs - public final StrictRuleBuilder forbid(Fact... forbid) { - return this.new StrictRuleBuilder().forbid(forbid); - } - - public RuleBuilder then(Component then) { - this.then = then; - return this; - } - - public StrictRuleBuilder strict() { - return this.new StrictRuleBuilder<>(); - } - - public Rule build() { - Validate.notNull(binding, "`binding` must not be null"); - Validate.notNull(where, "`where` must not be null"); - Validate.notNull(then, "`then` must not be null"); - return new Rule(binding, where, when, forbid, then); - } - - public final class StrictRuleBuilder { - private StrictRuleBuilder() {} - - public StrictRuleBuilder binding(InputBindingSupplier binding) { - RuleBuilder.this.binding(binding); - return this; - } - - public StrictRuleBuilder where(ActionLocation where) { - RuleBuilder.this.where(where); - return this; - } - - public StrictRuleBuilder when(Identifier... when) { - RuleBuilder.this.when(when); - return this; - } - - @SafeVarargs - public final StrictRuleBuilder when(Fact... when) { - for (Fact fact : when) { - RuleBuilder.this.when(fact.id()); - } - return this; - } - - public StrictRuleBuilder forbid(Identifier... when) { - RuleBuilder.this.forbid(when); - return this; - } - - @SafeVarargs - public final StrictRuleBuilder forbid(Fact... when) { - for (Fact fact : when) { - RuleBuilder.this.forbid(fact.id()); - } - return this; - } - - public StrictRuleBuilder then(Component then) { - RuleBuilder.this.then(then); - return this; - } - - public Rule build() { - return RuleBuilder.this.build(); - } - - public RuleBuilder lenient() { - return RuleBuilder.this; - } - } -} diff --git a/src/main/java/dev/isxander/controlify/config/dto/profile/GenericControllerConfig.java b/src/main/java/dev/isxander/controlify/config/dto/profile/GenericControllerConfig.java index 3135f2ebb..7eccea863 100644 --- a/src/main/java/dev/isxander/controlify/config/dto/profile/GenericControllerConfig.java +++ b/src/main/java/dev/isxander/controlify/config/dto/profile/GenericControllerConfig.java @@ -8,7 +8,7 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; -import dev.isxander.controlify.api.guide.GuideVerbosity; +import dev.isxander.controlify.api.contextual.GuideVerbosity; public record GenericControllerConfig( boolean autoJump, diff --git a/src/main/java/dev/isxander/controlify/config/settings/profile/GenericControllerSettings.java b/src/main/java/dev/isxander/controlify/config/settings/profile/GenericControllerSettings.java index 6613f8b21..278455d1a 100644 --- a/src/main/java/dev/isxander/controlify/config/settings/profile/GenericControllerSettings.java +++ b/src/main/java/dev/isxander/controlify/config/settings/profile/GenericControllerSettings.java @@ -6,7 +6,7 @@ */ package dev.isxander.controlify.config.settings.profile; -import dev.isxander.controlify.api.guide.GuideVerbosity; +import dev.isxander.controlify.api.contextual.GuideVerbosity; import dev.isxander.controlify.config.dto.profile.GenericControllerConfig; public class GenericControllerSettings { diff --git a/src/main/java/dev/isxander/controlify/contextual/ContextualDomainImpl.java b/src/main/java/dev/isxander/controlify/contextual/ContextualDomainImpl.java index 2373d3da3..78dd8818b 100644 --- a/src/main/java/dev/isxander/controlify/contextual/ContextualDomainImpl.java +++ b/src/main/java/dev/isxander/controlify/contextual/ContextualDomainImpl.java @@ -10,10 +10,9 @@ import com.google.gson.JsonParser; import com.mojang.serialization.DataResult; import com.mojang.serialization.JsonOps; -import dev.isxander.controlify.contextual.api.Context; -import dev.isxander.controlify.contextual.api.ContextualDomain; -import dev.isxander.controlify.contextual.api.ContextualStateContributor; -import dev.isxander.controlify.contextual.api.GuideInstance; +import dev.isxander.controlify.api.contextual.*; +import dev.isxander.controlify.controller.dualsense.TriggerEffectInstanceImpl; +import dev.isxander.controlify.gui.guide.GuideInstanceImpl; import dev.isxander.controlify.platform.client.resource.SimpleControlifyReloadListener; import net.minecraft.client.gui.Font; import net.minecraft.resources.FileToIdConverter; @@ -33,8 +32,8 @@ public final class ContextualDomainImpl implements Contextual private static final FileToIdConverter converter = FileToIdConverter.json(DIRECTORY); private final Identifier id; - private volatile List> contributors = List.of(); - private volatile FactDependencyGraph factGraph = FactDependencyGraph.empty(); + private ContextualStateContributor contributor = ContextualStateContributor.noop(); + private FactDependencyGraph factGraph = FactDependencyGraph.empty(); public ContextualDomainImpl(Identifier id) { this.id = Objects.requireNonNull(id, "id"); @@ -47,21 +46,30 @@ public Identifier id() { } @Override - public synchronized void registerContributor(ContextualStateContributor contributor) { - List> replacement = new ArrayList<>(this.contributors); - replacement.add(Objects.requireNonNull(contributor, "contributor")); - this.contributors = List.copyOf(replacement); + public void registerContributor(ContextualStateContributor contributor) { + Objects.requireNonNull(contributor, "contributor"); + + this.contributor = this.contributor.andThen(contributor); } @Override public GuideInstance createGuideInstance(Font font) { - RuleSetManager ruleSetManager = null; // TODO: get rule set manager from global state somewhere + RuleSetManager ruleSetManager = RuleSetManager.GUIDE_RULES; RuleEngine ruleEngine = ruleSetManager.getRuleEngine(this.id) .orElseThrow(() -> new IllegalCallerException("This contextual domain has no guide rules associated with it.")); return new GuideInstanceImpl<>(this, ruleEngine, font); } + @Override + public TriggerEffectInstance createTriggerEffectInstance() { + RuleSetManager ruleSetManager = RuleSetManager.TRIGGER_EFFECT_RULES; + RuleEngine ruleEngine = ruleSetManager.getRuleEngine(this.id) + .orElseThrow(() -> new IllegalCallerException("This contextual domain has no trigger effect rules associated with it.")); + + return new TriggerEffectInstanceImpl<>(this, ruleEngine); + } + @Override public CompletableFuture load(ResourceManager manager, Executor executor) { return CompletableFuture.supplyAsync(() -> { @@ -106,9 +114,7 @@ public ContextualState calculateState(C context, @Nullable Collection contributor : this.contributors) { - contributor.contribute(context, state); - } + this.contributor.contribute(context, state); FactDependencyGraph graph = this.factGraph; return graph.evaluate(state, requiredFacts); diff --git a/src/main/java/dev/isxander/controlify/contextual/ContextualDomains.java b/src/main/java/dev/isxander/controlify/contextual/ContextualDomains.java new file mode 100644 index 000000000..12ae69838 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/contextual/ContextualDomains.java @@ -0,0 +1,44 @@ +package dev.isxander.controlify.contextual; + +import dev.isxander.controlify.api.contextual.ContainerContext; +import dev.isxander.controlify.api.contextual.InGameContext; +import dev.isxander.controlify.api.contextual.Context; +import dev.isxander.controlify.api.contextual.ContextualDomain; +import dev.isxander.controlify.api.contextual.ContextualDomainRegistry; +import dev.isxander.controlify.platform.client.PlatformClientUtil; +import dev.isxander.controlify.utils.CUtil; +import net.minecraft.resources.Identifier; + +public final class ContextualDomains implements ContextualDomainRegistry { + + public static final ContextualDomains INSTANCE = new ContextualDomains(); + + private final ContextualDomain inGame = register( + CUtil.rl("in_game"), + ContextualStateContributors.IN_GAME + ); + private final ContextualDomain container = register( + CUtil.rl("container"), + ContextualStateContributors.CONTAINER + ); + + @Override + public ContextualDomain inGame() { + return this.inGame; + } + + @Override + public ContextualDomain container() { + return this.container; + } + + @Override + public ContextualDomain register(Identifier domainId) { + var domain = new ContextualDomainImpl(domainId); + PlatformClientUtil.registerAssetReloadListener(domain); + return domain; + } + + private ContextualDomains() { + } +} diff --git a/src/main/java/dev/isxander/controlify/contextual/ContextualPredicate.java b/src/main/java/dev/isxander/controlify/contextual/ContextualPredicate.java index 022045f8b..35e41c8ec 100644 --- a/src/main/java/dev/isxander/controlify/contextual/ContextualPredicate.java +++ b/src/main/java/dev/isxander/controlify/contextual/ContextualPredicate.java @@ -4,11 +4,15 @@ import com.mojang.serialization.DataResult; import com.mojang.serialization.codecs.RecordCodecBuilder; import dev.isxander.controlify.utils.codec.CExtraCodecs; +import dev.isxander.controlify.utils.predicates.ClientEntityPredicate; import net.minecraft.advancements.predicates.BlockPredicate; import net.minecraft.advancements.predicates.ItemPredicate; +import net.minecraft.client.Minecraft; import net.minecraft.resources.Identifier; +import java.util.Collection; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -34,30 +38,48 @@ public sealed interface ContextualPredicate { boolean matches(ContextualState state); - record Compound(Set require, Set forbid) implements ContextualPredicate { + record Compound( + Optional> allOf, + Optional> noneOf, + Optional> anyOf + ) implements ContextualPredicate { private static Codec createCodec(Codec childCodec) { Codec codec = RecordCodecBuilder.create(instance -> instance.group( - CExtraCodecs.set(childCodec).optionalFieldOf("require", Set.of()).forGetter(Compound::require), - CExtraCodecs.set(childCodec).optionalFieldOf("forbid", Set.of()).forGetter(Compound::forbid) + CExtraCodecs.set(childCodec).optionalFieldOf("all_of").forGetter(Compound::allOf), + CExtraCodecs.set(childCodec).optionalFieldOf("none_of").forGetter(Compound::noneOf), + CExtraCodecs.set(childCodec).optionalFieldOf("any_of").forGetter(Compound::anyOf) ).apply(instance, Compound::new)); return codec.validate(compound -> { - if (compound.require().isEmpty() && compound.forbid().isEmpty()) { - return DataResult.error(() -> "Compound predicate must define at least one 'require' or 'forbid' predicate"); + if (compound.allOf.isEmpty() && compound.noneOf.isEmpty() && compound.anyOf.isEmpty()) { + return DataResult.error(() -> "Compound predicate must define at least one selector: 'all_of', 'none_of', 'any_of'"); } + return DataResult.success(compound); }); } @Override public boolean matches(ContextualState state) { - return require().stream().allMatch(predicate -> predicate.matches(state)) - && forbid().stream().noneMatch(predicate -> predicate.matches(state)); + if (this.allOf.isPresent() && !this.allOf.get().stream().allMatch(predicate -> predicate.matches(state))) { + return false; + } + + if (this.noneOf.isPresent() && this.noneOf.get().stream().anyMatch(predicate -> predicate.matches(state))) { + return false; + } + + if (this.anyOf.isPresent() && this.anyOf.get().stream().noneMatch(predicate -> predicate.matches(state))) { + return false; + } + + return true; } @Override public Set factDependencies() { - return Stream.concat(require().stream(), forbid().stream()) + return Stream.concat(Stream.concat(this.allOf.stream(), this.noneOf.stream()), this.anyOf.stream()) + .flatMap(Collection::stream) .flatMap(predicate -> predicate.factDependencies().stream()) .collect(Collectors.toUnmodifiableSet()); } @@ -91,11 +113,11 @@ record Item(Identifier slot, ItemPredicate item) implements ContextualPredicate @Override public boolean matches(ContextualState state) { - var stack = state.items().get(slot()); - if (stack == null) { + var itemInstance = state.items().get(slot()); + if (itemInstance == null) { return false; } - return item().test(stack); + return item().test(itemInstance); } @Override @@ -137,7 +159,7 @@ public boolean matches(ContextualState state) { if (entityInWorld == null) { return false; } - return entity().matches(entityInWorld); + return entity().matches(Minecraft.getInstance().player, entityInWorld); } @Override diff --git a/src/main/java/dev/isxander/controlify/contextual/ContextualState.java b/src/main/java/dev/isxander/controlify/contextual/ContextualState.java index c532a6c01..b59f62cc9 100644 --- a/src/main/java/dev/isxander/controlify/contextual/ContextualState.java +++ b/src/main/java/dev/isxander/controlify/contextual/ContextualState.java @@ -2,6 +2,7 @@ import net.minecraft.resources.Identifier; import net.minecraft.world.entity.Entity; +import net.minecraft.world.item.ItemInstance; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.block.state.pattern.BlockInWorld; @@ -9,7 +10,7 @@ public record ContextualState( Map facts, - Map items, + Map items, Map blocks, Map entities ) { diff --git a/src/main/java/dev/isxander/controlify/contextual/ContextualStateAccumulator.java b/src/main/java/dev/isxander/controlify/contextual/ContextualStateAccumulator.java index 446639479..a43ea2778 100644 --- a/src/main/java/dev/isxander/controlify/contextual/ContextualStateAccumulator.java +++ b/src/main/java/dev/isxander/controlify/contextual/ContextualStateAccumulator.java @@ -6,11 +6,13 @@ */ package dev.isxander.controlify.contextual; -import dev.isxander.controlify.contextual.api.ContextualStateSink; +import dev.isxander.controlify.api.contextual.ContextualStateSink; import net.minecraft.resources.Identifier; import net.minecraft.world.entity.Entity; +import net.minecraft.world.item.ItemInstance; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.block.state.pattern.BlockInWorld; +import org.jetbrains.annotations.Nullable; import java.util.Collections; import java.util.LinkedHashMap; @@ -19,7 +21,7 @@ public final class ContextualStateAccumulator implements ContextualStateSink { private final Map facts = new LinkedHashMap<>(); - private final Map items = new LinkedHashMap<>(); + private final Map items = new LinkedHashMap<>(); private final Map blocks = new LinkedHashMap<>(); private final Map entities = new LinkedHashMap<>(); @@ -39,18 +41,27 @@ public void contributeFact(Identifier id, boolean value) { } @Override - public void contributeItem(Identifier slot, ItemStack stack) { - this.items.put(Objects.requireNonNull(slot, "slot"), Objects.requireNonNull(stack, "stack")); + public void contributeItem(Identifier slot, @Nullable ItemInstance item) { + if (item == null) { + return; + } + this.items.put(Objects.requireNonNull(slot, "slot"), item); } @Override - public void contributeBlock(Identifier slot, BlockInWorld block) { - this.blocks.put(Objects.requireNonNull(slot, "slot"), Objects.requireNonNull(block, "block")); + public void contributeBlock(Identifier slot, @Nullable BlockInWorld block) { + if (block == null) { + return; + } + this.blocks.put(Objects.requireNonNull(slot, "slot"), block); } @Override - public void contributeEntity(Identifier slot, Entity entity) { - this.entities.put(Objects.requireNonNull(slot, "slot"), Objects.requireNonNull(entity, "entity")); + public void contributeEntity(Identifier slot, @Nullable Entity entity) { + if (entity == null) { + return; + } + this.entities.put(Objects.requireNonNull(slot, "slot"), entity); } public ContextualState view() { diff --git a/src/main/java/dev/isxander/controlify/contextual/ContextualStateContributors.java b/src/main/java/dev/isxander/controlify/contextual/ContextualStateContributors.java index 5fa0278df..c0ad2d162 100644 --- a/src/main/java/dev/isxander/controlify/contextual/ContextualStateContributors.java +++ b/src/main/java/dev/isxander/controlify/contextual/ContextualStateContributors.java @@ -1,12 +1,11 @@ package dev.isxander.controlify.contextual; -import dev.isxander.controlify.api.guide.GuideVerbosity; -import dev.isxander.controlify.api.guide.InGameCtx; -import dev.isxander.controlify.contextual.api.Context; -import dev.isxander.controlify.contextual.api.ContextualStateContributor; +import dev.isxander.controlify.api.contextual.*; import dev.isxander.controlify.mixins.feature.guide.ingame.PlayerAccessor; import dev.isxander.controlify.utils.CUtil; import net.minecraft.resources.Identifier; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.BundleItem; import net.minecraft.world.level.block.state.pattern.BlockInWorld; import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.EntityHitResult; @@ -18,8 +17,10 @@ public final class ContextualStateContributors { private static final Identifier SLOT_HIT_RESULT = CUtil.rl("hit_result"); private static final Identifier SLOT_MAIN_HAND = CUtil.rl("main_hand"); private static final Identifier SLOT_OFF_HAND = CUtil.rl("off_hand"); - /// Picks the main hand, but if it is empty picks the offhand private static final Identifier SLOT_ACTIVE_ITEM = CUtil.rl("active_item"); + private static final Identifier SLOT_HOVERING_SLOT = CUtil.rl("hovering_slot"); + private static final Identifier SLOT_HOLDING_ITEM = CUtil.rl("holding_item"); + private static final Identifier SLOT_BUNDLE_SELECTED = CUtil.rl("bundle_selected"); public static final ContextualStateContributor COMMON = (context, sink) -> { sink.contributeFact( @@ -36,25 +37,21 @@ public final class ContextualStateContributors { ); }; - public static final ContextualStateContributor IN_GAME = (context, sink) -> { + public static final ContextualStateContributor IN_GAME = (context, sink) -> { switch (context.hitResult()) { case BlockHitResult hitResult when hitResult.getType() == HitResult.Type.BLOCK -> { sink.contributeBlock( SLOT_HIT_RESULT, new BlockInWorld(context.level(), hitResult.getBlockPos(), false) ); - sink.contributeFact(CUtil.rl("looking_at_block")); } case EntityHitResult hitResult -> { sink.contributeEntity( SLOT_HIT_RESULT, hitResult.getEntity() ); - sink.contributeFact(CUtil.rl("looking_at_entity")); - } - default -> { - sink.contributeFact(CUtil.rl("looking_at_air")); } + default -> {} } sink.contributeEntity(SLOT_SELF, context.player()); @@ -95,15 +92,46 @@ public final class ContextualStateContributors { context.player().input.getMoveVector().equals(Vec2.ZERO) ); - // TODO: add GameTypePredicate to ClientEntityPredicate - // and make them derived facts - sink.contributeFact( CUtil.rl("has_hearts"), !context.player().getAbilities().invulnerable ); + }; + + public static final ContextualStateContributor CONTAINER = (context, sink) -> { + if (context.hoveredSlot() != null) { + Slot hoveredSlot = context.hoveredSlot(); + + sink.contributeItem( + SLOT_HOVERING_SLOT, + hoveredSlot.getItem() + ); + + sink.contributeFact( + CUtil.rl("can_place_held_item"), + hoveredSlot.mayPlace(context.holdingItem()) + ); + sink.contributeFact( + CUtil.rl("can_pickup_slot"), + hoveredSlot.mayPickup(context.player()) + ); + sink.contributeItem( + SLOT_BUNDLE_SELECTED, + BundleItem.getSelectedItem(hoveredSlot.getItem()) + ); + } + + sink.contributeItem( + SLOT_HOLDING_ITEM, + context.holdingItem() + ); + + sink.contributeFact( + CUtil.rl("cursor_outside_container"), + context.cursorOutsideContainer() + ); }; private ContextualStateContributors() { diff --git a/src/main/java/dev/isxander/controlify/contextual/GuideInstanceImpl.java b/src/main/java/dev/isxander/controlify/contextual/GuideInstanceImpl.java deleted file mode 100644 index f625b401c..000000000 --- a/src/main/java/dev/isxander/controlify/contextual/GuideInstanceImpl.java +++ /dev/null @@ -1,113 +0,0 @@ -package dev.isxander.controlify.contextual; - -import dev.isxander.controlify.api.bind.InputBinding; -import dev.isxander.controlify.api.guide.ActionLocation; -import dev.isxander.controlify.contextual.api.Context; -import dev.isxander.controlify.contextual.api.GuideInstance; -import dev.isxander.controlify.controller.ControllerEntity; -import dev.isxander.controlify.font.BindingFontHelper; -import dev.isxander.controlify.gui.guide.GuideRenderer; -import dev.isxander.controlify.gui.guide.PrecomputedLines; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.Font; -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.client.gui.components.Renderable; -import net.minecraft.network.chat.Component; - -import java.util.List; - -public class GuideInstanceImpl implements GuideInstance { - private final ContextualDomainImpl domain; - private final RuleEngine ruleEngine; - private final Font font; - - public GuideInstanceImpl( - ContextualDomainImpl domain, - RuleEngine ruleEngine, - Font font - ) { - this.domain = domain; - this.ruleEngine = ruleEngine; - this.font = font; - } - - private PrecomputedLines leftGuides = PrecomputedLines.EMPTY; - private PrecomputedLines rightGuides = PrecomputedLines.EMPTY; - - @Override - public void update(C context) { - ContextualState state = this.domain.calculateState(context, this.ruleEngine.factDependencies()); - List guides = this.ruleEngine.evaluate(state); - - var leftBuilder = new PrecomputedLines.Builder(); - var rightBuilder = new PrecomputedLines.Builder(); - - ControllerEntity controller = context.controller(); - - for (GuideRule guide : guides) { - InputBinding binding = guide.binding().onOrNull(controller); - if (binding == null || binding.isUnbound()) { - // skip this rule if the binding is not bound - continue; - } - - PrecomputedLines.Builder builder = switch (guide.location()) { - case LEFT -> leftBuilder; - case RIGHT -> rightBuilder; - }; - - // put the glyph after or before the binding glyph based on the rule's location and font direction - boolean glyphAfter = font.isBidirectional() ^ (guide.location() == ActionLocation.RIGHT); - - // formulate the text to display - Component text = Component.empty() - .append(glyphAfter ? guide.text() : binding.inputGlyph()) - .append(" ") - .append(glyphAfter ? binding.inputGlyph() : guide.text()); - - // precompute the width and height of the text - int ruleNameWidth = font.width(guide.text()); - int width = font.width(text); - int glyphWidth = width - ruleNameWidth; - int backgroundLeft = glyphAfter ? 0 : glyphWidth; - int backgroundRight = backgroundLeft + ruleNameWidth; - - int height = BindingFontHelper.getComponentHeight(font, binding.inputGlyph()); // use input glyph height only as it's bound to be the tallest - - builder.addLine(text, width, height, backgroundLeft, backgroundRight); - } - this.leftGuides = leftBuilder.build(); - this.rightGuides = rightBuilder.build(); - } - - @Override - public void extractRenderState(GuiGraphicsExtractor graphics, boolean bottomAligned, boolean textContrast, int guiScale) { - GuideRenderer.extractRenderState( - graphics, - this, - Minecraft.getInstance(), - bottomAligned, - textContrast, - guiScale - ); - } - - @Override - public Renderable renderable(boolean bottomAligned, boolean textContrast, int guiScale) { - return new GuideRenderer.Renderable( - this, - Minecraft.getInstance(), - bottomAligned, - textContrast, - guiScale - ); - } - - public PrecomputedLines leftGuides() { - return this.leftGuides; - } - - public PrecomputedLines rightGuides() { - return this.rightGuides; - } -} diff --git a/src/main/java/dev/isxander/controlify/contextual/GuideRule.java b/src/main/java/dev/isxander/controlify/contextual/GuideRule.java index 28b62d661..dca46da96 100644 --- a/src/main/java/dev/isxander/controlify/contextual/GuideRule.java +++ b/src/main/java/dev/isxander/controlify/contextual/GuideRule.java @@ -3,20 +3,20 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; import dev.isxander.controlify.api.bind.InputBindingSupplier; -import dev.isxander.controlify.api.guide.ActionLocation; +import dev.isxander.controlify.api.contextual.GuideLocation; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.ComponentSerialization; import net.minecraft.resources.Identifier; public record GuideRule( InputBindingSupplier binding, - ActionLocation location, + GuideLocation location, ContextualPredicate predicate, Component text ) implements Rule { public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( InputBindingSupplier.CODEC.fieldOf("for").forGetter(GuideRule::binding), - ActionLocation.CODEC.fieldOf("where").forGetter(GuideRule::location), + GuideLocation.CODEC.fieldOf("where").forGetter(GuideRule::location), ContextualPredicate.CODEC.fieldOf("if").forGetter(GuideRule::predicate), ComponentSerialization.CODEC.fieldOf("then").forGetter(GuideRule::text) ).apply(instance, GuideRule::new)); @@ -28,6 +28,6 @@ public Key key() { public record Key( Identifier binding, - ActionLocation location + GuideLocation location ) {} } diff --git a/src/main/java/dev/isxander/controlify/contextual/RuleEngine.java b/src/main/java/dev/isxander/controlify/contextual/RuleEngine.java index 3da7960d8..08f2d64e5 100644 --- a/src/main/java/dev/isxander/controlify/contextual/RuleEngine.java +++ b/src/main/java/dev/isxander/controlify/contextual/RuleEngine.java @@ -14,6 +14,7 @@ import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.function.Predicate; /// An ordered set of contextual rules for one output type. /// @@ -49,12 +50,16 @@ public Set factDependencies() { } /// Returns the winning rules in their original precedence order. - public List evaluate(ContextualState state) { + public List evaluate(ContextualState state, Predicate shouldEvaluate) { Objects.requireNonNull(state, "state"); List matches = new ArrayList<>(); Set consumedKeys = new HashSet<>(); for (R rule : this.rules) { + if (!shouldEvaluate.test(rule)) { + continue; + } + K key = rule.key(); if (!consumedKeys.contains(key) && rule.matches(state)) { consumedKeys.add(key); @@ -64,4 +69,8 @@ public List evaluate(ContextualState state) { return List.copyOf(matches); } + + public List evaluate(ContextualState state) { + return this.evaluate(state, _ -> true); + } } diff --git a/src/main/java/dev/isxander/controlify/contextual/RuleSetManager.java b/src/main/java/dev/isxander/controlify/contextual/RuleSetManager.java index 735188644..99cb84ef8 100644 --- a/src/main/java/dev/isxander/controlify/contextual/RuleSetManager.java +++ b/src/main/java/dev/isxander/controlify/contextual/RuleSetManager.java @@ -18,7 +18,12 @@ import java.util.concurrent.Executor; public class RuleSetManager> implements SimpleControlifyReloadListener> { - public static final String DIRECTORY = "contextual/"; + public static final RuleSetManager GUIDE_RULES = + new RuleSetManager<>(GuideRule.CODEC, "guide"); + public static final RuleSetManager TRIGGER_EFFECT_RULES = + new RuleSetManager<>(TriggerEffectRule.CODEC, "trigger_effect"); + + private static final String DIRECTORY = "contextual/"; private final Codec> ruleSetCodec; private final String directory; diff --git a/src/main/java/dev/isxander/controlify/contextual/TriggerEffectRule.java b/src/main/java/dev/isxander/controlify/contextual/TriggerEffectRule.java new file mode 100644 index 000000000..004dd9972 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/contextual/TriggerEffectRule.java @@ -0,0 +1,27 @@ +package dev.isxander.controlify.contextual; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import dev.isxander.controlify.api.bind.InputBindingSupplier; +import dev.isxander.controlify.controller.dualsense.TriggerEffectCodecs; +import dev.isxander.controlify.driver.dualsense.DualsenseTriggerEffect; +import net.minecraft.resources.Identifier; + +/// @param binding the binding supplier associated with this action. +/// if the binding is not bound to either trigger, it is ignored. +public record TriggerEffectRule( + InputBindingSupplier binding, + ContextualPredicate predicate, + DualsenseTriggerEffect effect +) implements Rule { + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + InputBindingSupplier.CODEC.fieldOf("for").forGetter(TriggerEffectRule::binding), + ContextualPredicate.CODEC.fieldOf("if").forGetter(TriggerEffectRule::predicate), + TriggerEffectCodecs.CODEC.fieldOf("then").forGetter(TriggerEffectRule::effect) + ).apply(instance, TriggerEffectRule::new)); + + @Override + public Identifier key() { + return this.binding.bindId(); + } +} diff --git a/src/main/java/dev/isxander/controlify/contextual/TriggerRule.java b/src/main/java/dev/isxander/controlify/contextual/TriggerRule.java deleted file mode 100644 index 9140a58f2..000000000 --- a/src/main/java/dev/isxander/controlify/contextual/TriggerRule.java +++ /dev/null @@ -1,26 +0,0 @@ -package dev.isxander.controlify.contextual; - -import com.mojang.serialization.Codec; -import com.mojang.serialization.codecs.RecordCodecBuilder; -import dev.isxander.controlify.controller.dualsense.TriggerEffectCodecs; -import dev.isxander.controlify.driver.dualsense.DualsenseTriggerEffect; -import net.minecraft.resources.Identifier; - -/// @param trigger identifier of the logical action that may be bound to a trigger. -/// for the in_game domain, the accepted values would be `use_item` and `swing_item` -public record TriggerRule( - Identifier trigger, - ContextualPredicate predicate, - DualsenseTriggerEffect effect -) implements Rule { - public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( - Identifier.CODEC.fieldOf("for").forGetter(TriggerRule::trigger), - ContextualPredicate.CODEC.fieldOf("if").forGetter(TriggerRule::predicate), - TriggerEffectCodecs.CODEC.fieldOf("then").forGetter(TriggerRule::effect) - ).apply(instance, TriggerRule::new)); - - @Override - public Identifier key() { - return this.trigger(); - } -} diff --git a/src/main/java/dev/isxander/controlify/contextual/api/ContextualStateContributor.java b/src/main/java/dev/isxander/controlify/contextual/api/ContextualStateContributor.java deleted file mode 100644 index 525bd7f97..000000000 --- a/src/main/java/dev/isxander/controlify/contextual/api/ContextualStateContributor.java +++ /dev/null @@ -1,7 +0,0 @@ -package dev.isxander.controlify.contextual.api; - -public interface ContextualStateContributor { - /// Ran each update tick - /// The new alternative to the Guide API's old `Fact` where it was a Predicate per fact! - void contribute(C context, ContextualStateSink sink); -} diff --git a/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectInstanceImpl.java b/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectInstanceImpl.java new file mode 100644 index 000000000..6e3945680 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectInstanceImpl.java @@ -0,0 +1,86 @@ +package dev.isxander.controlify.controller.dualsense; + +import dev.isxander.controlify.api.contextual.Context; +import dev.isxander.controlify.api.contextual.TriggerEffectInstance; +import dev.isxander.controlify.contextual.ContextualDomainImpl; +import dev.isxander.controlify.contextual.ContextualState; +import dev.isxander.controlify.contextual.RuleEngine; +import dev.isxander.controlify.contextual.TriggerEffectRule; +import dev.isxander.controlify.controller.ControllerEntity; +import dev.isxander.controlify.controller.input.GamepadInputs; +import dev.isxander.controlify.driver.dualsense.DualsenseTriggerEffect; +import net.minecraft.resources.Identifier; + +import java.util.List; + +public class TriggerEffectInstanceImpl implements TriggerEffectInstance { + private final ContextualDomainImpl domain; + private final RuleEngine ruleEngine; + + private DualsenseTriggerEffect leftEffect = DualsenseTriggerEffect.Off.INSTANCE; + private DualsenseTriggerEffect rightEffect = DualsenseTriggerEffect.Off.INSTANCE; + + public TriggerEffectInstanceImpl( + ContextualDomainImpl domain, + RuleEngine ruleEngine + ) { + this.domain = domain; + this.ruleEngine = ruleEngine; + } + + @Override + public void update(C context) { + boolean useTriggerEffects = context.controller().dualSense() + .map(ds -> ds.settings().triggerEffects) + .orElse(false); + if (!useTriggerEffects) { + this.leftEffect = DualsenseTriggerEffect.Off.INSTANCE; + this.rightEffect = DualsenseTriggerEffect.Off.INSTANCE; + return; + } + + ContextualState state = this.domain.calculateState(context, this.ruleEngine.factDependencies()); + List passedRules = this.ruleEngine.evaluate( + state, + rule -> getTrigger(context.controller(), rule) != Trigger.NEITHER + ); + + // there should only be one each + this.leftEffect = passedRules.stream() + .filter(rule -> getTrigger(context.controller(), rule) == Trigger.LEFT) + .map(TriggerEffectRule::effect) + .findAny() + .orElse(DualsenseTriggerEffect.Off.INSTANCE); + this.rightEffect = passedRules.stream() + .filter(rule -> getTrigger(context.controller(), rule) == Trigger.RIGHT) + .map(TriggerEffectRule::effect) + .findAny() + .orElse(DualsenseTriggerEffect.Off.INSTANCE); + } + + @Override + public DualsenseTriggerEffect getLeftTriggerEffect() { + return this.leftEffect; + } + + @Override + public DualsenseTriggerEffect getRightTriggerEffect() { + return this.rightEffect; + } + + private Trigger getTrigger(ControllerEntity controller, TriggerEffectRule rule) { + List relevantInputs = rule.binding().on(controller).boundInput().getRelevantInputs(); + + if (relevantInputs.contains(GamepadInputs.LEFT_TRIGGER_AXIS)) { + return Trigger.LEFT; + } else if (relevantInputs.contains(GamepadInputs.RIGHT_TRIGGER_AXIS)) { + return Trigger.RIGHT; + } + + return Trigger.NEITHER; + } + + private enum Trigger { + LEFT, RIGHT, NEITHER + } +} diff --git a/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectManager.java b/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectManager.java index 3db803b81..d4260f945 100644 --- a/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectManager.java +++ b/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectManager.java @@ -6,122 +6,42 @@ */ package dev.isxander.controlify.controller.dualsense; -import dev.isxander.controlify.bindings.ControlifyBindings; +import dev.isxander.controlify.api.contextual.TriggerEffectInstance; import dev.isxander.controlify.controller.ControllerEntity; -import dev.isxander.controlify.controller.input.GamepadInputs; import dev.isxander.controlify.driver.dualsense.DualsenseTriggerEffect; import dev.isxander.controlify.utils.MinecraftUtil; import net.minecraft.client.Minecraft; -import net.minecraft.client.player.LocalPlayer; -import net.minecraft.resources.Identifier; -import net.minecraft.world.item.ItemStack; import java.util.*; public class TriggerEffectManager { private final Minecraft minecraft; - private final TriggerEffectRegistry registry; - public TriggerEffectManager(Minecraft minecraft, TriggerEffectRegistry registry) { + public TriggerEffectManager(Minecraft minecraft) { this.minecraft = minecraft; - this.registry = registry; } - public void applyTriggerEffects(ControllerEntity controller, boolean inputSuppressed) { + public void applyTriggerEffects( + ControllerEntity controller, + TriggerEffectInstance instance, + boolean inputSuppressed + ) { controller.dualSense().ifPresent(ds -> { - ds.setLeftTriggerEffect(this.getCurrentLeftTriggerEffect(controller, inputSuppressed)); - ds.setRightTriggerEffect(this.getCurrentRightTriggerEffect(controller, inputSuppressed)); + if (!shouldUseTriggerEffects(controller, inputSuppressed)) { + ds.setLeftTriggerEffect(DualsenseTriggerEffect.Off.INSTANCE); + ds.setRightTriggerEffect(DualsenseTriggerEffect.Off.INSTANCE); + return; + } + + ds.setLeftTriggerEffect(instance.getLeftTriggerEffect()); + ds.setRightTriggerEffect(instance.getRightTriggerEffect()); }); } - public DualsenseTriggerEffect getCurrentLeftTriggerEffect(ControllerEntity controller, boolean inputSuppressed) { - if (!this.shouldUseTriggerEffects(controller, inputSuppressed)) { - return DualsenseTriggerEffect.Off.INSTANCE; - } - - LocalPlayer player = Objects.requireNonNull(this.minecraft.player); - - if (this.getUseItemTrigger(controller) == Trigger.LEFT) { - return this.getUseItemTriggerEffect(player) - .orElse(DualsenseTriggerEffect.Off.INSTANCE); - } - if (this.getSwingItemTrigger(controller) == Trigger.LEFT) { - return this.getSwingItemTriggerEffect(player) - .orElse(DualsenseTriggerEffect.Off.INSTANCE); - } - - return DualsenseTriggerEffect.Off.INSTANCE; - } - - public DualsenseTriggerEffect getCurrentRightTriggerEffect(ControllerEntity controller, boolean inputSuppressed) { - if (!this.shouldUseTriggerEffects(controller, inputSuppressed)) { - return DualsenseTriggerEffect.Off.INSTANCE; - } - - LocalPlayer player = Objects.requireNonNull(this.minecraft.player); - - if (this.getUseItemTrigger(controller) == Trigger.RIGHT) { - return this.getUseItemTriggerEffect(player) - .orElse(DualsenseTriggerEffect.Off.INSTANCE); - } - if (this.getSwingItemTrigger(controller) == Trigger.RIGHT) { - return this.getSwingItemTriggerEffect(player) - .orElse(DualsenseTriggerEffect.Off.INSTANCE); - } - - return DualsenseTriggerEffect.Off.INSTANCE; - } - public boolean shouldUseTriggerEffects(ControllerEntity controller, boolean inputSuppressed) { return !inputSuppressed && MinecraftUtil.getScreen() == null && this.minecraft.player != null && controller.dualSense().map(ds -> ds.settings().triggerEffects).orElse(false); } - - private Trigger getUseItemTrigger(ControllerEntity controller) { - List relevantInputs = ControlifyBindings.USE.on(controller).boundInput().getRelevantInputs(); - - if (relevantInputs.contains(GamepadInputs.LEFT_TRIGGER_AXIS)) { - return Trigger.LEFT; - } else if (relevantInputs.contains(GamepadInputs.RIGHT_TRIGGER_AXIS)) { - return Trigger.RIGHT; - } else { - return Trigger.NEITHER; - } - } - - private Trigger getSwingItemTrigger(ControllerEntity controller) { - List relevantInputs = ControlifyBindings.ATTACK.on(controller).boundInput().getRelevantInputs(); - - if (relevantInputs.contains(GamepadInputs.LEFT_TRIGGER_AXIS)) { - return Trigger.LEFT; - } else if (relevantInputs.contains(GamepadInputs.RIGHT_TRIGGER_AXIS)) { - return Trigger.RIGHT; - } else { - return Trigger.NEITHER; - } - } - - public Optional getUseItemTriggerEffect(LocalPlayer player) { - return this.getUseItemTriggerEffect(player.getActiveItem()) - .or(() -> this.getUseItemTriggerEffect(player.getOffhandItem())); - } - - public Optional getUseItemTriggerEffect(ItemStack stack) { - return this.registry.getUseItemEffect(stack); - } - - public Optional getSwingItemTriggerEffect(LocalPlayer player) { - return this.getSwingItemTriggerEffect(player.getMainHandItem()); - } - - public Optional getSwingItemTriggerEffect(ItemStack stack) { - return this.registry.getSwingItemEffect(stack); - } - - - private enum Trigger { - LEFT, RIGHT, NEITHER - } } diff --git a/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectRegistry.java b/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectRegistry.java deleted file mode 100644 index 1422bdec4..000000000 --- a/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectRegistry.java +++ /dev/null @@ -1,325 +0,0 @@ -/* - * Copyright (C) 2026 isXander - * This file is part of Controlify. - * - * SPDX-License-Identifier: LGPL-3.0-or-later - */ -package dev.isxander.controlify.controller.dualsense; - -import com.google.gson.JsonElement; -import com.google.gson.JsonParser; -import com.mojang.logging.LogUtils; -import com.mojang.serialization.Codec; -import com.mojang.serialization.DynamicOps; -import com.mojang.serialization.JsonOps; -import com.mojang.serialization.codecs.RecordCodecBuilder; -import dev.isxander.controlify.driver.dualsense.DualsenseTriggerEffect; -import dev.isxander.controlify.platform.client.resource.SimpleControlifyReloadListener; -import dev.isxander.controlify.utils.CUtil; -//? if >=26.2 { -import net.minecraft.advancements.predicates.ItemPredicate; -//?} else { -/*import net.minecraft.advancements.criterion.ItemPredicate; -*///?} -import net.minecraft.client.Minecraft; -import net.minecraft.client.multiplayer.CacheSlot; -import net.minecraft.client.multiplayer.ClientLevel; -import net.minecraft.core.HolderLookup; -import net.minecraft.core.RegistryAccess; -import net.minecraft.core.component.DataComponentType; -import net.minecraft.resources.Identifier; -import net.minecraft.server.packs.resources.Resource; -import net.minecraft.server.packs.resources.ResourceManager; -import net.minecraft.util.PlaceholderLookupProvider; -import net.minecraft.util.RegistryContextSwapper; -import net.minecraft.world.item.Item; -import net.minecraft.world.item.ItemStack; -import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; - -import java.io.BufferedReader; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.WeakHashMap; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executor; -import java.util.function.Function; - -public class TriggerEffectRegistry implements SimpleControlifyReloadListener { - private static final Logger LOGGER = LogUtils.getLogger(); - - private static final Identifier USE_ITEM_RESOURCE = CUtil.rl("trigger_effect/use_item.json"); - private static final Identifier SWING_ITEM_RESOURCE = CUtil.rl("trigger_effect/swing_item.json"); - private static final Identifier RELOAD_ID = CUtil.rl("trigger_effect"); - - private static final Codec RULE_CODEC = - RecordCodecBuilder.create(instance -> instance.group( - ItemPredicate.CODEC.fieldOf("require").forGetter(Rule::when), - TriggerEffectCodecs.CODEC.fieldOf("effect").forGetter(Rule::effect) - ).apply(instance, Rule::new)); - private static final Codec> RULES_CODEC = RULE_CODEC.listOf(); - - private List useItemResourceLayers = List.of(); - private List swingItemResourceLayers = List.of(); - private final CacheSlot resolvedRules = new CacheSlot<>(this::resolveResourceRules); - - private final Map useItemResourceCache = new WeakHashMap<>(); - private final Map swingItemResourceCache = new WeakHashMap<>(); - - private final Map, Function> useItemComponentEffects = new LinkedHashMap<>(); - private final Map, Function> swingItemComponentEffects = new LinkedHashMap<>(); - private final Map useItemEffects = new LinkedHashMap<>(); - private final Map swingItemEffects = new LinkedHashMap<>(); - - @Override - public CompletableFuture load(ResourceManager manager, Executor executor) { - return CompletableFuture.supplyAsync(() -> new Preparations( - this.loadResourceStack(manager, USE_ITEM_RESOURCE), - this.loadResourceStack(manager, SWING_ITEM_RESOURCE) - ), executor); - } - - private List loadResourceStack(ResourceManager manager, Identifier id) { - List layers = new ArrayList<>(); - - for (Resource resource : manager.getResourceStack(id).reversed()) { - try (BufferedReader reader = resource.openAsReader()) { - PlaceholderLookupProvider lookup = new PlaceholderLookupProvider(RegistryAccess.EMPTY); - DynamicOps ops = lookup.createSerializationContext(JsonOps.INSTANCE); - JsonElement json = JsonParser.parseReader(reader); - List layerRules = RULES_CODEC - .parse(ops, json) - .getOrThrow(); - layers.add(new PreparedLayer( - List.copyOf(layerRules), - resource.sourcePackId(), - lookup.hasRegisteredPlaceholders() ? lookup.createSwapper() : null - )); - } catch (Exception e) { - LOGGER.error( - "Failed to load adaptive trigger effects from {} in pack {}; skipping this layer", - id, - resource.sourcePackId(), - e - ); - } - } - - return List.copyOf(layers); - } - - @Override - public CompletableFuture apply(Preparations data, ResourceManager manager, Executor executor) { - return CompletableFuture.runAsync(() -> { - this.useItemResourceLayers = data.useItemLayers; - this.swingItemResourceLayers = data.swingItemLayers; - this.invalidateResolvedResourceRules(); - LOGGER.info( - "Loaded {} use-item and {} swing-item adaptive trigger effect rule layers", - this.useItemResourceLayers.size(), - this.swingItemResourceLayers.size() - ); - }, executor); - } - - public void registerUseItemComponentEffect( - DataComponentType componentType, - Function effectFunction - ) { - this.useItemComponentEffects.put(componentType, castEffectFunction(effectFunction)); - } - - public void registerSwingItemComponentEffect( - DataComponentType componentType, - Function effectFunction - ) { - this.swingItemComponentEffects.put(componentType, castEffectFunction(effectFunction)); - } - - public void registerUseItemEffect(Item item, DualsenseTriggerEffect effect) { - this.useItemEffects.put(item, effect); - } - - public void registerSwingItemEffect(Item item, DualsenseTriggerEffect effect) { - this.swingItemEffects.put(item, effect); - } - - public Optional getUseItemEffect(ItemStack stack) { - ResolvedRules rules = this.currentResourceRules(); - return findEffect( - stack, - rules.useItemRules(), - this.useItemResourceCache, - this.useItemComponentEffects, - this.useItemEffects - ); - } - - public Optional getSwingItemEffect(ItemStack stack) { - ResolvedRules rules = this.currentResourceRules(); - return findEffect( - stack, - rules.swingItemRules(), - this.swingItemResourceCache, - this.swingItemComponentEffects, - this.swingItemEffects - ); - } - - private Optional findEffect( - ItemStack stack, - List resourceRules, - Map resourceCache, - Map, Function> componentEffects, - Map itemEffects - ) { - Optional resourceEffect = this.findResourceEffect(stack, resourceRules, resourceCache); - if (resourceEffect.isPresent()) { - return resourceEffect; - } - - for (var entry : componentEffects.entrySet()) { - if (stack.has(entry.getKey())) { - @Nullable DualsenseTriggerEffect effect = entry.getValue().apply(stack.get(entry.getKey())); - if (effect != null) { - return Optional.of(effect); - } - } - } - - return Optional.ofNullable(itemEffects.get(stack.getItem())); - } - - private Optional findResourceEffect( - ItemStack stack, - List rules, - Map cache - ) { - CachedResourceMatch cached = cache.get(stack); - if (cached == null || !ItemStack.matches(stack, cached.snapshot())) { - Optional effect = rules.stream() - .filter(rule -> rule.matches(stack)) - .map(Rule::effect) - .findFirst(); - cached = new CachedResourceMatch(stack.copy(), effect); - cache.put(stack, cached); - } - - return cached.effect(); - } - - private ResolvedRules currentResourceRules() { - ClientLevel level = Minecraft.getInstance().level; - return level == null ? ResolvedRules.EMPTY : this.resolvedRules.compute(level); - } - - private ResolvedRules resolveResourceRules(ClientLevel level) { - HolderLookup.Provider registries = level.registryAccess(); - List useItemRules = this.resolveResourceLayers(this.useItemResourceLayers, USE_ITEM_RESOURCE, registries); - List swingItemRules = this.resolveResourceLayers(this.swingItemResourceLayers, SWING_ITEM_RESOURCE, registries); - this.invalidateResourceRuleCaches(); - LOGGER.info( - "Resolved {} use-item and {} swing-item adaptive trigger effect rules for the current world", - useItemRules.size(), - swingItemRules.size() - ); - return new ResolvedRules(useItemRules, swingItemRules); - } - - private List resolveResourceLayers( - List layers, - Identifier id, - HolderLookup.Provider registries - ) { - List rules = new ArrayList<>(); - - for (PreparedLayer layer : layers) { - try { - List resolvedRules = layer.registrySwapper() == null - ? layer.rules() - : layer.registrySwapper().swapTo(RULES_CODEC, layer.rules(), registries).getOrThrow(); - rules.addAll(resolvedRules); - } catch (Exception e) { - LOGGER.error( - "Failed to resolve adaptive trigger effects from {} in pack {} for the current world; skipping this layer", - id, - layer.sourcePackId(), - e - ); - } - } - - return List.copyOf(rules); - } - - public void invalidateResolvedResourceRules() { - this.resolvedRules.clear(); - this.invalidateResourceRuleCaches(); - } - - public void invalidateResourceRuleCaches() { - this.useItemResourceCache.clear(); - this.swingItemResourceCache.clear(); - } - - @SuppressWarnings("unchecked") - private static Function castEffectFunction( - Function effectFunction - ) { - return (Function) effectFunction; - } - - @Override - public Identifier getReloadId() { - return RELOAD_ID; - } - - private record Rule( - ItemPredicate when, - DualsenseTriggerEffect effect - ) { - public boolean matches(ItemStack stack) { - // reimplement `ItemPredicate#matches` with ClientTags fallback, to allow `c:` tags with vanilla servers - // neoforge does not implement ClientTags api, but this is okay as nobody runs NeoForge client with vanilla - // servers anyway - if (when.items().isPresent() && !CUtil.isInWithLocalFallback(stack, when.items().get())) { - return false; - } else { - return when.count().matches(stack.count()) && when.components().test(stack); - } - } - } - - private record PreparedLayer( - List rules, - String sourcePackId, - @Nullable RegistryContextSwapper registrySwapper - ) { - } - - private record ResolvedRules( - List useItemRules, - List swingItemRules - ) { - private static final ResolvedRules EMPTY = new ResolvedRules(List.of(), List.of()); - } - - private record CachedResourceMatch( - ItemStack snapshot, - Optional effect - ) { - } - - public static final class Preparations { - private final List useItemLayers; - private final List swingItemLayers; - - private Preparations(List useItemLayers, List swingItemLayers) { - this.useItemLayers = useItemLayers; - this.swingItemLayers = swingItemLayers; - } - } -} diff --git a/src/main/java/dev/isxander/controlify/gui/guide/GuideDomainImpl.java b/src/main/java/dev/isxander/controlify/gui/guide/GuideDomainImpl.java deleted file mode 100644 index 6ea757364..000000000 --- a/src/main/java/dev/isxander/controlify/gui/guide/GuideDomainImpl.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright (C) 2026 isXander - * This file is part of Controlify. - * - * SPDX-License-Identifier: LGPL-3.0-or-later - */ -package dev.isxander.controlify.gui.guide; - -import com.google.gson.JsonElement; -import com.google.gson.JsonParser; -import com.mojang.serialization.DataResult; -import com.mojang.serialization.JsonOps; -import dev.isxander.controlify.api.guide.*; -import dev.isxander.controlify.platform.client.resource.SimpleControlifyReloadListener; -import net.minecraft.resources.FileToIdConverter; -import net.minecraft.resources.Identifier; -import net.minecraft.server.packs.resources.Resource; -import net.minecraft.server.packs.resources.ResourceManager; -import org.apache.commons.lang3.Validate; - -import java.io.BufferedReader; -import java.io.IOException; -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executor; - -public class GuideDomainImpl implements GuideDomain, SimpleControlifyReloadListener { - public static final String DIRECTORY = "guides"; - private static final FileToIdConverter converter = FileToIdConverter.json(DIRECTORY); - - /** The unique identifier of this domain */ - private final Identifier id; - - /** Facts registered via code */ - private final Map> facts = new HashMap<>(); - /** Registered dynamic rules - rules are dynamic if they were created in-code */ - private final List dynamicRules = new ArrayList<>(); - /** Whether facts and dynamic rules registries have been frozen */ - private boolean frozen = false; - - /** All rules loaded from resource packs as well as dynamic rules */ - private List rules; - /** Incremented on reload, so instances can check require they need to reset. */ - private int reloadEpoch; - - public GuideDomainImpl(Identifier id) { - this.id = id; - } - - public Identifier id() { - return this.id; - } - - @Override - public void registerFact(Fact fact) { - Validate.isTrue(!this.frozen, "Cannot register facts after the domain has been frozen"); - Validate.notNull(fact, "Fact cannot be null"); - Validate.isTrue(!this.facts.containsKey(fact.id()), "Fact with id %s already exists in domain %s", fact.id(), this.id); - - var invariantFact = new Fact(fact.id(), fact.provider()::test); - - this.facts.put(fact.id(), invariantFact); - } - - @Override - public void registerDynamicRule(Rule rule) { - Validate.isTrue(!this.frozen, "Cannot register dynamic rule after the domain has been frozen"); - Validate.notNull(rule, "Rule cannot be null"); - - this.dynamicRules.add(rule); - } - - @Override - public CompletableFuture load(ResourceManager manager, Executor executor) { - return CompletableFuture.supplyAsync(() -> { - List resources = manager.getResourceStack(converter.idToFile(this.id)); - - // add all rule sets to a list until we hit a replace = true - var ruleSets = new ArrayList(); - for (Resource resource : resources) { - try (BufferedReader reader = resource.openAsReader()) { - JsonElement element = JsonParser.parseReader(reader); - DataResult result = RuleSet.CODEC.parse(JsonOps.INSTANCE, element); - var ruleSet = result.getOrThrow(); - - ruleSets.add(ruleSet); - if (ruleSet.replace()) { - // if this rule set replaces all that's below it, we can skip parsing everything below - break; - } - } catch (IOException e) { - throw new IllegalStateException("Failed to load resource", e); - } - } - - // merge all the rules into a single list, where the topmost rules take precedence - List rules = ruleSets.stream() - .flatMap(rs -> rs.rules().stream()) - .toList(); - - return new Preparations(rules); - }, executor); - } - - @Override - public CompletableFuture apply(Preparations data, ResourceManager manager, Executor executor) { - return CompletableFuture.runAsync(() -> { - Validate.isTrue(this.frozen, "Cannot apply domain before the domain has been frozen"); - - int ruleCount = data.rules().size() + this.dynamicRules.size(); - - this.rules = new ArrayList<>(ruleCount); - this.rules.addAll(data.rules()); - this.rules.addAll(this.dynamicRules); - - this.reloadEpoch += 1; - }, executor); - } - - public boolean freeze() { - if (this.frozen) { - return false; // already frozen - } - this.frozen = true; - return true; - } - - public boolean isFrozen() { - return this.frozen; - } - - public List rules() { - return Collections.unmodifiableList(Validate.notNull(this.rules, "Rules have not been loaded yet for domain %s", this.id)); - } - - public Map> facts() { - return Collections.unmodifiableMap(Validate.notNull(this.facts, "Facts have not been loaded yet for domain %s", this.id)); - } - - public int currentReloadEpoch() { - return this.reloadEpoch; - } - - @Override - public Identifier getReloadId() { - return this.id.withPrefix("reload/"); - } - - @Override - public GuideInstance createInstance() { - return new GuideInstanceImpl<>(this); - } - - public record Preparations( - List rules - ) {} -} diff --git a/src/main/java/dev/isxander/controlify/gui/guide/GuideDomains.java b/src/main/java/dev/isxander/controlify/gui/guide/GuideDomains.java deleted file mode 100644 index aa1f4840a..000000000 --- a/src/main/java/dev/isxander/controlify/gui/guide/GuideDomains.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2026 isXander - * This file is part of Controlify. - * - * SPDX-License-Identifier: LGPL-3.0-or-later - */ -package dev.isxander.controlify.gui.guide; - -import dev.isxander.controlify.api.guide.ContainerCtx; -import dev.isxander.controlify.api.guide.ContainerFacts; -import dev.isxander.controlify.api.guide.InGameCtx; -import dev.isxander.controlify.api.guide.InGameFacts; -import dev.isxander.controlify.utils.CUtil; -import net.minecraft.resources.Identifier; - -import java.util.HashMap; -import java.util.Map; - -public final class GuideDomains { - private GuideDomains() {} - - public static final GuideDomainImpl IN_GAME = new GuideDomainImpl<>(CUtil.rl("in_game")); - public static final GuideDomainImpl CONTAINER = new GuideDomainImpl<>(CUtil.rl("container")); - - public static final Map> CUSTOM_DOMAINS = new HashMap<>(); - - public static void freeze() { - IN_GAME.freeze(); - CONTAINER.freeze(); - CUSTOM_DOMAINS.values().forEach(GuideDomainImpl::freeze); - } - - static { - InGameFacts.registerAll(); - ContainerFacts.registerAll(); - } -} diff --git a/src/main/java/dev/isxander/controlify/gui/guide/GuideInstanceImpl.java b/src/main/java/dev/isxander/controlify/gui/guide/GuideInstanceImpl.java index 9967870f5..64464f378 100644 --- a/src/main/java/dev/isxander/controlify/gui/guide/GuideInstanceImpl.java +++ b/src/main/java/dev/isxander/controlify/gui/guide/GuideInstanceImpl.java @@ -1,199 +1,108 @@ -/* - * Copyright (C) 2026 isXander - * This file is part of Controlify. - * - * SPDX-License-Identifier: LGPL-3.0-or-later - */ package dev.isxander.controlify.gui.guide; import dev.isxander.controlify.api.bind.InputBinding; -import dev.isxander.controlify.api.guide.*; +import dev.isxander.controlify.api.contextual.GuideLocation; +import dev.isxander.controlify.api.contextual.Context; +import dev.isxander.controlify.api.contextual.GuideInstance; +import dev.isxander.controlify.contextual.ContextualDomainImpl; +import dev.isxander.controlify.contextual.ContextualState; +import dev.isxander.controlify.contextual.GuideRule; +import dev.isxander.controlify.contextual.RuleEngine; import dev.isxander.controlify.controller.ControllerEntity; import dev.isxander.controlify.font.BindingFontHelper; -import net.minecraft.client.Minecraft; import net.minecraft.client.gui.Font; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.components.Renderable; import net.minecraft.network.chat.Component; -import net.minecraft.resources.Identifier; -import org.apache.commons.lang3.Validate; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.stream.Stream; +import java.util.List; -public class GuideInstanceImpl implements GuideInstance { - private final GuideDomainImpl domain; - - /** The facts that need to be resolved each tick based on references */ - private Map resolvedFacts = Map.of(); +public class GuideInstanceImpl implements GuideInstance { + private final ContextualDomainImpl domain; + private final RuleEngine ruleEngine; + private final Font font; private PrecomputedLines leftGuides = PrecomputedLines.EMPTY; private PrecomputedLines rightGuides = PrecomputedLines.EMPTY; - /** True after reset since font width/height may be different */ - private boolean precomputeInvalid = false; - private int reloadEpoch = 0; - - public GuideInstanceImpl(GuideDomainImpl domain) { - Validate.isTrue(domain.isFrozen(), "Cannot create Guide instance until domain has been frozen"); + public GuideInstanceImpl( + ContextualDomainImpl domain, + RuleEngine ruleEngine, + Font font + ) { this.domain = domain; - this.reset(); + this.ruleEngine = ruleEngine; + this.font = font; } @Override - public boolean update(T context, Font font) { - if (this.domain.currentReloadEpoch() != this.reloadEpoch) { - reset(); - } - - if (!this.updateFactResolution(context) && !this.precomputeInvalid) { - // no facts changed, no need to update guides - return false; - } - this.precomputeInvalid = false; + public void update(C context) { + ContextualState state = this.domain.calculateState(context, this.ruleEngine.factDependencies()); + List guides = this.ruleEngine.evaluate(state); - precomputeGuideLayout(context, font); - - return true; - } - - @Override - public void extractRenderState(GuiGraphicsExtractor graphics, boolean bottomAligned, boolean textContrast, int guiScale) { - GuideRenderer.extractRenderState( - graphics, - this, - Minecraft.getInstance(), - bottomAligned, - textContrast, - guiScale - ); - } - - @Override - public Renderable renderable(boolean bottomAligned, boolean textContrast, int guiScale) { - return new GuideRenderer.Renderable( - this, - Minecraft.getInstance(), - bottomAligned, - textContrast, - guiScale - ); - } - - /** - * Updates the fact resolution for all facts in this domain. - * @param context the context to use for fact resolution - * @return true if any facts changed, false otherwise - */ - private boolean updateFactResolution(T context) { - boolean changed = false; - for (Map.Entry entry : this.resolvedFacts.entrySet()) { - Identifier factId = entry.getKey(); - Fact fact = this.domain.facts().get(factId); // facts.get should never return null here - - boolean newValue = fact.provider().test(context); - changed |= (newValue != entry.getValue()); - - entry.setValue(newValue); - } - - return changed; - } - - private void precomputeGuideLayout(T context, Font font) { var leftBuilder = new PrecomputedLines.Builder(); var rightBuilder = new PrecomputedLines.Builder(); - var leftConsumed = new HashSet(this.leftGuides.lines().size() + 5); - var rightConsumed = new HashSet(this.rightGuides.lines().size() + 5); - ControllerEntity controller = context.controller(); - for (Rule rule : this.domain.rules()) { - InputBinding binding = rule.binding().onOrNull(controller); + for (GuideRule guide : guides) { + InputBinding binding = guide.binding().onOrNull(controller); if (binding == null || binding.isUnbound()) { - // skip this rule + // skip this rule if the binding is not bound continue; } - // get the builder and consumed set based on the rule's location - PrecomputedLines.Builder builder; - Set consumedBinds; - switch (rule.where()) { - case LEFT -> { - builder = leftBuilder; - consumedBinds = leftConsumed; - } - case RIGHT -> { - builder = rightBuilder; - consumedBinds = rightConsumed; - } - default -> throw new IllegalStateException("Unexpected action location: " + rule.where()); - } + PrecomputedLines.Builder builder = switch (guide.location()) { + case LEFT -> leftBuilder; + case RIGHT -> rightBuilder; + }; - // add the binding to the consumed set so we skip extra work for the same binding - if (consumedBinds.contains(binding.id())) { - continue; - } + // put the glyph after or before the binding glyph based on the rule's location and font direction + boolean glyphAfter = font.isBidirectional() ^ (guide.location() == GuideLocation.RIGHT); - // check if the rule's conditions are met - boolean whenPermits = rule.when().stream() - .allMatch(this.resolvedFacts::get); - boolean forbidPermits = rule.forbid().stream() - .noneMatch(this.resolvedFacts::get); + // formulate the text to display + Component text = Component.empty() + .append(glyphAfter ? guide.text() : binding.inputGlyph()) + .append(" ") + .append(glyphAfter ? binding.inputGlyph() : guide.text()); - if (whenPermits && forbidPermits) { - consumedBinds.add(binding.id()); + // precompute the width and height of the text + int ruleNameWidth = font.width(guide.text()); + int width = font.width(text); + int glyphWidth = width - ruleNameWidth; + int backgroundLeft = glyphAfter ? 0 : glyphWidth; + int backgroundRight = backgroundLeft + ruleNameWidth; - // put the glyph after or before the binding glyph based on the rule's location and font direction - boolean glyphAfter = font.isBidirectional() ^ (rule.where() == ActionLocation.RIGHT); + int height = BindingFontHelper.getComponentHeight(font, binding.inputGlyph()); // use input glyph height only as it's bound to be the tallest - // formulate the text to display - Component text = Component.empty() - .append(glyphAfter ? rule.then() : binding.inputGlyph()) - .append(" ") - .append(glyphAfter ? binding.inputGlyph() : rule.then()); - - // precompute the width and height of the text - int ruleNameWidth = font.width(rule.then()); - int width = font.width(text); - int glyphWidth = width - ruleNameWidth; - int backgroundLeft = glyphAfter ? 0 : glyphWidth; - int backgroundRight = backgroundLeft + ruleNameWidth; - - int height = BindingFontHelper.getComponentHeight(font, binding.inputGlyph()); // use input glyph height only as it's bound to be the tallest - - builder.addLine(text, width, height, backgroundLeft, backgroundRight); - } + builder.addLine(text, width, height, backgroundLeft, backgroundRight); } this.leftGuides = leftBuilder.build(); this.rightGuides = rightBuilder.build(); } - /** - * When the reload epoch of the backing domain changes, - * this method is called which ensures all state is reset. - */ - private void reset() { - this.resolvedFacts = new HashMap<>(this.domain.rules().size() * 2); - - Stream when = this.domain.rules().stream() - .flatMap(rule -> rule.when().stream()); - Stream forbid = this.domain.rules().stream() - .flatMap(rule -> rule.forbid().stream()); - Stream.concat(when, forbid) - .distinct() - .forEach(factId -> { - Fact fact = this.domain.facts().get(factId); - Validate.notNull(fact, "Fact %s is not registered in domain %s", factId, this.domain.id()); - this.resolvedFacts.put(factId, false); - }); - - this.reloadEpoch = this.domain.currentReloadEpoch(); - this.precomputeInvalid = true; + @Override + public void extractRenderState(GuiGraphicsExtractor graphics, boolean bottomAligned, boolean textContrast, int guiScale) { +// GuideRenderer.extractRenderState( +// graphics, +// this, +// Minecraft.getInstance(), +// bottomAligned, +// textContrast, +// guiScale +// ); + } + + @Override + public Renderable renderable(boolean bottomAligned, boolean textContrast, int guiScale) { +// return new GuideRenderer.Renderable( +// this, +// Minecraft.getInstance(), +// bottomAligned, +// textContrast, +// guiScale +// ); + return null; } public PrecomputedLines leftGuides() { @@ -203,9 +112,4 @@ public PrecomputedLines leftGuides() { public PrecomputedLines rightGuides() { return this.rightGuides; } - - @Override - public GuideDomain domain() { - return this.domain; - } } diff --git a/src/main/java/dev/isxander/controlify/gui/guide/InGameButtonGuide.java b/src/main/java/dev/isxander/controlify/gui/guide/InGameButtonGuide.java index 55b783105..a5fda7443 100644 --- a/src/main/java/dev/isxander/controlify/gui/guide/InGameButtonGuide.java +++ b/src/main/java/dev/isxander/controlify/gui/guide/InGameButtonGuide.java @@ -6,10 +6,11 @@ */ package dev.isxander.controlify.gui.guide; -import dev.isxander.controlify.api.guide.GuideInstance; +import dev.isxander.controlify.api.contextual.GuideInstance; import dev.isxander.controlify.config.settings.profile.GenericControllerSettings; +import dev.isxander.controlify.contextual.ContextualDomains; import dev.isxander.controlify.controller.ControllerEntity; -import dev.isxander.controlify.api.guide.InGameCtx; +import dev.isxander.controlify.api.contextual.InGameContext; import dev.isxander.controlify.mixins.feature.guide.ingame.MinecraftAccessor; import dev.isxander.controlify.utils.MinecraftUtil; import net.minecraft.client.Minecraft; @@ -18,12 +19,12 @@ public class InGameButtonGuide { private final ControllerEntity controller; private final Minecraft minecraft; - private final GuideInstance guideInstance; + private final GuideInstance guideInstance; public InGameButtonGuide(ControllerEntity controller, Minecraft minecraft) { this.controller = controller; this.minecraft = minecraft; - this.guideInstance = GuideDomains.IN_GAME.createInstance(); + this.guideInstance = ContextualDomains.INSTANCE.inGame().createGuideInstance(minecraft.font); } public void extractRenderState(GuiGraphicsExtractor graphics, float tickDelta) { @@ -49,15 +50,14 @@ public void tick() { ((MinecraftAccessor) minecraft).controlify$invokePick(1f); } this.guideInstance.update( - new InGameCtx( + new InGameContext( minecraft, minecraft.player, minecraft.level, minecraft.hitResult, controller, settings.verbosity - ), - minecraft.font + ) ); } } diff --git a/src/main/java/dev/isxander/controlify/gui/guide/RuleSet.java b/src/main/java/dev/isxander/controlify/gui/guide/RuleSet.java deleted file mode 100644 index e85c0224b..000000000 --- a/src/main/java/dev/isxander/controlify/gui/guide/RuleSet.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (C) 2026 isXander - * This file is part of Controlify. - * - * SPDX-License-Identifier: LGPL-3.0-or-later - */ -package dev.isxander.controlify.gui.guide; - -import com.mojang.serialization.Codec; -import com.mojang.serialization.codecs.RecordCodecBuilder; -import dev.isxander.controlify.api.guide.Rule; - -import java.util.List; - -public record RuleSet( - boolean replace, - List rules -) { - public static final Codec CODEC = RecordCodecBuilder.create( - instance -> instance.group( - Codec.BOOL.optionalFieldOf("replace", false).forGetter(RuleSet::replace), - Rule.CODEC.listOf().fieldOf("rules").forGetter(RuleSet::rules) - ).apply(instance, RuleSet::new) - ); -} diff --git a/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java b/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java index d9a9b4e22..3631e1f51 100644 --- a/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java +++ b/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java @@ -7,7 +7,7 @@ package dev.isxander.controlify.gui.screen; import dev.isxander.controlify.Controlify; -import dev.isxander.controlify.api.guide.GuideVerbosity; +import dev.isxander.controlify.api.contextual.GuideVerbosity; import dev.isxander.controlify.bindings.BindContext; import dev.isxander.controlify.bindings.ControlifyBindApiImpl; import dev.isxander.controlify.api.bind.InputBinding; diff --git a/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/AbstractContainerScreenProcessor.java b/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/AbstractContainerScreenProcessor.java index 1a6885efb..ad031fcd0 100644 --- a/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/AbstractContainerScreenProcessor.java +++ b/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/AbstractContainerScreenProcessor.java @@ -8,7 +8,7 @@ import dev.isxander.controlify.InputMode; import dev.isxander.controlify.api.ControlifyApi; -import dev.isxander.controlify.api.guide.ContainerCtx; +import dev.isxander.controlify.api.contextual.ContainerContext; import dev.isxander.controlify.api.guide.GuideInstance; import dev.isxander.controlify.bindings.ControlifyBindings; import dev.isxander.controlify.controller.ControllerEntity; @@ -32,7 +32,7 @@ public class AbstractContainerScreenProcessor> extends ScreenProcessor { - private final GuideInstance guideInstance; + private final GuideInstance guideInstance; private GuideRenderer.Renderable guideRenderable; private final Supplier hoveredSlot; @@ -57,7 +57,8 @@ public AbstractContainerScreenProcessor( protected void handleScreenVMouse(ControllerEntity controller, VirtualMouseHandler vmouse) { var accessor = (AbstractContainerScreenAccessor) screen; - var ctx = new ContainerCtx( + var ctx = new ContainerContext( + minecraft.player, hoveredSlot.get(), screen.getMenu().getCarried(), accessor.controlify$invokeHasClickedOutside( diff --git a/src/main/java/dev/isxander/controlify/contextual/ClientEntityPredicate.java b/src/main/java/dev/isxander/controlify/utils/predicates/ClientEntityPredicate.java similarity index 90% rename from src/main/java/dev/isxander/controlify/contextual/ClientEntityPredicate.java rename to src/main/java/dev/isxander/controlify/utils/predicates/ClientEntityPredicate.java index 3b0f5d99e..0be863a47 100644 --- a/src/main/java/dev/isxander/controlify/contextual/ClientEntityPredicate.java +++ b/src/main/java/dev/isxander/controlify/utils/predicates/ClientEntityPredicate.java @@ -1,18 +1,18 @@ -package dev.isxander.controlify.contextual; +package dev.isxander.controlify.utils.predicates; import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; -import dev.isxander.controlify.utils.ClientPredicateUtils; import net.minecraft.advancements.predicates.DistancePredicate; +import net.minecraft.advancements.predicates.GameTypePredicate; import net.minecraft.advancements.predicates.MobEffectsPredicate; import net.minecraft.advancements.predicates.SlotsPredicate; -import net.minecraft.advancements.predicates.entity.EntityEquipmentPredicate; import net.minecraft.advancements.predicates.entity.EntityFlagsPredicate; import net.minecraft.advancements.predicates.entity.EntityTypePredicate; import net.minecraft.advancements.predicates.entity.MovementPredicate; import net.minecraft.client.player.LocalPlayer; import net.minecraft.util.ExtraCodecs; import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; import net.minecraft.world.phys.Vec3; import net.minecraft.world.scores.Team; import org.jetbrains.annotations.Nullable; @@ -41,7 +41,8 @@ public record ClientEntityPredicate( Optional vehicle, Optional passenger, Optional team, - Optional slots + Optional slots, + Optional gameMode ) { public static final Codec CODEC = Codec.recursive( "ClientEntityPredicate", @@ -56,7 +57,8 @@ public record ClientEntityPredicate( subCodec.optionalFieldOf("vehicle").forGetter(ClientEntityPredicate::vehicle), subCodec.optionalFieldOf("passenger").forGetter(ClientEntityPredicate::passenger), Codec.STRING.optionalFieldOf("team").forGetter(ClientEntityPredicate::team), - SlotsPredicate.CODEC.optionalFieldOf("slots").forGetter(ClientEntityPredicate::slots) + SlotsPredicate.CODEC.optionalFieldOf("slots").forGetter(ClientEntityPredicate::slots), + GameTypePredicate.CODEC.optionalFieldOf("game_mode").forGetter(ClientEntityPredicate::gameMode) ).apply(instance, ClientEntityPredicate::new)) ); @@ -125,6 +127,10 @@ public boolean matches(@Nullable Vec3 position, Entity entity) { return false; } + if (this.gameMode.isPresent() && ((!(entity instanceof Player player) || !this.gameMode.get().matches(player.gameMode())))) { + return false; + } + return true; } } diff --git a/src/main/java/dev/isxander/controlify/utils/ClientPredicateUtils.java b/src/main/java/dev/isxander/controlify/utils/predicates/ClientPredicateUtils.java similarity index 94% rename from src/main/java/dev/isxander/controlify/utils/ClientPredicateUtils.java rename to src/main/java/dev/isxander/controlify/utils/predicates/ClientPredicateUtils.java index d2db1d559..f26fff87b 100644 --- a/src/main/java/dev/isxander/controlify/utils/ClientPredicateUtils.java +++ b/src/main/java/dev/isxander/controlify/utils/predicates/ClientPredicateUtils.java @@ -1,5 +1,6 @@ -package dev.isxander.controlify.utils; +package dev.isxander.controlify.utils.predicates; +import dev.isxander.controlify.utils.CUtil; import it.unimi.dsi.fastutil.ints.IntList; import net.minecraft.advancements.predicates.ItemPredicate; import net.minecraft.advancements.predicates.SlotsPredicate; diff --git a/src/main/java/dev/isxander/controlify/utils/predicates/EntityEquipmentPredicate.java b/src/main/java/dev/isxander/controlify/utils/predicates/EntityEquipmentPredicate.java new file mode 100644 index 000000000..78a8acc1b --- /dev/null +++ b/src/main/java/dev/isxander/controlify/utils/predicates/EntityEquipmentPredicate.java @@ -0,0 +1,63 @@ +package dev.isxander.controlify.utils.predicates; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import java.util.Optional; + +import net.minecraft.advancements.predicates.ItemPredicate; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.LivingEntity; +import org.jspecify.annotations.Nullable; + +public record EntityEquipmentPredicate( + Optional head, + Optional chest, + Optional legs, + Optional feet, + Optional body, + Optional mainhand, + Optional offhand, + Optional saddle +) { + public static final Codec CODEC = RecordCodecBuilder.create( + i -> i.group( + ItemPredicate.CODEC.optionalFieldOf("head").forGetter(EntityEquipmentPredicate::head), + ItemPredicate.CODEC.optionalFieldOf("chest").forGetter(EntityEquipmentPredicate::chest), + ItemPredicate.CODEC.optionalFieldOf("legs").forGetter(EntityEquipmentPredicate::legs), + ItemPredicate.CODEC.optionalFieldOf("feet").forGetter(EntityEquipmentPredicate::feet), + ItemPredicate.CODEC.optionalFieldOf("body").forGetter(EntityEquipmentPredicate::body), + ItemPredicate.CODEC.optionalFieldOf("mainhand").forGetter(EntityEquipmentPredicate::mainhand), + ItemPredicate.CODEC.optionalFieldOf("offhand").forGetter(EntityEquipmentPredicate::offhand), + ItemPredicate.CODEC.optionalFieldOf("saddle").forGetter(EntityEquipmentPredicate::saddle) + ) + .apply(i, EntityEquipmentPredicate::new) + ); + + public boolean matches(final @Nullable Entity entity) { + if (entity instanceof LivingEntity livingEntity) { + if (this.head.isPresent() && !ClientPredicateUtils.matches(this.head.get(), livingEntity.getItemBySlot(EquipmentSlot.HEAD))) { + return false; + } else if (this.chest.isPresent() && !ClientPredicateUtils.matches(this.chest.get(), livingEntity.getItemBySlot(EquipmentSlot.CHEST))) { + return false; + } else if (this.legs.isPresent() && !ClientPredicateUtils.matches(this.legs.get(), livingEntity.getItemBySlot(EquipmentSlot.LEGS))) { + return false; + } else if (this.feet.isPresent() && !ClientPredicateUtils.matches(this.feet.get(), livingEntity.getItemBySlot(EquipmentSlot.FEET))) { + return false; + } else if (this.body.isPresent() && !ClientPredicateUtils.matches(this.body.get(), livingEntity.getItemBySlot(EquipmentSlot.BODY))) { + return false; + } else if (this.mainhand.isPresent() && !ClientPredicateUtils.matches(this.mainhand.get(), livingEntity.getItemBySlot(EquipmentSlot.MAINHAND))) { + return false; + } else if (this.offhand.isPresent() && !ClientPredicateUtils.matches(this.offhand.get(), livingEntity.getItemBySlot(EquipmentSlot.OFFHAND))) { + return false; + } else if (this.saddle.isPresent() && !ClientPredicateUtils.matches(this.saddle.get(), livingEntity.getItemBySlot(EquipmentSlot.SADDLE))) { + return false; + } + + return true; + } + + return false; + } +} + diff --git a/src/main/resources/assets/controlify/contextual/facts/container.json b/src/main/resources/assets/controlify/contextual/facts/container.json new file mode 100644 index 000000000..95017c130 --- /dev/null +++ b/src/main/resources/assets/controlify/contextual/facts/container.json @@ -0,0 +1,50 @@ +{ + "controlify:hovering_slot": { + "slot": "controlify:hovering_slot", + "item": {} + }, + "controlify:hovering_item": { + "slot": "controlify:hovering_slot", + "item": { + "count": { + "min": 1 + } + } + }, + "controlify:hovering_many_items": { + "slot": "controlify:hovering_slot", + "item": { + "count": { + "min": 2 + } + } + }, + "controlify:holding_item": { + "slot": "controlify:holding_item", + "item": { + "count": { + "min": 1 + } + } + }, + "controlify:holding_many_items": { + "slot": "controlify:holding_item", + "item": { + "count": { + "min": 2 + } + } + }, + "controlify:hovering_item_is_bundle": { + "slot": "controlify:hovering_slot", + "item": { + "predicates": { + "minecraft:bundle_contents": {} + } + } + }, + "controlify:selected_bundle_slot": { + "slot": "controlify:bundle_selected", + "item": {} + } +} diff --git a/src/main/resources/assets/controlify/contextual/facts/in_game.json b/src/main/resources/assets/controlify/contextual/facts/in_game.json new file mode 100644 index 000000000..6153bbc62 --- /dev/null +++ b/src/main/resources/assets/controlify/contextual/facts/in_game.json @@ -0,0 +1,143 @@ +{ + "controlify:on_ground": { + "slot": "controlify:self", + "entity": { + "flags": { + "is_on_ground": true + } + } + }, + "controlify:in_vehicle": { + "slot": "controlify:self", + "entity": { + "vehicle": {} + } + }, + "controlify:riding_saddled_horse": { + "slot": "controlify:self", + "entity": { + "vehicle": { + "type": "#minecraft:can_equip_saddle", + "equipment": { + "saddle": {} + } + } + } + }, + "controlify:riding_happy_ghast": { + "slot": "controlify:self", + "entity": { + "vehicle": { + "type": "minecraft:happy_ghast" + } + } + }, + "controlify:flying": { + "slot": "controlify:self", + "entity": { + "flags": { + "is_flying": true + } + } + }, + "controlify:elytra_flying": { + "slot": "controlify:self", + "entity": { + "flags": { + "is_fall_flying": true + } + } + }, + "controlify:in_water": { + "slot": "controlify:self", + "entity": { + "flags": { + "is_in_water": true + } + } + }, + "controlify:sneaking": { + "slot": "controlify:self", + "entity": { + "flags": { + "is_sneaking": true + } + } + }, + "controlify:sprinting": { + "slot": "controlify:self", + "entity": { + "flags": { + "is_sprinting": true + } + } + }, + "controlify:is_spectator": { + "slot": "controlify:self", + "entity": { + "game_mode": "spectator" + } + }, + "controlify:is_creative": { + "slot": "controlify:self", + "entity": { + "game_mode": "creative" + } + }, + "controlify:is_survival": { + "slot": "controlify:self", + "entity": { + "game_mode": "survival" + } + }, + "controlify:looking_at_entity": { + "slot": "controlify:hit_result", + "entity": {} + }, + "controlify:looking_at_block": { + "slot": "controlify:hit_result", + "block": {} + }, + "controlify:looking_at_air": { + "none_of": [ + "controlify:looking_at_entity", + "controlify:looking_at_block" + ] + }, + "controlify:looking_at_villager": { + "slot": "controlify:hit_result", + "entity": { + "type": "#minecraft:villager" + } + }, + "controlify:has_item_in_main_hand": { + "slot": "controlify:main_hand", + "item": { + "count": { + "min": 1 + } + } + }, + "controlify:has_item_in_offhand": { + "slot": "controlify:off_hand", + "item": { + "count": { + "min": 1 + } + } + }, + "controlify:has_item_in_either_hand": { + "any_of": [ + "controlify:has_item_in_main_hand", + "controlify:has_item_in_offhand" + ] + }, + "controlify:has_multiple_items_in_main_hand": { + "slot": "controlify:main_hand", + "item": { + "count": { + "min": 2 + } + } + } +} From 6aa7a408806a560630e8cf0442663491ac072eda Mon Sep 17 00:00:00 2001 From: isxander Date: Fri, 21 Aug 2026 16:01:46 +0100 Subject: [PATCH 3/7] complete the contextual api --- docs/.vitepress/config.ts | 3 +- .../developers/adaptive-trigger-api.md | 46 - docs/content/developers/contextual-api.md | 119 ++ docs/content/developers/guide-api.md | 119 -- .../adaptive-trigger-effects.md | 303 ++--- docs/content/resource-packs/guides.md | 305 +++-- .../gametest/tests/ButtonGuideTests.java | 131 ++ src/gametest/resources/fabric.mod.json | 3 +- .../contextual/trigger_effect/in_game.json | 19 + .../controlify/trigger_effect/swing_item.json | 13 - .../contextual/trigger_effect/in_game.json | 18 + .../controlify/trigger_effect/swing_item.json | 12 - .../contextual/trigger_effect/in_game.json | 19 + .../controlify/trigger_effect/swing_item.json | 13 - .../dev/isxander/controlify/Controlify.java | 35 +- .../api/contextual/InGameContext.java | 21 + .../api/triggereffect/TriggerEffectApi.java | 45 - .../contextual/ContextualDebug.java | 236 ++++ .../contextual/ContextualDomainImpl.java | 145 +- .../contextual/ContextualDomains.java | 11 + .../contextual/ContextualPredicate.java | 5 + .../ContextualStateContributors.java | 18 +- .../contextual/FactDependencyGraph.java | 16 +- .../controlify/contextual/RuleEngine.java | 38 +- .../controlify/contextual/RuleSetManager.java | 134 +- .../dualsense/TriggerEffectInstanceImpl.java | 15 +- .../dualsense/TriggerEffectManager.java | 30 +- .../controlify/debug/DebugProperties.java | 3 + .../gui/guide/GuideInstanceImpl.java | 44 +- .../gui/guide/InGameButtonGuide.java | 12 +- .../AbstractContainerScreenProcessor.java | 8 +- .../predicates/ClientEntityPredicate.java | 18 +- .../predicates/ClientPredicateUtils.java | 9 +- .../predicates/EntityEquipmentPredicate.java | 5 +- .../controlify/contextual/facts/in_game.json | 308 ++++- .../guide}/container.json | 45 +- .../controlify/contextual/guide/in_game.json | 655 +++++++++ .../contextual/trigger_effect/in_game.json | 553 ++++++++ .../assets/controlify/guides/in_game.json | 200 --- .../assets/controlify/lang/en_us.json | 36 +- .../controlify/trigger_effect/swing_item.json | 15 - .../controlify/trigger_effect/use_item.json | 200 --- .../controlify/contextual/guide/in_game.json | 1194 +++++++++++++++++ 43 files changed, 3970 insertions(+), 1207 deletions(-) delete mode 100644 docs/content/developers/adaptive-trigger-api.md create mode 100644 docs/content/developers/contextual-api.md delete mode 100644 docs/content/developers/guide-api.md create mode 100644 src/gametest/java/dev/isxander/controlify/gametest/tests/ButtonGuideTests.java create mode 100644 src/gametest/resources/resourcepacks/adaptive_trigger_high_priority/assets/controlify/contextual/trigger_effect/in_game.json delete mode 100644 src/gametest/resources/resourcepacks/adaptive_trigger_high_priority/assets/controlify/trigger_effect/swing_item.json create mode 100644 src/gametest/resources/resourcepacks/adaptive_trigger_low_priority/assets/controlify/contextual/trigger_effect/in_game.json delete mode 100644 src/gametest/resources/resourcepacks/adaptive_trigger_low_priority/assets/controlify/trigger_effect/swing_item.json create mode 100644 src/gametest/resources/resourcepacks/adaptive_trigger_server_tag/assets/controlify/contextual/trigger_effect/in_game.json delete mode 100644 src/gametest/resources/resourcepacks/adaptive_trigger_server_tag/assets/controlify/trigger_effect/swing_item.json delete mode 100644 src/main/java/dev/isxander/controlify/api/triggereffect/TriggerEffectApi.java create mode 100644 src/main/java/dev/isxander/controlify/contextual/ContextualDebug.java rename src/main/resources/assets/controlify/{guides => contextual/guide}/container.json (57%) create mode 100644 src/main/resources/assets/controlify/contextual/guide/in_game.json create mode 100644 src/main/resources/assets/controlify/contextual/trigger_effect/in_game.json delete mode 100644 src/main/resources/assets/controlify/guides/in_game.json delete mode 100644 src/main/resources/assets/controlify/trigger_effect/swing_item.json delete mode 100644 src/main/resources/assets/controlify/trigger_effect/use_item.json create mode 100644 src/main/resources/resourcepacks/legacy_console/assets/controlify/contextual/guide/in_game.json diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 73b74fd59..72f996d05 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -97,8 +97,7 @@ export default defineConfig({ { text: 'Controlify Entrypoint', link: '/developers/controlify-entrypoint' }, { text: 'Bindings API', link: '/developers/bindings-api' }, { text: 'Screen Operation API', link: '/developers/screen-operation-api' }, - { text: 'Guides API', link: '/developers/guide-api' }, - { text: 'Adaptive Trigger API', link: '/developers/adaptive-trigger-api' } + { text: 'Contextual API', link: '/developers/contextual-api' } ] }, { diff --git a/docs/content/developers/adaptive-trigger-api.md b/docs/content/developers/adaptive-trigger-api.md deleted file mode 100644 index 122d6b39d..000000000 --- a/docs/content/developers/adaptive-trigger-api.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -title: Adaptive Trigger API ---- - -# Adaptive Trigger API - -Resource packs should be used for adaptive trigger effects based on item identities, tags, or serialized -data-component values. The Java API is available when an effect needs custom code or cannot be expressed -with resource-pack matching. - -Register effects during Controlify pre-initialization: - -```java -public final class MyControlifyEntrypoint implements ControlifyEntrypoint { - @Override - public void onControlifyPreInit(PreInitContext context) { - TriggerEffectApi.registerUseItemEffect( - DataComponents.CHARGED_PROJECTILES, - projectiles -> new DualsenseTriggerEffect.Feedback( - (byte) 3, - (byte) Math.min(8, projectiles.items().size() + 1) - ) - ); - } -} -``` - -`TriggerEffectApi` has use-item and swing-item overloads for: - -- A `DataComponentType` and a `Function`, for effects computed from - the component value. -- A `DataComponentType` and a constant `DualsenseTriggerEffect`. -- An `Item` and a constant `DualsenseTriggerEffect`. - -Use `registerUseItemEffect` for the input bound to **Use Item** and `registerSwingItemEffect` for the input -bound to **Attack**. - -## Priority - -[Resource-pack rules](../resource-packs/adaptive-trigger-effects) are always evaluated before Java -registrations, allowing users and pack authors to override mod-provided effects. Among Java registrations, -component functions are checked in registration order before the exact-item fallback. Registering the same -component or item again replaces its previous effect. - -Use a resource-pack `off` rule when an effect should be explicitly disabled. An `off` result returned from -Java also disables the trigger, but is only reached when no resource rule matched. diff --git a/docs/content/developers/contextual-api.md b/docs/content/developers/contextual-api.md new file mode 100644 index 000000000..b295bcf43 --- /dev/null +++ b/docs/content/developers/contextual-api.md @@ -0,0 +1,119 @@ +--- +title: Contextual API +--- + +# Contextual API + +Controlify's contextual system powers button guides and adaptive trigger effects. A contextual domain combines: + +- a context object supplied at runtime; +- contributors which expose source facts, items, blocks, and entities; +- data-defined facts derived from those values; +- guide and trigger-effect rule sets evaluated against the resulting state. + +For the resource formats, see [Button Guides](../resource-packs/guides) and +[Adaptive Trigger Effects](../resource-packs/adaptive-trigger-effects). + +## Accessing built-in domains + +Domains are available during Controlify pre-initialization: + +```java +private ContextualDomain inGameDomain; + +@Override +public void onControlifyPreInit(PreInitContext context) { + this.inGameDomain = context.contextualDomains().inGame(); +} +``` + +Controlify provides `inGame()` and `container()` domains. + +## Contributing state + +A contributor writes values into named slots. Resource-pack facts and rules can then test those slots with +Minecraft's standard predicates. + +```java +@Override +public void onControlifyPreInit(PreInitContext context) { + context.contextualDomains().inGame().registerContributor((inGame, sink) -> { + sink.contributeFact( + Identifier.fromNamespaceAndPath("example", "holding_wand"), + inGame.player().getMainHandItem().is(EXAMPLE_WAND) + ); + + sink.contributeItem( + Identifier.fromNamespaceAndPath("example", "focus_item"), + inGame.player().getOffhandItem() + ); + }); +} +``` + +`ContextualStateSink` can contribute booleans with `contributeFact`, or values with `contributeItem`, +`contributeBlock`, and `contributeEntity`. Contributors run in registration order; a later contribution to the +same identifier replaces the earlier value. + +## Creating a domain + +Custom domains use a context type implementing `Context`: + +```java +public record SpellContext( + ControllerEntity controller, + GuideVerbosity verbosity, + ItemStack selectedSpell +) implements Context {} +``` + +Register the domain and its contributor during pre-initialization: + +```java +private ContextualDomain spellDomain; + +@Override +public void onControlifyPreInit(PreInitContext context) { + Identifier id = Identifier.fromNamespaceAndPath("example", "spells"); + this.spellDomain = context.contextualDomains().register(id, (spell, sink) -> { + sink.contributeItem( + Identifier.fromNamespaceAndPath("example", "selected_spell"), + spell.selectedSpell() + ); + }); +} +``` + +Controlify automatically registers the domain's fact resource loader. Rules for the domain use the same domain +identifier in their asset path. + +## Using a guide instance + +Create the instance once, update it when contextual state may have changed, and render it independently: + +```java +GuideInstance guide = spellDomain.createGuideInstance(minecraft.font); + +void tick(SpellContext context) { + guide.update(context); +} +``` + +Use `extractRenderState` for HUD-style rendering, or `renderable` when adding the guide to a screen. + +## Using a trigger-effect instance + +Trigger-effect instances are evaluated in the same way: + +```java +TriggerEffectInstance effects = spellDomain.createTriggerEffectInstance(); + +void tick(SpellContext context) { + effects.update(context); + DualsenseTriggerEffect left = effects.getLeftTriggerEffect(); + DualsenseTriggerEffect right = effects.getRightTriggerEffect(); +} +``` + +The trigger-effect API is experimental. A matching rule only applies when its `for` binding is currently bound to +the corresponding controller trigger. diff --git a/docs/content/developers/guide-api.md b/docs/content/developers/guide-api.md deleted file mode 100644 index f7d1a2751..000000000 --- a/docs/content/developers/guide-api.md +++ /dev/null @@ -1,119 +0,0 @@ ---- -title: Guide API ---- - -# Guide API - -_Learn how to use existing Guide domains, and register your own, for use in your own Screens._ - -::: info -For instructions on modifying rules for existing Guide domains, -see the [resource pack page](../resource-packs/guides) -::: - -## How does this API work? - -The Guide API has a few distinct concepts: - -- **Guide Domain**: Holds a list of valid facts in the domain, - for example, `controlify:in_game` is a domain which holds facts such as `controlify:on_ground`. -- **Guide Context**: Holds data which facts use to determine if they are true or false. -- **Guide Instance**: Applies to a specific domain, holds current state of the facts applied to a context and the rules which pass. - -## Registering facts to a `GuideDomain` - -You may want to add additional facts relating to your mod to an existing `GuideDomain`, such as `controlify:in_game`, -which you would then use in an embedded resource pack to add additional rules. - -You may want to create and render your own `GuideDomain`, and you need to register your own set of facts. - -```java -@Override -public void onControlifyPreInit(PreInitContext ctx) { - ctx.guides().inGame().registerFact( - Identifier.fromNamespaceAndPath("my_mod", "holding_wand"), - ctx -> ctx.player().isItemInMainHandModdedWand() - ); -} -``` - -Now resource packs can use your additional rule. Refer to the [resource pack page](../resource-packs/guides) -to create rules using resource packs. - -If your mod is not loaded or does not register a referenced fact for whatever reason, the state of the fact -will be `false`, and a warning will be issued on resource reload that an unknown fact was referenced. - -## Rendering a Guide - -To render a Guide, you must create a new `GuideInstance`, update its facts, and then finally render it. - -Because the only time you have API-safe access to existing Guide Domains is during pre-init, -you must capture a reference to it to use it later. - -```java -private GuideDomain inGameGuideDomain; - -@Override -public void onControlifyPreInit(PreInitContext ctx) { - this.inGameGuideDomain = ctx.guides().inGame(); -} -``` - -Once you have a reference to a domain, you can create an instance from it. - -```java -GuideInstance guideInstance = guideDomain.createInstance(); -``` - -Ruleset and fact resolution is completely independent of rendering, you may update the guide once every tick, -while you render it every frame. - -Every fact in a `GuideDomain` requires a specific context type (`FactCtx`), you must construct this type to be -able to update an instance. - -```java -void tick() { - guideInstance.update(new InGameCtx(...), font); -} -``` - -Then, you can render it. You can either use the `extractRenderState` method, or you can get a `Renderable` -which you can then add to a `Screen`. - -```java -class MyScreen extends Screen { - @Override - protected void init() { - boolean bottomAligned = true; // if false, top aligned - boolean textContrast = false; // adds a translucent background behind text - this.addRenderableOnly(this.guideInstance.renderable(bottomAligned, textContrast)); - } -} -``` - -## Registering your own domain - -It is very simple to register your own domain. -Once registered, Controlify handles resource reloading for you. - -Before registering a new domain, you need to decide whether it will use an existing fact context -(either `InGameContext` or `ContainerContext`), or your own. This is the data you give to the guide instance -each update, which the facts use to resolve their state. Here is an example fact context. - -```java -public record ModdedCtx( - ModClient modClient -) implements FactCtx {} -``` - -Each fact you register would then be able to reference `modClient` and therefore the rest of your mod's state. - -```java -@Override -public void onControlifyPreInit(PreInitContext ctx) { - GuideDomain domain = ctx.guides().registerCustomDomain(Identifier.fromNamespaceAndPath("my_mod", "mod_context")); - domain.registerFact(...); -} -``` - - diff --git a/docs/content/resource-packs/adaptive-trigger-effects.md b/docs/content/resource-packs/adaptive-trigger-effects.md index b2da28bb4..704faa85e 100644 --- a/docs/content/resource-packs/adaptive-trigger-effects.md +++ b/docs/content/resource-packs/adaptive-trigger-effects.md @@ -4,52 +4,80 @@ title: Adaptive Trigger Effects # Adaptive Trigger Effects -Controlify's DualSense adaptive trigger effects can be changed with a resource pack. Effects are applied -to the controller input bound to either **Use Item** or **Attack**. +Adaptive trigger effects are contextual rules. Each rule associates a Controlify binding with a contextual predicate +and a DualSense trigger effect. -## Creating trigger effect rules +## Trigger-effect files -Create either or both of these files in your resource pack: +A domain's trigger rules are loaded from: -- `assets/controlify/trigger_effect/use_item.json` -- `assets/controlify/trigger_effect/swing_item.json` +```text +assets//contextual/trigger_effect/.json +``` -Each file contains an array of rules. Every rule has a `when` item predicate and the `effect` to apply. -The `when` object uses Minecraft's standard -[item predicate format](https://minecraft.wiki/w/Template:Nbt_inherit/conditions/item/template). When -multiple conditions are present, all of them must match. +The built-in in-game domain uses: -On Fabric, you can reference the `c:` convention tags even when connected to a vanilla server. -NeoForge does not provide this capability, but when on Singleplayer or connected to a NeoForge server, -you are able to reference convention tags. +```text +assets/controlify/contextual/trigger_effect/in_game.json +``` -For example, a bow can increase resistance as it is pulled: +A rule set contains an optional `replace` flag and an ordered `rules` array: ```json { - "when": { - "items": "minecraft:bow" - }, - "effect": { - "type": "feedback_slope", - "start_position": 3, - "end_position": 9, - "start_strength": 2, - "end_strength": 8 - } + "replace": false, + "rules": [ + { + "for": "controlify:use", + "if": { + "slot": "controlify:active_item", + "item": { + "items": "minecraft:bow" + } + }, + "then": { + "type": "feedback_slope", + "start_position": 3, + "end_position": 9, + "start_strength": 2, + "end_strength": 8 + } + } + ] } ``` -Component presence can give every weapon the same trigger stop: +Each rule has: + +- `for`: the Controlify binding whose physical trigger receives the effect; +- `if`: a [contextual predicate](./guides#contextual-predicates); +- `then`: the trigger effect. + +The binding is not restricted to Use Item or Attack. A rule is considered when its binding is currently bound to the +left or right trigger. + +## Matching in-game items + +The in-game domain provides these item slots: + +- `controlify:active_item`: the item currently being used; +- `controlify:main_hand`; +- `controlify:off_hand`. + +For an Attack effect, test the main hand: ```json { - "when": { - "predicates": { - "minecraft:weapon": {} + "for": "controlify:attack", + "if": { + "slot": "controlify:main_hand", + "item": { + "predicates": { + "minecraft:weapon": {} + } } }, - "effect": { + "then": { "type": "weapon", "start_position": 3, "end_position": 5, @@ -58,193 +86,99 @@ Component presence can give every weapon the same trigger stop: } ``` -Item tags and component conditions can be combined: - -```json -{ - "when": { - "items": "#minecraft:fox_food", - "predicates": { - "minecraft:consumable": {} - } - }, - "effect": { - "type": "feedback", - "position": 3, - "strength": 1 - } -} -``` - -Stack counts can select a vibration effect: - -```json -{ - "when": { - "items": "minecraft:snowball", - "count": { - "min": 8 - } - }, - "effect": { - "type": "vibration", - "position": 3, - "amplitude": 4, - "frequency": 30 - } -} -``` +To retain Controlify's Use Item fallback behavior, put all active-item rules first and then repeat them for the +off-hand slot. This makes an active-item match win before any off-hand match. -Semantic component predicates can select a multi-zone effect: +Item predicates use Minecraft's standard item predicate format and can match item IDs, tags, exact component values, +component predicates, and stack counts: ```json { - "when": { - "items": "minecraft:shield", - "predicates": { - "minecraft:damage": { - "damage": { - "min": 1 + "for": "controlify:use", + "if": { + "slot": "controlify:off_hand", + "item": { + "items": "#example:heavy_tools", + "predicates": { + "minecraft:custom_data": { + "example": { + "trigger_effect": "heavy" + } } } } }, - "effect": { + "then": { "type": "feedback_multiple_position", - "strength": [0, 0, 2, 2, 3, 4, 5, 6, 7, 8] + "strength": [0, 0, 2, 3, 4, 5, 6, 7, 8, 8] } } ``` -Exact component values can distinguish an unloaded crossbow: +Contextual predicates can also combine facts, items, blocks, and entities with `all_of`, `none_of`, and `any_of`. -```json -{ - "when": { - "items": "minecraft:crossbow", - "components": { - "minecraft:charged_projectiles": [] - } - }, - "effect": { - "type": "feedback_slope", - "start_position": 2, - "end_position": 9, - "start_strength": 5, - "end_strength": 8 - } -} -``` - -Use-item rules check the player's active item and then their offhand item. Swing-item rules check the -main-hand item. - -## Rule priority and stacking - -The files are additive across resource packs. Rules in the highest-priority pack are checked first, followed -by lower-priority packs. Within each file, rules are checked from top to bottom. The first matching rule wins. +## Rule precedence and replacement -An empty `when` object matches every item and can be used as a final catch-all rule. +Higher-priority resource packs are evaluated before lower-priority packs. Rules within a file are evaluated in array +order. The first matching rule for a binding wins. -To remove an effect supplied by Controlify or a lower-priority pack, add a higher-priority rule with an -`off` effect: - -```json -[ - { - "when": { - "items": "minecraft:bow" - }, - "effect": { - "type": "off" - } - } -] -``` - -A matching `off` rule stops evaluation, so rules from lower-priority packs and effects registered by mods -will not be used. - -If a file has invalid JSON, an unknown item, tag, component, or predicate, an invalid component or predicate -value, or an invalid effect, Controlify logs the error and skips that entire file layer. Other resource packs -continue to work. - -## Using trigger effects from a server - -A server can include trigger effect files in the resource pack it sends to players. The server itself does -not need Controlify: clients with Controlify apply the rules, while other clients ignore the additional -assets. This is useful for giving a server's custom items their own trigger effects. - -### Matching an item tag - -For example, a server datapack can define -`data/example/tags/item/heavy_weapons.json`: +Set `replace` to `true` to discard every lower-priority layer: ```json { - "values": [ - "minecraft:mace", - "minecraft:netherite_axe" - ] + "replace": true, + "rules": [] } ``` -The server resource pack can then contain -`assets/controlify/trigger_effect/swing_item.json`: +Use an `off` effect when a condition should explicitly disable a lower rule for the same binding: ```json -[ - { - "when": { - "items": "#example:heavy_weapons" - }, - "effect": { - "type": "weapon", - "start_position": 2, - "end_position": 8, - "strength": 6 +{ + "for": "controlify:use", + "if": { + "slot": "controlify:active_item", + "item": { + "items": "minecraft:bow" } + }, + "then": { + "type": "off" } -] +} ``` -The item tag is synchronized to clients when they join and whenever the server reloads its datapacks, so the -resource-pack rule follows changes made with `/reload`. - -### Matching custom item data +An invalid file layer is logged and skipped without aborting the resource reload. -Items which share a vanilla item type can instead be distinguished by their `minecraft:custom_data` -component. For example, a server can create a custom heavy weapon with: - -```mcfunction -/give @s minecraft:carrot_on_a_stick[minecraft:custom_data={example:{trigger_effect:"heavy"}}] -``` +## Server-provided registries and tags -Its server resource pack can match that data with the `minecraft:custom_data` component predicate: +Trigger and fact resources are decoded against the client's static registries, then remapped against the current +world's registry access. This allows a server resource pack to reference tags supplied by the server's datapack: ```json -[ - { - "when": { - "items": "minecraft:carrot_on_a_stick", - "predicates": { - "minecraft:custom_data": { - "example": { - "trigger_effect": "heavy" - } +{ + "rules": [ + { + "for": "controlify:attack", + "if": { + "slot": "controlify:main_hand", + "item": { + "items": "#example:heavy_weapons" } + }, + "then": { + "type": "weapon", + "start_position": 2, + "end_position": 8, + "strength": 6 } - }, - "effect": { - "type": "feedback_multiple_position", - "strength": [0, 0, 2, 3, 4, 5, 6, 7, 8, 8] } - } -] + ] +} ``` -The custom-data predicate is a partial match. The item may contain other custom data in addition to the -fields in the rule. +Resolved rules are invalidated whenever client tags update, so datapack `/reload` changes are reflected without a +resource-pack reload. ## Effect formats @@ -261,14 +195,15 @@ values use `0` for off and `8` for maximum. | `feedback_slope` | `start_position`: 0–8; `end_position`: greater than the start and at most 9; `start_strength` and `end_strength`: 1–8. | | `vibration_multiple_position` | `frequency`: positive signed-byte frequency in hertz; `amplitude`: exactly 10 values, each 0–8. | -For example, independent resistance in each trigger zone is written as: +For example: ```json { - "type": "feedback_multiple_position", - "strength": [0, 0, 2, 2, 3, 4, 5, 6, 7, 8] + "type": "vibration_multiple_position", + "frequency": 20, + "amplitude": [0, 0, 2, 2, 3, 4, 5, 6, 7, 8] } ``` -An amplitude or strength of zero disables that zone. Effects whose entire amplitude/strength is zero, or -whose vibration frequency is not positive, behave as `off`. +Effects whose entire amplitude or strength is zero, and vibration effects whose frequency is not positive, behave as +`off`. diff --git a/docs/content/resource-packs/guides.md b/docs/content/resource-packs/guides.md index 5002e3d12..36a3eba03 100644 --- a/docs/content/resource-packs/guides.md +++ b/docs/content/resource-packs/guides.md @@ -1,173 +1,194 @@ --- -title: Guides +title: Button Guides --- -# Guides +# Button Guides -## What are guides? +Button guides show contextual binding hints on the HUD and container screens. The contextual system separates +reusable facts from ordered guide rules. -Guides are a feature of Controlify that shows hints to the player about what buttons to press to perform certain actions. +## Guide rule files -Since version `2.3.0`, guides are data-driven, meaning you can override the default guides with your own. +A domain's guide rules are loaded from: -

+```text +assets//contextual/guide/.json +``` -![The in-game button guide](/images/in-game-guide.webp) +For Controlify's built-in domains, these are: -![The container button guide](/images/container-guide.webp) +- `assets/controlify/contextual/guide/in_game.json` +- `assets/controlify/contextual/guide/container.json` -
+A rule set contains an optional `replace` flag and an ordered `rules` array: -## Concepts +```json +{ + "replace": false, + "rules": [ + { + "for": "controlify:jump", + "where": "left", + "if": "controlify:on_ground", + "then": { "translate": "key.jump" } + } + ] +} +``` -There are three main concepts you need to understand to create your own guides. +Each guide rule has: -1. **Fact** - - A fact is a piece of information about the current game state. - - For example, the fact `controlify:on_ground` is true when the player is on the ground. -2. **Rule** - - A rule contains a list of permitting facts, and a list of forbidding facts. - - If all permitting facts are true and no forbidding facts are true, the rule is applied if its binding is bound. - - For example, a rule for `controlify:jump` requires the fact `controlify:on_ground`, then show some text and the button glyph for the jump binding. -3. **Domain** - - A domain contains all loaded facts and rules for a specific type of guide. - - `controlify:in_game` is a domain that has in-game specific facts, and rules to show when the player is in-game. - - `controlify:container` is a domain that has facts and rules for when the player is in a container GUI, like the inventory or a chest. - - For example, you wouldn't have a rule for `controlify:on_ground` in the `controlify:container` domain, since it's irrelevant in a container GUI. +- `for`: the Controlify binding to display; +- `where`: `left` or `right`; +- `if`: a contextual predicate; +- `then`: a literal string or Minecraft text component. -## Creating custom rules +Use `true` for an unconditional rule. -Controlify only allows resource packs to add and override _rules_, not _facts_ or _domains_. +## Contextual predicates -If Controlify does not have a _fact_ for your specific use case, you must create a mod that adds the _fact_ to Controlify, -or submit a feature request to get it added to Controlify itself. +A fact identifier directly tests a boolean fact: +```json +"if": "controlify:on_ground" +``` -::: code-group -```json [assets/controlify/guides/in_game.json] -{ - "override": false, - "rules": [ - { - "for": "controlify:jump", - "where": "left", - "when": ["controlify:on_ground"], - "forbid": [], - "then": "Jump" - } - ] +Predicates can be combined. `all_of` requires every child, `none_of` rejects any matching child, and `any_of` +requires at least one matching child: + +```json +"if": { + "all_of": [ + "controlify:in_water", + "controlify:input_moving" + ], + "none_of": [ + "controlify:sprinting", + "controlify:in_vehicle" + ] } ``` -::: -This example shows a _rule_ that applies when the player is on the ground, and the `controlify:jump` binding is bound. -When the _rule_ applies, it shows the text "Jump" and the glyph for the `controlify:jump` binding. +Rules can also test named item, block, or entity slots with Minecraft's predicate formats: + +```json +"if": { + "slot": "controlify:main_hand", + "item": { + "items": "#minecraft:swords" + } +} +``` -As well as literal text such as `"Jump"`, Controlify supports the Minecraft -[text component format](https://minecraft.wiki/w/Text_component_format) which allows you to use translations and styling. -For example, you can use `"then": {"translate": "mypack.jump"}` to use a translation key sourced from your pack's language files. +```json +"if": { + "slot": "controlify:hit_result", + "entity": { + "type": "#minecraft:villager" + } +} +``` -A rule can be displayed either on the `left` or `right` side of the screen, this is defined by the `"where"` field. +Replace `item` with `block` or `entity` to select the corresponding predicate type. A missing slot does not +match. -Because `"override": false`, this resource pack will not override the default rules, but instead add an additional rule. -If you want to override the default rules, or any resource pack below yours, set `"override": true`. +## Rule precedence -### Stacking rules +Higher-priority resource packs are evaluated before lower-priority packs. Within a file, rules are evaluated in +array order. For each binding and guide location, the first matching rule wins. -You can stack multiple rules for the same binding, the first rule that succeeds will be applied, the rest will be ignored. -However, if the rules have different locations (e.g. `left` and `right`), they will both be applied. +Set `replace` to `true` to discard all guide-rule layers below that file: -::: code-group -```json [assets/controlify/guides/in_game.json] +```json { - "override": false, - "rules": [ - { - "for": "controlify:jump", - "where": "left", - "when": ["controlify:in_water"], - "forbid": [], - "then": "Swim Up" - }, - { - "for": "controlify:jump", - "where": "left", - "when": ["controlify:on_ground"], - "forbid": [], - "then": "Jump" - } + "replace": true, + "rules": [] +} +``` + +An invalid file layer is logged and skipped without aborting the resource reload. + +## Defining facts + +Data-defined facts for a domain use: + +```text +assets//contextual/facts/.json +``` + +The file is a map from fact identifiers to contextual predicates: + +```json +{ + "example:holding_sword": { + "slot": "controlify:main_hand", + "item": { + "items": "#minecraft:swords" + } + }, + "example:ready_to_attack": { + "all_of": [ + "example:holding_sword", + "controlify:looking_at_entity" ] + } } ``` -::: - -In this snippet, the first rule will apply when the player is in water, -and the second rule will apply when the player is on the ground. - -Even when the player is in water and touching the ground, only the first rule will apply, -because it is the first rule that matches its conditions. - -## Facts - -Controlify has a set of built-in facts that you can use in your rules. - -Below is a list of the built-in facts for each domain. - -### Common facts - -These facts are available in all domains. - -| ID | Description | -|----------------------------------------|-----------------------------------------------------------| -| `controlify:verbosity_full` | When the guide verbosity level is set to full. | -| `controlify:verbosity_reduced_or_more` | When the guide verbosity level is either full or reduced. | -| `controlify:verbosity_reduced_or_less` | When the guide verbosity is set to reduced or less. | -| `controlify:verbosity_minimal` | When the guide verbosity is set to minimal. | - - -### `controlify:in_game` - -| ID | Description | -|-----------------------------------------|-------------------------------------------------------------------------------------------------------------------------------| -| `controlify:on_ground` | When the player is on the ground. | -| `controlify:in_vehicle` | When the player is in a vehicle. | -| `controlify:riding_saddled_horse` | When the currently ridden vehicle is a horse with a saddle. | -| `controlify:riding_happy_ghast` | When the currently ridden vehicle is a Happy Ghast. | -| `controlify:flying` | When the player is currently in creative flight. | -| `controlify:elytra_flying` | When the player is currently gliding with an elytra. | -| `controlify:can_elytra_fly` | When the player is in a state where pressing jump will cause the elytra to deploy. | -| `controlify:in_liquid` | When the player is touching liquid, such as water or lava. | -| `controlify:in_water` | When the player is touching water. | -| `controlify:under_water` | When the player has their eyes underwater. | -| `controlify:in_lava` | When the player is touching lava. | -| `controlify:sneaking` | When the player is attempting to sneak (pressing the sneak key, or it is toggled on). | -| `controlify:is_toggle_sneak` | When the player is using toggle sneak (does not mean it is currently toggled on). | -| `controlify:is_toggle_sprint` | When the player is using toggle sprint (does not mean it is currently toggled on). | -| `controlify:sprinting` | When the player is attempting to sprint (pressing the sprint key, or it is toggled on). | -| `controlify:input_moving` | When the player is applying movement input—even if the player is not physically moving, if they're trying to, this fact goes. | -| `controlify:is_spectator` | When the player is in spectator mode. | -| `controlify:is_creative` | When the player is in creative mode. | -| `controlify:has_hearts` | When the player is not invulnerable. | -| `controlify:is_adventure` | When the player is in adventure mode. | -| `controlify:is_survival` | When the player is in survival mode. | -| `controlify:looking_at_entity` | When the player is currently looking at an entity and is in range to interact with it. | -| `controlify:looking_at_block` | When the player is currently looking at a block and is in range to interact or destroy it. | -| `controlify:looking_at_air` | When the player is neither looking at a block nor looking at an entity. | -| `controlify:has_item_in_either_hand` | When the player has an item in their main hand or their offhand. | -| `controlify:has_item_in_mainhand` | When the player has an item in their main hand. | -| `controlify:has_item_in_offhand` | When the player has an item in their offhand. | -| `controlify:has_multiple_items_in_hand` | When the player is holding an item stack with a count greater than one. | - -### `controlify:container` - -| ID | Description | -|---------------------------------------|----------------------------------------------------------------------------------------------------------------------------| -| `controlify:hovering_slot` | When the user is hovering their cursor over a slot. | -| `controlify:hovering_item` | When the user is hovering their cursor over an occupied slot. | -| `controlify:hovering_many_items` | When the user is hovering their cursor over an occupied slot which has more than one item in it. | -| `controlify:holding_item` | When the user has grabbed an item and is moving it around with their cursor. | -| `controlify:holding_many_items` | When the user has grabbed an item and is moving it around with their cursor, and that item has more than one in the stack. | -| `controlify:can_place_held_item` | When the container allows the player to place down their held item into the currently hovered slot. | -| `controlify:cursor_outside_container` | When the user is hovering their cursor outside the container interface. | -| `controlify:hovering_item_is_bundle` | When the user is hovering their cursor over a slot which is occupied with an item tagged as a bundle. | -| `controlify:selected_bundle_slot` | When the user is currently selecting an item from within the bundle they're hovering. | + +Facts may reference other data-defined facts. Controlify orders their evaluation automatically and rejects cycles. +Mods can expose additional source facts and slots through the [Contextual API](../developers/contextual-api). + +## Built-in domain state + +Every domain exposes these source facts: + +- `controlify:verbosity_full` +- `controlify:verbosity_reduced` +- `controlify:verbosity_minimal` + +The `controlify:in_game` domain exposes these slots: + +- entity `controlify:self`; +- entity or block `controlify:hit_result`; +- items `controlify:main_hand`, `controlify:off_hand`, and `controlify:active_item`. + +It also provides source or data-defined facts including: + +- `controlify:on_ground`, `controlify:in_vehicle`, `controlify:flying`, `controlify:creative_flying`, + `controlify:elytra_flying`; +- `controlify:riding_saddled_horse`, `controlify:riding_saddled_equine`, + `controlify:riding_saddled_camel`, `controlify:riding_saddled_nautilus`, and + `controlify:riding_happy_ghast`; +- `controlify:can_elytra_fly`, `controlify:in_liquid`, `controlify:in_water`, `controlify:under_water`; +- `controlify:sneaking`, `controlify:sprinting`, `controlify:input_moving`, `controlify:on_climbable`; +- `controlify:is_toggle_sneak`, `controlify:is_toggle_sprint`; +- `controlify:is_spectator`, `controlify:is_creative`, `controlify:is_survival`; +- `controlify:looking_at_entity`, `controlify:looking_at_block`, `controlify:looking_at_air`; +- `controlify:looking_at_villager`, `controlify:looking_at_merchant`, + `controlify:looking_at_rideable`, `controlify:looking_at_chest_boat`, and + `controlify:looking_at_container_minecart`; +- `controlify:looking_at_closed_openable`, `controlify:looking_at_open_openable`, + `controlify:looking_at_container`, `controlify:looking_at_button`, `controlify:looking_at_lever`, + `controlify:looking_at_bell`, and `controlify:looking_at_bed`; +- workstation target facts named `controlify:looking_at_crafting_table`, `controlify:looking_at_furnace`, + `controlify:looking_at_smoker`, `controlify:looking_at_brewing_stand`, + `controlify:looking_at_enchanting_table`, `controlify:looking_at_smithing_table`, + `controlify:looking_at_repair_station`, `controlify:looking_at_stonecutter`, + `controlify:looking_at_loom`, and `controlify:looking_at_cartography_table`; +- `controlify:has_item_in_main_hand`, `controlify:has_item_in_offhand`; +- `controlify:has_item_in_either_hand`, `controlify:has_multiple_items_in_main_hand`, + `controlify:main_hand_is_tool`; +- `controlify:main_hand_is_block_item`, `controlify:off_hand_is_block_item`, and + `controlify:fishing_hook_cast`. + +The built-in in-game guide uses the verbosity facts as tiers. Minimal includes movement and contextual attack/use +actions, reduced adds sprint, inventory, drop, and swap-hands hints, and full additionally includes radial-menu, +drop-stack, and pick-block hints. + +The `controlify:container` domain exposes item slots `controlify:hovering_slot`, `controlify:holding_item`, and +`controlify:bundle_selected`. Its built-in facts include: + +- `controlify:hovering_slot`, `controlify:hovering_item`, `controlify:hovering_many_items`; +- `controlify:holding_item`, `controlify:holding_many_items`; +- `controlify:hovering_item_is_bundle`, `controlify:selected_bundle_slot`; +- `controlify:can_place_held_item`, `controlify:can_pickup_slot`, `controlify:cursor_outside_container`. diff --git a/src/gametest/java/dev/isxander/controlify/gametest/tests/ButtonGuideTests.java b/src/gametest/java/dev/isxander/controlify/gametest/tests/ButtonGuideTests.java new file mode 100644 index 000000000..188fc51d3 --- /dev/null +++ b/src/gametest/java/dev/isxander/controlify/gametest/tests/ButtonGuideTests.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ +package dev.isxander.controlify.gametest.tests; + +import dev.isxander.controlify.api.contextual.InGameContext; +import dev.isxander.controlify.api.contextual.GuideLocation; +import dev.isxander.controlify.contextual.*; +import dev.isxander.controlify.gametest.framework.CEntityTypes; +import dev.isxander.controlify.gametest.framework.CTestUtil; +import dev.isxander.controlify.gametest.framework.TestUnitWorldContext; +import dev.isxander.controlify.gametest.framework.controller.ControlifyGameTestContext; +import dev.isxander.controlify.gui.guide.GuideInstanceImpl; +import dev.isxander.controlify.utils.CUtil; +import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest; +import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext; +import net.minecraft.commands.arguments.EntityAnchorArgument; +import net.minecraft.core.BlockPos; +import net.minecraft.world.phys.EntityHitResult; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@SuppressWarnings({"UnstableApiUsage", "unchecked"}) +public class ButtonGuideTests implements FabricClientGameTest { + @Override + public void runTest(ClientGameTestContext context) { + var controlify = new ControlifyGameTestContext(context); + + try (var controller = controlify.virtualControllerBuilder().withXbox().attach(); + var world = new TestUnitWorldContext(context); + var region = world.allocateRegion(5, 5, 5)) { + var villager = region.spawnWithNoFreeWill(CEntityTypes.VILLAGER, new BlockPos(1, 1, 3)); + context.waitTicks(2); + + var villagerPosition = region.computeOnServer((_, _) -> villager.getBoundingBox().getCenter()); + context.runOnClient(minecraft -> minecraft.player.lookAt(EntityAnchorArgument.Anchor.EYES, villagerPosition)); + context.waitTicks(2); + + context.runOnClient(minecraft -> { + if (!(minecraft.hitResult instanceof EntityHitResult)) { + throw new AssertionError("Expected the villager to be the current entity hit result, got " + minecraft.hitResult); + } + + var domain = (ContextualDomainImpl) ContextualDomains.INSTANCE.inGame(); + var ruleEngine = RuleSetManager.GUIDE_RULES + .getRuleEngine(minecraft.level, CUtil.rl("in_game")) + .orElseThrow(); + assertRuleLocation(ruleEngine, "inventory", GuideLocation.LEFT); + assertRuleLocation(ruleEngine, "radial_menu", GuideLocation.LEFT); + var inGameContext = InGameContext.create(minecraft, controller.getControllerEntity()); + var state = domain.calculateState(inGameContext, ruleEngine.factDependencies()); + assertFact(state, "looking_at_entity"); + assertFact(state, "looking_at_merchant"); + + var matchingRules = ruleEngine.evaluate(state); + assertBindingRule(matchingRules, "attack"); + assertBindingRule(matchingRules, "use"); + + var guide = (GuideInstanceImpl) domain.createGuideInstance(minecraft.font); + guide.update(inGameContext); + if (guide.rightGuides().lines().stream().noneMatch(line -> line.text().getString().contains("Trade"))) { + throw new AssertionError("Expected the rendered right guide to contain Trade: " + guide.rightGuides().lines()); + } + }); + + try (var _ = CTestUtil.applyResourcePack(context, CUtil.rl("legacy_console"))) { + context.runOnClient(minecraft -> { + var ruleEngine = RuleSetManager.GUIDE_RULES + .getRuleEngine(minecraft.level, CUtil.rl("in_game")) + .orElseThrow(); + + Set expectedBindings = Set.of("jump", "sneak", "drop", "inventory", "attack", "use"); + Set actualBindings = ruleEngine.rules().stream() + .map(rule -> rule.binding().bindId()) + .map(id -> id.getPath()) + .collect(Collectors.toSet()); + if (!actualBindings.equals(expectedBindings)) { + throw new AssertionError("Expected the Legacy Console guide rules to replace the defaults. Bindings: " + actualBindings); + } + + assertRuleLocation(ruleEngine, "jump", GuideLocation.LEFT); + assertRuleLocation(ruleEngine, "sneak", GuideLocation.LEFT); + assertRuleLocation(ruleEngine, "drop", GuideLocation.LEFT); + assertRuleLocation(ruleEngine, "inventory", GuideLocation.RIGHT); + assertRuleLocation(ruleEngine, "attack", GuideLocation.RIGHT); + assertRuleLocation(ruleEngine, "use", GuideLocation.RIGHT); + }); + } + } + + controlify.resetSettings(); + } + + private static void assertFact(ContextualState state, String path) { + var id = CUtil.rl(path); + if (!state.facts().getOrDefault(id, false)) { + throw new AssertionError("Expected contextual fact '" + id + "' to be true. Facts: " + state.facts()); + } + } + + private static void assertBindingRule( + List rules, + String bindingPath + ) { + var bindingId = CUtil.rl(bindingPath); + if (rules.stream().noneMatch(rule -> rule.binding().bindId().equals(bindingId))) { + throw new AssertionError("Expected a matching guide rule for '" + bindingId + "': " + rules); + } + } + + private static void assertRuleLocation( + RuleEngine ruleEngine, + String bindingPath, + GuideLocation expectedLocation + ) { + var bindingId = CUtil.rl(bindingPath); + List rules = ruleEngine.rules().stream() + .filter(rule -> rule.binding().bindId().equals(bindingId)) + .toList(); + if (rules.isEmpty() || rules.stream().anyMatch(rule -> rule.location() != expectedLocation)) { + throw new AssertionError( + "Expected all guide rules for '" + bindingId + "' to be on " + expectedLocation + ": " + rules + ); + } + } +} diff --git a/src/gametest/resources/fabric.mod.json b/src/gametest/resources/fabric.mod.json index 58bea3f9c..a6af3326e 100644 --- a/src/gametest/resources/fabric.mod.json +++ b/src/gametest/resources/fabric.mod.json @@ -18,7 +18,8 @@ "dev.isxander.controlify.gametest.tests.VirtualControllerInputTests", "dev.isxander.controlify.gametest.tests.KeyboardLayoutTests", "dev.isxander.controlify.gametest.tests.RumbleTests", - "dev.isxander.controlify.gametest.tests.AdaptiveTriggerTests" + "dev.isxander.controlify.gametest.tests.AdaptiveTriggerTests", + "dev.isxander.controlify.gametest.tests.ButtonGuideTests" ], "client": [ "dev.isxander.controlify.gametest.tests.KeyboardLayoutTests", diff --git a/src/gametest/resources/resourcepacks/adaptive_trigger_high_priority/assets/controlify/contextual/trigger_effect/in_game.json b/src/gametest/resources/resourcepacks/adaptive_trigger_high_priority/assets/controlify/contextual/trigger_effect/in_game.json new file mode 100644 index 000000000..24338631f --- /dev/null +++ b/src/gametest/resources/resourcepacks/adaptive_trigger_high_priority/assets/controlify/contextual/trigger_effect/in_game.json @@ -0,0 +1,19 @@ +{ + "rules": [ + { + "for": "controlify:attack", + "if": { + "slot": "controlify:main_hand", + "item": { + "items": "minecraft:diamond_sword" + } + }, + "then": { + "type": "vibration", + "position": 1, + "amplitude": 4, + "frequency": 11 + } + } + ] +} diff --git a/src/gametest/resources/resourcepacks/adaptive_trigger_high_priority/assets/controlify/trigger_effect/swing_item.json b/src/gametest/resources/resourcepacks/adaptive_trigger_high_priority/assets/controlify/trigger_effect/swing_item.json deleted file mode 100644 index 18c2e4619..000000000 --- a/src/gametest/resources/resourcepacks/adaptive_trigger_high_priority/assets/controlify/trigger_effect/swing_item.json +++ /dev/null @@ -1,13 +0,0 @@ -[ - { - "when": { - "items": "minecraft:diamond_sword" - }, - "effect": { - "type": "vibration", - "position": 1, - "amplitude": 4, - "frequency": 11 - } - } -] diff --git a/src/gametest/resources/resourcepacks/adaptive_trigger_low_priority/assets/controlify/contextual/trigger_effect/in_game.json b/src/gametest/resources/resourcepacks/adaptive_trigger_low_priority/assets/controlify/contextual/trigger_effect/in_game.json new file mode 100644 index 000000000..24a66359e --- /dev/null +++ b/src/gametest/resources/resourcepacks/adaptive_trigger_low_priority/assets/controlify/contextual/trigger_effect/in_game.json @@ -0,0 +1,18 @@ +{ + "rules": [ + { + "for": "controlify:attack", + "if": { + "slot": "controlify:main_hand", + "item": { + "items": "minecraft:diamond_sword" + } + }, + "then": { + "type": "feedback", + "position": 2, + "strength": 2 + } + } + ] +} diff --git a/src/gametest/resources/resourcepacks/adaptive_trigger_low_priority/assets/controlify/trigger_effect/swing_item.json b/src/gametest/resources/resourcepacks/adaptive_trigger_low_priority/assets/controlify/trigger_effect/swing_item.json deleted file mode 100644 index e42601ffd..000000000 --- a/src/gametest/resources/resourcepacks/adaptive_trigger_low_priority/assets/controlify/trigger_effect/swing_item.json +++ /dev/null @@ -1,12 +0,0 @@ -[ - { - "when": { - "items": "minecraft:diamond_sword" - }, - "effect": { - "type": "feedback", - "position": 2, - "strength": 2 - } - } -] diff --git a/src/gametest/resources/resourcepacks/adaptive_trigger_server_tag/assets/controlify/contextual/trigger_effect/in_game.json b/src/gametest/resources/resourcepacks/adaptive_trigger_server_tag/assets/controlify/contextual/trigger_effect/in_game.json new file mode 100644 index 000000000..4a7126983 --- /dev/null +++ b/src/gametest/resources/resourcepacks/adaptive_trigger_server_tag/assets/controlify/contextual/trigger_effect/in_game.json @@ -0,0 +1,19 @@ +{ + "rules": [ + { + "for": "controlify:attack", + "if": { + "slot": "controlify:main_hand", + "item": { + "items": "#controlify_test:adaptive_trigger_test" + } + }, + "then": { + "type": "weapon", + "start_position": 2, + "end_position": 8, + "strength": 6 + } + } + ] +} diff --git a/src/gametest/resources/resourcepacks/adaptive_trigger_server_tag/assets/controlify/trigger_effect/swing_item.json b/src/gametest/resources/resourcepacks/adaptive_trigger_server_tag/assets/controlify/trigger_effect/swing_item.json deleted file mode 100644 index 3d35e43fa..000000000 --- a/src/gametest/resources/resourcepacks/adaptive_trigger_server_tag/assets/controlify/trigger_effect/swing_item.json +++ /dev/null @@ -1,13 +0,0 @@ -[ - { - "when": { - "items": "#controlify_test:adaptive_trigger_test" - }, - "effect": { - "type": "weapon", - "start_position": 2, - "end_position": 8, - "strength": 6 - } - } -] diff --git a/src/main/java/dev/isxander/controlify/Controlify.java b/src/main/java/dev/isxander/controlify/Controlify.java index 89802c5c1..089567902 100644 --- a/src/main/java/dev/isxander/controlify/Controlify.java +++ b/src/main/java/dev/isxander/controlify/Controlify.java @@ -9,12 +9,9 @@ import com.mojang.blaze3d.Blaze3D; import dev.isxander.controlify.api.ControlifyApi; import dev.isxander.controlify.api.bind.ControlifyBindApi; -import dev.isxander.controlify.api.contextual.ContainerContext; import dev.isxander.controlify.api.contextual.ContextualDomainRegistry; -import dev.isxander.controlify.api.contextual.InGameContext; import dev.isxander.controlify.api.entrypoint.InitContext; import dev.isxander.controlify.api.entrypoint.PreInitContext; -import dev.isxander.controlify.api.guide.*; import dev.isxander.controlify.bindings.BindContext; import dev.isxander.controlify.bindings.ControlifyBindApiImpl; import dev.isxander.controlify.bindings.ControlifyBindings; @@ -26,9 +23,9 @@ import dev.isxander.controlify.config.settings.device.DeviceSettings; import dev.isxander.controlify.config.settings.profile.ProfileSettings; import dev.isxander.controlify.contextual.ContextualDomains; +import dev.isxander.controlify.contextual.RuleSetManager; import dev.isxander.controlify.controller.*; import dev.isxander.controlify.controller.dualsense.TriggerEffectManager; -import dev.isxander.controlify.controller.dualsense.TriggerEffectRegistry; import dev.isxander.controlify.controller.id.ControllerTypeManager; import dev.isxander.controlify.controller.input.ControllerState; import dev.isxander.controlify.controller.input.ControllerStateView; @@ -40,8 +37,6 @@ import dev.isxander.controlify.driver.steamdeck.SteamDeckMode; import dev.isxander.controlify.driver.steamdeck.SteamDeckUtil; import dev.isxander.controlify.font.InputFontMapper; -import dev.isxander.controlify.gui.guide.GuideDomainImpl; -import dev.isxander.controlify.gui.guide.GuideDomains; import dev.isxander.controlify.gui.screen.*; import dev.isxander.controlify.debug.DebugProperties; import dev.isxander.controlify.ingame.ControllerPlayerMovement; @@ -68,7 +63,6 @@ import net.minecraft.client.gui.screens.PauseScreen; import net.minecraft.client.multiplayer.ServerData; import net.minecraft.network.chat.Component; -import net.minecraft.resources.Identifier; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.lwjgl.glfw.GLFW; @@ -100,7 +94,6 @@ public class Controlify implements ControlifyApi { private DefaultConfigManager defaultConfigManager; private ControllerTypeManager controllerTypeManager; private KeyboardLayoutManager keyboardLayoutManager; - private TriggerEffectRegistry triggerEffectRegistry; private TriggerEffectManager triggerEffectManager; @@ -147,16 +140,16 @@ public void preInitialiseControlify() { this.defaultConfigManager = new DefaultConfigManager(); this.controllerTypeManager = new ControllerTypeManager(); this.keyboardLayoutManager = new KeyboardLayoutManager(); - this.triggerEffectRegistry = new TriggerEffectRegistry(); + PlatformClientUtil.registerAssetReloadListener(inputFontMapper); PlatformClientUtil.registerAssetReloadListener(defaultBindManager); PlatformClientUtil.registerAssetReloadListener(defaultConfigManager); PlatformClientUtil.registerAssetReloadListener(controllerTypeManager); PlatformClientUtil.registerAssetReloadListener(keyboardLayoutManager); - PlatformClientUtil.registerAssetReloadListener(triggerEffectRegistry); PlatformClientUtil.registerAssetReloadListener(RadialIconManager.INSTANCE); - PlatformClientUtil.registerAssetReloadListener(GuideDomains.IN_GAME); - PlatformClientUtil.registerAssetReloadListener(GuideDomains.CONTAINER); + PlatformClientUtil.registerAssetReloadListener(RuleSetManager.GUIDE_RULES); + PlatformClientUtil.registerAssetReloadListener(RuleSetManager.TRIGGER_EFFECT_RULES); + ContextualDomains.touch(); registerBuiltinPack("legacy_console"); @@ -192,11 +185,15 @@ public void preInitialiseControlify() { policy.set(ServerPolicy.fromBoolean(packet.allowed())); }); - PlatformClientUtil.registerClientDisconnected((client) -> { + PlatformClientUtil.registerClientDisconnected(_ -> { DebugLog.log("Disconnected from server, resetting server policies"); ServerPolicies.unsetAll(); }); - PlatformClientUtil.registerClientTagsUpdated(client -> triggerEffectRegistry.invalidateResolvedResourceRules()); + PlatformClientUtil.registerClientTagsUpdated(_ -> { + ContextualDomains.INSTANCE.invalidateResolvedFactGraphs(); + RuleSetManager.GUIDE_RULES.invalidateResolvedRuleEngines(); + RuleSetManager.TRIGGER_EFFECT_RULES.invalidateResolvedRuleEngines(); + }); PlatformClientUtil.addHudLayer(CUtil.rl("button_guide"), (graphics, deltaTracker) -> inGameButtonGuide().ifPresent(guide -> guide.extractRenderState(graphics, deltaTracker.getGameTimeDeltaPartialTick(false)))); @@ -218,7 +215,6 @@ public ContextualDomainRegistry contextualDomains() { CUtil.LOGGER.error("Failed to run `onControlifyPreInit` on Controlify entrypoint: {}", entrypoint.getClass().getName(), e); } }); - GuideDomains.freeze(); } private void registerBuiltinPack(String id) { @@ -243,7 +239,7 @@ public void initializeControlify() { this.inGameInputHandler = null; // set require the current controller changes this.virtualMouseHandler = new VirtualMouseHandler(); - this.triggerEffectManager = new TriggerEffectManager(this.minecraft, this.triggerEffectRegistry); + this.triggerEffectManager = new TriggerEffectManager(); ControlifyEvents.CONTROLLER_CONNECTED.register(event -> this.onControllerAdded( event.controller(), event.hotplugged())); @@ -518,7 +514,8 @@ private void tickActiveController(ControllerEntity controller, boolean outOfFocu } boolean controllerInputSuppressed = outOfFocus || currentInputMode() == InputMode.KEYBOARD_MOUSE; - triggerEffectManager.applyTriggerEffects(controller, controllerInputSuppressed); + this.triggerEffectManager().tick(this.minecraft, controller); + this.triggerEffectManager().applyTriggerEffects(controller, controllerInputSuppressed); if (state.isGivingInput()) { minecraft.getFramerateLimitTracker().onInputReceived(); @@ -777,10 +774,6 @@ public TriggerEffectManager triggerEffectManager() { return triggerEffectManager; } - public TriggerEffectRegistry triggerEffectRegistry() { - return triggerEffectRegistry; - } - public Set thisTickBindContexts() { return this.thisTickContexts; } diff --git a/src/main/java/dev/isxander/controlify/api/contextual/InGameContext.java b/src/main/java/dev/isxander/controlify/api/contextual/InGameContext.java index 2b0830e37..bfcf9a0d4 100644 --- a/src/main/java/dev/isxander/controlify/api/contextual/InGameContext.java +++ b/src/main/java/dev/isxander/controlify/api/contextual/InGameContext.java @@ -7,10 +7,14 @@ package dev.isxander.controlify.api.contextual; import dev.isxander.controlify.controller.ControllerEntity; +import dev.isxander.controlify.mixins.feature.guide.ingame.MinecraftAccessor; import net.minecraft.client.Minecraft; import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.client.player.LocalPlayer; import net.minecraft.world.phys.HitResult; +import org.jetbrains.annotations.ApiStatus; + +import java.util.Objects; public record InGameContext( Minecraft client, @@ -20,4 +24,21 @@ public record InGameContext( ControllerEntity controller, GuideVerbosity verbosity ) implements Context { + @ApiStatus.Internal + public static InGameContext create(Minecraft minecraft, ControllerEntity controller) { + var hitResult = minecraft.hitResult; + if (hitResult == null) { + ((MinecraftAccessor) minecraft).controlify$invokePick(1f); + hitResult = minecraft.hitResult; + } + + return new InGameContext( + minecraft, + Objects.requireNonNull(minecraft.player, "player"), + Objects.requireNonNull(minecraft.level, "level"), + hitResult, + controller, + controller.settings().generic.guide.verbosity + ); + } } diff --git a/src/main/java/dev/isxander/controlify/api/triggereffect/TriggerEffectApi.java b/src/main/java/dev/isxander/controlify/api/triggereffect/TriggerEffectApi.java deleted file mode 100644 index 1eef74794..000000000 --- a/src/main/java/dev/isxander/controlify/api/triggereffect/TriggerEffectApi.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 2026 isXander - * This file is part of Controlify. - * - * SPDX-License-Identifier: LGPL-3.0-or-later - */ -package dev.isxander.controlify.api.triggereffect; - -import dev.isxander.controlify.Controlify; -import dev.isxander.controlify.driver.dualsense.DualsenseTriggerEffect; -import net.minecraft.core.component.DataComponentType; -import net.minecraft.world.item.Item; -import org.jetbrains.annotations.NotNull; - -import java.util.function.Function; - -public final class TriggerEffectApi { - - private TriggerEffectApi() { - } - - public static void registerUseItemEffect(DataComponentType componentType, Function effectFunction) { - Controlify.instance().triggerEffectRegistry().registerUseItemComponentEffect(componentType, effectFunction); - } - - public static void registerUseItemEffect(DataComponentType componentType, @NotNull DualsenseTriggerEffect effect) { - registerUseItemEffect(componentType, _ -> effect); - } - - public static void registerSwingItemEffect(DataComponentType componentType, Function effectFunction) { - Controlify.instance().triggerEffectRegistry().registerSwingItemComponentEffect(componentType, effectFunction); - } - - public static void registerSwingItemEffect(DataComponentType componentType, @NotNull DualsenseTriggerEffect effect) { - registerSwingItemEffect(componentType, _ -> effect); - } - - public static void registerUseItemEffect(Item item, @NotNull DualsenseTriggerEffect effect) { - Controlify.instance().triggerEffectRegistry().registerUseItemEffect(item, effect); - } - - public static void registerSwingItemEffect(Item item, @NotNull DualsenseTriggerEffect effect) { - Controlify.instance().triggerEffectRegistry().registerSwingItemEffect(item, effect); - } -} diff --git a/src/main/java/dev/isxander/controlify/contextual/ContextualDebug.java b/src/main/java/dev/isxander/controlify/contextual/ContextualDebug.java new file mode 100644 index 000000000..8f80d80c0 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/contextual/ContextualDebug.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ +package dev.isxander.controlify.contextual; + +import dev.isxander.controlify.debug.DebugProperties; +import dev.isxander.controlify.utils.CUtil; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.item.ItemInstance; +import net.minecraft.world.level.block.state.pattern.BlockInWorld; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.Comparator; +import java.util.Map; +import java.util.Set; +import java.util.StringJoiner; + +final class ContextualDebug { + private static final Comparator IDENTIFIER_ORDER = Comparator.comparing(Identifier::toString); + + private ContextualDebug() { + } + + static boolean enabled() { + return DebugProperties.DEBUG_CONTEXTUAL_RULES; + } + + static void logStateStart( + Identifier domain, + ContextualState state, + @Nullable Collection requiredFacts + ) { + if (!enabled()) { + return; + } + + CUtil.LOGGER.log( + "[Contextual/{}] Resolving state\n required facts: {}\n source facts: {}\n item slots: {}\n block slots: {}\n entity slots: {}", + domain, + describeIdentifiers(requiredFacts), + describeMap(state.facts(), String::valueOf), + describeMap(state.items(), ContextualDebug::describeItem), + describeMap(state.blocks(), ContextualDebug::describeBlock), + describeMap(state.entities(), ContextualDebug::describeEntity) + ); + } + + static void logStateComplete(Identifier domain, ContextualState state) { + if (!enabled()) { + return; + } + + CUtil.LOGGER.log( + "[Contextual/{}] Resolved state\n facts: {}", + domain, + describeMap(state.facts(), String::valueOf) + ); + } + + static PredicateEvaluation evaluatePredicate(ContextualPredicate predicate, ContextualState state) { + var trace = new StringBuilder(); + boolean result = evaluatePredicate(predicate, state, trace, 0); + return new PredicateEvaluation(result, trace.toString()); + } + + static void logFact( + Identifier domain, + Identifier fact, + Set dependencies, + PredicateEvaluation evaluation + ) { + if (!enabled()) { + return; + } + + CUtil.LOGGER.log( + "[Contextual/{}] Fact '{}' = {}\n dependencies: {}\n{}", + domain, + fact, + evaluation.result(), + describeIdentifiers(dependencies), + evaluation.trace() + ); + } + + static void logRule( + @Nullable String context, + int index, + Object key, + ContextualPredicate predicate, + ContextualState state, + String outcome, + @Nullable PredicateEvaluation evaluation + ) { + if (!enabled() || context == null) { + return; + } + + String trace = evaluation == null ? " predicate was not evaluated" : evaluation.trace(); + CUtil.LOGGER.log( + "[Contextual/{}] Rule #{} key={} -> {}\n contributing facts: {}\n predicate: {}\n{}", + context, + index, + key, + outcome, + describeFactValues(predicate.factDependencies(), state), + predicate, + trace + ); + } + + static void logRuleResult(@Nullable String context, Collection matches) { + if (enabled() && context != null) { + CUtil.LOGGER.log("[Contextual/{}] Winning rules: {}", context, matches); + } + } + + private static boolean evaluatePredicate( + ContextualPredicate predicate, + ContextualState state, + StringBuilder trace, + int depth + ) { + int start = trace.length(); + String indent = " ".repeat(depth + 1); + boolean result; + + if (predicate instanceof ContextualPredicate.Compound compound) { + boolean allOf = evaluateGroup("all_of", compound.allOf().orElse(Set.of()), state, trace, depth + 1, Group.ALL); + boolean noneOf = evaluateGroup("none_of", compound.noneOf().orElse(Set.of()), state, trace, depth + 1, Group.NONE); + boolean anyOf = compound.anyOf().isEmpty() + || evaluateGroup("any_of", compound.anyOf().get(), state, trace, depth + 1, Group.ANY); + result = allOf && noneOf && anyOf; + trace.insert(start, indent + "compound -> " + result + '\n'); + return result; + } + + result = predicate.matches(state); + String detail = switch (predicate) { + case ContextualPredicate.Fact fact -> "fact " + fact.fact() + "=" + state.facts().getOrDefault(fact.fact(), false); + case ContextualPredicate.Item item -> "item slot " + item.slot() + "=" + describeItem(state.items().get(item.slot())) + + ", predicate=" + item.item(); + case ContextualPredicate.Block block -> "block slot " + block.slot() + "=" + describeBlock(state.blocks().get(block.slot())) + + ", predicate=" + block.block(); + case ContextualPredicate.Entity entity -> "entity slot " + entity.slot() + "=" + describeEntity(state.entities().get(entity.slot())) + + ", predicate=" + entity.entity(); + case ContextualPredicate.Static staticPredicate -> "static " + staticPredicate.value(); + case ContextualPredicate.Compound _ -> throw new AssertionError(); + }; + trace.append(indent).append(detail).append(" -> ").append(result).append('\n'); + return result; + } + + private static boolean evaluateGroup( + String name, + Set predicates, + ContextualState state, + StringBuilder trace, + int depth, + Group group + ) { + if (predicates.isEmpty()) { + return true; + } + + String indent = " ".repeat(depth + 1); + trace.append(indent).append(name).append(':').append('\n'); + boolean any = false; + boolean all = true; + for (ContextualPredicate child : predicates) { + boolean childResult = evaluatePredicate(child, state, trace, depth + 1); + any |= childResult; + all &= childResult; + } + + return switch (group) { + case ALL -> all; + case NONE -> !any; + case ANY -> any; + }; + } + + private static String describeFactValues(Set facts, ContextualState state) { + var values = new StringJoiner(", ", "{", "}"); + facts.stream().sorted(IDENTIFIER_ORDER).forEach(id -> + values.add(id + "=" + state.facts().getOrDefault(id, false)) + ); + return values.toString(); + } + + private static String describeIdentifiers(@Nullable Collection identifiers) { + if (identifiers == null) { + return ""; + } + + var result = new StringJoiner(", ", "[", "]"); + identifiers.stream().sorted(IDENTIFIER_ORDER).forEach(id -> result.add(id.toString())); + return result.toString(); + } + + private static String describeMap(Map map, java.util.function.Function describer) { + var result = new StringJoiner(", ", "{", "}"); + map.entrySet().stream() + .sorted(Map.Entry.comparingByKey(IDENTIFIER_ORDER)) + .forEach(entry -> result.add(entry.getKey() + "=" + describer.apply(entry.getValue()))); + return result.toString(); + } + + private static String describeItem(@Nullable ItemInstance item) { + return item == null ? "" : item.toString(); + } + + private static String describeBlock(@Nullable BlockInWorld block) { + return block == null ? "" : block.getState() + " at " + block.getPos(); + } + + private static String describeEntity(@Nullable Entity entity) { + return entity == null + ? "" + : entity.getType() + "#" + entity.getId() + " ('" + entity.getDisplayName().getString() + "') at " + entity.position(); + } + + record PredicateEvaluation(boolean result, String trace) { + } + + private enum Group { + ALL, + NONE, + ANY + } +} diff --git a/src/main/java/dev/isxander/controlify/contextual/ContextualDomainImpl.java b/src/main/java/dev/isxander/controlify/contextual/ContextualDomainImpl.java index 78dd8818b..045575ca7 100644 --- a/src/main/java/dev/isxander/controlify/contextual/ContextualDomainImpl.java +++ b/src/main/java/dev/isxander/controlify/contextual/ContextualDomainImpl.java @@ -14,15 +14,23 @@ import dev.isxander.controlify.controller.dualsense.TriggerEffectInstanceImpl; import dev.isxander.controlify.gui.guide.GuideInstanceImpl; import dev.isxander.controlify.platform.client.resource.SimpleControlifyReloadListener; +import dev.isxander.controlify.utils.CUtil; +import net.minecraft.client.Minecraft; import net.minecraft.client.gui.Font; +import net.minecraft.client.multiplayer.CacheSlot; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.multiplayer.ClientRegistryLayer; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.RegistryAccess; import net.minecraft.resources.FileToIdConverter; import net.minecraft.resources.Identifier; import net.minecraft.server.packs.resources.Resource; import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.util.PlaceholderLookupProvider; +import net.minecraft.util.RegistryContextSwapper; import org.jetbrains.annotations.Nullable; import java.io.BufferedReader; -import java.io.IOException; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; @@ -32,8 +40,10 @@ public final class ContextualDomainImpl implements Contextual private static final FileToIdConverter converter = FileToIdConverter.json(DIRECTORY); private final Identifier id; + private final CacheSlot resolvedFactGraphs = new CacheSlot<>(this::resolveFactGraph); private ContextualStateContributor contributor = ContextualStateContributor.noop(); - private FactDependencyGraph factGraph = FactDependencyGraph.empty(); + private volatile Preparations preparations = Preparations.EMPTY; + private FactDependencyGraph factGraphWithoutLevel = FactDependencyGraph.empty(); public ContextualDomainImpl(Identifier id) { this.id = Objects.requireNonNull(id, "id"); @@ -55,54 +65,136 @@ public void registerContributor(ContextualStateContributor contributo @Override public GuideInstance createGuideInstance(Font font) { RuleSetManager ruleSetManager = RuleSetManager.GUIDE_RULES; - RuleEngine ruleEngine = ruleSetManager.getRuleEngine(this.id) - .orElseThrow(() -> new IllegalCallerException("This contextual domain has no guide rules associated with it.")); - return new GuideInstanceImpl<>(this, ruleEngine, font); + return new GuideInstanceImpl<>(this, () -> this.requireRuleEngine(ruleSetManager, "guide"), font); } @Override public TriggerEffectInstance createTriggerEffectInstance() { RuleSetManager ruleSetManager = RuleSetManager.TRIGGER_EFFECT_RULES; - RuleEngine ruleEngine = ruleSetManager.getRuleEngine(this.id) - .orElseThrow(() -> new IllegalCallerException("This contextual domain has no trigger effect rules associated with it.")); - return new TriggerEffectInstanceImpl<>(this, ruleEngine); + return new TriggerEffectInstanceImpl<>(this, () -> this.requireRuleEngine(ruleSetManager, "trigger effect")); + } + + private > RuleEngine requireRuleEngine(RuleSetManager ruleSetManager, String type) { + return ruleSetManager.getRuleEngine(Minecraft.getInstance().level, this.id) + .orElseThrow(() -> new IllegalCallerException("This contextual domain has no " + type + " rules associated with it.")); } @Override public CompletableFuture load(ResourceManager manager, Executor executor) { return CompletableFuture.supplyAsync(() -> { List resources = manager.getResourceStack(converter.idToFile(this.id)); + var lookup = new PlaceholderLookupProvider(RegistryAccess.EMPTY); + var ops = lookup.createSerializationContext(JsonOps.INSTANCE); var factsMap = new HashMap(); for (Resource resource : resources) { try (BufferedReader reader = resource.openAsReader()) { JsonElement json = JsonParser.parseReader(reader); - DataResult> result = FactDefinition.LIST_CODEC.parse(JsonOps.INSTANCE, json); - var facts = result.getOrThrow(); + DataResult> result = FactDefinition.LIST_CODEC.parse(ops, json); + var facts = result.getOrThrow(error -> new IllegalArgumentException("Failed to parse contextual facts: " + error)); for (FactDefinition fact : facts) { factsMap.remove(fact.id()); factsMap.put(fact.id(), fact); } - } catch (IOException e) { - throw new IllegalStateException("Failed to load resource", e); + } catch (Exception e) { + CUtil.LOGGER.error( + "Failed to load contextual facts for domain '{}' from pack '{}'; skipping this layer", + e, + this.id, + resource.sourcePackId() + ); } } - var facts = factsMap.values(); - var factGraph = FactDependencyGraph.compile(facts); - - return new Preparations(factGraph); + List facts = List.copyOf(factsMap.values()); + RegistryContextSwapper registrySwapper = lookup.hasRegisteredPlaceholders() + ? lookup.createSwapper() + : null; + Preparations preparations = new Preparations(facts, FactDependencyGraph.empty(), registrySwapper); + try { + FactDependencyGraph factGraph = resolveFactGraph( + preparations, + ClientRegistryLayer.createRegistryAccess().compositeAccess(), + false + ); + return new Preparations(facts, factGraph, registrySwapper); + } catch (Exception e) { + CUtil.LOGGER.error( + "Failed to compile contextual facts for domain '{}'; keeping the previous definitions", + e, + this.id + ); + return this.preparations; + } }, executor); } @Override public CompletableFuture apply(Preparations data, ResourceManager manager, Executor executor) { - return CompletableFuture.runAsync(() -> { - this.factGraph = data.facts(); - }, executor); + return CompletableFuture.runAsync(() -> this.apply(data), executor); + } + + private synchronized void apply(Preparations data) { + this.preparations = data; + this.factGraphWithoutLevel = data.factGraphWithoutLevel(); + this.resolvedFactGraphs.clear(); + } + + public synchronized void invalidateResolvedFactGraph() { + this.resolvedFactGraphs.clear(); + } + + private FactDependencyGraph resolveFactGraph(ClientLevel level) { + Preparations preparations = this.preparations; + try { + return resolveFactGraph(preparations, level.registryAccess(), true); + } catch (Exception e) { + CUtil.LOGGER.error( + "Failed to resolve contextual facts for domain '{}'; using client-static definitions", + e, + this.id + ); + return preparations.factGraphWithoutLevel(); + } + } + + private FactDependencyGraph resolveFactGraph( + Preparations preparations, + HolderLookup.Provider registries, + boolean logFailures + ) { + if (preparations.registrySwapper() == null) { + return FactDependencyGraph.compile(preparations.facts()); + } + + var resolvedFacts = new ArrayList(preparations.facts().size()); + for (FactDefinition fact : preparations.facts()) { + try { + ContextualPredicate predicate = preparations.registrySwapper() + .swapTo(ContextualPredicate.CODEC, fact.predicate(), registries) + .getOrThrow(); + resolvedFacts.add(new FactDefinition(fact.id(), predicate)); + } catch (Exception e) { + if (logFailures) { + CUtil.LOGGER.warn( + "Failed to resolve contextual fact '{}' for domain '{}'; leaving its registry references unresolved", + e, + fact.id(), + this.id + ); + } + resolvedFacts.add(fact); + } + } + + return FactDependencyGraph.compile(resolvedFacts); + } + + private synchronized FactDependencyGraph currentFactGraph(@Nullable ClientLevel level) { + return level == null ? this.factGraphWithoutLevel : this.resolvedFactGraphs.compute(level); } @Override @@ -115,12 +207,19 @@ public ContextualState calculateState(C context, @Nullable Collection facts, + FactDependencyGraph factGraphWithoutLevel, + @Nullable RegistryContextSwapper registrySwapper + ) { + private static final Preparations EMPTY = new Preparations(List.of(), FactDependencyGraph.empty(), null); + } } diff --git a/src/main/java/dev/isxander/controlify/contextual/ContextualDomains.java b/src/main/java/dev/isxander/controlify/contextual/ContextualDomains.java index 12ae69838..db90e1e1f 100644 --- a/src/main/java/dev/isxander/controlify/contextual/ContextualDomains.java +++ b/src/main/java/dev/isxander/controlify/contextual/ContextualDomains.java @@ -9,10 +9,14 @@ import dev.isxander.controlify.utils.CUtil; import net.minecraft.resources.Identifier; +import java.util.ArrayList; +import java.util.List; + public final class ContextualDomains implements ContextualDomainRegistry { public static final ContextualDomains INSTANCE = new ContextualDomains(); + private final List> domains = new ArrayList<>(); private final ContextualDomain inGame = register( CUtil.rl("in_game"), ContextualStateContributors.IN_GAME @@ -35,10 +39,17 @@ public ContextualDomain container() { @Override public ContextualDomain register(Identifier domainId) { var domain = new ContextualDomainImpl(domainId); + this.domains.add(domain); PlatformClientUtil.registerAssetReloadListener(domain); return domain; } + public void invalidateResolvedFactGraphs() { + this.domains.forEach(ContextualDomainImpl::invalidateResolvedFactGraph); + } + + public static void touch() {} + private ContextualDomains() { } } diff --git a/src/main/java/dev/isxander/controlify/contextual/ContextualPredicate.java b/src/main/java/dev/isxander/controlify/contextual/ContextualPredicate.java index 35e41c8ec..2b01924a0 100644 --- a/src/main/java/dev/isxander/controlify/contextual/ContextualPredicate.java +++ b/src/main/java/dev/isxander/controlify/contextual/ContextualPredicate.java @@ -5,8 +5,13 @@ import com.mojang.serialization.codecs.RecordCodecBuilder; import dev.isxander.controlify.utils.codec.CExtraCodecs; import dev.isxander.controlify.utils.predicates.ClientEntityPredicate; +//? if >=26.2 { import net.minecraft.advancements.predicates.BlockPredicate; import net.minecraft.advancements.predicates.ItemPredicate; +//?} else { +/*import net.minecraft.advancements.criterion.BlockPredicate; +import net.minecraft.advancements.criterion.ItemPredicate; +*///?} import net.minecraft.client.Minecraft; import net.minecraft.resources.Identifier; diff --git a/src/main/java/dev/isxander/controlify/contextual/ContextualStateContributors.java b/src/main/java/dev/isxander/controlify/contextual/ContextualStateContributors.java index c0ad2d162..5cc408785 100644 --- a/src/main/java/dev/isxander/controlify/contextual/ContextualStateContributors.java +++ b/src/main/java/dev/isxander/controlify/contextual/ContextualStateContributors.java @@ -5,6 +5,7 @@ import dev.isxander.controlify.utils.CUtil; import net.minecraft.resources.Identifier; import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.BlockItem; import net.minecraft.world.item.BundleItem; import net.minecraft.world.level.block.state.pattern.BlockInWorld; import net.minecraft.world.phys.BlockHitResult; @@ -18,6 +19,11 @@ public final class ContextualStateContributors { private static final Identifier SLOT_MAIN_HAND = CUtil.rl("main_hand"); private static final Identifier SLOT_OFF_HAND = CUtil.rl("off_hand"); private static final Identifier SLOT_ACTIVE_ITEM = CUtil.rl("active_item"); + private static final Identifier FACT_MAIN_HAND_IS_BLOCK_ITEM = CUtil.rl("main_hand_is_block_item"); + private static final Identifier FACT_OFF_HAND_IS_BLOCK_ITEM = CUtil.rl("off_hand_is_block_item"); + private static final Identifier FACT_FISHING_HOOK_CAST = CUtil.rl("fishing_hook_cast"); + private static final Identifier FACT_ON_CLIMBABLE = CUtil.rl("on_climbable"); + private static final Identifier SLOT_HOVERING_SLOT = CUtil.rl("hovering_slot"); private static final Identifier SLOT_HOLDING_ITEM = CUtil.rl("holding_item"); private static final Identifier SLOT_BUNDLE_SELECTED = CUtil.rl("bundle_selected"); @@ -59,6 +65,16 @@ public final class ContextualStateContributors { sink.contributeItem(SLOT_MAIN_HAND, context.player().getMainHandItem()); sink.contributeItem(SLOT_OFF_HAND, context.player().getOffhandItem()); sink.contributeItem(SLOT_ACTIVE_ITEM, context.player().getActiveItem()); + sink.contributeFact( + FACT_MAIN_HAND_IS_BLOCK_ITEM, + context.player().getMainHandItem().getItem() instanceof BlockItem + ); + sink.contributeFact( + FACT_OFF_HAND_IS_BLOCK_ITEM, + context.player().getOffhandItem().getItem() instanceof BlockItem + ); + sink.contributeFact(FACT_FISHING_HOOK_CAST, context.player().fishing != null); + sink.contributeFact(FACT_ON_CLIMBABLE, context.player().onClimbable()); sink.contributeFact( CUtil.rl("can_elytra_fly"), @@ -89,7 +105,7 @@ public final class ContextualStateContributors { ); sink.contributeFact( CUtil.rl("input_moving"), - context.player().input.getMoveVector().equals(Vec2.ZERO) + !context.player().input.getMoveVector().equals(Vec2.ZERO) ); sink.contributeFact( diff --git a/src/main/java/dev/isxander/controlify/contextual/FactDependencyGraph.java b/src/main/java/dev/isxander/controlify/contextual/FactDependencyGraph.java index bc6caa2f9..77fc7938d 100644 --- a/src/main/java/dev/isxander/controlify/contextual/FactDependencyGraph.java +++ b/src/main/java/dev/isxander/controlify/contextual/FactDependencyGraph.java @@ -116,11 +116,25 @@ public Set externalDependencies() { /// Resolves required data-defined facts on top of a source state. public ContextualState evaluate(ContextualStateAccumulator state, @Nullable Collection requiredFacts) { + return this.evaluate(state, requiredFacts, null); + } + + ContextualState evaluate( + ContextualStateAccumulator state, + @Nullable Collection requiredFacts, + @Nullable Identifier debugDomain + ) { ContextualState view = state.view(); for (Identifier id : this.evaluationOrder()) { if (requiredFacts == null || requiredFacts.contains(id)) { FactDefinition definition = this.definitions().get(id); - state.contributeFact(id, definition.predicate().matches(view)); + if (ContextualDebug.enabled() && debugDomain != null) { + ContextualDebug.PredicateEvaluation evaluation = ContextualDebug.evaluatePredicate(definition.predicate(), view); + state.contributeFact(id, evaluation.result()); + ContextualDebug.logFact(debugDomain, id, this.dependencies.get(id), evaluation); + } else { + state.contributeFact(id, definition.predicate().matches(view)); + } } } return state.snapshot(); diff --git a/src/main/java/dev/isxander/controlify/contextual/RuleEngine.java b/src/main/java/dev/isxander/controlify/contextual/RuleEngine.java index 08f2d64e5..3c7fcfc26 100644 --- a/src/main/java/dev/isxander/controlify/contextual/RuleEngine.java +++ b/src/main/java/dev/isxander/controlify/contextual/RuleEngine.java @@ -7,6 +7,7 @@ package dev.isxander.controlify.contextual; import net.minecraft.resources.Identifier; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.HashSet; @@ -51,23 +52,54 @@ public Set factDependencies() { /// Returns the winning rules in their original precedence order. public List evaluate(ContextualState state, Predicate shouldEvaluate) { + return this.evaluate(state, shouldEvaluate, null); + } + + public List evaluate(ContextualState state, Predicate shouldEvaluate, @Nullable String debugContext) { Objects.requireNonNull(state, "state"); List matches = new ArrayList<>(); Set consumedKeys = new HashSet<>(); - for (R rule : this.rules) { + for (int index = 0; index < this.rules.size(); index++) { + R rule = this.rules.get(index); if (!shouldEvaluate.test(rule)) { + ContextualDebug.logRule(debugContext, index, rule.key(), rule.predicate(), state, "FILTERED", null); continue; } K key = rule.key(); - if (!consumedKeys.contains(key) && rule.matches(state)) { + if (consumedKeys.contains(key)) { + ContextualDebug.logRule(debugContext, index, key, rule.predicate(), state, "SKIPPED (key already won)", null); + continue; + } + + boolean matchesRule; + ContextualDebug.PredicateEvaluation evaluation = null; + if (ContextualDebug.enabled() && debugContext != null) { + evaluation = ContextualDebug.evaluatePredicate(rule.predicate(), state); + matchesRule = evaluation.result(); + } else { + matchesRule = rule.matches(state); + } + + if (matchesRule) { consumedKeys.add(key); matches.add(rule); } + ContextualDebug.logRule( + debugContext, + index, + key, + rule.predicate(), + state, + matchesRule ? "MATCHED" : "DID NOT MATCH", + evaluation + ); } - return List.copyOf(matches); + List result = List.copyOf(matches); + ContextualDebug.logRuleResult(debugContext, result); + return result; } public List evaluate(ContextualState state) { diff --git a/src/main/java/dev/isxander/controlify/contextual/RuleSetManager.java b/src/main/java/dev/isxander/controlify/contextual/RuleSetManager.java index 99cb84ef8..ffd23303a 100644 --- a/src/main/java/dev/isxander/controlify/contextual/RuleSetManager.java +++ b/src/main/java/dev/isxander/controlify/contextual/RuleSetManager.java @@ -4,15 +4,23 @@ import com.google.gson.JsonParser; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; +import com.mojang.serialization.DynamicOps; import com.mojang.serialization.JsonOps; import dev.isxander.controlify.platform.client.resource.SimpleControlifyReloadListener; import dev.isxander.controlify.utils.CUtil; +import net.minecraft.client.multiplayer.CacheSlot; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.multiplayer.ClientRegistryLayer; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.RegistryAccess; import net.minecraft.resources.FileToIdConverter; import net.minecraft.resources.Identifier; import net.minecraft.server.packs.resources.Resource; import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.util.PlaceholderLookupProvider; +import net.minecraft.util.RegistryContextSwapper; +import org.jetbrains.annotations.Nullable; -import java.io.IOException; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; @@ -26,66 +34,147 @@ public class RuleSetManager> implements SimpleControlifyRel private static final String DIRECTORY = "contextual/"; private final Codec> ruleSetCodec; + private final Codec ruleCodec; private final String directory; private final FileToIdConverter converter; + private final CacheSlot>> resolvedRuleEngines = + new CacheSlot<>(this::resolveRuleEngines); - private volatile Map> ruleEngineByDomain; + private volatile Preparations preparations; + private Map> ruleEnginesWithoutLevel; public RuleSetManager(Codec ruleCodec, String directory) { this.ruleSetCodec = RuleSet.createCodec(ruleCodec); + this.ruleCodec = ruleCodec; this.directory = DIRECTORY + directory; this.converter = FileToIdConverter.json(this.directory); - this.ruleEngineByDomain = Map.of(); + this.preparations = new Preparations<>(Map.of(), null); + this.ruleEnginesWithoutLevel = Map.of(); } - public synchronized Optional> getRuleEngine(Identifier domainId) { - return Optional.ofNullable(this.ruleEngineByDomain.get(domainId)); + /// With no level, the engine uses the client-static registry context captured during reload. + /// References to remote registries remain placeholders until the rules are requested with a level. + public synchronized Optional> getRuleEngine(@Nullable ClientLevel level, Identifier domainId) { + Objects.requireNonNull(domainId, "domainId"); + + Map> ruleEngines = level == null + ? this.ruleEnginesWithoutLevel + : this.resolvedRuleEngines.compute(level); + return Optional.ofNullable(ruleEngines.get(domainId)); } @Override public CompletableFuture> load(ResourceManager manager, Executor executor) { return CompletableFuture.supplyAsync(() -> { Map> ruleSetResourcesByDomain = converter.listMatchingResourceStacks(manager); + var lookup = new PlaceholderLookupProvider(RegistryAccess.EMPTY); + DynamicOps ops = lookup.createSerializationContext(JsonOps.INSTANCE); - var ruleEngineByDomain = new HashMap>(); + var rulesByDomain = new HashMap>(); for (var entry : ruleSetResourcesByDomain.entrySet()) { - List rules = parseRuleSets(entry.getValue()); - var ruleEngine = new RuleEngine<>(rules); - ruleEngineByDomain.put(entry.getKey(), ruleEngine); + Identifier domainId = converter.fileToId(entry.getKey()); + List rules = parseRuleSets(domainId, entry.getValue(), ops); + rulesByDomain.put(domainId, rules); } - return new Preparations<>(ruleEngineByDomain); + RegistryContextSwapper registrySwapper = lookup.hasRegisteredPlaceholders() + ? lookup.createSwapper() + : null; + return new Preparations<>(Map.copyOf(rulesByDomain), registrySwapper); }, executor); } // highest priority resource pack's first rule should be the head of the resultant list. - private List parseRuleSets(List ruleSetResources) { + private List parseRuleSets(Identifier domainId, List ruleSetResources, DynamicOps ops) { var rules = new ArrayList(); - for (Resource ruleSetResource : ruleSetResources) { + for (var ruleSetResource : ruleSetResources.reversed()) { try (var reader = ruleSetResource.openAsReader()) { JsonElement json = JsonParser.parseReader(reader); - DataResult> ruleSetResult = this.ruleSetCodec.parse(JsonOps.INSTANCE, json); + DataResult> ruleSetResult = this.ruleSetCodec.parse(ops, json); RuleSet ruleSet = ruleSetResult.getOrThrow(); + rules.addAll(ruleSet.rules()); + + // if this rule set is a replace, then anything below it can be ignored if (ruleSet.replace()) { - rules.clear(); + break; } + } catch (Exception e) { + CUtil.LOGGER.error( + "Failed to load contextual {} rules for domain '{}' from pack '{}'; skipping this layer", + e, + this.directory, + domainId, + ruleSetResource.sourcePackId() + ); + } + } - rules.addAll(ruleSet.rules()); - } catch (IOException e) { - throw new IllegalStateException("Could not parse rule set", e); + return List.copyOf(rules); + } + + private Map> resolveRuleEngines(ClientLevel level) { + return resolveRuleEngines(this.preparations, level.registryAccess(), true); + } + + private Map> resolveRuleEngines( + Preparations preparations, + HolderLookup.Provider registries, + boolean logFailures + ) { + if (preparations.registrySwapper() == null) { + return createRuleEngines(preparations.rulesByDomain()); + } + + var resolvedRulesByDomain = new HashMap>(preparations.rulesByDomain().size()); + for (var entry : preparations.rulesByDomain().entrySet()) { + var resolvedRules = new ArrayList(entry.getValue().size()); + for (R rule : entry.getValue()) { + try { + resolvedRules.add(preparations.registrySwapper() + .swapTo(this.ruleCodec, rule, registries) + .getOrThrow()); + } catch (Exception e) { + if (logFailures) { + CUtil.LOGGER.warn( + "Failed to resolve a contextual rule for domain '{}'; leaving its registry references unresolved", + e, + entry.getKey() + ); + } + resolvedRules.add(rule); + } } + resolvedRulesByDomain.put(entry.getKey(), List.copyOf(resolvedRules)); } - return rules; + return createRuleEngines(resolvedRulesByDomain); + } + + private Map> createRuleEngines(Map> rulesByDomain) { + var ruleEngines = new HashMap>(rulesByDomain.size()); + rulesByDomain.forEach((domainId, rules) -> ruleEngines.put(domainId, new RuleEngine<>(rules))); + return Map.copyOf(ruleEngines); } @Override public CompletableFuture apply(Preparations data, ResourceManager manager, Executor executor) { - return CompletableFuture.runAsync(() -> { - this.ruleEngineByDomain = data.ruleEngineByDomain(); - }, executor); + return CompletableFuture.runAsync(() -> this.apply(data), executor); + } + + private synchronized void apply(Preparations data) { + this.preparations = data; + this.invalidateResolvedRuleEngines(); + } + + public synchronized void invalidateResolvedRuleEngines() { + this.ruleEnginesWithoutLevel = resolveRuleEngines( + this.preparations, + ClientRegistryLayer.createRegistryAccess().compositeAccess(), + false + ); + this.resolvedRuleEngines.clear(); } @Override @@ -94,6 +183,7 @@ public Identifier getReloadId() { } public record Preparations>( - Map> ruleEngineByDomain + Map> rulesByDomain, + @Nullable RegistryContextSwapper registrySwapper ) {} } diff --git a/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectInstanceImpl.java b/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectInstanceImpl.java index 6e3945680..748111fb2 100644 --- a/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectInstanceImpl.java +++ b/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectInstanceImpl.java @@ -12,24 +12,26 @@ import net.minecraft.resources.Identifier; import java.util.List; +import java.util.function.Supplier; public class TriggerEffectInstanceImpl implements TriggerEffectInstance { private final ContextualDomainImpl domain; - private final RuleEngine ruleEngine; + private final Supplier> ruleEngineSupplier; private DualsenseTriggerEffect leftEffect = DualsenseTriggerEffect.Off.INSTANCE; private DualsenseTriggerEffect rightEffect = DualsenseTriggerEffect.Off.INSTANCE; public TriggerEffectInstanceImpl( ContextualDomainImpl domain, - RuleEngine ruleEngine + Supplier> ruleEngineSupplier ) { this.domain = domain; - this.ruleEngine = ruleEngine; + this.ruleEngineSupplier = ruleEngineSupplier; } @Override public void update(C context) { + RuleEngine ruleEngine = this.ruleEngineSupplier.get(); boolean useTriggerEffects = context.controller().dualSense() .map(ds -> ds.settings().triggerEffects) .orElse(false); @@ -39,10 +41,11 @@ public void update(C context) { return; } - ContextualState state = this.domain.calculateState(context, this.ruleEngine.factDependencies()); - List passedRules = this.ruleEngine.evaluate( + ContextualState state = this.domain.calculateState(context, ruleEngine.factDependencies()); + List passedRules = ruleEngine.evaluate( state, - rule -> getTrigger(context.controller(), rule) != Trigger.NEITHER + rule -> getTrigger(context.controller(), rule) != Trigger.NEITHER, + "trigger_effect/" + this.domain.id() ); // there should only be one each diff --git a/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectManager.java b/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectManager.java index d4260f945..1052acf8c 100644 --- a/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectManager.java +++ b/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectManager.java @@ -6,7 +6,9 @@ */ package dev.isxander.controlify.controller.dualsense; +import dev.isxander.controlify.api.contextual.InGameContext; import dev.isxander.controlify.api.contextual.TriggerEffectInstance; +import dev.isxander.controlify.contextual.ContextualDomains; import dev.isxander.controlify.controller.ControllerEntity; import dev.isxander.controlify.driver.dualsense.DualsenseTriggerEffect; import dev.isxander.controlify.utils.MinecraftUtil; @@ -15,13 +17,31 @@ import java.util.*; public class TriggerEffectManager { - private final Minecraft minecraft; - public TriggerEffectManager(Minecraft minecraft) { - this.minecraft = minecraft; + private final TriggerEffectInstance instance; + + public TriggerEffectManager() { + this.instance = ContextualDomains.INSTANCE.inGame().createTriggerEffectInstance(); } public void applyTriggerEffects( + ControllerEntity controller, + boolean inputSuppressed + ) { + applyTriggerEffects(controller, instance, inputSuppressed); + } + + public void tick(Minecraft minecraft, ControllerEntity controller) { + if (minecraft.player == null || minecraft.level == null) { + return; + } + + this.instance.update( + InGameContext.create(minecraft, controller) + ); + } + + public static void applyTriggerEffects( ControllerEntity controller, TriggerEffectInstance instance, boolean inputSuppressed @@ -38,10 +58,10 @@ public void applyTriggerEffects( }); } - public boolean shouldUseTriggerEffects(ControllerEntity controller, boolean inputSuppressed) { + public static boolean shouldUseTriggerEffects(ControllerEntity controller, boolean inputSuppressed) { return !inputSuppressed && MinecraftUtil.getScreen() == null - && this.minecraft.player != null + && Minecraft.getInstance().player != null && controller.dualSense().map(ds -> ds.settings().triggerEffects).orElse(false); } } diff --git a/src/main/java/dev/isxander/controlify/debug/DebugProperties.java b/src/main/java/dev/isxander/controlify/debug/DebugProperties.java index dc5bdbe2f..b33753310 100644 --- a/src/main/java/dev/isxander/controlify/debug/DebugProperties.java +++ b/src/main/java/dev/isxander/controlify/debug/DebugProperties.java @@ -18,6 +18,9 @@ public class DebugProperties { private static final List> properties = new ArrayList<>(); public static final boolean DEBUG_LOGGING = boolProp("controlify.debug.logging", false, true); + /// Logs every contextual state contribution, derived fact evaluation, and rule evaluation. + /// This is intentionally separate from general debug logging because it is extremely verbose. + public static final boolean DEBUG_CONTEXTUAL_RULES = boolProp("controlify.debug.contextual_rules", false, false); /// Renders debug overlay for vmouse snapping public static final boolean DEBUG_SNAPPING = boolProp("controlify.debug.snapping", false, false); /// Forces all gamepads to be treated as a regular joystick diff --git a/src/main/java/dev/isxander/controlify/gui/guide/GuideInstanceImpl.java b/src/main/java/dev/isxander/controlify/gui/guide/GuideInstanceImpl.java index 64464f378..df0c5bfee 100644 --- a/src/main/java/dev/isxander/controlify/gui/guide/GuideInstanceImpl.java +++ b/src/main/java/dev/isxander/controlify/gui/guide/GuideInstanceImpl.java @@ -10,16 +10,18 @@ import dev.isxander.controlify.contextual.RuleEngine; import dev.isxander.controlify.controller.ControllerEntity; import dev.isxander.controlify.font.BindingFontHelper; +import net.minecraft.client.Minecraft; import net.minecraft.client.gui.Font; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.components.Renderable; import net.minecraft.network.chat.Component; import java.util.List; +import java.util.function.Supplier; public class GuideInstanceImpl implements GuideInstance { private final ContextualDomainImpl domain; - private final RuleEngine ruleEngine; + private final Supplier> ruleEngineSupplier; private final Font font; private PrecomputedLines leftGuides = PrecomputedLines.EMPTY; @@ -27,18 +29,19 @@ public class GuideInstanceImpl implements GuideInstance { public GuideInstanceImpl( ContextualDomainImpl domain, - RuleEngine ruleEngine, + Supplier> ruleEngineSupplier, Font font ) { this.domain = domain; - this.ruleEngine = ruleEngine; + this.ruleEngineSupplier = ruleEngineSupplier; this.font = font; } @Override public void update(C context) { - ContextualState state = this.domain.calculateState(context, this.ruleEngine.factDependencies()); - List guides = this.ruleEngine.evaluate(state); + RuleEngine ruleEngine = this.ruleEngineSupplier.get(); + ContextualState state = this.domain.calculateState(context, ruleEngine.factDependencies()); + List guides = ruleEngine.evaluate(state, _ -> true, "guide/" + this.domain.id()); var leftBuilder = new PrecomputedLines.Builder(); var rightBuilder = new PrecomputedLines.Builder(); @@ -83,26 +86,25 @@ public void update(C context) { @Override public void extractRenderState(GuiGraphicsExtractor graphics, boolean bottomAligned, boolean textContrast, int guiScale) { -// GuideRenderer.extractRenderState( -// graphics, -// this, -// Minecraft.getInstance(), -// bottomAligned, -// textContrast, -// guiScale -// ); + GuideRenderer.extractRenderState( + graphics, + this, + Minecraft.getInstance(), + bottomAligned, + textContrast, + guiScale + ); } @Override public Renderable renderable(boolean bottomAligned, boolean textContrast, int guiScale) { -// return new GuideRenderer.Renderable( -// this, -// Minecraft.getInstance(), -// bottomAligned, -// textContrast, -// guiScale -// ); - return null; + return new GuideRenderer.Renderable( + this, + Minecraft.getInstance(), + bottomAligned, + textContrast, + guiScale + ); } public PrecomputedLines leftGuides() { diff --git a/src/main/java/dev/isxander/controlify/gui/guide/InGameButtonGuide.java b/src/main/java/dev/isxander/controlify/gui/guide/InGameButtonGuide.java index a5fda7443..e29e7182c 100644 --- a/src/main/java/dev/isxander/controlify/gui/guide/InGameButtonGuide.java +++ b/src/main/java/dev/isxander/controlify/gui/guide/InGameButtonGuide.java @@ -46,18 +46,8 @@ public void tick() { GenericControllerSettings.GuideSettings settings = controller.settings().generic.guide; if (settings.showIngameGuide) { - if (minecraft.hitResult == null) { - ((MinecraftAccessor) minecraft).controlify$invokePick(1f); - } this.guideInstance.update( - new InGameContext( - minecraft, - minecraft.player, - minecraft.level, - minecraft.hitResult, - controller, - settings.verbosity - ) + InGameContext.create(minecraft, controller) ); } } diff --git a/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/AbstractContainerScreenProcessor.java b/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/AbstractContainerScreenProcessor.java index ad031fcd0..4753536c1 100644 --- a/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/AbstractContainerScreenProcessor.java +++ b/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/AbstractContainerScreenProcessor.java @@ -9,11 +9,11 @@ import dev.isxander.controlify.InputMode; import dev.isxander.controlify.api.ControlifyApi; import dev.isxander.controlify.api.contextual.ContainerContext; -import dev.isxander.controlify.api.guide.GuideInstance; +import dev.isxander.controlify.api.contextual.GuideInstance; import dev.isxander.controlify.bindings.ControlifyBindings; +import dev.isxander.controlify.contextual.ContextualDomains; import dev.isxander.controlify.controller.ControllerEntity; import dev.isxander.controlify.controller.haptic.HapticEffects; -import dev.isxander.controlify.gui.guide.GuideDomains; import dev.isxander.controlify.gui.guide.GuideRenderer; import dev.isxander.controlify.mixins.feature.guide.screen.AbstractContainerScreenAccessor; import dev.isxander.controlify.mixins.feature.screenop.ScreenAccessor; @@ -50,7 +50,7 @@ public AbstractContainerScreenProcessor( this.hoveredSlot = hoveredSlot; this.clickSlotFunction = clickSlotFunction; this.doItemSlotActions = doItemSlotActions; - this.guideInstance = GuideDomains.CONTAINER.createInstance(); + this.guideInstance = ContextualDomains.INSTANCE.container().createGuideInstance(minecraft.font); } @Override @@ -70,7 +70,7 @@ protected void handleScreenVMouse(ControllerEntity controller, VirtualMouseHandl controller, controller.settings().generic.guide.verbosity ); - this.guideInstance.update(ctx, minecraft.font); + this.guideInstance.update(ctx); Slot hoveredSlot = this.hoveredSlot.get(); if (hoveredSlot != null) { diff --git a/src/main/java/dev/isxander/controlify/utils/predicates/ClientEntityPredicate.java b/src/main/java/dev/isxander/controlify/utils/predicates/ClientEntityPredicate.java index 0be863a47..6900aaf9e 100644 --- a/src/main/java/dev/isxander/controlify/utils/predicates/ClientEntityPredicate.java +++ b/src/main/java/dev/isxander/controlify/utils/predicates/ClientEntityPredicate.java @@ -2,13 +2,12 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; -import net.minecraft.advancements.predicates.DistancePredicate; -import net.minecraft.advancements.predicates.GameTypePredicate; -import net.minecraft.advancements.predicates.MobEffectsPredicate; -import net.minecraft.advancements.predicates.SlotsPredicate; -import net.minecraft.advancements.predicates.entity.EntityFlagsPredicate; -import net.minecraft.advancements.predicates.entity.EntityTypePredicate; -import net.minecraft.advancements.predicates.entity.MovementPredicate; +//? if >=26.2 { +import net.minecraft.advancements.predicates.*; +import net.minecraft.advancements.predicates.entity.*; +//?} else { +/*import net.minecraft.advancements.criterion.*; +*///?} import net.minecraft.client.player.LocalPlayer; import net.minecraft.util.ExtraCodecs; import net.minecraft.world.entity.Entity; @@ -19,7 +18,7 @@ import java.util.Optional; -/// A subset of {@link net.minecraft.advancements.predicates.entity.EntityPredicate} that works +/// A subset of vanilla's entity predicate that works /// for all entities on the client. The vanilla one requires server-side-only data. /// /// This predicate also queries client-sided tags via {@link ClientPredicateUtils}. @@ -30,6 +29,9 @@ /// - `type_specific` /// - `targeted_entity` /// - data component matcher +/// +/// This predicate has additional conditions which its server-sided counterpart doesn't have: +/// - `game_mode` public record ClientEntityPredicate( Optional entityType, Optional distanceToPlayer, diff --git a/src/main/java/dev/isxander/controlify/utils/predicates/ClientPredicateUtils.java b/src/main/java/dev/isxander/controlify/utils/predicates/ClientPredicateUtils.java index f26fff87b..97ccad12c 100644 --- a/src/main/java/dev/isxander/controlify/utils/predicates/ClientPredicateUtils.java +++ b/src/main/java/dev/isxander/controlify/utils/predicates/ClientPredicateUtils.java @@ -2,9 +2,12 @@ import dev.isxander.controlify.utils.CUtil; import it.unimi.dsi.fastutil.ints.IntList; -import net.minecraft.advancements.predicates.ItemPredicate; -import net.minecraft.advancements.predicates.SlotsPredicate; -import net.minecraft.advancements.predicates.entity.EntityTypePredicate; +//? if >=26.2 { +import net.minecraft.advancements.predicates.*; +import net.minecraft.advancements.predicates.entity.*; +//?} else { +/*import net.minecraft.advancements.criterion.*; +*///?} import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.SlotAccess; import net.minecraft.world.entity.SlotProvider; diff --git a/src/main/java/dev/isxander/controlify/utils/predicates/EntityEquipmentPredicate.java b/src/main/java/dev/isxander/controlify/utils/predicates/EntityEquipmentPredicate.java index 78a8acc1b..829106c6a 100644 --- a/src/main/java/dev/isxander/controlify/utils/predicates/EntityEquipmentPredicate.java +++ b/src/main/java/dev/isxander/controlify/utils/predicates/EntityEquipmentPredicate.java @@ -4,7 +4,11 @@ import com.mojang.serialization.codecs.RecordCodecBuilder; import java.util.Optional; +//? if >=26.2 { import net.minecraft.advancements.predicates.ItemPredicate; +//?} else { +/*import net.minecraft.advancements.criterion.ItemPredicate; +*///?} import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.LivingEntity; @@ -60,4 +64,3 @@ public boolean matches(final @Nullable Entity entity) { return false; } } - diff --git a/src/main/resources/assets/controlify/contextual/facts/in_game.json b/src/main/resources/assets/controlify/contextual/facts/in_game.json index 6153bbc62..27fc1ceea 100644 --- a/src/main/resources/assets/controlify/contextual/facts/in_game.json +++ b/src/main/resources/assets/controlify/contextual/facts/in_game.json @@ -1,94 +1,97 @@ { "controlify:on_ground": { "slot": "controlify:self", - "entity": { - "flags": { - "is_on_ground": true - } - } + "entity": { "flags": { "is_on_ground": true } } }, "controlify:in_vehicle": { "slot": "controlify:self", - "entity": { - "vehicle": {} - } + "entity": { "vehicle": {} } }, "controlify:riding_saddled_horse": { "slot": "controlify:self", "entity": { "vehicle": { "type": "#minecraft:can_equip_saddle", - "equipment": { - "saddle": {} - } + "equipment": { "saddle": { "count": { "min": 1 } } } } } }, - "controlify:riding_happy_ghast": { + "controlify:riding_saddled_equine": { "slot": "controlify:self", "entity": { "vehicle": { - "type": "minecraft:happy_ghast" + "type": ["minecraft:horse", "minecraft:skeleton_horse", "minecraft:zombie_horse", "minecraft:donkey", "minecraft:mule"], + "equipment": { "saddle": { "count": { "min": 1 } } } } } }, - "controlify:flying": { + "controlify:riding_saddled_camel": { "slot": "controlify:self", "entity": { - "flags": { - "is_flying": true + "vehicle": { + "type": ["minecraft:camel", "minecraft:camel_husk"], + "equipment": { "saddle": { "count": { "min": 1 } } } } } }, - "controlify:elytra_flying": { + "controlify:riding_saddled_nautilus": { "slot": "controlify:self", "entity": { - "flags": { - "is_fall_flying": true + "vehicle": { + "type": ["minecraft:nautilus", "minecraft:zombie_nautilus"], + "equipment": { "saddle": { "count": { "min": 1 } } } } } }, - "controlify:in_water": { + "controlify:riding_happy_ghast": { "slot": "controlify:self", "entity": { - "flags": { - "is_in_water": true + "vehicle": { + "type": "minecraft:happy_ghast", + "equipment": { + "body": { + "items": "#minecraft:harnesses", + "count": { "min": 1 } + } + } } } }, + "controlify:flying": { + "slot": "controlify:self", + "entity": { "flags": { "is_flying": true } } + }, + "controlify:elytra_flying": { + "slot": "controlify:self", + "entity": { "flags": { "is_fall_flying": true } } + }, + "controlify:creative_flying": { + "all_of": ["controlify:flying"], + "none_of": ["controlify:elytra_flying"] + }, + "controlify:in_water": { + "slot": "controlify:self", + "entity": { "flags": { "is_in_water": true } } + }, "controlify:sneaking": { "slot": "controlify:self", - "entity": { - "flags": { - "is_sneaking": true - } - } + "entity": { "flags": { "is_sneaking": true } } }, "controlify:sprinting": { "slot": "controlify:self", - "entity": { - "flags": { - "is_sprinting": true - } - } + "entity": { "flags": { "is_sprinting": true } } }, "controlify:is_spectator": { "slot": "controlify:self", - "entity": { - "game_mode": "spectator" - } + "entity": { "game_mode": ["spectator"] } }, "controlify:is_creative": { "slot": "controlify:self", - "entity": { - "game_mode": "creative" - } + "entity": { "game_mode": ["creative"] } }, "controlify:is_survival": { "slot": "controlify:self", - "entity": { - "game_mode": "survival" - } + "entity": { "game_mode": ["survival"] } }, "controlify:looking_at_entity": { "slot": "controlify:hit_result", @@ -99,45 +102,220 @@ "block": {} }, "controlify:looking_at_air": { - "none_of": [ - "controlify:looking_at_entity", - "controlify:looking_at_block" - ] + "none_of": ["controlify:looking_at_entity", "controlify:looking_at_block"] }, "controlify:looking_at_villager": { + "slot": "controlify:hit_result", + "entity": { "type": "minecraft:villager" } + }, + "controlify:looking_at_merchant": { + "slot": "controlify:hit_result", + "entity": { "type": ["minecraft:villager", "minecraft:wandering_trader"] } + }, + "controlify:looking_at_chest_boat": { "slot": "controlify:hit_result", "entity": { - "type": "#minecraft:villager" + "type": [ + "minecraft:acacia_chest_boat", + "minecraft:bamboo_chest_raft", + "minecraft:birch_chest_boat", + "minecraft:cherry_chest_boat", + "minecraft:dark_oak_chest_boat", + "minecraft:jungle_chest_boat", + "minecraft:mangrove_chest_boat", + "minecraft:oak_chest_boat", + "minecraft:pale_oak_chest_boat", + "minecraft:spruce_chest_boat" + ] } }, + "controlify:looking_at_container_minecart": { + "slot": "controlify:hit_result", + "entity": { "type": ["minecraft:chest_minecart", "minecraft:hopper_minecart"] } + }, + "controlify:looking_at_rideable": { + "any_of": [ + { + "slot": "controlify:hit_result", + "entity": { "type": "#minecraft:boat" } + }, + "controlify:looking_at_chest_boat", + { + "slot": "controlify:hit_result", + "entity": { "type": "minecraft:minecart" } + }, + { + "slot": "controlify:hit_result", + "entity": { + "type": "#minecraft:can_equip_saddle", + "equipment": { "saddle": { "count": { "min": 1 } } } + } + }, + { + "slot": "controlify:hit_result", + "entity": { + "type": "minecraft:happy_ghast", + "equipment": { + "body": { + "items": "#minecraft:harnesses", + "count": { "min": 1 } + } + } + } + } + ] + }, + "controlify:looking_at_closed_openable": { + "any_of": [ + { + "slot": "controlify:hit_result", + "block": { "blocks": "#minecraft:doors", "state": { "open": "false" } } + }, + { + "slot": "controlify:hit_result", + "block": { "blocks": "#minecraft:trapdoors", "state": { "open": "false" } } + }, + { + "slot": "controlify:hit_result", + "block": { "blocks": "#minecraft:fence_gates", "state": { "open": "false" } } + } + ], + "none_of": [ + { + "slot": "controlify:hit_result", + "block": { "blocks": ["minecraft:iron_door", "minecraft:iron_trapdoor"] } + } + ] + }, + "controlify:looking_at_open_openable": { + "any_of": [ + { + "slot": "controlify:hit_result", + "block": { "blocks": "#minecraft:doors", "state": { "open": "true" } } + }, + { + "slot": "controlify:hit_result", + "block": { "blocks": "#minecraft:trapdoors", "state": { "open": "true" } } + }, + { + "slot": "controlify:hit_result", + "block": { "blocks": "#minecraft:fence_gates", "state": { "open": "true" } } + } + ], + "none_of": [ + { + "slot": "controlify:hit_result", + "block": { "blocks": ["minecraft:iron_door", "minecraft:iron_trapdoor"] } + } + ] + }, + "controlify:looking_at_container": { + "any_of": [ + { + "slot": "controlify:hit_result", + "block": { + "blocks": [ + "minecraft:barrel", + "minecraft:chest", + "minecraft:dispenser", + "minecraft:dropper", + "minecraft:ender_chest", + "minecraft:hopper", + "minecraft:trapped_chest" + ] + } + }, + { + "slot": "controlify:hit_result", + "block": { "blocks": "#minecraft:shulker_boxes" } + }, + { + "slot": "controlify:hit_result", + "block": { "blocks": "#minecraft:copper_chests" } + } + ] + }, + "controlify:looking_at_button": { + "slot": "controlify:hit_result", + "block": { "blocks": "#minecraft:buttons" } + }, + "controlify:looking_at_lever": { + "slot": "controlify:hit_result", + "block": { "blocks": "minecraft:lever" } + }, + "controlify:looking_at_bell": { + "slot": "controlify:hit_result", + "block": { "blocks": "minecraft:bell" } + }, + "controlify:looking_at_bed": { + "slot": "controlify:hit_result", + "block": { "blocks": "#minecraft:beds" } + }, + "controlify:looking_at_crafting_table": { + "slot": "controlify:hit_result", + "block": { "blocks": "minecraft:crafting_table" } + }, + "controlify:looking_at_furnace": { + "slot": "controlify:hit_result", + "block": { "blocks": ["minecraft:furnace", "minecraft:blast_furnace"] } + }, + "controlify:looking_at_smoker": { + "slot": "controlify:hit_result", + "block": { "blocks": "minecraft:smoker" } + }, + "controlify:looking_at_brewing_stand": { + "slot": "controlify:hit_result", + "block": { "blocks": "minecraft:brewing_stand" } + }, + "controlify:looking_at_enchanting_table": { + "slot": "controlify:hit_result", + "block": { "blocks": "minecraft:enchanting_table" } + }, + "controlify:looking_at_smithing_table": { + "slot": "controlify:hit_result", + "block": { "blocks": "minecraft:smithing_table" } + }, + "controlify:looking_at_repair_station": { + "any_of": [ + { + "slot": "controlify:hit_result", + "block": { "blocks": "#minecraft:anvil" } + }, + { + "slot": "controlify:hit_result", + "block": { "blocks": "minecraft:grindstone" } + } + ] + }, + "controlify:looking_at_stonecutter": { + "slot": "controlify:hit_result", + "block": { "blocks": "minecraft:stonecutter" } + }, + "controlify:looking_at_loom": { + "slot": "controlify:hit_result", + "block": { "blocks": "minecraft:loom" } + }, + "controlify:looking_at_cartography_table": { + "slot": "controlify:hit_result", + "block": { "blocks": "minecraft:cartography_table" } + }, "controlify:has_item_in_main_hand": { "slot": "controlify:main_hand", - "item": { - "count": { - "min": 1 - } - } + "item": { "count": { "min": 1 } } }, "controlify:has_item_in_offhand": { "slot": "controlify:off_hand", - "item": { - "count": { - "min": 1 - } - } + "item": { "count": { "min": 1 } } }, "controlify:has_item_in_either_hand": { - "any_of": [ - "controlify:has_item_in_main_hand", - "controlify:has_item_in_offhand" - ] + "any_of": ["controlify:has_item_in_main_hand", "controlify:has_item_in_offhand"] }, "controlify:has_multiple_items_in_main_hand": { "slot": "controlify:main_hand", - "item": { - "count": { - "min": 2 - } - } + "item": { "count": { "min": 2 } } + }, + "controlify:main_hand_is_tool": { + "slot": "controlify:main_hand", + "item": { "predicates": { "minecraft:tool": {} } } } } diff --git a/src/main/resources/assets/controlify/guides/container.json b/src/main/resources/assets/controlify/contextual/guide/container.json similarity index 57% rename from src/main/resources/assets/controlify/guides/container.json rename to src/main/resources/assets/controlify/contextual/guide/container.json index b2a7eb917..f945ccea7 100644 --- a/src/main/resources/assets/controlify/guides/container.json +++ b/src/main/resources/assets/controlify/contextual/guide/container.json @@ -3,75 +3,82 @@ { "for": "controlify:gui_back", "where": "left", + "if": true, "then": { "translate": "controlify.guide.container.exit" } }, - { "for": "controlify:inv_select", "where": "left", - "when": ["controlify:holding_many_items", "controlify:hovering_item", "controlify:can_place_held_item"], + "if": { "all_of": ["controlify:holding_many_items", "controlify:hovering_item", "controlify:can_place_held_item"] }, "then": { "translate": "controlify.guide.container.place_all" } }, { "for": "controlify:inv_select", "where": "left", - "when": ["controlify:holding_item", "controlify:hovering_item", "controlify:can_place_held_item"], + "if": { "all_of": ["controlify:holding_item", "controlify:hovering_item", "controlify:can_place_held_item"] }, "then": { "translate": "controlify.guide.container.place_one" } }, { "for": "controlify:inv_select", "where": "left", - "when": ["controlify:holding_item", "controlify:hovering_item"], - "forbid": ["controlify:can_place_held_item"], + "if": { + "all_of": ["controlify:holding_item", "controlify:hovering_item"], + "none_of": ["controlify:can_place_held_item"] + }, "then": { "translate": "controlify.guide.container.swap" } }, { "for": "controlify:inv_select", "where": "left", - "when": ["controlify:cursor_outside_container", "controlify:holding_item"], + "if": { "all_of": ["controlify:cursor_outside_container", "controlify:holding_item"] }, "then": { "translate": "controlify.guide.container.drop" } }, { "for": "controlify:inv_select", "where": "left", - "when": ["controlify:hovering_item"], - "forbid": ["controlify:holding_item"], + "if": { + "all_of": ["controlify:hovering_item"], + "none_of": ["controlify:holding_item"] + }, "then": { "translate": "controlify.guide.container.take" } }, - { "for": "controlify:drop_inventory", "where": "right", - "when": ["controlify:holding_item"], + "if": "controlify:holding_item", "then": { "translate": "controlify.guide.container.drop" } }, - { "for": "controlify:inv_quick_move", "where": "right", - "when": ["controlify:hovering_item"], - "forbid": ["controlify:holding_item"], + "if": { + "all_of": ["controlify:hovering_item"], + "none_of": ["controlify:holding_item"] + }, "then": { "translate": "controlify.guide.container.quick_move" } }, - { "for": "controlify:inv_take_half", "where": "right", - "when": ["controlify:hovering_item_is_bundle", "controlify:selected_bundle_slot"], - "forbid": ["controlify:holding_item"], + "if": { + "all_of": ["controlify:hovering_item_is_bundle", "controlify:selected_bundle_slot"], + "none_of": ["controlify:holding_item"] + }, "then": { "translate": "controlify.guide.container.take_from_bundle" } }, { "for": "controlify:inv_take_half", "where": "right", - "when": ["controlify:hovering_many_items"], - "forbid": ["controlify:holding_item"], + "if": { + "all_of": ["controlify:hovering_many_items"], + "none_of": ["controlify:holding_item"] + }, "then": { "translate": "controlify.guide.container.take_half" } }, { "for": "controlify:inv_take_half", "where": "right", - "when": ["controlify:hovering_slot", "controlify:holding_item", "controlify:can_place_held_item"], + "if": { "all_of": ["controlify:hovering_slot", "controlify:holding_item", "controlify:can_place_held_item"] }, "then": { "translate": "controlify.guide.container.take_one" } } ] diff --git a/src/main/resources/assets/controlify/contextual/guide/in_game.json b/src/main/resources/assets/controlify/contextual/guide/in_game.json new file mode 100644 index 000000000..cf0b937b4 --- /dev/null +++ b/src/main/resources/assets/controlify/contextual/guide/in_game.json @@ -0,0 +1,655 @@ +{ + "rules": [ + { + "for": "controlify:jump", + "where": "left", + "if": { "all_of": ["controlify:verbosity_minimal", "controlify:riding_happy_ghast"] }, + "then": { "translate": "controlify.guide.ingame.fly_up" } + }, + { + "for": "controlify:jump", + "where": "left", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { "any_of": ["controlify:riding_saddled_camel", "controlify:riding_saddled_nautilus"] } + ] + }, + "then": { "translate": "controlify.guide.ingame.dash" } + }, + { + "for": "controlify:jump", + "where": "left", + "if": { "all_of": ["controlify:verbosity_minimal", "controlify:riding_saddled_equine"] }, + "then": { "translate": "key.jump" } + }, + { + "for": "controlify:jump", + "where": "left", + "if": { "all_of": ["controlify:verbosity_minimal", "controlify:creative_flying"] }, + "then": { "translate": "controlify.guide.ingame.fly_up" } + }, + { + "for": "controlify:jump", + "where": "left", + "if": { "all_of": ["controlify:verbosity_minimal", "controlify:in_water"] }, + "then": { "translate": "controlify.guide.ingame.swim_up" } + }, + { + "for": "controlify:jump", + "where": "left", + "if": { "all_of": ["controlify:verbosity_minimal", "controlify:on_climbable"] }, + "then": { "translate": "controlify.guide.ingame.climb_up" } + }, + { + "for": "controlify:jump", + "where": "left", + "if": { "all_of": ["controlify:verbosity_minimal", "controlify:on_ground"] }, + "then": { "translate": "key.jump" } + }, + { + "for": "controlify:jump", + "where": "left", + "if": { "all_of": ["controlify:verbosity_minimal", "controlify:can_elytra_fly"] }, + "then": { "translate": "controlify.guide.ingame.start_elytra" } + }, + { + "for": "controlify:sneak", + "where": "left", + "if": { "all_of": ["controlify:verbosity_minimal", "controlify:in_vehicle"] }, + "then": { "translate": "controlify.guide.ingame.dismount" } + }, + { + "for": "controlify:sneak", + "where": "left", + "if": { "all_of": ["controlify:verbosity_minimal", "controlify:creative_flying"] }, + "then": { "translate": "controlify.guide.ingame.fly_down" } + }, + { + "for": "controlify:sneak", + "where": "left", + "if": { + "all_of": ["controlify:verbosity_minimal", "controlify:in_water"], + "none_of": ["controlify:on_ground"] + }, + "then": { "translate": "controlify.guide.ingame.swim_down" } + }, + { + "for": "controlify:sneak", + "where": "left", + "if": { "all_of": ["controlify:verbosity_minimal", "controlify:on_climbable"] }, + "then": { "translate": "controlify.guide.ingame.climb_down" } + }, + { + "for": "controlify:sneak", + "where": "left", + "if": { "all_of": ["controlify:verbosity_minimal", "controlify:is_toggle_sneak", "controlify:sneaking"] }, + "then": { "translate": "controlify.guide.ingame.stop_sneaking" } + }, + { + "for": "controlify:sneak", + "where": "left", + "if": { + "all_of": ["controlify:verbosity_minimal", "controlify:is_toggle_sneak"], + "none_of": ["controlify:sneaking"] + }, + "then": { "translate": "controlify.guide.ingame.start_sneaking" } + }, + { + "for": "controlify:sneak", + "where": "left", + "if": { + "all_of": ["controlify:verbosity_minimal"], + "none_of": ["controlify:is_toggle_sneak", "controlify:sneaking"] + }, + "then": { "translate": "controlify.guide.ingame.sneak" } + }, + { + "for": "controlify:sprint", + "where": "left", + "if": { + "all_of": ["controlify:verbosity_reduced", "controlify:input_moving", "controlify:under_water"], + "none_of": ["controlify:sprinting", "controlify:in_vehicle", "controlify:flying"] + }, + "then": { "translate": "controlify.guide.ingame.start_swimming" } + }, + { + "for": "controlify:sprint", + "where": "left", + "if": { + "all_of": ["controlify:verbosity_reduced", "controlify:input_moving"], + "none_of": ["controlify:sprinting", "controlify:in_vehicle", "controlify:flying", "controlify:under_water"] + }, + "then": { "translate": "controlify.guide.ingame.start_sprinting" } + }, + { + "for": "controlify:sprint", + "where": "left", + "if": { + "all_of": ["controlify:verbosity_reduced", "controlify:sprinting", "controlify:is_toggle_sprint", "controlify:under_water"], + "none_of": ["controlify:in_vehicle", "controlify:flying"] + }, + "then": { "translate": "controlify.guide.ingame.stop_swimming" } + }, + { + "for": "controlify:sprint", + "where": "left", + "if": { + "all_of": ["controlify:verbosity_reduced", "controlify:sprinting", "controlify:is_toggle_sprint"], + "none_of": ["controlify:under_water", "controlify:in_vehicle", "controlify:flying"] + }, + "then": { "translate": "controlify.guide.ingame.stop_sprinting" } + }, + { + "for": "controlify:inventory", + "where": "left", + "if": "controlify:verbosity_reduced", + "then": { "translate": "controlify.guide.ingame.inventory" } + }, + { + "for": "controlify:radial_menu", + "where": "left", + "if": "controlify:verbosity_full", + "then": { "translate": "controlify.gui.radial_menu" } + }, + { + "for": "controlify:attack", + "where": "right", + "if": { "all_of": ["controlify:verbosity_minimal", "controlify:looking_at_entity", "controlify:is_spectator"] }, + "then": { "translate": "controlify.guide.ingame.spectate" } + }, + { + "for": "controlify:attack", + "where": "right", + "if": { + "all_of": ["controlify:verbosity_minimal", "controlify:looking_at_entity"], + "none_of": ["controlify:is_spectator"] + }, + "then": { "translate": "controlify.guide.ingame.attack" } + }, + { + "for": "controlify:attack", + "where": "right", + "if": { "all_of": ["controlify:verbosity_minimal", "controlify:looking_at_block", "controlify:main_hand_is_tool"] }, + "then": { "translate": "controlify.guide.ingame.mine" } + }, + { + "for": "controlify:attack", + "where": "right", + "if": { "all_of": ["controlify:verbosity_minimal", "controlify:looking_at_block"] }, + "then": { "translate": "controlify.guide.ingame.break" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { "all_of": ["controlify:verbosity_minimal", "controlify:looking_at_entity", "controlify:is_spectator"] }, + "then": { "translate": "controlify.guide.ingame.spectate" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { "all_of": ["controlify:verbosity_minimal", "controlify:looking_at_merchant"] }, + "then": { "translate": "controlify.guide.ingame.trade" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { "all_of": ["controlify:verbosity_minimal", "controlify:looking_at_chest_boat", "controlify:sneaking"] }, + "then": { "translate": "controlify.guide.ingame.open" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { "all_of": ["controlify:verbosity_minimal", "controlify:looking_at_container_minecart"] }, + "then": { "translate": "controlify.guide.ingame.open" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": ["controlify:verbosity_minimal", "controlify:looking_at_rideable"], + "none_of": ["controlify:sneaking"] + }, + "then": { "translate": "controlify.guide.ingame.ride" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": ["controlify:verbosity_minimal", "controlify:looking_at_closed_openable"], + "none_of": ["controlify:sneaking"] + }, + "then": { "translate": "controlify.guide.ingame.open" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": ["controlify:verbosity_minimal", "controlify:looking_at_open_openable"], + "none_of": ["controlify:sneaking"] + }, + "then": { "translate": "controlify.guide.ingame.close" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": ["controlify:verbosity_minimal", "controlify:looking_at_container"], + "none_of": ["controlify:sneaking"] + }, + "then": { "translate": "controlify.guide.ingame.open" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { "all_of": ["controlify:verbosity_minimal", "controlify:looking_at_button"] }, + "then": { "translate": "controlify.guide.ingame.press" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { "all_of": ["controlify:verbosity_minimal", "controlify:looking_at_lever"] }, + "then": { "translate": "controlify.guide.ingame.toggle" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { "all_of": ["controlify:verbosity_minimal", "controlify:looking_at_bell"] }, + "then": { "translate": "controlify.guide.ingame.ring" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": ["controlify:verbosity_minimal", "controlify:looking_at_bed"], + "none_of": ["controlify:sneaking"] + }, + "then": { "translate": "controlify.guide.ingame.sleep" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { "all_of": ["controlify:verbosity_minimal", "controlify:looking_at_crafting_table"], "none_of": ["controlify:sneaking"] }, + "then": { "translate": "controlify.guide.ingame.craft" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { "all_of": ["controlify:verbosity_minimal", "controlify:looking_at_furnace"], "none_of": ["controlify:sneaking"] }, + "then": { "translate": "controlify.guide.ingame.smelt" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { "all_of": ["controlify:verbosity_minimal", "controlify:looking_at_smoker"], "none_of": ["controlify:sneaking"] }, + "then": { "translate": "controlify.guide.ingame.cook" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { "all_of": ["controlify:verbosity_minimal", "controlify:looking_at_brewing_stand"], "none_of": ["controlify:sneaking"] }, + "then": { "translate": "controlify.guide.ingame.brew" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { "all_of": ["controlify:verbosity_minimal", "controlify:looking_at_enchanting_table"], "none_of": ["controlify:sneaking"] }, + "then": { "translate": "controlify.guide.ingame.enchant" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { "all_of": ["controlify:verbosity_minimal", "controlify:looking_at_smithing_table"], "none_of": ["controlify:sneaking"] }, + "then": { "translate": "controlify.guide.ingame.smith" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { "all_of": ["controlify:verbosity_minimal", "controlify:looking_at_repair_station"], "none_of": ["controlify:sneaking"] }, + "then": { "translate": "controlify.guide.ingame.repair" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { "all_of": ["controlify:verbosity_minimal", "controlify:looking_at_stonecutter"], "none_of": ["controlify:sneaking"] }, + "then": { "translate": "controlify.guide.ingame.cut_stone" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { "all_of": ["controlify:verbosity_minimal", "controlify:looking_at_loom"], "none_of": ["controlify:sneaking"] }, + "then": { "translate": "controlify.guide.ingame.weave" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { "all_of": ["controlify:verbosity_minimal", "controlify:looking_at_cartography_table"], "none_of": ["controlify:sneaking"] }, + "then": { "translate": "controlify.guide.ingame.edit_map" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { "slot": "controlify:main_hand", "item": { "items": "#c:tools/bow" } } + ] + }, + "then": { "translate": "controlify.guide.ingame.shoot" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { "slot": "controlify:main_hand", "item": { "predicates": { "minecraft:charged_projectiles": {} } } } + ], + "none_of": [ + { "slot": "controlify:main_hand", "item": { "components": { "minecraft:charged_projectiles": [] } } } + ] + }, + "then": { "translate": "controlify.guide.ingame.shoot" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { "slot": "controlify:main_hand", "item": { "items": "minecraft:crossbow", "components": { "minecraft:charged_projectiles": [] } } } + ] + }, + "then": { "translate": "controlify.guide.ingame.load" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { "slot": "controlify:main_hand", "item": { "predicates": { "minecraft:blocks_attacks": {} } } } + ] + }, + "then": { "translate": "controlify.guide.ingame.block" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { "slot": "controlify:main_hand", "item": { "predicates": { "minecraft:consumable": {} } } } + ] + }, + "then": { "translate": "controlify.guide.ingame.consume" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { + "any_of": [ + { "slot": "controlify:main_hand", "item": { "items": "#c:tools/trident" } }, + { "slot": "controlify:main_hand", "item": { "items": "#minecraft:spears" } }, + { "slot": "controlify:main_hand", "item": { "items": "#minecraft:eggs" } }, + { + "slot": "controlify:main_hand", + "item": { + "items": [ + "minecraft:snowball", + "minecraft:splash_potion", + "minecraft:lingering_potion", + "minecraft:experience_bottle", + "minecraft:ender_pearl", + "minecraft:wind_charge" + ] + } + } + ] + } + ] + }, + "then": { "translate": "controlify.guide.ingame.throw" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:fishing_hook_cast", + { "slot": "controlify:main_hand", "item": { "items": "#c:tools/fishing_rod" } } + ] + }, + "then": { "translate": "controlify.guide.ingame.reel_in" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { "slot": "controlify:main_hand", "item": { "items": "#c:tools/fishing_rod" } } + ], + "none_of": ["controlify:fishing_hook_cast"] + }, + "then": { "translate": "controlify.guide.ingame.cast" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { "all_of": ["controlify:verbosity_minimal", { "slot": "controlify:main_hand", "item": { "items": "#c:tools/brush" } }] }, + "then": { "translate": "controlify.guide.ingame.brush" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { "all_of": ["controlify:verbosity_minimal", { "slot": "controlify:main_hand", "item": { "items": "minecraft:spyglass" } }] }, + "then": { "translate": "controlify.guide.ingame.zoom" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { "all_of": ["controlify:verbosity_minimal", { "slot": "controlify:main_hand", "item": { "predicates": { "minecraft:instrument": {} } } }] }, + "then": { "translate": "controlify.guide.ingame.play" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { "all_of": ["controlify:verbosity_minimal", { "slot": "controlify:main_hand", "item": { "predicates": { "minecraft:equippable": {} } } }] }, + "then": { "translate": "controlify.guide.ingame.equip" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { "all_of": ["controlify:verbosity_minimal", "controlify:looking_at_block", "controlify:main_hand_is_block_item"] }, + "then": { "translate": "controlify.guide.ingame.place" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { "slot": "controlify:off_hand", "item": { "items": "#c:tools/bow" } } + ] + }, + "then": { "translate": "controlify.guide.ingame.shoot" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { "slot": "controlify:off_hand", "item": { "predicates": { "minecraft:charged_projectiles": {} } } } + ], + "none_of": [ + { "slot": "controlify:off_hand", "item": { "components": { "minecraft:charged_projectiles": [] } } } + ] + }, + "then": { "translate": "controlify.guide.ingame.shoot" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { "slot": "controlify:off_hand", "item": { "items": "minecraft:crossbow", "components": { "minecraft:charged_projectiles": [] } } } + ] + }, + "then": { "translate": "controlify.guide.ingame.load" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { "all_of": ["controlify:verbosity_minimal", { "slot": "controlify:off_hand", "item": { "predicates": { "minecraft:blocks_attacks": {} } } }] }, + "then": { "translate": "controlify.guide.ingame.block" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { "all_of": ["controlify:verbosity_minimal", { "slot": "controlify:off_hand", "item": { "predicates": { "minecraft:consumable": {} } } }] }, + "then": { "translate": "controlify.guide.ingame.consume" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { + "any_of": [ + { "slot": "controlify:off_hand", "item": { "items": "#c:tools/trident" } }, + { "slot": "controlify:off_hand", "item": { "items": "#minecraft:spears" } }, + { "slot": "controlify:off_hand", "item": { "items": "#minecraft:eggs" } }, + { + "slot": "controlify:off_hand", + "item": { + "items": [ + "minecraft:snowball", + "minecraft:splash_potion", + "minecraft:lingering_potion", + "minecraft:experience_bottle", + "minecraft:ender_pearl", + "minecraft:wind_charge" + ] + } + } + ] + } + ] + }, + "then": { "translate": "controlify.guide.ingame.throw" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:fishing_hook_cast", + { "slot": "controlify:off_hand", "item": { "items": "#c:tools/fishing_rod" } } + ] + }, + "then": { "translate": "controlify.guide.ingame.reel_in" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { "slot": "controlify:off_hand", "item": { "items": "#c:tools/fishing_rod" } } + ], + "none_of": ["controlify:fishing_hook_cast"] + }, + "then": { "translate": "controlify.guide.ingame.cast" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { "all_of": ["controlify:verbosity_minimal", { "slot": "controlify:off_hand", "item": { "items": "#c:tools/brush" } }] }, + "then": { "translate": "controlify.guide.ingame.brush" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { "all_of": ["controlify:verbosity_minimal", { "slot": "controlify:off_hand", "item": { "items": "minecraft:spyglass" } }] }, + "then": { "translate": "controlify.guide.ingame.zoom" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { "all_of": ["controlify:verbosity_minimal", { "slot": "controlify:off_hand", "item": { "predicates": { "minecraft:instrument": {} } } }] }, + "then": { "translate": "controlify.guide.ingame.play" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { "all_of": ["controlify:verbosity_minimal", { "slot": "controlify:off_hand", "item": { "predicates": { "minecraft:equippable": {} } } }] }, + "then": { "translate": "controlify.guide.ingame.equip" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { "all_of": ["controlify:verbosity_minimal", "controlify:looking_at_block", "controlify:off_hand_is_block_item"] }, + "then": { "translate": "controlify.guide.ingame.place" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": ["controlify:verbosity_minimal", "controlify:looking_at_entity"], + "none_of": ["controlify:is_spectator"] + }, + "then": { "translate": "controlify.guide.ingame.interact" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { "all_of": ["controlify:verbosity_minimal", "controlify:looking_at_block"] }, + "then": { "translate": "controlify.guide.ingame.use" } + }, + { + "for": "controlify:use", + "where": "right", + "if": { "all_of": ["controlify:verbosity_minimal", "controlify:has_item_in_either_hand"] }, + "then": { "translate": "controlify.guide.ingame.use" } + }, + { + "for": "controlify:drop", + "where": "right", + "if": { "all_of": ["controlify:verbosity_reduced", "controlify:has_item_in_main_hand"] }, + "then": { "translate": "controlify.guide.ingame.drop" } + }, + { + "for": "controlify:drop_stack", + "where": "right", + "if": { "all_of": ["controlify:verbosity_full", "controlify:has_multiple_items_in_main_hand"] }, + "then": { "translate": "controlify.binding.controlify.drop_stack" } + }, + { + "for": "controlify:swap_hands", + "where": "right", + "if": { "all_of": ["controlify:verbosity_reduced", "controlify:has_item_in_either_hand"] }, + "then": { "translate": "controlify.guide.ingame.swap_hands" } + }, + { + "for": "controlify:pick_block", + "where": "right", + "if": { "all_of": ["controlify:verbosity_full", "controlify:looking_at_block", "controlify:is_creative"] }, + "then": { "translate": "controlify.guide.ingame.pick_block" } + }, + { + "for": "controlify:pick_block_nbt", + "where": "right", + "if": { "all_of": ["controlify:verbosity_full", "controlify:looking_at_block", "controlify:is_creative"] }, + "then": { "translate": "controlify.binding.controlify.pick_block_nbt" } + } + ] +} diff --git a/src/main/resources/assets/controlify/contextual/trigger_effect/in_game.json b/src/main/resources/assets/controlify/contextual/trigger_effect/in_game.json new file mode 100644 index 000000000..cd16ab2f7 --- /dev/null +++ b/src/main/resources/assets/controlify/contextual/trigger_effect/in_game.json @@ -0,0 +1,553 @@ +{ + "rules": [ + { + "for": "controlify:use", + "if": { + "slot": "controlify:active_item", + "item": { + "items": "#c:tools/bow" + } + }, + "then": { + "type": "feedback_slope", + "start_position": 3, + "end_position": 9, + "start_strength": 2, + "end_strength": 8 + } + }, + { + "for": "controlify:use", + "if": { + "slot": "controlify:active_item", + "item": { + "components": { + "minecraft:charged_projectiles": [] + } + } + }, + "then": { + "type": "feedback_slope", + "start_position": 2, + "end_position": 9, + "start_strength": 5, + "end_strength": 8 + } + }, + { + "for": "controlify:use", + "if": { + "slot": "controlify:active_item", + "item": { + "predicates": { + "minecraft:charged_projectiles": {} + } + } + }, + "then": { + "type": "weapon", + "start_position": 3, + "end_position": 5, + "strength": 1 + } + }, + { + "for": "controlify:use", + "if": { + "slot": "controlify:active_item", + "item": { + "predicates": { + "minecraft:consumable": {} + } + } + }, + "then": { + "type": "feedback", + "position": 3, + "strength": 1 + } + }, + { + "for": "controlify:use", + "if": { + "slot": "controlify:active_item", + "item": { + "predicates": { + "minecraft:blocks_attacks": {} + } + } + }, + "then": { + "type": "feedback", + "position": 3, + "strength": 3 + } + }, + { + "for": "controlify:use", + "if": { + "slot": "controlify:active_item", + "item": { + "predicates": { + "minecraft:equippable": {} + } + } + }, + "then": { + "type": "weapon", + "start_position": 3, + "end_position": 5, + "strength": 1 + } + }, + { + "for": "controlify:use", + "if": { + "slot": "controlify:active_item", + "item": { + "predicates": { + "minecraft:kinetic_weapon": {} + } + } + }, + "then": { + "type": "feedback", + "position": 3, + "strength": 3 + } + }, + { + "for": "controlify:use", + "if": { + "slot": "controlify:active_item", + "item": { + "predicates": { + "minecraft:instrument": {} + } + } + }, + "then": { + "type": "feedback", + "position": 3, + "strength": 3 + } + }, + { + "for": "controlify:use", + "if": { + "slot": "controlify:active_item", + "item": { + "items": "#c:tools/brush" + } + }, + "then": { + "type": "vibration", + "position": 2, + "amplitude": 2, + "frequency": 15 + } + }, + { + "for": "controlify:use", + "if": { + "slot": "controlify:active_item", + "item": { + "items": "minecraft:spyglass" + } + }, + "then": { + "type": "feedback", + "position": 3, + "strength": 1 + } + }, + { + "for": "controlify:use", + "if": { + "slot": "controlify:active_item", + "item": { + "items": "#c:tools/trident" + } + }, + "then": { + "type": "feedback_slope", + "start_position": 2, + "end_position": 9, + "start_strength": 2, + "end_strength": 8 + } + }, + { + "for": "controlify:use", + "if": { + "slot": "controlify:active_item", + "item": { + "items": "#minecraft:eggs" + } + }, + "then": { + "type": "vibration", + "position": 3, + "amplitude": 6, + "frequency": 5 + } + }, + { + "for": "controlify:use", + "if": { + "slot": "controlify:active_item", + "item": { + "items": [ + "minecraft:snowball", + "minecraft:splash_potion", + "minecraft:lingering_potion", + "minecraft:experience_bottle" + ] + } + }, + "then": { + "type": "vibration", + "position": 3, + "amplitude": 6, + "frequency": 5 + } + }, + { + "for": "controlify:use", + "if": { + "slot": "controlify:active_item", + "item": { + "items": "#c:tools/fishing_rod" + } + }, + "then": { + "type": "weapon", + "start_position": 3, + "end_position": 5, + "strength": 1 + } + }, + { + "for": "controlify:use", + "if": { + "slot": "controlify:active_item", + "item": { + "predicates": { + "minecraft:bundle_contents": { + "items": { + "size": { + "min": 1 + } + } + } + } + } + }, + "then": { + "type": "feedback", + "position": 3, + "strength": 6 + } + }, + { + "for": "controlify:use", + "if": { + "slot": "controlify:active_item", + "item": { + "predicates": { + "minecraft:use_cooldown": {} + } + } + }, + "then": { + "type": "weapon", + "start_position": 3, + "end_position": 5, + "strength": 1 + } + }, + { + "for": "controlify:use", + "if": { + "slot": "controlify:off_hand", + "item": { + "items": "#c:tools/bow" + } + }, + "then": { + "type": "feedback_slope", + "start_position": 3, + "end_position": 9, + "start_strength": 2, + "end_strength": 8 + } + }, + { + "for": "controlify:use", + "if": { + "slot": "controlify:off_hand", + "item": { + "components": { + "minecraft:charged_projectiles": [] + } + } + }, + "then": { + "type": "feedback_slope", + "start_position": 2, + "end_position": 9, + "start_strength": 5, + "end_strength": 8 + } + }, + { + "for": "controlify:use", + "if": { + "slot": "controlify:off_hand", + "item": { + "predicates": { + "minecraft:charged_projectiles": {} + } + } + }, + "then": { + "type": "weapon", + "start_position": 3, + "end_position": 5, + "strength": 1 + } + }, + { + "for": "controlify:use", + "if": { + "slot": "controlify:off_hand", + "item": { + "predicates": { + "minecraft:consumable": {} + } + } + }, + "then": { + "type": "feedback", + "position": 3, + "strength": 1 + } + }, + { + "for": "controlify:use", + "if": { + "slot": "controlify:off_hand", + "item": { + "predicates": { + "minecraft:blocks_attacks": {} + } + } + }, + "then": { + "type": "feedback", + "position": 3, + "strength": 3 + } + }, + { + "for": "controlify:use", + "if": { + "slot": "controlify:off_hand", + "item": { + "predicates": { + "minecraft:equippable": {} + } + } + }, + "then": { + "type": "weapon", + "start_position": 3, + "end_position": 5, + "strength": 1 + } + }, + { + "for": "controlify:use", + "if": { + "slot": "controlify:off_hand", + "item": { + "predicates": { + "minecraft:kinetic_weapon": {} + } + } + }, + "then": { + "type": "feedback", + "position": 3, + "strength": 3 + } + }, + { + "for": "controlify:use", + "if": { + "slot": "controlify:off_hand", + "item": { + "predicates": { + "minecraft:instrument": {} + } + } + }, + "then": { + "type": "feedback", + "position": 3, + "strength": 3 + } + }, + { + "for": "controlify:use", + "if": { + "slot": "controlify:off_hand", + "item": { + "items": "#c:tools/brush" + } + }, + "then": { + "type": "vibration", + "position": 2, + "amplitude": 2, + "frequency": 15 + } + }, + { + "for": "controlify:use", + "if": { + "slot": "controlify:off_hand", + "item": { + "items": "minecraft:spyglass" + } + }, + "then": { + "type": "feedback", + "position": 3, + "strength": 1 + } + }, + { + "for": "controlify:use", + "if": { + "slot": "controlify:off_hand", + "item": { + "items": "#c:tools/trident" + } + }, + "then": { + "type": "feedback_slope", + "start_position": 2, + "end_position": 9, + "start_strength": 2, + "end_strength": 8 + } + }, + { + "for": "controlify:use", + "if": { + "slot": "controlify:off_hand", + "item": { + "items": "#minecraft:eggs" + } + }, + "then": { + "type": "vibration", + "position": 3, + "amplitude": 6, + "frequency": 5 + } + }, + { + "for": "controlify:use", + "if": { + "slot": "controlify:off_hand", + "item": { + "items": [ + "minecraft:snowball", + "minecraft:splash_potion", + "minecraft:lingering_potion", + "minecraft:experience_bottle" + ] + } + }, + "then": { + "type": "vibration", + "position": 3, + "amplitude": 6, + "frequency": 5 + } + }, + { + "for": "controlify:use", + "if": { + "slot": "controlify:off_hand", + "item": { + "items": "#c:tools/fishing_rod" + } + }, + "then": { + "type": "weapon", + "start_position": 3, + "end_position": 5, + "strength": 1 + } + }, + { + "for": "controlify:use", + "if": { + "slot": "controlify:off_hand", + "item": { + "predicates": { + "minecraft:bundle_contents": { + "items": { + "size": { + "min": 1 + } + } + } + } + } + }, + "then": { + "type": "feedback", + "position": 3, + "strength": 6 + } + }, + { + "for": "controlify:use", + "if": { + "slot": "controlify:off_hand", + "item": { + "predicates": { + "minecraft:use_cooldown": {} + } + } + }, + "then": { + "type": "weapon", + "start_position": 3, + "end_position": 5, + "strength": 1 + } + }, + { + "for": "controlify:attack", + "if": { + "slot": "controlify:main_hand", + "item": { + "predicates": { + "minecraft:weapon": {} + } + } + }, + "then": { + "type": "weapon", + "start_position": 3, + "end_position": 5, + "strength": 1 + } + } + ] +} diff --git a/src/main/resources/assets/controlify/guides/in_game.json b/src/main/resources/assets/controlify/guides/in_game.json deleted file mode 100644 index 800e6ada7..000000000 --- a/src/main/resources/assets/controlify/guides/in_game.json +++ /dev/null @@ -1,200 +0,0 @@ -{ - "rules": [ - { - "for": "controlify:jump", - "where": "left", - "when": ["controlify:riding_happy_ghast"], - "then": { "translate": "controlify.guide.ingame.fly_up" } - }, - { - "for": "controlify:jump", - "where": "left", - "when": ["controlify:riding_saddled_horse"], - "then": { "translate": "key.jump" } - }, - { - "for": "controlify:jump", - "where": "left", - "when": ["controlify:flying"], - "then": { "translate": "controlify.guide.ingame.fly_up" } - }, - { - "for": "controlify:jump", - "where": "left", - "when": ["controlify:in_water"], - "then": { "translate": "controlify.guide.ingame.swim_up" } - }, - { - "for": "controlify:jump", - "where": "left", - "when": ["controlify:on_ground"], - "then": { "translate": "key.jump" } - }, - { - "for": "controlify:jump", - "where": "left", - "when": ["controlify:can_elytra_fly"], - "then": { "translate": "controlify.guide.ingame.start_elytra" } - }, - - { - "for": "controlify:sneak", - "where": "left", - "when": ["controlify:in_vehicle"], - "then": { "translate": "controlify.guide.ingame.dismount" } - }, - { - "for": "controlify:sneak", - "where": "left", - "when": ["controlify:flying"], - "then": { "translate": "controlify.guide.ingame.fly_down" } - }, - { - "for": "controlify:sneak", - "where": "left", - "when": ["controlify:in_water"], - "forbid": ["controlify:on_ground"], - "then": { "translate": "controlify.guide.ingame.swim_down" } - }, - { - "for": "controlify:sneak", - "where": "left", - "when": ["controlify:is_toggle_sneak", "controlify:sneaking"], - "then": { "translate": "controlify.guide.ingame.stop_sneaking" } - }, - { - "for": "controlify:sneak", - "where": "left", - "when": ["controlify:is_toggle_sneak"], - "forbid": ["controlify:sneaking"], - "then": { "translate": "controlify.guide.ingame.start_sneaking" } - }, - { - "for": "controlify:sneak", - "where": "left", - "forbid": ["controlify:is_toggle_sneak", "controlify:sneaking"], - "then": { "translate": "controlify.guide.ingame.sneak" } - }, - - { - "for": "controlify:sprint", - "where": "left", - "when": ["controlify:input_moving", "controlify:under_water"], - "forbid": ["controlify:sprinting", "controlify:in_vehicle", "controlify:flying"], - "then": { "translate": "controlify.guide.ingame.start_swimming" } - }, - { - "for": "controlify:sprint", - "where": "left", - "when": ["controlify:input_moving"], - "forbid": ["controlify:sprinting", "controlify:in_vehicle"], - "then": { "translate": "controlify.guide.ingame.start_sprinting" } - }, - { - "for": "controlify:sprint", - "where": "left", - "when": ["controlify:sprinting", "controlify:is_toggle_sprint", "controlify:under_water", "controlify:in_vehicle"], - "then": { "translate": "controlify.guide.ingame.stop_swimming" } - }, - { - "for": "controlify:sprint", - "where": "left", - "when": ["controlify:sprinting", "controlify:is_toggle_sprint", "controlify:in_vehicle"], - "then": { "translate": "controlify.guide.ingame.stop_sprinting" } - }, - - - { - "for": "controlify:inventory", - "where": "right", - "then": { "translate": "controlify.guide.ingame.inventory" } - }, - - { - "for": "controlify:radial_menu", - "where": "right", - "then": { "translate": "controlify.gui.radial_menu" } - }, - - { - "for": "controlify:attack", - "where": "right", - "when": ["controlify:looking_at_entity", "controlify:is_spectator"], - "then": { "translate": "controlify.guide.ingame.spectate" } - }, - { - "for": "controlify:attack", - "where": "right", - "when": ["controlify:looking_at_entity"], - "forbid": ["controlify:is_spectator"], - "then": { "translate": "controlify.guide.ingame.attack" } - }, - { - "for": "controlify:attack", - "where": "right", - "when": ["controlify:looking_at_block"], - "then": { "translate": "controlify.guide.ingame.break" } - }, - - { - "for": "controlify:use", - "where": "right", - "when": ["controlify:looking_at_entity", "controlify:is_spectator"], - "then": { "translate": "controlify.guide.ingame.spectate" } - }, - { - "for": "controlify:use", - "where": "right", - "when": ["controlify:looking_at_entity"], - "forbid": ["controlify:is_spectator"], - "then": { "translate": "controlify.guide.ingame.interact" } - }, - { - "for": "controlify:use", - "where": "right", - "when": ["controlify:looking_at_block"], - "then": { "translate": "controlify.guide.ingame.use" } - }, - { - "for": "controlify:use", - "where": "right", - "when": ["controlify:has_item_in_either_hand"], - "then": { "translate": "controlify.guide.ingame.use" } - }, - - { - "for": "controlify:drop", - "where": "right", - "when": ["controlify:has_item_in_mainhand"], - "then": { "translate": "controlify.guide.ingame.drop" } - }, - - { - "for": "controlify:drop_stack", - "where": "right", - "when": ["controlify:has_multiple_items_in_hand"], - "then": { "translate": "controlify.binding.controlify.drop_stack" } - }, - - { - "for": "controlify:swap_hands", - "where": "right", - "when": ["controlify:has_item_in_either_hand"], - "then": { "translate": "controlify.guide.ingame.swap_hands" } - }, - - { - "for": "controlify:pick_block", - "where": "right", - "when": ["controlify:looking_at_block", "controlify:is_creative"], - "then": { "translate": "controlify.guide.ingame.pick_block" } - }, - - { - "for": "controlify:pick_block_nbt", - "where": "right", - "when": ["controlify:looking_at_block", "controlify:is_creative"], - "then": { "translate": "controlify.binding.controlify.pick_block_nbt" } - } - ] -} diff --git a/src/main/resources/assets/controlify/lang/en_us.json b/src/main/resources/assets/controlify/lang/en_us.json index d78e2bdb0..db3638dff 100644 --- a/src/main/resources/assets/controlify/lang/en_us.json +++ b/src/main/resources/assets/controlify/lang/en_us.json @@ -499,6 +499,40 @@ "controlify.guide.ingame.spectate": "Spectate", "controlify.guide.ingame.interact": "Interact", "controlify.guide.ingame.pick_block": "Pick Block", + "controlify.guide.ingame.trade": "Trade", + "controlify.guide.ingame.ride": "Ride", + "controlify.guide.ingame.open": "Open", + "controlify.guide.ingame.close": "Close", + "controlify.guide.ingame.press": "Press", + "controlify.guide.ingame.toggle": "Toggle", + "controlify.guide.ingame.ring": "Ring", + "controlify.guide.ingame.sleep": "Sleep", + "controlify.guide.ingame.craft": "Craft", + "controlify.guide.ingame.smelt": "Smelt", + "controlify.guide.ingame.cook": "Cook", + "controlify.guide.ingame.brew": "Brew", + "controlify.guide.ingame.enchant": "Enchant", + "controlify.guide.ingame.smith": "Smith", + "controlify.guide.ingame.repair": "Repair", + "controlify.guide.ingame.cut_stone": "Cut Stone", + "controlify.guide.ingame.weave": "Weave", + "controlify.guide.ingame.edit_map": "Edit Map", + "controlify.guide.ingame.block": "Block", + "controlify.guide.ingame.consume": "Consume", + "controlify.guide.ingame.shoot": "Shoot", + "controlify.guide.ingame.load": "Load", + "controlify.guide.ingame.throw": "Throw", + "controlify.guide.ingame.equip": "Equip", + "controlify.guide.ingame.brush": "Brush", + "controlify.guide.ingame.zoom": "Zoom", + "controlify.guide.ingame.play": "Play", + "controlify.guide.ingame.place": "Place", + "controlify.guide.ingame.cast": "Cast", + "controlify.guide.ingame.reel_in": "Reel In", + "controlify.guide.ingame.mine": "Mine", + "controlify.guide.ingame.dash": "Dash", + "controlify.guide.ingame.climb_up": "Climb Up", + "controlify.guide.ingame.climb_down": "Climb Down", "controlify.guide.container.place_all": "Place All", "controlify.guide.container.place_one": "Place", @@ -629,7 +663,7 @@ "controlify.extra_pack.legacy_console.name": "Legacy Console", - "controlify.extra_pack.legacy_console.desc": "Controlify - sets bind defaults/glyphs.", + "controlify.extra_pack.legacy_console.desc": "Applies Legacy Console-style bindings, glyphs, settings, and button guides.", "controlify.keyboard.special.shift": "Shift", "controlify.keyboard.special.shift_lock": "Shift Lock", diff --git a/src/main/resources/assets/controlify/trigger_effect/swing_item.json b/src/main/resources/assets/controlify/trigger_effect/swing_item.json deleted file mode 100644 index 76d92c043..000000000 --- a/src/main/resources/assets/controlify/trigger_effect/swing_item.json +++ /dev/null @@ -1,15 +0,0 @@ -[ - { - "when": { - "predicates": { - "minecraft:weapon": {} - } - }, - "effect": { - "type": "weapon", - "start_position": 3, - "end_position": 5, - "strength": 1 - } - } -] diff --git a/src/main/resources/assets/controlify/trigger_effect/use_item.json b/src/main/resources/assets/controlify/trigger_effect/use_item.json deleted file mode 100644 index cb3789bf3..000000000 --- a/src/main/resources/assets/controlify/trigger_effect/use_item.json +++ /dev/null @@ -1,200 +0,0 @@ -[ - { - "when": { - "items": "#c:tools/bow" - }, - "effect": { - "type": "feedback_slope", - "start_position": 3, - "end_position": 9, - "start_strength": 2, - "end_strength": 8 - } - }, - { - "when": { - "components": { - "minecraft:charged_projectiles": [] - } - }, - "effect": { - "type": "feedback_slope", - "start_position": 2, - "end_position": 9, - "start_strength": 5, - "end_strength": 8 - } - }, - { - "when": { - "predicates": { - "minecraft:charged_projectiles": {} - } - }, - "effect": { - "type": "weapon", - "start_position": 3, - "end_position": 5, - "strength": 1 - } - }, - { - "when": { - "predicates": { - "minecraft:consumable": {} - } - }, - "effect": { - "type": "feedback", - "position": 3, - "strength": 1 - } - }, - { - "when": { - "predicates": { - "minecraft:blocks_attacks": {} - } - }, - "effect": { - "type": "feedback", - "position": 3, - "strength": 3 - } - }, - { - "when": { - "predicates": { - "minecraft:equippable": {} - } - }, - "effect": { - "type": "weapon", - "start_position": 3, - "end_position": 5, - "strength": 1 - } - }, - { - "when": { - "predicates": { - "minecraft:kinetic_weapon": {} - } - }, - "effect": { - "type": "feedback", - "position": 3, - "strength": 3 - } - }, - { - "when": { - "predicates": { - "minecraft:instrument": {} - } - }, - "effect": { - "type": "feedback", - "position": 3, - "strength": 3 - } - }, - { - "when": { - "items": "#c:tools/brush" - }, - "effect": { - "type": "vibration", - "position": 2, - "amplitude": 2, - "frequency": 15 - } - }, - { - "when": { - "items": "minecraft:spyglass" - }, - "effect": { - "type": "feedback", - "position": 3, - "strength": 1 - } - }, - { - "when": { - "items": "#c:tools/trident" - }, - "effect": { - "type": "feedback_slope", - "start_position": 2, - "end_position": 9, - "start_strength": 2, - "end_strength": 8 - } - }, - { - "when": { - "items": "#minecraft:eggs" - }, - "effect": { - "type": "vibration", - "position": 3, - "amplitude": 6, - "frequency": 5 - } - }, - { - "when": { - "items": [ - "minecraft:snowball", - "minecraft:splash_potion", - "minecraft:lingering_potion", - "minecraft:experience_bottle" - ] - }, - "effect": { - "type": "vibration", - "position": 3, - "amplitude": 6, - "frequency": 5 - } - }, - { - "when": { - "items": "#c:tools/fishing_rod" - }, - "effect": { - "type": "weapon", - "start_position": 3, - "end_position": 5, - "strength": 1 - } - }, - { - "when": { - "predicates": { - "minecraft:bundle_contents": { - "items": { "size": { "min": 1 } } - } - } - }, - "effect": { - "type": "feedback", - "position": 3, - "strength": 6 - } - }, - { - "when": { - "predicates": { - "minecraft:use_cooldown": {} - } - }, - "effect": { - "type": "weapon", - "start_position": 3, - "end_position": 5, - "strength": 1 - } - } -] diff --git a/src/main/resources/resourcepacks/legacy_console/assets/controlify/contextual/guide/in_game.json b/src/main/resources/resourcepacks/legacy_console/assets/controlify/contextual/guide/in_game.json new file mode 100644 index 000000000..99a4c520c --- /dev/null +++ b/src/main/resources/resourcepacks/legacy_console/assets/controlify/contextual/guide/in_game.json @@ -0,0 +1,1194 @@ +{ + "replace": true, + "rules": [ + { + "for": "controlify:jump", + "where": "left", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:riding_happy_ghast" + ] + }, + "then": { + "translate": "controlify.guide.ingame.fly_up" + } + }, + { + "for": "controlify:jump", + "where": "left", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { + "any_of": [ + "controlify:riding_saddled_camel", + "controlify:riding_saddled_nautilus" + ] + } + ] + }, + "then": { + "translate": "controlify.guide.ingame.dash" + } + }, + { + "for": "controlify:jump", + "where": "left", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:riding_saddled_equine" + ] + }, + "then": { + "translate": "key.jump" + } + }, + { + "for": "controlify:jump", + "where": "left", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:creative_flying" + ] + }, + "then": { + "translate": "controlify.guide.ingame.fly_up" + } + }, + { + "for": "controlify:jump", + "where": "left", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:in_water" + ] + }, + "then": { + "translate": "controlify.guide.ingame.swim_up" + } + }, + { + "for": "controlify:jump", + "where": "left", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:on_climbable" + ] + }, + "then": { + "translate": "controlify.guide.ingame.climb_up" + } + }, + { + "for": "controlify:jump", + "where": "left", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:on_ground" + ] + }, + "then": { + "translate": "key.jump" + } + }, + { + "for": "controlify:jump", + "where": "left", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:can_elytra_fly" + ] + }, + "then": { + "translate": "controlify.guide.ingame.start_elytra" + } + }, + { + "for": "controlify:sneak", + "where": "left", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:in_vehicle" + ] + }, + "then": { + "translate": "controlify.guide.ingame.dismount" + } + }, + { + "for": "controlify:sneak", + "where": "left", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:creative_flying" + ] + }, + "then": { + "translate": "controlify.guide.ingame.fly_down" + } + }, + { + "for": "controlify:sneak", + "where": "left", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:in_water" + ], + "none_of": [ + "controlify:on_ground" + ] + }, + "then": { + "translate": "controlify.guide.ingame.swim_down" + } + }, + { + "for": "controlify:sneak", + "where": "left", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:on_climbable" + ] + }, + "then": { + "translate": "controlify.guide.ingame.climb_down" + } + }, + { + "for": "controlify:inventory", + "where": "right", + "if": "controlify:verbosity_reduced", + "then": { + "translate": "controlify.guide.ingame.inventory" + } + }, + { + "for": "controlify:attack", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:looking_at_entity", + "controlify:is_spectator" + ] + }, + "then": { + "translate": "controlify.guide.ingame.spectate" + } + }, + { + "for": "controlify:attack", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:looking_at_entity" + ], + "none_of": [ + "controlify:is_spectator" + ] + }, + "then": { + "translate": "controlify.guide.ingame.attack" + } + }, + { + "for": "controlify:attack", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:looking_at_block", + "controlify:main_hand_is_tool" + ] + }, + "then": { + "translate": "controlify.guide.ingame.mine" + } + }, + { + "for": "controlify:attack", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:looking_at_block" + ] + }, + "then": { + "translate": "controlify.guide.ingame.break" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:looking_at_entity", + "controlify:is_spectator" + ] + }, + "then": { + "translate": "controlify.guide.ingame.spectate" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:looking_at_merchant" + ] + }, + "then": { + "translate": "controlify.guide.ingame.trade" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:looking_at_chest_boat", + "controlify:sneaking" + ] + }, + "then": { + "translate": "controlify.guide.ingame.open" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:looking_at_container_minecart" + ] + }, + "then": { + "translate": "controlify.guide.ingame.open" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:looking_at_rideable" + ], + "none_of": [ + "controlify:sneaking" + ] + }, + "then": { + "translate": "controlify.guide.ingame.ride" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:looking_at_closed_openable" + ], + "none_of": [ + "controlify:sneaking" + ] + }, + "then": { + "translate": "controlify.guide.ingame.open" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:looking_at_open_openable" + ], + "none_of": [ + "controlify:sneaking" + ] + }, + "then": { + "translate": "controlify.guide.ingame.close" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:looking_at_container" + ], + "none_of": [ + "controlify:sneaking" + ] + }, + "then": { + "translate": "controlify.guide.ingame.open" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:looking_at_button" + ] + }, + "then": { + "translate": "controlify.guide.ingame.press" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:looking_at_lever" + ] + }, + "then": { + "translate": "controlify.guide.ingame.toggle" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:looking_at_bell" + ] + }, + "then": { + "translate": "controlify.guide.ingame.ring" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:looking_at_bed" + ], + "none_of": [ + "controlify:sneaking" + ] + }, + "then": { + "translate": "controlify.guide.ingame.sleep" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:looking_at_crafting_table" + ], + "none_of": [ + "controlify:sneaking" + ] + }, + "then": { + "translate": "controlify.guide.ingame.craft" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:looking_at_furnace" + ], + "none_of": [ + "controlify:sneaking" + ] + }, + "then": { + "translate": "controlify.guide.ingame.smelt" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:looking_at_smoker" + ], + "none_of": [ + "controlify:sneaking" + ] + }, + "then": { + "translate": "controlify.guide.ingame.cook" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:looking_at_brewing_stand" + ], + "none_of": [ + "controlify:sneaking" + ] + }, + "then": { + "translate": "controlify.guide.ingame.brew" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:looking_at_enchanting_table" + ], + "none_of": [ + "controlify:sneaking" + ] + }, + "then": { + "translate": "controlify.guide.ingame.enchant" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:looking_at_smithing_table" + ], + "none_of": [ + "controlify:sneaking" + ] + }, + "then": { + "translate": "controlify.guide.ingame.smith" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:looking_at_repair_station" + ], + "none_of": [ + "controlify:sneaking" + ] + }, + "then": { + "translate": "controlify.guide.ingame.repair" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:looking_at_stonecutter" + ], + "none_of": [ + "controlify:sneaking" + ] + }, + "then": { + "translate": "controlify.guide.ingame.cut_stone" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:looking_at_loom" + ], + "none_of": [ + "controlify:sneaking" + ] + }, + "then": { + "translate": "controlify.guide.ingame.weave" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:looking_at_cartography_table" + ], + "none_of": [ + "controlify:sneaking" + ] + }, + "then": { + "translate": "controlify.guide.ingame.edit_map" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { + "slot": "controlify:main_hand", + "item": { + "items": "#c:tools/bow" + } + } + ] + }, + "then": { + "translate": "controlify.guide.ingame.shoot" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { + "slot": "controlify:main_hand", + "item": { + "predicates": { + "minecraft:charged_projectiles": {} + } + } + } + ], + "none_of": [ + { + "slot": "controlify:main_hand", + "item": { + "components": { + "minecraft:charged_projectiles": [] + } + } + } + ] + }, + "then": { + "translate": "controlify.guide.ingame.shoot" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { + "slot": "controlify:main_hand", + "item": { + "items": "minecraft:crossbow", + "components": { + "minecraft:charged_projectiles": [] + } + } + } + ] + }, + "then": { + "translate": "controlify.guide.ingame.load" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { + "slot": "controlify:main_hand", + "item": { + "predicates": { + "minecraft:blocks_attacks": {} + } + } + } + ] + }, + "then": { + "translate": "controlify.guide.ingame.block" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { + "slot": "controlify:main_hand", + "item": { + "predicates": { + "minecraft:consumable": {} + } + } + } + ] + }, + "then": { + "translate": "controlify.guide.ingame.consume" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { + "any_of": [ + { + "slot": "controlify:main_hand", + "item": { + "items": "#c:tools/trident" + } + }, + { + "slot": "controlify:main_hand", + "item": { + "items": "#minecraft:spears" + } + }, + { + "slot": "controlify:main_hand", + "item": { + "items": "#minecraft:eggs" + } + }, + { + "slot": "controlify:main_hand", + "item": { + "items": [ + "minecraft:snowball", + "minecraft:splash_potion", + "minecraft:lingering_potion", + "minecraft:experience_bottle", + "minecraft:ender_pearl", + "minecraft:wind_charge" + ] + } + } + ] + } + ] + }, + "then": { + "translate": "controlify.guide.ingame.throw" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:fishing_hook_cast", + { + "slot": "controlify:main_hand", + "item": { + "items": "#c:tools/fishing_rod" + } + } + ] + }, + "then": { + "translate": "controlify.guide.ingame.reel_in" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { + "slot": "controlify:main_hand", + "item": { + "items": "#c:tools/fishing_rod" + } + } + ], + "none_of": [ + "controlify:fishing_hook_cast" + ] + }, + "then": { + "translate": "controlify.guide.ingame.cast" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { + "slot": "controlify:main_hand", + "item": { + "items": "#c:tools/brush" + } + } + ] + }, + "then": { + "translate": "controlify.guide.ingame.brush" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { + "slot": "controlify:main_hand", + "item": { + "items": "minecraft:spyglass" + } + } + ] + }, + "then": { + "translate": "controlify.guide.ingame.zoom" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { + "slot": "controlify:main_hand", + "item": { + "predicates": { + "minecraft:instrument": {} + } + } + } + ] + }, + "then": { + "translate": "controlify.guide.ingame.play" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { + "slot": "controlify:main_hand", + "item": { + "predicates": { + "minecraft:equippable": {} + } + } + } + ] + }, + "then": { + "translate": "controlify.guide.ingame.equip" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:looking_at_block", + "controlify:main_hand_is_block_item" + ] + }, + "then": { + "translate": "controlify.guide.ingame.place" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { + "slot": "controlify:off_hand", + "item": { + "items": "#c:tools/bow" + } + } + ] + }, + "then": { + "translate": "controlify.guide.ingame.shoot" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { + "slot": "controlify:off_hand", + "item": { + "predicates": { + "minecraft:charged_projectiles": {} + } + } + } + ], + "none_of": [ + { + "slot": "controlify:off_hand", + "item": { + "components": { + "minecraft:charged_projectiles": [] + } + } + } + ] + }, + "then": { + "translate": "controlify.guide.ingame.shoot" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { + "slot": "controlify:off_hand", + "item": { + "items": "minecraft:crossbow", + "components": { + "minecraft:charged_projectiles": [] + } + } + } + ] + }, + "then": { + "translate": "controlify.guide.ingame.load" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { + "slot": "controlify:off_hand", + "item": { + "predicates": { + "minecraft:blocks_attacks": {} + } + } + } + ] + }, + "then": { + "translate": "controlify.guide.ingame.block" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { + "slot": "controlify:off_hand", + "item": { + "predicates": { + "minecraft:consumable": {} + } + } + } + ] + }, + "then": { + "translate": "controlify.guide.ingame.consume" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { + "any_of": [ + { + "slot": "controlify:off_hand", + "item": { + "items": "#c:tools/trident" + } + }, + { + "slot": "controlify:off_hand", + "item": { + "items": "#minecraft:spears" + } + }, + { + "slot": "controlify:off_hand", + "item": { + "items": "#minecraft:eggs" + } + }, + { + "slot": "controlify:off_hand", + "item": { + "items": [ + "minecraft:snowball", + "minecraft:splash_potion", + "minecraft:lingering_potion", + "minecraft:experience_bottle", + "minecraft:ender_pearl", + "minecraft:wind_charge" + ] + } + } + ] + } + ] + }, + "then": { + "translate": "controlify.guide.ingame.throw" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:fishing_hook_cast", + { + "slot": "controlify:off_hand", + "item": { + "items": "#c:tools/fishing_rod" + } + } + ] + }, + "then": { + "translate": "controlify.guide.ingame.reel_in" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { + "slot": "controlify:off_hand", + "item": { + "items": "#c:tools/fishing_rod" + } + } + ], + "none_of": [ + "controlify:fishing_hook_cast" + ] + }, + "then": { + "translate": "controlify.guide.ingame.cast" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { + "slot": "controlify:off_hand", + "item": { + "items": "#c:tools/brush" + } + } + ] + }, + "then": { + "translate": "controlify.guide.ingame.brush" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { + "slot": "controlify:off_hand", + "item": { + "items": "minecraft:spyglass" + } + } + ] + }, + "then": { + "translate": "controlify.guide.ingame.zoom" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { + "slot": "controlify:off_hand", + "item": { + "predicates": { + "minecraft:instrument": {} + } + } + } + ] + }, + "then": { + "translate": "controlify.guide.ingame.play" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + { + "slot": "controlify:off_hand", + "item": { + "predicates": { + "minecraft:equippable": {} + } + } + } + ] + }, + "then": { + "translate": "controlify.guide.ingame.equip" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:looking_at_block", + "controlify:off_hand_is_block_item" + ] + }, + "then": { + "translate": "controlify.guide.ingame.place" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:looking_at_entity" + ], + "none_of": [ + "controlify:is_spectator" + ] + }, + "then": { + "translate": "controlify.guide.ingame.interact" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:looking_at_block" + ] + }, + "then": { + "translate": "controlify.guide.ingame.use" + } + }, + { + "for": "controlify:use", + "where": "right", + "if": { + "all_of": [ + "controlify:verbosity_minimal", + "controlify:has_item_in_either_hand" + ] + }, + "then": { + "translate": "controlify.guide.ingame.use" + } + }, + { + "for": "controlify:drop", + "where": "left", + "if": { + "all_of": [ + "controlify:verbosity_reduced", + "controlify:has_item_in_main_hand" + ] + }, + "then": { + "translate": "controlify.guide.ingame.drop" + } + } + ] +} From 9aa910d828c5e348f4e8b73442cdf6c8cb762157 Mon Sep 17 00:00:00 2001 From: isxander Date: Fri, 21 Aug 2026 16:31:53 +0100 Subject: [PATCH 4/7] fix bugs in contextual api --- .../contextual/ContextualPredicate.java | 3 +- .../contextual/FactDependencyGraph.java | 48 ++++++++++--- .../dualsense/TriggerEffectInstanceImpl.java | 9 ++- .../predicates/ClientEntityPredicate.java | 2 +- .../predicates/ClientPredicateUtils.java | 7 +- .../contextual/FactDependencyGraphTest.java | 72 +++++++++++++++++++ 6 files changed, 124 insertions(+), 17 deletions(-) create mode 100644 src/test/java/dev/isxander/controlify/contextual/FactDependencyGraphTest.java diff --git a/src/main/java/dev/isxander/controlify/contextual/ContextualPredicate.java b/src/main/java/dev/isxander/controlify/contextual/ContextualPredicate.java index 2b01924a0..46a929140 100644 --- a/src/main/java/dev/isxander/controlify/contextual/ContextualPredicate.java +++ b/src/main/java/dev/isxander/controlify/contextual/ContextualPredicate.java @@ -6,6 +6,7 @@ import dev.isxander.controlify.utils.codec.CExtraCodecs; import dev.isxander.controlify.utils.predicates.ClientEntityPredicate; //? if >=26.2 { +import dev.isxander.controlify.utils.predicates.ClientPredicateUtils; import net.minecraft.advancements.predicates.BlockPredicate; import net.minecraft.advancements.predicates.ItemPredicate; //?} else { @@ -122,7 +123,7 @@ public boolean matches(ContextualState state) { if (itemInstance == null) { return false; } - return item().test(itemInstance); + return ClientPredicateUtils.matches(item(), itemInstance); } @Override diff --git a/src/main/java/dev/isxander/controlify/contextual/FactDependencyGraph.java b/src/main/java/dev/isxander/controlify/contextual/FactDependencyGraph.java index 77fc7938d..bdfdae8c7 100644 --- a/src/main/java/dev/isxander/controlify/contextual/FactDependencyGraph.java +++ b/src/main/java/dev/isxander/controlify/contextual/FactDependencyGraph.java @@ -14,12 +14,14 @@ import java.util.Collections; import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; /// A compiled evaluation plan for data-defined facts. /// @@ -33,6 +35,7 @@ public final class FactDependencyGraph { private final Map> dependencies; private final List evaluationOrder; private final Set externalDependencies; + private final Map, List> evaluationPlans = new ConcurrentHashMap<>(); private FactDependencyGraph( Map definitions, @@ -114,7 +117,7 @@ public Set externalDependencies() { return this.externalDependencies; } - /// Resolves required data-defined facts on top of a source state. + /// Resolves required data-defined facts and their transitive dependencies on top of a source state. public ContextualState evaluate(ContextualStateAccumulator state, @Nullable Collection requiredFacts) { return this.evaluate(state, requiredFacts, null); } @@ -125,21 +128,44 @@ ContextualState evaluate( @Nullable Identifier debugDomain ) { ContextualState view = state.view(); - for (Identifier id : this.evaluationOrder()) { - if (requiredFacts == null || requiredFacts.contains(id)) { - FactDefinition definition = this.definitions().get(id); - if (ContextualDebug.enabled() && debugDomain != null) { - ContextualDebug.PredicateEvaluation evaluation = ContextualDebug.evaluatePredicate(definition.predicate(), view); - state.contributeFact(id, evaluation.result()); - ContextualDebug.logFact(debugDomain, id, this.dependencies.get(id), evaluation); - } else { - state.contributeFact(id, definition.predicate().matches(view)); - } + for (Identifier id : this.evaluationPlan(requiredFacts)) { + FactDefinition definition = this.definitions().get(id); + if (debugDomain != null && ContextualDebug.enabled()) { + ContextualDebug.PredicateEvaluation evaluation = ContextualDebug.evaluatePredicate(definition.predicate(), view); + state.contributeFact(id, evaluation.result()); + ContextualDebug.logFact(debugDomain, id, this.dependencies.get(id), evaluation); + } else { + state.contributeFact(id, definition.predicate().matches(view)); } } return state.snapshot(); } + private List evaluationPlan(@Nullable Collection requiredFacts) { + if (requiredFacts == null) { + return this.evaluationOrder; + } + if (requiredFacts.isEmpty() || this.evaluationOrder.isEmpty()) { + return List.of(); + } + + return this.evaluationPlans.computeIfAbsent(Set.copyOf(requiredFacts), this::createEvaluationPlan); + } + + private List createEvaluationPlan(Set requiredFacts) { + Set requiredClosure = new HashSet<>(requiredFacts); + for (int index = this.evaluationOrder.size() - 1; index >= 0; index--) { + Identifier id = this.evaluationOrder.get(index); + if (requiredClosure.contains(id)) { + requiredClosure.addAll(this.dependencies.get(id)); + } + } + + return this.evaluationOrder.stream() + .filter(requiredClosure::contains) + .toList(); + } + private static final class Compiler { private final Map definitions; private final Map> dependencies; diff --git a/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectInstanceImpl.java b/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectInstanceImpl.java index 748111fb2..aface8205 100644 --- a/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectInstanceImpl.java +++ b/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectInstanceImpl.java @@ -1,5 +1,6 @@ package dev.isxander.controlify.controller.dualsense; +import dev.isxander.controlify.api.bind.InputBinding; import dev.isxander.controlify.api.contextual.Context; import dev.isxander.controlify.api.contextual.TriggerEffectInstance; import dev.isxander.controlify.contextual.ContextualDomainImpl; @@ -72,7 +73,13 @@ public DualsenseTriggerEffect getRightTriggerEffect() { } private Trigger getTrigger(ControllerEntity controller, TriggerEffectRule rule) { - List relevantInputs = rule.binding().on(controller).boundInput().getRelevantInputs(); + InputBinding binding = rule.binding().onOrNull(controller); + + if (binding == null) { + return Trigger.NEITHER; + } + + List relevantInputs = binding.boundInput().getRelevantInputs(); if (relevantInputs.contains(GamepadInputs.LEFT_TRIGGER_AXIS)) { return Trigger.LEFT; diff --git a/src/main/java/dev/isxander/controlify/utils/predicates/ClientEntityPredicate.java b/src/main/java/dev/isxander/controlify/utils/predicates/ClientEntityPredicate.java index 6900aaf9e..1d71ea306 100644 --- a/src/main/java/dev/isxander/controlify/utils/predicates/ClientEntityPredicate.java +++ b/src/main/java/dev/isxander/controlify/utils/predicates/ClientEntityPredicate.java @@ -82,7 +82,7 @@ public boolean matches(@Nullable Vec3 position, Entity entity) { return false; } } else if (this.distanceToPlayer.isPresent() - && this.distanceToPlayer.get().matches(position.x, position.y, position.z, entity.getX(), entity.getY(), entity.getZ())) { + && !this.distanceToPlayer.get().matches(position.x, position.y, position.z, entity.getX(), entity.getY(), entity.getZ())) { return false; } diff --git a/src/main/java/dev/isxander/controlify/utils/predicates/ClientPredicateUtils.java b/src/main/java/dev/isxander/controlify/utils/predicates/ClientPredicateUtils.java index 97ccad12c..82c9d6bf1 100644 --- a/src/main/java/dev/isxander/controlify/utils/predicates/ClientPredicateUtils.java +++ b/src/main/java/dev/isxander/controlify/utils/predicates/ClientPredicateUtils.java @@ -12,6 +12,7 @@ import net.minecraft.world.entity.SlotAccess; import net.minecraft.world.entity.SlotProvider; import net.minecraft.world.inventory.SlotRange; +import net.minecraft.world.item.ItemInstance; import net.minecraft.world.item.ItemStack; import java.util.Map; @@ -25,11 +26,11 @@ public static boolean matches(EntityTypePredicate predicate, Entity entity) { return CUtil.isInWithLocalFallback(entity, predicate.types()); } - public static boolean matches(ItemPredicate predicate, ItemStack stack) { - if (predicate.items().isPresent() && !CUtil.isInWithLocalFallback(stack, predicate.items().get())) { + public static boolean matches(ItemPredicate predicate, ItemInstance item) { + if (predicate.items().isPresent() && !CUtil.isInWithLocalFallback(item, predicate.items().get())) { return false; } else { - return predicate.count().matches(stack.count()) && predicate.components().test(stack); + return predicate.count().matches(item.count()) && predicate.components().test(item); } } diff --git a/src/test/java/dev/isxander/controlify/contextual/FactDependencyGraphTest.java b/src/test/java/dev/isxander/controlify/contextual/FactDependencyGraphTest.java new file mode 100644 index 000000000..02cd302fa --- /dev/null +++ b/src/test/java/dev/isxander/controlify/contextual/FactDependencyGraphTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ +package dev.isxander.controlify.contextual; + +import net.minecraft.resources.Identifier; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class FactDependencyGraphTest { + private static final Identifier ROOT = id("root"); + private static final Identifier MIDDLE = id("middle"); + private static final Identifier LEAF = id("leaf"); + private static final Identifier UNUSED = id("unused"); + private static final Identifier SOURCE = id("source"); + private static final Identifier FROM_SOURCE = id("from_source"); + + @Test + void evaluatesTransitiveDependenciesBeforeRequiredFact() { + FactDependencyGraph graph = FactDependencyGraph.compile(List.of( + new FactDefinition(LEAF, new ContextualPredicate.Fact(MIDDLE)), + new FactDefinition(MIDDLE, new ContextualPredicate.Fact(ROOT)), + new FactDefinition(ROOT, new ContextualPredicate.Static(true)), + new FactDefinition(UNUSED, new ContextualPredicate.Static(true)) + )); + + ContextualState state = graph.evaluate(new ContextualStateAccumulator(), Set.of(LEAF)); + + assertTrue(state.facts().get(ROOT)); + assertTrue(state.facts().get(MIDDLE)); + assertTrue(state.facts().get(LEAF)); + assertFalse(state.facts().containsKey(UNUSED)); + } + + @Test + void resolvesExternalDependenciesFromSourceState() { + FactDependencyGraph graph = FactDependencyGraph.compile(List.of( + new FactDefinition(FROM_SOURCE, new ContextualPredicate.Fact(SOURCE)) + )); + ContextualStateAccumulator accumulator = new ContextualStateAccumulator(); + accumulator.contributeFact(SOURCE, true); + + ContextualState state = graph.evaluate(accumulator, Set.of(FROM_SOURCE)); + + assertEquals(Set.of(SOURCE, FROM_SOURCE), state.facts().keySet()); + assertTrue(state.facts().get(FROM_SOURCE)); + } + + @Test + void evaluatesNoDefinitionsWhenNoneAreRequired() { + FactDependencyGraph graph = FactDependencyGraph.compile(List.of( + new FactDefinition(ROOT, new ContextualPredicate.Static(true)) + )); + + ContextualState state = graph.evaluate(new ContextualStateAccumulator(), Set.of()); + + assertTrue(state.facts().isEmpty()); + } + + private static Identifier id(String path) { + return Identifier.fromNamespaceAndPath("test", path); + } +} From 0a70750af093b9d28fec6904a28508f1408819db Mon Sep 17 00:00:00 2001 From: isxander Date: Fri, 21 Aug 2026 16:39:14 +0100 Subject: [PATCH 5/7] spotless apply --- .../controlify/gametest/tests/ButtonGuideTests.java | 4 ++-- .../dev/isxander/controlify/api/contextual/Context.java | 6 ++++++ .../controlify/api/contextual/ContextualDomain.java | 6 ++++++ .../api/contextual/ContextualDomainRegistry.java | 8 ++++++-- .../controlify/api/contextual/ContextualInstance.java | 6 ++++++ .../api/contextual/ContextualStateContributor.java | 6 ++++++ .../controlify/api/contextual/ContextualStateSink.java | 6 ++++++ .../isxander/controlify/api/contextual/GuideInstance.java | 6 ++++++ .../controlify/api/contextual/TriggerEffectInstance.java | 6 ++++++ .../isxander/controlify/contextual/ContextualDomains.java | 6 ++++++ .../controlify/contextual/ContextualPredicate.java | 6 ++++++ .../isxander/controlify/contextual/ContextualState.java | 6 ++++++ .../contextual/ContextualStateContributors.java | 6 ++++++ .../isxander/controlify/contextual/FactDefinition.java | 6 ++++++ .../dev/isxander/controlify/contextual/GuideRule.java | 6 ++++++ .../java/dev/isxander/controlify/contextual/Rule.java | 6 ++++++ .../isxander/controlify/contextual/RuleSetManager.java | 6 ++++++ .../isxander/controlify/contextual/TriggerEffectRule.java | 6 ++++++ .../controller/dualsense/TriggerEffectInstanceImpl.java | 6 ++++++ .../isxander/controlify/gui/guide/GuideInstanceImpl.java | 6 ++++++ .../utils/predicates/ClientEntityPredicate.java | 6 ++++++ .../controlify/utils/predicates/ClientPredicateUtils.java | 6 ++++++ .../utils/predicates/EntityEquipmentPredicate.java | 6 ++++++ 23 files changed, 134 insertions(+), 4 deletions(-) diff --git a/src/gametest/java/dev/isxander/controlify/gametest/tests/ButtonGuideTests.java b/src/gametest/java/dev/isxander/controlify/gametest/tests/ButtonGuideTests.java index 188fc51d3..8dbdbf306 100644 --- a/src/gametest/java/dev/isxander/controlify/gametest/tests/ButtonGuideTests.java +++ b/src/gametest/java/dev/isxander/controlify/gametest/tests/ButtonGuideTests.java @@ -32,8 +32,8 @@ public void runTest(ClientGameTestContext context) { var controlify = new ControlifyGameTestContext(context); try (var controller = controlify.virtualControllerBuilder().withXbox().attach(); - var world = new TestUnitWorldContext(context); - var region = world.allocateRegion(5, 5, 5)) { + var world = new TestUnitWorldContext(context); + var region = world.allocateRegion(5, 5, 5)) { var villager = region.spawnWithNoFreeWill(CEntityTypes.VILLAGER, new BlockPos(1, 1, 3)); context.waitTicks(2); diff --git a/src/main/java/dev/isxander/controlify/api/contextual/Context.java b/src/main/java/dev/isxander/controlify/api/contextual/Context.java index 318cd7064..e77d7598f 100644 --- a/src/main/java/dev/isxander/controlify/api/contextual/Context.java +++ b/src/main/java/dev/isxander/controlify/api/contextual/Context.java @@ -1,3 +1,9 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ package dev.isxander.controlify.api.contextual; import dev.isxander.controlify.controller.ControllerEntity; diff --git a/src/main/java/dev/isxander/controlify/api/contextual/ContextualDomain.java b/src/main/java/dev/isxander/controlify/api/contextual/ContextualDomain.java index 6c2630f7e..fde17e2bf 100644 --- a/src/main/java/dev/isxander/controlify/api/contextual/ContextualDomain.java +++ b/src/main/java/dev/isxander/controlify/api/contextual/ContextualDomain.java @@ -1,3 +1,9 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ package dev.isxander.controlify.api.contextual; import net.minecraft.client.gui.Font; diff --git a/src/main/java/dev/isxander/controlify/api/contextual/ContextualDomainRegistry.java b/src/main/java/dev/isxander/controlify/api/contextual/ContextualDomainRegistry.java index 44b953e2c..048bd6c36 100644 --- a/src/main/java/dev/isxander/controlify/api/contextual/ContextualDomainRegistry.java +++ b/src/main/java/dev/isxander/controlify/api/contextual/ContextualDomainRegistry.java @@ -1,3 +1,9 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ package dev.isxander.controlify.api.contextual; import net.minecraft.resources.Identifier; @@ -15,5 +21,3 @@ default ContextualDomain register(Identifier domainId, Co return domain; } } - - diff --git a/src/main/java/dev/isxander/controlify/api/contextual/ContextualInstance.java b/src/main/java/dev/isxander/controlify/api/contextual/ContextualInstance.java index d4da6c24f..8e28bed75 100644 --- a/src/main/java/dev/isxander/controlify/api/contextual/ContextualInstance.java +++ b/src/main/java/dev/isxander/controlify/api/contextual/ContextualInstance.java @@ -1,3 +1,9 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ package dev.isxander.controlify.api.contextual; public interface ContextualInstance { diff --git a/src/main/java/dev/isxander/controlify/api/contextual/ContextualStateContributor.java b/src/main/java/dev/isxander/controlify/api/contextual/ContextualStateContributor.java index 2c5e4f45e..5b0df668b 100644 --- a/src/main/java/dev/isxander/controlify/api/contextual/ContextualStateContributor.java +++ b/src/main/java/dev/isxander/controlify/api/contextual/ContextualStateContributor.java @@ -1,3 +1,9 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ package dev.isxander.controlify.api.contextual; public interface ContextualStateContributor { diff --git a/src/main/java/dev/isxander/controlify/api/contextual/ContextualStateSink.java b/src/main/java/dev/isxander/controlify/api/contextual/ContextualStateSink.java index 5b2bbc983..5e02c5b92 100644 --- a/src/main/java/dev/isxander/controlify/api/contextual/ContextualStateSink.java +++ b/src/main/java/dev/isxander/controlify/api/contextual/ContextualStateSink.java @@ -1,3 +1,9 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ package dev.isxander.controlify.api.contextual; import net.minecraft.resources.Identifier; diff --git a/src/main/java/dev/isxander/controlify/api/contextual/GuideInstance.java b/src/main/java/dev/isxander/controlify/api/contextual/GuideInstance.java index ebeec305f..b8e9eebeb 100644 --- a/src/main/java/dev/isxander/controlify/api/contextual/GuideInstance.java +++ b/src/main/java/dev/isxander/controlify/api/contextual/GuideInstance.java @@ -1,3 +1,9 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ package dev.isxander.controlify.api.contextual; import net.minecraft.client.gui.GuiGraphicsExtractor; diff --git a/src/main/java/dev/isxander/controlify/api/contextual/TriggerEffectInstance.java b/src/main/java/dev/isxander/controlify/api/contextual/TriggerEffectInstance.java index 8eb1bebbc..52551b404 100644 --- a/src/main/java/dev/isxander/controlify/api/contextual/TriggerEffectInstance.java +++ b/src/main/java/dev/isxander/controlify/api/contextual/TriggerEffectInstance.java @@ -1,3 +1,9 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ package dev.isxander.controlify.api.contextual; import dev.isxander.controlify.driver.dualsense.DualsenseTriggerEffect; diff --git a/src/main/java/dev/isxander/controlify/contextual/ContextualDomains.java b/src/main/java/dev/isxander/controlify/contextual/ContextualDomains.java index db90e1e1f..f69f272b0 100644 --- a/src/main/java/dev/isxander/controlify/contextual/ContextualDomains.java +++ b/src/main/java/dev/isxander/controlify/contextual/ContextualDomains.java @@ -1,3 +1,9 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ package dev.isxander.controlify.contextual; import dev.isxander.controlify.api.contextual.ContainerContext; diff --git a/src/main/java/dev/isxander/controlify/contextual/ContextualPredicate.java b/src/main/java/dev/isxander/controlify/contextual/ContextualPredicate.java index 46a929140..93175568b 100644 --- a/src/main/java/dev/isxander/controlify/contextual/ContextualPredicate.java +++ b/src/main/java/dev/isxander/controlify/contextual/ContextualPredicate.java @@ -1,3 +1,9 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ package dev.isxander.controlify.contextual; import com.mojang.serialization.Codec; diff --git a/src/main/java/dev/isxander/controlify/contextual/ContextualState.java b/src/main/java/dev/isxander/controlify/contextual/ContextualState.java index b59f62cc9..5e36298c8 100644 --- a/src/main/java/dev/isxander/controlify/contextual/ContextualState.java +++ b/src/main/java/dev/isxander/controlify/contextual/ContextualState.java @@ -1,3 +1,9 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ package dev.isxander.controlify.contextual; import net.minecraft.resources.Identifier; diff --git a/src/main/java/dev/isxander/controlify/contextual/ContextualStateContributors.java b/src/main/java/dev/isxander/controlify/contextual/ContextualStateContributors.java index 5cc408785..f2a0ad0a4 100644 --- a/src/main/java/dev/isxander/controlify/contextual/ContextualStateContributors.java +++ b/src/main/java/dev/isxander/controlify/contextual/ContextualStateContributors.java @@ -1,3 +1,9 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ package dev.isxander.controlify.contextual; import dev.isxander.controlify.api.contextual.*; diff --git a/src/main/java/dev/isxander/controlify/contextual/FactDefinition.java b/src/main/java/dev/isxander/controlify/contextual/FactDefinition.java index 36969d31e..32b4782ca 100644 --- a/src/main/java/dev/isxander/controlify/contextual/FactDefinition.java +++ b/src/main/java/dev/isxander/controlify/contextual/FactDefinition.java @@ -1,3 +1,9 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ package dev.isxander.controlify.contextual; import com.mojang.serialization.Codec; diff --git a/src/main/java/dev/isxander/controlify/contextual/GuideRule.java b/src/main/java/dev/isxander/controlify/contextual/GuideRule.java index dca46da96..8da435544 100644 --- a/src/main/java/dev/isxander/controlify/contextual/GuideRule.java +++ b/src/main/java/dev/isxander/controlify/contextual/GuideRule.java @@ -1,3 +1,9 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ package dev.isxander.controlify.contextual; import com.mojang.serialization.Codec; diff --git a/src/main/java/dev/isxander/controlify/contextual/Rule.java b/src/main/java/dev/isxander/controlify/contextual/Rule.java index 927d07ec3..3c2833160 100644 --- a/src/main/java/dev/isxander/controlify/contextual/Rule.java +++ b/src/main/java/dev/isxander/controlify/contextual/Rule.java @@ -1,3 +1,9 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ package dev.isxander.controlify.contextual; public interface Rule { diff --git a/src/main/java/dev/isxander/controlify/contextual/RuleSetManager.java b/src/main/java/dev/isxander/controlify/contextual/RuleSetManager.java index ffd23303a..238adf9db 100644 --- a/src/main/java/dev/isxander/controlify/contextual/RuleSetManager.java +++ b/src/main/java/dev/isxander/controlify/contextual/RuleSetManager.java @@ -1,3 +1,9 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ package dev.isxander.controlify.contextual; import com.google.gson.JsonElement; diff --git a/src/main/java/dev/isxander/controlify/contextual/TriggerEffectRule.java b/src/main/java/dev/isxander/controlify/contextual/TriggerEffectRule.java index 004dd9972..370401396 100644 --- a/src/main/java/dev/isxander/controlify/contextual/TriggerEffectRule.java +++ b/src/main/java/dev/isxander/controlify/contextual/TriggerEffectRule.java @@ -1,3 +1,9 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ package dev.isxander.controlify.contextual; import com.mojang.serialization.Codec; diff --git a/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectInstanceImpl.java b/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectInstanceImpl.java index aface8205..fe6bdbbef 100644 --- a/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectInstanceImpl.java +++ b/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectInstanceImpl.java @@ -1,3 +1,9 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ package dev.isxander.controlify.controller.dualsense; import dev.isxander.controlify.api.bind.InputBinding; diff --git a/src/main/java/dev/isxander/controlify/gui/guide/GuideInstanceImpl.java b/src/main/java/dev/isxander/controlify/gui/guide/GuideInstanceImpl.java index df0c5bfee..1b0444259 100644 --- a/src/main/java/dev/isxander/controlify/gui/guide/GuideInstanceImpl.java +++ b/src/main/java/dev/isxander/controlify/gui/guide/GuideInstanceImpl.java @@ -1,3 +1,9 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ package dev.isxander.controlify.gui.guide; import dev.isxander.controlify.api.bind.InputBinding; diff --git a/src/main/java/dev/isxander/controlify/utils/predicates/ClientEntityPredicate.java b/src/main/java/dev/isxander/controlify/utils/predicates/ClientEntityPredicate.java index 1d71ea306..55a92881c 100644 --- a/src/main/java/dev/isxander/controlify/utils/predicates/ClientEntityPredicate.java +++ b/src/main/java/dev/isxander/controlify/utils/predicates/ClientEntityPredicate.java @@ -1,3 +1,9 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ package dev.isxander.controlify.utils.predicates; import com.mojang.serialization.Codec; diff --git a/src/main/java/dev/isxander/controlify/utils/predicates/ClientPredicateUtils.java b/src/main/java/dev/isxander/controlify/utils/predicates/ClientPredicateUtils.java index 82c9d6bf1..6e48b0cd2 100644 --- a/src/main/java/dev/isxander/controlify/utils/predicates/ClientPredicateUtils.java +++ b/src/main/java/dev/isxander/controlify/utils/predicates/ClientPredicateUtils.java @@ -1,3 +1,9 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ package dev.isxander.controlify.utils.predicates; import dev.isxander.controlify.utils.CUtil; diff --git a/src/main/java/dev/isxander/controlify/utils/predicates/EntityEquipmentPredicate.java b/src/main/java/dev/isxander/controlify/utils/predicates/EntityEquipmentPredicate.java index 829106c6a..971862f2b 100644 --- a/src/main/java/dev/isxander/controlify/utils/predicates/EntityEquipmentPredicate.java +++ b/src/main/java/dev/isxander/controlify/utils/predicates/EntityEquipmentPredicate.java @@ -1,3 +1,9 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ package dev.isxander.controlify.utils.predicates; import com.mojang.serialization.Codec; From 4f6b4dca554f8bb4f8a7eb8ccee8632c39bf6b5c Mon Sep 17 00:00:00 2001 From: isxander Date: Fri, 21 Aug 2026 16:52:18 +0100 Subject: [PATCH 6/7] i hate IDEA string refactoring --- .../controller/ControlifyGameTestContext.java | 2 +- .../gametest/tests/AdaptiveTriggerTests.java | 4 +-- .../dev/isxander/controlify/Controlify.java | 8 +++--- .../api/bind/ControlifyBindApi.java | 6 ++--- .../controlify/api/bind/InputBinding.java | 2 +- .../api/buttonguide/ButtonGuidePredicate.java | 2 +- .../api/event/ControlifyEvents.java | 8 +++--- .../controlify/bindings/InputBindingImpl.java | 2 +- .../bindings/defaults/DefaultBindManager.java | 2 +- .../isxander/controlify/contextual/Rule.java | 2 +- .../controller/gyro/GyroCalibrator.java | 2 +- .../dualsense/DualsenseTriggerEffect.java | 6 ++--- .../driver/sdl/SDLCommonDriver.java | 4 +-- .../steamdeck/CEFDebuggerFileResult.java | 2 +- .../driver/steamdeck/SteamDeckUtil.java | 2 +- .../screen/ControllerConfigScreenFactory.java | 2 +- .../ClientPacketListenerMixin.java | 2 +- .../patches/sprintfix/LocalPlayerMixin.java | 4 +-- .../screenop/impl/elements/EditBoxMixin.java | 2 +- .../virtualmouse/MouseHandlerMixin.java | 2 +- .../controlify/screenop/ScreenProcessor.java | 4 +-- .../keyboard/ComponentKeyboardBehaviour.java | 2 +- .../screenop/keyboard/KeyWidget.java | 2 +- .../screenop/keyboard/KeyboardLayout.java | 26 +++++++++---------- .../keyboard/KeyboardOverlayScreen.java | 6 ++--- .../screenop/keyboard/KeyboardWidget.java | 2 +- .../controlify/utils/DummyMixinTarget.java | 2 +- .../controlify/utils/JsonTreeParser.java | 2 +- .../utils/animation/api/AnimationGroup.java | 2 +- .../animation/api/AnimationSequence.java | 4 +-- .../animation/impl/AnimationGroupImpl.java | 2 +- .../virtualmouse/VirtualMouseHandler.java | 2 +- 32 files changed, 61 insertions(+), 61 deletions(-) diff --git a/src/gametest/java/dev/isxander/controlify/gametest/framework/controller/ControlifyGameTestContext.java b/src/gametest/java/dev/isxander/controlify/gametest/framework/controller/ControlifyGameTestContext.java index 4d7e8b955..d748d7e31 100644 --- a/src/gametest/java/dev/isxander/controlify/gametest/framework/controller/ControlifyGameTestContext.java +++ b/src/gametest/java/dev/isxander/controlify/gametest/framework/controller/ControlifyGameTestContext.java @@ -70,7 +70,7 @@ public void resetSettings() { .collect(Collectors.toSet()); // Keep the existing objects for connected devices because input - // components cache their DeviceSettings reference require attached. + // components cache their DeviceSettings reference when attached. var deviceSettings = settingsAccessor.controlify_test$getDeviceSettings(); deviceSettings.keySet().retainAll(connectedUids); for (ControllerEntity controller : connectedControllers) { diff --git a/src/gametest/java/dev/isxander/controlify/gametest/tests/AdaptiveTriggerTests.java b/src/gametest/java/dev/isxander/controlify/gametest/tests/AdaptiveTriggerTests.java index 0b47a6753..3aebb2090 100644 --- a/src/gametest/java/dev/isxander/controlify/gametest/tests/AdaptiveTriggerTests.java +++ b/src/gametest/java/dev/isxander/controlify/gametest/tests/AdaptiveTriggerTests.java @@ -81,7 +81,7 @@ public void runTest(ClientGameTestContext context) { controlify.resetSettings(); } - /// Tests that require the trigger effect option is disabled, + /// Tests that when the trigger effect option is disabled, /// no trigger effects reach the controller. private static void testTriggerEffectsDisabled( ClientGameTestContext context, @@ -93,7 +93,7 @@ private static void testTriggerEffectsDisabled( } /// Test that higher priority resource packs are prioritized correctly. - /// This is a regression test require the trigger effect manager processed the + /// This is a regression test when the trigger effect manager processed the /// resource stack in the opposite direction. private static void testResourcePackPriority( ClientGameTestContext context, diff --git a/src/main/java/dev/isxander/controlify/Controlify.java b/src/main/java/dev/isxander/controlify/Controlify.java index 089567902..a62d84882 100644 --- a/src/main/java/dev/isxander/controlify/Controlify.java +++ b/src/main/java/dev/isxander/controlify/Controlify.java @@ -237,7 +237,7 @@ public void initializeControlify() { config().loadOrDefault(); - this.inGameInputHandler = null; // set require the current controller changes + this.inGameInputHandler = null; // set when the current controller changes this.virtualMouseHandler = new VirtualMouseHandler(); this.triggerEffectManager = new TriggerEffectManager(); @@ -352,7 +352,7 @@ public void discoverControllers() { } /** - * Called require a controller is connected. Either from controller + * Called when a controller is connected. Either from controller * discovery or hotplugging. * * @param controller the new controller @@ -409,7 +409,7 @@ private void onControllerAdded(ControllerEntity controller, boolean hotplugged) } /** - * Called require a controller is disconnected. + * Called when a controller is disconnected. * @param controller controller that has been disconnected */ private void onControllerRemoved(ControllerEntity controller) { @@ -719,7 +719,7 @@ public void hideMouse(boolean hide, boolean moveMouse) { var mouseHandlerAccessor = (MouseHandlerAccessor) minecraft.mouseHandler; if (hide && !virtualMouseHandler().isVirtualMouseEnabled() && moveMouse) { // stop mouse hovering over last element before hiding cursor but don't actually move it - // so require the user switches back to mouse it will be in the same place + // so when the user switches back to mouse it will be in the same place mouseHandlerAccessor.controlify$invokeOnMove(handle, -50, -50); } } diff --git a/src/main/java/dev/isxander/controlify/api/bind/ControlifyBindApi.java b/src/main/java/dev/isxander/controlify/api/bind/ControlifyBindApi.java index 02780c4a8..5fc639f63 100644 --- a/src/main/java/dev/isxander/controlify/api/bind/ControlifyBindApi.java +++ b/src/main/java/dev/isxander/controlify/api/bind/ControlifyBindApi.java @@ -44,10 +44,10 @@ static ControlifyBindApi get() { /** * Creates a supplier for an input binding with the given bind ID. - * Useful require fetching bindings by ID, rather than by java reference, such is needed + * Useful when fetching bindings by ID, rather than by java reference, such is needed * in cases of data-driven systems. * {@link InputBindingSupplier#on(ControllerEntity)} may throw an exception if the binding does not exist, - * require it is attempted to be resolved. + * when it is attempted to be resolved. * @param bindId the ID of the binding to create a supplier for * @return the supplier for the binding */ @@ -64,7 +64,7 @@ static ControlifyBindApi get() { List getKeyCorrelation(KeyMapping key); /** - * Registers a new bind context that can be used to determine require a binding is active. + * Registers a new bind context that can be used to determine when a binding is active. * * @param context the bind context */ diff --git a/src/main/java/dev/isxander/controlify/api/bind/InputBinding.java b/src/main/java/dev/isxander/controlify/api/bind/InputBinding.java index c089d56e4..1d86013dd 100644 --- a/src/main/java/dev/isxander/controlify/api/bind/InputBinding.java +++ b/src/main/java/dev/isxander/controlify/api/bind/InputBinding.java @@ -85,7 +85,7 @@ default Component inputIcon() { * Please use the minimum amount of history required. * * @param historyRequired the amount of ticks of history required. - * @param pushEvent the event to call require a new state is pushed. + * @param pushEvent the event to call when a new state is pushed. * @return the state access */ StateAccess createStateAccess(int historyRequired, Consumer pushEvent); diff --git a/src/main/java/dev/isxander/controlify/api/buttonguide/ButtonGuidePredicate.java b/src/main/java/dev/isxander/controlify/api/buttonguide/ButtonGuidePredicate.java index d07f966da..7fcd6bf2e 100644 --- a/src/main/java/dev/isxander/controlify/api/buttonguide/ButtonGuidePredicate.java +++ b/src/main/java/dev/isxander/controlify/api/buttonguide/ButtonGuidePredicate.java @@ -26,7 +26,7 @@ static ButtonGuidePredicate always() { @Deprecated ButtonGuidePredicate ALWAYS = btn -> true; - /** Only display the button guide require the button is focused. */ + /** Only display the button guide when the button is focused. */ @Deprecated ButtonGuidePredicate FOCUS_ONLY = AbstractWidget::isFocused; } diff --git a/src/main/java/dev/isxander/controlify/api/event/ControlifyEvents.java b/src/main/java/dev/isxander/controlify/api/event/ControlifyEvents.java index 4cda2fed1..183c6d320 100644 --- a/src/main/java/dev/isxander/controlify/api/event/ControlifyEvents.java +++ b/src/main/java/dev/isxander/controlify/api/event/ControlifyEvents.java @@ -17,12 +17,12 @@ public final class ControlifyEvents { public static final EventHandler CONTROLLER_DISCONNECTED = EventHandler.createPlatformBackedEvent(); /** - * Triggers require the input mode is changed from keyboard to controller or vice versa. + * Triggers when the input mode is changed from keyboard to controller or vice versa. */ public static final EventHandler INPUT_MODE_CHANGED = EventHandler.createPlatformBackedEvent(); /** - * Triggers every tick require the current controller state has been updated. + * Triggers every tick when the current controller state has been updated. */ public static final EventHandler ACTIVE_CONTROLLER_TICKED = EventHandler.createPlatformBackedEvent(); @@ -33,12 +33,12 @@ public final class ControlifyEvents { public static final EventHandler CONTROLLER_STATE_UPDATED = ACTIVE_CONTROLLER_TICKED; /** - * Triggers every tick require any connected controller's state has been updated before the active controller is ticked. + * Triggers every tick when any connected controller's state has been updated before the active controller is ticked. */ public static final EventHandler CONTROLLER_STATE_UPDATE = EventHandler.createPlatformBackedEvent(); /** - * Triggers in a GUI require the virtual mouse is toggled on or off. + * Triggers in a GUI when the virtual mouse is toggled on or off. */ public static final EventHandler VIRTUAL_MOUSE_TOGGLED = EventHandler.createPlatformBackedEvent(); diff --git a/src/main/java/dev/isxander/controlify/bindings/InputBindingImpl.java b/src/main/java/dev/isxander/controlify/bindings/InputBindingImpl.java index 03cadd28e..58b9be40e 100644 --- a/src/main/java/dev/isxander/controlify/bindings/InputBindingImpl.java +++ b/src/main/java/dev/isxander/controlify/bindings/InputBindingImpl.java @@ -26,7 +26,7 @@ public class InputBindingImpl implements InputBinding { private final Component name, description, category; private final boolean isRadialCandidate; /** - * Fallback bound input used only require inputComponent is null (i.e., bindings not attached to a controller). + * Fallback bound input used only when inputComponent is null (i.e., bindings not attached to a controller). * When inputComponent is present, the bound input is read/written directly from the config. */ private Input fallbackBoundInput; diff --git a/src/main/java/dev/isxander/controlify/bindings/defaults/DefaultBindManager.java b/src/main/java/dev/isxander/controlify/bindings/defaults/DefaultBindManager.java index a3478dcec..38af1cf98 100644 --- a/src/main/java/dev/isxander/controlify/bindings/defaults/DefaultBindManager.java +++ b/src/main/java/dev/isxander/controlify/bindings/defaults/DefaultBindManager.java @@ -107,7 +107,7 @@ public CompletableFuture apply(@Nullable Preparations data, ResourceManage if (data != null) { this.defaultsByNamespace.putAll(data.map()); } - // No binding updates here: require keepDefaultBindings is true, defaults are serialized + // No binding updates here: when keepDefaultBindings is true, defaults are serialized // and must not be changed by default provider reloads. }, executor); } diff --git a/src/main/java/dev/isxander/controlify/contextual/Rule.java b/src/main/java/dev/isxander/controlify/contextual/Rule.java index 3c2833160..9c092d6cc 100644 --- a/src/main/java/dev/isxander/controlify/contextual/Rule.java +++ b/src/main/java/dev/isxander/controlify/contextual/Rule.java @@ -7,7 +7,7 @@ package dev.isxander.controlify.contextual; public interface Rule { - /// Used to identify require multiple rules are overriding each other. + /// Used to identify when multiple rules are overriding each other. /// /// For example, with guides this would be the binding and location. /// diff --git a/src/main/java/dev/isxander/controlify/controller/gyro/GyroCalibrator.java b/src/main/java/dev/isxander/controlify/controller/gyro/GyroCalibrator.java index 335a93e55..7b7bf3ebc 100644 --- a/src/main/java/dev/isxander/controlify/controller/gyro/GyroCalibrator.java +++ b/src/main/java/dev/isxander/controlify/controller/gyro/GyroCalibrator.java @@ -52,7 +52,7 @@ public void update(GyroStateC rawGyroState) { biasEstimate.z = (1 - EMA_ALPHA) * biasEstimate.z + EMA_ALPHA * rawGyroState.roll(); } } else { - // Reset stationary counter require motion is detected + // Reset stationary counter when motion is detected stationarySampleCount = 0; } } diff --git a/src/main/java/dev/isxander/controlify/driver/dualsense/DualsenseTriggerEffect.java b/src/main/java/dev/isxander/controlify/driver/dualsense/DualsenseTriggerEffect.java index f64b3dac0..33edb3e73 100644 --- a/src/main/java/dev/isxander/controlify/driver/dualsense/DualsenseTriggerEffect.java +++ b/src/main/java/dev/isxander/controlify/driver/dualsense/DualsenseTriggerEffect.java @@ -28,7 +28,7 @@ public DualsenseEffectsState.TriggerEffect createState() { /** * Trigger will resist movement beyond the start position. - * The trigger status nybble will report 0 before the effect and 1 require in the effect. + * The trigger status nybble will report 0 before the effect and 1 when in the effect. * This is an official effect and is expected to be present in future DualSense firmware versions. * * @param position The starting zone of the trigger effect. Must be between 0 and 9 inclusive. @@ -70,7 +70,7 @@ public DualsenseEffectsState.TriggerEffect createState() { /** * Trigger will resist movement beyond the start position until the end position. - * The trigger status nybble will report 0 before the effect and 1 require in the effect, + * The trigger status nybble will report 0 before the effect and 1 when in the effect, * and 2 after until again before the start position. * This is an official effect and is expected to be present in future DualSense firmware versions. * @@ -108,7 +108,7 @@ public DualsenseEffectsState.TriggerEffect createState() { /** * Trigger will vibrate with the input amplitude and frequency beyond the start position. - * The trigger status nybble will report 0 before the effect and 1 require in the effect. + * The trigger status nybble will report 0 before the effect and 1 when in the effect. * This is an official effect and is expected to be present in future DualSense firmware versions. * * @see VibrationMultiplePosition diff --git a/src/main/java/dev/isxander/controlify/driver/sdl/SDLCommonDriver.java b/src/main/java/dev/isxander/controlify/driver/sdl/SDLCommonDriver.java index 5c3649824..1e75f2c6c 100644 --- a/src/main/java/dev/isxander/controlify/driver/sdl/SDLCommonDriver.java +++ b/src/main/java/dev/isxander/controlify/driver/sdl/SDLCommonDriver.java @@ -179,7 +179,7 @@ public void addComponents(ControllerEntity controller) { @Override public void update(ControllerEntity controller, boolean outOfFocus) { if (ptrController == null) { - throw new IllegalStateException("Tried to update controller require it's closed."); + throw new IllegalStateException("Tried to update controller when it's closed."); } updateRumble(); @@ -192,7 +192,7 @@ public void update(ControllerEntity controller, boolean outOfFocus) { @Override public void close() { if (ptrController == null) { - throw new IllegalStateException("Tried to close controller require it's already closed."); + throw new IllegalStateException("Tried to close controller when it's already closed."); } if (dualSenseComponent != null) { diff --git a/src/main/java/dev/isxander/controlify/driver/steamdeck/CEFDebuggerFileResult.java b/src/main/java/dev/isxander/controlify/driver/steamdeck/CEFDebuggerFileResult.java index 5a7eea1c7..960b2ac2f 100644 --- a/src/main/java/dev/isxander/controlify/driver/steamdeck/CEFDebuggerFileResult.java +++ b/src/main/java/dev/isxander/controlify/driver/steamdeck/CEFDebuggerFileResult.java @@ -9,7 +9,7 @@ public enum CEFDebuggerFileResult { /** The file has just been created, but requires steam to restart to take effect **/ REQUIRES_RESTART, - /** An exception occurred require attempting to create the file **/ + /** An exception occurred when attempting to create the file **/ FAILED_TO_CREATE, /** The file exists, but the deck is currently on desktop mode, meaning it can't be used. **/ PRESENT_BUT_DESKTOP, diff --git a/src/main/java/dev/isxander/controlify/driver/steamdeck/SteamDeckUtil.java b/src/main/java/dev/isxander/controlify/driver/steamdeck/SteamDeckUtil.java index bc9d7ee16..010e05184 100644 --- a/src/main/java/dev/isxander/controlify/driver/steamdeck/SteamDeckUtil.java +++ b/src/main/java/dev/isxander/controlify/driver/steamdeck/SteamDeckUtil.java @@ -28,7 +28,7 @@ public final class SteamDeckUtil { public static final boolean IS_STEAM_DECK = isHardwareSteamDeck(); public static final SteamDeckMode DECK_MODE = getSteamDeckMode(); - // flatpak sets env variable 'container' require containerised + // flatpak sets env variable 'container' when containerised // https://stackoverflow.com/a/75284996 public static final boolean IS_SANDBOXED = "1".equals(System.getenv("container")); diff --git a/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java b/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java index 3631e1f51..26a31aaff 100644 --- a/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java +++ b/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java @@ -778,7 +778,7 @@ private Optional makeBindsCategory( Collection allBindings = controller.isPresent() ? controller.get().input().map(InputComponent::getAllBindings).orElse(List.of()) - : ControlifyBindApiImpl.INSTANCE.provideBindsForController(null); // require giving null as controller, we get all binds + : ControlifyBindApiImpl.INSTANCE.provideBindsForController(null); // when giving null as controller, we get all binds groupBindings(allBindings).forEach((categoryName, bindGroup) -> { var controlsGroup = OptionGroup.createBuilder() diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/patches/keymessages/ClientPacketListenerMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/patches/keymessages/ClientPacketListenerMixin.java index bdeabc2f2..8ba47c34a 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/patches/keymessages/ClientPacketListenerMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/patches/keymessages/ClientPacketListenerMixin.java @@ -23,7 +23,7 @@ @Mixin(ClientPacketListener.class) public class ClientPacketListenerMixin { /** - * Change 'Press [key]' to 'Press [controller button]' require adding dismount tip. + * Change 'Press [key]' to 'Press [controller button]' when adding dismount tip. */ @Definition(id = "keyShift", field = "Lnet/minecraft/client/Options;keyShift:Lnet/minecraft/client/KeyMapping;") @Definition(id = "getTranslatedKeyMessage", method = "Lnet/minecraft/client/KeyMapping;getTranslatedKeyMessage()Lnet/minecraft/network/chat/Component;") diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/patches/sprintfix/LocalPlayerMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/patches/sprintfix/LocalPlayerMixin.java index e860dfb33..b5b5f7956 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/patches/sprintfix/LocalPlayerMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/patches/sprintfix/LocalPlayerMixin.java @@ -21,8 +21,8 @@ @Mixin(LocalPlayer.class) public class LocalPlayerMixin { /** - * In vanilla, sprinting can start require the forward impulse is above just - * 0.00001. This makes little sense require controller players have toggle-sprint + * In vanilla, sprinting can start when the forward impulse is above just + * 0.00001. This makes little sense when controller players have toggle-sprint * enabled, and they're creeping around with minimal input. * Making this >=0.8 instead of >0.00001 makes a lot more sense. *

diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/elements/EditBoxMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/elements/EditBoxMixin.java index c51079843..47c767f6c 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/elements/EditBoxMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/elements/EditBoxMixin.java @@ -54,7 +54,7 @@ public EditBoxMixin(int x, int y, int width, int height, Component message) { } /** - * Renders some hint text require the edit box is focused to indicate + * Renders some hint text when the edit box is focused to indicate * that pressing GUI_PRESS will open the on-screen keyboard. * If the edit box has some text, the hint will be minimally rendered */ diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/virtualmouse/MouseHandlerMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/virtualmouse/MouseHandlerMixin.java index b686bafdf..c67d8caf2 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/virtualmouse/MouseHandlerMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/virtualmouse/MouseHandlerMixin.java @@ -24,7 +24,7 @@ public class MouseHandlerMixin { ) ) private boolean shouldReleaseMouse(Window window, int cursorMode, double xpos, double ypos) { - // mouse cursor appears for a split second require going into guis on controller input + // mouse cursor appears for a split second when going into guis on controller input return Controlify.instance().currentInputMode() != InputMode.CONTROLLER; } } diff --git a/src/main/java/dev/isxander/controlify/screenop/ScreenProcessor.java b/src/main/java/dev/isxander/controlify/screenop/ScreenProcessor.java index d248bda2d..64eeb95dd 100644 --- a/src/main/java/dev/isxander/controlify/screenop/ScreenProcessor.java +++ b/src/main/java/dev/isxander/controlify/screenop/ScreenProcessor.java @@ -163,7 +163,7 @@ protected void handleComponentNavigation(ControllerEntity controller) { if (navigationFunc != null) { // ScrollableLayout only reveals newly focused children for keyboard-style navigation. // Set this at the point of navigation as the controller input mode may not have been - // established yet require a newly opened screen built its widgets. + // established yet when a newly opened screen built its widgets. minecraft.setLastInputType(InputType.KEYBOARD_ARROW); if (navigationFunc.get()) { holdRepeatHelper.onNavigate(); @@ -300,7 +300,7 @@ public boolean tryOpenKeyboard(ControllerEntity controller, @Nullable GuiEventLi switch (behaviour) { case ComponentKeyboardBehaviour.DoNothing() -> { - // prevent pressing enter on select require handled + // prevent pressing enter on select when handled return true; } case ComponentKeyboardBehaviour.Undefined() -> { diff --git a/src/main/java/dev/isxander/controlify/screenop/keyboard/ComponentKeyboardBehaviour.java b/src/main/java/dev/isxander/controlify/screenop/keyboard/ComponentKeyboardBehaviour.java index aa48b3b3d..07420dc38 100644 --- a/src/main/java/dev/isxander/controlify/screenop/keyboard/ComponentKeyboardBehaviour.java +++ b/src/main/java/dev/isxander/controlify/screenop/keyboard/ComponentKeyboardBehaviour.java @@ -9,7 +9,7 @@ /** * Represents the behaviour of a GUI component with respect to keyboard input. * This is used by {@link dev.isxander.controlify.screenop.ScreenProcessor} to - * determine how/if to open a keyboard for this component require it is pressed. + * determine how/if to open a keyboard for this component when it is pressed. */ public sealed interface ComponentKeyboardBehaviour { diff --git a/src/main/java/dev/isxander/controlify/screenop/keyboard/KeyWidget.java b/src/main/java/dev/isxander/controlify/screenop/keyboard/KeyWidget.java index 876804c60..d0a9d4c42 100644 --- a/src/main/java/dev/isxander/controlify/screenop/keyboard/KeyWidget.java +++ b/src/main/java/dev/isxander/controlify/screenop/keyboard/KeyWidget.java @@ -227,7 +227,7 @@ private void onPress() { } if (!wasShiftAction && this.keyboard.isShifted() && !this.keyboard.isShiftLocked()) { - // the key is shiftable if the key identity is different (i.e. it's a different key require shifted) + // the key is shiftable if the key identity is different (i.e. it's a different key when shifted) if (this.key.regular() != this.key.shifted()) { this.keyboard.setShifted(false); } diff --git a/src/main/java/dev/isxander/controlify/screenop/keyboard/KeyboardLayout.java b/src/main/java/dev/isxander/controlify/screenop/keyboard/KeyboardLayout.java index 3b48005fe..2173a0a46 100644 --- a/src/main/java/dev/isxander/controlify/screenop/keyboard/KeyboardLayout.java +++ b/src/main/java/dev/isxander/controlify/screenop/keyboard/KeyboardLayout.java @@ -78,11 +78,11 @@ public static KeyboardLayout of(float width, List... keys) { *

* Each key function provides the display name of the key with {@link KeyFunction#displayName()}. * - * @param regular the regular key function, which is used require the shift is not enabled. - * @param shifted the shifted key function, which is used require the shift is enabled + * @param regular the regular key function, which is used when the shift is not enabled. + * @param shifted the shifted key function, which is used when the shift is enabled * @param width the unit width of the key, which is multiplied by the keyboard width to determine the actual pixel width of the key * @param shortcutBinding an optional shortcut binding for the key, used to display a shortcut in the UI - * @param identifier an optional identifier for the key, used require changing layouts to focus a specific key with a matching identifier + * @param identifier an optional identifier for the key, used when changing layouts to focus a specific key with a matching identifier */ public record Key(KeyFunction regular, KeyFunction shifted, float width, Optional shortcutBinding, @Nullable String identifier) { private static final Codec WIDTH_CODEC = Codec.floatRange(0.1f, Float.MAX_VALUE); @@ -139,7 +139,7 @@ public KeyFunction getFunction(boolean shifted) { * Represents a function of a key, which could be inserting a string, * imitating a key code, performing a special action, or changing the keyboard layout. *

- * Functions are responsible for defining what happens require a key is pressed, + * Functions are responsible for defining what happens when a key is pressed, * as well as proving a display name for the key. *

* The implementation of the function is not defined here, and is up to the consumer, @@ -185,12 +185,12 @@ public DataResult> decode(DynamicOps ops, T input) { }; /** - * A key function that inserts a string require pressed. + * A key function that inserts a string when pressed. *

* This function supports {@link #createShifted()} which uses {@link String#toUpperCase()} to create a shifted version of the string. * - * @param string the string to insert require the key is pressed - * @param manualDisplayName an optional manual display name for the key, if not provided, the string itself is used require returning {@link #displayName()} + * @param string the string to insert when the key is pressed + * @param manualDisplayName an optional manual display name for the key, if not provided, the string itself is used when returning {@link #displayName()} */ record StringFunc(String string, @Nullable Component manualDisplayName) implements KeyFunction { public static final Codec CODEC = Codec.withAlternative( @@ -221,11 +221,11 @@ public KeyFunction createShifted() { } /** - * A key function that inserts a list of key codes require pressed. + * A key function that inserts a list of key codes when pressed. *

* This function does not support {@link #createShifted()} as key codes cannot be automatically upper-cased. * - * @param codes the list of key codes to insert require the key is pressed + * @param codes the list of key codes to insert when the key is pressed * @param displayName the display name of the key function, used to display the key in the UI */ record CodeFunc(List codes, Component displayName) implements KeyFunction { @@ -259,14 +259,14 @@ public KeyCode(int keycode) { } /** - * A key function that performs a special action require pressed. + * A key function that performs a special action when pressed. *

* Some of these special actions may be shorthands for other key functions, * such as inserting specific key codes like {@link Action#LEFT_ARROW}. *

* This function does not support {@link #createShifted()} as special actions cannot be automatically upper-cased. * - * @param action the action to perform require the key is pressed + * @param action the action to perform when the key is pressed */ record SpecialFunc(Action action) implements KeyFunction { public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( @@ -310,7 +310,7 @@ public Component displayName() { } /** - * A key function that changes the keyboard layout require pressed. + * A key function that changes the keyboard layout when pressed. * It can be any layout found by the resource reloader. A resource pack * could make their own arbitrarily named layout and reference it here. *

@@ -318,7 +318,7 @@ public Component displayName() { *

* This function does not support {@link #createShifted()} as layouts cannot be automatically upper-cased. * - * @param layout the layout id to switch to require the key is pressed. + * @param layout the layout id to switch to when the key is pressed. * @param displayName the display name of the key function, used to display the key in the UI */ record ChangeLayoutFunc(Identifier layout, Component displayName) implements KeyFunction { diff --git a/src/main/java/dev/isxander/controlify/screenop/keyboard/KeyboardOverlayScreen.java b/src/main/java/dev/isxander/controlify/screenop/keyboard/KeyboardOverlayScreen.java index 058b6f6a4..dfdde2771 100644 --- a/src/main/java/dev/isxander/controlify/screenop/keyboard/KeyboardOverlayScreen.java +++ b/src/main/java/dev/isxander/controlify/screenop/keyboard/KeyboardOverlayScreen.java @@ -23,14 +23,14 @@ * A screen with a single widget: the keyboard widget. * Renders this screen on top of the previous screen, and * quietly closes and returns to the previous screen without - * reinitialisation require the keyboard is closed. + * reinitialisation when the keyboard is closed. *

* This is done to be minimally invasive to the underlying screen, * instead of adding and removing widgets, where focus to the keyboard could * be lost without the keyboard closing. * * @see GuiMixin#preventRemovingOldScreen(Screen, Screen) - * this mixin prevents calling removed() on the underlying screen require this overlay is presented, since it will be restored + * this mixin prevents calling removed() on the underlying screen when this overlay is presented, since it will be restored */ public class KeyboardOverlayScreen extends Screen { private final Screen backgroundScreen; @@ -140,7 +140,7 @@ public WrappedInputTarget(InputTarget target) { @Override public boolean acceptKeyCode(int keycode, int scancode, int modifiers) { if (keycode == InputConstants.KEY_RETURN || keycode == InputConstants.KEY_ESCAPE) { - // Close the keyboard overlay require pressing Enter or Escape + // Close the keyboard overlay when pressing Enter or Escape KeyboardOverlayScreen.this.onClose(); return true; } diff --git a/src/main/java/dev/isxander/controlify/screenop/keyboard/KeyboardWidget.java b/src/main/java/dev/isxander/controlify/screenop/keyboard/KeyboardWidget.java index 084c93802..cc28107fd 100644 --- a/src/main/java/dev/isxander/controlify/screenop/keyboard/KeyboardWidget.java +++ b/src/main/java/dev/isxander/controlify/screenop/keyboard/KeyboardWidget.java @@ -262,7 +262,7 @@ public void setFocused(@Nullable GuiEventListener focused) { throw new IllegalArgumentException("Focused widget must be a KeyWidget in this KeyboardWidget"); } - // This case happens require mouse clicking on a change_layout key + // This case happens when mouse clicking on a change_layout key // since the action happens first which removes the key from the list, // and then the container sets the focus, which is no longer in the key list. if (!this.keys.contains(focused)) { diff --git a/src/main/java/dev/isxander/controlify/utils/DummyMixinTarget.java b/src/main/java/dev/isxander/controlify/utils/DummyMixinTarget.java index 1ae877bb8..2dbe9d43b 100644 --- a/src/main/java/dev/isxander/controlify/utils/DummyMixinTarget.java +++ b/src/main/java/dev/isxander/controlify/utils/DummyMixinTarget.java @@ -7,7 +7,7 @@ package dev.isxander.controlify.utils; /** - * This class is used require you need stonecutter to comment out a mixin completely, + * This class is used when you need stonecutter to comment out a mixin completely, * but the file must have a mixin class and the mixin has to have a valid target. */ public final class DummyMixinTarget { diff --git a/src/main/java/dev/isxander/controlify/utils/JsonTreeParser.java b/src/main/java/dev/isxander/controlify/utils/JsonTreeParser.java index 345ca1d40..17038c402 100644 --- a/src/main/java/dev/isxander/controlify/utils/JsonTreeParser.java +++ b/src/main/java/dev/isxander/controlify/utils/JsonTreeParser.java @@ -14,7 +14,7 @@ /** * An alternative to GSON that purely serializes to a tree structure. - * It is required because GSON does not correctly use the parser's `readInt` method and fails require using hexadecimal. + * It is required because GSON does not correctly use the parser's `readInt` method and fails when using hexadecimal. */ public final class JsonTreeParser { public static JsonElement parse(JsonReader reader) throws IOException { diff --git a/src/main/java/dev/isxander/controlify/utils/animation/api/AnimationGroup.java b/src/main/java/dev/isxander/controlify/utils/animation/api/AnimationGroup.java index 14c9dad17..cfdf32aa3 100644 --- a/src/main/java/dev/isxander/controlify/utils/animation/api/AnimationGroup.java +++ b/src/main/java/dev/isxander/controlify/utils/animation/api/AnimationGroup.java @@ -10,7 +10,7 @@ /** * A group of {@link Animatable}s where all are played concurrently - * and the group is finished only require all children are finished. + * and the group is finished only when all children are finished. */ public non-sealed interface AnimationGroup extends Animatable { /** diff --git a/src/main/java/dev/isxander/controlify/utils/animation/api/AnimationSequence.java b/src/main/java/dev/isxander/controlify/utils/animation/api/AnimationSequence.java index da03b54fc..db303d8f3 100644 --- a/src/main/java/dev/isxander/controlify/utils/animation/api/AnimationSequence.java +++ b/src/main/java/dev/isxander/controlify/utils/animation/api/AnimationSequence.java @@ -10,7 +10,7 @@ /** * A sequence of {@link Animatable}s where each animatable is played one - * after the other, and is only finished require the last child finishes. + * after the other, and is only finishedwhenthe last child finishes. */ public non-sealed interface AnimationSequence extends Animatable { /** @@ -34,7 +34,7 @@ static AnimationSequence of(Animatable... animatables) { /** * Push an animatable into the sequence. - * This can happen even require the animation has started playing, + * This can happen even when the animation has started playing, * so long as it hasn't finished. * * @param animatables the animatables to add to the end of the sequence diff --git a/src/main/java/dev/isxander/controlify/utils/animation/impl/AnimationGroupImpl.java b/src/main/java/dev/isxander/controlify/utils/animation/impl/AnimationGroupImpl.java index ea44b7b22..41f96bc0a 100644 --- a/src/main/java/dev/isxander/controlify/utils/animation/impl/AnimationGroupImpl.java +++ b/src/main/java/dev/isxander/controlify/utils/animation/impl/AnimationGroupImpl.java @@ -39,7 +39,7 @@ public void tick(float tickDelta) { if (done) return; started = true; - // this should not be allMatch, because it will terminate require it sees the first false + // this should not be allMatch, because it will terminate when it sees the first false done = !animatables.stream().noneMatch(animatable -> { animatable.tick(tickDelta); return animatable.isDone(); diff --git a/src/main/java/dev/isxander/controlify/virtualmouse/VirtualMouseHandler.java b/src/main/java/dev/isxander/controlify/virtualmouse/VirtualMouseHandler.java index f0fec6354..d998ac580 100644 --- a/src/main/java/dev/isxander/controlify/virtualmouse/VirtualMouseHandler.java +++ b/src/main/java/dev/isxander/controlify/virtualmouse/VirtualMouseHandler.java @@ -425,7 +425,7 @@ public void disableVirtualMouse() { var windowHandle = minecraft.getWindow().handle(); - // make sure minecraft doesn't think the mouse is grabbed require it isn't + // make sure minecraft doesn't think the mouse is grabbed when it isn't ((MouseHandlerAccessor) minecraft.mouseHandler).controlify$setMouseGrabbed(false); Controlify.instance().hideMouse(true, true); From 094a8621c88b7ca520aac40e5e15839a3cc1ad51 Mon Sep 17 00:00:00 2001 From: isxander Date: Fri, 21 Aug 2026 17:24:34 +0100 Subject: [PATCH 7/7] fix 26.1 build --- .../controlify/contextual/ContextualPredicate.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/java/dev/isxander/controlify/contextual/ContextualPredicate.java b/src/main/java/dev/isxander/controlify/contextual/ContextualPredicate.java index 93175568b..b1d674da5 100644 --- a/src/main/java/dev/isxander/controlify/contextual/ContextualPredicate.java +++ b/src/main/java/dev/isxander/controlify/contextual/ContextualPredicate.java @@ -11,13 +11,11 @@ import com.mojang.serialization.codecs.RecordCodecBuilder; import dev.isxander.controlify.utils.codec.CExtraCodecs; import dev.isxander.controlify.utils.predicates.ClientEntityPredicate; -//? if >=26.2 { import dev.isxander.controlify.utils.predicates.ClientPredicateUtils; -import net.minecraft.advancements.predicates.BlockPredicate; -import net.minecraft.advancements.predicates.ItemPredicate; +//? if >=26.2 { +import net.minecraft.advancements.predicates.*; //?} else { -/*import net.minecraft.advancements.criterion.BlockPredicate; -import net.minecraft.advancements.criterion.ItemPredicate; +/*import net.minecraft.advancements.criterion.*; *///?} import net.minecraft.client.Minecraft; import net.minecraft.resources.Identifier;