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

# Conflicts:
#	core/src/mindustry/mod/DataImagePacker.java
This commit is contained in:
Anuken
2026-08-19 10:39:37 +02:00
19 changed files with 152 additions and 66 deletions

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

@@ -3016,7 +3016,7 @@ lenum.payenter = 进入/降落到单位下方的荷载方块中
lenum.flag = 赋予单位数字形式的标记
lenum.mine = 从某个位置采集矿物
lenum.build = 建造建筑
lenum.getblock = 根据坐标获取建筑物、环境块和环境墙体类型。\n单位必须在位置范围内,否则返回空值。
lenum.getblock = 根据坐标获取建筑物、环境块和环境墙体类型。\n坐标必须在单位的雷达范围内,否则返回空值。
lenum.within = 检查单位是否接近了某个位置
lenum.boost = 开始/停止助推

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;
@@ -2807,7 +2808,10 @@ public class Fx{
shieldBreak = new Effect(40, e -> {
color(e.color);
stroke(3f * e.fout());
Lines.poly(e.x, e.y, e.data instanceof Integer i ? i : 6, e.rotation + e.fin());
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, 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

@@ -168,7 +168,11 @@ public class NetClient implements ApplicationListener{
try(DataInputStream in = new DataInputStream(data.stream)){
String name = in.readUTF();
byte[] pngData = in.readAllBytes();
if(!headless) state.data.addTexture(name, pngData);
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);
}

View File

@@ -554,34 +554,36 @@ public class NetServer implements ApplicationListener{
/**
* 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 mindustry.mod.DataManager#removeTexture} 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}. */
* 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){
mainExecutor.submit(() -> {
var stream = packTexture(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){
mainExecutor.submit(() -> {
var stream = packTexture(name, pngData);
for(NetConnection con : net.getConnections()){
con.sendStreamAsync(new TextureStream(), stream);
}
});
}
private ByteArrayOutputStream packTexture(String name, byte[] pngData){
var stream = new ByteArrayOutputStream();
try(DataOutputStream out = new DataOutputStream(stream)){
out.writeUTF(name);
out.write(pngData);
}catch(IOException e){
throw new RuntimeException(e);
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);
}
return 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){

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

@@ -91,7 +91,7 @@ public class ForceFieldAbility extends Ability{
if(unit.shield <= 0f && !wasBroken){
unit.shield -= cooldown * regen;
Fx.shieldBreak.at(unit.x, unit.y, radius, unit.type.shieldColor(unit), sides);
Fx.shieldBreak.at(unit.x, unit.y, radius, unit.type.shieldColor(unit), this);
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

@@ -27,7 +27,9 @@ public class DataImagePacker{
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 ObjectMap<String, Texture> serverImages = new ObjectMap<>();
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){
@@ -175,7 +177,7 @@ public class DataImagePacker{
/** 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){
Vars.mainExecutor.submit(() -> {
textureExecutor.execute(() -> {
Pixmap pixmap;
try{
pixmap = new Pixmap(pngData);
@@ -208,8 +210,8 @@ public class DataImagePacker{
});
}
/** Removes a texture previously added with {@link #addTexture} */
public void removeTexture(String 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);
@@ -219,6 +221,12 @@ public class DataImagePacker{
}
public void printStats(PixmapPacker packer){
/** 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 mainPacker, PixmapPacker envPacker){
if(Log.level != LogLevel.debug) return;
int total = packer.getPages().sum(p -> p.rects.size);

View File

@@ -7,10 +7,10 @@ import arc.graphics.g2d.TextureAtlas.*;
import arc.struct.*;
import arc.util.*;
import mindustry.*;
import mindustry.annotations.Annotations.*;
import mindustry.ctype.*;
import mindustry.graphics.*;
import mindustry.mod.data.*;
import mindustry.net.*;
public class DataManager{
private DataPatcher patcher = new DataPatcher();
@@ -29,7 +29,6 @@ public class DataManager{
public void reloadContent(boolean reloadArrays){
patcher.unapply(reloadArrays);
patcher.apply(getPatches(), getContent(), reloadArrays);
rebuildOrderedAssets();
@@ -186,15 +185,15 @@ public class DataManager{
}
/** Adds/replaces a single image pushed by the server at runtime, independent of map/mod data patches.
* Use {@link mindustry.core.NetServer#sendTexture} to send a texture to connected clients. */
* 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}. */
@Remote(variants = Variant.both)
public static void removeTexture(String name){
if(!Vars.headless) Vars.state.data.packer.removeTexture(name);
/** 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(){

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

@@ -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

@@ -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

@@ -105,6 +105,9 @@ public class UiTreeBuilder{
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);
}
}

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;

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, sides);
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

@@ -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;
}
}
}