From f17e46015a1aaae6713759e2cc1c35e6ff3a4546 Mon Sep 17 00:00:00 2001 From: Anuken Date: Sun, 29 Sep 2019 15:21:50 -0400 Subject: [PATCH] JSON block, item loading --- .../io/anuke/mindustry/ClientLauncher.java | 11 +- core/src/io/anuke/mindustry/Vars.java | 6 +- .../anuke/mindustry/core/ContentLoader.java | 18 ++- .../io/anuke/mindustry/mod/ContentParser.java | 105 +++++++++++++++++- core/src/io/anuke/mindustry/mod/Mods.java | 71 ++++++------ .../io/anuke/mindustry/mod/TypeParser.java | 8 -- .../io/anuke/mindustry/net/CrashSender.java | 3 +- 7 files changed, 165 insertions(+), 57 deletions(-) delete mode 100644 core/src/io/anuke/mindustry/mod/TypeParser.java diff --git a/core/src/io/anuke/mindustry/ClientLauncher.java b/core/src/io/anuke/mindustry/ClientLauncher.java index 13f05a847c..2012d59c26 100644 --- a/core/src/io/anuke/mindustry/ClientLauncher.java +++ b/core/src/io/anuke/mindustry/ClientLauncher.java @@ -44,6 +44,7 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform assets.load("sprites/error.png", Texture.class); atlas = TextureAtlas.blankAtlas(); Vars.net = new Net(platform.getNet()); + Vars.mods = new Mods(); UI.loadSystemCursors(); @@ -55,12 +56,6 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform atlas = (TextureAtlas)t; }; - if(!mods.all().isEmpty()){ - assets.loadRun("mods", Mods.class, () -> { - mods.packSprites(); - }); - } - assets.loadRun("maps", Map.class, () -> maps.loadPreviews()); Musics.load(); @@ -69,8 +64,6 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform assets.loadRun("contentcreate", Content.class, () -> { content.createContent(); content.loadColors(); - - mods.loadContent(); }); add(logic = new Logic()); @@ -80,6 +73,8 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform add(netServer = new NetServer()); add(netClient = new NetClient()); + assets.load(mods); + assets.loadRun("contentinit", ContentLoader.class, () -> { content.init(); content.load(); diff --git a/core/src/io/anuke/mindustry/Vars.java b/core/src/io/anuke/mindustry/Vars.java index 9d0a51ebab..9315d55bf2 100644 --- a/core/src/io/anuke/mindustry/Vars.java +++ b/core/src/io/anuke/mindustry/Vars.java @@ -25,7 +25,7 @@ import io.anuke.mindustry.world.blocks.defense.ForceProjector.*; import java.nio.charset.*; import java.util.*; -import static io.anuke.arc.Core.settings; +import static io.anuke.arc.Core.*; @SuppressWarnings("unchecked") public class Vars implements Loadable{ @@ -195,7 +195,9 @@ public class Vars implements Loadable{ Version.init(); filet = new FileTree(); - mods = new Mods(); + if(mods == null){ + mods = new Mods(); + } content = new ContentLoader(); loops = new LoopControl(); defaultWaves = new DefaultWaves(); diff --git a/core/src/io/anuke/mindustry/core/ContentLoader.java b/core/src/io/anuke/mindustry/core/ContentLoader.java index a919233c2e..f05e043050 100644 --- a/core/src/io/anuke/mindustry/core/ContentLoader.java +++ b/core/src/io/anuke/mindustry/core/ContentLoader.java @@ -11,6 +11,7 @@ import io.anuke.mindustry.type.*; import io.anuke.mindustry.world.*; import static io.anuke.arc.Core.files; +import static io.anuke.mindustry.Vars.mods; /** * Loads all game content. @@ -57,6 +58,21 @@ public class ContentLoader{ list.load(); } + setupMapping(); + + mods.loadContent(); + + setupMapping(); + + loaded = true; + } + + private void setupMapping(){ + + for(ContentType type : ContentType.values()){ + contentNameMap[type.ordinal()].clear(); + } + for(ContentType type : ContentType.values()){ for(Content c : contentMap[type.ordinal()]){ @@ -79,8 +95,6 @@ public class ContentLoader{ } } } - - loaded = true; } /** Logs content statistics.*/ diff --git a/core/src/io/anuke/mindustry/mod/ContentParser.java b/core/src/io/anuke/mindustry/mod/ContentParser.java index 66c647f52a..0a6fd238bc 100644 --- a/core/src/io/anuke/mindustry/mod/ContentParser.java +++ b/core/src/io/anuke/mindustry/mod/ContentParser.java @@ -1,16 +1,61 @@ package io.anuke.mindustry.mod; import io.anuke.arc.collection.*; +import io.anuke.arc.util.*; +import io.anuke.arc.util.reflect.*; import io.anuke.arc.util.serialization.*; +import io.anuke.arc.util.serialization.Json.*; +import io.anuke.mindustry.*; import io.anuke.mindustry.game.*; import io.anuke.mindustry.type.*; +import io.anuke.mindustry.world.*; +@SuppressWarnings("unchecked") public class ContentParser{ - private Json parser = new Json(); - private ObjectMap> parsers = ObjectMap.of( + private static final boolean ignoreUnknownFields = true; + private ObjectMap, ContentType> contentTypes = new ObjectMap<>(); + private Json parser = new Json(){ + public T readValue(Class type, Class elementType, JsonValue jsonData){ + if(type != null && Content.class.isAssignableFrom(type)){ + return (T)Vars.content.getByName(contentTypes.getThrow(type, () -> new IllegalArgumentException("No content type for class: " + type.getSimpleName())), jsonData.asString()); + } + return super.readValue(type, elementType, jsonData); + } + }; + + private ObjectMap> parsers = ObjectMap.of( + ContentType.block, (TypeParser)(mod, name, value) -> { + String clas = value.getString("type"); + Class type = resolve("io.anuke.mindustry.world." + clas, "io.anuke.mindustry.world.blocks." + clas, "io.anuke.mindustry.world.blocks.defense" + clas); + Block block = type.getDeclaredConstructor(String.class).newInstance(mod + "-" + name); + value.remove("type"); + readFields(block, value); + + //make block visible + if(block.buildRequirements != null){ + block.buildVisibility = () -> true; + } + + return block; + } ); + private void init(){ + for(ContentType type : ContentType.all){ + Array arr = Vars.content.getBy(type); + if(!arr.isEmpty()){ + Class c = arr.first().getClass(); + //get base content class, skipping intermediates + while(!(c.getSuperclass() == Content.class || c.getSuperclass() == UnlockableContent.class || c.getSuperclass() == UnlockableContent.class)){ + c = c.getSuperclass(); + } + + contentTypes.put(c, type); + } + } + } + /** * Parses content from a json file. * @param name the name of the file without its extension @@ -18,12 +63,64 @@ public class ContentParser{ * @param type the type of content this is * @return the content that was parsed */ - public Content parse(String name, String json, ContentType type) throws Exception{ + public Content parse(String mod, String name, String json, ContentType type) throws Exception{ + if(contentTypes.isEmpty()){ + init(); + } + JsonValue value = parser.fromJson(null, json); if(!parsers.containsKey(type)){ throw new SerializationException("No parsers for content type '" + type + "'"); } - return parsers.get(type).parse(name, value); + return parsers.get(type).parse(mod, name, value); } + + private void readFields(Object object, JsonValue jsonMap){ + Class type = object.getClass(); + ObjectMap fields = parser.getFields(type); + for(JsonValue child = jsonMap.child; child != null; child = child.next){ + FieldMetadata metadata = fields.get(child.name().replace(" ", "_")); + if(metadata == null){ + if(ignoreUnknownFields){ + Log.err("{0}: Ignoring unknown field: " + child.name + " (" + type.getName() + ")", object); + continue; + }else{ + SerializationException ex = new SerializationException("Field not found: " + child.name + " (" + type.getName() + ")"); + ex.addTrace(child.trace()); + throw ex; + } + } + Field field = metadata.field; + try{ + field.set(object, parser.readValue(field.getType(), metadata.elementType, child)); + }catch(ReflectionException ex){ + throw new SerializationException("Error accessing field: " + field.getName() + " (" + type.getName() + ")", ex); + }catch(SerializationException ex){ + ex.addTrace(field.getName() + " (" + type.getName() + ")"); + throw ex; + }catch(RuntimeException runtimeEx){ + SerializationException ex = new SerializationException(runtimeEx); + ex.addTrace(child.trace()); + ex.addTrace(field.getName() + " (" + type.getName() + ")"); + throw ex; + } + } + } + + /** Tries to resolve a class from a list of potential class names. */ + private Class resolve(String... potentials) throws Exception{ + for(String type : potentials){ + try{ + return (Class)Class.forName(type); + }catch(Exception ignored){ + } + } + throw new IllegalArgumentException("Type not found: " + potentials[0]); + } + + public interface TypeParser{ + T parse(String mod, String name, JsonValue value) throws Exception; + } + } diff --git a/core/src/io/anuke/mindustry/mod/Mods.java b/core/src/io/anuke/mindustry/mod/Mods.java index 26a729b612..aaa22f92ea 100644 --- a/core/src/io/anuke/mindustry/mod/Mods.java +++ b/core/src/io/anuke/mindustry/mod/Mods.java @@ -2,6 +2,7 @@ package io.anuke.mindustry.mod; import io.anuke.annotations.Annotations.*; import io.anuke.arc.*; +import io.anuke.arc.assets.*; import io.anuke.arc.collection.*; import io.anuke.arc.files.*; import io.anuke.arc.function.*; @@ -20,12 +21,15 @@ import java.net.*; import static io.anuke.mindustry.Vars.*; -public class Mods{ +public class Mods implements Loadable{ private Json json = new Json(); private ContentParser parser = new ContentParser(); private ObjectMap> bundles = new ObjectMap<>(); private ObjectSet specialFolders = ObjectSet.with("bundles", "sprites"); + private int totalSprites; + private PixmapPacker packer; + private Array loaded = new Array<>(); private ObjectMap, ModMeta> metas = new ObjectMap<>(); private boolean requiresRestart; @@ -64,10 +68,11 @@ public class Mods{ } /** Repacks all in-game sprites. */ - public void packSprites(){ - int total = 0; + @Override + public void loadAsync(){ + if(loaded.isEmpty()) return; - PixmapPacker packer = new PixmapPacker(2048, 2048, Format.RGBA8888, 2, true); + packer = new PixmapPacker(2048, 2048, Format.RGBA8888, 2, true); for(LoadedMod mod : loaded){ try{ int packed = 0; @@ -76,10 +81,10 @@ public class Mods{ try(InputStream stream = file.read()){ byte[] bytes = Streams.copyStreamToByteArray(stream, Math.max((int)file.length(), 512)); Pixmap pixmap = new Pixmap(bytes, 0, bytes.length); - packer.pack(mod.name + ":" + file.nameWithoutExtension(), pixmap); + packer.pack(mod.name + "-" + file.nameWithoutExtension(), pixmap); pixmap.dispose(); packed ++; - total ++; + totalSprites ++; } } } @@ -90,25 +95,28 @@ public class Mods{ if(!headless) ui.showException(e); } } + } - //only pack if there's something to be packed - //TODO is disposing necessary/safe? - if(total > 0){ - Core.app.post(() -> { - TextureFilter filter = Core.settings.getBool("linear") ? TextureFilter.Linear : TextureFilter.Nearest; + @Override + public void loadSync(){ + if(packer == null) return; - packer.getPages().each(page -> page.updateTexture(filter, filter, false)); - packer.getPages().each(page -> page.getRects().each((name, rect) -> Core.atlas.addRegion(name, page.getTexture(), (int)rect.x, (int)rect.y, (int)rect.width, (int)rect.height))); - packer.dispose(); - }); - }else{ - packer.dispose(); + if(totalSprites > 0){ + TextureFilter filter = Core.settings.getBool("linear") ? TextureFilter.Linear : TextureFilter.Nearest; + packer.getPages().each(page -> page.updateTexture(filter, filter, false)); + packer.getPages().each(page -> page.getRects().each((name, rect) -> Core.atlas.addRegion(name, page.getTexture(), (int)rect.x, (int)rect.y, (int)rect.width, (int)rect.height))); } + + packer.dispose(); } /** Removes a mod file and marks it for requiring a restart. */ public void removeMod(LoadedMod mod){ - mod.file.delete(); + if(mod.file.isDirectory()){ + mod.file.deleteDirectory(); + }else{ + mod.file.delete(); + } loaded.remove(mod); requiresRestart = true; } @@ -120,7 +128,7 @@ public class Mods{ /** Loads all mods from the folder, but does call any methods on them.*/ public void load(){ for(FileHandle file : modDirectory.list()){ - if(!file.extension().equals("jar") && !file.extension().equals("zip")) continue; + if(!file.extension().equals("jar") && !file.extension().equals("zip") && !(file.isDirectory() && file.child("mod.json").exists())) continue; try{ loaded.add(loadMod(file)); @@ -178,13 +186,13 @@ public class Mods{ if(mod.root.child("content").exists()){ FileHandle contentRoot = mod.root.child("content"); for(ContentType type : ContentType.all){ - FileHandle folder = contentRoot.child(type.name()); + FileHandle folder = contentRoot.child(type.name() + "s"); if(folder.exists()){ for(FileHandle file : folder.list()){ if(file.extension().equals("json")){ try{ - Content loaded = parser.parse(file.nameWithoutExtension(), file.readString(), type); - Log.info("[{0}] Loaded '{1}'", loaded, mod.meta.name); + Content loaded = parser.parse(mod.name, file.nameWithoutExtension(), file.readString(), type); + Log.info("[{0}] Loaded '{1}'.", mod.meta.name, loaded); }catch(Exception e){ throw new RuntimeException("Failed to parse content file '" + file + "' for mod '" + mod.meta.name + "'.", e); } @@ -206,13 +214,14 @@ public class Mods{ loaded.each(p -> p.mod != null, p -> cons.accept(p.mod)); } - /** Loads a mod file+meta, but does not add it to the list. */ - private LoadedMod loadMod(FileHandle jar) throws Exception{ - FileHandle zip = new ZipFileHandle(jar); + /** Loads a mod file+meta, but does not add it to the list. + * Note that directories can be loaded as mods.*/ + private LoadedMod loadMod(FileHandle sourceFile) throws Exception{ + FileHandle zip = sourceFile.isDirectory() ? sourceFile : new ZipFileHandle(sourceFile); FileHandle metaf = zip.child("mod.json").exists() ? zip.child("mod.json") : zip.child("plugin.json"); if(!metaf.exists()){ - Log.warn("Mod {0} doesn't have a 'mod.json'/'plugin.json' file, skipping.", jar); + Log.warn("Mod {0} doesn't have a 'mod.json'/'plugin.json' file, skipping.", sourceFile); throw new IllegalArgumentException("No mod.json found."); } @@ -228,7 +237,7 @@ public class Mods{ throw new IllegalArgumentException("This mod is not compatible with " + (ios ? "iOS" : "Android") + "."); } - URLClassLoader classLoader = new URLClassLoader(new URL[]{jar.file().toURI().toURL()}, ClassLoader.getSystemClassLoader()); + URLClassLoader classLoader = new URLClassLoader(new URL[]{sourceFile.file().toURI().toURL()}, ClassLoader.getSystemClassLoader()); Class main = classLoader.loadClass(mainClass); metas.put(main, meta); mainMod = (Mod)main.getDeclaredConstructor().newInstance(); @@ -236,14 +245,14 @@ public class Mods{ mainMod = null; } - return new LoadedMod(jar, zip, mainMod, meta); + return new LoadedMod(sourceFile, zip, mainMod, meta); } /** Represents a plugin that has been loaded from a jar file.*/ public static class LoadedMod{ - /** The location of this mod's zip file on the disk. */ + /** The location of this mod's zip file/folder on the disk. */ public final FileHandle file; - /** The root zip file; points to the contents of this mod. */ + /** The root zip file; points to the contents of this mod. In the case of folders, this is the same as the mod's file. */ public final FileHandle root; /** The mod's main class; may be null. */ public final @Nullable Mod mod; @@ -260,7 +269,7 @@ public class Mods{ this.file = file; this.mod = mod; this.meta = meta; - this.name = Strings.camelize(meta.name); + this.name = meta.name.toLowerCase().replace(" ", "-"); } } diff --git a/core/src/io/anuke/mindustry/mod/TypeParser.java b/core/src/io/anuke/mindustry/mod/TypeParser.java deleted file mode 100644 index 16c449e133..0000000000 --- a/core/src/io/anuke/mindustry/mod/TypeParser.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.anuke.mindustry.mod; - -import io.anuke.arc.util.serialization.*; -import io.anuke.mindustry.game.*; - -public abstract class TypeParser{ - public abstract T parse(String name, JsonValue value); -} diff --git a/core/src/io/anuke/mindustry/net/CrashSender.java b/core/src/io/anuke/mindustry/net/CrashSender.java index cc29071615..8280557136 100644 --- a/core/src/io/anuke/mindustry/net/CrashSender.java +++ b/core/src/io/anuke/mindustry/net/CrashSender.java @@ -142,8 +142,7 @@ public class CrashSender{ private static void ex(Runnable r){ try{ r.run(); - }catch(Throwable t){ - t.printStackTrace(); + }catch(Throwable ignored){ } } }