diff --git a/core/assets/bundles/bundle.properties b/core/assets/bundles/bundle.properties index bf88e0992b..708d3bd0e1 100644 --- a/core/assets/bundles/bundle.properties +++ b/core/assets/bundles/bundle.properties @@ -444,6 +444,7 @@ editor.waves = Waves editor.rules = Rules editor.generation = Generation editor.objectives = Objectives +editor.locales = Locale Bundles editor.ingame = Edit In-Game editor.playtest = Playtest editor.publish.workshop = Publish On Workshop @@ -511,6 +512,7 @@ editor.errorlegacy = This map is too old, and uses a legacy map format that is n editor.errornot = This is not a map file. editor.errorheader = This map file is either not valid or corrupt. editor.errorname = Map has no name defined. Are you trying to load a save file? +editor.errorlocales = Error reading invalid locale bundles. editor.update = Update editor.randomize = Randomize editor.moveup = Move Up @@ -522,6 +524,7 @@ editor.sectorgenerate = Sector Generate editor.resize = Resize editor.loadmap = Load Map editor.savemap = Save Map +editor.savechanges = [scarlet]You have unsaved changes!\n\n[]Do you want to save them? editor.saved = Saved! editor.save.noname = Your map does not have a name! Set one in the 'map info' menu. editor.save.overwrite = Your map overwrites a built-in map! Pick a different name in the 'map info' menu. @@ -610,6 +613,24 @@ filter.option.threshold2 = Secondary Threshold filter.option.radius = Radius filter.option.percentile = Percentile +locales.info = Here, you can add locale bundles for specific languages to your map. In locale bundles, each property has a name and a value. These properties can be used by world processors and objectives using their names. They support text formatting (replacing placeholders with actual values).\n\n[cyan]Example property:\n[]name: [accent]timer[]\nvalue: [accent]Example timer, time left: @[]\n\n[cyan]Usage:\n[]Set it as objective's text: [accent]@timer\n\n[]Print it in a world processor:\n[accent]localeprint "timer"\nformat time\n[gray](where time is a separately calculated variable) +locales.deletelocale = Are you sure you want to delete this locale bundle? +locales.applytoall = Apply Changes To All Locales +locales.addtoother = Add To Other Locales +locales.rollback = Rollback to last applied +locales.filter = Property filter +locales.searchname = Search name... +locales.searchvalue = Search value... +locales.searchlocale = Search locale... +locales.byname = By name +locales.byvalue = By value +locales.showcorrect = Show properties that are present in all locales and have unique values everywhere +locales.showmissing = Show properties that are missing in some locales +locales.showsame = Show properties that have same values in different locales +locales.viewproperty = View in all locales +locales.viewing = Viewing property "{0}" +locales.addicon = Add Icon + width = Width: height = Height: menu = Menu @@ -2267,6 +2288,7 @@ unit.emanate.description = Builds structures to defend the Acropolis core. Repai lst.read = Read a number from a linked memory cell. lst.write = Write a number to a linked memory cell. lst.print = Add text to the print buffer.\nDoes not display anything until [accent]Print Flush[] is used. +lst.format = Replace next placeholder ("[accent]@[]") in text buffer with a value.\nExample:\n[accent]print "test @"\nformat "example" lst.draw = Add an operation to the drawing buffer.\nDoes not display anything until [accent]Draw Flush[] is used. lst.drawflush = Flush queued [accent]Draw[] operations to a display. lst.printflush = Flush queued [accent]Print[] operations to a message block. @@ -2304,6 +2326,7 @@ lst.effect = Create a particle effect. lst.sync = Sync a variable across the network.\nLimited to 20 times a second per variable. lst.makemarker = Create a new logic marker in the world.\nAn ID to identify this marker must be provided.\nMarkers currently limited to 20,000 per world. lst.setmarker = Set a property for a marker.\nThe ID used must be the same as in the Make Marker instruction. +lst.localeprint = Add map locale property value to the text buffer.\nTo set map locale bundles in map editor, check [accent]Map Info > Locale Bundles[].\nIf client is a mobile device, tries to print a property ending in ".mobile" first. logic.nounitbuild = [red]Unit building logic is not allowed here. diff --git a/core/src/mindustry/core/Control.java b/core/src/mindustry/core/Control.java index 9d6a699bde..7e5ab7fde0 100644 --- a/core/src/mindustry/core/Control.java +++ b/core/src/mindustry/core/Control.java @@ -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){ diff --git a/core/src/mindustry/core/GameState.java b/core/src/mindustry/core/GameState.java index abad7d3899..5d9ee3515e 100644 --- a/core/src/mindustry/core/GameState.java +++ b/core/src/mindustry/core/GameState.java @@ -36,6 +36,8 @@ public class GameState{ public GameStats stats = new GameStats(); /** Markers not linked to objectives. Controlled by world processors. */ public IntMap 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. */ diff --git a/core/src/mindustry/editor/MapEditorDialog.java b/core/src/mindustry/editor/MapEditorDialog.java index 0010346f9a..56ce52d8e1 100644 --- a/core/src/mindustry/editor/MapEditorDialog.java +++ b/core/src/mindustry/editor/MapEditorDialog.java @@ -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.*; diff --git a/core/src/mindustry/editor/MapInfoDialog.java b/core/src/mindustry/editor/MapInfoDialog.java index 2c7e4e46ab..86a217e82e 100644 --- a/core/src/mindustry/editor/MapInfoDialog.java +++ b/core/src/mindustry/editor/MapInfoDialog.java @@ -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(); diff --git a/core/src/mindustry/editor/MapLocalesDialog.java b/core/src/mindustry/editor/MapLocalesDialog.java new file mode 100644 index 0000000000..0a8098fb29 --- /dev/null +++ b/core/src/mindustry/editor/MapLocalesDialog.java @@ -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 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 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 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).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 + } +} diff --git a/core/src/mindustry/game/MapObjectives.java b/core/src/mindustry/game/MapObjectives.java index cef72e7971..264375ae1d 100644 --- a/core/src/mindustry/game/MapObjectives.java +++ b/core/src/mindustry/game/MapObjectives.java @@ -483,6 +483,14 @@ public class MapObjectives implements Iterable, Eachable, Eachable, Eachable namesToIds = new ObjectIntMap<>(); private Seq 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]; diff --git a/core/src/mindustry/logic/LExecutor.java b/core/src/mindustry/logic/LExecutor.java index 643efb387f..253d452b0c 100644 --- a/core/src/mindustry/logic/LExecutor.java +++ b/core/src/mindustry/logic/LExecutor.java @@ -1099,6 +1099,41 @@ 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 = exec.textBuffer.indexOf("@"); + + 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 + 1, strValue); + }else{ + //display integer version when possible + if(Math.abs(v.numval - (long)v.numval) < 0.00001){ + exec.textBuffer.replace(placeholderIndex, placeholderIndex + 1, (long)v.numval + ""); + }else{ + exec.textBuffer.replace(placeholderIndex, placeholderIndex + 1, v.numval + ""); + } + } + } + } + public static class PrintFlushI implements LInstruction{ public int target; @@ -1996,5 +2031,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 } diff --git a/core/src/mindustry/logic/LStatements.java b/core/src/mindustry/logic/LStatements.java index fe3308212a..1a9e0473d8 100644 --- a/core/src/mindustry/logic/LStatements.java +++ b/core/src/mindustry/logic/LStatements.java @@ -301,6 +301,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"; @@ -2055,4 +2076,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; + } + } } diff --git a/core/src/mindustry/net/NetworkIO.java b/core/src/mindustry/net/NetworkIO.java index fdc223184c..48599c6b96 100644 --- a/core/src/mindustry/net/NetworkIO.java +++ b/core/src/mindustry/net/NetworkIO.java @@ -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(); diff --git a/core/src/mindustry/type/MapLocales.java b/core/src/mindustry/type/MapLocales.java new file mode 100644 index 0000000000..88dc81237a --- /dev/null +++ b/core/src/mindustry/type/MapLocales.java @@ -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 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; + } +} diff --git a/core/src/mindustry/ui/dialogs/CustomRulesDialog.java b/core/src/mindustry/ui/dialogs/CustomRulesDialog.java index e3112cf3b6..4c54569b4d 100644 --- a/core/src/mindustry/ui/dialogs/CustomRulesDialog.java +++ b/core/src/mindustry/ui/dialogs/CustomRulesDialog.java @@ -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; diff --git a/core/src/mindustry/ui/dialogs/LanguageDialog.java b/core/src/mindustry/ui/dialogs/LanguageDialog.java index 495ae5e2ce..eb1b4baf50 100644 --- a/core/src/mindustry/ui/dialogs/LanguageDialog.java +++ b/core/src/mindustry/ui/dialogs/LanguageDialog.java @@ -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.*;