Merge branches 'master' and 'mods' of https://github.com/Anuken/Mindustry
# Conflicts: # core/src/io/anuke/mindustry/entities/type/FlyingUnit.java # gradle.properties
This commit is contained in:
193
core/src/io/anuke/mindustry/mod/ContentParser.java
Normal file
193
core/src/io/anuke/mindustry/mod/ContentParser.java
Normal file
@@ -0,0 +1,193 @@
|
||||
package io.anuke.mindustry.mod;
|
||||
|
||||
import io.anuke.arc.collection.*;
|
||||
import io.anuke.arc.graphics.*;
|
||||
import io.anuke.arc.util.*;
|
||||
import io.anuke.arc.util.ArcAnnotate.*;
|
||||
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.content.*;
|
||||
import io.anuke.mindustry.entities.bullet.*;
|
||||
import io.anuke.mindustry.entities.type.*;
|
||||
import io.anuke.mindustry.game.*;
|
||||
import io.anuke.mindustry.type.*;
|
||||
import io.anuke.mindustry.world.*;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public class ContentParser{
|
||||
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){
|
||||
try{
|
||||
if(type == BulletType.class){
|
||||
BulletType b = (BulletType)Bullets.class.getField(jsonData.asString()).get(null);
|
||||
if(b == null) throw new IllegalArgumentException("Bullet type not found: " + jsonData.asString());
|
||||
return (T)b;
|
||||
}
|
||||
|
||||
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);
|
||||
}catch(Exception e){
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
},
|
||||
ContentType.item, (TypeParser<Item>)(mod, name, value) -> {
|
||||
Item item = new Item(mod + "-" + name, new Color(Color.black));
|
||||
readFields(item, value);
|
||||
return item;
|
||||
},
|
||||
ContentType.unit, (TypeParser<UnitType>)(mod, name, value) -> {
|
||||
String clas = value.getString("type");
|
||||
Class<BaseUnit> type = resolve("io.anuke.mindustry.entities.type.base." + clas);
|
||||
java.lang.reflect.Constructor<BaseUnit> cons = type.getDeclaredConstructor();
|
||||
UnitType unit = new UnitType(mod + "-" + name, type, () -> {
|
||||
try{
|
||||
return cons.newInstance();
|
||||
}catch(Exception e){
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
value.remove("type");
|
||||
readFields(unit, value);
|
||||
|
||||
return unit;
|
||||
}
|
||||
);
|
||||
|
||||
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
|
||||
* @param json the json to parse
|
||||
* @param type the type of content this is
|
||||
* @return the content that was parsed
|
||||
*/
|
||||
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 + "'");
|
||||
}
|
||||
|
||||
Content c = parsers.get(type).parse(mod, name, value);
|
||||
checkNulls(c);
|
||||
return c;
|
||||
}
|
||||
|
||||
/** Checks all @NonNull fields in this object, recursively.
|
||||
* Throws an exception if any are null.*/
|
||||
private void checkNulls(Object object){
|
||||
checkNulls(object, new ObjectSet<>());
|
||||
}
|
||||
|
||||
private void checkNulls(Object object, ObjectSet<Object> checked){
|
||||
checked.add(object);
|
||||
|
||||
parser.getFields(object.getClass()).each((name, field) -> {
|
||||
try{
|
||||
if(field.field.getType().isPrimitive()) return;
|
||||
|
||||
Object obj = field.field.get(object);
|
||||
if(field.field.isAnnotationPresent(NonNull.class) && field.field.get(object) == null){
|
||||
throw new RuntimeException("Field '" + name + "' in " + object.getClass().getSimpleName() + " is missing!");
|
||||
}
|
||||
|
||||
if(obj != null && !checked.contains(obj)){
|
||||
checkNulls(obj, checked);
|
||||
checked.add(obj);
|
||||
}
|
||||
}catch(Exception e){
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
30
core/src/io/anuke/mindustry/mod/Mod.java
Normal file
30
core/src/io/anuke/mindustry/mod/Mod.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package io.anuke.mindustry.mod;
|
||||
|
||||
import io.anuke.arc.files.*;
|
||||
import io.anuke.arc.util.*;
|
||||
import io.anuke.mindustry.*;
|
||||
|
||||
public class Mod{
|
||||
/** @return the config file for this plugin, as the file 'mods/[plugin-name]/config.json'.*/
|
||||
public FileHandle getConfig(){
|
||||
return Vars.mods.getConfig(this);
|
||||
}
|
||||
|
||||
/** Called after all plugins have been created and commands have been registered.*/
|
||||
public void init(){
|
||||
|
||||
}
|
||||
|
||||
/** Create any content needed here. */
|
||||
public void loadContent(){}
|
||||
|
||||
/** Register any commands to be used on the server side, e.g. from the console. */
|
||||
public void registerServerCommands(CommandHandler handler){
|
||||
|
||||
}
|
||||
|
||||
/** Register any commands to be used on the client side, e.g. sent from an in-game player.. */
|
||||
public void registerClientCommands(CommandHandler handler){
|
||||
|
||||
}
|
||||
}
|
||||
284
core/src/io/anuke/mindustry/mod/Mods.java
Normal file
284
core/src/io/anuke/mindustry/mod/Mods.java
Normal file
@@ -0,0 +1,284 @@
|
||||
package io.anuke.mindustry.mod;
|
||||
|
||||
import io.anuke.arc.*;
|
||||
import io.anuke.arc.assets.*;
|
||||
import io.anuke.arc.collection.*;
|
||||
import io.anuke.arc.files.*;
|
||||
import io.anuke.arc.function.*;
|
||||
import io.anuke.arc.graphics.*;
|
||||
import io.anuke.arc.graphics.Pixmap.*;
|
||||
import io.anuke.arc.graphics.Texture.*;
|
||||
import io.anuke.arc.graphics.g2d.*;
|
||||
import io.anuke.arc.util.*;
|
||||
import io.anuke.arc.util.ArcAnnotate.*;
|
||||
import io.anuke.arc.util.io.*;
|
||||
import io.anuke.arc.util.serialization.*;
|
||||
import io.anuke.mindustry.game.*;
|
||||
import io.anuke.mindustry.type.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
|
||||
import static io.anuke.mindustry.Vars.*;
|
||||
|
||||
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;
|
||||
|
||||
/** Returns a file named 'config.json' in a special folder for the specified plugin.
|
||||
* Call this in init(). */
|
||||
public FileHandle getConfig(Mod mod){
|
||||
ModMeta load = metas.get(mod.getClass());
|
||||
if(load == null) throw new IllegalArgumentException("Mod is not loaded yet (or missing)!");
|
||||
return modDirectory.child(load.name).child("config.json");
|
||||
}
|
||||
|
||||
/** @return the loaded mod found by class, or null if not found. */
|
||||
public @Nullable
|
||||
LoadedMod getMod(Class<? extends Mod> type){
|
||||
return loaded.find(l -> l.mod.getClass() == type);
|
||||
}
|
||||
|
||||
/** Imports an external mod file.*/
|
||||
public void importMod(FileHandle file) throws IOException{
|
||||
FileHandle dest = modDirectory.child(file.name());
|
||||
if(dest.exists()){
|
||||
throw new IOException("A mod with the same filename already exists!");
|
||||
}
|
||||
|
||||
file.copyTo(dest);
|
||||
try{
|
||||
loaded.add(loadMod(file));
|
||||
requiresRestart = true;
|
||||
}catch(IOException e){
|
||||
dest.delete();
|
||||
throw e;
|
||||
}catch(Throwable t){
|
||||
dest.delete();
|
||||
throw new IOException(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Repacks all in-game sprites. */
|
||||
@Override
|
||||
public void loadAsync(){
|
||||
if(loaded.isEmpty()) return;
|
||||
|
||||
packer = new PixmapPacker(2048, 2048, Format.RGBA8888, 2, true);
|
||||
for(LoadedMod mod : loaded){
|
||||
try{
|
||||
int packed = 0;
|
||||
for(FileHandle file : mod.root.child("sprites").list()){
|
||||
if(file.extension().equals("png")){
|
||||
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);
|
||||
pixmap.dispose();
|
||||
packed ++;
|
||||
totalSprites ++;
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.info("Packed {0} images for mod '{1}'.", packed, mod.meta.name);
|
||||
}catch(IOException e){
|
||||
Log.err("Error packing images for mod: {0}", mod.meta.name);
|
||||
e.printStackTrace();
|
||||
if(!headless) ui.showException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadSync(){
|
||||
if(packer == null) return;
|
||||
|
||||
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){
|
||||
if(mod.file.isDirectory()){
|
||||
mod.file.deleteDirectory();
|
||||
}else{
|
||||
mod.file.delete();
|
||||
}
|
||||
loaded.remove(mod);
|
||||
requiresRestart = true;
|
||||
}
|
||||
|
||||
public boolean requiresRestart(){
|
||||
return requiresRestart;
|
||||
}
|
||||
|
||||
/** 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") && !(file.isDirectory() && file.child("mod.json").exists())) continue;
|
||||
|
||||
try{
|
||||
loaded.add(loadMod(file));
|
||||
}catch(IllegalArgumentException ignored){
|
||||
}catch(Exception e){
|
||||
Log.err("Failed to load plugin file {0}. Skipping.", file);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
buildFiles();
|
||||
}
|
||||
|
||||
private void buildFiles(){
|
||||
for(LoadedMod mod : loaded){
|
||||
for(FileHandle file : mod.root.list()){
|
||||
//ignore special folders like bundles or sprites
|
||||
if(file.isDirectory() && !specialFolders.contains(file.name())){
|
||||
//TODO calling child/parent on these files will give you gibberish; create wrapper class.
|
||||
file.walk(f -> filet.addFile(f));
|
||||
}
|
||||
}
|
||||
|
||||
//load up bundles.
|
||||
FileHandle folder = mod.root.child("bundles");
|
||||
if(folder.exists()){
|
||||
for(FileHandle file : folder.list()){
|
||||
if(file.name().startsWith("bundle") && file.extension().equals("properties")){
|
||||
String name = file.nameWithoutExtension();
|
||||
bundles.getOr(name, Array::new).add(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//add new keys to each bundle
|
||||
I18NBundle bundle = Core.bundle;
|
||||
while(bundle != null){
|
||||
String str = bundle.getLocale().toString();
|
||||
String locale = "bundle" + (str.isEmpty() ? "" : "_" + str);
|
||||
for(FileHandle file : bundles.getOr(locale, Array::new)){
|
||||
try{
|
||||
PropertiesUtils.load(bundle.getProperties(), file.reader());
|
||||
}catch(Exception e){
|
||||
throw new RuntimeException("Error loading bundle: " + file + "/" + locale, e);
|
||||
}
|
||||
}
|
||||
bundle = bundle.getParent();
|
||||
}
|
||||
}
|
||||
|
||||
/** Creates all the content found in mod files. */
|
||||
public void loadContent(){
|
||||
for(LoadedMod mod : loaded){
|
||||
if(mod.root.child("content").exists()){
|
||||
FileHandle contentRoot = mod.root.child("content");
|
||||
for(ContentType type : ContentType.all){
|
||||
FileHandle folder = contentRoot.child(type.name().toLowerCase() + "s");
|
||||
if(folder.exists()){
|
||||
for(FileHandle file : folder.list()){
|
||||
if(file.extension().equals("json")){
|
||||
try{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
each(Mod::loadContent);
|
||||
}
|
||||
|
||||
/** @return all loaded mods. */
|
||||
public Array<LoadedMod> all(){
|
||||
return loaded;
|
||||
}
|
||||
|
||||
/** Iterates through each mod with a main class.*/
|
||||
public void each(Consumer<Mod> cons){
|
||||
loaded.each(p -> p.mod != null, p -> cons.accept(p.mod));
|
||||
}
|
||||
|
||||
/** 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.", sourceFile);
|
||||
throw new IllegalArgumentException("No mod.json found.");
|
||||
}
|
||||
|
||||
ModMeta meta = json.fromJson(ModMeta.class, metaf.readString());
|
||||
String camelized = meta.name.replace(" ", "");
|
||||
String mainClass = meta.main == null ? camelized.toLowerCase() + "." + camelized + "Mod" : meta.main;
|
||||
Mod mainMod;
|
||||
|
||||
//make sure the main class exists before loading it; if it doesn't just don't put it there
|
||||
if(zip.child(mainClass.replace('.', '/') + ".class").exists()){
|
||||
//other platforms don't have standard java class loaders
|
||||
if(mobile){
|
||||
throw new IllegalArgumentException("This mod is not compatible with " + (ios ? "iOS" : "Android") + ".");
|
||||
}
|
||||
|
||||
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();
|
||||
}else{
|
||||
mainMod = null;
|
||||
}
|
||||
|
||||
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/folder on the disk. */
|
||||
public final FileHandle file;
|
||||
/** 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;
|
||||
/** Internal mod name. Used for textures. */
|
||||
public final String name;
|
||||
/** This mod's metadata. */
|
||||
public final ModMeta meta;
|
||||
|
||||
//TODO implement
|
||||
protected boolean enabled;
|
||||
|
||||
public LoadedMod(FileHandle file, FileHandle root, Mod mod, ModMeta meta){
|
||||
this.root = root;
|
||||
this.file = file;
|
||||
this.mod = mod;
|
||||
this.meta = meta;
|
||||
this.name = meta.name.toLowerCase().replace(" ", "-");
|
||||
}
|
||||
}
|
||||
|
||||
/** Plugin metadata information.*/
|
||||
public static class ModMeta{
|
||||
public String name, author, description, version, main;
|
||||
public String[] dependencies = {}; //TODO implement
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user