Files
Mindustry/core/src/mindustry/ui/dialogs/SettingsMenuDialog.java
2023-07-26 10:31:18 -04:00

848 lines
28 KiB
Java

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<SettingsCategory> 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<String, Object> 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<SettingsTable> 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<SettingsTable> 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<SettingsTable> builder){
addCategory(name, (Drawable)null, builder);
}
public Seq<SettingsCategory> 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<Fi> 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<Table> 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<SettingsTable> builder;
public SettingsTable table;
public SettingsCategory(String name, Drawable icon, Cons<SettingsTable> builder){
this.name = name;
this.icon = icon;
this.builder = builder;
table = new SettingsTable();
builder.get(table);
}
}
public static class SettingsTable extends Table{
protected Seq<Setting> list = new Seq<>();
public SettingsTable(){
left();
}
public Seq<Setting> 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<String> 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<String> 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<String> changed;
public TextSetting(String name, String def, Cons<String> 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<String> 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();
}
}
}
}