Files
fabric-carpet/src/main/java/carpet/script/value/NBTSerializableValue.java
T

661 lines
21 KiB
Java

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;
}
};
}