diff --git a/src/main/java/github/io/lucunji/explayerenderer/Main.java b/src/main/java/github/io/lucunji/explayerenderer/Main.java index b44dd9f..7cee1e8 100644 --- a/src/main/java/github/io/lucunji/explayerenderer/Main.java +++ b/src/main/java/github/io/lucunji/explayerenderer/Main.java @@ -22,7 +22,7 @@ public void onInitializeClient() { "key." + MOD_ID + ".category")); ClientTickEvents.END_CLIENT_TICK.register(client -> { while (configKey.wasPressed()) { - client.setScreen(Configs.HANDLER.generateGui().generateScreen(client.currentScreen)); + client.setScreen(Configs.genGui(client.currentScreen)); } }); } diff --git a/src/main/java/github/io/lucunji/explayerenderer/client/render/PlayerHUDRenderer.java b/src/main/java/github/io/lucunji/explayerenderer/client/render/PlayerHUDRenderer.java index 329e1e9..7bb5876 100644 --- a/src/main/java/github/io/lucunji/explayerenderer/client/render/PlayerHUDRenderer.java +++ b/src/main/java/github/io/lucunji/explayerenderer/client/render/PlayerHUDRenderer.java @@ -4,7 +4,6 @@ import com.mojang.blaze3d.systems.RenderSystem; import github.io.lucunji.explayerenderer.client.render.DataBackup.DataBackupEntry; import github.io.lucunji.explayerenderer.config.Configs; -import github.io.lucunji.explayerenderer.config.PoseOffsetMethod; import github.io.lucunji.explayerenderer.mixin.ClientPlayerEntityAccessor; import github.io.lucunji.explayerenderer.mixin.EntityMixin; import github.io.lucunji.explayerenderer.mixin.LivingEntityAccessor; @@ -63,7 +62,7 @@ public class PlayerHUDRenderer { * Mimics the code in {@link InventoryScreen#drawEntity} */ public void render(float partialTicks) { - Configs configs = Configs.HANDLER.instance(); + Configs configs = Configs.getInstance(); if (client.world == null || client.player == null || !configs.enabled) return; LivingEntity targetEntity = client.world.getPlayers().stream().filter(p -> p.getName().getString().equals(configs.playerName)).findFirst().orElse(client.player); if (configs.spectatorAutoSwitch && client.player.isSpectator()) { @@ -77,15 +76,15 @@ public void render(float partialTicks) { int scaledWidth = client.getWindow().getScaledWidth(); int scaledHeight = client.getWindow().getScaledHeight(); - PoseOffsetMethod poseOffsetMethod = configs.poseOffsetMethod; + Configs.PoseOffsetMethod poseOffsetMethod = configs.poseOffsetMethod; var backup = new DataBackup<>(targetEntity, LIVINGENTITY_BACKUP_ENTRIES); backup.save(); - transformEntity(targetEntity, partialTicks, poseOffsetMethod == PoseOffsetMethod.FORCE_STANDING); + transformEntity(targetEntity, partialTicks, poseOffsetMethod == Configs.PoseOffsetMethod.FORCE_STANDING); DataBackup vehicleBackup = null; - if (configs.renderVehicle && poseOffsetMethod != PoseOffsetMethod.FORCE_STANDING && targetEntity.hasVehicle()) { + if (configs.renderVehicle && poseOffsetMethod != Configs.PoseOffsetMethod.FORCE_STANDING && targetEntity.hasVehicle()) { var vehicle = targetEntity.getVehicle(); assert vehicle != null; @@ -125,9 +124,9 @@ public void render(float partialTicks) { backup.restore(); } - private double getPoseOffsetY(LivingEntity targetEntity, float partialTicks, PoseOffsetMethod poseOffsetMethod) { - Configs configs = Configs.HANDLER.instance(); - if (poseOffsetMethod == PoseOffsetMethod.AUTO) { + private double getPoseOffsetY(LivingEntity targetEntity, float partialTicks, Configs.PoseOffsetMethod poseOffsetMethod) { + Configs configs = Configs.getInstance(); + if (poseOffsetMethod == Configs.PoseOffsetMethod.AUTO) { final float defaultPlayerEyeHeight = PlayerEntity.DEFAULT_EYE_HEIGHT; final float defaultPlayerSwimmingBBHeight = PlayerEntity.field_30650; final float eyeHeightRatio = 0.85f; @@ -142,7 +141,7 @@ private double getPoseOffsetY(LivingEntity targetEntity, float partialTicks, Pos } else { return PlayerEntity.DEFAULT_EYE_HEIGHT - targetEntity.getStandingEyeHeight(); } - } else if (poseOffsetMethod == PoseOffsetMethod.MANUAL) { + } else if (poseOffsetMethod == Configs.PoseOffsetMethod.MANUAL) { if (targetEntity.isFallFlying()) { return configs.elytraOffsetY * getFallFlyingLeaning(targetEntity, partialTicks); } else if ((targetEntity.isInSwimmingPose()) && targetEntity.getLeaningPitch(partialTicks) > 0 || targetEntity.isUsingRiptide()) { // require nonzero leaning to filter out glitch @@ -157,7 +156,7 @@ private double getPoseOffsetY(LivingEntity targetEntity, float partialTicks, Pos } private void transformEntity(LivingEntity targetEntity, float partialTicks, boolean forceStanding) { - Configs configs = Configs.HANDLER.instance(); + Configs configs = Configs.getInstance(); // synchronize values to remove glitch if (!targetEntity.isSwimming() && !targetEntity.isFallFlying() && !targetEntity.isCrawling()) { targetEntity.setPose(targetEntity.isInSneakingPose() ? EntityPose.CROUCHING : EntityPose.STANDING); @@ -207,7 +206,7 @@ private void transformEntity(LivingEntity targetEntity, float partialTicks, bool @SuppressWarnings("deprecation") private void performRendering(Entity targetEntity, double posX, double posY, double size, boolean mirror, Vector3f offset, double lightDegree, float partialTicks) { - Configs configs = Configs.HANDLER.instance(); + Configs configs = Configs.getInstance(); EntityRenderDispatcher entityRenderDispatcher = client.getEntityRenderDispatcher(); Matrix4fStack matrixStack1 = RenderSystem.getModelViewStack(); @@ -263,7 +262,7 @@ private void performRendering(Entity targetEntity, double posX, double posY, dou } private static int getLight(Entity entity, float tickDelta) { - Configs configs = Configs.HANDLER.instance(); + Configs configs = Configs.getInstance(); if (configs.useWorldLight) { World world = entity.getWorld(); int blockLight = world.getLightLevel(LightType.BLOCK, BlockPos.ofFloored(entity.getCameraPosVec(tickDelta))); diff --git a/src/main/java/github/io/lucunji/explayerenderer/compat/modmenu/ModMenuApiImpl.java b/src/main/java/github/io/lucunji/explayerenderer/compat/modmenu/ModMenuApiImpl.java index f126deb..f82dcde 100644 --- a/src/main/java/github/io/lucunji/explayerenderer/compat/modmenu/ModMenuApiImpl.java +++ b/src/main/java/github/io/lucunji/explayerenderer/compat/modmenu/ModMenuApiImpl.java @@ -7,6 +7,6 @@ public class ModMenuApiImpl implements ModMenuApi { @Override public ConfigScreenFactory getModConfigScreenFactory() { - return screen -> Configs.HANDLER.generateGui().generateScreen(screen); + return Configs::genGui; } } \ No newline at end of file diff --git a/src/main/java/github/io/lucunji/explayerenderer/config/Configs.java b/src/main/java/github/io/lucunji/explayerenderer/config/Configs.java index aa08b50..34382e6 100644 --- a/src/main/java/github/io/lucunji/explayerenderer/config/Configs.java +++ b/src/main/java/github/io/lucunji/explayerenderer/config/Configs.java @@ -1,5 +1,8 @@ package github.io.lucunji.explayerenderer.config; +import dev.isxander.yacl3.api.NameableEnum; +import dev.isxander.yacl3.api.YetAnotherConfigLib; +import dev.isxander.yacl3.api.utils.OptionUtils; import dev.isxander.yacl3.config.v2.api.ConfigClassHandler; import dev.isxander.yacl3.config.v2.api.SerialEntry; import dev.isxander.yacl3.config.v2.api.autogen.*; @@ -12,13 +15,23 @@ import net.minecraft.util.Identifier; public class Configs { - public static ConfigClassHandler HANDLER = ConfigClassHandler.createBuilder(Configs.class) + private static final ConfigClassHandler HANDLER = ConfigClassHandler.createBuilder(Configs.class) .id(Identifier.of(Main.MOD_ID, "config")) .serializer(config -> GsonConfigSerializerBuilder.create(config) .setPath(FabricLoader.getInstance().getConfigDir().resolve(Main.MOD_ID + "_yacl.json")) .build()) .build(); + public static Configs getInstance() { + return HANDLER.instance(); + } + + public static Screen genGui(Screen parent) { + YetAnotherConfigLib yacl = HANDLER.generateGui(); + OptionUtils.forEachOptions(yacl, option -> option.addListener((o, p) -> o.applyValue())); + return yacl.generateScreen(parent); + } + public static boolean isConfigScreen(Screen screen) { return screen != null && screen.getTitle() != null && screen.getTitle().equals(Text.translatable("yacl3.config." + Main.MOD_ID + ":config.title")); } @@ -135,4 +148,19 @@ public static boolean isConfigScreen(Screen screen) { @AutoGen(category = "details") @Boolean public boolean renderVehicle = true; + + public enum PoseOffsetMethod implements NameableEnum { + AUTO, MANUAL, FORCE_STANDING, DISABLED; + + public final String nameKey; + + PoseOffsetMethod() { + this.nameKey = "yacl3.config." + Main.MOD_ID + ":config.poseOffsetMethod." + this.name(); + } + + @Override + public Text getDisplayName() { + return Text.translatable(this.nameKey); + } + } } diff --git a/src/main/java/github/io/lucunji/explayerenderer/config/OptionPatch.java b/src/main/java/github/io/lucunji/explayerenderer/config/OptionPatch.java new file mode 100644 index 0000000..b176d79 --- /dev/null +++ b/src/main/java/github/io/lucunji/explayerenderer/config/OptionPatch.java @@ -0,0 +1,7 @@ +package github.io.lucunji.explayerenderer.config; + +public interface OptionPatch { + T extraPlayerRenderer$getSavedValue(); + boolean extraPlayerRenderer$savePendingValue(); + void extraPlayerRenderer$restoreSavedValue(); +} diff --git a/src/main/java/github/io/lucunji/explayerenderer/config/PoseOffsetMethod.java b/src/main/java/github/io/lucunji/explayerenderer/config/PoseOffsetMethod.java deleted file mode 100644 index 6f0cd3b..0000000 --- a/src/main/java/github/io/lucunji/explayerenderer/config/PoseOffsetMethod.java +++ /dev/null @@ -1,20 +0,0 @@ -package github.io.lucunji.explayerenderer.config; - -import dev.isxander.yacl3.api.NameableEnum; -import github.io.lucunji.explayerenderer.Main; -import net.minecraft.text.Text; - -public enum PoseOffsetMethod implements NameableEnum { - AUTO, MANUAL, FORCE_STANDING, DISABLED; - - public final String nameKey; - - PoseOffsetMethod() { - this.nameKey = "yacl3.config." + Main.MOD_ID + ":config.poseOffsetMethod." + this.name(); - } - - @Override - public Text getDisplayName() { - return Text.translatable(this.nameKey); - } -} diff --git a/src/main/java/github/io/lucunji/explayerenderer/mixin/yacl/YACLScreenMixin.java b/src/main/java/github/io/lucunji/explayerenderer/mixin/yacl/YACLScreenMixin.java deleted file mode 100644 index d1aa417..0000000 --- a/src/main/java/github/io/lucunji/explayerenderer/mixin/yacl/YACLScreenMixin.java +++ /dev/null @@ -1,70 +0,0 @@ -package github.io.lucunji.explayerenderer.mixin.yacl; - -import dev.isxander.yacl3.api.Option; -import dev.isxander.yacl3.api.YetAnotherConfigLib; -import dev.isxander.yacl3.api.utils.OptionUtils; -import dev.isxander.yacl3.gui.YACLScreen; -import dev.isxander.yacl3.impl.utils.YACLConstants; -import github.io.lucunji.explayerenderer.config.Configs; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.tab.TabManager; -import net.minecraft.text.Text; -import org.spongepowered.asm.mixin.Final; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Mixin(YACLScreen.class) -public abstract class YACLScreenMixin extends Screen { - @Shadow(remap = false) - public abstract boolean pendingChanges(); - - @Shadow(remap = false) - @Final - public YetAnotherConfigLib config; - - @Shadow(remap = false) - @Final - public TabManager tabManager; - - @Shadow(remap = false) private boolean pendingChanges; - - protected YACLScreenMixin(Text title) { - super(title); - } - - /** - * Instantly apply changes, without saving to file or triggering any flag. - * Force {@link #pendingChanges} to {@code true} - */ - @Inject(method = "onOptionChanged", at = @At("RETURN"), remap = false) - public void applyOnOptionChanged(Option opt, CallbackInfo ci) { - if (!Configs.isConfigScreen(this)) return; - - pendingChanges = true; - - OptionUtils.forEachOptions(config, Option::applyValue); - OptionUtils.forEachOptions(config, option -> { - if (option.changed()) { - option.forgetPendingValue(); - YACLConstants.LOGGER.error("Option '{}' value mismatch after applying! Reset to binding's getter.", option.name().getString()); - } - }); - - if (tabManager.getCurrentTab() instanceof YACLScreen.CategoryTab categoryTab) { - categoryTab.updateButtons(); - categoryTab.undoButton.active = false; - } - } - - @Inject(method = "cancelOrReset", at = @At("HEAD"), remap = false) - public void reloadOnCancelled(CallbackInfo ci) { - if (Configs.isConfigScreen(this)) return; - - if (pendingChanges()) { - Configs.HANDLER.load(); - } - } -} diff --git a/src/main/java/github/io/lucunji/explayerenderer/mixin/yacl/patch/OptionImplMixin.java b/src/main/java/github/io/lucunji/explayerenderer/mixin/yacl/patch/OptionImplMixin.java new file mode 100644 index 0000000..ba3544f --- /dev/null +++ b/src/main/java/github/io/lucunji/explayerenderer/mixin/yacl/patch/OptionImplMixin.java @@ -0,0 +1,87 @@ +package github.io.lucunji.explayerenderer.mixin.yacl.patch; + +import com.google.common.collect.ImmutableSet; +import dev.isxander.yacl3.api.*; +import dev.isxander.yacl3.impl.OptionImpl; +import github.io.lucunji.explayerenderer.config.Configs; +import github.io.lucunji.explayerenderer.config.OptionPatch; +import net.minecraft.client.MinecraftClient; +import net.minecraft.text.Text; +import org.jetbrains.annotations.NotNull; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.Collection; +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.Function; + +@SuppressWarnings("UnstableApiUsage") +@Mixin(value = OptionImpl.class, priority = Integer.MIN_VALUE) +public abstract class OptionImplMixin implements Option, OptionPatch { + @Shadow + @Final + private Binding binding; + @Shadow private T pendingValue; + @Shadow public abstract boolean applyValue(); + + @Shadow public abstract void requestSet(@NotNull T value); + + /** + * We should rename it to isDirty + */ + @Shadow public abstract boolean changed(); + + @Shadow public abstract boolean isPendingValueDefault(); + + @Unique private T savedValue; + + @Inject(method = "", at = @At("RETURN"), remap = false) + public void onInit( + @NotNull Text name, + @NotNull Function descriptionFunction, + @NotNull Function, Controller> controlGetter, + @NotNull Binding binding, + boolean available, + ImmutableSet flags, + @NotNull Collection, T>> listeners, + CallbackInfo ci) { + this.savedValue = binding.getValue(); + } + + @Inject(method = "changed", at = @At("HEAD"), cancellable = true, remap = false) + public void onChanged(CallbackInfoReturnable cir) { + if (!Configs.isConfigScreen(MinecraftClient.getInstance().currentScreen)) return; + // the second case is needed to allow applyValue to work when running Undo/Cancel + cir.setReturnValue(!Objects.equals(pendingValue, savedValue) || !Objects.equals(pendingValue, binding.getValue())); + } + + + @Unique + @Override + public T extraPlayerRenderer$getSavedValue() { + return savedValue; + } + + @Unique + @Override + public boolean extraPlayerRenderer$savePendingValue() { + if (changed()) { + binding().setValue(savedValue = pendingValue); + return true; + } + return false; + } + + @Unique + @Override + public void extraPlayerRenderer$restoreSavedValue() { + requestSet(savedValue); + } +} diff --git a/src/main/java/github/io/lucunji/explayerenderer/mixin/yacl/patch/YACLScreenMixin.java b/src/main/java/github/io/lucunji/explayerenderer/mixin/yacl/patch/YACLScreenMixin.java new file mode 100644 index 0000000..0c4c43b --- /dev/null +++ b/src/main/java/github/io/lucunji/explayerenderer/mixin/yacl/patch/YACLScreenMixin.java @@ -0,0 +1,98 @@ +package github.io.lucunji.explayerenderer.mixin.yacl.patch; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.api.OptionFlag; +import dev.isxander.yacl3.api.YetAnotherConfigLib; +import dev.isxander.yacl3.api.utils.OptionUtils; +import dev.isxander.yacl3.gui.YACLScreen; +import dev.isxander.yacl3.impl.utils.YACLConstants; +import github.io.lucunji.explayerenderer.config.Configs; +import github.io.lucunji.explayerenderer.config.OptionPatch; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.tab.TabManager; +import net.minecraft.text.Text; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import static dev.isxander.yacl3.gui.YACLScreen.CategoryTab; + +import java.util.HashSet; +import java.util.Set; + +@Mixin(value = YACLScreen.class, priority = Integer.MIN_VALUE) +public abstract class YACLScreenMixin extends Screen { + @Shadow + @Final + public YetAnotherConfigLib config; + @Shadow public Text saveButtonMessage; + + /** + * pendingChanges should be renamed to isDirty + */ + @Shadow + public abstract boolean pendingChanges(); + + @Shadow private boolean pendingChanges; + @Shadow + @Final + public TabManager tabManager; + + + protected YACLScreenMixin(Text title) {super(title);} + + @Inject(method = "undo", at = @At("HEAD"), remap = false, cancellable = true) + public void onUndo(CallbackInfo ci) { + if (!Configs.isConfigScreen(MinecraftClient.getInstance().currentScreen)) return; + ci.cancel(); + OptionUtils.forEachOptions(config, option -> ((OptionPatch) option).extraPlayerRenderer$restoreSavedValue()); + } + + @Inject(method = "cancelOrReset", at = @At("HEAD"), remap = false, cancellable = true) + public void onCancelOrReset(CallbackInfo ci) { + if (!Configs.isConfigScreen(MinecraftClient.getInstance().currentScreen)) return; + ci.cancel(); + if (pendingChanges()) { // if pending changes, button acts as a cancel button + OptionUtils.forEachOptions(config, option -> ((OptionPatch) option).extraPlayerRenderer$restoreSavedValue()); + close(); + } else { // if not, button acts as a reset button + OptionUtils.forEachOptions(config, Option::requestSetDefault); + } + } + + @Inject(method = "finishOrSave", at = @At("HEAD"), remap = false, cancellable = true) + public void onFinishOrSave(CallbackInfo ci) { + if (!Configs.isConfigScreen(MinecraftClient.getInstance().currentScreen)) return; + ci.cancel(); + saveButtonMessage = null; + + if (pendingChanges()) { + Set flags = new HashSet<>(); + OptionUtils.forEachOptions(config, option -> { + if (((OptionPatch) option).extraPlayerRenderer$savePendingValue()) { + flags.addAll(option.flags()); + } + }); + OptionUtils.forEachOptions(config, option -> { + if (option.changed()) { + // if still changed after applying, reset to the current value from binding + // as something has gone wrong. + ((OptionPatch) option).extraPlayerRenderer$restoreSavedValue(); + YACLConstants.LOGGER.error("Option '{}' value mismatch after applying! Reset to saved value.", option.name().getString()); + } + }); + config.saveFunction().run(); + + flags.forEach(flag -> flag.accept(client)); + + pendingChanges = false; + if (tabManager.getCurrentTab() instanceof CategoryTab categoryTab) { + categoryTab.updateButtons(); + } + } else close(); + } +} diff --git a/src/main/resources/explayerenderer.mixins.json b/src/main/resources/explayerenderer.mixins.json index e4c17e4..58e392c 100644 --- a/src/main/resources/explayerenderer.mixins.json +++ b/src/main/resources/explayerenderer.mixins.json @@ -13,7 +13,8 @@ "yacl.PressableWidgetMixin", "yacl.ScreenMixin", "yacl.TextFieldWidgetMixin", - "yacl.YACLScreenMixin" + "yacl.patch.OptionImplMixin", + "yacl.patch.YACLScreenMixin" ], "injectors": { "defaultRequire": 1 diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index a57974c..b013f39 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -1,7 +1,7 @@ { "schemaVersion": 1, "id": "explayerenderer", - "version": "3.0.0-alpha.1", + "version": "3.0.0-alpha.2", "name": "ExtraPlayerRenderer", "description": "Render an extra player figure on your screen. Made for game streaming and recordings.",