| 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 |
| 17 | static 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 | |
| 32 | static 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 | |
| 45 | CAutoMapper::CAutoMapper(CEditorMap *pMap) : |
| 46 | CMapObject(pMap) |
| 47 | { |
| 48 | } |
| 49 | |
| 50 | void 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 | |
| 362 | void CAutoMapper::Unload() |
| 363 | { |
| 364 | m_FileLoaded = false; |
| 365 | m_vConfigs.clear(); |
| 366 | } |
| 367 | |
| 368 | int 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 | |
| 382 | const 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 | |
| 391 | void 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 | |
| 459 | void 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 | |