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