| 1 | /* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ |
| 2 | /* If you are missing that file, acquire a complete release at teeworlds.com. */ |
| 3 | #include "map.h" |
| 4 | |
| 5 | #include <base/dbg.h> |
| 6 | #include <base/fs.h> |
| 7 | #include <base/log.h> |
| 8 | #include <base/mem.h> |
| 9 | #include <base/str.h> |
| 10 | |
| 11 | #include <engine/storage.h> |
| 12 | |
| 13 | #include <game/gamecore.h> |
| 14 | #include <game/mapitems.h> |
| 15 | |
| 16 | CMap::CMap() = default; |
| 17 | |
| 18 | CMap::~CMap() |
| 19 | { |
| 20 | Unload(); |
| 21 | } |
| 22 | |
| 23 | int CMap::GetDataSize(int Index) const |
| 24 | { |
| 25 | return m_DataFile.GetDataSize(Index); |
| 26 | } |
| 27 | |
| 28 | void *CMap::GetData(int Index) |
| 29 | { |
| 30 | return m_DataFile.GetData(Index); |
| 31 | } |
| 32 | |
| 33 | void *CMap::GetDataSwapped(int Index) |
| 34 | { |
| 35 | return m_DataFile.GetDataSwapped(Index); |
| 36 | } |
| 37 | |
| 38 | const char *CMap::GetDataString(int Index) |
| 39 | { |
| 40 | return m_DataFile.GetDataString(Index); |
| 41 | } |
| 42 | |
| 43 | void CMap::UnloadData(int Index) |
| 44 | { |
| 45 | m_DataFile.UnloadData(Index); |
| 46 | } |
| 47 | |
| 48 | int CMap::NumData() const |
| 49 | { |
| 50 | return m_DataFile.NumData(); |
| 51 | } |
| 52 | |
| 53 | int CMap::GetItemSize(int Index) |
| 54 | { |
| 55 | return m_DataFile.GetItemSize(Index); |
| 56 | } |
| 57 | |
| 58 | void *CMap::GetItem(int Index, int *pType, int *pId, CUuid *pUuid) |
| 59 | { |
| 60 | return m_DataFile.GetItem(Index, pType, pId, pUuid); |
| 61 | } |
| 62 | |
| 63 | void CMap::GetType(int Type, int *pStart, int *pNum) |
| 64 | { |
| 65 | m_DataFile.GetType(Type, pStart, pNum); |
| 66 | } |
| 67 | |
| 68 | int CMap::FindItemIndex(int Type, int Id) |
| 69 | { |
| 70 | return m_DataFile.FindItemIndex(Type, Id); |
| 71 | } |
| 72 | |
| 73 | void *CMap::FindItem(int Type, int Id) |
| 74 | { |
| 75 | return m_DataFile.FindItem(Type, Id); |
| 76 | } |
| 77 | |
| 78 | int CMap::NumItems() const |
| 79 | { |
| 80 | return m_DataFile.NumItems(); |
| 81 | } |
| 82 | |
| 83 | static bool AtMostOneBitSet(int Flags) |
| 84 | { |
| 85 | // https://graphics.stanford.edu/~seander/bithacks.html#DetermineIfPowerOf2 |
| 86 | return (Flags & (Flags - 1)) == 0; |
| 87 | } |
| 88 | |
| 89 | bool CMap::Load(const char *pFullName, IStorage *pStorage, const char *pPath, int StorageType) |
| 90 | { |
| 91 | // Ensure current datafile is not left in an inconsistent state if loading fails, |
| 92 | // by loading the new datafile separately first. |
| 93 | CDataFileReader NewDataFile; |
| 94 | if(!NewDataFile.Open(pFullName, pStorage, pPath, StorageType)) |
| 95 | return false; |
| 96 | |
| 97 | if(!ValidateMapVersion(NewDataFile)) |
| 98 | { |
| 99 | NewDataFile.Close(); |
| 100 | return false; |
| 101 | } |
| 102 | |
| 103 | int GroupsStart, GroupsNum, , LayersNum; |
| 104 | NewDataFile.GetType(Type: MAPITEMTYPE_GROUP, pStart: &GroupsStart, pNum: &GroupsNum); |
| 105 | NewDataFile.GetType(Type: MAPITEMTYPE_LAYER, pStart: &LayersStart, pNum: &LayersNum); |
| 106 | |
| 107 | // Replace map items for old versions with items compatible with latest version to avoid version checks when using the map items. |
| 108 | // Ensure that we have a game layer and game group. |
| 109 | const CMapItemLayerTilemap *pGameLayer = nullptr; |
| 110 | std::set<int> UsedLayerItemIndices; |
| 111 | for(int GroupIndex = 0; GroupIndex < GroupsNum; GroupIndex++) |
| 112 | { |
| 113 | const size_t GroupItemSize = NewDataFile.GetItemSize(Index: GroupsStart + GroupIndex); |
| 114 | if(GroupItemSize < sizeof(CMapItemGroup_v1)) |
| 115 | { |
| 116 | log_error("map/load" , "Group %d is truncated (size %" PRIzu ")." , GroupIndex, GroupItemSize); |
| 117 | return false; |
| 118 | } |
| 119 | const CMapItemGroup *pGroup = static_cast<CMapItemGroup *>(NewDataFile.GetItem(Index: GroupsStart + GroupIndex)); |
| 120 | if(pGroup->m_StartLayer < 0 || pGroup->m_NumLayers < 0 || |
| 121 | (int64_t)pGroup->m_StartLayer + pGroup->m_NumLayers > LayersNum) |
| 122 | { |
| 123 | log_error("map/load" , "Group %d uses invalid layers %d to %d (the map contains %d layers)." , |
| 124 | GroupIndex, pGroup->m_StartLayer, pGroup->m_StartLayer + pGroup->m_NumLayers - 1, LayersNum); |
| 125 | return false; |
| 126 | } |
| 127 | for(int LayerIndex = 0; LayerIndex < pGroup->m_NumLayers; LayerIndex++) |
| 128 | { |
| 129 | const int LayerItemIndex = LayersStart + pGroup->m_StartLayer + LayerIndex; |
| 130 | const auto &[_, LayerUnique] = UsedLayerItemIndices.emplace(args: LayerItemIndex); |
| 131 | if(!LayerUnique) |
| 132 | { |
| 133 | log_error("map/load" , "Layer %d in group %d is also being used by another group." , LayerIndex, GroupIndex); |
| 134 | return false; |
| 135 | } |
| 136 | CMapItemLayer *pLayer = static_cast<CMapItemLayer *>(NewDataFile.GetItem(Index: LayerItemIndex)); |
| 137 | const size_t LayerItemSize = NewDataFile.GetItemSize(Index: LayerItemIndex); |
| 138 | if(LayerItemSize < sizeof(CMapItemLayer)) |
| 139 | { |
| 140 | log_error("map/load" , "Layer %d in group %d is truncated (size %" PRIzu ")." , LayerIndex, GroupIndex, LayerItemSize); |
| 141 | return false; |
| 142 | } |
| 143 | |
| 144 | if(pLayer->m_Version != 0) |
| 145 | { |
| 146 | log_debug("map/load" , "Layer %d in group %d has unused version set to %d. Resetting to 0." , LayerIndex, GroupIndex, pLayer->m_Version); |
| 147 | pLayer->m_Version = 0; |
| 148 | } |
| 149 | |
| 150 | if(pLayer->m_Type == LAYERTYPE_TILES) |
| 151 | { |
| 152 | if(!UpgradeAndValidateTilesLayerItem(NewDataFile, GroupIndex, LayerIndex, pLayerTilemapBase: reinterpret_cast<CMapItemLayerTilemap *>(pLayer), LayerItemIndex, LayerItemSize)) |
| 153 | { |
| 154 | return false; |
| 155 | } |
| 156 | // The item may have been replaced, so the pointer must be determined again. |
| 157 | const CMapItemLayerTilemap *pLayerTilemap = static_cast<CMapItemLayerTilemap *>(NewDataFile.GetItem(Index: LayerItemIndex)); |
| 158 | if(pLayerTilemap->m_Flags & TILESLAYERFLAG_GAME) |
| 159 | { |
| 160 | pGameLayer = pLayerTilemap; |
| 161 | } |
| 162 | } |
| 163 | } |
| 164 | } |
| 165 | if(pGameLayer == nullptr) |
| 166 | { |
| 167 | log_error("map/load" , "Game layer is missing." ); |
| 168 | return false; |
| 169 | } |
| 170 | |
| 171 | // Lazily validate data and replace compressed tile layers with uncompressed ones. |
| 172 | std::set<int> UsedDataIndices; |
| 173 | for(int GroupIndex = 0; GroupIndex < GroupsNum; GroupIndex++) |
| 174 | { |
| 175 | const CMapItemGroup *pGroup = static_cast<CMapItemGroup *>(NewDataFile.GetItem(Index: GroupsStart + GroupIndex)); |
| 176 | for(int LayerIndex = 0; LayerIndex < pGroup->m_NumLayers; LayerIndex++) |
| 177 | { |
| 178 | CMapItemLayer *pLayer = static_cast<CMapItemLayer *>(NewDataFile.GetItem(Index: LayersStart + pGroup->m_StartLayer + LayerIndex)); |
| 179 | if(pLayer->m_Type == LAYERTYPE_TILES) |
| 180 | { |
| 181 | if(!ValidateAndUnpackTilesLayerData(NewDataFile, GroupIndex, LayerIndex, pLayerTilemap: reinterpret_cast<const CMapItemLayerTilemap *>(pLayer), GameLayer: *pGameLayer, UsedDataIndices)) |
| 182 | { |
| 183 | return false; |
| 184 | } |
| 185 | } |
| 186 | } |
| 187 | } |
| 188 | |
| 189 | // Load and implicitly validate game layer tile data immediately because we need it. |
| 190 | // Do not preload other data to avoid excessive memory usage. |
| 191 | if(NewDataFile.GetData(Index: pGameLayer->m_Data) == nullptr) |
| 192 | { |
| 193 | log_error("map/load" , "Game layer data is invalid." ); |
| 194 | return false; |
| 195 | } |
| 196 | |
| 197 | // Replace existing datafile with new datafile |
| 198 | m_DataFile.Close(); |
| 199 | m_DataFile = std::move(NewDataFile); |
| 200 | return true; |
| 201 | } |
| 202 | |
| 203 | bool CMap::Load(IStorage *pStorage, const char *pPath, int StorageType) |
| 204 | { |
| 205 | char aFilename[IO_MAX_PATH_LENGTH]; |
| 206 | fs_split_file_extension(filename: fs_filename(path: pPath), name: aFilename, name_size: sizeof(aFilename)); |
| 207 | return Load(pFullName: aFilename, pStorage, pPath, StorageType); |
| 208 | } |
| 209 | |
| 210 | void CMap::Unload() |
| 211 | { |
| 212 | m_DataFile.Close(); |
| 213 | } |
| 214 | |
| 215 | bool CMap::IsLoaded() const |
| 216 | { |
| 217 | return m_DataFile.IsOpen(); |
| 218 | } |
| 219 | |
| 220 | IOHANDLE CMap::File() const |
| 221 | { |
| 222 | return m_DataFile.File(); |
| 223 | } |
| 224 | |
| 225 | const char *CMap::FullName() const |
| 226 | { |
| 227 | return m_DataFile.FullName(); |
| 228 | } |
| 229 | |
| 230 | const char *CMap::BaseName() const |
| 231 | { |
| 232 | return m_DataFile.BaseName(); |
| 233 | } |
| 234 | |
| 235 | const char *CMap::Path() const |
| 236 | { |
| 237 | return m_DataFile.Path(); |
| 238 | } |
| 239 | |
| 240 | SHA256_DIGEST CMap::Sha256() const |
| 241 | { |
| 242 | return m_DataFile.Sha256(); |
| 243 | } |
| 244 | |
| 245 | unsigned CMap::Crc() const |
| 246 | { |
| 247 | return m_DataFile.Crc(); |
| 248 | } |
| 249 | |
| 250 | int CMap::Size() const |
| 251 | { |
| 252 | return m_DataFile.Size(); |
| 253 | } |
| 254 | |
| 255 | bool CMap::ValidateMapVersion(CDataFileReader &NewDataFile) |
| 256 | { |
| 257 | const int VersionItemIndex = NewDataFile.FindItemIndex(Type: MAPITEMTYPE_VERSION, Id: 0); |
| 258 | if(VersionItemIndex < 0) |
| 259 | { |
| 260 | log_error("map/load" , "Map version item is missing." ); |
| 261 | return false; |
| 262 | } |
| 263 | const size_t VersionItemSize = NewDataFile.GetItemSize(Index: VersionItemIndex); |
| 264 | if(VersionItemSize < sizeof(CMapItemVersion)) |
| 265 | { |
| 266 | log_error("map/load" , "Map version item is truncated (size %" PRIzu ")." , VersionItemSize); |
| 267 | return false; |
| 268 | } |
| 269 | const CMapItemVersion *pVersionItem = static_cast<CMapItemVersion *>(NewDataFile.GetItem(Index: VersionItemIndex)); |
| 270 | if(pVersionItem->m_Version != 1) |
| 271 | { |
| 272 | log_error("map/load" , "Map version %d is not supported." , pVersionItem->m_Version); |
| 273 | return false; |
| 274 | } |
| 275 | return true; |
| 276 | } |
| 277 | |
| 278 | bool CMap::(CTile *pDest, size_t DestSize, const CTile *pSrc, size_t SrcSize) |
| 279 | { |
| 280 | size_t DestIndex = 0; |
| 281 | size_t SrcIndex = 0; |
| 282 | while(DestIndex < DestSize && SrcIndex < SrcSize) |
| 283 | { |
| 284 | if(pSrc[SrcIndex].m_MustBe0 != 0) |
| 285 | { |
| 286 | log_error("map/load" , "Tile layer data contains non-zero padding value %d at index %" PRIzu "." , |
| 287 | pSrc[SrcIndex].m_MustBe0, SrcIndex); |
| 288 | return false; |
| 289 | } |
| 290 | for(unsigned Counter = 0; Counter <= pSrc[SrcIndex].m_Skip && DestIndex < DestSize; Counter++) |
| 291 | { |
| 292 | pDest[DestIndex].m_Index = pSrc[SrcIndex].m_Index; |
| 293 | pDest[DestIndex].m_Flags = pSrc[SrcIndex].m_Flags; |
| 294 | pDest[DestIndex].m_Skip = 0; |
| 295 | pDest[DestIndex].m_MustBe0 = 0; |
| 296 | DestIndex++; |
| 297 | } |
| 298 | SrcIndex++; |
| 299 | } |
| 300 | if(DestIndex != DestSize) |
| 301 | { |
| 302 | log_error("map/load" , "Tile layer data is truncated (got %" PRIzu ", wanted %" PRIzu ")." , |
| 303 | DestIndex, DestSize); |
| 304 | return false; |
| 305 | } |
| 306 | if(SrcIndex != SrcSize) |
| 307 | { |
| 308 | log_error("map/load" , "Too much tile layer data (read %" PRIzu ", total %" PRIzu ")." , |
| 309 | SrcIndex, SrcSize); |
| 310 | return false; |
| 311 | } |
| 312 | return true; |
| 313 | } |
| 314 | |
| 315 | static bool EnsureTileLayerProperties(int GroupIndex, int LayerIndex, CMapItemLayerTilemap &LayerTilemap) |
| 316 | { |
| 317 | if(LayerTilemap.m_Width < 2) |
| 318 | { |
| 319 | log_error("map/load" , "Tile layer %d in group %d has invalid width %d." , |
| 320 | LayerIndex, GroupIndex, LayerTilemap.m_Width); |
| 321 | return false; |
| 322 | } |
| 323 | |
| 324 | if(LayerTilemap.m_Height < 2) |
| 325 | { |
| 326 | log_error("map/load" , "Tile layer %d in group %d has invalid height %d." , |
| 327 | LayerIndex, GroupIndex, LayerTilemap.m_Height); |
| 328 | return false; |
| 329 | } |
| 330 | |
| 331 | const auto &&EnsureValidName = [&](const char *pExpectedName) { |
| 332 | char aCurrentName[sizeof(LayerTilemap.m_aName)]; |
| 333 | if(!IntsToStr(pInts: LayerTilemap.m_aName, NumInts: std::size(LayerTilemap.m_aName), pStr: aCurrentName, StrSize: std::size(aCurrentName))) |
| 334 | { |
| 335 | log_error("map/load" , "Tile layer %d in group %d has invalid name." , |
| 336 | LayerIndex, GroupIndex); |
| 337 | return false; |
| 338 | } |
| 339 | else if(pExpectedName != nullptr && str_comp(a: aCurrentName, b: pExpectedName) != 0) |
| 340 | { |
| 341 | log_debug("map/load" , "Physics tile layer %d in group %d has unexpected name '%s'. Resetting to '%s'." , |
| 342 | LayerIndex, GroupIndex, aCurrentName, pExpectedName); |
| 343 | StrToInts(pInts: LayerTilemap.m_aName, NumInts: std::size(LayerTilemap.m_aName), pStr: pExpectedName); |
| 344 | } |
| 345 | return true; |
| 346 | }; |
| 347 | |
| 348 | const auto &&EnsureDefaultColor = [&]() { |
| 349 | const CColor DefaultColor = CColor{255, 255, 255, 255}; |
| 350 | if(LayerTilemap.m_Color != DefaultColor) |
| 351 | { |
| 352 | log_debug("map/load" , "Physics tile layer %d in group %d has unexpected color (%d, %d, %d, %d). Resetting to default." , |
| 353 | LayerIndex, GroupIndex, LayerTilemap.m_Color.r, LayerTilemap.m_Color.g, LayerTilemap.m_Color.b, LayerTilemap.m_Color.a); |
| 354 | LayerTilemap.m_Color = DefaultColor; |
| 355 | } |
| 356 | }; |
| 357 | |
| 358 | const auto &&EnsureNoDetailFlag = [&]() { |
| 359 | if(LayerTilemap.m_Layer.m_Flags & LAYERFLAG_DETAIL) |
| 360 | { |
| 361 | log_debug("map/load" , "Physics tile layer %d in group %d has detail flag set. Resetting to non-detail." , |
| 362 | LayerIndex, GroupIndex); |
| 363 | LayerTilemap.m_Layer.m_Flags &= ~LAYERFLAG_DETAIL; |
| 364 | } |
| 365 | }; |
| 366 | |
| 367 | if(LayerTilemap.m_Flags & TILESLAYERFLAG_GAME) |
| 368 | { |
| 369 | if(!EnsureValidName("Game" )) |
| 370 | { |
| 371 | return false; |
| 372 | } |
| 373 | EnsureDefaultColor(); |
| 374 | EnsureNoDetailFlag(); |
| 375 | } |
| 376 | else if(LayerTilemap.m_Flags & TILESLAYERFLAG_TELE) |
| 377 | { |
| 378 | if(!EnsureValidName("Tele" )) |
| 379 | { |
| 380 | return false; |
| 381 | } |
| 382 | EnsureDefaultColor(); |
| 383 | EnsureNoDetailFlag(); |
| 384 | } |
| 385 | else if(LayerTilemap.m_Flags & TILESLAYERFLAG_SPEEDUP) |
| 386 | { |
| 387 | if(!EnsureValidName("Speedup" )) |
| 388 | { |
| 389 | return false; |
| 390 | } |
| 391 | EnsureDefaultColor(); |
| 392 | EnsureNoDetailFlag(); |
| 393 | } |
| 394 | else if(LayerTilemap.m_Flags & TILESLAYERFLAG_FRONT) |
| 395 | { |
| 396 | if(!EnsureValidName("Front" )) |
| 397 | { |
| 398 | return false; |
| 399 | } |
| 400 | EnsureDefaultColor(); |
| 401 | EnsureNoDetailFlag(); |
| 402 | } |
| 403 | else if(LayerTilemap.m_Flags & TILESLAYERFLAG_SWITCH) |
| 404 | { |
| 405 | if(!EnsureValidName("Switch" )) |
| 406 | { |
| 407 | return false; |
| 408 | } |
| 409 | EnsureDefaultColor(); |
| 410 | EnsureNoDetailFlag(); |
| 411 | } |
| 412 | else if(LayerTilemap.m_Flags & TILESLAYERFLAG_TUNE) |
| 413 | { |
| 414 | if(!EnsureValidName("Tune" )) |
| 415 | { |
| 416 | return false; |
| 417 | } |
| 418 | EnsureDefaultColor(); |
| 419 | EnsureNoDetailFlag(); |
| 420 | } |
| 421 | else |
| 422 | { |
| 423 | if(!EnsureValidName(nullptr)) |
| 424 | { |
| 425 | return false; |
| 426 | } |
| 427 | |
| 428 | if(!in_range(a: LayerTilemap.m_Color.r, lower: 0, upper: 255) || |
| 429 | !in_range(a: LayerTilemap.m_Color.g, lower: 0, upper: 255) || |
| 430 | !in_range(a: LayerTilemap.m_Color.b, lower: 0, upper: 255) || |
| 431 | !in_range(a: LayerTilemap.m_Color.a, lower: 0, upper: 255)) |
| 432 | { |
| 433 | log_error("map/load" , "Tile layer %d in group %d has invalid color (%d, %d, %d, %d)." , |
| 434 | LayerIndex, GroupIndex, LayerTilemap.m_Color.r, LayerTilemap.m_Color.g, LayerTilemap.m_Color.b, LayerTilemap.m_Color.a); |
| 435 | return false; |
| 436 | } |
| 437 | } |
| 438 | |
| 439 | const auto &&EnsureUnsetPhysicsData = [&](int TilesLayerFlag, int *pDataIndex, const char *pName) { |
| 440 | if((LayerTilemap.m_Flags & TilesLayerFlag) == 0 && *pDataIndex != -1) |
| 441 | { |
| 442 | log_debug("map/load" , "Tile layer %d in group %d has unused %s data index %d. Resetting to -1." , |
| 443 | LayerIndex, GroupIndex, pName, *pDataIndex); |
| 444 | *pDataIndex = -1; |
| 445 | } |
| 446 | }; |
| 447 | EnsureUnsetPhysicsData(TILESLAYERFLAG_TELE, &LayerTilemap.m_Tele, "tele" ); |
| 448 | EnsureUnsetPhysicsData(TILESLAYERFLAG_SPEEDUP, &LayerTilemap.m_Speedup, "speedup" ); |
| 449 | EnsureUnsetPhysicsData(TILESLAYERFLAG_FRONT, &LayerTilemap.m_Front, "front" ); |
| 450 | EnsureUnsetPhysicsData(TILESLAYERFLAG_SWITCH, &LayerTilemap.m_Switch, "switch" ); |
| 451 | EnsureUnsetPhysicsData(TILESLAYERFLAG_TUNE, &LayerTilemap.m_Tune, "tune" ); |
| 452 | |
| 453 | return true; |
| 454 | } |
| 455 | |
| 456 | bool CMap::UpgradeAndValidateTilesLayerItem( |
| 457 | CDataFileReader &NewDataFile, int GroupIndex, int LayerIndex, |
| 458 | CMapItemLayerTilemap_v2 *pLayerTilemapBase, int LayerItemIndex, size_t LayerItemSize) |
| 459 | { |
| 460 | if(LayerItemSize < sizeof(CMapItemLayerTilemap_v2)) |
| 461 | { |
| 462 | log_error("map/load" , "Tile layer %d in group %d is truncated (size %" PRIzu ")." , |
| 463 | LayerIndex, GroupIndex, LayerItemSize); |
| 464 | return false; |
| 465 | } |
| 466 | |
| 467 | if(!in_range(a: pLayerTilemapBase->m_Version, lower: 2, upper: 4)) |
| 468 | { |
| 469 | log_error("map/load" , "Tile layer %d in group %d has unsupported version %d." , |
| 470 | LayerIndex, GroupIndex, pLayerTilemapBase->m_Version); |
| 471 | return false; |
| 472 | } |
| 473 | |
| 474 | if(!AtMostOneBitSet(Flags: pLayerTilemapBase->m_Flags & (TILESLAYERFLAG_GAME | TILESLAYERFLAG_TELE | TILESLAYERFLAG_SPEEDUP | TILESLAYERFLAG_FRONT | TILESLAYERFLAG_SWITCH | TILESLAYERFLAG_TUNE))) |
| 475 | { |
| 476 | log_error("map/load" , "Tile layer %d in group %d has invalid combination of flags %d. At most one physics tile layer flag can be set." , |
| 477 | LayerIndex, GroupIndex, pLayerTilemapBase->m_Flags); |
| 478 | return false; |
| 479 | } |
| 480 | |
| 481 | const auto &&UnpackPhysicsLayerDataIndex = [&](int TilesLayerFlag, int *pTargetDataIndex, const int *pSourceDataIndex, const CMapItemLayerTilemap_v2 *pSourceTileLayer, const char *pName) { |
| 482 | // We have to check the size individually for each tile data index because old maps were created |
| 483 | // containing only some prefix of the physics tile data indices without incrementing the version. |
| 484 | if(LayerItemSize < reinterpret_cast<const uint8_t *>(pSourceDataIndex) - reinterpret_cast<const uint8_t *>(pSourceTileLayer) + sizeof(*pSourceDataIndex)) |
| 485 | { |
| 486 | if(pSourceTileLayer->m_Flags & TilesLayerFlag) |
| 487 | { |
| 488 | log_error("map/load" , "%s layer %d in group %d is truncated (version %d, size %" PRIzu ")." , |
| 489 | pName, LayerIndex, GroupIndex, pSourceTileLayer->m_Version, LayerItemSize); |
| 490 | return false; |
| 491 | } |
| 492 | *pTargetDataIndex = -1; |
| 493 | } |
| 494 | else |
| 495 | { |
| 496 | *pTargetDataIndex = *pSourceDataIndex; |
| 497 | } |
| 498 | return true; |
| 499 | }; |
| 500 | |
| 501 | if(pLayerTilemapBase->m_Version == 2) |
| 502 | { |
| 503 | const CMapItemLayerTilemap_v2Legacy *pLayerTilemapLegacy = static_cast<const CMapItemLayerTilemap_v2Legacy *>(pLayerTilemapBase); |
| 504 | CMapItemLayerTilemap OverriddenLayerTilemap; |
| 505 | mem_copy(dest: &OverriddenLayerTilemap, source: pLayerTilemapLegacy, size: sizeof(CMapItemLayerTilemap_v2)); |
| 506 | |
| 507 | // Version 2 items have no name. Default to empty string. We fix the name of physics layers later. |
| 508 | StrToInts(pInts: OverriddenLayerTilemap.m_aName, NumInts: std::size(OverriddenLayerTilemap.m_aName), pStr: "" ); |
| 509 | |
| 510 | if(!UnpackPhysicsLayerDataIndex(TILESLAYERFLAG_TELE, &OverriddenLayerTilemap.m_Tele, &pLayerTilemapLegacy->m_Tele, pLayerTilemapBase, "Tele" ) || |
| 511 | !UnpackPhysicsLayerDataIndex(TILESLAYERFLAG_SPEEDUP, &OverriddenLayerTilemap.m_Speedup, &pLayerTilemapLegacy->m_Speedup, pLayerTilemapBase, "Speedup" ) || |
| 512 | !UnpackPhysicsLayerDataIndex(TILESLAYERFLAG_FRONT, &OverriddenLayerTilemap.m_Front, &pLayerTilemapLegacy->m_Front, pLayerTilemapBase, "Front" ) || |
| 513 | !UnpackPhysicsLayerDataIndex(TILESLAYERFLAG_SWITCH, &OverriddenLayerTilemap.m_Switch, &pLayerTilemapLegacy->m_Switch, pLayerTilemapBase, "Switch" ) || |
| 514 | !UnpackPhysicsLayerDataIndex(TILESLAYERFLAG_TUNE, &OverriddenLayerTilemap.m_Tune, &pLayerTilemapLegacy->m_Tune, pLayerTilemapBase, "Tune" )) |
| 515 | { |
| 516 | return false; |
| 517 | } |
| 518 | if(!EnsureTileLayerProperties(GroupIndex, LayerIndex, LayerTilemap&: OverriddenLayerTilemap)) |
| 519 | { |
| 520 | return false; |
| 521 | } |
| 522 | if(!NewDataFile.OverrideItemData(Index: LayerItemIndex, pData: &OverriddenLayerTilemap, Size: sizeof(OverriddenLayerTilemap))) |
| 523 | { |
| 524 | return false; |
| 525 | } |
| 526 | } |
| 527 | else if(LayerItemSize < sizeof(CMapItemLayerTilemap_v3Teeworlds)) |
| 528 | { |
| 529 | // Only the physics layer data indices added by DDRace may be truncated in |
| 530 | // version 3 and 4 items, the layer name must always be complete. |
| 531 | log_error("map/load" , "Tile layer %d in group %d is truncated (version %d, size %" PRIzu ")." , |
| 532 | LayerIndex, GroupIndex, pLayerTilemapBase->m_Version, LayerItemSize); |
| 533 | return false; |
| 534 | } |
| 535 | else if(LayerItemSize < sizeof(CMapItemLayerTilemap)) |
| 536 | { |
| 537 | const CMapItemLayerTilemap *pLayerTilemapLegacy = static_cast<const CMapItemLayerTilemap *>(pLayerTilemapBase); |
| 538 | CMapItemLayerTilemap OverriddenLayerTilemap; |
| 539 | mem_copy(dest: &OverriddenLayerTilemap, source: pLayerTilemapLegacy, size: sizeof(CMapItemLayerTilemap_v3Teeworlds)); |
| 540 | |
| 541 | if(!UnpackPhysicsLayerDataIndex(TILESLAYERFLAG_TELE, &OverriddenLayerTilemap.m_Tele, &pLayerTilemapLegacy->m_Tele, pLayerTilemapBase, "Tele" ) || |
| 542 | !UnpackPhysicsLayerDataIndex(TILESLAYERFLAG_SPEEDUP, &OverriddenLayerTilemap.m_Speedup, &pLayerTilemapLegacy->m_Speedup, pLayerTilemapBase, "Speedup" ) || |
| 543 | !UnpackPhysicsLayerDataIndex(TILESLAYERFLAG_FRONT, &OverriddenLayerTilemap.m_Front, &pLayerTilemapLegacy->m_Front, pLayerTilemapBase, "Front" ) || |
| 544 | !UnpackPhysicsLayerDataIndex(TILESLAYERFLAG_SWITCH, &OverriddenLayerTilemap.m_Switch, &pLayerTilemapLegacy->m_Switch, pLayerTilemapBase, "Switch" ) || |
| 545 | !UnpackPhysicsLayerDataIndex(TILESLAYERFLAG_TUNE, &OverriddenLayerTilemap.m_Tune, &pLayerTilemapLegacy->m_Tune, pLayerTilemapBase, "Tune" )) |
| 546 | { |
| 547 | return false; |
| 548 | } |
| 549 | if(!EnsureTileLayerProperties(GroupIndex, LayerIndex, LayerTilemap&: OverriddenLayerTilemap)) |
| 550 | { |
| 551 | return false; |
| 552 | } |
| 553 | if(!NewDataFile.OverrideItemData(Index: LayerItemIndex, pData: &OverriddenLayerTilemap, Size: sizeof(OverriddenLayerTilemap))) |
| 554 | { |
| 555 | return false; |
| 556 | } |
| 557 | } |
| 558 | else // latest version, whole CMapItemLayerTilemap available |
| 559 | { |
| 560 | if(!EnsureTileLayerProperties(GroupIndex, LayerIndex, LayerTilemap&: *static_cast<CMapItemLayerTilemap *>(pLayerTilemapBase))) |
| 561 | { |
| 562 | return false; |
| 563 | } |
| 564 | } |
| 565 | |
| 566 | return true; |
| 567 | } |
| 568 | |
| 569 | bool CMap::ValidateAndUnpackTilesLayerData(CDataFileReader &NewDataFile, int GroupIndex, int LayerIndex, const CMapItemLayerTilemap *pLayerTilemap, const CMapItemLayerTilemap &GameLayer, std::set<int> &UsedDataIndices) |
| 570 | { |
| 571 | size_t TileSize; |
| 572 | int DataIndex; |
| 573 | int LayerType; |
| 574 | if(pLayerTilemap->m_Flags & TILESLAYERFLAG_GAME) |
| 575 | { |
| 576 | TileSize = sizeof(CTile); |
| 577 | DataIndex = pLayerTilemap->m_Data; |
| 578 | LayerType = LAYERTYPE_GAME; |
| 579 | } |
| 580 | else if(pLayerTilemap->m_Flags & TILESLAYERFLAG_TELE) |
| 581 | { |
| 582 | TileSize = sizeof(CTeleTile); |
| 583 | DataIndex = pLayerTilemap->m_Tele; |
| 584 | LayerType = LAYERTYPE_TELE; |
| 585 | } |
| 586 | else if(pLayerTilemap->m_Flags & TILESLAYERFLAG_SPEEDUP) |
| 587 | { |
| 588 | TileSize = sizeof(CSpeedupTile); |
| 589 | DataIndex = pLayerTilemap->m_Speedup; |
| 590 | LayerType = LAYERTYPE_SPEEDUP; |
| 591 | } |
| 592 | else if(pLayerTilemap->m_Flags & TILESLAYERFLAG_FRONT) |
| 593 | { |
| 594 | TileSize = sizeof(CTile); |
| 595 | DataIndex = pLayerTilemap->m_Front; |
| 596 | LayerType = LAYERTYPE_FRONT; |
| 597 | } |
| 598 | else if(pLayerTilemap->m_Flags & TILESLAYERFLAG_SWITCH) |
| 599 | { |
| 600 | TileSize = sizeof(CSwitchTile); |
| 601 | DataIndex = pLayerTilemap->m_Switch; |
| 602 | LayerType = LAYERTYPE_SWITCH; |
| 603 | } |
| 604 | else if(pLayerTilemap->m_Flags & TILESLAYERFLAG_TUNE) |
| 605 | { |
| 606 | TileSize = sizeof(CTuneTile); |
| 607 | DataIndex = pLayerTilemap->m_Tune; |
| 608 | LayerType = LAYERTYPE_TUNE; |
| 609 | } |
| 610 | else |
| 611 | { |
| 612 | TileSize = sizeof(CTile); |
| 613 | DataIndex = pLayerTilemap->m_Data; |
| 614 | LayerType = LAYERTYPE_TILES; |
| 615 | } |
| 616 | |
| 617 | if(DataIndex < 0 || DataIndex >= NewDataFile.NumData()) |
| 618 | { |
| 619 | log_error("map/load" , "Tile data index %d of layer %d in group %d is invalid." , DataIndex, LayerIndex, GroupIndex); |
| 620 | return false; |
| 621 | } |
| 622 | |
| 623 | const auto &[_, DataUnique] = UsedDataIndices.emplace(args&: DataIndex); |
| 624 | if(!DataUnique) |
| 625 | { |
| 626 | log_error("map/load" , "Tile data index %d of layer %d in group %d is not unique." , DataIndex, LayerIndex, GroupIndex); |
| 627 | return false; |
| 628 | } |
| 629 | |
| 630 | const size_t TilemapCount = (size_t)pLayerTilemap->m_Width * pLayerTilemap->m_Height; |
| 631 | const size_t TilemapSize = TilemapCount * TileSize; |
| 632 | |
| 633 | if(((int)TilemapCount / pLayerTilemap->m_Width != pLayerTilemap->m_Height) || (TilemapSize / TileSize != TilemapCount)) |
| 634 | { |
| 635 | log_error("map/load" , "Tile layer %d in group %d is too big (%d * %d * %" PRIzu " causes an integer overflow)." , |
| 636 | LayerIndex, GroupIndex, pLayerTilemap->m_Width, pLayerTilemap->m_Height, TileSize); |
| 637 | return false; |
| 638 | } |
| 639 | |
| 640 | // The collision uses the size of the game layer for the data of all physics layers, |
| 641 | // so physics layers must contain at least as many tiles as the game layer. |
| 642 | if(LayerType != LAYERTYPE_TILES && LayerType != LAYERTYPE_GAME && |
| 643 | TilemapCount < (size_t)GameLayer.m_Width * GameLayer.m_Height) |
| 644 | { |
| 645 | log_error("map/load" , "Physics layer %d in group %d is smaller than the game layer (%d * %d < %d * %d)." , |
| 646 | LayerIndex, GroupIndex, pLayerTilemap->m_Width, pLayerTilemap->m_Height, GameLayer.m_Width, GameLayer.m_Height); |
| 647 | return false; |
| 648 | } |
| 649 | |
| 650 | NewDataFile.AddDataProcessor(Index: DataIndex, DataProcessor: [pLayerTilemap, TileSize, LayerType, GroupIndex, LayerIndex, TilemapCount, TilemapSize](void *pData, size_t Size) -> std::pair<void *, size_t> { |
| 651 | const size_t SavedTilesSize = Size / TileSize; |
| 652 | if(pLayerTilemap->m_Version >= 4) |
| 653 | { |
| 654 | // CMapItemLayerTilemap with this version are only written to maps in upstream Teeworlds. |
| 655 | // The tile data of tilemaps using this version must be unpacked by repeating tiles |
| 656 | // according to the CTile::m_Skip values of the packed tile data. |
| 657 | if(LayerType != LAYERTYPE_TILES && LayerType != LAYERTYPE_GAME) |
| 658 | { |
| 659 | log_error("map/load" , "Layer %d in group %d uses tileskip but this is only supported for tiles and game layers." , |
| 660 | LayerIndex, GroupIndex); |
| 661 | free(ptr: pData); |
| 662 | return std::make_pair(x: nullptr, y: 0); |
| 663 | } |
| 664 | CTile *pTiles = static_cast<CTile *>(malloc(size: TilemapSize)); |
| 665 | if(pTiles == nullptr) |
| 666 | { |
| 667 | log_error("map/load" , "Failed to allocate memory for layer %d in group %d (size %d * %d)." , |
| 668 | LayerIndex, GroupIndex, pLayerTilemap->m_Width, pLayerTilemap->m_Height); |
| 669 | free(ptr: pData); |
| 670 | return std::make_pair(x: nullptr, y: 0); |
| 671 | } |
| 672 | else if(!ExtractTiles(pDest: pTiles, DestSize: (size_t)pLayerTilemap->m_Width * pLayerTilemap->m_Height, pSrc: static_cast<const CTile *>(pData), SrcSize: SavedTilesSize)) |
| 673 | { |
| 674 | log_error("map/load" , "Failed to extract tiles of layer %d in group %d." , |
| 675 | LayerIndex, GroupIndex); |
| 676 | free(ptr: pTiles); |
| 677 | free(ptr: pData); |
| 678 | return std::make_pair(x: nullptr, y: 0); |
| 679 | } |
| 680 | free(ptr: pData); |
| 681 | return std::make_pair(x&: pTiles, y: TilemapSize); |
| 682 | } |
| 683 | else if(SavedTilesSize < TilemapCount) |
| 684 | { |
| 685 | log_error("map/load" , "Tile data of layer %d in group %d is truncated (got %" PRIzu ", wanted %" PRIzu ")." , |
| 686 | LayerIndex, GroupIndex, SavedTilesSize, TilemapCount); |
| 687 | free(ptr: pData); |
| 688 | return std::make_pair(x: nullptr, y: 0); |
| 689 | } |
| 690 | else if(LayerType == LAYERTYPE_TILES || LayerType == LAYERTYPE_GAME || LayerType == LAYERTYPE_FRONT) |
| 691 | { |
| 692 | const CTile *pTileData = static_cast<const CTile *>(pData); |
| 693 | for(size_t TileIndex = 0; TileIndex < TilemapCount; ++TileIndex) |
| 694 | { |
| 695 | if(pTileData[TileIndex].m_Skip != 0) |
| 696 | { |
| 697 | log_error("map/load" , "Tile data of layer %d in group %d contains non-zero skip value %d at index %" PRIzu " but version %d does not use tileskip." , |
| 698 | LayerIndex, GroupIndex, pTileData[TileIndex].m_Skip, TileIndex, pLayerTilemap->m_Version); |
| 699 | free(ptr: pData); |
| 700 | return std::make_pair(x: nullptr, y: 0); |
| 701 | } |
| 702 | if(pTileData[TileIndex].m_MustBe0 != 0) |
| 703 | { |
| 704 | log_error("map/load" , "Tile data of layer %d in group %d contains non-zero padding value %d at index %" PRIzu "." , |
| 705 | LayerIndex, GroupIndex, pTileData[TileIndex].m_MustBe0, TileIndex); |
| 706 | free(ptr: pData); |
| 707 | return std::make_pair(x: nullptr, y: 0); |
| 708 | } |
| 709 | } |
| 710 | } |
| 711 | else if(LayerType == LAYERTYPE_SPEEDUP) |
| 712 | { |
| 713 | const CSpeedupTile *pSpeedupData = static_cast<const CSpeedupTile *>(pData); |
| 714 | for(size_t TileIndex = 0; TileIndex < TilemapCount; ++TileIndex) |
| 715 | { |
| 716 | if(pSpeedupData[TileIndex].m_MustBe0 != 0) |
| 717 | { |
| 718 | log_error("map/load" , "Speedup tile data of layer %d in group %d contains non-zero padding value %d at index %" PRIzu "." , |
| 719 | LayerIndex, GroupIndex, pSpeedupData[TileIndex].m_MustBe0, TileIndex); |
| 720 | free(ptr: pData); |
| 721 | return std::make_pair(x: nullptr, y: 0); |
| 722 | } |
| 723 | } |
| 724 | } |
| 725 | return std::make_pair(x&: pData, y&: Size); |
| 726 | }); |
| 727 | |
| 728 | return true; |
| 729 | } |
| 730 | |
| 731 | extern std::unique_ptr<IMap> CreateMap() |
| 732 | { |
| 733 | return std::make_unique<CMap>(); |
| 734 | } |
| 735 | |