EBLAN_ENGINE/editor/src/level_io.h

129 lines
7.6 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#pragma once
#include "level_format.h"
#include "cutscene_format.h"
#include "lighting_format.h"
#include <array>
#include <cstring>
#include <fstream>
#include <string>
#include <vector>
// Parse into a separate value: a failed load must never partially replace a room.
struct BinaryLevel {
MapHeader header{};
std::array<uint8_t, MAP_TOTAL_CELLS> cells{};
std::vector<SpriteEntry> sprites;
std::vector<LevelTransition> transitions;
CutsceneTrack cutscene;
LevelLighting lighting;
};
inline bool IsMapFilename(const char* name) {
size_t length = 0;
for (; length < 32 && name[length]; ++length) {
unsigned char ch = static_cast<unsigned char>(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<bool, MAP_TOTAL_CELLS> 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("Портал не имеет настроенного перехода");
if(!ValidateLighting(level.lighting.settings,level.lighting.lights.data(),level.lighting.lights.size(),error)) return false;
for(auto c:level.lighting.rgb) if(c&0xFF000000) {error="Неверный цвет запечённого света";return false;}
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<char*>(&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 lightCount=h.version>=5?h.lightCount:0;
if(lightCount>MAX_EDITOR_LIGHTS || (h.version>=5&&h.lightingReserved)) {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 +
(h.version>=5?sizeof(LightingSettings)+lightCount*sizeof(EditorLightSource)+4096:0);
if (fileSize != static_cast<std::streamoff>(expected)) {
error = "Размер файла не соответствует заголовку карты"; return false;
}
candidate.sprites.resize(h.spriteCount);
candidate.transitions.resize(transitionCount);
in.read(reinterpret_cast<char*>(candidate.cells.data()), MAP_TOTAL_CELLS);
if (!candidate.sprites.empty())
in.read(reinterpret_cast<char*>(candidate.sprites.data()), candidate.sprites.size() * sizeof(SpriteEntry));
if (!candidate.transitions.empty())
in.read(reinterpret_cast<char*>(candidate.transitions.data()), candidate.transitions.size() * sizeof(LevelTransition));
candidate.cutscene.events.resize(eventCount);
if (eventCount) in.read(reinterpret_cast<char*>(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<const char*>(std::memchr(bytes, 0, sizeof(bytes)));
if (!end) { error = "Строка кат-сцены не имеет завершающего нуля"; return false; }
candidate.cutscene.strings.push_back(CutsceneDecode(bytes, static_cast<int>(end - bytes)));
}
if(h.version>=5) {
in.read(reinterpret_cast<char*>(&candidate.lighting.settings),sizeof(LightingSettings));
candidate.lighting.lights.resize(lightCount);
if(lightCount)in.read(reinterpret_cast<char*>(candidate.lighting.lights.data()),lightCount*sizeof(EditorLightSource));
in.read(reinterpret_cast<char*>(candidate.lighting.rgb.data()),4096);
}
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;
}