1#include <cinttypes>
2#include <cstdio> // sscanf
3
4#include <engine/console.h>
5#include <engine/shared/linereader.h>
6#include <engine/storage.h>
7
8#include <game/editor/mapitems/layer_tiles.h>
9#include <game/mapitems.h>
10
11#include "auto_map.h"
12#include "editor_actions.h"
13
14// Based on triple32inc from https://github.com/skeeto/hash-prospector/tree/79a6074062a84907df6e45b756134b74e2956760
15static uint32_t HashUInt32(uint32_t Num)
16{
17 Num++;
18 Num ^= Num >> 17;
19 Num *= 0xed5ad4bbu;
20 Num ^= Num >> 11;
21 Num *= 0xac4c1b51u;
22 Num ^= Num >> 15;
23 Num *= 0x31848babu;
24 Num ^= Num >> 14;
25 return Num;
26}
27
28#define HASH_MAX 65536
29
30static int HashLocation(uint32_t Seed, uint32_t Run, uint32_t Rule, uint32_t X, uint32_t Y)
31{
32 const uint32_t Prime = 31;
33 uint32_t Hash = 1;
34 Hash = Hash * Prime + HashUInt32(Num: Seed);
35 Hash = Hash * Prime + HashUInt32(Num: Run);
36 Hash = Hash * Prime + HashUInt32(Num: Rule);
37 Hash = Hash * Prime + HashUInt32(Num: X);
38 Hash = Hash * Prime + HashUInt32(Num: Y);
39 Hash = HashUInt32(Num: Hash * Prime); // Just to double-check that values are well-distributed
40 return Hash % HASH_MAX;
41}
42
43CAutoMapper::CAutoMapper(CEditor *pEditor)
44{
45 Init(pEditor);
46}
47
48void CAutoMapper::Load(const char *pTileName)
49{
50 char aPath[IO_MAX_PATH_LENGTH];
51 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "editor/automap/%s.rules", pTileName);
52 CLineReader LineReader;
53 if(!LineReader.OpenFile(File: Storage()->OpenFile(pFilename: aPath, Flags: IOFLAG_READ, Type: IStorage::TYPE_ALL)))
54 {
55 char aBuf[IO_MAX_PATH_LENGTH + 32];
56 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "failed to load %s", aPath);
57 Console()->Print(Level: IConsole::OUTPUT_LEVEL_DEBUG, pFrom: "editor/automap", pStr: aBuf);
58 return;
59 }
60
61 CConfiguration *pCurrentConf = nullptr;
62 CRun *pCurrentRun = nullptr;
63 CIndexRule *pCurrentIndex = nullptr;
64
65 // read each line
66 while(const char *pLine = LineReader.Get())
67 {
68 // skip blank/empty lines as well as comments
69 if(str_length(str: pLine) > 0 && pLine[0] != '#' && pLine[0] != '\n' && pLine[0] != '\r' && pLine[0] != '\t' && pLine[0] != '\v' && pLine[0] != ' ')
70 {
71 if(pLine[0] == '[')
72 {
73 // new configuration, get the name
74 pLine++;
75 CConfiguration NewConf;
76 NewConf.m_aName[0] = '\0';
77 NewConf.m_StartX = 0;
78 NewConf.m_StartY = 0;
79 NewConf.m_EndX = 0;
80 NewConf.m_EndY = 0;
81 m_vConfigs.push_back(x: NewConf);
82 int ConfigurationId = m_vConfigs.size() - 1;
83 pCurrentConf = &m_vConfigs[ConfigurationId];
84 str_copy(dst: pCurrentConf->m_aName, src: pLine, dst_size: minimum<int>(a: sizeof(pCurrentConf->m_aName), b: str_length(str: pLine)));
85
86 // add start run
87 CRun NewRun;
88 NewRun.m_AutomapCopy = true;
89 pCurrentConf->m_vRuns.push_back(x: NewRun);
90 int RunId = pCurrentConf->m_vRuns.size() - 1;
91 pCurrentRun = &pCurrentConf->m_vRuns[RunId];
92 }
93 else if(str_startswith(str: pLine, prefix: "NewRun") && pCurrentConf)
94 {
95 // add new run
96 CRun NewRun;
97 NewRun.m_AutomapCopy = true;
98 pCurrentConf->m_vRuns.push_back(x: NewRun);
99 int RunId = pCurrentConf->m_vRuns.size() - 1;
100 pCurrentRun = &pCurrentConf->m_vRuns[RunId];
101 }
102 else if(str_startswith(str: pLine, prefix: "Index") && pCurrentRun)
103 {
104 // new index
105 int Id = 0;
106 char aOrientation1[128] = "";
107 char aOrientation2[128] = "";
108 char aOrientation3[128] = "";
109
110 sscanf(s: pLine, format: "Index %d %127s %127s %127s", &Id, aOrientation1, aOrientation2, aOrientation3);
111
112 CIndexRule NewIndexRule;
113 NewIndexRule.m_Id = Id;
114 NewIndexRule.m_Flag = 0;
115 NewIndexRule.m_RandomProbability = 1.0f;
116 NewIndexRule.m_DefaultRule = true;
117 NewIndexRule.m_SkipEmpty = false;
118 NewIndexRule.m_SkipFull = false;
119
120 if(str_length(str: aOrientation1) > 0)
121 {
122 if(!str_comp(a: aOrientation1, b: "XFLIP"))
123 NewIndexRule.m_Flag |= TILEFLAG_XFLIP;
124 else if(!str_comp(a: aOrientation1, b: "YFLIP"))
125 NewIndexRule.m_Flag |= TILEFLAG_YFLIP;
126 else if(!str_comp(a: aOrientation1, b: "ROTATE"))
127 NewIndexRule.m_Flag |= TILEFLAG_ROTATE;
128 }
129
130 if(str_length(str: aOrientation2) > 0)
131 {
132 if(!str_comp(a: aOrientation2, b: "XFLIP"))
133 NewIndexRule.m_Flag |= TILEFLAG_XFLIP;
134 else if(!str_comp(a: aOrientation2, b: "YFLIP"))
135 NewIndexRule.m_Flag |= TILEFLAG_YFLIP;
136 else if(!str_comp(a: aOrientation2, b: "ROTATE"))
137 NewIndexRule.m_Flag |= TILEFLAG_ROTATE;
138 }
139
140 if(str_length(str: aOrientation3) > 0)
141 {
142 if(!str_comp(a: aOrientation3, b: "XFLIP"))
143 NewIndexRule.m_Flag |= TILEFLAG_XFLIP;
144 else if(!str_comp(a: aOrientation3, b: "YFLIP"))
145 NewIndexRule.m_Flag |= TILEFLAG_YFLIP;
146 else if(!str_comp(a: aOrientation3, b: "ROTATE"))
147 NewIndexRule.m_Flag |= TILEFLAG_ROTATE;
148 }
149
150 // add the index rule object and make it current
151 pCurrentRun->m_vIndexRules.push_back(x: NewIndexRule);
152 int IndexRuleId = pCurrentRun->m_vIndexRules.size() - 1;
153 pCurrentIndex = &pCurrentRun->m_vIndexRules[IndexRuleId];
154 }
155 else if(str_startswith(str: pLine, prefix: "Pos") && pCurrentIndex)
156 {
157 int x = 0, y = 0;
158 char aValue[128];
159 int Value = CPosRule::NORULE;
160 std::vector<CIndexInfo> vNewIndexList;
161
162 sscanf(s: pLine, format: "Pos %d %d %127s", &x, &y, aValue);
163
164 if(!str_comp(a: aValue, b: "EMPTY"))
165 {
166 Value = CPosRule::INDEX;
167 CIndexInfo NewIndexInfo = {.m_Id: 0, .m_Flag: 0, .m_TestFlag: false};
168 vNewIndexList.push_back(x: NewIndexInfo);
169 }
170 else if(!str_comp(a: aValue, b: "FULL"))
171 {
172 Value = CPosRule::NOTINDEX;
173 CIndexInfo NewIndexInfo1 = {.m_Id: 0, .m_Flag: 0, .m_TestFlag: false};
174 // CIndexInfo NewIndexInfo2 = {-1, 0};
175 vNewIndexList.push_back(x: NewIndexInfo1);
176 // vNewIndexList.push_back(NewIndexInfo2);
177 }
178 else if(!str_comp(a: aValue, b: "INDEX") || !str_comp(a: aValue, b: "NOTINDEX"))
179 {
180 if(!str_comp(a: aValue, b: "INDEX"))
181 Value = CPosRule::INDEX;
182 else
183 Value = CPosRule::NOTINDEX;
184
185 int pWord = 4;
186 while(true)
187 {
188 int Id = 0;
189 char aOrientation1[128] = "";
190 char aOrientation2[128] = "";
191 char aOrientation3[128] = "";
192 char aOrientation4[128] = "";
193 sscanf(s: str_trim_words(str: pLine, words: pWord), format: "%d %127s %127s %127s %127s", &Id, aOrientation1, aOrientation2, aOrientation3, aOrientation4);
194
195 CIndexInfo NewIndexInfo;
196 NewIndexInfo.m_Id = Id;
197 NewIndexInfo.m_Flag = 0;
198 NewIndexInfo.m_TestFlag = false;
199
200 if(!str_comp(a: aOrientation1, b: "OR"))
201 {
202 vNewIndexList.push_back(x: NewIndexInfo);
203 pWord += 2;
204 continue;
205 }
206 else if(str_length(str: aOrientation1) > 0)
207 {
208 NewIndexInfo.m_TestFlag = true;
209 if(!str_comp(a: aOrientation1, b: "XFLIP"))
210 NewIndexInfo.m_Flag = TILEFLAG_XFLIP;
211 else if(!str_comp(a: aOrientation1, b: "YFLIP"))
212 NewIndexInfo.m_Flag = TILEFLAG_YFLIP;
213 else if(!str_comp(a: aOrientation1, b: "ROTATE"))
214 NewIndexInfo.m_Flag = TILEFLAG_ROTATE;
215 else if(!str_comp(a: aOrientation1, b: "NONE"))
216 NewIndexInfo.m_Flag = 0;
217 else
218 NewIndexInfo.m_TestFlag = false;
219 }
220 else
221 {
222 vNewIndexList.push_back(x: NewIndexInfo);
223 break;
224 }
225
226 if(!str_comp(a: aOrientation2, b: "OR"))
227 {
228 vNewIndexList.push_back(x: NewIndexInfo);
229 pWord += 3;
230 continue;
231 }
232 else if(str_length(str: aOrientation2) > 0 && NewIndexInfo.m_Flag != 0)
233 {
234 if(!str_comp(a: aOrientation2, b: "XFLIP"))
235 NewIndexInfo.m_Flag |= TILEFLAG_XFLIP;
236 else if(!str_comp(a: aOrientation2, b: "YFLIP"))
237 NewIndexInfo.m_Flag |= TILEFLAG_YFLIP;
238 else if(!str_comp(a: aOrientation2, b: "ROTATE"))
239 NewIndexInfo.m_Flag |= TILEFLAG_ROTATE;
240 }
241 else
242 {
243 vNewIndexList.push_back(x: NewIndexInfo);
244 break;
245 }
246
247 if(!str_comp(a: aOrientation3, b: "OR"))
248 {
249 vNewIndexList.push_back(x: NewIndexInfo);
250 pWord += 4;
251 continue;
252 }
253 else if(str_length(str: aOrientation3) > 0 && NewIndexInfo.m_Flag != 0)
254 {
255 if(!str_comp(a: aOrientation3, b: "XFLIP"))
256 NewIndexInfo.m_Flag |= TILEFLAG_XFLIP;
257 else if(!str_comp(a: aOrientation3, b: "YFLIP"))
258 NewIndexInfo.m_Flag |= TILEFLAG_YFLIP;
259 else if(!str_comp(a: aOrientation3, b: "ROTATE"))
260 NewIndexInfo.m_Flag |= TILEFLAG_ROTATE;
261 }
262 else
263 {
264 vNewIndexList.push_back(x: NewIndexInfo);
265 break;
266 }
267
268 if(!str_comp(a: aOrientation4, b: "OR"))
269 {
270 vNewIndexList.push_back(x: NewIndexInfo);
271 pWord += 5;
272 continue;
273 }
274 else
275 {
276 vNewIndexList.push_back(x: NewIndexInfo);
277 break;
278 }
279 }
280 }
281
282 if(Value != CPosRule::NORULE)
283 {
284 CPosRule NewPosRule = {.m_X: x, .m_Y: y, .m_Value: Value, .m_vIndexList: vNewIndexList};
285 pCurrentIndex->m_vRules.push_back(x: NewPosRule);
286
287 pCurrentConf->m_StartX = minimum(a: pCurrentConf->m_StartX, b: NewPosRule.m_X);
288 pCurrentConf->m_StartY = minimum(a: pCurrentConf->m_StartY, b: NewPosRule.m_Y);
289 pCurrentConf->m_EndX = maximum(a: pCurrentConf->m_EndX, b: NewPosRule.m_X);
290 pCurrentConf->m_EndY = maximum(a: pCurrentConf->m_EndY, b: NewPosRule.m_Y);
291
292 if(x == 0 && y == 0)
293 {
294 for(const auto &Index : vNewIndexList)
295 {
296 if(Value == CPosRule::INDEX && Index.m_Id == 0)
297 pCurrentIndex->m_SkipFull = true;
298 else
299 pCurrentIndex->m_SkipEmpty = true;
300 }
301 }
302 }
303 }
304 else if(str_startswith(str: pLine, prefix: "Random") && pCurrentIndex)
305 {
306 float Value;
307 char Specifier = ' ';
308 sscanf(s: pLine, format: "Random %f%c", &Value, &Specifier);
309 if(Specifier == '%')
310 {
311 pCurrentIndex->m_RandomProbability = Value / 100.0f;
312 }
313 else
314 {
315 pCurrentIndex->m_RandomProbability = 1.0f / Value;
316 }
317 }
318 else if(str_startswith(str: pLine, prefix: "NoDefaultRule") && pCurrentIndex)
319 {
320 pCurrentIndex->m_DefaultRule = false;
321 }
322 else if(str_startswith(str: pLine, prefix: "NoLayerCopy") && pCurrentRun)
323 {
324 pCurrentRun->m_AutomapCopy = false;
325 }
326 }
327 }
328
329 // add default rule for Pos 0 0 if there is none
330 for(auto &Config : m_vConfigs)
331 {
332 for(auto &Run : Config.m_vRuns)
333 {
334 for(auto &IndexRule : Run.m_vIndexRules)
335 {
336 bool Found = false;
337 for(const auto &Rule : IndexRule.m_vRules)
338 {
339 if(Rule.m_X == 0 && Rule.m_Y == 0)
340 {
341 Found = true;
342 break;
343 }
344 }
345 if(!Found && IndexRule.m_DefaultRule)
346 {
347 std::vector<CIndexInfo> vNewIndexList;
348 CIndexInfo NewIndexInfo = {.m_Id: 0, .m_Flag: 0, .m_TestFlag: false};
349 vNewIndexList.push_back(x: NewIndexInfo);
350 CPosRule NewPosRule = {.m_X: 0, .m_Y: 0, .m_Value: CPosRule::NOTINDEX, .m_vIndexList: vNewIndexList};
351 IndexRule.m_vRules.push_back(x: NewPosRule);
352
353 IndexRule.m_SkipEmpty = true;
354 IndexRule.m_SkipFull = false;
355 }
356 if(IndexRule.m_SkipEmpty && IndexRule.m_SkipFull)
357 {
358 IndexRule.m_SkipEmpty = false;
359 IndexRule.m_SkipFull = false;
360 }
361 }
362 }
363 }
364
365 char aBuf[IO_MAX_PATH_LENGTH + 16];
366 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "loaded %s", aPath);
367 Console()->Print(Level: IConsole::OUTPUT_LEVEL_DEBUG, pFrom: "editor/automap", pStr: aBuf);
368
369 m_FileLoaded = true;
370}
371
372const char *CAutoMapper::GetConfigName(int Index)
373{
374 if(Index < 0 || Index >= (int)m_vConfigs.size())
375 return "";
376
377 return m_vConfigs[Index].m_aName;
378}
379
380void CAutoMapper::ProceedLocalized(CLayerTiles *pLayer, int ConfigId, int Seed, int X, int Y, int Width, int Height)
381{
382 if(!m_FileLoaded || pLayer->m_Readonly || ConfigId < 0 || ConfigId >= (int)m_vConfigs.size())
383 return;
384
385 if(Width < 0)
386 Width = pLayer->m_Width;
387
388 if(Height < 0)
389 Height = pLayer->m_Height;
390
391 CConfiguration *pConf = &m_vConfigs[ConfigId];
392
393 int CommitFromX = clamp(val: X + pConf->m_StartX, lo: 0, hi: pLayer->m_Width);
394 int CommitFromY = clamp(val: Y + pConf->m_StartY, lo: 0, hi: pLayer->m_Height);
395 int CommitToX = clamp(val: X + Width + pConf->m_EndX, lo: 0, hi: pLayer->m_Width);
396 int CommitToY = clamp(val: Y + Height + pConf->m_EndY, lo: 0, hi: pLayer->m_Height);
397
398 int UpdateFromX = clamp(val: X + 3 * pConf->m_StartX, lo: 0, hi: pLayer->m_Width);
399 int UpdateFromY = clamp(val: Y + 3 * pConf->m_StartY, lo: 0, hi: pLayer->m_Height);
400 int UpdateToX = clamp(val: X + Width + 3 * pConf->m_EndX, lo: 0, hi: pLayer->m_Width);
401 int UpdateToY = clamp(val: Y + Height + 3 * pConf->m_EndY, lo: 0, hi: pLayer->m_Height);
402
403 CLayerTiles *pUpdateLayer = new CLayerTiles(Editor(), UpdateToX - UpdateFromX, UpdateToY - UpdateFromY);
404
405 for(int y = UpdateFromY; y < UpdateToY; y++)
406 {
407 for(int x = UpdateFromX; x < UpdateToX; x++)
408 {
409 CTile *pIn = &pLayer->m_pTiles[y * pLayer->m_Width + x];
410 CTile *pOut = &pUpdateLayer->m_pTiles[(y - UpdateFromY) * pUpdateLayer->m_Width + x - UpdateFromX];
411 pOut->m_Index = pIn->m_Index;
412 pOut->m_Flags = pIn->m_Flags;
413 }
414 }
415
416 Proceed(pLayer: pUpdateLayer, ConfigId, Seed, SeedOffsetX: UpdateFromX, SeedOffsetY: UpdateFromY);
417
418 for(int y = CommitFromY; y < CommitToY; y++)
419 {
420 for(int x = CommitFromX; x < CommitToX; x++)
421 {
422 CTile *pIn = &pUpdateLayer->m_pTiles[(y - UpdateFromY) * pUpdateLayer->m_Width + x - UpdateFromX];
423 CTile *pOut = &pLayer->m_pTiles[y * pLayer->m_Width + x];
424 CTile Previous = *pOut;
425 pOut->m_Index = pIn->m_Index;
426 pOut->m_Flags = pIn->m_Flags;
427 pLayer->RecordStateChange(x, y, Previous, Tile: *pOut);
428 }
429 }
430
431 delete pUpdateLayer;
432}
433
434void CAutoMapper::Proceed(CLayerTiles *pLayer, int ConfigId, int Seed, int SeedOffsetX, int SeedOffsetY)
435{
436 if(!m_FileLoaded || pLayer->m_Readonly || ConfigId < 0 || ConfigId >= (int)m_vConfigs.size())
437 return;
438
439 if(Seed == 0)
440 Seed = rand();
441
442 CConfiguration *pConf = &m_vConfigs[ConfigId];
443 pLayer->ClearHistory();
444
445 // for every run: copy tiles, automap, overwrite tiles
446 for(size_t h = 0; h < pConf->m_vRuns.size(); ++h)
447 {
448 CRun *pRun = &pConf->m_vRuns[h];
449
450 // don't make copy if it's requested
451 CLayerTiles *pReadLayer;
452 if(pRun->m_AutomapCopy)
453 {
454 pReadLayer = new CLayerTiles(Editor(), pLayer->m_Width, pLayer->m_Height);
455
456 for(int y = 0; y < pLayer->m_Height; y++)
457 {
458 for(int x = 0; x < pLayer->m_Width; x++)
459 {
460 CTile *pIn = &pLayer->m_pTiles[y * pLayer->m_Width + x];
461 CTile *pOut = &pReadLayer->m_pTiles[y * pLayer->m_Width + x];
462 pOut->m_Index = pIn->m_Index;
463 pOut->m_Flags = pIn->m_Flags;
464 }
465 }
466 }
467 else
468 {
469 pReadLayer = pLayer;
470 }
471
472 // auto map
473 for(int y = 0; y < pLayer->m_Height; y++)
474 {
475 for(int x = 0; x < pLayer->m_Width; x++)
476 {
477 CTile *pTile = &(pLayer->m_pTiles[y * pLayer->m_Width + x]);
478 Editor()->m_Map.OnModify();
479
480 for(size_t i = 0; i < pRun->m_vIndexRules.size(); ++i)
481 {
482 CIndexRule *pIndexRule = &pRun->m_vIndexRules[i];
483 if(pIndexRule->m_SkipEmpty && pTile->m_Index == 0) // skip empty tiles
484 continue;
485 if(pIndexRule->m_SkipFull && pTile->m_Index != 0) // skip full tiles
486 continue;
487
488 bool RespectRules = true;
489 for(size_t j = 0; j < pIndexRule->m_vRules.size() && RespectRules; ++j)
490 {
491 CPosRule *pRule = &pIndexRule->m_vRules[j];
492
493 int CheckIndex, CheckFlags;
494 int CheckX = x + pRule->m_X;
495 int CheckY = y + pRule->m_Y;
496 if(CheckX >= 0 && CheckX < pLayer->m_Width && CheckY >= 0 && CheckY < pLayer->m_Height)
497 {
498 int CheckTile = CheckY * pLayer->m_Width + CheckX;
499 CheckIndex = pReadLayer->m_pTiles[CheckTile].m_Index;
500 CheckFlags = pReadLayer->m_pTiles[CheckTile].m_Flags & (TILEFLAG_ROTATE | TILEFLAG_XFLIP | TILEFLAG_YFLIP);
501 }
502 else
503 {
504 CheckIndex = -1;
505 CheckFlags = 0;
506 }
507
508 if(pRule->m_Value == CPosRule::INDEX)
509 {
510 RespectRules = false;
511 for(const auto &Index : pRule->m_vIndexList)
512 {
513 if(CheckIndex == Index.m_Id && (!Index.m_TestFlag || CheckFlags == Index.m_Flag))
514 {
515 RespectRules = true;
516 break;
517 }
518 }
519 }
520 else if(pRule->m_Value == CPosRule::NOTINDEX)
521 {
522 for(const auto &Index : pRule->m_vIndexList)
523 {
524 if(CheckIndex == Index.m_Id && (!Index.m_TestFlag || CheckFlags == Index.m_Flag))
525 {
526 RespectRules = false;
527 break;
528 }
529 }
530 }
531 }
532
533 if(RespectRules &&
534 (pIndexRule->m_RandomProbability >= 1.0f || HashLocation(Seed, Run: h, Rule: i, X: x + SeedOffsetX, Y: y + SeedOffsetY) < HASH_MAX * pIndexRule->m_RandomProbability))
535 {
536 CTile Previous = *pTile;
537 pTile->m_Index = pIndexRule->m_Id;
538 pTile->m_Flags = pIndexRule->m_Flag;
539 pLayer->RecordStateChange(x, y, Previous, Tile: *pTile);
540 }
541 }
542 }
543 }
544
545 // clean-up
546 if(pRun->m_AutomapCopy && pReadLayer != pLayer)
547 delete pReadLayer;
548 }
549}
550