diff --git a/core/src/mindustry/Vars.java b/core/src/mindustry/Vars.java index 0d35391606..ffecfb93fd 100644 --- a/core/src/mindustry/Vars.java +++ b/core/src/mindustry/Vars.java @@ -233,6 +233,7 @@ public class Vars implements Loadable{ public static BlockIndexer indexer; public static Pathfinder pathfinder; public static ControlPathfinder controlPath; + public static FogControl fogControl; public static Control control; public static Logic logic; @@ -300,6 +301,7 @@ public class Vars implements Loadable{ indexer = new BlockIndexer(); pathfinder = new Pathfinder(); controlPath = new ControlPathfinder(); + fogControl = new FogControl(); bases = new BaseRegistry(); constants = new GlobalConstants(); javaPath = diff --git a/core/src/mindustry/core/Logic.java b/core/src/mindustry/core/Logic.java index 44ce03dd9f..9a30d5c22d 100644 --- a/core/src/mindustry/core/Logic.java +++ b/core/src/mindustry/core/Logic.java @@ -399,6 +399,10 @@ public class Logic implements ApplicationListener{ state.updateId ++; state.teams.updateTeamStats(); + if(state.rules.fog){ + fogControl.update(); + } + if(state.isCampaign()){ state.rules.sector.info.update(); } diff --git a/core/src/mindustry/entities/comp/UnitComp.java b/core/src/mindustry/entities/comp/UnitComp.java index 5a43dbbfcd..fda535b269 100644 --- a/core/src/mindustry/entities/comp/UnitComp.java +++ b/core/src/mindustry/entities/comp/UnitComp.java @@ -51,8 +51,8 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I //TODO could be better represented as a unit transient @Nullable UnitType dockedType; - transient float shadowAlpha = -1f; - transient float healTime; + transient float shadowAlpha = -1f, healTime; + transient int lastFogPos; private transient float resupplyTime = Mathf.random(10f); private transient boolean wasPlayer; private transient boolean wasHealed; diff --git a/core/src/mindustry/game/FogControl.java b/core/src/mindustry/game/FogControl.java new file mode 100644 index 0000000000..f945c047dc --- /dev/null +++ b/core/src/mindustry/game/FogControl.java @@ -0,0 +1,298 @@ +package mindustry.game; + +import arc.*; +import arc.struct.*; +import arc.util.*; +import mindustry.*; +import mindustry.annotations.Annotations.*; +import mindustry.game.EventType.*; +import mindustry.gen.*; +import mindustry.io.SaveFileReader.*; +import mindustry.io.*; + +import java.io.*; + +import static mindustry.Vars.*; + +public class FogControl implements CustomChunk{ + private static volatile int ww, wh; + private static final int buildLight = 1; + + private final Object sync = new Object(); + /** indexed by [team] [packed array tile pos] */ + private @Nullable boolean[][] fog; + + private final LongSeq events = new LongSeq(); + private @Nullable Thread fogThread; + + private boolean read = false; + + public FogControl(){ + Events.on(ResetEvent.class, e -> { + clear(); + }); + + Events.on(WorldLoadEvent.class, e -> { + clear(); + + ww = world.width(); + wh = world.height(); + + //all old buildings have light around them + if(state.rules.fog && read){ + for(var build : Groups.build){ + synchronized(events){ + events.add(FogEvent.get(build.tile.x, build.tile.y, buildLight, build.team.id)); + } + } + + read = false; + } + }); + + Events.on(TileChangeEvent.class, event -> { + if(state.rules.fog && event.tile.build != null && event.tile.isCenter()){ + synchronized(events){ + //TODO event per team? + pushEvent(FogEvent.get(event.tile.x, event.tile.y, buildLight + event.tile.block().size, event.tile.build.team.id)); + } + } + }); + + SaveVersion.addCustomChunk("fogdata", this); + } + + public @Nullable boolean[] getData(Team team){ + return fog == null ? null : fog[team.id]; + } + + public boolean isCovered(Team team, int x, int y){ + var data = getData(team); + if(data == null || x < 0 || y < 0 || x >= ww || y >= wh) return false; + return !data[x + y * ww]; + } + + void clear(){ + fog = null; + //I don't care whether the fog thread crashes here, it's about to die anyway + events.clear(); + if(fogThread != null){ + fogThread.interrupt(); + fogThread = null; + } + } + + void pushEvent(long event){ + events.add(event); + if(!headless && FogEvent.team(event) == Vars.player.team().id){ + renderer.fog.handleEvent(event); + } + } + + public void update(){ + if(fog == null){ + fog = new boolean[256][]; + } + + if(fogThread == null && !net.client()){ + fogThread = new FogThread(); + fogThread.setDaemon(true); + fogThread.start(); + } + + for(var team : state.teams.present){ + if(!team.team.isAI()){ + + if(fog[team.team.id] == null){ + fog[team.team.id] = new boolean[world.width() * world.height()]; + } + + synchronized(events){ + //TODO slow? + for(var unit : team.units){ + int tx = unit.tileX(), ty = unit.tileY(), pos = tx + ty * ww; + if(unit.lastFogPos != pos){ + pushEvent(FogEvent.get(tx, ty, (int)unit.type.fogRadius, team.team.id)); + unit.lastFogPos = pos; + } + } + } + } + } + + //wake up, it's time to draw some circles + if(events.size > 0 && fogThread != null){ + synchronized(sync){ + sync.notify(); + } + } + } + + public class FogThread extends Thread{ + + @Override + public void run(){ + while(true){ + try{ + synchronized(sync){ + try{ + //wait until an event happens + sync.wait(); + }catch(InterruptedException e){ + //end thread + return; + } + } + + //I really don't like synchronizing here, but there should be some performance benefit at least + synchronized(events){ + int size = events.size; + for(int i = 0; i < size; i++){ + long event = events.items[i]; + int x = FogEvent.x(event), y = FogEvent.y(event), rad = FogEvent.radius(event), team = FogEvent.team(event); + var arr = fog[team]; + if(arr != null){ + circle(arr, x, y, rad); + } + } + events.clear(); + } + //ignore, don't want to crash this thread + }catch(Exception e){} + } + } + + void circle(boolean[] arr, int x, int y, int radius){ + int f = 1 - radius; + int ddFx = 1, ddFy = -2 * radius; + int px = 0, py = radius; + + hline(arr, x, x, y + radius); + hline(arr, x, x, y - radius); + hline(arr, x - radius, x + radius, y); + + while(px < py){ + if(f >= 0){ + py--; + ddFy += 2; + f += ddFy; + } + px++; + ddFx += 2; + f += ddFx; + hline(arr, x - px, x + px, y + py); + hline(arr, x - px, x + px, y - py); + hline(arr, x - py, x + py, y + px); + hline(arr, x - py, x + py, y - px); + } + } + + void hline(boolean[] arr, int x1, int x2, int y){ + if(y < 0 || y >= wh) return; + int tmp; + + if(x1 > x2){ + tmp = x1; + x1 = x2; + x2 = tmp; + } + + if(x1 >= ww) return; + if(x2 < 0) return; + + if(x1 < 0) x1 = 0; + if(x2 >= ww) x2 = ww - 1; + x2++; + int off = y * ww; + + while(x1 != x2){ + arr[off + x1++] = true; + } + } + } + + @Override + public void write(DataOutput stream) throws IOException{ + int used = 0; + for(int i = 0; i < 256; i++){ + if(fog[i] != null) used ++; + } + + stream.writeByte(used); + stream.writeShort(world.width()); + stream.writeShort(world.height()); + + for(int i = 0; i < 256; i++){ + if(fog[i] != null){ + stream.writeByte(i); + boolean[] data = fog[i]; + + int pos = 0, size = data.length; + while(pos < size){ + int consecutives = 0; + boolean cur = data[pos]; + while(consecutives < 127 && pos < size){ + boolean next = data[pos]; + if(cur != next){ + break; + } + + consecutives ++; + pos ++; + } + int mask = (cur ? 0b1000_0000 : 0); + stream.write(mask | (consecutives)); + } + } + } + } + + @Override + public void read(DataInput stream) throws IOException{ + if(fog == null) fog = new boolean[256][]; + + int teams = stream.readUnsignedByte(); + int w = stream.readShort(), h = stream.readShort(); + int len = w * h; + + for(int ti = 0; ti < teams; ti++){ + int team = stream.readUnsignedByte(); + int pos = 0; + boolean[] bools = fog[team] = new boolean[w * h]; + + while(pos < len){ + int data = stream.readByte() & 0xff; + boolean sign = (data & 0b1000_0000) != 0; + int consec = data & 0b0111_1111; + + if(sign){ + for(int i = 0; i < consec; i++){ + bools[pos ++] = true; + } + }else{ + pos += consec; + } + } + } + + read = true; + + } + + @Override + public boolean shouldWrite(){ + return state.rules.fog && fog != null; + } + + @Struct + class FogEventStruct{ + @StructField(16) + int x; + @StructField(16) + int y; + @StructField(16) + int radius; + @StructField(8) + int team; + } +} diff --git a/core/src/mindustry/graphics/FogRenderer.java b/core/src/mindustry/graphics/FogRenderer.java index 1e7d74b5cc..3536d53a20 100644 --- a/core/src/mindustry/graphics/FogRenderer.java +++ b/core/src/mindustry/graphics/FogRenderer.java @@ -9,49 +9,31 @@ import arc.math.geom.*; import arc.struct.*; import arc.util.*; import mindustry.game.EventType.*; +import mindustry.game.*; import mindustry.gen.*; -import mindustry.io.*; -import mindustry.io.SaveFileReader.*; - -import java.io.*; -import java.nio.*; import static mindustry.Vars.*; /** Highly experimental fog-of-war renderer. */ -public class FogRenderer implements CustomChunk{ +public class FogRenderer{ private static final float fogSpeed = 1f; private FrameBuffer buffer = new FrameBuffer(); - private Seq events = new Seq<>(); - private boolean read = false; + private LongSeq events = new LongSeq(); private Rect rect = new Rect(); + private @Nullable Team lastTeam; public FogRenderer(){ - SaveVersion.addCustomChunk("fog", this); - Events.on(WorldLoadEvent.class, event -> { - if(state.rules.fog){ - buffer.resize(world.width(), world.height()); - - events.clear(); - Groups.build.copy(events); - - //clear - if(!read){ - buffer.begin(Color.black); - buffer.end(); - } - - read = false; - } + lastTeam = null; + events.clear(); }); - //draw fog when tile is placed. - Events.on(TileChangeEvent.class, event -> { - if(state.rules.fog && event.tile.build != null && event.tile.isCenter()){ - events.add(event.tile.build); - } - }); + //TODO draw fog when tile is placed? + + } + + public void handleEvent(long event){ + events.add(event); } public Texture getTexture(){ @@ -59,122 +41,78 @@ public class FogRenderer implements CustomChunk{ } public void drawFog(){ + //there is no fog. + if(fogControl.getData(player.team()) == null) return; + //resize if world size changes boolean clear = buffer.resizeCheck(world.width(), world.height()); - //set projection to whole map - Draw.proj(0, 0, buffer.getWidth() * tilesize, buffer.getHeight() * tilesize); - - //if the buffer resized, it contains garbage now, clear it. - if(clear){ - buffer.begin(Color.black); - }else{ - buffer.begin(); + if(player.team() != lastTeam){ + copyFromCpu(); + lastTeam = player.team(); + clear = false; } - Gl.blendEquationSeparate(Gl.max, Gl.max); - ScissorStack.push(rect.set(1, 1, buffer.getWidth() - 2, buffer.getHeight() - 2)); + //grab events + if(clear || events.size > 0){ + //set projection to whole map + Draw.proj(0, 0, buffer.getWidth(), buffer.getHeight()); - Draw.color(Color.white); - - for(var build : events){ - if(build.team == player.team()){ - Fill.circle(build.x, build.y, 40f); + //if the buffer resized, it contains garbage now, clear it. + if(clear){ + buffer.begin(Color.black); + }else{ + buffer.begin(); } + + ScissorStack.push(rect.set(1, 1, buffer.getWidth() - 2, buffer.getHeight() - 2)); + + Draw.color(Color.white); + + //process new fog events + for(int i = 0; i < events.size; i++){ + long e = events.items[i]; + Fill.poly(FogEvent.x(e) + 0.5f, FogEvent.y(e) + 0.5f, 20, FogEvent.radius(e) + 0.3f); + } + + events.clear(); + + buffer.end(); + ScissorStack.pop(); + Draw.proj(Core.camera); } - events.clear(); - Draw.alpha(fogSpeed * Math.max(Time.delta, 1f)); - - //TODO slow and terrible - Groups.unit.each(u -> { - if(u.team == player.team()){ - Fill.circle(u.x, u.y, u.type.lightRadius * 1.5f); - } - }); - - buffer.end(); buffer.getTexture().setFilter(TextureFilter.linear); - Gl.blendEquationSeparate(Gl.funcAdd, Gl.funcAdd); - ScissorStack.pop(); - - Draw.proj(Core.camera); Draw.shader(Shaders.fog); Draw.fbo(buffer.getTexture(), world.width(), world.height(), tilesize); Draw.shader(); } - @Override - public void write(DataOutput stream) throws IOException{ - ByteBuffer bytes = Buffers.newUnsafeByteBuffer(buffer.getWidth() * buffer.getHeight() * 4); - try{ - bytes.position(0); - buffer.begin(); - Gl.readPixels(0, 0, buffer.getWidth(), buffer.getHeight(), Gl.rgba, Gl.unsignedByte, bytes); - buffer.end(); - bytes.position(0); - stream.writeShort(buffer.getWidth()); - stream.writeShort(buffer.getHeight()); - - //TODO flip? - - int pos = 0, size = bytes.capacity() / 4; - while(pos < size){ - int consecutives = 0; - boolean cur = bytes.get(pos * 4) != 0; - while(consecutives < 127 && pos < size){ - boolean next = bytes.get(pos * 4) != 0; - if(cur != next){ - break; - } - - consecutives ++; - pos ++; - } - int mask = (cur ? 0b1000_0000 : 0); - stream.write(mask | (consecutives)); - } - }finally{ - Buffers.disposeUnsafeByteBuffer(bytes); - } - } - - @Override - public void read(DataInput stream) throws IOException{ - short w = stream.readShort(), h = stream.readShort(); - int pos = 0; - int len = w * h; - buffer.resize(w, h); + public void copyFromCpu(){ + buffer.resize(world.width(), world.height()); buffer.begin(Color.black); Draw.proj(0, 0, buffer.getWidth(), buffer.getHeight()); Draw.color(); + int ww = world.width(), wh = world.height(); - while(pos < len){ - int data = stream.readByte() & 0xff; - boolean sign = (data & 0b1000_0000) != 0; - int consec = data & 0b0111_1111; + boolean[] data = fogControl.getData(player.team()); + if(data != null){ + for(int i = 0; i < data.length; i++){ + if(data[i]){ + //TODO slow, could do scanlines instead at the very least. + int x = i % ww, y = i / ww; - if(sign){ - for(int i = 0; i < consec; i++){ - int x = pos % w, y = pos / w; - //TODO this is slow - Fill.rect(x + 0.5f, y + 0.5f, 1f, 1f); - - pos ++; + //manually clip with 1 pixel of padding so the borders are never fully revealed + if(x > 0 && y > 0 && x < ww - 1 && y < wh - 1){ + Fill.rect(x + 0.5f, y + 0.5f, 1f, 1f); + } } - }else{ - pos += consec; } } buffer.end(); Draw.proj(Core.camera); - read = true; } - @Override - public boolean shouldWrite(){ - return state.rules.fog && buffer.getTexture() != null && buffer.getWidth() > 0; - } } diff --git a/core/src/mindustry/io/SaveFileReader.java b/core/src/mindustry/io/SaveFileReader.java index d16f1f32be..5d450f0e89 100644 --- a/core/src/mindustry/io/SaveFileReader.java +++ b/core/src/mindustry/io/SaveFileReader.java @@ -203,8 +203,15 @@ public abstract class SaveFileReader{ public interface CustomChunk{ void write(DataOutput stream) throws IOException; void read(DataInput stream) throws IOException; + + /** @return whether this chunk is enabled at all */ default boolean shouldWrite(){ return true; } + + /** @return whether this chunk should be written to connecting clients (default true) */ + default boolean writeNet(){ + return true; + } } } diff --git a/core/src/mindustry/io/SaveVersion.java b/core/src/mindustry/io/SaveVersion.java index 38fe61ef33..835f56350f 100644 --- a/core/src/mindustry/io/SaveVersion.java +++ b/core/src/mindustry/io/SaveVersion.java @@ -84,11 +84,11 @@ public abstract class SaveVersion extends SaveFileReader{ region("content", stream, this::writeContentHeader); region("map", stream, this::writeMap); region("entities", stream, this::writeEntities); - region("custom", stream, this::writeCustomChunks); + region("custom", stream, s -> writeCustomChunks(s, false)); } - public void writeCustomChunks(DataOutput stream) throws IOException{ - var chunks = customChunks.orderedKeys().select(s -> customChunks.get(s).shouldWrite()); + public void writeCustomChunks(DataOutput stream, boolean net) throws IOException{ + var chunks = customChunks.orderedKeys().select(s -> customChunks.get(s).shouldWrite() && (!net || customChunks.get(s).writeNet())); stream.writeInt(chunks.size); for(var chunkName : chunks){ var chunk = customChunks.get(chunkName); diff --git a/core/src/mindustry/net/NetworkIO.java b/core/src/mindustry/net/NetworkIO.java index 8a521ee3df..1a576bd0b8 100644 --- a/core/src/mindustry/net/NetworkIO.java +++ b/core/src/mindustry/net/NetworkIO.java @@ -61,6 +61,7 @@ public class NetworkIO{ SaveIO.getSaveWriter().writeContentHeader(stream); SaveIO.getSaveWriter().writeMap(stream); SaveIO.getSaveWriter().writeTeamBlocks(stream); + SaveIO.getSaveWriter().writeCustomChunks(stream, true); }catch(IOException e){ throw new RuntimeException(e); } @@ -97,6 +98,7 @@ public class NetworkIO{ SaveIO.getSaveWriter().readContentHeader(stream); SaveIO.getSaveWriter().readMap(stream, world.context); SaveIO.getSaveWriter().readTeamBlocks(stream); + SaveIO.getSaveWriter().readCustomChunks(stream); }catch(IOException e){ throw new RuntimeException(e); }finally{ diff --git a/core/src/mindustry/type/UnitType.java b/core/src/mindustry/type/UnitType.java index a7b5fd3f10..20768362ff 100644 --- a/core/src/mindustry/type/UnitType.java +++ b/core/src/mindustry/type/UnitType.java @@ -172,6 +172,7 @@ public class UnitType extends UnlockableContent{ public float hitSize = 6f; public float itemOffsetY = 3f; public float lightRadius = -1f, lightOpacity = 0.6f; + public float fogRadius = -1f; public Color lightColor = Pal.powerLight; public boolean drawCell = true, drawItems = true, drawShields = true, drawBody = true; public int trailLength = 0; @@ -437,6 +438,10 @@ public class UnitType extends UnlockableContent{ lightRadius = Math.max(60f, hitSize * 2.3f); } + if(fogRadius < 0){ + fogRadius = lightRadius * 1.5f / 8f; + } + clipSize = Math.max(clipSize, lightRadius * 1.1f); singleTarget = weapons.size <= 1 && !forceMultiTarget;