Skip to content

Commit abb302c

Browse files
committed
feat: make schematic format detection faster
1 parent 99f4cd9 commit abb302c

File tree

1 file changed

+113
-1
lines changed

1 file changed

+113
-1
lines changed

worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/ClipboardFormats.java

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,19 @@
3030
import com.google.common.collect.Multimaps;
3131
import com.google.common.io.ByteSource;
3232
import com.google.common.io.Files;
33+
import com.sk89q.jnbt.NBTConstants;
34+
import com.sk89q.jnbt.NBTInputStream;
3335
import com.sk89q.worldedit.LocalConfiguration;
3436
import com.sk89q.worldedit.WorldEdit;
3537
import com.sk89q.worldedit.extension.platform.Actor;
38+
import com.sk89q.worldedit.internal.util.LogManagerCompat;
3639
import com.sk89q.worldedit.util.formatting.text.TextComponent;
40+
import it.unimi.dsi.fastutil.io.FastBufferedInputStream;
41+
import org.apache.logging.log4j.Logger;
3742

3843
import javax.annotation.Nullable;
44+
import java.io.DataInputStream;
45+
import java.io.EOFException;
3946
import java.io.File;
4047
import java.io.IOException;
4148
import java.io.InputStream;
@@ -55,8 +62,11 @@
5562
import java.util.Locale;
5663
import java.util.Map;
5764
import java.util.Map.Entry;
65+
import java.util.Set;
5866
import java.util.regex.Pattern;
67+
import java.util.zip.GZIPInputStream;
5968
import java.util.zip.ZipEntry;
69+
import java.util.zip.ZipException;
6070
import java.util.zip.ZipInputStream;
6171

6272
import static com.google.common.base.Preconditions.checkNotNull;
@@ -70,6 +80,22 @@ public class ClipboardFormats {
7080
// FAWE end
7181
private static final List<ClipboardFormat> registeredFormats = new ArrayList<>();
7282

83+
// FAWE start - provide logger instance + track fast-search formats
84+
private static final Logger LOGGER = LogManagerCompat.getLogger();
85+
// a list of all schematic formats which are handled by the faster detection algorithm.
86+
// contains all builtin formats as of the time of writing, but in case we forget updating the algorithm after introducing a
87+
// new format, we keep track manually to avoid breaking something.
88+
@SuppressWarnings("deprecation")
89+
private static final Set<ClipboardFormat> FAST_SEARCH_BUILTIN_FORMATS = Set.of(
90+
BuiltInClipboardFormat.PNG, BuiltInClipboardFormat.MCEDIT_SCHEMATIC, BuiltInClipboardFormat.MINECRAFT_STRUCTURE,
91+
BuiltInClipboardFormat.SPONGE_V1_SCHEMATIC, BuiltInClipboardFormat.FAST_V2, BuiltInClipboardFormat.FAST_V3,
92+
// the following formats are not explicitly supported, but either don't support reading or
93+
// are handled by previous formats (e.g. SPONGE_V2_SCHEMATIC -> FAST_V2)
94+
BuiltInClipboardFormat.SPONGE_V2_SCHEMATIC, BuiltInClipboardFormat.SPONGE_V3_SCHEMATIC,
95+
BuiltInClipboardFormat.BROKENENTITY
96+
);
97+
// FAWE end
98+
7399
public static void registerClipboardFormat(ClipboardFormat format) {
74100
checkNotNull(format);
75101

@@ -112,24 +138,110 @@ public static ClipboardFormat findByAlias(String alias) {
112138
return aliasMap.get(alias.toLowerCase(Locale.ROOT).trim());
113139
}
114140

141+
//FAWE start - optimize format detection for builtin / known formats
142+
115143
/**
116144
* Detect the format of given a file.
117145
*
118146
* @param file the file
119147
* @return the format, otherwise null if one cannot be detected
120148
*/
149+
@SuppressWarnings("removal") // NBTInputStream + NBTConstants
121150
@Nullable
122151
public static ClipboardFormat findByFile(File file) {
123152
checkNotNull(file);
124153

154+
if (file.getName().toLowerCase().endsWith(".png")) {
155+
return BuiltInClipboardFormat.PNG;
156+
}
157+
158+
/* Conditions for known formats
159+
FAST_V3: Compound_Tag("") -> Compound_Tag("Schematic") -> Int_Tag("Version") == 3
160+
MINECRAFT_STRUCTURE: Compound_Tag("") -> len(Nbt_List_Tag("size"))
161+
FAST_V2: Compound_Tag("Schematic") -> Int_Tag("Version") == 2
162+
SPONGE_V1: Compound_Tag("Schematic") -> Int_Tag("Version") == 1
163+
MC_EDIT: Compound_Tag("Schematic") -> exist(Byte_Array_Tag("Blocks") || String_Tag("Materials"))
164+
*/
165+
try (final DataInputStream inputStream = new DataInputStream(new GZIPInputStream(new FastBufferedInputStream(java.nio.file.Files.newInputStream(
166+
file.toPath()))));
167+
final NBTInputStream nbtInputStream = new NBTInputStream(inputStream)) {
168+
if (inputStream.readByte() != NBTConstants.TYPE_COMPOUND) {
169+
return findByFileInExternalFormats(file);
170+
}
171+
final int rootNameTagLength = inputStream.readShort() & 0xFFFF;
172+
if (rootNameTagLength != 0 && rootNameTagLength != 9) { // Only allow "" and "Schematic"
173+
return findByFileInExternalFormats(file);
174+
}
175+
final String rootName = new String(inputStream.readNBytes(rootNameTagLength));
176+
if (rootName.isEmpty()) {
177+
// Only FAST_V3 and MINECRAFT_STRUCTURE use empty named root compound tags
178+
// FAST_V3 only contains a single child component - if that's not present only MINECRAFT_STRUCTURE is possible
179+
do {
180+
byte type = inputStream.readByte();
181+
if (type == NBTConstants.TYPE_END) {
182+
return findByFileInExternalFormats(file);
183+
}
184+
String name = nbtInputStream.readNamedTagName(type);
185+
if (type == NBTConstants.TYPE_COMPOUND && name.equals("Schematic")) {
186+
// unwrap inner schematic compound for general processing below
187+
break;
188+
}
189+
// search for almost all known compound children for a fast return path (lowercase is specific enough for now)
190+
if (type == NBTConstants.TYPE_LIST && (name.equals("size") || name.equals("palette") || name.equals("blocks") || name.equals("entities"))) {
191+
return BuiltInClipboardFormat.MINECRAFT_STRUCTURE;
192+
}
193+
nbtInputStream.readTagPayloadLazy(type, 0); // skip unwanted tags and continue search
194+
} while (true);
195+
}
196+
197+
do {
198+
byte type = inputStream.readByte();
199+
if (type == NBTConstants.TYPE_END) {
200+
return findByFileInExternalFormats(file);
201+
}
202+
String name = nbtInputStream.readNamedTagName(type);
203+
if ((type == NBTConstants.TYPE_BYTE_ARRAY && name.equals("Blocks")) || (type == NBTConstants.TYPE_STRING && name.equals("Materials"))) {
204+
return BuiltInClipboardFormat.MCEDIT_SCHEMATIC;
205+
}
206+
if (type == NBTConstants.TYPE_INT && name.equals("Version")) {
207+
int version = inputStream.readInt();
208+
return switch (version) {
209+
case 1 -> BuiltInClipboardFormat.SPONGE_V1_SCHEMATIC;
210+
case 2 -> BuiltInClipboardFormat.FAST_V2;
211+
case 3 -> BuiltInClipboardFormat.FAST_V3;
212+
default -> findByFileInExternalFormats(file);
213+
};
214+
}
215+
nbtInputStream.readTagPayloadLazy(type, 0); // skip unwanted tags and continue search
216+
} while (true);
217+
} catch (ZipException | EOFException ignored) {
218+
// ignore gzip errors and EOFs - the file format might not use gzip, or we expected more data from known formats
219+
// all other builtin formats use gzip compression
220+
} catch (IOException e) {
221+
// other IO errors (non gzip-related) should be logged
222+
LOGGER.error("Failed determining clipboard format for file {}", file.getAbsolutePath(), e);
223+
return null;
224+
}
225+
226+
// no builtin format seems to match - test the remaining registered formats (added by other plugins, for example)
227+
return findByFileInExternalFormats(file);
228+
}
229+
230+
private static ClipboardFormat findByFileInExternalFormats(File file) {
231+
if (registeredFormats.size() == FAST_SEARCH_BUILTIN_FORMATS.size()) {
232+
return null;
233+
}
125234
for (ClipboardFormat format : registeredFormats) {
235+
if (FAST_SEARCH_BUILTIN_FORMATS.contains(format)) {
236+
continue;
237+
}
126238
if (format.isFormat(file)) {
127239
return format;
128240
}
129241
}
130-
131242
return null;
132243
}
244+
//FAWE end
133245

134246
/**
135247
* A mapping from extensions to formats.

0 commit comments

Comments
 (0)