diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6d39692e5..4fb943691 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -963,6 +963,7 @@ set (PCH_SOURCES p_map.cpp p_maputl.cpp p_mobj.cpp + p_openmap.cpp p_pillar.cpp p_plats.cpp p_pspr.cpp diff --git a/src/cmdlib.cpp b/src/cmdlib.cpp index af89cb352..b313f5691 100644 --- a/src/cmdlib.cpp +++ b/src/cmdlib.cpp @@ -195,33 +195,6 @@ bool DirEntryExists(const char *pathname, bool *isdir) return res; } -//========================================================================== -// -// DefaultExtension -- char array version -// -// Appends the extension to a pathname if it does not already have one. -// -//========================================================================== - -void DefaultExtension (char *path, const char *extension) -{ - char *src; -// -// if path doesn't have a .EXT, append extension -// (extension should include the .) -// - src = path + strlen(path) - 1; - - while (src != path && !IsSeperator(*src)) - { - if (*src == '.') - return; // it has an extension - src--; - } - - strcat (path, extension); -} - //========================================================================== // // DefaultExtension -- FString version diff --git a/src/cmdlib.h b/src/cmdlib.h index 7a5fa21a6..d187b43cf 100644 --- a/src/cmdlib.h +++ b/src/cmdlib.h @@ -27,7 +27,6 @@ extern FString progdir; void FixPathSeperator (char *path); static void inline FixPathSeperator (FString &path) { path.ReplaceChars('\\', '/'); } -void DefaultExtension (char *path, const char *extension); void DefaultExtension (FString &path, const char *extension); FString ExtractFilePath (const char *path); diff --git a/src/g_levellocals.h b/src/g_levellocals.h index c9eceabb6..ee9cbbe9c 100644 --- a/src/g_levellocals.h +++ b/src/g_levellocals.h @@ -46,7 +46,54 @@ #include "r_data/r_sections.h" #include "r_data/r_canvastexture.h" -struct FLevelLocals + +struct FLevelData +{ + TArray vertexes; + TArray sectors; + TArray linebuffer; // contains the line lists for the sectors. + TArray lines; + TArray sides; + TArray segs; + TArray subsectors; + TArray nodes; + TArray gamesubsectors; + TArray gamenodes; + node_t *headgamenode; + TArray rejectmatrix; + TArray Zones; + + TArray sectorPortals; + TArray linePortals; + + // Portal information. + FDisplacementTable Displacements; + FPortalBlockmap PortalBlockmap; + TArray linkedPortals; // only the linked portals, this is used to speed up looking for them in P_CollectConnectedGroups. + TArray portalGroups; + TArray linePortalSpans; + FSectionContainer sections; + FCanvasTextureInfo canvasTextureInfo; + + // [ZZ] Destructible geometry information + TMap healthGroups; + + FBlockmap blockmap; + + // These are copies of the loaded map data that get used by the savegame code to skip unaltered fields + // Without such a mechanism the savegame format would become too slow and large because more than 80-90% are normally still unaltered. + TArray loadsectors; + TArray loadlines; + TArray loadsides; + + // Maintain single and multi player starting spots. + TArray deathmatchstarts; + FPlayerStart playerstarts[MAXPLAYERS]; + TArray AllPlayerStarts; + +}; + +struct FLevelLocals : public FLevelData { void Tick(); void Mark(); @@ -76,57 +123,12 @@ struct FLevelLocals uint64_t ShaderStartTime = 0; // tell the shader system when we started the level (forces a timer restart) - TArray vertexes; - TArray sectors; - TArray linebuffer; // contains the line lists for the sectors. - TArray lines; - TArray sides; - TArray segs; - TArray subsectors; - TArray nodes; - TArray gamesubsectors; - TArray gamenodes; - node_t *headgamenode; - TArray rejectmatrix; - static const int BODYQUESIZE = 32; TObjPtr bodyque[BODYQUESIZE]; int bodyqueslot; - - TArray sectorPortals; - TArray linePortals; - - // Portal information. - FDisplacementTable Displacements; - FPortalBlockmap PortalBlockmap; - TArray linkedPortals; // only the linked portals, this is used to speed up looking for them in P_CollectConnectedGroups. - TArray portalGroups; - TArray linePortalSpans; - FSectionContainer sections; - FCanvasTextureInfo canvasTextureInfo; - int NumMapSections; - TArray Zones; - - // [ZZ] Destructible geometry information - TMap healthGroups; - - FBlockmap blockmap; - - // These are copies of the loaded map data that get used by the savegame code to skip unaltered fields - // Without such a mechanism the savegame format would become too slow and large because more than 80-90% are normally still unaltered. - TArray loadsectors; - TArray loadlines; - TArray loadsides; - - // Maintain single and multi player starting spots. - TArray deathmatchstarts; - FPlayerStart playerstarts[MAXPLAYERS]; - TArray AllPlayerStarts; - - uint32_t flags; uint32_t flags2; uint32_t flags3; @@ -224,6 +226,8 @@ struct FLevelLocals } }; +#ifndef NO_DEFINE_LEVEL + extern FLevelLocals level; inline int vertex_t::Index() const @@ -350,3 +354,4 @@ inline bool line_t::hitSkyWall(AActor* mo) const backsector->GetTexture(sector_t::ceiling) == skyflatnum && mo->Z() >= backsector->ceilingplane.ZatPoint(mo->PosRelative(this)); } +#endif \ No newline at end of file diff --git a/src/p_openmap.cpp b/src/p_openmap.cpp new file mode 100644 index 000000000..9331dca72 --- /dev/null +++ b/src/p_openmap.cpp @@ -0,0 +1,413 @@ +/* +** p_openmap.cpp +** +** creates the data structures needed to load a map from the resource files. +** +**--------------------------------------------------------------------------- +** Copyright 2005-2018 Christoph Oelckers +** Copyright 2005-2016 Randy Heit +** All rights reserved. +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions +** are met: +** +** 1. Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** 2. Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in the +** documentation and/or other materials provided with the distribution. +** 3. The name of the author may not be used to endorse or promote products +** derived from this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**--------------------------------------------------------------------------- +** +*/ + +#include "p_setup.h" +#include "i_system.h" +#include "cmdlib.h" +#include "w_wad.h" +#include "md5.h" +#include "g_levellocals.h" + +inline bool P_IsBuildMap(MapData *map) +{ + return false; +} + +//=========================================================================== +// +// GetMapIndex +// +// Gets the type of map lump or -1 if invalid or -2 if required and not found. +// +//=========================================================================== + +struct checkstruct +{ + const char lumpname[9]; + bool required; +}; + +static int GetMapIndex(const char *mapname, int lastindex, const char *lumpname, bool needrequired) +{ + static const checkstruct check[] = + { + {"", true}, + {"THINGS", true}, + {"LINEDEFS", true}, + {"SIDEDEFS", true}, + {"VERTEXES", true}, + {"SEGS", false}, + {"SSECTORS", false}, + {"NODES", false}, + {"SECTORS", true}, + {"REJECT", false}, + {"BLOCKMAP", false}, + {"BEHAVIOR", false}, + //{"SCRIPTS", false}, + }; + + if (lumpname==NULL) lumpname=""; + + for(size_t i=lastindex+1;iresource = FResourceFile::OpenResourceFile(mapname, true); + wadReader = map->resource->GetReader(); + } + else + { + FString fmt; + int lump_wad; + int lump_map; + int lump_name = -1; + + // Check for both *.wad and *.map in order to load Build maps + // as well. The higher one will take precedence. + // Names with more than 8 characters will only be checked as .wad and .map. + if (strlen(mapname) <= 8) lump_name = Wads.CheckNumForName(mapname); + fmt.Format("maps/%s.wad", mapname); + lump_wad = Wads.CheckNumForFullName(fmt); + fmt.Format("maps/%s.map", mapname); + lump_map = Wads.CheckNumForFullName(fmt); + + if (lump_name > lump_wad && lump_name > lump_map && lump_name != -1) + { + int lumpfile = Wads.GetLumpFile(lump_name); + int nextfile = Wads.GetLumpFile(lump_name+1); + + map->lumpnum = lump_name; + + if (lumpfile != nextfile) + { + // The following lump is from a different file so whatever this is, + // it is not a multi-lump Doom level so let's assume it is a Build map. + map->MapLumps[0].Reader = Wads.ReopenLumpReader(lump_name); + if (!P_IsBuildMap(map)) + { + delete map; + return NULL; + } + return map; + } + + // This case can only happen if the lump is inside a real WAD file. + // As such any special handling for other types of lumps is skipped. + map->MapLumps[0].Reader = Wads.ReopenLumpReader(lump_name); + strncpy(map->MapLumps[0].Name, Wads.GetLumpFullName(lump_name), 8); + map->Encrypted = Wads.IsEncryptedFile(lump_name); + map->InWad = true; + + if (map->Encrypted) + { // If it's encrypted, then it's a Blood file, presumably a map. + if (!P_IsBuildMap(map)) + { + delete map; + return NULL; + } + return map; + } + + int index = 0; + + if (stricmp(Wads.GetLumpFullName(lump_name + 1), "TEXTMAP") != 0) + { + for(int i = 1;; i++) + { + // Since levels must be stored in WADs they can't really have full + // names and for any valid level lump this always returns the short name. + const char * lumpname = Wads.GetLumpFullName(lump_name + i); + try + { + index = GetMapIndex(mapname, index, lumpname, !justcheck); + } + catch(...) + { + delete map; + throw; + } + if (index == -2) + { + delete map; + return NULL; + } + if (index == ML_BEHAVIOR) map->HasBehavior = true; + + // The next lump is not part of this map anymore + if (index < 0) break; + + map->MapLumps[index].Reader = Wads.ReopenLumpReader(lump_name + i); + strncpy(map->MapLumps[index].Name, lumpname, 8); + } + } + else + { + map->isText = true; + map->MapLumps[1].Reader = Wads.ReopenLumpReader(lump_name + 1); + for(int i = 2;; i++) + { + const char * lumpname = Wads.GetLumpFullName(lump_name + i); + + if (lumpname == NULL) + { + I_Error("Invalid map definition for %s", mapname); + } + else if (!stricmp(lumpname, "ZNODES")) + { + index = ML_GLZNODES; + } + else if (!stricmp(lumpname, "BLOCKMAP")) + { + // there is no real point in creating a blockmap but let's use it anyway + index = ML_BLOCKMAP; + } + else if (!stricmp(lumpname, "REJECT")) + { + index = ML_REJECT; + } + else if (!stricmp(lumpname, "DIALOGUE")) + { + index = ML_CONVERSATION; + } + else if (!stricmp(lumpname, "BEHAVIOR")) + { + index = ML_BEHAVIOR; + map->HasBehavior = true; + } + else if (!stricmp(lumpname, "ENDMAP")) + { + break; + } + else continue; + map->MapLumps[index].Reader = Wads.ReopenLumpReader(lump_name + i); + strncpy(map->MapLumps[index].Name, lumpname, 8); + } + } + return map; + } + else + { + if (lump_map > lump_wad) + { + lump_wad = lump_map; + } + if (lump_wad == -1) + { + delete map; + return NULL; + } + map->lumpnum = lump_wad; + auto reader = Wads.ReopenLumpReader(lump_wad); + map->resource = FResourceFile::OpenResourceFile(Wads.GetLumpFullName(lump_wad), reader, true); + wadReader = map->resource->GetReader(); + } + } + uint32_t id; + + // Although we're using the resource system, we still want to be sure we're + // reading from a wad file. + wadReader->Seek(0, FileReader::SeekSet); + wadReader->Read(&id, sizeof(id)); + + if (id == IWAD_ID || id == PWAD_ID) + { + char maplabel[9]=""; + int index=0; + + map->MapLumps[0].Reader = map->resource->GetLump(0)->NewReader(); + strncpy(map->MapLumps[0].Name, map->resource->GetLump(0)->Name, 8); + + for(uint32_t i = 1; i < map->resource->LumpCount(); i++) + { + const char* lumpname = map->resource->GetLump(i)->Name; + + if (i == 1 && !strnicmp(lumpname, "TEXTMAP", 8)) + { + map->isText = true; + map->MapLumps[ML_TEXTMAP].Reader = map->resource->GetLump(i)->NewReader(); + strncpy(map->MapLumps[ML_TEXTMAP].Name, lumpname, 8); + for(int i = 2;; i++) + { + lumpname = map->resource->GetLump(i)->Name; + if (!strnicmp(lumpname, "ZNODES",8)) + { + index = ML_GLZNODES; + } + else if (!strnicmp(lumpname, "BLOCKMAP",8)) + { + // there is no real point in creating a blockmap but let's use it anyway + index = ML_BLOCKMAP; + } + else if (!strnicmp(lumpname, "REJECT",8)) + { + index = ML_REJECT; + } + else if (!strnicmp(lumpname, "DIALOGUE",8)) + { + index = ML_CONVERSATION; + } + else if (!strnicmp(lumpname, "BEHAVIOR",8)) + { + index = ML_BEHAVIOR; + map->HasBehavior = true; + } + else if (!strnicmp(lumpname, "ENDMAP",8)) + { + return map; + } + else continue; + map->MapLumps[index].Reader = map->resource->GetLump(i)->NewReader(); + strncpy(map->MapLumps[index].Name, lumpname, 8); + } + } + + if (i>0) + { + try + { + index = GetMapIndex(maplabel, index, lumpname, !justcheck); + } + catch(...) + { + delete map; + throw; + } + if (index == -2) + { + delete map; + return NULL; + } + if (index == ML_BEHAVIOR) map->HasBehavior = true; + + // The next lump is not part of this map anymore + if (index < 0) break; + } + else + { + strncpy(maplabel, lumpname, 8); + maplabel[8]=0; + } + + map->MapLumps[index].Reader = map->resource->GetLump(i)->NewReader(); + strncpy(map->MapLumps[index].Name, lumpname, 8); + } + } + else + { + // This is a Build map and not subject to WAD consistency checks. + //map->MapLumps[0].Size = wadReader->GetLength(); + if (!P_IsBuildMap(map)) + { + delete map; + return NULL; + } + } + return map; +} + +bool P_CheckMapData(const char *mapname) +{ + MapData *mapd = P_OpenMapData(mapname, true); + if (mapd == NULL) return false; + delete mapd; + return true; +} + +//=========================================================================== +// +// MapData :: GetChecksum +// +// Hashes a map based on its header, THINGS, LINEDEFS, SIDEDEFS, SECTORS, +// and BEHAVIOR lumps. Node-builder generated lumps are not included. +// +//=========================================================================== + +void MapData::GetChecksum(uint8_t cksum[16]) +{ + MD5Context md5; + + if (isText) + { + md5.Update(Reader(ML_TEXTMAP), Size(ML_TEXTMAP)); + } + else + { + md5.Update(Reader(ML_LABEL), Size(ML_LABEL)); + md5.Update(Reader(ML_THINGS), Size(ML_THINGS)); + md5.Update(Reader(ML_LINEDEFS), Size(ML_LINEDEFS)); + md5.Update(Reader(ML_SIDEDEFS), Size(ML_SIDEDEFS)); + md5.Update(Reader(ML_SECTORS), Size(ML_SECTORS)); + } + if (HasBehavior) + { + md5.Update(Reader(ML_BEHAVIOR), Size(ML_BEHAVIOR)); + } + md5.Final(cksum); +} diff --git a/src/p_setup.cpp b/src/p_setup.cpp index 16f609152..7dc77bf85 100644 --- a/src/p_setup.cpp +++ b/src/p_setup.cpp @@ -136,11 +136,6 @@ CVAR (Bool, showloadtimes, false, 0); static void P_Shutdown (); -inline bool P_IsBuildMap(MapData *map) -{ - return false; -} - inline bool P_LoadBuildMap(uint8_t *mapdata, size_t len, FMapThing **things, int *numthings) { return false; @@ -168,385 +163,6 @@ bool ForceNodeBuild; static void P_AllocateSideDefs (MapData *map, int count); -//=========================================================================== -// -// GetMapIndex -// -// Gets the type of map lump or -1 if invalid or -2 if required and not found. -// -//=========================================================================== - -struct checkstruct -{ - const char lumpname[9]; - bool required; -}; - -static int GetMapIndex(const char *mapname, int lastindex, const char *lumpname, bool needrequired) -{ - static const checkstruct check[] = - { - {"", true}, - {"THINGS", true}, - {"LINEDEFS", true}, - {"SIDEDEFS", true}, - {"VERTEXES", true}, - {"SEGS", false}, - {"SSECTORS", false}, - {"NODES", false}, - {"SECTORS", true}, - {"REJECT", false}, - {"BLOCKMAP", false}, - {"BEHAVIOR", false}, - //{"SCRIPTS", false}, - }; - - if (lumpname==NULL) lumpname=""; - - for(size_t i=lastindex+1;iresource = FResourceFile::OpenResourceFile(mapname, true); - wadReader = map->resource->GetReader(); - } - else - { - FString fmt; - int lump_wad; - int lump_map; - int lump_name = -1; - - // Check for both *.wad and *.map in order to load Build maps - // as well. The higher one will take precedence. - // Names with more than 8 characters will only be checked as .wad and .map. - if (strlen(mapname) <= 8) lump_name = Wads.CheckNumForName(mapname); - fmt.Format("maps/%s.wad", mapname); - lump_wad = Wads.CheckNumForFullName(fmt); - fmt.Format("maps/%s.map", mapname); - lump_map = Wads.CheckNumForFullName(fmt); - - if (lump_name > lump_wad && lump_name > lump_map && lump_name != -1) - { - int lumpfile = Wads.GetLumpFile(lump_name); - int nextfile = Wads.GetLumpFile(lump_name+1); - - map->lumpnum = lump_name; - - if (lumpfile != nextfile) - { - // The following lump is from a different file so whatever this is, - // it is not a multi-lump Doom level so let's assume it is a Build map. - map->MapLumps[0].Reader = Wads.ReopenLumpReader(lump_name); - if (!P_IsBuildMap(map)) - { - delete map; - return NULL; - } - return map; - } - - // This case can only happen if the lump is inside a real WAD file. - // As such any special handling for other types of lumps is skipped. - map->MapLumps[0].Reader = Wads.ReopenLumpReader(lump_name); - strncpy(map->MapLumps[0].Name, Wads.GetLumpFullName(lump_name), 8); - map->Encrypted = Wads.IsEncryptedFile(lump_name); - map->InWad = true; - - if (map->Encrypted) - { // If it's encrypted, then it's a Blood file, presumably a map. - if (!P_IsBuildMap(map)) - { - delete map; - return NULL; - } - return map; - } - - int index = 0; - - if (stricmp(Wads.GetLumpFullName(lump_name + 1), "TEXTMAP") != 0) - { - for(int i = 1;; i++) - { - // Since levels must be stored in WADs they can't really have full - // names and for any valid level lump this always returns the short name. - const char * lumpname = Wads.GetLumpFullName(lump_name + i); - try - { - index = GetMapIndex(mapname, index, lumpname, !justcheck); - } - catch(...) - { - delete map; - throw; - } - if (index == -2) - { - delete map; - return NULL; - } - if (index == ML_BEHAVIOR) map->HasBehavior = true; - - // The next lump is not part of this map anymore - if (index < 0) break; - - map->MapLumps[index].Reader = Wads.ReopenLumpReader(lump_name + i); - strncpy(map->MapLumps[index].Name, lumpname, 8); - } - } - else - { - map->isText = true; - map->MapLumps[1].Reader = Wads.ReopenLumpReader(lump_name + 1); - for(int i = 2;; i++) - { - const char * lumpname = Wads.GetLumpFullName(lump_name + i); - - if (lumpname == NULL) - { - I_Error("Invalid map definition for %s", mapname); - } - else if (!stricmp(lumpname, "ZNODES")) - { - index = ML_GLZNODES; - } - else if (!stricmp(lumpname, "BLOCKMAP")) - { - // there is no real point in creating a blockmap but let's use it anyway - index = ML_BLOCKMAP; - } - else if (!stricmp(lumpname, "REJECT")) - { - index = ML_REJECT; - } - else if (!stricmp(lumpname, "DIALOGUE")) - { - index = ML_CONVERSATION; - } - else if (!stricmp(lumpname, "BEHAVIOR")) - { - index = ML_BEHAVIOR; - map->HasBehavior = true; - } - else if (!stricmp(lumpname, "ENDMAP")) - { - break; - } - else continue; - map->MapLumps[index].Reader = Wads.ReopenLumpReader(lump_name + i); - strncpy(map->MapLumps[index].Name, lumpname, 8); - } - } - return map; - } - else - { - if (lump_map > lump_wad) - { - lump_wad = lump_map; - } - if (lump_wad == -1) - { - delete map; - return NULL; - } - map->lumpnum = lump_wad; - auto reader = Wads.ReopenLumpReader(lump_wad); - map->resource = FResourceFile::OpenResourceFile(Wads.GetLumpFullName(lump_wad), reader, true); - wadReader = map->resource->GetReader(); - } - } - uint32_t id; - - // Although we're using the resource system, we still want to be sure we're - // reading from a wad file. - wadReader->Seek(0, FileReader::SeekSet); - wadReader->Read(&id, sizeof(id)); - - if (id == IWAD_ID || id == PWAD_ID) - { - char maplabel[9]=""; - int index=0; - - map->MapLumps[0].Reader = map->resource->GetLump(0)->NewReader(); - strncpy(map->MapLumps[0].Name, map->resource->GetLump(0)->Name, 8); - - for(uint32_t i = 1; i < map->resource->LumpCount(); i++) - { - const char* lumpname = map->resource->GetLump(i)->Name; - - if (i == 1 && !strnicmp(lumpname, "TEXTMAP", 8)) - { - map->isText = true; - map->MapLumps[ML_TEXTMAP].Reader = map->resource->GetLump(i)->NewReader(); - strncpy(map->MapLumps[ML_TEXTMAP].Name, lumpname, 8); - for(int i = 2;; i++) - { - lumpname = map->resource->GetLump(i)->Name; - if (!strnicmp(lumpname, "ZNODES",8)) - { - index = ML_GLZNODES; - } - else if (!strnicmp(lumpname, "BLOCKMAP",8)) - { - // there is no real point in creating a blockmap but let's use it anyway - index = ML_BLOCKMAP; - } - else if (!strnicmp(lumpname, "REJECT",8)) - { - index = ML_REJECT; - } - else if (!strnicmp(lumpname, "DIALOGUE",8)) - { - index = ML_CONVERSATION; - } - else if (!strnicmp(lumpname, "BEHAVIOR",8)) - { - index = ML_BEHAVIOR; - map->HasBehavior = true; - } - else if (!strnicmp(lumpname, "ENDMAP",8)) - { - return map; - } - else continue; - map->MapLumps[index].Reader = map->resource->GetLump(i)->NewReader(); - strncpy(map->MapLumps[index].Name, lumpname, 8); - } - } - - if (i>0) - { - try - { - index = GetMapIndex(maplabel, index, lumpname, !justcheck); - } - catch(...) - { - delete map; - throw; - } - if (index == -2) - { - delete map; - return NULL; - } - if (index == ML_BEHAVIOR) map->HasBehavior = true; - - // The next lump is not part of this map anymore - if (index < 0) break; - } - else - { - strncpy(maplabel, lumpname, 8); - maplabel[8]=0; - } - - map->MapLumps[index].Reader = map->resource->GetLump(i)->NewReader(); - strncpy(map->MapLumps[index].Name, lumpname, 8); - } - } - else - { - // This is a Build map and not subject to WAD consistency checks. - //map->MapLumps[0].Size = wadReader->GetLength(); - if (!P_IsBuildMap(map)) - { - delete map; - return NULL; - } - } - return map; -} - -bool P_CheckMapData(const char *mapname) -{ - MapData *mapd = P_OpenMapData(mapname, true); - if (mapd == NULL) return false; - delete mapd; - return true; -} - -//=========================================================================== -// -// MapData :: GetChecksum -// -// Hashes a map based on its header, THINGS, LINEDEFS, SIDEDEFS, SECTORS, -// and BEHAVIOR lumps. Node-builder generated lumps are not included. -// -//=========================================================================== - -void MapData::GetChecksum(uint8_t cksum[16]) -{ - MD5Context md5; - - if (isText) - { - md5.Update(Reader(ML_TEXTMAP), Size(ML_TEXTMAP)); - } - else - { - md5.Update(Reader(ML_LABEL), Size(ML_LABEL)); - md5.Update(Reader(ML_THINGS), Size(ML_THINGS)); - md5.Update(Reader(ML_LINEDEFS), Size(ML_LINEDEFS)); - md5.Update(Reader(ML_SIDEDEFS), Size(ML_SIDEDEFS)); - md5.Update(Reader(ML_SECTORS), Size(ML_SECTORS)); - } - if (HasBehavior) - { - md5.Update(Reader(ML_BEHAVIOR), Size(ML_BEHAVIOR)); - } - md5.Final(cksum); -} - -DEFINE_ACTION_FUNCTION(FLevelLocals, GetChecksum) -{ - PARAM_SELF_STRUCT_PROLOGUE(FLevelLocals); - char md5string[33]; - - for(int j = 0; j < 16; ++j) - { - sprintf(md5string + j * 2, "%02x", level.md5[j]); - } - - ACTION_RETURN_STRING((const char*)md5string); -} - //=========================================================================== // // Sets a sidedef's texture and prints a message if it's not present. diff --git a/src/polyrenderer/drawers/poly_triangle.cpp b/src/polyrenderer/drawers/poly_triangle.cpp index 1949b06e3..fab366296 100644 --- a/src/polyrenderer/drawers/poly_triangle.cpp +++ b/src/polyrenderer/drawers/poly_triangle.cpp @@ -68,17 +68,16 @@ void PolyTriangleDrawer::SetViewport(const DrawerCommandQueuePtr &queue, int x, isBgraRenderTarget = dest_bgra; int offsetx = clamp(x, 0, dest_width); - int offsety = clamp(y, 0, dest_height); int pixelsize = dest_bgra ? 4 : 1; int viewport_x = x - offsetx; - int viewport_y = y - offsety; + int viewport_y = y; int viewport_width = width; int viewport_height = height; - dest += (offsetx + offsety * dest_pitch) * pixelsize; + dest += offsetx * pixelsize; dest_width = clamp(viewport_x + viewport_width, 0, dest_width - offsetx); - dest_height = clamp(viewport_y + viewport_height, 0, dest_height - offsety); + dest_height = clamp(viewport_y + viewport_height, 0, dest_height); queue->Push(viewport_x, viewport_y, viewport_width, viewport_height, dest, dest_width, dest_height, dest_pitch, dest_bgra); } @@ -127,13 +126,11 @@ void PolyTriangleThreadData::ClearStencil(uint8_t value) int height = buffer->Height(); uint8_t *data = buffer->Values(); - int start_y = numa_node * height / num_numa_nodes; - int end_y = (numa_node + 1) * height / num_numa_nodes; - int core_skip = (num_cores - (start_y - core) % num_cores) % num_cores; - start_y += core_skip; + int skip = skipped_by_thread(0); + int count = count_for_thread(0, height); - data += start_y * width; - for (int y = start_y; y < end_y; y += num_cores) + data += skip * width; + for (int i = 0; i < count; i++) { memset(data, value, width); data += num_cores * width; @@ -151,8 +148,6 @@ void PolyTriangleThreadData::SetViewport(int x, int y, int width, int height, ui dest_height = new_dest_height; dest_pitch = new_dest_pitch; dest_bgra = new_dest_bgra; - numa_start_y = numa_node * screen->GetHeight() / num_numa_nodes; - numa_end_y = (numa_node + 1) * screen->GetHeight() / num_numa_nodes; ccw = true; weaponScene = false; } diff --git a/src/polyrenderer/drawers/poly_triangle.h b/src/polyrenderer/drawers/poly_triangle.h index fadc799d6..4504a3950 100644 --- a/src/polyrenderer/drawers/poly_triangle.h +++ b/src/polyrenderer/drawers/poly_triangle.h @@ -71,7 +71,11 @@ public: int numa_start_y; int numa_end_y; - // The number of lines to skip to reach the first line to be rendered by this thread + bool line_skipped_by_thread(int line) + { + return line < numa_start_y || line >= numa_end_y || line % num_cores != core; + } + int skipped_by_thread(int first_line) { int clip_first_line = MAX(first_line, numa_start_y); @@ -79,6 +83,13 @@ public: return clip_first_line + core_skip - first_line; } + int count_for_thread(int first_line, int count) + { + count = MIN(count, numa_end_y - first_line); + int c = (count - skipped_by_thread(first_line) + num_cores - 1) / num_cores; + return MAX(c, 0); + } + // Varyings float worldposX[MAXWIDTH]; float worldposY[MAXWIDTH]; @@ -97,6 +108,8 @@ public: uint8_t *dest = nullptr; bool weaponScene = false; + int viewport_y = 0; + private: ShadedTriVertex ShadeVertex(const PolyDrawArgs &drawargs, const void *vertices, int index); void DrawShadedTriangle(const ShadedTriVertex *vertices, bool ccw, TriDrawTriangleArgs *args); @@ -105,7 +118,6 @@ private: static int ClipEdge(const ShadedTriVertex *verts, ShadedTriVertex *clippedvert); int viewport_x = 0; - int viewport_y = 0; int viewport_width = 0; int viewport_height = 0; bool ccw = true; diff --git a/src/polyrenderer/drawers/screen_triangle.cpp b/src/polyrenderer/drawers/screen_triangle.cpp index 551753c20..151e86a9f 100644 --- a/src/polyrenderer/drawers/screen_triangle.cpp +++ b/src/polyrenderer/drawers/screen_triangle.cpp @@ -60,73 +60,84 @@ void ScreenTriangle::Draw(const TriDrawTriangleArgs *args, PolyTriangleThreadDat ShadedTriVertex *sortedVertices[3]; SortVertices(args, sortedVertices); + int clipleft = 0; + int cliptop = MAX(thread->viewport_y, thread->numa_start_y); int clipright = thread->dest_width; - int cliptop = thread->numa_start_y; int clipbottom = MIN(thread->dest_height, thread->numa_end_y); int topY = (int)(sortedVertices[0]->y + 0.5f); int midY = (int)(sortedVertices[1]->y + 0.5f); int bottomY = (int)(sortedVertices[2]->y + 0.5f); - topY = MAX(topY, 0); - midY = clamp(midY, 0, clipbottom); + topY = MAX(topY, cliptop); + midY = MIN(midY, clipbottom); bottomY = MIN(bottomY, clipbottom); if (topY >= bottomY) return; + topY += thread->skipped_by_thread(topY); + int num_cores = thread->num_cores; + // Find start/end X positions for each line covered by the triangle: int16_t edges[MAXHEIGHT * 2]; + int y = topY; + float longDX = sortedVertices[2]->x - sortedVertices[0]->x; float longDY = sortedVertices[2]->y - sortedVertices[0]->y; float longStep = longDX / longDY; - float longPos = sortedVertices[0]->x + longStep * (topY + 0.5f - sortedVertices[0]->y) + 0.5f; + float longPos = sortedVertices[0]->x + longStep * (y + 0.5f - sortedVertices[0]->y) + 0.5f; + longStep *= num_cores; - if (topY < midY) + if (y < midY) { float shortDX = sortedVertices[1]->x - sortedVertices[0]->x; float shortDY = sortedVertices[1]->y - sortedVertices[0]->y; float shortStep = shortDX / shortDY; - float shortPos = sortedVertices[0]->x + shortStep * (topY + 0.5f - sortedVertices[0]->y) + 0.5f; + float shortPos = sortedVertices[0]->x + shortStep * (y + 0.5f - sortedVertices[0]->y) + 0.5f; + shortStep *= num_cores; - for (int y = topY; y < midY; y++) + while (y < midY) { int x0 = (int)shortPos; int x1 = (int)longPos; if (x1 < x0) std::swap(x0, x1); - x0 = clamp(x0, 0, clipright); - x1 = clamp(x1, 0, clipright); + x0 = clamp(x0, clipleft, clipright); + x1 = clamp(x1, clipleft, clipright); edges[y << 1] = x0; edges[(y << 1) + 1] = x1; shortPos += shortStep; longPos += longStep; + y += num_cores; } } - if (midY < bottomY) + if (y < bottomY) { float shortDX = sortedVertices[2]->x - sortedVertices[1]->x; float shortDY = sortedVertices[2]->y - sortedVertices[1]->y; float shortStep = shortDX / shortDY; - float shortPos = sortedVertices[1]->x + shortStep * (midY + 0.5f - sortedVertices[1]->y) + 0.5f; + float shortPos = sortedVertices[1]->x + shortStep * (y + 0.5f - sortedVertices[1]->y) + 0.5f; + shortStep *= num_cores; - for (int y = midY; y < bottomY; y++) + while (y < bottomY) { int x0 = (int)shortPos; int x1 = (int)longPos; if (x1 < x0) std::swap(x0, x1); - x0 = clamp(x0, 0, clipright); - x1 = clamp(x1, 0, clipright); + x0 = clamp(x0, clipleft, clipright); + x1 = clamp(x1, clipleft, clipright); edges[y << 1] = x0; edges[(y << 1) + 1] = x1; shortPos += shortStep; longPos += longStep; + y += num_cores; } } @@ -183,8 +194,14 @@ void DrawTriangle(const TriDrawTriangleArgs *args, PolyTriangleThreadData *threa if (OptT::Flags & SWTRI_WriteStencil) stencilWriteValue = args->uniforms->StencilWriteValue(); + float weaponWOffset; + if ((OptT::Flags & SWTRI_DepthTest) || (OptT::Flags & SWTRI_WriteDepth)) + { + weaponWOffset = thread->weaponScene ? 1.0f : 0.0f; + } + int num_cores = thread->num_cores; - for (int y = topY + thread->skipped_by_thread(topY); y < bottomY; y += num_cores) + for (int y = topY; y < bottomY; y += num_cores) { int x = edges[y << 1]; int xend = edges[(y << 1) + 1]; @@ -198,7 +215,7 @@ void DrawTriangle(const TriDrawTriangleArgs *args, PolyTriangleThreadData *threa float startX = x + (0.5f - v1X); float startY = y + (0.5f - v1Y); - posXW = v1W + stepXW * startX + args->gradientY.W * startY + (thread->weaponScene ? 1.0f : 0.0f); + posXW = v1W + stepXW * startX + args->gradientY.W * startY + weaponWOffset; } #ifndef NO_SSE diff --git a/src/scripting/vm/jit.cpp b/src/scripting/vm/jit.cpp index 1e94e3ada..1813d19ca 100644 --- a/src/scripting/vm/jit.cpp +++ b/src/scripting/vm/jit.cpp @@ -156,6 +156,8 @@ asmjit::CCFunc *JitCompiler::Codegen() LineInfo[j] = info; } + std::stable_sort(LineInfo.begin(), LineInfo.end(), [](const JitLineInfo &a, const JitLineInfo &b) { return a.InstructionIndex < b.InstructionIndex; }); + return func; } @@ -444,17 +446,15 @@ void JitCompiler::EmitNullPointerThrow(int index, EVMAbortException reason) cc.je(label); } -void JitCompiler::ThrowException(VMScriptFunction *func, VMOP *line, int reason) +void JitCompiler::ThrowException(int reason) { - ThrowAbortException(func, line, (EVMAbortException)reason, nullptr); + ThrowAbortException((EVMAbortException)reason, nullptr); } void JitCompiler::EmitThrowException(EVMAbortException reason) { - auto call = CreateCall(&JitCompiler::ThrowException); - call->setArg(0, asmjit::imm_ptr(sfunc)); - call->setArg(1, asmjit::imm_ptr(pc)); - call->setArg(2, asmjit::imm(reason)); + auto call = CreateCall(&JitCompiler::ThrowException); + call->setArg(0, asmjit::imm(reason)); } asmjit::Label JitCompiler::EmitThrowExceptionLabel(EVMAbortException reason) @@ -464,6 +464,12 @@ asmjit::Label JitCompiler::EmitThrowExceptionLabel(EVMAbortException reason) cc.bind(label); EmitThrowException(reason); cc.setCursor(cursor); + + JitLineInfo info; + info.Label = label; + info.LineNumber = sfunc->PCToLine(pc); + LineInfo.Push(info); + return label; } diff --git a/src/scripting/vm/jit_flow.cpp b/src/scripting/vm/jit_flow.cpp index 0ca8bf437..96b7bcec0 100644 --- a/src/scripting/vm/jit_flow.cpp +++ b/src/scripting/vm/jit_flow.cpp @@ -330,15 +330,18 @@ void JitCompiler::EmitBOUND() auto cursor = cc.getCursor(); auto label = cc.newLabel(); cc.bind(label); - auto call = CreateCall(&JitCompiler::ThrowArrayOutOfBounds); - call->setArg(0, asmjit::imm_ptr(sfunc)); - call->setArg(1, asmjit::imm_ptr(pc)); - call->setArg(2, regD[A]); - call->setArg(3, asmjit::imm(BC)); + auto call = CreateCall(&JitCompiler::ThrowArrayOutOfBounds); + call->setArg(0, regD[A]); + call->setArg(1, asmjit::imm(BC)); cc.setCursor(cursor); cc.cmp(regD[A], (int)BC); cc.jae(label); + + JitLineInfo info; + info.Label = label; + info.LineNumber = sfunc->PCToLine(pc); + LineInfo.Push(info); } void JitCompiler::EmitBOUND_K() @@ -346,15 +349,18 @@ void JitCompiler::EmitBOUND_K() auto cursor = cc.getCursor(); auto label = cc.newLabel(); cc.bind(label); - auto call = CreateCall(&JitCompiler::ThrowArrayOutOfBounds); - call->setArg(0, asmjit::imm_ptr(sfunc)); - call->setArg(1, asmjit::imm_ptr(pc)); - call->setArg(2, regD[A]); - call->setArg(3, asmjit::imm(konstd[BC])); + auto call = CreateCall(&JitCompiler::ThrowArrayOutOfBounds); + call->setArg(0, regD[A]); + call->setArg(1, asmjit::imm(konstd[BC])); cc.setCursor(cursor); cc.cmp(regD[A], (int)konstd[BC]); cc.jae(label); + + JitLineInfo info; + info.Label = label; + info.LineNumber = sfunc->PCToLine(pc); + LineInfo.Push(info); } void JitCompiler::EmitBOUND_R() @@ -362,18 +368,21 @@ void JitCompiler::EmitBOUND_R() auto cursor = cc.getCursor(); auto label = cc.newLabel(); cc.bind(label); - auto call = CreateCall(&JitCompiler::ThrowArrayOutOfBounds); - call->setArg(0, asmjit::imm_ptr(sfunc)); - call->setArg(1, asmjit::imm_ptr(pc)); - call->setArg(2, regD[A]); - call->setArg(3, regD[B]); + auto call = CreateCall(&JitCompiler::ThrowArrayOutOfBounds); + call->setArg(0, regD[A]); + call->setArg(1, regD[B]); cc.setCursor(cursor); cc.cmp(regD[A], regD[B]); cc.jae(label); + + JitLineInfo info; + info.Label = label; + info.LineNumber = sfunc->PCToLine(pc); + LineInfo.Push(info); } -void JitCompiler::ThrowArrayOutOfBounds(VMScriptFunction *func, VMOP *line, int index, int size) +void JitCompiler::ThrowArrayOutOfBounds(int index, int size) { if (index >= size) { diff --git a/src/scripting/vm/jitintern.h b/src/scripting/vm/jitintern.h index deb0378f0..67659e6e0 100644 --- a/src/scripting/vm/jitintern.h +++ b/src/scripting/vm/jitintern.h @@ -219,8 +219,8 @@ private: void EmitThrowException(EVMAbortException reason); asmjit::Label EmitThrowExceptionLabel(EVMAbortException reason); - static void ThrowArrayOutOfBounds(VMScriptFunction *func, VMOP *line, int index, int size); - static void ThrowException(VMScriptFunction *func, VMOP *line, int reason); + static void ThrowArrayOutOfBounds(int index, int size); + static void ThrowException(int reason); asmjit::X86Gp CheckRegD(int r0, int r1); asmjit::X86Xmm CheckRegF(int r0, int r1); diff --git a/src/scripting/vmthunks.cpp b/src/scripting/vmthunks.cpp index ad44203b1..b29af9f9c 100644 --- a/src/scripting/vmthunks.cpp +++ b/src/scripting/vmthunks.cpp @@ -2506,6 +2506,19 @@ DEFINE_ACTION_FUNCTION_NATIVE(FLevelLocals, GetUDMFString, ZGetUDMFString) ACTION_RETURN_STRING(GetUDMFString(type, index, key)); } +DEFINE_ACTION_FUNCTION(FLevelLocals, GetChecksum) +{ + PARAM_SELF_STRUCT_PROLOGUE(FLevelLocals); + char md5string[33]; + + for (int j = 0; j < 16; ++j) + { + sprintf(md5string + j * 2, "%02x", level.md5[j]); + } + + ACTION_RETURN_STRING((const char*)md5string); +} + //===================================================================================== // // diff --git a/src/swrenderer/drawers/r_thread.cpp b/src/swrenderer/drawers/r_thread.cpp index 5055e259b..e8b6eeada 100644 --- a/src/swrenderer/drawers/r_thread.cpp +++ b/src/swrenderer/drawers/r_thread.cpp @@ -142,6 +142,11 @@ void DrawerThreads::WorkerMain(DrawerThread *thread) thread->current_queue++; thread->numa_start_y = thread->numa_node * screen->GetHeight() / thread->num_numa_nodes; thread->numa_end_y = (thread->numa_node + 1) * screen->GetHeight() / thread->num_numa_nodes; + if (thread->poly) + { + thread->poly->numa_start_y = thread->numa_start_y; + thread->poly->numa_end_y = thread->numa_end_y; + } start_lock.unlock(); // Do the work: diff --git a/src/tarray.h b/src/tarray.h index 524c0942e..1c5faca43 100644 --- a/src/tarray.h +++ b/src/tarray.h @@ -497,6 +497,14 @@ public: Array = nullptr; } } + + void Swap(TArray &other) + { + std::swap(Array, other.Array); + std::swap(Count, other.Count); + std::swap(Most, other.Most); + } + private: T *Array; unsigned int Count; @@ -760,6 +768,7 @@ struct FMap hash_t NumUsed; }; + template class TMapIterator; template class TMapConstIterator; @@ -947,6 +956,14 @@ public: DelKey(key); } + void Swap(MyType &other) + { + std::swap(Nodes, other.Nodes); + std::swap(LastFree, other.LastFree); + std::swap(Size, other.Size); + std::swap(NumUsed, other.NumUsed); + } + protected: struct IPair // This must be the same as Pair above, but with a { // non-const Key. @@ -1518,4 +1535,3 @@ private: T *Array; unsigned int Count; }; - diff --git a/src/textures/formats/multipatchtexture.h b/src/textures/formats/multipatchtexture.h index 3d7800832..8337ffe6c 100644 --- a/src/textures/formats/multipatchtexture.h +++ b/src/textures/formats/multipatchtexture.h @@ -89,8 +89,30 @@ struct BuildInfo int LeftOffset[2] = {}; int TopOffset[2] = {}; FImageTexture *tex = nullptr; + + void swap(BuildInfo &other) + { + Name.Swap(other.Name); + Parts.Swap(other.Parts); + Inits.Swap(other.Inits); + std::swap(Width, other.Width); + std::swap(Height, other.Height); + std::swap(Scale, other.Scale); + std::swap(bWorldPanning, other.bWorldPanning); + std::swap(DefinitionLump, other.DefinitionLump); + std::swap(bComplex, bComplex); + std::swap(textual, other.textual); + std::swap(bNoDecals, other.bNoDecals); + std::swap(LeftOffset[0], other.LeftOffset[0]); + std::swap(LeftOffset[1], other.LeftOffset[1]); + std::swap(TopOffset[0], other.TopOffset[0]); + std::swap(TopOffset[1], other.TopOffset[1]); + std::swap(tex, other.tex); + } }; + + class FMultipatchTextureBuilder { FTextureManager &TexMan; diff --git a/src/textures/multipatchtexturebuilder.cpp b/src/textures/multipatchtexturebuilder.cpp index 7be8a69e5..4675220f5 100644 --- a/src/textures/multipatchtexturebuilder.cpp +++ b/src/textures/multipatchtexturebuilder.cpp @@ -922,11 +922,21 @@ void FMultipatchTextureBuilder::ResolveAllPatches() } // Now try to resolve the images. We only can do this at the end when all multipatch textures are set up. int i = 0; + + // reverse the list so that the Delete operation in the loop below deletes at the end. + // For normal sized lists this is of no real concern, but Total Chaos has over 250000 textures where this becomes a performance issue. + for (unsigned i = 0; i < BuiltTextures.Size() / 2; i++) + { + // std::swap is VERY inefficient here... + BuiltTextures[i].swap(BuiltTextures[BuiltTextures.Size() - 1 - i]); + } + + while (BuiltTextures.Size() > 0) { bool donesomething = false; - for (unsigned i = 0; i < BuiltTextures.Size(); i++) + for (int i = BuiltTextures.Size()-1; i>= 0; i--) { auto &buildinfo = BuiltTextures[i]; bool hasEmpty = false; @@ -969,7 +979,6 @@ void FMultipatchTextureBuilder::ResolveAllPatches() } BuiltTextures.Delete(i); - i--; donesomething = true; } } diff --git a/src/textures/texture.cpp b/src/textures/texture.cpp index 44203413a..118bbf5bc 100644 --- a/src/textures/texture.cpp +++ b/src/textures/texture.cpp @@ -138,7 +138,7 @@ FTexture::FTexture (const char *name, int lumpnum) : Scale(1,1), SourceLump(lumpnum), UseType(ETextureType::Any), bNoDecals(false), bNoRemap0(false), bWorldPanning(false), - bMasked(true), bAlphaTexture(false), bHasCanvas(false), bWarped(0), bComplex(false), bMultiPatch(false), bKeepAround(false), bFullNameTexture(false), + bMasked(true), bAlphaTexture(false), bHasCanvas(false), bWarped(0), bComplex(false), bMultiPatch(false), bFullNameTexture(false), Rotations(0xFFFF), SkyOffset(0), Width(0), Height(0) { bBrightmapChecked = false; diff --git a/src/textures/texturemanager.cpp b/src/textures/texturemanager.cpp index 799edb4aa..1c535f4df 100644 --- a/src/textures/texturemanager.cpp +++ b/src/textures/texturemanager.cpp @@ -469,16 +469,9 @@ void FTextureManager::ReplaceTexture (FTextureID picnum, FTexture *newtexture, b newtexture->Name = oldtexture->Name; newtexture->UseType = oldtexture->UseType; Textures[index].Texture = newtexture; - newtexture->id = oldtexture->id; - if (free && !oldtexture->bKeepAround) - { - delete oldtexture; - } - else - { - oldtexture->id.SetInvalid(); - } + oldtexture->Name = ""; + AddTexture(oldtexture); } //========================================================================== diff --git a/src/textures/textures.h b/src/textures/textures.h index a91be051b..52ed052db 100644 --- a/src/textures/textures.h +++ b/src/textures/textures.h @@ -407,7 +407,6 @@ protected: // fully composited before subjected to any kind of postprocessing instead of // doing it per patch. uint8_t bMultiPatch:2; // This is a multipatch texture (we really could use real type info for textures...) - uint8_t bKeepAround:1; // This texture was used as part of a multi-patch texture. Do not free it. uint8_t bFullNameTexture : 1; uint8_t bBrightmapChecked : 1; // Set to 1 if brightmap has been checked uint8_t bGlowing : 1; // Texture glow color diff --git a/src/zstring.h b/src/zstring.h index 67fef4a18..334fbcb1f 100644 --- a/src/zstring.h +++ b/src/zstring.h @@ -148,6 +148,11 @@ public: char *LockBuffer(); // Obtain write access to the character buffer void UnlockBuffer(); // Allow shared access to the character buffer + void Swap(FString &other) + { + std::swap(Chars, other.Chars); + } + operator const char *() const { return Chars; } const char *GetChars() const { return Chars; } diff --git a/wadsrc/static/zscript/inventory/ammo.txt b/wadsrc/static/zscript/inventory/ammo.txt index b08f39601..cc97611e0 100644 --- a/wadsrc/static/zscript/inventory/ammo.txt +++ b/wadsrc/static/zscript/inventory/ammo.txt @@ -66,7 +66,7 @@ class Ammo : Inventory // //=========================================================================== - Class GetParentAmmo () + virtual Class GetParentAmmo () { class type = GetClass();