JSON block, item loading

This commit is contained in:
Anuken 2019-09-29 15:21:50 -04:00
parent 9c175ac893
commit f17e46015a
7 changed files with 165 additions and 57 deletions

View File

@ -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();

View File

@ -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();

View File

@ -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.*/

View File

@ -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<ContentType, TypeParser<?>> parsers = ObjectMap.of(
private static final boolean ignoreUnknownFields = true;
private ObjectMap<Class<?>, ContentType> contentTypes = new ObjectMap<>();
private Json parser = new Json(){
public <T> T readValue(Class<T> 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<ContentType, TypeParser<?>> parsers = ObjectMap.of(
ContentType.block, (TypeParser<Block>)(mod, name, value) -> {
String clas = value.getString("type");
Class<Block> 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<Content> 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<String, FieldMetadata> 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 <T> Class<T> resolve(String... potentials) throws Exception{
for(String type : potentials){
try{
return (Class<T>)Class.forName(type);
}catch(Exception ignored){
}
}
throw new IllegalArgumentException("Type not found: " + potentials[0]);
}
public interface TypeParser<T extends Content>{
T parse(String mod, String name, JsonValue value) throws Exception;
}
}

View File

@ -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<String, Array<FileHandle>> bundles = new ObjectMap<>();
private ObjectSet<String> specialFolders = ObjectSet.with("bundles", "sprites");
private int totalSprites;
private PixmapPacker packer;
private Array<LoadedMod> loaded = new Array<>();
private ObjectMap<Class<?>, 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(" ", "-");
}
}

View File

@ -1,8 +0,0 @@
package io.anuke.mindustry.mod;
import io.anuke.arc.util.serialization.*;
import io.anuke.mindustry.game.*;
public abstract class TypeParser<T extends Content>{
public abstract T parse(String name, JsonValue value);
}

View File

@ -142,8 +142,7 @@ public class CrashSender{
private static void ex(Runnable r){
try{
r.run();
}catch(Throwable t){
t.printStackTrace();
}catch(Throwable ignored){
}
}
}