diff options
Diffstat (limited to 'src/main')
72 files changed, 2082 insertions, 608 deletions
diff --git a/src/main/java/moe/nea/firmament/init/AutoDiscoveryPlugin.java b/src/main/java/moe/nea/firmament/init/AutoDiscoveryPlugin.java index 0713068..a9db7f9 100644 --- a/src/main/java/moe/nea/firmament/init/AutoDiscoveryPlugin.java +++ b/src/main/java/moe/nea/firmament/init/AutoDiscoveryPlugin.java @@ -1,6 +1,9 @@ package moe.nea.firmament.init; +import moe.nea.firmament.util.ErrorUtil; +import moe.nea.firmament.util.compatloader.ICompatMeta; + import java.io.File; import java.io.IOException; import java.net.MalformedURLException; @@ -94,7 +97,7 @@ public class AutoDiscoveryPlugin { String norm = (className.substring(0, className.length() - ".class".length())) .replace("\\", "/") .replace("/", "."); - if (norm.startsWith(getMixinPackage() + ".") && !norm.endsWith(".")) { + if (norm.startsWith(getMixinPackage() + ".") && !norm.endsWith(".") && ICompatMeta.Companion.shouldLoad(norm)) { mixins.add(norm.substring(getMixinPackage().length() + 1)); } } @@ -125,24 +128,25 @@ public class AutoDiscoveryPlugin { */ public List<String> getMixins() { if (mixins != null) return mixins; - System.out.println("Trying to discover mixins"); - mixins = new ArrayList<>(); - URL classUrl = getClass().getProtectionDomain().getCodeSource().getLocation(); - System.out.println("Found classes at " + classUrl); - tryDiscoverFromContentFile(classUrl); - var classRoots = System.getProperty("firmament.classroots"); - if (classRoots != null && !classRoots.isBlank()) { - System.out.println("Found firmament class roots: " + classRoots); - for (String s : classRoots.split(File.pathSeparator)) { - if (s.isBlank()) { - continue; - } - try { + try { + System.out.println("Trying to discover mixins"); + mixins = new ArrayList<>(); + URL classUrl = getClass().getProtectionDomain().getCodeSource().getLocation(); + System.out.println("Found classes at " + classUrl); + tryDiscoverFromContentFile(classUrl); + var classRoots = System.getProperty("firmament.classroots"); + if (classRoots != null && !classRoots.isBlank()) { + System.out.println("Found firmament class roots: " + classRoots); + for (String s : classRoots.split(File.pathSeparator)) { + if (s.isBlank()) { + continue; + } tryDiscoverFromContentFile(new File(s).toURI().toURL()); - } catch (MalformedURLException e) { - throw new RuntimeException(e); } } + } catch (Exception e) { + e.printStackTrace(); + System.exit(1); } return mixins; } diff --git a/src/main/java/moe/nea/firmament/mixins/AppendRepoAsResourcePack.java b/src/main/java/moe/nea/firmament/mixins/AppendRepoAsResourcePack.java index 22ce991..d8e35d7 100644 --- a/src/main/java/moe/nea/firmament/mixins/AppendRepoAsResourcePack.java +++ b/src/main/java/moe/nea/firmament/mixins/AppendRepoAsResourcePack.java @@ -1,28 +1,34 @@ package moe.nea.firmament.mixins; +import com.llamalad7.mixinextras.sugar.Local; import moe.nea.firmament.repo.RepoModResourcePack; import net.fabricmc.fabric.api.resource.ModResourcePack; +import net.fabricmc.fabric.impl.resource.loader.ModResourcePackSorter; import net.fabricmc.fabric.impl.resource.loader.ModResourcePackUtil; +import net.fabricmc.loader.api.FabricLoader; import net.minecraft.resource.ResourceType; import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import java.util.List; @Mixin(ModResourcePackUtil.class) public class AppendRepoAsResourcePack { - @Inject(method = "appendModResourcePacks", at = @At("TAIL")) - private static void onAppendModResourcePack( - List<ModResourcePack> packs, - ResourceType type, - @Nullable String subPath, - CallbackInfo ci - ) { - RepoModResourcePack.Companion.append(packs); - } + @Inject( + method = "getModResourcePacks", + at = @At(value = "INVOKE", target = "Lnet/fabricmc/fabric/impl/resource/loader/ModResourcePackSorter;getPacks()Ljava/util/List;"), + require = 0 + ) + private static void onAppendModResourcePack( + FabricLoader fabricLoader, ResourceType type, @Nullable String subPath, CallbackInfoReturnable<List<ModResourcePack>> cir, + @Local ModResourcePackSorter sorter + ) { + RepoModResourcePack.Companion.append(sorter); + } } diff --git a/src/main/java/moe/nea/firmament/mixins/DisableHurtCam.java b/src/main/java/moe/nea/firmament/mixins/DisableHurtCam.java new file mode 100644 index 0000000..ed7a2d4 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/DisableHurtCam.java @@ -0,0 +1,18 @@ +package moe.nea.firmament.mixins; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import moe.nea.firmament.features.fixes.Fixes; +import net.minecraft.client.render.GameRenderer; +import org.objectweb.asm.Opcodes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(GameRenderer.class) +public class DisableHurtCam { + @ModifyExpressionValue(method = "tiltViewWhenHurt", at = @At(value = "FIELD", target = "Lnet/minecraft/entity/LivingEntity;hurtTime:I", opcode = Opcodes.GETFIELD)) + private int replaceHurtTime(int original) { + if (Fixes.TConfig.INSTANCE.getNoHurtCam()) + return 0; + return original; + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/EntityUpdateEventListener.java b/src/main/java/moe/nea/firmament/mixins/EntityUpdateEventListener.java index c2d6e46..d956da9 100644 --- a/src/main/java/moe/nea/firmament/mixins/EntityUpdateEventListener.java +++ b/src/main/java/moe/nea/firmament/mixins/EntityUpdateEventListener.java @@ -12,6 +12,7 @@ import net.minecraft.entity.Entity; import net.minecraft.entity.LivingEntity; import net.minecraft.network.ClientConnection; import net.minecraft.network.packet.s2c.play.EntityAttributesS2CPacket; +import net.minecraft.network.packet.s2c.play.EntityEquipmentUpdateS2CPacket; import net.minecraft.network.packet.s2c.play.EntityTrackerUpdateS2CPacket; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; @@ -22,21 +23,26 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(ClientPlayNetworkHandler.class) public abstract class EntityUpdateEventListener extends ClientCommonNetworkHandler { - @Shadow - private ClientWorld world; + @Shadow + private ClientWorld world; - protected EntityUpdateEventListener(MinecraftClient client, ClientConnection connection, ClientConnectionState connectionState) { - super(client, connection, connectionState); - } + protected EntityUpdateEventListener(MinecraftClient client, ClientConnection connection, ClientConnectionState connectionState) { + super(client, connection, connectionState); + } - @Inject(method = "onEntityAttributes", at = @At("TAIL")) - private void onAttributeUpdate(EntityAttributesS2CPacket packet, CallbackInfo ci) { - EntityUpdateEvent.Companion.publish(new EntityUpdateEvent.AttributeUpdate( - (LivingEntity) world.getEntityById(packet.getEntityId()), packet.getEntries())); - } + @Inject(method = "onEntityEquipmentUpdate", at = @At(value = "INVOKE", target = "Ljava/util/List;forEach(Ljava/util/function/Consumer;)V", shift = At.Shift.AFTER)) + private void onEquipmentUpdate(EntityEquipmentUpdateS2CPacket packet, CallbackInfo ci, @Local LivingEntity entity) { + EntityUpdateEvent.Companion.publish(new EntityUpdateEvent.EquipmentUpdate(entity, packet.getEquipmentList())); + } - @Inject(method = "onEntityTrackerUpdate", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/data/DataTracker;writeUpdatedEntries(Ljava/util/List;)V", shift = At.Shift.AFTER)) - private void onEntityTracker(EntityTrackerUpdateS2CPacket packet, CallbackInfo ci, @Local Entity entity) { - EntityUpdateEvent.Companion.publish(new EntityUpdateEvent.TrackedDataUpdate(entity, packet.trackedValues())); - } + @Inject(method = "onEntityAttributes", at = @At("TAIL")) + private void onAttributeUpdate(EntityAttributesS2CPacket packet, CallbackInfo ci) { + EntityUpdateEvent.Companion.publish(new EntityUpdateEvent.AttributeUpdate( + (LivingEntity) world.getEntityById(packet.getEntityId()), packet.getEntries())); + } + + @Inject(method = "onEntityTrackerUpdate", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/data/DataTracker;writeUpdatedEntries(Ljava/util/List;)V", shift = At.Shift.AFTER)) + private void onEntityTracker(EntityTrackerUpdateS2CPacket packet, CallbackInfo ci, @Local Entity entity) { + EntityUpdateEvent.Companion.publish(new EntityUpdateEvent.TrackedDataUpdate(entity, packet.trackedValues())); + } } diff --git a/src/main/java/moe/nea/firmament/mixins/LenientProfileComponentPatch.java b/src/main/java/moe/nea/firmament/mixins/LenientProfileComponentPatch.java deleted file mode 100644 index 76b34ba..0000000 --- a/src/main/java/moe/nea/firmament/mixins/LenientProfileComponentPatch.java +++ /dev/null @@ -1,25 +0,0 @@ - -package moe.nea.firmament.mixins; - -import com.llamalad7.mixinextras.injector.ModifyExpressionValue; -import com.mojang.serialization.Codec; -import com.mojang.serialization.DataResult; -import com.mojang.serialization.Lifecycle; -import com.mojang.util.UndashedUuid; -import moe.nea.firmament.util.json.FirmCodecs; -import net.minecraft.component.type.ProfileComponent; -import net.minecraft.util.Uuids; -import org.objectweb.asm.Opcodes; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; - -import java.util.UUID; - -@Mixin(ProfileComponent.class) -public class LenientProfileComponentPatch { - // lambda in RecordCodecBuilder.create for BASE_CODEC - @ModifyExpressionValue(method = "method_57508", at = @At(value = "FIELD", opcode = Opcodes.GETSTATIC, target = "Lnet/minecraft/util/Uuids;INT_STREAM_CODEC:Lcom/mojang/serialization/Codec;")) - private static Codec<UUID> onStaticInit(Codec<UUID> original) { - return FirmCodecs.UUID_LENIENT_PREFER_INT_STREAM; - } -} diff --git a/src/main/java/moe/nea/firmament/mixins/PropertySignatureIgnorePatch.java b/src/main/java/moe/nea/firmament/mixins/PropertySignatureIgnorePatch.java deleted file mode 100644 index e7331c5..0000000 --- a/src/main/java/moe/nea/firmament/mixins/PropertySignatureIgnorePatch.java +++ /dev/null @@ -1,36 +0,0 @@ - - -package moe.nea.firmament.mixins; - -import com.mojang.authlib.properties.Property; -import moe.nea.firmament.features.fixes.Fixes; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; - -import java.security.PublicKey; - -@Mixin(value = Property.class, remap = false) -public class PropertySignatureIgnorePatch { - @Inject(method = "isSignatureValid", cancellable = true, at = @At("HEAD"), remap = false) - public void onValidateSignature(PublicKey publicKey, CallbackInfoReturnable<Boolean> cir) { - if (Fixes.TConfig.INSTANCE.getFixUnsignedPlayerSkins()) { - cir.setReturnValue(true); - } - } - - @Inject(method = "signature", cancellable = true, at = @At("HEAD"), remap = false) - public void returnEmptySignatureInsteadOfNull(CallbackInfoReturnable<String> cir) { - if (Fixes.TConfig.INSTANCE.getFixUnsignedPlayerSkins()) { - cir.setReturnValue(""); - } - } - - @Inject(method = "hasSignature", cancellable = true, at = @At("HEAD"), remap = false) - public void onHasSignature(CallbackInfoReturnable<Boolean> cir) { - if (Fixes.TConfig.INSTANCE.getFixUnsignedPlayerSkins()) { - cir.setReturnValue(true); - } - } -} diff --git a/src/main/java/moe/nea/firmament/mixins/accessor/AccessorWorldRenderer.java b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorWorldRenderer.java new file mode 100644 index 0000000..8b25562 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorWorldRenderer.java @@ -0,0 +1,17 @@ +package moe.nea.firmament.mixins.accessor; + +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import net.minecraft.client.render.WorldRenderer; +import net.minecraft.entity.player.BlockBreakingInfo; +import org.jetbrains.annotations.NotNull; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import java.util.SortedSet; + +@Mixin(WorldRenderer.class) +public interface AccessorWorldRenderer { + @Accessor("blockBreakingProgressions") + @NotNull + Long2ObjectMap<SortedSet<BlockBreakingInfo>> getBlockBreakingProgressions_firmament(); +} diff --git a/src/main/java/moe/nea/firmament/mixins/render/entitytints/ChangeColorOfLivingEntities.java b/src/main/java/moe/nea/firmament/mixins/render/entitytints/ChangeColorOfLivingEntities.java new file mode 100644 index 0000000..2b96e5c --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/render/entitytints/ChangeColorOfLivingEntities.java @@ -0,0 +1,62 @@ +package moe.nea.firmament.mixins.render.entitytints; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import com.llamalad7.mixinextras.sugar.Local; +import moe.nea.firmament.events.EntityRenderTintEvent; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.entity.LivingEntityRenderer; +import net.minecraft.client.render.entity.model.EntityModel; +import net.minecraft.client.render.entity.state.LivingEntityRenderState; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.entity.LivingEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyArg; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * Applies various rendering modifications from {@link EntityRenderTintEvent} + */ +@Mixin(LivingEntityRenderer.class) +public class ChangeColorOfLivingEntities<T extends LivingEntity, S extends LivingEntityRenderState, M extends EntityModel<? super S>> { + @ModifyReturnValue(method = "getMixColor", at = @At("RETURN")) + private int changeColor(int original, @Local(argsOnly = true) S state) { + var tintState = EntityRenderTintEvent.HasTintRenderState.cast(state); + if (tintState.getHasTintOverride_firmament()) + return tintState.getTint_firmament(); + return original; + } + + @ModifyArg( + method = "getOverlay", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/OverlayTexture;getU(F)I"), + allow = 1 + ) + private static float modifyLightOverlay(float originalWhiteOffset, @Local(argsOnly = true) LivingEntityRenderState state) { + var tintState = EntityRenderTintEvent.HasTintRenderState.cast(state); + if (tintState.getHasTintOverride_firmament() || tintState.getOverlayTexture_firmament() != null) { + return 1F; // TODO: add interpolation percentage to render state extension + } + return originalWhiteOffset; + } + + @Inject(method = "render(Lnet/minecraft/client/render/entity/state/LivingEntityRenderState;Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;I)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/util/math/MatrixStack;pop()V")) + private void afterRender(S livingEntityRenderState, MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int i, CallbackInfo ci) { + var tintState = EntityRenderTintEvent.HasTintRenderState.cast(livingEntityRenderState); + var overlayTexture = tintState.getOverlayTexture_firmament(); + if (overlayTexture != null && vertexConsumerProvider instanceof VertexConsumerProvider.Immediate imm) { + imm.drawCurrentLayer(); + } + EntityRenderTintEvent.overlayOverride = null; + } + + @Inject(method = "render(Lnet/minecraft/client/render/entity/state/LivingEntityRenderState;Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;I)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/util/math/MatrixStack;push()V")) + private void beforeRender(S livingEntityRenderState, MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int i, CallbackInfo ci) { + var tintState = EntityRenderTintEvent.HasTintRenderState.cast(livingEntityRenderState); + var overlayTexture = tintState.getOverlayTexture_firmament(); + if (overlayTexture != null) { + EntityRenderTintEvent.overlayOverride = overlayTexture; + } + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/render/entitytints/EntityRenderStateTint.java b/src/main/java/moe/nea/firmament/mixins/render/entitytints/EntityRenderStateTint.java new file mode 100644 index 0000000..1019027 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/render/entitytints/EntityRenderStateTint.java @@ -0,0 +1,55 @@ +package moe.nea.firmament.mixins.render.entitytints; + +import moe.nea.firmament.events.EntityRenderTintEvent; +import moe.nea.firmament.util.render.TintedOverlayTexture; +import net.minecraft.client.render.entity.state.EntityRenderState; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(EntityRenderState.class) +public class EntityRenderStateTint implements EntityRenderTintEvent.HasTintRenderState { + @Unique + int tint = -1; + @Unique + TintedOverlayTexture overlayTexture; + @Unique + boolean hasTintOverride = false; + + @Override + public int getTint_firmament() { + return tint; + } + + @Override + public void setTint_firmament(int i) { + tint = i; + hasTintOverride = true; + } + + @Override + public boolean getHasTintOverride_firmament() { + return hasTintOverride; + } + + @Override + public void setHasTintOverride_firmament(boolean b) { + hasTintOverride = b; + } + + @Override + public void reset_firmament() { + hasTintOverride = false; + overlayTexture = null; + } + + @Override + public @Nullable TintedOverlayTexture getOverlayTexture_firmament() { + return overlayTexture; + } + + @Override + public void setOverlayTexture_firmament(@Nullable TintedOverlayTexture tintedOverlayTexture) { + this.overlayTexture = tintedOverlayTexture; + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/render/entitytints/InjectIntoRenderState.java b/src/main/java/moe/nea/firmament/mixins/render/entitytints/InjectIntoRenderState.java new file mode 100644 index 0000000..7938340 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/render/entitytints/InjectIntoRenderState.java @@ -0,0 +1,30 @@ +package moe.nea.firmament.mixins.render.entitytints; + +import moe.nea.firmament.events.EntityRenderTintEvent; +import net.minecraft.client.render.entity.EntityRenderer; +import net.minecraft.client.render.entity.state.EntityRenderState; +import net.minecraft.entity.Entity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * Dispatches {@link EntityRenderTintEvent} to collect additional render state used by {@link ChangeColorOfLivingEntities} + */ +@Mixin(EntityRenderer.class) +public class InjectIntoRenderState<T extends Entity, S extends EntityRenderState> { + + @Inject( + method = "updateRenderState", + at = @At("RETURN")) + private void onUpdateRenderState(T entity, S state, float tickDelta, CallbackInfo ci) { + var renderState = EntityRenderTintEvent.HasTintRenderState.cast(state); + renderState.reset_firmament(); + var tintEvent = new EntityRenderTintEvent( + entity, + renderState + ); + EntityRenderTintEvent.Companion.publish(tintEvent); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/render/entitytints/ReplaceOverlayTexture.java b/src/main/java/moe/nea/firmament/mixins/render/entitytints/ReplaceOverlayTexture.java new file mode 100644 index 0000000..61e5c65 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/render/entitytints/ReplaceOverlayTexture.java @@ -0,0 +1,24 @@ +package moe.nea.firmament.mixins.render.entitytints; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import moe.nea.firmament.events.EntityRenderTintEvent; +import net.minecraft.client.render.OverlayTexture; +import net.minecraft.client.render.RenderLayer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +/** + * Replaces the overlay texture used by rendering with the override specified in {@link EntityRenderTintEvent#overlayOverride} + */ +@Mixin(RenderLayer.Overlay.class) +public class ReplaceOverlayTexture { + @ModifyExpressionValue( + method = {"method_23555", "method_23556"}, + expect = 2, + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/GameRenderer;getOverlayTexture()Lnet/minecraft/client/render/OverlayTexture;")) + private static OverlayTexture replaceOverlayTexture(OverlayTexture original) { + if (EntityRenderTintEvent.overlayOverride != null) + return EntityRenderTintEvent.overlayOverride; + return original; + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableEquipmentRenderer.java b/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableEquipmentRenderer.java new file mode 100644 index 0000000..d9c174c --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableEquipmentRenderer.java @@ -0,0 +1,34 @@ +package moe.nea.firmament.mixins.render.entitytints; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import moe.nea.firmament.events.EntityRenderTintEvent; +import net.minecraft.client.render.OverlayTexture; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.entity.equipment.EquipmentRenderer; +import net.minecraft.util.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +/** + * Patch to make {@link EquipmentRenderer} use a {@link RenderLayer} that allows uses Minecraft's overlay texture, if a {@link EntityRenderTintEvent#overlayOverride} is specified. + */ +@Mixin(EquipmentRenderer.class) +public class UseOverlayableEquipmentRenderer { + @WrapOperation(method = "render(Lnet/minecraft/client/render/entity/equipment/EquipmentModel$LayerType;Lnet/minecraft/registry/RegistryKey;Lnet/minecraft/client/model/Model;Lnet/minecraft/item/ItemStack;Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;ILnet/minecraft/util/Identifier;)V", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/RenderLayer;getArmorCutoutNoCull(Lnet/minecraft/util/Identifier;)Lnet/minecraft/client/render/RenderLayer;")) + private RenderLayer replace(Identifier texture, Operation<RenderLayer> original) { + if (EntityRenderTintEvent.overlayOverride != null) + return RenderLayer.getEntityTranslucent(texture); + return original.call(texture); + } + + @ModifyExpressionValue(method = "render(Lnet/minecraft/client/render/entity/equipment/EquipmentModel$LayerType;Lnet/minecraft/registry/RegistryKey;Lnet/minecraft/client/model/Model;Lnet/minecraft/item/ItemStack;Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;ILnet/minecraft/util/Identifier;)V", + at = @At(value = "FIELD", target = "Lnet/minecraft/client/render/OverlayTexture;DEFAULT_UV:I")) + private int replaceUvIndex(int original) { + if (EntityRenderTintEvent.overlayOverride != null) + return OverlayTexture.packUv(15, 10); // TODO: store this info in a global alongside overlayOverride + return original; + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableHeadFeatureRenderer.java b/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableHeadFeatureRenderer.java new file mode 100644 index 0000000..07bc5cf --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableHeadFeatureRenderer.java @@ -0,0 +1,25 @@ +package moe.nea.firmament.mixins.render.entitytints; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import moe.nea.firmament.events.EntityRenderTintEvent; +import net.minecraft.client.render.OverlayTexture; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.entity.feature.HeadFeatureRenderer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +/** + * Patch to make {@link HeadFeatureRenderer} use a {@link RenderLayer} that allows uses Minecraft's overlay texture, if a {@link EntityRenderTintEvent#overlayOverride} is specified. + * @see UseOverlayableItemRenderer + */ +@Mixin(HeadFeatureRenderer.class) +public class UseOverlayableHeadFeatureRenderer { + + @ModifyExpressionValue(method = "render(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;ILnet/minecraft/client/render/entity/state/LivingEntityRenderState;FF)V", + at = @At(value = "FIELD", target = "Lnet/minecraft/client/render/OverlayTexture;DEFAULT_UV:I")) + private int replaceUvIndex(int original) { + if (EntityRenderTintEvent.overlayOverride != null) + return OverlayTexture.packUv(15, 10); // TODO: store this info in a global alongside overlayOverride + return original; + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableItemRenderer.java b/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableItemRenderer.java new file mode 100644 index 0000000..620ab2c --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableItemRenderer.java @@ -0,0 +1,25 @@ +package moe.nea.firmament.mixins.render.entitytints; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import moe.nea.firmament.events.EntityRenderTintEvent; +import net.minecraft.client.render.OverlayTexture; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.RenderPhase; +import net.minecraft.client.render.item.ItemRenderState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +/** + * Patch to make {@link ItemRenderState} use a {@link RenderLayer} that allows uses Minecraft's overlay texture. + * + * @see UseOverlayableHeadFeatureRenderer + */ +@Mixin(ItemRenderState.LayerRenderState.class) +public class UseOverlayableItemRenderer { + @ModifyExpressionValue(method = "render", at = @At(value = "FIELD", target = "Lnet/minecraft/client/render/item/ItemRenderState$LayerRenderState;renderLayer:Lnet/minecraft/client/render/RenderLayer;")) + private RenderLayer replace(RenderLayer original) { + if (EntityRenderTintEvent.overlayOverride != null && original instanceof RenderLayer.MultiPhase multiPhase && multiPhase.phases.texture instanceof RenderPhase.Texture texture && texture.getId().isPresent()) + return RenderLayer.getEntityTranslucent(texture.getId().get()); + return original; + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableSkullBlockEntityRenderer.java b/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableSkullBlockEntityRenderer.java new file mode 100644 index 0000000..9905af1 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableSkullBlockEntityRenderer.java @@ -0,0 +1,25 @@ +package moe.nea.firmament.mixins.render.entitytints; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import moe.nea.firmament.events.EntityRenderTintEvent; +import net.minecraft.client.render.OverlayTexture; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.block.entity.SkullBlockEntityRenderer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +/** + * Patch to make {@link SkullBlockEntityRenderer} use a {@link RenderLayer} that allows uses Minecraft's overlay texture, if a {@link EntityRenderTintEvent#overlayOverride} is specified. + */ + +@Mixin(SkullBlockEntityRenderer.class) +public class UseOverlayableSkullBlockEntityRenderer { + @ModifyExpressionValue(method = "renderSkull", + at = @At(value = "FIELD", target = "Lnet/minecraft/client/render/OverlayTexture;DEFAULT_UV:I")) + private static int replaceUvIndex(int original) { + if (EntityRenderTintEvent.overlayOverride != null) + return OverlayTexture.packUv(15, 10); // TODO: store this info in a global alongside overlayOverride + return original; + } + +} diff --git a/src/main/kotlin/Compat.kt b/src/main/kotlin/Compat.kt new file mode 100644 index 0000000..ba3c88d --- /dev/null +++ b/src/main/kotlin/Compat.kt @@ -0,0 +1,11 @@ +package moe.nea.firmament + +import moe.nea.firmament.util.compatloader.CompatMeta +import moe.nea.firmament.util.compatloader.ICompatMeta + +@CompatMeta +object Compat : ICompatMeta { + override fun shouldLoad(): Boolean { + return true + } +} diff --git a/src/main/kotlin/Firmament.kt b/src/main/kotlin/Firmament.kt index 01905c7..0191036 100644 --- a/src/main/kotlin/Firmament.kt +++ b/src/main/kotlin/Firmament.kt @@ -19,6 +19,8 @@ import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents +import net.fabricmc.fabric.api.resource.ResourceManagerHelper +import net.fabricmc.fabric.api.resource.ResourcePackActivationType import net.fabricmc.loader.api.FabricLoader import net.fabricmc.loader.api.Version import net.fabricmc.loader.api.metadata.ModMetadata @@ -49,6 +51,7 @@ import moe.nea.firmament.repo.RepoManager import moe.nea.firmament.util.MC import moe.nea.firmament.util.SBData import moe.nea.firmament.util.data.IDataHolder +import moe.nea.firmament.util.tr object Firmament { val modContainer by lazy { FabricLoader.getInstance().getModContainer(MOD_ID).get() } @@ -117,9 +120,8 @@ object Firmament { @JvmStatic fun onClientInitialize() { FeatureManager.subscribeEvents() - var tick = 0 ClientTickEvents.END_CLIENT_TICK.register(ClientTickEvents.EndTick { instance -> - TickEvent.publish(TickEvent(tick++)) + TickEvent.publish(TickEvent(MC.currentTick++)) }) IDataHolder.registerEvents() RepoManager.initialize() @@ -145,6 +147,12 @@ object Firmament { }) }) ClientInitEvent.publish(ClientInitEvent()) + ResourceManagerHelper.registerBuiltinResourcePack( + identifier("transparent_storage"), + modContainer, + tr("firmament.resourcepack.transparentstorage", "Transparent Firmament Storage Overlay"), + ResourcePackActivationType.NORMAL + ) } diff --git a/src/main/kotlin/commands/rome.kt b/src/main/kotlin/commands/rome.kt index 13acb3c..c3eb03d 100644 --- a/src/main/kotlin/commands/rome.kt +++ b/src/main/kotlin/commands/rome.kt @@ -1,6 +1,7 @@ package moe.nea.firmament.commands import com.mojang.brigadier.CommandDispatcher +import com.mojang.brigadier.arguments.IntegerArgumentType import com.mojang.brigadier.arguments.StringArgumentType.string import io.ktor.client.statement.bodyAsText import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource @@ -15,6 +16,7 @@ import moe.nea.firmament.features.debug.PowerUserTools import moe.nea.firmament.features.inventory.buttons.InventoryButtons import moe.nea.firmament.features.inventory.storageoverlay.StorageOverlayScreen import moe.nea.firmament.features.inventory.storageoverlay.StorageOverviewScreen +import moe.nea.firmament.features.mining.MiningBlockInfoUi import moe.nea.firmament.gui.config.AllConfigsGui import moe.nea.firmament.gui.config.BooleanHandler import moe.nea.firmament.gui.config.ManagedConfig @@ -129,6 +131,15 @@ fun firmamentCommand() = literal("firmament") { } } thenLiteral("repo") { + thenLiteral("checkpr") { + thenArgument("prnum", IntegerArgumentType.integer(1)) { prnum -> + thenExecute { + val prnum = this[prnum] + source.sendFeedback(tr("firmament.repo.reload.pr", "Temporarily reloading repo from PR #${prnum}.")) + RepoManager.downloadOverridenBranch("refs/pull/$prnum/head") + } + } + } thenLiteral("reload") { thenLiteral("fetch") { thenExecute { @@ -217,6 +228,11 @@ fun firmamentCommand() = literal("firmament") { } } } + thenLiteral("blocks") { + thenExecute { + ScreenUtil.setScreenLater(MiningBlockInfoUi.makeScreen()) + } + } thenLiteral("dumpchat") { thenExecute { MC.inGameHud.chatHud.messages.forEach { @@ -246,6 +262,7 @@ fun firmamentCommand() = literal("firmament") { source.sendFeedback(Text.stringifiedTranslatable("firmament.sbinfo.gametype", locrawInfo.gametype)) source.sendFeedback(Text.stringifiedTranslatable("firmament.sbinfo.mode", locrawInfo.mode)) source.sendFeedback(Text.stringifiedTranslatable("firmament.sbinfo.map", locrawInfo.map)) + source.sendFeedback(tr("firmament.sbinfo.custommining", "Custom Mining: ${formatBool(locrawInfo.skyblockLocation?.hasCustomMining ?: false)}")) } } } @@ -254,6 +271,7 @@ fun firmamentCommand() = literal("firmament") { val player = MC.player ?: return@thenExecute player.world.getOtherEntities(player, player.boundingBox.expand(12.0)) .forEach(PowerUserTools::showEntity) + PowerUserTools.showEntity(player) } } thenLiteral("callUrsa") { diff --git a/src/main/kotlin/events/CustomItemModelEvent.kt b/src/main/kotlin/events/CustomItemModelEvent.kt index e7b6eb8..21ee326 100644 --- a/src/main/kotlin/events/CustomItemModelEvent.kt +++ b/src/main/kotlin/events/CustomItemModelEvent.kt @@ -5,28 +5,39 @@ import kotlin.jvm.optionals.getOrNull import net.minecraft.item.ItemStack import net.minecraft.util.Identifier import moe.nea.firmament.util.collections.WeakCache +import moe.nea.firmament.util.mc.IntrospectableItemModelManager // TODO: assert an order on these events data class CustomItemModelEvent( val itemStack: ItemStack, + val itemModelManager: IntrospectableItemModelManager, var overrideModel: Identifier? = null, ) : FirmamentEvent() { companion object : FirmamentEventBus<CustomItemModelEvent>() { val cache = WeakCache.memoize("ItemModelIdentifier", ::getModelIdentifier0) @JvmStatic - fun getModelIdentifier(itemStack: ItemStack?): Identifier? { + fun getModelIdentifier(itemStack: ItemStack?, itemModelManager: IntrospectableItemModelManager): Identifier? { if (itemStack == null) return null - return cache.invoke(itemStack).getOrNull() + return cache.invoke(itemStack, itemModelManager).getOrNull() } - fun getModelIdentifier0(itemStack: ItemStack): Optional<Identifier> { + fun getModelIdentifier0( + itemStack: ItemStack, + itemModelManager: IntrospectableItemModelManager + ): Optional<Identifier> { // TODO: add an error / warning if the model does not exist - return Optional.ofNullable(publish(CustomItemModelEvent(itemStack)).overrideModel) + return Optional.ofNullable(publish(CustomItemModelEvent(itemStack, itemModelManager)).overrideModel) } } fun overrideIfExists(overrideModel: Identifier) { - this.overrideModel = overrideModel + if (itemModelManager.hasModel_firmament(overrideModel)) + this.overrideModel = overrideModel + } + + fun overrideIfEmpty(identifier: Identifier) { + if (overrideModel == null) + overrideModel = identifier } } diff --git a/src/main/kotlin/events/EntityRenderTintEvent.kt b/src/main/kotlin/events/EntityRenderTintEvent.kt new file mode 100644 index 0000000..29b888b --- /dev/null +++ b/src/main/kotlin/events/EntityRenderTintEvent.kt @@ -0,0 +1,66 @@ +package moe.nea.firmament.events + +import net.minecraft.client.render.GameRenderer +import net.minecraft.client.render.OverlayTexture +import net.minecraft.client.render.entity.state.EntityRenderState +import net.minecraft.entity.Entity +import net.minecraft.entity.LivingEntity +import moe.nea.firmament.util.render.TintedOverlayTexture + +/** + * Change the tint color of a [LivingEntity] + */ +class EntityRenderTintEvent( + val entity: Entity, + val renderState: HasTintRenderState +) : FirmamentEvent.Cancellable() { + init { + if (entity !is LivingEntity) { + cancel() + } + } + + companion object : FirmamentEventBus<EntityRenderTintEvent>() { + /** + * Static variable containing an override for [GameRenderer.getOverlayTexture]. Should be only set briefly. + * + * This variable only affects render layers that naturally make use of the overlay texture, have proper overlay UVs set (`overlay u != 0`), and have a shader that makes use of the overlay (does not have the `NO_OVERLAY` flag set in its json definition). + * + * Currently supported layers: [net.minecraft.client.render.entity.equipment.EquipmentRenderer], [net.minecraft.client.render.entity.model.PlayerEntityModel], as well as some others naturally. + * + * @see moe.nea.firmament.mixins.render.entitytints.ReplaceOverlayTexture + * @see TintedOverlayTexture + */ + @JvmField + var overlayOverride: OverlayTexture? = null + } + + @Suppress("PropertyName", "FunctionName") + interface HasTintRenderState { + /** + * Multiplicative tint applied before the overlay. + */ + var tint_firmament: Int + + /** + * Must be set for [tint_firmament] to have any effect. + */ + var hasTintOverride_firmament: Boolean + + // TODO: allow for more specific selection of which layers get tinted + /** + * Specify a [TintedOverlayTexture] to be used. This does not apply to render layers not using the overlay texture. + * @see overlayOverride + */ + var overlayTexture_firmament: TintedOverlayTexture? + fun reset_firmament() + + companion object { + @JvmStatic + fun cast(state: EntityRenderState): HasTintRenderState { + return state as HasTintRenderState + } + } + } + +} diff --git a/src/main/kotlin/events/EntityUpdateEvent.kt b/src/main/kotlin/events/EntityUpdateEvent.kt index d091984..27a90f9 100644 --- a/src/main/kotlin/events/EntityUpdateEvent.kt +++ b/src/main/kotlin/events/EntityUpdateEvent.kt @@ -1,9 +1,11 @@ - package moe.nea.firmament.events +import com.mojang.datafixers.util.Pair import net.minecraft.entity.Entity +import net.minecraft.entity.EquipmentSlot import net.minecraft.entity.LivingEntity import net.minecraft.entity.data.DataTracker +import net.minecraft.item.ItemStack import net.minecraft.network.packet.s2c.play.EntityAttributesS2CPacket /** @@ -13,19 +15,24 @@ import net.minecraft.network.packet.s2c.play.EntityAttributesS2CPacket * *after* the values have been applied to the entity. */ sealed class EntityUpdateEvent : FirmamentEvent() { - companion object : FirmamentEventBus<EntityUpdateEvent>() + companion object : FirmamentEventBus<EntityUpdateEvent>() + + abstract val entity: Entity - abstract val entity: Entity + data class AttributeUpdate( + override val entity: LivingEntity, + val attributes: List<EntityAttributesS2CPacket.Entry>, + ) : EntityUpdateEvent() - data class AttributeUpdate( - override val entity: LivingEntity, - val attributes: List<EntityAttributesS2CPacket.Entry>, - ) : EntityUpdateEvent() + data class TrackedDataUpdate( + override val entity: Entity, + val trackedValues: List<DataTracker.SerializedEntry<*>>, + ) : EntityUpdateEvent() - data class TrackedDataUpdate( - override val entity: Entity, - val trackedValues: List<DataTracker.SerializedEntry<*>>, - ) : EntityUpdateEvent() + data class EquipmentUpdate( + override val entity: Entity, + val newEquipment: List<Pair<EquipmentSlot, ItemStack>>, + ) : EntityUpdateEvent() -// TODO: onEntityPassengersSet, onEntityAttach?, onEntityEquipmentUpdate, onEntityStatusEffect +// TODO: onEntityPassengersSet, onEntityAttach?, onEntityStatusEffect } diff --git a/src/main/kotlin/events/IsSlotProtectedEvent.kt b/src/main/kotlin/events/IsSlotProtectedEvent.kt index eac2d9b..8fe0a96 100644 --- a/src/main/kotlin/events/IsSlotProtectedEvent.kt +++ b/src/main/kotlin/events/IsSlotProtectedEvent.kt @@ -6,6 +6,10 @@ import net.minecraft.screen.slot.SlotActionType import net.minecraft.text.Text import moe.nea.firmament.util.CommonSoundEffects import moe.nea.firmament.util.MC +import moe.nea.firmament.util.grey +import moe.nea.firmament.util.hover +import moe.nea.firmament.util.red +import moe.nea.firmament.util.tr data class IsSlotProtectedEvent( val slot: Slot?, @@ -35,6 +39,7 @@ data class IsSlotProtectedEvent( INVENTORY_MOVE ; } + companion object : FirmamentEventBus<IsSlotProtectedEvent>() { @JvmStatic @JvmOverloads @@ -47,7 +52,11 @@ data class IsSlotProtectedEvent( val event = IsSlotProtectedEvent(slot, action, false, itemStackOverride, origin) publish(event) if (event.isProtected && !event.silent) { - MC.sendChat(Text.translatable("firmament.protectitem").append(event.itemStack.name)) + MC.sendChat(tr("firmament.protectitem", "Firmament protected your item: ${event.itemStack.name}.\n") + .red() + .append(tr("firmament.protectitem.hoverhint", "Hover for more info.").grey()) + .hover(tr("firmament.protectitem.hint", + "To unlock this item use the Lock Slot or Lock Item keybind from Firmament while hovering over this item."))) CommonSoundEffects.playFailure() } return event.isProtected diff --git a/src/main/kotlin/features/FeatureManager.kt b/src/main/kotlin/features/FeatureManager.kt index 9a3cbf8..f0c1857 100644 --- a/src/main/kotlin/features/FeatureManager.kt +++ b/src/main/kotlin/features/FeatureManager.kt @@ -31,89 +31,96 @@ import moe.nea.firmament.features.mining.PickaxeAbility import moe.nea.firmament.features.mining.PristineProfitTracker import moe.nea.firmament.features.world.FairySouls import moe.nea.firmament.features.world.Waypoints +import moe.nea.firmament.util.compatloader.ICompatMeta import moe.nea.firmament.util.data.DataHolder object FeatureManager : DataHolder<FeatureManager.Config>(serializer(), "features", ::Config) { - @Serializable - data class Config( - val enabledFeatures: MutableMap<String, Boolean> = mutableMapOf() - ) + @Serializable + data class Config( + val enabledFeatures: MutableMap<String, Boolean> = mutableMapOf() + ) - private val features = mutableMapOf<String, FirmamentFeature>() + private val features = mutableMapOf<String, FirmamentFeature>() - val allFeatures: Collection<FirmamentFeature> get() = features.values + val allFeatures: Collection<FirmamentFeature> get() = features.values - private var hasAutoloaded = false + private var hasAutoloaded = false - fun autoload() { - synchronized(this) { - if (hasAutoloaded) return - loadFeature(MinorTrolling) - loadFeature(FairySouls) - loadFeature(AutoCompletions) - // TODO: loadFeature(FishingWarning) - loadFeature(SlotLocking) - loadFeature(StorageOverlay) - loadFeature(PristineProfitTracker) - loadFeature(CraftingOverlay) - loadFeature(PowerUserTools) - loadFeature(Waypoints) - loadFeature(ChatLinks) - loadFeature(InventoryButtons) - loadFeature(CompatibliltyFeatures) - loadFeature(AnniversaryFeatures) - loadFeature(QuickCommands) - loadFeature(PetFeatures) - loadFeature(SaveCursorPosition) - loadFeature(PriceData) - loadFeature(Fixes) - loadFeature(DianaWaypoints) - loadFeature(ItemRarityCosmetics) - loadFeature(PickaxeAbility) - loadFeature(CarnivalFeatures) - if (Firmament.DEBUG) { - loadFeature(DeveloperFeatures) - loadFeature(DebugView) - } - allFeatures.forEach { it.config } - FeaturesInitializedEvent.publish(FeaturesInitializedEvent(allFeatures.toList())) - hasAutoloaded = true - } - } + fun autoload() { + synchronized(this) { + if (hasAutoloaded) return + loadFeature(MinorTrolling) + loadFeature(FairySouls) + loadFeature(AutoCompletions) + // TODO: loadFeature(FishingWarning) + loadFeature(SlotLocking) + loadFeature(StorageOverlay) + loadFeature(PristineProfitTracker) + loadFeature(CraftingOverlay) + loadFeature(PowerUserTools) + loadFeature(Waypoints) + loadFeature(ChatLinks) + loadFeature(InventoryButtons) + loadFeature(CompatibliltyFeatures) + loadFeature(AnniversaryFeatures) + loadFeature(QuickCommands) + loadFeature(PetFeatures) + loadFeature(SaveCursorPosition) + loadFeature(PriceData) + loadFeature(Fixes) + loadFeature(DianaWaypoints) + loadFeature(ItemRarityCosmetics) + loadFeature(PickaxeAbility) + loadFeature(CarnivalFeatures) + if (Firmament.DEBUG) { + loadFeature(DeveloperFeatures) + loadFeature(DebugView) + } + allFeatures.forEach { it.config } + FeaturesInitializedEvent.publish(FeaturesInitializedEvent(allFeatures.toList())) + hasAutoloaded = true + } + } - fun subscribeEvents() { - SubscriptionList.allLists.forEach { - it.provideSubscriptions { - it.owner.javaClass.classes.forEach { - runCatching { it.getDeclaredField("INSTANCE").get(null) } + fun subscribeEvents() { + SubscriptionList.allLists.forEach { list -> + if (ICompatMeta.shouldLoad(list.javaClass.name)) + runCatching { + list.provideSubscriptions { + it.owner.javaClass.classes.forEach { + runCatching { it.getDeclaredField("INSTANCE").get(null) } + } + subscribeSingleEvent(it) + } + }.getOrElse { + // TODO: allow annotating source sets to specifically opt out of loading for mods, maybe automatically + Firmament.logger.info("Ignoring events from $list, likely due to a missing compat mod.", it) } - subscribeSingleEvent(it) - } - } - } + } + } - private fun <T : FirmamentEvent> subscribeSingleEvent(it: Subscription<T>) { - it.eventBus.subscribe(false, "${it.owner.javaClass.simpleName}:${it.methodName}", it.invoke) - } + private fun <T : FirmamentEvent> subscribeSingleEvent(it: Subscription<T>) { + it.eventBus.subscribe(false, "${it.owner.javaClass.simpleName}:${it.methodName}", it.invoke) + } - fun loadFeature(feature: FirmamentFeature) { - synchronized(features) { - if (feature.identifier in features) { - Firmament.logger.error("Double registering feature ${feature.identifier}. Ignoring second instance $feature") - return - } - features[feature.identifier] = feature - feature.onLoad() - } - } + fun loadFeature(feature: FirmamentFeature) { + synchronized(features) { + if (feature.identifier in features) { + Firmament.logger.error("Double registering feature ${feature.identifier}. Ignoring second instance $feature") + return + } + features[feature.identifier] = feature + feature.onLoad() + } + } - fun isEnabled(identifier: String): Boolean? = - data.enabledFeatures[identifier] + fun isEnabled(identifier: String): Boolean? = + data.enabledFeatures[identifier] - fun setEnabled(identifier: String, value: Boolean) { - data.enabledFeatures[identifier] = value - markDirty() - } + fun setEnabled(identifier: String, value: Boolean) { + data.enabledFeatures[identifier] = value + markDirty() + } } diff --git a/src/main/kotlin/features/debug/AnimatedClothingScanner.kt b/src/main/kotlin/features/debug/AnimatedClothingScanner.kt new file mode 100644 index 0000000..11b47a9 --- /dev/null +++ b/src/main/kotlin/features/debug/AnimatedClothingScanner.kt @@ -0,0 +1,51 @@ +package moe.nea.firmament.features.debug + +import net.minecraft.component.DataComponentTypes +import net.minecraft.entity.Entity +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.commands.thenLiteral +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.events.EntityUpdateEvent +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.tr + +object AnimatedClothingScanner { + + var observedEntity: Entity? = null + + @OptIn(ExperimentalStdlibApi::class) + @Subscribe + fun onUpdate(event: EntityUpdateEvent) { + if (event.entity != observedEntity) return + if (event is EntityUpdateEvent.EquipmentUpdate) { + event.newEquipment.forEach { + val id = it.second.skyBlockId?.neuItem + val colour = it.second.get(DataComponentTypes.DYED_COLOR) + ?.rgb?.toHexString(HexFormat.UpperCase) + ?.let { " #$it" } ?: "" + MC.sendChat(tr("firmament.fitstealer.update", + "[FIT CHECK][${MC.currentTick}] ${it.first.asString()} => ${id}${colour}")) + } + } + } + + @Subscribe + fun onSubCommand(event: CommandEvent.SubCommand) { + event.subcommand("dev") { + thenLiteral("stealthisfit") { + thenExecute { + observedEntity = + if (observedEntity == null) MC.instance.targetedEntity else null + + MC.sendChat( + observedEntity?.let { + tr("firmament.fitstealer.targeted", "Observing the equipment of ${it.name}.") + } ?: tr("firmament.fitstealer.targetlost", "No longer logging equipment."), + ) + } + } + } + } +} diff --git a/src/main/kotlin/features/debug/PowerUserTools.kt b/src/main/kotlin/features/debug/PowerUserTools.kt index 225bc13..8be5d5d 100644 --- a/src/main/kotlin/features/debug/PowerUserTools.kt +++ b/src/main/kotlin/features/debug/PowerUserTools.kt @@ -1,22 +1,31 @@ package moe.nea.firmament.features.debug +import com.mojang.serialization.Codec +import com.mojang.serialization.DynamicOps import com.mojang.serialization.JsonOps +import com.mojang.serialization.codecs.RecordCodecBuilder import kotlin.jvm.optionals.getOrNull import net.minecraft.block.SkullBlock import net.minecraft.block.entity.SkullBlockEntity import net.minecraft.component.DataComponentTypes import net.minecraft.component.type.ProfileComponent import net.minecraft.entity.Entity +import net.minecraft.entity.EntityType import net.minecraft.entity.LivingEntity import net.minecraft.item.ItemStack import net.minecraft.item.Items +import net.minecraft.nbt.NbtCompound import net.minecraft.nbt.NbtOps +import net.minecraft.nbt.NbtString +import net.minecraft.predicate.NbtPredicate import net.minecraft.text.Text import net.minecraft.text.TextCodecs import net.minecraft.util.Identifier import net.minecraft.util.hit.BlockHitResult import net.minecraft.util.hit.EntityHitResult import net.minecraft.util.hit.HitResult +import net.minecraft.util.math.Position +import net.minecraft.util.math.Vec3d import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.CustomItemModelEvent import moe.nea.firmament.events.HandledScreenKeyPressedEvent @@ -30,6 +39,8 @@ import moe.nea.firmament.mixins.accessor.AccessorHandledScreen import moe.nea.firmament.util.ClipboardUtils import moe.nea.firmament.util.MC import moe.nea.firmament.util.focusedItemStack +import moe.nea.firmament.util.mc.IntrospectableItemModelManager +import moe.nea.firmament.util.mc.SNbtFormatter import moe.nea.firmament.util.mc.SNbtFormatter.Companion.toPrettyString import moe.nea.firmament.util.mc.displayNameAccordingToNbt import moe.nea.firmament.util.mc.loreAccordingToNbt @@ -87,6 +98,11 @@ object PowerUserTools : FirmamentFeature { } fun showEntity(target: Entity) { + val nbt = NbtPredicate.entityToNbt(target) + nbt.remove("Inventory") + nbt.put("StyledName", TextCodecs.CODEC.encodeStart(NbtOps.INSTANCE, target.styledDisplayName).orThrow) + println(SNbtFormatter.prettify(nbt)) + ClipboardUtils.setTextContent(SNbtFormatter.prettify(nbt)) MC.sendChat(Text.translatable("firmament.poweruser.entity.type", target.type)) MC.sendChat(Text.translatable("firmament.poweruser.entity.name", target.name)) MC.sendChat(Text.stringifiedTranslatable("firmament.poweruser.entity.position", target.pos)) @@ -119,7 +135,11 @@ object PowerUserTools : FirmamentFeature { lastCopiedStack = Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.skyblockid", sbId.neuItem)) } else if (it.matches(TConfig.copyTexturePackId)) { - val model = CustomItemModelEvent.getModelIdentifier(item) // TODO: remove global texture overrides, maybe + val model = CustomItemModelEvent.getModelIdentifier0(item, object : IntrospectableItemModelManager { + override fun hasModel_firmament(identifier: Identifier): Boolean { + return true + } + }).getOrNull() // TODO: remove global texture overrides, maybe if (model == null) { lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.modelid.fail")) return diff --git a/src/main/kotlin/features/events/anniversity/CenturyRaffleFeatures.kt b/src/main/kotlin/features/events/anniversity/CenturyRaffleFeatures.kt new file mode 100644 index 0000000..9935051 --- /dev/null +++ b/src/main/kotlin/features/events/anniversity/CenturyRaffleFeatures.kt @@ -0,0 +1,63 @@ +package moe.nea.firmament.features.events.anniversity + +import java.util.Optional +import me.shedaniel.math.Color +import kotlin.jvm.optionals.getOrNull +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.text.Style +import net.minecraft.util.Formatting +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.EntityRenderTintEvent +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.render.TintedOverlayTexture +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.skyblock.SkyBlockItems + +object CenturyRaffleFeatures { + object TConfig : ManagedConfig("centuryraffle", Category.EVENTS) { + val highlightPlayersForSlice by toggle("highlight-cake-players") { true } +// val highlightAllPlayers by toggle("highlight-all-cake-players") { true } + } + + val cakeIcon = "⛃" + + val cakeColors = listOf( + CakeTeam(SkyBlockItems.SLICE_OF_BLUEBERRY_CAKE, Formatting.BLUE), + CakeTeam(SkyBlockItems.SLICE_OF_CHEESECAKE, Formatting.YELLOW), + CakeTeam(SkyBlockItems.SLICE_OF_GREEN_VELVET_CAKE, Formatting.GREEN), + CakeTeam(SkyBlockItems.SLICE_OF_RED_VELVET_CAKE, Formatting.RED), + CakeTeam(SkyBlockItems.SLICE_OF_STRAWBERRY_SHORTCAKE, Formatting.LIGHT_PURPLE), + ) + + data class CakeTeam( + val id: SkyblockId, + val formatting: Formatting, + ) { + val searchedTextRgb = formatting.colorValue!! + val brightenedRgb = Color.ofOpaque(searchedTextRgb)//.brighter(2.0) + val tintOverlay by lazy { + TintedOverlayTexture().setColor(brightenedRgb) + } + } + + val sliceToColor = cakeColors.associateBy { it.id } + + @Subscribe + fun onEntityRender(event: EntityRenderTintEvent) { + if (!TConfig.highlightPlayersForSlice) return + val requestedCakeTeam = sliceToColor[MC.stackInHand?.skyBlockId] ?: return + // TODO: cache the requested color + val player = event.entity as? PlayerEntity ?: return + val cakeColor: Style = player.styledDisplayName.visit( + { style, text -> + if (text == cakeIcon) Optional.of(style) + else Optional.empty() + }, Style.EMPTY).getOrNull() ?: return + if (cakeColor.color?.rgb == requestedCakeTeam.searchedTextRgb) { + event.renderState.overlayTexture_firmament = requestedCakeTeam.tintOverlay + } + } + +} diff --git a/src/main/kotlin/features/fixes/Fixes.kt b/src/main/kotlin/features/fixes/Fixes.kt index 7030319..3dae233 100644 --- a/src/main/kotlin/features/fixes/Fixes.kt +++ b/src/main/kotlin/features/fixes/Fixes.kt @@ -23,6 +23,7 @@ object Fixes : FirmamentFeature { val autoSprintHud by position("auto-sprint-hud", 80, 10) { Point(0.0, 1.0) } val peekChat by keyBindingWithDefaultUnbound("peek-chat") val hidePotionEffects by toggle("hide-mob-effects") { false } + val noHurtCam by toggle("disable-hurt-cam") { false } } override val config: ManagedConfig diff --git a/src/main/kotlin/features/inventory/SlotLocking.kt b/src/main/kotlin/features/inventory/SlotLocking.kt index 7a3a152..0083c40 100644 --- a/src/main/kotlin/features/inventory/SlotLocking.kt +++ b/src/main/kotlin/features/inventory/SlotLocking.kt @@ -4,8 +4,17 @@ package moe.nea.firmament.features.inventory import java.util.UUID import org.lwjgl.glfw.GLFW +import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.int import kotlinx.serialization.serializer import net.minecraft.client.gui.screen.ingame.HandledScreen import net.minecraft.entity.player.PlayerInventory @@ -51,9 +60,66 @@ object SlotLocking : FirmamentFeature { val lockedSlots: MutableSet<Int> = mutableSetOf(), val lockedSlotsRift: MutableSet<Int> = mutableSetOf(), val lockedUUIDs: MutableSet<UUID> = mutableSetOf(), - val boundSlots: MutableMap<Int, Int> = mutableMapOf() + val boundSlots: BoundSlots = BoundSlots() ) + @Serializable + data class BoundSlot( + val hotbar: Int, + val inventory: Int, + ) + + @Serializable(with = BoundSlots.Serializer::class) + data class BoundSlots( + val pairs: MutableSet<BoundSlot> = mutableSetOf() + ) { + fun findMatchingSlots(index: Int): List<BoundSlot> { + return pairs.filter { it.hotbar == index || it.inventory == index } + } + + fun removeDuplicateForInventory(index: Int) { + pairs.removeIf { it.inventory == index } + } + + fun removeAllInvolving(index: Int): Boolean { + return pairs.removeIf { it.inventory == index || it.hotbar == index } + } + + fun insert(hotbar: Int, inventory: Int) { + if (!TConfig.allowMultiBinding) { + removeAllInvolving(hotbar) + removeAllInvolving(inventory) + } + pairs.add(BoundSlot(hotbar, inventory)) + } + + object Serializer : KSerializer<BoundSlots> { + override val descriptor: SerialDescriptor + get() = serializer<JsonElement>().descriptor + + override fun serialize( + encoder: Encoder, + value: BoundSlots + ) { + serializer<MutableSet<BoundSlot>>() + .serialize(encoder, value.pairs) + } + + override fun deserialize(decoder: Decoder): BoundSlots { + decoder as JsonDecoder + val json = decoder.decodeJsonElement() + if (json is JsonObject) { + return BoundSlots(json.entries.map { + BoundSlot(it.key.toInt(), (it.value as JsonPrimitive).int) + }.toMutableSet()) + } + return BoundSlots(decoder.json.decodeFromJsonElement(serializer<MutableSet<BoundSlot>>(), json)) + + } + } + } + + object TConfig : ManagedConfig(identifier, Category.INVENTORY) { val lockSlot by keyBinding("lock") { GLFW.GLFW_KEY_L } val lockUUID by keyBindingWithOutDefaultModifiers("lock-uuid") { @@ -62,6 +128,7 @@ object SlotLocking : FirmamentFeature { val slotBind by keyBinding("bind") { GLFW.GLFW_KEY_L } val slotBindRequireShift by toggle("require-quick-move") { true } val slotRenderLines by choice("bind-render") { SlotRenderLinesMode.ONLY_BOXES } + val allowMultiBinding by toggle("multi-bind") { true } // TODO: filter based on this option val allowDroppingInDungeons by toggle("drop-in-dungeons") { true } } @@ -177,19 +244,19 @@ object SlotLocking : FirmamentFeature { @Subscribe fun onQuickMoveBoundSlot(it: IsSlotProtectedEvent) { - val boundSlots = DConfig.data?.boundSlots ?: mapOf() + val boundSlots = DConfig.data?.boundSlots ?: BoundSlots() val isValidAction = it.actionType == SlotActionType.QUICK_MOVE || (it.actionType == SlotActionType.PICKUP && !TConfig.slotBindRequireShift) if (!isValidAction) return val handler = MC.handledScreen?.screenHandler ?: return val slot = it.slot if (slot != null && it.slot.inventory is PlayerInventory) { - val boundSlot = boundSlots.entries.find { - it.value == slot.index || it.key == slot.index - } ?: return + val matchingSlots = boundSlots.findMatchingSlots(slot.index) + if (matchingSlots.isEmpty()) return it.protectSilent() - val inventorySlot = MC.handledScreen?.getSlotByIndex(boundSlot.value, true) - inventorySlot?.swapWithHotBar(handler, boundSlot.key) + val boundSlot = matchingSlots.singleOrNull() ?: return + val inventorySlot = MC.handledScreen?.getSlotByIndex(boundSlot.inventory, true) + inventorySlot?.swapWithHotBar(handler, boundSlot.hotbar) } } @@ -228,10 +295,8 @@ object SlotLocking : FirmamentFeature { val boundSlots = DConfig.data?.boundSlots ?: return lockedSlots?.remove(hotBarSlot.index) lockedSlots?.remove(invSlot.index) - boundSlots.entries.removeIf { - it.value == invSlot.index - } - boundSlots[hotBarSlot.index] = invSlot.index + boundSlots.removeDuplicateForInventory(invSlot.index) + boundSlots.insert(hotBarSlot.index, invSlot.index) DConfig.markDirty() CommonSoundEffects.playSuccess() return @@ -245,9 +310,7 @@ object SlotLocking : FirmamentFeature { storedLockingSlot = null val boundSlots = DConfig.data?.boundSlots ?: return if (slot != null) - boundSlots.entries.removeIf { - it.value == slot.index || it.key == slot.index - } + boundSlots.removeAllInvolving(slot.index) } } @@ -258,9 +321,10 @@ object SlotLocking : FirmamentFeature { val accScreen = event.screen as AccessorHandledScreen val sx = accScreen.x_Firmament val sy = accScreen.y_Firmament - for (it in boundSlots.entries) { - val hotbarSlot = findByIndex(it.key) ?: continue - val inventorySlot = findByIndex(it.value) ?: continue + val highlitSlots = mutableSetOf<Slot>() + for (it in boundSlots.pairs) { + val hotbarSlot = findByIndex(it.hotbar) ?: continue + val inventorySlot = findByIndex(it.inventory) ?: continue val (hotX, hotY) = hotbarSlot.lineCenter() val (invX, invY) = inventorySlot.lineCenter() @@ -268,22 +332,27 @@ object SlotLocking : FirmamentFeature { || accScreen.focusedSlot_Firmament === inventorySlot if (!anyHovered && TConfig.slotRenderLines == SlotRenderLinesMode.NOTHING) continue - val color = if (anyHovered) - me.shedaniel.math.Color.ofOpaque(0x00FF00) - else - me.shedaniel.math.Color.ofTransparent(0xc0a0f000.toInt()) + if (anyHovered) { + highlitSlots.add(hotbarSlot) + highlitSlots.add(inventorySlot) + } + fun color(highlit: Boolean) = + if (highlit) + me.shedaniel.math.Color.ofOpaque(0x00FF00) + else + me.shedaniel.math.Color.ofTransparent(0xc0a0f000.toInt()) if (TConfig.slotRenderLines == SlotRenderLinesMode.EVERYTHING || anyHovered) event.context.drawLine( invX + sx, invY + sy, hotX + sx, hotY + sy, - color + color(anyHovered) ) event.context.drawBorder(hotbarSlot.x + sx, hotbarSlot.y + sy, - 16, 16, color.color) + 16, 16, color(hotbarSlot in highlitSlots).color) event.context.drawBorder(inventorySlot.x + sx, inventorySlot.y + sy, - 16, 16, color.color) + 16, 16, color(inventorySlot in highlitSlots).color) } } @@ -339,11 +408,9 @@ object SlotLocking : FirmamentFeature { fun toggleSlotLock(slot: Slot) { val lockedSlots = lockedSlots ?: return - val boundSlots = DConfig.data?.boundSlots ?: mutableMapOf() + val boundSlots = DConfig.data?.boundSlots ?: BoundSlots() if (slot.inventory is PlayerInventory) { - if (boundSlots.entries.removeIf { - it.value == slot.index || it.key == slot.index - }) { + if (boundSlots.removeAllInvolving(slot.index)) { // intentionally do nothing } else if (slot.index in lockedSlots) { lockedSlots.remove(slot.index) diff --git a/src/main/kotlin/features/inventory/TimerInLore.kt b/src/main/kotlin/features/inventory/TimerInLore.kt index f1b77c6..309ea61 100644 --- a/src/main/kotlin/features/inventory/TimerInLore.kt +++ b/src/main/kotlin/features/inventory/TimerInLore.kt @@ -80,7 +80,8 @@ object TimerInLore { COMMUNITYPROJECTS("Contribute again", "Come back at"), CHOCOLATEFACTORY("Next Charge", "Available at"), STONKSAUCTION("Auction ends in", "Ends at"), - LIZSTONKREDEMPTION("Resets in:", "Resets at"); + LIZSTONKREDEMPTION("Resets in:", "Resets at"), + EVENTENDING("Event ends in:", "Ends at"); } val regex = diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt index 633a8fe..63a2f54 100644 --- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt @@ -357,6 +357,10 @@ class StorageOverlayScreen : Screen(Text.literal("")) { return super.keyReleased(keyCode, scanCode, modifiers) } + override fun shouldCloseOnEsc(): Boolean { + return this === MC.screen // Fixes this UI closing the handled screen on Escape press. + } + override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { if (typeMCComponentInPlace( controlComponent, diff --git a/src/main/kotlin/features/mining/MiningBlockInfoUi.kt b/src/main/kotlin/features/mining/MiningBlockInfoUi.kt new file mode 100644 index 0000000..e8ea4f4 --- /dev/null +++ b/src/main/kotlin/features/mining/MiningBlockInfoUi.kt @@ -0,0 +1,54 @@ +package moe.nea.firmament.features.mining + +import io.github.notenoughupdates.moulconfig.observer.ObservableList +import io.github.notenoughupdates.moulconfig.observer.Property +import io.github.notenoughupdates.moulconfig.platform.ModernItemStack +import io.github.notenoughupdates.moulconfig.xml.Bind +import net.minecraft.client.gui.screen.Screen +import net.minecraft.item.ItemStack +import moe.nea.firmament.repo.MiningRepoData +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.util.MoulConfigUtils +import moe.nea.firmament.util.SkyBlockIsland + +object MiningBlockInfoUi { + class MiningInfo(miningData: MiningRepoData) { + @field:Bind("search") + @JvmField + var search = "" + + @get:Bind("ores") + val blocks = miningData.customMiningBlocks.mapTo(ObservableList(mutableListOf())) { OreInfo(it, this) } + } + + class OreInfo(block: MiningRepoData.CustomMiningBlock, info: MiningInfo) { + @get:Bind("oreName") + val oreName = block.name ?: "No Name" + + @get:Bind("blocks") + val res = ObservableList(block.blocks189.map { BlockInfo(it, info) }) + } + + class BlockInfo(val block: MiningRepoData.Block189, val info: MiningInfo) { + @get:Bind("item") + val item = ModernItemStack.of(block.block?.let { ItemStack(it) } ?: ItemStack.EMPTY) + + @get:Bind("isSelected") + val isSelected get() = info.search.let { block.isActiveIn(SkyBlockIsland.forMode(it)) } + + @get:Bind("itemName") + val itemName get() = item.getDisplayName() + + @get:Bind("restrictions") + val res = ObservableList( + if (block.onlyIn != null) + block.onlyIn.map { " §r- §a${it.userFriendlyName}" } + else + listOf("Everywhere") + ) + } + + fun makeScreen(): Screen { + return MoulConfigUtils.loadScreen("mining_block_info/index", MiningInfo(RepoManager.miningData), null) + } +} diff --git a/src/main/kotlin/features/world/ColeWeightCompat.kt b/src/main/kotlin/features/world/ColeWeightCompat.kt new file mode 100644 index 0000000..b92a91e --- /dev/null +++ b/src/main/kotlin/features/world/ColeWeightCompat.kt @@ -0,0 +1,125 @@ +package moe.nea.firmament.features.world + +import kotlinx.serialization.Serializable +import net.minecraft.text.Text +import net.minecraft.util.math.BlockPos +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.DefaultSource +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.commands.thenLiteral +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.util.ClipboardUtils +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.tr + +object ColeWeightCompat { + @Serializable + data class ColeWeightWaypoint( + val x: Int, + val y: Int, + val z: Int, + val r: Int = 0, + val g: Int = 0, + val b: Int = 0, + ) + + fun fromFirm(waypoints: FirmWaypoints, relativeTo: BlockPos): List<ColeWeightWaypoint> { + return waypoints.waypoints.map { + ColeWeightWaypoint(it.x - relativeTo.x, it.y - relativeTo.y, it.z - relativeTo.z) + } + } + + fun intoFirm(waypoints: List<ColeWeightWaypoint>, relativeTo: BlockPos): FirmWaypoints { + val w = waypoints.map { + FirmWaypoints.Waypoint(it.x + relativeTo.x, it.y + relativeTo.y, it.z + relativeTo.z) + } + return FirmWaypoints( + "Imported Waypoints", + "imported", + null, + w.toMutableList(), + false + ) + } + + fun copyAndInform( + source: DefaultSource, + origin: BlockPos, + positiveFeedback: (Int) -> Text, + ) { + val waypoints = Waypoints.useNonEmptyWaypoints() + ?.let { fromFirm(it, origin) } + if (waypoints == null) { + source.sendError(Waypoints.textNothingToExport()) + return + } + val data = + Firmament.tightJson.encodeToString<List<ColeWeightWaypoint>>(waypoints) + ClipboardUtils.setTextContent(data) + source.sendFeedback(positiveFeedback(waypoints.size)) + } + + fun importAndInform( + source: DefaultSource, + pos: BlockPos?, + positiveFeedback: (Int) -> Text + ) { + val text = ClipboardUtils.getTextContents() + val wr = tryParse(text).map { intoFirm(it, pos ?: BlockPos.ORIGIN) } + val waypoints = wr.getOrElse { + source.sendError( + tr("firmament.command.waypoint.import.cw.error", + "Could not import ColeWeight waypoints.")) + Firmament.logger.error(it) + return + } + waypoints.lastRelativeImport = pos + Waypoints.waypoints = waypoints + source.sendFeedback(positiveFeedback(waypoints.size)) + } + + @Subscribe + fun onEvent(event: CommandEvent.SubCommand) { + event.subcommand(Waypoints.WAYPOINTS_SUBCOMMAND) { + thenLiteral("exportcw") { + thenExecute { + copyAndInform(source, BlockPos.ORIGIN) { + tr("firmament.command.waypoint.export.cw", + "Copied $it waypoints to clipboard in ColeWeight format.") + } + } + } + thenLiteral("exportrelativecw") { + thenExecute { + copyAndInform(source, MC.player?.blockPos ?: BlockPos.ORIGIN) { + tr("firmament.command.waypoint.export.cw.relative", + "Copied $it relative waypoints to clipboard in ColeWeight format. Make sure to stand in the same position when importing.") + } + } + } + thenLiteral("importcw") { + thenExecute { + importAndInform(source, null) { + Text.stringifiedTranslatable("firmament.command.waypoint.import.cw", + it) + } + } + } + thenLiteral("importrelativecw") { + thenExecute { + importAndInform(source, MC.player!!.blockPos) { + tr("firmament.command.waypoint.import.cw.relative", + "Imported $it relative waypoints from clipboard. Make sure you stand in the same position as when you exported these waypoints for them to line up correctly.") + } + } + } + } + } + + fun tryParse(string: String): Result<List<ColeWeightWaypoint>> { + return runCatching { + Firmament.tightJson.decodeFromString<List<ColeWeightWaypoint>>(string) + } + } +} diff --git a/src/main/kotlin/features/world/FirmWaypointManager.kt b/src/main/kotlin/features/world/FirmWaypointManager.kt new file mode 100644 index 0000000..d18483c --- /dev/null +++ b/src/main/kotlin/features/world/FirmWaypointManager.kt @@ -0,0 +1,168 @@ +package moe.nea.firmament.features.world + +import com.mojang.brigadier.arguments.StringArgumentType +import kotlinx.serialization.serializer +import net.minecraft.text.Text +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.DefaultSource +import moe.nea.firmament.commands.RestArgumentType +import moe.nea.firmament.commands.get +import moe.nea.firmament.commands.suggestsList +import moe.nea.firmament.commands.thenArgument +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.commands.thenLiteral +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.util.ClipboardUtils +import moe.nea.firmament.util.FirmFormatters +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.TemplateUtil +import moe.nea.firmament.util.data.MultiFileDataHolder +import moe.nea.firmament.util.tr + +object FirmWaypointManager { + object DataHolder : MultiFileDataHolder<FirmWaypoints>(serializer(), "waypoints") + + val SHARE_PREFIX = "FIRM_WAYPOINTS/" + val ENCODED_SHARE_PREFIX = TemplateUtil.getPrefixComparisonSafeBase64Encoding(SHARE_PREFIX) + + fun createExportableCopy( + waypoints: FirmWaypoints, + ): FirmWaypoints { + val copy = waypoints.copy(waypoints = waypoints.waypoints.toMutableList()) + if (waypoints.isRelativeTo != null) { + val origin = waypoints.lastRelativeImport + if (origin != null) { + copy.waypoints.replaceAll { + it.copy( + x = it.x - origin.x, + y = it.y - origin.y, + z = it.z - origin.z, + ) + } + } else { + TODO("Add warning!") + } + } + return copy + } + + fun loadWaypoints(waypoints: FirmWaypoints, sendFeedback: (Text) -> Unit) { + val copy = waypoints.deepCopy() + if (copy.isRelativeTo != null) { + val origin = MC.player!!.blockPos + copy.waypoints.replaceAll { + it.copy( + x = it.x + origin.x, + y = it.y + origin.y, + z = it.z + origin.z, + ) + } + copy.lastRelativeImport = origin.toImmutable() + sendFeedback(tr("firmament.command.waypoint.import.ordered.success", + "Imported ${copy.size} relative waypoints. Make sure you stand in the correct spot while loading the waypoints: ${copy.isRelativeTo}.")) + } else { + sendFeedback(tr("firmament.command.waypoint.import.success", + "Imported ${copy.size} waypoints.")) + } + Waypoints.waypoints = copy + } + + fun setOrigin(source: DefaultSource, text: String?) { + val waypoints = Waypoints.useEditableWaypoints() + waypoints.isRelativeTo = text ?: waypoints.isRelativeTo ?: "" + val pos = MC.player!!.blockPos + waypoints.lastRelativeImport = pos + source.sendFeedback(tr("firmament.command.waypoint.originset", + "Set the origin of waypoints to ${FirmFormatters.formatPosition(pos)}. Run /firm waypoints export to save the waypoints relative to this position.")) + } + + @Subscribe + fun onCommands(event: CommandEvent.SubCommand) { + event.subcommand(Waypoints.WAYPOINTS_SUBCOMMAND) { + thenLiteral("setorigin") { + thenExecute { + setOrigin(source, null) + } + thenArgument("hint", RestArgumentType) { text -> + thenExecute { + setOrigin(source, this[text]) + } + } + } + thenLiteral("clearorigin") { + thenExecute { + val waypoints = Waypoints.useEditableWaypoints() + waypoints.lastRelativeImport = null + waypoints.isRelativeTo = null + source.sendFeedback(tr("firmament.command.waypoint.originunset", + "Unset the origin of the waypoints. Run /firm waypoints export to save the waypoints with absolute coordinates.")) + } + } + thenLiteral("save") { + thenArgument("name", StringArgumentType.string()) { name -> + suggestsList { DataHolder.list().keys } + thenExecute { + val waypoints = Waypoints.useNonEmptyWaypoints() + if (waypoints == null) { + source.sendError(Waypoints.textNothingToExport()) + return@thenExecute + } + waypoints.id = get(name) + val exportableWaypoints = createExportableCopy(waypoints) + DataHolder.insert(get(name), exportableWaypoints) + DataHolder.save() + source.sendFeedback(tr("firmament.command.waypoint.saved", + "Saved waypoints locally as ${get(name)}. Use /firm waypoints load to load them again.")) + } + } + } + thenLiteral("load") { + thenArgument("name", StringArgumentType.string()) { name -> + suggestsList { DataHolder.list().keys } + thenExecute { + val name = get(name) + val waypoints = DataHolder.list()[name] + if (waypoints == null) { + source.sendError( + tr("firmament.command.waypoint.nosaved", + "No saved waypoint for ${name}. Use tab completion to see available names.")) + return@thenExecute + } + loadWaypoints(waypoints, source::sendFeedback) + } + } + } + thenLiteral("export") { + thenExecute { + val waypoints = Waypoints.useNonEmptyWaypoints() + if (waypoints == null) { + source.sendError(Waypoints.textNothingToExport()) + return@thenExecute + } + val exportableWaypoints = createExportableCopy(waypoints) + val data = TemplateUtil.encodeTemplate(SHARE_PREFIX, exportableWaypoints) + ClipboardUtils.setTextContent(data) + source.sendFeedback(tr("firmament.command.waypoint.export", + "Copied ${exportableWaypoints.size} waypoints to clipboard in Firmament format.")) + } + } + thenLiteral("import") { + thenExecute { + val text = ClipboardUtils.getTextContents() + if (text.startsWith("[")) { + source.sendError(tr("firmament.command.waypoint.import.lookslikecw", + "The waypoints in your clipboard look like they might be ColeWeight waypoints. If so, use /firm waypoints importcw or /firm waypoints importrelativecw.")) + return@thenExecute + } + val waypoints = TemplateUtil.maybeDecodeTemplate<FirmWaypoints>(SHARE_PREFIX, text) + if (waypoints == null) { + source.sendError(tr("firmament.command.waypoint.import.error", + "Could not import Firmament waypoints from your clipboard. Make sure they are Firmament compatible waypoints.")) + return@thenExecute + } + loadWaypoints(waypoints, source::sendFeedback) + } + } + } + } +} diff --git a/src/main/kotlin/features/world/FirmWaypoints.kt b/src/main/kotlin/features/world/FirmWaypoints.kt new file mode 100644 index 0000000..d0cd55a --- /dev/null +++ b/src/main/kotlin/features/world/FirmWaypoints.kt @@ -0,0 +1,37 @@ +package moe.nea.firmament.features.world + +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import net.minecraft.util.math.BlockPos + +@Serializable +data class FirmWaypoints( + var label: String, + var id: String, + /** + * A hint to indicate where to stand while loading the waypoints. + */ + var isRelativeTo: String?, + var waypoints: MutableList<Waypoint>, + var isOrdered: Boolean, + // TODO: val resetOnSwap: Boolean, +) { + + fun deepCopy() = copy(waypoints = waypoints.toMutableList()) + @Transient + var lastRelativeImport: BlockPos? = null + + val size get() = waypoints.size + @Serializable + data class Waypoint( + val x: Int, + val y: Int, + val z: Int, + ) { + val blockPos get() = BlockPos(x, y, z) + + companion object { + fun from(blockPos: BlockPos) = Waypoint(blockPos.x, blockPos.y, blockPos.z) + } + } +} diff --git a/src/main/kotlin/features/world/TemporaryWaypoints.kt b/src/main/kotlin/features/world/TemporaryWaypoints.kt new file mode 100644 index 0000000..b36c49d --- /dev/null +++ b/src/main/kotlin/features/world/TemporaryWaypoints.kt @@ -0,0 +1,73 @@ +package moe.nea.firmament.features.world + +import kotlin.compareTo +import kotlin.text.clear +import kotlin.time.Duration.Companion.seconds +import net.minecraft.text.Text +import net.minecraft.util.math.BlockPos +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ProcessChatEvent +import moe.nea.firmament.events.WorldReadyEvent +import moe.nea.firmament.events.WorldRenderLastEvent +import moe.nea.firmament.features.world.Waypoints.TConfig +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.render.RenderInWorldContext + +object TemporaryWaypoints { + data class TemporaryWaypoint( + val pos: BlockPos, + val postedAt: TimeMark, + ) + val temporaryPlayerWaypointList = mutableMapOf<String, TemporaryWaypoint>() + val temporaryPlayerWaypointMatcher = "(?i)x: (-?[0-9]+),? y: (-?[0-9]+),? z: (-?[0-9]+)".toPattern() + @Subscribe + fun onProcessChat(it: ProcessChatEvent) { + val matcher = temporaryPlayerWaypointMatcher.matcher(it.unformattedString) + if (it.nameHeuristic != null && TConfig.tempWaypointDuration > 0.seconds && matcher.find()) { + temporaryPlayerWaypointList[it.nameHeuristic] = TemporaryWaypoint(BlockPos( + matcher.group(1).toInt(), + matcher.group(2).toInt(), + matcher.group(3).toInt(), + ), TimeMark.now()) + } + } + @Subscribe + fun onRenderTemporaryWaypoints(event: WorldRenderLastEvent) { + temporaryPlayerWaypointList.entries.removeIf { it.value.postedAt.passedTime() > TConfig.tempWaypointDuration } + if (temporaryPlayerWaypointList.isEmpty()) return + RenderInWorldContext.renderInWorld(event) { + temporaryPlayerWaypointList.forEach { (_, waypoint) -> + block(waypoint.pos, 0xFFFFFF00.toInt()) + } + temporaryPlayerWaypointList.forEach { (player, waypoint) -> + val skin = + MC.networkHandler?.listedPlayerListEntries?.find { it.profile.name == player }?.skinTextures?.texture + withFacingThePlayer(waypoint.pos.toCenterPos()) { + waypoint(waypoint.pos, Text.stringifiedTranslatable("firmament.waypoint.temporary", player)) + if (skin != null) { + matrixStack.translate(0F, -20F, 0F) + // Head front + texture( + skin, 16, 16, + 1 / 8f, 1 / 8f, + 2 / 8f, 2 / 8f, + ) + // Head overlay + texture( + skin, 16, 16, + 5 / 8f, 1 / 8f, + 6 / 8f, 2 / 8f, + ) + } + } + } + } + } + + @Subscribe + fun onWorldReady(event: WorldReadyEvent) { + temporaryPlayerWaypointList.clear() + } + +} diff --git a/src/main/kotlin/features/world/Waypoints.kt b/src/main/kotlin/features/world/Waypoints.kt index 3ebfe70..b5c2b66 100644 --- a/src/main/kotlin/features/world/Waypoints.kt +++ b/src/main/kotlin/features/world/Waypoints.kt @@ -2,36 +2,24 @@ package moe.nea.firmament.features.world import com.mojang.brigadier.arguments.IntegerArgumentType import me.shedaniel.math.Color -import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource -import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString -import kotlin.collections.component1 -import kotlin.collections.component2 -import kotlin.collections.set import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.seconds import net.minecraft.command.argument.BlockPosArgumentType -import net.minecraft.server.command.CommandOutput -import net.minecraft.server.command.ServerCommandSource import net.minecraft.text.Text -import net.minecraft.util.math.BlockPos import net.minecraft.util.math.Vec3d -import moe.nea.firmament.Firmament import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.commands.get import moe.nea.firmament.commands.thenArgument import moe.nea.firmament.commands.thenExecute import moe.nea.firmament.commands.thenLiteral import moe.nea.firmament.events.CommandEvent -import moe.nea.firmament.events.ProcessChatEvent import moe.nea.firmament.events.TickEvent import moe.nea.firmament.events.WorldReadyEvent import moe.nea.firmament.events.WorldRenderLastEvent import moe.nea.firmament.features.FirmamentFeature import moe.nea.firmament.gui.config.ManagedConfig -import moe.nea.firmament.util.ClipboardUtils import moe.nea.firmament.util.MC -import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.mc.asFakeServer import moe.nea.firmament.util.render.RenderInWorldContext import moe.nea.firmament.util.tr @@ -43,99 +31,85 @@ object Waypoints : FirmamentFeature { val tempWaypointDuration by duration("temp-waypoint-duration", 0.seconds, 1.hours) { 30.seconds } val showIndex by toggle("show-index") { true } val skipToNearest by toggle("skip-to-nearest") { false } + val resetWaypointOrderOnWorldSwap by toggle("reset-order-on-swap") { true } // TODO: look ahead size } - data class TemporaryWaypoint( - val pos: BlockPos, - val postedAt: TimeMark, - ) - override val config get() = TConfig - - val temporaryPlayerWaypointList = mutableMapOf<String, TemporaryWaypoint>() - val temporaryPlayerWaypointMatcher = "(?i)x: (-?[0-9]+),? y: (-?[0-9]+),? z: (-?[0-9]+)".toPattern() - - val waypoints = mutableListOf<BlockPos>() - var ordered = false + var waypoints: FirmWaypoints? = null var orderedIndex = 0 - @Serializable - data class ColeWeightWaypoint( - val x: Int, - val y: Int, - val z: Int, - val r: Int = 0, - val g: Int = 0, - val b: Int = 0, - ) - @Subscribe fun onRenderOrderedWaypoints(event: WorldRenderLastEvent) { - if (waypoints.isEmpty()) return + val w = useNonEmptyWaypoints() ?: return RenderInWorldContext.renderInWorld(event) { - if (!ordered) { - waypoints.withIndex().forEach { - block(it.value, 0x800050A0.toInt()) - if (TConfig.showIndex) - withFacingThePlayer(it.value.toCenterPos()) { - text(Text.literal(it.index.toString())) - } + if (!w.isOrdered) { + w.waypoints.withIndex().forEach { + block(it.value.blockPos, 0x800050A0.toInt()) + if (TConfig.showIndex) withFacingThePlayer(it.value.blockPos.toCenterPos()) { + text(Text.literal(it.index.toString())) + } } } else { - orderedIndex %= waypoints.size + orderedIndex %= w.waypoints.size val firstColor = Color.ofRGBA(0, 200, 40, 180) color(firstColor) - tracer(waypoints[orderedIndex].toCenterPos(), lineWidth = 3f) - waypoints.withIndex().toList() - .wrappingWindow(orderedIndex, 3) - .zip( - listOf( - firstColor, - Color.ofRGBA(180, 200, 40, 150), - Color.ofRGBA(180, 80, 20, 140), - ) - ) - .reversed() - .forEach { (waypoint, col) -> - val (index, pos) = waypoint - block(pos, col.color) - if (TConfig.showIndex) - withFacingThePlayer(pos.toCenterPos()) { - text(Text.literal(index.toString())) - } + tracer(w.waypoints[orderedIndex].blockPos.toCenterPos(), lineWidth = 3f) + w.waypoints.withIndex().toList().wrappingWindow(orderedIndex, 3).zip(listOf( + firstColor, + Color.ofRGBA(180, 200, 40, 150), + Color.ofRGBA(180, 80, 20, 140), + )).reversed().forEach { (waypoint, col) -> + val (index, pos) = waypoint + block(pos.blockPos, col.color) + if (TConfig.showIndex) withFacingThePlayer(pos.blockPos.toCenterPos()) { + text(Text.literal(index.toString())) } + } } } } @Subscribe fun onTick(event: TickEvent) { - if (waypoints.isEmpty() || !ordered) return - orderedIndex %= waypoints.size + val w = useNonEmptyWaypoints() ?: return + if (!w.isOrdered) return + orderedIndex %= w.waypoints.size val p = MC.player?.pos ?: return if (TConfig.skipToNearest) { orderedIndex = - (waypoints.withIndex().minBy { it.value.getSquaredDistance(p) }.index + 1) % waypoints.size + (w.waypoints.withIndex().minBy { it.value.blockPos.getSquaredDistance(p) }.index + 1) % w.waypoints.size + } else { - if (waypoints[orderedIndex].isWithinDistance(p, 3.0)) { - orderedIndex = (orderedIndex + 1) % waypoints.size + if (w.waypoints[orderedIndex].blockPos.isWithinDistance(p, 3.0)) { + orderedIndex = (orderedIndex + 1) % w.waypoints.size } } } + + fun useEditableWaypoints(): FirmWaypoints { + var w = waypoints + if (w == null) { + w = FirmWaypoints("Unlabeled", "unknown", null, mutableListOf(), false) + waypoints = w + } + return w + } + + fun useNonEmptyWaypoints(): FirmWaypoints? { + val w = waypoints + if (w == null) return null + if (w.waypoints.isEmpty()) return null + return w + } + + val WAYPOINTS_SUBCOMMAND = "waypoints" + @Subscribe - fun onProcessChat(it: ProcessChatEvent) { - val matcher = temporaryPlayerWaypointMatcher.matcher(it.unformattedString) - if (it.nameHeuristic != null && TConfig.tempWaypointDuration > 0.seconds && matcher.find()) { - temporaryPlayerWaypointList[it.nameHeuristic] = TemporaryWaypoint( - BlockPos( - matcher.group(1).toInt(), - matcher.group(2).toInt(), - matcher.group(3).toInt(), - ), - TimeMark.now() - ) + fun onWorldSwap(event: WorldReadyEvent) { + if (TConfig.resetWaypointOrderOnWorldSwap) { + orderedIndex = 0 } } @@ -144,41 +118,77 @@ object Waypoints : FirmamentFeature { event.subcommand("waypoint") { thenArgument("pos", BlockPosArgumentType.blockPos()) { pos -> thenExecute { + source val position = pos.get(this).toAbsoluteBlockPos(source.asFakeServer()) - waypoints.add(position) - source.sendFeedback( - Text.stringifiedTranslatable( - "firmament.command.waypoint.added", - position.x, - position.y, - position.z - ) - ) + val w = useEditableWaypoints() + w.waypoints.add(FirmWaypoints.Waypoint.from(position)) + source.sendFeedback(Text.stringifiedTranslatable("firmament.command.waypoint.added", + position.x, + position.y, + position.z)) } } } - event.subcommand("waypoints") { + event.subcommand(WAYPOINTS_SUBCOMMAND) { + thenLiteral("reset") { + thenExecute { + orderedIndex = 0 + source.sendFeedback(tr( + "firmament.command.waypoint.reset", + "Reset your ordered waypoint index back to 0. If you want to delete all waypoints use /firm waypoints clear instead.")) + } + } + thenLiteral("changeindex") { + thenArgument("from", IntegerArgumentType.integer(0)) { fromIndex -> + thenArgument("to", IntegerArgumentType.integer(0)) { toIndex -> + thenExecute { + val w = useEditableWaypoints() + val toIndex = toIndex.get(this) + val fromIndex = fromIndex.get(this) + if (fromIndex !in w.waypoints.indices) { + source.sendError(textInvalidIndex(fromIndex)) + return@thenExecute + } + if (toIndex !in w.waypoints.indices) { + source.sendError(textInvalidIndex(toIndex)) + return@thenExecute + } + val waypoint = w.waypoints.removeAt(fromIndex) + w.waypoints.add( + if (toIndex > fromIndex) toIndex - 1 + else toIndex, + waypoint) + source.sendFeedback( + tr("firmament.command.waypoint.indexchange", + "Moved waypoint from index $fromIndex to $toIndex. Note that this only matters for ordered waypoints.") + ) + } + } + } + } thenLiteral("clear") { thenExecute { - waypoints.clear() + waypoints = null source.sendFeedback(Text.translatable("firmament.command.waypoint.clear")) } } thenLiteral("toggleordered") { thenExecute { - ordered = !ordered - if (ordered) { + val w = useEditableWaypoints() + w.isOrdered = !w.isOrdered + if (w.isOrdered) { val p = MC.player?.pos ?: Vec3d.ZERO - orderedIndex = - waypoints.withIndex().minByOrNull { it.value.getSquaredDistance(p) }?.index ?: 0 + orderedIndex = // TODO: this should be extracted to a utility method + w.waypoints.withIndex().minByOrNull { it.value.blockPos.getSquaredDistance(p) }?.index ?: 0 } - source.sendFeedback(Text.translatable("firmament.command.waypoint.ordered.toggle.$ordered")) + source.sendFeedback(Text.translatable("firmament.command.waypoint.ordered.toggle.${w.isOrdered}")) } } thenLiteral("skip") { thenExecute { - if (ordered && waypoints.isNotEmpty()) { - orderedIndex = (orderedIndex + 1) % waypoints.size + val w = useNonEmptyWaypoints() + if (w != null && w.isOrdered) { + orderedIndex = (orderedIndex + 1) % w.size source.sendFeedback(Text.translatable("firmament.command.waypoint.skip")) } else { source.sendError(Text.translatable("firmament.command.waypoint.skip.error")) @@ -189,90 +199,27 @@ object Waypoints : FirmamentFeature { thenArgument("index", IntegerArgumentType.integer(0)) { indexArg -> thenExecute { val index = get(indexArg) - if (index in waypoints.indices) { - waypoints.removeAt(index) - source.sendFeedback(Text.stringifiedTranslatable( - "firmament.command.waypoint.remove", - index)) + val w = useNonEmptyWaypoints() + if (w != null && index in w.waypoints.indices) { + w.waypoints.removeAt(index) + source.sendFeedback(Text.stringifiedTranslatable("firmament.command.waypoint.remove", + index)) } else { source.sendError(Text.stringifiedTranslatable("firmament.command.waypoint.remove.error")) } } } } - thenLiteral("export") { - thenExecute { - val data = Firmament.tightJson.encodeToString<List<ColeWeightWaypoint>>(waypoints.map { - ColeWeightWaypoint(it.x, - it.y, - it.z) - }) - ClipboardUtils.setTextContent(data) - source.sendFeedback(tr("firmament.command.waypoint.export", "Copied ${waypoints.size} waypoints to clipboard")) - } - } - thenLiteral("import") { - thenExecute { - val contents = ClipboardUtils.getTextContents() - val data = try { - Firmament.tightJson.decodeFromString<List<ColeWeightWaypoint>>(contents) - } catch (ex: Exception) { - Firmament.logger.error("Could not load waypoints from clipboard", ex) - source.sendError(Text.translatable("firmament.command.waypoint.import.error")) - return@thenExecute - } - waypoints.clear() - data.mapTo(waypoints) { BlockPos(it.x, it.y, it.z) } - source.sendFeedback( - Text.stringifiedTranslatable( - "firmament.command.waypoint.import", - data.size - ) - ) - } - } } } - @Subscribe - fun onRenderTemporaryWaypoints(event: WorldRenderLastEvent) { - temporaryPlayerWaypointList.entries.removeIf { it.value.postedAt.passedTime() > TConfig.tempWaypointDuration } - if (temporaryPlayerWaypointList.isEmpty()) return - RenderInWorldContext.renderInWorld(event) { - temporaryPlayerWaypointList.forEach { (player, waypoint) -> - block(waypoint.pos, 0xFFFFFF00.toInt()) - } - temporaryPlayerWaypointList.forEach { (player, waypoint) -> - val skin = - MC.networkHandler?.listedPlayerListEntries?.find { it.profile.name == player } - ?.skinTextures - ?.texture - withFacingThePlayer(waypoint.pos.toCenterPos()) { - waypoint(waypoint.pos, Text.stringifiedTranslatable("firmament.waypoint.temporary", player)) - if (skin != null) { - matrixStack.translate(0F, -20F, 0F) - // Head front - texture( - skin, 16, 16, - 1 / 8f, 1 / 8f, - 2 / 8f, 2 / 8f, - ) - // Head overlay - texture( - skin, 16, 16, - 5 / 8f, 1 / 8f, - 6 / 8f, 2 / 8f, - ) - } - } - } - } - } + fun textInvalidIndex(index: Int) = + tr("firmament.command.waypoint.invalid-index", + "Invalid index $index provided.") - @Subscribe - fun onWorldReady(event: WorldReadyEvent) { - temporaryPlayerWaypointList.clear() - } + fun textNothingToExport(): Text = + tr("firmament.command.waypoint.export.nowaypoints", + "No waypoints to export found. Add some with /firm waypoint ~ ~ ~.") } fun <E> List<E>.wrappingWindow(startIndex: Int, windowSize: Int): List<E> { @@ -285,35 +232,3 @@ fun <E> List<E>.wrappingWindow(startIndex: Int, windowSize: Int): List<E> { } return result } - - -fun FabricClientCommandSource.asFakeServer(): ServerCommandSource { - val source = this - return ServerCommandSource( - object : CommandOutput { - override fun sendMessage(message: Text?) { - source.player.sendMessage(message, false) - } - - override fun shouldReceiveFeedback(): Boolean { - return true - } - - override fun shouldTrackOutput(): Boolean { - return true - } - - override fun shouldBroadcastConsoleToOps(): Boolean { - return true - } - }, - source.position, - source.rotation, - null, - 0, - "FakeServerCommandSource", - Text.literal("FakeServerCommandSource"), - null, - source.player - ) -} diff --git a/src/main/kotlin/gui/CheckboxComponent.kt b/src/main/kotlin/gui/CheckboxComponent.kt index 761c086..fc48661 100644 --- a/src/main/kotlin/gui/CheckboxComponent.kt +++ b/src/main/kotlin/gui/CheckboxComponent.kt @@ -28,8 +28,8 @@ class CheckboxComponent<T>( val ctx = (context.renderContext as ModernRenderContext).drawContext ctx.drawGuiTexture( RenderLayer::getGuiTextured, - if (isEnabled()) Firmament.identifier("firmament:widget/checkbox_checked") - else Firmament.identifier("firmament:widget/checkbox_unchecked"), + if (isEnabled()) Firmament.identifier("widget/checkbox_checked") + else Firmament.identifier("widget/checkbox_unchecked"), 0, 0, 16, 16 ) @@ -43,6 +43,7 @@ class CheckboxComponent<T>( isClicking = false if (context.isHovered) state.set(value) + blur() return true } if (mouseEvent.mouseState && mouseEvent.mouseButton == 0 && context.isHovered) { diff --git a/src/main/kotlin/repo/BetterRepoRecipeCache.kt b/src/main/kotlin/repo/BetterRepoRecipeCache.kt index 4b32e57..6d18223 100644 --- a/src/main/kotlin/repo/BetterRepoRecipeCache.kt +++ b/src/main/kotlin/repo/BetterRepoRecipeCache.kt @@ -2,8 +2,10 @@ package moe.nea.firmament.repo import io.github.moulberry.repo.IReloadable import io.github.moulberry.repo.NEURepository +import io.github.moulberry.repo.data.NEUNpcShopRecipe import io.github.moulberry.repo.data.NEURecipe import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.skyblockId class BetterRepoRecipeCache(vararg val extraProviders: ExtraRecipeProvider) : IReloadable { var usages: Map<SkyblockId, Set<NEURecipe>> = mapOf() @@ -17,6 +19,9 @@ class BetterRepoRecipeCache(vararg val extraProviders: ExtraRecipeProvider) : IR .flatMap { it.recipes } (baseRecipes + extraProviders.flatMap { it.provideExtraRecipes() }) .forEach { recipe -> + if (recipe is NEUNpcShopRecipe) { + usages.getOrPut(recipe.isSoldBy.skyblockId, ::mutableSetOf).add(recipe) + } recipe.allInputs.forEach { usages.getOrPut(SkyblockId(it.itemId), ::mutableSetOf).add(recipe) } recipe.allOutputs.forEach { recipes.getOrPut(SkyblockId(it.itemId), ::mutableSetOf).add(recipe) } } diff --git a/src/main/kotlin/repo/ItemCache.kt b/src/main/kotlin/repo/ItemCache.kt index 9fa0083..0967ad1 100644 --- a/src/main/kotlin/repo/ItemCache.kt +++ b/src/main/kotlin/repo/ItemCache.kt @@ -61,11 +61,12 @@ object ItemCache : IReloadable { putShort("Damage", damage.toShort()) } - private fun NbtCompound.transformFrom10809ToModern(): NbtCompound? = + private fun NbtCompound.transformFrom10809ToModern() = convert189ToModern(this@transformFrom10809ToModern) + fun convert189ToModern(nbtComponent: NbtCompound): NbtCompound? = try { df.update( TypeReferences.ITEM_STACK, - Dynamic(NbtOps.INSTANCE, this), + Dynamic(NbtOps.INSTANCE, nbtComponent), -1, SharedConstants.getGameVersion().saveVersion.id ).value as NbtCompound @@ -184,31 +185,6 @@ object ItemCache : IReloadable { var job: Job? = null - object ReloadProgressHud : MoulConfigHud( - "repo_reload", HudMeta(HudPosition(0.0, 0.0, 1F), Text.literal("Repo Reload"), 180, 18)) { - - - var isEnabled = false - override fun shouldRender(): Boolean { - return isEnabled - } - - @get:Bind("current") - var current: Double = 0.0 - - @get:Bind("label") - var label: String = "" - - @get:Bind("max") - var max: Double = 0.0 - - fun reportProgress(label: String, current: Int, max: Int) { - this.label = label - this.current = current.toDouble() - this.max = max.toDouble() - } - } - override fun reload(repository: NEURepository) { val j = job if (j != null && j.isActive) { @@ -218,20 +194,10 @@ object ItemCache : IReloadable { isFlawless = true if (TestUtil.isInTest) return job = Firmament.coroutineScope.launch { - val items = repository.items?.items - if (items == null) { - ReloadProgressHud.isEnabled = false - return@launch - } - val recacheItems = I18n.translate("firmament.repo.cache") - ReloadProgressHud.reportProgress(recacheItems, 0, items.size) - ReloadProgressHud.isEnabled = true - var i = 0 + val items = repository.items?.items ?: return@launch items.values.forEach { it.asItemStack() // Rebuild cache - ReloadProgressHud.reportProgress(recacheItems, i++, items.size) } - ReloadProgressHud.isEnabled = false } } diff --git a/src/main/kotlin/repo/MiningRepoData.kt b/src/main/kotlin/repo/MiningRepoData.kt new file mode 100644 index 0000000..e40292d --- /dev/null +++ b/src/main/kotlin/repo/MiningRepoData.kt @@ -0,0 +1,131 @@ +package moe.nea.firmament.repo + +import io.github.moulberry.repo.IReloadable +import io.github.moulberry.repo.NEURepository +import io.github.moulberry.repo.data.NEUItem +import java.util.Collections +import java.util.NavigableMap +import java.util.TreeMap +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import kotlinx.serialization.serializer +import kotlin.jvm.optionals.getOrNull +import kotlin.streams.asSequence +import net.minecraft.block.Block +import net.minecraft.item.BlockItem +import net.minecraft.item.ItemStack +import net.minecraft.nbt.NbtCompound +import net.minecraft.text.Text +import moe.nea.firmament.repo.ReforgeStore.kJson +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.SkyBlockIsland +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.mc.FirmamentDataComponentTypes +import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.skyblockId + +class MiningRepoData : IReloadable { + var customMiningAreas: Map<SkyBlockIsland, CustomMiningArea> = mapOf() + private set + var customMiningBlocks: List<CustomMiningBlock> = listOf() + private set + var toolsByBreakingPower: NavigableMap<BreakingPowerKey, SBItemStack> = Collections.emptyNavigableMap() + private set + + + data class BreakingPowerKey( + val breakingPower: Int, + val itemId: SkyblockId? = null + ) { + companion object { + val COMPARATOR: Comparator<BreakingPowerKey> = + Comparator + .comparingInt<BreakingPowerKey> { it.breakingPower } + .thenComparing(Comparator.comparing( + { it.itemId }, + nullsFirst(Comparator.comparing<SkyblockId, Boolean> { "PICK" in it.neuItem || "BING" in it.neuItem }.thenComparing(Comparator.naturalOrder<SkyblockId>())))) + } + } + + override fun reload(repo: NEURepository) { + customMiningAreas = repo.file("mining/custom_mining_areas.json") + ?.kJson(serializer()) ?: mapOf() + customMiningBlocks = repo.tree("mining/blocks") + .asSequence() + .filter { it.path.endsWith(".json") } + .map { it.kJson(serializer<CustomMiningBlock>()) } + .toList() + toolsByBreakingPower = Collections.unmodifiableNavigableMap( + repo.items.items + .values + .asSequence() + .map { SBItemStack(it.skyblockId) } + .filter { it.breakingPower > 0 } + .associateTo(TreeMap<BreakingPowerKey, SBItemStack>(BreakingPowerKey.COMPARATOR)) { + BreakingPowerKey(it.breakingPower, it.skyblockId) to it + }) + } + + fun getToolsThatCanBreak(breakingPower: Int): Collection<SBItemStack> { + return toolsByBreakingPower.tailMap(BreakingPowerKey(breakingPower, null), true).values + } + + @Serializable + data class CustomMiningBlock( + val breakingPower: Int = 0, + val blockStrength: Int = 0, + val name: String? = null, + val baseDrop: SkyblockId? = null, + val blocks189: List<Block189> = emptyList() + ) { + @Transient + val dropItem = baseDrop?.let(::SBItemStack) + private val labeledStack by lazy { + dropItem?.asCopiedItemStack()?.also(::markItemStack) + } + + private fun markItemStack(itemStack: ItemStack) { + itemStack.set(FirmamentDataComponentTypes.CUSTOM_MINING_BLOCK_DATA, this) + if (name != null) + itemStack.displayNameAccordingToNbt = Text.literal(name) + } + + fun getDisplayItem(block: Block): ItemStack { + return labeledStack ?: ItemStack(block).also(::markItemStack) + } + } + + @Serializable + data class Block189( + val itemId: String, + val damage: Short = 0, + val onlyIn: List<SkyBlockIsland>? = null, + ) { + @Transient + val block = convertToModernBlock() + + val isCurrentlyActive: Boolean + get() = isActiveIn(SBData.skyblockLocation ?: SkyBlockIsland.NIL) + + fun isActiveIn(location: SkyBlockIsland) = onlyIn == null || location in onlyIn + + private fun convertToModernBlock(): Block? { + // TODO: this should be in a shared util, really + val newCompound = ItemCache.convert189ToModern(NbtCompound().apply { + putString("id", itemId) + putShort("Damage", damage) + }) ?: return null + val itemStack = ItemStack.fromNbt(MC.defaultRegistries, newCompound).getOrNull() ?: return null + val blockItem = itemStack.item as? BlockItem ?: return null + return blockItem.block + } + } + + @Serializable + data class CustomMiningArea( + val isSpecialMining: Boolean = true + ) + + +} diff --git a/src/main/kotlin/repo/RepoDownloadManager.kt b/src/main/kotlin/repo/RepoDownloadManager.kt index 3efd83b..888248d 100644 --- a/src/main/kotlin/repo/RepoDownloadManager.kt +++ b/src/main/kotlin/repo/RepoDownloadManager.kt @@ -1,5 +1,3 @@ - - package moe.nea.firmament.repo import io.ktor.client.call.body @@ -28,101 +26,102 @@ import moe.nea.firmament.util.iterate object RepoDownloadManager { - val repoSavedLocation = Firmament.DATA_DIR.resolve("repo-extracted") - val repoMetadataLocation = Firmament.DATA_DIR.resolve("loaded-repo-sha.txt") - - private fun loadSavedVersionHash(): String? = - if (repoSavedLocation.exists()) { - if (repoMetadataLocation.exists()) { - try { - repoMetadataLocation.readText().trim() - } catch (e: IOException) { - null - } - } else { - null - } - } else null - - private fun saveVersionHash(versionHash: String) { - latestSavedVersionHash = versionHash - repoMetadataLocation.writeText(versionHash) - } - - var latestSavedVersionHash: String? = loadSavedVersionHash() - private set - - @Serializable - private class GithubCommitsResponse(val sha: String) - - private suspend fun requestLatestGithubSha(): String? { - if (RepoManager.Config.branch == "prerelease") { - RepoManager.Config.branch = "master" - } - val response = - Firmament.httpClient.get("https://api.github.com/repos/${RepoManager.Config.username}/${RepoManager.Config.reponame}/commits/${RepoManager.Config.branch}") - if (response.status.value != 200) { - return null - } - return response.body<GithubCommitsResponse>().sha - } - - private suspend fun downloadGithubArchive(url: String): Path = withContext(IO) { - val response = Firmament.httpClient.get(url) - val targetFile = Files.createTempFile("firmament-repo", ".zip") - val outputChannel = Files.newByteChannel(targetFile, StandardOpenOption.CREATE, StandardOpenOption.WRITE) - response.bodyAsChannel().copyTo(outputChannel) - targetFile - } - - /** - * Downloads the latest repository from github, setting [latestSavedVersionHash]. - * @return true, if an update was performed, false, otherwise (no update needed, or wasn't able to complete update) - */ - suspend fun downloadUpdate(force: Boolean): Boolean = withContext(CoroutineName("Repo Update Check")) { - val latestSha = requestLatestGithubSha() - if (latestSha == null) { - logger.warn("Could not request github API to retrieve latest REPO sha.") - return@withContext false - } - val currentSha = loadSavedVersionHash() - if (latestSha != currentSha || force) { - val requestUrl = - "https://github.com/${RepoManager.Config.username}/${RepoManager.Config.reponame}/archive/$latestSha.zip" - logger.info("Planning to upgrade repository from $currentSha to $latestSha from $requestUrl") - val zipFile = downloadGithubArchive(requestUrl) - logger.info("Download repository zip file to $zipFile. Deleting old repository") - withContext(IO) { repoSavedLocation.toFile().deleteRecursively() } - logger.info("Extracting new repository") - withContext(IO) { extractNewRepository(zipFile) } - logger.info("Repository loaded on disk.") - saveVersionHash(latestSha) - return@withContext true - } else { - logger.debug("Repository on latest sha $currentSha. Not performing update") - return@withContext false - } - } - - private fun extractNewRepository(zipFile: Path) { - repoSavedLocation.createDirectories() - ZipInputStream(zipFile.inputStream()).use { cis -> - while (true) { - val entry = cis.nextEntry ?: break - if (entry.isDirectory) continue - val extractedLocation = - repoSavedLocation.resolve( - entry.name.substringAfter('/', missingDelimiterValue = "") - ) - if (repoSavedLocation !in extractedLocation.iterate { it.parent }) { - logger.error("Firmament detected an invalid zip file. This is a potential security risk, please report this in the Firmament discord.") - throw RuntimeException("Firmament detected an invalid zip file. This is a potential security risk, please report this in the Firmament discord.") - } - extractedLocation.parent.createDirectories() - extractedLocation.outputStream().use { cis.copyTo(it) } - } - } - } + val repoSavedLocation = Firmament.DATA_DIR.resolve("repo-extracted") + val repoMetadataLocation = Firmament.DATA_DIR.resolve("loaded-repo-sha.txt") + + private fun loadSavedVersionHash(): String? = + if (repoSavedLocation.exists()) { + if (repoMetadataLocation.exists()) { + try { + repoMetadataLocation.readText().trim() + } catch (e: IOException) { + null + } + } else { + null + } + } else null + + private fun saveVersionHash(versionHash: String) { + latestSavedVersionHash = versionHash + repoMetadataLocation.writeText(versionHash) + } + + var latestSavedVersionHash: String? = loadSavedVersionHash() + private set + + @Serializable + private class GithubCommitsResponse(val sha: String) + + private suspend fun requestLatestGithubSha(branchOverride: String?): String? { + if (RepoManager.Config.branch == "prerelease") { + RepoManager.Config.branch = "master" + } + val response = + Firmament.httpClient.get("https://api.github.com/repos/${RepoManager.Config.username}/${RepoManager.Config.reponame}/commits/${branchOverride ?: RepoManager.Config.branch}") + if (response.status.value != 200) { + return null + } + return response.body<GithubCommitsResponse>().sha + } + + private suspend fun downloadGithubArchive(url: String): Path = withContext(IO) { + val response = Firmament.httpClient.get(url) + val targetFile = Files.createTempFile("firmament-repo", ".zip") + val outputChannel = Files.newByteChannel(targetFile, StandardOpenOption.CREATE, StandardOpenOption.WRITE) + response.bodyAsChannel().copyTo(outputChannel) + targetFile + } + + /** + * Downloads the latest repository from github, setting [latestSavedVersionHash]. + * @return true, if an update was performed, false, otherwise (no update needed, or wasn't able to complete update) + */ + suspend fun downloadUpdate(force: Boolean, branch: String? = null): Boolean = + withContext(CoroutineName("Repo Update Check")) { + val latestSha = requestLatestGithubSha(branch) + if (latestSha == null) { + logger.warn("Could not request github API to retrieve latest REPO sha.") + return@withContext false + } + val currentSha = loadSavedVersionHash() + if (latestSha != currentSha || force) { + val requestUrl = + "https://github.com/${RepoManager.Config.username}/${RepoManager.Config.reponame}/archive/$latestSha.zip" + logger.info("Planning to upgrade repository from $currentSha to $latestSha from $requestUrl") + val zipFile = downloadGithubArchive(requestUrl) + logger.info("Download repository zip file to $zipFile. Deleting old repository") + withContext(IO) { repoSavedLocation.toFile().deleteRecursively() } + logger.info("Extracting new repository") + withContext(IO) { extractNewRepository(zipFile) } + logger.info("Repository loaded on disk.") + saveVersionHash(latestSha) + return@withContext true + } else { + logger.debug("Repository on latest sha $currentSha. Not performing update") + return@withContext false + } + } + + private fun extractNewRepository(zipFile: Path) { + repoSavedLocation.createDirectories() + ZipInputStream(zipFile.inputStream()).use { cis -> + while (true) { + val entry = cis.nextEntry ?: break + if (entry.isDirectory) continue + val extractedLocation = + repoSavedLocation.resolve( + entry.name.substringAfter('/', missingDelimiterValue = "") + ) + if (repoSavedLocation !in extractedLocation.iterate { it.parent }) { + logger.error("Firmament detected an invalid zip file. This is a potential security risk, please report this in the Firmament discord.") + throw RuntimeException("Firmament detected an invalid zip file. This is a potential security risk, please report this in the Firmament discord.") + } + extractedLocation.parent.createDirectories() + extractedLocation.outputStream().use { cis.copyTo(it) } + } + } + } } diff --git a/src/main/kotlin/repo/RepoManager.kt b/src/main/kotlin/repo/RepoManager.kt index 6d9ba14..cc36fba 100644 --- a/src/main/kotlin/repo/RepoManager.kt +++ b/src/main/kotlin/repo/RepoManager.kt @@ -54,6 +54,7 @@ object RepoManager { val essenceRecipeProvider = EssenceRecipeProvider() val recipeCache = BetterRepoRecipeCache(essenceRecipeProvider, ReforgeStore) + val miningData = MiningRepoData() fun makeNEURepository(path: Path): NEURepository { return NEURepository.of(path).apply { @@ -63,6 +64,8 @@ object RepoManager { registerReloadListener(ItemNameLookup) registerReloadListener(ReforgeStore) registerReloadListener(essenceRecipeProvider) + registerReloadListener(recipeCache) + registerReloadListener(miningData) ReloadRegistrationEvent.publish(ReloadRegistrationEvent(this)) registerReloadListener { if (TestUtil.isInTest) return@registerReloadListener @@ -73,7 +76,6 @@ object RepoManager { } } } - registerReloadListener(recipeCache) } } @@ -100,16 +102,16 @@ object RepoManager { fun getNEUItem(skyblockId: SkyblockId): NEUItem? = neuRepo.items.getItemBySkyblockId(skyblockId.neuItem) + fun downloadOverridenBranch(branch: String) { + Firmament.coroutineScope.launch { + RepoDownloadManager.downloadUpdate(true, branch) + reload() + } + } + fun launchAsyncUpdate(force: Boolean = false) { Firmament.coroutineScope.launch { - ItemCache.ReloadProgressHud.reportProgress("Downloading", 0, -1) // TODO: replace with a proper bouncy bar - ItemCache.ReloadProgressHud.isEnabled = true - try { - RepoDownloadManager.downloadUpdate(force) - ItemCache.ReloadProgressHud.reportProgress("Download complete", 1, 1) - } finally { - ItemCache.ReloadProgressHud.isEnabled = false - } + RepoDownloadManager.downloadUpdate(force) reload() } } @@ -127,10 +129,6 @@ object RepoManager { return } try { - ItemCache.ReloadProgressHud.reportProgress("Reloading from Disk", - 0, - -1) // TODO: replace with a proper bouncy bar - ItemCache.ReloadProgressHud.isEnabled = true logger.info("Repo reload started.") neuRepo.reload() logger.info("Repo reload completed.") @@ -140,7 +138,6 @@ object RepoManager { tr("firmament.repo.reloadfail", "Failed to reload repository. This will result in some mod features not working.") ) - ItemCache.ReloadProgressHud.isEnabled = false } } diff --git a/src/main/kotlin/repo/RepoModResourcePack.kt b/src/main/kotlin/repo/RepoModResourcePack.kt index 617efec..2fdf710 100644 --- a/src/main/kotlin/repo/RepoModResourcePack.kt +++ b/src/main/kotlin/repo/RepoModResourcePack.kt @@ -5,6 +5,7 @@ import java.nio.file.Files import java.nio.file.Path import java.util.* import net.fabricmc.fabric.api.resource.ModResourcePack +import net.fabricmc.fabric.impl.resource.loader.ModResourcePackSorter import net.fabricmc.loader.api.FabricLoader import net.fabricmc.loader.api.metadata.ModMetadata import kotlin.io.path.exists @@ -28,9 +29,9 @@ import moe.nea.firmament.Firmament class RepoModResourcePack(val basePath: Path) : ModResourcePack { companion object { - fun append(packs: MutableList<in ModResourcePack>) { + fun append(packs: ModResourcePackSorter) { Firmament.logger.info("Registering mod resource pack") - packs.add(RepoModResourcePack(RepoDownloadManager.repoSavedLocation)) + packs.addPack(RepoModResourcePack(RepoDownloadManager.repoSavedLocation)) } fun createResourceDirectly(identifier: Identifier): Optional<Resource> { diff --git a/src/main/kotlin/repo/SBItemStack.kt b/src/main/kotlin/repo/SBItemStack.kt index da34707..3690866 100644 --- a/src/main/kotlin/repo/SBItemStack.kt +++ b/src/main/kotlin/repo/SBItemStack.kt @@ -34,6 +34,7 @@ import moe.nea.firmament.util.modifyExtraAttributes import moe.nea.firmament.util.petData import moe.nea.firmament.util.prepend import moe.nea.firmament.util.reconstitute +import moe.nea.firmament.util.removeColorCodes import moe.nea.firmament.util.skyBlockId import moe.nea.firmament.util.skyblock.ItemType import moe.nea.firmament.util.skyblock.Rarity @@ -81,6 +82,7 @@ data class SBItemStack constructor( } val EMPTY = SBItemStack(SkyblockId.NULL, 0) + private val BREAKING_POWER_REGEX = "Breaking Power (?<power>[0-9]+)".toPattern() operator fun invoke(itemStack: ItemStack): SBItemStack { val skyblockId = itemStack.skyBlockId ?: SkyblockId.NULL return SBItemStack( @@ -349,6 +351,12 @@ data class SBItemStack constructor( private var itemStack_: ItemStack? = null + val breakingPower: Int + get() = + BREAKING_POWER_REGEX.useMatch(neuItem?.lore?.firstOrNull()?.removeColorCodes()) { + group("power").toInt() + } ?: 0 + private val itemStack: ItemStack get() { val itemStack = itemStack_ ?: run { diff --git a/src/main/kotlin/util/FirmFormatters.kt b/src/main/kotlin/util/FirmFormatters.kt index 4b32c2a..a660f51 100644 --- a/src/main/kotlin/util/FirmFormatters.kt +++ b/src/main/kotlin/util/FirmFormatters.kt @@ -9,11 +9,40 @@ import kotlin.io.path.isReadable import kotlin.io.path.isRegularFile import kotlin.io.path.listDirectoryEntries import kotlin.math.absoluteValue +import kotlin.math.roundToInt import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds import net.minecraft.text.Text +import net.minecraft.util.math.BlockPos object FirmFormatters { + + private inline fun shortIf( + value: Double, breakpoint: Double, char: String, + return_: (String) -> Nothing + ) { + if (value >= breakpoint) { + val broken = (value / breakpoint * 10).roundToInt() + if (broken > 99) + return_((broken / 10).toString() + char) + val decimals = broken.toString() + decimals.singleOrNull()?.let { + return_("0.$it$char") + } + return_("${decimals[0]}.${decimals[1]}$char") + } + } + + fun shortFormat(double: Double): String { + if (double < 0) return "-" + shortFormat(-double) + shortIf(double, 1_000_000_000_000.0, "t") { return it } + shortIf(double, 1_000_000_000.0, "b") { return it } + shortIf(double, 1_000_000.0, "m") { return it } + shortIf(double, 1_000.0, "k") { return it } + shortIf(double, 1.0, "") { return it } + return double.toString() + } + fun formatCommas(int: Int, segments: Int = 3): String = formatCommas(int.toLong(), segments) fun formatCommas(long: Long, segments: Int = 3, includeSign: Boolean = false): String { if (long < 0 && long != Long.MIN_VALUE) { @@ -103,4 +132,7 @@ object FirmFormatters { return if (boolean == trueIsGood) text.lime() else text.red() } + fun formatPosition(position: BlockPos): Text { + return Text.literal("x: ${position.x}, y: ${position.y}, z: ${position.z}") + } } diff --git a/src/main/kotlin/util/MC.kt b/src/main/kotlin/util/MC.kt index 215d2a8..c1a5e65 100644 --- a/src/main/kotlin/util/MC.kt +++ b/src/main/kotlin/util/MC.kt @@ -7,11 +7,13 @@ import net.minecraft.client.gui.hud.InGameHud import net.minecraft.client.gui.screen.Screen import net.minecraft.client.gui.screen.ingame.HandledScreen import net.minecraft.client.network.ClientPlayerEntity +import net.minecraft.client.render.GameRenderer import net.minecraft.client.render.WorldRenderer import net.minecraft.client.render.item.ItemRenderer import net.minecraft.client.world.ClientWorld import net.minecraft.entity.Entity import net.minecraft.item.Item +import net.minecraft.item.ItemStack import net.minecraft.network.packet.c2s.play.CommandExecutionC2SPacket import net.minecraft.registry.BuiltinRegistries import net.minecraft.registry.RegistryKeys @@ -85,6 +87,7 @@ object MC { inline val resourceManager get() = (instance.resourceManager as ReloadableResourceManagerImpl) inline val itemRenderer: ItemRenderer get() = instance.itemRenderer inline val worldRenderer: WorldRenderer get() = instance.worldRenderer + inline val gameRenderer: GameRenderer get() = instance.gameRenderer inline val networkHandler get() = player?.networkHandler inline val instance get() = MinecraftClient.getInstance() inline val keyboard get() = instance.keyboard @@ -96,6 +99,7 @@ object MC { inline val soundManager get() = instance.soundManager inline val player: ClientPlayerEntity? get() = TestUtil.unlessTesting { instance.player } inline val camera: Entity? get() = instance.cameraEntity + inline val stackInHand: ItemStack get() = player?.inventory?.mainHandStack ?: ItemStack.EMPTY inline val guiAtlasManager get() = instance.guiAtlasManager inline val world: ClientWorld? get() = TestUtil.unlessTesting { instance.world } inline val playerName: String? get() = player?.name?.unformattedString @@ -109,6 +113,7 @@ object MC { val defaultRegistries: RegistryWrapper.WrapperLookup by lazy { BuiltinRegistries.createWrapperLookup() } inline val currentOrDefaultRegistries get() = currentRegistries ?: defaultRegistries val defaultItems: RegistryWrapper.Impl<Item> by lazy { defaultRegistries.getOrThrow(RegistryKeys.ITEM) } + var currentTick = 0 var lastWorld: World? = null get() { field = world ?: field diff --git a/src/main/kotlin/util/SBData.kt b/src/main/kotlin/util/SBData.kt index b2f9449..1a4734c 100644 --- a/src/main/kotlin/util/SBData.kt +++ b/src/main/kotlin/util/SBData.kt @@ -31,6 +31,10 @@ object SBData { val hypixelTimeZone = ZoneId.of("US/Eastern") private var hasReceivedProfile = false var locraw: Locraw? = null + + /** + * The current server location the player is in. This will be null outside of SkyBlock. + */ val skyblockLocation: SkyBlockIsland? get() = locraw?.skyblockLocation val hasValidLocraw get() = locraw?.server !in listOf("limbo", null) val isOnSkyblock get() = locraw?.gametype == "SKYBLOCK" diff --git a/src/main/kotlin/util/SkyBlockIsland.kt b/src/main/kotlin/util/SkyBlockIsland.kt index a86543c..e7f955a 100644 --- a/src/main/kotlin/util/SkyBlockIsland.kt +++ b/src/main/kotlin/util/SkyBlockIsland.kt @@ -1,4 +1,3 @@ - package moe.nea.firmament.util import kotlinx.serialization.KSerializer @@ -13,33 +12,41 @@ import moe.nea.firmament.repo.RepoManager @Serializable(with = SkyBlockIsland.Serializer::class) class SkyBlockIsland private constructor( - val locrawMode: String, + val locrawMode: String, ) { - object Serializer : KSerializer<SkyBlockIsland> { - override val descriptor: SerialDescriptor - get() = PrimitiveSerialDescriptor("SkyBlockIsland", PrimitiveKind.STRING) - - override fun deserialize(decoder: Decoder): SkyBlockIsland { - return forMode(decoder.decodeString()) - } - - override fun serialize(encoder: Encoder, value: SkyBlockIsland) { - encoder.encodeString(value.locrawMode) - } - } - companion object { - private val allIslands = mutableMapOf<String, SkyBlockIsland>() - fun forMode(mode: String): SkyBlockIsland = allIslands.computeIfAbsent(mode, ::SkyBlockIsland) - val HUB = forMode("hub") - val PRIVATE_ISLAND = forMode("dynamic") - val RIFT = forMode("rift") - val MINESHAFT = forMode("mineshaft") - val GARDEN = forMode("garden") - val DUNGEON = forMode("dungeon") - } - - val userFriendlyName - get() = RepoManager.neuRepo.constants.islands.areaNames - .getOrDefault(locrawMode, locrawMode) + object Serializer : KSerializer<SkyBlockIsland> { + override val descriptor: SerialDescriptor + get() = PrimitiveSerialDescriptor("SkyBlockIsland", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): SkyBlockIsland { + return forMode(decoder.decodeString()) + } + + override fun serialize(encoder: Encoder, value: SkyBlockIsland) { + encoder.encodeString(value.locrawMode) + } + } + + companion object { + private val allIslands = mutableMapOf<String, SkyBlockIsland>() + fun forMode(mode: String): SkyBlockIsland = allIslands.computeIfAbsent(mode, ::SkyBlockIsland) + val HUB = forMode("hub") + val DWARVEN_MINES = forMode("dwarven_mines") + val CRYSTAL_HOLLOWS = forMode("crystal_hollows") + val CRIMSON_ISLE = forMode("crimson_isle") + val PRIVATE_ISLAND = forMode("dynamic") + val RIFT = forMode("rift") + val MINESHAFT = forMode("mineshaft") + val GARDEN = forMode("garden") + val DUNGEON = forMode("dungeon") + val NIL = forMode("_") + } + + val hasCustomMining + get() = RepoManager.miningData.customMiningAreas[this]?.isSpecialMining ?: false + + val userFriendlyName + get() = RepoManager.neuRepo.constants.islands.areaNames + .getOrDefault(locrawMode, locrawMode) } diff --git a/src/main/kotlin/util/SkyblockId.kt b/src/main/kotlin/util/SkyblockId.kt index a99afda..a31255c 100644 --- a/src/main/kotlin/util/SkyblockId.kt +++ b/src/main/kotlin/util/SkyblockId.kt @@ -34,7 +34,7 @@ import moe.nea.firmament.util.json.DashlessUUIDSerializer */ @JvmInline @Serializable -value class SkyblockId(val neuItem: String) { +value class SkyblockId(val neuItem: String) : Comparable<SkyblockId> { val identifier get() = Identifier.of("skyblockitem", neuItem.lowercase().replace(";", "__") @@ -48,6 +48,10 @@ value class SkyblockId(val neuItem: String) { return neuItem } + override fun compareTo(other: SkyblockId): Int { + return neuItem.compareTo(other.neuItem) + } + /** * A bazaar stock item id, as returned by the HyPixel bazaar api endpoint. * These are not equivalent to the in-game ids, or the NEU repo ids, and in fact, do not refer to items, but instead diff --git a/src/main/kotlin/util/compatloader/CompatLoader.kt b/src/main/kotlin/util/compatloader/CompatLoader.kt index 6b60e87..d1073af 100644 --- a/src/main/kotlin/util/compatloader/CompatLoader.kt +++ b/src/main/kotlin/util/compatloader/CompatLoader.kt @@ -6,7 +6,7 @@ import kotlin.reflect.KClass import kotlin.streams.asSequence import moe.nea.firmament.Firmament -abstract class CompatLoader<T : Any>(val kClass: Class<T>) { +open class CompatLoader<T : Any>(val kClass: Class<T>) { constructor(kClass: KClass<T>) : this(kClass.java) val loader: ServiceLoader<T> = ServiceLoader.load(kClass) diff --git a/src/main/kotlin/util/compatloader/CompatMeta.kt b/src/main/kotlin/util/compatloader/CompatMeta.kt new file mode 100644 index 0000000..cf63645 --- /dev/null +++ b/src/main/kotlin/util/compatloader/CompatMeta.kt @@ -0,0 +1,48 @@ +package moe.nea.firmament.util.compatloader + +import java.util.ServiceLoader +import moe.nea.firmament.events.subscription.SubscriptionList +import moe.nea.firmament.init.AutoDiscoveryPlugin +import moe.nea.firmament.util.ErrorUtil + +/** + * Declares the compat meta interface for the current source set. + * This is used by [CompatLoader], [SubscriptionList], and [AutoDiscoveryPlugin]. Annotate a [ICompatMeta] object with + * this. + */ +annotation class CompatMeta + +interface ICompatMetaGen { + fun owns(className: String): Boolean + val meta: ICompatMeta +} + +interface ICompatMeta { + fun shouldLoad(): Boolean + + companion object { + val allMetas = ServiceLoader + .load(ICompatMetaGen::class.java) + .toList() + + fun shouldLoad(className: String): Boolean { + // TODO: replace this with a more performant package lookup + val meta = if (ErrorUtil.aggressiveErrors) { + val fittingMetas = allMetas.filter { it.owns(className) } + require(fittingMetas.size == 1) { "Orphaned or duplicate owned class $className (${fittingMetas.map { it.meta }}). Consider adding a @CompatMeta object." } + fittingMetas.single() + } else { + allMetas.firstOrNull { it.owns(className) } + } + return meta?.meta?.shouldLoad() ?: true + } + } +} + +object CompatHelper { + fun isOwnedByPackage(className: String, vararg packages: String): Boolean { + // TODO: create package lookup structure once + val packageName = className.substringBeforeLast('.') + return packageName in packages + } +} diff --git a/src/main/kotlin/util/data/MultiFileDataHolder.kt b/src/main/kotlin/util/data/MultiFileDataHolder.kt new file mode 100644 index 0000000..94c6f05 --- /dev/null +++ b/src/main/kotlin/util/data/MultiFileDataHolder.kt @@ -0,0 +1,63 @@ +package moe.nea.firmament.util.data + +import kotlinx.serialization.KSerializer +import kotlin.io.path.createDirectories +import kotlin.io.path.deleteExisting +import kotlin.io.path.exists +import kotlin.io.path.extension +import kotlin.io.path.listDirectoryEntries +import kotlin.io.path.nameWithoutExtension +import kotlin.io.path.readText +import kotlin.io.path.writeText +import moe.nea.firmament.Firmament + +abstract class MultiFileDataHolder<T>( + val dataSerializer: KSerializer<T>, + val configName: String +) { // TODO: abstract this + ProfileSpecificDataHolder + val configDirectory = Firmament.CONFIG_DIR.resolve(configName) + private var allData = readValues() + protected fun readValues(): MutableMap<String, T> { + if (!configDirectory.exists()) { + configDirectory.createDirectories() + } + val profileFiles = configDirectory.listDirectoryEntries() + return profileFiles + .filter { it.extension == "json" } + .mapNotNull { + try { + it.nameWithoutExtension to Firmament.json.decodeFromString(dataSerializer, it.readText()) + } catch (e: Exception) { /* Expecting IOException and SerializationException, but Kotlin doesn't allow multi catches*/ + IDataHolder.badLoads.add(configName) + Firmament.logger.error( + "Exception during loading of multi file data holder $it ($configName). This will reset that profiles config.", + e + ) + null + } + }.toMap().toMutableMap() + } + + fun save() { + if (!configDirectory.exists()) { + configDirectory.createDirectories() + } + val c = allData + configDirectory.listDirectoryEntries().forEach { + if (it.nameWithoutExtension !in c.mapKeys { it.toString() }) { + it.deleteExisting() + } + } + c.forEach { (name, value) -> + val f = configDirectory.resolve("$name.json") + f.writeText(Firmament.json.encodeToString(dataSerializer, value)) + } + } + + fun list(): Map<String, T> = allData + val validPathRegex = "[a-zA-Z0-9_][a-zA-Z0-9\\-_.]*".toPattern() + fun insert(name: String, value: T) { + require(validPathRegex.matcher(name).matches()) { "Not a valid name: $name" } + allData[name] = value + } +} diff --git a/src/main/kotlin/util/mc/FirmamentDataComponentTypes.kt b/src/main/kotlin/util/mc/FirmamentDataComponentTypes.kt index 012f52e..0866665 100644 --- a/src/main/kotlin/util/mc/FirmamentDataComponentTypes.kt +++ b/src/main/kotlin/util/mc/FirmamentDataComponentTypes.kt @@ -1,12 +1,15 @@ package moe.nea.firmament.util.mc import com.mojang.serialization.Codec +import io.netty.buffer.ByteBuf import net.minecraft.component.ComponentType +import net.minecraft.network.codec.PacketCodec import net.minecraft.registry.Registries import net.minecraft.registry.Registry import moe.nea.firmament.Firmament import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.ClientInitEvent +import moe.nea.firmament.repo.MiningRepoData object FirmamentDataComponentTypes { @@ -26,11 +29,32 @@ object FirmamentDataComponentTypes { ) } + fun <T> errorCodec(message: String): PacketCodec<in ByteBuf, T> = + object : PacketCodec<ByteBuf, T> { + override fun decode(buf: ByteBuf?): T? { + error(message) + } + + override fun encode(buf: ByteBuf?, value: T?) { + error(message) + } + } + + fun <T, B : ComponentType.Builder<T>> B.neverEncode(message: String = "This element should never be encoded or decoded"): B { + packetCodec(errorCodec(message)) + codec(null) + return this + } + val IS_BROKEN = register<Boolean>( "is_broken" ) { it.codec(Codec.BOOL.fieldOf("is_broken").codec()) } + val CUSTOM_MINING_BLOCK_DATA = register<MiningRepoData.CustomMiningBlock>("custom_mining_block") { + it.neverEncode() + } + } diff --git a/src/main/kotlin/util/mc/IntrospectableItemModelManager.kt b/src/main/kotlin/util/mc/IntrospectableItemModelManager.kt new file mode 100644 index 0000000..e546fd3 --- /dev/null +++ b/src/main/kotlin/util/mc/IntrospectableItemModelManager.kt @@ -0,0 +1,7 @@ +package moe.nea.firmament.util.mc + +import net.minecraft.util.Identifier + +interface IntrospectableItemModelManager { + fun hasModel_firmament(identifier: Identifier): Boolean +} diff --git a/src/main/kotlin/util/mc/asFakeServer.kt b/src/main/kotlin/util/mc/asFakeServer.kt new file mode 100644 index 0000000..d3811bd --- /dev/null +++ b/src/main/kotlin/util/mc/asFakeServer.kt @@ -0,0 +1,37 @@ +package moe.nea.firmament.util.mc + +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource +import net.minecraft.server.command.CommandOutput +import net.minecraft.server.command.ServerCommandSource +import net.minecraft.text.Text + +fun FabricClientCommandSource.asFakeServer(): ServerCommandSource { + val source = this + return ServerCommandSource( + object : CommandOutput { + override fun sendMessage(message: Text?) { + source.player.sendMessage(message, false) + } + + override fun shouldReceiveFeedback(): Boolean { + return true + } + + override fun shouldTrackOutput(): Boolean { + return true + } + + override fun shouldBroadcastConsoleToOps(): Boolean { + return true + } + }, + source.position, + source.rotation, + null, + 0, + "FakeServerCommandSource", + Text.literal("FakeServerCommandSource"), + null, + source.player + ) +} diff --git a/src/main/kotlin/util/regex.kt b/src/main/kotlin/util/regex.kt index a44435c..f239810 100644 --- a/src/main/kotlin/util/regex.kt +++ b/src/main/kotlin/util/regex.kt @@ -16,12 +16,13 @@ import kotlin.time.Duration.Companion.seconds inline fun <T> String.ifMatches(regex: Regex, block: (MatchResult) -> T): T? = regex.matchEntire(this)?.let(block) -inline fun <T> Pattern.useMatch(string: String, block: Matcher.() -> T): T? { +inline fun <T> Pattern.useMatch(string: String?, block: Matcher.() -> T): T? { contract { callsInPlace(block, InvocationKind.AT_MOST_ONCE) } - return matcher(string) - .takeIf(Matcher::matches) + return string + ?.let(this::matcher) + ?.takeIf(Matcher::matches) ?.let(block) } diff --git a/src/main/kotlin/util/render/TintedOverlayTexture.kt b/src/main/kotlin/util/render/TintedOverlayTexture.kt new file mode 100644 index 0000000..a02eccc --- /dev/null +++ b/src/main/kotlin/util/render/TintedOverlayTexture.kt @@ -0,0 +1,44 @@ +package moe.nea.firmament.util.render + +import com.mojang.blaze3d.platform.GlConst +import com.mojang.blaze3d.systems.RenderSystem +import me.shedaniel.math.Color +import net.minecraft.client.render.OverlayTexture +import net.minecraft.util.math.ColorHelper +import moe.nea.firmament.util.ErrorUtil + +class TintedOverlayTexture : OverlayTexture() { + companion object { + val size = 16 + } + + private var lastColor: Color? = null + fun setColor(color: Color): TintedOverlayTexture { + val image = ErrorUtil.notNullOr(texture.image, "Disposed TintedOverlayTexture written to") { return this } + if (color == lastColor) return this + lastColor = color + + for (i in 0..<size) { + for (j in 0..<size) { + if (i < 8) { + image.setColorArgb(j, i, 0xB2FF0000.toInt()) + } else { + val k = ((1F - j / 15F * 0.75F) * 255F).toInt() + image.setColorArgb(j, i, ColorHelper.withAlpha(k, color.color)) + } + } + } + + RenderSystem.activeTexture(GlConst.GL_TEXTURE1) + texture.bindTexture() + texture.setFilter(false, false) + texture.setClamp(true) + image.upload(0, + 0, 0, + 0, 0, + image.width, image.height, + false) + RenderSystem.activeTexture(GlConst.GL_TEXTURE0) + return this + } +} diff --git a/src/main/kotlin/util/skyblock/SkyBlockItems.kt b/src/main/kotlin/util/skyblock/SkyBlockItems.kt index cfd8429..ca2b17b 100644 --- a/src/main/kotlin/util/skyblock/SkyBlockItems.kt +++ b/src/main/kotlin/util/skyblock/SkyBlockItems.kt @@ -8,4 +8,9 @@ object SkyBlockItems { val DIAMOND = SkyblockId("DIAMOND") val ANCESTRAL_SPADE = SkyblockId("ANCESTRAL_SPADE") val REFORGE_ANVIL = SkyblockId("REFORGE_ANVIL") + val SLICE_OF_BLUEBERRY_CAKE = SkyblockId("SLICE_OF_BLUEBERRY_CAKE") + val SLICE_OF_CHEESECAKE = SkyblockId("SLICE_OF_CHEESECAKE") + val SLICE_OF_GREEN_VELVET_CAKE = SkyblockId("SLICE_OF_GREEN_VELVET_CAKE") + val SLICE_OF_RED_VELVET_CAKE = SkyblockId("SLICE_OF_RED_VELVET_CAKE") + val SLICE_OF_STRAWBERRY_SHORTCAKE = SkyblockId("SLICE_OF_STRAWBERRY_SHORTCAKE") } diff --git a/src/main/kotlin/util/textutil.kt b/src/main/kotlin/util/textutil.kt index 33cdd2a..806f61e 100644 --- a/src/main/kotlin/util/textutil.kt +++ b/src/main/kotlin/util/textutil.kt @@ -2,6 +2,7 @@ package moe.nea.firmament.util import java.util.Optional import net.minecraft.text.ClickEvent +import net.minecraft.text.HoverEvent import net.minecraft.text.MutableText import net.minecraft.text.OrderedText import net.minecraft.text.PlainTextContent @@ -122,9 +123,11 @@ fun MutableText.pink() = withColor(Formatting.LIGHT_PURPLE) fun MutableText.yellow() = withColor(Formatting.YELLOW) fun MutableText.gold() = withColor(Formatting.GOLD) fun MutableText.grey() = withColor(Formatting.GRAY) +fun MutableText.darkGrey() = withColor(Formatting.DARK_GRAY) fun MutableText.red() = withColor(Formatting.RED) fun MutableText.white() = withColor(Formatting.WHITE) fun MutableText.bold(): MutableText = styled { it.withBold(true) } +fun MutableText.hover(text: Text): MutableText = styled {it.withHoverEvent(HoverEvent(HoverEvent.Action.SHOW_TEXT, text))} fun MutableText.clickCommand(command: String): MutableText { diff --git a/src/main/resources/assets/firmament/gui/mining_block_info/index.xml b/src/main/resources/assets/firmament/gui/mining_block_info/index.xml new file mode 100644 index 0000000..6404995 --- /dev/null +++ b/src/main/resources/assets/firmament/gui/mining_block_info/index.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<Root xmlns="http://notenoughupdates.org/moulconfig" + xmlns:firm="http://firmament.nea.moe/moulconfig" +> + <Gui> + <Column> + <Row> + <Text text="Search: "/> + <TextField value="@search"/> + </Row> + <ScrollPanel width="200" height="150"> + <Array data="@ores"> + <Column> + <Text text="@oreName"/> + <Array data="@blocks"> + <Row> + <When condition="@isSelected"> + <Center> + <Text text="§a+" textAlign="CENTER" width="10"/> + </Center> + <Spacer width="10" height="0"/> + </When> + <firm:Hover lines="@restrictions"> + <Row> + <ItemStack value="@item"/> + <Align horizontal="LEFT" vertical="CENTER"> + <Text text="@itemName"/> + </Align> + </Row> + </firm:Hover> + </Row> + </Array> + </Column> + </Array> + </ScrollPanel> + </Column> + </Gui> +</Root> diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 3b988b1..02c11ee 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -40,6 +40,9 @@ "modmenu": [ "moe.nea.firmament.compat.modmenu.FirmamentModMenuPlugin" ], + "jade": [ + "moe.nea.firmament.compat.jade.FirmamentJadePlugin" + ], "jarvis": [ "moe.nea.firmament.jarvis.JarvisIntegration" ] @@ -48,7 +51,7 @@ "firmament.mixins.json" ], "depends": { - "fabric": "*", + "fabric": ">=${fabric_api_version}", "fabric-language-kotlin": ">=${fabric_kotlin_version}", "minecraft": ">=${minecraft_version}" }, diff --git a/src/main/resources/firmament.accesswidener b/src/main/resources/firmament.accesswidener index 7418529..fd79cb5 100644 --- a/src/main/resources/firmament.accesswidener +++ b/src/main/resources/firmament.accesswidener @@ -20,3 +20,9 @@ mutable field net/minecraft/screen/slot/Slot y I accessible field net/minecraft/entity/player/PlayerEntity PLAYER_MODEL_PARTS Lnet/minecraft/entity/data/TrackedData; accessible field net/minecraft/client/render/WorldRenderer chunks Lnet/minecraft/client/render/BuiltChunkStorage; +accessible field net/minecraft/client/render/OverlayTexture texture Lnet/minecraft/client/texture/NativeImageBackedTexture; + +accessible method net/minecraft/client/render/RenderPhase$Texture getId ()Ljava/util/Optional; +accessible field net/minecraft/client/render/RenderLayer$MultiPhase phases Lnet/minecraft/client/render/RenderLayer$MultiPhaseParameters; +accessible field net/minecraft/client/render/RenderLayer$MultiPhaseParameters texture Lnet/minecraft/client/render/RenderPhase$TextureBase; +accessible field net/minecraft/client/network/ClientPlayerInteractionManager currentBreakingPos Lnet/minecraft/util/math/BlockPos; diff --git a/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/player_inventory.png b/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/player_inventory.png Binary files differnew file mode 100644 index 0000000..1831ef3 --- /dev/null +++ b/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/player_inventory.png diff --git a/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png b/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png Binary files differnew file mode 100644 index 0000000..5b774b2 --- /dev/null +++ b/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png diff --git a/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.mcmeta b/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.mcmeta new file mode 100644 index 0000000..94b9a1d --- /dev/null +++ b/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 17, + "height": 18, + "border": 2 + } + } +} diff --git a/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png b/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png Binary files differnew file mode 100644 index 0000000..d4852d8 --- /dev/null +++ b/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png diff --git a/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png.mcmeta b/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png.mcmeta new file mode 100644 index 0000000..5964a6f --- /dev/null +++ b/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 91, + "height": 184, + "border": 7 + } + } +} diff --git a/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png b/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png Binary files differnew file mode 100644 index 0000000..61e9ee5 --- /dev/null +++ b/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png diff --git a/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.mcmeta b/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.mcmeta new file mode 100644 index 0000000..cd2857e --- /dev/null +++ b/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.mcmeta @@ -0,0 +1,9 @@ +{ + "gui": { + "scaling": { + "type": "tile", + "width": 162, + "height": 18 + } + } +} diff --git a/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png b/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png Binary files differnew file mode 100644 index 0000000..653a99e --- /dev/null +++ b/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png diff --git a/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.mcmeta b/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.mcmeta new file mode 100644 index 0000000..a29299d --- /dev/null +++ b/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 176, + "height": 222, + "border": 10 + } + } +} diff --git a/src/main/resources/resourcepacks/transparent_storage/pack.mcmeta b/src/main/resources/resourcepacks/transparent_storage/pack.mcmeta new file mode 100644 index 0000000..c37df06 --- /dev/null +++ b/src/main/resources/resourcepacks/transparent_storage/pack.mcmeta @@ -0,0 +1,10 @@ +{ + "pack": { + "pack_format": 15, + "supported_formats": { + "min_inclusive": 15, + "max_inclusive": 2147483647 + }, + "description": "Adds a more transparent storage overlay for /firm storage" + } +} |