/
dieoska
/
ddnet
Обзор
Документация
Войти
/
dieoska
/
ddnet
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/game/editor/auto_map.cpp
610 строк
18 KB
Robert Müller
Cleanup editor includes
01 дек 2025, 01:16
01 дек 2025, 01:16
516b6bc
Код
Авторство
О чём код?
#include "auto_map.h" #include <base/log.h> #include <engine/shared/linereader.h> #include <engine/storage.h> #include <game/editor/editor_actions.h> #include <game/editor/mapitems/layer_tiles.h> #include <game/editor/mapitems/map.h> #include <game/mapitems.h> #include <cinttypes> #include <cstdio> // sscanf // Based on triple32inc from https://github.com/skeeto/hash-prospector/tree/79a6074062a84907df6e45b756134b74e2956760 static uint32_t HashUInt32(uint32_t Num) { Num++; Num ^= Num >> 17; Num *= 0xed5ad4bbu; Num ^= Num >> 11; Num *= 0xac4c1b51u; Num ^= Num >> 15; Num *= 0x31848babu; Num ^= Num >> 14; return Num; } #define HASH_MAX 65536 static int HashLocation(uint32_t Seed, uint32_t Run, uint32_t Rule, uint32_t X, uint32_t Y) { const uint32_t Prime = 31; uint32_t Hash = 1; Hash = Hash * Prime + HashUInt32(Seed); Hash = Hash * Prime + HashUInt32(Run); Hash = Hash * Prime + HashUInt32(Rule); Hash = Hash * Prime + HashUInt32(X); Hash = Hash * Prime + HashUInt32(Y); Hash = HashUInt32(Hash * Prime); // Just to double-check that values are well-distributed return Hash % HASH_MAX; } CAutoMapper::CAutoMapper(CEditorMap *pMap) : CMapObject(pMap) { } void CAutoMapper::Load(const char *pTileName) { char aPath[IO_MAX_PATH_LENGTH]; str_format(aPath, sizeof(aPath), "editor/automap/%s.rules", pTileName); if(!Storage()->FileExists(aPath, IStorage::TYPE_ALL)) { return; // Avoid error message if no rules exist } CLineReader LineReader; if(!LineReader.OpenFile(Storage()->OpenFile(aPath, IOFLAG_READ, IStorage::TYPE_ALL))) { log_error("editor/automap", "Failed to load rules from '%s'", aPath); return; } CConfiguration *pCurrentConf = nullptr; CRun *pCurrentRun = nullptr; CIndexRule *pCurrentIndex = nullptr; // read each line while(const char *pLine = LineReader.Get()) { // skip blank/empty lines as well as comments if(str_length(pLine) > 0 && pLine[0] != '#' && pLine[0] != '\n' && pLine[0] != '\r' && pLine[0] != '\t' && pLine[0] != '\v' && pLine[0] != ' ') { if(pLine[0] == '[') { // new configuration, get the name pLine++; CConfiguration NewConf; NewConf.m_aName[0] = '\0'; NewConf.m_StartX = 0; NewConf.m_StartY = 0; NewConf.m_EndX = 0; NewConf.m_EndY = 0; m_vConfigs.push_back(NewConf); int ConfigurationId = m_vConfigs.size() - 1; pCurrentConf = &m_vConfigs[ConfigurationId]; str_copy(pCurrentConf->m_aName, pLine, minimum<int>(sizeof(pCurrentConf->m_aName), str_length(pLine))); // add start run CRun NewRun; NewRun.m_AutomapCopy = true; pCurrentConf->m_vRuns.push_back(NewRun); int RunId = pCurrentConf->m_vRuns.size() - 1; pCurrentRun = &pCurrentConf->m_vRuns[RunId]; } else if(str_startswith(pLine, "NewRun") && pCurrentConf) { // add new run CRun NewRun; NewRun.m_AutomapCopy = true; pCurrentConf->m_vRuns.push_back(NewRun); int RunId = pCurrentConf->m_vRuns.size() - 1; pCurrentRun = &pCurrentConf->m_vRuns[RunId]; } else if(str_startswith(pLine, "Index") && pCurrentRun) { // new index CIndexRule NewIndexRule; char aOrientation1[128] = ""; char aOrientation2[128] = ""; char aOrientation3[128] = ""; sscanf(pLine, "Index %d %127s %127s %127s", &NewIndexRule.m_Id, aOrientation1, aOrientation2, aOrientation3); NewIndexRule.m_Flag = 0; NewIndexRule.m_RandomProbability = 1.0f; NewIndexRule.m_DefaultRule = true; NewIndexRule.m_SkipEmpty = false; NewIndexRule.m_SkipFull = false; if(str_length(aOrientation1) > 0) NewIndexRule.m_Flag = CheckIndexFlag(NewIndexRule.m_Flag, aOrientation1, false); if(str_length(aOrientation2) > 0) NewIndexRule.m_Flag = CheckIndexFlag(NewIndexRule.m_Flag, aOrientation2, false); if(str_length(aOrientation3) > 0) NewIndexRule.m_Flag = CheckIndexFlag(NewIndexRule.m_Flag, aOrientation3, false); // add the index rule object and make it current pCurrentRun->m_vIndexRules.push_back(NewIndexRule); int IndexRuleId = pCurrentRun->m_vIndexRules.size() - 1; pCurrentIndex = &pCurrentRun->m_vIndexRules[IndexRuleId]; } else if(str_startswith(pLine, "Pos") && pCurrentIndex) { int x = 0, y = 0; char aValue[128]; int Value = CPosRule::NORULE; std::vector<CIndexInfo> vNewIndexList; sscanf(pLine, "Pos %d %d %127s", &x, &y, aValue); if(!str_comp(aValue, "EMPTY")) { Value = CPosRule::INDEX; CIndexInfo NewIndexInfo = {0, 0, false}; vNewIndexList.push_back(NewIndexInfo); } else if(!str_comp(aValue, "FULL")) { Value = CPosRule::NOTINDEX; CIndexInfo NewIndexInfo1 = {0, 0, false}; // CIndexInfo NewIndexInfo2 = {-1, 0}; vNewIndexList.push_back(NewIndexInfo1); // vNewIndexList.push_back(NewIndexInfo2); } else if(!str_comp(aValue, "INDEX") || !str_comp(aValue, "NOTINDEX")) { if(!str_comp(aValue, "INDEX")) Value = CPosRule::INDEX; else Value = CPosRule::NOTINDEX; int pWord = 4; while(true) { CIndexInfo NewIndexInfo; char aOrientation1[128] = ""; char aOrientation2[128] = ""; char aOrientation3[128] = ""; char aOrientation4[128] = ""; sscanf(str_trim_words(pLine, pWord), "%d %127s %127s %127s %127s", &NewIndexInfo.m_Id, aOrientation1, aOrientation2, aOrientation3, aOrientation4); NewIndexInfo.m_Flag = 0; NewIndexInfo.m_TestFlag = false; if(!str_comp(aOrientation1, "OR")) { vNewIndexList.push_back(NewIndexInfo); pWord += 2; continue; } else if(str_length(aOrientation1) > 0) { NewIndexInfo.m_Flag = CheckIndexFlag(NewIndexInfo.m_Flag, aOrientation1, true); NewIndexInfo.m_TestFlag = !(NewIndexInfo.m_Flag == 0 && str_comp(aOrientation1, "NONE")); } else { vNewIndexList.push_back(NewIndexInfo); break; } if(!str_comp(aOrientation2, "OR")) { vNewIndexList.push_back(NewIndexInfo); pWord += 3; continue; } else if(str_length(aOrientation2) > 0 && NewIndexInfo.m_Flag != 0) { NewIndexInfo.m_Flag = CheckIndexFlag(NewIndexInfo.m_Flag, aOrientation2, false); } else { vNewIndexList.push_back(NewIndexInfo); break; } if(!str_comp(aOrientation3, "OR")) { vNewIndexList.push_back(NewIndexInfo); pWord += 4; continue; } else if(str_length(aOrientation3) > 0 && NewIndexInfo.m_Flag != 0) { NewIndexInfo.m_Flag = CheckIndexFlag(NewIndexInfo.m_Flag, aOrientation3, false); } else { vNewIndexList.push_back(NewIndexInfo); break; } if(!str_comp(aOrientation4, "OR")) { vNewIndexList.push_back(NewIndexInfo); pWord += 5; continue; } else { vNewIndexList.push_back(NewIndexInfo); break; } } } if(Value != CPosRule::NORULE) { CPosRule NewPosRule = {x, y, Value, vNewIndexList}; pCurrentIndex->m_vRules.push_back(NewPosRule); pCurrentConf->m_StartX = minimum(pCurrentConf->m_StartX, NewPosRule.m_X); pCurrentConf->m_StartY = minimum(pCurrentConf->m_StartY, NewPosRule.m_Y); pCurrentConf->m_EndX = maximum(pCurrentConf->m_EndX, NewPosRule.m_X); pCurrentConf->m_EndY = maximum(pCurrentConf->m_EndY, NewPosRule.m_Y); if(x == 0 && y == 0) { for(const auto &Index : vNewIndexList) { if(Index.m_Id == 0 && Value == CPosRule::INDEX) { // Skip full tiles if we have a rule "POS 0 0 INDEX 0" // because that forces the tile to be empty pCurrentIndex->m_SkipFull = true; } else if((Index.m_Id > 0 && Value == CPosRule::INDEX) || (Index.m_Id == 0 && Value == CPosRule::NOTINDEX)) { // Skip empty tiles if we have a rule "POS 0 0 INDEX i" where i > 0 // or if we have a rule "POS 0 0 NOTINDEX 0" pCurrentIndex->m_SkipEmpty = true; } } } } } else if(str_startswith(pLine, "Random") && pCurrentIndex) { float Value; char Specifier = ' '; sscanf(pLine, "Random %f%c", &Value, &Specifier); if(Specifier == '%') { pCurrentIndex->m_RandomProbability = Value / 100.0f; } else { pCurrentIndex->m_RandomProbability = 1.0f / Value; } } else if(str_startswith(pLine, "Modulo") && pCurrentIndex) { CModuloRule NewModuloRule; sscanf(pLine, "Modulo %d %d %d %d", &NewModuloRule.m_ModX, &NewModuloRule.m_ModY, &NewModuloRule.m_OffsetX, &NewModuloRule.m_OffsetY); if(NewModuloRule.m_ModX == 0) NewModuloRule.m_ModX = 1; if(NewModuloRule.m_ModY == 0) NewModuloRule.m_ModY = 1; pCurrentIndex->m_vModuloRules.push_back(NewModuloRule); } else if(str_startswith(pLine, "NoDefaultRule") && pCurrentIndex) { pCurrentIndex->m_DefaultRule = false; } else if(str_startswith(pLine, "NoLayerCopy") && pCurrentRun) { pCurrentRun->m_AutomapCopy = false; } } } // add default rule for Pos 0 0 if there is none for(auto &Config : m_vConfigs) { for(auto &Run : Config.m_vRuns) { for(auto &IndexRule : Run.m_vIndexRules) { bool Found = false; // Search for the exact rule "POS 0 0 INDEX 0" which corresponds to the default rule for(const auto &Rule : IndexRule.m_vRules) { if(Rule.m_X == 0 && Rule.m_Y == 0 && Rule.m_Value == CPosRule::INDEX) { for(const auto &Index : Rule.m_vIndexList) { if(Index.m_Id == 0) Found = true; } break; } if(Found) break; } // If the default rule was not found, and we require it, then add it if(!Found && IndexRule.m_DefaultRule) { std::vector<CIndexInfo> vNewIndexList; CIndexInfo NewIndexInfo = {0, 0, false}; vNewIndexList.push_back(NewIndexInfo); CPosRule NewPosRule = {0, 0, CPosRule::NOTINDEX, vNewIndexList}; IndexRule.m_vRules.push_back(NewPosRule); IndexRule.m_SkipEmpty = true; IndexRule.m_SkipFull = false; } if(IndexRule.m_SkipEmpty && IndexRule.m_SkipFull) { IndexRule.m_SkipEmpty = false; IndexRule.m_SkipFull = false; } } } } log_trace("editor/automap", "Loaded '%s'", aPath); m_FileLoaded = true; } void CAutoMapper::Unload() { m_FileLoaded = false; m_vConfigs.clear(); } int CAutoMapper::CheckIndexFlag(int Flag, const char *pFlag, bool CheckNone) const { if(!str_comp(pFlag, "XFLIP")) Flag |= TILEFLAG_XFLIP; else if(!str_comp(pFlag, "YFLIP")) Flag |= TILEFLAG_YFLIP; else if(!str_comp(pFlag, "ROTATE")) Flag |= TILEFLAG_ROTATE; else if(!str_comp(pFlag, "NONE") && CheckNone) Flag = 0; return Flag; } const char *CAutoMapper::GetConfigName(int Index) const { if(Index < 0 || Index >= (int)m_vConfigs.size()) { return "(unknown)"; } return m_vConfigs[Index].m_aName; } void CAutoMapper::ProceedLocalized(CLayerTiles *pLayer, CLayerTiles *pGameLayer, int ReferenceId, int ConfigId, int Seed, int X, int Y, int Width, int Height) { if(!m_FileLoaded || pLayer->m_Readonly || ConfigId < 0 || ConfigId >= (int)m_vConfigs.size()) return; if(Width < 0) Width = pLayer->m_Width; if(Height < 0) Height = pLayer->m_Height; CConfiguration *pConf = &m_vConfigs[ConfigId]; int CommitFromX = std::clamp(X + pConf->m_StartX, 0, pLayer->m_Width); int CommitFromY = std::clamp(Y + pConf->m_StartY, 0, pLayer->m_Height); int CommitToX = std::clamp(X + Width + pConf->m_EndX, 0, pLayer->m_Width); int CommitToY = std::clamp(Y + Height + pConf->m_EndY, 0, pLayer->m_Height); int UpdateFromX = std::clamp(X + 3 * pConf->m_StartX, 0, pLayer->m_Width); int UpdateFromY = std::clamp(Y + 3 * pConf->m_StartY, 0, pLayer->m_Height); int UpdateToX = std::clamp(X + Width + 3 * pConf->m_EndX, 0, pLayer->m_Width); int UpdateToY = std::clamp(Y + Height + 3 * pConf->m_EndY, 0, pLayer->m_Height); CLayerTiles *pUpdateLayer = new CLayerTiles(pLayer->Map(), UpdateToX - UpdateFromX, UpdateToY - UpdateFromY); CLayerTiles *pUpdateGame = new CLayerTiles(pLayer->Map(), UpdateToX - UpdateFromX, UpdateToY - UpdateFromY); for(int y = UpdateFromY; y < UpdateToY; y++) { for(int x = UpdateFromX; x < UpdateToX; x++) { const CTile *pInLayer = &pLayer->m_pTiles[y * pLayer->m_Width + x]; CTile *pOutLayer = &pUpdateLayer->m_pTiles[(y - UpdateFromY) * pUpdateLayer->m_Width + x - UpdateFromX]; pOutLayer->m_Index = pInLayer->m_Index; pOutLayer->m_Flags = pInLayer->m_Flags; const CTile *pInGame = &pGameLayer->m_pTiles[y * pGameLayer->m_Width + x]; CTile *pOutGame = &pUpdateGame->m_pTiles[(y - UpdateFromY) * pUpdateGame->m_Width + x - UpdateFromX]; pOutGame->m_Index = pInGame->m_Index; pOutGame->m_Flags = pInGame->m_Flags; } } Proceed(pUpdateLayer, pUpdateGame, ReferenceId, ConfigId, Seed, UpdateFromX, UpdateFromY); for(int y = CommitFromY; y < CommitToY; y++) { for(int x = CommitFromX; x < CommitToX; x++) { const CTile *pInLayer = &pUpdateLayer->m_pTiles[(y - UpdateFromY) * pUpdateLayer->m_Width + x - UpdateFromX]; CTile *pOutLayer = &pLayer->m_pTiles[y * pLayer->m_Width + x]; CTile PreviousLayer = *pOutLayer; pOutLayer->m_Index = pInLayer->m_Index; pOutLayer->m_Flags = pInLayer->m_Flags; pLayer->RecordStateChange(x, y, PreviousLayer, *pOutLayer); const CTile *pInGame = &pUpdateGame->m_pTiles[(y - UpdateFromY) * pUpdateGame->m_Width + x - UpdateFromX]; CTile *pOutGame = &pGameLayer->m_pTiles[y * pGameLayer->m_Width + x]; CTile PreviousGame = *pOutGame; pOutGame->m_Index = pInGame->m_Index; pOutGame->m_Flags = pInGame->m_Flags; pGameLayer->RecordStateChange(x, y, PreviousGame, *pOutGame); } } delete pUpdateLayer; delete pUpdateGame; } void CAutoMapper::Proceed(CLayerTiles *pLayer, CLayerTiles *pGameLayer, int ReferenceId, int ConfigId, int Seed, int SeedOffsetX, int SeedOffsetY) { if(!m_FileLoaded || pLayer->m_Readonly || ConfigId < 0 || ConfigId >= (int)m_vConfigs.size()) return; if(Seed == 0) Seed = rand(); CConfiguration *pConf = &m_vConfigs[ConfigId]; pLayer->ClearHistory(); const int LayerWidth = pLayer->m_Width; const int LayerHeight = pLayer->m_Height; static const int s_aTileIndex[] = {TILE_SOLID, TILE_DEATH, TILE_NOHOOK, TILE_FREEZE, TILE_UNFREEZE, TILE_DFREEZE, TILE_DUNFREEZE, TILE_LFREEZE, TILE_LUNFREEZE}; static_assert(std::size(AUTOMAP_REFERENCE_NAMES) == std::size(s_aTileIndex) + 1, "AUTOMAP_REFERENCE_NAMES and s_aTileIndex must include the same items"); // for every run: copy tiles, automap, overwrite tiles for(size_t h = 0; h < pConf->m_vRuns.size(); ++h) { CRun *pRun = &pConf->m_vRuns[h]; bool IsFilterable = h == 0 && ReferenceId >= 0; // don't make copy if it's requested CLayerTiles *pReadLayer; CLayerTiles *pBuffer = IsFilterable ? pGameLayer : pLayer; if(pRun->m_AutomapCopy) { pReadLayer = new CLayerTiles(pLayer->Map(), LayerWidth, LayerHeight); int LoopWidth = IsFilterable ? std::min(pGameLayer->m_Width, LayerWidth) : LayerWidth; int LoopHeight = IsFilterable ? std::min(pGameLayer->m_Height, LayerHeight) : LayerHeight; for(int y = 0; y < LoopHeight; y++) { for(int x = 0; x < LoopWidth; x++) { const CTile *pIn = &pBuffer->m_pTiles[y * pBuffer->m_Width + x]; CTile *pOut = &pReadLayer->m_pTiles[y * LayerWidth + x]; if(h == 0 && ReferenceId >= 1 && pIn->m_Index != s_aTileIndex[ReferenceId - 1]) pOut->m_Index = 0; else pOut->m_Index = pIn->m_Index; pOut->m_Flags = pIn->m_Flags; } } } else { pReadLayer = pBuffer; } // auto map for(int y = 0; y < LayerHeight; y++) { for(int x = 0; x < LayerWidth; x++) { CTile *pTile = &(pLayer->m_pTiles[y * LayerWidth + x]); const CTile *pReadTile = &(pReadLayer->m_pTiles[y * LayerWidth + x]); pLayer->Map()->OnModify(); for(size_t i = 0; i < pRun->m_vIndexRules.size(); ++i) { CIndexRule *pIndexRule = &pRun->m_vIndexRules[i]; if(pReadTile->m_Index == 0) { if(pTile->m_Index != 0 && IsFilterable) // TODO: This is a lazy workaround { CTile Previous = *pTile; pTile->m_Index = 0; pTile->m_Flags = pIndexRule->m_Flag; pLayer->RecordStateChange(x, y, Previous, *pTile); continue; } if(pIndexRule->m_SkipEmpty) // skip empty tiles continue; } if(pIndexRule->m_SkipFull && pReadTile->m_Index != 0) // skip full tiles continue; bool RespectRules = true; for(size_t j = 0; j < pIndexRule->m_vRules.size() && RespectRules; ++j) { CPosRule *pRule = &pIndexRule->m_vRules[j]; int CheckIndex, CheckFlags; int CheckX = x + pRule->m_X; int CheckY = y + pRule->m_Y; if(CheckX >= 0 && CheckX < LayerWidth && CheckY >= 0 && CheckY < LayerHeight) { int CheckTile = CheckY * LayerWidth + CheckX; CheckIndex = pReadLayer->m_pTiles[CheckTile].m_Index; CheckFlags = pReadLayer->m_pTiles[CheckTile].m_Flags & (TILEFLAG_ROTATE | TILEFLAG_XFLIP | TILEFLAG_YFLIP); } else { CheckIndex = -1; CheckFlags = 0; } if(pRule->m_Value == CPosRule::INDEX) { RespectRules = false; for(const auto &Index : pRule->m_vIndexList) { if(CheckIndex == Index.m_Id && (!Index.m_TestFlag || CheckFlags == Index.m_Flag)) { RespectRules = true; break; } } } else if(pRule->m_Value == CPosRule::NOTINDEX) { for(const auto &Index : pRule->m_vIndexList) { if(CheckIndex == Index.m_Id && (!Index.m_TestFlag || CheckFlags == Index.m_Flag)) { RespectRules = false; break; } } } } bool PassesModuloCheck; if(pIndexRule->m_vModuloRules.empty()) PassesModuloCheck = true; else PassesModuloCheck = std::any_of(pIndexRule->m_vModuloRules.cbegin(), pIndexRule->m_vModuloRules.cend(), [&](const CModuloRule &ModuloRule) { return (x + SeedOffsetX + ModuloRule.m_OffsetX) % ModuloRule.m_ModX == 0 && (y + SeedOffsetY + ModuloRule.m_OffsetY) % ModuloRule.m_ModY == 0; }); if(RespectRules && PassesModuloCheck && (pIndexRule->m_RandomProbability >= 1.0f || HashLocation(Seed, h, i, x + SeedOffsetX, y + SeedOffsetY) < HASH_MAX * pIndexRule->m_RandomProbability)) { CTile Previous = *pTile; pTile->m_Index = pIndexRule->m_Id; pTile->m_Flags = pIndexRule->m_Flag; pLayer->RecordStateChange(x, y, Previous, *pTile); } } } } // clean-up if(pRun->m_AutomapCopy && pReadLayer != pLayer) delete pReadLayer; } }