package carpet.script.value; import carpet.script.CarpetContext; import carpet.script.LazyValue; import carpet.script.exception.InternalExpressionException; import com.mojang.brigadier.StringReader; import com.mojang.brigadier.exceptions.CommandSyntaxException; import net.minecraft.block.Block; import net.minecraft.block.BlockState; import net.minecraft.block.ChestBlock; import net.minecraft.block.InventoryProvider; import net.minecraft.block.entity.BlockEntity; import net.minecraft.block.entity.ChestBlockEntity; import net.minecraft.command.arguments.ItemStackArgument; import net.minecraft.command.arguments.ItemStringReader; import net.minecraft.command.arguments.NbtPathArgumentType; import net.minecraft.entity.Entity; import net.minecraft.entity.passive.VillagerEntity; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.inventory.Inventory; import net.minecraft.item.ItemStack; import net.minecraft.nbt.AbstractListTag; import net.minecraft.nbt.AbstractNumberTag; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; import net.minecraft.nbt.StringNbtReader; import net.minecraft.nbt.StringTag; import net.minecraft.nbt.Tag; import net.minecraft.predicate.entity.EntityPredicates; 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.Box; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; public class NBTSerializableValue extends Value implements ContainerValueInterface { private String nbtString = null; private Tag nbtTag = null; private Supplier<Tag> nbtSupplier = null; private boolean owned = false; private NBTSerializableValue() {} public NBTSerializableValue(String nbtString) { nbtSupplier = () -> { try { return (new StringNbtReader(new StringReader(nbtString))).parseTag(); } catch (CommandSyntaxException e) { throw new InternalExpressionException("Incorrect NBT data: "+nbtString); } }; owned = true; } public NBTSerializableValue(Tag tag) { nbtTag = tag; owned = true; } public NBTSerializableValue(Supplier<Tag> tagSupplier) { nbtSupplier = tagSupplier; } public static Value fromStack(ItemStack stack) { if (stack.hasTag()) { NBTSerializableValue value = new NBTSerializableValue(); value.nbtSupplier = stack::getTag; return value; } return Value.NULL; } public static String nameFromRegistryId(Identifier id) { if (id == null) // should be Value.NULL return ""; if (id.getNamespace().equals("minecraft")) return id.getPath(); return id.toString(); } public static NBTSerializableValue parseString(String nbtString) { Tag tag; try { tag = (new StringNbtReader(new StringReader(nbtString))).parseTag(); } catch (CommandSyntaxException e) { throw new InternalExpressionException("Incorrect NBT tag: nbtString"); } NBTSerializableValue value = new NBTSerializableValue(tag); value.nbtString = nbtString; return value; } @Override public Value clone() { // sets only nbttag, even if emtpy; NBTSerializableValue copy = new NBTSerializableValue(nbtTag); copy.nbtSupplier = this.nbtSupplier; copy.nbtString = this.nbtString; copy.owned = this.owned; return copy; } @Override public Value deepcopy() { NBTSerializableValue copy = (NBTSerializableValue) clone(); copy.owned = false; ensureOwnership(); return copy; } // stolen from HopperBlockEntity, adjusted for threaded operation public static Inventory getInventoryAt(ServerWorld world, BlockPos blockPos) { Inventory inventory = null; BlockState blockState = world.getBlockState(blockPos); Block block = blockState.getBlock(); if (block instanceof InventoryProvider) { inventory = ((InventoryProvider)block).getInventory(blockState, world, blockPos); } else if (block.hasBlockEntity()) { BlockEntity blockEntity = BlockValue.getBlockEntity(world, blockPos); if (blockEntity instanceof Inventory) { inventory = (Inventory)blockEntity; if (inventory instanceof ChestBlockEntity && block instanceof ChestBlock) { inventory = ChestBlock.getInventory((ChestBlock)block, blockState, world, blockPos, true); } } } if (inventory == null) { List<Entity> list = world.getEntities( (Entity)null, new Box( blockPos.getX() - 0.5D, blockPos.getY() - 0.5D, blockPos.getZ() - 0.5D, blockPos.getX() + 0.5D, blockPos.getY() + 0.5D, blockPos.getZ() + 0.5D), EntityPredicates.VALID_INVENTORIES ); if (!list.isEmpty()) { inventory = (Inventory)list.get(world.random.nextInt(list.size())); } } return inventory; } public static InventoryLocator locateInventory(CarpetContext c, List<LazyValue> params, int offset) { try { Inventory inv = null; Value v1 = params.get(0 + offset).evalValue(c); if (v1 instanceof EntityValue) { Entity e = ((EntityValue) v1).getEntity(); if (e instanceof PlayerEntity) inv = ((PlayerEntity) e).inventory; else if (e instanceof Inventory) inv = (Inventory) e; else if (e instanceof VillagerEntity) inv = ((VillagerEntity) e).getInventory(); if (inv == null) return null; return new InventoryLocator(e, e.getBlockPos(), inv, offset + 1); } else if (v1 instanceof BlockValue) { BlockPos pos = ((BlockValue) v1).getPos(); if (pos == null) throw new InternalExpressionException("Block to access inventory needs to be positioned in the world"); inv = getInventoryAt(c.s.getWorld(), pos); if (inv == null) return null; return new InventoryLocator(pos, pos, inv, offset + 1); } else if (v1 instanceof ListValue) { List<Value> args = ((ListValue) v1).getItems(); BlockPos pos = new BlockPos( NumericValue.asNumber(args.get(0)).getDouble(), NumericValue.asNumber(args.get(1)).getDouble(), NumericValue.asNumber(args.get(2)).getDouble()); inv = getInventoryAt(c.s.getWorld(), pos); if (inv == null) return null; return new InventoryLocator(pos, pos, inv, offset + 1); } else if (v1 instanceof StringValue) { String strVal = v1.getString().toLowerCase(Locale.ROOT); if (strVal.equals("enderchest")) { Value v2 = params.get(1 + offset).evalValue(c); ServerPlayerEntity player = EntityValue.getPlayerByValue(c.s.getMinecraftServer(), v2); if (player == null) throw new InternalExpressionException("enderchest inventory requires player argument"); return new InventoryLocator(player, player.getBlockPos(), player.getEnderChestInventory(), offset + 2, true); } else { boolean isEnder = strVal.startsWith("enderchest_"); if (isEnder) strVal = strVal.substring(11); // len("enderchest_") ServerPlayerEntity player = c.s.getMinecraftServer().getPlayerManager().getPlayer(strVal); if (player == null) throw new InternalExpressionException("String description of an inventory should either denote a player or player's enderchest"); return new InventoryLocator( player, player.getBlockPos(), isEnder ? player.getEnderChestInventory() : player.inventory, offset + 1, isEnder ); } } BlockPos pos = new BlockPos( NumericValue.asNumber(v1).getDouble(), NumericValue.asNumber(params.get(1 + offset).evalValue(c)).getDouble(), NumericValue.asNumber(params.get(2 + offset).evalValue(c)).getDouble()); inv = getInventoryAt(c.s.getWorld(), pos); if (inv == null) return null; return new InventoryLocator(pos, pos, inv, offset + 3); } catch (IndexOutOfBoundsException e) { throw new InternalExpressionException("Inventory should be defined either by three coordinates, a block value, or an entity"); } } private static Map<String,ItemStackArgument> itemCache = new HashMap<>(); public static ItemStackArgument parseItem(String itemString) { return parseItem(itemString, null); } public static ItemStackArgument parseItem(String itemString, CompoundTag customTag) { try { ItemStackArgument res = itemCache.get(itemString); if (res != null) if (customTag == null) return res; else return new ItemStackArgument(res.getItem(), customTag); ItemStringReader parser = (new ItemStringReader(new StringReader(itemString), false)).consume(); res = new ItemStackArgument(parser.getItem(), parser.getTag()); itemCache.put(itemString, res); if (itemCache.size()>64000) itemCache.clear(); if (customTag == null) return res; else return new ItemStackArgument(res.getItem(), customTag); } catch (CommandSyntaxException e) { throw new InternalExpressionException("Incorrect item: "+itemString); } } public static int validateSlot(int slot, Inventory inv) { int invSize = inv.getInvSize(); if (slot < 0) slot = invSize + slot; if (slot < 0 || slot >= invSize) return inv.getInvSize(); // outside of inventory return slot; } private static Value decodeTag(Tag t) { if (t instanceof CompoundTag) return new NBTSerializableValue(() -> t); if (t instanceof AbstractNumberTag) return new NumericValue(((AbstractNumberTag) t).getDouble()); // more can be done here return new StringValue(t.asString()); } public Value toValue() { return decodeTagDeep(this.getTag()); } public static Value fromValue(Value v) { if (v instanceof NBTSerializableValue) return v; if (v instanceof NullValue) return Value.NULL; return NBTSerializableValue.parseString(v.getString()); } private static Value decodeTagDeep(Tag t) { if (t instanceof CompoundTag) { Map<Value, Value> pairs = new HashMap<>(); CompoundTag ctag = (CompoundTag)t; for (String key: ctag.getKeys()) { pairs.put(new StringValue(key), decodeTagDeep(ctag.get(key))); } return MapValue.wrap(pairs); } if (t instanceof AbstractListTag) { List<Value> elems = new ArrayList<>(); AbstractListTag<? extends Tag> ltag = (AbstractListTag<? extends Tag>)t; for (Tag elem: ltag) { elems.add(decodeTagDeep(elem)); } return ListValue.wrap(elems); } if (t instanceof AbstractNumberTag) return new NumericValue(((AbstractNumberTag) t).getDouble()); // more can be done here return new StringValue(t.asString()); } public Tag getTag() { if (nbtTag == null) nbtTag = nbtSupplier.get(); return nbtTag; } @Override public boolean equals(final Object o) { if (o instanceof NBTSerializableValue) return getTag().equals(((NBTSerializableValue) o).getTag()); return super.equals(o); } @Override public String getString() { if (nbtString == null) nbtString = getTag().toString(); return nbtString; } @Override public boolean getBoolean() { Tag tag = getTag(); if (tag instanceof CompoundTag) return !((CompoundTag) tag).isEmpty(); if (tag instanceof AbstractListTag) return ((AbstractListTag) tag).isEmpty(); if (tag instanceof AbstractNumberTag) return ((AbstractNumberTag) tag).getDouble()!=0.0; if (tag instanceof StringTag) return tag.asString().isEmpty(); return true; } public CompoundTag getCompoundTag() { try { ensureOwnership(); return (CompoundTag) getTag(); } catch (ClassCastException e) { throw new InternalExpressionException(getString()+" is not a valid compound tag"); } } @Override public boolean put(Value where, Value value) { return put(where, value, new StringValue("replace")); } @Override public boolean put(Value where, Value value, Value conditions) { /// WIP ensureOwnership(); NbtPathArgumentType.NbtPath path = cachePath(where.getString()); Tag tagToInsert = value instanceof NBTSerializableValue ? ((NBTSerializableValue) value).getTag() : new NBTSerializableValue(value.getString()).getTag(); boolean modifiedTag; if (conditions instanceof NumericValue) { modifiedTag = modify_insert((int)((NumericValue) conditions).getLong(), path, tagToInsert); } else { String ops = conditions.getString(); if (ops.equalsIgnoreCase("merge")) { modifiedTag = modify_merge(path, tagToInsert); } else if (ops.equalsIgnoreCase("replace")) { modifiedTag = modify_replace(path, tagToInsert); } else { return false; } } return modifiedTag; } private boolean modify_insert(int index, NbtPathArgumentType.NbtPath nbtPath, Tag newElement) { return modify_insert(index, nbtPath, newElement, this.getTag()); } private boolean modify_insert(int index, NbtPathArgumentType.NbtPath nbtPath, Tag newElement, Tag currentTag) { Collection<Tag> targets; try { targets = nbtPath.getOrInit(currentTag, ListTag::new); } catch (CommandSyntaxException e) { return false; } boolean modified = false; for (Tag target : targets) { if (!(target instanceof AbstractListTag)) { continue; } try { AbstractListTag<?> targetList = (AbstractListTag) target; if (!targetList.addTag(index < 0 ? targetList.size() + index + 1 : index, newElement.copy())) return false; modified = true; } catch (IndexOutOfBoundsException ignored) { } } return modified; } private boolean modify_merge(NbtPathArgumentType.NbtPath nbtPath, Tag replacement) //nbtPathArgumentType$NbtPath_1, list_1) { if (!(replacement instanceof CompoundTag)) { return false; } Tag ownTag = getTag(); try { for (Tag target : nbtPath.getOrInit(ownTag, CompoundTag::new)) { if (!(target instanceof CompoundTag)) { continue; } ((CompoundTag) target).copyFrom((CompoundTag) replacement); } } catch (CommandSyntaxException ignored) { return false; } return true; } private boolean modify_replace(NbtPathArgumentType.NbtPath nbtPath, Tag replacement) //nbtPathArgumentType$NbtPath_1, list_1) { Tag tag = getTag(); String pathText = nbtPath.toString(); if (pathText.endsWith("]")) // workaround for array replacement or item in the array replacement { if (nbtPath.remove(tag)==0) return false; Pattern pattern = Pattern.compile("\\[[^\\[]*]$"); Matcher matcher = pattern.matcher(pathText); if (!matcher.find()) // malformed path { return false; } String arrAccess = matcher.group(); int pos; if (arrAccess.length()==2) // we just removed entire array pos = 0; else { try { pos = Integer.parseInt(arrAccess.substring(1, arrAccess.length() - 1)); } catch (NumberFormatException e) { return false; } } NbtPathArgumentType.NbtPath newPath = cachePath(pathText.substring(0, pathText.length()-arrAccess.length())); return modify_insert(pos,newPath,replacement, tag); } try { nbtPath.put(tag, () -> replacement); } catch (CommandSyntaxException e) { return false; } return true; } @Override public Value get(Value value) { NbtPathArgumentType.NbtPath path = cachePath(value.getString()); try { List<Tag> tags = path.get(getTag()); if (tags.size()==0) return Value.NULL; if (tags.size()==1) return NBTSerializableValue.decodeTag(tags.get(0)); return ListValue.wrap(tags.stream().map(NBTSerializableValue::decodeTag).collect(Collectors.toList())); } catch (CommandSyntaxException ignored) { } return Value.NULL; } @Override public boolean has(Value where) { return cachePath(where.getString()).count(getTag()) > 0; } private void ensureOwnership() { if (!owned) { nbtTag = getTag().copy(); nbtString = null; nbtSupplier = null; // just to be sure owned = true; } } @Override public boolean delete(Value where) { NbtPathArgumentType.NbtPath path = cachePath(where.getString()); ensureOwnership(); int removed = path.remove(getTag()); return removed > 0; } public static class InventoryLocator { public Object owner; public BlockPos position; public Inventory inventory; public int offset; public boolean isEnder; InventoryLocator(Object owner, BlockPos pos, Inventory i, int o) { this(owner, pos, i, o, false); } InventoryLocator(Object owner, BlockPos pos, Inventory i, int o, boolean isEnder) { this.owner = owner; position = pos; inventory = i; offset = o; this.isEnder = isEnder; } } private static Map<String, NbtPathArgumentType.NbtPath> pathCache = new HashMap<>(); private static NbtPathArgumentType.NbtPath cachePath(String arg) { NbtPathArgumentType.NbtPath res = pathCache.get(arg); if (res != null) return res; try { res = NbtPathArgumentType.nbtPath().parse(new StringReader(arg)); } catch (CommandSyntaxException exc) { throw new InternalExpressionException("Incorrect nbt path: "+arg); } if (pathCache.size() > 1024) pathCache.clear(); pathCache.put(arg, res); return res; } @Override public String getTypeString() { return "nbt"; } @Override public Tag toTag(boolean force) { if (!force) throw new NBTSerializableValue.IncompatibleTypeException(this); ensureOwnership(); return getTag(); } public static class IncompatibleTypeException extends RuntimeException { private IncompatibleTypeException() {} public Value val; public IncompatibleTypeException(Value val) { this.val = val; } }; }