EBLAN_ENGINE/editor/src/level_io.h
Artvasil09 a1d1baa869 feat: complete engine and editor subsystems (Stages 1-5 from TODO_ARTEM)
- Stage 1: Eliminate fish-eye distortion with precomputed tangential ray offset table and fractional DDA depth
- Stage 2: Full asset management in EblanStudio (stb_image/stb_image_write, 128x128 BGRA texture packaging, WAV sound manager)
- Stage 3: Multi-level room streaming, LevelTransition/CELL_PORTAL trigger system, 16-frame screen fade to black
- Stage 4: 60Hz cutscene engine in pure x86 ASM (v4 binary track, waypoint camera motion, shortest-arc pan, CP1251 Russian subtitles, letterbox overlay, audio)
- Stage 5: EblanStudio cutscene timeline (F6) and windowless Live ASM Viewport over shared-memory IPC directly rendered into DX11
- Tooling & Tests: comprehensive regression test suite (94 editor + 107 engine tests), IPC tests, and cutscene demo
2026-10-22 16:24:22 +03:00

116 lines
6.7 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 <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;
};
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("Портал не имеет настроенного перехода");
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 expected = sizeof(MapHeader) + MAP_TOTAL_CELLS +
h.spriteCount * sizeof(SpriteEntry) + transitionCount * sizeof(LevelTransition) +
eventCount * sizeof(CutsceneEvent) + stringCount * CUTSCENE_STRING_BYTES;
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 (!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;
}