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