From e043f4bb6632da8de34c0bf3f425cca81688e8af Mon Sep 17 00:00:00 2001
From: Anuken <arnukren@gmail.com>
Date: Sat, 14 Dec 2019 18:19:02 -0500
Subject: [PATCH] API cleanup

---
 .../io/anuke/mindustry/core/NetServer.java    |   2 +-
 core/src/io/anuke/mindustry/mod/Mods.java     | 191 +++++-------------
 .../mindustry/ui/dialogs/ModsDialog.java      |   4 +-
 .../ui/fragments/PlacementFragment.java       |   3 +-
 .../mindustry/desktop/DesktopLauncher.java    |   8 +-
 .../mindustry/tools/ScriptStubGenerator.java  |   2 +-
 6 files changed, 63 insertions(+), 147 deletions(-)

diff --git a/core/src/io/anuke/mindustry/core/NetServer.java b/core/src/io/anuke/mindustry/core/NetServer.java
index 72d141576b..218cbd2d83 100644
--- a/core/src/io/anuke/mindustry/core/NetServer.java
+++ b/core/src/io/anuke/mindustry/core/NetServer.java
@@ -219,7 +219,7 @@ public class NetServer implements ApplicationListener{
 
     @Override
     public void init(){
-        mods.each(mod -> mod.registerClientCommands(clientCommands));
+        mods.eachClass(mod -> mod.registerClientCommands(clientCommands));
     }
 
     private void registerCommands(){
diff --git a/core/src/io/anuke/mindustry/mod/Mods.java b/core/src/io/anuke/mindustry/mod/Mods.java
index 737aa0ede6..0a35c0d089 100644
--- a/core/src/io/anuke/mindustry/mod/Mods.java
+++ b/core/src/io/anuke/mindustry/mod/Mods.java
@@ -33,13 +33,12 @@ public class Mods implements Loadable{
     private @Nullable Scripts scripts;
     private ContentParser parser = new ContentParser();
     private ObjectMap<String, Array<FileHandle>> bundles = new ObjectMap<>();
-    private ObjectSet<String> specialFolders = ObjectSet.with("bundles", "sprites");
+    private ObjectSet<String> specialFolders = ObjectSet.with("bundles", "sprites", "sprites-override");
 
     private int totalSprites;
     private MultiPacker packer;
 
-    private Array<LoadedMod> loaded = new Array<>();
-    private Array<LoadedMod> disabled = new Array<>();
+    private Array<LoadedMod> mods = new Array<>();
     private ObjectMap<Class<?>, ModMeta> metas = new ObjectMap<>();
     private boolean requiresReload;
 
@@ -53,19 +52,19 @@ public class Mods implements Loadable{
 
     /** Returns a list of files per mod subdirectory. */
     public void listFiles(String directory, Cons2<LoadedMod, FileHandle> cons){
-        for(LoadedMod mod : loaded){
+        eachEnabled(mod -> {
             FileHandle file = mod.root.child(directory);
             if(file.exists()){
                 for(FileHandle child : file.list()){
                     cons.get(mod, child);
                 }
             }
-        }
+        });
     }
 
     /** @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 != null && l.mod.getClass() == type);
+        return mods.find(m -> m.enabled() && m.main != null && m.main.getClass() == type);//loaded.find(l -> l.mod != null && l.mod.getClass() == type);
     }
 
     /** Imports an external mod file.*/
@@ -77,7 +76,7 @@ public class Mods implements Loadable{
 
         file.copyTo(dest);
         try{
-            loaded.add(loadMod(dest));
+            mods.add(loadMod(dest));
             requiresReload = true;
         }catch(IOException e){
             dest.delete();
@@ -91,19 +90,19 @@ public class Mods implements Loadable{
     /** Repacks all in-game sprites. */
     @Override
     public void loadAsync(){
-        if(loaded.isEmpty()) return;
+        if(!mods.contains(LoadedMod::enabled)) return;
         Time.mark();
 
         packer = new MultiPacker();
 
-        for(LoadedMod mod : loaded){
+        eachEnabled(mod -> {
             Array<FileHandle> sprites = mod.root.child("sprites").findAll(f -> f.extension().equals("png"));
             Array<FileHandle> overrides = mod.root.child("sprites-override").findAll(f -> f.extension().equals("png"));
             packSprites(sprites, mod, true);
             packSprites(overrides, mod, false);
             Log.debug("Packed {0} images for mod '{1}'.", sprites.size + overrides.size, mod.meta.name);
             totalSprites += sprites.size + overrides.size;
-        }
+        });
 
         for(AtlasRegion region : Core.atlas.getRegions()){
             PageType type = getPage(region);
@@ -198,8 +197,7 @@ public class Mods implements Loadable{
             ui.showErrorMessage("$mod.delete.error");
             return;
         }
-        loaded.remove(mod);
-        disabled.remove(mod);
+        mods.remove(mod);
         requiresReload = true;
     }
 
@@ -225,11 +223,7 @@ public class Mods implements Loadable{
             Log.debug("[Mods] Loading mod {0}", file);
             try{
                 LoadedMod mod = loadMod(file);
-                if(mod.enabled() || headless){
-                    loaded.add(mod);
-                }else{
-                    disabled.add(mod);
-                }
+                mods.add(mod);
             }catch(Exception e){
                 Log.err("Failed to load mod file {0}. Skipping.", file);
                 Log.err(e);
@@ -240,11 +234,7 @@ public class Mods implements Loadable{
         for(FileHandle file : platform.getWorkshopContent(LoadedMod.class)){
             try{
                 LoadedMod mod = loadMod(file);
-                if(mod.enabled()){
-                    loaded.add(mod);
-                }else{
-                    disabled.add(mod);
-                }
+                mods.add(mod);
                 mod.addSteamID(file.name());
             }catch(Exception e){
                 Log.err("Failed to load mod workshop file {0}. Skipping.", file);
@@ -252,28 +242,24 @@ public class Mods implements Loadable{
             }
         }
 
-        resolveDependencies();
+        resolveModState();
 
         //sort mods to make sure servers handle them properly.
-        loaded.sort(Structs.comparing(m -> m.name));
+        mods.sort(Structs.comparing(m -> m.name));
 
         buildFiles();
     }
 
-    private void resolveDependencies(){
-        Array<LoadedMod> incompatible = loaded.select(m -> !m.isSupported());
-        loaded.removeAll(incompatible);
-        disabled.addAll(incompatible);
+    private void resolveModState(){
+        mods.each(this::updateDependencies);
 
-        for(LoadedMod mod : Array.<LoadedMod>withArrays(loaded, disabled)){
-            updateDependencies(mod);
+        for(LoadedMod mod : mods){
+            mod.state =
+                !mod.isSupported() ? ModState.unsupported :
+                mod.hasUnmetDependencies() ? ModState.missingDependencies :
+                !mod.enabled() /*TODO check disabled state!*/ ? ModState.disabled :
+                ModState.enabled;
         }
-
-        disabled.addAll(loaded.select(LoadedMod::hasUnmetDependencies));
-        loaded.removeAll(LoadedMod::hasUnmetDependencies);
-        disabled.each(mod -> setEnabled(mod, false));
-        disabled.distinct();
-        loaded.distinct();
     }
 
     private void updateDependencies(LoadedMod mod){
@@ -298,16 +284,16 @@ public class Mods implements Loadable{
     private Array<LoadedMod> orderedMods(){
         ObjectSet<LoadedMod> visited = new ObjectSet<>();
         Array<LoadedMod> result = new Array<>();
-        for(LoadedMod mod : loaded){
+        eachEnabled(mod -> {
             if(!visited.contains(mod)){
                 topoSort(mod, result, visited);
             }
-        }
+        });
         return result;
     }
 
     private LoadedMod locateMod(String name){
-        return loaded.find(mod -> mod.name.equals(name));
+        return mods.find(mod -> mod.enabled() && mod.name.equals(name));
     }
 
     private void buildFiles(){
@@ -357,8 +343,7 @@ public class Mods implements Loadable{
         //TODO make it less epic
         Core.atlas = new TextureAtlas(Core.files.internal("sprites/sprites.atlas"));
 
-        loaded.clear();
-        disabled.clear();
+        mods.clear();
         load();
         Sounds.dispose();
         Sounds.load();
@@ -392,7 +377,7 @@ public class Mods implements Loadable{
         Time.mark();
 
         try{
-            for(LoadedMod mod : loaded){
+            eachEnabled(mod -> {
                 if(mod.root.child("scripts").exists()){
                     content.setCurrentMod(mod);
                     mod.scripts = mod.root.child("scripts").findAll(f -> f.extension().equals("js"));
@@ -414,7 +399,7 @@ public class Mods implements Loadable{
                         }
                     }
                 }
-            }
+            });
         }finally{
             content.setCurrentMod(null);
         }
@@ -486,41 +471,18 @@ public class Mods implements Loadable{
         parser.markError(content, error);
     }
 
-    /** @return all loaded mods. */
-    public Array<LoadedMod> all(){
-        return loaded;
-    }
-
-    /** @return all disabled mods. */
-    public Array<LoadedMod> disabled(){
-        return disabled;
-    }
-
-    /** @return a list of mod names only, without versions. */
-    public Array<String> getModNames(){
-        return loaded.select(l -> !l.meta.hidden).map(l -> l.name + ":" + l.meta.version);
-    }
-
     /** @return a list of mods and versions, in the format name:version. */
     public Array<String> getModStrings(){
-        return loaded.select(l -> !l.meta.hidden).map(l -> l.name + ":" + l.meta.version);
+        return mods.select(l -> !l.meta.hidden && l.enabled()).map(l -> l.name + ":" + l.meta.version);
     }
 
     /** Makes a mod enabled or disabled. shifts it.*/
     public void setEnabled(LoadedMod mod, boolean enabled){
         if(mod.enabled() != enabled){
             Core.settings.putSave("mod-" + mod.name + "-enabled", enabled);
-            Core.settings.save();
             requiresReload = true;
-            if(!enabled){
-                loaded.remove(mod);
-                if(!disabled.contains(mod)) disabled.add(mod);
-            }else{
-                if(!loaded.contains(mod)) loaded.add(mod);
-                disabled.remove(mod);
-            }
-            loaded.each(this::updateDependencies);
-            disabled.each(this::updateDependencies);
+            mod.state = enabled ? ModState.enabled : ModState.disabled;
+            mods.each(this::updateDependencies);
         }
     }
 
@@ -537,38 +499,19 @@ public class Mods implements Loadable{
         return result;
     }
 
-    /** Iterates through each mod with a main class.*/
-    public void each(Cons<Mod> cons){
-        loaded.each(p -> p.mod != null, p -> contextRun(p, () -> cons.get(p.mod)));
+    public Array<LoadedMod> list(){
+        return mods;
     }
 
-    /*
-    public void handleError(Throwable t, LoadedMod mod){
-        Array<Throwable> causes = Strings.getCauses(t);
-        Content content = null;
-        for(Throwable e : causes){
-            if(e instanceof ModLoadException && ((ModLoadException) e).content != null){
-                content = ((ModLoadException) e).content;
-            }
-        }
+    /** Iterates through each mod with a main class. */
+    public void eachClass(Cons<Mod> cons){
+        mods.each(p -> p.main != null, p -> contextRun(p, () -> cons.get(p.main)));
+    }
 
-        String realCause = "<???>";
-        for(int i = causes.size -1 ; i >= 0; i--){
-            if(causes.get(i).getMessage() != null){
-                realCause = causes.get(i).getMessage();
-                break;
-            }
-        }
-
-        setEnabled(mod, false);
-
-        if(content != null){
-            throw new ModLoadException(Strings.format("Error loading '{0}' from mod '{1}' ({2}):\n{3}",
-                content, mod.meta.name, content.sourceFile == null ? "<unknown file>" : content.sourceFile.name(), realCause), content, t);
-        }else{
-            throw new ModLoadException("Error loading mod " + mod.meta.name, t);
-        }
-    }*/
+    /** Iterates through each enabled mod. */
+    public void eachEnabled(Cons<LoadedMod> cons){
+        mods.each(LoadedMod::enabled, cons);
+    }
 
     public void contextRun(LoadedMod mod, Runnable run){
         try{
@@ -597,7 +540,7 @@ public class Mods implements Loadable{
         String mainClass = meta.main == null ? camelized.toLowerCase() + "." + camelized + "Mod" : meta.main;
         String baseName = meta.name.toLowerCase().replace(" ", "-");
 
-        if(loaded.contains(m -> m.name.equals(baseName)) || disabled.contains(m -> m.name.equals(baseName))){
+        if(mods.contains(m -> m.name.equals(baseName))){
             throw new IllegalArgumentException("A mod with the name '" + baseName + "' is already imported.");
         }
 
@@ -641,7 +584,7 @@ public class Mods implements Loadable{
         /** 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;
+        public final @Nullable Mod main;
         /** Internal mod name. Used for textures. */
         public final String name;
         /** This mod's metadata. */
@@ -652,17 +595,19 @@ public class Mods implements Loadable{
         public Array<String> missingDependencies = new Array<>();
         /** Script files to run. */
         public Array<FileHandle> scripts = new Array<>();
+        /** Current state of this mod. */
+        public ModState state = ModState.disabled;
 
-        public LoadedMod(FileHandle file, FileHandle root, Mod mod, ModMeta meta){
+        public LoadedMod(FileHandle file, FileHandle root, Mod main, ModMeta meta){
             this.root = root;
             this.file = file;
-            this.mod = mod;
+            this.main = main;
             this.meta = meta;
             this.name = meta.name.toLowerCase().replace(" ", "-");
         }
 
         public boolean enabled(){
-            return Core.settings.getBool("mod-" + name + "-enabled", true);
+            return state == ModState.enabled;
         }
 
         public boolean hasUnmetDependencies(){
@@ -760,37 +705,11 @@ public class Mods implements Loadable{
         }
     }
 
-    /** Thrown when an error occurs while loading a mod.
-    public static class ModLoadException extends RuntimeException{
-        public Content content;
-        public LoadedMod mod;
-
-        public ModLoadException(String message, Throwable cause){
-            super(message, cause);
-        }
-
-        public ModLoadException(String message, @Nullable Content content, Throwable cause){
-            super(message, cause);
-            this.content = content;
-            if(content != null){
-                this.mod = content.mod;
-            }
-        }
-
-        public ModLoadException(@Nullable Content content, Throwable cause){
-            super(cause);
-            this.content = content;
-            if(content != null){
-                this.mod = content.mod;
-            }
-        }
-
-        public ModLoadException(String message, @Nullable Content content){
-            super(message);
-            this.content = content;
-            if(content != null){
-                this.mod = content.mod;
-            }
-        }
-    }*/
+    public enum ModState{
+        enabled,
+        disabled,
+        missingDependencies,
+        unsupported,
+        contentErrors
+    }
 }
diff --git a/core/src/io/anuke/mindustry/ui/dialogs/ModsDialog.java b/core/src/io/anuke/mindustry/ui/dialogs/ModsDialog.java
index efbb02693a..a9a39816fb 100644
--- a/core/src/io/anuke/mindustry/ui/dialogs/ModsDialog.java
+++ b/core/src/io/anuke/mindustry/ui/dialogs/ModsDialog.java
@@ -75,7 +75,7 @@ public class ModsDialog extends FloatingDialog{
         hidden(() -> {
             if(mods.requiresReload()){
                 ui.loadAnd("$reloading", () -> {
-                    mods.all().each(mod -> {
+                    mods.eachEnabled(mod -> {
                         if(mod.hasUnmetDependencies()){
                             ui.showErrorMessage(Core.bundle.format("mod.nowdisabled", mod.name, mod.missingDependencies.toString(", ")));
                         }
@@ -107,7 +107,7 @@ public class ModsDialog extends FloatingDialog{
         cont.defaults().width(mobile ? 500 : 560f).pad(4);
         cont.add("$mod.reloadrequired").visible(mods::requiresReload).center().get().setAlignment(Align.center);
         cont.row();
-        if(!(mods.all().isEmpty() && mods.disabled().isEmpty())){
+        if(!mods.list().isEmpty()){
             cont.pane(table -> {
                 table.margin(10f).top();
                 Array<LoadedMod> all = Array.withArrays(mods.all(), mods.disabled());
diff --git a/core/src/io/anuke/mindustry/ui/fragments/PlacementFragment.java b/core/src/io/anuke/mindustry/ui/fragments/PlacementFragment.java
index 5d8e5ff0e5..54ebe7b814 100644
--- a/core/src/io/anuke/mindustry/ui/fragments/PlacementFragment.java
+++ b/core/src/io/anuke/mindustry/ui/fragments/PlacementFragment.java
@@ -10,6 +10,7 @@ import io.anuke.arc.scene.style.*;
 import io.anuke.arc.scene.ui.*;
 import io.anuke.arc.scene.ui.layout.*;
 import io.anuke.arc.util.*;
+import io.anuke.mindustry.content.*;
 import io.anuke.mindustry.entities.traits.BuilderTrait.*;
 import io.anuke.mindustry.entities.type.*;
 import io.anuke.mindustry.game.EventType.*;
@@ -461,6 +462,6 @@ public class PlacementFragment extends Fragment{
 
     /** Returns the block currently being hovered over in the world. */
     Block tileDisplayBlock(){
-        return hoverTile == null ? null : hoverTile.block().synthetic() ? hoverTile.block() : hoverTile.drop() != null ? hoverTile.overlay().itemDrop != null ? hoverTile.overlay() : hoverTile.floor() : null;
+        return hoverTile == null ? null : hoverTile.block().synthetic() ? hoverTile.block() : hoverTile.drop() != null  && hoverTile.block() == Blocks.air ? hoverTile.overlay().itemDrop != null ? hoverTile.overlay() : hoverTile.floor() : null;
     }
 }
diff --git a/desktop/src/io/anuke/mindustry/desktop/DesktopLauncher.java b/desktop/src/io/anuke/mindustry/desktop/DesktopLauncher.java
index 6f63976bf7..1a4b30d37a 100644
--- a/desktop/src/io/anuke/mindustry/desktop/DesktopLauncher.java
+++ b/desktop/src/io/anuke/mindustry/desktop/DesktopLauncher.java
@@ -17,7 +17,6 @@ import io.anuke.mindustry.core.GameState.*;
 import io.anuke.mindustry.core.*;
 import io.anuke.mindustry.desktop.steam.*;
 import io.anuke.mindustry.game.EventType.*;
-import io.anuke.mindustry.mod.Mods.*;
 import io.anuke.mindustry.net.*;
 import io.anuke.mindustry.net.Net.*;
 import io.anuke.mindustry.type.*;
@@ -195,12 +194,9 @@ public class DesktopLauncher extends ClientLauncher{
         boolean fbgp = badGPU;
 
         CrashSender.send(e, file -> {
-            Array<Throwable> causes = Strings.getCauses(e);
-            Throwable fc = causes.find(t -> t instanceof ModLoadException);
-            if(fc == null) fc = Strings.getFinalCause(e);
-            Throwable cause = fc;
+            Throwable fc = Strings.getFinalCause(e);
             if(!fbgp){
-                dialog.get(() -> message("A crash has occured. It has been saved in:\n" + file.getAbsolutePath() + "\n" + cause.getClass().getSimpleName().replace("Exception", "") + (cause.getMessage() == null ? "" : ":\n" + cause.getMessage())));
+                dialog.get(() -> message("A crash has occured. It has been saved in:\n" + file.getAbsolutePath() + "\n" + fc.getClass().getSimpleName().replace("Exception", "") + (fc.getMessage() == null ? "" : ":\n" + fc.getMessage())));
             }
         });
     }
diff --git a/tools/src/io/anuke/mindustry/tools/ScriptStubGenerator.java b/tools/src/io/anuke/mindustry/tools/ScriptStubGenerator.java
index ac29c6adb4..5a33c507c9 100644
--- a/tools/src/io/anuke/mindustry/tools/ScriptStubGenerator.java
+++ b/tools/src/io/anuke/mindustry/tools/ScriptStubGenerator.java
@@ -25,7 +25,7 @@ public class ScriptStubGenerator{
         Array<String> blacklist = Array.with("plugin", "mod", "net", "io", "tools");
         Array<String> nameBlacklist = Array.with("ClientLauncher", "NetClient", "NetServer", "ClassAccess");
         Array<Class<?>> whitelist = Array.with(Draw.class, Fill.class, Lines.class, Core.class, TextureAtlas.class, TextureRegion.class, Time.class, System.class, PrintStream.class,
-            AtlasRegion.class, String.class, Mathf.class, Angles.class, Color.class, Runnable.class, Object.class, Icon.class, Tex.class, Sounds.class, Musics.class, Call.class);
+            AtlasRegion.class, String.class, Mathf.class, Angles.class, Color.class, Runnable.class, Object.class, Icon.class, Tex.class, Sounds.class, Musics.class, Call.class, Texture.class, TextureData.class, Pixmap.class);
         Array<String> nopackage = Array.with("java.lang", "java");
 
         String fileTemplate = "package io.anuke.mindustry.mod;\n" +