This commit is contained in:
Anuken
2025-07-21 02:43:02 -04:00
155 changed files with 1482 additions and 731 deletions

View File

@@ -11,6 +11,7 @@ import mindustry.entities.effect.*;
import mindustry.entities.part.DrawPart.*;
import mindustry.entities.part.*;
import mindustry.entities.pattern.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.type.*;
@@ -57,8 +58,18 @@ public class Blocks{
//boulders
shaleBoulder, sandBoulder, daciteBoulder, boulder, snowBoulder, basaltBoulder, carbonBoulder, ferricBoulder, beryllicBoulder, yellowStoneBoulder,
arkyicBoulder, crystalCluster, vibrantCrystalCluster, crystalBlocks, crystalOrbs, crystallineBoulder, redIceBoulder, rhyoliteBoulder, redStoneBoulder,
metalFloor, metalFloorDamaged, metalFloor2, metalFloor3, metalFloor4, metalFloor5, basalt, magmarock, hotrock, snowWall, saltWall,
//old metal floors
darkPanel1, darkPanel2, darkPanel3, darkPanel4, darkPanel5, darkPanel6, darkMetal,
//new metal floors
metalTiles1, metalTiles2, metalTiles3, metalTiles4, metalTiles5, metalTiles6, metalTiles7, metalTiles8, metalTiles9, metalTiles10, metalTiles11, metalTiles12,
//colored
coloredFloor, coloredWall,
characterOverlayGray,
characterOverlayWhite,
pebbles, tendrils,
//ores
@@ -314,6 +325,7 @@ public class Blocks{
solid = true;
variants = 0;
canShadow = false;
drawEdgeOut = false;
}};
empty = new EmptyFloor("empty");
@@ -808,6 +820,107 @@ public class Blocks{
darkMetal = new StaticWall("dark-metal");
metalTiles1 = new Floor("metal-tiles-1"){{
autotile = true;
drawEdgeOut = false;
drawEdgeIn = false;
}};
metalTiles2 = new Floor("metal-tiles-2"){{
autotile = true;
drawEdgeOut = false;
drawEdgeIn = false;
}};
metalTiles3 = new Floor("metal-tiles-3"){{
autotile = true;
drawEdgeOut = false;
drawEdgeIn = false;
}};
metalTiles4 = new Floor("metal-tiles-4"){{
autotile = true;
drawEdgeOut = false;
drawEdgeIn = false;
}};
metalTiles5 = new Floor("metal-tiles-5"){{
autotile = true;
drawEdgeOut = false;
drawEdgeIn = false;
}};
metalTiles6 = new Floor("metal-tiles-6"){{
autotile = true;
drawEdgeOut = false;
drawEdgeIn = false;
emitLight = true;
lightRadius = 30f;
lightColor = Team.crux.color.cpy().a(0.3f);
}};
metalTiles7 = new Floor("metal-tiles-7"){{
autotile = true;
drawEdgeOut = false;
drawEdgeIn = false;
autotileMidVariants = 9;
}};
metalTiles8 = new Floor("metal-tiles-8"){{
autotile = true;
drawEdgeOut = false;
drawEdgeIn = false;
autotileMidVariants = 2;
}};
metalTiles9 = new Floor("metal-tiles-9"){{
autotile = true;
drawEdgeOut = false;
drawEdgeIn = false;
}};
metalTiles10 = new Floor("metal-tiles-10"){{
autotile = true;
drawEdgeOut = false;
drawEdgeIn = false;
}};
metalTiles11 = new Floor("metal-tiles-11"){{
autotile = true;
drawEdgeOut = false;
drawEdgeIn = false;
autotileVariants = 3;
}};
metalTiles12 = new Floor("metal-tiles-12"){{
autotile = true;
drawEdgeOut = false;
drawEdgeIn = false;
autotileVariants = 4;
emitLight = true;
lightRadius = 30f;
lightColor = Team.crux.color.cpy().a(0.3f);
}};
coloredFloor = new ColoredFloor("colored-floor"){{
autotile = true;
drawEdgeOut = false;
drawEdgeIn = false;
}};
coloredWall = new ColoredWall("colored-wall"){{
autotile = true;
}};
characterOverlayGray = new CharacterOverlay("character-overlay"){{
color = Pal.metalGrayDark;
}};
characterOverlayWhite = new CharacterOverlay("character-overlay-white"){{
color = Color.white;
}};
Seq.with(metalFloor, metalFloorDamaged, metalFloor2, metalFloor3, metalFloor4, metalFloor5, darkPanel1, darkPanel2, darkPanel3, darkPanel4, darkPanel5, darkPanel6)
.each(b -> b.asFloor().wall = darkMetal);

View File

@@ -40,7 +40,7 @@ public class Renderer implements ApplicationListener{
public @Nullable Bloom bloom;
public @Nullable FrameBuffer backgroundBuffer;
public FrameBuffer effectBuffer = new FrameBuffer();
public boolean animateShields, drawWeather = true, drawStatus, enableEffects, drawDisplays = true, drawLight = true, pixelate = false;
public boolean animateShields, animateWater, drawWeather = true, drawStatus, enableEffects, drawDisplays = true, drawLight = true, pixelate = false;
public float weatherAlpha;
/** minZoom = zooming out, maxZoom = zooming in, used by cutscenes */
public float minZoom = 1.5f, maxZoom = 6f;
@@ -167,6 +167,7 @@ public class Renderer implements ApplicationListener{
laserOpacity = settings.getInt("lasersopacity") / 100f;
bridgeOpacity = settings.getInt("bridgeopacity") / 100f;
animateShields = settings.getBool("animatedshields");
animateWater = settings.getBool("animatewater");
drawStatus = settings.getBool("blockstatus");
enableEffects = settings.getBool("effects");
drawDisplays = !settings.getBool("hidedisplays");
@@ -310,7 +311,7 @@ public class Renderer implements ApplicationListener{
graphics.clear(clearColor);
Draw.reset();
if(settings.getBool("animatedwater") || animateShields){
if(animateWater || animateShields){
effectBuffer.resize(graphics.getWidth(), graphics.getHeight());
}

View File

@@ -10,6 +10,13 @@ import mindustry.world.blocks.environment.*;
import static mindustry.Vars.*;
public class DrawOperation{
static final byte
opFloor = 0,
opBlock = 1,
opRotation = 2,
opTeam = 3,
opOverlay = 4;
private LongSeq array = new LongSeq();
public boolean isEmpty(){
@@ -39,45 +46,58 @@ public class DrawOperation{
}
short getTile(Tile tile, byte type){
if(type == OpType.floor.ordinal()){
return tile.floorID();
}else if(type == OpType.block.ordinal()){
return tile.blockID();
}else if(type == OpType.rotation.ordinal()){
return tile.build == null ? 0 : (byte)tile.build.rotation;
}else if(type == OpType.team.ordinal()){
return (byte)tile.getTeamID();
}else if(type == OpType.overlay.ordinal()){
return tile.overlayID();
}
throw new IllegalArgumentException("Invalid type.");
return switch(type){
case opFloor -> tile.floorID();
case opOverlay -> tile.overlayID();
case opBlock -> tile.blockID();
case opRotation -> tile.build == null ? 0 : (byte)tile.build.rotation;
case opTeam -> (byte)tile.getTeamID();
default -> throw new IllegalArgumentException("Invalid type.");
};
}
void setTile(Tile tile, byte type, short to){
if(type == opBlock || type == opTeam || type == opRotation){
tile.getLinkedTiles(t -> {
editor.renderer.updateBlock(t);
editor.renderer.updateStatic(t.x, t.y);
});
}else{
editor.renderer.updateStatic(tile.x, tile.y);
}
editor.load(() -> {
if(type == OpType.floor.ordinal()){
if(content.block(to) instanceof Floor floor){
tile.setFloor(floor);
switch(type){
case opFloor -> {
if(content.block(to) instanceof Floor floor){
tile.setFloor(floor);
}
}
}else if(type == OpType.block.ordinal()){
tile.getLinkedTiles(t -> editor.renderer.updatePoint(t.x, t.y));
Block block = content.block(to);
tile.setBlock(block, tile.team(), tile.build == null ? 0 : tile.build.rotation);
if(tile.build != null){
tile.build.enabled = true;
case opOverlay -> {
if(content.block(to) instanceof Floor floor){
tile.setOverlay(floor);
}
}
tile.getLinkedTiles(t -> editor.renderer.updatePoint(t.x, t.y));
}else if(type == OpType.rotation.ordinal()){
if(tile.build != null) tile.build.rotation = to;
}else if(type == OpType.team.ordinal()){
tile.setTeam(Team.get(to));
}else if(type == OpType.overlay.ordinal()){
tile.setOverlayID(to);
case opBlock -> {
Block block = content.block(to);
tile.setBlock(block, tile.team(), tile.build == null ? 0 : tile.build.rotation);
if(tile.build != null){
tile.build.enabled = true;
}
}
case opRotation -> {
if(tile.build != null) tile.build.rotation = to;
}
case opTeam -> tile.setTeam(Team.get(to));
}
});
editor.renderer.updatePoint(tile.x, tile.y);
if(type == opBlock || type == opTeam || type == opRotation){
tile.getLinkedTiles(t -> {
editor.renderer.updateBlock(t);
editor.renderer.updateStatic(t.x, t.y);
});
}
}
@Struct
@@ -87,12 +107,4 @@ public class DrawOperation{
byte type;
short value;
}
public enum OpType{
floor,
block,
rotation,
team,
overlay
}
}

View File

@@ -0,0 +1,171 @@
package mindustry.editor;
import arc.graphics.*;
import arc.graphics.g2d.*;
import arc.graphics.gl.*;
import arc.math.*;
import arc.struct.*;
import arc.util.*;
public class EditorSpriteCache implements Disposable{
//xy + color + uv
static final int vertexSize = 2 + 1 + 2;
private @Nullable Mesh mesh;
private final Seq<Texture> textures = new Seq<>(8);
private final IntSeq counts = new IntSeq(8);
private float[] tmpVertices;
/** Index in tmpVertices of current vertex data. */
private int index;
/** @param tmpVertices Temporary buffer to hold vertices while building up sprites. Should be large enough to hold all sprite data this cache will contain. */
public EditorSpriteCache(float[] tmpVertices){
this.tmpVertices = tmpVertices;
}
/** @return whether anything was added to the cache. */
public boolean isEmpty(){
return index == 0;
}
/**
* Builds this cache into a mesh that can be used for rendering. Use after calling {@link #draw(TextureRegion, float, float, float, float, float, float, float, float)}.
* Until this method is called, no mesh is created.
*
* @param indices The shared index data in standard quad format, as seen in SpriteBatch.
* Since this data is static, it should be the same across all caches, and be large enough to accommodate all sprites.
* */
public void build(IndexData indices){
if(mesh != null) mesh.dispose();
mesh = new Mesh(true, index / vertexSize, 0,
VertexAttribute.position,
VertexAttribute.color,
VertexAttribute.texCoords
);
mesh.indices = indices;
mesh.setVertices(tmpVertices, 0, index);
}
/** Adds the specified region to the cache. */
public void draw(TextureRegion region, float x, float y, float originX, float originY, float width, float height, float rotation, float colorPacked){
if(mesh != null) throw new IllegalStateException("This cache is already built. Call #clear() before drawing new sprites.");
// bottom left and top right corner points relative to origin
final float worldOriginX = x + originX;
final float worldOriginY = y + originY;
float fx = -originX;
float fy = -originY;
float fx2 = width - originX;
float fy2 = height - originY;
float x1, y1, x2, y2, x3, y3, x4, y4;
// rotate
if(rotation != 0){
final float cos = Mathf.cosDeg(rotation);
final float sin = Mathf.sinDeg(rotation);
x1 = cos * fx - sin * fy;
y1 = sin * fx + cos * fy;
x2 = cos * fx - sin * fy2;
y2 = sin * fx + cos * fy2;
x3 = cos * fx2 - sin * fy2;
y3 = sin * fx2 + cos * fy2;
x4 = x1 + (x3 - x2);
y4 = y3 - (y2 - y1);
}else{
x1 = fx;
y1 = fy;
x2 = fx;
y2 = fy2;
x3 = fx2;
y3 = fy2;
x4 = fx2;
y4 = fy;
}
x1 += worldOriginX;
y1 += worldOriginY;
x2 += worldOriginX;
y2 += worldOriginY;
x3 += worldOriginX;
y3 += worldOriginY;
x4 += worldOriginX;
y4 += worldOriginY;
final float u = region.u;
final float v = region.v2;
final float u2 = region.u2;
final float v2 = region.v;
int idx = index;
float[] verts = tmpVertices;
Texture texture = region.texture;
verts[idx + 0] = x1;
verts[idx + 1] = y1;
verts[idx + 2] = colorPacked;
verts[idx + 3] = u;
verts[idx + 4] = v;
verts[idx + 5] = x2;
verts[idx + 6] = y2;
verts[idx + 7] = colorPacked;
verts[idx + 8] = u;
verts[idx + 9] = v2;
verts[idx + 10] = x3;
verts[idx + 11] = y3;
verts[idx + 12] = colorPacked;
verts[idx + 13] = u2;
verts[idx + 14] = v2;
verts[idx + 15] = x4;
verts[idx + 16] = y4;
verts[idx + 17] = colorPacked;
verts[idx + 18] = u2;
verts[idx + 19] = v;
int lastIndex = textures.size - 1;
if(lastIndex < 0 || textures.get(lastIndex) != texture){
textures.add(texture);
counts.add(6);
}else{
counts.incr(lastIndex, 6);
}
index += vertexSize * 4;
}
/** Renders the cached mesh. The shader must already have the correct view matrix set as a uniform. */
public void render(Shader shader){
if(mesh == null) throw new IllegalStateException("Cache is empty, call build() first.");
int offset = 0;
for(int i = 0; i < textures.size; i++){
int count = counts.items[i];
textures.get(i).bind();
mesh.render(shader, Gl.triangles, offset, count);
offset += count;
}
}
@Override
public void dispose(){
if(mesh != null){
mesh.dispose();
mesh = null;
}
}
}

View File

@@ -2,9 +2,9 @@ package mindustry.editor;
import arc.func.*;
import mindustry.content.*;
import mindustry.editor.DrawOperation.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.world.*;
import mindustry.world.blocks.environment.*;
import mindustry.world.modules.*;
@@ -27,17 +27,17 @@ public class EditorTile extends Tile{
if(type instanceof OverlayFloor){
//don't place on liquids
if(floor.hasSurface() || !type.needsSurface){
setOverlayID(type.id);
setOverlay(type);
}
return;
}
if(floor == type && overlayID() == 0) return;
if(overlayID() != 0) op(OpType.overlay, overlayID());
if(floor != type) op(OpType.floor, floor.id);
if(floor == type) return;
op(DrawOperation.opFloor, floor.id);
this.floor = type;
this.overlay = (Floor)Blocks.air;
type.floorChanged(this);
}
@Override
@@ -47,30 +47,40 @@ public class EditorTile extends Tile{
@Override
public void setBlock(Block type, Team team, int rotation, Prov<Building> entityprov){
Block prev = this.block;
Tile prevCenter = (build == null ? this : build.tile);
if(skip()){
super.setBlock(type, team, rotation, entityprov);
return;
}
if(this.block == type && (build == null || build.rotation == rotation)){
update();
return;
}
if(!isCenter()){
EditorTile cen = (EditorTile)build.tile;
cen.op(OpType.rotation, (byte)build.rotation);
cen.op(OpType.team, (byte)build.team.id);
cen.op(OpType.block, block.id);
update();
cen.op(DrawOperation.opRotation, (byte)build.rotation);
cen.op(DrawOperation.opTeam, (byte)build.team.id);
cen.op(DrawOperation.opBlock, block.id);
updateStatic();
}else{
if(build != null) op(OpType.rotation, (byte)build.rotation);
if(build != null) op(OpType.team, (byte)build.team.id);
op(OpType.block, block.id);
if(build != null) op(DrawOperation.opRotation, (byte)build.rotation);
if(build != null) op(DrawOperation.opTeam, (byte)build.team.id);
op(DrawOperation.opBlock, block.id);
}
super.setBlock(type, team, rotation, entityprov);
if(requiresBlockUpdate(type) || requiresBlockUpdate(prev)){
if(prev.size > 1){
prevCenter.getLinkedTilesAs(prev, tile -> editor.renderer.updateBlock(tile));
}
getLinkedTiles(tile -> editor.renderer.updateBlock(tile));
}else{
renderer.blocks.updateShadowTile(this);
}
}
@Override
@@ -80,11 +90,11 @@ public class EditorTile extends Tile{
return;
}
if(getTeamID() == team.id) return;
op(OpType.team, (byte)getTeamID());
if(getTeamID() == team.id || !synthetic()) return;
op(DrawOperation.opTeam, (byte)getTeamID());
super.setTeam(team);
getLinkedTiles(t -> editor.renderer.updatePoint(t.x, t.y));
getLinkedTiles(t -> editor.renderer.updateBlock(t.x, t.y));
}
@Override
@@ -95,8 +105,8 @@ public class EditorTile extends Tile{
}
if(!floor.hasSurface() && overlay.asFloor().needsSurface && (overlay instanceof OreBlock || !floor.supportsOverlay)) return;
if(overlay() == overlay) return;
op(OpType.overlay, this.overlay.id);
if(this.overlay == overlay) return;
op(DrawOperation.opOverlay, this.overlay.id);
super.setOverlay(overlay);
}
@@ -105,7 +115,7 @@ public class EditorTile extends Tile{
if(skip()){
super.fireChanged();
}else{
update();
updateStatic();
}
}
@@ -114,7 +124,7 @@ public class EditorTile extends Tile{
if(skip()){
super.firePreChanged();
}else{
update();
updateStatic();
}
}
@@ -154,15 +164,24 @@ public class EditorTile extends Tile{
}
}
private void update(){
editor.renderer.updatePoint(x, y);
@Override
public boolean isDarkened(){
return skip() && super.isDarkened();
}
private boolean requiresBlockUpdate(Block block){
return block != Blocks.air && block.cacheLayer == CacheLayer.normal;
}
private void updateStatic(){
editor.renderer.updateStatic(x, y);
}
private boolean skip(){
return state.isGame() || editor.isLoading() || world.isGenerating();
}
private void op(OpType type, short value){
editor.addTileOp(TileOp.get(x, y, (byte)type.ordinal(), value));
private void op(int type, short value){
editor.addTileOp(TileOp.get(x, y, (byte)type, value));
}
}

View File

@@ -131,7 +131,7 @@ public enum EditorTool{
Block dest = tile.floor();
if(dest == editor.drawBlock) return;
tester = t -> t.floor() == dest;
setter = t -> t.setFloorUnder(editor.drawBlock.asFloor());
setter = t -> t.setFloor(editor.drawBlock.asFloor());
}else{
Block dest = tile.block();
if(dest == editor.drawBlock) return;

View File

@@ -7,7 +7,6 @@ import arc.math.*;
import arc.math.geom.*;
import arc.struct.*;
import mindustry.content.*;
import mindustry.editor.DrawOperation.*;
import mindustry.entities.units.*;
import mindustry.game.*;
import mindustry.gen.*;
@@ -149,6 +148,7 @@ public class MapEditor{
y = Mathf.clamp(y, (drawBlock.size - 1) / 2, height() - drawBlock.size / 2 - 1);
if(!hasOverlap(x, y)){
tile(x, y).setBlock(drawBlock, drawTeam, rotation);
addTileOp(TileOp.get((short)x, (short)y, DrawOperation.opTeam, (byte)drawTeam.id));
}
}else{
boolean isFloor = drawBlock.isFloor() && drawBlock != Blocks.air;
@@ -166,10 +166,14 @@ public class MapEditor{
}
}else if(!(tile.block().isMultiblock() && !drawBlock.isMultiblock())){
if(drawBlock.rotate && tile.build != null && tile.build.rotation != rotation){
addTileOp(TileOp.get(tile.x, tile.y, (byte)OpType.rotation.ordinal(), (byte)rotation));
addTileOp(TileOp.get(tile.x, tile.y, DrawOperation.opRotation, (byte)rotation));
}
tile.setBlock(drawBlock, drawTeam, rotation);
if(drawBlock.synthetic()){
addTileOp(TileOp.get(tile.x, tile.y, DrawOperation.opTeam, (byte)drawTeam.id));
}
}
};
@@ -375,7 +379,7 @@ public class MapEditor{
if(currentOp == null) currentOp = new DrawOperation();
currentOp.addOperation(data);
renderer.updatePoint(TileOp.x(data), TileOp.y(data));
renderer.updateStatic(TileOp.x(data), TileOp.y(data));
}
class Context implements WorldContext{

View File

@@ -29,6 +29,7 @@ import mindustry.ui.dialogs.*;
import mindustry.world.*;
import mindustry.world.blocks.environment.*;
import mindustry.world.blocks.storage.*;
import mindustry.world.blocks.storage.CoreBlock.*;
import mindustry.world.meta.*;
import static mindustry.Vars.*;
@@ -303,7 +304,7 @@ public class MapEditorDialog extends Dialog implements Disposable{
state.rules = (lastSavedRules == null ? new Rules() : lastSavedRules);
lastSavedRules = null;
saved = false;
editor.renderer.updateAll();
editor.renderer.recache();
}
private void editInGame(){
@@ -323,9 +324,8 @@ public class MapEditorDialog extends Dialog implements Disposable{
"width", editor.width(),
"height", editor.height()
));
state.set(State.playing);
world.endMapLoad();
player.set(world.width() * tilesize/2f, world.height() * tilesize/2f);
Core.camera.position.set(player);
player.clearUnit();
for(var unit : Groups.unit){
@@ -338,14 +338,17 @@ public class MapEditorDialog extends Dialog implements Disposable{
Groups.weather.clear();
logic.play();
if(player.team().core() == null){
player.set(world.width() * tilesize/2f, world.height() * tilesize/2f);
var unit = (state.rules.hasEnv(Env.scorching) ? UnitTypes.evoke : UnitTypes.alpha).spawn(player.team(), player.x, player.y);
unit.spawnedByCore = true;
player.unit(unit);
}
Point2 center = view.project(Core.graphics.getWidth()/2f, Core.graphics.getHeight()/2f);
player.checkSpawn();
CoreBuild best = player.bestCore();
player.set(center.x * tilesize, center.y * tilesize);
var unit = (best != null ? ((CoreBlock)best.block).unitType : (state.rules.hasEnv(Env.scorching) ? UnitTypes.evoke : UnitTypes.alpha)).spawn(editor.drawTeam, player.x, player.y);
unit.spawnedByCore = true;
player.unit(unit);
player.set(unit);
Core.camera.position.set(unit.x, unit.y);
});
}
@@ -748,7 +751,11 @@ public class MapEditorDialog extends Dialog implements Disposable{
//ctrl keys (undo, redo, save)
if(Core.input.ctrl()){
if(Core.input.keyTap(KeyCode.z)){
editor.undo();
if(Core.input.shift()){
editor.redo();
}else{
editor.undo();
}
}
if(Core.input.keyTap(KeyCode.y)){
@@ -827,7 +834,9 @@ public class MapEditorDialog extends Dialog implements Disposable{
if(i == 0) editor.drawBlock = block;
if(++i % 6 == 0){
int cols = mobile ? 4 : 6;
if(++i % cols == 0){
blockSelection.row();
}
}

View File

@@ -185,7 +185,7 @@ public class MapGenerateDialog extends BaseDialog{
}
//reset undo stack as generation... messes things up
editor.renderer.updateAll();
editor.renderer.recache();
editor.clearOp();
}

View File

@@ -41,7 +41,7 @@ public class MapProcessorsDialog extends BaseDialog{
foundAny = true;
tile.setNet(Blocks.worldProcessor, Team.sharded, 0);
if(ui.editor.isShown()){
Vars.editor.renderer.updatePoint(x, y);
Vars.editor.renderer.updateStatic(x, y);
}
break outer;
}

View File

@@ -3,186 +3,220 @@ package mindustry.editor;
import arc.*;
import arc.graphics.*;
import arc.graphics.g2d.*;
import arc.math.*;
import arc.graphics.gl.*;
import arc.math.geom.*;
import arc.struct.*;
import arc.util.*;
import mindustry.content.*;
import mindustry.game.*;
import mindustry.graphics.*;
import mindustry.world.*;
import mindustry.world.blocks.environment.*;
import static mindustry.Vars.*;
public class MapRenderer implements Disposable{
private static final int chunkSize = 64;
private IndexedRenderer[][] chunks;
private IntSet updates = new IntSet();
private IntSet delayedUpdates = new IntSet();
private TextureRegion clearEditor;
private static final int chunkSize = 60;
private static final Seq<Tile> tmpTiles = new Seq<>();
private EditorSpriteCache[][] chunks;
private IntSet recacheChunks = new IntSet();
private int width, height;
private Shader shader;
public void resize(int width, int height){
updates.clear();
delayedUpdates.clear();
if(chunks != null){
for(int x = 0; x < chunks.length; x++){
for(int y = 0; y < chunks[0].length; y++){
chunks[x][y].dispose();
}
}
}
dispose();
chunks = new IndexedRenderer[(int)Math.ceil((float)width / chunkSize)][(int)Math.ceil((float)height / chunkSize)];
recacheChunks.clear();
chunks = new EditorSpriteCache[(int)Math.ceil((float)width / chunkSize)][(int)Math.ceil((float)height / chunkSize)];
for(int x = 0; x < chunks.length; x++){
for(int y = 0; y < chunks[0].length; y++){
chunks[x][y] = new IndexedRenderer(chunkSize * chunkSize * 2);
}
}
this.width = width;
this.height = height;
updateAll();
recache();
}
public void draw(float tx, float ty, float tw, float th){
Draw.flush();
clearEditor = Core.atlas.find("clear-editor");
if(shader == null){
shader = new Shader(
"""
attribute vec4 a_position;
attribute vec4 a_color;
attribute vec2 a_texCoord0;
uniform mat4 u_projTrans;
varying vec4 v_color;
varying vec2 v_texCoords;
void main(){
v_color = a_color;
v_color.a = v_color.a * (255.0/254.0);
v_texCoords = a_texCoord0;
gl_Position = u_projTrans * a_position;
}
""",
updates.each(i -> render(i % width, i / width));
updates.clear();
updates.addAll(delayedUpdates);
delayedUpdates.clear();
//????
if(chunks == null){
return;
"""
varying lowp vec4 v_color;
varying vec2 v_texCoords;
uniform sampler2D u_texture;
void main(){
gl_FragColor = v_color * texture2D(u_texture, v_texCoords);
}
"""
);
}
var texture = Core.atlas.find("clear-editor").texture;
Draw.flush();
//don't process terrain updates every frame (helps with lag on low end devices)
boolean doUpdate = Core.graphics.getFrameId() % 2 == 0;
if(doUpdate) renderer.blocks.floor.checkChanges();
boolean prev = renderer.animateWater;
renderer.animateWater = false;
Tmp.v3.set(Core.camera.position);
Core.camera.position.set(world.width()/2f * tilesize, world.height()/2f * tilesize);
Core.camera.width = 999999f;
Core.camera.height = 999999f;
Core.camera.mat.set(Draw.proj()).mul(Tmp.m3.setToTranslation(tx, ty).scale(tw / (width * tilesize), th / (height * tilesize)).translate(4f, 4f));
renderer.blocks.floor.drawFloor();
Tmp.m2.set(Draw.proj());
//scissors are always enabled because this is drawn clipped in UI, make sure they don't interfere with drawing shadow events
Gl.disable(Gl.scissorTest);
if(doUpdate) renderer.blocks.processShadows();
Gl.enable(Gl.scissorTest);
Draw.proj(Core.camera.mat);
Draw.shader(Shaders.darkness);
Draw.rect(Draw.wrap(renderer.blocks.getShadowBuffer().getTexture()), world.width() * tilesize/2f - tilesize/2f, world.height() * tilesize/2f - tilesize/2f, world.width() * tilesize, -world.height() * tilesize);
Draw.shader();
Draw.proj(Tmp.m2);
renderer.blocks.floor.beginDraw();
renderer.blocks.floor.drawLayer(CacheLayer.walls);
renderer.animateWater = prev;
if(chunks == null) return;
if(doUpdate){
recacheChunks.each(i -> recacheChunk(Point2.x(i), Point2.y(i)));
recacheChunks.clear();
}
shader.bind();
shader.setUniformMatrix4("u_projTrans", Core.camera.mat);
for(int x = 0; x < chunks.length; x++){
for(int y = 0; y < chunks[0].length; y++){
IndexedRenderer mesh = chunks[x][y];
EditorSpriteCache mesh = chunks[x][y];
if(mesh == null){
continue;
if(mesh == null) continue;
mesh.render(shader);
}
}
Core.camera.position.set(Tmp.v3);
}
void updateStatic(int x, int y){
renderer.blocks.floor.recacheTile(x, y);
if(x > 0) renderer.blocks.floor.recacheTile(x - 1, y);
if(y > 0) renderer.blocks.floor.recacheTile(x, y - 1);
if(x < world.width() - 1) renderer.blocks.floor.recacheTile(x + 1, y);
if(y < world.height() - 1) renderer.blocks.floor.recacheTile(x, y + 1);
}
void updateBlock(Tile tile){
updateBlock(tile.x, tile.y);
renderer.blocks.updateShadowTile(tile);
}
void updateBlock(int x, int y){
recacheChunks.add(Point2.pack(x / chunkSize, y / chunkSize));
}
void recache(){
renderer.blocks.floor.clearTiles();
renderer.blocks.reload();
for(int x = 0; x < chunks.length; x++){
for(int y = 0; y < chunks[0].length; y++){
recacheChunk(x, y);
}
}
}
void recacheChunk(int cx, int cy){
if(chunks[cx][cy] != null){
chunks[cx][cy].dispose();
chunks[cx][cy] = null;
}
EditorSpriteCache cache = new EditorSpriteCache(renderer.blocks.floor.getVertexBuffer());
TextureRegion teamRegion = Core.atlas.find("block-border");
tmpTiles.clear();
for(int x = cx * chunkSize; x < (cx + 1) * chunkSize; x++){
for(int y = cy * chunkSize; y < (cy + 1) * chunkSize; y++){
Tile tile = world.tile(x, y);
if(tile != null && tile.block() != Blocks.air && tile.block().cacheLayer == CacheLayer.normal && tile.isCenter()){
tmpTiles.add(tile);
}
mesh.getTransformMatrix().setToTranslation(tx, ty).scale(tw / (width * tilesize), th / (height * tilesize));
mesh.setProjectionMatrix(Draw.proj());
mesh.render(texture);
}
}
}
public void updatePoint(int x, int y){
updates.add(x + y * width);
}
tmpTiles.sort(Structs.comparingBool(b -> !b.block().synthetic()));
public void updateAll(){
clearEditor = Core.atlas.find("clear-editor");
for(int x = 0; x < width; x++){
for(int y = 0; y < height; y++){
render(x, y);
}
}
}
for(Tile tile : tmpTiles){
int x = tile.x, y = tile.y;
Block block = tile.block();
private TextureRegion getIcon(Block wall, int index){
return !wall.editorIcon().found() ?
clearEditor : wall.variants > 0 ?
wall.editorVariantRegions()[Mathf.randomSeed(index, 0, wall.editorVariantRegions().length - 1)] :
wall.editorIcon();
}
TextureRegion region = block.fullIcon;
private void render(int wx, int wy){
int x = wx / chunkSize, y = wy / chunkSize;
if(x >= chunks.length || y >= chunks[0].length) return;
IndexedRenderer mesh = chunks[x][y];
Tile tile = editor.tiles().getn(wx, wy);
float width = region.width * region.scl(), height = region.height * region.scl();
Team team = tile.team();
Floor floor = tile.floor();
Floor overlay = tile.overlay();
Block wall = tile.block();
TextureRegion region;
int idxWall = (wx % chunkSize) + (wy % chunkSize) * chunkSize;
int idxDecal = (wx % chunkSize) + (wy % chunkSize) * chunkSize + chunkSize * chunkSize;
boolean center = tile.isCenter();
boolean useSyntheticWall = wall.synthetic() || overlay.wallOre;
//draw synthetic wall or floor OR standard wall if wall ore
if(wall != Blocks.air && useSyntheticWall){
region = !center ? clearEditor : getIcon(wall, idxWall);
float width = region.width * region.scl(), height = region.height * region.scl(), ox = wall.offset + (tilesize - width) / 2f, oy = wall.offset + (tilesize - height) / 2f;
//force fit to tile
if(overlay.wallOre && !wall.synthetic()){
width = height = tilesize;
ox = oy = 0f;
}
mesh.draw(idxWall, region,
wx * tilesize + ox,
wy * tilesize + oy,
cache.draw(block.fullIcon,
x * tilesize + block.offset - width / 2f,
y * tilesize + block.offset - height / 2f,
width/2f, height/2f,
width, height,
tile.build == null || !wall.rotate ? 0 : tile.build.rotdeg());
}else{
region = floor.editorVariantRegions()[Mathf.randomSeed(idxWall, 0, floor.editorVariantRegions().length - 1)];
tile.build == null || !block.rotate ? 0 : tile.build.rotdeg(),
Color.whiteFloatBits);
mesh.draw(idxWall, region, wx * tilesize, wy * tilesize, 8, 8);
if(tile.build != null){
cache.draw(teamRegion,
x * tilesize + block.offset - width / 2f,
y * tilesize + block.offset - height / 2f,
0f, 0f,
teamRegion.width * teamRegion.scl(), teamRegion.height * teamRegion.scl(),
0f,
tile.build.team.color.toFloatBits());
}
}
float offsetX = -((wall.size + 1) / 3) * tilesize, offsetY = -((wall.size + 1) / 3) * tilesize;
tmpTiles.clear();
//draw non-synthetic wall or ore
if((wall.update || wall.destructible) && center){
mesh.setColor(team.color);
region = Core.atlas.find("block-border-editor");
if(wall.size == 2){
offsetX += tilesize;
offsetY += tilesize;
}
}else if(!useSyntheticWall && wall != Blocks.air && center){
region = getIcon(wall, idxWall);
if(wall == Blocks.cliff){
mesh.setColor(Tmp.c1.set(floor.mapColor).mul(1.6f));
region = ((Cliff)Blocks.cliff).editorCliffs[tile.data & 0xff];
}
offsetX = tilesize / 2f - region.width * region.scl() / 2f;
offsetY = tilesize / 2f - region.height * region.scl() / 2f;
}else if((wall == Blocks.air || overlay.wallOre) && !overlay.isAir()){
if(floor.isLiquid){
mesh.setColor(Tmp.c1.set(1f, 1f, 1f, floor.overlayAlpha));
}
region = overlay.editorVariantRegions()[Mathf.randomSeed(idxWall, 0, tile.overlay().editorVariantRegions().length - 1)];
}else{
region = clearEditor;
if(!cache.isEmpty()){
cache.build(renderer.blocks.floor.getIndexData());
chunks[cx][cy] = cache;
}
float width = region.width * region.scl(), height = region.height * region.scl();
if(!wall.synthetic() && wall != Blocks.air && !wall.isMultiblock()){
offsetX = offsetY = 0f;
width = height = tilesize;
}
mesh.draw(idxDecal, region, wx * tilesize + offsetX, wy * tilesize + offsetY, width, height);
mesh.setColor(Color.white);
}
@Override
public void dispose(){
if(chunks == null){
return;
}
if(chunks == null) return;
for(int x = 0; x < chunks.length; x++){
for(int y = 0; y < chunks[0].length; y++){
if(chunks[x][y] != null){

View File

@@ -11,6 +11,7 @@ import arc.scene.*;
import arc.scene.event.*;
import arc.scene.ui.layout.*;
import arc.util.*;
import mindustry.*;
import mindustry.graphics.*;
import mindustry.input.*;
import mindustry.ui.*;
@@ -18,7 +19,7 @@ import mindustry.ui.*;
import static mindustry.Vars.*;
public class MapView extends Element implements GestureListener{
EditorTool tool = EditorTool.pencil;
EditorTool tool = Vars.mobile ? EditorTool.zoom : EditorTool.pencil;
private float offsetx, offsety;
private float zoom = 1f;
private boolean grid = false;
@@ -204,7 +205,7 @@ public class MapView extends Element implements GestureListener{
zoom = Mathf.clamp(zoom, 0.2f, 20f);
}
Point2 project(float x, float y){
public Point2 project(float x, float y){
float ratio = 1f / ((float)editor.width() / editor.height());
float size = Math.min(width, height);
float sclwidth = size * zoom;

View File

@@ -145,7 +145,7 @@ abstract class BuilderComp implements Posc, Statusc, Teamc, Rotc{
!Structs.contains(current.block.requirements, i -> !core.items.has(i.item, Math.min(Mathf.round(i.amount * state.rules.buildCostMultiplier), 1)));
if(hasAll){
Call.beginPlace(self(), current.block, team, current.x, current.y, current.rotation);
Call.beginPlace(self(), current.block, team, current.x, current.y, current.rotation, current.block.instantBuild ? current.config : null);
if(!net.client() && current.block.instantBuild){
if(plans.size > 0){

View File

@@ -64,48 +64,7 @@ public class BlockRenderer{
});
Events.on(WorldLoadEvent.class, event -> {
blockTree = new BlockQuadtree(new Rect(0, 0, world.unitWidth(), world.unitHeight()));
blockLightTree = new BlockLightQuadtree(new Rect(0, 0, world.unitWidth(), world.unitHeight()));
floorTree = new FloorQuadtree(new Rect(0, 0, world.unitWidth(), world.unitHeight()));
shadowEvents.clear();
updateFloors.clear();
lastCamY = lastCamX = -99; //invalidate camera position so blocks get updated
hadMapLimit = state.rules.limitMapArea;
shadows.getTexture().setFilter(TextureFilter.linear, TextureFilter.linear);
shadows.resize(world.width(), world.height());
shadows.begin();
Core.graphics.clear(Color.white);
Draw.proj().setOrtho(0, 0, shadows.getWidth(), shadows.getHeight());
Draw.color(blendShadowColor);
for(Tile tile : world.tiles){
recordIndex(tile);
if(tile.floor().updateRender(tile)){
updateFloors.add(new UpdateRenderState(tile, tile.floor()));
}
if(tile.overlay().updateRender(tile)){
updateFloors.add(new UpdateRenderState(tile, tile.overlay()));
}
if(tile.build != null && (tile.team() == player.team() || !state.rules.fog || (tile.build.visibleFlags & (1L << player.team().id)) != 0)){
tile.build.wasVisible = true;
}
if(tile.block().displayShadow(tile) && (tile.build == null || tile.build.wasVisible)){
Fill.rect(tile.x + 0.5f, tile.y + 0.5f, 1, 1);
}
}
Draw.flush();
Draw.color();
shadows.end();
updateDarkness();
reload();
});
//sometimes darkness gets disabled.
@@ -150,6 +109,51 @@ public class BlockRenderer{
});
}
public void reload(){
blockTree = new BlockQuadtree(new Rect(0, 0, world.unitWidth(), world.unitHeight()));
blockLightTree = new BlockLightQuadtree(new Rect(0, 0, world.unitWidth(), world.unitHeight()));
floorTree = new FloorQuadtree(new Rect(0, 0, world.unitWidth(), world.unitHeight()));
shadowEvents.clear();
updateFloors.clear();
lastCamY = lastCamX = -99; //invalidate camera position so blocks get updated
hadMapLimit = state.rules.limitMapArea;
shadows.getTexture().setFilter(TextureFilter.linear, TextureFilter.linear);
shadows.resize(world.width(), world.height());
shadows.begin();
Core.graphics.clear(Color.white);
Draw.proj().setOrtho(0, 0, shadows.getWidth(), shadows.getHeight());
Draw.color(blendShadowColor);
for(Tile tile : world.tiles){
recordIndex(tile);
if(tile.floor().updateRender(tile)){
updateFloors.add(new UpdateRenderState(tile, tile.floor()));
}
if(tile.overlay().updateRender(tile)){
updateFloors.add(new UpdateRenderState(tile, tile.overlay()));
}
if(tile.build != null && (tile.team() == player.team() || !state.rules.fog || (tile.build.visibleFlags & (1L << player.team().id)) != 0)){
tile.build.wasVisible = true;
}
if(tile.block().displayShadow(tile) && (tile.build == null || tile.build.wasVisible)){
Fill.rect(tile.x + 0.5f, tile.y + 0.5f, 1, 1);
}
}
Draw.flush();
Draw.color();
shadows.end();
updateDarkness();
}
public void updateDarkness(){
darkEvents.clear();
dark.getTexture().setFilter(TextureFilter.linear);
@@ -197,6 +201,10 @@ public class BlockRenderer{
}
}
public FrameBuffer getShadowBuffer(){
return shadows;
}
public void removeFloorIndex(Tile tile){
if(indexFloor(tile)) floorTree.remove(tile);
}
@@ -294,7 +302,7 @@ public class BlockRenderer{
}
}
public void drawShadows(){
public void processShadows(){
if(!shadowEvents.isEmpty()){
Draw.flush();
@@ -315,6 +323,10 @@ public class BlockRenderer{
Draw.proj(camera);
}
}
public void drawShadows(){
processShadows();
float ww = world.width() * tilesize, wh = world.height() * tilesize;
float x = camera.position.x + tilesize / 2f, y = camera.position.y + tilesize / 2f;
@@ -511,6 +523,10 @@ public class BlockRenderer{
}
}
public void updateShadowTile(Tile tile){
shadowEvents.add(tile);
}
static class BlockQuadtree extends QuadTree<Tile>{
public BlockQuadtree(Rect bounds){

View File

@@ -97,20 +97,20 @@ public class CacheLayer{
@Override
public void begin(){
if(!Core.settings.getBool("animatedwater")) return;
if(!renderer.animateWater) return;
renderer.effectBuffer.begin();
Core.graphics.clear(Color.clear);
renderer.blocks.floor.beginc();
renderer.blocks.floor.beginDraw();
}
@Override
public void end(){
if(!Core.settings.getBool("animatedwater")) return;
if(!renderer.animateWater) return;
renderer.effectBuffer.end();
renderer.effectBuffer.blit(shader);
renderer.blocks.floor.beginc();
renderer.blocks.floor.beginDraw();
}
}
}

View File

@@ -111,10 +111,21 @@ public class FloorRenderer{
Events.on(WorldLoadEvent.class, event -> clearTiles());
}
public IndexData getIndexData(){
return indexData;
}
public float[] getVertexBuffer(){
return vertices;
}
/** Queues up a cache change for a tile. Only runs in render loop. */
public void recacheTile(Tile tile){
//recaching all layers may not be necessary
recacheSet.add(Point2.pack(tile.x / chunksize, tile.y / chunksize));
recacheTile(tile.x, tile.y);
}
public void recacheTile(int x, int y){
recacheSet.add(Point2.pack(x / chunksize, y / chunksize));
}
public void drawFloor(){
@@ -127,10 +138,10 @@ public class FloorRenderer{
float pad = tilesize/2f;
int
minx = (int)((camera.position.x - camera.width/2f - pad) / chunkunits),
miny = (int)((camera.position.y - camera.height/2f - pad) / chunkunits),
maxx = Mathf.ceil((camera.position.x + camera.width/2f + pad) / chunkunits),
maxy = Mathf.ceil((camera.position.y + camera.height/2f + pad) / chunkunits);
minx = Math.max((int)((camera.position.x - camera.width/2f - pad) / chunkunits), 0),
miny = Math.max((int)((camera.position.y - camera.height/2f - pad) / chunkunits), 0),
maxx = Math.min(Mathf.ceil((camera.position.x + camera.width/2f + pad) / chunkunits), cache.length),
maxy = Math.min(Mathf.ceil((camera.position.y + camera.height/2f + pad) / chunkunits), cache[0].length);
int layers = CacheLayer.all.length;
@@ -176,14 +187,6 @@ public class FloorRenderer{
underwaterDraw.clear();
}
public void beginc(){
shader.bind();
shader.setUniformMatrix4("u_projectionViewMatrix", Core.camera.mat);
//only ever use the base environment texture
texture.bind(0);
}
public void checkChanges(){
if(recacheSet.size > 0){
//recache one chunk at a time
@@ -208,7 +211,11 @@ public class FloorRenderer{
Draw.flush();
beginc();
shader.bind();
shader.setUniformMatrix4("u_projectionViewMatrix", Core.camera.mat);
//only ever use the base environment texture
texture.bind(0);
Gl.enable(Gl.blend);
}
@@ -221,10 +228,10 @@ public class FloorRenderer{
Camera camera = Core.camera;
int
minx = (int)((camera.position.x - camera.width/2f - pad) / chunkunits),
miny = (int)((camera.position.y - camera.height/2f - pad) / chunkunits),
maxx = Mathf.ceil((camera.position.x + camera.width/2f + pad) / chunkunits),
maxy = Mathf.ceil((camera.position.y + camera.height/2f + pad) / chunkunits);
minx = Math.max((int)((camera.position.x - camera.width/2f - pad) / chunkunits), 0),
miny = Math.max((int)((camera.position.y - camera.height/2f - pad) / chunkunits), 0),
maxx = Math.min(Mathf.ceil((camera.position.x + camera.width/2f + pad) / chunkunits), cache.length),
maxy = Math.min(Mathf.ceil((camera.position.y + camera.height/2f + pad) / chunkunits), cache[0].length);
layer.begin();
@@ -336,7 +343,7 @@ public class FloorRenderer{
(cx+1) * tilesize * chunksize + tilesize/2f, (cy+1) * tilesize * chunksize + tilesize/2f);
mesh.setVertices(vertices, 0, vidx);
//all vertices are shared
//all indices are shared and identical
mesh.indices = indexData;
return mesh;
@@ -514,7 +521,14 @@ public class FloorRenderer{
@Override
protected void draw(Texture texture, float[] spriteVertices, int offset, int count){
throw new IllegalArgumentException("cache vertices unsupported");
if(spriteVertices.length != spriteSize){
throw new IllegalArgumentException("cached vertices must be in non-mixcolor format (20 per sprite, 5 per vertex)");
}
float[] verts = vertices;
int idx = vidx;
System.arraycopy(spriteVertices, offset, verts, idx, spriteSize);
vidx += spriteSize;
}
}
}

View File

@@ -1,219 +0,0 @@
package mindustry.graphics;
import arc.graphics.*;
import arc.graphics.g2d.*;
import arc.graphics.gl.*;
import arc.math.*;
import arc.util.*;
public class IndexedRenderer implements Disposable{
private static final int vsize = 5;
private final static Shader program = new Shader(
"""
attribute vec4 a_position;
attribute vec4 a_color;
attribute vec2 a_texCoord0;
uniform mat4 u_projTrans;
varying vec4 v_color;
varying vec2 v_texCoords;
void main(){
v_color = a_color;
v_color.a = v_color.a * (255.0/254.0);
v_texCoords = a_texCoord0;
gl_Position = u_projTrans * a_position;
}
""",
"""
varying lowp vec4 v_color;
varying vec2 v_texCoords;
uniform sampler2D u_texture;
void main(){
gl_FragColor = v_color * texture2D(u_texture, v_texCoords);
}
"""
);
private static final float[] tmpVerts = new float[vsize * 6];
private Mesh mesh;
private Mat projMatrix = new Mat();
private Mat transMatrix = new Mat();
private Mat combined = new Mat();
private float color = Color.white.toFloatBits();
public IndexedRenderer(int sprites){
resize(sprites);
}
public void render(Texture texture){
Gl.enable(Gl.blend);
updateMatrix();
program.bind();
texture.bind();
program.setUniformMatrix4("u_projTrans", combined);
mesh.render(program, Gl.triangles, 0, mesh.getMaxVertices());
}
public void setColor(Color color){
this.color = color.toFloatBits();
}
public void draw(int index, TextureRegion region, float x, float y, float w, float h){
float fx2 = x + w;
float fy2 = y + h;
float u = region.u;
float v = region.v2;
float u2 = region.u2;
float v2 = region.v;
float[] vertices = tmpVerts;
float color = this.color;
int idx = 0;
vertices[idx++] = x;
vertices[idx++] = y;
vertices[idx++] = color;
vertices[idx++] = u;
vertices[idx++] = v;
vertices[idx++] = x;
vertices[idx++] = fy2;
vertices[idx++] = color;
vertices[idx++] = u;
vertices[idx++] = v2;
vertices[idx++] = fx2;
vertices[idx++] = fy2;
vertices[idx++] = color;
vertices[idx++] = u2;
vertices[idx++] = v2;
//tri2
vertices[idx++] = fx2;
vertices[idx++] = fy2;
vertices[idx++] = color;
vertices[idx++] = u2;
vertices[idx++] = v2;
vertices[idx++] = fx2;
vertices[idx++] = y;
vertices[idx++] = color;
vertices[idx++] = u2;
vertices[idx++] = v;
vertices[idx++] = x;
vertices[idx++] = y;
vertices[idx++] = color;
vertices[idx++] = u;
vertices[idx++] = v;
mesh.updateVertices(index * vsize * 6, vertices);
}
public void draw(int index, TextureRegion region, float x, float y, float w, float h, float rotation){
float u = region.u;
float v = region.v2;
float u2 = region.u2;
float v2 = region.v;
float originX = w / 2, originY = h / 2;
float cos = Mathf.cosDeg(rotation);
float sin = Mathf.sinDeg(rotation);
float fx = -originX;
float fy = -originY;
float fx2 = w - originX;
float fy2 = h - originY;
float worldOriginX = x + originX;
float worldOriginY = y + originY;
float x1 = cos * fx - sin * fy + worldOriginX;
float y1 = sin * fx + cos * fy + worldOriginY;
float x2 = cos * fx - sin * fy2 + worldOriginX;
float y2 = sin * fx + cos * fy2 + worldOriginY;
float x3 = cos * fx2 - sin * fy2 + worldOriginX;
float y3 = sin * fx2 + cos * fy2 + worldOriginY;
float x4 = x1 + (x3 - x2);
float y4 = y3 - (y2 - y1);
float[] vertices = tmpVerts;
float color = this.color;
int idx = 0;
vertices[idx++] = x1;
vertices[idx++] = y1;
vertices[idx++] = color;
vertices[idx++] = u;
vertices[idx++] = v;
vertices[idx++] = x2;
vertices[idx++] = y2;
vertices[idx++] = color;
vertices[idx++] = u;
vertices[idx++] = v2;
vertices[idx++] = x3;
vertices[idx++] = y3;
vertices[idx++] = color;
vertices[idx++] = u2;
vertices[idx++] = v2;
//tri2
vertices[idx++] = x3;
vertices[idx++] = y3;
vertices[idx++] = color;
vertices[idx++] = u2;
vertices[idx++] = v2;
vertices[idx++] = x4;
vertices[idx++] = y4;
vertices[idx++] = color;
vertices[idx++] = u2;
vertices[idx++] = v;
vertices[idx++] = x1;
vertices[idx++] = y1;
vertices[idx++] = color;
vertices[idx++] = u;
vertices[idx++] = v;
mesh.updateVertices(index * vsize * 6, vertices);
}
public Mat getTransformMatrix(){
return transMatrix;
}
public void setProjectionMatrix(Mat matrix){
projMatrix = matrix;
}
public void resize(int sprites){
if(mesh != null) mesh.dispose();
mesh = new Mesh(true, 6 * sprites, 0,
VertexAttribute.position,
VertexAttribute.color,
VertexAttribute.texCoords);
//TODO why is this the only way to get it working properly? it should not need an array
mesh.setVertices(new float[6 * sprites * vsize]);
}
private void updateMatrix(){
combined.set(projMatrix).mul(transMatrix);
}
@Override
public void dispose(){
mesh.dispose();
}
}

View File

@@ -360,6 +360,7 @@ public class MinimapRenderer{
if(tile == null) return 0;
Block real = realBlock(tile);
int bc = real.minimapColor(tile);
if(bc == 0 && tile.block() == Blocks.air && tile.overlay() == Blocks.air) bc = tile.floor().minimapColor(tile);
Color color = Tmp.c1.set(bc == 0 ? MapIO.colorFor(real, tile.floor(), tile.overlay(), tile.team()) : bc);
color.mul(1f - Mathf.clamp(world.getDarkness(tile.x, tile.y) / 4f));

View File

@@ -118,8 +118,7 @@ public class MultiPacker implements Disposable{
environment(4096),
ui(4096),
rubble(4096, 2048),
editor(4096, 2048);
rubble(4096, 2048);
public static final PageType[] all = values();

View File

@@ -1627,6 +1627,9 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
/** Draws a placement icon for a specific block. */
protected void drawPlan(int x, int y, Block block, int rotation){
bplan.set(x, y, rotation, block);
if(block.saveConfig){
bplan.config = block.lastConfig;
}
bplan.animScale = 1f;
block.drawPlan(bplan, allPlans(), validPlace(x, y, block, rotation));
}

View File

@@ -141,6 +141,23 @@ public class MapIO{
}
return tile;
}
@Override
public void onReadTileData(){
//colored floor/wall tile data will affect the map preview
if(!tile.block().synthetic() && tile.block() != Blocks.air){
int color = tile.block().minimapColor(tile);
if(color != 0){
walls.set(tile.x, walls.height - 1 - tile.y, color);
}
}else if(tile.overlay() == Blocks.air && tile.block() == Blocks.air){
int color = tile.floor().minimapColor(tile);
if(color != 0){
floors.set(tile.x, floors.height - 1 - tile.y, color);
}
}
}
}));
floors.draw(walls, true);
@@ -156,7 +173,14 @@ public class MapIO{
for(int x = 0; x < pixmap.width; x++){
for(int y = 0; y < pixmap.height; y++){
Tile tile = tiles.getn(x, y);
pixmap.set(x, pixmap.height - 1 - y, colorFor(tile.block(), tile.floor(), tile.overlay(), tile.team()));
int color = 0;
if(!tile.block().synthetic() && tile.block() != Blocks.air){
color = tile.block().minimapColor(tile);
}else if(tile.overlay() == Blocks.air && tile.block() == Blocks.air){
color = tile.floor().minimapColor(tile);
}
if(color == 0) color = colorFor(tile.block(), tile.floor(), tile.overlay(), tile.team());
pixmap.set(x, pixmap.height - 1 - y, color);
}
}
return pixmap;
@@ -202,7 +226,7 @@ public class MapIO{
for(Tile tile : tiles){
//default to stone floor
if(tile.floor() == Blocks.air){
tile.setFloorUnder((Floor)Blocks.stone);
tile.setFloor((Floor)Blocks.stone);
}
}
}

View File

@@ -20,7 +20,7 @@ public class SaveIO{
/** Save format header. */
public static final byte[] header = {'M', 'S', 'A', 'V'};
public static final IntMap<SaveVersion> versions = new IntMap<>();
public static final Seq<SaveVersion> versionArray = Seq.with(new Save1(), new Save2(), new Save3(), new Save4(), new Save5(), new Save6(), new Save7(), new Save8());
public static final Seq<SaveVersion> versionArray = Seq.with(new Save1(), new Save2(), new Save3(), new Save4(), new Save5(), new Save6(), new Save7(), new Save8(), new Save9());
static{
for(SaveVersion version : versionArray){

View File

@@ -209,7 +209,7 @@ public abstract class SaveVersion extends SaveFileReader{
//floor + overlay
for(int i = 0; i < world.width() * world.height(); i++){
Tile tile = world.rawTile(i % world.width(), i / world.width());
Tile tile = world.tiles.geti(i);
stream.writeShort(tile.floorID());
stream.writeShort(tile.overlayID());
int consecutives = 0;
@@ -230,16 +230,26 @@ public abstract class SaveVersion extends SaveFileReader{
//blocks
for(int i = 0; i < world.width() * world.height(); i++){
Tile tile = world.rawTile(i % world.width(), i / world.width());
Tile tile = world.tiles.geti(i);
stream.writeShort(tile.blockID());
boolean savedata = tile.floor().saveData || tile.overlay().saveData || tile.block().saveData;
boolean savedata = tile.shouldSaveData();
byte packed = (byte)((tile.build != null ? 1 : 0) | (savedata ? 2 : 0));
//in the old version, the second bit was set to indicate presence of data, but that approach was flawed - it didn't allow buildings + data on the same tile
//so now the third bit is used instead
byte packed = (byte)((tile.build != null ? 1 : 0) | (savedata ? 4 : 0));
//make note of whether there was an entity/rotation here
//make note of whether there was an entity or custom tile data here
stream.writeByte(packed);
if(savedata){
//the new 'extra data' format writes 7 bytes of data instead of 1
stream.writeByte(tile.data);
stream.writeByte(tile.floorData);
stream.writeByte(tile.overlayData);
stream.writeInt(tile.extraData);
}
//only write the entity for multiblocks once - in the center
if(tile.build != null){
if(tile.isCenter()){
@@ -251,16 +261,14 @@ public abstract class SaveVersion extends SaveFileReader{
}else{
stream.writeBoolean(false);
}
}else if(savedata){
stream.writeByte(tile.data);
}else{
}else if(!savedata){ //don't write consecutive blocks when there is custom data
//write consecutive non-entity blocks
int consecutives = 0;
for(int j = i + 1; j < world.width() * world.height() && consecutives < 255; j++){
Tile nextTile = world.rawTile(j % world.width(), j / world.width());
if(nextTile.blockID() != tile.blockID()){
if(nextTile.blockID() != tile.blockID() || savedata != nextTile.shouldSaveData()){
break;
}
@@ -310,7 +318,19 @@ public abstract class SaveVersion extends SaveFileReader{
boolean isCenter = true;
byte packedCheck = stream.readByte();
boolean hadEntity = (packedCheck & 1) != 0;
boolean hadData = (packedCheck & 2) != 0;
//old data format (bit 2): 1 byte only if no building is present
//new data format (bit 3): 7 bytes (3x block-specific bytes + 1x 4-byte extra data int)
boolean hadDataOld = (packedCheck & 2) != 0, hadDataNew = (packedCheck & 4) != 0;
byte data = 0, floorData = 0, overlayData = 0;
int extraData = 0;
if(hadDataNew){
data = stream.readByte();
floorData = stream.readByte();
overlayData = stream.readByte();
extraData = stream.readInt();
}
if(hadEntity){
isCenter = stream.readBoolean();
@@ -321,6 +341,15 @@ public abstract class SaveVersion extends SaveFileReader{
tile.setBlock(block);
}
//must be assigned after setBlock, because that can reset data
if(hadDataNew){
tile.data = data;
tile.floorData = floorData;
tile.overlayData = overlayData;
tile.extraData = extraData;
context.onReadTileData();
}
if(hadEntity){
if(isCenter){ //only read entity for center blocks
if(block.hasBuilding()){
@@ -339,9 +368,12 @@ public abstract class SaveVersion extends SaveFileReader{
context.onReadBuilding();
}
}else if(hadData){
tile.setBlock(block);
tile.data = stream.readByte();
}else if(hadDataOld || hadDataNew){ //never read consecutive blocks if there's any kind of data
if(hadDataOld){
tile.setBlock(block);
//the old data format was only read in the case where there is no building, and only contained a single byte
tile.data = stream.readByte();
}
}else{
int consecutives = stream.readUnsignedByte();

View File

@@ -2,6 +2,7 @@ package mindustry.io.versions;
import mindustry.io.*;
/** Adds support for the marker binary data region. The code is unchanged here, because it was easier to add a >= 8 check in the SaveVersion class itself. */
public class Save8 extends SaveVersion{
public Save8(){

View File

@@ -0,0 +1,11 @@
package mindustry.io.versions;
import mindustry.io.*;
/** Adds support for the new 7-byte custom tile data. This can read Save8 data, but Save8 doesn't know how to handle this version's output, thus the version change. */
public class Save9 extends SaveVersion{
public Save9(){
super(9);
}
}

View File

@@ -273,7 +273,6 @@ public class Mods implements Loadable{
ObjectMap<Texture, PageType> pageTypes = ObjectMap.of(
Core.atlas.find("white").texture, PageType.main,
Core.atlas.find("stone1").texture, PageType.environment,
Core.atlas.find("clear-editor").texture, PageType.editor,
Core.atlas.find("whiteui").texture, PageType.ui,
Core.atlas.find("rubble-1-0").texture, PageType.rubble
);
@@ -405,7 +404,6 @@ public class Mods implements Loadable{
String path = file.path();
return
path.contains("sprites/blocks/environment") || path.contains("sprites-override/blocks/environment") ? PageType.environment :
path.contains("sprites/editor") || path.contains("sprites-override/editor") ? PageType.editor :
path.contains("sprites/rubble") || path.contains("sprites-override/rubble") ? PageType.rubble :
path.contains("sprites/ui") || path.contains("sprites-override/ui") ? PageType.ui :
PageType.main;

View File

@@ -72,12 +72,28 @@ public class HudFragment{
}
});
Table[] configTable = {null};
Block[] lastBlock = {null};
cont.table(search -> {
search.image(Icon.zoom).padRight(8);
search.field("", text -> rebuildBlockSelection(blockSelection, text)).growX()
.name("editor/search").maxTextLength(maxNameLength).get().setMessageText("@players.search");
}).growX().pad(-2).padLeft(6f);
cont.row();
cont.collapser(t -> {
configTable[0] = t;
}, () -> control.input.block != null && control.input.block.editorConfigurable).with(c -> c.setEnforceMinSize(true)).update(col -> {
if(lastBlock[0] != control.input.block){
configTable[0].clear();
if(control.input.block != null){
control.input.block.buildEditorConfig(configTable[0]);
col.invalidateHierarchy();
}
lastBlock[0] = control.input.block;
}
}).growX().row();
cont.add(pane).expandY().top().left();
rebuildBlockSelection(blockSelection, "");

View File

@@ -126,41 +126,44 @@ public class PlacementFragment{
}
boolean updatePick(InputHandler input){
if(Core.input.keyTap(Binding.pick) && player.isBuilder() && !Core.scene.hasDialog()){ //mouse eyedropper select
var build = world.buildWorld(Core.input.mouseWorld().x, Core.input.mouseWorld().y);
Tile tile = world.tileWorld(Core.input.mouseWorldX(), Core.input.mouseWorldY());
if(tile != null && Core.input.keyTap(Binding.pick) && player.isBuilder() && !Core.scene.hasDialog()){ //mouse eyedropper select
var build = tile.build;
//can't middle click buildings in fog
if(build != null && build.inFogTo(player.team())){
build = null;
}
Block tryRecipe = build == null ? null : build instanceof ConstructBuild c ? c.current : build.block;
Block tryBlock = build == null ? null : build instanceof ConstructBuild c ? c.current : build.block;
Object tryConfig = build == null || !build.block.copyConfig ? null : build.config();
for(BuildPlan req : player.unit().plans()){
if(!req.breaking && req.block.bounds(req.x, req.y, Tmp.r1).contains(Core.input.mouseWorld())){
tryRecipe = req.block;
tryBlock = req.block;
tryConfig = req.config;
break;
}
}
if(tryRecipe == null && state.rules.editor){
var tile = world.tileWorld(Core.input.mouseWorldX(), Core.input.mouseWorldY());
if(tile != null){
tryRecipe =
if(tryBlock == null && state.rules.editor){
tryBlock =
tile.block() != Blocks.air ? tile.block() :
tile.overlay() != Blocks.air ? tile.overlay() :
tile.floor() != Blocks.air ? tile.floor() : null;
}
}
if(tryRecipe != null && ((tryRecipe.isVisible() && unlocked(tryRecipe)) || state.rules.editor)){
input.block = tryRecipe;
tryRecipe.lastConfig = tryConfig;
if(tryRecipe.isVisible()){
if(tryBlock != null && build == null && tryConfig == null){
tryConfig = tryBlock.getConfig(tile);
}
if(tryBlock != null && ((tryBlock.isVisible() && unlocked(tryBlock)) || state.rules.editor)){
input.block = tryBlock;
tryBlock.lastConfig = tryConfig;
if(tryBlock.isVisible()){
currentCategory = input.block.category;
}
tryBlock.onPicked(tile);
return true;
}
}

View File

@@ -80,6 +80,8 @@ public class Block extends UnlockableContent implements Senseable{
public boolean displayFlow = true;
/** whether this block is visible in the editor */
public boolean inEditor = true;
/** if true, {@link #buildEditorConfig(Table)} will be called for configuring this block in the editor. */
public boolean editorConfigurable;
/** the last configuration value applied to this block. */
public @Nullable Object lastConfig;
/** whether to save the last config and apply it to newly placed blocks */
@@ -380,11 +382,10 @@ public class Block extends UnlockableContent implements Senseable{
protected Seq<Consume> consumeBuilder = new Seq<>();
protected TextureRegion[] generatedIcons;
protected TextureRegion[] editorVariantRegions;
/** Regions indexes from icons() that are rotated. If either of these is not -1, other regions won't be rotated in ConstructBlocks. */
public int regionRotated1 = -1, regionRotated2 = -1;
public TextureRegion region, editorIcon;
public TextureRegion region;
public @Load("@-shadow") TextureRegion customShadowRegion;
public @Load("@-team") TextureRegion teamRegion;
public TextureRegion[] teamRegions, variantRegions, variantShadowRegions;
@@ -566,6 +567,11 @@ public class Block extends UnlockableContent implements Senseable{
return forceDark;
}
/** If true, the 'map edge' darkness will be applied to this block. */
public boolean isDarkened(Tile tile){
return solid && ((!synthetic() && fillsTile) || checkForceDark(tile));
}
@Override
public void setStats(){
super.setStats();
@@ -857,24 +863,6 @@ public class Block extends UnlockableContent implements Senseable{
}
}
/** Never use outside of the editor! */
public TextureRegion editorIcon(){
return editorIcon == null ? (editorIcon = Core.atlas.find(name + "-icon-editor")) : editorIcon;
}
/** Never use outside of the editor! */
public TextureRegion[] editorVariantRegions(){
if(editorVariantRegions == null){
variantRegions();
editorVariantRegions = new TextureRegion[variantRegions.length];
for(int i = 0; i < variantRegions.length; i++){
AtlasRegion region = (AtlasRegion)variantRegions[i];
editorVariantRegions[i] = Core.atlas.find("editor-" + region.name);
}
}
return editorVariantRegions;
}
/** @return special icons to outline and save with an -outline variant. Vanilla only. */
public TextureRegion[] makeIconRegions(){
return new TextureRegion[0];
@@ -940,6 +928,21 @@ public class Block extends UnlockableContent implements Senseable{
return (envEnabled & env) != 0 && (envDisabled & env) == 0 && (envRequired == 0 || (envRequired & env) == envRequired);
}
/** Called to set up configuration UI in the editor. {@link #editorConfigurable} must be true.
* Config value should be assigned to lastConfig.*/
public void buildEditorConfig(Table table){}
/** Called when the block is picked (middle click). Clientside only! */
public void onPicked(Tile tile){}
/** @return the config value returned when this block is picked on a certain tile. This is only called for non-buildings. */
public Object getConfig(Tile tile){
return null;
}
/** Called when this block is set on the specified tile. */
public void blockChanged(Tile tile){}
/** Called when building of this block begins. */
public void placeBegan(Tile tile, Block previous){
@@ -951,7 +954,7 @@ public class Block extends UnlockableContent implements Senseable{
}
/** Called when building of this block ends. */
public void placeEnded(Tile tile, @Nullable Unit builder){
public void placeEnded(Tile tile, @Nullable Unit builder, int rotation, @Nullable Object config){
}
@@ -1366,13 +1369,6 @@ public class Block extends UnlockableContent implements Senseable{
mapColor.set(image.get(image.width/2, image.height/2));
}
if(variants > 0){
for(int i = 0; i < variants; i++){
String rname = name + (i + 1);
packer.add(PageType.editor, "editor-" + rname, Core.atlas.getPixmap(rname));
}
}
Seq<Pixmap> toDispose = new Seq<>();
//generate paletted team regions
@@ -1437,8 +1433,6 @@ public class Block extends UnlockableContent implements Senseable{
}
}
PixmapRegion editorBase;
if(gen.length > 1){
Pixmap base = Core.atlas.getPixmap(gen[0]).crop();
for(int i = 1; i < gen.length; i++){
@@ -1450,15 +1444,11 @@ public class Block extends UnlockableContent implements Senseable{
}
packer.add(PageType.main, "block-" + name + "-full", base);
editorBase = new PixmapRegion(base);
toDispose.add(base);
}else{
if(gen[0] != null) packer.add(PageType.main, "block-" + name + "-full", Core.atlas.getPixmap(gen[0]));
editorBase = gen[0] == null ? Core.atlas.getPixmap(fullIcon) : Core.atlas.getPixmap(gen[0]);
}
packer.add(PageType.editor, name + "-icon-editor", editorBase);
toDispose.each(Pixmap::dispose);
}

View File

@@ -66,9 +66,9 @@ public class Build{
Events.fire(new BlockBuildBeginEvent(tile, team, unit, true));
}
/** Places a ConstructBlock at this location. */
/** Places a ConstructBlock at this location. To preserve bandwidth, a config is only passed in the case of instant-place blocks. */
@Remote(called = Loc.server)
public static void beginPlace(@Nullable Unit unit, Block result, Team team, int x, int y, int rotation){
public static void beginPlace(@Nullable Unit unit, Block result, Team team, int x, int y, int rotation, @Nullable Object placeConfig){
if(!validPlace(result, team, x, y, rotation)){
return;
}
@@ -127,7 +127,7 @@ public class Build{
if(result.instantBuild){
Events.fire(new BlockBuildBeginEvent(tile, team, unit, false));
result.placeBegan(tile, tile.block, unit);
ConstructBlock.constructFinish(tile, result, unit, (byte)rotation, team, null);
ConstructBlock.constructFinish(tile, result, unit, (byte)rotation, team, placeConfig);
return;
}

View File

@@ -26,8 +26,14 @@ public class Tile implements Position, QuadTreeObject, Displayable{
private static final TileFloorChangeEvent floorChange = new TileFloorChangeEvent();
private static final ObjectSet<Building> tileSet = new ObjectSet<>();
/** Extra data for very specific blocks. */
public byte data;
/**
* Extra data for specific blocks. Only saved if Block#saveData is true.
* It is generally recommended that blocks only access data in their own category unless necessary - for example, a floor should not read/write overlay data.
* However, one byte may sometimes not be enough to hold enough data, in which case "overlapping" data storage is necessary.
* */
public byte data, floorData, overlayData;
/** Even more data for blocks. Use with caution; any floor/block can access this value. Due to 8-byte alignment of Java objects, this extra 4-byte field can be added with no additional cost.*/
public int extraData;
/** Tile entity, usually null. */
public @Nullable Building build;
public short x, y;
@@ -166,7 +172,7 @@ public class Tile implements Position, QuadTreeObject, Displayable{
}
public boolean isDarkened(){
return block.solid && ((!block.synthetic() && block.fillsTile) || block.checkForceDark(this));
return block.isDarkened(this);
}
public Floor floor(){
@@ -274,6 +280,8 @@ public class Tile implements Position, QuadTreeObject, Displayable{
changed();
changing = false;
block.blockChanged(this);
}
public void setBlock(Block type, Team team){
@@ -284,11 +292,11 @@ public class Tile implements Position, QuadTreeObject, Displayable{
setBlock(type, Team.derelict, 0);
}
/** This resets the overlay! */
public void setFloor(Floor type){
if(this.floor == type) return;
var prev = this.floor;
this.floor = type;
this.overlay = (Floor)Blocks.air;
if(!headless && !world.isGenerating() && !isEditorTile()){
renderer.blocks.removeFloorIndex(this);
@@ -302,24 +310,19 @@ public class Tile implements Position, QuadTreeObject, Displayable{
pathfinder.updateTile(this);
}
if(!world.isGenerating() && prev != type){
if(!world.isGenerating()){
Events.fire(floorChange.set(this, prev, type));
}
if(this.floor != prev){
this.floor.floorChanged(this);
}
}
public boolean isEditorTile(){
return false;
}
/** Sets the floor, preserving overlay.*/
public void setFloorUnder(Floor floor){
Block overlay = this.overlay;
setFloor(floor);
if(this.overlay != overlay){
setOverlay(overlay);
}
}
/** Sets the block to air. */
public void setAir(){
setBlock(Blocks.air);
@@ -402,11 +405,9 @@ public class Tile implements Position, QuadTreeObject, Displayable{
return floor.id;
}
public void setOverlayID(short ore){
setOverlay(content.block(ore));
}
public void setOverlay(Block block){
if(this.overlay == block) return;
this.overlay = (Floor)block;
recache();
@@ -421,7 +422,7 @@ public class Tile implements Position, QuadTreeObject, Displayable{
}
public void clearOverlay(){
setOverlayID((short)0);
setOverlay(Blocks.air);
}
public boolean passable(){
@@ -558,6 +559,10 @@ public class Tile implements Position, QuadTreeObject, Displayable{
null : null;
}
public boolean shouldSaveData(){
return floor.saveData || overlay.saveData || block.saveData;
}
public int staticDarkness(){
return block.solid && block.fillsTile && !block.synthetic() ? data : 0;
}

View File

@@ -26,6 +26,9 @@ public interface WorldContext{
/** Called when a building is finished reading. */
default void onReadBuilding(){}
/** Called when data finishes reading for a tile. */
default void onReadTileData(){}
default @Nullable Sector getSector(){
return null;
}

View File

@@ -78,7 +78,7 @@ public class ConstructBlock extends Block{
if(block instanceof OverlayFloor overlay){
tile.setOverlay(overlay);
}else if(block instanceof Floor floor){
tile.setFloorUnder(floor);
tile.setFloor(floor);
}else{
tile.setBlock(block, team, rotation);
}
@@ -112,7 +112,7 @@ public class ConstructBlock extends Block{
if(shouldPlay()) block.placeSound.at(tile, block.placePitchChange ? calcPitch(true) : 1f);
}
block.placeEnded(tile, builder);
block.placeEnded(tile, builder, rotation, config);
Events.fire(new BlockBuildEndEvent(tile, builder, team, false, config));
}

View File

@@ -1,5 +1,8 @@
package mindustry.world.blocks;
import arc.*;
import arc.graphics.g2d.*;
public class TileBitmask{
/** Autotile bitmasks for 8-directional sprites (see <a href="https://github.com/GglLfr/tile-gen">tile-gen</a>)*/
public static final int[] values = {
@@ -20,4 +23,23 @@ public class TileBitmask{
3, 0, 3, 0, 15, 42, 15, 12, 3, 0, 3, 0, 15, 42, 15, 12,
2, 1, 2, 1, 9, 45, 9, 19, 2, 1, 2, 1, 14, 18, 14, 13,
};
public static TextureRegion[] load(String name){
var regions = new TextureRegion[47];
for(int i = 0; i < 47; i++){
regions[i] = Core.atlas.find(name + "-" + i);
}
return regions;
}
public static TextureRegion[][] loadVariants(String name, int variants){
var regions = new TextureRegion[variants][47];
for(int v = 0; v < variants; v++){
for(int i = 0; i < 47; i++){
regions[v][i] = Core.atlas.find(name + "-" + (v+1) + "-" + i);
}
}
return regions;
}
}

View File

@@ -0,0 +1,95 @@
package mindustry.world.blocks.environment;
import arc.graphics.*;
import arc.graphics.g2d.*;
import arc.scene.ui.layout.*;
import arc.util.*;
import mindustry.*;
import mindustry.annotations.Annotations.*;
import mindustry.entities.units.*;
import mindustry.gen.*;
import mindustry.world.*;
public class CharacterOverlay extends OverlayFloor{
/** This is a special reduced character set that fits in 6 bits! It is not ASCII! */
public static final String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\"!?.,;:()[]{}<>|/@\\^-%+=#_&~";
public @Load(value = "character-overlay#", length = 64) TextureRegion[] letterRegions;
public Color color = Color.white;
public CharacterOverlay(String name){
super(name);
saveData = true;
variants = 0;
rotate = true;
drawArrow = false;
saveConfig = true;
editorConfigurable = true;
}
@Override
public void drawBase(Tile tile){
Draw.color(color);
int letterChar = CharOverlayData.character(tile.overlayData);
Draw.rect(letterRegions[letterChar], tile.worldx(), tile.worldy(), CharOverlayData.rotation(tile.overlayData) * 90f);
Draw.color();
}
@Override
public Object getConfig(Tile tile){
return (int)tile.overlayData;
}
@Override
public void drawPlanRegion(BuildPlan plan, Eachable<BuildPlan> list){
byte data = 0;
if(plan.config instanceof Integer i){
data = i.byteValue();
}
int letterChar = CharOverlayData.character(data);
TextureRegion reg = letterRegions[letterChar];
Draw.tint(color);
Draw.rect(reg, plan.drawx(), plan.drawy(), plan.rotation * 90);
Draw.tint(Color.white);
}
@Override
public void onPicked(Tile tile){
Vars.control.input.rotation = CharOverlayData.rotation(tile.overlayData);
}
@Override
public void buildEditorConfig(Table table){
char value = chars.charAt(lastConfig instanceof Integer i ? CharOverlayData.character(i.byteValue()) : 0);
table.field(value + "", val -> {
if(val.length() == 1){
lastConfig = (int)charToData(val.charAt(0));
}
}).valid(t -> t.length() == 1 && chars.indexOf(Character.toUpperCase(t.charAt(0))) != -1).maxTextLength(1);
}
@Override
public void placeEnded(Tile tile, @Nullable Unit builder, int rotation, @Nullable Object config){
byte data = 0;
if(config instanceof Integer i){
data = i.byteValue();
}
tile.overlayData = CharOverlayData.get(data, (byte)rotation);
}
public static byte charToData(char c){
int index = chars.indexOf(Character.toUpperCase(c));
return index == -1 ? 0 : (byte)index;
}
@Struct
class CharOverlayDataStruct{
@StructField(6)
byte character;
@StructField(2)
byte rotation;
}
}

View File

@@ -9,7 +9,6 @@ import mindustry.world.*;
public class Cliff extends Block{
public float size = 11f;
public @Load(value = "cliffmask#", length = 256) TextureRegion[] cliffs;
public @Load(value = "editor-cliffmask#", length = 256) TextureRegion[] editorCliffs;
public Cliff(String name){
super(name);

View File

@@ -0,0 +1,199 @@
package mindustry.world.blocks.environment;
import arc.graphics.*;
import arc.graphics.g2d.*;
import arc.math.geom.*;
import arc.scene.ui.layout.*;
import arc.util.*;
import mindustry.*;
import mindustry.entities.units.*;
import mindustry.gen.*;
import mindustry.ui.*;
import mindustry.world.*;
import mindustry.world.blocks.*;
import static mindustry.Vars.*;
public class ColoredFloor extends Floor{
/** If the alpha value of the color is set to this value, different colors are ignored and no border is drawn. */
public static final int flagIgnoreDifferentColor = 1;
/** If the alpha value of the color is set to this value, colors are interpolated across corners. This is essentially linear filtering for the whole "image". */
public static final int flagSmoothBlend = 2;
private static final float[] verts = new float[20];
public Color defaultColor = Color.white;
protected int defaultColorRgba;
public ColoredFloor(String name){
super(name);
saveData = true;
editorConfigurable = true;
saveConfig = true;
}
@Override
public void init(){
super.init();
lastConfig = defaultColorRgba = defaultColor.rgba();
}
@Override
public void buildEditorConfig(Table table){
showColorEdit(table, this);
}
public static void showColorEdit(Table t, Block block){
t.button(b -> {
b.margin(4f);
b.left();
b.table(Tex.pane, in -> {
in.image(Tex.whiteui).update(i -> {
if(block.lastConfig instanceof Integer col){
i.color.set(col | 0xff);
}
}).grow();
}).margin(4).size(50f).padRight(10);
b.add("@color");
}, Styles.cleart, () ->
ui.picker.show(
block.lastConfig instanceof Integer col ? new Color(col | 0xff) : new Color(Color.white), false,
col -> block.lastConfig = col.rgba8888())).left().width(250f).pad(3f).row();
}
@Override
public Object getConfig(Tile tile){
return tile.extraData;
}
@Override
public void drawBase(Tile tile){
//make sure to mask out the alpha channel - it's generally undesirable, and leads to invisible blocks when the data is not initialized
Draw.color(tile.extraData | 0xff);
if((tile.extraData & 0xff) == flagSmoothBlend && autotile){
//Only autotiling is supported right now for the sake of simplicity
int bits = 0;
for(int i = 0; i < 8; i++){
Tile other = tile.nearby(Geometry.d8[i]);
//force flagIgnoreDifferentColor by bypassing checkAutotileSame
if(other != null && other.floor().blendGroup == blendGroup){
bits |= (1 << i);
}
}
var region = autotileRegions[TileBitmask.values[bits]];
float s = Vars.tilesize/2f;
float x = tile.worldx(), y = tile.worldy();
verts[0] = x - s;
verts[1] = y - s;
verts[2] = sample(this, tile.x, tile.y, tile.x - 1, tile.y - 1, tile.x, tile.y - 1, tile.x - 1, tile.y);
verts[3] = region.u;
verts[4] = region.v2;
verts[5] = x + s;
verts[6] = y - s;
verts[7] = sample(this, tile.x, tile.y, tile.x, tile.y - 1, tile.x + 1, tile.y - 1, tile.x + 1, tile.y);
verts[8] = region.u2;
verts[9] = region.v2;
verts[10] = x + s;
verts[11] = y + s;
verts[12] = sample(this, tile.x, tile.y, tile.x + 1, tile.y + 1, tile.x, tile.y + 1, tile.x + 1, tile.y);
verts[13] = region.u2;
verts[14] = region.v;
verts[15] = x - s;
verts[16] = y + s;
verts[17] = sample(this, tile.x, tile.y, tile.x - 1, tile.y + 1, tile.x, tile.y + 1, tile.x - 1, tile.y);
verts[18] = region.u;
verts[19] = region.v;
Draw.vert(region.texture, verts, 0, verts.length);
}else{
super.drawBase(tile);
}
Draw.color();
}
static float sample(Block target, int tx1, int ty1, int tx2, int ty2, int tx3, int ty3, int tx4, int ty4){
int total = 0;
float r = 0f, g = 0f, b = 0f;
Tile t1 = Vars.world.tile(tx1, ty1);
Tile t2 = Vars.world.tile(tx2, ty2);
Tile t3 = Vars.world.tile(tx3, ty3);
Tile t4 = Vars.world.tile(tx4, ty4);
//manually unrolled loops, hooray
if(t1 != null && t1.floor() == target){
total ++;
r += Color.ri(t1.extraData);
g += Color.gi(t1.extraData);
b += Color.bi(t1.extraData);
}
if(t2 != null && t2.floor() == target){
total ++;
r += Color.ri(t2.extraData);
g += Color.gi(t2.extraData);
b += Color.bi(t2.extraData);
}
if(t3 != null && t3.floor() == target){
total ++;
r += Color.ri(t3.extraData);
g += Color.gi(t3.extraData);
b += Color.bi(t3.extraData);
}
if(t4 != null && t4.floor() == target){
total ++;
r += Color.ri(t4.extraData);
g += Color.gi(t4.extraData);
b += Color.bi(t4.extraData);
}
return Color.toFloatBits((int)(r/total), (int)(g/total), (int)(b/total), 255);
}
@Override
public void drawOverlay(Tile tile){
//make sure color doesn't carry over
Draw.color();
super.drawOverlay(tile);
}
@Override
public void floorChanged(Tile tile){
//reset to white
if(tile.extraData == 0){
tile.extraData = defaultColorRgba;
}
}
@Override
public void placeEnded(Tile tile, @Nullable Unit builder, int rotation, @Nullable Object config){
//config is assumed to be an integer RGBA color
if(config instanceof Integer i){
tile.extraData = i;
}
}
@Override
public void drawPlanRegion(BuildPlan plan, Eachable<BuildPlan> list){
if(plan.config instanceof Integer i){
Draw.tint(Tmp.c1.set(i | 0xff));
}
drawDefaultPlanRegion(plan, list);
}
@Override
public boolean checkAutotileSame(Tile tile, @Nullable Tile other){
return other != null && other.floor().blendGroup == blendGroup && ((tile.extraData & 0xff) == flagIgnoreDifferentColor || tile.extraData == other.extraData);
}
@Override
public int minimapColor(Tile tile){
return tile.extraData | 0xff;
}
}

View File

@@ -0,0 +1,89 @@
package mindustry.world.blocks.environment;
import arc.graphics.*;
import arc.graphics.g2d.*;
import arc.scene.ui.layout.*;
import arc.util.*;
import mindustry.entities.units.*;
import mindustry.gen.*;
import mindustry.world.*;
public class ColoredWall extends StaticWall{
/** If the alpha value of the color is set to this value, different colors are ignored and no border is drawn. */
public static final int flagIgnoreDifferentColor = 1;
/** If the alpha value of the color is set to this value, the wall will have darkness applied, as other walls do. */
public static final int flagApplyDarkness = 2;
public Color defaultColor = Color.white;
protected int defaultColorRgba;
public ColoredWall(String name){
super(name);
saveData = true;
editorConfigurable = true;
saveConfig = true;
}
@Override
public void init(){
super.init();
lastConfig = defaultColorRgba = defaultColor.rgba();
}
@Override
public Object getConfig(Tile tile){
return tile.extraData;
}
@Override
public void buildEditorConfig(Table table){
ColoredFloor.showColorEdit(table, this);
}
@Override
public void drawBase(Tile tile){
//make sure to mask out the alpha channel - it's generally undesirable, and leads to invisible blocks when thtoe data is not initialized
Draw.color(tile.extraData | 0xff);
super.drawBase(tile);
Draw.color();
}
@Override
public void blockChanged(Tile tile){
//reset to white on first placement
if(tile.extraData == 0){
tile.extraData = defaultColorRgba;
}
}
@Override
public void placeEnded(Tile tile, @Nullable Unit builder, int rotation, @Nullable Object config){
//config is assumed to be an integer RGBA color
if(config instanceof Integer i){
tile.extraData = i;
}
}
@Override
public void drawPlanRegion(BuildPlan plan, Eachable<BuildPlan> list){
if(plan.config instanceof Integer i){
Draw.tint(Tmp.c1.set(i | 0xff));
}
drawDefaultPlanRegion(plan, list);
}
@Override
public boolean checkAutotileSame(Tile tile, @Nullable Tile other){
return other != null && other.block() == this && ((tile.extraData == flagIgnoreDifferentColor) || tile.extraData == other.extraData);
}
@Override
public boolean isDarkened(Tile tile){
return (tile.extraData == flagApplyDarkness);
}
@Override
public int minimapColor(Tile tile){
return tile.extraData | 0xff;
}
}

View File

@@ -80,9 +80,18 @@ public class Floor extends Block{
public int tilingVariants = 0;
/** If true, this floor uses autotiling; variants are not supported. See https://github.com/GglLfr/tile-gen*/
public boolean autotile = false;
/** If >1, the middle region of the autotile has random variants. */
public int autotileMidVariants = 1;
/** Variants of the main autotile sprite. */
public int autotileVariants = 1;
/** If true (default), this floor will draw edges of other floors on itself. */
public boolean drawEdgeIn = true;
/** If true (default), this floor will draw its edges onto other floors. */
public boolean drawEdgeOut = true;
protected TextureRegion[][][] tilingRegions;
protected TextureRegion[] autotileRegions;
protected TextureRegion[] autotileRegions, autotileMidRegions;
protected TextureRegion[][] autotileVariantRegions;
protected int tilingSize;
protected TextureRegion[][] edges;
protected Seq<Floor> blenders = new Seq<>();
@@ -141,9 +150,15 @@ public class Floor extends Block{
}
if(autotile){
autotileRegions = new TextureRegion[47];
for(int i = 0; i < 47; i++){
autotileRegions[i] = Core.atlas.find(name + "-" + i);
autotileRegions = TileBitmask.load(name);
if(autotileVariants > 1){
autotileVariantRegions = TileBitmask.loadVariants(name, autotileVariants);
}
if(autotileMidVariants > 1){
autotileMidRegions = new TextureRegion[autotileMidVariants];
for(int i = 0; i < autotileMidVariants; i++){
autotileMidRegions[i] = Core.atlas.find(i == 0 ? name + "-13" : name + "-mid-" + (i + 1));
}
}
}
@@ -195,7 +210,6 @@ public class Floor extends Block{
@Override
public void createIcons(MultiPacker packer){
super.createIcons(packer);
packer.add(PageType.editor, "editor-" + name, Core.atlas.getPixmap(fullIcon));
if(blendGroup != this){
return;
@@ -226,25 +240,40 @@ public class Floor extends Block{
}else if(autotile){
int bits = 0;
TextureRegion[] regions = autotileVariants > 1 ? autotileVariantRegions[variant(tile.x, tile.y, autotileVariantRegions.length)] : autotileRegions;
for(int i = 0; i < 8; i++){
Tile other = tile.nearby(Geometry.d8[i]);
if(other != null && other.floor().blendGroup == blendGroup){
if(checkAutotileSame(tile, other)){
bits |= (1 << i);
}
}
Draw.rect(autotileRegions[TileBitmask.values[bits]], tile.worldx(), tile.worldy());
int bit = TileBitmask.values[bits];
TextureRegion region = bit == 13 && autotileMidVariants > 1 ? autotileMidRegions[variant(tile.x, tile.y, autotileMidRegions.length)] : regions[bit];
Draw.rect(region, tile.worldx(), tile.worldy());
}else{
Draw.rect(variantRegions[variant(tile.x, tile.y)], tile.worldx(), tile.worldy());
}
Draw.alpha(1f);
drawEdges(tile);
if(drawEdgeIn){
drawEdges(tile);
}
drawOverlay(tile);
}
public boolean checkAutotileSame(Tile tile, @Nullable Tile other){
return other != null && other.floor().blendGroup == blendGroup;
}
public int variant(int x, int y){
return Mathf.randomSeed(Point2.pack(x, y), 0, Math.max(0, variantRegions.length - 1));
return variant(x, y, variantRegions.length);
}
public int variant(int x, int y, int max){
return Mathf.randomSeed(Point2.pack(x, y), 0, Math.max(0, max - 1));
}
public void drawOverlay(Tile tile){
@@ -265,6 +294,9 @@ public class Floor extends Block{
return new TextureRegion[]{Core.atlas.find(Core.atlas.has(name) ? name : name + "1")};
}
/** Called when this floor is set on the specified tile. */
public void floorChanged(Tile tile){}
/** @return whether to index this floor by flag */
public boolean shouldIndex(Tile tile){
return true;
@@ -298,7 +330,7 @@ public class Floor extends Block{
Point2 point = Geometry.d8[i];
Tile other = tile.nearby(point);
//special case: empty is, well, empty, so never draw emptiness on top, as that would just be an incorrect black texture
if(other != null && other.floor().cacheLayer == layer && other.floor().edges(tile.x, tile.y) != null && other.floor() != Blocks.empty){
if(other != null && other.floor().drawEdgeOut && other.floor().cacheLayer == layer && other.floor().edges(tile.x, tile.y) != null){
if(!blended.getAndSet(other.floor().id)){
blenders.add(other.floor());
dirs[i] = other.floorID();
@@ -319,7 +351,7 @@ public class Floor extends Block{
Point2 point = Geometry.d8[i];
Tile other = tile.nearby(point);
if(other != null && doEdge(tile, other, other.floor()) && other.floor().cacheLayer == realCache && other.floor().edges(tile.x, tile.y) != null && other.floor() != Blocks.empty){
if(other != null && other.floor().drawEdgeOut && doEdge(tile, other, other.floor()) && other.floor().cacheLayer == realCache && other.floor().edges(tile.x, tile.y) != null){
if(!blended.getAndSet(other.floor().id)){
blenders.add(other.floor());
}
@@ -345,19 +377,6 @@ public class Floor extends Block{
}
}
//'new' style of edges with shadows instead of colors, not used currently
protected void drawEdgesFlat(Tile tile, boolean sameLayer){
for(int i = 0; i < 4; i++){
Tile other = tile.nearby(i);
if(other != null && doEdge(tile, other, other.floor())){
Color color = other.floor().mapColor;
Draw.color(color.r, color.g, color.b, 1f);
Draw.rect(edgeRegion, tile.worldx(), tile.worldy(), i*90);
}
}
Draw.color();
}
public int realBlendId(Tile tile){
if(tile.floor().isLiquid && !tile.overlay().isAir() && !(tile.overlay() instanceof OreBlock)){
return -((tile.overlay().blendId) | (tile.floor().blendId << 15));

View File

@@ -63,10 +63,8 @@ public class OreBlock extends OverlayFloor{
}
packer.add(PageType.environment, name + (i + 1), image);
packer.add(PageType.editor, "editor-" + name + (i + 1), image);
if(i == 0){
packer.add(PageType.editor, "editor-block-" + name + "-full", image);
packer.add(PageType.main, "block-" + name + "-full", image);
}

View File

@@ -44,7 +44,7 @@ public class RemoveOre extends OverlayFloor{
}
@Override
public void placeEnded(Tile tile, @Nullable Unit builder){
public void placeEnded(Tile tile, @Nullable Unit builder, int rotation, Object config){
tile.setOverlay(Blocks.air);
}

View File

@@ -43,7 +43,7 @@ public class RemoveWall extends Block{
}
@Override
public void placeEnded(Tile tile, @Nullable Unit builder){
public void placeEnded(Tile tile, @Nullable Unit builder, int rotation, Object config){
tile.setBlock(Blocks.air);
if(tile.overlay().wallOre){
tile.setOverlay(Blocks.air);

View File

@@ -47,9 +47,8 @@ public class ShallowLiquid extends Floor{
}
}
String baseName = this.name + "" + (++index);
String baseName = this.name + (++index);
packer.add(PageType.environment, baseName, res);
packer.add(PageType.editor, "editor-" + baseName, res);
res.dispose();
}

View File

@@ -4,16 +4,22 @@ import arc.*;
import arc.graphics.g2d.*;
import arc.math.*;
import arc.math.geom.*;
import arc.util.*;
import mindustry.annotations.Annotations.*;
import mindustry.content.*;
import mindustry.graphics.*;
import mindustry.world.*;
import mindustry.world.blocks.*;
import static mindustry.Vars.*;
public class StaticWall extends Prop{
public @Load("@-large") TextureRegion large;
public TextureRegion[][] split;
/** If true, this wall uses autotiling; variants are not supported. See https://github.com/GglLfr/tile-gen*/
public boolean autotile;
protected TextureRegion[] autotileRegions;
public StaticWall(String name){
super(name);
@@ -30,15 +36,28 @@ public class StaticWall extends Prop{
@Override
public void drawBase(Tile tile){
int rx = tile.x / 2 * 2;
int ry = tile.y / 2 * 2;
if(autotile){
int bits = 0;
if(Core.atlas.isFound(large) && eq(rx, ry) && Mathf.randomSeed(Point2.pack(rx, ry)) < 0.5 && split.length >= 2 && split[0].length >= 2){
Draw.rect(split[tile.x % 2][1 - tile.y % 2], tile.worldx(), tile.worldy());
}else if(variants > 0){
Draw.rect(variantRegions[Mathf.randomSeed(tile.pos(), 0, Math.max(0, variantRegions.length - 1))], tile.worldx(), tile.worldy());
for(int i = 0; i < 8; i++){
Tile other = tile.nearby(Geometry.d8[i]);
if(checkAutotileSame(tile, other)){
bits |= (1 << i);
}
}
Draw.rect(autotileRegions[TileBitmask.values[bits]], tile.worldx(), tile.worldy());
}else{
Draw.rect(region, tile.worldx(), tile.worldy());
int rx = tile.x / 2 * 2;
int ry = tile.y / 2 * 2;
if(Core.atlas.isFound(large) && eq(rx, ry) && Mathf.randomSeed(Point2.pack(rx, ry)) < 0.5 && split.length >= 2 && split[0].length >= 2){
Draw.rect(split[tile.x % 2][1 - tile.y % 2], tile.worldx(), tile.worldy());
}else if(variants > 0){
Draw.rect(variantRegions[Mathf.randomSeed(tile.pos(), 0, Math.max(0, variantRegions.length - 1))], tile.worldx(), tile.worldy());
}else{
Draw.rect(region, tile.worldx(), tile.worldy());
}
}
//draw ore on top
@@ -47,6 +66,10 @@ public class StaticWall extends Prop{
}
}
public boolean checkAutotileSame(Tile tile, @Nullable Tile other){
return other != null && other.block() == this;
}
@Override
public void load(){
super.load();
@@ -59,6 +82,10 @@ public class StaticWall extends Prop{
}
}
}
if(autotile){
autotileRegions = TileBitmask.load(name);
}
}
@Override

View File

@@ -11,15 +11,15 @@ public class BuildVisibility{
shown = new BuildVisibility(() -> true),
debugOnly = new BuildVisibility(() -> false),
editorOnly = new BuildVisibility(() -> Vars.state.rules.editor),
coreZoneOnly = new BuildVisibility(() -> Vars.indexer.isBlockPresent(Blocks.coreZone)),
coreZoneOnly = new BuildVisibility(() -> Vars.indexer.isBlockPresent(Blocks.coreZone) || !Vars.state.isGame()),
worldProcessorOnly = new BuildVisibility(() -> Vars.state.rules.editor || Vars.state.rules.allowEditWorldProcessors),
sandboxOnly = new BuildVisibility(() -> Vars.state == null || Vars.state.rules.infiniteResources),
campaignOnly = new BuildVisibility(() -> Vars.state == null || Vars.state.isCampaign()),
campaignOnly = new BuildVisibility(() -> Vars.state == null || Vars.state.isCampaign() || !Vars.state.isGame()),
legacyLaunchPadOnly = new BuildVisibility(() -> (Vars.state == null || Vars.state.isCampaign() && Vars.state.getPlanet().campaignRules.legacyLaunchPads) && Blocks.advancedLaunchPad != null && Blocks.advancedLaunchPad.unlocked()),
notLegacyLaunchPadOnly = new BuildVisibility(() -> (Vars.state == null || Vars.state.rules.infiniteResources || Vars.state.isCampaign() && !Vars.state.getPlanet().campaignRules.legacyLaunchPads)),
lightingOnly = new BuildVisibility(() -> Vars.state == null || Vars.state.rules.lighting || Vars.state.isCampaign()),
notLegacyLaunchPadOnly = new BuildVisibility(() -> (Vars.state == null || !Vars.state.isGame() || Vars.state.rules.infiniteResources || Vars.state.isCampaign() && !Vars.state.getPlanet().campaignRules.legacyLaunchPads)),
lightingOnly = new BuildVisibility(() -> Vars.state == null || Vars.state.rules.lighting || Vars.state.isCampaign() || !Vars.state.isGame()),
ammoOnly = new BuildVisibility(() -> Vars.state == null || Vars.state.rules.unitAmmo),
fogOnly = new BuildVisibility(() -> Vars.state == null || Vars.state.rules.fog || Vars.state.rules.editor);
fogOnly = new BuildVisibility(() -> Vars.state == null || Vars.state.rules.fog || Vars.state.rules.editor || !Vars.state.isGame());
private final Boolp visible;