Merge remote-tracking branch 'origin/v9' into v9

This commit is contained in:
Anuken
2026-08-26 09:32:20 -04:00
68 changed files with 1008 additions and 552 deletions

View File

@@ -33,7 +33,7 @@ jobs:
- name: Update bundles
if: ${{ github.repository == 'Anuken/Mindustry' }}
run: |
./gradlew updateBundles
./gradlew -DcompileMethodParams updateBundles
if [ -n "$(git status --porcelain)" ]; then
git config --global user.name "Github Actions"
@@ -45,7 +45,7 @@ jobs:
- name: Update scripts, class mappings and map names
if: ${{ github.repository == 'Anuken/Mindustry' }}
run: |
./gradlew updateScripts fixMaps
./gradlew -DcompileMethodParams updateScripts fixMaps
if [ -n "$(git status --porcelain)" ]; then
git config --global user.name "Github Actions"

View File

@@ -20,6 +20,7 @@ import javax.lang.model.element.*;
import javax.lang.model.type.*;
import java.lang.annotation.*;
import java.util.*;
import java.util.regex.*;
@SupportedAnnotationTypes({
"mindustry.annotations.Annotations.EntityDef",
@@ -29,6 +30,11 @@ import java.util.*;
"mindustry.annotations.Annotations.TypeIOHandler"
})
public class EntityProcess extends BaseProcessor{
static final Pattern selfParamPattern = Pattern.compile("this\\.<(.*)>self\\(\\)");
static final Pattern selfPattern = Pattern.compile("self\\(\\)");
static final Pattern yieldPattern = Pattern.compile(" yield ");
static final Pattern missingPattern = Pattern.compile("\\/\\*missing\\*\\/");
Seq<EntityDefinition> definitions = new Seq<>();
Seq<GroupDefinition> groupDefs = new Seq<>();
Seq<Stype> baseComponents;
@@ -77,12 +83,12 @@ public class EntityProcess extends BaseProcessor{
for(Smethod elem : component.methods()){
if(elem.is(Modifier.ABSTRACT) || elem.is(Modifier.NATIVE)) continue;
//get all statements in the method, store them
methodBlocks.put(elem.descString(), elem.tree().getBody().toString()
.replaceAll("this\\.<(.*)>self\\(\\)", "this") //fix parameterized self() calls
.replaceAll("self\\(\\)", "this") //fix self() calls
.replaceAll(" yield ", "") //fix enchanced switch
.replaceAll("\\/\\*missing\\*\\/", "var") //fix vars
);
String body = elem.tree().getBody().toString();
body = selfParamPattern.matcher(body).replaceAll("this"); //fix parameterized self() calls
body = selfPattern.matcher(body).replaceAll("this"); //fix self() calls
body = yieldPattern.matcher(body).replaceAll(""); //fix enhanced switch
body = missingPattern.matcher(body).replaceAll("var"); //fix vars
methodBlocks.put(elem.descString(), body);
}
}
@@ -566,7 +572,15 @@ public class EntityProcess extends BaseProcessor{
String blockName = elem.up().getSimpleName().toString().toLowerCase().replace("comp", "");
//skip empty blocks
if(str.replace("{", "").replace("\n", "").replace("}", "").replace("\t", "").replace(" ", "").isEmpty()){
boolean empty = true;
for(int i = 0; i < str.length(); i++){
char c = str.charAt(i);
if(c != '{' && c != '}' && c != '\n' && c != '\t' && c != ' '){
empty = false;
break;
}
}
if(empty){
continue;
}

View File

@@ -43,7 +43,7 @@ allprojects{
if(!project.hasProperty("versionType")) versionType = 'official'
appName = 'Mindustry'
steamworksVersion = 'da41c05b7a2a44c8625ca0ba7d6ca1a2e73f621a'
rhinoVersion = 'e74ac129d2bff72c1deb536d7d99a0dbb7046d2e'
rhinoVersion = '32395f942976a6d1d21ec9877664554cd5762306'
loadVersionProps = {
return new Properties().with{p -> p.load(file('../core/assets/version.properties').newReader()); return p }
@@ -529,3 +529,9 @@ task resolveDependencies{
}
}
}
if(System.getProperty("compileMethodParams") != null){
project(":core").tasks.withType(JavaCompile).configureEach{
options.compilerArgs << '-parameters'
}
}

View File

@@ -321,8 +321,8 @@ server.admins.none = No admins found!
server.add = Add Server
server.delete = Are you sure you want to delete this server?
server.edit = Edit Server
server.outdated = [scarlet]Outdated Server![]
server.outdated.client = [scarlet]Outdated Client![]
server.outdated = [red]⚠ Outdated Server![]
server.outdated.client = [red]⚠ Outdated Client![]
server.version = [gray]v{0} {1}
server.custombuild = [accent]Custom Build
confirmban = Are you sure you want to ban "{0}[white]"?

View File

@@ -319,8 +319,8 @@ server.admins.none = Няма намерени администратори!
server.add = Добави Сървър
server.delete = Сигурни ли сте, че искате да изтриете този сървър?
server.edit = Редактирай Сървър
server.outdated = [scarlet]Остарял Сървър![]
server.outdated.client = [scarlet]Остарял Клиент![]
server.outdated = [red]⚠ Остарял Сървър![]
server.outdated.client = [red]⚠ Остарял Клиент![]
server.version = [gray]в{0} {1}
server.custombuild = [accent]Персонализирана компилация
confirmban = Сигурни ли сте, че искате да баннете "{0}[white]"?

File diff suppressed because it is too large Load Diff

View File

@@ -319,8 +319,8 @@ server.admins.none = Žádní správci nebyli nalezeni.
server.add = Přidat server
server.delete = Jsi si jistý, že chceš smazat tento server?
server.edit = Upravit server
server.outdated = [scarlet]Zastaralá verze serveru![]
server.outdated.client = [scarlet]Zastaralá verze klienta![]
server.outdated = [red]⚠ Zastaralá verze serveru![]
server.outdated.client = [red]⚠ Zastaralá verze klienta![]
server.version = [gray]Verze: {0} {1}[]
server.custombuild = [accent]Upravená verze hry[]
confirmban = Jsi si jistý, že chceš zakázat hráči "{0}[white]" přístup na server?[]

View File

@@ -319,8 +319,8 @@ server.admins.none = ¡No hay ningún administrador!
server.add = Agregar servidor
server.delete = ¿Realmente quieres borrar este servidor?
server.edit = Editar Servidor
server.outdated = [scarlet]¡Servidor Desactualizado![]
server.outdated.client = [scarlet]¡Cliente Desactualizado![]
server.outdated = [red]⚠ ¡Servidor Desactualizado![]
server.outdated.client = [red]⚠ ¡Cliente Desactualizado![]
server.version = [gray]v{0} {1}
server.custombuild = [accent]Versión personalizada
confirmban = ¿Quieres vetar a "{0}[white]"?

View File

@@ -319,8 +319,8 @@ server.admins.none = Walang nahnap na admin!
server.add = Maglagay ng Server
server.delete = Sigurado ka bang gusto mong tanggalin ang server na ito?
server.edit = Ipalit ang Server
server.outdated = [scarlet]Di-bagong Server![]
server.outdated.client = [scarlet]Di-bagong Client![]
server.outdated = [red]⚠ Di-bagong Server![]
server.outdated.client = [red]⚠ Di-bagong Client![]
server.version = [gray]v{0} {1}
server.custombuild = [accent]Di-karaniwang Build
confirmban = Sigurado ka bang gusto mong itanggal si "{0}[white]"?

View File

@@ -319,8 +319,8 @@ server.admins.none = Aucun administrateur trouvé !
server.add = Ajouter un serveur
server.delete = Êtes-vous sûr de vouloir supprimer ce serveur ?
server.edit = Modifier le serveur
server.outdated = [scarlet]Serveur obsolète ![]
server.outdated.client = [scarlet]Client obsolète ![]
server.outdated = [red]⚠ Serveur obsolète ![]
server.outdated.client = [red]⚠ Client obsolète ![]
server.version = [gray]Version : {0} {1}
server.custombuild = [accent]Version personnalisée
confirmban = Êtes-vous sûr de vouloir bannir "{0}[white]" ?

View File

@@ -319,8 +319,8 @@ server.admins.none = Nem találhatók adminisztrátorok!
server.add = Kiszolgáló hozzáadása
server.delete = Biztosan törlöd ezt a kiszolgálót?
server.edit = Kiszolgáló szerkesztése
server.outdated = [scarlet]Elavult kiszolgáló![]
server.outdated.client = [scarlet]Elavult kliens![]
server.outdated = [red]⚠ Elavult kiszolgáló![]
server.outdated.client = [red]⚠ Elavult kliens![]
server.version = [gray]v{0} {1}
server.custombuild = [accent]Egyéni összeállítás
confirmban = Biztosan kitiltod a(z) „{0}[white]” nevű játékost?

View File

@@ -319,8 +319,8 @@ server.admins.none = Tidak ada admin ditemukan!
server.add = Tambah Server
server.delete = Anda yakin ingin menghapus server ini?
server.edit = Sunting Server
server.outdated = [scarlet]Server Kedaluwarsa![]
server.outdated.client = [scarlet]Klien Kedaluwarsa![]
server.outdated = [red]⚠ Server Kedaluwarsa![]
server.outdated.client = [red]⚠ Klien Kedaluwarsa![]
server.version = [gray]v{0} {1}
server.custombuild = [accent]Bentuk Kustom
confirmban = Anda yakin ingin melarang pemain ini untuk masuk lagi?

View File

@@ -319,8 +319,8 @@ server.admins.none = Nessun amministratore trovato!
server.add = Aggiungi Server
server.delete = Sei sicuro di voler eliminare questo server?
server.edit = Modifica Server
server.outdated = [scarlet]Server Obsoleto![]
server.outdated.client = [scarlet]Client Obsoleto![]
server.outdated = [red]⚠ Server Obsoleto![]
server.outdated.client = [red]⚠ Client Obsoleto![]
server.version = [gray]v{0} {1}
server.custombuild = [accent]Build Personalizzata
confirmban = Sei sicuro di voler bandire "{0}[white]"?

View File

@@ -187,7 +187,7 @@ mod.incompletedependencies.details = 依存関係が無効または欠損して
mod.requiresversion = ゲームのバージョンが必要です: [red]{0}
mod.errors = コンテンツの読み込み中にエラーが発生しました。
mod.errors.map = [scarlet]Errors have occurred loading map content. All custom content has been disabled.[]\nThis map will likely not function correctly.
mod.errors.map = [scarlet]マップコンテンツの読み込み中にエラーが発生しました。すべてのカスタムコンテンツは無効化されています。[]\nこのマップは正しく機能しない可能性があります。
mod.noerrorplay = [scarlet]以下のModにエラーがあります。[] Modを無効化するか、エラーを修正してください。
mod.enable = 有効化
mod.requiresrestart = このModをインストールするためにはゲームの再起動が必要です。
@@ -285,7 +285,7 @@ servers.remote = リモートサーバー
servers.global = コミュニティサーバー
servers.disclaimer = コミュニティサーバーは、[accent]開発者が所有、管理していません。[] \n\nサーバーには全ての年齢層に適さないコンテンツが存在する可能性があります。
servers.connect.confirm = Join the following server?\n\n[accent]{0}[]\n\nOnly connect to servers you trust!
servers.connect.confirm = このサーバーに参加しますか?\n\n[accent]{0}[]\n\n信用できるサーバーにのみ接続するようにしてください!
servers.showhidden = 非表示のサーバーを表示する
server.shown = 表示
server.hidden = 非表示
@@ -319,8 +319,8 @@ server.admins.none = 管理者はいません!
server.add = サーバーを追加
server.delete = サーバーを削除してもよろしいですか?
server.edit = サーバーを編集
server.outdated = [crimson]古いサーバーです![]
server.outdated.client = [crimson]古いクライアントです![]
server.outdated = [red]⚠ 古いサーバーです![]
server.outdated.client = [red]⚠ 古いクライアントです![]
server.version = [lightgray]バージョン: {0} {1}
server.custombuild = [accent]カスタムビルド
confirmban = {0} をBanしてもよろしいですか?
@@ -524,7 +524,7 @@ editor.filters.description = 説明
editor.filters.modname = Modの名前
editor.filters.prioritizemod = Modの優先順位
editor.filters.prioritizecustom = カスタムの優先度
editor.filters.planetselect = Planet Selection
editor.filters.planetselect = 惑星の選択
editor.shiftx = Shift X
editor.shifty = Shift Y
@@ -576,7 +576,7 @@ asset.content.badname = [red]無効な名前です!ほとんどの符号は許
asset.content.import.file = ファイルをインポート
asset.content.import.clipboard = クリップボードからインポート
asset.exists = そのタイプのアセットで、同じパス/名前のものが既に存在します:\n[lightgray]{0}
asset.exists.audio = A sound/music asset with that name is already loaded:\n[lightgray]{0}
asset.exists.audio = その名前の音声/音楽アセットは同じ名前のものが既に存在します:\n[lightgray]{0}
asset.broken = アセットファイルが見つかりませんでした。\nデータの不適切なインポート、または正しくエクスポートされていない保存ファイルが原因である可能性があります。
asset.audio.invalid = 音声ファイルを読み込めませんでした。\n有効な OGG または MP3 ファイルであることを確認してください。
asset.delete.confirm = このアセットを削除してもよろしいですか?
@@ -1440,10 +1440,10 @@ setting.milliseconds = {0} ミリ秒
setting.fullscreen.name = フルスクリーン
setting.fps.name = FPSを表示
setting.console.name = コンソールを有効にする
setting.drawhitboxes.name = Display Hitboxes
setting.showperformance.name = Display Performance Metrics
setting.buildingtimestep.name = Fixed Building Timestep
setting.buildingtimestep.description = Runs buildings on a fixed timestep with constant delta,\nas opposed to every frame with variable delta.\nIf buildings cannot be updated fast enough, the game will slow down.\nIf you don't know what that means, don't touch this option.
setting.drawhitboxes.name = 当たり判定を表示する
setting.showperformance.name = パフォーマンスを表示する
setting.buildingtimestep.name = 固定建築更新間隔
setting.buildingtimestep.description = 建築物を一定のデルタ時間で固定された更新間隔で動作させます。\nフレームごとの可変デルタ時間での更新とは異なります。\n建築物を十分な速度で更新できない場合、ゲームの進行が遅くなります。\nこの説明の意味が分からない場合は、この設定を変更しないでください。
setting.smoothcamera.name = スムーズなカメラ
setting.detach-camera.name = フリーカメラ
setting.detach-camera.description = 有効にすると、カメラがプレイヤーユニットから独立して移動します。\nこの設定はホットキーに割り当て可能です。
@@ -1586,9 +1586,9 @@ keybind.chat_mode.name = チャットモードの変更
keybind.drop_unit.name = ユニット投下
keybind.zoom_minimap.name = ミニマップのズーム
keybind.detach_camera.name = フリーカメラの切り替え
keybind.teleport_cursor.name = Teleport To Cursor (Sandbox)
keybind.debug_hitboxes.name = デバッグヒットボックスの切り替え
keybind.performance_metrics.name = Toggle Performance Metrics
keybind.teleport_cursor.name = カーソル位置にテレポート (Sandbox)
keybind.debug_hitboxes.name = 当たり判定のデバッグ切り替え
keybind.performance_metrics.name = パフォーマンス表示の切り替え
mode.help.title = モード説明
mode.survival.name = サバイバル
mode.survival.description = 通常のモードです。資源も限られる中、自動的にウェーブが進行していきます。\n[gray]マップに敵の出現設定が必要です。
@@ -1646,7 +1646,7 @@ rules.enemyCheat = 敵(赤チーム)の資源の無限化
rules.blockhealthmultiplier = ブロック体力倍率
rules.blockdamagemultiplier = ブロックダメージ倍率
rules.unitfactoryactivation = ユニット工場の起動遅延
rules.unitfactoryactivation.objective = Unit Factories Activating: {0}
rules.unitfactoryactivation.objective = ユニット工場の有効化: {0}
rules.unitbuildspeedmultiplier = ユニット製造速度倍率
rules.unitcostmultiplier = ユニット製造コスト倍率
rules.unithealthmultiplier = ユニット体力倍率
@@ -1682,22 +1682,22 @@ rules.title.experimental = 実験的なゲームプレイ
rules.title.environment = 環境
rules.title.teams = チーム
rules.title.planet = 惑星
rules.title.music = Music
rules.alwaysplaymusic = Always Play Music
rules.alwaysplaymusic.info = Overrides the 'always play music' audio setting.
rules.ambientmusic = Ambient Music
rules.ambientmusic.info = Comma-separated music tracks that randomly play under normal circumstances.\nIf empty, default music will be used.
rules.darkmusic = Conflict Music
rules.darkmusic.info = Comma-separated music tracks that are more likely to play during battle.\nIf empty, default music will be used.
rules.nomusic = Disable Automatic Music
rules.nomusic.info = Turns off all automatic music playback.\nUseful for maps that have custom music played using processors.
rules.title.music = 音楽
rules.alwaysplaymusic = 常に音楽を再生する
rules.alwaysplaymusic.info = '常に音楽を再生する'音楽設定を上書きします。
rules.ambientmusic = 環境音楽
rules.ambientmusic.info = 通常時にランダム再生される音楽トラック(カンマ区切り)。\n空欄の場合はデフォルトの音楽が使用されます。
rules.darkmusic = 戦闘音楽
rules.darkmusic.info = 戦闘中に再生されやすい音楽トラック(カンマ区切り)。\n空欄の場合はデフォルトの音楽が使用されます。
rules.nomusic = 自動音楽再生を無効化する
rules.nomusic.info = すべての自動音楽再生を無効にします。\nプロセッサでカスタム音楽を再生するマップに便利です。
rules.lighting =
rules.fog = 戦場の霧
rules.invasions = 敵セクター侵攻
rules.legacylaunchpads = 旧発射台仕様
rules.legacylaunchpads.info = 着陸台なしで発射台を使用できるようにします(v7.0仕様)。
landingpad.legacy.disabled = [scarlet]\ue815 無効[lightgray](旧発射台は有効)
rules.hidespawns = Hide Enemy Spawns
rules.hidespawns = 敵の出現地点を非表示にする
rules.randomwaveai = 不規則なウェーブAI
rules.pauseDisabled = 一時停止の無効化
rules.fire = 火災
@@ -2447,11 +2447,11 @@ block.illuminator.description = 電力が必要な光源です。
block.message.description = メッセージを保存し、仲間間の通信に使用します。
block.reinforced-message.description = メッセージを保存し、仲間間の通信に使用します。
block.world-message.description = マップ作成時に使用するメッセージブロックです。破壊することはできません。
block.world-processor.description = A processor for use in mapmaking. Cannot be destroyed. Capable of using privileged instructions, such as those that spawn units or change tiles.
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.description = An unbreakable shield projector for use in mapmaking.
block.world-processor.description = マップ作成に使用するプロセッサーです。破壊できません。ユニットの生成やタイルの変更など、特権命令を使用できます。
block.world-cell.description = マップ作成に使用するメモリセルです。破壊できません。
block.world-switch.description = マップ作成に使用するスイッチブロックです。破壊できません。同じチームのプレイヤーのみ操作できます。
block.shield-projector.description = マップ作成に使用する破壊不可能なシールドプロジェクターです。
block.large-shield-projector.description = マップ作成に使用する破壊不可能な大型シールドプロジェクターです。
block.graphite-press.description = 石炭を圧縮し、黒鉛を生成します。
block.multi-press.description = 黒鉛圧縮機のアップグレード版です。水と電力を使用して、より効率的に石炭を圧縮します。
block.silicon-smelter.description = 石炭と砂からシリコンを製造します。
@@ -2775,7 +2775,7 @@ lst.radar = 射程内のユニットを探します。
lst.sensor = 建物やユニットのデータを取得します。
lst.set = 変数を定義し、数値を代入します。
lst.operation = 一つから二つの変数を使って演算をします。
lst.select = Conditionally set a variable to one of two values.
lst.select = 条件に応じて、2つの値のうち1つを変数に設定します。
lst.end = 命令スタックの先頭にジャンプします。
lst.wait = 一定の秒数待ちます。
lst.stop = このプロセッサの実行を停止します。
@@ -2788,7 +2788,7 @@ lst.unitlocate = マップ上の任意の座標にある、特定のタイプの
lst.getblock = 任意の座標のタイルの情報を取得します。
lst.setblock = 任意の座標のタイルの情報を変更します。
lst.spawnunit = 任意の座標にユニットをスポーンさせます。
lst.spawnbullet = Spawns a bullet at a location.
lst.spawnbullet = 指定された位置に弾丸を生成します。
lst.applystatus = ユニットからステータス効果を適用または削除する。
lst.weathersense = 天候の種類が有効かどうかを確認します。
lst.weatherset = 天候の種類の現在の状態を設定します。
@@ -2855,8 +2855,8 @@ lglobal.@clientUnit = クライアントが操作しているユニット
lglobal.@clientName = クライアントのプレイヤー名
lglobal.@clientTeam = クライアントのチームID
lglobal.@clientMobile = クライアントがモバイルなら true、それ以外は false
lglobal.@clientMusicPlaying = True if any music is currently playing
lglobal.@clientCurrentMusic = Name of the current track being played, or null
lglobal.@clientMusicPlaying = 現在何らかの音楽が再生されている場合はtrue
lglobal.@clientCurrentMusic = 現在再生中のトラック名。再生されていない場合はnull
lenum.type = ユニットや建物の種類を取得します。\n例: 任意のルーターに対して、 [accent]@router[] を返します。\n文字列ではありません。
lenum.shoot = 指定した座標に向かって撃ちます。
@@ -3054,4 +3054,4 @@ lenum.solarmultiplier = ソーラーパネルの発電量倍率。
lenum.dragmultiplier = 環境による抵抗倍率。
lenum.ban = 配置または建造できないブロックやユニット。
lenum.unban = ユニットまたはブロックの禁止を解除する。
lenum.musicvolume = Global music volume multiplier.\nClamped to [0, 1].
lenum.musicvolume = 音楽の音量倍率。\n [0, 1]の範囲に制限されます。

View File

@@ -319,8 +319,8 @@ server.admins.none = 관리자를 찾을 수 없습니다!
server.add = 서버 추가
server.delete = 정말로 이 서버를 삭제하시겠습니까?
server.edit = 서버 편집
server.outdated = [scarlet]서버 버전이 낮습니다![]
server.outdated.client = [scarlet]클라이언트 버전이 낮습니다![]
server.outdated = [red]⚠ 서버 버전이 낮습니다![]
server.outdated.client = [red]⚠ 클라이언트 버전이 낮습니다![]
server.version = [gray]v{0} {1}
server.custombuild = [accent]사용자 정의 서버
confirmban = 정말로 "{0}[white]" 을(를) 차단하시겠습니까?

View File

@@ -319,8 +319,8 @@ server.admins.none = Nenhum administrador encontrado!
server.add = Adicionar servidor
server.delete = Você tem certeza que quer deletar esse servidor?
server.edit = Editar Servidor
server.outdated = [scarlet]Servidor Desatualizado![]
server.outdated.client = [scarlet]Cliente Desatualizado![]
server.outdated = [red]⚠ Servidor Desatualizado![]
server.outdated.client = [red]⚠ Cliente Desatualizado![]
server.version = [gray]v{0} {1}
server.custombuild = [accent]Versão Customizada
confirmban = Você tem certeza que quer banir "{0}[white]"?

View File

@@ -319,8 +319,8 @@ server.admins.none = Nu s-au găsit admini!
server.add = Adăugă server
server.delete = Sigur vrei să ștergi acest server?
server.edit = Editează Server
server.outdated = [scarlet]Server Învechit![]
server.outdated.client = [scarlet]Client Învechit![]
server.outdated = [red]⚠ Server Învechit![]
server.outdated.client = [red]⚠ Client Învechit![]
server.version = [gray]v{0} {1}
server.custombuild = [accent]Build Personalizat
confirmban = Sigur vrei să interzici jucătorul "{0}[white]"?

View File

@@ -319,8 +319,8 @@ server.admins.none = Администраторов нет!
server.add = Добавить сервер
server.delete = Вы действительно хотите удалить этот сервер?
server.edit = Редактировать сервер
server.outdated = [scarlet]Устаревший сервер![]
server.outdated.client = [scarlet]Устаревший клиент![]
server.outdated = [red]⚠ Устаревший сервер![]
server.outdated.client = [red]⚠ Устаревший клиент![]
server.version = [gray]Версия: {0} {1}
server.custombuild = [accent]Пользовательская сборка
confirmban = Вы действительно хотите заблокировать игрока «{0}[white]»?

View File

@@ -319,8 +319,8 @@ server.admins.none = Nema pronađenih administratora!
server.add = Dodaj Server
server.delete = Da li želite da obrišete ovaj server?
server.edit = Izmeni Server
server.outdated = [scarlet]Zastareli Server![]
server.outdated.client = [scarlet]Zastareli Klijent![]
server.outdated = [red]⚠ Zastareli Server![]
server.outdated.client = [red]⚠ Zastareli Klijent![]
server.version = [gray]v{0} {1}
server.custombuild = [accent]Svjojehodna Verzija
confirmban = Da li ste sigurni da želite da [scarlet]trajno[] izbacite "{0}[white]"?

View File

@@ -154,10 +154,10 @@ mods.update.available = [lightgray]Cập Nhật Có Sẵn: {0}
mods.update.all = Cập Nhật Tất Cả Bản Mod
mods.downloading = [accent]Đang Tải Xuống Mod: {0}
mod.warning = Mods can execute arbitrary code.\n\nNever download mods from sources you don't trust - they could steal data or compromise your computer.
mod.warning.read = I actually read this
mod.warning.accept = I accept the responsibility
mod.warning.deny = Disable my mods
mod.warning = Các bản mod có thể thực thi mã tùy ý.\n\nKhông bao giờ tải mod từ nguồn bạn không tin tưởng - họ có thể ăn cắp dữ liệu hoặc gây hại đến máy tính của bạn.
mod.warning.read = Tôi thực sự đã đọc diều này
mod.warning.accept = Tôi nhận trách nhiệm
mod.warning.deny = Tắt các mod của tôi
mod.installed = [[Đã cài đặt]
mod.display = [gray]Mod:[accent] {0}
mod.enabled = [lightgray]Đã bật
@@ -319,8 +319,8 @@ server.admins.none = Không tìm thấy quản trị viên nào!
server.add = Thêm máy chủ
server.delete = Bạn có chắc chắn muốn xóa máy chủ này không?
server.edit = Chỉnh sửa máy chủ
server.outdated = [scarlet]Máy chủ lỗi thời![]
server.outdated.client = [scarlet]Máy khách lỗi thời![]
server.outdated = [red]⚠ Máy chủ lỗi thời![]
server.outdated.client = [red]⚠ Máy khách lỗi thời![]
server.version = [gray]v{0} {1}
server.custombuild = [accent]Bản dựng tùy chỉnh
confirmban = Bạn có chắc chắn muốn cấm "{0}[white]"?

View File

@@ -319,8 +319,8 @@ server.admins.none = 找不到管理员!
server.add = 添加服务器
server.delete = 确定要删除这个服务器吗?
server.edit = 编辑服务器
server.outdated = [scarlet]服务器版本过低![]
server.outdated.client = [scarlet]客户端版本过低![]
server.outdated = [red]⚠ 服务器版本过低![]
server.outdated.client = [red]⚠ 客户端版本过低![]
server.version = [gray]v{0} {1}
server.custombuild = [accent]自行编译
confirmban = 确定要封禁“{0}[white]”吗?
@@ -3016,7 +3016,7 @@ lenum.payenter = 进入/降落到单位下方的荷载方块中
lenum.flag = 赋予单位数字形式的标记
lenum.mine = 从某个位置采集矿物
lenum.build = 建造建筑
lenum.getblock = 根据坐标获取建筑物、环境块和环境墙体类型。\n单位必须在位置范围内,否则返回空值。
lenum.getblock = 根据坐标获取建筑物、环境块和环境墙体类型。\n坐标必须在单位的雷达范围内,否则返回空值。
lenum.within = 检查单位是否接近了某个位置
lenum.boost = 开始/停止助推

View File

@@ -48,15 +48,28 @@ const Arrays = Packages.java.util.Arrays
const TextureFilter = Packages.arc.graphics.TextureFilter
const TextureWrap = Packages.arc.graphics.TextureWrap
importPackage(Packages.arc)
importPackage(Packages.arc.assets)
importPackage(Packages.arc.assets.loaders)
importPackage(Packages.arc.audio)
importPackage(Packages.arc.backend.headless)
importPackage(Packages.arc.files)
importPackage(Packages.arc.flabel)
importPackage(Packages.arc.freetype)
importPackage(Packages.arc.func)
importPackage(Packages.arc.fx)
importPackage(Packages.arc.fx.filters)
importPackage(Packages.arc.fx.util)
importPackage(Packages.arc.graphics)
importPackage(Packages.arc.graphics.g2d)
importPackage(Packages.arc.graphics.g3d)
importPackage(Packages.arc.graphics.gl)
importPackage(Packages.arc.input)
importPackage(Packages.arc.math)
importPackage(Packages.arc.math.geom)
importPackage(Packages.arc.mock)
importPackage(Packages.arc.net)
importPackage(Packages.arc.net.dns)
importPackage(Packages.arc.packer)
importPackage(Packages.arc.scene)
importPackage(Packages.arc.scene.actions)
importPackage(Packages.arc.scene.event)
@@ -194,6 +207,7 @@ const LaunchItemEvent = Packages.mindustry.game.EventType.LaunchItemEvent
const SectorInvasionEvent = Packages.mindustry.game.EventType.SectorInvasionEvent
const SectorLoseEvent = Packages.mindustry.game.EventType.SectorLoseEvent
const SaveLoadEvent = Packages.mindustry.game.EventType.SaveLoadEvent
const TextureStreamEvent = Packages.mindustry.game.EventType.TextureStreamEvent
const DataPatchLoadEvent = Packages.mindustry.game.EventType.DataPatchLoadEvent
const WorldLoadEndEvent = Packages.mindustry.game.EventType.WorldLoadEndEvent
const WorldLoadBeginEvent = Packages.mindustry.game.EventType.WorldLoadBeginEvent

View File

@@ -13,6 +13,7 @@ import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.type.*;
import mindustry.world.*;
import mindustry.world.blocks.defense.*;
import mindustry.world.blocks.units.UnitAssembler.*;
import static arc.graphics.g2d.Draw.rect;
@@ -1933,7 +1934,7 @@ public class Fx{
float angle = rand.random(360f);
float lenRand = rand.random(0.5f, 1.2f);
Tmp.v1.trns(angle, circleRad);
for(int s : Mathf.signs){
Drawf.tri(e.x + Tmp.v1.x, e.y + Tmp.v1.y, e.fout() * 10f, e.fout() * 10f * lenRand + 8f, angle + 90f + s * 90f);
}
@@ -2807,12 +2808,10 @@ public class Fx{
shieldBreak = new Effect(40, e -> {
color(e.color);
stroke(3f * e.fout());
if(e.data instanceof ForceFieldAbility ab){
Lines.poly(e.x, e.y, ab.sides, e.rotation + e.fin(), ab.rotation);
return;
}
int sides = e.data instanceof ForceProjector f ? f.sides : e.data instanceof ForceFieldAbility a ? a.sides : 6;
float rotation = e.data instanceof ForceProjector f ? f.shieldRotation : e.data instanceof ForceFieldAbility a ? a.rotation : 6;
Lines.poly(e.x, e.y, 6, e.rotation + e.fin());
Lines.poly(e.x, e.y, sides, e.rotation + e.fin(), rotation);
}).followParent(true),
arcShieldBreak = new Effect(40, e -> {

View File

@@ -476,7 +476,7 @@ public class Logic implements ApplicationListener{
PerfCounter.unitUpdate.begin();
if(editor){
Groups.unit.update(Unitc::isPlayer);
Groups.unit.update(u -> u.isPlayer() || u.spawnedByCore);
}else{
Groups.unit.update();
}

View File

@@ -164,6 +164,20 @@ public class NetClient implements ApplicationListener{
Call.requestAssets(missing.toArray());
});
net.handleClient(TextureStream.class, data -> {
try(DataInputStream in = new DataInputStream(data.stream)){
String name = in.readUTF();
byte[] pngData = in.readAllBytes();
if(!headless){
//empty image data means we're removing the texture instead. see NetServer.removeTexture()
if(pngData.length == 0) state.data.removeTexture(name);
else state.data.addTexture(name, pngData);
}
}catch(IOException e){
Log.err("Failed to read server texture stream", e);
}
});
net.handleClient(StreamBegin.class, data -> {
boolean isWorld = data.type == Net.packetIdWorldStream, isAssets = data.type == Net.packetIdAssetStream;

View File

@@ -552,6 +552,40 @@ public class NetServer implements ApplicationListener{
debug("Packed @ of world data to @ (@ / @)", Strings.formatByteCount(stream.size()), player.name, player.con.address, player.uuid());
}
/**
* Streams a texture to a single connected client. This may take some time if the image is large or if the connection is poor.
* Make sure to call {@link #removeTexture(NetConnection, String)} when the image is no longer needed to prevent resource leaks.
* Use {@link PixmapIO#writePngBytes} to get Pixmap bytes. Respect {@link mindustry.mod.DataPatcher#maxImageSize}.
* Should be called on main thread to ensure correct ordering of sends (multiple with same name), and removals (remove called right after adding). */
public void sendTexture(NetConnection con, String name, byte[] pngData){
var stream = new ByteArrayOutputStream();
NetworkIO.packTexture(stream, name, pngData);
con.sendStreamAsync(new TextureStream(), stream);
}
/** Streams a texture to every connected client.
* See {@link #sendTexture(NetConnection, String, byte[])} for more info. */
public void sendTexture(String name, byte[] pngData){
var stream = new ByteArrayOutputStream();
NetworkIO.packTexture(stream, name, pngData);
for(NetConnection con : net.getConnections()){
con.sendStreamAsync(new TextureStream(), stream);
}
}
/** Removes a texture previously sent with {@link #sendTexture(NetConnection, String, byte[])} from a single client.
* If called while the texture is in use, it will be replaced with a black rectangle. Do not do this.
* Should be called on main thread for the same reasons as sendTexture. */
public void removeTexture(NetConnection con, String name){
sendTexture(con, name, Streams.emptyBytes);
}
/** Removes a texture previously sent with {@link #sendTexture(String, byte[])} from every connected client.
* See {@link #removeTexture(NetConnection, String)} for more info. */
public void removeTexture(String name){
sendTexture(name, Streams.emptyBytes);
}
public void addPacketHandler(String type, Cons2<Player, String> handler){
customPacketHandlers.get(type, Seq::new).add(handler);
}

View File

@@ -329,9 +329,11 @@ public class UI implements ApplicationListener, Loadable{
}});
}else{
new Dialog(titleText){{
cont.margin(30).add(text).padRight(6f);
cont.image().width(400f).pad(2).height(4f).color(Pal.accent);
cont.row();
cont.add(text).row();
TextFieldFilter filter = numbers ? TextFieldFilter.digitsOnly : (f, c) -> true;
TextField field = cont.field(def, t -> {}).size(330f, 50f).get();
TextField field = cont.field(def, t -> {}).size(400f, 50f).get();
field.setMaxLength(textLength);
field.setFilter(filter);
buttons.defaults().size(120, 54).pad(4);

View File

@@ -171,7 +171,7 @@ public class MapContentView implements AssetView{
}
if(list.getChildren().isEmpty()){
list.add("@patch.none");
list.add("@none.found");
}
}

View File

@@ -120,7 +120,7 @@ public class ForceFieldAbility extends Ability{
//self-destructing units can have a shield on death
if(unit.shield > 0f && !wasBroken){
Fx.shieldBreak.at(unit.x, unit.y, radius, unit.type.shieldColor(unit), this);
Fx.shieldBreak.at(unit.x, unit.y, radius, unit.type.shieldColor(unit), sides);
breakSound.at(unit.x, unit.y);
}
}

View File

@@ -34,16 +34,22 @@ public class ShieldArcAbility extends Ability{
//translate bullet back to where it was upon collision
b.trns(-b.vel.x, -b.vel.y);
float penX = Math.abs(paramPos.x - b.x), penY = Math.abs(paramPos.y - b.y);
if(penX > penY){
b.vel.x *= -1;
b.vel.y *= paramField.reflectVel;
}else{
b.vel.y *= -1;
b.vel.x *= paramField.reflectVel;
float nx = b.x - paramPos.x, ny = b.y - paramPos.y;
float nlen = Mathf.len(nx, ny);
if(nlen > 0.0001f){
nx /= nlen;
ny /= nlen;
}
float dot = b.vel.x * nx + b.vel.y * ny;
float rx = b.vel.x - 2f * dot * nx;
float ry = b.vel.y - 2f * dot * ny;
float outDot = rx * nx + ry * ny;
float normalX = outDot * nx, normalY = outDot * ny;
float tangX = rx - normalX, tangY = ry - normalY;
b.vel.set(normalX + tangX * paramField.reflectVel, normalY + tangY * paramField.reflectVel);
b.owner = paramUnit;
b.team = paramUnit.team;
b.time = b.lifetime * paramField.reflectTime;

View File

@@ -119,6 +119,15 @@ public class EventType{
}
}
/** Called when a new texture is received from the server via {@link mindustry.core.NetServer#sendTexture}. */
public static class TextureStreamEvent {
public final String name;
public TextureStreamEvent(String name){
this.name = name;
}
}
public static class SaveLoadEvent{
public final boolean isMap;

View File

@@ -10,8 +10,67 @@ import mindustry.entities.units.*;
import mindustry.world.*;
import mindustry.world.blocks.distribution.*;
import java.util.*;
import static mindustry.Vars.*;
interface BridgePlacer{
boolean unlockedNow();
boolean positionsValid(int x1, int y1, int x2, int y2);
void applyToPlans(BuildPlan cur, BuildPlan other);
}
class ItemBridgePlacer implements BridgePlacer{
private final ItemBridge bridge;
ItemBridgePlacer(ItemBridge bridge){
this.bridge = bridge;
}
@Override
public boolean unlockedNow(){
return bridge.unlockedNow();
}
@Override
public boolean positionsValid(int x1, int y1, int x2, int y2){
return bridge.positionsValid(x1, y1, x2, y2);
}
@Override
public void applyToPlans(BuildPlan cur, BuildPlan other){
cur.block = bridge;
other.block = bridge;
other.config = new Point2(cur.x - other.x, cur.y - other.y);
}
}
class DirectionBridgePlacer implements BridgePlacer{
private final DirectionBridge bridge;
DirectionBridgePlacer(DirectionBridge bridge){
this.bridge = bridge;
}
@Override
public boolean unlockedNow(){
return bridge.unlockedNow();
}
@Override
public boolean positionsValid(int x1, int y1, int x2, int y2){
return bridge.positionsValid(x1, y1, x2, y2);
}
@Override
public void applyToPlans(BuildPlan cur, BuildPlan other){
cur.block = bridge;
other.block = bridge;
}
}
public class Placement{
private static final Seq<BuildPlan> plans1 = new Seq<>();
private static final Seq<Point2> tmpPoints = new Seq<>(), tmpPoints2 = new Seq<>();
@@ -19,6 +78,7 @@ public class Placement{
private static final NormalizeDrawResult drawResult = new NormalizeDrawResult();
private static final Bresenham2 bres = new Bresenham2();
private static final Seq<Point2> points = new Seq<>();
private static final IntSeq tmpInts = new IntSeq(), tmpInts2 = new IntSeq();
//for pathfinding
private static final IntFloatMap costs = new IntFloatMap();
@@ -97,7 +157,7 @@ public class Placement{
boolean addedLast = false;
outer:
for(int i = 0; i < base.size;){
for(int i = 0; i < base.size; ){
var point = base.get(i);
result.add(point);
if(i == base.size - 1) addedLast = true;
@@ -115,7 +175,7 @@ public class Placement{
}
//if it got here, that means nothing was found. try to proceed to the next node anyway
i ++;
i++;
}
if(!addedLast && !base.isEmpty()) result.add(base.peek());
@@ -132,7 +192,8 @@ public class Placement{
calculateBridges(plans, bridge, false, t -> false);
}
public static void calculateBridges(Seq<BuildPlan> plans, ItemBridge bridge, boolean hasJunction, Boolf<Block> avoid){
private static void calculateBridges(Seq<BuildPlan> plans, BridgePlacer bridge, boolean hasJunction, Boolf<Block> avoid){
//common checks
if(isSidePlace(plans) || plans.size == 0) return;
//check for orthogonal placement + unlocked state
@@ -140,139 +201,119 @@ public class Placement{
return;
}
smartCalculateBridges(plans, bridge, hasJunction, avoid);
}
private static void smartCalculateBridges(Seq<BuildPlan> plans, BridgePlacer bridge, boolean hasJunction, Boolf<Block> avoid){
Boolf<BuildPlan> placeable = plan ->
(plan.placeable(player.team()) || (plan.tile() != null && plan.tile().block() == plan.block)) && //don't count the same block as inaccessible
!(plan != plans.first() && plan.build() != null && plan.build().rotation != plan.rotation && avoid.get(plan.tile().block()));
(plan.placeable(player.team()) || (plan.tile() != null && plan.tile().block() == plan.block)) && //don't count the same block as inaccessible
!(plan != plans.first() && plan.build() != null && plan.build().rotation != plan.rotation && avoid.get(plan.tile().block()));
var result = plans1.clear();
var rotated = plans.first().tile() != null && plans.first().tile().absoluteRelativeTo(plans.peek().x, plans.peek().y) == Mathf.mod(plans.first().rotation + 2, 4);
outer:
for(int i = 0; i < plans.size;){
// Use DP for smarter bridge placement
final int conveyorCost = 3;
final int junctionCost = 30;
final int bridgeCost = 200;
final int bridgeOverEmptyPenalty = 5;
final int infCost = Integer.MAX_VALUE / 2; // Avoid overflow when adding
int N = plans.size;
var dp = tmpInts.setSize(2 * N);
var parent = tmpInts2.setSize(2 * N);
Arrays.fill(dp, 0, 2 * N, infCost);
Arrays.fill(parent, 0, 2 * N, -1);
dp[0] = 0;
dp[N] = bridgeCost;
for(int i = 1; i < N; i++){
var cur = plans.get(i);
result.add(cur);
boolean canPlace = placeable.get(cur);
boolean needJunction = hasJunction && (cur.tile() == null || avoid.get(cur.tile().block()));
//gap found
if(i < plans.size - 1 && placeable.get(cur) && !placeable.get(plans.get(i + 1))){
boolean wereSame = true;
if(!canPlace && !needJunction){
continue;
}
//find the closest valid position within range
for(int j = i + 1; j < plans.size; j++){
var other = plans.get(j);
//out of range now, set to current position and keep scanning forward for next occurrence
if(!bridge.positionsValid(cur.x, cur.y, other.x, other.y)){
//add 'missed' conveyors
for(int k = i + 1; k < j; k++){
result.add(plans.get(k));
}
i = j;
continue outer;
}else if(placeable.get(other)){
if(wereSame && hasJunction){
//the gap is fake, it's just conveyors that can be replaced with junctions
i ++;
continue outer;
}else{
//found a link, assign bridges
cur.block = bridge;
other.block = bridge;
if(rotated){
other.config = new Point2(cur.x - other.x, cur.y - other.y);
}else{
cur.config = new Point2(other.x - cur.x, other.y - cur.y);
}
i = j;
continue outer;
}
}
if(other.tile() != null && !avoid.get(other.tile().block())){
wereSame = false;
}
}
//if it got here, that means nothing was found. this likely means there's a bunch of stuff at the end; add it and bail out
for(int j = i + 1; j < plans.size; j++){
result.add(plans.get(j));
}
break;
if(canPlace){
dp[i] = dp[i - 1] + conveyorCost;
}else{
i ++;
dp[i] = dp[i - 1] + junctionCost;
}
parent[i] = i - 1;
if(dp[i] < infCost && canPlace){
dp[N + i] = dp[i] + bridgeCost;
parent[N + i] = i - 1;
}
// Consider bridges from all previous positions
if(i >= 2 && canPlace){
int emptyPenalty = 0;
if(placeable.get(plans.get(i - 1))){
emptyPenalty += bridgeOverEmptyPenalty;
}
for(int j = i - 2; j >= 0; j--){
var other = plans.get(j);
if(!bridge.positionsValid(cur.x, cur.y, other.x, other.y)){
break; // No need to check further back if this one is out of range
}
if(placeable.get(other)){
int cost = dp[N + j] + bridgeCost + emptyPenalty;
if(dp[N + i] > cost){
dp[N + i] = cost;
parent[N + i] = j;
}
emptyPenalty += bridgeOverEmptyPenalty;
}
}
}
if(dp[N + i] < dp[i]){
dp[i] = dp[N + i];
parent[i] = parent[N + i];
}
if(canPlace && dp[i] >= infCost){
// Unable to connect, restart a new segment
dp[i] = 0;
dp[N + i] = bridgeCost;
}
}
// Backtrack to assign bridges
int bridgeMode = 0;
for(int i = N - 1; i >= 0; ){
var cur = plans.get(i);
int p = parent[bridgeMode + i];
if(p == -1 || p == i - 1){
// No connection, connected by conveyor, or junction, no bridge needed
result.add(cur);
bridgeMode = 0;
i--;
}else{
// Connected by bridge, assign it
var other = plans.get(p);
bridge.applyToPlans(cur, other);
result.add(cur);
i = p;
bridgeMode = N;
}
}
result.reverse();
plans.set(result);
}
public static void calculateBridges(Seq<BuildPlan> plans, ItemBridge bridge, boolean hasJunction, Boolf<Block> avoid){
calculateBridges(plans, new ItemBridgePlacer(bridge), hasJunction, avoid);
}
public static void calculateBridges(Seq<BuildPlan> plans, DirectionBridge bridge, boolean hasJunction, Boolf<Block> avoid){
if(isSidePlace(plans) || plans.size == 0) return;
//check for orthogonal placement + unlocked state
if(!(plans.first().x == plans.peek().x || plans.first().y == plans.peek().y) || !bridge.unlockedNow()){
return;
}
Boolf<BuildPlan> placeable = plan ->
(plan.placeable(player.team()) || (plan.tile() != null && plan.tile().block() == plan.block)) && //don't count the same block as inaccessible
!(plan != plans.first() && plan.build() != null && plan.build().rotation != plan.rotation && avoid.get(plan.tile().block()));
var result = plans1.clear();
outer:
for(int i = 0; i < plans.size;){
var cur = plans.get(i);
result.add(cur);
//gap found
if(i < plans.size - 1 && placeable.get(cur) && !placeable.get(plans.get(i + 1))){
boolean wereSame = true;
//find the closest valid position within range
for(int j = i + 1; j < plans.size; j++){
var other = plans.get(j);
//out of range now, set to current position and keep scanning forward for next occurrence
if(!bridge.positionsValid(cur.x, cur.y, other.x, other.y)){
//add 'missed' conveyors
for(int k = i + 1; k < j; k++){
result.add(plans.get(k));
}
i = j;
continue outer;
}else if(placeable.get(other)){
if(wereSame && hasJunction){
//the gap is fake, it's just conveyors that can be replaced with junctions
i ++;
continue outer;
}else{
//found a link, assign bridges
cur.block = bridge;
other.block = bridge;
i = j;
continue outer;
}
}
if(other.tile() != null && !avoid.get(other.tile().block())){
wereSame = false;
}
}
//if it got here, that means nothing was found. this likely means there's a bunch of stuff at the end; add it and bail out
for(int j = i + 1; j < plans.size; j++){
result.add(plans.get(j));
}
break;
}else{
i ++;
}
}
plans.set(result);
calculateBridges(plans, new DirectionBridgePlacer(bridge), hasJunction, avoid);
}
private static float tileHeuristic(Tile tile, Tile other){

View File

@@ -0,0 +1,8 @@
package mindustry.logic;
import arc.struct.*;
public interface LDrawable {
boolean drawable(LExecutor exec);
void draw(LongSeq buffer);
}

View File

@@ -1040,8 +1040,8 @@ public class LExecutor{
@Override
public void run(LExecutor exec){
if(target.building() instanceof LogicDisplayBuild d && d.isValid() && (d.team == exec.team || exec.privileged)){
d.flushCommands(exec.graphicsBuffer);
if(target.building() instanceof LDrawable d && d.drawable(exec)){
d.draw(exec.graphicsBuffer);
}
exec.graphicsBuffer.clear();
}

View File

@@ -10,6 +10,7 @@ import arc.struct.*;
import arc.util.*;
import arc.util.Log.*;
import mindustry.*;
import mindustry.game.EventType.*;
import mindustry.mod.data.*;
import java.io.*;
@@ -21,8 +22,14 @@ public class DataImagePacker{
private static int defaultAtlasDepth = -1;
public static final String regionPrefix = "dp-";
/** Prefix for images pushed dynamically by the server via {@link #addTexture(String, byte[])}, as opposed to map/mod data patches. */
public static final String serverRegionPrefix = "net-";
private @Nullable Seq<AtlasRegion> addedRegions;
/** Textures added at runtime via addTexture(), keyed by their unprefixed name. Tracked separately from patchAtlas so they can be added/removed individually. */
private final ObjectMap<String, Texture> serverImages = new ObjectMap<>();
/** Single threaded executor so that concurrent calls always complete in FIFO order. */
private final ExecutorService textureExecutor = Threads.executor("Server Texture Streamer", 1);
/** Packs a new set of images. If images are already packed, disposes of the old ones. */
public void pack(Seq<ImageAsset> images){
@@ -159,6 +166,63 @@ public class DataImagePacker{
}
addedRegions = null;
}
serverImages.each((name, texture) -> {
Core.atlas.getRegionMap().remove(serverRegionPrefix + name);
Core.atlas.getDrawables().remove(serverRegionPrefix + name);
texture.dispose();
});
serverImages.clear();
}
/** Decodes PNG bytes and registers them into the atlas under "netRegionPrefix + name", replacing any existing image with the same name. Safe to call from any thread. */
public void addTexture(String name, byte[] pngData){
textureExecutor.execute(() -> {
Pixmap pixmap;
try{
pixmap = new Pixmap(pngData);
}catch(Throwable e){
Log.err("Invalid server-provided image data for '" + name + "'", e);
return;
}
if(pixmap.width > DataPatcher.maxImageSize || pixmap.height > DataPatcher.maxImageSize){
Log.err("Server-provided image '" + name + "' exceeds max size of " + DataPatcher.maxImageSize);
pixmap.dispose();
return;
}
Core.app.post(() -> {
removeTexture(name);
Texture texture = new Texture(pixmap);
pixmap.dispose();
var region = new TextureAtlas.AtlasRegion(texture, 0, 0, texture.width, texture.height);
region.name = serverRegionPrefix + name;
serverImages.put(name, texture);
Core.atlas.getRegionMap().put(serverRegionPrefix + name, region);
//fired with the full atlas region name
Events.fire(new TextureStreamEvent(serverRegionPrefix + name));
});
});
}
/** Removes a texture previously added with {@link #addTexture}. Must be called on the main thread. */
private void removeTexture(String name){
Texture texture = serverImages.remove(name);
if(texture != null){
Core.atlas.getRegionMap().remove(serverRegionPrefix + name);
Core.atlas.getDrawables().remove(serverRegionPrefix + name);
texture.dispose();
}
}
/** Queues a texture for removal. Will run after any pending {@link #addTexture} calls so that races do not occur. */
public void removeTextureQueued(String name){
textureExecutor.execute(() -> Core.app.post(() -> removeTexture(name)));
}
public void printStats(PixmapPacker packer){

View File

@@ -10,6 +10,7 @@ import mindustry.*;
import mindustry.ctype.*;
import mindustry.graphics.*;
import mindustry.mod.data.*;
import mindustry.net.*;
public class DataManager{
private DataPatcher patcher = new DataPatcher();
@@ -28,7 +29,6 @@ public class DataManager{
public void reloadContent(boolean reloadArrays){
patcher.unapply(reloadArrays);
patcher.apply(getPatches(), getContent(), reloadArrays);
rebuildOrderedAssets();
@@ -184,6 +184,18 @@ public class DataManager{
reloadImages();
}
/** Adds/replaces a single image pushed by the server at runtime, independent of map/mod data patches.
* Use {@link mindustry.core.NetServer#sendTexture(NetConnection, String, byte[])} to send a texture to connected clients. */
public void addTexture(String name, byte[] pngData){
if(!Vars.headless) packer.addTexture(name, pngData);
}
/** Removes a texture previously added with {@link #addTexture}.
* Use {@link mindustry.core.NetServer#removeTexture(NetConnection, String)} to remove a texture from connected clients. */
public void removeTexture(String name){
if(!Vars.headless) packer.removeTextureQueued(name);
}
public void reloadAudio(){
soundLoader.unload();
soundLoader.load(getSounds(), getMusic());

View File

@@ -8,6 +8,7 @@ import arc.util.serialization.Json.*;
import arc.util.serialization.*;
import arc.util.serialization.Jval.*;
import mindustry.*;
import mindustry.content.*;
import mindustry.core.*;
import mindustry.ctype.*;
import mindustry.entities.part.*;
@@ -92,7 +93,7 @@ public class DataPatcher{
public void apply(Seq<PatchAsset> patches, Seq<ContentAsset> content, boolean reloadContentWorld){
//if you're un-applying data patches, and it throws an error, just crash. this is not recoverable.
if(applied){
unapply();
unapply(reloadContentWorld);
applied = false;
}
@@ -130,7 +131,8 @@ public class DataPatcher{
Fi file = new Fi(asset.path);
//this is very important for resizing various arrays used in the game
if((asset.type == ContentType.item || asset.type == ContentType.liquid)){
//checking for blocks is also important, as those can be added/removed, and corresponding blocks need to be updated
if(asset.type == ContentType.item || asset.type == ContentType.liquid || asset.type == ContentType.block){
needsArrayFix = true;
}
@@ -333,9 +335,27 @@ public class DataPatcher{
if(!Vars.headless && Vars.ui != null && Vars.ui.editor != null && Vars.ui.editor.isShown()){
int wh = Vars.world.width() * Vars.world.height();
for(int i = 0; i < wh; i++){
var b = Vars.world.tiles.geti(i).build;
if(b != null && b.items != null) b.items.checkArrayCapacity(items);
if(b != null && b.liquids != null) b.liquids.checkArrayCapacity(items);
Tile tile = Vars.world.tiles.geti(i);
//stale checks for floor/overlay
if(tile.floor().removed) tile.setFloor(getReplacementBlock(tile.floor()).asFloor());
if(tile.overlay().removed) tile.setOverlay(getReplacementBlock(tile.overlay()).asFloor());
if(tile.block().removed){
Block mapped = getReplacementBlock(tile.block());
//tile refers to stale content; get rid of it.
if(mapped == Blocks.air){
tile.remove();
}else{
//update internal reference of block to point to the new one with correct ID
tile.updateBlockReference(mapped);
}
}
var b = tile.build;
if(b == null || !tile.isCenter()) continue;
if(b.items != null) b.items.checkArrayCapacity(items);
if(b.liquids != null) b.liquids.checkArrayCapacity(items);
}
}
@@ -344,6 +364,17 @@ public class DataPatcher{
needsArrayFix = false;
}
private static Block getReplacementBlock(Block existing){
Block other = Vars.content.block(existing.name);
if(other == null) return Blocks.air;
//make sure they are type compatible
if(other.getClass() == existing.getClass()){
return other;
}
//could not find an equivalent, clear it
return Blocks.air;
}
void visit(Object object){
visitStack.add(object);
if(object instanceof Content c && usedpatches.add(c)){

View File

@@ -93,7 +93,7 @@ public class Scripts implements Disposable{
private boolean run(String script, String file, boolean wrap){
try{
if(currentMod != null){
if(currentMod != null && wrap){
//inject script info into file
context.evaluateString(scope, "modName = \"" + currentMod.name + "\"\nscriptName = \"" + file + "\"", "initscript.js", 1);
}

View File

@@ -426,6 +426,7 @@ public class ArcNetProvider implements NetProvider{
@Override
public void sendStream(Streamable stream){
//listeners are processed in the order they're added and each reads into the buffer greedily before the next gets a turn, so concurrent streams are sent in FIFO order
connection.addListener(new InputStreamSender(stream.stream, 1024){
int id;

View File

@@ -22,7 +22,7 @@ import static mindustry.Vars.*;
@SuppressWarnings("unchecked")
public class Net{
public static final int packetIdAssetStream, packetIdWorldStream;
public static final int packetIdAssetStream, packetIdWorldStream, packetIdTextureStream;
private static Seq<Prov<? extends Packet>> packetProvs = new Seq<>();
private static Seq<Class<? extends Packet>> packetClasses = new Seq<>();
@@ -50,6 +50,7 @@ public class Net{
registerPacket(ConnectPacket::new);
registerPacket(AssetRequirementStream::new);
packetIdAssetStream = registerPacket(AssetStream::new);
packetIdTextureStream = registerPacket(TextureStream::new);
//register generated packet classes
Call.registerPackets();

View File

@@ -163,6 +163,15 @@ public class NetworkIO{
}
}
public static void packTexture(OutputStream os, String name, byte[] pngData){
try(DataOutputStream stream = new DataOutputStream(os)){
stream.writeUTF(name);
stream.write(pngData);
}catch(IOException e){
throw new RuntimeException(e);
}
}
public static ByteBuffer writeServerData(){
String name = (headless ? Config.serverName.string() : player.name);
String description = headless && !Config.desc.string().equals("off") ? Config.desc.string() : "";

View File

@@ -71,6 +71,10 @@ public class Packets{
}
public static class TextureStream extends Streamable{
}
public static class AssetRequirementStream extends Streamable{
}

View File

@@ -806,7 +806,7 @@ public class UnitType extends UnlockableContent implements Senseable{
stats.add(Stat.targetsAir, targetAir);
stats.add(Stat.targetsGround, targetGround);
if(abilities.any()){
if(abilities.contains(a -> a.display)){
stats.add(Stat.abilities, StatValues.abilities(abilities));
}
@@ -2000,6 +2000,7 @@ public class UnitType extends UnlockableContent implements Senseable{
public static class UnitEngine implements Cloneable{
public float x, y, radius, rotation;
public @Nullable Color color;
public UnitEngine(float x, float y, float radius, float rotation){
this.x = x;
@@ -2018,7 +2019,7 @@ public class UnitType extends UnlockableContent implements Senseable{
if(scale <= 0.0001f) return;
float rot = unit.rotation - 90;
Color color = type.engineColor == null ? unit.team.color : type.engineColor;
Color color = this.color != null ? this.color : type.engineColor == null ? unit.team.color : type.engineColor;
Tmp.v1.set(x, y).rotate(rot);
float ex = Tmp.v1.x, ey = Tmp.v1.y;

View File

@@ -0,0 +1,40 @@
package mindustry.ui;
import arc.func.*;
import arc.scene.ui.*;
import arc.util.*;
import mindustry.gen.*;
public class Elems{
/** Fancy check box with a background. Not a real check box. */
public static Button check(String text, Boolp checked, Boolc listener){
return check(text, checked != null && checked.get(), checked, listener);
}
/** Fancy check box with a background. Not a real check box. */
public static Button check(String text, boolean checked, Boolc listener){
return check(text, checked, null, listener);
}
/** Fancy check box with a background. Not a real check box. */
public static Button check(String text, boolean currentChecked, @Nullable Boolp checked, Boolc listener){
Button box = new Button(Styles.grayt);
box.background(Styles.grayPanel);
box.margin(10f);
box.add(new Image()).update(i -> i.setDrawable(box.isOver() ? (box.isChecked() ? Tex.checkOnOver : Tex.checkOver) : box.isChecked() ? Tex.checkOn : Tex.checkOff))
.size(32f).padRight(8f).padLeft(-4f);
box.add(text);
box.setChecked(currentChecked);
if(checked != null) box.update(() -> box.setChecked(checked.get()));
box.clicked(() -> listener.get(box.isChecked()));
box.left();
return box;
}
}

View File

@@ -83,7 +83,9 @@ public class Menus{
}
};
dialog.idToElement = UiTreeBuilder.build(dialog.cont, builder, dialog.originalListener);
var result = UiTreeBuilder.build(dialog.cont, builder, dialog.originalListener);
dialog.idToElement = result.idElements;
dialog.addImages(result.images);
var oldDialog = menuDialogs.remove(id);
@@ -108,12 +110,13 @@ public class Menus{
if(table == null) return;
table.clear();
var newElements = UiTreeBuilder.build(table, builder, result -> {
var result = UiTreeBuilder.build(table, builder, res -> {
//forward results to original listener
dialog.originalListener.get(result);
dialog.originalListener.get(res);
});
//save new element IDs
dialog.idToElement.putAll(newElements);
//save new element IDs and images
dialog.idToElement.putAll(result.idElements);
dialog.addImages(result.images);
}
@Remote(variants = Variant.both)
@@ -388,12 +391,31 @@ public class Menus{
static class MenuDialog extends BaseDialog{
//retained for easy lookup if an element needs to be replaced
ObjectMap<String, Element> idToElement;
//every streamed image built into this dialog, keyed by the region name
ObjectMap<String, Seq<Image>> images = new ObjectMap<>();
Cons<MenuResult> originalListener;
boolean wasHidden;
static{
// refresh images that are received after the dialog is already opened
Events.on(TextureStreamEvent.class, e -> {
for(var dialog : menuDialogs.values()){
var imgs = dialog.images.get(e.name);
if(imgs != null){
var drawable = Core.atlas.drawable(e.name);
imgs.each(img -> img.setDrawable(drawable));
}
}
});
}
public MenuDialog(){
super("");
}
void addImages(ObjectMap<String, Seq<Image>> images){
images.each((region, imgs) -> this.images.get(region, Seq::new).addAll(imgs));
}
}
public interface MenuListener{

View File

@@ -253,6 +253,7 @@ public class UiBuilder{
ImageBuilder(){ super(UiKey.image); }
public ImageBuilder region(String region){ return prop(UiKey.region, region); }
public ImageBuilder placeholder(String placeholder){ return prop(UiKey.placeholder, placeholder); }
public ImageBuilder scaling(Scaling scaling){ return prop(UiKey.scaling, scaling.name()); }
}
@@ -261,6 +262,7 @@ public class UiBuilder{
public ButtonBuilder text(String text){ return prop(UiKey.text, text); }
public ButtonBuilder icon(String icon){ return prop(UiKey.icon, icon); }
public ButtonBuilder placeholder(String placeholder){ return prop(UiKey.placeholder, placeholder); }
public ButtonBuilder style(String style){ return prop(UiKey.style, style); }
public ButtonBuilder clicked(String result){ return prop(UiKey.clicked, result); }
public ButtonBuilder disabled(boolean disabled){ return prop(UiKey.disabled, disabled); }
@@ -272,6 +274,7 @@ public class UiBuilder{
ImageButtonBuilder(){ super(UiKey.imageButton); }
public ImageButtonBuilder icon(String icon){ return prop(UiKey.icon, icon); }
public ImageButtonBuilder placeholder(String placeholder){ return prop(UiKey.placeholder, placeholder); }
public ImageButtonBuilder style(String style){ return prop(UiKey.style, style); }
public ImageButtonBuilder clicked(String result){ return prop(UiKey.clicked, result); }
public ImageButtonBuilder disabled(boolean disabled){ return prop(UiKey.disabled, disabled); }

View File

@@ -6,7 +6,7 @@ public enum UiKey{
table, pane, stack, 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,
text, wrap, region, icon, placeholder, scaling, background, margin, id, hint, maxLength, checked, min, max, step,
defaultValue, clicked, enter, style, group, condition, color, disabled,
// cell properties

View File

@@ -20,20 +20,21 @@ import arc.scene.utils.*;
import arc.struct.*;
import arc.util.*;
import mindustry.gen.*;
import mindustry.mod.*;
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<String, Element> build(Table root, NodeBuilder<?> builder){
public static BuildResult build(Table root, NodeBuilder<?> builder){
return build(root, builder, null);
}
public static ObjectMap<String, Element> build(Table root, NodeBuilder<?> builder, @Nullable Cons<MenuResult> resultListener){
public static BuildResult build(Table root, NodeBuilder<?> builder, @Nullable Cons<MenuResult> resultListener){
BuildContext ctx = new BuildContext(resultListener);
buildInto(root, builder.entries, ctx);
return ctx.idElements;
return new BuildResult(ctx.idElements, ctx.images);
}
private static void buildInto(Table table, Seq<Entry> entries, BuildContext ctx){
@@ -98,12 +99,15 @@ public class UiTreeBuilder{
var element = addNode(entry.key, child, ctx);
if(element == null) continue; // unknown/unsupported node - skip for forward compat
String id = node.str(UiKey.id);
String id = child.str(UiKey.id);
if(id != null){
element.name = id;
ctx.idElements.put(id, element);
}
String colorStr = child.str(UiKey.color); // Apply color if provided
if(colorStr != null) element.setColor(Strings.parseColor(colorStr, Color.white));
stack.add(element);
}
}
@@ -125,7 +129,10 @@ public class UiTreeBuilder{
yield label;
}
case image -> {
Image image = new Image(findDrawable(node.str(UiKey.region, node.str(UiKey.icon, "error"))));
String region = node.str(UiKey.region, node.str(UiKey.icon, "error"));
Image image = new Image(findDrawable(region, node.str(UiKey.placeholder)));
// track server-streamed images so that they can be reloaded when streaming finishes
if(region.startsWith(DataImagePacker.serverRegionPrefix)) ctx.images.get(region, Seq::new).add(image);
String scl = node.str(UiKey.scaling);
if(scl != null){
try{
@@ -141,7 +148,10 @@ public class UiTreeBuilder{
String icon = node.str(UiKey.icon);
if(icon != null){
btn.add(new Image(findDrawable(icon)).setScaling(Scaling.fit)).size(32f);
Image iconImage = new Image(findDrawable(icon, node.str(UiKey.placeholder))).setScaling(Scaling.fit);
// track server-streamed images so that they can be reloaded when streaming finishes
if(icon.startsWith(DataImagePacker.serverRegionPrefix)) ctx.images.get(icon, Seq::new).add(iconImage);
btn.add(iconImage).size(32f);
btn.getCells().reverse();
}
wireButton(btn, node, ctx);
@@ -149,7 +159,9 @@ public class UiTreeBuilder{
}
case imageButton -> {
String icon = node.str(UiKey.icon);
ImageButton btn = new ImageButton(icon != null ? findDrawable(icon) : null);
ImageButton btn = new ImageButton(icon != null ? findDrawable(icon, node.str(UiKey.placeholder)) : null);
// track server-streamed images so that they can be reloaded when streaming finishes
if(icon != null && icon.startsWith(DataImagePacker.serverRegionPrefix)) ctx.images.get(icon, Seq::new).add(btn.getImage());
applyStyle(node, ImageButtonStyle.class, btn::setStyle);
wireButton(btn, node, ctx);
yield btn;
@@ -308,13 +320,20 @@ public class UiTreeBuilder{
}
private static Drawable findDrawable(String name){
return findDrawable(name, "error");
}
/** @param placeholder shown instead of the error sprite if {@code name} isn't streamed from the server yet */
private static Drawable findDrawable(String name, @Nullable String placeholder){
if(Core.atlas.has(name)){
return Core.atlas.drawable(name);
}
//icons are a fallback (TODO: bad idea?)
var result = Icon.icons.get(name);
if(result == null) return Core.atlas.drawable("error");
return result;
if(result != null) return result;
if(placeholder != null) return findDrawable(placeholder, null);
return Core.atlas.drawable("nomap"); // no placeholder provided, use the default of the map preview texture
}
/** Evaluates a condition string: "portrait", "landscape", or "width|height >=|>|<|<= number". */
@@ -363,6 +382,7 @@ public class UiTreeBuilder{
final @Nullable Cons<MenuResult> resultListener;
final ObjectMap<String, Element> idElements = new ObjectMap<>();
final ObjectMap<String, ButtonGroup<Button>> buttonGroups = new ObjectMap<>();
final ObjectMap<String, Seq<Image>> images = new ObjectMap<>();
BuildContext(@Nullable Cons<MenuResult> resultListener){
this.resultListener = resultListener;
@@ -372,4 +392,15 @@ public class UiTreeBuilder{
return buttonGroups.get(name, ButtonGroup::new);
}
}
/** Result of building a NodeBuilder: named elements (those with an explicit id), plus any server-streamed images, keyed by region name */
public static class BuildResult{
public final ObjectMap<String, Element> idElements;
public final ObjectMap<String, Seq<Image>> images;
BuildResult(ObjectMap<String, Element> idElements, ObjectMap<String, Seq<Image>> images){
this.idElements = idElements;
this.images = images;
}
}
}

View File

@@ -779,11 +779,9 @@ public class JoinDialog extends BaseDialog{
}else if(host.version == 0){
return Core.bundle.get("server.outdated");
}else if(host.version < Version.build && Version.build != -1){
return Core.bundle.get("server.outdated") + "\n" +
Core.bundle.format("server.version", host.version, host.versionType);
return "\n" + Core.bundle.format("server.version", host.version, host.versionType) + " " + Core.bundle.get("server.outdated");
}else if(host.version > Version.build && Version.build != -1){
return Core.bundle.get("server.outdated.client") + "\n" +
Core.bundle.format("server.version", host.version, host.versionType);
return "\n" + Core.bundle.format("server.version", host.version, host.versionType) + " " + Core.bundle.get("server.outdated.client");
}else if(host.version == Version.build && Version.type.equals(host.versionType)){
//not important
return "";

View File

@@ -206,14 +206,22 @@ public class ModsDialog extends BaseDialog{
t.table(title1 -> {
title1.left();
title1.add(new BorderImage(){{
title1.add(new BorderImage()).size(h - 8f).padTop(-8f).padLeft(-8f).padRight(8f).with(b -> {
if(mod.iconTexture != null){
setDrawable(new TextureRegion(mod.iconTexture));
b.setDrawable(new TextureRegion(mod.iconTexture));
}else{
setDrawable(Tex.nomap);
b.setDrawable(Tex.nomap);
//if the texture gets loaded with a delay, update it
b.update(() -> {
if(mod.iconTexture != null){
b.setDrawable(new TextureRegion(mod.iconTexture));
//stop updating
b.update(null);
}
});
}
border(Pal.accent);
}}).size(h - 8f).padTop(-8f).padLeft(-8f).padRight(8f);
b.border(Pal.accent);
});
title1.table(text -> {
boolean hideDisabled = !mod.isSupported() || mod.hasUnmetDependencies() || mod.hasContentErrors();

View File

@@ -58,12 +58,16 @@ public class PausedDialog extends BaseDialog{
});
if(!mobile){
float tpad = -60f;
if(steam){
tpad = 0f;
cont.check("@steam.friendsonly", !Core.settings.getBool("steampublichost2"), val -> {
Core.settings.put("steampublichost2", !val);
platform.updateLobby();
}).colspan(2).left().with(c -> ui.addDescTooltip(c, "@steam.friendsonly.tooltip")).width(440f)
.visible(() -> net.server()).center().colspan(2).fillX().padBottom(10f).row();
.visible(() -> net.server()).center().colspan(2).fillX().padBottom(10f).padTop(-60f).row();
}
float dw = 220f;
@@ -72,10 +76,10 @@ public class PausedDialog extends BaseDialog{
boolean showObjective = state.rules.sector != null && state.rules.sector.preset != null && state.rules.sector.preset.description != null;
if(showObjective){
cont.button("@objective", Icon.info, () -> ui.fullText.show("@objective", state.rules.sector != null && state.rules.sector.preset != null ? state.rules.sector.preset.description : "oh dear")).padTop(-60f);
cont.button("@objective", Icon.info, () -> ui.fullText.show("@objective", state.rules.sector != null && state.rules.sector.preset != null ? state.rules.sector.preset.description : "oh dear")).padTop(tpad);
}
cont.button("@abandon", Icon.cancel, () -> ui.planet.abandonSectorConfirm(state.rules.sector, this::hide)).padTop(-60f)
cont.button("@abandon", Icon.cancel, () -> ui.planet.abandonSectorConfirm(state.rules.sector, this::hide)).padTop(tpad)
.colspan(showObjective ? 1 : 2).width(showObjective ? dw : dw * 2 + 10f)
.disabled(b -> net.client() || state.gameOver).visible(() -> state.rules.sector != null).row();

View File

@@ -439,7 +439,7 @@ public class SettingsMenuDialog extends BaseDialog{
graphics.sliderPref("screenshake", 4, 0, 8, i -> (i / 4f) + "x");
graphics.sliderPref("bloomintensity", 6, 0, 16, i -> (int)(i/4f * 100f) + "%");
graphics.sliderPref("bloomintensity", 6, 0, 16, i -> (int)(i / 4f * 100f) + "%");
graphics.sliderPref("bloomblur", 2, 1, 16, i -> i + "x");
graphics.sliderPref("fpscap", 240, 10, 245, 5, s -> {
@@ -810,25 +810,13 @@ public class SettingsMenuDialog extends BaseDialog{
@Override
public void add(SettingsTable table){
Button box = new Button(Styles.grayt);
box.background(Styles.grayPanel);
box.margin(10f);
box.add(new Image()).update(i -> i.setDrawable(box.isOver() ? (box.isChecked() ? Tex.checkOnOver : Tex.checkOver) : box.isChecked() ? Tex.checkOn : Tex.checkOff))
.size(32f).padRight(8f).padLeft(-4f);
box.add(title);
box.update(() -> box.setChecked(settings.getBool(name)));
box.clicked(() -> {
settings.put(name, box.isChecked());
Table box = Elems.check(title, () -> settings.getBool(name), value -> {
settings.put(name, value);
if(changed != null){
changed.get(box.isChecked());
changed.get(value);
}
});
box.left();
addDesc(table.add(box).minWidth(Math.min(500f, Core.graphics.getWidth() / 1.2f / Scl.scl(1f))).fillX().height(45f).left().padTop(7f).get());
table.row();
}

View File

@@ -196,6 +196,11 @@ public class Tile implements Position, QuadTreeObject, Displayable{
return overlay;
}
/** Internal method for data patches - do not use!! */
public void updateBlockReference(Block block){
this.block = block;
}
@SuppressWarnings("unchecked")
public <T extends Block> T cblock(){
return (T)block;
@@ -785,6 +790,44 @@ public class Tile implements Position, QuadTreeObject, Displayable{
}
}
/** Fills an area. x2/y2 are inclusive.*/
@Remote(called = Loc.server)
public static void fillTileBlocks(int x, int y, int x2, int y2, Block block, Team team){
for(int tx = x; tx <= x2; tx++){
for(int ty = y; ty <= y2; ty++){
Tile tile = world.tile(tx, ty);
if(tile == null) return; //out of bounds; return instead of breaking since it's a rectangle
tile.setBlock(block, team, 0);
}
}
}
/** Fills an area. x2/y2 are inclusive.*/
@Remote(called = Loc.server)
public static void fillTileFloors(int x, int y, int x2, int y2, Block block){
if(!(block instanceof Floor floor)) return;
for(int tx = x; tx <= x2; tx++){
for(int ty = y; ty <= y2; ty++){
Tile tile = world.tile(tx, ty);
if(tile == null) return; //out of bounds; return instead of breaking since it's a rectangle
tile.setFloor(floor);
}
}
}
/** Fills an area. x2/y2 are inclusive.*/
@Remote(called = Loc.server)
public static void fillTileOverlays(int x, int y, int x2, int y2, Block block){
if(!(block instanceof OverlayFloor floor)) return;
for(int tx = x; tx <= x2; tx++){
for(int ty = y; ty <= y2; ty++){
Tile tile = world.tile(tx, ty);
if(tile == null) return; //out of bounds; return instead of breaking since it's a rectangle
tile.setOverlay(floor);
}
}
}
@Remote(called = Loc.server)
public static void setFloor(Tile tile, Block floor, Block overlay){
tile.setFloor(floor.asFloor());

View File

@@ -242,7 +242,7 @@ public class ForceProjector extends Block{
if(buildup >= shieldHealth + phaseShieldBoost * phaseHeat && !broken){
broken = true;
buildup = shieldHealth;
shieldBreakEffect.at(x, y, realRadius(), team.color);
shieldBreakEffect.at(x, y, realRadius(), team.color, block);
breakSound.at(x, y);
if(team != state.rules.defaultTeam){
Events.fire(Trigger.forceProjectorBreak);

View File

@@ -73,7 +73,7 @@ public class ShockwaveTower extends Block{
if(potentialEfficiency > 0 && (reloadCounter += edelta()) >= reload && timer(timerCheck, checkInterval)){
targets.clear();
Groups.bullet.intersect(x - range, y - range, range * 2, range * 2, b -> {
if(b.team != team && b.type.hittable){
if(b.team != team && b.type.hittable && b.within(x, y, range + 1f)){
targets.add(b);
}
});

View File

@@ -81,7 +81,7 @@ public class LogicDisplay extends Block{
clipSize = Math.max(clipSize, scaleFactor * Draw.scl * displaySize);
}
public class LogicDisplayBuild extends Building{
public class LogicDisplayBuild extends Building implements LDrawable{
//The root display (bottom left corner of display for tileable displays)
public LogicDisplayBuild rootDisplay = this;
public @Nullable FrameBuffer buffer;
@@ -121,7 +121,13 @@ public class LogicDisplay extends Block{
};
}
public void flushCommands(LongSeq graphicsBuffer){
@Override
public boolean drawable(LExecutor exec){
return isValid() && (exec.privileged || (team == exec.team && !privileged));
}
@Override
public void draw(LongSeq graphicsBuffer){
int added = Math.min(graphicsBuffer.size, LExecutor.maxDisplayBuffer - commands.size);
for(int i = 0; i < added; i++){

View File

@@ -258,11 +258,11 @@ public class TileableLogicDisplay extends LogicDisplay{
}
@Override
public void flushCommands(LongSeq graphicsBuffer){
public void draw(LongSeq graphicsBuffer){
if(isRoot()){
super.flushCommands(graphicsBuffer);
super.draw(graphicsBuffer);
}else{
rootDisplay.flushCommands(graphicsBuffer);
rootDisplay.draw(graphicsBuffer);
}
}

View File

@@ -53,7 +53,7 @@ public class LiquidModule extends BlockModule{
flow = cacheFlow;
}
boolean updateFlow = flowTimer.get(15);
boolean updateFlow = flowTimer.get(flowVisualRefreshInterval);
for(int i = 0; i < liquids.length; i++){
flow[i].add(cacheSums[i]);
@@ -63,7 +63,7 @@ public class LiquidModule extends BlockModule{
cacheSums[i] = 0;
if(updateFlow){
displayFlow[i] = flow[i].hasEnoughData() ? flow[i].mean() / flowVisualRefreshInterval : -1;
displayFlow[i] = flow[i].hasEnoughData() ? flow[i].mean() / flowPollInterval : -1;
}
}
}

View File

@@ -4,7 +4,7 @@ buildscript{
}
dependencies{
classpath "com.mobidevelop.robovm:robovm-gradle-plugin:2.3.24"
classpath "com.mobidevelop.robovm:robovm-gradle-plugin:2.3.26"
}
}

View File

@@ -2,7 +2,7 @@
"doc": "Tech tree placement for this content. Also accepts a plain string, treated as 'parent'.",
"parent": {"type": "mindustry.ctype.UnlockableContent", "doc": "Name of the parent tech tree node; required unless 'root' is true."},
"requirements": {"type": "mindustry.type.ItemStack[]", "doc": "Overrides the default research cost, which is this content's build cost."},
"objectives": {"type": "mindustry.type.Objective[]", "doc": "Extra objectives to complete; items/liquids get a Produce objective automatically."},
"objectives": {"type": "mindustry.game.Objectives.Objective[]", "doc": "Extra objectives to complete; items/liquids get a Produce objective automatically."},
"planet": {"type": "mindustry.type.Planet", "doc": "Name of the planet this node belongs to; inherited from the parent otherwise."},
"root": {"type": "boolean", "doc": "If true, this is a root node and 'parent' is not required.", "default": "false"},
"name": {"type": "java.lang.String", "doc": "Display name for a root node; ignored for non-root nodes."},

View File

@@ -0,0 +1,4 @@
{
"abstract": true,
"doc": "Completion objective."
}

View File

@@ -0,0 +1,4 @@
{
"abstract": true,
"doc": "Planet mesh."
}

View File

@@ -1,5 +1,5 @@
{
"superclass": "java.lang.Object",
"superclass": "mindustry.graphics.g3d.GenericMesh",
"mesh": {
"type": "mindustry.graphics.g3d.GenericMesh",
"doc": "Wrapped mesh to render with the extra transform applied. Required - no default."

View File

@@ -1,5 +1,5 @@
{
"superclass": "java.lang.Object",
"superclass": "mindustry.graphics.g3d.GenericMesh",
"meshes": {
"type": "mindustry.graphics.g3d.GenericMesh[]",
"doc": "Meshes to render together, in order. Required - no default."

View File

@@ -0,0 +1,5 @@
{
"doc": "Planet mesh.",
"abstract": true,
"superclass": "mindustry.graphics.g3d.GenericMesh"
}

View File

@@ -16,6 +16,7 @@ import mindustry.ctype.*;
import mindustry.entities.*;
import mindustry.entities.units.*;
import mindustry.game.*;
import mindustry.game.Objectives.*;
import mindustry.gen.*;
import mindustry.type.*;
import mindustry.world.*;
@@ -65,7 +66,17 @@ public class SchemaGenerator{
return;
}
if(type.getSuperclass() != null) val.put("superclass", type.getSuperclass().getCanonicalName());
if(Modifier.isAbstract(type.getModifiers())){
val.put("abstract", true);
}
if(type.getSuperclass() != null){
val.put("superclass", type.getSuperclass().getCanonicalName());
}
if(Objective.class.isAssignableFrom(type)){
val.put("superclass", Objective.class.getCanonicalName());
}
if(typeDec.getJavadoc().isPresent()){
val.put("doc", typeDec.getJavadoc().get().toText());
@@ -74,7 +85,7 @@ public class SchemaGenerator{
for(var field : type.getFields()){
if(Modifier.isStatic(field.getModifiers()) || field.getDeclaringClass() != type) continue;
Jval inner = Jval.newObject();
inner.put("type", field.getGenericType().getTypeName());
inner.put("type", field.getGenericType().getTypeName().replace('$', '.'));
if(field.isAnnotationPresent(Nullable.class)) inner.put("nullable", true);
@@ -150,6 +161,14 @@ public class SchemaGenerator{
}
""".replace("%TYPES%", allClasses.select(UnitType.class::isAssignableFrom).toString(", ", t -> "\"" + t.getSimpleName() + "\"")));
writeSchema(BuildVisibility.class.getCanonicalName(), """
{
"doc": "Types of block build visibility.",
"superclass": "Enum",
"values": [%VALUES%]
}
""".replace("%VALUES%", Seq.with(BuildVisibility.class.getFields()).toString(", ", t -> "\"" + t.getName() + "\"")));
writeSchema(Interp.class.getCanonicalName(), """
{
"doc": "All types of interpolation.",
@@ -158,6 +177,14 @@ public class SchemaGenerator{
}
""".replace("%VALUES%", Seq.with(Interp.class.getFields()).toString(", ", t -> "\"" + t.getName() + "\"")));
writeSchema(AIController.class.getCanonicalName(), """
{
"doc": "Type of unit AI",
"superclass": "Enum",
"values": [%VALUES%]
}
""".replace("%VALUES%", decls.keys().toSeq().select(AIController.class::isAssignableFrom).toString(", ", t -> "\"" + t.getSimpleName() + "\"")));
writeSchema("allTeams", """
{
"doc": "All team names.",
@@ -186,6 +213,7 @@ public class SchemaGenerator{
}
""".replace("%VALUES%", Seq.with(Sounds.class.getFields()).select(f -> f.getType() == Sound.class).toString(", ", t -> "\"" + t.getName() + "\"")));
new Fi("../../tools/extra-schemas").copyFilesTo(outputDir);
}
@@ -194,6 +222,7 @@ public class SchemaGenerator{
injectCustomField(UnitType.class, "template", field("TemplateUnitType", "UnitType template class.", false));
injectCustomField(UnitType.class, "defaultController", field(AIController.class.getCanonicalName(), "Unconditional controller; always assigned, even if on the player team. This overwrites RTS AI.", false));
injectCustomField(UnitType.class, "aiController", field(AIController.class.getCanonicalName(), "Controller used when the unit is not on the player team.", false));
injectCustomField(UnitType.class, "controller", field(AIController.class.getCanonicalName(), "Controller used when the unit is not on the player team. Equivalent to aiController.", false));
injectCustomField(Planet.class, "sectorSize", field("int", "Planet sector subdivisions.", false));

View File

@@ -33,6 +33,7 @@ public class ScriptMainGenerator{
Seq<Class<?>> classes = Seq.withArrays(
getClasses("mindustry"),
getClasses("arc"),
getClasses("arc.func"),
getClasses("arc.struct"),
getClasses("arc.scene"),