package de.luca.lager; import net.minecraft.block.Block; import net.minecraft.block.state.IBlockState; import net.minecraft.client.Minecraft; import net.minecraft.nbt.CompressedStreamTools; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.tileentity.TileEntity; import net.minecraft.util.BlockPos; import net.minecraft.world.World; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileOutputStream; import java.text.SimpleDateFormat; import java.util.Date; import java.util.zip.GZIPOutputStream; /** * Schematic ziehen: zwei Ecken setzen, Taste druecken, fertige Datei. * * Gelesen wird nur, was dein Spiel sowieso schon geladen hat — also genau * das, was du auch mit eigenen Augen sehen koenntest. Was in Truhen liegt, * steht nicht drin: das weiss der Client nicht, solange du die Truhe nicht * aufmachst. * * Herauskommt das alte MCEdit-Format (.schematic). Damit koennen Schematica, * Litematica und WorldEdit etwas anfangen. */ public class Schematik { /** Groesste Kantenlaenge und groesste Gesamtzahl an Bloecken. */ private static final int MAX_KANTE = 256; private static final int MAX_BLOECKE = 2000000; public BlockPos ecke1; public BlockPos ecke2; /** Welche Ecke der naechste Tastendruck setzt. */ private boolean naechsteIstZwei; /** Bloecke, die beim letzten Speichern nicht geladen waren. */ public int nichtGeladen; public boolean vollstaendig() { return ecke1 != null && ecke2 != null; } /** Setzt abwechselnd Ecke 1 und Ecke 2. */ public int eckeSetzen(BlockPos p) { if (p == null) { return 0; } if (!naechsteIstZwei || ecke1 == null) { ecke1 = p; naechsteIstZwei = true; return 1; } ecke2 = p; naechsteIstZwei = false; return 2; } public void loeschen() { ecke1 = null; ecke2 = null; naechsteIstZwei = false; } /** {breite, hoehe, laenge} oder null, solange nicht beide Ecken stehen. */ public int[] masse() { if (!vollstaendig()) { return null; } return new int[] { Math.abs(ecke1.getX() - ecke2.getX()) + 1, Math.abs(ecke1.getY() - ecke2.getY()) + 1, Math.abs(ecke1.getZ() - ecke2.getZ()) + 1, }; } public long anzahl() { int[] m = masse(); if (m == null) { return 0; } return (long) m[0] * (long) m[1] * (long) m[2]; } /** * Schreibt die Auswahl in eine Datei und gibt sie zurueck. * Geht etwas nicht, kommt eine Ausnahme mit dem Grund. */ public File speichern(File ordner, String wunschname) throws Exception { if (!vollstaendig()) { throw new IllegalStateException("Es fehlt noch eine Ecke."); } int[] m = masse(); if (m[0] > MAX_KANTE || m[1] > MAX_KANTE || m[2] > MAX_KANTE) { throw new IllegalStateException("Zu gross: hoechstens " + MAX_KANTE + " Bloecke je Kante."); } if (anzahl() > MAX_BLOECKE) { throw new IllegalStateException("Zu viele Bloecke (" + anzahl() + ")."); } Minecraft mc = Minecraft.getMinecraft(); World welt = mc.theWorld; if (welt == null) { throw new IllegalStateException("Keine Welt da."); } final int minX = Math.min(ecke1.getX(), ecke2.getX()); final int minY = Math.max(Math.min(ecke1.getY(), ecke2.getY()), 0); final int minZ = Math.min(ecke1.getZ(), ecke2.getZ()); final int breite = m[0]; final int hoehe = Math.min(m[1], 256 - minY); final int laenge = m[2]; byte[] bloecke = new byte[breite * hoehe * laenge]; byte[] daten = new byte[bloecke.length]; byte[] hoch = null; // nur noetig bei Block-Nummern ueber 255 NBTTagList kisten = new NBTTagList(); nichtGeladen = 0; for (int y = 0; y < hoehe; y++) { for (int z = 0; z < laenge; z++) { for (int x = 0; x < breite; x++) { BlockPos p = new BlockPos(minX + x, minY + y, minZ + z); int i = (y * laenge + z) * breite + x; if (!welt.isBlockLoaded(p)) { nichtGeladen++; continue; // bleibt Luft } IBlockState zustand = welt.getBlockState(p); Block block = zustand.getBlock(); int nummer = Block.getIdFromBlock(block); int meta; try { meta = block.getMetaFromState(zustand); } catch (Throwable ignoriert) { meta = 0; } bloecke[i] = (byte) (nummer & 0xFF); daten[i] = (byte) (meta & 0x0F); if (nummer > 255) { if (hoch == null) { hoch = new byte[(bloecke.length + 1) / 2]; } int obereHaelfte = (nummer >> 8) & 0x0F; if ((i & 1) == 0) { hoch[i >> 1] |= (byte) obereHaelfte; } else { hoch[i >> 1] |= (byte) (obereHaelfte << 4); } } // Schilder und aehnliche Bloecke bringen eigene Daten mit. // Truhen sind hier immer leer, die kennt der Client nicht. TileEntity te = welt.getTileEntity(p); if (te != null) { try { NBTTagCompound c = new NBTTagCompound(); te.writeToNBT(c); c.setInteger("x", x); c.setInteger("y", y); c.setInteger("z", z); kisten.appendTag(c); } catch (Throwable ignoriert) { // Ein zickiger Block soll nicht die ganze Datei kosten. } } } } } NBTTagCompound wurzel = new NBTTagCompound(); wurzel.setShort("Width", (short) breite); wurzel.setShort("Height", (short) hoehe); wurzel.setShort("Length", (short) laenge); wurzel.setString("Materials", "Alpha"); wurzel.setByteArray("Blocks", bloecke); wurzel.setByteArray("Data", daten); if (hoch != null) { wurzel.setByteArray("AddBlocks", hoch); } wurzel.setTag("Entities", new NBTTagList()); wurzel.setTag("TileEntities", kisten); // Woher das Stueck stammt — das liest WorldEdit aus. wurzel.setInteger("WEOriginX", minX); wurzel.setInteger("WEOriginY", minY); wurzel.setInteger("WEOriginZ", minZ); wurzel.setInteger("WEOffsetX", 0); wurzel.setInteger("WEOffsetY", 0); wurzel.setInteger("WEOffsetZ", 0); ordner.mkdirs(); File datei = new File(ordner, dateiname(wunschname)); // Minecraft schreibt den Wurzeleintrag immer ohne Namen. WorldEdit // besteht aber darauf, dass er "Schematic" heisst — also schreibe ich // den Kopf selbst und haenge den Rest unveraendert dahinter. ByteArrayOutputStream puffer = new ByteArrayOutputStream(); DataOutputStream roh = new DataOutputStream(puffer); CompressedStreamTools.write(wurzel, roh); roh.flush(); byte[] inhalt = puffer.toByteArray(); // [0] = 10, [1..2] = Namenslaenge 0 DataOutputStream aus = new DataOutputStream( new GZIPOutputStream(new FileOutputStream(datei))); try { aus.writeByte(10); aus.writeUTF("Schematic"); aus.write(inhalt, 3, inhalt.length - 3); } finally { aus.close(); } return datei; } private static String dateiname(String wunsch) { String rein = ""; if (wunsch != null) { StringBuilder b = new StringBuilder(); for (int i = 0; i < wunsch.length() && b.length() < 40; i++) { char c = wunsch.charAt(i); if (Character.isLetterOrDigit(c) || c == '-' || c == '_') { b.append(c); } else if (c == ' ') { b.append('_'); } } rein = b.toString(); } String zeit = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss").format(new Date()); return (rein.isEmpty() ? "bau" : rein) + "_" + zeit + ".schematic"; } /** * Schiebt die Datei zusaetzlich auf die Webseite, damit du sie auch von * einem anderen Rechner holen kannst. Laeuft nebenher; klappt es nicht, * liegt die Datei trotzdem auf der Platte. */ public static void hochladen(final File datei, final String grundadresse, final String schluessel) { if (schluessel == null || schluessel.isEmpty()) { return; } new Thread(new Runnable() { public void run() { java.io.OutputStream aus = null; java.io.FileInputStream ein = null; try { String adresse = grundadresse + "?name=" + java.net.URLEncoder.encode(datei.getName(), "UTF-8"); java.net.HttpURLConnection v = (java.net.HttpURLConnection) new java.net.URL(adresse).openConnection(); v.setRequestMethod("POST"); v.setRequestProperty("Content-Type", "application/octet-stream"); v.setRequestProperty("X-Schluessel", schluessel); v.setConnectTimeout(5000); v.setReadTimeout(20000); v.setDoOutput(true); aus = v.getOutputStream(); ein = new java.io.FileInputStream(datei); byte[] p = new byte[8192]; int n; while ((n = ein.read(p)) > 0) { aus.write(p, 0, n); } aus.flush(); int code = v.getResponseCode(); String antwort = code == 200 ? lesen(v.getInputStream()) : ""; v.disconnect(); if (code != 200) { LagerMod.sagen("§8… hochladen ging nicht (" + code + ")"); return; } LagerMod.sagen("§8… liegt auch auf lukaba.duckdns.org/griefergames/schematik/"); // Hat der Server sie auch fuer WorldEdit hingelegt, sagt // er hier, mit welchem Befehl du sie im Spiel holst. String befehl = zwischen(antwort, "\"hinweis\":\"", "\""); if (befehl != null && !befehl.isEmpty()) { LagerMod.sagen("§7auf deinem Server: §f" + befehl + " §7und dann §f//paste"); } } catch (Throwable f) { LagerMod.sagen("§8… hochladen ging nicht, die Datei liegt aber auf der Platte."); } finally { try { if (aus != null) aus.close(); } catch (Exception e) { } try { if (ein != null) ein.close(); } catch (Exception e) { } } } }, "Lager-Schematik").start(); } /** Antwort des Servers einlesen, hoechstens ein paar Zeilen. */ private static String lesen(java.io.InputStream ein) { try { java.io.ByteArrayOutputStream b = new java.io.ByteArrayOutputStream(); byte[] p = new byte[512]; int n; while ((n = ein.read(p)) > 0 && b.size() < 8192) { b.write(p, 0, n); } return new String(b.toByteArray(), "UTF-8"); } catch (Throwable f) { return ""; } } /** Holt das Stueck zwischen zwei Markierungen heraus. */ private static String zwischen(String text, String vorne, String hinten) { if (text == null) { return null; } int a = text.indexOf(vorne); if (a < 0) { return null; } a += vorne.length(); int e = text.indexOf(hinten, a); return e < 0 ? null : text.substring(a, e); } }