1 module csvconv; 2 3 /* 4 * Converts from and to the TMX CSV map format. 5 * Can "force-export" the tiles' palette shifting attributes into a second CSV file (proprietary). 6 * NOTE: Importing might fail if names are used for tile elements, if a tile with the same name doesn't exist in the 7 * tile list. 8 */ 9 10 import pixelperfectengine.map.mapdata; 11 import pixelperfectengine.map.mapformat; 12 import pixelperfectengine.graphics.layers; 13 import pixelperfectengine.system.exc; 14 import pixelperfectengine.system.etc : csvParser, stringArrayParser; 15 16 import document; 17 import std.stdio; 18 import std.conv : to; 19 20 /** 21 * Defines CSV flags. 22 */ 23 enum CSVFlags { 24 Horizontal = 0x80_00_00_00, 25 Vertical = 0x40_00_00_00, 26 Diagonal = 0x20_00_00_00, 27 Test = 0xE0_00_00_00 28 } 29 /** 30 * Converts a layer's content into CSV data. 31 * Throws an exception if an error have been encountered. 32 */ 33 public void toCSV(string target, ITileLayer source) @trusted { 34 File tf = File(target, "wb"); 35 MappingElement[] mapping = source.getMapping; 36 char[] writeBuf; 37 const int mX = source.getMX, mY = source.getMY; 38 for (int y ; y < mY ; y++) { 39 for (int x ; x < mX ; x++) { 40 int element = mapping[(y * mX) + x].tileID == 0xFFFF ? -1 : mapping[(y * mX) + x].tileID; 41 if (mapping[(y * mX) + x].attributes.horizMirror && mapping[(y * mX) + x].attributes.vertMirror) 42 element |= CSVFlags.Diagonal; 43 else if (mapping[(y * mX) + x].attributes.horizMirror) 44 element |= CSVFlags.Horizontal; 45 else if (mapping[(y * mX) + x].attributes.vertMirror) 46 element |= CSVFlags.Vertical; 47 writeBuf ~= to!string(element).dup; 48 tf.rawWrite(writeBuf); 49 writeBuf.length = 0; 50 writeBuf = ",".dup; 51 } 52 writeBuf = "\n".dup; 53 tf.rawWrite(writeBuf); 54 writeBuf.length = 0; 55 } 56 } 57 /// Borrowed from Adam D. Ruppe, because the one in phobos is an abomination! 58 /// Returns the array of csv rows from the given in-memory data (the argument is NOT a filename). 59 package string[][] readCsv(string data) { 60 import std.array; 61 data = data.replace("\r\n", "\n"); 62 data = data.replace("\r", ""); 63 64 //auto idx = data.indexOf("\n"); 65 //data = data[idx + 1 .. $]; // skip headers 66 67 string[] fields; 68 string[][] records; 69 70 string[] current; 71 72 int state = 0; 73 string field; 74 foreach(c; data) { 75 tryit: switch(state) { 76 default: assert(0); 77 case 0: // normal 78 if(c == '"') 79 state = 1; 80 else if(c == ',') { 81 // commit field 82 current ~= field; 83 field = null; 84 } else if(c == '\n') { 85 // commit record 86 current ~= field; 87 88 records ~= current; 89 current = null; 90 field = null; 91 } else 92 field ~= c; 93 break; 94 case 1: // in quote 95 if(c == '"') { 96 state = 2; 97 } else 98 field ~= c; 99 break; 100 case 2: // is it a closing quote or an escaped one? 101 if(c == '"') { 102 field ~= c; 103 state = 1; 104 } else { 105 state = 0; 106 goto tryit; 107 } 108 } 109 } 110 111 if(field !is null) 112 current ~= field; 113 if(current !is null) 114 records ~= current; 115 116 117 return records; 118 } 119 /** 120 * Imports a CSV document to a layer. 121 * NOTE: Named tiles are not supported. 122 */ 123 public void fromCSV(string source, MapDocument dest) @trusted { 124 File sf = File(source, "rb+"); 125 MappingElement[] nativeMap; 126 int width, height; 127 char[] readbuffer; 128 readbuffer.length = cast(size_t)sf.size(); 129 sf.rawRead(readbuffer); 130 string[][] parsedData = readCsv(readbuffer.idup); 131 width = cast(int)parsedData[0].length; 132 height = cast(int)parsedData.length; 133 nativeMap.reserve(width * height); 134 foreach (string[] line; parsedData) { 135 foreach (string entry; line) { 136 int tile = to!int(entry); 137 MappingElement elem; 138 switch (tile & CSVFlags.Test) { 139 case CSVFlags.Horizontal: 140 elem.attributes.horizMirror = true; 141 break; 142 case CSVFlags.Vertical: 143 elem.attributes.vertMirror = true; 144 break; 145 case CSVFlags.Diagonal: 146 elem.attributes.horizMirror = true; 147 elem.attributes.vertMirror = true; 148 break; 149 default: 150 break; 151 } 152 elem.tileID = cast(wchar)(tile & ushort.max); 153 nativeMap ~= elem; 154 } 155 } 156 dest.assignImportedTilemap(nativeMap, width, height); 157 } 158 /** 159 * Thrown on import errors. 160 */ 161 public class CSVImportException : PPEException { 162 /// 163 @nogc @safe pure nothrow this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable nextInChain = null) 164 { 165 super(msg, file, line, nextInChain); 166 } 167 /// 168 @nogc @safe pure nothrow this(string msg, Throwable nextInChain, string file = __FILE__, size_t line = __LINE__) 169 { 170 super(msg, file, line, nextInChain); 171 } 172 }