diff --git a/annotations/src/main/java/mindustry/annotations/entity/EntityProcess.java b/annotations/src/main/java/mindustry/annotations/entity/EntityProcess.java index 1216115fda..2641966c75 100644 --- a/annotations/src/main/java/mindustry/annotations/entity/EntityProcess.java +++ b/annotations/src/main/java/mindustry/annotations/entity/EntityProcess.java @@ -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 definitions = new Seq<>(); Seq groupDefs = new Seq<>(); Seq 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; } diff --git a/core/assets/bundles/bundle_zh_CN.properties b/core/assets/bundles/bundle_zh_CN.properties index 87fbfc3117..766daf634f 100644 --- a/core/assets/bundles/bundle_zh_CN.properties +++ b/core/assets/bundles/bundle_zh_CN.properties @@ -3016,7 +3016,7 @@ lenum.payenter = 进入/降落到单位下方的荷载方块中 lenum.flag = 赋予单位数字形式的标记 lenum.mine = 从某个位置采集矿物 lenum.build = 建造建筑 -lenum.getblock = 根据坐标获取建筑物、环境块和环境墙体类型。\n单位必须在位置范围内,否则返回空值。 +lenum.getblock = 根据坐标获取建筑物、环境块和环境墙体类型。\n坐标必须在单位的雷达范围内,否则返回空值。 lenum.within = 检查单位是否接近了某个位置 lenum.boost = 开始/停止助推 diff --git a/core/src/mindustry/content/Fx.java b/core/src/mindustry/content/Fx.java index 1bf6751b42..bb4615dfd7 100644 --- a/core/src/mindustry/content/Fx.java +++ b/core/src/mindustry/content/Fx.java @@ -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 -> { diff --git a/core/src/mindustry/core/Logic.java b/core/src/mindustry/core/Logic.java index e86ef6844d..4b530b35ec 100644 --- a/core/src/mindustry/core/Logic.java +++ b/core/src/mindustry/core/Logic.java @@ -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(); } diff --git a/core/src/mindustry/core/NetClient.java b/core/src/mindustry/core/NetClient.java index 1da6632cd3..f8d12b3a6a 100644 --- a/core/src/mindustry/core/NetClient.java +++ b/core/src/mindustry/core/NetClient.java @@ -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); } diff --git a/core/src/mindustry/core/NetServer.java b/core/src/mindustry/core/NetServer.java index 25ba0ae1e1..5a6af323bc 100644 --- a/core/src/mindustry/core/NetServer.java +++ b/core/src/mindustry/core/NetServer.java @@ -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 handler){ diff --git a/core/src/mindustry/editor/data/MapContentView.java b/core/src/mindustry/editor/data/MapContentView.java index 8fef382c64..6e6d617465 100644 --- a/core/src/mindustry/editor/data/MapContentView.java +++ b/core/src/mindustry/editor/data/MapContentView.java @@ -171,7 +171,7 @@ public class MapContentView implements AssetView{ } if(list.getChildren().isEmpty()){ - list.add("@patch.none"); + list.add("@none.found"); } } diff --git a/core/src/mindustry/entities/abilities/ForceFieldAbility.java b/core/src/mindustry/entities/abilities/ForceFieldAbility.java index 2ee0b0d3d6..fab3c021c8 100644 --- a/core/src/mindustry/entities/abilities/ForceFieldAbility.java +++ b/core/src/mindustry/entities/abilities/ForceFieldAbility.java @@ -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); } diff --git a/core/src/mindustry/entities/abilities/ShieldArcAbility.java b/core/src/mindustry/entities/abilities/ShieldArcAbility.java index 97477b306e..188783f6e7 100644 --- a/core/src/mindustry/entities/abilities/ShieldArcAbility.java +++ b/core/src/mindustry/entities/abilities/ShieldArcAbility.java @@ -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; diff --git a/core/src/mindustry/mod/DataImagePacker.java b/core/src/mindustry/mod/DataImagePacker.java index 702e5e0548..89f18c1c04 100644 --- a/core/src/mindustry/mod/DataImagePacker.java +++ b/core/src/mindustry/mod/DataImagePacker.java @@ -27,7 +27,9 @@ public class DataImagePacker{ private @Nullable Seq 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 serverImages = new ObjectMap<>(); + private final ObjectMap 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 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); diff --git a/core/src/mindustry/mod/DataManager.java b/core/src/mindustry/mod/DataManager.java index 069ae517b3..0c72f2d7c2 100644 --- a/core/src/mindustry/mod/DataManager.java +++ b/core/src/mindustry/mod/DataManager.java @@ -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(){ diff --git a/core/src/mindustry/mod/DataPatcher.java b/core/src/mindustry/mod/DataPatcher.java index b878324edd..5059e19734 100644 --- a/core/src/mindustry/mod/DataPatcher.java +++ b/core/src/mindustry/mod/DataPatcher.java @@ -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 patches, Seq 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)){ diff --git a/core/src/mindustry/net/ArcNetProvider.java b/core/src/mindustry/net/ArcNetProvider.java index 92192e224b..a52344102c 100644 --- a/core/src/mindustry/net/ArcNetProvider.java +++ b/core/src/mindustry/net/ArcNetProvider.java @@ -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; diff --git a/core/src/mindustry/net/NetworkIO.java b/core/src/mindustry/net/NetworkIO.java index ac8eec8209..84c19ef6fa 100644 --- a/core/src/mindustry/net/NetworkIO.java +++ b/core/src/mindustry/net/NetworkIO.java @@ -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() : ""; diff --git a/core/src/mindustry/ui/builder/UiTreeBuilder.java b/core/src/mindustry/ui/builder/UiTreeBuilder.java index cac9bd8fc8..26f6e14e35 100644 --- a/core/src/mindustry/ui/builder/UiTreeBuilder.java +++ b/core/src/mindustry/ui/builder/UiTreeBuilder.java @@ -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); } } diff --git a/core/src/mindustry/world/Tile.java b/core/src/mindustry/world/Tile.java index 098dce52e1..d4b1d82d82 100644 --- a/core/src/mindustry/world/Tile.java +++ b/core/src/mindustry/world/Tile.java @@ -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 cblock(){ return (T)block; diff --git a/core/src/mindustry/world/blocks/defense/ForceProjector.java b/core/src/mindustry/world/blocks/defense/ForceProjector.java index 6f7626808e..b9d61cf8be 100644 --- a/core/src/mindustry/world/blocks/defense/ForceProjector.java +++ b/core/src/mindustry/world/blocks/defense/ForceProjector.java @@ -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); diff --git a/core/src/mindustry/world/blocks/defense/ShockwaveTower.java b/core/src/mindustry/world/blocks/defense/ShockwaveTower.java index 9def23cf33..9d197bcf5f 100644 --- a/core/src/mindustry/world/blocks/defense/ShockwaveTower.java +++ b/core/src/mindustry/world/blocks/defense/ShockwaveTower.java @@ -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); } }); diff --git a/core/src/mindustry/world/modules/LiquidModule.java b/core/src/mindustry/world/modules/LiquidModule.java index 0433ba7376..c5034ca298 100644 --- a/core/src/mindustry/world/modules/LiquidModule.java +++ b/core/src/mindustry/world/modules/LiquidModule.java @@ -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; } } }