package mindustry.ui.dialogs; import arc.*; import arc.files.*; import arc.func.*; import arc.graphics.*; import arc.graphics.Texture.*; import arc.input.*; import arc.scene.*; import arc.scene.event.*; import arc.scene.style.*; import arc.scene.ui.*; import arc.scene.ui.TextButton.*; import arc.scene.ui.layout.*; import arc.struct.*; import arc.util.*; import arc.util.io.*; import mindustry.content.*; import mindustry.content.TechTree.*; import mindustry.core.*; import mindustry.ctype.*; import mindustry.game.EventType.*; import mindustry.gen.*; import mindustry.graphics.*; import mindustry.input.*; import mindustry.ui.*; import java.io.*; import java.util.zip.*; import static arc.Core.*; import static mindustry.Vars.*; public class SettingsMenuDialog extends BaseDialog{ public SettingsTable graphics; public SettingsTable game; public SettingsTable sound; public SettingsTable main; private Table prefs; private Table menu; private BaseDialog dataDialog; private Seq categories = new Seq<>(); public SettingsMenuDialog(){ super(bundle.get("settings", "Settings")); addCloseButton(); cont.add(main = new SettingsTable()); shouldPause = true; shown(() -> { back(); rebuildMenu(); }); onResize(() -> { graphics.rebuild(); sound.rebuild(); game.rebuild(); updateScrollFocus(); }); cont.clearChildren(); cont.remove(); buttons.remove(); menu = new Table(Tex.button); game = new SettingsTable(); graphics = new SettingsTable(); sound = new SettingsTable(); prefs = new Table(); prefs.top(); prefs.margin(14f); rebuildMenu(); prefs.clearChildren(); prefs.add(menu); dataDialog = new BaseDialog("@settings.data"); dataDialog.addCloseButton(); dataDialog.cont.table(Tex.button, t -> { t.defaults().size(280f, 60f).left(); TextButtonStyle style = Styles.flatt; t.button("@settings.cleardata", Icon.trash, style, () -> ui.showConfirm("@confirm", "@settings.clearall.confirm", () -> { ObjectMap map = new ObjectMap<>(); for(String value : Core.settings.keys()){ if(value.contains("usid") || value.contains("uuid")){ map.put(value, Core.settings.get(value, null)); } } Core.settings.clear(); Core.settings.putAll(map); for(Fi file : dataDirectory.list()){ file.deleteDirectory(); } Core.app.exit(); })).marginLeft(4); t.row(); t.button("@settings.clearsaves", Icon.trash, style, () -> { ui.showConfirm("@confirm", "@settings.clearsaves.confirm", () -> { control.saves.deleteAll(); }); }).marginLeft(4); t.row(); t.button("@settings.clearresearch", Icon.trash, style, () -> { ui.showConfirm("@confirm", "@settings.clearresearch.confirm", () -> { universe.clearLoadoutInfo(); for(TechNode node : TechTree.all){ node.reset(); } content.each(c -> { if(c instanceof UnlockableContent u){ u.clearUnlock(); } }); settings.remove("unlocks"); }); }).marginLeft(4); t.row(); t.button("@settings.clearcampaignsaves", Icon.trash, style, () -> { ui.showConfirm("@confirm", "@settings.clearcampaignsaves.confirm", () -> { for(var planet : content.planets()){ for(var sec : planet.sectors){ sec.clearInfo(); if(sec.save != null){ sec.save.delete(); sec.save = null; } } } for(var slot : control.saves.getSaveSlots().copy()){ if(slot.isSector()){ slot.delete(); } } }); }).marginLeft(4); t.row(); t.button("@data.export", Icon.upload, style, () -> { if(ios){ Fi file = Core.files.local("mindustry-data-export.zip"); try{ exportData(file); }catch(Exception e){ ui.showException(e); } platform.shareFile(file); }else{ platform.showFileChooser(false, "zip", file -> { try{ exportData(file); ui.showInfo("@data.exported"); }catch(Exception e){ e.printStackTrace(); ui.showException(e); } }); } }).marginLeft(4); t.row(); t.button("@data.import", Icon.download, style, () -> ui.showConfirm("@confirm", "@data.import.confirm", () -> platform.showFileChooser(true, "zip", file -> { try{ importData(file); control.saves.resetSave(); state = new GameState(); Core.app.exit(); }catch(IllegalArgumentException e){ ui.showErrorMessage("@data.invalid"); }catch(Exception e){ e.printStackTrace(); if(e.getMessage() == null || !e.getMessage().contains("too short")){ ui.showException(e); }else{ ui.showErrorMessage("@data.invalid"); } } }))).marginLeft(4); if(!mobile){ t.row(); t.button("@data.openfolder", Icon.folder, style, () -> Core.app.openFolder(Core.settings.getDataDirectory().absolutePath())).marginLeft(4); } t.row(); t.button("@crash.export", Icon.upload, style, () -> { if(settings.getDataDirectory().child("crashes").list().length == 0 && !settings.getDataDirectory().child("last_log.txt").exists()){ ui.showInfo("@crash.none"); }else{ if(ios){ Fi logs = tmpDirectory.child("logs.txt"); logs.writeString(getLogs()); platform.shareFile(logs); }else{ platform.showFileChooser(false, "txt", file -> { try{ file.writeBytes(getLogs().getBytes(Strings.utf8)); app.post(() -> ui.showInfo("@crash.exported")); }catch(Throwable e){ ui.showException(e); } }); } } }).marginLeft(4); }); row(); pane(prefs).grow().top(); row(); add(buttons).fillX(); addSettings(); } String getLogs(){ Fi log = settings.getDataDirectory().child("last_log.txt"); StringBuilder out = new StringBuilder(); for(Fi fi : settings.getDataDirectory().child("crashes").list()){ out.append(fi.name()).append("\n\n").append(fi.readString()).append("\n"); } if(log.exists()){ out.append("\nlast log:\n").append(log.readString()); } return out.toString(); } /** Adds a custom settings category, with the icon being the specified region. */ public void addCategory(String name, @Nullable String region, Cons builder){ categories.add(new SettingsCategory(name, region == null ? null : new TextureRegionDrawable(atlas.find(region)), builder)); } /** Adds a custom settings category, for use in mods. The specified consumer should add all relevant mod settings to the table. */ public void addCategory(String name, @Nullable Drawable icon, Cons builder){ categories.add(new SettingsCategory(name, icon, builder)); } /** Adds a custom settings category, for use in mods. The specified consumer should add all relevant mod settings to the table. */ public void addCategory(String name, Cons builder){ addCategory(name, (Drawable)null, builder); } public Seq getCategories(){ return categories; } void rebuildMenu(){ menu.clearChildren(); TextButtonStyle style = Styles.flatt; float marg = 8f, isize = iconMed; menu.defaults().size(300f, 60f); menu.button("@settings.game", Icon.settings, style, isize, () -> visible(0)).marginLeft(marg).row(); menu.button("@settings.graphics", Icon.image, style, isize, () -> visible(1)).marginLeft(marg).row(); menu.button("@settings.sound", Icon.filters, style, isize, () -> visible(2)).marginLeft(marg).row(); menu.button("@settings.language", Icon.chat, style, isize, ui.language::show).marginLeft(marg).row(); if(!mobile || Core.settings.getBool("keyboard")){ menu.button("@settings.controls", Icon.move, style, isize, ui.controls::show).marginLeft(marg).row(); } menu.button("@settings.data", Icon.save, style, isize, () -> dataDialog.show()).marginLeft(marg).row(); int i = 3; for(var cat : categories){ int index = i; if(cat.icon == null){ menu.button(cat.name, style, () -> visible(index)).marginLeft(marg).row(); }else{ menu.button(cat.name, cat.icon, style, isize, () -> visible(index)).with(b -> ((Image)b.getChildren().get(1)).setScaling(Scaling.fit)).marginLeft(marg).row(); } i++; } } void addSettings(){ sound.sliderPref("musicvol", 100, 0, 100, 1, i -> i + "%"); sound.sliderPref("sfxvol", 100, 0, 100, 1, i -> i + "%"); sound.sliderPref("ambientvol", 100, 0, 100, 1, i -> i + "%"); game.sliderPref("saveinterval", 60, 10, 5 * 120, 10, i -> Core.bundle.format("setting.seconds", i)); if(mobile){ game.checkPref("autotarget", true); if(!ios){ game.checkPref("keyboard", false, val -> { control.setInput(val ? new DesktopInput() : new MobileInput()); input.setUseKeyboard(val); }); if(Core.settings.getBool("keyboard")){ control.setInput(new DesktopInput()); input.setUseKeyboard(true); } }else{ Core.settings.put("keyboard", false); } } //the issue with touchscreen support on desktop is that: //1) I can't test it //2) the SDL backend doesn't support multitouch /*else{ game.checkPref("touchscreen", false, val -> control.setInput(!val ? new DesktopInput() : new MobileInput())); if(Core.settings.getBool("touchscreen")){ control.setInput(new MobileInput()); } }*/ if(!mobile){ game.checkPref("crashreport", true); } game.checkPref("savecreate", true); game.checkPref("blockreplace", true); game.checkPref("conveyorpathfinding", true); game.checkPref("hints", true); game.checkPref("logichints", true); if(!mobile){ game.checkPref("backgroundpause", true); game.checkPref("buildautopause", false); } game.checkPref("doubletapmine", false); game.checkPref("commandmodehold", true); if(!ios){ game.checkPref("modcrashdisable", true); } if(steam){ game.sliderPref("playerlimit", 16, 2, 32, i -> { platform.updateLobby(); return i + ""; }); if(!Version.modifier.contains("beta")){ game.checkPref("steampublichost", false, i -> { platform.updateLobby(); }); } } if(!mobile){ game.checkPref("console", false); } int[] lastUiScale = {settings.getInt("uiscale", 100)}; graphics.sliderPref("uiscale", 100, 25, 300, 5, s -> { //if the user changed their UI scale, but then put it back, don't consider it 'changed' Core.settings.put("uiscalechanged", s != lastUiScale[0]); return s + "%"; }); graphics.sliderPref("screenshake", 4, 0, 8, i -> (i / 4f) + "x"); graphics.sliderPref("bloomintensity", 6, 0, 16, i -> (int)(i/4f * 100f) + "%"); graphics.sliderPref("bloomblur", 2, 1, 16, i -> i + "x"); graphics.sliderPref("fpscap", 240, 10, 245, 5, s -> (s > 240 ? Core.bundle.get("setting.fpscap.none") : Core.bundle.format("setting.fpscap.text", s))); graphics.sliderPref("chatopacity", 100, 0, 100, 5, s -> s + "%"); graphics.sliderPref("lasersopacity", 100, 0, 100, 5, s -> { if(ui.settings != null){ Core.settings.put("preferredlaseropacity", s); } return s + "%"; }); graphics.sliderPref("bridgeopacity", 100, 0, 100, 5, s -> s + "%"); if(!mobile){ graphics.checkPref("vsync", true, b -> Core.graphics.setVSync(b)); graphics.checkPref("fullscreen", false, b -> { if(b && settings.getBool("borderlesswindow")){ Core.graphics.setWindowedMode(Core.graphics.getWidth(), Core.graphics.getHeight()); settings.put("borderlesswindow", false); graphics.rebuild(); } if(b){ Core.graphics.setFullscreen(); }else{ Core.graphics.setWindowedMode(Core.graphics.getWidth(), Core.graphics.getHeight()); } }); graphics.checkPref("borderlesswindow", false, b -> { if(b && settings.getBool("fullscreen")){ Core.graphics.setWindowedMode(Core.graphics.getWidth(), Core.graphics.getHeight()); settings.put("fullscreen", false); graphics.rebuild(); } Core.graphics.setBorderless(b); }); Core.graphics.setVSync(Core.settings.getBool("vsync")); if(Core.settings.getBool("fullscreen")){ Core.app.post(() -> Core.graphics.setFullscreen()); } if(Core.settings.getBool("borderlesswindow")){ Core.app.post(() -> Core.graphics.setBorderless(true)); } }else if(!ios){ graphics.checkPref("landscape", false, b -> { if(b){ platform.beginForceLandscape(); }else{ platform.endForceLandscape(); } }); if(Core.settings.getBool("landscape")){ platform.beginForceLandscape(); } } graphics.checkPref("effects", true); graphics.checkPref("atmosphere", !mobile); graphics.checkPref("destroyedblocks", true); graphics.checkPref("blockstatus", false); graphics.checkPref("playerchat", true); if(!mobile){ graphics.checkPref("coreitems", true); } graphics.checkPref("minimap", !mobile); graphics.checkPref("smoothcamera", true); graphics.checkPref("position", false); if(!mobile){ graphics.checkPref("mouseposition", false); } graphics.checkPref("fps", false); graphics.checkPref("playerindicators", true); graphics.checkPref("indicators", true); graphics.checkPref("showweather", true); graphics.checkPref("animatedwater", true); if(Shaders.shield != null){ graphics.checkPref("animatedshields", !mobile); } graphics.checkPref("bloom", true, val -> renderer.toggleBloom(val)); graphics.checkPref("pixelate", false, val -> { if(val){ Events.fire(Trigger.enablePixelation); } }); //iOS (and possibly Android) devices do not support linear filtering well, so disable it if(!ios){ graphics.checkPref("linear", !mobile, b -> { for(Texture tex : Core.atlas.getTextures()){ TextureFilter filter = b ? TextureFilter.linear : TextureFilter.nearest; tex.setFilter(filter, filter); } }); }else{ settings.put("linear", false); } if(Core.settings.getBool("linear")){ for(Texture tex : Core.atlas.getTextures()){ TextureFilter filter = TextureFilter.linear; tex.setFilter(filter, filter); } } graphics.checkPref("skipcoreanimation", false); graphics.checkPref("hidedisplays", false); if(OS.isMac){ graphics.checkPref("macnotch", false); } if(!mobile){ Core.settings.put("swapdiagonal", false); } } public void exportData(Fi file) throws IOException{ Seq files = new Seq<>(); files.add(Core.settings.getSettingsFile()); files.addAll(customMapDirectory.list()); files.addAll(saveDirectory.list()); files.addAll(modDirectory.list()); files.addAll(schematicDirectory.list()); String base = Core.settings.getDataDirectory().path(); //add directories for(Fi other : files.copy()){ Fi parent = other.parent(); while(!files.contains(parent) && !parent.equals(settings.getDataDirectory())){ files.add(parent); } } try(OutputStream fos = file.write(false, 2048); ZipOutputStream zos = new ZipOutputStream(fos)){ for(Fi add : files){ String path = add.path().substring(base.length()); if(add.isDirectory()) path += "/"; //fix trailing / in path path = path.startsWith("/") ? path.substring(1) : path; zos.putNextEntry(new ZipEntry(path)); if(!add.isDirectory()){ try(var stream = add.read()){ Streams.copy(stream, zos); } } zos.closeEntry(); } } } public void importData(Fi file){ Fi dest = Core.files.local("zipdata.zip"); file.copyTo(dest); Fi zipped = new ZipFi(dest); Fi base = Core.settings.getDataDirectory(); if(!zipped.child("settings.bin").exists()){ throw new IllegalArgumentException("Not valid save data."); } //delete old saves so they don't interfere saveDirectory.deleteDirectory(); //purge existing tmp data, keep everything else tmpDirectory.deleteDirectory(); zipped.walk(f -> f.copyTo(base.child(f.path()))); dest.delete(); //clear old data settings.clear(); //load data so it's saved on exit settings.load(); } private void back(){ rebuildMenu(); prefs.clearChildren(); prefs.add(menu); } private void visible(int index){ prefs.clearChildren(); Seq tables = new Seq<>(); tables.addAll(game, graphics, sound); for(var custom : categories){ tables.add(custom.table); } prefs.add(tables.get(index)); } @Override public void addCloseButton(){ buttons.button("@back", Icon.left, () -> { if(prefs.getChildren().first() != menu){ back(); }else{ hide(); } }).size(210f, 64f); keyDown(key -> { if(key == KeyCode.escape || key == KeyCode.back){ if(prefs.getChildren().first() != menu){ back(); }else{ hide(); } } }); } public interface StringProcessor{ String get(int i); } public static class SettingsCategory{ public String name; public @Nullable Drawable icon; public Cons builder; public SettingsTable table; public SettingsCategory(String name, Drawable icon, Cons builder){ this.name = name; this.icon = icon; this.builder = builder; table = new SettingsTable(); builder.get(table); } } public static class SettingsTable extends Table{ protected Seq list = new Seq<>(); public SettingsTable(){ left(); } public Seq getSettings(){ return list; } public void pref(Setting setting){ list.add(setting); rebuild(); } public SliderSetting sliderPref(String name, int def, int min, int max, StringProcessor s){ return sliderPref(name, def, min, max, 1, s); } public SliderSetting sliderPref(String name, int def, int min, int max, int step, StringProcessor s){ SliderSetting res; list.add(res = new SliderSetting(name, def, min, max, step, s)); settings.defaults(name, def); rebuild(); return res; } public void checkPref(String name, boolean def){ list.add(new CheckSetting(name, def, null)); settings.defaults(name, def); rebuild(); } public void checkPref(String name, boolean def, Boolc changed){ list.add(new CheckSetting(name, def, changed)); settings.defaults(name, def); rebuild(); } public void textPref(String name, String def){ list.add(new TextSetting(name, def, null)); settings.defaults(name, def); rebuild(); } public void textPref(String name, String def, Cons changed){ list.add(new TextSetting(name, def, changed)); settings.defaults(name, def); rebuild(); } public void areaTextPref(String name, String def){ list.add(new AreaTextSetting(name, def, null)); settings.defaults(name, def); rebuild(); } public void areaTextPref(String name, String def, Cons changed){ list.add(new AreaTextSetting(name, def, changed)); settings.defaults(name, def); rebuild(); } public void rebuild(){ clearChildren(); for(Setting setting : list){ setting.add(this); } button(bundle.get("settings.reset", "Reset to Defaults"), () -> { for(Setting setting : list){ if(setting.name == null || setting.title == null) continue; settings.remove(setting.name); } rebuild(); }).margin(14).width(240f).pad(6); } public abstract static class Setting{ public String name; public String title; public @Nullable String description; public Setting(String name){ this.name = name; String winkey = "setting." + name + ".name.windows"; title = OS.isWindows && bundle.has(winkey) ? bundle.get(winkey) : bundle.get("setting." + name + ".name", name); description = bundle.getOrNull("setting." + name + ".description"); } public abstract void add(SettingsTable table); public void addDesc(Element elem){ ui.addDescTooltip(elem, description); } } public static class CheckSetting extends Setting{ boolean def; Boolc changed; public CheckSetting(String name, boolean def, Boolc changed){ super(name); this.def = def; this.changed = changed; } @Override public void add(SettingsTable table){ CheckBox box = new CheckBox(title); box.update(() -> box.setChecked(settings.getBool(name))); box.changed(() -> { settings.put(name, box.isChecked()); if(changed != null){ changed.get(box.isChecked()); } }); box.left(); addDesc(table.add(box).left().padTop(3f).get()); table.row(); } } public static class SliderSetting extends Setting{ int def, min, max, step; StringProcessor sp; public SliderSetting(String name, int def, int min, int max, int step, StringProcessor s){ super(name); this.def = def; this.min = min; this.max = max; this.step = step; this.sp = s; } @Override public void add(SettingsTable table){ Slider slider = new Slider(min, max, step, false); slider.setValue(settings.getInt(name)); Label value = new Label("", Styles.outlineLabel); Table content = new Table(); content.add(title, Styles.outlineLabel).left().growX().wrap(); content.add(value).padLeft(10f).right(); content.margin(3f, 33f, 3f, 33f); content.touchable = Touchable.disabled; slider.changed(() -> { settings.put(name, (int)slider.getValue()); value.setText(sp.get((int)slider.getValue())); }); slider.change(); addDesc(table.stack(slider, content).width(Math.min(Core.graphics.getWidth() / 1.2f, 460f)).left().padTop(4f).get()); table.row(); } } public static class TextSetting extends Setting{ String def; Cons changed; public TextSetting(String name, String def, Cons changed){ super(name); this.def = def; this.changed = changed; } @Override public void add(SettingsTable table){ TextField field = new TextField(); field.update(() -> field.setText(settings.getString(name))); field.changed(() -> { settings.put(name, field.getText()); if(changed != null){ changed.get(field.getText()); } }); Table prefTable = table.table().left().padTop(3f).get(); prefTable.add(field); prefTable.label(() -> title); addDesc(prefTable); table.row(); } } public static class AreaTextSetting extends TextSetting{ public AreaTextSetting(String name, String def, Cons changed){ super(name, def, changed); } @Override public void add(SettingsTable table){ TextArea area = new TextArea(""); area.setPrefRows(5); area.update(() -> { area.setText(settings.getString(name)); area.setWidth(table.getWidth()); }); area.changed(() -> { settings.put(name, area.getText()); if(changed != null){ changed.get(area.getText()); } }); addDesc(table.label(() -> title).left().padTop(3f).get()); table.row().add(area).left(); table.row(); } } } }