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:
Anuken
2019-09-29 17:03:28 -04:00
58 changed files with 1012 additions and 273 deletions

View 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;
}
}

View 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){
}
}

View 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
}
}