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 }