1#include <base/dbg.h>
2#include <base/fs.h>
3#include <base/logger.h>
4#include <base/mem.h>
5#include <base/os.h>
6#include <base/str.h>
7
8#include <engine/gfx/image_manipulation.h>
9#include <engine/shared/datafile.h>
10#include <engine/storage.h>
11
12#include <game/mapitems.h>
13
14#include <algorithm>
15#include <cstdint>
16#include <vector>
17
18static void ClearTransparentPixels(uint8_t *pImg, int Width, int Height)
19{
20 for(int y = 0; y < Height; ++y)
21 {
22 for(int x = 0; x < Width; ++x)
23 {
24 int Index = y * Width * 4 + x * 4;
25 if(pImg[Index + 3] == 0)
26 {
27 pImg[Index + 0] = 0;
28 pImg[Index + 1] = 0;
29 pImg[Index + 2] = 0;
30 }
31 }
32 }
33}
34
35static void CopyOpaquePixels(uint8_t *pDestImg, uint8_t *pSrcImg, int Width, int Height)
36{
37 for(int y = 0; y < Height; ++y)
38 {
39 for(int x = 0; x < Width; ++x)
40 {
41 int Index = y * Width * 4 + x * 4;
42 if(pSrcImg[Index + 3] > 0)
43 mem_copy(dest: &pDestImg[Index], source: &pSrcImg[Index], size: sizeof(uint8_t) * 4);
44 else
45 mem_zero(block: &pDestImg[Index], size: sizeof(uint8_t) * 4);
46 }
47 }
48}
49
50static void ClearPixelsTile(uint8_t *pImg, int Width, int Height, int TileIndex)
51{
52 int WTile = Width / 16;
53 int HTile = Height / 16;
54 int StartX = (TileIndex % 16) * WTile;
55 int StartY = (TileIndex / 16) * HTile;
56
57 for(int y = StartY; y < StartY + HTile; ++y)
58 {
59 for(int x = StartX; x < StartX + WTile; ++x)
60 {
61 int Index = y * Width * 4 + x * 4;
62 pImg[Index + 0] = 0;
63 pImg[Index + 1] = 0;
64 pImg[Index + 2] = 0;
65 pImg[Index + 3] = 0;
66 }
67 }
68}
69
70static void GetImageSHA256(uint8_t *pImgBuff, int ImgSize, int Width, int Height, char *pSHA256Str, size_t SHA256StrSize)
71{
72 uint8_t *pNewImgBuff = (uint8_t *)malloc(size: ImgSize);
73
74 // Clear fully transparent pixels, so the SHA is easier to identify with the original image
75 CopyOpaquePixels(pDestImg: pNewImgBuff, pSrcImg: pImgBuff, Width, Height);
76 SHA256_DIGEST SHAStr = sha256(message: pNewImgBuff, message_len: (size_t)ImgSize);
77
78 sha256_str(digest: SHAStr, str: pSHA256Str, max_len: SHA256StrSize);
79
80 free(ptr: pNewImgBuff);
81}
82
83int main(int argc, const char **argv)
84{
85 CCmdlineFix CmdlineFix(&argc, &argv);
86 log_set_global_logger_default();
87
88 std::unique_ptr<IStorage> pStorage = std::unique_ptr<IStorage>(CreateStorage(InitializationType: IStorage::EInitializationType::BASIC, NumArgs: argc, ppArguments: argv));
89 if(!pStorage)
90 {
91 log_error("map_optimize", "Error creating basic storage");
92 return -1;
93 }
94 if(argc <= 1 || argc > 3)
95 {
96 dbg_msg(sys: "map_optimize", fmt: "Usage: map_optimize <source map filepath> [<dest map filepath>]");
97 return -1;
98 }
99
100 char aFilename[IO_MAX_PATH_LENGTH];
101 if(argc == 3)
102 {
103 str_format(buffer: aFilename, buffer_size: sizeof(aFilename), format: "out/%s", argv[2]);
104
105 if(fs_makedir_rec_for(path: aFilename) != 0)
106 {
107 // Error already logged by fs_makedir_rec_for
108 return -1;
109 }
110 }
111 else
112 {
113 if(fs_makedir(path: "out") != 0)
114 {
115 // Error already logged by fs_makedir
116 return -1;
117 }
118 char aBuff[IO_MAX_PATH_LENGTH];
119 fs_split_file_extension(filename: fs_filename(path: argv[1]), name: aBuff, name_size: sizeof(aBuff));
120 str_format(buffer: aFilename, buffer_size: sizeof(aFilename), format: "out/%s.map", aBuff);
121 }
122
123 CDataFileReader Reader;
124 if(!Reader.Open(pStorage: pStorage.get(), pPath: argv[1], StorageType: IStorage::TYPE_ABSOLUTE))
125 {
126 dbg_msg(sys: "map_optimize", fmt: "Failed to open source file.");
127 return -1;
128 }
129
130 CDataFileWriter Writer;
131 if(!Writer.Open(pStorage: pStorage.get(), pFilename: aFilename, StorageType: IStorage::TYPE_ABSOLUTE))
132 {
133 dbg_msg(sys: "map_optimize", fmt: "Failed to open target file.");
134 return -1;
135 }
136
137 int aImageFlags[MAX_MAPIMAGES] = {
138 0,
139 };
140
141 bool aaImageTiles[MAX_MAPIMAGES][256]{
142 {
143 false,
144 },
145 };
146
147 struct SMapOptimizeItem
148 {
149 CMapItemImage *m_pImage;
150 int m_Index;
151 int m_Data;
152 int m_Text;
153 };
154
155 std::vector<SMapOptimizeItem> vDataFindHelper;
156
157 // add all items
158 for(int Index = 0, i = 0; Index < Reader.NumItems(); Index++)
159 {
160 int Type, Id;
161 CUuid Uuid;
162 void *pPtr = Reader.GetItem(Index, pType: &Type, pId: &Id, pUuid: &Uuid);
163
164 // Filter ITEMTYPE_EX items, they will be automatically added again.
165 if(Type == ITEMTYPE_EX)
166 {
167 continue;
168 }
169
170 // for all layers, check if it uses a image and set the corresponding flag
171 if(Type == MAPITEMTYPE_LAYER)
172 {
173 CMapItemLayer *pLayer = (CMapItemLayer *)pPtr;
174 if(pLayer->m_Type == LAYERTYPE_TILES)
175 {
176 CMapItemLayerTilemap *pTLayer = (CMapItemLayerTilemap *)pLayer;
177 if(pTLayer->m_Image >= 0 && pTLayer->m_Image < (int)MAX_MAPIMAGES && pTLayer->m_Flags == 0)
178 {
179 aImageFlags[pTLayer->m_Image] |= 1;
180 // check tiles that are used in this image
181 unsigned int DataSize = Reader.GetDataSize(Index: pTLayer->m_Data);
182 void *pTiles = Reader.GetData(Index: pTLayer->m_Data);
183
184 if(DataSize >= (size_t)pTLayer->m_Width * pTLayer->m_Height * sizeof(CTile))
185 {
186 for(int y = 0; y < pTLayer->m_Height; ++y)
187 {
188 for(int x = 0; x < pTLayer->m_Width; ++x)
189 {
190 int TileIndex = ((CTile *)pTiles)[y * pTLayer->m_Width + x].m_Index;
191 if(TileIndex > 0)
192 {
193 aaImageTiles[pTLayer->m_Image][TileIndex] = true;
194 }
195 }
196 }
197 }
198 }
199 }
200 else if(pLayer->m_Type == LAYERTYPE_QUADS)
201 {
202 CMapItemLayerQuads *pQLayer = (CMapItemLayerQuads *)pLayer;
203 if(pQLayer->m_Image >= 0 && pQLayer->m_Image < (int)MAX_MAPIMAGES)
204 {
205 aImageFlags[pQLayer->m_Image] |= 2;
206 }
207 }
208 }
209 else if(Type == MAPITEMTYPE_IMAGE)
210 {
211 CMapItemImage_v2 *pImg = (CMapItemImage_v2 *)pPtr;
212 if(!pImg->m_External && pImg->m_Version < 2)
213 {
214 SMapOptimizeItem Item;
215 Item.m_pImage = pImg;
216 Item.m_Index = i;
217 Item.m_Data = pImg->m_ImageData;
218 Item.m_Text = pImg->m_ImageName;
219 vDataFindHelper.push_back(x: Item);
220 }
221
222 // found an image
223 ++i;
224 }
225
226 int Size = Reader.GetItemSize(Index);
227 Writer.AddItem(Type, Id, Size, pData: pPtr, pUuid: &Uuid);
228 }
229
230 // add all data
231 for(int Index = 0; Index < Reader.NumData(); Index++)
232 {
233 bool DeletePtr = false;
234 void *pPtr = Reader.GetData(Index);
235 int Size = Reader.GetDataSize(Index);
236 auto MapDataItemIterator = std::find_if(first: vDataFindHelper.begin(), last: vDataFindHelper.end(), pred: [Index](const SMapOptimizeItem &Other) -> bool { return Other.m_Data == Index || Other.m_Text == Index; });
237 if(MapDataItemIterator != vDataFindHelper.end())
238 {
239 int Width = MapDataItemIterator->m_pImage->m_Width;
240 int Height = MapDataItemIterator->m_pImage->m_Height;
241
242 int ImageIndex = MapDataItemIterator->m_Index;
243 if(MapDataItemIterator->m_Data == Index)
244 {
245 DeletePtr = true;
246 // optimize embedded images
247 // use a new pointer, to be safe, when using the original image data
248 void *pNewPtr = malloc(size: Size);
249 mem_copy(dest: pNewPtr, source: pPtr, size: Size);
250 pPtr = pNewPtr;
251 uint8_t *pImgBuff = (uint8_t *)pPtr;
252
253 bool DoClearTransparentPixels = false;
254 bool DilateAs2DArray = false;
255 bool DoDilate = false;
256
257 // all tiles that aren't used are cleared(if image was only used by tilemap)
258 if(aImageFlags[ImageIndex] == 1)
259 {
260 for(int i = 0; i < 256; ++i)
261 {
262 if(!aaImageTiles[ImageIndex][i])
263 {
264 ClearPixelsTile(pImg: pImgBuff, Width, Height, TileIndex: i);
265 }
266 }
267
268 DoClearTransparentPixels = true;
269 DilateAs2DArray = true;
270 DoDilate = true;
271 }
272 else if(aImageFlags[ImageIndex] == 0)
273 {
274 mem_zero(block: pImgBuff, size: (size_t)Width * Height * 4);
275 }
276 else
277 {
278 DoClearTransparentPixels = true;
279 DoDilate = true;
280 }
281
282 if(DoClearTransparentPixels)
283 {
284 // clear unused pixels and make a clean dilate for the compressor
285 ClearTransparentPixels(pImg: pImgBuff, Width, Height);
286 }
287
288 if(DoDilate)
289 {
290 if(DilateAs2DArray)
291 {
292 for(int i = 0; i < 256; ++i)
293 {
294 int ImgTileW = Width / 16;
295 int ImgTileH = Height / 16;
296 int x = (i % 16) * ImgTileW;
297 int y = (i / 16) * ImgTileH;
298 DilateImageSub(pImageBuff: pImgBuff, w: Width, h: Height, x, y, SubWidth: ImgTileW, SubHeight: ImgTileH);
299 }
300 }
301 else
302 {
303 DilateImage(pImageBuff: pImgBuff, w: Width, h: Height);
304 }
305 }
306 }
307 else if(MapDataItemIterator->m_Text == Index)
308 {
309 char *pImgName = (char *)pPtr;
310 uint8_t *pImgBuff = (uint8_t *)Reader.GetData(Index: MapDataItemIterator->m_Data);
311 int ImgSize = Reader.GetDataSize(Index: MapDataItemIterator->m_Data);
312
313 char aSHA256Str[SHA256_MAXSTRSIZE];
314 // This is the important function, that calculates the SHA256 in a special way
315 // Please read the comments inside the functions to understand it
316 GetImageSHA256(pImgBuff, ImgSize, Width, Height, pSHA256Str: aSHA256Str, SHA256StrSize: sizeof(aSHA256Str));
317
318 char aNewName[IO_MAX_PATH_LENGTH];
319 int StrLen = str_format(buffer: aNewName, buffer_size: std::size(aNewName), format: "%s_cut_%s", pImgName, aSHA256Str);
320
321 DeletePtr = true;
322 // make the new name ready
323 char *pNewPtr = (char *)malloc(size: StrLen + 1);
324 str_copy(dst: pNewPtr, src: aNewName, dst_size: StrLen + 1);
325 pPtr = pNewPtr;
326 Size = StrLen + 1;
327 }
328 }
329
330 Writer.AddData(Size, pData: pPtr, CompressionLevel: CDataFileWriter::COMPRESSION_BEST);
331
332 if(DeletePtr)
333 free(ptr: pPtr);
334 }
335
336 Reader.Close();
337 Writer.Finish();
338
339 return 0;
340}
341