diff --git a/build.gradle b/build.gradle index 1631b6f7e0..c43ee7b6cd 100644 --- a/build.gradle +++ b/build.gradle @@ -470,7 +470,8 @@ project(":tools"){ implementation arcModule("natives:natives-freetype-desktop") implementation arcModule("backends:backend-headless") - implementation("com.google.guava:guava:33.3.1-jre") + implementation "com.google.guava:guava:33.3.1-jre" + implementation 'com.github.javaparser:javaparser-core:3.28.2' } } diff --git a/core/assets/bundles/bundle.properties b/core/assets/bundles/bundle.properties index 4a6bd4feea..2e2b9a89ea 100644 --- a/core/assets/bundles/bundle.properties +++ b/core/assets/bundles/bundle.properties @@ -2453,7 +2453,7 @@ block.world-processor.description = A processor for use in mapmaking. Cannot be block.world-cell.description = A memory cell for use in mapmaking. Cannot be destroyed. block.world-switch.description = A switch block for use in mapmaking. Cannot be destroyed. Can only be interacted by players on the same team. block.shield-projector.description = An unbreakable shield projector for use in mapmaking. -block.large-shield-projector = An unbreakable shield projector for use in mapmaking. +block.large-shield-projector.description = An unbreakable shield projector for use in mapmaking. block.graphite-press.description = Compresses coal into graphite. block.multi-press.description = Compresses coal into graphite. Requires water as coolant. block.silicon-smelter.description = Refines silicon from sand and coal. diff --git a/core/assets/bundles/bundle_be.properties b/core/assets/bundles/bundle_be.properties index a3d08118a6..7d4949db3d 100644 --- a/core/assets/bundles/bundle_be.properties +++ b/core/assets/bundles/bundle_be.properties @@ -2448,7 +2448,7 @@ block.world-processor.description = A processor for use in mapmaking. Cannot be block.world-cell.description = A memory cell for use in mapmaking. Cannot be destroyed. block.world-switch.description = A switch block for use in mapmaking. Cannot be destroyed. Can only be interacted by players on the same team. block.shield-projector.description = An unbreakable shield projector for use in mapmaking. -block.large-shield-projector = An unbreakable shield projector for use in mapmaking. +block.large-shield-projector.description = An unbreakable shield projector for use in mapmaking. block.graphite-press.description = Сцісквае кавалкі вугалю ў чыстыя пласты графіту. block.multi-press.description = Абноўленая версія графітавага прэса. Выкарыстоўвае ваду і энергію для хуткай і эфектыўнай апрацоўкі вугалю. block.silicon-smelter.description = Злучае пясок з чыстым вуглём. Вырабляе крэмній. diff --git a/core/assets/bundles/bundle_bg.properties b/core/assets/bundles/bundle_bg.properties index a602618065..b37fa61544 100644 --- a/core/assets/bundles/bundle_bg.properties +++ b/core/assets/bundles/bundle_bg.properties @@ -2448,7 +2448,7 @@ block.world-processor.description = A processor for use in mapmaking. Cannot be block.world-cell.description = A memory cell for use in mapmaking. Cannot be destroyed. block.world-switch.description = A switch block for use in mapmaking. Cannot be destroyed. Can only be interacted by players on the same team. block.shield-projector.description = An unbreakable shield projector for use in mapmaking. -block.large-shield-projector = An unbreakable shield projector for use in mapmaking. +block.large-shield-projector.description = An unbreakable shield projector for use in mapmaking. block.graphite-press.description = Компресира въглища до графит. block.multi-press.description = Компресира въглища до графит. Изисква вода като охладител. block.silicon-smelter.description = Рафинира силикон от пясък и въглища. diff --git a/core/assets/bundles/bundle_ca.properties b/core/assets/bundles/bundle_ca.properties index 4e27d62d58..8be2af6d6d 100644 --- a/core/assets/bundles/bundle_ca.properties +++ b/core/assets/bundles/bundle_ca.properties @@ -2448,7 +2448,7 @@ block.world-processor.description = A processor for use in mapmaking. Cannot be block.world-cell.description = A memory cell for use in mapmaking. Cannot be destroyed. block.world-switch.description = A switch block for use in mapmaking. Cannot be destroyed. Can only be interacted by players on the same team. block.shield-projector.description = An unbreakable shield projector for use in mapmaking. -block.large-shield-projector = An unbreakable shield projector for use in mapmaking. +block.large-shield-projector.description = An unbreakable shield projector for use in mapmaking. block.graphite-press.description = Comprimeix carbó fins obtenir-ne grafit. block.multi-press.description = Comprimeix carbó fins obtenir-ne grafit. Requereix refrigeració amb aigua. block.silicon-smelter.description = Refina silici a partir de sorra i carbó. diff --git a/core/assets/bundles/bundle_cs.properties b/core/assets/bundles/bundle_cs.properties index 4f2c9cf600..7d6f23d75d 100644 --- a/core/assets/bundles/bundle_cs.properties +++ b/core/assets/bundles/bundle_cs.properties @@ -2448,7 +2448,7 @@ block.world-processor.description = A processor for use in mapmaking. Cannot be block.world-cell.description = A memory cell for use in mapmaking. Cannot be destroyed. block.world-switch.description = A switch block for use in mapmaking. Cannot be destroyed. Can only be interacted by players on the same team. block.shield-projector.description = An unbreakable shield projector for use in mapmaking. -block.large-shield-projector = An unbreakable shield projector for use in mapmaking. +block.large-shield-projector.description = An unbreakable shield projector for use in mapmaking. block.graphite-press.description = Lisuje uhlí na grafit. block.multi-press.description = Lisuje uhlí na grafit. Vyžaduje vodu jako chladivo. block.silicon-smelter.description = Rafinuje křemík z písku a uhlí. diff --git a/core/assets/bundles/bundle_da.properties b/core/assets/bundles/bundle_da.properties index 1e6f8406c0..374d090013 100644 --- a/core/assets/bundles/bundle_da.properties +++ b/core/assets/bundles/bundle_da.properties @@ -2448,7 +2448,7 @@ block.world-processor.description = A processor for use in mapmaking. Cannot be block.world-cell.description = A memory cell for use in mapmaking. Cannot be destroyed. block.world-switch.description = A switch block for use in mapmaking. Cannot be destroyed. Can only be interacted by players on the same team. block.shield-projector.description = An unbreakable shield projector for use in mapmaking. -block.large-shield-projector = An unbreakable shield projector for use in mapmaking. +block.large-shield-projector.description = An unbreakable shield projector for use in mapmaking. block.graphite-press.description = Presser kulstykker sammen til ark af ren grafit. block.multi-press.description = En opgraderet version af kul-valsen. Bruger vand og strøm til hurtigere at prodcere en større mængde grafit per kulstykke. block.silicon-smelter.description = Smelter sand om til ren silicium ved brug af kul. diff --git a/core/assets/bundles/bundle_de.properties b/core/assets/bundles/bundle_de.properties index fa7209fc1d..7098c2a463 100644 --- a/core/assets/bundles/bundle_de.properties +++ b/core/assets/bundles/bundle_de.properties @@ -2448,7 +2448,7 @@ block.world-processor.description = A processor for use in mapmaking. Cannot be block.world-cell.description = A memory cell for use in mapmaking. Cannot be destroyed. block.world-switch.description = A switch block for use in mapmaking. Cannot be destroyed. Can only be interacted by players on the same team. block.shield-projector.description = An unbreakable shield projector for use in mapmaking. -block.large-shield-projector = An unbreakable shield projector for use in mapmaking. +block.large-shield-projector.description = An unbreakable shield projector for use in mapmaking. block.graphite-press.description = Komprimiert Kohle zu Graphit. block.multi-press.description = Komprimiert Kohle zu Graphit. Braucht für die Kühlung Wasser. block.silicon-smelter.description = Stellt aus Sand und Kohle Silizium her. diff --git a/core/assets/bundles/bundle_es.properties b/core/assets/bundles/bundle_es.properties index 382423378b..38a40261cb 100644 --- a/core/assets/bundles/bundle_es.properties +++ b/core/assets/bundles/bundle_es.properties @@ -2448,7 +2448,7 @@ block.world-processor.description = A processor for use in mapmaking. Cannot be block.world-cell.description = A memory cell for use in mapmaking. Cannot be destroyed. block.world-switch.description = A switch block for use in mapmaking. Cannot be destroyed. Can only be interacted by players on the same team. block.shield-projector.description = An unbreakable shield projector for use in mapmaking. -block.large-shield-projector = An unbreakable shield projector for use in mapmaking. +block.large-shield-projector.description = An unbreakable shield projector for use in mapmaking. block.graphite-press.description = Comprime carbón en piezas de grafito puro. block.multi-press.description = Utiliza agua y energía para procesar carbón más eficientemente. block.silicon-smelter.description = Refina silicio procesando arena y carbón. diff --git a/core/assets/bundles/bundle_et.properties b/core/assets/bundles/bundle_et.properties index 5714404ce0..8f041be419 100644 --- a/core/assets/bundles/bundle_et.properties +++ b/core/assets/bundles/bundle_et.properties @@ -2448,7 +2448,7 @@ block.world-processor.description = A processor for use in mapmaking. Cannot be block.world-cell.description = A memory cell for use in mapmaking. Cannot be destroyed. block.world-switch.description = A switch block for use in mapmaking. Cannot be destroyed. Can only be interacted by players on the same team. block.shield-projector.description = An unbreakable shield projector for use in mapmaking. -block.large-shield-projector = An unbreakable shield projector for use in mapmaking. +block.large-shield-projector.description = An unbreakable shield projector for use in mapmaking. block.graphite-press.description = Surub söekamakaid õhukesteks grafiidilehtedeks. block.multi-press.description = Grafiidipressi täiustatud versioon, mis kasutab vett ja energiat kiiremaks ja tõhusamaks töötlemiseks. block.silicon-smelter.description = Toodab räni, redutseerides liiva puhta söega. diff --git a/core/assets/bundles/bundle_eu.properties b/core/assets/bundles/bundle_eu.properties index b8e52ff3de..b0f433c3ab 100644 --- a/core/assets/bundles/bundle_eu.properties +++ b/core/assets/bundles/bundle_eu.properties @@ -2448,7 +2448,7 @@ block.world-processor.description = A processor for use in mapmaking. Cannot be block.world-cell.description = A memory cell for use in mapmaking. Cannot be destroyed. block.world-switch.description = A switch block for use in mapmaking. Cannot be destroyed. Can only be interacted by players on the same team. block.shield-projector.description = An unbreakable shield projector for use in mapmaking. -block.large-shield-projector = An unbreakable shield projector for use in mapmaking. +block.large-shield-projector.description = An unbreakable shield projector for use in mapmaking. block.graphite-press.description = Ikatz puskak zanpatzen ditu grafito hutsezko xaflak sortuz. block.multi-press.description = Grafito prentsaren bertsio hobetu bat. Ura eta energia behar ditu ikatza azkar eta eraginkorki prozesatzeko. block.silicon-smelter.description = Hondarra eta ikatz hutsa txikitzen ditu silizioa sortzeko. diff --git a/core/assets/bundles/bundle_fi.properties b/core/assets/bundles/bundle_fi.properties index 8a9ba7476a..9e819928cb 100644 --- a/core/assets/bundles/bundle_fi.properties +++ b/core/assets/bundles/bundle_fi.properties @@ -2448,7 +2448,7 @@ block.world-processor.description = A processor for use in mapmaking. Cannot be block.world-cell.description = A memory cell for use in mapmaking. Cannot be destroyed. block.world-switch.description = A switch block for use in mapmaking. Cannot be destroyed. Can only be interacted by players on the same team. block.shield-projector.description = An unbreakable shield projector for use in mapmaking. -block.large-shield-projector = An unbreakable shield projector for use in mapmaking. +block.large-shield-projector.description = An unbreakable shield projector for use in mapmaking. block.graphite-press.description = Kompressoi hiilenpaloja puhtaiksi grafiittilevyiksi. block.multi-press.description = Päivitetty versio grafiittipuristimesta. Hyödyntää vettä ja energiaa hiilen prosessoimiseen grafiitiksi nopeasti ja tehokkaasti. block.silicon-smelter.description = Pelkistää hiekkaa puhtaalla hiilellä. Tuottaa piitä. diff --git a/core/assets/bundles/bundle_fil.properties b/core/assets/bundles/bundle_fil.properties index b51e2ae813..c8049b2e7c 100644 --- a/core/assets/bundles/bundle_fil.properties +++ b/core/assets/bundles/bundle_fil.properties @@ -2448,7 +2448,7 @@ block.world-processor.description = A processor for use in mapmaking. Cannot be block.world-cell.description = A memory cell for use in mapmaking. Cannot be destroyed. block.world-switch.description = A switch block for use in mapmaking. Cannot be destroyed. Can only be interacted by players on the same team. block.shield-projector.description = An unbreakable shield projector for use in mapmaking. -block.large-shield-projector = An unbreakable shield projector for use in mapmaking. +block.large-shield-projector.description = An unbreakable shield projector for use in mapmaking. block.graphite-press.description = Pinipilit ang mga tipak ng karbon sa purong mga piraso ng grapayt. block.multi-press.description = Isang na-upgrade na bersyon ng graphite press. Gumagamit ng tubig at kapangyarihan upang iproseso ang karbon nang mabilis at mahusay. block.silicon-smelter.description = Binabawasan ang buhangin na may purong karbon. Gumagawa ng silikon. diff --git a/core/assets/bundles/bundle_fr.properties b/core/assets/bundles/bundle_fr.properties index 5d65f1691e..957ccf19cf 100644 --- a/core/assets/bundles/bundle_fr.properties +++ b/core/assets/bundles/bundle_fr.properties @@ -2448,7 +2448,7 @@ block.world-processor.description = A processor for use in mapmaking. Cannot be block.world-cell.description = A memory cell for use in mapmaking. Cannot be destroyed. block.world-switch.description = A switch block for use in mapmaking. Cannot be destroyed. Can only be interacted by players on the same team. block.shield-projector.description = An unbreakable shield projector for use in mapmaking. -block.large-shield-projector = An unbreakable shield projector for use in mapmaking. +block.large-shield-projector.description = An unbreakable shield projector for use in mapmaking. block.graphite-press.description = Compresse le charbon en graphite. block.multi-press.description = Une version améliorée de la presse à graphite, utilisant de l'eau et de l'électricité pour traiter le charbon plus rapidement et plus efficacement. block.silicon-smelter.description = Raffine du silicium avec du sable et du charbon. diff --git a/core/assets/bundles/bundle_hu.properties b/core/assets/bundles/bundle_hu.properties index 35a402deec..49a7248f31 100644 --- a/core/assets/bundles/bundle_hu.properties +++ b/core/assets/bundles/bundle_hu.properties @@ -2448,7 +2448,7 @@ block.world-processor.description = A processor for use in mapmaking. Cannot be block.world-cell.description = A memory cell for use in mapmaking. Cannot be destroyed. block.world-switch.description = A switch block for use in mapmaking. Cannot be destroyed. Can only be interacted by players on the same team. block.shield-projector.description = An unbreakable shield projector for use in mapmaking. -block.large-shield-projector = An unbreakable shield projector for use in mapmaking. +block.large-shield-projector.description = An unbreakable shield projector for use in mapmaking. block.graphite-press.description = Grafittá préseli a szenet. block.multi-press.description = Grafittá sajtolja a szenet. Hűtéséhez víz szükséges. block.silicon-smelter.description = A homokot és a szenet szilíciummá finomítja. diff --git a/core/assets/bundles/bundle_id_ID.properties b/core/assets/bundles/bundle_id_ID.properties index 2fcd594e22..c06d3691c9 100644 --- a/core/assets/bundles/bundle_id_ID.properties +++ b/core/assets/bundles/bundle_id_ID.properties @@ -2448,7 +2448,7 @@ block.world-processor.description = A processor for use in mapmaking. Cannot be block.world-cell.description = A memory cell for use in mapmaking. Cannot be destroyed. block.world-switch.description = A switch block for use in mapmaking. Cannot be destroyed. Can only be interacted by players on the same team. block.shield-projector.description = An unbreakable shield projector for use in mapmaking. -block.large-shield-projector = An unbreakable shield projector for use in mapmaking. +block.large-shield-projector.description = An unbreakable shield projector for use in mapmaking. block.graphite-press.description = Memadatkan bongkahan batu bara menjadi lempengan grafit. block.multi-press.description = Memadatkan bongkahan batu bara menjadi lempengan grafit. Memerlukan air sebagai pendingin. block.silicon-smelter.description = Memproduksi silikon dari pasir dan batu bara. diff --git a/core/assets/bundles/bundle_it.properties b/core/assets/bundles/bundle_it.properties index 73deb3954a..7ac99fd57c 100644 --- a/core/assets/bundles/bundle_it.properties +++ b/core/assets/bundles/bundle_it.properties @@ -2448,7 +2448,7 @@ block.world-processor.description = A processor for use in mapmaking. Cannot be block.world-cell.description = A memory cell for use in mapmaking. Cannot be destroyed. block.world-switch.description = A switch block for use in mapmaking. Cannot be destroyed. Can only be interacted by players on the same team. block.shield-projector.description = An unbreakable shield projector for use in mapmaking. -block.large-shield-projector = An unbreakable shield projector for use in mapmaking. +block.large-shield-projector.description = An unbreakable shield projector for use in mapmaking. block.graphite-press.description = Comprime il carbone in grafite. block.multi-press.description = Comprime il carbone in grafite. Necessite di acqua come refrigerante. block.silicon-smelter.description = Raffina il silicio da sabbia e carbone. diff --git a/core/assets/bundles/bundle_ja.properties b/core/assets/bundles/bundle_ja.properties index 76d4b11288..a8e744fccb 100644 --- a/core/assets/bundles/bundle_ja.properties +++ b/core/assets/bundles/bundle_ja.properties @@ -2448,7 +2448,7 @@ block.world-processor.description = A processor for use in mapmaking. Cannot be block.world-cell.description = A memory cell for use in mapmaking. Cannot be destroyed. block.world-switch.description = A switch block for use in mapmaking. Cannot be destroyed. Can only be interacted by players on the same team. block.shield-projector.description = An unbreakable shield projector for use in mapmaking. -block.large-shield-projector = An unbreakable shield projector for use in mapmaking. +block.large-shield-projector.description = An unbreakable shield projector for use in mapmaking. block.graphite-press.description = 石炭を圧縮し、黒鉛を生成します。 block.multi-press.description = 黒鉛圧縮機のアップグレード版です。水と電力を使用して、より効率的に石炭を圧縮します。 block.silicon-smelter.description = 石炭と砂からシリコンを製造します。 diff --git a/core/assets/bundles/bundle_ko.properties b/core/assets/bundles/bundle_ko.properties index 071f4b8936..08099bb20d 100644 --- a/core/assets/bundles/bundle_ko.properties +++ b/core/assets/bundles/bundle_ko.properties @@ -2448,7 +2448,7 @@ block.world-processor.description = A processor for use in mapmaking. Cannot be block.world-cell.description = A memory cell for use in mapmaking. Cannot be destroyed. block.world-switch.description = A switch block for use in mapmaking. Cannot be destroyed. Can only be interacted by players on the same team. block.shield-projector.description = An unbreakable shield projector for use in mapmaking. -block.large-shield-projector = An unbreakable shield projector for use in mapmaking. +block.large-shield-projector.description = An unbreakable shield projector for use in mapmaking. block.graphite-press.description = 석탄을 흑연으로 압축합니다. block.multi-press.description = 석탄을 흑연으로 압축합니다. 냉각수로 물이 필요합니다. block.silicon-smelter.description = 석탄과 모래에서 실리콘을 정제합니다. diff --git a/core/assets/bundles/bundle_lt.properties b/core/assets/bundles/bundle_lt.properties index 363d1d81cc..edbbfa6f94 100644 --- a/core/assets/bundles/bundle_lt.properties +++ b/core/assets/bundles/bundle_lt.properties @@ -2448,7 +2448,7 @@ block.world-processor.description = A processor for use in mapmaking. Cannot be block.world-cell.description = A memory cell for use in mapmaking. Cannot be destroyed. block.world-switch.description = A switch block for use in mapmaking. Cannot be destroyed. Can only be interacted by players on the same team. block.shield-projector.description = An unbreakable shield projector for use in mapmaking. -block.large-shield-projector = An unbreakable shield projector for use in mapmaking. +block.large-shield-projector.description = An unbreakable shield projector for use in mapmaking. block.graphite-press.description = Sukompresuoja anglies gabalus į grynas grafito plokštes. block.multi-press.description = Patobulinta grafito preso versija. Pasitelkia vandenį ir energiją greitam ir efektyviam anglies apdirbimui. block.silicon-smelter.description = Redukuoja smėlį su gryna anglimi. Gamina silicį. diff --git a/core/assets/bundles/bundle_nl.properties b/core/assets/bundles/bundle_nl.properties index 8699f69b27..4f4449dca1 100644 --- a/core/assets/bundles/bundle_nl.properties +++ b/core/assets/bundles/bundle_nl.properties @@ -2448,7 +2448,7 @@ block.world-processor.description = A processor for use in mapmaking. Cannot be block.world-cell.description = A memory cell for use in mapmaking. Cannot be destroyed. block.world-switch.description = A switch block for use in mapmaking. Cannot be destroyed. Can only be interacted by players on the same team. block.shield-projector.description = An unbreakable shield projector for use in mapmaking. -block.large-shield-projector = An unbreakable shield projector for use in mapmaking. +block.large-shield-projector.description = An unbreakable shield projector for use in mapmaking. block.graphite-press.description = Compresses chunks of coal into pure sheets of graphite. block.multi-press.description = An upgraded version of the graphite press. Employs water and power to process coal quickly and efficiently. block.silicon-smelter.description = Reduces sand with highly pure coal in order to produce silicon. diff --git a/core/assets/bundles/bundle_nl_BE.properties b/core/assets/bundles/bundle_nl_BE.properties index f983718453..26d3a6342a 100644 --- a/core/assets/bundles/bundle_nl_BE.properties +++ b/core/assets/bundles/bundle_nl_BE.properties @@ -2448,7 +2448,7 @@ block.world-processor.description = A processor for use in mapmaking. Cannot be block.world-cell.description = A memory cell for use in mapmaking. Cannot be destroyed. block.world-switch.description = A switch block for use in mapmaking. Cannot be destroyed. Can only be interacted by players on the same team. block.shield-projector.description = An unbreakable shield projector for use in mapmaking. -block.large-shield-projector = An unbreakable shield projector for use in mapmaking. +block.large-shield-projector.description = An unbreakable shield projector for use in mapmaking. block.graphite-press.description = Compresses chunks of coal into pure sheets of graphite. block.multi-press.description = An upgraded version of the graphite press. Employs water and power to process coal quickly and efficiently. block.silicon-smelter.description = Reduces sand with highly pure coal in order to produce silicon. diff --git a/core/assets/bundles/bundle_pl.properties b/core/assets/bundles/bundle_pl.properties index 2085d87528..20355e3364 100644 --- a/core/assets/bundles/bundle_pl.properties +++ b/core/assets/bundles/bundle_pl.properties @@ -2448,7 +2448,7 @@ block.world-processor.description = A processor for use in mapmaking. Cannot be block.world-cell.description = A memory cell for use in mapmaking. Cannot be destroyed. block.world-switch.description = A switch block for use in mapmaking. Cannot be destroyed. Can only be interacted by players on the same team. block.shield-projector.description = An unbreakable shield projector for use in mapmaking. -block.large-shield-projector = An unbreakable shield projector for use in mapmaking. +block.large-shield-projector.description = An unbreakable shield projector for use in mapmaking. block.graphite-press.description = Kompresuje kawałki węgla w czyste blaszki grafitu. block.multi-press.description = Ulepszona wersja prasy grafitowej. Dodanie wody wytwarza grafit znacznie szybciej i efektywniej. block.silicon-smelter.description = Redukuje piasek za pomocą wysoce czystego węgla w celu wytworzenia krzemu. diff --git a/core/assets/bundles/bundle_pt_BR.properties b/core/assets/bundles/bundle_pt_BR.properties index af9cbcf706..166c238d65 100644 --- a/core/assets/bundles/bundle_pt_BR.properties +++ b/core/assets/bundles/bundle_pt_BR.properties @@ -2448,7 +2448,7 @@ block.world-processor.description = A processor for use in mapmaking. Cannot be block.world-cell.description = A memory cell for use in mapmaking. Cannot be destroyed. block.world-switch.description = A switch block for use in mapmaking. Cannot be destroyed. Can only be interacted by players on the same team. block.shield-projector.description = An unbreakable shield projector for use in mapmaking. -block.large-shield-projector = An unbreakable shield projector for use in mapmaking. +block.large-shield-projector.description = An unbreakable shield projector for use in mapmaking. block.graphite-press.description = Comprime carvão em Grafite. block.multi-press.description = Comprime carvão em Grafite. Usa água como refrigerador. block.silicon-smelter.description = Refina silício com carvão e areia. diff --git a/core/assets/bundles/bundle_pt_PT.properties b/core/assets/bundles/bundle_pt_PT.properties index 0cb9fa0b42..3c33a942a1 100644 --- a/core/assets/bundles/bundle_pt_PT.properties +++ b/core/assets/bundles/bundle_pt_PT.properties @@ -2448,7 +2448,7 @@ block.world-processor.description = A processor for use in mapmaking. Cannot be block.world-cell.description = A memory cell for use in mapmaking. Cannot be destroyed. block.world-switch.description = A switch block for use in mapmaking. Cannot be destroyed. Can only be interacted by players on the same team. block.shield-projector.description = An unbreakable shield projector for use in mapmaking. -block.large-shield-projector = An unbreakable shield projector for use in mapmaking. +block.large-shield-projector.description = An unbreakable shield projector for use in mapmaking. block.graphite-press.description = Comprime pedaços de carvão em lâminas de grafite puro. block.multi-press.description = Uma versão melhorada da prensa de grafite. Usa água e energia para processar carvão rápida e eficientemente. block.silicon-smelter.description = Reduz areia com carvão puro. Produz silício silicio. diff --git a/core/assets/bundles/bundle_ro.properties b/core/assets/bundles/bundle_ro.properties index 27ffc3e77d..c8dab2eef9 100644 --- a/core/assets/bundles/bundle_ro.properties +++ b/core/assets/bundles/bundle_ro.properties @@ -2448,7 +2448,7 @@ block.world-processor.description = A processor for use in mapmaking. Cannot be block.world-cell.description = A memory cell for use in mapmaking. Cannot be destroyed. block.world-switch.description = A switch block for use in mapmaking. Cannot be destroyed. Can only be interacted by players on the same team. block.shield-projector.description = An unbreakable shield projector for use in mapmaking. -block.large-shield-projector = An unbreakable shield projector for use in mapmaking. +block.large-shield-projector.description = An unbreakable shield projector for use in mapmaking. block.graphite-press.description = Compresează cărbunele în grafit. block.multi-press.description = Compresează cărbunele în grafit. Are nevoie de apă ca răcitor. block.silicon-smelter.description = Rafinează silicon din nisip și cărbune. diff --git a/core/assets/bundles/bundle_ru.properties b/core/assets/bundles/bundle_ru.properties index 9868c67ffc..48ce487b86 100644 --- a/core/assets/bundles/bundle_ru.properties +++ b/core/assets/bundles/bundle_ru.properties @@ -2448,7 +2448,7 @@ block.world-processor.description = A processor for use in mapmaking. Cannot be block.world-cell.description = A memory cell for use in mapmaking. Cannot be destroyed. block.world-switch.description = A switch block for use in mapmaking. Cannot be destroyed. Can only be interacted by players on the same team. block.shield-projector.description = An unbreakable shield projector for use in mapmaking. -block.large-shield-projector = An unbreakable shield projector for use in mapmaking. +block.large-shield-projector.description = An unbreakable shield projector for use in mapmaking. block.graphite-press.description = Сжимает куски угля в листы графита. block.multi-press.description = Сжимает куски угля в листы графита. Требуется вода в качестве охлаждающей жидкости. block.silicon-smelter.description = Синтезирует кремний из песка и угля. diff --git a/core/assets/bundles/bundle_sr.properties b/core/assets/bundles/bundle_sr.properties index 196aa4e999..c869a2b060 100644 --- a/core/assets/bundles/bundle_sr.properties +++ b/core/assets/bundles/bundle_sr.properties @@ -2448,7 +2448,7 @@ block.world-processor.description = A processor for use in mapmaking. Cannot be block.world-cell.description = A memory cell for use in mapmaking. Cannot be destroyed. block.world-switch.description = A switch block for use in mapmaking. Cannot be destroyed. Can only be interacted by players on the same team. block.shield-projector.description = An unbreakable shield projector for use in mapmaking. -block.large-shield-projector = An unbreakable shield projector for use in mapmaking. +block.large-shield-projector.description = An unbreakable shield projector for use in mapmaking. block.graphite-press.description = Pritiska ugalj, pretvarajući ga u grafit. block.multi-press.description = Pritiska ugalj, pretvarajući ga u grafit. Zahteva vodu za hlađenje. block.silicon-smelter.description = Rafinira silicijum iz peska i uglja. diff --git a/core/assets/bundles/bundle_sv.properties b/core/assets/bundles/bundle_sv.properties index 17607d0940..91a08d2f96 100644 --- a/core/assets/bundles/bundle_sv.properties +++ b/core/assets/bundles/bundle_sv.properties @@ -2448,7 +2448,7 @@ block.world-processor.description = A processor for use in mapmaking. Cannot be block.world-cell.description = A memory cell for use in mapmaking. Cannot be destroyed. block.world-switch.description = A switch block for use in mapmaking. Cannot be destroyed. Can only be interacted by players on the same team. block.shield-projector.description = An unbreakable shield projector for use in mapmaking. -block.large-shield-projector = An unbreakable shield projector for use in mapmaking. +block.large-shield-projector.description = An unbreakable shield projector for use in mapmaking. block.graphite-press.description = Compresses chunks of coal into pure sheets of graphite. block.multi-press.description = An upgraded version of the graphite press. Employs water and power to process coal quickly and efficiently. block.silicon-smelter.description = Reduces sand with pure coal. Produces silicon. diff --git a/core/assets/bundles/bundle_th.properties b/core/assets/bundles/bundle_th.properties index 2cdb693c92..e6d40b503a 100644 --- a/core/assets/bundles/bundle_th.properties +++ b/core/assets/bundles/bundle_th.properties @@ -2448,7 +2448,7 @@ block.world-processor.description = A processor for use in mapmaking. Cannot be block.world-cell.description = A memory cell for use in mapmaking. Cannot be destroyed. block.world-switch.description = A switch block for use in mapmaking. Cannot be destroyed. Can only be interacted by players on the same team. block.shield-projector.description = An unbreakable shield projector for use in mapmaking. -block.large-shield-projector = An unbreakable shield projector for use in mapmaking. +block.large-shield-projector.description = An unbreakable shield projector for use in mapmaking. block.graphite-press.description = อัดก้อนถ่านหินให้เป็นแผ่นกราไฟต์บริสุทธิ์ block.multi-press.description = อัดก้อนถ่านหินให้เป็นแผ่นกราไฟต์บริสุทธิ์ ต้องใช้น้ำมาหล่อเย็นเพื่อเพิ่มประสิทธิภาพการแปรรูป block.silicon-smelter.description = ผลิตซิลิคอนจากการหลอมทรายและถ่านหินเข้าด้วยกัน diff --git a/core/assets/bundles/bundle_tk.properties b/core/assets/bundles/bundle_tk.properties index 74ee9b6599..0c0c858a4a 100644 --- a/core/assets/bundles/bundle_tk.properties +++ b/core/assets/bundles/bundle_tk.properties @@ -2448,7 +2448,7 @@ block.world-processor.description = A processor for use in mapmaking. Cannot be block.world-cell.description = A memory cell for use in mapmaking. Cannot be destroyed. block.world-switch.description = A switch block for use in mapmaking. Cannot be destroyed. Can only be interacted by players on the same team. block.shield-projector.description = An unbreakable shield projector for use in mapmaking. -block.large-shield-projector = An unbreakable shield projector for use in mapmaking. +block.large-shield-projector.description = An unbreakable shield projector for use in mapmaking. block.graphite-press.description = Compresses chunks of coal into pure sheets of graphite. block.multi-press.description = An upgraded version of the graphite press. Employs water and power to process coal quickly and efficiently. block.silicon-smelter.description = Reduces sand with highly pure coke in order to produce silicon. diff --git a/core/assets/bundles/bundle_tr.properties b/core/assets/bundles/bundle_tr.properties index bcda715d91..c37bc33a9f 100644 --- a/core/assets/bundles/bundle_tr.properties +++ b/core/assets/bundles/bundle_tr.properties @@ -2448,7 +2448,7 @@ block.world-processor.description = A processor for use in mapmaking. Cannot be block.world-cell.description = A memory cell for use in mapmaking. Cannot be destroyed. block.world-switch.description = A switch block for use in mapmaking. Cannot be destroyed. Can only be interacted by players on the same team. block.shield-projector.description = An unbreakable shield projector for use in mapmaking. -block.large-shield-projector = An unbreakable shield projector for use in mapmaking. +block.large-shield-projector.description = An unbreakable shield projector for use in mapmaking. block.graphite-press.description = Kömür parçalarını sıkıştırıp saf grafit tabakaları üretir. block.multi-press.description = Grafit presinin yükseltilmiş versiyonu. Kömürün hızlı ve verimli bir şekilde işlenmesi için su ve enerji kullanır. block.silicon-smelter.description = Kumu saf kömürle eritip silisyum üretir. diff --git a/core/assets/bundles/bundle_uk_UA.properties b/core/assets/bundles/bundle_uk_UA.properties index d76535aa16..20dec2698d 100644 --- a/core/assets/bundles/bundle_uk_UA.properties +++ b/core/assets/bundles/bundle_uk_UA.properties @@ -2448,7 +2448,7 @@ block.world-processor.description = A processor for use in mapmaking. Cannot be block.world-cell.description = A memory cell for use in mapmaking. Cannot be destroyed. block.world-switch.description = A switch block for use in mapmaking. Cannot be destroyed. Can only be interacted by players on the same team. block.shield-projector.description = An unbreakable shield projector for use in mapmaking. -block.large-shield-projector = An unbreakable shield projector for use in mapmaking. +block.large-shield-projector.description = An unbreakable shield projector for use in mapmaking. block.graphite-press.description = Англійська назва: Graphite Press\nСтискає вугілля в графіт. block.multi-press.description = Англійська назва: Multi Press\nСтискає вугілля в графіт. Потребує воду для охолодження. block.silicon-smelter.description = Англійська назва: Silicon Smelter\nСинтезує пісок із вугіллям для отримання кремнію. diff --git a/core/assets/bundles/bundle_vi.properties b/core/assets/bundles/bundle_vi.properties index 0d2fca074a..89099d2e20 100644 --- a/core/assets/bundles/bundle_vi.properties +++ b/core/assets/bundles/bundle_vi.properties @@ -2448,7 +2448,7 @@ block.world-processor.description = A processor for use in mapmaking. Cannot be block.world-cell.description = A memory cell for use in mapmaking. Cannot be destroyed. block.world-switch.description = A switch block for use in mapmaking. Cannot be destroyed. Can only be interacted by players on the same team. block.shield-projector.description = An unbreakable shield projector for use in mapmaking. -block.large-shield-projector = An unbreakable shield projector for use in mapmaking. +block.large-shield-projector.description = An unbreakable shield projector for use in mapmaking. block.graphite-press.description = Nén than thành than chì. block.multi-press.description = Nén than thành than chì. Cần nước làm mát. block.silicon-smelter.description = Tinh chế silicon từ cát và than. diff --git a/core/assets/bundles/bundle_zh_CN.properties b/core/assets/bundles/bundle_zh_CN.properties index 3bad28565d..4df01260fc 100644 --- a/core/assets/bundles/bundle_zh_CN.properties +++ b/core/assets/bundles/bundle_zh_CN.properties @@ -2448,7 +2448,7 @@ block.world-processor.description = A processor for use in mapmaking. Cannot be block.world-cell.description = A memory cell for use in mapmaking. Cannot be destroyed. block.world-switch.description = A switch block for use in mapmaking. Cannot be destroyed. Can only be interacted by players on the same team. block.shield-projector.description = An unbreakable shield projector for use in mapmaking. -block.large-shield-projector = An unbreakable shield projector for use in mapmaking. +block.large-shield-projector.description = An unbreakable shield projector for use in mapmaking. block.graphite-press.description = 将煤炭压缩为石墨。 block.multi-press.description = 将煤炭压缩为石墨,需要水进行冷却。 block.silicon-smelter.description = 将沙和煤炭精炼为硅。 diff --git a/core/assets/bundles/bundle_zh_TW.properties b/core/assets/bundles/bundle_zh_TW.properties index 5a0e3e87dc..55f4c8e453 100644 --- a/core/assets/bundles/bundle_zh_TW.properties +++ b/core/assets/bundles/bundle_zh_TW.properties @@ -2448,7 +2448,7 @@ block.world-processor.description = A processor for use in mapmaking. Cannot be block.world-cell.description = A memory cell for use in mapmaking. Cannot be destroyed. block.world-switch.description = A switch block for use in mapmaking. Cannot be destroyed. Can only be interacted by players on the same team. block.shield-projector.description = An unbreakable shield projector for use in mapmaking. -block.large-shield-projector = An unbreakable shield projector for use in mapmaking. +block.large-shield-projector.description = An unbreakable shield projector for use in mapmaking. block.graphite-press.description = 將煤炭壓縮成石墨。 block.multi-press.description = 石墨壓縮機的升級版。利用水和電力快速高效地處理煤炭。 block.silicon-smelter.description = 使用高純度焦炭還原沙子以生產矽。 diff --git a/core/assets/scripts/global.js b/core/assets/scripts/global.js index 22aeeb760e..6ac529fb87 100755 --- a/core/assets/scripts/global.js +++ b/core/assets/scripts/global.js @@ -110,6 +110,7 @@ importPackage(Packages.mindustry.type.unit) importPackage(Packages.mindustry.type.weapons) importPackage(Packages.mindustry.type.weather) importPackage(Packages.mindustry.ui) +importPackage(Packages.mindustry.ui.builder) importPackage(Packages.mindustry.ui.dialogs) importPackage(Packages.mindustry.ui.fragments) importPackage(Packages.mindustry.ui.layout) @@ -182,6 +183,7 @@ const SectorCaptureEvent = Packages.mindustry.game.EventType.SectorCaptureEvent const ClientChatEvent = Packages.mindustry.game.EventType.ClientChatEvent const PlayerChatEvent = Packages.mindustry.game.EventType.PlayerChatEvent const TextInputEvent = Packages.mindustry.game.EventType.TextInputEvent +const MenuBuilderOptionChooseEvent = Packages.mindustry.game.EventType.MenuBuilderOptionChooseEvent const MenuOptionChooseEvent = Packages.mindustry.game.EventType.MenuOptionChooseEvent const ClientServerConnectEvent = Packages.mindustry.game.EventType.ClientServerConnectEvent const ClientPreConnectEvent = Packages.mindustry.game.EventType.ClientPreConnectEvent diff --git a/core/src/mindustry/ai/types/MissileAI.java b/core/src/mindustry/ai/types/MissileAI.java index a9b98eef5e..696a487b7b 100644 --- a/core/src/mindustry/ai/types/MissileAI.java +++ b/core/src/mindustry/ai/types/MissileAI.java @@ -31,7 +31,7 @@ public class MissileAI extends AIController{ var build = unit.buildOn(); //kill instantly on enemy building contact - if(build != null && build.team != unit.team && (build == target || !build.block.underBullets)){ + if(build != null && unit.type.targetGround && build.team != unit.team && (build == target || !build.block.underBullets)){ unit.hasTarget = true; unit.kill(); } diff --git a/core/src/mindustry/core/UI.java b/core/src/mindustry/core/UI.java index 725de94b09..70666c8f20 100644 --- a/core/src/mindustry/core/UI.java +++ b/core/src/mindustry/core/UI.java @@ -34,7 +34,6 @@ import static arc.scene.actions.Actions.*; import static mindustry.Vars.*; public class UI implements ApplicationListener, Loadable{ - private static final StringBuilder buffer = new StringBuilder(); public static String billions, millions, thousands; @@ -81,8 +80,6 @@ public class UI implements ApplicationListener, Loadable{ public CampaignCompleteDialog campaignComplete; public CampaignRulesDialog campaignRules; - public IntMap followUpMenus; - public Cursor drillCursor, unloadCursor, targetCursor, repairCursor; private @Nullable Element lastAnnouncement; @@ -236,7 +233,6 @@ public class UI implements ApplicationListener, Loadable{ fullText = new FullTextDialog(); campaignComplete = new CampaignCompleteDialog(); campaignRules = new CampaignRulesDialog(); - followUpMenus = new IntMap<>(); Group group = Core.scene.root; @@ -671,81 +667,6 @@ public class UI implements ApplicationListener, Loadable{ dialog.show(); } - // TODO REPLACE INTEGER WITH arc.fun.IntCons(int, T) or something like that. - public Dialog newMenuDialog(String title, String message, String[][] options, Cons2 buttonListener){ - return new Dialog(title){{ - setFillParent(true); - removeChild(titleTable); - cont.add(titleTable).width(400f); - - cont.row(); - cont.image().width(400f).pad(2).colspan(2).height(4f).color(Pal.accent).bottom(); - cont.row(); - cont.pane(table -> { - table.add(message).width(400f).wrap().get().setAlignment(Align.center); - table.row(); - - int option = 0; - for(var optionsRow : options){ - if(optionsRow.length == 0) continue; - Table buttonRow = table.row().table().get().row(); - int fullWidth = 400 - (optionsRow.length - 1) * 8; // adjust to count padding as well - int width = fullWidth / optionsRow.length; - int lastWidth = fullWidth - width * (optionsRow.length - 1); // take the rest of space for uneven table - - for(int i = 0; i < optionsRow.length; i++){ - if(optionsRow[i] == null) continue; - - String optionName = optionsRow[i]; - int finalOption = option; - buttonRow.button(optionName, () -> buttonListener.get(finalOption, this)) - .size(i == optionsRow.length - 1 ? lastWidth : width, 50).pad(4); - option++; - } - } - }).growX(); - }}; - } - - /** Shows a menu that fires a callback when an option is selected. If nothing is selected, -1 is returned. */ - public void showMenu(String title, String message, String[][] options, Intc callback){ - Dialog dialog = newMenuDialog(title, message, options, (option, myself) -> { - callback.get(option); - myself.hide(); - }); - dialog.closeOnBack(() -> callback.get(-1)); - dialog.show(); - } - - /** Shows a menu that hides when another followUp-menu is shown or when nothing is selected. - * @see UI#showMenu(String, String, String[][], Intc) */ - public void showFollowUpMenu(int menuId, String title, String message, String[][] options, Intc callback) { - Dialog dialog = newMenuDialog(title, message, options, (option, myself) -> { - callback.get(option); - if(!state.isGame()){ - myself.hide(); - } - }); - dialog.closeOnBack(() -> { - followUpMenus.remove(menuId); - callback.get(-1); - }); - - Dialog oldDialog = followUpMenus.remove(menuId); - if(oldDialog != null){ - dialog.show(Core.scene, null); - oldDialog.hide(null); - }else{ - dialog.show(); - } - followUpMenus.put(menuId, dialog); - } - - public void hideFollowUpMenu(int menuId) { - if(!followUpMenus.containsKey(menuId)) return; - followUpMenus.remove(menuId).hide(); - } - /** * Finds all :name: in a string and replaces them with the icon, if such exists. * Based on TextFormatter::simpleFormat diff --git a/core/src/mindustry/game/EventType.java b/core/src/mindustry/game/EventType.java index a764d4edea..2da853508a 100644 --- a/core/src/mindustry/game/EventType.java +++ b/core/src/mindustry/game/EventType.java @@ -11,6 +11,7 @@ import mindustry.mod.data.*; import mindustry.net.*; import mindustry.net.Packets.*; import mindustry.type.*; +import mindustry.ui.builder.*; import mindustry.world.*; import mindustry.world.blocks.environment.*; import mindustry.world.blocks.storage.CoreBlock.*; @@ -208,6 +209,19 @@ public class EventType{ } } + /** Consider using Menus.registerMenu instead. */ + public static class MenuBuilderOptionChooseEvent{ + public final Player player; + public final int menuId; + public final MenuResult result; + + public MenuBuilderOptionChooseEvent(Player player, int menuId, MenuResult result){ + this.player = player; + this.menuId = menuId; + this.result = result; + } + } + /** Consider using Menus.registerTextInput instead. */ public static class TextInputEvent{ public final Player player; diff --git a/core/src/mindustry/game/MapObjectives.java b/core/src/mindustry/game/MapObjectives.java index 8b21d9b595..e9192a8171 100644 --- a/core/src/mindustry/game/MapObjectives.java +++ b/core/src/mindustry/game/MapObjectives.java @@ -25,7 +25,6 @@ import mindustry.world.*; import mindustry.world.blocks.logic.CanvasBlock.*; import mindustry.world.blocks.logic.LogicDisplay.*; -import java.io.*; import java.lang.annotation.*; import java.util.*; @@ -176,7 +175,7 @@ public class MapObjectives implements Iterable, Eachable, Eachable node){ + node.write(writes); + } + + public static NodeBuilder readUiBuilder(Reads reads){ + return NodeBuilder.read(reads); + } + + public static void writeMenuResult(Writes writes, MenuResult result){ + writes.l(result.token); + + writeString(writes, result.result); + + writes.s(Math.min(MenuResult.maxTotalValues, result.values.size)); + int i = 0; + for(var entry : result.values){ + if(++i > MenuResult.maxTotalValues){ + break; + } + + writes.str(entry.key); + if(entry.value instanceof Float f){ + writes.b(0); + writes.f(f); + }else if(entry.value instanceof Boolean b){ + writes.b(1); + writes.bool(b); + }else if(entry.value instanceof String s){ + writes.b(2); + writes.str(s); + }else{ + throw new ArcRuntimeException("Unknown object type: " + entry.value); + } + } + } + + public static MenuResult readMenuResult(Reads read){ + MenuResult result = new MenuResult(); + + result.token = read.l(); + + //would be nice to count bytes but sadly that's not really viable here + int remainingChars = MenuResult.maxTotalStringLen; + + byte exists = read.b(); + if(exists != 0){ + result.result = read.str(MenuResult.maxResultLen); + remainingChars -= result.result.length(); + } //else, no result, remains null + + int entries = read.us(); + if(entries > MenuResult.maxTotalValues) throw new ArcRuntimeException("UI result entry count exceeds max entries: " + entries); + + for(int i = 0; i < entries; i++){ + if(remainingChars <= 0) throw new ArcRuntimeException("Too many long strings in MenuResult, reduce your amount of elements/IDs."); + String key = read.str(Math.min(remainingChars, MenuResult.maxValueLen)); + remainingChars -= key.length(); + + byte type = read.b(); + + switch(type){ + case 0 -> result.values.put(key, read.f()); + case 1 -> result.values.put(key, read.bool()); + case 2 -> { + if(remainingChars <= 0) throw new ArcRuntimeException("Too many long strings in MenuResult, reduce your amount of elements/IDs."); + String value = read.str(Math.min(remainingChars, MenuResult.maxValueLen)); + remainingChars -= value.length(); + result.values.put(key, value); + } + default -> throw new ArcRuntimeException("Unknown UI entry type: " + type); + } + } + return result; + } + public static void writePayload(Writes writes, Payload payload){ Payload.write(payload, writes); } diff --git a/core/src/mindustry/logic/LogicScript.java b/core/src/mindustry/logic/LogicScript.java index b05850e632..06254e2940 100644 --- a/core/src/mindustry/logic/LogicScript.java +++ b/core/src/mindustry/logic/LogicScript.java @@ -1,11 +1,9 @@ package mindustry.logic; import arc.util.*; -import arc.util.serialization.*; -import arc.util.serialization.Json.*; //TODO: this isn't used in the game yet -public class LogicScript implements JsonSerializable{ +public class LogicScript{ public static final int defaultTimeoutMs = 200, maxTimeoutMs = 2000; /** Timeout in milliseconds. 0 = default timeout. */ @@ -56,6 +54,7 @@ public class LogicScript implements JsonSerializable{ } } + /* @Override public void write(Json json, JsonWriter writer){ json.writeValue(writer, "script", script); @@ -72,5 +71,5 @@ public class LogicScript implements JsonSerializable{ }else{ script = jsonData.asString(); } - } + }*/ } diff --git a/core/src/mindustry/maps/filters/GenerateFilter.java b/core/src/mindustry/maps/filters/GenerateFilter.java index cd0a3690a1..aff22e286a 100644 --- a/core/src/mindustry/maps/filters/GenerateFilter.java +++ b/core/src/mindustry/maps/filters/GenerateFilter.java @@ -5,6 +5,7 @@ import arc.math.*; import arc.scene.ui.*; import arc.util.*; import arc.util.noise.*; +import arc.util.serialization.*; import mindustry.*; import mindustry.annotations.Annotations.*; import mindustry.content.*; @@ -12,9 +13,7 @@ import mindustry.gen.*; import mindustry.world.*; import mindustry.world.blocks.environment.*; -import java.io.*; - -public abstract class GenerateFilter implements Cloneable, Serializable{ +public abstract class GenerateFilter implements Cloneable, AllowSerialization{ public int seed = 0; public void apply(Tiles tiles, GenerateInput in){ diff --git a/core/src/mindustry/mod/ContentParser.java b/core/src/mindustry/mod/ContentParser.java index 684ea4008e..b5e000d0f5 100644 --- a/core/src/mindustry/mod/ContentParser.java +++ b/core/src/mindustry/mod/ContentParser.java @@ -224,7 +224,6 @@ public class ContentParser{ readFields(result, data); return result; }); - //TODO this is untested put(PartProgress.class, (type, data) -> { //simple case: it's a string or number constant if(data.isString()) return field(PartProgress.class, data.asString()); @@ -1119,14 +1118,13 @@ public class ContentParser{ String tname = Strings.capitalize(data.getString("type", "NoiseMesh")); return switch(tname){ - //TODO NoiseMesh is bad case "NoiseMesh" -> new NoiseMesh(planet, - data.getInt("seed", 0), data.getInt("divisions", 1), data.getFloat("radius", 1f), - data.getInt("octaves", 1), data.getFloat("persistence", 0.5f), data.getFloat("scale", 1f), data.getFloat("mag", 0.5f), - Color.valueOf(data.getString("color1", data.getString("color", "ffffff"))), - Color.valueOf(data.getString("color2", data.getString("color", "ffffff"))), - data.getInt("colorOct", 1), data.getFloat("colorPersistence", 0.5f), data.getFloat("colorScale", 1f), - data.getFloat("colorThreshold", 0.5f)); + data.getInt("seed", 0), data.getInt("divisions", 1), data.getFloat("radius", 1f), + data.getInt("octaves", 1), data.getFloat("persistence", 0.5f), data.getFloat("scale", 1f), data.getFloat("mag", 0.5f), + Color.valueOf(data.getString("color1", data.getString("color", "ffffff"))), + Color.valueOf(data.getString("color2", data.getString("color", "ffffff"))), + data.getInt("colorOct", 1), data.getFloat("colorPersistence", 0.5f), data.getFloat("colorScale", 1f), + data.getFloat("colorThreshold", 0.5f)); case "SunMesh" -> { var cvals = data.get("colors").asStringArray(); var colors = new Color[cvals.length]; @@ -1139,9 +1137,9 @@ public class ContentParser{ data.getFloat("colorScale", 1f), colors); } case "HexSkyMesh" -> new HexSkyMesh(planet, - data.getInt("seed", 0), data.getFloat("speed", 0), data.getFloat("radius", 1f), - data.getInt("divisions", 3), Color.valueOf(data.getString("color", "ffffff")), data.getInt("octaves", 1), - data.getFloat("persistence", 0.5f), data.getFloat("scale", 1f), data.getFloat("thresh", 0.5f)); + data.getInt("seed", 0), data.getFloat("speed", 0), data.getFloat("radius", 1f), + data.getInt("divisions", 3), Color.valueOf(data.getString("color", "ffffff")), data.getInt("octaves", 1), + data.getFloat("persistence", 0.5f), data.getFloat("scale", 1f), data.getFloat("thresh", 0.5f)); case "MultiMesh" -> new MultiMesh(parseMeshes(planet, data.get("meshes"))); case "MatMesh" -> new MatMesh(parseMesh(planet, data.get("mesh")), parser.readValue(Mat3D.class, data.get("mat"))); default -> throw new RuntimeException("Unknown mesh type: " + tname); diff --git a/core/src/mindustry/net/Administration.java b/core/src/mindustry/net/Administration.java index 5a7c79a847..03c1fcb8c3 100644 --- a/core/src/mindustry/net/Administration.java +++ b/core/src/mindustry/net/Administration.java @@ -7,6 +7,7 @@ import arc.util.*; import arc.util.Log.*; import arc.util.pooling.Pool.*; import arc.util.pooling.*; +import arc.util.serialization.*; import mindustry.*; import mindustry.ai.*; import mindustry.gen.*; @@ -14,7 +15,6 @@ import mindustry.type.*; import mindustry.world.*; import mindustry.world.blocks.payloads.*; -import java.io.*; import java.util.regex.*; import static mindustry.Vars.*; @@ -635,7 +635,7 @@ public class Administration{ } } - public static class PlayerInfo implements Serializable{ + public static class PlayerInfo implements AllowSerialization{ public String id; public String lastName = "", lastIP = ""; public Seq ips = new Seq<>(); diff --git a/core/src/mindustry/net/ArcNetProvider.java b/core/src/mindustry/net/ArcNetProvider.java index b0b88d7d41..92192e224b 100644 --- a/core/src/mindustry/net/ArcNetProvider.java +++ b/core/src/mindustry/net/ArcNetProvider.java @@ -27,6 +27,8 @@ import java.util.concurrent.*; import static mindustry.Vars.*; public class ArcNetProvider implements NetProvider{ + public static final int clientReadBufferSize = 25_000; + final Client client; final Prov packetSupplier = () -> new DatagramPacket(new byte[512], 512); @@ -59,7 +61,7 @@ public class ArcNetProvider implements NetProvider{ packetSpamLimit = Config.packetSpamLimit.num(); }); - client = new Client(16384, 16384, new PacketSerializer()){ + client = new Client(16384, clientReadBufferSize, new PacketSerializer()){ @Override public void handleNetException(ArcNetException e){ //allow occasional UDP network errors @@ -107,7 +109,8 @@ public class ArcNetProvider implements NetProvider{ } }); - server = new Server(32768, 16384, new PacketSerializer()); + //include extra 16kb headroom for when the write buffer is full + server = new Server(clientReadBufferSize + 16_000, 16384, new PacketSerializer()); server.setMulticast(multicastGroup, multicastPort); server.setDiscoveryHandler((address, handler) -> { ByteBuffer buffer = NetworkIO.writeServerData(); diff --git a/core/src/mindustry/ui/Menus.java b/core/src/mindustry/ui/Menus.java index a268aa7f96..a6a0095fc6 100644 --- a/core/src/mindustry/ui/Menus.java +++ b/core/src/mindustry/ui/Menus.java @@ -1,40 +1,147 @@ package mindustry.ui; import arc.*; +import arc.func.*; +import arc.scene.*; +import arc.scene.ui.*; +import arc.scene.ui.layout.*; import arc.struct.*; import arc.util.*; import mindustry.annotations.Annotations.*; import mindustry.game.EventType.*; import mindustry.gen.*; +import mindustry.graphics.*; +import mindustry.ui.builder.*; +import mindustry.ui.builder.UiBuilder.*; +import mindustry.ui.dialogs.*; import static mindustry.Vars.*; -/** Class for handling menus and notifications across the network. Unstable API! */ +/** Class for handling menus and notifications across the network. */ public class Menus{ + private static IntMap followUpMenus = new IntMap<>(); + private static IntMap menuDialogs = new IntMap<>(); + private static final Seq menuListeners = new Seq<>(); + private static final Seq menuBuilderListeners = new Seq<>(); private static final Seq textInputListeners = new Seq<>(); - /** Register a *global* menu listener. If no option is chosen, the option is returned as -1. */ + /** Register a global menu listener. If no option is chosen, the option is returned as -1. This API is soft-deprecated; use {@link MenuBuilder} and {@link #registerMenuBuilder} instead. */ public static int registerMenu(MenuListener listener){ menuListeners.add(listener); return menuListeners.size - 1; } + /** Register a global menu builder listener. This is not the same as the 'old' MenuListener API; use it with {@link MenuBuilder}. */ + public static int registerMenuBuilder(MenuBuilderListener listener){ + menuBuilderListeners.add(listener); + return menuBuilderListeners.size - 1; + } + /** Register a *global* text input listener. If no text is provided, the text is returned as null. */ public static int registerTextInput(TextInputListener listener){ textInputListeners.add(listener); return textInputListeners.size - 1; } + //use the MenuBuilder version instead of invoking this with Call + @Remote(variants = Variant.both) + public static void menuBuilder(int id, long token, @Nullable String title, boolean hideOnClick, boolean hidePrevious, boolean fillScreen, NodeBuilder builder){ + MenuDialog dialog = new MenuDialog(); + dialog.setFillParent(fillScreen); + if(title != null){ + dialog.title.setText(title); + }else{ + dialog.titleTable.remove(); + } + + dialog.addCloseButton(); + + dialog.update(() -> { + if(!net.active()){ + dialog.hide(); + } + }); + + dialog.hidden(() -> { + menuDialogs.remove(id); + //let the server know nothing was selected + if(!dialog.wasHidden){ + var result = new MenuResult(); + result.token = token; + if(net.client()) Call.menuBuilderChoose(player, id, result); + } + }); + + dialog.originalListener = result -> { + dialog.wasHidden = true; //don't trigger the hidden listener + result.token = token; + if(net.client()) Call.menuBuilderChoose(player, id, result); + + if(hideOnClick){ + dialog.hide(); + } + }; + + dialog.idToElement = UiTreeBuilder.build(dialog.cont, builder, dialog.originalListener); + + var oldDialog = menuDialogs.remove(id); + + if(hidePrevious && oldDialog != null){ + //old dialog exists, replace it (show with no action) + dialog.show(Core.scene, null); + oldDialog.hide(null); + }else{ + dialog.show(); + } + + menuDialogs.put(id, dialog); + } + + @Remote(variants = Variant.both) + public static void menuBuilderUpdate(int id, String tableId, NodeBuilder builder){ + MenuDialog dialog = menuDialogs.get(id); + if(dialog == null) return; + Element element = dialog.idToElement.get(tableId); + Table table = element instanceof Table t ? t : null; + if(table == null && element instanceof ScrollPane pane && pane.getChildren().size > 0 && pane.getChildren().first() instanceof Table t) table = t; + if(table == null) return; + + table.clear(); + var newElements = UiTreeBuilder.build(table, builder, result -> { + //forward results to original listener + dialog.originalListener.get(result); + }); + //save new element IDs + dialog.idToElement.putAll(newElements); + } + + @Remote(variants = Variant.both) + public static void hideMenuBuilder(int menuId) { + if(!menuDialogs.containsKey(menuId)) return; + menuDialogs.remove(menuId).hide(); + } + //do not invoke any of the methods below directly, use Call + @Remote(targets = Loc.both, called = Loc.both) + public static void menuBuilderChoose(@Nullable Player player, int menuId, MenuResult results){ + if(player != null){ + Events.fire(new MenuBuilderOptionChooseEvent(player, menuId, results)); + + if(menuId >= 0 && menuId < menuBuilderListeners.size){ + menuBuilderListeners.get(menuId).get(player, results); + } + } + } + @Remote(variants = Variant.both) public static void menu(int menuId, String title, String message, String[][] options){ if(title == null) title = ""; if(message == null) message = ""; if(options == null) options = new String[0][0]; - ui.showMenu(title, message, options, (option) -> Call.menuChoose(player, menuId, option)); + showMenu(title, message, options, (option) -> Call.menuChoose(player, menuId, option)); } @Remote(variants = Variant.both) @@ -43,12 +150,82 @@ public class Menus{ if(message == null) message = ""; if(options == null) options = new String[0][0]; - ui.showFollowUpMenu(menuId, title, message, options, (option) -> Call.menuChoose(player, menuId, option)); + showFollowUpMenu(menuId, title, message, options, (option) -> Call.menuChoose(player, menuId, option)); } @Remote(variants = Variant.both) public static void hideFollowUpMenu(int menuId) { - ui.hideFollowUpMenu(menuId); + if(!followUpMenus.containsKey(menuId)) return; + followUpMenus.remove(menuId).hide(); + } + + static Dialog newMenuDialog(String title, String message, String[][] options, Cons2 buttonListener){ + return new Dialog(title){{ + setFillParent(true); + removeChild(titleTable); + cont.add(titleTable).width(400f); + + cont.row(); + cont.image().width(400f).pad(2).colspan(2).height(4f).color(Pal.accent).bottom(); + cont.row(); + cont.pane(table -> { + table.add(message).width(400f).wrap().get().setAlignment(Align.center); + table.row(); + + int option = 0; + for(var optionsRow : options){ + if(optionsRow.length == 0) continue; + Table buttonRow = table.row().table().get().row(); + int fullWidth = 400 - (optionsRow.length - 1) * 8; // adjust to count padding as well + int width = fullWidth / optionsRow.length; + int lastWidth = fullWidth - width * (optionsRow.length - 1); // take the rest of space for uneven table + + for(int i = 0; i < optionsRow.length; i++){ + if(optionsRow[i] == null) continue; + + String optionName = optionsRow[i]; + int finalOption = option; + buttonRow.button(optionName, () -> buttonListener.get(finalOption, this)) + .size(i == optionsRow.length - 1 ? lastWidth : width, 50).pad(4); + option++; + } + } + }).growX(); + }}; + } + + /** Shows a menu that fires a callback when an option is selected. If nothing is selected, -1 is returned. */ + static void showMenu(String title, String message, String[][] options, Intc callback){ + Dialog dialog = newMenuDialog(title, message, options, (option, myself) -> { + callback.get(option); + myself.hide(); + }); + dialog.closeOnBack(() -> callback.get(-1)); + dialog.show(); + } + + /** Shows a menu that hides when another followUp-menu is shown or when nothing is selected. + * @see #showMenu(String, String, String[][], Intc) */ + static void showFollowUpMenu(int menuId, String title, String message, String[][] options, Intc callback) { + Dialog dialog = newMenuDialog(title, message, options, (option, myself) -> { + callback.get(option); + if(!state.isGame()){ + myself.hide(); + } + }); + dialog.closeOnBack(() -> { + followUpMenus.remove(menuId); + callback.get(-1); + }); + + Dialog oldDialog = followUpMenus.remove(menuId); + if(oldDialog != null){ + dialog.show(Core.scene, null); + oldDialog.hide(null); + }else{ + dialog.show(); + } + followUpMenus.put(menuId, dialog); } @Remote(targets = Loc.both, called = Loc.both) @@ -208,10 +385,25 @@ public class Menus{ } } + static class MenuDialog extends BaseDialog{ + //retained for easy lookup if an element needs to be replaced + ObjectMap idToElement; + Cons originalListener; + boolean wasHidden; + + public MenuDialog(){ + super(""); + } + } + public interface MenuListener{ void get(Player player, int option); } + public interface MenuBuilderListener{ + void get(Player player, MenuResult result); + } + public interface TextInputListener{ void get(Player player, @Nullable String text); } diff --git a/core/src/mindustry/ui/builder/MenuBuilder.java b/core/src/mindustry/ui/builder/MenuBuilder.java new file mode 100644 index 0000000000..3940a362f0 --- /dev/null +++ b/core/src/mindustry/ui/builder/MenuBuilder.java @@ -0,0 +1,82 @@ +package mindustry.ui.builder; + +import arc.util.*; +import mindustry.gen.*; +import mindustry.ui.builder.UiBuilder.*; + +/** Builder for presenting a complex menu to the player. */ +public class MenuBuilder{ + /** Title string; if null, no title is shown at all (the orange bar is removed too) */ + public @Nullable String title; + /** If false, the dialog will not immediately be hidden when a button with a click listener is clicked. keep in mind that users will be able to spam buttons. */ + public boolean hideOnClick = true; + /** If true (default), existing menus with this ID are hidden. */ + public boolean hideExisting = true; + /** If true (default), the dialog fills the screen. If you're showing something small, make it false. For large complex menus, this has to be true in order to work properly. */ + public boolean fillScreen = true; + /** Unique value that is passed to the result after menu is closed. */ + public long token; + /** ID for menu callback; you can reuse this as long as you have unique keys for button results. */ + public int id; + /** contents of the dialog, must not be null */ + public NodeBuilder ui; + + /** @return a menu constructed from a builder. */ + public static MenuBuilder of(NodeBuilder ui){ + var builder = new MenuBuilder(); + builder.ui = ui; + return builder; + } + + /** @return a menu constructed from a DSL string (see UI DSL docs). */ + public static MenuBuilder of(String dslString){ + var builder = new MenuBuilder(); + builder.ui = UiDslParser.parse(dslString); + return builder; + } + + /** Submits this menu to the relevant player. */ + public void show(Player player){ + Call.menuBuilder(player.con, id, token, title, hideOnClick, hideExisting, fillScreen, ui); + } + + /** Submits this menu to all players. */ + public void showAll(){ + Call.menuBuilder(id, token, title, hideOnClick, hideExisting, fillScreen, ui); + } + + /** Rebuilds a specific Table with the specified elementId. This can be used for updating dialog content based on user interaction.*/ + public void update(Player player, String elementId){ + Call.menuBuilderUpdate(player.con, id, elementId, ui); + } + + public MenuBuilder hideExisting(boolean hideExisting){ + this.hideExisting = hideExisting; + return this; + } + + public MenuBuilder token(long token){ + this.token = token; + return this; + } + + public MenuBuilder title(String title){ + this.title = title; + return this; + } + + public MenuBuilder hideOnClick(boolean hideOnClick){ + this.hideOnClick = hideOnClick; + return this; + } + + public MenuBuilder fillScreen(boolean fillScreen){ + this.fillScreen = fillScreen; + return this; + } + + public MenuBuilder id(int id){ + this.id = id; + return this; + } +} diff --git a/core/src/mindustry/ui/builder/MenuResult.java b/core/src/mindustry/ui/builder/MenuResult.java new file mode 100644 index 0000000000..8a1d5a2fb5 --- /dev/null +++ b/core/src/mindustry/ui/builder/MenuResult.java @@ -0,0 +1,73 @@ +package mindustry.ui.builder; + +import arc.struct.*; +import arc.util.*; + +public class MenuResult{ + /** Maximum length of result string */ + public static final int maxResultLen = 500; + /** Maximum length of a single value in chars. */ + public static final int maxValueLen = 1000; + /** Maximum total length of all string values in chars. This includes IDs of result values. */ + public static final int maxTotalStringLen = 6000; + /** Maximum values count of any type that can be sent. */ + public static final int maxTotalValues = 500; + + /** Token passed to MenuBuilder. */ + public long token; + /** Button that was actually clicked, or null if the dialog was closed (back button). */ + public @Nullable String result = null; + /** Maps ID to element value (slider: value, text field: text, checkbox: checked) */ + public ObjectMap values = new ObjectMap<>(); + + /** Implies cancelled results. */ + public MenuResult(){ + } + + public MenuResult(@Nullable String result){ + this.result = result; + } + + /** @return if the dialog was closed with no choice being made. */ + public boolean wasCancelled(){ + return result == null; + } + + /** @return whether the result ID matches the specified parameter. */ + public boolean is(String text){ + return Structs.eq(text, result); + } + + public String getString(String id, String defaultValue){ + return values.get(id) instanceof String s ? s : defaultValue; + } + + public @Nullable String getString(String id){ + return values.get(id) instanceof String s ? s : null; + } + + public float getFloat(String id, float def){ + return values.get(id) instanceof Float s ? s : def; + } + + public float getFloat(String id){ + return getFloat(id, 0f); + } + + public boolean getBool(String id, boolean def){ + return values.get(id) instanceof Boolean s ? s : def; + } + + public boolean getBool(String id){ + return getBool(id, false); + } + + @Override + public String toString(){ + return "MenuResult{" + + "token=" + token + + ", result='" + result + '\'' + + ", values=" + values + + '}'; + } +} \ No newline at end of file diff --git a/core/src/mindustry/ui/builder/UiBuilder.java b/core/src/mindustry/ui/builder/UiBuilder.java new file mode 100644 index 0000000000..477c46ab87 --- /dev/null +++ b/core/src/mindustry/ui/builder/UiBuilder.java @@ -0,0 +1,304 @@ +package mindustry.ui.builder; + +import arc.struct.*; +import arc.util.*; +import arc.util.io.*; + +/** Builder-style API for building dialogs on servers. It is recommended to statically import all the methods in this class. */ +public class UiBuilder{ + + public static TableBuilder parse(String source){ return UiDslParser.parse(source); } + public static TableBuilder table(){ return new TableBuilder(); } + public static PaneBuilder pane(){ return new PaneBuilder(); } + public static LabelBuilder label(String text){ return new LabelBuilder().text(text); } + public static ImageBuilder image(){ return new ImageBuilder(); } + public static ImageBuilder image(String region){ return new ImageBuilder().region(region); } + public static ButtonBuilder button(String text){ return new ButtonBuilder().text(text); } + public static ImageButtonBuilder imageButton(String icon){ return new ImageButtonBuilder().icon(icon); } + public static FieldBuilder field(String text){ return new FieldBuilder().text(text); } + public static CheckBuilder check(){ return new CheckBuilder(); } + public static CheckBuilder check(String text){ return new CheckBuilder().text(text); } + public static SliderBuilder slider(float min, float max, float step){ return new SliderBuilder().min(min).max(max).step(step); } + public static SpaceBuilder space(){ return new SpaceBuilder(); } + public static DefaultsBuilder defaults(){ return new DefaultsBuilder(); } + public static ButtonTableBuilder buttonTable(){ return new ButtonTableBuilder(); } + + public static abstract class NodeBuilder>{ + final UiKey type; + final Seq entries = new Seq<>(); + + NodeBuilder(UiKey type){ + this.type = type; + } + + @SuppressWarnings("unchecked") + private T self(){ + return (T)this; + } + + protected T prop(UiKey key, String value){ entries.add(new Entry(key, value)); return self(); } + protected T prop(UiKey key, float value){ entries.add(new Entry(key, value)); return self(); } + protected T prop(UiKey key, boolean value){ entries.add(new Entry(key, value)); return self(); } + + public T id(String id){ return prop(UiKey.id, id); } + public T condition(String cond){ return prop(UiKey.condition, cond); } + public T grow(){ return prop(UiKey.grow, true); } + public T growX(){ return prop(UiKey.growX, true); } + public T growY(){ return prop(UiKey.growY, true); } + public T fill(){ return prop(UiKey.fill, true); } + public T fillX(){ return prop(UiKey.fillX, true); } + public T fillY(){ return prop(UiKey.fillY, true); } + public T expand(){ return prop(UiKey.expand, true); } + public T expandX(){ return prop(UiKey.expandX, true); } + public T expandY(){ return prop(UiKey.expandY, true); } + public T width(float w){ return prop(UiKey.width, w); } + public T height(float h){ return prop(UiKey.height, h); } + public T size(float s){ return prop(UiKey.size, s); } + public T minWidth(float w){ return prop(UiKey.minWidth, w); } + public T maxWidth(float w){ return prop(UiKey.maxWidth, w); } + public T minHeight(float h){ return prop(UiKey.minHeight, h); } + public T maxHeight(float h){ return prop(UiKey.maxHeight, h); } + public T pad(float p){ return prop(UiKey.pad, p); } + public T padTop(float p){ return prop(UiKey.padTop, p); } + public T padLeft(float p){ return prop(UiKey.padLeft, p); } + public T padBottom(float p){ return prop(UiKey.padBottom, p); } + public T padRight(float p){ return prop(UiKey.padRight, p); } + public T align(String align){ return prop(UiKey.align, align); } + public T colspan(int c){ return prop(UiKey.colspan, (float)c); } + public T uniform(){ return prop(UiKey.uniform, true); } + public T uniformX(){ return prop(UiKey.uniformX, true); } + public T uniformY(){ return prop(UiKey.uniformY, true); } + + public void write(Writes out){ + out.b(type.ordinal()); + writeEntries(out, entries); + } + + static void writeEntries(Writes out, Seq entries){ + out.s(entries.size); + for(Entry e : entries) writeEntry(out, e); + } + + public static NodeBuilder read(Reads in){ + UiKey type = UiKey.all[in.ub()]; + NodeBuilder node = create(type); + readEntries(in, node.entries); + return node; + } + + static void readEntries(Reads in, Seq entries){ + int count = in.us(); + for(int i = 0; i < count; i++) entries.add(readEntry(in)); + } + + static NodeBuilder create(UiKey type){ + return switch(type){ + case table -> new TableBuilder(); + case pane -> new PaneBuilder(); + case label -> new LabelBuilder(); + case image -> new ImageBuilder(); + case button -> new ButtonBuilder(); + case imageButton -> new ImageButtonBuilder(); + case field -> new FieldBuilder(); + case check -> new CheckBuilder(); + case slider -> new SliderBuilder(); + case space -> new SpaceBuilder(); + case defaults -> new DefaultsBuilder(); + case buttonTable -> new ButtonTableBuilder(); + default -> throw new IllegalArgumentException("Unknown node type " + type); + }; + } + + static void writeEntry(Writes out, Entry e){ + out.s(e.key.ordinal()); + if(e.value instanceof String s){ + out.b(0); + out.str(s); + }else if(e.value instanceof Float f){ + out.b(1); + out.f(f); + }else if(e.value instanceof Boolean b){ + out.b(2); + out.bool(b); + }else if(e.value instanceof NodeBuilder node){ + out.b(3); + node.write(out); + }else{ + throw new ArcRuntimeException("Unsupported entry value type: " + e.value); + } + } + + static Entry readEntry(Reads in){ + UiKey key = UiKey.all[in.us()]; + int tag = in.ub(); + Object value = switch(tag){ + case 0 -> in.str(); + case 1 -> in.f(); + case 2 -> in.bool(); + case 3 -> NodeBuilder.read(in); + default -> throw new ArcRuntimeException("Unknown entry value tag " + tag); + }; + return new Entry(key, value); + } + + String str(UiKey key){ + for(Entry e : entries) if(e.key == key && e.value instanceof String s) return s; + return null; + } + + String str(UiKey key, String def){ + String v = str(key); + return v != null ? v : def; + } + + Float num(UiKey key){ + for(Entry e : entries) if(e.key == key && e.value instanceof Float f) return f; + return null; + } + + float num(UiKey key, float def){ + Float v = num(key); + return v != null ? v : def; + } + + boolean bool(UiKey key){ + return bool(key, false); + } + + boolean bool(UiKey key, boolean def){ + for(Entry e : entries) if(e.key == key && e.value instanceof Boolean b) return b; + return def; + } + } + + static class Entry{ + final UiKey key; + final Object value; + + Entry(UiKey key, Object value){ + this.key = key; + this.value = value; + } + + @Override + public String toString(){ + return "Entry{" + + "key=" + key + + ", value=" + value + + '}'; + } + } + + /** Shared behavior for node types that can contain children (table, pane). */ + public static abstract class ContainerBuilder> extends NodeBuilder{ + ContainerBuilder(UiKey type){ + super(type); + } + + @SuppressWarnings("unchecked") + private T self(){ + return (T)this; + } + + public T row(){ + entries.add(new Entry(UiKey.row, true)); + return self(); + } + + /** Generic nesting point - append any already-configured node as a child. */ + public T add(NodeBuilder child){ + entries.add(new Entry(child.type, child)); + return self(); + } + } + + public static class TableBuilder extends ContainerBuilder{ + TableBuilder(){ super(UiKey.table); } + + public TableBuilder background(String name){ return prop(UiKey.background, name); } + public TableBuilder margin(float m){ return prop(UiKey.margin, m); } + public TableBuilder wrap(boolean wrap){ return prop(UiKey.wrap, wrap); } + } + + public static class PaneBuilder extends ContainerBuilder{ + PaneBuilder(){ super(UiKey.pane); } + + public PaneBuilder style(String style){ return prop(UiKey.style, style); } + } + + public static class LabelBuilder extends NodeBuilder{ + LabelBuilder(){ super(UiKey.label); } + + public LabelBuilder text(String text){ return prop(UiKey.text, text); } + public LabelBuilder wrap(){ return prop(UiKey.wrap, true); } + public LabelBuilder style(String style){ return prop(UiKey.style, style); } + public LabelBuilder labelAlign(String align){ return prop(UiKey.labelAlign, align); } + } + + public static class ImageBuilder extends NodeBuilder{ + ImageBuilder(){ super(UiKey.image); } + + public ImageBuilder region(String region){ return prop(UiKey.region, region); } + public ImageBuilder scaling(Scaling scaling){ return prop(UiKey.scaling, scaling.name()); } + } + + public static class ButtonBuilder extends NodeBuilder{ + ButtonBuilder(){ super(UiKey.button); } + + public ButtonBuilder text(String text){ return prop(UiKey.text, text); } + public ButtonBuilder icon(String icon){ return prop(UiKey.icon, icon); } + public ButtonBuilder style(String style){ return prop(UiKey.style, style); } + public ButtonBuilder clicked(String result){ return prop(UiKey.clicked, result); } + } + + public static class ImageButtonBuilder extends NodeBuilder{ + ImageButtonBuilder(){ super(UiKey.imageButton); } + + public ImageButtonBuilder icon(String icon){ return prop(UiKey.icon, icon); } + public ImageButtonBuilder style(String style){ return prop(UiKey.style, style); } + public ImageButtonBuilder clicked(String result){ return prop(UiKey.clicked, result); } + } + + public static class FieldBuilder extends NodeBuilder{ + FieldBuilder(){ super(UiKey.field); } + + public FieldBuilder text(String text){ return prop(UiKey.text, text); } + public FieldBuilder hint(String hint){ return prop(UiKey.hint, hint); } + public FieldBuilder maxLength(int length){ return prop(UiKey.maxLength, (float)length); } + public FieldBuilder style(String style){ return prop(UiKey.style, style); } + public FieldBuilder enter(String result){ return prop(UiKey.enter, result); } + } + + public static class CheckBuilder extends NodeBuilder{ + CheckBuilder(){ super(UiKey.check); } + + public CheckBuilder text(String text){ return prop(UiKey.text, text); } + public CheckBuilder checked(boolean checked){ return prop(UiKey.checked, checked); } + public CheckBuilder style(String style){ return prop(UiKey.style, style); } + } + + public static class SliderBuilder extends NodeBuilder{ + SliderBuilder(){ super(UiKey.slider); } + + public SliderBuilder min(float min){ return prop(UiKey.min, min); } + public SliderBuilder max(float max){ return prop(UiKey.max, max); } + public SliderBuilder step(float step){ return prop(UiKey.step, step); } + public SliderBuilder defaultValue(float value){ return prop(UiKey.defaultValue, value); } + public SliderBuilder style(String style){ return prop(UiKey.style, style); } + } + + public static class SpaceBuilder extends NodeBuilder{ + SpaceBuilder(){ super(UiKey.space); } + } + + public static class DefaultsBuilder extends NodeBuilder{ + DefaultsBuilder(){ super(UiKey.defaults); } + } + + public static class ButtonTableBuilder extends ContainerBuilder{ + ButtonTableBuilder(){ super(UiKey.buttonTable); } + + public ButtonTableBuilder style(String style){ return prop(UiKey.style, style); } + public ButtonTableBuilder clicked(String result){ return prop(UiKey.clicked, result); } + } +} \ No newline at end of file diff --git a/core/src/mindustry/ui/builder/UiDslParser.java b/core/src/mindustry/ui/builder/UiDslParser.java new file mode 100644 index 0000000000..c0892103c1 --- /dev/null +++ b/core/src/mindustry/ui/builder/UiDslParser.java @@ -0,0 +1,173 @@ +package mindustry.ui.builder; + +import arc.util.*; +import arc.util.serialization.*; +import mindustry.ui.builder.UiBuilder.*; + +/** Parses String -> UiBuilders. */ +public class UiDslParser{ + private final String src; + private int pos; + private boolean lastQuoted; // whether the most recently parsed value was a quoted string + + private UiDslParser(String src){ + this.src = src; + } + + /** Parses source text into a root TableBuilder (the implicit root table's body). */ + public static TableBuilder parse(String source){ + TableBuilder root = new TableBuilder(); + UiDslParser p = new UiDslParser(source); + p.parseStatementsInto(root); + p.skipWs(); + if(!p.atEnd()) throw new SerializationException("Unexpected character '" + p.peek() + "' at index " + p.pos); + return root; + } + + private void parseStatementsInto(NodeBuilder node){ + skipWs(); + while(!atEnd() && peek() != '}'){ + parseStatement(node); + skipWs(); + } + } + + private void parseStatement(NodeBuilder node){ + skipWs(); + if(atEnd() || peek() == '"') throw new SerializationException("Expected identifier at index " + pos); + String ident = readBareToken(); + if(ident.isEmpty()) throw new SerializationException("Unexpected character '" + peek() + "' at index " + pos); + + if(ident.equals("row")){ + node.entries.add(new Entry(UiKey.row, true)); + return; + } + + //TODO: throws on the client upon invalid value, that's probably fine? + UiKey key = UiKey.valueOf(ident); + + //TODO hack: everything before the row ordinal is a node type + boolean isNodeType = key.ordinal() < UiKey.row.ordinal(); + skipWs(); + + //"ident: value" - shorthand node (label: "text") or plain property (width: 100) + if(!atEnd() && peek() == ':'){ + pos++; + String raw = parseValue(); + + if(isNodeType){ + NodeBuilder child = NodeBuilder.create(key); + child.entries.add(new Entry(getShorthandType(key), raw)); + skipWs(); + if(!atEnd() && peek() == '{'){ + pos++; + parseStatementsInto(child); + expect('}'); + } + node.entries.add(new Entry(child.type, child)); + }else{ + node.entries.add(new Entry(key, coerce(raw))); + } + return; + } + + // "ident { ... }" - a node with a block + if(!atEnd() && peek() == '{'){ + pos++; + if(!isNodeType) throw new SerializationException("Unknown node type '" + ident + "' at index " + pos); + NodeBuilder child = NodeBuilder.create(key); + parseStatementsInto(child); + expect('}'); + node.entries.add(new Entry(child.type, child)); + return; + } + + // bare "ident" - childless node (e.g. "space") + if(!isNodeType) throw new SerializationException("Unknown node type '" + ident + "' at index " + pos); + node.entries.add(new Entry(key, NodeBuilder.create(key))); + } + + /** for statements such as image: "cat", the key type is region, not text. */ + private static UiKey getShorthandType(UiKey key){ + return switch(key){ + case image -> UiKey.region; + default -> UiKey.text; + }; + } + + /** Converts a parsed bare token into the most specific type it looks like; quoted strings stay strings. */ + private Object coerce(String raw){ + if(lastQuoted) return raw; + if(raw.equals("true")) return Boolean.TRUE; + if(raw.equals("false")) return Boolean.FALSE; + + float value = Strings.parseFloat(raw, Float.NaN); + if(!Float.isNaN(value)){ + return value; + }else{ + return raw; + } + } + + private String parseValue(){ + skipWs(); + if(!atEnd() && peek() == '"'){ + lastQuoted = true; + return parseString(); + } + lastQuoted = false; + String tok = readBareToken(); + if(tok.isEmpty()) throw new SerializationException("Expected value at index " + pos); + return tok; + } + + private String parseString(){ + pos++; // opening quote + StringBuilder sb = new StringBuilder(); + while(!atEnd() && peek() != '"'){ + char c = src.charAt(pos); + if(c == '\\' && pos + 1 < src.length()){ sb.append(src.charAt(pos + 1)); pos += 2; continue; } + sb.append(c); + pos++; + } + if(atEnd()) throw new SerializationException("Unterminated string"); + pos++; // closing quote + return sb.toString(); + } + + // reads until whitespace/brace/colon; covers identifiers, numbers, and bare-word values alike + private String readBareToken(){ + int start = pos; + while(!atEnd()){ + char c = src.charAt(pos); + if(Character.isWhitespace(c) || c == '{' || c == '}' || c == ':') break; + pos++; + } + return src.substring(start, pos); + } + + private void skipWs(){ + while(!atEnd()){ + char c = src.charAt(pos); + if(Character.isWhitespace(c)){ pos++; continue; } + if(c == '/' && pos + 1 < src.length() && src.charAt(pos + 1) == '/'){ // line comment + while(!atEnd() && src.charAt(pos) != '\n') pos++; + continue; + } + break; + } + } + + private void expect(char c){ + if(atEnd() || peek() != c) throw new SerializationException("Expected '" + c + "', found '" + (atEnd() ? "" : peek()) + "'"); + pos++; + } + + private boolean atEnd(){ + return pos >= src.length(); + } + + private char peek(){ + return src.charAt(pos); + } +} \ No newline at end of file diff --git a/core/src/mindustry/ui/builder/UiKey.java b/core/src/mindustry/ui/builder/UiKey.java new file mode 100644 index 0000000000..78b4abacf7 --- /dev/null +++ b/core/src/mindustry/ui/builder/UiKey.java @@ -0,0 +1,18 @@ +package mindustry.ui.builder; + +/** Enum used for encoding properties as single bytes instead of passing strings around. */ +public enum UiKey{ + // node types + table, pane, label, image, button, imageButton, field, check, slider, space, defaults, buttonTable, row, //NOTE: all node types must be before 'row' + + // node-specific properties + text, wrap, region, icon, scaling, background, margin, id, hint, maxLength, checked, min, max, step, defaultValue, clicked, enter, style, group, condition, + + // cell properties + grow, growX, growY, fill, fillX, fillY, expand, expandX, expandY, + width, height, size, minWidth, maxWidth, minHeight, maxHeight, + pad, padTop, padLeft, padBottom, padRight, + align, labelAlign, colspan, uniform, uniformX, uniformY; + + public static final UiKey[] all = values(); +} \ No newline at end of file diff --git a/core/src/mindustry/ui/builder/UiStyleLookup.java b/core/src/mindustry/ui/builder/UiStyleLookup.java new file mode 100644 index 0000000000..d19d2301bf --- /dev/null +++ b/core/src/mindustry/ui/builder/UiStyleLookup.java @@ -0,0 +1,25 @@ +package mindustry.ui.builder; + +import arc.struct.*; +import arc.util.*; +import mindustry.ui.*; + +/** Grabs style fields by name from {@link Styles}. */ +public class UiStyleLookup{ + private static final ObjectMap, ObjectMap> styles = new ObjectMap<>(); + + public static @Nullable T get(Class type, String name){ + if(styles.isEmpty()) init(); + return (T)styles.get(type, ObjectMap::new).get(name); + } + + private static void init(){ + for(var field : Styles.class.getFields()){ + try{ + styles.get(field.getType(), ObjectMap::new).put(field.getName(), field.get(null)); + }catch(Exception e){ + Log.err(e); + } + } + } +} diff --git a/core/src/mindustry/ui/builder/UiTreeBuilder.java b/core/src/mindustry/ui/builder/UiTreeBuilder.java new file mode 100644 index 0000000000..60bec1be2f --- /dev/null +++ b/core/src/mindustry/ui/builder/UiTreeBuilder.java @@ -0,0 +1,347 @@ +package mindustry.ui.builder; + +import arc.*; +import arc.func.*; +import arc.input.*; +import arc.scene.*; +import arc.scene.event.*; +import arc.scene.style.*; +import arc.scene.ui.*; +import arc.scene.ui.CheckBox.*; +import arc.scene.ui.ImageButton.*; +import arc.scene.ui.Label.*; +import arc.scene.ui.ScrollPane.*; +import arc.scene.ui.Slider.*; +import arc.scene.ui.TextButton.*; +import arc.scene.ui.TextField.*; +import arc.scene.ui.layout.*; +import arc.struct.*; +import arc.util.*; +import mindustry.gen.*; +import mindustry.ui.*; +import mindustry.ui.builder.UiBuilder.*; + +/** Builds a UI into an existing Table from a NodeBuilder tree. */ +public class UiTreeBuilder{ + + public static ObjectMap build(Table root, NodeBuilder builder){ + return build(root, builder, null); + } + + public static ObjectMap build(Table root, NodeBuilder builder, @Nullable Cons resultListener){ + BuildContext ctx = new BuildContext(resultListener); + buildInto(root, builder.entries, ctx); + return ctx.idElements; + } + + private static void buildInto(Table table, Seq entries, BuildContext ctx){ + // defaults{} only affects cells added later within this same block, not nested table/pane blocks + Seq defaults = new Seq<>(); + + for(Entry entry : entries){ + if(entry.key == UiKey.row){ + table.row(); + + }else if(entry.value instanceof NodeBuilder node){ + if(entry.key == UiKey.defaults){ + for(Entry e : node.entries) defaults.add(e); + continue; + } + + String cond = node.str(UiKey.condition); + if(cond != null && !evalCondition(cond)) continue; // condition false - node is not added at all + + Cell cell = addNode(table, entry.key, node, ctx); + if(cell == null) continue; // unknown/unsupported node - skip for forward compat + + for(Entry d : defaults) applyCellProp(cell, d.key, d.value); + for(Entry e : node.entries){ + if(!(e.value instanceof NodeBuilder)) applyCellProp(cell, e.key, e.value); + } + + String id = node.str(UiKey.id); + if(id != null){ + cell.name(id); // assigns element.name via Cell.name() + Object element = cell.get(); + if(element instanceof Element el) ctx.idElements.put(id, el); + } + + }else{ + applyTableProp(table, entry.key, entry.value); // a loose prop on this table's own body (background, margin, ...) + } + } + } + + /** Constructs the element for one node, adds it to the parent table, and returns its cell. */ + private static Cell addNode(Table table, UiKey type, NodeBuilder node, BuildContext ctx){ + return switch(type){ + case table -> { + String bg = node.str(UiKey.background); + Table t = node.bool(UiKey.wrap) ? new WrapTable() : new Table(); + if(bg != null) t.background(findDrawable(bg)); + Float margin = node.num(UiKey.margin); + if(margin != null) t.margin(margin); + + Cell cell = table.add(t); + buildInto(t, node.entries, ctx); // recurse with a fresh defaults scope + yield cell; + } + case pane -> { + Table inner = new Table(); + buildInto(inner, node.entries, ctx); + ScrollPane pane = new ScrollPane(inner); + applyStyle(node, ScrollPaneStyle.class, pane::setStyle); + yield table.add(pane); + } + case label -> { + Label label = new Label(node.str(UiKey.text, "")); + label.setWrap(node.bool(UiKey.wrap)); + String labelAlign = node.str(UiKey.labelAlign); + if(labelAlign != null) label.setAlignment(parseAlign(labelAlign)); + applyStyle(node, LabelStyle.class, label::setStyle); + yield table.add(label); + } + case image -> { + Image image = new Image(findDrawable(node.str(UiKey.region, node.str(UiKey.icon, "error")))); + String scl = node.str(UiKey.scaling); + if(scl != null){ + try{ + Scaling scaling = Scaling.valueOf(scl); + image.setScaling(scaling); + }catch(Exception ignored){} + } + yield table.add(image); + } + case button -> { + TextButton btn = new TextButton(node.str(UiKey.text, "")); + applyStyle(node, TextButtonStyle.class, btn::setStyle); + + String icon = node.str(UiKey.icon); + if(icon != null){ + btn.add(new Image(findDrawable(icon)).setScaling(Scaling.fit)).size(32f); + btn.getCells().reverse(); + } + wireButton(btn, node, ctx); + yield table.add(btn); + } + case imageButton -> { + String icon = node.str(UiKey.icon); + ImageButton btn = new ImageButton(icon != null ? findDrawable(icon) : null); + applyStyle(node, ImageButtonStyle.class, btn::setStyle); + wireButton(btn, node, ctx); + yield table.add(btn); + } + case field -> { + TextField field = new TextField(node.str(UiKey.text, "")); + String hint = node.str(UiKey.hint); + if(hint != null) field.setMessageText(hint); + Float maxLen = node.num(UiKey.maxLength); + field.setMaxLength(maxLen == null ? 1000 : Math.min(maxLen.intValue(), 1000)); + String enter = node.str(UiKey.enter); + if(enter != null) field.keyDown(KeyCode.enter, () -> fireResult(ctx, enter)); + applyStyle(node, TextFieldStyle.class, field::setStyle); + yield table.add(field); + } + case check -> { + CheckBox box = new CheckBox(node.str(UiKey.text, "")); + box.setChecked(node.bool(UiKey.checked)); + String group = node.str(UiKey.group); + if(group != null) ctx.group(group).add(box); + applyStyle(node, CheckBoxStyle.class, box::setStyle); + yield table.add(box); + } + case slider -> { + Slider slider = new Slider(node.num(UiKey.min, 0f), node.num(UiKey.max, 1f), node.num(UiKey.step, 0.1f), false); + Float def = node.num(UiKey.defaultValue); + if(def != null) slider.setValue(def); + applyStyle(node, SliderStyle.class, slider::setStyle); + + String text = node.str(UiKey.text); + boolean useBundle = text != null && text.length() > 0 && text.charAt(0) == '@'; + String bundleKey = useBundle ? text.substring(1) : null; + + Label label = new Label(() -> { + String formatted = Strings.autoFixed(slider.getValue(), 2); + if(bundleKey != null){ + return Core.bundle.format(bundleKey, formatted); + }else if(text != null){ + return text + ": " + formatted; + }else{ + return formatted; + } + }); + label.setAlignment(Align.center); + label.touchable = Touchable.disabled; + label.setStyle(Styles.outlineLabel); + yield table.add(new Stack(slider, label)); + } + case space -> table.add(); + case buttonTable -> { + Button btn = new Button(); + //can use any button style + if(!applyStyle(node, TextButtonStyle.class, btn::setStyle)) applyStyle(node, ImageButtonStyle.class, btn::setStyle); + Cell