diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index e98615e268..bead846691 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -17,21 +17,6 @@ jobs: java-version: 14 - name: Set env run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - - name: Create artifacts - run: | - ./gradlew desktop:dist server:dist core:javadoc -Pbuildversion=${RELEASE_VERSION:1} - - name: Update docs - run: | - cd ../ - git config --global user.email "cli@github.com" - git config --global user.name "Github Actions" - git clone --depth=1 https://github.com/MindustryGame/docs.git - cp -a Mindustry/core/build/docs/javadoc/. docs/ - cd docs - git add . - git commit -m "Update ${RELEASE_VERSION:1}" - git push https://Anuken:${{ secrets.API_TOKEN_GITHUB }}@github.com/MindustryGame/docs - cd ../Mindustry - name: Add Arc release run: | git clone --depth=1 --branch=master https://github.com/Anuken/Arc ../Arc @@ -39,6 +24,24 @@ jobs: git tag ${RELEASE_VERSION} git push https://Anuken:${{ secrets.API_TOKEN_GITHUB }}@github.com/Anuken/Arc ${RELEASE_VERSION}; cd ../Mindustry + - name: Create artifacts + run: | + ./gradlew desktop:dist server:dist core:mergedJavadoc -Pbuildversion=${RELEASE_VERSION:1} + - name: Update docs + run: | + cd ../ + git config --global user.email "cli@github.com" + git config --global user.name "Github Actions" + git clone --depth=1 https://github.com/MindustryGame/docs.git + cd docs + find . -maxdepth 1 ! -name ".git" ! -name . -exec rm -r {} \; + cd ../ + cp -a Mindustry/core/build/javadoc/. docs/ + cd docs + git add . + git commit -m "Update ${RELEASE_VERSION:1}" + git push https://Anuken:${{ secrets.API_TOKEN_GITHUB }}@github.com/MindustryGame/docs + cd ../Mindustry - name: Update F-Droid build string run: | git clone --depth=1 --branch=master https://github.com/Anuken/MindustryBuilds ../MindustryBuilds diff --git a/build.gradle b/build.gradle index dd83479eea..1c1ac09216 100644 --- a/build.gradle +++ b/build.gradle @@ -335,6 +335,25 @@ project(":core"){ annotationProcessor 'com.github.Anuken:jabel:34e4c172e65b3928cd9eabe1993654ea79c409cd' } + + afterEvaluate{ + task mergedJavadoc(type: Javadoc){ + def blacklist = [project(":android"), project(":ios"), project(":desktop"), project(":server"), project(":annotations")] + + source rootProject.subprojects.collect{ project -> + if(!blacklist.contains(project) && project.hasProperty("sourceSets")){ + return project.sourceSets.main.allJava + } + } + + classpath = files(rootProject.subprojects.collect { project -> + if(!blacklist.contains(project) && project.hasProperty("sourceSets")){ + return project.sourceSets.main.compileClasspath + } + }) + destinationDir = new File(buildDir, 'javadoc') + } + } } project(":server"){ diff --git a/core/assets/bundles/bundle.properties b/core/assets/bundles/bundle.properties index 7ce752f08b..12687c54ad 100644 --- a/core/assets/bundles/bundle.properties +++ b/core/assets/bundles/bundle.properties @@ -41,7 +41,7 @@ be.ignore = Ignore be.noupdates = No updates found. be.check = Check for updates -mod.featured.dialog.title = Mod Browser (WIP) +mods.browser = Mod Browser mods.browser.selected = Selected mod mods.browser.add = Install mods.browser.reinstall = Reinstall diff --git a/core/src/mindustry/Vars.java b/core/src/mindustry/Vars.java index b0db5ef272..32bd7103dc 100644 --- a/core/src/mindustry/Vars.java +++ b/core/src/mindustry/Vars.java @@ -50,8 +50,8 @@ public class Vars implements Loadable{ public static final Charset charset = Charset.forName("UTF-8"); /** main application name, capitalized */ public static final String appName = "Mindustry"; - /** URL for itch.io donations. */ - public static final String donationURL = "https://anuke.itch.io/mindustry/purchase"; + /** Github API URL. */ + public static final String ghApi = "https://api.github.com"; /** URL for discord invite. */ public static final String discordURL = "https://discord.gg/mindustry"; /** URL for sending crash reports to */ diff --git a/core/src/mindustry/mod/Mods.java b/core/src/mindustry/mod/Mods.java index fa5aba1e14..6c4e853cf9 100644 --- a/core/src/mindustry/mod/Mods.java +++ b/core/src/mindustry/mod/Mods.java @@ -667,16 +667,22 @@ public class Mods implements Loadable{ Mod mainMod; Fi mainFile = zip; - String[] path = (mainClass.replace('.', '/') + ".class").split("/"); - for(String str : path){ - if(!str.isEmpty()){ - mainFile = mainFile.child(str); + if(android){ + mainFile = mainFile.child("classes.dex"); + }else{ + String[] path = (mainClass.replace('.', '/') + ".class").split("/"); + for(String str : path){ + if(!str.isEmpty()){ + mainFile = mainFile.child(str); + } } } //make sure the main class exists before loading it; if it doesn't just don't put it there - if(mainFile.exists() && Core.settings.getBool("mod-" + baseName + "-enabled", true) && Version.isAtLeast(meta.minGameVersion) && meta.getMinMajor() >= 105){ - //mobile versions don't support class mods + //if the mod is explicitly marked as java, try loading it anyway + if((mainFile.exists() || meta.java) && + Core.settings.getBool("mod-" + baseName + "-enabled", true) && Version.isAtLeast(meta.minGameVersion) && meta.getMinMajor() >= 105){ + if(ios){ throw new IllegalArgumentException("Java class mods are not supported on iOS."); } @@ -746,6 +752,11 @@ public class Mods implements Loadable{ this.name = meta.name.toLowerCase().replace(" ", "-"); } + /** @return whether this is a java class mod. */ + public boolean isJava(){ + return meta.java || main != null; + } + @Nullable public String getRepo(){ return Core.settings.getString("mod-" + name + "-repo", meta.repo); @@ -867,6 +878,8 @@ public class Mods implements Loadable{ public Seq dependencies = Seq.with(); /** Hidden mods are only server-side or client-side, and do not support adding new content. */ public boolean hidden; + /** If true, this mod should be loaded as a Java class mod. This is technically optional, but highly recommended. */ + public boolean java; public String displayName(){ return displayName == null ? Strings.stripColors(name) : displayName; diff --git a/core/src/mindustry/ui/dialogs/ModsDialog.java b/core/src/mindustry/ui/dialogs/ModsDialog.java index c08405a6b8..20a54e2d1a 100644 --- a/core/src/mindustry/ui/dialogs/ModsDialog.java +++ b/core/src/mindustry/ui/dialogs/ModsDialog.java @@ -42,6 +42,10 @@ public class ModsDialog extends BaseDialog{ buttons.button("@mods.guide", Icon.link, () -> Core.app.openURI(modGuideURL)).size(210, 64f); + if(!mobile){ + buttons.button("@mods.openfolder", Icon.link, () -> Core.app.openFolder(modDirectory.absolutePath())); + } + shown(this::setup); if(mobile){ onResize(this::setup); @@ -169,93 +173,15 @@ public class ModsDialog extends BaseDialog{ dialog.hide(); ui.showTextInput("@mod.import.github", "", 64, Core.settings.getString("lastmod", ""), text -> { + //clean up the text in case somebody inputs a URL or adds random spaces + text = text.trim().replace(" ", ""); + if(text.startsWith("https://github.com/")) text = text.substring("https://github.com/".length()); + Core.settings.put("lastmod", text); - - githubImportMod(text); + //there's no good way to know if it's a java mod here, so assume it's not + githubImportMod(text, false); }); }).margin(12f); - - t.row(); - - t.button("@mod.featured.dialog.title", Icon.star, bstyle, () -> { - Runnable[] rebuildBrowser = {null}; - dialog.hide(); - BaseDialog browser = new BaseDialog("$mod.featured.dialog.title"); - browser.cont.table(table -> { - table.left(); - table.image(Icon.zoom); - table.field(searchtxt, res -> { - searchtxt = res; - rebuildBrowser[0].run(); - }).growX().get(); - table.button(Icon.list, Styles.clearPartiali, 32f, () -> { - orderDate = !orderDate; - rebuildBrowser[0].run(); - }).update(b -> b.getStyle().imageUp = (orderDate ? Icon.list : Icon.star)).size(40f).get() - .addListener(new Tooltip(tip -> tip.label(() -> orderDate ? "@mods.browser.sortdate" : "@mods.browser.sortstars").left())); - }).fillX().padBottom(4); - - browser.cont.row(); - - browser.cont.pane(tablebrow -> { - tablebrow.margin(10f).top(); - rebuildBrowser[0] = () -> { - tablebrow.clear(); - tablebrow.add("@loading"); - - getModList(rlistings -> { - tablebrow.clear(); - - Seq listings = rlistings; - if(!orderDate){ - listings = rlistings.copy(); - listings.sortComparing(m1 -> -m1.stars); - } - - for(ModListing mod : listings){ - if((mod.hasJava && Vars.ios) || !searchtxt.isEmpty() && !mod.repo.toLowerCase().contains(searchtxt.toLowerCase()) || (Vars.ios && mod.hasScripts)) continue; - - tablebrow.button(btn -> { - btn.top().left(); - btn.margin(12f); - btn.table(con -> { - con.left(); - con.add("[accent]" + mod.name + "[white]\n[lightgray]Author:[] " + mod.author + "\n[lightgray]\uE809 " + mod.stars + - (Version.isAtLeast(mod.minGameVersion) ? "" : "\n" + Core.bundle.format("mod.requiresversion", mod.minGameVersion))) - .width(388f).wrap().growX().pad(0f, 6f, 0f, 6f).left().labelAlign(Align.left); - con.add().growX().pad(0f, 6f, 0f, 6f); - }).fillY().growX().pad(0f, 6f, 0f, 6f); - }, Styles.modsb, () -> { - var sel = new BaseDialog(mod.name); - sel.cont.add(mod.description).width(mobile ? 400f : 500f).wrap().pad(4f).labelAlign(Align.center, Align.left); - sel.buttons.defaults().size(150f, 54f).pad(2f); - sel.setFillParent(false); - sel.buttons.button("@back", Icon.left, () -> { - sel.clear(); - sel.hide(); - }); - - var found = mods.list().find(l -> mod.repo != null && mod.repo.equals(l.getRepo())); - sel.buttons.button(found == null ? "@mods.browser.add" : "@mods.browser.reinstall", Icon.download, () -> { - sel.hide(); - githubImportMod(mod.repo); - }); - sel.buttons.button("@mods.github.open", Icon.link, () -> { - Core.app.openURI("https://github.com/" + mod.repo); - }); - sel.keyDown(KeyCode.escape, sel::hide); - sel.keyDown(KeyCode.back, sel::hide); - sel.show(); - }).width(460f).growX().left().fillY(); - tablebrow.row(); - } - }); - }; - rebuildBrowser[0].run(); - }).get().setScrollingDisabled(true, false); - browser.addCloseButton(); - browser.show(); - }).margin(12f); }); dialog.addCloseButton(); @@ -263,9 +189,7 @@ public class ModsDialog extends BaseDialog{ }).margin(margin); - if(!mobile){ - buttons.button("@mods.openfolder", Icon.link, style, () -> Core.app.openFolder(modDirectory.absolutePath())).margin(margin); - } + buttons.button("@mods.browser", Icon.menu, style, this::showModBrowser).margin(margin); }).width(w); cont.row(); @@ -377,7 +301,7 @@ public class ModsDialog extends BaseDialog{ boolean showImport = !mod.hasSteamID(); dialog.buttons.button("@mods.github.open", Icon.link, () -> Core.app.openURI("https://github.com/" + mod.getRepo())); if(mobile && showImport) dialog.buttons.row(); - if(showImport) dialog.buttons.button("@mods.browser.reinstall", Icon.download, () -> githubImportMod(mod.getRepo())); + if(showImport) dialog.buttons.button("@mods.browser.reinstall", Icon.download, () -> githubImportMod(mod.getRepo(), mod.isJava())); } //TODO improve this menu later @@ -432,6 +356,92 @@ public class ModsDialog extends BaseDialog{ dialog.show(); } + private void showModBrowser(){ + Runnable[] rebuildBrowser = {null}; + BaseDialog browser = new BaseDialog("@mods.browser"); + browser.cont.table(table -> { + table.left(); + table.image(Icon.zoom); + table.field(searchtxt, res -> { + searchtxt = res; + rebuildBrowser[0].run(); + }).growX().get(); + table.button(Icon.list, Styles.clearPartiali, 32f, () -> { + orderDate = !orderDate; + rebuildBrowser[0].run(); + }).update(b -> b.getStyle().imageUp = (orderDate ? Icon.list : Icon.star)).size(40f).get() + .addListener(new Tooltip(tip -> tip.label(() -> orderDate ? "@mods.browser.sortdate" : "@mods.browser.sortstars").left())); + }).fillX().padBottom(4); + + browser.cont.row(); + + browser.cont.pane(tablebrow -> { + tablebrow.margin(10f).top(); + rebuildBrowser[0] = () -> { + tablebrow.clear(); + tablebrow.add("@loading"); + + getModList(rlistings -> { + tablebrow.clear(); + + Seq listings = rlistings; + if(!orderDate){ + listings = rlistings.copy(); + listings.sortComparing(m1 -> -m1.stars); + } + + for(ModListing mod : listings){ + if((mod.hasJava && Vars.ios) || !searchtxt.isEmpty() && !mod.repo.toLowerCase().contains(searchtxt.toLowerCase()) || (Vars.ios && mod.hasScripts)) continue; + + tablebrow.button(btn -> { + btn.top().left(); + btn.margin(12f); + btn.table(con -> { + con.left(); + con.add("[accent]" + mod.name + "[white]\n[lightgray]Author:[] " + trimText(mod.author) + "\n[lightgray]\uE809 " + mod.stars + + (Version.isAtLeast(mod.minGameVersion) ? "" : "\n" + Core.bundle.format("mod.requiresversion", mod.minGameVersion))) + .width(388f).wrap().growX().pad(0f, 6f, 0f, 6f).left().labelAlign(Align.left); + con.add().growX().pad(0f, 6f, 0f, 6f); + }).fillY().growX().pad(0f, 6f, 0f, 6f); + }, Styles.modsb, () -> { + var sel = new BaseDialog(mod.name); + sel.cont.add(mod.description).width(mobile ? 400f : 500f).wrap().pad(4f).labelAlign(Align.center, Align.left); + sel.buttons.defaults().size(150f, 54f).pad(2f); + sel.setFillParent(false); + sel.buttons.button("@back", Icon.left, () -> { + sel.clear(); + sel.hide(); + }); + + var found = mods.list().find(l -> mod.repo != null && mod.repo.equals(l.getRepo())); + sel.buttons.button(found == null ? "@mods.browser.add" : "@mods.browser.reinstall", Icon.download, () -> { + sel.hide(); + githubImportMod(mod.repo, mod.hasJava); + }); + sel.buttons.button("@mods.github.open", Icon.link, () -> { + Core.app.openURI("https://github.com/" + mod.repo); + }); + sel.keyDown(KeyCode.escape, sel::hide); + sel.keyDown(KeyCode.back, sel::hide); + sel.show(); + }).width(460f).growX().left().fillY(); + tablebrow.row(); + } + }); + }; + rebuildBrowser[0].run(); + }).get().setScrollingDisabled(true, false); + browser.addCloseButton(); + browser.show(); + } + + private String trimText(String text){ + if(text.contains("\n")){ + return text.substring(0, text.indexOf("\n")); + } + return text; + } + private void handleMod(String repo, HttpResponse result){ try{ Fi file = tmpDirectory.child(repo.replace("/", "") + ".zip"); @@ -452,15 +462,58 @@ public class ModsDialog extends BaseDialog{ } } - private void githubImportMod(String name){ - ui.loadfrag.show(); - Core.net.httpGet("https://api.github.com/repos/" + name, res -> { - if(checkError(res)){ - String mainBranch = Jval.read(res.getResultAsString()).getString("default_branch"); + private void importFail(Throwable t){ + Core.app.post(() -> modError(t)); + } - githubImportBranch(mainBranch, name, this::showStatus); + private void githubImportMod(String repo, boolean isJava){ + ui.loadfrag.show(); + + if(isJava){ + githubImportJavaMod(repo); + }else{ + Core.net.httpGet(ghApi + "/repos/" + repo, res -> { + if(checkError(res)){ + var json = Jval.read(res.getResultAsString()); + String mainBranch = json.getString("default_branch"); + String language = json.getString("language", ""); + + //this is a crude heuristic for class mods; only required for direct github import + //TODO make a more reliable way to distinguish java mod repos + if(language.equals("Java") || language.equals("Kotlin")){ + githubImportJavaMod(repo); + }else{ + githubImportBranch(mainBranch, repo, this::showStatus); + } + } + }, this::importFail); + } + } + + private void githubImportJavaMod(String repo){ + //grab latest release + Core.net.httpGet(ghApi + "/repos/" + repo + "/releases/latest", res -> { + if(checkError(res)){ + var json = Jval.read(res.getResultAsString()); + var assets = json.get("assets").asArray(); + + //prioritize dexed jar, as that's what Sonnicon's mod template outputs + var dexedAsset = assets.find(j -> j.getString("name").startsWith("dexed") && j.getString("name").endsWith(".jar")); + var asset = dexedAsset == null ? assets.find(j -> j.getString("name").endsWith(".jar")) : dexedAsset; + + if(asset != null){ + //grab actual file + var url = asset.getString("browser_download_url"); + Core.net.httpGet(url, result -> { + if(checkError(result)){ + handleMod(repo, result); + } + }, this::importFail); + }else{ + throw new ArcRuntimeException("No JAR file found in releases. Make sure you have a valid jar file in the mod's latest Github Release."); + } } - }, t -> Core.app.post(() -> modError(t))); + }, this::importFail); } private boolean checkError(HttpResponse res){ @@ -480,7 +533,7 @@ public class ModsDialog extends BaseDialog{ } private void githubImportBranch(String branch, String repo, Cons err){ - Core.net.httpGet("https://api.github.com/repos/" + repo + "/zipball/" + branch, loc -> { + Core.net.httpGet(ghApi + "/repos/" + repo + "/zipball/" + branch, loc -> { if(loc.getStatus() == HttpStatus.OK){ if(loc.getHeader("Location") != null){ Core.net.httpGet(loc.getHeader("Location"), result -> { @@ -489,13 +542,13 @@ public class ModsDialog extends BaseDialog{ }else{ handleMod(repo, result); } - }, t2 -> Core.app.post(() -> modError(t2))); + }, this::importFail); }else{ handleMod(repo, loc); } }else{ err.get(loc.getStatus()); } - }, t2 -> Core.app.post(() -> modError(t2))); + }, this::importFail); } }