1#include <base/logger.h>
2#include <base/system.h>
3#include <engine/gfx/image_loader.h>
4#include <engine/graphics.h>
5#include <engine/shared/datafile.h>
6#include <engine/storage.h>
7#include <game/mapitems.h>
8
9bool CreatePixelArt(const char[3][IO_MAX_PATH_LENGTH], const int[2], const int[2], int[2], const bool[2]);
10void InsertCurrentQuads(CDataFileReader &, CMapItemLayerQuads *, CQuad *);
11int InsertPixelArtQuads(CQuad *, int &, const CImageInfo &, const int[2], const int[2], const bool[2]);
12
13bool LoadPng(CImageInfo *, const char *);
14bool OpenMaps(const char[2][IO_MAX_PATH_LENGTH], CDataFileReader &, CDataFileWriter &);
15void SaveOutputMap(CDataFileReader &, CDataFileWriter &, CMapItemLayerQuads *, int, CQuad *, int);
16
17CMapItemLayerQuads *GetQuadLayer(CDataFileReader &, const int[2], int *);
18CQuad CreateNewQuad(float, float, int, int, const uint8_t[4], const int[2]);
19
20bool GetPixelClamped(const CImageInfo &, size_t, size_t, uint8_t[4]);
21bool ComparePixel(const uint8_t[4], const uint8_t[4]);
22bool IsPixelOptimizable(const CImageInfo &, size_t, size_t, const uint8_t[4], const bool[]);
23void SetVisitedPixels(const CImageInfo &, size_t, size_t, size_t, size_t, bool[]);
24
25size_t GetImagePixelSize(const CImageInfo &);
26size_t FindSuperPixelSize(const CImageInfo &, const uint8_t[4], size_t, size_t, size_t, bool[]);
27void GetOptimizedQuadSize(const CImageInfo &, size_t, const uint8_t[4], size_t, size_t, size_t &, size_t &, bool[]);
28
29int main(int argc, const char **argv)
30{
31 CCmdlineFix CmdlineFix(&argc, &argv);
32 log_set_global_logger_default();
33
34 if(argc < 10 || argc > 12)
35 {
36 dbg_msg(sys: "map_create_pixelart", fmt: "Invalid arguments");
37 dbg_msg(sys: "map_create_pixelart", fmt: "Usage: %s <image.png> <img_pixelsize> <input_map> <layergroup_id> <layer_id> <pos_x> <pos_y> <quad_pixelsize> <output_map> [optimize=0|1] [centralize=0|1]", argv[0]);
38 dbg_msg(sys: "map_create_pixelart", fmt: "Note: use destination layer tiles as a reference for positions and pixels for sizes.");
39 dbg_msg(sys: "map_create_pixelart", fmt: "Note: set img_pixelsize to 0 to consider the largest possible size.");
40 dbg_msg(sys: "map_create_pixelart", fmt: "Note: set quad_pixelsize to 0 to consider the same value of img_pixelsize.");
41 dbg_msg(sys: "map_create_pixelart", fmt: "Note: if image.png has not a perfect pixelart grid, result might be unexpected, manually fix img_pixelsize to get it better.");
42 dbg_msg(sys: "map_create_pixelart", fmt: "Options: optimize tries to reduce the total number of quads (default: 1).");
43 dbg_msg(sys: "map_create_pixelart", fmt: "Options: centralize places all pivots at the same position (default: 0).");
44
45 return -1;
46 }
47
48 char aFilenames[3][IO_MAX_PATH_LENGTH];
49 str_copy(dst&: aFilenames[0], src: argv[3]); //input_map
50 str_copy(dst&: aFilenames[1], src: argv[9]); //output_map
51 str_copy(dst&: aFilenames[2], src: argv[1]); //image_file
52
53 if(str_comp_filenames(a: aFilenames[0], b: aFilenames[1]) == 0)
54 {
55 dbg_msg(sys: "map_create_pixelart", fmt: "Invalid usage: you can not use the same map as input and output");
56 return -1;
57 }
58
59 int aLayerId[2] = {str_toint(str: argv[4]), str_toint(str: argv[5])}; //layergroup_id, layer_id
60 int aStartingPos[2] = {str_toint(str: argv[6]) * 32, str_toint(str: argv[7]) * 32}; //pos_x, pos_y
61 int aPixelSizes[2] = {str_toint(str: argv[2]), str_toint(str: argv[8])}; //quad_pixelsize, img_pixelsize
62
63 bool aArtOptions[3];
64 aArtOptions[0] = argc > 10 ? str_toint(str: argv[10]) : true; //optimize
65 aArtOptions[1] = argc > 11 ? str_toint(str: argv[11]) : false; //centralize
66
67 dbg_msg(sys: "map_create_pixelart", fmt: "image_file='%s'; image_pixelsize='%dpx'; input_map='%s'; layergroup_id='#%d'; layer_id='#%d'; pos_x='#%dpx'; pos_y='%dpx'; quad_pixelsize='%dpx'; output_map='%s'; optimize='%d'; centralize='%d'",
68 aFilenames[2], aPixelSizes[0], aFilenames[0], aLayerId[0], aLayerId[1], aStartingPos[0], aStartingPos[1], aPixelSizes[1], aFilenames[1], aArtOptions[0], aArtOptions[1]);
69
70 return !CreatePixelArt(aFilenames, aLayerId, aStartingPos, aPixelSizes, aArtOptions);
71}
72
73bool CreatePixelArt(const char aFilenames[3][IO_MAX_PATH_LENGTH], const int aLayerId[2], const int aStartingPos[2], int aPixelSizes[2], const bool aArtOptions[2])
74{
75 CImageInfo Img;
76 if(!LoadPng(&Img, aFilenames[2]))
77 return false;
78
79 aPixelSizes[0] = aPixelSizes[0] ? aPixelSizes[0] : GetImagePixelSize(Img);
80 aPixelSizes[1] = aPixelSizes[1] ? aPixelSizes[1] : aPixelSizes[0];
81
82 CDataFileReader InputMap;
83 CDataFileWriter OutputMap;
84 if(!OpenMaps(aFilenames, InputMap, OutputMap))
85 return false;
86
87 int ItemNumber = 0;
88 CMapItemLayerQuads *pQuadLayer = GetQuadLayer(InputMap, aLayerId, &ItemNumber);
89 if(!pQuadLayer)
90 return false;
91
92 size_t MaxNewQuads = std::ceil(x: (Img.m_Width * Img.m_Height) / aPixelSizes[0]);
93 CQuad *pQuads = new CQuad[pQuadLayer->m_NumQuads + MaxNewQuads];
94
95 InsertCurrentQuads(InputMap, pQuadLayer, pQuads);
96 int QuadsCounter = InsertPixelArtQuads(pQuads, pQuadLayer->m_NumQuads, Img, aStartingPos, aPixelSizes, aArtOptions);
97 SaveOutputMap(InputMap, OutputMap, pQuadLayer, ItemNumber, pQuads, (int)sizeof(CQuad) * pQuadLayer->m_NumQuads);
98 delete[] pQuads;
99
100 dbg_msg(sys: "map_create_pixelart", fmt: "INFO: successfully added %d new pixelart quads.", QuadsCounter);
101 return true;
102}
103
104void InsertCurrentQuads(CDataFileReader &InputMap, CMapItemLayerQuads *pQuadLayer, CQuad *pNewQuads)
105{
106 CQuad *pCurrentQuads = (CQuad *)InputMap.GetDataSwapped(Index: pQuadLayer->m_Data);
107 for(int i = 0; i < pQuadLayer->m_NumQuads; i++)
108 pNewQuads[i] = pCurrentQuads[i];
109}
110
111int InsertPixelArtQuads(CQuad *pQuads, int &NumQuads, const CImageInfo &Img, const int aStartingPos[2], const int aPixelSizes[2], const bool aArtOptions[2])
112{
113 size_t ImgPixelSize = aPixelSizes[0], QuadPixelSize = aPixelSizes[1], OriginalNumQuads = NumQuads;
114 int aForcedPivot[2] = {std::numeric_limits<int>::max(), std::numeric_limits<int>::max()};
115 bool *aVisitedPixels = new bool[Img.m_Height * Img.m_Width];
116 mem_zero(block: aVisitedPixels, size: sizeof(bool) * Img.m_Height * Img.m_Width);
117
118 for(size_t y = 0; y < Img.m_Height; y += ImgPixelSize)
119 for(size_t x = 0; x < Img.m_Width; x += ImgPixelSize)
120 {
121 uint8_t aPixel[4];
122 if(aVisitedPixels[x + y * Img.m_Width] || !GetPixelClamped(Img, x, y, aPixel))
123 continue;
124
125 size_t Width = 1, Height = 1;
126 if(aArtOptions[0])
127 GetOptimizedQuadSize(Img, ImgPixelSize, aPixel, x, y, Width, Height, aVisitedPixels);
128
129 float Posx = aStartingPos[0] + ((x / (float)ImgPixelSize) + (Width / 2.f)) * QuadPixelSize;
130 float Posy = aStartingPos[1] + ((y / (float)ImgPixelSize) + (Height / 2.f)) * QuadPixelSize;
131 if(aArtOptions[1] && aForcedPivot[0] == std::numeric_limits<int>::max())
132 {
133 aForcedPivot[0] = Posx;
134 aForcedPivot[1] = Posy;
135 }
136
137 pQuads[NumQuads] = CreateNewQuad(Posx, Posy, QuadPixelSize * Width, QuadPixelSize * Height, aPixel, aArtOptions[1] ? aForcedPivot : 0x0);
138 NumQuads++;
139 }
140 delete[] aVisitedPixels;
141
142 return NumQuads - OriginalNumQuads;
143}
144
145void GetOptimizedQuadSize(const CImageInfo &Img, const size_t ImgPixelSize, const uint8_t aPixel[4], const size_t PosX, const size_t PosY, size_t &Width, size_t &Height, bool aVisitedPixels[])
146{
147 size_t w = 0, h = 0, OptimizedWidth = 0, OptimizedHeight = 0;
148
149 while(IsPixelOptimizable(Img, PosX + w, PosY + h, aPixel, aVisitedPixels))
150 {
151 while(IsPixelOptimizable(Img, PosX + w, PosY + h, aPixel, aVisitedPixels) && (!OptimizedHeight || h < OptimizedHeight))
152 h += ImgPixelSize;
153
154 if(!OptimizedHeight || h < OptimizedHeight)
155 OptimizedHeight = h;
156
157 h = 0;
158 w += ImgPixelSize;
159 OptimizedWidth = w;
160 }
161
162 SetVisitedPixels(Img, PosX, PosY, OptimizedWidth, OptimizedHeight, aVisitedPixels);
163 Width = OptimizedWidth / ImgPixelSize;
164 Height = OptimizedHeight / ImgPixelSize;
165}
166
167size_t GetImagePixelSize(const CImageInfo &Img)
168{
169 size_t ImgPixelSize = std::numeric_limits<size_t>::max();
170 bool *aVisitedPixels = new bool[Img.m_Height * Img.m_Width];
171 mem_zero(block: aVisitedPixels, size: sizeof(bool) * Img.m_Height * Img.m_Width);
172
173 for(size_t y = 0; y < Img.m_Height && ImgPixelSize > 1; y++)
174 for(size_t x = 0; x < Img.m_Width && ImgPixelSize > 1; x++)
175 {
176 uint8_t aPixel[4];
177 if(aVisitedPixels[x + y * Img.m_Width])
178 continue;
179
180 GetPixelClamped(Img, x, y, aPixel);
181 size_t SuperPixelSize = FindSuperPixelSize(Img, aPixel, x, y, 1, aVisitedPixels);
182 if(SuperPixelSize < ImgPixelSize)
183 ImgPixelSize = SuperPixelSize;
184 }
185 delete[] aVisitedPixels;
186
187 dbg_msg(sys: "map_create_pixelart", fmt: "INFO: automatically detected img_pixelsize of %" PRIzu "px", ImgPixelSize);
188 return ImgPixelSize;
189}
190
191size_t FindSuperPixelSize(const CImageInfo &Img, const uint8_t aPixel[4], const size_t PosX, const size_t PosY, const size_t CurrentSize, bool aVisitedPixels[])
192{
193 if(PosX + CurrentSize >= Img.m_Width || PosY + CurrentSize >= Img.m_Height)
194 {
195 SetVisitedPixels(Img, PosX, PosY, CurrentSize, CurrentSize, aVisitedPixels);
196 return CurrentSize;
197 }
198
199 for(int i = 0; i < 2; i++)
200 {
201 for(size_t j = 0; j < CurrentSize + 1; j++)
202 {
203 uint8_t aCheckPixel[4];
204 size_t x = PosX, y = PosY;
205 x += i == 0 ? j : CurrentSize;
206 y += i == 0 ? CurrentSize : j;
207
208 GetPixelClamped(Img, x, y, aCheckPixel);
209 if(x >= Img.m_Width || y >= Img.m_Height || !ComparePixel(aPixel, aCheckPixel))
210 {
211 SetVisitedPixels(Img, PosX, PosY, CurrentSize, CurrentSize, aVisitedPixels);
212 return CurrentSize;
213 }
214 }
215 }
216
217 return FindSuperPixelSize(Img, aPixel, PosX, PosY, CurrentSize: CurrentSize + 1, aVisitedPixels);
218}
219
220bool GetPixelClamped(const CImageInfo &Img, size_t x, size_t y, uint8_t aPixel[4])
221{
222 x = clamp<size_t>(val: x, lo: 0, hi: Img.m_Width - 1);
223 y = clamp<size_t>(val: y, lo: 0, hi: Img.m_Height - 1);
224 aPixel[0] = 255;
225 aPixel[1] = 255;
226 aPixel[2] = 255;
227 aPixel[3] = 255;
228
229 const size_t PixelSize = Img.PixelSize();
230 for(size_t i = 0; i < PixelSize; i++)
231 aPixel[i] = Img.m_pData[x * PixelSize + (Img.m_Width * PixelSize * y) + i];
232
233 return aPixel[3] > 0;
234}
235
236bool ComparePixel(const uint8_t aPixel1[4], const uint8_t aPixel2[4])
237{
238 for(int i = 0; i < 4; i++)
239 if(aPixel1[i] != aPixel2[i])
240 return false;
241 return true;
242}
243
244bool IsPixelOptimizable(const CImageInfo &Img, const size_t PosX, const size_t PosY, const uint8_t aPixel[4], const bool aVisitedPixels[])
245{
246 uint8_t aCheckPixel[4];
247 return PosX < Img.m_Width && PosY < Img.m_Height && !aVisitedPixels[PosX + PosY * Img.m_Width] && GetPixelClamped(Img, x: PosX, y: PosY, aPixel: aCheckPixel) && ComparePixel(aPixel1: aPixel, aPixel2: aCheckPixel);
248}
249
250void SetVisitedPixels(const CImageInfo &Img, size_t PosX, size_t PosY, size_t Width, size_t Height, bool aVisitedPixels[])
251{
252 for(size_t y = PosY; y < PosY + Height; y++)
253 for(size_t x = PosX; x < PosX + Width; x++)
254 aVisitedPixels[x + y * Img.m_Width] = true;
255}
256
257CMapItemLayerQuads *GetQuadLayer(CDataFileReader &InputMap, const int aLayerId[2], int *pItemNumber)
258{
259 int Start, Num;
260 InputMap.GetType(Type: MAPITEMTYPE_GROUP, pStart: &Start, pNum: &Num);
261
262 CMapItemGroup *pGroupItem = aLayerId[0] >= Num ? 0x0 : (CMapItemGroup *)InputMap.GetItem(Index: Start + aLayerId[0]);
263
264 if(!pGroupItem)
265 {
266 dbg_msg(sys: "map_create_pixelart", fmt: "ERROR: unable to find layergroup '#%d'", aLayerId[0]);
267 return 0x0;
268 }
269
270 InputMap.GetType(Type: MAPITEMTYPE_LAYER, pStart: &Start, pNum: &Num);
271 *pItemNumber = Start + pGroupItem->m_StartLayer + aLayerId[1];
272
273 CMapItemLayer *pLayerItem = aLayerId[1] >= pGroupItem->m_NumLayers ? 0x0 : (CMapItemLayer *)InputMap.GetItem(Index: *pItemNumber);
274 if(!pLayerItem)
275 {
276 dbg_msg(sys: "map_create_pixelart", fmt: "ERROR: unable to find layer '#%d' in group '#%d'", aLayerId[1], aLayerId[0]);
277 return 0x0;
278 }
279
280 if(pLayerItem->m_Type != LAYERTYPE_QUADS)
281 {
282 dbg_msg(sys: "map_create_pixelart", fmt: "ERROR: layer '#%d' in group '#%d' is not a quad layer", aLayerId[1], aLayerId[0]);
283 return 0x0;
284 }
285
286 return (CMapItemLayerQuads *)pLayerItem;
287}
288
289CQuad CreateNewQuad(const float PosX, const float PosY, const int Width, const int Height, const uint8_t aColor[4], const int aForcedPivot[2] = 0x0)
290{
291 CQuad Quad;
292 Quad.m_PosEnv = Quad.m_ColorEnv = -1;
293 Quad.m_PosEnvOffset = Quad.m_ColorEnvOffset = 0;
294 float x = f2fx(v: PosX), y = f2fx(v: PosY), w = f2fx(v: Width / 2.f), h = f2fx(v: Height / 2.f);
295
296 for(int i = 0; i < 2; i++)
297 {
298 Quad.m_aPoints[i].y = y - h;
299 Quad.m_aPoints[i + 2].y = y + h;
300 Quad.m_aPoints[i * 2].x = x - w;
301 Quad.m_aPoints[i * 2 + 1].x = x + w;
302 }
303
304 for(auto &QuadColor : Quad.m_aColors)
305 {
306 QuadColor.r = aColor[0];
307 QuadColor.g = aColor[1];
308 QuadColor.b = aColor[2];
309 QuadColor.a = aColor[3];
310 }
311
312 Quad.m_aPoints[4].x = aForcedPivot ? f2fx(v: aForcedPivot[0]) : x;
313 Quad.m_aPoints[4].y = aForcedPivot ? f2fx(v: aForcedPivot[1]) : y;
314
315 return Quad;
316}
317
318bool LoadPng(CImageInfo *pImg, const char *pFilename)
319{
320 IOHANDLE File = io_open(filename: pFilename, flags: IOFLAG_READ);
321 if(!File)
322 {
323 dbg_msg(sys: "map_create_pixelart", fmt: "ERROR: Unable to open file %s", pFilename);
324 return false;
325 }
326
327 io_seek(io: File, offset: 0, origin: IOSEEK_END);
328 long int FileSize = io_tell(io: File);
329 if(FileSize <= 0)
330 {
331 io_close(io: File);
332 dbg_msg(sys: "map_create_pixelart", fmt: "ERROR: Failed to get file size (%ld). filename='%s'", FileSize, pFilename);
333 return false;
334 }
335 io_seek(io: File, offset: 0, origin: IOSEEK_START);
336 TImageByteBuffer ByteBuffer;
337 SImageByteBuffer ImageByteBuffer(&ByteBuffer);
338
339 ByteBuffer.resize(new_size: FileSize);
340 io_read(io: File, buffer: &ByteBuffer.front(), size: FileSize);
341 io_close(io: File);
342
343 uint8_t *pImgBuffer = NULL;
344 EImageFormat ImageFormat;
345 int PngliteIncompatible;
346
347 if(!LoadPng(ByteLoader&: ImageByteBuffer, pFileName: pFilename, PngliteIncompatible, Width&: pImg->m_Width, Height&: pImg->m_Height, pImageBuff&: pImgBuffer, ImageFormat))
348 {
349 dbg_msg(sys: "map_create_pixelart", fmt: "ERROR: Unable to load a valid PNG from file %s", pFilename);
350 return false;
351 }
352
353 if(ImageFormat != IMAGE_FORMAT_RGBA && ImageFormat != IMAGE_FORMAT_RGB)
354 {
355 dbg_msg(sys: "map_create_pixelart", fmt: "ERROR: only RGB and RGBA PNG images are supported");
356 free(ptr: pImgBuffer);
357 return false;
358 }
359
360 pImg->m_pData = pImgBuffer;
361 pImg->m_Format = ImageFormat == IMAGE_FORMAT_RGB ? CImageInfo::FORMAT_RGB : CImageInfo::FORMAT_RGBA;
362
363 return true;
364}
365
366bool OpenMaps(const char pMapNames[2][IO_MAX_PATH_LENGTH], CDataFileReader &InputMap, CDataFileWriter &OutputMap)
367{
368 IStorage *pStorage = CreateLocalStorage();
369
370 if(!InputMap.Open(pStorage, pFilename: pMapNames[0], StorageType: IStorage::TYPE_ABSOLUTE))
371 {
372 dbg_msg(sys: "map_create_pixelart", fmt: "ERROR: unable to open map '%s'", pMapNames[0]);
373 return false;
374 }
375
376 if(!OutputMap.Open(pStorage, pFilename: pMapNames[1], StorageType: IStorage::TYPE_ABSOLUTE))
377 {
378 dbg_msg(sys: "map_create_pixelart", fmt: "ERROR: unable to open map '%s'", pMapNames[1]);
379 return false;
380 }
381
382 return true;
383}
384
385void SaveOutputMap(CDataFileReader &InputMap, CDataFileWriter &OutputMap, CMapItemLayerQuads *pNewItem, const int NewItemNumber, CQuad *pNewData, const int NewDataSize)
386{
387 for(int i = 0; i < InputMap.NumItems(); i++)
388 {
389 int Id, Type;
390 CUuid Uuid;
391 void *pItem = InputMap.GetItem(Index: i, pType: &Type, pId: &Id, pUuid: &Uuid);
392
393 // Filter ITEMTYPE_EX items, they will be automatically added again.
394 if(Type == ITEMTYPE_EX)
395 {
396 continue;
397 }
398
399 if(i == NewItemNumber)
400 pItem = pNewItem;
401
402 int Size = InputMap.GetItemSize(Index: i);
403 OutputMap.AddItem(Type, Id, Size, pData: pItem, pUuid: &Uuid);
404 }
405
406 for(int i = 0; i < InputMap.NumData(); i++)
407 {
408 if(i == pNewItem->m_Data)
409 OutputMap.AddData(Size: NewDataSize, pData: pNewData);
410 else
411 OutputMap.AddData(Size: InputMap.GetDataSize(Index: i), pData: InputMap.GetData(Index: i));
412 }
413
414 OutputMap.Finish();
415}
416