package mindustry.game; import arc.*; import arc.assets.*; import arc.files.*; import arc.graphics.*; import arc.graphics.g2d.*; import arc.graphics.gl.*; import arc.math.*; import arc.math.geom.*; import arc.struct.*; import arc.util.*; import arc.util.io.*; import arc.util.io.Streams.*; import arc.util.pooling.*; import arc.util.serialization.*; import mindustry.*; import mindustry.content.*; import mindustry.core.*; import mindustry.ctype.*; import mindustry.entities.units.*; import mindustry.game.EventType.*; import mindustry.game.Schematic.*; import mindustry.gen.*; import mindustry.input.*; import mindustry.input.Placement.*; import mindustry.io.*; import mindustry.world.*; import mindustry.world.blocks.*; import mindustry.world.blocks.distribution.*; import mindustry.world.blocks.legacy.*; import mindustry.world.blocks.power.*; import mindustry.world.blocks.production.*; import mindustry.world.blocks.sandbox.*; import mindustry.world.blocks.storage.*; import mindustry.world.meta.*; import java.io.*; import java.util.zip.*; import static mindustry.Vars.*; /** Handles schematics.*/ public class Schematics implements Loadable{ private static final Schematic tmpSchem = new Schematic(new Seq<>(), new StringMap(), 0, 0); private static final Schematic tmpSchem2 = new Schematic(new Seq<>(), new StringMap(), 0, 0); private static final byte[] header = {'m', 's', 'c', 'h'}; private static final byte version = 1; private static final int padding = 2; private static final int maxPreviewsMobile = 32; private static final int resolution = 32; private OptimizedByteArrayOutputStream out = new OptimizedByteArrayOutputStream(1024); private Seq all = new Seq<>(); private OrderedMap previews = new OrderedMap<>(); private ObjectSet errored = new ObjectSet<>(); private ObjectMap> loadouts = new ObjectMap<>(); private FrameBuffer shadowBuffer; private Texture errorTexture; private long lastClearTime; public Schematics(){ Events.on(DisposeEvent.class, e -> { previews.each((schem, m) -> m.dispose()); previews.clear(); shadowBuffer.dispose(); if(errorTexture != null){ errorTexture.dispose(); errorTexture = null; } }); Events.on(ClientLoadEvent.class, event -> { errorTexture = new Texture("sprites/error.png"); }); } @Override public void loadSync(){ load(); } /** Load all schematics in the folder immediately.*/ public void load(){ all.clear(); loadLoadouts(); for(Fi file : schematicDirectory.list()){ loadFile(file); } platform.getWorkshopContent(Schematic.class).each(this::loadFile); //mod-specific schematics, cannot be removed mods.listFiles("schematics", (mod, file) -> { Schematic s = loadFile(file); if(s != null){ s.mod = mod; } }); all.sort(); if(shadowBuffer == null){ Core.app.post(() -> shadowBuffer = new FrameBuffer(maxSchematicSize + padding + 8, maxSchematicSize + padding + 8)); } } private void loadLoadouts(){ Seq.with(Loadouts.basicShard, Loadouts.basicFoundation, Loadouts.basicNucleus).each(s -> checkLoadout(s, false)); } public void overwrite(Schematic target, Schematic newSchematic){ if(previews.containsKey(target)){ previews.get(target).dispose(); previews.remove(target); } target.tiles.clear(); target.tiles.addAll(newSchematic.tiles); target.width = newSchematic.width; target.height = newSchematic.height; newSchematic.tags.putAll(target.tags); newSchematic.file = target.file; loadouts.each((block, list) -> list.remove(target)); checkLoadout(target, true); try{ write(newSchematic, target.file); }catch(Exception e){ Log.err("Failed to overwrite schematic '@' (@)", newSchematic.name(), target.file); Log.err(e); ui.showException(e); } } private @Nullable Schematic loadFile(Fi file){ if(!file.extension().equals(schematicExtension)) return null; try{ Schematic s = read(file); all.add(s); checkLoadout(s, true); //external file from workshop if(!s.file.parent().equals(schematicDirectory)){ s.tags.put("steamid", s.file.parent().name()); } return s; }catch(Throwable e){ Log.err("Failed to read schematic from file '@'", file); Log.err(e); } return null; } public Seq all(){ return all; } public void saveChanges(Schematic s){ if(s.file != null){ try{ write(s, s.file); }catch(Exception e){ ui.showException(e); } } all.sort(); } public void savePreview(Schematic schematic, Fi file){ FrameBuffer buffer = getBuffer(schematic); Draw.flush(); buffer.begin(); Pixmap pixmap = ScreenUtils.getFrameBufferPixmap(0, 0, buffer.getWidth(), buffer.getHeight()); file.writePNG(pixmap); buffer.end(); } public Texture getPreview(Schematic schematic){ if(errored.contains(schematic)) return errorTexture; try{ return getBuffer(schematic).getTexture(); }catch(Throwable t){ Log.err("Failed to get preview for schematic '@' (@)", schematic.name(), schematic.file); Log.err(t); errored.add(schematic); return errorTexture; } } public boolean hasPreview(Schematic schematic){ return previews.containsKey(schematic); } public FrameBuffer getBuffer(Schematic schematic){ //dispose unneeded previews to prevent memory outage errors. //only runs every 2 seconds if(mobile && Time.timeSinceMillis(lastClearTime) > 1000 * 2 && previews.size > maxPreviewsMobile){ Seq keys = previews.orderedKeys().copy(); for(int i = 0; i < previews.size - maxPreviewsMobile; i++){ //dispose and remove unneeded previews previews.get(keys.get(i)).dispose(); previews.remove(keys.get(i)); } //update last clear time lastClearTime = Time.millis(); } if(!previews.containsKey(schematic)){ Draw.blend(); Draw.reset(); Tmp.m1.set(Draw.proj()); Tmp.m2.set(Draw.trans()); FrameBuffer buffer = new FrameBuffer((schematic.width + padding) * resolution, (schematic.height + padding) * resolution); shadowBuffer.begin(Color.clear); Draw.trans().idt(); 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.end(); buffer.begin(Color.clear); Draw.proj().setOrtho(0, buffer.getHeight(), buffer.getWidth(), -buffer.getHeight()); 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(); Seq requests = schematic.tiles.map(t -> new BuildPlan(t.x, t.y, t.rotation, t.block, t.config)); Draw.flush(); //scale each request to fit schematic Draw.trans().scale(resolution / tilesize, resolution / tilesize).translate(tilesize*1.5f, tilesize*1.5f); //draw requests requests.each(req -> { req.animScale = 1f; req.worldContext = false; req.block.drawRequestRegion(req, requests); }); requests.each(req -> req.block.drawRequestConfigTop(req, requests)); Draw.flush(); Draw.trans().idt(); buffer.end(); Draw.proj(Tmp.m1); Draw.trans(Tmp.m2); previews.put(schematic, buffer); } return previews.get(schematic); } /** Creates an array of build requests from a schematic's data, centered on the provided x+y coordinates. */ public Seq toRequests(Schematic schem, int x, int y){ return schem.tiles.map(t -> new BuildPlan(t.x + x - schem.width/2, t.y + y - schem.height/2, t.rotation, t.block, t.config).original(t.x, t.y, schem.width, schem.height)) .removeAll(s -> (!s.block.isVisible() && !(s.block instanceof CoreBlock)) || !s.block.unlockedNow()).sort(Structs.comparingInt(s -> -s.block.schematicPriority)); } /** @return all the valid loadouts for a specific core type. */ public Seq getLoadouts(CoreBlock block){ return loadouts.get(block, Seq::new); } public ObjectMap> getLoadouts(){ return loadouts; } /** Checks a schematic for deployment validity and adds it to the cache. */ private void checkLoadout(Schematic s, boolean validate){ Stile core = s.tiles.find(t -> t.block instanceof CoreBlock); int cores = s.tiles.count(t -> t.block instanceof CoreBlock); //make sure a core exists, and that the schematic is small enough. if(core == null || (validate && (s.width > core.block.size + maxLoadoutSchematicPad *2 || s.height > core.block.size + maxLoadoutSchematicPad *2 || s.tiles.contains(t -> t.block.buildVisibility == BuildVisibility.sandboxOnly || !t.block.unlocked()) || cores > 1))) return; //place in the cache loadouts.get((CoreBlock)core.block, Seq::new).add(s); } /** Adds a schematic to the list, also copying it into the files.*/ public void add(Schematic schematic){ all.add(schematic); try{ Fi file = schematicDirectory.child(Time.millis() + "." + schematicExtension); write(schematic, file); schematic.file = file; }catch(Exception e){ ui.showException(e); Log.err(e); } checkLoadout(schematic, true); all.sort(); } public void remove(Schematic s){ all.remove(s); loadouts.each((block, seq) -> seq.remove(s)); if(s.file != null){ s.file.delete(); } if(previews.containsKey(s)){ previews.get(s).dispose(); previews.remove(s); } all.sort(); } /** Creates a schematic from a world selection. */ public Schematic create(int x, int y, int x2, int y2){ NormalizeResult result = Placement.normalizeArea(x, y, x2, y2, 0, false, maxSchematicSize); x = result.x; y = result.y; x2 = result.x2; y2 = result.y2; int ox = x, oy = y, ox2 = x2, oy2 = y2; Seq tiles = new Seq<>(); int minx = x2, miny = y2, maxx = x, maxy = y; boolean found = false; for(int cx = x; cx <= x2; cx++){ for(int cy = y; cy <= y2; cy++){ Building linked = world.build(cx, cy); if(linked != null && (linked.block.isVisible() || linked.block() instanceof CoreBlock) && !(linked.block instanceof ConstructBlock)){ int top = linked.block.size/2; int bot = linked.block.size % 2 == 1 ? -linked.block.size/2 : -(linked.block.size - 1)/2; minx = Math.min(linked.tileX() + bot, minx); miny = Math.min(linked.tileY() + bot, miny); maxx = Math.max(linked.tileX() + top, maxx); maxy = Math.max(linked.tileY() + top, maxy); found = true; } } } if(found){ x = minx; y = miny; x2 = maxx; y2 = maxy; }else{ return new Schematic(new Seq<>(), new StringMap(), 1, 1); } int width = x2 - x + 1, height = y2 - y + 1; int offsetX = -x, offsetY = -y; IntSet counted = new IntSet(); for(int cx = ox; cx <= ox2; cx++){ for(int cy = oy; cy <= oy2; cy++){ Building tile = world.build(cx, cy); if(tile != null && !counted.contains(tile.pos()) && !(tile.block instanceof ConstructBlock) && (tile.block.isVisible() || tile.block instanceof CoreBlock)){ Object config = tile.config(); tiles.add(new Stile(tile.block, tile.tileX() + offsetX, tile.tileY() + offsetY, config, (byte)tile.rotation)); counted.add(tile.pos()); } } } return new Schematic(tiles, new StringMap(), width, height); } /** Converts a schematic to base64. Note that the result of this will always start with 'bXNjaAB'.*/ 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); } } /** Places the last launch loadout at the coordinates and fills it with the launch resources. */ public static void placeLaunchLoadout(int x, int y){ placeLoadout(universe.getLastLoadout(), x, y); if(world.tile(x, y).build == null) throw new RuntimeException("No core at loadout coordinates!"); world.tile(x, y).build.items.add(universe.getLaunchResources()); } public static void placeLoadout(Schematic schem, int x, int y){ placeLoadout(schem, x, y, state.rules.defaultTeam, Blocks.oreCopper); } public static void placeLoadout(Schematic schem, int x, int y, Team team, Block resource){ placeLoadout(schem, x, y, team, resource, true); } public static void placeLoadout(Schematic schem, int x, int y, Team team, Block resource, boolean check){ Stile coreTile = schem.tiles.find(s -> s.block instanceof CoreBlock); Seq seq = new Seq<>(); if(coreTile == null) throw new IllegalArgumentException("Loadout schematic has no core tile!"); int ox = x - coreTile.x, oy = y - coreTile.y; schem.tiles.each(st -> { Tile tile = world.tile(st.x + ox, st.y + oy); if(tile == null) return; //check for blocks that are in the way. if(check && !(st.block instanceof CoreBlock)){ seq.clear(); tile.getLinkedTilesAs(st.block, seq); if(seq.contains(t -> !t.block().alwaysReplace && !t.synthetic())){ return; } } tile.setBlock(st.block, team, st.rotation); Object config = st.config; if(tile.build != null){ tile.build.configureAny(config); } if(st.block instanceof Drill){ tile.getLinkedTiles(t -> t.setOverlay(resource)); } }); } public static void place(Schematic schem, int x, int y, Team team){ int ox = x - schem.width/2, oy = y - schem.height/2; schem.tiles.each(st -> { Tile tile = world.tile(st.x + ox, st.y + oy); if(tile == null) return; tile.setBlock(st.block, team, st.rotation); Object config = st.config; if(tile.build != null){ tile.build.configureAny(config); } }); } //region IO methods /** Loads a schematic from base64. May throw an exception. */ public static Schematic readBase64(String schematic){ try{ return read(new ByteArrayInputStream(Base64Coder.decode(schematic.trim()))); }catch(IOException e){ throw new RuntimeException(e); } } public static Schematic read(Fi file) throws IOException{ Schematic s = read(new DataInputStream(file.read(1024))); if(!s.tags.containsKey("name")){ s.tags.put("name", file.nameWithoutExtension()); } s.file = file; return s; } 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 = input.read(); try(DataInputStream stream = new DataInputStream(new InflaterInputStream(input))){ short width = stream.readShort(), height = stream.readShort(); StringMap map = new StringMap(); byte tags = stream.readByte(); for(int i = 0; i < tags; i++){ map.put(stream.readUTF(), stream.readUTF()); } IntMap blocks = new IntMap<>(); byte length = stream.readByte(); for(int i = 0; i < length; i++){ String name = stream.readUTF(); Block block = Vars.content.getByName(ContentType.block, SaveFileReader.fallback.get(name, name)); blocks.put(i, block == null || block instanceof LegacyBlock ? Blocks.air : block); } int total = stream.readInt(); Seq tiles = new Seq<>(total); for(int i = 0; i < total; i++){ Block block = blocks.get(stream.readByte()); int position = stream.readInt(); Object config = ver == 0 ? mapConfig(block, stream.readInt(), position) : TypeIO.readObject(Reads.get(stream)); byte rotation = stream.readByte(); if(block != Blocks.air){ tiles.add(new Stile(block, Point2.x(position), Point2.y(position), config, rotation)); } } return new Schematic(tiles, map, width, height); } } public static void write(Schematic schematic, Fi 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); stream.writeByte(schematic.tags.size); for(ObjectMap.Entry e : schematic.tags.entries()){ stream.writeUTF(e.key); stream.writeUTF(e.value); } 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(Point2.pack(tile.x, tile.y)); TypeIO.writeObject(Writes.get(stream), tile.config); stream.writeByte(tile.rotation); } } } /** Maps legacy int configs to new config objects. */ private static Object mapConfig(Block block, int value, int position){ if(block instanceof Sorter || block instanceof Unloader || block instanceof ItemSource) return content.item(value); if(block instanceof LiquidSource) return content.liquid(value); if(block instanceof MassDriver || block instanceof ItemBridge) return Point2.unpack(value).sub(Point2.x(position), Point2.y(position)); if(block instanceof LightBlock) return value; return null; } //endregion //region misc utility /** @return a temporary schematic representing the input rotated 90 degrees counterclockwise N times. */ public static Schematic rotate(Schematic input, int times){ if(times == 0) return input; boolean sign = times > 0; for(int i = 0; i < Math.abs(times); i++){ input = rotated(input, sign); } return input; } private static Schematic rotated(Schematic input, boolean counter){ int direction = Mathf.sign(counter); Schematic schem = input == tmpSchem ? tmpSchem2 : tmpSchem2; schem.width = input.width; schem.height = input.height; Pools.freeAll(schem.tiles); schem.tiles.clear(); for(Stile tile : input.tiles){ schem.tiles.add(Pools.obtain(Stile.class, Stile::new).set(tile)); } int ox = schem.width/2, oy = schem.height/2; schem.tiles.each(req -> { req.config = BuildPlan.pointConfig(req.block, req.config, p -> { int cx = p.x, cy = p.y; int lx = cx; if(direction >= 0){ cx = -cy; cy = lx; }else{ cx = cy; cy = -lx; } p.set(cx, cy); }); //rotate actual request, centered on its multiblock position float wx = (req.x - ox) * tilesize + req.block.offset, wy = (req.y - oy) * tilesize + req.block.offset; float x = wx; if(direction >= 0){ wx = -wy; wy = x; }else{ wx = wy; wy = -x; } req.x = (short)(World.toTile(wx - req.block.offset) + ox); req.y = (short)(World.toTile(wy - req.block.offset) + oy); req.rotation = (byte)Mathf.mod(req.rotation + direction, 4); }); //assign flipped values, since it's rotated schem.width = input.height; schem.height = input.width; return schem; } //endregion }