001/*
002 * Copyright 2020 zml
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *    http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package ca.stellardrift.confabricate;
017
018import com.google.common.collect.ImmutableSet;
019import net.minecraft.nbt.ByteArrayTag;
020import net.minecraft.nbt.ByteTag;
021import net.minecraft.nbt.CompoundTag;
022import net.minecraft.nbt.DoubleTag;
023import net.minecraft.nbt.EndTag;
024import net.minecraft.nbt.FloatTag;
025import net.minecraft.nbt.IntArrayTag;
026import net.minecraft.nbt.IntTag;
027import net.minecraft.nbt.ListTag;
028import net.minecraft.nbt.LongArrayTag;
029import net.minecraft.nbt.LongTag;
030import net.minecraft.nbt.ShortTag;
031import net.minecraft.nbt.StringTag;
032import net.minecraft.nbt.Tag;
033import org.checkerframework.checker.nullness.qual.NonNull;
034import org.spongepowered.configurate.BasicConfigurationNode;
035import org.spongepowered.configurate.ConfigurationNode;
036import org.spongepowered.configurate.ConfigurationOptions;
037
038import java.io.IOException;
039import java.util.List;
040import java.util.Map;
041
042/**
043 * A configuration adapter that will convert Minecraft NBT data into a
044 * Configurate {@link ConfigurationNode}.
045 *
046 * @since 1.0.0
047 */
048public final class NbtNodeAdapter {
049
050    private NbtNodeAdapter() {}
051
052    /**
053     * Given a tag, convert it to a node.
054     *
055     * <p>Depending on the configuration of the provided node, the conversion
056     * may lose some data when roundtripped back. For example, array tags may
057     * be converted to lists if the node provided does not support arrays.
058     *
059     * @param tag the tag to convert
060     * @param node the node to populate
061     * @throws IOException if invalid tags are provided
062     * @since 1.0.0
063     */
064    public static void tagToNode(final Tag tag, final ConfigurationNode node) throws IOException {
065        if (tag instanceof CompoundTag) {
066            final CompoundTag compoundTag = (CompoundTag) tag;
067            for (String key : compoundTag.getKeys()) {
068                tagToNode(compoundTag.get(key), node.node(key));
069            }
070        } else if (tag instanceof ListTag) {
071            for (Tag value : (ListTag) tag) {
072                tagToNode(value, node.appendListNode());
073            }
074        } else if (tag instanceof StringTag) {
075            node.raw(tag.asString());
076        } else if (tag instanceof ByteTag) {
077            node.raw(((ByteTag) tag).getByte());
078        } else if (tag instanceof ShortTag) {
079            node.raw(((ShortTag) tag).getShort());
080        } else if (tag instanceof IntTag) {
081            node.raw(((IntTag) tag).getInt());
082        } else if (tag instanceof LongTag) {
083            node.raw(((LongTag) tag).getLong());
084        } else if (tag instanceof FloatTag) {
085            node.raw(((FloatTag) tag).getFloat());
086        } else if (tag instanceof DoubleTag) {
087            node.raw(((DoubleTag) tag).getDouble());
088        } else if (tag instanceof ByteArrayTag) {
089            if (node.options().acceptsType(byte[].class)) {
090                node.raw(((ByteArrayTag) tag).getByteArray());
091            } else {
092                node.raw(null);
093                for (byte b : ((ByteArrayTag) tag).getByteArray()) {
094                    node.appendListNode().raw(b);
095                }
096            }
097        } else if (tag instanceof IntArrayTag) {
098            if (node.options().acceptsType(int[].class)) {
099                node.raw(((IntArrayTag) tag).getIntArray());
100            } else {
101                node.raw(null);
102                for (int i : ((IntArrayTag) tag).getIntArray()) {
103                    node.appendListNode().raw(i);
104                }
105            }
106
107        } else if (tag instanceof LongArrayTag) {
108            if (node.options().acceptsType(long[].class)) {
109                node.raw(((LongArrayTag) tag).getLongArray());
110            } else {
111                node.raw(null);
112                for (long l : ((LongArrayTag) tag).getLongArray()) {
113                    node.appendListNode().raw(l);
114                }
115            }
116        } else if (tag instanceof EndTag) {
117            // no-op
118        } else {
119            throw new IOException("Unknown tag type: " + tag.getClass());
120        }
121    }
122
123    /**
124     * Convert a node to tag. Because NBT is strongly typed and does not permit
125     * lists with mixed types, some configuration nodes will not be convertible
126     * to Tags.
127     *
128     * @param node the configuration node
129     * @return the converted tag object
130     * @throws IOException if an IO error occurs while converting the tag
131     * @since 1.0.0
132     */
133    public static Tag nodeToTag(final ConfigurationNode node) throws IOException {
134        if (node.isMap()) {
135            final CompoundTag tag = new CompoundTag();
136            for (Map.Entry<Object, ? extends ConfigurationNode> ent : node.childrenMap().entrySet()) {
137                tag.put(ent.getKey().toString(), nodeToTag(ent.getValue()));
138            }
139            return tag;
140        } else if (node.isList()) {
141            final ListTag list = new ListTag();
142            for (ConfigurationNode child : node.childrenList()) {
143                list.add(nodeToTag(child));
144            }
145            return list;
146        } else {
147            final Object obj = node.raw();
148            if (obj instanceof byte[]) {
149                return new ByteArrayTag((byte[]) obj);
150            } else if (obj instanceof int[]) {
151                return new IntArrayTag((int[]) obj);
152            } else if (obj instanceof long[]) {
153                return new LongArrayTag((long[]) obj);
154            } else if (obj instanceof Byte) {
155                return ByteTag.of((Byte) obj);
156            } else if (obj instanceof Short) {
157                return ShortTag.of((Short) obj);
158            } else if (obj instanceof Integer) {
159                return IntTag.of((Integer) obj);
160            } else if (obj instanceof Long) {
161                return LongTag.of((Long) obj);
162            } else if (obj instanceof Float) {
163                return FloatTag.of((Float) obj);
164            } else if (obj instanceof Double) {
165                return DoubleTag.of((Double) obj);
166            } else if (obj instanceof String) {
167                return StringTag.of((String) obj);
168            } else {
169                throw new IOException("Unsupported object type " + (obj == null ? null : obj.getClass()));
170            }
171        }
172    }
173
174    /**
175     * Create an empty node with options appropriate for handling NBT data.
176     *
177     * @return the new node
178     * @since 1.0.0
179     */
180    public static ConfigurationNode createEmptyNode() {
181        return createEmptyNode(Confabricate.confabricateOptions());
182    }
183
184    /**
185     * Create an empty node with options appropriate for handling NBT data.
186     *
187     * @param options options to work with
188     * @return the new node
189     * @since 1.0.0
190     */
191    public static ConfigurationNode createEmptyNode(final @NonNull ConfigurationOptions options) {
192        return BasicConfigurationNode.root(options
193                .nativeTypes(ImmutableSet.of(Map.class, List.class, Byte.class,
194                        Short.class, Integer.class, Long.class, Float.class, Double.class,
195                        long[].class, byte[].class, int[].class, String.class)));
196    }
197
198}