diff --git a/include/qbsp/map.hh b/include/qbsp/map.hh index 02446670..f3405866 100644 --- a/include/qbsp/map.hh +++ b/include/qbsp/map.hh @@ -102,12 +102,15 @@ public: int firstoutputfacenumber; int outputmodelnumber; + int32_t areaportalnum; + std::array portalareas; + const mapbrush_t &mapbrush(int i) const; mapentity_t() : firstmapbrush(0), nummapbrushes(0), solid(nullptr), sky(nullptr), detail(nullptr), detail_illusionary(nullptr), detail_fence(nullptr), liquid(nullptr), epairs(), brushes(nullptr), - numbrushes(0), firstoutputfacenumber(-1), outputmodelnumber(-1) + numbrushes(0), firstoutputfacenumber(-1), outputmodelnumber(-1), areaportalnum(0), portalareas({}) { VectorSet(origin, 0, 0, 0); } @@ -161,6 +164,8 @@ struct mapdata_t std::vector exported_leafbrushes; std::vector exported_brushsides; std::vector exported_brushes; + std::vector exported_areaportals; + std::vector exported_areas; std::string exported_entities; std::string exported_texdata; @@ -170,6 +175,9 @@ struct mapdata_t bool needslmshifts = false; std::vector exported_bspxbrushes; + // Q2 stuff + int32_t numareaportals; + // helpers const std::string &miptexTextureName(int mt) const { return miptex.at(mt).name; } @@ -183,7 +191,9 @@ bool ParseEntity(parser_t &parser, mapentity_t *entity); void EnsureTexturesLoaded(); void ProcessExternalMapEntity(mapentity_t *entity); +void ProcessAreaPortal(mapentity_t *entity); bool IsWorldBrushEntity(const mapentity_t *entity); +bool IsNonRemoveWorldBrushEntity(const mapentity_t *entity); void LoadMapFile(void); mapentity_t LoadExternalMap(const char *filename); void ConvertMapFile(void); diff --git a/include/qbsp/qbsp.hh b/include/qbsp/qbsp.hh index fe24d04c..cf1307a8 100644 --- a/include/qbsp/qbsp.hh +++ b/include/qbsp/qbsp.hh @@ -197,6 +197,7 @@ struct node_t // are detail. uint32_t firstleafbrush; // Q2 uint32_t numleafbrushes; + int32_t area; bool opaque() const; }; diff --git a/qbsp/map.cc b/qbsp/map.cc index ec39ebd7..ba2a40b8 100644 --- a/qbsp/map.cc +++ b/qbsp/map.cc @@ -1884,10 +1884,31 @@ void ProcessExternalMapEntity(mapentity_t *entity) SetKeyValue(entity, "origin", "0 0 0"); } +void ProcessAreaPortal(mapentity_t *entity) +{ + Q_assert(!options.fOnlyents); + + const char *classname = ValueForKey(entity, "classname"); + + if (Q_strcasecmp(classname, "func_areaportal")) + return; + + // areaportal entities move their brushes, but don't eliminate + // the entity + // FIXME: print entity ID/line number + if (entity->nummapbrushes != 1) + FError("func_areaportal can only be a single brush"); + + map.brushes[entity->firstmapbrush].contents = Q2_CONTENTS_AREAPORTAL; + map.faces[map.brushes[entity->firstmapbrush].firstface].contents = Q2_CONTENTS_AREAPORTAL; + entity->areaportalnum = ++map.numareaportals; + // set the portal number as "style" + SetKeyValue(entity, "style", std::to_string(map.numareaportals).c_str()); +} + /* * Special world entities are entities which have their brushes added to the - * world before being removed from the map. Currently func_detail and - * func_group. + * world before being removed from the map. */ bool IsWorldBrushEntity(const mapentity_t *entity) { @@ -1914,6 +1935,20 @@ bool IsWorldBrushEntity(const mapentity_t *entity) return false; } +/** + * Some games need special entities that are merged into the world, but not + * removed from the map entirely. + */ +bool IsNonRemoveWorldBrushEntity(const mapentity_t *entity) +{ + const char *classname = ValueForKey(entity, "classname"); + + if (!Q_strcasecmp(classname, "func_areaportal")) + return true; + + return false; +} + /** * Loads an external .map file. * diff --git a/qbsp/qbsp.cc b/qbsp/qbsp.cc index c45e7202..33c9ebad 100644 --- a/qbsp/qbsp.cc +++ b/qbsp/qbsp.cc @@ -280,6 +280,8 @@ static std::vector> AddBrushBevels(const brush_t *b static void ExportBrushList(const mapentity_t *entity, node_t *node, uint32_t &brush_offset) { + LogPrint(LOG_PROGRESS, "---- {} ----\n", __func__); + brush_state = {}; for (const brush_t *b = entity->brushes; b; b = b->next) { @@ -307,6 +309,218 @@ static void ExportBrushList(const mapentity_t *entity, node_t *node, uint32_t &b LogPrint(LOG_STAT, " {:8} total leaf brushes\n", brush_state.total_leaf_brushes); } +/* +========================================================= + +FLOOD AREAS + +========================================================= +*/ + +int32_t c_areas; + +/* +=============== +Portal_EntityFlood + +The entity flood determines which areas are +"outside" on the map, which are then filled in. +Flowing from side s to side !s +=============== +*/ +static bool Portal_EntityFlood(const portal_t *p, int32_t s) +{ + if (p->nodes[0]->planenum != PLANENUM_LEAF + || p->nodes[1]->planenum != PLANENUM_LEAF) + Error ("Portal_EntityFlood: not a leaf"); + + // can never cross to a solid + if ( (p->nodes[0]->contents.native & CONTENTS_SOLID) + || (p->nodes[1]->contents.native & CONTENTS_SOLID) ) + return false; + + // can flood through everything else + return true; +} + +/* +============= +FloodAreas_r +============= +*/ +static void FloodAreas_r(mapentity_t *entity, node_t *node) +{ + if (node->contents.native == Q2_CONTENTS_AREAPORTAL) + { + // this node is part of an area portal; + // if the current area has allready touched this + // portal, we are done + if (entity->portalareas[0] == c_areas || entity->portalareas[1] == c_areas) + return; + + // note the current area as bounding the portal + if (entity->portalareas[1]) + { + // FIXME: entity # + LogPrint("WARNING: areaportal entity touches > 2 areas\n Node Bounds: {} -> {}\n", + VecStr(node->bounds.mins()), VecStr(node->bounds.maxs())); + return; + } + + if (entity->portalareas[0]) + entity->portalareas[1] = c_areas; + else + entity->portalareas[0] = c_areas; + + return; + } + + if (node->area) + return; // already got it + + node->area = c_areas; + + int32_t s; + + for (portal_t *p = node->portals; p; p = p->next[s]) + { + s = (p->nodes[1] == node); +#if 0 + if (p->nodes[!s]->occupied) + continue; +#endif + if (!Portal_EntityFlood (p, s)) + continue; + + FloodAreas_r(entity, p->nodes[!s]); + } +} + +/* +============= +FindAreas_r + +Just decend the tree, and for each node that hasn't had an +area set, flood fill out from there +============= +*/ +static void FindAreas_r(mapentity_t *entity, node_t *node) +{ + if (node->planenum != PLANENUM_LEAF) + { + FindAreas_r(entity, node->children[0]); + FindAreas_r(entity, node->children[1]); + return; + } + + if (node->area) + return; // already got it + + if (node->contents.native & Q2_CONTENTS_SOLID) + return; + + // FIXME: how to do this since the nodes are destroyed by this point? + //if (!node->occupied) + // return; // not reachable by entities + + // area portals are always only flooded into, never + // out of + if (node->contents.native == Q2_CONTENTS_AREAPORTAL) + return; + + c_areas++; + FloodAreas_r(entity, node); +} + +/* +============= +SetAreaPortalAreas_r + +Just decend the tree, and for each node that hasn't had an +area set, flood fill out from there +============= +*/ +static void SetAreaPortalAreas_r(mapentity_t *entity, node_t *node) +{ + if (node->planenum != PLANENUM_LEAF) + { + SetAreaPortalAreas_r(entity, node->children[0]); + SetAreaPortalAreas_r(entity, node->children[1]); + return; + } + + if (node->contents.native != Q2_CONTENTS_AREAPORTAL) + return; + + if (node->area) + return; // already set + + node->area = entity->portalareas[0]; + if (!entity->portalareas[1]) + { + // FIXME: entity # + LogPrint("WARNING: areaportal entity doesn't touch two areas\n Node Bounds: {} -> {}\n", VecStr(entity->bounds.mins()), VecStr(entity->bounds.maxs())); + return; + } +} + +/* +============= +FloodAreas + +Mark each leaf with an area, bounded by CONTENTS_AREAPORTAL +============= +*/ +static void FloodAreas(mapentity_t *entity, node_t *headnode) +{ + LogPrint(LOG_PROGRESS, "---- {} ----\n", __func__); + FindAreas_r(entity, headnode); + SetAreaPortalAreas_r(entity, headnode); + LogPrint(LOG_STAT, "{:5} areas\n", c_areas); +} + +/* +============= +EmitAreaPortals + +============= +*/ +static void EmitAreaPortals(node_t *headnode) +{ + LogPrint(LOG_PROGRESS, "---- {} ----\n", __func__); + + map.exported_areaportals.emplace_back(); + map.exported_areas.emplace_back(); + + for (size_t i = 1; i <= c_areas; i++) { + darea_t &area = map.exported_areas.emplace_back(); + area.firstareaportal = map.exported_areaportals.size(); + + for (auto &e : map.entities) { + + if (!e.areaportalnum) + continue; + dareaportal_t &dp = map.exported_areaportals.emplace_back(); + + if (e.portalareas[0] == i) + { + dp.portalnum = e.areaportalnum; + dp.otherarea = e.portalareas[1]; + } + else if (e.portalareas[1] == i) + { + dp.portalnum = e.areaportalnum; + dp.otherarea = e.portalareas[0]; + } + } + + area.numareaportals = map.exported_areaportals.size() - area.firstareaportal; + } + + LogPrint(LOG_STAT, "{:5} numareas\n", map.exported_areas.size()); + LogPrint(LOG_STAT, "{:5} numareaportals\n", map.exported_areaportals.size()); +} + /* =============== ProcessEntity @@ -327,7 +541,7 @@ static void ProcessEntity(mapentity_t *entity, const int hullnum) * func_group and func_detail entities get their brushes added to the * worldspawn */ - if (IsWorldBrushEntity(entity)) + if (IsWorldBrushEntity(entity) || IsNonRemoveWorldBrushEntity(entity)) return; // Export a blank model struct, and reserve the index (only do this once, for all hulls) @@ -385,7 +599,9 @@ static void ProcessEntity(mapentity_t *entity, const int hullnum) /* Load external .map and change the classname, if needed */ ProcessExternalMapEntity(source); - if (IsWorldBrushEntity(source)) { + ProcessAreaPortal(source); + + if (IsWorldBrushEntity(source) || IsNonRemoveWorldBrushEntity(source)) { Brush_LoadEntity(entity, source, hullnum); } } @@ -496,6 +712,13 @@ static void ProcessEntity(mapentity_t *entity, const int hullnum) TJunc(entity, nodes); } + + // Area portals + if (options.target_game->id == GAME_QUAKE_II) { + FloodAreas(entity, nodes); + EmitAreaPortals(nodes); + } + FreeAllPortals(nodes); } @@ -515,7 +738,6 @@ static void ProcessEntity(mapentity_t *entity, const int hullnum) firstface = MakeFaceEdges(entity, nodes); if (options.target_game->id == GAME_QUAKE_II) { - LogPrint(LOG_PROGRESS, "---- ExportBrushList ----\n"); ExportBrushList(entity, nodes, brush_offset); } @@ -563,7 +785,7 @@ static void UpdateEntLump(void) if (!isBrushEnt) continue; - if (IsWorldBrushEntity(entity)) + if (IsWorldBrushEntity(entity) || IsNonRemoveWorldBrushEntity(entity)) continue; snprintf(modname, sizeof(modname), "*%d", modnum); @@ -774,7 +996,7 @@ static void BSPX_CreateBrushList(void) /* Load external .map and change the classname, if needed */ ProcessExternalMapEntity(source); - if (IsWorldBrushEntity(source)) { + if (IsWorldBrushEntity(source) || IsNonRemoveWorldBrushEntity(source)) { Brush_LoadEntity(ent, source, -1); } } diff --git a/qbsp/writebsp.cc b/qbsp/writebsp.cc index a4550d3f..b487f2e0 100644 --- a/qbsp/writebsp.cc +++ b/qbsp/writebsp.cc @@ -416,6 +416,8 @@ static void WriteBSPFile() bsp.dleafbrushes = std::move(map.exported_leafbrushes); bsp.dbrushsides = std::move(map.exported_brushsides); bsp.dbrushes = std::move(map.exported_brushes); + bsp.dareaportals = std::move(map.exported_areaportals); + bsp.dareas = std::move(map.exported_areas); bsp.dentdata = std::move(map.exported_entities); CopyString(map.exported_texdata, false, &bsp.texdatasize, (void **)&bsp.dtexdata); @@ -427,12 +429,6 @@ static void WriteBSPFile() BSPX_AddLump(&bspdata, "BRUSHLIST", map.exported_bspxbrushes.data(), map.exported_bspxbrushes.size()); } - // FIXME: temp - bsp.dareaportals.push_back({}); - - bsp.dareas.push_back({}); - bsp.dareas.push_back({ 0, 1 }); - if (!ConvertBSPFormat(&bspdata, options.target_version)) { const bspversion_t *extendedLimitsFormat = options.target_version->extended_limits;