diff --git a/core/assets/bundles/bundle.properties b/core/assets/bundles/bundle.properties index 4f0f08f9e3..948fc716fb 100644 --- a/core/assets/bundles/bundle.properties +++ b/core/assets/bundles/bundle.properties @@ -64,6 +64,15 @@ uploadingpreviewfile = Uploading Preview File committingchanges = Comitting Changes done = Done +mods = Mods +mods.none = [LIGHT_GRAY]No mods found! +mod.enabled = [lightgray]Enabled +mod.disabled = [scarlet]Disabled +mod.requiresrestart = The game will now close to apply the mod changes. +mod.import = Import Mod +mod.remove.confirm = This mod will be deleted. +mod.author = [LIGHT_GRAY]Author:[] {0} + about.button = About name = Name: noname = Pick a[accent] player name[] first. diff --git a/core/src/io/anuke/mindustry/core/UI.java b/core/src/io/anuke/mindustry/core/UI.java index ca5dafb8ed..0d2591398f 100644 --- a/core/src/io/anuke/mindustry/core/UI.java +++ b/core/src/io/anuke/mindustry/core/UI.java @@ -68,6 +68,7 @@ public class UI implements ApplicationListener, Loadable{ public DeployDialog deploy; public TechTreeDialog tech; public MinimapDialog minimap; + public ModsDialog mods; public Cursor drillCursor, unloadCursor; @@ -222,6 +223,7 @@ public class UI implements ApplicationListener, Loadable{ deploy = new DeployDialog(); tech = new TechTreeDialog(); minimap = new MinimapDialog(); + mods = new ModsDialog(); Group group = Core.scene.root; @@ -410,6 +412,18 @@ public class UI implements ApplicationListener, Loadable{ dialog.show(); } + public void showOkText(String title, String text, Runnable confirmed){ + FloatingDialog dialog = new FloatingDialog(title); + dialog.cont.add(text).width(500f).wrap().pad(4f).get().setAlignment(Align.center, Align.center); + dialog.buttons.defaults().size(200f, 54f).pad(2f); + dialog.setFillParent(false); + dialog.buttons.addButton("$ok", () -> { + dialog.hide(); + confirmed.run(); + }); + dialog.show(); + } + public String formatAmount(int number){ if(number >= 1000000){ return Strings.fixed(number / 1000000f, 1) + "[gray]mil[]"; diff --git a/core/src/io/anuke/mindustry/mod/Mods.java b/core/src/io/anuke/mindustry/mod/Mods.java index fcbebd7921..8f7017adf2 100644 --- a/core/src/io/anuke/mindustry/mod/Mods.java +++ b/core/src/io/anuke/mindustry/mod/Mods.java @@ -5,15 +5,18 @@ import io.anuke.arc.collection.*; import io.anuke.arc.files.*; import io.anuke.arc.function.*; import io.anuke.arc.util.*; -import io.anuke.mindustry.io.*; +import io.anuke.arc.util.serialization.*; +import java.io.*; import java.net.*; import static io.anuke.mindustry.Vars.*; public class Mods{ + private Json json = new Json(); private Array loaded = new Array<>(); private ObjectMap, 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(). */ @@ -28,13 +31,44 @@ public class Mods{ 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); + } + } + + /** Removes a mod file and marks it for requiring a restart. */ + public void removeMod(LoadedMod mod){ + 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")) continue; + if(!file.extension().equals("jar") && !file.extension().equals("zip")) continue; try{ - loaded.add(loadmod(file)); + loaded.add(loadMod(file)); }catch(IllegalArgumentException ignored){ }catch(Exception e){ Log.err("Failed to load plugin file {0}. Skipping.", file); @@ -55,22 +89,28 @@ public class Mods{ loaded.each(p -> p.mod != null, p -> cons.accept(p.mod)); } - private LoadedMod loadmod(FileHandle jar) throws Exception{ + /** 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); 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); - throw new IllegalArgumentException(); + throw new IllegalArgumentException("No mod.json found."); } - ModMeta meta = JsonIO.read(ModMeta.class, metaf.readString()); + 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[]{jar.file().toURI().toURL()}, ClassLoader.getSystemClassLoader()); Class main = classLoader.loadClass(mainClass); metas.put(main, meta); @@ -92,6 +132,8 @@ public class Mods{ public final @Nullable Mod mod; /** 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; diff --git a/core/src/io/anuke/mindustry/ui/dialogs/ModsDialog.java b/core/src/io/anuke/mindustry/ui/dialogs/ModsDialog.java new file mode 100644 index 0000000000..7664f9ddc9 --- /dev/null +++ b/core/src/io/anuke/mindustry/ui/dialogs/ModsDialog.java @@ -0,0 +1,82 @@ +package io.anuke.mindustry.ui.dialogs; + +import io.anuke.arc.*; +import io.anuke.mindustry.gen.*; +import io.anuke.mindustry.mod.Mods.*; +import io.anuke.mindustry.ui.*; + +import java.io.*; + +import static io.anuke.mindustry.Vars.*; + +public class ModsDialog extends FloatingDialog{ + + public ModsDialog(){ + super("$mods"); + addCloseButton(); + shown(this::setup); + + hidden(() -> { + if(mods.requiresRestart()){ + ui.showOkText("$mods", "$mod.requiresrestart", () -> { + Core.app.exit(); + }); + } + }); + } + + void setup(){ + cont.clear(); + cont.defaults().width(520f).pad(4); + if(!mods.all().isEmpty()){ + cont.pane(table -> { + table.margin(10f).top(); + for(LoadedMod mod : mods.all()){ + table.table(Styles.black6, t -> { + t.defaults().pad(2).left().top(); + t.margin(14f).left(); + t.table(title -> { + title.left(); + title.add("[accent]" + mod.meta.name + "[lightgray] v" + mod.meta.version); + title.add().growX(); + + title.addImageButton(Icon.trash16Small, Styles.cleari, () -> ui.showConfirm("$confirm", "$mod.remove.confirm", () -> { + mods.removeMod(mod); + setup(); + })).size(50f); + }).growX().left().padTop(-14f).padRight(-14f); + + t.row(); + if(mod.meta.author != null){ + t.add(Core.bundle.format("mod.author", mod.meta.author)); + t.row(); + } + if(mod.meta.description != null){ + t.labelWrap("[lightgray]" + mod.meta.description).growX(); + t.row(); + } + + }).width(500f); + table.row(); + } + }); + + }else{ + cont.table(Styles.black6, t -> t.add("$mods.none")).height(80f); + } + + cont.row(); + + cont.addImageTextButton("$mod.import", Icon.add, () -> { + platform.showFileChooser(true, "zip", file -> { + try{ + mods.importMod(file); + setup(); + }catch(IOException e){ + ui.showException(e); + e.printStackTrace(); + } + }); + }).margin(12f).width(500f); + } +} \ No newline at end of file diff --git a/core/src/io/anuke/mindustry/ui/fragments/MenuFragment.java b/core/src/io/anuke/mindustry/ui/fragments/MenuFragment.java index 946723983a..9afcabbac7 100644 --- a/core/src/io/anuke/mindustry/ui/fragments/MenuFragment.java +++ b/core/src/io/anuke/mindustry/ui/fragments/MenuFragment.java @@ -163,6 +163,7 @@ public class MenuFragment extends Fragment{ ), new Buttoni("$editor", Icon.editorSmall, ui.maps::show), steam ? new Buttoni("$workshop", Icon.saveSmall, platform::openWorkshop) : null, + new Buttoni("$mods", Icon.wikiSmall, ui.mods::show), new Buttoni("$settings", Icon.toolsSmall, ui.settings::show), new Buttoni("$about.button", Icon.infoSmall, ui.about::show), new Buttoni("$quit", Icon.exitSmall, Core.app::exit)