diff --git a/src/main/java/com/denizenscript/clientizen/access/BillboardParticleMixinAccess.java b/src/main/java/com/denizenscript/clientizen/access/BillboardParticleMixinAccess.java new file mode 100644 index 0000000..4e861ce --- /dev/null +++ b/src/main/java/com/denizenscript/clientizen/access/BillboardParticleMixinAccess.java @@ -0,0 +1,8 @@ +package com.denizenscript.clientizen.access; + +public interface BillboardParticleMixinAccess { + + float clientizen$getScale(); + + void clientizen$setScale(Float scale); +} diff --git a/src/main/java/com/denizenscript/clientizen/access/ParticleMixinAccess.java b/src/main/java/com/denizenscript/clientizen/access/ParticleMixinAccess.java new file mode 100644 index 0000000..f8ba338 --- /dev/null +++ b/src/main/java/com/denizenscript/clientizen/access/ParticleMixinAccess.java @@ -0,0 +1,17 @@ +package com.denizenscript.clientizen.access; + +import com.denizenscript.denizencore.flags.MapTagFlagTracker; +import net.minecraft.particle.ParticleType; + +import java.util.UUID; + +public interface ParticleMixinAccess { + + UUID clientizen$getUUID(); + + ParticleType clientizen$getType(); + + void clientizen$setType(ParticleType type); + + MapTagFlagTracker clientizen$getFlagTracker(); +} diff --git a/src/main/java/com/denizenscript/clientizen/access/RegistryMixinAccess.java b/src/main/java/com/denizenscript/clientizen/access/RegistryMixinAccess.java new file mode 100644 index 0000000..f550a52 --- /dev/null +++ b/src/main/java/com/denizenscript/clientizen/access/RegistryMixinAccess.java @@ -0,0 +1,10 @@ +package com.denizenscript.clientizen.access; + +import net.minecraft.util.Identifier; + +public interface RegistryMixinAccess { + + void clientizen$unfreeze(); + + void clientizen$remove(Identifier toRemove); +} diff --git a/src/main/java/com/denizenscript/clientizen/mixin/ParticleAccessor.java b/src/main/java/com/denizenscript/clientizen/mixin/ParticleAccessor.java deleted file mode 100644 index 3127930..0000000 --- a/src/main/java/com/denizenscript/clientizen/mixin/ParticleAccessor.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.denizenscript.clientizen.mixin; - -import net.minecraft.client.particle.Particle; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.gen.Invoker; - -@Mixin(Particle.class) -public interface ParticleAccessor { - - @Invoker - void invokeSetAlpha(float alpha); -} diff --git a/src/main/java/com/denizenscript/clientizen/mixin/RegistryMixin.java b/src/main/java/com/denizenscript/clientizen/mixin/RegistryMixin.java new file mode 100644 index 0000000..50c066d --- /dev/null +++ b/src/main/java/com/denizenscript/clientizen/mixin/RegistryMixin.java @@ -0,0 +1,87 @@ +package com.denizenscript.clientizen.mixin; + +import com.denizenscript.clientizen.access.RegistryMixinAccess; +import com.denizenscript.denizencore.exceptions.InvalidArgumentsRuntimeException; +import com.mojang.serialization.Lifecycle; +import it.unimi.dsi.fastutil.objects.ObjectList; +import it.unimi.dsi.fastutil.objects.Reference2IntMap; +import net.minecraft.registry.Registry; +import net.minecraft.registry.RegistryKey; +import net.minecraft.registry.SimpleRegistry; +import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.registry.entry.RegistryEntryInfo; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.IdentityHashMap; +import java.util.Map; + +@Mixin(SimpleRegistry.class) +public abstract class RegistryMixin implements RegistryMixinAccess { + + @Unique + boolean clientizen$isIntrusive; + + @Shadow + private boolean frozen; + @Shadow + public abstract RegistryKey> getKey(); + @Shadow + private @Nullable Map> intrusiveValueToEntry; + @Shadow + @Final + private ObjectList> rawIdToEntry; + @Shadow + @Final + private Map> idToEntry; + @Shadow + @Final + private Reference2IntMap entryToRawId; + @Shadow + @Final + private Map, RegistryEntry.Reference> keyToEntry; + @Shadow + @Final + private Map> valueToEntry; + @Shadow + @Final + private Map, RegistryEntryInfo> keyToEntryInfo; + + @Override + public void clientizen$unfreeze() { + if (!frozen) { + return; + } + frozen = false; + if (clientizen$isIntrusive) { + intrusiveValueToEntry = new IdentityHashMap<>(); + } + } + + @Override + public void clientizen$remove(Identifier toRemove) { + RegistryEntry.Reference value = idToEntry.get(toRemove); + if (value == null) { + throw new InvalidArgumentsRuntimeException("Unable to remove '" + toRemove + "' from registry '" + getKey() + "': registry has no value by that key."); + } + RegistryKey key = RegistryKey.of(getKey(), toRemove); + keyToEntry.remove(key); + idToEntry.remove(toRemove); + valueToEntry.remove(value.value()); + rawIdToEntry.remove(value); + entryToRawId.removeInt(value.value()); + keyToEntryInfo.remove(key); + } + + @Inject(method = "(Lnet/minecraft/registry/RegistryKey;Lcom/mojang/serialization/Lifecycle;Z)V", at = @At("TAIL")) + private void clientizen$saveIsIntrusive(RegistryKey key, Lifecycle lifecycle, boolean intrusive, CallbackInfo ci) { + clientizen$isIntrusive = intrusive; + } +} diff --git a/src/main/java/com/denizenscript/clientizen/mixin/particle/BillboardParticleMixin.java b/src/main/java/com/denizenscript/clientizen/mixin/particle/BillboardParticleMixin.java new file mode 100644 index 0000000..7b115ee --- /dev/null +++ b/src/main/java/com/denizenscript/clientizen/mixin/particle/BillboardParticleMixin.java @@ -0,0 +1,38 @@ +package com.denizenscript.clientizen.mixin.particle; + +import com.denizenscript.clientizen.access.BillboardParticleMixinAccess; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import net.minecraft.client.particle.BillboardParticle; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(BillboardParticle.class) +public abstract class BillboardParticleMixin implements BillboardParticleMixinAccess { + + @Shadow + protected float scale; + + @Unique + Float clientizen$scale; + + @Override + public float clientizen$getScale() { + return clientizen$scale != null ? clientizen$scale : scale; + } + + @Override + public void clientizen$setScale(Float scale) { + clientizen$scale = scale; + } + + @WrapOperation( + method = "buildGeometry", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/particle/BillboardParticle;getSize(F)F") + ) + private float clientizen$overrideScale(BillboardParticle particle, float tickDelta, Operation original) { + return clientizen$scale != null ? clientizen$scale : original.call(particle, tickDelta); + } +} diff --git a/src/main/java/com/denizenscript/clientizen/mixin/particle/ParticleAccessor.java b/src/main/java/com/denizenscript/clientizen/mixin/particle/ParticleAccessor.java new file mode 100644 index 0000000..eb514e3 --- /dev/null +++ b/src/main/java/com/denizenscript/clientizen/mixin/particle/ParticleAccessor.java @@ -0,0 +1,59 @@ +package com.denizenscript.clientizen.mixin.particle; + +import net.minecraft.client.particle.Particle; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(Particle.class) +public interface ParticleAccessor { + + // MCDev plugin bug, need to specify the name in the annotation to avoid IDE errors + @Accessor("x") + double getX(); + + @Accessor("y") + double getY(); + + @Accessor("z") + double getZ(); + + @Accessor + double getVelocityX(); + + @Accessor + double getVelocityY(); + + @Accessor + double getVelocityZ(); + + @Accessor + float getRed(); + + @Accessor + float getGreen(); + + @Accessor + float getBlue(); + + @Accessor + float getAlpha(); + + @Invoker + void invokeSetAlpha(float alpha); + + @Accessor + boolean isOnGround(); + + @Accessor("collidesWithWorld") + boolean collidesWithWorld(); + + @Accessor + void setCollidesWithWorld(boolean collidesWithWorld); + + @Accessor + int getAge(); + + @Accessor + void setAge(int age); +} diff --git a/src/main/java/com/denizenscript/clientizen/mixin/particle/ParticleManagerAccessor.java b/src/main/java/com/denizenscript/clientizen/mixin/particle/ParticleManagerAccessor.java new file mode 100644 index 0000000..1d72735 --- /dev/null +++ b/src/main/java/com/denizenscript/clientizen/mixin/particle/ParticleManagerAccessor.java @@ -0,0 +1,15 @@ +package com.denizenscript.clientizen.mixin.particle; + +import net.minecraft.client.particle.ParticleManager; +import net.minecraft.util.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import java.util.Map; + +@Mixin(ParticleManager.class) +public interface ParticleManagerAccessor { + + @Accessor("spriteAwareFactories") + Map getSpriteProviderMap(); +} diff --git a/src/main/java/com/denizenscript/clientizen/mixin/particle/ParticleManagerMixin.java b/src/main/java/com/denizenscript/clientizen/mixin/particle/ParticleManagerMixin.java new file mode 100644 index 0000000..8b8c798 --- /dev/null +++ b/src/main/java/com/denizenscript/clientizen/mixin/particle/ParticleManagerMixin.java @@ -0,0 +1,21 @@ +package com.denizenscript.clientizen.mixin.particle; + +import com.denizenscript.clientizen.access.ParticleMixinAccess; +import net.minecraft.client.particle.Particle; +import net.minecraft.client.particle.ParticleManager; +import net.minecraft.particle.ParticleEffect; +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; + +@Mixin(ParticleManager.class) +public abstract class ParticleManagerMixin { + + @Inject(method = "createParticle", at = @At("RETURN")) + private void clientizen$storeParticleType(T parameters, double x, double y, double z, double velocityX, double velocityY, double velocityZ, CallbackInfoReturnable cir) { + if (cir.getReturnValue() instanceof ParticleMixinAccess particle) { + particle.clientizen$setType(parameters.getType()); + } + } +} diff --git a/src/main/java/com/denizenscript/clientizen/mixin/particle/ParticleMixin.java b/src/main/java/com/denizenscript/clientizen/mixin/particle/ParticleMixin.java new file mode 100644 index 0000000..95e7190 --- /dev/null +++ b/src/main/java/com/denizenscript/clientizen/mixin/particle/ParticleMixin.java @@ -0,0 +1,58 @@ +package com.denizenscript.clientizen.mixin.particle; + +import com.denizenscript.clientizen.access.ParticleMixinAccess; +import com.denizenscript.clientizen.objects.ParticleTag; +import com.denizenscript.denizencore.flags.MapTagFlagTracker; +import net.minecraft.client.particle.Particle; +import net.minecraft.particle.ParticleType; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.UUID; + +@Mixin(Particle.class) +public abstract class ParticleMixin implements ParticleMixinAccess { + + @Unique + final UUID clientizen$id = UUID.randomUUID(); + @Unique + ParticleType clientizen$particleType; + @Unique + MapTagFlagTracker clientizen$flagMap; + + @Inject(method = "(Lnet/minecraft/client/world/ClientWorld;DDD)V", at = @At("TAIL")) + private void clientizen$onParticleCreated(CallbackInfo ci) { + ParticleTag.particles.put(clientizen$id, (Particle) (Object) this); + } + + @Inject(method = "markDead", at = @At("TAIL")) + private void clientizen$onParticleRemoved(CallbackInfo ci) { + ParticleTag.particles.remove(clientizen$id); + } + + @Override + public UUID clientizen$getUUID() { + return clientizen$id; + } + + @Override + public ParticleType clientizen$getType() { + return clientizen$particleType; + } + + @Override + public void clientizen$setType(ParticleType type) { + clientizen$particleType = type; + } + + @Override + public MapTagFlagTracker clientizen$getFlagTracker() { + if (clientizen$flagMap == null) { + clientizen$flagMap = new MapTagFlagTracker(); + } + return clientizen$flagMap; + } +} diff --git a/src/main/java/com/denizenscript/clientizen/mixin/particle/SpriteBillboardParticleAccessor.java b/src/main/java/com/denizenscript/clientizen/mixin/particle/SpriteBillboardParticleAccessor.java new file mode 100644 index 0000000..ba4ab5b --- /dev/null +++ b/src/main/java/com/denizenscript/clientizen/mixin/particle/SpriteBillboardParticleAccessor.java @@ -0,0 +1,17 @@ +package com.denizenscript.clientizen.mixin.particle; + +import net.minecraft.client.particle.SpriteBillboardParticle; +import net.minecraft.client.texture.Sprite; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(SpriteBillboardParticle.class) +public interface SpriteBillboardParticleAccessor { + + @Accessor + Sprite getSprite(); + + @Invoker + void invokeSetSprite(Sprite sprite); +} diff --git a/src/main/java/com/denizenscript/clientizen/objects/ClientizenObjectRegistry.java b/src/main/java/com/denizenscript/clientizen/objects/ClientizenObjectRegistry.java index 37a25ac..47e6e0d 100644 --- a/src/main/java/com/denizenscript/clientizen/objects/ClientizenObjectRegistry.java +++ b/src/main/java/com/denizenscript/clientizen/objects/ClientizenObjectRegistry.java @@ -10,12 +10,62 @@ public class ClientizenObjectRegistry { public static ObjectType TYPE_MATERIAL; public static ObjectType TYPE_ITEM; public static ObjectType TYPE_MOD; + public static ObjectType TYPE_PARTICLE; public static void registerObjects() { + + // <--[tag] + // @attribute ]> + // @returns EntityTag + // @description + // Returns an entity object constructed from the input value. + // Refer to <@link ObjectType EntityTag>. + // --> TYPE_ENTITY = ObjectFetcher.registerWithObjectFetcher(EntityTag.class, EntityTag.tagProcessor).setAsNOtherCode().generateBaseTag(); + + // <--[tag] + // @attribute ]> + // @returns LocationTag + // @description + // Returns a location object constructed from the input value. + // Refer to <@link ObjectType LocationTag>. + // --> TYPE_LOCATION = ObjectFetcher.registerWithObjectFetcher(LocationTag.class, LocationTag.tagProcessor).setAsNOtherCode().setCanConvertStatic().generateBaseTag(); + + // <--[tag] + // @attribute ]> + // @returns MaterialTag + // @description + // Returns a material object constructed from the input value. + // Refer to <@link ObjectType MaterialTag>. + // --> TYPE_MATERIAL = ObjectFetcher.registerWithObjectFetcher(MaterialTag.class, MaterialTag.tagProcessor).generateBaseTag(); + + // <--[tag] + // @attribute ]> + // @returns ItemTag + // @description + // Returns an item object constructed from the input value. + // Refer to <@link ObjectType ItemTag>. + // --> TYPE_ITEM = ObjectFetcher.registerWithObjectFetcher(ItemTag.class, ItemTag.tagProcessor).setAsNOtherCode().generateBaseTag(); + + // <--[tag] + // @attribute ]> + // @returns ModTag + // @description + // Returns a mod object constructed from the input value. + // Refer to <@link ObjectType ModTag>. + // --> TYPE_MOD = ObjectFetcher.registerWithObjectFetcher(ModTag.class, ModTag.tagProcessor).setAsNOtherCode().setCanConvertStatic().generateBaseTag(); + + // <--[tag] + // @attribute ]> + // @returns ParticleTag + // @description + // Returns a particle object constructed from the input value. + // Refer to <@link ObjectType ParticleTag>. + // --> + TYPE_PARTICLE = ObjectFetcher.registerWithObjectFetcher(ParticleTag.class, ParticleTag.tagProcessor).setAsNOtherCode().generateBaseTag(); } } diff --git a/src/main/java/com/denizenscript/clientizen/objects/ParticleTag.java b/src/main/java/com/denizenscript/clientizen/objects/ParticleTag.java new file mode 100644 index 0000000..c7f9306 --- /dev/null +++ b/src/main/java/com/denizenscript/clientizen/objects/ParticleTag.java @@ -0,0 +1,595 @@ +package com.denizenscript.clientizen.objects; + +import com.denizenscript.clientizen.access.BillboardParticleMixinAccess; +import com.denizenscript.clientizen.access.ParticleMixinAccess; +import com.denizenscript.clientizen.mixin.particle.ParticleAccessor; +import com.denizenscript.clientizen.mixin.particle.ParticleManagerAccessor; +import com.denizenscript.clientizen.mixin.particle.SpriteBillboardParticleAccessor; +import com.denizenscript.clientizen.scripts.containers.ParticleScriptContainer; +import com.denizenscript.clientizen.util.Utilities; +import com.denizenscript.denizencore.events.ScriptEvent; +import com.denizenscript.denizencore.flags.AbstractFlagTracker; +import com.denizenscript.denizencore.flags.FlaggableObject; +import com.denizenscript.denizencore.objects.Adjustable; +import com.denizenscript.denizencore.objects.Fetchable; +import com.denizenscript.denizencore.objects.Mechanism; +import com.denizenscript.denizencore.objects.ObjectTag; +import com.denizenscript.denizencore.objects.core.ColorTag; +import com.denizenscript.denizencore.objects.core.DurationTag; +import com.denizenscript.denizencore.objects.core.ElementTag; +import com.denizenscript.denizencore.objects.core.ScriptTag; +import com.denizenscript.denizencore.tags.Attribute; +import com.denizenscript.denizencore.tags.ObjectTagProcessor; +import com.denizenscript.denizencore.tags.TagContext; +import com.denizenscript.denizencore.utilities.CoreUtilities; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.particle.Particle; +import net.minecraft.client.particle.ParticleManager; +import net.minecraft.client.particle.SpriteBillboardParticle; +import net.minecraft.client.texture.Sprite; +import net.minecraft.client.texture.SpriteAtlasTexture; +import net.minecraft.registry.Registries; +import net.minecraft.util.Identifier; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class ParticleTag implements Adjustable, FlaggableObject { + + // <--[ObjectType] + // @name ParticleTag + // @prefix particle + // @ExampleTagBase [particle] + // @implements FlaggableObject + // @base ElementTag + // @format + // The identity format for particles is the particle's UUID. + // For example, 'particle@14ade8b1-746c-4952-881f-2844432aa277'. + // + // @description + // A ParticleTag represents a particle that currently exists in the world. + // Can be either a normal vanilla particle, one from a <@link language Particle Script Containers>, or one from another mod. + // + // This object type is flaggable. + // Flags on this object type will be stored on the particle. + // + // @Matchable + // ParticleTag matchers, sometimes identified as : + // "particle" plaintext: always matches. + // "script" plaintext: matches if the particle is from a <@link language Particle Script Containers>. + // Any particle type (see <@link tag ParticleTag.type> for formats): matches if the particle is of the given type, using advanced matchers. + // Any particle script name: matches if the particle is from the given script, using advanced matchers. + // + // --> + + public static final Map particles = new HashMap<>(); + + public static SpriteAtlasTexture getParticleAtlas() { + return (SpriteAtlasTexture) MinecraftClient.getInstance().getTextureManager().getTexture(SpriteAtlasTexture.PARTICLE_ATLAS_TEXTURE); + } + + public static Map getSpriteProviders() { + return ((ParticleManagerAccessor) MinecraftClient.getInstance().particleManager).getSpriteProviderMap(); + } + + @Fetchable("particle") + public static ParticleTag valueOf(String text, TagContext context) { + if (text.startsWith("particle@")) { + text = text.substring("particle@".length()); + } + UUID uuid = Utilities.uuidFromString(text); + if (uuid == null) { + Utilities.echoErrorByContext(context, "valueOf ParticleTag returning null: '" + text + "' isn't a valid UUID."); + return null; + } + Particle particle = particles.get(uuid); + if (particle == null) { + Utilities.echoErrorByContext(context, "valueOf ParticleTag returning null: UUID '" + uuid + "' is valid, but isn't matched to any particle."); + return null; + } + return new ParticleTag(particle); + } + + public static boolean matches(String text) { + if (text.startsWith("particle@")) { + return true; + } + return valueOf(text, CoreUtilities.noDebugContext) != null; + } + + public final Particle particle; + + public ParticleTag(Particle particle) { + this.particle = particle; + } + + public ParticleAccessor getAccessor() { + return (ParticleAccessor) particle; + } + + public ParticleMixinAccess getMixinAccess() { + return (ParticleMixinAccess) particle; + } + + public Identifier getTypeId() { + return Registries.PARTICLE_TYPE.getId(getMixinAccess().clientizen$getType()); + } + + public String getTypeString() { + return Utilities.idToString(getTypeId()); + } + + public static void register() { + AbstractFlagTracker.registerFlagHandlers(tagProcessor); + + // <--[tag] + // @attribute + // @returns ElementTag + // @description + // Returns the particle's particle type. + // For vanilla particles, this is their base vanilla type - see <@link url https://minecraft.wiki/w/Particles_(Java_Edition)#Types_of_particles>. + // For clientizen particles, this is their custom particle type ("clientizen:") - should generally prefer <@link tag ParticleTag.script>. + // For particles added by other mods, this is a custom particle type in namespaced key format (":"). + // @example + // # Use to check if a particle is a flame particle. + // - if <[particle].type> == flame: + // - narrate "It's a flame particle!" + // --> + tagProcessor.registerTag(ElementTag.class, "type", (attribute, object) -> { + return new ElementTag(object.getTypeString(), true); + }); + + // <--[tag] + // @attribute + // @returns ScriptTag + // @description + // Returns the particle script a particle was created from, if any. + // --> + tagProcessor.registerTag(ScriptTag.class, "script", (attribute, object) -> { + if (object.particle instanceof ParticleScriptContainer.ClientizenParticle clientizenParticle) { + return new ScriptTag(clientizenParticle.particleScript); + } + return null; + }); + + // <--[tag] + // @attribute + // @returns LocationTag + // @mechanism ParticleTag.location + // @description + // Returns the particle's location. + // @example + // # Use to move the particle 5 blocks up. + // - adjust <[particle]> location:<[particle].location.above[5]> + // --> + tagProcessor.registerTag(LocationTag.class, "location", (attribute, object) -> { + ParticleAccessor particle = object.getAccessor(); + return new LocationTag(particle.getX(), particle.getY(), particle.getZ()); + }); + + // <--[mechanism] + // @object ParticleTag + // @name location + // @input LocationTag + // @description + // Sets the particle's location. + // @tags + // + // @example + // # Use to move the particle 5 blocks up. + // - adjust <[particle]> location:<[particle].location.above[5]> + // --> + tagProcessor.registerMechanism("location", false, LocationTag.class, (object, mechanism, input) -> { + object.particle.setPos(input.getX(), input.getY(), input.getZ()); + }); + + // <--[tag] + // @attribute + // @returns LocationTag + // @mechanism ParticleTag.velocity + // @description + // Returns the particle's velocity as a LocationTag vector. + // @example + // # Use to check whether the particle is going upwards. + // - if <[particle].velocity.y> > 0: + // - narrate "The particle is heading upwards." + // --> + tagProcessor.registerTag(LocationTag.class, "velocity", (attribute, object) -> { + ParticleAccessor particle = object.getAccessor(); + return new LocationTag(particle.getVelocityX(), particle.getVelocityY(), particle.getVelocityZ()); + }); + + // <--[mechanism] + // @object ParticleTag + // @name velocity + // @input LocationTag + // @description + // Sets the particle's velocity to the given LocationTag vector. + // @tags + // + // @example + // # Use to make the particle move upwards. + // - adjust <[particle]> velocity:0,1,0 + // --> + tagProcessor.registerMechanism("velocity", false, LocationTag.class, (object, mechanism, input) -> { + object.particle.setVelocity(input.getX(), input.getY(), input.getZ()); + }); + + // <--[tag] + // @attribute + // @returns ElementTag + // @mechanism ParticleTag.texture + // @description + // Returns the particle's current texture as a namespaced key, if it's of a type that has textures. + // Note that the texture id is within the particle texture atlas, see <@link Texture Atlases> for more information. + // --> + tagProcessor.registerTag(ElementTag.class, "texture", (attribute, object) -> { + if (object.particle instanceof SpriteBillboardParticleAccessor spriteParticle) { + return new ElementTag(Utilities.idToString(spriteParticle.getSprite().getContents().getId()), true); + } + return null; + }); + + // <--[mechanism] + // @object ParticleTag + // @name texture + // @input ElementTag + // @description + // Sets the particle's texture, if it's of a type that allows them. + // The input is a namespaced key of the texture within the particle texture atlas, see <@link Texture Atlases> for more information. + // @tags + // + // --> + tagProcessor.registerMechanism("texture", false, ElementTag.class, (object, mechanism, input) -> { + if (!(object.particle instanceof SpriteBillboardParticleAccessor spriteParticle)) { + mechanism.echoError("Cannot set texture: particles of type '" + object.getTypeString() + "' don't support textures."); + return; + } + Identifier texture = Identifier.tryParse(input.asString()); + if (texture == null) { + mechanism.echoError("Invalid texture id specified: " + input + '.'); + return; + } + Sprite sprite = getParticleAtlas().getSprite(texture); + if (sprite == null) { + mechanism.echoError("Texture id '" + input + "' is valid, but doesn't match any texture."); + return; + } + spriteParticle.invokeSetSprite(sprite); + }); + + // <--[tag] + // @attribute + // @returns ColorTag + // @mechanism ParticleTag.color + // @description + // Returns the particle's color. + // Usually applied on top of a particle's existing texture, either coloring it or overriding it (while keeping the shape). + // --> + tagProcessor.registerTag(ColorTag.class, "color", (attribute, object) -> { + ParticleAccessor particle = object.getAccessor(); + return new ColorTag((int) (particle.getRed() * 255f), (int) (particle.getGreen() * 255f), (int) (particle.getBlue() * 255f), (int) (particle.getAlpha() * 255f)); + }); + + // <--[mechanism] + // @object ParticleTag + // @name color + // @input ColorTag + // @description + // Sets the particle's color. + // Usually applied on top of a particle's existing texture, either coloring it or overriding it (while keeping the shape). + // Note that alpha values can be set, but only visually apply to some particles. + // @tags + // + // --> + tagProcessor.registerMechanism("color", false, ColorTag.class, (object, mechanism, input) -> { + object.particle.setColor(input.red / 255f, input.green / 255f, input.blue / 255f); + object.getAccessor().invokeSetAlpha(input.alpha / 255f); + }); + + // <--[tag] + // @attribute + // @returns ElementTag(Boolean) + // @mechanism ParticleTag.world_collision + // @description + // Returns whether the particle will collide with the world. + // --> + tagProcessor.registerTag(ElementTag.class, "world_collision", (attribute, object) -> { + return new ElementTag(object.getAccessor().collidesWithWorld()); + }); + + // <--[mechanism] + // @object ParticleTag + // @name world_collision + // @input ElementTag(Boolean) + // @description + // Sets whether the particle will collide the with the world. + // @tags + // + // --> + tagProcessor.registerMechanism("world_collision", false, ElementTag.class, (object, mechanism, input) -> { + if (mechanism.requireBoolean()) { + object.getAccessor().setCollidesWithWorld(input.asBoolean()); + } + }); + + // <--[tag] + // @attribute + // @returns DurationTag + // @mechanism ParticleTag.time_lived + // @description + // Returns how long the particle's existed for. + // @example + // # Use to check if the particle's existed for at least 10 seconds. + // - if <[particle].time_lived.is_more_than[10s]>: + // - narrate "The particle's existed for more than 10 seconds." + // --> + tagProcessor.registerTag(DurationTag.class, "time_lived", (attribute, object) -> { + return new DurationTag((long) object.getAccessor().getAge()); + }); + + // <--[mechanism] + // @object ParticleTag + // @name time_lived + // @input DurationTag + // @description + // Sets the amount of time the particle's existed for. + // Generally shouldn't be needed, but may be useful in some specific cases. + // See <@link mechanism ParticleTag.time_to_live> for setting the amount of time the particle should exist for. + // @tags + // + // @example + // # Use to make it as if the particle just spawned in. + // - adjust <[particle]> time_lived:0 + // --> + tagProcessor.registerMechanism("time_lived", false, DurationTag.class, (object, mechanism, input) -> { + object.getAccessor().setAge(input.getTicksAsInt()); + }); + + // <--[tag] + // @attribute + // @returns DurationTag + // @mechanism ParticleTag.time_to_live + // @description + // Returns the amount of time the particle should exist for. + // Note that this is the total amount of time it should exist for after spawning, the return value doesn't change with time passing. + // See <@link tag ParticleTag.time_lived> for the amount of time the particle's existed. + // @example + // # Use to check how much time is left before the particle despawns. + // - narrate "The particle will despawn in <[particle].time_to_live.sub[<[particle].time_lived>].formatted>." + // --> + tagProcessor.registerTag(DurationTag.class, "time_to_live", (attribute, object) -> { + return new DurationTag((long) object.particle.getMaxAge()); + }); + + // <--[mechanism] + // @object ParticleTag + // @name time_to_live + // @input DurationTag + // @description + // Sets the amount of time the particle should exist for. + // Note that this is the total amount of time it should exist for after spawning, not relative to the amount of time it's already existed for. + // @tags + // + // @example + // # Use to make it so the particle will exist for 10 seconds (assuming it just spawned). + // - adjust <[particle]> time_to_live:10s + // --> + tagProcessor.registerMechanism("time_to_live", false, DurationTag.class, (object, mechanism, input) -> { + object.particle.setMaxAge(input.getTicksAsInt()); + }); + + // <--[tag] + // @attribute + // @returns ElementTag(Decimal) + // @mechanism ParticleTag.scale + // @description + // Returns the particle's scale, if it's of a type that supports scaling. + // Note that some particles may do additional processing on their base scale, which can be overriden using the mechanism. + // @example + // # Use to make a particle twice as large. + // - adjust <[particle]> scale:<[particle].scale.mul[2]> + // --> + tagProcessor.registerTag(ElementTag.class, "scale", (attribute, object) -> { + if (object.particle instanceof BillboardParticleMixinAccess billboardParticle) { + return new ElementTag(billboardParticle.clientizen$getScale()); + } + return null; + }); + + // <--[mechanism] + // @object ParticleTag + // @name scale + // @input ElementTag(Decimal) + // @description + // Sets the particle's scale, if it's of a type that supports scaling. + // Note that this overrides the particle's normal scale calculations (some particles rescale based on their time lived, for example) with a hard-coded scale, + // see <@link mechanism ParticleTag.multiply_scale> to modify the vanilla scaling instead of overriding it. + // And see <@link mechanism ParticleTag.reset_scale> to go back to the default vanilla scaling. + // @tags + // + // @example + // # Use to make a particle twice as large. + // - adjust <[particle]> scale:<[particle].scale.mul[2]> + // --> + tagProcessor.registerMechanism("scale", false, ElementTag.class, (object, mechanism, input) -> { + if (!(object.particle instanceof BillboardParticleMixinAccess billboardParticle)) { + mechanism.echoError("Cannot set scale: particles of type '" + object.getTypeString() + "' don't support scaling."); + return; + } + if (mechanism.requireFloat()) { + billboardParticle.clientizen$setScale(mechanism.getValue().asFloat()); + } + }); + + // <--[mechanism] + // @object ParticleTag + // @name reset_scale + // @input None + // @description + // Resets a scale override from <@link mechanism ParticleTag.scale> back to the particle's original vanilla scaling. + // --> + tagProcessor.registerMechanism("reset_scale", false, (object, mechanism) -> { + if (!(object.particle instanceof BillboardParticleMixinAccess billboardParticle)) { + mechanism.echoError("Cannot reset scale: particles of type '" + object.getTypeString() + "' don't support scaling."); + return; + } + billboardParticle.clientizen$setScale(null); + }); + + // <--[tag] + // @attribute + // @returns ElementTag(Boolean) + // @description + // Returns whether the particle's on the ground or in the air. + // See <@link tag ParticleTag.world_collision> for whether the particle collides with the ground. + // @example + // # Use to make the particle blue if it's on the ground. + // - if <[particle].on_ground>: + // - adjust <[particle]> color:blue + // --> + tagProcessor.registerTag(ElementTag.class, "on_ground", (attribute, object) -> { + return new ElementTag(object.getAccessor().isOnGround()); + }); + + // <--[mechanism] + // @object ParticleTag + // @name randomize_texture + // @input None + // @description + // Applies a random texture from the particle's texture list, if it's of a type that supports textures. + // See <@link mechanism ParticleTag.texture> for setting a specific texture. + // --> + tagProcessor.registerMechanism("randomize_texture", false, (object, mechanism) -> { + if (!(object.particle instanceof SpriteBillboardParticle spriteParticle)) { + mechanism.echoError("Cannot randomize texture: particles of type '" + object.getTypeString() + "' don't have textures."); + return; + } + ParticleManager.SimpleSpriteProvider spriteProvider = getSpriteProviders().get(object.getTypeId()); + spriteParticle.setSprite(spriteProvider); + }); + + // <--[mechanism] + // @object ParticleTag + // @name update_age_texture + // @input None + // @description + // Applies a texture from the particle's texture list based on the time it's lived. + // For example: a particle with 4 textures that exists for 20 seconds will apply a different texture every 5 seconds, when this mechanism is used. + // See <@link mechanism ParticleTag.texture> for setting a specific texture. + // --> + tagProcessor.registerMechanism("update_age_texture", false, (object, mechanism) -> { + if (!(object.particle instanceof SpriteBillboardParticle spriteParticle)) { + mechanism.echoError("Cannot update texture for age: particles of type '" + object.getTypeString() + "' don't have textures."); + return; + } + ParticleManager.SimpleSpriteProvider spriteProvider = getSpriteProviders().get(object.getTypeId()); + spriteParticle.setSpriteForAge(spriteProvider); + }); + + // <--[mechanism] + // @object ParticleTag + // @name multiply_scale + // @input ElementTag(Decimal) + // @description + // Multiplies the particle's current vanilla scale by the input amount, without overriding it. + // See <@link mechanism ParticleTag.scale> for overriding the vanilla scaling with a specific scale. + // @example + // # Use to make a particle twice as big without overriding its built-in scaling logic. + // - adjust <[particle]> multiply_scale:2 + // --> + tagProcessor.registerMechanism("multiply_scale", false, ElementTag.class, (object, mechanism, input) -> { + if (mechanism.requireFloat()) { + object.particle.scale(input.asFloat()); + } + }); + + // <--[mechanism] + // @object ParticleTag + // @name remove + // @input None + // @description + // Removes a particle from the world. + // --> + tagProcessor.registerMechanism("remove", false, (object, mechanism) -> { + object.particle.markDead(); + }); + } + + public static final ObjectTagProcessor tagProcessor = new ObjectTagProcessor<>(); + + @Override + public ObjectTag getObjectAttribute(Attribute attribute) { + return tagProcessor.getObjectAttribute(this, attribute); + } + + @Override + public void adjust(Mechanism mechanism) { + tagProcessor.processMechanism(this, mechanism); + } + + @Override + public void applyProperty(Mechanism mechanism) { + mechanism.echoError("Cannot apply properties to a ParticleTag."); + } + + @Override + public AbstractFlagTracker getFlagTracker() { + return getMixinAccess().clientizen$getFlagTracker(); + } + + @Override + public void reapplyTracker(AbstractFlagTracker tracker) {} + + @Override + public boolean isUnique() { + return true; + } + + @Override + public String identify() { + return "particle@" + getMixinAccess().clientizen$getUUID(); + } + + @Override + public String identifySimple() { + return identify(); + } + + @Override + public String debuggable() { + return "particle@" + getMixinAccess().clientizen$getUUID() + " (" + getTypeString() + ")"; + } + + @Override + public String toString() { + return identify(); + } + + @Override + public boolean advancedMatches(String matcher) { + return ScriptEvent.createMatcher(matcher).doesMatch( + particle instanceof ParticleScriptContainer.ClientizenParticle clientizenParticle ? clientizenParticle.particleScript.getName() : getTypeString(), text -> { + return switch (text) { + case "particle" -> true; + case "script" -> particle instanceof ParticleScriptContainer.ClientizenParticle; + default -> false; + }; + } + ); + } + + String prefix; + + @Override + public String getPrefix() { + return prefix; + } + + @Override + public ObjectTag setPrefix(String prefix) { + this.prefix = prefix; + return this; + } +} diff --git a/src/main/java/com/denizenscript/clientizen/scripts/commands/ParticleCommand.java b/src/main/java/com/denizenscript/clientizen/scripts/commands/ParticleCommand.java index 70054cb..644b806 100644 --- a/src/main/java/com/denizenscript/clientizen/scripts/commands/ParticleCommand.java +++ b/src/main/java/com/denizenscript/clientizen/scripts/commands/ParticleCommand.java @@ -1,12 +1,10 @@ package com.denizenscript.clientizen.scripts.commands; -import com.denizenscript.clientizen.mixin.ParticleAccessor; +import com.denizenscript.clientizen.Clientizen; import com.denizenscript.clientizen.mixin.WorldRendererAccessor; -import com.denizenscript.clientizen.objects.EntityTag; -import com.denizenscript.clientizen.objects.ItemTag; -import com.denizenscript.clientizen.objects.LocationTag; -import com.denizenscript.clientizen.objects.MaterialTag; -import com.denizenscript.clientizen.util.Utilities; +import com.denizenscript.clientizen.mixin.particle.ParticleAccessor; +import com.denizenscript.clientizen.objects.*; +import com.denizenscript.clientizen.scripts.containers.ParticleScriptContainer; import com.denizenscript.denizencore.exceptions.InvalidArgumentsRuntimeException; import com.denizenscript.denizencore.objects.ObjectTag; import com.denizenscript.denizencore.objects.core.ColorTag; @@ -14,6 +12,7 @@ import com.denizenscript.denizencore.objects.core.ElementTag; import com.denizenscript.denizencore.objects.core.MapTag; import com.denizenscript.denizencore.scripts.ScriptEntry; +import com.denizenscript.denizencore.scripts.ScriptRegistry; import com.denizenscript.denizencore.scripts.commands.AbstractCommand; import com.denizenscript.denizencore.scripts.commands.generator.ArgDefaultNull; import com.denizenscript.denizencore.scripts.commands.generator.ArgName; @@ -49,7 +48,7 @@ public class ParticleCommand extends AbstractCommand { // // @Description // Spawns a particle of the specified type in the world. - // The type can be any particle type, including ones added by other mods - see <@link url https://minecraft.wiki/w/Particles_(Java_Edition)#Types_of_particles> for all vanilla particle types. + // The type can be any particle type/particle script, including ones added by other mods - see <@link url https://minecraft.wiki/w/Particles_(Java_Edition)#Types_of_particles> for all vanilla particle types. // The location can be any location to play the particle at. // The velocity is a vector location for the particle's movement, which overrides its default movement (if any). // The color will override the particle's color or color its texture (depending on the particle), and can be any color. @@ -82,7 +81,7 @@ public class ParticleCommand extends AbstractCommand { // - <@link ObjectType DurationTag> "delay" key, for the amount of time the particle should wait before spawning. // // @Tags - // None + // returns a <@link ObjectType ParticleTag> of the particle that was spawned in. // // @Usage // Use to spawn a large flame particle above the player. @@ -96,6 +95,10 @@ public class ParticleCommand extends AbstractCommand { // Use to spawn a block marker particle of a stone block that slowly moves upwards. // - particle type:block_marker at:<[location]> data:[material=stone] velocity:0,0.1,0 // + // @Usage + // Use to spawn a particle from a particle script at the player's location. + // - particle type:my_particle_script at: + // // --> public ParticleCommand() { @@ -107,7 +110,8 @@ public ParticleCommand() { @Override public void addCustomTabCompletions(TabCompletionsBuilder tab) { - tab.addWithPrefix("type:", Utilities.listRegistryKeys(Registries.PARTICLE_TYPE)); + tab.addWithPrefix("type:", Registries.PARTICLE_TYPE.getIds().stream() + .map(id -> id.getNamespace().equals(Identifier.DEFAULT_NAMESPACE) || id.getNamespace().equals(Clientizen.ID) ? id.getPath() : id.toString()).toList()); } public static void autoExecute(ScriptEntry scriptEntry, @@ -121,8 +125,12 @@ public static void autoExecute(ScriptEntry scriptEntry, @ArgName("raw_data") @ArgPrefixed @ArgDefaultNull String rawData) { ParticleType type = Registries.PARTICLE_TYPE.get(Identifier.tryParse(particleName)); if (type == null) { - Debug.echoError("Invalid particle type specified: " + particleName + '.'); - return; + ParticleScriptContainer particleScript = ScriptRegistry.getScriptContainerAs(particleName, ParticleScriptContainer.class); + if (particleScript == null) { + Debug.echoError("Invalid particle type specified: " + particleName + '.'); + return; + } + type = Registries.PARTICLE_TYPE.get(particleScript.getId()); } ParticleEffect particle; if (rawData != null) { @@ -209,6 +217,7 @@ else if (type == ParticleTypes.SHRIEK) { } createdParticle.scale(scaleMultiplier.asFloat()); } + scriptEntry.saveObject("created_particle", new ParticleTag(createdParticle)); } private static Vector3f colorToVector(ColorTag color) { diff --git a/src/main/java/com/denizenscript/clientizen/scripts/containers/ClientizenContainerRegistry.java b/src/main/java/com/denizenscript/clientizen/scripts/containers/ClientizenContainerRegistry.java index 22fd296..8d37b84 100644 --- a/src/main/java/com/denizenscript/clientizen/scripts/containers/ClientizenContainerRegistry.java +++ b/src/main/java/com/denizenscript/clientizen/scripts/containers/ClientizenContainerRegistry.java @@ -7,5 +7,6 @@ public class ClientizenContainerRegistry { public static void registerContainers() { ScriptRegistry._registerType("gui", GuiScriptContainer.class); + ScriptRegistry._registerType("particle", ParticleScriptContainer.class); } } diff --git a/src/main/java/com/denizenscript/clientizen/scripts/containers/ParticleScriptContainer.java b/src/main/java/com/denizenscript/clientizen/scripts/containers/ParticleScriptContainer.java new file mode 100644 index 0000000..c85a6b8 --- /dev/null +++ b/src/main/java/com/denizenscript/clientizen/scripts/containers/ParticleScriptContainer.java @@ -0,0 +1,236 @@ +package com.denizenscript.clientizen.scripts.containers; + +import com.denizenscript.clientizen.Clientizen; +import com.denizenscript.clientizen.access.RegistryMixinAccess; +import com.denizenscript.clientizen.objects.ParticleTag; +import com.denizenscript.clientizen.scripts.containers.gui.GuiScriptContainer; +import com.denizenscript.denizencore.DenizenCore; +import com.denizenscript.denizencore.objects.Mechanism; +import com.denizenscript.denizencore.objects.ObjectTag; +import com.denizenscript.denizencore.objects.core.DurationTag; +import com.denizenscript.denizencore.scripts.ScriptEntry; +import com.denizenscript.denizencore.scripts.containers.ScriptContainer; +import com.denizenscript.denizencore.scripts.queues.ContextSource; +import com.denizenscript.denizencore.tags.TagContext; +import com.denizenscript.denizencore.utilities.CoreUtilities; +import com.denizenscript.denizencore.utilities.ScriptUtilities; +import com.denizenscript.denizencore.utilities.YamlConfiguration; +import com.denizenscript.denizencore.utilities.debugging.Debug; +import com.denizenscript.denizencore.utilities.text.StringHolder; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; +import net.fabricmc.fabric.api.client.particle.v1.ParticleFactoryRegistry; +import net.fabricmc.fabric.api.particle.v1.FabricParticleTypes; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.particle.*; +import net.minecraft.client.texture.Sprite; +import net.minecraft.client.texture.SpriteAtlasTexture; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.particle.ParticleType; +import net.minecraft.particle.SimpleParticleType; +import net.minecraft.registry.Registries; +import net.minecraft.registry.Registry; +import net.minecraft.util.Identifier; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class ParticleScriptContainer extends ScriptContainer { + + // <--[language] + // @name Texture Atlases + // @group Client Information + // @description + // A texture atlas is a group of textures for a specific use case. + // They are usually a folder under "assets//textures/" (see <@link url https://minecraft.wiki/w/Resource_pack#Folder_structure>), and contain textures. + // As each atlas is for a specific purpose and the client knows what a texture is for (E.g. when setting a texture on a particle it knows to look within the particle atlas), + // they can be referenced in code using just the namespace and texture name. + // So for example, "assets/my_server_pack/textures/particle/water_drop.png" can be referenced in a particle script as "my_server_pack:water_drop". + // --> + + // <--[language] + // @name Particle Script Containers + // @group Script Container System + // @description + // Particle script containers allow you to add your own custom particles, optionally setting their behaviour. + // They can be played using the <@link command particle> command by specifying the script name, see it's usage examples/meta. + // + // + // Particle_Script_Name: + // type: particle + + // # The particle's texture list. + // # Can be a single texture to use, or a list of textures which will be picked from randomly when the particle is spawned in. + // # This is the texture list used for features like <@link mechanism ParticleTag.randomize_texture> and <@link mechanism ParticleTag.update_age_texture>. + // # Note that the textures must be within the particle texture atlas, see <@link language Texture Atlases> for more information. + // # | All particle scripts MUST have this key! + // textures: + // - : + // + // # Mechanisms to apply to the particle when it's spawned in. + // # | Some particle scripts should have this key. + // mechanisms: + // # Examples of mechanisms being used, any valid ParticleTag mechanism can be specified. + // + // # | Do not copy this line, it is only an example. + // color: red/blue/green/... + // + // # | Do not copy this line, it is only an example. + // velocity: 1,0.5,1 + // + // # The rate at which the particle's update code should run, as a <@link ObjectType DurationTag> - defaults to running every tick. + // # | Some particle scripts should have this key. + // update_rate: 1s + // + // # The particle's update code; runs every tick by default, or based on the update rate if specified. + // # Provides : a <@link ObjectType ParticleTag> of the particle from this particle script that's being updated. + // # | Some particle scripts should have this key. + // update: + // # Use to make a particle red once it hits the ground + // - if : + // - adjust color:red + // + // --> + + public static final List customParticles = new ArrayList<>(); + + public static void clearCustomParticles() { + RegistryMixinAccess particleRegistry = (RegistryMixinAccess) Registries.PARTICLE_TYPE; + particleRegistry.clientizen$unfreeze(); + for (ParticleScriptContainer particleScript : customParticles) { + particleRegistry.clientizen$remove(particleScript.getId()); + } + } + + public static void registerCustomParticles() { + if (MinecraftClient.getInstance().particleManager == null) { + ClientLifecycleEvents.CLIENT_STARTED.register(client -> ParticleScriptContainer.registerCustomParticles()); + return; + } + Map spritesMap = ParticleTag.getSpriteProviders(); + for (ParticleScriptContainer particleScript : customParticles) { + SimpleParticleType type = FabricParticleTypes.simple(); + Identifier particleId = particleScript.getId(); + Registry.register(Registries.PARTICLE_TYPE, particleId, type); + ParticleFactoryRegistry.getInstance().register(type, spriteProvider -> new ClientizenParticle.Factory(spriteProvider, particleScript)); + spritesMap.get(particleId).setSprites(particleScript.textures); + } + Registries.PARTICLE_TYPE.freeze(); + } + + public List textures; + public final List updateScript; + public List mechanisms; + public long updateRate; + + public ParticleScriptContainer(YamlConfiguration configurationSection, String scriptContainerName) { + super(configurationSection, scriptContainerName); + Debug.pushErrorContext(this); + List textureInput = getStringList("textures", true); + if (textureInput == null) { + Debug.echoError("Missing required 'textures' key."); + Debug.popErrorContext(); + updateScript = null; + return; + } + SpriteAtlasTexture particlesAtlas = ParticleTag.getParticleAtlas(); + textures = new ArrayList<>(textureInput.size()); + for (String texture : textureInput) { + Identifier textureId = Identifier.tryParse(texture); + if (textureId == null) { + Debug.echoError("Invalid texture id specified: " + texture + '.'); + continue; + } + Sprite sprite = particlesAtlas.getSprite(textureId); + if (sprite == null) { + Debug.echoError("Texture id '" + texture + "' is valid, but a texture with that id cannot be found."); + continue; + } + textures.add(sprite); + } + updateScript = getEntries(DenizenCore.implementation.getEmptyScriptEntryData(), "update"); + TagContext scriptContext = DenizenCore.implementation.getTagContext(this); + DurationTag rateDuration = GuiScriptContainer.getTaggedObject(DurationTag.class, configurationSection, "update_rate", scriptContext); + if (rateDuration != null) { + updateRate = rateDuration.getMillis(); + } + YamlConfiguration mechanismsSection = getConfigurationSection("mechanisms"); + if (mechanismsSection != null) { + mechanisms = new ArrayList<>(mechanismsSection.contents.size()); + for (Map.Entry entry : mechanismsSection.contents.entrySet()) { + ObjectTag value = CoreUtilities.objectToTagForm(entry.getValue(), scriptContext, true, true, true); + mechanisms.add(new Mechanism(entry.getKey().low, value, scriptContext)); + } + } + customParticles.add(this); + Debug.popErrorContext(); + } + + public Identifier getId() { + return Clientizen.id(CoreUtilities.toLowerCase(getName())); + } + + public static class ClientizenParticle extends SpriteBillboardParticle { + + SpriteProvider spriteProvider; + public ParticleScriptContainer particleScript; + ContextSource.SimpleMap scriptContext; + long lastUpdateTime; + + protected ClientizenParticle(ClientWorld world, double x, double y, double z, double velocityX, double velocityY, double velocityZ, + SpriteProvider spriteProvider, ParticleScriptContainer particleScript, ParticleType particleType) { + super(world, x, y, z, velocityX, velocityY, velocityZ); + // Minecraft randomizes some values, don't want that for Clientizen particles + this.velocityX = velocityX; + this.velocityY = velocityY; + this.velocityZ = velocityZ; + this.scale = 0.5f; + this.spriteProvider = spriteProvider; + this.particleScript = particleScript; + this.scriptContext = new ContextSource.SimpleMap(); + setSprite(spriteProvider); + ParticleTag particleTag = new ParticleTag(this); + // Specifically set the type early, as we apply mechanisms + particleTag.getMixinAccess().clientizen$setType(particleType); + scriptContext.contexts = Map.of("particle", particleTag); + if (particleScript.mechanisms != null) { + particleScript.mechanisms.forEach(particleTag::safeAdjust); + } + } + + @Override + public void tick() { + if (age++ >= maxAge) { + markDead(); + return; + } + prevPosX = x; + prevPosY = y; + prevPosZ = z; + prevAngle = angle; + move(this.velocityX, this.velocityY, this.velocityZ); + if (particleScript.updateScript == null) { + return; + } + long currentTime = DenizenCore.currentTimeMillis; + if (currentTime - lastUpdateTime < particleScript.updateRate) { + return; + } + lastUpdateTime = currentTime; + ScriptUtilities.createAndStartQueueArbitrary(particleScript.getName() + "_UPDATE", particleScript.updateScript, null, scriptContext, null); + } + + @Override + public ParticleTextureSheet getType() { + return ParticleTextureSheet.PARTICLE_SHEET_TRANSLUCENT; + } + + public record Factory(SpriteProvider spriteProvider, ParticleScriptContainer particleScript) implements ParticleFactory { + + @Override + public Particle createParticle(SimpleParticleType parameters, ClientWorld world, double x, double y, double z, double velocityX, double velocityY, double velocityZ) { + return new ClientizenParticle(world, x, y, z, velocityX, velocityY, velocityZ, spriteProvider, particleScript, parameters); + } + } + } +} diff --git a/src/main/java/com/denizenscript/clientizen/util/impl/DenizenCoreImpl.java b/src/main/java/com/denizenscript/clientizen/util/impl/DenizenCoreImpl.java index eb1fc7f..c99412c 100644 --- a/src/main/java/com/denizenscript/clientizen/util/impl/DenizenCoreImpl.java +++ b/src/main/java/com/denizenscript/clientizen/util/impl/DenizenCoreImpl.java @@ -3,6 +3,7 @@ import com.denizenscript.clientizen.Clientizen; import com.denizenscript.clientizen.debuggui.DebugConsole; import com.denizenscript.clientizen.objects.LocationTag; +import com.denizenscript.clientizen.scripts.containers.ParticleScriptContainer; import com.denizenscript.clientizen.tags.ClientTagBase; import com.denizenscript.clientizen.tags.ClientizenTagContext; import com.denizenscript.denizencore.DenizenImplementation; @@ -40,10 +41,14 @@ public String getImplementationName() { } @Override - public void preScriptReload() {} + public void preScriptReload() { + ParticleScriptContainer.clearCustomParticles(); + } @Override - public void onScriptReload() {} + public void onScriptReload() { + ParticleScriptContainer.registerCustomParticles(); + } @Override public String queueHeaderInfo(ScriptEntry scriptEntry) { @@ -61,7 +66,9 @@ public boolean handleCustomArgs(ScriptEntry scriptEntry, Argument argument) { } @Override - public void refreshScriptContainers() {} + public void refreshScriptContainers() { + ParticleScriptContainer.customParticles.clear(); + } @Override public TagContext getTagContext(ScriptContainer scriptContainer) { diff --git a/src/main/resources/clientizen.accesswidener b/src/main/resources/clientizen.accesswidener index 768b8c6..60d135a 100644 --- a/src/main/resources/clientizen.accesswidener +++ b/src/main/resources/clientizen.accesswidener @@ -1,3 +1,4 @@ accessWidener v2 named accessible class net/minecraft/client/gui/screen/pack/ExperimentalWarningScreen$DetailsScreen +accessible class net/minecraft/client/particle/ParticleManager$SimpleSpriteProvider diff --git a/src/main/resources/clientizen.mixins.json b/src/main/resources/clientizen.mixins.json index f3e9e5f..307d732 100644 --- a/src/main/resources/clientizen.mixins.json +++ b/src/main/resources/clientizen.mixins.json @@ -7,19 +7,25 @@ "client": [ "ClientPlayNetworkHandlerMixin", "ClientWorldAccessor", - "EntityRenderedMixin", "ClientWorldMixin", + "EntityRenderedMixin", "EventKeyBindingMixin", "IntPropertyAccessor", "LivingEntityMixin", "MinecraftClientMixin", + "RegistryMixin", "SneakingKeyBindingMixin", "StickyKeyBindingAccessor", - "WorldRendererMixin", "WorldRendererAccessor", - "ParticleAccessor", + "WorldRendererMixin", "gui.WScrollPanelAccessor", - "gui.WTextAccessor" + "gui.WTextAccessor", + "particle.BillboardParticleMixin", + "particle.ParticleAccessor", + "particle.ParticleManagerAccessor", + "particle.ParticleManagerMixin", + "particle.ParticleMixin", + "particle.SpriteBillboardParticleAccessor" ], "injectors": { "defaultRequire": 1