Merge branch 'master' into port-field

This commit is contained in:
Anuken
2025-02-08 19:36:26 -05:00
committed by GitHub
2681 changed files with 151338 additions and 45271 deletions

View File

@@ -35,36 +35,36 @@ public class AboutDialog extends BaseDialog{
buttons.clear();
float h = Core.graphics.isPortrait() ? 90f : 80f;
float w = Core.graphics.isPortrait() ? 330f : 600f;
float w = Core.graphics.isPortrait() ? 400f : 600f;
Table in = new Table();
ScrollPane pane = new ScrollPane(in);
for(LinkEntry link : Links.getLinks()){
if((ios || OS.isMac || steam) && bannedItems.contains(link.name)){
if((ios || steam) && bannedItems.contains(link.name)){
continue;
}
Table table = new Table(Tex.underline);
Table table = new Table(Styles.grayPanel);
table.margin(0);
table.table(img -> {
img.image().height(h - 5).width(40f).color(link.color);
img.row();
img.image().height(5).width(40f).color(link.color.cpy().mul(0.8f, 0.8f, 0.8f, 1f));
img.image().height(5).width(40f).color(link.color.cpy().mul(0.6f, 0.6f, 0.8f, 1f));
}).expandY();
table.table(i -> {
i.background(Tex.buttonEdge3);
i.background(Styles.grayPanel);
i.image(link.icon);
}).size(h - 5, h);
table.table(inset -> {
inset.add("[accent]" + link.title).growX().left();
inset.row();
inset.labelWrap(link.description).width(w - 100f).color(Color.lightGray).growX();
inset.labelWrap(link.description).width(w - 100f - h).color(Color.lightGray).growX();
}).padLeft(8);
table.button(Icon.link, () -> {
table.button(Icon.link, Styles.clearNonei, () -> {
if(link.name.equals("wiki")) Events.fire(Trigger.openWiki);
if(!Core.app.openURI(link.link)){
@@ -84,12 +84,6 @@ public class AboutDialog extends BaseDialog{
buttons.button("@credits", this::showCredits).size(200f, 64f);
if(Core.graphics.isPortrait()){
for(Cell<?> cell : buttons.getCells()){
cell.width(140f);
}
}
}
public void showCredits(){

View File

@@ -1,5 +1,6 @@
package mindustry.ui.dialogs;
import arc.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import mindustry.gen.*;
@@ -39,7 +40,7 @@ public class AdminsDialog extends BaseDialog{
res.labelWrap("[lightgray]" + info.lastName).width(w - h - 24f);
res.add().growX();
res.button(Icon.cancel, () -> {
ui.showConfirm("@confirm", "@confirmunadmin", () -> {
ui.showConfirm("@confirm", Core.bundle.format("@confirmunadmin", info.lastName), () -> {
netServer.admins.unAdminPlayer(info.id);
Groups.player.each(player -> {
if(player != null && !player.isLocal() && player.uuid().equals(info.id)){

View File

@@ -11,7 +11,8 @@ import mindustry.graphics.*;
import static mindustry.Vars.*;
public class BaseDialog extends Dialog{
private boolean wasPaused;
protected boolean wasPaused;
/** If true, this dialog will pause the game while open. */
protected boolean shouldPause;
public BaseDialog(String title, DialogStyle style){
@@ -19,20 +20,17 @@ public class BaseDialog extends Dialog{
setFillParent(true);
this.title.setAlignment(Align.center);
titleTable.row();
titleTable.image(Tex.whiteui, Pal.accent)
.growX().height(3f).pad(4f);
titleTable.image(Tex.whiteui, Pal.accent).growX().height(3f).pad(4f);
hidden(() -> {
if(shouldPause && state.isGame()){
if(!wasPaused || net.active()){
state.set(State.playing);
}
if(shouldPause && state.isGame() && !net.active() && !wasPaused){
state.set(State.playing);
}
Sounds.back.play();
});
shown(() -> {
if(shouldPause && state.isGame()){
if(shouldPause && state.isGame() && !net.active()){
wasPaused = state.is(State.paused);
state.set(State.paused);
}
@@ -43,6 +41,14 @@ public class BaseDialog extends Dialog{
this(title, Core.scene.getStyle(DialogStyle.class));
}
/** Places the buttons as an overlay on top of the content. Used when the content can be scrolled through.*/
protected void makeButtonOverlay(){
clearChildren();
add(titleTable).growX().row();
stack(cont, buttons).grow();
buttons.bottom();
}
protected void onResize(Runnable run){
Events.on(ResizeEvent.class, event -> {
if(isShown() && Core.scene.getDialog() == this){
@@ -56,11 +62,15 @@ public class BaseDialog extends Dialog{
closeOnBack();
}
@Override
public void addCloseButton(){
buttons.defaults().size(210f, 64f);
buttons.button("@back", Icon.left, this::hide).size(210f, 64f);
public void addCloseButton(float width){
buttons.defaults().size(width, 64f);
buttons.button("@back", Icon.left, this::hide).size(width, 64f);
addCloseListener();
}
@Override
public void addCloseButton(){
addCloseButton(210f);
}
}

View File

@@ -0,0 +1,45 @@
package mindustry.ui.dialogs;
import arc.*;
import arc.math.*;
import arc.scene.actions.*;
import mindustry.*;
import mindustry.core.*;
import mindustry.gen.*;
import mindustry.type.*;
import static arc.scene.actions.Actions.*;
public class CampaignCompleteDialog extends BaseDialog{
public CampaignCompleteDialog(){
super("");
addCloseListener();
shouldPause = true;
buttons.defaults().size(210f, 64f);
buttons.button("@menu", Icon.left, () -> {
hide();
Vars.ui.paused.runExitSave();
});
buttons.button("@continue", Icon.ok, this::hide);
}
public void show(Planet planet){
cont.clear();
cont.add(Core.bundle.format("campaign.complete", "[#" + planet.iconColor + "]" + planet.localizedName + "[]")).row();
float playtime = planet.sectors.sumf(s -> s.hasSave() ? s.save.meta.timePlayed : 0) / 1000f;
//TODO needs more info?
cont.add(Core.bundle.format("campaign.playtime", UI.formatTime(playtime))).left().row();
setTranslation(0f, -Core.graphics.getHeight());
color.a = 0f;
show(Core.scene, Actions.sequence(parallel(fadeIn(1.1f, Interp.fade), translateBy(0f, Core.graphics.getHeight(), 6f, Interp.pow5Out))));
}
}

View File

@@ -0,0 +1,100 @@
package mindustry.ui.dialogs;
import arc.*;
import arc.func.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import mindustry.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.type.*;
import mindustry.ui.*;
public class CampaignRulesDialog extends BaseDialog{
Planet planet;
Table current;
public CampaignRulesDialog(){
super("@campaign.difficulty");
addCloseButton();
hidden(() -> {
if(planet != null){
planet.saveRules();
if(Vars.state.isGame() && Vars.state.isCampaign() && Vars.state.getPlanet() == planet){
planet.campaignRules.apply(planet, Vars.state.rules);
Call.setRules(Vars.state.rules);
}
}
});
onResize(() -> {
rebuild();
});
}
void rebuild(){
CampaignRules rules = planet.campaignRules;
cont.clear();
cont.top().pane(inner -> {
inner.top().left().defaults().fillX().left().pad(5);
current = inner;
current.table(Tex.button, t -> {
t.margin(10f);
var group = new ButtonGroup<>();
var style = Styles.flatTogglet;
t.defaults().size(140f, 50f);
for(Difficulty diff : Difficulty.all){
t.button(diff.localized(), style, () -> {
rules.difficulty = diff;
}).group(group).checked(b -> rules.difficulty == diff);
if(Core.graphics.isPortrait() && diff.ordinal() % 2 == 1){
t.row();
}
}
}).left().fill(false).expand(false, false).row();
if(planet.allowSectorInvasion){
check("@rules.invasions", b -> rules.sectorInvasion = b, () -> rules.sectorInvasion);
}
check("@rules.fog", b -> rules.fog = b, () -> rules.fog);
check("@rules.showspawns", b -> rules.showSpawns = b, () -> rules.showSpawns);
check("@rules.randomwaveai", b -> rules.randomWaveAI = b, () -> rules.randomWaveAI);
//TODO: this is intentionally hidden until the new mechanics have been well-tested. I don't want people immediately switching to the old mechanics
if(planet.allowLegacyLaunchPads){
// check("@rules.legacylaunchpads", b -> rules.legacyLaunchPads = b, () -> rules.legacyLaunchPads);
}
}).growY();
}
public void show(Planet planet){
this.planet = planet;
rebuild();
show();
}
void check(String text, Boolc cons, Boolp prov){
check(text, cons, prov, () -> true);
}
void check(String text, Boolc cons, Boolp prov, Boolp condition){
String infoText = text.substring(1) + ".info";
var cell = current.check(text, cons).checked(prov.get()).update(a -> a.setDisabled(!condition.get()));
if(Core.bundle.has(infoText)){
cell.tooltip(text + ".info");
}
cell.get().left();
current.row();
}
}

View File

@@ -2,13 +2,21 @@ package mindustry.ui.dialogs;
import arc.func.*;
import arc.graphics.*;
import arc.graphics.Texture.*;
import arc.graphics.g2d.*;
import arc.scene.*;
import arc.scene.ui.*;
import arc.util.*;
import mindustry.gen.*;
import mindustry.graphics.*;
public class ColorPicker extends BaseDialog{
private static Texture hueTex;
private Cons<Color> cons = c -> {};
Color current = new Color();
float h, s, v, a;
TextField hexField;
Slider hSlider, sSlider, vSlider, aSlider;
public ColorPicker(){
super("@pickcolor");
@@ -23,6 +31,17 @@ public class ColorPicker extends BaseDialog{
this.cons = consumer;
show();
if(hueTex == null){
hueTex = Pixmaps.hueTexture(128, 1);
hueTex.setFilter(TextureFilter.linear);
}
float[] values = color.toHsv(new float[3]);
h = values[0];
s = values[1];
v = values[2];
a = color.a;
cont.clear();
cont.pane(t -> {
t.table(Tex.pane, i -> {
@@ -32,26 +51,114 @@ public class ColorPicker extends BaseDialog{
}}).size(200f);
}).colspan(2).padBottom(5);
float w = 150f;
t.row();
t.defaults().padBottom(4);
t.add("R").color(Pal.remove);
t.slider(0f, 1f, 0.01f, current.r, current::r).width(w);
t.row();
t.add("G").color(Color.lime);
t.slider(0f, 1f, 0.01f, current.g, current::g).width(w);
t.row();
t.add("B").color(Color.royal);
t.slider(0f, 1f, 0.01f, current.b, current::b).width(w);
t.row();
t.defaults().padBottom(6).width(370f).height(44f);
t.stack(new Image(new TextureRegion(hueTex)), hSlider = new Slider(0f, 360f, 0.3f, false){{
setValue(h);
moved(value -> {
h = value;
updateColor();
});
}}).row();
t.stack(new Element(){
@Override
public void draw(){
float first = Tmp.c1.set(current).saturation(0f).a(parentAlpha).toFloatBits();
float second = Tmp.c1.set(current).saturation(1f).a(parentAlpha).toFloatBits();
Fill.quad(
x, y, first,
x + width, y, second,
x + width, y + height, second,
x, y + height, first
);
}
}, sSlider = new Slider(0f, 1f, 0.001f, false){{
setValue(s);
moved(value -> {
s = value;
updateColor();
});
}}).row();
t.stack(new Element(){
@Override
public void draw(){
float first = Tmp.c1.set(current).value(0f).a(parentAlpha).toFloatBits();
float second = Tmp.c1.fromHsv(h, s, 1f).a(parentAlpha).toFloatBits();
Fill.quad(
x, y, first,
x + width, y, second,
x + width, y + height, second,
x, y + height, first
);
}
}, vSlider = new Slider(0f, 1f, 0.001f, false){{
setValue(v);
moved(value -> {
v = value;
updateColor();
});
}}).row();
if(alpha){
t.add("A");
t.slider(0f, 1f, 0.01f, current.a, current::a).width(w);
t.row();
t.stack(new Image(Tex.alphaBgLine), new Element(){
@Override
public void draw(){
float first = Tmp.c1.set(current).a(0f).toFloatBits();
float second = Tmp.c1.set(current).a(parentAlpha).toFloatBits();
Fill.quad(
x, y, first,
x + width, y, second,
x + width, y + height, second,
x, y + height, first
);
}
}, aSlider = new Slider(0f, 1f, 0.001f, false){{
setValue(a);
moved(value -> {
a = value;
updateColor();
});
}}).row();
}
});
hexField = t.field(current.toString(), value -> {
try{
Color.valueOf(current, value);
current.toHsv(values);
h = values[0];
s = values[1];
v = values[2];
a = current.a;
hSlider.setValue(h);
sSlider.setValue(s);
vSlider.setValue(v);
if(aSlider != null){
aSlider.setValue(a);
}
updateColor(false);
}catch(Exception ignored){
}
}).size(130f, 40f).valid(text -> {
//garbage performance but who cares this runs only every key type anyway
try{
Color.valueOf(text);
return true;
}catch(Exception e){
return false;
}
}).get();
}).grow();
buttons.clear();
addCloseButton();
@@ -60,4 +167,21 @@ public class ColorPicker extends BaseDialog{
hide();
});
}
void updateColor(){
updateColor(true);
}
void updateColor(boolean updateField){
current.fromHsv(h, s, v);
current.a = a;
if(hexField != null && updateField){
String val = current.toString();
if(current.a >= 0.9999f){
val = val.substring(0, 6);
}
hexField.setText(val);
}
}
}

View File

@@ -1,20 +1,32 @@
package mindustry.ui.dialogs;
import arc.*;
import arc.scene.actions.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import mindustry.ctype.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.ui.*;
import mindustry.input.*;
import mindustry.world.meta.*;
import static arc.Core.*;
import static mindustry.Vars.*;
public class ContentInfoDialog extends BaseDialog{
public ContentInfoDialog(){
super("@info.title");
addCloseButton();
keyDown(key -> {
if(key == keybinds.get(Binding.block_info).key){
Core.app.post(this::hide);
}
});
}
public void show(UnlockableContent content){
@@ -27,10 +39,8 @@ public class ContentInfoDialog extends BaseDialog{
content.checkStats();
table.table(title1 -> {
int size = 8 * 6;
title1.image(content.icon(Cicon.xlarge)).size(size).scaling(Scaling.fit);
title1.add("[accent]" + content.localizedName).padLeft(5);
title1.image(content.uiIcon).size(iconXLarge).scaling(Scaling.fit);
title1.add("[accent]" + content.localizedName + (settings.getBool("console") ? "\n[gray]" + content.name : "")).padLeft(5);
});
table.row();
@@ -59,36 +69,43 @@ public class ContentInfoDialog extends BaseDialog{
if(map.size == 0) continue;
//TODO check
if(stats.useCategories){
table.add("@category." + cat.name()).color(Pal.accent).fillX();
table.add("@category." + cat.name).color(Pal.accent).fillX();
table.row();
}
for(Stat stat : map.keys()){
table.table(inset -> {
inset.left();
inset.add("[lightgray]" + stat.localized() + ":[] ").left();
inset.add("[lightgray]" + stat.localized() + ":[] ").left().top();
Seq<StatValue> arr = map.get(stat);
for(StatValue value : arr){
value.display(inset);
inset.add().size(10f);
}
}).fillX().padLeft(10);
table.row();
}
}
if(content.details != null){
table.add("[gray]" + content.details).pad(6).padTop(20).width(400f).wrap().fillX();
table.add("[gray]" + (content.unlocked() || !content.hideDetails ? content.details : Iconc.lock + " " + Core.bundle.get("unlock.incampaign"))).pad(6).padTop(20).width(400f).wrap().fillX();
table.row();
}
content.displayExtra(table);
ScrollPane pane = new ScrollPane(table);
table.marginRight(30f);
//TODO: some things (e.g. reconstructor requirements) are too long and screw up the layout
//pane.setScrollingDisabled(true, false);
cont.add(pane);
show();
if(isShown()){
show(scene, Actions.fadeIn(0f));
}else{
show();
}
}
}

View File

@@ -1,26 +0,0 @@
package mindustry.ui.dialogs;
import arc.input.*;
import arc.scene.ui.*;
import arc.util.*;
import mindustry.gen.*;
import mindustry.graphics.*;
public class ControlsDialog extends KeybindDialog{
public ControlsDialog(){
setFillParent(true);
title.setAlignment(Align.center);
titleTable.row();
titleTable.add(new Image()).growX().height(3f).pad(4f).get().setColor(Pal.accent);
}
@Override
public void addCloseButton(){
buttons.button("@back", Icon.left, this::hide).size(230f, 64f);
keyDown(key -> {
if(key == KeyCode.escape || key == KeyCode.back) hide();
});
}
}

View File

@@ -1,92 +1,16 @@
package mindustry.ui.dialogs;
import arc.*;
import arc.graphics.g2d.*;
import arc.scene.style.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.util.*;
import mindustry.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.maps.*;
import mindustry.ui.*;
public class CustomGameDialog extends BaseDialog{
public class CustomGameDialog extends MapListDialog{
private MapPlayDialog dialog = new MapPlayDialog();
public CustomGameDialog(){
super("@customgame");
addCloseButton();
shown(this::setup);
onResize(this::setup);
super("@customgame", false);
}
void setup(){
clearChildren();
add(titleTable);
row();
stack(cont, buttons).grow();
buttons.bottom();
cont.clear();
Table maps = new Table();
maps.marginRight(14);
maps.marginBottom(55f);
ScrollPane pane = new ScrollPane(maps);
pane.setFadeScrollBars(false);
int maxwidth = Math.max((int)(Core.graphics.getWidth() / Scl.scl(210)), 1);
float images = 146f;
int i = 0;
maps.defaults().width(170).fillY().top().pad(4f);
for(Map map : Vars.maps.all()){
if(i % maxwidth == 0){
maps.row();
}
ImageButton image = new ImageButton(new TextureRegion(map.safeTexture()), Styles.cleari);
image.margin(5);
image.top();
Image img = image.getImage();
img.remove();
image.row();
image.table(t -> {
t.left();
for(Gamemode mode : Gamemode.all){
TextureRegionDrawable icon = Vars.ui.getIcon("mode" + Strings.capitalize(mode.name()) + "Small");
if(mode.valid(map) && Core.atlas.isFound(icon.getRegion())){
t.image(icon).size(16f).pad(4f);
}
}
}).left();
image.row();
image.add(map.name()).pad(1f).growX().wrap().left().get().setEllipsis(true);
image.row();
image.image(Tex.whiteui, Pal.gray).growX().pad(3).height(4f);
image.row();
image.add(img).size(images);
BorderImage border = new BorderImage(map.safeTexture(), 3f);
border.setScaling(Scaling.fit);
image.replaceImage(border);
image.clicked(() -> dialog.show(map));
maps.add(image);
i++;
}
if(Vars.maps.all().size == 0){
maps.add("@maps.none").pad(50);
}
cont.add(pane).uniformX();
@Override
void showMap(Map map){
dialog.show(map);
}
}

View File

@@ -3,18 +3,20 @@ package mindustry.ui.dialogs;
import arc.*;
import arc.func.*;
import arc.graphics.*;
import arc.math.*;
import arc.scene.style.*;
import arc.scene.ui.*;
import arc.scene.ui.ImageButton.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import mindustry.*;
import mindustry.content.*;
import mindustry.ctype.*;
import mindustry.editor.BannedContentDialog;
import mindustry.game.*;
import mindustry.game.Rules.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.io.*;
import mindustry.type.*;
import mindustry.type.Weather.*;
import mindustry.ui.*;
@@ -28,87 +30,84 @@ public class CustomRulesDialog extends BaseDialog{
private Table main;
private Prov<Rules> resetter;
private LoadoutDialog loadoutDialog;
private BaseDialog banDialog;
private BannedContentDialog<Block> bannedBlocks = new BannedContentDialog<>("@bannedblocks", ContentType.block, Block::canBeBuilt);
private BannedContentDialog<UnitType> bannedUnits = new BannedContentDialog<>("@bannedunits", ContentType.unit, u -> !u.isHidden());
public boolean showRuleEditRule;
public Seq<Table> categories;
public Table current;
public Seq<String> categoryNames;
public String currentName = "";
public String ruleSearch = "";
public Seq<Runnable> additionalSetup; // for modding to easily add new rules
public CustomRulesDialog(){
this(false);
}
public CustomRulesDialog(boolean showRuleEditRule){
super("@mode.custom");
this.showRuleEditRule = showRuleEditRule;
loadoutDialog = new LoadoutDialog();
banDialog = new BaseDialog("@bannedblocks");
banDialog.addCloseButton();
banDialog.shown(this::rebuildBanned);
banDialog.buttons.button("@addall", Icon.add, () -> {
rules.bannedBlocks.addAll(content.blocks().select(Block::canBeBuilt));
rebuildBanned();
}).size(180, 64f);
banDialog.buttons.button("@clear", Icon.trash, () -> {
rules.bannedBlocks.clear();
rebuildBanned();
}).size(180, 64f);
setFillParent(true);
shown(this::setup);
addCloseButton();
}
private void rebuildBanned(){
float previousScroll = banDialog.cont.getChildren().isEmpty() ? 0f : ((ScrollPane)banDialog.cont.getChildren().first()).getScrollY();
banDialog.cont.clear();
banDialog.cont.pane(t -> {
t.margin(10f);
additionalSetup = new Seq<>();
categories = new Seq<>();
categoryNames = new Seq<>();
if(rules.bannedBlocks.isEmpty()){
t.add("@empty");
}
buttons.button("@edit", Icon.pencil, () -> {
BaseDialog dialog = new BaseDialog("@waves.edit");
dialog.addCloseButton();
dialog.setFillParent(false);
Seq<Block> array = Seq.with(rules.bannedBlocks);
array.sort();
dialog.cont.table(Tex.button, t -> {
var style = Styles.cleart;
t.defaults().size(280f, 64f).pad(2f);
int cols = mobile && Core.graphics.isPortrait() ? 1 : mobile ? 2 : 3;
int i = 0;
t.button("@waves.copy", Icon.copy, style, () -> {
ui.showInfoFade("@copied");
for(Block block : array){
t.table(Tex.underline, b -> {
b.left().margin(4f);
b.image(block.icon(Cicon.medium)).size(Cicon.medium.size).padRight(3);
b.add(block.localizedName).color(Color.lightGray).padLeft(3).growX().left().wrap();
//hack: don't write the spawns, they just waste space
var spawns = rules.spawns;
rules.spawns = new Seq<>();
Core.app.setClipboardText(JsonIO.write(rules));
rules.spawns = spawns;
dialog.hide();
}).marginLeft(12f).row();
b.button(Icon.cancel, Styles.clearPartiali, () -> {
rules.bannedBlocks.remove(block);
rebuildBanned();
}).size(70f).pad(-4f).padLeft(0f);
}).size(300f, 70f).padRight(5);
if(++i % cols == 0){
t.row();
}
}
}).get().setScrollYForce(previousScroll);
banDialog.cont.row();
banDialog.cont.button("@add", Icon.add, () -> {
BaseDialog dialog = new BaseDialog("@add");
dialog.cont.pane(t -> {
t.left().margin(14f);
int[] i = {0};
content.blocks().each(b -> !rules.bannedBlocks.contains(b) && b.canBeBuilt(), b -> {
int cols = mobile && Core.graphics.isPortrait() ? 4 : 12;
t.button(new TextureRegionDrawable(b.icon(Cicon.medium)), Styles.cleari, () -> {
rules.bannedBlocks.add(b);
rebuildBanned();
dialog.hide();
}).size(60f).get().resizeImage(Cicon.medium.size);
if(++i[0] % cols == 0){
t.row();
t.button("@waves.load", Icon.download, style, () -> {
try{
Rules newRules = JsonIO.read(Rules.class, Core.app.getClipboardText());
//objectives and spawns are considered to be map-specific; don't use them
newRules.spawns = rules.spawns;
newRules.objectives = rules.objectives;
rules = newRules;
refresh();
}catch(Throwable e){
Log.err(e);
ui.showErrorMessage("@rules.invaliddata");
}
});
dialog.hide();
}).disabled(Core.app.getClipboardText() == null || !Core.app.getClipboardText().startsWith("{")).marginLeft(12f).row();
t.button("@settings.reset", Icon.refresh, style, () -> {
rules = resetter.get();
refresh();
}).marginLeft(12f);
});
dialog.addCloseButton();
dialog.show();
}).size(300f, 64f);
});
}
void refresh(){
setup();
requestKeyboard();
requestScroll();
}
public void show(Rules rules, Prov<Rules> resetter){
@@ -118,125 +117,330 @@ public class CustomRulesDialog extends BaseDialog{
}
void setup(){
categories.clear();
cont.clear();
cont.pane(m -> main = m).get().setScrollingDisabled(true, false);
cont.table(t -> {
t.add("@search").padRight(10);
var field = t.field(ruleSearch, text -> {
ruleSearch = text.trim().replaceAll(" +", " ").toLowerCase();
setup();
}).grow().pad(8).get();
field.setCursorPosition(ruleSearch.length());
Core.scene.setKeyboardFocus(field);
t.button(Icon.cancel, Styles.emptyi, () -> {
ruleSearch = "";
setup();
}).padLeft(10f).size(35f);
t.button(Icon.zoom, Styles.emptyi, this::setup).size(54f);
}).row();
cont.pane(m -> main = m).scrollX(false);
main.margin(10f);
main.button("@settings.reset", () -> {
rules = resetter.get();
setup();
requestKeyboard();
requestScroll();
}).size(300f, 50f);
main.left().defaults().fillX().left().pad(5);
main.left().defaults().fillX().left();
main.row();
title("@rules.title.waves");
category("waves");
check("@rules.waves", b -> rules.waves = b, () -> rules.waves);
check("@rules.wavetimer", b -> rules.waveTimer = b, () -> rules.waveTimer);
check("@rules.waitForWaveToEnd", b -> rules.waitEnemies = b, () -> rules.waitEnemies);
number("@rules.wavespacing", false, f -> rules.waveSpacing = f * 60f, () -> rules.waveSpacing / 60f, () -> true, 1, Float.MAX_VALUE);
number("@rules.dropzoneradius", false, f -> rules.dropZoneRadius = f * tilesize, () -> rules.dropZoneRadius / tilesize, () -> true);
check("@rules.wavesending", b -> rules.waveSending = b, () -> rules.waveSending, () -> rules.waves);
check("@rules.wavetimer", b -> rules.waveTimer = b, () -> rules.waveTimer, () -> rules.waves);
check("@rules.waitForWaveToEnd", b -> rules.waitEnemies = b, () -> rules.waitEnemies, () -> rules.waves && rules.waveTimer);
check("@rules.randomwaveai", b -> rules.randomWaveAI = b, () -> rules.randomWaveAI, () -> rules.waves);
check("@rules.airUseSpawns", b -> rules.airUseSpawns = b, () -> rules.airUseSpawns, () -> rules.waves);
numberi("@rules.wavelimit", f -> rules.winWave = f, () -> rules.winWave, () -> rules.waves, 0, Integer.MAX_VALUE);
number("@rules.wavespacing", false, f -> rules.waveSpacing = f * 60f, () -> rules.waveSpacing / 60f, () -> rules.waves && rules.waveTimer, 1, Float.MAX_VALUE);
number("@rules.initialwavespacing", false, f -> rules.initialWaveSpacing = f * 60f, () -> rules.initialWaveSpacing / 60f, () -> rules.waves && rules.waveTimer, 0, Float.MAX_VALUE);
number("@rules.dropzoneradius", false, f -> rules.dropZoneRadius = f * tilesize, () -> rules.dropZoneRadius / tilesize, () -> rules.waves);
title("@rules.title.resourcesbuilding");
category("resourcesbuilding");
check("@rules.alloweditworldprocessors", b -> rules.allowEditWorldProcessors = b, () -> rules.allowEditWorldProcessors);
check("@rules.infiniteresources", b -> rules.infiniteResources = b, () -> rules.infiniteResources);
check("@rules.onlydepositcore", b -> rules.onlyDepositCore = b, () -> rules.onlyDepositCore);
check("@rules.derelictrepair", b -> rules.derelictRepair = b, () -> rules.derelictRepair);
check("@rules.reactorexplosions", b -> rules.reactorExplosions = b, () -> rules.reactorExplosions);
check("@rules.schematic", b -> rules.schematicsAllowed = b, () -> rules.schematicsAllowed);
check("@rules.coreincinerates", b -> rules.coreIncinerates = b, () -> rules.coreIncinerates);
check("@rules.cleanupdeadteams", b -> rules.cleanupDeadTeams = b, () -> rules.cleanupDeadTeams, () -> rules.pvp);
check("@rules.disableworldprocessors", b -> rules.disableWorldProcessors = b, () -> rules.disableWorldProcessors);
number("@rules.buildcostmultiplier", false, f -> rules.buildCostMultiplier = f, () -> rules.buildCostMultiplier, () -> !rules.infiniteResources);
number("@rules.buildspeedmultiplier", f -> rules.buildSpeedMultiplier = f, () -> rules.buildSpeedMultiplier, 0.00001f, 10000f);
number("@rules.buildspeedmultiplier", f -> rules.buildSpeedMultiplier = f, () -> rules.buildSpeedMultiplier, 0.001f, 50f);
number("@rules.deconstructrefundmultiplier", false, f -> rules.deconstructRefundMultiplier = f, () -> rules.deconstructRefundMultiplier, () -> !rules.infiniteResources);
number("@rules.blockhealthmultiplier", f -> rules.blockHealthMultiplier = f, () -> rules.blockHealthMultiplier);
number("@rules.blockdamagemultiplier", f -> rules.blockDamageMultiplier = f, () -> rules.blockDamageMultiplier);
main.button("@configure",
() -> loadoutDialog.show(Blocks.coreShard.itemCapacity, rules.loadout,
i -> true,
() -> rules.loadout.clear().add(new ItemStack(Items.copper, 100)),
() -> {}, () -> {}
)).left().width(300f);
main.row();
if(Core.bundle.get("configure").toLowerCase().contains(ruleSearch)){
current.button("@configure",
() -> loadoutDialog.show(999999, rules.loadout,
i -> true,
() -> rules.loadout.clear().add(new ItemStack(Items.copper, 100)),
() -> {}, () -> {}
)).left().width(300f).row();
}
main.button("@bannedblocks", banDialog::show).left().width(300f);
main.row();
if(Core.bundle.get("bannedblocks").toLowerCase().contains(ruleSearch)){
current.button("@bannedblocks", () -> bannedBlocks.show(rules.bannedBlocks)).left().width(300f).row();
}
check("@rules.hidebannedblocks", b -> rules.hideBannedBlocks = b, () -> rules.hideBannedBlocks);
check("@bannedblocks.whitelist", b -> rules.blockWhitelist = b, () -> rules.blockWhitelist);
title("@rules.title.unit");
check("@rules.unitammo", b -> rules.unitAmmo = b, () -> rules.unitAmmo);
number("@rules.unithealthmultiplier", f -> rules.unitHealthMultiplier = f, () -> rules.unitHealthMultiplier);
category("unit");
check("@rules.unitcapvariable", b -> rules.unitCapVariable = b, () -> rules.unitCapVariable);
check("@rules.unitpayloadsexplode", b -> rules.unitPayloadsExplode = b, () -> rules.unitPayloadsExplode);
numberi("@rules.unitcap", f -> rules.unitCap = f, () -> rules.unitCap, -999, 999);
number("@rules.unitdamagemultiplier", f -> rules.unitDamageMultiplier = f, () -> rules.unitDamageMultiplier);
number("@rules.unitbuildspeedmultiplier", f -> rules.unitBuildSpeedMultiplier = f, () -> rules.unitBuildSpeedMultiplier, 0.00001f, 100f);
number("@rules.unitcrashdamagemultiplier", f -> rules.unitCrashDamageMultiplier = f, () -> rules.unitCrashDamageMultiplier);
number("@rules.unitminespeedmultiplier", f -> rules.unitMineSpeedMultiplier = f, () -> rules.unitMineSpeedMultiplier);
number("@rules.unitbuildspeedmultiplier", f -> rules.unitBuildSpeedMultiplier = f, () -> rules.unitBuildSpeedMultiplier, 0f, 50f);
number("@rules.unitcostmultiplier", f -> rules.unitCostMultiplier = f, () -> rules.unitCostMultiplier);
number("@rules.unithealthmultiplier", f -> rules.unitHealthMultiplier = f, () -> rules.unitHealthMultiplier);
title("@rules.title.enemy");
if(Core.bundle.get("bannedunits").toLowerCase().contains(ruleSearch)){
current.button("@bannedunits", () -> bannedUnits.show(rules.bannedUnits)).left().width(300f).row();
}
check("@bannedunits.whitelist", b -> rules.unitWhitelist = b, () -> rules.unitWhitelist);
category("enemy");
check("@rules.attack", b -> rules.attackMode = b, () -> rules.attackMode);
check("@rules.buildai", b -> rules.teams.get(rules.waveTeam).ai = rules.teams.get(rules.waveTeam).infiniteResources = b, () -> rules.teams.get(rules.waveTeam).ai);
number("@rules.enemycorebuildradius", f -> rules.enemyCoreBuildRadius = f * tilesize, () -> Math.min(rules.enemyCoreBuildRadius / tilesize, 200));
check("@rules.corecapture", b -> rules.coreCapture = b, () -> rules.coreCapture);
check("@rules.placerangecheck", b -> rules.placeRangeCheck = b, () -> rules.placeRangeCheck);
check("@rules.polygoncoreprotection", b -> rules.polygonCoreProtection = b, () -> rules.polygonCoreProtection);
number("@rules.enemycorebuildradius", f -> rules.enemyCoreBuildRadius = f * tilesize, () -> Math.min(rules.enemyCoreBuildRadius / tilesize, 200), () -> !rules.polygonCoreProtection);
title("@rules.title.environment");
category("environment");
check("@rules.explosions", b -> rules.damageExplosions = b, () -> rules.damageExplosions);
check("@rules.fire", b -> rules.fire = b, () -> rules.fire);
check("@rules.fog", b -> rules.fog = b, () -> rules.fog);
check("@rules.lighting", b -> rules.lighting = b, () -> rules.lighting);
check("@rules.enemyLights", b -> rules.enemyLights = b, () -> rules.enemyLights);
main.button(b -> {
b.left();
b.table(Tex.pane, in -> {
in.stack(new Image(Tex.alphaBg), new Image(Tex.whiteui){{
update(() -> setColor(rules.ambientLight));
}}).grow();
}).margin(4).size(50f).padRight(10);
b.add("@rules.ambientlight");
}, () -> ui.picker.show(rules.ambientLight, rules.ambientLight::set)).left().width(250f).row();
check("@rules.limitarea", b -> rules.limitMapArea = b, () -> rules.limitMapArea);
numberi("x", x -> rules.limitX = x, () -> rules.limitX, () -> rules.limitMapArea, 0, 10000);
numberi("y", y -> rules.limitY = y, () -> rules.limitY, () -> rules.limitMapArea, 0, 10000);
numberi("w", w -> rules.limitWidth = w, () -> rules.limitWidth, () -> rules.limitMapArea, 0, 10000);
numberi("h", h -> rules.limitHeight = h, () -> rules.limitHeight, () -> rules.limitMapArea, 0, 10000);
main.button("@rules.weather", this::weatherDialog).width(250f).left().row();
number("@rules.solarmultiplier", f -> rules.solarMultiplier = f, () -> rules.solarMultiplier);
if(Core.bundle.get("rules.ambientlight").toLowerCase().contains(ruleSearch)){
current.button(b -> {
b.left();
b.table(Tex.pane, in -> {
in.stack(new Image(Tex.alphaBg), new Image(Tex.whiteui){{
update(() -> setColor(rules.ambientLight));
}}).grow();
}).margin(4).size(50f).padRight(10);
b.add("@rules.ambientlight");
}, () -> ui.picker.show(rules.ambientLight, rules.ambientLight::set)).left().width(250f).row();
}
if(Core.bundle.get("rules.weather").toLowerCase().contains(ruleSearch)){
current.button("@rules.weather", this::weatherDialog).width(250f).left().row();
}
category("planet");
if(Core.bundle.get("rules.title.planet").toLowerCase().contains(ruleSearch)){
current.table(Tex.button, t -> {
t.margin(10f);
var group = new ButtonGroup<>();
var style = Styles.flatTogglet;
t.defaults().size(140f, 50f);
for(Planet planet : content.planets().select(p -> p.accessible && p.visible && p.isLandable())){
t.button(planet.localizedName, style, () -> {
planet.applyRules(rules, true);
}).group(group).checked(b -> rules.planet == planet);
if(t.getChildren().size % 3 == 0){
t.row();
}
}
t.button("@rules.anyenv", style, () -> {
rules.env = Vars.defaultEnv;
rules.planet = Planets.sun;
}).group(group).checked(b -> rules.planet == Planets.sun);
}).left().fill(false).expand(false, false).row();
}
category("teams");
//not sure where else to put this
if(showRuleEditRule){
check("@rules.allowedit", b -> rules.allowEditRules = b, () -> rules.allowEditRules);
}
team("@rules.playerteam", t -> rules.defaultTeam = t, () -> rules.defaultTeam);
team("@rules.enemyteam", t -> rules.waveTeam = t, () -> rules.waveTeam);
for(Team team : Team.baseTeams){
boolean[] shown = {false};
Table wasCurrent = current;
Table teamRules = new Table(); // just button and collapser in one table
teamRules.button(team.coloredName(), Icon.downOpen, Styles.togglet, () -> {
shown[0] = !shown[0];
}).marginLeft(14f).width(260f).height(55f).update(t -> {
((Image)t.getChildren().get(1)).setDrawable(shown[0] ? Icon.upOpen : Icon.downOpen);
t.setChecked(shown[0]);
}).left().padBottom(2f).row();
teamRules.collapser(c -> {
c.left().defaults().fillX().left().pad(5);
current = c;
TeamRule teams = rules.teams.get(team);
number("@rules.blockhealthmultiplier", f -> teams.blockHealthMultiplier = f, () -> teams.blockHealthMultiplier);
number("@rules.blockdamagemultiplier", f -> teams.blockDamageMultiplier = f, () -> teams.blockDamageMultiplier);
check("@rules.rtsai", b -> teams.rtsAi = b, () -> teams.rtsAi, () -> team != rules.defaultTeam);
numberi("@rules.rtsminsquadsize", f -> teams.rtsMinSquad = f, () -> teams.rtsMinSquad, () -> teams.rtsAi, 0, 100);
numberi("@rules.rtsmaxsquadsize", f -> teams.rtsMaxSquad = f, () -> teams.rtsMaxSquad, () -> teams.rtsAi, 1, 1000);
number("@rules.rtsminattackweight", f -> teams.rtsMinWeight = f, () -> teams.rtsMinWeight, () -> teams.rtsAi);
//disallow on Erekir (this is broken for mods I'm sure, but whatever)
check("@rules.buildai", b -> teams.buildAi = b, () -> teams.buildAi, () -> team != rules.defaultTeam && rules.env != Planets.erekir.defaultEnv && !rules.pvp);
number("@rules.buildaitier", false, f -> teams.buildAiTier = f, () -> teams.buildAiTier, () -> teams.buildAi && rules.env != Planets.erekir.defaultEnv && !rules.pvp, 0, 1);
number("@rules.extracorebuildradius", f -> teams.extraCoreBuildRadius = f * tilesize, () -> Math.min(teams.extraCoreBuildRadius / tilesize, 200), () -> !rules.polygonCoreProtection);
check("@rules.infiniteresources", b -> teams.infiniteResources = b, () -> teams.infiniteResources);
number("@rules.buildspeedmultiplier", f -> teams.buildSpeedMultiplier = f, () -> teams.buildSpeedMultiplier, 0.001f, 50f);
number("@rules.unitdamagemultiplier", f -> teams.unitDamageMultiplier = f, () -> teams.unitDamageMultiplier);
number("@rules.unitcrashdamagemultiplier", f -> teams.unitCrashDamageMultiplier = f, () -> teams.unitCrashDamageMultiplier);
number("@rules.unitminespeedmultiplier", f -> teams.unitMineSpeedMultiplier = f, () -> teams.unitMineSpeedMultiplier);
number("@rules.unitbuildspeedmultiplier", f -> teams.unitBuildSpeedMultiplier = f, () -> teams.unitBuildSpeedMultiplier, 0.001f, 50f);
number("@rules.unitcostmultiplier", f -> teams.unitCostMultiplier = f, () -> teams.unitCostMultiplier);
number("@rules.unithealthmultiplier", f -> teams.unitHealthMultiplier = f, () -> teams.unitHealthMultiplier);
if(!current.hasChildren()){
teamRules.clear();
}else{
wasCurrent.add(teamRules).row();
}
current = wasCurrent;
}, () -> shown[0]).left().growX().row();
}
additionalSetup.each(Runnable::run);
for(var i = 0; i < categories.size; i++){
addToMain(categories.get(i), Core.bundle.get("rules.title." + categoryNames.get(i)));
}
}
void number(String text, Floatc cons, Floatp prov){
public void category(String name){
current = new Table();
current.left().defaults().fillX().left().pad(5);
currentName = name;
categories.add(current);
categoryNames.add(currentName);
}
void addToMain(Table category, String title){
if(category.hasChildren()){
main.add(title).color(Pal.accent).padTop(20).padRight(100f).padBottom(-3).fillX().left().pad(5).row();
main.image().color(Pal.accent).height(3f).padRight(100f).padBottom(20).fillX().left().pad(5).row();
main.add(category).row();
}
}
public void team(String text, Cons<Team> cons, Prov<Team> prov){
if(!Core.bundle.get(text.substring(1)).toLowerCase().contains(ruleSearch)) return;
current.table(t -> {
t.left();
t.add(text).left().padRight(5);
for(Team team : Team.baseTeams){
t.button(Tex.whiteui, Styles.squareTogglei, 38f, () -> {
cons.get(team);
}).pad(1f).checked(b -> prov.get() == team).size(60f).tooltip(team.coloredName()).with(i -> i.getStyle().imageUpColor = team.color);
}
}).padTop(0).row();
}
public void number(String text, Floatc cons, Floatp prov){
number(text, false, cons, prov, () -> true, 0, Float.MAX_VALUE);
}
void number(String text, Floatc cons, Floatp prov, float min, float max){
public void number(String text, Floatc cons, Floatp prov, float min, float max){
number(text, false, cons, prov, () -> true, min, max);
}
void number(String text, boolean integer, Floatc cons, Floatp prov, Boolp condition){
public void number(String text, boolean integer, Floatc cons, Floatp prov, Boolp condition){
number(text, integer, cons, prov, condition, 0, Float.MAX_VALUE);
}
void number(String text, Floatc cons, Floatp prov, Boolp condition){
public void number(String text, Floatc cons, Floatp prov, Boolp condition){
number(text, false, cons, prov, condition, 0, Float.MAX_VALUE);
}
void number(String text, boolean integer, Floatc cons, Floatp prov, Boolp condition, float min, float max){
main.table(t -> {
public void numberi(String text, Intc cons, Intp prov, int min, int max){
numberi(text, cons, prov, () -> true, min, max);
}
public void numberi(String text, Intc cons, Intp prov, Boolp condition, int min, int max){
if(!Core.bundle.get(text.substring(1)).toLowerCase().contains(ruleSearch)) return;
var cell = current.table(t -> {
t.left();
t.add(text).left().padRight(5)
.update(a -> a.setColor(condition.get() ? Color.white : Color.gray));
t.field((prov.get()) + "", s -> cons.get(Strings.parseInt(s)))
.update(a -> a.setDisabled(!condition.get()))
.padRight(100f)
.valid(f -> Strings.parseInt(f) >= min && Strings.parseInt(f) <= max).width(120f).left();
}).padTop(0);
ruleInfo(cell, text);
current.row();
}
public void number(String text, boolean integer, Floatc cons, Floatp prov, Boolp condition, float min, float max){
if(!Core.bundle.get(text.substring(1)).toLowerCase().contains(ruleSearch)) return;
var cell = current.table(t -> {
t.left();
t.add(text).left().padRight(5)
.update(a -> a.setColor(condition.get() ? Color.white : Color.gray));
t.field((integer ? (int)prov.get() : prov.get()) + "", s -> cons.get(Strings.parseFloat(s)))
.padRight(100f)
.update(a -> a.setDisabled(!condition.get()))
.valid(f -> Strings.canParsePositiveFloat(f) && Strings.parseFloat(f) >= min && Strings.parseFloat(f) <= max).width(120f).left().addInputDialog();
.valid(f -> Strings.canParsePositiveFloat(f) && Strings.parseFloat(f) >= min && Strings.parseFloat(f) <= max).width(120f).left();
}).padTop(0);
main.row();
ruleInfo(cell, text);
current.row();
}
void check(String text, Boolc cons, Boolp prov){
public void check(String text, Boolc cons, Boolp prov){
check(text, cons, prov, () -> true);
}
void check(String text, Boolc cons, Boolp prov, Boolp condition){
main.check(text, cons).checked(prov.get()).update(a -> a.setDisabled(!condition.get())).padRight(100f).get().left();
main.row();
public void check(String text, Boolc cons, Boolp prov, Boolp condition){
if(!Core.bundle.get(text.substring(1)).toLowerCase().contains(ruleSearch)) return;
var cell = current.check(text, cons).checked(prov.get()).update(a -> a.setDisabled(!condition.get()));
cell.get().left();
ruleInfo(cell, text);
current.row();
}
void title(String text){
main.add(text).color(Pal.accent).padTop(20).padRight(100f).padBottom(-3);
main.row();
main.image().color(Pal.accent).height(3f).padRight(100f).padBottom(20);
main.row();
public void ruleInfo(Cell<?> cell, String text){
if(Core.bundle.has(text.substring(1) + ".info")){
if(mobile){
Table table = new Table();
table.add(cell.get()).left().expandX().fillX();
cell.clearElement();
table.button(Icon.infoSmall, () -> ui.showInfo(text + ".info")).size(32f).padRight(24f).right();
cell.setElement(table).left().expandX().fillX();
}else{
cell.tooltip(text + ".info");
}
}
}
Cell<TextField> field(Table table, float value, Floatc setter){
return table.field(Strings.autoFixed(value, 2), v -> setter.get(Strings.parseFloat(v)))
.valid(Strings::canParsePositiveFloat)
.size(90f, 40f).pad(2f).addInputDialog();
.size(90f, 40f).pad(2f);
}
void weatherDialog(){
@@ -247,7 +451,7 @@ public class CustomRulesDialog extends BaseDialog{
rebuild[0] = () -> {
base.clearChildren();
int cols = Math.max(1, Core.graphics.getWidth() / 460);
int cols = Math.max(1, (int)(Core.graphics.getWidth() / Scl.scl(450)));
int idx = 0;
for(WeatherEntry entry : rules.weather){
@@ -323,8 +527,9 @@ public class CustomRulesDialog extends BaseDialog{
t.background(Tex.button);
int i = 0;
for(Weather weather : content.<Weather>getBy(ContentType.weather)){
if(weather.hidden) continue;
t.button(weather.localizedName, Styles.cleart, () -> {
t.button(weather.localizedName, Styles.flatt, () -> {
rules.weather.add(new WeatherEntry(weather));
rebuild[0].run();
@@ -337,19 +542,6 @@ public class CustomRulesDialog extends BaseDialog{
add.show();
}).width(170f);
//reset cooldown to random number
dialog.hidden(() -> {
float sum = 0;
Seq<WeatherEntry> sh = rules.weather.copy();
sh.shuffle();
for(WeatherEntry w : sh){
//add the previous cooldowns to the sum so weather events are staggered and don't happen all at once.
w.cooldown = sum + Mathf.random(w.minFrequency, w.maxFrequency);
sum += w.cooldown;
}
});
dialog.show();
}
}

View File

@@ -5,64 +5,137 @@ import arc.graphics.*;
import arc.input.*;
import arc.math.*;
import arc.scene.event.*;
import arc.scene.style.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import mindustry.*;
import mindustry.content.*;
import mindustry.ctype.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.type.*;
import mindustry.ui.*;
import mindustry.world.*;
import static arc.Core.*;
import static mindustry.Vars.*;
public class DatabaseDialog extends BaseDialog{
private TextField search;
private Table all = new Table();
private @Nullable Seq<UnlockableContent> allTabs;
//sun means "all content"
private UnlockableContent tab = Planets.sun;
public DatabaseDialog(){
super("@database");
shouldPause = true;
addCloseButton();
shown(this::rebuild);
shown(() -> {
checkTabList();
if(state.isCampaign() && allTabs.contains(state.getPlanet())){
tab = state.getPlanet();
}else if(state.isGame() && state.rules.planet != null && allTabs.contains(state.rules.planet)){
tab = state.rules.planet;
}
rebuild();
});
onResize(this::rebuild);
all.margin(20).marginTop(0f).marginRight(30f);
cont.top();
cont.table(s -> {
s.image(Icon.zoom).padRight(8);
search = s.field(null, text -> rebuild()).growX().get();
search.setMessageText("@players.search");
}).fillX().padBottom(4).row();
cont.pane(all).scrollX(false);
}
void checkTabList(){
if(allTabs == null){
Seq<Content>[] allContent = Vars.content.getContentMap();
ObjectSet<UnlockableContent> all = new ObjectSet<>();
for(var contents : allContent){
for(var content : contents){
if(content instanceof UnlockableContent u){
all.addAll(u.databaseTabs);
}
}
}
allTabs = all.toSeq().sort();
allTabs.insert(0, Planets.sun);
}
}
void rebuild(){
cont.clear();
checkTabList();
Table table = new Table();
table.margin(20);
ScrollPane pane = new ScrollPane(table);
all.clear();
var text = search.getText().toLowerCase();
Seq<Content>[] allContent = Vars.content.getContentMap();
all.table(t -> {
int i = 0;
for(var content : allTabs){
t.button(content == Planets.sun ? Icon.eyeSmall : content instanceof Planet p ? Icon.icons.get(p.icon, Icon.commandRally) : new TextureRegionDrawable(content.uiIcon), Styles.clearNoneTogglei, iconMed, () -> {
tab = content;
rebuild();
}).size(50f).checked(b -> tab == content).tooltip(content == Planets.sun ? "@all" : content.localizedName).with(but -> {
but.getStyle().imageUpColor = content instanceof Planet p ? p.iconColor : Color.white.cpy();
});
if(++i % 10 == 0) t.row();
}
}).row();;
for(int j = 0; j < allContent.length; j++){
ContentType type = ContentType.all[j];
Seq<Content> array = allContent[j].select(c -> c instanceof UnlockableContent u && (!u.isHidden() || u.node() != null));
Seq<UnlockableContent> array = allContent[j]
.select(c -> c instanceof UnlockableContent u && !u.isHidden() && !u.hideDatabase && (tab == Planets.sun || u.allDatabaseTabs || u.databaseTabs.contains(tab)) &&
(text.isEmpty() || u.localizedName.toLowerCase().contains(text))).as();
if(array.size == 0) continue;
table.add("@content." + type.name() + ".name").growX().left().color(Pal.accent);
table.row();
table.image().growX().pad(5).padLeft(0).padRight(0).height(3).color(Pal.accent);
table.row();
table.table(list -> {
all.add("@content." + type.name() + ".name").growX().left().color(Pal.accent);
all.row();
all.image().growX().pad(5).padLeft(0).padRight(0).height(3).color(Pal.accent);
all.row();
all.table(list -> {
list.left();
int cols = Mathf.clamp((Core.graphics.getWidth() - 30) / (32 + 10), 1, 18);
int cols = (int)Mathf.clamp((Core.graphics.getWidth() - Scl.scl(30)) / Scl.scl(32 + 12), 1, 22);
int count = 0;
for(int i = 0; i < array.size; i++){
UnlockableContent unlock = (UnlockableContent)array.get(i);
UnlockableContent unlock = array.get(i);
Image image = unlocked(unlock) ? new Image(unlock.uiIcon).setScaling(Scaling.fit) : new Image(Icon.lock, Pal.gray);
//banned cross
if(state.isGame() && (unlock instanceof UnitType u && u.isBanned() || unlock instanceof Block b && state.rules.isBanned(b))){
list.stack(image, new Image(Icon.cancel){{
setColor(Color.scarlet);
touchable = Touchable.disabled;
}}).size(8 * 4).pad(3);
}else{
list.add(image).size(8 * 4).pad(3);
}
Image image = unlocked(unlock) ? new Image(unlock.icon(Cicon.medium)).setScaling(Scaling.fit) : new Image(Icon.lock, Pal.gray);
list.add(image).size(8 * 4).pad(3);
ClickListener listener = new ClickListener();
image.addListener(listener);
if(!Vars.mobile && unlocked(unlock)){
if(!mobile && unlocked(unlock)){
image.addListener(new HandCursorListener());
image.update(() -> image.color.lerp(!listener.isOver() ? Color.lightGray : Color.white, 0.4f * Time.delta));
image.update(() -> image.color.lerp(!listener.isOver() ? Color.lightGray : Color.white, Mathf.clamp(0.4f * Time.delta)));
}
if(unlocked(unlock)){
@@ -71,10 +144,10 @@ public class DatabaseDialog extends BaseDialog{
Core.app.setClipboardText((char)Fonts.getUnicode(unlock.name) + "");
ui.showInfoFade("@copied");
}else{
Vars.ui.content.show(unlock);
ui.content.show(unlock);
}
});
image.addListener(new Tooltip(t -> t.background(Tex.button).add(unlock.localizedName)));
image.addListener(new Tooltip(t -> t.background(Tex.button).add(unlock.localizedName + (settings.getBool("console") ? "\n[gray]" + unlock.name : ""))));
}
if((++count) % cols == 0){
@@ -82,10 +155,12 @@ public class DatabaseDialog extends BaseDialog{
}
}
}).growX().left().padBottom(10);
table.row();
all.row();
}
cont.add(pane);
if(all.getChildren().isEmpty()){
all.add("@none.found");
}
}
boolean unlocked(UnlockableContent content){

View File

@@ -29,20 +29,20 @@ public class DiscordDialog extends Dialog{
}).expandY();
t.table(i -> {
i.background(Tex.button);
i.image(Icon.discord);
}).size(h).left();
t.add("@discord").color(Pal.accent).growX().padLeft(10f);
}).size(440f, h).pad(10f);
}).size(520f, h).pad(10f);
buttons.defaults().size(150f, 50);
buttons.defaults().size(170f, 50);
buttons.button("@back", this::hide);
buttons.button("@copylink", () -> {
buttons.button("@back", Icon.left, this::hide);
buttons.button("@copylink", Icon.copy, () -> {
Core.app.setClipboardText(discordURL);
ui.showInfoFade("@copied");
});
buttons.button("@openlink", () -> {
buttons.button("@openlink", Icon.discord, () -> {
if(!Core.app.openURI(discordURL)){
ui.showErrorMessage("@linkfail");
Core.app.setClipboardText(discordURL);

View File

@@ -8,48 +8,26 @@ import arc.util.*;
import mindustry.*;
import mindustry.game.EventType.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.io.*;
import mindustry.maps.*;
import mindustry.ui.*;
import static mindustry.Vars.*;
public class MapsDialog extends BaseDialog{
private BaseDialog dialog;
public class EditorMapsDialog extends MapListDialog{
public MapsDialog(){
super("@maps");
buttons.remove();
addCloseListener();
shown(this::setup);
onResize(() -> {
if(dialog != null){
dialog.hide();
}
setup();
});
public EditorMapsDialog(){
super("@maps", true);
}
void setup(){
buttons.clearChildren();
if(Core.graphics.isPortrait()){
buttons.button("@back", Icon.left, this::hide).size(210f*2f, 64f).colspan(2);
buttons.row();
}else{
buttons.button("@back", Icon.left, this::hide).size(210f, 64f);
}
@Override
void buildButtons(){
buttons.button("@editor.newmap", Icon.add, () -> {
ui.showTextInput("@editor.newmap", "@editor.mapname", "", text -> {
Runnable show = () -> ui.loadAnd(() -> {
hide();
ui.editor.show();
ui.editor.editor.tags.put("name", text);
editor.tags.put("name", text);
Events.fire(new MapMakeEvent());
});
@@ -72,12 +50,11 @@ public class MapsDialog extends BaseDialog{
Map map = MapIO.createMap(file, true);
//when you attempt to import a save, it will have no name, so generate one
String name = map.tags.get("name", () -> {
String result = "unknown";
int number = 0;
while(maps.byName(result + number++) != null);
while(maps.byName(result + number++) != null) ;
return result + number;
});
@@ -108,51 +85,11 @@ public class MapsDialog extends BaseDialog{
});
});
}).size(210f, 64f);
cont.clear();
Table maps = new Table();
maps.marginRight(24);
ScrollPane pane = new ScrollPane(maps);
pane.setFadeScrollBars(false);
int maxwidth = Math.max((int)(Core.graphics.getWidth() / Scl.scl(230)), 1);
float mapsize = 200f;
int i = 0;
for(Map map : Vars.maps.all()){
if(i % maxwidth == 0){
maps.row();
}
TextButton button = maps.button("", Styles.cleart, () -> showMapInfo(map)).width(mapsize).pad(8).get();
button.clearChildren();
button.margin(9);
button.add(map.name()).width(mapsize - 18f).center().get().setEllipsis(true);
button.row();
button.image().growX().pad(4).color(Pal.gray);
button.row();
button.stack(new Image(map.safeTexture()).setScaling(Scaling.fit), new BorderImage(map.safeTexture()).setScaling(Scaling.fit)).size(mapsize - 20f);
button.row();
button.add(map.custom ? "@custom" : map.workshop ? "@workshop" : map.mod != null ? "[lightgray]" + map.mod.meta.displayName() : "@builtin").color(Color.gray).padTop(3);
i++;
}
if(Vars.maps.all().size == 0){
maps.add("@maps.none");
}
cont.add(buttons).growX();
cont.row();
cont.add(pane).uniformX();
}
void showMapInfo(Map map){
dialog = new BaseDialog("@editor.mapinfo");
@Override
void showMap(Map map){
BaseDialog dialog = activeDialog = new BaseDialog("@editor.mapinfo");
dialog.addCloseButton();
float mapsize = Core.graphics.isPortrait() ? 160f : 300f;
@@ -195,7 +132,7 @@ public class MapsDialog extends BaseDialog{
dialog.hide();
hide();
}catch(Exception e){
e.printStackTrace();
Log.err(e);
ui.showErrorMessage("@error.mapnotfound");
}
}).fillX().height(54f).marginLeft(10);
@@ -214,4 +151,5 @@ public class MapsDialog extends BaseDialog{
dialog.show();
}
}

View File

@@ -0,0 +1,235 @@
package mindustry.ui.dialogs;
import arc.*;
import arc.func.*;
import arc.graphics.*;
import arc.graphics.g2d.*;
import arc.math.*;
import arc.scene.*;
import arc.scene.event.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import mindustry.content.*;
import mindustry.entities.*;
import mindustry.gen.*;
import mindustry.logic.*;
import mindustry.logic.LogicFx.*;
import mindustry.ui.*;
import mindustry.world.*;
public class EffectsDialog extends BaseDialog{
static BoundsBatch bounds = new BoundsBatch();
Iterable<EffectEntry> entries;
@Nullable Cons<EffectEntry> listener;
public EffectsDialog(Iterable<EffectEntry> entries){
super("Effects");
this.entries = entries;
addCloseButton();
makeButtonOverlay();
onResize(this::setup);
shown(this::setup);
setup();
}
public EffectsDialog(){
this(LogicFx.entries());
}
public static EffectsDialog withAllEffects(){
return new EffectsDialog(Seq.select(Fx.class.getFields(), f -> f.getType() == Effect.class).map(f -> new EffectEntry(Reflect.get(f)).name(f.getName())));
}
public Dialog show(Cons<EffectEntry> listener){
this.listener = listener;
return super.show();
}
@Override
public Dialog show(){
this.listener = null;
return super.show();
}
void setup(){
float size = 280f;
int cols = (int)Math.max(1, Core.graphics.getWidth() / Scl.scl(size + 12f));
cont.clearChildren();
cont.pane(t -> {
int i = 0;
for(var entry : entries){
float bounds = calculateSize(entry);
if(bounds <= 0) continue;
ClickListener cl = new ClickListener();
t.stack(
new EffectCell(entry, cl),
new Table(af -> af.add(entry.name).grow().labelAlign(Align.bottomLeft).style(Styles.outlineLabel).bottom().left())
).size(size).with(a -> {
a.clicked(() -> {
if(listener != null){
listener.get(entry);
hide();
}
});
a.addListener(cl);
a.addListener(new HandCursorListener(() -> listener != null, true));
});
if(++i % cols == 0) t.row();
}
}).grow().scrollX(false);
}
static Object getData(Class<?> type){
if(type == Block.class) return Blocks.router;
return null;
}
static float calculateSize(EffectEntry entry){
if(entry.bounds >= 0) return entry.bounds;
var effect = entry.effect;
try{
effect.init();
Batch prev = Core.batch;
bounds.reset();
Core.batch = bounds;
Object data = getData(entry.data);
float lifetime = effect.lifetime;
float rot = 1f;
int steps = 60;
int seeds = 4;
for(int s = 0; s < seeds; s++){
for(int i = 0; i <= steps; i++){
effect.render(1, Color.white, i / (float)steps * lifetime, lifetime, rot, 0f, 0f, data);
}
}
Core.batch = prev;
return entry.bounds = bounds.max * 2f;
}catch(Exception e){
//might crash with invalid data
return -1f;
}
}
static class BoundsBatch extends Batch{
float max;
void reset(){
max = 0f;
}
void max(float... xs){
for(float f : xs){
if(Float.isNaN(f)) continue;
max = Math.max(max, Math.abs(f));
}
}
@Override
protected void draw(Texture texture, float[] spriteVertices, int offset, int count){
for(int i = offset; i < count; i += SpriteBatch.VERTEX_SIZE){
max(spriteVertices[i], spriteVertices[i + 1]);
}
}
@Override
protected void draw(TextureRegion region, float x, float y, float originX, float originY, float width, float height, float rotation){
float worldOriginX = x + originX;
float worldOriginY = y + originY;
float fx = -originX;
float fy = -originY;
float fx2 = width - originX;
float fy2 = height - originY;
float cos = Mathf.cosDeg(rotation);
float sin = Mathf.sinDeg(rotation);
float x1 = cos * fx - sin * fy + worldOriginX;
float y1 = sin * fx + cos * fy + worldOriginY;
float x2 = cos * fx - sin * fy2 + worldOriginX;
float y2 = sin * fx + cos * fy2 + worldOriginY;
float x3 = cos * fx2 - sin * fy2 + worldOriginX;
float y3 = sin * fx2 + cos * fy2 + worldOriginY;
max(x1, y1, x2, y2, x3, y3, x1 + (x3 - x2), y3 - (y2 - y1));
}
@Override
protected void flush(){}
}
class EffectCell extends Element{
EffectEntry effect;
float size = -1f;
int id = 1;
float time = 0f;
float lifetime;
float rotation = 1f;
Object data;
ClickListener cl;
public EffectCell(EffectEntry effect, ClickListener cl){
this.effect = effect;
this.lifetime = effect.effect.lifetime;
this.cl = cl;
data = getData(effect.data);
}
@Override
public void draw(){
if(size < 0){
size = calculateSize(effect) + 1f;
}
color.fromHsv((Time.globalTime * 2f) % 360f, 1f, 1f);
if(clipBegin(x, y, width, height)){
Draw.colorl(cl.isOver() && listener != null ? 0.4f : 0.5f);
Draw.alpha(parentAlpha);
Tex.alphaBg.draw(x, y, width, height);
Draw.reset();
Draw.flush();
float scale = width / size;
Tmp.m1.set(Draw.trans());
Draw.trans().translate(x + width/2f, y + height/2f).scale(scale, scale);
Draw.flush();
this.lifetime = effect.effect.render(id, color, time, lifetime, rotation, 0f, 0f, data);
Draw.flush();
Draw.trans().set(Tmp.m1);
clipEnd();
}
Lines.stroke(Scl.scl(3f), Color.black);
Lines.rect(x, y, width, height);
Draw.reset();
}
@Override
public void act(float delta){
super.act(delta);
time += Time.delta;
if(time >= lifetime){
id ++;
}
time %= lifetime;
}
}
}

View File

@@ -20,8 +20,8 @@ public class FileChooser extends BaseDialog{
private static final Fi homeDirectory = Core.files.absolute(Core.files.getExternalStoragePath());
static Fi lastDirectory = Core.files.absolute(Core.settings.getString("lastDirectory", homeDirectory.absolutePath()));
Fi directory;
private Table files;
Fi directory = lastDirectory;
private ScrollPane pane;
private TextField navigation, filefield;
private TextButton ok;
@@ -37,6 +37,8 @@ public class FileChooser extends BaseDialog{
this.filter = filter;
this.selectListener = result;
directory = getLastDirectory();
onResize(() -> {
cont.clear();
setupWidgets();
@@ -191,7 +193,7 @@ public class FileChooser extends BaseDialog{
Fi[] names = getFileNames();
Image upimage = new Image(Icon.upOpen);
TextButton upbutton = new TextButton(".." + directory.toString(), Styles.clearTogglet);
TextButton upbutton = new TextButton(".." + directory.toString(), Styles.flatTogglet);
upbutton.clicked(() -> {
directory = directory.parent();
setLastDirectory(directory);
@@ -213,7 +215,7 @@ public class FileChooser extends BaseDialog{
String filename = file.name();
TextButton button = new TextButton(filename, Styles.clearTogglet);
TextButton button = new TextButton(filename.replace("[", "[["), Styles.flatTogglet);
button.getLabel().setWrap(false);
button.getLabel().setEllipsis(true);
group.add(button);
@@ -249,18 +251,16 @@ public class FileChooser extends BaseDialog{
if(open) filefield.clearText();
}
public static void setLastDirectory(Fi directory){
public static synchronized void setLastDirectory(Fi directory){
lastDirectory = directory;
Core.settings.put("lastDirectory", directory.absolutePath());
}
private String shorten(String string){
int max = 30;
if(string.length() <= max){
return string;
}else{
return string.substring(0, max - 3).concat("...");
public static synchronized Fi getLastDirectory(){
if(!lastDirectory.exists()){
lastDirectory = homeDirectory;
}
return lastDirectory;
}
public class FileHistory{
@@ -300,19 +300,5 @@ public class FileChooser extends BaseDialog{
public boolean canBack(){
return !(index == 1) && index > 0;
}
void print(){
System.out.println("\n\n\n\n\n\n");
int i = 0;
for(Fi file : history){
i++;
if(index == i){
System.out.println("[[" + file.toString() + "]]");
}else{
System.out.println("--" + file.toString() + "--");
}
}
}
}
}

View File

@@ -0,0 +1,21 @@
package mindustry.ui.dialogs;
import arc.util.*;
public class FullTextDialog extends BaseDialog{
public FullTextDialog(){
super("");
shouldPause = true;
addCloseButton();
}
public void show(String titleText, String text){
title.setText(titleText);
cont.clear();
cont.add(text).grow().wrap().labelAlign(Align.center);
super.show();
}
}

View File

@@ -1,21 +1,38 @@
package mindustry.ui.dialogs;
import arc.*;
import arc.flabel.*;
import arc.math.*;
import arc.scene.actions.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.util.*;
import mindustry.core.GameState.*;
import mindustry.game.EventType.*;
import mindustry.game.*;
import mindustry.type.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.ui.*;
import static mindustry.Vars.*;
public class GameOverDialog extends BaseDialog{
private Team winner;
private boolean hudShown;
public GameOverDialog(){
super("@gameover");
setFillParent(true);
shown(this::rebuild);
titleTable.remove();
shown(() -> {
hudShown = ui.hudfrag.shown;
ui.hudfrag.shown = false;
rebuild();
});
hidden(() -> ui.hudfrag.shown = hudShown);
Events.on(ResetEvent.class, e -> hide());
}
@@ -31,73 +48,119 @@ public class GameOverDialog extends BaseDialog{
}
void rebuild(){
title.setText(state.isCampaign() ? "@sector.curlost" : "@gameover");
buttons.clear();
cont.clear();
buttons.margin(10);
if(state.rules.pvp && winner != null){
cont.add(Core.bundle.format("gameover.pvp", winner.localized())).pad(6);
cont.table(t -> {
if(state.rules.pvp && winner != null){
t.add(Core.bundle.format("gameover.pvp", winner.coloredName())).center().pad(6);
}else{
t.add(state.isCampaign() ? Core.bundle.format("sector.lost", state.getSector().name()) : "@gameover").center().pad(6);
}
t.row();
if(control.isHighScore()){
t.add("@highscore").pad(6);
t.row();
}
t.pane(p -> {
p.margin(13f);
p.left().defaults().left();
p.setBackground(Styles.black3);
p.table(stats -> {
if(state.rules.waves) addStat(stats, Core.bundle.get("stats.wave"), state.stats.wavesLasted, 0f);
addStat(stats, Core.bundle.get("stats.unitsCreated"), state.stats.unitsCreated, 0.05f);
addStat(stats, Core.bundle.get("stats.enemiesDestroyed"), state.stats.enemyUnitsDestroyed, 0.1f);
addStat(stats, Core.bundle.get("stats.built"), state.stats.buildingsBuilt, 0.15f);
addStat(stats, Core.bundle.get("stats.destroyed"), state.stats.buildingsDestroyed, 0.2f);
addStat(stats, Core.bundle.get("stats.deconstructed"), state.stats.buildingsDeconstructed, 0.25f);
}).top().grow().row();
if(control.saves.getCurrent() != null){
p.table(pt -> {
pt.add(new FLabel(Core.bundle.get("stats.playtime"))).left().pad(5).growX();
pt.add(new FLabel("[accent]" + control.saves.getCurrent().getPlayTime())).right().pad(5);
}).growX();
}
}).grow().pad(12).top();
}).center().minWidth(370).maxSize(600, 550).grow();
if(state.isCampaign() && net.client()){
cont.row();
cont.add("@gameover.waiting").padTop(20f).row();
}
if(state.isCampaign()){
if(net.client()){
buttons.button("@gameover.disconnect", () -> {
logic.reset();
net.reset();
hide();
state.set(State.menu);
}).size(170f, 60f);
}else{
buttons.button("@continue", () -> {
hide();
ui.planet.show();
}).size(170f, 60f);
}
}else{
buttons.button("@menu", () -> {
hide();
logic.reset();
}).size(130f, 60f);
}else{
if(control.isHighScore()){
cont.add("@highscore").pad(6);
cont.row();
}
cont.pane(t -> {
t.margin(13f);
t.left().defaults().left();
t.add(Core.bundle.format("stat.wave", state.stats.wavesLasted)).row();
t.add(Core.bundle.format("stat.enemiesDestroyed", state.stats.enemyUnitsDestroyed)).row();
t.add(Core.bundle.format("stat.built", state.stats.buildingsBuilt)).row();
t.add(Core.bundle.format("stat.destroyed", state.stats.buildingsDestroyed)).row();
t.add(Core.bundle.format("stat.deconstructed", state.stats.buildingsDeconstructed)).row();
if(control.saves.getCurrent() != null){
t.add(Core.bundle.format("stat.playtime", control.saves.getCurrent().getPlayTime())).row();
}
if(state.isCampaign() && !state.stats.itemsDelivered.isEmpty()){
t.add("@stat.delivered").row();
for(Item item : content.items()){
if(state.stats.itemsDelivered.get(item, 0) > 0){
t.table(items -> {
items.add(" [lightgray]" + state.stats.itemsDelivered.get(item, 0));
items.image(item.icon(Cicon.small)).size(8 * 3).pad(4);
}).left().row();
}
}
}
if(state.isCampaign() && net.client()){
t.add("@gameover.waiting").padTop(20f).row();
}
}).pad(12);
if(state.isCampaign()){
if(net.client()){
buttons.button("@gameover.disconnect", () -> {
logic.reset();
net.reset();
hide();
state.set(State.menu);
}).size(170f, 60f);
}else{
buttons.button("@continue", () -> {
hide();
ui.planet.show();
}).size(170f, 60f);
}
}else{
buttons.button("@menu", () -> {
hide();
if(!ui.paused.checkPlaytest()){
logic.reset();
}).size(140f, 60f);
}
}
}).size(140f, 60f);
}
}
private void addStat(Table parent, String stat, int value, float delay){
parent.add(new StatLabel(stat, value, delay)).top().pad(5).growX().height(50).row();
}
private static class StatLabel extends Table {
private float progress = 0;
public StatLabel(String stat, int value, float delay){
setTransform(true);
setClip(true);
setBackground(Tex.whiteui);
setColor(Pal.accent);
margin(2f);
FLabel statLabel = new FLabel(stat);
statLabel.setStyle(Styles.outlineLabel);
statLabel.setWrap(true);
statLabel.pause();
Label valueLabel = new Label("", Styles.outlineLabel);
valueLabel.setAlignment(Align.right);
add(statLabel).left().growX().padLeft(5);
add(valueLabel).right().growX().padRight(5);
actions(
Actions.scaleTo(0, 1),
Actions.delay(delay),
Actions.parallel(
Actions.scaleTo(1, 1, 0.3f, Interp.pow3Out),
Actions.color(Pal.darkestGray, 0.3f, Interp.pow3Out),
Actions.sequence(
Actions.delay(0.3f),
Actions.run(() -> {
valueLabel.update(() -> {
progress = Math.min(1, progress + (Time.delta / 60));
valueLabel.setText("" + (int)Mathf.lerp(0, value, value < 10 ? progress : Interp.slowFast.apply(progress)));
});
statLabel.resume();
})
)
)
);
}
}
}

View File

@@ -4,10 +4,11 @@ import arc.*;
import arc.scene.ui.*;
import arc.util.*;
import mindustry.core.*;
import mindustry.game.EventType.*;
import mindustry.gen.*;
import mindustry.ui.*;
import java.io.*;
import java.util.*;
import static mindustry.Vars.*;
@@ -28,15 +29,24 @@ public class HostDialog extends BaseDialog{
ui.listfrag.rebuild();
}).grow().pad(8).get().setMaxLength(40);
ImageButton button = t.button(Tex.whiteui, Styles.clearFulli, 40, () -> {
ImageButton button = t.button(Tex.whiteui, Styles.squarei, 40, () -> {
new PaletteDialog().show(color -> {
player.color().set(color);
player.color.set(color);
Core.settings.put("color-0", color.rgba());
});
}).size(54f).get();
button.update(() -> button.getStyle().imageUpColor = player.color());
}).width(w).height(70f).pad(4).colspan(3);
if(steam){
cont.row();
cont.add().width(65f);
cont.check("@steam.friendsonly", !Core.settings.getBool("steampublichost"), val -> Core.settings.put("steampublichost", !val)).colspan(2).left()
.with(c -> ui.addDescTooltip(c, "@steam.friendsonly.tooltip")).padBottom(15f).row();
}
cont.row();
cont.table(t -> {
@@ -68,7 +78,11 @@ public class HostDialog extends BaseDialog{
runHost();
}).width(w).height(70f).disabled(b -> !portField.isValid());
cont.button("?", () -> ui.showInfo("@host.info")).size(65f, 70f).padLeft(6f);
if(!steam){
cont.button("?", () -> ui.showInfo("@host.info")).size(65f, 70f).padLeft(6f);
}else{
cont.add().size(65f, 70f).padLeft(6f);
}
shown(() -> {
if(!steam){
@@ -82,34 +96,20 @@ public class HostDialog extends BaseDialog{
Time.runTask(5f, () -> {
try{
net.host(Core.settings.getInt("port", port));
player.admin(true);
if(steam){
Core.app.post(() -> Core.settings.getBoolOnce("steampublic3", () -> {
ui.showCustomConfirm("@setting.publichost.name", "@public.confirm", "@yes", "@no", () -> {
ui.showCustomConfirm("@setting.publichost.name", "@public.confirm.really", "@no", "@yes", () -> {
Core.settings.put("publichost", true);
platform.updateLobby();
}, () -> {
Core.settings.put("publichost", false);
platform.updateLobby();
});
}, () -> {
Core.settings.put("publichost", false);
platform.updateLobby();
});
}));
player.admin = true;
Events.fire(new HostEvent());
if(steam && Core.settings.getBool("steampublichost")){
if(Version.modifier.contains("beta") || Version.modifier.contains("alpha")){
Core.settings.put("publichost", false);
Core.settings.put("steampublichost", false);
platform.updateLobby();
Core.settings.getBoolOnce("betapublic", () -> ui.showInfo("@public.beta"));
}
}
}catch(IOException e){
ui.showException("@server.error", e);
}catch(Exception e){
ui.showException(e.getMessage() != null && e.getMessage().toLowerCase(Locale.ROOT).contains("address already in use") ? "@server.error.addressinuse" : "@server.error", e);
}
ui.loadfrag.hide();
hide();

View File

@@ -0,0 +1,79 @@
package mindustry.ui.dialogs;
import arc.*;
import arc.func.*;
import arc.scene.style.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.util.*;
import mindustry.ctype.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.ui.*;
import static mindustry.Vars.*;
public class IconSelectDialog extends Dialog{
private Intc consumer = i -> Log.info("you have mere seconds");
private boolean allowLocked;
public IconSelectDialog(){
this(true);
}
public IconSelectDialog(boolean allowLocked){
closeOnBack();
setFillParent(true);
cont.pane(t -> {
resized(true, () -> {
t.clearChildren();
t.marginRight(19f);
t.defaults().size(48f);
t.button(Icon.none, Styles.flati, () -> {
hide();
consumer.get(0);
});
int cols = (int)Math.min(20, Core.graphics.getWidth() / Scl.scl(52f));
int i = 1;
for(var key : accessibleIcons){
var value = Icon.icons.get(key);
t.button(value, Styles.flati, () -> {
hide();
consumer.get(Iconc.codes.get(key));
});
if(++i % cols == 0) t.row();
}
for(ContentType ctype : defaultContentIcons){
t.row();
t.image().colspan(cols).growX().width(Float.NEGATIVE_INFINITY).height(3f).color(Pal.accent);
t.row();
i = 0;
for(UnlockableContent u : content.getBy(ctype).<UnlockableContent>as()){
if(!u.isHidden() && (allowLocked || u.unlocked())){
t.button(new TextureRegionDrawable(u.uiIcon), Styles.flati, iconMed, () -> {
hide();
consumer.get(u.emojiChar());
});
if(++i % cols == 0) t.row();
}
}
}
});
});
buttons.button("@back", Icon.left, this::hide).size(210f, 64f);
}
public void show(Intc listener){
consumer = listener;
super.show();
}
}

View File

@@ -1,11 +1,12 @@
package mindustry.ui.dialogs;
import arc.*;
import arc.Net.*;
import arc.freetype.FreeTypeFontGenerator.*;
import arc.graphics.*;
import arc.input.*;
import arc.math.*;
import arc.scene.ui.*;
import arc.scene.ui.TextButton.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
@@ -16,7 +17,7 @@ import mindustry.core.*;
import mindustry.game.EventType.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.io.legacy.*;
import mindustry.io.versions.*;
import mindustry.net.*;
import mindustry.net.Packets.*;
import mindustry.ui.*;
@@ -34,32 +35,53 @@ public class JoinDialog extends BaseDialog{
int totalHosts;
int refreshes;
boolean showHidden;
TextButtonStyle style;
Task fontIgnoreDirtyTask;
String lastIp;
int lastPort;
int lastPort, lastColumns = -1;
Task ping;
String serverSearch = "";
public JoinDialog(){
super("@joingame");
makeButtonOverlay();
style = new TextButtonStyle(){{
over = Styles.flatOver;
font = Fonts.def;
fontColor = Color.white;
disabledFontColor = Color.gray;
down = Styles.flatOver;
up = Styles.black5;
}};
loadServers();
if(!steam) buttons.add().width(60f);
//mobile players don't get information >:(
boolean infoButton = !steam && !mobile;
if(infoButton) buttons.add().width(60f);
buttons.add().growX().width(-1);
addCloseButton();
addCloseButton(mobile ? 190f : 210f);
buttons.button("@server.add", Icon.add, () -> {
renaming = null;
add.show();
});
buttons.add().growX().width(-1);
if(!steam){
buttons.button("?", () -> ui.showInfo("@join.info")).size(60f, 64f).width(-1);
}
if(infoButton) buttons.button("?", () -> ui.showInfo("@join.info")).size(60f, 64f);
add = new BaseDialog("@joingame.title");
add.cont.add("@joingame.ip").padRight(5f).left();
TextField field = add.cont.field(Core.settings.getString("ip"), text -> {
Core.settings.put("ip", text);
}).size(320f, 54f).maxTextLength(100).addInputDialog().get();
}).size(320f, 54f).maxTextLength(100).get();
add.cont.row();
add.buttons.defaults().size(140f, 60f).pad(4f);
@@ -97,8 +119,17 @@ public class JoinDialog extends BaseDialog{
});
onResize(() -> {
setup();
refreshAll();
//only refresh on resize when the minimum dimension is smaller than the maximum preferred width
//this means that refreshes on resize will only happen for small phones that need the list to fit in portrait mode
//also resize if number of cols changes
if(Math.min(Core.graphics.getWidth(), Core.graphics.getHeight()) / Scl.scl() * 0.9f < 500f || lastColumns != columns()){
setup();
refreshAll();
}
lastColumns = columns();
});
}
@@ -107,7 +138,9 @@ public class JoinDialog extends BaseDialog{
refreshLocal();
refreshRemote();
refreshGlobal();
if(Core.settings.getBool("communityservers", true)){
refreshCommunity();
}
}
void setupRemote(){
@@ -115,26 +148,30 @@ public class JoinDialog extends BaseDialog{
for(Server server : servers){
//why are java lambdas this bad
TextButton[] buttons = {null};
Button[] buttons = {null};
TextButton button = buttons[0] = remote.button("[accent]" + server.displayIP(), Styles.cleart, () -> {
Button button = buttons[0] = remote.button(b -> {}, style, () -> {
if(!buttons[0].childrenPressed()){
if(server.lastHost != null){
Events.fire(new ClientPreConnectEvent(server.lastHost));
safeConnect(server.ip, server.port, server.lastHost.version);
safeConnect(server.lastHost.address, server.lastHost.port, server.lastHost.version);
}else{
connect(server.ip, server.port);
}
}
}).width(targetWidth()).pad(4f).get();
}).width(targetWidth()).growY().top().left().pad(4f).get();
button.getLabel().setWrap(true);
if(remote.getChildren().size % columns() == 0){
remote.row();
}
Table inner = new Table(Tex.whiteui);
inner.setColor(Pal.gray);
Table inner = new Table();
button.clearChildren();
button.add(inner).growX();
inner.add(button.getLabel()).growX();
inner.add("[accent]" + server.displayIP()).left().padLeft(10f).wrap().style(Styles.outlineLabel).growX();
inner.button(Icon.upOpen, Styles.emptyi, () -> {
moveRemote(server, -1);
@@ -167,8 +204,6 @@ public class JoinDialog extends BaseDialog{
button.row();
server.content = button.table(t -> {}).grow().get();
remote.row();
}
}
@@ -200,55 +235,76 @@ public class JoinDialog extends BaseDialog{
void refreshServer(Server server){
server.content.clear();
server.content.label(() -> Core.bundle.get("server.refreshing") + Strings.animated(Time.time, 4, 11, "."));
server.content.background(Tex.whitePane).setColor(Pal.gray);
server.content.label(() -> Core.bundle.get("server.refreshing") + Strings.animated(Time.time, 4, 11, ".")).grow().center().labelAlign(Align.center).padBottom(4);
net.pingHost(server.ip, server.port, host -> setupServer(server, host), e -> {
server.content.clear();
server.content.add("@host.invalid").padBottom(4);
server.content.background(Tex.whitePane).setColor(Pal.gray);
server.content.add("@host.invalid").grow().center().labelAlign(Align.center);
});
}
void setupServer(Server server, Host host){
server.lastHost = host;
server.content.clear();
buildServer(host, server.content);
buildServer(host, server.content, false, true);
}
void buildServer(Host host, Table content){
String versionString;
void buildServer(Host host, Table content, boolean local, boolean addName){
content.top().left();
boolean isBanned = local && Vars.steam && host.description != null && host.description.equals("[banned]");
String versionString = getVersionString(host) + (isBanned ? "[red] [banned]" : "");
if(host.version == -1){
versionString = Core.bundle.format("server.version", Core.bundle.get("server.custombuild"), "");
}else if(host.version == 0){
versionString = Core.bundle.get("server.outdated");
}else if(host.version < Version.build && Version.build != -1){
versionString = Core.bundle.get("server.outdated") + "\n" +
Core.bundle.format("server.version", host.version, "");
}else if(host.version > Version.build && Version.build != -1){
versionString = Core.bundle.get("server.outdated.client") + "\n" +
Core.bundle.format("server.version", host.version, "");
}else if(host.version == Version.build && Version.type.equals(host.versionType)){
//not important
versionString = "";
}else{
versionString = Core.bundle.format("server.version", host.version, host.versionType);
float twidth = targetWidth() - 40f;
content.background(null);
Color color = Pal.gray;
if(addName){
content.table(Tex.whiteui, t -> {
t.left();
t.setColor(color);
t.add(host.name + " " + versionString).style(Styles.outlineLabel).padLeft(10f).width(twidth).left().ellipsis(true);
}).growX().height(36f).row();
}
content.table(t -> {
t.add("[lightgray]" + host.name + " " + versionString).width(targetWidth() - 10f).left().get().setEllipsis(true);
t.row();
if(!host.description.isEmpty()){
t.add("[gray]" + host.description).width(targetWidth() - 10f).left().wrap();
content.table(Tex.whitePane, t -> {
t.top().left();
t.setColor(color);
t.left();
if(!host.description.isEmpty() && !isBanned){
//limit newlines.
int count = 0;
StringBuilder result = new StringBuilder(host.description.length());
for(int i = 0; i < host.description.length(); i++){
char c = host.description.charAt(i);
if(c == '\n'){
count ++;
if(count < 3) result.append(c);
}else{
result.append(c);
}
}
t.add("[gray]" + result).width(twidth).left().wrap();
t.row();
}
t.add("[lightgray]" + (Core.bundle.format("players" + (host.players == 1 && host.playerLimit <= 0 ? ".single" : ""), (host.players == 0 ? "[lightgray]" : "[accent]") + host.players + (host.playerLimit > 0 ? "[lightgray]/[accent]" + host.playerLimit : "")+ "[lightgray]"))).left();
t.row();
t.add("[lightgray]" + Core.bundle.format("save.map", host.mapname) + "[lightgray] / " + (host.modeName == null ? host.mode.toString() : host.modeName)).width(targetWidth() - 10f).left().get().setEllipsis(true);
t.add("[lightgray]" + (Core.bundle.format("players" + (host.players == 1 && host.playerLimit <= 0 ? ".single" : ""),
(host.players == 0 ? "[lightgray]" : "[accent]") + host.players + (host.playerLimit > 0 ? "[lightgray]/[accent]" + host.playerLimit : "")+ "[lightgray]"))).left().row();
t.add("[lightgray]" + Core.bundle.format("save.map", host.mapname) + "[lightgray] / " + (host.modeName == null ? host.mode.toString() : host.modeName)).width(twidth).left().ellipsis(true).row();
if(host.ping > 0){
t.row();
t.add(Iconc.chartBar + " " + host.ping + "ms").color(Color.gray).left();
t.add(Iconc.chartBar + " " + host.ping + "ms").style(Styles.outlineLabel).color(Pal.gray).left();
}
}).expand().left().bottom().padLeft(12f).padBottom(8);
}).growY().growX().left().bottom();
}
void setup(){
@@ -258,10 +314,14 @@ public class JoinDialog extends BaseDialog{
float w = targetWidth();
hosts.clear();
//since the buttons are an overlay, make room for that
hosts.marginBottom(70f);
section("@servers.local", local, false);
section(steam ? "@servers.local.steam" : "@servers.local", local, false);
section("@servers.remote", remote, false);
section("@servers.global", global, true);
if(Core.settings.getBool("communityservers", true)){
section("@servers.global", global, true);
}
ScrollPane pane = new ScrollPane(hosts);
pane.setFadeScrollBars(false);
@@ -275,9 +335,9 @@ public class JoinDialog extends BaseDialog{
t.field(Core.settings.getString("name"), text -> {
player.name(text);
Core.settings.put("name", text);
}).grow().pad(8).addInputDialog(maxNameLength);
}).grow().pad(8).maxTextLength(maxNameLength);
ImageButton button = t.button(Tex.whiteui, Styles.clearFulli, 40, () -> {
ImageButton button = t.button(Tex.whiteui, Styles.squarei, 40, () -> {
new PaletteDialog().show(color -> {
player.color().set(color);
Core.settings.put("color-0", color.rgba8888());
@@ -286,27 +346,8 @@ public class JoinDialog extends BaseDialog{
button.update(() -> button.getStyle().imageUpColor = player.color());
}).width(w).height(70f).pad(4);
cont.row();
cont.add(pane).width(w + 38).pad(0);
cont.add(pane).width((w + 5) * columns() + 33).pad(0);
cont.row();
cont.buttonCenter("@server.add", Icon.add, () -> {
renaming = null;
add.show();
}).marginLeft(10).width(w).height(80f).update(button -> {
float pw = w;
float pad = 0f;
if(pane.getChildren().first().getPrefHeight() > pane.getHeight()){
pw = w + 30;
pad = 6;
}
var cell = ((Table)pane.parent).getCell(button);
if(!Mathf.equal(cell.minWidth(), pw)){
cell.width(pw);
cell.padLeft(pad);
pane.parent.invalidateHierarchy();
}
});
}
void section(String label, Table servers, boolean eye){
@@ -319,7 +360,7 @@ public class JoinDialog extends BaseDialog{
if(eye){
name.button(Icon.eyeSmall, Styles.emptyi, () -> {
showHidden = !showHidden;
refreshGlobal();
refreshCommunity();
}).update(i -> i.getStyle().imageUp = (showHidden ? Icon.eyeSmall : Icon.eyeOffSmall))
.size(40f).right().padRight(3).tooltip("@servers.showhidden");
}
@@ -332,7 +373,7 @@ public class JoinDialog extends BaseDialog{
hosts.row();
hosts.image().growX().pad(5).padLeft(10).padRight(10).height(3).color(Pal.accent);
hosts.row();
hosts.add(coll).width(targetWidth());
hosts.add(coll).width((targetWidth() + 5f) * columns());
hosts.row();
}
@@ -345,51 +386,61 @@ public class JoinDialog extends BaseDialog{
net.discoverServers(this::addLocalHost, this::finishLocalHosts);
}
void refreshGlobal(){
void refreshCommunity(){
int cur = refreshes;
global.clear();
global.background(null);
for(ServerGroup group : defaultServers){
global.table(t -> {
t.add("@search").padRight(10);
t.field(serverSearch, text ->
serverSearch = text.trim().replaceAll(" +", " ").toLowerCase()
).grow().pad(8).get().keyDown(KeyCode.enter, this::refreshCommunity);
t.button(Icon.zoom, Styles.emptyi, this::refreshCommunity).size(54f);
}).width((targetWidth() + 5f) * columns()).height(70f).pad(4).row();
for(int i = 0; i < defaultServers.size; i ++){
ServerGroup group = defaultServers.get((i + defaultServers.size/2) % defaultServers.size);
boolean hidden = group.hidden();
if(hidden && !showHidden){
continue;
}
Table[] groupTable = {null};
Table[] groupTable = {null, null};
if(group.prioritized){
addHeader(groupTable, group, hidden, false);
}
//table containing all groups
for(String address : group.addresses){
String resaddress = address.contains(":") ? address.split(":")[0] : address;
int resport = address.contains(":") ? Strings.parseInt(address.split(":")[1]) : port;
net.pingHost(resaddress, resport, res -> {
if(refreshes != cur) return;
res.port = resport;
//add header
if(groupTable[0] == null){
global.table(t -> groupTable[0] = t).row();
groupTable[0].table(head -> {
if(!group.name.isEmpty()){
head.add(group.name).color(Color.lightGray).padRight(4);
}
head.image().height(3f).growX().color(Color.lightGray);
//button for showing/hiding servers
ImageButton[] image = {null};
image[0] = head.button(hidden ? Icon.eyeOffSmall : Icon.eyeSmall, Styles.accenti, () -> {
group.setHidden(!group.hidden());
image[0].getStyle().imageUp = group.hidden() ? Icon.eyeOffSmall : Icon.eyeSmall;
if(group.hidden() && !showHidden){
groupTable[0].remove();
}
}).size(40f).get();
image[0].addListener(new Tooltip(t -> t.background(Styles.black6).margin(4).label(() -> !group.hidden() ? "@server.shown" : "@server.hidden")));
}).width(targetWidth()).padBottom(-2).row();
//don't recache the texture for a while
if(fontIgnoreDirtyTask == null){
FreeTypeFontData.ignoreDirty = true;
fontIgnoreDirtyTask = Time.runTask(0.6f * 60f, () -> {
FreeTypeFontData.ignoreDirty = false;
fontIgnoreDirtyTask = null;
});
}
addGlobalHost(res, groupTable[0]);
if(!serverSearch.isEmpty() && !(group.name.toLowerCase().contains(serverSearch)
|| res.name.toLowerCase().contains(serverSearch)
|| res.description.toLowerCase().contains(serverSearch)
|| res.mapname.toLowerCase().contains(serverSearch)
|| (res.modeName != null && res.modeName.toLowerCase().contains(serverSearch)))) return;
if(groupTable[0] == null){
addHeader(groupTable, group, hidden, true);
}else if(!groupTable[0].visible){
addHeader(groupTable, group, hidden, true);
}
addCommunityHost(res, groupTable[1]);
groupTable[0].margin(5f);
groupTable[0].pack();
@@ -398,11 +449,53 @@ public class JoinDialog extends BaseDialog{
}
}
void addGlobalHost(Host host, Table container){
void addHeader(Table[] groupTable, ServerGroup group, boolean hidden, boolean doInit){ // outlined separately
if(groupTable[0] == null){
global.table(t -> groupTable[0] = t).fillX().left().row();
}
groupTable[0].visible(() -> doInit);
if(!doInit){
return;
}
groupTable[0].table(head -> {
Color col = group.prioritized ? Pal.accent : Color.lightGray;
if(!group.name.isEmpty()){
head.add(group.name).color(col).padRight(4);
}
head.image().height(3f).growX().color(col);
//button for showing/hiding servers
ImageButton[] image = {null};
image[0] = head.button(hidden ? Icon.eyeOffSmall : Icon.eyeSmall, Styles.grayi, () -> {
group.setHidden(!group.hidden());
image[0].getStyle().imageUp = group.hidden() ? Icon.eyeOffSmall : Icon.eyeSmall;
if(group.hidden() && !showHidden){
groupTable[0].remove();
}
}).size(40f).get();
image[0].addListener(new Tooltip(t -> t.background(Styles.black6).margin(4).label(() -> !group.hidden() ? "@server.shown" : "@server.hidden")));
}).width(targetWidth() * columns()).padBottom(-2).row();
groupTable[1] = groupTable[0].row().table().top().left().grow().get();
}
int columns(){
return Mathf.clamp((int)((Core.graphics.getWidth() / Scl.scl() * 0.9f) / targetWidth()), 1, 4);
}
void addCommunityHost(Host host, Table container){
global.background(null);
String versionString = getVersionString(host);
float w = targetWidth();
container.button(b -> buildServer(host, b), Styles.cleart, () -> {
container.left().top();
Button[] button = {null};
button[0] = container.button(b -> {}, style, () -> {
if(button[0].childrenPressed()) return;
Events.fire(new ClientPreConnectEvent(host));
if(!Core.settings.getBool("server-disclaimer", false)){
ui.showCustomConfirm("@warning", "@servers.disclaimer", "@ok", "@back", () -> {
@@ -414,7 +507,32 @@ public class JoinDialog extends BaseDialog{
}else{
safeConnect(host.address, host.port, host.version);
}
}).width(w).row();
}).width(w).padBottom(7).padRight(4f).top().left().growY().uniformY().get();
Table inner = new Table(Tex.whiteui);
inner.setColor(Pal.gray);
button[0].clearChildren();
button[0].add(inner).growX();
inner.add(host.name + " " + versionString).left().padLeft(10f).wrap().style(Styles.outlineLabel).growX();
inner.button(Icon.add, Styles.emptyi, () -> {
Server server = new Server();
server.setIP(host.address + ":" + host.port);
servers.add(server);
saveServers();
setupRemote();
refreshRemote();
}).margin(3f).pad(8f).padRight(4f).top().right();
button[0].row();
buildServer(host, button[0].table(t -> {}).grow().get(), false, false);
if((container.getChildren().size) % columns() == 0){
container.row();
}
}
void finishLocalHosts(){
@@ -437,12 +555,14 @@ public class JoinDialog extends BaseDialog{
totalHosts++;
float w = targetWidth();
local.row();
if((local.getChildren().size) % columns() == 0){
local.row();
}
local.button(b -> buildServer(host, b), Styles.cleart, () -> {
local.button(b -> buildServer(host, b, true, true), style, () -> {
Events.fire(new ClientPreConnectEvent(host));
safeConnect(host.address, host.port, host.version);
}).width(w);
}).width(w).top().left().growY();
}
public void connect(String ip, int port){
@@ -463,8 +583,10 @@ public class JoinDialog extends BaseDialog{
net.reset();
Vars.netClient.beginConnecting();
net.connect(lastIp = ip, lastPort = port, () -> {
hide();
add.hide();
if(net.client()){
hide();
add.hide();
}
});
});
}
@@ -481,7 +603,7 @@ public class JoinDialog extends BaseDialog{
connect(lastIp, lastPort);
}, exception -> {});
}, 1, 1);
ui.loadfrag.setButton(() -> {
ui.loadfrag.hide();
if(ping == null) return;
@@ -492,7 +614,7 @@ public class JoinDialog extends BaseDialog{
void safeConnect(String ip, int port, int version){
if(version != Version.build && Version.build != -1 && version != -1){
ui.showInfo("[scarlet]" + (version > Version.build ? KickReason.clientOutdated : KickReason.serverOutdated).toString() + "\n[]" +
ui.showInfo("[scarlet]" + (version > Version.build ? KickReason.clientOutdated : KickReason.serverOutdated) + "\n[]" +
Core.bundle.format("server.versions", Version.build, version));
}else{
connect(ip, port);
@@ -500,7 +622,7 @@ public class JoinDialog extends BaseDialog{
}
float targetWidth(){
return Math.min(Core.graphics.getWidth() / Scl.scl() * 0.9f, 500f);
return Math.min(Core.graphics.getWidth() / Scl.scl() * 0.9f, 550f);
}
@SuppressWarnings("unchecked")
@@ -513,48 +635,76 @@ public class JoinDialog extends BaseDialog{
Core.settings.remove("server-list");
}
var url = becontrol.active() ? serverJsonBeURL : serverJsonV6URL;
Log.info("Fetching community servers at @", url);
fetchServers();
}
public static void fetchServers(){
var urls = Version.type.equals("bleeding-edge") || Vars.forceBeServers ? serverJsonBeURLs : serverJsonURLs;
if(Core.settings.getBool("communityservers", true)){
fetchServers(urls, 0);
}
}
private static void fetchServers(String[] urls, int index){
if(index >= urls.length) return;
//get servers
Core.net.httpGet(url, result -> {
try{
if(result.getStatus() != HttpStatus.OK){
Log.warn("Failed to fetch community servers: @", result.getStatus());
return;
}
Jval val = Jval.read(result.getResultAsString());
Core.app.post(() -> {
try{
defaultServers.clear();
val.asArray().each(child -> {
String name = child.getString("name", "");
String[] addresses;
if(child.has("addresses") || (child.has("address") && child.get("address").isArray())){
addresses = (child.has("addresses") ? child.get("addresses") : child.get("address")).asArray().map(Jval::asString).toArray(String.class);
}else{
addresses = new String[]{child.getString("address", "<invalid>")};
}
defaultServers.add(new ServerGroup(name, addresses));
});
Log.info("Fetched @ community servers.", defaultServers.size);
}catch(Throwable e){
Log.err("Failed to parse community servers.");
Log.err(e);
}
});
}catch(Throwable e){
Log.err("Failed to fetch community servers.");
Log.err(e);
Http.get(urls[index])
.error(t -> {
if(index < urls.length - 1){
//attempt fetching from the next URL upon failure
fetchServers(urls, index + 1);
}else{
Log.err("Failed to fetch community servers", t);
}
}, Log::err);
})
.submit(result -> {
Jval val = Jval.read(result.getResultAsString());
Seq<ServerGroup> servers = new Seq<>();
val.asArray().each(child -> {
String name = child.getString("name", "");
boolean prioritized = child.getBool("prioritized", false);
String[] addresses;
if(child.has("addresses") || (child.has("address") && child.get("address").isArray())){
addresses = (child.has("addresses") ? child.get("addresses") : child.get("address")).asArray().map(Jval::asString).toArray(String.class);
}else{
addresses = new String[]{child.getString("address", "<invalid>")};
}
servers.add(new ServerGroup(name, addresses, prioritized));
});
//modify default servers on main thread
Core.app.post(() -> {
servers.sort(s -> s.name == null ? Integer.MAX_VALUE : s.name.hashCode());
defaultServers.addAll(servers);
Log.info("Fetched @ community servers.", defaultServers.sum(s -> s.addresses.length));
});
});
}
private void saveServers(){
Core.settings.putJson("servers", Server.class, servers);
}
private String getVersionString(Host host){
if(host.version == -1){
return Core.bundle.format("server.version", Core.bundle.get("server.custombuild"), "");
}else if(host.version == 0){
return Core.bundle.get("server.outdated");
}else if(host.version < Version.build && Version.build != -1){
return Core.bundle.get("server.outdated") + "\n" +
Core.bundle.format("server.version", host.version, "");
}else if(host.version > Version.build && Version.build != -1){
return Core.bundle.get("server.outdated.client") + "\n" +
Core.bundle.format("server.version", host.version, "");
}else if(host.version == Version.build && Version.type.equals(host.versionType)){
//not important
return "";
}else{
return Core.bundle.format("server.version", host.version, host.versionType);
}
}
public static class Server{
public String ip;
public int port;
@@ -568,7 +718,7 @@ public class JoinDialog extends BaseDialog{
if(isIpv6 && ip.lastIndexOf("]:") != -1 && ip.lastIndexOf("]:") != ip.length() - 1){
int idx = ip.indexOf("]:");
this.ip = ip.substring(1, idx);
this.port = Integer.parseInt(ip.substring(idx + 2, ip.length()));
this.port = Integer.parseInt(ip.substring(idx + 2));
}else if(!isIpv6 && ip.lastIndexOf(':') != -1 && ip.lastIndexOf(':') != ip.length() - 1){
int idx = ip.lastIndexOf(':');
this.ip = ip.substring(0, idx);

View File

@@ -0,0 +1,233 @@
package mindustry.ui.dialogs;
import arc.*;
import arc.KeyBinds.*;
import arc.graphics.*;
import arc.input.*;
import arc.input.InputDevice.*;
import arc.scene.event.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.ui.*;
import static arc.Core.*;
public class KeybindDialog extends Dialog{
protected Section section;
protected KeyBind rebindKey = null;
protected boolean rebindAxis = false;
protected boolean rebindMin = true;
protected KeyCode minKey = null;
protected Dialog rebindDialog;
protected float scroll;
protected ObjectIntMap<Section> sectionControls = new ObjectIntMap<>();
public KeybindDialog(){
super(bundle.get("keybind.title"));
setup();
addCloseButton();
setFillParent(true);
title.setAlignment(Align.center);
titleTable.row();
titleTable.add(new Image()).growX().height(3f).pad(4f).get().setColor(Pal.accent);
}
@Override
public void addCloseButton(){
buttons.button("@back", Icon.left, this::hide).size(210f, 64f);
keyDown(key -> {
if(key == KeyCode.escape || key == KeyCode.back) hide();
});
}
private void setup(){
cont.clear();
Section[] sections = Core.keybinds.getSections();
Stack stack = new Stack();
ButtonGroup<TextButton> group = new ButtonGroup<>();
ScrollPane pane = new ScrollPane(stack);
pane.setFadeScrollBars(false);
pane.update(() -> scroll = pane.getScrollY());
this.section = sections[0];
for(Section section : sections){
if(!sectionControls.containsKey(section))
sectionControls.put(section, input.getDevices().indexOf(section.device, true));
if(sectionControls.get(section, 0) >= input.getDevices().size){
sectionControls.put(section, 0);
section.device = input.getDevices().get(0);
}
if(sections.length != 1){
TextButton button = new TextButton(bundle.get("section." + section.name + ".name", Strings.capitalize(section.name)));
if(section.equals(this.section))
button.toggle();
button.clicked(() -> this.section = section);
group.add(button);
cont.add(button).fill();
}
Table table = new Table();
Label device = new Label("Keyboard");
//device.setColor(style.controllerColor);
device.setAlignment(Align.center);
Seq<InputDevice> devices = input.getDevices();
Table stable = new Table();
stable.button("<", () -> {
int i = sectionControls.get(section, 0);
if(i - 1 >= 0){
sectionControls.put(section, i - 1);
section.device = devices.get(i - 1);
setup();
}
}).disabled(sectionControls.get(section, 0) - 1 < 0).size(40);
stable.add(device).minWidth(device.getMinWidth() + 60);
device.setText(input.getDevices().get(sectionControls.get(section, 0)).name());
stable.button(">", () -> {
int i = sectionControls.get(section, 0);
if(i + 1 < devices.size){
sectionControls.put(section, i + 1);
section.device = devices.get(i + 1);
setup();
}
}).disabled(sectionControls.get(section, 0) + 1 >= devices.size).size(40);
//no alternate devices until further notice
//table.add(stable).colspan(4).row();
table.add().height(10);
table.row();
if(section.device.type() == DeviceType.controller){
table.table(info -> info.add("Controller Type: [lightGray]" +
Strings.capitalize(section.device.name())).left());
}
table.row();
String lastCategory = null;
var tstyle = Styles.defaultt;
for(KeyBind keybind : keybinds.getKeybinds()){
if(lastCategory != keybind.category() && keybind.category() != null){
table.add(bundle.get("category." + keybind.category() + ".name", Strings.capitalize(keybind.category()))).color(Color.gray).colspan(4).pad(10).padBottom(4).row();
table.image().color(Color.gray).fillX().height(3).pad(6).colspan(4).padTop(0).padBottom(10).row();
lastCategory = keybind.category();
}
if(keybind.defaultValue(section.device.type()) instanceof Axis){
table.add(bundle.get("keybind." + keybind.name() + ".name", Strings.capitalize(keybind.name())), Color.white).left().padRight(40).padLeft(8);
table.labelWrap(() -> {
Axis axis = keybinds.get(section, keybind);
return axis.key != null ? axis.key.toString() : axis.min + " [red]/[] " + axis.max;
}).color(Pal.accent).left().minWidth(90).fillX().padRight(20);
table.button("@settings.rebind", tstyle, () -> {
rebindAxis = true;
rebindMin = true;
openDialog(section, keybind);
}).width(130f);
}else{
table.add(bundle.get("keybind." + keybind.name() + ".name", Strings.capitalize(keybind.name())), Color.white).left().padRight(40).padLeft(8);
table.label(() -> keybinds.get(section, keybind).key.toString()).color(Pal.accent).left().minWidth(90).padRight(20);
table.button("@settings.rebind", tstyle, () -> {
rebindAxis = false;
rebindMin = false;
openDialog(section, keybind);
}).width(130f);
}
table.button("@settings.resetKey", tstyle, () -> keybinds.resetToDefault(section, keybind)).width(130f).pad(2f).padLeft(4f);
table.row();
}
table.visible(() -> this.section.equals(section));
table.button("@settings.reset", () -> keybinds.resetToDefaults()).colspan(4).padTop(4).fill();
stack.add(table);
}
cont.row();
cont.add(pane).growX().colspan(sections.length);
}
void rebind(Section section, KeyBind bind, KeyCode newKey){
if(rebindKey == null) return;
rebindDialog.hide();
boolean isAxis = bind.defaultValue(section.device.type()) instanceof Axis;
if(isAxis){
if(newKey.axis || !rebindMin){
section.binds.get(section.device.type(), OrderedMap::new).put(rebindKey, newKey.axis ? new Axis(newKey) : new Axis(minKey, newKey));
}
}else{
section.binds.get(section.device.type(), OrderedMap::new).put(rebindKey, new Axis(newKey));
}
if(rebindAxis && isAxis && rebindMin && !newKey.axis){
rebindMin = false;
minKey = newKey;
openDialog(section, rebindKey);
}else{
rebindKey = null;
rebindAxis = false;
}
}
private void openDialog(Section section, KeyBind name){
rebindDialog = new Dialog(rebindAxis ? bundle.get("keybind.press.axis") : bundle.get("keybind.press"));
rebindKey = name;
rebindDialog.titleTable.getCells().first().pad(4);
if(section.device.type() == DeviceType.keyboard){
rebindDialog.addListener(new InputListener(){
@Override
public boolean touchDown(InputEvent event, float x, float y, int pointer, KeyCode button){
if(Core.app.isAndroid()) return false;
rebind(section, name, button);
return false;
}
@Override
public boolean keyDown(InputEvent event, KeyCode keycode){
rebindDialog.hide();
rebind(section, name, keycode);
return false;
}
@Override
public boolean scrolled(InputEvent event, float x, float y, float amountX, float amountY){
if(!rebindAxis) return false;
rebindDialog.hide();
rebind(section, name, KeyCode.scroll);
return false;
}
});
}
rebindDialog.show();
Time.runTask(1f, () -> getScene().setScrollFocus(rebindDialog));
}
}

View File

@@ -5,6 +5,7 @@ import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import mindustry.type.*;
import mindustry.ui.*;
import java.util.*;
@@ -12,18 +13,58 @@ import java.util.*;
import static mindustry.Vars.*;
public class LanguageDialog extends BaseDialog{
private Locale lastLocale;
private ObjectMap<Locale, String> displayNames = ObjectMap.of(
Locale.TRADITIONAL_CHINESE, "正體中文",
Locale.SIMPLIFIED_CHINESE, "简体中文"
public static final ObjectMap<String, String> displayNames = ObjectMap.of(
"ca", "Català",
"id_ID", "Bahasa Indonesia",
"da", "Dansk",
"de", "Deutsch",
"et", "Eesti",
"en", "English",
"es", "Español",
"eu", "Euskara",
"fil", "Filipino",
"fr", "Français",
"it", "Italiano",
"lt", "Lietuvių",
"hu", "Magyar",
"nl", "Nederlands",
"nl_BE", "Nederlands (België)",
"pl", "Polski",
"pt_BR", "Português (Brasil)",
"pt_PT", "Português (Portugal)",
"ro", "Română",
"fi", "Suomi",
"sv", "Svenska",
"vi", "Tiếng Việt",
"tk", "Türkmen dili",
"tr", "Türkçe",
"cs", "Čeština",
"be", "Беларуская",
"bg", "Български",
"ru", "Русский",
"sr", "Српски",
"uk_UA", "Українська",
"th", "ไทย",
"zh_CN", "简体中文",
"zh_TW", "正體中文",
"ja", "日本語",
"ko", "한국어",
"router", "router"
);
private Locale lastLocale;
public LanguageDialog(){
super("@settings.language");
addCloseButton();
setup();
}
public static String getDisplayName(Locale locale){
String str = locale.toString().replace("in_ID", "id_ID");
return displayNames.get(str, str);
}
private void setup(){
Table langs = new Table();
langs.marginRight(24f).marginLeft(24f);
@@ -33,11 +74,12 @@ public class LanguageDialog extends BaseDialog{
ButtonGroup<TextButton> group = new ButtonGroup<>();
for(Locale loc : locales){
TextButton button = new TextButton(Strings.capitalize(displayNames.get(loc, loc.getDisplayName(loc))), Styles.clearTogglet);
TextButton button = new TextButton(getDisplayName(loc), Styles.flatTogglet);
button.clicked(() -> {
if(getLocale().equals(loc)) return;
Core.settings.put("locale", loc.toString());
Log.info("Setting locale: @", loc.toString());
player.locale = loc.toString();
ui.showInfo("@language.restart");
});
langs.add(button).group(group).update(t -> t.setChecked(loc.equals(getLocale()))).size(400f, 50f).row();

View File

@@ -2,10 +2,12 @@ package mindustry.ui.dialogs;
import arc.*;
import arc.func.*;
import arc.math.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import mindustry.ctype.*;
import arc.util.*;
import mindustry.content.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.type.*;
@@ -24,12 +26,14 @@ public class LaunchLoadoutDialog extends BaseDialog{
Schematic selected;
//validity of loadout items
boolean valid;
//last calculated capacity
int lastCapacity;
public LaunchLoadoutDialog(){
super("@configure");
}
public void show(CoreBlock core, Sector sector, Runnable confirm){
public void show(CoreBlock core, Sector sector, Sector destination, Runnable confirm){
cont.clear();
buttons.clear();
@@ -40,19 +44,52 @@ public class LaunchLoadoutDialog extends BaseDialog{
ItemSeq sitems = sector.items();
//hide nonsensical items
ItemSeq launch = universe.getLaunchResources();
if(sector.planet.allowLaunchLoadout){
for(var item : content.items()){
if(!item.isOnPlanet(sector.planet)){
launch.set(item, 0);
}
}
universe.updateLaunchResources(launch);
}
//updates sum requirements
Runnable update = () -> {
int cap = selected.findCore().itemCapacity;
int cap = lastCapacity = (int)(sector.planet.launchCapacityMultiplier * selected.findCore().itemCapacity);
//cap resources based on core type
ItemSeq schems = selected.requirements();
ItemSeq resources = universe.getLaunchResources();
resources.min(cap);
int capacity = lastCapacity;
if(!destination.allowLaunchLoadout()){
resources.clear();
//TODO this should be set to a proper loadout based on sector.
if(destination.preset != null){
var rules = destination.preset.generator.map.rules();
for(var stack : rules.loadout){
if(stack.item.isOnPlanet(sector.planet)){
resources.add(stack.item, stack.amount);
}
}
}
}else if(getMax()){
for(Item item : content.items()){
resources.set(item, Mathf.clamp(sitems.get(item) - schems.get(item), 0, capacity));
}
}
universe.updateLaunchResources(resources);
total.clear();
selected.requirements().each(total::add);
universe.getLaunchResources().each(total::add);
valid = sitems.has(total);
valid = sitems.has(total) || PlanetDialog.debugSelect;
};
Cons<Table> rebuild = table -> {
@@ -63,10 +100,13 @@ public class LaunchLoadoutDialog extends BaseDialog{
ItemSeq launches = universe.getLaunchResources();
for(ItemStack s : total){
table.image(s.item.icon(Cicon.small)).left().size(Cicon.small.size);
int as = schems.get(s.item), al = launches.get(s.item);
String amountStr = (al + as) + "[gray] (" + (al + " + " + as + ")");
if(as + al == 0) continue;
table.image(s.item.uiIcon).left().size(iconSmall);
String amountStr = (al + as) + (destination.allowLaunchLoadout() ? "[gray] (" + (al + " + " + as + ")") : "");
table.add(
sitems.has(s.item, s.amount) ? amountStr :
@@ -82,53 +122,95 @@ public class LaunchLoadoutDialog extends BaseDialog{
Runnable rebuildItems = () -> rebuild.get(items);
buttons.button("@resources", Icon.terrain, () -> {
ItemSeq stacks = universe.getLaunchResources();
Seq<ItemStack> out = stacks.toSeq();
loadout.show(selected.findCore().itemCapacity, out, UnlockableContent::unlocked, out::clear, () -> {}, () -> {
universe.updateLaunchResources(new ItemSeq(out));
if(destination.allowLaunchLoadout()){
buttons.button("@resources.max", Icon.add, Styles.togglet, () -> {
setMax(!getMax());
update.run();
rebuildItems.run();
});
}).width(204);
}).checked(b -> getMax());
buttons.button("@launch.text", Icon.ok, () -> {
buttons.button("@resources", Icon.edit, () -> {
ItemSeq stacks = universe.getLaunchResources();
Seq<ItemStack> out = stacks.toSeq();
ItemSeq realItems = sitems.copy();
selected.requirements().each(realItems::remove);
loadout.show(lastCapacity, realItems, out, i -> i.unlocked() && i.isOnPlanet(sector.planet), out::clear, () -> {}, () -> {
universe.updateLaunchResources(new ItemSeq(out));
update.run();
rebuildItems.run();
});
}).disabled(b -> getMax());
}
boolean rows = Core.graphics.isPortrait() && mobile;
if(rows) buttons.row();
var cell = buttons.button("@launch.text", Icon.ok, () -> {
universe.updateLoadout(core, selected);
confirm.run();
hide();
}).disabled(b -> !valid);
if(rows){
cell.colspan(2).size(160f + 160f + 4f, 64f);
}
int cols = Math.max((int)(Core.graphics.getWidth() / Scl.scl(230)), 1);
ButtonGroup<Button> group = new ButtonGroup<>();
selected = universe.getLoadout(core);
if(selected == null) selected = schematics.getLoadouts().get((CoreBlock)Blocks.coreShard).first();
cont.add(Core.bundle.format("launch.from", sector.name())).row();
cont.pane(t -> {
int i = 0;
if(destination.allowLaunchSchematics()){
cont.pane(t -> {
int[] i = {0};
for(var entry : schematics.getLoadouts()){
if(entry.key.size <= core.size){
for(Schematic s : entry.value){
Cons<Schematic> handler = s -> {
if(s.tiles.contains(tile -> !tile.block.supportsEnv(sector.planet.defaultEnv) ||
//make sure block can be built here.
!tile.block.isOnPlanet(sector.planet))){
return;
}
t.button(b -> b.add(new SchematicImage(s)), Styles.togglet, () -> {
selected = s;
update.run();
rebuildItems.run();
}).group(group).pad(4).checked(s == selected).size(200f);
t.button(b -> b.add(new SchematicImage(s)), Styles.togglet, () -> {
selected = s;
update.run();
rebuildItems.run();
}).group(group).pad(4).checked(s == selected).size(200f);
if(++i % cols == 0){
t.row();
if(++i[0] % cols == 0){
t.row();
}
};
if(destination.allowLaunchSchematics() || schematics.getDefaultLoadout(core) == null){
for(var entry : schematics.getLoadouts()){
if(entry.key.size <= core.size){
for(Schematic s : entry.value){
handler.get(s);
}
}
}
}else{
//only allow launching with the standard loadout schematic
handler.get(schematics.getDefaultLoadout(core));
}
}
}).growX().scrollX(false);
cont.row();
}).growX().get().setScrollingDisabled(true, false);
cont.label(() -> Core.bundle.format("launch.capacity", lastCapacity)).row();
cont.row();
}else if(destination.preset != null && destination.preset.description != null){
cont.pane(p -> {
p.add(destination.preset.description).grow().wrap().labelAlign(Align.center);
}).pad(10f).grow().row();
}
cont.row();
cont.pane(items);
cont.row();
cont.add("@sector.missingresources").visible(() -> !valid);
@@ -138,4 +220,12 @@ public class LaunchLoadoutDialog extends BaseDialog{
show();
}
void setMax(boolean max){
Core.settings.put("maxresources", max);
}
boolean getMax(){
return Core.settings.getBool("maxresources", true);
}
}

View File

@@ -8,7 +8,9 @@ import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import mindustry.*;
import mindustry.core.GameState.*;
import mindustry.game.*;
import mindustry.game.Saves.*;
import mindustry.gen.*;
import mindustry.io.*;
@@ -20,8 +22,11 @@ import java.io.*;
import static mindustry.Vars.*;
public class LoadDialog extends BaseDialog{
ScrollPane pane;
Table slots;
String searchString;
Seq<Gamemode> hidden;
TextField searchField;
ScrollPane pane;
public LoadDialog(){
this("@loadgame");
@@ -31,7 +36,10 @@ public class LoadDialog extends BaseDialog{
super(title);
setup();
shown(this::setup);
shown(() -> {
searchString = "";
setup();
});
onResize(this::setup);
addCloseButton();
@@ -42,10 +50,40 @@ public class LoadDialog extends BaseDialog{
cont.clear();
slots = new Table();
hidden = new Seq<>();
pane = new ScrollPane(slots);
rebuild();
Table search = new Table();
search.image(Icon.zoom);
searchField = search.field("", t -> {
searchString = t.length() > 0 ? t.toLowerCase() : null;
rebuild();
}).maxTextLength(50).growX().get();
searchField.setMessageText("@save.search");
for(Gamemode mode : Gamemode.all){
TextureRegionDrawable icon = Vars.ui.getIcon("mode" + Strings.capitalize(mode.name()));
boolean sandbox = mode == Gamemode.sandbox;
if(Core.atlas.isFound(icon.getRegion()) || sandbox){
search.button(sandbox ? Icon.terrain : icon, Styles.emptyTogglei, () -> {
if(!hidden.addUnique(mode)) hidden.remove(mode);
rebuild();
}).size(60f).padLeft(-12f).checked(b -> !hidden.contains(mode)).tooltip("@mode." + mode.name() + ".name");
}
}
pane.setFadeScrollBars(false);
pane.setScrollingDisabled(true, false);
cont.add(search).growX();
cont.row();
cont.add(pane).growY();
}
public void rebuild(){
slots.clear();
slots.marginRight(24).marginLeft(20f);
Time.runTask(2f, () -> Core.scene.setScrollFocus(pane));
@@ -58,11 +96,15 @@ public class LoadDialog extends BaseDialog{
boolean any = false;
for(SaveSlot slot : array){
if(slot.isHidden()) continue;
if(slot.isHidden()
|| (searchString != null && !Strings.stripColors(slot.getName()).toLowerCase().contains(searchString))
|| (!hidden.isEmpty() && hidden.contains(slot.mode()))){
continue;
}
any = true;
TextButton button = new TextButton("", Styles.cleart);
TextButton button = new TextButton("", Styles.grayt);
button.getLabel().remove();
button.clearChildren();
@@ -75,21 +117,21 @@ public class LoadDialog extends BaseDialog{
t.right();
t.defaults().size(40f);
t.button(Icon.save, Styles.emptytogglei, () -> {
t.button(Icon.save, Styles.emptyTogglei, () -> {
slot.setAutosave(!slot.isAutosave());
}).checked(slot.isAutosave()).right();
t.button(Icon.trash, Styles.emptyi, () -> {
ui.showConfirm("@confirm", "@save.delete.confirm", () -> {
slot.delete();
setup();
rebuild();
});
}).right();
t.button(Icon.pencil, Styles.emptyi, () -> {
ui.showTextInput("@save.rename", "@save.rename.text", slot.getName(), text -> {
slot.setName(text);
setup();
rebuild();
});
}).right();
@@ -141,10 +183,8 @@ public class LoadDialog extends BaseDialog{
}
if(!any){
slots.button("@save.none", () -> {}).disabled(true).fillX().margin(20f).minWidth(340f).height(80f).pad(4f);
slots.add("@save.none");
}
cont.add(pane);
}
public void addSetup(){
@@ -152,12 +192,18 @@ public class LoadDialog extends BaseDialog{
buttons.button("@save.import", Icon.add, () -> {
platform.showFileChooser(true, saveExtension, file -> {
if(SaveIO.isSaveValid(file)){
try{
control.saves.importSave(file);
setup();
}catch(IOException e){
e.printStackTrace();
ui.showException("@save.import.fail", e);
var meta = SaveIO.getMeta(file);
if(meta.rules.sector != null){
ui.showErrorMessage("@save.nocampaign");
}else{
try{
control.saves.importSave(file);
rebuild();
}catch(IOException e){
e.printStackTrace();
ui.showException("@save.import.fail", e);
}
}
}else{
ui.showErrorMessage("@save.import.invalid");
@@ -193,4 +239,15 @@ public class LoadDialog extends BaseDialog{
}
});
}
@Override
public Dialog show(){
super.show();
if(Core.app.isDesktop() && searchField != null){
Core.scene.setKeyboardFocus(searchField);
}
return this;
}
}

View File

@@ -22,6 +22,7 @@ public class LoadoutDialog extends BaseDialog{
private Boolf<Item> validator = i -> true;
private Table items;
private int capacity;
private @Nullable ItemSeq total;
public LoadoutDialog(){
super("@configure");
@@ -46,6 +47,8 @@ public class LoadoutDialog extends BaseDialog{
buttons.button("@back", Icon.left, this::hide).size(210f, 64f);
buttons.button("@max", Icon.export, this::maxItems).size(210f, 64f);
buttons.button("@settings.reset", Icon.refresh, () -> {
resetter.run();
reseed();
@@ -54,12 +57,23 @@ public class LoadoutDialog extends BaseDialog{
}).size(210f, 64f);
}
public void maxItems(){
for(ItemStack stack : stacks){
stack.amount = total == null ? capacity : Math.max(Math.min(capacity, total.get(stack.item)), 0);
}
}
public void show(int capacity, Seq<ItemStack> stacks, Boolf<Item> validator, Runnable reseter, Runnable updater, Runnable hider){
show(capacity, null, stacks, validator, reseter, updater, hider);
}
public void show(int capacity, ItemSeq total, Seq<ItemStack> stacks, Boolf<Item> validator, Runnable reseter, Runnable updater, Runnable hider){
this.originalStacks = stacks;
this.validator = validator;
this.resetter = reseter;
this.updater = updater;
this.capacity = capacity;
this.total = total;
this.hider = hider;
reseed();
show();
@@ -75,17 +89,17 @@ public class LoadoutDialog extends BaseDialog{
for(ItemStack stack : stacks){
items.table(Tex.pane, t -> {
t.margin(4).marginRight(8).left();
t.button("-", Styles.cleart, () -> {
t.button("-", Styles.flatt, () -> {
stack.amount = Math.max(stack.amount - step(stack.amount), 0);
updater.run();
}).size(bsize);
t.button("+", Styles.cleart, () -> {
t.button("+", Styles.flatt, () -> {
stack.amount = Math.min(stack.amount + step(stack.amount), capacity);
updater.run();
}).size(bsize);
t.button(Icon.pencil, Styles.cleari, () -> ui.showTextInput("@configure", stack.item.localizedName, 10, stack.amount + "", true, str -> {
t.button(Icon.pencil, Styles.flati, () -> ui.showTextInput("@configure", stack.item.localizedName, 10, stack.amount + "", true, str -> {
if(Strings.canParsePositiveInt(str)){
int amount = Strings.parseInt(str);
if(amount >= 0 && amount <= capacity){
@@ -97,7 +111,7 @@ public class LoadoutDialog extends BaseDialog{
ui.showInfo(Core.bundle.format("configure.invalid", capacity));
})).size(bsize);
t.image(stack.item.icon(Cicon.small)).size(8 * 3).padRight(4).padLeft(4);
t.image(stack.item.uiIcon).size(8 * 3).padRight(4).padLeft(4);
t.label(() -> stack.amount + "").left().width(90f);
}).pad(2).left().fillX();
@@ -110,7 +124,7 @@ public class LoadoutDialog extends BaseDialog{
private void reseed(){
this.stacks = originalStacks.map(ItemStack::copy);
this.stacks.addAll(content.items().select(i -> validator.get(i) && !stacks.contains(stack -> stack.item == i)).map(i -> new ItemStack(i, 0)));
this.stacks.addAll(content.items().select(i -> validator.get(i) && !i.isHidden() && !stacks.contains(stack -> stack.item == i)).map(i -> new ItemStack(i, 0)));
this.stacks.sort(Structs.comparingInt(s -> s.item.id));
}

View File

@@ -0,0 +1,233 @@
package mindustry.ui.dialogs;
import arc.*;
import arc.graphics.*;
import arc.scene.style.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import mindustry.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.maps.*;
import mindustry.ui.*;
import static mindustry.Vars.*;
public abstract class MapListDialog extends BaseDialog{
BaseDialog activeDialog;
private String searchString;
private Seq<Gamemode> modes = new Seq<>();
private Table mapTable = new Table();
private TextField searchField;
private boolean
showBuiltIn = Core.settings.getBool("editorshowbuiltinmaps", true),
showCustom = Core.settings.getBool("editorshowcustommaps", true),
searchAuthor = Core.settings.getBool("editorsearchauthor", false),
searchDescription = Core.settings.getBool("editorsearchdescription", false),
displayType;
public MapListDialog(String title, boolean displayType){
super(title);
this.displayType = displayType;
buttons.remove();
addCloseListener();
shown(this::setup);
onResize(() -> {
if(activeDialog != null){
activeDialog.hide();
}
setup();
});
}
void buildButtons(){}
abstract void showMap(Map map);
void setup(){
makeButtonOverlay();
buttons.clearChildren();
searchString = null;
if(Core.graphics.isPortrait() && displayType){
buttons.button("@back", Icon.left, this::hide).size(210f * 2f, 64f).colspan(2);
buttons.row();
}else{
buttons.button("@back", Icon.left, this::hide).size(210f, 64f);
}
buildButtons();
cont.clear();
rebuildMaps();
ScrollPane pane = new ScrollPane(mapTable);
pane.setFadeScrollBars(false);
pane.setScrollingDisabledX(true);
Table search = new Table();
search.image(Icon.zoom);
searchField = search.field("", t -> {
searchString = t.length() > 0 ? t.toLowerCase() : null;
rebuildMaps();
}).maxTextLength(50).growX().get();
searchField.setMessageText("@editor.search");
search.button(Icon.filter, Styles.emptyi, this::showMapFilters).tooltip("@editor.filters");
cont.add(search).growX();
cont.row();
cont.add(pane).padLeft(28f).uniformX().growY();
}
void rebuildMaps(){
mapTable.clear();
mapTable.marginRight(12f);
int maxwidth = Math.max((int)(Core.graphics.getWidth() / Scl.scl(230)), 1);
float mapsize = 200f;
boolean noMapsShown = true;
int i = 0;
Seq<Map> mapList = showCustom ?
showBuiltIn ? maps.all() : maps.customMaps() :
showBuiltIn ? maps.defaultMaps() : null;
if(mapList != null){
for(Map map : mapList){
boolean invalid = false;
for(Gamemode mode : modes){
invalid |= !mode.valid(map);
}
if(invalid || (searchString != null
&& !map.plainName().toLowerCase().contains(searchString)
&& (!searchAuthor || !map.plainAuthor().toLowerCase().contains(searchString))
&& (!searchDescription || !map.plainDescription().toLowerCase().contains(searchString)))){
continue;
}
noMapsShown = false;
if(i % maxwidth == 0){
mapTable.row();
}
TextButton button = mapTable.button("", Styles.grayt, () -> showMap(map)).width(mapsize).bottom().pad(8).get();
button.clearChildren();
button.margin(9);
button.bottom();
//TODO hide in editor?
button.table(t -> {
t.left();
for(Gamemode mode : Gamemode.all){
TextureRegionDrawable icon = Vars.ui.getIcon("mode" + Strings.capitalize(mode.name()) + "Small");
if(mode.valid(map) && Core.atlas.isFound(icon.getRegion())){
t.image(icon).size(16f).pad(4f);
}
}
if(t.getChildren().size == 0){
t.add().size(16f).pad(4f);
}
}).left().row();
button.add(map.name()).width(mapsize - 18f).center().get().setEllipsis(true);
button.row();
button.image().growX().pad(4).color(Pal.gray);
button.row();
button.stack(new Image(map.safeTexture()).setScaling(Scaling.fit), new BorderImage(map.safeTexture()).setScaling(Scaling.fit)).size(mapsize - 20f);
if(displayType){
button.row();
button.add(map.custom ? "@custom" : map.workshop ? "@workshop" : map.mod != null ? "[lightgray]" + map.mod.meta.displayName : "@builtin").color(Color.gray).padTop(3);
}
i++;
}
}
if(noMapsShown){
mapTable.add("@maps.none");
}
}
void showMapFilters(){
activeDialog = new BaseDialog("@editor.filters");
activeDialog.addCloseButton();
activeDialog.cont.table(menu -> {
menu.add("@editor.filters.mode").width(150f).left();
menu.table(t -> {
for(Gamemode mode : Gamemode.all){
TextureRegionDrawable icon = Vars.ui.getIcon("mode" + Strings.capitalize(mode.name()));
if(Core.atlas.isFound(icon.getRegion())){
t.button(icon, Styles.emptyTogglei, () -> {
if(modes.contains(mode)){
modes.remove(mode);
}else{
modes.add(mode);
}
rebuildMaps();
}).size(60f).checked(modes.contains(mode)).tooltip("@mode." + mode.name() + ".name");
}
}
}).padBottom(10f);
menu.row();
menu.add("@editor.filters.type").width(150f).left();
menu.table(Tex.button, t -> {
t.button("@custom", Styles.flatTogglet, () -> {
showCustom = !showCustom;
Core.settings.put("editorshowcustommaps", showCustom);
rebuildMaps();
}).size(150f, 60f).checked(showCustom);
t.button("@builtin", Styles.flatTogglet, () -> {
showBuiltIn = !showBuiltIn;
Core.settings.put("editorshowbuiltinmaps", showBuiltIn);
rebuildMaps();
}).size(150f, 60f).checked(showBuiltIn);
}).padBottom(10f);
menu.row();
menu.add("@editor.filters.search").width(150f).left();
menu.table(Tex.button, t -> {
t.button("@editor.filters.author", Styles.flatTogglet, () -> {
searchAuthor = !searchAuthor;
Core.settings.put("editorsearchauthor", searchAuthor);
rebuildMaps();
}).size(150f, 60f).checked(searchAuthor);
t.button("@editor.filters.description", Styles.flatTogglet, () -> {
searchDescription = !searchDescription;
Core.settings.put("editorsearchdescription", searchDescription);
rebuildMaps();
}).size(150f, 60f).checked(searchDescription);
});
});
activeDialog.show();
}
@Override
public Dialog show(){
super.show();
if(Core.app.isDesktop() && searchField != null){
Core.scene.setKeyboardFocus(searchField);
}
return this;
}
}

View File

@@ -12,7 +12,9 @@ import mindustry.ui.*;
import static mindustry.Vars.*;
public class MapPlayDialog extends BaseDialog{
CustomRulesDialog dialog = new CustomRulesDialog();
public @Nullable Runnable playListener;
CustomRulesDialog dialog = new CustomRulesDialog(true);
Rules rules;
Gamemode selectedGamemode = Gamemode.survival;
Map lastMap;
@@ -31,6 +33,10 @@ public class MapPlayDialog extends BaseDialog{
}
public void show(Map map){
show(map, false);
}
public void show(Map map, boolean playtesting){
this.lastMap = map;
title.setText(map.name());
cont.clearChildren();
@@ -46,27 +52,27 @@ public class MapPlayDialog extends BaseDialog{
rules = map.applyRules(selectedGamemode);
Table selmode = new Table();
selmode.add("@level.mode").colspan(4);
selmode.add("@level.mode").colspan(2);
selmode.row();
int i = 0;
Table modes = new Table();
selmode.table(Tex.button, modes -> {
int i = 0;
for(Gamemode mode : Gamemode.all){
if(mode.hidden) continue;
for(Gamemode mode : Gamemode.values()){
if(mode.hidden) continue;
modes.button(mode.toString(), Styles.flatToggleMenut, () -> {
selectedGamemode = mode;
rules = map.applyRules(mode);
}).update(b -> b.setChecked(selectedGamemode == mode)).size(140f, mobile ? 44f : 54f).disabled(!mode.valid(map));
if(i++ % 2 == 1) modes.row();
}
});
modes.button(mode.toString(), Styles.togglet, () -> {
selectedGamemode = mode;
rules = map.applyRules(mode);
}).update(b -> b.setChecked(selectedGamemode == mode)).size(140f, 54f).disabled(!mode.valid(map));
if(i++ % 2 == 1) modes.row();
}
selmode.add(modes);
selmode.button("?", this::displayGameModeHelp).width(50f).fillY().padLeft(18f);
cont.add(selmode);
cont.row();
cont.button("@customize", Icon.settings, () -> dialog.show(rules, () -> rules = map.applyRules(selectedGamemode))).width(230);
cont.button("@customize", Icon.settings, () -> dialog.show(rules, () -> rules = map.applyRules(selectedGamemode))).height(50f).width(230);
cont.row();
cont.add(new BorderImage(map.safeTexture(), 3f)).size(mobile && !Core.graphics.isPortrait() ? 150f : 250f).get().setScaling(Scaling.fit);
//only maps with survival are valid for high scores
@@ -79,7 +85,8 @@ public class MapPlayDialog extends BaseDialog{
addCloseButton();
buttons.button("@play", Icon.play, () -> {
control.playMap(map, rules);
if(playListener != null) playListener.run();
control.playMap(map, rules, playtesting);
hide();
ui.custom.hide();
}).size(210f, 64f);
@@ -95,9 +102,9 @@ public class MapPlayDialog extends BaseDialog{
ScrollPane pane = new ScrollPane(table);
pane.setFadeScrollBars(false);
table.row();
for(Gamemode mode : Gamemode.values()){
for(Gamemode mode : Gamemode.all){
if(mode.hidden) continue;
table.labelWrap("[accent]" + mode.toString() + ":[] [lightgray]" + mode.description()).width(400f);
table.labelWrap("[accent]" + mode + ":[] [lightgray]" + mode.description()).width(400f);
table.row();
}

View File

@@ -1,70 +0,0 @@
package mindustry.ui.dialogs;
import arc.*;
import arc.graphics.*;
import arc.graphics.g2d.*;
import arc.input.*;
import arc.scene.event.*;
import arc.scene.ui.layout.*;
import mindustry.gen.*;
import static mindustry.Vars.*;
public class MinimapDialog extends BaseDialog{
public MinimapDialog(){
super("@minimap");
setFillParent(true);
shown(this::setup);
addCloseButton();
shouldPause = true;
titleTable.remove();
onResize(this::setup);
}
void setup(){
cont.clear();
cont.table(Tex.pane,t -> {
t.rect((x, y, width, height) -> {
if(renderer.minimap.getRegion() == null) return;
Draw.color(Color.white);
Draw.alpha(parentAlpha);
Draw.rect(renderer.minimap.getRegion(), x + width / 2f, y + height / 2f, width, height);
if(renderer.minimap.getTexture() != null){
renderer.minimap.drawEntities(x, y, width, height);
}
}).grow();
}).size(Math.min(Core.graphics.getWidth() / 1.1f, Core.graphics.getHeight() / 1.3f) / Scl.scl(1f)).padTop(-20f);
cont.addListener(new InputListener(){
@Override
public boolean scrolled(InputEvent event, float x, float y, float amountx, float amounty){
renderer.minimap.zoomBy(amounty);
return true;
}
});
cont.addListener(new ElementGestureListener(){
float lzoom = -1f;
@Override
public void touchUp(InputEvent event, float x, float y, int pointer, KeyCode button){
lzoom = renderer.minimap.getZoom();
}
@Override
public void zoom(InputEvent event, float initialDistance, float distance){
if(lzoom < 0){
lzoom = renderer.minimap.getZoom();
}
renderer.minimap.setZoom(initialDistance / distance * lzoom);
}
});
Core.app.post(() -> Core.scene.setScrollFocus(cont));
}
}

View File

@@ -1,112 +1,183 @@
package mindustry.ui.dialogs;
import arc.*;
import arc.Net.*;
import arc.files.*;
import arc.func.*;
import arc.graphics.*;
import arc.graphics.Texture.*;
import arc.graphics.g2d.*;
import arc.input.*;
import arc.scene.style.*;
import arc.scene.ui.TextButton.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import arc.util.Http.*;
import arc.util.io.*;
import arc.util.serialization.*;
import arc.util.serialization.Jval.*;
import mindustry.*;
import mindustry.core.*;
import mindustry.ctype.*;
import mindustry.game.EventType.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.io.*;
import mindustry.mod.*;
import mindustry.mod.Mods.*;
import mindustry.ui.*;
import java.io.*;
import java.text.*;
import java.util.*;
import static mindustry.Vars.*;
public class ModsDialog extends BaseDialog{
private ObjectMap<String, TextureRegion> textureCache = new ObjectMap<>();
private float modImportProgress;
private String searchtxt = "";
private @Nullable Seq<ModListing> modList;
private boolean orderDate = true;
private BaseDialog currentContent;
private BaseDialog browser;
private Table browserTable;
private float scroll = 0f;
public ModsDialog(){
super("@mods");
addCloseButton();
browser = new BaseDialog("@mods.browser");
browser.cont.table(table -> {
table.left();
table.image(Icon.zoom);
table.field(searchtxt, res -> {
searchtxt = res;
rebuildBrowser();
}).growX().get();
table.button(Icon.list, Styles.emptyi, 32f, () -> {
orderDate = !orderDate;
rebuildBrowser();
}).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();
browserTable = tablebrow;
}).scrollX(false);
browser.addCloseButton();
browser.makeButtonOverlay();
browser.onResize(this::rebuildBrowser);
buttons.button("@mods.guide", Icon.link, () -> Core.app.openURI(modGuideURL)).size(210, 64f);
shown(this::setup);
if(mobile){
onResize(this::setup);
if(!mobile){
buttons.button("@mods.openfolder", Icon.link, () -> Core.app.openFolder(modDirectory.absolutePath()));
}
shown(this::setup);
onResize(this::setup);
Events.on(ResizeEvent.class, event -> {
if(currentContent != null){
currentContent.hide();
currentContent = null;
}
});
hidden(() -> {
if(mods.requiresReload()){
reload();
}
});
shown(() -> Core.app.post(() -> {
Core.settings.getBoolOnce("modsalpha", () -> {
ui.showText("@mods", "@mods.alphainfo");
});
}));
}
void modError(Throwable error){
ui.loadfrag.hide();
if(Strings.getCauses(error).contains(t -> t.getMessage() != null && (t.getMessage().contains("trust anchor") || t.getMessage().contains("SSL") || t.getMessage().contains("protocol")))){
if(error instanceof NoSuchMethodError || Strings.getCauses(error).contains(t -> t.getMessage() != null && (t.getMessage().contains("trust anchor") || t.getMessage().contains("SSL") || t.getMessage().contains("protocol")))){
ui.showErrorMessage("@feature.unsupported");
}else if(error instanceof HttpStatusException st){
ui.showErrorMessage(Core.bundle.format("connectfail", Strings.capitalize(st.status.toString().toLowerCase())));
}else if(error.getMessage() != null && error.getMessage().toLowerCase(Locale.ROOT).contains("writable dex")){
ui.showException("@error.moddex", error);
}else{
ui.showException(error);
}
}
void getModList(Cons<Seq<ModListing>> listener){
if(modList == null){
Core.net.httpGet("https://raw.githubusercontent.com/Anuken/MindustryMods/master/mods.json", response -> {
String strResult = response.getResultAsString();
var status = response.getStatus();
void getModList(int index, Cons<Seq<ModListing>> listener){
if(index >= modJsonURLs.length) return;
if(modList != null){
listener.get(modList);
return;
}
Http.get(modJsonURLs[index], response -> {
String strResult = response.getResultAsString();
Core.app.post(() -> {
try{
modList = JsonIO.json.fromJson(Seq.class, ModListing.class, strResult);
var d = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
Func<String, Date> parser = text -> {
try{
return d.parse(text);
}catch(Exception e){
return new Date();
}
};
modList.sortComparing(m -> parser.get(m.lastUpdated)).reverse();
listener.get(modList);
}catch(Exception e){
Log.err(e);
ui.showException(e);
}
});
}, error -> {
if(index < modJsonURLs.length - 1){
getModList(index + 1, listener);
}else{
Core.app.post(() -> {
if(status != HttpStatus.OK){
ui.showErrorMessage(Core.bundle.format("connectfail", status));
}else{
modList = new Json().fromJson(Seq.class, ModListing.class, strResult);
//potentially sort mods by game version compatibility, or other criteria
//modList.sort(Structs.comparingBool(m -> !Version.isAtLeast(m.minGameVersion)));
listener.get(modList);
modError(error);
if(browser != null){
browser.hide();
}
});
}, error -> Core.app.post(() -> ui.showException(error)));
}else{
listener.get(modList);
}
}
});
}
void setup(){
float h = 110f;
float w = mobile ? 440f : 524f;
float w = Math.min(Core.graphics.getWidth() / Scl.scl(1.05f), 520f);
cont.clear();
cont.defaults().width(mobile ? 500 : 560f).pad(4);
cont.defaults().width(Math.min(Core.graphics.getWidth() / Scl.scl(1.05f), 556f)).pad(4);
cont.add("@mod.reloadrequired").visible(mods::requiresReload).center().get().setAlignment(Align.center);
cont.row();
cont.table(buttons -> {
buttons.left().defaults().growX().height(60f).uniformX();
TextButtonStyle style = Styles.clearPartialt;
TextButtonStyle style = Styles.flatBordert;
float margin = 12f;
buttons.button("@mod.import", Icon.add, style, () -> {
BaseDialog dialog = new BaseDialog("@mod.import");
TextButtonStyle bstyle = Styles.cleart;
TextButtonStyle bstyle = Styles.flatt;
dialog.cont.table(Tex.button, t -> {
t.defaults().size(300f, 70f);
@@ -116,21 +187,12 @@ public class ModsDialog extends BaseDialog{
dialog.hide();
platform.showMultiFileChooser(file -> {
Runnable go = () -> {
try{
mods.importMod(file);
setup();
}catch(IOException e){
ui.showException(e);
e.printStackTrace();
}
};
//show unsafe jar file warning
if(file.extEquals("jar")){
ui.showConfirm("@warning", "@mod.jarwarn", go);
}else{
go.run();
try{
mods.importMod(file);
setup();
}catch(Exception e){
ui.showException(e.getMessage() != null && e.getMessage().toLowerCase(Locale.ROOT).contains("writable dex") ? "@error.moddex" : "", e);
Log.err(e);
}
}, "zip", "jar");
}).margin(12f);
@@ -141,81 +203,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);
ui.loadfrag.show();
githubImportMod(text);
//there's no good way to know if it's a java mod here, so assume it's not
githubImportMod(text, false, null);
});
}).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();
}).fillX().padBottom(4);
browser.cont.row();
browser.cont.pane(tablebrow -> {
tablebrow.margin(10f).top();
rebuildBrowser[0] = () -> {
tablebrow.clear();
tablebrow.add("@loading");
getModList(listings -> {
tablebrow.clear();
for(ModListing mod : listings){
if(mod.hasJava || !searchtxt.isEmpty() && !mod.repo.contains(searchtxt) || (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();
});
sel.buttons.button("@mods.browser.add", 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(480f).growX().left().fillY();
tablebrow.row();
}
});
};
rebuildBrowser[0].run();
});
browser.addCloseButton();
browser.show();
}).margin(12f);
});
dialog.addCloseButton();
@@ -223,96 +219,118 @@ 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();
if(!mods.list().isEmpty()){
boolean[] anyDisabled = {false};
SearchBar.add(cont, mods.list(),
mod -> mod.meta.displayName(),
(table, mod) -> {
if(!mod.enabled() && !anyDisabled[0] && mods.list().size > 0){
anyDisabled[0] = true;
table.row();
table.image().growX().height(4f).pad(6f).color(Pal.gray);
table.row();
}
Table[] pane = {null};
table.button(t -> {
t.top().left();
t.margin(12f);
Cons<String> rebuild = query -> {
pane[0].clear();
boolean any = false;
for(LoadedMod item : mods.list()){
if(Strings.matches(query, item.meta.displayName)){
any = true;
if(!item.enabled() && !anyDisabled[0] && mods.list().size > 0){
anyDisabled[0] = true;
pane[0].row();
pane[0].image().growX().height(4f).pad(6f).color(Pal.gray).row();
}
t.defaults().left().top();
t.table(title -> {
title.left();
pane[0].button(t -> {
t.top().left();
t.margin(12f);
title.add(new BorderImage(){{
if(mod.iconTexture != null){
setDrawable(new TextureRegion(mod.iconTexture));
}else{
setDrawable(Tex.nomap);
}
border(Pal.accent);
}}).size(h - 8f).padTop(-8f).padLeft(-8f).padRight(8f);
String stateDetails = getStateDetails(item);
if(stateDetails != null){
t.addListener(new Tooltip(f -> f.background(Styles.black8).margin(4f).add(stateDetails).growX().width(400f).wrap()));
}
title.table(text -> {
boolean hideDisabled = !mod.isSupported() || mod.hasUnmetDependencies() || mod.hasContentErrors();
t.defaults().left().top();
t.table(title1 -> {
title1.left();
text.add("" + mod.meta.displayName() + "\n[lightgray]v" + mod.meta.version + (mod.enabled() || hideDisabled ? "" : "\n" + Core.bundle.get("mod.disabled") + ""))
title1.add(new BorderImage(){{
if(item.iconTexture != null){
setDrawable(new TextureRegion(item.iconTexture));
}else{
setDrawable(Tex.nomap);
}
border(Pal.accent);
}}).size(h - 8f).padTop(-8f).padLeft(-8f).padRight(8f);
title1.table(text -> {
boolean hideDisabled = !item.isSupported() || item.hasUnmetDependencies() || item.hasContentErrors();
String shortDesc = item.meta.shortDescription();
text.add("[accent]" + Strings.stripColors(item.meta.displayName) + "\n" +
(shortDesc.length() > 0 ? "[lightgray]" + shortDesc + "\n" : "")
//so does anybody care about version?
//+ "[gray]v" + Strings.stripColors(trimText(item.meta.version)) + "\n"
+ (item.enabled() || hideDisabled ? "" : Core.bundle.get("mod.disabled") + ""))
.wrap().top().width(300f).growX().left();
text.row();
if(mod.isOutdated()){
text.labelWrap("@mod.outdated").growX();
text.row();
}else if(!mod.isSupported()){
text.labelWrap(Core.bundle.format("mod.requiresversion", mod.meta.minGameVersion)).growX();
text.row();
}else if(mod.hasUnmetDependencies()){
text.labelWrap(Core.bundle.format("mod.missingdependencies", mod.missingDependencies.toString(", "))).growX();
t.row();
}else if(mod.hasContentErrors()){
text.labelWrap("@mod.erroredcontent").growX();
text.row();
}
}).top().growX();
title.add().growX();
}).growX().growY().left();
String state = getStateText(item);
if(state != null){
text.labelWrap(state).growX().row();
}
}).top().growX();
t.table(right -> {
right.right();
right.button(mod.enabled() ? Icon.downOpen : Icon.upOpen, Styles.clearPartiali, () -> {
mods.setEnabled(mod, !mod.enabled());
setup();
}).size(50f).disabled(!mod.isSupported());
title1.add().growX();
}).growX().growY().left();
right.button(mod.hasSteamID() ? Icon.link : Icon.trash, Styles.clearPartiali, () -> {
if(!mod.hasSteamID()){
ui.showConfirm("@confirm", "@mod.remove.confirm", () -> {
mods.removeMod(mod);
setup();
});
}else{
platform.viewListing(mod);
}
}).size(50f);
t.table(right -> {
right.right();
right.button(item.enabled() ? Icon.downOpen : Icon.upOpen, Styles.clearNonei, () -> {
mods.setEnabled(item, !item.enabled());
setup();
}).size(50f).disabled(!item.isSupported());
if(steam && !mod.hasSteamID()){
right.row();
right.button(Icon.download, Styles.clearTransi, () -> {
platform.publish(mod);
right.button(item.hasSteamID() ? Icon.link : Icon.trash, Styles.clearNonei, () -> {
if(!item.hasSteamID()){
ui.showConfirm("@confirm", "@mod.remove.confirm", () -> {
mods.removeMod(item);
setup();
});
}else{
platform.viewListing(item);
}
}).size(50f);
}
}).growX().right().padRight(-8f).padTop(-8f);
}, Styles.clearPartialt, () -> showMod(mod)).size(w, h).growX().pad(4f);
table.row();
}, !mobile || Core.graphics.isPortrait()).margin(10f).top();
if(steam && !item.hasSteamID()){
right.row();
right.button(Icon.export, Styles.clearNonei, () -> {
platform.publish(item);
}).size(50f);
}
}).growX().right().padRight(-8f).padTop(-8f);
}, Styles.flatBordert, () -> showMod(item)).size(w, h).growX().pad(4f);
pane[0].row();
}
}
if(!any){
pane[0].add("@none.found").color(Color.lightGray).pad(4);
}
};
if(!mobile || Core.graphics.isPortrait()){
cont.table(search -> {
search.image(Icon.zoom).padRight(8f);
search.field("", rebuild).growX();
}).fillX().padBottom(4);
}
cont.row();
cont.pane(table1 -> {
pane[0] = table1.margin(10f).top();
rebuild.get("");
}).scrollX(false).update(s -> scroll = s.getScrollY()).get().setScrollYForce(scroll);
}else{
cont.table(Styles.black6, t -> t.add("@mods.none")).height(80f);
}
@@ -320,12 +338,55 @@ public class ModsDialog extends BaseDialog{
cont.row();
}
private @Nullable String getStateText(LoadedMod item){
if(item.isOutdated()){
return "@mod.incompatiblemod";
}else if(item.isBlacklisted()){
return "@mod.blacklisted";
}else if(!item.isSupported()){
return "@mod.incompatiblegame";
}else if(item.state == ModState.circularDependencies){
return "@mod.circulardependencies";
}else if(item.state == ModState.incompleteDependencies){
return "@mod.incompletedependencies";
}else if(item.hasUnmetDependencies()){
return "@mod.unmetdependencies";
}else if(item.hasContentErrors()){
return "@mod.erroredcontent";
}else if(item.meta.hidden){
return "@mod.multiplayer.compatible";
}
return null;
}
private @Nullable String getStateDetails(LoadedMod item){
if(item.isOutdated()){
return "@mod.outdatedv7.details";
}else if(item.isBlacklisted()){
return "@mod.blacklisted.details";
}else if(!item.isSupported()){
return Core.bundle.format("mod.requiresversion.details", item.meta.minGameVersion);
}else if(item.state == ModState.circularDependencies){
return "@mod.circulardependencies.details";
}else if(item.state == ModState.incompleteDependencies){
return Core.bundle.format("mod.incompletedependencies.details", item.missingDependencies.toString(", "));
}else if(item.hasUnmetDependencies()){
return Core.bundle.format("mod.missingdependencies.details", item.missingDependencies.toString(", "));
}else if(item.hasContentErrors()){
return "@mod.erroredcontent.details";
}
return null;
}
private void reload(){
ui.showInfo("@mods.reloadexit", () -> Core.app.exit());
ui.showInfoOnHidden("@mods.reloadexit", () -> {
Log.info("Exiting to reload mods.");
Core.app.exit();
});
}
private void showMod(LoadedMod mod){
BaseDialog dialog = new BaseDialog(mod.meta.displayName());
BaseDialog dialog = new BaseDialog(mod.meta.displayName);
dialog.addCloseButton();
@@ -333,14 +394,20 @@ public class ModsDialog extends BaseDialog{
dialog.buttons.button("@mods.openfolder", Icon.link, () -> Core.app.openFolder(mod.file.absolutePath()));
}
//TODO improve this menu later
if(mod.getRepo() != null){
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(), mod.isJava(), null));
}
dialog.cont.pane(desc -> {
desc.center();
desc.defaults().padTop(10).left();
desc.add("@editor.name").padRight(10).color(Color.gray).padTop(0);
desc.row();
desc.add(mod.meta.displayName()).growX().wrap().padTop(2);
desc.add(mod.meta.displayName).growX().wrap().padTop(2);
desc.row();
if(mod.meta.author != null){
desc.add("@editor.author").padRight(10).color(Color.gray);
@@ -348,6 +415,12 @@ public class ModsDialog extends BaseDialog{
desc.add(mod.meta.author).growX().wrap().padTop(2);
desc.row();
}
if(mod.meta.version != null){
desc.add("@mod.version").padRight(10).color(Color.gray).top();
desc.row();
desc.add(mod.meta.version).growX().wrap().padTop(2);
desc.row();
}
if(mod.meta.description != null){
desc.add("@editor.description").padRight(10).color(Color.gray).top();
desc.row();
@@ -355,41 +428,230 @@ public class ModsDialog extends BaseDialog{
desc.row();
}
String state = getStateDetails(mod);
if(state != null){
desc.add("@mod.disabled").padTop(13f).padBottom(-6f).row();
desc.add(state).growX().wrap().row();
}
}).width(400f);
//TODO maybe enable later
if(false){
Seq<UnlockableContent> all = Seq.with(content.getContentMap()).<Content>flatten().select(c -> c.minfo.mod == mod && c instanceof UnlockableContent).as();
if(all.any()){
dialog.cont.row();
dialog.cont.pane(cs -> {
Seq<UnlockableContent> all = Seq.with(content.getContentMap()).<Content>flatten().select(c -> c.minfo.mod == mod && c instanceof UnlockableContent u && !u.isHidden()).as();
if(all.any()){
dialog.cont.row();
dialog.cont.button("@mods.viewcontent", Icon.book, () -> {
BaseDialog d = new BaseDialog(mod.meta.displayName);
d.cont.pane(cs -> {
int i = 0;
for(UnlockableContent c : all){
cs.button(new TextureRegionDrawable(c.icon(Cicon.medium)), Styles.cleari, Cicon.medium.size, () -> {
cs.button(new TextureRegionDrawable(c.uiIcon), Styles.flati, iconMed, () -> {
ui.content.show(c);
}).size(50f).with(im -> {
var click = im.getClickListener();
im.update(() -> im.getImage().color.lerp(!click.isOver() ? Color.lightGray : Color.white, 0.4f * Time.delta));
});
if(++i % 8 == 0) cs.row();
}).tooltip(c.localizedName);
if(++i % (int)Math.min(Core.graphics.getWidth() / Scl.scl(110), 14) == 0) cs.row();
}
}).growX().minHeight(60f);
}
}).grow();
d.addCloseButton();
d.show();
currentContent = d;
}).size(300, 50).pad(4);
}
dialog.show();
}
private void showModBrowser(){
rebuildBrowser();
browser.show();
}
private void rebuildBrowser(){
ObjectSet<String> installed = mods.list().map(m -> m.getRepo()).asSet();
browserTable.clear();
browserTable.add("@loading");
int cols = (int)Math.max(Core.graphics.getWidth() / Scl.scl(480), 1);
getModList(0, rlistings -> {
browserTable.clear();
int i = 0;
var listings = rlistings;
if(!orderDate){
listings = rlistings.copy();
listings.sortComparing(m1 -> -m1.stars);
}
for(ModListing mod : listings){
if(((mod.hasJava || mod.hasScripts) && Vars.ios) ||
(!Strings.matches(searchtxt, mod.name) && !Strings.matches(searchtxt, mod.repo))
//hack, I'm basically testing if 135.10 >= modVersion, which is equivalent to modVersion >= 136
|| (Version.isAtLeast(135, 10, mod.minGameVersion))
) continue;
float s = 64f;
browserTable.button(con -> {
con.margin(0f);
con.left();
String repo = mod.repo;
con.add(new BorderImage(){
TextureRegion last;
{
border(installed.contains(repo) ? Pal.accent : Color.lightGray);
setDrawable(Tex.nomap);
pad = Scl.scl(4f);
}
@Override
public void draw(){
super.draw();
//textures are only requested when the rendering happens; this assists with culling
if(!textureCache.containsKey(repo)){
textureCache.put(repo, last = Core.atlas.find("nomap"));
Http.get("https://raw.githubusercontent.com/Anuken/MindustryMods/master/icons/" + repo.replace("/", "_"), res -> {
Pixmap pix = new Pixmap(res.getResult());
Core.app.post(() -> {
try{
var tex = new Texture(pix);
tex.setFilter(TextureFilter.linear);
textureCache.put(repo, new TextureRegion(tex));
pix.dispose();
}catch(Exception e){
Log.err(e);
}
});
}, err -> {});
}
var next = textureCache.get(repo);
if(last != next){
last = next;
setDrawable(next);
}
}
}).size(s).pad(4f * 2f);
con.add(
"[accent]" + mod.name.replace("\n", "") +
(installed.contains(mod.repo) ? "\n[lightgray]" + Core.bundle.get("mod.installed") : "") +
"\n[lightgray]\uE809 " + mod.stars +
(Version.isAtLeast(mod.minGameVersion) ? "" :
"\n" + Core.bundle.format("mod.requiresversion", mod.minGameVersion)))
.width(358f).wrap().grow().pad(4f, 2f, 4f, 6f).top().left().labelAlign(Align.topLeft);
}, Styles.flatBordert, () -> {
var sel = new BaseDialog(mod.name);
sel.cont.pane(p -> p.add(mod.description + "\n\n[accent]" + Core.bundle.get("editor.author") + "[lightgray] " + mod.author)
.width(mobile ? 400f : 500f).wrap().pad(4f).labelAlign(Align.center, Align.left)).grow();
sel.buttons.defaults().size(150f, 54f).pad(2f);
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, null);
});
if(Core.graphics.isPortrait()){
sel.buttons.row();
}
sel.buttons.button("@mods.github.open", Icon.link, () -> {
Core.app.openURI("https://github.com/" + mod.repo);
});
sel.buttons.button("@mods.browser.view-releases", Icon.zoom, () -> {
BaseDialog load = new BaseDialog("");
load.cont.add("[accent]Fetching Releases...");
load.show();
Http.get(ghApi + "/repos/" + mod.repo + "/releases", res -> {
var json = Jval.read(res.getResultAsString());
JsonArray releases = json.asArray();
Core.app.post(() -> {
load.hide();
if(releases.size == 0){
ui.showInfo("@mods.browser.noreleases");
}else{
sel.hide();
var downloads = new BaseDialog("@mods.browser.releases");
downloads.cont.pane(p -> {
for(int j = 0; j < releases.size; j++){
var release = releases.get(j);
int index = j;
p.table(((TextureRegionDrawable)Tex.whiteui).tint(Pal.darkestGray), t -> {
t.add("[accent]" + release.getString("name") + (index == 0 ? " " + Core.bundle.get("mods.browser.latest") : "")).top().left().growX().wrap().pad(5f);
t.row();
t.add((release.getString("published_at")).substring(0, 10).replaceAll("-", "/")).top().left().growX().wrap().pad(5f).color(Color.gray);
t.row();
t.table(b -> {
b.defaults().size(150f, 54f).pad(2f);
b.button("@mods.github.open-release", Icon.link, () -> Core.app.openURI(release.getString("html_url")));
b.button("@mods.browser.add", Icon.download, () -> {
String releaseUrl = release.getString("url");
githubImportMod(mod.repo, mod.hasJava, releaseUrl.substring(releaseUrl.lastIndexOf("/") + 1));
});
}).right();
}).margin(5f).growX().pad(5f);
if(j < releases.size - 1) p.row();
}
}).width(500f).scrollX(false).fillY();
downloads.buttons.button("@back", Icon.left, () -> {
downloads.clear();
downloads.hide();
sel.show();
}).size(150f, 54f).pad(2f);
downloads.keyDown(KeyCode.escape, downloads::hide);
downloads.keyDown(KeyCode.back, downloads::hide);
downloads.hidden(sel::show);
downloads.show();
}
});
}, t -> Core.app.post(() -> {
modError(t);
load.hide();
}));
});
sel.keyDown(KeyCode.escape, sel::hide);
sel.keyDown(KeyCode.back, sel::hide);
sel.show();
}).width(438f).pad(4).growX().left().height(s + 8*2f).fillY();
if(++i % cols == 0) browserTable.row();
}
});
}
private void handleMod(String repo, HttpResponse result){
try{
Fi file = tmpDirectory.child(repo.replace("/", "") + ".zip");
Streams.copy(result.getResultAsStream(), file.write(false));
mods.importMod(file);
long len = result.getContentLength();
Floatc cons = len <= 0 ? f -> {} : p -> modImportProgress = p;
try(var stream = file.write(false)){
Streams.copyProgress(result.getResultAsStream(), stream, len, 4096, cons);
}
var mod = mods.importMod(file);
mod.setRepo(repo);
file.delete();
Core.app.post(() -> {
try{
setup();
ui.loadfrag.hide();
@@ -402,36 +664,83 @@ public class ModsDialog extends BaseDialog{
}
}
private void githubImportMod(String name){
//try several branches
//TODO use only the main branch as specified in meta
githubImportBranch("6.0", name, e1 -> {
githubImportBranch("master", name, e2 -> {
githubImportBranch("main", name, e3 -> {
ui.showErrorMessage(Core.bundle.format("connectfail", e2));
ui.loadfrag.hide();
});
});
});
private void importFail(Throwable t){
Core.app.post(() -> modError(t));
}
private void githubImportBranch(String branch, String repo, Cons<HttpStatus> err){
Core.net.httpGet("https://api.github.com/repos/" + repo + "/zipball/" + branch, loc -> {
if(loc.getStatus() == HttpStatus.OK){
if(loc.getHeader("Location") != null){
Core.net.httpGet(loc.getHeader("Location"), result -> {
if(result.getStatus() != HttpStatus.OK){
err.get(result.getStatus());
}else{
public void githubImportMod(String repo, boolean isJava){
githubImportMod(repo, isJava, null);
}
public void githubImportMod(String repo, boolean isJava, @Nullable String release){
modImportProgress = 0f;
ui.loadfrag.show("@downloading");
ui.loadfrag.setProgress(() -> modImportProgress);
if(isJava){
githubImportJavaMod(repo, release);
}else{
Http.get(ghApi + "/repos/" + repo, res -> {
var json = Jval.read(res.getResultAsString());
String mainBranch = json.getString("default_branch");
String language = json.getString("language", "<none>");
//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, release);
}else{
githubImportBranch(mainBranch, repo, release);
}
}, this::importFail);
}
}
private void githubImportJavaMod(String repo, @Nullable String release){
//grab latest release
Http.get(ghApi + "/repos/" + repo + "/releases/" + (release == null ? "latest" : release), 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");
Http.get(url, 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.");
}
}, this::importFail);
}
private void githubImportBranch(String branch, String repo, @Nullable String release){
if(release != null) {
Http.get(ghApi + "/repos/" + repo + "/releases/" + release, res -> {
String zipUrl = Jval.read(res.getResultAsString()).getString("zipball_url");
Http.get(zipUrl, loc -> {
if(loc.getHeader("Location") != null){
Http.get(loc.getHeader("Location"), result -> {
handleMod(repo, result);
}
}, t2 -> Core.app.post(() -> modError(t2)));
}, this::importFail);
}else{
handleMod(repo, loc);
}
}, this::importFail);
});
}else{
Http.get(ghApi + "/repos/" + repo + "/zipball/" + branch, loc -> {
if(loc.getHeader("Location") != null){
Http.get(loc.getHeader("Location"), result -> {
handleMod(repo, result);
}, this::importFail);
}else{
handleMod(repo, loc);
}
}else{
err.get(loc.getStatus());
}
}, t2 -> Core.app.post(() -> modError(t2)));
}, this::importFail);
}
}
}

View File

@@ -21,7 +21,7 @@ public class PaletteDialog extends Dialog{
for(int i = 0; i < playerColors.length; i++){
Color color = playerColors[i];
ImageButton button = table.button(Tex.whiteui, Styles.clearTogglei, 34, () -> {
ImageButton button = table.button(Tex.whiteui, Styles.squareTogglei, 34, () -> {
cons.get(color);
hide();
}).size(48).get();

View File

@@ -1,19 +1,40 @@
package mindustry.ui.dialogs;
import arc.*;
import arc.scene.ui.layout.*;
import mindustry.*;
import mindustry.editor.*;
import mindustry.game.*;
import mindustry.gen.*;
import static mindustry.Vars.*;
public class PausedDialog extends BaseDialog{
private MapProcessorsDialog processors = new MapProcessorsDialog();
private SaveDialog save = new SaveDialog();
private LoadDialog load = new LoadDialog();
private boolean wasClient = false;
private CustomRulesDialog rulesDialog = new CustomRulesDialog();
public PausedDialog(){
super("@menu");
shouldPause = true;
clearChildren();
add(titleTable).growX().row();
stack(cont, new Table(t -> {
t.bottom().left();
t.button(Icon.book, () -> {
Rules toEdit = Vars.state.rules.copy();
rulesDialog.show(toEdit, () -> state.rules.copy());
rulesDialog.hidden(() -> {
//apply rule changes only once it is hidden
Vars.state.rules = toEdit;
Call.setRules(toEdit);
});
}).size(70f).tooltip("@customize").visible(() -> state.rules.allowEditRules && (net.server() || !net.active()));
})).grow().row();
shown(this::rebuild);
addCloseListener();
@@ -32,6 +53,12 @@ public class PausedDialog extends BaseDialog{
float dw = 220f;
cont.defaults().width(dw).height(55).pad(5f);
cont.button("@objective", Icon.info, () -> ui.fullText.show("@objective", state.rules.sector.preset.description))
.visible(() -> state.rules.sector != null && state.rules.sector.preset != null && state.rules.sector.preset.description != null).padTop(-60f);
cont.button("@abandon", Icon.cancel, () -> ui.planet.abandonSectorConfirm(state.rules.sector, this::hide)).padTop(-60f)
.disabled(b -> net.client()).visible(() -> state.rules.sector != null).row();
cont.button("@back", Icon.left, this::hide).name("back");
cont.button("@settings", Icon.settings, ui.settings::show).name("settings");
@@ -43,21 +70,26 @@ public class PausedDialog extends BaseDialog{
cont.row();
cont.button("@hostserver", Icon.host, () -> {
//the button runs out of space when the editor button is added, so use the mobile text
cont.button(state.isEditor() ? "@hostserver.mobile" : "@hostserver", Icon.host, () -> {
if(net.server() && steam){
platform.inviteFriends();
}else{
if(steam){
ui.host.runHost();
}else{
ui.host.show();
}
ui.host.show();
}
}).disabled(b -> !((steam && net.server()) || !net.active())).colspan(2).width(dw * 2 + 20f).update(e -> e.setText(net.server() && steam ? "@invitefriends" : "@hostserver"));
}).disabled(b -> !((steam && net.server()) || !net.active())).colspan(state.isEditor() ? 1 : 2).width(state.isEditor() ? dw : dw * 2 + 10f)
.update(e -> e.setText(net.server() && steam ? "@invitefriends" : state.isEditor() ? "@hostserver.mobile" : "@hostserver"));
if(state.isEditor()){
cont.button("@editor.worldprocessors", Icon.logic, () -> {
hide();
processors.show();
});
}
cont.row();
cont.button("@quit", Icon.exit, this::showQuitConfirm).colspan(2).width(dw + 20f).update(s -> s.setText(control.saves.getCurrent() != null && control.saves.getCurrent().isAutosave() ? "@save.quit" : "@quit"));
cont.button("@quit", Icon.exit, this::showQuitConfirm).colspan(2).width(dw + 10f).update(s -> s.setText(control.saves.getCurrent() != null && control.saves.getCurrent().isAutosave() ? "@save.quit" : "@quit"));
}else{
cont.defaults().size(130f).pad(5);
@@ -93,21 +125,41 @@ public class PausedDialog extends BaseDialog{
}
void showQuitConfirm(){
ui.showConfirm("@confirm", "@quit.confirm", () -> {
wasClient = net.client();
if(net.client()) netClient.disconnectQuietly();
Runnable quit = () -> {
runExitSave();
hide();
});
};
if(confirmExit){
ui.showConfirm("@confirm", "@quit.confirm", quit);
}else{
quit.run();
}
}
public boolean checkPlaytest(){
if(state.playtestingMap != null){
//no exit save here
var testing = state.playtestingMap;
logic.reset();
ui.editor.resumeAfterPlaytest(testing);
return true;
}
return false;
}
public void runExitSave(){
boolean wasClient = net.client();
if(net.client()) netClient.disconnectQuietly();
if(state.isEditor() && !wasClient){
ui.editor.resumeEditing();
return;
}else if(checkPlaytest()){
return;
}
if(control.saves.getCurrent() == null || !control.saves.getCurrent().isAutosave() || wasClient){
if(control.saves.getCurrent() == null || !control.saves.getCurrent().isAutosave() || wasClient || state.gameOver || disableSave){
logic.reset();
return;
}

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,7 @@ import arc.scene.actions.*;
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.*;
@@ -30,85 +31,110 @@ import mindustry.ui.layout.TreeLayout.*;
import java.util.*;
import static mindustry.Vars.*;
import static mindustry.gen.Tex.*;
public class ResearchDialog extends BaseDialog{
public static boolean debugShowRequirements = false;
public final float nodeSize = Scl.scl(60f);
public ObjectSet<TechTreeNode> nodes = new ObjectSet<>();
public TechTreeNode root = new TechTreeNode(TechTree.root, null);
public TechTreeNode root = new TechTreeNode(TechTree.roots.first(), null);
public TechNode lastNode = root.node;
public Rect bounds = new Rect();
public ItemsDisplay itemDisplay;
public View view;
public ItemSeq items;
private boolean showTechSelect;
private boolean needsRebuild;
public ResearchDialog(){
super("");
Events.on(ResetEvent.class, e -> {
hide();
});
Events.on(UnlockEvent.class, e -> {
if(net.client() && !needsRebuild){
needsRebuild = true;
Core.app.post(() -> {
needsRebuild = false;
checkNodes(root);
view.hoverNode = null;
treeLayout();
view.rebuild();
Core.scene.act();
});
}
});
titleTable.remove();
titleTable.clear();
titleTable.top();
titleTable.button(b -> {
//TODO custom icon here.
b.imageDraw(() -> root.node.icon()).padRight(8).size(iconMed);
b.add().growX();
b.label(() -> root.node.localizedName()).color(Pal.accent);
b.add().growX();
b.add().size(iconMed);
}, () -> {
new BaseDialog("@techtree.select"){{
cont.pane(t -> {
t.table(Tex.button, in -> {
in.defaults().width(300f).height(60f);
for(TechNode node : TechTree.roots){
if(node.requiresUnlock && !node.content.unlockedHost() && node != getPrefRoot()) continue;
//TODO toggle
in.button(node.localizedName(), node.icon(), Styles.flatTogglet, iconMed, () -> {
if(node == lastNode){
return;
}
rebuildTree(node);
hide();
}).marginLeft(12f).checked(node == lastNode).row();
}
});
});
addCloseButton();
}}.show();
}).visible(() -> showTechSelect = TechTree.roots.count(node -> !(node.requiresUnlock && !node.content.unlockedHost())) > 1).minWidth(300f);
margin(0f).marginBottom(8);
cont.stack(view = new View(), itemDisplay = new ItemsDisplay()).grow();
cont.stack(titleTable, view = new View(), itemDisplay = new ItemsDisplay()).grow();
itemDisplay.visible(() -> !net.client());
titleTable.toFront();
shouldPause = true;
onResize(this::checkMargin);
shown(() -> {
checkMargin();
Core.app.post(this::checkMargin);
items = new ItemSeq(){
//store sector item amounts for modifications
ObjectMap<Sector, ItemSeq> cache = new ObjectMap<>();
Planet currPlanet = ui.planet.isShown() ?
ui.planet.state.planet :
state.isCampaign() ? state.rules.sector.planet : null;
{
//add global counts of each sector
for(Planet planet : content.planets()){
for(Sector sector : planet.sectors){
if(sector.hasSave() && sector.hasBase()){
ItemSeq cached = sector.items();
cache.put(sector, cached);
cached.each((item, amount) -> {
values[item.id] += Math.max(amount, 0);
total += Math.max(amount, 0);
});
}
}
}
}
//this is the only method that actually modifies the sequence itself.
@Override
public void add(Item item, int amount){
//only have custom removal logic for when the sequence gets items taken out of it (e.g. research)
if(amount < 0){
//remove items from each sector's storage, one by one
//negate amount since it's being *removed* - this makes it positive
amount = -amount;
//% that gets removed from each sector
double percentage = (double)amount / get(item);
int[] counter = {amount};
cache.each((sector, seq) -> {
if(counter[0] == 0) return;
//amount that will be removed
int toRemove = Math.min((int)Math.ceil(percentage * seq.get(item)), counter[0]);
//actually remove it from the sector
sector.removeItem(item, toRemove);
seq.remove(item, toRemove);
counter[0] -= toRemove;
});
//negate again to display correct number
amount = -amount;
}
super.add(item, amount);
}
};
if(currPlanet != null && currPlanet.techTree != null){
switchTree(currPlanet.techTree);
}
rebuildItems();
checkNodes(root);
treeLayout();
view.hoverNode = null;
view.infoTable.remove();
view.infoTable.clear();
});
hidden(ui.planet::setup);
@@ -172,6 +198,109 @@ public class ResearchDialog extends BaseDialog{
});
}
void checkMargin(){
if(Core.graphics.isPortrait() && showTechSelect){
itemDisplay.marginTop(60f);
}else{
itemDisplay.marginTop(0f);
}
itemDisplay.invalidate();
itemDisplay.layout();
}
public void rebuildItems(){
items = new ItemSeq(){
//store sector item amounts for modifications
ObjectMap<Sector, ItemSeq> cache = new ObjectMap<>();
{
//first, find a planet associated with the current tech tree
Planet rootPlanet = lastNode.planet != null ? lastNode.planet : content.planets().find(p -> p.techTree == lastNode);
//if there is no root, fall back to serpulo
if(rootPlanet == null) rootPlanet = Planets.serpulo;
//add global counts of each sector
for(Sector sector : rootPlanet.sectors){
if(sector.hasBase()){
ItemSeq cached = sector.items();
cache.put(sector, cached);
cached.each((item, amount) -> {
values[item.id] += Math.max(amount, 0);
total += Math.max(amount, 0);
});
}
}
}
//this is the only method that actually modifies the sequence itself.
@Override
public void add(Item item, int amount){
//only have custom removal logic for when the sequence gets items taken out of it (e.g. research)
if(amount < 0){
//remove items from each sector's storage, one by one
//negate amount since it's being *removed* - this makes it positive
amount = -amount;
//% that gets removed from each sector
double percentage = (double)amount / get(item);
int[] counter = {amount};
cache.each((sector, seq) -> {
if(counter[0] == 0) return;
//amount that will be removed
int toRemove = Math.min((int)Math.ceil(percentage * seq.get(item)), counter[0]);
//actually remove it from the sector
sector.removeItem(item, toRemove);
seq.remove(item, toRemove);
counter[0] -= toRemove;
});
//negate again to display correct number
amount = -amount;
}
super.add(item, amount);
}
};
itemDisplay.rebuild(items);
}
public @Nullable TechNode getPrefRoot(){
Planet currPlanet = ui.planet.isShown() ?
ui.planet.state.planet :
state.isCampaign() ? state.rules.sector.planet : null;
return currPlanet == null ? null : currPlanet.techTree;
}
public void switchTree(TechNode node){
if(lastNode == node || node == null) return;
nodes.clear();
root = new TechTreeNode(node, null);
lastNode = node;
view.rebuildAll();
rebuildItems();
}
public void rebuildTree(TechNode node){
switchTree(node);
view.panX = 0f;
view.panY = -200f;
view.setScale(1f);
view.hoverNode = null;
view.infoTable.remove();
view.infoTable.clear();
checkNodes(root);
treeLayout();
}
void treeLayout(){
float spacing = 20f;
LayoutNode node = new LayoutNode(root, null);
@@ -233,10 +362,10 @@ public class ResearchDialog extends BaseDialog{
void checkNodes(TechTreeNode node){
boolean locked = locked(node.node);
if(!locked) node.visible = true;
if(!locked && (node.parent == null || node.parent.visible)) node.visible = true;
node.selectable = selectable(node.node);
for(TechTreeNode l : node.children){
l.visible = !locked;
l.visible = !locked && l.parent.visible;
checkNodes(l);
}
@@ -244,11 +373,12 @@ public class ResearchDialog extends BaseDialog{
}
boolean selectable(TechNode node){
return node.content.unlocked() || !node.objectives.contains(i -> !i.complete());
//there's a desync here as far as sectors go, since the client doesn't know about that, but I'm not too concerned
return node.content.unlockedHost() || !node.objectives.contains(i -> !i.complete());
}
boolean locked(TechNode node){
return node.content.locked();
return !node.content.unlockedHost();
}
class LayoutNode extends TreeNode<LayoutNode>{
@@ -273,11 +403,9 @@ public class ResearchDialog extends BaseDialog{
this.parent = parent;
this.width = this.height = nodeSize;
nodes.add(this);
if(node.children != null){
children = new TechTreeNode[node.children.size];
for(int i = 0; i < children.length; i++){
children[i] = new TechTreeNode(node.children.get(i), this);
}
children = new TechTreeNode[node.children.size];
for(int i = 0; i < children.length; i++){
children[i] = new TechTreeNode(node.children.get(i), this);
}
}
}
@@ -289,36 +417,48 @@ public class ResearchDialog extends BaseDialog{
public Table infoTable = new Table();
{
rebuildAll();
}
public void rebuildAll(){
clear();
hoverNode = null;
infoTable.clear();
infoTable.touchable = Touchable.enabled;
for(TechTreeNode node : nodes){
ImageButton button = new ImageButton(node.node.content.icon(Cicon.medium), Styles.nodei);
ImageButton button = new ImageButton(node.node.content.uiIcon, Styles.nodei);
button.resizeImage(32f);
button.getImage().setScaling(Scaling.fit);
button.visible(() -> node.visible);
button.clicked(() -> {
if(moved) return;
if(!net.client()){
button.clicked(() -> {
if(moved) return;
if(mobile){
hoverNode = button;
rebuild();
float right = infoTable.getRight();
if(right > Core.graphics.getWidth()){
float moveBy = right - Core.graphics.getWidth();
addAction(new RelativeTemporalAction(){
{
setDuration(0.1f);
setInterpolation(Interp.fade);
}
if(mobile){
hoverNode = button;
rebuild();
float right = infoTable.getRight();
if(right > Core.graphics.getWidth()){
float moveBy = right - Core.graphics.getWidth();
addAction(new RelativeTemporalAction(){
{
setDuration(0.1f);
setInterpolation(Interp.fade);
}
@Override
protected void updateRelative(float percentDelta){
panX -= moveBy * percentDelta;
}
});
@Override
protected void updateRelative(float percentDelta){
panX -= moveBy * percentDelta;
}
});
}
}else if(canSpend(node.node) && locked(node.node)){
spend(node.node);
}
}else if(canSpend(node.node) && locked(node.node)){
spend(node.node);
}
});
});
}
button.hovered(() -> {
if(!mobile && hoverNode != button && node.visible){
hoverNode = button;
@@ -335,20 +475,21 @@ public class ResearchDialog extends BaseDialog{
button.userObject = node.node;
button.setSize(nodeSize);
button.update(() -> {
button.setDisabled(net.client() && !mobile);
float offset = (Core.graphics.getHeight() % 2) / 2f;
button.setPosition(node.x + panX + width / 2f, node.y + panY + height / 2f + offset, Align.center);
button.getStyle().up = !locked(node.node) ? Tex.buttonOver : !selectable(node.node) || !canSpend(node.node) ? Tex.buttonRed : Tex.button;
button.getStyle().up = !locked(node.node) ? Tex.buttonOver : !selectable(node.node) || (!canSpend(node.node) && !net.client()) ? Tex.buttonRed : Tex.button;
((TextureRegionDrawable)button.getStyle().imageUp).setRegion(node.selectable ? node.node.content.icon(Cicon.medium) : Icon.lock.getRegion());
((TextureRegionDrawable)button.getStyle().imageUp).setRegion(node.selectable ? node.node.content.uiIcon : Icon.lock.getRegion());
button.getImage().setColor(!locked(node.node) ? Color.white : node.selectable ? Color.gray : Pal.gray);
button.getImage().setScaling(Scaling.bounded);
button.getImage().layout();
});
addChild(button);
}
if(mobile){
tapped(() -> {
Element e = Core.scene.hit(Core.input.mouseX(), Core.input.mouseY(), true);
Element e = Core.scene.getHoverElement();
if(e == this){
hoverNode = null;
rebuild();
@@ -374,7 +515,7 @@ public class ResearchDialog extends BaseDialog{
}
boolean canSpend(TechNode node){
if(!selectable(node)) return false;
if(!selectable(node) || net.client()) return false;
if(node.requirements.length == 0) return true;
@@ -390,6 +531,8 @@ public class ResearchDialog extends BaseDialog{
}
void spend(TechNode node){
if(net.client()) return;
boolean complete = true;
boolean[] shine = new boolean[node.requirements.length];
@@ -425,6 +568,7 @@ public class ResearchDialog extends BaseDialog{
Core.scene.act();
rebuild(shine);
itemDisplay.rebuild(items, usedShine);
checkMargin();
}
void unlock(TechNode node){
@@ -479,97 +623,107 @@ public class ResearchDialog extends BaseDialog{
infoTable.table(b -> {
b.margin(0).left().defaults().left();
if(selectable && (node.content.description != null || node.content.stats.toMap().size > 0)){
b.button(Icon.info, Styles.cleari, () -> ui.content.show(node.content)).growY().width(50f);
if(selectable){
b.button(Icon.info, Styles.flati, () -> ui.content.show(node.content)).growY().width(50f);
}
b.add().grow();
b.table(desc -> {
desc.left().defaults().left();
desc.add(selectable ? node.content.localizedName : "[accent]???");
desc.row();
if(locked(node)){
if(locked(node) || (debugShowRequirements && !net.client())){
desc.table(t -> {
t.left();
if(selectable){
if(net.client()){
desc.add("@locked").color(Pal.remove);
}else{
desc.table(t -> {
t.left();
if(selectable){
//check if there is any progress, add research progress text
if(Structs.contains(node.finishedRequirements, s -> s.amount > 0)){
float sum = 0f, used = 0f;
boolean shiny = false;
//check if there is any progress, add research progress text
if(Structs.contains(node.finishedRequirements, s -> s.amount > 0)){
float sum = 0f, used = 0f;
boolean shiny = false;
for(int i = 0; i < node.requirements.length; i++){
sum += node.requirements[i].item.cost * node.requirements[i].amount;
used += node.finishedRequirements[i].item.cost * node.finishedRequirements[i].amount;
if(shine != null) shiny |= shine[i];
}
for(int i = 0; i < node.requirements.length; i++){
sum += node.requirements[i].item.cost * node.requirements[i].amount;
used += node.finishedRequirements[i].item.cost * node.finishedRequirements[i].amount;
if(shine != null) shiny |= shine[i];
}
Label label = t.add(Core.bundle.format("research.progress", Math.min((int)(used / sum * 100), 99))).left().get();
if(shiny){
label.setColor(Pal.accent);
label.actions(Actions.color(Color.lightGray, 0.75f, Interp.fade));
}else{
label.setColor(Color.lightGray);
}
t.row();
}
for(int i = 0; i < node.requirements.length; i++){
ItemStack req = node.requirements[i];
ItemStack completed = node.finishedRequirements[i];
//skip finished stacks
if(req.amount <= completed.amount) continue;
boolean shiny = shine != null && shine[i];
t.table(list -> {
int reqAmount = req.amount - completed.amount;
list.left();
list.image(req.item.icon(Cicon.small)).size(8 * 3).padRight(3);
list.add(req.item.localizedName).color(Color.lightGray);
Label label = list.label(() -> " " +
UI.formatAmount(Math.min(items.get(req.item), reqAmount)) + " / "
+ UI.formatAmount(reqAmount)).get();
Color targetColor = items.has(req.item) ? Color.lightGray : Color.scarlet;
Label label = t.add(Core.bundle.format("research.progress", Math.min((int)(used / sum * 100), 99))).left().get();
if(shiny){
label.setColor(Pal.accent);
label.actions(Actions.color(targetColor, 0.75f, Interp.fade));
label.actions(Actions.color(Color.lightGray, 0.75f, Interp.fade));
}else{
label.setColor(targetColor);
label.setColor(Color.lightGray);
}
}).fillX().left();
t.row();
}
for(int i = 0; i < node.requirements.length; i++){
ItemStack req = node.requirements[i];
ItemStack completed = node.finishedRequirements[i];
//skip finished stacks
if(req.amount <= completed.amount && !debugShowRequirements) continue;
boolean shiny = shine != null && shine[i];
t.table(list -> {
int reqAmount = debugShowRequirements ? req.amount : req.amount - completed.amount;
list.left();
list.image(req.item.uiIcon).size(8 * 3).padRight(3);
list.add(req.item.localizedName).color(Color.lightGray);
Label label = list.label(() -> " " +
UI.formatAmount(Math.min(items.get(req.item), reqAmount)) + " / "
+ UI.formatAmount(reqAmount)).get();
Color targetColor = items.has(req.item) ? Color.lightGray : Color.scarlet;
if(shiny){
label.setColor(Pal.accent);
label.actions(Actions.color(targetColor, 0.75f, Interp.fade));
}else{
label.setColor(targetColor);
}
}).fillX().left();
t.row();
}
}else if(node.objectives.size > 0){
t.table(r -> {
r.add("@complete").colspan(2).left();
r.row();
for(Objective o : node.objectives){
if(o.complete()) continue;
r.add("> " + o.display()).color(Color.lightGray).left();
r.image(o.complete() ? Icon.ok : Icon.cancel, o.complete() ? Color.lightGray : Color.scarlet).padLeft(3);
r.row();
}
});
t.row();
}
}else if(node.objectives.size > 0){
t.table(r -> {
r.add("@complete").colspan(2).left();
r.row();
for(Objective o : node.objectives){
if(o.complete()) continue;
r.add("> " + o.display()).color(Color.lightGray).left();
r.image(o.complete() ? Icon.ok : Icon.cancel, o.complete() ? Color.lightGray : Color.scarlet).padLeft(3);
r.row();
}
});
t.row();
}
});
});
}
}else{
desc.add("@completed");
}
}).pad(9);
if(mobile && locked(node)){
if(mobile && locked(node) && !net.client()){
b.row();
b.button("@research", Icon.ok, Styles.nodet, () -> spend(node))
.disabled(i -> !canSpend(node)).growX().height(44f).colspan(3);
b.button("@research", Icon.ok, new TextButtonStyle(){{
disabled = Tex.button;
font = Fonts.def;
fontColor = Color.white;
disabledFontColor = Color.gray;
up = buttonOver;
over = buttonDown;
}}, () -> spend(node)).disabled(i -> !canSpend(node)).growX().height(44f).colspan(3);
}
});
@@ -579,6 +733,10 @@ public class ResearchDialog extends BaseDialog{
}
addChild(infoTable);
checkMargin();
Core.app.post(() -> checkMargin());
infoTable.pack();
infoTable.act(Core.graphics.getDeltaTime());
}

View File

@@ -1,6 +1,7 @@
package mindustry.ui.dialogs;
import arc.*;
import arc.func.*;
import arc.graphics.*;
import arc.graphics.Texture.*;
import arc.graphics.g2d.*;
@@ -11,8 +12,10 @@ import arc.scene.ui.*;
import arc.scene.ui.ImageButton.*;
import arc.scene.ui.TextButton.*;
import arc.scene.ui.layout.*;
import arc.scene.utils.*;
import arc.struct.*;
import arc.util.*;
import mindustry.*;
import mindustry.ctype.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.graphics.*;
@@ -20,28 +23,42 @@ import mindustry.input.*;
import mindustry.type.*;
import mindustry.ui.*;
import java.util.regex.*;
import static mindustry.Vars.*;
public class SchematicsDialog extends BaseDialog{
private static final float tagh = 42f;
private SchematicInfoDialog info = new SchematicInfoDialog();
private Schematic firstSchematic;
private String search = "";
private TextField searchField;
private Runnable rebuildPane = () -> {}, rebuildTags = () -> {};
private Pattern ignoreSymbols = Pattern.compile("[`~!@#$%^&*()\\-_=+{}|;:'\",<.>/?]");
private Seq<String> tags, selectedTags = new Seq<>();
private boolean checkedTags;
public SchematicsDialog(){
super("@schematics");
Core.assets.load("sprites/schematic-background.png", Texture.class).loaded = t -> ((Texture)t).setWrap(TextureWrap.repeat);
Core.assets.load("sprites/schematic-background.png", Texture.class).loaded = t -> t.setWrap(TextureWrap.repeat);
tags = Core.settings.getJson("schematic-tags", Seq.class, String.class, Seq::new);
shouldPause = true;
addCloseButton();
buttons.button("@schematic.import", Icon.download, this::showImport);
makeButtonOverlay();
shown(this::setup);
onResize(this::setup);
}
void setup(){
if(!checkedTags){
checkTags();
checkedTags = true;
}
search = "";
Runnable[] rebuildPane = {null};
cont.top();
cont.clear();
@@ -51,19 +68,56 @@ public class SchematicsDialog extends BaseDialog{
s.image(Icon.zoom);
searchField = s.field(search, res -> {
search = res;
rebuildPane[0].run();
rebuildPane.run();
}).growX().get();
searchField.setMessageText("@schematic.search");
searchField.clicked(KeyCode.mouseRight, () -> {
if(!search.isEmpty()){
search = "";
searchField.clearText();
rebuildPane.run();
}
});
}).fillX().padBottom(4);
cont.row();
cont.table(in -> {
in.left();
in.add("@schematic.tags").padRight(4);
//tags (no scroll pane visible)
in.pane(Styles.noBarPane, t -> {
rebuildTags = () -> {
t.clearChildren();
t.left();
t.defaults().pad(2).height(tagh);
for(var tag : tags){
t.button(tag, Styles.togglet, () -> {
if(selectedTags.contains(tag)){
selectedTags.remove(tag);
}else{
selectedTags.add(tag);
}
rebuildPane.run();
}).checked(selectedTags.contains(tag)).with(c -> c.getLabel().setWrap(false));
}
};
rebuildTags.run();
}).fillX().height(tagh).scrollY(false);
in.button(Icon.pencilSmall, this::showAllTags).size(tagh).pad(2).tooltip("@schematic.edittags");
}).height(tagh).fillX();
cont.row();
cont.pane(t -> {
t.top();
t.margin(20f);
t.update(() -> {
if(Core.input.keyTap(Binding.chat) && Core.scene.getKeyboardFocus() == searchField && firstSchematic != null){
if(!Vars.state.rules.schematicsAllowed){
if(!state.rules.schematicsAllowed){
ui.showInfo("@schematic.disabled");
}else{
control.input.useSchematic(firstSchematic);
@@ -72,18 +126,20 @@ public class SchematicsDialog extends BaseDialog{
}
});
rebuildPane[0] = () -> {
rebuildPane = () -> {
int cols = Math.max((int)(Core.graphics.getWidth() / Scl.scl(230)), 1);
t.clear();
int i = 0;
String regex = "[`~!@#$%^&*()-_=+{}|;:'\",<.>/?]";
String searchString = search.toLowerCase().replaceAll(regex, " ");
String searchString = ignoreSymbols.matcher(search.toLowerCase()).replaceAll("");
firstSchematic = null;
for(Schematic s : schematics.all()){
if(!search.isEmpty() && !s.name().toLowerCase().replaceAll(regex, " ").contains(searchString)) continue;
//make sure *tags* fit
if(selectedTags.any() && !s.labels.containsAll(selectedTags)) continue;
//make sure search fits
if(!search.isEmpty() && !ignoreSymbols.matcher(s.name().toLowerCase()).replaceAll("").contains(searchString)) continue;
if(firstSchematic == null) firstSchematic = s;
Button[] sel = {null};
@@ -94,64 +150,26 @@ public class SchematicsDialog extends BaseDialog{
buttons.left();
buttons.defaults().size(50f);
ImageButtonStyle style = Styles.clearPartiali;
ImageButtonStyle style = Styles.emptyi;
buttons.button(Icon.info, style, () -> {
showInfo(s);
});
buttons.button(Icon.upload, style, () -> {
showExport(s);
});
buttons.button(Icon.pencil, style, () -> {
new Dialog("@schematic.rename"){{
cont.margin(30).add("@name").padRight(6f);
TextField nameField = cont.field(s.name(), null).size(400f, 55f).addInputDialog().get();
cont.row();
cont.margin(30).add("@editor.description").padRight(6f);
TextField descripionField = cont.area(s.description(), Styles.areaField, t -> {}).size(400f, 140f).addInputDialog().get();
Runnable accept = () -> {
s.tags.put("name", nameField.getText());
s.tags.put("description", descripionField.getText());
s.save();
hide();
rebuildPane[0].run();
};
buttons.defaults().size(120, 54).pad(4);
buttons.button("@ok", accept).disabled(b -> nameField.getText().isEmpty());
buttons.button("@cancel", this::hide);
keyDown(KeyCode.enter, () -> {
if(!nameField.getText().isEmpty() && Core.scene.getKeyboardFocus() != descripionField){
accept.run();
}
});
keyDown(KeyCode.escape, this::hide);
keyDown(KeyCode.back, this::hide);
show();
}};
});
buttons.button(Icon.info, style, () -> showInfo(s)).tooltip("@info.title");
buttons.button(Icon.upload, style, () -> showExport(s)).tooltip("@editor.export");
buttons.button(Icon.pencil, style, () -> showEdit(s)).tooltip("@schematic.edit");
if(s.hasSteamID()){
buttons.button(Icon.link, style, () -> platform.viewListing(s));
buttons.button(Icon.link, style, () -> platform.viewListing(s)).tooltip("@view.workshop");
}else{
buttons.button(Icon.trash, style, () -> {
if(s.mod != null){
ui.showInfo(Core.bundle.format("mod.item.remove", s.mod.meta.displayName()));
ui.showInfo(Core.bundle.format("mod.item.remove", s.mod.meta.displayName));
}else{
ui.showConfirm("@confirm", "@schematic.delete.confirm", () -> {
schematics.remove(s);
rebuildPane[0].run();
rebuildPane.run();
});
}
});
}).tooltip("@save.delete");
}
}).growX().height(50f);
b.row();
b.stack(new SchematicImage(s).setScaling(Scaling.fit), new Table(n -> {
@@ -167,14 +185,14 @@ public class SchematicsDialog extends BaseDialog{
if(state.isMenu()){
showInfo(s);
}else{
if(!Vars.state.rules.schematicsAllowed){
if(!state.rules.schematicsAllowed){
ui.showInfo("@schematic.disabled");
}else{
control.input.useSchematic(s);
hide();
}
}
}).pad(4).style(Styles.cleari).get();
}).pad(4).style(Styles.flati).get();
sel[0].getStyle().up = Tex.pane;
@@ -184,12 +202,16 @@ public class SchematicsDialog extends BaseDialog{
}
if(firstSchematic == null){
t.add("@none");
if(!searchString.isEmpty() || selectedTags.any()){
t.add("@none.found");
}else{
t.add("@none").color(Color.lightGray);
}
}
};
rebuildPane[0].run();
}).get().setScrollingDisabled(true, false);
rebuildPane.run();
}).grow().scrollX(false);
}
public void showInfo(Schematic schematic){
@@ -197,11 +219,11 @@ public class SchematicsDialog extends BaseDialog{
}
public void showImport(){
BaseDialog dialog = new BaseDialog("@editor.export");
BaseDialog dialog = new BaseDialog("@editor.import");
dialog.cont.pane(p -> {
p.margin(10f);
p.table(Tex.button, t -> {
TextButtonStyle style = Styles.cleart;
TextButtonStyle style = Styles.flatt;
t.defaults().size(280f, 60f).left();
t.row();
t.button("@schematic.copy.import", Icon.copy, style, () -> {
@@ -212,6 +234,7 @@ public class SchematicsDialog extends BaseDialog{
schematics.add(s);
setup();
ui.showInfoFade("@schematic.saved");
checkTags(s);
showInfo(s);
}catch(Throwable e){
ui.showException(e);
@@ -227,6 +250,7 @@ public class SchematicsDialog extends BaseDialog{
schematics.add(s);
setup();
showInfo(s);
checkTags(s);
}catch(Exception e){
ui.showException(e);
}
@@ -248,9 +272,9 @@ public class SchematicsDialog extends BaseDialog{
public void showExport(Schematic s){
BaseDialog dialog = new BaseDialog("@editor.export");
dialog.cont.pane(p -> {
p.margin(10f);
p.table(Tex.button, t -> {
TextButtonStyle style = Styles.cleart;
p.margin(10f);
p.table(Tex.button, t -> {
TextButtonStyle style = Styles.flatt;
t.defaults().size(280f, 60f).left();
if(steam && !s.hasSteamID()){
t.button("@schematic.shareworkshop", Icon.book, style,
@@ -275,18 +299,384 @@ public class SchematicsDialog extends BaseDialog{
dialog.show();
}
public void focusSearchField(){
if(searchField == null) return;
public void showEdit(Schematic s){
new BaseDialog("@schematic.edit"){{
setFillParent(true);
addCloseListener();
Core.scene.setKeyboardFocus(searchField);
cont.margin(30);
cont.add("@schematic.tags").padRight(6f);
cont.table(tags -> buildTags(s, tags, false)).maxWidth(400f).fillX().left().row();
cont.margin(30).add("@name").padRight(6f);
TextField nameField = cont.field(s.name(), null).size(400f, 55f).left().get();
cont.row();
cont.margin(30).add("@editor.description").padRight(6f);
TextField descField = cont.area(s.description(), Styles.areaField, t -> {}).size(400f, 140f).left().get();
Runnable accept = () -> {
s.tags.put("name", nameField.getText());
s.tags.put("description", descField.getText());
s.save();
hide();
rebuildPane.run();
};
buttons.defaults().size(210f, 64f).pad(4);
buttons.button("@ok", Icon.ok, accept).disabled(b -> nameField.getText().isEmpty());
buttons.button("@cancel", Icon.cancel, this::hide);
keyDown(KeyCode.enter, () -> {
if(!nameField.getText().isEmpty() && Core.scene.getKeyboardFocus() != descField){
accept.run();
}
});
}}.show();
}
//adds all new tags to the global list of tags
//alternatively, unknown tags could be discarded on import?
void checkTags(){
ObjectSet<String> encountered = new ObjectSet<>();
encountered.addAll(tags);
for(Schematic s : schematics.all()){
for(var tag : s.labels){
if(encountered.add(tag)){
tags.add(tag);
}
}
}
}
//adds any new tags found to the global tag list
//TODO remove tags from it instead?
void checkTags(Schematic s){
boolean any = false;
for(var tag : s.labels){
if(!tags.contains(tag)){
tags.add(tag);
any = true;
}
}
if(any){
rebuildTags.run();
}
}
void tagsChanged(){
rebuildTags.run();
if(selectedTags.any()){
rebuildPane.run();
}
Core.settings.putJson("schematic-tags", String.class, tags);
}
void addTag(Schematic s, String tag){
s.labels.add(tag);
s.save();
tagsChanged();
}
void removeTag(Schematic s, String tag){
s.labels.remove(tag);
s.save();
tagsChanged();
}
//shows a dialog for creating a new tag
void showNewTag(Cons<String> result){
ui.showTextInput("@schematic.addtag", "", "", out -> {
if(tags.contains(out)){
ui.showInfo("@schematic.tagexists");
}else{
tags.add(out);
tagsChanged();
result.get(out);
}
});
}
void showNewIconTag(Cons<String> cons){
new Dialog(){{
closeOnBack();
setFillParent(true);
//TODO: use IconSelectDialog
cont.pane(t -> {
resized(true, () -> {
t.clearChildren();
t.marginRight(19f).marginLeft(12f);
t.defaults().size(48f);
int cols = (int)Math.min(20, Core.graphics.getWidth() / Scl.scl(52f));
int i = 0;
for(String icon : accessibleIcons){
String out = (char)Iconc.codes.get(icon) + "";
if(tags.contains(out)) continue;
t.button(Icon.icons.get(icon), Styles.flati, iconMed, () -> {
tags.add(out);
tagsChanged();
cons.get(out);
hide();
});
if(++i % cols == 0) t.row();
}
for(ContentType ctype : defaultContentIcons){
var all = content.getBy(ctype).<UnlockableContent>as().select(u -> !u.isHidden() && u.unlockedNow() && u.hasEmoji());
t.row();
if(all.count(u -> !tags.contains(u.emoji())) > 0) t.image().colspan(cols).growX().width(Float.NEGATIVE_INFINITY).height(3f).color(Pal.accent);
t.row();
i = 0;
for(UnlockableContent u : all){
if(tags.contains(u.emoji())) continue;
t.button(new TextureRegionDrawable(u.uiIcon), Styles.flati, iconMed, () -> {
String out = u.emoji() + "";
tags.add(out);
tagsChanged();
cons.get(out);
hide();
}).tooltip(u.localizedName);
if(++i % cols == 0) t.row();
}
}
});
}).scrollX(false);
buttons.button("@back", Icon.left, this::hide).size(210f, 64f);
}}.show();
}
void showAllTags(){
var dialog = new BaseDialog("@schematic.edittags");
dialog.addCloseButton();
Runnable[] rebuild = {null};
dialog.cont.pane(p -> {
rebuild[0] = () -> {
p.clearChildren();
p.margin(12f).defaults().fillX().left();
float sum = 0f;
Table current = new Table().left();
for(var tag : tags){
float si = 40f;
var next = new Table(Tex.whiteui, n -> {
n.setColor(Pal.gray);
n.margin(5f);
n.table(move -> {
//move up
move.button(Icon.upOpen, Styles.emptyi, () -> {
int idx = tags.indexOf(tag);
if(idx > 0){
tags.swap(idx, idx - 1);
tagsChanged();
rebuild[0].run();
}
}).size(si).tooltip("@editor.moveup").row();
//move down
move.button(Icon.downOpen, Styles.emptyi, () -> {
int idx = tags.indexOf(tag);
if(idx < tags.size - 1){
tags.swap(idx, idx + 1);
tagsChanged();
rebuild[0].run();
}
}).size(si).tooltip("@editor.movedown");
}).fillY();
n.table(t -> {
t.add(tag).left().row();
t.add(Core.bundle.format("schematic.tagged", schematics.all().count(s -> s.labels.contains(tag)))).left()
.update(b -> b.setColor(b.hasMouse() ? Pal.accent : Color.lightGray)).get().clicked(() -> {
dialog.hide();
selectedTags.clear().add(tag);
rebuildTags.run();
rebuildPane.run();
});
}).growX().fillY();
n.table(b -> {
b.margin(2);
//rename tag
b.button(Icon.pencil, Styles.emptyi, () -> {
ui.showTextInput("@schematic.renametag", "@name", tag, result -> {
//same tag, nothing was renamed
if(result.equals(tag)) return;
if(tags.contains(result)){
ui.showInfo("@schematic.tagexists");
}else{
for(Schematic s : schematics.all()){
if(s.labels.any()){
s.labels.replace(tag, result);
s.save();
}
}
selectedTags.replace(tag, result);
tags.replace(tag, result);
tagsChanged();
rebuild[0].run();
}
});
}).size(si).tooltip("@schematic.renametag").row();
//delete tag
b.button(Icon.trash, Styles.emptyi, () -> {
ui.showConfirm("@schematic.tagdelconfirm", () -> {
for(Schematic s : schematics.all()){
if(s.labels.any()){
s.labels.remove(tag);
s.save();
}
}
selectedTags.remove(tag);
tags.remove(tag);
tagsChanged();
rebuildPane.run();
rebuild[0].run();
});
}).size(si).tooltip("@save.delete");
}).fillY();
});
next.pack();
float w = next.getWidth() + Scl.scl(9f);
if(w + sum >= Core.graphics.getWidth() * 0.9f){
p.add(current).row();
current = new Table();
current.left();
sum = 0;
}
current.add(next).minWidth(210).pad(4);
sum += w;
}
if(sum > 0){
p.add(current).row();
}
p.table(t -> {
t.left().defaults().fillX().height(tagh).pad(2);
t.button("@schematic.texttag", Icon.add, () -> showNewTag(res -> rebuild[0].run())).wrapLabel(false).get().getLabelCell().padLeft(5);
t.button("@schematic.icontag", Icon.add, () -> showNewIconTag(res -> rebuild[0].run())).wrapLabel(false).get().getLabelCell().padLeft(5);
});
};
resized(true, rebuild[0]);
}).scrollX(false);
dialog.show();
}
void buildTags(Schematic schem, Table t){
buildTags(schem, t, true);
}
void buildTags(Schematic schem, Table t, boolean name){
t.clearChildren();
t.left();
//sort by order in the main target array. the complexity of this is probably awful
schem.labels.sort(s -> tags.indexOf(s));
if(name) t.add("@schematic.tags").padRight(4);
t.pane(s -> {
s.left();
s.defaults().pad(3).height(tagh);
for(var tag : schem.labels){
s.table(Tex.button, i -> {
i.add(tag).padRight(4).height(tagh).labelAlign(Align.center);
i.button(Icon.cancelSmall, Styles.emptyi, () -> {
removeTag(schem, tag);
buildTags(schem, t, name);
}).size(tagh).padRight(-9f).padLeft(-9f);
});
}
}).fillX().left().height(tagh).scrollY(false);
t.button(Icon.addSmall, () -> {
var dialog = new BaseDialog("@schematic.addtag");
dialog.addCloseButton();
dialog.cont.pane(p -> resized(true, () -> {
p.clearChildren();
p.defaults().fillX().left();
float sum = 0f;
Table current = new Table().left();
for(var tag : tags){
if(schem.labels.contains(tag)) continue;
var next = Elem.newButton(tag, () -> {
addTag(schem, tag);
buildTags(schem, t, name);
dialog.hide();
});
next.getLabel().setWrap(false);
next.pack();
float w = next.getPrefWidth() + Scl.scl(6f);
if(w + sum >= Core.graphics.getWidth() * (Core.graphics.isPortrait() ? 1f : 0.8f)){
p.add(current).row();
current = new Table();
current.left();
current.add(next).height(tagh).pad(2);
sum = 0;
}else{
current.add(next).height(tagh).pad(2);
}
sum += w;
}
if(sum > 0){
p.add(current).row();
}
Cons<String> handleTag = res -> {
dialog.hide();
addTag(schem, res);
buildTags(schem, t, name);
};
p.row();
p.table(v -> {
v.left().defaults().fillX().height(tagh).pad(2);
v.button("@schematic.texttag", Icon.add, () -> showNewTag(handleTag)).wrapLabel(false).get().getLabelCell().padLeft(4);
v.button("@schematic.icontag", Icon.add, () -> showNewIconTag(handleTag)).wrapLabel(false).get().getLabelCell().padLeft(4);
});
}));
dialog.show();
}).size(tagh).tooltip("@schematic.addtag");
}
@Override
public Dialog show(){
super.show();
if(Core.app.isDesktop()){
focusSearchField();
if(Core.app.isDesktop() && searchField != null){
Core.scene.setKeyboardFocus(searchField);
}
return this;
@@ -298,6 +688,7 @@ public class SchematicsDialog extends BaseDialog{
public Color borderColor = Pal.gray;
private Schematic schematic;
private Texture lastTexture;
boolean set;
public SchematicImage(Schematic s){
@@ -320,6 +711,8 @@ public class SchematicsDialog extends BaseDialog{
if(!set){
Core.app.post(this::setPreview);
set = true;
}else if(lastTexture != null && lastTexture.isDisposed()){
set = wasSet = false;
}
Texture background = Core.assets.get("sprites/schematic-background.png", Texture.class);
@@ -346,37 +739,36 @@ public class SchematicsDialog extends BaseDialog{
}
private void setPreview(){
TextureRegionDrawable draw = new TextureRegionDrawable(new TextureRegion(schematics.getPreview(schematic)));
TextureRegionDrawable draw = new TextureRegionDrawable(new TextureRegion(lastTexture = schematics.getPreview(schematic)));
setDrawable(draw);
setScaling(Scaling.fit);
}
}
public static class SchematicInfoDialog extends BaseDialog{
public class SchematicInfoDialog extends BaseDialog{
SchematicInfoDialog(){
super("");
setFillParent(true);
addCloseButton();
addCloseListener();
}
public void show(Schematic schem){
cont.clear();
title.setText("[[" + Core.bundle.get("schematic") + "] " +schem.name());
cont.add(Core.bundle.format("schematic.info", schem.width, schem.height, schem.tiles.size)).color(Color.lightGray);
cont.row();
cont.add(new SchematicImage(schem)).maxSize(800f);
cont.row();
cont.add(Core.bundle.format("schematic.info", schem.width, schem.height, schem.tiles.size)).color(Color.lightGray).row();
cont.table(tags -> buildTags(schem, tags)).fillX().left().row();
cont.add(new SchematicImage(schem)).maxSize(800f).row();
ItemSeq arr = schem.requirements();
cont.table(r -> {
int i = 0;
for(ItemStack s : arr){
r.image(s.item.icon(Cicon.small)).left();
r.image(s.item.uiIcon).left().size(iconMed);
r.label(() -> {
Building core = player.core();
if(core == null || state.rules.infiniteResources || core.items.has(s.item, s.amount)) return "[lightgray]" + s.amount + "";
if(core == null || state.isMenu() || state.rules.infiniteResources || core.items.has(s.item, s.amount)) return "[lightgray]" + s.amount + "";
return (core.items.has(s.item, s.amount) ? "[lightgray]" : "[scarlet]") + Math.min(core.items.get(s.item), s.amount) + "[lightgray]/" + s.amount;
}).padLeft(2).left().padRight(4);
@@ -406,7 +798,13 @@ public class SchematicsDialog extends BaseDialog{
});
}
buttons.clearChildren();
buttons.defaults().size(Core.graphics.isPortrait() ? 150f : 210f, 64f);
buttons.button("@back", Icon.left, this::hide);
buttons.button("@editor.export", Icon.upload, () -> showExport(schem));
buttons.button("@edit", Icon.edit, () -> showEdit(schem));
show();
}
}
}
}

View File

@@ -2,11 +2,13 @@ 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.*;
@@ -15,7 +17,6 @@ import arc.util.*;
import arc.util.io.*;
import mindustry.content.*;
import mindustry.content.TechTree.*;
import mindustry.core.GameState.*;
import mindustry.core.*;
import mindustry.ctype.*;
import mindustry.game.EventType.*;
@@ -28,42 +29,37 @@ import java.io.*;
import java.util.zip.*;
import static arc.Core.*;
import static mindustry.Vars.net;
import static mindustry.Vars.*;
public class SettingsMenuDialog extends SettingsDialog{
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 boolean wasPaused;
private Seq<SettingsCategory> categories = new Seq<>();
public SettingsMenuDialog(){
hidden(() -> {
Sounds.back.play();
if(state.isGame()){
if(!wasPaused || net.active())
state.set(State.playing);
}
});
super(bundle.get("settings", "Settings"));
addCloseButton();
cont.add(main = new SettingsTable());
shouldPause = true;
shown(() -> {
back();
if(state.isGame()){
wasPaused = state.is(State.paused);
state.set(State.paused);
}
rebuildMenu();
});
setFillParent(true);
title.setAlignment(Align.center);
titleTable.row();
titleTable.add(new Image()).growX().height(3f).pad(4f).get().setColor(Pal.accent);
onResize(() -> {
graphics.rebuild();
sound.rebuild();
game.rebuild();
updateScrollFocus();
});
cont.clearChildren();
cont.remove();
@@ -89,7 +85,7 @@ public class SettingsMenuDialog extends SettingsDialog{
dataDialog.cont.table(Tex.button, t -> {
t.defaults().size(280f, 60f).left();
TextButtonStyle style = Styles.cleart;
TextButtonStyle style = Styles.flatt;
t.button("@settings.cleardata", Icon.trash, style, () -> ui.showConfirm("@confirm", "@settings.clearall.confirm", () -> {
ObjectMap<String, Object> map = new ObjectMap<>();
@@ -146,7 +142,7 @@ public class SettingsMenuDialog extends SettingsDialog{
}
}
}
for(var slot : control.saves.getSaveSlots().copy()){
if(slot.isSector()){
slot.delete();
@@ -184,6 +180,8 @@ public class SettingsMenuDialog extends SettingsDialog{
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");
@@ -214,37 +212,20 @@ public class SettingsMenuDialog extends SettingsDialog{
platform.shareFile(logs);
}else{
platform.showFileChooser(false, "txt", file -> {
file.writeString(getLogs());
app.post(() -> ui.showInfo("@crash.exported"));
try{
file.writeBytes(getLogs().getBytes(Strings.utf8));
app.post(() -> ui.showInfo("@crash.exported"));
}catch(Throwable e){
ui.showException(e);
}
});
}
}
}).marginLeft(4);
});
ScrollPane pane = new ScrollPane(prefs);
pane.addCaptureListener(new InputListener(){
@Override
public boolean touchDown(InputEvent event, float x, float y, int pointer, KeyCode button){
Element actor = pane.hit(x, y, true);
if(actor instanceof Slider){
pane.setFlickScroll(false);
return true;
}
return super.touchDown(event, x, y, pointer, button);
}
@Override
public void touchUp(InputEvent event, float x, float y, int pointer, KeyCode button){
pane.setFlickScroll(true);
super.touchUp(event, x, y, pointer, button);
}
});
pane.setFadeScrollBars(false);
row();
add(pane).grow().top();
pane(prefs).grow().top();
row();
add(buttons).fillX();
@@ -266,39 +247,76 @@ public class SettingsMenuDialog extends SettingsDialog{
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.cleart;
TextButtonStyle style = Styles.flatt;
float marg = 8f, isize = iconMed;
menu.defaults().size(300f, 60f);
menu.button("@settings.game", style, () -> visible(0));
menu.row();
menu.button("@settings.graphics", style, () -> visible(1));
menu.row();
menu.button("@settings.sound", style, () -> visible(2));
menu.row();
menu.button("@settings.language", style, ui.language::show);
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.row();
menu.button("@settings.controls", style, ui.controls::show);
menu.button("@settings.controls", Icon.move, style, isize, ui.controls::show).marginLeft(marg).row();
}
menu.row();
menu.button("@settings.data", style, () -> dataDialog.show());
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", bundle.get("setting.musicvol.name", "Music Volume"), 100, 0, 100, 1, i -> i + "%");
sound.sliderPref("sfxvol", bundle.get("setting.sfxvol.name", "SFX Volume"), 100, 0, 100, 1, i -> i + "%");
sound.sliderPref("ambientvol", bundle.get("setting.ambientvol.name", "Ambient Volume"), 100, 0, 100, 1, i -> i + "%");
sound.checkPref("alwaysmusic", false);
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));
game.screenshakePref();
if(mobile){
game.checkPref("autotarget", true);
game.checkPref("keyboard", false, val -> control.setInput(val ? new DesktopInput() : new MobileInput()));
if(Core.settings.getBool("keyboard")){
control.setInput(new DesktopInput());
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:
@@ -310,20 +328,35 @@ public class SettingsMenuDialog extends SettingsDialog{
control.setInput(new MobileInput());
}
}*/
game.sliderPref("saveinterval", 60, 10, 5 * 120, 10, i -> Core.bundle.format("setting.seconds", i));
if(!mobile){
game.checkPref("crashreport", true);
}
game.checkPref("communityservers", true, val -> {
defaultServers.clear();
if(val){
JoinDialog.fetchServers();
}
});
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("distinctcontrolgroups", true);
}
game.checkPref("doubletapmine", false);
game.checkPref("commandmodehold", true);
if(!ios){
game.checkPref("modcrashdisable", true);
}
if(steam){
@@ -333,19 +366,30 @@ public class SettingsMenuDialog extends SettingsDialog{
});
if(!Version.modifier.contains("beta")){
game.checkPref("publichost", false, i -> {
game.checkPref("steampublichost", false, i -> {
platform.updateLobby();
});
}
}
graphics.sliderPref("uiscale", 100, 25, 300, 25, s -> {
if(ui.settings != null){
Core.settings.put("uiscalechanged", true);
}
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("fpscap", 240, 15, 245, 5, s -> (s > 240 ? Core.bundle.get("setting.fpscap.none") : Core.bundle.format("setting.fpscap.text", 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){
@@ -353,27 +397,42 @@ public class SettingsMenuDialog extends SettingsDialog{
}
return s + "%";
});
graphics.sliderPref("unitlaseropacity", 100, 0, 100, 5, s -> 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.setFullscreenMode(Core.graphics.getDisplayMode());
Core.graphics.setFullscreen();
}else{
Core.graphics.setWindowedMode(Core.graphics.getWidth(), Core.graphics.getHeight());
}
});
graphics.checkPref("borderlesswindow", false, b -> Core.graphics.setUndecorated(b));
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.setFullscreenMode(Core.graphics.getDisplayMode()));
Core.app.post(() -> Core.graphics.setFullscreen());
}
if(Core.settings.getBool("borderlesswindow")){
Core.app.post(() -> Core.graphics.setUndecorated(true));
Core.app.post(() -> Core.graphics.setBorderless(true));
}
}else if(!ios){
graphics.checkPref("landscape", false, b -> {
@@ -391,6 +450,7 @@ public class SettingsMenuDialog extends SettingsDialog{
graphics.checkPref("effects", true);
graphics.checkPref("atmosphere", !mobile);
graphics.checkPref("drawlight", true);
graphics.checkPref("destroyedblocks", true);
graphics.checkPref("blockstatus", false);
graphics.checkPref("playerchat", true);
@@ -400,20 +460,20 @@ public class SettingsMenuDialog extends SettingsDialog{
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);
}
if(!ios){
graphics.checkPref("bloom", true, val -> renderer.toggleBloom(val));
}else{
Core.settings.put("bloom", false);
}
graphics.checkPref("bloom", true, val -> renderer.toggleBloom(val));
graphics.checkPref("pixelate", false, val -> {
if(val){
@@ -421,12 +481,17 @@ public class SettingsMenuDialog extends SettingsDialog{
}
});
graphics.checkPref("linear", !mobile, b -> {
for(Texture tex : Core.atlas.getTextures()){
TextureFilter filter = b ? TextureFilter.linear : TextureFilter.nearest;
tex.setFilter(filter, filter);
}
});
//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()){
@@ -435,11 +500,16 @@ public class SettingsMenuDialog extends SettingsDialog{
}
}
graphics.checkPref("skipcoreanimation", false);
graphics.checkPref("hidedisplays", false);
if(OS.isMac){
graphics.checkPref("macnotch", false);
}
if(!mobile){
Core.settings.put("swapdiagonal", false);
}
graphics.checkPref("flow", true);
}
public void exportData(Fi file) throws IOException{
@@ -447,16 +517,30 @@ public class SettingsMenuDialog extends SettingsDialog{
files.add(Core.settings.getSettingsFile());
files.addAll(customMapDirectory.list());
files.addAll(saveDirectory.list());
files.addAll(screenshotDirectory.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){
if(add.isDirectory()) continue;
zos.putNextEntry(new ZipEntry(add.path().substring(base.length())));
Streams.copy(add.read(), zos);
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();
}
}
@@ -495,18 +579,25 @@ public class SettingsMenuDialog extends SettingsDialog{
private void visible(int index){
prefs.clearChildren();
prefs.add(new Table[]{game, graphics, sound}[index]);
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.leftOpen, () -> {
buttons.button("@back", Icon.left, () -> {
if(prefs.getChildren().first() != menu){
back();
}else{
hide();
}
}).size(230f, 64f);
}).size(210f, 64f);
keyDown(key -> {
if(key == KeyCode.escape || key == KeyCode.back){
@@ -518,4 +609,250 @@ public class SettingsMenuDialog extends SettingsDialog{
}
});
}
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();
}
}
}
}

View File

@@ -4,6 +4,9 @@ import arc.*;
import arc.scene.ui.layout.*;
import mindustry.gen.*;
import mindustry.net.Administration.*;
import mindustry.ui.*;
import static mindustry.Vars.*;
public class TraceDialog extends BaseDialog{
@@ -11,7 +14,6 @@ public class TraceDialog extends BaseDialog{
super("@trace");
addCloseButton();
setFillParent(false);
}
public void show(Player player, TraceInfo info){
@@ -22,16 +24,38 @@ public class TraceDialog extends BaseDialog{
table.defaults().pad(1);
table.defaults().left();
table.add(Core.bundle.format("trace.playername", player.name));
table.row();
table.add(Core.bundle.format("trace.ip", info.ip));
table.row();
table.add(Core.bundle.format("trace.id", info.uuid));
table.row();
table.add(Core.bundle.format("trace.modclient", info.modded));
table.row();
table.add(Core.bundle.format("trace.mobile", info.mobile));
table.row();
var style = Styles.emptyi;
float s = 28f;
table.table(c -> {
c.left().defaults().left();
c.button(Icon.copySmall, style, () -> copy(player.name)).size(s).padRight(4f);
c.add(Core.bundle.format("trace.playername", player.name)).row();
c.button(Icon.copySmall, style, () -> copy(info.ip)).size(s).padRight(4f);
c.add(Core.bundle.format("trace.ip", info.ip)).row();
c.button(Icon.copySmall, style, () -> copy(info.locale)).size(s).padRight(4f);
c.add(Core.bundle.format("trace.language", info.locale)).row();
c.button(Icon.copySmall, style, () -> copy(info.uuid)).size(s).padRight(4f);
c.add(Core.bundle.format("trace.id", info.uuid)).row();
}).row();
table.add(Core.bundle.format("trace.modclient", info.modded)).row();
table.add(Core.bundle.format("trace.mobile", info.mobile)).row();
table.add(Core.bundle.format("trace.times.joined", info.timesJoined)).row();
table.add(Core.bundle.format("trace.times.kicked", info.timesKicked)).row();
for(int i = 0; i < 2; i++){
table.add(i == 0 ? "@trace.ips" : "@trace.names").row();
String[] list = i == 0 ? info.ips : info.names;
table.pane(t -> {
t.left();
for(String val : list){
t.add("[lightgray]" + val).left().row();
}
}).padLeft(20f).fill().left().row();
}
table.add().pad(5);
table.row();
@@ -40,4 +64,9 @@ public class TraceDialog extends BaseDialog{
show();
}
private void copy(String content){
Core.app.setClipboardText(content);
ui.showInfoFade("@copied");
}
}