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 fs_makedir_rec_for(path: aFilename);
106 }
107 else
108 {
109 fs_makedir(path: "out");
110 char aBuff[IO_MAX_PATH_LENGTH];
111 fs_split_file_extension(filename: fs_filename(path: argv[1]), name: aBuff, name_size: sizeof(aBuff));
112 str_format(buffer: aFilename, buffer_size: sizeof(aFilename), format: "out/%s.map", aBuff);
113 }
114
115 CDataFileReader Reader;
116 if(!Reader.Open(pStorage: pStorage.get(), pPath: argv[1], StorageType: IStorage::TYPE_ABSOLUTE))
117 {
118 dbg_msg(sys: "map_optimize", fmt: "Failed to open source file.");
119 return -1;
120 }
121
122 CDataFileWriter Writer;
123 if(!Writer.Open(pStorage: pStorage.get(), pFilename: aFilename, StorageType: IStorage::TYPE_ABSOLUTE))
124 {
125 dbg_msg(sys: "map_optimize", fmt: "Failed to open target file.");
126 return -1;
127 }
128
129 int aImageFlags[MAX_MAPIMAGES] = {
130 0,
131 };
132
133 bool aaImageTiles[MAX_MAPIMAGES][256]{
134 {
135 false,
136 },
137 };
138
139 struct SMapOptimizeItem
140 {
141 CMapItemImage *m_pImage;
142 int m_Index;
143 int m_Data;
144 int m_Text;
145 };
146
147 std::vector<SMapOptimizeItem> vDataFindHelper;
148
149 // add all items
150 for(int Index = 0, i = 0; Index < Reader.NumItems(); Index++)
151 {
152 int Type, Id;
153 CUuid Uuid;
154 void *pPtr = Reader.GetItem(Index, pType: &Type, pId: &Id, pUuid: &Uuid);
155
156 // Filter ITEMTYPE_EX items, they will be automatically added again.
157 if(Type == ITEMTYPE_EX)
158 {
159 continue;
160 }
161
162 // for all layers, check if it uses a image and set the corresponding flag
163 if(Type == MAPITEMTYPE_LAYER)
164 {
165 CMapItemLayer *pLayer = (CMapItemLayer *)pPtr;
166 if(pLayer->m_Type == LAYERTYPE_TILES)
167 {
168 CMapItemLayerTilemap *pTLayer = (CMapItemLayerTilemap *)pLayer;
169 if(pTLayer->m_Image >= 0 && pTLayer->m_Image < (int)MAX_MAPIMAGES && pTLayer->m_Flags == 0)
170 {
171 aImageFlags[pTLayer->m_Image] |= 1;
172 // check tiles that are used in this image
173 unsigned int DataSize = Reader.GetDataSize(Index: pTLayer->m_Data);
174 void *pTiles = Reader.GetData(Index: pTLayer->m_Data);
175
176 if(DataSize >= (size_t)pTLayer->m_Width * pTLayer->m_Height * sizeof(CTile))
177 {
178 for(int y = 0; y < pTLayer->m_Height; ++y)
179 {
180 for(int x = 0; x < pTLayer->m_Width; ++x)
181 {
182 int TileIndex = ((CTile *)pTiles)[y * pTLayer->m_Width + x].m_Index;
183 if(TileIndex > 0)
184 {
185 aaImageTiles[pTLayer->m_Image][TileIndex] = true;
186 }
187 }
188 }
189 }
190 }
191 }
192 else if(pLayer->m_Type == LAYERTYPE_QUADS)
193 {
194 CMapItemLayerQuads *pQLayer = (CMapItemLayerQuads *)pLayer;
195 if(pQLayer->m_Image >= 0 && pQLayer->m_Image < (int)MAX_MAPIMAGES)
196 {
197 aImageFlags[pQLayer->m_Image] |= 2;
198 }
199 }
200 }
201 else if(Type == MAPITEMTYPE_IMAGE)
202 {
203 CMapItemImage_v2 *pImg = (CMapItemImage_v2 *)pPtr;
204 if(!pImg->m_External && pImg->m_Version < 2)
205 {
206 SMapOptimizeItem Item;
207 Item.m_pImage = pImg;
208 Item.m_Index = i;
209 Item.m_Data = pImg->m_ImageData;
210 Item.m_Text = pImg->m_ImageName;
211 vDataFindHelper.push_back(x: Item);
212 }
213
214 // found an image
215 ++i;
216 }
217
218 int Size = Reader.GetItemSize(Index);
219 Writer.AddItem(Type, Id, Size, pData: pPtr, pUuid: &Uuid);
220 }
221
222 // add all data
223 for(int Index = 0; Index < Reader.NumData(); Index++)
224 {
225 bool DeletePtr = false;
226 void *pPtr = Reader.GetData(Index);
227 int Size = Reader.GetDataSize(Index);
228 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; });
229 if(MapDataItemIterator != vDataFindHelper.end())
230 {
231 int Width = MapDataItemIterator->m_pImage->m_Width;
232 int Height = MapDataItemIterator->m_pImage->m_Height;
233
234 int ImageIndex = MapDataItemIterator->m_Index;
235 if(MapDataItemIterator->m_Data == Index)
236 {
237 DeletePtr = true;
238 // optimize embedded images
239 // use a new pointer, to be safe, when using the original image data
240 void *pNewPtr = malloc(size: Size);
241 mem_copy(dest: pNewPtr, source: pPtr, size: Size);
242 pPtr = pNewPtr;
243 uint8_t *pImgBuff = (uint8_t *)pPtr;
244
245 bool DoClearTransparentPixels = false;
246 bool DilateAs2DArray = false;
247 bool DoDilate = false;
248
249 // all tiles that aren't used are cleared(if image was only used by tilemap)
250 if(aImageFlags[ImageIndex] == 1)
251 {
252 for(int i = 0; i < 256; ++i)
253 {
254 if(!aaImageTiles[ImageIndex][i])
255 {
256 ClearPixelsTile(pImg: pImgBuff, Width, Height, TileIndex: i);
257 }
258 }
259
260 DoClearTransparentPixels = true;
261 DilateAs2DArray = true;
262 DoDilate = true;
263 }
264 else if(aImageFlags[ImageIndex] == 0)
265 {
266 mem_zero(block: pImgBuff, size: (size_t)Width * Height * 4);
267 }
268 else
269 {
270 DoClearTransparentPixels = true;
271 DoDilate = true;
272 }
273
274 if(DoClearTransparentPixels)
275 {
276 // clear unused pixels and make a clean dilate for the compressor
277 ClearTransparentPixels(pImg: pImgBuff, Width, Height);
278 }
279
280 if(DoDilate)
281 {
282 if(DilateAs2DArray)
283 {
284 for(int i = 0; i < 256; ++i)
285 {
286 int ImgTileW = Width / 16;
287 int ImgTileH = Height / 16;
288 int x = (i % 16) * ImgTileW;
289 int y = (i / 16) * ImgTileH;
290 DilateImageSub(pImageBuff: pImgBuff, w: Width, h: Height, x, y, SubWidth: ImgTileW, SubHeight: ImgTileH);
291 }
292 }
293 else
294 {
295 DilateImage(pImageBuff: pImgBuff, w: Width, h: Height);
296 }
297 }
298 }
299 else if(MapDataItemIterator->m_Text == Index)
300 {
301 char *pImgName = (char *)pPtr;
302 uint8_t *pImgBuff = (uint8_t *)Reader.GetData(Index: MapDataItemIterator->m_Data);
303 int ImgSize = Reader.GetDataSize(Index: MapDataItemIterator->m_Data);
304
305 char aSHA256Str[SHA256_MAXSTRSIZE];
306 // This is the important function, that calculates the SHA256 in a special way
307 // Please read the comments inside the functions to understand it
308 GetImageSHA256(pImgBuff, ImgSize, Width, Height, pSHA256Str: aSHA256Str, SHA256StrSize: sizeof(aSHA256Str));
309
310 char aNewName[IO_MAX_PATH_LENGTH];
311 int StrLen = str_format(buffer: aNewName, buffer_size: std::size(aNewName), format: "%s_cut_%s", pImgName, aSHA256Str);
312
313 DeletePtr = true;
314 // make the new name ready
315 char *pNewPtr = (char *)malloc(size: StrLen + 1);
316 str_copy(dst: pNewPtr, src: aNewName, dst_size: StrLen + 1);
317 pPtr = pNewPtr;
318 Size = StrLen + 1;
319 }
320 }
321
322 Writer.AddData(Size, pData: pPtr, CompressionLevel: CDataFileWriter::COMPRESSION_BEST);
323
324 if(DeletePtr)
325 free(ptr: pPtr);
326 }
327
328 Reader.Close();
329 Writer.Finish();
330
331 return 0;
332}
333