diff --git a/core/assets/bundles/bundle.properties b/core/assets/bundles/bundle.properties index 645dfcaa12..9cc6fd0b5a 100644 --- a/core/assets/bundles/bundle.properties +++ b/core/assets/bundles/bundle.properties @@ -99,6 +99,8 @@ mod.import.github = Import Github Mod mod.remove.confirm = This mod will be deleted. mod.author = [LIGHT_GRAY]Author:[] {0} mod.missing = This save contains mods that you have recently updated or no longer have installed. Save corruption may occur. Are you sure you want to load it?\n[lightgray]Mods:\n{0} +mod.preview.missing = Before publishing this mod in the workshop, you must add an image preview.\nPlace an image named[accent] preview.png[] into the mod's folder and try again. +mod.folder.missing = Only mods in folder form can be published on the workshop.\nTo convert any mod into a folder, simply unzip its file into a folder and delete the old zip, then restart your game or reload your mods. about.button = About name = Name: @@ -255,14 +257,13 @@ map.nospawn = This map does not have any cores for the player to spawn in! Add a map.nospawn.pvp = This map does not have any enemy cores for player to spawn into! Add[SCARLET] non-orange[] cores to this map in the editor. map.nospawn.attack = This map does not have any enemy cores for player to attack! Add[SCARLET] red[] cores to this map in the editor. map.invalid = Error loading map: corrupted or invalid map file. -map.update = Update Map -map.load.error = Error fetching workshop details: {0} -map.missing = This map has been deleted or moved.\n[lightgray]The workshop listing has now been automatically un-linked from the map. +workshop.update = Update Item +workshop.error = Error fetching workshop details: {0} map.publish.confirm = Are you sure you want to publish this map?\n\n[lightgray]Make sure you agree to the Workshop EULA first, or your maps will not show up! map.menu = Select what you would like to do with this map. -map.changelog = Changelog (optional): +changelog = Changelog (optional): eula = Steam EULA -map.publish = Map published. +missing = This item has been deleted or moved.\n[lightgray]The workshop listing has now been automatically un-linked. publishing = [accent]Publishing... publish.confirm = Are you sure you want to publish this?\n\n[lightgray]Make sure you agree to the Workshop EULA first, or your items will not show up! publish.error = Error publishing item: {0} diff --git a/core/src/io/anuke/mindustry/core/Platform.java b/core/src/io/anuke/mindustry/core/Platform.java index 3428e928d5..c2fad7a15f 100644 --- a/core/src/io/anuke/mindustry/core/Platform.java +++ b/core/src/io/anuke/mindustry/core/Platform.java @@ -8,9 +8,9 @@ import io.anuke.arc.function.*; import io.anuke.arc.math.*; import io.anuke.arc.scene.ui.*; import io.anuke.arc.util.serialization.*; -import io.anuke.mindustry.maps.*; import io.anuke.mindustry.net.*; import io.anuke.mindustry.net.Net.*; +import io.anuke.mindustry.type.*; import io.anuke.mindustry.ui.dialogs.*; import static io.anuke.mindustry.Vars.mobile; @@ -24,32 +24,18 @@ public interface Platform{ default void inviteFriends(){} /** Steam: Share a map on the workshop.*/ - default void publishMap(Map map){} - - /** Steam: Return external workshop maps to be loaded.*/ - default Array getExternalMaps(){ - return Array.with(); - } - - /** Steam: Return external workshop mods to be loaded.*/ - default Array getExternalMods(){ - return Array.with(); - } - - /** Steam: Return external workshop schematics to be loaded.*/ - default Array getExternalSchematics(){ - return Array.with(); - } - - /** Steam: View a map listing on the workshop.*/ - default void viewMapListing(Map map){} + default void publish(Publishable pub){} /** Steam: View a listing on the workshop.*/ - default void viewListing(String mapid){} + default void viewListing(Publishable pub){} - /** Steam: View map workshop info, removing the map ID tag if its listing is deleted. - * Also presents the option to update the map. */ - default void viewMapListingInfo(Map map){} + /** Steam: View a listing on the workshop by an ID.*/ + default void viewListingID(String mapid){} + + /** Steam: Return external workshop maps to be loaded.*/ + default Array getWorkshopContent(Class type){ + return new Array<>(0); + } /** Steam: Open workshop for maps.*/ default void openWorkshop(){} diff --git a/core/src/io/anuke/mindustry/editor/MapEditorDialog.java b/core/src/io/anuke/mindustry/editor/MapEditorDialog.java index 88fe62e604..587ce46120 100644 --- a/core/src/io/anuke/mindustry/editor/MapEditorDialog.java +++ b/core/src/io/anuke/mindustry/editor/MapEditorDialog.java @@ -149,15 +149,16 @@ public class MapEditorDialog extends Dialog implements Disposable{ if(steam){ menu.cont.addImageTextButton("$editor.publish.workshop", Icon.linkSmall, () -> { Map builtin = maps.all().find(m -> m.name().equals(editor.getTags().get("name", "").trim())); + if(editor.getTags().containsKey("steamid") && builtin != null && !builtin.custom){ - platform.viewListing(editor.getTags().get("steamid")); + platform.viewListingID(editor.getTags().get("steamid")); return; } Map map = save(); if(editor.getTags().containsKey("steamid") && map != null){ - platform.viewMapListingInfo(map); + platform.viewListing(map); return; } @@ -173,7 +174,7 @@ public class MapEditorDialog extends Dialog implements Disposable{ return; } - platform.publishMap(map); + platform.publish(map); }).padTop(-3).size(swidth * 2f + 10, 60f).update(b -> b.setText(editor.getTags().containsKey("steamid") ? editor.getTags().get("author").equals(player.name) ? "$workshop.listing" : "$view.workshop" : "$editor.publish.workshop")); menu.cont.row(); diff --git a/core/src/io/anuke/mindustry/game/Schematic.java b/core/src/io/anuke/mindustry/game/Schematic.java index 9b7b8aabf1..ff5f533d05 100644 --- a/core/src/io/anuke/mindustry/game/Schematic.java +++ b/core/src/io/anuke/mindustry/game/Schematic.java @@ -5,10 +5,13 @@ import io.anuke.arc.collection.IntIntMap.*; import io.anuke.arc.files.*; import io.anuke.arc.util.ArcAnnotate.*; import io.anuke.mindustry.*; +import io.anuke.mindustry.game.Schematics.*; import io.anuke.mindustry.type.*; import io.anuke.mindustry.world.*; -public class Schematic{ +import static io.anuke.mindustry.Vars.*; + +public class Schematic implements Publishable{ public final Array tiles; public StringMap tags; public int width, height; @@ -41,8 +44,54 @@ public class Schematic{ return tags.get("name", "unknown"); } - public boolean isWorkshop(){ - return tags.containsKey("workshop"); + public void save(){ + schematics.saveChanges(this); + } + + @Override + public String getSteamID(){ + return tags.get("steamid"); + } + + @Override + public void addSteamID(String id){ + tags.put("steamid", id); + save(); + } + + @Override + public void removeSteamID(){ + tags.remove("steamid"); + save(); + } + + @Override + public String steamTitle(){ + return name(); + } + + @Override + public String steamDescription(){ + return null; + } + + @Override + public String steamTag(){ + return "schematic"; + } + + @Override + public FileHandle createSteamFolder(String id){ + FileHandle directory = tmpDirectory.child("schematic_" + id).child("schematic." + schematicExtension); + file.copyTo(directory); + return directory; + } + + @Override + public FileHandle createSteamPreview(String id){ + FileHandle preview = tmpDirectory.child("schematic_preview_" + id + ".png"); + schematics.savePreview(this, PreviewRes.high, preview); + return preview; } public static class Stile{ diff --git a/core/src/io/anuke/mindustry/game/Schematics.java b/core/src/io/anuke/mindustry/game/Schematics.java index 8d49a99ca0..14e28f5cc4 100644 --- a/core/src/io/anuke/mindustry/game/Schematics.java +++ b/core/src/io/anuke/mindustry/game/Schematics.java @@ -58,7 +58,7 @@ public class Schematics implements Loadable{ loadFile(file); } - platform.getExternalSchematics().each(this::loadFile); + platform.getWorkshopContent(Schematic.class).each(this::loadFile); Core.app.post(() -> { shadowBuffer = new FrameBuffer(maxSchematicSize + padding + 2, maxSchematicSize + padding + 2); @@ -69,7 +69,13 @@ public class Schematics implements Loadable{ if(!file.extension().equals(schematicExtension)) return; try{ - all.add(read(file)); + Schematic s = read(file); + all.add(s); + + //external file from workshop + if(!s.file.parent().equals(schematicDirectory)){ + s.tags.put("steamid", s.file.parent().name()); + } }catch(IOException e){ Log.err(e); } @@ -79,7 +85,30 @@ public class Schematics implements Loadable{ return all; } + public void saveChanges(Schematic s){ + if(s.file != null){ + try{ + write(s, s.file); + }catch(Exception e){ + ui.showException(e); + } + } + } + + public void savePreview(Schematic schematic, PreviewRes res, FileHandle file){ + FrameBuffer buffer = getBuffer(schematic, res); + Draw.flush(); + buffer.begin(); + Pixmap pixmap = ScreenUtils.getFrameBufferPixmap(0, 0, buffer.getWidth(), buffer.getHeight()); + file.writePNG(pixmap); + buffer.end(); + } + public Texture getPreview(Schematic schematic, PreviewRes res){ + return getBuffer(schematic, res).getTexture(); + } + + public FrameBuffer getBuffer(Schematic schematic, PreviewRes res){ if(!previews.getOr(schematic, ObjectMap::new).containsKey(res)){ int resolution = res.resolution; Draw.blend(); @@ -113,11 +142,6 @@ public class Schematics implements Loadable{ buffer.beginDraw(Color.clear); Draw.proj().setOrtho(0, buffer.getHeight(), buffer.getWidth(), -buffer.getHeight()); - for(int x = 0; x < schematic.width + padding; x++){ - for(int y = 0; y < schematic.height + padding; y++){ - //Draw.rect("metal-floor", x * resolution + resolution/2f, y * resolution + resolution/2f, resolution, resolution); - } - } Tmp.tr1.set(shadowBuffer.getTexture(), 0, 0, schematic.width + padding, schematic.height + padding); Draw.color(0f, 0f, 0f, 1f); @@ -127,8 +151,10 @@ public class Schematics implements Loadable{ Array requests = schematic.tiles.map(t -> new BuildRequest(t.x, t.y, t.rotation, t.block).configure(t.config)); Draw.flush(); + //scale each request to fit schematic Draw.trans().scale(resolution / tilesize, resolution / tilesize).translate(tilesize*1.5f, tilesize*1.5f); + //draw requests requests.each(req -> { req.animScale = 1f; req.block.drawRequestRegion(req, requests::each); @@ -148,7 +174,7 @@ public class Schematics implements Loadable{ Log.info("Time taken: {0}", Time.elapsed()); } - return previews.get(schematic).get(res).getTexture(); + return previews.get(schematic).get(res); } /** Creates an array of build requests from a schematic's data, centered on the provided x+y coordinates. */ diff --git a/core/src/io/anuke/mindustry/game/Tutorial.java b/core/src/io/anuke/mindustry/game/Tutorial.java index 8eb8e9e054..f402f88a5b 100644 --- a/core/src/io/anuke/mindustry/game/Tutorial.java +++ b/core/src/io/anuke/mindustry/game/Tutorial.java @@ -40,6 +40,10 @@ public class Tutorial{ Events.on(BlockInfoEvent.class, event -> events.add("blockinfo")); Events.on(DepositEvent.class, event -> events.add("deposit")); Events.on(WithdrawEvent.class, event -> events.add("withdraw")); + + for(TutorialStage stage : TutorialStage.values()){ + stage.load(); + } } /** update tutorial state, transition if needed */ @@ -204,13 +208,17 @@ public class Tutorial{ /** displayed tutorial stage text.*/ public String text(){ if(sentences == null){ - this.line = Core.bundle.has("tutorial." + name() + ".mobile") && mobile ? "tutorial." + name() + ".mobile" : "tutorial." + name(); - this.sentences = Array.select(Core.bundle.get(line).split("\n"), s -> !s.isEmpty()); + load(); } String line = sentences.get(control.tutorial.sentence); return line.contains("{") ? text.get(line) : line; } + void load(){ + this.line = Core.bundle.has("tutorial." + name() + ".mobile") && mobile ? "tutorial." + name() + ".mobile" : "tutorial." + name(); + this.sentences = Array.select(Core.bundle.get(line).split("\n"), s -> !s.isEmpty()); + } + /** called every frame when this stage is active.*/ void update(){ diff --git a/core/src/io/anuke/mindustry/input/DesktopInput.java b/core/src/io/anuke/mindustry/input/DesktopInput.java index f76a3e199a..7ccf1ba180 100644 --- a/core/src/io/anuke/mindustry/input/DesktopInput.java +++ b/core/src/io/anuke/mindustry/input/DesktopInput.java @@ -283,6 +283,11 @@ public class DesktopInput extends InputHandler{ schemY = rawCursorY; } + if(Core.input.keyTap(Binding.clear_building)){ + lastSchematic = null; + selectRequests.clear(); + } + if(Core.input.keyRelease(Binding.schematic)){ lastSchematic = schematics.create(schemX, schemY, rawCursorX, rawCursorY); useSchematic(lastSchematic); diff --git a/core/src/io/anuke/mindustry/maps/Map.java b/core/src/io/anuke/mindustry/maps/Map.java index 94fa841f08..378f3c8ff6 100644 --- a/core/src/io/anuke/mindustry/maps/Map.java +++ b/core/src/io/anuke/mindustry/maps/Map.java @@ -4,14 +4,17 @@ import io.anuke.arc.*; import io.anuke.arc.collection.*; import io.anuke.arc.files.*; import io.anuke.arc.graphics.*; +import io.anuke.arc.util.*; import io.anuke.mindustry.*; +import io.anuke.mindustry.game.EventType.*; import io.anuke.mindustry.game.*; import io.anuke.mindustry.io.*; import io.anuke.mindustry.maps.filters.*; +import io.anuke.mindustry.type.*; -import static io.anuke.mindustry.Vars.maps; +import static io.anuke.mindustry.Vars.*; -public class Map implements Comparable{ +public class Map implements Comparable, Publishable{ /** Whether this is a custom map. */ public final boolean custom; /** Metadata. Author description, display name, etc. */ @@ -131,6 +134,76 @@ public class Map implements Comparable{ return tags.containsKey(name); } + @Override + public String getSteamID(){ + return tags.get("steamid"); + } + + @Override + public void addSteamID(String id){ + tags.put("steamid", id); + + ui.editor.editor.getTags().put("steamid", id); + try{ + ui.editor.save(); + }catch(Exception e){ + Log.err(e); + } + Events.fire(new MapPublishEvent()); + } + + @Override + public void removeSteamID(){ + tags.remove("steamid"); + + ui.editor.editor.getTags().remove("steamid"); + try{ + ui.editor.save(); + }catch(Exception e){ + Log.err(e); + } + } + + @Override + public String steamTitle(){ + return name(); + } + + @Override + public String steamDescription(){ + return description(); + } + + @Override + public String steamTag(){ + return "map"; + } + + @Override + public FileHandle createSteamFolder(String id){ + return null; + } + + @Override + public FileHandle createSteamPreview(String id){ + return null; + } + + @Override + public Array extraTags(){ + Gamemode mode = Gamemode.attack.valid(this) ? Gamemode.attack : Gamemode.survival; + return Array.with(mode.name()); + } + + @Override + public boolean prePublish(){ + tags.put("author", player.name); + ui.editor.editor.getTags().put("author", tags.get("author")); + ui.editor.save(); + + return true; + } + @Override public int compareTo(Map map){ int work = -Boolean.compare(workshop, map.workshop); diff --git a/core/src/io/anuke/mindustry/maps/Maps.java b/core/src/io/anuke/mindustry/maps/Maps.java index 72d1b17b77..b19f3628ea 100644 --- a/core/src/io/anuke/mindustry/maps/Maps.java +++ b/core/src/io/anuke/mindustry/maps/Maps.java @@ -104,7 +104,7 @@ public class Maps{ } //workshop - for(FileHandle file : platform.getExternalMaps()){ + for(FileHandle file : platform.getWorkshopContent(Map.class)){ try{ Map map = loadMap(file, false); map.workshop = true; diff --git a/core/src/io/anuke/mindustry/mod/Mods.java b/core/src/io/anuke/mindustry/mod/Mods.java index b01161c9d0..cfc23bf0b1 100644 --- a/core/src/io/anuke/mindustry/mod/Mods.java +++ b/core/src/io/anuke/mindustry/mod/Mods.java @@ -180,7 +180,7 @@ public class Mods implements Loadable{ } //load workshop mods now - for(FileHandle file : platform.getExternalMods()){ + for(FileHandle file : platform.getWorkshopContent(LoadedMod.class)){ try{ LoadedMod mod = loadMod(file, true); if(mod.enabled()){ @@ -442,7 +442,7 @@ public class Mods implements Loadable{ } /** Represents a plugin that has been loaded from a jar file.*/ - public static class LoadedMod{ + public static class LoadedMod implements Publishable{ /** 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. */ @@ -453,8 +453,6 @@ public class Mods implements Loadable{ public final String name; /** This mod's metadata. */ public final ModMeta meta; - /** The ID of this mod in the workshop.*/ - public @Nullable String workshopID; public LoadedMod(FileHandle file, FileHandle root, Mod mod, ModMeta meta){ this.root = root; @@ -468,6 +466,63 @@ public class Mods implements Loadable{ return Core.settings.getBool(name + "-enabled", true); } + @Override + public String getSteamID(){ + return Core.settings.getString(name + "-steamid", null); + } + + @Override + public void addSteamID(String id){ + Core.settings.put(name + "-steamid", id); + Core.settings.save(); + } + + @Override + public void removeSteamID(){ + Core.settings.remove(name + "-steamid"); + Core.settings.save(); + } + + @Override + public String steamTitle(){ + return meta.name; + } + + @Override + public String steamDescription(){ + return meta.description; + } + + @Override + public String steamTag(){ + return "mod"; + } + + @Override + public FileHandle createSteamFolder(String id){ + return file; + } + + @Override + public FileHandle createSteamPreview(String id){ + return file.child("preview.png"); + } + + @Override + public boolean prePublish(){ + if(!file.isDirectory()){ + ui.showErrorMessage("$mod.folder.missing"); + return false; + } + + if(!file.child("preview.png").exists()){ + ui.showErrorMessage("$mod.preview.missing"); + return false; + } + + return true; + } + @Override public String toString(){ return "LoadedMod{" + diff --git a/core/src/io/anuke/mindustry/type/Publishable.java b/core/src/io/anuke/mindustry/type/Publishable.java new file mode 100644 index 0000000000..f3e8b45b88 --- /dev/null +++ b/core/src/io/anuke/mindustry/type/Publishable.java @@ -0,0 +1,40 @@ +package io.anuke.mindustry.type; + +import io.anuke.arc.collection.*; +import io.anuke.arc.files.*; +import io.anuke.arc.util.ArcAnnotate.*; +import io.anuke.mindustry.*; + +/** Defines a piece of content that can be published on the Workshop. */ +public interface Publishable{ + /** @return workshop item ID, or null if this isn't on the workshop. */ + @Nullable String getSteamID(); + /** adds a steam ID to this item once it's published. should save the item to make sure this change is persisted. */ + void addSteamID(String id); + /** removes the item ID; called when the item isn't found. */ + void removeSteamID(); + /** @return default title of the listing. */ + String steamTitle(); + /** @return standard steam listing description, may be null. this is editable by users after release.*/ + @Nullable String steamDescription(); + /** @return the tag that this content has. e.g. 'schematic' or 'map'. */ + String steamTag(); + /** @return a folder with everything needed for this piece of content in it; does not need to be a copy. */ + FileHandle createSteamFolder(String id); + /** @return a preview file PNG. */ + FileHandle createSteamPreview(String id); + /** @return any extra tags to add to this item.*/ + default Array extraTags(){ + return new Array<>(0); + } + /** @return whether this item is or was once on the workshop.*/ + default boolean hasSteamID(){ + return getSteamID() != null && Vars.steam; + } + /** called before this item is published. + * @return true to signify that everything is cool and good, or false to significy that the user has done something wrong. + * if false is returned, make sure to show a dialog explaining the error. */ + default boolean prePublish(){ + return true; + } +} diff --git a/core/src/io/anuke/mindustry/ui/dialogs/MapsDialog.java b/core/src/io/anuke/mindustry/ui/dialogs/MapsDialog.java index 968cb0983f..a2fb508fcb 100644 --- a/core/src/io/anuke/mindustry/ui/dialogs/MapsDialog.java +++ b/core/src/io/anuke/mindustry/ui/dialogs/MapsDialog.java @@ -205,7 +205,7 @@ public class MapsDialog extends FloatingDialog{ table.addImageTextButton(map.workshop && steam ? "$view.workshop" : "$delete", map.workshop && steam ? Icon.linkSmall : Icon.trash16Small, () -> { if(map.workshop && steam){ - platform.viewMapListing(map); + platform.viewListing(map); }else{ ui.showConfirm("$confirm", Core.bundle.format("map.delete", map.name()), () -> { maps.removeMap(map); diff --git a/core/src/io/anuke/mindustry/ui/dialogs/ModsDialog.java b/core/src/io/anuke/mindustry/ui/dialogs/ModsDialog.java index c06c730eca..7dfe21b5c2 100644 --- a/core/src/io/anuke/mindustry/ui/dialogs/ModsDialog.java +++ b/core/src/io/anuke/mindustry/ui/dialogs/ModsDialog.java @@ -101,14 +101,20 @@ public class ModsDialog extends FloatingDialog{ setup(); }).height(50f).margin(8f).width(130f); - title.addImageButton(mod.workshopID != null ? Icon.linkSmall : Icon.trash16Small, Styles.cleari, () -> { - if(mod.workshopID == null){ + if(steam && !mod.hasSteamID()){ + title.addImageButton(Icon.loadMapSmall, Styles.cleari, () -> { + platform.publish(mod); + }).size(50f); + } + + title.addImageButton(mod.hasSteamID() ? Icon.linkSmall : Icon.trash16Small, Styles.cleari, () -> { + if(!mod.hasSteamID()){ ui.showConfirm("$confirm", "$mod.remove.confirm", () -> { mods.removeMod(mod); setup(); }); }else{ - platform.viewListing(mod.workshopID); + platform.viewListing(mod); } }).size(50f); }).growX().left().padTop(-14f).padRight(-14f); diff --git a/core/src/io/anuke/mindustry/ui/dialogs/SchematicsDialog.java b/core/src/io/anuke/mindustry/ui/dialogs/SchematicsDialog.java index d124cc54fa..539f314c45 100644 --- a/core/src/io/anuke/mindustry/ui/dialogs/SchematicsDialog.java +++ b/core/src/io/anuke/mindustry/ui/dialogs/SchematicsDialog.java @@ -91,12 +91,16 @@ public class SchematicsDialog extends FloatingDialog{ }); }); - buttons.addImageButton(Icon.trash16Small, style, () -> { - ui.showConfirm("$confirm", "$schematic.delete.confirm", () -> { - schematics.remove(s); - rebuildPane[0].run(); + if(s.hasSteamID()){ + buttons.addImageButton(Icon.linkSmall, style, () -> platform.viewListing(s)); + }else{ + buttons.addImageButton(Icon.trash16Small, style, () -> { + ui.showConfirm("$confirm", "$schematic.delete.confirm", () -> { + schematics.remove(s); + rebuildPane[0].run(); + }); }); - }); + } }).growX().height(50f); b.row(); @@ -180,10 +184,9 @@ public class SchematicsDialog extends FloatingDialog{ p.table(Tex.button, t -> { TextButtonStyle style = Styles.cleart; t.defaults().size(280f, 60f).left(); - if(steam){ - t.addImageTextButton("$schematic.shareworkshop", Icon.wikiSmall, style, () -> { - - }).marginLeft(12f); + if(steam && !s.hasSteamID()){ + t.addImageTextButton("$schematic.shareworkshop", Icon.wikiSmall, style, + () -> platform.publish(s)).marginLeft(12f); t.row(); } t.addImageTextButton("$schematic.copy", Icon.copySmall, style, () -> { diff --git a/desktop/src/io/anuke/mindustry/desktop/DesktopLauncher.java b/desktop/src/io/anuke/mindustry/desktop/DesktopLauncher.java index f6e8758342..a35b74f619 100644 --- a/desktop/src/io/anuke/mindustry/desktop/DesktopLauncher.java +++ b/desktop/src/io/anuke/mindustry/desktop/DesktopLauncher.java @@ -22,10 +22,10 @@ import io.anuke.mindustry.core.GameState.*; import io.anuke.mindustry.desktop.steam.*; import io.anuke.mindustry.game.EventType.*; import io.anuke.mindustry.game.Version; -import io.anuke.mindustry.maps.Map; import io.anuke.mindustry.mod.Mods.*; import io.anuke.mindustry.net.*; import io.anuke.mindustry.net.Net.*; +import io.anuke.mindustry.type.*; import io.anuke.mindustry.ui.*; import java.io.*; @@ -249,34 +249,18 @@ public class DesktopLauncher extends ClientLauncher{ } @Override - public Array getExternalMaps(){ - return !steam ? super.getExternalMaps() : SVars.workshop.getMapFiles(); + public Array getWorkshopContent(Class type){ + return !steam ? super.getWorkshopContent(type) : SVars.workshop.getWorkshopFiles(type); } @Override - public Array getExternalMods(){ - return !steam ? super.getExternalMods() : SVars.workshop.getModFiles(); + public void viewListing(Publishable pub){ + SVars.workshop.viewListing(pub); } @Override - public Array getExternalSchematics(){ - return !steam ? super.getExternalMods() : SVars.workshop.getSchematicFiles(); - } - - - @Override - public void viewMapListing(Map map){ - viewListing(map.file.parent().name()); - } - - @Override - public void viewListing(String mapid){ - SVars.net.friends.activateGameOverlayToWebPage("steam://url/CommunityFilePage/" + mapid); - } - - @Override - public void viewMapListingInfo(Map map){ - SVars.workshop.viewMapListingInfo(map); + public void viewListingID(String id){ + SVars.net.friends.activateGameOverlayToWebPage("steam://url/CommunityFilePage/" + id); } @Override @@ -290,8 +274,8 @@ public class DesktopLauncher extends ClientLauncher{ } @Override - public void publishMap(Map map){ - SVars.workshop.publishMap(map); + public void publish(Publishable pub){ + SVars.workshop.publish(pub); } @Override diff --git a/desktop/src/io/anuke/mindustry/desktop/steam/SWorkshop.java b/desktop/src/io/anuke/mindustry/desktop/steam/SWorkshop.java index 1ece2ab2a3..b5b010bb9e 100644 --- a/desktop/src/io/anuke/mindustry/desktop/steam/SWorkshop.java +++ b/desktop/src/io/anuke/mindustry/desktop/steam/SWorkshop.java @@ -9,11 +9,11 @@ import io.anuke.arc.files.*; import io.anuke.arc.function.*; import io.anuke.arc.scene.ui.*; import io.anuke.arc.util.*; -import io.anuke.mindustry.game.EventType.*; import io.anuke.mindustry.game.*; import io.anuke.mindustry.gen.*; import io.anuke.mindustry.maps.*; import io.anuke.mindustry.mod.Mods.*; +import io.anuke.mindustry.type.*; import io.anuke.mindustry.ui.dialogs.*; import static io.anuke.mindustry.Vars.*; @@ -21,12 +21,10 @@ import static io.anuke.mindustry.Vars.*; public class SWorkshop implements SteamUGCCallback{ public final SteamUGC ugc = new SteamUGC(this); - //private Map lastMap; - private Array mapFiles; - private Array modFiles; - private Array schematicFiles; + private ObjectMap, Array> workshopFiles = new ObjectMap<>(); private ObjectMap, SteamResult>> detailHandlers = new ObjectMap<>(); private Array> itemHandlers = new Array<>(); + private ObjectMap updatedHandlers = new ObjectMap<>(); public SWorkshop(){ int items = ugc.getNumSubscribedItems(); @@ -39,88 +37,56 @@ public class SWorkshop implements SteamUGCCallback{ return new FileHandle(info.getFolder()); }).select(f -> f != null && f.list().length > 0); - mapFiles = folders.select(f -> f.list().length == 1 && f.list()[0].extension().equals(mapExtension)).map(f -> f.list()[0]); - schematicFiles = folders.select(f -> f.list().length == 1 && f.list()[0].extension().equals(schematicExtension)).map(f -> f.list()[0]); - modFiles = folders.select(f -> f.child("mod.json").exists()); + workshopFiles.put(Map.class, folders.select(f -> f.list().length == 1 && f.list()[0].extension().equals(mapExtension)).map(f -> f.list()[0])); + workshopFiles.put(Schematic.class, folders.select(f -> f.list().length == 1 && f.list()[0].extension().equals(schematicExtension)).map(f -> f.list()[0])); + workshopFiles.put(LoadedMod.class, folders.select(f -> f.child("mod.json").exists())); - if(!mapFiles.isEmpty()){ + if(!workshopFiles.get(Map.class).isEmpty()){ SAchievement.downloadMapWorkshop.complete(); } - Log.info("Fetching {0} subscribed maps.", mapFiles.size); - Log.info("Fetching {0} subscribed mods.", modFiles.size); - } - - public Array getMapFiles(){ - return mapFiles; - } - - public Array getModFiles(){ - return modFiles; - } - - - public Array getSchematicFiles(){ - return schematicFiles; - } - - public void publishMap(Map map){ - if(map.tags.containsKey("steamid")){ - Log.info("Map already published, redirecting to ID."); - SVars.net.friends.activateGameOverlayToWebPage("steam://url/CommunityFilePage/" + map.tags.get("steamid")); - return; - } - - //update author name when publishing - map.tags.put("author", SVars.net.friends.getPersonaName()); - ui.editor.editor.getTags().put("author", map.tags.get("author")); - ui.editor.save(); - - showPublish(id -> updateMap(map, id, "")); - } - - public void publishSchematic(Schematic schematic){ - - showPublish(id -> { - + workshopFiles.each((type, list) -> { + Log.info("Fetched content ({0}): {1}", type.getSimpleName(), list.size); }); } - public void publishMod(LoadedMod mod){ - + public Array getWorkshopFiles(Class type){ + return workshopFiles.getOr(type, () -> new Array<>(0)); } - private void showPublish(Consumer published){ - FloatingDialog dialog = new FloatingDialog("$confirm"); - dialog.setFillParent(false); - dialog.cont.add("$publish.confirm").width(600f).wrap(); - dialog.addCloseButton(); - dialog.buttons.addImageTextButton("$eula", Icon.linkSmall, () -> { - SVars.net.friends.activateGameOverlayToWebPage("https://steamcommunity.com/sharedfiles/workshoplegalagreement"); - }).size(210f, 64f); + /** Publish a new item and submit an update for it. + * If it is already published, redirects to its page.*/ + public void publish(Publishable p){ + if(p.hasSteamID()){ + Log.info("Content already published, redirecting to ID."); + viewListing(p); + return; + } - dialog.buttons.addImageTextButton("$ok", Icon.checkSmall, () -> { - ugc.createItem(SVars.steamID, WorkshopFileType.Community); - ui.loadfrag.show("$publishing"); - dialog.hide(); - itemHandlers.add(published); - }).size(170f, 64f); - dialog.show(); + if(!p.prePublish()){ + return; + } + + showPublish(id -> update(p, id, null)); } - public void viewMapListingInfo(Map map){ - String id = map.tags.get("steamid"); + /** Update an existing item with a changelog. */ + public void updateItem(Publishable p, String changelog){ + String id = p.getSteamID(); long handle = Strings.parseLong(id, -1); SteamPublishedFileID fid = new SteamPublishedFileID(handle); + update(p, fid, changelog); + } - Log.info("Requesting map listing view; id = " + id); + /** Fetches info for an item, checking to make sure that it exists.*/ + public void viewListing(Publishable p){ + long handle = Strings.parseLong(p.getSteamID(), -1); + SteamPublishedFileID id = new SteamPublishedFileID(handle); ui.loadfrag.show(); - query(ugc.createQueryUGCDetailsRequest(fid), (detailsList, result) -> { + query(ugc.createQueryUGCDetailsRequest(id), (detailsList, result) -> { ui.loadfrag.hide(); - Log.info("Map listing result: " + result + " " + detailsList); - if(result == SteamResult.OK){ SteamUGCDetails details = detailsList.first(); if(details.getResult() == SteamResult.OK){ @@ -132,57 +98,114 @@ public class SWorkshop implements SteamUGCCallback{ dialog.addCloseButton(); dialog.buttons.addImageTextButton("$view.workshop", Icon.linkSmall, () -> { - platform.viewListing(id); + viewListingID(id); dialog.hide(); }).size(210f, 64f); - dialog.buttons.addImageTextButton("$map.update", Icon.upgradeSmall, () -> { - new FloatingDialog("$map.update"){{ + dialog.buttons.addImageTextButton("$workshop.update", Icon.upgradeSmall, () -> { + new FloatingDialog("$workshop.update"){{ setFillParent(false); - cont.margin(10).add("$map.changelog").padRight(6f); + cont.margin(10).add("$changelog").padRight(6f); cont.row(); TextArea field = cont.addArea("", t -> {}).size(500f, 160f).get(); field.setMaxLength(400); buttons.defaults().size(120, 54).pad(4); buttons.addButton("$ok", () -> { - ui.loadfrag.show("publishing"); - updateMap(map, details.getPublishedFileID(), field.getText().replace("\r", "\n")); + ui.loadfrag.show("$publishing"); + updateItem(p, field.getText().replace("\r", "\n")); dialog.hide(); hide(); - - Log.info("Update map " + map.name()); }); buttons.addButton("$cancel", this::hide); }}.show(); }).size(210f, 64f); dialog.show(); - }else{ SVars.net.friends.activateGameOverlayToWebPage("steam://url/CommunityFilePage/" + SteamNativeHandle.getNativeHandle(details.getPublishedFileID())); } }else if(details.getResult() == SteamResult.FileNotFound){ - //force-remove tags - ui.editor.editor.getTags().remove("steamid"); - map.tags.remove("steamid"); - ui.editor.save(); - - ui.showErrorMessage("$map.missing"); + p.removeSteamID(); + ui.showErrorMessage("$missing"); }else{ - ui.showErrorMessage(Core.bundle.format("map.load.error", result.name())); + ui.showErrorMessage(Core.bundle.format("workshop.error", result.name())); } }else{ - ui.showErrorMessage(Core.bundle.format("map.load.error", result.name())); + ui.showErrorMessage(Core.bundle.format("workshop.error", result.name())); } }); } - public void query(SteamUGCQuery query, BiConsumer, SteamResult> handler){ - Log.info("POST " + query); + void viewListingID(SteamPublishedFileID id){ + SVars.net.friends.activateGameOverlayToWebPage("steam://url/CommunityFilePage/" + SteamNativeHandle.getNativeHandle(id)); + } + + void update(Publishable p, SteamPublishedFileID id, String changelog){ + String sid = SteamNativeHandle.getNativeHandle(id) + ""; + + updateItem(id, h -> { + if(p.steamDescription() != null){ + ugc.setItemDescription(h, p.steamDescription()); + } + + Array tags = p.extraTags(); + tags.add(p.steamTag()); + + ugc.setItemTitle(h, p.steamTitle()); + ugc.setItemTags(h, tags.toArray(String.class)); + ugc.setItemPreview(h, p.createSteamPreview(sid).absolutePath()); + ugc.setItemContent(h, p.createSteamFolder(sid).absolutePath()); + if(changelog == null){ + ugc.setItemVisibility(h, PublishedFileVisibility.Private); + } + ugc.submitItemUpdate(h, changelog == null ? "" : changelog); + }, () -> p.addSteamID(sid)); + } + + void showPublish(Consumer published){ + FloatingDialog dialog = new FloatingDialog("$confirm"); + dialog.setFillParent(false); + dialog.cont.add("$publish.confirm").width(600f).wrap(); + dialog.addCloseButton(); + dialog.buttons.addImageTextButton("$eula", Icon.linkSmall, + () -> SVars.net.friends.activateGameOverlayToWebPage("https://steamcommunity.com/sharedfiles/workshoplegalagreement")) + .size(210f, 64f); + + dialog.buttons.addImageTextButton("$ok", Icon.checkSmall, () -> { + ugc.createItem(SVars.steamID, WorkshopFileType.Community); + ui.loadfrag.show("$publishing"); + dialog.hide(); + itemHandlers.add(published); + }).size(170f, 64f); + dialog.show(); + } + + void query(SteamUGCQuery query, BiConsumer, SteamResult> handler){ + Log.info("POST QUERY " + query); detailHandlers.put(query, handler); ugc.sendQueryUGCRequest(query); } + void updateItem(SteamPublishedFileID publishedFileID, Consumer tagger, Runnable updated){ + SteamUGCUpdateHandle h = ugc.startItemUpdate(SVars.steamID, publishedFileID); + + tagger.accept(h); + + ItemUpdateInfo info = new ItemUpdateInfo(); + + ui.loadfrag.setProgress(() -> { + ItemUpdateStatus status = ugc.getItemUpdateProgress(h, info); + ui.loadfrag.setText("$" + status.name().toLowerCase()); + if(status == ItemUpdateStatus.Invalid){ + ui.loadfrag.setText("$done"); + return 1f; + } + return (float)status.ordinal() / (float)ItemUpdateStatus.values().length; + }); + + updatedHandlers.put(publishedFileID, updated); + } + @Override public void onRequestUGCDetails(SteamUGCDetails details, SteamResult result){ @@ -190,7 +213,7 @@ public class SWorkshop implements SteamUGCCallback{ @Override public void onUGCQueryCompleted(SteamUGCQuery query, int numResultsReturned, int totalMatchingResults, boolean isCachedData, SteamResult result){ - Log.info("GET " + query); + Log.info("GET QUERY " + query); if(detailHandlers.containsKey(query)){ if(numResultsReturned > 0){ @@ -236,38 +259,6 @@ public class SWorkshop implements SteamUGCCallback{ } } - void updateMap(Map map, SteamPublishedFileID publishedFileID, String changelog){ - SteamUGCUpdateHandle h = ugc.startItemUpdate(SVars.steamID, publishedFileID); - - Gamemode mode = Gamemode.attack.valid(map) ? Gamemode.attack : Gamemode.survival; - FileHandle mapFile = tmpDirectory.child("map_" + publishedFileID.toString()).child("map.msav"); - map.file.copyTo(mapFile); - - Log.info(mapFile.parent().absolutePath()); - Log.info(map.previewFile().absolutePath()); - - ugc.setItemTitle(h, map.name()); - ugc.setItemDescription(h, map.description()); - ugc.setItemTags(h, new String[]{"map", mode.name()}); - ugc.setItemVisibility(h, PublishedFileVisibility.Private); - ugc.setItemPreview(h, map.previewFile().absolutePath()); - ugc.setItemContent(h, mapFile.parent().absolutePath()); - ugc.addItemKeyValueTag(h, "mode", mode.name()); - ugc.submitItemUpdate(h, changelog); - - ItemUpdateInfo info = new ItemUpdateInfo(); - - ui.loadfrag.setProgress(() -> { - ItemUpdateStatus status = ugc.getItemUpdateProgress(h, info); - ui.loadfrag.setText("$" + status.name().toLowerCase()); - if(status == ItemUpdateStatus.Invalid){ - ui.loadfrag.setText("$done"); - return 1f; - } - return (float)status.ordinal() / (float)ItemUpdateStatus.values().length; - }); - } - @Override public void onSubmitItemUpdate(SteamPublishedFileID publishedFileID, boolean needsToAcceptWLA, SteamResult result){ ui.loadfrag.hide(); @@ -278,13 +269,10 @@ public class SWorkshop implements SteamUGCCallback{ if(needsToAcceptWLA){ SVars.net.friends.activateGameOverlayToWebPage("https://steamcommunity.com/sharedfiles/workshoplegalagreement"); } - ui.editor.editor.getTags().put("steamid", SteamNativeHandle.getNativeHandle(publishedFileID) + ""); - try{ - ui.editor.save(); - }catch(Exception e){ - Log.err(e); + + if(updatedHandlers.containsKey(publishedFileID)){ + updatedHandlers.get(publishedFileID).run(); } - Events.fire(new MapPublishEvent()); }else{ ui.showErrorMessage(Core.bundle.format("publish.error ", result.name())); }