diff --git a/CHANGELOG.md b/CHANGELOG.md index 7aa31bd..05c065a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Changes since ELFBSP 1.1 New features: * Added `--analysis` CLI parameter for BSP tree data analysis and visualization * Added `--debug-*` CLI parameters for runtime debugging, previously a compile-time directive, now a runtime option +* Added support for the DeePBSPV4, XGLN and XGL2 node formats, now enforced via the `--type` CLI parameter -BUgfixes: +Bugfixes: * Improved correctness for certain special effects, numerical effects of number 998 & 999 are now read from the line's tag instead of line's special diff --git a/CMakeLists.txt b/CMakeLists.txt index 1af2432..bae86c3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,6 +52,7 @@ if(SAN_COUNT GREATER 0) endif() add_executable(elfbsp + src/bsp.cpp src/elfbsp.cpp src/level.cpp src/node.cpp diff --git a/docs/binary_formats.md b/docs/binary_formats.md index 324e545..1e8bcc7 100644 --- a/docs/binary_formats.md +++ b/docs/binary_formats.md @@ -129,7 +129,7 @@ Differences from the vanilla BSP data include: * each version is built on top of the previous, replacing the predecessor's given struct type * the removal of angles and offsets from Segments * higher precision data types for fractional coordinates -* "mini-segs", indicated by `NO_INDEX`, to be skipped by the renderer, but needed for traversal +* "minisegs", indicated by `NO_INDEX`, to be skipped by the renderer, but needed for traversal * "linear" storage of Seg indexes on subsectors, see above ### XNOD vertex (`xnod_vertex_t`), 8 bytes diff --git a/src/bsp.cpp b/src/bsp.cpp new file mode 100644 index 0000000..eef03d4 --- /dev/null +++ b/src/bsp.cpp @@ -0,0 +1,943 @@ +//------------------------------------------------------------------------------ +// +// ELFBSP +// +//------------------------------------------------------------------------------ +// +// Copyright 2025-2026 Guilherme Miranda +// Copyright 2000-2018 Andrew Apted, et al +// Copyright 1994-1998 Colin Reed +// Copyright 1997-1998 Lee Killough +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +//------------------------------------------------------------------------------ + +#include + +#include "core.hpp" +#include "local.hpp" + +// +// Utility +// + +struct Compare_seg_pred +{ + inline bool operator()(const seg_t *A, const seg_t *B) const + { + return A->index < B->index; + } +}; + +void SortSegs(void) +{ + // sort segs into ascending index + std::sort(lev_segs.begin(), lev_segs.end(), Compare_seg_pred()); + + // remove unwanted segs + while (lev_segs.size() > 0 && lev_segs.back()->index == SEG_IS_GARBAGE) + { + UtilFree(lev_segs.back()); + lev_segs.pop_back(); + } +} + +static inline int16_t VanillaSegDist(const seg_t *seg) +{ + double lx = seg->side ? seg->linedef->end->x : seg->linedef->start->x; + double ly = seg->side ? seg->linedef->end->y : seg->linedef->start->y; + + // use the "true" starting coord (as stored in the wad) + double sx = round(seg->start->x); + double sy = round(seg->start->y); + + return static_cast(floor(hypot(sx - lx, sy - ly) + 0.5)); +} + +static inline short_angle_t VanillaSegAngle(const seg_t *seg) +{ + // compute the "true" delta + double dx = round(seg->end->x) - round(seg->start->x); + double dy = round(seg->end->y) - round(seg->start->y); + + double angle = ComputeAngle(dx, dy); + + if (angle < 0) + { + angle += 360.0; + } + + short_angle_t result = short_angle_t(floor(angle * 65536.0 / 360.0 + 0.5)); + + // TODO: make sure this works better -- Hexen, UDMF + // -Elf- ZokumBSP + // 1080 => Additive degrees stored in tag + // 1081 => Set to degrees stored in tag + // 1082 => Additive BAM stored in tag + // 1083 => Set to BAM stored in tag + if (seg->linedef->special == Special_RotateDegrees) + { + result += DegreesToShortBAM(static_cast(seg->linedef->tag)); + } + else if (seg->linedef->special == Special_RotateDegreesHard) + { + result = DegreesToShortBAM(static_cast(seg->linedef->tag)); + } + else if (seg->linedef->special == Special_RotateAngleT) + { + result += static_cast(seg->linedef->tag); + } + else if (seg->linedef->special == Special_RotateAngleTHard) + { + result = static_cast(seg->linedef->tag); + } + + return result; +} + +static void PutVertices_Vanilla(void) +{ + // this size is worst-case scenario + size_t size = lev_vertices.size() * sizeof(raw_vertex_t); + Lump_c *lump = CreateLevelLump("VERTEXES", size); + + size_t count = 0; + for (size_t i = 0; i < lev_vertices.size(); i++) + { + raw_vertex_t raw; + + const vertex_t *vert = lev_vertices[i]; + + if (vert->is_new) + { + continue; + } + + raw.x = GetLittleEndian(static_cast(floor(vert->x))); + raw.y = GetLittleEndian(static_cast(floor(vert->y))); + + lump->Write(&raw, sizeof(raw_vertex_t)); + + count++; + } + + lump->Finish(); + + if (count != num_old_vert) + { + PrintLine(LOG_ERROR, "PutVertices miscounted (%zu != %zu)", count, num_old_vert); + } + + if (count > 65534) + { + PrintLine(LOG_NORMAL, "FAILURE: Number of vertices has overflowed."); + lev_overflows = true; + } +} + +static inline uint32_t VertexIndex_XNOD(const vertex_t *v) +{ + if (v->is_new) + { + return static_cast(num_old_vert + v->index); + } + + return static_cast(v->index); +} + +// +// Vanilla format +// + +static void PutSegs_Vanilla(void) +{ + // this size is worst-case scenario + size_t size = lev_segs.size() * sizeof(raw_seg_vanilla_t); + + Lump_c *lump = CreateLevelLump("SEGS", size); + + for (size_t i = 0; i < lev_segs.size(); i++) + { + raw_seg_vanilla_t raw; + + const seg_t *seg = lev_segs[i]; + + raw.start = GetLittleEndian(static_cast(seg->start->index)); + raw.end = GetLittleEndian(static_cast(seg->end->index)); + raw.angle = GetLittleEndian(VanillaSegAngle(seg)); + raw.linedef = GetLittleEndian(static_cast(seg->linedef->index)); + raw.flip = GetLittleEndian(seg->side); + raw.dist = GetLittleEndian(VanillaSegDist(seg)); + + lump->Write(&raw, sizeof(raw_seg_vanilla_t)); + + if (HAS_BIT(config.debug, DEBUG_BSP)) + { + PrintLine(LOG_DEBUG, "[%s] %zu Vert %04X->%04X Line %04X %s Angle %04X (%1.1f,%1.1f) -> (%1.1f,%1.1f)", __func__, + seg->index, GetLittleEndian(raw.start), GetLittleEndian(raw.end), GetLittleEndian(raw.linedef), + seg->side ? "L" : "R", GetLittleEndian(raw.angle), seg->start->x, seg->start->y, seg->end->x, seg->end->y); + } + } + + lump->Finish(); + + if (lev_segs.size() > LIMIT_SEG) + { + PrintLine(LOG_NORMAL, "FAILURE: Number of segs has overflowed."); + lev_overflows = true; + } +} + +static void PutSubsecs_Vanilla(void) +{ + size_t size = lev_subsecs.size() * sizeof(raw_subsec_vanilla_t); + + Lump_c *lump = CreateLevelLump("SSECTORS", size); + + for (size_t i = 0; i < lev_subsecs.size(); i++) + { + raw_subsec_vanilla_t raw; + + const subsec_t *sub = lev_subsecs[i]; + + raw.first = GetLittleEndian(static_cast(sub->seg_list->index)); + raw.num = GetLittleEndian(static_cast(sub->seg_count)); + + lump->Write(&raw, sizeof(raw_subsec_vanilla_t)); + + if (HAS_BIT(config.debug, DEBUG_BSP)) + { + PrintLine(LOG_DEBUG, "[%s] %zu First %04X Num %04X", __func__, sub->index, GetLittleEndian(raw.first), + GetLittleEndian(raw.num)); + } + } + + if (lev_subsecs.size() > LIMIT_SUBSEC) + { + PrintLine(LOG_NORMAL, "FAILURE: Number of subsectors has overflowed."); + lev_overflows = true; + } + + lump->Finish(); +} + +static void PutOneNode_Vanilla(Lump_c *lump, node_t *node, size_t &node_cur_index) +{ + if (node->r.node) + { + PutOneNode_Vanilla(lump, node->r.node, node_cur_index); + } + + if (node->l.node) + { + PutOneNode_Vanilla(lump, node->l.node, node_cur_index); + } + + node->index = node_cur_index++; + + raw_node_vanilla_t raw; + + // note that x/y/dx/dy are always integral in non-UDMF maps + raw.x = GetLittleEndian(static_cast(floor(node->x))); + raw.y = GetLittleEndian(static_cast(floor(node->y))); + raw.dx = GetLittleEndian(static_cast(floor(node->dx))); + raw.dy = GetLittleEndian(static_cast(floor(node->dy))); + + raw.b1.minx = GetLittleEndian(static_cast(node->r.bounds.minx)); + raw.b1.miny = GetLittleEndian(static_cast(node->r.bounds.miny)); + raw.b1.maxx = GetLittleEndian(static_cast(node->r.bounds.maxx)); + raw.b1.maxy = GetLittleEndian(static_cast(node->r.bounds.maxy)); + + raw.b2.minx = GetLittleEndian(static_cast(node->l.bounds.minx)); + raw.b2.miny = GetLittleEndian(static_cast(node->l.bounds.miny)); + raw.b2.maxx = GetLittleEndian(static_cast(node->l.bounds.maxx)); + raw.b2.maxy = GetLittleEndian(static_cast(node->l.bounds.maxy)); + + if (node->r.node) + { + raw.right = GetLittleEndian(static_cast(node->r.node->index)); + } + else if (node->r.subsec) + { + raw.right = GetLittleEndian(static_cast(node->r.subsec->index | NF_SUBSECTOR_VANILLA)); + } + else + { + PrintLine(LOG_ERROR, "Bad right child in node %zu", node->index); + } + + if (node->l.node) + { + raw.left = GetLittleEndian(static_cast(node->l.node->index)); + } + else if (node->l.subsec) + { + raw.left = GetLittleEndian(static_cast(node->l.subsec->index | NF_SUBSECTOR_VANILLA)); + } + else + { + PrintLine(LOG_ERROR, "Bad left child in node %zu", node->index); + } + + lump->Write(&raw, sizeof(raw_node_vanilla_t)); + + if (HAS_BIT(config.debug, DEBUG_BSP)) + { + PrintLine(LOG_DEBUG, "[%s] %zu Left %04X Right %04X (%1.1f,%1.1f) -> (%1.1f,%1.1f)", __func__, node->index, + GetLittleEndian(raw.left), GetLittleEndian(raw.right), node->x, node->y, node->x + node->dx, node->y + node->dy); + } +} + +static void PutNodes_Vanilla(node_t *root_node) +{ + // this can be bigger than the actual size, but never smaller + size_t max_size = (lev_nodes.size() + 1) * sizeof(raw_node_vanilla_t); + size_t node_cur_index = 0; + Lump_c *lump = CreateLevelLump("NODES", max_size); + + if (root_node != nullptr) + { + PutOneNode_Vanilla(lump, root_node, node_cur_index); + } + + lump->Finish(); + + if (node_cur_index != lev_nodes.size()) + { + PrintLine(LOG_ERROR, "PutNodes miscounted (%zu != %zu)", node_cur_index, lev_nodes.size()); + } + + if (node_cur_index > 32767) + { + PrintLine(LOG_NORMAL, "FAILURE: Number of nodes has overflowed."); + lev_overflows = true; + } +} + +// +// DeepBSPV4 format +// + +static void PutSegs_DeepBSPV4(void) +{ + // this size is worst-case scenario + size_t size = lev_segs.size() * sizeof(raw_seg_deepbspv4_t); + Lump_c *lump = CreateLevelLump("SEGS", size); + + for (size_t i = 0; i < lev_segs.size(); i++) + { + raw_seg_deepbspv4_t raw; + + const seg_t *seg = lev_segs[i]; + + raw.start = GetLittleEndian(static_cast(seg->start->index)); + raw.end = GetLittleEndian(static_cast(seg->end->index)); + raw.angle = GetLittleEndian(VanillaSegAngle(seg)); + raw.linedef = GetLittleEndian(static_cast(seg->linedef->index)); + raw.flip = GetLittleEndian(seg->side); + raw.dist = GetLittleEndian(VanillaSegDist(seg)); + + lump->Write(&raw, sizeof(raw_seg_deepbspv4_t)); + + if (HAS_BIT(config.debug, DEBUG_BSP)) + { + PrintLine(LOG_DEBUG, "[%s] %zu Vert %08X->%08X Line %04X %s Angle %04X (%1.1f,%1.1f) -> (%1.1f,%1.1f)", __func__, + seg->index, GetLittleEndian(raw.start), GetLittleEndian(raw.end), GetLittleEndian(raw.linedef), + seg->side ? "L" : "R", GetLittleEndian(raw.angle), seg->start->x, seg->start->y, seg->end->x, seg->end->y); + } + } + + lump->Finish(); +} + +static void PutSubsecs_DeepBSPV4(void) +{ + size_t size = lev_subsecs.size() * sizeof(raw_subsec_deepbspv4_t); + + Lump_c *lump = CreateLevelLump("SSECTORS", size); + + for (size_t i = 0; i < lev_subsecs.size(); i++) + { + raw_subsec_deepbspv4_t raw; + + const subsec_t *sub = lev_subsecs[i]; + + raw.first = GetLittleEndian(static_cast(sub->seg_list->index)); + raw.num = GetLittleEndian(static_cast(sub->seg_count)); + + lump->Write(&raw, sizeof(raw_subsec_deepbspv4_t)); + + if (HAS_BIT(config.debug, DEBUG_BSP)) + { + PrintLine(LOG_DEBUG, "[%s] %zu First %08X Num %04X", __func__, sub->index, GetLittleEndian(raw.first), + GetLittleEndian(raw.num)); + } + } + + lump->Finish(); +} + +static void PutOneNode_DeepBSPV4(Lump_c *lump, node_t *node, size_t &node_cur_index) +{ + if (node->r.node) + { + PutOneNode_DeepBSPV4(lump, node->r.node, node_cur_index); + } + + if (node->l.node) + { + PutOneNode_DeepBSPV4(lump, node->l.node, node_cur_index); + } + + node->index = node_cur_index++; + + raw_node_deepbspv4_t raw; + + // note that x/y/dx/dy are always integral in non-UDMF maps + raw.x = GetLittleEndian(static_cast(floor(node->x))); + raw.y = GetLittleEndian(static_cast(floor(node->y))); + raw.dx = GetLittleEndian(static_cast(floor(node->dx))); + raw.dy = GetLittleEndian(static_cast(floor(node->dy))); + + raw.b1.minx = GetLittleEndian(static_cast(node->r.bounds.minx)); + raw.b1.miny = GetLittleEndian(static_cast(node->r.bounds.miny)); + raw.b1.maxx = GetLittleEndian(static_cast(node->r.bounds.maxx)); + raw.b1.maxy = GetLittleEndian(static_cast(node->r.bounds.maxy)); + + raw.b2.minx = GetLittleEndian(static_cast(node->l.bounds.minx)); + raw.b2.miny = GetLittleEndian(static_cast(node->l.bounds.miny)); + raw.b2.maxx = GetLittleEndian(static_cast(node->l.bounds.maxx)); + raw.b2.maxy = GetLittleEndian(static_cast(node->l.bounds.maxy)); + + if (node->r.node) + { + raw.right = GetLittleEndian(static_cast(node->r.node->index)); + } + else if (node->r.subsec) + { + raw.right = GetLittleEndian(static_cast(node->r.subsec->index | NF_SUBSECTOR)); + } + else + { + PrintLine(LOG_ERROR, "Bad right child in node %zu", node->index); + } + + if (node->l.node) + { + raw.left = GetLittleEndian(static_cast(node->l.node->index)); + } + else if (node->l.subsec) + { + raw.left = GetLittleEndian(static_cast(node->l.subsec->index | NF_SUBSECTOR)); + } + else + { + PrintLine(LOG_ERROR, "Bad left child in node %zu", node->index); + } + + lump->Write(&raw, sizeof(raw_node_deepbspv4_t)); + + if (HAS_BIT(config.debug, DEBUG_BSP)) + { + PrintLine(LOG_DEBUG, "[%s] %zu Left %08X Right %08X (%1.1f,%1.1f) -> (%1.1f,%1.1f)", __func__, node->index, + GetLittleEndian(raw.left), GetLittleEndian(raw.right), node->x, node->y, node->x + node->dx, node->y + node->dy); + } +} + +static void PutNodes_DeepBSPV4(node_t *root_node) +{ + // this can be bigger than the actual size, but never smaller + // 8 bytes for BSP_MAGIC_DEEPBSPV4 header + // size_t max_size = 8 + (lev_nodes.size() + 1) * sizeof(raw_node_deepbspv4_t); + size_t node_cur_index = 0; + + Lump_c *lump = CreateLevelLump("NODES"); + lump->Write(BSP_MAGIC_DEEPBSPV4, 8); + + if (root_node != nullptr) + { + PutOneNode_DeepBSPV4(lump, root_node, node_cur_index); + } + + lump->Finish(); + + if (node_cur_index != lev_nodes.size()) + { + PrintLine(LOG_ERROR, "PutNodes miscounted (%zu != %zu)", node_cur_index, lev_nodes.size()); + } +} + +// +// ZDoom format -- XNOD +// + +static void PutVertices_Xnod(Lump_c *lump) +{ + size_t orgverts = GetLittleEndian(num_old_vert); + size_t newverts = GetLittleEndian(num_new_vert); + + lump->Write(&orgverts, 4); + lump->Write(&newverts, 4); + + size_t count = 0; + for (size_t i = 0; i < lev_vertices.size(); i++) + { + raw_xnod_vertex_t raw; + + const vertex_t *vert = lev_vertices[i]; + + if (!vert->is_new) + { + continue; + } + + raw.x = GetLittleEndian(static_cast(floor(vert->x * 65536.0))); + raw.y = GetLittleEndian(static_cast(floor(vert->y * 65536.0))); + + lump->Write(&raw, sizeof(raw_xnod_vertex_t)); + + count++; + } + + if (count != num_new_vert) + { + PrintLine(LOG_ERROR, "PutZVertices miscounted (%zu != %zu)", count, num_new_vert); + } +} + +static void PutSubsecs_Xnod(Lump_c *lump) +{ + size_t raw_num = GetLittleEndian(lev_subsecs.size()); + lump->Write(&raw_num, 4); + + size_t cur_seg_index = 0; + for (size_t i = 0; i < lev_subsecs.size(); i++) + { + const subsec_t *sub = lev_subsecs[i]; + + raw_num = GetLittleEndian(sub->seg_count); + lump->Write(&raw_num, 4); + + // sanity check the seg index values + size_t count = 0; + for (const seg_t *seg = sub->seg_list; seg; seg = seg->next, cur_seg_index++) + { + if (cur_seg_index != seg->index) + { + PrintLine(LOG_ERROR, "PutZSubsecs: seg index mismatch in sub %zu (%zu != %zu)", i, cur_seg_index, seg->index); + } + + count++; + } + + if (count != sub->seg_count) + { + PrintLine(LOG_ERROR, "PutZSubsecs: miscounted segs in sub %zu (%zu != %zu)", i, count, sub->seg_count); + } + } + + if (cur_seg_index != lev_segs.size()) + { + PrintLine(LOG_ERROR, "PutZSubsecs miscounted segs (%zu != %zu)", cur_seg_index, lev_segs.size()); + } +} + +static void PutSegs_Xnod(Lump_c *lump) +{ + size_t raw_num = GetLittleEndian(lev_segs.size()); + lump->Write(&raw_num, 4); + + for (size_t i = 0; i < lev_segs.size(); i++) + { + const seg_t *seg = lev_segs[i]; + + if (seg->index != i) + { + PrintLine(LOG_ERROR, "PutZSegs: seg index mismatch (%zu != %zu)", seg->index, i); + } + + raw_xnod_seg_t raw = {}; + + raw.start = GetLittleEndian(VertexIndex_XNOD(seg->start)); + raw.end = GetLittleEndian(VertexIndex_XNOD(seg->end)); + raw.linedef = GetLittleEndian(static_cast(seg->linedef->index)); + raw.side = seg->side; + lump->Write(&raw, sizeof(raw_xnod_seg_t)); + } +} + +static void PutOneNode_Xnod(Lump_c *lump, node_t *node, size_t &node_cur_index) +{ + raw_xnod_node_t raw; + + if (node->r.node) + { + PutOneNode_Xnod(lump, node->r.node, node_cur_index); + } + + if (node->l.node) + { + PutOneNode_Xnod(lump, node->l.node, node_cur_index); + } + + node->index = node_cur_index++; + + raw.x = GetLittleEndian(static_cast(floor(node->x))); + raw.y = GetLittleEndian(static_cast(floor(node->y))); + raw.dx = GetLittleEndian(static_cast(floor(node->dx))); + raw.dy = GetLittleEndian(static_cast(floor(node->dy))); + + raw.b1.minx = GetLittleEndian(static_cast(node->r.bounds.minx)); + raw.b1.miny = GetLittleEndian(static_cast(node->r.bounds.miny)); + raw.b1.maxx = GetLittleEndian(static_cast(node->r.bounds.maxx)); + raw.b1.maxy = GetLittleEndian(static_cast(node->r.bounds.maxy)); + + raw.b2.minx = GetLittleEndian(static_cast(node->l.bounds.minx)); + raw.b2.miny = GetLittleEndian(static_cast(node->l.bounds.miny)); + raw.b2.maxx = GetLittleEndian(static_cast(node->l.bounds.maxx)); + raw.b2.maxy = GetLittleEndian(static_cast(node->l.bounds.maxy)); + + if (node->r.node) + { + raw.right = GetLittleEndian(static_cast(node->r.node->index)); + } + else if (node->r.subsec) + { + raw.right = GetLittleEndian(static_cast(node->r.subsec->index | NF_SUBSECTOR)); + } + else + { + PrintLine(LOG_ERROR, "Bad right child in ZDoom node %zu", node->index); + } + + if (node->l.node) + { + raw.left = GetLittleEndian(static_cast(node->l.node->index)); + } + else if (node->l.subsec) + { + raw.left = GetLittleEndian(static_cast(node->l.subsec->index | NF_SUBSECTOR)); + } + else + { + PrintLine(LOG_ERROR, "Bad left child in ZDoom node %zu", node->index); + } + + lump->Write(&raw, sizeof(raw_xnod_node_t)); + + if (HAS_BIT(config.debug, DEBUG_BSP)) + { + PrintLine(LOG_DEBUG, "[%s] %zu Left %08X Right %08X (%f,%f) -> (%f,%f)", __func__, node->index, + GetLittleEndian(raw.left), GetLittleEndian(raw.right), node->x, node->y, node->x + node->dx, node->y + node->dy); + } +} + +static void PutNodes_Xnod(Lump_c *lump, node_t *root) +{ + size_t node_cur_index = 0; + size_t raw_num = GetLittleEndian(lev_nodes.size()); + lump->Write(&raw_num, 4); + + if (root) + { + PutOneNode_Xnod(lump, root, node_cur_index); + } + + if (node_cur_index != lev_nodes.size()) + { + PrintLine(LOG_ERROR, "PutZNodes miscounted (%zu != %zu)", node_cur_index, lev_nodes.size()); + } +} + +static size_t CalcXnodNodesSize(void) +{ + // compute size of the ZDoom format nodes. + // it does not need to be exact, but it *does* need to be bigger + // (or equal) to the actual size of the lump. + + size_t size = 32; // header + a bit extra + + size += 8 + lev_vertices.size() * sizeof(raw_xnod_vertex_t); + size += 4 + lev_subsecs.size() * sizeof(raw_xnod_subsec_t); + size += 4 + lev_segs.size() * sizeof(raw_xgl2_seg_t); + size += 4 + lev_nodes.size() * sizeof(raw_xgl3_node_t); + + return size; +} + +// +// ZDoom format -- XGLN, XGL2, XGL3 +// + +static void PutSegs_Xgln(Lump_c *lump) +{ + size_t raw_num = GetLittleEndian(lev_segs.size()); + lump->Write(&raw_num, 4); + + for (size_t i = 0; i < lev_segs.size(); i++) + { + const seg_t *seg = lev_segs[i]; + + if (seg->index != i) + { + PrintLine(LOG_ERROR, "PutXGL3Segs: seg index mismatch (%zu != %zu)", seg->index, i); + } + + raw_xgln_seg_t raw = {}; + + raw.vertex = GetLittleEndian(VertexIndex_XNOD(seg->start)); + raw.partner = GetLittleEndian(static_cast(seg->partner ? seg->partner->index : NO_INDEX)); + raw.linedef = GetLittleEndian(static_cast(seg->linedef ? seg->linedef->index : NO_INDEX)); + raw.side = seg->side; + + lump->Write(&raw, sizeof(raw_xgln_seg_t)); + + if (HAS_BIT(config.debug, DEBUG_BSP)) + { + PrintLine(LOG_DEBUG, "[%s] SEG[%zu] v1=%d partner=%d line=%d side=%d", __func__, i, raw.vertex, raw.partner, raw.linedef, + raw.side); + } + } +} + +static void PutSegs_Xgl2(Lump_c *lump) +{ + size_t raw_num = GetLittleEndian(lev_segs.size()); + lump->Write(&raw_num, 4); + + for (size_t i = 0; i < lev_segs.size(); i++) + { + const seg_t *seg = lev_segs[i]; + + if (seg->index != i) + { + PrintLine(LOG_ERROR, "PutXGL3Segs: seg index mismatch (%zu != %zu)", seg->index, i); + } + + raw_xgl2_seg_t raw = {}; + + raw.vertex = GetLittleEndian(VertexIndex_XNOD(seg->start)); + raw.partner = GetLittleEndian(static_cast(seg->partner ? seg->partner->index : NO_INDEX)); + raw.linedef = GetLittleEndian(static_cast(seg->linedef ? seg->linedef->index : NO_INDEX)); + raw.side = seg->side; + + lump->Write(&raw, sizeof(raw_xgl2_seg_t)); + + if (HAS_BIT(config.debug, DEBUG_BSP)) + { + PrintLine(LOG_DEBUG, "[%s] SEG[%zu] v1=%d partner=%d line=%d side=%d", __func__, i, raw.vertex, raw.partner, raw.linedef, + raw.side); + } + } +} + +static void PutOneNode_Xgl3(Lump_c *lump, node_t *node, size_t &node_cur_index) +{ + raw_xgl3_node_t raw; + + if (node->r.node) + { + PutOneNode_Xgl3(lump, node->r.node, node_cur_index); + } + + if (node->l.node) + { + PutOneNode_Xgl3(lump, node->l.node, node_cur_index); + } + + node->index = node_cur_index++; + + raw.x = GetLittleEndian(static_cast(floor(node->x * 65536.0))); + raw.y = GetLittleEndian(static_cast(floor(node->y * 65536.0))); + raw.dx = GetLittleEndian(static_cast(floor(node->dx * 65536.0))); + raw.dy = GetLittleEndian(static_cast(floor(node->dy * 65536.0))); + + raw.b1.minx = GetLittleEndian(static_cast(node->r.bounds.minx)); + raw.b1.miny = GetLittleEndian(static_cast(node->r.bounds.miny)); + raw.b1.maxx = GetLittleEndian(static_cast(node->r.bounds.maxx)); + raw.b1.maxy = GetLittleEndian(static_cast(node->r.bounds.maxy)); + + raw.b2.minx = GetLittleEndian(static_cast(node->l.bounds.minx)); + raw.b2.miny = GetLittleEndian(static_cast(node->l.bounds.miny)); + raw.b2.maxx = GetLittleEndian(static_cast(node->l.bounds.maxx)); + raw.b2.maxy = GetLittleEndian(static_cast(node->l.bounds.maxy)); + + if (node->r.node) + { + raw.right = GetLittleEndian(static_cast(node->r.node->index)); + } + else if (node->r.subsec) + { + raw.right = GetLittleEndian(static_cast(node->r.subsec->index | NF_SUBSECTOR)); + } + else + { + PrintLine(LOG_ERROR, "Bad right child in ZDoom node %zu", node->index); + } + + if (node->l.node) + { + raw.left = GetLittleEndian(static_cast(node->l.node->index)); + } + else if (node->l.subsec) + { + raw.left = GetLittleEndian(static_cast(node->l.subsec->index | NF_SUBSECTOR)); + } + else + { + PrintLine(LOG_ERROR, "Bad left child in ZDoom node %zu", node->index); + } + + lump->Write(&raw, sizeof(raw_xgl3_node_t)); + + if (HAS_BIT(config.debug, DEBUG_BSP)) + { + PrintLine(LOG_DEBUG, "[%s] %zu Left %08X Right %08X (%f,%f) -> (%f,%f)", __func__, node->index, + GetLittleEndian(raw.left), GetLittleEndian(raw.right), node->x, node->y, node->x + node->dx, node->y + node->dy); + } +} + +static void PutNodes_Xgl3(Lump_c *lump, node_t *root) +{ + size_t node_cur_index = 0; + size_t raw_num = GetLittleEndian(lev_nodes.size()); + lump->Write(&raw_num, 4); + + if (root) + { + PutOneNode_Xgl3(lump, root, node_cur_index); + } + + if (node_cur_index != lev_nodes.size()) + { + PrintLine(LOG_ERROR, "PutZNodes miscounted (%zu != %zu)", node_cur_index, lev_nodes.size()); + } +} + +// +// Lump writing procedures +// + +void SaveFormat_Vanilla(node_t *root_node) +{ + // remove all the minisegs from subsectors + NormaliseBspTree(); + // reduce vertex precision for classic DOOM nodes. + // some segs can become "degenerate" after this, and these + // are removed from subsectors. + RoundOffBspTree(); + SortSegs(); + PutVertices_Vanilla(); + PutSegs_Vanilla(); + PutSubsecs_Vanilla(); + PutNodes_Vanilla(root_node); +} + +void SaveFormat_DeepBSPV4(node_t *root_node) +{ + // remove all the minisegs from subsectors + NormaliseBspTree(); + // reduce vertex precision for classic DOOM nodes. + // some segs can become "degenerate" after this, and these + // are removed from subsectors. + RoundOffBspTree(); + SortSegs(); + PutVertices_Vanilla(); + PutSegs_DeepBSPV4(); + PutSubsecs_DeepBSPV4(); + PutNodes_DeepBSPV4(root_node); +} + +void SaveFormat_Xnod(node_t *root_node) +{ + CreateLevelLump("SEGS")->Finish(); + CreateLevelLump("SSECTORS")->Finish(); + // remove all the minisegs from subsectors + NormaliseBspTree(); + SortSegs(); + + Lump_c *lump = CreateLevelLump("NODES", CalcXnodNodesSize()); + lump->Write(BSP_MAGIC_XNOD, 4); + PutVertices_Xnod(lump); + PutSubsecs_Xnod(lump); + PutSegs_Xnod(lump); + PutNodes_Xnod(lump, root_node); + + lump->Finish(); + lump = nullptr; +} + +void SaveFormat_Xgln(node_t *root_node) +{ + // leave SEGS empty + CreateLevelLump("SEGS")->Finish(); + + SortSegs(); + + // WISH : compute a max_size + Lump_c *lump = CreateLevelLump("SSECTORS", CalcXnodNodesSize()); + lump->Write(BSP_MAGIC_XGLN, 4); + PutVertices_Xnod(lump); + PutSubsecs_Xnod(lump); + PutSegs_Xgln(lump); + PutNodes_Xnod(lump, root_node); + + lump->Finish(); + lump = nullptr; + + // leave NODES empty + CreateLevelLump("NODES")->Finish(); +} + +void SaveFormat_Xgl2(node_t *root_node) +{ + // leave SEGS empty + CreateLevelLump("SEGS")->Finish(); + + SortSegs(); + + // WISH : compute a max_size + Lump_c *lump = CreateLevelLump("SSECTORS", CalcXnodNodesSize()); + lump->Write(BSP_MAGIC_XGL2, 4); + PutVertices_Xnod(lump); + PutSubsecs_Xnod(lump); + PutSegs_Xgl2(lump); + PutNodes_Xnod(lump, root_node); + + lump->Finish(); + lump = nullptr; + + // leave NODES empty + CreateLevelLump("NODES")->Finish(); +} + +void SaveFormat_Xgl3(node_t *root_node) +{ + // leave SEGS empty + CreateLevelLump("SEGS")->Finish(); + + SortSegs(); + + // WISH : compute a max_size + Lump_c *lump = CreateLevelLump("SSECTORS", CalcXnodNodesSize()); + lump->Write(BSP_MAGIC_XGL3, 4); + PutVertices_Xnod(lump); + PutSubsecs_Xnod(lump); + PutSegs_Xgl2(lump); + PutNodes_Xgl3(lump, root_node); + + lump->Finish(); + lump = nullptr; + + // leave NODES empty + CreateLevelLump("NODES")->Finish(); +} diff --git a/src/core.hpp b/src/core.hpp index f54cfcc..d8bffba 100644 --- a/src/core.hpp +++ b/src/core.hpp @@ -30,8 +30,6 @@ constexpr auto PROJECT_COMPANY = "Guilherme Miranda, et al"; constexpr auto PROJECT_COPYRIGHT = "Copyright (C) 1994-2026"; constexpr auto PROJECT_LICENSE = "GNU General Public License, version 2"; -constexpr auto PROJECT_NAME = "ELFBSP"; -constexpr auto PROJECT_VERSION = "v1.1"; constexpr auto PROJECT_STRING = "ELFBSP v1.1"; /* @@ -159,6 +157,11 @@ template constexpr T GetBigEndian(T value) } } +template constexpr void RaiseValue(T &var, T value) +{ + var = std::max(var, value); +} + // sized types using byte = uint8_t; using args_t = uint8_t[5]; @@ -697,11 +700,54 @@ using raw_hexen_thing_t = struct raw_hexen_thing_s // BSP TREE STRUCTURES //------------------------------------------------------------------------ -constexpr auto DEEP_MAGIC = "xNd4\0\0\0\0"; -constexpr auto XNOD_MAGIC = "XNOD"; -constexpr auto XGLN_MAGIC = "XGLN"; -constexpr auto XGL2_MAGIC = "XGL2"; -constexpr auto XGL3_MAGIC = "XGL3"; +// +// We do not write ZIP-compressed ZDoom nodes +// +using bsp_type_t = enum bsp_type_e : uint8_t +{ + BSP_VANILLA, + BSP_DEEPBSPV4, + BSP_XNOD, + BSP_XGLN, + BSP_XGL2, + BSP_XGL3, + + BSP_MIN = BSP_VANILLA, + BSP_MAX = BSP_XGL3, +}; + +// Obviously, vanilla did not include any magic headers +constexpr auto BSP_MAGIC_DEEPBSPV4 = "xNd4\0\0\0\0"; +constexpr auto BSP_MAGIC_XNOD = "XNOD"; +constexpr auto BSP_MAGIC_XGLN = "XGLN"; +constexpr auto BSP_MAGIC_XGL2 = "XGL2"; +constexpr auto BSP_MAGIC_XGL3 = "XGL3"; + +// Upper-most bit is used for distinguishing sub-sectors, i.e tree leaves +constexpr uint16_t NF_SUBSECTOR_VANILLA = UINT16_C(0x8000); +constexpr uint32_t NF_SUBSECTOR = UINT32_C(0x80000000); + +// +// Binary format upper bounds. +// Some of these, namely the BSP tree indexes, are addressed by using later formats, such as DeepBSPV4, etc +// +// * No known ports seem to reserve 0xFFFF for vertices +// * The linedef index 0xFFFF is reserved for minisegs in XGLN/XGL2/XGL3 nodes +// * The sidedef index 0xFFFF is reserved to mean "no side" in DOOM map format +// * No known ports seem to reserve 0xFFFF for sectors +// +// * Nodes and subsectors share the same index slot, distinguished only by the high bit +// * No known additional limits exist for segments +// + +constexpr size_t LIMIT_VERT = UINT16_MAX; +constexpr size_t LIMIT_LINE = UINT16_MAX - 1; +constexpr size_t LIMIT_SIDE = UINT16_MAX - 1; +constexpr size_t LIMIT_SECTOR = UINT16_MAX; + +constexpr size_t LIMIT_NODE = INT16_MAX; +constexpr size_t LIMIT_SUBSEC = INT16_MAX; +constexpr size_t LIMIT_SEG = UINT16_MAX; // // Vanilla blockmap @@ -723,7 +769,7 @@ using raw_blockmap_header_t = struct raw_blockmap_header_s // // Vanilla BSP // -using raw_node_t = struct raw_node_s +using raw_node_vanilla_t = struct raw_node_vanilla_s { int16_t x, y; // starting point int16_t dx, dy; // offset to ending point @@ -731,27 +777,27 @@ using raw_node_t = struct raw_node_s uint16_t right, left; // children: Node or SSector (if high bit is set) } PACKEDATTR; -using raw_subsec_t = struct raw_subsec_s +using raw_subsec_vanilla_t = struct raw_subsec_vanilla_s { uint16_t num; // number of Segs in this Sub-Sector uint16_t first; // first Seg } PACKEDATTR; -using raw_seg_t = struct raw_seg_s +using raw_seg_vanilla_t = struct raw_seg_vanilla_s { uint16_t start; // from this vertex... uint16_t end; // ... to this vertex uint16_t angle; // angle (0 = east, 16384 = north, ...) uint16_t linedef; // linedef that this seg goes along uint16_t flip; // true if not the same direction as linedef - uint16_t dist; // distance from starting point + int16_t dist; // distance from starting point } PACKEDATTR; // // DeepSea BSP // * compared to vanilla, some types were raise to 32bit // -using raw_node_deep_t = struct raw_node_deep_s +using raw_node_deepbspv4_t = struct raw_node_deepbspv4_s { int16_t x, y; // starting point int16_t dx, dy; // offset to ending point @@ -759,20 +805,20 @@ using raw_node_deep_t = struct raw_node_deep_s uint32_t right, left; // children: Node or SSector (if high bit is set) } PACKEDATTR; -using raw_subsec_deep_t = struct raw_subsec_deep_s +using raw_subsec_deepbspv4_t = struct raw_subsec_deepbspv4_s { uint16_t num; // number of Segs in this Sub-Sector uint32_t first; // first Seg } PACKEDATTR; -using raw_seg_deep_t = struct raw_seg_deep_s +using raw_seg_deepbspv4_t = struct raw_seg_deepbspv4_s { uint32_t start; // from this vertex... uint32_t end; // ... to this vertex uint16_t angle; // angle (0 = east, 16384 = north, ...) uint16_t linedef; // linedef that this seg goes along uint16_t flip; // true if not the same direction as linedef - uint16_t dist; // distance from starting point + int16_t dist; // distance from starting point } PACKEDATTR; // @@ -811,21 +857,18 @@ using raw_xnod_seg_t = struct raw_xnod_seg_s uint8_t side; // 0 if on right of linedef, 1 if on left } PACKEDATTR; -// XGLN segs use the same type definition as XNOD segs, with slightly -// different semantics for mini-segs - using raw_xgln_seg_t = struct raw_xgln_seg_s { - uint32_t vertex; // from this vertex... - uint32_t partner; // ... to this vertex + uint32_t vertex; // from this vertex ... to the next + uint32_t partner; // partner seg, unused by most ports outside of U/G/ZDoom uint16_t linedef; // linedef that this seg goes along, or NO_INDEX uint8_t side; // 0 if on right of linedef, 1 if on left } PACKEDATTR; using raw_xgl2_seg_t = struct raw_xgl2_seg_s { - uint32_t vertex; // from this vertex... - uint32_t partner; // ... to this vertex + uint32_t vertex; // from this vertex ... to the next + uint32_t partner; // partner seg, unused by most ports outside of U/G/ZDoom uint32_t linedef; // linedef that this seg goes along, or NO_INDEX uint8_t side; // 0 if on right of linedef, 1 if on left } PACKEDATTR; @@ -1369,8 +1412,7 @@ struct buildinfo_s // write out CSV for data analysis and visualization bool analysis = false; - bool force_xnod = false; - bool ssect_xgl3 = false; + bsp_type_t bsp_type = bsp_type_t::BSP_VANILLA; double split_cost = SPLIT_COST_DEFAULT; @@ -1411,6 +1453,8 @@ using build_result_t = enum build_result_e BUILD_LumpOverflow }; +extern bool lev_overflows; + // attempt to open a wad. on failure, the FatalError method in the // buildinfo_t interface is called. void OpenWad(const char *filename); diff --git a/src/elfbsp.cpp b/src/elfbsp.cpp index 52d51c3..9dedda2 100644 --- a/src/elfbsp.cpp +++ b/src/elfbsp.cpp @@ -449,18 +449,13 @@ void ParseShortArgument(const char *arg) case 'f': config.fast = true; continue; - case 'x': - config.force_xnod = true; - continue; - case 's': - config.ssect_xgl3 = true; - continue; case 'a': config.analysis = true; continue; case 'm': case 'o': + case 't': PrintLine(LOG_ERROR, "cannot use option '-%c' like that", c); return; @@ -601,13 +596,22 @@ int32_t ParseLongArgument(const char *name, const int32_t argc, const char *argv used = 1; } - else if (strcmp(name, "--xnod") == 0) - { - config.force_xnod = true; - } - else if (strcmp(name, "--ssect") == 0) + else if (strcmp(name, "--type") == 0) { - config.ssect_xgl3 = true; + if (argc < 1 || !isdigit(argv[0][0])) + { + PrintLine(LOG_ERROR, "missing value for '--type' option"); + } + + int32_t val = std::stoi(argv[0]); + + if (val < BSP_MIN || val > BSP_MAX) + { + PrintLine(LOG_ERROR, "illegal value for '--type' option"); + } + + RaiseValue(config.bsp_type, static_cast(val)); + used = 1; } else if (strcmp(name, "--cost") == 0) { @@ -701,6 +705,10 @@ void ParseCommandLine(int32_t argc, const char *argv[]) } // handle short args which are isolate and require a value + if (strcmp(arg, "-t") == 0) + { + arg = "--type"; + } if (strcmp(arg, "-c") == 0) { arg = "--cost"; diff --git a/src/level.cpp b/src/level.cpp index 7c9fbe3..1fcfa34 100644 --- a/src/level.cpp +++ b/src/level.cpp @@ -779,9 +779,8 @@ size_t lev_current_idx; size_t lev_current_start; map_format_e lev_format; -bool lev_force_xnod; -bool lev_overflows; +bool lev_overflows = false; // objects of loaded level, and stuff we've built std::vector lev_vertices; @@ -1368,63 +1367,6 @@ static void GetLinedefsHexen(void) } } -static inline uint16_t VanillaSegDist(const seg_t *seg) -{ - double lx = seg->side ? seg->linedef->end->x : seg->linedef->start->x; - double ly = seg->side ? seg->linedef->end->y : seg->linedef->start->y; - - // use the "true" starting coord (as stored in the wad) - double sx = round(seg->start->x); - double sy = round(seg->start->y); - - return static_cast(floor(hypot(sx - lx, sy - ly) + 0.5)); -} - -static inline short_angle_t VanillaSegAngle(const seg_t *seg) -{ - // compute the "true" delta - double dx = round(seg->end->x) - round(seg->start->x); - double dy = round(seg->end->y) - round(seg->start->y); - - double angle = ComputeAngle(dx, dy); - - if (angle < 0) - { - angle += 360.0; - } - - short_angle_t result = short_angle_t(floor(angle * 65536.0 / 360.0 + 0.5)); - - if (lev_format != MapFormat_Doom) - { - return result; - } - - // -Elf- ZokumBSP - // 1080 => Additive degrees stored in tag - // 1081 => Set to degrees stored in tag - // 1082 => Additive BAM stored in tag - // 1083 => Set to BAM stored in tag - if (seg->linedef->special == Special_RotateDegrees) - { - result += DegreesToShortBAM(static_cast(seg->linedef->tag)); - } - else if (seg->linedef->special == Special_RotateDegreesHard) - { - result = DegreesToShortBAM(static_cast(seg->linedef->tag)); - } - else if (seg->linedef->special == Special_RotateAngleT) - { - result += static_cast(seg->linedef->tag); - } - else if (seg->linedef->special == Special_RotateAngleTHard) - { - result = static_cast(seg->linedef->tag); - } - - return result; -} - /* ----- UDMF reading routines ------------------------- */ static constexpr uint32_t UDMF_THING = 1; @@ -1771,611 +1713,49 @@ void ParseUDMF(void) /* ----- writing routines ------------------------------ */ -static void MarkOverflow(int flags) -{ - // flags are ignored - lev_overflows = true; -} - -static void PutVertices(void) -{ - // this size is worst-case scenario - size_t size = lev_vertices.size() * sizeof(raw_vertex_t); - Lump_c *lump = CreateLevelLump("VERTEXES", size); - - size_t count = 0; - for (size_t i = 0; i < lev_vertices.size(); i++) - { - raw_vertex_t raw; - - const vertex_t *vert = lev_vertices[i]; - - if (vert->is_new) - { - continue; - } - - raw.x = GetLittleEndian(static_cast(floor(vert->x))); - raw.y = GetLittleEndian(static_cast(floor(vert->y))); - - lump->Write(&raw, sizeof(raw)); - - count++; - } - - lump->Finish(); - - if (count != num_old_vert) - { - PrintLine(LOG_ERROR, "PutVertices miscounted (%zu != %zu)", count, num_old_vert); - } - - if (count > 65534) - { - PrintLine(LOG_NORMAL, "FAILURE: Number of vertices has overflowed."); - MarkOverflow(LIMIT_VERTEXES); - } -} - -static inline uint16_t VertexIndex16Bit(const vertex_t *v) -{ - if (v->is_new) - { - return static_cast(v->index | 0x8000U); - } - - return static_cast(v->index); -} - -static inline uint32_t VertexIndex_XNOD(const vertex_t *v) -{ - if (v->is_new) - { - return static_cast(num_old_vert + v->index); - } - - return static_cast(v->index); -} - -static void PutSegs(void) -{ - // this size is worst-case scenario - size_t size = lev_segs.size() * sizeof(raw_seg_t); - - Lump_c *lump = CreateLevelLump("SEGS", size); - - for (size_t i = 0; i < lev_segs.size(); i++) - { - raw_seg_t raw; - - const seg_t *seg = lev_segs[i]; - - raw.start = GetLittleEndian(VertexIndex16Bit(seg->start)); - raw.end = GetLittleEndian(VertexIndex16Bit(seg->end)); - raw.angle = GetLittleEndian(VanillaSegAngle(seg)); - raw.linedef = GetLittleEndian(static_cast(seg->linedef->index)); - raw.flip = GetLittleEndian(seg->side); - raw.dist = GetLittleEndian(VanillaSegDist(seg)); - - // -Elf- ZokumBSP - if ((seg->linedef->dont_render_back && seg->side) || (seg->linedef->dont_render_front && !seg->side)) - { - raw = {}; - } - - lump->Write(&raw, sizeof(raw)); - - if (HAS_BIT(config.debug, DEBUG_BSP)) - { - PrintLine(LOG_DEBUG, "[%s] %zu Vert %04X->%04X Line %04X %s Angle %04X (%1.1f,%1.1f) -> (%1.1f,%1.1f)", __func__, - seg->index, GetLittleEndian(raw.start), GetLittleEndian(raw.end), GetLittleEndian(raw.linedef), - seg->side ? "L" : "R", GetLittleEndian(raw.angle), seg->start->x, seg->start->y, seg->end->x, seg->end->y); - } - } - - lump->Finish(); - - if (lev_segs.size() > 65534) - { - PrintLine(LOG_NORMAL, "FAILURE: Number of segs has overflowed."); - MarkOverflow(LIMIT_SEGS); - } -} - -static void PutSubsecs(void) -{ - size_t size = lev_subsecs.size() * sizeof(raw_subsec_t); - - Lump_c *lump = CreateLevelLump("SSECTORS", size); - - for (size_t i = 0; i < lev_subsecs.size(); i++) - { - raw_subsec_t raw; - - const subsec_t *sub = lev_subsecs[i]; - - raw.first = GetLittleEndian(static_cast(sub->seg_list->index)); - raw.num = GetLittleEndian(static_cast(sub->seg_count)); - - lump->Write(&raw, sizeof(raw)); - - if (HAS_BIT(config.debug, DEBUG_BSP)) - { - PrintLine(LOG_DEBUG, "[%s] %zu First %04X Num %04X", __func__, sub->index, GetLittleEndian(raw.first), - GetLittleEndian(raw.num)); - } - } - - if (lev_subsecs.size() > 32767) - { - PrintLine(LOG_NORMAL, "FAILURE: Number of subsectors has overflowed."); - MarkOverflow(LIMIT_SSECTORS); - } - - lump->Finish(); -} - -static size_t node_cur_index; - -static void PutOneNode(node_t *node, Lump_c *lump) -{ - if (node->r.node) - { - PutOneNode(node->r.node, lump); - } - - if (node->l.node) - { - PutOneNode(node->l.node, lump); - } - - node->index = node_cur_index++; - - raw_node_t raw; - - // note that x/y/dx/dy are always integral in non-UDMF maps - raw.x = GetLittleEndian(static_cast(floor(node->x))); - raw.y = GetLittleEndian(static_cast(floor(node->y))); - raw.dx = GetLittleEndian(static_cast(floor(node->dx))); - raw.dy = GetLittleEndian(static_cast(floor(node->dy))); - - raw.b1.minx = GetLittleEndian(static_cast(node->r.bounds.minx)); - raw.b1.miny = GetLittleEndian(static_cast(node->r.bounds.miny)); - raw.b1.maxx = GetLittleEndian(static_cast(node->r.bounds.maxx)); - raw.b1.maxy = GetLittleEndian(static_cast(node->r.bounds.maxy)); - - raw.b2.minx = GetLittleEndian(static_cast(node->l.bounds.minx)); - raw.b2.miny = GetLittleEndian(static_cast(node->l.bounds.miny)); - raw.b2.maxx = GetLittleEndian(static_cast(node->l.bounds.maxx)); - raw.b2.maxy = GetLittleEndian(static_cast(node->l.bounds.maxy)); - - if (node->r.node) - { - raw.right = GetLittleEndian(static_cast(node->r.node->index)); - } - else if (node->r.subsec) - { - raw.right = GetLittleEndian(static_cast(node->r.subsec->index | 0x8000)); - } - else - { - PrintLine(LOG_ERROR, "Bad right child in node %zu", node->index); - } - - if (node->l.node) - { - raw.left = GetLittleEndian(static_cast(node->l.node->index)); - } - else if (node->l.subsec) - { - raw.left = GetLittleEndian(static_cast(node->l.subsec->index | 0x8000)); - } - else - { - PrintLine(LOG_ERROR, "Bad left child in node %zu", node->index); - } - - lump->Write(&raw, sizeof(raw)); - - if (HAS_BIT(config.debug, DEBUG_BSP)) - { - PrintLine(LOG_DEBUG, "[%s] %zu Left %04X Right %04X (%1.1f,%1.1f) -> (%1.1f,%1.1f)", __func__, node->index, - GetLittleEndian(raw.left), GetLittleEndian(raw.right), node->x, node->y, node->x + node->dx, node->y + node->dy); - } -} - -static void PutNodes(node_t *root) -{ - size_t struct_size = sizeof(raw_node_t); - - // this can be bigger than the actual size, but never smaller - size_t max_size = (lev_nodes.size() + 1) * struct_size; - - Lump_c *lump = CreateLevelLump("NODES", max_size); - - node_cur_index = 0; - - if (root != nullptr) - { - PutOneNode(root, lump); - } - - lump->Finish(); - - if (node_cur_index != lev_nodes.size()) - { - PrintLine(LOG_ERROR, "PutNodes miscounted (%zu != %zu)", node_cur_index, lev_nodes.size()); - } - - if (node_cur_index > 32767) - { - PrintLine(LOG_NORMAL, "FAILURE: Number of nodes has overflowed."); - MarkOverflow(LIMIT_NODES); - } -} +// +// Check Limits +// -static void CheckLimits(void) +static void CheckBinaryFormatLimits(void) { - // this could potentially be 65536, since there are no reserved values - // for sectors, but there may be source ports or tools treating 0xFFFF - // as a special value, so we are extra cautious here (and in some of - // the other checks below, like the vertex counts). - if (lev_sectors.size() > 65535) + if (lev_sectors.size() > LIMIT_SECTOR) { PrintLine(LOG_NORMAL, "FAILURE: Map has too many sectors."); - MarkOverflow(LIMIT_SECTORS); + lev_overflows = true; } - // the sidedef 0xFFFF is reserved to mean "no side" in DOOM map format - if (lev_sidedefs.size() > 65535) + if (lev_sidedefs.size() > LIMIT_SIDE) { PrintLine(LOG_NORMAL, "FAILURE: Map has too many sidedefs."); - MarkOverflow(LIMIT_SIDEDEFS); + lev_overflows = true; } - // the linedef 0xFFFF is reserved for minisegs in GL nodes - if (lev_linedefs.size() > 65535) + if (lev_linedefs.size() > LIMIT_LINE) { PrintLine(LOG_NORMAL, "FAILURE: Map has too many linedefs."); - MarkOverflow(LIMIT_LINEDEFS); - } - - if (!(config.force_xnod || config.ssect_xgl3)) - { - if (num_old_vert > 32767 || num_new_vert > 32767 || lev_segs.size() > 32767 || lev_nodes.size() > 32767) - { - PrintLine(LOG_NORMAL, "WARNING: Forcing XNOD format nodes due to overflows."); - config.total_warnings++; - lev_force_xnod = true; - } - } -} - -struct Compare_seg_pred -{ - inline bool operator()(const seg_t *A, const seg_t *B) const - { - return A->index < B->index; - } -}; - -void SortSegs(void) -{ - // sort segs into ascending index - std::sort(lev_segs.begin(), lev_segs.end(), Compare_seg_pred()); - - // remove unwanted segs - while (lev_segs.size() > 0 && lev_segs.back()->index == SEG_IS_GARBAGE) - { - UtilFree(lev_segs.back()); - lev_segs.pop_back(); - } -} - -/* ----- ZDoom format writing --------------------------- */ - -static void PutXnodVertices(Lump_c *lump) -{ - size_t orgverts = GetLittleEndian(num_old_vert); - size_t newverts = GetLittleEndian(num_new_vert); - - lump->Write(&orgverts, 4); - lump->Write(&newverts, 4); - - size_t count = 0; - for (size_t i = 0; i < lev_vertices.size(); i++) - { - raw_xnod_vertex_t raw; - - const vertex_t *vert = lev_vertices[i]; - - if (!vert->is_new) - { - continue; - } - - raw.x = GetLittleEndian(static_cast(floor(vert->x * 65536.0))); - raw.y = GetLittleEndian(static_cast(floor(vert->y * 65536.0))); - - lump->Write(&raw, sizeof(raw)); - - count++; - } - - if (count != num_new_vert) - { - PrintLine(LOG_ERROR, "PutZVertices miscounted (%zu != %zu)", count, num_new_vert); - } -} - -static void PutXnodSubsecs(Lump_c *lump) -{ - size_t raw_num = GetLittleEndian(lev_subsecs.size()); - lump->Write(&raw_num, 4); - - size_t cur_seg_index = 0; - for (size_t i = 0; i < lev_subsecs.size(); i++) - { - const subsec_t *sub = lev_subsecs[i]; - - raw_num = GetLittleEndian(sub->seg_count); - lump->Write(&raw_num, 4); - - // sanity check the seg index values - size_t count = 0; - for (const seg_t *seg = sub->seg_list; seg; seg = seg->next, cur_seg_index++) - { - if (cur_seg_index != seg->index) - { - PrintLine(LOG_ERROR, "PutZSubsecs: seg index mismatch in sub %zu (%zu != %zu)", i, cur_seg_index, seg->index); - } - - count++; - } - - if (count != sub->seg_count) - { - PrintLine(LOG_ERROR, "PutZSubsecs: miscounted segs in sub %zu (%zu != %zu)", i, count, sub->seg_count); - } - } - - if (cur_seg_index != lev_segs.size()) - { - PrintLine(LOG_ERROR, "PutZSubsecs miscounted segs (%zu != %zu)", cur_seg_index, lev_segs.size()); - } -} - -static void PutXnodSegs(Lump_c *lump) -{ - size_t raw_num = GetLittleEndian(lev_segs.size()); - lump->Write(&raw_num, 4); - - for (size_t i = 0; i < lev_segs.size(); i++) - { - const seg_t *seg = lev_segs[i]; - - if (seg->index != i) - { - PrintLine(LOG_ERROR, "PutZSegs: seg index mismatch (%zu != %zu)", seg->index, i); - } - - raw_xnod_seg_t raw = {}; - - raw.start = GetLittleEndian(VertexIndex_XNOD(seg->start)); - raw.end = GetLittleEndian(VertexIndex_XNOD(seg->end)); - raw.linedef = GetLittleEndian(static_cast(seg->linedef->index)); - raw.side = seg->side; - lump->Write(&raw, sizeof(raw)); - } -} - -static void PutXgl2Segs(Lump_c *lump) -{ - size_t raw_num = GetLittleEndian(lev_segs.size()); - lump->Write(&raw_num, 4); - - for (size_t i = 0; i < lev_segs.size(); i++) - { - const seg_t *seg = lev_segs[i]; - - if (seg->index != i) - { - PrintLine(LOG_ERROR, "PutXGL3Segs: seg index mismatch (%zu != %zu)", seg->index, i); - } - - raw_xgl2_seg_t raw = {}; - - raw.vertex = GetLittleEndian(VertexIndex_XNOD(seg->start)); - raw.partner = GetLittleEndian(static_cast(seg->partner ? seg->partner->index : NO_INDEX)); - raw.linedef = GetLittleEndian(static_cast(seg->linedef ? seg->linedef->index : NO_INDEX)); - raw.side = seg->side; - - lump->Write(&raw, sizeof(raw)); - - if (HAS_BIT(config.debug, DEBUG_BSP)) - { - PrintLine(LOG_DEBUG, "[%s] SEG[%zu] v1=%d partner=%d line=%d side=%d", __func__, i, raw.vertex, raw.partner, raw.linedef, - raw.side); - } - } -} - -static void PutOneXnodNode(Lump_c *lump, node_t *node, bool xgl3) -{ - raw_xgl3_node_t raw; - - if (node->r.node) - { - PutOneXnodNode(lump, node->r.node, xgl3); - } - - if (node->l.node) - { - PutOneXnodNode(lump, node->l.node, xgl3); - } - - node->index = node_cur_index++; - - if (xgl3) - { - int32_t x = GetLittleEndian(static_cast(floor(node->x * 65536.0))); - int32_t y = GetLittleEndian(static_cast(floor(node->y * 65536.0))); - int32_t dx = GetLittleEndian(static_cast(floor(node->dx * 65536.0))); - int32_t dy = GetLittleEndian(static_cast(floor(node->dy * 65536.0))); - - lump->Write(&x, 4); - lump->Write(&y, 4); - lump->Write(&dx, 4); - lump->Write(&dy, 4); - } - else - { - raw.x = GetLittleEndian(static_cast(floor(node->x))); - raw.y = GetLittleEndian(static_cast(floor(node->y))); - raw.dx = GetLittleEndian(static_cast(floor(node->dx))); - raw.dy = GetLittleEndian(static_cast(floor(node->dy))); - - lump->Write(&raw.x, 2); - lump->Write(&raw.y, 2); - lump->Write(&raw.dx, 2); - lump->Write(&raw.dy, 2); - } - - raw.b1.minx = GetLittleEndian(static_cast(node->r.bounds.minx)); - raw.b1.miny = GetLittleEndian(static_cast(node->r.bounds.miny)); - raw.b1.maxx = GetLittleEndian(static_cast(node->r.bounds.maxx)); - raw.b1.maxy = GetLittleEndian(static_cast(node->r.bounds.maxy)); - - raw.b2.minx = GetLittleEndian(static_cast(node->l.bounds.minx)); - raw.b2.miny = GetLittleEndian(static_cast(node->l.bounds.miny)); - raw.b2.maxx = GetLittleEndian(static_cast(node->l.bounds.maxx)); - raw.b2.maxy = GetLittleEndian(static_cast(node->l.bounds.maxy)); - - lump->Write(&raw.b1, sizeof(raw.b1)); - lump->Write(&raw.b2, sizeof(raw.b2)); - - if (node->r.node) - { - raw.right = GetLittleEndian(static_cast(node->r.node->index)); - } - else if (node->r.subsec) - { - raw.right = GetLittleEndian(static_cast(node->r.subsec->index | 0x80000000U)); - } - else - { - PrintLine(LOG_ERROR, "Bad right child in ZDoom node %zu", node->index); - } - - if (node->l.node) - { - raw.left = GetLittleEndian(static_cast(node->l.node->index)); - } - else if (node->l.subsec) - { - raw.left = GetLittleEndian(static_cast(node->l.subsec->index | 0x80000000U)); - } - else - { - PrintLine(LOG_ERROR, "Bad left child in ZDoom node %zu", node->index); - } - - lump->Write(&raw.right, 4); - lump->Write(&raw.left, 4); - - if (HAS_BIT(config.debug, DEBUG_BSP)) - { - PrintLine(LOG_DEBUG, "[%s] %zu Left %08X Right %08X (%f,%f) -> (%f,%f)", __func__, node->index, - GetLittleEndian(raw.left), GetLittleEndian(raw.right), node->x, node->y, node->x + node->dx, node->y + node->dy); - } -} - -static void PutXnodNodes(Lump_c *lump, node_t *root) -{ - size_t raw_num = GetLittleEndian(lev_nodes.size()); - lump->Write(&raw_num, 4); - - node_cur_index = 0; - - if (root) - { - PutOneXnodNode(lump, root, false); - } - - if (node_cur_index != lev_nodes.size()) - { - PrintLine(LOG_ERROR, "PutZNodes miscounted (%zu != %zu)", node_cur_index, lev_nodes.size()); + lev_overflows = true; } } -static void PutXgl3Nodes(Lump_c *lump, node_t *root) +bsp_type_t CheckFormatBSP(void) { - size_t raw_num = GetLittleEndian(lev_nodes.size()); - lump->Write(&raw_num, 4); - - node_cur_index = 0; - - if (root) + if (lev_vertices.size() > LIMIT_VERT) { - PutOneXnodNode(lump, root, true); + PrintLine(LOG_NORMAL, "WARNING: Vertex overflow. Forcing XNOD node format."); + config.total_warnings++; + return BSP_XNOD; } - if (node_cur_index != lev_nodes.size()) + if (lev_vertices.size() <= LIMIT_VERT + && (lev_segs.size() > LIMIT_SEG || lev_subsecs.size() > LIMIT_SUBSEC || lev_nodes.size() > LIMIT_NODE)) { - PrintLine(LOG_ERROR, "PutZNodes miscounted (%zu != %zu)", node_cur_index, lev_nodes.size()); + PrintLine(LOG_NORMAL, "WARNING: BSP overflow. Forcing DeepBSPV4 node format."); + config.total_warnings++; + return BSP_DEEPBSPV4; } -} - -static size_t CalcXnodNodesSize(void) -{ - // compute size of the ZDoom format nodes. - // it does not need to be exact, but it *does* need to be bigger - // (or equal) to the actual size of the lump. - - size_t size = 32; // header + a bit extra - - size += 8 + lev_vertices.size() * sizeof(raw_xnod_vertex_t); - size += 4 + lev_subsecs.size() * sizeof(raw_xnod_subsec_t); - size += 4 + lev_segs.size() * sizeof(raw_xnod_seg_t); - size += 4 + lev_nodes.size() * sizeof(raw_xnod_node_t); - - return size; -} -void SaveXnodFormat(node_t *root_node) -{ - SortSegs(); - - size_t max_size = CalcXnodNodesSize(); - - Lump_c *lump = CreateLevelLump("NODES", max_size); - - lump->Write(XNOD_MAGIC, 4); - - PutXnodVertices(lump); - PutXnodSubsecs(lump); - PutXnodSegs(lump); - PutXnodNodes(lump, root_node); - - lump->Finish(); - lump = nullptr; -} - -static void SaveXgl3Format(Lump_c *lump, node_t *root_node) -{ - SortSegs(); - - // WISH : compute a max_size - - lump->Write(XGL3_MAGIC, 4); - - PutXnodVertices(lump); - PutXnodSubsecs(lump); - PutXgl2Segs(lump); - PutXgl3Nodes(lump, root_node); - - lump->Finish(); - lump = nullptr; + return BSP_VANILLA; } /* ----- whole-level routines --------------------------- */ @@ -2479,7 +1859,7 @@ static void AddMissingLump(const char *name, const char *after) cur_wad->AddLump(name)->Finish(); } -build_result_e SaveLevel(node_t *root_node) +build_result_e SaveBinaryFormatLevel(node_t *root_node) { // Note: root_node may be nullptr @@ -2493,47 +1873,44 @@ build_result_e SaveLevel(node_t *root_node) AddMissingLump("REJECT", "SECTORS"); AddMissingLump("BLOCKMAP", "REJECT"); - // user preferences - lev_force_xnod |= config.force_xnod; - // check for overflows... - // this sets the force_xxx vars if certain limits are breached - CheckLimits(); - - /* --- Normal nodes --- */ - if (config.ssect_xgl3 && num_real_lines > 0) - { - // leave SEGS empty - CreateLevelLump("SEGS")->Finish(); - Lump_c *lump = CreateLevelLump("SSECTORS"); - SaveXgl3Format(lump, root_node); - CreateLevelLump("NODES")->Finish(); - } - else if (lev_force_xnod && num_real_lines > 0) - { - CreateLevelLump("SEGS")->Finish(); - CreateLevelLump("SSECTORS")->Finish(); - // remove all the mini-segs from subsectors - NormaliseBspTree(); - SaveXnodFormat(root_node); - } - else - { - // remove all the mini-segs from subsectors - NormaliseBspTree(); - - // reduce vertex precision for classic DOOM nodes. - // some segs can become "degenerate" after this, and these - // are removed from subsectors. - RoundOffBspTree(); - - SortSegs(); + CheckBinaryFormatLimits(); - PutVertices(); + bsp_type_t level_type = CheckFormatBSP(); + level_type = std::max(config.bsp_type, level_type); - PutSegs(); - PutSubsecs(); - PutNodes(root_node); + switch (level_type) + { + case BSP_XGL3: + { + SaveFormat_Xgl3(root_node); + break; + } + case BSP_XGL2: + { + SaveFormat_Xgl2(root_node); + break; + } + case BSP_XGLN: + { + SaveFormat_Xgln(root_node); + break; + } + case BSP_XNOD: + { + SaveFormat_Xnod(root_node); + break; + } + case BSP_DEEPBSPV4: + { + SaveFormat_DeepBSPV4(root_node); + break; + } + case BSP_VANILLA: + { + SaveFormat_Vanilla(root_node); + break; + } } PutBlockmap(); @@ -2552,7 +1929,7 @@ build_result_e SaveLevel(node_t *root_node) return BUILD_OK; } -build_result_e SaveUDMF(node_t *root_node) +build_result_e SaveTextMapLevel(node_t *root_node) { cur_wad->BeginWrite(); @@ -2571,7 +1948,7 @@ build_result_e SaveUDMF(node_t *root_node) } else { - SaveXgl3Format(lump, root_node); + SaveFormat_Xgl3(root_node); } // -Elf- @@ -2745,10 +2122,10 @@ build_result_e BuildLevel(size_t lev_idx, const char *filename) { case MapFormat_Doom: case MapFormat_Hexen: - ret = SaveLevel(root_node); + ret = SaveBinaryFormatLevel(root_node); break; case MapFormat_UDMF: - ret = SaveUDMF(root_node); + ret = SaveTextMapLevel(root_node); break; default: break; diff --git a/src/local.hpp b/src/local.hpp index 2b65216..96becfc 100644 --- a/src/local.hpp +++ b/src/local.hpp @@ -204,7 +204,7 @@ struct seg_t linedef_t *linedef; // 0 for right, 1 for left - bool side; + bool side = false; // seg on other side, or nullptr if one-sided. This relationship is // always one-to-one -- if one of the segs is split, the partner seg @@ -214,7 +214,7 @@ struct seg_t // seg index. Only valid once the seg has been added to a // subsector. A negative value means it is invalid -- there // shouldn't be any of these once the BSP tree has been built. - size_t index; + size_t index = NO_INDEX; // when true, this seg has become zero length (integer rounding of the // start and end vertices produces the same location). It should be @@ -346,7 +346,7 @@ struct quadtree_c // 256x512). quadtree_c *subs[2]; - // count of real/mini segs contained in this node AND ALL CHILDREN. + // count of real/minisegs contained in this node AND ALL CHILDREN. size_t real_num; size_t mini_num; @@ -409,15 +409,6 @@ void FreeNodes(void); Lump_c *CreateLevelLump(const char *name, size_t max_size = NO_INDEX); Lump_c *FindLevelLump(const char *name); -/* limit flags, to show what went wrong */ -static constexpr uint32_t LIMIT_VERTEXES = BIT(0); -static constexpr uint32_t LIMIT_SECTORS = BIT(1); -static constexpr uint32_t LIMIT_SIDEDEFS = BIT(2); -static constexpr uint32_t LIMIT_LINEDEFS = BIT(3); -static constexpr uint32_t LIMIT_SEGS = BIT(4); -static constexpr uint32_t LIMIT_SSECTORS = BIT(5); -static constexpr uint32_t LIMIT_NODES = BIT(6); - //------------------------------------------------------------------------ // ANALYZE : Analyzing level structures //------------------------------------------------------------------------ @@ -555,3 +546,10 @@ void NormaliseBspTree(void); // vertices to integer coordinates (for example, removing segs whose // rounded coordinates degenerate to the same point). void RoundOffBspTree(void); + +void SaveFormat_Vanilla(node_t *root_node); +void SaveFormat_DeepBSPV4(node_t *root_node); +void SaveFormat_Xnod(node_t *root_node); +void SaveFormat_Xgln(node_t *root_node); +void SaveFormat_Xgl2(node_t *root_node); +void SaveFormat_Xgl3(node_t *root_node); diff --git a/src/misc.cpp b/src/misc.cpp index a6e9624..93bc81f 100644 --- a/src/misc.cpp +++ b/src/misc.cpp @@ -402,12 +402,9 @@ void PruneVerticesAtEnd(void) size_t unused = old_num - lev_vertices.size(); - if (unused > 0) + if (unused > 0 && config.verbose) { - if (config.verbose) - { - PrintLine(LOG_NORMAL, "Pruned %zu unused vertices at end", unused); - } + PrintLine(LOG_NORMAL, "Pruned %zu unused vertices at end", unused); } num_old_vert = lev_vertices.size(); diff --git a/src/node.cpp b/src/node.cpp index 39df8c3..46543a1 100644 --- a/src/node.cpp +++ b/src/node.cpp @@ -1012,7 +1012,7 @@ void AddMinisegs(intersection_t *cut_list, seg_t *part, seg_t **left_list, seg_t seg->index = buddy->index = NO_INDEX; seg->linedef = buddy->linedef = nullptr; - seg->side = buddy->side = 0; + seg->side = buddy->side = false; seg->source_line = buddy->source_line = part->linedef; @@ -1059,7 +1059,7 @@ void SetPartition(node_t *node, const seg_t *part) { SYS_ASSERT(part->linedef); - if (part->side == 0) + if (part->side == false) { node->x = part->linedef->start->x; node->y = part->linedef->start->y; @@ -1277,7 +1277,7 @@ int OnLineSide(quadtree_c *quadtree, const seg_t *part) return p1; } -seg_t *CreateOneSeg(linedef_t *line, vertex_t *start, vertex_t *end, sidedef_t *side, int what_side) +seg_t *CreateOneSeg(linedef_t *line, vertex_t *start, vertex_t *end, sidedef_t *side, bool what_side) { seg_t *seg = NewSeg(); @@ -1354,7 +1354,7 @@ seg_t *CreateSegs(void) if (line->right != nullptr) { - right = CreateOneSeg(line, line->start, line->end, line->right, 0); + right = CreateOneSeg(line, line->start, line->end, line->right, false); ListAddSeg(&list, right); } else @@ -1365,7 +1365,7 @@ seg_t *CreateSegs(void) if (line->left != nullptr) { - left = CreateOneSeg(line, line->end, line->start, line->left, 1); + left = CreateOneSeg(line, line->end, line->start, line->left, true); ListAddSeg(&list, left); if (right != nullptr) @@ -1864,12 +1864,12 @@ void RoundOff(subsec_t *subsec) seg_t *new_head = nullptr; seg_t *new_tail = nullptr; - seg_t *seg; + seg_t *seg = nullptr; seg_t *last_real_degen = nullptr; int real_total = 0; - int degen_total = 0; + if (HAS_BIT(config.debug, DEBUG_SUBSEC)) { PrintLine(LOG_DEBUG, "[%s] Rounding off %zu", __func__, subsec->index); @@ -1889,10 +1889,7 @@ void RoundOff(subsec_t *subsec) last_real_degen = seg; } - if (HAS_BIT(config.debug, DEBUG_SUBSEC)) - { - degen_total++; - } + degen_total++; continue; }