Merge remote-tracking branch 'upstream/master'

This commit is contained in:
ApsZoldat
2023-12-04 22:43:59 +03:00
60 changed files with 1904 additions and 43 deletions

View File

@@ -146,7 +146,7 @@ public class SoundControl{
if(state.isMenu()){
silenced = false;
if(ui.planet.isShown()){
play(Musics.launch);
play(ui.planet.state.planet.launchMusic);
}else if(ui.editor.isShown()){
play(Musics.editor);
}else{

View File

@@ -332,6 +332,13 @@ public class Control implements ApplicationListener, Loadable{
void createPlayer(){
player = Player.create();
player.name = Core.settings.getString("name");
String locale = Core.settings.getString("locale");
if(locale.equals("default")){
locale = Locale.getDefault().toString();
}
player.locale = locale;
player.color.set(Core.settings.getInt("color-0"));
if(mobile){

View File

@@ -36,6 +36,8 @@ public class GameState{
public GameStats stats = new GameStats();
/** Markers not linked to objectives. Controlled by world processors. */
public IntMap<ObjectiveMarker> markers = new IntMap<>();
/** Locale-specific string bundles of current map */
public MapLocales mapLocales = new MapLocales();
/** Global attributes of the environment, calculated by weather. */
public Attributes envAttrs = new Attributes();
/** Team data. Gets reset every new game. */

View File

@@ -24,6 +24,7 @@ import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.io.*;
import mindustry.maps.*;
import mindustry.type.*;
import mindustry.ui.*;
import mindustry.ui.dialogs.*;
import mindustry.world.*;

View File

@@ -7,6 +7,7 @@ import mindustry.game.*;
import mindustry.gen.*;
import mindustry.io.*;
import mindustry.maps.filters.*;
import mindustry.type.*;
import mindustry.ui.*;
import mindustry.ui.dialogs.*;
@@ -17,6 +18,7 @@ public class MapInfoDialog extends BaseDialog{
private final MapGenerateDialog generate;
private final CustomRulesDialog ruleInfo = new CustomRulesDialog();
private final MapObjectivesDialog objectives = new MapObjectivesDialog();
private final MapLocalesDialog locales = new MapLocalesDialog();
public MapInfoDialog(){
super("@editor.mapinfo");
@@ -94,6 +96,19 @@ public class MapInfoDialog extends BaseDialog{
});
hide();
}).marginLeft(10f);
r.row();
r.button("@editor.locales", Icon.fileText, style, () -> {
try{
MapLocales res = JsonIO.read(MapLocales.class, editor.tags.get("locales", "{}"));
locales.show(res);
}catch(Throwable e){
locales.show(new MapLocales());
ui.showException(e);
}
hide();
}).marginLeft(10f).width(0f).colspan(2).center().growX();
}).colspan(2).center();
name.change();

View File

@@ -0,0 +1,758 @@
package mindustry.editor;
import arc.Core;
import arc.func.*;
import arc.graphics.*;
import arc.scene.style.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.scene.utils.*;
import arc.struct.*;
import mindustry.*;
import mindustry.ctype.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.io.*;
import mindustry.type.*;
import mindustry.ui.*;
import mindustry.ui.dialogs.*;
import static mindustry.Vars.*;
public class MapLocalesDialog extends BaseDialog{
/** Width of UI property card. */
private static final float cardWidth = 400f;
/** Icons for use in map locales dialog. */
private static final ContentType[] contentIcons = {ContentType.item, ContentType.block, ContentType.liquid, ContentType.status, ContentType.unit};
private MapLocales locales;
private MapLocales lastSaved;
private boolean saved = true;
private Table langs;
private Table main;
private Table propView;
private String selectedLocale;
private boolean applytoall = true;
private boolean collapsed = false;
private String searchString = "";
private boolean searchByValue = false;
private boolean showCorrect = true;
private boolean showMissing = true;
private boolean showSame = true;
public MapLocalesDialog(){
super("@editor.locales");
selectedLocale = MapLocales.currentLocale();
langs = new Table(Tex.button);
main = new Table();
propView = new Table();
buttons.add("").uniform();
buttons.table(t -> {
t.defaults().pad(3).center();
t.button("@back", Icon.left, () -> {
if(!saved) ui.showConfirm("@editor.locales", "@editor.savechanges", () -> {
editor.tags.put("locales", JsonIO.write(locales));
state.mapLocales = locales;
});
hide();
}).size(210f, 64f);
closeOnBack(() -> {
if(!saved) ui.showConfirm("@editor.locales", "@editor.savechanges", () -> {
editor.tags.put("locales", JsonIO.write(locales));
state.mapLocales = locales;
});
});
t.button("@editor.apply", Icon.ok, () -> {
editor.tags.put("locales", JsonIO.write(locales));
state.mapLocales = locales;
lastSaved = locales.copy();
saved = true;
}).size(210f, 64f).disabled(b -> saved);
t.button("@edit", Icon.edit, this::editDialog).size(210f, 64f);
}).growX();
buttons.button("?", () -> ui.showInfo("@locales.info")).size(60f, 64f).uniform();
shown(this::setup);
}
public void show(MapLocales locales){
this.locales = locales;
lastSaved = locales.copy();
saved = true;
show();
}
private void setup(){
cont.clear();
buildTables();
cont.add(langs).left();
cont.table(t -> {
// search/collapse all/filter
t.table(a -> {
a.button(Icon.downOpen, Styles.emptyTogglei, () -> {
collapsed = !collapsed;
buildMain();
}).update(b -> {
b.replaceImage(new Image(collapsed ? Icon.upOpen : Icon.downOpen));
b.setChecked(collapsed);
}).size(35f);
a.button(Icon.filter, Styles.emptyi, () -> filterDialog(this::buildMain)).padLeft(10f).size(35f);
var field = a.field("", v -> {
searchString = v;
buildMain();
}).update(f -> f.setText(searchString)).maxTextLength(64).padLeft(10f).width(250f).update(f -> f.setMessageText(searchByValue ? "@locales.searchvalue": "@locales.searchname")).get();
a.button(Icon.cancel, Styles.emptyi, () -> {
searchString = "";
field.setText("");
buildMain();
}).padLeft(10f).size(35f);
}).row();
t.check("@locales.applytoall", applytoall, b -> applytoall = b).pad(10f).row();
t.add(main).center().grow().row();
}).pad(10f).grow();
// property addition
cont.table(Tex.button, t -> {
TextField name = t.field("name", s -> {}).maxTextLength(64).fillX().padTop(10f).get();
t.row();
TextField value = t.area("text", s -> {}).maxTextLength(1000).fillX().height(140f).get();
t.row();
t.button("@add", Icon.add, () -> {
if(applytoall){
for(var locale : locales.values()){
locale.put(name.getText(), value.getText());
}
}else{
locales.get(selectedLocale).put(name.getText(), value.getText());
}
saved = false;
buildMain();
}).padTop(10f).size(400f, 50f).fillX().row();
}).right();
}
private void buildTables(){
if(!locales.containsKey(selectedLocale)){
locales.put(selectedLocale, new StringMap());
}
buildLocalesTable();
buildMain();
}
private void buildLocalesTable(){
langs.clear();
langs.pane(p -> {
for(var loc : Vars.locales){
String name = loc.toString();
if(locales.containsKey(name)){
p.button(loc.getDisplayName(), Styles.flatTogglet, () -> {
if(name.equals(selectedLocale)) return;
selectedLocale = name;
buildTables();
}).update(b -> b.setChecked(selectedLocale.equals(name))).size(300f, 50f);
p.button(Icon.edit, Styles.flati, () -> localeEditDialog(name)).size(50f);
p.button(Icon.trash, Styles.flati, () -> ui.showConfirm("@confirm", "@locales.deletelocale", () -> {
locales.remove(name);
selectedLocale = (locales.size != 0 ? locales.keys().next() : Core.settings.getString("locale"));
saved = false;
buildTables();
})).size(50f).row();
}
}
}).row();
langs.button("@add", Icon.add, this::addLocaleDialog).padTop(10f).width(400f);
}
private void buildMain(){
main.clear();
StringMap props = locales.get(selectedLocale);
main.image().color(Pal.gray).height(3f).growX().expandY().top().row();
main.pane(p -> {
int cols = (Core.graphics.getWidth() - 380) / ((int)cardWidth + 10);
if(props.size == 0 || cols == 0){
main.add("@empty").center().row();
return;
}
p.defaults().top();
Table[] colTables = new Table[cols];
for(var i = 0; i < cols; i++){
colTables[i] = new Table();
}
int i = 0;
// To sort properties in alphabetic order
Seq<String> keys = props.keys().toSeq().sort();
for(var key : keys){
var comparsionString = (searchByValue ? props.get(key).toLowerCase() : key.toLowerCase());
if(!searchString.isEmpty() && !comparsionString.contains(searchString.toLowerCase())) continue;
PropertyStatus status = getPropertyStatus(key, props.get(key), selectedLocale, false);
if(status == PropertyStatus.correct && !showCorrect) continue;
if(status == PropertyStatus.missing && !showMissing) continue;
if(status == PropertyStatus.same && !showSame) continue;
colTables[i].table(Tex.whitePane, t -> {
boolean[] shown = {!collapsed};
String[] propKey = {key};
String[] propValue = {props.get(key)};
// collapse button
t.button(Icon.downOpen, Styles.emptyTogglei, () -> shown[0] = !shown[0]).update(b -> {
b.replaceImage(new Image(shown[0] ? Icon.upOpen : Icon.downOpen));
b.setChecked(shown[0]);
}).size(35f);
// property name field
t.field(propKey[0], (f, c) -> c != '=' && c != ':', v -> {
if(props.containsKey(v)){
t.setColor(Color.valueOf("f25555"));
return;
}
if(applytoall){
for(var bundle : locales.values()){
if(!bundle.containsKey(v)){
String value = bundle.get(propKey[0]);
if(value == null) continue;
bundle.remove(propKey[0]);
bundle.put(v, value);
}
}
}else{
if(!props.containsKey(v)){
props.remove(propKey[0]);
props.put(v, propValue[0]);
}
}
propKey[0] = v;
updateCard(t, v, propValue[0]);
saved = false;
}).maxTextLength(64).width(cardWidth - 125f);
// remove button
t.button(Icon.trash, Styles.emptyi, () -> {
if(applytoall){
for(var bundle : locales.values()){
bundle.remove(propKey[0]);
}
}else{
props.remove(propKey[0]);
}
saved = false;
buildMain();
}).size(35f);
// more actions
t.button(Icon.edit, Styles.emptyi, () -> propEditDialog(t, propKey[0], propValue[0])).size(35f).row();
// property value area
t.collapser(c -> c.area(propValue[0], v -> {
props.put(propKey[0], v);
updateCard(t, propKey[0], v);
saved = false;
}).maxTextLength(1000).height(140f).update(a -> {
propValue[0] = props.get(propKey[0]);
a.setText(props.get(propKey[0]));
}).growX(), () -> shown[0]).colspan(4).growX();
updateCard(t, propKey[0], propValue[0]);
}).top().width(cardWidth).pad(5f).row();
i = ++i % cols;
}
if(!colTables[0].hasChildren()){
main.add("@empty").center().row();
}else{
p.add(colTables);
}
}).growX().row();
main.image().color(Pal.gray).height(3f).growX().expandY().bottom().row();
}
private void updateCard(Table table, String propKey, String propValue){
updateCard(table, propKey, propValue, selectedLocale, false);
}
private void updateCard(Table table, String propKey, String propValue, String locale, boolean viewCard){
switch(getPropertyStatus(propKey, propValue, locale, viewCard)){
case missing -> table.setColor(Pal.accent);
case same -> table.setColor(Pal.techBlue);
case correct -> table.setColor(Pal.gray);
}
}
// Property statuses for main dialog and property view dialog are a bit different
private PropertyStatus getPropertyStatus(String propKey, String propValue, String locale, boolean forView){
if(forView && propValue == null) return PropertyStatus.missing;
for(var bundle : locales.entries()){
if(!forView && bundle.key.equals(selectedLocale)) continue;
if(forView && bundle.key.equals(locale)) continue;
StringMap props = bundle.value;
if(!props.containsKey(propKey)){
if(!forView) return PropertyStatus.missing;
}else{
if(props.get(propKey).equals(propValue)){
return PropertyStatus.same;
}
}
}
return PropertyStatus.correct;
}
private void addLocaleDialog(){
BaseDialog dialog = new BaseDialog("@add");
dialog.cont.pane(t -> {
for(var loc : Vars.locales){
String name = loc.toString();
if(!locales.containsKey(name)){
t.button(loc.getDisplayName(), Styles.flatTogglet, () -> {
if(name.equals(selectedLocale)) return;
locales.put(name, new StringMap());
selectedLocale = name;
saved = false;
buildTables();
dialog.hide();
}).update(b -> b.setChecked(selectedLocale.equals(name))).size(400f, 50f).row();
}
}
});
dialog.addCloseButton();
dialog.show();
}
private void propEditDialog(Table card, String key, String value){
BaseDialog dialog = new BaseDialog("@edit");
dialog.cont.pane(p -> {
p.margin(10f);
p.table(Tex.button, t -> {
t.defaults().size(450f, 60f).left();
t.button("@locales.addtoother", Icon.add, Styles.flatt, () -> {
for(var bundle : locales.values()){
if(!bundle.containsKey(key)){
bundle.put(key, value);
}
}
saved = false;
updateCard(card, key, value);
dialog.hide();
}).marginLeft(12f).row();
t.button("@locales.viewproperty", Icon.zoom, Styles.flatt, () -> {
viewPropertyDialog(key);
dialog.hide();
}).marginLeft(12f).row();
t.button("@locales.addicon", Icon.image, Styles.flatt, () -> {
addIconDialog(res -> {
locales.get(selectedLocale).put(key, value + res);
saved = false;
});
dialog.hide();
}).marginLeft(12f).row();
t.button("@locales.rollback", Icon.undo, Styles.flatt, () -> {
locales.get(selectedLocale).put(key, lastSaved.get(selectedLocale).get(key));
buildTables();
dialog.hide();
}).disabled(b -> {
if(!lastSaved.containsKey(selectedLocale)) return true;
StringMap savedMap = lastSaved.get(selectedLocale);
return !savedMap.containsKey(key) || savedMap.get(key).equals(locales.get(selectedLocale).get(key));
}).marginLeft(12f).row();
});
});
dialog.addCloseButton();
dialog.show();
}
private void localeEditDialog(String locale){
BaseDialog dialog = new BaseDialog("@edit");
dialog.cont.pane(p -> {
p.margin(10f);
p.table(Tex.button, t -> {
t.defaults().size(350f, 60f).left();
t.button("@waves.copy", Icon.copy, Styles.flatt, () -> {
Core.app.setClipboardText(writeLocale(locale));
ui.showInfoFade("@copied");
dialog.hide();
}).marginLeft(12f).row();
t.button("@waves.load", Icon.download, Styles.flatt, () -> {
locales.put(locale, readLocale(Core.app.getClipboardText()));
buildTables();
saved = false;
dialog.hide();
}).disabled(Core.app.getClipboardText() == null).marginLeft(12f).row();
});
});
dialog.addCloseButton();
dialog.show();
}
private void editDialog(){
BaseDialog dialog = new BaseDialog("@edit");
dialog.cont.pane(p -> {
p.margin(10f);
p.table(Tex.button, t -> {
t.defaults().size(450f, 60f).left();
t.button("@waves.copy", Icon.copy, Styles.flatt, () -> {
Core.app.setClipboardText(writeBundles());
ui.showInfoFade("@copied");
dialog.hide();
}).marginLeft(12f).row();
t.button("@waves.load", Icon.download, Styles.flatt, () -> {
locales = readBundles(Core.app.getClipboardText());
buildTables();
saved = false;
dialog.hide();
}).disabled(Core.app.getClipboardText() == null).marginLeft(12f).row();
t.button("@locales.rollback", Icon.undo, Styles.flatt, () -> {
locales = lastSaved.copy();
saved = true;
buildTables();
dialog.hide();
}).disabled(b -> saved).marginLeft(12f).row();
});
});
dialog.addCloseButton();
dialog.show();
}
private void viewPropertyDialog(String key){
BaseDialog dialog = new BaseDialog(Core.bundle.format("locales.viewing", key));
dialog.cont.table(t -> {
t.button(Icon.filter, Styles.emptyi, () -> filterDialog(() -> buildPropView(key))).size(35f);
var field = t.field(searchString, v -> {
searchString = v;
buildPropView(key);
}).update(f -> f.setText(searchString)).maxTextLength(64).padLeft(10f).width(250f).update(f -> f.setMessageText(searchByValue ? "@locales.searchvalue" : "@locales.searchlocale")).get();
t.button(Icon.cancel, Styles.emptyi, () -> {
searchString = "";
field.setText("");
buildPropView(key);
}).padLeft(10f).size(35f);
}).row();
buildPropView(key);
dialog.cont.add(propView).grow().center().row();
dialog.addCloseButton();
dialog.closeOnBack();
dialog.hidden(this::buildMain);
dialog.show();
}
private void buildPropView(String key){
propView.clear();
propView.image().color(Pal.gray).height(3f).fillX().top().row();
propView.pane(p -> {
int cols = (Core.graphics.getWidth() - 100) / ((int)cardWidth + 10);
if(cols == 0){
propView.add("@empty").center().row();
return;
}
p.defaults().top();
Table[] colTables = new Table[cols];
for(var i = 0; i < cols; i++){
colTables[i] = new Table();
}
int i = 0;
for(var loc : Vars.locales){
String name = loc.toString();
if(!locales.containsKey(name)) continue;
PropertyStatus status = getPropertyStatus(key, locales.get(name).get(key), name, true);
if(status == PropertyStatus.correct && !showCorrect) continue;
if(status == PropertyStatus.missing && !showMissing) continue;
if(status == PropertyStatus.same && !showSame) continue;
if(status != PropertyStatus.missing){
var comparsionString = (searchByValue ? locales.get(name).get(key).toLowerCase() : loc.getDisplayName().toLowerCase());
if(!searchString.isEmpty() && !comparsionString.contains(searchString.toLowerCase())) continue;
}
colTables[i].table(Tex.whitePane, t -> {
t.add(loc.getDisplayName()).left().color(Pal.accent).row();
t.image().color(Pal.accent).fillX().row();
if(status == PropertyStatus.missing){
t.table(b ->
b.button("@add", Icon.add, () -> {
locales.get(name).put(key, "moai");
t.getCells().get(2).clearElement();
t.getCells().remove(2);
t.area(locales.get(name).get(key), v -> {
locales.get(name).put(key, v);
saved = false;
}).maxTextLength(1000).height(140f).growX().row();
}).size(160f, 50f)).height(140f).growX().row();
}else{
t.area(locales.get(name).get(key), v -> {
locales.get(name).put(key, v);
saved = false;
}).maxTextLength(1000).height(140f).growX().row();
}
}).update(t -> updateCard(t, key, locales.get(name).get(key), name, true)).top().width(cardWidth).pad(5f).row();
i = ++i % cols;
}
if(!colTables[0].hasChildren()){
propView.add("@empty").center().row();
}else{
p.add(colTables);
}
}).grow().row();
propView.image().color(Pal.gray).height(3f).fillX().bottom().row();
}
private void filterDialog(Runnable hidden){
BaseDialog dialog = new BaseDialog("@locales.filter");
dialog.cont.table(t -> {
t.add("@search").row();
t.table(b -> {
b.button("@locales.byname", Styles.togglet, () -> searchByValue = false).size(300f, 50f).checked(v -> !searchByValue);
b.button("@locales.byvalue", Styles.togglet, () -> searchByValue = true).padLeft(10f).size(300f, 50f).checked(v -> searchByValue);
}).padTop(5f);
}).row();
dialog.cont.table(Tex.whitePane, t ->
t.button("@locales.showcorrect", Icon.ok, Styles.nonet, () -> showCorrect = !showCorrect).update(b -> {
((Image)b.getChildren().get(1)).setDrawable(showCorrect ? Icon.ok : Icon.cancel);
b.setChecked(showCorrect);
}).grow().pad(15f)).size(450f, 100f).color(Pal.gray).padTop(50f);
dialog.cont.row();
dialog.cont.table(Tex.whitePane, t ->
t.button("@locales.showmissing", Icon.ok, Styles.nonet, () -> showMissing = !showMissing).update(b -> {
((Image)b.getChildren().get(1)).setDrawable(showMissing ? Icon.ok : Icon.cancel);
b.setChecked(showMissing);
}).grow().pad(15f)).size(450f, 100f).color(Pal.accent).padTop(50f);
dialog.cont.row();
dialog.cont.table(Tex.whitePane, t ->
t.button("@locales.showsame", Icon.ok, Styles.nonet, () -> showSame = !showSame).update(b -> {
((Image)b.getChildren().get(1)).setDrawable(showSame ? Icon.ok : Icon.cancel);
b.setChecked(showSame);
}).grow().pad(15f)).size(450f, 100f).color(Pal.techBlue).padTop(50f);
dialog.buttons.button("@back", Icon.left, () -> {
hidden.run();
dialog.hide();
}).size(210f, 64f);
dialog.closeOnBack(hidden);
dialog.show();
}
private void addIconDialog(Cons<String> cons){
BaseDialog dialog = new BaseDialog("@locales.addicon");
Table icons = new Table();
TextField search = Elem.newField("", v -> iconsTable(icons, v.replace(" ", "").toLowerCase(), dialog, cons));
search.setMessageText("@search");
dialog.cont.table(t -> {
t.add(search).maxTextLength(64).padLeft(10f).width(250f);
t.button(Icon.cancel, Styles.emptyi, () -> {
search.setText("");
iconsTable(icons, "", dialog, cons);
}).padLeft(10f).size(35f);
}).row();
dialog.cont.pane(icons).scrollX(false);
dialog.resized(true, () -> iconsTable(icons, search.getText().replace(" ", "").toLowerCase(), dialog, cons));
dialog.addCloseButton();
dialog.closeOnBack();
dialog.setFillParent(true);
dialog.show();
}
private void iconsTable(Table table, String search, Dialog dialog, Cons<String> cons){
table.clear();
table.marginRight(19f).marginLeft(12f);
table.defaults().size(48f);
int cols = (int)Math.min(20, Core.graphics.getWidth() / Scl.scl(52f));
int i = 0;
var codes = new ObjectIntMap<>(Iconc.codes);
for(var name : codes.keys()){
if(!name.toLowerCase().contains(search)) codes.remove(name);
}
if(codes.size > 0) table.image().colspan(cols).growX().width(-1f).height(3f).color(Pal.accent).row();
for(var icon : codes){
String res = (char)icon.value + "";
table.button(Icon.icons.get(icon.key), Styles.flati, iconMed, () -> {
cons.get(res);
dialog.hide();
}).tooltip(icon.key);
if(++i % cols == 0) table.row();
}
for(ContentType ctype : contentIcons){
var all = content.getBy(ctype).<UnlockableContent>as().select(u -> u.localizedName.replace(" ", "").toLowerCase().contains(search) && u.uiIcon.found());
table.row();
if(all.size > 0) table.image().colspan(cols).growX().width(-1f).height(3f).color(Pal.accent).row();
i = 0;
for(UnlockableContent u : all){
table.button(new TextureRegionDrawable(u.uiIcon), Styles.flati, iconMed, () -> {
cons.get(u.emoji() + "");
dialog.hide();
}).tooltip(u.localizedName);
if(++i % cols == 0) table.row();
}
}
var teams = new Seq<>(Team.baseTeams);
teams = teams.select(u -> u.localized().toLowerCase().contains(search) && Core.atlas.has("team-" + u.name));
table.row();
if(teams.size > 0) table.image().colspan(cols).growX().width(-1f).height(3f).color(Pal.accent).row();
for(Team team : teams){
var region = Core.atlas.find("team-" + team.name);
table.button(new TextureRegionDrawable(region), Styles.flati, iconMed, () -> {
cons.get(team.emoji);
dialog.hide();
}).tooltip(team.localized());
if(++i % cols == 0) table.row();
}
}
private String writeBundles(){
StringBuilder data = new StringBuilder();
for(var locale : locales.keys()){
data.append(locale).append(":\n").append(writeLocale(locale));
}
return data.toString();
}
private String writeLocale(String key){
StringBuilder data = new StringBuilder();
if(!locales.containsKey(key)) return "";
for(var prop : locales.get(key).entries()){
data.append(prop.key).append(" = ").append(prop.value).append("\n");
}
return data.toString();
}
private MapLocales readBundles(String data){
MapLocales bundles = new MapLocales();
String currentLocale = "";
for(var line : data.split("\\r?\\n|\\r")){
if(line.endsWith(":") && !line.contains("=")){
currentLocale = line.substring(0, line.length() - 1);
bundles.put(currentLocale, new StringMap());
}else{
int sepIndex = line.indexOf(" = ");
if(sepIndex != -1 && !currentLocale.isEmpty()){
bundles.get(currentLocale).put(line.substring(0, sepIndex), line.substring(sepIndex + 3));
}
}
}
return bundles;
}
private StringMap readLocale(String data){
StringMap map = new StringMap();
for(var line : data.split("\\r?\\n|\\r")){
int sepIndex = line.indexOf(" = ");
if(sepIndex != -1){
map.put(line.substring(0, sepIndex), line.substring(sepIndex + 3));
}
}
return map;
}
private enum PropertyStatus{
correct,
missing,
same
}
}

View File

@@ -109,8 +109,8 @@ abstract class BuilderComp implements Posc, Statusc, Teamc, Rotc{
var core = core();
//nothing to build.
if(buildPlan() == null) return;
//nothing to build, or core doesn't exist
if(buildPlan() == null || (core == null && !infinite)) return;
//find the next build plan
if(plans.size > 1){
@@ -163,7 +163,7 @@ abstract class BuilderComp implements Posc, Statusc, Teamc, Rotc{
}
//if there is no core to build with or no build entity, stop building!
if((core == null && !infinite) || !(tile.build instanceof ConstructBuild entity)){
if(!(tile.build instanceof ConstructBuild entity)){
continue;
}

View File

@@ -40,6 +40,8 @@ public class WeaponMount{
public int totalShots;
/** counter for which barrel bullets have been fired from; used for alternating patterns */
public int barrelCounter;
/** Last aim length of weapon. Only used for point lasers. */
public float lastLength;
/** current bullet for continuous weapons */
public @Nullable Bullet bullet;
/** sound loop for continuous weapons */

View File

@@ -484,6 +484,14 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
timeString.append(s);
if(text.startsWith("@")){
if(state.mapLocales.containsProperty(text.substring(1))){
try{
return state.mapLocales.getFormatted(text.substring(1), timeString.toString());
}catch(IllegalArgumentException e){
//illegal text.
text = "";
}
}
return Core.bundle.format(text.substring(1), timeString.toString());
}else{
try{
@@ -590,9 +598,17 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
return state.rules.objectiveFlags.contains(flag);
}
@Nullable
@Override
public String text(){
return text != null && text.startsWith("@") ? Core.bundle.get(text.substring(1)) : text;
if(text == null) return null;
if(text.startsWith("@")){
if(state.mapLocales.containsProperty(text.substring(1))) return state.mapLocales.getProperty(text.substring(1));
return Core.bundle.get(text.substring(1));
}else{
return text;
}
}
}
@@ -651,12 +667,23 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
}
public static String fetchText(String text){
return text.startsWith("@") ?
//on mobile, try ${text}.mobile first for mobile-specific hints.
mobile ? Core.bundle.get(text.substring(1) + ".mobile", Core.bundle.get(text.substring(1))) :
Core.bundle.get(text.substring(1)) :
text;
if(text == null) return "";
if(text.startsWith("@")){
String key = text.substring(1);
if(mobile){
return state.mapLocales.containsProperty(key + ".mobile") ?
state.mapLocales.getProperty(key + ".mobile") :
Core.bundle.get(key + ".mobile", Core.bundle.get(key));
}else{
return state.mapLocales.containsProperty(key) ?
state.mapLocales.getProperty(key) :
Core.bundle.get(key);
}
}else{
return text;
}
}
}

View File

@@ -654,7 +654,13 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
}
if(player.team() == build.team && build.canControlSelect(player.unit())){
var before = player.unit();
build.onControlSelect(player.unit());
if(!before.dead && before.spawnedByCore && !before.isPlayer()){
Call.unitDespawn(before);
}
}
}
@@ -699,6 +705,10 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
//direct dock transfer???
unit.dockedType = before.dockedType;
}
if(before.spawnedByCore && !before.isPlayer()){
Call.unitDespawn(before);
}
}
Time.run(Fx.unitSpirit.lifetime, () -> Fx.unitControl.at(unit.x, unit.y, 0f, unit));

View File

@@ -17,6 +17,7 @@ import mindustry.game.MapObjectives.*;
import mindustry.game.Teams.*;
import mindustry.gen.*;
import mindustry.maps.Map;
import mindustry.type.*;
import mindustry.world.*;
import mindustry.world.meta.*;
@@ -141,6 +142,7 @@ public abstract class SaveVersion extends SaveFileReader{
"wavetime", state.wavetime,
"stats", JsonIO.write(state.stats),
"rules", JsonIO.write(state.rules),
"locales", JsonIO.write(state.mapLocales),
"mods", JsonIO.write(mods.getModStrings().toArray(String.class)),
"controlGroups", headless || control == null ? "null" : JsonIO.write(control.input.controlGroups),
"width", world.width(),
@@ -160,6 +162,7 @@ public abstract class SaveVersion extends SaveFileReader{
state.tick = map.getFloat("tick");
state.stats = JsonIO.read(GameStats.class, map.get("stats", "{}"));
state.rules = JsonIO.read(Rules.class, map.get("rules", "{}"));
state.mapLocales = JsonIO.read(MapLocales.class, map.get("locales", "{}"));
if(state.rules.spawns.isEmpty()) state.rules.spawns = waves.get();
lastReadBuild = map.getInt("build", -1);

View File

@@ -27,7 +27,7 @@ public class GlobalVars{
public static final Rand rand = new Rand();
//non-constants that depend on state
private static int varTime, varTick, varSecond, varMinute, varWave, varWaveTime, varServer, varClient, varClientLocale, varClientUnit, varClientName, varClientTeam;
private static int varTime, varTick, varSecond, varMinute, varWave, varWaveTime, varServer, varClient, varClientLocale, varClientUnit, varClientName, varClientTeam, varClientMobile;
private ObjectIntMap<String> namesToIds = new ObjectIntMap<>();
private Seq<Var> vars = new Seq<>(Var.class);
@@ -65,6 +65,7 @@ public class GlobalVars{
varClientUnit = put("@clientUnit", null, true);
varClientName = put("@clientName", null, true);
varClientTeam = put("@clientTeam", 0, true);
varClientMobile = put("@clientMobile", 0, true);
//special enums
put("@ctrlProcessor", ctrlProcessor);
@@ -168,6 +169,7 @@ public class GlobalVars{
vars.items[varClientUnit].objval = player.unit();
vars.items[varClientName].objval = player.name();
vars.items[varClientTeam].numval = player.team().id;
vars.items[varClientMobile].numval = mobile ? 1 : 0;
}
}
@@ -183,14 +185,18 @@ public class GlobalVars{
return arr != null && content.id >= 0 && content.id < arr.length ? arr[content.id] : -1;
}
/** @return a constant ID > 0 if there is a constant with this name, otherwise -1.
* Attempt to get privileged variable id from non-privileged logic executor returns null constant id. */
/**
* @return a constant ID > 0 if there is a constant with this name, otherwise -1.
* Attempt to get privileged variable id from non-privileged logic executor returns null constant id.
*/
public int get(String name){
return namesToIds.get(name, -1);
}
/** @return a constant variable by ID. ID is not bound checked and must be positive.
* Attempt to get privileged variable from non-privileged logic executor returns null constant */
/**
* @return a constant variable by ID. ID is not bound checked and must be positive.
* Attempt to get privileged variable from non-privileged logic executor returns null constant
*/
public Var get(int id, boolean privileged){
if(!privileged && privilegedIds.contains(id)) return vars.get(namesToIds.get("null"));
return vars.items[id];

View File

@@ -1116,6 +1116,55 @@ public class LExecutor{
}
}
public static class FormatI implements LInstruction{
public int value;
public FormatI(int value){
this.value = value;
}
FormatI(){}
@Override
public void run(LExecutor exec){
if(exec.textBuffer.length() >= maxTextBuffer) return;
int placeholderIndex = -1;
int placeholderNumber = 10;
for(int i = 0; i < exec.textBuffer.length(); i++){
if(exec.textBuffer.charAt(i) == '{' && exec.textBuffer.length() - i > 2){
char numChar = exec.textBuffer.charAt(i + 1);
if(numChar >= '0' && numChar <= '9' && exec.textBuffer.charAt(i + 2) == '}'){
if(numChar - '0' < placeholderNumber){
placeholderNumber = numChar - '0';
placeholderIndex = i;
}
}
}
}
if(placeholderIndex == -1) return;
//this should avoid any garbage allocation
Var v = exec.var(value);
if(v.isobj && value != 0){
String strValue = PrintI.toString(v.objval);
exec.textBuffer.replace(placeholderIndex, placeholderIndex + 3, strValue);
}else{
//display integer version when possible
if(Math.abs(v.numval - (long)v.numval) < 0.00001){
exec.textBuffer.replace(placeholderIndex, placeholderIndex + 3, (long)v.numval + "");
}else{
exec.textBuffer.replace(placeholderIndex, placeholderIndex + 3, v.numval + "");
}
}
}
}
public static class PrintFlushI implements LInstruction{
public int target;
@@ -2028,5 +2077,39 @@ public class LExecutor{
}
}
public static class LocalePrintI implements LInstruction{
public int name;
public LocalePrintI(int name){
this.name = name;
}
public LocalePrintI(){
}
@Override
public void run(LExecutor exec){
if(exec.textBuffer.length() >= maxTextBuffer) return;
//this should avoid any garbage allocation
Var v = exec.var(name);
if(v.isobj){
String name = PrintI.toString(v.objval);
String strValue;
if(mobile){
strValue = state.mapLocales.containsProperty(name + ".mobile") ?
state.mapLocales.getProperty(name + ".mobile") :
state.mapLocales.getProperty(name);
}else{
strValue = state.mapLocales.getProperty(name);
}
exec.textBuffer.append(strValue);
}
}
}
//endregion
}

View File

@@ -164,7 +164,7 @@ public class LStatements{
}
if(type == GraphicsType.print){
p2 = "bottomLeft";
p1 = "bottomLeft";
}
rebuild(table);
@@ -305,6 +305,27 @@ public class LStatements{
}
}
@RegisterStatement("format")
public static class FormatStatement extends LStatement{
public String value = "\"frog\"";
@Override
public void build(Table table){
field(table, value, str -> value = str).width(0f).growX().padRight(3);
}
@Override
public LInstruction build(LAssembler builder){
return new FormatI(builder.var(value));
}
@Override
public LCategory category(){
return LCategory.io;
}
}
@RegisterStatement("drawflush")
public static class DrawFlushStatement extends LStatement{
public String target = "display1";
@@ -2071,4 +2092,29 @@ public class LStatements{
return LCategory.world;
}
}
@RegisterStatement("localeprint")
public static class LocalePrintStatement extends LStatement{
public String value = "\"name\"";
@Override
public void build(Table table){
field(table, value, str -> value = str).width(0f).growX().padRight(3);
}
@Override
public boolean privileged(){
return true;
}
@Override
public LInstruction build(LAssembler builder){
return new LocalePrintI(builder.var(value));
}
@Override
public LCategory category(){
return LCategory.world;
}
}
}

View File

@@ -2,6 +2,7 @@ package mindustry.mod;
import arc.*;
import arc.assets.*;
import arc.assets.loaders.MusicLoader.*;
import arc.assets.loaders.SoundLoader.*;
import arc.audio.*;
import arc.files.*;
@@ -60,7 +61,6 @@ public class ContentParser{
ObjectMap<Class<?>, ContentType> contentTypes = new ObjectMap<>();
ObjectSet<Class<?>> implicitNullable = ObjectSet.with(TextureRegion.class, TextureRegion[].class, TextureRegion[][].class, TextureRegion[][][].class);
ObjectMap<String, AssetDescriptor<?>> sounds = new ObjectMap<>();
Seq<ParseListener> listeners = new Seq<>();
ObjectMap<Class<?>, FieldParser> classParsers = new ObjectMap<>(){{
@@ -271,18 +271,14 @@ public class ContentParser{
return new Vec3(data.getFloat("x", 0f), data.getFloat("y", 0f), data.getFloat("z", 0f));
});
put(Sound.class, (type, data) -> {
if(fieldOpt(Sounds.class, data) != null) return fieldOpt(Sounds.class, data);
if(Vars.headless) return new Sound();
var field = fieldOpt(Sounds.class, data);
String name = "sounds/" + data.asString();
String path = Vars.tree.get(name + ".ogg").exists() ? name + ".ogg" : name + ".mp3";
return field != null ? field : Vars.tree.loadSound(data.asString());
});
put(Music.class, (type, data) -> {
var field = fieldOpt(Musics.class, data);
if(sounds.containsKey(path)) return ((SoundParameter)sounds.get(path).params).sound;
var sound = new Sound();
AssetDescriptor<?> desc = Core.assets.load(path, Sound.class, new SoundParameter(sound));
desc.errored = Throwable::printStackTrace;
sounds.put(path, desc);
return sound;
return field != null ? field : Vars.tree.loadMusic(data.asString());
});
put(Objectives.Objective.class, (type, data) -> {
if(data.isString()){
@@ -995,7 +991,6 @@ public class ContentParser{
throw new RuntimeException(e);
}
}
Object fieldOpt(Class<?> type, JsonValue value){
try{
return type.getField(value.asString()).get(null);

View File

@@ -11,6 +11,7 @@ import mindustry.io.*;
import mindustry.logic.*;
import mindustry.maps.Map;
import mindustry.net.Administration.*;
import mindustry.type.*;
import java.io.*;
import java.nio.*;
@@ -36,6 +37,7 @@ public class NetworkIO{
}
stream.writeUTF(JsonIO.write(state.rules));
stream.writeUTF(JsonIO.write(state.mapLocales));
SaveIO.getSaveWriter().writeStringMap(stream, state.map.tags);
stream.writeInt(state.wave);
@@ -62,6 +64,7 @@ public class NetworkIO{
try(DataInputStream stream = new DataInputStream(is)){
Time.clear();
state.rules = JsonIO.read(Rules.class, stream.readUTF());
state.mapLocales = JsonIO.read(MapLocales.class, stream.readUTF());
state.map = new Map(SaveIO.getSaveWriter().readStringMap(stream));
state.wave = stream.readInt();

View File

@@ -0,0 +1,97 @@
package mindustry.type;
import arc.struct.*;
import arc.util.serialization.*;
import arc.util.serialization.Json.*;
import java.util.*;
import static arc.Core.*;
/** Class for storing map-specific locale bundles */
public class MapLocales extends ObjectMap<String, StringMap> implements JsonSerializable{
@Override
public void write(Json json){
for(var entry : entries()){
json.writeValue(entry.key, entry.value, StringMap.class, String.class);
}
}
@Override
public void read(Json json, JsonValue jsonData){
for(JsonValue value : jsonData){
put(value.name, json.readValue(StringMap.class, value));
}
}
@Override
public MapLocales copy(){
MapLocales out = new MapLocales();
for(var entry : this.entries()){
StringMap map = new StringMap();
map.putAll(entry.value);
out.put(entry.key, map);
}
return out;
}
public String getProperty(String key){
if(!containsProperty(currentLocale(), key)){
if(containsProperty("en", key)) return get("en").get(key);
return "???" + key + "???";
}
return get(currentLocale()).get(key);
}
private String getProperty(String locale, String key){
if(!containsProperty(locale, key)){
if(containsProperty("en", key)) return get("en").get(key);
return "???" + key + "???";
}
return get(locale).get(key);
}
public boolean containsProperty(String key){
return containsProperty(currentLocale(), key) || containsProperty("en", key);
}
private boolean containsProperty(String locale, String key){
if(!containsKey(locale)) return false;
return get(locale).containsKey(key);
}
public String getFormatted(String key, Object... args){
StringBuilder result = new StringBuilder();
if(!containsProperty(currentLocale(), key)){
if(containsProperty("en", key)){
result.append(getProperty("en", key));
}else{
return "???" + key + "???";
}
}else{
result.append(getProperty(currentLocale(), key));
}
for(var arg : args){
int placeholderIndex = result.indexOf("@");
if(placeholderIndex == -1) break;
result.replace(placeholderIndex, placeholderIndex + 1, arg.toString());
}
return result.toString();
}
// To handle default locale properly
public static String currentLocale(){
String locale = settings.getString("locale");
if(locale.equals("default")){
locale = Locale.getDefault().getLanguage();
}
return locale;
}
}

View File

@@ -1,6 +1,7 @@
package mindustry.type;
import arc.*;
import arc.audio.*;
import arc.func.*;
import arc.graphics.*;
import arc.graphics.g3d.*;
@@ -15,6 +16,7 @@ import mindustry.content.TechTree.*;
import mindustry.ctype.*;
import mindustry.game.*;
import mindustry.game.EventType.ContentInitEvent;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.graphics.g3d.*;
import mindustry.graphics.g3d.PlanetGrid.*;
@@ -128,6 +130,8 @@ public class Planet extends UnlockableContent{
public boolean allowLaunchToNumbered = true;
/** Icon as displayed in the planet selection dialog. This is a string, as drawables are null at load time. */
public String icon = "planet";
/** Plays in the planet dialog when this planet is selected. */
public Music launchMusic = Musics.launch;
/** Default core block for launching. */
public Block defaultCore = Blocks.coreShard;
/** Sets up rules on game load for any sector on this planet. */

View File

@@ -54,6 +54,8 @@ public class Weapon implements Cloneable{
public boolean continuous;
/** whether this weapon uses continuous fire without reloading; implies continuous = true */
public boolean alwaysContinuous;
/** Speed at which the turret can change its bullet "aim" distance. This is only used for point laser bullets. */
public float aimChangeSpeed = Float.POSITIVE_INFINITY;
/** whether this weapon can be aimed manually by players */
public boolean controllable = true;
/** whether this weapon can be automatically aimed by the unit */
@@ -370,6 +372,18 @@ public class Weapon implements Cloneable{
mount.sound.update(bulletX, bulletY, true);
}
//target length of laser
float shootLength = Math.min(Mathf.dst(bulletX, bulletY, mount.aimX, mount.aimY), range());
//current length of laser
float curLength = Mathf.dst(bulletX, bulletY, mount.bullet.aimX, mount.bullet.aimY);
//resulting length of the bullet (smoothed)
float resultLength = Mathf.approachDelta(curLength, shootLength, aimChangeSpeed);
//actual aim end point based on length
Tmp.v1.trns(shootAngle, mount.lastLength = resultLength).add(bulletX, bulletY);
mount.bullet.aimX = Tmp.v1.x;
mount.bullet.aimY = Tmp.v1.y;
if(alwaysContinuous && mount.shoot){
mount.bullet.time = mount.bullet.lifetime * mount.bullet.type.optimalLifeFract * mount.warmup;
mount.bullet.keepAlive = true;
@@ -468,7 +482,7 @@ public class Weapon implements Cloneable{
bulletY = mountY + Angles.trnsy(weaponRotation, this.shootX + xOffset + xSpread, this.shootY + yOffset),
shootAngle = bulletRotation(unit, mount, bulletX, bulletY) + angleOffset,
lifeScl = bullet.scaleLife ? Mathf.clamp(Mathf.dst(bulletX, bulletY, mount.aimX, mount.aimY) / bullet.range) : 1f,
angle = angleOffset + shootAngle + Mathf.range(inaccuracy + bullet.inaccuracy);
angle = shootAngle + Mathf.range(inaccuracy + bullet.inaccuracy);
Entityc shooter = unit.controller() instanceof MissileAI ai ? ai.shooter : unit; //Pass the missile's shooter down to its bullets
mount.bullet = bullet.create(unit, shooter, unit.team, bulletX, bulletY, angle, -1f, (1f - velocityRnd) + Mathf.random(velocityRnd), lifeScl, null, mover, mount.aimX, mount.aimY);
@@ -493,7 +507,18 @@ public class Weapon implements Cloneable{
//override to do special things to a bullet after spawning
protected void handleBullet(Unit unit, WeaponMount mount, Bullet bullet){
if(continuous){
float
weaponRotation = unit.rotation - 90 + (rotate ? mount.rotation : baseRotation),
mountX = unit.x + Angles.trnsx(unit.rotation - 90, x, y),
mountY = unit.y + Angles.trnsy(unit.rotation - 90, x, y),
bulletX = mountX + Angles.trnsx(weaponRotation, this.shootX, this.shootY),
bulletY = mountY + Angles.trnsy(weaponRotation, this.shootX, this.shootY);
//make sure the length updates to the last set value
Tmp.v1.trns(bulletRotation(unit, mount, bulletX, bulletY), shootY + mount.lastLength).add(bulletX, bulletY);
bullet.aimX = Tmp.v1.x;
bullet.aimY = Tmp.v1.y;
}
}
public void flip(){

View File

@@ -50,7 +50,7 @@ public class CustomRulesDialog extends BaseDialog{
t.defaults().size(280f, 64f).pad(2f);
t.button("@waves.copy", Icon.copy, style, () -> {
ui.showInfoFade("@waves.copied");
ui.showInfoFade("@copied");
//hack: don't write the spawns, they just waste space
var spawns = rules.spawns;

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.*;