592 lines
18 KiB
Java
592 lines
18 KiB
Java
package mindustry.game;
|
|
|
|
import arc.*;
|
|
import arc.math.*;
|
|
import arc.struct.Bits;
|
|
import arc.struct.*;
|
|
import arc.util.*;
|
|
import mindustry.*;
|
|
import mindustry.annotations.Annotations.*;
|
|
import mindustry.core.*;
|
|
import mindustry.game.EventType.*;
|
|
import mindustry.gen.*;
|
|
import mindustry.io.SaveFileReader.*;
|
|
import mindustry.io.*;
|
|
import mindustry.world.meta.*;
|
|
|
|
import java.io.*;
|
|
|
|
import static mindustry.Vars.*;
|
|
|
|
public final class FogControl implements CustomChunk{
|
|
private static volatile int ww, wh;
|
|
private static final int dynamicUpdateInterval = 1000 / 25; //25 FPS
|
|
private static final Object notifyStatic = new Object(), notifyDynamic = new Object();
|
|
|
|
/** indexed by team */
|
|
private volatile @Nullable FogData[] fog;
|
|
|
|
private final LongSeq staticEvents = new LongSeq();
|
|
private final LongSeq dynamicEventQueue = new LongSeq(), unitEventQueue = new LongSeq();
|
|
/** access must be synchronized; accessed from both threads */
|
|
private final LongSeq dynamicEvents = new LongSeq(100);
|
|
|
|
private @Nullable Thread staticFogThread;
|
|
private @Nullable Thread dynamicFogThread;
|
|
|
|
private boolean justLoaded = false;
|
|
private boolean loadedStatic = false;
|
|
private int lastEntityUpdateIndex = 0;
|
|
|
|
public FogControl(){
|
|
Events.on(ResetEvent.class, e -> {
|
|
stop();
|
|
});
|
|
|
|
Events.on(WorldLoadEvent.class, e -> {
|
|
stop();
|
|
|
|
loadedStatic = false;
|
|
justLoaded = true;
|
|
ww = world.width();
|
|
wh = world.height();
|
|
|
|
//all old buildings have static light scheduled around them
|
|
if(state.rules.fog && state.rules.staticFog){
|
|
pushStaticBlocks(true);
|
|
//force draw all static stuff immediately
|
|
updateStatic();
|
|
|
|
loadedStatic = true;
|
|
}
|
|
});
|
|
|
|
Events.on(TileChangeEvent.class, event -> {
|
|
if(state.rules.fog && event.tile.build != null && event.tile.isCenter() && !event.tile.build.team.isOnlyAI() && event.tile.block().flags.contains(BlockFlag.hasFogRadius)){
|
|
var data = data(event.tile.team());
|
|
if(data != null){
|
|
data.dynamicUpdated = true;
|
|
}
|
|
|
|
if(state.rules.staticFog){
|
|
synchronized(staticEvents){
|
|
//TODO event per team?
|
|
pushEvent(FogEvent.get(event.tile.x, event.tile.y, Mathf.round(event.tile.build.fogRadius()), event.tile.build.team.id), false);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
//on tile removed, dynamic fog goes away
|
|
Events.on(TilePreChangeEvent.class, e -> {
|
|
if(state.rules.fog && e.tile.build != null && !e.tile.build.team.isOnlyAI() && e.tile.block().flags.contains(BlockFlag.hasFogRadius)){
|
|
var data = data(e.tile.team());
|
|
if(data != null){
|
|
data.dynamicUpdated = true;
|
|
}
|
|
}
|
|
});
|
|
|
|
//unit dead -> fog updates
|
|
Events.on(UnitDestroyEvent.class, e -> {
|
|
if(state.rules.fog && fog[e.unit.team.id] != null){
|
|
fog[e.unit.team.id].dynamicUpdated = true;
|
|
}
|
|
});
|
|
|
|
SaveVersion.addCustomChunk("static-fog-data", this);
|
|
}
|
|
|
|
public @Nullable Bits getDiscovered(Team team){
|
|
return fog == null || fog[team.id] == null ? null : fog[team.id].staticData;
|
|
}
|
|
|
|
public boolean isDiscovered(Team team, int x, int y){
|
|
if(!state.rules.staticFog || !state.rules.fog || team == null || team.isAI()) return true;
|
|
|
|
var data = getDiscovered(team);
|
|
if(data == null) return false;
|
|
if(x < 0 || y < 0 || x >= ww || y >= wh) return false;
|
|
return data.get(x + y * ww);
|
|
}
|
|
|
|
public boolean isVisible(Team team, float x, float y){
|
|
return isVisibleTile(team, World.toTile(x), World.toTile(y));
|
|
}
|
|
|
|
public boolean isVisibleTile(Team team, int x, int y){
|
|
if(!state.rules.fog|| team == null || team.isAI()) return true;
|
|
|
|
var data = data(team);
|
|
if(data == null) return false;
|
|
return data.read.get(Mathf.clamp(x, 0, ww - 1) + Mathf.clamp(y, 0, wh - 1) * ww);
|
|
}
|
|
|
|
public void resetFog(){
|
|
fog = null;
|
|
}
|
|
|
|
@Nullable FogData data(Team team){
|
|
return fog == null || fog[team.id] == null ? null : fog[team.id];
|
|
}
|
|
|
|
void stop(){
|
|
lastEntityUpdateIndex = 0;
|
|
fog = null;
|
|
//I don't care whether the fog thread crashes here, it's about to die anyway
|
|
staticEvents.clear();
|
|
if(staticFogThread != null){
|
|
staticFogThread.interrupt();
|
|
staticFogThread = null;
|
|
}
|
|
|
|
dynamicEvents.clear();
|
|
if(dynamicFogThread != null){
|
|
dynamicFogThread.interrupt();
|
|
dynamicFogThread = null;
|
|
}
|
|
}
|
|
|
|
/** @param initial whether this is the initial update; if true, does not update renderer */
|
|
void pushStaticBlocks(boolean initial){
|
|
if(fog == null) fog = new FogData[256];
|
|
|
|
synchronized(staticEvents){
|
|
for(var build : Groups.build){
|
|
if(build.block.flags.contains(BlockFlag.hasFogRadius)){
|
|
if(fog[build.team.id] == null){
|
|
fog[build.team.id] = new FogData();
|
|
}
|
|
|
|
pushEvent(FogEvent.get(build.tile.x, build.tile.y, Mathf.round(build.fogRadius()), build.team.id), initial);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** @param skipRender whether the event is passed to the fog renderer */
|
|
void pushEvent(long event, boolean skipRender){
|
|
if(!state.rules.staticFog) return;
|
|
|
|
staticEvents.add(event);
|
|
if(!skipRender && !headless && FogEvent.team(event) == Vars.player.team().id){
|
|
renderer.fog.handleEvent(event);
|
|
}
|
|
}
|
|
|
|
public void forceUpdate(Team team, Building build){
|
|
if(state.rules.fog && fog[team.id] != null){
|
|
fog[team.id].dynamicUpdated = true;
|
|
|
|
if(state.rules.staticFog){
|
|
synchronized(staticEvents){
|
|
pushEvent(FogEvent.get(build.tile.x, build.tile.y, Mathf.round(build.fogRadius()), build.team.id), false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void update(){
|
|
if(fog == null){
|
|
fog = new FogData[256];
|
|
}
|
|
|
|
//force update static
|
|
if(state.rules.staticFog && !loadedStatic){
|
|
pushStaticBlocks(false);
|
|
updateStatic();
|
|
loadedStatic = true;
|
|
}
|
|
|
|
if(staticFogThread == null){
|
|
staticFogThread = new StaticFogThread();
|
|
staticFogThread.setPriority(Thread.NORM_PRIORITY - 1);
|
|
staticFogThread.setDaemon(true);
|
|
staticFogThread.start();
|
|
}
|
|
|
|
if(dynamicFogThread == null){
|
|
dynamicFogThread = new DynamicFogThread();
|
|
dynamicFogThread.setPriority(Thread.NORM_PRIORITY - 1);
|
|
dynamicFogThread.setDaemon(true);
|
|
dynamicFogThread.start();
|
|
}
|
|
|
|
//clear to prepare for queuing fog radius from units and buildings
|
|
dynamicEventQueue.clear();
|
|
|
|
//update fog visibility manually
|
|
if(state.rules.fog && !headless && Groups.build.size() > 0){
|
|
|
|
int size = Groups.build.size();
|
|
int chunkSize = 5; //fraction of entity list to iterate each frame
|
|
int chunks = Math.min(chunkSize, size);
|
|
|
|
int iterated = Math.max(1, size / chunks);
|
|
int steps = 0;
|
|
int i = lastEntityUpdateIndex % size;
|
|
|
|
while(steps < iterated){
|
|
Groups.build.index(i).updateFogVisibility();
|
|
|
|
steps ++;
|
|
i ++;
|
|
|
|
if(i >= size){
|
|
i = 0;
|
|
}
|
|
}
|
|
|
|
lastEntityUpdateIndex = i;
|
|
}
|
|
|
|
for(var team : state.teams.present){
|
|
//AI teams do not have fog
|
|
if(!team.team.isOnlyAI()){
|
|
//separate for each team
|
|
unitEventQueue.clear();
|
|
|
|
FogData data = fog[team.team.id];
|
|
|
|
if(data == null){
|
|
data = fog[team.team.id] = new FogData();
|
|
}
|
|
|
|
synchronized(staticEvents){
|
|
//TODO slow?
|
|
for(var unit : team.units){
|
|
int tx = unit.tileX(), ty = unit.tileY(), pos = tx + ty * ww;
|
|
if(unit.type.fogRadius <= 0f) continue;
|
|
long event = FogEvent.get(tx, ty, (int)unit.type.fogRadius, team.team.id);
|
|
|
|
//always update the dynamic events, but only *flush* the results when necessary?
|
|
unitEventQueue.add(event);
|
|
|
|
if(unit.lastFogPos != pos){
|
|
pushEvent(event, false);
|
|
unit.lastFogPos = pos;
|
|
data.dynamicUpdated = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
//if it's time for an update, flush *everything* onto the update queue
|
|
if(data.dynamicUpdated && Time.timeSinceMillis(data.lastDynamicMs) > dynamicUpdateInterval){
|
|
data.dynamicUpdated = false;
|
|
data.lastDynamicMs = Time.millis();
|
|
|
|
//add building updates
|
|
for(var build : indexer.getFlagged(team.team, BlockFlag.hasFogRadius)){
|
|
dynamicEventQueue.add(FogEvent.get(build.tile.x, build.tile.y, Mathf.round(build.fogRadius()), build.team.id));
|
|
}
|
|
|
|
//add unit updates
|
|
dynamicEventQueue.addAll(unitEventQueue);
|
|
}
|
|
}
|
|
}
|
|
|
|
if(dynamicEventQueue.size > 0){
|
|
//flush unit events over when something happens
|
|
synchronized(dynamicEvents){
|
|
dynamicEvents.clear();
|
|
dynamicEvents.addAll(dynamicEventQueue);
|
|
}
|
|
dynamicEventQueue.clear();
|
|
|
|
//force update so visibility doesn't have a pop-in
|
|
if(justLoaded){
|
|
updateDynamic(new Bits(256));
|
|
justLoaded = false;
|
|
}
|
|
|
|
//notify that it's time for rendering
|
|
//TODO this WILL block until it is done rendering, which is inherently problematic.
|
|
synchronized(notifyDynamic){
|
|
notifyDynamic.notify();
|
|
}
|
|
}
|
|
|
|
//wake up, it's time to draw some circles
|
|
if(state.rules.staticFog && staticEvents.size > 0 && staticFogThread != null){
|
|
synchronized(notifyStatic){
|
|
notifyStatic.notify();
|
|
}
|
|
}
|
|
}
|
|
|
|
class StaticFogThread extends Thread{
|
|
|
|
StaticFogThread(){
|
|
super("StaticFogThread");
|
|
}
|
|
|
|
@Override
|
|
public void run(){
|
|
while(true){
|
|
try{
|
|
synchronized(notifyStatic){
|
|
try{
|
|
//wait until an event happens
|
|
notifyStatic.wait();
|
|
}catch(InterruptedException e){
|
|
//end thread
|
|
return;
|
|
}
|
|
}
|
|
|
|
updateStatic();
|
|
//ignore, don't want to crash this thread
|
|
}catch(Exception e){}
|
|
}
|
|
}
|
|
}
|
|
|
|
void updateStatic(){
|
|
|
|
//I really don't like synchronizing here, but there should be *some* performance benefit at least
|
|
synchronized(staticEvents){
|
|
int size = staticEvents.size;
|
|
for(int i = 0; i < size; i++){
|
|
long event = staticEvents.items[i];
|
|
int x = FogEvent.x(event), y = FogEvent.y(event), rad = FogEvent.radius(event), team = FogEvent.team(event);
|
|
var data = fog[team];
|
|
if(data != null){
|
|
circle(data.staticData, x, y, rad);
|
|
}
|
|
}
|
|
staticEvents.clear();
|
|
}
|
|
}
|
|
|
|
class DynamicFogThread extends Thread{
|
|
final Bits cleared = new Bits();
|
|
|
|
DynamicFogThread(){
|
|
super("DynamicFogThread");
|
|
}
|
|
|
|
@Override
|
|
public void run(){
|
|
|
|
while(true){
|
|
try{
|
|
synchronized(notifyDynamic){
|
|
try{
|
|
//wait until an event happens
|
|
notifyDynamic.wait();
|
|
}catch(InterruptedException e){
|
|
//end thread
|
|
return;
|
|
}
|
|
}
|
|
|
|
updateDynamic(cleared);
|
|
|
|
//ignore, don't want to crash this thread
|
|
}catch(Exception e){
|
|
//log for debugging
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void updateDynamic(Bits cleared){
|
|
cleared.clear();
|
|
|
|
//ugly sync
|
|
synchronized(dynamicEvents){
|
|
int size = dynamicEvents.size;
|
|
|
|
//draw step
|
|
for(int i = 0; i < size; i++){
|
|
long event = dynamicEvents.items[i];
|
|
int x = FogEvent.x(event), y = FogEvent.y(event), rad = FogEvent.radius(event), team = FogEvent.team(event);
|
|
|
|
if(rad <= 0) continue;
|
|
|
|
var data = fog[team];
|
|
if(data != null){
|
|
|
|
//clear the buffer, since it is being re-drawn
|
|
if(!cleared.get(team)){
|
|
cleared.set(team);
|
|
|
|
data.write.clear();
|
|
}
|
|
|
|
//radius is always +1 to keep up with visuals
|
|
circle(data.write, x, y, rad + 1);
|
|
}
|
|
}
|
|
dynamicEvents.clear();
|
|
}
|
|
|
|
//swap step, no need for synchronization or anything
|
|
for(int i = 0; i < 256; i++){
|
|
if(cleared.get(i)){
|
|
var data = fog[i];
|
|
|
|
//swap buffers, flushing the data that was just drawn
|
|
Bits temp = data.read;
|
|
data.read = data.write;
|
|
data.write = temp;
|
|
}
|
|
}
|
|
}
|
|
|
|
@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);
|
|
Bits data = fog[i].staticData;
|
|
int size = ww * wh;
|
|
|
|
int pos = 0;
|
|
while(pos < size){
|
|
int consecutives = 0;
|
|
boolean cur = data.get(pos);
|
|
while(consecutives < 127 && pos < size){
|
|
if(cur != data.get(pos)){
|
|
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 FogData[256];
|
|
|
|
int teams = stream.readUnsignedByte();
|
|
int w = stream.readShort(), h = stream.readShort();
|
|
int len = w * h;
|
|
|
|
ww = w;
|
|
wh = h;
|
|
|
|
for(int ti = 0; ti < teams; ti++){
|
|
int team = stream.readUnsignedByte();
|
|
fog[team] = new FogData();
|
|
|
|
int pos = 0;
|
|
Bits bools = fog[team].staticData;
|
|
|
|
while(pos < len){
|
|
int data = stream.readByte() & 0xff;
|
|
boolean sign = (data & 0b1000_0000) != 0;
|
|
int consec = data & 0b0111_1111;
|
|
|
|
if(sign){
|
|
bools.set(pos, pos + consec);
|
|
pos += consec;
|
|
}else{
|
|
pos += consec;
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
@Override
|
|
public boolean shouldWrite(){
|
|
return state.rules.fog && state.rules.staticFog && fog != null;
|
|
}
|
|
|
|
static void circle(Bits 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);
|
|
}
|
|
}
|
|
|
|
static void hline(Bits 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;
|
|
|
|
arr.set(off + x1, off + x2);
|
|
}
|
|
|
|
static class FogData{
|
|
/** dynamic double-buffered data for dynamic (live) coverage */
|
|
volatile Bits read, write;
|
|
/** static map exploration fog*/
|
|
final Bits staticData;
|
|
|
|
/** last dynamic update timestamp. */
|
|
long lastDynamicMs = 0;
|
|
/** if true, a dynamic fog update must be scheduled. */
|
|
boolean dynamicUpdated = true;
|
|
|
|
FogData(){
|
|
int len = ww * wh;
|
|
|
|
read = new Bits(len);
|
|
write = new Bits(len);
|
|
staticData = new Bits(len);
|
|
}
|
|
}
|
|
|
|
@Struct
|
|
class FogEventStruct{
|
|
@StructField(16)
|
|
int x;
|
|
@StructField(16)
|
|
int y;
|
|
@StructField(16)
|
|
int radius;
|
|
@StructField(8)
|
|
int team;
|
|
}
|
|
}
|