package carpet.script.utils; import carpet.CarpetSettings; import carpet.network.ServerNetworkHandler; import carpet.script.CarpetContext; import carpet.script.Expression; import carpet.script.exception.InternalExpressionException; import carpet.script.value.BlockValue; import carpet.script.value.EntityValue; import carpet.script.value.ListValue; import carpet.script.value.NumericValue; import carpet.script.value.StringValue; import carpet.script.value.Value; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import com.mojang.brigadier.StringReader; import com.mojang.brigadier.exceptions.CommandSyntaxException; import net.minecraft.command.arguments.ParticleArgumentType; import net.minecraft.entity.Entity; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.FloatTag; import net.minecraft.nbt.IntTag; import net.minecraft.nbt.StringTag; import net.minecraft.nbt.Tag; import net.minecraft.particle.ParticleEffect; import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.server.world.ServerWorld; import net.minecraft.util.Identifier; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.MathHelper; import net.minecraft.util.math.Vec3d; import net.minecraft.util.registry.Registry; import net.minecraft.world.dimension.DimensionType; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.Random; import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; import static java.lang.Math.sqrt; public class ShapeDispatcher { private static final Map particleCache = new HashMap<>(); public static void sendShape(List players, ExpiringShape shape, Map params) { Consumer alternative = null; CompoundTag tag = null; for (ServerPlayerEntity player : players) { if (ServerNetworkHandler.validCarpetPlayers.contains(player)) { if (tag == null) tag = ExpiringShape.toTag(params); ServerNetworkHandler.sendCustomCommand(player,"scShape", tag); } else { if (alternative == null) alternative = shape.alternative(); alternative.accept(player); } } } public static ParticleEffect getParticleData(String name) { ParticleEffect particle = particleCache.get(name); if (particle != null) return particle; try { particle = ParticleArgumentType.readParameters(new StringReader(name)); } catch (CommandSyntaxException e) { throw new InternalExpressionException("No such particle: "+name); } particleCache.put(name, particle); return particle; } public static Map parseParams(List items) { // parses params from API function if (items.size()%2 == 1) throw new InternalExpressionException("Shape parameters list needs to be of even size"); Map param = new HashMap<>(); int i = 0; while(i < items.size()) { String name = items.get(i).getString(); Value val = items.get(i+1); param.put(name, val); i += 2; } return param; } public static ExpiringShape create(CarpetContext cc, String shapeType, Map userParams) { userParams.put("shape", new StringValue(shapeType)); userParams.keySet().forEach(key -> { Param param = Param.coders.get(key); if (param==null) throw new InternalExpressionException("Unknown feature for shape: "+key); userParams.put(key, param.validate(cc, userParams.get(key))); }); Function,ExpiringShape> factory = ExpiringShape.shapeProviders.get(shapeType); if (factory == null) throw new InternalExpressionException("Unknown shape: "+shapeType); return factory.apply(userParams); } // client public static ExpiringShape fromTag(CompoundTag tag) { Map options = new HashMap<>(); for (String key : tag.getKeys()) { Function decoder = Param.decoders.get(key); if (decoder==null) { CarpetSettings.LOG.info("Unknown parameter for shape: "+key); return null; } Value decodedValue = decoder.apply(tag.get(key)); options.put(key, decodedValue); } Value shapeValue = options.get("shape"); if (shapeValue == null) { CarpetSettings.LOG.info("Shape id missing in "+ String.join(", ", tag.getKeys())); return null; } Function,ExpiringShape> factory = ExpiringShape.shapeProviders.get(shapeValue.getString()); if (factory == null) { CarpetSettings.LOG.info("Unknown shape: "+shapeValue.getString()); return null; } try { return factory.apply(options); } catch (InternalExpressionException exc) { CarpetSettings.LOG.info(exc.getMessage()); } return null; } public abstract static class ExpiringShape { public static final Map,ExpiringShape>> shapeProviders = new HashMap, ExpiringShape>>(){{ put("line", creator(Line::new)); put("box", creator(Box::new)); put("sphere", creator(Sphere::new)); }}; private static Function,ExpiringShape> creator(Supplier shapeFactory) { return o -> { ExpiringShape shape = shapeFactory.get(); shape.fromOptions(o); return shape; }; } protected float r, g, b, a; protected int color; protected int duration = 0; private int key; protected ExpiringShape() { } public static CompoundTag toTag(Map params) { CompoundTag tag = new CompoundTag(); params.forEach((k, v) -> { Tag valTag = Param.coders.get(k).toTag(v); if (valTag != null) tag.put(k, valTag); }); return tag; } private void fromOptions(Map options) { Set optional = optionalParams(); Set required = requiredParams(); Set all = Sets.union(optional, required); if (!all.containsAll(options.keySet())) throw new InternalExpressionException("Received unexpected parameters for shape: "+Sets.difference(options.keySet(), all)); if (!options.keySet().containsAll(required)) throw new InternalExpressionException("Missing required parameters for shape: "+Sets.difference(required, options.keySet())); options.keySet().forEach(k ->{ if (!Param.coders.get(k).appliesTo(this)) throw new InternalExpressionException("Parameter "+k+" doesn't apply for shape "+options.get("shape").getString()); }); init(options); } protected void init(Map options) { duration = NumericValue.asNumber(options.get("duration")).getInt(); color = NumericValue.asNumber(options.getOrDefault("color", optional.get("color"))).getInt(); this.r = (float)(color >> 24 & 0xFF) / 255.0F; this.g = (float)(color >> 16 & 0xFF) / 255.0F; this.b = (float)(color >> 8 & 0xFF) / 255.0F; this.a = (float)(color & 0xFF) / 255.0F; key = 0; } public int getExpiry() { return duration; } public abstract Consumer alternative(); public int key() { if (key!=0) return key; key = calcKey(); return key; } protected int calcKey() { int hash = 17; hash = 31*hash + color; return hash; } // list of params that need to be there private final Set required = ImmutableSet.of("duration", "shape", "dim"); private final Map optional = ImmutableMap.of( "color", new NumericValue(-1), "player", Value.NULL ); protected Set requiredParams() {return required;} // list of params that can be there, with defaults protected Set optionalParams() {return optional.keySet();} } public static class Box extends ExpiringShape { private final Set required = ImmutableSet.of("from", "to"); private final Map optional = ImmutableMap.of( "width", new NumericValue(2.0), "fill", new NumericValue(0xffffff00) ); @Override protected Set requiredParams() { return Sets.union(super.requiredParams(), required); } @Override protected Set optionalParams() { return Sets.union(super.optionalParams(), optional.keySet()); } public Box() { } float x1, y1, z1; float x2, y2, z2; float lineWidth; protected float fr, fg, fb, fa; protected int fillColor; @Override protected void init(Map options) { super.init(options); List from = ((ListValue)options.get("from")).getItems(); x1 = NumericValue.asNumber(from.get(0)).getFloat(); y1 = NumericValue.asNumber(from.get(1)).getFloat(); z1 = NumericValue.asNumber(from.get(2)).getFloat(); List to = ((ListValue)options.get("to")).getItems(); x2 = NumericValue.asNumber(to.get(0)).getFloat(); y2 = NumericValue.asNumber(to.get(1)).getFloat(); z2 = NumericValue.asNumber(to.get(2)).getFloat(); lineWidth = NumericValue.asNumber(options.getOrDefault("width", optional.get("width"))).getFloat(); fillColor = NumericValue.asNumber(options.getOrDefault("fill", optional.get("fill"))).getInt(); this.fr = (float)(fillColor >> 24 & 0xFF) / 255.0F; this.fg = (float)(fillColor >> 16 & 0xFF) / 255.0F; this.fb = (float)(fillColor >> 8 & 0xFF) / 255.0F; this.fa = (float)(fillColor & 0xFF) / 255.0F; } @Override public Consumer alternative() { String particleName = String.format(Locale.ROOT , "dust %.1f %.1f %.1f 1.0", r, g, b); ParticleEffect particle = getParticleData(particleName); double dx = x1-x2; double dy = y1-y2; double dz = z1-z2; double density = Math.max(2.0, Math.sqrt(dx*dx+dy*dy+dz*dz) /50) / (a+0.1); return p -> mesh(Collections.singletonList(p), particle, density, x1, y1, z1, x2, y2, z2); } @Override public int calcKey() { int hash = super.calcKey(); hash = 31*hash + 1; hash = 31*hash + Float.hashCode(x1); hash = 31*hash + Float.hashCode(y1); hash = 31*hash + Float.hashCode(z1); hash = 31*hash + Float.hashCode(x2); hash = 31*hash + Float.hashCode(y2); hash = 31*hash + Float.hashCode(z2); hash = 31*hash + Float.hashCode(lineWidth); if (fa != 0.0) hash = 31*hash + fillColor; return hash; } public static int mesh(List playerList, ParticleEffect particle, double density, double x1, double y1, double z1, double x2, double y2, double z2 ) { return drawParticleLine(playerList, particle, new Vec3d(x1, y1, z1), new Vec3d(x1, y2, z1), density)+ drawParticleLine(playerList, particle, new Vec3d(x1, y2, z1), new Vec3d(x2, y2, z1), density)+ drawParticleLine(playerList, particle, new Vec3d(x2, y2, z1), new Vec3d(x2, y1, z1), density)+ drawParticleLine(playerList, particle, new Vec3d(x2, y1, z1), new Vec3d(x1, y1, z1), density)+ drawParticleLine(playerList, particle, new Vec3d(x1, y1, z2), new Vec3d(x1, y2, z2), density)+ drawParticleLine(playerList, particle, new Vec3d(x1, y2, z2), new Vec3d(x2, y2, z2), density)+ drawParticleLine(playerList, particle, new Vec3d(x2, y2, z2), new Vec3d(x2, y1, z2), density)+ drawParticleLine(playerList, particle, new Vec3d(x2, y1, z2), new Vec3d(x1, y1, z2), density)+ drawParticleLine(playerList, particle, new Vec3d(x1, y1, z1), new Vec3d(x1, y1, z2), density)+ drawParticleLine(playerList, particle, new Vec3d(x1, y2, z1), new Vec3d(x1, y2, z2), density)+ drawParticleLine(playerList, particle, new Vec3d(x2, y2, z1), new Vec3d(x2, y2, z2), density)+ drawParticleLine(playerList, particle, new Vec3d(x2, y1, z1), new Vec3d(x2, y1, z2), density); } } public static class Line extends ExpiringShape { private final Set required = ImmutableSet.of("from", "to"); private final Map optional = ImmutableMap.of("width", new NumericValue(2.0)); @Override protected Set requiredParams() { return Sets.union(super.requiredParams(), required); } @Override protected Set optionalParams() { return Sets.union(super.optionalParams(), optional.keySet()); } private Line() { super(); } float x1, y1, z1; float x2, y2, z2; float lineWidth; @Override protected void init(Map options) { super.init(options); List from = ((ListValue)options.get("from")).getItems(); x1 = NumericValue.asNumber(from.get(0)).getFloat(); y1 = NumericValue.asNumber(from.get(1)).getFloat(); z1 = NumericValue.asNumber(from.get(2)).getFloat(); List to = ((ListValue)options.get("to")).getItems(); x2 = NumericValue.asNumber(to.get(0)).getFloat(); y2 = NumericValue.asNumber(to.get(1)).getFloat(); z2 = NumericValue.asNumber(to.get(2)).getFloat(); lineWidth = NumericValue.asNumber(options.getOrDefault("width", optional.get("width"))).getFloat(); } @Override public Consumer alternative() { String particleName = String.format(Locale.ROOT , "dust %.1f %.1f %.1f 1.0", r, g, b); ParticleEffect particle = getParticleData(particleName); Vec3d pos1 = new Vec3d(x1, y1, z1); Vec3d pos2 = new Vec3d(x2, y2, z2); double dx = x1-x2; double dy = y1-y2; double dz = z1-z2; double density = Math.max(2.0, Math.sqrt(dx*dx+dy*dy+dz*dz) /50) / (a+0.1); return p -> drawParticleLine(Collections.singletonList(p), particle, pos1, pos2, density); } @Override public int calcKey() { int hash = super.calcKey(); hash = 31*hash + 2; hash = 31*hash + Float.hashCode(x1); hash = 31*hash + Float.hashCode(y1); hash = 31*hash + Float.hashCode(z1); hash = 31*hash + Float.hashCode(x2); hash = 31*hash + Float.hashCode(y2); hash = 31*hash + Float.hashCode(z2); hash = 31*hash + Float.hashCode(lineWidth); return hash; } } public static class Sphere extends ExpiringShape { private final Set required = ImmutableSet.of("center", "radius"); private final Map optional = ImmutableMap.of("width", new NumericValue(2.0), "level", Value.ZERO); @Override protected Set requiredParams() { return Sets.union(super.requiredParams(), required); } @Override protected Set optionalParams() { return Sets.union(super.optionalParams(), optional.keySet()); } private Sphere() { super(); } float cx, cy, cz; float radius; int level; int subdivisions; float lineWidth; @Override protected void init(Map options) { super.init(options); List from = ((ListValue)options.get("center")).getItems(); cx = NumericValue.asNumber(from.get(0)).getFloat(); cy = NumericValue.asNumber(from.get(1)).getFloat(); cz = NumericValue.asNumber(from.get(2)).getFloat(); radius = NumericValue.asNumber(options.get("radius")).getFloat(); level = NumericValue.asNumber(options.getOrDefault("level", optional.get("level"))).getInt(); subdivisions = level; if (subdivisions <= 0) { subdivisions = Math.max(10, (int)(10*Math.sqrt(radius))); } lineWidth = NumericValue.asNumber(options.getOrDefault("width", optional.get("width"))).getFloat(); } @Override public Consumer alternative() { return p -> { String particleName = String.format(Locale.ROOT , "dust %.1f %.1f %.1f 1.0", r, g, b); ParticleEffect particle = getParticleData(particleName); float pihalf = (float)(Math.PI / 180.0f); int partno = Math.min(1000,20*subdivisions); Random rand = p.world.getRandom(); ServerWorld world = p.getServerWorld(); for (int i=0; i> decoders = new HashMap>(){{ put("shape", StringParam::decode); put("dim", StringParam::decode); put("duration", ShapeDispatcher::decodeInt); put("color", ShapeDispatcher::decodeInt); put("from", Vec3Param::decode); put("to", Vec3Param::decode); put("width", ShapeDispatcher::decodeFloat); put("fill", ShapeDispatcher::decodeInt); put("center", Vec3Param::decode); put("radius", ShapeDispatcher::decodeFloat); put("level", ShapeDispatcher::decodeInt); }}; Map coders = new HashMap(){{ put("player", new PlayerParam()); put("shape", new ShapeParam()); put("dim", new DimensionParam()); put("duration", new DurationParam()); put("color", new ColorParam()); put("from", new FromParam()); put("to", new ToParam()); put("width", new WidthParam()); put("fill", new FillColorParam()); put("center", new CenterParam()); put("radius", new RadiusParam()); put("level", new LevelParam()); }}; Tag toTag(Value value); //validates value, returning null if not necessary to keep it and serialize Value validate(CarpetContext cc, Value value); // makes sure the value is proper boolean appliesTo(ExpiringShape shape); // check if applies to the shape String identify(); } public static class PlayerParam implements Param { @Override // doesn't need to be converted public Tag toTag(Value value) { return null; } @Override public Value validate(CarpetContext cc, Value value) { ServerPlayerEntity player = EntityValue.getPlayerByValue(cc.s.getMinecraftServer(), value); if (player == null) throw new InternalExpressionException("'player' parameter needs to represent an existing player"); return new EntityValue(player); } @Override public boolean appliesTo(ExpiringShape shape) { return true; } @Override public String identify() { return "player"; } public static Value decode(Tag tag) { throw new RuntimeException("player tag should not be passed to the client"); } } public abstract static class StringParam implements Param { @Override public Tag toTag(Value value) { return StringTag.of(value.getString()); } @Override public Value validate(CarpetContext cc, Value value) { return value; } public static Value decode(Tag tag) { return new StringValue(tag.asString()); } } public static class DimensionParam extends StringParam { @Override public Value validate(CarpetContext cc, Value value) { String dimStr = value.getString(); Optional dimOp = Registry.DIMENSION_TYPE.getOrEmpty(new Identifier(dimStr)); if (!dimOp.isPresent()) throw new InternalExpressionException("Unknown dimension "+dimStr); return value; } @Override public boolean appliesTo(ExpiringShape shape) { return true; } @Override public String identify() { return "dim"; } } public static class ShapeParam extends StringParam { @Override public Value validate(CarpetContext cc, Value value) { String shape = value.getString(); if (!ExpiringShape.shapeProviders.containsKey(shape)) throw new InternalExpressionException("Unknown shape: "+shape); return value; } @Override public boolean appliesTo(ExpiringShape shape) { return true; } @Override public String identify() { return "shape"; } } public static abstract class NumericParam implements Param { @Override public Value validate(CarpetContext cc, Value value) { if (!(value instanceof NumericValue)) throw new InternalExpressionException("'" + identify() + "' needs to be a number"); return value; } } public static abstract class PositiveParam extends NumericParam { @Override public Value validate(CarpetContext cc, Value value) { Value ret = super.validate(cc, value); if (((NumericValue)ret).getDouble()<=0) throw new InternalExpressionException("'"+identify()+"' should be positive"); return ret; } } public static abstract class PositiveFloatParam extends PositiveParam { @Override public Tag toTag(Value value) { return FloatTag.of(NumericValue.asNumber(value).getFloat()); } } public static abstract class PositiveIntParam extends PositiveParam { @Override public Tag toTag(Value value) { return IntTag.of(NumericValue.asNumber(value).getInt()); } } public static Value decodeInt(Tag tag) { return new NumericValue(((IntTag)tag).getInt()); } public static Value decodeFloat(Tag tag) { return new NumericValue(((FloatTag)tag).getFloat()); } public static class DurationParam extends PositiveIntParam { @Override public boolean appliesTo(ExpiringShape shape) { return true; } @Override public String identify() { return "duration"; } } public static class WidthParam extends PositiveFloatParam { @Override public boolean appliesTo(ExpiringShape shape) { return shape instanceof Box || shape instanceof Line; } @Override public String identify() { return "width"; } } public static abstract class Vec3Param implements Param { public abstract boolean roundsUpForBlocks(); public abstract String identify(); @Override public Value validate(CarpetContext cc, Value value) { if (value instanceof BlockValue) { BlockPos pos = ((BlockValue) value).getPos(); int offset = roundsUpForBlocks()?1:0; return ListValue.of( new NumericValue(pos.getX()+offset), new NumericValue(pos.getY()+offset), new NumericValue(pos.getZ()+offset) ); } if (value instanceof ListValue) { List values = ((ListValue) value).getItems(); if (values.size()!=3) throw new InternalExpressionException("'"+identify()+"' requires 3 numerical values"); for (Value component : values) { if (!(component instanceof NumericValue)) throw new InternalExpressionException("'"+identify()+"' requires 3 numerical values"); } return value; } if (value instanceof EntityValue) { Entity e = ((EntityValue) value).getEntity(); return ListValue.of( new NumericValue(e.getX()), new NumericValue(e.getY()), new NumericValue(e.getZ()) ); } CarpetSettings.LOG.error("Value: "+value.getString()); throw new InternalExpressionException("'"+identify()+"' requires a triple, block or entity to indicate position"); } public static Value decode(Tag tag) { CompoundTag ctag = (CompoundTag)tag; return ListValue.of( new NumericValue(ctag.getFloat("x")), new NumericValue(ctag.getFloat("y")), new NumericValue(ctag.getFloat("z")) ); } @Override public Tag toTag(Value value) { List lv = ((ListValue)value).getItems(); CompoundTag tag = new CompoundTag(); tag.putFloat("x", NumericValue.asNumber(lv.get(0)).getFloat()); tag.putFloat("y", NumericValue.asNumber(lv.get(1)).getFloat()); tag.putFloat("z", NumericValue.asNumber(lv.get(2)).getFloat()); return tag; } } public static class ColorParam extends NumericParam { @Override public Tag toTag(Value value) { return IntTag.of(NumericValue.asNumber(value).getInt()); } @Override public boolean appliesTo(ExpiringShape shape) { return true; } @Override public String identify() { return "color"; } } public static class FillColorParam extends ColorParam { @Override public boolean appliesTo(ExpiringShape shape) { return shape instanceof Box; } @Override public String identify() { return "fill"; } } public static class FromParam extends Vec3Param { @Override public boolean appliesTo(ExpiringShape shape) { return (shape instanceof Line) || (shape instanceof Box); } @Override public boolean roundsUpForBlocks() { return false; } @Override public String identify() { return "from"; } } public static class ToParam extends Vec3Param { @Override public boolean appliesTo(ExpiringShape shape) { return (shape instanceof Line) || (shape instanceof Box); } @Override public boolean roundsUpForBlocks() { return true; } @Override public String identify() { return "to"; } } public static class CenterParam extends Vec3Param { @Override public boolean appliesTo(ExpiringShape shape) { return (shape instanceof Sphere); } @Override public boolean roundsUpForBlocks() { return false; } @Override public String identify() { return "center"; } } public static class RadiusParam extends PositiveFloatParam { @Override public boolean appliesTo(ExpiringShape shape) { return shape instanceof Sphere; } @Override public String identify() { return "radius"; } } public static class LevelParam extends PositiveIntParam { @Override public boolean appliesTo(ExpiringShape shape) { return shape instanceof Sphere; } @Override public String identify() { return "level"; } } private static boolean isStraight(Vec3d from, Vec3d to, double density) { if ( (from.x == to.x && from.y == to.y) || (from.x == to.x && from.z == to.z) || (from.y == to.y && from.z == to.z)) return from.distanceTo(to) / density > 20; return false; } private static int drawOptimizedParticleLine(List playerList, ParticleEffect particle, Vec3d from, Vec3d to, double density) { double distance = from.distanceTo(to); int particles = (int)(distance/density); Vec3d towards = to.subtract(from); int parts = 0; for (ServerPlayerEntity player : playerList) { ServerWorld world = player.getServerWorld(); world.spawnParticles(player, particle, true, (towards.x)/2+from.x, (towards.y)/2+from.y, (towards.z)/2+from.z, particles/3, towards.x/6, towards.y/6, towards.z/6, 0.0); world.spawnParticles(player, particle, true, from.x, from.y, from.z,1,0.0,0.0,0.0,0.0); world.spawnParticles(player, particle, true, to.x, to.y, to.z,1,0.0,0.0,0.0,0.0); parts += particles/3+2; } int divider = 6; while (particles/divider > 1) { int center = (divider*2)/3; int dev = 2*divider; for (ServerPlayerEntity player : playerList) { ServerWorld world = player.getServerWorld(); world.spawnParticles(player, particle, true, (towards.x)/center+from.x, (towards.y)/center+from.y, (towards.z)/center+from.z, particles/divider, towards.x/dev, towards.y/dev, towards.z/dev, 0.0); world.spawnParticles(player, particle, true, (towards.x)*(1.0-1.0/center)+from.x, (towards.y)*(1.0-1.0/center)+from.y, (towards.z)*(1.0-1.0/center)+from.z, particles/divider, towards.x/dev, towards.y/dev, towards.z/dev, 0.0); } parts += 2*particles/divider; divider = 2*divider; } return parts; } public static int drawParticleLine(List players, ParticleEffect particle, Vec3d from, Vec3d to, double density) { if (isStraight(from, to, density)) return drawOptimizedParticleLine(players, particle, from, to, density); double lineLengthSq = from.squaredDistanceTo(to); if (lineLengthSq == 0) return 0; Vec3d incvec = to.subtract(from).multiply(2*density/sqrt(lineLengthSq)); int pcount = 0; for (Vec3d delta = new Vec3d(0.0,0.0,0.0); delta.lengthSquared()