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