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