#pragma once #include "level_format.h" #include "cutscene_format.h" #include #include #include #include #include // Parse into a separate value: a failed load must never partially replace a room. struct BinaryLevel { MapHeader header{}; std::array cells{}; std::vector sprites; std::vector transitions; CutsceneTrack cutscene; }; inline bool IsMapFilename(const char* name) { size_t length = 0; for (; length < 32 && name[length]; ++length) { unsigned char ch = static_cast(name[length]); if (ch < 32 || ch >= 127 || std::strchr("\\/:*?\"<>|", ch)) return false; } if (length < 5 || length >= 32) return false; const char* ext = name + length - 4; return ext[0] == '.' && (ext[1] | 32) == 'd' && (ext[2] | 32) == 'a' && (ext[3] | 32) == 't'; } inline bool IsWalkableCell(uint8_t cell) { return cell == CELL_EMPTY || cell == CELL_DOOR_OPEN || cell == CELL_PORTAL; } inline bool ValidateLevel(const BinaryLevel& level, std::string& error) { const auto& h = level.header; auto fail = [&](const char* reason) { error = reason; return false; }; if (h.magic != MAP_MAGIC || h.version < 1 || h.version > MAP_FORMAT_VERSION || h.mapWidth != MAP_GRID_W || h.mapHeight != MAP_GRID_H) return fail("Неверный формат или версия карты"); if (level.sprites.size() > MAX_LEVEL_SPRITES || level.transitions.size() > MAX_LEVEL_TRANSITIONS) return fail("Превышен лимит спрайтов или переходов"); if (h.playerStartX >= MAP_GRID_W || h.playerStartY >= MAP_GRID_H || h.playerStartAngle > 511) return fail("Неверные координаты или угол спавна"); for (uint8_t cell : level.cells) if (cell > 15) return fail("Неверный тип клетки"); if (!IsWalkableCell(level.cells[h.playerStartY * MAP_GRID_W + h.playerStartX])) return fail("Спавн игрока находится внутри стены"); for (const auto& sprite : level.sprites) { if (sprite.x < 0 || sprite.x >= MAP_GRID_W * 64 || sprite.y < 0 || sprite.y >= MAP_GRID_H * 64 || sprite.textureId >= 16 || sprite.scale < 0 || sprite.scale > 16 * 65536) return fail("Неверные координаты, текстура или масштаб спрайта"); } std::array triggers{}; for (const auto& transition : level.transitions) { if (transition.triggerCellX >= MAP_GRID_W || transition.triggerCellY >= MAP_GRID_H || transition.targetSpawnX >= MAP_GRID_W || transition.targetSpawnY >= MAP_GRID_H || transition.targetAngle > 511 || transition.fadeEffect > 1 || !IsMapFilename(transition.targetMap)) return fail("Неверные свойства перехода (имя файла: ASCII, до 31 символа, .dat)"); size_t index = transition.triggerCellY * MAP_GRID_W + transition.triggerCellX; if (triggers[index] || !IsWalkableCell(level.cells[index])) return fail("Повторный переход или триггер внутри стены"); triggers[index] = true; } for (size_t i = 0; i < level.cells.size(); ++i) if (level.cells[i] == CELL_PORTAL && !triggers[i]) return fail("Портал не имеет настроенного перехода"); return ValidateCutscene(level.cutscene, level.cells.data(), level.transitions.size(), error); } inline bool ReadLevelFile(const std::string& path, BinaryLevel& result, std::string& error) { std::ifstream in(path, std::ios::binary | std::ios::ate); if (!in) { error = "Не удалось открыть карту: " + path; return false; } const auto fileSize = in.tellg(); in.seekg(0); BinaryLevel candidate; if (!in.read(reinterpret_cast(&candidate.header), sizeof(MapHeader))) { error = "Обрезанный заголовок карты"; return false; } const auto& h = candidate.header; const size_t transitionCount = h.version == 1 ? 0 : h.transitionCount; if (h.spriteCount > MAX_LEVEL_SPRITES || transitionCount > MAX_LEVEL_TRANSITIONS) { error = "Превышен лимит объектов карты"; return false; } const size_t eventCount = h.version >= 4 ? h.cutsceneCount : 0; const size_t stringCount = h.version >= 4 ? h.cutsceneStringCount : 0; if (eventCount > MAX_CUTSCENE_EVENTS || stringCount > MAX_CUTSCENE_STRINGS) { error = "Превышен лимит событий или строк кат-сцены"; return false; } const size_t expected = sizeof(MapHeader) + MAP_TOTAL_CELLS + h.spriteCount * sizeof(SpriteEntry) + transitionCount * sizeof(LevelTransition) + eventCount * sizeof(CutsceneEvent) + stringCount * CUTSCENE_STRING_BYTES; if (fileSize != static_cast(expected)) { error = "Размер файла не соответствует заголовку карты"; return false; } candidate.sprites.resize(h.spriteCount); candidate.transitions.resize(transitionCount); in.read(reinterpret_cast(candidate.cells.data()), MAP_TOTAL_CELLS); if (!candidate.sprites.empty()) in.read(reinterpret_cast(candidate.sprites.data()), candidate.sprites.size() * sizeof(SpriteEntry)); if (!candidate.transitions.empty()) in.read(reinterpret_cast(candidate.transitions.data()), candidate.transitions.size() * sizeof(LevelTransition)); candidate.cutscene.events.resize(eventCount); if (eventCount) in.read(reinterpret_cast(candidate.cutscene.events.data()), eventCount * sizeof(CutsceneEvent)); candidate.cutscene.flags = h.version >= 4 ? h.cutsceneFlags : 0; for (size_t i = 0; i < stringCount; ++i) { char bytes[CUTSCENE_STRING_BYTES]{}; if (!in.read(bytes, sizeof(bytes))) { error = "Обрезанная строка кат-сцены"; return false; } const char* end = static_cast(std::memchr(bytes, 0, sizeof(bytes))); if (!end) { error = "Строка кат-сцены не имеет завершающего нуля"; return false; } candidate.cutscene.strings.push_back(CutsceneDecode(bytes, static_cast(end - bytes))); } if (!in) { error = "Не удалось прочитать карту целиком"; return false; } if (h.version == 1) for (auto& sprite : candidate.sprites) { sprite.x >>= 16; sprite.y >>= 16; } if (!ValidateLevel(candidate, error)) return false; result = std::move(candidate); return true; }