From eb21d5ab67e4a8cb9a02a99fa62397930ffbdde4 Mon Sep 17 00:00:00 2001 From: Anuken Date: Mon, 14 Oct 2019 21:34:06 -0400 Subject: [PATCH] Experimental schematics --- core/src/io/anuke/mindustry/Vars.java | 9 + .../mindustry/entities/type/TileEntity.java | 5 + .../io/anuke/mindustry/game/Schematic.java | 31 +++ .../io/anuke/mindustry/game/Schematics.java | 261 ++++++++++++++++++ .../src/io/anuke/mindustry/input/Binding.java | 1 + .../anuke/mindustry/input/DesktopInput.java | 40 ++- .../world/blocks/distribution/ItemBridge.java | 5 + .../world/blocks/distribution/MassDriver.java | 5 + 8 files changed, 356 insertions(+), 1 deletion(-) create mode 100644 core/src/io/anuke/mindustry/game/Schematic.java create mode 100644 core/src/io/anuke/mindustry/game/Schematics.java diff --git a/core/src/io/anuke/mindustry/Vars.java b/core/src/io/anuke/mindustry/Vars.java index 2d8f4f83c0..a045ef2af7 100644 --- a/core/src/io/anuke/mindustry/Vars.java +++ b/core/src/io/anuke/mindustry/Vars.java @@ -128,10 +128,14 @@ public class Vars implements Loadable{ public static FileHandle saveDirectory; /** data subdirectory used for mods */ public static FileHandle modDirectory; + /** data subdirectory used for schematics */ + public static FileHandle schematicDirectory; /** map file extension */ public static final String mapExtension = "msav"; /** save file extension */ public static final String saveExtension = "msav"; + /** schematic file extension */ + public static final String schematicExtension = "msch"; /** list of all locales that can be switched to */ public static Locale[] locales; @@ -146,6 +150,7 @@ public class Vars implements Loadable{ public static LoopControl loops; public static Platform platform = new Platform(){}; public static Mods mods; + public static Schematics schematics = new Schematics(); public static World world; public static Maps maps; @@ -251,11 +256,15 @@ public class Vars implements Loadable{ saveDirectory = dataDirectory.child("saves/"); tmpDirectory = dataDirectory.child("tmp/"); modDirectory = dataDirectory.child("mods/"); + schematicDirectory = dataDirectory.child("schematics/"); modDirectory.mkdirs(); mods.load(); maps.load(); + if(!headless){ + schematics.load(); + } } public static void loadSettings(){ diff --git a/core/src/io/anuke/mindustry/entities/type/TileEntity.java b/core/src/io/anuke/mindustry/entities/type/TileEntity.java index 5fa2ea9aae..177b2b6c34 100644 --- a/core/src/io/anuke/mindustry/entities/type/TileEntity.java +++ b/core/src/io/anuke/mindustry/entities/type/TileEntity.java @@ -236,6 +236,11 @@ public class TileEntity extends BaseEntity implements TargetTrait, HealthTrait{ return 0; } + /** @return whether the config is a position that should be translated.*/ + public boolean posConfig(){ + return false; + } + @Override public void removed(){ if(sound != null){ diff --git a/core/src/io/anuke/mindustry/game/Schematic.java b/core/src/io/anuke/mindustry/game/Schematic.java new file mode 100644 index 0000000000..4e82620bd2 --- /dev/null +++ b/core/src/io/anuke/mindustry/game/Schematic.java @@ -0,0 +1,31 @@ +package io.anuke.mindustry.game; + +import io.anuke.arc.collection.*; +import io.anuke.arc.util.ArcAnnotate.*; +import io.anuke.mindustry.world.*; + +public class Schematic{ + public final Array tiles; + public int width, height; + + public Schematic(Array tiles, int width, int height){ + this.tiles = tiles; + this.width = width; + this.height = height; + } + + public static class Stile{ + public @NonNull Block block; + public short x, y; + public int config; + public byte rotation; + + public Stile(Block block, int x, int y, int config, byte rotation){ + this.block = block; + this.x = (short)x; + this.y = (short)y; + this.config = config; + this.rotation = rotation; + } + } +} diff --git a/core/src/io/anuke/mindustry/game/Schematics.java b/core/src/io/anuke/mindustry/game/Schematics.java new file mode 100644 index 0000000000..ca86000ba1 --- /dev/null +++ b/core/src/io/anuke/mindustry/game/Schematics.java @@ -0,0 +1,261 @@ +package io.anuke.mindustry.game; + +import io.anuke.arc.*; +import io.anuke.arc.collection.*; +import io.anuke.arc.files.*; +import io.anuke.arc.graphics.*; +import io.anuke.arc.graphics.g2d.*; +import io.anuke.arc.graphics.glutils.*; +import io.anuke.arc.util.*; +import io.anuke.arc.util.io.Streams.*; +import io.anuke.arc.util.serialization.*; +import io.anuke.mindustry.*; +import io.anuke.mindustry.content.*; +import io.anuke.mindustry.entities.traits.BuilderTrait.*; +import io.anuke.mindustry.game.EventType.*; +import io.anuke.mindustry.game.Schematic.*; +import io.anuke.mindustry.type.*; +import io.anuke.mindustry.world.*; + +import java.io.*; +import java.util.zip.*; + +import static io.anuke.mindustry.Vars.*; + +/** Handles schematics.*/ +public class Schematics{ + private static final byte[] header = {'m', 's', 'c', 'h'}; + private static final byte version = 0; + + private static final int resolution = 64; + private static final int padding = 2; + private static final int maxSize = 64; + + private OptimizedByteArrayOutputStream out = new OptimizedByteArrayOutputStream(1024); + private Array all = new Array<>(); + private OrderedMap previews = new OrderedMap<>(); + private FrameBuffer shadowBuffer; + + public Schematics(){ + Events.on(DisposeEvent.class, e -> { + previews.each((schem, buffer) -> buffer.dispose()); + previews.clear(); + shadowBuffer.dispose(); + }); + } + + /** Load all schematics in the folder immediately.*/ + public void load(){ + all.clear(); + for(FileHandle file : schematicDirectory.list()){ + if(!file.extension().equals(schematicExtension)) continue; + + try{ + all.add(read(file)); + }catch(IOException e){ + e.printStackTrace(); + } + } + + Core.app.post(() -> { + shadowBuffer = new FrameBuffer(maxSize + padding, maxSize + padding); + }); + } + + public Texture getPreview(Schematic schematic){ + if(!previews.containsKey(schematic)){ + Draw.blend(); + Draw.color(); + Time.mark(); + FrameBuffer buffer = new FrameBuffer((schematic.width + padding) * resolution, (schematic.height + padding) * resolution); + Tmp.m1.set(Draw.proj()); + + shadowBuffer.beginDraw(Color.clear); + + Draw.proj().setOrtho(0, 0, shadowBuffer.getWidth(), shadowBuffer.getHeight()); + + Draw.color(); + schematic.tiles.each(t -> { + int size = t.block.size; + int offsetx = -(size - 1) / 2; + int offsety = -(size - 1) / 2; + for(int dx = 0; dx < size; dx++){ + for(int dy = 0; dy < size; dy++){ + int wx = t.x + dx + offsetx; + int wy = t.y + dy + offsety; + Fill.square(padding/2f + wx + 0.5f, padding/2f + wy + 0.5f, 0.5f); + } + } + }); + + shadowBuffer.endDraw(); + + buffer.beginDraw(Color.orange); + + Draw.proj().setOrtho(0, 0, buffer.getWidth(), buffer.getHeight()); + for(int x = 0; x < schematic.width + padding; x++){ + for(int y = 0; y < schematic.height + padding; y++){ + Draw.rect("dark-panel-4", x * resolution + resolution/2f, y * resolution + resolution/2f, resolution, resolution); + } + } + + Tmp.tr1.set(shadowBuffer.getTexture(), 0, 0, schematic.width + padding, schematic.height + padding); + Draw.color(0f, 0f, 0f, 1f); + Draw.rect(Tmp.tr1, buffer.getWidth()/2f, buffer.getHeight()/2f, buffer.getWidth(), buffer.getHeight()); + Draw.color(); + + schematic.tiles.each(t -> { + float offset = (t.block.size + 1) % 2 / 2f; + Draw.rect(t.block.icon(Cicon.full), + (t.x + 0.5f + padding/2f + offset) * resolution, + buffer.getHeight() - 1 - (t.y + 0.5f + padding/2f + offset) * resolution, + resolution * t.block.size, -resolution * t.block.size, t.block.rotate ? t.rotation * 90 : 0); + }); + + buffer.endDraw(); + + Draw.proj(Tmp.m3); + + previews.put(schematic, buffer); + Log.info("Time taken: {0}", Time.elapsed()); + } + + return previews.get(schematic).getTexture(); + } + + /** Creates an array of build requests from a schematic's data, centered on the provided x+y coordinates. */ + public Array toRequests(Schematic schem, int x, int y){ + return schem.tiles.map(t -> new BuildRequest(t.x + x - schem.width/2, t.y + y - schem.height/2, t.rotation, t.block).configure(t.config)); + } + + /** Creates a schematic from a world selection. */ + public Schematic create(int x, int y, int x2, int y2){ + if(x > x2){ + int temp = x; + x = x2; + x2 = temp; + } + + if(y > y2){ + int temp = y; + y = y2; + y2 = temp; + } + + Array tiles = new Array<>(); + + int width = x2 - x + 1, height = y2 - y + 1; + int offsetX = -x, offsetY = -y; + for(int cx = x; cx <= x2; cx++){ + for(int cy = y; cy <= y2; cy++){ + Tile tile = world.tile(cx, cy); + + if(tile != null && tile.entity != null){ + int config = tile.entity.config(); + if(tile.entity.posConfig()){ + config = Pos.get(Pos.x(config) + offsetX, Pos.y(config) + offsetY); + } + + tiles.add(new Stile(tile.block(), cx + offsetX, cy + offsetY, config, tile.rotation())); + } + } + } + + return new Schematic(tiles, width, height); + } + + /** Converts a schematic to base64. */ + public String writeBase64(Schematic schematic){ + try{ + out.reset(); + write(schematic, out); + return new String(Base64Coder.encode(out.getBuffer(), out.size())); + }catch(IOException e){ + throw new RuntimeException(e); + } + } + + /** Loads a schematic from base64. May throw an exception. */ + public Schematic readBase64(String schematic) throws IOException{ + return read(new ByteArrayInputStream(Base64Coder.decode(schematic))); + } + + //region IO methods + + public static Schematic read(FileHandle file) throws IOException{ + return read(new DataInputStream(file.read(1024))); + } + + public static Schematic read(InputStream input) throws IOException{ + for(byte b : header){ + if(input.read() != b){ + throw new IOException("Not a schematic file (missing header)."); + } + } + + int ver; + //version, currently discarded + if((ver = input.read()) != version){ + throw new IOException("Unknown version: " + ver); + } + + try(DataInputStream stream = new DataInputStream(new InflaterInputStream(input))){ + + short width = stream.readShort(), height = stream.readShort(); + IntMap blocks = new IntMap<>(); + byte length = stream.readByte(); + for(int i = 0; i < length; i++){ + Block block = Vars.content.getByName(ContentType.block, stream.readUTF()); + blocks.put(i, block == null ? Blocks.air : block); + } + + int total = stream.readInt(); + Array tiles = new Array<>(total); + for(int i = 0; i < total; i++){ + Block block = blocks.get(stream.readByte()); + int position = stream.readInt(); + int config = stream.readInt(); + byte rotation = stream.readByte(); + if(block != Blocks.air){ + tiles.add(new Stile(block, Pos.x(position), Pos.y(rotation), config, rotation)); + } + } + + return new Schematic(tiles, width, height); + } + } + + public static void write(Schematic schematic, FileHandle file) throws IOException{ + write(schematic, file.write(false, 1024)); + } + + public static void write(Schematic schematic, OutputStream output) throws IOException{ + output.write(header); + output.write(version); + + try(DataOutputStream stream = new DataOutputStream(new DeflaterOutputStream(output))){ + + stream.writeShort(schematic.width); + stream.writeShort(schematic.height); + OrderedSet blocks = new OrderedSet<>(); + schematic.tiles.each(t -> blocks.add(t.block)); + + //create dictionary + stream.writeByte(blocks.size); + for(int i = 0; i < blocks.size; i++){ + stream.writeUTF(blocks.orderedItems().get(i).name); + } + + stream.writeInt(schematic.tiles.size); + //write each tile + for(Stile tile : schematic.tiles){ + stream.writeByte(blocks.orderedItems().indexOf(tile.block)); + stream.writeInt(Pos.get(tile.x, tile.y)); + stream.writeInt(tile.config); + stream.writeByte(tile.rotation); + } + } + } + + //endregion +} diff --git a/core/src/io/anuke/mindustry/input/Binding.java b/core/src/io/anuke/mindustry/input/Binding.java index 90309b74ad..c3eab6a3d5 100644 --- a/core/src/io/anuke/mindustry/input/Binding.java +++ b/core/src/io/anuke/mindustry/input/Binding.java @@ -18,6 +18,7 @@ public enum Binding implements KeyBind{ rotateplaced(KeyCode.R), diagonal_placement(KeyCode.CONTROL_LEFT), pick(KeyCode.MOUSE_MIDDLE), + schematic(KeyCode.F), dash(KeyCode.SHIFT_LEFT), gridMode(KeyCode.BACKTICK), gridModeShift(KeyCode.ALT_LEFT), diff --git a/core/src/io/anuke/mindustry/input/DesktopInput.java b/core/src/io/anuke/mindustry/input/DesktopInput.java index 127d674ea4..b19449a4cb 100644 --- a/core/src/io/anuke/mindustry/input/DesktopInput.java +++ b/core/src/io/anuke/mindustry/input/DesktopInput.java @@ -3,19 +3,24 @@ package io.anuke.mindustry.input; import io.anuke.arc.*; import io.anuke.arc.Graphics.*; import io.anuke.arc.Graphics.Cursor.*; +import io.anuke.arc.graphics.*; import io.anuke.arc.graphics.g2d.*; import io.anuke.arc.math.*; import io.anuke.arc.scene.*; import io.anuke.arc.scene.ui.*; +import io.anuke.arc.util.*; import io.anuke.arc.util.ArcAnnotate.*; import io.anuke.mindustry.core.GameState.*; import io.anuke.mindustry.entities.traits.BuilderTrait.*; +import io.anuke.mindustry.game.*; import io.anuke.mindustry.game.EventType.*; import io.anuke.mindustry.gen.*; import io.anuke.mindustry.graphics.*; import io.anuke.mindustry.ui.*; import io.anuke.mindustry.world.*; +import java.io.*; + import static io.anuke.arc.Core.scene; import static io.anuke.mindustry.Vars.*; import static io.anuke.mindustry.input.PlaceMode.*; @@ -24,7 +29,7 @@ public class DesktopInput extends InputHandler{ /** Current cursor type. */ private Cursor cursorType = SystemCursor.arrow; /** Position where the player started dragging a line. */ - private int selectX, selectY; + private int selectX, selectY, schemX, schemY; /** Last known line positions.*/ private int lastLineX, lastLineY; /** Whether selecting mode is active. */ @@ -36,6 +41,8 @@ public class DesktopInput extends InputHandler{ /** Whether player is currently deleting removal requests. */ private boolean deleting = false; + private Schematic __REMOVE__; + @Override public void buildUI(Group group){ group.fill(t -> { @@ -94,7 +101,21 @@ public class DesktopInput extends InputHandler{ drawSelected(sreq.x, sreq.y, sreq.block, getRequest(sreq.x, sreq.y, sreq.block.size, sreq) != null ? Pal.remove : Pal.accent); } + if(Core.input.keyDown(Binding.schematic)){ + Lines.stroke(2f); + + Draw.color(Pal.accent); + Lines.rect(schemX * tilesize, schemY * tilesize, (cursorX - schemX) * tilesize, (cursorY - schemY) * tilesize); + } + Draw.reset(); + + if(__REMOVE__ != null){ + Texture tex = schematics.getPreview(__REMOVE__); + Draw.blend(Blending.disabled); + Draw.rect(Draw.wrap(tex), Core.camera.position.x, Core.camera.position.y, tex.getWidth() / 8f, tex.getHeight() / 8f); + Draw.blend(); + } } @Override @@ -194,6 +215,7 @@ public class DesktopInput extends InputHandler{ Tile selected = tileAt(Core.input.mouseX(), Core.input.mouseY()); int cursorX = tileX(Core.input.mouseX()); int cursorY = tileY(Core.input.mouseY()); + int rawCursorX = world.toTile(Core.input.mouseWorld().x), rawCursorY = world.toTile(Core.input.mouseWorld().y); if(Core.input.keyTap(Binding.deselect)){ player.setMineTile(null); @@ -203,6 +225,22 @@ public class DesktopInput extends InputHandler{ player.clearBuilding(); } + if(Core.input.keyTap(Binding.schematic)){ + schemX = rawCursorX; + schemY = rawCursorY; + } + + if(Core.input.keyRelease(Binding.schematic)){ + Schematic schem = schematics.create(schemX, schemY, rawCursorX, rawCursorY); + __REMOVE__= schem; + Log.info(schematics.writeBase64(schem)); + try{ + Schematics.write(schem, Core.files.external("schematic.msch")); + }catch(IOException e){ + throw new RuntimeException(e); + } + } + if(sreq != null){ float offset = ((sreq.block.size + 2) % 2) * tilesize / 2f; float x = Core.input.mouseWorld().x + offset; diff --git a/core/src/io/anuke/mindustry/world/blocks/distribution/ItemBridge.java b/core/src/io/anuke/mindustry/world/blocks/distribution/ItemBridge.java index 46acbc1420..d867f5fd26 100644 --- a/core/src/io/anuke/mindustry/world/blocks/distribution/ItemBridge.java +++ b/core/src/io/anuke/mindustry/world/blocks/distribution/ItemBridge.java @@ -361,6 +361,11 @@ public class ItemBridge extends Block{ return link; } + @Override + public boolean posConfig(){ + return true; + } + @Override public void write(DataOutput stream) throws IOException{ super.write(stream); diff --git a/core/src/io/anuke/mindustry/world/blocks/distribution/MassDriver.java b/core/src/io/anuke/mindustry/world/blocks/distribution/MassDriver.java index c5ada69c89..a9d8c008c6 100644 --- a/core/src/io/anuke/mindustry/world/blocks/distribution/MassDriver.java +++ b/core/src/io/anuke/mindustry/world/blocks/distribution/MassDriver.java @@ -319,6 +319,11 @@ public class MassDriver extends Block{ return link; } + @Override + public boolean posConfig(){ + return true; + } + @Override public void write(DataOutput stream) throws IOException{ super.write(stream);